[
  {
    "path": ".codeclimate.yml",
    "content": "vesion: '2'\n\nexclude_patterns:\n  # Ignore dot directories.\n  - .changelogs/\n  - .config/\n  - .github/\n  - .source/\n  - .wordpress-org\n  - ._private/\n  # Misc non-code directories.\n  - dist/\n  - docs/\n  - gulpfile.js/\n  - gulpfile.js\n  - i18n/\n  - languages/\n  - libraries/\n  - tests/\n  - tmp/\n  # Included vendor packages.\n  - '**/vendor/'\n  - '**/node_modules/'\n  # Minified scripts.\n  - '**/*.min.js'\n  - '**/index.php'\n  - '**/*.asset.php'\n  - assets/js/llms.js\n  - assets/js/llms-admin-addons.js\n  - assets/js/llms-builder.js\n  - assets/js/llms-components.js\n  - assets/js/llms-icons.js\n  - assets/js/llms-metaboxes.js\n  - assets/js/llms-utils.js\n"
  },
  {
    "path": ".cursor/BUGBOT.md",
    "content": "Do not review pull requests authored by dependabot[bot] or any automated dependency update bots.\n"
  },
  {
    "path": ".editorconfig",
    "content": "###\n#\n# This file is deployed into this repository via the \"Sync Organization Files\" workflow.\n#\n# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made\n# to the source file.\n#\n# @see Sync workflow {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflows/workflow-sync.yml}\n# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/.editorconfig}\n#\n###\n\n# This file is for unifying the coding style for different editors and IDEs\n# editorconfig.org\n\n# WordPress Coding Standards\n# https://developer.wordpress.org/coding-standards/wordpress-coding-standards/\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\ntab_width = 4\nindent_style = tab\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.{md,txt}]\ninsert_final_newline = false\ntrim_trailing_whitespace = false\n\n[*.{md,json,yml,yml.template}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "/**\n * ESlint config\n *\n * @package LifterLMS/Scripts/Dev\n *\n * @since Unknown\n * @version Unknown\n */\n\nconst config = require( '@lifterlms/scripts/config/.eslintrc.js' );\n\nmodule.exports = config;\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @brianhogg\n\n# Full Site Editing.\nincludes/class-llms-block-templates.php @ideadude\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "Contributing to LifterLMS\n=========================\n\nWe welcome and encourage contributions from the community. If you'd like to contribute to LifterLMS there are a few ways to do so. Here's our guidelines for contributions:\n\n*Please Note GitHub is for bug reports and contributions only! If you have a support question or a request for a customization this is not the right place to post it. Please refer to [LifterLMS Support](https://lifterlms.com/my-account/my-tickets) or the [community forums](https://wordpress.org/support/plugin/lifterlms). If you're looking for help customizing LifterLMS, please consider hiring a [LifterLMS Expert](https://lifterlms.com/docs/do-you-have-any-recommended-developers-who-can-modifycustomize-lifterlms/).*\n\n\n### Ways to Contribute\n\n+ [Submit bug and issues reports](#reporting-a-bug-or-issue)\n+ [Contribute new features](#contributing-new-features)\n+ [Contribute new code or bug fixes / patches](#contributing-code)\n+ [Translate and localize LifterLMS](#contribute-translations)\n\n\n### Reporting a Bug or Issue\n\nBugs and issues can be reported at [https://github.com/gocodebox/lifterlms/issues/new/choose](https://github.com/gocodebox/lifterlms/issues/new).\n\nBefore reporting a bug, [search existing issues](https://github.com/gocodebox/lifterlms/issues) and ensure you're not creating a duplicate. If the issue already exists you can add your information to the existing report.\n\nAlso check our [known issues and conflicts](https://lifterlms.com/doc-category/lifterlms/known-conflicts/) for possible resolutions.\n\n### Contributing New Features\n\nWhen contributing new features please communicate with us to ensure this is a feature we're interested in having added to LifterLMS before you start coding it.\n\nFirst check if we already have a feature request or proposal for the feature you're interested in developing. Take a look at our existing feature requests here in [GitHub](https://github.com/gocodebox/lifterlms/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22type%3A+feature+request%22) and on our [Feature Request voting board](https://trello.com/b/egC72ZZS/lifterlms-road-map-and-feature-voting).\n\nIf you can't find an existing feature request you should propose it by opening a new [feature request issue](https://github.com/gocodebox/lifterlms/issues/new?template=Feature_Request.md). In the issue we'll discuss your feature  before you start working on it.\n\nLifterLMS is a project that services a great many users. A feature which is attractive to a small number of users may create confusion for other users. These features may be better offered as a feature plugin instead of code in the core. In this scenario we'd be happy to help advise you on how to best develop and launch your feature as a plugin on WordPress.org! We'll even help market your add-on after you launch.\n\n### Contributing Code\n\n+ Fork the repository on GitHub.\n+ [Install LifterLMS for development](../docs/installing.md).\n+ Create a new branch from the 'trunk' branch.\n+ Make the changes to your forked repository.\n+ Ensure you stick to our [coding standards](https://github.com/gocodebox/lifterlms/blob/trunk/docs/coding-standards.md) and have properly documented new and updated functions, methods, actions, and filters following our [documentation standards](https://github.com/gocodebox/lifterlms/blob/trunk/docs/documentation-standards.md).\n+ Run PHPCS and ensure the output has no errors. We **will** reject pull requests if they fail codesniffing.\n+ Ensure new code doesn't break existing tests and add new code should aim to have 100% code coverage. See the [testing guide](https://github.com/gocodebox/lifterlms/blob/trunk/tests/phpunit/README.md) to get started with testing and let us know if you want help writing tests, we're happy to help!\n+ When making changes to (S)CSS and Javascript files, you should only modify the source files. The compiled and minified files *should not be committed* or included in your PR.\n+ When committing, reference your issue (if present) and include a note about the fix. Use [GitHub auto-references](https://help.github.com/en/articles/autolinked-references-and-urls).\n+ Push the changes to your fork\n+ Submit a pull request to the 'dev' branch of the LifterLMS repo.\n+ We'll review all pull requests, and make suggestions and changes if necessary. We're newly open source and supporting users and customers and our own internal pull requests and releases will take priority over pull requests from the community. Please be patient!\n\n\n### Contribute Translations\n\nAll translations to LifterLMS can be made via our GlotPress project at [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/lifterlms).\n\nAnyone can contribute translations. All you need is to login to your wordpress.org account. If you have questions about how to submit translations please refer to the [Translator's Handbook](https://make.wordpress.org/polyglots/handbook/).\n\nWe're always seeking Translation Editors who can manage and approve translations for their locale. If you're interested in becoming a translation editor for your locale please [review the documentation about translations here](https://lifterlms.com/docs/how-can-i-contribute-translations-to-lifterlms/).\n\n\n### Need Help Getting Started as a Contributor?\n\nA number of resources are available for first time contributors:\n\n+ Join our [LifterLMS Community Slack Channel](https://lifterlms.com/slack) and hop into the `#developers` channel. Our core contributors and maintainers are there to help out and answer questions.\n+ Check out the [LifterLMS Community Events Calendar](https://lifterlms.com/community-events/) for opportunities to interact with other contributors.\n+ Check out [this tutorial](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github) on how to submit pull requests on GitHub.\n+ Grab an issue marked tagged as a [`good first issue`](https://github.com/gocodebox/lifterlms/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug_Report.md",
    "content": "---\nname: Bug Report\nabout: Report a bug or issue\n\n---\n\n### Reproduction Steps\n\n+ Include clear and detailed step by step instructions on how the issue can be reliably reproduced\n+ Include screenshots where applicable\n+ Record a video if possible (if you post a video please still include a text version of your recreation steps!)\n\n\n### Expected Behavior\n\n+ Include a concise description of what you expected to happen (but didn't)\n\n\n### Actual Behavior\n\n+ Include a concise description of what actually happens (but isn't supposed to)\n\n\n### Error Messages / Logs\n\n+ Include any relevant error messages or log files\n```\n<!-- Paste error logs / backtraces below this line -->\n\n```\n\n### System and Environment Information\n\n<details>\n<summary>System Report</summary>\n\n<!-- Paste your System Report between the three backticks below this line -->\n```\n\n\n```\n\n</details>\n\n\nThis issue has be recreated:\n+ [ ] Locally\n+ [ ] On a staging site\n+ [ ] On a production website\n+ [ ] With only LifterLMS and a default theme\n\n### Browser, Device, and Operating System Information\n\n+ Browser name and version\n+ Operating System name and version\n+ Device name and version (if applicable)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Feature_Request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea or new feature for LifterLMS\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Question.md",
    "content": "---\nname: Question\nabout: Questions or 'how to' about LifterLMS\n\n---\n\nRemember that GitHub is NOT a support form! If you require user support with LifterLMS you will have more success in one of the following places:\n\n- Support Forums: https://wordpress.org/support/plugin/lifterlms\n- Official Support Tickets: https://lifterlms.com/my-account/my-tickets\n- LifterLMS Community Slack Channel: https://lifterlms.com/slack\n\nYou may also wish to peruse our documentation at https://lifterlms.com/docs\n\nIf none of these places seem appropriate ask away here.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\nContributors:\nPrior to opening a pull request, please review our contributing guidelines at https://github.com/gocodebox/lifterlms/blob/trunk/.github/CONTRIBUTING.md\n-->\n\n## Description\n<!-- Please describe what you have changed or added -->\n\nFixes #<!-- insert the related issue number here -->\n\n## How has this been tested?\n<!-- Please describe in detail how you tested your changes. -->\n<!-- Include details of your testing environment, tests ran to see how -->\n<!-- your change affects other areas of the code, etc. -->\n\n## Screenshots <!-- if applicable -->\n\n## Types of changes\n<!-- What types of changes does your code introduce?  -->\n<!-- Bug fix (non-breaking change which fixes an issue) -->\n<!-- New feature (non-breaking change which adds functionality) -->\n<!-- Breaking change (fix or feature that would cause existing functionality to not work as expected) -->\n\n## Checklist:\n- [ ] This PR requires and contains at least one changelog file. <!-- To create a changelog yml file: `npm run dev changelog add -- -i` and follow the prompt. See also: https://github.com/gocodebox/lifterlms/blob/trunk/packages/dev/README.md#changelog-add -->\n- [ ] My code has been tested.\n- [ ] My code passes all existing automated tests. <!-- Check code: `composer run-script tests-run`, Guidelines: https://github.com/gocodebox/lifterlms/blob/trunk/tests/README.md -->\n- [ ] My code follows the LifterLMS Coding & Documentation Standards. <!-- Check code: `composer run-script check-cs-errors`, Guidelines: https://github.com/gocodebox/lifterlms/blob/trunk/docs/coding-standards.md and https://github.com/gocodebox/lifterlms/blob/trunk/docs/documentation-standards.md -->\n\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "Security Policy\n---------------\n\n## Supported Versions\n\nThe current minor version (currently LifterLMS 7.8.x) is the only supported branch of LifterLMS. If you're using an unsupported version of LifterLMS we strongly recommend you upgrade to the latest version as soon as possible.\n\n## Reporting a Vulnerability\n\nThe LifterLMS team takes security issues and vulnerabilities very seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.\n\nTo report a vulnerability, please see our guidelines at https://lifterlms.com/security/\n"
  },
  {
    "path": ".github/workflow-matrix.yml",
    "content": "###\n#\n# Custom workflow matrix configurations\n#\n# @link https://github.com/gocodebox/.github/tree/trunk/.github/actions/setup-matrix\n#\n###\nTest PHPUnit:\n  __delete:\n    # Remove the LLMS Nightly job (intended for add-ons).\n    - include[1]\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: \"CodeQL\"\n\non:\n  pull_request:\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        # Override automatic language detection by changing the below list\n        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']\n        language: ['javascript']\n        # Learn more...\n        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n      with:\n        # We must fetch at least the immediate parents so that if this is\n        # a pull request then we can checkout the head.\n        fetch-depth: 2\n\n    # If this run was triggered by a pull request event, then checkout\n    # the head of the pull request instead of the merge commit.\n    - run: git checkout HEAD^2\n      if: ${{ github.event_name == 'pull_request' }}\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v1\n      with:\n         languages: ${{ matrix.language }}\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v1\n"
  },
  {
    "path": ".github/workflows/contributors.yml",
    "content": "name: Contributors\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - trunk\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }}\n  cancel-in-progress: true\n\njobs:\n\n  build:\n    name: Update contributors\n    runs-on: ubuntu-latest\n\n    steps:\n\n      - name: Checkout\n        uses: actions/checkout@v2\n        with:\n          token: ${{ secrets.ORG_WORKFLOWS }}\n\n      - name: Setup Node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '16'\n          cache: 'npm'\n\n      - name: Install dependencies\n        run: npm install contributor-faces\n\n      - name: Update README.md\n        run: ./node_modules/.bin/contributor-faces -e '*\\[bot\\]' -l 100\n\n      - name: Commit Updates\n        uses: stefanzweifel/git-auto-commit-action@v4\n        with:\n          commit_message: Update contributors list\n          branch: trunk\n          file_pattern: README.md\n          commit_user_name: contributors-workflow[bot]\n          commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com\n"
  },
  {
    "path": ".github/workflows/keep-alive.yml",
    "content": "###\n#\n# This workflow file is deployed into this repository via the \"Sync Organization Files\" workflow.\n#\n# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made\n# to the source file.\n#\n# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml}\n# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/keep-alive.yml}\n#\n# Keep Repo Alive\n#\n# This workflow ensures that cronjob workflows are not automatically disabled after 60 days of repo inactivity.\n# The workflow will automatically add an empty commit if the repo's latest commitwas made more than 50 days ago. This empty commit will prevent GitHub from automatically disabling this (and other)\n# cronjob actions in the repo.\n#\n###\nname: Keep Repo Alive\n\non:\n  # Once daily at 00:00 UTC.\n  schedule:\n    - cron: '0 0 * * *'\n\njobs:\n\n  keep-alive:\n    name: Keep Repo Alive\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          token: ${{ secrets.ORG_WORKFLOWS }}\n          fetch-depth: 0\n      - name: Add keep-alive commit\n        run: |\n          LAST_COMMIT_TIME=$( git --no-pager log -1 --format=%ct )\n          NOW=$( date +%s )\n          DIFF_IN_DAYS=$(( ( NOW - LAST_COMMIT_TIME ) / 86400 ))\n\n          echo \"Days since last commit: $DIFF_IN_DAYS\"\n\n          if (( $DIFF_IN_DAYS > 50 )); then\n            echo \"Adding a keep-alive commit.\"\n          \n            git config --global user.name \"keepalive[bot]\"\n            git config --global user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n\n            git commit --allow-empty -m \"Automated commit from the Keep Repo Alive workflow\"\n            git push origin HEAD\n          else\n            echo \"No keep-alive commit required.\"\n          fi"
  },
  {
    "path": ".github/workflows/lint-js.yml",
    "content": "###\n#\n# This workflow file is deployed into this repository via the \"Sync Organization Files\" workflow\n#\n# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made\n# to the source file.\n#\n# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml}\n# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/lint-js.yml}\n#\n###\nname: Lint JavaScript\n\non:\n  workflow_dispatch:\n  pull_request:\n  # Once daily at 00:00 UTC.\n  # schedule:\n  #  - cron: '0 0 * * *'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }}\n  cancel-in-progress: true\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v2\n\n    - name: Setup Node\n      uses: actions/setup-node@v1\n      with:\n        node-version: '16'\n        cache: 'npm'\n\n    - name: Install npm dependencies\n      run: npm ci\n\n    - name: Run linter\n      continue-on-error: true\n      run: npm run lint:js\n"
  },
  {
    "path": ".github/workflows/ossar-analysis.yml",
    "content": "# This workflow integrates a collection of open source static analysis tools\n# with GitHub code scanning. For documentation, or to provide feedback, visit\n# https://github.com/github/ossar-action\nname: OSSAR\n\non:\n  pull_request:\n\njobs:\n  OSSAR-Scan:\n    runs-on: windows-latest\n    steps:\n    - uses: actions/checkout@v2\n    \n    - name: Run OSSAR\n      uses: github/ossar-action@v1\n      id: ossar\n      \n    - name: Upload results to Security tab\n      uses: github/codeql-action/upload-sarif@v1\n      with:\n        sarif_file: ${{ steps.ossar.outputs.sarifFile }}\n"
  },
  {
    "path": ".github/workflows/project-automation.yml",
    "content": "name: Issue & PR Automation\n\non:\n  issues:\n    types:\n      - opened\n      - reopened\n  pull_request_target:\n    types:\n      - opened\n      - reopened\n      - review_requested\n\nenv:\n  PRIMARY_CODEOWNER: '@ideadude'\n  PROJECT_ORG: gocodebox\n  PROJECT_ID: 18\n\njobs:\n\n  #######################################\n  # Add issue to the Development project.\n  #######################################\n  issue-to-project:\n    name: Move Issue to Project Board\n    runs-on: ubuntu-latest\n    if: ( 'issues' == github.event_name && ( 'opened' == github.event.action || 'reopened' == github.event.action ) )\n    steps:\n      - name: Add Issue to Project\n        uses: leonsteinhaeuser/project-beta-automations@v2.1.0\n        with:\n          gh_token: ${{ secrets.ORG_WORKFLOWS }}\n          organization: ${{ env.PROJECT_ORG }}\n          project_id: ${{ env.PROJECT_ID }}\n          resource_node_id: ${{ github.event.issue.node_id }}\n          status_value: \"Awaiting Triage\"\n\n      # - uses: hmarr/debug-action@v2\n\n  ####################################\n  # Assign to the project's CODEOWNER.\n  ####################################\n  issue-assig:\n    name: Assign Issue to the Primary CODEOWNER\n    runs-on: ubuntu-latest\n    if: ( 'issues' == github.event_name && ( 'opened' == github.event.action || 'reopened' == github.event.action ) && ( null == github.event.issue.assignee ) )\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n        \n      - name: Check CODEOWNERS file existence\n        id: codeowners_file_exists\n        uses: andstor/file-existence-action@v2\n        with:\n          files: .github/CODEOWNERS\n\n      - name: Parse CODEOWNERS file\n        id: codeowner\n        if: steps.codeowners_file_exists.outputs.files_exists == 'true'\n        uses: SvanBoxel/codeowners-action@v1\n        with:\n          path: .github/CODEOWNERS\n      \n      - name: Update PRIMARY_CODEOWNER env var\n        if: steps.codeowners_file_exists.outputs.files_exists == 'true'\n        run: |\n          echo PRIMARY_CODEOWNER=$( echo '${{ steps.codeowner.outputs.codeowners }}' | jq -r '.\"*\"[0]' ) >> $GITHUB_ENV   \n\n      - name: Strip @ from username\n        run: |\n          echo \"PRIMARY_CODEOWNER=${PRIMARY_CODEOWNER#?}\" >> $GITHUB_ENV\n\n      - name: Assign issue\n        uses: pozil/auto-assign-issue@v1\n        with:\n          repo-token: ${{ secrets.ORG_WORKFLOWS }}\n          assignees: ${{ env.PRIMARY_CODEOWNER }}\n\n  #####################################\n  # Add PRs to the Development project.\n  #####################################\n  pr-to-board:\n    name: Move Pull Request to the Project Board\n    runs-on: ubuntu-latest\n    if: ( 'pull_request_target' == github.event_name && ( 'opened' == github.event.action || 'reopened' == github.event.action || 'review_requested' == github.event.action ) )\n    steps:\n      - name: Mark PR as Awaiting Review\n        uses: leonsteinhaeuser/project-beta-automations@v2.1.0\n        with:\n          gh_token: ${{ secrets.ORG_WORKFLOWS }}\n          organization: ${{ env.PROJECT_ORG }}\n          project_id: ${{ env.PROJECT_ID }}\n          resource_node_id: ${{ github.event.pull_request.node_id }}\n          status_value: \"Awaiting Review\"\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Release Publication and Distribution\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - trunk\n    paths:\n      - 'CHANGELOG.md'\n\njobs:\n\n  check-secrets:\n    name: \"Check for required secrets\"\n    runs-on: ubuntu-latest\n    outputs:\n      has-secrets: ${{ steps.check-secrets.outputs.has-secrets }}\n    steps:\n      - name: Test secrets\n        id: check-secrets\n        run: |\n          if [ ! -z \"${{ secrets.LLMS_COM_API_URL }}\" ] && [ ! -z \"${{ secrets.LLMS_COM_API_KEY }}\" ]; then\n            echo \"::set-output name=has-secrets::true\"\n          fi\n\n  update-metadata:\n    name: \"Update product metadata at LifterLMS.com\"\n    runs-on: ubuntu-latest\n\n    needs: check-secrets\n    if: ${{ 'true' == needs.check-secrets.outputs.has-secrets }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Setup Node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '16'\n          cache: 'npm'\n\n      - name: Install Node dependencies\n        run: npm i @lifterlms/dev\n\n      - name: Get metadata\n        run: |\n          __LLMS_METADATA=$( ./node_modules/.bin/llms-dev meta parse -f json )\n          echo __LLMS_PKG_VERSION=$( echo $__LLMS_METADATA | jq --raw-output '.[\"Version\"]' ) >> $GITHUB_ENV\n          echo __LLMS_WP_VERSION=$( echo $__LLMS_METADATA | jq --raw-output '.[\"Requires at least\"]' ) >> $GITHUB_ENV\n          echo __LLMS_PHP_VERSION=$( echo $__LLMS_METADATA | jq --raw-output '.[\"Requires PHP\"]' ) >> $GITHUB_ENV\n          echo __LLMS_LLMS_VERSION=$( echo $__LLMS_METADATA | jq --raw-output '.[\"LLMS Requires at least\"]' ) >> $GITHUB_ENV\n\n      - name: Test metadata\n        run: |\n          echo \"Package version: $__LLMS_PKG_VERSION\"\n          echo \"Min WP Version: $__LLMS_WP_VERSION\"\n          echo \"Min PHP Version: $__LLMS_PHP_VERSION\"\n          echo \"Min LLMS Version: $__LLMS_LLMS_VERSION\"\n\n      - name: Update metadata\n        run: |\n          curl --location --request PATCH \"${{ secrets.LLMS_COM_API_URL }}v3/products/${{ github.event.repository.name }}\" \\\n          --header \"X-API-KEY: ${{ secrets.LLMS_COM_API_KEY }}\" \\\n          --header 'Content-Type: application/x-www-form-urlencoded' \\\n          --data-urlencode \"version=$__LLMS_PKG_VERSION\" \\\n          --data-urlencode \"wp_version=$__LLMS_WP_VERSION\" \\\n          --data-urlencode \"php_version=$__LLMS_PHP_VERSION\" \\\n          --data-urlencode \"llms_version=$__LLMS_LLMS_VERSION\"\n"
  },
  {
    "path": ".github/workflows/sync-branches.yml",
    "content": "###\n#\n# This workflow file is deployed into this repository via the \"Sync Organization Files\" workflow\n#\n# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made\n# to the source file.\n#\n# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml}\n# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/sync-branches.yml}\n#\n###\nname: Sync Branches\non:\n  push:\n    branches:\n      - trunk\n  workflow_dispatch:\n\njobs:\n  sync:\n    name: trunk -> dev\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          token: ${{ secrets.ORG_WORKFLOWS }}\n          fetch-depth: 0\n      - name: Perform sync\n        run: |\n          git config --global user.name \"branch-sync[bot]\"\n          git config --global user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n          git checkout dev\n          git pull origin trunk --no-ff\n          git status\n          git push origin dev\n"
  },
  {
    "path": ".github/workflows/test-e2e.yml",
    "content": "###\n#\n# This workflow file is deployed into this repository via the \"Sync Organization Files\" workflow\n#\n# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made\n# to the source file.\n#\n# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml}\n# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/test-e2e.yml}\n#\n###\nname: Test E2E\n\non:\n  workflow_dispatch:\n  pull_request:\n  # Once daily at 00:00 UTC.\n#  schedule:\n#    - cron: '0 0 * * *'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }}\n  cancel-in-progress: true\n\njobs:\n  ###\n  #\n  # Setup the test matrix.\n  #\n  ###\n  set-matrix:\n    name: Setup Matrix\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.setup.outputs.matrix }}\n    steps:\n      - uses: actions/checkout@v2\n      - id: setup\n        uses: gocodebox/.github/.github/actions/setup-matrix@trunk\n\n  ###\n  #\n  # Run tests.\n  #\n  ###\n  test:\n    name: \"WP ${{ matrix.WP }}\"\n    needs: set-matrix\n    runs-on: ubuntu-latest\n    continue-on-error: ${{ matrix.allow-failure }}\n\n    strategy:\n      fail-fast: false\n      matrix: ${{ fromJSON( needs.set-matrix.outputs.matrix ) }}\n\n    steps:\n\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Setup Environment\n        uses: gocodebox/.github/.github/actions/setup-e2e@trunk\n        with:\n          wp-version: ${{ matrix.WP }}\n          docker-user: ${{ secrets.DOCKER_USERNAME }}\n          docker-pass: ${{ secrets.DOCKER_PASSWORD }}\n          node-version: '16'\n\n      # create a folder for the screenshots, this folder will be uploaded as artifact\n      - run: mkdir screenshots\n\n      - name: Run test suite\n        run: npm run test -- --verbose\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: error-artifacts-wp-${{ matrix.WP }}\n          path: |\n            tmp/artifacts\n            screenshots\n\n  ###\n  #\n  # Check the status of the entire test matrix.\n  #\n  # This will succeed if all jobs from the `test` job's matrix succeed. It allows jobs marked with `allow-failure`\n  # to fail.\n  #\n  # This job can be used as a single status check for branch protection rules. Without this\n  # we would need to require every job in the above build matrix.\n  #\n  ###\n  status:\n    name: Test E2E Status\n    runs-on: ubuntu-latest\n    if: always()\n    needs: test\n    steps:\n      - name: Check overall matrix status\n        if: ${{ 'success' != needs.test.result }}\n        run: exit 1\n"
  },
  {
    "path": ".github/workflows/test-js-unit.yml",
    "content": "name: Test JS Unit\n\non:\n  workflow_dispatch:\n  pull_request:\n    paths:\n        - src/js/**\n  # Once daily at 00:00 UTC.\n  # schedule:\n  #  - cron: '0 0 * * *'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }}\n  cancel-in-progress: true\n\njobs:\n  test:\n    name: \"Run JS Unit Tests\"\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Setup Node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '16'\n          cache: 'npm'\n\n      - name: Install Node dependencies\n        run: npm ci\n\n      - name: Run test suite\n        run: npm run test:unit\n"
  },
  {
    "path": ".github/workflows/test-phpunit.yml",
    "content": "###\n#\n# This workflow file is deployed into this repository via the \"Sync Organization Files\" workflow\n#\n# Direct edits to this file are at risk of being overwritten by the next sync. All edits should be made\n# to the source file.\n#\n# @see Sync workflow {@link https://github.com/gocodebox/.github/actions/workflows/workflow-sync.yml}\n# @see Workflow template {@link https://github.com/gocodebox/.github/blob/trunk/.github/workflow-templates/test-phpunit.yml}\n#\n###\nname: Test PHPUnit\n\non:\n  workflow_dispatch:\n    inputs:\n      cache-suffix:\n        description: Cache suffix\n        type: string\n  pull_request:\n  # Once daily at 01:00 UTC.\n  # schedule:\n  #  - cron: '0 1 * * *'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ 'pull_request' == github.event_name && github.head_ref || github.sha }}\n  cancel-in-progress: true\n\njobs:\n\n  ###\n  #\n  # Setup the test matrix.\n  #\n  ###\n  set-matrix:\n    name: Setup Matrix\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.setup.outputs.matrix }}\n    steps:\n      - uses: actions/checkout@v2\n      - id: setup\n        uses: gocodebox/.github/.github/actions/setup-matrix@trunk\n\n  ###\n  #\n  # Run tests.\n  #\n  ###\n  test:\n    name: WP ${{ matrix.WP }} on PHP ${{ matrix.PHP }}${{ matrix.name-append }}\n\n    needs: set-matrix\n    runs-on: ubuntu-latest\n    continue-on-error: ${{ matrix.allow-failure }}\n\n    strategy:\n      fail-fast: false\n      matrix: ${{ fromJSON( needs.set-matrix.outputs.matrix ) }}\n\n    steps:\n\n      - name: Checkout\n        uses: actions/checkout@v2\n\n      - name: Setup Environment\n        uses: gocodebox/.github/.github/actions/setup-phpunit@trunk\n        with:\n          php-version: ${{ matrix.PHP }}\n          wp-version: ${{ matrix.WP }}\n          llms-branch: ${{ matrix.LLMS }}\n          env-file: \".github/.env.test-phpunit\"\n          deploy-key: ${{ secrets.LLMS_DEPLOY_KEY }}\n          secrets: ${{ toJSON( secrets ) }}\n          cache-suffix: ${{ inputs.cache-suffix }}\n\n      - name: Setup Node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '16'\n\n      - name: Install NPM Dependencies\n        run: npm ci && npm run build\n\n      - name: Run Tests\n        run: composer run tests\n\n  ###\n  #\n  # Check the status of the entire test matrix.\n  #\n  # This will succeed if all jobs from the `test` job's matrix succeed. It allows jobs marked with `allow-failure`\n  # to fail.\n  #\n  # This job can be used as a single status check for branch protection rules. Without this\n  # we would need to require every job in the above build matrix.\n  #\n  ###\n  status:\n    name: Test PHPUnit Status\n    runs-on: ubuntu-latest\n    if: ${{ always() }}\n    needs: test\n    steps:\n      - name: Check overall matrix status\n        if: ${{ 'success' != needs.test.result }}\n        run: exit 1\n"
  },
  {
    "path": ".gitignore",
    "content": "# Package managers.\nnode_modules/\n/vendor/\n\n# Lock file intentionally excluded to allow easier testing against multiple php versions\n# This follows the precedent put forth by the WordPress core {@link https://github.com/WordPress/wordpress-develop/commit/0e442c4615bdcdc1c2e4f8db6f88f57e6859c2ff}\ncomposer.lock\n\n# Ignore built files\n/readme.txt\nassets/css/admin.css\nassets/css/admin.min.css\nassets/css/admin-rtl.css\nassets/css/admin-rtl.min.css\nassets/css/admin-wizard-rtl.css\nassets/css/admin-wizard-rtl.min.css\nassets/css/admin-wizard.css\nassets/css/admin-wizard.min.css\nassets/css/builder.css\nassets/css/builder.min.css\nassets/css/builder-rtl.css\nassets/css/builder-rtl.min.css\nassets/css/bricks-editor-rtl.css\nassets/css/admin-importer*\nassets/css/editor*\nassets/css/certificates*\nassets/css/lifterlms-rtl.css\nassets/css/lifterlms-rtl.min.css\nassets/css/lifterlms.css\nassets/css/lifterlms.min.css\nassets/css/llms-admin-addons.css\nassets/css/llms-admin-addons-rtl.css\nassets/css/llms-focus-mode*\n\nassets/js/llms-builder.js\nassets/js/llms-builder.min.js\nassets/js/llms-admin-elementor-editor.js\nassets/js/llms-admin-elementor-editor.asset.php\nassets/js/llms-admin-certificate-editor.asset.php\nassets/js/llms-admin-certificate-editor.js\nassets/js/llms-admin-media-protection-block-protect.asset.php\nassets/js/llms-admin-media-protection-block-protect.js\nassets/js/llms-components.asset.php\nassets/js/llms-components.js\nassets/js/llms-admin-addons.js\nassets/js/llms-admin-award-certificate.js\nassets/js/llms-icons.js\nassets/js/llms-quill-wordcount.js\nassets/js/llms-spinner.js\nassets/js/llms-utils.js\nassets/js/llms.js\nassets/js/*.min.js\nassets/js/*.asset.php\nassets/js/llms-metaboxes.js\nblocks/certificate-title/*\nblocks/my-account/*\nblocks/pricing-table/*\nblocks/access-plan-button/*\nblocks/checkout/*\nblocks/course-author/*\nblocks/course-continue/*\nblocks/course-meta-info/*\nblocks/course-outline/*\nblocks/course-prerequisites/*\nblocks/course-reviews/*\nblocks/course-syllabus/*\nblocks/courses/*\nblocks/login/*\nblocks/memberships/*\nblocks/my-achievements/*\nblocks/navigation-link/*\nblocks/registration/*\n\nincludes/class.llms.l10n.frontend.php\nlanguages/lifterlms.pot\n\n# Ignore composer-installed libs.\n/libraries/*\n!/libraries/index.php\n!/libraries/README.md\n\n# Exclude maps.\n/blocks/**/*.js.map\n/assets/css/*.map\n/assets/js/*.map\n/assets/maps/*\n\n# Misc.\n*.log\n.DS_Store\n\n# Release distribution directory.\n/dist/\n/tmp/\n\n# Non-distributable configs.\nphpunit.xml\n.llmsenv\n"
  },
  {
    "path": ".llmsconfig",
    "content": "{\n\t\"build\": {\n\t\t\"custom\": [ \"js-additional\", \"js-builder\" ]\n\t},\n\t\"docs\": {\n\t\t\"package\": \"LifterLMS\"\n\t},\n\t\"pot\": {\n\t\t\"bugReport\": \"https://github.com/gocodebox/lifterlms/issues\",\n\t\t\"domain\": \"lifterlms\",\n\t\t\"dest\": \"languages/\",\n\t\t\"jsClassname\": \"LLMS_L10n_JS\",\n\t\t\"jsFilename\": \"class.llms.l10n.frontend.php\",\n\t\t\"jsSince\": \"3.17.8\",\n\t\t\"jsSrc\": [ \"assets/js/**/*.js\", \"!assets/js/**/*.min.js\", \"!assets/js/**/*.js.map\" ],\n\t\t\"lastTranslator\": \"Thomas Patrick Levy <help@lifterlms.com>\",\n\t\t\"team\": \"LifterLMS <help@lifterlms.com>\",\n\t\t\"package\": \"lifterlms\",\n\t\t\"phpSrc\": [\n\t\t\t\"./*.php\", \"./**/*.php\",\n\t\t\t\"!vendor/*\", \"!vendor/**/*.php\", \"!tmp/**\", \"!tests/**\", \"!wordpress/**\",\n\t\t\t\"./vendor/lifterlms/lifterlms-blocks/*.php\", \"./vendor/lifterlms/lifterlms-blocks/**/*.php\",\n\t\t\t\"./vendor/lifterlms/lifterlms-rest/*.php\", \"./vendor/lifterlms/lifterlms-rest/**/*.php\"\n\t\t]\n\t},\n\t\"publish\": {\n\t\t\"title\": \"LifterLMS\",\n\t\t\"lifterlms\": {\n\t\t\t\"make\": {\n\t\t\t\t\"tags\": [ 6 ]\n\t\t\t},\n\t\t\t\"pot\": false\n\t\t}\n\t},\n\t\"scripts\": {\n\t\t\"src\": [\n\t\t\t\"assets/js/**/*.js\",\n\t\t\t\"!assets/js/llms-admin-addons.js\",\n\t\t\t\"!assets/js/llms-admin-award-certificate.js\",\n\t\t\t\"!assets/js/llms-admin-certificate-editor.js\",\n\t\t\t\"!assets/js/llms-admin-media-protection-block-protect.js\",\n\n\t\t\t\"!assets/js/**/*.min.js\",\n\t\t\t\"!assets/js/llms-builder*.js\",\n\t\t\t\"!assets/js/app/**/*.js\",\n\t\t\t\"!assets/js/builder/**/*.js\",\n\t\t\t\"!assets/js/partials/**/*.js\",\n\t\t\t\"!assets/js/private/**/*.js\",\n\n\n\t\t\t\"!assets/js/llms-components.js\",\n\t\t\t\"!assets/js/llms-icons.js\",\n\t\t\t\"!assets/js/llms-quill-wordcount.js\",\n\t\t\t\"!assets/js/llms-spinner.js\",\n\t\t\t\"!assets/js/llms-utils.js\"\n\t\t],\n\t\t\"dest\": \"assets/js/\"\n\t},\n\t\"watch\": {\n\t\t\"custom\": [ {\n\t\t\t\"glob\": [ \"assets/js/builder/**/*.js\", \"assets/js/private/**/*.js\", \"assets/js/app/*.js\" ],\n\t\t\t\"tasks\": [ \"js-additional\", \"js-builder\" ]\n\t\t} ]\n\t},\n\t\"zip\": {\n\t\t\"composer\": true,\n\t\t\"src\": {\n\t\t\t\"custom\": [\n\t\t\t\t\"!./**/CHANGELOG.md\",\n\t\t\t\t\"!./**/README.md\",\n\t\t\t\t\"!./_private/**\",\n\t\t\t\t\"!./_readme/**\",\n\t\t\t\t\"!./docs/**\",\n\t\t\t\t\"!./packages/**\",\n\t\t\t\t\"!./wordpress/**\",\n\t\t\t\t\"!lerna.json\",\n\t\t\t\t\"!babel.config.js\",\n\t\t\t\t\"!docker-compose.override.yml.template\"\n\t\t\t]\n\t\t}\n\t}\n}\n"
  },
  {
    "path": ".llmsdev.yml",
    "content": "pot:\n  dir: languages\n"
  },
  {
    "path": ".llmsdevrc",
    "content": "{\n\t\"readme\": {\n\t\t\"title\": \"LMS by LifterLMS - Online Course, Membership & Learning Management System Plugin for WordPress\",\n\t\t\"shortDescription\": \"LifterLMS is a powerful WordPress learning management system plugin that makes it easy to create, sell, and protect engaging online courses and training based membership websites.\",\n\t\t\"meta\": {\n\t\t\t\"Tags\": \"learning management system, LMS, membership, elearning, online courses, quizzes, sell courses, badges, gamification, learning, Lifter, LifterLMS\",\n\t\t\t\"Requires at least\": \"5.4\",\n\t\t\t\"Tested up to\": \"5.8\",\n\t\t\t\"Requires PHP\": \"7.3\"\n\t\t},\n\t\t\"changelog\": {\n\t\t\t\"link\": \"https://make.lifterlms.com/tag/lifterlms/\"\n\t\t},\n\t\t\"sections\": {\n\t\t\t\"Description\": \"file:./.wordpress-org/readme/description.md\",\n\t\t\t\"Installation\": \"file:./.wordpress-org/readme/installation.md\",\n\t\t\t\"Frequently Asked Questions\": \"file:./.wordpress-org/readme/faqs.md\",\n\t\t\t\"Screenshots\": \"file:./.wordpress-org/readme/screenshots.md\"\n\t\t}\n\t},\n\t\"i18n\": {\n\t\t\"dir\": \"./languages/\"\n\t}\n}\n"
  },
  {
    "path": ".llmsenv.dist",
    "content": "WORDPRESS_PORT=8080\nWORDPRESS_TITLE=LifterLMS Core e2e\n"
  },
  {
    "path": ".source/README.md",
    "content": "LifterLMS Source Images\n=======================\n\nThis directory contains source images (.svg, .ai, .psd, etc...) for any image assets used in the LifterLMS core.\n"
  },
  {
    "path": ".wordpress-org/README.md",
    "content": "WordPress.org Plugin Repository Content and Assets\n==================================================\n\nThis directory contains the text content and assets used on the LifterLMS plugin listing on [WordPress.org Plugin Repository](https://wordpress.org/plugins/lifterlms/). \n\n## README\n\nThe [readme](./readme) directory contains the markdown files representing the sections (and tabs) used on the listing.\n\nThese files are combined during a build step prior distribution and output as the [readme.txt](../readme.txt) file distributed with the LifterLMS plugin. Generally we do not ship updates for changes to the readme directory. These changes will be included in the next release which contains code changes.\n\nThe files are prepended with numbers to preserve their order when programmatically combined.\n\nThe command to build the readme file is `npm run dev readme`. See full documentation of the command in the [@lifterlms/dev package reference](https://github.com/gocodebox/lifterlms/tree/trunk/packages/dev#readme).\n\n### File Parts\n\n+ [01-header.md](./readme/01-header.md): The readme header containing the listing's display title, meta data, and a short description.\n+ [05-description.md](./readme/05-description.md): The main listing \"Details\" tab.\n+ [10-installation.md](./readme/10-installation.md): The contents of the \"Installation\" tab\n+ [15-faqs.md](./readme/15-faqs.md): A list of frequently asked questions. This is listed at the bottom of the \"Details\" tab on the listing page.\n+ [20-screenshots.md](./readme/15-faqs.md): An ordered list of screenshot captions. Each caption should correspond with a screenshot in the [assets directory](./assets). A screenshot with the filename `screenshot-5.png` corresponds to the item 5 in the screenshot list. \n+ [25-changelog.md](./readme/25-changelog.md): an auto-generated changelog containing the latest 10 changelog entries from the main [CHANGELOG.md](../CHANGELOG.md) file.\n\n### Merge Codes\n\nVarious merge codes are available for use in the readme file parts. See the [@lifterlms/dev package reference](https://github.com/gocodebox/lifterlms/tree/trunk/packages/dev#readme) for merge code documentation.\n\n## Assets\n\nThe [assets](./assets) directory contains the images used in the listing: banners, icons, and screenshots.\n\nSee also: [How Your Plugin Assets Work](https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/).\n\nAssets are manually synced to WordPress.org via SVN @thomasplevy and will be updated in conjunction with the next release following their update in this directory."
  },
  {
    "path": ".wordpress-org/readme/01-header.md",
    "content": "=== LifterLMS - WP LMS for eLearning, Online Courses, & Quizzes ===\nContributors: lifterlms, chrisbadgett, strangerstudios, kimannwall, d4z_c0nf, actuallyakash, codeboxllc, brianhogg\nDonate link: {{__PROJECT_URI__}}\nTags: lms, course, elearning, learning management system, quiz\nLicense: {{__LICENSE__}}\nLicense URI: {{__LICENSE_URI__}}\nRequires at least: {{__MIN_WP_VERSION__}}\nTested up to: {{__TESTED_WP_VERSION__}}\nRequires PHP: {{__MIN_PHP_VERSION__}}\nStable tag: {{__VERSION__}}\n\n{{__SHORT_DESCRIPTION__}}\n"
  },
  {
    "path": ".wordpress-org/readme/05-description.md",
    "content": "== Description ==\nLifterLMS is a secure easy-to-use WordPress LMS plugin packed with features to easily create & sell courses online.\n\nTurn your WordPress website into a professional eLearning platform with every customizable feature you could possibly need from your LMS.\n\n+ **Intuitive LMS Course Builder:** Create Courses, Sections, and Interactive Lessons with multimedia content.\n+ **Track Student Progress:** In-Depth Reporting, Create Timed or Open Quizzes, Drip Content, Add Prerequisites, Analyze Progress, and Award Certificates\n+ **Complete Ecommerce Platform:** Built-in Gateway Integration for Stripe and PayPal With Memberships and Subscriptions \n+ **Community and Social Learning:** Integrate a Community Forum or Discussion Area, Add Multiple Instructors, and Display Course Reviews\n\n[Explore All LMS Features](https://lifterlms.com/features/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n\nLifterLMS makes it easy to create, sell, and protect engaging online courses and training-based membership websites.\n\nhttps://www.youtube.com/watch?v=N72Zw2EBm4A\n\n### Integrate LifterLMS With Any Theme, Page Builder, & Block Editor\n\nLifterLMS works with any modern WordPress theme/FSE, the Block Editor (Gutenberg), and every popular WordPress page builder including Elementor, Beaver Builder, and Divi.\n\n**With over 10 years development,** our team is deeply engaged with the WordPress community. We encourage our integration partners to create the extensions you need most, like Affiliate WP, Monster Insights, WP Fusion, popular form plugins, GamiPress, Astra Pro, and more.\n\n\n### Open Source, Free Core Plugin\n\nLifterLMS gives back to the open-source community. The core LifterLMS plugin is a totally FREE forever LMS - no limits on your courses, memberships, enrollments, or earnings.\n\nWe believe in free, distributed learning for all. **LifterLMS exists to democratize education in the digital classroom.**\n\n### Premium Add-ons and Bundles\n\nGet to know our team and product. by signing up for a **[$1 temporary _30 Day_ website](https://lifterlms.com/try/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)**. You'll get instant access to a private demo site hosted on our servers pre-installed with:\n\n+ the core LifterLMS plugin, AND\n+ every premium LMS add-on\n\nSee why so many people start with or switch from another WordPress LMS or hosted platform to [LifterLMS](https://lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) for online courses, membership sites, and remote schools.\n\nhttps://www.youtube.com/watch?v=RnZflrWG5YQ\n\n### LifterLMS is Perfect For:\n\n**Builders**\nWe’re the favorite LMS plugin for WordPress developers, designers and IT pros who **build LMS websites and training portals** for clients, employers, and themselves\n\n**DIY**\nDo-it-yourself innovators love that LifterLMS helps them easily **create high-value online courses, coaching or training-based membership websites,** right on WordPress.\n\n**Switchers**\nHave you outgrown a hosted LMS platform or an incomplete WordPress LMS stack? Choose LifterLMS if you are looking for **more power, control, and better support**.\n\n### Meet The LifterLMS Team\n\n\nThe LifterLMS team is a **diverse group of talented course creators, developers, designers, marketers, and entrepreneurs**.\n\nBefore developing LifterLMS, we consulted and built custom WordPress-based online learning and membership sites for clients worldwide. LifterLMS was born through this deep hands-on experience building high-end, custom WordPress LMS websites from scratch.\n\nLearn more about [the people behind LifterLMS](https://lifterlms.com/about-us/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale#h-meet-the-team).\n\n### LifterLMS By The Numbers\n\n+ 8,377,042 Course Enrollments powered by LifterLMS\n+ 12,570,881 Course and lesson completions powered by LifterLMS\n+ 186,997 Achievement badges awarded by LifterLMS\n+ 310,728 Certificates awarded by LifterLMS\n+ Over 10,000 active installs of the WordPress LMS plugin\n+ [308 5-star reviews](https://wordpress.org/support/plugin/lifterlms/reviews/?filter=5)\n\n### [Features](https://lifterlms.com/features/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n\nStart with the free LMS plugin and [scale-up](https://lifterlms.com/pricing/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale) as you grow.\n\nhttps://www.youtube.com/watch?v=ZNwo5inRSdM\n\n**Make Money Teaching Online**\nSet up LifterLMS, activate built-in payments with [Stripe](https://lifterlms.com/product/stripe-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) or [PayPal](https://lifterlms.com/product/paypal-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale). That's all you need to get started.\n\nWhen you need more features and to expand your online learning business, we're here for you. We have several free and premium add-ons to help you create more value for your users, and scale your business revenue.\n\n+ Credit card payments\n+ One-time payments\n+ Recurring payments\n+ Payment plans\n+ Unlimited course and membership pricing models\n+ PayPal\n+ Subscriptions\n+ Checkout\n+ Free courses\n+ Course bundles\n+ Private coaching upsells\n+ Course and membership Coupons\n+ Bulk course and membership sales\n+ Affiliate ready\n+ Native course and membership sales pages\n+ Offline course and membership sales\n+ Customizable course and membership enrollment\n+ Country and currency\n+ E-commerce dashboard\n+ Credit card management\n+ Subscription switching\n+ Payment switching\n+ Native Zapier integration\n\n**Create Courses on Your WordPress LMS Website**\n\n\n+ Course multimedia lessons\n+ Course quizzes\n+ Quiz question banks\n+ Course builder\n+ Course cohorts\n+ Drip Content\n+ Course and lesson prerequisites\n+ Course tracks\n+ Course assignments\n+ Quiz time limits\n+ Student dashboard\n+ Student note-taking\n+ Multi-instructor courses\n+ Lesson downloads\n+ Course import & export\n+ Discussion areas\n+ Instructional design\n+ Forum integrations\n+ Graphics pack\n+ Course reviews\n+ Group enrollments for courses and memberships\n\n**Engage Your Students**\n\n\n+ Achievement badges\n+ Certificates\n+ Personalized email\n+ Social learning\n+ Private coaching\n+ Text messaging\n\n**Offer Memberships**\n\n+ Sitewide membership\n+ Course bundles\n+ Traditional memberships\n+ Automatic course enrollment\n+ Bulk course enrollment\n+ Content restrictions outside of a course\n+ Members-only payment plans\n+ Private group discussions\n+ Members-only forums\n\n**Integrate your WordPress LMS with the Tools You Need**\n\n+ Payment gateways\n+ Email marketing\n+ Forums\n+ Mobile friendly\n+ Use any theme or page builder\n+ Built for compatibility\n+ CRMs\n+ E-learning authoring tools\n+ Tin Can API (xAPI)\n\n**Secure and Protect Your Content**\n\n+ Course protection\n+ User account management and registration\n+ Members only content\n+ Course only content\n+ Restricted access\n+ Password management\n+ Self-hosted\n\n**Own and Manage Your WordPress LMS Platform**\n\n+ Detailed course, membership, ecommerce, and student reporting\n+ Course gradebook\n+ Email notifications\n+ Bulk course and membership enrollments\n+ Student management\n+ Course and membership access management\n+ Web design management\n+ Branding & typography\n+ WordPress LMS User Roles\n+ Security\n+ Require terms\n+ Scalable\n+ Layout\n+ Testing tools\n\n**Get Support For Your WordPress LMS Project**\n\n+ Technical support\n+ 30 Days of live weekly onboarding calls called [Liftoff Sessions](https://lifterlms.com/blog/liftoff/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale)\n+ [Live office hours](https://lifterlms.com/product/office-hours/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [Free training courses](https://academy.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale)\n+ [Free training webinars](https://lifterlms.com/lifterlms-webinars/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale)\n+ Setup wizard\n+ [Detailed documentation](https://lifterlms.com/docs/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ Dynamic resources\n+ Demo course\n+ System analyzer\n+ User community\n+ [REST API](https://developer.lifterlms.com/rest-api/)\n+ [Developer ecosystem](https://make.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale)\n+ [Recommended Resources](https://lifterlms.com/recommended-resources/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale) for course creators\n\n**More Resources**\n\n+ The [LifterLMS Official Homepage](https://lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ The [LifterLMS Knowledge base](https://lifterlms.com/docs/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ The [LifterLMS Blog](https://lifterlms.com/blog/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ The [LifterLMS Podcast](https://podcast.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ The [LifterLMS Academy](https://academy.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale)\n+ The [LifterLMS Developer Blog](https://make.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale)\n\n\n### Enhance Your LMS With LifterLMS Add-Ons\n\n**Advanced**\n\nIncrease your LMS website and it's training program's value with these add-ons:\n\n+ [LifterLMS Advanced Quizzes](https://lifterlms.com/product/advanced-quizzes//?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Assignments](https://lifterlms.com/product/lifterlms-assignments/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Private Areas](https://lifterlms.com/product/private-areas/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Social Learning](https://lifterlms.com/product/social-learning/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Advanced Video](https://lifterlms.com/product/advanced-video/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Custom Fields](https://lifterlms.com/product/custom-fields/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Groups](https://lifterlms.com/product/groups/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS PDFs](https://lifterlms.com/product/lifterlms-pdfs/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Private Site](https://lifterlms.com/product/private-site/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Course Cohorts](https://lifterlms.com/product/course-cohorts/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Advanced Coupons](https://lifterlms.com/product/lifterlms-advanced-coupons/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Notes](https://lifterlms.com/product/lifterlms-notes/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Continuing Education](https://lifterlms.com/product/lifterlms-continuing-education/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Gifts](https://lifterlms.com/product/lifterlms-gifts/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n\n**Integrations**\n\nIntegrate your LMS with the tools you use:\n\n+ [LifterLMS Stripe](https://lifterlms.com/product/stripe-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS PayPal](https://lifterlms.com/product/paypal-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Authorize.Net](https://lifterlms.com/product/authorize-net/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS WooCommerce](https://lifterlms.com/product/woocommerce-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Kit](https://lifterlms.com/product/lifterlms-convertkit/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Mailchimp](https://lifterlms.com/product/mailchimp-extension/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n\n### LMS Website and User Experience Design Tools\n\nUse LifterLMS with the modern LifterLMS [Sky Pilot Theme](https://lifterlms.com/product/sky-pilot/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) for even more beautiful results. LifterLMS works with any well-coded WordPress theme, but check out [Sky Pilot](https://lifterlms.com/product/sky-pilot/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) if you want to start with a modern, beautiful, full-site editing block-based theme. And consider using our [Aircraft page builder plugin](https://lifterlms.com/product/aircraft/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale), to make building beautiful web pages fast using our design template library.\n\n+ [LifterLMS Sky Pilot Theme](https://lifterlms.com/product/sky-pilot/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Aircraft](https://lifterlms.com/product/aircraft/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n+ [LifterLMS Powerpack](https://lifterlms.com/product/lifterlms-pro/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n\nhttps://www.youtube.com/watch?v=tE1K1FHiYDM\n\n### Why Do People Switch to LifterLMS From Other LMS Plugins?\n\nThere are several other LMS plugins like:\n\n+ Sensei LMS\n+ Tutor LMS\n+ WP Courseware\n+ LearnDash\n+ Masterstudy LMS\n+ Academy LMS\n+ Namaste LMS\n+ LearnPress\n\nThere are some membership plugins with some course features like:\n\n+ Paid Memberships Pro (Membership plugin that also integrates with LifterLMS)\n+ MemberPress\n\nThe main reasons we hear from users who switched to LifterLMS after trying out the [best WordPress LMS plugins](https://lifterlms.com/blog/best-wordpress-lms-plugins/) is that LifterLMS has:\n\n+ The best most feature-complete customizable well-coded features\n+ Outstanding customer support including live calls\n+ The free LifterLMS plugin is amazing making the project affordable\n+ Memberships, ecommerce, and gamification included in LifterLMS without the need for 3rd party plugins\n\n### Other Plugins and Themes Commonly Used with LifterLMS\n\nThe most common plugins and themes used with LifterLMS include Elementor, WooCommerce, Contact Form 7, Yoast SEO, WP Forms Lite, Akismet Anti-Spam, Elementor, Jetpack by WordPress.com, Classic Editor, Updrafts Plus Backup/Restore, Realy Simple SSL, All-in-One WP Migration, WordPress Importer, Starter Templates, Wordfence Security, Google Analytics for WordPress by MonsterInsights, Loco Translate, Slider Revolution, Astra Pro, Essential Addons for Elementor, WP Mail SMTP, WooCommerce Stripe Gateway, LiteSpeed Cache, Jetpack, Gravity Forms, MailChimp for WooCommerce, BuddyPress, BuddyBoss, Divi, Kadnece, Beaver Builder, bbpress, The Events Calendar, Ultmate Member, and more.\n\nConnect LifterLMS to over 7,000 other apps like Facebook, Google Sheets, Zoom, Shopify, etc. using the [LifterLMS Zapier app](https://lifterlms.com/blog/zapier/).\n\n\n### Support From LifterLMS\n\n**All of our paid products include priority private support.\n\n+ LifterLMS Support Ticket System, ready for any question you have about your LMS\n+ Liftoff Sessions access with live screen-sharing to help you get started with the LMS software\n+ [LifterLMS Office Hours](https://lifterlms.com/product/office-hours/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) is a weekly Mastermind group hosted by LifterLMS CEO Chris Badgett and special guests\n\n### Save on LifterLMS With A Bundle\n\nSave money and get more features.\n\n+ [Earth Bundle](https://lifterlms.com/product/earth-bundle/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) gives you all the essentials you need to get your online learning website up and running so it's collecting money today with the most powerful secure learning management system software.\n+ Level up your online course LMS website with our ecommerce, design, marketing technology, and automation tools with the [Universe Bundle](https://lifterlms.com/product/universe-bundle/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n+ Add more engagement and student transformation potential to your immersive training programs with our entire suite of products including advanced features used by the best teachers, experts, and coaches with the [Infinity Bundle](https://lifterlms.com/product/infinity-bundle/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n\n### Try the Best LMS Plugin\n\n+ Install the free core LifterLMS plugin right now. See how extensive and customizable our free core plugin is.\n\n+ Get a temporary _30 Day_ website on our servers with the core LifterLMS plugin AND all the premium add-ons installed. This demo website allows you to test drive all the LMS add-ons before you invest. Practice creating courses, test out the learner experience, and see how easy it will be to manage your course with WordPress. Install your favorite plugins & themes to test compatibility. **[Try LifterLMS for $1](https://lifterlms.com/try/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)** now.\n+ Test LifterLMS as a student. Take a **free** course on how to build a LifterLMS website in 20 minutes. [Take a Free Course](https://academy.lifterlms.com/course/how-to-build-a-learning-management-system-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) now.\n\n### Scaling LifterLMS\n\nLifterLMS is incredibly flexible, customizable, and scalable. Use it for a simple one course website. Use it as a course marketplace or multi-instructor online school.\n\nLifterLMS is lightweight enough to handle small niche sites, while also powering huge universities and employee training in Fortune 500 corporations. We've even worked with a site that has 4,470,829 course enrollments.\n\nUnlike hosted LMS software where you would pay increasing monthly fees for access and growth, LifterLMS does not charge you more per course, per instructor, per student, or based on your revenue.\n\nWhether you are going big or keeping it small, LifterLMS scales to meet your needs.\n\n### LifterLMS in Action\n+ [Success Stories](https://lifterlms.com/success/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale) — Discover these amazing stories and accomplishments from our community of course creators.\n+ [Showcase](https://lifterlms.com/showcase/?utm_source=LifterLMS%20Plugin&utm_medium=Readme&utm_campaign=Readme%20to%20Sale) — Check out these websites using LifterLMS\n\n### What Others Are Saying About LifterLMS\n\n> _\"I've used many LMS platforms over the years. And they were all fine… right up to the day when they weren't. The trouble is, they all want you to package and manage your course the way THEY think you should do it. THEIR feature set. THEIR way to do it. **Now I host all my courses on LifterLMS. TOTALLY different experience. I'm free to do things MY way. I've never yet hit a wall where LifterLMS didn't enable me to do things the way I wanted.** Love it! Great support and community too.\"_\n\n> _**Nick Usborne**, Teacher, Entrepreneur_\n\n***\n\n> _\"As a former School Teacher, professional User Experience Designer, and current online course creator – I can honestly attribute much of our success to LifterLMS and it’s consideration for multiple learning modalities, the LMS UI/UX out of the box, and natural student Engagement opportunities. **In less than 10 months we’ve gone from $0 to $300K in revenue with LifterLMS** playing a huge part in that!!\"_\n\n> _**Sarah Lorenzen**, Teacher, Entrepreneur_\n\n***\n\n\n### Join Our Growing Community\n\nWhen you download LifterLMS, you **join a thriving community** of education entrepreneurs, course creators, developers, LMS professionals, and WordPress enthusiasts.\n\nWe’re a fast growing open source eLearning community, and everyone seeking to build a sustainable online course business is welcome.\n\nJoin the [LifterLMS VIP Facebook group](https://www.facebook.com/groups/lifterlmsvip/) to:\n\n+ check out what other LifterLMS users are creating\n+ ask questions and support fellow course creators\n\nJoin the [LifterLMS Slack community](https://join.slack.com/t/lifterlms/shared_invite/enQtMzk3ODczNjc4Mjc3LTBlMmEzMWYyOTIwMDU3NDc2MmRhNGIxNGE0Nzc1OWIxZjg1OGVhM2E5YTkwYzZmMmM1ZTU4MDQxYjVlZDYyZTE) if you’d prefer to connect in Slack.\n\n### Contribute\n\nAre you a developer interested in contributing to LifterLMS? Visit the [LifterLMS GitHub Repository](https://github.com/gocodebox/lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) to find out how to support this open source WordPress LMS software.\n\nWant to add a new language to LifterLMS? Contribute language translations at [translate.wordpress.org](https://translate.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n\n\n### What Should You Do Next?\n\n** Install the free LifterLMS plugin on your website **,\n\nthen ...\n\n**[Try out all the premium add-ons for $1 by signing up >>HERE<<](https://lifterlms.com/try/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)**\n\n🚀\n\n<!-- Test Auto deployment -->\n\n"
  },
  {
    "path": ".wordpress-org/readme/10-installation.md",
    "content": "== Installation ==\n\n#### Minimum System Requirements\n\nLifterLMS Requires\n\n+ PHP 7.4 or later\n+ WordPress 5.6 or later\n+ MySQL 5.6 or later\n\nVisit our [full system requirements](https://lifterlms.com/docs/minimum-system-requirements-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) for additional information.\n\n#### Automatic Installation\n\nThe simplest way to install LifterLMS is through your existing WordPress site’s admin. Let WordPress handle file transfers for you - you’ll never need to leave the web browser or admin panel.\n\n1. Log in to your WordPress dashboard\n2. Navigate to Plugins -> Add New\n3. In the search field, type \"LifterLMS\" and click \"Search Plugins\"\n4. Once you've located LifterLMS, click \"Install Now\"\n5. Once installation is complete, click \"Activate\"\n\n#### Manual Installation\n\nTo manually install LifterLMS, you'll need to download the zip file using the \"Download\" link on this screen. Then, use FTP to manually upload the unzipped plugin folder to the proper plugins directory on your webserver.\n\nPlease see this [WordPress Codex document](https://wordpress.org/documentation/article/manage-plugins/#manual-plugin-installation-1) for full instructions on Manual Plugin Installation.\n\n#### Setup Wizard\n\nAfter installation, LifterLMS launches a friendly (and super quick) Setup Wizard.\n\nThis wizard helps you configure LifterLMS so you can get to the fun stuff - like creating your courses - as quickly as possible.\n\nThe wizard includes a few sample courses you can import if you want to see some examples before you start creating your own content.\n\nYou can return to the setup wizard at any time by following [these steps](https://lifterlms.com/docs/rerun-lifterlms-setup-wizard/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n"
  },
  {
    "path": ".wordpress-org/readme/15-faqs.md",
    "content": "== Frequently Asked Questions ==\n\n#### Where do I buy add-ons or bundles for my LifterLMS eLearning Website?\n\nYou can explore the individual learning management system add-ons [here](https://lifterlms.com/store/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) or save BIG with a [bundle](https://lifterlms.com/product-category/bundles/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n\n\n#### How do I troubleshoot issues with my LMS website?\n\nFirst, make sure that you're running the latest version of LifterLMS. And if you've got any other LifterLMS plugins active on your site, make sure those are running the most current version as well.\n\nThe most common issues we see are either plugin conflicts, theme conflicts, or outdated servers. You can test if a plugin or theme is conflicting by manually deactivating other plugins until just LifterLMS is running on your site. If the issue persists from there, revert to the default Twenty Fifteen theme. If the issue is resolved after deactivating a specific plugin or your theme, you'll know that is the source of the conflict. If it is a hosting issue, contact your web host and make sure they’re running the most current version of PHP.\n\nAlso be sure to check out the official LifterLMS [Knowledge Base](https://lifterlms.com/docs/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n\n\n#### How do I ask a question about my online course website?\n\nUsers of the free LifterLMS should post their questions here in our WordPress.org support area. If you find you're not getting support in as timely a fashion as you wish, you might want to consider [purchasing a product from LifterLMS](https://lifterlms.com/pricing/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale) so you can access the LifterLMS support team.\n\nIf you're already a LifterLMS customer, you can simply log into your account and contact the support team directly on the [LifterLMS website](https://lifterlms.com/my-account/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale). We can provide a deeper level of support in there and address your needs on a daily basis during the work week. Generally, except in times of increased support loads, we reply to all comments within 12 business hours.\n\n\n#### LifterLMS is awesome! Can you set up my online course site for me?\n\nLifterLMS offers technical support, but we do not offer custom website development services. However, we do recommend third party LifterLMS Experts who can help with web design, web development, instructional design or marketing for a fee.  Click here to visit the [LifterLMS Experts page](https://lifterlms.com/experts/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n\n\n#### I'm interested in purchasing add-ons for my WordPress LMS website, but I have a few questions first.\n\nIf you're not finding your questions answered here or on our website, you can ask your presales questions through this [contact form](https://lifterlms.com/contact/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale). You can also connect live with a member of our team [here](https://lifterlms.com/contact/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n\n\n#### What add-ons are available for LifterLMS, and where can I read more about them?\n\nYou can find a full list of official LifterLMS Add-ons [here](https://lifterlms.com/store/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)\n\n\n#### I have a feature idea. What's the best way to tell you about it?\n\nWe care about your feature ideas and what you have to say. You can [request a feature](https://lifterlms.com/contact/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale), [vote on existing feature requests](?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale), and checkout the [product roadmap](https://lifterlms.com/roadmap/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale).\n\n\n#### Do you have any training for building an online course website?\n\nBe sure you’ve taken the free tutorial training video course: [How to Create an Online Course with LifterLMS](https://academy.lifterlms.com/course/how-to-build-a-learning-management-system-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale). We also encourage you to get to know us by signing up for a $1 temporary _30 Day_ website on our servers which comes with the core LifterLMS plugin all our add-ons intalled. This demo allows you to test drive all the add-ons before you invest. Check it out here: **[Try LifterLMS for $1](https://lifterlms.com/try/?utm_source=LifterLMS%20Plugin&utm_medium=README&utm_campaign=Readme%20to%20Sale)**.\n\n\n#### I'm interested in contributing to LifterLMS, how can I start?\n\nLifterLMS is an open source project. We manage our team, developers, issues, and code on [GitHub](https://github.com/gocodebox/lifterlms/).\n\nWe welcome contributions of all kinds, anyone can contribute even if you don't write code! Check out our [Contributor's Guidelines](https://github.com/gocodebox/lifterlms/blob/master/.github/CONTRIBUTING.md) to get started.\n\n\n#### I found a security vulnerability or issue, how can I report it to the team?\n\nThe LifterLMS team takes security issues and vulnerabilities very seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.\n\nPlease contact team@lifterlms.com to report a security vulnerability.\n\nYou can review our full security policy at [https://lifterlms.com/security-policy](https://lifterlms.com/security-policy).\n"
  },
  {
    "path": ".wordpress-org/readme/20-screenshots.md",
    "content": "== Screenshots ==\n\n1. Infinitely customizable course catalog layouts: shown with course title, featured image, and instructor information.\n2. View a single course with customizable content including access plans, difficulty, instructor, and lesson syllabus.\n3. Edit courses in the WordPress block editor to add pricing tables, progress, outline, and more content.\n4. Use the interactive Course Builder to structure your course, sections, lessons, quizzes, assignments and more.\n5. Dashboard for course creators in the WordPress admin: an overview of recent statistics and quick links to common admin screens.\n6. Advanced reporting for every learner so admins can track an individual students's course progress, membership, engagements, and achievements.\n7. Clean and organized plugin settings to help you quickly and easily set up your course or membership site. \n8. Detailed sales and enrollment reporting with built-in time periods or custom fields to filter by term, student, course, and membership.\n9. Setup Wizard to help you install and configure your new online course website with LifterLMS in 5 simple steps."
  },
  {
    "path": ".wordpress-org/readme/25-changelog.md",
    "content": "== Changelog ==\n\n{{__CHANGELOG_ENTRIES__}}\n\n\n[Read the full changelog]({{__READ_MORE_LINK__}})\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "LifterLMS Changelog\n===================\n\nv10.0.0 - 2026-05-01\n--------------------\n\n##### New Features\n\n+ Lesson content can be edited within the Course Builder for new lessons, or existing lessons with no existing content.\n+ Adding tab for Events.\n+ Added an explicit 'Any' trigger option for engagements so one engagement can apply to all matching courses, memberships, lessons, quizzes, sections, access plans, or tracks. [#3109](https://github.com/gocodebox/lifterlms/issues/3109)\n+ Adding new \"focus mode\" when viewing lessons and quizzes in a course.\n+ Added [lifterlms_lesson_navigation] shortcode.\n\n##### Updates and Enhancements\n\n+ Adding order note when changing the access expiration date.\n+ Add additional check for course capacity on the checkout page. [#3086](https://github.com/gocodebox/lifterlms/issues/3086)\n+ Allow admin to mark lesson complete with unpublished quiz. [#3127](https://github.com/gocodebox/lifterlms/issues/3127)\n+ Scroll to the top of the quiz UI area when a quiz question is loaded.\n+ Various course builder fixes, with quizzes set to published by default. [#3033](https://github.com/gocodebox/lifterlms/issues/3033), [#3056](https://github.com/gocodebox/lifterlms/issues/3056), [#3097](https://github.com/gocodebox/lifterlms/issues/3097), [#2938](https://github.com/gocodebox/lifterlms/issues/2938), [#3030](https://github.com/gocodebox/lifterlms/issues/3030)\n+ Using standard WP nonce check functions instead of llms_verify_nonce.\n+ Removing use of SQL_CALC_FOUND_ROWS due to depreciation in MySQL and observed unreliability of count results.\n+ Updating helper and rest libraries, which switch to using standard WP nonce check functions.\n\n##### Bug Fixes\n\n+ Show \"Mark Complete\" button when quiz requirements are already met. Thanks [@faisalahammad](https://github.com/faisalahammad)! [#3058](https://github.com/gocodebox/lifterlms/issues/3058)\n+ Strip formatting when pasting into Course Builder title fields. Thanks [@faisalahammad](https://github.com/faisalahammad)! [#3057](https://github.com/gocodebox/lifterlms/issues/3057)\n+ Avoid \"block not found\" error in the theme template editor.\n+ Avoid error protecting media image when multiple thumbnail of same size exist, or registered thumbnail size is missing. [#3129](https://github.com/gocodebox/lifterlms/issues/3129)\n+ Close lesson settings panel when lesson has been trashed.\n+ Fix lifterlms_loop_columns filter styling to change the number of columns in a loop (ie. Course Catalog). [#3101](https://github.com/gocodebox/lifterlms/issues/3101)\n+ Fix pagination for My Courses when using Plain style permalinks. [#3134](https://github.com/gocodebox/lifterlms/issues/3134)\n+ Avoids PHP warning for passing null to exit() when using llms_exit().\n+ Avoid \"Launch Course Builder\" showing on orphaned lessons. [#2943](https://github.com/gocodebox/lifterlms/issues/2943)\n\n##### Deprecations\n\n+ Removing unused LLMS_Update class. [#1981](https://github.com/gocodebox/lifterlms/issues/1981)\n\n##### Developer Notes\n\n+ Adding actions for add-ons to insert additional blocks when a post is migrated to the block editor.\n+ Filter for modifying the field for a Media attachement.\n\n##### Updated Templates\n\n+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/10.0.0/templates/course/outline-list-small.php)\n+ [templates/loop/pagination.php](https://github.com/gocodebox/lifterlms/blob/10.0.0/templates/loop/pagination.php)\n+ [templates/single-lesson-focus.php](https://github.com/gocodebox/lifterlms/blob/10.0.0/templates/single-lesson-focus.php)\n\n\nv9.2.3 - 2026-04-07\n-------------------\n\n##### Security Fixes\n\n+ Additional permission checks when performing certain admin actions. Thanks [@nobody090909](https://github.com/nobody090909)!\n\n\nv9.2.2 - 2026-03-31\n-------------------\n\n##### Updates and Enhancements\n\n+ Various escaping and consistency changes.\n\n##### Bug Fixes\n\n+ Removes use of deprecated mb_convert_encoding(). [#2672](https://github.com/gocodebox/lifterlms/issues/2672)\n\n##### Security Fixes\n\n+ Validation of the order param for the quiz Students Without Attempts table.\n\n##### Updated Templates\n\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/9.2.2/templates/course/lesson-preview.php)\n+ [templates/myaccount/my-grades-single-table.php](https://github.com/gocodebox/lifterlms/blob/9.2.2/templates/myaccount/my-grades-single-table.php)\n+ [templates/quiz/questions/content-choice.php](https://github.com/gocodebox/lifterlms/blob/9.2.2/templates/quiz/questions/content-choice.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/9.2.2/templates/quiz/questions/content-picture_choice.php)\n\n\nv9.2.1 - 2026-02-02\n-------------------\n\n##### Bug Fixes\n\n+ Fix course data calculation not ending since 9.2.0. [#3087](https://github.com/gocodebox/lifterlms/issues/3087)\n\n\nv9.2.0 - 2026-01-15\n-------------------\n\n##### New Features\n\n+ Adding \"Order (High to Low)\" sorting option for the Course and Membership Catalog. [#3074](https://github.com/gocodebox/lifterlms/issues/3074)\n+ New global and course-level option to specify a page to redirect to upon course completion.\n+ Ability to edit pricing for future charges of a recurring Order.\n\n##### Updates and Enhancements\n\n+ Renaming \"Basic\" notification to \"Popup\" for clarity.\n+ Avoid showing license key on error.\n\n##### Bug Fixes\n\n+ Additional verifications when checking for an Elementor post to avoid fatal errors in some cases. [#3065](https://github.com/gocodebox/lifterlms/issues/3065)\n+ Allow selection of all categories in the Courses block when there are more than 10 categories. [#3078](https://github.com/gocodebox/lifterlms/issues/3078)\n+ Fixing aria label output for course favorites. Thanks [@DAnn2012](https://github.com/DAnn2012)!\n+ Avoid fatal error when importing a course with an empty picture choice question. [#3070](https://github.com/gocodebox/lifterlms/issues/3070)\n+ Fix protected images not loading on WPEngine hosting. [#3048](https://github.com/gocodebox/lifterlms/issues/3048)\n+ Fix count of currently enrolled students in the Course overview reporting for certain hosts and number of students. [#3073](https://github.com/gocodebox/lifterlms/issues/3073)\n+ Handle possible array of arrays in admin settings.\n\n##### Updated Templates\n\n+ [templates/course/favorite.php](https://github.com/gocodebox/lifterlms/blob/9.2.0/templates/course/favorite.php)\n\n\nv9.1.2 - 2025-11-20\n-------------------\n\n##### Updates and Enhancements\n\n+ Changed options for a new Access Plan to include gifts, and removing the Free Trial option.\n\n##### Bug Fixes\n\n+ Avoid warning when creating the first access plan on a course/membership. [#3046](https://github.com/gocodebox/lifterlms/issues/3046)\n\n##### Developer Notes\n\n+ Filters for displaying already enrolled message during checkout.\n+ Filter to avoid sending a Purchase Receipt under certain conditions.\n+ Filter to prevent automatic enrollment in a product after purchase completed.\n\n\nv9.1.1 - 2025-11-11\n-------------------\n\n##### Security Fixes\n\n+ Fixes security issue where student and instructor REST APIs can be used to modify roles incorrectly. Thanks [@shark3y](https://github.com/shark3y)!\n\n\nv9.1.0 - 2025-11-03\n-------------------\n\n##### New Features\n\n+ New tabs to view students who have not yet attempted a quiz, and listing of all quiz attempts for a student.\n+ Option to allow unlimited time for a time-limited quiz to certain users.\n\n##### Updates and Enhancements\n\n+ Apply filters to save any additional fields added to LifterLMS metaboxes.\n+ Adjusting syllabus styling for clarity on what can be clicked to navigate to a lesson. [#3041](https://github.com/gocodebox/lifterlms/issues/3041)\n+ Template changes for improved accessibility when taking a quiz when using a keyboard or screen reader.\n+ Re-label \"Exit Quiz\" button for clarity on resumable quizzes. [#3025](https://github.com/gocodebox/lifterlms/issues/3025)\n+ Show order summary for free enrolments.\n+ Removing \"Estimated Completion Time\" option from the Course Information block. [#3016](https://github.com/gocodebox/lifterlms/issues/3016)\n+ Show warning and avoid generating invalid checkout URLs if no Checkout Page is configured. [#2984](https://github.com/gocodebox/lifterlms/issues/2984)\n+ Making default password strength \"weak\" and changing strength requirements to minimize friction during checkout. [#2848](https://github.com/gocodebox/lifterlms/issues/2848)\n\n##### Bug Fixes\n\n+ Fix wording for adding a featured video on memberships. [#3034](https://github.com/gocodebox/lifterlms/issues/3034)\n+ Fixing \"user email required\" warning when editing a LifterLMS form and changing a pattern. [#2644](https://github.com/gocodebox/lifterlms/issues/2644)\n\n##### Developer Notes\n\n+ Additional filter to change available merge codes for certificates.\n+ Adds llms_embed_shortcode_output filter.\n+ Filter to control whether a question choice is marked as correct.\n\n##### Updated Templates\n\n+ [templates/admin/reporting/tabs/quizzes/non-attempts.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/admin/reporting/tabs/quizzes/non-attempts.php)\n+ [templates/admin/reporting/tabs/students/quiz_attempts.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/admin/reporting/tabs/students/quiz_attempts.php)\n+ [templates/admin/reporting/tabs/students/student.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/admin/reporting/tabs/students/student.php)\n+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/checkout/form-checkout.php)\n+ [templates/quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/quiz/meta-information.php)\n+ [templates/quiz/questions/content-choice.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/quiz/questions/content-choice.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/quiz/questions/content-picture_choice.php)\n+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/9.1.0/templates/quiz/results.php)\n\n\nv9.0.7 - 2025-09-16\n-------------------\n\n##### Bug Fixes\n\n+ Additional check to avoid conflict with certain plugins alongside the Classic Editor. [#3012](https://github.com/gocodebox/lifterlms/issues/3012)\n+ Fixing checkout for countries that have no states/provinces/regions listed.\n\n\nv9.0.6 - 2025-08-28\n-------------------\n\n##### Updates and Enhancements\n\n+ Upgrades select2 library to latest release.\n\n##### Bug Fixes\n\n+ Avoid loading the media protection attachment scripts when not needed. [#3004](https://github.com/gocodebox/lifterlms/issues/3004)\n+ Fix for \"only recurring access plan\" coupons. [#3002](https://github.com/gocodebox/lifterlms/issues/3002)\n\n\nv9.0.5 - 2025-08-25\n-------------------\n\n##### Bug Fixes\n\n+ Fix to allow checkout with UK and several other countries with no states/provinces/regions/areas. [#2997](https://github.com/gocodebox/lifterlms/issues/2997)\n\n\nv9.0.4 - 2025-08-19\n-------------------\n\n##### Bug Fixes\n\n+ Avoid fatal error if another plugin has loaded the Banner Notifications library.\n\n\nv9.0.3 - 2025-08-19\n-------------------\n\n##### Bug Fixes\n\n+ Additional checks for valid courses during the setup wizard. [#2992](https://github.com/gocodebox/lifterlms/issues/2992)\n\n\nv9.0.2 - 2025-08-19\n-------------------\n\n##### Bug Fixes\n\n+ Avoid fatal error during setup wizard if list of courses to import cannot be fetched. [#2992](https://github.com/gocodebox/lifterlms/issues/2992)\n+ Avoid i18n translation warning on brand new site when scheduling cron. [#2990](https://github.com/gocodebox/lifterlms/issues/2990)\n\n\nv9.0.1 - 2025-08-18\n-------------------\n\n##### Bug Fixes\n\n+ Fix \"unsaved changes\" warning when navigating away from a course/membeship with instructors block in it. [#2986](https://github.com/gocodebox/lifterlms/issues/2986)\n\n\nv9.0.0 - 2025-08-18\n-------------------\n\n##### New Features\n\n+ Adding Mailhawk back to the email provider services.\n+ Ability to protect media files for those enrolled in a specified Course or Membership.\n+ Adding banner notifications.\n+ Option for automatically blocking checkout spam.\n+ Support for Turnstile, reCAPTCHA v3 and Akismet with checkout and registration forms.\n+ Link to detach a Lesson from the course and section in the Lessons listing. [#2944](https://github.com/gocodebox/lifterlms/issues/2944)\n+ Featured video and audio options for Memberships.\n+ Mobile navigation for the Student Dashboard. [#2907](https://github.com/gocodebox/lifterlms/issues/2907)\n\n##### Updates and Enhancements\n\n+ Avoid showing hidden courses or memberships in a theme's next/previous links. [#2910](https://github.com/gocodebox/lifterlms/issues/2910)\n+ Additional payment gateway information notices.\n+ Avoid rendering LifterLMS blocks hidden in Text Editor blocks when using Elementor. [#2886](https://github.com/gocodebox/lifterlms/issues/2886)\n+ Filter to change the method used to fetch environment variables on certain hosting configurations. [#2383](https://github.com/gocodebox/lifterlms/issues/2383)\n+ Improved formatting of the individual order page.\n+ Moving instructors and Estimated Length from the sidebar to the main metabox options area in Courses and Memberships.\n+ Changes button text when updating a payment method,depending on whether it's a payment gateway with an external payment page. [#2957](https://github.com/gocodebox/lifterlms/issues/2957)\n+ Updating countries and states/countries/provinces. [#2151](https://github.com/gocodebox/lifterlms/issues/2151)\n\n##### Bug Fixes\n\n+ Can select an access plan with the Access Plan Button block when more than 10 access plans exist on the site. [#2526](https://github.com/gocodebox/lifterlms/issues/2526)\n+ Schedule course data recalculation throttles in the future, with new tool to clear incorrectly locked courses. [#2916](https://github.com/gocodebox/lifterlms/issues/2916)\n+ Now able to clear the value from date fields. [#2977](https://github.com/gocodebox/lifterlms/issues/2977)\n+ Avoid fatal error if a user who enrolled a student has been deleted, when viewing the course edit page. [#2979](https://github.com/gocodebox/lifterlms/issues/2979)\n+ Avoid breaking the layout on student dashboard pages with a form (ie. Edit Account, Redeem a Voucher). [#2946](https://github.com/gocodebox/lifterlms/issues/2946)\n+ Avoid console error when saving access plans when a free plan is included. [#2925](https://github.com/gocodebox/lifterlms/issues/2925)\n+ Avoid duplicated buttons when changing the Engagement Type when editing an engagement. [#2962](https://github.com/gocodebox/lifterlms/issues/2962)\n+ Fix link beside selected engagement when editing an engagement. [#2961](https://github.com/gocodebox/lifterlms/issues/2961)\n+ Resolves warning when first visiting the Logs tab. [#2947](https://github.com/gocodebox/lifterlms/issues/2947)\n+ Improved styling for course catalog and other grid layouts, including fix for course/membership featured video display.\n+ Allow clearing membership restrictions without switching plan availability. [#2931](https://github.com/gocodebox/lifterlms/issues/2931)\n\n##### Developer Notes\n\n+ Adding hookable filter for notification sending.\n\n##### Updated Templates\n\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/9.0.0/templates/checkout/form-switch-source.php)\n+ [templates/loop/featured-image.php](https://github.com/gocodebox/lifterlms/blob/9.0.0/templates/loop/featured-image.php)\n+ [templates/membership/audio.php](https://github.com/gocodebox/lifterlms/blob/9.0.0/templates/membership/audio.php)\n+ [templates/membership/video.php](https://github.com/gocodebox/lifterlms/blob/9.0.0/templates/membership/video.php)\n+ [templates/myaccount/my-orders.php](https://github.com/gocodebox/lifterlms/blob/9.0.0/templates/myaccount/my-orders.php)\n+ [templates/myaccount/navigation.php](https://github.com/gocodebox/lifterlms/blob/9.0.0/templates/myaccount/navigation.php)\n\n\nv8.0.7 - 2025-06-11\n-------------------\n\n##### Security Fixes\n\n+ Additional sanitation of the voucher field.\n\n\nv8.0.6 - 2025-04-21\n-------------------\n\n##### Bug Fixes\n\n+ Fix error when editing a lesson with a drip setting of a specific date. [#2926](https://github.com/gocodebox/lifterlms/issues/2926)\n\n\nv8.0.5 - 2025-04-17\n-------------------\n\n##### Updates and Enhancements\n\n+ Modifies the allowed HTML for a form, in case the allowed post values in WP have been filtered. [#2922](https://github.com/gocodebox/lifterlms/issues/2922)\n\n\nv8.0.4 - 2025-04-11\n-------------------\n\n##### Bug Fixes\n\n+ Avoid warning and possible error when serving protected files. [#2912](https://github.com/gocodebox/lifterlms/issues/2912)\n\n\nv8.0.3 - 2025-04-07\n-------------------\n\n##### New Features\n\n+ Initial support for the Bricks theme.\n+ New \"# of transactions\" sales reporting widget.\n\n##### Updates and Enhancements\n\n+ Change default date display format of lifterlms_course_info used in settings like Enrollment Start and End Date to match the site date format. [#2903](https://github.com/gocodebox/lifterlms/issues/2903)\n+ Display date pickers in the site date format, but save the data in the previous format. [#2447](https://github.com/gocodebox/lifterlms/issues/2447)\n+ Update Last Activity Date label to accurately reflect the data value. [#2898](https://github.com/gocodebox/lifterlms/issues/2898)\n\n##### Bug Fixes\n\n+ Updating postal code (eircode) display for Ireland. [#2891](https://github.com/gocodebox/lifterlms/issues/2891)\n+ Net Sales reporting includes partially refunded transactions, and transactions from orders regardless of status. [#2860](https://github.com/gocodebox/lifterlms/issues/2860), [#2861](https://github.com/gocodebox/lifterlms/issues/2861)\n+ On-hold, pending cancellation, cancelled and expired orders now included in \"# of Sales\" widget. [#2860](https://github.com/gocodebox/lifterlms/issues/2860)\n+ Fixes sales reporting for transactions or orders that happened between 23:23:59 and midnight. [#2858](https://github.com/gocodebox/lifterlms/issues/2858)\n+ Password reset email is now in the user's language. [#2879](https://github.com/gocodebox/lifterlms/issues/2879)\n\n##### Developer Notes\n\n+ Adds a new action so Turnstile or other captchas can be added without editing the free-enroll-form.php template directly.\n\n##### Updated Templates\n\n+ [templates/admin/reporting/tabs/students/courses-course.php](https://github.com/gocodebox/lifterlms/blob/8.0.3/templates/admin/reporting/tabs/students/courses-course.php)\n+ [templates/product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/8.0.3/templates/product/free-enroll-form.php)\n\n\nv8.0.2 - 2025-03-17\n-------------------\n\n##### Bug Fixes\n\n+ Avoid escaping the selected attribute of a form field select dropdown incorrectly.\n+ Adds additional verifications on permission for bulk enrolls, and REST API access for instructors.\n+ Updates helper library to remove depreciated notice when adding a license key.\n\n##### Security Fixes\n\n+ Additional checks for managing LifterLMS data. Thanks [@mikemyers](https://github.com/mikemyers)!\n\n##### Updated Templates\n\n+ [templates/course/favorite.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/course/favorite.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/course/lesson-preview.php)\n+ [templates/global/form-login.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/global/form-login.php)\n+ [templates/global/form-registration.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/global/form-registration.php)\n+ [templates/loop/featured-pricing.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/loop/featured-pricing.php)\n+ [templates/myaccount/form-edit-account.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/myaccount/form-edit-account.php)\n+ [templates/myaccount/form-redeem-voucher.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/myaccount/form-redeem-voucher.php)\n+ [templates/myaccount/my-grades-single.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/myaccount/my-grades-single.php)\n+ [templates/notifications/basic.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/notifications/basic.php)\n+ [templates/product/access-plan-button.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/product/access-plan-button.php)\n+ [templates/product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/product/free-enroll-form.php)\n+ [templates/quiz/questions/description.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/quiz/questions/description.php)\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/8.0.2/templates/quiz/results-attempt-questions-list.php)\n\n\nv8.0.1 - 2025-02-06\n-------------------\n\n##### New Features\n\n+ Setting to increase the frequency user tracked events are saved.\n\n##### Bug Fixes\n\n+ Additional escaping for form permalinks.\n+ Allow quiz question to be retrieved even if attempt limit reached when resuming a quiz. [#2865](https://github.com/gocodebox/lifterlms/issues/2865)\n+ Avoid grades table wrapping in the My Grades section of the student dashboard. [#2869](https://github.com/gocodebox/lifterlms/issues/2869)\n+ Open the lesson panel after adding a new lesson in the Course Builder. [#2855](https://github.com/gocodebox/lifterlms/issues/2855)\n+ Fixed PHP >= 8.3 warning when using WP CLI. Thanks [@jv-mtrz](https://github.com/jv-mtrz)!\n\n##### Deprecations\n\n+ Fixed issues with viewing quiz attempts when questions were deleted.\n\n##### Updated Templates\n\n+ [templates/myaccount/my-grades-single.php](https://github.com/gocodebox/lifterlms/blob/8.0.1/templates/myaccount/my-grades-single.php)\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/8.0.1/templates/quiz/results-attempt-questions-list.php)\n\n\nv8.0.0 - 2025-01-20\n-------------------\n\n##### New Features\n\n+ New popup when adding an Access Plan with templates to help set options quickly.\n+ Adds support for Beaver Builder for Courses, Memberships and Lessons. [#2761](https://github.com/gocodebox/lifterlms/issues/2761)\n+ Ability to show typed password for verification on the Student Dashboard login form.\n+ Adds new \"Featured Pricing Information\" setting for Courses and Memberships.\n+ Showing 'Has Quiz' or 'Has Assignment' in the Course Syllabus.\n\n##### Updates and Enhancements\n\n+ Improved accessibility of various front-end and back-end UIs including the Access Plans metaboxes to use proper labels and screen reader text where needed.\n+ Allow lesson access based on enrollment drip setting with Course Start Date set. [#2843](https://github.com/gocodebox/lifterlms/issues/2843)\n+ Course Information header is now h2 instead of h3 by default, for accessibility in heading order.\n+ Adding purchase checkout link to the access plan header as an icon. [#2793](https://github.com/gocodebox/lifterlms/issues/2793)\n+ Accessibility updates for lesson favorite and write a review forms. [#2852](https://github.com/gocodebox/lifterlms/issues/2852)\n\n##### Bug Fixes\n\n+ Allow additional shortcodes like [audio] within the description (content) of a quiz question.\n+ Can now deactivate the Confirmation field for blocks like E-mail address and Password in forms. [#2646](https://github.com/gocodebox/lifterlms/issues/2646)\n+ Fix SendWP connect button. [#2792](https://github.com/gocodebox/lifterlms/issues/2792)\n+ Avoid unloading the textdomain for core translations in case the lifterlms textdomain is used before init (WP 6.7). [#2807](https://github.com/gocodebox/lifterlms/issues/2807)\n+ Avoid wrapping of the text of buttons. [#2820](https://github.com/gocodebox/lifterlms/issues/2820)\n\n##### Developer Notes\n\n+ Improved llmsPostsSelect2 method when called on multiple elements at once, each with different options. [#2805](https://github.com/gocodebox/lifterlms/issues/2805)\n\n##### Updated Templates\n\n+ [templates/course/favorite.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/course/favorite.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/course/lesson-preview.php)\n+ [templates/global/form-login.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/global/form-login.php)\n+ [templates/global/form-registration.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/global/form-registration.php)\n+ [templates/loop/featured-pricing.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/loop/featured-pricing.php)\n+ [templates/myaccount/form-edit-account.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/myaccount/form-edit-account.php)\n+ [templates/myaccount/form-redeem-voucher.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/myaccount/form-redeem-voucher.php)\n+ [templates/notifications/basic.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/notifications/basic.php)\n+ [templates/product/access-plan-button.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/product/access-plan-button.php)\n+ [templates/product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/product/free-enroll-form.php)\n+ [templates/quiz/questions/description.php](https://github.com/gocodebox/lifterlms/blob/8.0.0/templates/quiz/questions/description.php)\n\n\nv7.8.7 - 2024-12-17\n-------------------\n\n##### Bug Fixes\n\n+ Fix translation error during the setup wizard. [#2835](https://github.com/gocodebox/lifterlms/issues/2835)\n+ Fixes Pagespeed notice regarding deprecated Javascript event usage. [#2620](https://github.com/gocodebox/lifterlms/issues/2620)\n\n\nv7.8.6 - 2024-12-16\n-------------------\n\n##### Bug Fixes\n\n+ Adds additional check for valid quiz attempt key when ending or exiting a quiz. [#2824](https://github.com/gocodebox/lifterlms/issues/2824)\n+ Fix for daylight savings and leap years when scheduling engagements. [#2799](https://github.com/gocodebox/lifterlms/issues/2799)\n+ Avoid showing course opens message if no Course Start Date has been set. [#2810](https://github.com/gocodebox/lifterlms/issues/2810)\n+ Improved accessibility of the lessons listing on a course page, when a lesson is restricted. [#2827](https://github.com/gocodebox/lifterlms/issues/2827)\n\n##### Security Fixes\n\n+ Adding additional checks before the deletion of a certificate. Thanks Lucio Sá!\n\n##### Updated Templates\n\n+ [templates/content-single-course-before.php](https://github.com/gocodebox/lifterlms/blob/7.8.6/templates/content-single-course-before.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/7.8.6/templates/course/lesson-preview.php)\n\n\nv7.8.5 - 2024-12-03\n-------------------\n\n##### Updates and Enhancements\n\n+ Now allows copying of text in input and textarea elements, even if copy protection is enabled.\n\n##### Security Fixes\n\n+ Fix to avoid saving password confirmation in user meta if Password block has been edited. [#2821](https://github.com/gocodebox/lifterlms/issues/2821)\n\n\nv7.8.4 - 2024-11-18\n-------------------\n\n##### Bug Fixes\n\n+ Fix translations not loading for LifterLMS in WordPress 6.7. [#2807](https://github.com/gocodebox/lifterlms/issues/2807)\n\n\nv7.8.3 - 2024-11-04\n-------------------\n\n##### Updates and Enhancements\n\n+ Switches new access plans to Paid by default in the new access plan UI. [#2794](https://github.com/gocodebox/lifterlms/issues/2794)\n\n\nv7.8.2 - 2024-10-31\n-------------------\n\n##### Updates and Enhancements\n\n+ Additional styling of the new Access Plan UI.\n\n##### Bug Fixes\n\n+ Renaming front-end javascript translation file not required, as some security scans appear to be deleting it incorrectly. [#2787](https://github.com/gocodebox/lifterlms/issues/2787)\n\n\nv7.8.1 - 2024-10-29\n-------------------\n\n##### Bug Fixes\n\n+ Fixing migration to allow SKUs to be entered in access plans if one already exists.\n\n\nv7.8.0 - 2024-10-29\n-------------------\n\n##### New Features\n\n+ Added attribute 'layout' to My Account block and shortcode to display content as columns or stacked.\n+ Changes styling in the Course Syllabus block for the current lesson, when used on a single lesson page. [#2777](https://github.com/gocodebox/lifterlms/issues/2777)\n+ Added new feature: Quiz Resume. [#2783](https://github.com/gocodebox/lifterlms/issues/2783)\n\n##### Updates and Enhancements\n\n+ Improved UI of the Access Plans UI for better usability.\n+ Improved accessibility of the Access Plans metaboxes to use proper labels and screen reader text where needed.\n+ Added Launch Course Builder to the WP Admin Bar when viewing a course.\n+ Added Launch Course Builder button when using the Classic Editor, beside “Add Course” button.\n+ Added “Clear Reporting Cache” button on admin reporting page.\n+ Improved help messaging on the Course Builder, and the Account and Checkout tabs of the LifterLMS settings.\n+ Added support for image upload in Result Clarifications box for quizzes.\n+ Removes spacing before or after answers for conditional quiz questions.\n\n##### Bug Fixes\n\n+ Fixed reference in `LLMS_Ajax_Handler::quiz_start()` to `LLMS_Quiz_Attempt::get_status()` method removed since LifterLMS 4.0.0.\n+ Fix for notifications no longer auto-dismissing. [#2772](https://github.com/gocodebox/lifterlms/issues/2772)\n+ Fix for the lifterlms_registration shortcode not working on certain themes on a separate page. [#2779](https://github.com/gocodebox/lifterlms/issues/2779)\n+ Using a more reliable method of keeping LifterLMS notices dismissed. [#2767](https://github.com/gocodebox/lifterlms/issues/2767)\n\n##### Breaking Changes\n\n+ Hiding the Plan SKU field for access plans if not in use. Set the `llms_access_plans_allow_skus` option to get this field back.\n\n##### Developer Notes\n\n+ Adds current-lesson CSS class for the current Lesson in the Course Syllabus block. [#2777](https://github.com/gocodebox/lifterlms/issues/2777)\n+ Adds new llms_before_registration_validation filter for 3rd party open registration validation.\n+ Added filter `llms_quiz_attempt_resume_time_period` for updating quiz resume allowed time period.\n\n##### Updated Templates\n\n+ [templates/admin/reporting/tabs/quizzes/attempt.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/admin/reporting/tabs/quizzes/attempt.php)\n+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/checkout/form-checkout.php)\n+ [templates/checkout/form-summary.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/checkout/form-summary.php)\n+ [templates/content-single-question.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/content-single-question.php)\n+ [templates/myaccount/form-redeem-voucher.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/myaccount/form-redeem-voucher.php)\n+ [templates/myaccount/header.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/myaccount/header.php)\n+ [templates/notifications/basic.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/notifications/basic.php)\n+ [templates/quiz/questions/content-choice.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/quiz/questions/content-choice.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/quiz/questions/content-picture_choice.php)\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/quiz/results-attempt-questions-list.php)\n+ [templates/quiz/results-attempt.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/quiz/results-attempt.php)\n+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/quiz/results.php)\n+ [templates/quiz/start-button.php](https://github.com/gocodebox/lifterlms/blob/7.8.0/templates/quiz/start-button.php)\n\n\nv7.7.8 - 2024-09-17\n-------------------\n\n##### Bug Fixes\n\n+ Update version of the Blocks library to avoid having an unsaved changes prompt appear after updating an access plan on a course.\n+ Fix for pricing display with additional formatting. [#2762](https://github.com/gocodebox/lifterlms/issues/2762)\n\n\nv7.7.7 - 2024-09-12\n-------------------\n\n##### Bug Fixes\n\n+ Avoid modifying non-LifterLMS block component styling in the editor. [#2752](https://github.com/gocodebox/lifterlms/issues/2752)\n+ Use student email for the student login merge code in notification emails if usernames are not used. [#2755](https://github.com/gocodebox/lifterlms/issues/2755)\n+ Show video or audio embed URLs in lessons which are valid but contain special characters. [#2749](https://github.com/gocodebox/lifterlms/issues/2749)\n\n\nv7.7.6 - 2024-08-22\n-------------------\n\n##### Bug Fixes\n\n+ Avoid modifying the Lost Password link if no LifterLMS Dashboard page is set. [#2741](https://github.com/gocodebox/lifterlms/issues/2741)\n+ Fixes placeholder label on the Dashboard Page selection dropdown. [#2708](https://github.com/gocodebox/lifterlms/issues/2708)\n+ Avoid outputting lifterlms_membership_link content if the membership is not published. [#2724](https://github.com/gocodebox/lifterlms/issues/2724)\n+ Fix display of quiz question when viewing the quiz results if it contains formatting. [#2734](https://github.com/gocodebox/lifterlms/issues/2734)\n+ Fixes sanitization as reported by FKSEC.\n+ Fixes warning when trying to get the contents of a media protection file that does not exist. [#2735](https://github.com/gocodebox/lifterlms/issues/2735)\n\n##### Updated Templates\n\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/7.7.6/templates/quiz/results-attempt-questions-list.php)\n\n\nv7.7.5 - 2024-08-15\n-------------------\n\n##### Bug Fixes\n\n+ Show video tiles for courses on the Dashboard and My Courses pages when enabled. [#2728](https://github.com/gocodebox/lifterlms/issues/2728)\n\n##### Updated Templates\n\n+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/7.7.5/templates/myaccount/dashboard-section.php)\n\n\nv7.7.4 - 2024-08-13\n-------------------\n\n##### Bug Fixes\n\n+ Reverts changes to restricting pages by membership functionality to avoid conflicts with certain themes and plugins. [#2714](https://github.com/gocodebox/lifterlms/issues/2714)\n\n\nv7.7.3 - 2024-08-12\n-------------------\n\n##### Bug Fixes\n\n+ Fixes revenue display in the Orders table. [#2719](https://github.com/gocodebox/lifterlms/issues/2719)\n\n\nv7.7.2 - 2024-08-12\n-------------------\n\n##### Bug Fixes\n\n+ Fixes fatal error when updating from an old version. Thanks [@verygoodplugins](https://github.com/verygoodplugins)! [#2716](https://github.com/gocodebox/lifterlms/issues/2716)\n+ Avoid errors on pages restricted by one or more memberships. [#2714](https://github.com/gocodebox/lifterlms/issues/2714)\n\n\nv7.7.1 - 2024-08-09\n-------------------\n\n##### Updates and Enhancements\n\n+ Removes placeholder image functionality with protected media files. Modify cache value for wordpress.com hosting.\n\n##### Bug Fixes\n\n+ Fixing the Award Certificate button appearing at the top of the Reporting > Students, Certificate tab. [#2709](https://github.com/gocodebox/lifterlms/issues/2709)\n+ Fixed warnings from running `wp_kses_post()` on empty `paginate_links()` calls.\n\n##### Updated Templates\n\n+ [templates/myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/7.7.1/templates/myaccount/my-notifications.php)\n\n\nv7.7.0 - 2024-07-19\n-------------------\n\n##### New Features\n\n+ Adding read-only input for easier sharing of a certificate. Thanks [@imknight](https://github.com/imknight)! [#1379](https://github.com/gocodebox/lifterlms/issues/1379)\n+ Adds additional protection for media files uploaded to quiz questions in the Course Builder.\n+ Adds native Elementor support for Courses, with a default Course template and several basic widgets.\n\n##### Updates and Enhancements\n\n+ Removes the Visibility setting for Vouchers and Coupons. [#2640](https://github.com/gocodebox/lifterlms/issues/2640)\n+ Updating internal libraries to their latest versions.\n+ Added support for mutliple membership restriction warning. [#2523](https://github.com/gocodebox/lifterlms/issues/2523)\n\n##### Bug Fixes\n\n+ Prevent backslashes from being removed from Result Clarifications. [#2675](https://github.com/gocodebox/lifterlms/issues/2675)\n+ Avoids JS error on the front-end. [#2678](https://github.com/gocodebox/lifterlms/issues/2678)\n+ Exclude hidden courses when toggled off in the Courses block. [#2690](https://github.com/gocodebox/lifterlms/issues/2690)\n+ Avoids saving review meta information for non-courses. Thanks [@bsetiawan88](https://github.com/bsetiawan88)! [#887](https://github.com/gocodebox/lifterlms/issues/887)\n+ Improvements to the frontend styling of LifterLMS screens for design, accessibility, and better compatibility with dark mode themes.\n+ Allow private VideoPress videos to play when the URL is pasted on Video Embed URL. [#2533](https://github.com/gocodebox/lifterlms/issues/2533)\n+ Fixes the Certificate Title block when creating a new certificate template. [#2696] (https://github.com/gocodebox/lifterlms/issues/2696)\n\n##### Security Fixes\n\n+ Adds various security improvements, e.g. better escaping of output, as suggested by the Plugin Checker Plugin.\n\n##### Performance Improvements\n\n+ Caching get_transaction_total queries to improve performance of the\n  orders table in the admin dashboard.\n\n##### Updated Templates\n\n+ [templates/achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/achievements/loop.php)\n+ [templates/achievements/template.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/achievements/template.php)\n+ [templates/admin/analytics/analytics.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/analytics/analytics.php)\n+ [templates/admin/notices/staging.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/notices/staging.php)\n+ [templates/admin/post-types/order-transactions.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/post-types/order-transactions.php)\n+ [templates/admin/post-types/students.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/post-types/students.php)\n+ [templates/admin/reporting/nav-filters.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/nav-filters.php)\n+ [templates/admin/reporting/reporting.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/reporting.php)\n+ [templates/admin/reporting/tabs/courses/course.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/courses/course.php)\n+ [templates/admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/courses/overview.php)\n+ [templates/admin/reporting/tabs/courses/students.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/courses/students.php)\n+ [templates/admin/reporting/tabs/memberships/membership.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/memberships/membership.php)\n+ [templates/admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/memberships/overview.php)\n+ [templates/admin/reporting/tabs/memberships/students.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/memberships/students.php)\n+ [templates/admin/reporting/tabs/quizzes/attempt.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/quizzes/attempt.php)\n+ [templates/admin/reporting/tabs/quizzes/attempts.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/quizzes/attempts.php)\n+ [templates/admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/quizzes/overview.php)\n+ [templates/admin/reporting/tabs/quizzes/quiz.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/quizzes/quiz.php)\n+ [templates/admin/reporting/tabs/students/achievements.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/achievements.php)\n+ [templates/admin/reporting/tabs/students/certificates.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/certificates.php)\n+ [templates/admin/reporting/tabs/students/courses-course.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/courses-course.php)\n+ [templates/admin/reporting/tabs/students/courses.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/courses.php)\n+ [templates/admin/reporting/tabs/students/information.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/information.php)\n+ [templates/admin/reporting/tabs/students/memberships.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/memberships.php)\n+ [templates/admin/reporting/tabs/students/student.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/student.php)\n+ [templates/admin/reporting/tabs/students/students.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/reporting/tabs/students/students.php)\n+ [templates/admin/user-edit.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/admin/user-edit.php)\n+ [templates/certificates/actions.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/actions.php)\n+ [templates/certificates/content-legacy.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/content-legacy.php)\n+ [templates/certificates/content.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/content.php)\n+ [templates/certificates/dynamic-styles.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/dynamic-styles.php)\n+ [templates/certificates/header.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/header.php)\n+ [templates/certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/loop.php)\n+ [templates/certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/preview.php)\n+ [templates/certificates/template.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/certificates/template.php)\n+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/checkout/form-checkout.php)\n+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/checkout/form-confirm-payment.php)\n+ [templates/checkout/form-coupon.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/checkout/form-coupon.php)\n+ [templates/checkout/form-gateways.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/checkout/form-gateways.php)\n+ [templates/checkout/form-summary.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/checkout/form-summary.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/checkout/form-switch-source.php)\n+ [templates/content-single-question.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/content-single-question.php)\n+ [templates/course/audio.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/audio.php)\n+ [templates/course/categories.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/categories.php)\n+ [templates/course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/complete-lesson-link.php)\n+ [templates/course/difficulty.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/difficulty.php)\n+ [templates/course/favorite.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/favorite.php)\n+ [templates/course/full-description.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/full-description.php)\n+ [templates/course/length.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/length.php)\n+ [templates/course/lesson-count.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/lesson-count.php)\n+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/lesson-navigation.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/lesson-preview.php)\n+ [templates/course/meta-wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/meta-wrapper-start.php)\n+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/outline-list-small.php)\n+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/parent-course.php)\n+ [templates/course/short-description.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/short-description.php)\n+ [templates/course/syllabus.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/syllabus.php)\n+ [templates/course/tags.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/tags.php)\n+ [templates/course/tracks.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/tracks.php)\n+ [templates/course/video.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/course/video.php)\n+ [templates/emails/footer.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/emails/footer.php)\n+ [templates/emails/header.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/emails/header.php)\n+ [templates/emails/reset-password.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/emails/reset-password.php)\n+ [templates/global/form-login.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/global/form-login.php)\n+ [templates/global/form-registration.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/global/form-registration.php)\n+ [templates/lesson/audio.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/lesson/audio.php)\n+ [templates/lesson/video.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/lesson/video.php)\n+ [templates/loop/author.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/loop/author.php)\n+ [templates/loop/enroll-date.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/loop/enroll-date.php)\n+ [templates/loop/enroll-status.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/loop/enroll-status.php)\n+ [templates/loop/featured-image.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/loop/featured-image.php)\n+ [templates/loop/loop-start.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/loop/loop-start.php)\n+ [templates/loop/none-found.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/loop/none-found.php)\n+ [templates/loop/pagination.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/loop/pagination.php)\n+ [templates/membership/full-description.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/membership/full-description.php)\n+ [templates/membership/price.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/membership/price.php)\n+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/dashboard-section.php)\n+ [templates/myaccount/form-edit-account.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/form-edit-account.php)\n+ [templates/myaccount/form-redeem-voucher.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/form-redeem-voucher.php)\n+ [templates/myaccount/my-favorites.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/my-favorites.php)\n+ [templates/myaccount/my-grades-single-table.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/my-grades-single-table.php)\n+ [templates/myaccount/my-grades-single.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/my-grades-single.php)\n+ [templates/myaccount/my-grades.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/my-grades.php)\n+ [templates/myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/my-notifications.php)\n+ [templates/myaccount/my-orders.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/my-orders.php)\n+ [templates/myaccount/navigation.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/navigation.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/view-order-actions.php)\n+ [templates/myaccount/view-order-information.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/view-order-information.php)\n+ [templates/myaccount/view-order-transactions.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/view-order-transactions.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/myaccount/view-order.php)\n+ [templates/notifications/basic.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/notifications/basic.php)\n+ [templates/product/access-plan-button.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan-button.php)\n+ [templates/product/access-plan-description.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan-description.php)\n+ [templates/product/access-plan-feature.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan-feature.php)\n+ [templates/product/access-plan-pricing.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan-pricing.php)\n+ [templates/product/access-plan-restrictions.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan-restrictions.php)\n+ [templates/product/access-plan-title.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan-title.php)\n+ [templates/product/access-plan-trial.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan-trial.php)\n+ [templates/product/access-plan.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/access-plan.php)\n+ [templates/product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/free-enroll-form.php)\n+ [templates/product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/product/pricing-table.php)\n+ [templates/quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/meta-information.php)\n+ [templates/quiz/questions/content-choice.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/questions/content-choice.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/questions/content-picture_choice.php)\n+ [templates/quiz/questions/description.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/questions/description.php)\n+ [templates/quiz/questions/video.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/questions/video.php)\n+ [templates/quiz/questions/wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/questions/wrapper-start.php)\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/results-attempt-questions-list.php)\n+ [templates/quiz/results-attempt.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/results-attempt.php)\n+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/results.php)\n+ [templates/quiz/return-to-lesson.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/return-to-lesson.php)\n+ [templates/quiz/start-button.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/quiz/start-button.php)\n+ [templates/shared/instructors.php](https://github.com/gocodebox/lifterlms/blob/7.7.0/templates/shared/instructors.php)\n\n\nv7.6.3 - 2024-05-31\n-------------------\n\n##### Bug Fixes\n\n+ Adds additional filtering when using the lifterlms_favorites shortcode. Thanks, Peter Thaleikis.\n\n##### Updated Templates\n\n+ [templates/admin/reporting/nav-filters.php](https://github.com/gocodebox/lifterlms/blob/7.6.3/templates/admin/reporting/nav-filters.php)\n\n\nv7.6.2 - 2024-05-28\n-------------------\n\n##### New Features\n\n+ Added functionality to disable quiz retake after a passed attempt.\n\n##### Updates and Enhancements\n\n+ Adds ability to search by an order ID number in the Orders table. Thanks [@bsetiawan88](https://github.com/bsetiawan88)! [#1583](https://github.com/gocodebox/lifterlms/issues/1583)\n+ Added support for showing Private, Drafts and Pending Courses/Memberships in Reporting > Sales Page. [#2490](https://github.com/gocodebox/lifterlms/issues/2490)\n\n##### Bug Fixes\n\n+ Fixes issue of not being able to save an imported or cloned course using Divi or the Classic Editor plugin. [#2649](https://github.com/gocodebox/lifterlms/issues/2649)\n+ Fixes broken View link after creating a new lesson using the Course Builder. [#2662](https://github.com/gocodebox/lifterlms/issues/2662)\n+ Upgrading Quill text editor version. [#2655](https://github.com/gocodebox/lifterlms/issues/2655)\n\n##### Developer Notes\n\n+ Added logic to delete vouchers data from table when vouchers are deleted. Thanks [@bsetiawan88](https://github.com/bsetiawan88)! [#1087](https://github.com/gocodebox/lifterlms/issues/1087)\n\n##### Updated Templates\n\n+ [templates/admin/reporting/nav-filters.php](https://github.com/gocodebox/lifterlms/blob/7.6.2/templates/admin/reporting/nav-filters.php)\n\n\nv7.6.1 - 2024-05-02\n-------------------\n\n##### Bug Fixes\n\n+ Fix error when trying to add a new lesson outside the course builder. [#2636](https://github.com/gocodebox/lifterlms/issues/2636)\n+ Show correct course title when launching Course Builder immediately after creating a new course. [#2606](https://github.com/gocodebox/lifterlms/issues/2606)\n+ Updating lifter blocks version for the Launch Course Builder button to appear on the Course edit page.\n\n\nv7.6.0 - 2024-04-18\n-------------------\n\n##### New Features\n\n+ Adds course-level lesson drip settings.\n+ Loads translation files later for compatibility with plugins like Loco Translate. [#2429](https://github.com/gocodebox/lifterlms/issues/2429), [#2525](https://github.com/gocodebox/lifterlms/issues/2525)\n+ Adds settings in the Permalinks page to edit the custom post type and taxonomy slugs. Slugs are saved in the site language on install on update.\n+ Adds `llms_switch_to_site_locale` and `llms_restore_locale` to help LifterLMS add-ons switch to the site language when getting translation strings.\n\n##### Updates and Enhancements\n\n+ Improved the Course Builder UI.\n+ Updating the Blocks and Helpers libraries to the latest version.\n\n##### Bug Fixes\n\n+ Allows the style tag when embedding content (iframe), in order to support more services. [#2610](https://github.com/gocodebox/lifterlms/issues/2610)\n+ Removes non-working editing of course title in the Course Builder. [#2607](https://github.com/gocodebox/lifterlms/issues/2607)\n+ Avoid issue with lost content when the course builder is launched immediately after creating a new course. [#2606](https://github.com/gocodebox/lifterlms/issues/2606)\n+ LifterLMS block editor strings now appear in the user's language. [#2525](https://github.com/gocodebox/lifterlms/issues/2525)\n+ Fixed user's language setting not honored on backend. [#2324](https://github.com/gocodebox/lifterlms/issues/2324)\n+ Fixes XSS, sanitization, and other security issues reported by Signal Labs.\n+ Fixes typo with CHF currency.\n\n##### Updated Templates\n\n+ [templates/myaccount/my-orders.php](https://github.com/gocodebox/lifterlms/blob/7.6.0/templates/myaccount/my-orders.php)\n\n\nv7.5.3 - 2024-02-22\n-------------------\n\n##### Bug Fixes\n\n+ Fix fatal error when rendering single course page with reviews enabled. [#2604](https://github.com/gocodebox/lifterlms/issues/2604)\n\n\nv7.5.2 - 2024-02-16\n-------------------\n\n##### Updates and Enhancements\n\n+ Added product images for Aircraft and Memberlite.\n+ Updates LifterLMS Rest to [v1.0.0](https://make.lifterlms.com/2024/01/22/lifterlms-rest-api-version-1-0-0/).\n\n##### Bug Fixes\n\n+ Adds error handling when taking a quiz in case of temporary server error or internet issue.\n\n##### Security Fixes\n\n+ Reviews handler now checks nonces and user limits. Thanks, Francesco Carlucci at Wordfence.\n\n##### Updated Templates\n\n+ [templates/emails/footer.php](https://github.com/gocodebox/lifterlms/blob/7.5.2/templates/emails/footer.php)\n\n\nv7.5.1 - 2024-01-24\n-------------------\n\n##### Updates and Enhancements\n\n+ Added action and description links to the plugins page.\n\n##### Bug Fixes\n\n+ Style updates for buttons in editor.\n+ Fixed logic to validate that the terms page exists before adding to email footer.\n+ Removed .clear styles since WordPress already sets them by default. [#2573](https://github.com/gocodebox/lifterlms/issues/2573)\n+ Improved image appearance in quiz multiple choice and image choice question types. [#2588](https://github.com/gocodebox/lifterlms/issues/2588)\n\n##### Security Fixes\n\n+ Added nonce for course clone link. Thanks, Dhabaleshwar Das.\n\n##### Updated Templates\n\n+ [templates/emails/footer.php](https://github.com/gocodebox/lifterlms/blob/7.5.1/templates/emails/footer.php)\n\n\nv7.5.0 - 2023-11-05\n-------------------\n\n##### New Features\n\n+ Added `LLMS_Add_On::get_image()` method to get the addon and author image. [#2511](https://github.com/gocodebox/lifterlms/issues/2511)\n+ Added a paragraph to show Number of lessons in a course at Course Catalog and My Courses. [#2434](https://github.com/gocodebox/lifterlms/issues/2434)\n\n##### Updates and Enhancements\n\n+ Updates LifterLMS Blocks to [v2.5.2](https://make.lifterlms.com/2023/11/01/lifterlms-blocks-version-2-5-2/).\n+ Bundled Add-ons & More Banners/Author Images in Core LifterLMS. [#2511](https://github.com/gocodebox/lifterlms/issues/2511)\n+ Updates LifterLMS Rest to [v1.0.0-beta.29](https://make.lifterlms.com/2023/10/24/lifterlms-rest-api-version-1-0-0-beta-29/).\n+ Update Action Scheduler to version 3.5.4. To improve compatibility with PHP 8.2.\n\n##### Bug Fixes\n\n+ Fixed checking for the wrong function name when defining the pluggable function `lifterlms_student_dashboard`. [#2550](https://github.com/gocodebox/lifterlms/issues/2550)\n+ Only show LifterLMS-authored Addons in All section.\n+ Improved compatibility with WordPress 6.4 by using `traverse_and_serialize_blocks` in place of the deprecated `_inject_theme_attribute_in_block_template_content`.\n+ PHP 8.2 compatibility fix: Fixed creation of dynamic property `LLMS_Meta_Box_Access::$_saved`.\n\n##### Developer Notes\n\n+ Added `LLMS_Payment_Gateway::can_process_access_plan()` method to determine if an access plan can be processed by the gateway. Also added the filter hook `llms_can_gateway_process_access_plan` to filter its result.\n+ Added a check on whether the gateway can process a specific plan when purchasing a plan, or switching the payment gateway of a recurring payment.\n+ Added action hook `llms_checkout_form_gateway_cant_process_plan` fired on the checkout form gateways section, when a gateway cannot process a specific plan.\n+ Added new filter hook `llms_unschedule_recurring_payment_on_access_pan_expiration` to control whether or not the recurring payments fo an order need to be unscheduled when the related access plan expires (`true` by default).\n+ Added 'favorites' in User postmeta for getting all user's favorites.\n+ Added filter `llms_course_syllabus_lesson_favorite_visibility` for disabling favorites in syllabus view.\n+ Added filter `llms_is_$object_type_favorite` to change object's (lesson, student, course) favorite boolean value.\n+ Added `llms_lesson_preview_before_title` and `llms_lesson_preview_after_title` action hooks.\n+ Added function `llms_template_syllabus_favorite_lesson_preview`.\n+ Added filter `llms_favorites_enabled` to enable/disable Favorites feature.\n+ Removed references to the unused quiz's property `random_answers`. Thanks [@AlexVCS](https://github.com/AlexVCS)! [#2552](https://github.com/gocodebox/lifterlms/issues/2552)\n+ Improved some unit tests compatibility with PHP 8.2.\n\n##### Security Fixes\n\n+ Improved security when exporting a reporting table: make sure to avoid path traversals. Thanks [Huseyin Tintas (stif)](https://linkedin.com/in/huseyintintas)!\n\n##### Updated Templates\n\n+ [templates/checkout/form-gateways.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/checkout/form-gateways.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/checkout/form-switch-source.php)\n+ [templates/content-single-lesson-before.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/content-single-lesson-before.php)\n+ [templates/course/favorite.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/course/favorite.php)\n+ [templates/course/length.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/course/length.php)\n+ [templates/course/lesson-count.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/course/lesson-count.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/course/lesson-preview.php)\n+ [templates/loop/content.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/loop/content.php)\n+ [templates/myaccount/dashboard.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/myaccount/dashboard.php)\n+ [templates/myaccount/my-favorites.php](https://github.com/gocodebox/lifterlms/blob/7.5.0/templates/myaccount/my-favorites.php)\n\n\nv7.4.2 - 2023-10-06\n-------------------\n\n##### Developer Notes\n\n+ Fixing issues in the 7.4.1 release.\n\n\nv7.4.1 - 2023-10-06\n-------------------\n\n##### New Features\n\n+ Added new admin Resources page.\n\n##### Bug Fixes\n\n+ Fixed possible issues when cloning a course containing a quiz built with the Advanced Quizzes addon, after disabling it.\n\n##### Developer Notes\n\n+ Moved attempt randomization logic into the new static method `LLMS_Quiz_Attempt::randomize_attempt_questions()`.\n+ Added filter hook `llms_quiz_attempt_questions_array` to allow filtering the quiz attempt's question arrays.\n\n\nv7.4.0 - 2023-10-03\n-------------------\n\n##### New Features\n\n+ Added method `LLMS_Quiz::get_questions_count()` for getting count of questions.\n+ Added support for the upcoming \"Question Bank\" feature of the LifterLMS Advanced Quizzes add-on.\n\n##### Updates and Enhancements\n\n+ Added `nocache_headers()` to prevent browser caching for temporary redirects.\n\n##### Bug Fixes\n\n+ Added \"Chaiyaphum\" province for the Thailand. [#2527](https://github.com/gocodebox/lifterlms/issues/2527)\n\n##### Developer Notes\n\n+ Course Builder: Correctly get/set (and track changes of) Backbone's model properties which are objects.\n+ Added filter hook `llms_admin_show_header` to allow 3rd parties filtering whether or not to show the branded header in the admin.\n+ Added filter `llms_generator_new_post_data`, to allow third parties to filter the data used when creating a new post while cloning/exporting a course or lesson.\n+ Abstracted the `LLMS_Admin_Setup_Wizard` class, added the class `LLMS_Abstract_Admin_Wizard`.\n+ Added filter `llms_quiz_attempt_questions_randomize` to enable/disable questions randomize.\n+ Added filter `llms_quiz_attempt_questions` to modify the questions array for the quiz.\n+ Added filter `llms_quiz_questions_count` to filter the quiz's question count.\n\n##### Updated Templates\n\n+ [templates/quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/7.4.0/templates/quiz/meta-information.php)\n\n\nv7.3.0 - 2023-08-08\n-------------------\n\n##### Updates and Enhancements\n\n+ When a notice is shown for an access plan on the course edit screen (e.g. When using the WooCommerce integration and no product has been associated to the access plan.) Also display a warning icon next to the access plan title.\n+ Made sure only who can `view_others_lifterlms_reports` will be able to see the analytics widget content in the WordPress admin.\n+ Better rounding of float values on some reporting screens.\n+ Avoid creating a post revision when cloning a course/lesson.\n+ When creating pages via `llms_create_pages()`: strip all tags from the page title and slash the page data prior to inserting the page in the db via `wp_insert_post()` to prevent slashes from being stripped from the page title.\n+ Updated the WordPress tested version up to 6.3.\n+ Improved compatibility with the Divi theme by fixing an issue with the quiz attempt result clarifications not being visible when the Divi option `Defer jQuery And jQuery Migrate` was enabled. [#2470](https://github.com/gocodebox/lifterlms/issues/2470)\n\n##### Bug Fixes\n\n+ Fix spacer block when creating new certificate templates in WP 6.3.\n+ Fixed PHP Warning when no course/membership catalog page was set or if the selected page doesn't exist anymore. [#2496](https://github.com/gocodebox/lifterlms/issues/2496)\n+ Don't include WordPress default sidebar.php template when using a block theme. [#2488](https://github.com/gocodebox/lifterlms/issues/2488)\n+ Updated Kazakhstani Tenge's currency symbol. [#2475](https://github.com/gocodebox/lifterlms/issues/2475)\n+ Make the dashboard widget visible only if the current user has LMS Manager capabilities. [#2500](https://github.com/gocodebox/lifterlms/issues/2500)\n+ Fixed issue with LifterLMS Navigation Link block and block visibility settings. [#2474](https://github.com/gocodebox/lifterlms/issues/2474)\n+ Use student dashboard as default value for navigation link block. [#2465](https://github.com/gocodebox/lifterlms/issues/2465)\n+ Fixed typo in a function name that could potentially produce a fatal. Thanks [@kamalahmed](https://github.com/kamalahmed)!\n\n##### Developer Notes\n\n+ Added the parameter `$tab` (ID/slug of the tab) to the `lifterlms_reporting_tab_cap` filter hook. Thanks [@sapayth](https://github.com/sapayth)! [#2468](https://github.com/gocodebox/lifterlms/issues/2468)\n+ Added new filter hook `llms_can_analytics_widget_be_processed` that will allow to filter whether or not an analytics widget can be processed/displayed.\n+ Added new filter `llms_install_get_pages`.\n+ Added new public static method `LLMS_Admin_Dashboard_Widget::get_dashboard_widget_data()`.\n+ Added `llms_dashboard_checklist` and `llms_dashboard_widget_data` filters to adjust dashboard content. [#2491](https://github.com/gocodebox/lifterlms/issues/2491)\n\n##### Updated Templates\n\n+ [templates/admin/reporting/tabs/widgets.php](https://github.com/gocodebox/lifterlms/blob/7.3.0/templates/admin/reporting/tabs/widgets.php)\n+ [templates/global/sidebar.php](https://github.com/gocodebox/lifterlms/blob/7.3.0/templates/global/sidebar.php)\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/7.3.0/templates/quiz/results-attempt-questions-list.php)\n\n\nv7.2.1 - 2023-06-13\n-------------------\n\n##### Updates and Enhancements\n\n+ Updated LifterLMS Blocks to [2.5.1](https://make.lifterlms.com/2023/06/13/lifterlms-blocks-version-2-5-1/). [#2461](https://github.com/gocodebox/lifterlms/issues/2461)\n\n\nv7.2.0 - 2023-06-07\n-------------------\n\n##### New Features\n\n+ Added `LLMS_ASSETS_VERSION` constant for cache busting.\n+ Add course builder explainer video and lesson IDs.\n+ Add new dashboard widget.\n+ Added query to remove order comments on plugin uninstall when the constant `LLMS_REMOVE_ALL_DATA` is set to `true`. [#2322](https://github.com/gocodebox/lifterlms/issues/2322)\n+ Added support for showing multiple difficulties when using Gutenberg Editor. [#2433](https://github.com/gocodebox/lifterlms/issues/2433)\n+ Add shortcode wrapper blocks.\n+ Added new navigation link block.\n+ Added `llms_is_editor_block_rendering` helper function.\n+ Added `llms_is_block_editor` helper function.\n\n##### Updates and Enhancements\n\n+ Adjusted `llms_modify_dashboard_pagination_links_disable` filter to return false only on Dashboard page.\n+ Updates LifterLMS REST to [v1.0.0-beta.27](https://make.lifterlms.com/2023/05/31/lifterlms-rest-api-version-1-0-0-beta-27).\n+ Raised the minimum support WordPress core version to 5.9.\n+ Updates LifterLMS Blocks to [2.5.0](https://make.lifterlms.com/2023/06/06/lifterlms-blocks-version-2-5-0/).\n\n##### Bug Fixes\n\n+ Fixed LifterLMS specific block templates not correctly working on Windows file system.\n+ Added `function_exists` check for `llms_blocks_is_post_migrated()`.\n+ Update so dismissed notifications don't remain on viewport top layer.\n+ Made sure to always enqueue iziModal assets when rendering achievements cards.\n\n##### Developer Notes\n\n+ Added new filter hook `llms_builder_settings` to filter the settings passed to the course builder.\n\n##### Updated Templates\n\n+ [templates/admin/reporting/tabs/widgets.php](https://github.com/gocodebox/lifterlms/blob/7.2.0/templates/admin/reporting/tabs/widgets.php)\n+ [templates/course/syllabus.php](https://github.com/gocodebox/lifterlms/blob/7.2.0/templates/course/syllabus.php)\n\n\nv7.1.4 - 2023-04-28\n-------------------\n\n##### Bug Fixes\n\n+ Fixed an issue that prevented the correct saving of the course length when using the block editor. [#2426](https://github.com/gocodebox/lifterlms/issues/2426)\n\n##### Developer Notes\n\n+ Fixed an issue running unit tests on PHP 7.4 and WordPress 6.2 expecting `render_block()` returning a string while we were applying a filter that returned the boolean `true`.\n\n\nv7.1.3 - 2023-04-25\n-------------------\n\n##### Updates and Enhancements\n\n+ Wrapped some elements in HTML for better styling.\n+ In Course and Lesson settings, replaced outdated URLs to WordPress' documentation about the list of sites you can embed from.\n+ Updated few Italian province names. [#2256](https://github.com/gocodebox/lifterlms/issues/2256)\n+ Avoid use of inline styles in course reviews. [#410](https://github.com/gocodebox/lifterlms/issues/410)\n\n##### Bug Fixes\n\n+ Fixed \"Unsaved Data\" warning when adding vouchers. [#2394](https://github.com/gocodebox/lifterlms/issues/2394)\n+ Fixed \"Course Length\" and \"Difficulty\" fields visible in the Block Editor which is meant for Classic Editor. [#2174](https://github.com/gocodebox/lifterlms/issues/2174)\n+ Added missing `$post_id` parameter to the `the_title` filter hook when retrieving a form title. [#2332](https://github.com/gocodebox/lifterlms/issues/2332)\n+ Added missing Armed Forces options to the US States dropdown in the Billing information form. [#2325](https://github.com/gocodebox/lifterlms/issues/2325)\n+ Using `strpos()` instead of `str_starts_with()` for compatibility. [#2415](https://github.com/gocodebox/lifterlms/issues/2415)\n\n##### Developer Notes\n\n+ Added helper function `llms_get_floats_rounding_precision()` to return precision for rounding off floating values and filter hook `lifterlms_floats_rounding_precision` to filter precision value in reporting. [#2237](https://github.com/gocodebox/lifterlms/issues/2237)\n+ Added `lifterlms_dashboard_memberships_not_enrolled_text` filter hook to allow altering the message displaying on the student dashboard when the current user is not enrolled in any memberships. [#2396](https://github.com/gocodebox/lifterlms/issues/2396)\n+ Added `lifterlms_dashboard_courses_not_enrolled_text` filter hook to allow altering the message displaying on the student dashboard when the current user is not enrolled in any courses. [#2396](https://github.com/gocodebox/lifterlms/issues/2396)\n\n##### Updated Templates\n\n+ [templates/course/syllabus.php](https://github.com/gocodebox/lifterlms/blob/7.1.3/templates/course/syllabus.php)\n\n\nv7.1.2 - 2023-03-27\n-------------------\n\n##### Updates and Enhancements\n\n+ Making the LifterLMS logo link to the LifterLMS.com site.\n\n##### Bug Fixes\n\n+ Fix bug in `llms_featured_img` function when featured image file is not available. [#2381](https://github.com/gocodebox/lifterlms/issues/2381)\n+ Fixed manual certificates awarding broken when using the block editor. [#2386](https://github.com/gocodebox/lifterlms/issues/2386)\n\n\nv7.1.1 - 2023-03-13\n-------------------\n\n##### Bug Fixes\n\n+ Fixed notice display on WooCommerce dashboard pages.\n+ Fixed View button URL when using WP in subdirectory.\n+ Fixed blank System Report's copy for Support.\n\n\nv7.1.0 - 2023-03-02\n-------------------\n\n##### New Features\n\n+ Added lessons count column on the Courses post list table.\n+ Added a new Dashboard page under the LifterLMS menu in the admin, whicih includes recent activity widgets and links to useful resources.\n+ Added link to the course builder for each lesson on the Lessons post list table. Also added a link to either edit or add a quiz.\n\n##### Updates and Enhancements\n\n+ Updates LifterLMS Helper to [v3.5.0](https://make.lifterlms.com/2023/02/28/lifterlms-helper-version-3-5-0/).\n+ Make the LifterLMS menu meta box initially available on Appearance -> Menus.\n+ Updates LifterLMS REST to [v1.0.0-beta.26](https://make.lifterlms.com/2023/02/28/lifterlms-rest-api-version-1-0-0-beta-26/).\n\n##### Bug Fixes\n\n+ Catch possible fatal when trying to display a \"broken\" basic notification and set its status to 'error' so that it'll be excluded from the next fetches.\n+ Catch possible fatal when sending notification emails and in that case remove from the queue the item that produced it.\n+ Fix cloned course retaining original course's ID in some restriction messages.\n+ Fixed possible admin notices duplication when activating/deactivating or installing add-ons from the page Add-ons & more.\n+ Avoided setting the `llms-tracking` cookie when there are no events to track.\n+ Updated styles across the entire plugin.\n+ Updated Add-ons & more list to hide old (uncategorized) products.\n\n##### Deprecations\n\n+ Deprecated methods `LLMS_Admin_Notices_Core::sidebar_support()` and `LLMS_Admin_Notices_Core::clear_sidebar_notice()`.\n+ Removed notice for theme sidebar support.\n\n##### Developer Notes\n\n+ The function `llms_is_user_enrolled()` will always return `false` for non existing users. While, before, it could return `true` if a now removed user was enrolled into a the given course or membership.\n+ Added new `LLMS_Course::get_lessons_count()` method. It can be used in place of `count( LLMS_Course::get_lessons() )` to improve performance.\n+ Fixed compatibility with PHP 8.1 by using an empty string as menu parent page for the course builder submenu page in place of NULL.\n+ Avoid passing null values to `urlencode()` and `urldecode()` that would produce PHP warnings on PHP 8.1+.\n+ Added `$autoload` parameter to the function `llms_get_student`.\n\n##### Performance Improvements\n\n+ Improve performance when querying notifications via the LLMS_Notifications_Query and there's no need to count the total notifications found, or for pagination information.\n+ Immediately return false when running `llms_is_user_enrolled()` on logged out or no longer existing users, avoiding running additional DB queries e.g. when displaying course or membership catalogs for visitors.\n+ Skip counting the total transactions found when retrieving the last or the first transaction for an order.\n\n##### Updated Templates\n\n+ templates/admin/reporting/nav-filters.php\n+ templates/admin/reporting/reporting.php\n+ templates/admin/reporting/tabs/courses/course.php\n+ templates/admin/reporting/tabs/memberships/membership.php\n+ templates/admin/reporting/tabs/quizzes/quiz.php\n+ templates/admin/reporting/tabs/students/student.php\n+ templates/admin/reporting/tabs/widgets.php\n+ templates/checkout/form-confirm-payment.php\n\n\nv7.0.1 - 2022-11-14\n-------------------\n\n##### Bug Fixes\n\n+ Fixed a fatal error encountered on the payment confirmation screen when attempting to confirm a non-existent order. [#2093](https://github.com/gocodebox/lifterlms/issues/2093)\n+ Use `sanitize_file_name()` in favor of `sanitize_title()` for generating the file name of reporting table export files. [#1540](https://github.com/gocodebox/lifterlms/issues/1540)\n+ Resolved conflict encountered on post edit screens when using LifterLMS, Yoast SEO, and the Classic Editor plugin. [#2298](https://github.com/gocodebox/lifterlms/issues/2298)\n\n##### Developer Notes\n\n+ A stub method, `get_title()` has been added to the `LLMS_Abstract_Exportable_Admin_Table` abstract class. This method should be defined by any extending classes and will throw a `_doing_it_wrong()` error when undefined.\n+ Added new filter to allow customizing which user roles are affected by the `LLMS_Admin_Menus::instructor_menu_hack` function.\n\n\nv7.0.0 - 2022-10-04\n-------------------\n\n##### New Features\n\n+ Added handling for admin settings options that store their option values in a nested array.\n+ Added new AJAX checkout and payment source switching endpoints for payment gateways to utilize instead of the preexisting synchronous form submission methods.\n+ On purchase completed retrieve the redirection URL from the INPUT_POST 'redirect' variable, if no 'redirect' variable is passed via INPUT_GET. The INPUT_POST 'redirect' variable comes from the new checkout form's hidden field 'redirect' populated with LLMS_Access_Plan::get_redirection_url(). [#2229](https://github.com/gocodebox/lifterlms/issues/2229)\n\n##### Updates and Enhancements\n\n+ Full Site Editing: **[BREAKING]** The wrappers in the custom header and footer templates have been changed to the semantic HTML tags `<header>` and `<footer>` in favor of default `<div>` tags. [#2281](https://github.com/gocodebox/lifterlms/issues/2281)\n+ When an order post is restored from the trash its post status will now be \"llms-pending\" in favor of the default \"draft\" status.\n\n##### Bug Fixes\n\n+ Fixed unclosed checkout div wrapper on empty cart. [#2277](https://github.com/gocodebox/lifterlms/issues/2277)\n+ Don't attempt to lookup the default payment gateway from user meta data.\n+ Fixed required fields duplication when the form is a child of a `.wp-block-column` element. [#2134](https://github.com/gocodebox/lifterlms/issues/2134)\n+ Fixed an issue that prevented disabling the access plan’s option, Override Membership Redirects, once enabled. [#2234](https://github.com/gocodebox/lifterlms/issues/2234)\n+ Disabled `scroll-behavior: smooth` on checkout screen to address form element validity checking issues on Chromium-based browsers. [#2206](https://github.com/gocodebox/lifterlms/issues/2206)\n\n##### Deprecations\n\n+ Deprecated `LLMS_Controller_Orders::switch_payment_source()` in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Deprecated the `lifterlms_update_option_{$type}` action in favor of the `llms_update_option_{$type}` filter.\n+ Method `LLMS_Controller_Orders::confirm_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::confirm_pending_order()`.\n+ Method `LLMS_Controller_Orders::create_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::create_pending_order()`.\n+ Method `LLMS_Controller_Orders::switch_payment_source()` is deprecated in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Passing jQuery selections into the `window.LLMS.Spinner` functions is deprecated. Use JS `Elements` or selection strings parseable by `document.querySelector()` instead.\n+ Deprecated hook `llms_{$method}_title` in favor of `llms_{$method}_refund_title`.\n\n##### Developer Notes\n\n+ Added admin settings helper function, `llms_get_dashicon_link()`, intended to enable the addition of external resource helper links to settings field descriptions.\n+ The `LLMS_Student` object can be instantiated as an empty object and bypass current user autoloading. In the future this may affect integrations using the `lifterlms_new_pending_order` action hook which will receive an \"empty\" student object during order setup by gateways utilizing new AJAX-powered checkout endpoints.\n+ Added a filter, `llms_gateway_{$this->id}_logging_enabled`, which will allow force enabling/disabling of gateway logging functions.\n+ Improved payment gateway secure string logging by adding a method, `add_secure_string()` allowing developers to add secure strings during runtime without the necessity of registering the strings using filters.\n+ Introduces new function `llms_is_option_secure()` for determining if an \"secured\" option is defined in a \"secure\" manner.\n+ Implemented new gateway feature: `modify_recurring_payments`. [#2176](https://github.com/gocodebox/lifterlms/issues/2176)\n+ Added two new parameters to LLMS_Access_Plan::get_redirection_url() - `$encode` to optionally get a raw (not encoded) URL. - `$querystring_only` to optionally get only the redirect URL if set via NPUT_GET variable.\n+ Added new parameter `$querystring_only` to the filter hook `llms_plan_get_checkout_redirection`.\n+ Admin settings fields now display `after_html` for additional field types which support `desc`.\n+ The CSS for `.llms-spinning` and `.llms-spinner` elements is no longer loaded as part of the `lifterlms.css` and `admin.css` files, instead it is loaded dynamically when `window.LLMS.Spinner` functions are called. In some cases CSS overrides to these elements which relied on CSS rule load order may no longer successfully override the default CSS rules. These overrides may need to be updated to have more specific selectors in order to ensure the overrides are retained.\n+ The Javascript object, `window.LLMS.Spinner`, has been converted to a module accessible from the same variable.\n+ The `window.LLMS.Spinner` methods now accept JS Elements and selector strings parseable by `document.querySelector()` in addition to jQuery selections.\n+ Added new filter `llms_transaction_can_be_refunded` enabling custom refund restrictions to be applied to a transaction.\n\n##### Updated Templates\n\n+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/archive-course.html)\n+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/archive-llms_membership.html)\n+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/single-no-access.html)\n+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/taxonomy-course_cat.html)\n+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/taxonomy-course_difficulty.html)\n+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/taxonomy-course_tag.html)\n+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/taxonomy-course_track.html)\n+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/taxonomy-membership_cat.html)\n+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/block-templates/taxonomy-membership_tag.html)\n+ [templates/checkout/form-gateways.php](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/checkout/form-gateways.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/checkout/form-switch-source.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/7.0.0/templates/myaccount/view-order-actions.php)\n\n\nv6.11.0 - 2022-09-22\n--------------------\n\n##### Updates and Enhancements\n\n+ Since version 6.0.0, the Certificate Title Block provided the option to use four Google-hosted fonts. These fonts will now be served from the site's server in favor of serving them from the Google Fonts CDN. For more information about this change, please refer to https://make.wordpress.org/themes/2022/06/18/complying-with-gdpr-when-using-google-fonts/. If you wish to continue loading fonts from Google's CDN, add the following code to your functions.php file: `add_filter( 'llms_use_google_webfonts', '__return_true' );`. [#2189](https://github.com/gocodebox/lifterlms/issues/2189)\n+ Upgraded included library, `@woocommerce/action-scheduler`, to version [3.5.2](https://github.com/woocommerce/action-scheduler/releases/tag/3.5.2).\n\n##### Bug Fixes\n\n+ Fixed a division by zero error encountered on quiz reporting screens for quizzes with 0 total available points. [#2270](https://github.com/gocodebox/lifterlms/issues/2270)\n\n\nv7.0.0-rc.1 - 2022-09-14\n------------------------\n\n##### New Features\n\n+ Added handling for admin settings options that store their option values in a nested array.\n+ Added new AJAX checkout and payment source switching endpoints for payment gateways to utilize instead of the preexisting synchronous form submission methods.\n+ On purchase completed retrieve the redirection URL from the INPUT_POST 'redirect' variable, if no 'redirect' variable is passed via INPUT_GET. The INPUT_POST 'redirect' variable comes from the new checkout form's hidden field 'redirect' populated with LLMS_Access_Plan::get_redirection_url(). [#2229](https://github.com/gocodebox/lifterlms/issues/2229)\n\n##### Updates and Enhancements\n\n+ When an order post is restored from the trash its post status will now be \"llms-pending\" in favor of the default \"draft\" status.\n\n##### Bug Fixes\n\n+ Don't attempt to lookup the default payment gateway from user meta data.\n+ Fixed an issue that prevented disabling the access plan’s option, Override Membership Redirects, once enabled. [#2234](https://github.com/gocodebox/lifterlms/issues/2234)\n+ Disabled `scroll-behavior: smooth` on checkout screen to address form element validity checking issues on Chromium-based browsers. [#2206](https://github.com/gocodebox/lifterlms/issues/2206)\n\n##### Deprecations\n\n+ Deprecated `LLMS_Controller_Orders::switch_payment_source()` in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Deprecated the `lifterlms_update_option_{$type}` action in favor of the `llms_update_option_{$type}` filter.\n+ Method `LLMS_Controller_Orders::confirm_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::confirm_pending_order()`.\n+ Method `LLMS_Controller_Orders::create_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::create_pending_order()`.\n+ Method `LLMS_Controller_Orders::switch_payment_source()` is deprecated in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Passing jQuery selections into the `window.LLMS.Spinner` functions is deprecated. Use JS `Elements` or selection strings parseable by `document.querySelector()` instead.\n+ Deprecated hook `llms_{$method}_title` in favor of `llms_{$method}_refund_title`.\n\n##### Developer Notes\n\n+ Added admin settings helper function, `llms_get_dashicon_link()`, intended to enable the addition of external resource helper links to settings field descriptions.\n+ The `LLMS_Student` object can be instantiated as an empty object and bypass current user autoloading. In the future this may affect integrations using the `lifterlms_new_pending_order` action hook which will receive an \"empty\" student object during order setup by gateways utilizing new AJAX-powered checkout endpoints.\n+ Added a filter, `llms_gateway_{$this->id}_logging_enabled`, which will allow force enabling/disabling of gateway logging functions.\n+ Improved payment gateway secure string logging by adding a method, `add_secure_string()` allowing developers to add secure strings during runtime without the necessity of registering the strings using filters.\n+ Introduces new function `llms_is_option_secure()` for determining if an \"secured\" option is defined in a \"secure\" manner.\n+ Implemented new gateway feature: `modify_recurring_payments`. [#2176](https://github.com/gocodebox/lifterlms/issues/2176)\n+ Added two new parameters to LLMS_Access_Plan::get_redirection_url() - `$encode` to optionally get a raw (not encoded) URL. - `$querystring_only` to optionally get only the redirect URL if set via NPUT_GET variable.\n+ Added new parameter `$querystring_only` to the filter hook `llms_plan_get_checkout_redirection`.\n+ Admin settings fields now display `after_html` for additional field types which support `desc`.\n+ The CSS for `.llms-spinning` and `.llms-spinner` elements is no longer loaded as part of the `lifterlms.css` and `admin.css` files, instead it is loaded dynamically when `window.LLMS.Spinner` functions are called. In some cases CSS overrides to these elements which relied on CSS rule load order may no longer successfully override the default CSS rules. These overrides may need to be updated to have more specific selectors in order to ensure the overrides are retained.\n+ The Javascript object, `window.LLMS.Spinner`, has been converted to a module accessible from the same variable.\n+ The `window.LLMS.Spinner` methods now accept JS Elements and selector strings parseable by `document.querySelector()` in addition to jQuery selections.\n+ Added new filter `llms_transaction_can_be_refunded` enabling custom refund restrictions to be applied to a transaction.\n\n##### Updated Templates\n\n+ [templates/checkout/form-gateways.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-rc.1/templates/checkout/form-gateways.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-rc.1/templates/checkout/form-switch-source.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-rc.1/templates/myaccount/view-order-actions.php)\n\n\nv6.10.2 - 2022-09-14\n--------------------\n\n##### Updates and Enhancements\n\n+ Updated `woocommerce/action-scheduler` to version [3.5.1](https://github.com/woocommerce/action-scheduler/releases/tag/3.5.1).\n\n##### Security Fixes\n\n+ Fixed a data sanitization issue related to achievement permalinks.\n\n\nv6.10.1 - 2022-09-07\n--------------------\n\n##### Bug Fixes\n\n+ Fixed a PHP warning raised when logging errors during email notification dispatch. [#2250](https://github.com/gocodebox/lifterlms/issues/2250)\n+ Fixed issue preventing one-time orders for being included in membership revenue reporting widgets. [#2254](https://github.com/gocodebox/lifterlms/issues/2254)\n\n\nv7.0.0-beta.1 - 2022-08-29\n--------------------------\n\n##### New Features\n\n+ Added handling for admin settings options that store their option values in a nested array.\n+ Added new AJAX checkout and payment source switching endpoints for payment gateways to utilize instead of the preexisting synchronous form submission methods.\n+ On purchase completed retrieve the redirection URL from the INPUT_POST 'redirect' variable, if no 'redirect' variable is passed via INPUT_GET. The INPUT_POST 'redirect' variable comes from the new checkout form's hidden field 'redirect' populated with LLMS_Access_Plan::get_redirection_url(). [#2229](https://github.com/gocodebox/lifterlms/issues/2229)\n\n##### Updates and Enhancements\n\n+ When an order post is restored from the trash its post status will now be \"llms-pending\" in favor of the default \"draft\" status.\n\n##### Bug Fixes\n\n+ Don't attempt to lookup the default payment gateway from user meta data.\n+ Fixed an issue that prevented disabling the access plan’s option, Override Membership Redirects, once enabled. [#2234](https://github.com/gocodebox/lifterlms/issues/2234)\n+ Disabled `scroll-behavior: smooth` on checkout screen to address form element validity checking issues on Chromium-based browsers. [#2206](https://github.com/gocodebox/lifterlms/issues/2206)\n\n##### Deprecations\n\n+ Deprecated `LLMS_Controller_Orders::switch_payment_source()` in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Deprecated the `lifterlms_update_option_{$type}` action in favor of the `llms_update_option_{$type}` filter.\n+ Method `LLMS_Controller_Orders::confirm_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::confirm_pending_order()`.\n+ Method `LLMS_Controller_Orders::create_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::create_pending_order()`.\n+ Method `LLMS_Controller_Orders::switch_payment_source()` is deprecated in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Passing jQuery selections into the `window.LLMS.Spinner` functions is deprecated. Use JS `Elements` or selection strings parseable by `document.querySelector()` instead.\n+ Deprecated hook `llms_{$method}_title` in favor of `llms_{$method}_refund_title`.\n\n##### Developer Notes\n\n+ Added admin settings helper function, `llms_get_dashicon_link()`, intended to enable the addition of external resource helper links to settings field descriptions.\n+ The `LLMS_Student` object can be instantiated as an empty object and bypass current user autoloading. In the future this may affect integrations using the `lifterlms_new_pending_order` action hook which will receive an \"empty\" student object during order setup by gateways utilizing new AJAX-powered checkout endpoints.\n+ Added a filter, `llms_gateway_{$this->id}_logging_enabled`, which will allow force enabling/disabling of gateway logging functions.\n+ Improved payment gateway secure string logging by adding a method, `add_secure_string()` allowing developers to add secure strings during runtime without the necessity of registering the strings using filters.\n+ Introduces new function `llms_is_option_secure()` for determining if an \"secured\" option is defined in a \"secure\" manner.\n+ Implemented new gateway feature: `modify_recurring_payments`. [#2176](https://github.com/gocodebox/lifterlms/issues/2176)\n+ Added two new parameters to LLMS_Access_Plan::get_redirection_url() - `$encode` to optionally get a raw (not encoded) URL. - `$querystring_only` to optionally get only the redirect URL if set via NPUT_GET variable.\n+ Added new parameter `$querystring_only` to the filter hook `llms_plan_get_checkout_redirection`.\n+ Admin settings fields now display `after_html` for additional field types which support `desc`.\n+ The CSS for `.llms-spinning` and `.llms-spinner` elements is no longer loaded as part of the `lifterlms.css` and `admin.css` files, instead it is loaded dynamically when `window.LLMS.Spinner` functions are called. In some cases CSS overrides to these elements which relied on CSS rule load order may no longer successfully override the default CSS rules. These overrides may need to be updated to have more specific selectors in order to ensure the overrides are retained.\n+ The Javascript object, `window.LLMS.Spinner`, has been converted to a module accessible from the same variable.\n+ The `window.LLMS.Spinner` methods now accept JS Elements and selector strings parseable by `document.querySelector()` in addition to jQuery selections.\n+ Added new filter `llms_transaction_can_be_refunded` enabling custom refund restrictions to be applied to a transaction.\n\n##### Updated Templates\n\n+ [templates/checkout/form-gateways.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-beta.1/templates/checkout/form-gateways.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-beta.1/templates/checkout/form-switch-source.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-beta.1/templates/myaccount/view-order-actions.php)\n\n\nv6.10.0 - 2022-08-29\n--------------------\n\n##### Updates and Enhancements\n\n+ Updtaed woocommerce/action-scheduler to version [3.5.0](https://github.com/woocommerce/action-scheduler/releases/tag/3.5.0).\n+ Upgrades the bundled `quill-wordcount` module to version 2.0, addressing an issue encountered when counting words with non-Latin character languages.\n\n##### Bug Fixes\n\n+ Make `<pre>` elements in quiz attempt results scrollable.\n+ Make sure the current user can edit the lesson, when changing its completion status from the admin reporting.\n+ Added missing textodmain for the string 'Move {post_title} to the Trash'. [#2224](https://github.com/gocodebox/lifterlms/issues/2224)\n+ Fixed PHP fatal error when quick editing an award. [#2231](https://github.com/gocodebox/lifterlms/issues/2231)\n+ Updated Spain's provinces list. [#2243](https://github.com/gocodebox/lifterlms/issues/2243)\n\n##### Deprecations\n\n+ The files `assets/vendor/quill/quill.module.wordcount.js` and `assets/vendor/quill/quill.module.wordcount.min.js` are to be removed in the next major release. Instead of loading these files directly, use `wp_enqueue_script( 'llms-quill-wordcount' )`.\n\n\nv6.9.0 - 2022-07-28\n-------------------\n\n##### Updates and Enhancements\n\n+ Removed site-wide font-weight styles targeting `<h1>` through `<h6>` elements. [#2217](https://github.com/gocodebox/lifterlms/issues/2217)\n\n##### Bug Fixes\n\n+ Fixed issue preventing decimals from being used for coupon discount amounts. [#2149](https://github.com/gocodebox/lifterlms/issues/2149)\n+ Added AR (Arezzo) to Italy's states list. [#2214](https://github.com/gocodebox/lifterlms/issues/2214)\n\n\nv7.0.0-alpha.4 - 2022-07-18\n---------------------------\n\n+ Fixed error causing recurring payment reschedules to fail with a fatal error.\n\n\nv7.0.0-alpha.3 - 2022-07-16\n---------------------------\n\n+ Add max-length sanitization to admin settings which specify a max length.\n+ Fixed invalid user links on admin order screens when viewing incomplete orders missing a registered user.\n+ Added new function `llms_is_secure()`.\n+ Added `lifterlms-` and `llms-` as automatically stripped prefixed when using `llms_strip_prefixes()`.\n+ Added new temporary metadata, `temp_gateway_ids` to orders for use by gateways when switching payment methods.\n+ Moved `LLMS.Spinner` Javascript into an `@lifterlms/components` module and removed its reliance on jQuery.\n+ Disabled `scroll-behavior: scroll` on checkout screens to address a validity reporting issue on Chromium-based browsers.\n\n\nv6.8.0 - 2022-07-12\n-------------------\n\n##### Bug Fixes\n\n+ Fixed Hello Theme's word-break and spacing for quiz answer options. [#2132](https://github.com/gocodebox/lifterlms/issues/2132)\n+ Fixed text/label alignment in Twenty-Twenty-Two theme.\n+ Fixed regression introduced in version 6.3.0 which prevented the Courses nav item from being customized in the BuddyPress profile nav menu. [#2142](https://github.com/gocodebox/lifterlms/issues/2142)\n\n##### Developer Notes\n\n+ Added new filter `llms_product_get_restrictions` hook to filter the list of restrictions placed on a given product. [#2201](https://github.com/gocodebox/lifterlms/issues/2201)\n\n\nv7.0.0-alpha.2 - 2022-06-23\n---------------------------\n\n##### New Features\n\n+ Added handling for admin settings options that store their option values in a nested array.\n+ Added new AJAX checkout and payment source switching endpoints for payment gateways to utilize instead of the preexisting synchronous form submission methods.\n\n##### Bug Fixes\n\n+ Don't attempt to lookup the default payment gateway from user meta data.\n+ Fixes Hello Theme's word-break and spacing for quiz answer options. Also fixes text/label alignment in Twenty-Twenty-Two Theme. [#2132](https://github.com/gocodebox/lifterlms/issues/2132)\n\n##### Deprecations\n\n+ Deprecated `LLMS_Controller_Orders::switch_payment_source()` in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Deprecated the `lifterlms_update_option_{$type}` action in favor of the `llms_update_option_{$type}` filter.\n+ Method `LLMS_Controller_Orders::confirm_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::confirm_pending_order()`.\n+ Method `LLMS_Controller_Orders::create_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::create_pending_order()`.\n+ Method `LLMS_Controller_Orders::switch_payment_source()` is deprecated in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Deprecated hook `llms_{$method}_title` in favor of `llms_{$method}_refund_title`.\n\n##### Developer Notes\n\n+ Added admin settings helper function, `llms_get_dashicon_link()`, intended to enable the addition of external resource helper links to settings field descriptions.\n+ The `LLMS_Student` object can be instantiated as an empty object and bypass current user autoloading. In the future this may affect integrations using the `lifterlms_new_pending_order` action hook which will receive an \"empty\" student object during order setup by gateways utilizing new AJAX-powered checkout endpoints.\n+ Added a filter, `llms_gateway_{$this->id}_logging_enabled`, which will allow force enabling/disabling of gateway logging functions.\n+ Improved payment gateway secure string logging by adding a method, `add_secure_string()` allowing developers to add secure strings during runtime without the necessity of registering the strings using filters.\n+ Implemented new gateway feature: `modify_recurring_payments`. [#2176](https://github.com/gocodebox/lifterlms/issues/2176)\n+ Admin settings fields now display `after_html` for additional field types which support `desc`.\n+ Added new filter `llms_transaction_can_be_refunded` enabling custom refund restrictions to be applied to a transaction.\n\n##### Updated Templates\n\n+ [templates/checkout/form-gateways.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-alpha.2/templates/checkout/form-gateways.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-alpha.2/templates/checkout/form-switch-source.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-alpha.2/templates/myaccount/view-order-actions.php)\n\n\nv7.0.0-alpha.1 - 2022-06-15\n---------------------------\n\n##### New Features\n\n+ Added new AJAX checkout and payment source switching endpoints for payment gateways to utilize instead of the preexisting synchronous form submission methods.\n\n##### Bug Fixes\n\n+ Don't attempt to lookup the default payment gateway from user meta data.\n\n##### Deprecations\n\n+ Deprecated `LLMS_Controller_Orders::switch_payment_source()` in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Method `LLMS_Controller_Orders::confirm_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::confirm_pending_order()`.\n+ Method `LLMS_Controller_Orders::create_pending_order()` is deprecated in favor of `LLMS_Controller_Checkout::create_pending_order()`.\n+ Method `LLMS_Controller_Orders::switch_payment_source()` is deprecated in favor of `LLMS_Controller_Checkout::switch_payment_source()`.\n+ Deprecated hook `llms_{$method}_title` in favor of `llms_{$method}_refund_title`.\n\n##### Developer Notes\n\n+ The `LLMS_Student` object can be instantiated as an empty object and bypass current user autoloading. In the future this may affect integrations using the `lifterlms_new_pending_order` action hook which will receive an \"empty\" student object during order setup by gateways utilizing new AJAX-powered checkout endpoints.\n+ Improved payment gateway secure string logging by adding a method, `add_secure_string()` allowing developers to add secure strings during runtime without the necessity of registering the strings using filters.\n+ Implemented new gateway feature: `modify_recurring_payments`. [#2176](https://github.com/gocodebox/lifterlms/issues/2176)\n+ Added new filter `llms_transaction_can_be_refunded` enabling custom refund restrictions to be applied to a transaction.\n\n##### Updated Templates\n\n+ [templates/checkout/form-gateways.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-alpha.1/templates/checkout/form-gateways.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-alpha.1/templates/checkout/form-switch-source.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/7.0.0-alpha.1/templates/myaccount/view-order-actions.php)\n\n\nv6.7.0 - 2022-06-09\n-------------------\n\n##### Updates and Enhancements\n\n+ Update LifterLMS Blocks to [v2.4.3](https://make.lifterlms.com/2022/06/09/lifterlms-blocks-version-2-4-3/).\n+ Upgraded Action Scheduler to [v3.4.1](https://github.com/woocommerce/action-scheduler/releases/tag/3.4.1).\n+ Upgraded Action Scheduler to [v3.4.2](https://github.com/woocommerce/action-scheduler/releases/tag/3.4.2).\n\n##### Bug Fixes\n\n+ Fixed a fatal error on PHP 8+ when restoring a post type from revision. [#2164](https://github.com/gocodebox/lifterlms/issues/2164)\n\n\nv6.6.0 - 2022-05-23\n-------------------\n\n##### PHP Minimum Required Version Change\n\n+ **Raised the minimum supported PHP version to 7.4.**\n\n##### WordPress Minimum Required Version Change\n\n+ **Raised the minimum supported WordPress core version to 5.6.**\n\n##### New Features\n\n+ Added support for WordPress 6.0.\n\n##### Bug Fixes\n\n+ Fixed the ability for 3rd party plugins to use the `lifterlms_external_engagement_handler_arguments` and `lifterlms_external_engagement_query_arguments` filters.\n+ Added automatic exclusion of \"no cache\" pages from the WP Engine server-side cache when using \"pretty\" permalinks. [#1717](https://github.com/gocodebox/lifterlms/issues/1717)\n+ Stop subtracting LifterLMS order note comments from global comment counts via the `wp_count_comments` filter on WordPress 6.0 and later. See related WordPress Trac ticket [#19901](https://core.trac.wordpress.org/ticket/19901)\n\n\nv6.5.0 - 2022-05-11\n-------------------\n\n##### Upcoming PHP Version Requirement Change\n\n**This will be the last version of LifterLMS to support PHP 7.3. The next version of LifterLMS, expected before the end of May 2022, will raise the minimum supported PHP version to 7.4. PHP 7.3 reached its official [end of life](https://www.php.net/eol.php) on December 6, 2021. If you are still using PHP 7.3 please upgrade to PHP 7.4 or later as soon as possible.**\n\n##### Updates and Enhancements\n\n+ Updates LifterLMS Rest to [v1.0.0-beta.25](https://make.lifterlms.com/2022/05/11/lifterlms-rest-api-version-1-0-0-beta-25/).\n\n##### Bug Fixes\n\n+ Students who have already completed a lesson will now automatically bypass the lesson's drip restrictions. [#1835](https://github.com/gocodebox/lifterlms/issues/1835)\n+ Properly encode certificate JS localization data. [#2140](https://github.com/gocodebox/lifterlms/issues/2140)\n\n##### Developer Notes\n\n+ Added a new filter, `llms_lesson_drip_bypass_if_completed`, which controls the automatic bypass of drip restrictions for completed lessons. [#1835](https://github.com/gocodebox/lifterlms/issues/1835)\n+ Allow avoiding error return when updating an `LLMS_Post_Model` post meta with the same value as the one stored in the database. [#909](https://github.com/gocodebox/lifterlms/issues/909)\n\n\nv6.4.0 - 2022-04-19\n-------------------\n\n##### Upcoming PHP Version Requirement Change\n\n**LifterLMS will drop support for PHP 7.3 by May, 2022. This will raise the minimum supported PHP version to 7.4. PHP 7.3 reached its official [end of life](https://www.php.net/eol.php) on December 6, 2021. If you are still using PHP 7.3 please upgrade to PHP 7.4 or later as soon as possible.**\n\n##### New Features\n\n+ Any \"secure\" payment gateway options will be automatically masked when written to debug log files.\n\n##### Updates and Enhancements\n\n+ When building notification content, only parse merge codes used in the notification. [#1465](https://github.com/gocodebox/lifterlms/issues/1465)\n+ Improved checks related to the number of quiz attempts allowed for each student.\n+ Prevent browser page caching on quizzes. [#2092](https://github.com/gocodebox/lifterlms/issues/2092)\n\n##### Bug Fixes\n\n+ Allowed classes extended from the manual payment gateway class to display payment instructions.\n+ Allowed the `LLMS_Shortcode_User_Info` class to be filtered by the `llms_load_shortcodes` and `llms_load_shortcode_path` hooks.\n+ Stop using the deprecated `FILTER_SANITIZE_STRING` constant.\n+ Fixed an issue that caused shortcodes to not be replaced in some engagement emails. [#2070](https://github.com/gocodebox/lifterlms/issues/2070)\n+ Improve core forms detection so to exclude duplicates. [#2052](https://github.com/gocodebox/lifterlms/issues/2052)\n+ Added Aosta (AO) to the list of Italian provinces. [#2098](https://github.com/gocodebox/lifterlms/issues/2098)\n+ Fixed a compatibility issue with the Elementor Pro Theme Builder encountered on course and membership catalogs. [#2111](https://github.com/gocodebox/lifterlms/issues/2111)\n+ Fixed an issue where merge codes in reusable blocks on certificate templates were not replaced when the template was displayed or when the certificate was awarded and published. [#2058](https://github.com/gocodebox/lifterlms/issues/2058)\n+ Fixed an issue with OceanWP and Twenty Twenty themes where the Terms and Conditions checkbox was displayed incorrectly. [#1938](https://github.com/gocodebox/lifterlms/issues/1938)\n\n##### Developer Notes\n\n+ Added a new filter, `llms_secure_strings` allowing developers to register strings that should be automatically masked when written to log files.\n+ Added new filter `llms_no_cache` to control whether or not LifterLMS will send nocache headers. [#2092](https://github.com/gocodebox/lifterlms/issues/2092)\n+ Added new filter `llms_template_loader_restricted_priority` to control the priority of the `template_include` hook callback used to load restricted content single templates.\n\n\nv6.3.0 - 2022-04-07\n-------------------\n\n##### Upcoming PHP Version Requirement Change\n\n**LifterLMS will drop support for PHP 7.3 by May, 2022. This will raise the minimum supported PHP version to 7.4. PHP 7.3 reached its official [end of life](https://www.php.net/eol.php) on December 6, 2021. If you are still using PHP 7.3 please upgrade to PHP 7.4 or later as soon as possible.**\n\n##### New Features\n\n+ Automatically add student's dashboard endpoints to the BuddyPress profile nav. [#627](https://github.com/gocodebox/lifterlms/issues/627)\n\n##### Updates and Enhancements\n\n+ Upgraded LifterLMS Blocks to [v2.4.2](https://make.lifterlms.com/2022/04/07/lifterlms-blocks-version-2-4-2/).\n+ Updated LifterLMS Helper to [v3.4.2](https://make.lifterlms.com/2022/04/01/lifterlms-helper-version-3-4-2/).\n\n##### Bug Fixes\n\n+ Fixed paged queries in student dashboard not working when using plain permalinks.\n+ Fixed an issue that prevented searching students in some admin areas when WordPress was installed in a subdirectory. [#2096](https://github.com/gocodebox/lifterlms/issues/2096)\n+ Fixed lesson's comments status not reflecting default global setting when created with the course builder. [#2099](https://github.com/gocodebox/lifterlms/issues/2099)\n\n##### Deprecations\n\n+ Deprecated `LLMS_Integration_Buddypress::achievements_screen()` method with no replacement.\n+ Deprecated `LLMS_Integration_Buddypress::certificates_screen()` method with no replacement.\n+ Deprecated `LLMS_Integration_Buddypress::courses_screen()` method with no replacement.\n+ Deprecated `LLMS_Integration_Buddypress::memberships_screen()` method with no replacement.\n+ Deprecated `LLMS_Integration_Buddypress::remove_courses_paginate_links_filter()` method with no replacement.\n+ Deprecated `LLMS_Integration_Buddypress::modify_courses_paginate_links()` method with no replacement.\n\n##### Developer Notes\n\n+ Added `llms_get_paged_query_var()` function that returns the page number query var for the current request.\n+ Added new filter `llms_buddypress_profile_endpoints` to control the LifterLMS endpoints to be added to the BuddyPress profile.\n+ Added new filter `llms_buddypress_min_nav_item_slug` to control the LifterLMS main BuddyPress' nav item slug.\n+ Added new filter `llms_buddypress_min_nav_item_label` to control the LifterLMS main BuddyPress' nav item label.\n+ Added new filter `llms_buddypress_min_nav_item_position` to control the LifterLMS main BuddyPress' nav item position.\n\n\nv6.2.0 - 2022-03-30\n-------------------\n\n##### Updates and Enhancements\n\n+ Changed the `llmsStudentsSelect2()` JavaScript function to use the LifterLMS REST API \"list students\" endpoint instead of the `LLMS_AJAX_Handler::query_students()` PHP function.\n+ Upgraded LifterLMS Blocks to [v2.4.1](https://make.lifterlms.com/2022/03/30/lifterlms-blocks-version-2-4-1/).\n\n##### Bug Fixes\n\n+ Fixed issue with hidden checkboxes on LifterLMS forms.\n+ Fixed a compatiblity issue with the Divi Theme Builder ignoring access restrictions when using template with custom body. [#2063](https://github.com/gocodebox/lifterlms/issues/2063)\n+ Fixed an error encountered on the Engagements > Certificates screen when using the BuddyBoss theme. [#2080](https://github.com/gocodebox/lifterlms/issues/2080)\n\n##### Deprecations\n\n+ Deprecated `LLMS_AJAX_Handler::query_students()`. Use the [REST API list students](https://developer.lifterlms.com/rest-api/#tag/Students/paths/~1students/get) endpoint instead.\n\n##### Developer Notes\n\n+ Added new filter `llms_template_loader_priority` to control the priority of the `template_include` hook callback used to load restricted content templates.\n\n\nv6.1.0 - 2022-03-23\n-------------------\n\n##### Upcoming PHP Version Requirement Change\n\n**LifterLMS will drop support for PHP 7.3 by May, 2022. This will raise the minimum supported PHP version to 7.4. PHP 7.3 reached its official [end of life](https://www.php.net/eol.php) on December 6, 2021. If you are still using PHP 7.3 please upgrade to PHP 7.4 or later as soon as possible.**\n\n##### New Features\n\n+ Added the `{earned_date}` certificate merge code.\n\n##### Updates and Enhancements\n\n+ Changed the label for the `{current_date}` certificate merge code from 'Earned Date' to 'Current Date'.\n+ Updates LifterLMS REST to [v1.0.0-beta.24](https://make.lifterlms.com/2022/03/17/lifterlms-rest-api-version-1-0-0-beta-24/).\n\n##### Bug Fixes\n\n+ Fixed an issue encountered when editing an order with a completed payment plan. [#2067](https://github.com/gocodebox/lifterlms/issues/2067)\n+ Fixed access of protected LLMS_Abstract_Query properties.\n\n\nv6.0.0 - 2022-03-08\n-------------------\n\n**This major release of LifterLMS focuses on improving the experience of creating, designing, and managing achievements and certificates: use the block editor to design certificates, sync awards with their templates, award achievements and certificates on demand without requiring an engagement trigger, and [much more](https://lifterlms.com/docs/getting-started-with-lifterlms-6-0/). In addition, this release removes a significant number of previously deprecated classes, methods, and functions. Please read the full Breaking Changes sections for more information on removed code.**\n\n##### New Features\n\n+ The block editor is now enabled by default for certificates when using WordPress versions 5.8 and later.\n  + Existing certificates are marked as \"legacy\" and will continue to use the classic editor until migrated.\n  + To migrate a certificate, click the \"Migrate Certificate\" button. This will force the certificate's content into blocks.\n+ A number of new settings are available to certificates when using the block editor:\n  + Set the certificate's display (and print) size using common paper sizes such as US Letter, US Legal, A3, A4, and more.\n  + Set the certificate's display orientation: portrait of landscape.\n  + Set the certificate's inner margins.\n  + Set the certificate's background color.\n+ A new block, the Certificate Title Block, has been made available to certificates.\n  + The block works like a WordPress core Heading Block with added options for selecting from a few display fonts (provided by Google Web Fonts).\n  + The block controls the title of awarded certificates.\n+ Added the ability for administrators and LMS managers to edit earned certificates/achievements from the students reporting screen, as well as award new certificates/achievements to students.\n+ Added the ability to sync awarded certificates with the template used to generate them. [#1078](https://github.com/gocodebox/lifterlms/issues/1078)\n+ The `post_name` of earned certificate posts will be generated with a randomized 3+ character string in favor of relying on sequential numbers.\n+ Added certificate global options for the default size of new certificates and certificate templates.\n+ Certificate and email template merge code buttons now include [llms-user] information shortcodes.\n+ Added certificate sequential ID functionality merge code.\n+ Added a link to return to the student dashboard when viewing an awarded certificate. [#1959](https://github.com/gocodebox/lifterlms/issues/1959)\n+ Provide additional information to hooks on the student single course reporting screen.\n\n##### Updates and Enhancements\n\n+ Added new default images for use with achievements and certificates.\n  + The site-wide default images can be customized on the admin panel under Settings -> Engagements.\n  + The old default images will automatically be used for legacy certificates and can be forced by filtering `llms_use_legacy_engagement_images`. [#1081](https://github.com/gocodebox/lifterlms/issues/1081)\n+ Certificates no longer use the `header.php` and `footer.php` files from the site's theme, instead custom templates (`templates/certificates/header.php` and `templates/certificates/footer.php`) are used instead. These templates are minimal and exclude theme wrappers which reduces the visual conflicts encountered from theme wrappers, backgrounds, and more, especially when printing certificates. [#463](https://github.com/gocodebox/lifterlms/issues/463)\n+ The achievements and certificates dashboard endpoints are now paginated. [#669](https://github.com/gocodebox/lifterlms/issues/669)\n+ Added pagination to achievement and certificate reporting pages.\n+ The URL of earned user certificates has been changed from \"my_certificate\" to \"certificate\". Requests to the old url are automatically redirected to the new url, including instances where the URL slug has been translated.\n+ The URL of certificate template previews has been changed from \"certificate\" to \"certificate-template\".\n+ The certificate merge code, `{first_name}`, now outputs an empty string in favor of falling back to the user's nickname when there is no first name for the user. [#1640](https://github.com/gocodebox/lifterlms/issues/1640)\n+ The look and behavior of the certificate {{MINI_CERTIFICATE}} pop-over notification merge code now displays a placeholder preview of the certificate in favor of attempting to render a tiny version of the actual certificate. [#1950](https://github.com/gocodebox/lifterlms/issues/1950)\n+ The coupon code in the student's order details table is now wrapped in a `<code>` tag instead of an `<a>` tag. [#2033](https://github.com/gocodebox/lifterlms/issues/2033)\n+ Updates LifterLMS REST to [v1.0.0-beta.23](https://make.lifterlms.com/2022/02/23/lifterlms-rest-api-version-1-0-0-beta-23/).\n+ Updated LifterLMS Blocks to [version 2.4.0](https://make.lifterlms.com/2022/02/25/lifterlms-blocks-version-2-4-0/).\n\n##### Bug Fixes\n\n+ Delayed engagements are automatically unscheduled when the related post is deleted.\n+ A disabled student dashboard endpoint will no longer display the endpoint's summary on the main dashboard page. [#535](https://github.com/gocodebox/lifterlms/issues/535)\n+ Prior to sending a delayed engagement the recipient's enrollment in the related post is verified resulting the engagement not being triggered if the recipient's enrollment has been terminated. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ Post search filter boxes on various post tables will now longer display a link to the selected post.\n+ Basic notification code is no longer loaded on the admin panel.\n+ Fixed the label hover on picture type quizzes in some themes. [#2015](https://github.com/gocodebox/lifterlms/issues/2015)\n\n##### Database Migration\n\n+ A database migration is required when upgrading from versions earlier than 6.0.0. A description of the required updates can be found at [https://lifterlms.com/docs/lifterlms-database-updates/#600](https://lifterlms.com/docs/lifterlms-database-updates/#600).\n\n##### Deprecations\n\n+ Public access to properties of the abstract `LLMS_Database_Query` has been deprecated.\n  + Public access to class property `LLMS_Database_Query::$found_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_found_results()`.\n  + Public access to class property `LLMS_Database_Query::$max_pages`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_max_pages()`.\n  + Public access to class property `LLMS_Database_Query::$number_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_number_results()`.\n  + Public access to class property `LLMS_Database_Query::$results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_results()`.\n  + Public access to class property `LLMS_Database_Query::$query_vars`. The variable as a whole cannot be publicly accessed, instead use `LLMS_Database_Query::get()` and `LLMS_Database_Query::set()` to read and write to the array.\n  + The above changes were made to the abstract class `LLMS_Database_Query` but the following concrete classes that utilize the abstract are also affected by this change: `LLMS_Query_User_Postmeta`, `LLMS_Student_Query`, `LLMS_Query_Quiz_Attempt`, `LLMS_Events_Query`, and `LLMS_Notifications_Query`.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::create()` is deprecated with no replacement.\n+ Method `LLMS_Achievements::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement()`.\n+ Class `LLMS_Certificate` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n+ Method `LLMS_Certificates::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate()`.\n+ Method `LLMS_Engagements::init()` is deprecated with no replacement.\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Method `LLMS_Database_Query::set_found_results()` is deprecated.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement_User::has_user_earned()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::get_content_html()` is deprecated with no replacement.\n+ Class `LLMS_Certificate_User` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::set_shortcode_user()` is deprecated with no replacement.\n+ Engagement debug logging is removed. Use `llms_log()` directly instead.\n+ Filter `llms_db_query_get_default_args` is deprecated in favor of `llms_{$this->id}_query_get_default_args`.\n+ Filter `llms_certificate_has_user_earned` is deprecated in favor of `llms_earned_certificate_dupcheck`.\n+ Unused public class property `LLMS_Achievements::$content` is deprecated with no replacement.\n+ Method `LLMS_Admin_Post_Types::meta_metabox_init()` is deprecated with no replacement.\n+ The site options `lifterlms_certificate_bg_img_width`, `lifterlms_certificate_bg_img_height`, and `lifterlms_certificate_legacy_image_size` are now used only for certificates and certificate templates created using the classic editor.\n  + The settings, found on the Engagements Settings screen, are hidden by default.\n  + During the database upgrade from versions earlier than 6.x, an site option, `llms_has_legacy_certificates`  is added when at least one certificate is found. This option will display the settings so they can continue to be used for legacy certificates.\n  + After migrating all certificates on a site, the settings will still display. In order to remove them from the screen a developer can either delete the option `llms_has_legacy_certificates` or return `false` from the filter `llms_has_legacy_certificates`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ The constant `LLMS_ENGAGEMENT_DEBUG` is deprecated with no replacement.\n+ Engagement debugging via `LLMS_Engagements::log` is deprecated. Use `llms_log()` instead.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Filter `lifterlms_register_post_type_llms_my_certificate` is deprecated in favor of `lifterlms_register_post_type_my_certificate`.\n+ Deprecated the misspelled protected method `LLMS_Database_Query::preprare_query()` and replaced with `LLMS_Database_Query::prepare_query()`.\n  + Class method `LLMS_Events_Query::preprare_query` replaced with `LLMS_Events_Query::prepare_query()`.\n  + Class method `LLMS_Query_Quiz_Attempt::preprare_query` replaced with `LLMS_Query_Quiz_Attempt::prepare_query()`.\n  + Class method `LLMS_Query_User_Postmeta::preprare_query` replaced with `LLMS_Query_User_Postmeta::prepare_query()`.\n  + Class method `LLMS_Student_Query::preprare_query` replaced with `LLMS_Student_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`. [#859](https://github.com/gocodebox/lifterlms/issues/859)\n\n##### Breaking Changes\n\n+ Removed FSE template: `templates/block-templates/single-certificate.html`.\n+ Removed the deprecated `LLMS()` function in favor of the `llms()` function.\n+ Removed the deprecated the `LLMS_SendWP::do_remote_install()` method in favor of the `LLMS_Abstract_Email_Provider::do_remote_install()` method.\n+ Removed the deprecated `LLMS_Abstract_Email_Provider::output_css()` method.\n+ Removed the deprecated `LLMS_Abstract_Generator_Posts::increment()` method.\n+ Removed the deprecated `LLMS_Admin_Users_Table::load_dependencies()` method.\n+ Removed the deprecated `LLMS_Admin_Import::localize_stat()` method.\n+ Removed the deprecated `LLMS_Admin_Notices_Core::check_staging()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::generator_course_status()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::output_step_html()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::scripts()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::watch_course_generation()` method.\n+ Removed the deprecated `llms_format_decimal()` function.\n+ Removed the deprecated `llms_set_person_auth_cookie()` function.\n+ Removed the deprecated `LLMS_Course::sections` property.\n+ Removed the deprecated `LLMS_Course::sku` property.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_pw_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::is_inline_script_enqueued()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::format_date()` method.\n+ Removed the deprecated `LLMS_Generator::get_author_id_from_raw()` method.\n+ Removed the deprecated `LLMS_Generator::get_default_post_status()` method.\n+ Removed the deprecated `LLMS_Generator::get_generated_posts()` method.\n+ Removed the deprecated `LLMS_Generator::increment()` method.\n+ Removed the deprecated `llms__created` action hook from the `LLMS_Abstract_Database_Store::create()` method.\n+ Removed the deprecated `llms__deleted` action hook from the `LLMS_Abstract_Database_Store::delete()` method.\n+ Removed the deprecated `llms__updated` action hook from the `LLMS_Abstract_Database_Store::update()` method.\n+ Removed the deprecated `llms_user_removed_from_membership_level` action hook from the `LLMS_Student::unenroll()` method.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `lifterlms_template_pricing_table()` function.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `templates/product/pricing-table.php` file.\n+ Removed the deprecated `LLMS_Frontend_Password` class.\n+ Removed the deprecated `LLMS_Install::db_updates()` method.\n+ Removed the deprecated `LLMS_Install::update_notice()` method.\n+ Removed the deprecated `LLMS_Notifications::dispatch_processors()` method.\n+ Removed the deprecated `llms_processors_async_dispatching` filter hook from the `LLMS_Notifications::__construct()` method.\n+ Removed the deprecated `LLMS_Notifications::$_instance` property.\n+ Removed the deprecated `LLMS_Person_Handler::register()` method.\n+ Removed the deprecated `LLMS_Person_Handler::sanitize_field()` method.\n+ Removed the deprecated `LLMS_Person_Handler::update()` method.\n+ Removed the deprecated `LLMS_Person_Handler::validate_fields()` method.\n+ Removed the deprecated `LLMS_Person_Handler::voucher_toggle_script()` method.\n+ Removed the deprecated `templates/admin/notices/db-update.php` file.\n+ Removed the deprecated `templates/admin/notices/db-updating.php` file.\n+ Removed the deprecated `llms_usernames_blacklist` filter hook in the `llms_get_usernames_blocklist()` function.\n+ Removed the deprecated `includes/libraries/wp-background-processing/index.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-async-request.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-background-process.php` file.\n+ Removed the deprecated `LLMS_Section::get_next_available_lesson_order()` method.\n+ Removed the deprecated `LLMS_Section::get_order()` method.\n+ Removed the deprecated `LLMS_Section::get_parent_course()` method.\n+ Removed the deprecated `LLMS_Section::set_parent_course()` method.\n+ Removed the deprecated `LLMS_AJAX::check_voucher_duplicate()` method.\n+ Removed the deprecated `LLMS_AJAX::get_ajax_data()` method.\n+ Removed the deprecated `LLMS_AJAX::register_script()` method.\n+ Removed the deprecated `LLMS_Interface_Post_Audio` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Sales_Page` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Video` interface.\n+ Removed the deprecated `LLMS_Achievements::$_instance` property.\n+ Removed the deprecated `LLMS_Certificates::$_instance` property.\n+ Removed the deprecated `LLMS_Emails::$_instance` property.\n+ Removed the deprecated `LLMS_Engagements::$_instance` property.\n+ Removed the deprecated `LLMS_Events::$_instance` property.\n+ Removed the deprecated `LLMS_Grades::$_instance` property.\n+ Removed the deprecated `LLMS_Integrations::$_instance` property.\n+ Removed the deprecated `LLMS_Payment_Gateways::$_instance` property.\n+ Removed the deprecated `LLMS_Processors::$_instance` property.\n+ Removed the deprecated `LLMS_Sessions::$_instance` property.\n\n##### Developer Notes\n\n+ Added `LLMS_Awards_Query`, used for querying data about awarded certificates and achievements.\n  + The method signature `LLMS_Student::get_achievements()` and `LLMS_Student::get_certificates()` now use this class under tho hood.\n  + The previous method signature, which passed data into a direct SQL query, is now deprecated.\n+ Achievement and certificate data storage locations have been modified, primarily to reduce reliance on the `wp_postmeta` table which will result in a site-wide performance improvement, especially on large sites.\n  + Meta properties `_llms_achievement_content` and `_llms_certificate_content` have been removed in favor of `WP_Post::$post_content`.\n  + Meta properties `_llms_achievement_title` and `_llms_certificate_title` have been removed in favor of `WP_Post::$post_title`.\n  + Meta properties `_llms_achievement_template` and `_llms_certificate_template` have been removed in favor of `WP_Post::$post_parent`.\n  + Meta properties `_llms_achievement_image` and `_llms_certificate_image` have been moved the meta property `_thumbnail_id` in order to utilize the WordPress core's featured image functionality and internal APIs.\n+ Reliance on `lifterlms_user_postmeta` for achievement and certificate data will be removed in a future release.\n  + User postmeta properties `_achievement_earned` and `_certificate_earned` will continue to be recorded but are no longer being used internally.\n  + The `updated_date` is now accessible via `WP_Post::$post_date`.\n  + The `user_id` is now accessible via `WP_Post::$post_author`.\n+ Added new Javascript UI components library, modeled after `@wordpress/components`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/components).\n+ Added a new SVG icon library, modeled after `@wordpress/icons`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/icons).\n+ The merge code button seen on certificate and email template editors is now an SVG image instead of a PNG.\n+ Added utility function for escaping and quoting strings. [#1027](https://github.com/gocodebox/lifterlms/issues/1027)\n+ Added the ability to force an admin metabox field value through the new `meta` arg. [#2016](https://github.com/gocodebox/lifterlms/issues/2016)\n+ Added new utility function for stripping prefixes from strings.\n\n##### Performance Improvements\n\n+ Increased the number of files that are autoloaded instead of manually loaded.\n\n##### Updated Templates\n\n+ [templates/achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/achievements/loop.php)\n+ [templates/achievements/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/achievements/template.php)\n+ [templates/admin/notices/db-update.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/admin/notices/db-update.php)\n+ [templates/admin/notices/db-updating.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/admin/notices/db-updating.php)\n+ [templates/admin/reporting/reporting.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/admin/reporting/reporting.php)\n+ [templates/admin/reporting/tabs/students/courses-course.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/admin/reporting/tabs/students/courses-course.php)\n+ [templates/admin/reporting/tabs/students/information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/admin/reporting/tabs/students/information.php)\n+ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/block-templates/single-certificate.html)\n+ [templates/certificates/actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/actions.php)\n+ [templates/certificates/content-legacy.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/content-legacy.php)\n+ [templates/certificates/content.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/content.php)\n+ [templates/certificates/dynamic-styles.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/dynamic-styles.php)\n+ [templates/certificates/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/footer.php)\n+ [templates/certificates/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/header.php)\n+ [templates/certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/loop.php)\n+ [templates/certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/preview.php)\n+ [templates/certificates/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/certificates/template.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/checkout/form-switch-source.php)\n+ [templates/content-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/content-certificate.php)\n+ [templates/emails/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/emails/footer.php)\n+ [templates/emails/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/emails/header.php)\n+ [templates/myaccount/my-grades-single-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/myaccount/my-grades-single-table.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/myaccount/view-order-actions.php)\n+ [templates/myaccount/view-order-information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/myaccount/view-order-information.php)\n+ [templates/myaccount/view-order-transactions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/myaccount/view-order-transactions.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/myaccount/view-order.php)\n+ [templates/product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/product/pricing-table.php)\n+ [templates/single-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0/templates/single-certificate.php)\n\n\nv6.0.0-rc.1 - 2022-03-03\n------------------------\n\n##### New Features\n\n+ Added a link to return to the student dashboard when viewing an awarded certificate. [#1959](https://github.com/gocodebox/lifterlms/issues/1959)\n+ The block editor is now enabled by default for certificates when using WordPress versions 5.8 and later.\n  + Existing certificates are marked as \"legacy\" and will continue to use the classic editor until migrated.\n  + To migrate a certificate, click the \"Migrate Certificate\" button. This will force the certificate's content into blocks.\n+ A number of new settings are available to certificates when using the block editor:\n  + Set the certificate's display (and print) size using common paper sizes such as US Letter, US Legal, A3, A4, and more.\n  + Set the certificate's display orientation: portrait of landscape.\n  + Set the certificate's inner margins.\n  + Set the certificate's background color.\n+ A new block, the Certificate Title Block, has been made available to certificates.\n  + The block works like a WordPress core Heading Block with added options for selecting from a few display fonts (provided by Google Web Fonts).\n  + The block controls the title of awarded certificates.\n+ + Added the ability to sync awarded certificates with the template used to generate them. [#1078](https://github.com/gocodebox/lifterlms/issues/1078)\n+ The `post_name` of earned certificate posts will be generated with a randomized 3+ character string in favor of relying on sequential numbers.\n+ Added the ability for administrators and LMS managers to edit earned certificates/achievements from the students reporting screen, as well as award new certificates/achievements to students.\n+ + Added certificate global options for the default size of new certificates and certificate templates.\n+ Certificate and email template merge code buttons now include [llms-user] information shortcodes.\n+ Added certificate sequential ID functionality merge code. [Read more](@TODO).\n+ Provide additional information to hooks on the student single course reporting screen.\n\n##### Updates and Enhancements\n\n+ Added pagination to achievement and certificate reporting pages.\n+ Certificates no longer use the `header.php` and `footer.php` files from the site's theme, instead custom templates (`templates/certificates/header.php` and `templates/certificates/footer.php`) are used instead. These templates are minimal and exclude theme wrappers which reduces the visual conflicts encountered from theme wrappers, backgrounds, and more, especially when printing certificates. [#463](https://github.com/gocodebox/lifterlms/issues/463)\n+ The achievements and certificates dashboard endpoints are now paginated. [#669](https://github.com/gocodebox/lifterlms/issues/669)\n+ Added new default images for use with achievements and certificates.\n  + The site-wide default images can be customized on the admin panel under Settings -> Engagements.\n  + The old default images can be used by filtering `llms_use_legacy_engagement_images`. [#1081](https://github.com/gocodebox/lifterlms/issues/1081)\n+ The URL of earned user certificates has been changed from \"my_certificate\" to \"certificate\". Requests to the old url are automatically redirected to the new url, including instances where the URL slug has been translated.\n+ The URL of certificate template previews has been changed from \"certificate\" to \"certificate-template\".\n+ The certificate merge code, `{first_name}`, now outputs an empty string in favor of falling back to the user's nickname when there is no first name for the user. [#1640](https://github.com/gocodebox/lifterlms/issues/1640)\n+ Updates LifterLMS REST to [v1.0.0-beta.23](https://make.lifterlms.com/2022/02/23/lifterlms-rest-api-version-1-0-0-beta-23/).\n+ Updated LifterLMS Blocks to [version 2.4.0](https://make.lifterlms.com/2022/02/25/lifterlms-blocks-version-2-4-0/).\n+ The look and behavior of the certificate {{MINI_CERTIFICATE}} pop-over notification merge code now displays a placeholder preview of the certificate in favor of attempting to render a tiny version of the actual certificate. [#1950](https://github.com/gocodebox/lifterlms/issues/1950)\n\n##### Bug Fixes\n\n+ + Fixed how the protected `LLMS_Notifications_Query::$found_results` property is accessed in `LLMS_Abstract_Notification_Controller::has_subscriber_received()`. + Fixed how the protected `LLMS_Notifications_Query::$max_pages` property is accessed in `lifterlms_template_student_dashboard_my_notifications()`.\n+ Delayed engagements are automatically unscheduled when the related post is deleted.\n+ Prior to sending a delayed engagement the recipient's enrollment in the related post is verified resulting the engagement not being triggered if the recipient's enrollment has been terminated. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ A disabled student dashboard endpoint will no longer display the endpoint's summary on the main dashboard page. [#535](https://github.com/gocodebox/lifterlms/issues/535)\n+ Post search filter boxes on various post tables will now longer display a link to the selected post.\n+ Basic notification code is no longer loaded on the admin panel.\n\n##### Deprecations\n\n+ Public access to properties of the abstract `LLMS_Database_Query` has been deprecated.\n  + Public access to class property `LLMS_Database_Query::$found_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_found_results()`.\n  + Public access to class property `LLMS_Database_Query::$max_pages`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_max_pages()`.\n  + Public access to class property `LLMS_Database_Query::$number_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_number_results()`.\n  + Public access to class property `LLMS_Database_Query::$results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_results()`.\n  + Public access to class property `LLMS_Database_Query::$query_vars`. The variable as a whole cannot be publicly accessed, instead use `LLMS_Database_Query::get()` and `LLMS_Database_Query::set()` to read and write to the array.\n  + The above changes were made to the abstract class `LLMS_Database_Query` but the following concrete classes that utilize the abstract are also affected by this change: `LLMS_Query_User_Postmeta`, `LLMS_Student_Query`, `LLMS_Query_Quiz_Attempt`, `LLMS_Events_Query`, and `LLMS_Notifications_Query`.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::create()` is deprecated with no replacement.\n+ Method `LLMS_Achievements::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement()`.\n+ Class `LLMS_Certificate` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n+ Method `LLMS_Certificates::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate()`.\n+ Method `LLMS_Engagements::init()` is deprecated with no replacement.\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Method `LLMS_Database_Query::set_found_results()` is deprecated.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement_User::has_user_earned()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::get_content_html()` is deprecated with no replacement.\n+ Class `LLMS_Certificate_User` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::set_shortcode_user()` is deprecated with no replacement.\n+ Engagement debug logging is removed. Use `llms_log()` directly instead.\n+ Filter `llms_db_query_get_default_args` is deprecated in favor of `llms_{$this->id}_query_get_default_args`.\n+ Filter `llms_certificate_has_user_earned` is deprecated in favor of `llms_earned_certificate_dupcheck`.\n+ Unused public class property `LLMS_Achievements::$content` is deprecated with no replacement.\n+ Method `LLMS_Admin_Post_Types::meta_metabox_init()` is deprecated with no replacement.\n+ The site options `lifterlms_certificate_bg_img_width`, `lifterlms_certificate_bg_img_height`, and `lifterlms_certificate_legacy_image_size` are now used only for certificates and certificate templates created using the classic editor.\n  + The settings, found on the Engagements Settings screen, are hidden by default.\n  + During the database upgrade from versions earlier than 6.x, an site option, `llms_has_legacy_certificates`  is added when at least one certificate is found. This option will display the settings so they can continue to be used for legacy certificates.\n  + After migrating all certificates on a site, the settings will still display. In order to remove them from the screen a developer can either delete the option `llms_has_legacy_certificates` or return `false` from the filter `llms_has_legacy_certificates`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ The constant `LLMS_ENGAGEMENT_DEBUG` is deprecated with no replacement.\n+ Engagement debugging via `LLMS_Engagements::log` is deprecated. Use `llms_log()` instead.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Filter `lifterlms_register_post_type_llms_my_certificate` is deprecated in favor of `lifterlms_register_post_type_my_certificate`.\n+ Deprecated the misspelled protected method `LLMS_Database_Query::preprare_query()` and replaced with `LLMS_Database_Query::prepare_query()`.\n  + Class method `LLMS_Events_Query::preprare_query` replaced with `LLMS_Events_Query::prepare_query()`.\n  + Class method `LLMS_Query_Quiz_Attempt::preprare_query` replaced with `LLMS_Query_Quiz_Attempt::prepare_query()`.\n  + Class method `LLMS_Query_User_Postmeta::preprare_query` replaced with `LLMS_Query_User_Postmeta::prepare_query()`.\n  + Class method `LLMS_Student_Query::preprare_query` replaced with `LLMS_Student_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`. [#859](https://github.com/gocodebox/lifterlms/issues/859)\n\n##### Breaking Changes\n\n+ Removed FSE template: `templates/block-templates/single-certificate.html`.\n+ Removed the deprecated `LLMS()` function in favor of the `llms()` function.\n+ Removed the deprecated the `LLMS_SendWP::do_remote_install()` method in favor of the `LLMS_Abstract_Email_Provider::do_remote_install()` method.\n+ Removed the deprecated `LLMS_Abstract_Email_Provider::output_css()` method.\n+ Removed the deprecated `LLMS_Abstract_Generator_Posts::increment()` method.\n+ Removed the deprecated `LLMS_Admin_Users_Table::load_dependencies()` method.\n+ Removed the deprecated `LLMS_Admin_Import::localize_stat()` method.\n+ Removed the deprecated `LLMS_Admin_Notices_Core::check_staging()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::generator_course_status()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::output_step_html()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::scripts()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::watch_course_generation()` method.\n+ Removed the deprecated `llms_format_decimal()` function.\n+ Removed the deprecated `llms_set_person_auth_cookie()` function.\n+ Removed the deprecated `LLMS_Course::sections` property.\n+ Removed the deprecated `LLMS_Course::sku` property.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_pw_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::is_inline_script_enqueued()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::format_date()` method.\n+ Removed the deprecated `LLMS_Generator::get_author_id_from_raw()` method.\n+ Removed the deprecated `LLMS_Generator::get_default_post_status()` method.\n+ Removed the deprecated `LLMS_Generator::get_generated_posts()` method.\n+ Removed the deprecated `LLMS_Generator::increment()` method.\n+ Removed the deprecated `llms__created` action hook from the `LLMS_Abstract_Database_Store::create()` method.\n+ Removed the deprecated `llms__deleted` action hook from the `LLMS_Abstract_Database_Store::delete()` method.\n+ Removed the deprecated `llms__updated` action hook from the `LLMS_Abstract_Database_Store::update()` method.\n+ Removed the deprecated `llms_user_removed_from_membership_level` action hook from the `LLMS_Student::unenroll()` method.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `lifterlms_template_pricing_table()` function.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `templates/product/pricing-table.php` file.\n+ Removed the deprecated `LLMS_Frontend_Password` class.\n+ Removed the deprecated `LLMS_Install::db_updates()` method.\n+ Removed the deprecated `LLMS_Install::update_notice()` method.\n+ Removed the deprecated `LLMS_Notifications::dispatch_processors()` method.\n+ Removed the deprecated `llms_processors_async_dispatching` filter hook from the `LLMS_Notifications::__construct()` method.\n+ Removed the deprecated `LLMS_Notifications::$_instance` property.\n+ Removed the deprecated `LLMS_Person_Handler::register()` method.\n+ Removed the deprecated `LLMS_Person_Handler::sanitize_field()` method.\n+ Removed the deprecated `LLMS_Person_Handler::update()` method.\n+ Removed the deprecated `LLMS_Person_Handler::validate_fields()` method.\n+ Removed the deprecated `LLMS_Person_Handler::voucher_toggle_script()` method.\n+ Removed the deprecated `templates/admin/notices/db-update.php` file.\n+ Removed the deprecated `templates/admin/notices/db-updating.php` file.\n+ Removed the deprecated `llms_usernames_blacklist` filter hook in the `llms_get_usernames_blocklist()` function.\n+ Removed the deprecated `includes/libraries/wp-background-processing/index.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-async-request.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-background-process.php` file.\n+ Removed the deprecated `LLMS_Section::get_next_available_lesson_order()` method.\n+ Removed the deprecated `LLMS_Section::get_order()` method.\n+ Removed the deprecated `LLMS_Section::get_parent_course()` method.\n+ Removed the deprecated `LLMS_Section::set_parent_course()` method.\n+ Removed the deprecated `LLMS_AJAX::check_voucher_duplicate()` method.\n+ Removed the deprecated `LLMS_AJAX::get_ajax_data()` method.\n+ Removed the deprecated `LLMS_AJAX::register_script()` method.\n+ Removed the deprecated `LLMS_Interface_Post_Audio` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Sales_Page` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Video` interface.\n+ Removed the deprecated `LLMS_Achievements::$_instance` property.\n+ Removed the deprecated `LLMS_Certificates::$_instance` property.\n+ Removed the deprecated `LLMS_Emails::$_instance` property.\n+ Removed the deprecated `LLMS_Engagements::$_instance` property.\n+ Removed the deprecated `LLMS_Events::$_instance` property.\n+ Removed the deprecated `LLMS_Grades::$_instance` property.\n+ Removed the deprecated `LLMS_Integrations::$_instance` property.\n+ Removed the deprecated `LLMS_Payment_Gateways::$_instance` property.\n+ Removed the deprecated `LLMS_Processors::$_instance` property.\n+ Removed the deprecated `LLMS_Sessions::$_instance` property.\n\n##### Developer Notes\n\n+ Added `LLMS_Awards_Query`, used for querying data about awarded certificates and achievements.\n  + The method signature `LLMS_Student::get_achievements()` and `LLMS_Student::get_certificates()` now use this class under tho hood.\n  + The previous method signature, which passed data into a direct SQL query, is now deprecated.\n+ Achievement and certificate data storage locations have been modified, primarily to reduce reliance on the `wp_postmeta` table which will result in a site-wide performance improvement, especially on large sites.\n  + Meta properties `_llms_achievement_content` and `_llms_certificate_content` have been removed in favor of `WP_Post::$post_content`.\n  + Meta properties `_llms_achievement_title` and `_llms_certificate_title` have been removed in favor of `WP_Post::$post_title`.\n  + Meta properties `_llms_achievement_template` and `_llms_certificate_template` have been removed in favor of `WP_Post::$post_parent`.\n  + Meta properties `_llms_achievement_image` and `_llms_certificate_image` have been moved the meta property `_thumbnail_id` in order to utilize the WordPress core's featured image functionality and internal APIs.\n+ Reliance on `lifterlms_user_postmeta` for achievement and certificate data will be removed in a future release.\n  + User postmeta properties `_achievement_earned` and `_certificate_earned` will continue to be recorded but are no longer being used internally.\n  + The `updated_date` is now accessible via `WP_Post::$post_date`.\n  + The `user_id` is now accessible via `WP_Post::$post_author`.\n+ Added new Javascript UI components library, modeled after `@wordpress/components`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/components).\n+ Added a new SVG icon library, modeled after `@wordpress/icons`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/icons).\n+ The merge code button seen on certificate and email template editors is now an SVG image instead of a PNG.\n+ Added utility function for escaping and quoting strings. [#1027](https://github.com/gocodebox/lifterlms/issues/1027)\n+ Added the ability to force an admin metabox field value through the new `meta` arg. [#2016](https://github.com/gocodebox/lifterlms/issues/2016)\n+ Added new utility function for stripping prefixes from strings.\n\n##### Performance Improvements\n\n+ Increased the number of files that are autoloaded instead of manually loaded.\n\n##### Updated Templates\n\n+ [templates/achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/achievements/loop.php)\n+ [templates/achievements/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/achievements/template.php)\n+ [templates/admin/notices/db-update.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/admin/notices/db-update.php)\n+ [templates/admin/notices/db-updating.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/admin/notices/db-updating.php)\n+ [templates/admin/reporting/reporting.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/admin/reporting/reporting.php)\n+ [templates/admin/reporting/tabs/students/courses-course.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/admin/reporting/tabs/students/courses-course.php)\n+ [templates/admin/reporting/tabs/students/information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/admin/reporting/tabs/students/information.php)\n+ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/block-templates/single-certificate.html)\n+ [templates/certificates/actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/actions.php)\n+ [templates/certificates/content-legacy.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/content-legacy.php)\n+ [templates/certificates/content.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/content.php)\n+ [templates/certificates/dynamic-styles.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/dynamic-styles.php)\n+ [templates/certificates/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/footer.php)\n+ [templates/certificates/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/header.php)\n+ [templates/certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/loop.php)\n+ [templates/certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/preview.php)\n+ [templates/certificates/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/certificates/template.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/checkout/form-switch-source.php)\n+ [templates/content-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/content-certificate.php)\n+ [templates/emails/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/emails/footer.php)\n+ [templates/emails/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/emails/header.php)\n+ [templates/myaccount/my-grades-single-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/myaccount/my-grades-single-table.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/myaccount/view-order-actions.php)\n+ [templates/myaccount/view-order-information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/myaccount/view-order-information.php)\n+ [templates/myaccount/view-order-transactions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/myaccount/view-order-transactions.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/myaccount/view-order.php)\n+ [templates/product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/product/pricing-table.php)\n+ [templates/single-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-rc.1/templates/single-certificate.php)\n\n\nv6.0.0-beta.2 - 2022-02-22\n--------------------------\n\n##### New Features\n\n+ Added a link to return to the student dashboard when viewing an awarded certificate. [#1959](https://github.com/gocodebox/lifterlms/issues/1959)\n+ The block editor is now enabled by default for certificates when using WordPress versions 5.8 and later.\n  + Existing certificates are marked as \"legacy\" and will continue to use the classic editor until migrated.\n  + To migrate a certificate, click the \"Migrate Certificate\" button. This will force the certificate's content into blocks.\n+ A number of new settings are available to certificates when using the block editor:\n  + Set the certificate's display (and print) size using common paper sizes such as US Letter, US Legal, A3, A4, and more.\n  + Set the certificate's display orientation: portrait of landscape.\n  + Set the certificate's inner margins.\n  + Set the certificate's background color.\n+ A new block, the Certificate Title Block, has been made available to certificates.\n  + The block works like a WordPress core Heading Block with added options for selecting from a few display fonts (provided by Google Web Fonts).\n  + The block controls the title of awarded certificates.\n+ + Added the ability to sync awarded certificates with the template used to generate them. [#1078](https://github.com/gocodebox/lifterlms/issues/1078)\n+ The `post_name` of earned certificate posts will be generated with a randomized 3+ character string in favor of relying on sequential numbers.\n+ Added the ability for administrators and LMS managers to edit earned certificates/achievements from the students reporting screen, as well as award new certificates/achievements to students.\n+ + Added certificate global options for the default size of new certificates and certificate templates.\n+ Certificate and email template merge code buttons now include [llms-user] information shortcodes.\n+ Added certificate sequential ID functionality merge code. [Read more](@TODO).\n\n##### Updates and Enhancements\n\n+ Added pagination to achievement and certificate reporting pages.\n+ Certificates no longer use the `header.php` and `footer.php` files from the site's theme, instead custom templates (`templates/certificates/header.php` and `templates/certificates/footer.php`) are used instead. These templates are minimal and exclude theme wrappers which reduces the visual conflicts encountered from theme wrappers, backgrounds, and more, especially when printing certificates. [#463](https://github.com/gocodebox/lifterlms/issues/463)\n+ The achievements and certificates dashboard endpoints are now paginated. [#669](https://github.com/gocodebox/lifterlms/issues/669)\n+ Added new default images for use with achievements and certificates.\n  + The site-wide default images can be customized on the admin panel under Settings -> Engagements.\n  + The old default images can be used by filtering `llms_use_legacy_engagement_images`. [#1081](https://github.com/gocodebox/lifterlms/issues/1081)\n+ The URL of earned user certificates has been changed from \"my_certificate\" to \"certificate\". Requests to the old url are automatically redirected to the new url, including instances where the URL slug has been translated.\n+ The URL of certificate template previews has been changed from \"certificate\" to \"certificate-template\".\n+ The certificate merge code, `{first_name}`, now outputs an empty string in favor of falling back to the user's nickname when there is no first name for the user. [#1640](https://github.com/gocodebox/lifterlms/issues/1640)\n+ Updates LifterLMS REST to [v1.0.0-beta.22](https://make.lifterlms.com/2021/12/15/lifterlms-rest-api-version-1-0-0-beta-22/).\n+ The look and behavior of the certificate {{MINI_CERTIFICATE}} pop-over notification merge code now displays a placeholder preview of the certificate in favor of attempting to render a tiny version of the actual certificate. [#1950](https://github.com/gocodebox/lifterlms/issues/1950)\n\n##### Bug Fixes\n\n+ + Fixed how the protected `LLMS_Notifications_Query::$found_results` property is accessed in `LLMS_Abstract_Notification_Controller::has_subscriber_received()`. + Fixed how the protected `LLMS_Notifications_Query::$max_pages` property is accessed in `lifterlms_template_student_dashboard_my_notifications()`.\n+ Delayed engagements are automatically unscheduled when the related post is deleted.\n+ Prior to sending a delayed engagement the recipient's enrollment in the related post is verified resulting the engagement not being triggered if the recipient's enrollment has been terminated. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ A disabled student dashboard endpoint will no longer display the endpoint's summary on the main dashboard page. [#535](https://github.com/gocodebox/lifterlms/issues/535)\n+ Post search filter boxes on various post tables will now longer display a link to the selected post.\n+ Basic notification code is no longer loaded on the admin panel.\n\n##### Deprecations\n\n+ Public access to properties of the abstract `LLMS_Database_Query` has been deprecated.\n  + Public access to class property `LLMS_Database_Query::$found_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_found_results()`.\n  + Public access to class property `LLMS_Database_Query::$max_pages`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_max_pages()`.\n  + Public access to class property `LLMS_Database_Query::$number_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_number_results()`.\n  + Public access to class property `LLMS_Database_Query::$results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_results()`.\n  + Public access to class property `LLMS_Database_Query::$query_vars`. The variable as a whole cannot be publicly accessed, instead use `LLMS_Database_Query::get()` and `LLMS_Database_Query::set()` to read and write to the array.\n  + The above changes were made to the abstract class `LLMS_Database_Query` but the following concrete classes that utilize the abstract are also affected by this change: `LLMS_Query_User_Postmeta`, `LLMS_Student_Query`, `LLMS_Query_Quiz_Attempt`, `LLMS_Events_Query`, and `LLMS_Notifications_Query`.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::create()` is deprecated with no replacement.\n+ Method `LLMS_Achievements::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement()`.\n+ Class `LLMS_Certificate` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n+ Method `LLMS_Certificates::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate()`.\n+ Method `LLMS_Engagements::init()` is deprecated with no replacement.\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Method `LLMS_Database_Query::set_found_results()` is deprecated.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement_User::has_user_earned()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::get_content_html()` is deprecated with no replacement.\n+ Class `LLMS_Certificate_User` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::set_shortcode_user()` is deprecated with no replacement.\n+ Engagement debug logging is removed. Use `llms_log()` directly instead.\n+ Filter `llms_db_query_get_default_args` is deprecated in favor of `llms_{$this->id}_query_get_default_args`.\n+ Filter `llms_certificate_has_user_earned` is deprecated in favor of `llms_earned_certificate_dupcheck`.\n+ Unused public class property `LLMS_Achievements::$content` is deprecated with no replacement.\n+ Method `LLMS_Admin_Post_Types::meta_metabox_init()` is deprecated with no replacement.\n+ The site options `lifterlms_certificate_bg_img_width`, `lifterlms_certificate_bg_img_height`, and `lifterlms_certificate_legacy_image_size` are now used only for certificates and certificate templates created using the classic editor.\n  + The settings, found on the Engagements Settings screen, are hidden by default.\n  + During the database upgrade from versions earlier than 6.x, an site option, `llms_has_legacy_certificates`  is added when at least one certificate is found. This option will display the settings so they can continue to be used for legacy certificates.\n  + After migrating all certificates on a site, the settings will still display. In order to remove them from the screen a developer can either delete the option `llms_has_legacy_certificates` or return `false` from the filter `llms_has_legacy_certificates`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ The constant `LLMS_ENGAGEMENT_DEBUG` is deprecated with no replacement.\n+ Engagement debugging via `LLMS_Engagements::log` is deprecated. Use `llms_log()` instead.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Filter `lifterlms_register_post_type_llms_my_certificate` is deprecated in favor of `lifterlms_register_post_type_my_certificate`.\n+ Deprecated the misspelled protected method `LLMS_Database_Query::preprare_query()` and replaced with `LLMS_Database_Query::prepare_query()`.\n  + Class method `LLMS_Events_Query::preprare_query` replaced with `LLMS_Events_Query::prepare_query()`.\n  + Class method `LLMS_Query_Quiz_Attempt::preprare_query` replaced with `LLMS_Query_Quiz_Attempt::prepare_query()`.\n  + Class method `LLMS_Query_User_Postmeta::preprare_query` replaced with `LLMS_Query_User_Postmeta::prepare_query()`.\n  + Class method `LLMS_Student_Query::preprare_query` replaced with `LLMS_Student_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`. [#859](https://github.com/gocodebox/lifterlms/issues/859)\n\n##### Breaking Changes\n\n+ Removed FSE template: `templates/block-templates/single-certificate.html`.\n+ Removed the deprecated `LLMS()` function in favor of the `llms()` function.\n+ Removed the deprecated the `LLMS_SendWP::do_remote_install()` method in favor of the `LLMS_Abstract_Email_Provider::do_remote_install()` method.\n+ Removed the deprecated `LLMS_Abstract_Email_Provider::output_css()` method.\n+ Removed the deprecated `LLMS_Abstract_Generator_Posts::increment()` method.\n+ Removed the deprecated `LLMS_Admin_Users_Table::load_dependencies()` method.\n+ Removed the deprecated `LLMS_Admin_Import::localize_stat()` method.\n+ Removed the deprecated `LLMS_Admin_Notices_Core::check_staging()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::generator_course_status()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::output_step_html()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::scripts()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::watch_course_generation()` method.\n+ Removed the deprecated `llms_format_decimal()` function.\n+ Removed the deprecated `llms_set_person_auth_cookie()` function.\n+ Removed the deprecated `LLMS_Course::sections` property.\n+ Removed the deprecated `LLMS_Course::sku` property.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_pw_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::is_inline_script_enqueued()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::format_date()` method.\n+ Removed the deprecated `LLMS_Generator::get_author_id_from_raw()` method.\n+ Removed the deprecated `LLMS_Generator::get_default_post_status()` method.\n+ Removed the deprecated `LLMS_Generator::get_generated_posts()` method.\n+ Removed the deprecated `LLMS_Generator::increment()` method.\n+ Removed the deprecated `llms__created` action hook from the `LLMS_Abstract_Database_Store::create()` method.\n+ Removed the deprecated `llms__deleted` action hook from the `LLMS_Abstract_Database_Store::delete()` method.\n+ Removed the deprecated `llms__updated` action hook from the `LLMS_Abstract_Database_Store::update()` method.\n+ Removed the deprecated `llms_user_removed_from_membership_level` action hook from the `LLMS_Student::unenroll()` method.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `lifterlms_template_pricing_table()` function.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `templates/product/pricing-table.php` file.\n+ Removed the deprecated `LLMS_Frontend_Password` class.\n+ Removed the deprecated `LLMS_Install::db_updates()` method.\n+ Removed the deprecated `LLMS_Install::update_notice()` method.\n+ Removed the deprecated `LLMS_Notifications::dispatch_processors()` method.\n+ Removed the deprecated `llms_processors_async_dispatching` filter hook from the `LLMS_Notifications::__construct()` method.\n+ Removed the deprecated `LLMS_Notifications::$_instance` property.\n+ Removed the deprecated `LLMS_Person_Handler::register()` method.\n+ Removed the deprecated `LLMS_Person_Handler::sanitize_field()` method.\n+ Removed the deprecated `LLMS_Person_Handler::update()` method.\n+ Removed the deprecated `LLMS_Person_Handler::validate_fields()` method.\n+ Removed the deprecated `LLMS_Person_Handler::voucher_toggle_script()` method.\n+ Removed the deprecated `templates/admin/notices/db-update.php` file.\n+ Removed the deprecated `templates/admin/notices/db-updating.php` file.\n+ Removed the deprecated `llms_usernames_blacklist` filter hook in the `llms_get_usernames_blocklist()` function.\n+ Removed the deprecated `includes/libraries/wp-background-processing/index.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-async-request.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-background-process.php` file.\n+ Removed the deprecated `LLMS_Section::get_next_available_lesson_order()` method.\n+ Removed the deprecated `LLMS_Section::get_order()` method.\n+ Removed the deprecated `LLMS_Section::get_parent_course()` method.\n+ Removed the deprecated `LLMS_Section::set_parent_course()` method.\n+ Removed the deprecated `LLMS_AJAX::check_voucher_duplicate()` method.\n+ Removed the deprecated `LLMS_AJAX::get_ajax_data()` method.\n+ Removed the deprecated `LLMS_AJAX::register_script()` method.\n+ Removed the deprecated `LLMS_Interface_Post_Audio` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Sales_Page` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Video` interface.\n+ Removed the deprecated `LLMS_Achievements::$_instance` property.\n+ Removed the deprecated `LLMS_Certificates::$_instance` property.\n+ Removed the deprecated `LLMS_Emails::$_instance` property.\n+ Removed the deprecated `LLMS_Engagements::$_instance` property.\n+ Removed the deprecated `LLMS_Events::$_instance` property.\n+ Removed the deprecated `LLMS_Grades::$_instance` property.\n+ Removed the deprecated `LLMS_Integrations::$_instance` property.\n+ Removed the deprecated `LLMS_Payment_Gateways::$_instance` property.\n+ Removed the deprecated `LLMS_Processors::$_instance` property.\n+ Removed the deprecated `LLMS_Sessions::$_instance` property.\n\n##### Developer Notes\n\n+ Added `LLMS_Awards_Query`, used for querying data about awarded certificates and achievements.\n  + The method signature `LLMS_Student::get_achievements()` and `LLMS_Student::get_certificates()` now use this class under tho hood.\n  + The previous method signature, which passed data into a direct SQL query, is now deprecated.\n+ Achievement and certificate data storage locations have been modified, primarily to reduce reliance on the `wp_postmeta` table which will result in a site-wide performance improvement, especially on large sites.\n  + Meta properties `_llms_achievement_content` and `_llms_certificate_content` have been removed in favor of `WP_Post::$post_content`.\n  + Meta properties `_llms_achievement_title` and `_llms_certificate_title` have been removed in favor of `WP_Post::$post_title`.\n  + Meta properties `_llms_achievement_template` and `_llms_certificate_template` have been removed in favor of `WP_Post::$post_parent`.\n  + Meta properties `_llms_achievement_image` and `_llms_certificate_image` have been moved the meta property `_thumbnail_id` in order to utilize the WordPress core's featured image functionality and internal APIs.\n+ Reliance on `lifterlms_user_postmeta` for achievement and certificate data will be removed in a future release.\n  + User postmeta properties `_achievement_earned` and `_certificate_earned` will continue to be recorded but are no longer being used internally.\n  + The `updated_date` is now accessible via `WP_Post::$post_date`.\n  + The `user_id` is now accessible via `WP_Post::$post_author`.\n+ Added new Javascript UI components library, modeled after `@wordpress/components`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/components).\n+ Added a new SVG icon library, modeled after `@wordpress/icons`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/icons).\n+ The merge code button seen on certificate and email template editors is now an SVG image instead of a PNG.\n+ Added utility function for escaping and quoting strings. [#1027](https://github.com/gocodebox/lifterlms/issues/1027)\n+ Added new utility function for stripping prefixes from strings.\n\n##### Performance Improvements\n\n+ Increased the number of files that are autoloaded instead of manually loaded.\n\n##### Updated Templates\n\n+ [templates/achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/achievements/loop.php)\n+ [templates/achievements/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/achievements/template.php)\n+ [templates/admin/notices/db-update.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/admin/notices/db-update.php)\n+ [templates/admin/notices/db-updating.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/admin/notices/db-updating.php)\n+ [templates/admin/reporting/reporting.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/admin/reporting/reporting.php)\n+ [templates/admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/admin/reporting/tabs/courses/overview.php)\n+ [templates/admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/admin/reporting/tabs/memberships/overview.php)\n+ [templates/admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/admin/reporting/tabs/quizzes/overview.php)\n+ [templates/admin/reporting/tabs/students/information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/admin/reporting/tabs/students/information.php)\n+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/archive-course.html)\n+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/archive-llms_membership.html)\n+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/single-no-access.html)\n+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/taxonomy-course_cat.html)\n+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/taxonomy-course_difficulty.html)\n+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/taxonomy-course_tag.html)\n+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/taxonomy-course_track.html)\n+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/taxonomy-membership_cat.html)\n+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/block-templates/taxonomy-membership_tag.html)\n+ [templates/certificates/actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/actions.php)\n+ [templates/certificates/content-legacy.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/content-legacy.php)\n+ [templates/certificates/content.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/content.php)\n+ [templates/certificates/dynamic-styles.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/dynamic-styles.php)\n+ [templates/certificates/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/footer.php)\n+ [templates/certificates/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/header.php)\n+ [templates/certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/loop.php)\n+ [templates/certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/preview.php)\n+ [templates/certificates/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/certificates/template.php)\n+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/checkout/form-confirm-payment.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/checkout/form-switch-source.php)\n+ [templates/content-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/content-certificate.php)\n+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/course/lesson-navigation.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/course/lesson-preview.php)\n+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/course/parent-course.php)\n+ [templates/emails/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/emails/footer.php)\n+ [templates/emails/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/emails/header.php)\n+ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/loop-main.php)\n+ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/loop.php)\n+ [templates/myaccount/my-grades-single-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/myaccount/my-grades-single-table.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/myaccount/view-order-actions.php)\n+ [templates/myaccount/view-order-information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/myaccount/view-order-information.php)\n+ [templates/myaccount/view-order-transactions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/myaccount/view-order-transactions.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/myaccount/view-order.php)\n+ [templates/product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/product/pricing-table.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/quiz/questions/content-picture_choice.php)\n+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/quiz/results.php)\n+ [templates/single-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.2/templates/single-certificate.php)\n\n\nv5.10.0 - 2022-02-22\n--------------------\n\n##### Updates and Enhancements\n\n+ Added an option to specify a custom checkout form title for free access plans. [#1774](https://github.com/gocodebox/lifterlms/issues/1774)\n+ Updated LifterLMS Blocks to [v2.3.2](https://make.lifterlms.com/2022/02/22/lifterlms-blocks-version-2-3-2/). [#1774](https://github.com/gocodebox/lifterlms/issues/1774)\n\n##### Bug Fixes\n\n+ Fixed ability to sort course students table by completed date. [#1969](https://github.com/gocodebox/lifterlms/issues/1969)\n+ Fixed reporting issue encountered when a course has no lessons. [#2012](https://github.com/gocodebox/lifterlms/issues/2012)\n+ Fixed broken checkout on Twenty Twenty-Two Theme when using the password strength meter. [#1997](https://github.com/gocodebox/lifterlms/issues/1997)\n+ Fixed block template slug generation from path in Windows environments. [#2001](https://github.com/gocodebox/lifterlms/issues/2001)\n+ Fixed an issue encountered when using the search box on the voucher admin posts list screen. [#2005](https://github.com/gocodebox/lifterlms/issues/2005)\n\n##### Updated Templates\n\n+ [templates/admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/admin/reporting/tabs/courses/overview.php)\n+ [templates/admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/admin/reporting/tabs/memberships/overview.php)\n+ [templates/admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/admin/reporting/tabs/quizzes/overview.php)\n+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/archive-course.html)\n+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/archive-llms_membership.html)\n+ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/single-certificate.html)\n+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/single-no-access.html)\n+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_cat.html)\n+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_difficulty.html)\n+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_tag.html)\n+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-course_track.html)\n+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-membership_cat.html)\n+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/block-templates/taxonomy-membership_tag.html)\n+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/checkout/form-confirm-payment.php)\n+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/course/lesson-navigation.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/course/lesson-preview.php)\n+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/course/parent-course.php)\n+ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/loop-main.php)\n+ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/loop.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/myaccount/view-order.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/quiz/questions/content-picture_choice.php)\n+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/5.10.0/templates/quiz/results.php)\n\n\nv6.0.0-beta.1 - 2022-02-16\n--------------------------\n\n##### New Features\n\n+ Added a link to return to the student dashboard when viewing an awarded certificate. [#1959](https://github.com/gocodebox/lifterlms/issues/1959)\n+ The block editor is now enabled by default for certificates when using WordPress versions 5.8 and later.\n  + Existing certificates are marked as \"legacy\" and will continue to use the classic editor until migrated.\n  + To migrate a certificate, click the \"Migrate Certificate\" button. This will force the certificate's content into blocks.\n+ A number of new settings are available to certificates when using the block editor:\n  + Set the certificate's display (and print) size using common paper sizes such as US Letter, US Legal, A3, A4, and more.\n  + Set the certificate's display orientation: portrait of landscape.\n  + Set the certificate's inner margins.\n  + Set the certificate's background color.\n+ A new block, the Certificate Title Block, has been made available to certificates.\n  + The block works like a WordPress core Heading Block with added options for selecting from a few display fonts (provided by Google Web Fonts).\n  + The block controls the title of awarded certificates.\n+ + Added the ability to sync awarded certificates with the template used to generate them. [#1078](https://github.com/gocodebox/lifterlms/issues/1078)\n+ The `post_name` of earned certificate posts will be generated with a randomized 3+ character string in favor of relying on sequential numbers.\n+ Added the ability for administrators and LMS managers to edit earned certificates/achievements from the students reporting screen, as well as award new certificates/achievements to students.\n+ + Added certificate global options for the default size of new certificates and certificate templates.\n+ Certificate and email template merge code buttons now include `[llms-user]` information shortcodes.\n+ Added certificate sequential ID functionality merge code.\n\n##### Updates and Enhancements\n\n+ Added pagination to achievement and certificate reporting pages.\n+ Certificates no longer use the `header.php` and `footer.php` files from the site's theme, instead custom templates (`templates/certificates/header.php` and `templates/certificates/footer.php`) are used instead. These templates are minimal and exclude theme wrappers which reduces the visual conflicts encountered from theme wrappers, backgrounds, and more, especially when printing certificates. [#463](https://github.com/gocodebox/lifterlms/issues/463)\n+ The achievements and certificates dashboard endpoints are now paginated. [#669](https://github.com/gocodebox/lifterlms/issues/669)\n+ Added new default images for use with achievements and certificates.\n  + The site-wide default images can be customized on the admin panel under Settings -> Engagements.\n  + The old default images can be used by filtering `llms_use_legacy_engagement_images`. [#1081](https://github.com/gocodebox/lifterlms/issues/1081)\n+ The URL of earned user certificates has been changed from \"my_certificate\" to \"certificate\". Requests to the old url are automatically redirected to the new url, including instances where the URL slug has been translated.\n+ The URL of certificate template previews has been changed from \"certificate\" to \"certificate-template\".\n+ The certificate merge code, `{first_name}`, now outputs an empty string in favor of falling back to the user's nickname when there is no first name for the user. [#1640](https://github.com/gocodebox/lifterlms/issues/1640)\n+ Updates LifterLMS REST to [v1.0.0-beta.22](https://make.lifterlms.com/2021/12/15/lifterlms-rest-api-version-1-0-0-beta-22/).\n+ The look and behavior of the certificate {{MINI_CERTIFICATE}} pop-over notification merge code now displays a placeholder preview of the certificate in favor of attempting to render a tiny version of the actual certificate. [#1950](https://github.com/gocodebox/lifterlms/issues/1950)\n\n##### Bug Fixes\n\n+ + Fixed how the protected `LLMS_Notifications_Query::$found_results` property is accessed in `LLMS_Abstract_Notification_Controller::has_subscriber_received()`. + Fixed how the protected `LLMS_Notifications_Query::$max_pages` property is accessed in `lifterlms_template_student_dashboard_my_notifications()`.\n+ Delayed engagements are automatically unscheduled when the related post is deleted.\n+ Prior to sending a delayed engagement the recipient's enrollment in the related post is verified resulting the engagement not being triggered if the recipient's enrollment has been terminated. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ A disabled student dashboard endpoint will no longer display the endpoint's summary on the main dashboard page. [#535](https://github.com/gocodebox/lifterlms/issues/535)\n+ Post search filter boxes on various post tables will now longer display a link to the selected post.\n+ Basic notification code is no longer loaded on the admin panel.\n\n##### Deprecations\n\n+ Public access to properties of the abstract `LLMS_Database_Query` has been deprecated.\n  + Public access to class property `LLMS_Database_Query::$found_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_found_results()`.\n  + Public access to class property `LLMS_Database_Query::$max_pages`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_max_pages()`.\n  + Public access to class property `LLMS_Database_Query::$number_results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_number_results()`.\n  + Public access to class property `LLMS_Database_Query::$results`. The property is no longer publicly writable but can be read via `LLMS_Database_Query::get_results()`.\n  + Public access to class property `LLMS_Database_Query::$query_vars`. The variable as a whole cannot be publicly accessed, instead use `LLMS_Database_Query::get()` and `LLMS_Database_Query::set()` to read and write to the array.\n  + The above changes were made to the abstract class `LLMS_Database_Query` but the following concrete classes that utilize the abstract are also affected by this change: `LLMS_Query_User_Postmeta`, `LLMS_Student_Query`, `LLMS_Query_Quiz_Attempt`, `LLMS_Events_Query`, and `LLMS_Notifications_Query`.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::create()` is deprecated with no replacement.\n+ Method `LLMS_Achievements::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement()`.\n+ Class `LLMS_Certificate` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n+ Method `LLMS_Certificates::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate()`.\n+ Method `LLMS_Engagements::init()` is deprecated with no replacement.\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Method `LLMS_Database_Query::set_found_results()` is deprecated.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement_User::has_user_earned()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::get_content_html()` is deprecated with no replacement.\n+ Class `LLMS_Certificate_User` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::set_shortcode_user()` is deprecated with no replacement.\n+ Engagement debug logging is removed. Use `llms_log()` directly instead.\n+ Filter `llms_db_query_get_default_args` is deprecated in favor of `llms_{$this->id}_query_get_default_args`.\n+ Filter `llms_certificate_has_user_earned` is deprecated in favor of `llms_earned_certificate_dupcheck`.\n+ Unused public class property `LLMS_Achievements::$content` is deprecated with no replacement.\n+ Method `LLMS_Admin_Post_Types::meta_metabox_init()` is deprecated with no replacement.\n+ The site options `lifterlms_certificate_bg_img_width`, `lifterlms_certificate_bg_img_height`, and `lifterlms_certificate_legacy_image_size` are now used only for certificates and certificate templates created using the classic editor.\n  + The settings, found on the Engagements Settings screen, are hidden by default.\n  + During the database upgrade from versions earlier than 6.x, an site option, `llms_has_legacy_certificates`  is added when at least one certificate is found. This option will display the settings so they can continue to be used for legacy certificates.\n  + After migrating all certificates on a site, the settings will still display. In order to remove them from the screen a developer can either delete the option `llms_has_legacy_certificates` or return `false` from the filter `llms_has_legacy_certificates`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`. [#290](https://github.com/gocodebox/lifterlms/issues/290)\n+ The constant `LLMS_ENGAGEMENT_DEBUG` is deprecated with no replacement.\n+ Engagement debugging via `LLMS_Engagements::log` is deprecated. Use `llms_log()` instead.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Filter `lifterlms_register_post_type_llms_my_certificate` is deprecated in favor of `lifterlms_register_post_type_my_certificate`.\n+ Deprecated the misspelled protected method `LLMS_Database_Query::preprare_query()` and replaced with `LLMS_Database_Query::prepare_query()`.\n  + Class method `LLMS_Events_Query::preprare_query` replaced with `LLMS_Events_Query::prepare_query()`.\n  + Class method `LLMS_Query_Quiz_Attempt::preprare_query` replaced with `LLMS_Query_Quiz_Attempt::prepare_query()`.\n  + Class method `LLMS_Query_User_Postmeta::preprare_query` replaced with `LLMS_Query_User_Postmeta::prepare_query()`.\n  + Class method `LLMS_Student_Query::preprare_query` replaced with `LLMS_Student_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`. [#859](https://github.com/gocodebox/lifterlms/issues/859)\n\n##### Breaking Changes\n\n+ Removed FSE template: `templates/block-templates/single-certificate.html`.\n+ Removed the deprecated `LLMS()` function in favor of the `llms()` function.\n+ Removed the deprecated the `LLMS_SendWP::do_remote_install()` method in favor of the `LLMS_Abstract_Email_Provider::do_remote_install()` method.\n+ Removed the deprecated `LLMS_Abstract_Email_Provider::output_css()` method.\n+ Removed the deprecated `LLMS_Abstract_Generator_Posts::increment()` method.\n+ Removed the deprecated `LLMS_Admin_Users_Table::load_dependencies()` method.\n+ Removed the deprecated `LLMS_Admin_Import::localize_stat()` method.\n+ Removed the deprecated `LLMS_Admin_Notices_Core::check_staging()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::generator_course_status()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::output_step_html()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::scripts()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::watch_course_generation()` method.\n+ Removed the deprecated `llms_format_decimal()` function.\n+ Removed the deprecated `llms_set_person_auth_cookie()` function.\n+ Removed the deprecated `LLMS_Course::sections` property.\n+ Removed the deprecated `LLMS_Course::sku` property.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_pw_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::is_inline_script_enqueued()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::format_date()` method.\n+ Removed the deprecated `LLMS_Generator::get_author_id_from_raw()` method.\n+ Removed the deprecated `LLMS_Generator::get_default_post_status()` method.\n+ Removed the deprecated `LLMS_Generator::get_generated_posts()` method.\n+ Removed the deprecated `LLMS_Generator::increment()` method.\n+ Removed the deprecated `llms__created` action hook from the `LLMS_Abstract_Database_Store::create()` method.\n+ Removed the deprecated `llms__deleted` action hook from the `LLMS_Abstract_Database_Store::delete()` method.\n+ Removed the deprecated `llms__updated` action hook from the `LLMS_Abstract_Database_Store::update()` method.\n+ Removed the deprecated `llms_user_removed_from_membership_level` action hook from the `LLMS_Student::unenroll()` method.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `lifterlms_template_pricing_table()` function.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `templates/product/pricing-table.php` file.\n+ Removed the deprecated `LLMS_Frontend_Password` class.\n+ Removed the deprecated `LLMS_Install::db_updates()` method.\n+ Removed the deprecated `LLMS_Install::update_notice()` method.\n+ Removed the deprecated `LLMS_Notifications::dispatch_processors()` method.\n+ Removed the deprecated `llms_processors_async_dispatching` filter hook from the `LLMS_Notifications::__construct()` method.\n+ Removed the deprecated `LLMS_Notifications::$_instance` property.\n+ Removed the deprecated `LLMS_Person_Handler::register()` method.\n+ Removed the deprecated `LLMS_Person_Handler::sanitize_field()` method.\n+ Removed the deprecated `LLMS_Person_Handler::update()` method.\n+ Removed the deprecated `LLMS_Person_Handler::validate_fields()` method.\n+ Removed the deprecated `LLMS_Person_Handler::voucher_toggle_script()` method.\n+ Removed the deprecated `templates/admin/notices/db-update.php` file.\n+ Removed the deprecated `templates/admin/notices/db-updating.php` file.\n+ Removed the deprecated `llms_usernames_blacklist` filter hook in the `llms_get_usernames_blocklist()` function.\n+ Removed the deprecated `includes/libraries/wp-background-processing/index.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-async-request.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-background-process.php` file.\n+ Removed the deprecated `LLMS_Section::get_next_available_lesson_order()` method.\n+ Removed the deprecated `LLMS_Section::get_order()` method.\n+ Removed the deprecated `LLMS_Section::get_parent_course()` method.\n+ Removed the deprecated `LLMS_Section::set_parent_course()` method.\n+ Removed the deprecated `LLMS_AJAX::check_voucher_duplicate()` method.\n+ Removed the deprecated `LLMS_AJAX::get_ajax_data()` method.\n+ Removed the deprecated `LLMS_AJAX::register_script()` method.\n+ Removed the deprecated `LLMS_Interface_Post_Audio` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Sales_Page` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Video` interface.\n+ Removed the deprecated `LLMS_Achievements::$_instance` property.\n+ Removed the deprecated `LLMS_Certificates::$_instance` property.\n+ Removed the deprecated `LLMS_Emails::$_instance` property.\n+ Removed the deprecated `LLMS_Engagements::$_instance` property.\n+ Removed the deprecated `LLMS_Events::$_instance` property.\n+ Removed the deprecated `LLMS_Grades::$_instance` property.\n+ Removed the deprecated `LLMS_Integrations::$_instance` property.\n+ Removed the deprecated `LLMS_Payment_Gateways::$_instance` property.\n+ Removed the deprecated `LLMS_Processors::$_instance` property.\n+ Removed the deprecated `LLMS_Sessions::$_instance` property.\n\n##### Developer Notes\n\n+ Added `LLMS_Awards_Query`, used for querying data about awarded certificates and achievements.\n  + The method signature `LLMS_Student::get_achievements()` and `LLMS_Student::get_certificates()` now use this class under tho hood.\n  + The previous method signature, which passed data into a direct SQL query, is now deprecated.\n+ Achievement and certificate data storage locations have been modified, primarily to reduce reliance on the `wp_postmeta` table which will result in a site-wide performance improvement, especially on large sites.\n  + Meta properties `_llms_achievement_content` and `_llms_certificate_content` have been removed in favor of `WP_Post::$post_content`.\n  + Meta properties `_llms_achievement_title` and `_llms_certificate_title` have been removed in favor of `WP_Post::$post_title`.\n  + Meta properties `_llms_achievement_template` and `_llms_certificate_template` have been removed in favor of `WP_Post::$post_parent`.\n  + Meta properties `_llms_achievement_image` and `_llms_certificate_image` have been moved the meta property `_thumbnail_id` in order to utilize the WordPress core's featured image functionality and internal APIs.\n+ Reliance on `lifterlms_user_postmeta` for achievement and certificate data will be removed in a future release.\n  + User postmeta properties `_achievement_earned` and `_certificate_earned` will continue to be recorded but are no longer being used internally.\n  + The `updated_date` is now accessible via `WP_Post::$post_date`.\n  + The `user_id` is now accessible via `WP_Post::$post_author`.\n+ Added new Javascript UI components library, modeled after `@wordpress/components`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/components).\n+ Added a new SVG icon library, modeled after `@wordpress/icons`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/icons).\n+ The merge code button seen on certificate and email template editors is now an SVG image instead of a PNG.\n+ Added utility function for escaping and quoting strings. [#1027](https://github.com/gocodebox/lifterlms/issues/1027)\n+ Added new utility function for stripping prefixes from strings.\n\n##### Performance Improvements\n\n+ Increased the number of files that are autoloaded instead of manually loaded.\n\n##### Updated Templates\n\n+ [templates/achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/achievements/loop.php)\n+ [templates/achievements/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/achievements/template.php)\n+ [templates/admin/notices/db-update.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/admin/notices/db-update.php)\n+ [templates/admin/notices/db-updating.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/admin/notices/db-updating.php)\n+ [templates/admin/reporting/reporting.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/admin/reporting/reporting.php)\n+ [templates/admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/admin/reporting/tabs/courses/overview.php)\n+ [templates/admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/admin/reporting/tabs/memberships/overview.php)\n+ [templates/admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/admin/reporting/tabs/quizzes/overview.php)\n+ [templates/admin/reporting/tabs/students/information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/admin/reporting/tabs/students/information.php)\n+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/archive-course.html)\n+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/archive-llms_membership.html)\n+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/single-no-access.html)\n+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/taxonomy-course_cat.html)\n+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/taxonomy-course_difficulty.html)\n+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/taxonomy-course_tag.html)\n+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/taxonomy-course_track.html)\n+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/taxonomy-membership_cat.html)\n+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/block-templates/taxonomy-membership_tag.html)\n+ [templates/certificates/actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/actions.php)\n+ [templates/certificates/content-legacy.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/content-legacy.php)\n+ [templates/certificates/content.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/content.php)\n+ [templates/certificates/dynamic-styles.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/dynamic-styles.php)\n+ [templates/certificates/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/footer.php)\n+ [templates/certificates/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/header.php)\n+ [templates/certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/loop.php)\n+ [templates/certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/preview.php)\n+ [templates/certificates/template.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/certificates/template.php)\n+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/checkout/form-confirm-payment.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/checkout/form-switch-source.php)\n+ [templates/content-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/content-certificate.php)\n+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/course/lesson-navigation.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/course/lesson-preview.php)\n+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/course/parent-course.php)\n+ [templates/emails/footer.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/emails/footer.php)\n+ [templates/emails/header.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/emails/header.php)\n+ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/loop-main.php)\n+ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/loop.php)\n+ [templates/myaccount/my-grades-single-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/myaccount/my-grades-single-table.php)\n+ [templates/myaccount/view-order-actions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/myaccount/view-order-actions.php)\n+ [templates/myaccount/view-order-information.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/myaccount/view-order-information.php)\n+ [templates/myaccount/view-order-transactions.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/myaccount/view-order-transactions.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/myaccount/view-order.php)\n+ [templates/product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/product/pricing-table.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/quiz/questions/content-picture_choice.php)\n+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/quiz/results.php)\n+ [templates/single-certificate.php](https://github.com/gocodebox/lifterlms/blob/6.0.0-beta.1/templates/single-certificate.php)\n\n\nv5.9.0 - 2022-02-15\n-------------------\n\n##### Updates and Enhancements\n\n+ Picture choice questions are now organized using flexbox in favor of a float-powered column layout.\n+ Resolved PHP 8.1 deprecation warnings. [#1859](https://github.com/gocodebox/lifterlms/issues/1859)\n\n##### Bug Fixes\n\n+ Updated `llms_get_endpoint_url()` to better adhere to a site's permalink structure with regards to the presence of a trailing slash in the generated url. [#1983](https://github.com/gocodebox/lifterlms/issues/1983)\n+ Only allow users with `edit_post` capabilities to bypass content restrictions.\n+ Fixed stretched images in quiz description/questions when using the Twenty Twenty-Two theme. [#1976](https://github.com/gocodebox/lifterlms/issues/1976)\n\n##### Deprecations\n\n+ Method `LLMS_AJAX::check_voucher_duplicate()` is deprecated in favor of `LLMS_AJAX_HANDLER::check_voucher_duplicate()`.\n\n##### Updated Templates\n\n+ [templates/admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/admin/reporting/tabs/courses/overview.php)\n+ [templates/admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/admin/reporting/tabs/memberships/overview.php)\n+ [templates/admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/admin/reporting/tabs/quizzes/overview.php)\n+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/archive-course.html)\n+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/archive-llms_membership.html)\n+ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/single-certificate.html)\n+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/single-no-access.html)\n+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_cat.html)\n+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_difficulty.html)\n+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_tag.html)\n+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-course_track.html)\n+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-membership_cat.html)\n+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/block-templates/taxonomy-membership_tag.html)\n+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/checkout/form-confirm-payment.php)\n+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/course/lesson-navigation.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/course/lesson-preview.php)\n+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/course/parent-course.php)\n+ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/loop-main.php)\n+ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/loop.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/myaccount/view-order.php)\n+ [templates/quiz/questions/content-picture_choice.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/quiz/questions/content-picture_choice.php)\n+ [templates/quiz/results.php](https://github.com/gocodebox/lifterlms/blob/5.9.0/templates/quiz/results.php)\n\n\nv6.0.0-alpha.4 - 2022-02-11\n---------------------------\n\n##### Updates and Enhancements\n\n+ Removed usage of PHP features deprecated in PHP 8.1.\n+ Added a link to return to the student dashboard when viewing an awarded certificate.\n+ Allow block templates to be overridden from themes or plugins.\n+ Added a \"Reset Certificate\" button to restore certificates to the default template.\n+ Added links from achievement and certificate templates to view all awards generated from the template.\n+ Added the ability to sync achievements (sync all awards to the parent template and sync one award to it's parent).\n+ Improved class autoloading.\n\n##### Bug Fixes\n\n+ Fixed certificate print compatibility issues with the OceanWP and Genesis themes.\n+ Fixed custom font usage in the Certificate Title block to utilize WP Core functionality introduced in version 5.9.\n+ Fixed access to protected properties in the `LLMS_Notifications_Query` class.\n\n##### Breaking Changes\n\n+ Removed the Single Certificate block template.\n\n\nv5.8.0 - 2022-01-26\n-------------------\n\n##### New Features\n\n+ Add theme support for the Twenty Twenty-Two theme. [#1824](https://github.com/gocodebox/lifterlms/issues/1824)\n+ Added WordPress Full Site Editing compatibility for various LifterLMS-powered templates.\n\n##### Updates and Enhancements\n\n+ The minimum required WordPress core version is now version 5.5.\n+ Tested against WordPress version 5.9.\n+ Updated LifterLMS Blocks: [v2.3.0](https://make.lifterlms.com/2022/01/25/lifterlms-blocks-version-2-3-0/), [v2.3.1](https://make.lifterlms.com/2022/01/26/lifterlms-blocks-version-2-3-1/).\n+ Remove the \"description\" registered with LifterLMS custom post types. [#710](https://github.com/gocodebox/lifterlms/issues/710)\n\n##### Updated Templates\n\n+ [templates/block-templates/archive-course.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/archive-course.html)\n+ [templates/block-templates/archive-llms_membership.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/archive-llms_membership.html)\n+ [templates/block-templates/single-certificate.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/single-certificate.html)\n+ [templates/block-templates/single-no-access.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/single-no-access.html)\n+ [templates/block-templates/taxonomy-course_cat.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_cat.html)\n+ [templates/block-templates/taxonomy-course_difficulty.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_difficulty.html)\n+ [templates/block-templates/taxonomy-course_tag.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_tag.html)\n+ [templates/block-templates/taxonomy-course_track.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-course_track.html)\n+ [templates/block-templates/taxonomy-membership_cat.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-membership_cat.html)\n+ [templates/block-templates/taxonomy-membership_tag.html](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/block-templates/taxonomy-membership_tag.html)\n+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/course/lesson-navigation.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/course/lesson-preview.php)\n+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/course/parent-course.php)\n+ [templates/loop-main.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/loop-main.php)\n+ [templates/loop.php](https://github.com/gocodebox/lifterlms/blob/5.8.0/templates/loop.php)\n\n\nv6.0.0-alpha.3 - 2022-01-14\n---------------------------\n\n##### Updates and Enhancements\n\n+ Automatically dequeue print-only stylesheets to reduce theme and plugin conflicts when printing certificates.\n+ Only enable the Certificate Title block font-family selector for WordPress 5.9 and later.\n+ Only enable the Block Editor for certificates on WordPress 5.8 and later.\n+ Replaced welcome message placeholder text with a real welcome message.\n\n##### Bug Fixes\n\n+ Explicitly define a default font-family (\"default\") for the Certificate Title block.\n+ Fixed visual issues encountered on certificates when resizing the browser window.\n+ Fixed issue with the certificate block template on WordPress 5.8 (divider blocks aren't centered by default).\n\n##### Breaking Changes\n\n+ Removed the deprecated `LLMS()` function in favor of the `llms()` function.\n+ Removed the deprecated `LLMS_SendWP::do_remote_install()` method in favor of the `LLMS_Abstract_Email_Provider::do_remote_install()` method.\n+ Removed the deprecated `LLMS_Abstract_Email_Provider::output_css()` method.\n+ Removed the deprecated `LLMS_Abstract_Generator_Posts::increment()` method.\n+ Removed the deprecated `LLMS_Admin_Users_Table::load_dependencies()` method.\n+ Removed the deprecated `LLMS_Admin_Import::localize_stat()` method.\n+ Removed the deprecated `LLMS_Admin_Notices_Core::check_staging()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::generator_course_status()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::output_step_html()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::scripts()` method.\n+ Removed the deprecated `LLMS_Admin_Setup_Wizard::watch_course_generation()` method.\n+ Removed the deprecated `llms_format_decimal()` function.\n+ Removed the deprecated `llms_set_person_auth_cookie()` function.\n+ Removed the deprecated `LLMS_Course::sections` property.\n+ Removed the deprecated `LLMS_Course::sku` property.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_pw_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::enqueue_inline_script()` method.\n+ Removed the deprecated `LLMS_Frontend_Assets::is_inline_script_enqueued()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::add_custom_values()` method.\n+ Removed the deprecated `LLMS_Generator::format_date()` method.\n+ Removed the deprecated `LLMS_Generator::get_author_id_from_raw()` method.\n+ Removed the deprecated `LLMS_Generator::get_default_post_status()` method.\n+ Removed the deprecated `LLMS_Generator::get_generated_posts()` method.\n+ Removed the deprecated `LLMS_Generator::increment()` method.\n+ Removed the deprecated `llms__created` action hook from the `LLMS_Abstract_Database_Store::create()` method.\n+ Removed the deprecated `llms__deleted` action hook from the `LLMS_Abstract_Database_Store::delete()` method.\n+ Removed the deprecated `llms__updated` action hook from the `LLMS_Abstract_Database_Store::update()` method.\n+ Removed the deprecated `llms_user_removed_from_membership_level` action hook from the `LLMS_Student::unenroll()` method.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `lifterlms_template_pricing_table()` function.\n+ Removed the deprecated and misspelled `$purchaseable` global variable in the `templates/product/pricing-table.php` file.\n+ Removed the deprecated `LLMS_Frontend_Password` class.\n+ Removed the deprecated `LLMS_Install::db_updates()` method.\n+ Removed the deprecated `LLMS_Install::update_notice()` method.\n+ Removed the deprecated `LLMS_Notifications::dispatch_processors()` method.\n+ Removed the deprecated `llms_processors_async_dispatching` filter hook from the `LLMS_Notifications::__construct()` method.\n+ Removed the deprecated `LLMS_Notifications::$_instance` property.\n+ Removed the deprecated `LLMS_Person_Handler::register()` method.\n+ Removed the deprecated `LLMS_Person_Handler::sanitize_field()` method.\n+ Removed the deprecated `LLMS_Person_Handler::update()` method.\n+ Removed the deprecated `LLMS_Person_Handler::validate_fields()` method.\n+ Removed the deprecated `LLMS_Person_Handler::voucher_toggle_script()` method.\n+ Removed the deprecated `templates/admin/notices/db-update.php` file.\n+ Removed the deprecated `templates/admin/notices/db-updating.php` file.\n+ Removed the deprecated `llms_usernames_blacklist` filter hook in the `llms_get_usernames_blocklist()` function.\n+ Removed the deprecated `includes/libraries/wp-background-processing/index.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-async-request.php` file.\n+ Removed the deprecated `includes/libraries/wp-background-processing/wp-background-process.php` file.\n+ Removed the deprecated `LLMS_Section::get_next_available_lesson_order()` method.\n+ Removed the deprecated `LLMS_Section::get_order()` method.\n+ Removed the deprecated `LLMS_Section::get_parent_course()` method.\n+ Removed the deprecated `LLMS_Section::set_parent_course()` method.\n+ Removed the deprecated `LLMS_AJAX::get_ajax_data()` method.\n+ Removed the deprecated `LLMS_AJAX::register_script()` method.\n+ Removed the deprecated `LLMS_Interface_Post_Audio` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Sales_Page` interface.\n+ Removed the deprecated `LLMS_Interface_Post_Video` interface.\n+ Removed the deprecated `LLMS_Achievements::$_instance` property.\n+ Removed the deprecated `LLMS_Certificates::$_instance` property.\n+ Removed the deprecated `LLMS_Emails::$_instance` property.\n+ Removed the deprecated `LLMS_Engagements::$_instance` property.\n+ Removed the deprecated `LLMS_Events::$_instance` property.\n+ Removed the deprecated `LLMS_Grades::$_instance` property.\n+ Removed the deprecated `LLMS_Integrations::$_instance` property.\n+ Removed the deprecated `LLMS_Payment_Gateways::$_instance` property.\n+ Removed the deprecated `LLMS_Processors::$_instance` property.\n+ Removed the deprecated `LLMS_Sessions::$_instance` property.\n\n\nv5.7.0 - 2022-01-11\n-------------------\n\n##### Updates and Enhancements\n\n+ Informed developers about the deprecated `LLMS_Section::get_next_available_lesson_order()` method.\n+ Informed developers about the deprecated `LLMS_Section::get_order()` method.\n+ Informed developers about the deprecated `LLMS_Section::get_parent_course()` method.\n+ Informed developers about the deprecated `LLMS_Section::set_parent_course()` method.\n\n##### Deprecations\n\n+ Deprecated `LLMS_Frontend_Assets::enqueue_inline_pw_script()` with no replacement.\n+ Deprecated the `LLMS_Lesson::get_order()` method in favor of the `LLMS_Lesson::get( 'order' )` method.\n+ Deprecated the `LLMS_Lesson::get_parent_course()` method in favor of the `LLMS_Lesson::get( 'parent_course' )` method.\n+ Deprecated the `LLMS_Lesson::set_parent_course()` method in favor of the `LLMS_Lesson::set( 'parent_course', $course_id )` method.\n+ Deprecated the `LLMS_AJAX_Handler::add_lesson_to_course()` method with no replacement.\n+ Deprecated the `LLMS_AJAX_Handler::create_lesson()` method with no replacement.\n+ Deprecated the `LLMS_AJAX_Handler::create_section()` method with no replacement.\n+ Deprecated the `LLMS_Lesson_Handler::assign_to_course()` method with no replacement.\n+ Deprecated the `LLMS_Post_Handler::create_section()` method with no replacement.\n\n##### Updated Templates\n\n+ [templates/course/lesson-navigation.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/course/lesson-navigation.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/course/lesson-preview.php)\n+ [templates/course/parent-course.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/course/parent-course.php)\n\n\nv6.0.0-alpha.2 - 2022-01-04\n---------------------------\n\n##### New Features\n\n+ Added certificate global options for the default size of new certificates and certificate templates.\n\n##### Updates and enhancements\n\n+ The site options `lifterlms_certificate_bg_img_width`,\n`lifterlms_certificate_bg_img_height`, and\n`lifterlms_certificate_legacy_image_size` are now used only for certificates\nand certificate templates created using the classic editor.\n  + The settings, found on the Engagements Settings screen, are hidden by default.\n  + During the database upgrade from versions earlier than 6.x, an site option, `llms_has_legacy_certificates`  is added when at least one certificate is found. This option will display the settings so they can continue to be used for legacy certificates.\n  + After migrating all certificates on a site, the settings will still display. In order to remove them from the screen a developer can either delete the option `llms_has_legacy_certificates` or return `false` from the filter `llms_has_legacy_certificates`.\n+ Restore certificate save hooks after executing callback updates to facilitate scenarios where more than one certificate is updated in a single request.\n\n##### Bug Fixes\n\n+ Only register the Certificate Title block for use on certificate post types.\n\n##### Updated Templates\n\n+ [templates/certificates/content-legacy.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/content-legacy.php)\n\n\nv6.0.0-alpha.1 - 2021-12-28\n---------------------------\n\n**This version is an unstable pre-release! We strongly advise against installing this in a production environment.**\n\n##### New Features\n\n+ The block editor is now enabled by default for certificates.\n  + Existing certificates are marked as \"legacy\" and will continue to use the classic editor until migrated.\n  + To migrate a certificate, click the \"Migrate Certificate\" button. This will force the certificate's content into blocks.\n+ A number of new settings are available to certificates when using the block editor:\n  + Set the certificate's display (and print) size using common paper sizes such as US Letter, US Legal, A3, A4, and more.\n  + Set the certificate's display orientation: portrait of landscape.\n  + Set the certificate's inner margins.\n  + Set the certificate's background color.\n+ A new block, the Certificate Title Block, has been made available to certificates.\n  + The block works like a WordPress core Heading Block with added options for selecting from a few display fonts (provided by Google Web Fonts).\n  + The block controls the title of awarded certificates.\n+ + Added the ability to sync awarded certificates with the template used to generate them. [#1078](https://github.com/gocodebox/lifterlms#1078)\n+ The `post_name` of earned certificate posts will be generated with a randomized 3+ character string in favor of relying on sequential numbers.\n+ Added the ability for administrators and LMS managers to edit earned certificates/achievements from the students reporting screen, as well as award new certificates/achievements to students.\n+ Certificate and email template merge code buttons now include [llms-user] information shortcodes.\n+ Added certificate sequential ID functionality merge code. [Read more](@TODO).\n\n##### Updates and Enhancements\n\n+ Added pagination to achievement and certificate reporting pages.\n+ Certificates no longer use the `header.php` and `footer.php` files from the site's theme, instead custom templates (`templates/certificates/header.php` and `templates/certificates/footer.php`) are used instead. These templates are minimal and exclude theme wrappers which reduces the visual conflicts encountered from theme wrappers, backgrounds, and more, especially when printing certificates. [#463](https://github.com/gocodebox/lifterlms#463)\n+ The achievements and certificates dashboard endpoints are now paginated. [#669](https://github.com/gocodebox/lifterlms#669)\n+ Added new default images for use with achievements and certificates.\n  + The site-wide default images can be customized on the admin panel under Settings -> Engagements.\n  + The old default images can be used by filtering `llms_use_legacy_engagement_images`. [#1081](https://github.com/gocodebox/lifterlms#1081)\n+ The URL of earned user certificates has been changed from \"my_certificate\" to \"certificate\". Requests to the old url are automatically redirected to the new url, including instances where the URL slug has been translated.\n+ The URL of certificate template previews has been changed from \"certificate\" to \"certificate-template\".\n+ The certificate merge code, `{first_name}`, now outputs an empty string in favor of falling back to the user's nickname when there is no first name for the user. [#1640](https://github.com/gocodebox/lifterlms#1640)\n+ Updates LifterLMS REST to [v1.0.0-beta.22](https://make.lifterlms.com/2021/12/15/lifterlms-rest-api-version-1-0-0-beta-22/).\n\n##### Bug Fixes\n\n+ Delayed engagements are automatically unscheduled when the related post is deleted.\n+ Prior to sending a delayed engagement the recipient's enrollment in the related post is verified resulting the engagement not being triggered if the recipient's enrollment has been terminated. [#290](https://github.com/gocodebox/lifterlms#290)\n+ A disabled student dashboard endpoint will no longer display the endpoint's summary on the main dashboard page. [#535](https://github.com/gocodebox/lifterlms#535)\n+ Post search filter boxes on various post tables will now longer display a link to the selected post.\n+ Basic notification code is no longer loaded on the admin panel.\n\n##### Deprecations\n\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Achievement::create()` is deprecated with no replacement.\n+ Method `LLMS_Achievments::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement()`.\n+ Class `LLMS_Certificate` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate::is_enabled()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_blogname()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::format_string()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate::get_title()` is deprecated with no replacement.\n+ Method `LLMS_Certificates::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate()`.\n+ Method `LLMS_Engagements::init()` is deprecated with no replacement.\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Method `LLMS_Database_Query::set_found_results()` is deprecated.\n+ Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n  + Method `LLMS_Achievement_User::has_user_earned()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Achievement_User::get_content_html()` is deprecated with no replacement.\n+ Class `LLMS_Certificate_User` is deprecated with no direct replacement.\n  + Method `LLMS_Certificate_User::init()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::trigger()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::get_content_html()` is deprecated with no replacement.\n  + Method `LLMS_Certificate_User::set_shortcode_user()` is deprecated with no replacement.\n+ Engagement debug logging is removed. Use `llms_log()` directly instead.\n+ Filter `llms_db_query_get_default_args` is deprecated in favor of `llms_{$this->id}_query_get_default_args`.\n+ Filter `llms_certificate_has_user_earned` is deprecated in favor of `llms_earned_certificate_dupcheck`.\n+ Unused public class property `LLMS_Achievements::$content` is deprecated with no replacement.\n+ Method `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`. [#290](https://github.com/gocodebox/lifterlms#290)\n+ Method `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`. [#290](https://github.com/gocodebox/lifterlms#290)\n+ The constant `LLMS_ENGAGEMENT_DEBUG` is deprecated with no replacement.\n+ Engagement debugging via `LLMS_Engagements::log` is deprecated. Use `llms_log()` instead.\n+ Method `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n+ Filter `lifterlms_register_post_type_llms_my_certificate` is deprecated in favor of `lifterlms_register_post_type_my_certificate`.\n+ Deprecated the misspelled protected method `LLMS_Database_Query::preprare_query()` and replaced with `LLMS_Database_Query::prepare_query()`.\n  + Class method `LLMS_Events_Query::preprare_query` replaced with `LLMS_Events_Query::prepare_query()`.\n  + Class method `LLMS_Query_Quiz_Attempt::preprare_query` replaced with `LLMS_Query_Quiz_Attempt::prepare_query()`.\n  + Class method `LLMS_Query_User_Postmeta::preprare_query` replaced with `LLMS_Query_User_Postmeta::prepare_query()`.\n  + Class method `LLMS_Student_Query::preprare_query` replaced with `LLMS_Student_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`.\n  + Class method `LLMS_Notifications_Query::preprare_query` replaced with `LLMS_Notifications_Query::prepare_query()`. [#859](https://github.com/gocodebox/lifterlms#859)\n\n##### Developer Notes\n\n+ Added `LLMS_Awards_Query`, used for querying data about awarded certificates and achievements.\n  + The method signature `LLMS_Student::get_achievements()` and `LLMS_Student::get_certificates()` now use this class under tho hood.\n  + The previous method signature, which passed data into a direct SQL query, is now deprecated.\n+ Achievement and certificate data storage locations have been modified, primarily to reduce reliance on the `wp_postmeta` table which will result in a site-wide performance improvement, especially on large sites.\n  + Meta properties `_llms_achievement_content` and `_llms_certificate_content` have been removed in favor of `WP_Post::$post_content`.\n  + Meta properties `_llms_achievement_title` and `_llms_certificate_title` have been removed in favor of `WP_Post::$post_title`.\n  + Meta properties `_llms_achievement_template` and `_llms_certificate_template` have been removed in favor of `WP_Post::$post_parent`.\n  + Meta properties `_llms_achievement_image` and `_llms_certificate_image` have been moved the meta property `_thumbnail_id` in order to utilize the WordPress core's featured image functionality and internal APIs.\n+ Reliance on `lifterlms_user_postmeta` for achievement and certificate data will be removed in a future release.\n  + User postmeta properties `_achievement_earned` and `_certificate_earned` will continue to be recorded but are no longer being used internally.\n  + The `updated_date` is now accessible via `WP_Post::$post_date`.\n  + The `user_id` is now accessible via `WP_Post::$post_author`.\n+ Added new Javascript UI components library, modeled after `@wordpress/components`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/components).\n+ Added a new SVG icon library, modeled after `@wordpress/icons`. [Read more](https://github.com/gocodebox/lifterlms/tree/dev-600/packages/icons).\n+ The merge code button seen on certificate and email template editors is now an SVG image instead of a PNG.\n+ Added utility function for escaping and quoting strings. [#1027](https://github.com/gocodebox/lifterlms#1027)\n+ Added new utility function for stripping prefixes from strings.\n\n##### Updated Templates\n\n+ [templates/achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/achievements/loop.php)\n+ [templates/achievements/template.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/achievements/template.php)\n+ [templates/admin/reporting/tabs/students/information.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/admin/reporting/tabs/students/information.php)\n+ [templates/certificates/actions.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/actions.php)\n+ [templates/certificates/content-legacy.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/content-legacy.php)\n+ [templates/certificates/content.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/content.php)\n+ [templates/certificates/dynamic-styles.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/dynamic-styles.php)\n+ [templates/certificates/footer.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/footer.php)\n+ [templates/certificates/header.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/header.php)\n+ [templates/certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/loop.php)\n+ [templates/certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/preview.php)\n+ [templates/certificates/template.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/certificates/template.php)\n+ [templates/content-certificate.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/content-certificate.php)\n+ [templates/single-certificate.php](https://github.com/gocodebox/lifterlms/blob/trunk/templates/single-certificate.php)\n\n\nv5.6.0 - 2021-12-07\n-------------------\n\n##### New Features\n\n+ Added an option to prevent users (by role) from copying site content and saving local copies of images.\n+ Added new site setting to disallow concurrent user sessions for specified user roles.\n\n##### Updates and Enhancements\n\n+ Updates LifterLMS REST to [v1.0.0-beta.21](https://make.lifterlms.com/2021/12/07/lifterlms-rest-api-version-1-0-0-beta-21/).\n\n##### Developer Notes\n\n+ Database migration functions can now be namespaced, eliminating the need to prefix update function names with a version number.\n\n\nv5.5.0 - 2021-11-05\n-------------------\n\n##### New Features\n\n+ Includes the LLMS-CLI beta, a set of WP-CLI commands for LifterLMS and LifterLMS add-ons, as part of the core plugin:\n  + To get started, run `wp llms --help` in your terminal or read the [online command documentation](https://developer.lifterlms.com/cli/commands/llms/).\n  + Please note that the LLMS-CLI is included as a public beta feature. The command API is in a pre-release state and, as such, is subject to change without warning.\n  + If you encounter any issues or wish to provide feedback on the LLMS-CLI please get in touch at [https://github.com/gocodebox/lifterlms-cli](https://github.com/gocodebox/lifterlms-cli).\n\n##### Bug Fixes\n\n+ Fix AJAX post search when using search queries containing quotes.\n\n##### Deprecations\n\n+ The `lifterlms_register_post_type_llms_engagement` is deprecated in favor of `lifterlms_register_post_type_engagement`.\n+ The `lifterlms_register_post_type_llms_achievement` is deprecated in favor of `lifterlms_register_post_type_achievement`.\n+ The `lifterlms_register_post_type_llms_certificate` is deprecated in favor of `lifterlms_register_post_type_certificate`.\n+ The `lifterlms_register_post_type_llms_my_certificate` is deprecated in favor of `lifterlms_register_post_type_my_certificate`.\n+ The `lifterlms_register_post_type_llms_email` is deprecated in favor of `lifterlms_register_post_type_email`.\n+ The `lifterlms_register_post_type_llms_coupon` is deprecated in favor of `lifterlms_register_post_type_coupon`.\n+ The `lifterlms_register_post_type_llms_voucher` is deprecated in favor of `lifterlms_register_post_type_voucher`.\n\n##### Developer Notes\n\n+ The `llms-addons` style asset no longer ships an unminified version.\n+ The `llms-admin-add-ons` style asset no longer ships an unminified version and the filename of the distributed file has changed.\n+ All the LifterLMS post types are now registered using the static method `LLMS_Post_Types::register_post_type()`.\n+ Upgraded woocommerce/action-scheduler to [v3.4.0](https://github.com/woocommerce/action-scheduler/releases/tag/3.4.0).\n\n\nv5.4.1 - 2021-10-26\n-------------------\n\n##### Bug fixes\n\n+ Exclude internal-use-only properties (related to reporting caches and student counts) when exporting or cloning courses. [#1532](https://github.com/gocodebox/lifterlms/issues/1532)\n+ Don't sanitize input from user forms until validation has succeeded. [#1829](https://github.com/gocodebox/lifterlms/issues/1829.)\n+ Fixed an issue encountered when fields are removed from reusable blocks, causing some user forms from functioning as expected. [#1832](https://github.com/gocodebox/lifterlms/issues/1832)\n\n\nv5.4.0 - 2021-10-14\n-------------------\n\n##### Updates\n\n+ Added logic to prevent the permanent deletion of courses or memberships with active subscriptions.\n+ When a subscription attempts to charge a recurring payment against a deleted course or membership the transaction will be cancelled and the order marked as failed.\n+ Updates LifterLMS Blocks to [v2.2.1](https://make.lifterlms.com/2021/09/29/lifterlms-blocks-version-2-2-1/).\n+ Updates LifterLMS REST to [v1.0.0-beta.20](https://make.lifterlms.com/2021/10/11/lifterlms-rest-api-version-1-0-0-beta-20/).\n\n##### Bug fixes\n\n+ Fixed issue encountered when cloning lessons with attached assignments.\n+ Fixed an error encountered when viewing an order for a deleted course or membership on the student dashboard.\n\n##### Templates Updated\n\n+ templates/myaccount/view-order.php\n\n\nv5.3.3 - 2021-10-05\n-------------------\n\n##### Updates\n\n+ Update woocommerce/actions-scheduler to version 3.3.0.\n\n##### Bug fixes\n\n+ Fixed an issue causing the latest earned achievement to not display on the \"My Grades\" tab in certain scenarios.\n+ Fix issue causing a `waiting...` message to display on the JS dev console.\n+ Fix improper usage of `apply_filters_deprecated()` encountered when using deprecated theme settings filters in the course builder.\n+ Fixed missing text domain, thanks [chetansatasiya](https://github.com/chetansatasiya)!\n\n##### Developer notes\n\n+ Improved the `LLMS.waitFor()` runtime JS dependency loader to output improved debugging information.\n\n\nv5.3.2 - 2021-09-21\n-------------------\n\n##### Updates\n\n+ Updated the SendWP integration account management URL.\n\n##### Bug fixes\n\n+ Fixed issue encountered with TinyMCE editor instances in repeater metabox groups.\n+ Fixed issue causing the latest achievement to not display when reviewing grades on the student dashboard.\n\n\nv5.3.1 - 2021-09-13\n-------------------\n\n##### Bug fixes\n\n+ Fixed quote slashing for non-admin roles when editing content in the course builder.\n+ The LifterLMS admin icon now uses an encoded SVG to improve admin color scheme compatibility.\n+ Fixed an issue with empty admin notices.\n\n##### Dev updates\n\n+ The creation date of `llms_orders` is now determined by `llms_current_time()`.\n\n\nv5.3.0 - 2021-08-31\n-------------------\n\n##### Updates\n\n+ Improved logic used to determine when a limited length subscription has completed its payment schedule.\n+ Improved accessibility of various icon buttons on the admin orders view/edit screen.\n+ Improved display of quiz attempts containing questions which have been deleted from the database.\n+ POT files from included library plugins (like LifterLMS REST) are now excluded from LifterLMS distributions.\n\n##### Development updates\n\n+ Introduced `LLMS_Trait_Singleton` to replace redundant singleton pattern definitions across classes in the codebase.\n+ Moved the loading of the autoloader to the main `lifterlms.php` file.\n+ Updated the `LLMS_Payment_Gateway` abstract class to utilize `LLMS_Abstract_Options_Data` for accessing gateway options.\n+ Audio and video embed methods shared by `LLMS_Course` and `LLMS_Membership` have been relocated to `LLMS_Trait_Audio_Video_Embed`.\n+ Sales page methods shared by `LLMS_Course` and `LLMS_Membership` have been relocated to `LLMS_Trait_Sales_Page`.\n\n##### Bug Fixes\n\n+ Fixed a visual issue encountered on the payment confirmation screen on small screens / mobile devices.\n+ Fix untranslatable time period strings (day, week, month, and year) found on the admin orders view/edit screen.\n+ Fixed an error encountered when attempting to grade a quiz attempt containing deleted questions.\n\n##### Deprecations\n\n+ Removed usage and references to the `LLMS_Order` post meta property `date_billing_end`. To determine if a subscription has ended, use `LLMS_Order::get_remaining_payments()` instead.\n+ Removed private method `LLMS_Order::calculate_billing_end_date()`.\n+ Deprecated the class property `$_instance` from the following classes, use the public method `instance()` instead:\n  + `LLMS_Achievements`\n  + `LLMS_Certificates`\n  + `LLMS_Emails`\n  + `LLMS_Engagements`\n  + `LLMS_Events`\n  + `LLMS_Grades`\n  + `LLMS_Integrations`\n  + `LLMS_Notifications`\n  + `LLMS_Payment_Gateways`\n  + `LLMS_Processors`\n  + `LLMS_Sessions`\n\n##### Templates Updated\n\n+ templates/checkout/form-confirm-payment.php\n+ templates/admin/reporting/tabs/quizzes/attempt.php\n+ templates/quiz/results-attempt-questions-list.php\n\n\nv5.2.1 - 2021-08-17\n-------------------\n\n##### Updates\n\n+ [LifterLMS Helper Version 3.4.1](https://make.lifterlms.com/2021/08/17/lifterlms-helper-version-3-4-1/).\n+ Made minor development-related changes to the `LLMS_Order` class.\n\n##### Bug Fixes\n\n+ Fixed an issue encountered when a course or membership sales page redirect is enabled but no URL is saved.\n\n\nv5.2.0 - 2021-08-10\n-------------------\n\n##### Upcoming Payment Reminder Notification\n\n+ A new notification, the \"Upcoming Payment Reminder\" notification has been added. This notification sends a reminder to students a configurable number of days before a payment is do for a recurring subscription.\n+ When upgrading to version 5.2.0, this notification will be automatically *disabled*, visit LifterLMS -> Settings -> Notifications and select the new notification to enable it after upgrading.\n+ Props to [@niluzok](https://github.com/niluzok) for doing the initial work required to build this notification!\n\n##### Updates\n\n+ Reworked the database upgrader script to allow for minor upgrades which don't require significant data migration to upgrade silently without requiring user consent to initiate.\n+ Improved internal methods used to generate tables in the body of email notifications.\n\n##### Bug Fixes\n\n+ Student registration date is now displayed in the site's timezone in favor of UTC time.\n+ Properly pass options `template_path` and `default_path` to the template handler when creating an admin notice using a template.\n+ Removed translation (and incorrect text domain) from a logging function encountered when a recurring payment errors as a result of the payment gateway having been deactivated.\n\n##### Deprecations\n\n+ `LLMS_Install::db_updates()` is deprecated, use ``LLMS_DB_Upgrader::enqueue_updates()` instead.\n+ `LLMS_Install::update_notice()` is deprecated with no replacement.\n+ Template `admin/notices/db-update.php` is deprecated in favor of `includes/admin/views/db-update.php`.\n+ Template `admin/notices/db-updating.php` is deprecated with no replacement.\n\n\nv5.1.3 - 2021-08-04\n-------------------\n\n+ Bugfix: Fixed an issue where a white box would be output over the certificate background image.\n+ Bugfix: Fixed an issue in the course builder causing lessons to be orphaned from a course when moved into an unsaved section.\n+ [LifterLMS Helper Version 3.4.0](https://make.lifterlms.com/2021/08/04/lifterlms-helper-version-3-4-0/)\n\n\nv5.1.2 - 2021-07-28\n-------------------\n\n+ Bugfix: Pass second parameter to the `get_the_excerpt` filter.\n+ Fix: Corrected typos in error messages encountered during password reset.\n\n\nv5.1.1 - 2021-07-26\n-------------------\n\n+ Bugfix: Fixed a bug causing malformed character codes to be rendered in forms when installing forms with translated labels.\n+ [LifterLMS Helper version 3.3.1](https://make.lifterlms.com/2021/07/26/lifterlms-helper-version-3-3-1/)\n\n\nv5.1.0 - 2021-07-19\n-------------------\n\n##### Updates\n\n+ **Raised the minimum required WordPress core version to 5.8!**\n+ Adds WordPress core 5.8 compatibility.\n+ Improved user information forms required field validation.\n+ Added functionality to ensure that user email and password fields are *always* displayed to logged out users on checkout and registration forms.\n+ Added functionality to ensure that user email and password fields are *always* displayed on the account edit form.\n+ [LifterLMS Blocks version 2.2.0](https://make.lifterlms.com/2021/07/19/lifterlms-blocks-version-2-2-0/)\n\n##### Bug fixes\n\n+ Fixed an issue preventing certain orphaned quizzes from being deleted.\n+ Prevent users from submitting a password change without submitting their current password.\n+ Allow logged in users to checkout when no form fields are set to display.\n\n\nv5.0.2 - 2021-07-08\n-------------------\n\n##### LifterLMS Blocks\n\n+ Upgraded to [version 2.1.1](https://make.lifterlms.com/2021/07/08/lifterlms-blocks-version-2-1-1/).\n\n##### Bug Fixes\n\n+ Fixed issue with non-Latin characters in dashboard endpoint URL slugs.\n+ Fixed issue preventing address localization when using the [lifterlms_registration] shortcode.\n\n\nv5.0.1 - 2021-06-28\n-------------------\n\n##### Updates\n\n+ Update to [LifterLMS Blocks v2.1.0](https://make.lifterlms.com/2021/06/28/lifterlms-blocks-version-2-1-0/).\n+ Added a new filter to allow programmatically alter required field validation results.\n\n##### Bugfixes\n\n+ Fixed an issue causing preventing form layout options from working when passed into shortcodes.\n+ Fixed an issue preventing custom radio, select, and dropdown fields from working during checkout.\n+ Fixed an accessibility issue encountered during password strength validation.\n\n\nv5.0.0 - 2021-06-22\n-------------------\n\n##### User Information Form Builder\n\n+ Customize all user information collection forms using the block editor for drag and drop and WYSIWYG form building.\n+ Customize field labels, placeholders, descriptions and more with an easy point and click interface.\n+ Determine if fields are required or optional with a simple toggle switch.\n+ Update the form layout with the block editor. Reorder fields, add columns, and more with a simple drag and drop interface.\n+ Remove unwanted fields with the click of a button.\n\n##### User Location Information Form Fields\n\n+ During user account creation and updates the user location fields are now locale aware ensuring that the proper terminology is used and only locale-required fields are displayed for the selected locale.\n+ The \"Country\" field has been updated to be automatically populated with a list of countries. View the full list in the file at `languages/countries.php` and the filter `lifterlms_countries` can be used to modify the default list at runtime.\n+ The \"State\" field on user forms has been updated to be automatically populated with a list of states (provinces or regions) for the selected country. This list of states can be found in the file at `languages/states.php` and the filter `lifterlms_states` can be used to modify the default list at runtime.\n+ Both \"Country\" and \"State\" fields are now searchable dropdowns elements.\n+ The lists of countries and states will be automatically updated during future releases based on information provided by [GeoNames](https://www.geonames.org/) APIs.\n\n##### Mergecodes everywhere via new `[llms-user]` shortcode\n\n+ Allows merging most user information field data into any post or page, email, or notification (as well as widgets and more).\n\n##### Updates\n\n+ Email and password confirmation fields may now be made optional.\n+ \"User Information Options\" have been largely removed in favor of determining which fields are displayed via the forms UI\n+ The former \"User Information Options\" settings area has been renamed to \"User Privacy Options\".\n+ Removed email lookup logic since `wp_authenticate()` supports email addresses as `user_login` since WP 4.5.\n+ Custom user fields added via filters are now displayed on the admin panel at priority 11 instead of 10.\n+ Added shortcode processing in LifterLMS-generated emails.\n+ If a symbol cannot be found for the supplied currency code, return the code instead of an empty string.\n\n##### Bug Fixes\n\n+ Changed the filter on return of `LLMS_Person_Handler::get_password_reset_fields()` from `lifterlms_lost_password_fields` to `llms_password_reset_fields`.\n+ Fixed duplicate references to the `llms-select2` script.\n\n##### Development changes\n\n+ Added before and after actions hooks for admin tools.\n+ The filter `lifterlms_before_user_${action}` is now triggered by `do_action_ref_array()` instead of `do_action()` allowing modification of `$posted_data` and `$fields` via hooks.\n+ A number of action and filter hooks have been moved to new locations within the codebase. They will continue to function as expected (with some minor exceptions).\n+ Enqueue select2 on account and checkout pages for searchable dropdowns for country & state.\n+ Stop loading removed processor \"table_to_csv\".\n\n##### Library & Vendor Updates\n\n+ Updates LifterLMS Blocks to version 2.0.1.\n+ Updates woocommerce/actions-scheduler to version 3.2.1.\n+ Load core libraries from new location and load WP Background Processing lib.\n+ The vendor script dependency `topModal.js` has been removed.\n\n##### Templates Updated\n\n+ templates/checkout/form-checkout.php\n+ templates/checkout/form-confirm-payment.php\n+ templates/checkout/form-gateways.php\n+ templates/global/form-login.php\n+ templates/global/form-registration.php\n+ templates/myaccount/form-edit-account.php\n+ templates/product/free-enroll-form.php\n\n##### Deprecations\n\nThe following have been deprecated and will be removed from LifterLMS in a major update following version 5.0.0.\n\n+ Class Method: `LLMS_Person_Handler::get_available_fields()` is deprecated in favor of `LLMS_Forms::get_form_fields()`.\n+ Class Method: `LLMS_Person_Handler::register()` is deprecated, in favor of `llms_register_user()`.\n+ Class Method: `LLMS_Person_Handler::sanitize_field()` (private method) is deprecated with no replacement.\n+ Class Method: `LLMS_Person_Handler::update()` is deprecated, in favor of `llms_update_user()`.\n+ Class Method: `LLMS_Person_Handler::validate_fields()` is deprecated with no replacement.\n+ Class Method: `LLMS_Person_Handler::voucher_toggle_script()` is deprecated with no replacement.\n+ Filter: `llms_usernames_blacklist` is deprecated, use `llms_usernames_blocklist` instead.\n+ Filter: `lifterlms_get_user_custom_fields` is deprecated with no replacement.\n+ Function: `llms_get_minimum_password_strength()` is deprecated with no replacement.\n+ Option: `lifterlms_registration_generate_username` is deprecated in favor of the new method `LLMS_Forms::are_usernames_enabled()`.\n\n##### Removed Items\n\n+ Private method `LLMS_Processors::includes()` has been removed.\n+ Private methods `LLMS_Person_Handler::fill_fields()` and `LLMS_Person_Handler::insert_data()` were removed.\n+ Previously deprecated class method `LLMS_Quiz::get_lessons()` has been removed.\n+ Previously deprecated class method `LLMS_Controller_Quizzes::take_quiz()` has been removed.\n+ Previously deprecated class `LLMS_Processor_Table_To_Csv` has been removed.\n\n\nv5.0.0-rc.2 - 2021-06-18\n------------------------\n\n+ Remove password description merge codes from reusable block schema.\n+ Explicitly define required field attributes on reusable block schema.\n+ Requires WP 5.7 or later to edit forms & show an upgrade nudge when requirements are not met.\n+ Add a link from the (now) legacy account settings area to help experienced users find the new form building area\n+ Add a (subtle) custom fields add-on upgrade nudge when viewing the forms list on the admin panel\n+ Update LifterLMS Blocks to 2.0.0-rc.2\n\n\nv5.0.0-rc.1 - 2021-06-15\n------------------------\n\n+ Updates Action Scheduler library to version 3.2.0\n+ Remove the {min_strength} and {min_length} merge codes from the User Password block description.\n+ Don't load removed files during OptimizePress compatibility.\n+ Add a 5.0.0 DB upgrade routine and welcome notice\n+ Add the LifterLMS Helper as an included library\n+ Add WordPress 5.8 compatibility on the Widgets and Customizer screens.\n+ Move form location definitions into a schema file\n+ Require WordPress 5.7+ to manage forms via the block editor\n+ Upgrades LifterLMS Blocks to 2.0.0-rc.1\n\n\nv5.0.0-beta.2 - 2021-06-01\n---------------------------\n\n+ Updates LifterLMS Blocks to 2.0.0-beta.6.\n+ (Re-)introduces the user information shortcode as `[llms-user]`.\n+ Add Admins status tool to reinstall core forms & reusable blocks.\n+ Fixed issue causing data from conditionally disabled fields (like state) from being cleared during form submission\n+ Updated form post type labels and added missing labels\n+ Removed the previously deprecated class `LLMS_Frontend_Forms` and it's deprecated class methods `reset_password()` and `voucher_check()`.\n+ Removed the previously deprecated class `LLMS_Frontend_Password` and it's deprecated class methods: `retrieve_password()`, `check_password()`, and `reset_password()`.\n+ Updated country and state localization lists.\n\n\nv5.0.0-beta.1 - 2021-05-19\n---------------------------\n\n+ LifterLMS Blocks 2.0.0-beta.5\n+ Added site-wide field name validation\n+ Reworked the output of user information fields on the admin panel to share a handler and APIs with frontend fields.\n+ Deprecated filter: `lifterlms_get_user_custom_fields` in favor of `llms_admin_profile_fields`\n+ Improved previewing of form posts using WP Core block editor UI elements\n+ Open Registration form can now always be previewed regardless of the open registration site setting\n\n\nv5.0.0-alpha.6 - 2021-05-07\n---------------------------\n\n+ LifterLMS Blocks 2.0.0-beta.4\n+ Fix default reusable password field type from plain text to password\n+ Change the default reusable block post titles to reduce confusion when searching for blocks in the editor\n\n\nv5.0.0-alpha.5 - 2021-05-03\n---------------------------\n\n+ Reorganized new files into subdirectories.\n+ Added serverside password minimum length validation.\n+ Fix duplicate password strength meter output.\n+ Fix the user password field type from text to password\n+ Fix the phone number field type from text to tel\n+ Fix user state select field\n+ Don't autoload field values from specified datastore when a \"value\" is explicitly passed to the field.\n+ Only load published reusable blocks on the frontend of the website\n+ Improved the UX for editing a users account by automatically \"hiding\" password and email fields and only requiring them to be submitted when users explicit request an update via the field's \"change\" toggle button.\n\n\nv5.0.0-alpha.4 - 2021-04-26\n---------------------------\n\n+ Default form templates now use reusable blocks.\n+ Improved the user experience surrounding fields with a confirmation field (email address and password).\n+ Added the ability to define a field's column width instead of requiring the usage of WP column blocks.\n+ Added support for reusable blocks on form posts\n+ Upgraded LifterLMS Blocks to 2.0.0-beta.3.\n\n\nv5.0.0-alpha.3 - 2021-03-23\n---------------------------\n\n+ Fixed issue preventing users from editing their email address and password on the dashboard account edit screens.\n+ Fixed issues with country names with the article \"the\" in their name, for example \"The Netherlands\" instead of \"Netherlands The\".\n+ Upgraded LifterLMS Blocks to version 2.0.0-beta.2.\n\n\nv5.0.0-alpha.2 - 2021-03-22\n---------------------------\n\n##### Updates\n\n+ Updates LifterLMS Blocks to version 2.0.0-beta.1\n+ Adds functionality to force usage of the Block Editor for editing LifterLMS forms\n+ Updates localization functionality and methods to have more accurate information.\n+ Added a function for determining if open registration is enabled.\n+ Added a WP Admin Bar link below the \"Edit Page\" link to enable editing the form (if a form exists on the page).\n\n##### Bug Fixes\n\n+ Fixed an issue encountered when custom HTML fields exist on a form (backwards compatibility for pre 5.x fields API).\n\n\nv5.0.0-alpha.1 - 2021-01-07\n---------------------------\n\n##### User Information Form Builder\n\n+ Customize all user information collection forms using the block editor for drag and drop and WYSIWYG form building.\n+ Customize field labels, placeholders, descriptions and more with an easy point and click interface.\n+ Determine if fields are required or optional with a simple toggle switch.\n+ Update the form layout with the block editor. Reorder fields, add columns, and more with a simple drag and drop interface.\n+ Remove unwanted fields with the click of a button.\n\n##### User Location Information Form Fields\n\n+ During user account creation and updates the user location fields are now locale aware ensuring that the proper terminology is used and only locale-required fields are displayed for the selected locale.\n+ The \"Country\" field has been updated to be automatically populated with a list of countries. View the full list in the file at `languages/countries.php` and the filter `lifterlms_countries` can be used to modify the default list at runtime.\n+ The \"State\" field on user forms has been updated to be automatically populated with a list of states (provinces or regions) for the selected country. This list of states can be found in the file at `languages/states.php` and the filter `lifterlms_states` can be used to modify the default list at runtime.\n+ Both \"Country\" and \"State\" fields are now searchable dropdowns elements.\n+ The lists of countries and states will be automatically updated during future releases based on information provided by [GeoNames](https://www.geonames.org/) APIs.\n\n##### Mergecodes everywhere via new `[user]` shortcode\n\n+ TODO.\n\n##### Updates\n\n+ Email and password confirmation fields may now be made optional.\n+ \"User Information Options\" have been largely removed in favor of determining which fields are displayed via the forms UI\n+ The former \"User Information Options\" settings area has been renamed to \"User Privacy Options\".\n\n##### Bug Fixes\n\n+ Changed the filter on return of `LLMS_Person_Handler::get_password_reset_fields()` from `lifterlms_lost_password_fields` to `llms_password_reset_fields`.\n\n##### Development changes\n\n+ The filter `lifterlms_before_user_${action}` is now triggered by `do_action_ref_array()` instead of `do_action()` allowing modification of `$posted_data` and `$fields` via hooks.\n+ A number of action and filter hooks have been moved to new locations within the codebase. They will continue to function as expected (with some minor exceptions).\n+ Enqueue select2 on account and checkout pages for searchable dropdowns for country & state.\n\n##### Library & Vendor Updates\n\n+ Load core libraries from new location and load WP Background Processing lib.\n+ The vendor script dependency `topModal.js` has been removed.\n\n##### Templates Updated\n\n+ templates/global/form-login.php\n+ templates/global/form-registration.php\n+ templates/product/free-enroll-form.php\n\n##### Deprecations\n\nThe following have been deprecated and will be removed from LifterLMS in a major update following version 5.0.0.\n\n+ Class Method: `LLMS_Person_Handler::get_available_fields()` is deprecated in favor of `LLMS_Forms::get_form_fields()`.\n+ Class Method: `LLMS_Person_Handler::register()` is deprecated, in favor of `llms_register_user()`.\n+ Class Method: `LLMS_Person_Handler::sanitize_field()` (private method) is deprecated with no replacement.\n+ Class Method: `LLMS_Person_Handler::update()` is deprecated, in favor of `llms_update_user()`.\n+ Class Method: `LLMS_Person_Handler::validate_fields()` is deprecated with no replacement.\n+ Class Method: `LLMS_Person_Handler::voucher_toggle_script()` is deprecated with no replacement.\n+ Filter: `llms_usernames_blacklist` is deprecated, use `llms_usernames_blocklist` instead.\n+ Function: `llms_get_minimum_password_strength()` is deprecated with no replacement.\n+ Option: `lifterlms_registration_generate_username` is deprecated in favor of the new method `LLMS_Forms::are_usernames_enabled()`.\n\n##### Removed Items\n\n+ Private method `LLMS_Processors::includes()` has been removed.\n+ Private methods `LLMS_Person_Handler::fill_fields()` and `LLMS_Person_Handler::insert_data()` were removed.\n+ Previously deprecated class method `LLMS_Quiz::get_lessons()` has been removed.\n+ Previously deprecated class method `LLMS_Controller_Quizzes::take_quiz()` has been removed.\n+ Previously deprecated class `LLMS_Processor_Table_To_Csv` has been removed.\n\n\nv4.21.3 - 2021-05-31\n--------------------\n\n##### Updates\n\n+ Increase 3rd party support for WP core hook `lostpassword_post` hook.\n\n##### Bug fixes\n\n+ Props to [Hemant Patidar](https://www.linkedin.com/in/hemantsolo/) for discovering an issue preventing rate limiting in various security plugins from working on the LifterLMS password recovery form.\n+ Fixed an issue encountered when updating LifterLMS premium add-ons via the LifterLMS Helper encountered when API errors are occur.\n+ Updated the failure error code from 'activation' to 'deactivation' in the `LLMS_Add_On` class.\n+ Updated the API connection error message returned when using the `LLMS_Abstract_API_Handler` class.\n\n##### Deprecations\n\n+ Class `LLMS_Frontend_Password` is deprecated, see deprecated methods and their replacements below:\n\n  + `LLMS_Frontend_Password::retrieve_password()` is deprecated in favor of `LLMS_Controller_Account::lost_password()`.\n  + `LLMS_Frontend_Password::check_password_reset_key()` is deprecated in favor of `check_password_reset_key()`.\n  + `LLMS_Frontend_Password::reset_password()` is deprecated in favor of `reset_password()`.\n\n\nv4.21.2 - 2021-05-17\n--------------------\n\n##### Security Update\n\nThis releases fixes a security issue affecting LifterLMS versions 4.21.1 and earlier:\n\n+ Thank you to [Amirmohammad vakili](https://www.linkedin.com/in/amirmuhammad-vakili-65a7a11b3/) for reporting an insecure direct object reference issue.\n\n##### Updates\n\n+ Added the `view_grades` capability which is used to determine whether or not a user has the ability to view another user's grades on the website's frontend.\n\n##### Bug fixes\n\n+ Fixed an issue causing PHP errors when attempting to access a quiz attempt that doesn't exist.\n+ Fixed a localization issue encountered when entering transaction amounts on the admin panel.\n\n\nv4.21.1 - 2021-04-29\n--------------------\n\n##### Security Update\n\nThis releases fixes two security issues affecting LifterLMS versions 4.21.0 and earlier:\n\n+ Thank you to [Amirmohammad vakili](https://www.linkedin.com/in/amirmuhammad-vakili-65a7a11b3/) for reporting a way to store XSS.\n+ Thank you to Ashish Jha from [Bluefire Redteam](https://www.bluefire-redteam.com/) for reporting a reflected XSS issue on checkout screens.\n\n\nv4.21.0 - 2021-04-19\n--------------------\n\n##### Updates\n\n+ Certificate exports will now automatically include (most) externally hosted images and stylesheets.\n+ Opt-in forward compatibility changes have been made to the `LLMS_Abstract_Options_Data` class.\n\n##### Bugfixes\n\n+ Fixed an issue causing one-time payment orders from being included in totals on some reporting screens.\n+ Fixed an issue causing student enrollment counts to be incorrect under some circumstances.\n+ Fixed issues resulting in unnecessary duplicated instances of course background data processing.\n+ Fixed an error encountered when a course is deleted prior to its background data being processed.\n+ Fixed an escaping issue causing passwords with a backslash character from being usable following a password reset.\n\n\nv4.20.0 - 2021-03-16\n--------------------\n\n##### Bugfixes\n\n+ Fixed an issue causing a fatal error when attempting to access reports for deleted students. Thanks Thanks [@pondermatic](https://github.com/pondermatic)!\n+ Fixed an issue encountered on the builder causing the last section to be returned when retrieving the previous section for the first section.\n\n\nv4.19.0 - 2021-03-11\n--------------------\n\n##### Supported Version Requirement Updates\n\n+ **The minimum supported PHP version has been raised to PHP 7.3. Please upgrade to a [supported PHP version](https://www.php.net/supported-versions).**\n+ **The minimum supported WordPress core version has been raised to version 5.3.**\n\n##### Bug fixes\n\n+ Fixed an issue causing TinyMCE editor instances to be unusable within metaboxes when using the block editor.\n\n\nv4.18.0 - 2021-03-04\n--------------------\n\n**This is the last release of LifterLMS that will declare support for PHP 7.2. PHP 7.2 reached its official [end of life](https://www.php.net/eol.php) on November 30, 2020. With the next release of LifterLMS the minimum supported PHP version will be raised to 7.3. If you're currently using PHP 7.2 please contact your host and request an upgrade to a [supported PHP version](https://www.php.net/supported-versions) as soon as possible!**\n\n##### Updates\n\n+ Tested up to WordPress core version 5.7\n+ Updated several occurrences of `json_encode()` with preferred `wp_json_encode()`.\n\n##### Bug fixes\n\n+ Added a tie-breaker when there are multiple enrollment statuses with the same date & time. Thanks [@pondermatic](https://github.com/pondermatic)!\n+ On admin order pages and tables don't print links for deleted students.\n+ Fixed an issue on admin order pages when viewing an order for a deleted student.\n\n\nv4.17.0 - 2021-02-22\n--------------------\n\n##### Updates\n\n+ The post type feature \"llms-sales-page\" has been added to course and membership post types, signifying they support custom sales pages.\n\n##### Bug fixes\n\n+ Fixed compatibility issues with Yoast SEO 15.8.\n+ Fixed duplicate action hook in `content-no-access-after.php` template.\n+ Added early returns to several templates to prevent undefined variables errors.\n+ Fixed an undefined variable encountered in course builder JS debug logging.\n\n##### Templates Updated\n\n+ content-no-access-after.php\n+ quiz/meta-information.php\n+ quiz/results.php\n+ quiz/start-button.php\n\n\nv4.16.0 - 2021-02-18\n--------------------\n\n##### Updates\n\n+ Added preview management to the student dashboard to allow previewing of the dashboard as a site visitor.\n+ Added a new filter to allow customization of courses output by the [lifterlms_courses] shortcode. Thanks [@reedhewitt](https://github.com/reedhewitt)!\n+ Added compatibility code to reduce plugin conflicts encountered in the course builder. Resolves a conflict encountered when building quizzes with Yoast SEO installed.\n\n##### Bug fixes\n\n+ Fixed undefined variable error encountered when creating custom notification types. Thanks [@pondermatic](https://github.com/pondermatic)!\n+ Fixed incorrect variables passed to `sprintf()` in logging functions used by the course data background processor. Thanks [@pondermatic](https://github.com/pondermatic)!\n\n\nv4.15.0 - 2021-02-09\n--------------------\n\n##### Updates\n\n+ Database migration: remove any \"orphaned\" access plans which were not properly cleaned up during deletion of parent course or membership.\n+ Improved performance of membership post association query methods.\n\n##### Bug fixes\n\n+ Access plans will now be automatically deleted when their parent course or membership is deleted.\n+ Fix an issue with donut charts/graphs on RTL sites.\n+ Fix an issue causing unpublished (draft/private) courses from being returned during queries for membership post associations.\n\n##### LifterLMS REST 1.0.0-beta.15\n\n###### Updates\n\n+ Added Access Plan resource and endpoint.\n+ Provide a more significant error message when trying to delete an item without permissions.\n+ Use `WP_Http` constants in favor of integers when referencing HTTP status codes.\n\n###### Bug fixes\n\n+ Fixes localization issues where a singular name was used in favor of the expected plural form.\n+ Fixed issues where an error object was not properly returned when expected\n+ Fixed call to undefined function `llms_bad_request_error()`, must be `llms_rest_bad_request_error()`.\n+ Fixed access plans resource link.\n+ Fixed wrong trigger retrieved when multiple trigger were present for the same user/post pair on Student Enrollment resources.\n\n\nv4.14.0 - 2021-02-04\n--------------------\n\n##### Updates\n\n+ Added a user preference option allowing users to opt-out of the course builder's autosave functionality. [More information](https://lifterlms.com/docs/using-course-builder/#manual-saving).\n+ 5-star review request displayed at 30 enrollments instead of 50.\n\n##### Bug fixes\n\n+ Fixed an issue encountered when using shortcodes in the description of an access plan.\n+ Fixed an issue encountered when editing auto-draft courses on the course builder.\n\n##### Deprecations\n\n+ `LLMS_Controller_Quizzes::take_quiz()` is deprecated in favor of `LLMS_AJAX_Handler::quiz_start()`.\n+ Method `LLMS_Quiz::get_lessons()` is deprecated with no replacement.\n\n\nv4.13.0 - 2021-01-26\n--------------------\n\n##### Updates\n\n+ **The minimum supported WordPress core version has been raised to 5.2.** For more information, please review the [LifterLMS Minimum System Requirements](https://lifterlms.com/docs/minimum-system-requirements-lifterlms/).\n+ When cloning courses and lessons the cloned post will be created as a draft.\n+ When cloning courses the suffix \"(Clone)\" will be appended to the title of the course to unify cloning behavior with lessons.\n+ Added information about LifterLMS specific constant values to the LifterLMS system report.\n+ Added a new constant `LLMS_IS_SITE_CLONE` which can be used to force the site's clone status.\n\n##### Bug fixes\n\n+ Reverts site clone detection check changes implemented in 4.12.0 to restore pre 4.12.0 functionality which only runs checks on the admin panel for logged in users with the `manage_lifterlms` capability.\n+ Restore reliance on `mb_convert_encoding()` when passing html strings into `DOMDocument` and use the alternate method introduced in version 4.8.0 as a fallback.\n+ Fixed an issue encountered when unexpected or malformed data is stored in the LifterLMS admin notices option.\n\n\nv4.12.0 - 2021-01-20\n--------------------\n\n##### Updates\n\n+ Automatic site clone detection checks have been adjusted to always run in favor of only running on the admin panel.\n+ LifterLMS Site Features (like recurring payment status) can now be configured via constant values.\n+ Added `llms_load_admin_tools` action to allow 3rd parties to easily hook into our admin tools system.\n+ Made numerous performance improvements on the course data background processor.\n+ Course data background processing will now be automatically throttled for courses with 500 students or more as opposed to the old value of 2,500 or more.\n\n##### Bug fixes\n\n+ Fixed an incorrect HTML `for` attribute and added an `id` to the related input element on the student dashboard voucher redemption endpoint.\n+ Fixed a pagination error encountered when using course or membership list shortcodes on the static front page.\n+ Make sure `is_lifterlms()` exists before calling it in navigation menu-related classes.\n\n##### Deprecations\n\n+ `LLMS_Admin_Notices_Core::check_staging()` is deprecated in favor of `LLMS_Staging::notice()`.\n+ Unused property `LLMS_Course::$sections` is replaced by `LLMS_Course::get_sections()`.\n+ Unused property `LLMS_Course::$sku` is deprecated with no replacement.\n+ `LLMS_Frontend_Forms` is deprecated, functionality is available via `LLMS_Controller_Account`.\n+ `LLMS_Frontend_Forms::reset_password()` is deprecated in favor of `LLMS_Controller_Account::reset_password()`.\n\n##### Templates Updated\n\n+ templates/myaccount/form-redeem-voucher.php\n\n\nv4.11.0 - 2021-01-07\n--------------------\n\n##### Updates\n\n+ Adds the ability to use the Instructors blocks on the membership post type. Thanks [@alaa-alshamy](https://github.com/alaa-alshamy)!\n+ Updated LifterLMS Blocks to [Version 1.11.1](https://make.lifterlms.com/2020/12/29/lifterlms-blocks-version-1-11-1/).\n\n##### Bug fixes\n\n+ Fixed a PHP Notice encountered when trying to retrieve next lesson from an empty section.\n\n##### Templates updated\n\n+ templates/course/author.php\n\n\nv4.10.2 - 2021-01-04\n--------------------\n\n##### Updates\n\n+ Improved performance of `llms_get_enrolled_students()`.\n+ Refactored lesson navigation query functions.\n\n##### Bug fixes\n\n+ Fixed sorting error when sorting student reports by name.\n\n\nv4.10.1 - 2020-12-10\n--------------------\n\n##### Bug fixes\n\n+ Fixed visual issues encountered on the admin Add-Ons screen.\n+ Use `hr.wp-header-end` in favor of a second (hidden) <h1> to \"catch\" admin notices on the Add-Ons screen.\n+ Replace incorrect usage of invalid ID `llms_shop` with `courses` during catalog template loader checks.\n+ Function `llms_get_post()` will now only allow instantiation of LifterLMS classes.\n+ Remove unneeded require autoloaded file `includes/class.llms.quiz.data.php`.\n\n\nv4.10.0 - 2020-12-01\n--------------------\n\n##### Updates\n\n+ Adds native theme support for the WordPress default theme Twenty Twenty-One.\n+ Improved the `llms_archive_description()` function and related filter.\n\n##### Bug fixes\n\n+ Fix issue encountered when using multiple role plugins to add the Instructor role to an Administrator user account. Thanks [@daniel-shuy](https://github.com/daniel-shuy)!\n+ Fixed an issue encountered when using non-latin characters in a course post URL slug. Thanks [@alaa-alshamy](https://github.com/alaa-alshamy)!\n\n##### Templates Updated\n\n+ templates/loop/pagination.php\n\n\nv4.9.0 - 2020-11-24\n-------------------\n\n+ Tested up to WordPress core 5.6 (RC.1).\n+ Raised the minimum required WordPress core version to 5.1.\n+ Add new localization utilities for developers.\n+ Fixed various issues found on PHP 8.\n+ Added script localization for block editor scripts.\n+ Updated LifterLMS Rest to [Version 1.0.0-beta.17](https://make.lifterlms.com/2020/11/24/lifterlms-rest-api-version-1-0-0-beta-17/).\n+ Updated LifterLMS Blocks to [Version 1.10.0](https://make.lifterlms.com/2020/11/24/lifterlms-blocks-version-1-10-0/).\n\n\nv4.8.0 - 2020-11-16\n-------------------\n\n##### Updates\n\n+ Added additional course imports and templates at the end of the setup wizard\n+ Added a cloud importer enabling 1-click importing of courses and course templates via the importer at LifterLMS -> Import\n+ Added strict comparisons in several places.\n+ Course \"extra\" data is only added to course arrays during exports to improve performance on the course builder.\n+ Improved template override loading performance on sites with no child theme.\n\n##### Bug fixes\n\n+ Fixed issues related to reliance on methods provided by the `mb_string` PHP module.\n\n##### Deprecations\n\n+ `LLMS_Admin_Setup_Wizard::generator_course_status()` is deprecated with no replacement.\n+ `LLMS_Admin_Setup_Wizard::watch_course_generation()` is deprecated with no replacement.\n\n\nv4.7.1 - 2020-11-05\n-------------------\n\n##### Bug fixes\n\n+ During import generation set the post excerpt during the initial post insert instead of during metadata updates after creation.\n\n##### LifterLMS REST API 1.0.0-beta.16\n\n+ Improved performance of various database queries.\n\n\nv4.7.0 - 2020-11-02\n-------------------\n\n##### Updates\n\n+ Major refactor of the `LLMS_Generator` class.\n+ Course export structure improved to include images and reusable blocks found in post content.\n+ When importing courses images will be automatically sideloaded into the media library as new attachment posts\n+ When importing courses reusable blocks will be imported\n+ Improved the success message displayed following a course import\n+ The class `LLMS_Admin_Reporting` is now always loaded on the admin panel.\n+ Performance improvements have been made to the `LLMS_Events_Query` to support using the `no_found_rows` query argument.\n+ When an order's billing plan \"completes\", a new meta property will be added to the order, `plan_ended`, which can be used to query orders with completed plans.\n+ Made improvements to the admin payment rescheduler tool to have more accurate reporting information.\n\n##### Bug fixes\n\n+ Replaced an instance of the LifterLMS (old) 1.0 rocket logo with the current rocket logo. Thanks [@imknight](https://github.com/imknight)!\n+ Ensure builder `switch-number` fields are set with the `number` type attribute. Thanks [@imknight](https://github.com/imknight)!\n+ Don't display a \"View Post\" link when updating post types that aren't publicly queryable. Thanks [@imknight](https://github.com/imknight)!\n+ Fixed the incorrect output of an achievement's title in a popover notification when using the {{ACHIEVEMENT_TITLE}} merge code. Thanks [@CadenG150](https://github.com/@CadenG150)!\n+ Fixed an error encountered when plugins utilize the `WP_Users_List_Table` class outside of the `users.php` screen.\n\n##### Deprecations\n\n+ `LLMS_Admin_Import::localize_stat()` is deprecated with no replacement.\n+ `LLMS_Admin_Users_Table::load_dependencies()` is deprecated with no replacement. The included class, `LLMS_Admin_Reporting` is now always loaded.\n+ `LLMS_Generator::add_custom_values()` is deprecated in favor of `LLMS_Generator_Courses::add_custom_values`.\n+ `LLMS_Generator::get_author_id_from_raw()` is deprecated in favor of `LLMS_Generator_Courses::get_author_id_from_raw()`.\n+ `LLMS_Generator::get_default_post_status()` is deprecated in favor of `LLMS_Generator_Courses::get_default_post_status()`.\n+ `LLMS_Generator::get_generated_posts()` is deprecated in favor of `LLMS_Generator::get_generated_content()`.\n+ `LLMS_Generator::format_date()` is deprecated in favor of `LLMS_Generator_Courses::format_date()`.\n+ `LLMS_Generator::increment()` is deprecated with no replacement.\n\n\nv4.6.0 - 2020-10-19\n-------------------\n\n+ Added an admin tool to help automatically identify and schedule missed recurring payments\n+ Use `llms_deprecated_function()` in favor of `llms_log()`.\n+ Removed logging and use `apply_filters_deprecated()` in favor of `apply_filters()`.\n\n\nv4.5.1 - 2020-10-14\n-------------------\n\n##### Updates\n\n+ Added logic in `LLMS_Database_Query` to reduce unnecessary DB reads when total results are not required.\n\n##### Bug fixes\n\n+ Removed the course \"Excerpt\" area in favor of utilization of the course sales page content.\n+ Show sales reporting currency symbol based on LifterLMS site options in favor of the browser's locale settings.\n+ Fixed an issue causing achievement-related JS DOM events to be bound unnecessarily. Thanks to [@imknight](https://github.com/imknight)!\n+ Fixed an issue causing site administrator capabilities to be removed during LifterLMS data removal.\n+ Fixed an issue causing an instructors course post count to display 0 on the admin panel courses post table. Thanks to [nhandl3](https://github.com/nhandl3)!\n+ Only display the admin bar \"View Manager\" to users who can bypass content restrictions.\n+ Updated jQuery code to stop using deprecated events and methods in preparation for jQuery upgrades in the WordPress core.\n+ Fixed PHP notice encountered on the admin panel when using Yoast SEO.\n\n\nv4.5.0 - 2020-10-06\n-------------------\n\n##### Updates\n\n+ Students can now choose to make their certificates publicly accessible. Huge thanks to [@alaa-alshamy](https://github.com/alaa-alshamy) for contributing this awesome new feature!\n+ When accessing a certificate that does not have sharing enabled, a 404 will be served in favor of an error message.\n+ Admin payment gateway notices will no longer redisplay a week after being dismissed.\n+ Log files will be automatically split when a file is 5MB or larger, ensuring that log files never grow too large.\n+ During student registration, `wp_signon()` is used to login the newly created user.\n+ Improved slow background process database queries run during the automatic \"closing\" of idle user sessions.\n\n##### Bug fixes\n\n+ `LLMS_User_Certificate::get_related_post_id()` and `LLMS_User_Certificate::get_user_id()` will now always return an integer.\n+ Fixes issues related to account sign on/out and session start/end events being recorded incorrectly.\n\n##### Deprecations\n\n+ `llms_set_person_auth_cookie()` is deprecated in favor of WP core methods such as `wp_signon()`, `wp_set_current_user()`, and/or `wp_set_auth_cookie()`.\n\n\nv4.4.4 - 2020-09-21\n-------------------\n\n##### Bug fixes\n\n+ Don't pass unsupported parameter `$use_cache` to the `calculate_grade()` method, thanks [@pondermatic](https://github.com/pondermatic)!\n+ Add an HTML title attribute to the admin setup wizard page.\n+ Fix issue causing notices to be logged during quiz attempt deletion on the admin panel.\n\n##### Deprecations\n\n+ Method `LLMS_Admin_Setup_Wizard::scripts()` & `LLMS_Admin_Setup_Wizard::output_step_html()` are deprecated with no replacements.\n\n##### LifterLMS REST API version 1.0.0-beta.15\n\n+ Bugfix: Created lessons will now have the derivative `course_id` property set according to the ID of the lesson's parent section.\n+ Bugfix: The `course_id` property of lessons is now properly marked as read-only.\n\n\nv4.4.3 - 2020-09-16\n-------------------\n\n+ Bugfix: Fix engagement email duplicate check issue.\n+ Bugfix: Fix transposition issue found in engagement email dupcheck debug log message.\n\n\nv4.4.2 - 2020-09-08\n-------------------\n\n+ Bugfix: Fix lesson navigation regression introduced in 4.4.0.\n\n\nv4.4.1 - 2020-09-04\n-------------------\n\n+ Bugfix: Delayed engagement emails will not be sent to students who's enrollment is not active in the related course or membership which triggered the email.\n+ Bugfix: Fixed regression introduced in 4.4.0 preventing the `certificates.css` stylesheet from loading on certificate screens.\n+ Update: Engagement email related logs will be logged to a separate logfile, `engagement-emails` in favor of the main `llms` log.\n\n\nv4.4.0 - 2020-09-02\n-------------------\n\n##### Updates\n\n+ Improved LifterLMS static asset registration, queuing, definitions, and management.\n+ Added strict comparators in various areas of the codebase.\n\n##### Changes to deprecated function logs and warnings\n\n+ The `llms_deprecated_function()` method now uses `_deprecated_function()` (from the WP core) under the hood.\n+ LifterLMS deprecation warnings are logged to the WP core `debug.log` file in favor of the LifterLMS log file.\n+ LifterLMS deprecation warnings will now trigger a `E_USER_DEPRECATED` error when `WP_DEBUG` is enabled.\n\n##### Bugfixes\n\n+ Fixed a lesson navigation issue encountered when sections contain unpublished lessons.\n+ Fixed an undefined variable notice encountered on the student dashboard.\n+ Fixed an issue encountered when the `wp_login_url()` function returns an empty string.\n+ Fixed a double slash found in an asset URI.\n\n##### Deprecations\n\n+ `LLMS_Frontend_Assets::is_inline_script_enqueued()` is deprecated in favor of `LLMS_Frontend_Assets::is_inline_enqueued()`.\n+ `LLMS_Ajax::register_script()` is deprecated with no replacement.\n+ `LLMS_Ajax::get_ajax_data()` is deprecated with no replacement.\n+ Javascript AJAX nonce variable is moved from `wp_ajax_data.nonce` to `window.llms.ajax-nonce`.\n\n##### Templates Updated\n\n+ templates/checkout/form-gateways.php\n+ templates/course/lesson-preview.php\n+ templates/course/syllabus.php\n\n\nv4.3.3 - 2020-08-17\n-------------------\n\n+ Fixed an issue causing legends of reporting charts to be truncated and only readable after a mouse hover.\n+ Fixed an issue caused by passing `null` values to `wp_insert_post()`.\n+ Fixed a javascript error encountered on LifterLMS settings screens.\n\n\nv4.3.2 - 2020-08-10\n-------------------\n\n+ WP 5.5 compatibility: Automatically deregister \"protected\" post types from wp-sitemap.xml.\n\n\nv4.3.1 - 2020-08-06\n-------------------\n\n+ When resetting tracking data cookies, set a \"secure\" cookie where possible.\n+ Catch an unhandled error encountered when generating certificate exports.\n+ When an error is encountered during certificate export generation, display an error notice instead of a general notice.\n\n\nv4.3.0 - 2020-07-28\n-------------------\n\n##### Security Fix\n\n+ Fixed an XSS issue on account edit and registration forms. Thanks to [Morningstar](https://twitter.com/0xMstar) for reporting this issue!\n\n##### Bug fixes\n\n+ Fixed an error encountered during customizer live theme preview encountered when Twenty-twenty is the current theme.\n+ The `$type` property of the `LLMS_Abstract_Database_Store` is now set to a default placeholder value (`_db_record_`) in favor of an empty string.\n+ Set the `$type` property of the `LLMS_Event` class to `event`.\n+ Set the `$type` property of the `LLMS_Quiz_Attempt` class to `quiz_attempt`.\n+ Set the `$type` property of the `LLMS_User_Post_Meta` class to `user_postmeta`.\n\n##### Updates\n\n+ Added a filter `llms_form_field_args` to allow extending form fields prior to HTML rendering.\n\n##### Deprecations\n\nThe following filter hooks have been deprecated. These hooks were being called as the result of a bug (noted above) and should no longer be used. They will be removed in the next *major* version of LifterLMS.\n\n+ `llms__created` has been deprecated, use `llms_{$type}_created` where `{$type}` is the database record type defined by the class property.\n+ `llms__deleted` has been deprecated, use `llms_{$type}_deleted` where `{$type}` is the database record type defined by the class property.\n+ `llms__updated` has been deprecated, use `llms_{$type}_updated` where `{$type}` is the database record type defined by the class property.\n\n\nv4.2.0 - 2020-07-21\n-------------------\n\n##### Updates\n\n+ Admins can now preview the checkout screen as visitors or students using the \"View As\" function from the WP Admin bar\n+ Javascript cookies now set cookies with `sameSite` set to `strict` as recommended by Firefox/Mozilla.\n+ Added filters to allow 3rd parties to use LifterLMS completion tracking APIs to \"complete\" external or non-LMS content.\n+ Added \"deep\" orphan checks when checking the relationship between a quiz and a lesson.\n+ Normalized the return structure in `LLMS_Post_Instructors::get_instructors()` when no instructor set, thanks [@nicolas-jaussaud](https://github.com/nicolas-jaussaud)!\n+ Update LifterLMS rocket icon used in the WP Admin Bar in the \"View As\" area.\n\n##### Bug fixes\n\n+ When deleting a quiz attempt the related lesson will now be automatically marked as \"Incomplete\" when appropriate.\n+ `LLMS_Abstract_User_Data::get_id()` now always returns an integer.\n+ Fixed a 404 error resulting from settings tooltips referencing a missing icon asset.\n+ Added logic to set the order status to 'cancelled' when an enrollment linked to an order is deleted.\n\n\n\nv4.1.0 - 2020-07-06\n-------------------\n\n##### LifterLMS REST 1.0.0-beta.14\n\n+ **Breaking**: `LLMS_REST_Controller::prepare_links()` now requires a second parameter, the `WP_REST_Request` for the current request. Any classes extending and overwriting this method must adjust their method signature to accommodate this change.\n+ Bugfix: Fixed issue causing response objects to unintentionally include keys of remapped fields. This error occurs only when extending core controllers and attempting to exclude core fields.\n\n\nv4.0.0 - 2020-06-25\n-------------------\n\nThis is a *major* release. Many backwards incompatible changes have been made that may affect your site if you have custom code which rely on previously deprecated functions or methods. If you're not sure about your custom code, test the upgrade in a [staging site](https://lifterlms.com/docs/staging/).\n\n##### Bug Fixes\n\n+ Fixed an issue encountered during quiz grading.\n+ Add RTL language support for popover interfaces found throughout the course builder.\n+ Fixed issue encountered in MySQL 8.0 when using the bbPress integration.\n\n##### LifterLMS REST API 1.0.0-beta.13\n\n+ Bugfix: Fixed error response messages on the instructors endpoint.\n+ Bugfix: Fixed student progress deletion endpoint issues preventing progress from being fully removed.\n\n##### Action Scheduler Library\n\nSwitches from prospress/action-scheduler to woocommerce/action-scheduler. The repository has been moved but it's the same library & upgrades to latest version (3.1.6).\n\nWhile this is a semantically major upgrade of the library there are no backwards incompatible changes to the public API.\n\nThere have been several deprecated functions/classes. The LifterLMS core does not directly use any of these deprecated functions but 3rd parties might and should review the changelog of the library to see if they are affected by any deprecations: https://github.com/woocommerce/action-scheduler/releases.\n\n##### Deprecations\n\n+ Function `LLMS()` is deprecated in favor of `llms()`.\n\n##### Templates Modified\n\n+ templates/global/form-login.php\n+ templates/global/form-registration.php\n\n##### Miscellaneous Breaking Changes\n\n**WP Session Manager Library**\n\nRemoves the bundled WP Session Manager plugin dependency, all public methods included with this plugin have been removed without direct replacements.\n\n**Removed JS dependencies**\n\nRemoves bundled JS bootstrap 3 dependencies: \"collapse\" and \"transition\"\n\n**Removed CSS Classes**\n\nRemoves classnames from student dashboard login and registration form wrapper elements which conflict with bootstrap causing visual issues.\n\nThese classes are not used by the LifterLMS core or add-ons and are a legacy class that hasn't been removed for fear of creating backwards compatibility issues with any custom css, 3rd party themes, etc...\n\n+ templates/global/form-login.php: Removes `col-1` class from the `div.llms-person-login-form-wrapper` element.\n+ templates/global/form-registration.php: : Removes `col-2` class from the `div.llms-new-person-form-wrapper` element.\n\n**Removed SVG assets and functionality**\n\n+ LifterLMS no longer utilizes SVGs powered by the `LLMS_Svg` class. The class has been deprecated and removed (see below).\n+ The `assets/svg` directory (and all SVG assets contained within) has been removed.\n+ The constant `LLMS_SVG_DIR` has been removed.\n\n##### Previously deprecated classes (and files) that have been removed\n\n+ `LLMS_Admin_Analytics`: `includes/admin/class.llms.admin.analytics.php`\n+ `LLMS_Analytics`: `includes/class.llms.analytics.php`\n+ `LLMS_Analytics_Courses`: `includes/admin/analytics/class.llms.analytics.courses.php`\n+ `LLMS_Analytics_Memberships`: `includes/admin/analytics/class.llms.analytics.memberships.php`\n+ `LLMS_Analytics_Page`: `includes/admin/analytics/class.llms.analytics.page.php`\n+ `LLMS_Analytics_Sales`: `includes/admin/analytics/class.llms.analytics.sales.php`\n+ `LLMS_Course_Basic`: `includes/class.llms.course.basic.php`\n+ `LLMS_Course_Handler`: `includes/class.llms.course.handler.php`\n+ `LLMS_Course_Factory`: `includes/class.llms.course.factory.php`\n+ `LLMS_Lesson_Basic`: `includes/class.llms.lesson.basic.php`\n+ `LLMS_Meta_Box_Expiration`: `includes/admin/post-types/meta-boxes/class.llms.meta.box.expiration.php`\n+ `LLMS_Meta_Box_Video`: `includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php`\n+ `LLMS_Number`: `includes/class.llms.number.php`\n+ `LLMS_Person`: `includes/class.llms.person.php`\n+ `LLMS_Quiz_Legacy`: `includes/class.llms.quiz.legacy.php`\n+ `LLMS_Svg`: `includes/class.llms.svg.php`\n+ `LLMS_Table_Questions`: `includes/admin/reporting/tables/llms.table.questions.php`\n+ `LLMS\\Users\\User`: `includes/Users/User.php`\n\n##### Previously deprecated class properties that have been removed\n\n+ `LifterLMS->person` (generally accessed via `LLMS()->person`).\n+ `LLMS_Analytics_Widget->date_end`\n+ `LLMS_Analytics_Widget->date_start`\n+ `LLMS_Analytics_Widget->output`\n+ `LLMS_Certificate->enabled`\n+ `LLMS_Course_Data->$course`\n+ `LLMS_Course_Data->$course_id`\n\n##### Previously deprecated class methods that have been removed:\n\n+ `LLMS_Admin_Table::queue_export()`\n+ `LLMS_AJAX::get_achievements()`\n+ `LLMS_AJAX::get_all_posts()`\n+ `LLMS_AJAX::get_associated_lessons()`\n+ `LLMS_AJAX::get_certificates()`\n+ `LLMS_AJAX::get_courses()`\n+ `LLMS_AJAX::get_course_tracks()`\n+ `LLMS_AJAX::get_emails()`\n+ `LLMS_AJAX::get_enrolled_students()`\n+ `LLMS_AJAX::get_enrolled_students_ids()`\n+ `LLMS_AJAX::get_lesson()`\n+ `LLMS_AJAX::get_lessons()`\n+ `LLMS_AJAX::get_lessons_alt()`\n+ `LLMS_AJAX::get_memberships()`\n+ `LLMS_AJAX::get_question()`\n+ `LLMS_AJAX::get_sections()`\n+ `LLMS_AJAX::get_sections_alt()`\n+ `LLMS_AJAX::get_students()`\n+ `LLMS_AJAX::update_syllabus()`\n+ `LLMS_Course::get_children_sections()`\n+ `LLMS_Course::get_children_lessons()`\n+ `LLMS_Course::get_author()`\n+ `LLMS_Course::get_author_id()`\n+ `LLMS_Course::get_author_name()`\n+ `LLMS_Course::get_sku()`\n+ `LLMS_Course::get_id()`\n+ `LLMS_Course::get_title()`\n+ `LLMS_Course::get_permalink()`\n+ `LLMS_Course::get_user_postmeta_data()`\n+ `LLMS_Course::get_user_postmetas_by_key()`\n+ `LLMS_Course::get_checkout_url()`\n+ `LLMS_Course::get_start_date()`\n+ `LLMS_Course::get_end_date()`\n+ `LLMS_Course::get_next_uncompleted_lesson()`\n+ `LLMS_Course::get_lesson_ids()`\n+ `LLMS_Course::get_syllabus_sections()`\n+ `LLMS_Course::get_short_description()`\n+ `LLMS_Course::get_syllabus()`\n+ `LLMS_Course::get_user_enroll_date()`\n+ `LLMS_Course::get_user_post_data()`\n+ `LLMS_Course::check_enrollment()`\n+ `LLMS_Course::is_user_enrolled()`\n+ `LLMS_Course::get_student_progress()`\n+ `LLMS_Course::get_membership_link()`\n+ `LLMS_Lesson::get_assigned_quiz()`\n+ `LLMS_Lesson::get_drip_days()`\n+ `LLMS_Lesson::mark_complete()`\n+ `LLMS_PlayNice::divi_fb_wc_product_tabs_after()`\n+ `LLMS_PlayNice::divi_fb_wc_product_tabs_before()`\n+ `LLMS_PlayNice::wc_is_account_page()`\n+ `LLMS_Post_Instructors::get_defaults()`\n+ `LLMS_Query::set_dashboard_pagination()`\n+ `LLMS_Query::add_query_vars()`\n+ `LLMS_Question::get_correct_option()`\n+ `LLMS_Question::get_correct_option_key()`\n+ `LLMS_Question::get_options()`\n+ `LLMS_Quiz::get_assoc_lesson()`\n+ `LLMS_Quiz::get_passing_percent()`\n+ `LLMS_Quiz::get_remaining_attempts_by_user()`\n+ `LLMS_Quiz::get_time_limit()`\n+ `LLMS_Quiz::get_total_allowed_attempts()`\n+ `LLMS_Quiz::get_total_attempts_by_user()`\n+ `LLMS_Quiz_Attempt::get_status()`\n+ `LLMS_Shortcode_My_Account::lost_password()`\n+ `LLMS_Section::count_children_lessons()`\n+ `LLMS_Section::delete()`\n+ `LLMS_Section::get_children_lessons()`\n+ `LLMS_Section::remove_all_child_lessons()`\n+ `LLMS_Section::remove_child_lesson()`\n+ `LLMS_Section::set_order()`\n+ `LLMS_Section::set_title()`\n+ `LLMS_Section::update()`\n+ `LLMS_Session::init()`\n+ `LLMS_Session::maybe_start_session()`\n+ `LLMS_Session::set_expiration_variant_time()`\n+ `LLMS_Session::set_expiration_time()`\n+ `LLMS_Session::use_php_sessions()`\n+ `LLMS_Student::delete_quiz_attempt()`\n+ `LLMS_Student::get_best_quiz_attempt()`\n+ `LLMS_Student::get_quiz_data()`\n+ `LLMS_Student::has_access()`\n+ `LLMS_Student_Dashboard::output_courses_content()`\n+ `LLMS_Student_Dashboard::output_dashboard_content()`\n+ `LLMS_Student_Dashboard::output_notifications_content()`\n+ `LLMS_Widget_Course_Progress::widget_contents()`\n\n##### Previously deprecated functions that have been removed\n\n+ `is_filtered()`\n+ `lifterlms_template_loop_view_link()`\n+ `llms_add_user_table_columns()`\n+ `llms_add_user_table_rows()`\n+ `llms_create_new_person()`\n+ `llms_get_question()`\n+ `llms_get_quiz()`\n+ `llms_set_user_password_rest_key()`\n+ `llms_setup_product_data()`\n+ `llms_setup_question_data()`\n+ `llms_verify_password_reset_key()`\n\n##### Previously deprecated hooks that have been removed\n\n+ Action: `lifterlms_before_memberships_loop_item_title`\n+ Action: `lifterlms_after_memberships_loop_item_title`\n+ Action: `lifterlms_after_memberships_loop_item_title`\n+ Filter: `lifterlms_completed_transaction_message`\n+ Filter: `lifterlms_is_filtered`\n+ Filter: `lifterlms_get_analytics_pages`\n+ Filter: `lifterlms_analytics_tabs_array`\n\n##### Previously deprecated shortcodes that have been removed\n\n+ `[courses]`\n+ `[lifterlms_user_statistics]`\n\n##### Previously deprecated templates that have been removed\n\n+ `templates/loop/view-link.php`\n\n##### Previously deprecated global variables that have been removed\n\n+ `$product`\n+ `$question`\n\n\nv3.41.1 - 2020-06-23\n--------------------\n\n+ Apply restrictions to post content and excerpts during WP REST requests.\n\n\nv4.0.0-rc.1 - 2020-06-18\n------------------------\n\nView release notes at [https://make.lifterlms.com/2020/06/18/lifterlms-version-4-0-0-rc-1/](https://make.lifterlms.com/2020/06/18/lifterlms-version-4-0-0-rc-1/).\n\n\nv3.41.0 - 2020-06-12\n--------------------\n\n##### Bug Fixes\n\n+ Fix issues encountered when a user role with the `edit_users` capability has multiple LifterLMS roles (like Student).\n\n##### LifterLMS 4.0.0 Release Preparation\n\nLifterLMS 4.0.0, our first major release in several years, is nearing the end of it's beta testing cycle. Many unused legacy functions, classes, and files are being removed in version 4.0.0 and well as many functions, classes, and files that were previously deprecated.\n\nThe following is a list of items that have not been previously deprecated but will be removed from LifterLMS 4.0.0.\n\nFor full details on the release, information on beta testing, and more, see our [blog post on the release](https://make.lifterlms.com/2020/06/01/preparing-for-lifterlms-4-0-0/).\n\n##### Deprecations\n\nThe WP Session Manager plugin / library that is bundled into the LifterLMS core code base is deprecated from our code base and is being fully removed in favor of an internal session manager.\n\nThe bundled Javascript Boostrap 3 modules, \"collapse\" and \"transition\" are deprecated from our codebase and are being removed.\n\nThe following CSS classes are deprecated and will be removed:\n\n+ `templates/global/form-login.php`: The `col-1` class from the `div.llms-person-login-form-wrapper` element will be removed.\n+ `templates/global/form-registration.php`: : The `col-2` class from the `div.llms-new-person-form-wrapper` element will be removed.\n\nThe following classes are deprecated:\n\n+ `LLMS_Number`: `includes/class.llms.number.php`\n+ `LLMS_Person`: `includes/class.llms.person.php`\n+ `LLMS_Table_Questions`: `includes/admin/reporting/tables/llms.table.questions.php`\n\nThe following class methods are deprecated:\n\n+ `LLMS_PlayNice::divi_fb_wc_product_tabs_after()`\n+ `LLMS_PlayNice::divi_fb_wc_product_tabs_before()`\n+ `LLMS_Question::get_correct_option()`\n+ `LLMS_Question::get_correct_option_key()`\n+ `LLMS_Quiz::get_passing_percent()`, use `LLMS_Quiz::get( 'passing_percent' )` instead.\n+ `LLMS_Quiz::get_assoc_lesson()`, use `LLMS_Quiz::get( 'lesson_id' )` instead.\n+ `LLMS_Session::init()`\n+ `LLMS_Session::maybe_start_session()`\n+ `LLMS_Session::set_expiration_variant_time()`\n+ `LLMS_Session::set_expiration_time()`\n+ `LLMS_Session::use_php_sessions()`\n\nThe following class properties are deprecated:\n\n+ `LifterLMS->person` (generally accessed via `LLMS()->person`).\n\nThe following functions are deprecated:\n\n+ `lifterlms_template_loop_view_link()`\n+ `llms_add_user_table_columns()`\n+ `llms_add_user_table_rows()`\n+ `llms_get_question()`\n+ `llms_get_quiz()`\n+ `llms_setup_product_data()`\n+ `llms_setup_question_data()`\n\nThe following global variables are deprecated:\n\n+ `$product`\n+ `$question`\n\nThe following action hooks are deprecated:\n\n+ `lifterlms_before_memberships_loop_item_title`\n+ `lifterlms_after_memberships_loop_item_title`\n+ `lifterlms_after_memberships_loop_item_title`\n\nThe following template file is deprecated:\n\n+ `templates/loop/view-link.php`\n\n\nv4.0.0-beta.3 - 2020-06-10\n--------------------------\n\nView beta release notes at [https://make.lifterlms.com/2020/06/10/lifterlms-version-4-0-0-beta-3/](https://make.lifterlms.com/2020/06/10/lifterlms-version-4-0-0-beta-3/).\n\n\nv3.40.0 - 2020-06-09\n--------------------\n\n##### Updates\n\n+ Adds a 1-click installation connector for the MailHawk email delivery plugin.\n\n##### Bugfixes\n\n+ Fixed an issue encountered during checkout when using a coupon against an access plan with a free trial.\n\n##### Deprecations\n\n+ `LLMS_SendWP::do_remote_install()` will be converted to a protected method and should no longer be called directly.\n+ `LLMS_Abstract_Email_Provider::output_css()`\n\n##### Templates updated\n\n+ templates/checkout/form-gateways.php\n\n\nv4.0.0-beta.2 - 2020-06-04\n--------------------------\n\nView beta release notes at [https://make.lifterlms.com/2020/06/04/lifterlms-version-4-0-0-beta-2/](https://make.lifterlms.com/2020/06/04/lifterlms-version-4-0-0-beta-2/).\n\n\nv4.0.0-beta.1 - 2020-06-01\n--------------------------\n\nView beta release notes at [https://make.lifterlms.com/2020/06/01/lifterlms-version-4-0-0-beta-1/](https://make.lifterlms.com/2020/06/01/lifterlms-version-4-0-0-beta-1/).\n\n\nv3.39.0 - 2020-05-28\n--------------------\n\n+ Student Welcome notifications and user registered engagements now fire when users are created via the REST POST requests to the `/students` endpoint.\n+ Bugfix: Error encountered when printing full-page certificates on certain themes.\n\n##### LifterLMS REST 1.0.0-beta.12\n\n+ Feature: Added the ability to filter student and instructor collection list requests by various user information fields.\n+ Fix: Prevent infinite loops encountered when invalid API keys are utilized.\n+ Fix: Add an action used to fire LifterLMS core engagement and notification emails\n\n\nv3.38.2 - 2020-05-19\n--------------------\n\n+ Added a default question type (\"choice\") to prevent malformed questions from being inadvertently stored in the database.\n+ When retrieving question data from the database, automatically fall back to the default question type value if no question type is saved.\n\n\nv3.38.1 - 2020-05-11\n--------------------\n\n+ Update: Added methods for retrieving a list of posts associated with a membership.\n+ Bug fix: Fixed an issue causing certificate backgrounds to be cropped or cut in certain circumstances.\n+ Bug fix: Fixed an issue generating certificate downloads on servers where `mime_content_type()` does not exist.\n+ Bug fix: Fixed an issue which caused bbPress course forum restrictions to stop working.\n\n\nv3.38.0 - 2020-04-29\n--------------------\n\n##### Updates\n\n+ The output of course restriction errors which may prevent enrollment is now displayed in it's own template in favor of the logic being included in the `product/pricing-table.php` template.\n+ The course progress bar shortcode will now only display the progress bar to enrolled users. An additional option has been added to the shortcode to allow showing a 0% progress bar to non-enrolled users. [Read more](https://lifterlms.com/docs/shortcodes/#lifterlms_course_progress).\n+ The \"Course Progress\" widget now has an option to optionally display the progress bar to non-enrolled users. By default it will display only to enrolled students.\n+ Updates LifterLMS Blocks to version 1.9.0\n\n##### Bug fixes\n\n+ Fixed an issue causing free access plans to bypass course enrollment restrictions like capacity and enrollment time periods.\n+ Fixed an issue causing custom checkout success redirects to fail when using gateways that require a payment confirmation step. This fixes an issue in the LifterLMS PayPal payment gateway.\n+ Fixed an issue causing deprecation theme-compatibility related deprecation notices to be incorrectly thrown.\n+ Fixed spelling error in variable passed to the `product/pricing-table.php` template. The misspelled variable is still being passed to the variable for backwards compatibility.\n+ Updated the way notification background processors are dispatched. This fixes an issue in the LifterLMS Twilio add-on.\n\n##### Deprecations\n\n+ `LLMS_Notifications::dispatch_processors()` is deprecated in favor of async dispatching via `LLMS_Notifications::schedule_processors_dispatch()`.\n\n##### Templates Updated\n\n+ templates/product/pricing-table.php\n\n##### LifterLMS Blocks\n\n+ Update: Improved script dependencies definitions.\n+ Update: Updated asset paths for consistency with other LifterLMS projects.\n+ Update: Updated various WP Core references that have been deprecated (maintains backwards compatibility).\n+ Update: The Lesson Progression block is no longer rendered server-side in the block editor (minor performance improvement).\n+ Update: Converted the course progress block into a dynamic block. Fixes an issue allowing the progress block to be visible to non-enrolled students.\n+ Update: Added a filter on the output of the Pricing Table block: `llms_blocks_render_pricing_table_block`.\n+ Bug fix: Fixed an issue encountered when using the WP Core \"Table\" block.\n+ Bug fix: Fixed a few areas where `class` was being used instead of `className` to define CSS classes on elements in the block editor.\n+ Bug fix: Fixed a user-experience issues encountered on the Course Information block when all possible information is disabled.\n+ Bug fix: Fixed an issue causing visibility attributes to render on blocks that don't support them.\n+ Bug fix: Fixed an issue preventing 3rd party blocks from modifying default block visibility settings.\n+ Bug fix: Fixed a spelling error visible inside the block editor.\n+ Bug fix: Fixed an issue causing the \"Course Progress\" block to be shown to non-enrolled students and visitors.\n+ Bug fix: Removed redundant CSS from frontend.\n+ Bug fix: Stop outputting editor CSS on the frontend.\n+ Bug fix: Dynamic blocks with no content to render will now only output their empty render messages inside the block editor, not on the frontend.\n+ Changes to the Classic Editor Block:\n  + The classic editor block will no longer show block visibility settings because it is impossible to use those settings to filter the block on the frontend.\n  + In order to apply visibility settings to the classic editor block, place the Classic Editor within a \"Group\" block and apply visibility settings to the Group.\n\n\nv3.37.19 - 2020-04-20\n---------------------\n\n##### Updates\n\n+ Added a new debugging tool to clear pending batches created by background processors.\n+ Added a new method `LLMS_Abstract_Notification_View::get_object()` which can be used by notification views to override the loading of the post (or object) which triggered the notification.\n\n##### Bug Fixes\n\n+ Added localization to strings on the coupon admin screen. Thanks [parfilov](https://github.com/parfilov)!\n+ Fixed issue encountered in metaboxes when the `$post` global variable is not set.\n\n\nv3.37.18 - 2020-04-14\n---------------------\n\n+ Fix regression introduced in version 3.34.0 which prevented checkout success redirection to external domains.\n+ Resolved a conflict with LifterLMS, Divi, and WooCommerce encountered when using the Divi frontend pagebuilder on courses and memberships.\n+ Fixed issue causing localization issues when creating access plans, thanks [@mcguffin](https://github.com/mcguffin)!\n\n\nv3.37.17 - 2020-04-10\n---------------------\n\n##### Updates\n\n+ Updated the lost password and password reset form handlers for improved error handling and extendability by other plugins.\n\n##### Bug Fixes\n\n+ Fixed a conflict with WooCommerce resulting in password reset issues on the WooCommerce account dashboard.\n+ Fixed an issue allowing voucher codes from deleted vouchers to still be redeemed.\n+ Fixed an issue with pagination on the courses tab of a users BuddyPress profile.\n+ Fixed a typo in the `post_status` query arg when retrieving access plans for a course or membership.\n\n##### Deprecations\n\n+ `LLMS_PlayNice::wc_is_account_page()` is no longer required and is deprecated with no replacement\n+ WP core `get_password_reset_key()` should be used in favor of `llms_set_user_password_rest_key()`.\n+ WP core `check_password_reset_key()` should be used in favor of `llms_verify_password_reset_key()`.\n\n\nv3.37.16 - 2020-03-31\n---------------------\n\n+ Bugfix: Fix issue causing student dashboard notification view to work incorrectly.\n\n\nv3.37.15 - 2020-03-27\n---------------------\n\n##### Security Notice\n\n**This releases fixes a security issue. Please upgrade immediately!**\n\nProps to [Omri Herscovici and Sagi Tzadik from Check Point Research](https://www.checkpoint.com/) who found and disclosed the vulnerability resolved in this release.\n\n##### Updates & Bug Fixes\n\n+ Excluded `page.*` events in order to keep the events table small.\n+ Fixed error encountered when errors encountered validating custom fields. Thanks to [@wenchen](https://github.com/wenchen)!\n+ Fixed issue causing course pagination issues in certain scenarios.\n\n##### LifterLMS REST API Version 1.0.0-beta.11\n\n+ Bugfix: Correctly store user `billing_postcode` meta data.\n+ Bugfix: Fixed issue preventing course.created (and other post.created) webhooks from firing.\n\n\nv3.37.14 - 2020-03-25\n---------------------\n\n+ Update: Added the ability to view the PHP error log file (as defined by `ini_get( 'error_log' )` ) on the LifterLMS -> Status -> Logs page.\n+ Update: Added strict comparisons for various condition checks.\n+ Bugfix: Fixed an issue where users might be redirected to the wrong course following a course import at the conclusion of the setup wizard.\n+ Bugfix: Fixed issue with tracking event data being lost due to cookie size limitations.\n+ Bugfix: Fixed issue potentially encountered when checking user capabilities for certificates and achievements.\n+ Bugfix: Fixed an issue preventing additional instances of the JS `LLMS.Storage` class from being instantiated.\n\n\nv3.37.13 - 2020-03-10\n---------------------\n\n+ Remove usage of internal functions marked as deprecated.\n\n\nv3.37.12 - 2020-03-10\n---------------------\n\n##### Updates\n\n+ Tested up to WordPress Core version 5.4.\n+ Added support for post revisions for course, lesson, and membership post types.\n\n##### Developer updates\n\n+ Added strict comparisons for various condition checks.\n+ Added a new filter, `llms_builder_{$post_type}_force_delete` which allows control over whether a post is moved to the trash or immediately deleted when trashed via the course builder.\n\n##### Bugfixes\n\n+ Fixed the name of the \"actions\" column on the quiz reporting screen.\n+ Fixed PHP warnings resulting from functions used to exclude order notes from comment counts.\n+ Fixed issue causing order notes to be included in the count displayed on the admin comments list despite their exclusion from the table itself.\n+ Fixed PHP notice thrown on the WordPress menu editor interface encountered when student dashboard endpoints have been deleted or removed.\n+ Fixed issue causing quotes to be encoded in various email, achievement, and certificate fields.\n\n##### Deprecations\n\nThe following have been deprecated with no replacements and will be removed in the next major update:\n\n+ `LLMS_Course_Factory::get_course()`\n+ `LLMS_Course_Factory::get_lesson()`\n+ `LLMS_Course_Factory::get_product()`\n+ `LLMS_Course_Factory::get_quiz()`\n+ `LLMS_Course_Factory::get_question()`\n+ `LLMS_Course_Handler::get_users_not_enrolled()`\n\n\nv3.37.11 - 2020-03-03\n---------------------\n\n##### Updates\n\n+ Resolved a conflict with the \"Starter Templates\" plugin which made it impossible to edit quizzes while the plugin was enabled.\n\n##### Bugfixes\n\n+ Fixed an issue causing lesson post authors to be \"lost\" when adding an existing lesson to a course.\n+ Fixed an issue causing php notices to be generated during existing lesson addition on the course builder.\n+ Fixed an issue causing course bbPress forums to be lost when editing that course using the \"Quick Edit\" function from the courses table.\n\n##### LifterLMS REST v1.0.0-beta.10\n\n+ Added text domain to i18n functions that were missing the domain.\n+ Added a \"trigger\" parameter to enrollment-related endpoints.\n+ Added `llms_rest_enrollments_item_schema`, `llms_rest_prepare_enrollment_object_response`, `llms_rest_enrollment_links` filter hooks.\n+ Fixed setting roles instead of appending them when updating user, thanks [@pondermatic](https://github.com/pondermatic)!\n+ Fixed return when the enrollment to be deleted doesn't exist, returns `204` instead of `404`.\n+ Fixed 'context' query parameter schema, thanks [@pondermatic](https://github.com/pondermatic)!\n\n\nv3.37.10 - 2020-02-19\n---------------------\n\n+ Update: Exclude the privacy policy page from the sitewide restriction.\n+ Update: Added filter `llms_enable_open_registration`.\n+ Fix: Notices are printed on pages configured as a membership restriction redirect page.\n+ Fix: Do not apply membership restrictions on the page set as membership's restriction redirect page.\n+ Fix: Added flag to print notices when landing on the redirected page.\n\n\nv3.37.9 - 2020-02-11\n--------------------\n\n+ Updated CSS classes used in privacy policy text suggestions per changes in WordPress core 5.3. Thanks [@garretthyder](https://github.com/garretthyder)!\n+ Added privacy exported group descriptions. Thanks [@garretthyder](https://github.com/garretthyder)!\n+ Added filters `llms_user_enrollment_allowed_post_types` & `llms_user_enrollment_status_allowed_post_types` which allow 3rd parties to enroll users into additional post types via core enrollment methods.\n+ Added option for admin settings fields to show an asterisk for required fields.\n+ Added option for integration plugins can now add automatically generated \"Settings\" link to the plugins screen.\n+ Bugfix: Fixed an IE compatibility issue related to usage of `Object.assign()`.\n\n\nv3.37.8 - 2020-01-21\n--------------------\n\n+ Fix: Student quiz attempts are now automatically deleted when a quiz is deleted.\n+ Fix: \"Orphaned\" quizzes (those with no parent course and/or lesson) can be deleted from the Quiz reporting table.\n+ Fix: Quiz IDs on the quiz reporting screen now link to the quiz within the course builder. If the quiz is an \"orphan\" there will be no link.\n\n\nv3.38.0-beta.2 - 2019-12-19\n---------------------------\n\n+ Update LifterLMS Blocks to v1.7.3.\n\n\nv3.38.0-beta.1 - 2019-12-13\n---------------------------\n\n##### Form Management Improvements\n\n+ Forms (registration, checkout, account) are now managed via a block editor interface.\n+ Customize field labels, description, and placeholders in a simple WYSIWYG interface.\n+ Mark fields as required with a toggle.\n+ Reorder fields with drag and drop.\n+ Customize layout using block editor columns.\n+ Use LifterLMS block-level visibility to conditionally display fields based on enrollment or logged in status.\n\n##### Form Localization\n\n+ Added default country and state/region lists (see the \"languages\" directory).\n+ Country and state forms are now searchable dropdowns that adjusted based on the currently selected country.\n+ Each country's locale information (such as what a \"post code\" is called and whether or not the country has states or post codes) will update automatically based on the selected country.\n+ Enqueue select2 on account and checkout pages for searchable dropdowns for country & state.\n\n##### Updates\n\n+ New shortcode `[user]` which is used to output user information in a merge code interface.\n+ Improved form field generation via `LLMS_Form_Field` class.\n+ LifterLMS Settings: renamed \"User Information Options\" to \"User Privacy Options\".\n+ Reorganized open registration setting.\n+ Use `LLMS.wait_for()` for dependency waiting.\n+ Moved checkout template variable declarations to the checkout shortcode controller.\n+ Removed field display settings in favor of form customization using the form editors.\n+ Organized function files. Some functions have been moved.\n+ Function `llms_get_minimum_password_strength_name()` now accepts a parameter to retrieve strength name by key.\n+ Use `LLMS.wait_for()` for dependency waiting.\n\n##### LifterLMS Blocks v1.6.0\n\n+ Feature: Added form field blocks for use on the Forms manager.\n+ Feature: Add logic for `logged_in` and `logged_out` block visibility options.\n+ Update: Added isDisabled property to Search component.\n+ Update: Adjusted priority of `render_block` filter to 20.\n+ Bug fix: Import `InspectorControls` from `wp.blockEditor` in favor of deprecated `wp.editor`\n+ Bug fix: Automatically store course/membership instructor with `post_author` data when the post is created.\n+ Bug fix: Pass style rules as camelCase.\n\n##### Removed unused Javascript assets\n\n+ Remove unused bootstrap transition and collapse scripts.\n+ Remove topModal vendor dependency.\n+ Remove password strength inline enqueues.\n\n##### Bug fixes\n\n+ Only attempt to add a nonce to the datastore when a nonce exists in the settings object.\n\n##### Deprecations\n\n+ Deprecated `LLMS_Person_Handler::register()` method, use `llms_register_user()` instead.\n+ Deprecated `llms_get_minimum_password_strength()` with no replacement.\n\n##### Template Updates\n\n+ templates/checkout/form-checkout.php\n+ templates/checkout/form-gateways.php\n+ templates/global/form-registration.php\n\nv3.37.7 - 2020-01-08\n--------------------\n\n+ Fix error resulting from undefined default value.\n+ Fix PHP 7.4 deprecation notice.\n\n\nv3.37.6 - 2019-12-12\n--------------------\n\n+ New transaction creation date is now specified using `llms_current_time()`.\n+ Use the last successful transaction time to calculate from when the previously stored next payment date is in the future.\n+ Fixed an issue causing transaction post titles to be recorded with missing data due to invalid `strftime()` placeholders.\n\n\nv3.37.5 - 2019-12-09\n--------------------\n\n+ Update LifterLMS Blocks to v1.7.2: fixes a bug causing the block editor to encounter a fatal error when accessing custom post types that don't support custom fields.\n\n\nv3.37.4 - 2019-12-06\n--------------------\n\n##### Bug Fixes\n\n+ Fixed a bug causing certificate _template_ exports to export the site's homepage instead of the certificate preview.\n+ When exporting a certificate template, use the `post_author` to determine what user to use for merge code data.\n+ Revert Accounts settings tab page id to \"account\".\n\n##### LifterLMS Blocks v1.7.1\n\n+ Feature: Add logic for `logged_in` and `logged_out` block visibility options.\n+ Update: Added `isDisabled` property to Search component.\n+ Update: Adjusted priority of `render_block` filter to 20.\n+ Update: Added filter, `llms_block_supports_visibility` to allow modification of the return of the check.\n+ Update: Disabled block visibility on registration & account forms to prevent a potentially confusing form creation experience.\n+ Update: Added block editor rendering for password type fields.\n+ Update: Perform post migrations on `current_screen` instead of `admin_enqueue_scripts`.\n+ Update: Update various dependencies to use updated gutenberg packages.\n+ Bug fix: Fixed a WordPress 5.3 issues with JSON data affecting the ability to save course/membership instructors.\n+ Bug fix: Import `InspectorControls` from `wp.blockEditor` in favor of deprecated `wp.editor`\n+ Bug fix: Automatically store course/membership instructor with `post_author` data when the post is created.\n+ Bug fix: Pass style rules as camelCase.\n+ Bug fix: Fixed an issue causing \"No HTML Returned\" to be displayed in place of the Lesson Progression block on free lessons when viewed by a logged-out user.\n\n\nv3.37.3 - 2019-12-03\n--------------------\n\n+ Added an action `llms_certificate_generate_export` to allow modification of certificate exports before being stored on the server.\n+ Don't unslash uploaded file `tmp_name`, thanks [@pondermatic](https://github.com/pondermatic)!\n+ TwentyTwenty Theme Support: Hide site header and footer, and set a white body background in single certificates.\n+ Renamed setting field IDs to be unique for open/close wrapper fields on the engagements and account settings pages.\n+ Removed redundant functions defined in the `LLMS_Settings_Page` class to reduce code redundancy in account and engagement setting page classes.\n+ The `LLMS_Settings_Page` base class now automatically defines actions to save and output settings content.\n\n\nv3.37.2 - 2019-11-22\n--------------------\n\n+ LifterLMS notices will now be displayed on pages defined as a Course or Membership sales page.\n+ TwentyTwenty Theme: Updated to use `background-color` property instead of `background` shorthand when adding custom elements to style.\n+ Added filter `llms_sessions_end_idle_cron_recurrence` to allow customization of the recurrence of the idle session cleanup cronjob.\n+ Added filter `llms_quiz_is_open` to allow customization of whether or not a quiz is available to a student.\n+ When adding an client-side tracking events to the always make sure the server-side verification nonce is always set on the storage object.\n+ The Course/Membership filter on the main students reporting screen now correctly limits post results based on instructor access.\n\n\nv3.37.1 - 2019-11-13\n--------------------\n\n+ TwentyTwenty Theme: Fixed course information block misalignment.\n+ Fixed conflict with WooCommerce resulting from the movement of the deprecated LifterLMS function `is_filtered()`.\n\n\nv3.37.0 - 2019-11-11\n--------------------\n\n##### Updates\n\n+ Tested and compatible with WordPress core 5.3.\n+ Add theme support for the TwentyTwenty core default theme.\n+ Improved security and data sanitization in with regards to the SendWP integration connector.\n\n##### LifterLMS Rest API 1.0.0-beta.8\n\n+ Added memberships controller, huge thanks to [@pondermatic](https://github.com/pondermatic)!\n+ Added new filters:\n\n  + `llms_rest_lesson_filters_removed_for_response`\n  + `llms_rest_course_item_schema`\n  + `llms_rest_pre_insert_course`\n  + `llms_rest_prepare_course_object_response`\n  + `llms_rest_course_links`\n\n+ Improved validation when defining instructors for courses.\n+ Improved performance on post collection listing functions.\n+ Ensure that a course instructor is always set for courses.\n+ Fixed `sales_page_url` not returned in `edit` context.\n+ In `update_additional_object_fields()` method, use `WP_Error::$errors` in place of `WP_Error::has_errors()` to support WordPress version prior to 5.1.\n\n\nv3.36.5 - 2019-11-05\n--------------------\n\n+ Add filter: `llms_user_caps_edit_others_posts_post_types` to allow 3rd parties to utilize core methods for determining if a user can manage another users LMS content on the admin panel.\n\n\nv3.36.4 - 2019-11-01\n--------------------\n\n+ Fixes a conflict with CartFlows introduced by a Divi theme compatibility fix added in 3.36.3. Is WordPress complicated or what?\n\n\nv3.36.3 - 2019-10-24\n--------------------\n\n##### Updates\n\n+ Added new `LLMS_Membership` class methods: `get_categories()`, `get_tags()` and `toArrayAfter()` methods. Thanks [@pondermatic](https://github.com/pondermatic)!\n\n##### Compatibility\n\n+ Fixed access plan description conflicts with the Classic Editor block. This also resolves compatibility issues with Elementor which uses a hidden TinyMCE instance.\n+ Changed `pre_get_posts` callback from `10` (default) to `15`. Fixes conflict with Divi (and possibly other themes) which prevented LifterLMS catalog settings from functioning properly.\n\n##### Bugfixes\n\n+ Added translation to error message encountered when non-members attempt to purchase a members-only access plan. Thanks [@mrosati84](https://github.com/mrosati84)!\n+ Fix return of `LLMS_Generator::set_generator()`.\n+ Fixed a typo causing invalid imports from returning the expected error. Thanks [@pondermatic](https://github.com/pondermatic)!\n+ Fixed issue preventing membership post type settings from saving properly due to incorrect sanitization filters.\n+ Fixed issue where `wp_list_pluck()` would run on non arrays.\n\n\nv3.36.2 - 2019-10-01\n--------------------\n\n##### Updates\n\n+ Tested to WordPress 5.3.0-beta.2\n+ Upgrade UI on student course reporting screens.\n+ Added logic to physically remove from the membership level and remove enrollments data on related products, when deleting a membership enrollment.\n+ Lesson metabox \"start\" drip method made available only if the parent course has a start date set.\n\n##### Bugfixes\n\n+ Fixed JS error when client-side event tracking settings aren't loaded, thanks [@wenchen](https://github.com/wenchen)!\n+ Fixed PHP warning resulting from drip the \"Course Start\" lesson drip settings when no course start date exists.\n+ Fixed fatal error encountered when reviewing an order placed with a payment gateway that's been deactivated.\n\n##### Files Updated\n\n+ assets/js/app/llms-tracking.js\n+ includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php\n+ includes/models/model.llms.lesson.php\n+ includes/models/model.llms.student.php\n+ lifterlms.php\n\n##### Templates Updated\n\n+ templates/admin/post-types/order-details.php\n+ templates/admin/reporting/tabs/students/courses-course.php\n\n\nv3.36.1 - 2019-09-24\n--------------------\n\n##### Updates\n\n+ Include SendWP Connector in LifterLMS Engagement Settings.\n+ Removed usage of `WP_Error::has_errors()` to support WordPress version prior to 5.1.\n+ Improve performances when checking if an event is valid in `LLMS_Events->is_event_valid()`.\n+ Remove redundant check on `is_singular()` and `is_post_type_archive()` in `LLMS_Events->should_track_client_events()`.\n\n##### Bugfixes\n\n+ Fixed a compatibility issue with FitVids.js causing excess white space displayed around videos when using the library, WP plugin, or themes that utilize the library.\n+ Fixed an issue allowing recurring charges to continue processing after the order or customer had been deleted from the site.\n+ Fixed issue causing Membership Restriction settings from properly saving.\n+ Fixed issue that allowed instructors to see all quizzes on a site when the instructor had either no courses or only empty courses (courses with no lessons).\n+ Fixed \"Last Seen\" column displaying wrong date when the student last login date was saved as timestamp.\n+ Fixed an issue causing popover notifications to be skipped (never displayed) as a result of redirects.\n\n\nv3.36.0 - 2019-09-16\n--------------------\n\n##### User Interaction event and session Tracking\n\n+ Added user interaction tracking for the following events:\n\n  + User sign in and out.\n  + Page load and exit (for LMS content)\n  + Page focus and blur (for LMS content)\n  + And more to come\n\n+ Interaction events are grouped into sessions automatically. A session is \"closed\" after 30 minutes of inactivity or a log-out event.\n+ Added \"Last Seen\" student reporting column which reports the last recorded activity for the student.\n\n##### Enhancements\n\n+ Automatically hydrate when calling LLMS_Abstract_Database_Store::to_array().\n+ Added CSS to make course and lesson video embeds automatically responsive.\n\n##### Bug Fixes\n\n+ Correctly pass the `$remember` variable when using `llms_set_person_auth_cookie()`.\n+ Fixed undefined index error when retrieving an unset value from an unsaved database model.\n+ Fix issue causing quotes to be encoded in shortcodes used in course and membership restriction message settings fields.\n+ Fix issue preventing manual updates of order dates (next payment, trial expiration, and access expiration) from being saved properly.\n\n\nv3.35.2 - 2019-09-06\n--------------------\n\n+ When sanitizing settings, don't strip tags on editor and textarea fields that allow HTML.\n+ Added JS filter `llms_lesson_rerender_change_events` to lesson editor view re-render change events.\n\n\nv3.35.1 - 2019-09-04\n--------------------\n\n+ Fix instances of improper input sanitization and handling.\n+ Include scripts, styles, and images for reporting charts and datepickers\n\n\nv3.35.0 - 2019-09-04\n--------------------\n\n##### Security Notice\n\n+ Fixed a security vulnerability disclosed by the WordPress plugin review team. Please upgrade immediately!\n\n##### Updates\n\n+ Explicitly setting css and js file versions for various static assets..\n+ Added data sanitization methods in various form handlers.\n+ Added nonce verification to various form handlers.\n\n##### Bug fixes\n\n+ Fixed some translation strings that had literal variables instead of placeholders.\n+ Fixed undefined index error encountered when attempting to email a voucher export.\n+ Fixed undefined index error when PHP file upload errors are encountered during a course import.\n\n##### Deprecations\n\nThe following unused classes have been marked as deprecated and will be removed from LifterLMS in the next major release.\n\n+ LLMS_Analytics_Memberships\n+ LLMS_Analytics_Courses\n+ LLMS_Analytics_Sales\n+ LLMS_Meta_Box_Expiration\n+ LLMS_Meta_Box_Video\n\n##### Template Updates\n\n+  [admin/reporting/tabs/courses/overview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/courses/overview.php)\n+  [admin/reporting/tabs/memberships/overview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/memberships/overview.php)\n+  [admin/reporting/tabs/quizzes/attempts.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/attempts.php)\n+  [admin/reporting/tabs/quizzes/overview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/overview.php)\n+  [admin/reporting/tabs/students/courses-course.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses-course.php)\n+  [admin/reporting/tabs/students/courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses.php)\n+  [loop/featured-image.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/featured-image.php)\n+  [myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php)\n+  [quiz/results.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results.php)\n+  [single-certificate.php](https://github.com/gocodebox/lifterlms/blob/master/templates/single-certificate.php)\n+  [single-no-access.php](https://github.com/gocodebox/lifterlms/blob/master/templates/single-no-access.php)\n+  [taxonomy-course_cat.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_cat.php)\n+  [taxonomy-course_difficulty.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_difficulty.php)\n+  [taxonomy-course_tag.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_tag.php)\n+  [taxonomy-course_track.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-course_track.php)\n+  [taxonomy-membership_cat.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-membership_cat.php)\n+  [taxonomy-membership_tag.php](https://github.com/gocodebox/lifterlms/blob/master/templates/taxonomy-membership_tag.php)\n\n\nv3.34.5 - 2019-08-29\n--------------------\n\n+ Fixed logic issues preventing pending orders from being completed.\n\n##### Templates Changed\n\n+ [checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-confirm-payment.php)\n\nv3.34.4 - 2019-08-27\n--------------------\n\n+ Add a new admin settings field type, \"keyval\", used for displaying custom html alongside a setting.\n+ Added filter `llms_order_can_be_confirmed`.\n+ Always bind JS for the login form handler on checkout and registration screens.\n\n##### Templates Changed\n\n+ [checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-confirm-payment.php)\n\n##### LifterLMS REST API v1.0.0-beta.6\n\n+ Fix issue causing certain webhooks to not trigger as a result of action load order.\n+ Change \"access_plans\" to \"Access Plans\" for better human reading.\n\n\nv3.34.3 - 2019-08-22\n--------------------\n\n+ During payment gateway order completion, use `llms_redirect_and_exit()` instead of `wp_redirect()` and `exit()`.\n\n##### LifterLMS REST API v1.0.0-beta.5\n\n+ Load all required files and functions when authentication is triggered.\n+ Access `$_SERVER` variables via `filter_var` instead of `llms_filter_input` to work around PHP bug https://bugs.php.net/bug.php?id=49184.\n\n\nv3.34.2 - 2019-08-21\n--------------------\n\n##### LifterLMS REST API v1.0.0-beta.4\n\n+ Load authentication handlers as early as possible. Fixes conflicts with numerous plugins which load user information earlier than expected by the WordPress core.\n+ Harden permissions associated with viewing student enrollment information.\n+ Returns a 400 Bad Request when invalid dates are supplied.\n+ Student Enrollment objects return student and post id's as integers instead of strings.\n+ Fixed references to an undefined function.\n\n\nv3.34.1 - 2019-08-19\n--------------------\n\n+ Update LifterLMS REST to v1.0.0-beta.3\n\n##### Interface and Experience improvements during API Key creation\n\n+ Better expose that API Keys are never shown again after the initial creation.\n+ Allow downloading of API Credentials as a `.txt` file.\n+ Add `required` properties to required fields.\n\n##### Updates\n\n+ Added the ability to CRUD webhooks via the REST API.\n+ Conditionally throw `_doing_it_wrong` on server controller stubs.\n+ Improve performance by returning early when errors are encountered for various methods.\n+ Utilizes a new custom property `show_in_llms_rest` to determine if taxonomies should be displayed in the LifterLMS REST API.\n+ On the webhooks table the \"Delivery URL\" is trimmed to 40 characters to improve table readability.\n\n##### Bug fixes\n\n+ Fixed a formatting error when creating webhooks with the default auto-generated webhook name.\n+ On the webhooks table a translatable string is output for the status instead of the database value.\n+ Fix an issue causing the \"Last\" page pagination link to display for lists with 0 possible results.\n+ Don't output the \"Last\" page pagination link on the last page.\n\n\n\nv3.34.0 - 2019-08-15\n--------------------\n\n##### LifterLMS REST API v1.0.0-beta.1\n\n+ A robust REST API is now included in the LifterLMS core.\n+ Create API Keys to consume and manage LifterLMS resources and students from external applications.\n+ Create webhooks to pass LifterLMS resource data to external applications (like Zapier!).\n+ The full API specification can be found at [https://gocodebox.github.io/lifterlms-rest/](https://gocodebox.github.io/lifterlms-rest/).\n\n##### Student management capabilities\n\n+ Explicit capabilities have been added to determine which users can create, view, update, and delete students.\n+ Admins and LMS Managers have all student management capabilities.\n+ Instructors and instructors assistants are granted limited view capabilities allowing them to only view students enrolled in their own courses/memberships.\n+ Added the `list_users` capability to the \"Instructor\" role, allowing instructor's to better view and manage their assistant instructors.\n+ The new capabilities are: `create_students`, `view_students`, `view_others_students`, `edit_students`, `edit_others_students`, `delete_students`, & `delete_others_students`.\n\n##### Updates\n\n+ Added new actions to help differentiate enrollment creation and update events.\n+ Added methods and logic for managing user management of other users.\n+ Added a filter `llms_table_get_table_classes` to LifterLMS admin tables which allows customization of the CSS classes applied to the `<table>` elements. Thanks  [@pondermatic](https://github.com/pondermatic)!\n+ Added a filter `llms_install_get_schema` to the database schema to allow 3rd parties to run table installations alongside the core.\n+ Added the ability to pull \"raw\" (unfiltered) data from the database via classes extending the `LLMS_Post_Model` abstract.\n+ Added a `bulk_set()` method to the `LLMS_Post_Model` abstract allowing the updating of multiple properties in one command.\n+ Added `comment_status`, `ping_status`, `date_gmt`, `modified_gmt`, `menu_order`, `post_password` as gettable\\settable post properties via the `LLMS_Post_Model` abstract.\n+ Links on reporting tables are now the proper color.\n+ The `editable_roles` filter which determines which roles can manage which other roles is now always loaded (instead of being loaded only on the admin panel).\n+ Updated LifterLMS Blocks to 1.5.2\n\n##### Bug Fixes\n\n+ Fixed an issue preventing the `user_url` property from being retrieved by the `get()` method of the `LLMS_Abstract_User_Data` class.\n+ Fixed an issue causing the `LLMS_Instructors::get_assistants()` method to return assistants for the currently logged in user instead of the instructor of the instantiated object.\n+ Fixed an issue which would allow LMS Managers to edit and delete site administrators.\n\n##### Deprecations\n\n**The following functions and methods have been marked as deprecated and will be removed from LifterLMS with the next major release.**\n\n+ LLMS_Course::get_children_sections() use LLMS_Course::get_sections( 'posts' )\" instead\n+ LLMS_Course::get_children_lessons() use LLMS_Course::get_lessons( 'posts' )\" instead\n+ LLMS_Course::get_author()\n+ LLMS_Course::get_author_id() use LLMS_Course::get( \"author\" ) instead\n+ LLMS_Course::get_author_name()\n+ LLMS_Course::get_sku() use LLMS_Course::get( \"sku\" ) instead\n+ LLMS_Course::get_id() use LLMS_Course::get( \"id\" ) instead\n+ LLMS_Course::get_title() use get_the_title() instead\n+ LLMS_Course::get_permalink() use get_permalink() instead\n+ LLMS_Course::get_user_postmeta_data()\n+ LLMS_Course::get_user_postmetas_by_key()\n+ LLMS_Course::get_checkout_url()\n+ LLMS_Course::get_start_date() use LLMS_Course::get_date( \"start_date\" ) instead\n+ LLMS_Course::get_end_date() use LLMS_Course::get_date( \"end_date\" ) instead\n+ LLMS_Course::get_next_uncompleted_lesson()\n+ LLMS_Course::get_lesson_ids() use LLMS_Course::get_lessons( \"ids\" ) instead\n+ LLMS_Course::get_syllabus_sections() use LLMS_Course::get_sections() instead\n+ LLMS_Course::get_short_description() use LLMS_Course::get( \"excerpt\" ) instead\n+ LLMS_Course::get_syllabus() use LLMS_Course::get_sections() instead\n+ LLMS_Course::get_user_enroll_date()\n+ LLMS_Course::get_user_post_data()\n+ LLMS_Course::check_enrollment()\n+ LLMS_Course::is_user_enrolled() use llms_is_user_enrolled() instead\n+ LLMS_Course::get_student_progress() use LLMS_Student::get_progress() instead\n+ LLMS_Course::get_membership_link()\n\n\nv3.33.2 - 2019-06-26\n--------------------\n\n+ It is now possible to send test copies of the \"Student Welcome\" email to yourself.\n+ Improved information logged when an error is encountered during an email send.\n+ Add backwards compatibility for legacy add-on integrations priority loading method.\n+ Fixed undefined index notice when viewing log files on the admin status screen.\n\n\nv3.33.1 - 2019-06-25\n--------------------\n\n##### Updates\n\n+ Added method to retrieve the load priority of integrations.\n+ The capabilities used to determine if uses can clone and export courses now check `edit_course` instead of `edit_post`.\n\n##### Bug Fixes\n\n+ Fixed an issue which would cause the \"Net Sales\" line to sometimes display as a bar on the sales revenue reporting chart.\n+ Fixed an issue causing a PHP notice to be logged when viewing the sales reporting screen.\n+ Fixed an issue causing backslashes to be added before quotation marks in access plan descriptions.\n+ Integration classes are now loaded in the order defined by the integration class.\n+ Fixed an issue causing a PHP error when viewing the admin logs screen when no logs exist.\n\n\nv3.33.0 - 2019-05-21\n--------------------\n\n##### Updates\n\n+ Added the ability for site administrators to delete (completely remove) enrollment records from the database.\n+ Catalogs sorted by Order (`menu_order`) now have an additional sort (by post title) to improve ordering consistency for items with the same order, thanks [@pondermatic](https://github.com/pondermatic)!\n+ Hooks in the dashboard order review template now pass the `LLMS_Order`.\n\n##### LifterLMS Blocks\n\n+ Updated to version 1.5.1\n+ All blocks are now registered only for post types where they can actually be used.\n+ Only register block visibility settings on static blocks. Fixes an issue causing core (or 3rd party) dynamic blocks from being managed within the block editor.\n\n##### Bug Fixes\n\n+ If an enrolled student accesses checkout for a course/membership they're already enrolled in they will be shown a message stating as much.\n+ Removed a redundant check for the existence of an order on the dashboard order review template.\n+ When an order is deleted, student enrollment records for that order will be removed. This fixes an issue causing admins to not be able to manage the enrollment status of a student enrolled via a deleted order.\n+ Fix issue causing errors when using the `[lifterlms_lesson_mark_complete]` shortcode on course post types.\n+ Fixed an issue causing quiz questions to generate publicly accessible permalinks which could be indexed by search engines.\n\n##### Templates Changed\n\n+ [course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/templates/myaccount/view-order.php)\n\n\nv3.32.0 - 2019-05-13\n--------------------\n\n##### Updates\n\n+ Added Membership reporting\n+ Added the ability to restrict coupons to courses and memberships which are in draft or scheduled status.\n+ When recurring payments are disabled, output a \"Staging\" bubble on the \"Orders\" menu item.\n+ Recurring recharges now add order notes and trigger actions when gateway or recurring payment status errors are encountered.\n+ When managing recurring payment status through the warning notice, stay on the same page and clear nonces instead of redirecting to the LifterLMS Settings screen.\n+ Updated the Action Scheduler library to the latest version (2.2.5)\n+ Exposed the Action Scheduler's scheduled actions interface as a tab on the LifterLMS Status page.\n\n##### LifterLMS Blocks\n\n+ Updated to version 1.4.1.\n+ Fixed issue causing asset paths to have invalid double slashes.\n+ Fixed issue causing frontend css assets to look for an unresolvable dependency.\n\n##### Bug Fixes\n\n+ Fixed an issue allowing instructors to view a list of students from courses and memberships they don't have access to.\n+ WooCommerce compatibility filters added in 3.31.0 are now scheduled at `init` instead of `plugins_loaded`, resolves conflicts with several WooCommerce add-ons which utilize core WC functions before LifterLMS functions are loaded.\n\n\nv3.31.0 - 2019-05-06\n--------------------\n\n##### Updates\n\n+ Tested to WordPress 5.2\n+ Adds explicit support for the twentynineteen default theme.\n+ The main students reporting table can now be filtered to show only students enrolled in a specific course or membership.\n+ Resolve conflict with WooCommerce (3.6 and later) resulting in 404s on the dashboard endpoints \"lost password\", \"order history\", and \"edit account\".\n+ Adds a dynamic filter (`llms_notification_view{$trigger_id}_basic_options`) to basic (pop-over) notifications to allow configuration of their settings.\n+ The filter `llms_plan_get_checkout_url` now passes a 3rd parameter: `$check_availability`\n+ Improves `LLMS_Course_Data` and `LLMS_Quiz_Data` classes by adding shared functionality to a shared abstract, `LLMS_Abstract_Post_Data`\n+ Changed access on class methods in `LLMS_Shortcode_Courses` from private to protected, thanks [@andrewvaughan](https://github.com/andrewvaughan)!\n\n##### Bug fixes\n\n+ Treats `post_excerpt` data as HTML instead of plain text. Fixes an issue resulting in HTML tags being stripped from lesson excerpts when duplicating a lesson in the course builder or importing lessons via the course importer.\n+ Fix an issue allowing access plan sales prices to be set as negative values.\n\n##### LifterLMS Blocks\n\n+ Updated to LifterLMS Blocks 1.4.0.\n+ Adds an \"unmigration\" utility to LifterLMS -> Status -> Tools & Utilities which can be used to remove LifterLMS blocks from courses and lessons which were migrated to the block editor structure.\n+ This tool is only available when the Classic Editor plugin is installed and enabled and it will remove blocks from ALL courses and lessons regardless of whether or not the block editor is being utilized on that post.\n\n##### Deprecations\n\n+ `LLMS_Query::add_query_vars()` use `LLMS_Query::set_query_vars()` instead.\n\n\nv3.30.3 - 2019-04-22\n--------------------\n\n##### Updates\n\n+ Fixed typos and spelling errors in various strings.\n+ Corrected a typo in the `content-disposition` header used when exporting voucher CSVs, thanks [@pondermatic](https://github.com/pondermatic)!\n+ Improved the quiz attempt grading experience by automatically focusing the remarks field and only toggling the first answer if it's not visible, thanks [@eri-trabiccolo](https://github.com/eri-trabiccolo)!\n+ Removed commented out code on the Student Dashboard Notifications Tab template, thanks [@tnorthcutt](https://github.com/tnorthcutt)!\n\n##### Bug Fixes\n\n+ Renamed \"descrpition\" key to \"description\" found in the return of `LLMS_Instructor()->toArray()`.\n+ Fixed an issue causing slashes to be stripped from course content when cloning a course.\n+ Fixed an issue causing JS warnings to be thrown in the Javascript console on Course and Membership edit pages on the admin panel due to variables being defined too late, thanks [@eri-trabiccolo](https://github.com/eri-trabiccolo)!\n+ Fixed an undefined variable notice encountered when filtering quiz attempts on the quiz attempts reporting screen, thanks [@eri-trabiccolo](https://github.com/eri-trabiccolo)!\n+ Fixed an issue causing slashes to appear before quotation marks when saving remarks on a quiz attempt, thanks [@eri-trabiccolo](https://github.com/eri-trabiccolo)!\n+ [@pondermatic](https://github.com/pondermatic) fixed typos and misspellings in comment and docs in over 200 files and while that doesn't concern most users it's worthy of a mention.\n\n##### Deprecations\n\nThe following unused classes have been marked as deprecated and will be removed from LifterLMS in the next major release.\n\n+ `LLMS\\Users\\User`\n+ `LLMS_Analytics_Page`\n+ `LLMS_Course_Basic`\n+ `LLMS_Lesson_Basic`\n+ `LLMS_Quiz_Legacy`\n\n##### Template Updates\n\n+ [templates/myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-notifications.php)\n\n\nv3.30.2 - 2019-04-09\n--------------------\n\n+ Added new filter to allow 3rd parties to determine if a `LLMS_Post_Model` field should be added to the `custom` array when converting the post to an array.\n+ Added hooks and filters to the `LLMS_Generator` class to allow 3rd parties to easily generate content during course clone and import operations.\n+ Fixed an issue causing all available courses to display when the [lifterlms_courses] shortcode is used with the \"mine\" parameter and the current user viewing the shortcode is not enrolled in any courses.\n+ Fixed a PHP undefined variable warning present on the payment confirmation screen.\n\n##### Template Updates\n\n+ [templates/checkout/form-confirm-payment.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-confirm-payment.php)\n\n\nv3.30.1 - 2019-04-04\n--------------------\n\n##### Updates\n\n+ Added handler to automatically resume pending (incomplete or abandoned) orders.\n+ Classes extending the `LLMS_Abstract_API_Handler` can now prevent a request body from being sent.\n+ Added dynamic filter `'llms_' . $action . '_more'` to allow customization of the \"More\" button text and url for student dashboard sections. Thanks @[pondermatic](https://github.com/pondermatic).\n+ Remove unused CSS code on the admin panel.\n\n##### Bug Fixes\n\n+ Fixed a bug preventing course imports as a result of action priority ordering issues.\n+ Function `llms_get_order_by_key()` correctly returns `null` instead of false when no order is found and will return an `int` instead of a numeric string when an order is found.\n+ Changed the method used to sort question choices to accommodate numeric choice markers. This fixes an issue in the Advanced Quizzes add-on causing reorder questions with 10+ choices to sort display in the incorrect order.\n+ Increased the specificity of LifterLMS element tooltip hovers. Resolves a conflict causing issues on the WooCommerce tax rate management screen.\n+ Fixed an issue causing certain fields in the Customizer from displaying a blue background as a result of very unspecific CSS rules, thanks [@Swapnildhanrale](https://github.com/Swapnildhanrale)!\n+ Fixed builder deep links to quizzes freezing due to dependencies not being available during initialization.\n+ Fixed builder issue causing duplicate copies of questions to be added when adding existing questions multiple times.\n\n##### Template Updates\n\n+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php)\n\n\nv3.30.0 - 2019-03-21\n--------------------\n\n##### Updates\n\n+ **Create custom thank you pages with new access plan checkout redirect options.**\n+ Added the ability to sort items on the membership auto enrollment table (drag and drop to sort and reorder).\n+ Improved the interface and interactions with the membership auto enrollment table settings.\n\n##### LifterLMS Blocks\n\n+ Updated LifterLMS Blocks to 1.3.8.\n+ Fixed an issue causing some installations to be unable to use certain blocks due to jQuery dependencies being declared improperly.\n\n##### Bug Fixes\n\n+ Fixed issue preventing courses with the same title from properly displayed on the membership automatic enrollment courses table on the admin panel.\n+ Fixed an issue preventing builder custom fields from being able to specify a custom sanitization callback.\n+ Fixed an issue preventing builder custom fields from being able to properly save and render multi-select data.\n\n##### Template Updates\n\n+ [templates/product/access-plan-restrictions.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-restrictions.php)\n+ [templates/product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/free-enroll-form.php)\n\n\nv3.29.4 - 2019-03-08\n--------------------\n\n+ Fixed an issue preventing users with email addresses containing an apostrophe from being able to login.\n\n\nv3.29.3 - 2019-03-01\n--------------------\n\n##### Bug Fixes\n\n+ Removed attempts to validate & save access plan data when the Classic Editor \"post\" form is submitted.\n+ Fix issue causing 1-click free-enrollment for logged in users to refresh the screen without actually performing an enrollment.\n\n##### Template Updates\n\n+ [product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/free-enroll-form.php)\n\n\nv3.29.2 - 2019-02-28\n--------------------\n\n+ Fix issue causing blank \"period\" values on access plans from being updated.\n+ Fix an issue preventing paid access plans from being switched to \"Free\".\n\n\nv3.29.1 - 2019-02-27\n--------------------\n\n+ Automatically reorder access plans when a plan is deleted.\n+ Skip (don't create) empty plans passed to the access plan save method as a result of deleted access plans.\n\n\nv3.29.0 - 2019-02-27\n--------------------\n\n##### Improved Access Plan Management\n\n+ Added a set of methods for creating access plans programmatically.\n+ Updated the Access Plan metabox on courses and lessons with improved data validation.\n+ When using the block editor, the \"Pricing Table\" block will automatically update when access plan changes are saved to the database (from LifterLMS Blocks 1.3.5).\n+ Access plans are now created and updated via AJAX requests, resolves a 5.0 editor issue causing duplicated access plans to be created.\n\n##### Student Management Improvements\n\n+ Added the ability for instructors and admins to mark lessons complete and incomplete for students via the student course reporting table.\n\n##### Admin Panel Settings and Reporting Design Changes\n\n+ Replaced LifterLMS logos and icons on the admin panel with our new logo LifterLMS Logo and Icons.\n+ Revamped the design and layout of settings and reporting screens.\n\n##### Checkout Improvements\n\n+ Updated checkout javascript to expose an error addition functions\n+ Abstracted the checkout form submission functionality into a callable function not directly tied to `$_POST` data\n+ Removed display order field from payment gateway settings in favor of using the gateway table sortable list\n\n##### Other Updates\n\n+ Removed code related to an incompatibility between Yoast SEO Premium and LifterLMS resulting from former access plan save methods.\n+ Reduced application logic in the `course/complete-lesson-link.php` template file by refactoring button display filters into functions.\n+ Added function for checking if request is a REST request\n+ Updated LifterLMS Blocks to version 1.3.7\n\n##### Bug Fixes\n\n+ Fixed an issue preventing \"Pricing Table\" blocks from displaying on the admin panel when the current user was enrolled in the course or no payment gateways were enabled on the site.\n+ Fixed the checkout nonce to have a unique ID & name\n+ Fixed an issue with deleted quizzes causing quiz notification's to throw fatal errors.\n+ Fixed an issue preventing notification timestamps from displaying on the notifications dashboard page.\n+ Fix an issue causing `GET` requests with no query string variables from causing issues via incorrect JSON encoding via the API Handler abstract.\n+ Fix an issue causing access plan sale end dates from using the default WordPress date format settings.\n+ `LLMS_Lesson::has_quiz()` will now properly return a boolean instead of the ID of the associated quiz (or 0 when none found)\n\n##### Template Updates\n\n+ [checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php)\n+ [course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php)\n+ [product/access-plan-pricing.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-pricing.php)\n+ [notifications/basic.php](https://github.com/gocodebox/lifterlms/blob/master/templates/notifications/basic.php)\n\n##### Templates Removed\n\nAdmin panel templates replaced with view files which cannot be overridden from a theme or custom plugin.\n\n+ `admin/post-types/product-access-plan.php`\n+ `admin/post-types/product.php`\n\n\nv3.28.3 - 2019-02-14\n--------------------\n\n+ ❤❤❤ Happy Valentines Day or whatever ❤❤❤\n+ Tested to WordPress 5.1\n+ Fixed an issue causing JSON data saved by 3rd party plugins in course or lesson postmeta fields to be not duplicate properly during course duplications and imports.\n\n\nv3.28.2 - 2019-02-11\n--------------------\n\n##### Updates\n\n+ Updated default country list to remove non-existent countries and resolve capitalization issues, thanks [nrherron92](https://github.com/nrherron92)!\n\n##### Bug fixes\n\n+ Fixed an issue causing the email notification content getter to use the same filter as popover notifications.\n+ Fixed an issue preventing default blog date & time settings from being used when displaying an access plan's access expiration date on course and membership pricing tables.\n+ Fixed an issue causing 404s on paginated dashboard endpoints when the permalink structure is set to anything other than `%postname%`.\n\n##### Deprecations\n\n+ `LLMS_Query->set_dashboard_pagination()`\n\n\nv3.28.1 - 2019-02-01\n--------------------\n\n+ Fixed an issues preventing exports to be accessible on Apache servers.\n+ Fixed an issue causing servers with certain nginx rules to open CSV exports directly instead of downloading them.\n\n\nv3.28.0 - 2019-01-29\n--------------------\n\n##### Updates\n\n+ Updated reporting table export functions to provide immediate download prompts of the files. Exports are generated in real time and you *must* remain on the page while it generates. The good news is if your site had issues with email or cronjobs it'll no longer be an issue for you.\n+ Updated lesson metabox to use icons for attached quizzes\n+ Added an orange highlight to the admin \"Add-Ons & More\" menu item\n+ Removed unused cron event.\n\n##### LifterLMS Blocks\n\n+ Updated LifterLMS Blocks to 1.3.4\n+ Adds support for handling courses & lessons in \"Classic Editor\" mode as defined by the Divi page builder\n+ Skips course and lesson migration when \"Classic\" mode is enabled.\n+ Adds conditions to identify \"Classic\" mode when the Classic Editor plugin settings are configured to enforce classic (or block) mode for *all* posts.\n\n##### Database Updates\n\n+ Unschedules the aforementioned unused cron event.\n\n##### Bug fixes\n\n+ Fixed an issue preventing the temp directory old file cleanup cron from firing on schedule.\n+ During plugin uninstallation the tmp cleanup cron will now be properly unscheduled.\n+ Fixed an issue causing notifications on the student dashboard to appear on top of static headers or the WP Admin Bar when scrolling.\n+ Fixed an issue preventing manual updating of customer and source information on orders resulting from unfocusable hidden form fields.\n+ Fixed mismatched HTML tags on the Admin Add-Ons screen\n\n##### Deprecations\n\n+ Class method: `LLMS_Admin_Table::queue_export()`\n+ Class: `LLMS_Processor_Table_To_Csv`\n\n\nv3.27.0 - 2019-01-22\n--------------------\n\n###### Updates\n\n+ Added the ability to add existing questions to a quiz in the course builder. This allows cloning of existing questions as well as attaching \"orphaned\" questions currently attached to no quizzes.\n+ Added the ability to detach questions from quizzes. Coupled with adding existing questions, questions can now be easily moved between quizzes.\n+ Added permalink capabilities to the builder to allow linking to specific items within the builder (a lesson, quiz, etc...).\n+ Quizzes with 0 possible points will no longer show a Pass/Fail chart with a 0% (failing) grade on quiz results screens.\n+ Replaced option `lifterlms_lock_down` which cannot be set via any setting with a filter to reduce database calls. This will have no effect on anyone unless you manually set this option to \"no\" via a database query. Having done this would allow the admin bar to be shown to students.\n\n##### Bug Fixes\n\n+ Fixed an issue causing the default \"Redeem Voucher\" and \"My Orders\" student dashboard endpoint slugs from not having the correct default values. Thanks [@tnorthcutt](https://github.com/tnorthcutt)!\n+ Fixed an issue causing quotation marks in quiz question answers to show escaping slashes on results screens.\n+ Fixed a bug preventing viewing quiz results for quizzes with questions that have been deleted.\n+ Fixed a bug causing a PHP Notice to be output when registering a new user with a valid voucher.\n\n##### Templates Changed\n\n+ [quiz/results-attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt.php)\n\n\nv3.26.4 - 2019-01-16\n--------------------\n\n+ Update to [LifterLMS Blocks 1.3.2](https://make.lifterlms.com/2019/01/15/lifterlms-blocks-version-1-3-1/), fixing an issue preventing template actions from being removed from migrated courses & lessons.\n\n\nv3.26.3 - 2019-01-15\n--------------------\n\n##### Updates\n\n+ Fix issue preventing course difficulty and course length from being edited when using the classic editor plugin.\n+ Improved pagination methods on Student Dashboard Endpoints\n+ \"My Notifications\" dashboard tab now consistently paginated like other dashboard endpoints\n+ Update to [LifterLMS Blocks 1.3.1](https://make.lifterlms.com/2019/01/15/lifterlms-blocks-version-1-3-1/).\n\n##### Bug Fixes\n\n+ Fixed an issue preventing course difficulty and course length from being edited when using various page builders.\n+ Fixed issues causing errors on quiz reporting screens for quiz attempts made by deleted users.\n\n##### Deprecated Functions\n\n+ `LLMS_Student_Dashboard::output_notifications_content()` replaced with `lifterlms_template_student_dashboard_my_notifications()`\n\n##### Templates Changed\n\n+ [myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-notifications.php)\n+ [admin/reporting/tabs/quizzes/attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/attempt.php)\n\n\nv3.26.2 - 2019-01-09\n--------------------\n\n+ Fast follow to fix incorrect version number pushed to the readme files for 3.26.1 which prevents upgrading to 3.26.1\n\n\nv3.26.1 - 2019-01-09\n--------------------\n\n##### Updates\n\n+ Tested to WordPress 5.0.3\n+ Student CSV reports will now bypass cached data during report generation.\n+ Add course and membership catalog visibility settings into the block editor.\n+ Includes LifterLMS Blocks 1.3.0.\n\n##### Bug Fixes\n\n+ Fixed issue preventing the course instructors metabox from displaying when using the classic editor plugin.\n+ Fixed an issue causing membership background enrollment from processing when the course background processor is disabled via filters.\n+ Fixed an issue causing errors when reviewing orders on the admin panel which were placed via a payment gateway which is no longer active.\n+ Fixed an issue preventing course difficulty and course length from being edited when using the classic editor plugin.\n+ Fixed a very convoluted conflict between LifterLMS, WooCommerce, and Elementor explained at https://github.com/gocodebox/lifterlms/issues/730.\n\n\nv3.26.0 - 2018-12-27\n--------------------\n\n+ Adds conditional support for page builders: Beaver Builder, Divi Builder, and Elementor.\n+ Fixed issue causing LifterLMS core sales pages from outputting automatic content (like pricing tables) on migrated posts.\n+ Student unenrollment calls always bypass cache during enrollment precheck.\n+ Membership post type \"name\" label is now plural (as it is supposed to be).\n\n\nv3.25.4 - 2018-12-17\n--------------------\n\n+ Adds a filter (`llms_blocks_is_post_migrated`) to allow determining if a course or lesson has been migrated to the WP 5.0 block editor.\n+ Added a filter (`llms_dashboard_courses_wp_query_args`) to the WP_Query used to display courses on the student dashboard.\n+ Fixed issue on course builder causing prerequisites to not be saved when the first lesson in a course was selected as the prereq.\n+ Fixed issue on course builder causing lesson settings to be inaccessible without first saving the lesson to the database.\n\n\nv3.25.3 - 2018-12-14\n--------------------\n\n+ Fixed compatibility issue with the Classic Editor plugin when it was added after a post was migrated to the new editor structure.\n\n\nv3.25.2 - 2018-12-13\n--------------------\n\n+ Added new filters to the `LLMS_Product` model.\n+ Fix issue with student dashboard login redirect causing a white screen on initial login.\n\n\nv3.25.1 - 2018-12-12\n--------------------\n\n##### Updates\n\n+ Editor blocks now display a lock icon when hovering/selecting a block which corresponds to the enrollment visibility settings of the block.\n+ Removal of core actions is now handled by a general migrator function instead of by individual blocks.\n\n##### Bug fixes\n\n+ Fixed issue preventing strings from the lifterlms-blocks package from being translatable.\n+ Fix issue causing block visibility options to not be properly set when enrollment visibility is first enabled for a block.\n+ Fixed compatibility issue with Yoast SEO Premium redirect manager settings, thanks [@moorscode](https://github.com/moorscode)!\n+ Fixed typo preventing tag size options (or filters) of course information block from functioning properly. Thanks [@tnorthcutt](https://github.com/tnorthcutt)!\n\n##### Templates Changed\n\n+ [templates/course/meta-wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/meta-wrapper-start.php)\n\n\nv3.25.0 - 2018-12-05\n--------------------\n\n##### WordPress 5.0 Ready!\n\n+ **Tested with WordPress core 5.0 (Gutenberg)!**\n+ Editor Blocks: Course and Lesson layouts are now (preferably) powered by various editor blocks.\n+ When a block is added to a course or lesson, the template hook that automatically outputs that element is removed automatically (preventing duplicates).\n+ If you use the LifterLMS Labs: Action Manager you may no longer need it!\n+ Course & Membership instructors are now managed through an editor \"plugin\". Check out the rocket icon near the \"Publish/Update\" button.\n+ Instructor metabox will load conditionally based on presence of the block editor\n+ New courses and lessons will automatically have a preloaded block editor template\n+ Courses and lessons will automatically be \"migrated\" to these templates when edited on the admin panel\n+ Various course settings conditionally load based on the presence of the block editor\n+ Added filter to the headline size in the `course/meta-wrapper-start.php` template. Allows customization of headline via the \"Course Information\" block settings.\n+ If you're not ready for WordPress 5.0 you can still upgrade LifterLMS. This release is fully functional without the block editor.\n\n##### Bug Fixes\n\n+ Fixed typo in `quiz/start-button.php` template.\n+ Fixed error occurring during activation of LaunchPad via the Add-Ons & More screen.\n+ Fixed issue causing quiz reporting screens to be blank for users without `view_others_lifterlms_reports` capabilities.\n\n##### Templates Changed\n\n+ [templates/course/author.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/author.php)\n+ [course/meta-wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/meta-wrapper-start.php)\n+ [quiz/start-button.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/start-button.php)\n\n\nv3.24.3 - 2018-11-13\n--------------------\n\n##### Updates\n\n+ Added user email, login, url, nicename, display name, first name, and last name as fields searched when searching orders. Thanks Thanks [@yojance](https://github.com/yojance)!\n\n##### Bug Fixes\n\n+ Fixed issue causing fatal errors encountered during certificate downloading caused by CSS `<link>` tags existing outside of the `<head>` element.\n+ Certificates downloaded by users who can see the WP Admin Bar will no longer show the admin bar on the downloaded certificate\n+ Fixed issue on iOS Safari causing multiple choice quiz questions to require a \"long press\" to be properly selected\n+ Fixed issue causing access plan sales to end 36m and 1s prior to end of the day on the desired sale end date. Thanks [@eri-trabiccolo](https://github.com/eri-trabiccolo)!\n+ Ensure that fallback url slugs for course & membership archives are translatable.\n\n\nv3.24.2 - 2018-10-30\n--------------------\n\n+ Fix issue causing newline characters to be malformed on course builder description fields, resulting in `n` characters being output in strange places.\n\n\nv3.24.1 - 2018-10-29\n--------------------\n\n##### Updates\n\n+ The shortcode `[lifterlms_hide_content]` now accepts multiple IDs and can specify whether the user must belong to either *all* or *any one* of the specified memberships. Thanks [@yojance](https://github.com/yojance)!\n+ The action `llms_voucher_used`, called when a voucher code is used, will now pass the voucher code as a 3rd parameter. Thanks [@yojance](https://github.com/yojance)!\n\n##### Bug Fixes\n\n+ Fixed a typo in engagement drop creation dropdown. Thanks [README1ST](https://github.com/README1ST)!\n+ Fixed issue causing backslash characters (`\\`) to be removed from course elements (sections, lessons, quizzes, and assignments) constructed in the course builder.\n+ Fixed an issue in the 3.16.0 database migration script that would cause migrations to get stuck as a result of malformed data saved in an invalid format.\n+ Added processing handlers to payment confirmation form. Fixes an issue which would allow multiple payment confirmation requests to be made (if the form was submitted multiple times before the page reloaded) resulting in duplicate charges.\n\n##### Templates Changed\n\n+  templates/checkout/form-confirm-payment.php\n\n\nv3.24.0 - 2018-10-23\n--------------------\n\n##### \"My Grades\" Student Dashboard Endpoint\n\n+ A new student dashboard endpoint, \"My Grades\", has been added\n+ The main screen displays a paginated and sortable list of all courses a student is enrolled in and outputs their progress and grade in the courses\n+ Students can drill into individual reporting screens for each course where specific details for each course are available for review\n\n##### Grading Enhancements\n\n+ Each lesson can now be assigned an individual \"points\" value\n+ When a course is graded the points assigned to each lesson will be used to calculate the value of the lesson's grade within the overall course grade\n+ Lessons can also be assigned a value of \"0\" to allow a lesson to not count towards the overall grade of the course.\n+ Email notifications are now sent to a student when an instructor reviews, grades, or leaves remarks on a quiz attempt.\n\n##### Test Email Notifications\n\n+ An interface and API for sending test email notifications has been added, the following notifications can now be tested:\n\n  + Purchase Receipt\n  + Quizzes: Failed (Thanks [@philwp](https://github.com/philwp)!)\n  + Quizzes: Graded\n  + Quizzes: Passed (Thanks [@philwp](https://github.com/philwp)!)\n\n##### Updates and Enhancements\n\n+ Quiz Passed & Quiz Failed notifications have new names on the admin panel (\"Quizzes: Quiz Passed\" & \"Quizzes: Quiz Failed\")\n+ The default content for Quiz Passed and Quiz Failed notifications have been enhanced. If you've modified these you can delete your modified content to have your notifications \"restored\" to the improved defaults.\n+ Change the page title of the Student Dashboard page installed via the Setup Wizard to be \"Dashboard\" instead of \"My Courses.\" Thanks [@philwp](https://github.com/philwp)!\n+ In the course builder when a lesson is duplicated, the attached quiz will be duplicated as well\n+ Minor increase to performance in the `LLMS_Course->get_lessons()` method\n+ Added `student_id` as a parameter passed to the `llms_student_get_progress` filter\n+ Updated all access plan templates added in 3.23.0 to ensure `ABSPATH` is defined to prevent direct template access\n+ Remove use of deprecated `LLMS_Lesson->get_children_lessons()` in the `LLMS_Course` and `LLMS_Lesson` models as well as in the `course/syllabus.php` template\n+ Refactored the `LLMS_Section->get_percent_complete()` method to utilize methods from the `LLMS_Student` model\n+ Added the ability for admin table classes to define `<tr>` element CSS classes\n+ Admin settings pages with no settings to save (like the Notifications list) no longer display a \"Save\" button\n+ Added actions when creating, updating, and deleting records managed by `LLMS_Abstract_Database_Store` classes\n+ Updated system report to include URLs to settings with URLs, adds a small speed boost to support request turn around time.\n\n##### Please Rate & Review LifterLMS on WordPress.org\n\n+ Added a WordPress.org review request link to the footer of LifterLMS admin pages.\n+ Added a WordPress.org review request notice which displays a week after installation if the site has 50+ active students.\n\n##### Bug fixes\n\n+ Fixed issue causing HTML entity codes to display in email subject lines. Thanks [@philwp](https://github.com/philwp)!\n+ Fixed issue causing post cleanup functions to run queries against unsupported post types.\n+ Fixed typos in a handful of i18n functions so that the proper textdomain is now being used\n+ Removed `get_option()` call to unused option `lifterlms_logout_endpoint` which ran on WordPress initialization unnecessarily.\n+ Removed 3.21.0 fixes for iOS touch issues that are now causing iOS touch issues on quizzes.\n+ When an order is deleted, all order transactions will also be deleted. This does not happen until the order is deleted (transactions will remain while the order is in the trash)\n+ Fixed an issue causing duplicated quizzes to initially show images for question images & image choices (reorder pictures & picture choice) but the image data would not be properly saved so when returning to the builder or viewing a quiz on the frontend the images would be lost\n\n##### Deprecated Functions & Methods\n\n+ Deprecated `LLMS_Section->get_children_lessons()`, use `LLMS_Section->get_lessons( 'posts' )` instead\n\n##### Template Updates\n\n+ [course/syllabus.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/syllabus.php)\n+ [product/access-plan-button.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-button.php)\n+ [product/access-plan-description.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-description.php)\n+ [product/access-plan-feature.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-feature.php)\n+ [product/access-plan-pricing.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-pricing.php)\n+ [product/access-plan-restrictions.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-restrictions.php)\n+ [product/access-plan-title.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-title.php)\n+ [product/access-plan-trial.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/access-plan-trial.php)\n+ [product/free-enroll-form.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/free-enroll-form.php)\n\n\nv3.23.0 - 2018-08-27\n--------------------\n\n##### Access Plan & Pricing Table Template Improvements\n\n+ The pricing table template has been split into multiple templates which are now rendered via action hooks. No visual changes have been made but if you've customized the template using a template override you'll want to review the template changes before updating!\n+ New action hooks are available to modify the rendering of access plans in course / membership pricing tables.\n\n  + `llms_access_plan`: Main hook for outputting an entire access plan within the pricing table\n  + `llms_before_access_plan`: Called before main content of access plan. Outputs the \"Featured\" area of plans\n  + `llms_acces_plan_content`: Main access plan content. Outputs title, pricing info, restrictions, and description\n  + `llms_acces_plan_footer`: Called after main content. Outputs trial info and the checkout / enrollment button\n\n+ Added filters to the returns of many of the functions in the `LLMS_Acces_Plan` model.\n+ Minor improvements made to `LLMS_Access_Plan` model\n\n##### Updates and Enhancements\n\n+ Improved handling of empty blank / empty data when adding instructors to courses and memberships\n+ Added filters to the \"Sales Page Content\" type options & functions for courses and memberships to allow 3rd parties to define their own type of sales page functionality\n+ Added filters to the saving of access plan data\n+ Improved the HTML and added CSS classes to the access plan admin panel html view\n\n##### Bug Fixes\n\n+ Fixes issue causing the \"Preview Changes\" button on courses to lock the \"Update\" publishing button which prevents changes from being properly saved.gi\n+ Fixed issue causing PHP errors when viewing courses / memberships on the admin panel when an instructor user was deleted\n+ Fixed issue causing PHP notices when viewing course / membership post lists on the admin panel when an instructor user was deleted\n+ Fixed issue causing PHP warnings to be generated when viewing the user add / edit screen on the admin panel\n+ Fixed an issue which would cause access plans to never be available to users. *This bug didn't affect any existing installations except if you wrote custom code that called the `LLMS_Access_Plan::is_available_to_user()` method.*\n\n##### Template Updates\n\n+ [templates/admin/post-types/product-access-plan.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/product-access-plan.php)\n+ [templates/product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/pricing-table.php)\n\n\nv3.22.2 - 2018-08-13\n--------------------\n\n+ Fixed issue causing banners on general settings screen to cause a fatal error when api connection errors occurred\n+ Improved CSS on setup wizard\n\n\nv3.22.1 - 2018-08-06\n--------------------\n\n+ Fix issue causing themes to appear as requiring updates when using the LifterLMS Helper\n\n\nv3.22.0 - 2018-07-31\n--------------------\n\n+ Frontend notifications are no longer powered by AJAX requests. This change will significantly reduce the number of requests made but will remove the ability for students to receive asynchronous notifications. This means that notifications will only be displayed on page load as notification polling will no longer occur while a student is on a page (while reading the content a lesson, for example).\n+ Course and membership catalogs items in navigation menus will now have expected CSS classes to identify current item and current item parents\n+ The admin panel add-ons screen has been reworked to be powered by the lifterlms.com REST api\n+ Some visual changes have been made to the add-ons screen\n+ The colors on the voucher screen on the admin panel have been updated to match the rest of the interfaces in LifterLMS\n\n\nv3.21.1 - 2018-07-24\n--------------------\n\n+ Fixed issue causing visual issues on checkout summary when using coupons which apply discounts to a plan trial\n+ Fixed issue causing `.mo` files stored in the `languages/lifterlms` safe directory from being loaded before files stored in the default location `languages/plugins`\n+ Added methods to integration abstract to allow integration developers to automatically describe missing integration dependencies\n+ Tested to WordPress 4.9.8\n\n##### Template Updates\n\n+ [templates/checkout/form-summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-summary.php)\n\n\nv3.21.0 - 2018-07-18\n--------------------\n\n##### Updates and Enhancements\n\n+ Added new actions before and after global login form HTML: `llms_before_person_login_form` & `llms_after_person_login_form`\n+ Settings API can now create disabled fields\n+ Added new actions to the checkout form: `lifterlms_pre_checkout_form` && `lifterlms_post_checkout_form`\n+ Added CRUD functions for interacting with data located in the `wp_lifterlms_user_postmeta` table\n+ Replaced various database queries for CRUD user postmeta data with new CRUD functions\n+ Added new utility function to allow splicing data into associative arrays\n\n##### Bug Fixes\n\n+ If all user information fields are disabled, the \"Student Information\" are will now be hidden during checkout for logged in users instead of displaying an empty information box\n+ Fixed plugin compatibility issue with Advanced Custom Fields\n+ Fixed issue causing multiple choice quiz questions to require a double tap on some iOS devices\n+ Fixed incorrectly named filter causing section titles to not display on student course reporting screens\n+ We do not advocate using PHP 5.5 or lower but if you were using 5.5 or lower and encountered an error during bulk enrollment we've fixed that for. Please upgrade to 7.2 though. We all want faster more secure websites.\n\n##### Template Updates\n\n+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php)\n+ [templates/global/form-login.php](https://github.com/gocodebox/lifterlms/blob/master/templates/global/form-login.php)\n\n\nv3.20.0 - 2018-07-12\n--------------------\n\n+ Updated user interfaces on admin panel for courses and memberships with relation to \"Enrolled\" and \"Non-Enrolled\" student descriptions\n+ \"Enrolled Student Description\" is now the default WordPress editor\n+ \"Non-Enrolled Student Description\" is now the \"Sales Page\"\n+ Additional options for sales pages (the content displayed to visitors and non-enrolled students) have been added:\n  + Do nothing (show course description)\n  + Show custom content (use a WYSIWYG editor to define content)\n  + Redirect to a WordPress page (use custom templates and enhance page builder compatibility and capabilities)\n  + Redirect to a custom URL (use a sales page hosted on another domain!)\n+ Tested to WordPress 4.9.7\n\nv3.19.6 - 2018-07-06\n--------------------\n\n+ Fix file load paths in OptimizePress plugin compatibility function\n\n\nv3.19.5 - 2018-07-05\n--------------------\n\n+ Fixed bug causing `select2` multi-selects from functioning as multi-selects\n+ Fixed visual issue with `select2` elements being set without a width causing them to be both too small and too large in various scenarios.\n+ Fixed duplicate action on dashboard section template\n\n##### Template Updates\n\n+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php)\n\n\nv3.19.4 - 2018-07-02\n--------------------\n\n##### Updates and enhancements\n\n+ Bulk enroll multiple users into a course or membership from the Users table on your admin panel. See how at [https://lifterlms.com/docs/student-bulk-enrollment/](https://lifterlms.com/docs/student-bulk-enrollment/)\n+ Added event on builder to allow integrations to run trigger events when course elements are saved\n+ Added general redirect method `llms_redirect_and_exit()` which is a wrapper for `wp_redirect()` and `wp_safe_redirect()` which can be plugged (and tested via phpunit)\n+ Added new action called before validation occurs for a user account update form submission: `llms_before_user_account_update_submit`\n+ Removed placeholders from form fields. Fixes a UX issue causing registration forms to appear cluttered due to having both placeholders and labels.\n\n##### Bug fixes\n\n+ Fixed issue allowing nonce checks to be bypassed on login and registration forms\n+ Fixed issue causing a PHP notice if the registration form is submitted without an email address and automatic username generation is enabled\n+ Fixed issue preventing email addresses with the \"'\" character from being able to register, login, or update account information\n+ Fixed typo in automatic username generation filter `lifterlms_generated_username` (previously was `lifterlms_gnerated_username`)\n+ Fixed issue causing admin panel static assets to have a double slash (//) in the asset URI path\n+ Fixed issue allowing users with `view_lifterlms_reports` capability (Instructors) to access sales & enrollment reporting screens. The `view_others_lifterlms_reports` capability (Admins & LMS Managers) is now required to view these reporting tabs.\n+ Updated IDs of login and registration nonces to be unique. Fixes an issue causing Chrome to throw non-unique ID warnings in the developer console. Also, IDs are supposed to be unique _anyway_ but thanks for helping us out Google.\n\n\nv3.19.3 - 2018-06-14\n--------------------\n\n+ Fix issue causing new quizzes to be unable to load questions list without reloading the builder\n\n\nv3.19.2 - 2018-06-14\n--------------------\n\n##### Updates and enhancements\n\n+ The course builder will now load quiz question data when the quiz is opened instead of loading all quizzes on builder page load. Improves builder load times and addresses an issue which could cause timeouts in certain environments when attempting to edit very large courses.\n+ The currently viewed lesson will now be bold in the lesson outline widget.\n+ Added a CSS class `.llms-widget-syllabus .llms-lesson.current-lesson` which can be used to customize the display of the current lesson in the widget.\n+ Added the ability to filter quiz attempt reports by quiz status\n+ Updated language for access plans on with a limited number of payments to reflect the total number of payments due as opposed to the length (for example in years) that the plan will run.\n\n##### Bug fixes\n\n+ Fixed issue preventing oEmbed media from being used in quiz question descriptions\n+ Fixed issue preventing `<iframes>` from being used in quiz question descriptions\n+ Quiz results will now exclude questions with 0 points value when displaying the number of questions in the quiz.\n+ Fixed error occurring when sorting was applied to quiz attempt reports which would cause quiz attempts from other quizzes to be included in the new sorted report\n+ Fixed filter `lifterlms_reviews_section_title` which was unusable due to the incorrect usage of `_e()` within the filter. Now using `__()` as expected.\n+ Fixed issue causing course featured image to display in place of lesson feature images\n\n##### Template Updates\n\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/lesson-preview.php)\n+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/outline-list-small.php)\n+ [templates/quiz/results-attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt.php)\n\n\nv3.19.1 - 2018-06-07\n--------------------\n\n+ Fixed CSS specificity issue on admin panel causing white text on white background on system status pages\n\n\nv3.19.0 - 2018-06-07\n--------------------\n\n##### Updates and enhancements\n\n+ Added a \"My Memberships\" tab to the student dashboard\n+ \"My Memberships\" preview area\n+ Updated admin panel order status badges to match frontend order status badges\n+ Added a new recurring order status \"Pending Cancel.\" Orders in this state will allow students to access course / membership content until the next payment is due, on this date, instead of a recurring charge being made the order will move to \"Cancelled\" and the student's enrollment status will change to \"Cancelled\" removing their access to the course or membership.\n+ When a student cancels an active recurring order from the student dashboard, the order will move to \"Pending Cancellation\" instead of \"Cancelled\"\n+ Students can re-activate an order that's Pending Cancellation moving the expiration date to the next payment due date\n+ Added the ability to edit the access expiration date for orders with limited access settings and for orders in the \"pending-cancel\" state\n+ Added a filter to allow customization of the URL used to generate certificate downloads from\n+ When viewing taxonomy archives for any course or membership taxonomy (categories, tags, and tracks), if a term description exists, it will be used instead of the default catalog description content defined on the catalog page.\n+ Added a filter (`llms_archive_description`) to allow filtering of the archive description\n+ When `WP_DEBUG` is disabled the scheduled-actions posttype interface is now available via direct link. Useful for debugging but don't want to expose a menu-item link to clients. Access via wp-admin/edit.php?post_type=scheduled-action. Be warned: you shouldn't be modifying scheduled actions manually and that's why we're not exposing this directly, this should be used for debugging only!\n+ Updated the function used to check if lessons have featured images to improve performance and resolve an incompatibility issue with WP Overlays plugin.\n\n##### Bug fixes\n\n+ Fixed issue causing \"My Courses\" title to be duplicated on the student dashboard when viewing the endpoint\n+ Fixed issue causing the trial price to be displayed with a strike-through during a sale\n+ Fixed coupon issue causing coupons to expire at the beginning of the day on the expiration date instead of at the end of the day\n+ Fixed issue causing CSS rules to lose their declared order during exports causing export rendering issues with certain themes and plugin combinations\n\n##### Template Updates\n\n+ [templates/checkout/form-summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-summary.php)\n+ [templates/checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-switch-source.php)\n+ [templates/course/lesson-preview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/lesson-preview.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php)\n\n\nv3.18.2 - 2018-05-24\n--------------------\n\n+ Improved integrations settings screen to allow each integration to have it's own settings tab (page) with only its own settings\n+ Allow programmatic access to notification content when notification views are accessed via filters\n+ Fixed issue causing subscription cancellation notifications to be sent to admins when new orders were created\n+ Fixed warning message displayed prior to membership bulk enrollment\n+ Fixed multibyte character encoding issue encountered during certificate exports\n\n\nv3.18.1 - 2018-05-18\n--------------------\n\n+ Attached `llms_privacy_policy_form_field()` and `llms_agree_to_terms_form_field()` to an action hook `llms_registration_privacy`\n+ Define minimum WordPress version requirement as 4.8.\n\n##### Template Updates\n\n+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php)\n+ [templates/global/form-registration.php](https://github.com/gocodebox/lifterlms/blob/master/templates/global/form-registration.php)\n\n\nv3.18.0 - 2018-05-16\n--------------------\n\n##### Privacy & GDPR Compliance Tools\n\n+ Added privacy policy notice on checkout, enrollment, and registration that integrates with the WP Core 4.9.6 Privacy Policy Page setting\n+ Added settings to allow customization of the privacy policy and terms & conditions notices during checkout, enrollment, and registration\n+ Added suggested Privacy Policy language outlining information gathered by a default LifterLMS site\n\n+ During a WordPress Personal Data Export request the following LifterLMS information will be added to the export\n\n  + All personal information gathered from registration, checkout, and enrollment forms\n  + Course and membership enrollments, progress, and grades\n  + Earned achievements and certificates\n  + All order data\n\n+ During a WordPress Personal Data Erasure request the following LifterLMS information will be erased\n\n  + All personal information gathered from registration, checkout, and enrollment forms\n  + Earned achievements and certificates\n  + All notifications for or about the user\n  + If the \"Remove Order Data\" setting is enabled, the order will be anonymized by removing student personal information from the order and, if the order is a recurring order, it will be cancelled.\n  + If the \"Remove Student LMS Data\" setting is enabled, all student data related to course and membership activity will be removed\n\n+ All of the above relies on features available in WordPress core 4.9.6\n\n##### Updates and Enhancements\n\n+ Tested up to WordPress 4.9.6\n+ Improved pricing table UX for members-only access plans. An access plan button for a plan belonging to only one membership will click directly to the membership as opposed to opening a popover. Plan's with access via multiple memberships will continue to open a popover listing all availability options.\n+ Added a \"My Certificates\" tab to the Student Dashboard\n+ Certificates can be downloaded as HTML files (available when viewing a certificate or from the certificate reporting screen on the admin panel)\n+ Admins can now delete certificates and achievements from reporting screens on the admin panel\n+ Added additional information to certificate and achievement reporting tables\n+ Expanded widths of admin settings page setting names to be a bit wider and more readable\n+ Now conditionally hiding some settings when they are no longer relevant\n+ Added daily cron automatically remove files from the `LLMS_TMP_DIR` which are more that 24 hours old\n+ Removed unused template `content-llms_membership.php`\n+ Added initialization actions for use by integration classes\n\n##### Bug Fixes\n\n+ Fixed issue causing coupon reports to always display \"1\" regardless of actual number of coupons used\n+ Fixed issue causing new posts created via the Course Builder to always be created for user_id #1\n+ Fixed issue causing \"My Achievements\" to display twice on the My Achievements student dashboard tab\n+ Fixed issue preventing lessons from being completed when a quiz in draft mode was attached to the lesson\n+ Fixed issue causing minified RTL stylesheets to 404\n\n##### Template Updates\n\n+ [templates/admin/post-types/order-details.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/order-details.php)\n+ [templates/checkout/form-checkout.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-checkout.php)\n+ [templates/content-certificate.php](https://github.com/gocodebox/lifterlms/blob/master/templates/content-certificate.php)\n+ [templates/global/form-registration.php](https://github.com/gocodebox/lifterlms/blob/master/templates/global/form-registration.php)\n+ [templates/myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php)\n\n\nv3.17.8 - 2018-05-04\n--------------------\n\n##### Updates and Enhancements\n\n+ Added admin email notification when student cancels a subscription\n+ Quiz results will now display the question's description when reviewing results as a student and on the admin panel during grading\n+ Add action hook fired when a student cancels a subscription (`llms_subscription_cancelled_by_student`)\n+ Reduce unnecessary DB queries for integrations by checking for dependencies and then calling querying the options table to see if the integration has been enabled.\n+ Updated the notifications settings table to be more friendly to the human eye\n\n##### Bug Fixes\n\n+ Fix admin scripts enqueue order. Fixes issue preventing manual student enrollment selection from functioning properly in certain scenarios.\n+ Shift + Enter when in a question choice field now adds a return as expected instead of exiting the field\n+ When pasting into question choice fields HTML from RTF documents will be automatically stripped\n+ Ensure certificates print with a white background regardless of theme CSS\n+ Fix issue causing themes with `overflow:hidden` on divs from cutting certificate background images\n+ Upon export completion unlock tables regardless of mail success / failure\n+ Resolve issue causing incorrect number of access plans to be returned on systems that have custom defaults set for `WP_Query` `post_per_page` parameter\n+ Fix error occurring when all 3rd party integrations are disabled by filter, credit to [@Mte90](https://github.com/Mte90)!\n+ Ensure `LLMS()->integrations()->integrations()` returns all integrations regardless of availability.\n+ Updated `LLMS_Abstract_Options_Data` to have an option set method\n\n##### Template Updates\n\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt-questions-list.php)\n\n\nv3.17.7 - 2018-04-27\n--------------------\n\n+ Fix issue preventing assignments passing grade requirement from saving properly\n+ Fix issue preventing builder toggle switches from properly saving some switch field data\n+ Fix with \"Launch Builder\" button causing it to extend outside the bounds of its container\n+ Fix issue with builder radio select fields during view rerenders\n+ Course Outline shortcode (and widget) now retrieve parent course of the current page more consistently with other shortcodes\n+ Added ability to filter which custom post types which can be children of a course (allows course shortcodes & widgets to be used in assignment sidebars of custom content areas)\n\n\nv3.17.6 - 2018-04-26\n--------------------\n\n+ Updated language on recurring orders with no expiration settings. Orders no longer say \"Lifetime Access\" and instead output no expiration information\n+ Quiz editor on builder updated to be consistent visually and functionally to the lesson settings editor\n+ Improved the builder field API to allow for radio element fields\n+ Fix issue causing JS error on admin settings pages\n+ Updated CSS for Certificates to be more generally compatible with theme styles when printed\n+ Allow system print settings to control print layout for certificates by removing explicit landscape declarations\n+ Now passing additional data to filters used to create custom columns on reporting screens\n+ Remove unused JS files & Chosen JS library\n+ Added filter to allow opting into alternate student dashboard order layout. Use `add_filter( 'llms_sd_stacked_order_layout', '__return_true' )` to stack the payment update sidebar below the main order information. This is disabled by default.\n+ Achievement and Certificate basic notifications now auto-dismiss after 10 seconds like all other basic notifications\n+ Deprecated Filter `llms_get_quiz_theme_settings` and added backwards compatible methods to transition themes using this filter to the new custom field api. For more information see new methods at https://lifterlms.com/docs/course-builder-custom-fields-for-developers/\n+ Increased default z-index on notifications to prevent notifications from being hidden behind floating / static navigation menus\n\n\n##### Template Updates\n\n+ [templates/myaccount/my-orders.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-orders.php)\n+ [templates/myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php)\n\n\nv3.17.5 - 2018-04-23\n--------------------\n\n##### Admin Settings Interface Improvements\n\n+ Improved admin settings page interface to allow for section navigation\n+ Updated checkout setting pages to utilize a separate section (page) for each available payment gateway\n+ Added a table of payment gateways to see at a glance which gateways are enabled and allows drag and drop reordering of gateway display order\n+ Moved dashboard endpoints to a separate section on the accounts settings area\n+ Updated CSS on settings page to have more regular spacing between subtitles and settings fields\n+ Added a \"View\" button next to any admin setting post/page selection field to allow quick viewing of the selected post\n+ Purchase page setting field is now ajax powered like all other page selection settings\n+ Renamed dashboard settings section titles to be more consistent with language in other areas of LifterLMS\n+ All dashboard endpoints now automatically sanitized to be URL safe\n\n##### Updates and Enhancements\n\n+ Dashboard endpoints can now be deregistered by setting the endpoint slug to be blank on account settings\n\n##### Bug Fixes\n\n+ Fix issue causing 404s for various script files when SCRIPT_DEBUG is enabled\n+ Fix issue with audio & video embeds to prevent fallback to default post attachments\n+ Fix issue causing student selection boxes to malfunction due to missing dependencies when loaded over slow connections\n\n##### Template Updates\n\n+ [templates/myaccount/navigation.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/navigation.php)\n\n\nv3.17.4 - 2018-04-17\n--------------------\n\n+ Added core RTL language support\n+ Fixed fatal error on student management tables resulting from deleted admin users who manually enrolled students\n+ Added filter to allow 3rd parties to disable achievement dupchecking (`llms_achievement_has_user_earned`)\n+ Added {student_id} merge code which can be utilized on certificates\n+ Added merge code insert button to certificates editor\n+ Added filter to allow 3rd parties to disable certificate dupchecking (`llms_certificate_has_user_earned`)\n+ Added filter to allow 3rd parties to add custom merge codes to certificates (`llms_certificate_merge_codes`)\n+ Fix restriction check issue for lessons with drip or prerequisites on course outline widget / shortcode\n+ Bumped WP tested to version to 4.9.5\n\n##### Template Updates\n\n+ [templates/course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php)\n+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/outline-list-small.php)\n\n\nv3.17.3 - 2018-04-11\n--------------------\n\n+ Course and Membership instructor metabox search field now correctly states \"Select an Instructor\" instead of previous \"Select a Student\"\n+ Added missing translation for \"Select a Student\" on admin panel student selection search fields\n+ Fix issue causing reporting export CSVs to throw a SYLK interpretation error when opened in Excel\n+ Fix issue causing drafted courses and memberships to be published when the \"Update\" button is clicked to save changes\n+ Remove use of PHP 7.2 deprecated `create_function`\n+ Fix errors resulting from quiz questions which have been deleted\n+ Fix issue causing current date / time to display as the End Date for incomplete quiz attempts on quiz reporting screens\n\n##### Template Updates\n\n+ [templates/admin/reporting/tabs/quizzes/attempt.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/quizzes/attempt.php)\n+ [templates/quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt-questions-list.php)\n\n\nv3.17.2 - 2018-04-09\n--------------------\n\n+ Fixed issue preventing lesson video and audio embeds from being *removed* when using the course builder settings editor\n+ Fixed issue causing question images to lose the image source\n+ Updated student management table for courses and memberships to show the name (and a link to the user profile) of the site user who manually enrolled the student.\n+ Add \"All Time\" reporting to various reporting filters\n+ Added API for builder fields to enable multiple select fields\n+ Fix memory leak related to assignments rendering on course builder\n+ Fix issue causing course progress and enrollment checks to incorrectly display progress data cached for other users\n+ Lesson progression actions (Mark Complete & Take Quiz buttons) will now always display to users with edit capabilities regardless of enrollment status\n\n##### Template Updates\n\n+ [templates/course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php)\n+ [templates/course/outline-list-small.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/outline-list-small.php)\n\n\nv3.17.1 - 2018-03-30\n--------------------\n\n+ Refactored lesson completion methods to allow 3rd party customization of lesson completion behavior via filters and hooks.\n+ Remove duplicate lesson completion notice implemented. Only popover notifications will display now instead of popovers and inline messages.\n+ Object completion will now automatically prevent multiple records of completion from being recorded for a single object.\n+ Lesson Mark Complete button and lessons completed by quiz now utilizes a generic trigger to mark lessons as complete: `llms_trigger_lesson_completion`.\n+ Removed several unused functions from frontend forms class\n+ Moved lesson completion form controllers to their own class\n\n##### Templates updates\n\n+ [templates/course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php)\n\n\nv3.17.0 - 2018-03-27\n--------------------\n\n##### Builder Updates\n\n+ Moved action buttons for each lesson (for opening quiz and lesson editor) to be static below the lesson title as opposed to only being visible on hover\n+ Added new audio and video status indicator icons for each lesson\n+ Various status indicator icons will now have different icons in addition to different colors depending on their state\n+ Replaced \"pencil\" icons that open the WordPress post editor with a small \"WP\" icon\n+ Added several actions and filters to backend functions so that 3rd parties can hook into builder saves\n+ Added lesson settings editing to the builder. Lesson settings can now be updated from settings metaboxes on the lesson post edit screen AND on the builder.\n+ Added prerequisite validation for lessons to prevent accidental impossible prerequisite creating (eg: Lesson 5 can never be a prerequisite for Lesson 4)\n+ Added functions and filters to allow 3rd parties to add custom fields to the builder. For more details see [an example](https://lifterlms.com/docs/course-builder-custom-fields-for-developers/).\n+ Fixed issue causing changes made in \"Text\" mode on content editors wouldn't trigger save events\n+ Fixed issue causing lesson prerequisites to not properly display on the course builder\n+ Fixed CSS z-index issues related to builder field tooltip displays\n+ Removed unused Javascript dependencies\n\n##### Bug Fixes\n\n+ Fixed typo on filter on quiz question image getter function\n\n##### Updates\n\n+ Performance improvements made to database queries and functions related to student enrollment status and student course progress queries. Thanks to [@mte90](https://github.com/Mte90) for raising issues and testing solutions related to these updates and changes!\n+ Added PHP Requires plugin header (5.6 minimum)\n+ Added HTTP User Agent data to the system report\n+ [LifterLMS Assignments Beta](https://lifterlms.com/product/lifterlms-assignments?utm_source=LifterLMS%20Plugin&utm_medium=CHANGELOG&utm_campaign=assignments%20preorder) is imminent and this release adds functionality to the Builder which will be extended by Assignments upon when availability\n\n\nv3.16.16 - 2018-03-19\n---------------------\n\n+ Fixed builder issue causing multiple question choices to be incorrectly selected\n+ Fixed builder issue with media library uploads causing an error message to prevent new uploads before the quiz or question has been persisted to the database\n+ Fixed builder issue preventing quizzes from being deleted before they were persisted to the database\n+ Fixed builder issue causing autosaves to interrupt typing and reset lesson and section titles\n+ Fixed JS console error related to LifterLMS JS dependency checks\n\n\nv3.16.15 - 2018-03-13\n---------------------\n\n##### Quiz Results Improvements and fixes\n\n+ Improved quiz result user and correct answer handling functions for more consistent HTML output\n+ Result answers (correct and user) will display as lists\n+ image question types will display without bullets and will \"float\" next to each other\n+ Fixed issue causing quiz results with multiple answers from outputting all HTMLS with no spaces between them\n\n##### Quiz Grading\n\n+ Fixed issue causing advanced reorder and reorder question types from being graded incorrectly in some scenarios\n+ Advanced fill in the blank questions are now case insensitive. Case sensitivity can be enabled with a filter: `add_filter( 'llms_quiz_grading_case_sensitive', '__return_true' )`\n\n##### Fixes\n\n+ Updated spacing and returns found in the email header and footer templates to prevent line breaks from occurring in undesirable places on previews of HTML emails in mobile email clients\n+ Added options for themes to add layout support to quizzes where the custom field utilizes an underscore at the beginning of the field key\n+ Fixed CSS issue causing blanks of fill in the blanks to not be visible on the course builder when using Chrome on Windows\n+ Removed unnecessary `get_option()` call to unused option `lifterlms_permalinks`\n+ Updated permissions required to see various LifterLMS post types to rely on `manage_lifterlms` capabilities as opposed to `manage_options`\n  + This will only affect the LMS Manager core role or any custom role which was provided with the `manage_options` capability. Manages will now be able to access all LMS content and custom roles would now not be able to access LMS content\n  + Affected content types are: Orders, Coupons, Vouchers, Engagements, Achievements, Certificates, and Emails\n+ Several references to an option removed in LifterLMS 3.0 still existed in the codebase and have now been removed.\n  + Option `lifterlms_course_display_banner` is no longer called or referenced\n  + Template function `lifterlms_template_single_featured_image()` has been removed\n  + Actions referencing `lifterlms_template_single_featured_image()` have been removed\n  + Template function `lifterlms_get_featured_image_banner()` has been removed\n  + Template `templates/course/featured-image.php` has been removed\n\n##### Templates updates\n\n+ [quiz/results-attempt-questions-list.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results-attempt-questions-list.php)\n\n\nv3.16.14 - 2018-03-07\n---------------------\n\n+ Courses reporting table now includes courses with the \"Private\" status\n+ Fixed issue causing some achievement notifications to be blank\n+ Added tooltips to question choice add / delete icon buttons\n+ Quiz results meta information elements now have unique CSS classes\n+ Removed reliance PHP 7.2 deprecated function `create_function()`\n+ Fixed invalid PHP 7.2 syntax creating a warning found on the setup wizard\n+ Fixed undefined index error related to admin notices\n+ Fixed untranslatable string on Users table (\"No Memberships\")\n+ Fixed discrepancy between membership restrictions as presented to logged out users and logged in users who cannot access membership\n+ Fixed FireFox and Edge issue causing changes to number inputs made via HTML5 input arrows from properly triggering save events\n\n\nv3.16.13 - 2018-02-28\n---------------------\n\n+ Hotfix: Only create quizzes on the builder if quizzes exist on the lesson\n\n\nv3.16.12 - 2018-02-27\n---------------------\n\n+ Quizzes can now be detached (removed from a lesson) or deleted (deleted from the lesson and the database) via the Course Builder\n+ Improved question choice randomization to ensure randomized choices never display in their original order.\n+ When a lesson is deleted, any quiz attached to the lesson will become an orphan\n+ When a lesson is deleted, any lesson with this lesson as a prerequisite will have it's prerequisite data removed\n+ When a quiz is deleted, all questions attached to the quiz will also be deleted\n+ When a quiz is deleted, the lesson associated with the quiz will have those associations removed\n+ Fixed grammar issue on restricted lesson tooltips when no custom message is stored on the course.\n+ Updated functions causing issues in PHP 5.4 to work on PHP 5.4. This has been done to reduce frustration for users still using PHP 5.4 and lower; [This does not mean we advocate using software past the end of its life or that we support PHP 5.4 and lower](https://lifterlms.com/docs/minimum-system-requirements-lifterlms/).\n\n\nv3.16.11 - 2018-02-22\n---------------------\n\n+ Course import/exports and lesson duplication now carry custom meta data from 3rd party plugins and themes\n+ Added course completion date column to Course reporting students list\n+ Restriction checks made against a quiz will now properly cascade to the quiz's parent lesson\n+ Fixed issue preventing featured images from being exported with courses and lessons\n+ Fixed duplicate lesson issue causing quizzes to be double assigned to the old and new lesson\n+ Fixed issue allowing blog archive to be viewed by non-members when sitewide membership is enabled\n+ Fixed builder issue causing data to be lost during autosaves if data was edited during an autosave\n+ Fixed builder issue preventing lessons from moving between sections when clicking the \"Prev\" and \"Next\" section buttons\n+ Added actions to `LLMS_Generator` to allow 3rd parties to extend core generator functionality\n\n\nv3.16.10 - 2018-02-19\n---------------------\n\n+ Content added to the editor of course & membership catalog pages will now be output *above* the catalog loop\n+ Fix issue preventing iframes and some shortcodes from working when added to a Quiz question description\n+ Added new columns to the Quizzes reporting table to display Course and Lesson relationships\n+ Improved the task handler of background updater to ensure upgrade functions that need to run multiple times can do so\n+ Fixed JS Backup confirmation dialog on the background updater.\n+ Add support for 32-bit systems in the `LLMS_Hasher` class\n+ Fix issue causing HTML template content to be added to lessons when duplicating an existing lesson within the course builder\n\n##### 3.16.0 migration improvements\n\n+ Accommodates questions imported by 3rd party Excel to LifterLMS Quiz plugin. Fixes an issue where choices would have no correct answer designated after migration.\n+ All migration functions now run on a loop. This improves progress reporting of the migration and prevents timeouts on mature databases with lots of quizzes, questions, and/or attempts.\n+ Fix an issue that caused duplicate quizzes or questions to be created when the \"Taking too long?\" link was clicked\n\n\nv3.16.9 - 2018-02-15\n--------------------\n\n+ Fix issue causing error on student dashboard when reviewing an order with an access plan that was deleted.\n+ Fixed spelling error on course metabox\n+ Fixed spelling error on frontend quiz interface\n+ Fixed issues with 0 point questions:\n  + Will no longer prevent quizzes from being automatically graded when a 0 point question is in an otherwise automatically gradable quiz\n  + Point value not editable during review\n  + Visual display on results displays with grey background not as an orange \"pending\" question\n+ Table schema uses default database charset. Fixes an issue with databases that don't support `utf8mb4` charsets.\n+ Updated `LLMS_Hasher` class for better compatibility with older versions of PHP\n\n\nv3.16.8 - 2018-02-13\n--------------------\n\n##### Updates\n\n+ Added theme compatibility API so theme developers can add layout options to the quiz settings on the course builder. For details on adding theme compatibility see: [https://lifterlms.com/docs/quiz-theme-compatibility-developers/](https://lifterlms.com/docs/quiz-theme-compatibility-developers/).\n+ Quiz results \"donut\" chart had alternate styles for quizzes pending review (Dark grey text rather than red). You can target with the `.llms-donut.pending` CSS class to customize appearance.\n+ Allow filtering when retrieving student answer for a quiz attempt question via `llms_quiz_attempt_question_get_answer` filter\n\n##### Bug Fixes\n\n+ Fix issues causing conditionally gradable question types (fill in the blank and scale) from displaying without a status icon or possible points when awaiting admin review / grading.\n+ Fix issue preventing conditionally gradable question types (fill in the blank and scale) from being reviewable on the admin panel when the question is configured as requiring manual grading.\n+ Fix analytics widget undefined index warning during admin-ajax calls. Thanks [@Mte90](https://github.com/Mte90)!\n+ Fix issue causing `is_search()` to be called incorrectly. Thanks [@Mte90](https://github.com/Mte90)!\n+ Fix issue preventing text / html formatting from saving properly for access plan description fields\n+ Fix html character encoding issue on reporting widgets causing currency symbols to display as a character code instead of the symbol glyph.\n\n##### Templates changed\n\n+ templates/quiz/results-attempt-questions-list.php\n+ templates/quiz/results-attempt.php\n\n\nv3.16.7 - 2018-02-08\n--------------------\n\n+ Added manual saving methods for the course builder that passes data via standard ajax calls. Allows users (hosts) to disable the Heartbeat API but still save builder data.\n+ Added an \"Exit\" button to the builder sidebar to allow exiting the builder back to the WP Edit Post screen for the current course\n+ Added dashboard links to the WP Admin Bar to allow existing the course builder to various areas of the dashboard\n+ Added data attribute to progress bars so JS (or CSS) can read the progress of a progress bar. Thanks [@dineshchouhan](https://github.com/dineshchouhan)!\n+ Fixed issue causing newly created lessons to lose their assigned quiz\n+ Fixed php `max_input_vars` issue causing a 400 Bad Request error when trying to save large courses in the course builder\n+ Removed reliance on PHP bcmath functions\n\n\nv3.16.6 - 2018-02-07\n--------------------\n\n+ Removed reliance on PHP Hashids Library in favor of a simpler solution with no PHP module dependencies\n+ Added interfaces to allow customization of quiz url / slug\n+ Fixed [audio] shortcodes added to quiz question descriptions\n+ Fixed untranslatable strings on frontend of quizzes\n+ Fix issue causing certificate notifications to display as empty\n+ Fix issue preventing quiz pass/fail notifications from triggering properly for manually graded quizzes\n+ Fix undefined index warning on quiz pass/fail notifications\n\n\nv3.16.5 - 2018-02-06\n--------------------\n\n+ Fix issue preventing manually graded quiz review points from saving properly\n+ Improved background updater to ensure scripts don't timeout during upgrades\n+ Admin builder JS now minified for increased performance\n+ Made frontend quiz and quiz-builder strings output via Javascript translatable\n\n\nv3.16.4 - 2018-02-05\n--------------------\n\n+ Fix issue causing newly created quizzes to not be properly related to their parent lesson\n+ Fix issue preventing quiz time limits from starting unless an attempt limit is also set\n+ Fixes a WP Engine issue that prevented the builder from loading due to a blocked dependency\n\n\nv3.16.3 - 2018-02-02\n--------------------\n\n+ When switching a quiz to \"Published\" it will now update the parent lesson to ensure it's recorded as having an enabled quiz.\n+ Declared the WordPress heartbeat API script as a dependency for the Course Builder JS. It seems that some servers and hosts dequeue the heartbeat when not explicitly required. This resolves a saving issue on those hosts.\n+ Added a Quiz Description content editor under quiz settings. This is the \"Editor\" from pre 3.16.0 quizzes and any content saved in these fields is now available in this description field\n+ Fixed issue causing points percentage calculation tooltip on quiz builder to show the incorrect percentage value\n+ Fix issue preventing lessons with no drip settings from being updated on the WP post editor\n+ Fix issue causing 500 error on lesson settings metabox for lessons not attached to sections\n+ Add a \"Quiz Description\" field to allow quiz post content to be edited on the quiz builder\n+ Added a database migration script to ensure quizzes migrated from 3.16 and lower that had quiz post content to automatically have the optional quiz description to be enabled\n\n\nv3.16.2 - 2018-02-02\n--------------------\n\n+ Add an update notice to 3.16.0 migration scripts to provide more information about the major update.\n+ Removed quiz assignment fields on the lesson metabox to reduce confusion as quizzes are now managed exclusively on the quiz builder.\n+ Ensure questions migrated during 3.16 updates retain their initial points value from the quiz.\n\n\nv3.16.1 - 2018-02-01\n--------------------\n\n+ Ensure quizzes in draft mode are only accessible by those with edit access (instructors, admins, etc...)\n+ Restore pre 3.16 actions and filters related to quiz start buttons\n+ Remove legacy error message for quiz accessibility issues by site admins\n+ Students who cannot access a quiz are redirected to the parent lesson if they attempt to access a quiz directly\n+ Fix undefined index warning on wp-login.php related to LifterLMS js assets. Thanks [Mte90](https://github.com/Mte90)!\n+ Update checkout error message to provide user with direction when they already have access to a course. Thanks [@andreasblumberg](https://github.com/andreasblumberg)!\n\n\nv3.16.0 - 2018-02-01\n--------------------\n\n##### Quizzes\n\n+ New question types: True/False, Picture Choice, and Non-question content\n+ Picture & Multiple choice have options for multiple correct answers (checkbox-like questions)\n+ You can now create questions with NO POINTS (maybe for surveys?)\n+ Upgraded student quiz review interface\n+ Upgraded instructor quiz attempt review interface\n+ Admins may now leave remarks on questions directly\n+ Improved data available related to quizzes and quiz attempts on reporting screens\n+ Improved quiz user interface\n+ Added a progress bar to the quiz interface\n+ Shrunk the quiz timer\n+ Added a question # counter on the quiz interface\n+ Fixed issue causing randomized questions to get \"lost\" when navigating back through a quiz attempt\n+ Improved error handling on quizzes\n+ Overhauled quiz data structure for improved performance and scalability\n+ Requires database migration and update: [3.16.0](https://lifterlms.com/docs/lifterlms-database-updates/#3160)\n\n##### Course Builder Improvements\n\n+ Quiz-building is now available on the course builder\n+ Quiz and Question WordPress editors no longer available. Quizzes and Questions HAVE NOT DISAPPEARED, they've been improved and relocated\n+ All hooks & filters attached to `the_content` and `the_title` are now being removed when loading the course builder. This should prevent infinite spinners on builder loading and builder AJAX calls due to third-parties accidentally outputting html during these events.\n\n##### Updates\n\n+ Added space between arrows and \"Next\" and \"Previous\" text on pagination lists. Thanks [sujaypawar](https://github.com/sujaypawar)!\n+ Updated Quiz post type slug from \"llms_quiz\" to \"quiz\".\n+ Updated default return of `llms_get_post()` to be `false` rather than a `WP_Post` object when a LifterLMS post cannot be located\n\n##### Bug Fixes\n\n+ Fixed a potential database read error related to database store abstract\n+ Now passing Post ID as second parameter to the `the_title` filter called on post model getters\n\n\n##### Removed templates\n\nThe following quiz templates have been removed. Customization of these templates causes quiz application functionality to break and they should not have been available for customization but were due to oversights. This has been corrected.\n\n+ templates/content-single-question-after.php\n+ templates/content-single-question-before.php\n+ templates/quiz/next-question.php\n+ templates/quiz/previous-question.php\n+ templates/quiz/question-count.php\n+ templates/quiz/quiz-question.php\n+ templates/quiz/single-choice.php\n+ templates/quiz/single-choice_ajax.php\n+ templates/quiz/summary.php\n+ templates/quiz/timer.php\n+ templates/quiz/wrapper-end.php\n+ templates/quiz/wrapper-start.php\n\n##### Removed Functions\n\nVarious template functions related to quizzes were removed due to the deprecation of their related templates\n\n+ `lifterlms_template_quiz_timer()`\n+ `lifterlms_template_single_next_question()`\n+ `lifterlms_template_single_prev_question()`\n+ `lifterlms_template_single_single_choice()`\n+ `lifterlms_template_single_single_choice_ajax()`\n+ `lifterlms_template_single_question_count()`\n\n\nv3.15.1 - 2017-12-05\n--------------------\n\n+ Ensure course & membership titles with HTML characters are decoded during reporting exports\n+ Fix issue causing some courses to display in membership columns on reporting exports\n\n\nv3.15.0 - 2017-12-04\n--------------------\n\n##### Reporting Updates (and CSV exports!)\n\n+ Added course-level reporting table (see \"Courses\" tab of Reporting screen)\n+ Updated the interface on reporting screen when reviewing a single student\n+ Added reporting exports: students list, courses list, and list of students per course\n\n##### Bug fixes\n\n+ Fix error when `[lifterlms_course_continue_button]` shortcode is displayed to logged out or students not enrolled in the chosen course\n\n##### Minor updates\n\n+ Tested up to WordPress 4.9.1\n+ Added background data processors to ensure reporting data stays up to date in close to real time\n+ Add nocache constants and headers on student dashboard & checkout page to increase compatibility with caching plugins\n+ Added filter to student dashboard courses query\n\n\nv3.14.9 - 2017-11-27\n--------------------\n\n+ Tested up to WordPress 4.9\n+ Fix error during uninstall related to missing file\n+ Fix issue with rewinding quiz using \"Previous Question\" button\n+ On final question of a quiz the \"Next Lesson\" button now says \"Complete Quiz\"\n+ When completing a quiz, the loading message will now say \"Grading Quiz\" the entire time instead of \"Loading Question\" and then \"Grading Quiz\"\n+ Fix issue causing the `<title>` element on course builder pages from being partially empty\n\n\nv3.14.8 - 2017-11-06\n--------------------\n\n+ Lessons can be cloned via the \"Clone\" action from the lessons post table\n\n##### Builder Improvements & Fixes\n\n+ Add \"Existing Lesson\" functionality can now clone and attach the clone (when adding a lesson currently attached to a course) OR attach orphans\n+ Lessons created via Course builder will have their slugs renamed the first time the lesson title is updated via the builder\n+ No longer display notices on the course builder\n+ Add extra space to the scrollable area on course builder\n+ Removed logging and debugging functions from admin builder class\n+ JS-generated error messages on the course builder are now translatable\n\n##### Bug Fixes\n\n+ Fix: Show all memberships on dashboard\n\n\nv3.14.7 - 2017-10-25\n--------------------\n\n##### Navigation Menu Items\n\n+ Add LifterLMS endpoints to your nav menu\n+ Add Sign In and Sign Out links which display conditionally based on whether or not the visitor is logged in\n+ Checkout the docs at [https://lifterlms.com/docs/lifterlms-navigation-menu-items/](https://lifterlms.com/docs/lifterlms-navigation-menu-items/)\n\n##### Bug Fixes\n\n+ Fix SQL query issue with orphaned lesson query on course builder\n+ Fix undefined index warning occurring during theme switches\n+ Fix issue causing duplicate error messages to display on certain servers\n\n\nv3.14.6 - 2017-10-21\n--------------------\n\n+ Fix: `<iframes>` are no longer stripped when exporting or duplicating courses (this applies to lessons within the courses as well)\n+ Fix: Achievements on student dashboard now output the correct achievement title\n+ Fix: Courses on student dashboard ordered by Order attributes will obey settings correctly\n\n\nv3.14.5 - 2017-10-14\n--------------------\n\n+ Course builder will persist open/collapsed state of sections when they are re-ordered\n+ Course builder lessons in a section are draggable after reordering a section\n\n\nv3.14.4 - 2017-10-13\n--------------------\n\n+ You were right and we were wrong & we are sorry. This update returns the ability to add existing lessons to a course via the course builder.\n+ Lessons added to a section will no longer visually disappear when editing a section title on the course builder\n+ BuddyPress integration BP template fixes\n\n\nv3.14.3 - 2017-10-12\n--------------------\n\n+ Fix [lifterlms_my_account] shortcode issue affecting Divi theme users\n\n\nv3.14.2 - 2017-10-11\n--------------------\n\n+ Instructor query utilizes correct `$wpdb->prefix` for filtering by role instead of `wp_` which will not work when the `$table_prefix` in wp-config.php is customized\n+ include the admin notices class when running database update functions\n\n\nv3.14.1 - 2017-10-10\n--------------------\n\n+ Fix `[lifterlms_my_achievements]` shortcode\n+ Fix reference to deprecated core function related to checking the permissions of content restricted to a membership\n+ Builder titles will be saved on all field focusout/blur events, not just tab & enter key presses\n+ LifterLMS custom meta save metaboxes will not trigger actions during ajax requests\n+ Fix issue displaying certificates on admin panel reporting screens\n\n\nv3.14.0 - 2017-10-10\n--------------------\n\n+ Updated JS for 3.13 course builder to address issues on PHP 5.6 servers with asp_tags enabled\n+ Normalized date returns with various dates related to enrollments, achievements, and certificates. These dates now utilize the WP Core `date_format` option.\n+ Fixed strict comparison issue related to database query abstract (affected checks for last page & first page on admin reporting screens)\n+ Added a new capability `llms_instructor` for admins, lms managers, instructors, and instructor's assistant to easily differentiate \"instructors\" from \"students\"\n+ Fix `$wpdb->prepare` issue related to notification queries. Fixes WP 4.9-beta issue.\n\n##### Student Dashboard Updates\n\n+ Achievements on student dashboard now viewable in popover modal.\n+ Achievements tab added to student dashboard\n+ Courses, Memberships, Achievements, and Certificates have been updated to have a unified style\n+ Courses & Memberships extend the default catalog tiles\n+ Courses shortcode has new parameters useful for displaying a list of a specific users courses only. [More info](https://lifterlms.com/docs/shortcodes/#lifterlms_courses)\n\n##### Deprecated functions\n\n+ `LLMS_Student_Dashboard::output_courses_content()` replaced with `lifterlms_template_student_dashboard_my_courses( false )`\n+ `LLMS_Student_Dashboard::output_dashboard_content` replaced with `lifterlms_template_student_dashboard_home()`\n\n##### Template Updates\n\n+ [achievements/loop.php](https://github.com/gocodebox/lifterlms/blob/master/templates/achievements/loop.php)\n+ [achievements/template.php](https://github.com/gocodebox/lifterlms/blob/master/templates/achievements/template.php)\n+ [certificates/loop.php](https://github.com/gocodebox/lifterlms/blob/master/templates/certificates/loop.php)\n+ [certificates/preview.php](https://github.com/gocodebox/lifterlms/blob/master/templates/certificates/preview.php)\n+ [loop.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop.php)\n+ [loop/content.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/content.php)\n+ [loop/enroll-date.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/enroll-date.php)\n+ [loop/enroll-status.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/enroll-status.php)\n+ [loop/pagination.php](https://github.com/gocodebox/lifterlms/blob/master/templates/loop/pagination.php)\n+ [myaccount/dashboard-section.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard-section.php)\n+ [myaccount/dashboard.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/dashboard.php)\n+ [myaccount/header.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/header.php)\n\n##### Deleted Templates\n\n+ /myaccount/my-achievements.php\n+ /myaccount/my-courses.php\n+ /myaccount/my-memberships.php\n\n\nv3.13.1 - 2017-10-04\n--------------------\n\n+ Fix caching issue preventing quiz pass & fail engagements from triggering.\n+ Fix issue causing the \"Builder\" link to display on the lesson post table screen.\n+ Fix issue preventing new courses & memberships from being moved from draft -> published.\n+ Fix `wpdb->prepare()` empty placeholder issue related to engagement queries. Fixes warning added in WP 4.9.\n+ Add better version numbering to static assets to prevent caching issues during plugin updates\n\n\nv3.13.0 - 2017-10-02\n--------------------\n\n##### An All New Course Builder\n\n+ The \"Course Outline\" metabox found on the admin panel when editing any LifterLMS course has been savagely beaten. We stole its lunch money and we put it towards the construction of an all interface\n+ Asynchronous loading: fixes issues where very large courses would drastically slow and possibly even time out the loading of the course edit screen\n+ Course outline is now collapsible and expandable. This Fixes issues where it was very hard to move lessons and sections around on very large courses\n+ In addition to the familiar (and now improved) drag and drop functionality, you may now also move sections and lessons up and down with button clicks. You can also move lessons between sections with button clicks\n+ Add new lessons and sections with a click or drag a new lesson or section into the existing course\n+ Edit section and lesson titles faster with inline title editing. No more modals with a potentially slow ajax load to update a title. Click the title, change it, and exit the field to automatically save!\n+ Delete sections and lessons with the click of a button\n+ Quick links to view (frontend) and edit (backend) lessons\n+ Completely internationalized. Thanks for you patience translators!\n+ Want to know more? Check out the [docs](https://lifterlms.com/docs/using-course-builder/).\n\n##### New User Roles\n\n+ Added new roles to enable you to provide access to LifterLMS (settings, courses building, etc...) without having to make an admin or mess with complicated code snippets.\n+ New Roles:\n\n  + LMS Manager: Do everything in LifterLMS and nothing with plugins, themes, core settings, and so on\n  + Instructor: Create, update, and delete courses and memberships\n  + Instructor's Assistant: Edit courses and memberships\n\n+ More details and a full list of new LifterLMS capabilities are available [here](https://lifterlms.com/docs/roles-and-capabilities/).\n\n##### Updates & Fixes\n\n+ Tested up to WordPress 4.8.2\n+ The \"Lesson Tree\" metabox has been replaced with a simplified version of the lesson tree and a link to the launch the Course Builder.\n+ Course and membership categories and tags will now display on their respective post tables for sorting and filtering. They can be disabled on a per-user basis via the screen options.\n+ Removed `var_dump()` from bbPress integration restriction check\n\n##### Uninstall Script\n\n+ Uninstall script now removes all the things LifterLMS creates in your database if a constant is defined. Read more [here](https://lifterlms.com/docs/remove-lifterlms-data-plugin-uninstallation/).\n\n##### Database Update\n\n+ Adds default Instructor data for all LifterLMS Courses & Memberships based off of the post author of the course or membership\n+ [More information](https://lifterlms.com/docs/lifterlms-database-updates/#3130)\n\n##### Template Updates\n\n+ [admin/post-types/students.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/students.php)\n+ [admin/reporting/tabs/students/courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses.php)\n\n##### Deprecated Functions\n\n+ The following AJAX functions are no longer utilized by LifterLMS core. If you are utilizing them find alternatives (they all exist). These will be remove in the next **major** release:\n\n  + `LLMS_AJAX::get_achievements()`\n  + `LLMS_AJAX::get_all_posts()`\n  + `LLMS_AJAX::get_associated_lessons()`\n  + `LLMS_AJAX::get_certificates()`\n  + `LLMS_AJAX::get_courses()`\n  + `LLMS_AJAX::get_course_tracks()`\n  + `LLMS_AJAX::get_emails()`\n  + `LLMS_AJAX::get_enrolled_students()`\n  + `LLMS_AJAX::get_enrolled_students_ids()`\n  + `LLMS_AJAX::get_lesson()`\n  + `LLMS_AJAX::get_lessons()`\n  + `LLMS_AJAX::get_lessons_alt()`\n  + `LLMS_AJAX::get_memberships()`\n  + `LLMS_AJAX::get_question()`\n  + `LLMS_AJAX::get_sections()`\n  + `LLMS_AJAX::get_sections_alt()`\n  + `LLMS_AJAX::get_students()`\n  + `LLMS_AJAX::update_syllabus()`\n\n##### Removed Filters\n\n+ The following filters have been removed and are no longer in use.\n\n  + `lifterlms_admin_courses_access`: replaced with user capability `edit_courses`\n  + `lifterlms_admin_membership_access`: replaced with user capability `edit_memberships`\n  + `lifterlms_admin_reporting_access`: replaced with user capability `manage_lifterlms`\n  + `lifterlms_admin_settings_access`: replaced with user capability `manage_lifterlms`\n  + `lifterlms_admin_import_access`: replaced with user capability `manage_lifterlms`\n  + `lifterlms_admin_system_report_access`: replaced with user capability `manage_lifterlms`\n\n\nv3.12.2 - 2017-09-18\n--------------------\n\n##### Bug fixes\n\n+ Fix issue with LifterLMS bbPress integration preventing course-restricted topics from being accessible by enrolled students\n+ Fix an issue preventing students expired from courses via access expiration settings from being manually re-enrolled by admins\n\n##### Deprecations\n\n+ `LLMS_Student` class function `has_access` is scheduled for deprecation in next major release. Developers should switch to `LLMS_Student->is_enrolled()`\n\n\nv3.12.1 - 2017-08-25\n--------------------\n\n+ Prevent duplicate loading of repeater metabox fields\n+ Fix undefined warning related to quiz completion\n+ Ensure that the bbPress course forums shortcode & widget properly cascade up when used on a lesson or quiz\n\n\nv3.12.0 - 2017-08-17\n--------------------\n\n+ New quiz feature: randomize the order of quiz questions each attempt! Props to [Larry Groebe](https://github.com/larrygroebe)\n+ Fixed logic error related to access checks when bubbling from quiz->lesson->course\n+ Fixed JS loader check for tinyMCE editors in repeater fields\n+ Fixed CSS issue related to tinyMCE editors in repeater fields\n+ Fixed issue causing tinyMCE editors in repeater fields to stop working after reordering rows\n+ LifterLMS alert box notices are now cleared during shutdown instead of immediately after rendering. Fixes some plugin compatibility issues.\n+ Fix reference to invalid meta key on order notes admin screen.\n+ Record order note when orders with a defined length complete\n+ When a payment is scheduled for an order with a defined length, calculate end date if no end date is saved\n+ Minor updates to the `LLMS_Abstract_Integration` class\n+ Fix undefined reference error on 404 pages resulting from the preview manager.\n\n##### bbPress Integration Updates\n\n+ Add \"Private\" Course Forums which allows forums to be made available only to students enrolled in the associated course\n+ Adds a shortcode and widget for outputting a list of forums associated with a course\n+ Adds the ability to restrict the page set as the bbPress forum index (via bbPress settings) to be restricted to LifterLMS memberships\n+ Adds engagement triggers to allow engagements to be fired when a student posts a reply or creates a new topic\n+ Improves integration membership restriction check performance\n+ Migrated to the `LLMS_Abstract_Integration` class. Visually changes the settings display but has no other impact\n+ [More information](https://lifterlms.com/docs/lifterlms-and-bbpress/)\n\n##### BuddyPress Integration Updates\n\n+ Add the ability to restrict activity, group, and member directory pages to LifterLMS memberships.\n+ Migrated to the `LLMS_Abstract_Integration` class. Visually changes the settings display but has no other impact\n+ [More information](https://lifterlms.com/docs/lifterlms-and-bbpress/)\n\n##### Database update\n\n+ calculate and store end dates for orders created prior to version 3.11.0 which have a defined length and do not have a stored end date.\n+ migrate bbPress and BuddyPress options to `LLMS_Abstract_Integration` naming convention\n+ [More information](https://lifterlms.com/docs/lifterlms-database-updates/#3120)\n\n##### Admin Post Table Upgrades\n\n+ Lessons\n  + Fix section titles which formerly were a dead link. Now they're just text\n  + Add filtering the table by associated course\n+ Quizzes\n  + Display associated course and lesson columns with links\n  + Add filtering by associated course and/or lesson\n+ Quiz Questions\n  + Display associated Quizzes with links\n  + Add filtering by associated quiz\n\n##### Template Updates\n\n+ [admin/post-types/order-details.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/order-details.php)\n\n\nv3.11.2 - 2017-08-14\n--------------------\n\n+ Tested up to WP Core 3.8.1\n\n##### System Status and Reporting updates\n\n+ System Report renamed to \"Status\"\n+ Added information of template overrides to the system report\n+ Added \"Get Help\" button linking to LifterLMS Ticketing submission page\n+ Added \"Logs\" tab which allows for easy viewing & management of LifterLMS logs\n+ Added \"Tools and Utilities\" tab and moved tools from the General Settings screen to this tab\n+ Improved Session Reset tool\n\n\nv3.11.1 - 2017-08-03\n--------------------\n\n+ New shortcode: `[lifterlms_course_continue_button]`. See [shortcode docs](https://lifterlms.com/docs/shortcodes/#lifterlms_course_continue_button) for more information.\n+ New shortcode: `[lifterlms_lesson_mark_complete]`. See [shortcode docs](https://lifterlms.com/docs/shortcodes/#lifterlms_lesson_mark_complete) for more information.\n+ Added filter `llms_product_pricing_table_enrollment_status` to allow forceful display of course/membership pricing tables regardless of user enrollment status.\n+ Fix course author shortcode to allow usage outside of a course via the `course_id` parameter.\n\n##### Template Updates\n\n+ [product/pricing-table.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/pricing-table.php)\n+ [product/course/progress.php](https://github.com/gocodebox/lifterlms/blob/master/templates/product/course/progress.php)\n\n\nv3.11.0 - 2017-07-31\n--------------------\n\n+ New engagement trigger \"Student purchases access plan\" allows engagements to be triggered from a specific access plan!\n+ Minor performance improvements to notification-related database queries\n+ Fix issue causing payment gateways to always use test mode links from Orders on the admin panel\n+ Added default email notification merge code for outputting an HTML divider\n+ Added new actions to Dashboard template to allow adding custom content to course tiles on the dashboard\n\n##### Template Updates\n\n+ [myaccount/my-courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-courses.php)\n\n\nv3.10.2 - 2017-07-14\n--------------------\n\n+ Fix fatal error related to purchase receipts for trashed or deleted orders\n+ l10n \"Reviews\" tab title on course settings\n+ Remove commented out sample preheader text from email header template which was displaying in some email clients.\n\n##### Template Updates\n\n+ [emails/header.php](https://github.com/gocodebox/lifterlms/blob/master/templates/emails/header.php)\n\n\nv3.10.1 - 2017-07-12\n--------------------\n\n##### Bugfixes\n\n+ Prevent errors related to attempting to display notification data related to deleted students\n+ Fix errors related to displaying notifications for deleted post (courses, sections, lessons, quizzes, etc...)\n+ Fix error causing email notifications being sent after related user has been deleted\n+ Fix typo preventing `llms_form_field()` from outputting textareas\n\n##### Updates\n\n+ Add new filter `llms_allow_subscription_cancellation` useful for preventing students from self-cancelling their subscriptions on the student dashboard. [More info](https://lifterlms.com/docs/lifterlms-filters/#llms_allow_subscription_cancellation).\n+ Add new API for querying students via AJAX select2 elements\n+ Select2 Post Query elements can now query multiple post types simultaneously\n+ Select2 Post Query elements can now support `<optgroup>`\n\n###### i18n\n\n+ Course option metabox for reviews is not translatable\n\n\nv3.10.0 - 2017-07-05\n--------------------\n\n##### Recurring Order Management (for Admins)\n\n+ Admins can now edit various pieces of data related to a recurring order from the order screen on the admin panel\n  + Allow editing of the Next Payment Date\n  + Allow editing of the Trial End Date (when a trial is active for the order)\n  + Edit Payment Gateway and related gateway fields (Customer ID, Source ID, and Subscription ID)\n+ If you're using LifterLMS Stripe or LifterLMS PayPal please update to the latest version of these add-ons to take advantage of these new features!\n\n##### Recurring Order Management (for Students)\n\n+ Students can now switch the payment method (source) for their recurring subscriptions from the student dashboard\n+ Students can now cancel their recurring orders to prevent future payments on recurring orders\n+ If you're using LifterLMS Stripe or LifterLMS PayPal please update to the latest version of these add-ons to take advantage of these new features!\n\n##### Automatic Payment Retries (for supporting gateways)\n\n+ LifterLMS Stripe and LifterLMS PayPal can now automatically retry failed payments to help recover lost revenue as a result of temporary declines to payment sources. Please see our documentation on this new feature [here](https://lifterlms.com/docs/automatic-retry-failed-payments/).\n+ If you're using LifterLMS Stripe or LifterLMS PayPal please update to the latest version of these add-ons to take advantage of these new features!\n\n##### Manual Payment Gateway Enhancements\n\n+ The Manual Payment Gateway (bundled with LifterLMS Core) can now handle recurring payments. For more information on utilizing recurring payments with the Manual Gateway please see the [gateway documentation](https://lifterlms.com/docs/using-lifterlms-manual-payment-gateway/).\n\n##### Updates and Fixes\n\n+ Force SSL setting now applies to Student Dashboard screens. This is useful as Google now recommends any page where a password is submitted should be encrypted and allows gateway communication from student dashboard screen with APIs that require an SSL connection.\n+ Fixed spelling error related to quizzes\n\n##### Templates changed\n\n**NEW**\n\n+ [checkout/form-switch-source.php](https://github.com/gocodebox/lifterlms/blob/master/templates/checkout/form-switch-source.php)\n+ [myaccount/view-order-transactions.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order-transactions.php)\n\n**UPDATED**\n\n+ [admin/post-types/order-details.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/post-types/order-details.php)\n+ [myaccount/my-orders.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-orders.php)\n+ [myaccount/navigation.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/navigation.php)\n+ [myaccount/view-order.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/view-order.php)\n+ [quiz/summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/summary.php)\n\n\nv3.9.5 - 2017-06-13\n-------------------\n\n+ Increased css z-index of basic notifications to prevent issues with themes that have high z-index on menus and other elements\n+ Increased the frequency of basic notification heartbeat check from 10 to 20 seconds\n+ Added filter to allow for customization of the notifications heartbeat interval, example [here](https://lifterlms.com/docs/lifterlms-filters/#llms_notifications_settings).\n+ Fixed error related to password reset when the \"Disable Usernames\" account setting is disabled\n\n\nv3.9.4 - 2017-06-12\n-------------------\n\n+ Fix hardcoded db reference to `wp_posts` table\n\n\nv3.9.3 - 2017-06-09\n-------------------\n\n+ Fix typo in notifications query\n\n\nv3.9.2 - 2017-06-07\n-------------------\n\n+ Tested up to WordPress 4.8\n+ Fixed issue with merge codes on WP Editors for notifications, emails, etc...\n+ Update notifications query to only return results related to posts which actually exist. Prevents errors occurring when reviewing achievements on the student dashboard for courses, lessons, etc which have been deleted/trashed.\n+ Only display quiz time limit meta information when a time limit exists\n+ Fix display of quiz question order (question x of x)\n+ Improved logic powering quiz attempt grading for increased consistency, especially with regards to floats and rounding\n\n##### Templates Changed\n\n+ [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php)\n+ [quiz/question-count.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/question-count.php)\n\n\nv3.9.1 - 2017-06-02\n-------------------\n\n+ Fix engagement triggers with relation to quizzes to properly receive 3.9 api updates\n+ Fix quiz attempt counting issue resulting in the total attempts by a student always being one more than the actual value\n+ Fix membership access plan restrictions tooltip\n\n\nv3.9.0 - 2017-06-02\n-------------------\n\n##### Quizzes\n\n+ All new quiz results interface for students\n  + Donut charts are now animated\n  + Donuts will be green for passing attempt and red for failing\n  + Students can now review previous quiz attempts and summaries\n  + Removed the juxtaposition of the current and best attempts to reduce confusion on the interface\n  + Improved the consistency of the quiz meta information markup\n  + Adjusted various pieces of language for an improved student experience\n+ Improvements to the quiz taking experience\n  + Added the LLMS_Spinner (seen on checkout screens and various places on the admin panel) and various loading messages when starting quiz, transitioning between questions, and completing a quiz\n  + Better error handling and management should issues arise during a quiz\n  + Better unload & beforeunload JS management to warn students when they attempt to leave a quiz in progress\n+ Improved quiz data handling and management\n  + Improved API calls and handlers related to taking quizzes for increased performance and consistency\n  + quiz data can now be programmatically queried via consistent apis and data classes, see `LLMS_Student->quizzes()` and `LLMS_Quiz_Attempt`\n+ Quizzes no longer rely on session and cookie data. All quiz data will always be saved directly to the database and related to the student. Fixes an issue on certain servers preventing student from starting quizzes.\n+ Deprecated `LLMS_Quiz::start_quiz()`, `LLMS_Quiz::answer_question()`, and, `LLMS_Quiz::complete_quiz()`\n  + Ajax handler functions of the same names should be used instead.\n  + To programmatically \"take\" quizzes use related functions of similar names from the `LLMS_Quiz_Attempt` class\n\n##### Templates changed\n\n+ New\n  + [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php)\n\n+ Updated\n  + [admin/reporting/tabs/students/courses.php](https://github.com/gocodebox/lifterlms/blob/master/templates/admin/reporting/tabs/students/courses.php)\n  + [content-certificate.php](https://github.com/gocodebox/lifterlms/blob/master/templates/content-certificate.php)\n  + [course/complete-lesson-link.php](https://github.com/gocodebox/lifterlms/blob/master/templates/course/complete-lesson-link.php)\n  + [myaccount/my-notifications.php](https://github.com/gocodebox/lifterlms/blob/master/templates/myaccount/my-notifications.php)\n  + [quiz/next-question.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/next-question.php)\n  + [quiz/previous-question.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/previous-question.php)\n  + [quiz/question-count.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/question-count.php)\n  + [quiz/quiz-question.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/quiz-question.php)\n  + [quiz/quiz-wrapper-end.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/quiz-wrapper-end.php)\n  + [quiz/quiz-wrapper-start.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/quiz-wrapper-start.php)\n  + [quiz/results.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/results.php)\n  + [quiz/return-to-lesson.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/return-to-lesson.php)\n  + [quiz/single-choice_ajax.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/single-choice_ajax.php)\n  + [quiz/start-button.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/start-button.php)\n  + [quiz/summary.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/summary.php)\n\n+ Removed\n  + quiz/attempts.php - replaced by [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php)\n  + quiz/passing-percent.php - replaced by [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php)\n  + quiz/time-limit.php - replaced by [quiz/meta-information.php](https://github.com/gocodebox/lifterlms/blob/master/templates/quiz/meta-information.php)\n\n##### Fixes\n\n+ Student Dashboard notifications page will not display pagination links unless there's results to page through\n+ Student Dashboard notifications page will now display a message when no notifications are found\n+ Certificate previewing now takes into consideration the preview setting roles to allow admins (or other roles) to preview certificates\n+ Made student name self fallback (you) i18n friendly\n\n\nv3.8.1 - 2017-05-21\n-------------------\n\n+ Fix merge code issue related to course title on quiz notifications\n\n\nv3.8.0 - 2017-05-20\n-------------------\n\n+ Automatic email and basic (on-screen) notifications for various events within LifterLMS\n  + All notifications can be customized\n  + Email notifications can be optionally sent to custom email address, course authors, and more\n+ Students will automatically receive email receipts when making purchases and when recurring access plans rebill\n+ Hidden Access Plans\n+ Add a \"Purchase Link\" view button to access plans so admins can quickly grab the direct URL to an access plan\n+ Notifications history screen on Student Dashboard to review past notifications that have been received\n+ Updated LLMS_Email class and functionality\n+ Email templates have been completely rewritten and styled\n+ Updated and rewritten password reset flow\n+ Earned certificates are only accessible by the student who earned the certificate\n+ Added the functionality for image upload via options & settings api\n+ Removed a handful of unused templates related to LifterLMS certificates that were replaced a long time ago but still existed in the codebase for unknown reasons.\n+ Fixed filter on engagements settings page\n+ Minor adjustments to language and settings order on Engagements settings screen for email settings\n+ Email Header Image field is now an upload field as opposed to a \"paste a url here\" setting\n+ Phone number recorded to order and displayed on order for admin panel during purchases\n+ Order details now display full country name as opposed to the country code\n+ Fix installation script to ensure admin can preview by default\n\n\nv3.7.7 - 2017-05-16\n-------------------\n\n+ Updated a few strings on the admin panel to be translatable\n+ Fix PHP warning output during plugin activation\n+ Fix reporting issue related to outputting quiz question answers where the correct answer is the first available answer\n+ Fix PHP 7.1 issue on the checkout screen\n+ Removed some unnecessary files from vendor libraries\n\n\nv3.7.6 - 2017-05-05\n-------------------\n\n+ New translations for new categories on Add-ons screen\n+ Update to general settings which utilizes featured items from the general settings screen\n+ Update readme & related meta files\n+ Removed advert image files\n\n\nv3.7.5 - 2017-05-02\n-------------------\n\n+ Upgrade WP Session Manager to latest version\n+ Code style updates across most files in codebase to bring to most recent styling guidelines put forth by [WP Coding Standards](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards)\n\n\nv3.7.4 - 2017-04-26\n-------------------\n\n+ When cloned site detected automatically disable recurring_payments feature & trigger an action 3rd parties can hook into for custom 3rd party features\n+ Add better JS dependency management to prevent issues where assets loaded in the wrong order\n+ Fix issue where dismiss icon on LifterLMS admin notices was positioned poorly on non-LifterLMS admin screens\n+ Fix issue preventing edit account form submission on student dashboard when password strength meter is disabled\n\n\nv3.7.3 - 2017-04-21\n-------------------\n\n+ Fixed issues where Course Track checks were not functioning properly with relation to prerequisite associations\n+ `LLMS_Generator` can now be used to generate course(s) from a raw array of course data using the SingleCourseGenerator and BulkCourseGenerator\n+ `LLMS_Generator` default post status can be set at runtime using `set_default_post_status()`\n+ Fixed an issue causing JS errors on the `wp-login.php` screen\n+ Tested up to WordPress 4.7.4\n\n### Template Updates\n\n+ `course/prerequisites.php` - Prerequisite checks check for 'course_track' rather than 'track'\n\n\nv3.7.2 - 2017-04-17\n-------------------\n\n+ Resolved a JS errors on admin panel resulting from overly strict asset loading added in 3.7.0\n\n\nv3.7.1 - 2017-04-14\n-------------------\n\n+ Fix php notice when no roles are selected for preview management feature\n\n\nv3.7.0 - 2017-04-13\n-------------------\n\n**Preview Management**\n\n+ All new view management for users to make editing content easier for course builders\n+ Admins may customize the roles of users who can access view management\n+ Qualifying users can view content as an enrolled student or a non-enrolled visitor\n+ Default view allows users to bypass all restrictions (drip, membership, enrollment, and so on) for easy course navigation and management\n+ Thanks to [@fabianmarz](https://github.com/fabianmarz) and the team at and the team at [netzstrategen](https://github.com/netzstrategen) for their assistance with this feature!\n\n**Improvements**\n\n+ Edit Account Screen now utilizes updated APIs for better customization management\n+ Improve intelligence of enqueued admin js & css files\n\n**Fixes**\n\n+ Fixed coupon calculation issue related to currencies using commas as the decimal separator\n+ Properly display track related information when reviewing engagements on the admin panel\n+ fixed issue preventing course tracks from being recorded as completed\n\n\nv3.6.2 - 2017-04-10\n-------------------\n\n+ Fix issue preventing export of vouchers via email\n+ added action `after_llms_mark_complete` to allow custom actions to happen after a course, lesson, etc... is marked complete\n\n\nv3.6.1 - 2017-03-28\n-------------------\n\n+ Fix issue related to taking a quiz for the first time when no quiz data is available for a user\n+ Fix issue when course outline shortcode is displayed on non LifterLMS post types\n\n\nv3.6.0 - 2017-03-27\n-------------------\n\n+ Courses and Memberships now have settings to control their visibility in catalogs and search results. For more information visit the [knowledge base](https://lifterlms.com/docs/course-membership-visibility-settings/).\n+ Courses are now a searchable post type. All existing courses will automatically remain excluded from search via new catalog visibility settings. New courses added after this date will be searchable unless the visibility is updated prior to publishing the course.\n+ Added options (and filters) to allow customization of the order of courses displayed on the Student Dashboard\n  + Existing behavior (ordered by enrollment date, most recent to least recent) will be preserved\n  + New installations will default (by popular demand) to Order (Low to High) which will obey the \"Order\" settings of courses\n  + Customize or update the order for your site by visiting LifterLMS -> Settings -> Accounts and changing the setting for \"Courses Sorting\" under \"Account Dashboard\"\n+ New Shortcodes:\n  + `[lifterlms_course_author]` -  Display the Course Author's name, avatar, and (optionally) biography. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_author)\n  + `[lifterlms_course_continue]` - Display a progress bar and continue button for enrolled students only. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_continue)\n  + `[lifterlms_course_meta_info]` - Display all meta information for a course. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_meta_info)\n  + `[lifterlms_course_prerequisites]` - Display a notice describing unfulfilled prerequisites for a course. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_prerequisites)\n  + `[lifterlms_course_reviews]` - Display reviews and review form for a LifterLMS Course. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_reviews)\n  + `[lifterlms_course_syllabus]` - Display the course syllabus. [Info & Usage](https://lifterlms.com/docs/shortcodes/#lifterlms_course_syllabus)\n+ \"Back\" & \"Next\" pagination links on Student Dashboard View Courses are now buttons instead of text links\n+ Fixed an issue preventing pagination links from displaying on the \"View Courses\" page of the student dashboard when the endpoint slug was customized\n+ Course and Membership taxonomy archive pages will now properly match the heights of tiles\n+ Fixed typo in `lifterlms_get_enrollment_status_name` filter\n+ Fixed typo in `lifterlms_get_order_status_name` filter\n+ Reduced complexity and redundancy of `llms_get_enrolled_students()`\n\n\nv3.5.3 - 2017-03-21\n-------------------\n\n+ Ensure that access plan subscription schedule details are fully translatable\n+ Ensure \"Services\" title on admin add-ons screen can be translated\n+ Fix \"View All My Courses\" link on Student Dashboard to obey endpoint slug customizations\n+ Membership restriction checks only run on singular posts (not on archives)\n+ Ensure `[lifterlms_course_outline]` and Course Syllabus widget can be used on Quizzes.\n+ Fix reporting widgets for course & lesson completions to report the correct completion types only\n\n\nv3.5.2 - 2017-03-16\n-------------------\n\n+ Fix course outline shortcode when used on a lesson\n+ Fix custom html form fields produced by `llms_form_field()`\n\n\nv3.5.1 - 2017-03-15\n-------------------\n\n+ Lessons marked as incomplete will now display as incomplete in the course outline generated by the above Course Syllabus Widget and the course outline shortcode\n+ Updated course outline shortcode / course syllabus widget to utilize new APIs\n+ The template at `templates/course/outline-list-small.php` updated to reflect above changes. If you're overriding this template please review the changes and update accordingly\n+ Fix issue preventing course auto advance on lesson completion\n+ Shortcodes added within `[lifterlms_hide_content]` will now be processed\n\n\nv3.5.0 - 2017-03-13\n-------------------\n\n+ New course setting **Retake Lessons** allows students to mark lessons as \"incomplete\" after completing lessons. Admins may enable this site-wide setting under Settings -> Courses.\n+ Course and Membership catalog per page settings will now only accept numbers\n+ \"Catalogs\" settings tab has been split into \"Course\" and \"Membership\" settings\n+ Settings added via filter `lifterlms_catalogs_settings` will be added to the \"Course\" settings tab and deprecated in the next major release\n+ Default course and membership catalog courses per page changed to 9. Previous default was 10 which results in a 4th row on catalogs with only one item.\n+ Tweaked size of LifterLMS admin tab menu items\n+ Pass API Mode Context to links generated by LifterLMS payment gateways\n+ Fixed typo on general settings screen\n+ Moved LifterLMS Add-on Banners from General Settings to an Add-Ons menu\n+ If required fields exist on checkout and are empty during free quick enrollment users will be redirected to the normal checkout page where they can enter required fields\n+ Updated action scheduler lib to latest version. Minor changes, fixes compatibility with WooMemberships.\n+ Recent activity stats widgets on general settings screen updated to be more reliable and accurate (and performant!)\n+ Added 3 new widgets to enrollments reporting tab: courses completed, lessons completed, and user registrations\n\n\nv3.4.8 - 2017-03-07\n-------------------\n\n+ Tested to WordPress Version 4.7.3\n+ Fixed undefined index notice on admin panel\n+ Added a real description to new `_nx()` functions\n+ Access plan trial periods now allow proper translations\n\n\nv3.4.7 - 2017-03-03\n-------------------\n\n+ Ensure run when the `lifterlms_db_version` option doesn't exist in the database\n\n\nv3.4.6 - 2017-03-03\n-------------------\n\n+ Fixed a text domain typo preventing translation of \"Correct Answer\" on quiz results screen\n+ Ensure access plan \"periods\" are translatable\n+ Now using `date_i18n()` for certificate dates so that dates are properly localized\n+ Load plugin textdomain during `init` rather than `plugins_loaded`\n\n\nv3.4.5 - 2017-02-23\n-------------------\n\n+ Ensure free access plans are available to logged out users\n\n\nv3.4.4 - 2017-02-22\n-------------------\n\n+ Added a popup to warn students when leaving a quiz they've already started\n+ Enable removal of student quiz attempts by admins from student reporting screens\n+ Fix an undefined error on quiz reporting screens for incomplete quizzes\n+ Display incomplete (abandoned) quizzes as incomplete (instead of as still running) on the quiz reporting screen\n+ Prevent logged in users from bypassing membership restrictions for free members-only access plans\n\n\nv3.4.3 - 2017-02-20\n-------------------\n\n+ Fix issue with bbPress integration so that forums restricted to multiple memberships allow users of at least one membership that the forum is restricted to access topics within that forum\n+ Ensure that the correct ajax url is used for quizzes, resolves issue for sites utilizing `FORCE_SSL_ADMIN`\n+ Refactored database background update scripts for increased reliability & performance\n+ Database update 3.3.0 moved to 3.4.3 in order to accommodate users who were unable to run the 3.3.0 update, please read the [3.4.3 database update notes](https://lifterlms.com/docs/lifterlms-database-updates/#343) for more information.\n+ WIP: refactoring shortcodes to a more sane set of functions and classes\n\n\nv3.4.2 - 2017-02-14\n-------------------\n\n+ Backwards compatible css for tooltips\n\n\nv3.4.1 - 2017-02-14\n-------------------\n\n+ Password strength meter now functions correctly when using the [lifterlms_registration] shortcode\n+ Ensure open registration with required voucher prevents registration with invalid vouchers\n+ Lesson completion via quiz completion only recorded the first time the quiz is completed\n+ Fix issue preventing membership catalog from obeying the catalog's ordering settings\n+ Prevent duplicate engagements from being triggered\n+ Admin tables can display percentages as a progress bar!\n+ Students reporting table displays overall progress as a progress bar\n+ Refactored frontend assets class to allow better management of inline scripts\n\n\nv3.4.0 - 2017-02-10\n-------------------\n\n+ Enrollment for free access plans has improved based on your feedback. For more information see [https://lifterlms.com/docs/checkout-free-access-plans/](https://lifterlms.com/docs/checkout-free-access-plans/)\n+ Upgraded Student Management Table for courses and memberships:\n  + Allow searching students by name / email\n  + Allow filtering of students by current status\n  + Allow sorting of students by name, user id, status, and enrollment updated date\n  + Added student's grade to the table (courses only)\n  + Table pagination allows skipping to the first and last pages\n  + Student names link to full student reporting screen\n  + Student IDs added to the table. ID links to the WP User Edit screen which was previously accessible by clicking the student's name\n  + Utilizing improved database queries for displaying data on the table\n+ One-click bulk enrollment of all current members of a membership into an auto-enrollment course. More info [here](https://lifterlms.com/docs/membership-auto-enrollment/#bulk-enrollment)\n+ Students reporting table pagination can now jump to first and last page\n+ Students reporting table pagination now displays current page and total number of pages\n+ Added new class `LLMS_Student_Query` which is modeled, loosely, off of the `WP_User_Query` and allows for querying student data in relation to courses\n+ `LLMS_Admin_Table` abstract now supports filtering and jump to first and last page pagination options\n+ `llms_get_enrolled_students` now utilizes `LLMS_Student_Query` and resolves a bug where some users returned by this query would be returned with the incorrect status.\n+ Ensure `LLMS_Course::has_prerequisite( 'course' )` & `LLMS_Course::has_prerequisite( 'track' )` always return booleans\n+ Made a small performance tweak for courses without audio / video embeds\n+ Fix coupon expiration dates check to be more i18n friendly\n+ Update `LLMS_Coupon` class to utilize 3.3.0 class property enhancements\n+ added `llms_current_time`, a pluggable wrapper for `current_time()` to enable easier unit testing of date-related functions\n+ Shortcodes within course restriction messages are now handled properly to output their intend content rather than the raw shortcode\n+ Ensure the Page Attributes area is available on lessons so WordPress 4.7 custom post type page templates can be utilized\n\n\nv3.3.1 - 2017-01-31\n-------------------\n\n+ Tested up to WordPress core 4.7.2\n+ Added new engagement triggers for Quiz completion, quiz failure, and quiz passed.\n+ Refactored Lesson Completion for sanity\n+ Added function `llms_mark_complete()` for simple programmatic completion of courses, sections, lessons, and tracks. See [usage docs](https://github.com/gocodebox/lifterlms/blob/master/includes/functions/llms.functions.person.php#L146-L162) for more information.\n+ Class function `LLMS_Lesson::mark_complete()` has been staged for deprecation. It will still function but developers should update code to use above function.\n+ LifterLMS background updaters will now display a progress report on the admin panel to add some transparency to how the update is doing.\n+ Added `author` support to `llms_membership` post type\n+ Added a way to remove all LifterLMS-generated data during plugin uninstallation.\n+ `llms_get_post()` will now work with any LifterLMS Post Model post types\n+ Removed references to `LLMS_Activate` class which was removed back in 2.0.\n+ Changed include method to session related classes for better handling via phpunit\n+ Refactored some of the `LLMS_Install` class for reliability and test coverage\n  + Changed order of table and option creation during installation. Prevents a database warning from being thrown during installation.\n  + Added function for retrieving default difficulty categories added during installation\n  + Added function for removing default categories added during installation\n+ `llms_are_terms_and_conditions_required()` ensure the page id used in this function is an absint\n+ Removed redundant function `LLMS_Lesson::single_mark_complete_text()`\n+ Add css classes for buttons to be auto-width rather than the width of their containers\n+ Fix ID of engagement email class. Allows some filters and actions to actually be used.\n+ Properly display quiz failures as failures on the quiz results screen\n+ `loop/feature-image.php` now works for unsupported PHP 5.5 and down\n+ Fix issue with modifying section titles from within the course builder\n+ Fix undefined warning resulting from admin notice \"flash\" being undefined on pre-existing saved notices\n+ Updated template at `templates/course/complete-lesson-link.php` to include a few new CSS classes and utilize `llms_form_field()` to standardize buttons\n\n\nv3.3.0 - 2017-01-23\n-------------------\n\n+ New course option allows displaying the video embed in place of the featured image on course tiles displayed on the course catalog screen\n+ Courses can now be exported individually or in bulk. Export of a course includes all course content, sections, lessons, and quizzes.\n+ Courses can now be duplicated. Duplication duplicates all course content, sections, lessons, and quizzes.\n+ Upon completion of the Setup Wizard a sample course can be automatically installed.\n+ Postmeta keys for Lessons and Sections which denote their relationship to their parents have been renamed for consistency, database upgrade 330 included in this release will rename the keys automatically. [Read more here](https://lifterlms.com/docs/lifterlms-database-updates/#330)\n+ Update to `LLMS_Post_Model` to allow easier programmatic definition and handling of extending class properties\n+ classes extending `LLMS_Post_Model` can now be serialized to json and converted to arrays programmatically\n+ new function `llms_get_post()` allows easier instantiation of an `LLMS_Post_Model` instance\n+ Added LifterLMS Database Version to the system report\n\n\nv3.2.7 - 2017-01-16\n-------------------\n\n+ Fix float conversion of large numbers with relation to coupon price adjustments\n\n\nv3.2.6 - 2017-01-16\n-------------------\n\n+ Tested up to WordPress Core 4.7.1\n+ Fix the display of track-related engagements on the engagement admin screen\n+ Fix float conversion of large numbers with relation to prices\n\n\nv3.2.5 - 2017-01-10\n-------------------\n\n+ New shortcode: `[lifterlms_pricing_table]` allows pricing table display outside of a course or membership. See [https://lifterlms.com/docs/shortcodes/#lifterlms_pricing_table](https://lifterlms.com/docs/shortcodes/#lifterlms_pricing_table) for usage information.\n+ New shortcode: `[lifterlms_access_plan_button]` allows custom buttons for individual access plans to be created outside of a pricing table. See [https://lifterlms.com/docs/shortcodes/#lifterlms_access_plan_button](https://lifterlms.com/docs/shortcodes/#lifterlms_access_plan_button) for usage information.\n+ ensure every return from `llms_page_restricted` is filtered. Thanks to @matthalliday\n+ Ensure purchase page can only load for valid access plans\n+ Course / Membership taxonomy archives now obey orders defined by their respective catalog settings\n+ Fix language of automatic validation error message for numeric field types\n+ Fix translation function error causing course syllabus to display incorrect \"x of x\" text\n+ Added correct text domain to an i18n string displayed on the checkout confirmation screen, thanks @ymashev\n+ Ensure search result pages are viewable by members and non members regardless of result membership restrictions (unless site is restricted to sitewide membership)\n\n\nv3.2.4 - 2017-01-03\n-------------------\n\n+ Fixed tooltips on lesson preview tiles (in course syllabus and on next/prev tiles inside lessons) to show the actual reason the lesson is inaccessible rather than always showing a generic enrollment message\n+ Removed the language \"You must enroll in this course to unlock this lesson\" in favor of \"You do not have permission to access to this content\" as a restriction message fallback when no better message is available\n+ \"Quiz Results\" title is now translatable\n+ Removed deprecated JS file \"llms-metabox-data.js\" which controlled UI/X for 2.x subscription data on courses and memberships\n+ Non LMS Content (pages, posts, forums, etc...) restricted to multiple memberships will now correctly allow users access to the content as long as they have access to at least one of the memberships\n+ Fixed a redirect loop encountered if direct access to a lesson with an incomplete prerequisite was attempted\n\n\nv3.2.3 - 2016-12-29\n-------------------\n\n+ Progress and Grade are now sortable columns on the student reporting table\n+ Make enrollment statuses translatable for courses and memberships on the Student Dashboard\n+ \"Sign Out\" text on student dashboard is now translatable, thanks @yumashev\n+ Fixed prerequisite lesson display on lesson post tables\n+ Ensure post archive (blog) is visible regardless of post membership restrictions\n+ Moved lesson post table management functions to their own class\n+ Unused section post table management functions removed\n\n\nv3.2.2 - 2016-12-21\n-------------------\n\n+ Adds filter `llms_student_dashboard_login_redirect` allowing customization of the redirect upon login via the Student Dashboard\n+ Adds a shortcode parameter, `login_redirect` to `[lifterlms_my_account]` allowing customization of the redirect upon login via the Student Dashboard\n+ Adds a new tool under \"Tools and Utilities\" on the LifterLMS Settings screen which allows users to clear the cached student overall progress and overall grade data\n+ Fixes a compatibility issue with the OptimizePress live editor\n+ Adds a text domain to a translation function where none was present, rendering the string untranslatable\n\n\nv3.2.1 - 2016-12-14\n-------------------\n\n+ Fix operator position on `is_complete` check\n\n\nv3.2.0 - 2016-12-13\n-------------------\n\n##### LifterLMS Reporting Beta\n\n+ Students overview displays broad information about your students in a searchable and sortable table\n+ Review data about individual students including:\n  + Membership enrollments and statuses\n  + Course enrollments, status, and progress\n  + Quiz attempts and and their submitted answers\n  + Earned achievements and certificates\n+ Sales and Enrollments analytics are now found under the \"Reporting\" screen\n+ Feedback on the beta? Let us know at [https://lifterlms.com/docs/lifterlms-reporting-beta/](https://lifterlms.com/docs/lifterlms-reporting-beta/)\n\n##### Other Updates & Fixes\n\n+ Lesson completion checks now look for at least one record of the completed lesson as opposed to looking for exactly one\n+ Fix positioning of teacher avatar on course/membership tiles\n+ Remove explicit color definition from Student Dashboard navigation links for greater theme compatibility\n\n\nv3.1.7 - 2016-12-06\n-------------------\n\n+ Added support for WordPress Twenty Seventeen theme\n+ Improved the messaging and functions related to LifterLMS Sidebar support\n+ Add alternate language for a quiz requiring 100% grade to pass\n+ Added CSS class `.llms-button-primaray` to lesson \"Mark as Complete\" buttons\n+ Add box-sizing css rule to LifterLMS form field elements. Fixes layout issues on themes that don't border-box everything.\n+ Fix an issue that prevented the admin notice to enable/disable recurring payments from clearing when a button was pressed from screens other than the LLMS Settings screen\n+ Fix next payment date error when viewing a cancelled recurring order on the student dashboard\n+ Recurring payments now scheduled based on UTC time in accordance with the action scheduler which executes based on UTC rather than site timezone\n+ Add existing lesson to course modal now relies on async search. Improves performance and prevents timeouts on sites with a 500+ lessons\n+ Removed 2.x -> 3.x update notification message\n+ Fix an issue with comment counting on PHP7\n+ Updated action scheduler library to latest version\n\n\nv3.1.6 - 2016-11-11\n-------------------\n\n+ Handle empty responses on analytics more responsibly\n+ Fix typo preventing completed orders from displaying in analytics when using course / membership filters\n+ Quiz builder now leverages llmsSelect2 rather than select2 directly. Resolves a number of theme and plugin compatibility issues.\n+ Prevent bullets and weird margins on LifterLMS notices with slightly more specific CSS\n+ Login error messages will now display regardless of whether or not open registration is enabled\n+ Attempts to access quizzes are redirected or error messages are output when student is not enrolled.\n\n\nv3.1.5 - 2016-11-10\n-------------------\n\n+ Fix Month display on Analytics Screen\n\n\nv3.1.4 - 2016-11-10\n-------------------\n\n+ Progress bars are slightly more intelligent to prevent a widowed \"%\" on themes with larger base font sizes\n+ LifterLMS Merge code button only displays where it's supposed to now\n+ Fix issue where users removed from a membership were not properly removed from courses they were auto-enrolled into because of that membership\n+ Fix analytics screen JS parsing error\n\n\nv3.1.3 - 2016-11-04\n-------------------\n\n+ Added new action hooks to the course syllabus widget/shortcode template\n+ Added a small text link on the student dashboard which links to the full courses list of the dashboard\n+ Display order revenue for legacy orders instead of 0\n+ Make the Order History table on the Student Dashboard responsive\n+ Only display _published_ courses on the student dashboard\n+ Fixes a conflict with WP Seo Premium's redirect manager which was creating access plan redirects\n+ Reenable course review options on the admin panel\n+ Updates review output method so reviews are now output via a removeable action\n\n\nv3.1.2 - 2016-10-31\n-------------------\n\n+ Update all course and lesson templates to rely only on `global $post` rather than on `$course` and `$lesson` globals which are working inconsistently across environments\n+ Fix typo related to the line-height of LifterLMS order notes on the admin panel. Thanks [@edent](https://github.com/edent)!\n\n\nv3.1.1 - 2016-10-28\n-------------------\n\n+ Shortcode `[lifterlms_hide_content]` has some new functionality. See [documentation](https://lifterlms.com/docs/shortcodes/#lifterlms_hide_content) for usage and more information!\n+ Fix logic when determining if terms and condition checkboxes should be displayed on checkout & open registration.\n+ Define a placeholder on the Terms & Conditions page selection so it can be removed\n+ Explicitly declare `LLMS_Lesson` on lesson audio/video embed templates instead of relying the global `$lesson`. Some environments appear to be losing the global.\n+ Removed unused lesson template \"full-description\"\n\n\nv3.1.0 - 2016-10-27\n-------------------\n\n+ New engagement triggers available to allow engagements to be fired when a student enrolls into a course or membership!\n+ Add custom email addresses for to, cc, and bcc when sending email engagements\n+ New Merge Code button for easy merging of custom merge codes when creating emails\n+ Added post table data for LifterLMS Engagements\n+ Added new filter `llms_email_engagement_date_format` which allows customization of the format of the `{current_date}` merge code available in LifterLMS Emails\n+ Added explicit max width declaration to images within LLMS Catalogs to prevent image overflow. Fixes some theme compatibility issues.\n+ Optimize course and lesson audio video templates for faster loads\n+ Fix course & lesson video to load videos instead of duplicating audio embeds\n+ Fix coupon usage query so that coupons cannot be used more than the maximum number of times. Also now displays the correct number of coupons used on the coupons post table.\n+ Fix LLMS Engagement Email merge codes to work in subject line\n\n\nv3.0.4 - 2016-10-20\n-------------------\n\n+ Added shortcode `[lifterlms_login]` so the login form can be displayed. Information usage at [https://lifterlms.com/docs/shortcodes/#lifterlms_login](https://lifterlms.com/docs/shortcodes/#lifterlms_login)\n+ Added internal function `LLMS_Student->get_name()`\n+ Three basic course difficulties will be automatically created on installation and upgrades\n+ Updated course difficulty save methods to rely only on the taxonomy rather than the taxonomy and postmeta table\n+ Updated admin settings screens to only flush rewrite rules on screens where it is necessary to update rewrites\n+ Fix issue with customization of LifterLMS account endpoint URLs\n+ Fix a conflict with [Redirection](https://wordpress.org/plugins/redirection/) url monitoring that was causing redirects to be created from Courses and Memberships to the site home page automatically whenever updating the post\n+ Fix an undefined index warning on courses / memberships when updating post data\n+ Remove confusing and invalid warning message from Membership post screen on admin panel\n\n\nv3.0.3 - 2016-10-17\n-------------------\n\n+ Added filter `llms_show_preview_excerpt` which can be used to hide the excerpt on course syllabus or next/back preview tiles in lesson navigation\n+ Fix logic so that only free lessons are marked as free lessons post 3.0 upgrade\n+ Fix incorrect display of the \"restricted\" and \"non-restricted\" content areas for memberships\n+ Fix undefined index warning output by membership metaboxes\n+ Fix dead like under \"Force SSL\" checkout setting\n+ Course & Membership tiles output by course or membership shortcodes now automatically match column heights like the default catalogs do.\n+ Correctly register students as the \"Student\" Role\n+ Database Upgrade script converts users with the role \"studnet\" to \"student\"\n\n\nv3.0.2 - 2016-10-14\n-------------------\n\n+ Added action `lifterlms_before_student_dashboard_tab`\n+ Added action `lifterlms_after_student_dashboard_greeting`\n+ Added action `lifterlms_after_student_dashboard_tab`\n+ Added action `lifterlms_sd_before_membership`\n+ Added action `lifterlms_sd_after_membership`\n+ Fix membership shortcode\n+ Fix issue that prevented \"Student Dashboard\" from rendering if the page was set as the child of another page\n+ Fix undefined function error in admin notices\n+ Fix nonce errors resulting from admin notice html being served from the database rather than being dynamically generated\n+ Fix db upgrade script which was enabling course time period for restrictions for all courses regardless of their pre 3.0 restriction settings\n+ Fix db upgrade script that was causing empty sale dates to show start of unix epoch b/c they were empty strings\n+ Fix Javascript parse error preventing section & lesson editing from within the course outline on the admin panel\n+ Fix lesson icons from highlighting lesson settings like drip delay & quiz association\n+ Updated course outline color scheme to match the 3.0 admin color scheme overhaul\n+ `LLMS_Lesson::get_assigned_quiz()` will output deprecation warnings for those using debug mode. LLMS core no longer uses this function and will be deprecated in the next major release.\n+ Handle enrollment status of legacy orders based on enrollment rather than enrollment AND order status\n\n\nv3.0.1 - 2016-10-13\n-------------------\n\n+ Properly prefix `llms_is_ajax()` to prevent 500 errors when leaving HTTPS forced checkout screen\n+ Fix student unenrollment from memberships which was leaving a trace of enrollment in the user_meta table\n+ Update student dashboard nav list items to have more specific no styles to prevent \"double discs\" on various themes\n+ Return course progress bar and \"continue\" button which was accidentally removed\n+ Added core support for \"Divi\" theme sidebars\n\n\nv3.0.0 - 2016-10-10\n-------------------\n\n**This is a massive update which _breaks_ backwards compatibility for many LifterLMS features. A database migration is also necessary for upgrading users to reformat certain pieces of information which are being accessed differently in 3.0.0**\n\n**We strongly recommend that you backup your website before upgrading and, if possible, test LifterLMS 3.0.0 in a non-public-facing testing environment to ensure compatibility with your theme and other plugins and to ensure that 3.0.0 changes do not adversely affect your existing website.**\n\n**Please thoroughly read the following changelog and, if necessary, submit support tickets or post in the forums with any questions _prior_ to upgrading. LifterLMS Support _cannot_ and _will not_ manually resolve migration issues which may arise from upgrading to 3.0.0.**\n\n+ New shortcodes to be documented later, checkout \"includes/class.llms.shortcodes.php\" if you're feeling anxious\n+ All kinds of CSS changes to make LifterLMS, in general, be a little less old looking\n+ Added a number of CSS classes to various areas in the Checkout template at \"templates/checkout/form-checkout.php\"\n+ Added a \"Cancel\" button that allows you to hide the coupon form if the user decides not to add a coupon\n+ Removed jQuery animations from the coupon form toggle in favor of a CSS class toggle. If you decide you want some animations on the form add some CSS transitions to the `.llms-coupon-entry` element (and children) to change when the class `.active` is added or removed from the element.\n+ Refactored JavaScript related to LifterLMS Checkout. Improvements are minimal (if any) but the file is now smaller and more readable! Yay code stuff.\n+ Fixed some redundant text on single payment confirmation screen. (\"Single payment of single payment of\")\n+ Added a link to memberships listed under \"My Memberships\" on the LifterLMS Account Screen\n+ LifterLMS Order posts have been renamed in the database from \"order\" to \"llms_order\" to prevent any potential conflicts with other plugins. Automated database migration will handle the renaming of old orders.\n+ Fixed undefined variable notice generated by Sections without any lessons inside of them\n+ renamed function `add_query_var_product_id()` to `llms_add_query_var_product_id()`\n+ added a class for interacting with a course TRACK, instantiated by a track term or term_id (`LLMS_Track`)\n+ password strength meter and related settings / options via utilizing WordPress password strength functions available\n+ cleaned up the lesson locked tooltips to be a bit more sane and also utilized in course navigation on individual lessons.\n+ Updated admin menus for LifterLMS content to be more sane and organized and intuitive and so on and so forth\n\n##### Payment Gateways\n\n**NOTE: at this release, LifterLMS PayPal is the only payment gateway that will work with this release. We haven't started working on Stripe 4.0.0 which will work with LifterLMS 3.0.0**\n\n+ Payment gateways powered by a new abstract gateway class\n+ PayPal has been removed from LifterLMS and is available as premium extension\n\n##### Frontend Notices\n\n+ LifterLMS \"Notices\" have been rewritten, slightly.\n+ Most templates have been updated\n+ associated CSS has been updated\n+ Some sanity has been added to the related functions\n\n##### Post \"Model\" Concept / Overhaul\n\nUpdated classes for programmatically accessing all sorts of data related to custom post types registered by LifterLMS.\n\nThese post types currently include:\n\n+ Access Plans -- a non-public post type associated with courses and memberships which store payment related information\n+ Coupons (replaces includes/class.llms.coupon.php)\n+ Courses (replaces includes/class.llms.course.php)\n+ Lessons (replaces includes/class.llms.lesson.php)\n+ Memberships\n+ Orders (replaces includes/class.llms.order.php\n+ Products -- can be instantiated from courses or memberships (replaces includes/class.llms.product.php)\n+ Transaction -- a non-public post type associated with orders which store completed/attempted transaction data\n\n##### Improved admin metabox methods (and related)\n\n+ Updated custom LifterLMS Admin Metaboxes to have a more sane programmatic interface. This affects nearly all admin metabox classes in the plugin.\n+ A set of methods and classes have been added to improve the programmatic interface around custom post type post tables. These can be found in \"includes/admin/post-types/post-tables\"\n\n##### Coupons\n\n+ New class `LLMS_Coupon` allows for easy getting & setting of coupon data.\n+ Updated coupon post table to include relevant coupon information for all coupons at a glance\n+ Refactored admin panel coupon metabox generation to utilize new model for saving data\n+ Added translation functions to all strings in coupon settings screen\n+ Added new coupon settings\n  + _Expiration Date_ -- coupons cannot be applied to a purchase after the expiration date\n  + _Payment Type_ -- coupons can only be applied to either single or recurring payment plans. Existing coupons will be treated as single payment coupons until updated by the Admin.\n  + _First Payment Discount_ -- Applies only to recurring payment coupons. Determines the discount applied to the first payment of a recurring payment transaction.\n  + _Recurring Payments Discount_ -- Applies only to recurring payment coupons. Determines the discount applied all payments (other than the first) of a recurring payment transaction.\n  + _Description_ -- Record internal notes for a coupon visible only by admins on the admin panel\n+ The \"Coupon Code\" field has been removed in favor of the WordPress Coupon Post Title being utilized as the code. After upgrading, an automated database migration will move all coupon code fields to the title. The title previously functioned as the coupon description. During the migration the existing title will be moved to the new description field.\n\n##### Orders\n\n+ Added Order Statuses\n  + Completed - Single payment only. Denotes a successful transaction\n  + Active - Recurring only. Denotes the subscription is active with no issues\n  + Expired - Recurring only. Denotes the subscription has ended and is no longer active\n  + Refunded - Denotes the order has been refunded.\n  + Cancelled - Denotes the order has been cancelled manually by an admin.\n  + Failed - Denotes payment has failed. For subscriptions a failed payment will switch from \"active\" to \"failed\"\n  + Pending - Denotes that the order has been created but payment has not been completed yet\n+ Admin panel order table new features:\n  + The following columns are now sortable in ascending and descending orders: Order, Product, and Date\n  + Added totals based on order type (single or recurring) to the \"Total\" column\n  + Added an order status column for quick status review\n+ Order notes available for internal and system notes. powered by WP comments. lots of inspiration (and code) from WooCommerce, thank you <3\n+ Added a bunch of currency settings (as well as right-side currency and decimal-less currency support!)\n\n##### New Templates\n\n+ __Pricing Table__ at \"templates/product/pricing-table.php\" utilized by courses and memberships for displaying access plan information. Replaces \"templates/membership/purchase-link.php\" and \"templates/course/purchase-link.php\"\n+ __Course Taxonomy Templates__ at \"templates/course/categories.php\", \"templates/course/tags.php\", and \"templates/course/tracks.php\" display comma separated lists for course custom taxonomy terms\n+ __Course Prerequisite Template__ at \"templates/course/prerequisites.php\" displays prerequisite information (course and tracks) for a given course.\n+ __Meta Wrapper__ templates at \"templates/course/meta-wrapper-end.php\" and \"templates/course/meta-wrapper-start.php\" wrap some HTML around various meta data output about a course\n+ Significantly updated checkout process with all kinds of new templates including:\n  + templates/checkout/form-gateways.php\n  + templates/checkout/form-summary.php\n+ __Unified \"Lesson Preview\"__ at \"templates/course/lesson-preview.php\" displays \"buttons\" in course syllabus (on course page) and in course navigation (on lesson pages)\n+ Various template hook priority changes in order to make adding content between default LifterLMS areas easier\n\n##### Deleted Templates\n\n+ templates/checkout/form-checkout-cc.php\n+ templates/checkout/form-pricing.php\n\n##### New & Updated Admin Interfaces & Templates\n\n+ Significantly improved, changed, or brand new templates for metaboxes for various post types:\n  + templates/admin/post-types/order-details.php\n  + templates/admin/post-types/order-transactions.php\n  + templates/admin/post-types/product-access-plan.php\n  + templates/admin/post-types/product.php\n\n##### New Functions\n\n+ `llms_confirm_payment_url()` - Retrieve the URL used for confirming LifterLMS Payments\n+ `llms_cancel_payment_url()`  - Retrieve the URL users are directed to when cancelling a payment\n\n##### Install Script\n\n+ Removed some legacy default options that were being created and are no longer required for new installations.\n+ Removed unused `update_courses_archive()` function & related hook\n\n##### Select2\n\nNow utilizing a forked version of Select2 to prevent 3.5.x conflicts we've been plagued with\n\n##### Deprecated\n\n+ Removed filter `lifterlms_get_price_html`, use `lifterlms_get_single_price_html` instead\n+ Removed unused `LLMS_Product->get_price_suffix_html()` function\n+ Removed `LLMS_Product->set_price_html_as_value()` because we didn't like it anymore, don't use anything instead.\n+ Removed `add_query_var_course_id()` function\n+ Removed `displaying_sidebar_in_post_types()` function with the `LLMS_Sidebars::replace_default_sidebars()` function\n+ Filter `lifterlms_order_process_pending_redirect` has been replaced with `lifterlms_order_process_payment_redirect`\n+ Action `lifterlms_order_process_begin` has been deprecated\n+ Removed  `lifterlms_order_process_complete` action\n+ Replaced `LLMS_Course::check_enrollment()` with various new utilities. See `llms_is_user_enrolled()` for fastest use.\n+ Officially removed the `LLMS_Language` class\n+ Officially removed the `PluginUpdateChecker` class stubs we created to prevent updating issues with LifterLMS extensions during our transition to 2.0.0. This library has caused nothing but pain for everyone on our team and many of our users. It's gone now, forever.\n+ Removed function `lifterlms_template_single_price()` and replaced with `lifterlms_template_pricing_table()`\n+ Removed templates at \"includes/course/price.php\" and \"includes/membership/price.php\" in favor of \"includes/product/pricing-table.php\"\n+ Removed `LLMS_Person::create_new_person()` in favor of `LLMS_Person_Handler::register()` or `llms_create_new_person()`\n+ Removed `LLMS_Person->set_user_login_timestamp_on_register()` and are simply adding the metadata during registration\n+ Removed `lifterlms_register_post` action hook which fired after new user registration validation, this has been replaced with `lifterlms_user_registration_after_validation`\n+ Removed `lifterlms_new_person_data` and `lifterlms_new_person_address` filters, replaced with `lifterlms_user_registration_data`\n+ Removed `LLMS_Person::login_user()` in favor of `LLMS_Person_Handler::login()`\n+ background updater\n+ system report facelift + inclusion of all new settings via `LLMS_Data` class\n+ Fix setup wizard styles to follow update admin panel styles\n+ add links to last step of setup wizard for documentation and demo\n+ removed a bunch of deprecated coupon-related functions\n+ added a \"force ssl\" option to ensure checkout is secured\n+ added settings and options around recurring payments and staging sites to prevent duplicate charges when testing on a cloned site\n+ Check course restrictions automatically when checking lesson\n+ Added user_id to all access function checks to allow for checks for non current user\n+ course restriction messages display regardless of enrollment status\n+ check memberships and lock purchase of members only access plans\n+ Fixed titles of course closed and open messages on the course restrictions options\n+ record a start date for access plans based off when order moves to complete or active for the first time\n+ automatically expire limited access plans\n+ gave a quick facelift & unification to a lot of admin panel elements\n+ Color consistency updated according to LLMS brand guide\n+ Unified front and backend button classes\n+ Updated all frontend buttons to have consistent classes\n+ Removed the \"FREE\" lesson SVG in favor of simple text which allows translating\n+ Install & activation overhauls. Resolves [#179](https://github.com/gocodebox/lifterlms/issues/179)\n+ jQuery MatchHeight lib unignored\n+ A bunch of settings pages updated and a bunch of settings deprecated\n+ Gateways setting page removed\n+ Memberships & Courses page combined into \"Catalogs\" settings\n+ Added a data getting class used by the tracker class\n+ added a new page creation function with better intelligence that (hopefully) prevents duplicate pages from being created during core page installation\n+ new default country setting\n+ all order status changes recorded as order notes\n+ pending orders can be completed after failed payments\n+ better handling for gateways with fields\n+ JS spinners support multiples via start & stop!\n+ Updated (and semi-finished) analytics\n+ achievement metabox converted\n+ minor updates to voucher class\n+ Added a \"post state\" visible on the Pages posts table identifying if the page is saved as a LifterLMS page (EG: Checkout Page)\n+ Fixed copy/paste error of duplicate enrollment closed message on course restrictions tab\n+ Removed WC integration in favor of WC\n+ Upgrade \"back to course\" template to new lesson API\n+ Renamed `course/parent_course.php` to `course/parent-course.php` for template naming consistency\n+ use `strict` when auto generating usernames when creating from email addresses, resolves [#182](https://github.com/gocodebox/lifterlms/issues/182)\n\n##### 3.0.0 Auto Upgrader\n\n+ lots of postmeta data rekeyed\n+ intelligently generated defaults for various pieces of new meta data on courses, lessons, and memberships\n+ automatically generate access plans from existing course and membership data\n+ update existing orders to pull semi-accurate data into analytics based on new database structure\n+ cleans database of a ton of deprecated options and postmeta data\n\n##### Deprecated\n\n+ function `llms_is_user_member()`, use `llms_is_user_enrolled()` instead\n+ function `llms_check_course_date_restrictions()`\n+ function `quiz_restricted()`\n+ function `membership_page_restricted()`\n+ function `is_topic_restricted()`\n+ function `llms_get_post_memberships()`\n+ function `llms_get_parent_post_memberships()`\n+ function `parent_page_restricted_by_membership()`\n+ function `outstanding_prerequisite_exists()`\n+ function `find_prerequisite()`\n+ function `llms_get_course_enrolled_date()`\n+ function `llms_get_lesson_start_date()`\n+ function `lesson_start_date_in_future()`\n+ function `page_restricted_by_membership_alert()`\n+ function `llms_does_user_memberships_contain_course()`\n+ class `LLMS_Checkout`\n+ function `LLMS()->checkout()`\n\n##### Auto Enrollment\n\n+ Course auto enrollment for Memberships has been restored\n+ Works exactly the same as previously except auto-enrollment is not dependent on a course \"belonging to\" the membership via membership restrictions. This is because membership restrictions no longer apply to courses\n\n##### Analytics\n\n+ Charts! I'm really excited about this. I know we still need more data but please say nice things to me, I worked really hard on these little charts.\n+ Updated styles & interface\n\n##### bbPress\n\n+ Restrict individual forums (and their topics) to LifterLMS Membership levels\n\n##### BuddyPress\n\n+ Fixes broken course display on bp profile\n+ Adds memberships subpage to bp profile\n\n##### notices\n\n+ Admin notices class for managing admin notices, it's pretty neat!\n\n##### Student Management on Courses and Memberships\n\n+ All new and improved student management interface for managing student enrollments from courses and memberships\n\n##### Deprecated\n\n+ filter: `llms_meta_fields_course_main`, replace with `llms_metabox_fields_lifterlms_course_options`\n\n##### Manual Payments\n\n+ Manual Payment Gateway can now be enabled on the frontend!\n+ When a manual payment is recorded the user will be redirected to a view order page where they will be prompted to make a manual payment\n+ Define the payment instructions on the admin panel \"Checkout Settings\"\n+ Once you verify payment, head to the pending order and hit the \"Record a Manual Payment\" button to record the transaction\n+ Upon recording the order status will be upgraded to \"Complete\" and the user will be enrolled automatically\n\n##### Student Dashboard Upgrades\n\n+ More sane template hooks and functions\n+ Pagination on Courses endpoint (view only a preview on the main dashboard)\n+ Orders history & view orders screens!\n\nDeprecated options (and related functions where applicable) for the following course & membership options:\n\n  + `lifterlms_button_purchase_membership_custom_text`\n  + `lifterlms_course_display_outline_lesson_thumbnails`\n  + `lifterlms_course_display_author`\n  + `lifterlms_course_display_banner`\n  + `lifterlms_course_display_difficulty`\n  + `lifterlms_course_display_length`\n  + `lifterlms_course_display_categories`\n  + `lifterlms_course_display_tags`\n  + `lifterlms_course_display_tracks`\n  + `lifterlms_lesson_nav_display_excerpt`\n  + `lifterlms_course_display_outline`\n  + `lifterlms_course_display_outline_titles`\n  + `lifterlms_course_display_outline_lesson_thumbnails`\n  + `lifterlms_display_lesson_complete_placeholders`\n  + `redirect_to_checkout`\n\nIn all scenarios either a `add_filter` (returning false) or a `remove_action()` can be used to replicate the option.\n\n\nv3.0.0-beta.4 - 2016-09-01\n--------------------------\n\n+ fix issue with course prereq checks\n+ next payment due date visible on order admin view\n+ trial end date visible on order admin view\n\n##### Free Access Plans\n\n+ \"Free\" access plans now defined as such based on a checkbox rather than by entering 0 into the price\n+ Only single payment access plans can be free (a free recurring payment makes no sense but we can certainly discuss this if you disagree with me)\n+ trials are disabled with free plans (because trials only apply to recurring plans)\n+ sales are disabled for free access plans\n\n##### Checkout Form JS API\n\n+ unified JS checkout handler\n+ allows extensions to enqueue validation or pre-submission JS functions that should run prior to checkout form submission\n\n##### Manual Payment Gateway\n\n+ handles purchase of access plans marked ar FREE & orders that are discounted to 100% via coupons\n\n##### Open Enrollment\n\n+ Open Enrollment allows users to register on the account dashboard without purchasing a course\n+ Voucher settings are available to customize whether vouchers should be optional or required during open registration\n+ Better error reporting around voucher usage during enrollment\n\n##### Deprecated Functions\n\n+ `llms_get_coupon()`\n+ `get_section_id()`\n+ `check_course_capacity()`\n\n##### Quizzes\n\n+ Updated admin metaboxes to use new metabox abstract class\n+ display 0 instead of negative attempts on quiz summary\n+ updated logic in start button template\n\n##### Emails (for engagements)\n\n+ Admin metabox updated to new API\n+ Postmeta data migration:\n  + `_email_subject` renamed to `_llms_email_subject`\n  + `_email_heading` renamed to `_llms_email_heading`\n\n\nv2.7.12 - 2016-09-22\n--------------------\n\n+ Added a new filter on content returned after port permission checks\n+ Added additional information to plugin update message in preparation for major 3.0 release\n+ Updated plugin contributor metadata\n\n\nv2.7.11 - 2016-07-22\n--------------------\n\n+ Removed a duplicate action hook on course archive loop.\n+ Switched registration template include to use a more sane function\n+ Added updated banner adds with prettier ones. Wooooooo.\n\n\nv2.7.10 - 2016-07-19\n--------------------\n\n+ Fix undefined noticed related to LifterLMS custom post type archive filtering\n+ Fix filter which was supposed to allow custom engagement types to be queried & triggered by engagements automatically but was passing data incorrectly\n\n\nv2.7.9 - 2016-07-11\n-------------------\n\n+ We are now properly storing delayed engagement trigger data.\n+ Fixed an issue with our engagement query functions that caused, in very rare circumstances, the extra engagements to be triggered during an engagement trigger due to a lack of specificity in our query\n+ Fixed an undefined property notice related to email engagements when the email had no subject or header\n+ Fixed a typo in the description of a translation function.\n+ Added an engagement debug logging function. You can log all sorts of data related to engagements by adding `define( 'LLMS_ENGAGEMENT_DEBUG', true );` to your `wp-config.php` file.\n+ Allow course title shortcode to be used on course pages (and quizzes too). Documentation incorrectly said it was available on courses so we've fixed the function to allow for use on courses.\n\n\nv2.7.8 - 2016-07-05\n-------------------\n\n+ Bugfix: Restore access to quiz results on quiz completion\n\n\nv2.7.7 - 2016-07-01\n-------------------\n\n##### Russian\n\n+ LifterLMS is now 100% Translated into Russian thanks to our new Russian Translation Editor [@kellerpt](https://profiles.wordpress.org/kellerpt/)\n\n##### l18n\n\n+ All transition messages between questions during a Quiz are now translatable.\n+ LifterLMS subpages below the LifterLMS icon on the admin panel will now always display regardless of how you've chosen to translate the menu items. Hopefully puts to rest a long-standing i18n issue.\n\n###### Bug fixes\n\n+ Attempting to access a quiz when not enrolled in the associated course and having not properly started the quiz now results in a useful error message rather than a PHP warning.\n+ We've adjusted the way we're adding a admin panel \"separator\" to reduce conflicts with other plugins that have menu items with the same position as our separator (51).\n+ Added new logic to display an error message (instead of nothing) if there's an error during question loading.\n+ Resolve issue with course progress bar when added to a quiz sidebar (assuming your theme has sidebar support on your quizzes).\n+ Updated version number in the changelog for last version (it was supposed to be 2.7.6)\n\n\nv2.7.6 - 2016-06-28\n-------------------\n\n+ Students manually removed by Memberships by using the \"Students\" tab of a LifterLMS Membership will now be fully removed from the membership.\n+ Updated a few time-related strings to be l18n friendly. These items were all around Quiz time reporting and quiz time limits.\n+ Updated testing information, tested up to WP 4.5.3\n+ Fixed date of last release on changelog. It had the wrong date. Does that really matter?\n+ Updated readme.txt description area, we have a new youtube video! Yassss.\n\n\nv2.7.5 - 2016-06-13\n-------------------\n\n##### New features\n+ Added an \"id\" parameter to both LifterLMS Courses and LifterLMS Memberships shortcodes\n\n##### i18n\n+ Allow date translation on quiz results screen by using `date_i18n()` instead of `date()`\n+ Allow date translation on my courses screen by using `date_i18n()` instead of `date()`\n+ Ensure course status \"Enrolled\" is translatable on my courses screen\n\n##### Fixes\n+ Thanks to [@kjohnson](https://github.com/kjohnson) who fixed undefined index warnings & errors which occurred when viewing the last lesson in a section when the next section contained no lessons.\n+ Resolved an issue where formatting for \"Restricted Access Description\" course content would not display proper formatting.\n+ Fixed an issue with the \"FREE\" stamp for a free lesson caused layout issues.\n+ Removed the \"is-complete\" css class from incorrectly being added to lesson preview tiles for free lessons\n+ Fix an escaping issue when rendering Course titles inside LifterLMS notices. Prevents \"\\'s\" from displaying when \"'s\" should be displaying (and similar issues).\n\n\nv2.7.4 - 2016-05-26\n-------------------\n\n+ Fixed a bug with the new localization methods from 2.7.3\n+ Removed bundled it_IT translation files in favor of official language pack available at [https://translate.wordpress.org/projects/wp-plugins/lifterlms/language-packs](https://translate.wordpress.org/projects/wp-plugins/lifterlms/language-packs).\n+ Removed bundled en_US translation files because LifterLMS is in English so the files are unnecessary.\n+ Fixed a few mis-labeled filters applied when registering LifterLMS Custom Post Types\n+ Adjusted the default supported features of LifterLMS Quizzes and Questions\n  + Quizzes now support custom fields as per user request\n  + Commenting, thumbnails, and excerpts are no longer \"supported\" as they were never intended to be and were never correctly implemented.\n    + If you are relying on any of these features for your quizzes or questions please use the following filters to re-implement these features: `lifterlms_register_post_type_quiz` or `lifterlms_register_post_type_question`. These will allow you filter the default arguments LifterLMS passes to the WordPress function `register_post_type()`\n\n\nv2.7.3 - 2016-05-23\n-------------------\n\n+ Added a separate filter for login redirects `lifterlms_login_redirect` and added the user_id as a second parameter available to the filter.\n+ Added second parameter to `lifterlms_registration_redirect` to allow access to the registered user's user_id.\n+ Fixed a timestamp conversion issue on Course sale price checks that caused indefinite sales (those with no date restrictions) to appear not on sale during certain periods of time. The period would differ depending on the server's timezone settings and the time of visit.\n+ Added a \"Pointer\" when hovering quiz summary accordion to allow for a slightly more obvious user experience that the elements are expandable.\n+ Added some new localization methods to ensure strings that only appear in Javascript files will be translator friendly. This initially fixes a few issues on the Quiz Summary page and during quiz taking where strings only appeared in Javascript and were, therefore, completely inaccessible to translators.\n\n\nv2.7.2 - 2016-05-19\n-------------------\n\n+ In course syllabus widget & shortcodes free lessons will now be clickable links.\n+ Record `llms_last_login` timestamp in usermeta when a user registers.\n\n\nv2.7.1 - 2016-05-09\n-------------------\n\n##### Enrollment & Voucher Checks\n\n+ Enrollment functions will now automatically check to ensure that users are not already enrolled in a course or membership before enrolling. This addresses an issue which would create double enrollment for user redeeming a voucher for a product they were already enrolled in.\n+ Vouchers will now automatically check to see if the user has already redeemed this voucher before allowing the user to redeem it. This would have caused multiple enrollments and would allow one user to eat up an entire voucher by using it over and over again for funsies. A voucher can now *only* be redeemed once by a user as intended.\n+ `llms_is_user_enrolled()` now allows developers to check membership enrollment. Previously this function would only check enrollment of Courses despite what the documentation stated.\n\n##### Translation\n\n+ 3 strings have had translation functions added to them. This makes LifterLMS voucher redemptions translatable!\n\n##### Bugs & Fixes\n\n+ Fix javascript dependency & enqueueing issue on admin panel which prevented LifterLMS settings from saving correctly in various places\n+ Removed inline CSS from \"next lesson button\" on quiz completion / summary screen. This was overriding some default styles and making the button very thin and gross.\n\n\nv2.7.0 - 2016-05-05\n-------------------\n\n##### LifterLMS Custom User Fields Exposed\n\n+ Custom fields added during registration via LifterLMS account settings are now exposed on the admin panel via the student's WordPress user profile\n+ All custom fields that are available (billing and phone) are editable on the WordPress user profile by anyone with profile edit access regardless of LifterLMS settings. If the settings are disabled (eg not required for registration) you can still add this information manually to a user's profile. This is useful if you require the information and then disable it later, you would still be able to access the information on the admin panel but would no longer be required for user's during registration.\n+ A few new filters added to help developers customize the experience here. Check out the documentation at [https://lifterlms.com/docs/lifterlms-filters/#admin-user-custom-fields](https://lifterlms.com/docs/lifterlms-filters/#admin-user-custom-fields)\n\n##### Membership Manual Add & Remove Student Functions\n\n+ Duplicated \"Students\" tab from the Course admin screen to Memberships\n  + Students can be manually added to a membership by an admin\n  + Students can be removed manually from a membership by an admin\n\n##### Updates\n\n+ Added the ability for students to edit their phone number via their account settings page if the phone number registration option is enabled on the site.\n\n##### Fixes\n\n+ Fixed a few spelling errors on LifterLMS admin panel order screens\n+ Fixed a typo on meta data for LifterLMS admin created (manual) orders\n\n\nv2.6.3 - 2016-05-02\n-------------------\n\n+ Removed redirecting action from WooCommerce integration that was causing issues on multiple product purchase checkouts with larger databases.\n+ Added a new payment action `lifterlms_order_complete` which runs at the same time as some previous actions during payment processing but servers a different purpose. This is mostly in preparation for a forthcoming AffiliateWP integration.\n+ Fixed an issue with LifterLMS certificate background image that caused the wrong dimensions to be returned when outputting a LifterLMS certificate background image\n\n\nv2.6.2 - 2016-04-27\n-------------------\n\n+ Fix class conflict in collapsible course outline widget template which caused some UX issues.\n+ Added new filters run during course & lesson sidebar registration to allow customization of LifterLMS sidebars\n  + `lifterlms_register_course_sidebar`\n  + `lifterlms_register_lesson_sidebar`\n+ Removed a stray logging function.\n+ Cleaned up some undefined variable warnings & notices on the quiz summary template\n+ Fixed an issue appearing when registering users did not submit the optional phone number which caused a PHP notice\n+ LifterLMS Orders generated by WooCommerce will now have a payment method of \"WooCommerce\". This also addresses an undefined notice produced during WooCommerce order completion because a LifterLMS Payment Method wasn't being defined.\n\n\nv2.6.1 - 2016-04-26\n-------------------\n\n+ Fix class conflict in collapsible course outline widget template which caused some UX issues.\n\n\nv2.6.0 - 2016-04-25\n-------------------\n\n##### Collapsible Course Outline Widget\n\n+ By request we've added an option to make your course outline widgets collapsible!\n+ View feature [Documentation](https://lifterlms.com/docs/course-syllabus-widget/)\n+ New translations available related to feature. I think it's 4 strings.\n\n##### Bug Fixes\n\n+ Removed an unused CSS selector that caused some issues on the admin panel. This resolves an issue identified with the Page Builder by SiteOrigin plugin. The selector was very generic (`.title`) and may have caused issues with other themes or plugins using that class.\n+ Resolved an issue that prevented post update, save, and publishing messages for core post types (posts, pages) from displaying properly.\n\n\nv2.5.1 - 2016-04-22\n-------------------\n\n+ Fixed session handler initialization as it was being initialized prior to user data availability.\n+ Staged `LLMS_Language` class  for deprecation in favor of WordPress translation functions `__()`, `_e()`, etc... **If you're a developer you'll start seeing warning's on screen or in your logs if you're using this function, it will be completely removed in the next MAJOR release (3.0.0)**\n+ Added a new function to handle the deprecation warning above (`llms_deprecated_function`) and now that we have this function we'll start deprecating all the things. Just kidding, or am I?\n+ This gives translators access to 69 new strings that were previously untranslatable! However, this number might be inaccurate +/- 5 strings. I only counted it once and I don't feel like the exact number is important enough for a recount to ensure accuracy. /shrug\n\n\nv2.5.0 - 2016-04-15\n-------------------\n\n**Admin Panel Order Table Updates**\n\n+ Several visual improvements to the table\n+ Exposed the following fields on the table\n  + Order number\n  + Customer name (with a link to their WP profile)\n  + Customer email (mailto link)\n  + Payment gateway used (this is filterable per gateway as well so gateways can improve the functionality here in the future)\n+ Added a link to the product edit page from the product column\n+ Free orders will now display as \"Free\" as opposed to {currency}0.00\n+ Removed the not-so-useful \"Order\" column which was a long ugly string of data that was displayed in other columns already\n+ Removed the \"Password Protected\" flag since *all* orders are always automatically password protected for added security. This flag distracts from the interface so we've removed it. Orders _are_ still password protected though.\n+ Numerous strings that were previously not translatable have been made translatable on this screen\n+ A few new strings that previously didn't exist are now available for translation\n\n**Fixes and other small changes**\n\n+ Fixed a translation issue on the LifterLMS menu that we thought we fixed in the last release but have now really fixed (probably).\n+ Fixed a few small issues with engagements as they related to external engagements triggered by other plugins and LifterLMS extensions.\n+ Tired of seeing a banner for a plugin you've already installed? We have your back! The general settings area will now only display banners for plugins that aren't installed.\n+ Fixed various javascript issues, mostly removed `console.log()` statements.\n+ Fixed a spelling error on the membership admin panel settings screen\n\n\nv2.4.1 - 2016-04-07\n-------------------\n\n+ Tested and compatible with WordPress 4.5 Release Candidate.\n+ Fixed a pagination issue related to updates to the quiz builder from 2.4.0 which would cause results to return incorrect results on the last page of paginated results in the \"Add Question\" dropdown.\n+ Added translation functions to LifterLMS Menu Items. Resolves an issue where translated LifterLMS installations might not see all the menu items under the LifterLMS Icon.\n+ Italian translation updates courtesy of [@AndreaBarghigiani](https://github.com/AndreaBarghigiani)\n+ On some themes the \"Next Lesson\" button was displayed while quizzes were being taken. We now *always* hide the next lesson button when a quiz is being taken.\n+ Adjusted some static functions to be non static in `class.llms.post-types.php`\n+ Added a function to ensure support for post thumbnails on LifterLMS custom post types\n+ If a user views a course that is available to them because it belongs to a membership level they are a member of, course pricing information will no longer be visible. This addresses a confusing user experience issue. Previously it _appeared_ like payment for a course was still required even though it really wasn't.\n+ Fixed undefined variable warning on quiz summary screen\n+ Resolve an issue with quiz timer that caused issues on time display if the time limit was set to a fraction of a minute (eg 1.5 minutes)\n+ resolved an undefined variable warning resulting from courses still holding a reference to a membership after the membership has been deleted or trashed\n\n\nv2.4.0 - 2016-03-29\n-------------------\n\n##### Performance Improvements on the LifterLMS Quiz Builder\n\n+ Completely rewrote Javascript associated with building a LifterLMS Quiz. Our users have been identifying some performance issues and slowness when working with larger databases. We've refactored the Javascript and our related database queries to allow faster quiz building and fewer timeouts when working in the quiz builder.\n+ Fixed a bunch of undefined variables that would produce PHP warnings in various quiz templates\n+ Added validation to quiz questions on the admin panel to prevent the same question from being added to a quiz multiple times.\n+ Fixed an issue that prevented quizzes from correctly marking the lesson as completed when the quiz was passed.\n+ Added three new actions now available for developers to hook into.\n  + `lifterlms_quiz_completed` called upon completion of a quiz (regardless of grade)\n  + `lifterlms_quiz_passed` called when a quiz is completed with a passing grade\n  + `lifterlms_quiz_failed` called when a quiz is completed with a failing grade\n+ Course Progress and Course Syllabus shortcodes (and widgets) now work on Quiz pages\n+ Completed Metabox refactor for the LifterLMS Quiz post type and removed `LLMS_Meta_Box_Quiz_General` class. All functions now exist in `LLMS_Meta_Box_Quiz`\n+ Added validation to the Quiz general settings\n  + Cannot only enter numbers in attempts, percentage, and time limit fields\n  + Cannot enter a negative number or a number greater than 100 in the percentage field\n+ Removed the membership restriction metabox from quiz admin and question admin screens\n\n##### Other fixes\n\n+ Fixed an issue that caused multiple certificates awarded for the same Course or Lesson to not properly display on the My Account page.\n+ Removed an event bound to the publishing of a LifterLMS Question that called a function that didn't exist and caused a Javascript error on the console (but didn't actually cause any problems)\n+ Removed a warning message that would display on sidebars when a shortcode was being displayed in a place that it couldn't function. We now simply don't display any content if the shortcode can't function.\n+ Resolved an issue that prevent users from \"purchasing\" products when using a 100% coupon and the Stripe payment gateway. Users experiencing this issue should also update to Stripe 3.0.1.\n+ Fixed an AJAX related issue that was incompatible with PHP7\n+ Added the ability to have a \"max\" value on LifterLMS Admin Metabox number fields\n\n\nv2.3.0 - 2016-03-24\n-------------------\n\n##### Engagements Refactoring (lots of bugfixes, performance improvements, more hook & filter friendly)\n\n+ We've completely rewritten the LifterLMS Engagement Handler methods (`class LLMS_Engagements`) and added some new engagement actions.\n+ The rewrite unifies engagement handling into one function that can be easily hooked into by plugin and theme developers.\n+ We've moved any engagement related data out of the main `LifterLMS` class\n+ Fixed the broken engagement delay functionality which now runs of `wp_schedule_single_event`. This makes the function more reliable and also keeps it within the traditional WordPress architecture.\n+ Added an additional check before sending emails or triggering any engagements that will prevent the achievement from being awarded or the email from being sent if the post is in not published. This fixes an issue that caused emails in the trash from still being emailed.\n+ Removed the unused `LLMS_Engagements` class and file\n+ Added two new engagement trigger events \"Membership Purchased\" and \"Course Purchased\"\n+ Deprecated actions -- Removes some redundancy because the triggering actions (`lifterlms_course_completed` triggered the notification action, instead `lifterlms_course_completed` simply triggers the engagement now).\n  + `lifterlms_lesson_completed_notification`\n  + `lifterlms_section_completed_notification`\n  + `lifterlms_course_completed_notification`\n  + `lifterlms_course_track_completed_notification`\n  + `lifterlms_course_completed_notification`\n  + `lifterlms_user_purchased_product_notification`\n  + `lifterlms_created_person_notification`\n\n##### Bug and Issue fixes\n\n+ Adjusted the size of the LifterLMS Admin Menu Icon. It was super big because of, perhaps, some overcompensation. It caused an issue on Gravity Forms admin pages for some reason (we didn't ever determine why) but we've resolved it by using an appropriately sized icon.\n+ Fixed a CSS issue that caused some weirdness on the course archive page on mobile devices\n+ Fixed an issue with automated membership expirations\n+ Fixed a function that should have been called statically in `LLMS_Ajax` class\n+ Fixed a ton of issues related to the triggering of engagements and cleaned up a lot of classes and functions associated with them.\n+ Properly instantiate `LifterLMS` singleton via LLMS() function and prevent direct instantiation of the class via `new LifterLMS()`.\n+ Removed the deprecated 'class.llms.email.person.new.php' file as it was rendered useless a long time ago and caused some duplicate emails.\n\n\nv2.2.3 - 2016-03-15\n-------------------\n\n##### Translations\n\n+ Added translation functions around quite a few untranslated strings. Thanks to the team at [Netzstrategen](http://netzstrategen.com)\n+ Added German translation .mo and .po files again thanks to the team at [Netzstrategen](http://netzstrategen.com)\n\n##### Student Enrollment Functions\n\nWe've refactored a bit of our code related to how to programmatically enroll a student in a course or membership during registration and purchase.\n\nA new class `LLMS_Student` makes working with a LifterLMS student (user) a bit easier. We'll begin exposing user meta data through this class as we continue to improve the usability of the codebase for other developers.\n\nWe've also created a simple enrollment function `llms_enroll_student()` which enables programmatic enrollment to LifterLMS courses or memberships. This was previously handled in a pretty schizophrenic manner and this unifies various ways of enrollment into one clean function. All enrollment moving forward will use this functions.\n\nThe enrollment function calls a new action as well as calling existing enrollment-related actions:\n\n+ `before_llms_user_enrollment` - called immediately prior to beginning the user enrollment function\n+ `llms_user_enrolled_in_course` (previously existing)\n+ `llms_user_added_to_membership_level` (previously existing)\n\nThis also addresses an issue that prevented the `llms_user_enrolled_in_course` action from being called when a user was auto-enrolled in a course because they joined a membership level that included auto-enrollment in one or more courses.\n\n##### Bug and Issue fixes\n\n+ Fixed an inconsistency in the way membership IDs were being saved to the postmeta table that would cause courses to not *appear* restricted on the Membership Enrollment tab, even though they were actually restricted and functioning correctly.\n+ New lines are now preserved in the quiz question clarification text areas, thanks to @atimmer\n+ Escape HTML in the quiz question description fields on the admin panel to allow outputting html without rendering it, thanks @atimmer\n+ Fixed an issue related to the outputting of restricted course and membership content which caused errors on certain themes\n+ added a clearfix to the `.llms-lesson-preview` element on the course syllabus template\n+ Removed the `class.llms.person.handler.php` file as it wasn't actually being used by anything anywhere and contained no functions\n+ Removed some unused and deprecated class functions from the LLMS Student Metabox class\n+ Fixed an undefined javascript error resulting from code cleanup in 2.2.2. This issue prevented Vouchers from being published. The code has been further cleaned.\n\n\nv2.2.2 - 2016-03-15\n-------------------\n\n##### One step closer to a public GitHub repository\n\nWe've made a massive syntactical update to almost every file in the codebase for a (finally) unified and clearly defined coding standard. This puts us one step closer to beginning to open our GitHub repo publicly and accepting pull requests and contributions from developers everywhere.\n\nOkay, we haven't exactly _clearly_ defined it yet. We're working off a modified version of the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/).\n\nNotable exceptions are related to file names because Thomas Levy didn't have the energy to rename a bunch of files as well as ignoring the Yoda Conditions standards. We'll be fixing these deviations in the future.\n\n##### Quizzes\n\n+ Created new time calculation and humanizing functions related to the display of quiz time on quiz results pages\n+ Quizzes will now display hours, minutes, and seconds depending on the time it took to take the quiz\n+ Timing calculations are more accurate and quizzes that are completed in less than 60 seconds will not bug out and display incredibly long lengths\n+ Resolved an issue that occasionally prevented quiz data from saving during the last question causing the quiz to hang in an uncompletable state\n+ Quiz questions now have a default point value of 1, thanks @atimmer\n+ Quiz question answers now accept valid HTML as per `wp_kses_post`, thanks again to @atimmer\n\n##### Translations\n\n+ Thanks to @AndreaBarghigiani and the team at [codeat](http://codeat.co/) LifterLMS now ships with Italian language files!\n\n##### Issue and bug resolutions\n\n+ Fixed a restriction issue that would happen when individual lessons were restricted to a membership level\n+ Fixed an issue with the `[lifterlms_my_account]` shortcode that was preventing the shortcode from working on the Divi theme.\n+ Engagements will now only be triggered if they are \"Published\". Resolves an issue where draft or trashed engagements were still firing.\n+ Fixed CSS overflow on LifterLMS Meta boxes. Fixes an issue where select boxes would be hidden inside a metabox.\n+ Changed the ConvertKit extension banner image on the LifterLMS general settings page and replaced added a link to the extension now that it's available.\n+ Added a link to the new ConvertKit extension to the .org readme\n+ When restricting an entire site to a membership level the page selected as the \"Terms and Conditions\" page in LifterLMS settings will automatically bypass Membership restriction settings. This will allow your unregistered users to actually read the T&C that they're confirming during registration.\n+ CSS fix for `has-icon` class on course syllabus\n+ Fixed a PHP warning that displayed when purchasing a membership with no auto-enrollment courses\n+ Fixed an undefined variable warning in the WooCommerce integration class\n+ Fixed a few templating issues related to certificates\n+ Added a few new CSS rules that should make certificates more compatible across various themes\n+ Added a css class to LifterLMS Next Lesson buttons, `llms-next-lesson`\n+ Updated the scheduled event name for cleaning up LifterLMS session data from the WP database. It had a conflicting name with the scheduled event for expiring LifterLMS memberships.\n\n\nv2.2.1 - 2016-03-07\n-------------------\n\n+ Added a few actions to the `class.llms.voucher.php` class.\n\n\nv2.2.0 - 2016-03-04\n-------------------\n\n##### Translations\n\n+ We've updated our .pot file for the first time in quite a while. We're really sorry for de-emphasizing translation. An updated .pot file will now accompany each version of LifterLMS whenever a translatable string is adjusted or when a new string is added.\n+ We've also made it easier to include custom translations. Read our [Translation Guide](https://lifterlms.readme.io/docs/getting-started-with-translation).\n\n##### Certificate Background Images\n\n_We've completely rewritten the certificates template (but it's all backwards compatible)._\n\n+ New filters are available to make customizing the certificate template easier for developers. All new filters are documented at [https://lifterlms.readme.io/docs/functions-certificates](https://lifterlms.readme.io/docs/functions-certificates).\n+ A new WordPress Image Size is now available and will be used for generating the image used by default when uploading certificates to the media library. Fore more information on these new settings visit [https://lifterlms.com/docs/certificate-background-image-sizes/](https://lifterlms.com/docs/certificate-background-image-sizes/).\n\n##### Course and Membership Pricing & Sales\n\n+ Sale price start and end date are now completely optional.\n  + Provide neither a start date nor an end date to have a sale run indefinitely\n  + Provide a start date with no end date to have a sale start at a pre-determined time with no pre-determined ending\n  + Provide an end date with no start date to have a sale end a a pre-determined date but start immediately\n  + Provide a start date and an end date to have a sale run for a pre-determined period of time\n+ Optimized the `LLMS_Product` class to provide more reliable and extendable use of the class\n+ The templates related to pricing functions have been refactored. Affected templates include: \"templates/course/price.php\", \"templates/loop/price.php\", \"templates/membership/price.php\"\n+ Many people complained about the size of the `.llms-price` element on course and membership tiles on loop pages. We removed the inflated size and will now default to your theme for sizing. You selector remains the same if you wish to customize the size of the price text.\n\n##### Coupon Updates\n\n+ Coupons can (finally) be removed after being applied!\n+ Coupons can now be restricted to specific courses and/or memberships\n+ Percentage based coupons can no longer be created with a value larger than 100%\n+ Added numeric restrictions to usage and coupon amount fields on the admin panel\n+ Fixed a programmatic error that prevented product restrictions from being entirely removed\n+ Fixed a few instances where hardcoded a US Dollar symbol ($) where a dynamic currency symbol should have been displayed.\n\n##### Wow Bad Syntax, Very Typo, Such Grammar, So Undefined\n\n+ Fixed a typo in filter associated with modifying the registration of the lesson post type (`lifterlms_register_post_type_lesson`)\n+ Fixed a grammatical error in a Membership restriction message\n+ Fixed a syntax error in \"/templates/course/outline-list-small.php\" that prevented the `done` CSS class from being properly applied to completed lessons\n+ Fixed a few typos and grammatical errors on the Course and Membership settings metaboxes\n+ Fixed an undefined variable in \"templates/course/syllabus.php\"\n+ Fixed an issue on the system report that prevented the \"Courses Page\" from being reported properly\n+ Fixed an issue that caused PHP warnings on the admin panel for students or WP users with no LifterLMS menu permissions\n+ Fixed an installation warning caused by a reference to an undefined class variable\n+ Fixed an HTML character encoding issue that caused `&ndash;` to display on the admin panel when viewing LifterLMS Orders\n+ Fixed an undefined variable found during engagement triggering for non-email engagements.\n\n##### Additional, less exciting updates\n\n+ Added input type restrictions to course & membership price fields.\n+ The \"Emails\" LifterLMS Settings Tab has been renamed \"Engagements.\" All Email settings are found under this tab as well as some new settings related to other kinds of LifterLMS engagements.\n+ Added `the_content` filter to the content of emails sent by LifterLMS\n+ Fixed some CSS issues on Voucher screens\n+ Updated Courses settings retrieval function to retrieve the correct \"shop\" page id\n+ Added translation functions to voucher export meta box class\n+ Vouchers Export metabox will only allow export after a voucher has been published. This prevents an issue caused by attempting to export voucher codes before they were saved in the database via the publish / save action.\n+ Vouchers can no longer be saved with a use of \"0\"\n+ added a CSS class for various syllabus outputs that notes that the lesson has an icon. Previously CSS relied on \"is-complete\" to output styles for having an icon but with the addition of placeholders the \"is-complete\" is used only to note that the lesson is completed and \"has-icon\" is a more semantic class that applies to both complete and incomplete lessons with an icon.\n+ Removed the membership restriction metabox from some post types where it shouldn't have been displaying.\n+ admin select fields now have an option `allow_null` (default to \"true\") which can be set to `false` in order to prevent the output of the default \"None\" option\n\n\nv2.1.1 - 2016-02-15\n-------------------\n\n##### System Report\n\n+ A new LifterLMS Admin Page is available which reports information about various server, WordPress, and LifterLMS settings that will help expedite support requests.\n+ More information about the system report is available at [https://lifterlms.com/docs/how-to-use-the-lifterlms-system-report/](https://lifterlms.com/docs/how-to-use-the-lifterlms-system-report/)\n\n##### Additional Updates\n\n+ Fixed a javascript issue which prevented users from saving vouchers\n+ Cleaned up formatting in a large number of included PHP files\n\n\nv2.0.5 - 2016-02-15\n-------------------\n\n+ PayPal requests now using HTTP Version 1.1 in preparation for June 2016 [TLS 1.2 and HTTP/1.1 Updates](https://www.paypal-knowledge.com/infocenter/index?page=content&widgetview=true&id=FAQ1914&viewlocale=en_US). This resolves user's inability to begin PayPal checkout when using Sandbox mode.\n+ Updated deprecated function opt out to run off a constant that can be defined in `wp-config.php` instead of using a filter that is hard to use in the way that it is intended.\n\n\nv2.0.4 - 2016-02-15\n-------------------\n\n+ Fixed a typo on the `class_exists` check in the deprecated functions file\n+ added a filter so that progressive users can opt out of loading the deprecated functions file\n\n\nv2.0.3 - 2016-02-12\n-------------------\n\n+ Removed an unused quiz stub\n\n\nv2.0.2 - 2016-02-11\n-------------------\n\n+ Bugfix: removed a progressive syntax array that caused fatal errors on older versions of PHP\n\n\nv2.0.1 - 2016-02-11\n-------------------\n\n##### Updated General Settings Screen\n\n+ Improved the general settings interface to be more visually appealing and to provide some ad space to alert customers to other LifterLMS products and information.\n+ Moved Currency options to the Checkout settings screen\n\n##### Bug Fixes\n\n+ Properly initialized jQuery on the vouchers metabox admin scripts\n+ removed some php shortcut echos (`<?= $var; ?>`)\n+ Resolve issue where courses that are available with a membership or on it's own outside of the membership would prevent users from accessing content if they were not a member.\n+ Fixed a few files where undefined variables were being referenced and generating php notices\n+ removed an call to a WordPress core function that has never existed. Not sure what we were thinking there...\n\n###### Enhancements\n\n+ Updated CSS to provide better course syllabus layout on smaller screens\n+ Added validation to prevent against duplicate voucher code creation\n\n\nv2.0.0 - 2016-02-04\n-------------------\n\n##### Auto-advancing lessons\n\n+ We've heard your feedback and added a new global course option which will auto-advance a student to the next lesson upon lesson completion.\n\n##### Bug Fixes\n\n+ Added spaces between numbers and \"of\" on the counter for course syllabus templates\n+ Removed a template hook that was creating duplicate lesson thumbnails on quite a few themes\n\n##### Membership Admin Improvements\n\nVisit the \"Enrollment\" tab on any membership to see some new additions to make managing your memberships easier.\n\n+ You can now add courses to and remove courses from a Membership from the Membership itself\n+ You can now opt to automatically enroll students in a course (or multiple courses) when they sign up for a membership by checking \"Auto Enroll\" next to the course on the Membership enrollment tab\n\n##### Student Enrollment & Removal on Courses Admin Screen\n\nWe've updated the Students tab interface for performance and usability!\n\n+ AJAX enabled searching by student name and or email\n+ Increased performance for course page load by only calling student information when needed. This resolves a bug identified by users with large user databases and/or low-powered servers.\n+ Allow for addition or removal of several students at a time.\n\n##### Syllabus Template\n\n+ Added a Course setting to optionally enable Lesson Thumbnails on the Course Syllabus\n+ Added a Course setting Display greyed out lesson completion checkmark icons on lessons not competed in the course syllabus\n+ Reworded CSS on the course syllabus to rely on floats rather than absolute positioning, should allow for more robust customization with less frustration\n+ Refactored the syllabus template at \"templates/course/syllabus.php\" for better performance and readability\n\n##### Updates and enhancements\n\n+ User email is now displayed on the \"Students\" table on student analytics screens\n+ Membership now has it's own admin menu\n+ Reordered the LifterLMS admin menu and submenu items\n+ Removed membership specific taxonomies from courses\n+ Removed course specific taxonomies from memberships\n+ Coupon code is now a required field when creating a coupon\n+ \"Humbled\" the metabox on all post types that restricts the post to a membership. The metabox would previously gain priority over the WordPress publishing actions metabox. The priority has been reduced to \"default\" and will to fall into line with all other metaboxes on the screen and appear based on registration priority. If you can't find the metabox, SCROLL DOWN! If you want to put it back up on the top, you can simply drag it up there and WordPress will save your preference.\n\n##### Deprecated Classes\n\nWe've added a \"deprecated\" file which holds a few stubs for classes and functions deprecated below as to prevent fatal errors. The functions and classes in the deprecated class are classes which we know are being utilized by approved LifterLMS extensions and will allow users to upgrade LifterLMS without upgrade extensions without breaking their websites!\n\n+ `LLMS_Activate` which as previously used to activate the plugin for updates via the LifterLMS Update Server and is no longer required.\n+ PUC (plugin update checker) Library has been completely removed as it is no longer required for plugin updates.\n+ `LLMS_Analytics_Dashboard` was removed as it was a stub that was never used and shouldn't have ever been released as a part of the LifterLMS codebase. I can't believe no one reported this bug!\n\n##### Deprecated Functions\n\n+  `lifterlms_template_section_syllabus()`\n\n**The following are officially deprecated and removed to prevent WooCommerce compatibility conflicts**\n\n+ `is_shop()` replaced by `is_llms_shop()`\n+ `is_account_page()` replaced by `is_llms_account_page()`\n+ `is_checkout()` replaced by `is_llms_checkot()`\n\n##### Deprecated Templates\n\n+ templates/course/section_syllabus.php\n\n##### New Account Dashboard Filters\n\n*[View documentation for more information](https://lifterlms.readme.io/docs/filters-account)*\n\n+ `lifterlms_account_greeting`\n+ `lifterlms_my_courses_title`\n+ `lifterlms_my_courses_enrollment_status_html`\n+ `lifterlms_my_courses_start_date_html`\n+ `lifterlms_my_courses_course_button_text`\n+ `lifterlms_my_certificates_title`\n\n##### New Checkout Page Filters:\n\n*[View documentation for more information](https://lifterlms.readme.io/docs/filters-checkout)*\n\n+ `lifterlms_checkout_user_logged_in_output`\n+ `lifterlms_checkout_user_not_logged_in_output`\n\n##### New Course Filters:\n\n*[View documentation for more information](https://lifterlms.readme.io/docs/filters-course)*\n\n+ `lifterlms_product_purchase_account_redirect`\n+ `lifterlms_product_purchase_redirect_membership_required`\n+ `lifterlms_product_purchase_checkout_redirect`\n+ `lifterlms_product_purchase_membership_redirect`\n+ `lifterlms_lesson_complete_icon`\n\n\nv1.5.0 - 2016-01-22\n-------------------\n\n##### WooCommerce Integration Enhancements\n\n__NOTE: The following enhancements only apply when the WooCommerce Integration is enabled__\n\n**Always redirect to the WooCommerce Cart when a SKU Matched Product can be found**\n\n+ LifterLMS Products (courses and memberships) which are SKU matched to a WooCommerce product will now automatically add the related WooCommerce product to the WooCommerce shopping cart and then automatically redirect the visitor to the WooCommerce cart when the visitor attempts to enroll in a course or membership from the LifterLMS course or membership page.\n+ If no WooCommerce product is found via a SKU match, the user will proceed to the LifterLMS checkout.\n+ This will enable you to determine which Cart you want a user to use on a product by product basis. You may sell certain courses via WooCommerce and others via LifterLMS (should you choose to do so).\n\n**Multiple Item Checkout**\n\n+ When a WooCommerce order is complete user's will now be automatically enrolled in **all** courses and/or memberships in the WooCommerce order. This improves upon a previously limitation that would only allow WooCommerce checkout with one LifterLMS product at a time.\n+ The products in the order will be intelligently SKU matched to LifterLMS Courses or Memberships.\n+ You may also mix and match between WooCommerce products matched to LifterLMS products and those which are not matched to LifterLMS products. For example, your customers may now buy a Course via SKU matching as well as a T-Shirt that is not matched to a LifterLMS course via a SKU.\n\n##### Other Fixes and improvements\n\n+ Fixed a bug that caused quiz results to display for users who had never taken the quiz.\n+ Added Wistia as an oEmbed provider to fix an issue related to default oembed handling in WordPress 4.4.\n+ added a `.cc_cvv` class that mimics the existing `#cc_cvv` styles to allow gateway extensions to change the ID of the field in their credit card forms\n+ Added support for new 1.4.5 capability fixes to be also be reflected under the \"+New\" menu item in the WP Admin Bar. There are no changes to the filters, the capability filters will simply also remove restricted post types from the admin bar now (as they should).\n+ Tested and compatible up to WordPress 4.4.1\n\n##### Deprecations\n\n**The following functions have been staged for deprecation in LifterLMS 2.0!**\n\n+ Setup the `is_account_page()` function to be replaced by `is_llms_account_page()` function. The original causes conflicts when WooCommerce is installed as WooCommerce includes a core function by the same name. All references to `is_account_page()` in LifterLMS have been removed and the original has been left to prevent issues with developers currently relying on the LifterLMS version of the function.\n+ Setup the `is_checkout()` function to be replaced by `is_llms_checkout()` function. The original causes conflicts when WooCommerce is installed as WooCommerce includes a core function by the same name. All references to `is_checkout()` in LifterLMS have been removed and the original has been left to prevent issues with developers currently relying on the LifterLMS version of the function.\n\n\nv1.4.5 - 2016-01-13\n-------------------\n\n+ Significant improvements to LifterLMS admin permissions as well as a hardening of permissions. Previously LifterLMS admin screens and menus were available to any users with `edit_posts` capabilities. This has been changed to `manage_options`. Filters for all screens and menus have been added with this release. If you're site currently relies on users with `edit_posts` to be able to access LifterLMS settings and analytics screens you must utilize these new filters in order to maintain their access. Please see full documentation on the new filters at [https://lifterlms.readme.io/docs/filters-admin-menu-and-screen-permissions](https://lifterlms.readme.io/docs/filters-admin-menu-and-screen-permissions). **Please consider testing your changes outside of production before updating to LifterLMS 1.4.5 in production.**\n+ Allow \"Payment Method\" to be translated on the \"Confirm Payment\" screen\n+ Allow the name of the payment gateway to be filtered on the \"Confirm Payment\" screen\n+ Added pagination support to lifterlms membership archive pages\n+ Fixed a bug related to some required global variables for quizzes and lessons being incorrectly set on certain hosts\n+ updated readme file to remove incomplete documentation\n+ Added Chosen multi-select options to admin panel metaboxes (settings and posts)\n+ Added two new actions that developers can hook into:\n  + `llms_user_enrolled_in_course`, called when users are enrolled in a course. Usage details available [here](https://lifterlms.readme.io/docs/actions-user#llms_user_enrolled_in_course).\n  + `llms_user_added_to_membership_level`, called when users are added to a membership level. Usage details available [here](https://lifterlms.readme.io/docs/actions-user#llms_user_added_to_membership_level).\n\n\nv1.4.4 - 2015-12-21\n-------------------\n\n##### Updates\n\n+ My account page can now (optionally) display a list of memberships a student is currently enrolled in\n+ Student analytics on the admin panel display student's Memberships\n+ Student analytics on the admin panel will now display student's progress through courses in addition to their current enrollment status.\n+ Custom taxonomy archive templates for Course tags, categories, tracks, and difficulties now exist and properly function.\n+ Custom taxonomy archive templates for Membership categories and tags now exist and properly function.\n+ Added the `[lifterlms_memberships]` shortcode which was documented but never implemented. Details on usage available at [https://lifterlms.readme.io/docs/short-codes#memberships-lifterlms_memberships](https://lifterlms.readme.io/docs/short-codes#memberships-lifterlms_memberships)\n+ Added basic styles to LifterLMS pagination HTML elements (elements with class `.llms-pagination`) which formerly had no associated CSS.\n\n##### Deprecations\n\n+ Setup the `is_shop()` function to be replaced by `is_llms_shop()` function. The original causes conflicts when WooCommerce is installed as WooCommerce includes a core function by the same name. All references to `is_shop()` in LifterLMS have been removed and the original has been left to prevent issues with developers currently relying on the LifterLMS version of the function. It *will* be removed in the next major update (2.0) and will be noted as an officially deprecated feature at that time.\n\n##### Bug fixes\n\n+ Fixed pagination issues when using the `[lifterlms_courses]` shortcode\n+ Fixed an issue with the `is_shop()` function that prevented courses per page option from functioning properly on the default course archive page\n+ Student analytics profile on admin panel will display the correct number of memberships the student is enrolled in.\n+ Fixed a small CSS issue that caused extra white space to be displayed above Course or Membership tiles on archive pages when using the WordPress Twentyfifteen default theme\n\n##### Miscellaneous\n\n+ Account settings screen displays the correct title (\"Account Settings\" it previously said \"Archive Settings\")\n+ Made language changes to the LifterLMS settings intro screen copy\n+ Added link to CourseClinic on settings intro screen\n+ Added link to LifterLMS documentation on the settings intro screen\n\n\nv1.4.3 - 2015-12-11\n-------------------\n\n+ Fixed an issue that could prevent some older servers from being able to run LifterLMS\n\n\nv1.4.2 - 2015-12-10\n-------------------\n\n+ Tested and compatible with WordPress version 4.4\n+ BugFixes: fixed issue in `llms_featured_img()` that was preventing the `$size` variable from being passed to the WP core function being utilized.\n+ BugFixes: correctly handling conflicts with Plugin Update library\n\n\nv1.4.1 - 2015-12-02\n-------------------\n+ Feature: Custom single price text - Display custom text for the single price on the courses and course page. Custom field does not require a single payment price be set. IE: Free!\n+ Feature: Custom Purchase Course Button Text Option. Change the text of the Take This Course button in Settings->Courses.\n+ Feature: New Become A Member button on courses that are restricted to memberships.\n+ Feature: Custom Become A Member Text Option. Change the text of the become a member button in Settings->Courses.\n+ Feature: Paypal Debug Mode. Enable debug mode in Settings->Gateways to view responses from Paypal API when errors occur.\n+ Updates: Updated support links in Settings->General.\n+ Updates: added minor styling to course page to increase margin and padding for some themes.\n+ Updates: Achievement content now available to pull into custom templates. The Achievement content is not by default displayed but can now be used in custom templates.\n+ BugFixes: Resolved issue with no default price selected at checkout when only recurring option existed.\n+ BugFixes: Lesson prerequisite now alert the user and provide a link to redirect the user to the next required lesson in the course.\n+ BugFixes: Paypal errors now return error message instead of white screen when Paypal API fails.\n+ BugFixes: Corrected JavaScript error with modals on course edit page in Internet Explorer 11.\n\n\nv1.4.0 - 2015-10-29\n-------------------\n+ Feature: Free lessons - demo lessons that can be taken at any time by any user\n+ Feature: Guest lessons - demo lessons that can be taken by a non-logged in user\n+ Feature: Random quiz question - quiz questions can now be set to be in user set order or random order\n+ Updates: Automatically registers appropriate sidebars for Genesis theme\n+ Updates: Backend file cleanup\n+ Updates: Text cleanup\n+ Updates: Adds greater localization support (more strings to translate! yay!)\n+ Updates: Cleans up some unnecessary console.log() calls\n+ Updates: Removes mass of commented out code (cleaner reading)\n+ Updates: 'Next Lesson' button added after successful completion of quiz\n+ Updates: 'Next Lesson' button at bottom of lesson properly gets starting lesson of next section at the end of the previous section\n+ Updates: 'Previous Lesson' button at bottom of lesson will now properly get last lesson of previous section (if applicable)\n+ Updates: Move Registration Form to global templates to allow users to disable registration on login page but use registration form on custom page.\n+ BugFixes: WordPress pages are now properly restricted by memberships\n+ BugFixes: Fixes bug that caused order screen to act up if user was deleted\n+ BugFixes: Resolves nasty little bug that caused syllabus numbers to be out of whack\n+ BugFixes: Resolved error with WooCommerce integration where courses would not always register the user\n+ BugFixes: Corrected CSS conflict with Bridge theme settings page\n\n\nv1.3.10 - 2015-10-15\n--------------------\n+ Updates: Clarifies some prerequisite text\n+ Updates: Quiz questions are now randomized!\n+ Updates: Fixes small CSS issue\n+ BugFixes: Resolves fatal errors with a small subset of premium themes\n\n\nv1.3.9 - 2015-10-5\n------------------\n+ BugFixes: Removes conflict with Yoast SEO\n+ BugFixes: Fixes CSS issues with box-sizing takeover\n+ Feature: New Settings Tile: Session Management. Found at LifterLMS->Settings->General.\n+ Feature: Clear User Session Tool. You can now clear all LifterLMS user session data from your site in LifterLMS->Settings->General\n+ Updates: Backend code cleanup\n\n\nv1.3.8 - 2015-10-02\n-------------------\n+ BugFixes: Fixes Random error notices\n+ Updates: Updates email template handler\n\n\nv1.3.7 - 2015-09-25\n-------------------\n+ Updates: Adds Spanish translation\n+ Updates: Adds new filter 'lifterlms_single_payment_text' to customize single payment string on checkout\n+ Updates: Student analytics now indicate which courses a student has completed\n+ BugFixes: Resolved security issue with WordPress searches and lessons\n+ BugFixes: Fixes analytics bug that potentially arises after a course is deleted\n\n\nv1.3.6 - 2015-09-18\n-------------------\n+ BugFixes: Fixes pesky Zend Error that plagued some unfortunate victims\n+ BugFixes: Students can now be properly deleted from the course\n+ BugFixes: Fixes random class redeclaration error messages\n+ Updates: Adds new filter 'lifterlms_quiz_passed' to customize 'Passed' text after quiz\n+ Updates: Adds new filter 'lifterlms_quiz_failed' to customize 'Failed' text after quiz\n\n\nv1.3.5 - 2015-09-11\n-------------------\n+ Revisions: Fixes typos\n+ Updates: Adds sidebar functionality to various themes\n\n\nv1.3.4 - 2015-09-04\n-------------------\n+ BugFixes: Fixes bug with featured image on course page\n+ BugFixes: Fixes issue with lesson completed percentage on analytics page\n\n\nv1.3.3 - 2015-09-01\n-------------------\n+ Updates: Removes deprecated plugin updater\n+ Updates: Adds Course Track prerequisite\n+ Updates: Various text fixes\n+ BugFixes: Fixes lesson name on prerequisite notification\n+ BugFixes: Fixes critical error with WordPress customizer\n\n\nv1.3.2 - 2015-08-30\n-------------------\n+ Hotfix: resolves issues with sidebar shortcodes\n+ Updates: Text clarifications\n\n\nv1.3.1 - 2015-08-28\n-------------------\n+ Hotfix: resolves issue with ajax url\n\n\nv1.3.0 - 2015-08-28\n-------------------\n+ Improved popover behavior in course creation.\n+ BugFixing. Prevent multiple lesson and section form submission\n+ Fixed typos at backend quiz page\n+ Fixed check for update bug when plugin isn't properly activated.\n+ BugFixing, quiz post type should show author metabox\n+ Added course category filter to lifter_lms shortcode\n+ BugFixing, typo in [lifterlms_course_progress shortcode]\n+ BugFixing, Analytics shouldn't fetch students meta info from users were deleted.\n+ Adds in basic review functionality\n+ Updates plugin-updater to remedy PHP conflicts\n+ Fixes date bug in Analytics\n+ Cleans up jQuery console messages\n+ Adds in course tracks\n\n\nv1.2.8 - 2015-07-17\n-------------------\n+ Updated Portuguese translation file\n+ Fixed issue where quiz score could not be equal to required grade.\n+ New Feature: Quiz Results Summary. Display the quiz results to the user on quiz completion.\n+ New feature: Clarification. Display information about correct and incorrect answers to users\n+ New Feature: Display correct answers to user on quiz completion\n+ Removed ability to add negative time limit to quiz\n+ New Membership feature: Make membership archive links go directly to checkout. Setting allows you to skip membership sales page and send users directly to registration and checkout.\n+ Sidebar support for prototype theme\n+ Sidebar support for X theme\n+ Sidebar support for WooCanvas\n+ New Shortcode: [lifterlms_hide_content]: Use to restrict content on a page, course or lesson to a specific membership. Pass the post id of the membership you want to restrict the content to. Example: [lifterlms_hide_content membership=\"5\"]\n+ New updates to gulp build process\n+ Class autoloading and LLMS namespace introduced for more efficient coding.\n\n\nv1.2.7 - 2015-06-05\n-------------------\n+ Minor bug fix with lesson redirect to quiz\n+ Minor change to global Course object instantiation.\n+ Bug Fix: Remove student from course\n+ Bug Fix: Appearance Menus missing select field (THANKS ANDREA!)\n+ New Course Setting: Hide Course Outline on course page\n+ New Shortcode: [lifterlms_course_outline] - displays course outline with settings (see documentation)\n+ Membership metabox design update\n+ Certificate metabox design update\n+ Achievement metabox design update\n+ Lesson metabox design update\n+ Emails metabox design update\n+ Coupons metabox design update\n+ Update to certificate design (better alignment and theme functionality)\n+ Better theme sidebar support\n+ More awesome control for developers building new settings for LifterLMS\n+ Advanced filter system for metabox fields with finite control for 3rd party developers.\n+ Woocommerce conflict correction to archive templates\n+ Style updates to allow themes better control on design\n\n\nv1.2.6 - 2015-04-28\n-------------------\n+ Corrected issue with lesson re-order on save\n+ corrected html formatting issue on purchase page\n+ corrected html formatting issue on course page\n\n\nv1.2.5 - 2015-04-23\n-------------------\n+ Corrected excerpt to not pull in lesson navigation\n+ Modified metabox api for better extension integration\n+ Corrected issue with order not displaying all information if coupon was not applied to order\n\n\nv1.2.4 - 2015-04-22\n-------------------\n+ Moved All Course metaboxes to global Course Options Metabox\n+ Move Enrolled and Non-Enrolled user wysiwyg post editors to Options Metabox\n+ Removed Course Syllabus metabox, Added Course Outline Metabox\n+ Set priority of Course Outline and Course Options Metabox to top\n+ Added ability to Create new section to Course Outline\n+ Added ability to Create new lesson to Course Outline\n+ Added ability to add existing Lesson to Course Outline\n+ Added Lesson duplicate functionality when adding lesson previously assigned to another course.\n+ Added ability to drag lessons between sections in Course Outline\n+ Added ability to edit Section Title in Course Outline\n+ Added ability to edit lesson title and excerpt in Course Outline\n+ Added New Style and Design for better usability to Course Outline\n+ Added Lesson Icon with tooltip to Course Outline: Prerequisite - shows if prerequisite exists and displays name of prerequisite\n+ Added Lesson Icon with tooltip to Course Outline: Quiz - shows if quiz is assigned to course and displays name of quiz\n+ Added Lesson Icon with tooltip to Course Outline: Drip Content - shows if drip days are set and # of days\n+ Added Lesson Icon with tooltip to Course Outline: Content - displays if lesson has content added.\n+ Added Course Outline Metabox to Lesson Post Editor: Allows you to assign lesson to section and view entire course tree. Links to Course and all other lessons in course.\n+ Style Update: backgrounds on frontend. Removed all references to white background on front end elements\n+ Corrected Restriction for course in past. Updated course in past message to display as Course ended instead of Course not available until.\n+ Added restriction message when user attempts to visit a restricted lesson.\n+ Updated course syllabus sidebar widget to not display lessons as links if user is not enrolled in course.\n+ Added ability to use Attribute Order for sorting Courses and Memberships on Archive pages.\n+ Added support for selling memberships with Woocommerce. LifterLMS now checks memberships for SKU matches in addition to Courses when products are purchased using WooCommerce.\n+ Added gulp for scss, js and svg management\n+ Added svg sprite and svg class for managing svg elements on front and backend.\n+ Added better language translation support for strings\n+ Refactored Ajax Classes for cleaner, faster development\n+ Refactored metabox build class for cleaner, faster development\n+ Refactored Course syllabus to reduce query size for larger, complex courses\n+ Added Handler classes for Lessons, Sections, Courses and Posts\n+ Refactored Course get / set methods to reduce database queries\n\n\nv1.2.3 - 2015-03-12\n-------------------\n+ Achievement design and functionality updates\n+ Achievement shortcode added\n+ Better searching added to engagement screen\n+ Achievement bug fixes\n+ On screen error reporting added to activation for trouble shooting\n+ Custom engagement methods added to certificate, achievement and sections\n+ Corrected new user registration engagement bug\n+ LifterLMS access reduced from manage_options to edit_posts\n+ Filters added to analytics to allow custom development\n+ Engagement bug fix: Section and Lesson bug select\n+ Syllabus bug corrected: No longer displays lessons in section box if no sections exist.\n+ Removed depreciated achievement template\n+ Membership Bug fix: Membership restriction will now only display on single posts.\n\n\nv1.2.2 - 2015-02-23\n-------------------\n+ Corrected drip content bug\n+ Added Ajax functionality to quiz\n+ rounded quiz grades\n+ Added quiz time limit setting to Quiz\n+ Added quiz timer to quiz, front end\n+ Quiz allowed attempts field now allows unlimited attempts\n+ Set Ajax lesson delete method to not return empty lesson value\n+ Set next and previous questions to display below quiz question\n+ Decoupled Single option select question type from quiz to allow for more question types\n+ Added Quiz time limit to display on Quiz page\n+ Added functionality to automatically complete quiz when quiz timer reaches 0\n+ Moved Quiz functionality methods from front end forms class to Quiz class\n\nv1.2.1 - 2015-02-19\n-------------------\n+ Updated settings page theming\n+ Added Set up Quick Start Guide\n+ Added Plugin Deactivation Option\n+ Updated language POT file\n+ Added Portuguese language support. Thank you Fernando Cassino for the translation :)\n\n\nv1.2.0 - 2015-02-17\n-------------------\n+ Admin Course Analytics Dashboard Page. View at LifterLMS->Analytics->Course\n+ Admin Sales Analytics Dashboard Page. View at LifterLMS->Analytics->Sales\n+ Admin Memberships Analytics Dashboard Page. View at LifterLMS->Analytics->Memberships\n+ Admin Students Search Page. View at LifterLMS->Students\n+ Admin Student Profile Page ( View user information related to courses and memberships )\n+ Lesson and Course Sidebar Widgets ( Syllabus, Course Progress )\n+ Course Syllabus: Lesson blocks greyed out. Clicking lesson displays message to take course.\n+ Misc. Front end bug fixes\n+ Misc. Admin bug fixes\n+ Course and Lesson prerequisites: Can no longer select a prerequisite without marking \"Has Prerequisite\"\n+ Admin CSS updates\n+ Better Session Management\n+ Number and Date formatting handled by separate classes to provide consistent date formats across system\n+ Zero dollar coupon management: Coupons that set total to 0 will bypass payment gateway, generate order and enroll users.\n+ Better coupon verification.\n+ Better third party payment gateway support. Third party gateway plugins are now easier to develop and integrate.\n+ User Registration: Phone Number Registration field option now available in Accounts settings page.\n\n\nv1.1.2 - 2014-12-18\n-------------------\n+ Moved Sidebar registration from plugin install to init\n\n\nv1.1.1 - 2014-12-16\n-------------------\n+ Added user registration settings to require users to agree to Terms and Conditions on user registration\n+ Added comments to all classes methods and functions\n+ Removed unused and depreciated methods\n+ Added Lesson and Course Sidebar Widget Areas\n+ Fixed bug with course capacity option\n+ Fixed bug with endpoint rewrite\n+ Added localization POT file and us_EN.po translation file\n\n\nv1.1.0 - 2014-12-08\n-------------------\n+ Updated HTML / CSS on Registration form\n+ Added Coupon Creation\n+ Added Coupon support for checkout processing\n+ Added Credit Card Support processing support\n+ Added Form filters for external integration\n+ Added Form templates for external integration\n+ Added Account Setting: Require First and Last Name on registration\n+ Added Account Setting: Require Billing Address on registration\n+ Added Account Setting: Require users to validate email address (double entry)\n+ Added password validation (double entry) on user registration / account creation\n+ Added Quiz Question post type and associated metaboxes\n+ Added Quiz post type and associated metaboxes\n+ Added ability to assign a quiz to a lesson\n+ Added front end quiz functionality\n+ Added Course capacity (limit # of students)\n\n### User Admin Table\n+ Added Membership Custom Column that displays user's membership information\n+ Added \"Last Login\" custom column that displays user's last login date/time\n\n### User Roles\n+ Updated user role from \"person\" to \"student\"\n+ Added temporary migration function to transition any register users with \"person\" role to \"student\" role\n+ Added \"Student\" role install function\n\n\n### BUDDYPRESS\n+ BuddyPress Screen Permission Fix\n+ Added two additional screens to BuddyPress: Certificates and Achievements\n\n### MISC\n+ Added llms options for course archive pagination and added course archive page pagination template\n+ Added user statistics shortcode\n\n\nv1.0.5 - 2014-11-12\n-------------------\n\n+ Fixed a mis-placed parenthesis in templates/course/lesson-navigation.php related to outputting excerpt in navigation option\n+ Changed theme override template directory from /llms to /lifterlms\n+ Update the position & name of the \"My Courses\" Menu in BuddyPress Compatibility file\n+ New meta_key _parent_section added for easier connection and quicker queries.\n+ Section sorting on course syllabus\n+ Edit links added to course syllabus\n+ Assign section to course and view associated lessons metabox added to sections\n+ Assign lesson to section and view associated lessons metabox added to lessons\n+ Assigned Course, Assigned Section, Prerequisite and Membership Required added to lesson edit grid\n+ Assigned Course added to section edit grid'\n+ New membership setting: Restrict Entire Site by Membership Level (allows site restriction to everything but membership purchase and account).\n+ Updated template overriding to check child & parent themes\n+ Updated template overriding to apply filters to directories to check for overrides to allow themes and plugins to add their own directories\n\n\nv1.0.4 - 2014-11-04\n-------------------\n\n+ Templating bug fix\n+ Added shortcode and autop support to course and lesson content / excerpt\n\n\nv1.0.3 - 2014-11-04\n-------------------\n\n+ Major Templating Update!\n+ Removed Course, Lesson and Membership single lesson templates.\n+ Course and Section content templates now filter through WP content\n\n\nv1.0.2 - 2014-10-31\n-------------------\n\n+ Added lesson short description to previous lesson preview links -- it was rendering on \"Next\" but not \"Previous\"\n+ Added a class to course shop links wrapper to signify the course has been completed\n+ Removed an unnecessary CSS rule related to the progress bar\n\n\nv1.0.2 - 2014-10-30\n-------------------\n\n+ Fixed SSL certificate issues when retrieving data from https://lifterlms.com\n+ Added rocket settings icon back into repo\n\n\nv1.0.1 - 2014-10-30\n-------------------\n\n+ Updated activation endpoint url to point towards live server rather than dev\n\n\nv1.0.0 - 2014-10-30\n-------------------\n\n+ Initial public release.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 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 General Public License is a free, copyleft license for\nsoftware and other kinds of works.\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,\nthe GNU General Public License is 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.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\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  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\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 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. Use with the GNU Affero General Public License.\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 Affero 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 special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe 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 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 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 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 General Public License as published by\n    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 General Public License for more details.\n\n    You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    {project}  Copyright (C) {year}  {fullname}\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\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 GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n  <img src=\".github/lifterlms-logo.png\" alt=\"LifterLMS logo\" width=\"300\">\n</h1>\n\n<p align=\"center\"><a href=\"https://lifterlms.com\" title=\"LifterLMS website external link\">LifterLMS</a> is a powerful WordPress learning management system plugin that makes it easy to create, sell, and protect engaging online courses and training based membership websites.</p>\n\n<hr />\n\n<div align=\"center\">\n\n[![WordPress Plugin Version][img-wp-plugin]][link-wp-repo]\n[![WordPress Plugin Tested WP Version][img-wp-tested]][link-wp-repo]\n[![PHP Supported Version][img-php]][link-php]\n\n[![WordPress Plugin Rating][img-wp-rating]][link-wp-reviews]\n[![WordPress Plugin Downloads][img-wp-downloads]][link-wp-advanced]\n[![WordPress Plugin Active Installs][img-wp-installs]][link-wp-advanced]\n\n[![PHPUnit Tests][img-phpunit-tests]][link-phpunit-tests]\n[![PHPCS Coding Standards][img-phpcs-checks]][link-phpcs-checks]\n[![Code Climate maintainability][img-cc-maintainability]][link-cc]\n[![Code Climate test coverage][img-cc-coverage]][link-cc-coverage]\n\n[![Contributions Welcome][img-contributions-welcome]](.github/CONTRIBUTING.md)\n[![Contributors][img-contributors]](#contributors)\n[![Slack community][img-slack]][link-slack]\n\n</div>\n\n<hr />\n\nWelcome to the LifterLMS GitHub repository. This repository serves as the core project's central location for issue tracking and feature development.\n\nIf you're not a developer or contributor, please use [LifterLMS plugin page][link-wp-repo] at WordPress.org.\n\n\n### Getting Help and Support\n\nGitHub is for bug reports and contributions only! If you have a support question or a request for a customization this is not the right place to post it. Please refer to [LifterLMS Support][link-support] or the [community forums][link-support-forums]. If you're looking for help customizing LifterLMS, please consider hiring a [LifterLMS Expert][link-experts].\n\n\n### Resources and Documentation\n\n+ [Changelog](./CHANGELOG.md)\n+ User documentation and knowledge base: https://lifterlms.com/docs/\n+ Contributor's blog: https://make.lifterlms.com/\n+ Developer portal: https://developer.lifterlms.com/\n\n\n### Included Core Packages\n\nThe LifterLMS core includes several additional packages which are included in releases through composer. These core projects are installable as standalone plugins for development and testing purposes. The stable versions are automatically included in LifterLMS core releases.\n\nThese packages have their own GitHub repositories:\n\n+ [LifterLMS Blocks](https://github.com/gocodebox/lifterlms-blocks)\n+ [LifterLMS REST API](https://github.com/gocodebox/lifterlms-rest)\n\n\n### Reporting a Bug\n\nBugs can be reported at https://github.com/gocodebox/lifterlms/issues/new.\n\nBefore reporting a bug, [search existing issues](https://github.com/gocodebox/lifterlms/issues) and ensure you're not creating a duplicate. If the issue already exists you can add your information to the existing report.\n\nAlso check our [known issues and conflicts](https://lifterlms.com/doc-category/lifterlms/known-conflicts/) for possible resolutions.\n\n\n### Reporting a Security Vulnerability\n\nSecurity issues and vulnerabilities should be responsibly disclosed directly to the LifterLMS core developers via email. Please see our [Security Policy](.github/SECURITY.md) for details on disclosing a security vulnerability.\n\n\n### Installing\n\nIf you clone or download this repo directly it will not run as a plugin inside WordPress!\n\nInstallable production releases are available in on the [Releases tab](https://github.com/gocodebox/lifterlms/releases). You can get the latest stable release from [WordPress.org](https://downloads.wordpress.org/plugin/lifterlms.zip)\n\nIf you're interested in installing development versions, see [Installing for Development](docs/installing.md)\n\n\n### Contributing\n\n[![Contributions Welcome][img-contributions-welcome]](.github/CONTRIBUTING.md)\n\nInterested in contributing to LifterLMS? We'd love to have your contributions. Read our contributor's guidelines [here](.github/CONTRIBUTING.md).\n\n\n### Contributors\n\n[![Contributors][img-contributors]](#contributors)\n\nEndless thanks to all our incredible contributors!\n\n[//]: contributor-faces\n<a href=\"https://github.com/thomasplevy\"><img src=\"https://avatars.githubusercontent.com/u/1290739?v=4\" title=\"thomasplevy\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/eri-trabiccolo\"><img src=\"https://avatars.githubusercontent.com/u/7689242?v=4\" title=\"eri-trabiccolo\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/brianhogg\"><img src=\"https://avatars.githubusercontent.com/u/627497?v=4\" title=\"brianhogg\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/pondermatic\"><img src=\"https://avatars.githubusercontent.com/u/5377968?v=4\" title=\"pondermatic\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/ideadude\"><img src=\"https://avatars.githubusercontent.com/u/33220397?v=4\" title=\"ideadude\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/therealmarknelson\"><img src=\"https://avatars.githubusercontent.com/u/5050601?v=4\" title=\"therealmarknelson\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/PSmolic\"><img src=\"https://avatars.githubusercontent.com/u/4542049?v=4\" title=\"PSmolic\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/actuallyakash\"><img src=\"https://avatars.githubusercontent.com/u/18614782?v=4\" title=\"actuallyakash\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/seothemes\"><img src=\"https://avatars.githubusercontent.com/u/24793388?v=4\" title=\"seothemes\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/kimcoleman\"><img src=\"https://avatars.githubusercontent.com/u/5312875?v=4\" title=\"kimcoleman\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/bmatt468\"><img src=\"https://avatars.githubusercontent.com/u/8673706?v=4\" title=\"bmatt468\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/chrisbadgett\"><img src=\"https://avatars.githubusercontent.com/u/12163552?v=4\" title=\"chrisbadgett\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/MaximilianoRicoTabo\"><img src=\"https://avatars.githubusercontent.com/u/1678457?v=4\" title=\"MaximilianoRicoTabo\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/alimathis\"><img src=\"https://avatars.githubusercontent.com/u/16086976?v=4\" title=\"alimathis\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/nrherron92\"><img src=\"https://avatars.githubusercontent.com/u/47434271?v=4\" title=\"nrherron92\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/daniel-shuy\"><img src=\"https://avatars.githubusercontent.com/u/17351764?v=4\" title=\"daniel-shuy\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/andreasblumberg\"><img src=\"https://avatars.githubusercontent.com/u/1697968?v=4\" title=\"andreasblumberg\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/imknight\"><img src=\"https://avatars.githubusercontent.com/u/77604?v=4\" title=\"imknight\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/philwp\"><img src=\"https://avatars.githubusercontent.com/u/5949352?v=4\" title=\"philwp\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/alaa-alshamy\"><img src=\"https://avatars.githubusercontent.com/u/2883734?v=4\" title=\"alaa-alshamy\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/chetansatasiya\"><img src=\"https://avatars.githubusercontent.com/u/7081284?v=4\" title=\"chetansatasiya\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/actual-saurabh\"><img src=\"https://avatars.githubusercontent.com/u/1739834?v=4\" title=\"actual-saurabh\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/Mte90\"><img src=\"https://avatars.githubusercontent.com/u/403283?v=4\" title=\"Mte90\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/nikolapasic\"><img src=\"https://avatars.githubusercontent.com/u/10199798?v=4\" title=\"nikolapasic\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/AndreaBarghigiani\"><img src=\"https://avatars.githubusercontent.com/u/190159?v=4\" title=\"AndreaBarghigiani\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/yojance\"><img src=\"https://avatars.githubusercontent.com/u/1916064?v=4\" title=\"yojance\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/tpkemme\"><img src=\"https://avatars.githubusercontent.com/u/3424234?v=4\" title=\"tpkemme\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/paulgoodchild\"><img src=\"https://avatars.githubusercontent.com/u/10562196?v=4\" title=\"paulgoodchild\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/wenchen\"><img src=\"https://avatars.githubusercontent.com/u/959457?v=4\" title=\"wenchen\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/mcguffin\"><img src=\"https://avatars.githubusercontent.com/u/402988?v=4\" title=\"mcguffin\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/dineshchouhan\"><img src=\"https://avatars.githubusercontent.com/u/15683967?v=4\" title=\"dineshchouhan\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/hovpoghosyan\"><img src=\"https://avatars.githubusercontent.com/u/9405480?v=4\" title=\"hovpoghosyan\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/tnorthcutt\"><img src=\"https://avatars.githubusercontent.com/u/796639?v=4\" title=\"tnorthcutt\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/ThePikJoker\"><img src=\"https://avatars.githubusercontent.com/u/16877156?v=4\" title=\"ThePikJoker\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/nicolas-jaussaud\"><img src=\"https://avatars.githubusercontent.com/u/33153717?v=4\" title=\"nicolas-jaussaud\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/mrosati84\"><img src=\"https://avatars.githubusercontent.com/u/855068?v=4\" title=\"mrosati84\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/jasonyingling\"><img src=\"https://avatars.githubusercontent.com/u/4986487?v=4\" title=\"jasonyingling\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/jasonyingling-hlk\"><img src=\"https://avatars.githubusercontent.com/u/196813470?v=4\" title=\"jasonyingling-hlk\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/bsetiawan88\"><img src=\"https://avatars.githubusercontent.com/u/5827051?v=4\" title=\"bsetiawan88\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/yumashev\"><img src=\"https://avatars.githubusercontent.com/u/37841388?v=4\" title=\"yumashev\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/sujaypawar\"><img src=\"https://avatars.githubusercontent.com/u/2222249?v=4\" title=\"sujaypawar\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/AlexVCS\"><img src=\"https://avatars.githubusercontent.com/u/49458917?v=4\" title=\"AlexVCS\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/edent\"><img src=\"https://avatars.githubusercontent.com/u/837136?v=4\" title=\"edent\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/sekanderb\"><img src=\"https://avatars.githubusercontent.com/u/3262638?v=4\" title=\"sekanderb\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/sapayth\"><img src=\"https://avatars.githubusercontent.com/u/15567340?v=4\" title=\"sapayth\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/reedhewitt\"><img src=\"https://avatars.githubusercontent.com/u/957141?v=4\" title=\"reedhewitt\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/Nikschavan\"><img src=\"https://avatars.githubusercontent.com/u/2931091?v=4\" title=\"Nikschavan\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/nhandl3\"><img src=\"https://avatars.githubusercontent.com/u/1247539?v=4\" title=\"nhandl3\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/matthalliday\"><img src=\"https://avatars.githubusercontent.com/u/249506?v=4\" title=\"matthalliday\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/kamalahmed\"><img src=\"https://avatars.githubusercontent.com/u/6803549?v=4\" title=\"kamalahmed\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/moorscode\"><img src=\"https://avatars.githubusercontent.com/u/2005352?v=4\" title=\"moorscode\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/iTechsTR\"><img src=\"https://avatars.githubusercontent.com/u/33372714?v=4\" title=\"iTechsTR\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/unt01d\"><img src=\"https://avatars.githubusercontent.com/u/11303423?v=4\" title=\"unt01d\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/cadengrey\"><img src=\"https://avatars.githubusercontent.com/u/30481164?v=4\" title=\"cadengrey\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/andrewvaughan\"><img src=\"https://avatars.githubusercontent.com/u/1119590?v=4\" title=\"andrewvaughan\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/andrewlimaza\"><img src=\"https://avatars.githubusercontent.com/u/12629136?v=4\" title=\"andrewlimaza\" width=\"80\" height=\"80\"></a>\n<a href=\"https://github.com/README1ST\"><img src=\"https://avatars.githubusercontent.com/u/30046495?v=4\" title=\"README1ST\" width=\"80\" height=\"80\"></a>\n\n[//]: contributor-faces\n\n\n### Partners and Sponsors\n\n[<img src=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/.github/sponsors/browserstack-logo.png\" height=\"60\" alt=\"BrowserStack\">](https://www.browserstack.com/)\n\n[BrowserStack](https://www.browserstack.com/) helps us ensure LifterLMS looks great and works on every imaginable browser and device.\n\n<!-- References: Links -->\n[link-cc]: https://codeclimate.com/github/gocodebox/lifterlms \"LifterLMS on Code Climate\"\n[link-cc-coverage]: https://codeclimate.com/github/gocodebox/lifterlms/coverage \"Code coverage reports on Code Climate\"\n[link-experts]: https://lifterlms.com/docs/do-you-have-any-recommended-developers-who-can-modifycustomize-lifterlms/ \"Hire a LifterLMS Expert\"\n[link-php]: https://www.php.net/supported-versions \"PHP Support Versions\"\n[link-phpunit-tests]: https://github.com/gocodebox/lifterlms/actions/workflows/test-phpunit.yml \"PHPUnit Tests Status\"\n[link-phpcs-checks]: https://github.com/gocodebox/lifterlms/actions/workflows/coding-standards.yml \"PHPCS Coding Standards Checks\"\n[link-slack]: https://lifterlms.com/slack \"Chat with the community on Slack\"\n[link-support]: https://lifterlms.com/my-account/my-tickets \"LifterLMS customer support\"\n[link-support-forums]: https://wordpress.org/support/plugin/lifterlms \"LifterLMS user support forums\"\n[link-wp-advanced]:https://wordpress.org/plugins/lifterlms/advanced/ \"Advanced plugin details on the WordPress plugin repository\"\n[link-wp-repo]:https://wordpress.org/plugins/lifterlms/ \"LifterLMS on the WordPress plugin repository\"\n[link-wp-reviews]:https://wordpress.org/support/plugin/lifterlms/reviews/ \"Leave a review on the WordPress plugin repository\"\n\n[img-cc-coverage]:https://img.shields.io/codeclimate/coverage/gocodebox/lifterlms?style=for-the-badge&logo=code-climate\n[img-cc-maintainability]:https://img.shields.io/codeclimate/maintainability/gocodebox/lifterlms?logo=code-climate&style=for-the-badge\n[img-contributors]: https://img.shields.io/github/contributors/gocodebox/lifterlms?color=blue&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJzdmcyIiB3aWR0aD0iNjQ1IiBoZWlnaHQ9IjU4NSIgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPiA8ZyBpZD0ibGF5ZXIxIj4gIDxwYXRoIGlkPSJwYXRoMjQxNyIgZD0ibTI5Ny4zIDU1MC44N2MtMTMuNzc1LTE1LjQzNi00OC4xNzEtNDUuNTMtNzYuNDM1LTY2Ljg3NC04My43NDQtNjMuMjQyLTk1LjE0Mi03Mi4zOTQtMTI5LjE0LTEwMy43LTYyLjY4NS01Ny43Mi04OS4zMDYtMTE1LjcxLTg5LjIxNC0xOTQuMzQgMC4wNDQ1MTItMzguMzg0IDIuNjYwOC01My4xNzIgMTMuNDEtNzUuNzk3IDE4LjIzNy0zOC4zODYgNDUuMS02Ni45MDkgNzkuNDQ1LTg0LjM1NSAyNC4zMjUtMTIuMzU2IDM2LjMyMy0xNy44NDUgNzYuOTQ0LTE4LjA3IDQyLjQ5My0wLjIzNDgzIDUxLjQzOSA0LjcxOTcgNzYuNDM1IDE4LjQ1MiAzMC40MjUgMTYuNzE0IDYxLjc0IDUyLjQzNiA2OC4yMTMgNzcuODExbDMuOTk4MSAxNS42NzIgOS44NTk2LTIxLjU4NWM1NS43MTYtMTIxLjk3IDIzMy42LTEyMC4xNSAyOTUuNSAzLjAzMTYgMTkuNjM4IDM5LjA3NiAyMS43OTQgMTIyLjUxIDQuMzgwMSAxNjkuNTEtMjIuNzE1IDYxLjMwOS02NS4zOCAxMDguMDUtMTY0LjAxIDE3OS42OC02NC42ODEgNDYuOTc0LTEzNy44OCAxMTguMDUtMTQyLjk4IDEyOC4wMy01LjkxNTUgMTEuNTg4LTAuMjgyMTYgMS44MTU5LTI2LjQwOC0yNy40NjF6IiBmaWxsPSIjZGQ1MDRmIi8%2BIDwvZz48L3N2Zz4%3D\n[img-contributions-welcome]: https://img.shields.io/badge/contributions-welcome-blue.svg?style=for-the-badge&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik02NzIgMTQ3MnEwLTQwLTI4LTY4dC02OC0yOC02OCAyOC0yOCA2OCAyOCA2OCA2OCAyOCA2OC0yOCAyOC02OHptMC0xMTUycTAtNDAtMjgtNjh0LTY4LTI4LTY4IDI4LTI4IDY4IDI4IDY4IDY4IDI4IDY4LTI4IDI4LTY4em02NDAgMTI4cTAtNDAtMjgtNjh0LTY4LTI4LTY4IDI4LTI4IDY4IDI4IDY4IDY4IDI4IDY4LTI4IDI4LTY4em05NiAwcTAgNTItMjYgOTYuNXQtNzAgNjkuNXEtMiAyODctMjI2IDQxNC02NyAzOC0yMDMgODEtMTI4IDQwLTE2OS41IDcxdC00MS41IDEwMHYyNnE0NCAyNSA3MCA2OS41dDI2IDk2LjVxMCA4MC01NiAxMzZ0LTEzNiA1Ni0xMzYtNTYtNTYtMTM2cTAtNTIgMjYtOTYuNXQ3MC02OS41di04MjBxLTQ0LTI1LTcwLTY5LjV0LTI2LTk2LjVxMC04MCA1Ni0xMzZ0MTM2LTU2IDEzNiA1NiA1NiAxMzZxMCA1Mi0yNiA5Ni41dC03MCA2OS41djQ5N3E1NC0yNiAxNTQtNTcgNTUtMTcgODcuNS0yOS41dDcwLjUtMzEgNTktMzkuNSA0MC41LTUxIDI4LTY5LjUgOC41LTkxLjVxLTQ0LTI1LTcwLTY5LjV0LTI2LTk2LjVxMC04MCA1Ni0xMzZ0MTM2LTU2IDEzNiA1NiA1NiAxMzZ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+\n[img-php]: https://img.shields.io/badge/PHP-7.2%2B-brightgreen?style=for-the-badge&logoColor=white&logo=php\n[img-phpunit-tests]: https://img.shields.io/github/workflow/status/gocodebox/lifterlms/Test%20PHPUnit?label=PHPUnit&logo=github&style=for-the-badge\n[img-phpcs-checks]: https://img.shields.io/github/workflow/status/gocodebox/lifterlms/Coding%20Standards?label=PHPCS&logo=github&style=for-the-badge\n[img-slack]: https://img.shields.io/badge/chat-on%20slack-blueviolet?style=for-the-badge&logo=slack\n[img-wp-downloads]: https://img.shields.io/wordpress/plugin/dt/lifterlms.svg?style=for-the-badge&logo=wordpress\n[img-wp-installs]: https://img.shields.io/wordpress/plugin/installs/lifterlms.svg?style=for-the-badge&logo=wordpress\n[img-wp-plugin]:https://img.shields.io/wordpress/plugin/v/lifterlms.svg?style=for-the-badge&logo=wordpress\n[img-wp-rating]:https://img.shields.io/wordpress/plugin/r/lifterlms.svg?style=for-the-badge&logo=wordpress\n[img-wp-tested]:https://img.shields.io/wordpress/v/lifterlms.svg?style=for-the-badge&logo=wordpress\n"
  },
  {
    "path": "assets/css/bricks-editor.css",
    "content": "i.llms-bricks-icon:before {\n\tcontent: \"\";\n\tdisplay: block;\n\theight: 20px;\n\twidth: 20px;\n\tbackground-size: 20px 20px;\n\tbackground-repeat: no-repeat;\n\tmargin: 15px auto;\n}\n\ni.llms-bricks-icon-lesson-progression::before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' width='24' height='24' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z'></path></svg>\");\n}\n\ni.llms-bricks-icon-course-author:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z'></path></svg>\")\n}\n\ni.llms-bricks-icon-course-continue:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M448 160H320V128H448v32zM48 64C21.5 64 0 85.5 0 112v64c0 26.5 21.5 48 48 48H464c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zM448 352v32H192V352H448zM48 288c-26.5 0-48 21.5-48 48v64c0 26.5 21.5 48 48 48H464c26.5 0 48-21.5 48-48V336c0-26.5-21.5-48-48-48H48z'></path></svg>\")\n}\n\ni.llms-bricks-icon-course-information:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' width='24' height='24' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zm64 0v64h64V96H64zm384 0H192v64H448V96zM64 224v64h64V224H64zm384 0H192v64H448V224zM64 352v64h64V352H64zm384 0H192v64H448V352z'></path></svg>\")\n}\n\ni.llms-bricks-icon-course-meta-info:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z'></path></svg>\")\n}\n\ni.llms-bricks-icon-course-progress:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' width='24' height='24' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M64 64c0-17.7-14.3-32-32-32S0 46.3 0 64V400c0 44.2 35.8 80 80 80H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H80c-8.8 0-16-7.2-16-16V64zm406.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L320 210.7l-57.4-57.4c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L240 221.3l57.4 57.4c12.5 12.5 32.8 12.5 45.3 0l128-128z'></path></svg>\")\n}\n\ni.llms-bricks-icon-course-syllabus:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M152.1 38.2c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 113C-2.3 103.6-2.3 88.4 7 79s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zm0 160c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 273c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zM224 96c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zm0 160c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zM160 416c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-17.7 0-32-14.3-32-32zM48 368a48 48 0 1 1 0 96 48 48 0 1 1 0-96z'></path></svg>\")\n}\n\ni.llms-bricks-icon-instructors:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512' width='24' height='24' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M192 96a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm-8 384V352h16V480c0 17.7 14.3 32 32 32s32-14.3 32-32V192h56 64 16c17.7 0 32-14.3 32-32s-14.3-32-32-32H384V64H576V256H384V224H320v48c0 26.5 21.5 48 48 48H592c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H368c-26.5 0-48 21.5-48 48v80H243.1 177.1c-33.7 0-64.9 17.7-82.3 46.6l-58.3 97c-9.1 15.1-4.2 34.8 10.9 43.9s34.8 4.2 43.9-10.9L120 256.9V480c0 17.7 14.3 32 32 32s32-14.3 32-32z'></path></svg>\")\n}\n\ni.llms-bricks-icon-pricing-table:before {\n\tbackground-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512' width='24' height='24' class='llms-block-icon' aria-hidden='true' focusable='false'><path d='M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zM272 192H496c8.8 0 16 7.2 16 16s-7.2 16-16 16H272c-8.8 0-16-7.2-16-16s7.2-16 16-16zM256 304c0-8.8 7.2-16 16-16H496c8.8 0 16 7.2 16 16s-7.2 16-16 16H272c-8.8 0-16-7.2-16-16zM164 152v13.9c7.5 1.2 14.6 2.9 21.1 4.7c10.7 2.8 17 13.8 14.2 24.5s-13.8 17-24.5 14.2c-11-2.9-21.6-5-31.2-5.2c-7.9-.1-16 1.8-21.5 5c-4.8 2.8-6.2 5.6-6.2 9.3c0 1.8 .1 3.5 5.3 6.7c6.3 3.8 15.5 6.7 28.3 10.5l.7 .2c11.2 3.4 25.6 7.7 37.1 15c12.9 8.1 24.3 21.3 24.6 41.6c.3 20.9-10.5 36.1-24.8 45c-7.2 4.5-15.2 7.3-23.2 9V360c0 11-9 20-20 20s-20-9-20-20V345.4c-10.3-2.2-20-5.5-28.2-8.4l0 0 0 0c-2.1-.7-4.1-1.4-6.1-2.1c-10.5-3.5-16.1-14.8-12.6-25.3s14.8-16.1 25.3-12.6c2.5 .8 4.9 1.7 7.2 2.4c13.6 4.6 24 8.1 35.1 8.5c8.6 .3 16.5-1.6 21.4-4.7c4.1-2.5 6-5.5 5.9-10.5c0-2.9-.8-5-5.9-8.2c-6.3-4-15.4-6.9-28-10.7l-1.7-.5c-10.9-3.3-24.6-7.4-35.6-14c-12.7-7.7-24.6-20.5-24.7-40.7c-.1-21.1 11.8-35.7 25.8-43.9c6.9-4.1 14.5-6.8 22.2-8.5V152c0-11 9-20 20-20s20 9 20 20z'></path></svg>\")\n}\n"
  },
  {
    "path": "assets/css/dancing-script.css",
    "content": "/* vietnamese */\n@font-face {\n  font-family: 'Dancing Script';\n  src: url(\"../fonts/dancing-script/dancing-script-vietnamese-regular.woff2\") format(\"woff2\");\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n  font-family: 'Dancing Script';\n  src: url(\"../fonts/dancing-script/dancing-script-latin-ext.woff2\") format(\"woff2\");\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Dancing Script';\n  src: url(\"../fonts/dancing-script/dancing-script-latin-regular.woff2\") format(\"woff2\");\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\n"
  },
  {
    "path": "assets/css/imperial-script.css",
    "content": "/* vietnamese */\n@font-face {\n  font-family: 'Imperial Script';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url(\"../fonts/imperial-script/imperial-script-vietnamese-regular.woff2\") format('woff2');\n  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n  font-family: 'Imperial Script';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url(\"../fonts/imperial-script/imperial-script-latin-ext.woff2\") format('woff2');\n  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Imperial Script';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url(\"../fonts/imperial-script/imperial-script-latin-regular.woff2\") format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}"
  },
  {
    "path": "assets/css/pirata-one.css",
    "content": "/* latin-ext */\n@font-face {\n  font-family: 'Pirata One';\n  src: url(\"../fonts/pirata-one/pirata-one-latin-ext.woff2\") format('woff2');\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n  font-family: 'Pirata One';\n  src: url(\"../fonts/pirata-one/pirata-one-latin-regular.woff2\") format('woff2');\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}"
  },
  {
    "path": "assets/css/unifraktur-maguntia.css",
    "content": "/* latin */\n@font-face {\n  font-family: 'UnifrakturMaguntia';\n  src: url(\"../fonts/unifraktur-maguntia/unifraktur-maguntia-latin-regular.woff2\") format('woff2');\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}"
  },
  {
    "path": "assets/js/app/llms-achievements.js",
    "content": "/**\n * Front End Achievements\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.14.0\n * @version 6.10.2\n */\n\nLLMS.Achievements = {\n\n\t/**\n\t * Init\n\t *\n\t * @since 3.14.0\n\t * @since 4.5.1 Fix conditional loading check.\n\t *\n\t * @return {void}\n\t */\n\tinit: function() {\n\n\t\tif ( $( '.llms-achievement' ).length ) {\n\n\t\t\tvar self = this;\n\n\t\t\t$( function() {\n\t\t\t\tself.bind();\n\t\t\t\tself.maybe_open();\n\t\t\t} );\n\t\t}\n\n\t},\n\n\t/**\n\t * Bind DOM events\n\t *\n\t * @since 3.14.0\n\t *\n\t * @return {void}\n\t */\n\tbind: function() {\n\n\t\tvar self = this;\n\n\t\t$( '.llms-achievement' ).each( function() {\n\n\t\t\tself.create_modal( $( this ) );\n\n\t\t} );\n\n\t\t$( '.llms-achievement' ).on( 'click', function() {\n\n\t\t\tvar $this  = $( this ),\n\t\t\t\tid     = 'achievement-' + $this.attr( 'data-id' ),\n\t\t\t\t$modal = $( '#' + id );\n\n\t\t\tif ( ! $modal.length ) {\n\t\t\t\tself.create_modal( $this );\n\t\t\t}\n\n\t\t\t$modal.iziModal( 'open' );\n\n\t\t} );\n\n\t},\n\n\t/**\n\t * Creates modal a modal for an achievement\n\t *\n\t * @since 3.14.0\n\t *\n\t * @param obj $el The jQuery selector for the modal card.\n\t * @return {void}\n\t */\n\tcreate_modal: function( $el ) {\n\n\t\tvar id     = 'achievement-' + $el.attr( 'data-id' ),\n\t\t\t$modal = $( '#' + id );\n\n\t\tif ( ! $modal.length ) {\n\t\t\t$modal = $( '<div class=\"llms-achievement-modal\" id=\"' + id + '\" />' );\n\t\t\t$( 'body' ).append( $modal );\n\t\t}\n\n\t\t$modal.iziModal( {\n\t\t\theaderColor: '#3a3a3a',\n\t\t\tgroup: 'achievements',\n\t\t\thistory: true,\n\t\t\tloop: true,\n\t\t\toverlayColor: 'rgba( 0, 0, 0, 0.6 )',\n\t\t\ttransitionIn: 'fadeInDown',\n\t\t\ttransitionOut: 'fadeOutDown',\n\t\t\twidth: 340,\n\t\t\tonOpening: function( modal ) {\n\n\t\t\t\tmodal.setTitle( $el.find( '.llms-achievement-title' ).html() );\n\t\t\t\tmodal.setSubtitle( $el.find( '.llms-achievement-date' ).html() );\n\t\t\t\tmodal.setContent( '<div class=\"llms-achievement\">' + $el.html() + '</div>' );\n\n\t\t\t},\n\n\t\t\tonClosing: function() {\n\t\t\t\twindow.history.pushState( '', document.title, window.location.pathname + window.location.search );\n\t\t\t},\n\n\t\t} );\n\n\t},\n\n\t/**\n\t * On page load, opens a modal if the URL contains an achievement in the location hash\n\t *\n\t * @since 3.14.0\n\t * @since 6.10.2 Sanitize achievement IDs before using window.location.hash to trigger the modal open.\n\t *\n\t * @return {void}\n\t */\n\tmaybe_open: function() {\n\n\t\tlet hash = window.location.hash.split( '-' );\n\t\tif ( 2 !== hash.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\thash[1] = parseInt( hash[1] );\n\t\tif ( '#achievement-' !== hash[0] || ! Number.isInteger( hash[1] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst a = document.querySelector( `a[href=\"${ hash.join( '-' ) }\"]` )\n\t\tif ( ! a ) {\n\t\t\treturn;\n\t\t}\n\n\t\ta.click();\n\n\t}\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-ajax.js",
    "content": "/**\n * Main Ajax class\n * Handles Primary Ajax connection\n *\n * @package LifterLMS/Scripts\n *\n * @since Unknown\n * @version Unknown\n */\n\nLLMS.Ajax = {\n\n\t/**\n\t * Url\n\t *\n\t * @type {String}\n\t */\n\turl: window.ajaxurl || window.llms.ajaxurl,\n\n\t/**\n\t * Type\n\t *\n\t * @type {[type]}\n\t */\n\ttype: 'post',\n\n\t/**\n\t * Data\n\t *\n\t * @type {[type]}\n\t */\n\tdata: [],\n\n\t/**\n\t * Cache\n\t *\n\t * @type {[type]}\n\t */\n\tcache: false,\n\n\t/**\n\t * DataType\n\t * defaulted to json\n\t *\n\t * @type {String}\n\t */\n\tdataType: 'json',\n\n\t/**\n\t * Async\n\t * default to false\n\t *\n\t * @type {Boolean}\n\t */\n\tasync: true,\n\n\tresponse:[],\n\n\t/**\n\t * Initialize Ajax methods\n\t *\n\t * @since Unknown\n\t * @since 4.4.0 Update ajax nonce source.\n\t *\n\t * @param {Object} obj Options object.\n\t * @return {Object}\n\t */\n\tinit: function( obj ) {\n\n\t\t// If obj is not of type object or null return false.\n\t\tif ( obj === null || typeof obj !== 'object' ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// set object defaults if values are not supplied\n\t\tobj.url      = 'url'         in obj ? obj.url : this.url;\n\t\tobj.type     = 'type' \t\tin obj ? obj.type : this.type;\n\t\tobj.data     = 'data' \t\tin obj ? obj.data : this.data;\n\t\tobj.cache    = 'cache' \t\tin obj ? obj.cache : this.cache;\n\t\tobj.dataType = 'dataType'\tin obj ? obj.dataType : this.dataType;\n\t\tobj.async    = 'async'\t\tin obj ? obj.async : this.async;\n\n\t\t// Add nonce to data object.\n\t\tobj.data._ajax_nonce = window.llms.ajax_nonce || wp_ajax_data.nonce;\n\n\t\t// Add post id to data object.\n\t\tvar $R           = LLMS.Rest,\n\t\tquery_vars       = $R.get_query_vars();\n\t\tobj.data.post_id = 'post' in query_vars ? query_vars.post : null;\n\t\tif ( ! obj.data.post_id && $( 'input#post_ID' ).length ) {\n\t\t\tobj.data.post_id = $( 'input#post_ID' ).val();\n\t\t}\n\n\t\treturn obj;\n\t},\n\n\t/**\n\t * Call\n\t * Called by external classes\n\t * Sets up jQuery Ajax object\n\t *\n\t * @param  {[object]} [object of ajax settings]\n\t * @return {[mixed]} [false if not object or this]\n\t */\n\tcall: function(obj) {\n\n\t\t// get default variables if not included in call\n\t\tvar settings = this.init( obj );\n\n\t\t// if init return a response of false\n\t\tif ( ! settings) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\tthis.request( settings );\n\t\t}\n\n\t\treturn this;\n\n\t},\n\n\t/**\n\t * Calls jQuery Ajax on settings object\n\t *\n\t * @return {[object]} [this]\n\t */\n\trequest: function(settings) {\n\n\t\t$.ajax( settings );\n\n\t\treturn this;\n\n\t}\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-donut.js",
    "content": "/**\n * Create a Donut Chart\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.9.0\n * @version 4.15.0\n *\n * @link https://gist.github.com/joeyinbox/8205962\n *\n * @param {Object} $el jQuery element to draw a chart within.\n */\n\nLLMS.Donut = function( $el ) {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.9.0\n\t * @since 4.15.0 Flip animation in RTL.\n\t *\n\t * @param {Object} options Donut options.\n\t * @return {Void}\n\t */\n\tfunction Donut(options) {\n\n\t\tthis.settings = $.extend( {\n\t\t\telement: options.element,\n\t\t\tpercent: 100\n\t\t}, options );\n\n\t\tthis.circle                = this.settings.element.find( 'path' );\n\t\tthis.settings.stroke_width = parseInt( this.circle.css( 'stroke-width' ) );\n\t\tthis.radius                = ( parseInt( this.settings.element.css( 'width' ) ) - this.settings.stroke_width ) / 2;\n\t\tthis.angle                 = $( 'body' ).hasClass( 'rtl' ) ? 82.5 : 97.5; // Origin of the draw at the top of the circle\n\t\tthis.i                     = Math.round( 0.75 * this.settings.percent );\n\t\tthis.first                 = true;\n\t\tthis.increment             = $( 'body' ).hasClass( 'rtl' ) ? -5 : 5;\n\n\t\tthis.animate = function() {\n\t\t\tthis.timer = setInterval( this.loop.bind( this ), 10 );\n\t\t};\n\n\t\tthis.loop = function() {\n\t\t\tthis.angle += this.increment;\n\t\t\tthis.angle %= 360;\n\t\t\tvar radians = ( this.angle / 180 ) * Math.PI,\n\t\t\t\tx       = this.radius + this.settings.stroke_width / 2 + Math.cos( radians ) * this.radius,\n\t\t\t\ty       = this.radius + this.settings.stroke_width / 2 + Math.sin( radians ) * this.radius,\n\t\t\t\td;\n\t\t\tif (this.first === true) {\n\t\t\t\td          = this.circle.attr( 'd' ) + ' M ' + x + ' ' + y;\n\t\t\t\tthis.first = false;\n\t\t\t} else {\n\t\t\t\td = this.circle.attr( 'd' ) + ' L ' + x + ' ' + y;\n\t\t\t}\n\t\t\tthis.circle.attr( 'd', d );\n\t\t\tthis.i--;\n\n\t\t\tif (this.i <= 0) {\n\t\t\t\tclearInterval( this.timer );\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Draw donut element\n\t *\n\t * @since 3.9.0\n\t *\n\t * @param {Object} $el jQuery element to draw a chart within.\n\t * @return {Void}\n\t */\n\tfunction draw( $el ) {\n\t\tvar path = '<path d=\"M100,100\" />';\n\t\t$el.append( '<svg preserveAspectRatio=\"xMidYMid\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">' + path + '</svg>' );\n\t\tvar donut = new Donut( {\n\t\t\telement: $el,\n\t\t\tpercent: $el.attr( 'data-perc' )\n\t\t} );\n\t\tdonut.animate();\n\t}\n\n\tdraw( $el );\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-forms.js",
    "content": "/**\n * Forms\n *\n * @package LifterLMS/Scripts\n *\n * @since 5.0.0\n * @version 7.0.0\n */\n\nLLMS.Forms = {\n\n\t/**\n\t * Stores locale information.\n\t *\n\t * Added via PHP.\n\t *\n\t * @type {Object}\n\t */\n\taddress_info: {},\n\n\t/**\n\t * jQuery ref. to the city text field.\n\t *\n\t * @type {Object}\n\t */\n\t$cities: null,\n\n\t/**\n\t * jQuery ref. to the countries select field.\n\t *\n\t * @type {Object}\n\t */\n\t$countries: null,\n\n\t/**\n\t * jQuery ref. to the states select field.\n\t *\n\t * @type {Object}\n\t */\n\t$states: null,\n\n\t/**\n\t * jQuery ref. to the hidden states holder field.\n\t *\n\t * @type {Object}\n\t */\n\t$states_holder: null,\n\n\t/**\n\t * Init\n\t *\n \t * @since 5.0.0\n \t * @since 5.3.3 Move select2 dependency check into the `bind_l10_selects()` method.\n \t *\n \t * @return {void}\n\t */\n\tinit: function() {\n\n\t\tif ( $( 'body' ).hasClass( 'wp-admin' ) ) {\n\t\t\tif ( ! ( $( 'body' ).hasClass( 'profile-php' ) || $( 'body' ).hasClass( 'user-edit-php' ) ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tvar self = this;\n\n\t\tself.bind_matching_fields();\n\t\tself.bind_voucher_field();\n\t\tself.bind_edit_account();\n\t\tself.bind_l10n_selects();\n\n\t},\n\n\t/**\n\t * Bind DOM events for the edit account screen.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {void}\n\t */\n\tbind_edit_account: function() {\n\n\t\t// Not an edit account form.\n\t\tif ( ! $( 'form.llms-person-form.edit-account' ).length ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$( '.llms-toggle-fields' ).on( 'click', this.handle_toggle_click );\n\n\t},\n\n\t/**\n\t * Bind DOM Events fields with dynamic localization values and language.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Bind select2-related events after ensuring select2 is available.\n\t *\n\t * @return {void}\n\t */\n\tbind_l10n_selects: function() {\n\n\t\tvar self = this;\n\n\t\tself.$cities    = $( '#llms_billing_city' );\n\t\tself.$countries = $( '.llms-l10n-country-select select' );\n\t\tself.$states    = $( '.llms-l10n-state-select select' );\n\t\tself.$zips      = $( '#llms_billing_zip' );\n\n\t\tif ( ! self.$countries.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar isSelect2Available = function() {\n\t\t\treturn ( undefined !== $.fn.llmsSelect2 );\n\t\t};\n\n\t\tLLMS.wait_for( isSelect2Available, function() {\n\n\t\t\tif ( self.$states.length ) {\n\t\t\t\tself.prep_state_field();\n\t\t\t}\n\n\t\t\tself.$countries.add( self.$states ).llmsSelect2( { width: '100%' } );\n\n\t\t\tif ( window.llms.address_info ) {\n\t\t\t\tself.address_info = JSON.parse( window.llms.address_info );\n\t\t\t}\n\n\t\t\tself.$countries.on( 'change', function() {\n\n\t\t\t\tvar val = $( this ).val();\n\t\t\t\tself.update_locale_info( val );\n\n\t\t\t} ).trigger( 'change' );\n\n\t\t}, 'llmsSelect2' );\n\n\t},\n\n\t/**\n\t * Ensure \"matching\" fields match.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {Void}\n\t */\n\tbind_matching_fields: function() {\n\n\t\tvar $fields = $( 'input[data-match]' ).not( '[type=\"password\"]' );\n\n\t\t$fields.each( function() {\n\n\t\t\tvar $field = $( this ),\n\t\t\t\t$match = $( '#' + $field.attr( 'data-match' ) ),\n\t\t\t\t$parents;\n\n\t\t\tif ( $match.length ) {\n\n\t\t\t\t$parents = $field.closest( '.llms-form-field' ).add( $match.closest( '.llms-form-field' ) );\n\n\t\t\t\t$field.on( 'input change', function() {\n\n\t\t\t\t\tvar val_1 = $field.val(),\n\t\t\t\t\t\tval_2 = $match.val();\n\n\t\t\t\t\tif ( val_1 && val_2 && val_1 !== val_2 ) {\n\t\t\t\t\t\t$parents.addClass( 'invalid' );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$parents.removeClass( 'invalid' );\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t} );\n\n\t},\n\n\t/**\n\t * Bind DOM events for voucher toggles UX.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {void}\n\t */\n\tbind_voucher_field: function() {\n\n\t\t$( '#llms-voucher-toggle' ).on( 'click', function( e ) {\n\t\t\te.preventDefault();\n\t\t\t$( '#llms_voucher' ).toggle();\n\t\t} );\n\n\t},\n\n\t/**\n\t * Retrieve the parent element for a given field.\n\t *\n\t * The parent element is hidden when the field isn't required.\n\t *\n\t * @since 5.0.0\n\t * @since 7.0.0 Do not look for a WP column wrapper anymore, always return the field's wrapper div.\n\t *\n\t * @param {Object} $field jQuery dom object.\n\t * @return {Object}\n\t */\n\tget_field_parent: function( $field ) {\n\n\t\treturn $field.closest( '.llms-form-field' );\n\n\t},\n\n\t/**\n\t * Retrieve the text of a label\n\t *\n\t * Removes any children HTML elements (eg: required span elements) and returns only the labels text.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {Object} $label jQuery object for a label element.\n\t * @return {String}\n\t */\n\tget_label_text: function( $label ) {\n\n\t\tvar $clone = $label.clone();\n\t\t$clone.find( '*' ).remove();\n\t\treturn $clone.text().trim();\n\n\t},\n\n\t/**\n\t * Callback function to handle the \"toggle\" button links for changing email address and password on account edit forms\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {Object} event Native JS event object.\n\t * @return {void}\n\t */\n\thandle_toggle_click: function( event ) {\n\n\t\tevent.preventDefault();\n\n\t\tvar $this       = $( this ),\n\t\t\t$fields     = $( $( this ).attr( 'data-fields' ) ),\n\t\t\tisShowing   = $this.attr( 'data-is-showing' ) || 'no',\n\t\t\tdisplayFunc = 'yes' === isShowing ? 'hide' : 'show',\n\t\t\tdisabled    = 'yes' === isShowing ? 'disabled' : null,\n\t\t\ttextAttr    = 'yes' === isShowing ? 'data-change-text' : 'data-cancel-text';\n\n\t\t$fields.each( function() {\n\n\t\t\t$( this ).closest( '.llms-form-field' )[ displayFunc ]();\n\t\t\t$( this ).attr( 'disabled', disabled );\n\n\t\t} );\n\n\t\t$this.text( $this.attr( textAttr ) );\n\t\t$this.attr( 'data-is-showing', 'yes' === isShowing ? 'no' : 'yes' );\n\n\t},\n\n\t/**\n\t * Prepares the state select field.\n\t *\n\t * Moves All optgroup elements into a hidden & disabled select element.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {void}\n\t */\n\tprep_state_field: function() {\n\n\t\tvar $parent = this.$states.closest( '.llms-form-field' );\n\n\t\tthis.$holder = $( '<select disabled style=\"display:none !important;\" />' );\n\n\t\tthis.$holder.appendTo( $parent );\n\t\tthis.$states.find( 'optgroup' ).appendTo( this.$holder );\n\n\t},\n\n\t/**\n\t * Updates the text of a label for a given field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {Object} $field jQuery object of the form field.\n\t * @param {String} text Label text.\n\t * @return {void}\n\t */\n\tupdate_label: function( $field, text ) {\n\n\t\tvar $label = this.get_field_parent( $field ).find( 'label' ),\n\t\t\t$required = $label.find( '.llms-required' ).clone();\n\n\t\t$label.html( text );\n\t\t$label.append( $required );\n\n\t},\n\n\t/**\n\t * Update form fields based on selected country\n\t *\n\t * Replaces label text with locale-specific language and\n\t * hides or shows zip fields based on whether or not\n\t * they are required for the given country.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {String} country_code Currently selected country code.\n\t * @return {void}\n\t */\n\tupdate_locale_info: function( country_code ) {\n\n\t\tif ( ! this.address_info || ! this.address_info[ country_code ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar info = this.address_info[ country_code ];\n\n\t\tthis.update_state_options( country_code );\n\t\tthis.update_label( this.$states, info.state );\n\n\t\tthis.update_locale_info_for_field( this.$cities, info.city );\n\t\tthis.update_locale_info_for_field( this.$zips, info.postcode );\n\n\t},\n\n\t/**\n\t * Update locale info for a given field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {Object}         $field The jQuery object for the field.\n\t * @param {String|Boolean} label  The text of the label, or `false` when the field isn't supported.\n\t * @return {Void}\n\t */\n\tupdate_locale_info_for_field: function( $field, label ) {\n\n\t\tif ( label ) {\n\t\t\tthis.update_label( $field, label );\n\t\t\tthis.enable_field( $field );\n\t\t} else {\n\t\t\tthis.disable_field( $field );\n\t\t}\n\n\t},\n\n\t/**\n\t * Update the available options in the state field\n\t *\n\t * Removes existing options and copies the options\n\t * for the requested country from the hidden select field.\n\t *\n\t * If there are no states for the given country the state\n\t * field will be hidden.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {String} country_code Currently selected country code.\n\t * @return {void}\n\t */\n\tupdate_state_options: function( country_code ) {\n\n\t\tif ( ! this.$states.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar opts = this.$holder.find( 'optgroup[data-key=\"' + country_code + '\"] option' ).clone();\n\n\t\tif ( ! opts.length ) {\n\t\t\tthis.$states.html( '<option>&nbsp</option>' );\n\t\t\tthis.disable_field( this.$states );\n\t\t} else {\n\t\t\tthis.enable_field( this.$states );\n\t\t\tthis.$states.html( opts );\n\t\t}\n\n\t},\n\n\t/**\n\t * Disable a given field\n\t *\n\t * It also hides the parent element, and adds an empty hidden input field\n\t * with the same 'name' as teh being disabled field so to be sure to clear the field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {Object} $field The jQuery object for the field.\n\t */\n\tdisable_field: function( $field ) {\n\t\t$(\n\t\t\t'<input>',\n\t\t\t{ name: $field.attr('name'), class: $field.attr( 'class' ) + ' hidden', type: 'hidden' }\n\t\t).insertAfter( $field );\n\t\t$field.attr( 'disabled', 'disabled' );\n\t\tthis.get_field_parent( $field ).hide();\n\t},\n\n\t/**\n\t * Enable a given field\n\t *\n\t * It also shows the parent element, and removes the empty hidden input field\n\t * previously added by disable_field().\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {Object} $field The jQuery object for the field.\n\t */\n\tenable_field: function( $field ) {\n\t\t$field.removeAttr( 'disabled' );\n\t\t$field.next( '.hidden[name='+$field.attr('name')+']' ).detach();\n\t\tthis.get_field_parent( $field ).show();\n\t}\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-instructors.js",
    "content": "/**\n * Instructors List\n *\n * @package LifterLMS/Scripts\n *\n * @since  Unknown\n * @version  Unknown\n */\n\nLLMS.Instructors = {\n\n\t/**\n\t * Init\n\t */\n\tinit: function() {\n\n\t\tvar self = this;\n\n\t\tif ( $( 'body' ).hasClass( 'wp-admin' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( $( '.llms-instructors' ).length ) {\n\n\t\t\tLLMS.wait_for_matchHeight( function() {\n\t\t\t\tself.bind();\n\t\t\t} );\n\n\t\t}\n\n\t},\n\n\t/**\n\t * Bind Method\n\t * Handles dom binding on load\n\t *\n\t * @return {[type]} [description]\n\t */\n\tbind: function() {\n\n\t\t$( '.llms-instructors .llms-author' ).matchHeight();\n\n\t},\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-l10n.js",
    "content": "/**\n * Localization functions for LifterLMS Javascript\n *\n * @package LifterLMS/Scripts\n *\n * @since  2.7.3\n * @version  2.7.3\n *\n * @todo  we need more robust translation functions to handle sprintf and pluralization\n *        at this moment we don't need those and haven't stubbed them out\n *        those will be added when they're needed\n */\n\nLLMS.l10n = LLMS.l10n || {};\n\nLLMS.l10n.translate = function ( string ) {\n\n\tvar self = this;\n\n\tif ( self.strings[string] ) {\n\n\t\treturn self.strings[string];\n\n\t} else {\n\n\t\treturn string;\n\n\t}\n\n};\n\n/**\n * Translate and replace placeholders in a string\n *\n * @example LLMS.l10n.replace( 'This is a %2$s %1$s String', {\n *           \t'%1$s': 'cool',\n *    \t\t\t'%2$s': 'very'\n *    \t\t} );\n *    \t\tOutput: \"This is a very cool String\"\n *\n * @param    string   string        text string\n * @param    object   replacements  object containing token => replacement pairs\n * @return   string\n * @since    3.16.0\n * @version  3.16.0\n */\nLLMS.l10n.replace = function( string, replacements ) {\n\n\tvar str = this.translate( string );\n\n\t$.each( replacements, function( token, value ) {\n\n\t\tif ( -1 !== token.indexOf( 's' ) ) {\n\t\t\tvalue = value.toString();\n\t\t} else if ( -1 !== token.indexOf( 'd' ) ) {\n\t\t\tvalue = value * 1;\n\t\t}\n\n\t\tstr = str.replace( token, value );\n\n\t} );\n\n\treturn str;\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-lesson-preview.js",
    "content": "/**\n * Handle Lesson Preview Elements\n *\n * @package LifterLMS/Scripts\n *\n * @since    3.0.0\n * @version  3.16.12\n */\n\nLLMS.LessonPreview = {\n\n\t/**\n\t * A jQuery object of all outlines present on the current screen\n\t *\n\t * @type obj\n\t */\n\t$els: null,\n\n\t/**\n\t * Initialize\n\t *\n\t * @return void\n\t */\n\tinit: function() {\n\n\t\tvar self = this;\n\n\t\tthis.$locked = $( '.llms-lesson-locked' );\n\n\t\tif ( this.$locked.length ) {\n\n\t\t\tself.bind();\n\n\t\t}\n\n\t\tif ( $( '.llms-course-navigation' ).length ) {\n\n\t\t\tLLMS.wait_for_matchHeight( function() {\n\n\t\t\t\tself.match_height();\n\n\t\t\t} );\n\n\t\t}\n\n\t},\n\n\t/**\n\t * Bind DOM events\n\t *\n\t * @return void\n\t * @since    3.0.0\n\t * @version  3.16.12\n\t */\n\tbind: function() {\n\n\t\tvar self = this;\n\n\t\tthis.$locked.on( 'mouseenter', function() {\n\n\t\t\tvar $tip = $( this ).find( '.llms-tooltip' );\n\t\t\tif ( ! $tip.length ) {\n\t\t\t\tvar msg = $( this ).attr( 'data-tooltip-msg' );\n\t\t\t\tif ( ! msg ) {\n\t\t\t\t\tmsg = LLMS.l10n.translate( 'You do not have permission to access this content' );\n\t\t\t\t}\n\t\t\t\t$tip = self.get_tooltip( msg );\n\t\t\t\t$( this ).append( $tip );\n\t\t\t}\n\t\t\tsetTimeout( function() {\n\t\t\t\t$tip.addClass( 'show' );\n\t\t\t}, 10 );\n\n\t\t} );\n\n\t\tthis.$locked.on( 'mouseleave', function() {\n\n\t\t\tvar $tip = $( this ).find( '.llms-tooltip' );\n\t\t\t$tip.removeClass( 'show' );\n\n\t\t} );\n\n\t},\n\n\t/**\n\t * Match the height of lesson preview items in course navigation blocks\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tmatch_height: function() {\n\n\t\t$( '.llms-course-navigation .llms-lesson-link' ).matchHeight();\n\n\t},\n\n\t/**\n\t * Get a tooltip element\n\t *\n\t * @param    string   msg   message to display inside the tooltip\n\t * @return   obj\n\t * @since    3.0.0\n\t * @version  3.2.4\n\t */\n\tget_tooltip: function( msg ) {\n\t\tvar $el = $( '<div class=\"llms-tooltip\" />' );\n\t\t$el.append(\n\t\t\t$('<div>', {\n\t\t\t\t'class': 'llms-tooltip-content',\n\t\t\t\t'aria-hidden': 'true'\n\t\t\t}).text( msg )\n\t\t);\n\t\treturn $el;\n\t},\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-outline-collapse.js",
    "content": "/**\n * Handle the Collapsible Syllabus Widget / Shortcode\n *\n * @package LifterLMS/Scripts\n *\n * @since Unknown\n * @version Unknown\n */\n\nLLMS.OutlineCollapse = {\n\n\t/**\n\t * A jQuery object of all outlines present on the current screen\n\t *\n\t * @type obj\n\t */\n\t$outlines: null,\n\n\t/**\n\t * Initialize\n\t *\n\t * @return void\n\t */\n\tinit: function() {\n\n\t\tthis.$outlines = $( '.llms-widget-syllabus--collapsible' );\n\n\t\tif ( this.$outlines.length ) {\n\n\t\t\tthis.bind();\n\n\t\t}\n\n\t},\n\n\t/**\n\t * Bind DOM events\n\t *\n\t * @return void\n\t */\n\tbind: function() {\n\n\t\tvar self = this;\n\n\t\tthis.$outlines.each( function() {\n\n\t\t\tvar $outline = $( this ),\n\t\t\t\t$headers = $outline.find( '.llms-section .section-header' );\n\n\t\t\t// bind header clicks\n\t\t\t$headers.on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\tvar $toggle  = $( this ),\n\t\t\t\t\t$section = $toggle.closest( '.llms-section' ),\n\t\t\t\t\tstate    = self.get_section_state( $section );\n\n\t\t\t\tswitch ( state ) {\n\n\t\t\t\t\tcase 'closed':\n\t\t\t\t\t\tself.open_section( $section );\n\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'opened':\n\t\t\t\t\t\tself.close_section( $section );\n\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t// bind optional toggle \"buttons\"\n\t\t\t$outline.find( '.llms-collapse-toggle' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\tvar $btn            = $( this ),\n\t\t\t\t\taction          = $btn.attr( 'data-action' ),\n\t\t\t\t\topposite_action = ( 'close' === action ) ? 'opened' : 'closed';\n\n\t\t\t\t$headers.each( function() {\n\n\t\t\t\t\tvar $section = $( this ).closest( '.llms-section' ),\n\t\t\t\t\t\tstate    = self.get_section_state( $section );\n\n\t\t\t\t\tif ( opposite_action !== state ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch ( state ) {\n\n\t\t\t\t\t\tcase 'closed':\n\t\t\t\t\t\t\tself.close_section( $section );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase 'opened':\n\t\t\t\t\t\t\tself.open_section( $section );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t$( this ).trigger( 'click' );\n\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t} );\n\n\t},\n\n\t/**\n\t * Close an outline section\n\t *\n\t * @param  obj    $section   jQuery selector of a '.llms-section'\n\t * @return void\n\t */\n\tclose_section: function( $section ) {\n\n\t\t$section.removeClass( 'llms-section--opened' ).addClass( 'llms-section--closed' );\n\n\t},\n\n\t/**\n\t * Open an outline section\n\t *\n\t * @param  obj    $section   jQuery selector of a '.llms-section'\n\t * @return void\n\t */\n\topen_section: function( $section ) {\n\n\t\t$section.removeClass( 'llms-section--closed' ).addClass( 'llms-section--opened' );\n\n\t},\n\n\t/**\n\t * Get the current state (open or closed) of an outline section\n\t *\n\t * @param  obj    $section   jQuery selector of a '.llms-section'\n\t * @return string            'opened' or 'closed'\n\t */\n\tget_section_state: function( $section ) {\n\n\t\treturn $section.hasClass( 'llms-section--opened' ) ? 'opened' : 'closed';\n\n\t}\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-password-strength.js",
    "content": "/**\n * Handle Password Strength Meter for registration and password update fields\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.0.0\n * @version 5.0.0\n */\n\n$.extend( LLMS.PasswordStrength, {\n\n\t/**\n\t * jQuery ref for the password strength meter object.\n\t *\n\t * @type {Object}\n\t */\n\t$meter: $( '.llms-password-strength-meter' ),\n\n\t/**\n\t * jQuery ref for the password field.\n\t *\n\t * @type {Object}\n\t */\n\t$pass: null,\n\n\t/**\n\t * jQuery ref for the password confirmation field\n\t *\n\t * @type {Object}\n\t */\n\t$conf: null,\n\n\t/**\n\t * jQuery ref for form element.\n\t *\n\t * @type {Object}\n\t */\n\t$form: null,\n\n\t/**\n\t * Init\n\t * loads class methods\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.0 Unknown\n\t * @since 5.0.0 Move reference setup to `setup_references()`.\n\t *              Use `LLMS.wait_for()` for dependency waiting.\n\t *\n\t * @return {Void}\n\t */\n\tinit: function() {\n\n\t\tif ( $( 'body' ).hasClass( 'wp-admin' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! this.setup_references() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar self = this;\n\n\t\tLLMS.wait_for( function() {\n\t\t\treturn ( 'undefined' !== typeof wp && 'undefined' !== typeof wp.passwordStrength );\n\t\t}, function() {\n\t\t\tself.bind();\n\t\t\tself.$form.trigger( 'llms-password-strength-ready' );\n\t\t} );\n\n\t},\n\n\t/**\n\t * Bind DOM Events\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tbind: function() {\n\n\t\tvar self = this;\n\n\t\t// add submission event handlers when not on a checkout form\n\t\tif ( ! this.$form.hasClass( 'llms-checkout' ) ) {\n\t\t\tself.$form.on( 'submit', self, self.submit );\n\t\t}\n\n\t\t// check password strength on keyup\n\t\tself.$pass.add( self.$conf ).on( 'keyup', function() {\n\t\t\tself.check_strength();\n\t\t} );\n\n\t},\n\n\t/**\n\t * Check the strength of a user entered password\n\t * and update elements depending on the current strength\n\t *\n\t * @since 3.0.0\n\t * @since 5.0.0 Allow password confirmation to be optional when checking strength.\n\t *\n\t * @return void\n\t */\n\tcheck_strength: function() {\n\n\t\tvar $pass_field = this.$pass.closest( '.llms-form-field' ),\n\t\t\t$conf_field = this.$conf && this.$conf.length ? this.$conf.closest( '.llms-form-field' ) : null,\n\t\t\tpass_length = this.$pass.val().length,\n\t\t\tconf_length = this.$conf && this.$conf.length ? this.$conf.val().length : 0;\n\n\t\t// hide the meter if both fields are empty\n\t\tif ( ! pass_length && ! conf_length ) {\n\t\t\t$pass_field.removeClass( 'valid invalid' );\n\t\t\tif ( $conf_field ) {\n\t\t\t\t$conf_field.removeClass( 'valid invalid' );\n\t\t\t}\n\t\t\tthis.$meter.hide();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( this.get_current_strength_status() ) {\n\t\t\t$pass_field.removeClass( 'invalid' ).addClass( 'valid' );\n\t\t\tif ( conf_length ) {\n\t\t\t\t$conf_field.removeClass( 'invalid' ).addClass( 'valid' );\n\t\t\t}\n\t\t} else {\n\t\t\t$pass_field.removeClass( 'valid' ).addClass( 'invalid' );\n\t\t\tif ( conf_length ) {\n\t\t\t\t$conf_field.removeClass( 'valid' ).addClass( 'invalid' );\n\t\t\t}\n\t\t}\n\n\t\tthis.$meter.removeClass( 'too-short very-weak weak medium strong mismatch' );\n\t\tthis.$meter.show().addClass( this.get_current_strength( 'slug' ) );\n\t\tthis.$meter.html( this.get_current_strength( 'text' ) );\n\n\t},\n\n\t/**\n\t * Form submission action called during registration on checkout screen\n\t *\n\t * @since    3.0.0\n\t *\n\t * @param    obj       self      instance of this class\n\t * @param    Function  callback  callback function, passes error message or success back to checkout handler\n\t * @return   void\n\t */\n\tcheckout: function( self, callback ) {\n\n\t\tif ( self.get_current_strength_status() ) {\n\n\t\t\tcallback( true );\n\n\t\t} else {\n\n\t\t\tcallback( LLMS.l10n.translate( 'There is an issue with your chosen password.' ) );\n\n\t\t}\n\t},\n\n\t/**\n\t * Get the list of blocklisted strings\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tget_blocklist: function() {\n\n\t\t// Default values from WP Core + any values added via settings filter..\n\t\tvar blocklist = wp.passwordStrength.userInputDisallowedList().concat( this.get_setting( 'blocklist', [] ) );\n\n\t\t// Add values from all text fields in the form.\n\t\tthis.$form.find( 'input[type=\"text\"], input[type=\"email\"], input[type=\"tel\"], input[type=\"number\"]' ).each( function() {\n\t\t\tvar val = $( this ).val();\n\t\t\tif ( val ) {\n\t\t\t\tblocklist.push( val );\n\t\t\t}\n\t\t} );\n\n\t\treturn blocklist;\n\n\t},\n\n\t/**\n\t * Retrieve current strength as a number, a slug, or a translated text string\n\t *\n\t * @since 3.0.0\n\t * @since 5.0.0 Allow password confirmation to be optional when checking strength.\n\t *\n\t * @param {String} format Derived return format [int|slug|text] defaults to int.\n\t * @return mixed\n\t */\n\tget_current_strength: function( format ) {\n\n\t\tformat   = format || 'int';\n\t\tvar pass = this.$pass.val(),\n\t\t\tconf = this.$conf && this.$conf.length ? this.$conf.val() : '',\n\t\t\tval;\n\n\t\t// enforce custom length requirement\n\t\tif ( pass.length < this.get_setting( 'min_length', 6 ) ) {\n\t\t\tval = -1;\n\t\t} else {\n\t\t\tval = wp.passwordStrength.meter( pass, this.get_blocklist(), conf );\n\t\t\t// 0 is very weak (ie. using something on the block list), 1 is considered weak.\n\t\t\tif ( 1 === val ) {\n\t\t\t\tval = 2;\n\t\t\t}\n\t\t\tif ( 0 === val ) {\n\t\t\t\tval = 1;\n\t\t\t}\n\t\t}\n\n\t\tif ( 'slug' === format ) {\n\t\t\treturn this.get_strength_slug( val );\n\t\t} else if ( 'text' === format ) {\n\t\t\treturn this.get_strength_text( val );\n\t\t} else {\n\t\t\treturn val;\n\t\t}\n\t},\n\n\t/**\n\t * Determines if the current password strength meets the user-defined\n\t * minimum password strength requirements\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return   boolean\n\t */\n\tget_current_strength_status: function() {\n\t\tvar curr = this.get_current_strength(),\n\t\t\tmin  = this.get_strength_value( this.get_minimum_strength() );\n\t\treturn ( 5 === curr ) ? false : ( curr >= min );\n\t},\n\n\t/**\n\t * Retrieve the minimum password strength for the current form.\n\t *\n\t * @since 3.0.0\n\t * @since 5.0.0 Replaces the version output via an inline PHP script in favor of utilizing values configured in the settings object.\n\t *\n\t * @return {string}\n\t */\n\tget_minimum_strength: function() {\n\t\treturn this.get_setting( 'min_strength', 'weak' );\n\t},\n\n\t/**\n\t * Get a setting and fallback to a default value.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param {String} key Setting key.\n\t * @param {mixed} default_val Default value when the requested setting cannot be located.\n\t * @return {mixed}\n\t */\n\tget_setting: function( key, default_val ) {\n\t\tvar settings = this.get_settings();\n\t\treturn settings[ key ] ? settings[ key ] : default_val;\n\t},\n\n\t/**\n\t * Get the slug associated with a strength value\n\t *\n\t * @since  3.0.0\n\t *\n\t * @param int strength_val Strength value number.\n\t * @return string\n\t */\n\tget_strength_slug: function( strength_val ) {\n\n\t\tvar slugs = {\n\t\t\t'-1': 'too-short',\n\t\t\t1: 'very-weak',\n\t\t\t2: 'weak',\n\t\t\t3: 'medium',\n\t\t\t4: 'strong',\n\t\t\t5: 'mismatch',\n\t\t};\n\n\t\treturn ( slugs[ strength_val ] ) ? slugs[ strength_val ] : slugs[5];\n\n\t},\n\n\t/**\n\t * Gets the translated text associated with a strength value\n\t *\n\t * @since  3.0.0\n\t *\n\t * @param {Integer} strength_val Strength value\n\t * @return {String}\n\t */\n\tget_strength_text: function( strength_val ) {\n\n\t\tvar texts = {\n\t\t\t'-1': LLMS.l10n.translate( 'Too Short' ),\n\t\t\t1: LLMS.l10n.translate( 'Very Weak' ),\n\t\t\t2: LLMS.l10n.translate( 'Weak' ),\n\t\t\t3: LLMS.l10n.translate( 'Medium' ),\n\t\t\t4: LLMS.l10n.translate( 'Strong' ),\n\t\t\t5: LLMS.l10n.translate( 'Mismatch' ),\n\t\t};\n\n\t\treturn ( texts[ strength_val ] ) ? texts[ strength_val ] : texts[5];\n\n\t},\n\n\t/**\n\t * Get the value associated with a strength slug\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string strength_slug A strength slug.\n\t * @return {Integer}\n\t */\n\tget_strength_value: function( strength_slug ) {\n\n\t\tvar values = {\n\t\t\t'too-short': -1,\n\t\t\t'very-weak': 1,\n\t\t\tweak: 2,\n\t\t\tmedium: 3,\n\t\t\tstrong: 4,\n\t\t\tmismatch: 5,\n\t\t};\n\n\t\treturn ( values[ strength_slug ] ) ? values[ strength_slug ] : values.mismatch;\n\n\t},\n\n\t/**\n\t * Setup jQuery references to DOM elements needed to power the password meter.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {Boolean} Returns `true` if a meter element and password field are found, otherwise returns `false`.\n\t */\n\tsetup_references: function() {\n\n\t\tif ( ! this.$meter.length ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.$form = this.$meter.closest( 'form' );\n\t\tthis.$pass = this.$form.find( 'input#password' );\n\n\t\tif ( this.$pass.length && this.$pass.attr( 'data-match' ) ) {\n\t\t\tthis.$conf = this.$form.find( '#' + this.$pass.attr( 'data-match' ) );\n\t\t}\n\n\t\treturn ( this.$pass.length > 0 );\n\n\t},\n\n\t/**\n\t * Form submission handler for registration and update forms\n\t *\n\t * @since 3.0.0\n\t * @since 5.0.0 Allow the account edit for to bypass strength checking when the password field is disabled (not being submitted).\n\t *\n\t * @param obj e Event data.\n\t * @return void\n\t */\n\tsubmit: function( e ) {\n\n\t\tvar self = e.data;\n\t\te.preventDefault();\n\t\tself.$pass.trigger( 'keyup' );\n\n\t\t// Meets the status requirements OR we're on the account edit form and the password field is disabled.\n\t\tif ( self.get_current_strength_status() || ( self.$form.hasClass( 'edit-account' ) && 'disabled' === self.$pass.attr( 'disabled' ) ) ) {\n\t\t\tself.$form.off( 'submit', self.submit );\n\t\t\tself.$form.trigger( 'submit' );\n\t\t} else {\n\t\t\t$( 'html, body' ).animate( {\n\t\t\t\tscrollTop: self.$meter.offset().top - 100,\n\t\t\t}, 200 );\n\t\t\tself.$meter.hide();\n\t\t\tsetTimeout( function() {\n\t\t\t\tself.$meter.fadeIn( 400 );\n\t\t\t}, 220 );\n\t\t}\n\t},\n\n\t/**\n\t * Get the list of blocklist strings\n\t *\n\t * @since 3.0.0\n\t * @deprecated 5.0.0 `LLMS.PasswordStrength.get_blacklist()` is deprecated in favor of `LLMS.PasswordStrength.get_blocklist()`.\n\t *\n\t * @return array\n\t */\n\tget_blacklist: function() {\n\t\tconsole.log( 'Method `get_blacklist()` is deprecated in favor of `get_blocklist()`.' );\n\t\treturn this.get_blacklist();\n\t},\n\n} );\n"
  },
  {
    "path": "assets/js/app/llms-pricing-tables.js",
    "content": "/**\n * Pricing Table UI\n *\n * @package LifterLMS/Scripts\n *\n * @since  Unknown.\n * @version  Unknown.\n */\n\nLLMS.Pricing_Tables = {\n\n\t/**\n\t * Init\n\t */\n\tinit: function() {\n\n\t\tvar self = this;\n\n\t\tif ( $( 'body' ).hasClass( 'wp-admin' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( $( '.llms-access-plans' ).length ) {\n\n\t\t\tLLMS.wait_for_matchHeight( function() {\n\t\t\t\tself.bind();\n\t\t\t} );\n\n\t\t\tthis.$locked = $( 'a[href=\"#llms-plan-locked\"]' );\n\n\t\t\tif ( this.$locked.length ) {\n\n\t\t\t\tLLMS.wait_for_popover( function() {\n\t\t\t\t\tself.bind_locked();\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t}\n\n\t},\n\n\t/**\n\t * Bind Method\n\t * Handles dom binding on load\n\t *\n\t * @return {[type]} [description]\n\t */\n\tbind: function() {\n\n\t\t$( '.llms-access-plan-content' ).matchHeight();\n\t\t$( '.llms-access-plan-pricing.trial' ).matchHeight();\n\n\t},\n\n\t/**\n\t * Setup a popover for members-only restricted plans\n\t *\n\t * @return void\n\t * @since    3.0.0\n\t * @version  3.9.1\n\t */\n\tbind_locked: function() {\n\n\t\tthis.$locked.each( function() {\n\n\t\t\t$( this ).webuiPopover( {\n\t\t\t\tanimation: 'pop',\n\t\t\t\tcloseable: true,\n\t\t\t\tcontent: function( e ) {\n\t\t\t\t\tvar $content = $( '<div class=\"llms-members-only-restrictions\" />' );\n\t\t\t\t\t$content.append( e.$element.closest( '.llms-access-plan' ).find( '.llms-access-plan-restrictions ul' ).clone() );\n\t\t\t\t\treturn $content;\n\t\t\t\t},\n\t\t\t\tplacement: 'top',\n\t\t\t\tstyle: 'inverse',\n\t\t\t\ttitle: LLMS.l10n.translate( 'Members Only Pricing' ),\n\t\t\t\twidth: '280px',\n\t\t\t} );\n\n\t\t} );\n\n\t},\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-quiz-attempt.js",
    "content": "/**\n * Quiz Attempt\n *\n * @package LifterLMS/Scripts\n *\n * @since 7.3.0\n * @version 7.3.0\n */\n\nLLMS.Quiz_Attempt = {\n\t/**\n\t * Initialize\n\t *\n\t * @return void\n\t */\n\tinit: function() {\n\n\t\t$( '.llms-quiz-attempt-question-header a.toggle-answer' ).on( 'click', function( e ) {\n\n\t\t\te.preventDefault();\n\n\t\t\tvar $curr = $( this ).closest( 'header' ).next( '.llms-quiz-attempt-question-main' );\n\n\t\t\t$( this ).closest( 'li' ).siblings().find( '.llms-quiz-attempt-question-main' ).slideUp( 200 );\n\n\t\t\tif ( $curr.is( ':visible' ) ) {\n\t\t\t\t$curr.slideUp( 200 );\n\t\t\t}  else {\n\t\t\t\t$curr.slideDown( 200 );\n\t\t\t}\n\n\t\t} );\n\t}\n\n}\n"
  },
  {
    "path": "assets/js/app/llms-review.js",
    "content": "/**\n * LifterLMS Reviews JS\n *\n * @package LifterLMS/Scripts\n *\n * @since Unknown\n * @version Unknown\n */\n\nLLMS.Review = {\n\t/**\n\t * Init\n\t * loads class methods\n\t */\n\tinit: function() {\n\t\t// console.log('Initializing Review ');\n\t\tthis.bind();\n\t},\n\n\t/**\n\t * This function binds actions to the appropriate hooks\n\t */\n\tbind: function() {\n\t\t$( '#llms_review_submit_button' ).click(function()\n\t\t\t{\n\t\t\tif ($( '#review_title' ).val() !== '' && $( '#review_text' ).val() !== '') {\n\t\t\t\tjQuery.ajax({\n\t\t\t\t\ttype : 'post',\n\t\t\t\t\tdataType : 'json',\n\t\t\t\t\turl : window.llms.ajaxurl,\n\t\t\t\t\tdata : {\n\t\t\t\t\t\taction : 'LLMSSubmitReview',\n\t\t\t\t\t\treview_title: $( '#review_title' ).val(),\n\t\t\t\t\t\treview_text: $( '#review_text' ).val(),\n\t\t\t\t\t\tpageID : $( '#post_ID' ).val(),\n\t\t\t\t\t\tllms_review_nonce: $( '#llms_review_nonce' ).val()\n\t\t\t\t\t},\n\t\t\t\t\tsuccess: function()\n\t\t\t\t\t{\n\t\t\t\t\t\tconsole.log( 'Review success' );\n\t\t\t\t\t\t$( '#review_box' ).hide( 'swing' );\n\t\t\t\t\t\t$( '#thank_you_box' ).show( 'swing' );\n\t\t\t\t\t},\n\t\t\t\t\terror: function(jqXHR, textStatus, errorThrown )\n\t\t\t\t\t{\n\t\t\t\t\t\tconsole.log( jqXHR );\n\t\t\t\t\t\tconsole.log( textStatus );\n\t\t\t\t\t\tconsole.log( errorThrown );\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tif ($( '#review_title' ).val() === '') {\n\t\t\t\t\t$( '#review_title_error' ).show( 'swing' );\n\t\t\t\t} else {\n\t\t\t\t\t$( '#review_title_error' ).hide( 'swing' );\n\t\t\t\t}\n\t\t\t\tif ($( '#review_text' ).val() === '') {\n\t\t\t\t\t$( '#review_text_error' ).show( 'swing' );\n\t\t\t\t} else {\n\t\t\t\t\t$( '#review_text_error' ).hide( 'swing' );\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tif ( $( '#_llms_display_reviews' ).attr( 'checked' ) ) {\n\t\t\t$( '.llms-num-reviews-top' ).addClass( 'top' );\n\t\t\t$( '.llms-num-reviews-bottom' ).show();\n\n\t\t} else {\n\t\t\t$( '.llms-num-reviews-bottom' ).hide();\n\t\t}\n\t\t$( '#_llms_display_reviews' ).change(function() {\n\t\t\tif ( $( '#_llms_display_reviews' ).attr( 'checked' ) ) {\n\t\t\t\t$( '.llms-num-reviews-top' ).addClass( 'top' );\n\t\t\t\t$( '.llms-num-reviews-bottom' ).show();\n\t\t\t} else {\n\t\t\t\t$( '.llms-num-reviews-top' ).removeClass( 'top' );\n\t\t\t\t$( '.llms-num-reviews-bottom' ).hide();\n\t\t\t}\n\t\t});\n\n\t},\n};\n"
  },
  {
    "path": "assets/js/app/llms-storage.js",
    "content": "/* global LLMS, $ */\n\n// = include ../vendor/js.cookie.js\n\n/**\n * Create a no conflict reference to JS Cookies.\n *\n * @type {Object}\n */\nLLMS.CookieStore = Cookies.noConflict();\n\n/**\n * Store information in Local Storage by group.\n *\n * @since 3.36.0\n * @since 3.37.14 Use persistent reference to JS Cookies.\n * @since 4.2.0 Set sameSite to `strict` for cookies.\n *\n * @param string group Storage group id/name.\n */\nLLMS.Storage = function( group ) {\n\n\tvar self = this,\n\t\tstore = LLMS.CookieStore;\n\n\t/**\n\t * Clear all data for the group.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tthis.clearAll = function() {\n\t\tstore.remove( group );\n\t};\n\n\t/**\n\t * Clear a single item from the group by key.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return obj\n\t */\n\tthis.clear = function( key ) {\n\t\tvar data = self.getAll();\n\t\tdelete data[ key ];\n\t\treturn store.set( group, data );\n\t};\n\n\t/**\n\t * Retrieve (and parse) all data stored for the group.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return obj\n\t */\n\tthis.getAll = function() {\n\t\treturn store.getJSON( group ) || {};\n\t}\n\n\t/**\n\t * Retrieve an item from the group by key.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param string key Item key/name.\n\t * @param mixed default_val Item default value to be returned when item not found in the group.\n\t * @return mixed\n\t */\n\tthis.get = function( key, default_val ) {\n\t\tvar data = self.getAll();\n\t\treturn data[ key ] ? data[ key ] : default_val;\n\t}\n\n\t/**\n\t * Store an item in the group by key.\n\t *\n\t * @since 3.36.0\n\t * @since 4.2.0 Set sameSite to `strict` for cookies.\n\t *\n\t * @param string key Item key name.\n\t * @param mixed val Item value\n\t * @return obj\n\t */\n\tthis.set = function( key, val ) {\n\t\tvar data = self.getAll();\n\t\tdata[ key ] = val;\n\t\treturn store.set( group, data, { sameSite: 'strict' } );\n\t};\n\n}\n"
  },
  {
    "path": "assets/js/app/llms-student-dashboard.js",
    "content": "/**\n * Student Dashboard related JS\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.7.0\n * @since 3.10.0 Bind events on the orders screen.\n * @since 5.0.0 Removed redundant password toggle logic for edit account screen.\n * @version 5.0.0\n */\nLLMS.StudentDashboard = {\n\n\t/**\n\t * Slug for the current screen/endpoint\n\t *\n\t * @type  {String}\n\t */\n\tscreen: '',\n\n\t/**\n\t * Init\n\t *\n\t * @since 3.7.0\n\t * @since 3.10.0 Unknown\n\t * @since 5.0.0 Removed password toggle logic.\n\t *\n\t * @return void\n\t */\n\tinit: function() {\n\n\t\tif ( $( '.llms-student-dashboard' ).length ) {\n\t\t\tthis.bind();\n\t\t\tif ( 'orders' === this.get_screen() ) {\n\t\t\t\tthis.bind_orders();\n\t\t\t}\n\t\t}\n\n\t},\n\n\t/**\n\t * Bind DOM events\n\t *\n\t * @since 3.7.0\n\t * @since 3.7.4 Unknown.\n\t * @since 5.0.0 Removed password toggle logic.\n\t *\n\t * @return   void\n\t */\n\tbind: function() {\n\n\t\t$( '.llms-donut' ).each( function() {\n\t\t\tLLMS.Donut( $( this ) );\n\t\t} );\n\n\t},\n\n\t/**\n\t * Bind events related to the orders screen on the dashboard\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return void\n\t */\n\tbind_orders: function() {\n\n\t\t$( '#llms-cancel-subscription-form' ).on( 'submit', this.order_cancel_warning );\n\t\t$( '#llms_update_payment_method' ).on( 'click', function() {\n\t\t\t$( 'input[name=\"llms_payment_gateway\"]:checked' ).trigger( 'change' );\n\t\t\t$( this ).closest( 'form' ).find( '.llms-switch-payment-source-main' ).slideToggle( '200' );\n\t\t} );\n\n\t},\n\n\t/**\n\t * Get the current dashboard endpoint/tab slug\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return void\n\t */\n\tget_screen: function() {\n\t\tif ( ! this.screen ) {\n\t\t\tthis.screen = $( '.llms-student-dashboard' ).attr( 'data-current' );\n\t\t}\n\t\treturn this.screen;\n\t},\n\n\t/**\n\t * Show a confirmation warning when Cancel Subscription form is submitted\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param obj e JS event data.\n\t * @return void\n\t */\n\torder_cancel_warning: function( e ) {\n\t\te.preventDefault();\n\t\tvar msg = LLMS.l10n.translate( 'Are you sure you want to cancel your subscription?' );\n\t\tif ( window.confirm( LLMS.l10n.translate( msg ) ) ) {\n\t\t\t$( this ).off( 'submit', this.order_cancel_warning );\n\t\t\t$( this ).submit();\n\t\t}\n\t},\n\n};\n"
  },
  {
    "path": "assets/js/app/llms-tracking.js",
    "content": "/* global LLMS, $ */\n\n/**\n * User event/interaction tracking.\n *\n * @since 3.36.0\n * @since 3.36.2 Fix JS error when settings aren't loaded.\n * @since 3.37.2 When adding an event to the storae also make sure the nonce is set for server-side verification.\n * @since 3.37.9 Fix IE compatibility issue related to usage of `Object.assign()`.\n * @since 3.37.14 Persist the tracking events via ajax when reaching the cookie size limit.\n * @since 5.0.0 Set `settings` as an empty object when no settings supplied.\n * @since 7.1.0 Only attempt to add a nonce to the datastore when a nonce exists in the settings object.\n */\nLLMS.Tracking = function( settings ) {\n\n\tsettings = settings || {};\n\n\tvar self = this,\n\t\tstore = new LLMS.Storage( 'llms-tracking' );\n\n\tsettings = 'string' === typeof settings ? JSON.parse( settings ) : settings;\n\n\t/**\n\t * Initialize / Bind all tracking event listeners.\n\t *\n\t * @since 3.36.0\n\t * @since 5.0.0 Only attempt to add a nonce to the datastore when a nonce exists in the settings object.\n\t * @since 7.1.0 Do not add a nonce to the datastore by default, will be added/updated\n\t *              when storing an event to track.\n\t *\n\t * @return {void}\n\t */\n\tfunction init() {\n\n\t\tself.addEvent( 'page.load' );\n\n\t\twindow.addEventListener( 'beforeunload', onBeforeUnload );\n\t\twindow.addEventListener( 'pagehide', onUnload );\n\n\t\tdocument.addEventListener( 'visibilitychange', onVisibilityChange );\n\n\t};\n\n\t/**\n\t * Add an event.\n\t *\n\t * @since 3.36.0\n\t * @since 3.36.2 Fix error when settings aren't loaded.\n\t * @since 3.37.2 Always make sure the nonce is set for server-side verification.\n\t * @since 3.37.14 Persist the tracking events via ajax when reaching the cookie size limit.\n\t * @since 7.1.0 Only attempt to add a nonce to the datastore when a nonce exists in the settings object.\n\t *\n\t * @param string|obj event Event Id (type.event) or a full event object from `this.makeEventObj()`.\n\t * @param int args Optional additional arguments to pass to `this.makeEventObj()`.\n\t * @return {void}\n\t */\n\tthis.addEvent = function( event, args ) {\n\n\t\targs  = args || {};\n\t\tif ( 'string' === typeof event ) {\n\t\t\targs.event = event;\n\t\t}\n\n\t\t// If the event isn't registered in the settings don't proceed.\n\t\tif ( !settings.events || -1 === settings.events.indexOf( args.event ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure the nonce is set for server-side verification.\n\t\tif ( settings.nonce ) {\n\t\t\tstore.set( 'nonce', settings.nonce );\n\t\t}\n\n\t\tevent = self.makeEventObj( args );\n\n\t\tvar all = store.get( 'events', [] );\n\t\tall.push( event );\n\t\tstore.set( 'events', all );\n\n\t\t// If couldn't store the latest event because of size limits.\n\t\tif (  settings.saving_frequency === 'always' || all.length > store.get( 'events', [] ).length ) {\n\n\t\t\t// Copy the cookie in a temporary variable.\n\t\t\tvar _temp = store.getAll();\n\t\t\t// Clear the events from the cookie.\n\t\t\tstore.clear('events');\n\n\t\t\t// Add the latest event to the temporary variable.\n\t\t\t_temp['events'].push( event );\n\n\t\t\t// Send the temporary variable as string via ajax.\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'persist_tracking_events',\n\t\t\t\t\t'llms-tracking': JSON.stringify(_temp)\n\t\t\t\t},\n\n\t\t\t\terror: function( xhr, status, error ) {\n\n\t\t\t\t\tconsole.log( xhr, status, error );\n\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tif ( 'error' === r.code ) {\n\t\t\t\t\t\tconsole.log(r.code, r.message);\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieve initialization settings.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return obj\n\t */\n\tthis.getSettings = function() {\n\t\treturn settings;\n\t}\n\n\t/**\n\t * Create an event object suitable to save as an event.\n\t *\n\t * @since 3.36.0\n\t * @since 3.37.9 Use `$.extend()` in favor of `Object.assign()`.\n\t *\n\t * @param obj event {\n\t *     Event hash\n\t *\n\t *     @param {string} event (Required) Event ID, eg: \"page.load\".\n\t *     @param {url} url Event URL. (Optional, added automatically) Stored as metadata and used to infer an object_id for post events.\n\t *     @param {time} float (Optional, added automatically) Timestamp (in milliseconds). Used for the event creation date.\n\t *     @param {int} obj_id (Optional). The object ID. Inferred automatically via `url` if not provided.\n\t *     @param {obj} meta (Optional) Hash of metadata to store with the event.\n\t * }\n\t * @return obj\n\t */\n\tthis.makeEventObj = function( event ) {\n\t\treturn $.extend( event, {\n\t\t\turl: window.location.href,\n\t\t\ttime: Math.round( new Date().getTime() / 1000 ),\n\t\t} );\n\t}\n\n\n\t/**\n\t * Remove the visibility change event listener on window.beforeunload\n\t *\n\t * Prevents actual unloading from recording a blur event from the visibility change listener\n\t *\n\t * @param obj e JS event object.\n\t * @return void\n\t */\n\tfunction onBeforeUnload( e ) {\n\t\tdocument.removeEventListener( 'visibilitychange', onVisibilityChange );\n\t}\n\n\t/**\n\t * Record a `page.exit` event on window.unload.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param obj e JS event object.\n\t * @return void\n\t */\n\tfunction onUnload( e ) {\n\t\tself.addEvent( 'page.exit' );\n\t}\n\n\t/**\n\t * Record `page.blur` and `page.focus` events via document.visilibitychange events.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param obj e JS event object.\n\t * @return void\n\t */\n\tfunction onVisibilityChange( e ) {\n\n\t\tvar event = document.hidden ? 'page.blur' : 'page.focus';\n\t\tself.addEvent( event );\n\n\t}\n\n\t// Initialize on the frontend only.\n\tif ( ! $( 'body' ).hasClass( 'wp-admin' ) ) {\n\t\tinit();\n\t}\n\n};\n\nllms.tracking = new LLMS.Tracking( llms.tracking );\n"
  },
  {
    "path": "assets/js/app/llms-visibility-toggle.js",
    "content": "/**\n * Handle Password Visibility Toggle for LifterLMS Forms\n *\n * @package LifterLMS/Scripts\n *\n * @since TBD\n */\n\nLLMS.PasswordVisibility = {\n\n\t/**\n\t * Initialize references and setup event binding\n\t *\n\t * @since TBD\n\t * @return void\n\t */\n\tinit: function() {\n\t\tthis.$toggleButtons = $( '.llms-visibility-toggle button' );\n\n\t\tif ( this.$toggleButtons.length ) {\n\t\t\tthis.$toggleButtons.removeClass( 'hide-if-no-js' );\n\t\t\tthis.bind();\n\t\t}\n\t},\n\n\t/**\n\t * Bind DOM events for toggle buttons\n\t *\n\t * @since TBD\n\t * @return void\n\t */\n\tbind: function() {\n\t\tvar self = this;\n\n\t\t// Remove any previous click events and bind the new click event\n\t\tthis.$toggleButtons.off('click').on('click', function(event) {\n\t\t\tself.toggleVisibility( $(this) );\n\t\t});\n\t},\n\n\t/**\n\t * Toggle visibility of password fields\n\t *\n\t * @since TBD\n\t * @param {Object} $button The jQuery object of the clicked button\n\t * @return void\n\t */\n\ttoggleVisibility: function( $button ) {\n\t\tvar isVisible = parseInt( $button.attr('data-toggle'), 10 );\n\t\tvar $form = $button.closest( '.llms-form-fields' );\n\t\tvar $passwordFields = $form.find( 'input.llms-field-input' );\n\t\tvar $icon = $button.find( 'i' );\n\t\tvar $stateText = $button.find( '.llms-visibility-toggle-state' );\n\n\t\t// Toggle the visibility state\n\t\tif ( isVisible === 1 ) {\n\t\t\t// Show password\n\t\t\t$passwordFields.filter('[type=\"password\"]').attr('type', 'text');\n\t\t\t$button.attr('data-toggle', 0);\n\t\t\t$icon.removeClass('fa-eye').addClass('fa-eye-slash');\n\t\t\t$stateText.text(LLMS.l10n.translate('Hide Password'));\n\t\t} else {\n\t\t\t// Hide password\n\t\t\t$passwordFields.filter('[type=\"text\"]').attr('type', 'password');\n\t\t\t$button.attr('data-toggle', 1);\n\t\t\t$icon.removeClass('fa-eye-slash').addClass('fa-eye');\n\t\t\t$stateText.text(LLMS.l10n.translate('Show Password'));\n\t\t}\n\t}\n\n};\n\n// Initialize the Password Visibility module on document ready\njQuery(document).ready(function($) {\n\tLLMS.PasswordVisibility.init();\n});\n"
  },
  {
    "path": "assets/js/app/rest.js",
    "content": "/**\n * Rest Methods\n * Manages URL and Rest object parsing\n *\n * @package LifterLMS/Scripts\n *\n * @since Unknown\n * @version  Unknown\n */\n\nLLMS.Rest = {\n\n\t/**\n\t * Init\n\t * loads class methods\n\t */\n\tinit: function() {\n\t\tthis.bind();\n\t},\n\n\t/**\n\t * Bind Method\n\t * Handles dom binding on load\n\t *\n\t * @return {[type]} [description]\n\t */\n\tbind: function() {\n\t},\n\n\t/**\n\t * Searches for string matches in url path\n\t *\n\t * @param  {Array}  strings [Array of strings to search for matches]\n\t * @return {Boolean}         [Was a match found?]\n\t */\n\tis_path: function( strings ) {\n\n\t\tvar path_exists = false,\n\t\t\turl         = window.location.href;\n\n\t\tfor ( var i = 0; i < strings.length; i++ ) {\n\n\t\t\tif ( url.search( strings[i] ) > 0 && ! path_exists ) {\n\n\t\t\t\tpath_exists = true;\n\t\t\t}\n\t\t}\n\n\t\treturn path_exists;\n\t},\n\n\t/**\n\t * Retrieves query variables\n\t *\n\t * @return {[Array]} [array object of query variable key=>value pairs]\n\t */\n\tget_query_vars: function() {\n\n\t\tvar vars   = [], hash,\n\t\t\thashes = window.location.href.slice( window.location.href.indexOf( '?' ) + 1 ).split( '&' );\n\n\t\tfor (var i = 0; i < hashes.length; i++) {\n\t\t\thash = hashes[i].split( '=' );\n\t\t\tvars.push( hash[0] );\n\t\t\tvars[hash[0]] = hash[1];\n\t\t}\n\n\t\treturn vars;\n\t}\n\n};\n"
  },
  {
    "path": "assets/js/builder/Collections/Lessons.js",
    "content": "/**\n * Lessons Collection\n *\n * @since    3.13.0\n * @version  3.17.0\n */\ndefine( [ 'Models/Lesson' ], function( model ) {\n\n\treturn Backbone.Collection.extend( {\n\n\t\t/**\n\t\t * Model for collection items\n\t\t *\n\t\t * @type  obj\n\t\t */\n\t\tmodel: model,\n\n\t\t/**\n\t\t * Initializer\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\t// reorder called by LessonList view when sortable drops occur\n\t\t\tthis.on( 'reorder', this.on_reorder );\n\n\t\t\t// when a lesson is added or removed, update order\n\t\t\tthis.on( 'add', this.on_reorder );\n\t\t\tthis.on( 'remove', this.on_reorder );\n\n\t\t},\n\n\t\t/**\n\t\t * On lesson reorder callback\n\t\t *\n\t\t * Update the order attr of each lesson to reflect the new lesson order\n\t\t * Validate prerequisite (if set) and unset it if it's no longer a valid prereq\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\ton_reorder: function() {\n\t\t\tthis.update_order();\n\t\t\tthis.validate_prereqs();\n\t\t},\n\n\t\t/**\n\t\t * Update lesson order attribute of all lessons when lessons are reordered\n\t\t *\n\t\t * @return      void\n\t\t * @since       3.16.0\n\t\t * @version     3.17.0\n\t\t */\n\t\tupdate_order: function() {\n\n\t\t\tthis.each( function( lesson ) {\n\t\t\t\tlesson.set( 'order', this.indexOf( lesson ) + 1 );\n\t\t\t}, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Validate prerequisite (if set) and unset it if it's no longer a valid prereq\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tvalidate_prereqs: function() {\n\n\t\t\tthis.each( function( lesson ) {\n\n\t\t\t\t// validate prereqs\n\t\t\t\tif ( 'yes' === lesson.get( 'has_prerequisite' ) ) {\n\t\t\t\t\tvar valid = _.pluck( _.flatten( _.pluck( lesson.get_available_prereq_options(), 'options' ) ), 'key' );\n\t\t\t\t\tif ( -1 === valid.indexOf( lesson.get( 'prerequisite' ) * 1 ) ) {\n\t\t\t\t\t\tlesson.set( {\n\t\t\t\t\t\t\tprerequisite: 0,\n\t\t\t\t\t\t\thas_prerequisite: 'no',\n\t\t\t\t\t\t} );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}, this );\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Collections/QuestionChoices.js",
    "content": "/**\n * Question Choice Collection\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Models/QuestionChoice' ], function( model ) {\n\n\treturn Backbone.Collection.extend( {\n\n\t\t/**\n\t\t * Model for collection items\n\t\t *\n\t\t * @type  obj\n\t\t */\n\t\tmodel: model,\n\n\t\tinitialize: function() {\n\n\t\t\t// reorder called by QuestionList view when sortable drops occur\n\t\t\tthis.on( 'reorder', this.update_order );\n\n\t\t\t// when a choice is added or removed, update order\n\t\t\tthis.on( 'add', this.update_order );\n\t\t\tthis.on( 'remove', this.update_order );\n\n\t\t\t// when a choice is added or remove, ensure min/max correct answers exist\n\t\t\tthis.on( 'add', this.update_correct );\n\t\t\tthis.on( 'remove', this.update_correct );\n\n\t\t\t// when a choice is toggled, ensure min/max correct exist\n\t\t\tthis.on( 'correct-update', this.update_correct );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the number of correct choices in the collection\n\t\t *\n\t\t * @return   int\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tcount_correct: function() {\n\n\t\t\treturn _.size( this.get_correct() );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the collection reduced to only correct choices\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_correct: function() {\n\t\t\treturn this.filter( function( choice ) {\n\t\t\t\treturn choice.get( 'correct' );\n\t\t\t} );\n\t\t},\n\n\t\t/**\n\t\t * Ensure min/max correct choices exist in the collection based on the question's settings\n\t\t *\n\t\t * @param    obj      choice  model of the choice that was toggled\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_correct: function( choice ) {\n\n\t\t\tif ( ! this.parent.get( 'question_type' ).get_choice_selectable() ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar siblings = this.without( choice ), // exclude the toggled choice from loops\n\t\t\t\tquestion = this.parent;\n\n\t\t\t// if multiple choices aren't enabled turn all other choices to incorrect\n\t\t\tif ( 'no' === question.get( 'multi_choices' ) ) {\n\t\t\t\t_.each( siblings, function( model ) {\n\t\t\t\t\tmodel.set( 'correct', false );\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\t// if we don't have a single correct answer & the question has points, set one\n\t\t\t// allows users to create quizzes / questions with no points and therefore no correct answers are allowed\n\t\t\tif ( 0 === this.count_correct() && question.get( 'points' ) > 0 ) {\n\t\t\t\tvar models = 1 === this.size() ? this.models : siblings;\n\t\t\t\t_.first( models ).set( 'correct', true );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Update the marker attr of each choice in the list to reflect the order of the collection\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_order: function() {\n\n\t\t\tvar self     = this,\n\t\t\t\tquestion = this.parent;\n\n\t\t\tthis.each( function( choice ) {\n\t\t\t\tchoice.set( 'marker', question.get( 'question_type' ).get_choice_markers()[ self.indexOf( choice ) ] );\n\t\t\t} );\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Collections/QuestionTypes.js",
    "content": "/**\n * Quiz Question Type Collection\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Models/QuestionType' ], function( model ) {\n\n\treturn Backbone.Collection.extend( {\n\n\t\t/**\n\t\t * Model for collection items\n\t\t *\n\t\t * @type  obj\n\t\t */\n\t\tmodel: model,\n\n\t\t/**\n\t\t * Initializer\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.on( 'add', this.comparator );\n\t\t\tthis.on( 'remove', this.comparator );\n\n\t\t},\n\n\t\t/**\n\t\t * Comparator (sorts collection)\n\t\t *\n\t\t * @param    obj   model  QuestionType model\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tcomparator: function( model ) {\n\n\t\t\treturn model.get( 'group' ).order;\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Collections/Questions.js",
    "content": "/**\n * Questions Collection\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Models/Question' ], function( model ) {\n\n\treturn Backbone.Collection.extend( {\n\n\t\t/**\n\t\t * Model for collection items\n\t\t *\n\t\t * @type  obj\n\t\t */\n\t\tmodel: model,\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\t// reorder called by QuestionList view when sortable drops occur\n\t\t\tthis.on( 'reorder', this.update_order );\n\n\t\t\t// when a question is added or removed, update order\n\t\t\tthis.on( 'add', this.update_order );\n\t\t\tthis.on( 'remove', this.update_order );\n\n\t\t\tthis.on( 'add', this.update_parent );\n\n\t\t},\n\n\t\t/**\n\t\t * Update the order attr of each question in the list to reflect the order of the collection\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_order: function() {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.each( function( question ) {\n\n\t\t\t\tquestion.set( 'menu_order', self.indexOf( question ) + 1 );\n\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * When adding a question to a question list, update the question's parent\n\t\t * Will ensure that questions moved into and out of groups always have the correct parent_id\n\t\t *\n\t\t * @param    obj   model  instance of the question model\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_parent: function( model ) {\n\n\t\t\tmodel.set( 'parent_id', this.parent.get( 'id' ) );\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Collections/Sections.js",
    "content": "/**\n * Sections Collection\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Models/Section' ], function( model ) {\n\n\treturn Backbone.Collection.extend( {\n\n\t\t/**\n\t\t * Model for collection items\n\t\t *\n\t\t * @type  obj\n\t\t */\n\t\tmodel: model,\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tvar self = this;\n\n\t\t\t// reorder called by SectionList view when sortable drops occur\n\t\t\tthis.on( 'reorder', this.update_order );\n\n\t\t\t// when a section is added or removed, update order\n\t\t\tthis.on( 'add', this.update_order );\n\t\t\tthis.on( 'remove', this.update_order );\n\n\t\t},\n\n\t\t/**\n\t\t * Update the order attr of each section in the list to reflect the order of the collection\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_order: function() {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.each( function( section ) {\n\n\t\t\t\tsection.set( 'order', self.indexOf( section ) + 1 );\n\n\t\t\t} );\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Collections/loader.js",
    "content": "/**\n * Lessons Collection\n *\n * @since    3.13.0\n * @version  3.16.0\n */\ndefine( [\n\t\t'Collections/Lessons',\n\t\t'Collections/QuestionChoices',\n\t\t'Collections/Questions',\n\t\t'Collections/QuestionTypes',\n\t\t'Collections/Sections'\n\t], function(\n\t\tLessons,\n\t\tQuestionChoices,\n\t\tQuestions,\n\t\tQuestionTypes,\n\t\tSections\n\t) {\n\n\t\treturn {\n\t\t\tLessons: Lessons,\n\t\t\tQuestionChoices: QuestionChoices,\n\t\t\tQuestions: Questions,\n\t\t\tQuestionTypes: QuestionTypes,\n\t\t\tSections: Sections,\n\t\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Controllers/Construct.js",
    "content": "/**\n * Constructor functions for constructing models, views, and collections\n *\n * @since    3.16.0\n * @version  3.17.1\n */\ndefine( [\n\t\t'Collections/loader',\n\t\t'Models/loader',\n\t\t'Views/_loader'\n\t], function(\n\t\tCollections,\n\t\tModels,\n\t\tViews\n\t) {\n\n\t\treturn function() {\n\n\t\t\t/**\n\t\t\t * Internal getter\n\t\t\t * Constructs new Collections, Models, and Views\n\t\t\t *\n\t\t\t * @param    obj      type     type of object to construct [Collection,Model,View]\n\t\t\t * @param    string   name     name of the object to construct\n\t\t\t * @param    obj      data     object data to pass into the object's constructor\n\t\t\t * @param    obj      options  object options to pass into the constructor\n\t\t\t * @return   obj\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tfunction get( type, name, data, options ) {\n\n\t\t\t\tif ( ! type[ name ] ) {\n\t\t\t\t\tconsole.log( '\"' + name + '\" not found.' );\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\treturn new type[ name ]( data, options );\n\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Instantiate a collection\n\t\t *\n\t\t\t * @param    string   name     Collection class name (EG: \"Sections\")\n\t\t\t * @param    array    data     Array of model objects to pass to the constructor\n\t\t\t * @param    obj      options  Object of options to pass to the constructor\n\t\t\t * @return   obj\n\t\t\t * @since    3.17.0\n\t\t\t * @version  3.17.0\n\t\t\t */\n\t\t\tthis.get_collection = function( name, data, options ) {\n\n\t\t\t\treturn get( Collections, name, data, options );\n\n\t\t\t};\n\n\t\t\t/**\n\t\t\t * Instantiate a model\n\t\t\t *\n\t\t\t * @param    string   name     Model class name (EG: \"Section\")\n\t\t\t * @param    obj      data     Object of model attributes to pass to the constructor\n\t\t\t * @param    obj      options  Object of options to pass to the constructor\n\t\t\t * @return   obj\n\t\t\t * @since    3.17.0\n\t\t\t * @version  3.17.0\n\t\t\t */\n\t\t\tthis.get_model = function( name, data, options ) {\n\n\t\t\t\treturn get( Models, name, data, options );\n\n\t\t\t};\n\n\t\t\t/**\n\t\t\t * Let 3rd parties extend a view using any of the mixin (_) views\n\t\t\t *\n\t\t\t * @param    {obj}     view     base object used for the view\n\t\t\t * @param... {string}  extends  any number of strings that should be mixed into the view\n\t\t\t * @return   obj\n\t\t\t * @since    3.17.1\n\t\t\t * @version  3.17.1\n\t\t\t */\n\t\t\tthis.extend_view = function() {\n\n\t\t\t\tvar view = arguments[0],\n\t\t\t\ti        = 1;\n\n\t\t\t\twhile ( arguments[ i ] ) {\n\n\t\t\t\t\tvar classname = arguments[ i ];\n\t\t\t\t\tif ( Views[ classname ] ) {\n\n\t\t\t\t\t\tif ( view.events && Views[ classname ].events ) {\n\t\t\t\t\t\t\tview.events = _.defaults( view.events, Views[ classname ].events );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tview = _.defaults( view, Views[ classname ] );\n\n\t\t\t\t\t}\n\n\t\t\t\t\ti++;\n\t\t\t\t}\n\n\t\t\t\treturn Backbone.View.extend( view );\n\n\t\t\t};\n\n\t\t\t/**\n\t\t\t * Allows custom collection registration by extending the default BackBone collection\n\t\t\t *\n\t\t\t * @param    string   name   model name\n\t\t\t * @param    obj      props  properties to extend the collection with\n\t\t\t * @return   void\n\t\t\t * @since    3.17.1\n\t\t\t * @version  3.17.1\n\t\t\t */\n\t\t\tthis.register_collection = function( name, props ) {\n\n\t\t\t\tCollections[ name ] = Backbone.Collection.extend( props );\n\n\t\t\t};\n\n\t\t\t/**\n\t\t\t * Allows custom model registration by extending the default abstract model\n\t\t\t *\n\t\t\t * @param    string   name   model name\n\t\t\t * @param    obj      props  properties to extend the abstract model with\n\t\t\t * @return   void\n\t\t\t * @since    3.17.0\n\t\t\t * @version  3.17.0\n\t\t\t */\n\t\t\tthis.register_model = function( name, props ) {\n\n\t\t\t\tModels[ name ] = Models['Abstract'].extend( props );\n\n\t\t\t};\n\n\t\t\treturn this;\n\n\t\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Controllers/Debug.js",
    "content": "/**\n * LifterLMS Builder Debugging suite\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn function( settings ) {\n\n\t\tvar self    = this,\n\t\t\tenabled = settings.enabled || false;\n\n\t\t/**\n\t\t * Disable debugging\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tthis.disable = function() {\n\n\t\t\tself.log( 'LifterLMS Builder debugging disabled' );\n\t\t\tenabled = false;\n\n\t\t};\n\n\t\t/**\n\t\t * Enable debugging\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tthis.enable = function() {\n\n\t\t\tenabled = true;\n\t\t\tself.log( 'LifterLMS Builder debugging enabled' );\n\n\t\t};\n\n\t\t/**\n\t\t * General logging function\n\t\t * Logs to the js console only if logging is enabled\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tthis.log = function() {\n\n\t\t\tif ( ! enabled ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t_.each( arguments, function( data ) {\n\t\t\t\tconsole.log( data );\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Toggles current state of the logger on or off\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tthis.toggle = function() {\n\n\t\t\tif ( enabled ) {\n\t\t\t\tself.disable();\n\t\t\t} else {\n\t\t\t\tself.enable();\n\t\t\t}\n\n\t\t};\n\n\t\t// on startup, log a message if logging is enabled\n\t\tif ( enabled ) {\n\t\t\tself.enable();\n\t\t}\n\n\t}\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Controllers/Schemas.js",
    "content": "/**\n * Model schema functions\n *\n * @since    3.17.0\n * @version  3.17.0\n */\ndefine( [], function() {\n\n\t/**\n\t * Main Schemas class\n\t *\n\t * @param    obj   schemas  schemas definitions initialized via PHP filters\n\t * @return   obj\n\t * @since    3.17.0\n\t * @version  3.17.0\n\t */\n\treturn function( schemas ) {\n\n\t\t// initialize any custom schemas defined via PHP\n\t\tvar custom_schemas = schemas;\n\t\t_.each( custom_schemas, function( type ) {\n\t\t\t_.each( type, function( schema ) {\n\t\t\t\tschema.custom = true;\n\t\t\t} );\n\t\t} );\n\n\t\t/**\n\t\t * Retrieve a schema for a given model by type\n\t\t * Extends default schemas definitions with custom 3rd party definitions\n\t\t *\n\t\t * @param    obj      schema      default schema definition from the model (or empty object if none defined)\n\t\t * @param    string   model_type  the model type ('lesson', 'quiz', etc)\n\t\t * @param    obj      model       Instance of the Backbone.Model for the given model\n\t\t * @return   obj\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tthis.get = function( schema, model_type, model ) {\n\n\t\t\t// extend the default schema with custom php schemas for the type if they exist\n\t\t\tif ( custom_schemas[ model_type ] ) {\n\t\t\t\tschema = _.extend( schema, custom_schemas[ model_type ] );\n\t\t\t}\n\n\t\t\treturn schema;\n\n\t\t};\n\n\t\treturn this;\n\n\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Controllers/Sync.js",
    "content": "/**\n * Sync builder data to the server\n *\n * @since 3.16.0\n * @version 4.17.0\n */\ndefine( [], function() {\n\n\treturn function( Course, settings ) {\n\n\t\tthis.saving = false;\n\n\t\tvar self              = this,\n\t\t\tautosave          = ( 'yes' === window.llms_builder.autosave ),\n\t\t\tcheck_interval    = null,\n\t\t\tcheck_interval_ms = settings.check_interval_ms || ( ( 'yes' === window.llms_builder.autosave ) ? 10000 : 1000 ),\n\t\t\tdetached          = new Backbone.Collection(),\n\t\t\ttrashed           = new Backbone.Collection();\n\n\t\t/**\n\t\t * init\n\t\t *\n\t\t * @since 3.16.7\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tfunction init() {\n\n\t\t\t// determine if autosaving is possible\n\t\t\tif ( 'undefined' === typeof wp.heartbeat ) {\n\n\t\t\t\twindow.llms_builder.debug.log( 'WordPress Heartbeat disabled. Autosaving is disabled!' );\n\t\t\t\tautosave = false;\n\n\t\t\t}\n\n\t\t\t// setup the check interval\n\t\t\tif ( check_interval_ms ) {\n\t\t\t\tself.set_check_interval( check_interval_ms );\n\t\t\t}\n\n\t\t\t// warn when users attempt to leave the page\n\t\t\t$( window ).on( 'beforeunload', function() {\n\n\t\t\t\tif ( self.has_unsaved_changes() ) {\n\t\t\t\t\tcheck_for_changes();\n\t\t\t\t\treturn 'Are you sure you want to abandon your changes?';\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/*\n\t\t\t  /$$             /$$                                             /$$                           /$$\n\t\t\t |__/            | $$                                            | $$                          |__/\n\t\t\t  /$$ /$$$$$$$  /$$$$$$    /$$$$$$   /$$$$$$  /$$$$$$$   /$$$$$$ | $$        /$$$$$$   /$$$$$$  /$$\n\t\t\t | $$| $$__  $$|_  $$_/   /$$__  $$ /$$__  $$| $$__  $$ |____  $$| $$       |____  $$ /$$__  $$| $$\n\t\t\t | $$| $$  \\ $$  | $$    | $$$$$$$$| $$  \\__/| $$  \\ $$  /$$$$$$$| $$        /$$$$$$$| $$  \\ $$| $$\n\t\t\t | $$| $$  | $$  | $$ /$$| $$_____/| $$      | $$  | $$ /$$__  $$| $$       /$$__  $$| $$  | $$| $$\n\t\t\t | $$| $$  | $$  |  $$$$/|  $$$$$$$| $$      | $$  | $$|  $$$$$$$| $$      |  $$$$$$$| $$$$$$$/| $$\n\t\t\t |__/|__/  |__/   \\___/   \\_______/|__/      |__/  |__/ \\_______/|__/       \\_______/| $$____/ |__/\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t | $$\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t | $$\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t |__/\n\t\t */\n\n\t\t/**\n\t\t * Adds error message(s) to the data object returned by heartbeat-tick\n\t\t *\n\t\t * @param    obj            data  llms_builder data object from heartbeat-tick\n\t\t * @param    string|array   err   error messages array or string\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tfunction add_error_msg( data, err ) {\n\n\t\t\tif ( 'success' === data.status ) {\n\t\t\t\tdata.message = [];\n\t\t\t}\n\n\t\t\tdata.status = 'error';\n\t\t\tif ( 'string' === typeof err ) {\n\t\t\t\terr = [ err ];\n\t\t\t}\n\t\t\tdata.message = data.message.concat( err );\n\n\t\t\treturn data;\n\n\t\t};\n\n\t\t/**\n\t\t * Publish sync status so other areas of the application can see what's happening here\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tfunction check_for_changes() {\n\n\t\t\tvar data                 = {};\n\t\t\tdata.changes             = self.get_unsaved_changes();\n\t\t\tdata.has_unsaved_changes = self.has_unsaved_changes( data.changes );\n\t\t\tdata.saving              = self.saving;\n\n\t\t\twindow.llms_builder.debug.log( '==== start changes check ====', data, '==== finish changes check ====' );\n\n\t\t\tBackbone.pubSub.trigger( 'current-save-status', data );\n\n\t\t};\n\n\t\t/**\n\t\t * Manually Save data via Admin AJAX when the heartbeat API has been disabled\n\t\t *\n\t\t * @since 3.16.7\n\t\t * @since 4.17.0 Fixed undefined variable error when logging an error response.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tfunction do_ajax_save() {\n\n\t\t\t// prevent simultaneous saves\n\t\t\tif ( self.saving ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar changes = self.get_unsaved_changes();\n\n\t\t\t// only send data if we have data to send\n\t\t\tif ( self.has_unsaved_changes( changes ) ) {\n\n\t\t\t\tchanges.id = Course.get( 'id' );\n\n\t\t\t\tLLMS.Ajax.call( {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\taction: 'llms_builder',\n\t\t\t\t\t\taction_type: 'ajax_save',\n\t\t\t\t\t\tcourse_id: changes.id,\n\t\t\t\t\t\tllms_builder: JSON.stringify( changes ),\n\t\t\t\t\t},\n\t\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\t\twindow.llms_builder.debug.log( '==== start do_ajax_save before ====', changes, '==== finish do_ajax_save before ====' );\n\n\t\t\t\t\t\tself.saving = true;\n\n\t\t\t\t\t\tBackbone.pubSub.trigger( 'heartbeat-send', self );\n\n\t\t\t\t\t},\n\t\t\t\t\terror: function( xhr, status, error ) {\n\n\t\t\t\t\t\twindow.llms_builder.debug.log( '==== start do_ajax_save error ====', xhr, '==== finish do_ajax_save error ====' );\n\n\t\t\t\t\t\tself.saving = false;\n\n\t\t\t\t\t\tBackbone.pubSub.trigger( 'heartbeat-tick', self, {\n\t\t\t\t\t\t\tstatus: 'error',\n\t\t\t\t\t\t\tmessage: xhr.responseText + ' (' + error + ' ' + status + ')',\n\t\t\t\t\t\t} );\n\n\t\t\t\t\t},\n\t\t\t\t\tsuccess: function( res ) {\n\n\t\t\t\t\t\tif ( ! res.llms_builder ) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\twindow.llms_builder.debug.log( '==== start do_ajax_save success ====', res, '==== finish do_ajax_save success ====' );\n\n\t\t\t\t\t\tres.llms_builder = process_removals( res.llms_builder );\n\t\t\t\t\t\tres.llms_builder = process_updates( res.llms_builder );\n\n\t\t\t\t\t\tself.saving = false;\n\n\t\t\t\t\t\tBackbone.pubSub.trigger( 'heartbeat-tick', self, res.llms_builder );\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Retrieve all the attributes changed on a model since the last sync\n\t\t *\n\t\t * For a new model (a model with a temp ID) or a model where _forceSync has been defined ALL atts will be returned\n\t\t * For an existing model (without a temp ID) only retrieves changed attributes as tracked by Backbone.TrackIt\n\t\t *\n\t\t * This function excludes any attributes defined as child attributes via the models relationship settings\n\t\t *\n\t\t * @param    obj   model  instance of a Backbone.Model\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.6\n\t\t */\n\t\tfunction get_changed_attributes( model ) {\n\n\t\t\tvar atts = {},\n\t\t\t\tsync_type;\n\n\t\t\t// don't save mid editing\n\t\t\tif ( model.get( '_has_focus' ) ) {\n\t\t\t\treturn atts;\n\t\t\t}\n\n\t\t\t// model hasn't been persisted to the database to get a real ID yet\n\t\t\t// send *all* of it's atts\n\t\t\tif ( has_temp_id( model ) || true === model.get( '_forceSync' ) ) {\n\n\t\t\t\tatts      = _.clone( model.attributes );\n\t\t\t\tsync_type = 'full';\n\n\t\t\t\t// only send the changed atts\n\t\t\t} else {\n\n\t\t\t\tatts      = model.unsavedAttributes();\n\t\t\t\tsync_type = 'partial';\n\n\t\t\t}\n\n\t\t\tvar exclude = ( model.get_relationships ) ? model.get_child_props() : [];\n\t\t\tatts        = _.omit( atts, function( val, key ) {\n\n\t\t\t\t// exclude keys that start with an underscore which are used by the\n\t\t\t\t// application but don't need to be stored in the database\n\t\t\t\tif ( 0 === key.indexOf( '_' ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t} else if ( -1 !== exclude.indexOf( key ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\n\t\t\t} );\n\n\t\t\tif ( model.before_save ) {\n\t\t\t\tatts = model.before_save( atts, sync_type );\n\t\t\t}\n\n\t\t\treturn atts;\n\n\t\t};\n\n\t\t/**\n\t\t * Get all the changes to an object (either a Model or a Collection of models)\n\t\t * Returns only changes to models and the IDs of that model (should changes exist)\n\t\t * Uses get_changed_attributes() to determine if all atts or only changed atts are needed\n\t\t * Processes children intelligently to only return changed children rather than the entire collection of children\n\t\t *\n\t\t * @param    obj        object  instance of a Backbone.Model or Backbone.Collection\n\t\t * @return   obj|array\t  \t\tif object is a model, returns an object\n\t\t *                            \tif object is a collection, returns an array of objects\n\t\t * @since    3.16.0\n\t\t * @version  3.16.11\n\t\t */\n\t\tfunction get_changes_to_object( object ) {\n\n\t\t\tvar changed_atts;\n\n\t\t\tif ( object instanceof Backbone.Model ) {\n\n\t\t\t\tchanged_atts = get_changed_attributes( object );\n\n\t\t\t\tif ( object.get_relationships ) {\n\n\t\t\t\t\t_.each( object.get_child_props(), function( prop ) {\n\n\t\t\t\t\t\tvar children = get_changes_to_object( object.get( prop ) );\n\t\t\t\t\t\tif ( ! _.isEmpty( children ) ) {\n\t\t\t\t\t\t\tchanged_atts[ prop ] = children;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t\t// if we have any data, add the id to the model\n\t\t\t\tif ( ! _.isEmpty( changed_atts ) ) {\n\t\t\t\t\tchanged_atts.id = object.get( 'id' );\n\t\t\t\t}\n\n\t\t\t} else if ( object instanceof Backbone.Collection ) {\n\n\t\t\t\tchanged_atts = [];\n\t\t\t\tobject.each( function( model ) {\n\t\t\t\t\tvar model_changes = get_changes_to_object( model );\n\t\t\t\t\tif ( ! _.isEmpty( model_changes ) ) {\n\t\t\t\t\t\tchanged_atts.push( model_changes );\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\treturn changed_atts;\n\n\t\t};\n\n\t\t/**\n\t\t * Determines if a model has a temporary ID or a real persisted ID\n\t\t *\n\t\t * @param    obj   model  instance of a model\n\t\t * @return   boolean\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tfunction has_temp_id( model ) {\n\n\t\t\treturn ( ! _.isNumber( model.id ) && 0 === model.id.indexOf( 'temp_' ) );\n\n\t\t};\n\n\t\t/**\n\t\t * Compares changes synced to the server against current model and restarts\n\t\t * tracking on elements that haven't changed since the last sync\n\t\t *\n\t\t * @param    obj   model  instance of a Backbone.Model\n\t\t * @param    obj   data   data set that was processed by the server\n\t\t * @return   void\n\t\t * @since    3.16.11\n\t\t * @version  3.19.4\n\t\t */\n\t\tfunction maybe_restart_tracking( model, data ) {\n\n\t\t\tBackbone.pubSub.trigger( model.get( 'type' ) + '-maybe-restart-tracking', model, data );\n\n\t\t\tvar omit = [ 'id', 'orig_id' ];\n\n\t\t\tif ( model.get_relationships ) {\n\t\t\t\tomit.concat( model.get_child_props() );\n\t\t\t}\n\n\t\t\t_.each( _.omit( data, omit ), function( val, prop ) {\n\n\t\t\t\tif ( _.isEqual( model.get( prop ), val ) ) {\n\t\t\t\t\tdelete model._unsavedChanges[ prop ];\n\t\t\t\t\tmodel._originalAttrs[ prop ] = val;\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t// if syncing was forced, allow tracking to move forward as normal moving forward\n\t\t\tmodel.unset( '_forceSync' );\n\n\t\t};\n\n\t\t/**\n\t\t * Processes response data from heartbeat-tick related to trashing & detaching models\n\t\t * On success, removes from local removal collection\n\t\t * On error, appends error messages to the data object returned to UI for on-screen feedback\n\t\t *\n\t\t * @param    obj   data  data.llms_builder object from heartbeat-tick response\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.17.1\n\t\t */\n\t\tfunction process_removals( data ) {\n\n\t\t\t// check removals for errors\n\t\t\tvar removals = {\n\t\t\t\tdetach: detached,\n\t\t\t\ttrash: trashed,\n\t\t\t};\n\n\t\t\t_.each( removals, function( coll, key ) {\n\n\t\t\t\tif ( data[ key ] ) {\n\n\t\t\t\t\tvar errors = [];\n\n\t\t\t\t\t_.each( data[ key ] , function( info ) {\n\n\t\t\t\t\t\t// successfully detached, remove it from the detached collection\n\t\t\t\t\t\tif ( ! info.error ) {\n\n\t\t\t\t\t\t\tcoll.remove( info.id );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\terrors.push( info.error );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} );\n\n\t\t\t\t\tif ( errors.length ) {\n\t\t\t\t\t\t_.extend( data, add_error_msg( data, errors ) );\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\treturn data;\n\t\t}\n\n\t\t/**\n\t\t * Processes response data from heartbeat-tick related to creating / updating a single object\n\t\t * Handles both collections and models as a recursive function\n\t\t *\n\t\t * @param    {[type]}   data       [description]\n\t\t * @param    {[type]}   type       [description]\n\t\t * @param    {[type]}   parent     [description]\n\t\t * @param    {[type]}   main_data  [description]\n\t\t * @return   {[type]}\n\t\t * @since    3.16.0\n\t\t * @version  3.16.11\n\t\t */\n\t\tfunction process_object_updates( data, type, parent, main_data ) {\n\n\t\t\tif ( ! data[ type ] ) {\n\t\t\t\treturn data;\n\t\t\t}\n\n\t\t\tif ( parent.get( type ) instanceof Backbone.Model ) {\n\n\t\t\t\tvar info = data[ type ];\n\n\t\t\t\tif ( info.error ) {\n\n\t\t\t\t\t_.extend( main_data, add_error_msg( main_data, info.error ) );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tvar model = parent.get( type );\n\n\t\t\t\t\t// update temp ids with the real id\n\t\t\t\t\tif ( info.id != info.orig_id ) {\n\t\t\t\t\t\tmodel.set( 'id', info.id );\n\t\t\t\t\t\tdelete model._unsavedChanges.id;\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( info.permalink ) {\n\t\t\t\t\t\tmodel.set( 'permalink', info.permalink );\n\t\t\t\t\t}\n\t\t\t\t\tif ( info.name ) {\n\t\t\t\t\t\tmodel.set( 'name', info.name );\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( info.content_added_in_builder ) {\n\t\t\t\t\t\tmodel.set( 'content_added_in_builder', info.content_added_in_builder );\n\t\t\t\t\t}\n\n\t\t\t\t\tmaybe_restart_tracking( model, info );\n\n\t\t\t\t\t// check children\n\t\t\t\t\tif ( model.get_relationships ) {\n\n\t\t\t\t\t\t_.each( model.get_child_props(), function( child_key ) {\n\t\t\t\t\t\t\t_.extend( data[ type ], process_object_updates( data[ type ], child_key, model, main_data ) );\n\t\t\t\t\t\t} );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else if ( parent.get( type ) instanceof Backbone.Collection ) {\n\n\t\t\t\t_.each( data[ type ], function( info, index ) {\n\n\t\t\t\t\tif ( info.error ) {\n\n\t\t\t\t\t\t_.extend( main_data, add_error_msg( main_data, info.error ) );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tvar model = parent.get( type ).get( info.orig_id );\n\n\t\t\t\t\t\t// update temp ids with the real id\n\t\t\t\t\t\tif ( info.id != info.orig_id ) {\n\t\t\t\t\t\t\tmodel.set( 'id', info.id );\n\t\t\t\t\t\t\tdelete model._unsavedChanges.id;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Update permalink and name if provided by the server.\n\t\t\t\t\t\tif ( info.permalink ) {\n\t\t\t\t\t\t\tmodel.set( 'permalink', info.permalink );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( info.name ) {\n\t\t\t\t\t\t\tmodel.set( 'name', info.name );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( info.content_added_in_builder ) {\n\t\t\t\t\t\t\tmodel.set( 'content_added_in_builder', info.content_added_in_builder );\n\t\t\t\t\t\t}\n\n\n\t\t\t\t\t\tmaybe_restart_tracking( model, info );\n\n\t\t\t\t\t\t// check children\n\t\t\t\t\t\tif ( model.get_relationships ) {\n\n\t\t\t\t\t\t\t_.each( model.get_child_props(), function( child_key ) {\n\t\t\t\t\t\t\t\t_.extend( data[ type ], process_object_updates( data[ type ][ index ], child_key, model, main_data ) );\n\t\t\t\t\t\t\t} );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\treturn main_data;\n\n\t\t};\n\n\t\t/**\n\t\t * Processes response data from heartbeat-tick related to updating & creating new models\n\t\t * On success, removes from local removal collection\n\t\t * On error, appends error messages to the data object returned to UI for on-screen feedback\n\t\t *\n\t\t * @param    obj   data  data.llms_builder object from heartbeat-tick response\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tfunction process_updates( data ) {\n\n\t\t\t// only mess with updates data\n\t\t\tif ( ! data.updates ) {\n\t\t\t\treturn data;\n\t\t\t}\n\n\t\t\tif ( data.updates ) {\n\t\t\t\tdata = process_object_updates( data.updates, 'sections', Course, data );\n\t\t\t}\n\n\t\t\treturn data;\n\n\t\t};\n\n\t\t/*\n\t\t\t\t\t\t\t\t /$$       /$$ /$$                                     /$$\n\t\t\t\t\t\t\t\t| $$      | $$|__/                                    |__/\n\t\t\t  /$$$$$$  /$$   /$$| $$$$$$$ | $$ /$$  /$$$$$$$        /$$$$$$   /$$$$$$  /$$\n\t\t\t /$$__  $$| $$  | $$| $$__  $$| $$| $$ /$$_____/       |____  $$ /$$__  $$| $$\n\t\t\t| $$  \\ $$| $$  | $$| $$  \\ $$| $$| $$| $$              /$$$$$$$| $$  \\ $$| $$\n\t\t\t| $$  | $$| $$  | $$| $$  | $$| $$| $$| $$             /$$__  $$| $$  | $$| $$\n\t\t\t| $$$$$$$/|  $$$$$$/| $$$$$$$/| $$| $$|  $$$$$$$      |  $$$$$$$| $$$$$$$/| $$\n\t\t\t| $$____/  \\______/ |_______/ |__/|__/ \\_______/       \\_______/| $$____/ |__/\n\t\t\t| $$                                                            | $$\n\t\t\t| $$                                                            | $$\n\t\t\t|__/                                                            |__/\n\t\t*/\n\n\t\t/**\n\t\t * Retrieve all unsaved changes for the builder instance\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.17.1\n\t\t */\n\t\tthis.get_unsaved_changes = function() {\n\n\t\t\treturn {\n\t\t\t\tdetach: detached.pluck( 'id' ),\n\t\t\t\ttrash: trashed.pluck( 'id' ),\n\t\t\t\tupdates: get_changes_to_object( Course ),\n\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Check if the builder instance has unsaved changes\n\t\t *\n\t\t * @param    obj      changes    optionally pass in an object from the return of this.get_unsaved_changes()\n\t\t *                               save some resources by not running the check twice during heartbeats\n\t\t * @return   boolean\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tthis.has_unsaved_changes = function( changes ) {\n\n\t\t\tif ( 'undefined' === typeof changes ) {\n\t\t\t\tchanges = self.get_unsaved_changes();\n\t\t\t}\n\n\t\t\t// check all possible keys, once we find one with content we have some changes to persist\n\t\t\tvar found = _.find( changes, function( data ) {\n\n\t\t\t\treturn ( false === _.isEmpty( data ) );\n\n\t\t\t} );\n\n\t\t\treturn found ? true : false;\n\n\t\t};\n\n\t\t/**\n\t\t * Save changes right now.\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.7\n\t\t */\n\t\tthis.save_now = function() {\n\t\t\tif ( autosave ) {\n\t\t\t\twp.heartbeat.connectNow();\n\t\t\t} else {\n\t\t\t\tdo_ajax_save();\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Update the interval that checks for changes to the builder instance\n\t\t *\n\t\t * @param    int        ms   time (in milliseconds) to run the check on\n\t\t *                           pass 0 to disable the check\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tthis.set_check_interval = function( ms ) {\n\t\t\tcheck_interval_ms = ms;\n\t\t\tif ( check_interval ) {\n\t\t\t\tclearInterval( check_interval );\n\t\t\t}\n\t\t\tif ( check_interval_ms ) {\n\t\t\t\tcheck_interval = setInterval( check_for_changes, check_interval_ms );\n\t\t\t}\n\t\t};\n\n\t\t/*\n\t\t\t /$$ /$$             /$$\n\t\t\t| $$|__/            | $$\n\t\t\t| $$ /$$  /$$$$$$$ /$$$$$$    /$$$$$$  /$$$$$$$   /$$$$$$   /$$$$$$   /$$$$$$$\n\t\t\t| $$| $$ /$$_____/|_  $$_/   /$$__  $$| $$__  $$ /$$__  $$ /$$__  $$ /$$_____/\n\t\t\t| $$| $$|  $$$$$$   | $$    | $$$$$$$$| $$  \\ $$| $$$$$$$$| $$  \\__/|  $$$$$$\n\t\t\t| $$| $$ \\____  $$  | $$ /$$| $$_____/| $$  | $$| $$_____/| $$       \\____  $$\n\t\t\t| $$| $$ /$$$$$$$/  |  $$$$/|  $$$$$$$| $$  | $$|  $$$$$$$| $$       /$$$$$$$/\n\t\t\t|__/|__/|_______/    \\___/   \\_______/|__/  |__/ \\_______/|__/      |_______/\n\t\t*/\n\n\t\t/**\n\t\t * Listen for detached models and send them to the server for persistence\n\t\t *\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tBackbone.pubSub.on( 'model-detached', function( model ) {\n\n\t\t\t// detached models with temp ids haven't been persisted so we don't care\n\t\t\tif ( has_temp_id( model ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tdetached.add( _.clone( model.attributes ) );\n\n\t\t} );\n\n\t\t/**\n\t\t * Listen for trashed models and send them to the server for deletion\n\t\t *\n\t\t * @since    3.16.0\n\t\t * @version  3.17.1\n\t\t */\n\t\tBackbone.pubSub.on( 'model-trashed', function( model ) {\n\n\t\t\t// if the model has a temp ID we don't have to persist the deletion\n\t\t\tif ( has_temp_id( model ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar data = _.clone( model.attributes );\n\n\t\t\tif ( model.get_trash_id ) {\n\t\t\t\tdata.id = model.get_trash_id();\n\t\t\t}\n\n\t\t\ttrashed.add( data );\n\n\t\t} );\n\n\t\t/*\n\t\t\t /$$                                       /$$     /$$                             /$$\n\t\t\t| $$                                      | $$    | $$                            | $$\n\t\t\t| $$$$$$$   /$$$$$$   /$$$$$$   /$$$$$$  /$$$$$$  | $$$$$$$   /$$$$$$   /$$$$$$  /$$$$$$\n\t\t\t| $$__  $$ /$$__  $$ |____  $$ /$$__  $$|_  $$_/  | $$__  $$ /$$__  $$ |____  $$|_  $$_/\n\t\t\t| $$  \\ $$| $$$$$$$$  /$$$$$$$| $$  \\__/  | $$    | $$  \\ $$| $$$$$$$$  /$$$$$$$  | $$\n\t\t\t| $$  | $$| $$_____/ /$$__  $$| $$        | $$ /$$| $$  | $$| $$_____/ /$$__  $$  | $$ /$$\n\t\t\t| $$  | $$|  $$$$$$$|  $$$$$$$| $$        |  $$$$/| $$$$$$$/|  $$$$$$$|  $$$$$$$  |  $$$$/\n\t\t\t|__/  |__/ \\_______/ \\_______/|__/         \\___/  |_______/  \\_______/ \\_______/   \\___/\n\t\t*/\n\n\n\t\t/**\n\t\t * Add data to the WP heartbeat to persist new models, changes, and deletions to the DB\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.16.7 Unknown\n\t\t * @since 4.14.0 Return early when autosaving is disabled.\n\t\t */\n\t\t$( document ).on( 'heartbeat-send', function( event, data ) {\n\n\t\t\t// Autosaving is disabled.\n\t\t\tif ( ! autosave ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// prevent simultaneous saves\n\t\t\tif ( self.saving ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar changes = self.get_unsaved_changes();\n\n\t\t\t// only send data if we have data to send\n\t\t\tif ( self.has_unsaved_changes( changes ) ) {\n\n\t\t\t\tchanges.id        = Course.get( 'id' );\n\t\t\t\tself.saving       = true;\n\t\t\t\tdata.llms_builder = JSON.stringify( changes );\n\n\t\t\t}\n\n\t\t\twindow.llms_builder.debug.log( '==== start heartbeat-send ====', data, '==== finish heartbeat-send ====' );\n\n\t\t\tBackbone.pubSub.trigger( 'heartbeat-send', self );\n\n\t\t} );\n\n\t\t/**\n\t\t * Confirm detachments & deletions and replace temp IDs with new persisted IDs\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 4.14.0 Return early when autosaving is disabled.\n\t\t */\n\t\t$( document ).on( 'heartbeat-tick', function( event, data ) {\n\n\t\t\t// Autosaving is disabled.\n\t\t\tif ( ! autosave ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( ! data.llms_builder ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\twindow.llms_builder.debug.log( '==== start heartbeat-tick ====', data, '==== finish heartbeat-tick ====' );\n\n\t\t\tdata.llms_builder = process_removals( data.llms_builder );\n\t\t\tdata.llms_builder = process_updates( data.llms_builder );\n\n\t\t\tself.saving = false;\n\n\t\t\tBackbone.pubSub.trigger( 'heartbeat-tick', self, data.llms_builder );\n\n\t\t} );\n\n\t\t/**\n\t\t * On heartbeat errors publish an error to the main builder application\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 4.14.0 Return early when autosaving is disabled.\n\t\t */\n\t\t$( document ).on( 'heartbeat-error', function( event, data ) {\n\n\t\t\t// Autosaving is disabled.\n\t\t\tif ( ! autosave ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\twindow.llms_builder.debug.log( '==== start heartbeat-error ====', data, '==== finish heartbeat-error ====' );\n\n\t\t\tself.saving = false;\n\n\t\t\tBackbone.pubSub.trigger( 'heartbeat-tick', self, {\n\t\t\t\tstatus: 'error',\n\t\t\t\tmessage: data.responseText + ' (' + data.status + ' ' + data.statusText + ')',\n\t\t\t} );\n\n\t\t} );\n\n\t\t/*\n\t\t\t /$$           /$$   /$$\n\t\t\t|__/          |__/  | $$\n\t\t\t /$$ /$$$$$$$  /$$ /$$$$$$\n\t\t\t| $$| $$__  $$| $$|_  $$_/\n\t\t\t| $$| $$  \\ $$| $$  | $$\n\t\t\t| $$| $$  | $$| $$  | $$ /$$\n\t\t\t| $$| $$  | $$| $$  |  $$$$/\n\t\t\t|__/|__/  |__/|__/   \\___/\n\t\t*/\n\t\tinit();\n\n\t\treturn this;\n\n\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/Abstract.js",
    "content": "/**\n * Abstract LifterLMS Model\n *\n * @since    3.17.0\n * @version  3.17.0\n */\ndefine( [ 'Models/_Relationships', 'Models/_Utilities' ], function( Relationships, Utilities ) {\n\n\treturn Backbone.Model.extend( _.defaults( {}, Relationships, Utilities ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/Course.js",
    "content": "/**\n * Course Model.\n *\n * @since 3.16.0\n * @since 3.24.0 Added `get_total_points()` method.\n * @since 3.37.11 Use lesson author ID instead of author object when adding existing lessons to a course.\n * @version 5.4.0\n */\ndefine( [ 'Collections/Sections', 'Models/_Relationships', 'Models/_Utilities' ], function( Sections, Relationships, Utilities ) {\n\n\treturn Backbone.Model.extend( _.defaults( {\n\n\t\trelationships: {\n\t\t\tchildren: {\n\t\t\t\tsections: {\n\t\t\t\t\tclass: 'Sections',\n\t\t\t\t\tmodel: 'section',\n\t\t\t\t\ttype: 'collection',\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * New Course Defaults.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Object}\n\t\t */\n\t\tdefaults: function() {\n\t\t\treturn {\n\t\t\t\tedit_url: '',\n\t\t\t\tsections: [],\n\t\t\t\ttitle: 'New Course',\n\t\t\t\ttype: 'course',\n\t\t\t\tview_url: '',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Init.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.startTracking();\n\t\t\tthis.init_relationships();\n\n\t\t\t// Sidebar \"New Section\" button broadcast.\n\t\t\tBackbone.pubSub.on( 'add-new-section', this.add_section, this );\n\n\t\t\t// Sidebar \"New Lesson\" button broadcast.\n\t\t\tBackbone.pubSub.on( 'add-new-lesson', this.add_lesson, this );\n\n\t\t\tBackbone.pubSub.on( 'lesson-search-select', this.add_existing_lesson, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Add an existing lesson to the course.\n\t\t *\n\t\t * Duplicate a lesson from this or another course or attach an orphaned lesson.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.24.0 Unknown.\n\t\t * @since 3.37.11 Use the author id instead of the author object.\n\t\t * @since 5.4.0 Added filter hook 'llms_adding_existing_lesson_data'.\n\t\t *               On cloning, duplicate assignments too, if assignment add-on active and assignment attached.\n\t\t *\n\t\t * @param {Object} lesson Lesson data obj.\n\t\t * @return {Void}\n\t\t */\n\t\tadd_existing_lesson: function( lesson ) {\n\n\t\t\tvar data = lesson.data;\n\n\t\t\tif ( 'clone' === lesson.action ) {\n\n\t\t\t\tdelete data.id;\n\n\t\t\t\t// If a quiz is attached, duplicate the quiz also.\n\t\t\t\tif ( data.quiz ) {\n\t\t\t\t\tdata.quiz                   = _.prepareQuizObjectForCloning( data.quiz );\n\t\t\t\t\tdata.quiz._questions_loaded = true;\n\t\t\t\t}\n\n\t\t\t\t// If assignment add-on active and assignment attached, duplicate the assignment too.\n\t\t\t\tif ( window.llms_builder.assignments && data.assignment ) {\n\t\t\t\t\tdata.assignment = _.prepareAssignmentObjectForCloning( data.assignment );\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tdata._forceSync = true;\n\n\t\t\t}\n\n\t\t\tdelete data.order;\n\t\t\tdelete data.parent_course;\n\t\t\tdelete data.parent_section;\n\n\t\t\t// Use author id instead of the lesson author object.\n\t\t\tdata = _.prepareExistingPostObjectDataForAddingOrCloning( data );\n\n\t\t\t/**\n\t\t\t * Filters the data of the existing lesson being added.\n\t\t\t *\n\t\t\t * @since 5.4.0\n\t\t\t *\n\t\t\t * @param {Object} data   Lesson data.\n\t\t\t * @param {String} action Action being performed. [clone|attach].\n\t\t\t * @param {Object} course The lesson's course parent model.\n\t\t\t */\n\t\t\tdata = window.llms.hooks.applyFilters( 'llms_adding_existing_lesson_data', data, lesson.action, this );\n\n\t\t\tthis.add_lesson( data );\n\n\t\t},\n\n\t\t/**\n\t\t * Add a new lesson to the course.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param {Object} data Lesson data.\n\t\t * @return {Object} Backbone.Model of the lesson.\n\t\t */\n\t\tadd_lesson: function( data ) {\n\n\t\t\tdata        = data || {};\n\t\t\tvar options = {},\n\t\t\t\tsection;\n\n\t\t\tif ( ! data.parent_section ) {\n\t\t\t\tsection = this.get_selected_section();\n\t\t\t\tif ( ! section ) {\n\t\t\t\t\tsection = this.get( 'sections' ).last();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsection = this.get( 'sections' ).get( data.parent_section );\n\t\t\t}\n\n\t\t\tdata._selected = true;\n\n\t\t\tdata.parent_course = this.get( 'id' );\n\n\t\t\tvar lesson = section.add_lesson( data, options );\n\t\t\tBackbone.pubSub.trigger( 'new-lesson-added', lesson );\n\n\t\t\t// Expand the section.\n\t\t\tsection.set( '_expanded', true );\n\n\t\t\treturn lesson;\n\n\t\t},\n\n\t\t/**\n\t\t * Add a new section to the course.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param {Object} data Section data.\n\t\t * @return {Void}\n\t\t */\n\t\tadd_section: function( data ) {\n\n\t\t\tdata         = data || {};\n\t\t\tvar sections = this.get( 'sections' ),\n\t\t\t\toptions  = {},\n\t\t\t\tselected = this.get_selected_section();\n\n\t\t\t// If a section is selected, add the new section after the currently selected one.\n\t\t\tif ( selected ) {\n\t\t\t\toptions.at = sections.indexOf( selected ) + 1;\n\t\t\t}\n\n\t\t\tsections.add( data, options );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the currently selected section in the course.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Object|undefined}\n\t\t */\n\t\tget_selected_section: function() {\n\n\t\t\treturn this.get( 'sections' ).find( function( model ) {\n\t\t\t\treturn model.get( '_selected' );\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the total number of points in the course.\n\t\t *\n\t\t * @since 3.24.0\n\t\t *\n\t\t * @return {Integer}\n\t\t */\n\t\tget_total_points: function() {\n\n\t\t\tvar points = 0;\n\n\t\t\tthis.get( 'sections' ).each( function( section ) {\n\t\t\t\tsection.get( 'lessons' ).each( function( lesson ) {\n\t\t\t\t\tvar lesson_points = lesson.get( 'points' );\n\t\t\t\t\tif ( ! _.isNumber( lesson_points ) ) {\n\t\t\t\t\t\tlesson_points = 0;\n\t\t\t\t\t}\n\t\t\t\t\tpoints += lesson_points * 1;\n\t\t\t\t} );\n\t\t\t} );\n\n\t\t\treturn points;\n\n\t\t},\n\n\t}, Relationships, Utilities ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/Image.js",
    "content": "/**\n * Image object model for use in various models for the 'image' attribute\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn Backbone.Model.extend( {\n\n\t\tdefaults: {\n\t\t\tenabled: 'no',\n\t\t\tid: '',\n\t\t\tsize: 'full',\n\t\t\tsrc: '',\n\t\t},\n\n\t\tinitialize: function() {\n\t\t\tthis.startTracking();\n\t\t},\n\n\t} );\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/Lesson.js",
    "content": "/**\n * Lesson Model\n *\n * @since 3.13.0\n * @version 4.20.0\n */\ndefine( [ 'Models/Quiz', 'Models/_Relationships', 'Models/_Utilities', 'Schemas/Lesson' ], function( Quiz, Relationships, Utilities, LessonSchema ) {\n\n\treturn Backbone.Model.extend( _.defaults( {\n\n\t\t/**\n\t\t * Model relationships\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\trelationships: {\n\t\t\tparents: {\n\t\t\t\tmodel: 'section',\n\t\t\t\ttype: 'model',\n\t\t\t},\n\t\t\tchildren: {\n\t\t\t\tquiz: {\n\t\t\t\t\tclass: 'Quiz',\n\t\t\t\t\tconditional: function( model ) {\n\t\t\t\t\t\t// if quiz is enabled OR not enabled but we have some quiz data as an obj\n\t\t\t\t\t\treturn ( 'yes' === model.get( 'quiz_enabled' ) || ! _.isEmpty( model.get( 'quiz' ) ) );\n\t\t\t\t\t},\n\t\t\t\t\tmodel: 'llms_quiz',\n\t\t\t\t\ttype: 'model',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Lesson Settings Schema\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tschema: LessonSchema,\n\n\t\t/**\n\t\t * New lesson defaults\n\t\t *\n\t\t * @since 3.13.0\n\t\t * @since 3.24.0 Unknown.\n\t\t *\n\t\t * @return {Object} Default options associative array (js object).\n\t\t */\n\t\tdefaults: function() {\n\t\t\treturn {\n\t\t\t\tid: _.uniqueId( 'temp_' ),\n\t\t\t\ttitle: LLMS.l10n.translate( 'New Lesson' ),\n\t\t\t\ttype: 'lesson',\n\t\t\t\torder: this.collection ? this.collection.length + 1 : 1,\n\t\t\t\tparent_course: window.llms_builder.course.id,\n\t\t\t\tparent_section: '',\n\n\t\t\t\t// Urls.\n\t\t\t\tedit_url: '',\n\t\t\t\tview_url: '',\n\n\t\t\t\t// Editable fields.\n\t\t\t\tcontent: '',\n\t\t\t\taudio_embed: '',\n\t\t\t\thas_prerequisite: 'no',\n\t\t\t\trequire_passing_grade: 'yes',\n\t\t\t\trequire_assignment_passing_grade: 'yes',\n\t\t\t\tvideo_embed: '',\n\t\t\t\tfree_lesson: '',\n\t\t\t\tpoints: 1,\n\n\t\t\t\t// Other fields.\n\t\t\t\tassignment: {}, // Assignment model/data.\n\t\t\t\tassignment_enabled: 'no',\n\n\t\t\t\tquiz: {}, // Quiz model/data.\n\t\t\t\tquiz_enabled: 'no',\n\n\t\t\t\tcontent_added_in_builder: '',\n\n\t\t\t\t_forceSync: false,\n\n\t\t\t};\n\t\t},\n\n\t\t/**\n\t\t * Initializer\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.17.0 Unknown\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.init_custom_schema();\n\t\t\tthis.startTracking();\n\t\t\tthis.maybe_init_assignments();\n\t\t\tthis.init_relationships();\n\n\t\t\t// If the lesson ID isn't set on a quiz, set it.\n\t\t\tvar quiz = this.get( 'quiz' );\n\t\t\tif ( ! _.isEmpty( quiz ) && ! quiz.get( 'lesson_id' ) ) {\n\t\t\t\tquiz.set( 'lesson_id', this.get( 'id' ) );\n\t\t\t}\n\n\t\t\twindow.llms.hooks.doAction( 'llms_lesson_model_init', this );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve a reference to the parent course of the lesson\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 4.14.0 Use Section.get_course() in favor of Section.get_parent().\n\t\t *\n\t\t * @return {Object} The parent course model of the lesson.\n\t\t */\n\t\tget_course: function() {\n\t\t\treturn this.get_parent().get_course();\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the translated post type name for the model's type\n\t\t *\n\t\t * @since  3.16.12\n\t\t *\n\t\t * @param bool plural If true, returns the plural, otherwise returns singular.\n\t\t * @return string The translated post type name.\n\t\t */\n\t\tget_l10n_type: function( plural ) {\n\n\t\t\tif ( plural ) {\n\t\t\t\treturn LLMS.l10n.translate( 'lessons' );\n\t\t\t}\n\n\t\t\treturn LLMS.l10n.translate( 'lesson' );\n\t\t},\n\n\t\t/**\n\t\t * Override default get_parent to grab from collection if models parent isn't set\n\t\t *\n\t\t * @since 3.17.0\n\t\t *\n\t\t * @return {Object}|false The parent model or false if not available.\n\t\t */\n\t\tget_parent: function() {\n\n\t\t\tvar rels = this.get_relationships();\n\t\t\tif ( rels.parent && rels.parent.reference ) {\n\t\t\t\treturn rels.parent.reference;\n\t\t\t} else if ( this.collection && this.collection.parent ) {\n\t\t\t\treturn this.collection.parent;\n\t\t\t}\n\t\t\treturn false;\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the questions percentage value within the quiz\n\t\t *\n\t\t * @since 3.24.0\n\t\t *\n\t\t * @return {String} Questions percentage value within the quiz.\n\t\t */\n\t\tget_points_percentage: function() {\n\n\t\t\tvar total  = this.get_course().get_total_points(),\n\t\t\t\tpoints = this.get( 'points' ) * 1;\n\n\t\t\tif ( ! _.isNumber( points ) ) {\n\t\t\t\tpoints = 0;\n\t\t\t}\n\n\t\t\tif ( 0 === total ) {\n\t\t\t\treturn '0%';\n\t\t\t}\n\n\t\t\treturn ( ( points / total ) * 100 ).toFixed( 2 ) + '%';\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve an array of prerequisite options available for the current lesson\n\t\t *\n\t\t * @since 3.17.0\n\t\t *\n\t\t * @return {Object} Prerequisite options.\n\t\t */\n\t\tget_available_prereq_options: function() {\n\n\t\t\tvar parent_section_index    = this.get_parent().collection.indexOf( this.get_parent() ),\n\t\t\t\tlesson_index_in_section = this.collection.indexOf( this ),\n\t\t\t\toptions                 = [];\n\n\t\t\tthis.get_course().get( 'sections' ).each( function( section, curr_sec_index ) {\n\t\t\t\tif ( curr_sec_index <= parent_section_index ) {\n\t\t\t\t\tvar group = {\n\t\t\t\t\t\t\t// Translators: %1$d = section order number, %2$s = section title.\n\t\t\t\t\t\tlabel: LLMS.l10n.replace( 'Section %1$d: %2$s', {\n\t\t\t\t\t\t\t'%1$d': section.get( 'order' ),\n\t\t\t\t\t\t\t'%2$s': section.get( 'title' )\n\t\t\t\t\t\t} ),\n\t\t\t\t\toptions: [],\n\t\t\t\t\t};\n\n\t\t\t\t\tsection.get( 'lessons' ).each( function( lesson, curr_les_index ) {\n\t\t\t\t\t\tif ( curr_sec_index !== parent_section_index || curr_les_index < lesson_index_in_section ) {\n\t\t\t\t\t\t\t// Translators: %1$d = lesson order number, %2$s = lesson title.\n\t\t\t\t\t\t\tgroup.options.push( {\n\t\t\t\t\t\t\t\tkey: lesson.get( 'id' ),\n\t\t\t\t\t\t\t\tval: LLMS.l10n.replace( 'Lesson %1$d: %2$s', {\n\t\t\t\t\t\t\t\t\t'%1$d': lesson.get( 'order' ),\n\t\t\t\t\t\t\t\t\t'%2$s': lesson.get( 'title' )\n\t\t\t\t\t\t\t\t} ),\n\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t}\n\t\t\t\t\t}, this );\n\n\t\t\t\t\toptions.push( group );\n\t\t\t\t}\n\t\t\t}, this );\n\n\t\t\treturn options;\n\n\t\t},\n\n\t\t/**\n\t\t * Add a new quiz to the lesson\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.27.0 Unknown.\n\t\t *\n\t\t * @param {Object} data Object of quiz data used to construct a new quiz model.\n\t\t * @return {Object} Model for the created quiz.\n\t\t */\n\t\tadd_quiz: function( data ) {\n\n\t\t\tdata = data || {};\n\n\t\t\tdata.lesson_id         = this.id;\n\t\t\tdata._questions_loaded = true;\n\n\t\t\tif ( ! data.title ) {\n\n\t\t\t\tdata.title = LLMS.l10n.replace( '%1$s Quiz', {\n\t\t\t\t\t'%1$s': this.get( 'title' ),\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\tthis.set( 'quiz', data );\n\t\t\tthis.init_relationships();\n\n\t\t\tvar quiz = this.get( 'quiz' );\n\t\t\tthis.set( 'quiz_enabled', 'yes' );\n\n\t\t\twindow.llms.hooks.doAction( 'llms_lesson_add_quiz', quiz, this );\n\n\t\t\treturn quiz;\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if this is the first lesson\n\t\t *\n\t\t * @since 3.17.0\n\t\t * @since 4.20.0 Use is_first_in_section() new method.\n\t\t *\n\t\t * @return {Boolean} Whether this is the first lesson of its course.\n\t\t */\n\t\tis_first_in_course: function() {\n\n\t\t\t// If it's not the first item in the section it cant be the first lesson.\n\t\t\tif ( ! this.is_first_in_section() ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// If it's not the first section it cant' be first lesson.\n\t\t\tvar section = this.get_parent();\n\t\t\tif ( section.collection.indexOf( section ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// It's first lesson in first section.\n\t\t\treturn true;\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if this is the last lesson of the course\n\t\t *\n\t\t * @since 4.20.0\n\t\t *\n\t\t * @return {Boolean} Whether this is the last lesson of its course.\n\t\t */\n\t\t is_last_in_course: function() {\n\n\t\t\t// If it's not last item in the section it cant be the last lesson.\n\t\t\tif ( ! this.is_last_in_section() ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// If it's not the last section it cant' be last lesson.\n\t\t\tvar section = this.get_parent();\n\t\t\tif ( section.collection.indexOf( section ) < ( section.collection.size() - 1 ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// It's last lesson in last section.\n\t\t\treturn true;\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if this is the first lesson within its section\n\t\t *\n\t\t * @since 4.20.0\n\t\t *\n\t\t * @return {Boolean} Whether this is the first lesson of its section.\n\t\t */\n\t\tis_first_in_section: function() {\n\t\t\treturn 0 === this.collection.indexOf( this );\n\t\t},\n\n\t\t/**\n\t\t * Determine if this is the last lesson within its section\n\t\t *\n\t\t * @since 4.20.0\n\t\t *\n\t\t * @return {Boolean} Whether this is the last lesson of its section.\n\t\t */\n\t\tis_last_in_section: function() {\n\t\t\treturn this.collection.indexOf( this ) === ( this.collection.size() - 1 );\n\t\t},\n\n\t\t/**\n\t\t * Get prev lesson in a course\n\t\t *\n\t\t * @since 4.20.0\n\t\t *\n\t\t * @param {String} status Prev lesson post status. If not specified any status will be taken into account.\n\t\t * @return {Object}|false Previous lesson model or `false` if no previous lesson could be found.\n\t\t */\n\t\tget_prev: function( status ) {\n\t\t\treturn this.get_sibling( 'prev', status );\n\t\t},\n\n\t\t/**\n\t\t * Get next lesson in a course\n\t\t *\n\t\t * @since 4.20.0\n\t\t *\n\t\t * @param {String} status Next lesson post status. If not specified any status will be taken into account.\n\t\t * @return {Object}|false Next lesson model or `false` if no next lesson could be found.\n\t\t */\n\t\tget_next: function( status ) {\n\t\t\treturn this.get_sibling( 'next', status );\n\t\t},\n\n\t\t/**\n\t\t * Get a sibling lesson\n\t\t *\n\t\t * @param {String} direction Siblings direction [next|prev]. If not specified will fall back on 'prev'.\n\t\t * @param {String} status    Sibling lesson post status. If not specified any status will be taken into account.\n\t\t * @return {Object}|false Sibling lesson model, in the specified direction, or `false` if no sibling lesson could be found.\n\t\t */\n\t\tget_sibling: function( direction, status ) {\n\n\t\t\tdirection = 'next' === direction ? direction : 'prev';\n\n\t\t\t// Functions and vars to use when direction is 'prev' (default).\n\t\t\tvar is_course_limit_reached_f               = 'is_first_in_course',\n\t\t\t\tis_section_limit_reached_f              = 'is_first_in_section',\n\t\t\t\tsibling_index_increment                 = -1,\n\t\t\t\tget_sibling_lesson_in_sibling_section_f = 'last';\n\n\t\t\tif ( 'next' === direction ) {\n\t\t\t\tis_course_limit_reached_f               = 'is_last_in_course';\n\t\t\t\tis_section_limit_reached_f              = 'is_last_in_section';\n\t\t\t\tsibling_index_increment                 = 1,\n\t\t\t\tget_sibling_lesson_in_sibling_section_f = 'first';\n\t\t\t}\n\n\t\t\tif ( this[ is_course_limit_reached_f ]() ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tvar sibling_index  = this.collection.indexOf( this ) + sibling_index_increment,\n\t\t\t\tsibling_lesson = this.collection.at( sibling_index );\n\n\t\t\tif ( this[ 'next' === direction ? 'is_last_in_section' : 'is_first_in_section' ]() ) {\n\t\t\t\tvar cur_section     = this.get_parent(),\n\t\t\t\t\tsibling_section = cur_section[ 'get_' + direction ]( false );\n\n\t\t\t\t// Skip sibling empty sections.\n\t\t\t\twhile ( sibling_section && ! sibling_section.get( 'lessons' ).size() ) {\n\t\t\t\t\tsibling_section = sibling_section[ 'get_' + direction ]( false );\n\t\t\t\t}\n\n\t\t\t\t// Couldn't find any suitable lesson.\n\t\t\t\tif ( ! sibling_section || ! sibling_section.get( 'lessons' ).size() ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tsibling_lesson = sibling_section.get( 'lessons' )[ get_sibling_lesson_in_sibling_section_f ]();\n\n\t\t\t}\n\n\t\t\t// If we need a specific lesson status.\n\t\t\tif ( status && status !== sibling_lesson.get( 'status' ) ) {\n\t\t\t\treturn sibling_lesson.get_sibling( direction, status );\n\t\t\t}\n\n\t\t\treturn sibling_lesson;\n\n\t\t},\n\n\t\t/**\n\t\t * Initialize lesson assignments *if* the assignments addon is available and enabled\n\t\t *\n\t\t * @since 3.17.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tmaybe_init_assignments: function() {\n\n\t\t\tif ( ! window.llms_builder.assignments ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.relationships.children.assignment = {\n\t\t\t\tclass: 'Assignment',\n\t\t\t\tconditional: function( model ) {\n\t\t\t\t\t// If assignment is enabled OR not enabled but we have some assignment data as an obj.\n\t\t\t\t\treturn ( 'yes' === model.get( 'assignment_enabled' ) || ! _.isEmpty( model.get( 'assignment' ) ) );\n\t\t\t\t},\n\t\t\t\tmodel: 'llms_assignment',\n\t\t\t\ttype: 'model',\n\t\t\t};\n\n\t\t},\n\n\t}, Relationships, Utilities ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/Question.js",
    "content": "/**\n * Quiz Question\n *\n * @since    3.16.0\n * @version  3.27.0\n */\ndefine( [\n\t\t'Models/Image',\n\t\t'Collections/Questions',\n\t\t'Collections/QuestionChoices',\n\t\t'Models/QuestionType',\n\t\t'Models/_Relationships',\n\t\t'Models/_Utilities'\n\t], function(\n\t\tImage,\n\t\tQuestions,\n\t\tQuestionChoices,\n\t\tQuestionType,\n\t\tRelationships,\n\t\tUtilities\n\t) {\n\n\t\treturn Backbone.Model.extend( _.defaults( {\n\n\t\t\t/**\n\t\t\t * Model relationships\n\t\t\t *\n\t\t\t * @type  {Object}\n\t\t\t */\n\t\t\trelationships: {\n\t\t\t\tparent: {\n\t\t\t\t\tmodel: 'llms_quiz',\n\t\t\t\t\ttype: 'model',\n\t\t\t\t},\n\t\t\t\tchildren: {\n\t\t\t\t\tchoices: {\n\t\t\t\t\t\tclass: 'QuestionChoices',\n\t\t\t\t\t\tmodel: 'choice',\n\t\t\t\t\t\ttype: 'collection',\n\t\t\t\t\t},\n\t\t\t\t\timage: {\n\t\t\t\t\t\tclass: 'Image',\n\t\t\t\t\t\tmodel: 'image',\n\t\t\t\t\t\ttype: 'model',\n\t\t\t\t\t},\n\t\t\t\t\tquestions: {\n\t\t\t\t\t\tclass: 'Questions',\n\t\t\t\t\t\tconditional: function( model ) {\n\t\t\t\t\t\t\tvar type = model.get( 'question_type' ),\n\t\t\t\t\t\t\ttype_id  = _.isString( type ) ? type : type.get( 'id' );\n\t\t\t\t\t\t\treturn ( 'group' === type_id );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmodel: 'llms_question',\n\t\t\t\t\t\ttype: 'collection',\n\t\t\t\t\t},\n\t\t\t\t\tquestion_type: {\n\t\t\t\t\t\tclass: 'QuestionType',\n\t\t\t\t\t\tlookup: function( val ) {\n\t\t\t\t\t\t\tif ( _.isString( val ) ) {\n\t\t\t\t\t\t\t\treturn window.llms_builder.questions.get( val );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn val;\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmodel: 'question_type',\n\t\t\t\t\t\ttype: 'model',\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Model defaults\n\t\t\t *\n\t\t\t * @return   obj\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tdefaults: function() {\n\t\t\t\treturn {\n\t\t\t\t\tid: _.uniqueId( 'temp_' ),\n\t\t\t\t\tchoices: [],\n\t\t\t\t\tcontent: '',\n\t\t\t\t\tdescription_enabled: 'no',\n\t\t\t\t\timage: {},\n\t\t\t\t\tmulti_choices: 'no',\n\t\t\t\t\tmenu_order: 1,\n\t\t\t\t\tpoints: 1,\n\t\t\t\t\tquestion_type: 'generic',\n\t\t\t\t\tquestions: [], // for question groups\n\t\t\t\t\tparent_id: '',\n\t\t\t\t\ttitle: '',\n\t\t\t\t\ttype: 'llms_question',\n\t\t\t\t\tvideo_enabled: 'no',\n\t\t\t\t\tvideo_src: '',\n\n\t\t\t\t\t_expanded: false,\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Initializer\n\t\t\t *\n\t\t\t * @param    obj   data     object of data for the model\n\t\t\t * @param    obj   options  additional options\n\t\t\t * @return   void\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tinitialize: function( data, options ) {\n\n\t\t\t\tvar self = this;\n\n\t\t\t\tthis.startTracking();\n\t\t\t\tthis.init_relationships( options );\n\n\t\t\t\tif ( false !== this.get( 'question_type' ).choices ) {\n\n\t\t\t\t\tthis._ensure_min_choices();\n\n\t\t\t\t\t// when a choice is removed, maybe add back some defaults so we always have the minimum\n\t\t\t\t\tthis.listenTo( this.get( 'choices' ), 'remove', function() {\n\t\t\t\t\t\t// new items are added at index 0 when there's only 1 item in the collection, not sure why exactly...\n\t\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t\tself._ensure_min_choices();\n\t\t\t\t\t\t}, 0 );\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t\t// ensure question types that don't support points don't record default 1 point in database\n\t\t\t\tif ( ! this.get( 'question_type' ).get( 'points' ) ) {\n\t\t\t\t\tthis.set( 'points', 0 );\n\t\t\t\t}\n\n\t\t\t\t_.delay( function( self ) {\n\t\t\t\t\tself.on( 'change:points', self.get_parent().update_points, self.get_parent() );\n\t\t\t\t}, 1, this );\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Add a new question choice\n\t\t\t *\n\t\t\t * @param    obj   data     object of choice data\n\t\t\t * @param    obj   options  additional options\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tadd_choice: function( data, options ) {\n\n\t\t\t\tvar max = this.get( 'question_type' ).get_max_choices();\n\t\t\t\tif ( this.get( 'choices' ).size() >= max ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tdata    = data || {};\n\t\t\t\toptions = options || {};\n\n\t\t\t\tdata.choice_type = this.get( 'question_type' ).get_choice_type();\n\t\t\t\tdata.question_id = this.get( 'id' );\n\t\t\t\toptions.parent   = this;\n\n\t\t\t\tvar choice = this.get( 'choices' ).add( data, options );\n\n\t\t\t\tBackbone.pubSub.trigger( 'question-add-choice', choice, this );\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Collapse question_type attribute during full syncs to save to database\n\t\t\t * Not needed because question types cannot be adjusted after question creation\n\t\t\t * Called from sync controller\n\t\t\t *\n\t\t\t * @param    obj      atts       flat object of attributes to be saved to db\n\t\t\t * @param    string   sync_type  full or partial\n\t\t\t *                                 full indicates a force resync or that the model isn't persisted yet\n\t\t\t * @return   obj\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tbefore_save: function( atts, sync_type  ) {\n\t\t\t\tif ( 'full' === sync_type ) {\n\t\t\t\t\tatts.question_type = this.get( 'question_type' ).get( 'id' );\n\t\t\t\t}\n\t\t\t\treturn atts;\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Retrieve the model's parent (if set)\n\t\t\t *\n\t\t\t * @return   obj|false\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tget_parent: function() {\n\n\t\t\t\tvar rels = this.get_relationships();\n\n\t\t\t\tif ( rels.parent ) {\n\t\t\t\t\tif ( this.collection && this.collection.parent ) {\n\t\t\t\t\t\treturn this.collection.parent;\n\t\t\t\t\t} else if ( rels.parent.reference ) {\n\t\t\t\t\t\treturn rels.parent.reference;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Retrieve the translated post type name for the model's type\n\t\t\t *\n\t\t\t * @param    bool     plural  if true, returns the plural, otherwise returns singular\n\t\t\t * @return   string\n\t\t\t * @since    3.27.0\n\t\t\t * @version  3.27.0\n\t\t\t */\n\t\t\tget_l10n_type: function( plural ) {\n\n\t\t\t\tif ( plural ) {\n\t\t\t\t\treturn LLMS.l10n.translate( 'questions' );\n\t\t\t\t}\n\n\t\t\t\treturn LLMS.l10n.translate( 'question' );\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Gets the index of the question within it's parent\n\t\t\t * Question numbers skip content elements\n\t\t\t * & content elements skip questions\n\t\t\t *\n\t\t\t * @return   int\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tget_type_index: function() {\n\n\t\t\t\t// current models type, used to check the predicate in the filter function below\n\t\t\t\tvar curr_type = this.get( 'question_type' ).get( 'id' ),\n\t\t\t\tquestions;\n\n\t\t\t\tquestions = this.collection.filter( function( question ) {\n\n\t\t\t\t\tvar type = question.get( 'question_type' ).get( 'id' );\n\n\t\t\t\t\t// if current model is not content, return all non-content questions\n\t\t\t\t\tif ( curr_type !== 'content' ) {\n\t\t\t\t\t\treturn ( 'content' !== type );\n\t\t\t\t\t}\n\n\t\t\t\t\t// current model is content, return only content questions\n\t\t\t\t\treturn 'content' === type;\n\n\t\t\t\t} );\n\n\t\t\t\treturn questions.indexOf( this );\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Gets iterator for the given type\n\t\t\t * Questions use numbers and content uses alphabet\n\t\t\t *\n\t\t\t * @return   mixed\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tget_type_iterator: function() {\n\n\t\t\t\tvar index = this.get_type_index();\n\n\t\t\t\tif ( -1 === index ) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\n\t\t\t\tif ( 'content' === this.get( 'question_type' ).get( 'id' ) ) {\n\t\t\t\t\tvar alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split( '' );\n\t\t\t\t\treturn alphabet[ index ];\n\t\t\t\t}\n\n\t\t\t\treturn index + 1;\n\n\t\t\t},\n\n\t\t\tget_qid: function() {\n\n\t\t\t\tvar parent = this.get_parent_question(),\n\t\t\t\tprefix     = '';\n\n\t\t\t\tif ( parent ) {\n\n\t\t\t\t\tprefix = parent.get_qid() + '.';\n\n\t\t\t\t}\n\n\t\t\t\t// return short_id + this.get_type_iterator();\n\t\t\t\treturn prefix + this.get_type_iterator();\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Retrieve the parent question (if the question is in a question group)\n\t\t\t *\n\t\t\t * @return   obj|false\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tget_parent_question: function() {\n\n\t\t\t\tif ( this.is_in_group() ) {\n\n\t\t\t\t\treturn this.collection.parent;\n\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Retrieve the parent quiz\n\t\t\t *\n\t\t\t * @return   obj\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tget_parent_quiz: function() {\n\t\t\t\treturn this.get_parent();\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Points getter\n\t\t\t * ensures that 0 is always returned if the question type doesn't support points\n\t\t\t *\n\t\t\t * @return   int\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tget_points: function() {\n\n\t\t\t\tif ( ! this.get( 'question_type' ).get( 'points' ) ) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\n\t\t\t\treturn this.get( 'points' );\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Retrieve the questions percentage value within the quiz\n\t\t\t *\n\t\t\t * @return   string\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tget_points_percentage: function() {\n\n\t\t\t\tvar total = this.get_parent().get( '_points' ),\n\t\t\t\tpoints    = this.get( 'points' );\n\n\t\t\t\tif ( 0 === total ) {\n\t\t\t\t\treturn '0%';\n\t\t\t\t}\n\n\t\t\t\treturn ( ( points / total ) * 100 ).toFixed( 2 ) + '%';\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Determine if the question belongs to a question group\n\t\t\t *\n\t\t\t * @return   {Boolean}\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tis_in_group: function() {\n\n\t\t\t\treturn ( 'question' === this.collection.parent.get( 'type' ) );\n\n\t\t\t},\n\n\t\t\t_ensure_min_choices: function() {\n\n\t\t\t\tvar choices = this.get( 'choices' );\n\t\t\t\twhile ( choices.size() < this.get( 'question_type' ).get_min_choices() ) {\n\t\t\t\t\tthis.add_choice();\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t}, Relationships, Utilities ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/QuestionChoice.js",
    "content": "/**\n * Quiz Question Choice\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Models/Image', 'Models/_Relationships' ], function( Image, Relationships ) {\n\n\treturn Backbone.Model.extend( _.defaults( {\n\n\t\t/**\n\t\t * Model relationships\n\t\t *\n\t\t * @type  {Object}\n\t\t */\n\t\trelationships: {\n\t\t\tparent: {\n\t\t\t\tmodel: 'llms_question',\n\t\t\t\ttype: 'model',\n\t\t\t},\n\t\t\tchildren: {\n\t\t\t\tchoice: {\n\t\t\t\t\tconditional: function( model ) {\n\t\t\t\t\t\treturn ( 'image' === model.get( 'choice_type' ) );\n\t\t\t\t\t},\n\t\t\t\t\tclass: 'Image',\n\t\t\t\t\tmodel: 'image',\n\t\t\t\t\ttype: 'model',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Model defaults\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tdefaults: function() {\n\t\t\treturn {\n\t\t\t\tid: _.uniqueId( 'temp_' ),\n\t\t\t\tchoice: '',\n\t\t\t\tchoice_type: 'text',\n\t\t\t\tcorrect: false,\n\t\t\t\tmarker: 'A',\n\t\t\t\tquestion_id: '',\n\t\t\t\ttype: 'choice',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Initializer\n\t\t *\n\t\t * @param    obj   data     object of model attributes\n\t\t * @param    obj   options  additional options\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function( data, options ) {\n\n\t\t\tthis.startTracking();\n\t\t\tthis.init_relationships( options );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the choice's parent question\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_parent: function() {\n\t\t\treturn this.collection.parent;\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the ID used when trashing the model\n\t\t *\n\t\t * @return   string\n\t\t * @since    3.17.1\n\t\t * @version  3.17.1\n\t\t */\n\t\tget_trash_id: function() {\n\t\t\treturn this.get( 'question_id' ) + ':' + this.get( 'id' );\n\t\t},\n\n\t\t/**\n\t\t * Determine if \"selection\" is enabled for the question type\n\t\t * Choice type questions are selectable by reorder type questions are not but still use choices\n\t\t *\n\t\t * @return   {Boolean}\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tis_selectable: function() {\n\t\t\treturn this.get_parent().get( 'question_type' ).get_choice_selectable();\n\t\t},\n\n\t}, Relationships ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/QuestionType.js",
    "content": "/**\n * Quiz Question Type\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn Backbone.Model.extend( {\n\n\t\t/**\n\t\t * Get model default attributes\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tdefaults: function() {\n\t\t\treturn {\n\t\t\t\tchoices: false,\n\t\t\t\tclarifications: true,\n\t\t\t\tdefault_choices: [],\n\t\t\t\tdescription: true,\n\t\t\t\ticon: 'question',\n\t\t\t\tid: 'generic',\n\t\t\t\timage: true,\n\t\t\t\tkeywords: [],\n\t\t\t\tname: 'Generic',\n\t\t\t\tplaceholder: '',\n\t\t\t\tpoints: true,\n\t\t\t\tvideo: true,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Retrieve an array of keywords for the question type\n\t\t * Used for filtering questions by search term in the quiz builder\n\t\t *\n\t\t * @return   array\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_keywords: function() {\n\n\t\t\tvar name  = this.get( 'name' ),\n\t\t\t\twords = [ name ];\n\n\t\t\treturn words.concat( this.get( 'keywords' ) ).concat( name.split( ' ' ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Get marker array for the question choices\n\t\t *\n\t\t * @return   array\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_choice_markers: function() {\n\n\t\t\treturn this._get_choice_option( 'markers' );\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if the question's choices are selectable\n\t\t *\n\t\t * @return   bool\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_choice_selectable: function() {\n\n\t\t\treturn this._get_choice_option( 'selectable' );\n\n\t\t},\n\n\t\t/**\n\t\t * Get the choice type (text,image)\n\t\t *\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_choice_type: function() {\n\n\t\t\treturn this._get_choice_option( 'type' );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve defined min. choices\n\t\t *\n\t\t * @return   int\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_min_choices: function() {\n\n\t\t\treturn this._get_choice_option( 'min' );\n\n\t\t},\n\n\t\t/**\n\t\t * Get type-defined max choices\n\t\t *\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_max_choices: function() {\n\n\t\t\treturn this._get_choice_option( 'max' );\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if multi-choice selection is enabled\n\t\t *\n\t\t * @return   bool\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_multi_choices: function() {\n\n\t\t\tvar choices = this.get( 'choices' );\n\n\t\t\tif ( ! choices  ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\treturn this._get_choice_option( 'multi' );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve data from the type's \"choices\" attribute\n\t\t * Allows quick handling of types with no choice definitions w/o additional checks\n\t\t *\n\t\t * @param    string   option  name of the choice option to retrieve\n\t\t * @return   mixed\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_get_choice_option: function( option ) {\n\n\t\t\tvar choices = this.get( 'choices' );\n\n\t\t\tif ( ! choices || ! choices[ option ] ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\treturn choices[ option ];\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/Quiz.js",
    "content": "/**\n * Quiz Model.\n *\n * @since 3.16.0\n * @version 7.5.0\n */\ndefine( [\n\t\t'Collections/Questions',\n\t\t'Models/Lesson',\n\t\t'Models/Question',\n\t\t'Models/_Relationships',\n\t\t'Models/_Utilities',\n\t\t'Schemas/Quiz',\n\t], function(\n\t\tQuestions,\n\t\tLesson,\n\t\tQuestion,\n\t\tRelationships,\n\t\tUtilities,\n\t\tQuizSchema\n\t) {\n\n\treturn Backbone.Model.extend( _.defaults( {\n\n\t\t/**\n\t\t * model relationships\n\t\t * @type  {Object}\n\t\t */\n\t\trelationships: {\n\t\t\tparent: {\n\t\t\t\tmodel: 'lesson',\n\t\t\t\ttype: 'model',\n\t\t\t},\n\t\t\tchildren: {\n\t\t\t\tquestions: {\n\t\t\t\t\tclass: 'Questions',\n\t\t\t\t\tmodel: 'llms_question',\n\t\t\t\t\ttype: 'collection',\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Lesson Settings Schema\n\t\t * @type  {Object}\n\t\t */\n\t\tschema: QuizSchema,\n\n\t\t/**\n\t\t * New quiz defaults.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 7.4.0 Added filter for filtering defaults.\n\t\t * @since 7.5.0 Replaced unused `random_answers` property with `random_questions`.\n\t\t * @since 7.8.0 Added filter for filtering defaults and `can_be_resumed` property.\n\t\t *\n\t\t * @return {Object}\n\t\t */\n\t\tdefaults: function() {\n\n\t\t\treturn window.llms.hooks.applyFilters( 'llms_quiz_model_defaults', {\n\t\t\t\tid: _.uniqueId( 'temp_' ),\n\t\t\t\ttitle: LLMS.l10n.translate( 'New Quiz' ),\n\t\t\t\ttype: 'llms_quiz',\n\t\t\t\tlesson_id: '',\n\n\t\t\t\tstatus: 'publish',\n\n\t\t\t\t// editable fields.\n\t\t\t\tcontent: '',\n\t\t\t\tallowed_attempts: 5,\n\t\t\t\tlimit_attempts: 'no',\n\t\t\t\tlimit_time: 'no',\n\t\t\t\tpassing_percent: 65,\n\t\t\t\tname: '',\n\t\t\t\trandom_questions: 'no',\n\t\t\t\ttime_limit: 30,\n\t\t\t\tshow_correct_answer: 'no',\n\t\t\t\tcan_be_resumed: 'no',\n\t\t\t\tdisable_retake: 'no',\n\n\t\t\t\tquestions: [],\n\n\t\t\t\t// calculated.\n\t\t\t\t_points: 0,\n\n\t\t\t\t// display.\n\t\t\t\tpermalink: '',\n\t\t\t\t_show_settings: false,\n\t\t\t\t_questions_loaded: false,\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Initializer\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.24.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.init_custom_schema();\n\t\t\tthis.startTracking();\n\t\t\tthis.init_relationships();\n\n\t\t\tthis.listenTo( this.get( 'questions' ), 'add', this.update_points );\n\t\t\tthis.listenTo( this.get( 'questions' ), 'remove', this.update_points );\n\n\t\t\tthis.set( '_points', this.get_total_points() );\n\n\t\t\t// when a quiz is published, ensure the parent lesson is marked as \"Enabled\" for quizzing\n\t\t\tthis.on( 'change:status', function() {\n\t\t\t\tif ( 'publish' === this.get( 'status' ) ) {\n\t\t\t\t\tthis.get_parent().set( 'quiz_enabled', 'yes' );\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\twindow.llms.hooks.doAction( 'llms_quiz_model_init', this );\n\n\t\t},\n\n\t\t/**\n\t\t * Add a new question to the quiz\n\t\t * @param    obj   data   question data\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tadd_question: function( data ) {\n\n\t\t\tdata.parent_id = this.get( 'id' );\n\t\t\tvar question = this.get( 'questions' ).add( data, {\n\t\t\t\tparent: this,\n\t\t\t} );\n\t\t\tBackbone.pubSub.trigger( 'quiz-add-question', question, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the translated post type name for the model's type\n\t\t * @param    bool     plural  if true, returns the plural, otherwise returns singular\n\t\t * @return   string\n\t\t * @since    3.16.12\n\t\t * @version  3.16.12\n\t\t */\n\t\tget_l10n_type: function( plural ) {\n\n\t\t\tif ( plural ) {\n\t\t\t\treturn LLMS.l10n.translate( 'quizzes' );\n\t\t\t}\n\n\t\t\treturn LLMS.l10n.translate( 'quiz' );\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the quiz's total points\n\t\t * @return   int\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_total_points: function() {\n\n\t\t\tvar points = 0;\n\n\t\t\tthis.get( 'questions' ).each( function( question ) {\n\t\t\t\tpoints += question.get_points();\n\t\t\t} );\n\n\t\t\treturn points;\n\n\t\t},\n\n\t\t/**\n\t\t * Lazy load questions via AJAX\n\t\t * @param    {Function}  cb  callback function\n\t\t * @return   void\n\t\t * @since    3.19.2\n\t\t * @version  3.19.2\n\t\t */\n\t\tload_questions: function( cb ) {\n\n\t\t\tif ( this.get( '_questions_loaded' ) ) {\n\n\t\t\t\tcb();\n\n\t\t\t} else {\n\n\t\t\t\tvar self = this;\n\n\t\t\t\tLLMS.Ajax.call( {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\taction: 'llms_builder',\n\t\t\t\t\t\taction_type: 'lazy_load',\n\t\t\t\t\t\tcourse_id: window.llms_builder.CourseModel.get( 'id' ),\n\t\t\t\t\t\tload_id: this.get( 'id' ),\n\t\t\t\t\t},\n\t\t\t\t\terror: function( xhr, status, error ) {\n\n\t\t\t\t\t\tconsole.log( xhr, status, error );\n\t\t\t\t\t\twindow.llms_builder.debug.log( '==== start load_questions error ====', xhr, status, error, '==== finish load_questions error ====' );\n\t\t\t\t\t\tcb( true );\n\n\t\t\t\t\t},\n\t\t\t\t\tsuccess: function( res ) {\n\t\t\t\t\t\tif ( res && res.questions ) {\n\t\t\t\t\t\t\tself.set( '_questions_loaded', true );\n\t\t\t\t\t\t\tif ( res.questions ) {\n\t\t\t\t\t\t\t\t_.each( res.questions, self.add_question, self );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcb();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcb( true );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\n\t\t},\n\n\t\t/**\n\t\t * Update total number of points calculated property\n\t\t * @return   int\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_points: function() {\n\n\t\t\tthis.set( '_points', this.get_total_points() );\n\n\t\t},\n\n\t}, Relationships, Utilities ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/Section.js",
    "content": "/**\n * Section Model\n *\n * @since 3.16.0\n * @version 4.20.0\n */\ndefine( [ 'Collections/Lessons', 'Models/_Relationships' ], function( Lessons, Relationships ) {\n\n\treturn Backbone.Model.extend( _.defaults( {\n\n\t\trelationships: {\n\t\t\tparent: {\n\t\t\t\tmodel: 'course',\n\t\t\t\ttype: 'model',\n\t\t\t},\n\t\t\tchildren: {\n\t\t\t\tlessons: {\n\t\t\t\t\tclass: 'Lessons',\n\t\t\t\t\tmodel: 'lesson',\n\t\t\t\t\ttype: 'collection',\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * New section defaults\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Object}\n\t\t */\n\t\tdefaults: function() {\n\t\t\treturn {\n\t\t\t\tid: _.uniqueId( 'temp_' ),\n\t\t\t\tlessons: [],\n\t\t\t\torder: this.collection ? this.collection.length + 1 : 1,\n\t\t\t\tparent_course: window.llms_builder.course.id,\n\t\t\t\ttitle: LLMS.l10n.translate( 'New Section' ),\n\t\t\t\ttype: 'section',\n\n\t\t\t\t// Expand the first 100 sections by default to avoid timeout issues.\n\t\t\t\t_expanded: ! this.collection || this.collection.length <= 100 ? true : false,\n\t\t\t\t_selected: false,\n\t\t\t};\n\t\t},\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.startTracking();\n\t\t\tthis.init_relationships();\n\n\t\t},\n\n\t\t/**\n\t\t * Add a lesson to the section\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.16.11 Unknown.\n\t\t *\n\t\t * @param {Object} data    Hash of lesson data (creates new lesson)\n\t\t *                         or existing lesson as a Backbone.Model.\n\t\t * @param {Object} options Hash of options.\n\t\t * @return {Object} Backbone.Model of the new/updated lesson.\n\t\t */\n\t\tadd_lesson: function( data, options ) {\n\n\t\t\tdata    = data || {};\n\t\t\toptions = options || {};\n\n\t\t\tif ( data instanceof Backbone.Model ) {\n\t\t\t\tdata.set( 'status', 'publish' );\n\t\t\t\tdata.set( 'parent_section', this.get( 'id' ) );\n\t\t\t\tdata.set_parent( this );\n\t\t\t} else {\n\t\t\t\tdata.status = 'publish';\n\t\t\t\tdata.parent_section = this.get( 'id' );\n\t\t\t}\n\n\t\t\treturn this.get( 'lessons' ).add( data, options );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the translated post type name for the model's type\n\t\t *\n\t\t * @since 3.16.12\n\t\t *\n\t\t * @param {Boolean} plural If true, returns the plural, otherwise returns singular.\n\t\t * @return {String}\n\t\t */\n\t\tget_l10n_type: function( plural ) {\n\n\t\t\tif ( plural ) {\n\t\t\t\treturn LLMS.l10n.translate( 'sections' );\n\t\t\t}\n\n\t\t\treturn LLMS.l10n.translate( 'section' );\n\t\t},\n\n\t\t/**\n\t\t * Get next section in the collection\n\t\t *\n\t\t * @since 3.16.11\n\t\t *\n\t\t * @param {boolean} circular If true handles the collection in a circle.\n\t\t *                           If current is the last section, returns the first section.\n\t\t * @return {Object}|false\n\t\t */\n\t\tget_next: function( circular ) {\n\t\t\treturn this._get_sibling( 'next', circular );\n\t\t},\n\n\t\t/**\n\t\t * Retrieve a reference to the parent course of the section\n\t\t *\n\t\t * @since 4.14.0\n\t\t *\n\t\t * @return {Object}\n\t\t */\n\t\tget_course: function() {\n\n\t\t\t// When working with an unsaved draft course the parent isn't properly set on the creation of a section.\n\t\t\tif ( ! this.get_parent() ) {\n\t\t\t\tthis.set_parent( window.llms_builder.CourseModel );\n\t\t\t}\n\n\t\t\treturn this.get_parent();\n\n\t\t},\n\n\t\t/**\n\t\t * Get prev section in the collection\n\t\t *\n\t\t * @since 3.16.11\n\t\t *\n\t\t * @param {Boolean} circular If true handles the collection in a circle.\n\t\t *                           If current is the first section, returns the last section.\n\t\t * @return {Object}|false\n\t\t */\n\t\tget_prev: function( circular ) {\n\t\t\treturn this._get_sibling( 'prev', circular );\n\t\t},\n\n\t\t/**\n\t\t * Get a sibling section\n\t\t *\n\t\t * @since 3.16.11\n\t\t * @since 4.20.0 Fix case when the last section was returned when looking for the prev of the first section and not `circular`.\n\t\t *\n\t\t * @param {String}  direction Siblings direction [next|prev].\n\t\t * @param {Boolean} circular  If true handles the collection in a circle.\n\t\t *                            If current is the last section, returns the first section.\n\t\t *                            If current is the first section, returns the last section.\n\t\t * @return {Object}|false\n\t\t */\n\t\t_get_sibling: function( direction, circular ) {\n\n\t\t\tcircular = ( 'undefined' === circular ) ? true : circular;\n\n\t\t\tvar max   = this.collection.size() - 1,\n\t\t\t\tindex = this.collection.indexOf( this ),\n\t\t\t\tsibling_index;\n\n\t\t\tif ( 'next' === direction ) {\n\t\t\t\tsibling_index = index + 1;\n\t\t\t} else if ( 'prev' === direction ) {\n\t\t\t\tsibling_index = index - 1;\n\t\t\t}\n\n\t\t\t// Don't retrieve greater than max or less than min.\n\t\t\tif ( sibling_index <= max || sibling_index >= 0 ) {\n\n\t\t\t\treturn this.collection.at( sibling_index );\n\n\t\t\t} else if ( circular ) {\n\n\t\t\t\tif ( 'next' === direction ) {\n\t\t\t\t\treturn this.collection.first();\n\t\t\t\t} else if ( 'prev' === direction ) {\n\t\t\t\t\treturn this.collection.last();\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t},\n\n\t}, Relationships ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/_Relationships.js",
    "content": "/**\n * Model relationships mixin\n *\n * @since    3.16.0\n * @version  3.16.11\n */\ndefine( [], function() {\n\n\treturn {\n\n\t\t/**\n\t\t * Default relationship settings object\n\t\t *\n\t\t * @type  {Object}\n\t\t */\n\t\trelationship_defaults: {\n\t\t\tparent: {},\n\t\t\tchildren: {},\n\t\t},\n\n\t\t/**\n\t\t * Relationship settings object\n\t\t * Should be overridden in the model\n\t\t *\n\t\t * @type  {Object}\n\t\t */\n\t\trelationships: {},\n\n\t\t/**\n\t\t * Initialize all parent and child relationships\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinit_relationships: function( options ) {\n\n\t\t\tvar rels = this.get_relationships();\n\n\t\t\t// initialize parent relationships\n\t\t\t// useful when adding a model to ensure parent is initialized\n\t\t\tif ( rels.parent && options && options.parent ) {\n\t\t\t\tthis.set_parent( options.parent );\n\t\t\t}\n\n\t\t\t// initialize all children relationships\n\t\t\t_.each( rels.children, function( child_data, child_key ) {\n\n\t\t\t\tif ( ! child_data.conditional || true === child_data.conditional( this ) ) {\n\n\t\t\t\t\tvar child_val = this.get( child_key ),\n\t\t\t\t\t\tchild;\n\n\t\t\t\t\tif ( child_data.lookup ) {\n\t\t\t\t\t\tchild = child_data.lookup( child_val );\n\t\t\t\t\t} else if ( 'model' === child_data.type ) {\n\t\t\t\t\t\tchild = window.llms_builder.construct.get_model( child_data.class, child_val );\n\t\t\t\t\t} else if ( 'collection' === child_data.type ) {\n\t\t\t\t\t\tchild = window.llms_builder.construct.get_collection( child_data.class, child_val );\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.set( child_key, child );\n\n\t\t\t\t\t// if the child defines a parent, save a reference to the parent on the child\n\t\t\t\t\tif ( 'model' === child_data.type ) {\n\t\t\t\t\t\tthis._maybe_set_parent_reference( child );\n\n\t\t\t\t\t\t// save directly to each model in the collection\n\t\t\t\t\t} else if ( 'collection' === child_data.type ) {\n\n\t\t\t\t\t\tchild.parent = this;\n\t\t\t\t\t\tchild.each( function( child_model ) {\n\n\t\t\t\t\t\t\tthis._maybe_set_parent_reference( child_model );\n\n\t\t\t\t\t\t}, this );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the property names for all children of the model\n\t\t *\n\t\t * @return   array\n\t\t * @since    3.16.11\n\t\t * @version  3.16.11\n\t\t */\n\t\tget_child_props: function() {\n\n\t\t\tvar props = [];\n\n\t\t\t_.each( this.get_relationships().children, function( data, key ) {\n\n\t\t\t\tif ( ! data.conditional || true === data.conditional( this ) ) {\n\t\t\t\t\tprops.push( key );\n\t\t\t\t}\n\n\t\t\t}, this );\n\n\t\t\treturn props;\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the model's parent (if set)\n\t\t *\n\t\t * @return   obj|false\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_parent: function() {\n\n\t\t\tvar rels = this.get_relationships();\n\n\t\t\tif ( rels.parent ) {\n\t\t\t\treturn rels.parent.reference;\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve relationships for the model\n\t\t * Extends with defaults\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_relationships: function() {\n\n\t\t\treturn $.extend( true, this.relationships, this.relationship_defaults );\n\n\t\t},\n\n\t\t/**\n\t\t * Set the parent reference for the given model\n\t\t *\n\t\t * @param    obj   obj   parent model obj\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tset_parent: function( obj ) {\n\t\t\tthis.relationships.parent.reference = obj;\n\t\t},\n\n\t\t/**\n\t\t * Set up the parent relationships for qualifying children during relationship initialization\n\t\t *\n\t\t * @param    obj   model  child model\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_maybe_set_parent_reference: function( model ) {\n\n\t\t\tif ( ! model || ! model.get_relationships ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar rels = model.get_relationships();\n\t\t\tif ( rels.parent && rels.parent.model === this.get( 'type' ) ) {\n\t\t\t\tmodel.set_parent( this );\n\t\t\t}\n\n\t\t},\n\n\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/_Utilities.js",
    "content": "/**\n * Utility functions for Models.\n *\n * @since 3.16.0\n * @version 7.4.0\n */\ndefine( [], function() {\n\n\treturn {\n\n\t\tfields: [],\n\n\t\t/**\n\t\t * Override Backbone `set` method.\n\t\t *\n\t\t * Takes into account attributes of the form object[prop].\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param {Mixed} attr The attribute to be set.\n\t\t * @param {Mixed} val  The value to set.\n\t\t */\n\t\tset: function ( attr, val ) {\n\n\t\t\tif ( 'string' === typeof attr ) {\n\n\t\t\t\tconst matches = attr.match( /(.*?)\\[(.*?)\\]/ );\n\t\t\t\tif ( matches && 3 === matches.length ) {\n\n\t\t\t\t\tconst\n\t\t\t\t\t\trealAttr   = matches[1],\n\t\t\t\t\t\tcurrentVal = Backbone.Model.prototype.get.call( this, realAttr );\n\n\t\t\t\t\tvar newVal = undefined !== currentVal ? currentVal : {};\n\n\t\t\t\t\tnewVal[ matches[2] ] = val;\n\n\t\t\t\t\targuments[0] = realAttr;\n\t\t\t\t\targuments[1] = newVal;\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Continue with Backbone default `set` behavior.\n\t\t\tBackbone.Model.prototype.set.apply( this, arguments );\n\n\t\t},\n\n\t\t/**\n\t\t * Override Backbone `get` method.\n\t\t *\n\t\t * Takes into account attributes of the form object[prop].\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param {Mixed} attr The attribute name.\n\t\t */\n\t\tget: function( attr ) {\n\n\t\t\tconst matches = attr.match( /(.*?)\\[(.*?)\\]/ );\n\t\t\tif ( matches && 3 === matches.length ) {\n\t\t\t\tconst val = Backbone.Model.prototype.get.call( this, matches[1] );\n\t\t\t\tif ( val && undefined !== val[ matches[2] ] ) {\n\t\t\t\t\treturn val[ matches[2] ];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Continue with Backbone default `get` behavior.\n\t\t\treturn Backbone.Model.prototype.get.call( this, attr );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the edit post link for the current model.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return string\n\t\t */\n\t\tget_edit_post_link: function() {\n\n\t\t\tif ( this.has_temp_id() ) {\n\t\t\t\treturn '';\n\t\t\t}\n\n\t\t\treturn window.llms_builder.admin_url + 'post.php?post=' + this.get( 'id' ) + '&action=edit';\n\n\t\t},\n\n\t\tget_view_post_link: function() {\n\t\t\tif ( this.has_temp_id() ) {\n\t\t\t\treturn '';\n\t\t\t}\n\n\t\t\tif ( this.get( 'permalink' ) ) {\n\t\t\t\treturn this.get( 'permalink' );\n\t\t\t}\n\n\t\t\tif ( this.get( 'status' ) === 'publish' ) {\n\t\t\t\treturn window.llms_builder.home_url + '?p=' + this.get( 'id' );\n\t\t\t}\n\n\t\t\treturn window.llms_builder.home_url + '?p=' + this.get( 'id' ) + '&preview=true&post_type=' + this.get( 'type' );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve schema fields defined for the model\n\t\t *\n\t\t * @return   object\n\t\t * @since    3.17.0\n\t\t * @version  3.17.1\n\t\t */\n\t\tget_settings_fields: function() {\n\n\t\t\tvar schema = this.schema || {};\n\t\t\treturn window.llms_builder.schemas.get( schema, this.get( 'type' ).replace( 'llms_', '' ), this );\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if the model has a temporary ID\n\t\t *\n\t\t * @return   {Boolean}\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\thas_temp_id: function() {\n\n\t\t\treturn ( ! _.isNumber( this.get( 'id' ) ) && 0 === this.get( 'id' ).indexOf( 'temp_' ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Initializes 3rd party custom schema (field) data for a model\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tinit_custom_schema: function() {\n\n\t\t\tvar groups = _.filter( this.get_settings_fields(), function( group ) {\n\t\t\t\treturn ( group.custom );\n\t\t\t} );\n\n\t\t\t_.each( groups, function( group ) {\n\t\t\t\t_.each( _.flatten( group.fields ), function( field ) {\n\n\t\t\t\t\tvar keys    = [ field.attribute ],\n\t\t\t\t\t\tcustoms = this.get( 'custom' );\n\n\t\t\t\t\tif ( field.switch_attribute ) {\n\t\t\t\t\t\tkeys.push( field.switch_attribute );\n\t\t\t\t\t}\n\n\t\t\t\t\t_.each( keys, function( key ) {\n\t\t\t\t\t\tvar attr = field.attribute_prefix ? field.attribute_prefix + key : key;\n\t\t\t\t\t\tif ( customs && customs[ attr ] ) {\n\t\t\t\t\t\t\tthis.set( key, customs[ attr ][0] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}, this );\n\n\t\t\t\t}, this );\n\t\t\t}, this );\n\n\t\t},\n\n\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Models/loader.js",
    "content": "/**\n * Load all models\n *\n * @return   obj\n * @since    3.16.0\n * @version  3.17.0\n */\ndefine( [\n\t\t'Models/Abstract',\n\t\t'Models/Course',\n\t\t'Models/Image',\n\t\t'Models/Lesson',\n\t\t'Models/Question',\n\t\t'Models/QuestionChoice',\n\t\t'Models/QuestionType',\n\t\t'Models/Quiz',\n\t\t'Models/Section'\n\t],\n\tfunction(\n\t\tAbstract,\n\t\tCourse,\n\t\tImage,\n\t\tLesson,\n\t\tQuestion,\n\t\tQuestionChoice,\n\t\tQuestionType,\n\t\tQuiz,\n\t\tSection\n\t) {\n\n\t\treturn {\n\t\t\tAbstract: Abstract,\n\t\t\tCourse: Course,\n\t\t\tImage: Image,\n\t\t\tLesson: Lesson,\n\t\t\tQuestion: Question,\n\t\t\tQuestionChoice: QuestionChoice,\n\t\t\tQuestionType: QuestionType,\n\t\t\tQuiz: Quiz,\n\t\t\tSection: Section,\n\t\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Schemas/Lesson.js",
    "content": "/**\n * Lesson Schemas\n *\n * @since    3.17.0\n * @version  3.25.4\n */\ndefine( [], function() {\n\n\treturn window.llms.hooks.applyFilters( 'llms_define_lesson_schema', {\n\n\t\tdefault: {\n\t\t\ttitle: LLMS.l10n.translate( 'General Settings' ),\n\t\t\ttoggleable: true,\n\t\t\tfields: [\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\tattribute: 'permalink',\n\t\t\t\t\tid: 'permalink',\n\t\t\t\t\ttype: 'permalink',\n\t\t},\n\t\t\t], [\n\t\t\t\t{\n\t\t\t\t\tattribute: 'content',\n\t\t\t\t\tid: 'content',\n\t\t\t\t\tlabel: LLMS.l10n.translate( 'Content' ),\n\t\t\t\t\ttype: 'editor',\n\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\treturn '' === this.get( 'content' ) || 'yes' === this.get( 'content_added_in_builder' );\n\t\t\t\t\t},\n\t\t},\n\t\t\t], [\n\t\t\t\t{\n\t\t\t\t\tid: 'content-page-builder-notice',\n\t\t\t\t\tlabel: LLMS.l10n.translate( 'Content' ),\n\t\t\t\t\ttype: 'page_builder_notice',\n\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\treturn '' !== this.get( 'content' ) && 'yes' !== this.get( 'content_added_in_builder' );\n\t\t\t\t\t},\n\t\t},\n\t\t\t], [\n\t\t\t\t{\n\t\t\t\t\tattribute: 'video_embed',\n\t\t\t\t\t\tid: 'video-embed',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Video Embed URL' ),\n\t\t\t\t\t\ttype: 'video_embed',\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'audio_embed',\n\t\t\t\t\t\tid: 'audio-embed',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Audio Embed URL' ),\n\t\t\t\t\t\ttype: 'audio_embed',\n\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'free_lesson',\n\t\t\t\t\t\tid: 'free-lesson',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Free Lesson' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Free lessons can be accessed without enrollment.' ),\n\t\t\t\t\t\ttype: 'switch',\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'require_passing_grade',\n\t\t\t\t\t\tid: 'require-passing-grade',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Require Passing Grade on Quiz' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'When enabled, students must pass this quiz before the lesson can be completed.' ),\n\t\t\t\t\t\ttype: 'switch',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn ( 'yes' === this.get( 'quiz_enabled' ) );\n\t\t\t\t\t\t},\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'require_assignment_passing_grade',\n\t\t\t\t\t\tid: 'require-assignment-passing-grade',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Require Passing Grade on Assignment' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'When enabled, students must pass this assignment before the lesson can be completed.' ),\n\t\t\t\t\t\ttype: 'switch',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn ( 'undefined' !== window.llms_builder.assignments && 'yes' === this.get( 'assignment_enabled' ) );\n\t\t\t\t\t\t},\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'points',\n\t\t\t\t\t\tid: 'points',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Lesson Weight' ),\n\t\t\t\t\t\tlabel_after: LLMS.l10n.translate( 'POINTS' ),\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\tmax: 99,\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Determines the weight of the lesson when calculating the overall grade of the course.' ),\n\t\t\t\t\t\ttip_position: 'top-left',\n\t\t\t\t\t\ttype: 'number',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn ( ( 'yes' === this.get( 'quiz_enabled' ) ) || ( 'undefined' !== window.llms_builder.assignments && 'yes' === this.get( 'assignment_enabled' ) ) );\n\t\t\t\t\t\t},\n\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'prerequisite',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn ( false === this.is_first_in_course() );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tid: 'prerequisite',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Prerequisite' ),\n\t\t\t\t\t\tswitch_attribute: 'has_prerequisite',\n\t\t\t\t\t\ttype: 'switch-select',\n\t\t\t\t\t\toptions: function() {\n\t\t\t\t\t\t\treturn this.get_available_prereq_options();\n\t\t\t\t\t\t},\n\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Course Drip Method' ),\n\t\t\t\t\t\tid: 'course-drip',\n\t\t\t\t\t\ttype: 'heading',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdetail: LLMS.l10n.translate( 'Drip settings are currently set at the course level, under the Restrictions settings tab. Disable to allow lesson level drip settings.' ) + ' <a href=\\\"javascript:document.getElementById(\\'llms-exit-button\\').click()\\\">' + LLMS.l10n.translate( 'Edit Course' ) + '</a>',\n\t\t\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Course Drip Method' ),\n\t\t\t\t\t\tid: 'course-drip',\n\t\t\t\t\t\ttype: 'heading',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn ( ! this.get_course() || 'yes' !== this.get_course().get( 'lesson_drip' ) || ! this.get_course().get( 'drip_method' ) );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdetail: LLMS.l10n.translate( 'Drip settings can be set at the course level to release course content at a specified interval, in the Restrictions settings tab.' ) + ' <a href=\\\"javascript:document.getElementById(\\'llms-exit-button\\').click()\\\">' + LLMS.l10n.translate( 'Edit Course' ) + '</a>',\n\t\t\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'drip_method',\n\t\t\t\t\t\tid: 'drip-method',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Drip Method' ),\n\t\t\t\t\t\tswitch_attribute: 'drip_method',\n\t\t\t\t\t\ttype: 'select',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn ( ! this.get_course() || 'yes' !== this.get_course().get( 'lesson_drip' ) || ! this.get_course().get( 'drip_method' ) );\n\t\t\t\t\t\t},\n\t\t\t\t\t\toptions: function() {\n\n\t\t\t\t\t\t\tvar options = [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkey: '',\n\t\t\t\t\t\t\t\t\tval: LLMS.l10n.translate( 'None' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkey: 'date',\n\t\t\t\t\t\t\t\t\tval: LLMS.l10n.translate( 'On a specific date' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkey: 'enrollment',\n\t\t\t\t\t\t\t\t\tval: LLMS.l10n.translate( '# of days after course enrollment' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t];\n\n\t\t\t\t\t\t\tif ( this.get_course() && this.get_course().get( 'start_date' ) ) {\n\t\t\t\t\t\t\t\toptions.push( {\n\t\t\t\t\t\t\t\t\tkey: 'start',\n\t\t\t\t\t\t\t\t\tval: LLMS.l10n.translate( '# of days after course start date' ),\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ( 'yes' === this.get( 'has_prerequisite' ) ) {\n\t\t\t\t\t\t\t\toptions.push( {\n\t\t\t\t\t\t\t\t\tkey: 'prerequisite',\n\t\t\t\t\t\t\t\t\tval: LLMS.l10n.translate( '# of days after prerequisite lesson completion' ),\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn options;\n\n\t\t\t\t\t\t},\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'days_before_available',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\tif ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) {\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn ( -1 !== [ 'enrollment', 'start', 'prerequisite' ].indexOf( this.get( 'drip_method' ) ) );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tid: 'days-before-available',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( '# of days' ),\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\ttype: 'number',\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'date_available',\n\t\t\t\t\t\tdate_format: 'Y-m-d',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\tif ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) {\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn ( 'date' === this.get( 'drip_method' ) );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tid: 'date-available',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Date' ),\n\t\t\t\t\t\ttimepicker: 'false',\n\t\t\t\t\t\ttype: 'datepicker',\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'time_available',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\tif ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) {\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn ( 'date' === this.get( 'drip_method' ) );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdatepicker: 'false',\n\t\t\t\t\t\tdate_format: 'h:i A',\n\t\t\t\t\t\tid: 'time-available',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Time' ),\n\t\t\ttype: 'datepicker',\n\t\t},\n\t\t\t], [\n\t\t\t\t{\n\t\t\t\t\tlabel: LLMS.l10n.translate( 'Associated Event(s)' ),\n\t\t\t\t\tid: 'llms-events-promo',\n\t\t\t\t\ttype: 'heading',\n\t\t\t\t\tdetail: LLMS.l10n.translate( 'Schedule events for your lessons with the LifterLMS Events add-on.' ) + ' <a href=\"https://lifterlms.com/product/lifterlms-events/?utm_source=LifterLMS%20Plugin&utm_medium=Lesson%20Builder&utm_campaign=Events%20Addon%20Upsell\" target=\"_blank\">' + LLMS.l10n.translate( 'Learn More' ) + '</a>',\n\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\treturn ! window.llms_builder.events;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t],\n\t},\n\n} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Schemas/Quiz.js",
    "content": "/**\n * Quiz Schema.\n *\n * @since 3.17.6\n * @since 7.4.0 Added upsell for Question Bank and condition in `random_questions` schema.\n * @since 7.6.2 Added `disable_retake` schema.\n * @since 7.8.0 Added `can_be_resumed` option.\n * @version 7.8.0\n\n */\ndefine( [], function() {\n\n\treturn window.llms.hooks.applyFilters( 'llms_define_quiz_schema', {\n\n\t\tdefault: {\n\t\t\ttitle: LLMS.l10n.translate( 'General Settings' ),\n\t\t\ttoggleable: true,\n\t\t\tfields: [\n\t\t\t\t[\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'permalink',\n\t\t\t\t\t\tid: 'permalink',\n\t\t\t\t\t\ttype: 'permalink',\n\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'content',\n\t\t\t\t\t\tid: 'description',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Description' ),\n\t\t\t\t\t\ttype: 'editor',\n\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'passing_percent',\n\t\t\t\t\t\tid: 'passing-percent',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Passing Percentage' ),\n\t\t\t\t\t\tmin: 0,\n\t\t\t\t\t\tmax: 100,\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Minimum percentage of total points required to pass the quiz' ),\n\t\t\t\t\t\ttype: 'number',\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'allowed_attempts',\n\t\t\t\t\t\tid: 'allowed-attempts',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Limit Attempts' ),\n\t\t\t\t\t\tswitch_attribute: 'limit_attempts',\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Limit the maximum number of times a student can take this quiz' ),\n\t\t\t\t\t\ttype: 'switch-number',\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'time_limit',\n\t\t\t\t\t\tid: 'time-limit',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Time Limit' ),\n\t\t\t\t\t\tmin: 1,\n\t\t\t\t\t\tmax: 360,\n\t\t\t\t\t\tswitch_attribute: 'limit_time',\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Enforce a maximum number of minutes a student can spend on each attempt' ),\n\t\t\t\t\t\ttype: 'switch-number',\n\t\t\t},\n\t\t\t\t], [\n\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'can_be_resumed',\n\t\t\t\t\t\tid: 'resume',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Can be resumed' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Allow a new attempt on this quiz to be resumed' ),\n\t\t\t\t\t\ttype: 'switch',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn 'yes' === this.get( 'limit_time' ) ? false : true;\n\t\t\t\t\t\t}\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'show_correct_answer',\n\t\t\t\t\t\tid: 'show-correct-answer',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Show Correct Answers' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'When enabled, students will be shown the correct answer to any question they answered incorrectly.' ),\n\t\t\t\t\t\ttype: 'switch',\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'random_questions',\n\t\t\t\t\t\tid: 'random-questions',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Randomize Question Order' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Display questions in a random order for each attempt. Content questions are locked into their defined positions.' ),\n\t\t\t\t\t\ttype: 'switch',\n\t\t\t\t\t\tcondition: function() {\n\t\t\t\t\t\t\treturn 'yes' === this.get( 'question_bank' ) ? false : true;\n\t\t\t\t\t\t}\n\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tattribute: 'disable_retake',\n\t\t\t\t\t\tid: 'disable-retake',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Disable Retake' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'Prevent quiz retake after student passed the quiz.' ),\n\t\t\t\t\t\ttype: 'switch',\n\t\t\t},\n\t\t\t\t], [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: 'question-bank',\n\t\t\t\t\t\tlabel: LLMS.l10n.translate( 'Question Bank' ),\n\t\t\t\t\t\ttip: LLMS.l10n.translate( 'A question bank helps prevent cheating and reinforces learning by allowing instructors to create assessments with randomized questions pulled from a bank of questions. (Available in Advanced Quizzes addon)' ),\n\t\t\t\t\t\ttype: 'upsell',\n\t\t\t\t\t\ttext: LLMS.l10n.translate( 'Get LifterLMS Advanced Quizzes' ),\n\t\t\t\t\t\turl: 'https://lifterlms.com/product/advanced-quizzes/?utm_source=LifterLMS%20Plugin&utm_medium=Quiz%20Builder%20Button&utm_campaign=Advanced%20Question%20Upsell&utm_content=3.16.0&utm_term=Questions%20Bank'\n\t\t\t\t\t}\n\t\t\t\t]\n\n\t\t\t],\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Assignment.js",
    "content": "/**\n * Single Assignment View.\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.17.0\n * @version 5.4.0\n */\n\ndefine( [\n\t\t'Views/Popover',\n\t\t'Views/PostSearch',\n\t\t'Views/_Detachable',\n\t\t'Views/_Editable',\n\t\t'Views/_Trashable',\n\t\t'Views/_Subview',\n\t\t'Views/SettingsFields'\n\t], function(\n\t\tPopover,\n\t\tPostSearch,\n\t\tDetachable,\n\t\tEditable,\n\t\tTrashable,\n\t\tSubview,\n\t\tSettingsFields\n\t) {\n\n\t\treturn Backbone.View.extend( _.defaults( {\n\n\t\t\t/**\n\t\t\t * Current view state.\n\t\t\t *\n\t\t\t * @type {String}\n\t\t\t */\n\t\t\tstate: 'default',\n\n\t\t\t/**\n\t\t\t * Current Subviews.\n\t\t\t *\n\t\t\t * @type {Object}\n\t\t\t */\n\t\t\tviews: {\n\t\t\t\tsettings: {\n\t\t\t\t\tclass: SettingsFields,\n\t\t\t\t\tinstance: null,\n\t\t\t\t\tstate: 'default',\n\t\t\t\t},\n\t\t\t},\n\n\t\t\tel: '#llms-editor-assignment',\n\n\t\t\t/**\n\t\t\t * DOM Events.\n\t\t\t *\n\t\t\t * @since 3.17.1\n\t\t\t *\n\t\t\t * @return {Object}\n\t\t\t */\n\t\t\tevents: function() {\n\t\t\t\tvar addon_events = this.is_addon_available() ? window.llms_builder.assignments.get_view_events() : {};\n\t\t\t\treturn _.defaults( {\n\t\t\t\t\t'click #llms-existing-assignment': 'add_existing_assignment_click',\n\t\t\t\t\t'click #llms-new-assignment': 'add_new_assignment',\n\t\t\t\t}, Detachable.events, Editable.events, Trashable.events, addon_events );\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Wrapper Tag name.\n\t\t\t *\n\t\t\t * @type {String}\n\t\t\t */\n\t\t\ttagName: 'div',\n\n\t\t\t/**\n\t\t\t * Get the underscore template.\n\t\t\t *\n\t\t\t * @type {[type]}\n\t\t\t */\n\t\t\ttemplate: wp.template( 'llms-assignment-template' ),\n\n\t\t\t/**\n\t\t\t * Initialization callback func (renders the element on screen).\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t * @since 3.17.2 Unknown.\n\t\t\t *\n\t\t\t * @return {Void}\n\t\t\t */\n\t\t\tinitialize: function( data ) {\n\n\t\t\t\tthis.lesson = data.lesson;\n\n\t\t\t\t// initialize the model if the assignment is enabled or it's disabled but we still have data for a assignment\n\t\t\t\tif ( 'yes' === this.lesson.get( 'assignment_enabled' ) || ! _.isEmpty( this.lesson.get( 'assignment' ) ) ) {\n\n\t\t\t\t\tthis.model = this.lesson.get( 'assignment' );\n\n\t\t\t\t\t/**\n\t\t\t\t\t * Todo Item.\n\t\t\t\t\t *\n\t\t\t\t\t * @todo  this is a terrible terrible patch\n\t\t\t\t\t *        I've spent nearly 3 days trying to figure out how to not use this line of code\n\t\t\t\t\t *        ISSUE REPRODUCTION:\n\t\t\t\t\t *        Open course builder\n\t\t\t\t\t *        Open a lesson (A) and add a assignment\n\t\t\t\t\t *        Switch to a new lesson (B)\n\t\t\t\t\t *        Add a new assignment\n\t\t\t\t\t *        Return to lesson A and the assignment's parent will be set to LESSON B\n\t\t\t\t\t *        This will happen for *every* assignment in the builder...\n\t\t\t\t\t *        Adding this set_parent on init guarantees that the assignment's correct parent is set\n\t\t\t\t\t *        after adding new assignment's to other lessons\n\t\t\t\t\t *        it's awful and it's gross...\n\t\t\t\t\t *        I'm confused and tired and going to miss release dates again because of it\n\t\t\t\t\t */\n\t\t\t\t\tthis.model.set_parent( this.lesson );\n\n\t\t\t\t\tthis.listenTo( this.model, 'change:permalink', this.render_settings );\n\n\t\t\t\t}\n\n\t\t\t\tthis.on( 'model-trashed', this.on_trashed );\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Compiles the template and renders the view.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t * @since 3.17.7 Unknown.\n\t\t\t *\n\t\t\t * @return {Self} For chaining.\n\t\t\t */\n\t\t\trender: function() {\n\n\t\t\t\tthis.$el.html( this.template( this.model ) );\n\n\t\t\t\tif ( this.model && this.is_addon_available() ) {\n\n\t\t\t\t\tthis.stopListening( this.model, 'change:assignment_type', this.render );\n\n\t\t\t\t\tthis.render_subview( 'settings', {\n\t\t\t\t\t\tel: '#llms-assignment-settings-fields',\n\t\t\t\t\t\tmodel: this.model,\n\t\t\t\t\t} );\n\n\t\t\t\t\t// this.init_datepickers();\n\t\t\t\t\tthis.init_selects();\n\n\t\t\t\t\twindow.llms_builder.assignments.render_editor( this );\n\n\t\t\t\t\tthis.listenTo( this.model, 'change:assignment_type', this.render );\n\n\t\t\t\t}\n\n\t\t\t\treturn this;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Re-render the settings subview when permalink updates after saving.\n\t\t\t *\n\t\t\t * @since 10.0.0\n\t\t\t *\n\t\t\t * @return {Void}\n\t\t\t */\n\t\t\trender_settings: function() {\n\n\t\t\t\tvar view = this.get_subview( 'settings' );\n\t\t\t\tif ( view && view.instance ) {\n\t\t\t\t\tview.instance.render();\n\t\t\t\t\tthis.init_selects();\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Adds a new assignment to a lesson which currently has no assignment associated with it.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t *\n\t\t\t * @return {Void}\n\t\t\t */\n\t\t\tadd_new_assignment: function() {\n\n\t\t\t\tif ( this.is_addon_available() ) {\n\n\t\t\t\t\tthis.model = window.llms_builder.assignments.get_assignment( {\n\t\t\t\t\t\t/* Translators: %1$s = associated lesson title */\n\t\t\t\t\t\ttitle: LLMS.l10n.replace( '%1$s Assignment', {\n\t\t\t\t\t\t\t'%1$s': this.lesson.get( 'title' ),\n\t\t\t\t\t\t} ),\n                        lesson_id: this.lesson.get( 'id' ),\n\t\t\t\t\t} );\n\n\t\t\t\t\tthis.lesson.set( 'assignment_enabled', 'yes' );\n\t\t\t\t\tthis.lesson.set( 'assignment', this.model );\n\n\t\t\t\t\tthis.listenTo( this.model, 'change:permalink', this.render_settings );\n\t\t\t\t\tthis.render();\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis.show_ad_popover( '#llms-new-assignment' );\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * When an assignment is selected from the post select popover\n\t\t\t * instantiate it and add it to the current lesson.\n\t\t\t *\n\t\t\t * @param {Object} event Data from the select2 select event.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t * @since 5.4.0 Prepare assignment object for cloning and use author id instead of the quiz author object.\n\t\t\t */\n\t\t\tadd_existing_assignment: function( event ) {\n\n\t\t\t\tthis.post_search_popover.hide();\n\n\t\t\t\tvar assignment = event.data;\n\n\t\t\t\tif ( 'clone' === event.action ) {\n\n\t\t\t\t\tassignment = _.prepareAssignmentObjectForCloning( assignment );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// Use author id instead of the assignment author object.\n\t\t\t\t\tassignment = _.prepareExistingPostObjectDataForAddingOrCloning( assignment );\n\t\t\t\t\tassignment._forceSync = true;\n\n\t\t\t\t}\n\n\t\t\t\tassignment.lesson_id = this.lesson.get( 'id' )\n\n\t\t\t\tassignment = window.llms_builder.construct.get_model( 'Assignment', assignment );\n\n\t\t\t\tthis.lesson.set( 'assignment_enabled', 'yes' );\n\t\t\t\tthis.lesson.set( 'assignment', assignment );\n\t\t\t\tthis.model = assignment;\n\n\t\t\t\tthis.listenTo( this.model, 'change:permalink', this.render_settings );\n\t\t\t\tthis.render();\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Open add existing assignment popover.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t *\n\t\t\t * @param {Object} event JS event object.\n\t\t\t * @return {Void}\n\t\t\t */\n\t\t\tadd_existing_assignment_click: function( event ) {\n\n\t\t\t\tevent.preventDefault();\n\n\t\t\t\tif ( this.is_addon_available() ) {\n\n\t\t\t\t\tthis.post_search_popover = new Popover( {\n\t\t\t\t\t\tel: '#llms-existing-assignment',\n\t\t\t\t\t\targs: {\n\t\t\t\t\t\t\tbackdrop: true,\n\t\t\t\t\t\t\tcloseable: true,\n\t\t\t\t\t\t\tcontainer: '.wrap.lifterlms.llms-builder',\n\t\t\t\t\t\t\tdismissible: true,\n\t\t\t\t\t\t\tplacement: 'left',\n\t\t\t\t\t\t\twidth: 480,\n\t\t\t\t\t\t\ttitle: LLMS.l10n.translate( 'Add Existing Assignment' ),\n\t\t\t\t\t\t\tcontent: new PostSearch( {\n\t\t\t\t\t\t\t\tpost_type: 'llms_assignment',\n\t\t\t\t\t\t\t\tsearching_message: LLMS.l10n.translate( 'Search for existing assignments...' ),\n\t\t\t\t\t\t\t} ).render().$el,\n\t\t\t\t\t\tonHide: function() {\n\t\t\t\t\t\t\tBackbone.pubSub.off( 'assignment-search-select' );\n\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\t\t\t\t\tthis.post_search_popover.show();\n\t\t\t\t\tBackbone.pubSub.once( 'assignment-search-select', this.add_existing_assignment, this );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis.show_ad_popover( '#llms-existing-assignment' );\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Determine if Assignments addon is available to use.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t *\n\t\t\t * @return {Boolean}\n\t\t\t */\n\t\t\tis_addon_available: function() {\n\n\t\t\t\treturn ( window.llms_builder.assignments );\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Called when assignment is trashed.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t *\n\t\t\t * @param {Oject} assignment Assignment Model.\n\t\t\t * @return {Void}\n\t\t\t */\n\t\t\ton_trashed: function( assignment ) {\n\n\t\t\t\tthis.lesson.set( 'assignment_enabled', 'no' );\n\t\t\t\tthis.lesson.set( 'assignment', '' );\n\n\t\t\t\tdelete this.model;\n\n\t\t\t\tthis.render();\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Shows a dirty dirty ad popover for advanced assignments.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t *\n\t\t\t * @param {Sring} el The jQuery selector string.\n\t\t\t * @return {Void}\n\t\t\t */\n\t\t\tshow_ad_popover: function( el ) {\n\n\t\t\t\tvar h3 = LLMS.l10n.translate( 'Get Your Students Taking Action' ),\n\t\t\t\tp      = 'Great learning content is only half of teaching online. When your learners fully engage, they will take your content and move into action. Remove barriers for your learners by telling them what to do to apply what they just learned. Create graded assignments or simply give them a checklist of action items to complete before moving on.',\n\t\t\t\tbtn    = LLMS.l10n.translate( 'Get Assignments Now!' ),\n\t\t\t\turl    = 'https://lifterlms.com/product/lifterlms-assignments?utm_source=LifterLMS%20Plugin&utm_medium=Assignment%20Builder%20Button&utm_campaign=Assignment%20Addon%20Upsell&utm_content=3.17.0';\n\n\t\t\t\tthis.ad_popover = new Popover( {\n\t\t\t\t\tel: el,\n\t\t\t\t\targs: {\n\t\t\t\t\t\tbackdrop: true,\n\t\t\t\t\t\tcloseable: true,\n\t\t\t\t\t\tcontainer: '.wrap.lifterlms.llms-builder',\n\t\t\t\t\t\tdismissible: true,\n\t\t\t\t\t\t// placement: 'left',\n\t\t\t\t\t\twidth: 380,\n\t\t\t\t\t\ttitle: LLMS.l10n.translate( 'Unlock LifterLMS Assignments' ),\n\t\t\t\t\t\t// This is here for translation but not actually used by the popover.\n\t\t\t\t\t\tcloseLabel: LLMS.l10n.translate( 'Close' ),\n\t\t\t\t\t\tcontent: '<h3>' + h3 + '</h3><p>' + p + '</p><br><p><a class=\"llms-button-primary\" href=\"' + url + '\" target=\"_blank\">' + btn + '</a></p>'\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t\tthis.ad_popover.show();\n\n\t\t\t},\n\n\t\t}, Detachable, Editable, Trashable, Subview, SettingsFields ) );\n\n\t} );\n"
  },
  {
    "path": "assets/js/builder/Views/Course.js",
    "content": "/**\n * Single Course View\n *\n * @since    3.13.0\n * @version  3.16.0\n */\ndefine( [\n\t'Views/SectionList',\n\t'Views/_Detachable',\n\t'Views/_Editable',\n\t'Views/_Shiftable',\n\t'Views/_Trashable'\n], function(\n\t  SectionListView,\n\t  Detachable,\n\t  Editable,\n\t  Shiftable,\n\t  Trashable\n) {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * Get default attributes for the html wrapper element\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.13.0\n\t\t * @version  3.13.0\n\t\t */\n\t\tattributes: function() {\n\t\t\treturn {\n\t\t\t\t'data-id': this.model.id,\n\t\t\t};\n\t\t},\n\n\t\t/**\n\t\t * HTML element selector\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\tel: '#llms-builder-main',\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'div',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-course-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.13.0\n\t\t * @version  3.13.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tvar self = this;\n\n\t\t\t// this.listenTo( this.model, 'sync', this.render );\n\t\t\tthis.render();\n\n\t\t\tthis.sectionListView = new SectionListView( {\n\t\t\t\tcollection: this.model.get( 'sections' ),\n\t\t\t} );\n\t\t\tthis.sectionListView.render();\n\t\t\t// drag and drop start\n\t\t\tthis.sectionListView.on( 'sortStart', this.sectionListView.sortable_start );\n\t\t\t// drag and drop stop\n\t\t\tthis.sectionListView.on( 'sortStop', this.sectionListView.sortable_stop );\n\t\t\t// selection changes\n\t\t\tthis.sectionListView.on( 'selectionChanged', this.active_section_change );\n\t\t\t// \"select\" a section when it's added to the course\n\t\t\tthis.listenTo( this.model.get( 'sections' ), 'add', this.on_section_add );\n\n\t\t\tBackbone.pubSub.on( 'section-toggle', this.on_section_toggle, this );\n\n\t\t\tBackbone.pubSub.on( 'section-select', this.on_section_select, this );\n\n\t\t\tBackbone.pubSub.on( 'expand-section', this.expand_section, this );\n\n\t\t\tBackbone.pubSub.on( 'lesson-selected', this.active_lesson_change, this );\n\n\t\t\t// Select the first section by default on load.\n\t\t\tvar firstSection = this.model.get( 'sections' ).first();\n\t\t\tif ( firstSection ) {\n\t\t\t\tthis.sectionListView.setSelectedModel( firstSection );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Events\n\t\t * @type  {Object}\n\t\t * @version  7.6.0\n\t\t */\n\t\tevents: _.defaults( {\n\t\t\t'click .new-section': 'add_new_section',\n\t\t}, Detachable.events, Editable.events, Trashable.events ),\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t *\n\t\t * @return   self (for chaining)\n\t\t * @since    3.13.0\n\t\t * @version  3.13.0\n\t\t */\n\t\trender: function() {\n\t\t\tthis.$el.html( this.template( this.model ) );\n\t\t\treturn this;\n\t\t},\n\n\t\tactive_lesson_change: function( model ) {\n\n\t\t\t// set parent section to be active\n\t\t\tvar section = this.model.get( 'sections' ).get( model.get( 'parent_section' ) );\n\t\t\tthis.sectionListView.setSelectedModel( section );\n\n\t\t},\n\n\t\t/**\n\t\t * When a section \"selection\" changes in the list\n\t\t * Update each section model so we can figure out which one is selected from other views\n\t\t *\n\t\t * @param    array   current   array of selected models\n\t\t * @param    array   previous  array of previously selected models\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tactive_section_change: function( current, previous ) {\n\n\t\t\t_.each( current, function( model ) {\n\t\t\t\tmodel.set( '_selected', true, { silent: true } );\n\t\t\t} );\n\n\t\t\t_.each( previous, function( model ) {\n\t\t\t\tmodel.set( '_selected', false, { silent: true } );\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * \"Selects\" the new section when it's added to the course\n\t\t *\n\t\t * @param    obj   model  Section model that's just been added\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ton_section_add: function( model ) {\n\n\t\t\tthis.sectionListView.setSelectedModel( model );\n\n\t\t},\n\n\t\t/**\n\t\t * When expanding/collapsing sections\n\t\t * if collapsing, unselect, if expanding, select\n\t\t *\n\t\t * @param    obj   model  toggled section\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ton_section_toggle: function( model ) {\n\n\t\t\tthis.sectionListView.setSelectedModel( model );\n\n\t\t},\n\n\n\t\t/**\n\t\t * When doing things like adding a lesson, seelct the section.\n\t\t *\n\t\t * @param    obj   model  toggled section\n\t\t * @return   void\n\t\t * @since    7.6.0\n\t\t * @version  7.6.0\n\t\t */\n\t\ton_section_select: function( model ) {\n\n\t\t\tthis.sectionListView.setSelectedModel( model );\n\n\t\t},\n\n\t\tadd_new_section: function( event ) {\n\n\t\t\tevent.preventDefault();\n\t\t\tBackbone.pubSub.trigger( 'add-new-section' );\n\t\t},\n\n\n\t}, Editable ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Editor.js",
    "content": "/**\n * Sidebar Editor View\n *\n * @since    3.16.0\n * @version  3.27.0\n */\ndefine( [\n\t\t'Views/LessonEditor',\n\t\t'Views/Quiz',\n\t\t'Views/Assignment',\n\t\t'Views/_Subview'\n\t], function(\n\t\tLessonEditor,\n\t\tQuiz,\n\t\tAssignment,\n\t\tSubview\n\t) {\n\n\t\treturn Backbone.View.extend( _.defaults( {\n\n\t\t\t/**\n\t\t\t * Current view state\n\t\t\t *\n\t\t\t * @type  {String}\n\t\t\t */\n\t\t\tstate: 'lesson', // [lesson|quiz]\n\n\t\t\t/**\n\t\t\t * Current Subviews\n\t\t\t *\n\t\t\t * @type  {Object}\n\t\t\t */\n\t\t\tviews: {\n\t\t\t\tlesson: {\n\t\t\t\t\tclass: LessonEditor,\n\t\t\t\t\tinstance: null,\n\t\t\t\t\tstate: 'lesson',\n\t\t\t\t},\n\t\t\t\tassignment: {\n\t\t\t\t\tclass: Assignment,\n\t\t\t\t\tinstance: null,\n\t\t\t\t\tstate: 'assignment',\n\t\t\t\t},\n\t\t\t\tquiz: {\n\t\t\t\t\tclass: Quiz,\n\t\t\t\t\tinstance: null,\n\t\t\t\t\tstate: 'quiz',\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * HTML element selector\n\t\t\t *\n\t\t\t * @type  {String}\n\t\t\t */\n\t\t\tel: '#llms-editor',\n\n\t\t\tevents: {\n\t\t\t\t'click .llms-editor-nav a[href=\"#llms-editor-close\"]': 'close_editor',\n\t\t\t\t'click .llms-editor-nav a:not([href=\"#llms-editor-close\"])': 'switch_tab',\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Wrapper Tag name\n\t\t\t *\n\t\t\t * @type  {String}\n\t\t\t */\n\t\t\ttagName: 'div',\n\n\t\t\t/**\n\t\t\t * Get the underscore template\n\t\t\t *\n\t\t\t * @type  {[type]}\n\t\t\t */\n\t\t\ttemplate: wp.template( 'llms-editor-template' ),\n\n\t\t\t/**\n\t\t\t * Initialization callback func (renders the element on screen)\n\t\t\t *\n\t\t\t * @return   void\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\tinitialize: function( data ) {\n\n\t\t\t\tthis.SidebarView = data.SidebarView;\n\t\t\t\tif ( data.tab ) {\n\t\t\t\t\tthis.state = data.tab;\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Compiles the template and renders the view\n\t\t\t *\n\t\t\t * @return   self (for chaining)\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.16.0\n\t\t\t */\n\t\t\trender: function( view_data ) {\n\n\t\t\t\tview_data = view_data || {};\n\n\t\t\t\tthis.$el.html( this.template( this ) );\n\n\t\t\t\tthis.render_subviews( _.extend( view_data, {\n\t\t\t\t\tlesson: this.model,\n\t\t\t\t} ) );\n\n\t\t\t\treturn this;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Click event for close sidebar editor button\n\t\t\t * Sends event to main SidebarView to trigger editor closing events\n\t\t\t *\n\t\t\t * @param    obj   event  js event obj\n\t\t\t * @return   void\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.27.0\n\t\t\t */\n\t\t\tclose_editor: function( event ) {\n\n\t\t\t\tevent.preventDefault();\n\t\t\t\tBackbone.pubSub.trigger( 'sidebar-editor-close' );\n\t\t\t\twindow.location.hash = '';\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Click event for switching tabs in the editor navigation\n\t\t\t *\n\t\t\t * @param    object  event  js event object\n\t\t\t * @return   void\n\t\t\t * @since    3.16.0\n\t\t\t * @version  3.27.0\n\t\t\t */\n\t\t\tswitch_tab: function( event ) {\n\n\t\t\t\tevent.preventDefault();\n\n\t\t\t\tvar $btn = $( event.target ),\n\t\t\t\tview     = $btn.attr( 'data-view' ),\n\t\t\t\t$tab     = this.$el.find( $btn.attr( 'href' ) );\n\n\t\t\t\tthis.set_state( view ).render();\n\t\t\t\tthis.set_hash( view );\n\n\t\t\t\t// Backbone.pubSub.trigger( 'editor-tab-activated', $btn.attr( 'href' ).substring( 1 ) );\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Adds a hash for deep linking to a specific lesson tab\n\t\t\t *\n\t\t\t * @param  string  subtab subtab [quiz|assignment]\n\t\t\t * @return void\n\t\t\t * @since   3.27.0\n\t\t\t * @version 3.27.0\n\t\t\t */\n\t\t\tset_hash: function( subtab ) {\n\n\t\t\t\tvar hash = 'lesson:' + this.model.get( 'id' );\n\n\t\t\t\tif ( 'lesson' !== subtab ) {\n\t\t\t\t\thash += ':' + subtab;\n\t\t\t\t}\n\n\t\t\t\twindow.location.hash = hash;\n\n\t\t\t},\n\n\t\t}, Subview ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Elements.js",
    "content": "/**\n * Sidebar Elements View\n *\n * @since    3.16.0\n * @version  3.16.12\n */\ndefine( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'Views/Popover', 'Views/PostSearch' ], function( Section, SectionView, Lesson, LessonView, Popover, LessonSearch ) {\n\n\treturn Backbone.View.extend( {\n\n\t\t/**\n\t\t * HTML element selector\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\tel: '#llms-elements',\n\n\t\tevents: {\n\t\t\t'click #llms-new-section': 'add_new_section',\n\t\t\t'click #llms-new-lesson': 'add_new_lesson',\n\t\t\t'click #llms-existing-lesson': 'add_existing_lesson',\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'div',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-elements-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function( data ) {\n\n\t\t\t// save a reference to the main Course view\n\t\t\tthis.SidebarView = data.SidebarView;\n\n\t\t\t// watch course sections and enable/disable lesson buttons conditionally\n\t\t\tthis.listenTo( this.SidebarView.CourseView.model.get( 'sections' ), 'add', this.maybe_disable_buttons );\n\t\t\tthis.listenTo( this.SidebarView.CourseView.model.get( 'sections' ), 'remove', this.maybe_disable_buttons );\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t *\n\t\t * @return   self (for chaining)\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.html( this.template() );\n\t\t\tthis.draggable();\n\t\t\tthis.maybe_add_initial_section();\n\n\t\t\treturn this;\n\t\t},\n\n\t\tdraggable: function() {\n\n\t\t\t$( '#llms-new-section' ).draggable( {\n\t\t\t\tappendTo: '#llms-sections',\n\t\t\t\tcancel: false,\n\t\t\t\tconnectToSortable: '.llms-sections',\n\t\t\t\thelper: function() {\n\t\t\t\t\treturn new SectionView( { model: new Section() } ).render().$el;\n\t\t\t\t},\n\t\t\t\tstart: function() {\n\t\t\t\t\t$( '.llms-sections' ).addClass( 'dragging' );\n\t\t\t\t},\n\t\t\t\tstop: function() {\n\t\t\t\t\t$( '.llms-sections' ).removeClass( 'dragging' );\n\t\t\t\t},\n\t\t\t} );\n\n\t\t\t$( '#llms-new-lesson' ).draggable( {\n\t\t\t\t// appendTo: '#llms-sections .llms-section:first-child .llms-lessons',\n\t\t\t\tappendTo: '#llms-sections',\n\t\t\t\tcancel: false,\n\t\t\t\tconnectToSortable: '.llms-lessons',\n\t\t\t\thelper: function() {\n\t\t\t\t\treturn new LessonView( { model: new Lesson() } ).render().$el;\n\t\t\t\t},\n\t\t\t\tstart: function() {\n\n\t\t\t\t\t$( '.llms-lessons' ).addClass( 'dragging' );\n\n\t\t\t\t},\n\t\t\t\tstop: function() {\n\t\t\t\t\t$( '.llms-lessons' ).removeClass( 'dragging' );\n\t\t\t\t\t$( '.drag-expanded' ).removeClass( '.drag-expanded' );\n\t\t\t\t},\n\t\t\t} );\n\n\t\t},\n\n\t\tadd_new_section: function( event ) {\n\n\t\t\tevent.preventDefault();\n\t\t\tBackbone.pubSub.trigger( 'add-new-section' );\n\t\t},\n\n\t\tadd_new_lesson: function( event ) {\n\t\t\tevent.preventDefault();\n\t\t\tBackbone.pubSub.trigger( 'add-new-lesson' );\n\t\t},\n\n\t\t/**\n\t\t * Show the popover to add an existing lessons\n\t\t *\n\t\t * @param    object   event  JS Event Object\n\t\t * @return   void\n\t\t * @since    3.16.12\n\t\t * @version  3.16.12\n\t\t */\n\t\tadd_existing_lesson: function( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tvar pop, onLessonSelect;\n\n\t\t\tpop = new Popover( {\n\t\t\t\tel: '#llms-existing-lesson',\n\t\t\t\targs: {\n\t\t\t\t\tbackdrop: true,\n\t\t\t\t\tcloseable: true,\n\t\t\t\t\tcontainer: '.wrap.lifterlms.llms-builder',\n\t\t\t\t\tdismissible: true,\n\t\t\t\t\tplacement: 'left',\n\t\t\t\t\twidth: 480,\n\t\t\t\t\ttitle: LLMS.l10n.translate( 'Add Existing Lesson' ),\n\t\t\t\t\tcontent: new LessonSearch( {\n\t\t\t\t\t\tpost_type: 'lesson',\n\t\t\t\t\t\tsearching_message: LLMS.l10n.translate( 'Search for existing lessons...' ),\n\t\t\t\t\t} ).render().$el,\n\t\t\t\t\tonHide: function() {\n\t\t\t\t\t\tBackbone.pubSub.off( 'lesson-search-select', onLessonSelect );\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tonLessonSelect = function() {\n\t\t\t\tpop.hide();\n\n\t\t\t\t// Ref #3097 — pop.hide() doesn't always remove the DOM elements.\n\t\t\t\t$( '.webui-popover' ).remove();\n\t\t\t\t$( '.webui-popover-backdrop' ).remove();\n\t\t\t};\n\n\t\t\tpop.show();\n\t\t\tBackbone.pubSub.once( 'lesson-search-select', onLessonSelect );\n\n\t\t},\n\n\t\t/**\n\t\t * Disables lesson add buttons if no sections are available to add a lesson to\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tmaybe_add_initial_section: function() {\n\n\t\t\tvar $els = $( '#llms-new-lesson, #llms-existing-lesson' );\n\n\t\t\tif ( ! this.SidebarView.CourseView.model.get( 'sections' ).length ) {\n\t\t\t\tBackbone.pubSub.trigger( 'add-new-section' );\n\t\t\t\tBackbone.pubSub.trigger( 'add-new-lesson' );\n\t\t\t\tBackbone.pubSub.trigger( 'add-new-lesson' );\n\t\t\t\tBackbone.pubSub.trigger( 'add-new-lesson' );\n\t\t\t} else {\n\t\t\t\t$els.removeAttr( 'disabled' );\n\t\t\t}\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/FormattingToolbar.js",
    "content": "/**\n * Sidebar Elements View\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn Backbone.View.extend( {\n\n\t\tclassName: 'llms-input-formatting',\n\n\t\tevents: {\n\t\t\t'mousedown a[href=\"#llms-formatting\"]': 'on_click',\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'div',\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function( data ) {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.$input = data.$input;\n\t\t\tthis.tags   = data.tags;\n\n\t\t\tthis.$input.on( 'keyup focus click', function() {\n\n\t\t\t\t_.each( self.tags, function( tag ) {\n\n\t\t\t\t\tvar name = self._get_formatting_name( tag );\n\n\t\t\t\t\tif ( document.queryCommandState( name ) ) {\n\t\t\t\t\t\tself.toggle_button_state( name, 'on' );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tself.toggle_button_state( name, 'off' );\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t *\n\t\t * @return   self (for chaining)\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.html( this.template() );\n\t\t\treturn this;\n\n\t\t},\n\n\t\ttemplate: function() {\n\n\t\t\tvar self     = this,\n\t\t\t\t$toolbar = $( '<div />' );\n\n\t\t\t_.each( this.tags, function( tag ) {\n\n\t\t\t\t$toolbar.append( self._get_formatting_icon( tag ) );\n\n\t\t\t} );\n\n\t\t\treturn $toolbar.html();\n\n\t\t},\n\n\t\ton_click: function( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tvar $btn      = $( event.target ),\n\t\t\t\tselection = window.getSelection(),\n\t\t\t\tcommands  = [ 'bold', 'italic', 'underline' ],\n\t\t\t\trange, cmd;\n\n\t\t\tif ( $btn.hasClass( 'fa' ) ) {\n\t\t\t\t$btn = $btn.closest( 'a' );\n\t\t\t}\n\n\t\t\tcmd = $btn.attr( 'data-cmd' );\n\n\t\t\tif ( -1 === commands.indexOf( cmd ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$btn.addClass( 'active' );\n\t\t\tdocument.execCommand( cmd );\n\n\t\t},\n\n\t\ttoggle_button_state: function( tag_name, state ) {\n\n\t\t\tvar $btn = this.$el.find( 'a[data-cmd=\"' + tag_name + '\"]' ),\n\t\t\t\tdel  = 'on' === state ? '' : 'active',\n\t\t\t\tadd  = 'on' === state ? 'active' : '';\n\n\t\t\t$btn.removeClass( del ).addClass( add );\n\n\t\t},\n\n\t\t_get_formatting_name: function( tag ) {\n\n\t\t\tvar tags = {\n\t\t\t\tb: 'bold',\n\t\t\t\ti: 'italic',\n\t\t\t\tu: 'underline',\n\t\t\t};\n\n\t\t\treturn tags[ tag ];\n\n\t\t},\n\n\t\t_get_formatting_icon: function( tag ) {\n\n\t\t\tvar name = this._get_formatting_name( tag );\n\n\t\t\treturn '<a class=\"llms-action-icon\" data-cmd=\"' + name + '\" href=\"#llms-formatting\"><i class=\"fa fa-' + name + '\" aria-hidden=\"true\"></i></a>';\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Lesson.js",
    "content": "/**\n * Single Lesson View\n * @since    3.16.0\n * @version  3.27.0\n */\ndefine( [\n\t\t'Views/_Detachable',\n\t\t'Views/_Editable',\n\t\t'Views/_Shiftable',\n\t\t'Views/_Trashable'\n\t], function(\n\t\tDetachable,\n\t\tEditable,\n\t\tShiftable,\n\t\tTrashable\n\t) {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * Get default attributes for the html wrapper element\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tattributes: function() {\n\t\t\treturn {\n\t\t\t\t'data-id': this.model.id,\n\t\t\t\t'data-section-id': this.model.get( 'parent_section' ),\n\t\t\t};\n\t\t},\n\n\t\t/**\n\t\t * HTML class names\n\t\t * @type  {String}\n\t\t */\n\t\tclassName: 'llms-builder-item llms-lesson',\n\n\t\t/**\n\t\t * Events\n\t\t * @type  {Object}\n\t\t * @since    3.16.0\n\t\t * @version  3.16.12\n\t\t */\n\t\tevents: _.defaults( {\n\t\t\t'click .edit-lesson': 'edit_lesson',\n\t\t\t'click .llms-headline': 'edit_lesson',\n\t\t\t'click .edit-quiz': 'open_quiz_editor',\n\t\t\t'click .edit-assignment': 'open_assignment_editor',\n\t\t\t'click .section-prev': 'section_prev',\n\t\t\t'click .section-next': 'section_next',\n\t\t\t'click .shift-up--lesson': 'shift_up',\n\t\t\t'click .shift-down--lesson': 'shift_down',\n\t\t}, Detachable.events, Editable.events, Trashable.events ),\n\n\t\t/**\n\t\t * HTML element wrapper ID attribute\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tid: function() {\n\t\t\treturn 'llms-lesson-' + this.model.id;\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'li',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-lesson-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t * @return   void\n\t\t * @since    3.14.1\n\t\t * @version  3.14.1\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.render();\n\n\t\t\tthis.listenTo( this.model, 'change', this.render );\n\n\t\t\tBackbone.pubSub.on(  'lesson-selected', this.on_select, this );\n\t\t\tBackbone.pubSub.on(  'new-lesson-added', this.maybe_open_editor, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t * @return   self (for chaining)\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.html( this.template( this.model ) );\n\t\t\tthis.maybe_hide_shiftable_buttons();\n\t\t\tif ( this.model.get( '_selected' ) ) {\n\t\t\t\tthis.$el.addClass( 'selected' );\n\t\t\t} else {\n\t\t\t\tthis.$el.removeClass( 'selected' );\n\t\t\t}\n\t\t\treturn this;\n\n\t\t},\n\n\t\t/**\n\t\t * Click event for the assignment editor action icon\n\t\t * Opens sidebar to the assignment editor tab\n\t\t * @param    obj event JS Event obj.\n\t\t * @return   void\n\t\t * @since    3.17.0\n\t\t * @version  3.27.0\n\t\t */\n\t\topen_assignment_editor: function( event ) {\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\n\t\t\tBackbone.pubSub.trigger( 'lesson-selected', this.model, 'assignment' );\n\t\t\tthis.model.set( '_selected', true );\n\t\t\tthis.set_hash( 'assignment' );\n\n\t\t},\n\n\t\t/**\n\t\t * Click event for lesson settings action icon\n\t\t * Opens sidebar to the lesson editor tab\n\t\t * @param    obj event JS Event obj.\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.27.0\n\t\t */\n\t\tedit_lesson: function( event ) {\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\n\t\t\tthis.open_lesson_editor();\n\t\t},\n\n\t\topen_lesson_editor: function() {\n\t\t\tBackbone.pubSub.trigger( 'lesson-selected', this.model, 'lesson' );\n\t\t\tthis.model.set( '_selected', true );\n\t\t\tthis.set_hash( false );\n\t\t},\n\n\t\tmaybe_open_editor: function( model ) {\n\t\t\tif ( this.model.id === model.id ) {\n\t\t\t\tthis.open_lesson_editor();\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Click event for the quiz editor action icon\n\t\t * Opens sidebar to the quiz editor tab\n\t\t * @param    obj event JS Event obj.\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.27.0\n\t\t */\n\t\topen_quiz_editor: function( event ) {\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\n\t\t\tBackbone.pubSub.trigger( 'lesson-selected', this.model, 'quiz' );\n\t\t\tthis.model.set( '_selected', true );\n\t\t\tthis.set_hash( 'quiz' );\n\n\t\t},\n\n\t\t/**\n\t\t * When a lesson is selected mark it as selected in the hidden prop\n\t\t * Allows views to re-render and reflect current state properly\n\t\t * @param    obj   model  lesson model that's been selected\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ton_select: function( model ) {\n\n\t\t\tif ( this.model.id !== model.id ) {\n\t\t\t\tthis.model.set( '_selected', false );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Click event for the \"Next Section\" button\n\t\t * @param    obj   event   js event obj\n\t\t * @return   void\n\t\t * @since    3.16.11\n\t\t * @version  3.16.11\n\t\t */\n\t\tsection_next: function( event ) {\n\t\t\tevent.preventDefault();\n\t\t\tthis._move_to_section( 'next' );\n\t\t},\n\n\t\t/**\n\t\t * Click event for the \"Previous Section\" button\n\t\t * @param    obj   event   js event obj\n\t\t * @return   void\n\t\t * @since    3.16.11\n\t\t * @version  3.16.11\n\t\t */\n\t\tsection_prev: function( event ) {\n\t\t\tevent.preventDefault();\n\t\t\tthis._move_to_section( 'prev' );\n\t\t},\n\n\t\t/**\n\t\t * Adds a hash for deep linking to a specific lesson tab\n\t\t * @param  string  subtab subtab [quiz|assignment]\n\t\t * @return void\n\t\t * @since   3.27.0\n\t\t * @version 3.27.0\n\t\t */\n\t\tset_hash: function( subtab ) {\n\n\t\t\tvar hash = 'lesson:' + this.model.get( 'id' );\n\n\t\t\tif ( subtab ) {\n\t\t\t\thash += ':' + subtab;\n\t\t\t}\n\n\t\t\twindow.location.hash = hash;\n\n\t\t},\n\n\t\t/**\n\t\t * Move the lesson into a new section\n\t\t * @param    string   direction  direction [prev|next]\n\t\t * @return   void\n\t\t * @since    3.16.11\n\t\t * @version  3.16.11\n\t\t */\n\t\t_move_to_section: function( direction ) {\n\n\t\t\tvar from_coll = this.model.collection,\n\t\t\t\tto_section;\n\n\t\t\tif ( 'next' === direction ) {\n\t\t\t\tto_section = from_coll.parent.get_next();\n\t\t\t} else if ( 'prev' === direction ) {\n\t\t\t\tto_section = from_coll.parent.get_prev();\n\t\t\t}\n\n\t\t\tif ( to_section ) {\n\n\t\t\t\tfrom_coll.remove( this.model );\n\t\t\t\tto_section.add_lesson( this.model );\n\t\t\t\tto_section.set( '_expanded', true );\n\n\t\t\t}\n\n\t\t},\n\n\t}, Detachable, Editable, Shiftable, Trashable ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/LessonEditor.js",
    "content": "/**\n * Lesson Editor (Sidebar) View\n *\n * @package LifterLMS/Scripts/Builder\n *\n * @since 3.17.0\n * @since 3.35.2 Added filter `llms_lesson_rerender_change_events` to view re-render change events.\n * @version 3.35.2\n */\ndefine( [\n\t\t'Views/_Detachable',\n\t\t'Views/_Editable',\n\t\t'Views/_Trashable',\n\t\t'Views/_Subview',\n\t\t'Views/SettingsFields'\n\t], function(\n\t\tDetachable,\n\t\tEditable,\n\t\tTrashable,\n\t\tSubview,\n\t\tSettingsFields\n\t) {\n\n\t\treturn Backbone.View.extend( _.defaults( {\n\n\t\t\t/**\n\t\t\t * Current view state\n\t\t\t *\n\t\t\t * @type  {String}\n\t\t\t */\n\t\t\tstate: 'default',\n\n\t\t\t/**\n\t\t\t * Current Subviews\n\t\t\t *\n\t\t\t * @type  {Object}\n\t\t\t */\n\t\t\tviews: {\n\t\t\t\tsettings: {\n\t\t\t\t\tclass: SettingsFields,\n\t\t\t\t\tinstance: null,\n\t\t\t\t\tstate: 'default',\n\t\t\t\t},\n\t\t\t},\n\n\t\t\tel: '#llms-editor-lesson',\n\n\t\t\t/**\n\t\t\t * Events\n\t\t\t *\n\t\t\t * @type  {Object}\n\t\t\t */\n\t\t\tevents: _.defaults( {}, Detachable.events, Editable.events, Trashable.events ),\n\n\t\t\t/**\n\t\t\t * Template function\n\t\t\t *\n\t\t\t * @type  {[type]}\n\t\t\t */\n\t\t\ttemplate: wp.template( 'llms-lesson-settings-template' ),\n\n\t\t\t/**\n\t\t\t * Init\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t * @since 3.24.0 Unknown\n\t\t\t * @since 3.35.2 Added filter to change events.\n\t\t\t *\n\t\t\t * @param {obj} data Parent template data.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tinitialize: function( data ) {\n\n\t\t\t\tthis.model = data.lesson;\n\n\t\t\t\tvar change_events = window.llms.hooks.applyFilters( 'llms_lesson_rerender_change_events', [\n\t\t\t\t\t'change:date_available',\n\t\t\t\t\t'change:drip_method',\n\t\t\t\t\t'change:permalink',\n\t\t\t\t\t'change:content_added_in_builder',\n\t\t\t\t\t'change:name',\n\t\t\t\t\t'change:time_available',\n\t\t\t\t] );\n\t\t\t\t_.each( change_events, function( event ) {\n\t\t\t\t\tthis.listenTo( this.model, event, this.render );\n\t\t\t\t}, this );\n\n\t\t\t\t// render only the tooltip for points percentage when points change\n\t\t\t\tthis.listenTo( this.model, 'change:points', this.render_points_percentage );\n\n\t\t\t\t// when the \"has_prerequisite\" attr is toggled ON\n\t\t\t\t// trigger the prereq select object to set the default (first available) prereq for the lesson\n\t\t\t\tthis.listenTo( this.model, 'change:has_prerequisite', function( lesson, val ) {\n\t\t\t\t\tif ( 'yes' === val ) {\n\t\t\t\t\t\tthis.$el.find( 'select[name=\"prerequisite\"]' ).trigger( 'change' );\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Render the view\n\t\t\t *\n\t\t\t * @return   obj\n\t\t\t * @since    3.17.0\n\t\t\t * @version  3.24.0\n\t\t\t */\n\t\t\trender: function() {\n\n\t\t\t\tthis.$el.html( this.template( this.model ) );\n\n\t\t\t\tthis.remove_subview( 'settings' );\n\n\t\t\t\tthis.render_subview( 'settings', {\n\t\t\t\t\tel: '#llms-lesson-settings-fields',\n\t\t\t\t\tmodel: this.model,\n\t\t\t\t} );\n\n\t\t\t\tthis.init_datepickers();\n\t\t\t\tthis.init_selects();\n\n\t\t\t\tthis.render_points_percentage();\n\n\t\t\t\tthis.$('.llms-editable-title').focus();\n\n\t\t\t\treturn this;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Render the portion of the template which displays the points percentage\n\t\t\t *\n\t\t\t * @return   void\n\t\t\t * @since    3.24.0\n\t\t\t * @version  3.24.0\n\t\t\t */\n\t\t\trender_points_percentage: function() {\n\t\t\t\tthis.$el.find( '#llms-model-settings-field--points .llms-editable-input' )\n\t\t\t\t.addClass( 'tip--top-left' )\n\t\t\t\t.attr( 'data-tip', this.model.get_points_percentage() );\n\t\t\t}\n\n\t\t}, Detachable, Editable, Trashable, Subview, SettingsFields ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/LessonList.js",
    "content": "/**\n * Single Section View\n *\n * @since    3.13.0\n * @version  3.16.0\n */\ndefine( [ 'Views/Lesson', 'Views/_Receivable' ], function( LessonView, Receivable ) {\n\n\treturn Backbone.CollectionView.extend( _.defaults( {\n\n\t\tclassName: 'llms-lessons',\n\n\t\t/**\n\t\t * Section model\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\tmodelView: LessonView,\n\n\t\t/**\n\t\t * Are sections selectable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tselectable: false,\n\n\t\t/**\n\t\t * Are sections sortable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tsortable: true,\n\n\t\tsortableOptions: {\n\t\t\taxis: false,\n\t\t\tconnectWith: '.llms-lessons',\n\t\t\tcursor: 'move',\n\t\t\thandle: '.drag-lesson',\n\t\t\titems: '.llms-lesson',\n\t\t\tplaceholder: 'llms-lesson llms-sortable-placeholder',\n\t\t},\n\n\t\tsortable_start: function( collection ) {\n\t\t\t$( '.llms-lessons' ).addClass( 'dragging' );\n\t\t},\n\n\t\tsortable_stop: function( collection ) {\n\t\t\t$( '.llms-lessons' ).removeClass( 'dragging' );\n\t\t},\n\n\t\t/**\n\t\t * Overloads the function from Backbone.CollectionView core because it doesn't send stop events\n\t\t * if moving from one sortable to another... :-(\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @param    obj   ui     jQuery UI object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_sortStop : function( event, ui ) {\n\n\t\t\tvar modelBeingSorted     = this.collection.get( ui.item.attr( 'data-model-cid' ) ),\n\t\t\t\tmodelViewContainerEl = this._getContainerEl(),\n\t\t\t\tnewIndex             = modelViewContainerEl.children().index( ui.item );\n\n\t\t\tif ( newIndex == -1 && modelBeingSorted ) {\n\t\t\t\tthis.collection.remove( modelBeingSorted );\n\t\t\t}\n\n\t\t\tthis._reorderCollectionBasedOnHTML();\n\t\t\tthis.updateDependentControls();\n\n\t\t\tif ( this._isBackboneCourierAvailable() ) {\n\t\t\t\tthis.spawn( 'sortStop', { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );\n\t\t\t} else {\n\t\t\t\tthis.trigger( 'sortStop', modelBeingSorted, newIndex );\n\t\t\t}\n\n\t\t},\n\n\t}, Receivable ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Popover.js",
    "content": "/**\n * Popover View\n *\n * @since 3.16.0\n * @version 4.0.0\n */\ndefine( [], function() {\n\n\treturn Backbone.View.extend( {\n\n\t\t/**\n\t\t * Default Properties\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tdefaults: {\n\t\t\tplacement: 'auto',\n\t\t\t// container: document.body,\n\t\t\twidth: 'auto',\n\t\t\ttrigger: 'manual',\n\t\t\tstyle: 'light',\n\t\t\tanimation: 'pop',\n\t\t\ttitle: '',\n\t\t\tcontent: '',\n\t\t\tcloseable: false,\n\t\t\tbackdrop: false,\n\t\t\tonShow: function( $el ) {},\n\t\t\tonHide: function( $el ) {},\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t *\n\t\t * @type {String}\n\t\t */\n\t\ttagName: 'div',\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t *\n\t\t * @since 3.14.1\n\t\t * @since 4.0.0 Add RTL support for popovers.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tinitialize: function( data ) {\n\n\t\t\tif ( this.$el.length ) {\n\t\t\t\tthis.defaults.container = this.$el.parent();\n\t\t\t}\n\n\t\t\tthis.args = _.defaults( data.args, this.defaults );\n\n\t\t\t// Reverse directions for RTL sites.\n\t\t\tif ( $( 'body' ).hasClass( 'rtl' ) ) {\n\n\t\t\t\tif ( -1 !== this.args.placement.indexOf( 'left' ) ) {\n\t\t\t\t\tthis.args.placement = this.args.placement.replace( 'left', 'right' );\n\t\t\t\t} else if ( -1 !== this.args.placement.indexOf( 'right' ) ) {\n\t\t\t\t\tthis.args.placement = this.args.placement.replace( 'right', 'left' );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tthis.render();\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Object} Instance of the Backbone.view.\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.webuiPopover( this.args );\n\t\t\treturn this;\n\n\t\t},\n\n\t\t/**\n\t\t * Hide the popover\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.16.12 Unknown.\n\t\t *\n\t\t * @return {Object} Instance of the Backbone.view.\n\t\t */\n\t\thide: function() {\n\n\t\t\tthis.$el.webuiPopover( 'hide' );\n\t\t\treturn this;\n\n\t\t},\n\n\t\t/**\n\t\t * Show the popover\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.16.12 Unknown.\n\t\t *\n\t\t * @return {Object} Instance of the Backbone.view.\n\t\t */\n\t\tshow: function() {\n\n\t\t\tthis.$el.webuiPopover( 'show' );\n\t\t\treturn this;\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/PostSearch.js",
    "content": "/**\n * Post Popover Search content View\n *\n * @since 3.16.0\n * @version 4.4.0\n */\ndefine( [], function() {\n\n\treturn Backbone.View.extend( {\n\n\t\t/**\n\t\t * DOM Events\n\t\t *\n\t\t * @type     obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tevents: {\n\t\t\t'select2:select': 'add_post',\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'select',\n\n\t\t/**\n\t\t * Initializer\n\t\t *\n\t\t * @param    obj   data  customize the search box with data\n\t\t * @return   void\n\t\t * @since    3.16.12\n\t\t * @version  3.16.12\n\t\t */\n\t\tinitialize: function( data ) {\n\n\t\t\tthis.post_type         = data.post_type;\n\t\t\tthis.searching_message = data.searching_message || LLMS.l10n.translate( 'Searching...' );\n\n\t\t},\n\n\t\t/**\n\t\t * Select event, adds the existing lesson to the course\n\t\t *\n\t\t * @param    obj   event  select2:select event object\n\t\t * @since    3.16.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tadd_post: function( event ) {\n\n\t\t\tvar type = this.$el.attr( 'data-post-type' );\n\n\t\t\tBackbone.pubSub.trigger( type.replace( 'llms_', '' ) + '-search-select', event.params.data, event );\n\t\t\tthis.$el.val( null ).trigger( 'change' );\n\n\t\t},\n\n\t\t/**\n\t\t * Render the section\n\t\t *\n\t\t * Initializes a new collection and views for all lessons in the section.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.16.12 Unknown.\n\t\t * @since 4.4.0 Update ajax nonce source.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\trender: function() {\n\t\t\tvar self = this;\n\t\t\tsetTimeout( function () {\n\t\t\t\tself.$el.llmsSelect2( {\n\t\t\t\t\tajax: {\n\t\t\t\t\t\tdataType: 'JSON',\n\t\t\t\t\t\tdelay: 250,\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\turl: window.ajaxurl,\n\t\t\t\t\t\tdata: function( params ) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\taction: 'llms_builder',\n\t\t\t\t\t\t\t\taction_type: 'search',\n\t\t\t\t\t\t\t\tcourse_id: window.llms_builder.course.id,\n\t\t\t\t\t\t\t\tpost_type: self.post_type,\n\t\t\t\t\t\t\t\tterm: params.term,\n\t\t\t\t\t\t\t\tpage: params.page,\n\t\t\t\t\t\t\t\t_ajax_nonce: window.llms.ajax_nonce,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tdropdownParent: $( '.wrap.lifterlms.llms-builder' ),\n\t\t\t\t\t// Don't escape html from render_result.\n\t\t\t\t\tescapeMarkup: function( markup ) {\n\t\t\t\t\t\treturn markup;\n\t\t\t\t\t},\n\t\t\t\t\tplaceholder: self.searching_message,\n\t\t\t\t\ttemplateResult: self.render_result,\n\t\t\t\t\twidth: '100%',\n\t\t\t\t} );\n\t\t\t\tself.$el.attr( 'data-post-type', self.post_type );\n\t\t\t}, 0 );\n\t\t\treturn this;\n\n\t\t},\n\n\t\t/**\n\t\t * Render a nicer UI for each search result in the in the Select2 search results\n\t\t *\n\t\t * @param    object   res  result data\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.16.12\n\t\t */\n\t\trender_result: function( res ) {\n\n\t\t\tvar $html = $( '<div class=\"llms-existing-lesson-result\" />' );\n\n\t\t\tif ( res.loading ) {\n\t\t\t\treturn $html.append( res.text );\n\t\t\t}\n\n\t\t\tvar $side = $( '<aside class=\"llms-existing-action\" />' ),\n\t\t\t\t$main = $( '<div class=\"llms-existing-info\" />' );\n\t\t\t\ticon  = ( 'attach' === res.action ) ? 'paperclip' : 'clone',\n\t\t\t\ttext  = ( 'attach' === res.action ) ? LLMS.l10n.translate( 'Attach' ) : LLMS.l10n.translate( 'Clone' );\n\n\t\t\t$side.append( '<i class=\"fa fa-' + icon + '\" aria-hidden=\"true\"></i><small>' + text + '</small>' );\n\n\t\t\t$main.append( '<h4>' + res.data.title + '</h4>' );\n\t\t\t$main.append( '<h5>' + LLMS.l10n.translate( 'ID' ) + ': <em>' + res.data.id + '</em></h5>' );\n\n\t\t\t_.each( res.parents, function( parent ) {\n\t\t\t\t$main.append( '<h5>' + parent + '</em></h5>' );\n\t\t\t} );\n\n\t\t\treturn $html.append( $side ).append( $main );\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Question.js",
    "content": "/**\n * Single Question View.\n *\n * @since 3.16.0\n * @version 7.8.0\n */\ndefine( [\n\t\t'Views/_Detachable',\n\t\t'Views/_Editable',\n\t\t'Views/QuestionChoiceList'\n\t], function(\n\t\tDetachable,\n\t\tEditable,\n\t\tChoiceListView\n\t) {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * Generate CSS classes for the question\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tclassName: function() {\n\t\t\treturn 'llms-question qtype--' + this.model.get( 'question_type' ).get( 'id' );\n\t\t},\n\n\t\tevents: _.defaults( {\n\t\t\t'click .clone--question': 'clone',\n\t\t\t'click .delete--question': 'delete',\n\t\t\t'click .expand--question': 'expand',\n\t\t\t'click .collapse--question': 'collapse',\n\t\t\t'change input[name=\"question_points\"]': 'update_points',\n\t\t}, Detachable.events, Editable.events ),\n\n\t\t/**\n\t\t * HTML element wrapper ID attribute\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tid: function() {\n\t\t\treturn 'llms-question-' + this.model.id;\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'li',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-question-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tvar change_events = [\n\t\t\t\t'change:_expanded',\n\t\t\t\t'change:menu_order',\n\t\t\t];\n\t\t\t_.each( change_events, function( event ) {\n\t\t\t\tthis.listenTo( this.model, event, this.render );\n\t\t\t}, this );\n\n\t\t\tthis.listenTo( this.model.get( 'image' ), 'change', this.render );\n\n\t\t\tthis.listenTo( this.model.get_parent(), 'change:_points', this.render_points_percentage );\n\n\t\t\tthis.on( 'multi_choices_toggle', this.multi_choices_toggle, this );\n\n\t\t\tBackbone.pubSub.on( 'del-question-choice', this.del_choice, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 7.8.0 Added support for image upload in tinyMCE editor.\n\t\t *\n\t\t * @return self (for chaining)\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.html( this.template( this.model ) );\n\n\t\t\tif ( this.model.get( 'question_type').get( 'choices' ) ) {\n\n\t\t\t\tthis.choiceListView = new ChoiceListView( {\n\t\t\t\t\tel: this.$el.find( '.llms-question-choices' ),\n\t\t\t\t\tcollection: this.model.get( 'choices' ),\n\t\t\t\t} );\n\t\t\t\tthis.choiceListView.render();\n\t\t\t\tthis.choiceListView.on( 'sortStart', this.choiceListView.sortable_start );\n\t\t\t\tthis.choiceListView.on( 'sortStop', this.choiceListView.sortable_stop );\n\n\t\t\t}\n\n\t\t\tif ( 'group' === this.model.get( 'question_type' ).get( 'id' ) ) {\n\n\t\t\t\tvar self = this;\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\tself.questionListView = self.collectionListView.quiz.get_question_list( {\n\t\t\t\t\t\tel: self.$el.find( '.llms-quiz-questions' ),\n\t\t\t\t\t\tcollection: self.model.get( 'questions' ),\n\t\t\t\t\t} );\n\t\t\t\t\tself.questionListView.render();\n\t\t\t\t\tself.questionListView.on( 'sortStart', self.questionListView.sortable_start );\n\t\t\t\t\tself.questionListView.on( 'sortStop', self.questionListView.sortable_stop );\n\t\t\t\t}, 1 );\n\n\t\t\t}\n\n\t\t\tif ( this.model.get( 'description_enabled' ) ) {\n\t\t\t\tthis.init_editor( 'question-desc--' + this.model.get( 'id' ) );\n\t\t\t}\n\n\t\t\tif ( this.model.get( 'clarifications_enabled' ) ) {\n\t\t\t\tthis.init_editor( 'question-clarifications--' + this.model.get( 'id' ), {\n\t\t\t\t\tmediaButtons: true,\n\t\t\t\t\ttinymce: {\n\t\t\t\t\t\ttoolbar1: 'bold,italic,strikethrough,bullist,numlist,alignleft,aligncenter,alignright',\n\t\t\t\t\t\ttoolbar2: '',\n\t\t\t\t\t\tsetup: _.bind( this.on_editor_ready, this ),\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\tthis.init_formatting_els();\n\t\t\tthis.init_selects();\n\n\t\t\treturn this;\n\t\t},\n\n\t\t/**\n\t\t * rerender points percentage when question points are updated\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender_points_percentage: function() {\n\n\t\t\tthis.$el.find( '.llms-question-points' ).attr( 'data-tip', this.model.get_points_percentage() );\n\n\t\t},\n\n\t\t/**\n\t\t * Click event to duplicate a question within a quiz\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tclone: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\t\t\tevent.preventDefault();\n\t\t\tthis.model.collection.add( this._get_question_clone( this.model ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Recursive clone function which will correctly clone children of a question\n\t\t * @param    obj   question  question model\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_get_question_clone: function( question ) {\n\n\t\t\t// create a duplicate\n\t\t\tvar clone = _.clone( question.attributes );\n\n\t\t\t// remove id (we want the duplicate to have a temp id)\n\t\t\tdelete clone.id;\n\n\t\t\tclone.parent_id = question.get( 'id' );\n\n\t\t\t// set the question type ID\n\t\t\tclone.question_type = question.get( 'question_type' ).get( 'id' );\n\n\t\t\t// clone the image attributes separately\n\t\t\tclone.image = _.clone( question.get( 'image' ).attributes );\n\n\t\t\t// if it has choices clone all the choices\n\t\t\tif ( question.get( 'choices' ) ) {\n\n\t\t\t\tclone.choices = [];\n\n\t\t\t\tquestion.get( 'choices' ).each( function ( choice ) {\n\n\t\t\t\t\tvar choice_clone = _.clone( choice.attributes );\n\t\t\t\t\tdelete choice_clone.id;\n\t\t\t\t\tdelete choice_clone.question_id;\n\n\t\t\t\t\tclone.choices.push( choice_clone );\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\tif ( 'group' === question.get( 'question_type' ).get( 'id' ) ) {\n\n\t\t\t\tclone.questions = [];\n\t\t\t\tquestion.get( 'questions' ).each( function( child ) {\n\t\t\t\t\tclone.questions.push( this._get_question_clone( child ) );\n\t\t\t\t}, this );\n\n\t\t\t}\n\n\t\t\treturn clone;\n\n\t\t},\n\n\t\t/**\n\t\t * Collapse a question and hide it's settings\n\t\t * @param obj event js event obj.\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.27.0\n\t\t */\n\t\tcollapse: function( event ) {\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\n\t\t\tthis.model.set( '_expanded', false );\n\n\t\t},\n\n\t\t/**\n\t\t * Delete the question from a quiz / question group\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tdelete: function( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tif ( window.confirm( LLMS.l10n.translate( 'Are you sure you want to delete this question?' ) ) ) {\n\n\t\t\t\tthis.model.collection.remove( this.model );\n\t\t\t\tBackbone.pubSub.trigger( 'model-trashed', this.model );\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Click event to reveal a question's settings & choices\n\t\t * @param obj event js event obj.\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.27.0\n\t\t */\n\t\texpand: function( event ) {\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\n\t\t\tthis.model.set( '_expanded', true );\n\n\t\t},\n\n\t\t/**\n\t\t * When toggling multiple correct answers *off* remove all correct choices except the first correct choice in the list\n\t\t * @param    string   val  value of the question's `multi_choice` attr [yes|no]\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tmulti_choices_toggle: function( val ) {\n\n\t\t\tif ( 'yes' === val ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.model.get( 'choices' ).update_correct( _.first( this.model.get( 'choices' ).get_correct() ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Update the model's points when the value of the points input is updated\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_points: function() {\n\n\t\t\tthis.model.set( 'points', this.$el.find( 'input[name=\"question_points\"]' ).val() * 1 );\n\n\t\t}\n\n\t}, Detachable, Editable ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/QuestionBank.js",
    "content": "/**\n * Quiz question bank view\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Views/QuestionType' ], function( QuestionView ) {\n\n\treturn Backbone.CollectionView.extend( {\n\n\t\tclassName: 'llms-question',\n\n\t\t/**\n\t\t * Parent element\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\tel: '#llms-question-bank',\n\n\t\t/**\n\t\t * Section model\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\tmodelView: QuestionView,\n\n\t\t/**\n\t\t * Are sections selectable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tselectable: false,\n\n\t\t/**\n\t\t * Are sections sortable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tsortable: false,\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/QuestionChoice.js",
    "content": "/**\n * Single Question Choice View\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Views/_Editable', ], function( Editable ) {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * HTML class names\n\t\t * @type  {String}\n\t\t */\n\t\tclassName: 'llms-question-choice',\n\n\t\tevents: _.defaults( {\n\t\t\t'change input[name=\"correct\"]': 'toggle_correct',\n\t\t\t'click .llms-action-icon[href=\"#llms-add-choice\"]': 'add_choice',\n\t\t\t'click .llms-action-icon[href=\"#llms-del-choice\"]': 'del_choice',\n\t\t}, Editable.events ),\n\n\t\t/**\n\t\t * HTML element wrapper ID attribute\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tid: function() {\n\t\t\treturn 'llms-question-choice-' + this.model.id;\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'li',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-question-choice-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t * @return   void\n\t\t * @since    3.14.1\n\t\t * @version  3.14.1\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.render();\n\n\t\t\tthis.listenTo( this.model.collection, 'add', this.maybe_disable_buttons );\n\t\t\tthis.listenTo( this.model, 'change', this.render );\n\n\t\t\tif ( 'image' === this.model.get( 'choice_type' ) ) {\n\t\t\t\tthis.listenTo( this.model.get( 'choice' ), 'change', this.render );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t * @return   self (for chaining)\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender: function() {\n\t\t\tthis.$el.html( this.template( this.model ) );\n\t\t\treturn this;\n\t\t},\n\n\t\t/**\n\t\t * Add a new choice to the current choice list\n\t\t * Adds *after* the clicked choice\n\t\t * @param    obj   event  JS event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tadd_choice: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\t\t\tevent.preventDefault();\n\n\t\t\tvar index = this.model.collection.indexOf( this.model );\n\t\t\tthis.model.collection.parent.add_choice( {}, {\n\t\t\t\tat: index + 1,\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Delete the choice from the choice list & ensure there's at least one correct choice\n\t\t * @param    obj   event  js event obj\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tdel_choice: function( event ) {\n\n\t\t\tevent.preventDefault();\n\t\t\tBackbone.pubSub.trigger( 'model-trashed', this.model );\n\t\t\tthis.model.collection.remove( this.model );\n\n\t\t},\n\n\t\t/**\n\t\t * When the correct answer input changes sync status to model\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ttoggle_correct: function() {\n\n\t\t\tvar correct = this.$el.find( 'input[name=\"correct\"]' ).is( ':checked' );\n\t\t\tthis.model.set( 'correct', correct );\n\t\t\tthis.model.collection.trigger( 'correct-update', this.model );\n\n\t\t},\n\n\t}, Editable ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/QuestionChoiceList.js",
    "content": "/**\n * Quiz question bank view\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Views/QuestionChoice' ], function( ChoiceView ) {\n\n\treturn Backbone.CollectionView.extend( {\n\n\t\tclassName: 'llms-quiz-questions',\n\n\t\t/**\n\t\t * Choice model view\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\tmodelView: ChoiceView,\n\n\t\t/**\n\t\t * Enable keyboard events\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tprocessKeyEvents: false,\n\n\t\t/**\n\t\t * Are sections selectable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tselectable: false,\n\n\t\t/**\n\t\t * Are sections sortable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tsortable: true,\n\n\t\tsortableOptions: {\n\t\t\taxis: false,\n\t\t\t// connectWith: '.llms-lessons',\n\t\t\tcursor: 'move',\n\t\t\thandle: '.llms-choice-id',\n\t\t\titems: '.llms-question-choice',\n\t\t\tplaceholder: 'llms-question-choice llms-sortable-placeholder',\n\t\t},\n\n\t\tsortable_start: function( model ) {\n\t\t\tthis.$el.addClass( 'dragging' );\n\t\t},\n\n\t\tsortable_stop: function( model ) {\n\t\t\tthis.$el.removeClass( 'dragging' );\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/QuestionList.js",
    "content": "/**\n * Quiz question bank view\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [ 'Views/Question' ], function( QuestionView ) {\n\n\treturn Backbone.CollectionView.extend( {\n\n\t\tclassName: 'llms-quiz-questions',\n\n\t\t/**\n\t\t * Parent element\n\t\t * @type  {String}\n\t\t */\n\t\t// el: '#llms-quiz-questions',\n\n\t\t/**\n\t\t * Section model\n\t\t * @type  {[type]}\n\t\t */\n\t\tmodelView: QuestionView,\n\n\t\t/**\n\t\t * Enable keyboard events\n\t\t * @type  {Bool}\n\t\t */\n\t\tprocessKeyEvents: false,\n\n\t\t/**\n\t\t * Are sections selectable?\n\t\t * @type  {Bool}\n\t\t */\n\t\tselectable: false,\n\n\t\t/**\n\t\t * Are sections sortable?\n\t\t * @type  {Bool}\n\t\t */\n\t\tsortable: true,\n\n\t\tsortableOptions: {\n\t\t\taxis: false,\n\t\t\tconnectWith: '.llms-quiz-questions',\n\t\t\tcursor: 'move',\n\t\t\thandle: '.llms-data-stamp',\n\t\t\titems: '.llms-question',\n\t\t\tplaceholder: 'llms-question llms-sortable-placeholder',\n\t\t},\n\n\t\t/**\n\t\t * Highlight drop areas when dragging starts\n\t\t * @param    obj   model  model being sorted\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tsortable_start: function( model ) {\n\t\t\tvar selector = 'group' === model.get( 'question_type' ).get( 'id' ) ? '.llms-editor-tab > .llms-quiz-questions' : '.llms-quiz-questions';\n\t\t\t$( selector ).addClass( 'dragging' );\n\t\t},\n\n\t\t/**\n\t\t * Remove highlights when dragging stops\n\t\t * @param    obj   model  model being sorted\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tsortable_stop: function() {\n\t\t\t$( '.llms-quiz-questions' ).removeClass( 'dragging' );\n\t\t},\n\n\t\t/**\n\t\t * Overrides receive to ensure that question groups can't be moved into question groups\n\t\t * @param    obj   event  js event object\n\t\t * @param    obj   ui     jQuery UI Sortable ui object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_receive : function( event, ui ) {\n\n\t\t\tevent.stopPropagation();\n\n\t\t\t// prevent moving a question group into a question group\n\t\t\tif ( ui.item.hasClass( 'qtype--group' ) && $( event.target ).closest( '.qtype--group' ).length ) {;\n\t\t\t\tui.sender.sortable( 'cancel' );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar senderListEl = ui.sender;\n\t\t\tvar senderCollectionListView = senderListEl.data( \"view\" );\n\t\t\tif( ! senderCollectionListView || ! senderCollectionListView.collection ) return;\n\n\t\t\tvar newIndex = this._getContainerEl().children().index( ui.item );\n\t\t\tvar modelReceived = senderCollectionListView.collection.get( ui.item.attr( \"data-model-cid\" ) );\n\t\t\tsenderCollectionListView.collection.remove( modelReceived );\n\t\t\tthis.collection.add( modelReceived, { at : newIndex } );\n\t\t\tmodelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.\n\t\t\tthis.setSelectedModel( modelReceived );\n\t\t},\n\n\t\t/**\n\t\t * Override to allow manipulation of placeholder element\n\t\t * @param    {[type]}   event  [description]\n\t\t * @param    {[type]}   ui     [description]\n\t\t * @return   {[type]}\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_sortStart : function( event, ui ) {\n\n\t\t\tvar modelBeingSorted = this.collection.get( ui.item.attr( \"data-model-cid\" ) );\n\n\t\t\tui.placeholder.addClass( 'qtype--' + modelBeingSorted.get( 'question_type' ).get( 'id' ) );\n\n\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\tthis.spawn( \"sortStart\", { modelBeingSorted : modelBeingSorted } );\n\t\t\telse this.trigger( \"sortStart\", modelBeingSorted );\n\t\t},\n\n\t\t/**\n\t\t * Overloads the function from Backbone.CollectionView core because it doesn't send stop events\n\t\t * if moving from one sortable to another... :-(\n\t\t * @param    obj   event  js event object\n\t\t * @param    obj   ui     jQuery UI object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_sortStop : function( event, ui ) {\n\n\t\t\tevent.stopPropagation();\n\n\t\t\tvar modelBeingSorted = this.collection.get( ui.item.attr( 'data-model-cid' ) ),\n\t\t\t\tmodelViewContainerEl = this._getContainerEl(),\n\t\t\t\tnewIndex = modelViewContainerEl.children().index( ui.item );\n\n\t\t\tif ( newIndex == -1 && modelBeingSorted ) {\n\t\t\t\tthis.collection.remove( modelBeingSorted );\n\t\t\t}\n\n\t\t\tthis._reorderCollectionBasedOnHTML();\n\t\t\tthis.updateDependentControls();\n\n\t\t\tif( this._isBackboneCourierAvailable() ) {\n\t\t\t\tthis.spawn( 'sortStop', { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );\n\t\t\t} else {\n\t\t\t\tthis.trigger( 'sortStop', modelBeingSorted, newIndex );\n\t\t\t}\n\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/QuestionType.js",
    "content": "/**\n * Question Type View\n *\n * @since 3.16.0\n * @since 3.30.1 Fixed issue causing multiple binds for add_existing_question events.\n * @version 5.4.0\n */\ndefine( [ 'Views/Popover', 'Views/PostSearch' ], function( Popover, QuestionSearch ) {\n\n\treturn Backbone.View.extend( {\n\n\t\t/**\n\t\t * HTML class names.\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\tclassName: 'llms-question-type',\n\n\t\tevents: {\n\t\t\t'click .llms-add-question': 'add_question',\n\t\t},\n\n\t\t/**\n\t\t * HTML element wrapper ID attribute.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {String}\n\t\t */\n\t\tid: function() {\n\t\t\treturn 'llms-question-type-' + this.model.id;\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name.\n\t\t *\n\t\t * @type {String}\n\t\t */\n\t\ttagName: 'li',\n\n\t\t/**\n\t\t * Get the underscore template.\n\t\t *\n\t\t * @type {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-question-type-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen).\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.render();\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Self} For chaining.\n\t\t */\n\t\trender: function() {\n\t\t\tthis.$el.html( this.template( this.model ) );\n\t\t\treturn this;\n\t\t},\n\n\t\t/**\n\t\t * Add a question of the selected type to the current quiz.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.27.0 Unknown.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tadd_question: function() {\n\n\t\t\tif ( 'existing' === this.model.get( 'id' ) ) {\n\t\t\t\tthis.add_existing_question_click();\n\t\t\t} else {\n\t\t\t\tthis.add_new_question();\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Add a new question to the quiz.\n\t\t *\n\t\t * @since 3.27.0\n\t\t * @since 3.30.1 Fixed issue causing multiple binds.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tadd_existing_question_click: function() {\n\n\t\t\tvar pop = new Popover( {\n\t\t\t\tel: '#llms-add-question--existing',\n\t\t\t\targs: {\n\t\t\t\t\tbackdrop: true,\n\t\t\t\t\tcloseable: true,\n\t\t\t\t\tcontainer: '#llms-builder-sidebar',\n\t\t\t\t\tdismissible: true,\n\t\t\t\t\tplacement: 'top-left',\n\t\t\t\t\twidth: 'calc( 100% - 40px )',\n\t\t\t\t\toffsetLeft: 250,\n\t\t\t\t\toffsetTop: 60,\n\t\t\t\t\ttitle: LLMS.l10n.translate( 'Add Existing Question' ),\n\t\t\t\t\tcontent: new QuestionSearch( {\n\t\t\t\t\t\tpost_type: 'llms_question',\n\t\t\t\t\t\tsearching_message: LLMS.l10n.translate( 'Search for existing questions...' ),\n\t\t\t\t\t} ).render().$el,\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tpop.show();\n\t\t\tBackbone.pubSub.on( 'question-search-select', this.add_existing_question, this );\n\t\t\tBackbone.pubSub.on( 'question-search-select', function( event ) {\n\t\t\t\tpop.hide();\n\t\t\t\tBackbone.pubSub.off( 'question-search-select', this.add_existing_question, this );\n\t\t\t}, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Callback event fired when a question is selected from the Add Existing Question popover interface.\n\t\t *\n\t\t * @since 3.27.0\n\t\t * @since 5.4.0 Use author id instead of the question author object.\n\t\t *\n\t\t * @param {Object} event JS event object.\n\t\t * @return {Void}\n\t\t */\n\t\tadd_existing_question: function( event ) {\n\n\t\t\tvar question = event.data;\n\n\t\t\tif ( 'clone' === event.action ) {\n\t\t\t\tquestion = _.prepareQuestionObjectForCloning( question );\n\t\t\t} else {\n\t\t\t\t// Use author id instead of the question author object.\n\t\t\t\tquestion = _.prepareExistingPostObjectDataForAddingOrCloning( question );\n\t\t\t\tquestion._forceSync = true;\n\t\t\t}\n\n\t\t\tquestion._expanded = true;\n\t\t\tthis.quiz.add_question( question );\n\n\t\t\tthis.quiz.trigger( 'new-question-added' );\n\n\t\t},\n\n\t\t/**\n\t\t * Add a new question to the quiz.\n\t\t *\n\t\t * @since 3.27.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tadd_new_question: function() {\n\n\t\t\tthis.quiz.add_question( {\n\t\t\t\t_expanded: true,\n\t\t\t\tchoices: this.model.get( 'default_choices' ) ? this.model.get( 'default_choices' ) : null,\n\t\t\t\tquestion_type: this.model,\n\t\t\t} );\n\n\t\t\tthis.quiz.trigger( 'new-question-added' );\n\n\t\t},\n\n\t\t// filter: function( term ) {\n\n\t\t// var words = this.model.get_keywords().map( function( word ) {\n\t\t// return word.toLowerCase();\n\t\t// } );\n\n\t\t// term = term.toLowerCase();\n\n\t\t// if ( -1 === words.indexOf( term ) ) {\n\t\t// this.$el.addClass( 'filtered' );\n\t\t// } else {\n\t\t// this.$el.removeClass( 'filtered' );\n\t\t// }\n\n\t\t// },\n\n\t\t// clear_filter: function() {\n\t\t// this.$el.removeClass( 'filtered' );\n\t\t// }\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Quiz.js",
    "content": "/**\n * Single Quiz View.\n *\n * @since 3.16.0\n * @version 5.4.0\n */\ndefine( [\n\t\t'Models/Quiz',\n\t\t'Views/Popover',\n\t\t'Views/PostSearch',\n\t\t'Views/QuestionBank',\n\t\t'Views/QuestionList',\n\t\t'Views/SettingsFields',\n\t\t'Views/_Detachable',\n\t\t'Views/_Editable',\n\t\t'Views/_Subview',\n\t\t'Views/_Trashable'\n\t], function(\n\t\tQuizModel,\n\t\tPopover,\n\t\tPostSearch,\n\t\tQuestionBank,\n\t\tQuestionList,\n\t\tSettingsFields,\n\t\tDetachable,\n\t\tEditable,\n\t\tSubview,\n\t\tTrashable\n\t) {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * Current view state.\n\t\t *\n\t\t * @type {String}\n\t\t */\n\t\tstate: 'default',\n\n\t\t/**\n\t\t * Current Subviews.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tviews: {\n\t\t\tsettings: {\n\t\t\t\tclass: SettingsFields,\n\t\t\t\tinstance: null,\n\t\t\t\tstate: 'default',\n\t\t\t},\n\t\t\tbank: {\n\t\t\t\tclass: QuestionBank,\n\t\t\t\tinstance: null,\n\t\t\t\tstate: 'default',\n\t\t\t},\n\t\t\tlist: {\n\t\t\t\tclass: QuestionList,\n\t\t\t\tinstance: null,\n\t\t\t\tstate: 'default',\n\t\t\t},\n\t\t},\n\n\t\tel: '#llms-editor-quiz',\n\n\t\t/**\n\t\t * Events.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tevents: _.defaults( {\n\t\t\t'click #llms-existing-quiz': 'add_existing_quiz_click',\n\t\t\t'click #llms-new-quiz': 'add_new_quiz',\n\t\t\t'click #llms-show-question-bank': 'show_tools',\n\t\t\t'click .bulk-toggle': 'bulk_toggle',\n\t\t\t// 'keyup #llms-question-bank-filter': 'filter_question_types',\n\t\t\t// 'search #llms-question-bank-filter': 'filter_question_types',\n\t\t}, Detachable.events, Editable.events, Trashable.events ),\n\n\t\t/**\n\t\t * Wrapper Tag name.\n\t\t *\n\t\t * @type {String}\n\t\t */\n\t\ttagName: 'div',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t *\n\t\t * @type {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-quiz-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen).\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.19.2 Unknown.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tinitialize: function( data ) {\n\n\t\t\tthis.lesson = data.lesson;\n\n\t\t\t// Initialize the model if the quiz is enabled or it's disabled but we still have data for a quiz.\n\t\t\tif ( 'yes' === this.lesson.get( 'quiz_enabled' ) || ! _.isEmpty( this.lesson.get( 'quiz' ) ) ) {\n\n\t\t\t\tthis.model = this.lesson.get( 'quiz' );\n\n\t\t\t\t/**\n\t\t\t\t * @todo  this is a terrible terrible patch\n\t\t\t\t *        I've spent nearly 3 days trying to figure out how to not use this line of code\n\t\t\t\t *        ISSUE REPRODUCTION:\n\t\t\t\t *        Open course builder\n\t\t\t\t *        Open a lesson (A) and add a quiz\n\t\t\t\t *        Switch to a new lesson (B)\n\t\t\t\t *        Add a new quiz\n\t\t\t\t *        Return to lesson A and the quizzes parent will be set to LESSON B\n\t\t\t\t *        This will happen for *every* quiz in the builder...\n\t\t\t\t *        Adding this set_parent on init guarantees that the quizzes correct parent is set\n\t\t\t\t *        after adding new quizzes to other lessons\n\t\t\t\t *        it's awful and it's gross...\n\t\t\t\t *        I'm confused and tired and going to miss release dates again because of it\n\t\t\t\t */\n\t\t\t\tthis.model.set_parent( this.lesson );\n\n\t\t\t\tthis.listenTo( this.model, 'change:_points', this.render_points );\n\t\t\t\tthis.listenTo( this.model, 'change:permalink', this.render_settings );\n\t\t\t\tthis.listenTo( this.model, 'change:name', this.render_settings );\n\n\t\t\t}\n\n\t\t\tthis.on( 'model-trashed', this.on_trashed );\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.19.2 Unknown.\n\t\t *\n\t\t * @return {Self} For chaining.\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.html( this.template( this.model ) );\n\n\t\t\t// Render the quiz builder.\n\t\t\tif ( this.model ) {\n\n\t\t\t\t// Don't allow interaction until questions are lazy loaded.\n\t\t\t\tLLMS.Spinner.start( this.$el );\n\n\t\t\t\tthis.render_subview( 'settings', {\n\t\t\t\t\tel: '#llms-quiz-settings-fields',\n\t\t\t\t\tmodel: this.model,\n\t\t\t\t} );\n\n\t\t\t\tthis.init_datepickers();\n\t\t\t\tthis.init_selects();\n\n\t\t\t\tthis.render_subview( 'bank', {\n\t\t\t\t\tcollection: window.llms_builder.questions,\n\t\t\t\t} );\n\n\t\t\t\tvar last_group = null,\n\t\t\t\t\tgroup = null;\n\t\t\t\t// Let all the question types reference the quiz for adding questions quickly.\n\t\t\t\tthis.get_subview( 'bank' ).instance.viewManager.each( function( view ) {\n\n\t\t\t\t\tview.quiz = this.model;\n\n\t\t\t\t\tgroup = view.model.get( 'group' ).name;\n\n\t\t\t\t\tif ( last_group !== group ) {\n\t\t\t\t\t\tlast_group = group;\n\t\t\t\t\t\tview.$el.before( '<li class=\"llms-question-bank-header\"><h4>' + group + '</h4></li>' );\n\t\t\t\t\t}\n\n\t\t\t\t}, this );\n\n\t\t\t\tthis.model.load_questions( _.bind( function( err ) {\n\n\t\t\t\t\tif ( err ) {\n\t\t\t\t\t\talert( LLMS.l10n.translate( 'An error occurred while trying to load the questions. Please refresh the page and try again.' ) );\n\t\t\t\t\t\treturn this;\n\t\t\t\t\t}\n\n\t\t\t\t\tLLMS.Spinner.stop( this.$el );\n\t\t\t\t\tthis.render_subview( 'list', {\n\t\t\t\t\t\tel: '#llms-quiz-questions',\n\t\t\t\t\t\tcollection: this.model.get( 'questions' ),\n\t\t\t\t\t} );\n\t\t\t\t\tvar list = this.get_subview( 'list' ).instance;\n\t\t\t\t\tlist.quiz = this;\n\t\t\t\t\tlist.collection.on( 'add', function() {\n\t\t\t\t\t\tlist.collection.trigger( 'reorder' );\n\t\t\t\t\t}, this );\n\t\t\t\t\tlist.on( 'sortStart', list.sortable_start );\n\t\t\t\t\tlist.on( 'sortStop', list.sortable_stop );\n\n\t\t\t\t}, this ) );\n\n\t\t\t\tthis.model.on( 'new-question-added', function() {\n\t\t\t\t\tvar $questions = this.$el.find( '#llms-quiz-questions' );\n\t\t\t\t\t$questions.animate( { scrollTop: $questions.prop( 'scrollHeight' ) }, 200 );\n\t\t\t\t}, this );\n\n\t\t\t}\n\n\t\t\treturn this;\n\n\t\t},\n\n\t\t/**\n\t\t * On quiz points update, update the value of the Total Points area in the header.\n\t\t *\n\t\t * @since 3.17.6\n\t\t *\n\t\t * @param {Object} quiz   Instance of the quiz model.\n\t\t * @param {Int}    points Updated number of points.\n\t\t * @return {Void}\n\t\t */\n\t\trender_points: function( quiz, points ) {\n\n\t\t\tthis.$el.find( '#llms-quiz-total-points' ).text( points );\n\n\t\t},\n\n\t\t/**\n\t\t * Re-render the settings subview.\n\t\t *\n\t\t * Used when the permalink is updated after saving so the settings\n\t\t * panel reflects the new permalink without a full re-render.\n\t\t *\n\t\t * @since 10.0.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\trender_settings: function() {\n\n\t\t\tvar view = this.get_subview( 'settings' );\n\t\t\tif ( view && view.instance ) {\n\t\t\t\tview.instance.render();\n\t\t\t\tthis.init_datepickers();\n\t\t\t\tthis.init_selects();\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Bulk expand / collapse question buttons.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param {Object} Event JS event object.\n\t\t * @return {Void}\n\t\t */\n\t\tbulk_toggle: function( event ) {\n\n\t\t\tvar expanded = ( 'expand' === $( event.target ).attr( 'data-action' ) );\n\n\t\t\tthis.model.get( 'questions' ).each( function( question ) {\n\t\t\t\tquestion.set( '_expanded', expanded );\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Adds a new quiz to a lesson which currently has no quiz associated with it.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tadd_new_quiz: function() {\n\n\t\t\tvar quiz = this.lesson.get( 'quiz' );\n\t\t\tif ( _.isEmpty( quiz ) ) {\n\t\t\t\tquiz = this.lesson.add_quiz();\n\t\t\t} else {\n\t\t\t\tthis.lesson.set( 'quiz_enabled', 'yes' );\n\t\t\t}\n\n\t\t\tthis.model = quiz;\n\t\t\tthis.listenTo( this.model, 'change:_points', this.render_points );\n\t\t\tthis.listenTo( this.model, 'change:permalink', this.render_settings );\n\t\t\tthis.listenTo( this.model, 'change:name', this.render_settings );\n\t\t\tthis.render();\n\n\t\t},\n\n\n\t\t/**\n\t\t * Add an existing quiz to a lesson.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.24.0 Unknown.\n\t\t * @since 5.4.0 Use author id instead of the quiz author object.\n\t\t *\n\t\t * @param {Object} event JS event object.\n\t\t * @return {Void}\n\t\t */\n\t\tadd_existing_quiz: function( event ) {\n\n\t\t\tthis.post_search_popover.hide();\n\n\t\t\tvar quiz = event.data;\n\n\t\t\tif ( 'clone' === event.action ) {\n\n\t\t\t\tquiz = _.prepareQuizObjectForCloning( quiz );\n\n\t\t\t} else {\n\n\t\t\t\t// Use author id instead of the quiz author object.\n\t\t\t\tquiz = _.prepareExistingPostObjectDataForAddingOrCloning( quiz );\n\t\t\t\tquiz._forceSync = true;\n\n\t\t\t}\n\n\t\t\tdelete quiz.lesson_id;\n\n\t\t\tthis.lesson.add_quiz( quiz );\n\t\t\tthis.model = this.lesson.get( 'quiz' );\n\t\t\tthis.listenTo( this.model, 'change:_points', this.render_points );\n\t\t\tthis.listenTo( this.model, 'change:permalink', this.render_settings );\n\t\t\tthis.listenTo( this.model, 'change:name', this.render_settings );\n\t\t\tthis.render();\n\n\t\t},\n\n\t\t/**\n\t\t * Open add existing quiz popover.\n\t\t *\n\t\t * @since 3.16.12\n\t\t *\n\t\t * @param {Object} event JS event object.\n\t\t * @return {Void}\n\t\t */\n\t\tadd_existing_quiz_click: function( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tthis.post_search_popover = new Popover( {\n\t\t\t\tel: '#llms-existing-quiz',\n\t\t\t\targs: {\n\t\t\t\t\tbackdrop: true,\n\t\t\t\t\tcloseable: true,\n\t\t\t\t\tcontainer: '.wrap.lifterlms.llms-builder',\n\t\t\t\t\tdismissible: true,\n\t\t\t\t\tplacement: 'left',\n\t\t\t\t\twidth: 480,\n\t\t\t\t\ttitle: LLMS.l10n.translate( 'Add Existing Quiz' ),\n\t\t\t\t\tcontent: new PostSearch( {\n\t\t\t\t\t\tpost_type: 'llms_quiz',\n\t\t\t\t\t\tsearching_message: LLMS.l10n.translate( 'Search for existing quizzes...' ),\n\t\t\t\t\t} ).render().$el,\n\t\t\t\t\tonHide: function() {\n\t\t\t\t\t\tBackbone.pubSub.off( 'quiz-search-select' );\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tthis.post_search_popover.show();\n\t\t\tBackbone.pubSub.once( 'quiz-search-select', this.add_existing_quiz, this );\n\n\t\t},\n\n\t\t// filter_question_types: _.debounce( function( event ) {\n\n\t\t// \tvar term = $( event.target ).val();\n\n\t\t// \tthis.QuestionBankView.viewManager.each( function( view ) {\n\t\t// \t\tif ( ! term ) {\n\t\t// \t\t\tview.clear_filter();\n\t\t// \t\t} else {\n\t\t// \t\t\tview.filter( term );\n\t\t// \t\t}\n\t\t// \t} );\n\n\n\t\t// }, 300 ),\n\n\t\t/**\n\t\t * Callback function when the quiz has been deleted.\n\t\t *\n\t\t * @since 3.16.6\n\t\t *\n\t\t * @param {Oject} quiz Quiz Model.\n\t\t * @return {Void}\n\t\t */\n\t\ton_trashed: function( quiz ) {\n\n\t\t\tthis.lesson.set( 'quiz_enabled', 'no' );\n\t\t\tthis.lesson.set( 'quiz', '' );\n\n\t\t\tdelete this.model;\n\n\t\t\tthis.render();\n\n\t\t},\n\n\t\t/**\n\t\t * \"Add Question\" button click event.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * Creates a popover with question type list interface.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tshow_tools: function() {\n\n\t\t\t// Create popover,\n\t\t\tvar pop = new Popover( {\n\t\t\t\tel: '#llms-show-question-bank',\n\t\t\t\targs: {\n\t\t\t\t\tbackdrop: true,\n\t\t\t\t\tcloseable: true,\n\t\t\t\t\tcontainer: '#llms-builder-sidebar',\n\t\t\t\t\tdismissible: true,\n\t\t\t\t\tplacement: 'top-left',\n\t\t\t\t\twidth: 'calc( 100% - 40px )',\n\t\t\t\t\ttitle: LLMS.l10n.translate( 'Add a Question' ),\n\t\t\t\t\turl: '#llms-quiz-tools',\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\t// Show it.\n\t\t\tpop.show();\n\n\t\t\t// If a question is added, hide the popover.\n\t\t\tthis.model.on( 'new-question-added', function() {\n\t\t\t\tpop.hide();\n\t\t\t} );\n\n\t\t},\n\n\t\tget_question_list: function( options ) {\n\t\t\treturn new QuestionList( options );\n\t\t}\n\n\t}, Detachable, Editable, Subview, Trashable, SettingsFields ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Section.js",
    "content": "/**\n * Single Section View\n * @since    3.13.0\n * @version  3.16.12\n */\ndefine( [\n\t\t'Views/LessonList',\n\t\t'Views/_Editable',\n\t\t'Views/_Shiftable',\n\t\t'Views/_Trashable'\n\t], function(\n\t\tLessonListView,\n\t\tEditable,\n\t\tShiftable,\n\t\tTrashable\n\t) {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * Get default attributes for the html wrapper element\n\t\t * @return   obj\n\t\t * @since    3.13.0\n\t\t * @version  3.13.0\n\t\t */\n\t\tattributes: function() {\n\t\t\treturn {\n\t\t\t\t'data-id': this.model.id,\n\t\t\t};\n\t\t},\n\n\t\t/**\n\t\t * Element class names\n\t\t * @type  {String}\n\t\t */\n\t\tclassName: 'llms-builder-item llms-section',\n\n\t\t/**\n\t\t * Events\n\t\t * @type     {Object}\n\t\t * @since    3.16.0\n\t\t * @version  3.16.12\n\t\t */\n\t\tevents: _.defaults( {\n\n\t\t\t'click .expand': 'expand',\n\t\t\t'click .collapse': 'collapse',\n\t\t\t'click .shift-up--section': 'shift_up',\n\t\t\t'click .shift-down--section': 'shift_down',\n\t\t\t'click .new-lesson': 'add_new_lesson',\n\t\t\t'click .llms-builder-header': 'toggle',\n\t\t\t'mouseenter .llms-lessons': 'on_mouseenter',\n\n\t\t}, Editable.events, Trashable.events ),\n\n\t\t/**\n\t\t * HTML element wrapper ID attribute\n\t\t * @return   string\n\t\t * @since    3.13.0\n\t\t * @version  3.13.0\n\t\t */\n\t\tid: function() {\n\t\t\treturn 'llms-section-' + this.model.id;\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'li',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-section-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t * @return   void\n\t\t * @since    3.13.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\tthis.render();\n\t\t\tthis.listenTo( this.model, 'change', this.render );\n\t\t\tthis.listenTo( this.model, 'change:_expanded', this.toggle_expanded );\n\t\t\tthis.lessonListView.collection.on( 'add', this.on_lesson_add, this );\n\n\t\t\tthis.dragTimeout = null;\n\n\t\t\tBackbone.pubSub.on( 'expand-all', this.expand, this );\n\t\t\tBackbone.pubSub.on( 'collapse-all', this.collapse, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Render the section\n\t\t * Initializes a new collection and views for all lessons in the section\n\t\t * @return   void\n\t\t * @since    3.13.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.html( this.template( this.model.toJSON() ) );\n\n\t\t\tthis.maybe_hide_shiftable_buttons();\n\n\t\t\tthis.lessonListView = new LessonListView( {\n\t\t\t\tel: this.$el.find( '.llms-lessons' ),\n\t\t\t\tcollection: this.model.get( 'lessons' ),\n\t\t\t} );\n\t\t\tthis.lessonListView.render();\n\t\t\tthis.lessonListView.on( 'sortStart', this.lessonListView.sortable_start );\n\t\t\tthis.lessonListView.on( 'sortStop', this.lessonListView.sortable_stop );\n\n\t\t\t// selection changes\n\t\t\tthis.lessonListView.on( 'selectionChanged', this.active_lesson_change, this );\n\n\t\t\tthis.maybe_hide_trash_button();\n\n\t\t\treturn this;\n\n\t\t},\n\n\t\tadd_new_lesson: function( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tBackbone.pubSub.trigger( 'section-select', this.model );\n\t\t\tBackbone.pubSub.trigger( 'add-new-lesson' );\n\n\t\t},\n\n\t\tactive_lesson_change: function( current, previous ) {\n\n\t\t\tBackbone.pubSub.trigger( 'active-lesson-change', {\n\t\t\t\tcurrent: current,\n\t\t\t\tprevious: previous,\n\t\t\t} );\n\n\t\t},\n\n\t\ttoggle: function( event, update ) {\n\n\t\t\t// We only want to expand/collapse when the actual header div is clicked, not an element inside it.\n\t\t\tif ( 'llms-builder-header' !== event.target.className ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( this.model.get( '_expanded' ) ) {\n\t\t\t\tthis.collapse( event, update );\n\t\t\t} else {\n\t\t\t\tthis.expand( event, update );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Collapse lessons within the section\n\t\t * @param    obj   event    js event object\n\t\t * @param    bool  update   if true, updates the model to reflect the new state\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tcollapse: function( event, update ) {\n\n\t\t\tif ( 'undefined' === typeof update ) {\n\t\t\t\tupdate = true;\n\t\t\t}\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.stopPropagation();\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\n\t\t\tthis.$el.removeClass( 'expanded' ).find( '.drag-expanded' ).removeClass( 'drag-expanded' );\n\t\t\tif ( update ) {\n\t\t\t\tthis.model.set( '_expanded', false );\n\t\t\t}\n\t\t\tBackbone.pubSub.trigger( 'section-toggle', this.model );\n\n\t\t},\n\n\t\t/**\n\t\t * Expand lessons within the section\n\t\t * @param    obj   event    js event object\n\t\t * @param    bool  update   if true, updates the model to reflect the new state\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\texpand: function( event, update ) {\n\n\t\t\tif ( 'undefined' === typeof update ) {\n\t\t\t\tupdate = true;\n\t\t\t}\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.stopPropagation();\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\n\t\t\tthis.$el.addClass( 'expanded' );\n\t\t\tif ( update ) {\n\t\t\t\tthis.model.set( '_expanded', true );\n\t\t\t}\n\t\t\tBackbone.pubSub.trigger( 'section-toggle', this.model );\n\n\t\t},\n\n\t\tmaybe_hide_trash_button: function() {\n\n\t\t\tvar $btn = this.$el.find( '.trash--section' );\n\n\t\t\tif ( this.model.get( 'lessons' ).isEmpty() ) {\n\n\t\t\t\t$btn.show();\n\n\t\t\t} else {\n\n\t\t\t\t$btn.hide()\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * When a lesson is added to the section trigger a collection reorder & update the lesson's id\n\t\t * @param    obj   model  Lesson model\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ton_lesson_add: function( model ) {\n\n\t\t\tthis.lessonListView.collection.trigger( 'reorder' );\n\t\t\tmodel.set( 'parent_section', this.model.get( 'id' ) );\n\t\t\tthis.expand();\n\n\t\t},\n\n\t\ton_mouseenter: function( event ) {\n\n\n\t\t\tif ( $( event.target ).hasClass( 'dragging' ) ) {\n\n\t\t\t\t$( '.drag-expanded' ).removeClass( 'drag-expanded' );\n\t\t\t\t$( event.target ).addClass( 'drag-expanded' );\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Expand\n\t\t * @param    {[type]}   model  [description]\n\t\t * @param    {[type]}   value  [description]\n\t\t * @return   {[type]}\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ttoggle_expanded: function( model, value ) {\n\n\t\t\tif ( value ) {\n\t\t\t\tthis.expand( null, false );\n\t\t\t} else {\n\t\t\t\tthis.collapse( null, false );\n\t\t\t}\n\n\t\t},\n\n\t}, Editable, Shiftable, Trashable ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/SectionList.js",
    "content": "/**\n * Single Section View\n *\n * @since    3.13.0\n * @version  3.16.0\n */\ndefine( [ 'Views/Section', 'Views/_Receivable' ], function( SectionView, Receivable ) {\n\n\treturn Backbone.CollectionView.extend( _.defaults( {\n\n\t\t/**\n\t\t * Parent element\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\tel: '#llms-sections',\n\n\t\tevents : {\n\t\t\t'mousedown > li.llms-section' : '_listItem_onMousedown',\n\t\t\t// 'dblclick > li, tbody > tr > td' : '_listItem_onDoubleClick',\n\t\t\t'click' : '_listBackground_onClick',\n\t\t\t'click ul.collection-view' : '_listBackground_onClick',\n\t\t\t'keydown' : '_onKeydown'\n\t\t},\n\n\t\t/**\n\t\t * Section model\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\tmodelView: SectionView,\n\n\t\t/**\n\t\t * Enable keyboard events\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tprocessKeyEvents: false,\n\n\t\t/**\n\t\t * Are sections selectable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tselectable: true,\n\n\t\t/**\n\t\t * Are sections sortable?\n\t\t *\n\t\t * @type  {Bool}\n\t\t */\n\t\tsortable: true,\n\n\t\tsortableOptions: {\n\t\t\taxis: false,\n\t\t\tcursor: 'move',\n\t\t\thandle: '.drag-section',\n\t\t\titems: '.llms-section',\n\t\t\tplaceholder: 'llms-section llms-sortable-placeholder',\n\t\t},\n\n\t\tsortable_start: function( collection ) {\n\t\t\tthis.$el.addClass( 'dragging' );\n\t\t},\n\n\t\tsortable_stop: function( collection ) {\n\t\t\tthis.$el.removeClass( 'dragging' );\n\t\t},\n\n\t}, Receivable ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/SettingsFields.js",
    "content": "/**\n * Model settings fields view\n *\n * @since 3.17.0\n * @version 4.7.0\n */\ndefine( [], function() {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * DOM events\n\t\t *\n\t\t * @type  {Object}\n\t\t */\n\t\tevents: {\n\t\t\t'click .llms-settings-group-toggle': 'toggle_group',\n\t\t},\n\n\t\t/**\n\t\t * Processed fields data\n\t\t * Allows access by ID without traversing the schema\n\t\t *\n\t\t * @type  {Object}\n\t\t */\n\t\tfields: {},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'div',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-settings-fields-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\t// initialize: function() {},\n\n\t\t/**\n\t\t * Retrieve an array of all editor fields in all groups\n\t\t *\n\t\t * @return   array\n\t\t * @since    3.17.1\n\t\t * @version  3.17.1\n\t\t */\n\t\tget_editor_fields: function() {\n\t\t\treturn _.filter( this.fields, function( field ) {\n\t\t\t\treturn this.is_editor_field( field.type );\n\t\t\t}, this );\n\t\t},\n\n\t\t/**\n\t\t * Get settings group data from a model\n\t\t *\n\t\t * @return   {[type]}\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tget_groups: function() {\n\n\t\t\treturn this.model.get_settings_fields();\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if a settings group is hidden in localStorage\n\t\t *\n\t\t * @param    string   group_id  id of the group\n\t\t * @return   {Boolean}\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tis_group_hidden: function( group_id ) {\n\n\t\t\tvar id = 'llms-' + this.model.get( 'type' ) + '-settings-group--' + group_id;\n\n\t\t\tif ( 'undefined' !== window.localStorage ) {\n\t\t\t\treturn ( 'hidden' === window.localStorage.getItem( id ) );\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t},\n\n\t\t/**\n\t\t * Get the switch attribute for a field with switches\n\t\t *\n\t\t * @param    obj   field  field data obj\n\t\t * @return   string\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tget_switch_attribute: function( field ) {\n\n\t\t\treturn field.switch_attribute ? field.switch_attribute : field.attribute;\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if a field has a switch\n\t\t *\n\t\t * @param    string   type  field type string\n\t\t * @return   {Boolean}\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\thas_switch: function( type ) {\n\t\t\treturn ( -1 !== type.indexOf( 'switch' ) );\n\t\t},\n\n\t\t/**\n\t\t * Determine if a field is a default (text) field\n\t\t *\n\t\t * @param    string   type  field type string\n\t\t * @return   {Boolean}\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tis_default_field: function( type ) {\n\n\t\t\tvar types = [ 'audio_embed', 'datepicker', 'number', 'text', 'video_embed' ];\n\t\t\treturn ( -1 !== types.indexOf( type.replace( 'switch-', '' ) ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if a field is a WYSIWYG editor field\n\t\t *\n\t\t * @param    string   type  field type string\n\t\t * @return   {Boolean}\n\t\t * @since    3.17.1\n\t\t * @version  3.17.1\n\t\t */\n\t\tis_editor_field: function( type ) {\n\n\t\t\tvar types = [ 'editor', 'switch-editor' ];\n\t\t\treturn ( -1 !== types.indexOf( type.replace( 'switch-', '' ) ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if a switch is enabled for a field\n\t\t *\n\t\t * @param    obj   field  field data object\n\t\t * @return   {Boolean}\n\t\t * @since    3.17.0\n\t\t * @version  3.17.6\n\t\t */\n\t\tis_switch_condition_met: function( field ) {\n\n\t\t\treturn ( field.switch_on === this.model.get( field.switch_attribute ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t *\n\t\t * @return   self (for chaining)\n\t\t * @since    3.17.0\n\t\t * @version  3.17.1\n\t\t */\n\t\trender: function() {\n\n\t\t\tthis.$el.html( this.template( this ) );\n\n\t\t\t// if editors exist, render them\n\t\t\t_.each( this.get_editor_fields(), function( field ) {\n\t\t\t\tthis.render_editor( field );\n\t\t\t}, this );\n\n\t\t\treturn this;\n\n\t\t},\n\n\t\t/**\n\t\t * Renders an editor field\n\t\t *\n\t\t * @since 3.17.1\n\t\t * @since 3.37.11 Replace references to `wp.editor` with `_.getEditor()` helper.\n\t\t *\n\t\t * @param  {Object} field Field data object.\n\t\t * @return {Void}\n\t\t */\n\t\trender_editor: function( field ) {\n\n\t\t\tvar self     = this,\n\t\t\t\twpEditor = _.getEditor();\n\n\t\t\t// Exit early if there's no editor to work with.\n\t\t\tif ( undefined === wpEditor ) {\n\t\t\t\tconsole.error( 'Unable to access `wp.oldEditor` or `wp.editor`.' );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\twpEditor.remove( field.id );\n\t\t\tfield.settings.tinymce.setup = function( editor ) {\n\n\t\t\t\tvar $ed     = $( '#' + editor.id ),\n\t\t\t\t\t$parent = $ed.closest( '.llms-editable-editor' ),\n\t\t\t\t\t$label  = $parent.find( '.llms-label' ),\n\t\t\t\t\tprop    = $ed.attr( 'data-attribute' )\n\n\t\t\t\tif ( $label.length ) {\n\t\t\t\t\t$label.prependTo( $parent.find( '.wp-editor-tools' ) );\n\t\t\t\t}\n\n\t\t\t\t// save changes to the model via Visual ed\n\t\t\t\teditor.on( 'change', function( event ) {\n\t\t\t\t\tself.model.set( prop, wpEditor.getContent( editor.id ) );\n\t\t\t\t} );\n\n\t\t\t\t// save changes via Text ed\n\t\t\t\t$ed.on( 'input', function( event ) {\n\t\t\t\t\tself.model.set( prop, $ed.val() );\n\t\t\t\t} );\n\n\t\t\t\t// trigger an input on the Text ed when quicktags buttons are clicked\n\t\t\t\t$parent.on( 'click', '.quicktags-toolbar .ed_button', function() {\n\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t$ed.trigger( 'input' );\n\t\t\t\t\t}, 10 );\n\t\t\t\t} );\n\t\t\t};\n\n\t\t\twpEditor.initialize( field.id, field.settings );\n\n\t\t},\n\n\t\t/**\n\t\t * Get the HTML for a select field\n\t\t *\n\t\t * @param    obj      options    flat or multi-dimensional options object\n\t\t * @param    string   attribute  name of the select field's attribute\n\t\t * @return   string\n\t\t * @since    3.17.0\n\t\t * @version  3.17.2\n\t\t */\n\t\trender_select_options: function( options, attribute ) {\n\n\t\t\tvar html     = '',\n\t\t\t\tselected = this.model.get( attribute );\n\n\t\t\tfunction option_html( label, val ) {\n\n\t\t\t\treturn '<option value=\"' + val + '\"' + _.selected( val, selected ) + '>' + label.substring( 0, 100 ) + ( label.length > 100 ? '...' : '' ) + '</option>';\n\n\t\t\t}\n\n\t\t\t_.each( options, function( option, index ) {\n\n\t\t\t\t// this will be an key:val object\n\t\t\t\tif ( 'string' === typeof option ) {\n\t\t\t\t\thtml += option_html( option, index );\n\t\t\t\t\t// either option group or array of key,val objects\n\t\t\t\t} else if ( 'object' === typeof option ) {\n\t\t\t\t\t// option group\n\t\t\t\t\tif ( option.label && option.options ) {\n\t\t\t\t\t\thtml += '<optgroup label=\"' + option.label + '\">';\n\t\t\t\t\t\thtml += this.render_select_options( option.options, attribute );\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtml += option_html( option.val, option.key );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}, this );\n\n\t\t\treturn html;\n\n\t\t},\n\n\t\t/**\n\t\t * Setup and fill fields with default data based on field type\n\t\t *\n\t\t * @since 3.17.0\n\t\t * @since 3.24.0 Unknown.\n\t\t * @since 3.37.11 Replace reference to `wp.editor` with `_.getEditor()` helper.\n\t\t * @since 4.7.0 Ensure `switch-number` fields are set with the `number` type attribute.\n\t\t *\n\t\t * @param  {Object}  orig_field  Original field as defined in the settings.\n\t\t * @param  {Integer} field_index Index of the field in the current row.\n\t\t * @return {Object}\n\t\t */\n\t\tsetup_field: function( orig_field, field_index ) {\n\n\t\t\tvar defaults = {\n\t\t\t\tclasses: [],\n\t\t\t\tid: _.uniqueId( orig_field.attribute + '_' ),\n\t\t\t\tinput_type: 'text',\n\t\t\t\tlabel: '',\n\t\t\t\toptions: {},\n\t\t\t\tplaceholder: '',\n\t\t\t\ttip: '',\n\t\t\t\ttip_position: 'top-right',\n\t\t\t\tsettings: {},\n\t\t\t};\n\n\t\t\t// check the field condition if set\n\t\t\tif ( orig_field.condition && false === _.bind( orig_field.condition, this.model )() ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tswitch ( orig_field.type ) {\n\n\t\t\t\tcase 'audio_embed':\n\t\t\t\t\tdefaults.classes.push( 'llms-editable-audio' );\n\t\t\t\t\tdefaults.placeholder = 'https://';\n\t\t\t\t\tdefaults.tip         = LLMS.l10n.translate( 'Use SoundCloud or Spotify audio URLS.' );\n\t\t\t\t\tdefaults.input_type  = 'url';\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'datepicker':\n\t\t\t\t\tdefaults.classes.push( 'llms-editable-date' );\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'editor':\n\t\t\t\tcase 'switch-editor':\n\t\t\t\t\tvar orig_settings = orig_field.settings || {};\n\t\t\t\t\tdefaults.settings = $.extend( true, _.getEditor().getDefaultSettings(), {\n\t\t\t\t\t\tmediaButtons: true,\n\t\t\t\t\t\ttinymce: {\n\t\t\t\t\t\t\ttoolbar1: 'bold,italic,strikethrough,bullist,numlist,blockquote,hr,alignleft,aligncenter,alignright,link,unlink,wp_adv',\n\t\t\t\t\t\t\ttoolbar2: 'formatselect,underline,alignjustify,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',\n\t\t\t\t\t\t}\n\t\t\t\t\t}, orig_settings );\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'number':\n\t\t\t\tcase 'switch-number':\n\t\t\t\t\tdefaults.input_type = 'number';\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'permalink':\n\t\t\t\t\tdefaults.label = LLMS.l10n.translate( 'Permalink' );\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'video_embed':\n\t\t\t\t\tdefaults.classes.push( 'llms-editable-video' );\n\t\t\t\t\tdefaults.placeholder = 'https://';\n\t\t\t\t\tdefaults.tip         = LLMS.l10n.translate( 'Use YouTube, Vimeo, or Wistia video URLS.' );\n\t\t\t\t\tdefaults.input_type  = 'url';\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\tif ( this.has_switch( orig_field.type ) ) {\n\t\t\t\tdefaults.switch_on  = 'yes';\n\t\t\t\tdefaults.switch_off = 'no';\n\t\t\t}\n\n\t\t\tvar field = _.defaults( _.deepClone( orig_field ), defaults );\n\n\t\t\t// if options is a function run it\n\t\t\tif ( _.isFunction( field.options ) ) {\n\t\t\t\tfield.options = _.bind( field.options, this.model )();\n\t\t\t}\n\n\t\t\t// if it's a radio field options values can be submitted as images\n\t\t\t// this will transform those images into <img> html\n\t\t\tif ( -1 !== [ 'radio', 'switch-radio' ].indexOf( orig_field.type ) ) {\n\n\t\t\t\tvar has_images = false;\n\t\t\t\t_.each( orig_field.options, function( val, key ) {\n\t\t\t\t\tif ( -1 !== val.indexOf( '.png' ) || -1 !== val.indexOf( '.jpg' ) ) {\n\t\t\t\t\t\tfield.options[key] = '<span><img src=\"' + val + '\"></span>';\n\t\t\t\t\t\thas_images         = true;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\tif ( has_images ) {\n\t\t\t\t\tfield.classes.push( 'has-images' );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// transform classes array to a css class string\n\t\t\tif ( field.classes.length ) {\n\t\t\t\tfield.classes = ' ' + field.classes.join( ' ' );\n\t\t\t}\n\n\t\t\tthis.fields[ field.id ] = field;\n\n\t\t\treturn field;\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if toggling a switch select should rerender the view\n\t\t *\n\t\t * @param    string   field_type  field type string\n\t\t * @return   boolean\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tshould_rerender_on_toggle: function( field_type ) {\n\n\t\t\treturn ( -1 !== field_type.indexOf( 'switch-' ) ) ? 'yes' : 'no';\n\n\t\t},\n\n\t\t/**\n\t\t * Click event for toggling visibility of settings groups\n\t\t * If localStorage is available, persist state\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\ttoggle_group: function( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tvar $el    = $( event.currentTarget ),\n\t\t\t\t$group = $el.closest( '.llms-model-settings' );\n\n\t\t\t$group.toggleClass( 'hidden' );\n\n\t\t\tif ( 'undefined' !== window.localStorage ) {\n\n\t\t\t\tvar id = $group.attr( 'id' );\n\t\t\t\tif ( $group.hasClass( 'hidden' ) ) {\n\t\t\t\t\twindow.localStorage.setItem( id, 'hidden' );\n\t\t\t\t} else {\n\t\t\t\t\twindow.localStorage.removeItem( id );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t},\n\n\t} ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Sidebar.js",
    "content": "/**\n * Main sidebar view\n *\n * @since 3.16.0\n * @version 7.2.0\n */\ndefine( [\n\t'Views/Editor',\n\t'Views/Elements',\n\t'Views/Utilities',\n\t'Views/_Subview'\n], function(\n\tEditor,\n\tElements,\n\tUtilities,\n\tSubview\n) {\n\n\treturn Backbone.View.extend( _.defaults( {\n\n\t\t/**\n\t\t * Current builder state\n\t\t * @type  {String}\n\t\t */\n\t\tstate: 'builder', // [builder|editor]\n\n\t\t/**\n\t\t * Current Subviews\n\t\t * @type  {Object}\n\t\t */\n\t\tviews: {\n\t\t\telements: {\n\t\t\t\tclass: Elements,\n\t\t\t\tinstance: null,\n\t\t\t\tstate: 'builder',\n\t\t\t},\n\t\t\tutilities: {\n\t\t\t\tclass: Utilities,\n\t\t\t\tinstance: null,\n\t\t\t\tstate: 'builder',\n\t\t\t},\n\t\t\teditor: {\n\t\t\t\tclass: Editor,\n\t\t\t\tinstance: null,\n\t\t\t\tstate: 'editor',\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * HTML element selector\n\t\t * @type  {String}\n\t\t */\n\t\tel: '#llms-builder-sidebar',\n\n\t\t/**\n\t\t * DOM events\n\t\t * @type  {Object}\n\t\t */\n\t\tevents: {\n\t\t\t'click #llms-save-button': 'save_now',\n\t\t\t'click #llms-exit-button': 'exit_now',\n\t\t\t'click .llms-builder-error': 'clear_errors',\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'aside',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-sidebar-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function( data ) {\n\n\t\t\t// save a reference to the main Course view\n\t\t\tthis.CourseView = data.CourseView;\n\n\t\t\tthis.render();\n\n\t\t\tBackbone.pubSub.on( 'current-save-status', this.changes_made, this );\n\n\t\t\tBackbone.pubSub.on( 'heartbeat-send', this.heartbeat_send, this );\n\t\t\tBackbone.pubSub.on( 'heartbeat-tick', this.heartbeat_tick, this );\n\n\t\t\tBackbone.pubSub.on( 'lesson-selected', this.on_lesson_select, this );\n\t\t\tBackbone.pubSub.on( 'sidebar-editor-close', this.on_editor_close, this );\n\n\t\t\tthis.$saveButton = $( '#llms-save-button' );\n\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t * @return   self (for chaining)\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender: function( view_data ) {\n\n\t\t\tview_data = view_data || {};\n\n\t\t\tthis.$el.html( this.template() );\n\n\t\t\tthis.render_subviews( _.extend( view_data, {\n\t\t\t\tSidebarView: this,\n\t\t\t} ) );\n\n\t\t\tvar $el = $( '.wrap.lifterlms.llms-builder' );\n\t\t\tif ( 'builder' === this.state ) {\n\t\t\t\t$el.removeClass( 'editor-active' );\n\t\t\t} else {\n\t\t\t\t$el.addClass( 'editor-active' );\n\t\t\t}\n\n\t\t\tthis.$saveButton = this.$el.find( '#llms-save-button' );\n\n\t\t\treturn this;\n\n\t\t},\n\n\t\t/**\n\t\t * Adds error message element\n\t\t * @param    {[type]}   $err  [description]\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tadd_error: function( $err ) {\n\n\t\t\tthis.$el.find( '.llms-builder-save' ).prepend( $err );\n\n\t\t},\n\n\t\t/**\n\t\t * Clear any existing error message elements\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tclear_errors: function() {\n\n\t\t\tthis.$el.find( '.llms-builder-save .llms-builder-error' ).remove();\n\n\t\t},\n\n\t\t/**\n\t\t * Update save status button when changes are detected\n\t\t * runs on an interval to check status of course regularly for unsaved changes\n\t\t * @param    obj   sync  instance of the sync controller\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tchanges_made: function( sync ) {\n\n\t\t\t// if a save is currently running, don't do anything\n\t\t\tif ( sync.saving ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( sync.has_unsaved_changes ) {\n\n\t\t\t\tthis.$saveButton.attr( 'data-status', 'unsaved' );\n\t\t\t\tthis.$saveButton.removeAttr( 'disabled' );\n\n\t\t\t} else {\n\n\t\t\t\tthis.$saveButton.attr( 'data-status', 'saved' );\n\t\t\t\tthis.$saveButton.attr( 'disabled', 'disabled' );\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Exit the builder and return to the WP Course Editor\n\t\t * @return   void\n\t\t * @since    3.16.7\n\t\t * @version  3.16.7\n\t\t */\n\t\texit_now: function() {\n\n\t\t\twindow.location.href = window.llms_builder.CourseModel.get_edit_post_link();\n\n\t\t},\n\n\t\t/**\n\t\t * Triggered when a heartbeat send event starts containing builder information\n\t\t * @param    obj   sync  instance of the sync controller\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\theartbeat_send: function( sync ) {\n\n\t\t\tif ( sync.saving ) {\n\t\t\t\tLLMS.Spinner.start( this.$saveButton.find( 'i' ), 'small' );\n\t\t\t\tthis.$saveButton.attr( {\n\t\t\t\t\t'data-status': 'saving',\n\t\t\t\t\tdisabled: 'disabled',\n\t\t\t\t} );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Triggered when a heartbeat tick completes and updates save status or appends errors\n\t\t * @param    obj   sync  instance of the sync controller\n\t\t * @param    obj   data  updated data\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\theartbeat_tick: function( sync, data ) {\n\n\t\t\tif ( ! sync.saving ) {\n\n\t\t\t\tvar status = 'saved';\n\n\t\t\t\tthis.clear_errors();\n\n\t\t\t\tif ( 'error' === data.status ) {\n\n\t\t\t\t\tstatus = 'error';\n\n\t\t\t\t\tvar msg = data.message,\n\t\t\t\t\t\t$err = $( '<ol class=\"llms-builder-error\" />' );\n\n\t\t\t\t\tif ( 'object' === typeof msg ) {\n\t\t\t\t\t\t_.each( msg, function( txt ) {\n\t\t\t\t\t\t\t$err.append( '<li>' + txt + '</li>' );\n\t\t\t\t\t\t} );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$err = $err.append( '<li>' + msg + '</li>' );;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.add_error( $err );\n\n\t\t\t\t}\n\n\t\t\t\tthis.$saveButton.find( '.llms-spinning' ).remove();\n\t\t\t\tthis.$saveButton.attr( {\n\t\t\t\t\t'data-status': status,\n\t\t\t\t\tdisabled: 'disabled',\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if the editor is the currently active state\n\t\t * @return   boolean\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tis_editor_active: function() {\n\n\t\t\treturn ( 'editor' === this.state );\n\n\t\t},\n\n\t\t/**\n\t\t * Triggered when the editor closes, updates state to be the course builder view\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ton_editor_close: function() {\n\n\t\t\tthis.set_state( 'builder' ).render();\n\n\t\t},\n\n\t\t/**\n\t\t * When a lesson is selected, opens the sidebar to the editor view\n\t\t * @param    obj   lesson_model  instance of the lesson model which was selected\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ton_lesson_select: function( lesson_model, tab ) {\n\n\t\t\tif ( 'editor' !== this.state ) {\n\t\t\t\tthis.set_state( 'editor' );\n\t\t\t} else {\n\t\t\t\tthis.remove_subview( 'editor' );\n\t\t\t}\n\n\t\t\tthis.render( {\n\t\t\t\tmodel: lesson_model,\n\t\t\t\ttab: tab,\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Save button click event\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tsave_now: function() {\n\n\t\t\twindow.llms_builder.sync.save_now();\n\n\t\t},\n\n\t}, Subview ) );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/Utilities.js",
    "content": "/**\n * Sidebar Utilities View\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn Backbone.View.extend( {\n\n\t\t/**\n\t\t * HTML element selector\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\tel: '#llms-utilities',\n\n\t\tevents: {\n\t\t\t'click #llms-collapse-all': 'collapse_all',\n\t\t\t'click #llms-expand-all': 'expand_all'\n\t\t},\n\n\t\t/**\n\t\t * Wrapper Tag name\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\ttagName: 'div',\n\n\t\t/**\n\t\t * Get the underscore template\n\t\t *\n\t\t * @type  {[type]}\n\t\t */\n\t\ttemplate: wp.template( 'llms-utilities-template' ),\n\n\t\t/**\n\t\t * Initialization callback func (renders the element on screen)\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinitialize: function() {\n\n\t\t\t// this.render();\n\t\t},\n\n\t\t/**\n\t\t * Compiles the template and renders the view\n\t\t *\n\t\t * @return   self (for chaining)\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender: function() {\n\t\t\tthis.$el.html( this.template() );\n\t\t\treturn this;\n\t\t},\n\n\t\t/**\n\t\t * Collapse all sections\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tcollapse_all: function( event ) {\n\t\t\tevent.preventDefault();\n\t\t\tBackbone.pubSub.trigger( 'collapse-all' );\n\t\t},\n\n\t\t/**\n\t\t * Expand all sections\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\texpand_all: function( event ) {\n\t\t\tevent.preventDefault();\n\t\t\tBackbone.pubSub.trigger( 'expand-all' );\n\t\t},\n\n\t} );\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/_Detachable.js",
    "content": "/**\n * Detachable model\n *\n * @package LifterLMS/Scripts\n *\n * @since    3.16.12\n * @version  3.16.12\n */\n\ndefine( [], function() {\n\n\treturn {\n\n\t\t/**\n\t\t * DOM Events\n\t\t *\n\t\t * @type  {Object}\n\t\t * @since    3.16.12\n\t\t * @version  3.16.12\n\t\t */\n\t\tevents: {\n\t\t\t'click a[href=\"#llms-detach-model\"]': 'detach_model',\n\t\t\t'click button.llms-detach-model': 'detach_model',\n\t\t},\n\n\t\t/**\n\t\t * Detaches a model from it's parent (doesn't delete)\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.12\n\t\t * @version  3.16.12\n\t\t */\n\t\tdetach_model: function( event ) {\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tevent.stopPropagation();\n\t\t\t}\n\n\t\t\tvar msg = LLMS.l10n.replace( 'Are you sure you want to detach this %s?', {\n\t\t\t\t'%s': this.model.get_l10n_type(),\n\t\t\t} );\n\n\t\t\tif ( window.confirm( msg ) ) {\n\n\t\t\t\tif ( this.model.collection ) {\n\t\t\t\t\tthis.model.collection.remove( this.model );\n\t\t\t\t}\n\n\t\t\t\t// publish global event\n\t\t\t\tBackbone.pubSub.trigger( 'model-detached', this.model );\n\n\t\t\t\t// trigger local event so extending views can run other actions where necessary\n\t\t\t\tthis.trigger( 'model-trashed', this.model );\n\n\t\t\t}\n\n\t\t},\n\n\t}\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/_Editable.js",
    "content": "/**\n * Handles UX and Events for inline editing of views\n *\n * Use with a Model's View\n * Allows editing model.title field via .llms-editable-title elements\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.16.0\n * @since 3.25.4 Unknown\n * @since 3.37.11 Replace reference to `wp.editor` with `_.getEditor()` helper.\n * @since 10.0.0 Add paste event handler for plain contenteditable elements to strip formatting. Fixes #3057.\n * @version 10.0.0\n */\ndefine( [], function() {\n\n\treturn {\n\n\t\tmedia_lib: null,\n\n\t\t/**\n\t\t * DOM Events\n\t\t *\n\t\t * @type  {Object}\n\t\t * @since    3.16.0\n\t\t * @version  3.17.8\n\t\t */\n\t\tevents: {\n\t\t\t'click .llms-add-image': 'open_media_lib',\n\t\t\t'click a[href=\"#llms-edit-slug\"]': 'make_slug_editable',\n\t\t\t'click a[href=\"#llms-remove-image\"]': 'remove_image',\n\t\t\t'change .llms-editable-select select': 'on_select',\n\t\t\t'change .llms-switch input[type=\"checkbox\"]': 'toggle_switch',\n\t\t\t'change .llms-editable-radio input': 'on_radio_select',\n\t\t\t'focusin .llms-input': 'on_focus',\n\t\t\t'focusout .llms-input': 'on_blur',\n\t\t\t'keydown .llms-input': 'on_keydown',\n\t\t\t'input .llms-input[type=\"number\"]': 'on_blur',\n\t\t\t'paste .llms-input[data-formatting]': 'on_paste',\n\t\t\t'paste .llms-input[contenteditable]:not([data-formatting])': 'on_paste',\n\t\t},\n\n\t\t/**\n\t\t * Retrieve a list of allowed tags for a given element\n\t\t *\n\t\t * @param    obj   $el  jQuery selector for the element\n\t\t * @return   array\n\t\t * @since    3.16.0\n\t\t * @version  3.17.8\n\t\t */\n\t\tget_allowed_tags: function( $el ) {\n\n\t\t\tif ( $el.attr( 'data-formatting' ) ) {\n\t\t\t\treturn _.map( $el.attr( 'data-formatting' ).split( ',' ), function( tag ) {\n\t\t\t\t\treturn tag.trim();\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn [ 'b', 'i', 'u', 'strong', 'em' ];\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the content of an element\n\t\t *\n\t\t * @param    obj   $el  jQuery object of the element\n\t\t * @return   string\n\t\t * @since    3.16.0\n\t\t * @version  3.17.8\n\t\t */\n\t\tget_content: function( $el ) {\n\n\t\t\tif ( 'INPUT' === $el[0].tagName ) {\n\t\t\t\treturn $el.val();\n\t\t\t}\n\n\t\t\tif ( ! $el.attr( 'data-formatting' ) && ! $el.hasClass( 'ql-editor' ) ) {\n\t\t\t\treturn $el.text();\n\t\t\t}\n\n\t\t\treturn _.stripFormatting( $el.html(), this.get_allowed_tags( $el ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Determine if changes have been made to the element\n\t\t *\n\t\t * @param    {[obj]}   event  js event object\n\t\t * @return   {Boolean}        true when changes have been made, false otherwise\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\thas_changed: function( event ) {\n\t\t\tvar $el = $( event.target );\n\t\t\treturn ( $el.attr( 'data-original-content' ) !== this.get_content( $el ) );\n\t\t},\n\n\t\t/**\n\t\t * Ensure that new content is at least 1 character long\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   boolean\n\t\t * @since    3.16.0\n\t\t * @version  3.17.2\n\t\t */\n\t\tis_valid: function( event ) {\n\n\t\t\tvar self    = this,\n\t\t\t\t$el     = $( event.target ),\n\t\t\t\tcontent = this.get_content( $el ),\n\t\t\t\ttype    = $el.attr( 'data-type' );\n\n\t\t\tif ( ( $el.attr( 'required' ) || $el.attr( 'data-required' ) ) && content.length < 1 ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif ( 'url' === type || 'video' === type ) {\n\t\t\t\tif ( ! this._validate_url( this.get_content( $el ) ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t} else if ( 'permalink' === type ) {\n\n\t\t\t\tLLMS.Ajax.call( {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\taction: 'llms_builder',\n\t\t\t\t\t\taction_type: 'get_permalink',\n\t\t\t\t\t\tcourse_id: window.llms_builder.CourseModel.get( 'id' ),\n\t\t\t\t\t\tid: self.model.get( 'id' ),\n\t\t\t\t\t\ttitle: self.model.get( 'title' ),\n\t\t\t\t\t\tslug: content,\n\t\t\t\t\t},\n\t\t\t\t\tbeforeSend: function() {\n\t\t\t\t\t\tLLMS.Spinner.start( $el.closest( '.llms-editable-toggle-group' ), 'small' );\n\t\t\t\t\t},\n\t\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\t\tif ( r.permalink && r.slug ) {\n\t\t\t\t\t\t\tself.model.set( 'permalink', r.permalink );\n\t\t\t\t\t\t\tself.model.set( 'name', r.slug );\n\t\t\t\t\t\t\tself.render();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\treturn true;\n\n\t\t},\n\n\t\t/**\n\t\t * Initialize datepicker elements\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.17.0\n\t\t * @version  3.17.0\n\t\t */\n\t\tinit_datepickers: function() {\n\n\t\t\tthis.$el.find( '.llms-editable-date input' ).each( function() {\n\n\t\t\t\t$( this ).datetimepicker( {\n\t\t\t\t\tformat: $( this ).attr( 'data-date-format' ) || 'Y-m-d h:i A',\n\t\t\t\t\tdatepicker: ( undefined === $( this ).attr( 'data-date-datepicker' ) ) ? true : ( 'true' == $( this ).attr( 'data-date-datepicker' ) ),\n\t\t\t\t\ttimepicker: ( undefined === $( this ).attr( 'data-date-timepicker' ) ) ? true : ( 'true' == $( this ).attr( 'data-date-timepicker' ) ),\n\t\t\t\t\tonClose: function( current_time, $input ) {\n\t\t\t\t\t\t$input.blur();\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Initialize elements that allow inline formatting\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tinit_formatting_els: function() {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.$el.find( '.llms-input-formatting[data-formatting]' ).each( function() {\n\n\t\t\t\tvar formatting = $( this ).attr( 'data-formatting' ).split( ',' ),\n\t\t\t\t\tattr       = $( this ).attr( 'data-attribute' );\n\n\t\t\t\tvar ed = new Quill( this, {\n\t\t\t\t\tmodules: {\n\t\t\t\t\t\ttoolbar: [ formatting ],\n\t\t\t\t\t\tkeyboard: {\n\t\t\t\t\t\t\tbindings: {\n\t\t\t\t\t\t\t\ttab: {\n\t\t\t\t\t\t\t\t\tkey: 9,\n\t\t\t\t\t\t\t\t\thandler: function( range, context ) {\n\t\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t13: {\n\t\t\t\t\t\t\t\t\tkey: 13,\n\t\t\t\t\t\t\t\t\thandler: function( range, context ) {\n\t\t\t\t\t\t\t\t\t\ted.root.blur();\n\t\t\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tplaceholder: $( this ).attr( 'data-placeholder' ),\n\t\t\t\t\ttheme: 'bubble',\n\t\t\t\t} );\n\n\t\t\t\ted.on( 'text-change', function( delta, oldDelta, source ) {\n\t\t\t\t\tself.model.set( attr, self.get_content( $( ed.root ) ) );\n\t\t\t\t} );\n\n\t\t\t\tBackbone.pubSub.trigger( 'formatting-ed-init', ed, $( this ), self );\n\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Initialize editable select elements\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.25.4\n\t\t */\n\t\tinit_selects: function() {\n\n\t\t\tthis.$el.find( '.llms-editable-select select' ).llmsSelect2( {\n\t\t\t\twidth: '100%',\n\t\t\t} ).trigger( 'change' );\n\n\t\t},\n\n\t\t/**\n\t\t * Blur/focusout function for .llms-editable-title elements\n\t\t * Automatically saves changes if changes have been made\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.6\n\t\t */\n\t\ton_blur: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\n\t\t\tthis.model.set( '_has_focus', false, { silent: true } );\n\n\t\t\tvar self    = this,\n\t\t\t\t$el     = $( event.target ),\n\t\t\t\tchanged = this.has_changed( event );\n\n\t\t\tif ( changed ) {\n\n\t\t\t\tif ( ! self.is_valid( event ) ) {\n\t\t\t\t\tself.revert_edits( event );\n\t\t\t\t} else {\n\t\t\t\t\tthis.save_edits( event );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Focus event for editable inputs\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.6\n\t\t * @version  3.16.6\n\t\t */\n\t\ton_focus: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\t\t\tthis.model.set( '_has_focus', true, { silent: true } );\n\n\t\t},\n\n\t\t/**\n\t\t * Handle content pasted into contenteditable fields\n\t\t * This will ensure that HTML from RTF editors isn't pasted into the dom\n\t\t *\n\t\t * @param    obj   event  js event obj\n\t\t * @return   void\n\t\t * @since    3.17.8\n\t\t * @version  3.17.8\n\t\t */\n\t\ton_paste: function( event ) {\n\n\t\t\tevent.preventDefault();\n\t\t\tevent.stopPropagation();\n\n\t\t\tvar text = ( event.originalEvent || event ).clipboardData.getData( 'text/plain' );\n\t\t\twindow.document.execCommand( 'insertText', false, text );\n\n\t\t},\n\n\t\t/**\n\t\t * Change event for selectables\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ton_select: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\n\t\t\tvar $el       = $( event.target ),\n\t\t\t\tmulti     = ( $el.attr( 'multiple' ) ),\n\t\t\t\tattr      = $el.attr( 'name' ),\n\t\t\t\t$selected = $el.find( 'option:selected' ),\n\t\t\t\tval;\n\n\t\t\tif ( multi ) {\n\t\t\t\tval = [];\n\t\t\t\tval = $selected.map( function() {\n\t\t\t\t\treturn this.value;\n\t\t\t\t} ).get();\n\t\t\t} else {\n\t\t\t\tval = $selected[0].value;\n\t\t\t}\n\n\t\t\tthis.model.set( attr, val );\n\n\t\t},\n\n\t\t/**\n\t\t * Change event for radio element groups\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.17.6\n\t\t * @version  3.17.6\n\t\t */\n\t\ton_radio_select: function( event ) {\n\n\t\t\tvar $el  = $( event.target ),\n\t\t\t\tattr = $el.attr( 'name' ),\n\t\t\t\tval  = $el.val();\n\n\t\t\tthis.model.set( attr, val );\n\n\t\t},\n\n\t\t/**\n\t\t * Keydown function for .llms-editable-title elements\n\t\t * Blurs\n\t\t *\n\t\t * @param    {obj}   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.17.8\n\t\t */\n\t\ton_keydown: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\n\t\t\tvar self  = this,\n\t\t\t\tkey   = event.which || event.keyCode,\n\t\t\t\tshift = event.shiftKey;\n\t\t\t\t// ctrl = event.metaKey || event.ctrlKey;\n\n\t\t\tswitch ( key ) {\n\n\t\t\t\tcase 13: // enter\n\t\t\t\t\t// shift + enter should add a return\n\t\t\t\t\tif ( ! shift ) {\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\tevent.target.blur();\n\t\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\t\tcase 27: // escape\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tthis.revert_edits( event );\n\t\t\t\t\tevent.target.blur();\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Open the WP media lib\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.6\n\t\t */\n\t\topen_media_lib: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\n\t\t\tvar self = this,\n\t\t\t\t$el  = $( event.currentTarget );\n\n\t\t\tif ( self.media_lib ) {\n\n\t\t\t\tself.media_lib.uploader.uploader.param( 'post_id' );\n\n\t\t\t} else {\n\n\t\t\t\tself.media_lib = wp.media.frames.file_frame = wp.media( {\n\t\t\t\t\ttitle: LLMS.l10n.translate( 'Select an image' ),\n\t\t\t\t\tbutton: {\n\t\t\t\t\t\ttext: LLMS.l10n.translate( 'Use this image' ),\n\t\t\t\t\t},\n\t\t\t\t\tmultiple: false\t// Set to true to allow multiple files to be selected\n\t\t\t\t} );\n\n\t\t\t\tself.media_lib.on( 'select', function() {\n\n\t\t\t\t\tvar size       = $el.attr( 'data-image-size' ),\n\t\t\t\t\t\tattachment = self.media_lib.state().get( 'selection' ).first().toJSON(),\n\t\t\t\t\t\timage      = self.model.get( $el.attr( 'data-attribute' ) ),\n\t\t\t\t\t\turl;\n\n\t\t\t\t\tif ( size && attachment.sizes[ size ] ) {\n\t\t\t\t\t\turl = attachment.sizes[ size ].url;\n\t\t\t\t\t} else {\n\t\t\t\t\t\turl = attachment.url;\n\t\t\t\t\t}\n\n\t\t\t\t\timage.set( {\n\t\t\t\t\t\tid: attachment.id,\n\t\t\t\t\t\tsrc: url,\n\t\t\t\t\t} );\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t\t// This flag is used to protect the media files uploaded via places like the Quiz questions (Picture type)\n\t\t\tself.media_lib.uploader.options.uploader.params.llms = 1;\n\n\t\t\tself.media_lib.open();\n\n\t\t},\n\n\t\t/**\n\t\t * Click event to remove an image\n\t\t *\n\t\t * @param    obj   event  js event obj\n\t\t * @return   voids\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tremove_image: function( event ) {\n\n\t\t\tevent.preventDefault();\n\n\t\t\tthis.model.get( $( event.currentTarget ).attr( 'data-attribute' ) ).set( {\n\t\t\t\tid: '',\n\t\t\t\tsrc: '',\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Helper to undo changes\n\t\t * Bound to \"escape\" key via on_keydown function\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trevert_edits: function( event ) {\n\t\t\tvar $el = $( event.target ),\n\t\t\t\tval = $el.attr( 'data-original-content' );\n\t\t\t$el.html( val );\n\t\t},\n\n\t\t/**\n\t\t * Sync changes to the model and DB\n\t\t *\n\t\t * @param    {obj}   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tsave_edits: function( event ) {\n\n\t\t\tvar $el = $( event.target ),\n\t\t\t\tval = this.get_content( $el );\n\n\t\t\tthis.model.set( $el.attr( 'data-attribute' ), val );\n\n\t\t},\n\n\t\t/**\n\t\t * Change event for a switch element\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.17.0\n\t\t */\n\t\ttoggle_switch: function( event ) {\n\n\t\t\tevent.stopPropagation();\n\t\t\tvar $el      = $( event.target ),\n\t\t\t\tattr     = $el.attr( 'name' ),\n\t\t\t\trerender = $el.attr( 'data-rerender' ),\n\t\t\t\tval;\n\n\t\t\tif ( $el.is( ':checked' ) ) {\n\t\t\t\tval = $el.attr( 'data-on' ) ? $el.attr( 'data-on' ) : 'yes';\n\t\t\t} else {\n\t\t\t\tval = $el.attr( 'data-off' ) ? $el.attr( 'data-off' ) : 'no';\n\t\t\t}\n\n\t\t\tif ( -1 !== attr.indexOf( '.' ) ) {\n\n\t\t\t\tvar split = attr.split( '.' );\n\n\t\t\t\tif ( 'parent' === split[0] ) {\n\t\t\t\t\tthis.model.get_parent().set( split[1], val );\n\t\t\t\t} else {\n\t\t\t\t\tthis.model.get( split[0] ).set( split[1], val );\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tthis.model.set( attr, val );\n\n\t\t\t}\n\n\t\t\tthis.trigger( attr.replace( '.', '-' ) + '_toggle', val );\n\n\t\t\tif ( ! rerender || 'yes' === rerender ) {\n\t\t\t\tvar self = this;\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\tself.render();\n\t\t\t\t}, 100 );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Initializes a WP Editor on a textarea\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.37.11 Replace reference to `wp.editor` with `_.getEditor()` helper.\n\t\t *\n\t\t * @param {String} id        CSS ID of the editor (don't include #).\n\t\t * @param {Object} settings  Optional object of settings to pass to wp.oldEditor.initialize().\n\t\t * @return {Void}\n\t\t */\n\t\tinit_editor: function( id, settings ) {\n\n\t\t\tsettings = settings || {};\n\n\t\t\tvar editor = _.getEditor();\n\n\t\t\teditor.remove( id );\n\n\t\t\teditor.initialize( id, $.extend( true, editor.getDefaultSettings(), {\n\t\t\t\tmediaButtons: true,\n\t\t\t\ttinymce: {\n\t\t\t\t\ttoolbar1: 'bold,italic,strikethrough,bullist,numlist,blockquote,hr,alignleft,aligncenter,alignright,link,unlink,wp_adv',\n\t\t\t\t\ttoolbar2: 'formatselect,underline,alignjustify,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',\n\t\t\t\t\tsetup: _.bind( this.on_editor_ready, this ),\n\t\t\t\t}\n\t\t\t}, settings ) );\n\n\t\t},\n\n\t\t/**\n\t\t * Setup a permalink editor to allow editing of a permalink\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.6\n\t\t * @version  3.16.6\n\t\t */\n\t\tmake_slug_editable: function( event ) {\n\n\t\t\tvar self      = this,\n\t\t\t\t$btn      = $( event.currentTarget ),\n\t\t\t\t$link     = $btn.prevAll( 'a' ),\n\t\t\t\t$input    = $btn.prev( 'input.permalink' ),\n\t\t\t\tfull_url  = $link.attr( 'href' ),\n\t\t\t\tslug      = $input.val(),\n\t\t\t\tshort_url = full_url.replace( slug, '' );\n\n\t\t\t// hide the button\n\t\t\t$btn.hide();\n\n\t\t\t// make the link not clickable\n\t\t\t$link.css( {\n\t\t\t\tcolor: '#999',\n\t\t\t\t'pointer-events': 'none',\n\t\t\t\t'text-decoration': 'none',\n\t\t\t} );\n\n\t\t\t// remove the current slug & trailing slash from the URL\n\t\t\t$link.text( short_url.substring( 0, short_url.length - 1 ) );\n\n\t\t\t// focus in on the field\n\t\t\t$input.show().focus();\n\n\t\t},\n\n\t\t/**\n\t\t * Callback function called after initialization of an editor\n\t\t *\n\t\t * Updates UI if a label is present.\n\t\t *\n\t\t * Binds a change event to ensure editor changes are saved to the model.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.17.1 Uknown.\n\t\t * @since 3.37.11 Replace references to `wp.editor` with `_.getEditor()` helper.\n\t\t *\n\t\t * @param {Object} editor TinyMCE Editor instance.\n\t\t * @return {Void}\n\t\t */\n\t\ton_editor_ready: function( editor ) {\n\n\t\t\tvar self    = this,\n\t\t\t\t$ed     = $( '#' + editor.id ),\n\t\t\t\t$parent = $ed.closest( '.llms-editable-editor' ),\n\t\t\t\t$label  = $parent.find( '.llms-label' ),\n\t\t\t\tprop    = $ed.attr( 'data-attribute' )\n\n\t\t\tif ( $label.length ) {\n\t\t\t\t$label.prependTo( $parent.find( '.wp-editor-tools' ) );\n\t\t\t}\n\n\t\t\t// save changes to the model via Visual ed\n\t\t\teditor.on( 'change', function( event ) {\n\t\t\t\tself.model.set( prop, _.getEditor().getContent( editor.id ) );\n\t\t\t} );\n\n\t\t\t// save changes via Text ed\n\t\t\t$ed.on( 'input', function( event ) {\n\t\t\t\tself.model.set( prop, $ed.val() );\n\t\t\t} );\n\n\t\t\t// trigger an input on the Text ed when quicktags buttons are clicked\n\t\t\t$parent.on( 'click', '.quicktags-toolbar .ed_button', function() {\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t$ed.trigger( 'input' );\n\t\t\t\t}, 10 );\n\t\t\t} );\n\n\t\t},\n\n\t\t_validate_url: function( str ) {\n\n\t\t\tvar a  = document.createElement( 'a' );\n\t\t\ta.href = str;\n\t\t\treturn ( a.host && a.host !== window.location.host );\n\n\t\t}\n\n\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/_Receivable.js",
    "content": "/**\n * _receive override for Backbone.CollectionView core\n * enables connection with jQuery UI draggable buttons\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn {\n\n\t\t/**\n\t\t * Overloads the function from Backbone.CollectionView core because it doesn't properly handle\n\t\t * receives from a jQuery UI draggable object\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @param    obj   ui     jQuery UI object\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\t_receive : function( event, ui ) {\n\n\t\t\t// came from sidebar drag\n\t\t\tif ( ui.sender.hasClass( 'ui-draggable' ) ) {\n\t\t\t\tvar index = this._getContainerEl().children().index( ui.helper );\n\t\t\t\tui.helper.remove(); // remove the helper\n\t\t\t\tthis.collection.add( {}, { at: index } );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar senderListEl             = ui.sender;\n\t\t\tvar senderCollectionListView = senderListEl.data( 'view' );\n\t\t\tif ( ! senderCollectionListView || ! senderCollectionListView.collection ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar newIndex      = this._getContainerEl().children().index( ui.item );\n\t\t\tvar modelReceived = senderCollectionListView.collection.get( ui.item.attr( 'data-model-cid' ) );\n\t\t\tsenderCollectionListView.collection.remove( modelReceived );\n\t\t\tthis.collection.add( modelReceived, { at : newIndex } );\n\t\t\tmodelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.\n\t\t\tthis.setSelectedModel( modelReceived );\n\t\t},\n\n\t}\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/_Shiftable.js",
    "content": "/**\n * Shiftable view mixin function\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn {\n\n\t\t/**\n\t\t * Conditionally hide action buttons based on section position in collection\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tmaybe_hide_shiftable_buttons: function() {\n\n\t\t\tif ( ! this.model.collection ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar type = this.model.get( 'type' );\n\n\t\t\tif ( this.model.collection.first() === this.model ) {\n\t\t\t\tthis.$el.find( '.shift-up--' + type ).hide();\n\t\t\t} else if ( this.model.collection.last() === this.model ) {\n\t\t\t\tthis.$el.find( '.shift-down--' + type ).hide();\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Move an item in a collection from one position to another\n\t\t *\n\t\t * @param    int   old_index  current (old) index within the collection\n\t\t * @param    int   new_index  desired (new) index within the collection\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tshift: function( old_index, new_index ) {\n\n\t\t\tvar collection = this.model.collection;\n\n\t\t\tcollection.remove( this.model );\n\t\t\tcollection.add( this.model, { at: new_index } );\n\t\t\tcollection.trigger( 'reorder' );\n\n\t\t},\n\n\t\t/**\n\t\t * Move an item down the tree one position\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tshift_down: function( e ) {\n\n\t\t\te.preventDefault();\n\t\t\tvar index = this.model.collection.indexOf( this.model );\n\t\t\tthis.shift( index, index + 1 );\n\n\t\t},\n\n\t\t/**\n\t\t * Move an item up the tree one position\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tshift_up: function( e ) {\n\n\t\t\te.preventDefault();\n\t\t\tvar index = this.model.collection.indexOf( this.model );\n\t\t\tthis.shift( index, index - 1 );\n\n\t\t},\n\n\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/_Subview.js",
    "content": "/**\n * Subview utility mixin\n *\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( [], function() {\n\n\treturn {\n\n\t\tsubscriptions: {},\n\n\t\t/**\n\t\t * Name of the current subview\n\t\t *\n\t\t * @type  {String}\n\t\t */\n\t\tstate: '',\n\n\t\t/**\n\t\t * Object of subview data\n\t\t *\n\t\t * @type  {Object}\n\t\t */\n\t\tviews: {},\n\n\t\t/**\n\t\t * Retrieve a subview by name from this.views\n\t\t *\n\t\t * @param    string   name   name of the subview\n\t\t * @return   obl|false\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_subview: function( name ) {\n\n\t\t\tif ( this.views[ name ] ) {\n\t\t\t\treturn this.views[ name ];\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t},\n\n\t\tevents_subscribe: function( events ) {\n\n\t\t\t_.each( events, function( func, event ) {\n\n\t\t\t\tthis.subscriptions[ event ] = func;\n\t\t\t\tBackbone.pubSub.on( event, func, this );\n\n\t\t\t}, this );\n\n\t\t},\n\n\t\tevents_unsubscribe: function() {\n\n\t\t\t_.each( this.subscriptions, function( func, event ) {\n\n\t\t\t\tBackbone.pubSub.off( event, func, this );\n\t\t\t\tdelete this.subscriptions[ event ];\n\n\t\t\t}, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Remove a single subview (and all it's subviews) by name\n\t\t *\n\t\t * @param    string   name   name of the subview\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tremove_subview: function( name ) {\n\n\t\t\tvar view = this.get_subview( name );\n\n\t\t\tif ( ! view ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( view.instance ) {\n\n\t\t\t\t// remove the subviews if the view has subviews\n\t\t\t\tif ( ! _.isEmpty( view.instance.views ) ) {\n\t\t\t\t\tview.instance.events_unsubscribe();\n\t\t\t\t\tview.instance.remove_subviews();\n\t\t\t\t}\n\n\t\t\t\tview.instance.off();\n\t\t\t\tview.instance.off( null, null, null );\n\t\t\t\tview.instance.remove();\n\t\t\t\tview.instance.undelegateEvents();\n\n\t\t\t\t// _.each( view.instance, function( val, key ) {\n\t\t\t\t// delete view.instance[ key ];\n\t\t\t\t// } );\n\n\t\t\t\tview.instance = null;\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Remove all subviews (and all the subviews of those subviews)\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tremove_subviews: function() {\n\n\t\t\t_.each( this.views, function( data, name ) {\n\n\t\t\t\tthis.remove_subview( name );\n\n\t\t\t}, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Render subviews based on current state\n\t\t *\n\t\t * @param    obj   view_data  additional data to pass to the subviews\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender_subviews: function( view_data ) {\n\n\t\t\tview_data = view_data || {};\n\n\t\t\t_.each( this.views, function( data, name ) {\n\n\t\t\t\tif ( this.state === data.state ) {\n\n\t\t\t\t\tthis.render_subview( name, view_data );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthis.remove_subview( name );\n\n\t\t\t\t}\n\n\t\t\t}, this );\n\n\t\t},\n\n\t\t/**\n\t\t * Render a single subview by name\n\t\t *\n\t\t * @param    string   name       name of the subview\n\t\t * @param    obj      view_data  additional data to pass to the subview initializer\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\trender_subview: function( name, view_data ) {\n\n\t\t\tvar view = this.get_subview( name );\n\n\t\t\tif ( ! view ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.remove_subview( name );\n\n\t\t\tif ( ! view.instance ) {\n\t\t\t\tview.instance = new view.class( view_data );\n\t\t\t}\n\n\t\t\tview.instance.render();\n\n\t\t},\n\n\t\t/**\n\t\t * Set the current subview\n\t\t * Must call render after!\n\t\t *\n\t\t * @param    string   state  name of the state [builder|editor]\n\t\t * @return   obj             this for chaining\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tset_state: function ( state ) {\n\n\t\t\tthis.state = state;\n\t\t\treturn this;\n\n\t\t},\n\n\t}\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/_Trashable.js",
    "content": "/**\n * Trashable model\n *\n * @type     {Object}\n * @since    3.16.12\n * @version  3.16.12\n */\ndefine( [], function() {\n\n\treturn {\n\n\t\t/**\n\t\t * DOM Events\n\t\t *\n\t\t * @type  {Object}\n\t\t * @since    3.16.12\n\t\t * @version  3.16.12\n\t\t */\n\t\tevents: {\n\t\t\t'click a[href=\"#llms-trash-model\"]': 'trash_model',\n\t\t\t'click button.llms-trash-model': 'trash_model',\n\t\t},\n\n\t\t/**\n\t\t * Remove a model from it's parent and delete it\n\t\t *\n\t\t * @param    obj   event  js event object\n\t\t * @return   void\n\t\t * @since    3.16.12\n\t\t * @version  3.16.12\n\t\t */\n\t\ttrash_model: function( event ) {\n\n\t\t\tif ( event ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tevent.stopPropagation();\n\t\t\t}\n\n\t\t\tvar msg = LLMS.l10n.replace( 'Are you sure you want to move this %s to the trash?', {\n\t\t\t\t'%s': this.model.get_l10n_type(),\n\t\t\t} );\n\n\t\t\tif ( window.confirm( msg ) ) {\n\n\t\t\t\tif ( this.model.collection ) {\n\t\t\t\t\tthis.model.collection.remove( this.model );\n\t\t\t\t}\n\n\t\t\t\t// publish event\n\t\t\t\tBackbone.pubSub.trigger( 'model-trashed', this.model );\n\n\t\t\t\t// close the editor sidebar if the trashed model is the one currently being edited\n\t\t\t\tif ( this.model.get( '_selected' ) ) {\n\t\t\t\t\tBackbone.pubSub.trigger( 'sidebar-editor-close' );\n\t\t\t\t}\n\n\t\t\t\t// trigger local event so extending views can run other actions where necessary\n\t\t\t\tthis.trigger( 'model-trashed', this.model );\n\n\t\t\t}\n\n\t\t},\n\n\t}\n\n} );\n"
  },
  {
    "path": "assets/js/builder/Views/_loader.js",
    "content": "/**\n * Load view mixins\n *\n * @package LifterLMS/Scripts\n *\n * @since    3.17.1\n * @version  3.17.1\n */\n\ndefine( [\n\t\t'Views/_Detachable',\n\t\t'Views/_Editable',\n\t\t'Views/_Receivable',\n\t\t'Views/_Shiftable',\n\t\t'Views/_Subview',\n\t\t'Views/_Trashable'\n\t],\n\tfunction(\n\t\tDetachable,\n\t\tEditable,\n\t\tReceivable,\n\t\tShiftable,\n\t\tSubview,\n\t\tTrashable\n\t) {\n\n\t\treturn {\n\t\t\tDetachable: Detachable,\n\t\t\tEditable: Editable,\n\t\t\tReceivable: Receivable,\n\t\t\tShiftable: Shiftable,\n\t\t\tSubview: Subview,\n\t\t\tTrashable: Trashable,\n\t\t};\n\n} );\n"
  },
  {
    "path": "assets/js/builder/backbone.js",
    "content": "/**\n * Returns the WordPress-loaded version of Backbone for use with things that need it and use Require.\n *\n * @return   obj\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( function() {\n\treturn Backbone;\n} );\n"
  },
  {
    "path": "assets/js/builder/jquery.js",
    "content": "/**\n * Returns the WordPress-loaded version of Underscore for use with things that need it and use Require.\n *\n * @package LifterLMS/Scripts\n *\n * @since    3.16.0\n * @version  3.16.0\n */\n\ndefine( function() {\n\treturn jQuery;\n} );\n"
  },
  {
    "path": "assets/js/builder/main.js",
    "content": "/**\n * LifterLMS JS Builder App Bootstrap\n *\n * @since 3.16.0\n * @since 3.37.11 Added `_.getEditor()` helper.\n * @version 5.4.0\n */\nrequire( [\n\t'vendor/wp-hooks',\n\t'vendor/backbone.collectionView',\n\t'vendor/backbone.trackit',\n\t'Controllers/Construct',\n\t'Controllers/Debug',\n\t'Controllers/Schemas',\n\t'Controllers/Sync',\n\t'Models/loader',\n\t'Views/Course',\n\t'Views/Sidebar'\n\t], function(\n\tHooks,\n\tCV,\n\tTrackIt,\n\tConstruct,\n\tDebug,\n\tSchemas,\n\tSync,\n\tModels,\n\tCourseView,\n\tSidebarView\n\t) {\n\n\t\twindow.llms_builder.debug     = new Debug( window.llms_builder.debug );\n\t\twindow.llms_builder.construct = new Construct();\n\t\twindow.llms_builder.schemas   = new Schemas( window.llms_builder.schemas );\n\n\t\t/**\n\t\t * Compare values, used by _.checked & _.selected mixins.\n\t\t *\n\t\t * @since 3.17.2\n\t\t *\n\t\t * @param {Mixed} expected expected Value, probably a string (the value of a select option or checkbox element).\n\t\t * @param {Mixed} mixed    actual   Actual value, probably a string (the return of model.get( 'something' ) )\n\t\t *                                  but could be an array like a multiselect.\n\t\t * @return {Bool}\n\t\t */\n\t\tfunction value_compare( expected, actual ) {\n\t\t\treturn ( ( _.isArray( actual ) && -1 !== actual.indexOf( expected ) ) || expected == actual );\n\t\t};\n\n\t\t/**\n\t\t * Underscores templating utilities\n\t\t *\n\t\t * @since    3.17.0\n\t\t * @version  3.27.0\n\t\t */\n\t\t_.mixin( {\n\n\t\t\t/**\n\t\t\t * Determine if two values are equal and output checked attribute if they are.\n\t\t\t *\n\t\t\t * Useful for templating checkboxes & radio elements\n\t\t\t * like WP Core PHP checked() but in JS.\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t * @since 3.17.2 Unknown.\n\t\t\t *\n\t\t\t * @param {Mixed} expected Expected element value.\n\t\t\t * @param {Mixed} actual   Actual element value.\n\t\t\t * @return {String}\n\t\t\t */\n\t\t\tchecked: function( expected, actual ) {\n\t\t\t\tif ( value_compare( expected, actual ) ) {\n\t\t\t\t\treturn ' checked=\"checked\"';\n\t\t\t\t}\n\t\t\t\treturn '';\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Recursively clone an object via _.clone().\n\t\t\t *\n\t\t\t * @since 3.17.7\n\t\t\t *\n\t\t\t * @param {Object} obj Object to clone.\n\t\t\t * @return {Object}\n\t\t\t */\n\t\t\tdeepClone: function( obj ) {\n\n\t\t\t\tvar clone = _.clone( obj );\n\n\t\t\t\t_.each( clone, function( val, key ) {\n\t\t\t\t\tif ( ! _.isFunction( val ) && _.isObject( val ) ) {\n\t\t\t\t\t\tclone[ key ] = _.deepClone( val );\n\t\t\t\t\t};\n\t\t\t\t} );\n\n\t\t\t\treturn clone;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Retrieve the wp.editor instance.\n\t\t\t *\n\t\t\t * Uses `wp.oldEditor` (when available) which was added in WordPress 5.0.\n\t\t\t *\n\t\t\t * Falls back to `wp.editor()` which will usually be the same as `wp.oldEditor` unless\n\t\t\t * the `@wordpress/editor` module has been loaded by another plugin or a theme.\n\t\t\t *\n\t\t\t * @since 3.37.11\n\t\t\t *\n\t\t\t * @return {Object}\n\t\t\t */\n\t\t\tgetEditor: function() {\n\n\t\t\t\tif ( undefined !== wp.oldEditor ) {\n\n\t\t\t\t\tvar ed = wp.oldEditor;\n\n\t\t\t\t\t// Inline scripts added by WordPress are not ported to `wp.oldEditor`, see https://github.com/WordPress/WordPress/blob/641c632b0c9fde4e094b217f50749984ca43a2fa/wp-includes/class-wp-editor.php#L977.\n\t\t\t\t\tif ( undefined !== wp.editor && undefined !== wp.editor.getDefaultSettings ) {\n\t\t\t\t\t\ted.getDefaultSettings = wp.editor.getDefaultSettings;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn ed;\n\n\t\t\t\t} else if ( undefined !== wp.editor && undefined !== wp.editor.autop ){\n\n\t\t\t\t\treturn wp.editor;\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Strips IDs & Parent References from quizzes and all quiz questions.\n\t\t\t *\n\t\t\t * @since 3.24.0\n\t\t\t * @since 3.27.0 Unknown.\n\t\t\t * @since 5.4.0 Use author id instead of the question author object.\n\t\t\t *\n\t\t\t * @param {Object} quiz Raw quiz object (not a model).\n\t\t\t * @return {Object}\n\t\t\t */\n\t\t\tprepareQuizObjectForCloning: function( quiz ) {\n\n\t\t\t\tdelete quiz.id;\n\t\t\t\tdelete quiz.lesson_id;\n\n\t\t\t\t_.each( quiz.questions, function( question ) {\n\n\t\t\t\t\tquestion = _.prepareQuestionObjectForCloning( question );\n\n\t\t\t\t} );\n\n\t\t\t\t// Use author id instead of the quiz author object.\n\t\t\t\tquiz = _.prepareExistingPostObjectDataForAddingOrCloning( quiz );\n\n\t\t\t\treturn quiz;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Strips IDs & Parent References from a question.\n\t\t\t *\n\t\t\t * @since 3.27.0\n\t\t\t * @since 5.4.0 Use author id instead of the question author object.\n\t\t\t *\n\t\t\t * @param {Object} question Raw question object (not a model).\n\t\t\t * @return {Object}\n\t\t\t */\n\t\t\tprepareQuestionObjectForCloning: function( question ) {\n\n\t\t\t\tdelete question.id;\n\t\t\t\tdelete question.parent_id;\n\n\t\t\t\tif ( question.image && _.isObject( question.image ) ) {\n\t\t\t\t\tquestion.image._forceSync = true;\n\t\t\t\t}\n\n\t\t\t\tif ( question.choices ) {\n\n\t\t\t\t\t_.each( question.choices, function( choice ) {\n\n\t\t\t\t\t\tdelete choice.question_id;\n\t\t\t\t\t\tdelete choice.id;\n\t\t\t\t\t\tif ( 'image' === choice.choice_type && _.isObject( choice.choice ) ) {\n\t\t\t\t\t\t\tchoice.choice._forceSync = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t\t// Use author id instead of the question author object.\n\t\t\t\tquestion = _.prepareExistingPostObjectDataForAddingOrCloning( question );\n\n\t\t\t\treturn question;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Strips IDs & Parent References from assignments and all assignment tasks.\n\t\t\t *\n\t\t\t * @since 5.4.0\n\t\t\t *\n\t\t\t * @param {Object} assignment Raw assignment object (not a model).\n\t\t\t * @return {Object}\n\t\t\t */\n\t\t\t prepareAssignmentObjectForCloning: function( assignment ) {\n\n\t\t\t\tdelete assignment.id;\n\t\t\t\tdelete assignment.lesson_id;\n\n\t\t\t\t// Clone tasks.\n\t\t\t\tif ( 'tasklist' === assignment.assignment_type ) {\n\t\t\t\t\t_.each( assignment.tasks, function( task ) {\n\t\t\t\t\t\tdelete task.id;\n\t\t\t\t\t\tdelete task.assignment_id;\n\t\t\t\t\t} );\n\t\t\t\t}\n\n\t\t\t\t// Use author id instead of the quiz author object.\n\t\t\t\tassignment = _.prepareExistingPostObjectDataForAddingOrCloning( assignment );\n\n\t\t\t\treturn assignment;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Prepare post object data for adding or cloning.\n\t\t\t *\n\t\t\t * Use author id instead of the post type author object.\n\t\t\t *\n\t\t\t * @since 5.4.0\n\t\t\t *\n\t\t\t * @param {Object} quiz Raw post object (not a model).\n\t\t\t * @return {Object}\n\t\t\t */\n\t\t\tprepareExistingPostObjectDataForAddingOrCloning: function( post_data ) {\n\n\t\t\t\tif ( post_data.author && _.isObject( post_data.author ) && post_data.author.id ) {\n\t\t\t\t\tpost_data.author = post_data.author.id;\n\t\t\t\t}\n\n\t\t\t\treturn post_data;\n\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Determine if two values are equal and output selected attribute if they are.\n\t\t\t *\n\t\t\t * Useful for templating select elements\n\t\t\t * like WP Core PHP selected() but in JS.\n\t\t\t *\n\t\t\t *\n\t\t\t * @since 3.17.0\n\t\t\t * @since 3.17.2 Unknown.\n\t\t\t *\n\t\t\t * @param {Mixed} expected Expected element value.\n\t\t\t * @param {Mixed} actual   Actual element value.\n\t\t\t * @return {String}\n\t\t\t */\n\t\t\tselected: function( expected, actual ) {\n\t\t\t\tif ( value_compare( expected, actual ) ) {\n\t\t\t\t\treturn ' selected=\"selected\"';\n\t\t\t\t}\n\t\t\t\treturn '';\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Generic function for stripping HTML tags from a string.\n\t\t\t *\n\t\t\t * @since 3.17.8\n\t\t\t *\n\t\t\t * @param {String} content      Raw string.\n\t\t\t * @param {Array}  allowed_tags Array of allowed HTML tags.\n\t\t\t * @return {String}\n\t\t\t */\n\t\t\tstripFormatting: function( content, allowed_tags ) {\n\n\t\t\t\tif ( ! allowed_tags ) {\n\t\t\t\t\tallowed_tags = [ 'b', 'i', 'u', 'strong', 'em' ];\n\t\t\t\t}\n\n\t\t\t\tvar $html = $( '<div>' + content + '</div>' );\n\n\t\t\t\t$html.find( '*' ).not( allowed_tags.join( ',' ) ).each( function( ) {\n\n\t\t\t\t\t$( this ).replaceWith( this.innerHTML );\n\n\t\t\t\t} );\n\n\t\t\t\treturn $html.html();\n\n\t\t\t},\n\n\t\t} );\n\n\t\tBackbone.pubSub = _.extend( {}, Backbone.Events );\n\n\t\t$( document ).trigger( 'llms-builder-pre-init' );\n\n\t\twindow.llms_builder.questions = window.llms_builder.construct.get_collection( 'QuestionTypes', window.llms_builder.questions );\n\n\t\tvar CourseModel                 = window.llms_builder.construct.get_model( 'Course', window.llms_builder.course );\n\t\twindow.llms_builder.CourseModel = CourseModel;\n\n\t\twindow.llms_builder.sync = new Sync( CourseModel, window.llms_builder.sync );\n\n\t\tvar Course = new CourseView( {\n\t\t\tmodel: CourseModel,\n\t\t} );\n\n\t\tvar Sidebar = new SidebarView( {\n\t\t\tCourseView: Course\n\t\t} );\n\n\t\t$( document ).trigger( 'llms-builder-init', {\n\t\t\tcourse: Course,\n\t\t\tsidebar: Sidebar,\n\t\t} );\n\n\t\t/**\n\t\t * Do deep linking to Lesson / Quiz / Assignments.\n\t\t *\n\t\t * Hash should be in the form of #lesson:{lesson_id}:{subtab}\n\t\t * subtab can be either \"quiz\" or \"assignment\". If none found assumes the \"lesson\" tab.\n\t\t *\n\t\t * @since 3.27.0\n\t\t * @since 3.30.1 Wait for wp.editor & window.tinymce to load before opening deep link tabs.\n\t\t * @since 3.37.11 Use `_.getEditor()` helper when checking for the presence of `wp.editor`.\n\t\t */\n\t\tif ( window.location.hash ) {\n\n\t\t\tvar hash = window.location.hash;\n\t\t\tif ( -1 === hash.indexOf( '#lesson:' ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar parts = hash.replace( '#lesson:', '' ).split( ':' ),\n\t\t\t$lesson   = $( '#llms-lesson-' + parts[0] );\n\n\t\t\tif ( $lesson.length ) {\n\n\t\t\t\tLLMS.wait_for( function() {\n\t\t\t\t\treturn ( undefined !== _.getEditor() && undefined !== window.tinymce );\n                    }, function() {\n\t\t\t\t\t$lesson.closest( '.llms-builder-item.llms-section' ).find( 'a.llms-action-icon.expand' ).trigger( 'click' );\n\t\t\t\t\tvar subtab = parts[1] ? parts[1] : 'lesson';\n\t\t\t\t\t$( '#llms-lesson-' + parts[0] ).find( 'a.llms-action-icon.edit-' + subtab ).trigger( 'click' );\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t}\n\n\t} );\n"
  },
  {
    "path": "assets/js/builder/underscore.js",
    "content": "/**\n * Returns the WordPress-loaded version of Underscore for use with things that need it and use Require.\n *\n * @return   obj\n * @since    3.16.0\n * @version  3.16.0\n */\ndefine( function() {\n\treturn _;\n} );\n"
  },
  {
    "path": "assets/js/builder/vendor/almond.js",
    "content": "/**\n * @license almond 0.3.3 Copyright jQuery Foundation and other contributors.\n * Released under MIT license, http://github.com/requirejs/almond/LICENSE\n */\n//Going sloppy to avoid 'use strict' string cost, but strict practices should\n//be followed.\n/*global setTimeout: false */\n\nvar requirejs, require, define;\n(function (undef) {\n\tvar main, req, makeMap, handlers,\n\t\tdefined = {},\n\t\twaiting = {},\n\t\tconfig = {},\n\t\tdefining = {},\n\t\thasOwn = Object.prototype.hasOwnProperty,\n\t\taps = [].slice,\n\t\tjsSuffixRegExp = /\\.js$/;\n\n\tfunction hasProp(obj, prop) {\n\t\treturn hasOwn.call(obj, prop);\n\t}\n\n\t/**\n\t * Given a relative module name, like ./something, normalize it to\n\t * a real name that can be mapped to a path.\n\t * @param {String} name the relative name\n\t * @param {String} baseName a real name that the name arg is relative\n\t * to.\n\t * @returns {String} normalized name\n\t */\n\tfunction normalize(name, baseName) {\n\t\tvar nameParts, nameSegment, mapValue, foundMap, lastIndex,\n\t\t\tfoundI, foundStarMap, starI, i, j, part, normalizedBaseParts,\n\t\t\tbaseParts = baseName && baseName.split(\"/\"),\n\t\t\tmap = config.map,\n\t\t\tstarMap = (map && map['*']) || {};\n\n\t\t//Adjust any relative paths.\n\t\tif (name) {\n\t\t\tname = name.split('/');\n\t\t\tlastIndex = name.length - 1;\n\n\t\t\t// If wanting node ID compatibility, strip .js from end\n\t\t\t// of IDs. Have to do this here, and not in nameToUrl\n\t\t\t// because node allows either .js or non .js to map\n\t\t\t// to same file.\n\t\t\tif (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {\n\t\t\t\tname[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');\n\t\t\t}\n\n\t\t\t// Starts with a '.' so need the baseName\n\t\t\tif (name[0].charAt(0) === '.' && baseParts) {\n\t\t\t\t//Convert baseName to array, and lop off the last part,\n\t\t\t\t//so that . matches that 'directory' and not name of the baseName's\n\t\t\t\t//module. For instance, baseName of 'one/two/three', maps to\n\t\t\t\t//'one/two/three.js', but we want the directory, 'one/two' for\n\t\t\t\t//this normalization.\n\t\t\t\tnormalizedBaseParts = baseParts.slice(0, baseParts.length - 1);\n\t\t\t\tname = normalizedBaseParts.concat(name);\n\t\t\t}\n\n\t\t\t//start trimDots\n\t\t\tfor (i = 0; i < name.length; i++) {\n\t\t\t\tpart = name[i];\n\t\t\t\tif (part === '.') {\n\t\t\t\t\tname.splice(i, 1);\n\t\t\t\t\ti -= 1;\n\t\t\t\t} else if (part === '..') {\n\t\t\t\t\t// If at the start, or previous value is still ..,\n\t\t\t\t\t// keep them so that when converted to a path it may\n\t\t\t\t\t// still work when converted to a path, even though\n\t\t\t\t\t// as an ID it is less than ideal. In larger point\n\t\t\t\t\t// releases, may be better to just kick out an error.\n\t\t\t\t\tif (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (i > 0) {\n\t\t\t\t\t\tname.splice(i - 1, 2);\n\t\t\t\t\t\ti -= 2;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t//end trimDots\n\n\t\t\tname = name.join('/');\n\t\t}\n\n\t\t//Apply map config if available.\n\t\tif ((baseParts || starMap) && map) {\n\t\t\tnameParts = name.split('/');\n\n\t\t\tfor (i = nameParts.length; i > 0; i -= 1) {\n\t\t\t\tnameSegment = nameParts.slice(0, i).join(\"/\");\n\n\t\t\t\tif (baseParts) {\n\t\t\t\t\t//Find the longest baseName segment match in the config.\n\t\t\t\t\t//So, do joins on the biggest to smallest lengths of baseParts.\n\t\t\t\t\tfor (j = baseParts.length; j > 0; j -= 1) {\n\t\t\t\t\t\tmapValue = map[baseParts.slice(0, j).join('/')];\n\n\t\t\t\t\t\t//baseName segment has  config, find if it has one for\n\t\t\t\t\t\t//this name.\n\t\t\t\t\t\tif (mapValue) {\n\t\t\t\t\t\t\tmapValue = mapValue[nameSegment];\n\t\t\t\t\t\t\tif (mapValue) {\n\t\t\t\t\t\t\t\t//Match, update name to the new value.\n\t\t\t\t\t\t\t\tfoundMap = mapValue;\n\t\t\t\t\t\t\t\tfoundI = i;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (foundMap) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t//Check for a star map match, but just hold on to it,\n\t\t\t\t//if there is a shorter segment match later in a matching\n\t\t\t\t//config, then favor over this star map.\n\t\t\t\tif (!foundStarMap && starMap && starMap[nameSegment]) {\n\t\t\t\t\tfoundStarMap = starMap[nameSegment];\n\t\t\t\t\tstarI = i;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!foundMap && foundStarMap) {\n\t\t\t\tfoundMap = foundStarMap;\n\t\t\t\tfoundI = starI;\n\t\t\t}\n\n\t\t\tif (foundMap) {\n\t\t\t\tnameParts.splice(0, foundI, foundMap);\n\t\t\t\tname = nameParts.join('/');\n\t\t\t}\n\t\t}\n\n\t\treturn name;\n\t}\n\n\tfunction makeRequire(relName, forceSync) {\n\t\treturn function () {\n\t\t\t//A version of a require function that passes a moduleName\n\t\t\t//value for items that may need to\n\t\t\t//look up paths relative to the moduleName\n\t\t\tvar args = aps.call(arguments, 0);\n\n\t\t\t//If first arg is not require('string'), and there is only\n\t\t\t//one arg, it is the array form without a callback. Insert\n\t\t\t//a null so that the following concat is correct.\n\t\t\tif (typeof args[0] !== 'string' && args.length === 1) {\n\t\t\t\targs.push(null);\n\t\t\t}\n\t\t\treturn req.apply(undef, args.concat([relName, forceSync]));\n\t\t};\n\t}\n\n\tfunction makeNormalize(relName) {\n\t\treturn function (name) {\n\t\t\treturn normalize(name, relName);\n\t\t};\n\t}\n\n\tfunction makeLoad(depName) {\n\t\treturn function (value) {\n\t\t\tdefined[depName] = value;\n\t\t};\n\t}\n\n\tfunction callDep(name) {\n\t\tif (hasProp(waiting, name)) {\n\t\t\tvar args = waiting[name];\n\t\t\tdelete waiting[name];\n\t\t\tdefining[name] = true;\n\t\t\tmain.apply(undef, args);\n\t\t}\n\n\t\tif (!hasProp(defined, name) && !hasProp(defining, name)) {\n\t\t\tthrow new Error('No ' + name);\n\t\t}\n\t\treturn defined[name];\n\t}\n\n\t//Turns a plugin!resource to [plugin, resource]\n\t//with the plugin being undefined if the name\n\t//did not have a plugin prefix.\n\tfunction splitPrefix(name) {\n\t\tvar prefix,\n\t\t\tindex = name ? name.indexOf('!') : -1;\n\t\tif (index > -1) {\n\t\t\tprefix = name.substring(0, index);\n\t\t\tname = name.substring(index + 1, name.length);\n\t\t}\n\t\treturn [prefix, name];\n\t}\n\n\t//Creates a parts array for a relName where first part is plugin ID,\n\t//second part is resource ID. Assumes relName has already been normalized.\n\tfunction makeRelParts(relName) {\n\t\treturn relName ? splitPrefix(relName) : [];\n\t}\n\n\t/**\n\t * Makes a name map, normalizing the name, and using a plugin\n\t * for normalization if necessary. Grabs a ref to plugin\n\t * too, as an optimization.\n\t */\n\tmakeMap = function (name, relParts) {\n\t\tvar plugin,\n\t\t\tparts = splitPrefix(name),\n\t\t\tprefix = parts[0],\n\t\t\trelResourceName = relParts[1];\n\n\t\tname = parts[1];\n\n\t\tif (prefix) {\n\t\t\tprefix = normalize(prefix, relResourceName);\n\t\t\tplugin = callDep(prefix);\n\t\t}\n\n\t\t//Normalize according\n\t\tif (prefix) {\n\t\t\tif (plugin && plugin.normalize) {\n\t\t\t\tname = plugin.normalize(name, makeNormalize(relResourceName));\n\t\t\t} else {\n\t\t\t\tname = normalize(name, relResourceName);\n\t\t\t}\n\t\t} else {\n\t\t\tname = normalize(name, relResourceName);\n\t\t\tparts = splitPrefix(name);\n\t\t\tprefix = parts[0];\n\t\t\tname = parts[1];\n\t\t\tif (prefix) {\n\t\t\t\tplugin = callDep(prefix);\n\t\t\t}\n\t\t}\n\n\t\t//Using ridiculous property names for space reasons\n\t\treturn {\n\t\t\tf: prefix ? prefix + '!' + name : name, //fullName\n\t\t\tn: name,\n\t\t\tpr: prefix,\n\t\t\tp: plugin\n\t\t};\n\t};\n\n\tfunction makeConfig(name) {\n\t\treturn function () {\n\t\t\treturn (config && config.config && config.config[name]) || {};\n\t\t};\n\t}\n\n\thandlers = {\n\t\trequire: function (name) {\n\t\t\treturn makeRequire(name);\n\t\t},\n\t\texports: function (name) {\n\t\t\tvar e = defined[name];\n\t\t\tif (typeof e !== 'undefined') {\n\t\t\t\treturn e;\n\t\t\t} else {\n\t\t\t\treturn (defined[name] = {});\n\t\t\t}\n\t\t},\n\t\tmodule: function (name) {\n\t\t\treturn {\n\t\t\t\tid: name,\n\t\t\t\turi: '',\n\t\t\t\texports: defined[name],\n\t\t\t\tconfig: makeConfig(name)\n\t\t\t};\n\t\t}\n\t};\n\n\tmain = function (name, deps, callback, relName) {\n\t\tvar cjsModule, depName, ret, map, i, relParts,\n\t\t\targs = [],\n\t\t\tcallbackType = typeof callback,\n\t\t\tusingExports;\n\n\t\t//Use name if no relName\n\t\trelName = relName || name;\n\t\trelParts = makeRelParts(relName);\n\n\t\t//Call the callback to define the module, if necessary.\n\t\tif (callbackType === 'undefined' || callbackType === 'function') {\n\t\t\t//Pull out the defined dependencies and pass the ordered\n\t\t\t//values to the callback.\n\t\t\t//Default to [require, exports, module] if no deps\n\t\t\tdeps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;\n\t\t\tfor (i = 0; i < deps.length; i += 1) {\n\t\t\t\tmap = makeMap(deps[i], relParts);\n\t\t\t\tdepName = map.f;\n\n\t\t\t\t//Fast path CommonJS standard dependencies.\n\t\t\t\tif (depName === \"require\") {\n\t\t\t\t\targs[i] = handlers.require(name);\n\t\t\t\t} else if (depName === \"exports\") {\n\t\t\t\t\t//CommonJS module spec 1.1\n\t\t\t\t\targs[i] = handlers.exports(name);\n\t\t\t\t\tusingExports = true;\n\t\t\t\t} else if (depName === \"module\") {\n\t\t\t\t\t//CommonJS module spec 1.1\n\t\t\t\t\tcjsModule = args[i] = handlers.module(name);\n\t\t\t\t} else if (hasProp(defined, depName) ||\n\t\t\t\t\t\t   hasProp(waiting, depName) ||\n\t\t\t\t\t\t   hasProp(defining, depName)) {\n\t\t\t\t\targs[i] = callDep(depName);\n\t\t\t\t} else if (map.p) {\n\t\t\t\t\tmap.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});\n\t\t\t\t\targs[i] = defined[depName];\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error(name + ' missing ' + depName);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tret = callback ? callback.apply(defined[name], args) : undefined;\n\n\t\t\tif (name) {\n\t\t\t\t//If setting exports via \"module\" is in play,\n\t\t\t\t//favor that over return value and exports. After that,\n\t\t\t\t//favor a non-undefined return value over exports use.\n\t\t\t\tif (cjsModule && cjsModule.exports !== undef &&\n\t\t\t\t\t\tcjsModule.exports !== defined[name]) {\n\t\t\t\t\tdefined[name] = cjsModule.exports;\n\t\t\t\t} else if (ret !== undef || !usingExports) {\n\t\t\t\t\t//Use the return value from the function.\n\t\t\t\t\tdefined[name] = ret;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (name) {\n\t\t\t//May just be an object definition for the module. Only\n\t\t\t//worry about defining if have a module name.\n\t\t\tdefined[name] = callback;\n\t\t}\n\t};\n\n\trequirejs = require = req = function (deps, callback, relName, forceSync, alt) {\n\t\tif (typeof deps === \"string\") {\n\t\t\tif (handlers[deps]) {\n\t\t\t\t//callback in this case is really relName\n\t\t\t\treturn handlers[deps](callback);\n\t\t\t}\n\t\t\t//Just return the module wanted. In this scenario, the\n\t\t\t//deps arg is the module name, and second arg (if passed)\n\t\t\t//is just the relName.\n\t\t\t//Normalize module name, if it contains . or ..\n\t\t\treturn callDep(makeMap(deps, makeRelParts(callback)).f);\n\t\t} else if (!deps.splice) {\n\t\t\t//deps is a config object, not an array.\n\t\t\tconfig = deps;\n\t\t\tif (config.deps) {\n\t\t\t\treq(config.deps, config.callback);\n\t\t\t}\n\t\t\tif (!callback) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (callback.splice) {\n\t\t\t\t//callback is an array, which means it is a dependency list.\n\t\t\t\t//Adjust args if there are dependencies\n\t\t\t\tdeps = callback;\n\t\t\t\tcallback = relName;\n\t\t\t\trelName = null;\n\t\t\t} else {\n\t\t\t\tdeps = undef;\n\t\t\t}\n\t\t}\n\n\t\t//Support require(['a'])\n\t\tcallback = callback || function () {};\n\n\t\t//If relName is a function, it is an errback handler,\n\t\t//so remove it.\n\t\tif (typeof relName === 'function') {\n\t\t\trelName = forceSync;\n\t\t\tforceSync = alt;\n\t\t}\n\n\t\t//Simulate async callback;\n\t\tif (forceSync) {\n\t\t\tmain(undef, deps, callback, relName);\n\t\t} else {\n\t\t\t//Using a non-zero value because of concern for what old browsers\n\t\t\t//do, and latest browsers \"upgrade\" to 4 if lower value is used:\n\t\t\t//http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:\n\t\t\t//If want a value immediately, use require('id') instead -- something\n\t\t\t//that works in almond on the global level, but not guaranteed and\n\t\t\t//unlikely to work in other AMD implementations.\n\t\t\tsetTimeout(function () {\n\t\t\t\tmain(undef, deps, callback, relName);\n\t\t\t}, 4);\n\t\t}\n\n\t\treturn req;\n\t};\n\n\t/**\n\t * Just drops the config on the floor, but returns req in case\n\t * the config return value is used.\n\t */\n\treq.config = function (cfg) {\n\t\treturn req(cfg);\n\t};\n\n\t/**\n\t * Expose module registry for debugging and tooling\n\t */\n\trequirejs._defined = defined;\n\n\tdefine = function (name, deps, callback) {\n\t\tif (typeof name !== 'string') {\n\t\t\tthrow new Error('See almond README: incorrect module build, no module name');\n\t\t}\n\n\t\t//This module may not have dependencies\n\t\tif (!deps.splice) {\n\t\t\t//deps is not an array, so probably means\n\t\t\t//an object literal or factory function for\n\t\t\t//the value. Adjust args.\n\t\t\tcallback = deps;\n\t\t\tdeps = [];\n\t\t}\n\n\t\tif (!hasProp(defined, name) && !hasProp(waiting, name)) {\n\t\t\twaiting[name] = [name, deps, callback];\n\t\t}\n\t};\n\n\tdefine.amd = {\n\t\tjQuery: true\n\t};\n}());\n"
  },
  {
    "path": "assets/js/builder/vendor/backbone.collectionView.js",
    "content": "/*!\n* Backbone.CollectionView, v1.3.4\n* Copyright (c)2013 Rotunda Software, LLC.\n* Distributed under MIT license\n* http://github.com/rotundasoftware/backbone-collection-view\n*/\n\n( function( root, factory ) {\n\t// UMD wrapper\n\tif ( typeof define === 'function' && define.amd ) {\n\t\t// AMD\n\t\tdefine( [ 'underscore', 'backbone', 'jquery' ], factory );\n\t} else if ( typeof exports !== 'undefined' ) {\n\t\t// Node/CommonJS\n\t\tmodule.exports = factory( require('underscore' ), require( 'backbone' ), require( 'backbone' ).$ );\n\t} else {\n\t\t// Browser globals\n\t\tfactory( root._, root.Backbone, ( root.jQuery || root.Zepto || root.$ ) );\n\t}\n}( this, function( _, Backbone, $ ) {\n\tvar mDefaultModelViewConstructor = Backbone.View;\n\n\tvar kDefaultReferenceBy = \"model\";\n\n\tvar kOptionsRequiringRerendering = [ \"collection\", \"modelView\", \"modelViewOptions\", \"itemTemplate\", \"itemTemplateFunction\", \"detachedRendering\" ];\n\n\tvar kStylesForEmptyListCaption = {\n\t\t\"background\" : \"transparent\",\n\t\t\"border\" : \"none\",\n\t\t\"box-shadow\" : \"none\"\n\t};\n\n\tBackbone.CollectionView = Backbone.View.extend( {\n\n\t\ttagName : \"ul\",\n\n\t\tevents : {\n\t\t\t\"mousedown > li, tbody > tr > td\" : \"_listItem_onMousedown\",\n\t\t\t\"dblclick > li, tbody > tr > td\" : \"_listItem_onDoubleClick\",\n\t\t\t\"click\" : \"_listBackground_onClick\",\n\t\t\t\"click ul.collection-view, table.collection-view\" : \"_listBackground_onClick\",\n\t\t\t\"keydown\" : \"_onKeydown\"\n\t\t},\n\n\t\t// only used if Backbone.Courier is available\n\t\tspawnMessages : {\n\t\t\t\"focus\" : \"focus\"\n\t\t},\n\n\t\t//only used if Backbone.Courier is available\n\t\tpassMessages : { \"*\" : \".\" },\n\n\t\t// viewOption definitions with default values.\n\t\tinitializationOptions : [\n\t\t\t{ \"collection\" : null },\n\t\t\t{ \"modelView\" : null },\n\t\t\t{ \"modelViewOptions\" : {} },\n\t\t\t{ \"itemTemplate\" : null },\n\t\t\t{ \"itemTemplateFunction\" : null },\n\t\t\t{ \"selectable\" : true },\n\t\t\t{ \"clickToSelect\" : true },\n\t\t\t{ \"selectableModelsFilter\" : null },\n\t\t\t{ \"visibleModelsFilter\" : null },\n\t\t\t{ \"sortableModelsFilter\" : null },\n\t\t\t{ \"selectMultiple\" : false },\n\t\t\t{ \"clickToToggle\" : false },\n\t\t\t{ \"processKeyEvents\" : true },\n\t\t\t{ \"sortable\" : false },\n\t\t\t{ \"sortableOptions\" : null },\n\t\t\t{ \"reuseModelViews\" : true },\n\t\t\t{ \"detachedRendering\" : false },\n\t\t\t{ \"emptyListCaption\" : null }\n\t\t],\n\n\t\tinitialize : function( options ) {\n\t\t\tBackbone.ViewOptions.add( this, \"initializationOptions\" ); // setup the ViewOptions functionality.\n\t\t\tthis.setOptions( options ); // and make use of any provided options\n\n\t\t\tif( ! this.collection ) this.collection = new Backbone.Collection();\n\n\t\t\tthis._hasBeenRendered = false;\n\n\t\t\tif( this._isBackboneCourierAvailable() ) {\n\t\t\t\tBackbone.Courier.add( this );\n\t\t\t}\n\n\t\t\tthis.$el.data( \"view\", this ); // needed for connected sortable lists\n\t\t\tthis.$el.addClass( \"collection-view collection-list\" ); // collection-list is in there for legacy purposes\n\t\t\tif( this.selectable ) this.$el.addClass( \"selectable\" );\n\n\t\t\tif( this.selectable && this.processKeyEvents )\n\t\t\t\tthis.$el.attr( \"tabindex\", 0 ); // so we get keyboard events\n\n\t\t\tthis.selectedItems = [];\n\n\t\t\tthis._updateItemTemplate();\n\n\t\t\tif( this.collection )\n\t\t\t\tthis._registerCollectionEvents();\n\n\t\t\tthis.viewManager = new ChildViewContainer();\n\t\t},\n\n\t\t_onOptionsChanged : function( changedOptions, originalOptions ) {\n\t\t\tvar _this = this;\n\t\t\tvar rerender = false;\n\n\t\t\t_.each( _.keys( changedOptions ), function( changedOptionKey ) {\n\t\t\t\tvar newVal = changedOptions[ changedOptionKey ];\n\t\t\t\tvar oldVal = originalOptions[ changedOptionKey ];\n\t\t\t\tswitch( changedOptionKey ) {\n\t\t\t\t\tcase \"collection\" :\n\t\t\t\t\t\tif ( newVal !== oldVal ) {\n\t\t\t\t\t\t\t_this.stopListening( oldVal );\n\t\t\t\t\t\t\t_this._registerCollectionEvents();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"selectMultiple\" :\n\t\t\t\t\t\tif( ! newVal && _this.selectedItems.length > 1 )\n\t\t\t\t\t\t\t_this.setSelectedModel( _.first( _this.selectedItems ), { by : \"cid\" } );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"selectable\" :\n\t\t\t\t\t\tif( ! newVal && _this.selectedItems.length > 0 )\n\t\t\t\t\t\t\t_this.setSelectedModels( [] );\n\n\t\t\t\t\t\tif( newVal && this.processKeyEvents ) _this.$el.attr( \"tabindex\", 0 ); // so we get keyboard events\n\t\t\t\t\t\telse _this.$el.removeAttr( \"tabindex\", 0 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"sortable\" :\n\t\t\t\t\t\tchangedOptions.sortable ? _this._setupSortable() : _this.$el.sortable( \"destroy\" );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"selectableModelsFilter\" :\n\t\t\t\t\t\t_this.reapplyFilter( 'selectableModels' );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"sortableOptions\" :\n\t\t\t\t\t\t_this.$el.sortable( \"destroy\" );\n\t\t\t\t\t\t_this._setupSortable();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"sortableModelsFilter\" :\n\t\t\t\t\t\t_this.reapplyFilter( 'sortableModels' );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"visibleModelsFilter\" :\n\t\t\t\t\t\t_this.reapplyFilter( 'visibleModels' );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"itemTemplate\" :\n\t\t\t\t\t\t_this._updateItemTemplate();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"processKeyEvents\" :\n\t\t\t\t\t\tif( newVal && this.selectable ) _this.$el.attr( \"tabindex\", 0 ); // so we get keyboard events\n\t\t\t\t\t\telse _this.$el.removeAttr( \"tabindex\", 0 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"modelView\" :\n\t\t\t\t\t\t//need to remove all old view instances\n\t\t\t\t\t\t_this.viewManager.each( function( view ) {\n\t\t\t\t\t\t\t_this.viewManager.remove( view );\n\t\t\t\t\t\t\t// destroy the View itself\n\t\t\t\t\t\t\tview.remove();\n\t\t\t\t\t\t} );\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif( _.contains( kOptionsRequiringRerendering, changedOptionKey ) ) rerender = true;\n\t\t\t} );\n\n\t\t\tif( this._hasBeenRendered && rerender ) {\n\t\t\t\tthis.render();\n\t\t\t}\n\t\t},\n\n\t\tsetOption : function( optionName, optionValue ) { // now is merely a wrapper around backbone.viewOptions' setOptions()\n\t\t\tvar optionHash = {};\n\t\t\toptionHash[ optionName ] = optionValue;\n\t\t\tthis.setOptions( optionHash );\n\t\t},\n\n\t\tgetSelectedModel : function( options ) {\n\t\t\treturn this.selectedItems.length ? _.first( this.getSelectedModels( options ) ) : null;\n\t\t},\n\n\t\tgetSelectedModels : function ( options ) {\n\t\t\tvar _this = this;\n\n\t\t\toptions = _.extend( {}, {\n\t\t\t\tby : kDefaultReferenceBy\n\t\t\t}, options );\n\n\t\t\tvar referenceBy = options.by;\n\t\t\tvar items = [];\n\n\t\t\tswitch( referenceBy ) {\n\t\t\t\tcase \"id\" :\n\t\t\t\t\t_.each( this.selectedItems, function ( item ) {\n\t\t\t\t\t\titems.push( _this.collection.get( item ).id );\n\t\t\t\t\t} );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"cid\" :\n\t\t\t\t\titems = items.concat( this.selectedItems );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"offset\" :\n\t\t\t\t\tvar curLineNumber = 0;\n\n\t\t\t\t\tvar itemElements = this._getVisibleItemEls();\n\n\t\t\t\t\titemElements.each( function() {\n\t\t\t\t\t\tvar thisItemEl = $( this );\n\t\t\t\t\t\tif( thisItemEl.is( \".selected\" ) )\n\t\t\t\t\t\t\titems.push( curLineNumber );\n\t\t\t\t\t\tcurLineNumber++;\n\t\t\t\t\t} );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"model\" :\n\t\t\t\t\t_.each( this.selectedItems, function ( item ) {\n\t\t\t\t\t\titems.push( _this.collection.get( item ) );\n\t\t\t\t\t} );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"view\" :\n\t\t\t\t\t_.each( this.selectedItems, function ( item ) {\n\t\t\t\t\t\titems.push( _this.viewManager.findByModel( _this.collection.get( item ) ) );\n\t\t\t\t\t} );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault :\n\t\t\t\t\tthrow new Error( \"Invalid referenceBy option: \" + referenceBy );\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\treturn items;\n\n\t\t},\n\n\t\tsetSelectedModels : function( newSelectedItems, options ) {\n\t\t\tif( ! _.isArray( newSelectedItems ) ) throw \"Invalid parameter value\";\n\t\t\tif( ! this.selectable && newSelectedItems.length > 0 ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors\n\n\t\t\toptions = _.extend( {}, {\n\t\t\t\tsilent : false,\n\t\t\t\tby : kDefaultReferenceBy\n\t\t\t}, options );\n\n\t\t\tvar referenceBy = options.by;\n\t\t\tvar newSelectedCids = [];\n\n\t\t\tswitch( referenceBy ) {\n\t\t\t\tcase \"cid\" :\n\t\t\t\t\tnewSelectedCids = newSelectedItems;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"id\" :\n\t\t\t\t\tthis.collection.each( function( thisModel ) {\n\t\t\t\t\t\tif( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid );\n\t\t\t\t\t} );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"model\" :\n\t\t\t\t\tnewSelectedCids = _.pluck( newSelectedItems, \"cid\" );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"view\" :\n\t\t\t\t\t_.each( newSelectedItems, function( item ) {\n\t\t\t\t\t\tnewSelectedCids.push( item.model.cid );\n\t\t\t\t\t} );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"offset\" :\n\t\t\t\t\tvar curLineNumber = 0;\n\t\t\t\t\tvar selectedItems = [];\n\n\t\t\t\t\tvar itemElements = this._getVisibleItemEls();\n\t\t\t\t\titemElements.each( function() {\n\t\t\t\t\t\tvar thisItemEl = $( this );\n\t\t\t\t\t\tif( _.contains( newSelectedItems, curLineNumber ) )\n\t\t\t\t\t\t\tnewSelectedCids.push( thisItemEl.attr( \"data-model-cid\" ) );\n\t\t\t\t\t\tcurLineNumber++;\n\t\t\t\t\t} );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault :\n\t\t\t\t\tthrow new Error( \"Invalid referenceBy option: \" + referenceBy );\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tvar oldSelectedModels = this.getSelectedModels();\n\t\t\tvar oldSelectedCids = _.clone( this.selectedItems );\n\n\t\t\tthis.selectedItems = this._convertStringsToInts( newSelectedCids );\n\t\t\tthis._validateSelection();\n\n\t\t\tvar newSelectedModels = this.getSelectedModels();\n\n\t\t\tif( ! this._containSameElements( oldSelectedCids, this.selectedItems ) )\n\t\t\t{\n\t\t\t\tthis._addSelectedClassToSelectedItems( oldSelectedCids );\n\n\t\t\t\tif( ! options.silent )\n\t\t\t\t{\n\t\t\t\t\tif( this._isBackboneCourierAvailable() ) {\n\t\t\t\t\t\tthis.spawn( \"selectionChanged\", {\n\t\t\t\t\t\t\tselectedModels : newSelectedModels,\n\t\t\t\t\t\t\toldSelectedModels : oldSelectedModels\n\t\t\t\t\t\t} );\n\t\t\t\t\t} else this.trigger( \"selectionChanged\", newSelectedModels, oldSelectedModels );\n\t\t\t\t}\n\n\t\t\t\tthis.updateDependentControls();\n\t\t\t}\n\t\t},\n\n\t\tsetSelectedModel : function( newSelectedItem, options ) {\n\t\t\tif( ! newSelectedItem && newSelectedItem !== 0 )\n\t\t\t\tthis.setSelectedModels( [], options );\n\t\t\telse\n\t\t\t\tthis.setSelectedModels( [ newSelectedItem ], options );\n\t\t},\n\n\t\tgetView : function( reference, options ) {\n\t\t\toptions = _.extend( {}, {\n\t\t\t\tby : kDefaultReferenceBy\n\t\t\t}, options );\n\n\t\t\tswitch( options.by ) {\n\t\t\t\tcase \"id\" :\n\t\t\t\tcase \"cid\" :\n\t\t\t\t\tvar model = this.collection.get( reference ) || null;\n\t\t\t\t\treturn model && this.viewManager.findByModel( model );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"offset\" :\n\t\t\t\t\tvar itemElements = this._getVisibleItemEls();\n\t\t\t\t\treturn $( itemElements.get( reference ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"model\" :\n\t\t\t\t\treturn this.viewManager.findByModel( reference );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault :\n\t\t\t\t\tthrow new Error( \"Invalid referenceBy option: \" + referenceBy );\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t},\n\n\t\trender : function() {\n\t\t\tvar _this = this;\n\n\t\t\tthis._hasBeenRendered = true;\n\n\t\t\tif( this.selectable ) this._saveSelection();\n\n\t\t\tvar modelViewContainerEl;\n\n\t\t\t// If collection view element is a table and it has a tbody\n\t\t\t// within it, render the model views inside of the tbody\n\t\t\tmodelViewContainerEl = this._getContainerEl();\n\n\t\t\tvar oldViewManager = this.viewManager;\n\t\t\tthis.viewManager = new ChildViewContainer();\n\n\t\t\t// detach each of our subviews that we have already created to represent models\n\t\t\t// in the collection. We are going to re-use the ones that represent models that\n\t\t\t// are still here, instead of creating new ones, so that we don't loose state\n\t\t\t// information in the views.\n\t\t\toldViewManager.each( function( thisModelView ) {\n\t\t\t\t// to boost performance, only detach those views that will be sticking around.\n\t\t\t\t// we won't need the other ones later, so no need to detach them individually.\n\t\t\t\tif( this.reuseModelViews && this.collection.get( thisModelView.model.cid ) ) {\n\t\t\t\t\tthisModelView.$el.detach();\n\t\t\t\t} else thisModelView.remove();\n\t\t\t}, this );\n\n\t\t\tmodelViewContainerEl.empty();\n\t\t\tvar fragmentContainer;\n\n\t\t\tif( this.detachedRendering )\n\t\t\t\tfragmentContainer = document.createDocumentFragment();\n\n\t\t\tthis.collection.each( function( thisModel ) {\n\t\t\t\tvar thisModelView = oldViewManager.findByModelCid( thisModel.cid );\n\t\t\t\tif( ! this.reuseModelViews || _.isUndefined( thisModelView ) ) {\n\t\t\t\t\t// if the model view has not already been created on a\n\t\t\t\t\t// previous render then create and initialize it now.\n\t\t\t\t\tthisModelView = this._createNewModelView( thisModel, this._getModelViewOptions( thisModel ) );\n\t\t\t\t}\n\n\t\t\t\tthis._insertAndRenderModelView( thisModelView, fragmentContainer || modelViewContainerEl );\n\t\t\t}, this );\n\n\t\t\tif( this.detachedRendering )\n\t\t\t\tmodelViewContainerEl.append( fragmentContainer );\n\n\t\t\tif( this.sortable ) this._setupSortable();\n\n\t\t\tthis._showEmptyListCaptionIfAppropriate();\n\n\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\tthis.spawn( \"render\" );\n\t\t\telse this.trigger( \"render\" );\n\n\t\t\tif( this.selectable ) {\n\t\t\t\tthis._restoreSelection();\n\t\t\t\tthis.updateDependentControls();\n\t\t\t}\n\n\t\t\tthis.forceRerenderOnNextSortEvent = false;\n\t\t},\n\n\t\t_showEmptyListCaptionIfAppropriate : function ( ) {\n\t\t\tthis._removeEmptyListCaption();\n\n\t\t\tif( this.emptyListCaption ) {\n\t\t\t\tvar visibleEls = this._getVisibleItemEls();\n\n\t\t\t\tif( visibleEls.length === 0 ) {\n\t\t\t\t\tvar emptyListString;\n\n\t\t\t\t\tif( _.isFunction( this.emptyListCaption ) )\n\t\t\t\t\t\temptyListString = this.emptyListCaption();\n\t\t\t\t\telse\n\t\t\t\t\t\temptyListString = this.emptyListCaption;\n\n\t\t\t\t\tvar $emptyListCaptionEl;\n\t\t\t\t\tvar $varEl = $( \"<var class='empty-list-caption'>\" + emptyListString + \"</var>\" );\n\n\t\t\t\t\t// need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td)\n\t\t\t\t\tif( this._isRenderedAsList() )\n\t\t\t\t\t\t$emptyListCaptionEl = $varEl.wrapAll( \"<li class='not-sortable'></li>\" ).parent().css( kStylesForEmptyListCaption );\n\t\t\t\t\telse\n\t\t\t\t\t\t$emptyListCaptionEl = $varEl.wrapAll( \"<tr class='not-sortable'><td colspan='1000'></td></tr>\" ).parent().parent().css( kStylesForEmptyListCaption );\n\n\t\t\t\t\tthis._getContainerEl().append( $emptyListCaptionEl );\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t_removeEmptyListCaption : function( ) {\n\t\t\tif( this._isRenderedAsList() )\n\t\t\t\tthis._getContainerEl().find( \"> li > var.empty-list-caption\" ).parent().remove();\n\t\t\telse\n\t\t\t\tthis._getContainerEl().find( \"> tr > td > var.empty-list-caption\" ).parent().parent().remove();\n\t\t},\n\n\t\t// Render a single model view in container object \"parentElOrDocumentFragment\", which is either\n\t\t// a documentFragment or a jquery object. optional arg atIndex is not support for document fragments.\n\t\t_insertAndRenderModelView : function( modelView, parentElOrDocumentFragment, atIndex ) {\n\t\t\tvar thisModelViewWrapped = this._wrapModelView( modelView );\n\n\t\t\tif( parentElOrDocumentFragment.nodeType === 11 ) // if we are inserting into a document fragment, we need to use the DOM appendChild method\n\t\t\t\tparentElOrDocumentFragment.appendChild( thisModelViewWrapped.get( 0 ) );\n\t\t\telse {\n\t\t\t\tvar numberOfModelViewsCurrentlyInDOM = parentElOrDocumentFragment.children().length;\n\t\t\t\tif( ! _.isUndefined( atIndex ) && atIndex >= 0 && atIndex < numberOfModelViewsCurrentlyInDOM )\n\t\t\t\t\t// note this.collection.length might be greater than parentElOrDocumentFragment.children().length here\n\t\t\t\t\tparentElOrDocumentFragment.children().eq( atIndex ).before( thisModelViewWrapped );\n\t\t\t\telse {\n\t\t\t\t\t// if we are attempting to insert a modelView in an position that is beyond what is currently in the\n\t\t\t\t\t// DOM, then make a note that we need to re-render the collection view on the next sort event. If we dont\n\t\t\t\t\t// force this re-render, we can end up with modelViews in the wrong order when the collection defines\n\t\t\t\t\t// a comparator and multiple models are added at once. See https://github.com/rotundasoftware/backbone.collectionView/issues/69\n\t\t\t\t\tif( ! _.isUndefined( atIndex ) && atIndex > numberOfModelViewsCurrentlyInDOM ) this.forceRerenderOnNextSortEvent = true;\n\n\t\t\t\t\tparentElOrDocumentFragment.append( thisModelViewWrapped );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.viewManager.add( modelView );\n\n\t\t\t// we have to render the modelView after it has been put in context, as opposed to in the\n\t\t\t// initialize function of the modelView, because some rendering might be dependent on\n\t\t\t// the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself,\n\t\t\t// it must be in full context in the DOM tree or else the stretch will not behave as intended.\n\t\t\tvar renderResult = modelView.render();\n\n\t\t\t// return false from the view's render function to hide this item\n\t\t\tif( renderResult === false ) {\n\t\t\t\tthisModelViewWrapped.hide();\n\t\t\t\tthisModelViewWrapped.addClass( \"not-visible\" );\n\t\t\t}\n\n\t\t\tvar hideThisModelView = false;\n\t\t\tif( _.isFunction( this.visibleModelsFilter ) )\n\t\t\t\thideThisModelView = ! this.visibleModelsFilter( modelView.model );\n\n\t\t\tif( thisModelViewWrapped.children().length === 1 )\n\t\t\t\tthisModelViewWrapped.toggle( ! hideThisModelView );\n\t\t\telse modelView.$el.toggle( ! hideThisModelView );\n\n\t\t\tthisModelViewWrapped.toggleClass( \"not-visible\", hideThisModelView );\n\n\t\t\tif( ! hideThisModelView && this.emptyListCaption ) this._removeEmptyListCaption();\n\t\t},\n\n\t\tupdateDependentControls : function() {\n\t\t\tif( this._isBackboneCourierAvailable() ) {\n\t\t\t\tthis.spawn( \"updateDependentControls\", {\n\t\t\t\t\tselectedModels : this.getSelectedModels()\n\t\t\t\t} );\n\t\t\t} else this.trigger( \"updateDependentControls\", this.getSelectedModels() );\n\t\t},\n\n\t\t// Override `Backbone.View.remove` to also destroy all Views in `viewManager`\n\t\tremove : function() {\n\t\t\tthis.viewManager.each( function( view ) {\n\t\t\t\tview.remove();\n\t\t\t} );\n\n\t\t\tBackbone.View.prototype.remove.apply( this, arguments );\n\t\t},\n\n\t\treapplyFilter : function( whichFilter ) {\n\t\t\tvar _this = this;\n\n\t\t\tif( ! _.contains( [ \"selectableModels\", \"sortableModels\", \"visibleModels\" ], whichFilter ) ) {\n\t\t\t\tthrow new Error( \"Invalid filter identifier supplied to reapplyFilter: \" + whichFilter );\n\t\t\t}\n\n\t\t\tswitch( whichFilter ) {\n\t\t\t\tcase \"visibleModels\":\n\t\t\t\t\t_this.viewManager.each( function( thisModelView ) {\n\t\t\t\t\t\tvar notVisible = _this.visibleModelsFilter && ! _this.visibleModelsFilter.call( _this, thisModelView.model );\n\n\t\t\t\t\t\tthisModelView.$el.toggleClass( \"not-visible\", notVisible );\n\t\t\t\t\t\tif( _this._modelViewHasWrapperLI( thisModelView ) ) {\n\t\t\t\t\t\t\tthisModelView.$el.closest( \"li\" ).toggleClass( \"not-visible\", notVisible ).toggle( ! notVisible );\n\t\t\t\t\t\t} else thisModelView.$el.toggle( ! notVisible );\n\t\t\t\t\t} );\n\n\t\t\t\t\tthis._showEmptyListCaptionIfAppropriate();\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"sortableModels\":\n\t\t\t\t\t_this.$el.sortable( \"destroy\" );\n\n\t\t\t\t\t_this.viewManager.each( function( thisModelView ) {\n\t\t\t\t\t\tvar notSortable = _this.sortableModelsFilter && ! _this.sortableModelsFilter.call( _this, thisModelView.model );\n\n\t\t\t\t\t\tthisModelView.$el.toggleClass( \"not-sortable\", notSortable );\n\t\t\t\t\t\tif( _this._modelViewHasWrapperLI( thisModelView ) ) {\n\t\t\t\t\t\t\tthisModelView.$el.closest( \"li\" ).toggleClass( \"not-sortable\", notSortable );\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\t\t\t\t\t_this._setupSortable();\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"selectableModels\":\n\t\t\t\t\t_this.viewManager.each( function( thisModelView ) {\n\t\t\t\t\t\tvar notSelectable = _this.selectableModelsFilter && ! _this.selectableModelsFilter.call( _this, thisModelView.model );\n\n\t\t\t\t\t\tthisModelView.$el.toggleClass( \"not-selectable\", notSelectable );\n\t\t\t\t\t\tif( _this._modelViewHasWrapperLI( thisModelView ) ) {\n\t\t\t\t\t\t\tthisModelView.$el.closest( \"li\" ).toggleClass( \"not-selectable\", notSelectable );\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\t\t\t\t\t_this._validateSelection();\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t},\n\n\t\t// A method to remove the view relating to model.\n\t\t_removeModelView : function( modelView ) {\n\t\t\tif( this.selectable ) this._saveSelection();\n\n\t\t\tthis.viewManager.remove( modelView ); // Remove the view from the viewManager\n\t\t\tif( this._modelViewHasWrapperLI( modelView ) ) modelView.$el.parent().remove(); // Remove the li wrapper from the DOM\n\t\t\tmodelView.remove(); // Remove the view from the DOM and stop listening to events\n\n\t\t\tif( this.selectable ) this._restoreSelection();\n\n\t\t\tthis._showEmptyListCaptionIfAppropriate();\n\t\t},\n\n\t\t_validateSelectionAndRender : function() {\n\t\t\tthis._validateSelection();\n\t\t\tthis.render();\n\t\t},\n\n\t\t_registerCollectionEvents : function() {\n\n\t\t\tthis.listenTo( this.collection, \"add\", function( model ) {\n\t\t\t\tvar modelView;\n\t\t\t\tif( this._hasBeenRendered ) {\n\t\t\t\t\tmodelView = this._createNewModelView( model, this._getModelViewOptions( model ) );\n\t\t\t\t\tthis._insertAndRenderModelView( modelView, this._getContainerEl(), this.collection.indexOf( model ) );\n\t\t\t\t}\n\n\t\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\t\tthis.spawn( \"add\", modelView );\n\t\t\t\telse this.trigger( \"add\", modelView );\n\t\t\t} );\n\n\t\t\tthis.listenTo( this.collection, \"remove\", function( model ) {\n\t\t\t\tvar modelView;\n\n\t\t\t\tif( this._hasBeenRendered ) {\n\t\t\t\t\tmodelView = this.viewManager.findByModelCid( model.cid );\n\t\t\t\t\tthis._removeModelView( modelView );\n\t\t\t\t}\n\n\t\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\t\tthis.spawn( \"remove\" );\n\t\t\t\telse this.trigger( \"remove\" );\n\t\t\t} );\n\n\t\t\tthis.listenTo( this.collection, \"reset\", function() {\n\t\t\t\tif( this._hasBeenRendered ) this.render();\n\t\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\t\tthis.spawn( \"reset\" );\n\t\t\t\telse this.trigger( \"reset\" );\n\t\t\t} );\n\n\t\t\t// we should not be listening to change events on the model as a default behavior. the models\n\t\t\t// should be responsible for re-rendering themselves if necessary, and if the collection does\n\t\t\t// also need to re-render as a result of a model change, this should be handled by overriding\n\t\t\t// this method. by default the collection view should not re-render in response to model changes\n\t\t\t// this.listenTo( this.collection, \"change\", function( model ) {\n\t\t\t// \tif( this._hasBeenRendered ) this.viewManager.findByModel( model ).render();\n\t\t\t// \tif( this._isBackboneCourierAvailable() )\n\t\t\t// \t\tthis.spawn( \"change\", { model : model } );\n\t\t\t// } );\n\n\t\t\tthis.listenTo( this.collection, \"sort\", function( collection, options ) {\n\t\t\t\tif( this._hasBeenRendered && ( options.add !== true || this.forceRerenderOnNextSortEvent ) ) this.render();\n\t\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\t\tthis.spawn( \"sort\" );\n\t\t\t\telse this.trigger( \"sort\" );\n\t\t\t} );\n\t\t},\n\n\t\t_getContainerEl : function() {\n\t\t\tif ( this._isRenderedAsTable() ) {\n\t\t\t\t// not all tables have a tbody, so we test\n\t\t\t\tvar tbody = this.$el.find( \"> tbody\" );\n\t\t\t\tif ( tbody.length > 0 )\n\t\t\t\t\treturn tbody;\n\t\t\t}\n\t\t\treturn this.$el;\n\t\t},\n\n\t\t_getClickedItemId : function( theEvent ) {\n\t\t\tvar clickedItemId = null;\n\n\t\t\t// important to use currentTarget as opposed to target, since we could be bubbling\n\t\t\t// an event that took place within another collectionList\n\t\t\tvar clickedItemEl = $( theEvent.currentTarget );\n\t\t\tif( clickedItemEl.closest( \".collection-view\" ).get(0) !== this.$el.get(0) ) return;\n\n\t\t\t// determine which list item was clicked. If we clicked in the blank area\n\t\t\t// underneath all the elements, we want to know that too, since in this\n\t\t\t// case we will want to deselect all elements. so check to see if the clicked\n\t\t\t// DOM element is the list itself to find that out.\n\t\t\tvar clickedItem = clickedItemEl.closest( \"[data-model-cid]\" );\n\t\t\tif( clickedItem.length > 0 )\n\t\t\t{\n\t\t\t\tclickedItemId = clickedItem.attr( \"data-model-cid\" );\n\t\t\t\tif( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 );\n\t\t\t}\n\n\t\t\treturn clickedItemId;\n\t\t},\n\n\t\t_updateItemTemplate : function() {\n\t\t\tvar itemTemplateHtml;\n\t\t\tif( this.itemTemplate )\n\t\t\t{\n\t\t\t\tif( $( this.itemTemplate ).length === 0 )\n\t\t\t\t\tthrow \"Could not find item template from selector: \" + this.itemTemplate;\n\n\t\t\t\titemTemplateHtml = $( this.itemTemplate ).html();\n\t\t\t}\n\t\t\telse\n\t\t\t\titemTemplateHtml = this.$( \".item-template\" ).html();\n\n\t\t\tif( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml );\n\n\t\t},\n\n\t\t_validateSelection : function() {\n\t\t\t// note can't use the collection's proxy to underscore because \"cid\" is not an attribute,\n\t\t\t// but an element of the model object itself.\n\t\t\tvar modelReferenceIds = _.pluck( this.collection.models, \"cid\" );\n\t\t\tthis.selectedItems = _.intersection( modelReferenceIds, this.selectedItems );\n\n\t\t\tif( _.isFunction( this.selectableModelsFilter ) )\n\t\t\t{\n\t\t\t\tthis.selectedItems = _.filter( this.selectedItems, function( thisItemId ) {\n\t\t\t\t\treturn this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) );\n\t\t\t\t}, this );\n\t\t\t}\n\t\t},\n\n\t\t_saveSelection : function() {\n\t\t\t// save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called.\n\t\t\tif( ! this.selectable ) throw \"Attempt to save selection on non-selectable list\";\n\t\t\tthis.savedSelection = {\n\t\t\t\titems : _.clone( this.selectedItems ),\n\t\t\t\toffset : this.getSelectedModel( { by : \"offset\" } )\n\t\t\t};\n\t\t},\n\n\t\t_restoreSelection : function() {\n\t\t\tif( ! this.savedSelection ) throw \"Attempt to restore selection but no selection has been saved!\";\n\n\t\t\t// reset selectedItems to empty so that we \"redraw\" all \"selected\" classes\n\t\t\t// when we set our new selection. We do this because it is likely that our\n\t\t\t// contents have been refreshed, and we have thus lost all old \"selected\" classes.\n\t\t\tthis.setSelectedModels( [], { silent : true } );\n\n\t\t\tif( this.savedSelection.items.length > 0 )\n\t\t\t{\n\t\t\t\t// first try to restore the old selected items using their reference ids.\n\t\t\t\tthis.setSelectedModels( this.savedSelection.items, { by : \"cid\", silent : true } );\n\n\t\t\t\t// all the items with the saved reference ids have been removed from the list.\n\t\t\t\t// ok. try to restore the selection based on the offset that used to be selected.\n\t\t\t\t// this is the expected behavior after a item is deleted from a list (i.e. select\n\t\t\t\t// the line that immediately follows the deleted line).\n\t\t\t\tif( this.selectedItems.length === 0 )\n\t\t\t\t\tthis.setSelectedModel( this.savedSelection.offset, { by : \"offset\" } );\n\n\t\t\t\t// Trigger a selection changed if the previously selected items were not all found\n\t\t\t\tif (this.selectedItems.length !== this.savedSelection.items.length)\n\t\t\t\t{\n\t\t\t\t\tif( this._isBackboneCourierAvailable() ) {\n\t\t\t\t\t\tthis.spawn( \"selectionChanged\", {\n\t\t\t\t\t\t\tselectedModels : this.getSelectedModels(),\n\t\t\t\t\t\t\toldSelectedModels : []\n\t\t\t\t\t\t} );\n\t\t\t\t\t} else this.trigger( \"selectionChanged\", this.getSelectedModels(), [] );\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t_addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) {\n\t\t\tif( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = [];\n\n\t\t\t// oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we\n\t\t\t// only have to add / remove the \"selected\" class from those items that \"selected\" state has changed.\n\n\t\t\tvar itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass;\n\t\t\titemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems );\n\n\t\t\t_.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) {\n\t\t\t\tthis._getContainerEl().find( \"[data-model-cid=\" + thisItemId + \"]\" ).removeClass( \"selected\" );\n\n\t\t\t\tif( this._isRenderedAsList() ) {\n\t\t\t\t\tthis._getContainerEl().find( \"li[data-model-cid=\" + thisItemId + \"] > *\" ).removeClass( \"selected\" );\n\t\t\t\t}\n\t\t\t}, this );\n\n\t\t\tvar itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;\n\t\t\titemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );\n\n\t\t\t_.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {\n\t\t\t\tthis._getContainerEl().find( \"[data-model-cid=\" + thisItemId + \"]\" ).addClass( \"selected\" );\n\n\t\t\t\tif( this._isRenderedAsList() ) {\n\t\t\t\t\tthis._getContainerEl().find( \"li[data-model-cid=\" + thisItemId + \"] > *\" ).addClass( \"selected\" );\n\t\t\t\t}\n\t\t\t}, this );\n\t\t},\n\n\t\t_reorderCollectionBasedOnHTML : function() {\n\n\t\t\tvar _this = this;\n\n\t\t\tthis._getContainerEl().children().each( function() {\n\t\t\t\tvar thisModelCid = $( this ).attr( \"data-model-cid\" );\n\n\t\t\t\tif( thisModelCid )\n\t\t\t\t{\n\t\t\t\t\t// remove the current model and then add it back (at the end of the collection).\n\t\t\t\t\t// When we are done looping through all models, they will be in the correct order.\n\t\t\t\t\tvar thisModel = _this.collection.get( thisModelCid );\n\t\t\t\t\tif( thisModel )\n\t\t\t\t\t{\n\t\t\t\t\t\t_this.collection.remove( thisModel, { silent : true } );\n\t\t\t\t\t\t_this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tif( this._isBackboneCourierAvailable() ) this.spawn( \"reorder\" );\n\t\t\telse this.collection.trigger( \"reorder\" );\n\n\t\t\tif( this.collection.comparator ) this.collection.sort();\n\n\t\t},\n\n\t\t_getModelViewConstructor : function( thisModel ) {\n\t\t\treturn this.modelView || mDefaultModelViewConstructor;\n\t\t},\n\n\t\t_getModelViewOptions : function( thisModel ) {\n\t\t\tvar modelViewOptions = this.modelViewOptions;\n\t\t\tif( _.isFunction( modelViewOptions ) ) modelViewOptions = modelViewOptions( thisModel );\n\n\t\t\treturn _.extend( { model : thisModel }, modelViewOptions );\n\t\t},\n\n\t\t_createNewModelView : function( model, modelViewOptions ) {\n\t\t\tvar modelViewConstructor = this._getModelViewConstructor( model );\n\t\t\tif( _.isUndefined( modelViewConstructor ) ) throw \"Could not find modelView constructor for model\";\n\n\t\t\tvar newModelView = new( modelViewConstructor )( modelViewOptions );\n\t\t\tnewModelView.collectionListView = newModelView.collectionView = this;  // collectionListView for legacy\n\n\t\t\treturn newModelView;\n\t\t},\n\n\t\t_wrapModelView : function( modelView ) {\n\t\t\tvar _this = this;\n\n\t\t\t// we use items client ids as opposed to real ids, since we may not have a representation\n\t\t\t// of these models on the server\n\t\t\tvar modelViewWrapperEl;\n\n\t\t\tif( this._isRenderedAsTable() ) {\n\t\t\t\t// if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid\n\t\t\t\tmodelViewWrapperEl = modelView.$el;\n\t\t\t\tmodelView.$el.attr( \"data-model-cid\", modelView.model.cid );\n\t\t\t}\n\t\t\telse if( this._isRenderedAsList() ) {\n\t\t\t\t// if we are rendering the collection in a list, we need wrap each item in an <li></li> (if its not already an <li>)\n\t\t\t\t// and set the data-model-cid\n\t\t\t\tif( modelView.$el.is( \"li\" ) ) {\n\t\t\t\t\tmodelViewWrapperEl = modelView.$el;\n\t\t\t\t\tmodelView.$el.attr( \"data-model-cid\", modelView.model.cid );\n\t\t\t\t} else {\n\t\t\t\t\tmodelViewWrapperEl = modelView.$el.wrapAll( \"<li data-model-cid='\" + modelView.model.cid + \"'></li>\" ).parent();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif( _.isFunction( this.sortableModelsFilter ) )\n\t\t\t\tif( ! this.sortableModelsFilter.call( _this, modelView.model ) ) {\n\t\t\t\t\tmodelViewWrapperEl.addClass( \"not-sortable\" );\n\t\t\t\t\tmodelView.$el.addClass( \"not-selectable\" );\n\t\t\t\t}\n\n\t\t\tif( _.isFunction( this.selectableModelsFilter ) )\n\t\t\t\tif( ! this.selectableModelsFilter.call( _this, modelView.model ) ) {\n\t\t\t\t\tmodelViewWrapperEl.addClass( \"not-selectable\" );\n\t\t\t\t\tmodelView.$el.addClass( \"not-selectable\" );\n\t\t\t\t}\n\n\t\t\treturn modelViewWrapperEl;\n\t\t},\n\n\t\t_convertStringsToInts : function( theArray ) {\n\t\t\treturn _.map( theArray, function( thisEl ) {\n\t\t\t\tif( ! _.isString( thisEl ) ) return thisEl;\n\t\t\t\tvar thisElAsNumber = parseInt( thisEl, 10 );\n\t\t\t\treturn( thisElAsNumber == thisEl ? thisElAsNumber : thisEl );\n\t\t\t} );\n\t\t},\n\n\t\t_containSameElements : function( arrayA, arrayB ) {\n\t\t\tif( arrayA.length != arrayB.length ) return false;\n\t\t\tvar intersectionSize = _.intersection( arrayA, arrayB ).length;\n\t\t\treturn intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length\n\t\t},\n\n\t\t_isRenderedAsTable : function() {\n\t\t\treturn this.$el.prop( \"tagName\" ).toLowerCase() === \"table\";\n\t\t},\n\n\t\t_isRenderedAsList : function() {\n\t\t\treturn ! this._isRenderedAsTable();\n\t\t},\n\n\t\t_modelViewHasWrapperLI : function( modelView ) {\n\t\t\treturn this._isRenderedAsList() && ! modelView.$el.is( \"li\" );\n\t\t},\n\n\t\t// Returns the wrapper HTML element for each visible modelView.\n\t\t// When rendering in a table context, the returned elements are the $el of each modelView.\n\t\t// When rendering in a list context,\n\t\t//   If the $el of the modelView is an <li>, the returned elements are the $el of each modelView.\n\t\t//   Otherwise, the returned elements are the <li>'s the collectionView wrapped around each modelView $el.\n\t\t_getVisibleItemEls : function() {\n\t\t\tvar itemElements = [];\n\t\t\titemElements = this._getContainerEl().find( \"> [data-model-cid]:not(.not-visible)\" );\n\n\t\t\treturn itemElements;\n\t\t},\n\n\t\t_charCodes : {\n\t\t\tupArrow : 38,\n\t\t\tdownArrow : 40\n\t\t},\n\n\t\t_isBackboneCourierAvailable : function() {\n\t\t\treturn !_.isUndefined( Backbone.Courier );\n\t\t},\n\n\t\t_setupSortable : function() {\n\t\t\tvar sortableOptions = _.extend( {\n\t\t\t\taxis : \"y\",\n\t\t\t\tdistance : 10,\n\t\t\t\tforcePlaceholderSize : true,\n\t\t\t\titems : this._isRenderedAsTable() ? \"> tbody > tr:not(.not-sortable)\" : \"> li:not(.not-sortable)\",\n\t\t\t\tstart : _.bind( this._sortStart, this ),\n\t\t\t\tchange : _.bind( this._sortChange, this ),\n\t\t\t\tstop : _.bind( this._sortStop, this ),\n\t\t\t\treceive : _.bind( this._receive, this ),\n\t\t\t\tover : _.bind( this._over, this )\n\t\t\t}, _.result( this, \"sortableOptions\" ) );\n\n\t\t\tthis.$el = this.$el.sortable( sortableOptions );\n\t\t\t//this.$el.sortable( \"enable\" ); // in case it was disabled previously\n\t\t},\n\n\t\t_sortStart : function( event, ui ) {\n\t\t\tvar modelBeingSorted = this.collection.get( ui.item.attr( \"data-model-cid\" ) );\n\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\tthis.spawn( \"sortStart\", { modelBeingSorted : modelBeingSorted } );\n\t\t\telse this.trigger( \"sortStart\", modelBeingSorted );\n\t\t},\n\n\t\t_sortChange : function( event, ui ) {\n\t\t\tvar modelBeingSorted = this.collection.get( ui.item.attr( \"data-model-cid\" ) );\n\n\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\tthis.spawn( \"sortChange\", { modelBeingSorted : modelBeingSorted } );\n\t\t\telse this.trigger( \"sortChange\", modelBeingSorted );\n\t\t},\n\n\t\t_sortStop : function( event, ui ) {\n\t\t\tvar modelBeingSorted = this.collection.get( ui.item.attr( \"data-model-cid\" ) );\n\t\t\tvar modelViewContainerEl = this._getContainerEl();\n\t\t\tvar newIndex = modelViewContainerEl.children().index( ui.item );\n\n\t\t\tif( newIndex == -1 && modelBeingSorted ) {\n\t\t\t\t// the element was removed from this list. can happen if this sortable is connected\n\t\t\t\t// to another sortable, and the item was dropped into the other sortable.\n\t\t\t\tthis.collection.remove( modelBeingSorted );\n\t\t\t}\n\n\t\t\tif( ! modelBeingSorted ) return; // something is wacky. we don't mess with this case, preferring to guarantee that we can always provide a reference to the model\n\n\t\t\tthis._reorderCollectionBasedOnHTML();\n\t\t\tthis.updateDependentControls();\n\n\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\tthis.spawn( \"sortStop\", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );\n\t\t\telse this.trigger( \"sortStop\", modelBeingSorted, newIndex );\n\t\t},\n\n\t\t_receive : function( event, ui ) {\n\n\t\t\tvar senderListEl = ui.sender;\n\t\t\tvar senderCollectionListView = senderListEl.data( \"view\" );\n\t\t\tif( ! senderCollectionListView || ! senderCollectionListView.collection ) return;\n\n\t\t\tvar newIndex = this._getContainerEl().children().index( ui.item );\n\t\t\tvar modelReceived = senderCollectionListView.collection.get( ui.item.attr( \"data-model-cid\" ) );\n\t\t\tsenderCollectionListView.collection.remove( modelReceived );\n\t\t\tthis.collection.add( modelReceived, { at : newIndex } );\n\t\t\tmodelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.\n\t\t\tthis.setSelectedModel( modelReceived );\n\t\t},\n\n\t\t_over : function( event, ui ) {\n\t\t\t// when an item is being dragged into the sortable,\n\t\t\t// hide the empty list caption if it exists\n\t\t\tthis._getContainerEl().find( \"> var.empty-list-caption\" ).hide();\n\t\t},\n\n\t\t_onKeydown : function( event ) {\n\t\t\tif( ! this.processKeyEvents ) return true;\n\n\t\t\tvar trap = false;\n\n\t\t\tif( this.getSelectedModels( { by : \"offset\" } ).length == 1 )\n\t\t\t{\n\t\t\t\t// need to trap down and up arrows or else the browser\n\t\t\t\t// will end up scrolling a autoscroll div.\n\n\t\t\t\tvar currentOffset = this.getSelectedModel( { by : \"offset\" } );\n\t\t\t\tif( event.which === this._charCodes.upArrow && currentOffset !== 0 )\n\t\t\t\t{\n\t\t\t\t\tthis.setSelectedModel( currentOffset - 1, { by : \"offset\" } );\n\t\t\t\t\ttrap = true;\n\t\t\t\t}\n\t\t\t\telse if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 )\n\t\t\t\t{\n\t\t\t\t\tthis.setSelectedModel( currentOffset + 1, { by : \"offset\" } );\n\t\t\t\t\ttrap = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn ! trap;\n\t\t},\n\n\t\t_listItem_onMousedown : function( theEvent ) {\n\t\t\tvar clickedItemId = this._getClickedItemId( theEvent );\n\n\t\t\tif( clickedItemId ) {\n\t\t\t\tvar clickedModel = this.collection.get( clickedItemId );\n\t\t\t\tif( this._isBackboneCourierAvailable() ) {\n\t\t\t\t\tvar data = {\n\t\t\t\t\t\tclickedModel : clickedModel,\n\t\t\t\t\t\tmetaKeyPressed : theEvent.ctrlKey || theEvent.metaKey\n\t\t\t\t\t};\n\n\t\t\t\t\t_.each( [ 'preventDefault', 'stopPropagation', 'stopImmediatePropagation' ], function( thisMethod ) {\n\t\t\t\t\t\tdata[ thisMethod ] = function() {\n\t\t\t\t\t\t\ttheEvent[ thisMethod ]();\n\t\t\t\t\t\t};\n\t\t\t\t\t} );\n\n\t\t\t\t\tthis.spawn( \"click\", data );\n\t\t\t\t}\n\t\t\t\telse this.trigger( \"click\", clickedModel );\n\t\t\t}\n\n\t\t\tif( ! this.selectable || ! this.clickToSelect ) return;\n\n\t\t\tif( clickedItemId )\n\t\t\t{\n\t\t\t\t// Exit if an unselectable item was clicked\n\t\t\t\tif( _.isFunction( this.selectableModelsFilter ) &&\n\t\t\t\t\t! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) )\n\t\t\t\t{\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// a selectable list item was clicked\n\t\t\t\tif( this.selectMultiple && theEvent.shiftKey )\n\t\t\t\t{\n\t\t\t\t\tvar firstSelectedItemIndex = -1;\n\n\t\t\t\t\tif( this.selectedItems.length > 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\tthis.collection.find( function( thisItemModel ) {\n\t\t\t\t\t\t\tfirstSelectedItemIndex++;\n\n\t\t\t\t\t\t\t// exit when we find our first selected element\n\t\t\t\t\t\t\treturn _.contains( this.selectedItems, thisItemModel.cid );\n\t\t\t\t\t\t}, this );\n\t\t\t\t\t}\n\n\t\t\t\t\tvar clickedItemIndex = -1;\n\t\t\t\t\tthis.collection.find( function( thisItemModel ) {\n\t\t\t\t\t\tclickedItemIndex++;\n\n\t\t\t\t\t\t// exit when we find the clicked element\n\t\t\t\t\t\treturn thisItemModel.cid == clickedItemId;\n\t\t\t\t\t}, this );\n\n\t\t\t\t\tvar shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex;\n\t\t\t\t\tvar minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex );\n\t\t\t\t\tvar maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex );\n\n\t\t\t\t\tvar newSelectedItems = [];\n\t\t\t\t\tfor( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ )\n\t\t\t\t\t\tnewSelectedItems.push( this.collection.at( thisIndex ).cid );\n\t\t\t\t\tthis.setSelectedModels( newSelectedItems, { by : \"cid\" } );\n\n\t\t\t\t\t// shift clicking will usually highlight selectable text, which we do not want.\n\t\t\t\t\t// this is a cross browser (hopefully) snippet that deselects all text selection.\n\t\t\t\t\tif( document.selection && document.selection.empty )\n\t\t\t\t\t\tdocument.selection.empty();\n\t\t\t\t\telse if(window.getSelection) {\n\t\t\t\t\t\tvar sel = window.getSelection();\n\t\t\t\t\t\tif( sel && sel.removeAllRanges )\n\t\t\t\t\t\t\tsel.removeAllRanges();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if( ( this.selectMultiple || _.contains( this.selectedItems, clickedItemId ) ) && ( this.clickToToggle || theEvent.metaKey || theEvent.ctrlKey ) )\n\t\t\t\t{\n\t\t\t\t\tif( _.contains( this.selectedItems, clickedItemId ) )\n\t\t\t\t\t\tthis.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : \"cid\" } );\n\t\t\t\t\telse this.setSelectedModels( _.union( this.selectedItems, [clickedItemId] ), { by : \"cid\" } );\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\tthis.setSelectedModels( [ clickedItemId ], { by : \"cid\" } );\n\t\t\t}\n\t\t\telse\n\t\t\t\t// the blank area of the list was clicked\n\t\t\t\tthis.setSelectedModels( [] );\n\n\t\t},\n\n\t\t_listItem_onDoubleClick : function( theEvent ) {\n\n\t\t\tvar clickedItemId = this._getClickedItemId( theEvent );\n\n\t\t\tif( clickedItemId )\n\t\t\t{\n\t\t\t\tvar clickedModel = this.collection.get( clickedItemId );\n\n\t\t\t\tif( this._isBackboneCourierAvailable() )\n\t\t\t\t\tthis.spawn( \"doubleClick\", { clickedModel : clickedModel, metaKeyPressed : theEvent.ctrlKey || theEvent.metaKey } );\n\t\t\t\telse this.trigger( \"doubleClick\", clickedModel );\n\t\t\t}\n\t\t},\n\n\t\t_listBackground_onClick : function( theEvent ) {\n\t\t\tif( ! this.selectable || ! this.clickToSelect ) return;\n\t\t\tif( ! $( theEvent.target ).is( \".collection-view\" ) ) return;\n\n\t\t\tthis.setSelectedModels( [] );\n\t\t}\n\n\t}, {\n\t\tsetDefaultModelViewConstructor : function( theConstructor ) {\n\t\t\tmDefaultModelViewConstructor = theConstructor;\n\t\t}\n\t});\n\n\t/*\n\t* Backbone.ViewOptions, v0.2.4\n\t* Copyright (c)2014 Rotunda Software, LLC.\n\t* Distributed under MIT license\n\t* http://github.com/rotundasoftware/backbone.viewOptions\n\t*/\n\n\tBackbone.ViewOptions = {};\n\n\tBackbone.ViewOptions.add = function( view, optionsDeclarationsProperty ) {\n\t\tif( _.isUndefined( optionsDeclarationsProperty ) ) optionsDeclarationsProperty = \"options\";\n\n\t\t// ****************** Public methods added to view ******************\n\n\t\tview.setOptions = function( options ) {\n\t\t\tvar _this = this;\n\t\t\tvar optionsThatWereChanged = {};\n\t\t\tvar optionsThatWereChangedPreviousValues = {};\n\n\t\t\tvar optionDeclarations = _.result( this, optionsDeclarationsProperty );\n\n\t\t\tif( ! _.isUndefined( optionDeclarations ) ) {\n\t\t\t\tvar normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );\n\n\t\t\t\t_.each( normalizedOptionDeclarations, function( thisOptionProperties, thisOptionName ) {\n\t\t\t\t\tvar thisOptionRequired = thisOptionProperties.required;\n\t\t\t\t\tvar thisOptionDefaultValue = thisOptionProperties.defaultValue;\n\n\t\t\t\t\tif( thisOptionRequired ) {\n\t\t\t\t\t\t// note we do not throw an error if a required option is not supplied, but it is\n\t\t\t\t\t\t// found on the object itself (due to a prior call of view.setOptions, most likely)\n\n\t\t\t\t\t\tif( ( ! options || ! _.contains( _.keys( options ), thisOptionName ) ) && _.isUndefined( _this[ thisOptionName ] ) )\n\t\t\t\t\t\t\tthrow new Error( \"Required option \\\"\" + thisOptionName + \"\\\" was not supplied.\" );\n\n\t\t\t\t\t\tif( options && _.contains( _.keys( options ), thisOptionName ) && _.isUndefined( options[ thisOptionName ] ) )\n\t\t\t\t\t\t\tthrow new Error( \"Required option \\\"\" + thisOptionName + \"\\\" can not be set to undefined.\" );\n\t\t\t\t\t}\n\n\t\t\t\t\t// attach the supplied value of this option, or the appropriate default value, to the view object\n\t\t\t\t\tif( options && thisOptionName in options && ! _.isUndefined( options[ thisOptionName ] ) ) {\n\t\t\t\t\t\tvar oldValue = _this[ thisOptionName ];\n\t\t\t\t\t\tvar newValue = options[ thisOptionName ];\n\t\t\t\t\t\t// if this option already exists on the view, and the new value is different,\n\t\t\t\t\t\t// make a note that we will be changing it\n\t\t\t\t\t\tif( ! _.isUndefined( oldValue ) && oldValue !== newValue ) {\n\t\t\t\t\t\t\toptionsThatWereChangedPreviousValues[ thisOptionName ] = oldValue;\n\t\t\t\t\t\t\toptionsThatWereChanged[ thisOptionName ] = newValue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_this[ thisOptionName ] = newValue;\n\t\t\t\t\t\t// note we do NOT delete the option off the options object here so that\n\t\t\t\t\t\t// multiple views can be passed the same options object without issue.\n\t\t\t\t\t}\n\t\t\t\t\telse if( _.isUndefined( _this[ thisOptionName ] ) ) {\n\t\t\t\t\t\t// note defaults do not write over any existing properties on the view itself.\n\t\t\t\t\t\t_this[ thisOptionName ] = thisOptionDefaultValue;\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\tif( _.keys( optionsThatWereChanged ).length > 0 ) {\n\t\t\t\tif( _.isFunction( _this.onOptionsChanged ) )\n\t\t\t\t\t_this.onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedPreviousValues );\n\t\t\t\telse if( _.isFunction( _this._onOptionsChanged ) )\n\t\t\t\t\t_this._onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedPreviousValues );\n\t\t\t}\n\t\t};\n\n\t\tview.getOptions = function() {\n\t\t\tvar optionDeclarations = _.result( this, optionsDeclarationsProperty );\n\t\t\tif( _.isUndefined( optionDeclarations ) ) return {};\n\n\t\t\tvar normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );\n\t\t\tvar optionsNames = _.keys( normalizedOptionDeclarations );\n\n\t\t\treturn _.pick( this, optionsNames );\n\t\t};\n\t};\n\n\t// ****************** Private Utility Functions ******************\n\n\tfunction _normalizeOptionDeclarations( optionDeclarations ) {\n\t\t// convert our short-hand option syntax (with exclamation marks, etc.)\n\t\t// to a simple array of standard option declaration objects.\n\n\t\tvar normalizedOptionDeclarations = {};\n\n\t\tif( ! _.isArray( optionDeclarations ) ) throw new Error( \"Option declarations must be an array.\" );\n\n\t\t_.each( optionDeclarations, function( thisOptionDeclaration ) {\n\t\t\tvar thisOptionName, thisOptionRequired, thisOptionDefaultValue;\n\n\t\t\tthisOptionRequired = false;\n\t\t\tthisOptionDefaultValue = undefined;\n\n\t\t\tif( _.isString( thisOptionDeclaration ) )\n\t\t\t\tthisOptionName = thisOptionDeclaration;\n\t\t\telse if( _.isObject( thisOptionDeclaration ) ) {\n\t\t\t\tthisOptionName = _.first( _.keys( thisOptionDeclaration ) );\n\t\t\t\tif( _.isFunction( thisOptionDeclaration[ thisOptionName ] ) )\n\t\t\t\t\tthisOptionDefaultValue = thisOptionDeclaration[ thisOptionName ];\n\t\t\t\telse\n\t\t\t\t\tthisOptionDefaultValue = _.clone( thisOptionDeclaration[ thisOptionName ] );\n\t\t\t}\n\t\t\telse throw new Error( \"Each element in the option declarations array must be either a string or an object.\" );\n\n\t\t\tif( thisOptionName[ thisOptionName.length - 1 ] === \"!\" ) {\n\t\t\t\tthisOptionRequired = true;\n\t\t\t\tthisOptionName = thisOptionName.slice( 0, thisOptionName.length - 1 );\n\t\t\t}\n\n\t\t\tnormalizedOptionDeclarations[ thisOptionName ] = normalizedOptionDeclarations[ thisOptionName ] || {};\n\t\t\tnormalizedOptionDeclarations[ thisOptionName ].required = thisOptionRequired;\n\t\t\tif( ! _.isUndefined( thisOptionDefaultValue ) ) normalizedOptionDeclarations[ thisOptionName ].defaultValue = thisOptionDefaultValue;\n\t\t} );\n\n\t\treturn normalizedOptionDeclarations;\n\t}\n\n\n\t// Backbone.BabySitter\n\t// -------------------\n\t// v0.0.6\n\t//\n\t// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.\n\t// Distributed under MIT license\n\t//\n\t// http://github.com/babysitterjs/backbone.babysitter\n\n\t// Backbone.ChildViewContainer\n\t// ---------------------------\n\t//\n\t// Provide a container to store, retrieve and\n\t// shut down child views.\n\n\tChildViewContainer = (function(Backbone, _){\n\n\t\t// Container Constructor\n\t\t// ---------------------\n\n\t\tvar Container = function(views){\n\t\t\tthis._views = {};\n\t\t\tthis._indexByModel = {};\n\t\t\tthis._indexByCustom = {};\n\t\t\tthis._updateLength();\n\n\t\t\t_.each(views, this.add, this);\n\t\t};\n\n\t\t// Container Methods\n\t\t// -----------------\n\n\t\t_.extend(Container.prototype, {\n\n\t\t\t// Add a view to this container. Stores the view\n\t\t\t// by `cid` and makes it searchable by the model\n\t\t\t// cid (and model itself). Optionally specify\n\t\t\t// a custom key to store an retrieve the view.\n\t\t\tadd: function(view, customIndex){\n\t\t\t\tvar viewCid = view.cid;\n\n\t\t\t\t// store the view\n\t\t\t\tthis._views[viewCid] = view;\n\n\t\t\t\t// index it by model\n\t\t\t\tif (view.model){\n\t\t\t\t\tthis._indexByModel[view.model.cid] = viewCid;\n\t\t\t\t}\n\n\t\t\t\t// index by custom\n\t\t\t\tif (customIndex){\n\t\t\t\t\tthis._indexByCustom[customIndex] = viewCid;\n\t\t\t\t}\n\n\t\t\t\tthis._updateLength();\n\t\t\t},\n\n\t\t\t// Find a view by the model that was attached to\n\t\t\t// it. Uses the model's `cid` to find it.\n\t\t\tfindByModel: function(model){\n\t\t\t\treturn this.findByModelCid(model.cid);\n\t\t\t},\n\n\t\t\t// Find a view by the `cid` of the model that was attached to\n\t\t\t// it. Uses the model's `cid` to find the view `cid` and\n\t\t\t// retrieve the view using it.\n\t\t\tfindByModelCid: function(modelCid){\n\t\t\t\tvar viewCid = this._indexByModel[modelCid];\n\t\t\t\treturn this.findByCid(viewCid);\n\t\t\t},\n\n\t\t\t// Find a view by a custom indexer.\n\t\t\tfindByCustom: function(index){\n\t\t\t\tvar viewCid = this._indexByCustom[index];\n\t\t\t\treturn this.findByCid(viewCid);\n\t\t\t},\n\n\t\t\t// Find by index. This is not guaranteed to be a\n\t\t\t// stable index.\n\t\t\tfindByIndex: function(index){\n\t\t\t\treturn _.values(this._views)[index];\n\t\t\t},\n\n\t\t\t// retrieve a view by it's `cid` directly\n\t\t\tfindByCid: function(cid){\n\t\t\t\treturn this._views[cid];\n\t\t\t},\n\n\t\t\tfindIndexByCid : function( cid ) {\n\t\t\t\tvar index = -1;\n\t\t\t\tvar view = _.find( this._views, function ( view ) {\n\t\t\t\t\tindex++;\n\t\t\t\t\tif( view.model.cid == cid )\n\t\t\t\t\t\treturn view;\n\t\t\t\t} );\n\t\t\t\treturn ( view ) ? index : -1;\n\t\t\t},\n\n\t\t\t// Remove a view\n\t\t\tremove: function(view){\n\t\t\t\tvar viewCid = view.cid;\n\n\t\t\t\t// delete model index\n\t\t\t\tif (view.model){\n\t\t\t\t\tdelete this._indexByModel[view.model.cid];\n\t\t\t\t}\n\n\t\t\t\t// delete custom index\n\t\t\t\t_.any(this._indexByCustom, function(cid, key) {\n\t\t\t\t\tif (cid === viewCid) {\n\t\t\t\t\t\tdelete this._indexByCustom[key];\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}, this);\n\n\t\t\t\t// remove the view from the container\n\t\t\t\tdelete this._views[viewCid];\n\n\t\t\t\t// update the length\n\t\t\t\tthis._updateLength();\n\t\t\t},\n\n\t\t\t// Call a method on every view in the container,\n\t\t\t// passing parameters to the call method one at a\n\t\t\t// time, like `function.call`.\n\t\t\tcall: function(method){\n\t\t\t\tthis.apply(method, _.tail(arguments));\n\t\t\t},\n\n\t\t\t// Apply a method on every view in the container,\n\t\t\t// passing parameters to the call method one at a\n\t\t\t// time, like `function.apply`.\n\t\t\tapply: function(method, args){\n\t\t\t\t_.each(this._views, function(view){\n\t\t\t\t\tif (_.isFunction(view[method])){\n\t\t\t\t\t\tview[method].apply(view, args || []);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\n\t\t\t// Update the `.length` attribute on this container\n\t\t\t_updateLength: function(){\n\t\t\t\tthis.length = _.size(this._views);\n\t\t\t}\n\t\t});\n\n\t\t// Borrowing this code from Backbone.Collection:\n\t\t// http://backbonejs.org/docs/backbone.html#section-106\n\t\t//\n\t\t// Mix in methods from Underscore, for iteration, and other\n\t\t// collection related features.\n\t\tvar methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',\n\t\t\t       'select', 'reject', 'every', 'all', 'some', 'any', 'include',\n\t\t\t       'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',\n\t\t\t       'last', 'without', 'isEmpty', 'pluck'];\n\n\t\t_.each(methods, function(method) {\n\t\t\tContainer.prototype[method] = function() {\n\t\t\t\tvar views = _.values(this._views);\n\t\t\t\tvar args = [views].concat(_.toArray(arguments));\n\t\t\t\treturn _[method].apply(_, args);\n\t\t\t};\n\t\t});\n\n\t\t// return the public API\n\t\treturn Container;\n\t})(Backbone, _);\n\n\treturn Backbone.CollectionView;\n} ) );\n"
  },
  {
    "path": "assets/js/builder/vendor/backbone.trackit.js",
    "content": "/**\n * backbone.trackit - 0.1.0\n *\n * The MIT License\n * Copyright (c) 2013 The New York Times, CMS Group, Matthew DeLambo <delambo@gmail.com>\n *\n * @since 7.4.0 Added support for deep models (attributes that are objects themselves).\n */\n(function() {\n\n\t// Unsaved Record Keeping\n\t// ----------------------\n\n\t// Collection of all models in an app that have unsaved changes.\n\tvar unsavedModels = [];\n\n\t// If the given model has unsaved changes then add it to\n\t// the `unsavedModels` collection, otherwise remove it.\n\tvar updateUnsavedModels = function(model) {\n\t\tif (!_.isEmpty(model._unsavedChanges)) {\n\t\t\tif (!_.findWhere(unsavedModels, {cid:model.cid})) unsavedModels.push(model);\n\t\t} else {\n\t\t\tunsavedModels = _.filter(unsavedModels, function(m) { return model.cid != m.cid; });\n\t\t}\n\t};\n\n\t// Unload Handlers\n\t// ---------------\n\n\t// Helper which returns a prompt message for an unload handler.\n\t// Uses the given function name (one of the callback names\n\t// from the `model.unsaved` configuration hash) to evaluate\n\t// whether a prompt is needed/returned.\n\tvar getPrompt = function(fnName) {\n\t\tvar prompt, args = _.rest(arguments);\n\t\t// Evaluate and return a boolean result. The given `fn` may be a\n\t\t// boolean value, a function, or the name of a function on the model.\n\t\tvar evaluateModelFn = function(model, fn) {\n\t\t\tif (_.isBoolean(fn)) return fn;\n\t\t\treturn (_.isString(fn) ? model[fn] : fn).apply(model, args);\n\t\t};\n\t\t_.each(unsavedModels, function(model) {\n\t\t\tif (!prompt && evaluateModelFn(model, model._unsavedConfig[fnName]))\n\t\t\t\tprompt = model._unsavedConfig.prompt;\n\t\t});\n\t\treturn prompt;\n\t};\n\n\t// Wrap Backbone.History.navigate so that in-app routing\n\t// (`router.navigate('/path')`) can be intercepted with a\n\t// confirmation if there are any unsaved models.\n\tBackbone.History.prototype.navigate = _.wrap(Backbone.History.prototype.navigate, function(oldNav, fragment, options) {\n\t\tvar prompt = getPrompt('unloadRouterPrompt', fragment, options);\n\t\tif (prompt) {\n\t\t\tif (confirm(prompt + ' \\n\\nAre you sure you want to leave this page?')) {\n\t\t\t\toldNav.call(this, fragment, options);\n\t\t\t}\n\t\t} else {\n\t\t\toldNav.call(this, fragment, options);\n\t\t}\n\t});\n\n\t// Create a browser unload handler which is triggered\n\t// on the refresh, back, or forward button.\n\twindow.onbeforeunload = function(e) {\n\t\treturn getPrompt('unloadWindowPrompt', e);\n\t};\n\n\t// Backbone.Model API\n\t// ------------------\n\n\t_.extend(Backbone.Model.prototype, {\n\n\t\tunsaved: {},\n\t\t_trackingChanges: false,\n\t\t_originalAttrs: {},\n\t\t_unsavedChanges: {},\n\n\t\t// Opt in to tracking attribute changes\n\t\t// between saves.\n\t\tstartTracking: function() {\n\t\t\tthis._unsavedConfig = _.extend({}, {\n\t\t\t\tprompt: 'You have unsaved changes!',\n\t\t\t\tunloadRouterPrompt: false,\n\t\t\t\tunloadWindowPrompt: false\n\t\t\t}, this.unsaved || {});\n\t\t\tthis._trackingChanges = true;\n\t\t\tthis._resetTracking();\n\t\t\tthis._triggerUnsavedChanges();\n\t\t\treturn this;\n\t\t},\n\n\t\t// Resets the default tracking values\n\t\t// and stops tracking attribute changes.\n\t\tstopTracking: function() {\n\t\t\tthis._trackingChanges = false;\n\t\t\tthis._originalAttrs = {};\n\t\t\tthis._unsavedChanges = {};\n\t\t\tthis._triggerUnsavedChanges();\n\t\t\treturn this;\n\t\t},\n\n\t\t// Gets rid of accrued changes and\n\t\t// resets state.\n\t\trestartTracking: function() {\n\t\t\tthis._resetTracking();\n\t\t\tthis._triggerUnsavedChanges();\n\t\t\treturn this;\n\t\t},\n\n\t\t// Restores this model's attributes to\n\t\t// their original values since tracking\n\t\t// started, the last save, or last restart.\n\t\tresetAttributes: function() {\n\t\t\tif (!this._trackingChanges) return;\n\t\t\tthis.attributes = this._originalAttrs;\n\t\t\tthis._resetTracking();\n\t\t\tthis._triggerUnsavedChanges();\n\t\t\treturn this;\n\t\t},\n\n\t\t// Symmetric to Backbone's `model.changedAttributes()`,\n\t\t// except that this returns a hash of the model's attributes that\n\t\t// have changed since the last save, or `false` if there are none.\n\t\t// Like `changedAttributes`, an external attributes hash can be\n\t\t// passed in, returning the attributes in that hash which differ\n\t\t// from the model.\n\t\tunsavedAttributes: function(attrs) {\n\t\t\tif (!attrs) return _.isEmpty(this._unsavedChanges) ? false : _.clone(this._unsavedChanges);\n\t\t\tvar val, changed = false, old = this._unsavedChanges;\n\t\t\tfor (var attr in attrs) {\n\t\t\t\tif (_.isEqual(old[attr], (val = attrs[attr]))) continue;\n\t\t\t\t(changed || (changed = {}))[attr] = val;\n\t\t\t}\n\t\t\treturn changed;\n\t\t},\n\n\t\t/**\n\t\t * Reset tracking.\n\t\t *\n\t\t * @since 7.4.0 Added support for deep models (attributes that are objects themselves),\n\t\t *                  by using `_.deepClone` in place of `_.clone`.\n\t\t */\n\t\t_resetTracking: function() {\n\t\t\tthis._originalAttrs = _.deepClone(this.attributes);\n\t\t\tthis._unsavedChanges = {};\n\t\t},\n\n\t\t// Trigger an `unsavedChanges` event on this model,\n\t\t// supplying the result of whether there are unsaved\n\t\t// changes and a changed attributes hash.\n\t\t_triggerUnsavedChanges: function() {\n\t\t\tthis.trigger('unsavedChanges', !_.isEmpty(this._unsavedChanges), _.clone(this._unsavedChanges));\n\t\t\tif (this.unsaved) updateUnsavedModels(this);\n\t\t}\n\t});\n\n\t// Wrap `model.set()` and update the internal\n\t// unsaved changes record keeping.\n\tBackbone.Model.prototype.set = _.wrap(Backbone.Model.prototype.set, function(oldSet, key, val, options) {\n\t\tvar attrs, ret;\n\t\tif (key == null) return this;\n\t\t// Handle both `\"key\", value` and `{key: value}` -style arguments.\n\t\tif (typeof key === 'object') {\n\t\t\tattrs = key;\n\t\t\toptions = val;\n\t\t} else {\n\t\t\t(attrs = {})[key] = val;\n\t\t}\n\t\toptions || (options = {});\n\n\t\t// Delegate to Backbone's set.\n\t\tret = oldSet.call(this, attrs, options);\n\n\t\tif (this._trackingChanges && !options.silent) {\n\t\t\t_.each(attrs, _.bind(function(val, key) {\n\t\t\t\tif (_.isEqual(this._originalAttrs[key], val))\n\t\t\t\t\tdelete this._unsavedChanges[key];\n\t\t\t\telse\n\t\t\t\t\tthis._unsavedChanges[key] = val;\n\t\t\t}, this));\n\t\t\tthis._triggerUnsavedChanges();\n\t\t}\n\t\treturn ret;\n\t});\n\n\t// Intercept `model.save()` and reset tracking/unsaved\n\t// changes if it was successful.\n\tBackbone.sync = _.wrap(Backbone.sync, function(oldSync, method, model, options) {\n\t\toptions || (options = {});\n\n\t\tif (method == 'update') {\n\t\t\toptions.success = _.wrap(options.success, _.bind(function(oldSuccess, data, textStatus, jqXHR) {\n\t\t\t\tvar ret;\n\t\t\t\tif (oldSuccess) ret = oldSuccess.call(this, data, textStatus, jqXHR);\n\t\t\t\tif (model._trackingChanges) {\n\t\t\t\t\tmodel._resetTracking();\n\t\t\t\t\tmodel._triggerUnsavedChanges();\n\t\t\t\t}\n\t\t\t\treturn ret;\n\t\t\t}, this));\n\t\t}\n\t\treturn oldSync(method, model, options);\n\t});\n\n})();\n"
  },
  {
    "path": "assets/js/builder/vendor/wp-hooks.js",
    "content": "/**\n * This is a slightly modified and forward compatible version of the @wordpress/hooks package\n * as included in the Gutenberg feature plugin version 3.8.0\n */\nwindow.llms=window.llms||{};\n// use the core hooks if available\nif ( 'undefined' !== typeof window.wp && 'undefined' !== typeof window.wp.hooks ) {\n\twindow.llms.hooks = window.wp.hooks;\n// otherwise load our own\n} else {\n\twindow.llms.hooks=function(n){var r={};function e(t){if(r[t])return r[t].exports;var o=r[t]={i:t,l:!1,exports:{}};return n[t].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=r,e.d=function(n,r,t){e.o(n,r)||Object.defineProperty(n,r,{configurable:!1,enumerable:!0,get:t})},e.r=function(n){Object.defineProperty(n,\"__esModule\",{value:!0})},e.n=function(n){var r=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(r,\"a\",r),r},e.o=function(n,r){return Object.prototype.hasOwnProperty.call(n,r)},e.p=\"\",e(e.s=209)}({209:function(n,r,e){\"use strict\";e.r(r);var t=function(n){return\"string\"!=typeof n||\"\"===n?(console.error(\"The namespace must be a non-empty string.\"),!1):!!/^[a-zA-Z][a-zA-Z0-9_.\\-\\/]*$/.test(n)||(console.error(\"The namespace can only contain numbers, letters, dashes, periods, underscores and slashes.\"),!1)};var o=function(n){return\"string\"!=typeof n||\"\"===n?(console.error(\"The hook name must be a non-empty string.\"),!1):/^__/.test(n)?(console.error(\"The hook name cannot begin with `__`.\"),!1):!!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(n)||(console.error(\"The hook name can only contain numbers, letters, dashes, periods and underscores.\"),!1)};var i=function(n){return function(r,e,i){var u=arguments.length>3&&void 0!==arguments[3]?arguments[3]:10;if(o(r)&&t(e))if(\"function\"==typeof i)if(\"number\"==typeof u){var c={callback:i,priority:u,namespace:e};if(n[r]){for(var a=n[r].handlers,l=0;l<a.length&&!(a[l].priority>u);)l++;a.splice(l,0,c),(n.__current||[]).forEach(function(n){n.name===r&&n.currentIndex>=l&&n.currentIndex++})}else n[r]={handlers:[c],runs:0};\"hookAdded\"!==r&&b(\"hookAdded\",r,e,i,u)}else console.error(\"If specified, the hook priority must be a number.\");else console.error(\"The hook callback must be a function.\")}};var u=function(n,r){return function(e,i){if(o(e)&&(r||t(i))){if(!n[e])return 0;var u=0;if(r)u=n[e].handlers.length,n[e]={runs:n[e].runs,handlers:[]};else for(var c=n[e].handlers,a=function(r){c[r].namespace===i&&(c.splice(r,1),u++,(n.__current||[]).forEach(function(n){n.name===e&&n.currentIndex>=r&&n.currentIndex--}))},l=c.length-1;l>=0;l--)a(l);return\"hookRemoved\"!==e&&b(\"hookRemoved\",e,i),u}}};var c=function(n){return function(r){return r in n}};var a=function(n,r){return function(e){n[e]||(n[e]={handlers:[],runs:0}),n[e].runs++;for(var t=n[e].handlers,o=arguments.length,i=new Array(o>1?o-1:0),u=1;u<o;u++)i[u-1]=arguments[u];if(!t||!t.length)return r?i[0]:void 0;var c={name:e,currentIndex:0};for(n.__current.push(c),n[e]||(n[e]={runs:0,handlers:[]});c.currentIndex<t.length;){var a=t[c.currentIndex].callback.apply(null,i);r&&(i[0]=a),c.currentIndex++}return n.__current.pop(),r?i[0]:void 0}};var l=function(n){return function(){return n.__current&&n.__current.length?n.__current[n.__current.length-1].name:null}};var s=function(n){return function(r){return void 0===r?void 0!==n.__current[0]:!!n.__current[0]&&r===n.__current[0].name}};var d=function(n){return function(r){if(o(r))return n[r]&&n[r].runs?n[r].runs:0}};var f=function(){var n=Object.create(null),r=Object.create(null);return n.__current=[],r.__current=[],{addAction:i(n),addFilter:i(r),removeAction:u(n),removeFilter:u(r),hasAction:c(n),hasFilter:c(r),removeAllActions:u(n,!0),removeAllFilters:u(r,!0),doAction:a(n),applyFilters:a(r,!0),currentAction:l(n),currentFilter:l(r),doingAction:s(n),doingFilter:s(r),didAction:d(n),didFilter:d(r),actions:n,filters:r}};e.d(r,\"addAction\",function(){return p}),e.d(r,\"addFilter\",function(){return v}),e.d(r,\"removeAction\",function(){return m}),e.d(r,\"removeFilter\",function(){return A}),e.d(r,\"hasAction\",function(){return _}),e.d(r,\"hasFilter\",function(){return F}),e.d(r,\"removeAllActions\",function(){return g}),e.d(r,\"removeAllFilters\",function(){return y}),e.d(r,\"doAction\",function(){return b}),e.d(r,\"applyFilters\",function(){return k}),e.d(r,\"currentAction\",function(){return x}),e.d(r,\"currentFilter\",function(){return I}),e.d(r,\"doingAction\",function(){return w}),e.d(r,\"doingFilter\",function(){return O}),e.d(r,\"didAction\",function(){return T}),e.d(r,\"didFilter\",function(){return j}),e.d(r,\"actions\",function(){return z}),e.d(r,\"filters\",function(){return Z}),e.d(r,\"createHooks\",function(){return f});var h=f(),p=h.addAction,v=h.addFilter,m=h.removeAction,A=h.removeFilter,_=h.hasAction,F=h.hasFilter,g=h.removeAllActions,y=h.removeAllFilters,b=h.doAction,k=h.applyFilters,x=h.currentAction,I=h.currentFilter,w=h.doingAction,O=h.doingFilter,T=h.didAction,j=h.didFilter,z=h.actions,Z=h.filters}});\n}\n"
  },
  {
    "path": "assets/js/llms-admin-forms.js",
    "content": "/**\n * Show an upgrade to custom fields notice when viewing the forms post type table\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\n( function() {\n\n\tvar __         = window.wp.i18n.__,\n\t\tBTN_CLASS  = 'page-title-action',\n\t\tHELP_CLASS = 'llms-forms-help-text',\n\t\taddNewBtn = document.querySelector( '.' + BTN_CLASS );\n\n\t// Don't do anything if the button already exists.\n\tif ( addNewBtn ) {\n\t\treturn;\n\t}\n\n\t/**\n\t * Create the disabled \"Add New Form\" button\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {Element} Button DOM node.\n\t */\n\tfunction createNewButton() {\n\n\t\tvar btn = document.createElement( 'button' );\n\n\t\tbtn.className = BTN_CLASS + ' button';\n\t\tbtn.innerHTML = __( 'Add New Form', 'lifterlms' );\n\t\tbtn.disabled  = 'disabled';\n\t\tbtn.style     = 'vertical-align: inherit';\n\n\t\treturn btn;\n\n\t}\n\n\t/**\n\t * Create the toggle \"Help\" icon button\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {Element} Button DOM node.\n\t */\n\tfunction createHelpIcon() {\n\n\t\tvar btn = document.createElement( 'button' ),\n\t\t\ttxt = __( 'Help', 'lifterlms' );\n\n\t\tbtn.className = 'button dashicons dashicons-editor-help';\n\t\tbtn.style     = [\n\t\t\t'background-color: #466dd8',\n\t\t\t'border-radius: 50%;',\n\t\t\t'border-color: #466dd8',\n\t\t\t'color: #FFF',\n\t\t\t'font-size: 23px;',\n\t\t\t'height: 30px;',\n\t\t\t'line-height: 1;',\n\t\t\t'margin-left: 5px;',\n\t\t\t'padding: 0;',\n\t\t\t'position: relative;',\n\t\t\t'top: 3px',\n\t\t\t'vertical-align: baseline;',\n\t\t\t'width: 30px;',\n\t\t].join( ';' );\n\n\t\tbtn.innerHTML = '<span class=\"screen-reader-text>' + txt + '</span>';\n\t\tbtn.title     = __( 'Help', 'lifterlms' );\n\n\t\tbtn.addEventListener( 'click', toggleHelpNode );\n\n\t\treturn btn;\n\n\t}\n\n\t/**\n\t * Create the help notice node\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {Element} Notice div DOM node.\n\t */\n\tfunction createHelpNode() {\n\n\t\tvar div = document.createElement( 'div' );\n\n\t\tdiv.className = HELP_CLASS;\n\t\tdiv.style     = 'display:none';\n\n\t\tdiv.innerHTML = '<div class=\"llms-admin-notice-icon\"></div><div class=\"llms-admin-notice-content\"><h3>Create Custom Forms and Fields</h3><p>Create unique student information forms for specific courses and memberships. Also unlock the power of custom fields so you can collect and display any form field data you can imagine.</p><p><a class=\"llms-button-primary\" target=\"_blank\" rel=\"noopener\" href=\"https://lifterlms.com/product/custom-fields/?utm_source=LifterLMS%20Plugin&utm_medium=Add%20Form%20Notice&utm_campaign=Add%20Form%20In%20App%20Upgrade%20Flow\">Learn More</a></p></div>';\n\n\t\treturn div;\n\n\t}\n\n\t/**\n\t * Callback function for toggling the help notice dispaly\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {void}\n\t */\n\tfunction toggleHelpNode() {\n\n\t\tvar el = document.querySelector( '.' + HELP_CLASS );\n\n\t\tif ( 'none' === el.style.display ) {\n\t\t\tel.style.display = 'flex';\n\t\t\tel.className += ' notice notice-info llms-admin-notice';\n\t\t} else {\n\t\t\tel.style.display = 'none';\n\t\t}\n\n\t}\n\n\t/**\n\t * Initialize\n\t *\n\t * Creates and add elements to the dom and binds UI events.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return {void}\n\t */\n\tfunction init() {\n\n\t\tvar title = document.querySelector( '.wp-heading-inline' ),\n\t\t\tbtn   = createNewButton();\n\n\t\ttitle.after( btn );\n\t\tbtn.after( createHelpIcon() );\n\n\t\tdocument.querySelector( '.wrap' ).insertBefore( createHelpNode(), document.querySelector( '.wp-header-end' ) );\n\n\t}\n\n\t// Go.\n\tinit();\n\n} )();\n\n\n"
  },
  {
    "path": "assets/js/llms-admin-media-protection-attachment-settings.js",
    "content": "( function() {\n\tif ( 'undefined' === typeof wp || 'undefined' === typeof wp.media || 'undefined' === typeof wp.media.view || 'undefined' === typeof wp.media.view.Attachment || 'undefined' === typeof wp.media.view.Attachment.Details ) {\n\t\treturn;\n\t}\n\n\tvar originalCompat = wp.media.view.AttachmentCompat;\n\twp.media.view.AttachmentCompat = originalCompat.extend( {\n\t\tinitialize: function() {\n\t\t\t// Call the parent initialize.\n\t\t\toriginalCompat.prototype.initialize.apply( this, arguments );\n\n\t\t\t// Bind to the render event.\n\t\t\tthis.on( 'compatRendered', this.initializeLifterlmsSelect2 );\n\n\t\t\t// Listen for changes to the protection dropdown.\n\t\t\tthis.listenTo( this.model, 'change', this.refreshAttachmentUrl );\n\t\t},\n\n\t\trender: function() {\n\t\t\t// Call the parent render\n\t\t\toriginalCompat.prototype.render.apply(this, arguments);\n\n\t\t\t// Trigger our custom event after render.\n\t\t\t// TODO: Check if we need the defer.\n\t\t\t_.defer(() => {\n\t\t\t\tthis.trigger( 'compatRendered' );\n\t\t\t});\n\n\t\t\treturn this;\n\t\t},\n\n\t\tinitializeLifterlmsSelect2: function() {\n\t\t\tconst $select = jQuery( '.media-modal .llms-posts-select2' );\n\t\t\tif ( $select.length && ! $select.data( 'select2' )) {\n\t\t\t\t$select.llmsPostsSelect2();\n\t\t\t}\n\t\t},\n\n\t\trefreshAttachmentUrl: function() {\n\t\t\t// When the protection setting changes, we need to refresh the URL\n\t\t\tif ( this.model.hasChanged( 'url' ) ) {\n\t\t\t\t// Update the URL in the UI.\n\t\t\t\tjQuery( '#attachment-details-copy-link' ).val( this.model.get( 'url' ) );\n\t\t\t}\n\t\t}\n\t} );\n} )();\n"
  },
  {
    "path": "assets/js/llms-admin-settings.js",
    "content": ";/**\n * LifterLMS Settings Pages UI / UX\n *\n * @since    3.7.3\n * @version  3.18.0\n */( function( $, undefined ) {\n\n\twindow.llms                = window.llms || {};\n\twindow.llms.admin_settings = function() {\n\n\t\tthis.file_frame = null;\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.7.3\n\t\t * @version  3.18.0\n\t\t */\n\t\tthis.init = function() {\n\t\t\tthis.bind();\n\t\t\tthis.bind_conditionals();\n\t\t};\n\n\t\t/**\n\t\t * Bind DOM events\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.7.3\n\t\t * @version  3.17.5\n\t\t */\n\t\tthis.bind = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tif ( $( '.llms-image-field-upload' ).length ) {\n\t\t\t\t$( '.llms-image-field-upload' ).on( 'click', function( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tself.image_upload_click( $( this ), e );\n\t\t\t\t} );\n\n\t\t\t\t$( '.llms-image-field-remove' ).on( 'click', function( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tself.update_image( $( this ), '', '' );\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\tif ( $( '.llms-gateway-table' ).length ) {\n\t\t\t\t$( '.llms-gateway-table tbody' ).sortable( {\n\t\t\t\t\taxis: 'y',\n\t\t\t\t\tcursor: 'move',\n\t\t\t\t\titems: 'tr',\n\t\t\t\t\t// containment: 'parent',\n\t\t\t\t\thandle: 'td.sort',\n\t\t\t\t\thelper: function( event, ui ) {\n\t\t\t\t\t\tui.children().each( function() {\n\t\t\t\t\t\t\t$( this ).width( $( this ).width() );\n\t\t\t\t\t\t} );\n\t\t\t\t\t\t// ui.css( 'left', '0' );\n\t\t\t\t\t\treturn ui;\n\t\t\t\t\t},\n\t\t\t\t\tupdate: function( event, ui ) {\n\t\t\t\t\t\t$( this ).find( 'td.sort input' ).each( function( i ) {\n\t\t\t\t\t\t\t$( this ).val( i );\n\t\t\t\t\t\t} );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Allow checkboxes to conditionally display other settings\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.18.0\n\t\t * @version  3.18.0\n\t\t */\n\t\tthis.bind_conditionals = function() {\n\n\t\t\t$( '.llms-conditional-controller' ).each( function() {\n\n\t\t\t\t$( this ).on( 'change', function() {\n\n\n\t\t\t\t\tif ( 'checkbox' === $( this ).attr( 'type' ) ) {\n\t\t\t\t\t\tvar $controls = $( $( this ).attr( 'data-controls' ) ).closest( 'tr' );\n\n\t\t\t\t\t\tif ( $( this ).is( ':checked' ) ) {\n\t\t\t\t\t\t\t$controls.show();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t$controls.hide();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif ( 'select' !== $( this ).prop( 'tagName' ).toLowerCase() ) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t$( this ).find( 'option' ).each( function() {\n\t\t\t\t\t\tvar val = $( this ).val();\n\t\t\t\t\t\tif ( ! val ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar $controls_for_option = $( $( this ).parent().attr( 'data-controls-' + $.escapeSelector( val ) ) ).closest( 'tr' );\n\n\t\t\t\t\t\tif ( $( this ).is( ':selected' ) ) {\n\t\t\t\t\t\t\t$controls_for_option.show();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t$controls_for_option.hide();\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\n\n\t\t\t\t} ).trigger( 'change' );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Click event for image upload fields\n\t\t *\n\t\t * @param    obj   $btn  jQuery object for clicked button\n\t\t * @param    obj   e     JS event object\n\t\t * @return   void\n\t\t * @since    3.7.3\n\t\t * @version  3.7.3\n\t\t */\n\t\tthis.image_upload_click = function( $btn, e ) {\n\n\t\t\tvar self  = this,\n\t\t\t\tframe = null;\n\n\t\t\tif ( ! frame ) {\n\t\t\t\tvar title       = $btn.attr( 'data-frame-title' ) || LLMS.l10n.translate( 'Select an Image' ),\n\t\t\t\t\tbutton_text = $btn.attr( 'data-frame-button' ) || LLMS.l10n.translate( 'Select Image' );\n\t\t\t\tframe           = wp.media.frames.file_frame = wp.media({\n\t\t\t\t\ttitle: title,\n\t\t\t\t\tbutton: {\n\t\t\t\t\t\ttext: button_text,\n\t\t\t\t\t},\n\t\t\t\t\tmultiple: false\t// Set to true to allow multiple files to be selected\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tframe.on( 'select', function() {\n\n\t\t\t\t// We set multiple to false so only get one image from the uploader\n\t\t\t\tvar attachment = frame.state().get( 'selection' ).first().toJSON();\n\n\t\t\t\tself.update_image( $btn, attachment.id, attachment.url );\n\n\t\t\t});\n\n\t\t\tframe.open();\n\n\t\t};\n\n\t\t/**\n\t\t * Update the DOM with a selected image\n\t\t *\n\t\t * @param    obj      $btn  jQuery object of the clicked button\n\t\t * @param    int      id    WP Attachment ID of the image\n\t\t * @param    string   src   <img> src of the selected image\n\t\t * @return   void\n\t\t * @since    3.7.3\n\t\t * @version  3.7.3\n\t\t */\n\t\tthis.update_image = function( $btn, id, src ) {\n\n\t\t\tvar $input   = $( '#' + $btn.attr( 'data-id' ) ),\n\t\t\t\t$preview = $btn.prevAll( 'img.llms-image-field-preview' )\n\t\t\t\t$remove  = $btn.hasClass( 'llms-image-field-remove' ) ? $btn : $btn.next( 'input.llms-image-field-remove' );\n\n\t\t\t$input.val( id );\n\t\t\t$preview.attr( 'src', src );\n\n\t\t\tif ( '' !== id ) {\n\t\t\t\t$remove.removeClass( 'hidden' );\n\t\t\t} else {\n\t\t\t\t$remove.addClass( 'hidden' );\n\t\t\t}\n\n\t\t}\n\n\t\t// go\n\t\tthis.init();\n\n\t};\n\n\tvar a = new window.llms.admin_settings();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-admin-tables.js",
    "content": "/**\n * LifterLMS Admin Tables\n * @since    3.2.0\n * @version  3.28.1\n */\n;( function( $, undefined ) {\n\n\twindow.llms = window.llms || {};\n\n\tvar AdminTables = function() {\n\n\t\tthis.$tables = null;\n\n\t\t/**\n\t\t * Initialize\n\t\t * @return void\n\t\t * @since  3.2.0\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tself.$tables = $( '.llms-gb-table' );\n\n\t\t\tif ( self.$tables.length ) {\n\t\t\t\tself.bind();\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Bind DOM events\n\t\t * @return   void\n\t\t * @since    2.3.0\n\t\t * @version  3.28.0\n\t\t */\n\t\tthis.bind = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.$tables.each( function() {\n\n\t\t\t\tvar $table = $( this );\n\n\t\t\t\t$table.parent().find('form#llms-clear-student-progress-cache' ).on( 'submit', function( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tself.clear_cache( $table, $( this ) );\n\n\t\t\t\t} );\n\n\t\t\t\t$table.on( 'click', 'button[name=\"llms-table-paging\"]', function( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tself.change_page( $table, $( this ) );\n\t\t\t\t} );\n\n\t\t\t\t$table.on( 'click', 'button[name=\"llms-table-export\"]', function( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tself.export( $table, $( this ) );\n\t\t\t\t} );\n\n\t\t\t\t$table.on( 'click', 'a.llms-sortable', function( e ) {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tself.change_order( $table, $( this ) );\n\t\t\t\t} );\n\n\t\t\t\t$table.parent().find( '.llms-table-filters' ).on( 'change', 'select.llms-table-filter', function( e ) {\n\t\t\t\t\tself.change_filter( $table, $( this ) );\n\t\t\t\t} );\n\n\t\t\t\t$table.parent().find( '.llms-table-search' ).on( 'keyup', 'input', debounce( function( e ) {\n\n\t\t\t\t\tswitch ( e.keyCode ) {\n\n\t\t\t\t\t\tcase 37:\n\t\t\t\t\t\tcase 38:\n\t\t\t\t\t\tcase 39:\n\t\t\t\t\t\tcase 40:\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tself.search( $table, $( this ) );\n\n\t\t\t\t\t}\n\n\t\t\t\t}, 250 ) );\n\n\t\t\t} );\n\n\t\t};\n\n\t\tthis.clear_cache = function( $table, $form ) {\n\t\t\tvar self = this;\n\t\t\tvar $btn = $form.find( 'button' );\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\turl: $form.attr( 'action' ),\n\t\t\t\tmethod: 'POST',\n\t\t\t\tdataType: 'html',\n\t\t\t\tdata: {\n\t\t\t\t\t'_wpnonce' : $form.find('[name=\"_wpnonce\"]').val(),\n\t\t\t\t\t'_wp_http_referer' : $form.find('[name=\"_wp_http_referer\"]').val(),\n\t\t\t\t\t'llms_tool': 'clear-cache'\n\t\t\t\t},\n\t\t\t\tbeforeSend: function() {\n\t\t\t\t\tif ( $btn ) {\n\t\t\t\t\t\t$btn.prop( 'disabled', true );\n\t\t\t\t\t\tLLMS.Spinner.start( $btn, 'small' );\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\terror: function( jqXHR, status, error ) {\n\t\t\t\t\tif ( $btn ) {\n\t\t\t\t\t\t$btn.prop( 'disabled', false );\n\t\t\t\t\t\tLLMS.Spinner.stop( $btn );\n\t\t\t\t\t}\n\n\t\t\t\t\tconsole.error( error );\n\t\t\t\t},\n\t\t\t\tsuccess: function( res ) {\n\t\t\t\t\tif ( $btn ) {\n\t\t\t\t\t\t$btn.prop( 'disabled', false );\n\t\t\t\t\t\tLLMS.Spinner.stop( $btn );\n\t\t\t\t\t}\n\n\t\t\t\t\tself.reload( $table, {} );\n\t\t\t\t}\n\t\t\t} );\n\t\t};\n\n\t\t/**\n\t\t * Handle clicks on sortable column headers\n\t\t * @param    obj   $table   jQuery selector for the current table\n\t\t * @param    obj   $anchor  jQuery selector for the clicked column head anchor\n\t\t * @return   void\n\t\t * @since    3.2.0\n\t\t * @version  3.2.0\n\t\t */\n\t\tthis.change_order = function( $table, $anchor ) {\n\n\t\t\tthis.reload( $table, {\n\t\t\t\torder: $anchor.attr( 'data-order' ),\n\t\t\t\torderby: $anchor.attr( 'data-orderby' ),\n\t\t\t\tpage: 1,\n\t\t\t} );\n\n\t\t};\n\n\t\tthis.change_filter = function( $table, $select ) {\n\n\t\t\tthis.reload( $table, {\n\t\t\t\tfilter: $select.val(),\n\t\t\t\tfilterby: $select.attr( 'name' ),\n\t\t\t\tpage: 1,\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Change the current page of the table on a next/back click\n\t\t * @param    obj   $table  jQuery selector for the current table\n\t\t * @param    obj   $btn    jQuery selector for the clicked button\n\t\t * @return   void\n\t\t * @since    3.2.0\n\t\t * @version  3.4.0\n\t\t */\n\t\tthis.change_page = function( $table, $btn ) {\n\n\t\t\tthis.reload( $table, {\n\t\t\t\torder: this.get_args( $table, 'order' ),\n\t\t\t\torderby: this.get_args( $table, 'orderby' ),\n\t\t\t\tpage: $btn.attr( 'data-page' ),\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Handle\n\t\t * @param    obj    $table    jQuery object for the table\n\t\t * @param    obj    $btn      jQuery object for the clicked button\n\t\t * @param    string filename  filename of the export in progress.\n\t\t * @return   void\n\t\t * @since    3.15.0\n\t\t * @version  3.28.1\n\t\t */\n\t\tthis.export = function( $table, $btn, filename ) {\n\n\t\t\tvar self = this,\n\t\t\t\t$msg = $table.find( '.llms-table-export .llms-table-export-msg' ),\n\t\t\t\t$progress = $table.find( '.llms-table-export .llms-table-progress' );\n\n\t\t\tfunction activate_button() {\n\t\t\t\tLLMS.Spinner.stop( $btn, 'small' );\n\t\t\t\t$btn.removeAttr( 'disabled' );\n\t\t\t}\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: $.extend( {\n\t\t\t\t\taction: 'export_admin_table',\n\t\t\t\t\thandler: $table.attr( 'data-handler' ),\n\t\t\t\t\tfilename: filename,\n\t\t\t\t}, JSON.parse( $table.attr( 'data-args' ) ) ),\n\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\tif ( ! $btn.attr( 'disabled' ) ) {\n\t\t\t\t\t\t$btn.attr( 'disabled', 'disabled' );\n\t\t\t\t\t\tLLMS.Spinner.start( $btn, 'small' );\n\t\t\t\t\t}\n\n\t\t\t\t},\n\t\t\t\terror: function( jqXHR, status, error ) {\n\n\t\t\t\t\tvar msg = LLMS.l10n.translate( 'An error was encountered generating the export' );\n\t\t\t\t\tactivate_button();\n\t\t\t\t\t$progress.hide();\n\t\t\t\t\t$msg.html( '<span class=\"llms-error\">' + msg + ': ' + error + '</span>' );\n\t\t\t\t\tconsole.error( jqXHR );\n\n\t\t\t\t},\n\t\t\t\tsuccess: function( res ) {\n\n\t\t\t\t\tif ( ! res.success && res.message ) {\n\n\t\t\t\t\t\tactivate_button();\n\t\t\t\t\t\t$progress.hide();\n\t\t\t\t\t\t$msg.html( '<span class=\"llms-error\">' + res.message + '</span>' );\n\n\t\t\t\t\t} else if ( res.success && res.data && res.data.progress ) {\n\n\t\t\t\t\t\t$msg.html( '' );\n\n\t\t\t\t\t\t// only show a progress bar if it's going to take more than one request.\n\t\t\t\t\t\tif ( ! $progress.is( 'visible' ) && res.data.progress !== 100 ) {\n\t\t\t\t\t\t\t$progress.css( 'display', 'inline-block' );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$progress.find( '.llms-table-progress-text' ).text( res.data.progress + '%' );\n\t\t\t\t\t\t$progress.find( '.llms-table-progress-inner' ).css( 'width', res.data.progress + '%' );\n\n\t\t\t\t\t\t// if we're not finished, make another request.\n\t\t\t\t\t\tif ( 100 !== res.data.progress ) {\n\t\t\t\t\t\t\tself.export( $table, $btn, res.data.filename );\n\n\t\t\t\t\t\t// finished, download the file and cleanup the interface.\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t\t\tvar id = 'llms-dl-export';\n\t\t\t\t\t\t\t\t$( '#' + id ).remove();\n\t\t\t\t\t\t\t\t$( '<a />', {\n\t\t\t\t\t\t\t\t\tid: id,\n\t\t\t\t\t\t\t\t\thref: res.data.url,\n\t\t\t\t\t\t\t\t\tstyle: 'display: hidden;',\n\t\t\t\t\t\t\t\t\tdownload: '',\n\t\t\t\t\t\t\t\t} ).appendTo( 'body' );\n\t\t\t\t\t\t\t\t$( '#' + id )[0].click();\n\t\t\t\t\t\t\t\tactivate_button();\n\t\t\t\t\t\t\t\t$progress.hide();\n\t\t\t\t\t\t\t}, 1000 );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t\t} );\n\n\t\t}\n\n\t\t/**\n\t\t * Retrieve arguments stored in the table and parse into a readable object\n\t\t * @param    obj     $table  jQuery selector for the current table\n\t\t * @param    string  item    key to grab a specific value from the args object\n\t\t * @return   mixed\n\t\t * @since    3.2.0\n\t\t * @version  3.2.0\n\t\t */\n\t\tthis.get_args = function( $table, item ) {\n\n\t\t\tvar args = JSON.parse( $table.attr( 'data-args' ) );\n\n\t\t\tif ( item ) {\n\t\t\t\treturn ( args[ item ] ) ? args[ item ] : false;\n\t\t\t} else {\n\t\t\t\treturn args;\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Reload a table\n\t\t * @param    obj   $table  jQuery selector for the current table\n\t\t * @param    obj   args    arguments to pass with the ajax query\n\t\t * @return   void\n\t\t * @since    3.2.0\n\t\t * @version  3.2.0\n\t\t */\n\t\tthis.reload = function( $table, args ) {\n\n\t\t\targs = $.extend( {\n\t\t\t\taction: 'get_admin_table_data',\n\t\t\t\thandler: $table.attr( 'data-handler' ),\n\t\t\t}, JSON.parse( $table.attr( 'data-args' ) ), args );\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: args,\n\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\tLLMS.Spinner.start( $table.closest( '.llms-table-wrap' ) );\n\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tLLMS.Spinner.stop( $table.closest( '.llms-table-wrap' ) )\n\n\t\t\t\t\tif ( r.success ) {\n\n\t\t\t\t\t\t$table.attr( 'data-args', r.data.args );\n\n\t\t\t\t\t\t$table.find( 'thead' ).replaceWith( r.data.thead );\n\t\t\t\t\t\t$table.find( 'tbody' ).replaceWith( r.data.tbody );\n\t\t\t\t\t\t$table.find( 'tfoot' ).replaceWith( r.data.tfoot );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Executes an AJAX search query\n\t\t * @param    obj   $table  jQuery selector for the current table\n\t\t * @param    obj   $input  jQuery selector for the search input\n\t\t * @return   void\n\t\t * @since    3.2.0\n\t\t * @version  3.2.0\n\t\t */\n\t\tthis.search = function( $table, $input ) {\n\n\t\t\tvar val = $input.val()\n\t\t\t\tlen = val.length;\n\n\t\t\tif ( 0 === len || len >= 3 ) {\n\t\t\t\tthis.reload( $table, {\n\t\t\t\t\tpage: 1,\n\t\t\t\t\tsearch: $input.val(),\n\t\t\t\t} );\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Throttle function by a delay in ms\n\t\t * @param    Function  fn     callback function\n\t\t * @param    int       delay  delay in millisecond\n\t\t * @return   function\n\t\t * @since    3.2.0\n\t\t * @version  3.2.0\n\t\t */\n\t\tfunction debounce( fn, delay ) {\n\t\t\tvar timer = null;\n\t\t\treturn function () {\n\t\t\t\tvar context = this,\n\t\t\t\t\targs = arguments;\n\t\t\t\twindow.clearTimeout( timer );\n\t\t\t\ttimer = window.setTimeout( function () {\n\t\t\t\t\tfn.apply( context, args );\n\t\t\t\t}, delay );\n\t\t\t};\n\t\t}\n\n\t\t// go\n\t\tthis.init();\n\n\t};\n\n\t// initialize the object\n\twindow.llms.admin_tables = new AdminTables();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-admin-wizard.js",
    "content": "/**\n * JS from the admin setup wizard\n *\n * @since 4.8.0\n * @version 4.8.0\n */\n\n( function() {\n\tconst\n\t\tcurrStep       = document.getElementById( 'llms-setup-current-step' ),\n\t\texitLink       = document.querySelector( '.llms-exit-setup' ),\n\t\timports        = document.querySelectorAll( 'input[name=\"llms_setup_course_import_ids[]\"]' ),\n\t\tcheckboxToggle = document.getElementsByClassName( 'llms-checkbox-toggle' )[ 0 ] ?? null;\n\n\tif ( imports.length ) {\n\t\tconst\n\t\t\tsubmit = document.getElementById( 'llms-setup-submit' ),\n\t\t\tmsgs   = document.querySelectorAll( '.llms-importing-msgs .llms-importing-msg' );\n\n\t\t/**\n\t\t * Retrieve the number of courses to be imported\n\t\t *\n\t\t * @since 4.8.0\n\t\t *\n\t\t * @return {Number} The number of courses to be imported.\n\t\t */\n\t\tfunction getSelectedImportCount() {\n\t\t\tlet count = 0;\n\n\t\t\timports.forEach( function( el ) {\n\t\t\t\tif ( el.checked ) {\n\t\t\t\t\t++count;\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\treturn count;\n\t\t}\n\n\t\t/**\n\t\t * Update UI when a user toggles an import on or off.\n\t\t *\n\t\t * @since 4.8.0\n\t\t */\n\t\timports.forEach( function( el ) {\n\t\t\tel.addEventListener( 'change', function() {\n\t\t\t\t// Hide all messages.\n\t\t\t\tmsgs.forEach( function( msg ) {\n\t\t\t\t\tmsg.style.display = 'none';\n\t\t\t\t} );\n\n\t\t\t\tconst selectedCount = getSelectedImportCount();\n\n\t\t\t\t// If there's no courses to be imported, disable the submit button.\n\t\t\t\tsubmit.disabled = 0 === getSelectedImportCount() ? 'disabled' : null;\n\n\t\t\t\t// Show messages where applicable.\n\t\t\t\tif ( 1 === selectedCount ) {\n\t\t\t\t\tmsgs[0].style.display = 'block';\n\t\t\t\t} else if ( selectedCount >= 2 ) {\n\t\t\t\t\tmsgs[1].style.display = 'block';\n\t\t\t\t\tdocument.getElementById( 'llms-importing-number' ).textContent = selectedCount;\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\n\t\t// Trigger a change event so the UI displays properly on page load.\n\t\timports[0].dispatchEvent( new Event( 'change' ) );\n\n\t\t/**\n\t\t * Start a spinner when the \"Import Courses\" button is clicked.\n\t\t *\n\t\t * @since 4.8.0\n\t\t */\n\t\tsubmit.addEventListener( 'click', function() {\n\t\t\tLLMS.Spinner.start( jQuery( submit ), 'small' );\n\t\t} );\n\t}\n\n\tif ( exitLink && 'finish' !== currStep.value ) {\n\t\t/**\n\t\t * When users click \"Exit Setup\" prior to setup completion, open a confirmation dialog\n\t\t *\n\t\t * @since 4.8.0\n\t\t */\n\t\texitLink.addEventListener( 'click', function( e ) {\n\t\t\tif ( ! window.confirm( exitLink.dataset.confirm ) ) {\n\t\t\t\te.preventDefault();\n\t\t\t}\n\t\t} );\n\t}\n\n\tif ( checkboxToggle ) {\n\t\tcheckboxToggle.addEventListener( 'click', function() {\n\t\t\tconst hiddenFields = this.parentNode.parentNode.querySelectorAll( '.is-hidden,.is-visible' );\n\n\t\t\tfor ( let i = 0; i < hiddenFields.length; i++ ) {\n\t\t\t\thiddenFields[ i ].classList.toggle( 'is-visible' );\n\t\t\t\thiddenFields[ i ].classList.toggle( 'is-hidden' );\n\t\t\t}\n\t\t} );\n\t}\n}() );\n"
  },
  {
    "path": "assets/js/llms-admin.js",
    "content": "/**\n * LifterLMS Admin Panel Javascript\n *\n * @since Unknown\n * @version 7.3.0\n *\n * @param obj $ Traditional jQuery reference.\n * @return void\n */\n;( function( $ ) {\n\n\twindow.llms = window.llms || {};\n\n\twindow.llms.widgets = function() {\n\n\t\tthis.$widgets      = $( '.llms-widget' );\n\t\tthis.$info_toggles = $( '.llms-widget-info-toggle' );\n\n\t\tthis.init = function() {\n\t\t\tthis.bind();\n\t\t};\n\n\t\tthis.bind = function() {\n\n\t\t\tthis.$info_toggles.on( 'mouseenter mouseleave', function( evt ) {\n\t\t\t\t$(this).closest( '.llms-widget' )\n\t\t\t\t\t.toggleClass( 'info-showing', 'mouseenter' === evt.type );\n\t\t\t} );\n\n\t\t};\n\n\t\t// Go.\n\t\tthis.init();\n\n\t\treturn this;\n\n\t};\n\n\tvar llms_widgets = new window.llms.widgets();\n\n\tvar $headerEnd = $( '.wp-header-end' );\n\tif ( ! $headerEnd.length ) {\n\t\t$headerEnd = $( '.wrap h1, .wrap h2' ).first();\n\t}\n\t$( '#lifterlms-notifications' ).insertAfter( $headerEnd );\n\n\t$( document ).ready(function () {\n\t\t$( document ).on('click', '.lifterlms-notice-button.notice-dismiss', function () {\n\t\t\tvar notification_id = $( this ).val();\n\t\t\tvar nonce = $( this ).data( 'nonce' );\n\n\t\t\tvar postData = {\n\t\t\t\taction: 'lifterlms_hide_notice',\n\t\t\t\tnotification_id: notification_id,\n\t\t\t\tnonce: nonce\n\t\t\t}\n\n\t\t\t$.ajax( {\n\t\t\t\ttype: \"POST\",\n\t\t\t\tdata: postData,\n\t\t\t\turl: ajaxurl,\n\t\t\t\tsuccess: function (response) {\n\t\t\t\t\t$( '#' + notification_id ).hide();\n\t\t\t\t}\n\t\t\t} );\n\t\t});\n\t});\n\n\n\t/**\n\t * Simple jQuery plugin to transform select elements into Select2-powered elements to query for Courses/Memberships via AJAX.\n\t *\n\t * @since 3.19.4\n\t * @since 3.32.0 Added ability to fetch posts based on their post status.\n\t * @since 3.37.2 Added ability to fetch posts (llms posts) filtered by their instructor id.\n\t * @since 4.4.0 Update ajax nonce source.\n\t *\n\t * @param obj options Options passed to Select2.\n\t *                    Each default option will pulled from the elements data-attributes.\n\t * @return void\n\t */\n\t$.fn.llmsPostsSelect2 = function( options ) {\n\t\tvar localOptions = options;\n\n\t\tthis.each( function() {\n\t\t\tvar self = $( this ),\n\t\t\t\toptions = localOptions || {},\n\t\t\t\tdefaults = {\n\t\t\t\t\tmultiple: false,\n\t\t\t\t\tplaceholder: self.attr( 'data-placeholder' ) || ( undefined !== LLMS.l10n ? LLMS.l10n.translate( 'Select a Course/Membership' ) : 'Select a Course/Membership' ),\n\t\t\t\t\tpost_type: self.attr( 'data-post-type' ) || 'post',\n\t\t\t\t\tpost_statuses: self.attr( 'data-post-statuses' ) || 'publish',\n\t\t\t\t\tinstructor_id: null,\n\t\t\t\t\tallow_clear: self.attr( 'data-post-type' ) || false,\n\t\t\t\t\twidth: null,\n\t\t\t\t};\n\n\t\t\t$.each( defaults, function( setting ) {\n\t\t\t\tif ( self.attr( 'data-' + setting ) ) {\n\t\t\t\t\toptions[ setting ] = self.attr( 'data-' + setting );\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tif ( 'multiple' === self.attr( 'multiple' ) ) {\n\t\t\t\toptions.multiple = true;\n\t\t\t}\n\n\t\t\toptions = $.extend( defaults, options );\n\n\t\t\t$( this ).llmsSelect2( {\n\t\t\t\tallowClear: options.allow_clear,\n\t\t\t\tajax: {\n\t\t\t\t\tdataType: 'JSON',\n\t\t\t\t\tdelay: 250,\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\turl: window.ajaxurl,\n\t\t\t\t\tdata: function( params ) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\taction: 'select2_query_posts',\n\t\t\t\t\t\t\tpage: ( params.page ) ? params.page - 1 : 0, // 0 index the pages to make it simpler for the database query\n\t\t\t\t\t\t\tpost_type: options.post_type,\n\t\t\t\t\t\t\tinstructor_id : options.instructor_id,\n\t\t\t\t\t\t\tpost_statuses: options.post_statuses,\n\t\t\t\t\t\t\tterm: params.term,\n\t\t\t\t\t\t\t_ajax_nonce: window.llms.ajax_nonce,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t\tprocessResults: function( data, params ) {\n\n\t\t\t\t\t\t// recursive function for creating\n\t\t\t\t\t\tfunction map_data( items ) {\n\n\t\t\t\t\t\t\t// this is a flat array of results\n\t\t\t\t\t\t\t// used when only one post type is selected\n\t\t\t\t\t\t\t// and to format children when using optgroups with multiple post types\n\t\t\t\t\t\t\tif ( Array.isArray( items ) ) {\n\t\t\t\t\t\t\t\treturn $.map( items, function( item ) {\n\t\t\t\t\t\t\t\t\treturn format_item( item );\n\t\t\t\t\t\t\t\t} );\n\n\t\t\t\t\t\t\t\t// this sets up the top level optgroups when using multiple post types\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn $.map( items, function( item ) {\n\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\ttext: item.label,\n\t\t\t\t\t\t\t\t\t\tchildren: map_data( item.items ),\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// format a single result (option)\n\t\t\t\t\t\tfunction format_item( item ) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\ttext: item.name,\n\t\t\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tresults: map_data( data.items ),\n\t\t\t\t\t\t\tpagination: {\n\t\t\t\t\t\t\t\tmore: data.more\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tcache: true,\n\t\t\t\tplaceholder: options.placeholder,\n\t\t\t\tmultiple: options.multiple,\n\t\t\t\twidth: options.width,\n\t\t\t} );\n\t\t} );\n\t};\n\n\t// automatically setup any select with the `llms-posts-select2` class\n\t$( 'select.llms-posts-select2' ).llmsPostsSelect2();\n\n\t/**\n\t * Simple jQuery plugin to transform select elements into Select2-powered elements to query for Students via AJAX\n\t *\n\t * @since Unknown\n\t * @since 3.17.5 Unknown.\n\t * @since 4.4.0 Update ajax nonce source.\n\t * @since 6.2.0 Use the LifterLMS REST API \"list students\" endpoint\n\t *              instead of the `LLMS_AJAX_Handler::query_students()` PHP function.\n\t * @since 6.3.0 Fixed student's REST API URL.\n\t * @since 7.3.0 Early bail when the element doesn't exist.\n\t *\n\t * @param {Object} options Options passed to Select2. Each default option will be pulled from the elements data-attributes.\n\t * @return {jQuery}\n\t */\n\t$.fn.llmsStudentsSelect2 = function( options ) {\n\n\t\tif ( ! this.length ) {\n\t\t\treturn this;\n\t\t}\n\n\t\tvar self = this,\n\t\t\toptions = options || {},\n\t\t\tdefaults = {\n\t\t\t\tallow_clear: false,\n\t\t\t\tenrolled_in: '',\n\t\t\t\tmultiple: false,\n\t\t\t\tnot_enrolled_in: '',\n\t\t\t\tplaceholder: undefined !== LLMS.l10n ? LLMS.l10n.translate( 'Select a student' ) : 'Select a student',\n\t\t\t\troles: '',\n\t\t\t\twidth: '100%',\n\t\t\t};\n\n\t\t$.each( defaults, function( setting ) {\n\t\t\tif ( self.attr( 'data-' + setting ) ) {\n\t\t\t\toptions[ setting ] = self.attr( 'data-' + setting );\n\t\t\t}\n\t\t} );\n\n\t\toptions = $.extend( defaults, options );\n\n\t\tthis.llmsSelect2({\n\t\t\tallowClear: options.allow_clear,\n\t\t\tajax: {\n\t\t\t\tdataType: 'JSON',\n\t\t\t\tdelay: 250,\n\t\t\t\tmethod: 'GET',\n\t\t\t\turl: window.wpApiSettings.root + 'wp/v2/users',\n\t\t\t\tdata: function( params ) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t_wpnonce: window.wpApiSettings.nonce,\n\t\t\t\t\t\tcontext: 'edit',\n\t\t\t\t\t\tpage: params.page || 1,\n\t\t\t\t\t\tper_page: 10,\n\t\t\t\t\t\tnot_enrolled_in: params.not_enrolled_in || options.not_enrolled_in,\n\t\t\t\t\t\tenrolled_in: params.enrolled_in || options.enrolled_in,\n\t\t\t\t\t\troles: params.roles || options.roles,\n\t\t\t\t\t\tsearch: params.term,\n\t\t\t\t\t\tsearch_columns: 'email,name,username',\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tprocessResults: function( data, params ) {\n\t\t\t\t\tvar page       = params.page || 1;\n\t\t\t\t\tvar totalPages = this._request.getResponseHeader( 'X-WP-TotalPages' );\n\t\t\t\t\treturn {\n\t\t\t\t\t\tresults: $.map( data, function( item ) {\n\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\ttext: item.name + ' <' + item.email + '>',\n\t\t\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t} ),\n\t\t\t\t\t\tpagination: {\n\t\t\t\t\t\t\tmore: page < totalPages\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t},\n\t\t\tcache: true,\n\t\t\tplaceholder: options.placeholder,\n\t\t\tmultiple: options.multiple,\n\t\t\twidth: options.width,\n\t\t});\n\n\t\treturn this;\n\n\t};\n\n\t/**\n\t * Scripts for use on the engagements settings tab for email provider connector plugins\n\t *\n\t * @since 3.40.0\n\t */\n\twindow.llms.emailConnectors = {\n\n\t\t/**\n\t\t * Register a client\n\t\t *\n\t\t * Builds and submits a form used to direct the user to the connector's oAuth\n\t\t * authorization endpoint.\n\t\t *\n\t\t * @since 3.40.0\n\t\t *\n\t\t * @param {String} url    Redirect URL.\n\t\t * @param {Object} fields Hash of fields where the key is the field name and the value if the field value.\n\t\t * @return {Void}\n\t\t */\n\t\tregisterClient: function( url, fields ) {\n\n\t\t\tvar form = document.createElement( 'form' );\n\t\t\tform.setAttribute( 'method', 'POST' );\n\t\t\tform.setAttribute( 'action', url );\n\n\t\t\tfunction appendInput( name, value ) {\n\t\t\t\tvar input = document.createElement( 'input' );\n\t\t\t\tinput.setAttribute( 'type', 'hidden' );\n\t\t\t\tinput.setAttribute( 'name', name );\n\t\t\t\tinput.setAttribute( 'value', value );\n\t\t\t\tform.appendChild( input );\n\t\t\t}\n\n\t\t\t$.each( fields, function( key, val ) {\n\t\t\t\tappendInput( key, val );\n\t\t\t} );\n\n\t\t\tdocument.body.appendChild( form );\n\t\t\tform.submit();\n\n\t\t},\n\n\t\t/**\n\t\t * Performs an AJAX request to perform remote installation of the connector plugin\n\t\t *\n\t\t * The callback will more than likely use `registerClient()` on success.\n\t\t *\n\t\t * @since 3.40.0\n\t\t *\n\t\t * @param {Object}   $btn     jQuery object for the connector button.\n\t\t * @param {Object}   data     Hash of data used for the ajax request.\n\t\t * @param {Function} callback Success callback function.\n\t\t * @return {Void}\n\t\t */\n\t\tremoteInstall: function( $btn, data, callback ) {\n\n\t\t\tconst self = this;\n\n\t\t\t$btn.parent().find( '.llms-error' ).remove();\n\t\t\t$.post( ajaxurl, data, function() {\n\t\t\t\t// Do a second call to get the redirect URL, since the plugin isn't in memory initially.\n\t\t\t\tdata.action = data.action + '_verify';\n\t\t\t\t$.post( ajaxurl, data, callback ).fail( function( jqxhr ) {\n\t\t\t\t\tself.handleRemoteInstallFailure( $btn, jqxhr );\n\t\t\t\t} );\n\t\t\t} )\n\t\t\t\t.fail( function (jqxhr ) {\n\t\t\t\t\tself.handleRemoteInstallFailure( $btn, jqxhr );\n\t\t\t\t} );\n\t\t},\n\n\t\thandleRemoteInstallFailure: function( $btn, jqxhr ) {\n\t\t\tLLMS.Spinner.stop( $btn );\n\t\t\tvar msg = jqxhr.responseJSON && jqxhr.responseJSON.message ? jqxhr.responseJSON.message : jqxhr.responseText;\n\t\t\tif ( msg ) {\n\t\t\t\t$( '<p class=\"llms-error\">' + LLMS.l10n.replace( 'Error: %s', { '%s': msg } ) + '</p>' ).insertAfter( $btn );\n\t\t\t}\n\t\t},\n\n\t};\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-ajax.js",
    "content": "function Ajax ( type, data, cache ) {\n\n\tthis.type     = type;\n\tthis.data     = data;\n\tthis.cache    = cache;\n\tthis.dataType = 'json';\n\tthis.url      = window.ajaxurl || window.llms.ajaxurl;\n\n}\n\nAjax.prototype.check_voucher_duplicate = function () {\n\n\tjQuery.ajax({\n\t\ttype \t\t: this.type,\n\t\turl\t\t\t: this.url,\n\t\tdata \t\t: this.data,\n\t\tcache\t\t: this.cache,\n\t\tdataType\t: this.dataType,\n\t\tsuccess\t\t: function( response ) {\n\t\t\tllms_on_voucher_duplicate( response.duplicates );\n\t\t}\n\t});\n};\n"
  },
  {
    "path": "assets/js/llms-analytics.js",
    "content": ";/**\n * LifterLMS Admin Reporting Widgets & Charts\n *\n * @since 3.0.0\n * @since 3.17.2 Unknown.\n * @since 3.33.1 Fix issue that produced series options not aligned with the chart data.\n * @since 3.36.3 Added the `allow_clear` paramater when initializiing the `llmsStudentSelect2`.\n * @since 4.3.3 Legends will automatically display on top of the chart.\n * @since 4.5.1 Show sales reporting currency symbol based on LifterLMS site options.\n * @version 7.3.0\n *\n */( function( $, undefined ) {\n\n\twindow.llms = window.llms || {};\n\n\t/**\n\t * LifterLMS Admin Analytics.\n\t *\n\t * @since 3.0.0\n\t * @since 3.5.0 Unknown\n\t * @since 4.5.1 Added `opts` parameter.\n\t * @since [verison] Early bail if no `#llms-analytics-json` is available.\n\t *\n\t * @param {Object} options Options object.\n\t * @return {Object} Class instance.\n\t */\n\tvar Analytics = function( opts ) {\n\n\t\tif ( ! $( '#llms-analytics-json' ).length ) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.charts_loaded = false;\n\t\tthis.data          = {};\n\t\tthis.query         = $.parseJSON( $( '#llms-analytics-json' ).text() );\n\t\tthis.timeout       = 8000;\n\t\tthis.options       = opts;\n\n\t\tthis.$widgets = $( '.llms-widget[data-method]' );\n\n\t\t/**\n\t\t * Initializer\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\tgoogle.charts.load( 'current', {\n\t\t\t\tpackages: [\n\t\t\t\t\t'corechart'\n\t\t\t\t]\n\t\t\t} );\n\t\t\tgoogle.charts.setOnLoadCallback( this.charts_ready );\n\n\t\t\tthis.bind();\n\t\t\tthis.load_widgets();\n\n\t\t};\n\n\t\t/**\n\t\t * Bind DOM events.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.36.3 Added the `allow_clear` paramater when initializiing the `llmsStudentSelect2`.\n\t\t * @since 7.2.0 Added check for datepicker before initializing.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tthis.bind = function() {\n\n\t\t\tif ( $( '.llms-datepicker' ).length && $.fn.datepicker ) {\n\t\t\t\t$( '.llms-datepicker' ).datepicker( {\n\t\t\t\t\tdateFormat: 'yy-mm-dd',\n\t\t\t\t\tmaxDate: 0,\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\t$( '#llms-students-ids-filter' ).llmsStudentsSelect2( {\n\t\t\t\tmultiple: true,\n\t\t\t\tplaceholder: LLMS.l10n.translate( 'Filter by Student(s)' ),\n\t\t\t\tallow_clear: true,\n\t\t\t} );\n\n\t\t\t$( 'a[href=\"#llms-toggle-filters\"]' ).on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\t$( '.llms-analytics-filters' ).slideToggle( 100 );\n\t\t\t} );\n\n\t\t\t$( '#llms-custom-date-submit' ).on( 'click', function() {\n\t\t\t\t$( 'input[name=\"range\"]' ).val( 'custom' );\n\t\t\t} );\n\n\t\t\t$( '#llms-date-quick-filters a.llms-nav-link[data-range]' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\t\t\t\t$( 'input[name=\"range\"]' ).val( $( this ).attr( 'data-range' ) );\n\n\t\t\t\t$( 'form.llms-reporting-nav' ).submit();\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Called  by Google Charts when the library is loaded and ready\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.charts_ready = function() {\n\n\t\t\twindow.llms.analytics.charts_loaded = true;\n\t\t\twindow.llms.analytics.draw_chart();\n\n\t\t};\n\n\t\t/**\n\t\t * Render the chart\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.17.6 Unknown\n\t\t * @since 4.3.3 Force the legend to appear on top of the chart.\n\t\t * @since 4.5.1 Display sales numbers according to the site's currency settings instead of the browser's locale.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tthis.draw_chart = function() {\n\n\t\t\tif ( ! this.charts_loaded || ! this.is_loading_finished() ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar el = document.getElementById( 'llms-charts-wrapper' );\n\n\t\t\tif ( ! el ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar self    = this,\n\t\t\t\tchart   = new google.visualization.ComboChart( el ),\n\t\t\t\tdata    = self.get_chart_data(),\n\t\t\t\toptions = {\n\t\t\t\t\tlegend: 'top',\n\t\t\t\t\tchartArea: {\n\t\t\t\t\t\theight: '75%',\n\t\t\t\t\t\twidth: '85%',\n\t\t\t\t\t},\n\t\t\t\t\tcolors: ['#606C38','#E85D75','#EF8354','#C64191','#731963','#2B6CB0','#E1B530','#319795'],\n\t\t\t\t\theight: 560,\n\t\t\t\t\tlineWidth: 4,\n\t\t\t\t\tseriesType: 'bars',\n\t\t\t\t\tseries: self.get_chart_series_options(),\n\t\t\t\t\tvAxes: {\n\t\t\t\t\t\t0: {\n\t\t\t\t\t\t\tformat: this.options.currency_format || 'currency',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t1: {\n\t\t\t\t\t\t\tformat: '',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t};\n\n\t\t\tif ( data.length ) {\n\n\t\t\t\tdata = google.visualization.arrayToDataTable( data );\n\t\t\t\tdata.sort( [{column: 0}] );\n\t\t\t\tchart.draw( data, options );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Check if a widget is still loading\n\t\t *\n\t\t * @return   bool\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.is_loading_finished = function() {\n\t\t\tif ( $( '.llms-widget.is-loading' ).length ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t};\n\n\t\t/**\n\t\t * Start loading all widgets on the current screen\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.load_widgets = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.$widgets.each( function() {\n\t\t\t\tself.load_widget( $( this ) );\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Load a specific widget.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 7.2.0 Change h1 tag to .llms-widget-content.\n\t\t * @since 7.3.0 Append `_ajax_nonce` to the ajax data packet.\n\t\t *\n\t\t * @param {Object} $widget The jQuery selector of the widget element.\n\t\t * @return {Void}\n\t\t */\n\t\tthis.load_widget = function( $widget ) {\n\n\t\t\tvar self         = this,\n\t\t\t\tmethod       = $widget.attr( 'data-method' ),\n\t\t\t\t$content     = $widget.find( '.llms-widget-content' ),\n\t\t\t\t$retry       = $widget.find( '.llms-reload-widget' ),\n\t\t\t\tcontent_text = LLMS.l10n.translate( 'Error' ),\n\t\t\t\tstatus;\n\n\t\t\t$widget.addClass( 'is-loading' );\n\n\t\t\t$.ajax( {\n\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'llms_widget_' + method,\n\t\t\t\t\tdates: self.query.dates,\n\t\t\t\t\tcourses: self.query.current_courses,\n\t\t\t\t\tmemberships: self.query.current_memberships,\n\t\t\t\t\tstudents: self.query.current_students,\n\t\t\t\t\t_ajax_nonce: window.llms.ajax_nonce,\n\t\t\t\t},\n\t\t\t\tmethod: 'POST',\n\t\t\t\ttimeout: self.timeout,\n\t\t\t\turl: window.ajaxurl,\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tstatus = 'success';\n\n\t\t\t\t\tif ( 'undefined' !== typeof r.response ) {\n\n\t\t\t\t\t\tcontent_text = r.response;\n\n\t\t\t\t\t\tself.data[method] = {\n\t\t\t\t\t\t\tchart_data: r.chart_data,\n\t\t\t\t\t\t\tresponse: r.response,\n\t\t\t\t\t\t\tresults: r.results,\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t$retry.remove();\n\n\t\t\t\t\t}\n\n\t\t\t\t},\n\t\t\t\terror: function( r ) {\n\n\t\t\t\t\tstatus = 'error';\n\n\t\t\t\t},\n\t\t\t\tcomplete: function( r ) {\n\n\t\t\t\t\tif ( 'error' === status ) {\n\n\t\t\t\t\t\tif ( 'timeout' === r.statusText ) {\n\n\t\t\t\t\t\t\tcontent_text = LLMS.l10n.translate( 'Request timed out' );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tcontent_text = LLMS.l10n.translate( 'Error' );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( ! $retry.length ) {\n\n\t\t\t\t\t\t\t$retry = $( '<a class=\"llms-reload-widget\" href=\"#\">' + LLMS.l10n.translate( 'Retry' ) + '</a>' );\n\t\t\t\t\t\t\t$retry.on( 'click', function( e ) {\n\n\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\tself.load_widget( $widget );\n\n\t\t\t\t\t\t\t} );\n\n\t\t\t\t\t\t\t$widget.append( $retry );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\t$widget.removeClass( 'is-loading' );\n\t\t\t\t\t$content.html( content_text );\n\n\t\t\t\t\tself.widget_finished( $widget );\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Get the time in seconds between the queried dates\n\t\t *\n\t\t * @return   int\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.get_date_diff = function() {\n\n\t\t\tvar end   = new Date( this.query.dates.end ),\n\t\t\t\tstart = new Date( this.query.dates.start );\n\n\t\t\treturn Math.abs( end.getTime() - start.getTime() );\n\n\t\t};\n\n\t\t/**\n\t\t * Builds an object of data that can be used to, ultimately, draw the screen's chart\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.0.0\n\t\t * @version  3.1.6\n\t\t */\n\t\tthis.get_chart_data_object = function() {\n\n\t\t\tvar self         = this,\n\t\t\t\tmax_for_days = ( ( 1000 * 3600 * 24 ) * 30 ) * 4, // 4 months in seconds\n\t\t\t\tdiff         = this.get_date_diff(),\n\t\t\t\tdata         = {},\n\t\t\t\tres, i, d, date;\n\n\t\t\tfor ( var method in self.data ) {\n\n\t\t\t\tif ( ! self.data.hasOwnProperty( method ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( 'object' !== typeof self.data[ method ].chart_data || 'object' !== typeof self.data[ method ].results ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tres = self.data[ method ].results;\n\n\t\t\t\tif ( res ) {\n\n\t\t\t\t\tfor ( i = 0; i < res.length; i++ ) {\n\n\t\t\t\t\t\td = this.init_date( res[i].date );\n\n\t\t\t\t\t\t// group by days\n\t\t\t\t\t\tif ( diff <= max_for_days ) {\n\t\t\t\t\t\t\tdate = new Date( d.getFullYear(), d.getMonth(), d.getDate() );\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// group by months\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tdate = new Date( d.getFullYear(), d.getMonth(), 1 );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( ! data[ date ] ) {\n\t\t\t\t\t\t\tdata[ date ] = this.get_empty_data_object( date )\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tswitch ( self.data[ method ].chart_data.type ) {\n\n\t\t\t\t\t\t\tcase 'amount':\n\t\t\t\t\t\t\t\tdata[ date ][ method ] = data[ date ][ method ] + ( res[i][ self.data[ method ].chart_data.key ] * 1 );\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\tcase 'count':\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tdata[ date ][ method ]++;\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn data;\n\n\t\t};\n\n\t\t/**\n\t\t * Get the data google charts needs to initiate the current chart\n\t\t *\n\t\t * @return   obj\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.get_chart_data = function() {\n\n\t\t\tvar self = this,\n\t\t\t\tobj  = self.get_chart_data_object(),\n\t\t\t\tdata = self.get_chart_headers();\n\n\t\t\tfor ( var date in obj ) {\n\n\t\t\t\tif ( ! obj.hasOwnProperty( date ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tvar row = [ obj[ date ]._date ];\n\n\t\t\t\tfor ( var item in obj[ date ] ) {\n\t\t\t\t\tif ( ! obj[ date ].hasOwnProperty( item ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// skip meta items\n\t\t\t\t\tif ( 0 === item.indexOf( '_' ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\trow.push( obj[ date ][ item ] );\n\t\t\t\t}\n\n\t\t\t\tdata.push( row );\n\n\t\t\t}\n\n\t\t\treturn data;\n\n\t\t};\n\n\t\t/**\n\t\t * Get a stub of the data object used by this.get_data_object\n\t\t *\n\t\t * @param    string   date  date to instantiate the object with\n\t\t * @return   obj\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.get_empty_data_object = function( date ) {\n\n\t\t\tvar self = this,\n\t\t\t\tobj  = {\n\t\t\t\t\t_date: date,\n\t\t\t};\n\n\t\t\tfor ( var method in self.data ) {\n\t\t\t\tif ( ! self.data.hasOwnProperty( method ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( self.data[ method ].chart_data ) {\n\t\t\t\t\tobj[ method ] = 0;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn obj;\n\n\t\t};\n\n\t\t/**\n\t\t * Builds an array of chart header data\n\t\t *\n\t\t * @return   array\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.get_chart_headers = function() {\n\n\t\t\tvar self = this,\n\t\t\t\th    = [];\n\n\t\t\t// date headers go first\n\t\t\th.push( {\n\t\t\t\tlabel: LLMS.l10n.translate( 'Date' ),\n\t\t\t\tid: 'date',\n\t\t\t\ttype: 'date',\n\t\t\t} );\n\n\t\t\tfor ( var method in self.data ) {\n\t\t\t\tif ( ! self.data.hasOwnProperty( method ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( self.data[ method ].chart_data ) {\n\t\t\t\t\th.push( self.data[ method ].chart_data.header );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn [ h ];\n\n\t\t};\n\n\t\t/**\n\t\t * Get a object of series options needed to draw the chart.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since Fix issue that produced series options not aligned with the chart data.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.get_chart_series_options = function() {\n\n\t\t\tvar self    = this,\n\t\t\t\toptions = {}\n\t\t\t\ti       = 0;\n\n\t\t\tfor ( var method in self.data ) {\n\t\t\t\tif ( ! self.data.hasOwnProperty( method ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( self.data[ method ].chart_data ) {\n\n\t\t\t\t\tvar type = self.data[ method ].chart_data.type;\n\n\t\t\t\t\toptions[ i ] = {\n\t\t\t\t\t\ttype: ( 'count' === type ) ? 'bars' : 'line',\n\t\t\t\t\t\ttargetAxisIndex: ( 'count' === type ) ? 1 : 0,\n\t\t\t\t\t};\n\n\t\t\t\t\ti++;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn options;\n\n\t\t};\n\n\t\t/**\n\t\t * Instantiate a Date instance via a date string\n\t\t *\n\t\t * @param    string   string  date string, expected format should be from php date( 'Y-m-d H:i:s' )\n\t\t * @return   obj\n\t\t * @since    3.1.4\n\t\t * @version  3.1.5\n\t\t */\n\t\tthis.init_date = function( string ) {\n\n\t\t\tvar parts, date, time;\n\n\t\t\tparts = string.split( ' ' );\n\n\t\t\tdate = parts[0].split( '-' );\n\t\t\ttime = parts[1].split( ':' );\n\n\t\t\treturn new Date( date[0], date[1] - 1, date[2], time[0], time[1], time[2] );\n\n\t\t};\n\n\t\t/**\n\t\t * Called when a widget is finished loading\n\t\t * Updates the current chart with the new data from the widget\n\t\t *\n\t\t * @param    obj   $widget  jQuery selector of the widget element\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.widget_finished = function( $widget ) {\n\n\t\t\tif ( this.is_loading_finished() ) {\n\t\t\t\tthis.draw_chart();\n\t\t\t}\n\n\t\t};\n\n\t\t// go\n\t\tthis.init();\n\n\t\t// return\n\t\treturn this;\n\n\t};\n\n\twindow.llms.analytics = new Analytics( window.llms.analytics || {} );\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-favorites.js",
    "content": "/* global LLMS, $ */\n/* jshint strict: true */\n\n/**\n * Front End Favorite Class.\n *\n * @type {Object}\n *\n * @since 7.5.0\n * @version 7.5.0\n */\n( function( $ ) {\n\n\tvar favorite = {\n\n\t\t/**\n\t\t * Bind DOM events.\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tbind: function() {\n\n\t\t\tvar self = this;\n\n\t\t\t// Favorite clicked.\n\t\t\t$( '.llms-favorite-wrapper button' ).on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.favorite( $( this ) );\n\t\t\t} );\n\n\t\t\t// Adding class in Favorite's parent.\n\t\t\t$( '.llms-favorite-wrapper' ).parent().addClass( 'llms-has-favorite' );\n\n\t\t},\n\n\t\t/**\n\t\t * Favorite / Unfavorite an object.\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @param {Object} $btn jQuery object for the \"Favorite / Unfavorite\" button.\n\t\t * @return {Void}\n\t\t */\n\t\tfavorite: function( $btn ) {\n\n\t\t\tvar object_id \t= $btn.attr( 'data-id' ),\n\t\t\t\tobject_type = $btn.attr( 'data-type' ),\n\t\t\t\tuser_action\t= $btn.attr( 'data-action' );\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'favorite_object',\n\t\t\t\t\tobject_id: object_id,\n\t\t\t\t\tobject_type: object_type,\n\t\t\t\t\tuser_action: user_action\n\t\t\t\t},\n\t\t\t\tbeforeSend: function() {},\n\t\t\t\tsuccess: function( r ) {\n\t\t\t\t\t/**\n\t\t\t\t\t * Get all the favorite buttons on the page related to the same lesson, e.g. when the syllabus\n\t\t\t\t\t * is shown on the sidebar of a lesson or a course, in that case you will have the same favorite\n\t\t\t\t\t * button twice. The code below makes sure both the buttons are updated.\n\t\t\t\t\t */\n\t\t\t\t\tvar $fav_btns = $( '[data-id='+object_id+'][data-type='+object_type+'][data-action='+user_action+']' );\n\t\t\t\t\tif( r.success ) {\n\t\t\t\t\t\t$fav_btns.each(\n\t\t\t\t\t\t\tfunction() {\n\t\t\t\t\t\t\t\tif( 'favorite' === user_action ) {\n\t\t\t\t\t\t\t\t\t$(this).find( '.llms-heart-btn' ).removeClass( 'fa-heart-o' ).addClass( 'fa-heart' );\n\t\t\t\t\t\t\t\t\t$(this).attr( 'data-action', 'unfavorite' );\n\t\t\t\t\t\t\t\t\t$(this).attr( 'aria-pressed', true );\n\t\t\t\t\t\t\t\t} else if ( 'unfavorite' === user_action ) {\n\t\t\t\t\t\t\t\t\t$(this).find( '.llms-heart-btn' ).removeClass( 'fa-heart' ).addClass( 'fa-heart-o' );\n\t\t\t\t\t\t\t\t\t$(this).attr( 'data-action', 'favorite' );\n\t\t\t\t\t\t\t\t\t$(this).attr( 'aria-pressed', false );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Updating count.\n\t\t\t\t\t\t\t\t$(this).closest( '.llms-favorite-wrapper' ).find( '.llms-favorites-count' ).text( r.total_favorites );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t};\n\n\tfavorite.bind();\n\n\twindow.llms             = window.llms || {};\n\twindow.llms.favorites   = favorite;\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-focus-mode.js",
    "content": "/**\n * LifterLMS Focus Mode sidebar toggle.\n *\n * @package LifterLMS\n *\n * @since 10.0.0\n * @version 10.0.0\n */\n( function() {\n\t'use strict';\n\n\tvar STORAGE_KEY = 'llms_focus_sidebar_collapsed';\n\n\tfunction init() {\n\t\tvar toggle = document.querySelector( '.llms-focus-mode-sidebar-toggle' );\n\t\tif ( ! toggle ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar body = document.body;\n\n\t\tif ( localStorage.getItem( STORAGE_KEY ) === '1' ) {\n\t\t\tbody.classList.add( 'llms-sidebar-collapsed' );\n\t\t}\n\n\t\ttoggle.addEventListener( 'click', function() {\n\t\t\tvar collapsed = body.classList.toggle( 'llms-sidebar-collapsed' );\n\t\t\tlocalStorage.setItem( STORAGE_KEY, collapsed ? '1' : '0' );\n\t\t} );\n\t}\n\n\tif ( document.readyState === 'loading' ) {\n\t\tdocument.addEventListener( 'DOMContentLoaded', init );\n\t} else {\n\t\tinit();\n\t}\n} )();\n"
  },
  {
    "path": "assets/js/llms-form-checkout.js",
    "content": "/**\n * LifterLMS Checkout Screen related events and interactions\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.0.0\n * @version 7.0.0\n */\n\n( function( $ ) {\n\n\tvar llms_checkout = function() {\n\n\t\t/**\n\t\t * Array of validation functions to call on form submission\n\t\t *\n\t\t * @type    array\n\t\t * @since   3.0.0\n\t\t * @version 3.0.0\n\t\t */\n\t\tvar before_submit = [];\n\n\t\t/**\n\t\t * Array of gateways to be automatically bound when needed\n\t\t *\n\t\t * @type    array\n\t\t * @since   3.0.0\n\t\t * @version 3.0.0\n\t\t */\n\t\tvar gateways = [];\n\n\t\tthis.$checkout_form = $( '#llms-product-purchase-form' );\n\t\tthis.$confirm_form  = $( '#llms-product-purchase-confirm-form' );\n\t\tthis.$form_sections = false;\n\t\tthis.form_action    = false;\n\n\t\t/**\n\t\t * Initialize checkout JS & bind if on the checkout screen\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.34.5 Make sure we bind click events for the Show / Hide login area at the top of the checkout screen\n\t\t *               even if there's no llms product purchase form.\n\t\t * @since 7.0.0 Disable smooth scroll-behavior on checkout.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tif ( $( '.llms-checkout-wrapper' ).length ) {\n\t\t\t\tthis.bind_login();\n\t\t\t}\n\n\t\t\tif ( this.$checkout_form.length ) {\n\n\t\t\t\tthis.form_action    = 'checkout';\n\t\t\t\tthis.$form_sections = this.$checkout_form.find( '.llms-checkout-section' );\n\n\t\t\t\tthis.$checkout_form.on( 'submit', this, this.submit );\n\n\t\t\t\t/**\n\t\t\t\t * Fix `HTMLFormElement.reportValidity()` when `scroll-behavior: smooth`.\n\t\t\t\t *\n\t\t\t\t * @see {@link https://github.com/gocodebox/lifterlms/issues/2206}\n\t\t\t\t */\n\t\t\t\tdocument.querySelector( 'html' ).style.scrollBehavior = 'auto';\n\n\t\t\t\t// add before submit event for password strength meter if one's found\n\t\t\t\tif ( $( '.llms-password-strength-meter' ).length ) {\n\t\t\t\t\tthis.add_before_submit_event( {\n\t\t\t\t\t\tdata: LLMS.PasswordStrength,\n\t\t\t\t\t\thandler: LLMS.PasswordStrength.checkout,\n\t\t\t\t\t} );\n\t\t\t\t}\n\n\t\t\t\tthis.bind_coupon();\n\n\t\t\t\tthis.bind_gateways();\n\n\t\t\t\t$( document ).trigger( \"llms-checkout-refreshed\" );\n\n\t\t\t} else if ( this.$confirm_form.length ) {\n\n\t\t\t\tthis.form_action    = 'confirm';\n\t\t\t\tthis.$form_sections = this.$confirm_form.find( '.llms-checkout-section' );\n\n\t\t\t\tthis.$confirm_form.on( 'submit', function() {\n\t\t\t\t\tself.processing( 'start' );\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Public function which allows other classes or extensions to add\n\t\t * before submit events to llms checkout private \"before_submit\" array\n\t\t *\n\t\t * @param    object  obj  object of data to push to the array\n\t\t *                        requires at least a \"handler\" key which should pass a callable function\n\t\t *                        \"data\" can be anything, will be passed as the first parameter to the handler function\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.add_before_submit_event = function( obj ) {\n\n\t\t\tif ( ! obj.handler || 'function' !== typeof obj.handler ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( ! obj.data ) {\n\t\t\t\tobj.data = null;\n\t\t\t}\n\n\t\t\tbefore_submit.push( obj );\n\n\t\t};\n\n\t\t/**\n\t\t * Add an error message\n\t\t *\n\t\t * @param    string     message  error message string\n\t\t * @param    mixed      data     optional error data to output on the console\n\t\t * @return   void\n\t\t * @since    3.27.0\n\t\t * @version  3.27.0\n\t\t */\n\t\tthis.add_error = function( message, data ) {\n\n\t\t\tvar id   = 'llms-checkout-errors';\n\t\t\t\t$err = $( '#' + id );\n\n\t\t\tif ( ! $err.length ) {\n\t\t\t\t$err = $( '<ul class=\"llms-notice llms-error\" id=\"' + id + '\" />' );\n\t\t\t\t$( '.llms-checkout-wrapper' ).prepend( $err );\n\t\t\t}\n\n\t\t\t$err.append( '<li>' + message + '</li>' );\n\n\t\t\tif ( data ) {\n\t\t\t\tconsole.error( data );\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Public function which allows other classes or extensions to add\n\t\t * gateways classes that should be bound by this class\n\t\t *\n\t\t * @param    obj   gateway_class  callable class object\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.add_gateway = function( gateway_class ) {\n\n\t\t\tgateways.push( gateway_class );\n\n\t\t};\n\n\n\n\t\t/**\n\t\t * Bind coupon add & remove button events\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.bind_coupon = function() {\n\n\t\t\tvar self = this;\n\n\t\t\t// show & hide the coupon field & button\n\t\t\t$( 'a[href=\"#llms-coupon-toggle\"]' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\t\t\t\t$( '.llms-coupon-entry' ).slideToggle( 400 );\n\n\t\t\t} );\n\n\t\t\t// apply coupon click\n\t\t\t$( '#llms-apply-coupon' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\t\t\t\tself.coupon_apply( $( this ) );\n\n\t\t\t} );\n\n\t\t\t// remove coupon click\n\t\t\t$( '#llms-remove-coupon' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\t\t\t\tself.coupon_remove( $( this ) );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind gateway section events\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.bind_gateways = function() {\n\n\t\t\tthis.load_gateways();\n\n\t\t\tif ( ! $( 'input[name=\"llms_payment_gateway\"]' ).length ) {\n\t\t\t\t$( '#llms_create_pending_order' ).removeAttr( 'disabled' );\n\t\t\t}\n\n\t\t\t// add class and trigger watchable event when gateway selection changes\n\t\t\t$( 'input[name=\"llms_payment_gateway\"]' ).on( 'change', function() {\n\n\t\t\t\t$( 'input[name=\"llms_payment_gateway\"]' ).each( function() {\n\n\t\t\t\t\tvar $el          = $( this ),\n\t\t\t\t\t\t$parent      = $el.closest( '.llms-payment-gateway' ),\n\t\t\t\t\t\t$fields      = $parent.find( '.llms-gateway-fields' ).find( 'input, textarea, select' ),\n\t\t\t\t\t\tchecked      = $el.is( ':checked' ),\n\t\t\t\t\t\tdisplay_func = ( checked ) ? 'addClass' : 'removeClass';\n\n\t\t\t\t\t$parent[ display_func ]( 'is-selected' );\n\n\t\t\t\t\tif ( checked ) {\n\n\t\t\t\t\t\t// enable fields\n\t\t\t\t\t\t$fields.removeAttr( 'disabled' );\n\n\t\t\t\t\t\t// emit a watchable event for extensions to hook onto\n\t\t\t\t\t\t$( '.llms-payment-gateways' ).trigger( 'llms-gateway-selected', {\n\t\t\t\t\t\t\tid: $el.val(),\n\t\t\t\t\t\t\t$selector: $parent,\n\t\t\t\t\t\t} );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// disable fields\n\t\t\t\t\t\t$fields.attr( 'disabled', 'disabled' );\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t\t// enable / disable buttons depending on field validation status\n\t\t\t$( '.llms-payment-gateways' ).on( 'llms-gateway-selected', function( e, data ) {\n\n\t\t\t\tvar $submit = $( '#llms_create_pending_order' );\n\n\t\t\t\tif ( data.$selector && data.$selector.find( '.llms-gateway-fields .invalid' ).length ) {\n\t\t\t\t\t$submit.attr( 'disabled', 'disabled' );\n\t\t\t\t} else {\n\t\t\t\t\t$submit.removeAttr( 'disabled' );\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind click events for the Show / Hide login area at the top of the checkout screen\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.34.5 When showing the login form area make sure we slide up the `.llms-notice` link's parent too.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.bind_login = function() {\n\n\t\t\t$( 'a[href=\"#llms-show-login\"]' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\t\t\t\t$( this ).closest( '.llms-info,.llms-notice' ).slideUp( 400 );\n\t\t\t\t$( 'form.llms-login' ).slideDown( 400 );\n\n\t\t\t} );\n\t\t};\n\n\t\t/**\n\t\t * Clear error messages\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.27.0\n\t\t * @version  3.27.0\n\t\t */\n\t\tthis.clear_errors = function() {\n\t\t\t$( '#llms-checkout-errors' ).remove();\n\t\t};\n\n\t\t/**\n\t\t * Triggered by clicking the \"Apply Coupon\" Button\n\t\t * Validates the coupon via JS and adds error / success messages\n\t\t * On success it will replace partials on the checkout screen with updated\n\t\t * prices and a \"remove coupon\" button\n\t\t *\n\t\t * @param    obj   $btn  jQuery selector of the Apply button\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.coupon_apply = function ( $btn ) {\n\n\t\t\tvar self       = this,\n\t\t\t\t$code      = $( '#llms_coupon_code' ),\n\t\t\t\tcode       = $code.val(),\n\t\t\t\t$messages  = $( '.llms-coupon-messages' ),\n\t\t\t\t$errors    = $messages.find( '.llms-error' ),\n\t\t\t\t$container = $( 'form.llms-checkout' );\n\n\t\t\tLLMS.Spinner.start( $container );\n\n\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'validate_coupon_code',\n\t\t\t\t\tcode: code,\n\t\t\t\t\tplan_id: $( '#llms-plan-id' ).val(),\n\t\t\t\t},\n\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\t$errors.hide();\n\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tLLMS.Spinner.stop( $container );\n\n\t\t\t\t\tif ( 'error' === r.code ) {\n\n\t\t\t\t\t\tvar $message = $( '<li>' + r.message + '</li>' );\n\n\t\t\t\t\t\tif ( ! $errors.length ) {\n\n\t\t\t\t\t\t\t$errors = $( '<ul class=\"llms-notice llms-error\" />' );\n\t\t\t\t\t\t\t$messages.append( $errors );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t$errors.empty();\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$message.appendTo( $errors );\n\t\t\t\t\t\t$errors.show();\n\n\t\t\t\t\t} else if ( r.success ) {\n\n\t\t\t\t\t\t$( '.llms-coupon-wrapper' ).replaceWith( r.data.coupon_html );\n\t\t\t\t\t\tself.bind_coupon();\n\n\t\t\t\t\t\t$( '.llms-payment-gateways' ).replaceWith( r.data.gateways_html );\n\t\t\t\t\t\tself.bind_gateways();\n\n\t\t\t\t\t\t$( '.llms-order-summary' ).replaceWith( r.data.summary_html );\n\n\t\t\t\t\t\t$( document ).trigger( \"llms-checkout-refreshed\" );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Called by clicking the \"Remove Coupon\" button\n\t\t * Removes the coupon via AJAX and unsets related session data\n\t\t *\n\t\t * @param    obj   $btn  jQuery selector of the Remove button\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.coupon_remove = function( $btn ) {\n\n\t\t\tvar self       = this,\n\t\t\t\t$container = $( 'form.llms-checkout' );\n\n\t\t\tLLMS.Spinner.start( $container );\n\n\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'remove_coupon_code',\n\t\t\t\t\tplan_id: $( '#llms-plan-id' ).val(),\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tLLMS.Spinner.stop( $container );\n\n\t\t\t\t\tif ( r.success ) {\n\n\t\t\t\t\t\t$( '.llms-coupon-wrapper' ).replaceWith( r.data.coupon_html );\n\t\t\t\t\t\tself.bind_coupon();\n\n\t\t\t\t\t\t$( '.llms-order-summary' ).replaceWith( r.data.summary_html );\n\n\t\t\t\t\t\t$( '.llms-payment-gateways' ).replaceWith( r.data.gateways_html );\n\t\t\t\t\t\tself.bind_gateways();\n\n\t\t\t\t\t\t$( document ).trigger( \"llms-checkout-refreshed\" );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Scroll error messages into view\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.27.0\n\t\t * @version  3.27.0\n\t\t */\n\t\tthis.focus_errors = function() {\n\t\t\t$( 'html, body' ).animate( {\n\t\t\t\tscrollTop: $( '#llms-checkout-errors' ).offset().top - 50,\n\t\t\t}, 200 );\n\t\t};\n\n\t\t/**\n\t\t * Bind external gateway JS\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.load_gateways = function() {\n\n\t\t\tfor ( var i = 0; i <= gateways.length; i++ ) {\n\t\t\t\tvar g = gateways[i];\n\t\t\t\tif ( typeof g === 'object' && g !== null ) {\n\t\t\t\t\tif ( g.bind !== undefined && 'function' === typeof g.bind  ) {\n\t\t\t\t\t\tg.bind();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Start or stop processing events on the checkout form\n\t\t *\n\t\t * @param    string   action  whether to start or stop processing [start|stop]\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.24.1\n\t\t */\n\t\tthis.processing = function( action ) {\n\n\t\t\tvar func, $form;\n\n\t\t\tswitch ( action ) {\n\n\t\t\t\tcase 'stop':\n\t\t\t\t\tfunc = 'removeClass';\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'start':\n\t\t\t\tdefault:\n\t\t\t\t\tfunc = 'addClass';\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\tif ( 'checkout' === this.form_action ) {\n\t\t\t\t$form = this.$checkout_form;\n\t\t\t} else if ( 'confirm' === this.form_action ) {\n\t\t\t\t$form = this.$confirm_form;\n\t\t\t}\n\n\t\t\t$form[ func ]( 'llms-is-processing' );\n\t\t\tLLMS.Spinner[ action ]( this.$form_sections );\n\n\t\t};\n\n\t\t/**\n\t\t * Handles form submission\n\t\t * Calls all validation events in `before_submit[]`\n\t\t * waits for call backs and either displays returned errors\n\t\t * or submits the form when all are successful\n\t\t *\n\t\t * @param    obj   e  JS event object\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.27.0\n\t\t */\n\t\tthis.submit = async function( e ) {\n\n\t\t\tvar self       = e.data;\n\n\t\t\te.preventDefault();\n\n\t\t\t// add spinners\n\t\t\tself.processing( 'start' );\n\n\t\t\t// remove errors to prevent duplicates\n\t\t\tself.clear_errors();\n\n\t\t\t// Turn every handler into a promise-returning function\n\t\t\tfunction runHandler({ handler, data }) {\n\t\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\t\treject( new Error( LLMS.l10n.translate('Operation timed out, please try again' ) ) );\n\t\t\t\t\t}, 60000 );\n\n\t\t\t\t\thandler(data, result => {\n\t\t\t\t\t\tclearTimeout( timer );\n\n\t\t\t\t\t\tif ( result === true ) {\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t} else if ( typeof result === 'string' ) {\n\t\t\t\t\t\t\treject( new Error( result ) );\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treject( new Error( LLMS.l10n.translate( 'Unknown response' ) ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Run all before-submit handlers sequentially to avoid issues of handlers interfering with each other.\n\t\t\ttry {\n\t\t\t\tfor ( const obj of before_submit ) {\n\t\t\t\t\tawait runHandler( obj );\n\t\t\t\t}\n\n\t\t\t\tself.$checkout_form.off( 'submit', self.submit );\n\t\t\t\tself.$checkout_form.trigger( 'submit' );\n\t\t\t\tself.processing( 'stop' );\n\t\t\t} catch (err) {\n\t\t\t\tself.add_error(err.message);\n\t\t\t\tself.focus_errors();\n\t\t\t\tself.processing( 'stop' );\n\t\t\t}\n\t\t};\n\n\t\t// initialize\n\t\tthis.init();\n\n\t\treturn this;\n\n\t};\n\n\twindow.llms          = window.llms || {};\n\twindow.llms.checkout = new llms_checkout();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-launch-course-button.js",
    "content": "/**\n * Add Launch Course Builder button to the classic editor.\n *\n * @since Unknown\n * @version 3.35.0\n */\n\n( function( $ ){\n\n\tvar mainCourseButton = $( '.page-title-action' );\n\n\tif ( mainCourseButton.length ) {\n\t\t// Add your custom button beside the \"Add Course\" button\n\t\t$( '<a href=\"' + llms_launch_course.builder_url + '\" class=\"page-title-action button-primary\">' + LLMS.l10n.translate( 'Launch Course Builder' ) + '</a>' ).insertAfter( mainCourseButton );\n\t}\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-metabox-achievement.js",
    "content": "( function( $ ) {\n\n\t// Open Media Manager modal.\n\t$( '.achievement_image_button' ).click( function( e ) {\n\n\t\t// Create Media Manager On Click to allow multiple on one Page\n\t\tvar achievement_uploader;\n\n\t\te.preventDefault();\n\n\t\t// Setup the Variables based on the Button Clicked to enable multiple\n\t\tvar achievement_img_input_id = '#' + this.id + '.upload_achievement_image';\n\t\tvar achievement_img_src      = 'img#' + this.id + '.llms_achievement_image';\n\n\t\t// If the uploader object has already been created, reopen the dialog\n\t\tif (achievement_uploader) {\n\t\t\tachievement_uploader.open();\n\t\t\treturn;\n\t\t}\n\n\t\t// Extend the wp.media object\n\t\tachievement_uploader = wp.media.frames.file_frame = wp.media({\n\t\t\ttitle: 'Choose Achievement Image',\n\t\t\tbutton: {\n\t\t\t\ttext: 'Choose Achievement'\n\t\t\t},\n\t\t\tmultiple: false\n\t\t});\n\n\t\t// When a file is selected, grab the URL and set it as the text field's value\n\t\tachievement_uploader.on('select', function() {\n\t\t\tattachment = achievement_uploader.state().get( 'selection' ).first().toJSON();\n\t\t\t// Set the Field with the Image ID\n\t\t\t$( achievement_img_input_id ).val( attachment.id );\n\t\t\t// Set the Sample Image with the URL\n\t\t\t$( achievement_img_src ).attr( 'src', attachment.url );\n\n\t\t});\n\n\t\t// Open the uploader dialog\n\t\tachievement_uploader.open();\n\n\t});\n\n\t// Remove Image and replace with default and Erase Image ID for achievement\n\t$( '.llms_achievement_clear_image_button' ).click(function(e) {\n\t\te.preventDefault();\n\t\tvar achievement_remove_input_id = 'input#' + this.id + '.upload_achievement_image';\n\t\tvar achievement_img_src         = 'img#' + this.id + '.llms_achievement_image';\n\t\tvar achievement_default_img_src = $( 'img#' + this.id + '.llms_achievement_default_image' ).attr( \"src\" );\n\n\t\t$( achievement_remove_input_id ).val( '' );\n\t\t$( achievement_img_src ).attr( 'src', achievement_default_img_src );\n\t});\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-metabox-certificate.js",
    "content": "jQuery( document ).ready(function($){\n\n\t$( '.certificate_image_button' ).click(function(e) {\n\n\t\t// Create Media Manager On Click to allow multiple on one Page\n\t\tvar certificate_uploader;\n\n\t\te.preventDefault();\n\n\t\t// Setup the Variables based on the Button Clicked to enable multiple\n\t\tvar certificate_img_input_id = '#' + this.id + '.upload_certificate_image';\n\t\tvar certificate_img_src      = 'img#' + this.id + '.llms_certificate_image';\n\n\t\t// If the uploader object has already been created, reopen the dialog\n\t\tif (certificate_uploader) {\n\t\t\tcertificate_uploader.open();\n\t\t\treturn;\n\t\t}\n\n\t\t// Extend the wp.media object\n\t\tcertificate_uploader = wp.media.frames.file_frame = wp.media({\n\t\t\ttitle: 'Choose Certificate Image',\n\t\t\tbutton: {\n\t\t\t\ttext: 'Choose Certificate'\n\t\t\t},\n\t\t\tmultiple: false\n\t\t});\n\n\t\t// When a file is selected, grab the URL and set it as the text field's value\n\t\tcertificate_uploader.on('select', function() {\n\t\t\tattachment = certificate_uploader.state().get( 'selection' ).first().toJSON();\n\t\t\t// Set the Field with the Image ID\n\t\t\t$( certificate_img_input_id ).val( attachment.id );\n\t\t\t// Set the Sample Image with the URL\n\t\t\t$( certificate_img_src ).attr( 'src', attachment.url );\n\n\t\t});\n\n\t\t// Open the uploader dialog\n\t\tcertificate_uploader.open();\n\n\t});\n\n});\n/*\n* Media Manager 3.5\n* @version 1.70\n*/\njQuery( document ).ready(function($){\n\t// Remove Image and replace with default and Erase Image ID for Certificate\n\t$( '.llms_certificate_clear_image_button' ).click(function(e) {\n\t\te.preventDefault();\n\t\tvar certificate_remove_input_id = 'input#' + this.id + '.upload_certificate_image';\n\t\tvar certificate_img_src         = 'img#' + this.id + '.llms_certificate_image';\n\t\tvar certificate_default_img_src = $( 'img#' + this.id + '.llms_certificate_default_image' ).attr( \"src\" );\n\n\t\t$( certificate_remove_input_id ).val( '' );\n\t\t$( certificate_img_src ).attr( 'src', certificate_default_img_src );\n\t});\n\n});\n"
  },
  {
    "path": "assets/js/llms-metabox-fields.js",
    "content": "/**\n * Global admin functions.\n *\n * @since Unknown\n * @version 3.35.0\n */\n\n( function( $ ){\n\n\t// Toggle sales price settings.\n\tclear_fields = function (fields) {\n\t\tvar fields = fields;\n\n\t\t$.each( fields, function( i, val ) {\n\t\t\t$( val ).val( '' );\n\t\t});\n\t}\n\n\t// Load ajax animation functionality\n\tload_ajax_animation = function() {\n\t\t$( '#loading' ).hide();\n\n\t\t$( document ).ajaxStop(function(){\n\t\t\t$( '#loading' ).hide();\n\t\t});\n\n\t\t$( document ).ajaxStart(function(){\n\t\t\t$( '#loading' ).show();\n\t\t});\n\t}\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-metabox-instructors.js",
    "content": "/**\n * Instructors Metabox\n *\n * @since    3.13.0\n * @version  3.13.0\n */\n( function( $ ) {\n\n\twindow.llms = window.llms || {};\n\n\twindow.llms.metabox_instructors = function() {\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @return  void\n\t\t * @since   3.13.0\n\t\t * @version 3.13.0\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\t// before saving, update the wp core hidden field for post_author\n\t\t\t// so that the first instructor is always set as the post author\n\t\t\t$( '._llms_instructors_data.repeater' ).on( 'llms-repeater-before-save', function( e, params ) {\n\t\t\t\tvar author_id = params.$el.find( '.llms-repeater-rows .llms-repeater-row' ).first().find( 'select[name^=\"_llms_id\"]' ).val();\n\t\t\t\t$( '#post_author' ).val( author_id );\n\t\t\t} );\n\n\t\t\t$( '._llms_instructors_data.repeater' ).on( 'llms-new-repeater-row', function( e, params ) {\n\n\t\t\t\tvar $instructor = params.$row.find( 'select[name^=\"_llms_id\"]' ),\n\t\t\t\t\t$target     = params.$row.find( '.llms-repeater-title' );\n\n\t\t\t\t$instructor.on( 'select2:select', function( e ) {\n\t\t\t\t\tif ( ! e.params ) {\n\t\t\t\t\t\t$target.html( $instructor.find( 'option[selected=\"selected\"]' ).html() );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$target.text( e.params.data.text );\n\t\t\t\t\t}\n\t\t\t\t} ).trigger( 'select2:select' );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t// go\n\t\tthis.init();\n\n\t};\n\n\tvar a = new window.llms.metabox_instructors();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-metabox-options.js",
    "content": "/**\n * JS for the Course and Membership metabox options\n *\n * @since 8.0.0\n */\n\n( function( $ ){\n\n\t$( '.llms-mb-container .llms-basic-editor' ).each( function() {\n\n\t\tconst name \t   = $( this ).attr( 'data-name' );\n\n\t\tconst ed = new Quill( this, {\n\t\t\tmodules: {\n\t\t\t\ttoolbar: ['bold', 'italic', 'underline', 'strike', { 'script': 'sub'}, { 'script': 'super' }],\n\t\t\t\tkeyboard: {\n\t\t\t\t\tbindings: {\n\t\t\t\t\t\ttab: {\n\t\t\t\t\t\t\tkey: 9,\n\t\t\t\t\t\t\thandler: function( range, context ) {\n\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t13: {\n\t\t\t\t\t\t\tkey: 13,\n\t\t\t\t\t\t\thandler: function( range, context ) {\n\t\t\t\t\t\t\t\ted.root.blur();\n\t\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tplaceholder: $( this ).attr( 'data-placeholder' ),\n\t\t\ttheme: 'bubble',\n\t\t} );\n\n\t\tconst keyboard = ed.getModule('keyboard');\n\t\tkeyboard.bindings['Enter'] = null;\n\n\t\ted.on( 'text-change', function() {\n\t\t\t$( 'input[name=\"' + name + '\"]' ).val( ed.getSemanticHTML() );\n\t\t});\n\n\t} );\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-metabox-product.js",
    "content": "/**\n * Product Options MetaBox.\n *\n * Displays on Course & Membership Post Types.\n *\n * @since 3.0.0\n * @since 3.30.3 Unknown.\n * @since 3.36.3 Fixed conflicts with the Classic Editor block.\n * @version 7.3.0\n */\n( function( $ ) {\n\n\twindow.llms = window.llms || {};\n\n\twindow.llms.metabox_product = function() {\n\n\t\t/**\n\t\t * jQuery obj for the main $( '#llms-access-plans' ) element.\n\t\t *\n\t\t * @type obj\n\t\t */\n\t\tthis.$plans = null;\n\n\t\t/**\n\t\t * jQuery obj for the main $( '#llms-save-access-plans' ) save button element.\n\t\t *\n\t\t * @type obj\n\t\t */\n\t\tthis.$save = null;\n\n\t\tthis.$plan_dialog = null;\n\n\t\t/**\n\t\t * A randomly generated temporary ID used for the tinyMCE editor's id\n\t\t * when a new plan is added\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tthis.temp_id = Math.floor( ( Math.random() * 7777 ) + 777 );\n\n\t\t/**\n\t\t * CSS class name used to highlight validation errors for plan fields\n\t\t *\n\t\t * @type string\n\t\t */\n\t\tthis.validation_class = 'llms-invalid';\n\n\t\t/**\n\t\t * Initialize.\n\t\t *\n\t\t * @param bool skip_dep_checks If true, skips dependency checks.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.29.3 Unknown.\n\t\t * @since 7.3.0 Check on whether access plans require attention.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tthis.init = function( skip_dep_checks ) {\n\n\t\t\tvar self = this;\n\n\t\t\tself.$plans = $( '#llms-access-plans' );\n\t\t\tself.$save  = $( '#llms-save-access-plans' );\n\n\t\t\tself.bind_visibility();\n\n\t\t\tvar $mb = $( '#lifterlms-product #llms-product-options-access-plans' );\n\n\t\t\tif ( ! $mb.length ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check whether the warning icon should be displayed.\n\t\t\tself.requiresAttention();\n\n\t\t\tif ( skip_dep_checks ) {\n\t\t\t\tself.bind();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tLLMS.Spinner.start( $mb );\n\n\t\t\t// we rely on TinyMCE but WordPress doesn't register TinyMCE\n\t\t\t// like every other admin script so we'll do a little dependency check here...\n\t\t\tvar counter = 0,\n\t\t\t\tinterval;\n\n\t\t\tinterval = setInterval( function() {\n\n\t\t\t\t// if we get to 30 seconds display an error message\n\t\t\t\tif ( counter >= 300 ) {\n\n\t\t\t\t\t$mb.html( LLMS.l10n.translate( 'There was an error loading the necessary resources. Please try again.' ) );\n\n\t\t\t\t}\n\t\t\t\t// if we can't access tinyMCE, increment and wait...\n\t\t\t\telse if ( 'undefined' === typeof tinyMCE ) {\n\n\t\t\t\t\tcounter++;\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\t\t\t\t// bind the events, we're good!\n\t\t\t\telse {\n\n\t\t\t\t\tself.bind();\n\n\t\t\t\t}\n\n\t\t\t\tclearInterval( interval );\n\t\t\t\tLLMS.Spinner.stop( $mb );\n\n\t\t\t}, 100 );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind DOM Events.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.30.0 Add checkout redirect fields events.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tthis.bind = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tsetTimeout( function() {\n\t\t\t\tif ( self.has_plan_limit_been_reached() ) {\n\t\t\t\t\tself.toggle_create_button( 'disable' );\n\t\t\t\t}\n\t\t\t}, 500 );\n\n\t\t\tif ( 0 === self.get_current_plan_count() ) {\n\t\t\t\tself.toggle_save_button( 'disable' );\n\t\t\t}\n\n\t\t\t// save access plans button.\n\t\t\tself.$save.on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.save_plans();\n\t\t\t} );\n\n\t\t\t// bind change events to form element that controls another form element\n\t\t\tself.$plans.on( 'change', '[data-controller-id]', function() {\n\t\t\t\tself.controller_change( $( this ) );\n\t\t\t} );\n\n\t\t\t// @todo Replace this with multiple data-controller functionality in llms-metaboxes.js\n\t\t\tself.$plans.on( 'change', 'select[name$=\"[availability]\"]', function() {\n\t\t\t\tvar $plan_container         = $( this ).closest( '.llms-access-plan' ),\n\t\t\t\t\t$plan_redirect_forced   = $plan_container.find( 'input[name$=\"[checkout_redirect_forced]\"]' ),\n\t\t\t\t\t$plan_redirect_settings = $plan_container.find( '.llms-checkout-redirect-settings' );\n\n\t\t\t\tif ( 'members' === $( this ).val() ) {\n\t\t\t\t\tif ( ! $plan_redirect_forced.prop( 'checked' ) ) {\n\t\t\t\t\t\t$plan_redirect_settings.hide();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$plan_redirect_settings.show();\n\t\t\t\t\t}\n\n\t\t\t\t\t$plan_redirect_forced.on( 'change', function() {\n\t\t\t\t\t\t$plan_redirect_settings.toggle();\n\t\t\t\t\t} );\n\n\t\t\t\t} else {\n\t\t\t\t\t$plan_redirect_forced.off( 'change' );\n\t\t\t\t\t$plan_redirect_settings.show();\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t$( '#llms-access-plans .llms-access-plan-datepicker' ).datepicker( {\n\t\t\t\tdateFormat: \"mm/dd/yy\"\n\t\t\t} );\n\n\t\t\t// trigger changes on load for all existing plans\n\t\t\t$( '#llms-access-plans [data-controller-id]' ).trigger( 'change' );\n\n\t\t\tvar dialogEl = document.getElementById( 'llms-access-plan-dialog' );\n\t\t\tif ( dialogEl ) {\n\t\t\t\tself.$plan_dialog = new A11yDialog( dialogEl );\n\t\t\t}\n\n\t\t\t// If PMPro or others are hiding the Restrictions tab, or there's no time period option, we want to hide the Presell option.\n\t\t\tif ( ! $( 'span.llms-nav-link' ).filter(function() {\n\t\t\t\t\treturn $( this ).text().trim() === LLMS.l10n.translate( 'Restrictions' );\n\t\t\t\t}).length ||\n\t\t\t\t! $( '#_llms_time_period' ).length ) {\n\t\t\t\t$( '.llms-access-plan-templates button[data-template=\"presell\"]' ).hide();\n\t\t\t}\n\n\t\t\t// add a new empty plan interface on new plan button click.\n\t\t\t$( '#llms-new-access-plan' ).on( 'click', function() {\n\t\t\t\tself.init_plan();\n\n\t\t\t\tif ( self.$plan_dialog ) {\n\t\t\t\t\tself.$plan_dialog.show();\n\t\t\t\t}\n\n\t\t\t\tself.toggle_create_button( 'disable' );\n\t\t\t\tself.toggle_save_button( 'enable' );\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\tif ( ! self.has_plan_limit_been_reached() ) {\n\t\t\t\t\t\tself.toggle_create_button( 'enable' );\n\t\t\t\t\t}\n\t\t\t\t}, 500 );\n\t\t\t} );\n\n\t\t\t$( '#llms-access-plan-dialog .llms-access-plan-templates button.template').on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tif ( self.$plan_dialog ) {\n\t\t\t\t\tself.$plan_dialog.hide();\n\t\t\t\t}\n\n\t\t\t\tvar $last_access_plan = self.$plans.find( '.llms-access-plan' ).last();\n\n\t\t\t\tswitch ( $( this ).attr( 'data-template' ) ) {\n\t\t\t\t\tcase 'free':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Free' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[is_free]\"]').val( 'yes' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'monthly':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Monthly' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '100' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[frequency]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[period]\"]').val( 'month' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'annual':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Annual' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '1000' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[frequency]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[period]\"]').val( 'year' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'one-time':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'One Time' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '1000' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[frequency]\"]').val( '0' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[access_expiration]\"]').val( 'limited-period' ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[access_length]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[access_period]\"]').val( 'year' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'lifetime':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Lifetime' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '1000' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[frequency]\"]').val( '0' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[access_expiration]\"]').val( 'lifetime' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'paid-trial':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Paid Trial' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '100' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[frequency]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[period]\"]').val( 'month' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[trial_offer]\"]').val( 'yes' ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[trial_price]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[trial_length]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[trial_period]\"]').val( 'week' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'free-trial':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Free Trial' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '100' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[frequency]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[period]\"]').val( 'month' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[trial_offer]\"]').val( 'yes' ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[trial_price]\"]').val( '0' ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[trial_length]\"]').val( '1' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[trial_period]\"]').val( 'week' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'hidden-access':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Hidden Access' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[visibility]\"]').val( 'hidden' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[is_free]\"]').val( 'yes' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'sale':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Sale' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '1000' ).change();\n\t\t\t\t\t\t$last_access_plan.find('select[name^=\"_llms_plans[\"][name$=\"[on_sale]\"]').val( 'yes' ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[sale_price]\"]').val( '500' ).change();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'presell':\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[title]\"]').val( LLMS.l10n.translate( 'Pre-sale' ) ).change();\n\t\t\t\t\t\t$last_access_plan.find('input[name^=\"_llms_plans[\"][name$=\"[price]\"]').val( '1000' ).change();\n\t\t\t\t\t\tvar $restrictions_tab = $('span.llms-nav-link').filter(function() {\n\t\t\t\t\t\t\treturn $(this).text().trim() === 'Restrictions';\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif ( $restrictions_tab.length ) {\n\t\t\t\t\t\t\t$restrictions_tab.click();\n\t\t\t\t\t\t\tif ( ! $('#_llms_time_period').is(':checked') ) {\n\t\t\t\t\t\t\t\t$('#_llms_time_period').click();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tvar today = new Date();\n\t\t\t\t\t\t\ttoday.setMonth(today.getMonth() + 1);\n\n\t\t\t\t\t\t\tvar month = (today.getMonth() + 1).toString().padStart(2, '0');\n\t\t\t\t\t\t\tvar day = today.getDate().toString().padStart(2, '0');\n\t\t\t\t\t\t\tvar year = today.getFullYear().toString();\n\n\t\t\t\t\t\t\tvar formattedDate = `${month}/${day}/${year}`;\n\t\t\t\t\t\t\t$('#_llms_start_date').val( formattedDate ).change();\n\t\t\t\t\t\t\t// TODO: Add some kind of a notice or something to let the user know that the start date has been set.\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'advanced':\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tself.$plans.trigger( 'llms-access-plan-template-' + $( this ).attr( 'data-template' ) );\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tself.$plans.sortable( {\n\t\t\t\thandle: '.llms-drag-handle',\n\t\t\t\titems: '.llms-access-plan',\n\t\t\t\tstart: function( event, ui ) {\n\t\t\t\t\tself.$plans.addClass( 'dragging' );\n\t\t\t\t},\n\t\t\t\tstop: function( event, ui ) {\n\t\t\t\t\tself.$plans.removeClass( 'dragging' );\n\t\t\t\t\tself.update_plan_orders();\n\t\t\t\t},\n\t\t\t} );\n\n\t\t\t// bind text entered into the title to the displayed title for fancy fun\n\t\t\tself.$plans.on( 'keyup', 'input.llms-plan-title', function( ) {\n\n\t\t\t\tvar $input   = $( this ),\n\t\t\t\t\t$plan    = $input.closest( '.llms-access-plan' ),\n\t\t\t\t\t$display = $plan.find( 'span.llms-plan-title' ),\n\t\t\t\t\tval      = $input.val(),\n\t\t\t\t\ttitle    = ( val ) ? val : $display.attr( 'data-default' );\n\n\t\t\t\t$display.text( title );\n\n\t\t\t} );\n\n\t\t\t// Record that a field has been focused so we can tweak validation to only validate \"edited\" fields.\n\t\t\tself.$plans.on( 'focusin', 'input', function( e, data ) {\n\t\t\t\t$( this ).addClass( 'llms-has-been-focused' );\n\t\t\t} );\n\n\t\t\t// Validate a single input field\n\t\t\tself.$plans.on( 'keyup focusout llms-validate-plan-field', 'input', function( e, data ) {\n\n\t\t\t\tvar $input = $( this );\n\n\t\t\t\tif ( $input[0].checkValidity() ) {\n\t\t\t\t\t$input.removeClass( self.validation_class );\n\t\t\t\t} else {\n\t\t\t\t\t$input.addClass( self.validation_class );\n\t\t\t\t\tif ( 'keyup' === e.type ) {\n\t\t\t\t\t\t$input[0].reportValidity();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( ! data || data.cascade ) {\n\t\t\t\t\t$input.closest( '.llms-access-plan' ).trigger( 'llms-validate-plan', { original_event: e.type } );\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tself.$plans.on( 'llms-validate-plan', '.llms-access-plan', function( e, data ) {\n\n\t\t\t\tdata = data || {};\n\n\t\t\t\tvar $plan = $( this ),\n\t\t\t\t\t// only validate \"edited\" fields during cascading validation from input validations.\n\t\t\t\t\tselector = data.original_event ? 'input.llms-has-been-focused' : 'input';\n\n\t\t\t\t$plan.find( selector ).each( function() {\n\t\t\t\t\t$( this ).trigger( 'llms-validate-plan-field', { cascade: false } );\n\t\t\t\t} );\n\n\t\t\t\tif ( $plan.find( '.' + self.validation_class ).length ) {\n\t\t\t\t\t$plan.addClass( self.validation_class );\n\t\t\t\t} else {\n\t\t\t\t\t$plan.removeClass( self.validation_class );\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tself.$plans.on( 'llms-collapsible-toggled', '.llms-access-plan', function() {\n\n\t\t\t\tvar $plan = $( this );\n\n\t\t\t\tif ( $plan.hasClass( 'opened' ) ) {\n\t\t\t\t\t// wait for animation to complete to prevent focusable errors in the console.\n\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t$plan.find( 'input.llms-invalid' ).each( function() {\n\t\t\t\t\t\t\t$( this )[0].reportValidity();\n\t\t\t\t\t\t} );\n\t\t\t\t\t}, 500 );\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tself.$plans.on( 'click', '.llms-plan-delete', function( e ) {\n\t\t\t\te.stopPropagation();\n\t\t\t\tself.delete_plan( $( this ) );\n\t\t\t} );\n\n\t\t\t// select2ify membership availability fields\n\t\t\twindow.llms.metaboxes.post_select( $( '#llms-access-plans .llms-availability-restrictions' ) );\n\n\t\t\t// select2ify redirection page fields\n\t\t\twindow.llms.metaboxes.post_select( $( '#llms-access-plans .llms-checkout-redirect-page' ) );\n\n\t\t\t// disable the textarea generated by the wp_editor function\n\t\t\t// can't figure out how to do this during initialization\n\t\t\t$( '#_llms_plans_content_llms-new-access-plan-model' ).attr( 'disabled', 'disabled' );\n\t\t\ttinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, '_llms_plans_content_llms-new-access-plan-model' );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind DOM events for editing product visibility\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.6.0\n\t\t * @version  3.6.0\n\t\t */\n\t\tthis.bind_visibility = function() {\n\n\t\t\tvar $radios = $( '#llms-catalog-visibility-select' ),\n\t\t\t\t$toggle = $( 'a.llms-edit-catalog-visibility' ),\n\t\t\t\t$save   = $( 'a.llms-save-catalog-visibility' ),\n\t\t\t\t$cancel = $( 'a.llms-cancel-catalog-visibility' );\n\n\t\t\t$toggle.on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\t$radios.slideDown( 'fast' );\n\t\t\t\t$toggle.hide();\n\t\t\t} );\n\n\t\t\t$save.on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\t$radios.slideUp( 'fast' );\n\t\t\t\t$toggle.show();\n\t\t\t\t$( '#llms-catalog-visibility-display' ).text( $( 'input[name=\"_llms_visibility\"]:checked' ).attr( 'data-label' ) );\n\t\t\t} );\n\n\t\t\t$cancel.on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\t$radios.slideUp( 'fast' );\n\t\t\t\t$toggle.show();\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Checks whether the access plan requires attention, e.g. because it contains notices.\n\t\t *\n\t\t * And if so adds the class `llms-needs-attention` to the access plan.\n\t\t *\n\t\t * @since 7.3.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tthis.requiresAttention = function() {\n\t\t\tvar self = this;\n\t\t\tself.$plans.find( '.llms-access-plan' ).each(\n\t\t\t\tfunction() {\n\t\t\t\t\t$(this).toggleClass( 'llms-needs-attention', $(this).find('.notice').not('.llms-payment-gateway-warning').length > 0 );\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * Handle physical deletion of a plan element\n\t\t * If the plan hasn't be persisted to the database it's removed from the dom\n\t\t * if it already exists in the database a confirm modal is displayed\n\t\t * upon confirmation AJAX call will be made to move the plan to the trash\n\t\t * and upon success the element will be removed from the dom\n\t\t *\n\t\t * @param  obj $btn jQuery selector of the \"X\" button clicked to initiate deletion\n\t\t * @return void\n\t\t * @since  3.0.0\n\t\t * @version 3.29.1\n\t\t */\n\t\tthis.delete_plan = function( $btn ) {\n\n\t\t\tvar self    = this,\n\t\t\t\t$plan   = $btn.closest( '.llms-access-plan' ),\n\t\t\t\tplan_id = $plan.attr( 'data-id' ),\n\t\t\t\twarning = LLMS.l10n.translate( 'After deleting this access plan, any students subscribed to this plan will still have access and will continue to make recurring payments according to the access plan\\'s settings. If you wish to terminate their plans you must do so manually. This action cannot be reversed.' );\n\n\t\t\t// if there's no ID just remove the element from the DOM\n\t\t\tif ( ! plan_id ) {\n\n\t\t\t\tself.remove_plan_el( $plan );\n\n\t\t\t\t// Throw a confirmation warning\n\t\t\t} else if ( window.confirm( warning ) ) {\n\n\t\t\t\tLLMS.Spinner.start( $plan );\n\t\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\taction: 'delete_access_plan',\n\t\t\t\t\t\tplan_id: plan_id,\n\t\t\t\t\t},\n\t\t\t\t\tsuccess: function( r ) {\n\t\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t\tLLMS.Spinner.stop( $plan );\n\t\t\t\t\t\t}, 550 );\n\t\t\t\t\t\tif ( r.success ) {\n\t\t\t\t\t\t\tself.remove_plan_el( $plan );\n\t\t\t\t\t\t\tself.trigger_update_hook();\n\t\t\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t\t\tself.update_plan_orders();\n\t\t\t\t\t\t\t}, 500 );\n\t\t\t\t\t\t} else if ( r.message ) {\n\t\t\t\t\t\t\talert( r.message );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Handle hiding & showing various pieces of an access plan form\n\t\t * This is bound to any form element with a \"data-controller-id\" property\n\t\t *\n\t\t * @param  obj  $el   jQuery selector for the changed form element\n\t\t * @return void\n\t\t * @since  3.0.0\n\t\t */\n\t\tthis.controller_change = function( $el ) {\n\n\t\t\tvar id        = $el.attr( 'data-controller-id' ),\n\t\t\t\tval       = $el.val(),\n\t\t\t\t$form     = $el.closest( '.llms-access-plan' ),\n\t\t\t\t$controls = $form.find( '[data-controller=\"' + id + '\"]' );\n\n\t\t\tif ( 'checkbox' === $el.attr( 'type' ) ) {\n\t\t\t\tval = ( $el.is( ':checked' ) ) ? val : 'no';\n\t\t\t}\n\n\t\t\t$controls.each( function() {\n\n\t\t\t\tvar $c         = $( this ),\n\t\t\t\t\t$els       = ( 'SELECT' === $c[0].nodeName || 'INPUT' === $c[0].nodeName || 'TEXTAREA' === $c[0].nodeName ) ? $c : $c.find( 'input, select, textarea' ),\n\t\t\t\t\tequals     = $c.attr( 'data-value-is' ),\n\t\t\t\t\tnot_equals = $c.attr( 'data-value-is-not' ),\n\t\t\t\t\taction, operator;\n\n\t\t\t\tif ( typeof equals !== typeof undefined && equals !== false ) {\n\n\t\t\t\t\toperator = '==';\n\n\t\t\t\t} else if ( typeof not_equals !== typeof undefined && not_equals !== false ) {\n\n\t\t\t\t\toperator = '!=';\n\n\t\t\t\t}\n\n\t\t\t\tswitch ( operator ) {\n\n\t\t\t\t\tcase '==':\n\n\t\t\t\t\t\tif ( val == equals ) {\n\t\t\t\t\t\t\taction = 'show';\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\taction = 'hide';\n\t\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase '!=':\n\n\t\t\t\t\t\tif ( val != not_equals ) {\n\t\t\t\t\t\t\taction = 'show';\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\taction = 'hide';\n\t\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t\tif ( 'show' === action ) {\n\t\t\t\t\t$c.show();\n\t\t\t\t\t$els.removeAttr( 'disabled' ).trigger( 'change' );\n\t\t\t\t} else if ( 'hide' === action ) {\n\t\t\t\t\t$c.hide();\n\t\t\t\t\t$els.attr( 'disabled', 'disabled' );\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Retrieve the current number of access plans for the course / membership (saved or unsaved)\n\t\t *\n\t\t * @return  int\n\t\t * @since   3.29.0\n\t\t * @version 3.29.0\n\t\t */\n\t\tthis.get_current_plan_count = function() {\n\t\t\treturn this.$plans.find( '.llms-access-plan' ).length;\n\t\t}\n\n\t\t/**\n\t\t * Retrieve access plan data as an array of JSON built from the dom element field values.\n\t\t *\n\t\t * @return  array\n\t\t * @since   3.29.0\n\t\t * @version 3.29.0\n\t\t */\n\t\tthis.get_plans_array = function() {\n\n\t\t\t// ensure all content editors are saved properly.\n\t\t\ttinyMCE.triggerSave();\n\n\t\t\tvar self  = this,\n\t\t\t\tform  = self.$plans.closest( 'form' ).serializeArray(),\n\t\t\t\tplans = [];\n\n\t\t\tfor ( var i = 0; i < form.length; i++ ) {\n\n\t\t\t\t// Skip non plan data from the form.\n\t\t\t\tif ( -1 === form[ i ].name.indexOf( '_llms_plans' ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tvar keys  = form[ i ].name.replace( '_llms_plans[', '' ).split( '][' ),\n\t\t\t\t\tindex = ( keys[0] * 1 ) - 1,\n\t\t\t\t\tname  = keys[1].replace( ']', '' ),\n\t\t\t\t\ttype  = 3 === keys.length ? 'array' : 'single';\n\n\t\t\t\tif ( ! plans[ index ] ) {\n\t\t\t\t\tplans[ index ] = {};\n\t\t\t\t}\n\n\t\t\t\tif ( 'array' === type ) {\n\n\t\t\t\t\tif ( ! plans[ index ][ name ] ) {\n\t\t\t\t\t\tplans[ index ][ name ] = [];\n\t\t\t\t\t}\n\t\t\t\t\tplans[ index ][ name ].push( form[ i ].value );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tplans[ index ][ name ] = form[ i ].value;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn plans;\n\n\t\t};\n\n\t\t/**\n\t\t * Determine if the access plan limit has been reached\n\t\t *\n\t\t * @return Boolean\n\t\t * @since  3.0.0\n\t\t * @version  3.29.0\n\t\t */\n\t\tthis.has_plan_limit_been_reached = function() {\n\n\t\t\tvar limit = window.llms.product.access_plan_limit;\n\t\t\treturn this.get_current_plan_count() >= limit;\n\n\t\t};\n\n\t\t/**\n\t\t * Initializes a new plan and adds it to the list of plans in the DOM\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.30.0 Initialize select2 on checkout redirect fields.\n\t\t * @version 3.30.0\n\t\t *\n\t\t * @return   void\n\t\t */\n\t\tthis.init_plan = function() {\n\n\t\t\t// don't do anything if we've reached the plan limit\n\t\t\tif ( this.has_plan_limit_been_reached() ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar $clone          = $( '#llms-new-access-plan-model' ).clone()\n\t\t\t\t$existing_plans = $( '#llms-access-plans .llms-access-plan' ),\n\t\t\t\t$editor         = $clone.find( '#_llms_plans_content_llms-new-access-plan-model' );\n\n\t\t\t// remove ID from the item\n\t\t\t$clone.removeAttr( 'id' );\n\n\t\t\t// give a temporary id to the editor element\n\t\t\t$editor.removeAttr( 'id' ).attr( 'id', '_llms_plans_content_' + this.temp_id );\n\t\t\tthis.temp_id++; // increment the temp_id ID so we don't use it again\n\n\t\t\t// activate all elements\n\t\t\t$clone.find( 'select, input, textarea' ).each( function() {\n\t\t\t\t$( this ).removeAttr( 'disabled' ); // enabled the field\n\t\t\t} );\n\n\t\t\t$clone.appendTo( '#llms-access-plans' );\n\n\t\t\t// rewrite the order of all elements\n\t\t\tthis.update_plan_orders();\n\n\t\t\t// Bind the datepicker to the new plan AFTER the input names have been updated via update_plan_orders() above.\n\t\t\t$clone.find( '.llms-access-plan-datepicker' ).datepicker( {\n\t\t\t\tdateFormat: \"mm/dd/yy\"\n\t\t\t} );\n\n\t\t\t$clone.find( '.llms-collapsible-header' ).trigger( 'click' );\n\n\t\t\t// check if the limit has been reached and toggle the button if it has\n\t\t\tif ( this.has_plan_limit_been_reached() ) {\n\t\t\t\tthis.toggle_create_button( 'disable' );\n\t\t\t}\n\n\t\t\t// select2ify membership availability field\n\t\t\twindow.llms.metaboxes.post_select( $clone.find( '.llms-availability-restrictions' ) );\n\n\t\t\t// select2ify redirection page fields\n\t\t\twindow.llms.metaboxes.post_select( $clone.find( '.llms-checkout-redirect-page' ) );\n\n\t\t\t$clone.find( '[data-controller-id]' ).trigger( 'change' );\n\t\t\t$( document ).trigger( 'llms-plan-init', $clone );\n\n\t\t};\n\n\t\t/**\n\t\t * Persist access plans to the DB if they pass validation\n\t\t *\n\t\t * @since 3.29.0\n\t\t * @since 3.30.3 Fixed typo in error message.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.save_plans = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tself.$plans.find( '.llms-access-plan' ).not( '#llms-new-access-plan-model' ).each( function() {\n\t\t\t\t$( this ).trigger( 'llms-validate-plan' );\n\t\t\t} );\n\n\t\t\tif ( self.$plans.find( '.' + self.validation_class ).length ) {\n\t\t\t\tself.$plans.find( '.llms-access-plan.' + self.validation_class ).not( '.opened' ).first().find( '.llms-collapsible-header' ).trigger( 'click' );\n\t\t\t\t$( document ).trigger( 'llms-access-plan-validation-errors' );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tLLMS.Spinner.start( self.$plans );\n\t\t\tself.$save.attr( 'disabled', 'disabled' );\n\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'llms_update_access_plans',\n\t\t\t\t\tplans: self.get_plans_array(),\n\t\t\t\t},\n\t\t\t\tcomplete: function() {\n\t\t\t\t\tLLMS.Spinner.stop( self.$plans );\n\t\t\t\t\tself.$save.removeAttr( 'disabled' );\n\t\t\t\t},\n\t\t\t\terror: function( jqXHR, textStatus, errorThrown ) {\n\t\t\t\t\tconsole.error( 'llms access plan save error encounterd:', jqXHR );\n\t\t\t\t\talert( LLMS.l10n.translate( 'An error was encountered during the save attempt. Please try again.' ) + ' [' + textStatus + ': ' + errorThrown + ']' );\n\t\t\t\t},\n\t\t\t\tsuccess: function( res ) {\n\n\t\t\t\t\tif ( ! res.success && res.code && 'error' === res.code ) {\n\t\t\t\t\t\talert( res.message );\n\t\t\t\t\t} else if ( res.data && res.data.html ) {\n\n\t\t\t\t\t\t// replace the metabox with updated data from the server.\n\t\t\t\t\t\t$( '#llms-product-options-access-plans' ).replaceWith( res.data.html );\n\n\t\t\t\t\t\t// reinit.\n\t\t\t\t\t\tself.init( true );\n\t\t\t\t\t\twindow.llms.metaboxes.init();\n\t\t\t\t\t\tself.update_plan_orders();\n\n\t\t\t\t\t\t// notify the block editor\n\t\t\t\t\t\tself.trigger_update_hook();\n\n\t\t\t\t\t}\n\n\t\t\t\t},\n\n\t\t\t} );\n\t\t};\n\n\t\t/**\n\t\t * Toggle the status of a button\n\t\t *\n\t\t * @param   Object  $btn   jQuery selector of a button element\n\t\t * @param  string status enable or disable\n\t\t * @return  void\n\t\t * @since   3.29.0\n\t\t * @version 3.29.0\n\t\t */\n\t\tthis.toggle_button = function( $btn, status ) {\n\n\t\t\tif ( 'disable' === status ) {\n\t\t\t\t$btn.attr( 'disabled', 'disabled' );\n\t\t\t} else {\n\t\t\t\t$btn.removeAttr( 'disabled' );\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Control the status of the \"New Access Plan\" Button\n\t\t *\n\t\t * @param  string status enable or disable\n\t\t * @return void\n\t\t * @since  3.0.0\n\t\t * @since  3.29.0\n\t\t */\n\t\tthis.toggle_create_button = function( status ) {\n\t\t\tthis.toggle_button( $( '#llms-new-access-plan' ), status );\n\t\t};\n\n\t\t/**\n\t\t * Control the status of the \"Save Access Plans\" Button\n\t\t *\n\t\t * @param  string status enable or disable\n\t\t * @return void\n\t\t * @since  3.0.0\n\t\t * @since  3.29.0\n\t\t */\n\t\tthis.toggle_save_button = function( status ) {\n\t\t\tthis.toggle_button( this.$save, status );\n\t\t}\n\n\t\t/**\n\t\t * Visually hide and then physically remove a plan element from the DOM\n\t\t * Additionally determines if the New Plan Button should be re-enabled\n\t\t * after deletion\n\t\t *\n\t\t * @param  obj   $plan jQuery selector of the plan element\n\t\t * @return void\n\t\t * @since 3.0.0\n\t\t * @version 3.29.0\n\t\t */\n\t\tthis.remove_plan_el = function( $plan ) {\n\n\t\t\tvar self = this;\n\n\t\t\t// fade out nicely\n\t\t\t$plan.fadeOut( 400 );\n\n\t\t\t// remove from dom after it's hidden visually\n\t\t\tsetTimeout(function() {\n\n\t\t\t\t$plan.remove();\n\n\t\t\t\t// check if we need to reenable the create button and hide the message\n\t\t\t\tif ( ! self.has_plan_limit_been_reached() ) {\n\t\t\t\t\tself.toggle_create_button( 'enable' );\n\t\t\t\t}\n\n\t\t\t\tif ( 0 === self.get_current_plan_count() ) {\n\t\t\t\t\tself.toggle_save_button( 'disable' );\n\t\t\t\t}\n\n\t\t\t}, 450 );\n\n\t\t};\n\n\t\t/**\n\t\t * Trigger WP Block Editor hook so the pricing table block can be re-rendered with new plan information.\n\t\t *\n\t\t * @return  void\n\t\t * @since   3.29.0\n\t\t * @version 3.29.0\n\t\t */\n\t\tthis.trigger_update_hook = function() {\n\n\t\t\t$( document ).trigger( 'llms-access-plans-updated' );\n\n\t\t};\n\n\t\t/**\n\t\t * Reorder the array indexes and the menu order hidden inputs.\n\t\t * Called by jQuery UI Sortable on sort completion.\n\t\t * Also called after adding a new plan to the DOM so the newest item is always\n\t\t * persisted as the last in the database if no UX reorders the item.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.36.3 Fixed conflicts with the classic editor block.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.update_plan_orders = function() {\n\n\t\t\t$( '#llms-access-plans .llms-access-plan' ).each( function() {\n\n\t\t\t\tvar $p        = $( this ),\n\t\t\t\t\t$order    = $p.find( '.plan-order' ),\n\t\t\t\t\t$editor   = $p.find( 'textarea[id^=\"_llms_plans_content_\"]' ),\n\t\t\t\t\teditor_id = $editor.attr( 'id' ),\n\t\t\t\t\torig      = $order.val() * 1,\n\t\t\t\t\tcurr      = $p.index(),\n\t\t\t\t\teditor    = tinyMCE.EditorManager.get(editor_id),\n\t\t\t\t\tesettings = editor ? editor.settings : tinyMCE.EditorManager.settings;\n\n\t\t\t\t// make sure the editor settings have the right selector.\n\t\t\t\tesettings.selector = '#' + editor_id;\n\n\t\t\t\t// de-init tinyMCE from the editor.\n\t\t\t\ttinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, editor_id );\n\n\t\t\t\t// update the order of each label and field in the plan.\n\t\t\t\t$p.find( 'label, select, input, textarea' ).each( function() {\n\n\t\t\t\t\tvar labelFor = $( this ).attr( 'for' );\n\t\t\t\t\tif ( labelFor ) {\n\t\t\t\t\t\t$( this ).attr( 'for', labelFor.replace( orig, curr ) );\n\t\t\t\t\t}\n\n\t\t\t\t\tvar inputID = $( this ).attr( 'id' );\n\t\t\t\t\tif ( inputID ) {\n\t\t\t\t\t\t$( this ).attr( 'id', inputID.replace( orig, curr ) );\n\t\t\t\t\t}\n\n\t\t\t\t\tvar inputName = $( this ).attr( 'name' );\n\t\t\t\t\tif ( inputName ) {\n\t\t\t\t\t\t$( this ).attr( 'name', inputName.replace( orig, curr ) );\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t\t// re-init tinyMCE on the editor.\n\t\t\t\t// We used:\ttinyMCE.EditorManager.execCommand( 'mceAddEditor', true, editor_id );\n\t\t\t\t// but it turned out to create conflicts with the Classic Editor block.\n\t\t\t\ttinyMCE.EditorManager.init( esettings );\n\n\t\t\t\t$order.val( curr );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t// go\n\t\tthis.init();\n\n\t};\n\n\tvar a = new window.llms.metabox_product();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-metabox-students.js",
    "content": "/**\n * LifterLMS Students Metabox Functions\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.0.0\n * @version 3.33.0\n */\n\n/**\n * LifterLMS Students Metabox Functions\n *\n * @since 3.0.0\n * @since 3.33.0 Added the logic to handle the Student's enrollment deletion.\n */\n( function( $, undefined ) {\n\n\twindow.llms = window.llms || {};\n\n\tvar MetaboxStudents = function() {\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @return void\n\t\t * @since  3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\tvar screens = [ 'course', 'llms_membership' ];\n\n\t\t\tif ( window.llms.post.post_type && -1 !== screens.indexOf( window.llms.post.post_type ) ) {\n\n\t\t\t\tthis.$metabox = $( '#lifterlms-students' );\n\n\t\t\t\tthis.bind();\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Bind dom events\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.4.0 Unknown.\n\t\t * @since 3.33.0 Added enrollment deletion handlers.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.bind = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.$metabox.on( 'click', 'a.llms-remove-student', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.remove_student( $( this ) );\n\t\t\t} );\n\n\t\t\tthis.$metabox.on( 'click', 'a.llms-delete-enrollment', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.delete_student_enrollment( $( this ) );\n\t\t\t} );\n\n\t\t\tthis.$metabox.on( 'click', 'a.llms-add-student', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.add_student( $( this ) );\n\t\t\t} );\n\n\t\t\t$( '#llms-add-student-select' ).llmsStudentsSelect2( { multiple: true } );\n\n\t\t\t$( '#llms-enroll-students' ).on( 'click', function() {\n\t\t\t\tself.enroll_students( $( this ) );\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Add a Student\n\t\t *\n\t\t * @param    obj  $el  jQuery selector of the add button\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.add_student = function( $el ) {\n\t\t\tthis.update_student_enrollment( $el.attr( 'data-id' ), 'add' );\n\t\t};\n\n\t\t/**\n\t\t * Handle bulk enrollment via \"Enroll New Students\" area\n\t\t *\n\t\t * @param    obj   $el  jQuery selector for the triggering button\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.4.0\n\t\t */\n\t\tthis.enroll_students = function( $el ) {\n\n\t\t\tvar self       = this,\n\t\t\t\t$select    = $( '#llms-add-student-select' ),\n\t\t\t\tids        = $select.val(),\n\t\t\t\t$container = this.$metabox.find( '.llms-metabox-students-add-new' );\n\n\t\t\tLLMS.Spinner.start( $container );\n\n\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'bulk_enroll_students',\n\t\t\t\t\tstudent_ids: ids,\n\t\t\t\t},\n\t\t\t\tbeforeSend: function( xhr ) {\n\t\t\t\t\tif ( ! ids ) {\n\t\t\t\t\t\t$el.before( '<span class=\"llms-error\">' + LLMS.l10n.translate( 'Please select a student to enroll' ) + '</span>' );\n\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\tLLMS.Spinner.stop( $container );\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\t$select.val( null ).trigger( 'change' );\n\t\t\t\t\tLLMS.Spinner.stop( $container );\n\t\t\t\t\twindow.llms.admin_tables.reload( $( '#llms-gb-table-student-management' ) );\n\n\t\t\t\t},\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Remove a Student\n\t\t *\n\t\t * @param    obj  $el  jQuery selector of the add button\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.remove_student = function( $el ) {\n\t\t\tthis.update_student_enrollment( $el.attr( 'data-id' ), 'remove' );\n\t\t};\n\n\t\t/**\n\t\t * Delete a Student's enrollment.\n\t\t *\n\t\t * @since 3.33.0\n\t\t *\n\t\t * @param  obj  $el  jQuery selector of the add button.\n\t\t * @return void\n\t\t */\n\t\tthis.delete_student_enrollment = function( $el ) {\n\t\t\tthis.update_student_enrollment( $el.attr( 'data-id' ), 'delete' );\n\t\t};\n\n\t\t/**\n\t\t * Execute AJAX call, add spinners, update html view\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param int id Student id.\n\t\t * @param string status New status [add|remove|delete].\n\t\t * @return void\n\t\t */\n\t\tthis.update_student_enrollment = function( id, status ) {\n\n\t\t\tvar $table     = $( '#llms-gb-table-student-management' ),\n\t\t\t\t$container = $table.closest( '.llms-table-wrap' );\n\n\t\t\tLLMS.Spinner.start( $container );\n\n\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'update_student_enrollment',\n\t\t\t\t\tstatus: status,\n\t\t\t\t\tstudent_id: id,\n\t\t\t\t},\n\t\t\t\tsuccess: function() {\n\t\t\t\t\t// spinner doesn't stop because the table reloader will stop it\n\t\t\t\t\twindow.llms.admin_tables.reload( $table );\n\t\t\t\t},\n\t\t\t} );\n\n\t\t};\n\n\t\t// go\n\t\tthis.init();\n\n\t};\n\n\t// initialize the object\n\twindow.llms.MetaboxStudents = new MetaboxStudents();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-metabox-voucher.js",
    "content": "( function( $ ) {\n\n\tvar deleteIds = [];\n\n\t$( document ).ready( function () {\n\n\t\tvar changeNotSaved          = false;\n\t\tvar codesAddedSinceLastSave = 0;\n\n\t\t$( '#llms_voucher_add_codes' ).click(function ( e ) {\n\t\t\te.preventDefault();\n\n\t\t\tvar qty  = $( '#llms_voucher_add_quantity' ).val();\n\t\t\tvar uses = $( '#llms_voucher_add_uses' ).val();\n\t\t\tvar html = '';\n\n\t\t\tchangeNotSaved = true;\n\n\t\t\tif ( $.isNumeric( qty ) && $.isNumeric( uses ) ) {\n\t\t\t\tif ( parseInt( qty ) > 0 && parseInt( uses ) > 0 ) {\n\n\t\t\t\t\tif ( qty > 50 ) {\n\t\t\t\t\t\talert( \"You can only generate 50 rows at a time\" );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcodesAddedSinceLastSave += parseInt( qty );\n\n\t\t\t\t\tif ( codesAddedSinceLastSave > 50 ) {\n\t\t\t\t\t\talert( \"Please save before adding any more codes, limit is 50 at a time\" );\n\t\t\t\t\t\tcodesAddedSinceLastSave -= parseInt( qty );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tfor ( var i = 1; i <= parseInt( qty ); i++ ) {\n\t\t\t\t\t\thtml += '<tr>' +\n\t\t\t\t\t\t\t'<td></td>' +\n\t\t\t\t\t\t\t'<td>' +\n\t\t\t\t\t\t\t'<input type=\"text\" maxlength=\"20\" placeholder=\"Code\" value=\"' + randomizeCode() + '\" name=\"llms_voucher_code[]\">' +\n\t\t\t\t\t\t\t'<input type=\"hidden\" name=\"llms_voucher_code_id[]\" value=\"0\">' +\n\t\t\t\t\t\t\t'</td>' +\n\t\t\t\t\t\t\t'<td><span>0 / </span><input type=\"text\" placeholder=\"Uses\" value=\"' + uses + '\" class=\"llms-voucher-uses\" name=\"llms_voucher_uses[]\"></td>' +\n\t\t\t\t\t\t\t'<td><a href=\"#\" class=\"llms-voucher-delete\">' + delete_icon + '</a></td>' +\n\t\t\t\t\t\t\t'</tr>';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$( '#llms_voucher_tbody' ).append( html );\n\n\t\t\tbindDeleteVoucherCode();\n\t\t} );\n\n\t\tbindDeleteVoucherCode();\n\n\t\t$( '#llms_voucher_tbody input' ).change( function() {\n\t\t\tchangeNotSaved = true;\n\t\t} );\n\n\t\t$( \"#post\" ).on( 'submit', function() {\n\t\t\tif ( $( '#publish' ).attr( 'name' ) === 'publish' ) {\n\t\t\t\t$( '<input />' ).attr( 'type', 'hidden' )\n\t\t\t\t\t.attr( 'name', \"publish\" )\n\t\t\t\t\t.attr( 'value', \"true\" )\n\t\t\t\t\t.appendTo( '#post' );\n\t\t\t}\n\t\t\treturn true;\n\t\t} );\n\n\t\twindow.onbeforeunload = function() {\n\t\t\treturn changeNotSaved ? \"If you leave this page you will lose your unsaved changes.\" : null;\n\t\t};\n\n\t\t$( 'input[type=submit][name=publish], input[type=submit][name=save]' ).click( function ( e ) {\n\t\t\tvar unique_values = {};\n\t\t\tvar duplicate     = false;\n\n\t\t\t$( 'input[name=\"llms_voucher_code[]\"]' ).each( function() {\n\t\t\t\tvar val = $( this ).val()\n\t\t\t\tif ( ! unique_values[val] ) {\n\t\t\t\t\tunique_values[val] = true;\n\t\t\t\t} else {\n\t\t\t\t\t$( this ).css( 'background-color', 'rgba(226, 96, 73, 0.6)' );\n\t\t\t\t\tduplicate = true;\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tif ( duplicate ) {\n\t\t\t\talert( 'Please make sure that there are no duplicate voucher codes.' );\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// If course or membership is not selected, don't allow user to save.\n\t\t\tif ( ! $( '#_llms_voucher_courses' ).val().length && ! $( '#_llms_voucher_membership' ).val().length ) {\n\t\t\t\talert( 'Please select course or membership before saving.' );\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tchangeNotSaved = false;\n\t\t\tcheck_voucher_duplicate();\n\t\t\treturn false;\n\t\t} );\n\n\t\tfunction bindDeleteVoucherCode() {\n\t\t\t$( '.llms-voucher-delete' ).unbind( 'click' );\n\t\t\t$( '.llms-voucher-delete' ).click( function ( e ) {\n\t\t\t\te.preventDefault();\n\n\t\t\t\tvar t   = $( this );\n\t\t\t\tvar old = t.data( 'id' );\n\n\t\t\t\tchangeNotSaved = true;\n\n\t\t\t\tif ( old ) {\n\t\t\t\t\tdeleteIds.push( old );\n\n\t\t\t\t\t$( '#delete_ids' ).val( deleteIds.join( ',' ) );\n\t\t\t\t} else {\n\t\t\t\t\tcodesAddedSinceLastSave--;\n\t\t\t\t}\n\n\t\t\t\t// remove html block\n\t\t\t\tt.closest( 'tr' ).remove();\n\t\t\t} );\n\t\t}\n\t} );\n\n\tfunction randomizeCode() {\n\t\tvar text     = '';\n\t\tvar possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\n\t\tfor ( var i = 0; i < 12; i++ ) {\n\t\t\ttext += possible.charAt( Math.floor( Math.random() * possible.length ) );\n\t\t}\n\n\t\treturn text;\n\t}\n\n\t/**\n\t * Check for voucher duplicates in other posts.\n\t *\n\t * @since Unknown\n\t * @since 5.9.0 Add nonce.\n\t * @since 7.1.3 Add check for empty vouchers.\n\t *\n\t * @return {void}\n\t */\n\tfunction check_voucher_duplicate() {\n\n\t\tvar vouchers = get_codes_from_inputs();\n\n\t\tif( ! vouchers.length ) {\n\t\t\t$( \"#post\" ).submit();\n\t\t\treturn;\n\t\t}\n\n\t\tvar data = {\n\t\t\taction: 'check_voucher_duplicate',\n\t\t\tpostId: $( '#post_ID' ).val(),\n\t\t\tcodes: vouchers,\n\t\t\t_ajax_nonce: window.llms.ajax_nonce,\n\t\t};\n\n\t\tvar ajax = new Ajax( 'post', data, false );\n\t\tajax.check_voucher_duplicate();\n\t}\n\n\tfunction get_codes_from_inputs() {\n\t\tvar codes = [];\n\t\t$( 'input[name=\"llms_voucher_code[]\"]' ).each( function() {\n\t\t\tcodes.push( $( this ).val() );\n\t\t} );\n\n\t\treturn codes;\n\t}\n\n} )( jQuery );\n\nfunction llms_on_voucher_duplicate( results ) {\n\tif ( results.length ) {\n\t\tfor ( var i = 0; i < results.length; i++ ) {\n\t\t\tjQuery( 'input[value=\"' + results[i].code + '\"]' ).css( 'background-color', 'rgba(226, 96, 73, 0.6)' );\n\t\t}\n\t\talert( 'Please make sure that there are no duplicate voucher codes.' );\n\t} else {\n\t\tjQuery( \"#post\" ).submit();\n\t}\n}\n"
  },
  {
    "path": "assets/js/llms-notifications.js",
    "content": ";/**\n * LifterLMS Basic Notifications Displayer\n *\n * @since    3.8.0\n * @version  3.22.0\n */( function( $ ) {\n\n\tvar llms_notifications = function() {\n\n\t\tvar self          = this,\n\t\t\tnotifications = [];\n\n\t\t/**\n\t\t * Bind dom events\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tfunction bind_events() {\n\t\t\t$( 'body' ).on( 'click', '.llms-notification-dismiss', function() {\n\t\t\t\tself.dismiss( $( this ).closest( '.llms-notification' ) );\n\t\t\t} );\n\t\t};\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.8.0\n\t\t * @version  3.22.0\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tif ( ! this.is_user_logged_in() ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( window.llms.queued_notifications ) {\n\t\t\t\tself.queue( window.llms.queued_notifications );\n\t\t\t\tself.show_all();\n\t\t\t}\n\n\t\t\tbind_events();\n\n\t\t};\n\n\t\t/**\n\t\t * Queue notifications to be displayed\n\t\t *\n\t\t * @param    object   new_notis  array of notifications\n\t\t * @return   void\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.queue = function( new_notis ) {\n\n\t\t\tvar self = this;\n\n\t\t\tfor ( var n in new_notis ) {\n\n\t\t\t\tif ( ! new_notis.hasOwnProperty( n ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// add the new notification if it doesnt exist\n\t\t\t\tif ( false === self.notification_exists( new_notis[ n ].id ) ) {\n\n\t\t\t\t\tnotifications.push( new_notis[ n ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Dismiss a notification\n\t\t *\n\t\t * @param    obj   $el  notification dom element\n\t\t * @return   void\n\t\t * @since    3.8.0\n\t\t * @version  3.22.0\n\t\t */\n\t\tthis.dismiss = function( $el ) {\n\t\t\tvar self = this;\n\t\t\t$el.removeClass( 'visible' );\n\t\t\tsetTimeout( function() {\n\t\t\t\tself.reposition( $el.next( '.llms-notification.visible' ) );\n\t\t\t}, 10 );\n\t\t};\n\n\t\t/**\n\t\t * Determine if a notification already exists in the notifications array\n\t\t *\n\t\t * @param    int        id  notification id\n\t\t * @return   int|false      index of the notification in the array OR false if not found\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.notification_exists = function( id ) {\n\n\t\t\tfor ( var noti in notifications ) {\n\n\t\t\t\tif ( ! notifications.hasOwnProperty( noti ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( id === notifications[ noti ].id ) {\n\t\t\t\t\treturn noti;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t};\n\n\t\t/**\n\t\t * Get the vertical offset (on screen) relative to an element\n\t\t * used for notification positioning\n\t\t *\n\t\t * @param    obj   $relative_el  element to get an offset relative to\n\t\t * @return   int\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.get_offset = function( $relative_el ) {\n\n\t\t\tvar spacer = 12;\n\n\t\t\tif ( ! $relative_el ) {\n\t\t\t\t$relative_el = $( '.llms-notification.visible' ).last()\n\t\t\t}\n\n\t\t\tif ( ! $relative_el.offset() ) {\n\t\t\t\treturn 24;\n\t\t\t}\n\n\t\t\tvar top    = $relative_el.offset().top,\n\t\t\t\theight = $relative_el.outerHeight();\n\n\t\t\treturn top + height + spacer;\n\n\t\t};\n\n\t\t/**\n\t\t * Determine if there are notifications to show\n\t\t *\n\t\t * @return   Boolean\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.has_notifications = function() {\n\t\t\treturn ( notifications.length );\n\t\t};\n\n\t\t/**\n\t\t * Determine if a user is logged in\n\t\t *\n\t\t * @return   boolean\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.is_user_logged_in = function() {\n\t\t\treturn $( 'body' ).hasClass( 'logged-in' );\n\t\t};\n\n\t\t/**\n\t\t * Reposition elements, starting with the specified element\n\t\t *\n\t\t * @param    obj   $start_el  element to start repositioning with\n\t\t * @return   void\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.reposition = function( $start_el ) {\n\n\t\t\tvar self     = this,\n\t\t\t\tselector = '.llms-notification.visible',\n\t\t\t\t$next_el;\n\n\t\t\tif ( ! $start_el.length ) {\n\t\t\t\t$start_el = $( selector ).first();\n\t\t\t}\n\n\t\t\t$start_el.css( 'top', self.get_offset( $start_el.prevAll( selector ).first() ) );\n\n\t\t\t$next_el = $start_el.next( selector );\n\t\t\tif ( $next_el.length ) {\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\tself.reposition( $next_el );\n\t\t\t\t}, 150 );\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Show all queued notifications\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.show_all = function() {\n\n\t\t\tvar self = this,\n\t\t\t\ti    = 0,\n\t\t\t\tinterval;\n\n\t\t\tinterval = setInterval( function() {\n\n\t\t\t\tif ( i < notifications.length ) {\n\n\t\t\t\t\tif ( ! notifications[ i ].shown ) {\n\t\t\t\t\t\tnotifications[ i ].shown = true;\n\t\t\t\t\t\tself.show_one( notifications[ i ] );\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tclearInterval( interval );\n\n\t\t\t\t}\n\n\t\t\t}, 100 );\n\n\t\t}\n\n\t\t/**\n\t\t * Show a single notification\n\t\t *\n\t\t * @param    object   n  notification object data\n\t\t * @return   void\n\t\t * @since    3.8.0\n\t\t * @version  3.22.0\n\t\t */\n\t\tthis.show_one = function( n ) {\n\n\t\t\tvar self  = this,\n\t\t\t\t$html = $( n.html );\n\n\t\t\t$html.find( 'a' ).on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tvar $this       = $( this );\n\t\t\t\twindow.location = $this.attr( 'href' );\n\t\t\t} );\n\n\t\t\t$( 'body' ).append( $html );\n\t\t\t$html.css( 'top', self.get_offset() );\n\n\t\t\tsetTimeout( function() {\n\t\t\t\t$html.addClass( 'visible' );\n\t\t\t}, 1 );\n\n\t\t\t// if it's auto dismissing, set up a dismissal\n\t\t\tif ( $html.attr( 'data-auto-dismiss' ) ) {\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\tself.dismiss( $html );\n\t\t\t\t}, $html.attr( 'data-auto-dismiss' ) );\n\t\t\t}\n\n\t\t}\n\n\t\t// initialize\n\t\tthis.init();\n\n\t\treturn this;\n\n\t};\n\n\twindow.llms               = window.llms || {};\n\twindow.llms.notifications = new llms_notifications();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-quiz-attempt-review.js",
    "content": ";/**\n * Quiz attempt review / grading UI & UX\n *\n * @since 3.16.0\n * @since 3.30.3 Unknown.\n * @version 5.3.0\n */( function( $ ) {\n\n\t/**\n\t * Handle UX for graving quiz attempts.\n\t *\n\t * @since 3.16.0\n\t * @since 3.30.3 Improve grading UX\n\t */\n\tvar Grading = function() {\n\n\t\t/**\n\t\t * Bind DOM events\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.16.9 Unknown.\n\t\t * @return {Void}\n\t\t */\n\t\tfunction bind() {\n\n\t\t\t$( 'button[name=\"llms_quiz_attempt_action\"][value=\"llms_attempt_grade\"]' ).one( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\t$( this ).addClass( 'grading' );\n\n\t\t\t\tsetup_fields();\n\n\t\t\t} );\n\n\t\t}\n\n\t\t/**\n\t\t * Create editable fields for grading / remarking\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 3.30.3 When starting a review only toggle first item if it's hidden and always automatically focus on the remarks field.\n\t\t * @since 5.3.0 Exclude removed question items.\n\t\t *\n\t\t * @return {Grading}\n\t\t */\n\t\tfunction setup_fields() {\n\n\t\t\t$els = $( '.llms-quiz-attempt-question:not(.type--content):not(.type--removed)' );\n\n\t\t\tif ( $els.length < 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar title  = LLMS.l10n.translate( 'Remarks to Student' ),\n\t\t\t\tpoints = LLMS.l10n.translate( 'points' );\n\n\t\t\t$els.each( function() {\n\n\t\t\t\tvar id        = $( this ).attr( 'data-question-id' ),\n\t\t\t\t\t$existing = $( this ).find( '.llms-quiz-attempt-answer-section.llms-remarks' ),\n\t\t\t\t\t$ui       = $( '<div class=\"llms-quiz-attempt-answer-section llms-remarks\" />' ),\n\t\t\t\t\t$textarea = $( '<textarea class=\"llms-remarks-field\" name=\"remarks[' + id + ']\"></textarea>' )\n\t\t\t\t\tgradeable = ( 'yes' === $( this ).attr( 'data-grading-manual' ) );\n\n\t\t\t\t$ui.append( '<p class=\"llms-quiz-results-label remarks\">' + title + ':</p>' )\n\t\t\t\t$ui.append( $textarea );\n\t\t\t\tif ( gradeable ) {\n\t\t\t\t\tvar pts = $( this ).attr( 'data-points-curr' ),\n\t\t\t\t\t\tmax = $( this ).attr( 'data-points' );\n\t\t\t\t\t$ui.append( '<input name=\"points[' + id + ']\" max=\"' + max + '\" min=\"0\" type=\"number\" value=\"' + pts + '\"> / ' + max + ' ' + points );\n\t\t\t\t}\n\n\t\t\t\tif ( $existing.length ) {\n\n\t\t\t\t\t$textarea.text( $existing.find( '.llms-remarks' ).text() );\n\t\t\t\t\t$existing.replaceWith( $ui );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t$( this ).find( '.llms-quiz-attempt-question-main' ).append( $ui );\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tvar $els_first = $els.first();\n\t\t\tif ( ! $els_first.find( '.llms-quiz-attempt-question-main' ).is( ':visible' ) ) {\n\t\t\t\t// expand the first question toggle.\n\t\t\t\t$els_first.find( '.toggle-answer' ).trigger( 'click' );\n\t\t\t}\n\t\t\t// focus on its remark textarea.\n\t\t\t$els_first.find( '.llms-remarks-field' ).focus();\n\t\t}\n\n\t\tbind();\n\n\t\treturn this;\n\n\t};\n\n\twindow.llms         = window.llms || {};\n\twindow.llms.grading = new Grading();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-quiz.js",
    "content": ";/* global LLMS, $ */\n/* jshint strict: true */\n\n/**\n * Front End Quiz Class.\n *\n * @type {Object}\n *\n * @since 1.0.0\n * @version 7.8.0\n */( function( $ ) {\n\n\tvar quiz = {\n\n\t\t/**\n\t\t * Selector of all the available button elements\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\t$buttons: null,\n\n\t\t/**\n\t\t * Main Question Container Element.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\t$container: null,\n\n\t\t/**\n\t\t * Main Quiz container UI element.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\t$ui: null,\n\n\t\t/**\n\t\t * Attempt key for the current quiz.\n\t\t *\n\t\t * @type {[type]}\n\t\t */\n\t\tattempt_key: null,\n\n\t\t/**\n\t\t * Question ID of the current question.\n\t\t *\n\t\t * @type {Number}\n\t\t */\n\t\tcurrent_question: 0,\n\n\t\t/**\n\t\t * Total number of questions in the current quiz.\n\t\t *\n\t\t * @type {Number}\n\t\t */\n\t\ttotal_questions: 0,\n\n\t\t/**\n\t\t * Object of quiz question HTML.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tquestions: {},\n\n\t\t/**\n\t\t * Validator functions for question type.\n\t\t * Third party custom question types can register validators for use when answering questions.\n\t\t *\n\t\t * @type {Object}\n\t\t */\n\t\tvalidators: {},\n\n\t\t/**\n\t\t * Records current status of a quiz session.\n\t\t * If a user attempts to navigate away from a quiz\n\t\t * while taking the quiz they'll be warned that their progress\n\t\t * will not be saved if this status is not null.\n\t\t *\n\t\t * @type {Bool}\n\t\t */\n\t\tstatus: null,\n\n\t\t/**\n\t\t * Records if the quiz can be resumed.\n\t\t */\n\t\tresumable: null,\n\n\t\t/**\n\t\t * Flag if the user is exiting the quiz.\n\t\t */\n\t\texiting_quiz: false,\n\n\t\t/**\n\t\t * Bind DOM events.\n\t\t *\n\t\t * @since 1.0.0\n\t\t * @since 3.16.6 Unknown.\n\t\t * @since 7.8.0 Add quiz resume and hide leave warning if quiz is resumable.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tbind: function() {\n\n\t\t\tvar self = this;\n\n\t\t\t// Start quiz.\n\t\t\t$( '#llms_start_quiz' ).on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.start_quiz();\n\t\t\t} );\n\n\t\t\t// Resume quiz.\n\t\t\t$( '#llms_resume_quiz' ).on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.resume_quiz();\n\t\t\t} );\n\n\t\t\t// Draw quiz grade circular chart.\n\t\t\t$( '.llms-donut' ).each( function() {\n\t\t\t\tLLMS.Donut( $( this ) );\n\t\t\t} );\n\n\t\t\t// Redirect to attempt on attempt selection change.\n\t\t\t$( '#llms-quiz-attempt-select' ).on( 'change', function() {\n\t\t\t\tvar val = $( this ).val();\n\t\t\t\tif ( val ) {\n\t\t\t\t\twindow.location.href = val;\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\t// Warn when quiz is running and user tries to leave the page when quiz is not resumable.\n\t\t\t$( window ).on( 'beforeunload', function() {\n\t\t\t\tif ( self.status && ! self.exiting_quiz ) {\n\t\t\t\t\treturn LLMS.l10n.translate( 'Are you sure you wish to quit this quiz attempt?' );\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t} );\n\n\t\t\t// Complete the quiz attempt when user leaves if the quiz is running.\n\t\t\t$( window ).on( 'unload', function() {\n\t\t\t\tif ( self.status && ! self.resumable ) {\n\t\t\t\t\tself.complete_quiz();\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\t$( document ).on( 'llms-post-append-question', self.post_append_question );\n\n\t\t\t// Register validators.\n\t\t\tthis.register_validator( 'content', this.validate );\n\t\t\tthis.register_validator( 'choice', this.validate_choice );\n\t\t\tthis.register_validator( 'picture_choice', this.validate_choice );\n\t\t\tthis.register_validator( 'true_false', this.validate_choice );\n\n\t\t},\n\n\t\t/**\n\t\t * Add an error message to the UI\n\t\t *\n\t\t * @param    string   msg  error message string\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tadd_error: function( msg ) {\n\n\t\t\tvar self = this;\n\n\t\t\tself.$container.find( '#llms-quiz-error-container' ).remove();\n\t\t\tself.$container.append( '<div id=\"llms-quiz-error-container\" role=\"alert\" aria-atomic=\"true\"></div>' );\n\t\t\tconst $error_container = self.$container.find( '#llms-quiz-error-container' );\n\n\t\t\tvar $err = $( '<p class=\"llms-error\">' + msg + '<a href=\"#\"><i class=\"fa fa-times-circle\" aria-hidden=\"true\"></i></a></p>' );\n\t\t\t$err.on( 'click', 'a', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\t$err.fadeOut( '200' );\n\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t$err.remove();\n\t\t\t\t}, 210 );\n\t\t\t} );\n\t\t\t$error_container.append( $err );\n\n\t\t},\n\n\t\tsave_question: function( options ) {\n\t\t\tvar self      = this,\n\t\t\t\t$question = this.$container.find( '.llms-question-wrapper' ),\n\t\t\t\ttype      = $question.attr( 'data-type' ),\n\t\t\t\tvalid;\n\n\t\t\tif ( ! this.validators[ type ] ) {\n\t\t\t\tconsole.log( 'No validator registered for question type ' + type );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvalid = this.validators[ type ]( $question );\n\n\t\t\tvar requestData = {\n\t\t\t\taction: 'quiz_answer_question',\n\t\t\t\tanswer: valid.answer,\n\t\t\t\tattempt_key: self.attempt_key,\n\t\t\t\tquestion_id: $question.attr( 'data-id' ),\n\t\t\t\tquestion_type: $question.attr( 'data-type' ),\n\t\t\t};\n\n\t\t\tif ( options && options.exit_quiz ) {\n\t\t\t\trequestData.via_exit_quiz = true;\n\t\t\t}\n\n\t\t\tif ( options && options.previous_question ) {\n\t\t\t\trequestData.via_previous_question = true;\n\t\t\t}\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: requestData,\n\t\t\t\tsuccess: function( r ) {\n\t\t\t\t\tif (options && typeof options.callback === 'function') {\n\t\t\t\t\t\toptions.callback();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\n\t\t/**\n\t\t * Answer a Question\n\t\t *\n\t\t * @param    obj   $btn   jQuery object for the \"Next Lesson\" button\n\t\t * @return   void\n\t\t * @since    1.0.0\n\t\t * @version  3.16.6\n\t\t */\n\t\tanswer_question: function( $btn ) {\n\n\t\t\tvar self      = this,\n\t\t\t\t$question = this.$container.find( '.llms-question-wrapper' ),\n\t\t\t\ttype      = $question.attr( 'data-type' ),\n\t\t\t\tvalid;\n\n\t\t\tif ( ! this.validators[ type ] ) {\n\n\t\t\t\tconsole.log( 'No validator registered for question type ' + type );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tvalid = this.validators[ type ]( $question );\n\t\t\tif ( ! valid || true !== valid.valid || ! valid.answer ) {\n\t\t\t\treturn self.add_error( valid.valid );\n\t\t\t}\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'quiz_answer_question',\n\t\t\t\t\tanswer: valid.answer,\n\t\t\t\t\tattempt_key: self.attempt_key,\n\t\t\t\t\tquestion_id: $question.attr( 'data-id' ),\n\t\t\t\t\tquestion_type: $question.attr( 'data-type' ),\n\t\t\t\t},\n\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\tvar msg = $btn.hasClass( 'llms-button-quiz-complete' ) ? LLMS.l10n.translate( 'Grading Quiz...' ) : LLMS.l10n.translate( 'Loading Question...' );\n\t\t\t\t\tself.toggle_loader( 'show', msg );\n\n\t\t\t\t\tself.update_progress_bar( 'increment' );\n\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tself.toggle_loader( 'hide' );\n\n\t\t\t\t\tif ( r.data && r.data.html ) {\n\n\t\t\t\t\t\t// load html from the cached questions if it exists already\n\t\t\t\t\t\tif ( r.data.question_id && self.questions[ 'q-' + r.data.question_id ] ) {\n\n\t\t\t\t\t\t\tself.load_question( self.questions[ 'q-' + r.data.question_id ] );\n\n\t\t\t\t\t\t\t// load html from server if the question's never been seen before\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tself.load_question( r.data.html );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else if ( r.data && r.data.redirect ) {\n\n\t\t\t\t\t\tself.redirect( r.data.redirect );\n\n\t\t\t\t\t} else if ( r.message ) {\n\n\t\t\t\t\t\tself.$container.append( '<p>' + r.message + '</p>' );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tvar msg = LLMS.l10n.translate( 'An unknown error occurred. Please try again.' );\n\t\t\t\t\t\tself.$container.append( '<p>' + msg + '</p>' );\n\n\t\t\t\t\t}\n\n\t\t\t\t},\n\t\t\t\terror: function ( jqXHR, status, error ) {\n\t\t\t\t\tself.reload_question();\n\t\t\t\t\tself.add_error( LLMS.l10n.translate( 'An unknown error occurred. Please try again.' ) );\n\t\t\t\t\tconsole.log( error );\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Complete the quiz\n\t\t * Called when timed quizzes reach time limit\n\t\t * & during unload events to record the attempt as abandoned\n\t\t *\n\t\t * @return   void\n\t\t * @since    1.0.0\n\t\t * @version  3.9.0\n\t\t */\n\t\tcomplete_quiz: function() {\n\n\t\t\tvar self = this;\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: {\n\t\t\t\t\taction: 'quiz_end',\n\t\t\t\t\tattempt_key: self.attempt_key,\n\t\t\t\t},\n\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\tself.toggle_loader( 'show', 'Grading Quiz...' );\n\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tself.toggle_loader( 'hide' );\n\n\t\t\t\t\tif ( r.data && r.data.redirect ) {\n\n\t\t\t\t\t\tself.redirect( r.data.redirect );\n\n\t\t\t\t\t} else if ( r.message ) {\n\n\t\t\t\t\t\tself.$container.append( '<p>' + r.message + '</p>' );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tvar msg = LLMS.l10n.translate( 'An unknown error occurred. Please try again.' );\n\t\t\t\t\t\tself.$container.append( '<p>' + msg + '</p>' );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t},\n\n\t\t/**\n\t\t * Retrieve the index of a question by question id\n\t\t *\n\t\t * @param    int   qid  WP Post ID of the question\n\t\t * @return   int\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tget_question_index: function( qid ) {\n\n\t\t\treturn Object.keys( this.questions ).indexOf( 'q-' + qid );\n\n\t\t},\n\n\t\t/**\n\t\t * Redirect on quiz completion / timeout\n\t\t *\n\t\t * @param    string   url  redirect url\n\t\t * @return   void\n\t\t * @since    3.9.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tredirect: function( url ) {\n\n\t\t\tthis.toggle_loader( 'show', 'Grading Quiz...' );\n\t\t\tthis.status          = null;\n\t\t\twindow.location.href = url;\n\n\t\t},\n\n\t\treload_question: function() {\n\t\t\tvar self = this;\n\n\t\t\tself.toggle_loader( 'show', LLMS.l10n.translate( 'Loading Question...' ) );\n\t\t\tself.update_progress_bar( 'reload' );\n\n\t\t\tsetTimeout( function() {\n\t\t\t\tself.toggle_loader( 'hide' );\n\t\t\t\tself.load_question( self.questions[ 'q-' + self.current_question ] );\n\t\t\t}, 100 );\n\n\t\t},\n\n\t\t/**\n\t\t * Return to the previous question.\n\t\t *\n\t\t * @since 1.0.0\n\t\t * @since 3.16.6 Unknown.\n\t\t * @since 7.8.0 Retrieve question HTML from the server when not cached.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tprevious_question: function() {\n\n\t\t\tvar self = this;\n\n\t\t\tthis.save_question( {\n\t\t\t\tprevious_question: true,\n\t\t\t\tcallback: function() {\n\n\t\t\t\t\tself.toggle_loader( 'show', LLMS.l10n.translate( 'Loading Question...' ) );\n\t\t\t\t\tself.update_progress_bar( 'decrement' );\n\n\t\t\t\t\tvar ids     = Object.keys( self.questions ),\n\t\t\t\t\t\tcurr    = ids.indexOf( 'q-' + self.current_question ),\n\t\t\t\t\t\tprev_id = ids[0];\n\n\t\t\t\t\tif ( curr >= 1 ) {\n\t\t\t\t\t\tprev_id = ids[ curr - 1 ];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Retrieve previous question HTML from the server.\n\t\t\t\t\tif ( ! self.questions[ prev_id ] ) {\n\t\t\t\t\t\tLLMS.Ajax.call( {\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\taction     : 'quiz_get_question',\n\t\t\t\t\t\t\t\tattempt_key: self.attempt_key,\n\t\t\t\t\t\t\t\tquestion_id: prev_id.substring(2), // Remove 'q-'.\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\t\t\t\tself.toggle_loader( 'hide' );\n\t\t\t\t\t\t\t\tif ( r.data && r.data.html ) {\n\n\t\t\t\t\t\t\t\t\tself.load_question( r.data.html );\n\n\t\t\t\t\t\t\t\t} else if ( r.data && r.data.redirect ) {\n\n\t\t\t\t\t\t\t\t\tself.redirect( r.data.redirect );\n\n\t\t\t\t\t\t\t\t} else if ( r.message ) {\n\n\t\t\t\t\t\t\t\t\tself.$container.append( '<p>' + r.message + '</p>' );\n\n\t\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\t\tvar msg = LLMS.l10n.translate( 'An unknown error occurred. Please try again.' );\n\t\t\t\t\t\t\t\t\tself.$container.append( '<p>' + msg + '</p>' );\n\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} );\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t\tself.toggle_loader( 'hide' );\n\t\t\t\t\t\t\tself.load_question( self.questions[ prev_id ] );\n\t\t\t\t\t\t}, 100 );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\n\t\t/**\n\t\t * Register question type validator functions\n\t\t *\n\t\t * @param    string     type  question type id\n\t\t * @param    function   func  callback function to validate the question with\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tregister_validator: function( type, func ) {\n\n\t\t\tthis.validators[ type ] = func;\n\n\t\t},\n\n\t\t/**\n\t\t * Start a Quiz.\n\t\t *\n         * @since 1.0.0\n         * @since 3.24.3 Unknown.\n\t\t * @since 7.8.0 Abstracted the function in `init_quiz`.\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tstart_quiz: function () {\n\n\t\t\tthis.init_quiz( 'quiz_start' );\n\t\t},\n\n\t\t/**\n\t\t * Resume a Quiz.\n\t\t *\n\t\t * @since 7.8.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tresume_quiz: function () {\n\n\t\t\tthis.init_quiz( 'quiz_resume' );\n\t\t},\n\n\t\t/**\n\t\t * Initiate 'Start' or 'Resume' action on a Quiz via AJAX call.\n\t\t *\n\t\t * @since 7.8.0\n\t\t *\n\t\t * @return {Void}\n\t\t */\n\t\tinit_quiz: function ( action ) {\n\n\t\t\tvar self = this;\n\n\t\t\tif( 'quiz_resume' === action ) {\n\t\t\t\t// Disable resume button.\n\t\t\t\t$( '#llms_resume_quiz' ).attr( 'disabled', 'disabled' );\n\t\t\t}\n\n\t\t\tthis.load_ui_elements();\n\t\t\tthis.$ui        = $( '#llms-quiz-ui' );\n\t\t\tthis.$buttons   = $( '#llms-quiz-nav button' );\n\t\t\tthis.$container = $( '#llms-quiz-question-wrapper' );\n\n\t\t\t// Bind submission event for answering questions.\n\t\t\t$( '#llms-next-question, #llms-complete-quiz' ).on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.answer_question( $( this ) );\n\t\t\t} );\n\n\t\t\t// Bind submission event for navigating backwards.\n\t\t\t$( '#llms-prev-question' ).on( 'click', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.previous_question();\n\t\t\t} );\n\n\t\t\t// Bind exit event for quiz.\n\t\t\t$( '#llms-quiz-nav' ).on( 'click', '#llms-exit-quiz', function( e ) {\n\t\t\t\te.preventDefault();\n\t\t\t\tself.save_question( {\n\t\t\t\t\texit_quiz: true,\n\t\t\t\t\tcallback: function() {\n\t\t\t\t\t\tself.exiting_quiz = true;\n\t\t\t\t\t\twindow.location.reload();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} );\n\n\t\t\tif ( 'quiz_resume' === action ) {\n\t\t\t\tdata = {\n\t\t\t\t\taction: 'quiz_resume',\n\t\t\t\t\tattempt_key: $( '#llms-attempt-key' ).val(),\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tdata = {\n\t\t\t\t\taction: 'quiz_start',\n\t\t\t\t\tattempt_key: $( '#llms-attempt-key' ).val(),\n\t\t\t\t\tlesson_id : $( '#llms-lesson-id' ).val(),\n\t\t\t\t\tquiz_id : $( '#llms-quiz-id' ).val(),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tLLMS.Ajax.call( {\n\t\t\t\tdata: data,\n\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\tself.status = true;\n\t\t\t\t\t$( '#llms-quiz-wrapper, #quiz-start-button, #quiz-resume-button' ).remove();\n\t\t\t\t\t$( 'html, body' ).stop().animate( {scrollTop: 0 }, 500 );\n\t\t\t\t\tself.toggle_loader( 'show', LLMS.l10n.translate( 'Loading Quiz...' ) );\n\n\t\t\t\t},\n\t\t\t\terror: function( r, s, t ) {\n\t\t\t\t\tconsole.log( r, s, t );\n\t\t\t\t},\n\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\tself.toggle_loader( 'hide' );\n\n\t\t\t\t\tif ( r.data && r.data.html ) {\n\n\t\t\t\t\t\tself.attempt_key     = r.data.attempt_key;\n\t\t\t\t\t\tself.total_questions = r.data.total;\n\t\t\t\t\t\tself.resumable       = r.data.can_be_resumed;\n\n\t\t\t\t\t\tif( 'quiz_resume' === action ) {\n\t\t\t\t\t\t\tr.data.question_ids.forEach( id => self.questions[`q-${id}`] = '' );\n\t\t\t\t\t\t} else if ( r.data.time_limit ) {\n\t\t\t\t\t\t\tself.start_quiz_timer( r.data.time_limit );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Adding Exit Button in Layout if quiz is resumable.\n\t\t\t\t\t\tif ( self.resumable ) {\n\t\t\t\t\t\t\t$( '#llms-quiz-nav' ).append( '<button class=\"button llms-button-secondary\" id=\"llms-exit-quiz\" name=\"llms_exit_quiz\">' + LLMS.l10n.translate( 'Save & Exit Quiz' ) + '</button>' );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tself.load_question( r.data.html );\n\n\t\t\t\t\t\tif ( 'quiz_resume' === action ) {\n\t\t\t\t\t\t\tself.update_progress_bar( 'reload' );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else if ( r.message ) {\n\n\t\t\t\t\t\tself.$container.append( '<p>' + r.message + '</p>' );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tvar msg = LLMS.l10n.translate( 'An unknown error occurred. Please try again.' );\n\t\t\t\t\t\tself.$container.append( '<p>' + msg + '</p>' );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t/**\n\t\t\t * Use JS mouse events instead of CSS :hover because iOS is really smart\n\t\t\t *\n\t\t\t * @see: https://css-tricks.com/annoying-mobile-double-tap-link-issue/\n\t\t\t */\n\t\t\tif ( ! LLMS.is_touch_device() ) {\n\n\t\t\t\tthis.$ui.on( 'mouseenter', 'li.llms-choice label', function() {\n\t\t\t\t\t$( this ).addClass( 'hovered' );\n\t\t\t\t} );\n\t\t\t\tthis.$ui.on( 'mouseleave', 'li.llms-choice label', function() {\n\t\t\t\t\t$( this ).removeClass( 'hovered' );\n\t\t\t\t} );\n\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Start Quiz Timer\n\t\t * Gets minutes from hidden field\n\t\t * Not used as actual quiz timer. Quiz is timed on the server from the quiz class\n\t\t * Calculates minutes to milliseconds and then converts to hours / minutes\n\t\t * When time limit reaches 0 calls complete_quiz() to complete quiz.\n\t\t *\n\t\t * @return Calls get_count_down at a set interval of 1 second\n\t\t * @since    1.0.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tstart_quiz_timer: function( total_minutes ) {\n\n\t\t\t// create and append the UI for the countdown clock\n\t\t\tvar $el = $( '<div class=\"llms-quiz-timer\" id=\"llms-quiz-timer\" />' ),\n\t\t\t\tmsg = LLMS.l10n.translate( 'Time Remaining' );\n\n\t\t\t$el.append( '<i class=\"fa fa-clock-o\" aria-hidden=\"true\"></i><span class=\"screen-reader-text\">' + msg + '</span>' );\n\t\t\tconst quiz_header = $( '#llms-quiz-header' );\n\t\t\t$el.append( '<time aria-describedby=\"llms-timer-hint\" id=\"llms-quiz-time-remaining\" class=\"llms-tiles\" datetime=\"\"></time>' );\n\n\t\t\tquiz_header.append( $el );\n\t\t\tquiz_header.append( '<div id=\"llms-timer-live\" class=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\"></div>' );\n\t\t\tquiz_header.append( '<p id=\"llms-timer-hint\" class=\"sr-only\">Announcements every minute; every 10 seconds in the last minute.</p>' );\n\n\t\t\t// start the timer\n\t\t\tvar self        = this,\n\t\t\t\ttarget_date = new Date().getTime() + ( ( total_minutes * 60 ) * 1000 ), // set the countdown date\n\t\t\t\ttime_limit  = ( ( total_minutes * 60 ) * 1000 ),\n\t\t\t\tcountdown   = document.getElementById( 'llms-quiz-time-remaining' ), // get tag element\n\t\t\t\tdays, hours, minutes, seconds; // variables for time units\n\n\t\t\t// set actual timer\n\t\t\tsetTimeout( function() {\n\t\t\t\tself.complete_quiz();\n\t\t\t}, time_limit + 1000 );\n\n\t\t\tthis.getCountdown(\n\t\t\t\ttotal_minutes,\n\t\t\t\ttarget_date,\n\t\t\t\ttime_limit,\n\t\t\t\tdays,\n\t\t\t\thours,\n\t\t\t\tminutes,\n\t\t\t\tseconds,\n\t\t\t\tcountdown\n\t\t\t);\n\n\t\t\t// call get_count_down every 1 second\n\t\t\tsetInterval( function () {\n\t\t\t\tself.getCountdown(\n\t\t\t\t\ttotal_minutes,\n\t\t\t\t\ttarget_date,\n\t\t\t\t\ttime_limit,\n\t\t\t\t\tdays,\n\t\t\t\t\thours,\n\t\t\t\t\tminutes,\n\t\t\t\t\tseconds,\n\t\t\t\t\tcountdown\n\t\t\t\t);\n\t\t\t}, 1000 );\n\t\t},\n\n\t\t/**\n\t\t * Trigger events\n\t\t *\n\t\t * @param    string   event  event to trigger\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\ttrigger: function( event ) {\n\n\t\t\tvar self = this;\n\n\t\t\t// trigger question submission for the current question\n\t\t\tif ( 'answer_question' === event ) {\n\n\t\t\t\tif ( this.get_question_index( self.current_question ) === self.total_questions ) {\n\n\t\t\t\t\t$( '#llms-complete-quiz' ).trigger( 'click' );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t$( '#llms-next-question' ).trigger( 'click' );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Load the HTML of a question into the DOM and the question cache\n\t\t *\n\t\t * @param    string   html  string of html\n\t\t * @return   void\n\t\t * @since    3.9.0\n\t\t * @version  3.16.6\n\t\t */\n\t\tload_question: function( html ) {\n\n\t\t\tvar $html = $( html ),\n\t\t\t\tqid   = $html.attr( 'data-id' );\n\n\t\t\t// cache the question HTML for faster rewinds\n\t\t\tif ( ! this.questions[ 'q-' + qid ] ) {\n\t\t\t\tthis.questions[ 'q-' + qid ] = $html;\n\t\t\t}\n\n\t\t\tthis.update_progress( qid );\n\n\t\t\tthis.current_question = qid;\n\n\t\t\t$( document ).trigger( 'llms-pre-append-question', $html );\n\n\t\t\tthis.$container.append( $html );\n\n\t\t\t$( document ).trigger( 'llms-post-append-question', $html );\n\n\t\t\tvar $quizUi = $( '#llms-quiz-ui' );\n\t\t\tif ( $quizUi.length ) {\n\t\t\t\t$( 'html, body' ).animate( {\n\t\t\t\t\tscrollTop: $quizUi.offset().top - 50\n\t\t\t\t}, 300 );\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Constructs the quiz UI & adds the elements into the DOM\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.9\n\t\t */\n\t\tload_ui_elements: function() {\n\n\t\t\t// Removing the quiz UI elements if they already exist.\n\t\t\tif ( $( '#llms-quiz-ui').length > 0 ) {\n\t\t\t\t$( '#llms-quiz-ui' ).remove();\n\t\t\t}\n\n\t\t\tvar $html   = $( '<div class=\"llms-quiz-ui\" id=\"llms-quiz-ui\" />' ),\n\t\t\t\t$header = $( '<header class=\"llms-quiz-header\" id=\"llms-quiz-header\" />' )\n\t\t\t\t$footer = $( '<footer class=\"llms-quiz-nav\" id=\"llms-quiz-nav\" />' );\n\n\t\t\t$footer.append( '<button class=\"button large llms-button-action\" id=\"llms-next-question\" name=\"llms_next_question\" type=\"submit\">' + LLMS.l10n.translate( 'Next Question' ) + '</button>' );\n\t\t\t$footer.append( '<button class=\"button large llms-button-action llms-button-quiz-complete\" id=\"llms-complete-quiz\" name=\"llms_complete_quiz\" type=\"submit\" style=\"display:none;\">' + LLMS.l10n.translate( 'Complete Quiz' ) + '</button>' );\n\t\t\t$footer.append( '<button class=\"button llms-button-secondary\" id=\"llms-prev-question\" name=\"llms_prev_question\" type=\"submit\" style=\"display:none;\">' + LLMS.l10n.translate( 'Previous Question' ) + '</button>' );\n\n\t\t\t$header.append( '<div class=\"llms-progress\"><div class=\"progress-bar-complete\"></div></div>' );\n\t\t\t$footer.append( '<div class=\"llms-quiz-counter\" id=\"llms-quiz-counter\"><span class=\"llms-current\"></span><span class=\"llms-sep\">/</span><span class=\"llms-total\"></span></div>' )\n\n\t\t\t$html.append( $header )\n\t\t\t\t .append( '<div class=\"llms-quiz-question-wrapper\" id=\"llms-quiz-question-wrapper\" />' )\n\t\t\t\t .append( $footer );\n\n\t\t\t$( '#llms-quiz-wrapper' ).after( $html );\n\n\t\t},\n\n\t\t/**\n\t\t * Perform actions on question HTML after it's been appended to the DOM\n\t\t *\n\t\t * @param    obj      event  js event object\n\t\t * @param    obj      html   js HTML object\n\t\t * @return   void\n\t\t * @since    3.16.6\n\t\t * @version  3.16.6\n\t\t */\n\t\tpost_append_question: function( event, html ) {\n\n\t\t\tvar $html = $( html );\n\n\t\t\tif ( $html.find( 'audio' ).length ) {\n\t\t\t\twp.mediaelement.initialize();\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Show or hide the \"loading\" spinner with an option message\n\t\t *\n\t\t * @param    string   display  show|hide\n\t\t * @param    string   msg      text to display when showing\n\t\t * @return   void\n\t\t * @since    3.9.0\n\t\t * @version  3.16.6\n\t\t */\n\t\ttoggle_loader: function( display, msg ) {\n\n\t\t\tif ( 'show' === display ) {\n\n\t\t\t\tmsg = msg || LLMS.l10n.translate( 'Loading...' );\n\n\t\t\t\tthis.$buttons.attr( 'disabled', 'disabled' );\n\n\t\t\t\tthis.$container.empty();\n\t\t\t\tLLMS.Spinner.start( this.$container );\n\t\t\t\tthis.$container.append( '<div class=\"llms-quiz-loading\">' + LLMS.l10n.translate( msg ) + '</div>' );\n\n\t\t\t} else {\n\n\t\t\t\tLLMS.Spinner.stop( this.$container );\n\t\t\t\tthis.$buttons.removeAttr( 'disabled' );\n\t\t\t\tthis.$container.find( '.llms-quiz-loading' ).remove();\n\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Update the progress bar and toggle button availability based on question the question being shown.\n\t\t *\n\t\t * @since 3.16.0\n\t\t * @since 7.8.0 Show counter and set the total as when needed.\n\t\t *\n\t\t * @param {Int} qid Question ID.\n\t\t * @return {Void}\n\t\t */\n\t\tupdate_progress: function( qid ) {\n\n\t\t\tvar index = this.get_question_index( qid );\n\n\t\t\tif ( -1 === index ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tindex++;\n\n\t\t\t$( '#llms-quiz-counter .llms-current' ).text( index );\n\t\t\tif ( index > 0 && ! $( '#llms-quiz-counter .llms-total' ).text() ) {\n\t\t\t\t$( '#llms-quiz-counter .llms-total' ).text( this.total_questions );\n\t\t\t\t$( '#llms-quiz-counter' ).show();\n\t\t\t}\n\n\t\t\t// Handle prev question.\n\t\t\tif ( index >= 2 ) {\n\t\t\t\t$( '#llms-prev-question' ).show();\n\t\t\t} else {\n\t\t\t\t$( '#llms-prev-question' ).hide();\n\t\t\t}\n\n\t\t\tif ( index === this.total_questions ) {\n\t\t\t\t$( '#llms-next-question' ).hide();\n\t\t\t\t$( '#llms-complete-quiz' ).show();\n\t\t\t} else {\n\t\t\t\t$( '#llms-next-question' ).show();\n\t\t\t\t$( '#llms-complete-quiz' ).hide();\n\t\t\t}\n\n\t\t},\n\n\t\t/**\n\t\t * Increase progress bar ui element\n\t\t *\n\t\t * @param    string   dir  update direction [increment|decrement|reload]\n\t\t * @return   void\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tupdate_progress_bar: function( dir ) {\n\n\t\t\tvar index = this.get_question_index( this.current_question );\n\t\t\tswitch ( dir ) {\n\t\t\t\tcase 'increment':\n\t\t\t\t\tindex++;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'decrement':\n\t\t\t\t\tindex--;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'reload':\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tprogress = ( index / this.total_questions ) * 100;\n\t\t\tthis.$ui.find( '.progress-bar-complete' ).css( 'width', progress + '%' );\n\n\t\t},\n\n\t\t/**\n\t\t * Get Count Down\n\t\t * Called every second to update the on screen countdown timer\n\t\t * Changes color to yellow at 1/2 of total time\n\t\t * Changes color to red at 1/4 of total time\n\t\t *\n\t\t * @param  {[int]} minutes     [description]\n\t\t * @param  {[date]} target_date [description]\n\t\t * @param  {[int]} time_limit  [description]\n\t\t * @param  {[int]} days        [description]\n\t\t * @param  {[int]} hours       [description]\n\t\t * @param  {[int]} minutes     [description]\n\t\t * @param  {[int]} seconds     [description]\n\t\t * @param  {[int]} countdown   [description]\n\t\t * @return Displays updates hours, minutes on quiz timer\n\t\t * @since    1.0.0\n\t\t * @version  1.0.0\n\t\t */\n\t\tgetCountdown: function( total_minutes, target_date, time_limit, days, hours, minutes, seconds, countdown ){\n\n\t\t\t// find the amount of \"seconds\" between now and target\n\t\t\tvar current_date = new Date().getTime(),\n\t\t\t\tseconds_left = parseInt( ( target_date - current_date ) / 1000 );\n\n\t\t\tconst live = document.getElementById( 'llms-timer-live' );\n\n\t\t\tif ( seconds_left >= 0 ) {\n\n\t\t\t\tif ( ( seconds_left * 1000 ) < ( time_limit / 2 ) ) {\n\n\t\t\t\t\t$( '#llms-quiz-timer' ).addClass( 'color-half' );\n\n\t\t\t\t}\n\n\t\t\t\tif ( ( seconds_left * 1000 ) < ( time_limit / 4 ) ) {\n\n\t\t\t\t\t$( '#llms-quiz-timer' ).removeClass( 'color-half' );\n\t\t\t\t\t$( '#llms-quiz-timer' ).addClass( 'color-empty' );\n\n\t\t\t\t}\n\n\t\t\t\tconst shouldAnnounce = (s) => {\n\t\t\t\t\tif ( s === parseInt( time_limit / 1000 ) ) return true;      // first render\n\t\t\t\t\tif ( s <= 10 ) return true;          // last 10 seconds: every second\n\t\t\t\t\tif ( s <= 60 ) return s % 10 === 0; // last minute: every 10s\n\t\t\t\t\treturn s % 60 === 0;               // otherwise: each minute mark\n\t\t\t\t};\n\n\t\t\t\tconst speak = ( hours, mins, secs ) => {\n\t\t\t\t\tif ( hours > 0 ) {\n\t\t\t\t\t\t// Translators: %1$s hours, %2$s minutes, and %3$s seconds remaining.\n\t\t\t\t\t\tlive.textContent = hours > 1 ? LLMS.l10n.replace( '%1$s hours, %2$s minutes remaining', {\n\t\t\t\t\t\t\t'%1$s': hours,\n\t\t\t\t\t\t\t'%2$s': mins,\n\t\t\t\t\t\t} ) :\n\t\t\t\t\t\t// Translators: 1 hour, %2$s minutes.\n\t\t\t\t\t\tLLMS.l10n.replace( '1 hour, %2$s minutes remaining', {\n\t\t\t\t\t\t\t'%2$s': mins,\n\t\t\t\t\t\t} );\n\t\t\t\t\t} else if ( mins > 0 && secs === 0 ) {\n\t\t\t\t\t\t// Translators: %1$s minutes remaining.\n\t\t\t\t\t\tlive.textContent = mins > 1 ? LLMS.l10n.replace( '%1$s minutes remaining', {\n\t\t\t\t\t\t\t'%1$s': mins,\n\t\t\t\t\t\t} ) :\n\t\t\t\t\t\t// Translators: 1 minute remaining.\n\t\t\t\t\t\tLLMS.l10n.replace( '%1$s minute remaining', {\n\t\t\t\t\t\t\tminutes: mins,\n\t\t\t\t\t\t} );\n\t\t\t\t\t} else if ( mins === 0 ) {\n\t\t\t\t\t\tlive.textContent = LLMS.l10n.replace( '%1$s seconds remaining', {\n\t\t\t\t\t\t\t'%1$s': secs,\n\t\t\t\t\t\t} );\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tdays         = parseInt( seconds_left / 86400 );\n\t\t\t\tlet remainder = seconds_left % 86400;\n\t\t\t\thours        = parseInt( remainder / 3600 );\n\t\t\t\tremainder = seconds_left % 3600;\n\t\t\t\tminutes      = parseInt( remainder / 60 );\n\t\t\t\tseconds      = parseInt( remainder % 60 );\n\n\t\t\t\tcountdown.innerHTML = this.pad( hours ) + ':' + this.pad( minutes ) + ':' + this.pad( seconds );\n\n\t\t\t\t$( countdown ).attr( 'datetime', 'PT' + ( hours > 0 ? hours + 'H' : '' ) + ( minutes > 0 ? minutes + 'M' : '' ) + ( seconds > 0 ? seconds + 'S' : '' ) );\n\n\t\t\t\tif ( shouldAnnounce( seconds_left ) ) {\n\t\t\t\t\tspeak( hours, minutes, seconds );\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Pad Number\n\t\t * pads number with 0 if single digit.\n\t\t *\n\t\t * @param  {[int]} n [number]\n\t\t * @return {[string]} [padded number]\n\t\t * @since    1.0.0\n\t\t * @version  1.0.0\n\t\t */\n\t\tpad: function(n) {\n\t\t\treturn (n < 10 ? '0' : '') + n;\n\t\t},\n\n\t\t/**\n\t\t * Basic validation method which performs no validation and returns a validation object\n\t\t * in the format required by the application\n\t\t *\n\t\t * @param    obj   $question  jQuery selector of the question\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tvalidate: function( $question ) {\n\t\t\treturn {\n\t\t\t\tanswer: [],\n\t\t\t\tvalid: true,\n\t\t\t};\n\t\t},\n\n\t\t/**\n\t\t * Validates a choice question to ensure there's at least one checked input\n\t\t *\n\t\t * @param    obj   $question  jQuery selector of the question\n\t\t * @return   obj\n\t\t * @since    3.16.0\n\t\t * @version  3.16.0\n\t\t */\n\t\tvalidate_choice: function( $question ) {\n\n\t\t\tvar ret     = window.llms.quizzes.validate( $question ),\n\t\t\t\tchecked = $question.find( 'input:checked' );\n\n\t\t\tif ( ! checked.length ) {\n\t\t\t\tret.valid = LLMS.l10n.translate( 'You must select an answer to continue.' );\n\t\t\t} else {\n\t\t\t\tchecked.each( function() {\n\t\t\t\t\tret.answer.push( $( this ).val() );\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn ret;\n\n\t\t},\n\n\t};\n\n\tquiz.bind();\n\n\twindow.llms         = window.llms || {};\n\twindow.llms.quizzes = quiz;\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-view-manager.js",
    "content": "/**\n * JS events for the view manager\n *\n * @package LifterLMS/Scripts\n *\n * @since 3.8.0\n * @since 4.2.0 Added access plans action button selector to the list of links to update.\n *\n * @version 4.2.0\n */\n\n( function( $, undefined ) {\n\n\twindow.llms = window.llms || {};\n\n\tvar ViewManager = function() {\n\n\t\tvar currentView = 'self',\n\t\t\tcurrentNonce;\n\n\t\t/**\n\t\t * Set the current Nonce\n\t\t *\n\t\t * @param    string   nonce    a nonce\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.set_nonce = function( nonce ) {\n\t\t\tcurrentNonce = nonce;\n\t\t\treturn this;\n\t\t}\n\n\t\t/**\n\t\t * Set the current view\n\t\t *\n\t\t * @param    string   view   a view option\n\t\t * @since    3.8.0\n\t\t * @version  3.8.0\n\t\t */\n\t\tthis.set_view = function( view ) {\n\t\t\tcurrentView = view;\n\t\t\treturn this;\n\t\t}\n\n\t\t/**\n\t\t * Update various links on the page for easy navigation when using views.\n\t\t *\n\t\t * @since 3.8.0\n\t\t * @since 4.2.0 Added access plans action button selector to the list of links to update.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.update_links = function() {\n\n\t\t\tif ( 'self' === currentView || ! currentNonce ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar $links = $( '.llms-widget-syllabus .llms-lesson a, .llms-course-progress a, .llms-lesson-preview a.llms-lesson-link, .llms-parent-course-link a.llms-lesson-link, .llms-access-plans a.llms-button-action' );\n\n\t\t\t$links.each( function() {\n\n\t\t\t\tvar $link = $( this ),\n\t\t\t\t\thref  = $link.attr( 'href' ),\n\t\t\t\t\tsplit = href.split( '?' ),\n\t\t\t\t\tqs    = {};\n\n\t\t\t\tif ( split.length > 1 ) {\n\n\t\t\t\t\t$.each( split[1].split( '&' ), function( i, pair ) {\n\t\t\t\t\t\tpair          = pair.split( '=' );\n\t\t\t\t\t\tqs[ pair[0] ] = pair[1];\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t\tqs['llms-view-as'] = currentView;\n\t\t\t\tqs.view_nonce      = currentNonce;\n\n\t\t\t\t$link.attr( 'href', split[0] + '?' + $.param( qs ) );\n\n\t\t\t} );\n\n\t\t}\n\n\t};\n\n\t// initialize the object\n\twindow.llms.ViewManager = new ViewManager();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/llms-widget-syllabus.js",
    "content": "( function( $ ) {\n\n\twindow.llms = window.llms || {};\n\n\t/**\n\t * Manage\n\t *\n\t * @return obj    instance of the class\n\t * @since 2.6.0\n\t */\n\twindow.llms.widget_syllabus = function() {\n\n\t\t/**\n\t\t * Init\n\t\t *\n\t\t * @return void\n\t\t * @since 2.6.0\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\tthis.bind();\n\n\t\t};\n\n\t\t/**\n\t\t * Bind DOM events\n\t\t *\n\t\t * @return void\n\t\t * @since 2.6.0\n\t\t */\n\t\tthis.bind = function() {\n\n\t\t\tvar self = this;\n\n\t\t\t// bind all existing toggles on load\n\t\t\tself.bind_toggles( $( '#widgets-right .llms-course-outline-collapse' ) );\n\n\t\t\t$( document ).on( 'ajaxStop', function( r ) {\n\n\t\t\t\t// self.toggle( $( this ) );\n\t\t\t\t$( '#widgets-right .llms-course-outline-collapse:not([data-is-bound=\"true\"])' ).each( function() {\n\n\t\t\t\t\tself.bind_toggles( $( this ) );\n\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind change events to a specific toggle or set of toggles\n\t\t *\n\t\t * @param  obj      $toggles   jQuery selector of toggle input ('input.llms-course-outline-collapse')\n\t\t * @return void\n\t\t * @since 2.6.0\n\t\t */\n\t\tthis.bind_toggles = function( $toggles ) {\n\n\t\t\tvar self = this;\n\n\t\t\t$toggles.attr( 'data-is-bound', 'true' );\n\n\t\t\t// bind input change on load\n\t\t\t$toggles.on( 'change', function() {\n\n\t\t\t\tself.toggle( $( this ) );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Toggle the visibility of the secondary option to display toggles\n\t\t *\n\t\t * @param  obj      $input   jQuery selector of a single collapse toggle element ('input.llms-course-outline-collapse')\n\t\t * @return void\n\t\t * @since 2.6.0\n\t\t */\n\t\tthis.toggle = function( $input ) {\n\n\t\t\t$input.closest( '.widget' ).find( '.llms-course-outline-toggle-wrapper' ).toggle();\n\n\t\t};\n\n\t\t// GO\n\t\tthis.init();\n\n\t\t// whatever\n\t\treturn this;\n\n\t};\n\n\tvar a = new window.llms.widget_syllabus();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/partials/_metabox-field-repeater.js",
    "content": "/**\n * LifterLMS Admin Metabox Repeater Field\n *\n * @package LifterLMS/Scripts/Partials\n *\n * @since 3.11.0\n * @version 5.3.2\n */\n\nthis.repeaters = {\n\n\t/**\n\t * Reference to the parent metabox class\n\t *\n\t * @type {Object}\n\t */\n\tmetaboxes: this,\n\n\t/**\n\t * A jQuery selector for all repeater elements on the current screen\n\t *\n\t * @type {Object}\n\t */\n\t$repeaters: null,\n\n\t/**\n\t * Init\n\t *\n\t * @since 3.11.0\n\t * @since 3.23.0 Unknown.\n\t *\n\t * @return {void}\n\t */\n\tinit: function() {\n\n\t\tvar self = this;\n\n\t\tself.$repeaters = $( '.llms-mb-list.repeater' );\n\n\t\tif ( self.$repeaters.length ) {\n\n\t\t\t// Wait for tinyMCE just in case their editors in the repeaters.\n\t\t\tLLMS.wait_for(\n\t\t\t\tfunction() {\n\t\t\t\t\treturn ( 'undefined' !== typeof tinyMCE );\n\t\t\t\t},\n\t\t\t\tfunction() {\n\t\t\t\t\tself.load();\n\t\t\t\t\tself.bind();\n\t\t\t\t}\n\t\t\t);\n\n\t\t\t/**\n\t\t\t * On click of any post submit buttons add some data to the submit button\n\t\t\t * so we can see which button to trigger after repeaters are finished.\n\t\t\t */\n\t\t\t$( '#post input[type=\"submit\"], #post-preview' ).on( 'click', function() {\n\t\t\t\t$( this ).attr( 'data-llms-clicked', 'yes' );\n\t\t\t} );\n\n\t\t\t// Handle post submission.\n\t\t\t$( '#post' ).on( 'submit', self.handle_submit );\n\n\t\t}\n\n\t},\n\n\t/**\n\t * Bind DOM Events\n\t *\n\t * @since 3.11.0\n\t * @since 3.13.0 Unknown.\n\t * @since 5.3.2 Don't remove the model's mceEditor instance (it's removed before cloning a row now).\n\t *\n\t * @return {void}\n\t */\n\tbind: function() {\n\n\t\tvar self = this;\n\n\t\tself.$repeaters.each( function() {\n\n\t\t\tvar $repeater = $( this ),\n\t\t\t\t$rows     = $repeater.find( '.llms-repeater-rows' );\n\n\t\t\t// For the repeater + button.\n\t\t\t$repeater.find( '.llms-repeater-new-btn' ).on( 'click', function() {\n\t\t\t\tself.add_row( $repeater, null, true );\n\t\t\t} );\n\n\t\t\t// Make repeater rows sortable.\n\t\t\t$rows.sortable( {\n\t\t\t\thandle: '.llms-drag-handle',\n\t\t\t\titems: '.llms-repeater-row',\n\t\t\t\tstart: function( event, ui ) {\n\t\t\t\t\t$rows.addClass( 'dragging' );\n\t\t\t\t},\n\t\t\t\tstop: function( event, ui ) {\n\t\t\t\t\t$rows.removeClass( 'dragging' );\n\n\t\t\t\t\tvar $eds = ui.item.find( 'textarea.wp-editor-area' );\n\t\t\t\t\t$eds.each( function() {\n\t\t\t\t\t\tvar ed_id = $( this ).attr( 'id' );\n\t\t\t\t\t\ttinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, ed_id );\n\t\t\t\t\t\ttinyMCE.EditorManager.execCommand( 'mceAddEditor', true, ed_id );\n\t\t\t\t\t} );\n\n\t\t\t\t\tself.save( $repeater );\n\t\t\t\t},\n\t\t\t} );\n\n\t\t\t$repeater.on( 'click', '.llms-repeater-remove', function( e ) {\n\t\t\t\te.stopPropagation();\n\t\t\t\tvar $row = $( this ).closest( '.llms-repeater-row' );\n\t\t\t\tif ( window.confirm( LLMS.l10n.translate( 'Are you sure you want to delete this row? This cannot be undone.' ) ) ) {\n\t\t\t\t\t$row.remove();\n\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\tself.save( $repeater );\n\t\t\t\t\t}, 1 );\n\t\t\t\t}\n\t\t\t} );\n\n\t\t} );\n\n\t},\n\n\t/**\n\t * Add a new row to a repeater rows group\n\t *\n\t * @since 3.11.0\n\t * @since 5.3.2 Use `self.clone_row()` to retrieve the model's base HTML for the row to be added.\n\t *\n\t * @param {Object}  $repeater A jQuery selector for the repeater to add a row to.\n\t * @param {Object}  data      Optional object of data to fill fields in the row with.\n\t * @param {Boolean} expand    If true, will automatically open the row after adding it to the dom.\n\t * @return {void}\n\t */\n\tadd_row: function( $repeater, data, expand ) {\n\n\t\tvar self      = this,\n\t\t\t$rows     = $repeater.find( '.llms-repeater-rows' ),\n\t\t\t$model    = $repeater.find( '.llms-repeater-model' ),\n\t\t\t$row      = self.clone_row( $model.find( '.llms-repeater-row' ) ),\n\t\t\tnew_index = $repeater.find( '.llms-repeater-row' ).length,\n\t\t\teditor    = self.reindex( $row, new_index );\n\n\t\tif ( data ) {\n\t\t\t$.each( data, function( key, val ) {\n\n\t\t\t\tvar $field = $row.find( '[name^=\"' + key + '\"]' );\n\n\t\t\t\tif ( $field.hasClass( 'llms-select2-student' ) ) {\n\t\t\t\t\t$.each( val, function( i, data ) {\n\t\t\t\t\t\t$field.append( '<option value=\"' + data.key + '\" selected=\"selected\">' + data.title + '</option>' )\n\t\t\t\t\t} );\n\t\t\t\t\t$field.trigger( 'change' );\n\t\t\t\t} else {\n\t\t\t\t\t$field.val( val );\n\t\t\t\t}\n\n\t\t\t} );\n\t\t}\n\n\t\tsetTimeout( function() {\n\t\t\tself.bind_row( $row );\n\t\t}, 1 );\n\n\t\t$rows.append( $row );\n\t\tif ( expand ) {\n\t\t\t$row.find( '.llms-collapsible-header' ).trigger( 'click' );\n\t\t}\n\t\ttinyMCE.EditorManager.execCommand( 'mceAddEditor', true, editor );\n\n\t\t$repeater.trigger( 'llms-new-repeater-row', {\n\t\t\t$row: $row,\n\t\t\tdata: data,\n\t\t} );\n\n\t},\n\n\t/**\n\t * Bind DOM events for a single repeater row\n\t *\n\t * @since 3.11.0\n\t * @since 3.13.0 Unknown.\n\t *\n\t * @param {Object} $row A jQuery selector for the row.\n\t * @return {void}\n\t */\n\tbind_row: function( $row ) {\n\n\t\tthis.bind_row_header( $row );\n\n\t\t$row.find( '.llms-select2' ).llmsSelect2( {\n\t\t\twidth: '100%',\n\t\t} );\n\n\t\t$row.find( '.llms-select2-student' ).llmsStudentsSelect2();\n\n\t\tthis.metaboxes.bind_datepickers( $row.find( '.llms-datepicker' ) );\n\t\tthis.metaboxes.bind_controllers( $row.find( '[data-is-controller]' ) );\n\t\t// This.metaboxes.bind_merge_code_buttons( $row.find( '.llms-merge-code-wrapper' ) );.\n\t},\n\n\t/**\n\t * Bind row header events\n\t *\n\t * @since 3.11.0\n\t *\n\t * @param {Object} $row jQuery selector for the row.\n\t * @return {void}\n\t */\n\tbind_row_header: function( $row ) {\n\n\t\t// Handle the title field binding.\n\t\tvar $title = $row.find( '.llms-repeater-title' ),\n\t\t\t$field = $row.find( '.llms-collapsible-header-title-field' );\n\n\t\t$title.attr( 'data-default', $title.text() );\n\n\t\t$field.on( 'keyup focusout blur', function() {\n\t\t\tvar val = $( this ).val();\n\t\t\tif ( ! val ) {\n\t\t\t\tval = $title.attr( 'data-default' );\n\t\t\t}\n\t\t\t$title.text( val );\n\t\t} ).trigger( 'keyup' );\n\n\t},\n\n\t/**\n\t * Create a copy of the model's row after removing any tinyMCE editor instances present in the model.\n\t *\n\t * @since 5.3.2\n\t *\n\t * @param {Object} $row A jQuery object of the row to be cloned.\n\t * @return {Object} A clone of the jQuery object.\n\t */\n\tclone_row: function( $row ) {\n\n\t\t$ed = $row.find( '.editor textarea' );\n\t\tif ( $ed.length ) {\n\t\t\ttinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, $ed.attr( 'id' ) );\n\t\t}\n\n\t\treturn $row.clone()\n\n\t},\n\n\t/**\n\t * Handle WP Post form submission to ensure repeaters are saved before submitting the form to save/publish the post\n\t *\n\t * @since 3.11.0\n\t * @since 3.23.0 Unknown.\n\t *\n\t * @param {Object} e An event object.\n\t * @return {void}\n\t */\n\thandle_submit: function( e ) {\n\n\t\t// Get the button used to submit the form.\n\t\tvar $btn     = $( '#post [data-llms-clicked=\"yes\"]' ),\n\t\t\t$spinner = $btn.parent().find( '.spinner' );\n\n\t\tif ( $btn.is( '#post-preview' ) ) {\n\t\t\t$btn.removeAttr( 'data-llms-clicked' );\n\t\t\treturn;\n\t\t}\n\n\t\te.preventDefault();\n\n\t\t// Core UX to prevent multi-click/or the appearance of a delay.\n\t\t$( '#post input[type=\"submit\"]' ).addClass( 'disabled' ).attr( 'disabled', 'disabled' );\n\t\t$spinner.addClass( 'is-active' );\n\n\t\tvar self = window.llms.metaboxes.repeaters,\n\t\t\ti    = 0,\n\t\t\twait;\n\n\t\tself.$repeaters.each( function() {\n\t\t\tself.save( $( this ) );\n\t\t} );\n\n\t\twait = setInterval( function() {\n\n\t\t\tif ( i >= 59 || ! $( '.llms-mb-list.repeater.processing' ).length ) {\n\n\t\t\t\tclearInterval( wait );\n\t\t\t\t$( '#post' ).off( 'submit', this.handle_submit );\n\t\t\t\t$spinner.removeClass( 'is-active' );\n\t\t\t\t$btn.removeClass( 'disabled' ).removeAttr( 'disabled' ).trigger( 'click' );\n\n\t\t\t} else {\n\n\t\t\t\ti++;\n\n\t\t\t}\n\n\t\t}, 1000 );\n\n\t},\n\n\t/**\n\t * Load repeater data from the server and create rows in the DOM\n\t *\n\t * @since 3.11.0\n\t * @since 3.12.1 Unknown.\n\t *\n\t * @return {void}\n\t */\n\tload: function() {\n\n\t\tvar self = this;\n\n\t\tself.$repeaters.each( function() {\n\n\t\t\tvar $repeater = $( this );\n\n\t\t\t// Ensure the repeater is only loaded once to prevent duplicates resulting from duplicating binding.\n\t\t\t// On certain sites which I cannot quite explain...\n\t\t\tif ( $repeater.hasClass( 'is-loaded' ) || $repeater.hasClass( 'processing' ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tself.store( $repeater, 'load', function( data ) {\n\n\t\t\t\t$repeater.addClass( 'is-loaded' );\n\n\t\t\t\t$.each( data.data, function( i, obj ) {\n\t\t\t\t\tself.add_row( $repeater, obj, false );\n\t\t\t\t} );\n\n\t\t\t\t// For each row within the repeater.\n\t\t\t\t$repeater.find( '.llms-repeater-rows .llms-repeater-row' ).each( function() {\n\t\t\t\t\tself.bind_row( $( this ) );\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t} );\n\n\t},\n\n\t/**\n\t * Reindex a row\n\t *\n\t * Renames ids, attrs, and etc...\n\t *\n\t * Used when cloning the model for new rows.\n\t *\n\t * @since 3.11.0\n\t *\n\t * @param {Object} $row  jQuery selector for the row.\n\t * @param {string} index The index (or id) to use when renaming.\n\t * @return {string}\n\t */\n\treindex: function( $row, index ) {\n\n\t\tvar old_index = $row.attr( 'data-row-order' ),\n\t\t\t$ed       = $row.find( '.llms-mb-list.editor textarea' );\n\n\t\ttinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, $ed.attr( 'id' ) );\n\n\t\tfunction replace_attr( $el, attr ) {\n\t\t\t$el.each( function() {\n\t\t\t\tvar str = $( this ).attr( attr );\n\t\t\t\t$( this ).attr( attr, str.replace( old_index, index ) );\n\t\t\t} );\n\t\t};\n\n\t\t$row.attr( 'data-row-order', index );\n\n\t\treplace_attr( $row, 'data-row-order' );\n\n\t\treplace_attr( $row.find( 'button.insert-media' ), 'data-editor' );\n\n\t\treplace_attr( $row.find( 'input[name^=\"_llms\"], textarea[name^=\"_llms\"], select[name^=\"_llms\"]' ), 'id' );\n\t\treplace_attr( $row.find( 'input[name^=\"_llms\"], textarea[name^=\"_llms\"], select[name^=\"_llms\"]' ), 'name' );\n\t\treplace_attr( $row.find( '[data-controller]' ), 'data-controller' );\n\t\treplace_attr( $row.find( '[data-controller]' ), 'data-controller' );\n\t\treplace_attr( $row.find( 'button.wp-switch-editor' ), 'data-wp-editor-id' );\n\t\treplace_attr( $row.find( 'button.wp-switch-editor' ), 'id' );\n\t\treplace_attr( $row.find( '.wp-editor-tools' ), 'id' );\n\t\treplace_attr( $row.find( '.wp-editor-container' ), 'id' );\n\n\t\treturn $ed.attr( 'id' );\n\n\t},\n\n\t/**\n\t * Save a single repeaters data to the server\n\t *\n\t * @since 3.11.0\n\t * @since 3.13.0 Unknown.\n\t *\n\t * @param {Object} $repeater jQuery selector for a repeater element.\n\t * @return {void}\n\t */\n\tsave: function( $repeater ) {\n\t\t$repeater.trigger( 'llms-repeater-before-save', { $el: $repeater } );\n\t\tthis.store( $repeater, 'save' );\n\t},\n\n\t/**\n\t * Convert a repeater element into an array of objects that can be saved to the database\n\t *\n\t * @since 3.11.0\n\t *\n\t * @param {Object} $repeater A jQuery selector for a repeater element.\n\t * @return {void}\n\t */\n\tserialize: function( $repeater ) {\n\n\t\tvar rows = [];\n\n\t\t$repeater.find( '.llms-repeater-rows .llms-repeater-row' ).each( function() {\n\n\t\t\tvar obj = {};\n\n\t\t\t// Easy...\n\t\t\t$( this ).find( 'input[name^=\"_llms\"], select[name^=\"_llms\"]' ).each( function() {\n\t\t\t\tobj[ $( this ).attr( 'name' ) ] = $( this ).val();\n\t\t\t} );\n\n\t\t\t// Check if the textarea is a tinyMCE instance.\n\t\t\t$( this ).find( 'textarea[name^=\"_llms\"]' ).each( function() {\n\n\t\t\t\tvar name = $( this ).attr( 'name' );\n\n\t\t\t\t// If it is an editor.\n\t\t\t\tif ( tinyMCE.editors[ name ] ) {\n\t\t\t\t\tobj[ name ] = tinyMCE.editors[ name ].getContent();\n\t\t\t\t\t// Grab the val of the textarea.\n\t\t\t\t} else {\n\t\t\t\t\tobj[ name ] = $( this ).val();\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\trows.push( obj );\n\n\t\t} );\n\n\t\treturn rows;\n\n\t},\n\n\t/**\n\t * AJAX method for interacting with the repeater's handler on the server\n\t *\n\t * @since 3.11.0\n\t *\n\t * @param {Object}   $repeater jQuery selector for the repeater element.\n\t * @param {string}   action    Action to call [save|load].\n\t * @param {Function} cb        Callback function.\n\t * @return {void}\n\t */\n\tstore: function( $repeater, action, cb ) {\n\n\t\tcb       = cb || function(){};\n\t\tvar self = this,\n\t\t\tdata = {\n\t\t\t\taction: $repeater.find( '.llms-repeater-field-handler' ).val(),\n\t\t\t\tstore_action: action,\n\t\t};\n\n\t\tif ( 'save' === action ) {\n\t\t\tdata.rows = self.serialize( $repeater );\n\t\t}\n\n\t\tLLMS.Ajax.call( {\n\t\t\tdata: data,\n\t\t\tbeforeSend: function() {\n\n\t\t\t\t$repeater.addClass( 'processing' );\n\t\t\t\tLLMS.Spinner.start( $repeater );\n\n\t\t\t},\n\t\t\tsuccess: function( r ) {\n\n\t\t\t\tcb( r );\n\t\t\t\tLLMS.Spinner.stop( $repeater );\n\t\t\t\t$repeater.removeClass( 'processing' );\n\n\t\t\t}\n\n\t\t} );\n\n\t}\n\n};\nthis.repeaters.init();\n"
  },
  {
    "path": "assets/js/private/llms-metaboxes.js",
    "content": "/**\n * LifterLMS Admin Panel Metabox Functions\n *\n * @since 3.0.0\n * @version 7.1.1\n */\n ( function( $ ) {\n\n\t $( document ).ready( function() {\n\t\t // Avoid confusion by hiding the visibility option for coupons and vouchers if currently set to public.\n\t\t if ( $( 'input[name=\"visibility\"]:checked' ).val() === 'public' ) {\n\t\t\t $( 'body.post-type-llms_coupon #visibility, body.post-type-llms_voucher #visibility' ).hide();\n\t\t }\n\t } );\n\n\t const collapseHeaderClicked = function() {\n\t\t var $parent = $(this).closest('.llms-collapsible'),\n\t\t\t $siblings = $parent.siblings('.llms-collapsible');\n\n\t\t $parent.toggleClass('opened').trigger('llms-collapsible-toggled');\n\n\t\t $parent.find('.llms-collapsible-body').slideToggle(400);\n\n\t\t $siblings.each(function () {\n\t\t\t $(this).removeClass('opened');\n\t\t\t $(this).find('.llms-collapsible-body').slideUp(400);\n\t\t });\n\t }\n\n\t /**\n\t * jQuery plugin to allow \"collapsible\" sections\n\t *\n\t * @return  jQuery object\n\t * @since   3.0.0\n\t * @version 3.29.0\n\t */\n\t$.fn.llmsCollapsible = function() {\n\n\t\tvar $group = this;\n\n\t\tthis.off( 'click', '.llms-collapsible-header', collapseHeaderClicked )\n\t\t\t.on( 'click', '.llms-collapsible-header', collapseHeaderClicked );\n\n\t\treturn this;\n\n\t};\n\n\twindow.llms = window.llms || {};\n\n\tvar Metaboxes = function() {\n\n\t\t/**\n\t\t * load all partials\n\t\t */\n\t\t// = include ../partials/*.js\n\n\t\t/**\n\t\t * Initialize\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.13.0 Unknown.\n\t\t * @since 4.19.0 Add `this.bind_mce_fixes()`.\n\t\t * @since 5.3.0 Bind editables when editable buttons are present in addition to anchors.\n\t\t *\n\t\t * @return   void\n\t\t */\n\t\tthis.init = function() {\n\n\t\t\tvar self = this;\n\n\t\t\t$( '.llms-select2-post' ).each( function() {\n\t\t\t\tself.post_select( $( this ) );\n\t\t\t} );\n\n\t\t\t$( '.llms-collapsible-group' ).llmsCollapsible();\n\n\t\t\tthis.bind_tabs();\n\n\t\t\tthis.bind_mce_fixes();\n\n\t\t\t// bind everything better and less repetitively...\n\t\t\tvar bindings = [\n\t\t\t\t{\n\t\t\t\t\tselector: $( '.llms-datepicker' ),\n\t\t\t\t\tfunc: 'bind_datepickers',\n\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: $( '.llms-select2' ),\n\t\t\t\t\tfunc: function( $selector ) {\n\t\t\t\t\t\t$selector.llmsSelect2( {\n\t\t\t\t\t\t\twidth: '100%',\n\t\t\t\t\t\t} );\n\t\t\t\t\t},\n\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: $( '.llms-select2-student' ),\n\t\t\t\t\tfunc: function( $selector ) {\n\t\t\t\t\t\t$selector.llmsStudentsSelect2();\n\t\t\t\t\t}\n\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: $( 'input[type=\"checkbox\"][data-controls]' ),\n\t\t\t\t\tfunc: 'bind_cb_controllers',\n\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: $( '[data-is-controller]' ),\n\t\t\t\t\tfunc: 'bind_controllers',\n\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: $( '.llms-table' ),\n\t\t\t\t\tfunc: 'bind_tables',\n\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: $( '.llms-merge-code-wrapper' ),\n\t\t\t\t\tfunc: 'bind_merge_code_buttons',\n\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tselector: $( 'a.llms-editable, button.llms-editable' ),\n\t\t\t\t\tfunc: 'bind_editables',\n\t\t\t},\n\t\t\t];\n\n\t\t\t// bind all the bindables but don't bind things in repeaters\n\t\t\t$.each( bindings, function( index, obj ) {\n\n\t\t\t\tif ( obj.selector.length ) {\n\n\t\t\t\t\t// reduce the selector to exclude items in a repeater\n\t\t\t\t\tvar reduced = obj.selector.filter( function() {\n\t\t\t\t\t\treturn ( 0 === $( this ).closest( '.llms-repeater-model' ).length );\n\t\t\t\t\t} );\n\n\t\t\t\t\t// bind by string\n\t\t\t\t\tif ( 'string' === typeof obj.func ) {\n\t\t\t\t\t\tself[ obj.func ]( reduced );\n\t\t\t\t\t}\n\t\t\t\t\t// bind by an anonymous function\n\t\t\t\t\telse if ( 'function' === typeof obj.func ) {\n\t\t\t\t\t\tobj.func( reduced );\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t// if a post type is set & a bind exists for it, bind it\n\t\t\tif ( window.llms.post.post_type ) {\n\n\t\t\t\tvar func = 'bind_' + window.llms.post.post_type;\n\n\t\t\t\tif ( 'function' === typeof this[func] ) {\n\n\t\t\t\t\tthis[func]();\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Bind checkboxes that control the display of other elements\n\t\t *\n\t\t * @param    obj   $controllers  jQuery selector for checkboxes to be bound as checkbox controllers\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.11.0\n\t\t */\n\t\tthis.bind_cb_controllers = function( $controllers ) {\n\n\t\t\t$controllers = $controllers || $( 'input[type=\"checkbox\"][data-controls]' );\n\n\t\t\t$controllers.each( function() {\n\n\t\t\t\tvar $cb         = $( this ),\n\t\t\t\t\t$controlled = $( $cb.attr( 'data-controls' ) ).closest( '.llms-mb-list' );\n\n\t\t\t\t$cb.on( 'change', function() {\n\n\t\t\t\t\tif ( $( this ).is( ':checked' ) ) {\n\n\t\t\t\t\t\t$controlled.slideDown( 200 );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t$controlled.slideUp( 200 );\n\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t\t$cb.trigger( 'change' );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind elements that control the display of other elements\n\t\t *\n\t\t * @param    obj   $controllers  jQuery selector for elements to be bound as checkbox controllers\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.11.0\n\t\t */\n\t\tthis.bind_controllers = function( $controllers ) {\n\n\t\t\t$controllers = $controllers || $( '[data-is-controller]' );\n\n\t\t\t$controllers.each( function() {\n\n\t\t\t\tvar $el         = $( this ),\n\t\t\t\t\t$controlled = $( '[data-controller=\"#' + $el.attr( 'id' ) + '\"]' ),\n\t\t\t\t\tval;\n\n\t\t\t\t$el.on( 'change', function() {\n\n\t\t\t\t\tif ( 'checkbox' === $el.attr( 'type' ) ) {\n\n\t\t\t\t\t\tval = $el.is( ':checked' ) ? $el.val() : 'false';\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tval = $el.val();\n\n\t\t\t\t\t}\n\n\t\t\t\t\t$controlled.each( function() {\n\n\t\t\t\t\t\tvar possible = $( this ).attr( 'data-controller-value' ),\n\t\t\t\t\t\t\tvals     = [];\n\n\t\t\t\t\t\tif ( -1 !== possible.indexOf( ',' ) ) {\n\n\t\t\t\t\t\t\tvals = possible.split( ',' );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\tvals.push( possible );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( -1 !== vals.indexOf( val ) ) {\n\n\t\t\t\t\t\t\t$( this ).slideDown( 200 );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t$( this ).slideUp( 200 );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} );\n\n\t\t\t\t} );\n\n\t\t\t\t$el.trigger( 'change' );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind a single datepicker element\n\t\t *\n\t\t * @param    obj   $el  jQuery selector for the input to bind the datepicker to\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.10.0\n\t\t */\n\t\tthis.bind_datepicker = function( $el ) {\n\t\t\tvar format  = $el.attr( 'data-format' ) || 'mm/dd/yy',\n\t\t\t\taltFormat = $el.attr( 'data-alt-format' ) || '',\n\t\t\t\taltField = $el.attr( 'data-alt-field' ) || '',\n\t\t\t\tmaxDate = $el.attr( 'data-max-date' ) || null,\n\t\t\t\tminDate = $el.attr( 'data-min-date' ) || null;\n\t\t\t$el.datepicker( {\n\t\t\t\tdateFormat: format,\n\t\t\t\tmaxDate: maxDate,\n\t\t\t\tminDate: minDate,\n\t\t\t\taltField: altField,\n\t\t\t\taltFormat: altFormat\n\t\t\t} ).on( \"keyup\", function() {\n\t\t\t\tvar date;\n\t\t\t\ttry {\n\t\t\t\t\tdate = $.datepicker.parseDate( $.datepicker._defaults.dateFormat, this.value );\n\t\t\t\t} catch ( e ) { }\n\n\t\t\t\tif ( !date && altField.length > 0 ) {\n\t\t\t\t\tif ( /^#[A-Za-z0-9\\-_]+$/.test( altField ) ) {\n\t\t\t\t\t\t$( altField ).val( \"\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\t/**\n\t\t * Bind all LifterLMS datepickers\n\t\t *\n\t\t * @param    obj   $datepickers  jQuery selector for the elements to bind\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.11.0\n\t\t */\n\t\tthis.bind_datepickers = function( $datepickers ) {\n\n\t\t\tvar self = this;\n\n\t\t\t$datepickers = $datepickers || $( '.llms-datepicker' );\n\n\t\t\t$datepickers.each( function() {\n\t\t\t\tself.bind_datepicker( $( this ) );\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind llms-editable metabox fields and related dom interactions\n\t\t *\n\t\t * @since 3.10.0\n\t\t * @since 3.28.0 Unknown.\n\t\t * @since 5.3.0 Bind editables when editable buttons are present in addition to anchors.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.bind_editables = function() {\n\n\t\t\tvar self = this;\n\n\t\t\tfunction make_editable( $field ) {\n\n\t\t\t\tvar $label   = $field.find( 'label' ).clone(),\n\t\t\t\t\tname     = $field.attr( 'data-llms-editable' ),\n\t\t\t\t\ttype     = $field.attr( 'data-llms-editable-type' ),\n\t\t\t\t\trequired = $field.attr( 'data-llms-editable-required' ) || 'no',\n\t\t\t\t\tval      = $field.attr( 'data-llms-editable-value' ),\n\t\t\t\t\t$input;\n\n\t\t\t\trequired = ( 'yes' === required ) ? ' required=\"required\"' : '';\n\n\t\t\t\tif ( 'select' === type ) {\n\n\t\t\t\t\tvar options = JSON.parse( $field.attr( 'data-llms-editable-options' ) ),\n\t\t\t\t\t\tselected;\n\n\t\t\t\t\t$input = $( '<select name=\"' + name + '\"' + required + ' />' );\n\t\t\t\t\tfor ( var key in options ) {\n\t\t\t\t\t\tselected = val === key ? ' selected=\"selected\"' : '';\n\t\t\t\t\t\t$input.append( '<option value=\"' + key + '\"' + selected + '>' + options[ key ] + '</option>' );\n\t\t\t\t\t}\n\n\t\t\t\t} else if ( 'datetime' === type ) {\n\n\t\t\t\t\t$input = $( '<div class=\"llms-datetime-field\" />' );\n\n\t\t\t\t\tval          = JSON.parse( val );\n\t\t\t\t\tvar format   = $field.attr( 'data-llms-editable-date-format' ) || '',\n\t\t\t\t\t\tmin_date = $field.attr( 'data-llms-editable-date-min' ) || '',\n\t\t\t\t\t\tmax_date = $field.attr( 'data-llms-editable-date-max' ) || '';\n\n\t\t\t\t\t$picker = $( '<input class=\"llms-date-input llms-datepicker\" data-format=\"' + format + '\" data-max-date=\"' + max_date + '\" data-min-date=\"' + min_date + '\" name=\"' + name + '[date]\" type=\"text\" value=\"' + val.date + '\">' );\n\t\t\t\t\tself.bind_datepicker( $picker );\n\t\t\t\t\t$input.append( $picker );\n\t\t\t\t\t$input.append( '<em>@</em>' );\n\n\t\t\t\t\t$input.append( '<input class=\"llms-time-input\" max=\"23\" min=\"0\" name=\"' + name + '[hour]\" type=\"number\" value=\"' + val.hour + '\">' );\n\t\t\t\t\t$input.append( '<em>:</em>' );\n\t\t\t\t\t$input.append( '<input class=\"llms-time-input\" max=\"59\" min=\"0\" name=\"' + name + '[minute]\" type=\"number\" value=\"' + val.minute + '\">' );\n\n\t\t\t\t} else if ( 'price' === type ) {\n\n\t\t\t\t\t$input = $( '<input>' )\n\t\t\t\t\t\t.attr('name', name)\n\t\t\t\t\t\t.attr('type', 'number')\n\t\t\t\t\t\t.attr('min', '0')\n\t\t\t\t\t\t.attr('step', 'any')\n\t\t\t\t\t\t.attr('value', val);\n\t\t\t\t\tif (required) {\n\t\t\t\t\t\t$input.attr('required', 'required');\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\t\t\t\t\t$input = $( '<input>' )\n\t\t\t\t\t\t.attr('name', name)\n\t\t\t\t\t\t.attr('type', type)\n\t\t\t\t\t\t.attr('value', val);\n\t\t\t\t\tif (required) {\n\t\t\t\t\t\t$input.attr('required', 'required');\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$field.empty().append( $label ).append( $input );\n\t\t\t\tif ( 'select' === type ) {\n\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t$input.trigger( 'change' );\n\t\t\t\t\t}, 100 );\n\t\t\t\t}\n\n\t\t\t};\n\n\t\t\t$( 'a.llms-editable, button.llms-editable' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\tvar $btn = $( this ),\n\t\t\t\t\t$fields;\n\n\t\t\t\tif ( $btn.attr( 'data-fields' ) ) {\n\t\t\t\t\t$fields = $( $btn.attr( 'data-fields' ) );\n\t\t\t\t} else {\n\t\t\t\t\t$fields = $btn.closest( '.llms-metabox-section' ).find( '[data-llms-editable]' );\n\t\t\t\t}\n\n\t\t\t\t$btn.remove();\n\n\t\t\t\t$fields.each( function() {\n\t\t\t\t\tmake_editable( $( this ) );\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind Engagement post type JS\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.1.0\n\t\t * @version  3.1.0\n\t\t */\n\t\tthis.bind_llms_engagement = function() {\n\n\t\t\tvar self = this;\n\n\t\t\t// when the engagement type changes we need to do some things to the UI\n\t\t\t$( '#_llms_engagement_type' ).on( 'change', function() {\n\n\t\t\t\t$( '#_llms_engagement' ).trigger( 'llms-engagement-type-change', $( this ).val() );\n\n\t\t\t} );\n\n\t\t\t// custom trigger when called when the engagement type changes\n\t\t\t$( '#_llms_engagement' ).on( 'llms-engagement-type-change', function( e, engagement_type ) {\n\n\t\t\t\tvar $select = $( this );\n\n\t\t\t\tswitch ( engagement_type ) {\n\n\t\t\t\t\t/**\n\t\t\t\t\t * core engagements related to a CPT\n\t\t\t\t\t */\n\t\t\t\t\tcase 'achievement':\n\t\t\t\t\tcase 'certificate':\n\t\t\t\t\tcase 'email':\n\n\t\t\t\t\t\tvar cpt = 'llms_' + engagement_type;\n\n\t\t\t\t\t\t$select.val( null ).attr( 'data-post-type', cpt ).trigger( 'change' );\n\t\t\t\t\t\tself.post_select( $select );\n\n\t\t\t\t\tbreak;\n\n\t\t\t\t\t/**\n\t\t\t\t\t * Allow other plugins and developers to hook into the engagement type change action\n\t\t\t\t\t */\n\t\t\t\t\tdefault:\n\n\t\t\t\t\t\t$select.trigger( 'llms-engagement-type-change-external', engagement_type );\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\tthis.bind_course = function() {\n\t\t\tconst canEditPost =\n\t\t\t\twp?.data &&\n\t\t\t\ttypeof wp.data.dispatch === 'function' &&\n\t\t\t\twp.data.dispatch( 'core/editor' ) !== null &&\n\t\t\t\ttypeof wp.data.dispatch( 'core/editor' ).editPost === 'function';\n\n\t\t\tif ( ! canEditPost ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar $course_length = $( '#_llms_length' );\n\t\t\t$course_length.on( 'input change', function( e ) {\n\t\t\t\twp.data.dispatch( 'core/editor' ).editPost( {\n\t\t\t\t\tmeta: { _llms_length: e.target.value }\n\t\t\t\t} );\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Actions for memberships\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.30.0 Made autoenroll table sortable, added AJAX save for adding new courses.\n\t\t * @version 3.30.0\n\t\t *\n\t\t * @return   void\n\t\t */\n\t\tthis.bind_llms_membership = function() {\n\n\t\t\tvar $table = $( '.llms-mb-list._llms_content_table' );\n\n\t\t\t/**\n\t\t\t * Hide/Show empty message header row depending on the number of rows in the tbody\n\t\t\t *\n\t\t\t * @since 3.30.0\n\t\t\t * @version 3.30.0\n\t\t\t *\n\t\t\t * @return void\n\t\t\t */\n\t\t\tfunction toggle_header_row() {\n\n\t\t\t\tvar $rows = $table.find( 'tbody tr' );\n\t\t\t\tif ( 1 === $rows.length ) {\n\t\t\t\t\t$rows.first().show();\n\t\t\t\t} else {\n\t\t\t\t\t$rows.first().hide();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Retrieve an array of course IDs in the table.\n\t\t\t *\n\t\t\t * @since 3.30.0\n\t\t\t * @version 3.30.0\n\t\t\t *\n\t\t\t * @return array\n\t\t\t */\n\t\t\tfunction get_course_ids() {\n\n\t\t\t\tvar courses = [];\n\t\t\t\t$table.find( 'tbody tr a[href=\"#llms-course-remove\"]' ).each( function() {\n\t\t\t\t\tcourses.push( $( this ).attr( 'data-id' ) );\n\t\t\t\t} );\n\t\t\t\treturn courses;\n\n\t\t\t}\n\n\t\t\t// On init, toggle the header row visibility.\n\t\t\ttoggle_header_row();\n\n\t\t\t// remove auto-enroll course\n\t\t\t$table.on( 'click', 'a[href=\"#llms-course-remove\"]', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\tvar $el        = $( this ),\n\t\t\t\t\t$row       = $el.closest( 'tr' ),\n\t\t\t\t\t$container = $el.closest( '.llms-mb-list' );\n\n\t\t\t\tLLMS.Spinner.start( $container );\n\n\t\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\taction: 'membership_remove_auto_enroll_course',\n\t\t\t\t\t\tcourse_id: $el.attr( 'data-id' ),\n\t\t\t\t\t},\n\t\t\t\t\tbeforeSend: function() {\n\n\t\t\t\t\t\t$container.find( 'p.error' ).remove();\n\n\t\t\t\t\t},\n\t\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\t\tif ( r.success ) {\n\n\t\t\t\t\t\t\t$row.fadeOut( 200 );\n\t\t\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\t\t\t$row.remove();\n\t\t\t\t\t\t\t\ttoggle_header_row();\n\t\t\t\t\t\t\t}, 400 );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t$container.prepend( '<p class=\"error\">' + r.message + '</p>' );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tLLMS.Spinner.stop( $container );\n\t\t\t\t\t},\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t\t// bulk enroll all members into a course\n\t\t\t$table.on( 'click', 'a[href=\"#llms-course-bulk-enroll\"]', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\tvar $el        = $( this ),\n\t\t\t\t\t$row       = $el.closest( 'tr' ),\n\t\t\t\t\t$container = $el.closest( '.llms-mb-list' );\n\n\t\t\t\tif ( ! window.confirm( LLMS.l10n.translate( 'Click okay to enroll all active members into the selected course. Enrollment will take place in the background and you may leave your site after confirmation. This action cannot be undone!' ) ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tLLMS.Spinner.start( $container );\n\n\t\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\taction: 'bulk_enroll_membership_into_course',\n\t\t\t\t\t\tcourse_id: $el.attr( 'data-id' ),\n\t\t\t\t\t},\n\t\t\t\t\tbeforeSend: function() {\n\t\t\t\t\t\t$container.find( 'p.error' ).remove();\n\t\t\t\t\t},\n\t\t\t\t\tsuccess: function( r ) {\n\n\t\t\t\t\t\tif ( r.success ) {\n\n\t\t\t\t\t\t\t$el.replaceWith( '<strong style=\"float:right;\">' + r.data.message + '&nbsp;&nbsp;</strong>' );\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t$container.prepend( '<p class=\"error\">' + r.message + '</p>' );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tLLMS.Spinner.stop( $container );\n\t\t\t\t\t},\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t\t// Add an item to the autoenroll table on select.\n\t\t\t$( '#_llms_auto_enroll' ).on( 'change', function() {\n\n\t\t\t\tvar id    = $( this ).val(),\n\t\t\t\t\ttitle = $( this ).find( 'option[value=\"' + $( this ).val() + '\"]' ).text();\n\n\t\t\t\t// If there's no ID\n\t\t\t\tif ( ! id ) {\n\t\t\t\t\treturn;\n\t\t\t\t\t// Prevent Dupes.\n\t\t\t\t} else if ( -1 !== get_course_ids().indexOf( id ) ) {\n\n\t\t\t\t\talert( LLMS.l10n.replace( '\"%s\" is already in the course list.', { '%s': title } ) )\n\n\t\t\t\t\t// reset the select field.\n\t\t\t\t\t$( this ).val( '' ).trigger( 'change' );\n\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t\tvar $table = $( '.llms-mb-list._llms_content_table' );\n\t\t\t\t\t$tr    = $( '<tr />' );\n\n\t\t\t\t$tr.append( '<td><span class=\"dashicons dashicons-menu llms-drag-handle ui-sortable-handle\"></span></td>' );\n\t\t\t\t$tr.append( '<td><a href=\"' + window.llms.admin_url + 'post.php?action=edit&post=' + id + '\">' + title + '</a></td>' );\n\t\t\t\t$tr.append( '<td><a class=\"llms-button-danger small\" data-id=\"' + id + '\" href=\"#llms-course-remove\" style=\"float:right;\">' + LLMS.l10n.translate( 'Remove course' ) + '</a><a class=\"llms-button-secondary small\" data-id=\"' + id + '\" href=\"#llms-course-bulk-enroll\" style=\"float:right;margin-right:5px;\">' + LLMS.l10n.translate( 'Enroll All Members' ) + '</a></td>' );\n\n\t\t\t\t// append the element to the table.\n\t\t\t\t$table.find( 'table tbody' ).append( $tr );\n\n\t\t\t\t// reset the select field.\n\t\t\t\t$( this ).val( '' ).trigger( 'change' );\n\n\t\t\t\t// Show the header row.\n\t\t\t\ttoggle_header_row();\n\n\t\t\t\t// trigger a save event.\n\t\t\t\t$table.trigger( 'llms-save-autoenroll-courses' );\n\n\t\t\t} );\n\n\t\t\t// Make autoenrollment table sortable.\n\t\t\t$table.find( 'table tbody' ).sortable( {\n\t\t\t\thandle: '.llms-drag-handle',\n\t\t\t\t// Save order on stop.\n\t\t\t\tstop: function( event, ui ) {\n\t\t\t\t\tui.item.closest( '.llms-mb-list' ).trigger( 'llms-save-autoenroll-courses' );\n\t\t\t\t},\n\t\t\t} );\n\n\t\t\t// Save courses & course order.\n\t\t\t$table.on( 'llms-save-autoenroll-courses', function() {\n\n\t\t\t\tvar $container = $( this );\n\n\t\t\t\tLLMS.Spinner.start( $container );\n\n\t\t\t\twindow.LLMS.Ajax.call( {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\taction: 'llms_save_membership_autoenroll_courses',\n\t\t\t\t\t\tcourses: get_course_ids(),\n\t\t\t\t\t},\n\t\t\t\t\terror: function( jqxhr, code, error_msg ) {\n\t\t\t\t\t\talert( error_msg );\n\t\t\t\t\t},\n\t\t\t\t\tcomplete: function() {\n\t\t\t\t\t\tLLMS.Spinner.stop( $container );\n\t\t\t\t\t},\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Actions for ORDERS\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.28.0\n\t\t */\n\t\tthis.bind_llms_order = function() {\n\n\t\t\t$( 'button[name=\"llms-refund-toggle\"]' ).on( 'click', function() {\n\n\t\t\t\tvar $btn              = $( this ),\n\t\t\t\t\t$row              = $btn.closest( 'tr' ),\n\t\t\t\t\ttxn_id            = $row.attr( 'data-transaction-id' ),\n\t\t\t\t\trefundable_amount = $btn.attr( 'data-refundable' ),\n\t\t\t\t\tgateway_supports  = ( '1' === $btn.attr( 'data-gateway-supports' ) ) ? true : false,\n\t\t\t\t\tgateway_title     = $btn.attr( 'data-gateway' ),\n\t\t\t\t\t$new_row          = $( '#llms-txn-refund-model .llms-txn-refund-form' ).clone(),\n\t\t\t\t\t$gateway_btn      = $new_row.find( '.gateway-btn' );\n\n\t\t\t\t// configure and add the form\n\t\t\t\tif ( 'remove' !== $btn.attr( 'data-action' ) ) {\n\n\t\t\t\t\t$btn.text( LLMS.l10n.translate( 'Cancel' ) );\n\t\t\t\t\t$btn.attr( 'data-action', 'remove' );\n\t\t\t\t\t$new_row.find( 'input' ).removeAttr( 'disabled' );\n\t\t\t\t\t$new_row.find( 'input[name=\"llms_refund_amount\"]' ).attr( 'max', refundable_amount );\n\t\t\t\t\t$new_row.find( 'input[name=\"llms_refund_txn_id\"]' ).val( txn_id );\n\n\t\t\t\t\tif ( gateway_supports ) {\n\t\t\t\t\t\t$gateway_btn.find( '.llms-gateway-title' ).text( gateway_title );\n\t\t\t\t\t\t$gateway_btn.show();\n\t\t\t\t\t}\n\n\t\t\t\t\t$row.after( $new_row );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t$btn.text( LLMS.l10n.translate( 'Refund' ) );\n\t\t\t\t\t$btn.attr( 'data-action', '' );\n\t\t\t\t\t$row.next( 'tr' ).remove();\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t$( 'button[name=\"llms-manual-txn-toggle\"]' ).on( 'click', function() {\n\n\t\t\t\tvar $btn     = $( this ),\n\t\t\t\t\t$row     = $btn.closest( 'tr' ),\n\t\t\t\t\t$new_row = $( '#llms-manual-txn-model .llms-manual-txn-form' ).clone();\n\n\t\t\t\t// configure and add the form\n\t\t\t\tif ( 'remove' !== $btn.attr( 'data-action' ) ) {\n\n\t\t\t\t\t$btn.text( LLMS.l10n.translate( 'Cancel' ) );\n\t\t\t\t\t$btn.attr( 'data-action', 'remove' );\n\t\t\t\t\t$new_row.find( 'input' ).removeAttr( 'disabled' );\n\n\t\t\t\t\t$row.after( $new_row );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t$btn.text( LLMS.l10n.translate( 'Record a Manual Payment' ) );\n\t\t\t\t\t$btn.attr( 'data-action', '' );\n\t\t\t\t\t$row.next( 'tr' ).remove();\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t// cache the original value when focusing on a payment gateway select\n\t\t\t// used below so the original field related data can be restored when switching back to the originally selected gateway\n\t\t\t$( '.llms-metabox' ).one( 'focus', '.llms-metabox-field[data-llms-editable=\"payment_gateway\"] select', function() {\n\n\t\t\t\tif ( ! $( this ).attr( 'data-original-value' ) ) {\n\t\t\t\t\t$( this ).attr( 'data-original-value', $( this ).val() );\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\t// when selecting a new payment gateway get field data and update the dom to only display the fields\n\t\t\t// supported/needed by the newly selected gateway\n\t\t\t$( '.llms-metabox' ).on( 'change', '.llms-metabox-field[data-llms-editable=\"payment_gateway\"] select', function() {\n\n\t\t\t\tvar $select      = $( this ),\n\t\t\t\t\tgateway      = $select.val(),\n\t\t\t\t\tdata         = JSON.parse( $select.closest( '.llms-metabox-field' ).attr( 'data-gateway-fields' ) ),\n\t\t\t\t\tgateway_data = data[ gateway ];\n\n\t\t\t\tfor ( var field in gateway_data ) {\n\n\t\t\t\t\tvar $field = $( 'input[name=\"' + gateway_data[ field ].name + '\"]' ),\n\t\t\t\t\t\t$wrap  = $field.closest( '.llms-metabox-field' );\n\n\t\t\t\t\t// if the field is enabled show it the field and, if we're switching back to the originally selected\n\t\t\t\t\t// gateway, reload the value from the dom\n\t\t\t\t\tif ( gateway_data[ field ].enabled ) {\n\n\t\t\t\t\t\t$wrap.show();\n\t\t\t\t\t\t$field.attr( 'required', 'required' );\n\t\t\t\t\t\t$field.removeAttr( 'disabled' );\n\n\t\t\t\t\t\tif ( gateway === $select.attr( 'data-original-value' ) ) {\n\t\t\t\t\t\t\t$field.val( $wrap.attr( 'data-llms-editable-value' ) );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// otherwise hide the field\n\t\t\t\t\t\t// this will ensure it gets updated in the database\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// always clear the value when switching\n\t\t\t\t\t\t// ensures that outdated data is removed from the DB\n\t\t\t\t\t\t$field.attr( 'value', '' );\n\n\t\t\t\t\t\t$field.removeAttr( 'required' );\n\t\t\t\t\t\t// $field.attr( 'disabled', 'disabled' );\n\t\t\t\t\t\t$wrap.hide();\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Re-initializes TinyMCE Editors found within metaboxes\n\t\t *\n\t\t * @since 4.19.0\n\t\t * @since 4.21.2 Improve early return dependency check.\n\t\t * @since 7.0.1 Add `undefined` condition on early return check.\n\t\t *\n\t\t * @link https://github.com/gocodebox/lifterlms/issues/1553\n\t\t * @link https://github.com/gocodebox/lifterlms/pull/1618\n\t\t * @link https://github.com/gocodebox/lifterlms/issues/2298\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\t this.bind_mce_fixes = function() {\n\n\t\t\t// We need `wp.data` to proceed.\n\t\t\tif ( undefined === wp.data || [ null, undefined ].includes( wp.data.select( 'core/edit-post' ) ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tLLMS.wait_for(\n\t\t\t\tfunction() {\n\t\t\t\t\treturn undefined !== wp.data.select( 'core/edit-post' ).getMetaBoxesPerLocation( 'normal' );\n\t\t\t\t},\n\t\t\t\tfunction() {\n\n\t\t\t\t\tvar shouldRun = false;\n\t\t\t\t\t\tfind      = [ 'lifterlms-product', 'lifterlms-membership', 'lifterlms-course-options' ];\n\t\t\t\t\t\tmetaboxes = wp.data.select( 'core/edit-post' ).getMetaBoxesPerLocation( 'normal' );\n\n\t\t\t\t\t// Determine if we should run the fixer.\n\t\t\t\t\tfor ( var key in metaboxes ) {\n\t\t\t\t\t\tif ( -1 !== find.indexOf( metaboxes[ key ].id ) ) {\n\t\t\t\t\t\t\tshouldRun = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! shouldRun ) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fix them.\n\t\t\t\t\tvar toFix = {};\n\n\t\t\t\t\t/**\n\t\t\t\t\t * Determines if the TinyMCE instance should be fixed.\n\t\t\t\t\t *\n\t\t\t\t\t * @since 4.19.0\n\t\t\t\t\t *\n\t\t\t\t\t * @param {string} key Editor Key. This is the HTML id attribute of the textarea powering the editor instance.\n\t\t\t\t\t * @return {Boolean} Returns `true` if the editor should be fixed.\n\t\t\t\t\t */\n\t\t\t\t\tfunction llmsShouldFixTinyMCEEditor( key ) {\n\t\t\t\t\t\treturn ( 'excerpt' === key || -1 !== key.indexOf( 'llms' ) || -1 !== key.indexOf( 'lifterlms' ) )\n\t\t\t\t\t};\n\n\t\t\t\t\t// Loop through all the loaded editors.\n\t\t\t\t\tfor ( var key in tinyMCE.EditorManager.editors ) {\n\n\t\t\t\t\t\t// Mark LifterLMS editors to be fixed & de-init the editor.\n\t\t\t\t\t\tif ( llmsShouldFixTinyMCEEditor( key ) ) {\n\n\t\t\t\t\t\t\ttoFix[ key ] = tinyMCE.EditorManager.get( key );\n\t\t\t\t\t\t\ttinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, key );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// If we remove and re-init immediately it doesn't work, so we'll wait a bit and then re-init them all.\n\t\t\t\t\tsetTimeout( function() {\n\t\t\t\t\t\tfor ( var key in toFix ) {\n\t\t\t\t\t\t\ttinyMCE.EditorManager.init( toFix[ key ].settings || tinyMCE.EditorManager.settings );\n\t\t\t\t\t\t}\n\t\t\t\t\t}, 500 );\n\n\t\t\t\t}\n\t\t\t);\n\n\t\t};\n\n\t\t/**\n\t\t * Binds custom llms merge code buttons\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.1.0\n\t\t * @version  3.9.2\n\t\t */\n\t\tthis.bind_merge_code_buttons = function( $wrappers ) {\n\n\t\t\t$wrappers = $wrappers || $( '.llms-merge-code-wrapper' );\n\n\t\t\t$wrappers.find( '.llms-merge-code-button' ).on( 'click', function() {\n\n\t\t\t\t$( this ).next( '.llms-merge-codes' ).toggleClass( 'active' );\n\n\t\t\t} );\n\n\t\t\t$wrappers.find( '.llms-merge-codes li' ).on( 'click', function() {\n\n\t\t\t\tvar $el     = $( this ),\n\t\t\t\t\t$parent = $el.closest( '.llms-merge-codes' ),\n\t\t\t\t\ttarget  = $parent.attr( 'data-target' ),\n\t\t\t\t\tcode    = $el.attr( 'data-code' );\n\n\t\t\t\t// dealing with a tinymce instance\n\t\t\t\tif ( -1 === target.indexOf( '#' ) ) {\n\n\t\t\t\t\tvar editor = window.tinymce.editors[ target ];\n\t\t\t\t\tif ( editor ) {\n\t\t\t\t\t\teditor.insertContent( code );\n\t\t\t\t\t} // fallback in case we can't access the editor directly\n\t\t\t\t\telse {\n\t\t\t\t\t\talert( LLMS.l10n.translate( 'Copy this code and paste it into the desired area' ) + ': ' + code );\n\t\t\t\t\t}\n\n\t\t\t\t}\n\t\t\t\t// dealing with a DOM id\n\t\t\t\telse {\n\n\t\t\t\t\t$( target ).val( $( target ).val() + code );\n\n\t\t\t\t}\n\n\t\t\t\t$parent.removeClass( 'active' );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t/**\n\t\t * Bind metabox tabs\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.bind_tabs = function() {\n\t\t\t$( '.llms-nav-tab-wrapper .tabs li' ).on( 'click', function() {\n\n\t\t\t\tvar $btn     = $( this ),\n\t\t\t\t\t$metabox = $btn.closest( '.llms-mb-container' ),\n\t\t\t\t\ttab_id   = $btn.attr( 'data-tab' );\n\n\t\t\t\t$btn.siblings().removeClass( 'llms-active' );\n\n\t\t\t\t$metabox.find( '.tab-content' ).removeClass( 'llms-active' );\n\n\t\t\t\t$btn.addClass( 'llms-active' );\n\t\t\t\t$( '#' + tab_id ).addClass( 'llms-active' );\n\n\t\t\t} );\n\t\t};\n\n\t\t/**\n\t\t * Enable WP Post Table searches for applicable select2 boxes\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 3.21.0 Unknown.\n\t\t * @since 6.0.0 Show element at 100% width if not displaying a view button.\n\t\t * @since 7.1.1 Fixed `home_url` for view button.\n\t\t *\n\t\t * @return void\n\t\t */\n\t\tthis.post_select = function( $el ) {\n\n\t\t\tvar multi = 'multiple' === $el.attr( 'multiple' ),\n\t\t\t\tnoViewBtn = $el.attr( 'data-no-view-button' ),\n\t\t\t\teditBtn = $el.attr( 'data-edit-button' );\n\n\t\t\t$el.llmsPostsSelect2( {\n\t\t\t\twidth: multi || noViewBtn ? '100%' : '65%',\n\t\t\t} );\n\n\t\t\tif ( multi || noViewBtn ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// add a \"View\" button to see what the selected page looks like, if it doesn't already exist.\n\t\t\t$btn = $el.next('.select2').next( 'a.llms-button-secondary' );\n\t\t\tif ( ! $btn.length ) {\n\t\t\t\tvar msg  = editBtn ? LLMS.l10n.translate( 'Edit' ) : LLMS.l10n.translate( 'View' ),\n\t\t\t\t\t$btn = $( '<a class=\"llms-button-secondary small\" style=\"margin-left:5px;\" target=\"_blank\" href=\"#\">' + msg + ' <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a>' );\n\t\t\t\t$el.next( '.select2' ).after( $btn );\n\n\t\t\t\t$el.on( 'change', function() {\n\t\t\t\t\tvar id = $( this ).val();\n\t\t\t\t\tif ( id ) {\n\t\t\t\t\t\tif ( editBtn ) {\n\t\t\t\t\t\t\t$btn.attr( 'href', window.llms.admin_url + 'post.php?action=edit&post=' + parseInt( id )  ).show();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t$btn.attr( 'href', window.llms.home_url + '/?p=' + parseInt( id ) ).show();\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$btn.hide();\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\t$el.trigger( 'change' );\n\t\t};\n\n\t\t/**\n\t\t * Bind dom events for .llms-tables\n\t\t *\n\t\t * @return   void\n\t\t * @since    3.0.0\n\t\t * @version  3.0.0\n\t\t */\n\t\tthis.bind_tables = function() {\n\n\t\t\t$( '.llms-table button[name=\"llms-expand-table\"]' ).on( 'click', function() {\n\n\t\t\t\tvar $btn   = $( this ),\n\t\t\t\t\t$table = $btn.closest( '.llms-table' )\n\n\t\t\t\t// switch the text on the button if alt text is found\n\t\t\t\tif ( $btn.attr( 'data-text' ) ) {\n\t\t\t\t\tvar text = $btn.text();\n\t\t\t\t\t$btn.text( $btn.attr( 'data-text' ) );\n\t\t\t\t\t$btn.attr( 'data-text', text );\n\t\t\t\t}\n\n\t\t\t\t// switch classes on all expandable elements\n\t\t\t\t$table.find( '.expandable' ).each( function() {\n\n\t\t\t\t\tif ( $( this ).hasClass( 'closed' ) ) {\n\t\t\t\t\t\t$( this ).addClass( 'opened' ).removeClass( 'closed' );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$( this ).addClass( 'closed' ).removeClass( 'opened' );\n\t\t\t\t\t}\n\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t};\n\n\t\t// go\n\t\tthis.init();\n\n\t};\n\n\t// initialize the object\n\twindow.llms.metaboxes = new Metaboxes();\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/private/llms.js",
    "content": "/**\n * Main LLMS Namespace\n *\n * @since 1.0.0\n * @version 5.3.3\n */\n\nvar LLMS = window.LLMS || {};\n( function( $ ){\n\n\t'use strict';\n\n\t/**\n\t * Load all app modules\n\t */\n\t// = include ../app/*.js\n\n\t// = include ../llms-spinner.js\n\n\t/**\n\t * Initializes all classes within the LLMS Namespace\n\t *\n\t * @since Unknown\n\t *\n\t * @return {void}\n\t */\n\tLLMS.init = function() {\n\n\t\tfor (var func in LLMS) {\n\n\t\t\tif ( typeof LLMS[func] === 'object' && LLMS[func] !== null ) {\n\n\t\t\t\tif ( LLMS[func].init !== undefined ) {\n\n\t\t\t\t\tif ( typeof LLMS[func].init === 'function') {\n\t\t\t\t\t\tLLMS[func].init();\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t};\n\n\t/**\n\t * Determine if the current device is touch-enabled\n\t *\n\t * @since 3.24.3\n\t *\n\t * @see {@link https://stackoverflow.com/a/4819886/400568}\n\t *\n\t * @return {Boolean} Whether or not the device is touch-enabled.\n\t */\n\tLLMS.is_touch_device = function() {\n\n\t\tvar prefixes = ' -webkit- -moz- -o- -ms- '.split( ' ' );\n\t\tvar mq       = function( query ) {\n\t\t\treturn window.matchMedia( query ).matches;\n\t\t}\n\n\t\tif ( ( 'ontouchstart' in window ) || window.DocumentTouch && document instanceof DocumentTouch ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t/**\n\t\t * Include the 'heartz' as a way to have a non matching MQ to help terminate the join.\n\t\t *\n\t\t * @see {@link https://git.io/vznFH}\n\t\t */\n\t\tvar query = ['(', prefixes.join( 'touch-enabled),(' ), 'heartz', ')'].join( '' );\n\t\treturn mq( query );\n\n\t};\n\n\t/**\n\t * Wait for matchHeight to load\n\t *\n\t * @since 3.0.0\n\t * @since 3.16.6 Unknown.\n\t * @since 5.3.3 Pass a dependency name to `wait_for()`.\n\t *\n\t * @param {Function} cb Callback function to run when matchheight is ready.\n\t * @return {void}\n\t */\n\tLLMS.wait_for_matchHeight = function( cb ) {\n\t\tthis.wait_for( function() {\n\t\t\treturn ( undefined !== $.fn.matchHeight );\n\t\t}, cb, 'matchHeight' );\n\t}\n\n\t/**\n\t * Wait for webuiPopover to load\n\t *\n\t * @since 3.9.1\n\t * @since 3.16.6 Unknown.\n\t *\n\t * @param {Function} cb Callback function to run when matchheight is ready.\n\t * @return {void}\n\t */\n\tLLMS.wait_for_popover = function( cb ) {\n\t\tthis.wait_for( function() {\n\t\t\treturn ( undefined !== $.fn.webuiPopover );\n\t\t}, cb, 'webuiPopover' );\n\t}\n\n\t/**\n\t * Wait for a dependency to load and then run a callback once it has\n\t *\n\t * Temporary fix for a less-than-optimal assets loading function on the PHP side of things.\n\t *\n\t * @since 3.9.1\n\t * @since 5.3.3 Added optional `name` parameter.\n\t *\n\t * @param {Function} test A function that returns a truthy if the dependency is loaded.\n\t * @param {Function} cb   A callback function executed once the dependency is loaded.\n\t * @param {string}   name The dependency name.\n\t * @return {void}\n\t */\n\tLLMS.wait_for = function( test, cb, name ) {\n\n\t\tvar counter = 0,\n\t\t\tinterval;\n\n\t\tname = name ? name : 'unnamed';\n\n\t\tinterval = setInterval( function() {\n\n\t\t\t// If we get to 30 seconds log an error message.\n\t\t\tif ( counter >= 300 ) {\n\n\t\t\t\tconsole.log( 'Unable to load dependency: ' + name );\n\n\t\t\t\t// If we can't access yet, increment and wait...\n\t\t\t} else {\n\n\t\t\t\t// Bind the events, we're good!\n\t\t\t\tif ( test() ) {\n\t\t\t\t\tcb();\n\t\t\t\t} else {\n\t\t\t\t\t// console.log( 'Waiting for dependency: ' + name );\n\t\t\t\t\tcounter++;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tclearInterval( interval );\n\n\t\t}, 100 );\n\n\t};\n\n\tLLMS.init( $ );\n\n} )( jQuery );\n"
  },
  {
    "path": "assets/js/vendor/jquery.matchHeight.js",
    "content": "/**\n* jquery-match-height 0.7.0 by @liabru\n* http://brm.io/jquery-match-height/\n* License: MIT\n*/\n\n;(function(factory) { // eslint-disable-line no-extra-semi\n\t'use strict';\n\tif (typeof define === 'function' && define.amd) {\n\t\t// AMD\n\t\tdefine(['jquery'], factory);\n\t} else if (typeof module !== 'undefined' && module.exports) {\n\t\t// CommonJS\n\t\tmodule.exports = factory(require('jquery'));\n\t} else {\n\t\t// Global\n\t\tfactory(jQuery);\n\t}\n})(function($) {\n\t/*\n\t*  internal\n\t*/\n\n\tvar _previousResizeWidth = -1,\n\t\t_updateTimeout = -1;\n\n\t/*\n\t*  _parse\n\t*  value parse utility function\n\t*/\n\n\tvar _parse = function(value) {\n\t\t// parse value and convert NaN to 0\n\t\treturn parseFloat(value) || 0;\n\t};\n\n\t/*\n\t*  _rows\n\t*  utility function returns array of jQuery selections representing each row\n\t*  (as displayed after float wrapping applied by browser)\n\t*/\n\n\tvar _rows = function(elements) {\n\t\tvar tolerance = 1,\n\t\t\t$elements = $(elements),\n\t\t\tlastTop = null,\n\t\t\trows = [];\n\n\t\t// group elements by their top position\n\t\t$elements.each(function(){\n\t\t\tvar $that = $(this),\n\t\t\t\ttop = $that.offset().top - _parse($that.css('margin-top')),\n\t\t\t\tlastRow = rows.length > 0 ? rows[rows.length - 1] : null;\n\n\t\t\tif (lastRow === null) {\n\t\t\t\t// first item on the row, so just push it\n\t\t\t\trows.push($that);\n\t\t\t} else {\n\t\t\t\t// if the row top is the same, add to the row group\n\t\t\t\tif (Math.floor(Math.abs(lastTop - top)) <= tolerance) {\n\t\t\t\t\trows[rows.length - 1] = lastRow.add($that);\n\t\t\t\t} else {\n\t\t\t\t\t// otherwise start a new row group\n\t\t\t\t\trows.push($that);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// keep track of the last row top\n\t\t\tlastTop = top;\n\t\t});\n\n\t\treturn rows;\n\t};\n\n\t/*\n\t*  _parseOptions\n\t*  handle plugin options\n\t*/\n\n\tvar _parseOptions = function(options) {\n\t\tvar opts = {\n\t\t\tbyRow: true,\n\t\t\tproperty: 'height',\n\t\t\ttarget: null,\n\t\t\tremove: false\n\t\t};\n\n\t\tif (typeof options === 'object') {\n\t\t\treturn $.extend(opts, options);\n\t\t}\n\n\t\tif (typeof options === 'boolean') {\n\t\t\topts.byRow = options;\n\t\t} else if (options === 'remove') {\n\t\t\topts.remove = true;\n\t\t}\n\n\t\treturn opts;\n\t};\n\n\t/*\n\t*  matchHeight\n\t*  plugin definition\n\t*/\n\n\tvar matchHeight = $.fn.matchHeight = function(options) {\n\t\tvar opts = _parseOptions(options);\n\n\t\t// handle remove\n\t\tif (opts.remove) {\n\t\t\tvar that = this;\n\n\t\t\t// remove fixed height from all selected elements\n\t\t\tthis.css(opts.property, '');\n\n\t\t\t// remove selected elements from all groups\n\t\t\t$.each(matchHeight._groups, function(key, group) {\n\t\t\t\tgroup.elements = group.elements.not(that);\n\t\t\t});\n\n\t\t\t// TODO: cleanup empty groups\n\n\t\t\treturn this;\n\t\t}\n\n\t\tif (this.length <= 1 && !opts.target) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// keep track of this group so we can re-apply later on load and resize events\n\t\tmatchHeight._groups.push({\n\t\t\telements: this,\n\t\t\toptions: opts\n\t\t});\n\n\t\t// match each element's height to the tallest element in the selection\n\t\tmatchHeight._apply(this, opts);\n\n\t\treturn this;\n\t};\n\n\t/*\n\t*  plugin global options\n\t*/\n\n\tmatchHeight.version = '0.7.0';\n\tmatchHeight._groups = [];\n\tmatchHeight._throttle = 80;\n\tmatchHeight._maintainScroll = false;\n\tmatchHeight._beforeUpdate = null;\n\tmatchHeight._afterUpdate = null;\n\tmatchHeight._rows = _rows;\n\tmatchHeight._parse = _parse;\n\tmatchHeight._parseOptions = _parseOptions;\n\n\t/*\n\t*  matchHeight._apply\n\t*  apply matchHeight to given elements\n\t*/\n\n\tmatchHeight._apply = function(elements, options) {\n\t\tvar opts = _parseOptions(options),\n\t\t\t$elements = $(elements),\n\t\t\trows = [$elements];\n\n\t\t// take note of scroll position\n\t\tvar scrollTop = $(window).scrollTop(),\n\t\t\thtmlHeight = $('html').outerHeight(true);\n\n\t\t// get hidden parents\n\t\tvar $hiddenParents = $elements.parents().filter(':hidden');\n\n\t\t// cache the original inline style\n\t\t$hiddenParents.each(function() {\n\t\t\tvar $that = $(this);\n\t\t\t$that.data('style-cache', $that.attr('style'));\n\t\t});\n\n\t\t// temporarily must force hidden parents visible\n\t\t$hiddenParents.css('display', 'block');\n\n\t\t// get rows if using byRow, otherwise assume one row\n\t\tif (opts.byRow && !opts.target) {\n\n\t\t\t// must first force an arbitrary equal height so floating elements break evenly\n\t\t\t$elements.each(function() {\n\t\t\t\tvar $that = $(this),\n\t\t\t\t\tdisplay = $that.css('display');\n\n\t\t\t\t// temporarily force a usable display value\n\t\t\t\tif (display !== 'inline-block' && display !== 'flex' && display !== 'inline-flex') {\n\t\t\t\t\tdisplay = 'block';\n\t\t\t\t}\n\n\t\t\t\t// cache the original inline style\n\t\t\t\t$that.data('style-cache', $that.attr('style'));\n\n\t\t\t\t$that.css({\n\t\t\t\t\t'display': display,\n\t\t\t\t\t'padding-top': '0',\n\t\t\t\t\t'padding-bottom': '0',\n\t\t\t\t\t'margin-top': '0',\n\t\t\t\t\t'margin-bottom': '0',\n\t\t\t\t\t'border-top-width': '0',\n\t\t\t\t\t'border-bottom-width': '0',\n\t\t\t\t\t'height': '100px',\n\t\t\t\t\t'overflow': 'hidden'\n\t\t\t\t});\n\t\t\t});\n\n\t\t\t// get the array of rows (based on element top position)\n\t\t\trows = _rows($elements);\n\n\t\t\t// revert original inline styles\n\t\t\t$elements.each(function() {\n\t\t\t\tvar $that = $(this);\n\t\t\t\t$that.attr('style', $that.data('style-cache') || '');\n\t\t\t});\n\t\t}\n\n\t\t$.each(rows, function(key, row) {\n\t\t\tvar $row = $(row),\n\t\t\t\ttargetHeight = 0;\n\n\t\t\tif (!opts.target) {\n\t\t\t\t// skip apply to rows with only one item\n\t\t\t\tif (opts.byRow && $row.length <= 1) {\n\t\t\t\t\t$row.css(opts.property, '');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// iterate the row and find the max height\n\t\t\t\t$row.each(function(){\n\t\t\t\t\tvar $that = $(this),\n\t\t\t\t\t\tstyle = $that.attr('style'),\n\t\t\t\t\t\tdisplay = $that.css('display');\n\n\t\t\t\t\t// temporarily force a usable display value\n\t\t\t\t\tif (display !== 'inline-block' && display !== 'flex' && display !== 'inline-flex') {\n\t\t\t\t\t\tdisplay = 'block';\n\t\t\t\t\t}\n\n\t\t\t\t\t// ensure we get the correct actual height (and not a previously set height value)\n\t\t\t\t\tvar css = { 'display': display };\n\t\t\t\t\tcss[opts.property] = '';\n\t\t\t\t\t$that.css(css);\n\n\t\t\t\t\t// find the max height (including padding, but not margin)\n\t\t\t\t\tif ($that.outerHeight(false) > targetHeight) {\n\t\t\t\t\t\ttargetHeight = $that.outerHeight(false);\n\t\t\t\t\t}\n\n\t\t\t\t\t// revert styles\n\t\t\t\t\tif (style) {\n\t\t\t\t\t\t$that.attr('style', style);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$that.css('display', '');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// if target set, use the height of the target element\n\t\t\t\ttargetHeight = opts.target.outerHeight(false);\n\t\t\t}\n\n\t\t\t// iterate the row and apply the height to all elements\n\t\t\t$row.each(function(){\n\t\t\t\tvar $that = $(this),\n\t\t\t\t\tverticalPadding = 0;\n\n\t\t\t\t// don't apply to a target\n\t\t\t\tif (opts.target && $that.is(opts.target)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// handle padding and border correctly (required when not using border-box)\n\t\t\t\tif ($that.css('box-sizing') !== 'border-box') {\n\t\t\t\t\tverticalPadding += _parse($that.css('border-top-width')) + _parse($that.css('border-bottom-width'));\n\t\t\t\t\tverticalPadding += _parse($that.css('padding-top')) + _parse($that.css('padding-bottom'));\n\t\t\t\t}\n\n\t\t\t\t// set the height (accounting for padding and border)\n\t\t\t\t$that.css(opts.property, (targetHeight - verticalPadding) + 'px');\n\t\t\t});\n\t\t});\n\n\t\t// revert hidden parents\n\t\t$hiddenParents.each(function() {\n\t\t\tvar $that = $(this);\n\t\t\t$that.attr('style', $that.data('style-cache') || null);\n\t\t});\n\n\t\t// restore scroll position if enabled\n\t\tif (matchHeight._maintainScroll) {\n\t\t\t$(window).scrollTop((scrollTop / htmlHeight) * $('html').outerHeight(true));\n\t\t}\n\n\t\treturn this;\n\t};\n\n\t/*\n\t*  matchHeight._applyDataApi\n\t*  applies matchHeight to all elements with a data-match-height attribute\n\t*/\n\n\tmatchHeight._applyDataApi = function() {\n\t\tvar groups = {};\n\n\t\t// generate groups by their groupId set by elements using data-match-height\n\t\t$('[data-match-height], [data-mh]').each(function() {\n\t\t\tvar $this = $(this),\n\t\t\t\tgroupId = $this.attr('data-mh') || $this.attr('data-match-height');\n\n\t\t\tif (groupId in groups) {\n\t\t\t\tgroups[groupId] = groups[groupId].add($this);\n\t\t\t} else {\n\t\t\t\tgroups[groupId] = $this;\n\t\t\t}\n\t\t});\n\n\t\t// apply matchHeight to each group\n\t\t$.each(groups, function() {\n\t\t\tthis.matchHeight(true);\n\t\t});\n\t};\n\n\t/*\n\t*  matchHeight._update\n\t*  updates matchHeight on all current groups with their correct options\n\t*/\n\n\tvar _update = function(event) {\n\t\tif (matchHeight._beforeUpdate) {\n\t\t\tmatchHeight._beforeUpdate(event, matchHeight._groups);\n\t\t}\n\n\t\t$.each(matchHeight._groups, function() {\n\t\t\tmatchHeight._apply(this.elements, this.options);\n\t\t});\n\n\t\tif (matchHeight._afterUpdate) {\n\t\t\tmatchHeight._afterUpdate(event, matchHeight._groups);\n\t\t}\n\t};\n\n\tmatchHeight._update = function(throttle, event) {\n\t\t// prevent update if fired from a resize event\n\t\t// where the viewport width hasn't actually changed\n\t\t// fixes an event looping bug in IE8\n\t\tif (event && event.type === 'resize') {\n\t\t\tvar windowWidth = $(window).width();\n\t\t\tif (windowWidth === _previousResizeWidth) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_previousResizeWidth = windowWidth;\n\t\t}\n\n\t\t// throttle updates\n\t\tif (!throttle) {\n\t\t\t_update(event);\n\t\t} else if (_updateTimeout === -1) {\n\t\t\t_updateTimeout = setTimeout(function() {\n\t\t\t\t_update(event);\n\t\t\t\t_updateTimeout = -1;\n\t\t\t}, matchHeight._throttle);\n\t\t}\n\t};\n\n\t/*\n\t*  bind events\n\t*/\n\n\t// apply on DOM ready event\n\t$(matchHeight._applyDataApi);\n\n\t// update heights on load and resize events\n\t$(window).bind('load', function(event) {\n\t\tmatchHeight._update(false, event);\n\t});\n\n\t// throttled update heights on resize events\n\t$(window).bind('resize orientationchange', function(event) {\n\t\tmatchHeight._update(true, event);\n\t});\n\n});"
  },
  {
    "path": "assets/js/vendor/js.cookie.js",
    "content": "/*!\n * JavaScript Cookie v2.2.1\n * https://github.com/js-cookie/js-cookie\n *\n * Copyright 2006, 2015 Klaus Hartl & Fagner Brack\n * Released under the MIT license\n */\n;(function (factory) {\n\tvar registeredInModuleLoader;\n\tif (typeof define === 'function' && define.amd) {\n\t\tdefine(factory);\n\t\tregisteredInModuleLoader = true;\n\t}\n\tif (typeof exports === 'object') {\n\t\tmodule.exports = factory();\n\t\tregisteredInModuleLoader = true;\n\t}\n\tif (!registeredInModuleLoader) {\n\t\tvar OldCookies = window.Cookies;\n\t\tvar api = window.Cookies = factory();\n\t\tapi.noConflict = function () {\n\t\t\twindow.Cookies = OldCookies;\n\t\t\treturn api;\n\t\t};\n\t}\n}(function () {\n\tfunction extend () {\n\t\tvar i = 0;\n\t\tvar result = {};\n\t\tfor (; i < arguments.length; i++) {\n\t\t\tvar attributes = arguments[ i ];\n\t\t\tfor (var key in attributes) {\n\t\t\t\tresult[key] = attributes[key];\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\tfunction decode (s) {\n\t\treturn s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);\n\t}\n\n\tfunction init (converter) {\n\t\tfunction api() {}\n\n\t\tfunction set (key, value, attributes) {\n\t\t\tif (typeof document === 'undefined') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tattributes = extend({\n\t\t\t\tpath: '/'\n\t\t\t}, api.defaults, attributes);\n\n\t\t\tif (typeof attributes.expires === 'number') {\n\t\t\t\tattributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5);\n\t\t\t}\n\n\t\t\t// We're using \"expires\" because \"max-age\" is not supported by IE\n\t\t\tattributes.expires = attributes.expires ? attributes.expires.toUTCString() : '';\n\n\t\t\ttry {\n\t\t\t\tvar result = JSON.stringify(value);\n\t\t\t\tif (/^[\\{\\[]/.test(result)) {\n\t\t\t\t\tvalue = result;\n\t\t\t\t}\n\t\t\t} catch (e) {}\n\n\t\t\tvalue = converter.write ?\n\t\t\t\tconverter.write(value, key) :\n\t\t\t\tencodeURIComponent(String(value))\n\t\t\t\t\t.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);\n\n\t\t\tkey = encodeURIComponent(String(key))\n\t\t\t\t.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent)\n\t\t\t\t.replace(/[\\(\\)]/g, escape);\n\n\t\t\tvar stringifiedAttributes = '';\n\t\t\tfor (var attributeName in attributes) {\n\t\t\t\tif (!attributes[attributeName]) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tstringifiedAttributes += '; ' + attributeName;\n\t\t\t\tif (attributes[attributeName] === true) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Considers RFC 6265 section 5.2:\n\t\t\t\t// ...\n\t\t\t\t// 3.  If the remaining unparsed-attributes contains a %x3B (\";\")\n\t\t\t\t//     character:\n\t\t\t\t// Consume the characters of the unparsed-attributes up to,\n\t\t\t\t// not including, the first %x3B (\";\") character.\n\t\t\t\t// ...\n\t\t\t\tstringifiedAttributes += '=' + attributes[attributeName].split(';')[0];\n\t\t\t}\n\n\t\t\treturn (document.cookie = key + '=' + value + stringifiedAttributes);\n\t\t}\n\n\t\tfunction get (key, json) {\n\t\t\tif (typeof document === 'undefined') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar jar = {};\n\t\t\t// To prevent the for loop in the first place assign an empty array\n\t\t\t// in case there are no cookies at all.\n\t\t\tvar cookies = document.cookie ? document.cookie.split('; ') : [];\n\t\t\tvar i = 0;\n\n\t\t\tfor (; i < cookies.length; i++) {\n\t\t\t\tvar parts = cookies[i].split('=');\n\t\t\t\tvar cookie = parts.slice(1).join('=');\n\n\t\t\t\tif (!json && cookie.charAt(0) === '\"') {\n\t\t\t\t\tcookie = cookie.slice(1, -1);\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tvar name = decode(parts[0]);\n\t\t\t\t\tcookie = (converter.read || converter)(cookie, name) ||\n\t\t\t\t\t\tdecode(cookie);\n\n\t\t\t\t\tif (json) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tcookie = JSON.parse(cookie);\n\t\t\t\t\t\t} catch (e) {}\n\t\t\t\t\t}\n\n\t\t\t\t\tjar[name] = cookie;\n\n\t\t\t\t\tif (key === name) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (e) {}\n\t\t\t}\n\n\t\t\treturn key ? jar[key] : jar;\n\t\t}\n\n\t\tapi.set = set;\n\t\tapi.get = function (key) {\n\t\t\treturn get(key, false /* read as raw */);\n\t\t};\n\t\tapi.getJSON = function (key) {\n\t\t\treturn get(key, true /* read as json */);\n\t\t};\n\t\tapi.remove = function (key, attributes) {\n\t\t\tset(key, '', extend(attributes, {\n\t\t\t\texpires: -1\n\t\t\t}));\n\t\t};\n\n\t\tapi.defaults = {};\n\n\t\tapi.withConverter = init;\n\n\t\treturn api;\n\t}\n\n\treturn init(function () {});\n}));"
  },
  {
    "path": "assets/scss/_includes/_buttons.scss",
    "content": ".llms-button-action,\n.llms-button-danger,\n.llms-button-primary,\n.llms-button-secondary {\n\tborder:none;\n\tborder-radius: 8px;\n\tcolor: $color-white;\n\tcursor: pointer;\n\tdisplay: inline-block;\n\tfont-size: 18px;\n\tfont-weight: 700;\n\ttext-decoration: none;\n\ttext-shadow: none;\n\tline-height: 1;\n\tmargin: 0;\n\tmax-width: 100%;\n\tpadding: 12px 24px;\n\tposition: relative;\n\ttransition: all .5s ease;\n\twhite-space: nowrap;\n\n\t&:disabled {\n\t\topacity: 0.5;\n\t}\n\t&:hover, &:active {\n\t\tcolor: $color-white;\n\t}\n\t&:focus {\n\t\tcolor: $color-white;\n\t}\n\n\t&.auto {\n\t\twidth: auto;\n\t}\n\n\t&.full {\n\t\tdisplay: block;\n\t\ttext-align: center;\n\t\twidth: 100%;\n\t}\n\n\t&.square {\n\t\tpadding: 12px;\n\t}\n\n\t&.small {\n\t\tfont-size: 13px;\n\t\tpadding: 8px 14px;\n\t\t&.square { padding: 8px; }\n\t}\n\n\t&.large {\n\t\tfont-size: 18px;\n\t\tline-height: 1.2;\n\t\tpadding: 16px 32px;\n\t\t&.square { padding: 16px; }\n\t\t.fa {\n\t\t\tleft: -7px;\n\t\t\tposition: relative;\n\t\t}\n\t}\n\n}\n\n/* Fix for cases where link color overrides the button color */\na.llms-button-action,\na.llms-button-danger,\na.llms-button-primary {\n\tcolor: $color-white;\n}\n\n.llms-button-primary {\n\tbackground: $color-brand-blue;\n\t&:hover,\n\t&.clicked {\n\t\tbackground: $color-brand-blue-dark;\n\t}\n\t&:focus,\n\t&:active {\n\t\tbackground: $color-brand-blue-light;\n\t}\n}\n\n.llms-button-secondary {\n\tbackground: #e1e1e1;\n\tcolor: $color-cinder;\n\t&:hover {\n\t\tcolor: #414141;\n\t\tbackground: darken( #e1e1e1, 8 );\n\t}\n\t&:focus,\n\t&:active {\n\t\tcolor: #414141;\n\t\tbackground: lighten( #e1e1e1, 4 );\n\t}\n}\n/* Fix for cases where link color overrides the button color */\na.llms-button-secondary {\n\tcolor: $color-cinder;\n}\n\n.llms-button-action {\n\tbackground: $color-orange;\n\t&:hover,\n\t&.clicked {\n\t\tbackground: $color-brand-orange-dark;\n\t}\n\t&:focus,\n\t&:active {\n\t\tbackground: $color-brand-orange-light;\n\t}\n}\n\n.llms-button-danger {\n\tbackground: $color-danger;\n\t&:hover {\n\t\tbackground: darken( $color-danger, 8 );\n\t}\n\t&:focus,\n\t&:active {\n\t\tbackground: lighten( $color-danger, 4 );\n\t}\n}\n\n.llms-button-outline {\n\tbackground: transparent;\n\tborder: 3px solid #1D2327;\n\tborder-radius: 8px;\n\tcolor: #1D2327;\n\tcursor: pointer;\n\tfont-size: 16px;\n\tfont-weight: 700;\n\ttext-decoration: none;\n\ttext-shadow: none;\n\tline-height: 1;\n\tmargin: 0;\n\tmax-width: 100%;\n\tpadding: 12px 24px;\n\tposition: relative;\n\ttransition: all .5s ease;\n\n\t&:disabled {\n\t\topacity: 0.5;\n\t}\n\t&:hover, &:active {\n\t\tcolor: #1D2327;\n\t}\n\t&:focus {\n\t\tcolor: #1D2327;\n\t}\n\n\t&.auto {\n\t\twidth: auto;\n\t}\n\n\t&.full {\n\t\tdisplay: block;\n\t\ttext-align: center;\n\t\twidth: 100%;\n\t}\n\n\t&.square {\n\t\tpadding: 12px;\n\t}\n\n}\n\n.llms-button-plain {\n\tbackground: transparent;\n\tborder: none;\n\tborder-radius: 3px;\n\tcolor: #1D2327;\n\tcursor: pointer;\n\tfont-size: 16px;\n\tfont-weight: 700;\n\ttext-decoration: none;\n\ttext-shadow: none;\n\tline-height: 1;\n\tmargin: 0;\n\tmax-width: 100%;\n\tpadding: 1px 3px;\n\tposition: relative;\n\n\t&:hover, &:active {\n\t\tcolor: #1D2327;\n\t}\n\t&:focus {\n\t\tcolor: #1D2327;\n\t}\n\n}\n\n.llms-course-continue-button {\n\tdisplay: inline-block;\n}\n"
  },
  {
    "path": "assets/scss/_includes/_extends.scss",
    "content": "%cf,\n%clearfix {\n\t&:before,\n\t&:after {\n\t    content: \" \";\n\t    display: table;\n\t}\n\n\t&:after {\n    \tclear: both;\n\t}\n}\n\n\n\n%llms-element {\n\tbackground: $el-background;\n\tbox-shadow: $el-box-shadow;\n\tdisplay: block;\n\tcolor: #212121;\n\tmin-height: 85px;\n\tpadding: 15px;\n\ttext-decoration: none;\n\tposition: relative;\n\ttransition: background .4s ease;\n\n\t&:hover {\n\t\tbackground: $el-background-hover;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/_includes/_grid.scss",
    "content": "//\n// Floated columns.\n//\n// Utilized prior to the introduction of `.llms-flex-cols`. Prefer\n// usage of flex cols for new code where possible.\n//\n.llms-cols {\n\t@extend %clearfix;\n\n\t.llms-col { width: 100%; }\n\n\t@media all and (min-width: 600px) {\n\t\t[class*=\"llms-col-\"] {\n\t\t\tfloat: left;\n\t\t}\n\t}\n\n}\n\n//\n// Flex-box columns.\n//\n// Preferred over floated `.llms-cols` wherever possible.\n//\n.llms-flex-cols {\n\tdisplay: flex;\n\tflex-flow: row wrap;\n\n\t[class*=\"llms-col\"] {\n\t\tflex: 0 1 auto;\n\t\twidth: 100%;\n\t}\n}\n\n@media all and (min-width: 600px) {\n\t.llms-cols, .llms-flex-cols {\n\t\t$cols: 1;\n\t\t@while $cols <= 12 {\n\t\t\t.llms-col-#{$cols} {\n\t\t\t\twidth: calc( 100% / $cols );\n\t\t\t}\n\t\t\t$cols: $cols + 1;\n\t\t}\n\t}\n}\n\n"
  },
  {
    "path": "assets/scss/_includes/_llms-donut.scss",
    "content": ".llms-donut {\n\n\t@include clearfix;\n\n\tbackground-color: #f1f1f1;\n\tbackground-image: none;\n\tborder-radius: 50%;\n\tcolor: $color-brand-blue;\n\theight: 200px;\n\toverflow: hidden;\n\tposition: relative;\n\twidth: 200px;\n\n\tsvg {\n\t\toverflow: visible !important;\n\t\tpointer-events: none;\n\t\twidth: 100%;\n\t}\n\n\tsvg path {\n\t\tfill: none;\n\t\tstroke-width: 35px;\n\t\tstroke: $color-brand-blue;\n\t}\n\n\t&.mini {\n\t\theight: 36px;\n\t\twidth: 36px;\n\t\t.percentage {\n\t\t\tfont-size: 10px;\n\t\t}\n\t}\n\t&.small {\n\t\theight: 100px;\n\t\twidth: 100px;\n\t\t.percentage {\n\t\t\tfont-size: 18px;\n\t\t}\n\t}\n\t&.medium {\n\t\theight: 130px;\n\t\twidth: 130px;\n\t\t.percentage {\n\t\t\tfont-size: 26px;\n\t\t}\n\t}\n\t&.large {\n\t\theight: 260px;\n\t\twidth: 260px;\n\t\t.percentage {\n\t\t\tfont-size: 48px;\n\t\t}\n\t}\n\n\t.inside {\n\t\talign-items: center;\n\t\tbackground: #fff;\n\t\tborder-radius: 50%;\n\t\tbox-sizing: border-box;\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\theight: 80%;\n\t\tjustify-content: center;\n\t\tleft: 50%;\n\t\tposition: absolute;\n\t\ttext-align: center;\n\t\ttransform: translate(-50%, -50%);\n\t\twidth: 80%;\n\t\ttop: 50%;\n\t\tz-index: 3;\n\t}\n\n\t.percentage {\n\t\tline-height: 1.2;\n\t\tfont-size: 34px;\n\t}\n\n\t.caption {\n\t\tfont-size: 75%;\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/_includes/_llms-form-field.scss",
    "content": ".llms-form-fields {\n\t@extend %clearfix;\n\tbox-sizing: border-box;\n\t& * {\n\t\tbox-sizing: border-box;\n\t}\n\t&.flush {\n\t\t.llms-form-field {\n\t\t\tpadding: 0 0 20px;\n\t\t}\n\t}\n\n\tlabel:not(.llms-field-html label) {\n\t\tfont-weight: 700;\n\t}\n\n\t.wp-block-columns, .wp-block-column {\n\t\tmargin-bottom: 0;\n\t}\n}\n\n\t.llms-form-heading {\n\t\tpadding: 0 10px 10px;\n\t}\n\n\t.llms-form-field {\n\t\tfloat: left;\n\t\tpadding: 0 10px 20px;\n\t\tposition: relative;\n\t\twidth: 100%;\n\n\t\t// Ensure \"empty\" labels don't break the layout.\n\t\t// See the billing_address_2 field which has no label.\n\t\tlabel:empty:after {\n\t\t\tcontent: '\\00a0';\n\t\t}\n\n\t\t[type=\"text\"],\n\t\t[type=\"password\"],\n\t\t[type=\"email\"],\n\t\t[type=\"url\"],\n\t\t[type=\"tel\"],\n\t\t[type=\"number\"] {\n\t\t\tbackground-color: $color-white;\n\t\t\tbackground-clip: padding-box;\n\t\t\tborder: 1px solid $color-grey;\n\t\t\tborder-radius: $radius-small;\n\t\t\tbox-sizing: border-box;\n\t\t\tfont-size: 16px;\n\t\t\tline-height: 1;\n\t\t\tpadding: 8px 12px;\n\t\t\ttransition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n\t\t\twidth: 100%;\n\t\t}\n\n\t\tinput:focus,\n\t\tinput:focus-visible {\n\t\t\tborder-color: $color-brand-blue-light;\n\t\t\toutline: thin solid;\n\t\t}\n\n\t\tselect {\n\t\t\t-webkit-appearance: none;\n\t\t\t-moz-appearance: none;\n\t\t\t-ms-appearance: none;\n\t\t\tappearance: none;\n\t\t\tbackground-color: $color-white;\n\t\t\tborder: 1px solid $color-grey;\n\t\t\tborder-radius: $radius-small;\n\t\t\tbox-sizing: border-box;\n\t\t\tcolor: $color-black;\n\t\t\tpadding: 8px 12px;\n\t\t\tmargin: 0;\n\t\t\twidth: 100%;\n\t\t\tfont-family: inherit;\n\t\t\tfont-size: 16px;\n\t\t\tcursor: inherit;\n\t\t\tline-height: 1.6;\n\t\t\tz-index: 1;\n\t\t\toutline: none;\n\t\t\tbackground-repeat: no-repeat;\n\t\t\tbackground-image: linear-gradient(45deg, transparent 50%, currentcolor 50%), linear-gradient(135deg, currentcolor 50%, transparent 50%);\n\t\t\tbackground-position: right 15px top 1.3rem, right 10px top 1.3rem;\n\t\t\tbackground-size: 5px 5px, 5px 5px;\n\t\t}\n\n\t\tselect::-ms-expand {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\tselect:focus {\n\t\t\tborder: 1px solid $color-brand-blue-light;\n\t\t\toutline: thin solid;\n\t\t}\n\n\t\t&.valid {\n\t\t\tinput[type=\"date\"], input[type=\"time\"], input[type=\"datetime-local\"], input[type=\"week\"], input[type=\"month\"], input[type=\"text\"], input[type=\"email\"], input[type=\"url\"], input[type=\"password\"], input[type=\"search\"], input[type=\"tel\"], input[type=\"number\"], textarea, select {\n\t\t\t\tbackground: rgba( #83c373, .3 );\n\t\t\t\tborder-color: #83c373;\n\t\t\t}\n\t\t}\n\n\t\t&.error,\n\t\t&.invalid {\n\t\t\tinput[type=\"date\"], input[type=\"time\"], input[type=\"datetime-local\"], input[type=\"week\"], input[type=\"month\"], input[type=\"text\"], input[type=\"email\"], input[type=\"url\"], input[type=\"password\"], input[type=\"search\"], input[type=\"tel\"], input[type=\"number\"], textarea, select {\n\t\t\t\tbackground: rgba( $color-red, .3 );\n\t\t\t\tborder-color: $color-red;\n\t\t\t}\n\t\t}\n\n\t\t&.llms-visually-hidden-field {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t&.align-right {\n\t\t\ttext-align: right;\n\t\t}\n\n\t\t@media screen and ( min-width: 600px ) {\n\t\t\t$i: 1;\n\t\t\t@while $i <= 12 {\n\t\t\t\t&.llms-cols-#{$i} {\n\t\t\t\t\twidth: calc( $i / 12 ) * 100%;\n\t\t\t\t\t$i: $i + 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t&.type-hidden { padding: 0; }\n\n\t\t&.type-radio,\n\t\t&.type-checkbox {\n\t\t\tinput,\n\t\t\tlabel {\n\t\t\t\tdisplay: inline-block;\n\t\t\t\twidth: auto;\n\t\t\t}\n\t\t\tinput {\n\t\t\t\tmargin-right: 10px;\n\t\t\t}\n\t\t\tlabel + .llms-description {\n\t\t\t\tdisplay: block;\n\t\t\t}\n\t\t}\n\n\t\t&.type-radio:not(.is-group) {\n\n\t\t\tinput[type=\"radio\"] {\n\t\t\t\tposition: absolute;\n\t\t\t\topacity: 0;\n\t\t\t\tvisibility: none;\n\t\t\t}\n\n\t\t\tlabel:before {\n\t\t\t\tbackground: #fafafa;\n\t\t\t\tbackground-position: -24px 0;\n\t\t\t\tbackground-repeat: no-repeat;\n\t\t\t\tborder-radius: 50%;\n\t\t\t\tbox-shadow: hsla( 0,0%,100%,.15) 0 1px 1px, inset hsla(0,0%,0%,.35) 0 0 0 1px;\n\t\t\t\tcontent: '';\n\t\t\t\tcursor: pointer;\n\t\t\t\tdisplay: inline-block;\n\t\t\t\theight: 22px;\n\t\t\t\tmargin-right: 5px;\n\t\t\t\tposition: relative;\n\t\t\t\ttransition: background-position .15s cubic-bezier(.8, 0, 1, 1);\n\t\t\t\ttop: -3px;\n\t\t\t\tvertical-align: middle;\n\t\t\t\twidth: 22px;\n\t\t\t\tz-index: 2;\n\t\t\t}\n\n\t\t\tinput[type=\"radio\"]:checked + label:before {\n\t\t\t\ttransition: background-position .2s .15s cubic-bezier(0, 0, .2, 1);\n\t\t\t\tbackground-position: 0 0;\n\t\t\t\tbackground-image: radial-gradient(ellipse at center,  $color-brand-blue 0%,$color-brand-blue 40%, #fafafa 45%);\n\t\t\t}\n\n\t\t}\n\n\t\t.llms-input-group {\n\t\t\tmargin-top: 5px;\n\t\t\t.llms-form-field {\n\t\t\t\tpadding: 0 0 5px 5px;\n\t\t\t}\n\t\t}\n\n\t\t&.type-reset,\n\t\t&.type-button,\n\t\t&.type-submit {\n\t\t\tbutton:not(.auto) { width: 100%; }\n\t\t}\n\n\t\t.llms-description {\n\t\t\tfont-size: 14px;\n\t\t\tfont-style: italic;\n\t\t\tline-height: 18px;\n\t\t}\n\n\t\t.llms-required {\n\t\t\tcolor: $color-red;\n\t\t\tmargin-left: 4px;\n\t\t}\n\n\t\tinput, textarea, select {\n\t\t\twidth: 100%;\n\t\t\tmargin-bottom: 5px;\n\t\t}\n\n\t\t.select2-container .select2-selection--single {\n\t\t\theight: auto;\n\t\t\tpadding: 4px 6px;\n\t\t}\n\t\t.select2-container--default .select2-selection--single .select2-selection__arrow {\n\t\t\theight: 100%;\n\t\t}\n\n\t\t&:has(.llms-visibility-toggle) {\n\t\t\talign-items: center;\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-areas:\n\t\t\t\t\"label toggle\"\n\t\t\t\t\"input input\";\n\t\t\tgrid-template-columns: 1fr auto;\n\n\t\t\tlabel {\n\t\t\t\tgrid-area: label;\n\t\t\t}\n\n\t\t\tinput {\n\t\t\t\tgrid-area: input;\n\t\t\t}\n\n\t\t\t.llms-visibility-toggle {\n\t\t\t\tgrid-area: toggle;\n\t\t\t\tjustify-self: end;\n\t\t\t}\n\t\t}\n\n\t}\n\n\n\t.llms-password-strength-meter {\n\t\tborder: 1px solid #dadada;\n\t\tdisplay: none;\n\t\tfont-size: 10px;\n\t\tmargin-top: -10px;\n\t\tpadding: 1px;\n\t\tposition: relative;\n\t\ttext-align: center;\n\n\t\t&:before {\n\t\t\tbottom: 0;\n\t\t\tcontent: '';\n\t\t\tleft: 0;\n\t\t\tposition: absolute;\n\t\t\ttop: 0;\n\t\t\ttransition: width .4s ease;\n\t\t}\n\n\t\t&.mismatch,\n\t\t&.too-short,\n\t\t&.very-weak {\n\t\t\tborder-color: #e35b5b;\n\t\t\t&:before {\n\t\t\t\tbackground: rgba( #e35b5b, 0.25 );\n\t\t\t\twidth: 25%;\n\t\t\t}\n\t\t}\n\n\t\t&.too-short:before {\n\t\t\twidth: 0;\n\t\t}\n\n\t\t&.weak {\n\t\t\tborder-color: #f78b53;\n\t\t\t&:before {\n\t\t\t\tbackground: rgba( #f78b53, 0.25 );\n\t\t\t\twidth: 50%;\n\t\t\t}\n\t\t}\n\n\t\t&.medium {\n\t\t\tborder-color: #ffc733;\n\t\t\t&:before {\n\t\t\t\tbackground: rgba( #ffc733, 0.25 );\n\t\t\t\twidth: 75%;\n\t\t\t}\n\t\t}\n\n\t\t&.strong {\n\t\t\tborder-color: #83c373;\n\t\t\t&:before {\n\t\t\t\tbackground: rgba( #83c373, 0.25 );\n\t\t\t\twidth: 100%;\n\t\t\t}\n\t\t}\n\n\t\t+ .llms-description {\n\t\t\tdisplay: block;\n\t\t}\n\t}\n"
  },
  {
    "path": "assets/scss/_includes/_mixins.scss",
    "content": "\n@mixin clearfix() {\n\t&:before,\n\t&:after {\n\t    content: \" \";\n\t    display: table;\n\t}\n\t&:after {\n\t    clear: both;\n\t}\n}\n\n//\n// Positioning mixin\n//\n// @param [string] $position: position\n// @param [list] $args (()): offsets list\n//\n// @source http://hugogiraudel.com/2013/08/05/offsets-sass-mixin/\n//\n@mixin position($position, $args: ()) {\n\t$offsets: top right bottom left;\n\tposition: $position;\n\n\t@each $offset in $offsets {\n\t\t$index: index($args, $offset);\n\n\t\t@if $index {\n\t\t\t@if $index == length($args) {\n\t\t\t\t#{$offset}: 0;\n\t\t\t}\n\t\t\t@else {\n\t\t\t\t$next: nth($args, $index + 1);\n\t\t\t\t@if is-valid-length($next) {\n\t\t\t\t\t#{$offset}: $next;\n\t\t\t\t}\n\t\t\t\t@else if index($offsets, $next) {\n\t\t\t\t\t#{$offset}: 0;\n\t\t\t\t}\n\t\t\t\t@else {\n\t\t\t\t\t@warn \"Invalid value `#{$next}` for offset `#{$offset}`.\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n//\n// Function checking if $value is a valid length\n// @param [literal] $value: value to test\n// @return [bool]\n//\n@function is-valid-length($value) {\n\t$r: (type-of($value) == \"number\" and not unitless($value)) or (index(auto initial inherit 0, $value) != null);\n\t@return $r;\n}\n\n//\n// Shorthands\n//\n@mixin absolute($args: ()) {\n\t@include position(absolute, $args);\n}\n\n@mixin fixed($args: ()) {\n\t@include position(fixed, $args);\n}\n\n@mixin relative($args: ()) {\n\t@include position(relative, $args);\n}\n\n\n\n@mixin order_status_badges() {\n\n\t.llms-status {\n\t\tborder-radius: $radius-small;\n\t\tdisplay: inline-block;\n\t\tfont-size: 13px;\n\t\tfont-weight: 700;\n\t\tline-height: 1.4;\n\t\tpadding: 2px 6px;\n\t\tvertical-align: middle;\n\n\t\t&.llms-size--large {\n\t\t\tfont-size: 105%;\n\t\t\tpadding: 6px 12px;\n\t\t}\n\n\t\t&.llms-active,\n\t\t&.llms-completed,\n\t\t&.llms-pass, // quiz\n\t\t&.llms-txn-succeeded {\n\t\t\tcolor: $color-green;\n\t\t\tbackground-color: rgba( $color-green, .15 );\n\t\t}\n\n\t\t&.llms-fail, // quiz\n\t\t&.llms-failed,\n\t\t&.llms-expired,\n\t\t&.llms-cancelled,\n\t\t&.llms-txn-failed {\n\t\t\tcolor: $color-red;\n\t\t\tbackground-color: rgba( $color-red, .15 );\n\t\t}\n\n\t\t&.llms-incomplete, // assignment\n\t\t&.llms-on-hold,\n\t\t&.llms-pending,\n\t\t&.llms-pending-cancel,\n\t\t&.llms-refunded,\n\t\t&.llms-txn-pending,\n\t\t&.llms-txn-refunded {\n\t\t\tcolor: $color-orange;\n\t\t\tbackground-color: rgba( $color-orange, .15 );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/_includes/_quiz-result-question-list.scss",
    "content": ".llms-quiz-attempt-results {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style-type: none;\n\n\t.llms-quiz-attempt-question {\n\t\tbackground: #efefef;\n\t\tborder-radius: $radius-small;\n\t\tmargin: 0 0 15px;\n\t\tposition: relative;\n\t\tlist-style-type: none;\n\t\t.toggle-answer {\n\t\t\tcolor: inherit;\n\t\t\tdisplay: flex;\n\t\t\tgap: 10px;\n\t\t\tjustify-content: space-between;\n\t\t\tpadding: 15px 35px 15px 15px;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t&.status--waiting.correct,\n\t\t&.status--waiting.incorrect {\n\t\t\tbackground: rgba( $color-orange, 0.2 );\n\t\t\t.llms-status-icon {\n\t\t\t\tbackground-color: $color-orange;\n\t\t\t}\n\t\t}\n\n\t\t&.status--graded.correct {\n\t\t\tbackground: rgba( $color-green, 0.15 );\n\t\t\t.llms-status-icon {\n\t\t\t\tbackground-color: $color-green;\n\t\t\t}\n\t\t}\n\t\t&.status--graded.incorrect {\n\t\t\tbackground: rgba( $color-red, 0.15 );\n\t\t\t.llms-status-icon {\n\t\t\t\tbackground-color: $color-red;\n\t\t\t}\n\t\t}\n\t\tpre {\n\t\t\toverflow: auto;\n\t\t}\n\t\t.llms-question-title {\n\t\t\tfont-size: 22px;\n\t\t\tmargin: 0;\n\t\t\tline-height: 1.4;\n\t\t}\n\n\t\t.llms-points {\n\t\t\tline-height: 1.4;\n\t\t}\n\n\t\t.llms-status-icon-tip {\n\t\t\tposition: absolute;\n\t\t\tright: -12px;\n\t\t\ttop: -2px;\n\t\t}\n\n\t\t.llms-status-icon {\n\t\t\tcolor: rgba( 255, 255, 255, 0.65 );\n\t\t\tborder-radius: 50%;\n\t\t\tfont-size: 30px;\n\t\t\theight: 40px;\n\t\t\tline-height: 40px;\n\t\t\ttext-align: center;\n\t\t\twidth: 40px;\n\t\t}\n\n\t\t.llms-quiz-attempt-question-main {\n\t\t\tdisplay: none;\n\t\t\tpadding: 0 15px 15px;\n\n\t\t\t.llms-quiz-results-label {\n\t\t\t\tfont-weight: 700;\n\t\t\t\tmargin: 0 0 10px;\n\t\t\t\tpadding: 0;\n\t\t\t}\n\n\t\t\tul.llms-quiz-attempt-answers {\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 0;\n\t\t\t\tli.llms-quiz-attempt-answer {\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tmargin: 0 0 0 30px;\n\t\t\t\t\t&:only-child {\n\t\t\t\t\t\tlist-style-type: none;\n\t\t\t\t\t\tmargin-left: 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\timg {\n\t\t\t\theight: auto;\n\t\t\t\tmax-width: 200px;\n\t\t\t}\n\n\t\t\t.llms-quiz-attempt-answer-section {\n\t\t\t\tborder-top: 2px solid rgba( #fff, 0.5 );\n\t\t\t\tmargin-top: 20px;\n\t\t\t\tpadding-top: 20px;\n\t\t\t\t&:first-child {\n\t\t\t\t\tborder-top: none;\n\t\t\t\t\tmargin-top: 0;\n\t\t\t\t\tpadding-top: 0;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t&.type--picture_choice,\n\t\t&.type--picture_reorder {\n\t\t\tul.llms-quiz-attempt-answers {\n\t\t\t\tlist-style-type: none;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 0;\n\n\t\t\t\tli.llms-quiz-attempt-answer {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tlist-style-type: none;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 5px;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t&.type--removed {\n\t\t\t.llms-question-title {\n\t\t\t\tfont-style: italic;\n\t\t\t\tfont-weight: normal;\n\t\t\t}\n\t\t\topacity: .5;\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "assets/scss/_includes/_tooltip.scss",
    "content": ".lifterlms, // Settings & Course Builder.\n.llms-metabox, // Some Metaboxes.\n.llms-mb-container, // Other Metaboxes.\n.llms-quiz-wrapper { // Quiz results.\n\n\t[data-tip],\n\t[data-title-default],\n\t[data-title-active] {\n\n\t\t$bgcolor: #444;\n\n\t\tposition: relative;\n\n\t\t&.tip--top-right {\n\t\t\t&:before {\n\t\t\t\tbottom: 100%;\n\t\t\t\tleft: -10px;\n\t\t\t}\n\t\t\t&:hover:before {\n\t\t\t\tbottom: calc( 100% + 6px );\n\t\t\t}\n\t\t\t&:after {\n\t\t\t\tborder-top-color: $bgcolor;\n\t\t\t\tleft: 6px;\n\t\t\t\ttop: 0;\n\t\t\t}\n\t\t\t&:hover:after {\n\t\t\t\ttop: -6px;\n\t\t\t}\n\t\t}\n\n\n\t\t&.tip--top-left {\n\t\t\t&:before {\n\t\t\t\tbottom: 100%;\n\t\t\t\tright: -10px;\n\t\t\t}\n\t\t\t&:hover:before {\n\t\t\t\tbottom: calc( 100% + 6px );\n\t\t\t}\n\t\t\t&:after {\n\t\t\t\tborder-top-color: $bgcolor;\n\t\t\t\tright: 6px;\n\t\t\t\ttop: 0;\n\t\t\t}\n\t\t\t&:hover:after {\n\t\t\t\ttop: -6px;\n\t\t\t}\n\t\t}\n\n\n\n\t\t&.tip--bottom-left {\n\t\t\t&:before {\n\t\t\t\ttop: 100%;\n\t\t\t\tright: -10px;\n\t\t\t}\n\t\t\t&:hover:before {\n\t\t\t\ttop: calc( 100% + 6px );\n\t\t\t}\n\t\t\t&:after {\n\t\t\t\tborder-bottom-color: $bgcolor;\n\t\t\t\tright: 6px;\n\t\t\t\tbottom: 0;\n\t\t\t}\n\t\t\t&:hover:after {\n\t\t\t\tbottom: -6px;\n\t\t\t}\n\t\t}\n\n\t\t&.tip--bottom-right {\n\t\t\t&:before {\n\t\t\t\ttop: 100%;\n\t\t\t\tleft: -10px;\n\t\t\t}\n\t\t\t&:hover:before {\n\t\t\t\ttop: calc( 100% + 6px );\n\t\t\t}\n\t\t\t&:after {\n\t\t\t\tborder-bottom-color: $bgcolor;\n\t\t\t\tleft: 6px;\n\t\t\t\tbottom: 0;\n\t\t\t}\n\t\t\t&:hover:after {\n\t\t\t\tbottom: -6px;\n\t\t\t}\n\t\t}\n\n\t\t&:before {\n\t\t\tbackground: $bgcolor;\n\t\t\tborder-radius: 4px;\n\t\t\tcolor: #fff;\n\t\t\tfont-size: 13px;\n\t\t\tline-height: 1.2;\n\t\t\tpadding: 8px;\n\t\t\tmax-width: 300px;\n\t\t\twidth: max-content;\n\t\t}\n\t\t&:after {\n\t\t\tcontent: '';\n\t\t\tborder: 6px solid transparent;\n\t\t\theight: 0;\n\t\t\twidth: 0;\n\t\t}\n\n\t\t&:before,\n\t\t&:after {\n\t\t\topacity: 0;\n\t\t\ttransition: all 0.2s 0.1s ease;\n\t\t\tposition: absolute;\n\t\t\tpointer-events: none;\n\t\t\tvisibility: hidden;\n\t\t}\n\t\t&:hover:before,\n\t\t&:hover:after {\n\t\t\topacity: 1;\n\t\t\ttransition: all 0 0.1s ease;\n\t\t\tvisibility: visible;\n\t\t\tz-index: 99999999;\n\t\t}\n\n\t}\n\n\t[data-tip] {\n\t\t&:before {\n\t\t\tcontent: attr(data-tip);\n\t\t}\n\t}\n\t[data-tip].active {\n\t\t&:before {\n\t\t\tcontent: attr(data-tip-active);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/_includes/_vars-brand-colors.scss",
    "content": "//\n// LifterLMS Brand Colors\n// Currently overrides brand colors on the admin panel\n//\n\n$color-brand-blue: #466dd8;\n$color-brand-blue-dark: darken( $color-brand-blue, 8 );\n$color-brand-dark-blue: darken( $color-brand-blue, 24 );\n$color-brand-blue-light: lighten( $color-brand-blue, 8 );\n\n$color-brand-orange: #f8954f;\n$color-brand-orange-dark: #f67d28;\n$color-brand-orange-light: lighten( $color-brand-orange, 8 );\n\n$color-brand-aqua: #17bebb;\n\n$color-brand-pink: #ef476f;\n\n$color-blue: $color-brand-blue;\n"
  },
  {
    "path": "assets/scss/_includes/_vars.scss",
    "content": "// ----- LifterLMS Brand Colors ----- \\\\\n$color-brand-dark-blue: #243c56;\n\n$color-brand-blue: #466dd8;\n$color-brand-blue-dark: darken( $color-brand-blue, 12 ); // #0077e4\n$color-brand-blue-light: lighten( $color-brand-blue, 8 );\n\n$color-brand-orange: #f8954f;\n$color-brand-orange-dark: #f67d28;\n$color-brand-orange-light: lighten( $color-brand-orange, 8 );\n\n$color-brand-aqua: #17bebb;\n\n$color-brand-pink: #ef476f;\n\n\n\n// ----- name our versions of common colors ----- \\\\\n$color-black: #010101;\n$color-green: #4d8d3c;\n$color-blue: $color-brand-blue;\n$color-red: #bb231c;\n$color-white: #fefefe;\n$color-aqua: #35bbaa;\n$color-purple: #845ef7;\n$color-orange: #c05621;\n\n$color-red-hover: darken($color-red,5);\n\n\n// ----- state / action names ----- \\\\\n$color-success: $color-green;\n$color-danger: $color-red;\n\n\n\n\n\n\n\n\n$color-lightgrey:\t\t#ccc;\n$color-grey: \t\t\t#999;\n$color-darkgrey:\t\t#666;\n$color-cinder:\t\t\t#444;\n$color-lightblue:\t\t#33b1cb;\n$color-darkblue:\t\t#0185a3;\n\n\n\n\n\n\n\n\n\n\n\n\n$color-border: #dedede;\n\n$el-box-shadow: 0 1px 2px 0 rgba($color-black,0.4);\n$el-background: #f1f1f1;\n$el-background-hover: #eaeaea;\n\n$break-xsmall: 320px;\n$break-small: 641px;\n$break-medium: 768px;\n$break-large: 1024px;\n\n$radius-small: 6px;\n$radius-medium: 12px;\n"
  },
  {
    "path": "assets/scss/_includes/vendor/_font-awesome.scss",
    "content": "/*!\n *  Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome\n *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)\n */\n/* FONT PATH\n * -------------------------- */\n@font-face {\n  font-family: 'FontAwesome';\n  src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');\n  src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');\n  font-weight: normal;\n  font-style: normal;\n}\n.fa {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n/* makes the font 33% larger relative to the icon container */\n.fa-lg {\n  font-size: 1.33333333em;\n  line-height: 0.75em;\n  vertical-align: -15%;\n}\n.fa-2x {\n  font-size: 2em;\n}\n.fa-3x {\n  font-size: 3em;\n}\n.fa-4x {\n  font-size: 4em;\n}\n.fa-5x {\n  font-size: 5em;\n}\n.fa-fw {\n  width: 1.28571429em;\n  text-align: center;\n}\n.fa-ul {\n  padding-left: 0;\n  margin-left: 2.14285714em;\n  list-style-type: none;\n}\n.fa-ul > li {\n  position: relative;\n}\n.fa-li {\n  position: absolute;\n  left: -2.14285714em;\n  width: 2.14285714em;\n  top: 0.14285714em;\n  text-align: center;\n}\n.fa-li.fa-lg {\n  left: -1.85714286em;\n}\n.fa-border {\n  padding: .2em .25em .15em;\n  border: solid 0.08em #eeeeee;\n  border-radius: .1em;\n}\n.fa-pull-left {\n  float: left;\n}\n.fa-pull-right {\n  float: right;\n}\n.fa.fa-pull-left {\n  margin-right: .3em;\n}\n.fa.fa-pull-right {\n  margin-left: .3em;\n}\n/* Deprecated as of 4.4.0 */\n.pull-right {\n  float: right;\n}\n.pull-left {\n  float: left;\n}\n.fa.pull-left {\n  margin-right: .3em;\n}\n.fa.pull-right {\n  margin-left: .3em;\n}\n.fa-spin {\n  -webkit-animation: fa-spin 2s infinite linear;\n  animation: fa-spin 2s infinite linear;\n}\n.fa-pulse {\n  -webkit-animation: fa-spin 1s infinite steps(8);\n  animation: fa-spin 1s infinite steps(8);\n}\n@-webkit-keyframes fa-spin {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(359deg);\n    transform: rotate(359deg);\n  }\n}\n@keyframes fa-spin {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(359deg);\n    transform: rotate(359deg);\n  }\n}\n.fa-rotate-90 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)\";\n  -webkit-transform: rotate(90deg);\n  -ms-transform: rotate(90deg);\n  transform: rotate(90deg);\n}\n.fa-rotate-180 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)\";\n  -webkit-transform: rotate(180deg);\n  -ms-transform: rotate(180deg);\n  transform: rotate(180deg);\n}\n.fa-rotate-270 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)\";\n  -webkit-transform: rotate(270deg);\n  -ms-transform: rotate(270deg);\n  transform: rotate(270deg);\n}\n.fa-flip-horizontal {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)\";\n  -webkit-transform: scale(-1, 1);\n  -ms-transform: scale(-1, 1);\n  transform: scale(-1, 1);\n}\n.fa-flip-vertical {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)\";\n  -webkit-transform: scale(1, -1);\n  -ms-transform: scale(1, -1);\n  transform: scale(1, -1);\n}\n:root .fa-rotate-90,\n:root .fa-rotate-180,\n:root .fa-rotate-270,\n:root .fa-flip-horizontal,\n:root .fa-flip-vertical {\n  filter: none;\n}\n.fa-stack {\n  position: relative;\n  display: inline-block;\n  width: 2em;\n  height: 2em;\n  line-height: 2em;\n  vertical-align: middle;\n}\n.fa-stack-1x,\n.fa-stack-2x {\n  position: absolute;\n  left: 0;\n  width: 100%;\n  text-align: center;\n}\n.fa-stack-1x {\n  line-height: inherit;\n}\n.fa-stack-2x {\n  font-size: 2em;\n}\n.fa-inverse {\n  color: #ffffff;\n}\n/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen\n   readers do not read off random characters that represent icons */\n.fa-glass:before {\n  content: \"\\f000\";\n}\n.fa-music:before {\n  content: \"\\f001\";\n}\n.fa-search:before {\n  content: \"\\f002\";\n}\n.fa-envelope-o:before {\n  content: \"\\f003\";\n}\n.fa-heart:before {\n  content: \"\\f004\";\n}\n.fa-star:before {\n  content: \"\\f005\";\n}\n.fa-star-o:before {\n  content: \"\\f006\";\n}\n.fa-user:before {\n  content: \"\\f007\";\n}\n.fa-film:before {\n  content: \"\\f008\";\n}\n.fa-th-large:before {\n  content: \"\\f009\";\n}\n.fa-th:before {\n  content: \"\\f00a\";\n}\n.fa-th-list:before {\n  content: \"\\f00b\";\n}\n.fa-check:before {\n  content: \"\\f00c\";\n}\n.fa-remove:before,\n.fa-close:before,\n.fa-times:before {\n  content: \"\\f00d\";\n}\n.fa-search-plus:before {\n  content: \"\\f00e\";\n}\n.fa-search-minus:before {\n  content: \"\\f010\";\n}\n.fa-power-off:before {\n  content: \"\\f011\";\n}\n.fa-signal:before {\n  content: \"\\f012\";\n}\n.fa-gear:before,\n.fa-cog:before {\n  content: \"\\f013\";\n}\n.fa-trash-o:before {\n  content: \"\\f014\";\n}\n.fa-home:before {\n  content: \"\\f015\";\n}\n.fa-file-o:before {\n  content: \"\\f016\";\n}\n.fa-clock-o:before {\n  content: \"\\f017\";\n}\n.fa-road:before {\n  content: \"\\f018\";\n}\n.fa-download:before {\n  content: \"\\f019\";\n}\n.fa-arrow-circle-o-down:before {\n  content: \"\\f01a\";\n}\n.fa-arrow-circle-o-up:before {\n  content: \"\\f01b\";\n}\n.fa-inbox:before {\n  content: \"\\f01c\";\n}\n.fa-play-circle-o:before {\n  content: \"\\f01d\";\n}\n.fa-rotate-right:before,\n.fa-repeat:before {\n  content: \"\\f01e\";\n}\n.fa-refresh:before {\n  content: \"\\f021\";\n}\n.fa-list-alt:before {\n  content: \"\\f022\";\n}\n.fa-lock:before {\n  content: \"\\f023\";\n}\n.fa-flag:before {\n  content: \"\\f024\";\n}\n.fa-headphones:before {\n  content: \"\\f025\";\n}\n.fa-volume-off:before {\n  content: \"\\f026\";\n}\n.fa-volume-down:before {\n  content: \"\\f027\";\n}\n.fa-volume-up:before {\n  content: \"\\f028\";\n}\n.fa-qrcode:before {\n  content: \"\\f029\";\n}\n.fa-barcode:before {\n  content: \"\\f02a\";\n}\n.fa-tag:before {\n  content: \"\\f02b\";\n}\n.fa-tags:before {\n  content: \"\\f02c\";\n}\n.fa-book:before {\n  content: \"\\f02d\";\n}\n.fa-bookmark:before {\n  content: \"\\f02e\";\n}\n.fa-print:before {\n  content: \"\\f02f\";\n}\n.fa-camera:before {\n  content: \"\\f030\";\n}\n.fa-font:before {\n  content: \"\\f031\";\n}\n.fa-bold:before {\n  content: \"\\f032\";\n}\n.fa-italic:before {\n  content: \"\\f033\";\n}\n.fa-text-height:before {\n  content: \"\\f034\";\n}\n.fa-text-width:before {\n  content: \"\\f035\";\n}\n.fa-align-left:before {\n  content: \"\\f036\";\n}\n.fa-align-center:before {\n  content: \"\\f037\";\n}\n.fa-align-right:before {\n  content: \"\\f038\";\n}\n.fa-align-justify:before {\n  content: \"\\f039\";\n}\n.fa-list:before {\n  content: \"\\f03a\";\n}\n.fa-dedent:before,\n.fa-outdent:before {\n  content: \"\\f03b\";\n}\n.fa-indent:before {\n  content: \"\\f03c\";\n}\n.fa-video-camera:before {\n  content: \"\\f03d\";\n}\n.fa-photo:before,\n.fa-image:before,\n.fa-picture-o:before {\n  content: \"\\f03e\";\n}\n.fa-pencil:before {\n  content: \"\\f040\";\n}\n.fa-map-marker:before {\n  content: \"\\f041\";\n}\n.fa-adjust:before {\n  content: \"\\f042\";\n}\n.fa-tint:before {\n  content: \"\\f043\";\n}\n.fa-edit:before,\n.fa-pencil-square-o:before {\n  content: \"\\f044\";\n}\n.fa-share-square-o:before {\n  content: \"\\f045\";\n}\n.fa-check-square-o:before {\n  content: \"\\f046\";\n}\n.fa-arrows:before {\n  content: \"\\f047\";\n}\n.fa-step-backward:before {\n  content: \"\\f048\";\n}\n.fa-fast-backward:before {\n  content: \"\\f049\";\n}\n.fa-backward:before {\n  content: \"\\f04a\";\n}\n.fa-play:before {\n  content: \"\\f04b\";\n}\n.fa-pause:before {\n  content: \"\\f04c\";\n}\n.fa-stop:before {\n  content: \"\\f04d\";\n}\n.fa-forward:before {\n  content: \"\\f04e\";\n}\n.fa-fast-forward:before {\n  content: \"\\f050\";\n}\n.fa-step-forward:before {\n  content: \"\\f051\";\n}\n.fa-eject:before {\n  content: \"\\f052\";\n}\n.fa-chevron-left:before {\n  content: \"\\f053\";\n}\n.fa-chevron-right:before {\n  content: \"\\f054\";\n}\n.fa-plus-circle:before {\n  content: \"\\f055\";\n}\n.fa-minus-circle:before {\n  content: \"\\f056\";\n}\n.fa-times-circle:before {\n  content: \"\\f057\";\n}\n.fa-check-circle:before {\n  content: \"\\f058\";\n}\n.fa-question-circle:before {\n  content: \"\\f059\";\n}\n.fa-info-circle:before {\n  content: \"\\f05a\";\n}\n.fa-crosshairs:before {\n  content: \"\\f05b\";\n}\n.fa-times-circle-o:before {\n  content: \"\\f05c\";\n}\n.fa-check-circle-o:before {\n  content: \"\\f05d\";\n}\n.fa-ban:before {\n  content: \"\\f05e\";\n}\n.fa-arrow-left:before {\n  content: \"\\f060\";\n}\n.fa-arrow-right:before {\n  content: \"\\f061\";\n}\n.fa-arrow-up:before {\n  content: \"\\f062\";\n}\n.fa-arrow-down:before {\n  content: \"\\f063\";\n}\n.fa-mail-forward:before,\n.fa-share:before {\n  content: \"\\f064\";\n}\n.fa-expand:before {\n  content: \"\\f065\";\n}\n.fa-compress:before {\n  content: \"\\f066\";\n}\n.fa-plus:before {\n  content: \"\\f067\";\n}\n.fa-minus:before {\n  content: \"\\f068\";\n}\n.fa-asterisk:before {\n  content: \"\\f069\";\n}\n.fa-exclamation-circle:before {\n  content: \"\\f06a\";\n}\n.fa-gift:before {\n  content: \"\\f06b\";\n}\n.fa-leaf:before {\n  content: \"\\f06c\";\n}\n.fa-fire:before {\n  content: \"\\f06d\";\n}\n.fa-eye:before {\n  content: \"\\f06e\";\n}\n.fa-eye-slash:before {\n  content: \"\\f070\";\n}\n.fa-warning:before,\n.fa-exclamation-triangle:before {\n  content: \"\\f071\";\n}\n.fa-plane:before {\n  content: \"\\f072\";\n}\n.fa-calendar:before {\n  content: \"\\f073\";\n}\n.fa-random:before {\n  content: \"\\f074\";\n}\n.fa-comment:before {\n  content: \"\\f075\";\n}\n.fa-magnet:before {\n  content: \"\\f076\";\n}\n.fa-chevron-up:before {\n  content: \"\\f077\";\n}\n.fa-chevron-down:before {\n  content: \"\\f078\";\n}\n.fa-retweet:before {\n  content: \"\\f079\";\n}\n.fa-shopping-cart:before {\n  content: \"\\f07a\";\n}\n.fa-folder:before {\n  content: \"\\f07b\";\n}\n.fa-folder-open:before {\n  content: \"\\f07c\";\n}\n.fa-arrows-v:before {\n  content: \"\\f07d\";\n}\n.fa-arrows-h:before {\n  content: \"\\f07e\";\n}\n.fa-bar-chart-o:before,\n.fa-bar-chart:before {\n  content: \"\\f080\";\n}\n.fa-twitter-square:before {\n  content: \"\\f081\";\n}\n.fa-facebook-square:before {\n  content: \"\\f082\";\n}\n.fa-camera-retro:before {\n  content: \"\\f083\";\n}\n.fa-key:before {\n  content: \"\\f084\";\n}\n.fa-gears:before,\n.fa-cogs:before {\n  content: \"\\f085\";\n}\n.fa-comments:before {\n  content: \"\\f086\";\n}\n.fa-thumbs-o-up:before {\n  content: \"\\f087\";\n}\n.fa-thumbs-o-down:before {\n  content: \"\\f088\";\n}\n.fa-star-half:before {\n  content: \"\\f089\";\n}\n.fa-heart-o:before {\n  content: \"\\f08a\";\n}\n.fa-sign-out:before {\n  content: \"\\f08b\";\n}\n.fa-linkedin-square:before {\n  content: \"\\f08c\";\n}\n.fa-thumb-tack:before {\n  content: \"\\f08d\";\n}\n.fa-external-link:before {\n  content: \"\\f08e\";\n}\n.fa-sign-in:before {\n  content: \"\\f090\";\n}\n.fa-trophy:before {\n  content: \"\\f091\";\n}\n.fa-github-square:before {\n  content: \"\\f092\";\n}\n.fa-upload:before {\n  content: \"\\f093\";\n}\n.fa-lemon-o:before {\n  content: \"\\f094\";\n}\n.fa-phone:before {\n  content: \"\\f095\";\n}\n.fa-square-o:before {\n  content: \"\\f096\";\n}\n.fa-bookmark-o:before {\n  content: \"\\f097\";\n}\n.fa-phone-square:before {\n  content: \"\\f098\";\n}\n.fa-twitter:before {\n  content: \"\\f099\";\n}\n.fa-facebook-f:before,\n.fa-facebook:before {\n  content: \"\\f09a\";\n}\n.fa-github:before {\n  content: \"\\f09b\";\n}\n.fa-unlock:before {\n  content: \"\\f09c\";\n}\n.fa-credit-card:before {\n  content: \"\\f09d\";\n}\n.fa-feed:before,\n.fa-rss:before {\n  content: \"\\f09e\";\n}\n.fa-hdd-o:before {\n  content: \"\\f0a0\";\n}\n.fa-bullhorn:before {\n  content: \"\\f0a1\";\n}\n.fa-bell:before {\n  content: \"\\f0f3\";\n}\n.fa-certificate:before {\n  content: \"\\f0a3\";\n}\n.fa-hand-o-right:before {\n  content: \"\\f0a4\";\n}\n.fa-hand-o-left:before {\n  content: \"\\f0a5\";\n}\n.fa-hand-o-up:before {\n  content: \"\\f0a6\";\n}\n.fa-hand-o-down:before {\n  content: \"\\f0a7\";\n}\n.fa-arrow-circle-left:before {\n  content: \"\\f0a8\";\n}\n.fa-arrow-circle-right:before {\n  content: \"\\f0a9\";\n}\n.fa-arrow-circle-up:before {\n  content: \"\\f0aa\";\n}\n.fa-arrow-circle-down:before {\n  content: \"\\f0ab\";\n}\n.fa-globe:before {\n  content: \"\\f0ac\";\n}\n.fa-wrench:before {\n  content: \"\\f0ad\";\n}\n.fa-tasks:before {\n  content: \"\\f0ae\";\n}\n.fa-filter:before {\n  content: \"\\f0b0\";\n}\n.fa-briefcase:before {\n  content: \"\\f0b1\";\n}\n.fa-arrows-alt:before {\n  content: \"\\f0b2\";\n}\n.fa-group:before,\n.fa-users:before {\n  content: \"\\f0c0\";\n}\n.fa-chain:before,\n.fa-link:before {\n  content: \"\\f0c1\";\n}\n.fa-cloud:before {\n  content: \"\\f0c2\";\n}\n.fa-flask:before {\n  content: \"\\f0c3\";\n}\n.fa-cut:before,\n.fa-scissors:before {\n  content: \"\\f0c4\";\n}\n.fa-copy:before,\n.fa-files-o:before {\n  content: \"\\f0c5\";\n}\n.fa-paperclip:before {\n  content: \"\\f0c6\";\n}\n.fa-save:before,\n.fa-floppy-o:before {\n  content: \"\\f0c7\";\n}\n.fa-square:before {\n  content: \"\\f0c8\";\n}\n.fa-navicon:before,\n.fa-reorder:before,\n.fa-bars:before {\n  content: \"\\f0c9\";\n}\n.fa-list-ul:before {\n  content: \"\\f0ca\";\n}\n.fa-list-ol:before {\n  content: \"\\f0cb\";\n}\n.fa-strikethrough:before {\n  content: \"\\f0cc\";\n}\n.fa-underline:before {\n  content: \"\\f0cd\";\n}\n.fa-table:before {\n  content: \"\\f0ce\";\n}\n.fa-magic:before {\n  content: \"\\f0d0\";\n}\n.fa-truck:before {\n  content: \"\\f0d1\";\n}\n.fa-pinterest:before {\n  content: \"\\f0d2\";\n}\n.fa-pinterest-square:before {\n  content: \"\\f0d3\";\n}\n.fa-google-plus-square:before {\n  content: \"\\f0d4\";\n}\n.fa-google-plus:before {\n  content: \"\\f0d5\";\n}\n.fa-money:before {\n  content: \"\\f0d6\";\n}\n.fa-caret-down:before {\n  content: \"\\f0d7\";\n}\n.fa-caret-up:before {\n  content: \"\\f0d8\";\n}\n.fa-caret-left:before {\n  content: \"\\f0d9\";\n}\n.fa-caret-right:before {\n  content: \"\\f0da\";\n}\n.fa-columns:before {\n  content: \"\\f0db\";\n}\n.fa-unsorted:before,\n.fa-sort:before {\n  content: \"\\f0dc\";\n}\n.fa-sort-down:before,\n.fa-sort-desc:before {\n  content: \"\\f0dd\";\n}\n.fa-sort-up:before,\n.fa-sort-asc:before {\n  content: \"\\f0de\";\n}\n.fa-envelope:before {\n  content: \"\\f0e0\";\n}\n.fa-linkedin:before {\n  content: \"\\f0e1\";\n}\n.fa-rotate-left:before,\n.fa-undo:before {\n  content: \"\\f0e2\";\n}\n.fa-legal:before,\n.fa-gavel:before {\n  content: \"\\f0e3\";\n}\n.fa-dashboard:before,\n.fa-tachometer:before {\n  content: \"\\f0e4\";\n}\n.fa-comment-o:before {\n  content: \"\\f0e5\";\n}\n.fa-comments-o:before {\n  content: \"\\f0e6\";\n}\n.fa-flash:before,\n.fa-bolt:before {\n  content: \"\\f0e7\";\n}\n.fa-sitemap:before {\n  content: \"\\f0e8\";\n}\n.fa-umbrella:before {\n  content: \"\\f0e9\";\n}\n.fa-paste:before,\n.fa-clipboard:before {\n  content: \"\\f0ea\";\n}\n.fa-lightbulb-o:before {\n  content: \"\\f0eb\";\n}\n.fa-exchange:before {\n  content: \"\\f0ec\";\n}\n.fa-cloud-download:before {\n  content: \"\\f0ed\";\n}\n.fa-cloud-upload:before {\n  content: \"\\f0ee\";\n}\n.fa-user-md:before {\n  content: \"\\f0f0\";\n}\n.fa-stethoscope:before {\n  content: \"\\f0f1\";\n}\n.fa-suitcase:before {\n  content: \"\\f0f2\";\n}\n.fa-bell-o:before {\n  content: \"\\f0a2\";\n}\n.fa-coffee:before {\n  content: \"\\f0f4\";\n}\n.fa-cutlery:before {\n  content: \"\\f0f5\";\n}\n.fa-file-text-o:before {\n  content: \"\\f0f6\";\n}\n.fa-building-o:before {\n  content: \"\\f0f7\";\n}\n.fa-hospital-o:before {\n  content: \"\\f0f8\";\n}\n.fa-ambulance:before {\n  content: \"\\f0f9\";\n}\n.fa-medkit:before {\n  content: \"\\f0fa\";\n}\n.fa-fighter-jet:before {\n  content: \"\\f0fb\";\n}\n.fa-beer:before {\n  content: \"\\f0fc\";\n}\n.fa-h-square:before {\n  content: \"\\f0fd\";\n}\n.fa-plus-square:before {\n  content: \"\\f0fe\";\n}\n.fa-angle-double-left:before {\n  content: \"\\f100\";\n}\n.fa-angle-double-right:before {\n  content: \"\\f101\";\n}\n.fa-angle-double-up:before {\n  content: \"\\f102\";\n}\n.fa-angle-double-down:before {\n  content: \"\\f103\";\n}\n.fa-angle-left:before {\n  content: \"\\f104\";\n}\n.fa-angle-right:before {\n  content: \"\\f105\";\n}\n.fa-angle-up:before {\n  content: \"\\f106\";\n}\n.fa-angle-down:before {\n  content: \"\\f107\";\n}\n.fa-desktop:before {\n  content: \"\\f108\";\n}\n.fa-laptop:before {\n  content: \"\\f109\";\n}\n.fa-tablet:before {\n  content: \"\\f10a\";\n}\n.fa-mobile-phone:before,\n.fa-mobile:before {\n  content: \"\\f10b\";\n}\n.fa-circle-o:before {\n  content: \"\\f10c\";\n}\n.fa-quote-left:before {\n  content: \"\\f10d\";\n}\n.fa-quote-right:before {\n  content: \"\\f10e\";\n}\n.fa-spinner:before {\n  content: \"\\f110\";\n}\n.fa-circle:before {\n  content: \"\\f111\";\n}\n.fa-mail-reply:before,\n.fa-reply:before {\n  content: \"\\f112\";\n}\n.fa-github-alt:before {\n  content: \"\\f113\";\n}\n.fa-folder-o:before {\n  content: \"\\f114\";\n}\n.fa-folder-open-o:before {\n  content: \"\\f115\";\n}\n.fa-smile-o:before {\n  content: \"\\f118\";\n}\n.fa-frown-o:before {\n  content: \"\\f119\";\n}\n.fa-meh-o:before {\n  content: \"\\f11a\";\n}\n.fa-gamepad:before {\n  content: \"\\f11b\";\n}\n.fa-keyboard-o:before {\n  content: \"\\f11c\";\n}\n.fa-flag-o:before {\n  content: \"\\f11d\";\n}\n.fa-flag-checkered:before {\n  content: \"\\f11e\";\n}\n.fa-terminal:before {\n  content: \"\\f120\";\n}\n.fa-code:before {\n  content: \"\\f121\";\n}\n.fa-mail-reply-all:before,\n.fa-reply-all:before {\n  content: \"\\f122\";\n}\n.fa-star-half-empty:before,\n.fa-star-half-full:before,\n.fa-star-half-o:before {\n  content: \"\\f123\";\n}\n.fa-location-arrow:before {\n  content: \"\\f124\";\n}\n.fa-crop:before {\n  content: \"\\f125\";\n}\n.fa-code-fork:before {\n  content: \"\\f126\";\n}\n.fa-unlink:before,\n.fa-chain-broken:before {\n  content: \"\\f127\";\n}\n.fa-question:before {\n  content: \"\\f128\";\n}\n.fa-info:before {\n  content: \"\\f129\";\n}\n.fa-exclamation:before {\n  content: \"\\f12a\";\n}\n.fa-superscript:before {\n  content: \"\\f12b\";\n}\n.fa-subscript:before {\n  content: \"\\f12c\";\n}\n.fa-eraser:before {\n  content: \"\\f12d\";\n}\n.fa-puzzle-piece:before {\n  content: \"\\f12e\";\n}\n.fa-microphone:before {\n  content: \"\\f130\";\n}\n.fa-microphone-slash:before {\n  content: \"\\f131\";\n}\n.fa-shield:before {\n  content: \"\\f132\";\n}\n.fa-calendar-o:before {\n  content: \"\\f133\";\n}\n.fa-fire-extinguisher:before {\n  content: \"\\f134\";\n}\n.fa-rocket:before {\n  content: \"\\f135\";\n}\n.fa-maxcdn:before {\n  content: \"\\f136\";\n}\n.fa-chevron-circle-left:before {\n  content: \"\\f137\";\n}\n.fa-chevron-circle-right:before {\n  content: \"\\f138\";\n}\n.fa-chevron-circle-up:before {\n  content: \"\\f139\";\n}\n.fa-chevron-circle-down:before {\n  content: \"\\f13a\";\n}\n.fa-html5:before {\n  content: \"\\f13b\";\n}\n.fa-css3:before {\n  content: \"\\f13c\";\n}\n.fa-anchor:before {\n  content: \"\\f13d\";\n}\n.fa-unlock-alt:before {\n  content: \"\\f13e\";\n}\n.fa-bullseye:before {\n  content: \"\\f140\";\n}\n.fa-ellipsis-h:before {\n  content: \"\\f141\";\n}\n.fa-ellipsis-v:before {\n  content: \"\\f142\";\n}\n.fa-rss-square:before {\n  content: \"\\f143\";\n}\n.fa-play-circle:before {\n  content: \"\\f144\";\n}\n.fa-ticket:before {\n  content: \"\\f145\";\n}\n.fa-minus-square:before {\n  content: \"\\f146\";\n}\n.fa-minus-square-o:before {\n  content: \"\\f147\";\n}\n.fa-level-up:before {\n  content: \"\\f148\";\n}\n.fa-level-down:before {\n  content: \"\\f149\";\n}\n.fa-check-square:before {\n  content: \"\\f14a\";\n}\n.fa-pencil-square:before {\n  content: \"\\f14b\";\n}\n.fa-external-link-square:before {\n  content: \"\\f14c\";\n}\n.fa-share-square:before {\n  content: \"\\f14d\";\n}\n.fa-compass:before {\n  content: \"\\f14e\";\n}\n.fa-toggle-down:before,\n.fa-caret-square-o-down:before {\n  content: \"\\f150\";\n}\n.fa-toggle-up:before,\n.fa-caret-square-o-up:before {\n  content: \"\\f151\";\n}\n.fa-toggle-right:before,\n.fa-caret-square-o-right:before {\n  content: \"\\f152\";\n}\n.fa-euro:before,\n.fa-eur:before {\n  content: \"\\f153\";\n}\n.fa-gbp:before {\n  content: \"\\f154\";\n}\n.fa-dollar:before,\n.fa-usd:before {\n  content: \"\\f155\";\n}\n.fa-rupee:before,\n.fa-inr:before {\n  content: \"\\f156\";\n}\n.fa-cny:before,\n.fa-rmb:before,\n.fa-yen:before,\n.fa-jpy:before {\n  content: \"\\f157\";\n}\n.fa-ruble:before,\n.fa-rouble:before,\n.fa-rub:before {\n  content: \"\\f158\";\n}\n.fa-won:before,\n.fa-krw:before {\n  content: \"\\f159\";\n}\n.fa-bitcoin:before,\n.fa-btc:before {\n  content: \"\\f15a\";\n}\n.fa-file:before {\n  content: \"\\f15b\";\n}\n.fa-file-text:before {\n  content: \"\\f15c\";\n}\n.fa-sort-alpha-asc:before {\n  content: \"\\f15d\";\n}\n.fa-sort-alpha-desc:before {\n  content: \"\\f15e\";\n}\n.fa-sort-amount-asc:before {\n  content: \"\\f160\";\n}\n.fa-sort-amount-desc:before {\n  content: \"\\f161\";\n}\n.fa-sort-numeric-asc:before {\n  content: \"\\f162\";\n}\n.fa-sort-numeric-desc:before {\n  content: \"\\f163\";\n}\n.fa-thumbs-up:before {\n  content: \"\\f164\";\n}\n.fa-thumbs-down:before {\n  content: \"\\f165\";\n}\n.fa-youtube-square:before {\n  content: \"\\f166\";\n}\n.fa-youtube:before {\n  content: \"\\f167\";\n}\n.fa-xing:before {\n  content: \"\\f168\";\n}\n.fa-xing-square:before {\n  content: \"\\f169\";\n}\n.fa-youtube-play:before {\n  content: \"\\f16a\";\n}\n.fa-dropbox:before {\n  content: \"\\f16b\";\n}\n.fa-stack-overflow:before {\n  content: \"\\f16c\";\n}\n.fa-instagram:before {\n  content: \"\\f16d\";\n}\n.fa-flickr:before {\n  content: \"\\f16e\";\n}\n.fa-adn:before {\n  content: \"\\f170\";\n}\n.fa-bitbucket:before {\n  content: \"\\f171\";\n}\n.fa-bitbucket-square:before {\n  content: \"\\f172\";\n}\n.fa-tumblr:before {\n  content: \"\\f173\";\n}\n.fa-tumblr-square:before {\n  content: \"\\f174\";\n}\n.fa-long-arrow-down:before {\n  content: \"\\f175\";\n}\n.fa-long-arrow-up:before {\n  content: \"\\f176\";\n}\n.fa-long-arrow-left:before {\n  content: \"\\f177\";\n}\n.fa-long-arrow-right:before {\n  content: \"\\f178\";\n}\n.fa-apple:before {\n  content: \"\\f179\";\n}\n.fa-windows:before {\n  content: \"\\f17a\";\n}\n.fa-android:before {\n  content: \"\\f17b\";\n}\n.fa-linux:before {\n  content: \"\\f17c\";\n}\n.fa-dribbble:before {\n  content: \"\\f17d\";\n}\n.fa-skype:before {\n  content: \"\\f17e\";\n}\n.fa-foursquare:before {\n  content: \"\\f180\";\n}\n.fa-trello:before {\n  content: \"\\f181\";\n}\n.fa-female:before {\n  content: \"\\f182\";\n}\n.fa-male:before {\n  content: \"\\f183\";\n}\n.fa-gittip:before,\n.fa-gratipay:before {\n  content: \"\\f184\";\n}\n.fa-sun-o:before {\n  content: \"\\f185\";\n}\n.fa-moon-o:before {\n  content: \"\\f186\";\n}\n.fa-archive:before {\n  content: \"\\f187\";\n}\n.fa-bug:before {\n  content: \"\\f188\";\n}\n.fa-vk:before {\n  content: \"\\f189\";\n}\n.fa-weibo:before {\n  content: \"\\f18a\";\n}\n.fa-renren:before {\n  content: \"\\f18b\";\n}\n.fa-pagelines:before {\n  content: \"\\f18c\";\n}\n.fa-stack-exchange:before {\n  content: \"\\f18d\";\n}\n.fa-arrow-circle-o-right:before {\n  content: \"\\f18e\";\n}\n.fa-arrow-circle-o-left:before {\n  content: \"\\f190\";\n}\n.fa-toggle-left:before,\n.fa-caret-square-o-left:before {\n  content: \"\\f191\";\n}\n.fa-dot-circle-o:before {\n  content: \"\\f192\";\n}\n.fa-wheelchair:before {\n  content: \"\\f193\";\n}\n.fa-vimeo-square:before {\n  content: \"\\f194\";\n}\n.fa-turkish-lira:before,\n.fa-try:before {\n  content: \"\\f195\";\n}\n.fa-plus-square-o:before {\n  content: \"\\f196\";\n}\n.fa-space-shuttle:before {\n  content: \"\\f197\";\n}\n.fa-slack:before {\n  content: \"\\f198\";\n}\n.fa-envelope-square:before {\n  content: \"\\f199\";\n}\n.fa-wordpress:before {\n  content: \"\\f19a\";\n}\n.fa-openid:before {\n  content: \"\\f19b\";\n}\n.fa-institution:before,\n.fa-bank:before,\n.fa-university:before {\n  content: \"\\f19c\";\n}\n.fa-mortar-board:before,\n.fa-graduation-cap:before {\n  content: \"\\f19d\";\n}\n.fa-yahoo:before {\n  content: \"\\f19e\";\n}\n.fa-google:before {\n  content: \"\\f1a0\";\n}\n.fa-reddit:before {\n  content: \"\\f1a1\";\n}\n.fa-reddit-square:before {\n  content: \"\\f1a2\";\n}\n.fa-stumbleupon-circle:before {\n  content: \"\\f1a3\";\n}\n.fa-stumbleupon:before {\n  content: \"\\f1a4\";\n}\n.fa-delicious:before {\n  content: \"\\f1a5\";\n}\n.fa-digg:before {\n  content: \"\\f1a6\";\n}\n.fa-pied-piper-pp:before {\n  content: \"\\f1a7\";\n}\n.fa-pied-piper-alt:before {\n  content: \"\\f1a8\";\n}\n.fa-drupal:before {\n  content: \"\\f1a9\";\n}\n.fa-joomla:before {\n  content: \"\\f1aa\";\n}\n.fa-language:before {\n  content: \"\\f1ab\";\n}\n.fa-fax:before {\n  content: \"\\f1ac\";\n}\n.fa-building:before {\n  content: \"\\f1ad\";\n}\n.fa-child:before {\n  content: \"\\f1ae\";\n}\n.fa-paw:before {\n  content: \"\\f1b0\";\n}\n.fa-spoon:before {\n  content: \"\\f1b1\";\n}\n.fa-cube:before {\n  content: \"\\f1b2\";\n}\n.fa-cubes:before {\n  content: \"\\f1b3\";\n}\n.fa-behance:before {\n  content: \"\\f1b4\";\n}\n.fa-behance-square:before {\n  content: \"\\f1b5\";\n}\n.fa-steam:before {\n  content: \"\\f1b6\";\n}\n.fa-steam-square:before {\n  content: \"\\f1b7\";\n}\n.fa-recycle:before {\n  content: \"\\f1b8\";\n}\n.fa-automobile:before,\n.fa-car:before {\n  content: \"\\f1b9\";\n}\n.fa-cab:before,\n.fa-taxi:before {\n  content: \"\\f1ba\";\n}\n.fa-tree:before {\n  content: \"\\f1bb\";\n}\n.fa-spotify:before {\n  content: \"\\f1bc\";\n}\n.fa-deviantart:before {\n  content: \"\\f1bd\";\n}\n.fa-soundcloud:before {\n  content: \"\\f1be\";\n}\n.fa-database:before {\n  content: \"\\f1c0\";\n}\n.fa-file-pdf-o:before {\n  content: \"\\f1c1\";\n}\n.fa-file-word-o:before {\n  content: \"\\f1c2\";\n}\n.fa-file-excel-o:before {\n  content: \"\\f1c3\";\n}\n.fa-file-powerpoint-o:before {\n  content: \"\\f1c4\";\n}\n.fa-file-photo-o:before,\n.fa-file-picture-o:before,\n.fa-file-image-o:before {\n  content: \"\\f1c5\";\n}\n.fa-file-zip-o:before,\n.fa-file-archive-o:before {\n  content: \"\\f1c6\";\n}\n.fa-file-sound-o:before,\n.fa-file-audio-o:before {\n  content: \"\\f1c7\";\n}\n.fa-file-movie-o:before,\n.fa-file-video-o:before {\n  content: \"\\f1c8\";\n}\n.fa-file-code-o:before {\n  content: \"\\f1c9\";\n}\n.fa-vine:before {\n  content: \"\\f1ca\";\n}\n.fa-codepen:before {\n  content: \"\\f1cb\";\n}\n.fa-jsfiddle:before {\n  content: \"\\f1cc\";\n}\n.fa-life-bouy:before,\n.fa-life-buoy:before,\n.fa-life-saver:before,\n.fa-support:before,\n.fa-life-ring:before {\n  content: \"\\f1cd\";\n}\n.fa-circle-o-notch:before {\n  content: \"\\f1ce\";\n}\n.fa-ra:before,\n.fa-resistance:before,\n.fa-rebel:before {\n  content: \"\\f1d0\";\n}\n.fa-ge:before,\n.fa-empire:before {\n  content: \"\\f1d1\";\n}\n.fa-git-square:before {\n  content: \"\\f1d2\";\n}\n.fa-git:before {\n  content: \"\\f1d3\";\n}\n.fa-y-combinator-square:before,\n.fa-yc-square:before,\n.fa-hacker-news:before {\n  content: \"\\f1d4\";\n}\n.fa-tencent-weibo:before {\n  content: \"\\f1d5\";\n}\n.fa-qq:before {\n  content: \"\\f1d6\";\n}\n.fa-wechat:before,\n.fa-weixin:before {\n  content: \"\\f1d7\";\n}\n.fa-send:before,\n.fa-paper-plane:before {\n  content: \"\\f1d8\";\n}\n.fa-send-o:before,\n.fa-paper-plane-o:before {\n  content: \"\\f1d9\";\n}\n.fa-history:before {\n  content: \"\\f1da\";\n}\n.fa-circle-thin:before {\n  content: \"\\f1db\";\n}\n.fa-header:before {\n  content: \"\\f1dc\";\n}\n.fa-paragraph:before {\n  content: \"\\f1dd\";\n}\n.fa-sliders:before {\n  content: \"\\f1de\";\n}\n.fa-share-alt:before {\n  content: \"\\f1e0\";\n}\n.fa-share-alt-square:before {\n  content: \"\\f1e1\";\n}\n.fa-bomb:before {\n  content: \"\\f1e2\";\n}\n.fa-soccer-ball-o:before,\n.fa-futbol-o:before {\n  content: \"\\f1e3\";\n}\n.fa-tty:before {\n  content: \"\\f1e4\";\n}\n.fa-binoculars:before {\n  content: \"\\f1e5\";\n}\n.fa-plug:before {\n  content: \"\\f1e6\";\n}\n.fa-slideshare:before {\n  content: \"\\f1e7\";\n}\n.fa-twitch:before {\n  content: \"\\f1e8\";\n}\n.fa-yelp:before {\n  content: \"\\f1e9\";\n}\n.fa-newspaper-o:before {\n  content: \"\\f1ea\";\n}\n.fa-wifi:before {\n  content: \"\\f1eb\";\n}\n.fa-calculator:before {\n  content: \"\\f1ec\";\n}\n.fa-paypal:before {\n  content: \"\\f1ed\";\n}\n.fa-google-wallet:before {\n  content: \"\\f1ee\";\n}\n.fa-cc-visa:before {\n  content: \"\\f1f0\";\n}\n.fa-cc-mastercard:before {\n  content: \"\\f1f1\";\n}\n.fa-cc-discover:before {\n  content: \"\\f1f2\";\n}\n.fa-cc-amex:before {\n  content: \"\\f1f3\";\n}\n.fa-cc-paypal:before {\n  content: \"\\f1f4\";\n}\n.fa-cc-stripe:before {\n  content: \"\\f1f5\";\n}\n.fa-bell-slash:before {\n  content: \"\\f1f6\";\n}\n.fa-bell-slash-o:before {\n  content: \"\\f1f7\";\n}\n.fa-trash:before {\n  content: \"\\f1f8\";\n}\n.fa-copyright:before {\n  content: \"\\f1f9\";\n}\n.fa-at:before {\n  content: \"\\f1fa\";\n}\n.fa-eyedropper:before {\n  content: \"\\f1fb\";\n}\n.fa-paint-brush:before {\n  content: \"\\f1fc\";\n}\n.fa-birthday-cake:before {\n  content: \"\\f1fd\";\n}\n.fa-area-chart:before {\n  content: \"\\f1fe\";\n}\n.fa-pie-chart:before {\n  content: \"\\f200\";\n}\n.fa-line-chart:before {\n  content: \"\\f201\";\n}\n.fa-lastfm:before {\n  content: \"\\f202\";\n}\n.fa-lastfm-square:before {\n  content: \"\\f203\";\n}\n.fa-toggle-off:before {\n  content: \"\\f204\";\n}\n.fa-toggle-on:before {\n  content: \"\\f205\";\n}\n.fa-bicycle:before {\n  content: \"\\f206\";\n}\n.fa-bus:before {\n  content: \"\\f207\";\n}\n.fa-ioxhost:before {\n  content: \"\\f208\";\n}\n.fa-angellist:before {\n  content: \"\\f209\";\n}\n.fa-cc:before {\n  content: \"\\f20a\";\n}\n.fa-shekel:before,\n.fa-sheqel:before,\n.fa-ils:before {\n  content: \"\\f20b\";\n}\n.fa-meanpath:before {\n  content: \"\\f20c\";\n}\n.fa-buysellads:before {\n  content: \"\\f20d\";\n}\n.fa-connectdevelop:before {\n  content: \"\\f20e\";\n}\n.fa-dashcube:before {\n  content: \"\\f210\";\n}\n.fa-forumbee:before {\n  content: \"\\f211\";\n}\n.fa-leanpub:before {\n  content: \"\\f212\";\n}\n.fa-sellsy:before {\n  content: \"\\f213\";\n}\n.fa-shirtsinbulk:before {\n  content: \"\\f214\";\n}\n.fa-simplybuilt:before {\n  content: \"\\f215\";\n}\n.fa-skyatlas:before {\n  content: \"\\f216\";\n}\n.fa-cart-plus:before {\n  content: \"\\f217\";\n}\n.fa-cart-arrow-down:before {\n  content: \"\\f218\";\n}\n.fa-diamond:before {\n  content: \"\\f219\";\n}\n.fa-ship:before {\n  content: \"\\f21a\";\n}\n.fa-user-secret:before {\n  content: \"\\f21b\";\n}\n.fa-motorcycle:before {\n  content: \"\\f21c\";\n}\n.fa-street-view:before {\n  content: \"\\f21d\";\n}\n.fa-heartbeat:before {\n  content: \"\\f21e\";\n}\n.fa-venus:before {\n  content: \"\\f221\";\n}\n.fa-mars:before {\n  content: \"\\f222\";\n}\n.fa-mercury:before {\n  content: \"\\f223\";\n}\n.fa-intersex:before,\n.fa-transgender:before {\n  content: \"\\f224\";\n}\n.fa-transgender-alt:before {\n  content: \"\\f225\";\n}\n.fa-venus-double:before {\n  content: \"\\f226\";\n}\n.fa-mars-double:before {\n  content: \"\\f227\";\n}\n.fa-venus-mars:before {\n  content: \"\\f228\";\n}\n.fa-mars-stroke:before {\n  content: \"\\f229\";\n}\n.fa-mars-stroke-v:before {\n  content: \"\\f22a\";\n}\n.fa-mars-stroke-h:before {\n  content: \"\\f22b\";\n}\n.fa-neuter:before {\n  content: \"\\f22c\";\n}\n.fa-genderless:before {\n  content: \"\\f22d\";\n}\n.fa-facebook-official:before {\n  content: \"\\f230\";\n}\n.fa-pinterest-p:before {\n  content: \"\\f231\";\n}\n.fa-whatsapp:before {\n  content: \"\\f232\";\n}\n.fa-server:before {\n  content: \"\\f233\";\n}\n.fa-user-plus:before {\n  content: \"\\f234\";\n}\n.fa-user-times:before {\n  content: \"\\f235\";\n}\n.fa-hotel:before,\n.fa-bed:before {\n  content: \"\\f236\";\n}\n.fa-viacoin:before {\n  content: \"\\f237\";\n}\n.fa-train:before {\n  content: \"\\f238\";\n}\n.fa-subway:before {\n  content: \"\\f239\";\n}\n.fa-medium:before {\n  content: \"\\f23a\";\n}\n.fa-yc:before,\n.fa-y-combinator:before {\n  content: \"\\f23b\";\n}\n.fa-optin-monster:before {\n  content: \"\\f23c\";\n}\n.fa-opencart:before {\n  content: \"\\f23d\";\n}\n.fa-expeditedssl:before {\n  content: \"\\f23e\";\n}\n.fa-battery-4:before,\n.fa-battery:before,\n.fa-battery-full:before {\n  content: \"\\f240\";\n}\n.fa-battery-3:before,\n.fa-battery-three-quarters:before {\n  content: \"\\f241\";\n}\n.fa-battery-2:before,\n.fa-battery-half:before {\n  content: \"\\f242\";\n}\n.fa-battery-1:before,\n.fa-battery-quarter:before {\n  content: \"\\f243\";\n}\n.fa-battery-0:before,\n.fa-battery-empty:before {\n  content: \"\\f244\";\n}\n.fa-mouse-pointer:before {\n  content: \"\\f245\";\n}\n.fa-i-cursor:before {\n  content: \"\\f246\";\n}\n.fa-object-group:before {\n  content: \"\\f247\";\n}\n.fa-object-ungroup:before {\n  content: \"\\f248\";\n}\n.fa-sticky-note:before {\n  content: \"\\f249\";\n}\n.fa-sticky-note-o:before {\n  content: \"\\f24a\";\n}\n.fa-cc-jcb:before {\n  content: \"\\f24b\";\n}\n.fa-cc-diners-club:before {\n  content: \"\\f24c\";\n}\n.fa-clone:before {\n  content: \"\\f24d\";\n}\n.fa-balance-scale:before {\n  content: \"\\f24e\";\n}\n.fa-hourglass-o:before {\n  content: \"\\f250\";\n}\n.fa-hourglass-1:before,\n.fa-hourglass-start:before {\n  content: \"\\f251\";\n}\n.fa-hourglass-2:before,\n.fa-hourglass-half:before {\n  content: \"\\f252\";\n}\n.fa-hourglass-3:before,\n.fa-hourglass-end:before {\n  content: \"\\f253\";\n}\n.fa-hourglass:before {\n  content: \"\\f254\";\n}\n.fa-hand-grab-o:before,\n.fa-hand-rock-o:before {\n  content: \"\\f255\";\n}\n.fa-hand-stop-o:before,\n.fa-hand-paper-o:before {\n  content: \"\\f256\";\n}\n.fa-hand-scissors-o:before {\n  content: \"\\f257\";\n}\n.fa-hand-lizard-o:before {\n  content: \"\\f258\";\n}\n.fa-hand-spock-o:before {\n  content: \"\\f259\";\n}\n.fa-hand-pointer-o:before {\n  content: \"\\f25a\";\n}\n.fa-hand-peace-o:before {\n  content: \"\\f25b\";\n}\n.fa-trademark:before {\n  content: \"\\f25c\";\n}\n.fa-registered:before {\n  content: \"\\f25d\";\n}\n.fa-creative-commons:before {\n  content: \"\\f25e\";\n}\n.fa-gg:before {\n  content: \"\\f260\";\n}\n.fa-gg-circle:before {\n  content: \"\\f261\";\n}\n.fa-tripadvisor:before {\n  content: \"\\f262\";\n}\n.fa-odnoklassniki:before {\n  content: \"\\f263\";\n}\n.fa-odnoklassniki-square:before {\n  content: \"\\f264\";\n}\n.fa-get-pocket:before {\n  content: \"\\f265\";\n}\n.fa-wikipedia-w:before {\n  content: \"\\f266\";\n}\n.fa-safari:before {\n  content: \"\\f267\";\n}\n.fa-chrome:before {\n  content: \"\\f268\";\n}\n.fa-firefox:before {\n  content: \"\\f269\";\n}\n.fa-opera:before {\n  content: \"\\f26a\";\n}\n.fa-internet-explorer:before {\n  content: \"\\f26b\";\n}\n.fa-tv:before,\n.fa-television:before {\n  content: \"\\f26c\";\n}\n.fa-contao:before {\n  content: \"\\f26d\";\n}\n.fa-500px:before {\n  content: \"\\f26e\";\n}\n.fa-amazon:before {\n  content: \"\\f270\";\n}\n.fa-calendar-plus-o:before {\n  content: \"\\f271\";\n}\n.fa-calendar-minus-o:before {\n  content: \"\\f272\";\n}\n.fa-calendar-times-o:before {\n  content: \"\\f273\";\n}\n.fa-calendar-check-o:before {\n  content: \"\\f274\";\n}\n.fa-industry:before {\n  content: \"\\f275\";\n}\n.fa-map-pin:before {\n  content: \"\\f276\";\n}\n.fa-map-signs:before {\n  content: \"\\f277\";\n}\n.fa-map-o:before {\n  content: \"\\f278\";\n}\n.fa-map:before {\n  content: \"\\f279\";\n}\n.fa-commenting:before {\n  content: \"\\f27a\";\n}\n.fa-commenting-o:before {\n  content: \"\\f27b\";\n}\n.fa-houzz:before {\n  content: \"\\f27c\";\n}\n.fa-vimeo:before {\n  content: \"\\f27d\";\n}\n.fa-black-tie:before {\n  content: \"\\f27e\";\n}\n.fa-fonticons:before {\n  content: \"\\f280\";\n}\n.fa-reddit-alien:before {\n  content: \"\\f281\";\n}\n.fa-edge:before {\n  content: \"\\f282\";\n}\n.fa-credit-card-alt:before {\n  content: \"\\f283\";\n}\n.fa-codiepie:before {\n  content: \"\\f284\";\n}\n.fa-modx:before {\n  content: \"\\f285\";\n}\n.fa-fort-awesome:before {\n  content: \"\\f286\";\n}\n.fa-usb:before {\n  content: \"\\f287\";\n}\n.fa-product-hunt:before {\n  content: \"\\f288\";\n}\n.fa-mixcloud:before {\n  content: \"\\f289\";\n}\n.fa-scribd:before {\n  content: \"\\f28a\";\n}\n.fa-pause-circle:before {\n  content: \"\\f28b\";\n}\n.fa-pause-circle-o:before {\n  content: \"\\f28c\";\n}\n.fa-stop-circle:before {\n  content: \"\\f28d\";\n}\n.fa-stop-circle-o:before {\n  content: \"\\f28e\";\n}\n.fa-shopping-bag:before {\n  content: \"\\f290\";\n}\n.fa-shopping-basket:before {\n  content: \"\\f291\";\n}\n.fa-hashtag:before {\n  content: \"\\f292\";\n}\n.fa-bluetooth:before {\n  content: \"\\f293\";\n}\n.fa-bluetooth-b:before {\n  content: \"\\f294\";\n}\n.fa-percent:before {\n  content: \"\\f295\";\n}\n.fa-gitlab:before {\n  content: \"\\f296\";\n}\n.fa-wpbeginner:before {\n  content: \"\\f297\";\n}\n.fa-wpforms:before {\n  content: \"\\f298\";\n}\n.fa-envira:before {\n  content: \"\\f299\";\n}\n.fa-universal-access:before {\n  content: \"\\f29a\";\n}\n.fa-wheelchair-alt:before {\n  content: \"\\f29b\";\n}\n.fa-question-circle-o:before {\n  content: \"\\f29c\";\n}\n.fa-blind:before {\n  content: \"\\f29d\";\n}\n.fa-audio-description:before {\n  content: \"\\f29e\";\n}\n.fa-volume-control-phone:before {\n  content: \"\\f2a0\";\n}\n.fa-braille:before {\n  content: \"\\f2a1\";\n}\n.fa-assistive-listening-systems:before {\n  content: \"\\f2a2\";\n}\n.fa-asl-interpreting:before,\n.fa-american-sign-language-interpreting:before {\n  content: \"\\f2a3\";\n}\n.fa-deafness:before,\n.fa-hard-of-hearing:before,\n.fa-deaf:before {\n  content: \"\\f2a4\";\n}\n.fa-glide:before {\n  content: \"\\f2a5\";\n}\n.fa-glide-g:before {\n  content: \"\\f2a6\";\n}\n.fa-signing:before,\n.fa-sign-language:before {\n  content: \"\\f2a7\";\n}\n.fa-low-vision:before {\n  content: \"\\f2a8\";\n}\n.fa-viadeo:before {\n  content: \"\\f2a9\";\n}\n.fa-viadeo-square:before {\n  content: \"\\f2aa\";\n}\n.fa-snapchat:before {\n  content: \"\\f2ab\";\n}\n.fa-snapchat-ghost:before {\n  content: \"\\f2ac\";\n}\n.fa-snapchat-square:before {\n  content: \"\\f2ad\";\n}\n.fa-pied-piper:before {\n  content: \"\\f2ae\";\n}\n.fa-first-order:before {\n  content: \"\\f2b0\";\n}\n.fa-yoast:before {\n  content: \"\\f2b1\";\n}\n.fa-themeisle:before {\n  content: \"\\f2b2\";\n}\n.fa-google-plus-circle:before,\n.fa-google-plus-official:before {\n  content: \"\\f2b3\";\n}\n.fa-fa:before,\n.fa-font-awesome:before {\n  content: \"\\f2b4\";\n}\n.fa-handshake-o:before {\n  content: \"\\f2b5\";\n}\n.fa-envelope-open:before {\n  content: \"\\f2b6\";\n}\n.fa-envelope-open-o:before {\n  content: \"\\f2b7\";\n}\n.fa-linode:before {\n  content: \"\\f2b8\";\n}\n.fa-address-book:before {\n  content: \"\\f2b9\";\n}\n.fa-address-book-o:before {\n  content: \"\\f2ba\";\n}\n.fa-vcard:before,\n.fa-address-card:before {\n  content: \"\\f2bb\";\n}\n.fa-vcard-o:before,\n.fa-address-card-o:before {\n  content: \"\\f2bc\";\n}\n.fa-user-circle:before {\n  content: \"\\f2bd\";\n}\n.fa-user-circle-o:before {\n  content: \"\\f2be\";\n}\n.fa-user-o:before {\n  content: \"\\f2c0\";\n}\n.fa-id-badge:before {\n  content: \"\\f2c1\";\n}\n.fa-drivers-license:before,\n.fa-id-card:before {\n  content: \"\\f2c2\";\n}\n.fa-drivers-license-o:before,\n.fa-id-card-o:before {\n  content: \"\\f2c3\";\n}\n.fa-quora:before {\n  content: \"\\f2c4\";\n}\n.fa-free-code-camp:before {\n  content: \"\\f2c5\";\n}\n.fa-telegram:before {\n  content: \"\\f2c6\";\n}\n.fa-thermometer-4:before,\n.fa-thermometer:before,\n.fa-thermometer-full:before {\n  content: \"\\f2c7\";\n}\n.fa-thermometer-3:before,\n.fa-thermometer-three-quarters:before {\n  content: \"\\f2c8\";\n}\n.fa-thermometer-2:before,\n.fa-thermometer-half:before {\n  content: \"\\f2c9\";\n}\n.fa-thermometer-1:before,\n.fa-thermometer-quarter:before {\n  content: \"\\f2ca\";\n}\n.fa-thermometer-0:before,\n.fa-thermometer-empty:before {\n  content: \"\\f2cb\";\n}\n.fa-shower:before {\n  content: \"\\f2cc\";\n}\n.fa-bathtub:before,\n.fa-s15:before,\n.fa-bath:before {\n  content: \"\\f2cd\";\n}\n.fa-podcast:before {\n  content: \"\\f2ce\";\n}\n.fa-window-maximize:before {\n  content: \"\\f2d0\";\n}\n.fa-window-minimize:before {\n  content: \"\\f2d1\";\n}\n.fa-window-restore:before {\n  content: \"\\f2d2\";\n}\n.fa-times-rectangle:before,\n.fa-window-close:before {\n  content: \"\\f2d3\";\n}\n.fa-times-rectangle-o:before,\n.fa-window-close-o:before {\n  content: \"\\f2d4\";\n}\n.fa-bandcamp:before {\n  content: \"\\f2d5\";\n}\n.fa-grav:before {\n  content: \"\\f2d6\";\n}\n.fa-etsy:before {\n  content: \"\\f2d7\";\n}\n.fa-imdb:before {\n  content: \"\\f2d8\";\n}\n.fa-ravelry:before {\n  content: \"\\f2d9\";\n}\n.fa-eercast:before {\n  content: \"\\f2da\";\n}\n.fa-microchip:before {\n  content: \"\\f2db\";\n}\n.fa-snowflake-o:before {\n  content: \"\\f2dc\";\n}\n.fa-superpowers:before {\n  content: \"\\f2dd\";\n}\n.fa-wpexplorer:before {\n  content: \"\\f2de\";\n}\n.fa-meetup:before {\n  content: \"\\f2e0\";\n}\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n  position: static;\n  width: auto;\n  height: auto;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n}\n"
  },
  {
    "path": "assets/scss/admin/_course-builder.scss",
    "content": "body.admin_page_llms-course-builder {\n\tbackground: #fff;\n\n\t#adminmenumain { display: none; }\n\t#wpbody-content { padding-bottom: 0; }\n\t#wpfooter { display: none; }\n\n\t#wpcontent, #wpfooter {\n\t\tmargin-left: 0;\n\t}\n\n\t.llms-button-secondary {\n\t\t.fa {\n\t\t\tmargin-right: 5px;\n\t\t}\n\t}\n\n\t// &.folded {\n\t// \t.llms-course-builder {\n\t// \t\tleft: 56px;\n\t// \t}\n\t// }\n\n\t.webui-popover {\n\t\t.select2-container--default {\n\t\t\t.select2-results__group {\n\t\t\t\tfont-size: 16px;\n\t\t\t}\n\t\t\t.select2-results__option .select2-results__option {\n\t\t\t\tpadding-left: 2em;\n\t\t\t}\n\t\t}\n\n\t}\n}\n\n.wrap.lifterlms.llms-builder {\n\tmargin: 0;\n\tpadding: 0;\n\tposition: relative;\n\n\n\t&.editor-active {\n\t\t.llms-video-explainer {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t.llms-builder-sidebar {\n\t\t\tpadding: 10px;\n\t\t\twidth: calc( 100% - 200px );\n\t\t\tz-index: 3;\n\t\t}\n\t\t@media only screen and ( min-width: 1200px ) {\n\t\t\t.llms-builder-main {\n\t\t\t\twidth: 560px;\n\t\t\t}\n\t\t\t.llms-builder-sidebar {\n\t\t\t\twidth: calc( 100% - 640px );\n\t\t\t}\n\t\t}\n\t\t@media only screen and ( min-width: 1440px ) {\n\t\t\t.llms-builder-main {\n\t\t\t\twidth: calc( 100% - 780px );\n\t\t\t}\n\t\t\t.llms-builder-sidebar {\n\t\t\t\twidth: 720px;\n\t\t\t}\n\t\t}\n\t\t@media only screen and ( min-width: 1680px ) {\n\t\t\t.llms-builder-main {\n\t\t\t\twidth: calc( 100% - 1000px );\n\t\t\t}\n\t\t\t.llms-builder-sidebar {\n\t\t\t\twidth: 940px;\n\t\t\t}\n\t\t}\n\t\t@media only screen and ( max-width: 782px ) {\n\t\t\t.llms-builder-sidebar {\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 0;\n\t\t\t\twidth: auto;\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-headline {\n\t\tdisplay: inline-block;\n\t\tfont-weight: 400;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t\ttransition: width 0.3s ease-in-out;\n\t\tvertical-align: middle;\n\t}\n\n\t.llms-builder-main {\n\t\tpadding: 30px 30px 30px 0;\n\t\tposition: relative;\n\t\twidth: calc( 100% - 450px );\n\t\tz-index: 2;\n\n\t\t.llms-action-icons {\n\t\t\tdisplay: inline-block;\n\t\t\tposition: relative;\n\t\t\tvertical-align: middle;\n\t\t\tbutton {\n\t\t\t\tbackground: none;\n\t\t\t\tcolor: inherit;\n\t\t\t\tborder: none;\n\t\t\t\tpadding: 0;\n\t\t\t\tcursor: pointer;\n\t\t\t\toutline: inherit;\n\t\t\t}\n\t\t}\n\n\t\t.llms-action-icons-lesson-id {\n\t\t\tvertical-align: top;\n\t\t}\n\n\t\t// Course\n\t\t.llms-course-header {\n\t\t\talign-items: center;\n\t\t\tdisplay: flex;\n\t\t\tflex-wrap: wrap;\n\t\t\tgap: 15px;\n\t\t\tposition: relative;\n\t\t\tz-index: 1;\n\t\t\t.llms-button-secondary {\n\t\t\t\tmargin-right: 10px;\n\t\t\t}\n\t\t}\n\n\n\t\t// Sections\n\t\tul.llms-sections {\n\t\t\tbox-shadow: 0 0 0 3px transparent;\n\t\t\tmin-height: 60px;\n\t\t\tpadding: 10px 0;\n\t\t\ttransition: box-shadow 0.6s ease, min-height 0.2s ease;\n\t\t\t&.dragging {\n\t\t\t\tbox-shadow: 0 0 0 3px $color-brand-blue;\n\t\t\t}\n\t\t}\n\n\t\t\tli.llms-section {\n\t\t\t\tbackground: #fff;\n\t\t\t\tborder: 1px solid #efefef;\n\t\t\t\tborder-radius: 6px;\n\t\t\t\tbox-shadow: 2px 2px 8px rgba( 0, 0, 0, 0.08 );\n\t\t\t\tposition: relative;\n\t\t\t\tmargin: 0 0 20px 0;\n\t\t\t\tpadding: 0;\n\n\t\t\t\t> .llms-builder-header {\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tgap: 15px;\n\t\t\t\t\tjustify-content: space-between;\n\t\t\t\t\tpadding: 20px 10px 20px 30px;\n\t\t\t\t\t.llms-action-icons {\n\t\t\t\t\t\talign-items: center;\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\tgap: 15px;\n\t\t\t\t\t\tjustify-content: space-between;\n\t\t\t\t\t\t.llms-action-icons-right {\n\t\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\t\tgap: 0px;\n\t\t\t\t\t\t\tmargin-top: -5px;\n\t\t\t\t\t\t\t.llms-action-icon {\n\t\t\t\t\t\t\t\tpadding: 5px 10px;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t.llms-action-icons-left {\n\t\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\t\tgap: 15px;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t@media only screen and ( min-width: 1200px ) {\n\t\t\t\t\t\t\t.llms-action-icons-right,\n\t\t\t\t\t\t\t.llms-action-icons-left {\n\t\t\t\t\t\t\t\twhite-space: nowrap;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&.expanded {\n\t\t\t\t\t.llms-lessons { overflow: visible; }\n\t\t\t\t}\n\t\t\t\t&.selected {\n\t\t\t\t\tborder-left: 3px solid $color-brand-blue;\n\t\t\t\t\tbox-shadow: 2px 2px 8px rgba( 0, 0, 0, 0.12 );\n\t\t\t\t\tmargin-left: -2px;\n\t\t\t\t\t.llms-drag-utility.drag-section {\n\t\t\t\t\t\tborder-color: $color-brand-blue;\n\t\t\t\t\t}\n\t\t\t\t\t> .llms-builder-header .llms-headline {\n\t\t\t\t\t\tfont-weight: 400;\n\t\t\t\t\t\tcolor: $color-brand-blue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t> .llms-builder-footer {\n\t\t\t\t\tborder-top: 1px solid #efefef;\n\t\t\t\t\tpadding: 20px 20px 20px 30px;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tli.llms-section:first-child:before {\n\t\t\t\ttop: 30px;\n\t\t\t}\n\n\t\t\tli.llms-section:last-child:before {\n\t\t\t\tbottom: 55px;\n\t\t\t}\n\n\t\t\tli.llms-section.expanded:last-child:before {\n\t\t\t\tbottom: 86px;\n\t\t\t}\n\n\t\t// Lessons\n\t\tul.llms-lessons {\n\t\t\tbox-shadow: 0 0 0 3px transparent;\n\t\t\theight: 0;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t\ttransition: box-shadow 0.6s ease, min-height 0.2s ease;\n\t\t\t&.dragging {\n\t\t\t\tbox-shadow: 0 0 0 3px $color-brand-blue;\n\t\t\t\tmin-height: 60px;\n\t\t\t}\n\t\t\t&.expanded, // added via backbone view events\n\t\t\t&.drag-expanded { // added only during dragover events and ignores model attrs\n\t\t\t\theight: auto;\n\t\t\t\tli.llms-lesson {\n\t\t\t\t\tpointer-events: auto;\n\t\t\t\t\tvisibility: visible;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t\tli.llms-lesson {\n\t\t\t\tbackground: #fff;\n\t\t\t\tborder-top: 1px solid #efefef;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 20px 10px 20px 30px;\n\t\t\t\tposition: relative;\n\t\t\t\tpointer-events: none;\n\t\t\t\tvisibility: hidden;\n\n\t\t\t\t&.selected {\n\t\t\t\t\t.llms-drag-utility.drag-lesson {\n\t\t\t\t\t\tborder-color: $color-brand-blue;\n\t\t\t\t\t}\n\t\t\t\t\t> .llms-builder-header .llms-headline {\n\t\t\t\t\t\tcolor: $color-brand-blue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t> .llms-builder-header {\n\t\t\t\t\t.llms-headline {\n\t\t\t\t\t\tfont-weight: 700;\n\t\t\t\t\t\tmargin-left: 10px;\n\t\t\t\t\t\tcursor: pointer;\n\t\t\t\t\t}\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-wrap: wrap;\n\t\t\t\t\tgap: 15px;\n\t\t\t\t\tjustify-content: space-between;\n\t\t\t\t\t.llms-action-icons {\n\t\t\t\t\t\talign-items: center;\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\tflex: 1;\n\t\t\t\t\t\tgap: 15px;\n\t\t\t\t\t\tjustify-content: space-between;\n\t\t\t\t\t\t.llms-action-icons-left {\n\t\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\t\tgap: 15px;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t.llms-action-icons-right {\n\t\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\t\tgap: 0;\n\t\t\t\t\t\t\tmargin-top: -5px;\n\t\t\t\t\t\t\t.llms-action-icon {\n\t\t\t\t\t\t\t\tpadding: 5px 10px;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t@media only screen and ( min-width: 1200px ) {\n\t\t\t\t\t\t\t.llms-action-icons-right,\n\t\t\t\t\t\t\t.llms-action-icons-left {\n\t\t\t\t\t\t\t\twhite-space: nowrap;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t@media only screen and ( min-width: 1200px ) {\n\t\t\t\t\t\t> .llms-builder-header {\n\t\t\t\t\t\t\tflex-wrap: nowrap;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t// Drag Utilities\n\t\tli.llms-section .llms-drag-utility {\n\t\t\tbackground: #fff;\n\t\t\tborder: 2px solid #ccc;\n\t\t\tborder-radius: 50%;\n\t\t\theight: 10px;\n\t\t\tleft: 13px;\n\t\t\tposition: absolute;\n\t\t\ttop: 24px;\n\t\t\twidth: 10px;\n\t\t}\n\n\t\tli.llms-lesson .llms-drag-utility {\n\t\t\theight: 6px;\n\t\t\tleft: 14px;\n\t\t\ttop: 25px !important;\n\t\t\twidth: 6px;\n\t\t}\n\n\t\t.llms-section:hover > .llms-drag-utility,\n\t\t.llms-lesson:hover > .llms-drag-utility {\n\t\t\tborder-color: #fff;\n\t\t\tcursor: move;\n\t\t\t&:hover:after {\n\t\t\t\tcolor: $color-brand-blue;\n\t\t\t}\n\t\t\t&:after {\n\t\t\t\tbackground: #fff;\n\t\t\t\tcontent: '\\00b7\\00b7\\A\\00b7\\00b7\\A\\00b7\\00b7';\n\t\t\t\tcolor: #ccc;\n\t\t\t\tdisplay: block;\n\t\t\t\tfont-size: 36px;\n\t\t\t\theight: 29px;\n\t\t\t\tletter-spacing: -1px;\n\t\t\t\tline-height: 8px;\n\t\t\t\tleft: -7px;\n\t\t\t\tposition: absolute;\n\t\t\t\ttext-align: center;\n\t\t\t\ttop: -12px;\n\t\t\t\twidth: 23px;\n\t\t\t}\n\t\t}\n\n\t\t// Sortable\n\t\tli.llms-section,\n\t\tli.llms-lesson {\n\t\t\t&.ui-sortable-helper,\n\t\t\t&.ui-draggable-dragging {\n\t\t\t\tborder: 1px solid #ccc;\n\t\t\t\tbackground: #fff;\n\t\t\t\ttransform: rotate( 2deg );\n\t\t\t\tvisibility: visible !important;\n\t\t\t\tz-index: 999;\n\t\t\t}\n\n\t\t\t&.llms-sortable-placeholder {\n\t\t\t\tborder: 3px dashed $color-brand-blue;\n\t\t\t\tbackground: rgba( $color-brand-blue, 0.3 );\n\t\t\t\tmargin: 0 10px;\n\t\t\t\tpadding: 5px;\n\t\t\t\t&:before { display: none; }\n\t\t\t}\n\t\t}\n\n\t\tul.llms-sections > li.llms-lesson.ui-draggable-dragging .llms-drag-utility {\n\t\t\tposition: relative;\n\t\t\t&:after {\n\t\t\t\tleft: -35px;\n\t\t\t\ttop: -28px;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t// Editable\n\t.llms-input-wrapper {\n\t\tposition: relative;\n\t}\n\n\t.llms-input-formatting.ql-container {\n\t\tfont-size: inherit;\n\t\tfont-family: inherit;\n\t\t.ql-editor.ql-blank::before {\n\t\t\tcolor: #a0a0a0;\n\t\t\tleft: 8px;\n\t\t\tright: 8px;\n\t\t}\n\t\t.ql-editor {\n\t\t\tp {\n\t\t\t\tfont-size: inherit;\n\t\t\t\tline-height: 1;\n\t\t\t}\n\t\t}\n\t\t.ql-tooltip {\n\t\t\tz-index: 1;\n\t\t}\n\t}\n\n\t.llms-input,\n\t.llms-input-formatting .ql-editor {\n\t\tborder: none;\n\t\tborder-bottom: 2px dotted transparent;\n\t\tbox-shadow: none;\n\t\tcursor: text;\n\t\tdisplay: inline-block;\n\t\tfont-size: inherit;\n\t\tfont-weight: 700;\n\t\theight: auto;\n\t\tline-height: 1;\n\t\tmargin: 0 8px;\n\t\tmin-width: 60px;\n\t\tpadding: 0;\n\t\ttransition: border 0.2s ease, box-shadow 0.2s ease;\n\t\t&:empty:before {\n\t\t\tcolor: #a0a0a0;\n\t\t\tcontent: attr( data-placeholder );\n\t\t}\n\t\t&:hover {\n\t\t\tborder-bottom-color: $color-brand-blue;\n\t\t}\n\t\t&[disabled] {\n\t\t\tcursor: not-allowed;\n\t\t\t&:hover {\n\t\t\t\tborder-bottom-color: transparent;\n\t\t\t}\n\t\t}\n\t\t&:focus {\n\t\t\tbackground: #fff;\n\t\t\tbox-shadow: 0 0 0 4px #fff, 0 0 0 6px $color-brand-blue;\n\t\t\tborder-bottom: none;\n\t\t\toutline: none;\n\t\t}\n\t\tb, strong {\n\t\t\tfont-weight: 700;\n\t\t}\n\t\t&.standard {\n\t\t\tborder: 1px solid #e6e6e6;\n\t\t\tmargin: 2px;\n\t\t\tpadding: 5px 3px;\n\t\t\t&:hover {\n\t\t\t\tborder-color: #d6d6d6;\n\t\t\t}\n\t\t\t&:focus {\n\t\t\t\tbox-shadow: 0 0 0 2px $color-brand-blue;\n\t\t\t}\n\t\t}\n\t\t&.permalink {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t.llms-input-formatting .ql-editor {\n\t\tpadding: 0 1px;\n\t}\n\n\t.llms-label {\n\t\tfont-weight: 500;\n\t\t.fa {\n\t\t\tcolor: #aaa;\n\t\t\tpadding-left: 6px;\n\t\t}\n\t}\n\n\t// .llms-editable-image,\n\t// .llms-editable-video,\n\t// .llms-editable-editor {\n\t// }\n\n\t.llms-editable-editor {\n\t\t.llms-label {\n\t\t\tfloat: left;\n\t\t\tmargin-right: 10px;\n\t\t\tposition: relative;\n\t\t\ttop: 10px;\n\t\t}\n\t\ttextarea {\n\t\t\tborder: none;\n\t\t\tpadding: 10px;\n\t\t\tdisplay: block;\n\t\t\twidth: 100%;\n\t\t}\n\t}\n\n\t.llms-editable-image {\n\t\tbutton.llms-add-image {\n\t\t\twidth: 130px;\n\t\t}\n\t\t.llms-image {\n\t\t\tdisplay: inline-block;\n\t\t\tposition: relative;\n\t\t\t&:hover .llms-action-icon {\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t\t.llms-action-icon {\n\t\t\t\tcolor: #fff;\n\t\t\t\tfont-size: 24px;\n\t\t\t\topacity: 0;\n\t\t\t\tpadding: 0;\n\t\t\t\tposition: absolute;\n\t\t\t\ttransition: opacity 0.2s ease;\n\t\t\t\tright: 3px;\n\t\t\t\ttop: 1px;\n\t\t\t\tz-index: 1;\n\t\t\t}\n\t\t\timg {\n\t\t\t\tdisplay: block;\n\t\t\t\theight: 100px;\n\t\t\t\tmax-width: 100%;\n\t\t\t\twidth: auto;\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-settings-field,\n\t.llms-editable-toggle-group {\n\t\tbackground: #f4f4f4;\n\t\tpadding: 10px;\n\t\tposition: relative;\n\t\tmargin: 0 1px;\n\n\t\t&.has-label-after {\n\t\t\talign-items: center;\n\t\t\tdisplay: flex;\n\t\t\tflex-wrap: wrap;\n\n\t\t\t.llms-label {\n\t\t\t\tmin-width: 100%;\n\t\t\t}\n\t\t\t.llms-editable-input {\n\t\t\t\tflex: 2;\n\t\t\t}\n\t\t\t.llms-label--after {\n\t\t\t\tcolor: #888;\n\t\t\t\tmin-width: auto;\n\t\t\t\tfont-size: 85%;\n\t\t\t\tpadding-left: 10px;\n\t\t\t}\n\t\t}\n\n\t\t.llms-switch {\n\t\t\tdisplay: block;\n\t\t\twidth: 100%;\n\t\t\t@include clearfix;\n\n\t\t\t.llms-label {\n\t\t\t\twidth: calc( 100% - 34px );\n\t\t\t}\n\t\t}\n\n\t\t.llms-editable-image,\n\t\t.llms-editable-video,\n\t\t.llms-editable-editor {\n\t\t\tmargin-top: 2px;\n\t\t}\n\n\t\t.llms-input.standard {\n\t\t\tdisplay: block;\n\t\t\twidth: 100%;\n\t\t\t&.two-digits,\n\t\t\t&.three-digits,\n\t\t\t&.four-digits {\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t.llms-editable-number {\n\t\t.llms-input {\n\t\t\tcolor: #888;\n\t\t\tmin-width: 30px;\n\t\t\ttext-align: right;\n\t\t\t&.two-digits {\n\t\t\t\twidth: 30px;\n\t\t\t}\n\t\t\t&.three-digits {\n\t\t\t\twidth: 40px;\n\t\t\t}\n\t\t\t&.four-digits {\n\t\t\t\twidth: 60px;\n\t\t\t}\n\t\t}\n\t\tsmall {\n\t\t\tcolor: #888;\n\t\t\ttext-transform: uppercase;\n\t\t}\n\t}\n\n\t.llms-model-settings {\n\t\tbackground-color: #FFF;\n\t\t-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13);\n\t\tbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.13);\n\t\t@include clearfix();\n\n\t\t.llms-settings-group-header {\n\t\t\tborder-bottom: 1px solid #efefef;\n\t\t\tpadding: 10px;\n\n\t\t\t.fa-caret-up { display: block; }\n\t\t\t.fa-caret-down { display: none; }\n\t\t}\n\t\t&.hidden {\n\t\t\t.llms-settings-group-header {\n\t\t\t\t.fa-caret-up { display: none; }\n\t\t\t\t.fa-caret-down { display: block; }\n\t\t\t}\n\t\t\t.llms-settings-group-body { display: none; }\n\t\t}\n\t}\n\n\t.llms-settings-group-header {\n\t\t@include clearfix();\n\t\t.llms-settings-group-title {\n\t\t\tdisplay: inline-block;\n\t\t\tfont-size: 16px;\n\t\t\tfont-weight: 700;\n\t\t\tline-height: 1.5;\n\t\t\tmargin: 0 5px;\n\t\t\tpadding: 0;\n\t\t}\n\t\t.llms-settings-group-toggle {\n\t\t\tfloat: right;\n\t\t\tfont-size: 18px;\n\t\t\tpadding: 2px;\n\t\t}\n\t}\n\n\t.llms-settings-group-body {\n\t\tpadding: 16px;\n\t}\n\n\t.llms-settings-row {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tmargin: 2px 0;\n\n\t\t.llms-settings-field,\n\t\t.llms-editable-toggle-group {\n\t\t\tflex: 1;\n\t\t\t&:first-child {\n\t\t\t\tmargin-left: 0;\n\t\t\t}\n\t\t\t&:last-child {\n\t\t\t\tmargin-right: 0;\n\t\t\t}\n\t\t}\n\n\t\t.llms-breaker {\n\t\t\tmargin: 2px 0;\n\t\t\twidth: 100%;\n\t\t}\n\t}\n\n\t.llms-editable-select {\n\t\tmargin: 2px 0;\n\t\t.select2-container--default.select2-container--focus .select2-selection--multiple {\n\t\t\tborder-color: #aaa;\n\t\t}\n\t}\n\n\t.llms-editable-radio {\n\t\tlabel {\n\t\t\tdisplay: block;\n\t\t}\n\t\t&.has-images {\n\t\t\tinput { display: none; }\n\t\t\tlabel {\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tmargin: 0 3px;\n\t\t\t}\n\t\t\tlabel > span {\n\t\t\t\ttransition: background 0.2s ease;\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tpadding: 3px;\n\t\t\t}\n\t\t\timg { display: block; }\n\t\t\tinput:checked + span {\n\t\t\t\tbackground: $color-brand-blue;\n\t\t\t}\n\t\t}\n\t}\n\n\t.settings-field--disabled {\n\t\topacity: 0.5;\n\t}\n\n\t// Icons\n\t.llms-action-icon {\n\t\tcolor: #666;\n\t\tdisplay: inline-block;\n\t\tfont-size: 13px;\n\t\ttext-decoration: none;\n\t\t&:hover {\n\t\t\tcolor: $color-brand-blue;\n\t\t\t&.danger { color: $color-danger; }\n\t\t}\n\t\t&.circle {\n\t\t\tborder: 2px solid #aaa;\n\t\t\tborder-radius: 50%;\n\t\t\tfont-size: 9px;\n\t\t\theight: 8px;\n\t\t\tline-height: 1;\n\t\t\tpadding: 5px;\n\t\t\ttext-align: center;\n\t\t\twidth: 8px;\n\t\t\t&:hover {\n\t\t\t\tborder-color: $color-brand-blue;\n\t\t\t\t&.danger {\n\t\t\t\t\tborder-color: $color-danger;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tul.llms-info-list {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tgap: 15px;\n\t\tmargin: 10px 0 0 8px;\n\t\tpadding: 0;\n\t\tli.llms-info-item {\n\t\t\tcolor: #666;\n\t\t\tfont-size: 13px;\n\t\t\tmargin: 0;\n\t\t\t&.active,\n\t\t\t&.active .llms-action-icon {\n\t\t\t\tcolor: $color-brand-blue;\n\t\t\t\t.fa {\n\t\t\t\t\tmargin-right: 5px;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbutton {\n\t\t\t\tbackground: none;\n\t\t\t\tcolor: inherit;\n\t\t\t\tborder: none;\n\t\t\t\tpadding: 0;\n\t\t\t\tcursor: pointer;\n\t\t\t\toutline: inherit;\n\t\t\t\t&:hover {\n\t\t\t\t\ttext-decoration: underline;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Sidebar\n\t.llms-builder-sidebar {\n\t\tbackground: #e6e6e6;\n\t\tbottom: 0;\n\t\toverflow: hidden;\n\t\tpadding: 30px;\n\t\tposition: fixed;\n\t\ttransition: width 0.3s ease-in-out;\n\t\ttop: 32px;\n\t\tright: 0;\n\t\twidth: 360px;\n\t\tz-index: 1;\n\n\t\t.llms-utilities {\n\n\t\t\tul, li {\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 0;\n\t\t\t}\n\n\t\t\tul {\n\t\t\t\tdisplay: flex;\n\t\t\t\tgap: 15px;\n\t\t\t\tli {\n\t\t\t\t\tflex: 1;\n\t\t\t\t\t&:last-child {\n\t\t\t\t\t\tmargin-right: 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-utility {\n\t\t\t\tbackground: #efefef;\n\t\t\t\tborder: 1px solid #ccc;\n\t\t\t\tborder-radius: 8px;\n\t\t\t\tcolor: inherit;\n\t\t\t\tcursor: pointer;\n\t\t\t\tdisplay: block;\n\t\t\t\toverflow: hidden;\n\t\t\t\tpadding: 6px 12px;\n\t\t\t\tposition: relative;\n\t\t\t\ttext-align: center;\n\t\t\t\twidth: 100%;\n\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: #fefefe;\n\t\t\t\t}\n\n\t\t\t\t.fa {\n\t\t\t\t\tbackground: #848484;\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\tleft: 0;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\tpadding: 7px;\n\t\t\t\t\tcolor: #fff;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t.llms-sidebar-headline {\n\t\t\tmargin: 0 0 10px;\n\t\t\tfont-size: 22px;\n\t\t}\n\n\t\t.llms-elements-list {\n\t\t\tmargin-bottom: 30px;\n\t\t\tli {\n\t\t\t\tmargin-bottom: 10px;\n\t\t\t}\n\t\t}\n\n\t\t.llms-utility {\n\t\t\tcolor: #444;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t.llms-element-button {\n\t\t\tbackground: $color-brand-blue;\n\t\t\tborder-radius: 8px;\n\t\t\tborder: none;\n\t\t\tcolor: #fff;\n\t\t\tcursor: pointer;\n\t\t\tdisplay: block;\n\t\t\tmargin: 0;\n\t\t\toverflow: hidden;\n\t\t\tpadding: 17px 20px;\n\t\t\tposition: relative;\n\t\t\ttransition: background 0.2s ease, color 0.2s ease;\n\t\t\ttext-align: center;\n\t\t\twidth: 100%;\n\n\t\t\t&:hover {\n\t\t\t\tbackground: $color-brand-blue-dark;\n\t\t\t}\n\n\t\t\t&.secondary {\n\t\t\t\tbackground: #efefef;\n\t\t\t\tcolor: #444;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: #fefefe;\n\t\t\t\t}\n\t\t\t\t.fa {\n\t\t\t\t\tbackground: #848484;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.fa {\n\t\t\t\tbackground: $color-brand-dark-blue;\n\t\t\t\tborder-radius: 4px 0 0 4px;\n\t\t\t\tcolor: #fff;\n\t\t\t\tdisplay: block;\n\t\t\t\tfont-size: 20px;\n\t\t\t\tpadding: 15px 20px;\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 0;\n\t\t\t\tleft: 0;\n\t\t\t}\n\n\t\t\t&[disabled=\"disabled\"] {\n\t\t\t\topacity: 0.4;\n\t\t\t}\n\n\t\t\t&.small {\n\n\t\t\t\tpadding: 8px 10px 8px 46px;\n\t\t\t\t.fa {\n\t\t\t\t\tfont-size: 15px;\n\t\t\t\t\tpadding: 9px 10px;\n\t\t\t\t\twidth: 20px;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t&.right {\n\n\t\t\t\t&.small {\n\t\t\t\t\tpadding-left: 10px;\n\t\t\t\t\tpadding-right: 46px;\n\t\t\t\t}\n\n\t\t\t\t.fa {\n\t\t\t\t\tborder-radius: 0 4px 4px 0;\n\t\t\t\t\tleft: auto;\n\t\t\t\t\tright: 0;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\n\n\t\t.llms-editor {\n\t\t\theight: 100%;\n\t\t\tmin-height: 100%;\n\t\t\tposition: relative;\n\t\t}\n\n\t\t\t// .llms-builder-close-editor {\n\t\t\t// \tbackground: $color-brand-blue;\n\t\t\t// \tborder: none;\n\t\t\t// \tborder-radius: 50%;\n\t\t\t// \tcolor: #fff;\n\t\t\t// \tcursor: pointer;\n\t\t\t// \tdisplay: inline-block;\n\t\t\t// \tfont-size: 18px;\n\t\t\t// \theight: 30px;\n\t\t\t// \tmargin: 0;\n\t\t\t// \tposition: absolute;\n\t\t\t// \tright: 0;\n\t\t\t// \ttext-align: center;\n\t\t\t// \ttop: 3px;\n\t\t\t// \twidth: 30px;\n\t\t\t// \tz-index: 3;\n\t\t\t// }\n\n\t\t\t.llms-editor-nav {\n\t\t\t\tbackground-color: $color-brand-dark-blue;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 8px 0 0 8px;\n\t\t\t\tfont-size: 0;\n\t\t\t\tmargin: -10px -10px 10px -10px;\n\t\t\t\tposition: relative;\n\t\t\t\tz-index: 2;\n\n\t\t\t\t.llms-editor-menu {\n\t\t\t\t\tlist-style-type: none;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tposition: relative;\n\n\t\t\t\t\t.llms-editor-menu-item {\n\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\tmargin: 0 6px 0 0;\n\t\t\t\t\t\tpadding: 0;\n\n\t\t\t\t\t\t> .llms-editor-menu {\n\t\t\t\t\t\t\tdisplay: none;\n\t\t\t\t\t\t\t&:before {\n\t\t\t\t\t\t\t\tborder: 8px solid transparent;\n\t\t\t\t\t\t\t\tborder-left-color: #cacaca;\n\t\t\t\t\t\t\t\tcontent: '';\n\t\t\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t\t\t\ttop: 11px;\n\t\t\t\t\t\t\t\tleft: 0;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t.llms-editor-menu-item:hover > a,\n\t\t\t\t\t\t\t.llms-editor-menu-item.active > a {\n\t\t\t\t\t\t\t\tbackground: #dfdfdf;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t&:hover > a {\n\t\t\t\t\t\t\tbackground-color: $color-brand-blue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t&.active > a {\n\t\t\t\t\t\t\tbackground-color: #e6e6e6;\n\t\t\t\t\t\t\tcolor: $color-brand-blue;\n\t\t\t\t\t\t\tfont-weight: 700;\n\n\t\t\t\t\t\t\t&:focus {\n\t\t\t\t\t\t\t\tbox-shadow: none;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t&.active > .llms-editor-menu {\n\t\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ta {\n\t\t\t\t\t\t\tborder-top-left-radius: 4px;\n\t\t\t\t\t\t\tborder-top-right-radius: 4px;\n\t\t\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\t\tpadding: 9px 18px;\n\t\t\t\t\t\t\ttext-decoration: none;\n\t\t\t\t\t\t\ttransition: background 0.2s ease;\n\t\t\t\t\t\t\tfont-size: 15px;\n\t\t\t\t\t\t}\n\n\n\t\t\t\t\t\t&.right {\n\t\t\t\t\t\t\tfloat: right;\n\n\t\t\t\t\t\t\ta,\n\t\t\t\t\t\t\t&:hover {\n\t\t\t\t\t\t\t\tbackground: transparent;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-editor-tab {\n\t\t\t\tdisplay: none;\n\t\t\t\theight: calc( 100% - 90px );\n\t\t\t\toverflow: scroll;\n\t\t\t\tposition: relative;\n\t\t\t\tz-index: 1;\n\t\t\t\t&.active {\n\t\t\t\t\tdisplay: block;\n\n\t\t\t\t\t&.tab--quiz {\n\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\tflex-direction: column;\n\n\t\t\t\t\t\t.llms-quiz-questions {\n\t\t\t\t\t\t\tflex: 1 0 auto;\n\t\t\t\t\t\t\toverflow: scroll;\n\n\t\t\t\t\t\t\t// groups\n\t\t\t\t\t\t\t.llms-quiz-questions {\n\t\t\t\t\t\t\t\toverflow: visible;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\n\t\t\t// .llms-builder-editor {\n\n\t\t\t// \topacity: 0;\n\t\t\t// \tmargin: 10px 0;\n\t\t\t// \ttransition: opacity 0.2s linear;\n\n\t\t\t// \t&.ready {\n\t\t\t// \t\topacity: 1;\n\t\t\t// \t}\n\n\t\t\t// \ttextarea {\n\t\t\t// \t\tborder: none;\n\t\t\t// \t\tdisplay: block;\n\t\t\t// \t\twidth: 100%;\n\t\t\t// \t}\n\t\t\t// }\n\n\t\t.llms-builder-save {\n\n\t\t\tbottom: 10px;\n\t\t\tleft: 10px;\n\t\t\tposition: absolute;\n\t\t\tright: 10px;\n\t\t\tz-index: 1;\n\n\t\t\t.llms-builder-error {\n\t\t\t\tbackground: $color-danger;\n\t\t\t\tborder-radius: 4px;\n\t\t\t\tcolor: #fff;\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tfont-style: italic;\n\t\t\t\tpadding: 5px 15px 7px 25px;\n\t\t\t\tmargin: 0 0 10px;\n\n\t\t\t\tli {\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t.llms-save {\n\t\t\t\twidth: 75%;\n\t\t\t}\n\t\t\t.llms-exit {\n\t\t\t\tpadding-left: 5px;\n\t\t\t\tpadding-right: 5px;\n\t\t\t\twidth: 23%;\n\t\t\t}\n\n\t\t\tbutton {\n\t\t\t\tposition: relative;\n\t\t\t\ti {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\tleft: 10px;\n\t\t\t\t\ttop: 10px;\n\n\t\t\t\t\t.llms-spinner {\n\t\t\t\t\t\tborder-color: #fff;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tbutton[data-status] .llms-status-indicator { display: none; }\n\t\t\tbutton[data-status=\"saved\"] .status--saved { display: block; }\n\t\t\tbutton[data-status=\"unsaved\"] {\n\t\t\t\tbackground-color: $color-brand-orange;\n\t\t\t\t.status--unsaved { display: block; }\n\t\t\t}\n\t\t\tbutton[data-status=\"saving\"] .status--saving { display: block; }\n\t\t\tbutton[data-status=\"error\"] .status--error { display: block; }\n\n\t\t}\n\n\t}\n\t@media only screen and ( max-width: 782px ) {\n\t\t.llms-builder-sidebar {\n\t\t\tmargin-right: 10px;\n\t\t\tposition: relative;\n\t\t\ttop: 0;\n\t\t\twidth: auto;\n\t\t}\n\t\t.llms-builder-main {\n\t\t\tpadding-right: 10px;\n\t\t\twidth: auto;\n\t\t}\n\t}\n\n\t// Search Popover\n\t .select2-container {\n\t \tz-index: 99999999;\n\t }\n\n\t.select2-results__option {\n\t\tpadding: 0;\n\t}\n\n\t.select2-container--default .select2-results__option--highlighted[aria-selected] {\n\t\tbackground: $color-brand-blue;\n\t\t.llms-existing-action {\n\t\t\tcolor: #fff;\n\t\t}\n\t}\n\n\t.llms-existing-lesson-result {\n\n\t\talign-items: center;\n\t\tdisplay: flex;\n\t\tpadding: 5px 5px 5px 0;\n\n\t\t.llms-existing-info {\n\t\t\tflex: 6;\n\n\t\t\th4, h5 {\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\th4 {\n\t\t\t\tfont-weight: 400;\n\t\t\t}\n\n\t\t\th5 {\n\t\t\t\tfont-weight: 300;\n\t\t\t}\n\t\t}\n\n\t\t.llms-existing-action {\n\t\t\tcolor: $color-brand-blue;\n\t\t\tflex: 1;\n\t\t\ttext-align: center;\n\n\t\t\t.fa {\n\t\t\t\tdisplay: block;\n\t\t\t\tfont-size: 30px;\n\t\t\t}\n\n\t\t\tsmall {\n\t\t\t\ttext-transform: uppercase;\n\t\t\t}\n\n\t\t}\n\n\n\t}\n\n\n\t// Quiz\n\t .llms-quiz-empty {\n    \tmargin: 100px auto;\n    \ttext-align: center;\n\n    \tp { font-size: 18px; }\n    \tbutton.llms-element-button {\n    \t\tmax-width: 320px;\n    \t\tmargin: 0 auto;\n    \t}\n\n\t }\n\n\t.llms-editor-tab.tab--quiz {\n\t\t.llms-model-header {\n\t\t\t.llms-model-title {\n\t\t\t\twidth: calc( 100% - 310px );\n\t\t\t}\n\t\t\t.llms-quiz-points {\n\t\t\t\tfloat: left;\n\t\t\t\tmargin-right: 10px;\n\t\t\t\twidth: 100px;\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-model-header {\n\t\tbackground-color: #FFF;\n\t\t-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13);\n\t\tbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.13);\n\t\tpadding: 10px;\n\t\t@include clearfix();\n\n\t\t.llms-model-title {\n\t\t\tfloat: left;\n\t\t\tmargin-right: 10px;\n\t\t\twidth: calc( 100% - 200px );\n\t\t\t.llms-input {\n\t\t\t\twidth: calc( 100% - 65px );\n\t\t\t}\n\t\t}\n\t\t.llms-model-status.llms-switch {\n\t\t\tfloat: left;\n\t\t\tmargin-right: 10px;\n\t\t\tposition: relative;\n\t\t\ttext-align: right;\n\t\t\ttop: -2px;\n\t\t\twidth: 100px;\n\t\t}\n\t\t.llms-action-icons {\n\t\t\tfloat: left;\n\t\t\tposition: relative;\n\t\t\ttext-align: right;\n\t\t\twidth: 80px;\n\t\t\tz-index: 1;\n\t\t\t.llms-action-icon {\n\t\t\t\tmargin-left: 10px;\n\t\t\t}\n\t\t\t.fa {\n\t\t\t\tmax-width: 15px;\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-model-header + .llms-model-settings.active {\n\t\tmargin-top: -10px;\n\t}\n\n\t.llms-model-settings {\n\t\tclear: both;\n\t\tdisplay: none;\n\n\t\t&.active {\n\t\t\tdisplay: block;\n\t\t\tmargin-top: 10px;\n\t\t}\n\t}\n\n\t.llms-quiz-footer {\n\t\tdisplay: flex;\n\t\tbutton.llms-element-button {\n\t\t\tflex: 1;\n\t\t\tmargin: 0 5px;\n\t\t\t&:first-child { margin-left: 0; }\n\t\t\t&:last-child { margin-right: 0; }\n\t\t\t&.llms-show-question-bank {\n\t\t\t\tflex: 2;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Question Bank\n\t.llms-quiz-tools {\n\t\tdisplay: none;\n\t\twidth: 100%;\n\t\tposition: relative;\n\n\t\t// .llms-quiz-tools-search {\n\t\t// \tpadding: 0 10px;\n\t\t// \tmargin-bottom: 15px;\n\n\t\t// \t.fa {\n\t\t// \t\tcolor: #888;\n\t\t// \t\tfont-size: 16px;\n\t\t// \t}\n\n\t\t// \tinput[type=\"search\"] {\n\t\t// \t\tbackground: inherit;\n\t\t// \t\tborder: none;\n\t\t// \t\tborder-bottom: 1px solid #bbb;\n\t\t// \t\tbox-shadow: none;\n\t\t// \t\tfont-size: 16px;\n\t\t// \t\tmargin: 8px 0 0;\n\t\t// \t\tpadding: 2px 5px;\n\t\t// \t\twidth: calc( 100% - 200px );\n\n\t\t// \t\t&:focus {\n\t\t// \t\t\tborder-bottom-color: $color-brand-blue;\n\t\t// \t\t}\n\t\t// \t}\n\n\t\t// }\n\n\t}\n\n\tul.llms-question-bank {\n\n\t\tlist-style-type: none;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t\t@include clearfix;\n\n\t\tli.llms-question-bank-header {\n\t\t\tclear: both;\n\t\t\tpadding-top: 20px;\n\t\t\t&:first-child {\n\t\t\t\tpadding-top: 0;\n\t\t\t}\n\t\t\th4 {\n\t\t\t\tfont-size: 20px;\n\t\t\t\tmargin: 10px 5px;\n\t\t\t}\n\t\t}\n\n\t\tli.llms-question-type {\n\t\t\tbox-sizing: border-box;\n\t\t\tfloat: left;\n\t\t\tmargin: 0;\n\t\t\tpadding: 3px;\n\t\t\twidth: 33.3333%;\n\t\t\ttransition: opacity 0.3s ease-in-out;\n\n\t\t\t&.filtered {\n\t\t\t\topacity: 0.3;\n\t\t\t}\n\n\t\t\t.llms-type-unavailable {\n\t\t\t\tdisplay: block;\n\t\t\t\tposition: relative;\n\t\t\t\ttext-decoration: none;\n\t\t\t\t.llms-element-button {\n\t\t\t\t\topacity: 0.5;\n\t\t\t\t\tpointer-events: none;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t// Quiz Questions\n\tul.llms-quiz-questions {\n\n\t\tmargin: 10px 3px;\n\t\tpadding: 5px;\n\t\ttransition: box-shadow 0.6s ease;\n\n\t\t&.dragging {\n\t\t\tbox-shadow: 0 0 0 3px $color-brand-blue;\n\t\t}\n\n\t\t&:empty:before {\n\t\t\tbackground: #fff;\n\t\t\tcontent: attr(data-empty-msg);\n\t\t\tdisplay: block;\n\t\t\tfont-size: 18px;\n\t\t\tmargin: 0 auto;\n\t\t\tpadding: 100px 0;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\tli.llms-question {\n\n\t\t\tbackground: #fff;\n\t\t\tmargin: 0 0 3px;\n\t\t\tpadding: 15px 12px 10px;\n\n\t\t\t&:hover {\n\t\t\t\t> .llms-builder-header .llms-action-icons {\n\t\t\t\t\topacity: 1;\n\t\t\t\t\tpointer-events: auto;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// groups\n\t\t\tul.llms-quiz-questions {\n\t\t\t\tmargin-left: 12px;\n\t\t\t\t.llms-question {\n\t\t\t\t\tborder-bottom: 2px solid #e6e6e6;\n\t\t\t\t}\n\t\t\t\t&:empty:before {\n\t\t\t\t\tcontent: attr(data-empty-msg);\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tfont-size: 18px;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tmargin: 20px auto;\n\t\t\t\t}\n\t\t\t\tli.llms-question.llms-sortable-placeholder.qtype--group {\n\t\t\t\t\tdisplay: none !important;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-builder-header {\n\t\t\t\t@include clearfix;\n\t\t\t\t> * {\n\t\t\t\t\tfloat: left;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-question-body {\n\t\t\t\tdisplay: none;\n\t\t\t\t&.active {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-data-stamp {\n\t\t\t\tbackground: $color-brand-blue;\n\t\t\t\tborder-radius: 4px;\n\t\t\t\tcolor: #fff;\n\t\t\t\tcursor: move;\n\t\t\t\tfont-size: 90%;\n\t\t\t\tmargin-top: -5px;\n\t\t\t\tpadding: 4px 10px 6px;\n\n\t\t\t\tsmall, .fa {\n\t\t\t\t\tline-height: 1.2;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t}\n\n\t\t\t\t.fa {\n\t\t\t\t\tmargin-right: 4px;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t.llms-headline {\n\t\t\t\twidth: calc( 100% - 110px - 90px - 55px );\n\t\t\t\t.ql-editor {\n\t\t\t\t\twidth: calc( 100% - 16px );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-action-icons {\n\t\t\t\twidth: 110px;\n\t\t\t\topacity: 0;\n\t\t\t\tpointer-events: none;\n\t\t\t}\n\n\t\t\t.llms-question-points {\n\t\t\t\twidth: 90px;\n\t\t\t}\n\n\t\t\t.llms-question-features {\n\t\t\t\tmargin: 10px 0 0;\n\t\t\t\t&:last-child {\n\t\t\t\t\tmargin: 0;\n\t\t\t\t}\n\t\t\t\t.llms-switch {\n\t\t\t\t\tmargin-right: 15px;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-editable-video {\n\t\t\t\tposition: relative;\n\t\t\t\tz-index: 1;\n\t\t\t}\n\n\t\t}\n\n\t\t\t.llms-question-choices-wrapper {\n\t\t\t\tbackground: #f4f4f4;\n\t\t\t\tmargin: 2px 1px;\n\t\t\t\tpadding: 10px;\n\t\t\t}\n\n\t\t\t\t.llms-question-choices-list-header {\n\t\t\t\t\t@include clearfix;\n\t\t\t\t\tmargin-bottom: 10px;\n\n\t\t\t\t\t.llms-switch {\n\t\t\t\t\t\tfloat: right;\n\t\t\t\t\t\ttext-align: right;\n\t\t\t\t\t\twidth: 260px;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tul.llms-question-choices {\n\t\t\t\tborder: 3px solid #f4f4f4;\n\t\t\t\tmargin: -3px;\n\t\t\t\tpadding: 0;\n\t\t\t\ttransition: box-shadow 0.6s ease;\n\n\t\t\t\t&.dragging {\n\t\t\t\t\tbox-shadow: 0 0 0 3px $color-brand-blue;\n\t\t\t\t}\n\n\t\t\t\t&.multi-choices li.llms-question-choice .llms-choice-id span {\n\t\t\t\t\tborder-radius: 4px;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t\tli.llms-question-choice {\n\t\t\t\t\tmargin: 0 0 5px;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\t&:last-child { margin-bottom: 0; }\n\n\t\t\t\t\t.llms-choice-id {\n\n\t\t\t\t\t\tinput[type=\"checkbox\"] {\n\t\t\t\t\t\t\tdisplay: none;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tinput[type=\"checkbox\"]:checked + .llms-marker {\n\t\t\t\t\t\t\tbackground: $color-green;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t.llms-marker {\n\t\t\t\t\t\t\tborder-radius: 50%;\n\t\t\t\t\t\t\tbackground: #d0d0d0;\n\t\t\t\t\t\t\tbox-shadow: inset 0 0 1px #848484;\n\t\t\t\t\t\t\tcolor: #444;\n\t\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\t\t\theight: 20px;\n\t\t\t\t\t\t\tline-height: 20px;\n\t\t\t\t\t\t\tpadding: 5px;\n\t\t\t\t\t\t\tposition: relative;\n\t\t\t\t\t\t\ttext-align: center;\n\t\t\t\t\t\t\ttransition: background 0.1s ease;\n\t\t\t\t\t\t\twidth: 20px;\n\n\t\t\t\t\t\t\t.fa {\n\t\t\t\t\t\t\t\tleft: 7px;\n\t\t\t\t\t\t\t\topacity: 0;\n\t\t\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t\t\t\ttop: 7px;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t&.selectable:hover {\n\t\t\t\t\t\t\t\tb { opacity: 0 }\n\t\t\t\t\t\t\t\t.fa { opacity: 1; }\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\t.llms-input-wrapper,\n\t\t\t\t\t.llms-editable-image {\n\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\t// action icons width, label width, ul margins\n\t\t\t\t\t\twidth: calc( 100% - 55px - 35px - 5px );\n\t\t\t\t\t}\n\n\t\t\t\t\t\t.llms-input {\n\t\t\t\t\t\t\twidth: calc( 100% - 16px );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t.llms-editable-image .llms-image {\n\t\t\t\t\t\tvertical-align: middle;\n\t\t\t\t\t\timg {\n\t\t\t\t\t\t\theight: 50px;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t.llms-action-icons {\n\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\topacity: 1;\n\t\t\t\t\t\tpointer-events: auto;\n\t\t\t\t\t\ttext-align: right;\n\t\t\t\t\t\twidth: 55px;\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tli.llms-question-choice.llms-sortable-placeholder {\n\t\t\t\t\tborder: 3px dashed $color-brand-blue !important;\n\t\t\t\t\tbackground: rgba( $color-brand-blue, 0.3 );\n\t\t\t\t}\n\n\t\t\t\tli.llms-question-choice.ui-sortable-helper {\n\t\t\t\t\tborder: 1px solid #ccc;\n\t\t\t\t\tbackground: #fff;\n\t\t\t\t\tpadding: 10px;\n\t\t\t\t\ttransform: rotate( 2deg );\n\t\t\t\t\tz-index: 999;\n\t\t\t\t}\n\n\t\tli.llms-question.ui-sortable-helper,\n\t\tli.llms-question.ui-draggable-dragging {\n\t\t\tborder: 1px solid #ccc;\n\t\t\tbackground: #fff;\n\t\t\ttransform: rotate( 2deg );\n\t\t\tz-index: 999;\n\t\t}\n\n\t\tli.llms-question.llms-sortable-placeholder {\n\t\t\tborder: 3px dashed $color-brand-blue !important;\n\t\t\tbackground: rgba( $color-brand-blue, 0.3 );\n\t\t}\n\n\t}\n\n\n\t.llms-switch {\n\t\tdisplay: inline-block;\n\t\tfloat: none;\n\t\twidth: auto;\n\n\t\tinput[type=\"checkbox\"] {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\tinput[type=\"checkbox\"]:checked + .llms-switch-slider {\n\t\t\tbackground: $color-green;\n\t\t}\n\n\t\tinput[type=\"checkbox\"]:checked + .llms-switch-slider:after {\n\t\t\ttransform: translateX( 14px );\n\t\t}\n\n\t\t.llms-label {\n\t\t\tdisplay: inline-block;\n\t\t\tvertical-align: top;\n\t\t}\n\n\t\t.llms-switch-slider {\n\t\t\tbackground: #e0e0e0;\n\t\t\tborder-radius: 8px;\n\t\t\tdisplay: inline-block;\n\t\t\theight: 16px;\n\t\t\tmargin-top: 2px;\n\t\t\tposition: relative;\n\t\t\ttransition: background 0.2s ease;\n\t\t\tvertical-align: top;\n\t\t\twidth: 30px;\n\n\t\t\t&:after {\n\t\t\t\tbackground: #fff;\n\t\t\t\tborder-radius: 8px;\n\t\t\t\tcontent: '';\n\t\t\t\tdisplay: block;\n\t\t\t\theight: 12px;\n\t\t\t\tleft: 2px;\n\t\t\t\tposition: relative;\n\t\t\t\ttransition: transform 0.2s ease;\n\t\t\t\ttop: 2px;\n\t\t\t\twidth: 12px;\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t.llms-video-explainer-trigger {\n\t\tdisplay: block;\n\t\tmargin: 40px 0 0;\n\t\tcursor: pointer;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\ttransition: color 0.2s ease;\n\t\tcolor: initial;\n\t\t&:hover {\n\t\t\tcolor: currentColor;\n\t\t}\n\t\t&:after {\n\t\t\tposition: absolute;\n\t\t\tinset: 0;\n\t\t\tmargin: auto;\n\t\t\tpointer-events: none;\n\t\t\tcontent: '▶';\n\t\t\tdisplay: flex;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\tclear: both;\n\t\t\theight: 2em;\n\t\t\twidth: 2em;\n\t\t\tborder-radius: 4em;\n\t\t\tborder: 4px solid currentColor;\n\t\t\tfont-size: 2.5em;\n\t\t}\n\t\timg {\n\t\t\tdisplay: block;\n\t\t\twidth: 100%;\n\t\t\tborder-radius: 4px;\n\t\t}\n\t}\n}\n\n\n\n.llms-multi-input .llms-input-wrapper {\n\twidth: 50%;\n\tfloat: left;\n\tmargin-bottom: 6px;\n\tfont-size: 12px;\n}\n\n.llms-multi-input .llms-input-wrapper .llms-input {\n\tfont-size: 12px;\n    padding: 5px;\n}\n"
  },
  {
    "path": "assets/scss/admin/_dashboard-widget.scss",
    "content": "#llms_dashboard_widget {\n\n\t.inside {\n\t\tmargin: 0;\n\t\tpadding-bottom: 8px;\n\t}\n\n\t.llms-dashboard-widget-wrap {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\talign-items: center;\n\t\tpadding-top: 12px;\n\t}\n\n\t.activity-block {\n\t\tpadding-bottom: 8px;\n\t\tborder-color: #e8e8e8;\n\t}\n\n\th3 {\n\t\tmargin-bottom: 0;\n\t}\n\n\t.llms-charts-wrapper {\n\t\tdisplay: none;\n\t}\n\n\t.llms-widget-row {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\tgap: 8px;\n\t\twidth: 100%;\n\t\talign-items: stretch;\n\t\tpadding: 4px 0;\n\n\t\t&:before,\n\t\t&:after {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t.llms-widget-1-4 {\n\t\tpadding: 0;\n\t\tflex: 1;\n\t}\n\n\t.llms-widget {\n\t\tpadding: 8px 8px 12px;\n\t\tmargin: 0;\n\t\tborder-radius: 6px;\n\t\tborder: 1px solid #e8e8e8;\n\t\tbox-shadow: 0px 2px 4px rgb(246 247 247);\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tjustify-content: center;\n\t\talign-items: flex-end;\n\t}\n\n\t.llms-label {\n\t\tfont-size: 14px;\n\t\twidth: 100%;\n\t\talign-self: flex-start;\n\t\twhite-space: nowrap;\n\t\ttext-overflow: ellipsis;\n\t\toverflow: hidden;\n\t}\n\n\t.llms-widget-content {\n\t\tfont-size: 20px;\n\t\tmargin: 0;\n\t}\n\n\t.llms-widget-info-toggle {\n\t\tdisplay: none;\n\t}\n\n\ta {\n\t\tborder: 0;\n\t}\n\n\t.subsubsub {\n\t\tcolor: #dcdcde;\n\t}\n}\n\n.llms-dashboard-widget-feed {\n\tmargin: 0 -12px;\n\tpadding: 0;\n\tbackground-color: #f6f7f7;\n\n\tli {\n\t\tmargin: 0;\n\t\tpadding: 8px 12px;\n\t\tborder-bottom: 1px solid #e8e8e8;\n\t}\n\n\tspan {\n\t\tdisplay: block;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/_dashboard.scss",
    "content": ".wrap.llms-dashboard {\n\n\t.llms-inside-wrap {\n\t\tpadding-top: 30px;\n\t}\n\n\t#poststuff {\n\n\t\th2 {\n\t\t\tpadding: 12px 20px;\n\t\t}\n\n\t}\n\n\t.llms-dashboard-activity {\n\n\t\th2 {\n\t\t\tfont-size: 20px;\n\t\t\tfont-weight: 700;\n\t\t\tline-height: 1.5;\n\t\t\tmargin-top: 0;\n\t\t\ttext-align: center;\n\t\t}\n\t}\n\n\t.postbox {\n\t\tbackground-color: #FFF;\n\t\tborder: none;\n\t\tbox-shadow: 0 1px 3px rgba( 0, 0, 0, 0.13 );\n\n\t\t.postbox-header {\n\t\t\tborder-bottom-color: #efefef;\n\n\t\t}\n\n\t\t.inside {\n\t\t\tpadding: 20px;\n\t\t}\n\t}\n\n\t#llms_dashboard_addons {\n\n\t\t.llms-addons-wrap {\n\t\t\tmargin-top: 0;\n\n\t\t\t.llms-add-on-item {\n\t\t\t\tmargin-top: 0;\n\n\t\t\t\tp {\n\t\t\t\t\ttext-align: left;\n\t\t\t\t}\n\n\t\t\t\tfooter.llms-actions {\n\t\t\t\t\tpadding-top: 0;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\tp {\n\t\t\ttext-align: center;\n\n\t\t\t.llms-button-primary {\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tmargin-top: 15px;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t#llms_dashboard_quick_links {\n\n\t\tul {\n\t\t\tlist-style: disc;\n\t\t\tmargin: 5px 0 0 20px;\n\n\t\t\tli {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tline-height: 1.5;\n\t\t\t}\n\t\t}\n\n\t\t.llms-quick-links {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: 1fr;\n\t\t\tgrid-gap: 30px;\n\n\t\t\ta {\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\n\t\t\t.llms-list {\n\n\t\t\t\th3 {\n\t\t\t\t\tmargin: 0 0 10px 0;\n\t\t\t\t}\n\n\t\t\t\tul {\n\t\t\t\t\tmargin-bottom: 20px;\n\n\t\t\t\t\t&.llms-checklist {\n\t\t\t\t\t\tlist-style: none;\n\t\t\t\t\t\tmargin-left: 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.fa {\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\twidth: 16px;\n\t\t\t\t}\n\n\t\t\t\t.fa-check {\n\t\t\t\t\tcolor: #008a20;\n\t\t\t\t}\n\n\t\t\t\t.fa-times {\n\t\t\t\t\tcolor: #d63638;\n\t\t\t\t}\n\n\t\t\t\t.llms-button-primary,\n\t\t\t\t.llms-button-secondary,\n\t\t\t\t.llms-button-action {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t@media only screen and (min-width: 782px) {\n\t\t\t\tgrid-template-columns: 1fr 1fr 1fr;\n\t\t\t}\n\t\t}\n\n\t\t.llms-help-links {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: 1fr 1fr;\n\t\t\tgrid-gap: 20px;\n\n\t\t\t.llms-list {\n\n\t\t\t\th3 {\n\t\t\t\t\tmargin: 0;\n\n\t\t\t\t\t.dashicons {\n\t\t\t\t\t\tcolor: #AAA;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t@media only screen and (min-width: 782px) {\n\t\t\t\tgrid-template-columns: 1fr 1fr 1fr 1fr;\n\t\t\t}\n\n\t\t}\n\n\t}\n\t#llms_dashboard_blog,\n\t#llms_dashboard_podcast {\n\n\t\tul {\n\t\t\tmargin: 0;\n\n\t\t\tli {\n\t\t\t\tmargin: 0 0 15px 0;\n\n\t\t\t\ta {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tp {\n\t\t\tmargin: 15px 0;\n\t\t\ttext-align: center;\n\n\t\t\ta {\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/_fonts.scss",
    "content": "#llms-options-page-contents {\n\th2 {\n\t\tcolor: #999;\n\t\tfont-weight: 500;\n\t\tletter-spacing: 2px;\n\t\tborder-bottom: 1px solid #999;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/_llms-table.scss",
    "content": ".llms-table-wrap {\n\tposition: relative;\n}\n\n.llms-table-header {\n\tpadding: 0;\n\n\t@include clearfix();\n\n\th2 {\n\t\tfont-size: 20px;\n\t\tpadding: 0;\n\t\tdisplay: inline-block;\n\t\tline-height: 1.5;\n\t\tmargin: 0 0 20px 0;\n\t\tvertical-align: middle;\n\t}\n\n\t.llms-table-search,\n\t.llms-table-filters {\n\t\tfloat: right;\n\t\tpadding-left: 10px;\n\t}\n\n\t.llms-table-search input {\n\t\tmargin: 0;\n\t\tpadding: 0 8px;\n\t}\n\n}\n\n.llms-table {\n\n\tborder: 1px solid #c3c4c7;\n\tborder-collapse: collapse;\n\twidth: 100%;\n\n\ta:not(.small) {\n\t\tcolor: $color-brand-blue;\n\t\t&:hover {\n\t\t\tcolor: $color-brand-blue-dark;\n\t\t}\n\t}\n\n\ttd, th {\n\t\tborder-bottom: 1px solid #c3c4c7;\n\t\tpadding: 10px 12px;\n\t\ttext-align: center;\n\n\t\t&.expandable.closed {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t.llms-button-primary,\n\t\t.llms-button-secondary,\n\t\t.llms-button-action,\n\t\t.llms-button-danger {\n\t\t\tdisplay: inline-block;\n\t\t}\n\n\t}\n\n\ttr.llms-quiz-pending {\n\t\ttd {\n\t\t\tfont-weight: 700;\n\t\t}\n\t}\n\n\tthead th,\n\ttfoot th {\n\t\tbackground-color: #FFF;\n\t\tfont-weight: 700;\n\n\t\ta.llms-sortable {\n\t\t\t// display: block;\n\t\t\tpadding-right: 16px;\n\t\t\tposition: relative;\n\t\t\ttext-decoration: none;\n\t\t\twidth: 100%;\n\t\t\t&.active {\n\t\t\t\t// show the current sorted when a sort is active\n\t\t\t\t&[data-order=\"DESC\"] .asc { opacity: 1; }\n\t\t\t\t&[data-order=\"ASC\"] .desc { opacity: 1; }\n\t\t\t}\n\t\t\t// show the opposite on hover\n\t\t\t&:hover {\n\t\t\t\t&[data-order=\"DESC\"] {\n\t\t\t\t\t.asc { opacity: 0; }\n\t\t\t\t\t.desc { opacity: 1; }\n\t\t\t\t}\n\t\t\t\t&[data-order=\"ASC\"] {\n\t\t\t\t\t.asc { opacity: 1; }\n\t\t\t\t\t.desc { opacity: 0; }\n\t\t\t\t}\n\t\t\t}\n\t\t\t.dashicons {\n\t\t\t\tcolor: #444;\n\t\t\t\tfont-size: 16px;\n\t\t\t\theight: 16px;\n\t\t\t\topacity: 0;\n\t\t\t\tposition: absolute;\n\t\t\t\twidth: 16px;\n\t\t\t}\n\t\t}\n\t}\n\n\ttfoot th {\n\t\tborder-bottom: none;\n\n\t\t.llms-table-export {\n\t\t\tfloat: left;\n\t\t\t.llms-table-progress {\n\t\t\t\tbackground: #efefef;\n\t\t\t\tdisplay: none;\n\t\t\t\tmargin-left: 8px;\n\t\t\t\tvertical-align: middle;\n\t\t\t\twidth: 100px;\n\t\t\t}\n\t\t}\n\n\t\t.llms-clear-resumable-attempts {\n\t\t\tfloat: left;\n\t\t}\n\n\t\t.llms-table-pagination {\n\t\t\tfloat: right;\n\t\t}\n\n\t}\n\n\t&.zebra tbody tr:nth-child( even ) {\n\t\tth, td { background-color: #fff; }\n\t}\n\n\t&.zebra tbody tr:nth-child( odd ) {\n\t\tth, td { background-color: #f6f7f7; }\n\t}\n\n\t&.text-left {\n\t\ttd, th {\n\t\t\ttext-align: left;\n\t\t}\n\t}\n\n\t&.size-large {\n\t\ttd, th {\n\t\t\tfont-size: 14px;\n\t\t\tpadding: 10px 12px;\n\t\t}\n\t}\n\n\t.llms-drag-handle {\n\t\tcolor: #777;\n\t\tcursor: pointer;\n\t\t-webkit-transition: color 0.4s ease;\n\t\ttransition: color 0.4s ease;\n\t}\n\n\t.llms-action-icon {\n\t\tcolor: #777;\n\t\ttext-decoration: none;\n\n\t\t.tooltip {\n\t\t\tcursor: pointer;\n\t\t}\n\n\t\t&:hover {\n\t\t\tcolor: $color-blue;\n\t\t}\n\n\t\t&.danger:hover {\n\t\t\tcolor: $color-danger;\n\t\t}\n\t}\n\n\t.llms-table-page-count {\n\t\tfont-size: 12px;\n\t\tpadding: 0 5px;\n\t}\n\n}\n\n// progress bars within the tables\n.llms-table-progress {\n\ttext-align: center;\n\t.llms-table-progress-bar {\n\t\tbackground: #eee;\n\t\tborder-radius: 10px;\n\t\theight: 16px;\n\t\toverflow: hidden;\n\t\tposition: relative;\n\t\t.llms-table-progress-inner {\n\t\t\tbackground: $color-brand-blue;\n\t\t\theight: 100%;\n\t\t\ttransition: width 0.2s ease;\n\t\t}\n\t}\n\t.llms-table-progress-text {\n\t\tcolor: $color-brand-blue;\n\t\tfont-size: 12px;\n\t\tfont-weight: 700;\n\t\tline-height: 16px;\n\t}\n}\n\n\n.llms-table.llms-gateway-table,\n.llms-table.llms-integrations-table {\n\t.status {\n\t\t.fa {\n\t\t\tcolor: $color-brand-blue;\n\t\t\tfont-size: 22px;\n\t\t}\n\t}\n\t.sort {\n\t\tcursor: move;\n\t\ttext-align: center;\n\t\twidth: 10px;\n\t}\n}\n\n.llms-gb-table-notifications {\n\tth, td {\n\t\ttext-align: left;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/_main.scss",
    "content": ".wrap.lifterlms {\n\tmargin-top: 0;\n}\n\n.llms-header {\n\tbackground-color: #FFF;\n\tmargin: 0 0 0 -20px;\n\tpadding: 20px 10px;\n\tposition: relative;\n\tz-index: 1;\n\n\t.llms-inside-wrap {\n\t\tdisplay: flex;\n\t\tpadding: 0 10px;\n\t}\n\n\n\t.lifterlms-logo {\n\t\tflex: 0 0 190px;\n\t\tmax-height: 52px;\n\t\tmargin-right: 10px;\n\t}\n\n\t.llms-meta {\n\t\talign-self: center;\n\t\tdisplay: flex;\n\t\tflex: 1;\n\t\tfont-size: 16px;\n\t\tjustify-content: space-between;\n\t\tline-height: 1.5;\n\n\t\t.llms-version {\n\t\t\tbackground-color: #1d2327;\n\t\t\tcolor: #FFF;\n\t\t\tborder-radius: 999px;\n\t\t\tfont-size: 13px;\n\t\t\tfont-weight: 700;\n\t\t\tpadding: 6px 12px;\n\t\t}\n\n\t\ta {\n\t\t\tdisplay: inline-block;\n\t\t}\n\n\t\t.llms-license {\n\t\t\tborder-right: 1px solid #CCC;\n\t\t\tfont-weight: 700;\n\t\t\tmargin-right: 12px;\n\t\t\tpadding-right: 12px;\n\n\t\t\ta {\n\t\t\t\ttext-decoration: none;\n\n\t\t\t\t&:before {\n\t\t\t\t\tcontent: \"\\f534\";\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tfont: 400 16px/1 dashicons;\n\t\t\t\t\tleft: 0;\n\t\t\t\t\tpadding-right: 3px;\n\t\t\t\t\tposition: relative;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t\tvertical-align: text-bottom;\n\t\t\t\t}\n\n\t\t\t\t&.llms-license-none {\n\t\t\t\t\tcolor: #888;\n\n\t\t\t\t\t&:before {\n\t\t\t\t\t\tcontent: \"\\f335\";\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&.llms-license-active {\n\t\t\t\t\tcolor: #008a20;\n\n\t\t\t\t\t&:before {\n\t\t\t\t\t\tcontent: \"\\f112\";\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\n\t\t.llms-support {\n\t\t\tfont-weight: 700;\n\n\t\t\ta {\n\t\t\t\tcolor: #1d2327;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n\n.llms-subheader {\n\talign-items: center;\n\tbackground-color: #FFF;\n\tbox-shadow: 0 1px 3px rgba( 0, 0, 0, 0.15 );\n\tdisplay: flex;\n\tflex-direction: row;\n\tmargin-left: -20px;\n\tpadding: 10px 20px;\n\twidth: 100%;\n\tz-index: 1;\n\n\th1 {\n\t\tfont-weight: 700;\n\t\tpadding: 0;\n\n\t\ta {\n\t\t\tcolor: inherit;\n\t\t\ttext-decoration: none;\n\t\t}\n\t}\n\n}\n\n#post_course_difficulty {\n\tmin-width: 200px;\n}\n#_video-embed, #_audio-embed {\n\twidth: 100%;\n}\n\nhr {\n\tbackground-color: #CCC;\n\tborder: none;\n\theight: 1px;\n\tmargin: 30px 0;\n\tpadding: 0;\n}\n\n.llms_certificate_default_image, .llms_certificate_image  {\n\twidth: 300px;\n}\n\n.llms_achievement_default_image, .llms_achievement_image  {\n\twidth: 120px;\n}\n\ndiv[id^=\"lifterlms-\"] .inside {\n\toverflow: visible;\n}\n\n.lifterlms_notification {\n\tmargin-bottom: 15px;\n}\n\n.notice.llms-admin-notice, #lifterlms-notifications .lifterlms_notification {\n\tbackground-color: #FFF;\n\tborder: 1px solid #ccd0d4;\n\tbox-shadow: 0 1px 4px rgba( 0, 0, 0, 0.15 );\n\tdisplay: flex;\n\tpadding: 0 !important;\n\tposition: relative;\n\n\t.notice-dismiss {\n\t\ttext-decoration: none;\n\t}\n\n\t&.notice-warning, &.lifterlms_notification-warning {\n\t\tborder-left: 4px solid #F8954F;\n\n\t\t.llms-admin-notice-icon, .lifterlms_notification-icon {\n\t\t\tbackground-color: #FEF4ED;\n\t\t}\n\t}\n\n\t&.notice-info, &.lifterlms_notification-info {\n\t\tborder-left: 4px solid #466DD8;\n\n\t\th3 {\n\t\t\tcolor: #466DD8;\n\t\t}\n\n\t\t.llms-admin-notice-icon, .lifterlms_notification-icon {\n\t\t\tbackground-color: #EDF0FB;\n\t\t}\n\n\t}\n\n\t&.notice-success, &.lifterlms_notification-success {\n\t\tborder-left: 4px solid #18A957;\n\n\t\th3 {\n\t\t\tcolor: #18A957;\n\t\t}\n\n\t\t.llms-admin-notice-icon, .lifterlms_notification-icon {\n\t\t\tbackground-color: #E8F6EE;\n\t\t}\n\n\t}\n\n\t&.notice-error, &.lifterlms_notification-error {\n\t\tborder-left: 4px solid #DF1642;\n\n\t\th3 {\n\t\t\tcolor: #9C0F2E;\n\t\t}\n\n\t\t.llms-admin-notice-icon, .lifterlms_notification-icon {\n\t\t\tbackground-color: #FCE8EC;\n\t\t}\n\n\t}\n\n\t.llms-admin-notice-icon {\n\t\tbackground-image: url(../../assets/images/lifterlms-icon-color.png);\n\t\tbackground-position: center center;\n\t\tbackground-repeat: no-repeat;\n\t\tbackground-size: 30px;\n\t\tmin-width: 70px;\n\t}\n\n\t.lifterlms_notification-icon {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tmin-width: 70px;\n\n\t\tspan.dashicons {\n\t\t\tfont-size: 30px;          /* Set icon size */\n\t\t\tline-height: 1;           /* Remove excess line height */\n\t\t\tcolor: #0073aa;           /* Optional: icon color */\n\t\t\tdisplay: flex;            /* Ensures the icon is centered as content */\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\twidth: 100%;              /* Fill container horizontally */\n\t\t\theight: 100%;             /* Fill container vertically */\n\t\t}\n\t}\n\n\t.llms-admin-notice-content, .lifterlms_notification-content {\n\t\tcolor: #111;\n\t\tpadding: 20px;\n\t}\n\n\th3 {\n\t\tfont-size: 18px;\n\t\tfont-weight: 700;\n\t\tline-height: 25px;\n\t\tmargin: 0 0 15px 0;\n\t}\n\n\tbutton,\n\t.llms-button-primary {\n\t\tdisplay: inline-block;\n\t}\n\n\tp {\n\t\tfont-size: 14px;\n\t\tline-height: 22px;\n\t\tmargin: 0 0 15px 0;\n\t\tmax-width: 65em;\n\t\tpadding: 0;\n\t}\n\n\tp:last-of-type {\n\t\tmargin-bottom: 0;\n\t}\n\n\t.lifterlms_notification-content p:last-of-type {\n\t\tmargin-bottom: 15px;\n\t}\n}\n\n.llms-button-action,\n.llms-button-danger,\n.llms-button-primary,\n.llms-button-secondary {\n\t&.small .dashicons {\n\t\tfont-size: 13px;\n\t\theight: 13px;\n\t\twidth: 13px;\n\t}\n\n\t&:hover {\n\t\tbox-shadow: none;\n\t}\n}\n\na.llms-view-as {\n\tline-height: 2;\n\tmargin-right: 8px;\n}\n\n.llms-image-field-preview {\n\tmax-height: 80px;\n\tvertical-align: middle;\n\twidth: auto;\n}\n\n.llms-image-field-remove {\n\t&.hidden { display: none; }\n}\n\n.llms-log-viewer {\n\tbackground: #fff;\n\tborder: 1px solid #e5e5e5;\n\tbox-shadow: 0 1px 1px rgba(0,0,0,.04);\n\tmargin: 20px 0;\n\tpadding: 25px;\n\n\tpre {\n\t\tfont-family: monospace;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t\twhite-space: pre-wrap;\n\t}\n}\n\n.llms-status--tools {\n\t.llms-table {\n\t\tbackground: #fff;\n\t\tborder: 1px solid #e5e5e5;\n\t\tbox-shadow: 0 1px 1px rgba(0,0,0,.04);\n\t\ttd, th {\n\t\t\tpadding: 10px;\n\t\t\tvertical-align: top;\n\t\t}\n\t\tth {\n\t\t\twidth: 28%;\n\t\t}\n\t\tp {\n\t\t\tmargin: 0 0 10px;\n\t\t}\n\t}\n}\n\n.llms-error {\n\tcolor: $color-red;\n\tfont-style: italic;\n}\n\n.llms-rating-stars {\n\tcolor: #ffb900;\n\ttext-decoration: none;\n}\n\n@media screen and (max-width: 782px) {\n\t.llms-header {\n\t\ttop: 46px;\n\n\t\t.llms-inside-wrap {\n\t\t\tflex-direction: column;\n\t\t\tgap: 20px;\n\n\t\t\t.lifterlms-logo {\n\t\t\t\talign-self: center;\n\t\t\t\tflex: inherit;\n\t\t\t\tmax-height: initial;\n\t\t\t\tmax-width: 200px;\n\t\t\t}\n\n\t\t\t.llms-meta {\n\t\t\t\tcolumn-gap: 10px;\n\t\t\t}\n\t\t}\n\t}\n}\n\n.llms-basic-editor.ql-container .ql-editor {\n\tborder-radius: 4px;\n\tborder: 1px solid #8c8f94;\n\n\t&:focus {\n\t\tborder-color: var(--wp-admin-theme-color);\n\t\tbox-shadow: 0 0 0 .5px var(--wp-admin-theme-color);\n\t\toutline: 2px solid #0000;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/_media-protection.scss",
    "content": ".llms-media-protection-warning {\n\tborder-left: 4px solid $color-orange;\n\tpadding-left: 10px;\n\tmax-width: 400px;\n\tmargin: 10px 0;\n}\n"
  },
  {
    "path": "assets/scss/admin/_quiz-attempt-review.scss",
    "content": ".llms-remarks {\n\n\t.llms-remarks-field {\n\t\theight: 120px;\n\t\twidth: 100%;\n\t}\n\n\tinput[type=\"number\"] {\n\t\twidth: 60px;\n\t}\n\n\n}\n\n\nbutton[name=\"llms_quiz_attempt_action\"] {\n\t.save { display: none; }\n\t&.grading {\n\t\t.default { display: none };\n\t\t.save { display: inline; }\n\t}\n}\n\n"
  },
  {
    "path": "assets/scss/admin/_reporting.scss",
    "content": ".llms-reporting.wrap {\n\n\t.llms-options-page-contents {\n\n\t\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\t\tbox-shadow: 0 1px 3px rgba( 0, 0, 0, 0.15 );\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\n\t\t}\n\t}\n\n\t.llms-stab-title {\n\t\tcolor: $color-brand-dark-blue;\n\t\tfont-size: 36px;\n\t\tfont-weight: 300;\n\t\tmargin-bottom: 20px;\n\t}\n\n\ttd.id a {\n\t\ttext-decoration: none;\n\t}\n\n\tth.id, td.id,\n\tth.name, td.name,\n\tth.registered, td.registered,\n\tth.last_seen, td.last_seen,\n\tth.overall_progress, td.overall_progress,\n\tth.title, td.title,\n\tth.course, td.course,\n\tth.lesson, td.lesson { text-align: left; }\n\n\ttd.section-title {\n\t\tbackground: #eaeaea;\n\t\ttext-align: left;\n\t\tfont-weight: 700;\n\t\tpadding: 16px 4px;\n\t}\n\n\ttd.questions-table {\n\t\ttext-align: left;\n\n\t\t.correct,\n\t\t.question,\n\t\t.selected {\n\t\t\ttext-align: left;\n\t\t\tmax-width: 300px;\n\n\t\t\timg {\n\t\t\t\theight: auto;\n\t\t\t\tmax-width: 64px;\n\t\t\t}\n\t\t}\n\t}\n\n\ttable.quiz-attempts {\n\t\tmargin-bottom: 40px;\n\t}\n\n\t&.tab--students {\n\t\t.llms-options-page-contents {\n\n\t\t\t#llms-award-certificate-wrapper .components-button.is-secondary {\n\t\t\t\tbackground: #e1e1e1;\n\t\t\t\tborder-radius: 8px;\n\t\t\t\tbox-shadow: none;\n\t\t\t\tcolor: #414141;\n\t\t\t\tfont-size: 13px;\n\t\t\t\tfont-weight: 700;\n\t\t\t\theight: auto;\n\t\t\t\tpadding: 8px 14px;\n\t\t\t}\n\n\t\t}\n\t}\n\n\t&.tab--enrollments,\n\t&.tab--sales {\n\n\t\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\n\t\t.llms-options-page-contents {\n\t\t\tbox-shadow: none;\n\t\t\tbackground: none;\n\t\t\tmargin-top: 20px;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\t.llms-nav-style-filters {\n\n\t\t\t.llms-analytics-form {\n\t\t\t\talign-items: center;\n\t\t\t\talign-self: center;\n\t\t\t\tcolor: #FFF;\n\t\t\t\tdisplay: flex;\n\t\t\t\tfont-size: 13px;\n\t\t\t\tgap: 5px;\n\n\t\t\t\tlabel {\n\t\t\t\t\tfont-weight: 700;\n\t\t\t\t}\n\n\t\t\t\tinput {\n\t\t\t\t\tborder: 0;\n\t\t\t\t\tfont-size: 13px;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 0 4px;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t\twidth: 110px;\n\t\t\t\t}\n\n\t\t\t\t.select2-container {\n\t\t\t\t\tinput {\n\t\t\t\t\t\twidth: 100% !important;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t.button.small {\n\t\t\theight: 23px;\n\t\t\tline-height: 23px;\n\t\t}\n\n\t\t.llms-analytics-filters {\n\t\t\tdisplay: none;\n\n\t\t\t.llms-inside-wrap {\n\t\t\t\tbackground-color: #FFF;\n\t\t\t\tbackground-color: #FFF;\n\t\t\t\tbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.13);\n\t\t\t\tmargin: -20px 10px 20px 10px;\n\t\t\t\tpadding: 20px;\n\t\t\t}\n\n\t\t\t.llms-nav-items {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 20px;\n\t\t\t\tjustify-content: space-between;\n\t\t\t\tmargin: 0;\n\n\t\t\t\t@media only screen and (min-width: 782px) {\n\t\t\t\t\tflex-direction: row;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t.llms-nav-item {\n\t\t\t\tbox-sizing: border-box;\n\t\t\t\twidth: 100%;\n\n\t\t\t\tlabel {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tfont-weight: 700;\n\t\t\t\t\tmargin: 0 0 5px 0;\n\t\t\t\t}\n\n\t\t\t\t.select2-selection__rendered{\n\t\t\t\t\tword-wrap: break-word;\n\t\t\t\t\ttext-overflow: inherit;\n\t\t\t\t\twhite-space: normal;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tp {\n\t\t\t\tmargin: 0;\n\t\t\t\ttext-align: right;\n\n\t\t\t\t.llms-button-primary {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\t}\n\n\t.llms-reporting-tab.llms-reporting-quiz .llms-table-filter-wrap {\n\t\twidth: 160px;\n\t}\n\n\t.llms-table-header {\n\t\tform { display: inline; }\n\t\t.button {\n\t\t\tmargin-left: 5px;\n\t\t}\n\t}\n}\n\n.llms-charts-wrapper {\n\tbackground-color: #FFF;\n\tborder: 1px solid #dedede;\n\tborder-radius: 12px;\n\tbox-shadow: 0px 0px 1px rgba(48, 49, 51, 0.05), 0px 2px 4px rgba(48, 49, 51, 0.1);\n\tpadding: 20px;\n}\n\n.llms-reporting-tab {\n\n\th1, h2, h3, h4, h5, h6 {\n\t\tmargin: 0;\n\t\ta {\n\t\t\tcolor: $color-brand-blue;\n\t\t\ttext-decoration: none;\n\t\t\t&:hover {\n\t\t\t\tcolor: $color-brand-blue;\n\t\t\t}\n\t\t}\n\t}\n\n\th2 {\n\t\tfont-size: 22px;\n\t\tline-height: 1.5;\n\n\t\t&.llms-table-title {\n\t\t\tmargin-bottom: 20px;\n\t\t}\n\n\t}\n\n\th5 {\n\t\tfont-size: 15px;\n\t\tline-height: 1.5;\n\t}\n\n\t.llms-reporting-body {\n\t\tbackground-color: #FFF;\n\t\tbox-shadow: 0 1px 3px rgba( 0, 0, 0, .13 );\n\t\tmargin: 20px auto;\n\t\tpadding: 0;\n\n\t\t.llms-reporting-stab {\n\t\t\tpadding: 30px;\n\n\t\t\t.llms-table-header {\n\t\t\t\tmargin: 0;\n\t\t\t}\n\t\t}\n\n\t\t.llms-gb-tab {\n\t\t\tpadding: 30px;\n\t\t}\n\n\t\t.llms-reporting-header {\n\t\t\tpadding: 30px;\n\t\t\tmargin: 0;\n\n\t\t\t.llms-reporting-header-img {\n\t\t\t\tborder-radius: 50%;\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tmargin-right: 10px;\n\t\t\t\toverflow: hidden;\n\t\t\t\tvertical-align: middle;\n\t\t\t\timg {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tmax-height: 64px;\n\t\t\t\t\twidth: auto;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-reporting-header-info {\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tvertical-align: middle;\n\n\t\t\t}\n\n\t\t}\n\t}\n\n}\n\n.llms-reporting-breadcrumbs {\n\tmargin: 0;\n\tpadding: 0;\n\ta {\n\t\tcolor: $color-brand-blue;\n\t\tfont-size: 15px;\n\t\ttext-decoration: none;\n\t\t&:hover {\n\t\t\tcolor: $color-brand-blue-dark;\n\t\t}\n\t\t&:after {\n\t\t\tcontent: ' > ';\n\t\t\tcolor: #646970;\n\t\t}\n\n\t\t&:last-child {\n\t\t\tcolor: #000;\n\t\t\tfont-weight: 700;\n\t\t\t&:after { display: none; }\n\t\t}\n\t}\n}\n\n#llms-students-table .name {\n\ttext-align: left;\n}\n\n.llms-reporting-tab-content {\n\tdisplay: flex;\n\n\t> header {\n\t\t@include clearfix;\n\t}\n\n\th3 {\n\t\tmargin-bottom: 20px;\n\t}\n\n\t.llms-reporting-tab-filter {\n\t\tfloat: right;\n\t\tposition: relative;\n\t\tmargin-right: 0.75em;\n\t\twidth: 180px;\n\t\ttop: -3px;\n\t}\n\n\n\t.llms-reporting-tab-main {\n\t\tflex: 3;\n\t\tmax-width: 75%;\n\t}\n\t.llms-reporting-tab-side {\n\t\tflex: 1;\n\t\tmargin-left: 20px;\n\t}\n\n\t> .llms-table-wrap {\n\t\tflex: 1;\n\t}\n\n}\n\n\n.llms-reporting-widgets {\n\t@include clearfix;\n}\n\n.llms-reporting-widget {\n\n\tborder-top: 4px solid $color-brand-blue;\n\tbackground: #fafafa;\n\tmargin-bottom: 10px;\n\tpadding: 30px;\n\t@include clearfix;\n\n\t.fa {\n\t\tcolor: #999;\n\t\tfloat: left;\n\t\tfont-size: 32px;\n\t\tmargin-right: 10px;\n\t}\n\n\tstrong {\n\t\tcolor: #333;\n\t\tfont-size: 20px;\n\t\tline-height: 1.2;\n\t}\n\n\t&.llms-reporting-student-address {\n\t\tstrong {\n\t\t\tline-height: 1.1;\n\t\t}\n\t}\n\n\tsup,\n\t.llms-price-currency-symbol {\n\t\tfont-size: 75%;\n\t\tposition: relative;\n\t\ttop: -4px;\n\t\tvertical-align: baseline;\n\t}\n\n\tsmall {\n\t\tfont-size: 13px;\n\t\t&.compare {\n\t\t\tmargin-left: 5px;\n\t\t\t&.positive {\n\t\t\t\tcolor: $color-green;\n\t\t\t}\n\t\t\t&.negative {\n\t\t\t\tcolor: $color-red;\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n.llms-reporting-event {\n\tborder-left: 4px solid #555;\n\tbackground: #fafafa;\n\tfont-size: 11px;\n\tline-height: 1.2;\n\tmargin-bottom: 0.75em;\n\tpadding: 10px;\n\t@include clearfix;\n\n\t&.color--blue {\n\t\tborder-left-color: $color-blue;\n\t}\n\n\t&.color--green,\n\t&._enrollment_trigger,\n\t&._is_complete.yes {\n\t\tborder-left-color: $color-green;\n\t}\n\n\t&.color--purple,\n\t&._status.enrolled {\n\t\tborder-left-color: $color-purple;\n\t}\n\n\t&.color--red,\n\t&._status.expired,\n\t&._status.cancelled {\n\t\tborder-left-color: $color-red;\n\t}\n\t&.color--orange,\n\t&._achievement_earned,\n\t&._certificate_earned,\n\t&._email_sent {\n\t\tborder-left-color: $color-orange;\n\t}\n\n\ttime {\n\t\tcolor: #888;\n\t}\n\n\t.llms-student-avatar {\n\t\tmargin-left: 10px;\n\t\tfloat: right;\n\t}\n\n\ta {\n\t\ttext-decoration: none;\n\t\tcolor: inherit;\n\t}\n\n}\n\n@media only screen and (min-width: 782px) {\n\t.llms-reporting.wrap {\n\t\t.llms-options-page-contents {\n\t\t\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\t\t\tmargin-left: 0;\n\t\t\t\tmargin-right: 0;\n\t\t\t}\n\t\t}\n\t}\n}\n\n@import \"../_includes/quiz-result-question-list\";\n"
  },
  {
    "path": "assets/scss/admin/_resources.scss",
    "content": ".wrap.llms-resources {\n\n\t.llms-inside-wrap {\n\t\tpadding-top: 30px;\n\t}\n\n\t#poststuff {\n\n\t\t#post-body.columns-2 {\n\t\t\tmargin-right: 350px;\n\n\t\t\t#side-sortables {\n\t\t\t\twidth: 330px;\n\n\t\t\t\t@media only screen and (max-width: 850px) {\n\t\t\t\t\twidth: auto;\n\t\t\t\t}\t\n\n\t\t\t}\n\n\t\t}\n\n\t\t#postbox-container-1 {\n\t\t\tfloat: right;\n\t\t\tmargin-right: -350px;\n\t\t\twidth: 330px\n\t\t}\n\n\t\th2 {\n\t\t\tpadding: 12px 20px;\n\t\t}\n\n\t}\n\n\t#poststuff \n\t.postbox {\n\t\tbackground-color: #FFF;\n\t\tborder: none;\n\t\tbox-shadow: 0 1px 3px rgba( 0, 0, 0, 0.13 );\n\n\t\t.postbox-header {\n\t\t\tborder-bottom-color: #efefef;\n\n\t\t}\n\n\t\t.inside {\n\t\t\tmargin: 0;\n\t\t\tpadding: 20px;\n\t\t}\n\t}\n\n\t#llms_dashboard_welcome_video {\n\n\t\t.llms-welcome-video {\n\n\t\t\tp {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tline-height: 1.5;\n\t\t\t\tmargin: 0 0 40px 0;\n\t\t\t}\n\n\t\t\t.llms-welcome-video-container {\n\t\t\t\theight: 0;\n\t\t\t\toverflow: hidden;\n\t\t\t\tpadding-top: 30px;\n\t\t\t\tpadding-bottom: 56.25%;\n\t\t\t\tposition: relative;\n\n\t\t\t\tiframe,\n\t\t\t\tobject,\n\t\t\t\tembed {\n\t\t\t\t\tleft: 0;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t#llms_dashboard_getting_started {\n\n\t\tul {\n\t\t\tmargin: 0 0 20px 0;\n\n\t\t\tli {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tline-height: 1.5;\n\t\t\t\tmargin-bottom: 15px;\n\t\t\t}\n\t\t}\n\n\t\t.llms-button-primary {\n\t\t\tdisplay: block;\n\t\t\tmargin-top: auto;\n\t\t\tmax-width: 300px;\n\t\t\ttext-align: center;\n\t\t}\n\n\t}\n\n\t#llms_dashboard_resource_links {\n\n\t\tul {\n\t\t\tlist-style: disc;\n\t\t\tmargin: 5px 0 0 20px;\n\n\t\t\tli {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tline-height: 1.5;\n\t\t\t}\n\t\t}\n\n\t\t.llms-resource-links {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: 1fr;\n\t\t\tgrid-gap: 60px;\n\n\t\t\ta {\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\n\t\t\t.llms-list {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\n\t\t\t\th3 {\n\t\t\t\t\tmargin: 0 0 10px 0;\n\n\t\t\t\t\t.dashicons {\n\t\t\t\t\t\tcolor: #AAA;\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tul {\n\t\t\t\t\tmargin-bottom: 20px;\n\t\t\t\t}\n\n\t\t\t\t.llms-button-primary,\n\t\t\t\t.llms-button-secondary,\n\t\t\t\t.llms-button-action {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tmargin-top: auto;\n\t\t\t\t\tmax-width: 300px;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t@media only screen and (min-width: 782px) {\n\t\t\t\tgrid-template-columns: 1fr 1fr 1fr;\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/_settings.scss",
    "content": ".wrap.llms-reporting,\n.wrap.lifterlms-settings,\n.wrap.llms-status {\n\n\t.llms-inside-wrap {\n\t\tbox-sizing: border-box;\n\t\tmargin: 0 auto;\n\n\t\t.llms-nav-text {\n\t\t\tmargin: 0 auto;\n\t\t}\n\t}\n\n\t.llms-subheader {\n\n\t\t.llms-save {\n\t\t\tflex: auto;\n\t\t\ttext-align: right;\n\t\t}\n\n\t}\n\n\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\tbackground-color: #FFF;\n\t\tbox-shadow: 0 1px 3px rgba( 0, 0, 0, 0.15 );\n\t\tmargin: 0 -20px 20px -10px;\n\n\t\t.llms-nav-items {\n\t\t\tpadding-left: 0;\n\t\t}\n\n\t\t.llms-nav-item {\n\t\t\t.llms-nav-link:hover {\n\t\t\t\tbackground: #f0f0f1;\n\t\t\t\tcolor: #222222;\n\t\t\t}\n\n\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\tbackground: #fafafa;\n\t\t\t\tcolor: $color-blue;\n\t\t\t\tborder-top-color: $color-blue;\n\t\t\t}\n\n\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\tfont-weight: 700;\n\t\t\t}\n\t\t}\n\n\t\t.llms-nav-link {\n\t\t\tborder-top: 2px solid transparent;\n\t\t\tpadding: 14px;\n\t\t}\n\n\t}\n\n\t.llms-setting-group {\n\t\tbackground-color: #FFF;\n\t\tbox-shadow: 0 1px 3px rgba( 0, 0, 0, .13 );\n\t\tmargin: 20px auto;\n\t\tpadding: 20px;\n\n\t\t.llms-label {\n\t\t\tborder-bottom: 1px solid #efefef;\n\t\t\tfont-weight: 700;\n\t\t\tfont-size: 20px;\n\t\t\tpadding: 20px;\n\t\t\tmargin: -20px -20px 20px;\n\n\t\t\t.llms-button-primary {\n\t\t\t\tmargin-left: 10px;\n\t\t\t}\n\t\t}\n\n\t\t.llms-help-tooltip .dashicons {\n\t\t\tcolor: #444;\n\t\t\tcursor: help;\n\t\t}\n\n\t\t.form-table {\n\t\t\tmargin: 0;\n\t\t\ttr:first-child .llms-subtitle {\n\t\t\t\tmargin-top: 0;\n\t\t\t}\n\t\t}\n\n\t\ttd[colspan=\"2\"] {\n\t\t\tpadding-top: 0;\n\t\t\tpadding-left: 0;\n\t\t}\n\n\t\ttr.llms-disabled-field {\n\t\t\topacity: 0.5;\n\t\t\tpointer-events: none;\n\t\t}\n\n\t\tp {\n\t\t\tfont-size: 14px;\n\t\t}\n\t\tinput[type=\"text\"],\n\t\tinput[type=\"password\"],\n\t\tinput[type=\"datetime\"],\n\t\tinput[type=\"datetime-local\"],\n\t\tinput[type=\"date\"],\n\t\tinput[type=\"month\"],\n\t\tinput[type=\"time\"],\n\t\tinput[type=\"week\"],\n\t\tinput[type=\"number\"],\n\t\tinput[type=\"email\"],\n\t\tinput[type=\"url\"],\n\t\tinput[type=\"search\"],\n\t\tinput[type=\"tel\"],\n\t\tinput[type=\"color\"],\n\t\tselect,\n\t\ttextarea:not(.wp-editor-area) {\n\t\t\twidth: 50%;\n\t\t\t&.medium { width: 30%; }\n\t\t\t&.small { width: 20%; }\n\t\t\t&.tiny { width: 10%; }\n\t\t}\n\t}\n\n\t@media only screen and (min-width: 782px) {\n\t\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\t\t.llms-nav-item {\n\t\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\t\tbackground: #fff;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Email Delivery providers.\n\t#llms-mailhawk-connect {\n\t\theight: auto;\n\t\tmargin: 0 0 6px;\n\t\tposition: relative;\n\n\t\t.dashicons {\n\t\t\tmargin: -4px 4px 0 0;\n\t\t\tvertical-align: middle;\n\t\t}\n\t}\n\t#llms-sendwp-connect {\n\t\theight: auto;\n\t\tmargin: 0 0 6px;\n\t\tposition: relative;\n\n\t\t.fa {\n\t\t\tmargin-right: 4px;\n\t\t}\n\t}\n\n}\n\n@media only screen and (min-width: 782px) {\n\t.wrap.lifterlms-settings {\n\t\t.llms-subheader {\n\t\t\theight: 40px;\n\t\t\tposition: sticky;\n\t\t\ttop: 32px;\n\t\t}\n\t\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\t\tmargin: 0 -20px 20px -22px;\n\n\t\t\t.llms-nav-items {\n\t\t\t\tpadding-left: 10px;\n\t\t\t}\n\n\t\t}\n\t}\n\n\t.wrap.llms-reporting {\n\t\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\t\tmargin: 0 -20px 20px -22px;\n\n\t\t\t.llms-nav-items {\n\t\t\t\tpadding-left: 10px;\n\t\t\t}\n\n\t\t}\n\t}\n\t\n\n\t.wrap.llms-status {\n\t\t.llms-nav-tab-wrapper.llms-nav-secondary {\n\t\t\tmargin: 0 -20px 20px -22px;\n\n\t\t\t.llms-nav-items {\n\t\t\t\tpadding-left: 10px;\n\t\t\t}\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/_tabs.scss",
    "content": ".llms-nav-tab-wrapper {\n\tbackground: $color-blue;\n\tmargin: 20px 0;\n\n\t&.llms-nav-secondary {\n\t\tbackground: #e1e1e1;\n\n\t\t.llms-nav-item {\n\t\t\tmargin: 0;\n\n\t\t\t.llms-nav-link:hover,\n\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\tbackground: darken( #e1e1e1, 8 );\n\t\t\t}\n\n\t\t}\n\n\t\t.llms-nav-link {\n\t\t\tcolor: #414141;\n\t\t\tfont-size: 15px;\n\t\t\tpadding: 8px 14px;\n\n\t\t\t.dashicons {\n\t\t\t\tfont-size: 15px;\n\t\t\t\theight: 15px;\n\t\t\t\twidth: 15px;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t&.llms-nav-text {\n\t\tbackground: inherit;\n\t\t.llms-nav-items {\n\t\t\tpadding-left: 0;\n\t\t}\n\t\t.llms-nav-item {\n\t\t\tbackground: inherit;\n\t\t\tcolor: #646970;\n\t\t\t&:last-child:after {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\t\t\t&:after {\n\t\t\t\tcontent: '|';\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tmargin: 0 8px 0 6px;\n\t\t\t}\n\t\t\t.llms-nav-link:hover {\n\t\t\t\tbackground: inherit;\n\t\t\t\tcolor: $color-brand-blue;\n\t\t\t}\n\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\tbackground: inherit;\n\t\t\t\tcolor: #000;\n\t\t\t\tfont-weight: 600;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t\t.llms-nav-link {\n\t\t\t\tcolor: $color-brand-blue;\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tletter-spacing: 0;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 0;\n\t\t\t\ttext-decoration: underline;\n\t\t\t\ttext-transform: none;\n\t\t\t}\n\t\t}\n\t}\n\n\t&.llms-nav-style-tabs {\n\t\tbackground-color: $color-brand-dark-blue;\n\t\tmargin: 0;\n\t\tpadding-top: 8px;\n\n\t\t.llms-nav-item {\n\t\t\tmargin: 0 3px;\n\n\t\t\t.llms-nav-link {\n\t\t\t\tborder-top-left-radius: 4px;\n\t\t\t\tborder-top-right-radius: 4px;\n\t\t\t}\n\n\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\tbackground-color: #FFF;\n\t\t\t\tcolor: $color-brand-blue;\n\t\t\t\tfont-weight: 700;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t&.llms-nav-style-filters {\n\t\tbackground-color: $color-brand-blue;\n\t\tborder-radius: 12px;\n\t\tmargin: 20px 0;\n\t\toverflow: hidden;\n\t\tpadding: 0;\n\n\t\t.llms-nav-items {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tjustify-content: space-between;\n\t\t\tpadding-left: 0;\n\n\t\t\t@media only screen and (min-width: 782px) {\n\t\t\t\tflex-direction: row;\n\t\t\t}\n\n\t\t\t.llms-nav-item {\n\t\t\t\tfloat: none;\n\n\t\t\t\t.llms-nav-link {\n\t\t\t\t\tpadding: 14px;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-nav-items {\n\t\t@include clearfix;\n\t\tmargin: 0;\n\t\tpadding-left: 10px;\n\t}\n\n\t\t.llms-nav-item {\n\t\t\tmargin: 0;\n\n\t\t\t.llms-nav-link:hover {\n\t\t\t\tbackground: $color-brand-blue;\n\t\t\t}\n\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\tbackground: $color-brand-dark-blue;\n\t\t\t}\n\n\t\t\t&.llms-active .llms-nav-link {\n\t\t\t\tfont-weight: 400;\n\t\t\t}\n\n\t\t\t@media only screen and (min-width: 768px) {\n\t\t\t\tfloat: left;\n\n\t\t\t\t&.llms-nav-item-right {\n\t\t\t\t\tfloat: right;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t\t.llms-nav-link {\n\n\t\t\t\tcolor: #fff;\n\t\t\t\tcursor: pointer;\n\t\t\t\tdisplay: block;\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 15px;\n\t\t\t\tpadding: 9px 18px;\n\t\t\t\ttext-align: center;\n\t\t\t\ttext-decoration: none;\n\t\t\t\ttransition: all .3s ease;\n\n\t\t\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/_wp-menu.scss",
    "content": "#adminmenu {\n\n\t.toplevel_page_lifterlms .wp-menu-image img {\n\t\tpadding-top: 6px;\n\t\twidth: 20px;\n\t}\n\n\t.toplevel_page_lifterlms,\n\t.opensub .wp-submenu li.current,\n\t.wp-submenu li.current,\n\t.wp-submenu li.current,\n\t.wp-submenu li.current,\n\ta.wp-has-current-submenu:focus+.wp-submenu li.current {\n\t\ta[href*=\"page=llms-add-ons\"] {\n\t\t\tcolor: $color-brand-orange;\n\t\t}\n\t}\n\n}\n\n\n"
  },
  {
    "path": "assets/scss/admin/breakpoints/_1030up.scss",
    "content": "/******************************************************************\n\nDesktop Stylesheet\n\n******************************************************************/\n\n//option page tab menu\n.llms-nav-tab {\n\tdisplay: inline-block;\n\twidth: 33.333%;\n}\n.llms-nav-tab-settings {\n\tdisplay: inline-block;\n\twidth: 25%;\n}\n\n//select box form wrapper\n#llms-form-wrapper {\n\t.llms-select {\n\t\tdisplay: inline-block;\n\t\twidth: 47.5%;\n\t\t&:first-child {\n\t\t\tmargin-right: 5%;\n\t\t}\n\n\t}.llms-filter-options {\n\t\tdisplay: inline-block;\n\t\twidth: 47.5%;\n\n\t\t&.date-filter {\n\t\t\tmargin-right: 5%;\n\t\t}.llms-date-select {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\n\t}.llms-date-select {\n\t\twidth: 47.5%;\n\n\t\t&:first-child {\n\t\t\tmargin-right: 5%\n\t\t}\n\n\t}\n}\n\n.llms-widget-row {\n\t@include clearfix;\n\t.llms-widget-1-5 {\n\t\tvertical-align: top;\n\t\twidth: 20%;\n\t\tfloat: left;\n\t\tbox-sizing: border-box;\n\t\tpadding: 0 5px;\n\t}\n\t.llms-widget-1-4 {\n\t\tvertical-align: top;\n\t\twidth: 25%;\n\t\tfloat: left;\n\t\tbox-sizing: border-box;\n\t\tpadding: 0 5px;\n\t}\n\t.llms-widget-1-3 {\n\t\twidth: 33.3%;\n\t\tfloat: left;\n\t\tbox-sizing: border-box;\n\t\tpadding: 0 5px;\n\t}\n\t.llms-widget-1-2 {\n\t\twidth: 50%;\n\t\tfloat: left;\n\t\tbox-sizing: border-box;\n\t\tpadding: 0 5px;\n\t\tvertical-align: top;\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/breakpoints/_1240up.scss",
    "content": "/******************************************************************\n\nlarge Monitor Stylesheet\n\n******************************************************************/\n\n.llms-nav-tab-filters,\n.llms-nav-tab-settings {\n\tfloat: left;\n\twidth: 12.5%;\n}\n"
  },
  {
    "path": "assets/scss/admin/breakpoints/_481up.scss",
    "content": "/******************************************************************\n\nLarger Phones\n\n******************************************************************/\n\n//select box form wrapper\n#llms-form-wrapper {\n\t\n\t.llms-checkbox {\n\t\twidth: 33%;\n\t\t//text-align: center;\n\t\t\t\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/breakpoints/_768up.scss",
    "content": "/******************************************************************\n\nTablets and small computers\n\n******************************************************************/\n\nul.tabs li{\n      display: inline-block;\n    }\n\n//option page tab menu\n.llms-nav-tab {\n\tdisplay: inline-block;\n\twidth: 33%;\n}\n.llms-nav-tab-settings {\n\tdisplay: inline-block;\n\twidth: 25%;\n}\n\n//select box form wrapper\n#llms-form-wrapper {\n\t.llms-select {\n\t\twidth: 50%;\n\t\tmax-width: 500px;\n\n\t}.llms-filter-options {\n\t\twidth: 50%;\n\t\t//display: inline-block;\n\t\tmax-width: 500px;\n\n\t}.llms-date-select {\n\t\twidth: 47.5%;\n\n\t\t&:first-child {\n\t\t\tmargin-right: 5%\n\t\t}\n\n\t}\n}\n\n.llms-widget {\n\tinput[type=\"text\"],\n\tinput[type=\"password\"],\n\tinput[type=\"datetime\"],\n\tinput[type=\"datetime-local\"],\n\tinput[type=\"date\"],\n\tinput[type=\"month\"],\n\tinput[type=\"time\"],\n\tinput[type=\"week\"],\n\tinput[type=\"number\"],\n\tinput[type=\"email\"],\n\tinput[type=\"url\"],\n\tinput[type=\"search\"],\n\tinput[type=\"tel\"],\n\tinput[type=\"color\"],\n\tselect,\n\ttextarea, {\n\t\twidth: 50%;\n\n\t\t&.medium { width: 30%; }\n\t\t&.small { width: 20%; }\n\t\t&.tiny { width: 10%; }\n\t}\n\n\t// .form-table th {\n\t// \twidth: 140px;\n\t// }\n\n}\n\n\n\n"
  },
  {
    "path": "assets/scss/admin/breakpoints/_base.scss",
    "content": "/******************************************************************\n\nBase Mobile\n\n******************************************************************/\n\n.llms-nav-tab,\n.llms-nav-tab-filters {\n\tdisplay: block;\n\twidth: 100%;\n}\n\nform.llms-nav-tab-filters.full-width {\n\twidth: 100%;\n\n\tlabel {\n\t\tdisplay: inline-block;\n\t\twidth: 10%;\n\t\ttext-align: left;\n\t}\n\n\t.select2-container {\n\t\twidth: 85% !important;\n\t}\n}\n\n.llms-nav-tab-settings {\n\tdisplay: block;\n\twidth: 100%;\n}\n\n//select box form wrapper\n#llms-form-wrapper {\n\t.llms-select {\n\t\twidth: 100%;\n\t\tmargin-bottom: 20px;\n\n\t}.llms-checkbox {\n\t\tdisplay: inline-block;\n\t\twidth: 100%;\n\t\ttext-align: left;\n\n\t}.llms-filter-options {\n\t\twidth: 100%;\n\t\t//margin-bottom: 20px;\n\n\t}.llms-date-select {\n\t\twidth: 100%;\n\t\tdisplay: inline-block;\n\t\tmargin-bottom: 20px;\n\t\tinput[type=\"text\"] {\n\t\t\twidth: 100%;\n\t\t}\n\n\t}.llms-search-button {\n\t\t//display: inline-block;\n\t\t//width: 30%;\n\t\t#llms-search-button {\n\n\t\t//float: right;\n\t}\n\n\t}\n\n}\n\n// .llms-widget-full {\n// \t&.top {\n// \t\tmargin-top: 20px;\n// \t}\n// }\n// .llms-widget {\n// \t.form-table td {\n// \t\tpadding: 15px 0;\n// \t\tul { margin: 5px 0 0; }\n\n\n// \t\t.conditional-field {\n// \t\t\tdisplay: none;\n// \t\t\tmargin-left: 25px;\n// \t\t}\n// \t\t.conditional-radio:checked ~ .conditional-field {\n// \t\t\tdisplay: block;\n// \t\t}\n\n\n// \t}\n// }\n\nul.tabs li{\n      display: block;\n    }\n\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_builder-launcher.scss",
    "content": ".llms-builder-launcher {\n\n\tp {\n\t\tmargin-top: 0;\n\t}\n\n\tol {\n\t\tmargin-top: -6px;\n\t}\n\n\t.llms-button-primary {\n\t\tbox-sizing: border-box;\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_llms-metabox.scss",
    "content": "\n// This is a \"legacy\" rule that may be removable\n.llms-mb-list {\n\n\tlabel {\n\t\tfont-size: 15px;\n\t\tfont-weight: 700;\n\t\tline-height: 1.5;\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t}\n\n\t.description {\n\t\tmargin-bottom: 8px;\n\t}\n\n\t.input-full {\n\t\twidth: 100%;\n\t}\n}\n\n\n#poststuff .llms-metabox {\n\n\t@extend %cf;\n\n\th2, h3, h4, h6 {\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t}\n\n\th2 {\n\t\tfont-size: 18px;\n\t\tfont-weight: 700;\n\t}\n\n\th3 {\n\t\tcolor: #1e1e1e;\n\t\tfont-size: 16px;\n\t}\n\n\th4 {\n\t\tcolor: #1e1e1e;\n\t\tfont-size: 16px;\n\t\tline-height: 1.5;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t}\n\n\th4:not(.llms-collapsible-body h4:first-of-type) {\n\t\tmargin-top: 20px;\n\t}\n\n\t.llms-transaction-test-mode {\n\t\tbackground: #ffffd7;\n\t\tfont-style: italic;\n\t\tleft: 0;\n\t\tpadding: 2px;\n\t\tposition: absolute;\n\t\tright: 0;\n\t\ttop: 0;\n\t\ttext-align: center;\n\t}\n\n\ta.llms-editable,\n\t.llms-metabox-icon,\n\tbutton.llms-editable {\n\t\tcolor: $color-grey;\n\t\ttext-decoration: none;\n\t\t&:hover {\n\t\t\tcolor: $color-brand-blue;\n\t\t}\n\t}\n\n\tbutton.llms-editable {\n\t\tborder: none;\n\t\tbackground: none;\n\t\tcursor: pointer;\n\t\tpadding: 0;\n\t\tvertical-align: top;\n\t}\n\n\th4 button.llms-editable {\n\t\tfloat: right;\n\t}\n\n\t.llms-table {\n\t\tmargin-top: 10px;\n\n\t}\n\n\t.llms-button-primary {\n\t\tborder-radius: 8px;\n\t\tfont-size: 16px;\n\t\tfont-weight: 700;\n\t\theight: auto;\n\t}\n}\n\n.llms-metabox-section {\n\tbackground: #fff;\n\tmargin-top: 25px;\n\tposition: relative;\n\n\t&.no-top-margin {\n\t\tmargin-top: 0;\n\t}\n\n\t.llms-metabox-field {\n\t\tmargin: 5px 0;\n\t\tposition: relative;\n\t\tlabel {\n\t\t\tcolor: #1e1e1e;\n\t\t\tdisplay: block;\n\t\t\tmargin-bottom: 5px;\n\t\t\tfont-size: 14px;\n\t\t\tfont-weight: 400;\n\t\t}\n\n\t\tselect,\n\t\ttextarea,\n\t\tinput[type=\"text\"],\n\t\tinput[type=\"number\"] {\n\t\t\twidth: 100%;\n\t\t}\n\n\t\tinput.md-text {\n\t\t\twidth: 105px;\n\t\t}\n\n\t\tinput.sm-text {\n\t\t\twidth: 45px;\n\t\t}\n\n\t\t.llms-datetime-field {\n\n\t\t\t.llms-date-input {\n\t\t\t\twidth: 95px;\n\t\t\t}\n\t\t\t.llms-time-input {\n\t\t\t\twidth: 45px;\n\t\t\t}\n\t\t\tem {\n\t\t\t\tfont-style: normal;\n\t\t\t\tpadding: 0 3px;\n\t\t\t}\n\n\t\t}\n\n\t\t.select2-container--default .select2-search--inline .select2-search__field {\n\t\t\tmin-height: auto;\n\t\t}\n\n\t\t.select2-container--default .select2-selection--multiple .select2-selection__rendered li {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\n\t}\n\n\t/* Fixes to tighten the space between tinymce editor and label */\n\t.llms-metabox-field:has(.wp-editor-wrap) label {\n\t\tposition: relative;\n\t\tz-index: 5;\n\t}\n\n\t.llms-metabox-field .wp-editor-wrap {\n\t\tmargin-top: -30px;\n\t}\n\n}\n\n.llms-collapsible {\n\n\t@extend %clearfix;\n\n\tbackground: #fff;\n\tborder: 1px solid #efefef;\n\tborder-radius: 6px;\n\tbox-shadow: 2px 2px 8px rgba(0, 0, 0, 0.08);\n\tdisplay: block;\n\tmargin: 0;\n\tpadding: 0;\n\tposition: relative;\n\ttext-align: center;\n\tmargin-top: 0;\n\n\t&:last-child {\n\t\tmargin-bottom: 0;\n\t}\n\n\t&.opened .llms-collapsible-header {\n\t\t.dashicons-arrow-down {\n\t\t\tdisplay: none;\n\t\t}\n\t\t.dashicons-arrow-up {\n\t\t\tdisplay: inline;\n\t\t}\n\t}\n\n\t.llms-collapsible-header {\n\t\t@extend %clearfix;\n\t\tpadding: 15px;\n\n\t\t[class^=\"d-\"] {\n\t\t\talign-items: center;\n\t\t\tdisplay: flex;\n\t\t\tgap: 5px;\n\t\t}\n\n\t\t.d-right {\n\t\t\tjustify-content: flex-end;\n\t\t\tpadding-right: 0;\n\t\t}\n\n\t\th3 {\n\t\t\tcolor: #1e1e1e;\n\t\t\tdisplay: inline;\n\t\t\tmargin: 0;\n\t\t\tfont-size: 16px;\n\t\t\tfont-weight: 700;\n\n\t\t\tsmall {\n\t\t\t\tfont-weight: 400;\n\t\t\t}\n\t\t}\n\n\t\t.dashicons-arrow-up {\n\t\t\tdisplay: inline;\n\t\t}\n\t\t.dashicons-arrow-up {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\ta {\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t.dashicons {\n\t\t\tcolor: #1e1e1e;\n\t\t\tcursor: pointer;\n\t\t\ttransition: color .4s ease;\n\t\t\t&:hover {\n\t\t\t\tcolor: $color-blue;\n\t\t\t}\n\n\t\t\t&.dashicons-warning,&.dashicons-warning:hover,\n\t\t\t&.dashicons-trash:hover,\n\t\t\t&.dashicons-no:hover {\n\t\t\t\tcolor: $color-danger;\n\t\t\t}\n\t\t\t&.dashicons-warning.medium-danger {\n\t\t\t\t&,\n\t\t\t\t&:hover {\n\t\t\t\t\tcolor: $color-orange;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\t.llms-collapsible-body {\n\t\t@extend %clearfix;\n\t\tdisplay: none;\n\t\tpadding: 15px;\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_metabox-engagements-type.scss",
    "content": ".submitbox .llms-mb-section,\n.llms-award-engagement-submitbox .llms-mb-list {\n\tmargin-bottom: 12px;\n\t&:last-of-type {\n\t\tmargin-bottom: 0;\n\t}\n\t@at-root .sync-action,\n\t&.student-info,\n\t&.post_author_override label {\n\t\t&:before {\n\t\t\t// dashicons-admin-users.\n\t\t\tfont: normal 20px/1 dashicons;\n\t\t\tspeak: never;\n\t\t\tdisplay: inline-block;\n\t\t\tmargin-left: -1px;\n\t\t\tpadding-right: 3px;\n\t\t\tvertical-align: top;\n\t\t\t-webkit-font-smoothing: antialiased;\n\t\t\t-moz-osx-font-smoothing: grayscale;\n\t\t\tposition: relative;\n\t\t\ttop: -1px;\n\t\t\tcolor: #8c8f94;\n\t\t\tbody:not(.admin-color-fresh) & {\n\t\t\t\tcolor: currentColor; // Used when selecting a different admin color scheme from the default one.\n\t\t\t}\n\t\t}\n\t}\n\t&.student-info,\n\t&.post_author_override label {\n\t\t&:before {\n\t\t\tcontent: '\\f110';\n\t\t}\n\t}\n\t@at-root .sync-action:before {\n\t\tcontent: '\\f113';\n\t\tcolor: white;\n\t}\n\t&.post_author_override label {\n\t\tdisplay: inline-block;\n\t\twidth: auto;;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_metabox-field-repeater.scss",
    "content": ".llms-mb-container .tab-content ul:not(.select2-selection__rendered).llms-mb-repeater-fields > li.llms-mb-list {\n\tborder-bottom: none;\n\tpadding: 0 0 10px;\n}\n\n.llms-mb-list.repeater {\n\n\t.llms-repeater-rows {\n\t\tposition: relative;\n\t\tmargin-top: 10px;\n\t\tmin-height: 10px;\n\n\t\t&.dragging {\n\t\t\tbackground: #efefef;\n\t\t\tbox-shadow: inset 0 0 0 1px #e5e5e5;\n\t\t}\n\t}\n\n\t.llms-repeater-row {\n\t\tbackground: #fff;\n\t}\n\n\t.llms-mb-repeater-fields {\n\n\t}\n\n\t.llms-mb-repeater-footer {\n\t\ttext-align: right;\n\t\tmargin-top: 20px;\n\t}\n\n\t.tmce-active .wp-editor-area {\n\t\tcolor: #32373c; // wp core default color\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_metabox-instructors.scss",
    "content": "._llms_instructors_data.repeater {\n\t.llms-repeater-rows .llms-repeater-row:first-child {\n\t\t.llms-repeater-remove { display: none; }\n\t}\n\n\t.llms-mb-list {\n\t\tpadding: 0 5px !important;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_metabox-orders.scss",
    "content": ".post-type-llms_order #post-body-content { display: none; }\n#lifterlms-order-details {\n\t.handlediv,\n\t.handlediv.button-link,\n\t.postbox-header { display: none;}\n\t.inside {\n\t\tpadding: 20px;\n\t\tmargin-top: 0;\n\n\t}\n}\n\n// failed transaction color\n.llms-table tbody tr.llms-txn-failed td {\n\tbackground-color: rgba( $color-red, 0.5 );\n\tborder-bottom-color: rgba( $color-red, 0.5 );\n}\n\n// refunded transaction color\n.llms-table tbody tr.llms-txn-refunded td {\n\tbackground-color: rgba( orange, 0.5 );\n\tborder-bottom-color: rgba( orange, 0.5 );\n}\n\n.llms-txn-refund-form,\n.llms-manual-txn-form {\n\t.llms-metabox-section {\n\t\tmargin-top: 0;\n\t}\n\t.llms-metabox-field {\n\t\ttext-align: right;\n\t\tinput {\n\t\t\t&[type=\"number\"] { max-width: 100px; }\n\t\t\t&[type=\"text\"] { max-width: 340px; }\n\n\t\t}\n\t}\n}\n\n.llms-manual-txn-form {\n\tbackground-color: #eaeaea;\n\t.llms-metabox-section {\n\t\tbackground-color: #eaeaea;\n\t}\n}\n\n#llms-remaining-edit {\n\tdisplay: none;\n}\n.llms-remaining-edit--content {\n\tlabel, span, textarea {\n\t\tdisplay: block;\n\t}\n\n\tlabel {\n\t\tmargin-bottom: 20px;\n\t}\n\n\ttextarea, input {\n\t\twidth: 100%;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_metabox-product.scss",
    "content": ".llms-metabox {\n\n\t> p:first-child {\n\t\tmargin-top: 0;\n\t}\n\n\t#llms-new-access-plan-model {\n\t\tdisplay: none;\n\t}\n\n\t.llms-access-plans {\n\t\t@extend %clearfix;\n\t\tcolor: #1e1e1e;\n\n\t\t> .llms-no-plans-msg {\n\t\t\tdisplay: none;\n\t\t\t.notice {\n\t\t\t\tmargin-right: 0;\n\t\t\t\tmargin-left: 0;\n\t\t\t}\n\t\t}\n\t\t> .llms-no-plans-msg:last-child {\n\t\t\tdisplay: block;\n\t\t}\n\n\t\t&.dragging {\n\t\t\tbackground: #efefef;\n\t\t\tbox-shadow: inset 0 0 0 1px #e5e5e5;\n\t\t}\n\n\t\t.llms-spinning {\n\t\t\tz-index: 1;\n\t\t}\n\n\t\t.llms-invalid {\n\t\t\tborder-color: $color-danger;\n\t\t\t.dashicons-warning {\n\t\t\t\tdisplay: inline;\n\t\t\t}\n\t\t}\n\t\t.llms-needs-attention .dashicons-warning.medium-danger {\n\t\t\tdisplay: inline;\n\t\t}\n\t\t.dashicons-warning {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t.llms-access-plan {\n\t\tmargin-bottom: 20px;\n\t\ttext-align: left;\n\n\t\t[data-tip]:before {\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.llms-plan-link,\n\t\t[data-controller] {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t&:hover,\n\t\t&.opened {\n\t\t\t.llms-plan-link {\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\t\t}\n\n\t\t.llms-metabox-field {\n\t\t\tmargin: 15px 0;\n\t\t}\n\n\t\t.llms-required {\n\t\t\tcolor: $color-danger;\n\t\t\tmargin-left: 3px;\n\t\t}\n\t\t.notice {\n\t\t\tmargin-left: 0;\n\t\t}\n\t}\n\n}\n\n.llms-dialog-container,\n.llms-dialog-overlay {\n\tposition: fixed;\n\tinset: 0;\n}\n\n.llms-dialog-container {\n\tz-index: 2;\n\tdisplay: flex;\n\n\t.llms-dialog-overlay {\n\t\tbackground-color: rgb(43 46 56 / 0.9);\n\t}\n\n\t&[aria-hidden='true'] {\n\t\tdisplay: none;\n\t}\n\n\t.llms-dialog-content {\n\t\tmargin: auto;\n\t\tpadding: 2em;\n\t\tz-index: 2;\n\t\tmax-width: 90%;\n\t\twidth: 700px;\n\t\tborder-radius: 6px;\n\t\tposition: relative;\n\t\tbackground-color: white;\n\t\toverflow: auto;\n\t\tmax-height: 90vh;\n\n\t\t.llms-dialog-close {\n\t\t\tposition: absolute;\n\t\t\ttop: 0.5em;\n\t\t\tright: 0.5em;\n\t\t\tborder: 0;\n\t\t\tpadding: 0.25em;\n\t\t\tbackground-color: transparent;\n\t\t\tfont-size: 1.5em;\n\t\t\twidth: 1.5em;\n\t\t\theight: 1.5em;\n\t\t\ttext-align: center;\n\t\t\tcursor: pointer;\n\t\t\ttransition: 0.15s;\n\t\t\tborder-radius: 50%;\n\t\t}\n\n\t\t.llms-access-plan-templates {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: 1fr 1fr 1fr;\n\t\t\tgrid-gap: 15px;\n\n\t\t\tbutton, a {\n\t\t\t\tbackground: #e1e1e1;\n\t\t\t\tborder: 1px solid $color-cinder;\n\t\t\t\tcolor: $color-cinder;\n\t\t\t\tpadding: 25px;\n\t\t\t\tfont-size: 14px;\n\t\t\t\ttext-align: center;\n\t\t\t\tvertical-align: baseline;\n\n\t\t\t\tstrong {\n\t\t\t\t\tfont-size: 18px;\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t}\n\t\t\t\tspan {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\n\t\t\t\tposition: relative;\n\t\t\t\ttext-decoration: none;\n\t\t\t\ttransition: transform .2s;\n\t\t\t\tcursor: pointer;\n\t\t\t}\n\n\t\t\ta {\n\t\t\t\tborder: 1px solid $color-brand-blue;\n\t\t\t\tbackground: #efefef;\n\t\t\t}\n\n\t\t\tspan.add-on {\n\t\t\t\tbackground-color: #999;\n\t\t\t\tborder-bottom-left-radius: 5px;\n\t\t\t\tborder-bottom-right-radius: 5px;\n\t\t\t\tcolor: #FFF;\n\t\t\t\tfont-size: 11px;\n\t\t\t\tfont-weight: bold;\n\t\t\t\tleft: 10px;\n\t\t\t\ttext-align: center;\n\t\t\t\tpadding: 2px 6px;\n\t\t\t\tposition: absolute;\n\t\t\t\ttext-transform: uppercase;\n\t\t\t\ttop: 0;\n\t\t\t\twidth: auto;\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n"
  },
  {
    "path": "assets/scss/admin/metaboxes/_metabox-students.scss",
    "content": ".llms-metabox-students {\n\t.llms-table {\n\t\ttr .name {\n\t\t\ttext-align: left;\n\t\t}\n\t}\n\n\t.llms-add-student:hover {\n\t\tcolor: $color-green;\n\t}\n\t.llms-remove-student:hover {\n\t\tcolor: $color-red;\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/modules/_forms.scss",
    "content": "/******************************************************************\n\nForm Styles\n\n******************************************************************/\n\n// lifterlms form wrapper\n#llms-form-wrapper {\n\n\t// setup defaults\n\tinput[type=\"text\"],\n\tinput[type=\"password\"],\n\tinput[type=\"datetime\"],\n\tinput[type=\"datetime-local\"],\n\tinput[type=\"date\"],\n\tinput[type=\"month\"],\n\tinput[type=\"time\"],\n\tinput[type=\"week\"],\n\tinput[type=\"number\"],\n\tinput[type=\"email\"],\n\tinput[type=\"url\"],\n\tinput[type=\"search\"],\n\tinput[type=\"tel\"],\n\tinput[type=\"color\"],\n\tinput[type=\"checkbox\"],\n\tselect,\n\ttextarea,\n\t.llms-field {\n\n\t\t// a focused input (or hovered on)\n\t\t&:focus,\n\t\t&:active {\n\n\t\t} // end hover or focus\n\t}\n\n\t// sub wrapper for search filter form (analytics)\n\t.llms-search-form-wrapper {\n\t\tborder-bottom: 1px solid $color-grey;\n\t\tmargin: 20px 0;\n\n\t}\n\n\n\t#llms_analytics_search {\n\t\tborder:none !important;\n\t\ttext-shadow: none !important;\n\t\tborder: none !important;\n\t\toutline: none !important;\n\t\tbox-shadow: none !important;\n\t\tmargin: 0 !important;\n\t\tcolor: $color-white !important;\n\t\tbackground: $color-blue !important;\n\t\tborder-radius: 0;\n\t\ttransition: .5s;\n\n\t\t&:hover {\n\t\t\tbackground: $color-darkblue !important;\n\n\t\t}&:active {\n\t\t\tbackground: $color-lightblue !important;\n\t\t}\n\t}\n\n} // end input defaults\n\n\n#llms-skip-setup-form {\n\t.llms-admin-link {\n\t\tbackground:none!important;\n\t\tborder:none;\n\t\tpadding:0!important;\n\t\tcolor:#0074a2;\n\t\tcursor:pointer;\n\t\t&:hover {\n\t\t\tcolor:#2ea2cc\n\t\t}&:focus{\n\t\t\tcolor:#124964;\n\t\t}\n\n\t}\n\n}\n\n/**\n * Toggle Switch ( replaces checkbox on admin panels )\n */\n.llms-switch {\n\tposition: relative;\n\n\t.llms-toggle {\n\t\tposition: absolute;\n\t\tmargin-left: -9999px;\n\t\tvisibility: hidden;\n\t}\n\n\t.llms-toggle + label {\n\t\tbox-sizing: border-box;\n\t\tdisplay: block;\n\t\tposition: relative;\n\t\tcursor: pointer;\n\t\toutline: none;\n\t\tuser-select: none;\n\t}\n\n\tinput.llms-toggle-round + label {\n\t\tborder: 2px solid #6c7781;\n\t\tborder-radius: 10px;\n\t\theight: 20px;\n\t\twidth: 36px;\n\t}\n\tinput.llms-toggle-round + label:before,\n\tinput.llms-toggle-round + label:after {\n\t\tbox-sizing: border-box;\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\ttransition: background 0.4s;\n\t}\n\n\tinput.llms-toggle-round:checked + label {\n\t\tborder-color: #11a0d2;\n\t \tbackground-color: #11a0d2;\n\t}\n\n\t// Primary dot (that moves.)\n\tinput.llms-toggle-round + label:after {\n\t\theight: 12px;\n\t\tleft: 2px;\n\t\ttop: 2px;\n\t\tbackground-color: #6c7781;\n\t\tborder-radius: 50%;\n\t\ttransition: margin 0.4s;\n\t\twidth: 12px;\n\t\tz-index: 3;\n\t}\n\n\t// Primary dot when toggle on.\n\tinput.llms-toggle-round:checked + label:after {\n\t\tbackground-color: $color-white;\n\t\tmargin-left: 16px;\n\t}\n\n\t// Secondary dot: empty on the right side of the toggle when toggled off.\n\tinput.llms-toggle-round + label:before {\n\t\theight: 8px;\n\t\ttop: 4px;\n\t\tborder: 1px solid #6c7781;\n\t\tborder-radius: 50%;\n\t\tright: 4px;\n\t\twidth: 8px;\n\t\tz-index: 2;\n\t}\n\n\tinput.llms-toggle-round:checked + label:before {\n\t\tborder-color: $color-white;\n\t\tborder-radius: 0;\n\t\tleft: 6px;\n\t\tright: auto;\n\t\twidth: 2px;\n\t}\n\n}\n\n#llms-profile-fields {\n\tmargin: 50px 0;\n\t.llms-form-field {\n\t\tpadding-left: 0;\n\t}\n\tlabel {\n\t\tdisplay: block;\n\t\tfont-weight: bold;\n\t\tpadding: 8px 0 2px;\n\t}\n\t.type-checkbox .type-checkbox {\n\t\tlabel {\n\t\t\tdisplay: initial;\n\t\t\tfont-weight: initial;\n\t\t\tpadding: 0;\n\t\t}\n\t\tinput {\n\t\t\tdisplay: inline-block;\n\t\t\tmargin-bottom: 0;\n\t\t\twidth: 1rem;\n\t\t}\n\t}\n\tselect {\n\t\tmax-width: 100%;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin/modules/_icons.scss",
    "content": "/******************************************************************\n\nSVG Styles\n\n******************************************************************/\n\nsvg {\n  &.icon {\n    height: 24px;\n    width: 24px;\n    display: inline-block;\n    fill: currentColor; // Inherit color\n    vertical-align: baseline; // Options: baseline, sub, super, text-top, text-bottom, middle, top, bottom\n\n    // Different styling for when an icon appears in a button element\n    button & {\n        height: 18px;\n        width: 18px;\n        margin: 4px -4px 0 4px;\n        filter: drop-shadow( 0 1px #eee );\n        float: right;\n    \n    }&.menu-icon {\n        height: 20px;\n        width: 20px;\n        display: inline-block;\n        fill: currentColor;\n        vertical-align: text-bottom;\n        margin-right: 10px;\n    \n    }&.tree-icon {\n        height: 13px;\n        width: 13px;\n        vertical-align: middle;\n    \n    }&.section-icon {\n        height: 16px;\n        width: 16px;\n        vertical-align: text-bottom;\n    \n    }&.button-icon {\n        height: 16px;\n        width: 16px;\n        vertical-align: text-bottom;\n    \n    }&.button-icon-attr {\n        height: 10px;\n        width: 10px;\n        vertical-align: middle;\n    \n    }&.list-icon {\n        height: 12px;\n        width: 12px;\n        vertical-align: middle;\n        \n        &.on {\n            color: $color-blue;\n\n        }&.off {\n            color: $color-cinder;\n        }\n    \n        }&.detail-icon {\n            height: 16px;\n            width: 16px;\n            vertical-align: text-bottom;\n            cursor:default;\n          \n            &.on {\n                color: $color-blue;\n\n            }&.off {\n                color: $color-lightgrey;\n            }\n        }\n\n    }\n\n    &.icon-ion {}\n\n    &.icon-ion-edit {}\n\n    // rotate for arrow tips\n    &.icon-ion-arrow-up {\n    transform: rotate(90deg);\n    }\n\n    use {\n    pointer-events: none;\n    }\n\n}"
  },
  {
    "path": "assets/scss/admin/modules/_llms-order-note.scss",
    "content": ".llms-order-note {\n\n\t.llms-order-note-content {\n\t\tbackground: #efefef;\n\t\tmargin-bottom: 10px;\n\t\tpadding: 10px;\n\t\tposition: relative;\n\t\t&:after {\n\t\t\tborder-style: solid;\n\t\t\tborder-color: #efefef transparent;\n\t\t\tborder-width: 10px 10px 0 0;\n\t\t\tbottom: -10px;\n\t\t\tcontent: '';\n\t\t\tdisplay: block;\n\t\t\theight: 0;\n\t\t\tleft: 20px;\n\t\t\tposition: absolute;\n\t\t\twidth: 0;\n\n\t\t}\n\t\tp {\n\t\t\tfont-size: 13px;\n\t\t\tmargin: 0;\n\t\t\tline-height: 1.5;\n\t\t}\n\t}\n\n\t.llms-order-note-meta {\n\t\tcolor: #999;\n\t\tfont-size: 11px;\n\t\tmargin-left: 10px;\n\t}\n\n\n}\n"
  },
  {
    "path": "assets/scss/admin/modules/_mb-tabs.scss",
    "content": "/******************************************************************\n\nMetabox Tabs\n\n******************************************************************/\n\n// free space up if the metabox is on the side\n#side-sortables .tab-content {\n\tpadding: 0;\n}\n\n.llms-mb-container .tab-content{\n    display: none;\n    background-color: #FFF;\n    padding: 0 20px;\n    \n    ul:not(.select2-selection__rendered)  {\n        margin: 0 0 15px 0;\n\n        > li {\n            padding: 20px 0;\n            margin: 0;\n\n            &.select:not([class*=\"d-\"]) {\n            \twidth: 100%;\n            }\n\n            &:last-child {\n                border: 0;\n                padding-bottom: 0;\n\n            }\n\n            &.top {\n                border-bottom: 0;\n                padding-bottom: 0;\n            }\n\n        }\n    }\n\n    .full-width { width: 100%; }\n\n    #wp-content-editor-tools {\n        background: none;\n    }\n\n}\n\n.llms-mb-container .tab-content.llms-active{\n    display: inherit;\n}\n\n\n.llms-mb-container .tab-content .no-border {\n    border-bottom: 0px;\n}\n"
  },
  {
    "path": "assets/scss/admin/modules/_merge-codes.scss",
    "content": ".button.llms-merge-code-button {\n\tvertical-align: middle;\n\tsvg {\n\t\tmargin-right: 2px;\n\t\tposition: relative;\n\t\ttop: 4px;\n\t\twidth: 16px;\n\t\tg {\n\t\t\tfill: currentColor;\n\t\t}\n\t}\n}\n\n.llms-mb-container {\n\t.llms-merge-code-wrapper {\n\t\tfloat: right;\n\t\ttop: -5px;\n\t}\n}\n\n.llms-merge-code-wrapper {\n\tdisplay: inline;\n\tposition: relative;\n}\n\n.llms-merge-codes {\n    background: #f7f7f7;\n\tborder: 1px solid #ccc;\n\tborder-radius: 3px;\n    box-shadow: 0 1px 0 #ccc;\n    color: #555;\n\tdisplay: none;\n\tleft: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\ttop: 30px;\n\twidth: 200px;\n\n\tul {\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t}\n\n\tli {\n\t\tcursor: pointer;\n\t\tmargin: 0;\n\t\tpadding: 4px 8px !important;\n\t\tborder-bottom: 1px solid #ccc;\n\t}\n\n\tli:hover {\n\t\tcolor: #23282d;\n\t\tbackground: #fefefe;\n\t}\n\n\t&.active {\n\t\tdisplay: block;\n\t\tz-index: 777;\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin/modules/_top-modal.scss",
    "content": "/******************************************************************\n\nStyles for topModal modal\n\n******************************************************************/\n\n/**\n * Base modal styles\n */\n.topModal {\n    display:none;\n    position:relative;\n    border:4px solid #808080;\n    background:#fff;\n    z-index:1000001;\n    padding:2px;\n    max-width:500px;\n    margin: 34px auto 0;\n    box-sizing: border-box;\n    box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n    background-color: #ffffff;\n    border-radius: 2px;\n    border: 1px solid #dddddd;\n\n}.topModalClose {\n    float:right;\n    cursor:pointer;\n    margin-right: 10px;\n    margin-top: 10px;\n\n}.topModalContainer {\n    display: none;\n    overflow: auto;\n    overflow-y: hidden;\n    position: fixed;\n    top: 0 !important;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    -webkit-overflow-scrolling: touch;\n    width: auto !important;\n    margin-left: 0 !important;\n    background-color: transparent !important;\n    z-index: 100002 !important;\n\n}.topModalBackground {\n    display:none;\n    background:#000;\n    position:fixed;\n    height:100%;\n    width:100%;\n    top:0 !important;\n    left:0;\n    margin-left: 0 !important;\n    z-index: 100002 !important;\n    box-sizing: border-box;\n    overflow: auto;\n    overflow-y: hidden;\n\n}body.modal-open {\n    overflow: hidden;\n\n}.llms-modal-header {\n    border-top-right-radius: 1px;\n    border-top-left-radius: 1px;\n    background: $color-blue;\n    color: #eeeeee;\n    padding: 10px 15px;\n    font-size: 18px;\n\n}#llms-icon-modal-close {\n    width:16px;\n    height: 16px;\n    fill: $color-white;\n\n}.llms-modal-content {\n    padding: 20px;\n\n    h3 {\n        margin-top: 0;\n    }\n\n}\n\n/**\n * Custom Modal Styles for LifterLMS\n */\n.llms-modal-form {\n\n    h1 {\n        margin-top: 0;\n    }\n\n    input[type=text] {\n        width: 100%;\n    }\n\n    textarea,\n    input[type=\"text\"],\n    input[type=\"password\"],\n    input[type=\"file\"],\n    input[type=\"datetime\"],\n    input[type=\"datetime-local\"],\n    input[type=\"date\"],\n    input[type=\"month\"],\n    input[type=\"time\"],\n    input[type=\"week\"],\n    input[type=\"number\"],\n    input[type=\"email\"],\n    input[type=\"url\"],\n    input[type=\"search\"],\n    input[type=\"tel\"],\n    input[type=\"color\"] {\n        padding: 0 .4em 0 .4em;\n        margin-bottom: 2em;\n        vertical-align: middle;\n        border-radius: 3px;\n        min-width: 50px;\n        max-width: 635px;\n        width: 100%;\n        min-height: 32px;\n        background-color: #fff;\n        border: 1px solid $color-lightgrey;\n        margin: 0 0 24px 0;\n        outline: none;\n        transition: border 0.3s ease-in-out 0s;\n\n        &:focus {\n            background: $color-white;\n            border: 1px solid $color-blue;\n\n        }\n    }\n\n    textarea {\n        padding: .4em !important;\n        height: 100px !important;\n        border-radius: 3px;\n        vertical-align: middle;\n        min-height: 32px;\n        outline: none;\n        box-sizing: border-box;\n\n        &:focus {\n            background: $color-white;\n            border: 1px solid $color-blue;\n\n        }\n\n    }\n\n    .chosen-container-single .chosen-single {\n        border-radius: 3px;\n        vertical-align: middle;\n        min-height: 32px;\n        border: 1px solid $color-lightgrey;\n        width: 100%;\n        background: $color-white !important;\n        outline: none;\n        box-sizing: border-box;\n        box-shadow: 0 0 0 #fff;\n        line-height: 32px;\n        margin: 0 0 24px 0;\n\n        &:focus {\n        background: $color-white;\n        border: 1px solid $color-blue;\n        }\n    }\n\n    .chosen-container-single .chosen-single div b {\n        margin-top: 4px;\n    }\n\n    .chosen-search input[type=text] {\n        border: 1px solid $color-lightgrey;\n\n        &:focus {\n            background-color: $color-white;\n            border: 1px solid $color-blue;\n        }\n\n    }\n\n    .chosen-container-single .chosen-drop {\n        margin-top: -28px;\n    }\n\n    .llms-button-primary, .llms-button-secondary {\n        padding: 10px 10px;\n        border-radius: 0;\n        transition: .5s;\n        box-shadow: 0 1px 1px #ccc;\n\n        &.full {\n            width: 100%;\n        }\n    }\n}\n\n.modal-open .select2-dropdown {\n  z-index: 1000005;\n}\n"
  },
  {
    "path": "assets/scss/admin/modules/_voucher.scss",
    "content": "a.llms-voucher-delete {\n\tbackground: $color-danger;\n\tcolor: $color-white;\n\tdisplay: block;\n\tpadding: 4px 2px;\n\ttext-decoration: none;\n\ttransition: ease .3s all;\n\n\t&:hover {\n\t\tbackground: #af3a26;\n\t}\n}\n\n\n\n.llms-voucher-codes-wrapper,\n.llms-voucher-redemption-wrapper {\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n\n    th, td {\n      border: none;\n    }\n\n    thead {\n      background-color: $color-blue;\n      color:#fff;\n      th {\n        padding: 10px 10px;\n      }\n    }\n\n    tr {\n      counter-increment: row-counter;\n      &:nth-child(even) {\n        background-color: #F1F1F1;\n      }\n\n      td {\n        padding: 5px;\n        &:first-child:before {\n          content: counter( row-counter );\n        }\n      }\n    }\n  }\n}\n\n.llms-voucher-codes-wrapper {\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n\n    th, td {\n      border: none;\n    }\n\n    thead {\n      background-color: $color-blue;\n      color:#fff;\n    }\n\n    tr {\n      &:nth-child(even) {\n        background-color: #F1F1F1;\n      }\n\n      td {\n\n        span {\n          display: inline-block;\n          min-width: 30px;\n        }\n      }\n    }\n  }\n\n  button {\n    cursor: pointer;\n  }\n\n  .llms-voucher-delete {\n    float: right;\n    margin-right: 15px;\n  }\n\n  .llms-voucher-uses {\n    width: 50px;\n  }\n\n  .llms-voucher-add-codes {\n    float: right;\n\n    input[type=\"text\"] {\n      width: 30px;\n    }\n  }\n}\n\n.llms-voucher-export-wrapper {\n\n  .llms-voucher-export-type {\n    width: 100%;\n\n    p {\n      margin: 0 0 0 15px;\n    }\n  }\n\n  .llms-voucher-email-wrapper {\n    width: 100%;\n    margin: 25px 0;\n\n    input[type=\"text\"] {\n      width: 100%;\n    }\n\n    p {\n      margin: 0;\n    }\n  }\n\n  > button {\n    float: right;\n  }\n}\n\n.postbox .inside {\n  overflow: auto;\n}\n"
  },
  {
    "path": "assets/scss/admin/modules/_widgets.scss",
    "content": ".llms-widget {\n\tbackground-color: #FFF;\n\tborder: 1px solid #dedede;\n\tborder-radius: 12px;\n\tbox-shadow: 0px 0px 1px rgba(48, 49, 51, 0.05), 0px 2px 4px rgba(48, 49, 51, 0.1);\n\tbox-sizing: border-box;\n\tmargin-bottom: 20px;\n\tpadding: 20px;\n\tposition: relative;\n\twidth: 100%;\n\n\t&.alt {\n\n\t\tborder: 1px solid $color-lightgrey;\n\t\tbackground-color: #efefef;\n\t\tmargin-bottom: 10px;\n\n\t\t.llms-label {\n\t\t\tcolor: #777;\n\t\t\tfont-size: 14px;\n\t\t\tmargin-bottom: 10px;\n\t\t\tpadding-bottom: 5px;\n\t\t}\n\n\t\th2 {\n\t\t\tcolor: #444;\n\t\t\tfont-weight: 300;\n\t\t}\n\n\t}\n\n\ta {\n\t\tborder-bottom: 1px dotted $color-brand-blue;\n\t\tdisplay: inline-block;\n\t\ttext-decoration: none;\n\n\t\t&:hover {\n\t\t\tborder-bottom: 1px dotted $color-brand-blue-dark;\n\t\t}\n\t}\n\n\t// Nested for specificity (matches h1).\n\t.llms-widget-content {\n\t\tmargin: .67em 0;\n\t\tcolor: $color-brand-blue;\n\t\tfont-size: 2.4em;\n\t\tfont-weight: 700;\n\t\tline-height: 1;\n\t\tword-break: break-all;\n\t}\n\n\th2 {\n\t\tfont-size: 1.8em;\n\t}\n\n\t.llms-label {\n\t\tbox-sizing: border-box;\n\t\tfont-size: 18px;\n\t\tfont-weight: 400;\n\t\tmargin: 0 0 10px 0;\n\t\ttext-align: center;\n\t}\n\n\t.llms-chart {\n\t\twidth: 100%;\n\t\tpadding: 10px;\n\t\tbox-sizing: border-box;\n\t}\n\n\tmark.yes {\n\t\tbackground-color: #7ad03a;\n\t}\n\n\t.llms-subtitle {\n\t\tmargin-bottom: 0;\n\t}\n\n\t.spinner {\n\t\tfloat: none;\n\t\tleft: 50%;\n\t\tmargin: -10px 0 0 -10px;\n\t\tposition: absolute;\n\t\ttop: 50%;\n\t\tz-index: 2;\n\t}\n\n\t&.is-loading {\n\n\t\t&:before {\n\t\t\tbackground: $color-white;\n\t\t\tbottom: 0;\n\t\t\tcontent: '';\n\t\t\tleft: 0;\n\t\t\topacity: 0.9;\n\t\t\tposition: absolute;\n\t\t\tright: 0;\n\t\t\ttop: 0;\n\t\t\tz-index: 1;\n\t\t}\n\n\t\t.spinner {\n\t\t\tvisibility: visible;\n\t\t}\n\n\t}\n\n\ttd[colspan=\"2\"] {\n\t\tpadding-left: 0;\n\t}\n\n\ttr.llms-disabled-field {\n\t\topacity: 0.5;\n\t\tpointer-events: none;\n\t}\n\n}\n\n.llms-widget-1-3,\n.llms-widget-1-4,\n.llms-widget-1-5 {\n\ttext-align: center;\n}\n\n\n.llms-widget {\n\t.llms-widget-info-toggle {\n\t\tcolor: #AAA;\n\t\tcursor: pointer;\n\t\tfont-size: 16px;\n\t\tposition: absolute;\n\t\tright: 20px;\n\t\ttop: 20px;\n\t}\n\n\t&.info-showing {\n\t\t.llms-widget-info {\n\t\t\tdisplay: block;\n\t\t}\n\t}\n}\n.llms-widget-info {\n\tbackground: $color-cinder;\n\tcolor: $color-white;\n\tbottom: -50px;\n\tdisplay: none;\n\tpadding: 15px;\n\tposition: absolute;\n\ttext-align: center;\n\tleft: 10px;\n\tright: 15px;\n\tz-index: 3;\n\t&:before {\n\t\tcontent: '';\n\t\tborder: 12px solid transparent;\n\t\tborder-bottom-color: $color-cinder;\n\t\tleft: 50%;\n\t\tmargin-left: -12px;\n\t\tposition: absolute;\n\t\ttop: -24px;\n\t}\n\n\tp {\n\t\tmargin: 0;\n\t}\n\n}\n\n.llms-widget-row {\n\t@include clearfix();\n}\n\n.llms-widget-row .no-padding {\n\tpadding: 0 !important;\n}\n"
  },
  {
    "path": "assets/scss/admin/partials/_grid.scss",
    "content": "/******************************************************************\n\nGrids for Breakpoints\n\n******************************************************************/\n\n// using a mixin since we can't use placeholder selectors\n@mixin grid-col {\n  float: left;\n  padding-right: 0.75em;\n  box-sizing: border-box;\n\n}\n\n// the last column\n.last-col {\n  float: right;\n  padding-right: 0 !important;\n}\n.last-col:after {\n    clear: both;\n}\n\n/*\nMobile Grid Styles\nThese are the widths for the mobile grid.\nThere are four types, but you can add or customize\nthem however you see fit.\n*/\n@media (max-width: 767px) {\n\n  .m-all {\n    @include grid-col;\n    width: 100%;\n    padding-right: 0;\n  }\n\n  .m-1of2 {\n    @include grid-col;\n    width: 50%;\n  }\n\n  .m-1of3 {\n    @include grid-col;\n    width: 33.33%;\n  }\n\n  .m-2of3 {\n    @include grid-col;\n    width: 66.66%;\n  }\n\n  .m-1of4 {\n    @include grid-col;\n    width: 25%;\n  }\n\n  .m-3of4 {\n    @include grid-col;\n    width: 75%;\n  }\n\n\t.m-right {\n\t\ttext-align: center;\n\t}\n\t.m-center {\n\t\ttext-align: center;\n\t}\n\t.m-left {\n\t\ttext-align: center;\n\t}\n\n  .d-right {\n    text-align: right;\n  }\n  .d-center {\n    text-align: center;\n  }\n  .d-left {\n    text-align: left;\n  }\n\n} // end mobile styles\n\n\n/* Portrait tablet to landscape */\n@media (min-width: 768px) and (max-width: 1029px) {\n\n  .t-all {\n    @include grid-col;\n    width: 100%;\n    padding-right: 0;\n  }\n\n  .t-1of2 {\n    @include grid-col;\n    width: 50%;\n  }\n\n  .t-1of3 {\n    @include grid-col;\n    width: 33.33%;\n  }\n\n  .t-2of3 {\n    @include grid-col;\n    width: 66.66%;\n  }\n\n  .t-1of4 {\n    @include grid-col;\n    width: 25%;\n  }\n\n  .t-3of4 {\n    @include grid-col;\n    width: 75%;\n  }\n\n  .t-1of5 {\n    @include grid-col;\n    width: 20%;\n  }\n\n  .t-2of5 {\n    @include grid-col;\n    width: 40%;\n  }\n\n  .t-3of5 {\n    @include grid-col;\n    width: 60%;\n  }\n\n  .t-4of5 {\n    @include grid-col;\n    width: 80%;\n  }\n\n\t.d-right {\n\t\ttext-align: right;\n\t}\n\t.d-center {\n\t\ttext-align: center;\n\t}\n\t.d-left {\n\t\ttext-align: left;\n\t}\n\n} // end tablet\n\n/* Landscape to small desktop */\n@media (min-width: 1030px) {\n\n  .d-all  {\n    @include grid-col;\n    width: 100%;\n    padding-right: 0;\n  }\n\n  .d-1of2 {\n    @include grid-col;\n    width: 50%;\n  }\n\n  .d-1of3 {\n    @include grid-col;\n    width: 33.33%;\n  }\n\n  .d-2of3 {\n    @include grid-col;\n    width: 66.66%;\n  }\n\n  .d-1of4 {\n    @include grid-col;\n    width: 25%;\n  }\n\n  .d-3of4 {\n    @include grid-col;\n    width: 75%;\n  }\n\n  .d-1of5 {\n    @include grid-col;\n    width: 20%;\n  }\n\n  .d-2of5 {\n    @include grid-col;\n    width: 40%;\n  }\n\n  .d-3of5 {\n    @include grid-col;\n    width: 60%;\n  }\n\n  .d-4of5 {\n    @include grid-col;\n    width: 80%;\n  }\n\n  .d-1of6 {\n    @include grid-col;\n    width: 16.6666666667%;\n  }\n\n  .d-1of7 {\n    @include grid-col;\n    width: 14.2857142857%;\n  }\n\n  .d-2of7 {\n    @include grid-col;\n    width: 28.5714286%;\n  }\n\n  .d-3of7 {\n    @include grid-col;\n    width: 42.8571429%;\n  }\n\n  .d-4of7 {\n    @include grid-col;\n    width: 57.1428572%;\n  }\n\n  .d-5of7 {\n    @include grid-col;\n    width: 71.4285715%;\n  }\n\n  .d-6of7 {\n    @include grid-col;\n    width: 85.7142857%;\n  }\n\n  .d-1of8 {\n    @include grid-col;\n    width: 12.5%;\n  }\n\n  .d-1of9 {\n    @include grid-col;\n    width: 11.1111111111%;\n  }\n\n  .d-1of10 {\n    @include grid-col;\n    width: 10%;\n  }\n\n  .d-1of11 {\n    @include grid-col;\n    width: 9.09090909091%;\n  }\n\n  .d-1of12 {\n    @include grid-col;\n    width: 8.33%;\n  }\n\n\t.d-right {\n\t\ttext-align: right;\n\t}\n\t.d-center {\n\t\ttext-align: center;\n\t}\n\t.d-left {\n\t\ttext-align: left;\n\t}\n\n} // end desktop styles\n"
  },
  {
    "path": "assets/scss/admin/post-tables/_llms_orders.scss",
    "content": ".wp-list-table {\n\t@include order_status_badges();\n}\n\n#lifterlms-order-transactions .llms-table tfoot th {\n\ttext-align: right;\n}\n"
  },
  {
    "path": "assets/scss/admin/post-tables/_post-tables.scss",
    "content": ".llms-post-table-post-filter {\n\tdisplay: inline-block;\n\tmargin-right: 6px;\n\tmax-width: 100%;\n\twidth: 220px;\n}\n"
  },
  {
    "path": "assets/scss/admin-importer.scss",
    "content": "@import \"_includes/vars\";\n@import \"_includes/mixins\";\n\n.llms-import-file-wrap {\n    background: #fafafa;\n    border: 1px solid #ccd0d4;\n    padding: 10px;\n    margin: 20px auto;\n    display: inline-flex;\n    justify-content: space-between;\n    align-items: center;\n}\n\n.llms-cloud-import-help.button-link {\n\tcolor: inherit;\n\tvertical-align: top;\n\ttext-decoration: none;\n}\n\n.wrap.llms-import-export {\n\n\tul.llms-importable-courses {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tflex-wrap: wrap;\n\t\tgap: 20px;\n\n\t\t@media only screen and ( min-width: 680px ) {\n\t\t\tflex-direction: row;\n\t\t}\n\n\t\t.llms-importable-course {\n\t\t\tbackground-color: #FFF;\n\t\t\tborder: 1px solid #dedede;\n\t\t\tborder-radius: 12px;\n\t\t\tbox-shadow: 0px 0px 1px rgba(48, 49, 51, 0.05), 0px 2px 4px rgba(48, 49, 51, 0.1);\n\t\t\tlist-style: none;\n\t\t\tmargin: 20px 0;\n\t\t\toverflow: hidden;\n\n\t\t\t@media only screen and ( min-width: 680px ) {\n\t\t\t\t-webkit-box-flex: 0;\n\t\t\t\tflex-grow: 0;\n\t\t\t\tflex-shrink: 1;\n\t\t\t\tflex-basis: calc(31%);\n\t\t\t}\n\n\t\t\timg {\n\t\t\t\tdisplay: block;\n\t\t\t\tmax-width: 100%;\n\t\t\t}\n\n\t\t\th3 {\n\t\t\t\tcolor: #1d2327;\n\t\t\t\tfont-size: 20px;\n\t\t\t\tline-height: 1.5;\n\t\t\t\tmargin: 30px 20px 15px;\n\t\t\t}\n\n\t\t\tp {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tline-height: 1.5;\n\t\t\t\tmargin: 0 20px 15px;\n\t\t\t}\n\n\t\t\t&.has-action-button {\n\t\t\t\tpadding-bottom: 10px;\n\t\t\t\tposition: relative;\n\t\t\t\t.button {\n\t\t\t\t\tbottom: 20px;\n\t\t\t\t\tright: 20px;\n\t\t\t\t\tposition: absolute;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t@media only screen and (min-width: 600px) {\n\t\tul.llms-importable-courses {\n\t\t\tdisplay: flex;\n\t\t\tflex-wrap: wrap;\n\t\t}\n\t}\n\n\t@media only screen and (min-width: 600px) and (max-width: 767px) {\n\t\tul.llms-importable-courses {\n\t\t\tli.llms-importable-course {\n\t\t\t\tflex: 1 0 calc( 50% - 20px );\n\t\t\t\tmax-width: calc( 50% - 20px );\n\t\t\t}\n\t\t}\n\t}\n\n\t@media only screen and (min-width: 768px) {\n\t\tul.llms-importable-courses {\n\t\t\tli.llms-importable-course {\n\t\t\t\tflex: 1 0 calc( 33% - 20px );\n\t\t\t\tmax-width: calc( 33% - 20px );\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/admin-wizard.scss",
    "content": "@import \"_includes/vars\";\n\n$input_padding: 0.3em 0.6em;\n\n#wpadminbar, #adminmenumain, #wpfooter {\n\tdisplay: none;\n}\n\n#llms-setup-wizard {\n\n\tbackground-color: #F0F0F1;\n\theight: 100%;\n\tleft: 0;\n\toverflow: scroll;\n\tposition: fixed;\n\ttop: 0;\n\twidth: 100%;\n\n}\n\n.llms-setup-wrapper {\n\tmargin: 30px auto;\n\tmax-width: 640px;\n}\n\n#llms-logo {\n\ttext-align: center;\n\n\ta {\n\t\tdisplay: inline-block;\n\t}\n\n\timg {\n\t\tmax-width: 200px;\n\t}\n\n}\n\n.llms-setup-content {\n\tbackground-color: #FFF;\n\tbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.13);\n\tpadding: 30px;\n\n\th1, h2, h3, h4, h5, h6 {\n\t\tcolor: #3C434A;\n\t}\n\n\ta:not( .llms-button-primary ):not( .llms-button-secondary ) {\n\t\tcolor: $color-brand-blue;\n\t}\n\n\tp, li {\n\t\tcolor: #3C434A;\n\t\tfont-size: 16px;\n\t}\n\n\tp.error {\n\t\tcolor: $color-red;\n\t\tbackground: rgba($color-red,0.1);\n\t\tborder: 1px solid currentColor;\n\t\tpadding: 1em;\n\t\tborder-radius: 4px;\n\t\tmargin: 1.5em 0 0;\n\t\tfont-size: 15px;\n\t\ttext-align: left;\n\t}\n\n\tlabel {\n\t\tfont-weight: 500;\n\t}\n\n\ttable {\n\t\tborder-bottom: 1px solid #f1f1f1;\n\t\tborder-collapse: collapse;\n\t\twidth: 100%;\n\t}\n\n\ttd {\n\t\tborder-top: 1px solid #f1f1f1;\n\n\t\t&:first-child {\n\t\t\tpadding-right: 10px;\n\t\t\twidth: 33%;\n\n\t\t\ta {\n\t\t\t\tfont-size: 16px;\n\t\t\t\tfont-weight: 500;\n\t\t\t}\n\n\t\t\ti {\n\t\t\t\tdisplay: block;\n\t\t\t\tfont-size: 13px;\n\t\t\t\tmargin-top: 10px;\n\n\t\t\t\ta {\n\t\t\t\t\tfont-size: 13px;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tsmall {\n\t\tfont-size: small;\n\t}\n\n\t.has-checkbox-field {\n\t\tdisplay: flex;\n\t\tflex-direction: row-reverse;\n\t\tjustify-content: flex-end;\n\t\talign-items: center;\n\t\talign-content: center;\n\t\tgap: 0.5em;\n\t\tfont-weight: normal;\n\t\tfont-size: 16px;\n\n\t\tinput {\n\t\t\tmargin: 0;\n\t\t}\n\t}\n\n\tlabel {\n\t\tfont-size: 16px;\n\t\tdisplay: block;\n\t\tmargin-bottom: 0.5em;\n\t}\n\n\tsmall + label {\n\t\tmargin-top: 1em;\n\t}\n\n\t[type=text] {\n\t\twidth: 100%;\n\t\tclear: both;\n\t\tmargin: 0;\n\t\tpadding: $input_padding;\n\t}\n\n\t.has-date-field {\n\t\tfont-size: 16px;\n\t\tdisplay: flex;\n\t\tmargin-bottom: 0.5em;\n\t\tflex-wrap: wrap;\n\t\tflex-direction: column;\n\t\tgap: 0.5em;\n\t}\n\n\t[type=date] {\n\t\tpadding: $input_padding;\n\t}\n\n\t.is-hidden {\n\t\tdisplay: none;\n\t}\n}\n\n.llms-setup-date-fields {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 1em;\n\tpadding-top: 2em;\n}\n\n.llms-setup-date-field {\n\tflex: 1;\n}\n\n.llms-setup-actions {\n\tmargin-top: 2em;\n\ttext-align: right;\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 1em;\n\n\t.llms-button-primary {\n\t\tdisplay: inline-block;\n\n\t\t&:after {\n\t\t\tcontent: \"\\f054\";\n\t\t\tfont-family: 'FontAwesome';\n\t\t\tfont-weight: 900;\n\t\t\tpadding-left: 10px;\n\t\t}\n\n\t}\n}\n\n.llms-exit-setup {\n\tcolor: inherit !important;\n\tmargin-right: 10px;\n\n\t+ .llms-button-primary,\n\t+ .llms-button-secondary {\n\t\tmargin-left: auto;\n\t}\n}\n\n.llms-importing-msgs {\n\ta {\n\t\tcolor: $color-brand-blue;\n\t}\n\n\t.llms-importing-msg {\n\t\tdisplay: none;\n\t\tfont-size: 14px;\n\t\tfont-style: italic;\n\t\ttext-align: right;\n\t}\n}\n\nul.llms-importable-courses {\n\tborder-bottom: 1px solid #f1f1f1;\n\tdisplay: block;\n\n\tli.llms-importable-course {\n\t\tborder-top: 1px solid #f1f1f1;\n\t\tdisplay: block;\n\t\tmargin: 0;\n\t\tmax-width: 100%;\n\t\tpadding: 20px 0;\n\n\t\th3 {\n\t\t\tgrid-area: title;\n\t\t\tline-height: 1.5;\n\t\t\tmargin: 0;\n\t\t}\n\n\t\tp {\n\t\t\tgrid-area: description;\n\t\t\tmargin: 0;\n\t\t}\n\n\t\tlabel {\n\t\t\tcolumn-gap: 20px;\n\t\t\tdisplay: grid;\n\t\t\tfont-weight: 400;\n\t\t\tgrid-template-areas:\n\t\t\t\t\"image title switch\"\n\t\t\t\t\"image description switch\";\n\t\t\t;\n\t\t\tgrid-template-columns: 1fr 3fr auto;\n\t\t\trow-gap: 5px;\n\t\t}\n\n\t\timg {\n\t\t\tgrid-area: image;\n\t\t\tmax-width: 160px;\n\t\t}\n\n\t\t.llms-switch {\n\t\t\tgrid-area: switch;\n\n\t\t\tinput.llms-toggle-round:checked + label {\n\t\t\t\tborder-color: $color-brand-blue;\n\t\t\t\tbackground-color: $color-brand-blue;\n\t\t\t}\n\n\t\t}\n\n\t}\n}\n\n.llms-setup-progress {\n\tdisplay: flex;\n\tmargin: 20px 0;\n\n\tli {\n\t\tborder-bottom: 4px solid $color-brand-blue;\n\t\tdisplay: inline-block;\n\t\tfont-size: 14px;\n\t\tpadding-bottom: 10px;\n\t\tposition: relative;\n\t\ttext-align: center;\n\t\tflex: 1;\n\n\t\ta {\n\t\t\tcolor: $color-brand-blue;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\t&:after {\n\t\t\tbackground: $color-brand-blue;\n\t\t\tbottom: 0;\n\t\t\tcontent: '';\n\t\t\tborder: 4px solid $color-brand-blue;\n\t\t\tborder-radius: 100%;\n\t\t\theight: 4px;\n\t\t\tposition: absolute;\n\t\t\tleft: 50%;\n\t\t\tmargin-left: -6px;\n\t\t\tmargin-bottom: -8px;\n\t\t\twidth: 4px;\n\t\t}\n\n\t\t&.current {\n\t\t\tfont-weight: 700;\n\n\t\t\t&:after {\n\t\t\t\tbackground: #fff;\n\t\t\t}\n\t\t}\n\n\t\t&.current ~ li {\n\t\t\tborder-bottom-color: #ccc;\n\n\t\t\t&:after {\n\t\t\t\tbackground: #ccc;\n\t\t\t\tborder-color: #ccc;\n\t\t\t}\n\n\t\t\ta {\n\t\t\t\tcolor: #bbb;\n\t\t\t}\n\t\t}\n\n\t}\n}\n\n.llms-setup-wrapper {\n\n\t// Calendar icon for custom date fields.\n\t[type=\"text\"].llms-datepicker {\n\t\tbackground-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512' fill='%23ccc'%3E%3Cpath d='M152 24c0-13.3-10.7-24-24-24s-24 10.7-24 24v40H64C28.7 64 0 92.7 0 128v320c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64h-40V24c0-13.3-10.7-24-24-24s-24 10.7-24 24v40H152V24zM48 192h352v256c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V192z'/%3E%3C/svg%3E\");\n\t\tbackground-repeat: no-repeat;\n\t\tbackground-position: right 8px center;\n\t\tbackground-size: 16px 16px;\n\t\tpadding-right: 40px;\n\t}\n}\n\n@media only screen and ( max-width: 782px ) {\n\tul.llms-importable-courses {\n\t\tli.llms-importable-course {\n\t\t\tlabel {\n\t\t\t\tgrid-template-areas:\n\t\t\t\t\t\"image switch\"\n\t\t\t\t\t\"title title\"\n\t\t\t\t\t\"description description\";\n\t\t\t\tgrid-template-columns: 3fr auto;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/scss/admin.scss",
    "content": "//\n// Main Admin CSS File\n//\n\n@import \"_includes/vars\";\n@import \"_includes/vars-brand-colors\";\n\n@import \"_includes/extends\";\n@import \"_includes/buttons\";\n@import \"_includes/mixins\";\n\n@import \"_includes/tooltip\";\n\n// wp menu item\n@import \"admin/_wp-menu\";\n\n// grid layout for breakpoints\n@import \"admin/partials/grid\";\n\n// forms\n@import \"admin/modules/forms\";\n\n// voucher\n@import \"admin/modules/voucher\";\n\n// widgets\n@import \"admin/modules/widgets\";\n\n// icons\n@import \"admin/modules/icons\";\n\n// icons\n@import \"admin/modules/mb-tabs\";\n\n// icons\n@import \"admin/modules/top-modal\";\n\n@import \"admin/modules/merge-codes\";\n\n// Base (mobile)\n@import \"admin/breakpoints/base\";\n\n// Larger mobile devices\n@media only screen and (min-width: 481px) {\n\t@import \"admin/breakpoints/481up\";\n}\n\n// Tablets and smaller laptops\n@media only screen and (min-width: 768px) {\n\t@import \"admin/breakpoints/768up\";\n}\n\n// Desktops\n@media only screen and (min-width: 1030px) {\n\t@import \"admin/breakpoints/1030up\";\n}\n\n// Larger Monitors and TVs\n@media only screen and (min-width: 1240px) {\n\t@import \"admin/breakpoints/1240up\";\n}\n\n@import \"admin/main\";\n\n@import \"admin/llms-table\";\n@import \"admin/modules/llms-order-note\";\n\n// metabox related\n@import \"admin/metaboxes/llms-metabox\";\n@import \"admin/metaboxes/metabox-instructors\";\n@import \"admin/metaboxes/metabox-orders\";\n@import \"admin/metaboxes/metabox-engagements-type\";\n@import \"admin/metaboxes/metabox-product\";\n@import \"admin/metaboxes/metabox-students\";\n@import \"admin/metaboxes/metabox-field-repeater\";\n@import \"admin/metaboxes/builder-launcher\";\n@import \"admin/media-protection\";\n\n@import \"admin/post-tables/llms_orders\";\n@import \"admin/post-tables/post-tables\";\n\n@import \"admin/tabs\";\n@import \"admin/fonts\";\n@import \"admin/reporting\";\n\n@import \"admin/settings\";\n\n@import \"admin/dashboard\";\n@import \"admin/dashboard-widget\";\n@import \"admin/resources\";\n\n@import \"admin/quiz-attempt-review\";\n\n@import \"_includes/llms-form-field\";\n@import \"_includes/vendor/_font-awesome\";\n"
  },
  {
    "path": "assets/scss/builder.scss",
    "content": "@import \"_includes/vars\";\n@import \"_includes/vars-brand-colors\";\n@import \"_includes/extends\";\n@import \"_includes/mixins\";\n\n@import \"admin/course-builder\";\n"
  },
  {
    "path": "assets/scss/certificates.scss",
    "content": "/**\n * Reset.\n */\nbody {\n\tbackground-color: #fff;\n\tbackground-image: none;\n\tmargin: 0 auto;\n}\n\n.header, .footer,\n.wrap-header, .wrap-footer,\n.site-header, .site-footer,\n.nav-primary, .primary-nav {\n\tdisplay: none;\n}\n\n.llms-certificate-container {\n\tmargin: 40px auto 0;\n}\n\n/**\n * Legacy Template.\n */\n.llms-certificate-container:not(.cert-template-v2) {\n\n\tpadding: 0;\n\toverflow: hidden;\n\n\t.certificate-background {\n\t\tposition: relative;\n\t\tz-index: 1;\n\t\twidth: 100%;\n\t\tdisplay: block;\n\t}\n\n\t.llms_certificate,\n\t.llms_my_certificate {\n\t\tmargin: 80px;\n\t\tposition: relative;\n\t\tz-index: 2;\n\t}\n\n\th1:first-child {\n\t\ttext-align: center;\n\t}\n\n}\n\n/**\n * V2 Template\n */\n.llms-certificate-wrapper {\n\tmargin: 0 auto;\n}\n.llms-certificate-container.cert-template-v2 {\n\twidth: 100%;\n\theight: 100%;\n\tbackground-size: 100% 100% !important;\n\tbox-sizing: border-box; \n\n\t.wp-block-columns .wp-block-column > * {\n\t\tmargin-top: 0 !important;\n\t\tmargin-bottom: 0 !important;\n\t}\n}\n\n/**\n * Certificate Actions Footer.\n */\n.llms-print-certificate {\n\tmargin-top: 40px;\n\tmargin-bottom: 40px;\n\ttext-align: center;\n\n\tform {\n\t\tdisplay: inline;\n\t}\n}\n\n\n\n@media print {\n\n\thtml, body {\n\t\tprint-color-adjust: exact !important;\n\t\theight: 100%;\n\t\toverflow: hidden;\n\t}\n\n\t@page { size: auto; }\n\n\t.no-print {\n\t\tdisplay: none;\n\t}\n\n\t// Make everything on the page invisible.\n\tbody * {\n\t\tvisibility: hidden !important;\n\t\tbackground: #fff none;\n\t}\n\n\t.site, .site-content {\n\t\toverflow: visible;\n\t}\n\n\t// Remove all headers, menus and footers.\n\theader, aside, nav, footer {\n\t\tdisplay: none !important;\n\t}\n\n\t// Make sure a .container parent doesn't shift the certificate see: https://github.com/gocodebox/lifterlms/issues/1163.\n\t.single-llms_my_certificate .container,\n\t.single-llms_certificate .container {\n\t\twidth: 100%;\n\t}\n\n\t// Make only the certificate container and its children visible.\n\t.llms-certificate-container,\n\t.llms-certificate-container * {\n\t\tvisibility: visible !important;\n\t\tbackground: transparent none;\n\t}\n\n \t// Position certificate absolutely and center horizontally.\n\t.llms-certificate-container:not(.cert-template-v2) {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tmargin: 0 auto;\n\t\tbackground: #fff none;\n\t}\n\n\t.llms-certificate-container.cert-template-v2 {\n\t\tmargin: 0 auto !important;\n\t\tprint-color-adjust: exact !important;\n\t\ttransform: scale( 0.95 ); // Don't ram the edge of the paper.\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/editor.scss",
    "content": ".llms-block-empty,\n.llms-block-error {\n\tpadding: 1em;\n\tborder: 1px solid #e0e0e0;\n\tfont-size: 16px;\n}\n\n.llms-block-empty {\n\tfont-style: italic;\n}\n\n.wp-block .llms-button-primary {\n\ttext-decoration: none;\n}\n\n.llms-navigation-link-settings .components-panel__row > .components-base-control {\n\twidth: 100%;\n}\n\n.llms-block-icon {\n\ttransform: scale(0.75);\n}\n\n.llms-loop-item {\n\t.llms-video-wrapper {\n\t\taspect-ratio: 16/9;\n\t\tposition: relative;\n\t\tiframe {\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tposition: absolute;\n\t\t\tleft: 0;\n\t\t\ttop: 0;\n\t\t\tobject-fit: cover;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_checkout.scss",
    "content": ".llms-checkout-wrapper {\n\t.llms-person-login-form-wrapper {\n\t\tmargin-bottom: 40px;\n\t}\t\n\tform.llms-login {\n\t\tdisplay: none;\n\t}\n\t.llms-form-heading {\n\t\tmargin: 0 0 20px;\n\t}\n}\n\n.llms-checkout {\n\tposition: relative;\n}\n\n.llms-checkout-cols-2 {\n\t@extend %clearfix;\n\n\t&.llms-col-2 {\n\n\t\tsection {\n\t\t\tbackground-color: $color-white;\n\t\t\tborder: 1px solid $color-lightgrey;\n\t\t\tborder-radius: $radius-medium;\n\t\t\tpadding: 20px;\n\n\t\t\t.llms-form-heading {\n\t\t\t\tpadding: 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t@media all and (min-width: 800px) {\n\n\t\t.llms-checkout-col {\n\t\t\tfloat: left;\n\n\t\t\t&.llms-col-1 {\n\t\t\t\tmargin-right: 20px;\n\t\t\t\twidth: calc( 58% - 20px );\n\t\t\t}\n\t\t\t&.llms-col-2 {\n\t\t\t\tmargin-left: 20px;\n\t\t\t\twidth: calc( 42% - 20px );\n\n\t\t\t\tbutton {\n\t\t\t\t\twidth: 100%;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n\n\t.llms-checkout-section {\n\t\tmargin-bottom: 40px;\n\t\tposition: relative;\n\t}\n\n\t\t.llms-checkout-section-content {\n\t\t\t&.llms-form-fields {\n\t\t\t\tmargin: 0px;\n\t\t\t}\n\n\t\t\t.llms-label {\n\t\t\t\tfont-weight: 400;\n\t\t\t}\n\n\t\t\t.llms-order-summary {\n\t\t\t\tlist-style-type: none;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 0;\n\n\t\t\t\tli {\n\t\t\t\t\tlist-style-type: none;\n\t\t\t\t\tmargin: 0 0 15px 0;\n\n\t\t\t\t}\n\n\t\t\t\t:not(.llms-label) {\n\t\t\t\t\tfont-weight: 700;\n\t\t\t\t}\n\n\t\t\t\tli.llms-pricing {\n\t\t\t\t\t&.on-sale,\n\t\t\t\t\t&.has-coupon {\n\t\t\t\t\t\t.price-regular { text-decoration: line-through; }\n\t\t\t\t\t}\n\t\t\t\t}\n\n\n\t\t\t}\n\n\t\t\t.llms-coupon-wrapper {\n\t\t\t\tborder-top: 1px solid $color-border;\n\t\t\t\tmargin-top: 20px;\n\t\t\t\tpadding-top: 20px;\n\n\t\t\t\t.llms-coupon-entry {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t\tmargin-top: 20px;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t.llms-form-field.llms-payment-gateway-option {\n\t\t\talign-items: center;\n\t\t\tdisplay: flex;\n\t\t\tgap: 8px;\n\n\t\t\tlabel + span.llms-description {\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tmargin-left: 5px;\n\t\t\t}\n\n\t\t\t.llms-description {\n\t\t\t\ta {\n\t\t\t\t\tborder: none;\n\t\t\t\t\tbox-shadow: none;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t}\n\t\t\t\timg {\n\t\t\t\t\tdisplay: inline;\n\t\t\t\t\tmax-height: 22px;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t.llms-checkout-wrapper ul.llms-payment-gateways {\n\t\t\tmargin: 5px 0 0;\n\t\t\tpadding: 0;\n\t\t}\n\t\tul.llms-payment-gateways {\n\t\t\tlist-style-type: none;\n\n\t\t\tli:last-child:after {\n\t\t\t\tborder-bottom: 1px solid $color-border;\n\t\t\t\tcontent: '';\n\t\t\t\tdisplay: block;\n\t\t\t\tmargin: 20px 0;\n\t\t\t}\n\n\t\t\t.llms-payment-gateway {\n\t\t\t\tmargin-bottom: 30px;\n\t\t\t\tlist-style-type: none;\n\t\t\t\t&:last-child {\n\t\t\t\t\tmargin-bottom: none;\n\t\t\t\t}\n\n\t\t\t\t&.is-selected {\n\t\t\t\t\t.llms-payment-gateway-option label {\n\t\t\t\t\t\tfont-weight: 700;\n\t\t\t\t\t}\n\t\t\t\t\t.llms-gateway-fields {\n\t\t\t\t\t\tdisplay: block;\n\n\t\t\t\t\t\t.llms-notice {\n\t\t\t\t\t\t\tmargin-left: 10px;\n\t\t\t\t\t\t\tmargin-right: 10px;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.llms-form-field {\n\t\t\t\t\tpadding-bottom: 0;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t\t.llms-gateway-description {\n\t\t\t\t\tmargin-left: 40px;\n\t\t\t\t}\n\n\t\t\t\t.llms-gateway-fields {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t\tmargin: 5px 0 20px;\n\t\t\t\t}\n\n\t\t\t.llms-payment-gateway-error {\n\t\t\t\tpadding: 0 10px;\n\t\t\t}\n\t\t}\n\n\t\t.llms-checkout-confirm {\n\t\t\tmargin: 0;\n\t\t}\n\n\t\t.llms-payment-method {\n\t\t\tmargin: 10px 10px 0;\n\t\t}\n\n\t\t.llms-gateway-description {\n\t\t\tp {\n\t\t\t\tfont-size: 14px;\n\t\t\t\tfont-style: italic;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\t\t}\n"
  },
  {
    "path": "assets/scss/frontend/_course.scss",
    "content": ".llms-meta-info {\n\tmargin: 40px 0;\n\t.llms-meta-title {\n\t\tmargin: 0;\n\t}\n\t.llms-meta {\n\t\tp {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\t\tspan {\n\t\t\tfont-weight: 700;\n\t\t}\n\t}\n}\n.llms-course-progress {\n\tmargin: 40px auto;\n\tmax-width: 480px;\n\ttext-align: center;\n}\n"
  },
  {
    "path": "assets/scss/frontend/_focus-mode.scss",
    "content": "//\n// Focus Mode\n//\n\n$focus-header-height: 60px;\n$focus-sidebar-width: 350px;\n$focus-sidebar-collapsed-width: 48px;\n$admin-bar-height: 32px;\n$admin-bar-height-mobile: 46px;\n\n.llms-focus-mode {\n\tmargin: 0;\n\tpadding: 0;\n\tbackground-color: #f9f9f9;\n\toverflow: hidden;\n\n\t.llms-focus-mode-wrapper {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\theight: 100dvh;\n\t\toverflow: hidden;\n\t}\n\n\t// -- Header: fixed to top, full width --\n\t.llms-focus-mode-header {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\talign-items: center;\n\t\tgap: 20px;\n\t\tbackground-color: #fff;\n\t\tborder-bottom: 1px solid #e5e5e5;\n\t\tpadding: 0 20px;\n\t\theight: $focus-header-height;\n\t\tmin-height: $focus-header-height;\n\t\tbox-sizing: border-box;\n\t\tflex-shrink: 0;\n\t\tz-index: 20;\n\n\t\t.llms-focus-mode-header-left {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tflex-shrink: 0;\n\t\t\twhite-space: nowrap;\n\n\t\t\t> .clear {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\t\t}\n\n\t\t.llms-focus-mode-header-center {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tflex: 1;\n\t\t\tmin-width: 0;\n\t\t}\n\n\t\t.llms-progress {\n\t\t\twidth: 100%;\n\t\t\tmax-width: 300px;\n\n\t\t\t.llms-progress-bar {\n\t\t\t\twidth: 100%;\n\t\t\t\tmin-width: 20px;\n\t\t\t}\n\t\t}\n\n\t\t.llms-focus-mode-header-nav {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 8px;\n\t\t\tflex-shrink: 0;\n\t\t}\n\t}\n\n\t.llms-focus-mode-nav-btn {\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tgap: 6px;\n\t\tpadding: 6px 14px;\n\t\tborder: 1px solid #e5e5e5;\n\t\tborder-radius: 6px;\n\t\tbackground: #fafafa;\n\t\tcolor: #1d2327;\n\t\ttext-decoration: none;\n\t\tfont-size: 0.8125rem;\n\t\tfont-weight: 500;\n\t\tline-height: 1;\n\t\twhite-space: nowrap;\n\t\ttransition: background-color 0.15s ease, border-color 0.15s ease;\n\n\t\t&:hover {\n\t\t\tbackground: #f0f0f0;\n\t\t\tborder-color: #ccc;\n\t\t\tcolor: #1d2327;\n\t\t}\n\n\t\t.llms-focus-mode-nav-icon {\n\t\t\twidth: 12px;\n\t\t\theight: 12px;\n\t\t\tfill: currentColor;\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\t.llms-focus-mode-nav-label {\n\t\t\t@media (max-width: 768px) {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\t\t}\n\n\t\t@media (max-width: 768px) {\n\t\t\tpadding: 6px 8px;\n\t\t}\n\n\t\t&.llms-lesson-locked {\n\t\t\tcursor: not-allowed;\n\t\t\tposition: relative;\n\t\t\tcolor: #a7aaad;\n\t\t\tborder-color: #e5e5e5;\n\t\t\tbackground: #f9f9f9;\n\n\t\t\t.llms-tooltip {\n\t\t\t\ttop: auto;\n\t\t\t\tbottom: auto;\n\t\t\t\tleft: auto;\n\t\t\t\tright: 0;\n\t\t\t\ttransform: none;\n\t\t\t\twhite-space: normal;\n\t\t\t\tmin-width: 200px;\n\t\t\t\tmax-width: 200px;\n\n\t\t\t\t&.show {\n\t\t\t\t\ttop: auto;\n\t\t\t\t\tbottom: -112px;\n\t\t\t\t}\n\n\t\t\t\t&:after {\n\t\t\t\t\tbottom: auto;\n\t\t\t\t\ttop: -8px;\n\t\t\t\t\tleft: auto;\n\t\t\t\t\tright: 12px;\n\t\t\t\t\ttransform: none;\n\t\t\t\t\tborder-top: 0;\n\t\t\t\t\tborder-bottom: 8px solid #2a2a2a;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-focus-mode-back-link {\n\t\ttext-decoration: none;\n\t\tcolor: #0073aa;\n\t\tfont-weight: 500;\n\n\t\t&:hover {\n\t\t\tcolor: #005177;\n\t\t}\n\t}\n\n\t// -- Body: sidebar + content side by side below header --\n\t.llms-focus-mode-body {\n\t\tdisplay: flex;\n\t\tflex: 1;\n\t\toverflow: hidden;\n\t\theight: calc(100dvh - #{$focus-header-height});\n\t}\n\n\t// -- Sidebar: fixed width, split into fixed header + scrollable list --\n\t.llms-focus-mode-sidebar {\n\t\tposition: relative;\n\t\twidth: $focus-sidebar-width;\n\t\tmin-width: $focus-sidebar-width;\n\t\tflex-shrink: 0;\n\t\tbackground-color: #fff;\n\t\tborder-right: 1px solid #e5e5e5;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\toverflow: visible;\n\n\t\t.llms-focus-mode-sidebar-header {\n\t\t\tpadding: 15px 20px;\n\t\t\tmargin: 0;\n\t\t\tfont-size: 1rem;\n\t\t\tfont-weight: 600;\n\t\t\tcolor: #333;\n\t\t\tborder-bottom: 1px solid #e5e5e5;\n\t\t\tflex-shrink: 0;\n\n\t\t\th3 {\n\t\t\t\tmargin: 0;\n\t\t\t\tfont-size: inherit;\n\t\t\t\tfont-weight: inherit;\n\t\t\t\tcolor: inherit;\n\t\t\t}\n\t\t}\n\n\t\t.llms-focus-mode-sidebar-content {\n\t\t\tflex: 1;\n\t\t\toverflow-y: auto;\n\t\t\tpadding: 20px;\n\n\t\t\t.llms-widget-syllabus:not(.llms-widget-syllabus--collapsible) {\n\n\t\t\t\t.section-title {\n\t\t\t\t\tfont-weight: normal;\n\t\t\t\t\tmargin: 20px 0 10px;\n\t\t\t\t}\n\n\t\t\t\t.llms-section:first-child {\n\t\t\t\t\t.section-title {\n\t\t\t\t\t\tmargin-top: 10px;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t.llms-lesson-locked {\n\t\t\t\tcursor: not-allowed;\n\t\t\t}\n\t\t}\n\t}\n\n\t// -- Sidebar toggle button (expanded state: floating pill on sidebar edge) --\n\t.llms-focus-mode-sidebar-toggle {\n\t\tposition: absolute;\n\t\ttop: 20px;\n\t\tright: -14px;\n\t\tz-index: 11;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 28px;\n\t\theight: 28px;\n\t\tpadding: 0;\n\t\tborder: 1px solid #e5e5e5;\n\t\tborder-radius: 8px;\n\t\tbackground: #fff;\n\t\tbox-shadow: 2px 0 4px rgba(0, 0, 0, 0.08);\n\t\tcursor: pointer;\n\t\tcolor: #666;\n\t\tline-height: 1;\n\t\ttransition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;\n\n\t\t&:hover {\n\t\t\tcolor: #1d2327;\n\t\t\tbackground: #f0f0f0;\n\t\t\tbox-shadow: 2px 0 6px rgba(0, 0, 0, 0.12);\n\t\t}\n\n\t\tsvg {\n\t\t\twidth: 14px;\n\t\t\theight: 14px;\n\t\t\tfill: currentColor;\n\t\t}\n\n\t\t.llms-chevron-left {\n\t\t\tdisplay: block;\n\t\t}\n\n\t\t.llms-chevron-right {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t// -- Main wrapper: content + footer in a column that fills remaining space --\n\t.llms-focus-mode-main {\n\t\tflex: 1;\n\t\tmin-width: 0;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\toverflow: hidden;\n\t}\n\n\t// -- Content: scrollable lesson area --\n\t.llms-focus-mode-content {\n\t\tflex: 1;\n\t\tmin-width: 0;\n\t\toverflow-y: auto;\n\t\tpadding: 40px;\n\t\tbox-sizing: border-box;\n\n\t\t@media (max-width: 768px) {\n\t\t\tpadding: 20px;\n\t\t}\n\n\t\t.llms-focus-mode-title {\n\t\t\tmargin: 0 0 30px;\n\t\t\tfont-size: 1.8rem;\n\t\t\tfont-weight: 700;\n\t\t\tcolor: #1d2327;\n\t\t}\n\n\t\t.llms-lesson-content {\n\t\t\tmax-width: 100%;\n\n\t\t\timg {\n\t\t\t\tmax-width: 100%;\n\t\t\t\theight: auto;\n\t\t\t}\n\t\t}\n\t}\n\n\t// -- Footer: lesson action buttons pinned to bottom of content area --\n\t.llms-focus-mode-footer {\n\t\tflex-shrink: 0;\n\t\tborder-top: 1px solid #e5e5e5;\n\t\tbackground: #fff;\n\t\tbox-sizing: border-box;\n\n\t\t.clear {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t.llms-lesson-button-wrapper {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tgap: 10px;\n\t\t\tmargin: 0;\n\t\t\tpadding: 20px 10px;\n\n\t\t\t.llms-form-field {\n\t\t\t\tpadding-top: 0;\n\t\t\t\tpadding-bottom: 0;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\t\t}\n\n\t\t&:not(.llms-focus-mode-footer--mobile) {\n\t\t\t@media (max-width: 768px) {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\t\t}\n\n\t\t&.llms-focus-mode-footer--mobile {\n\t\t\tdisplay: none;\n\n\t\t\t@media (max-width: 768px) {\n\t\t\t\tdisplay: block;\n\t\t\t\tposition: sticky;\n\t\t\t\tbottom: 0;\n\t\t\t\tz-index: 10;\n\t\t\t}\n\t\t}\n\t}\n\n\t// =============================================\n\t// Sidebar collapsed state\n\t// =============================================\n\t&.llms-sidebar-collapsed {\n\n\t\t.llms-focus-mode-sidebar {\n\t\t\twidth: $focus-sidebar-collapsed-width;\n\t\t\tmin-width: $focus-sidebar-collapsed-width;\n\t\t\toverflow: hidden;\n\n\t\t\t.llms-focus-mode-sidebar-header,\n\t\t\t.llms-focus-mode-sidebar-content {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\n\t\t\t.llms-focus-mode-sidebar-toggle {\n\t\t\t\tposition: static;\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\tborder: none;\n\t\t\t\tborder-radius: 0;\n\t\t\t\tbackground: #fafafa;\n\t\t\t\tbox-shadow: none;\n\t\t\t\tright: auto;\n\n\t\t\t\tsvg {\n\t\t\t\t\twidth: 18px;\n\t\t\t\t\theight: 18px;\n\t\t\t\t}\n\n\t\t\t\t.llms-chevron-left {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t}\n\n\t\t\t\t.llms-chevron-right {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =============================================\n\t// Sidebar position: right\n\t// =============================================\n\t&.llms-focus-mode-sidebar-right {\n\n\t\t.llms-focus-mode-body {\n\t\t\tflex-direction: row-reverse;\n\t\t}\n\n\t\t.llms-focus-mode-sidebar {\n\t\t\tborder-right: 0;\n\t\t\tborder-left: 1px solid #e5e5e5;\n\t\t}\n\n\t\t.llms-focus-mode-sidebar-toggle {\n\t\t\tright: auto;\n\t\t\tleft: -14px;\n\t\t\tbox-shadow: -2px 0 4px rgba(0, 0, 0, 0.08);\n\n\t\t\t&:hover {\n\t\t\t\tbox-shadow: -2px 0 6px rgba(0, 0, 0, 0.12);\n\t\t\t}\n\n\t\t\t.llms-chevron-left {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\n\t\t\t.llms-chevron-right {\n\t\t\t\tdisplay: block;\n\t\t\t}\n\t\t}\n\n\t\t&.llms-sidebar-collapsed .llms-focus-mode-sidebar {\n\t\t\tborder-left: 0;\n\n\t\t\t.llms-focus-mode-sidebar-toggle {\n\t\t\t\tleft: auto;\n\t\t\t\tbox-shadow: none;\n\n\t\t\t\t.llms-chevron-left {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\n\t\t\t\t.llms-chevron-right {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =============================================\n\t// Content width variants\n\t// =============================================\n\t@each $width in 768, 960, 1180, 1600 {\n\t\t&.llms-focus-mode-width-#{$width} .llms-focus-mode-content {\n\n\t\t\t.llms-focus-mode-title,\n\t\t\t.llms-lesson-content {\n\t\t\t\tmax-width: #{$width}px;\n\t\t\t\tmargin-left: auto;\n\t\t\t\tmargin-right: auto;\n\t\t\t}\n\t\t}\n\t}\n\n\t// =============================================\n\t// Mobile: sidebar stacks below content\n\t// =============================================\n\t@media (max-width: 768px) {\n\n\t\t.llms-focus-mode-body {\n\t\t\tflex-direction: column;\n\t\t\toverflow-y: auto;\n\t\t}\n\n\t\t.llms-focus-mode-main {\n\t\t\tflex: none;\n\t\t\toverflow-y: visible;\n\t\t\torder: 1;\n\t\t}\n\n\t\t.llms-focus-mode-sidebar {\n\t\t\twidth: 100%;\n\t\t\tmin-width: 100%;\n\t\t\tborder-right: 0;\n\t\t\tborder-top: 1px solid #e5e5e5;\n\t\t\toverflow: hidden;\n\t\t\torder: 2;\n\n\t\t\t.llms-focus-mode-sidebar-content {\n\t\t\t\toverflow-y: visible;\n\t\t\t}\n\t\t}\n\n\t\t.llms-focus-mode-sidebar-toggle {\n\t\t\tdisplay: none;\n\t\t}\n\n\t\t// Override collapsed state so the sidebar is always visible on mobile.\n\t\t&.llms-sidebar-collapsed .llms-focus-mode-sidebar {\n\t\t\twidth: 100%;\n\t\t\tmin-width: 100%;\n\n\t\t\t.llms-focus-mode-sidebar-header,\n\t\t\t.llms-focus-mode-sidebar-content {\n\t\t\t\tdisplay: block;\n\t\t\t}\n\t\t}\n\n\t\t// Sidebar-right doesn't need reversed direction on mobile.\n\t\t&.llms-focus-mode-sidebar-right {\n\t\t\t.llms-focus-mode-body {\n\t\t\t\tflex-direction: column;\n\t\t\t}\n\n\t\t\t.llms-focus-mode-sidebar {\n\t\t\t\tborder-left: 0;\n\t\t\t\tborder-top: 1px solid #e5e5e5;\n\t\t\t}\n\t\t}\n\t}\n\n\n\t// Fix position of the Notes button.\n\t.llms-note-button.position-fixed__top, .llms-note-wrapper {\n\t\ttop: 68px;\n\t}\n\n\t@media screen and (max-width: 782px) {\n\t\t.llms-note-button.position-fixed__bottom {\n\t\t\tbottom: 86px;\n\t\t}\n\t}\n}\n\n.admin-bar.llms-focus-mode {\n\t.llms-focus-mode-wrapper {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\theight: calc( 100dvh - $admin-bar-height );\n\t\toverflow: hidden;\n\t}\n\n\t.llms-note-button.position-fixed__top, .llms-note-wrapper {\n\t\ttop: 100px;\n\t}\n\n\t@media screen and (max-width: 782px) {\n\t\t.llms-note-button.position-fixed__top, .llms-note-wrapper {\n\t\t\ttop: 112px;\n\t\t}\n\n\t\t.llms-focus-mode-wrapper {\n\t\t\theight: calc( 100dvh - $admin-bar-height-mobile );\n\t\t}\n\t}\n}\n\n\n"
  },
  {
    "path": "assets/scss/frontend/_llms-access-plans.scss",
    "content": ".llms-access-plans {\n\t@extend %clearfix;\n\n\t@media all and (min-width: 600px) {\n\t\t$cols: 1;\n\t\t@while $cols <= 5 {\n\t\t\t&.cols-#{$cols} .llms-access-plan {\n\t\t\t\twidth: calc( 100% / $cols );\n\t\t\t}\n\t\t\t$cols: $cols + 1;\n\t\t}\n\t}\n\n}\n\n.llms-free-enroll-form {\n\tmargin-bottom: 0;\n}\n\n.llms-access-plan {\n\tbox-sizing: border-box;\n\tfloat: left;\n\ttext-align: center;\n\twidth: 100%;\n\n\t.llms-access-plan-content,\n\t.llms-access-plan-footer {\n\t\tbackground: #f1f1f1;\n\t}\n\n\t&.featured {\n\n\t\t.llms-access-plan-featured {\n\t\t\tbackground: $color-brand-blue;\n\t\t\tborder-top-left-radius: $radius-small;\n\t\t\tborder-top-right-radius: $radius-small;\n\t\t}\n\n\t\t.llms-access-plan-content {\n\t\t\tborder-radius: 0;\n\t\t}\n\n\t\t.llms-access-plan-title {\n\t\t\tborder-top-left-radius: 0;\n\t\t\tborder-top-right-radius: 0;\n\t\t}\n\n\t\t.llms-access-plan-footer,\n\t\t.llms-access-plan-content {\n\t\t\tborder-left: 3px solid $color-brand-blue;\n\t\t\tborder-right: 3px solid $color-brand-blue;\n\t\t}\n\n\t\t.llms-access-plan-footer {\n\t\t\tborder-bottom-color: $color-brand-blue;\n\t\t}\n\n\t}\n\n\t&.on-sale {\n\t\t.price-regular {\n\t\t\ttext-decoration: line-through;\n\t\t\t.lifterlms-price {\n\t\t\t\tfont-weight: 400;\n\t\t\t}\n\t\t}\n\t}\n\n\t.stamp {\n\t\tbackground: $color-brand-blue;\n\t\tborder-radius: $radius-small;\n\t\tcolor: $color-white;\n\t\tdisplay: inline-block;\n\t\tfont-size: 14px;\n\t\tfont-style: normal;\n\t\tfont-weight: bold;\n\t\tline-height: 1;\n\t\tpadding: 5px 8px 4px;\n\t}\n}\n\n.llms-access-plan-featured {\n\tcolor: #fff;\n\tfont-size: 14px;\n\tfont-weight: 700;\n\tletter-spacing: 1px;\n\tmargin: 0 2px 0 2px;\n\tpadding: 5px 0 0;\n}\n\n.llms-access-plan-content {\n\tborder-top-right-radius: $radius-small;\n\tborder-top-left-radius: $radius-small;\n\tmargin: 0 2px 0;\n\toverflow: hidden;\n\n\t.llms-access-plan-pricing {\n\t\tcolor: $color-black;\n\t\tpadding: 20px 0 0;\n\t}\n\n\t.llms-form-fields {\n\t\tpadding: 10px;\n\t}\n}\n\n.llms-access-plan-title {\n\tbackground: $color-brand-blue;\n\tcolor: $color-white;\n\tmargin: 0;\n\tpadding: 10px 15px;\n}\n\n.llms-access-plan-pricing {\n\n\t.llms-price-currency-symbol {\n\t\tfont-size: 14px;\n\t\tvertical-align: top;\n\t}\n\n}\n\n.llms-access-plan-price {\n\n\t.lifterlms-price {\n\t\tfont-weight: 700;\n\t}\n\n\t&.sale {\n\t\tpadding: 5px 0;\n\t\tborder-top: 1px solid #d0d0d0;\n\t\tborder-bottom: 1px solid #d0d0d0;\n\t}\n}\n\n.llms-access-plan-trial,\n.llms-access-plan-schedule,\n.llms-access-plan-sale-end,\n.llms-access-plan-expiration {\n\tfont-size: 16px;\n}\n\n.llms-access-plan-description {\n\tcolor: $color-black;\n\tfont-size: 16px;\n\tpadding: 10px 10px 0;\n\n\tul {\n\t\tmargin: 0;\n\t\tli {\n\t\t\tborder-bottom: 1px solid #d0d0d0;\n\t\t\tlist-style-type: none;\n\t\t\t&:last-child {\n\t\t\t\tborder-bottom: none;\n\t\t\t}\n\t\t}\n\t}\n\n\tdiv, img, p, ul, li {\n\t\t&:last-child { margin-bottom: 0; }\n\t}\n}\n\n.llms-access-plan-restrictions {\n\tmargin-top: 20px;\n\n\t.stamp {\n\t\tvertical-align: baseline;\n\t}\n\n\tul {\n\t\tmargin: 5px 0 0 0;\n\t\tpadding: 0;\n\n\t\tli {\n\t\t\tfont-size: 14px;\n\t\t\tlist-style-type: none;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\t}\n\n\ta {\n\t\tcolor: $color-orange;\n\t\t&:hover {\n\t\t\tcolor: $color-brand-orange-dark;\n\t\t}\n\t}\n}\n\n.llms-access-plan-footer {\n\tborder-bottom: 3px solid #f1f1f1;\n\tborder-bottom-left-radius: $radius-small;\n\tborder-bottom-right-radius: $radius-small;\n\tpadding: 15px;\n\tmargin: 0 2px 2px 2px;\n\n\t.button {\n\t\tdisplay: inline-block;\n\t}\n\n\t.llms-access-plan-pricing {\n\t\tpadding: 0 0 10px;\n\t}\n}\n\n.webui-popover-content .llms-members-only-restrictions {\n\ttext-align: center;\n\tul,ol,li,p {\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t}\n\tul,ol,li {\n\t\tlist-style-type: none;\n\t}\n\tli {\n\t\tpadding: 8px 0;\n\t\tborder-top: 1px solid #3b3b3b;\n\t\t&:first-child {\n\t\t\tborder-top: none;\n\t\t}\n\t\ta {\n\t\t\tdisplay: block;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_llms-achievements-certs.scss",
    "content": "ul.llms-achievements-loop,\n.lifterlms ul.llms-achievements-loop,\nul.llms-certificates-loop,\n.lifterlms ul.llms-certificates-loop {\n\n\t@include clearfix();\n\tlist-style-type: none;\n\tmargin: 0 -10px;\n\tpadding: 0;\n\n\tli.llms-achievement-loop-item,\n\tli.llms-certificate-loop-item {\n\t\tbox-sizing: border-box;\n\t\tdisplay: block;\n\t\tfloat: left;\n\t\tlist-style-type: none;\n\t\tmargin: 0;\n\t\tpadding: 10px;\n\t\twidth: 100%;\n\t}\n\n\t@media all and (min-width: 600px) {\n\t\t$cols: 1;\n\t\t@while $cols <= 5 {\n\t\t\t&.loop-cols-#{$cols} li.llms-achievement-loop-item,\n\t\t\t&.loop-cols-#{$cols} li.llms-certificate-loop-item {\n\t\t\t\twidth: calc( 100% / $cols );\n\t\t\t}\n\t\t\t$cols: $cols + 1;\n\t\t}\n\t}\n\n}\n\n.llms-achievement,\n.llms-certificate {\n\n\tbackground: $color-white;\n\tborder: none;\n\tborder-radius: $radius-medium;\n\tcolor: $color-black;\n\tdisplay: block;\n\tpadding: 20px 0;\n\ttext-decoration: none;\n\twidth: 100%;\n\n\t&:hover {\n\t\tbackground: #eaeaea;\n\t}\n\n\t.llms-achievement-img {\n\t\tdisplay: block;\n\t\tmargin: 0 auto;\n\t\tmax-width: 80%;\n\t}\n\n\t.llms-achievement-title {\n\t\tcolor: $color-black;\n\t\tfont-size: 16px;\n\t\tmargin: 0;\n\t\tpadding: 10px;\n\t\ttext-align: center;\n\t}\n\n\t.llms-certificate-title {\n\t\tcolor: $color-black;\n\t\tfont-size: 16px;\n\t\tmargin: 0;\n\t\tpadding: 0 0 10px;\n\t}\n\n\t.llms-achievement-info,\n\t.llms-achievement-date {\n\t\tdisplay: none;\n\t}\n\n\t.llms-achievement-content {\n\t\tpadding: 20px;\n\t\t&:empty {\n\t\t\tpadding: 0;\n\t\t}\n\t\t*:last-child {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\t}\n\n}\n\n.llms-certificate {\n\tborder: 4px double $color-border;\n\tpadding: 20px 10px;\n\tbackground: $color-white;\n\ttext-align: center;\n\t&:hover {\n\t\tbackground: $color-white;\n\t\tborder-color: #eaeaea;\n\t}\n}\n\n.llms-achievement-modal {\n\t.llms-achievement {\n\t\tbackground: #fff;\n\t}\n\t.llms-achievement-info {\n\t\tdisplay: block;\n\t}\n\t.llms-achievement-title {\n\t\tdisplay: none;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_llms-author.scss",
    "content": ".llms-author {\n\t.name {\n\t\tmargin-left: 5px;\n\t}\n\t.label {\n\t\tmargin-left: 5px;\n\t}\n\t.avatar {\n\t\tborder-radius: 50%;\n\t}\n\t.bio {\n\t\tmargin-top: 5px;\n\t}\n}\n\n\n.llms-instructor-info {\n\tmargin: 40px 0;\n\t.llms-meta-title {\n\t\tmargin: 0;\n\t}\n\t.llms-instructors {\n\n\t\t.llms-col {\n\t\t\t&:first-child .llms-author {\n\t\t\t\tmargin-left: 0;\n\t\t\t}\n\t\t\t&:last-child .llms-author {\n\t\t\t\tmargin-right: 0;\n\t\t\t}\n\t\t}\n\n\t\t.llms-author {\n\t\t\tbackground: $color-white;\n\t\t\tborder: 1px solid $color-border;\n\t\t\tborder-top: 4px solid $color-blue;\n\t\t\tborder-bottom-right-radius: $radius-small;\n\t\t\tborder-bottom-left-radius: $radius-small;\n\t\t\tcolor: $color-black;\n\t\t\ttext-align: center;\n\t\t\tmargin: 45px 0 0;\n\t\t\tpadding: 0 20px 20px;\n\n\t\t\t.avatar {\n\t\t\t\tbackground: $color-brand-blue;\n\t\t\t\tborder: 4px solid $color-blue;\n\t\t\t\tdisplay: block;\n\t\t\t\tmargin: -35px auto 10px;\n\t\t\t}\n\n\t\t\t.llms-author-info {\n\t\t\t\tdisplay: block;\n\t\t\t\t// margin: 0 0 5px;\n\t\t\t\t&.name {\n\t\t\t\t\tfont-weight: 700;\n\t\t\t\t}\n\t\t\t\t&.label {\n\t\t\t\t\tfont-size: 85%;\n\t\t\t\t}\n\t\t\t\t&.bio {\n\t\t\t\t\tfont-size: 90%;\n\t\t\t\t\tmargin-bottom: 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/frontend/_llms-notifications.scss",
    "content": ".llms-notification {\n\n\t@include clearfix();\n\n\tbackground: $color-white;\n\tbox-shadow: 0 1px 2px -2px #333, 0 1px 1px -1px #333;\n\tborder-top: 4px solid $color-blue;\n\tcolor: $color-black;\n\topacity: 0;\n\tpadding: 12px;\n\tposition: fixed;\n\tright: -800px;\n\ttop: 24px;\n\ttransition:\n\t\topacity 0.4s ease-in-out,\n\t\tright 0.4s ease-in-out,\n\t;\n\tvisibility: hidden;\n\twidth: auto;\n\tz-index: 9999999;\n\n\t&.visible {\n\t\tleft: 12px;\n\t\topacity: 1;\n\t\tright: 12px;\n\t\ttransition:\n\t\t\topacity 0.4s ease-in-out,\n\t\t\tright 0.4s ease-in-out,\n\t\t\ttop 0.1s ease-in-out,\n\t\t\tbackground 0.2s ease-in-out,\n\t\t\ttransform 0.2s ease-in-out\n\t\t;\n\t\tvisibility: visible;\n\n\t\t&:hover {\n\t\t\t.llms-notification-dismiss {\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t.llms-notification-content {\n\t\talign-items: center;\n\t\tdisplay: flex;\n\n\t}\n\n\t\t.llms-notification-main {\n\t\t\talign-self: flex-start;\n\t\t\tflex: 4;\n\t\t\torder: 2;\n\t\t}\n\n\t\t\t.llms-notification-title {\n\t\t\t\tcolor: $color-black;\n\t\t\t\tfont-size: 18px;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.llms-notification-body {\n\t\t\t\tfont-size: 14px;\n\t\t\t\tline-height: 1.4;\n\t\t\t\tp, li {\n\t\t\t\t\tfont-size: inherit;\n\t\t\t\t}\n\t\t\t\tp {\n\t\t\t\t\tmargin-bottom: 8px;\n\t\t\t\t\timg {\n\t\t\t\t\t\tmax-width: 100%;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.llms-mini-cert {\n\t\t\t\t\tbackground: #f6f6f6;\n\t\t\t\t\tborder: 1px solid #d0d0d0;\n\t\t\t\t\tbox-shadow: inset 0 0 0 3px #fefefe, inset 0 0 0 4px #dedede;\n\t\t\t\t\tpadding: 16px 16px 24px;\n\t\t\t\t\tmargin-top: 12px;\n\t\t\t\t\t.llms-mini-cert-title {\n\t\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\t\tfont-weight: 700;\n\t\t\t\t\t\tmargin: 12px auto;\n\t\t\t\t\t\ttext-align: center;\n\t\t\t\t\t}\n\t\t\t\t\t.llms-mini-cert--body {\n\t\t\t\t\t\twidth: 100%;\n\t\t\t\t\t\t> div {\n\t\t\t\t\t\t\t&:nth-child(1) { width:65%; }\n\t\t\t\t\t\t\t&:nth-child(2) { width:35%; }\n\t\t\t\t\t\t\t&:nth-child(3) { width:85%; }\n\t\t\t\t\t\t\t&:nth-child(4) { width:75%; margin-top: 18px; }\n\t\t\t\t\t\t\t&:nth-child(5) { width:70%; }\n\t\t\t\t\t\t\t&:nth-child(6) { margin-left: 12px; margin-bottom:-24px; } // Dot.\n\t\t\t\t\t\t\t&:nth-child(7) { width:65%; margin-right: 12px; }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t.llms-mini-cert--mock-line {\n\t\t\t\t\t\tborder-radius: 2px;\n\t\t\t\t\t\theight: 8px;\n\t\t\t\t\t\tbackground: #b0b0b0;\n\t\t\t\t\t\tbackground-image: linear-gradient( to right, #b0b0b0 0%, #a0a0a0 20%, #b0b0b0 40%, #b0b0b0 100% );\n\t\t\t\t\t\tbackground-repeat: no-repeat;\n\t\t\t\t\t\tmargin: 4px auto;\n\t\t\t\t\t}\n\t\t\t\t\t.llms-mini-cert--mock-dot {\n\t\t\t\t\t\tbackground: #b0b0b0;\n\t\t\t\t\t\tborder-radius: 50%;\n\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t\tcontent: '';\n\t\t\t\t\t\theight: 24px;\n\t\t\t\t\t\twidth: 24px;\n\t\t\t\t\t}\n\t\t\t\t\tp { margin-bottom: 0; }\n\t\t\t\t}\n\t\t\t}\n\n\t\t.llms-notification-aside {\n\t\t\talign-self: flex-start;\n\t\t\tflex: 1;\n\t\t\tmargin-right: 12px;\n\t\t\torder: 1;\n\t\t}\n\n\t\t\t.llms-notification-icon {\n\t\t\t\tdisplay: block;\n\t\t\t\tmax-width: 64px;\n\t\t\t}\n\n\t.llms-notification-footer {\n\t\tborder-top: 1px solid #e5e5e5;\n\t\tfont-size: 12px;\n\t\tmargin-top: 12px;\n\t\tpadding: 6px 6px 0;\n\t\ttext-align: right;\n\n\t\ta {\n\t\t\tcolor: $color-black;\n\t\t}\n\t}\n\n\t.llms-notification-dismiss {\n\t\tcolor: $color-danger;\n\t\tcursor: pointer;\n\t\tfont-size: 22px;\n\t\tposition: absolute;\n\t\tright: 10px;\n\t\ttop: 8px;\n\t\ttransition: opacity 0.4s ease-in-out;\n\t}\n\n}\n\n.llms-sd-notification-center {\n\n\t.llms-notification-list,\n\t.llms-notification-list-item {\n\t\tlist-style-type: none;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t}\n\n\t.llms-notification-list-item {\n\t\t&:hover .llms-notification {\n\t\t\tbackground: #fcfcfc;\n\t\t}\n\t}\n\n\t.llms-notification {\n\t\topacity: 1;\n\t\tborder-top: 1px solid #e5e5e5;\n\t\tleft: auto;\n\t\tpadding: 24px;\n\t\tposition: relative;\n\t\tright: auto;\n\t\ttop: auto;\n\t\tvisibility: visible;\n\t\twidth: auto;\n\t\t.llms-notification-aside {\n\t\t\tmax-width: 64px;\n\t\t}\n\t\t.llms-notification-footer {\n\t\t\tborder: none;\n\t\t\tpadding: 0;\n\t\t\ttext-align: left;\n\t\t}\n\t\t.llms-progress {\n\t\t\tdisplay: none !important;\n\t\t}\n\t\t.llms-notification-date {\n\t\t\tcolor: #515151;\n\t\t\tfloat: left;\n\t\t\tmargin-right: 6px;\n\t\t}\n\t\t.llms-mini-cert {\n\t\t\tmargin: 0 auto;\n\t\t\tmax-width: 380px;\n\t\t}\n\t}\n}\n\n@media all and (min-width: 480px) {\n\t.llms-notification {\n\t\tright: -800px;\n\t\twidth: 360px;\n\t\t&.visible {\n\t\t\tleft: auto;\n\t\t\tright: 24px;\n\t\t}\n\t\t.llms-notification-dismiss {\n\t\t\topacity: 0;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_llms-outline-collapse.scss",
    "content": ".llms-widget-syllabus--collapsible {\n\n\t.llms-section {\n\n\t\t.section-header {\n\n\t\t\tcursor: pointer;\n\n\t\t}\n\n\t\t&.llms-section--opened {\n\n\t\t\t.llms-collapse-caret {\n\t\t\t\t.fa-caret-right { display: none; }\n\t\t\t}\n\n\t\t}\n\n\t\t&.llms-section--closed {\n\n\t\t\t.llms-collapse-caret {\n\t\t\t\t.fa-caret-down { display: none; }\n\t\t\t}\n\n\t\t\t.llms-lesson {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t.llms-syllabus-footer {\n\n\t\ttext-align: left;\n\n\t}\n\n}\n"
  },
  {
    "path": "assets/scss/frontend/_llms-pagination.scss",
    "content": ".llms-pagination {\n\n\tul {\n\t\tlist-style-type: none;\n\t\tmargin: 40px 0;\n\t\tpadding: 0;\n\t\t@extend %cf;\n\n\t\tli {\n\n\t\t\tfloat: left;\n\n\t\t\ta {\n\t\t\t\tborder-bottom: 0;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\n\t\t\t.page-numbers {\n\t\t\t\tpadding: 0.5em;\n\t\t\t\ttext-decoration: underline;\n\n\t\t\t\t&.current {\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}"
  },
  {
    "path": "assets/scss/frontend/_llms-progress.scss",
    "content": "/* progress bar */\n.llms-progress {\n\tclear: both;\n\tdisplay: flex;\n\tflex-direction: row-reverse;\n\tposition: relative;\n\theight: 1em;\n\twidth: 100%;\n\tmargin: 15px 0;\n}\n\n.llms-progress .llms-progress-bar {\n\tbackground-color: $color-border;\n\tposition: relative;\n\theight: .4em;\n\ttop: .3em;\n\twidth: 100%;\n}\n\n.llms-progress .progress-bar-complete {\n\tbackground-color: $color-brand-pink;\n\theight: 100%;\n}\n\n.progress__indicator {\n\tfloat: right;\n\ttext-align: right;\n\theight: 1em;\n\tline-height: 1em;\n\tmargin-left: 5px;\n\twhite-space: nowrap;\n}\n"
  },
  {
    "path": "assets/scss/frontend/_llms-quizzes.scss",
    "content": ".single-llms_quiz {\n\t#llms-quiz-wrapper {\n\t\t@import \"../_includes/quiz-result-question-list\";\n\t}\n\t.llms-return {\n\t\tmargin-bottom: 10px;\n\t}\n\n\t.llms-quiz-results {\n\t\t@include clearfix();\n\n\t\t.llms-donut {\n\t\t\t&.passing {\n\t\t\t\tcolor: $color-success;\n\t\t\t\tsvg path {\n\t\t\t\t\tstroke: $color-success;\n\t\t\t\t}\n\t\t\t}\n\t\t\t&.pending {\n\t\t\t\tcolor: #555;\n\t\t\t\tsvg path {\n\t\t\t\t\tstroke: #555;\n\t\t\t\t}\n\t\t\t}\n\t\t\t&.failing {\n\t\t\t\tcolor: $color-danger;\n\t\t\t\tsvg path {\n\t\t\t\t\tstroke: $color-danger;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t.llms-quiz-results-aside,\n\t\t.llms-quiz-results-main,\n\t\t.llms-quiz-results-history {\n\t\t\tmargin-bottom: 20px;\n\t\t}\n\n\n\t\t@media all and (min-width: 600px) {\n\t\t\t.llms-quiz-results-aside {\n\t\t\t\tfloat: left;\n\t\t\t\twidth: 220px;\n\n\t\t\t\t+ .llms-quiz-results-main {\n\t\t\t\t\tfloat: right;\n\t\t\t\t\twidth: calc( 100% - 300px );\n\t\t\t\t\t+ .llms-quiz-results-history {\n\t\t\t\t\t\tfloat: right;\n\t\t\t\t\t\twidth: calc( 100% - 300px );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t.llms-quiz-results-history {\n\t\t\t\tclear: right;\n\t\t\t}\n\t\t}\n\n\t}\n\n\tul.llms-quiz-meta-info {\n\t\tlist-style-type: none;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\n\t\tli {\n\t\t\tlist-style-type: none;\n\t\t\tmargin: 10px 0 0;\n\t\t\tpadding: 0\n\t\t}\n\t}\n\n\tul.llms-quiz-meta-info {\n\t\tmargin-bottom: 10px;\n\t}\n\n\t.llms-quiz-buttons {\n\t\tmargin-top: 10px;\n\t\ttext-align: left;\n\n\t\tform { display: inline-block; }\n\t}\n\n}\n\n.llms-quiz-question-wrapper {\n\tmin-height: 140px;\n\tposition: relative;\n\n\t.llms-quiz-loading {\n\t\tbottom: 20px;\n\t\tleft: 0;\n\t\tposition: absolute;\n\t\tright: 0;\n\t\ttext-align: center;\n\t\tz-index: 1;\n\t}\n\n\t.llms-question-image {\n\n\t\timg {\n\t\t\theight: auto;\n\t\t\tmax-width: 100%;\n\t\t}\n\n\t}\n}\n\n.llms-quiz-ui {\n\tposition: relative;\n\n\t.llms-quiz-header {\n\t\talign-items: center;\n\t\tdisplay: flex;\n\t\tmargin: 0 0 40px;\n\t}\n\n\t.llms-progress {\n\t\tbackground-color: $color-border;\n\t\tflex-direction: row;\n\t\theight: 8px;\n\t\tmargin: 0;\n\t\toverflow: hidden;\n\t\t.progress-bar-complete {\n\t\t\ttransition: width 0.3s ease-in;\n\t\t\twidth: 0;\n\t\t}\n\t}\n\n\t.llms-error {\n\t\t@include clearfix();\n\t\tbackground: rgba( $color-red, .15 );\n\t\tborder: 1px solid $color-red;\n\t\tborder-radius: $radius-small;\n\t\tmargin: 20px 0;\n\t\tpadding: 15px;\n\n\t\ta {\n\t\t\tcolor: $color-red;\n\t\t\tfloat: right;\n\t\t\tfont-size: 22px;\n\t\t\tline-height: 1;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t}\n\n\t.llms-quiz-counter {\n\t\tdisplay: none;\n\n\t\tcolor: #6a6a6a;\n\t\tfloat: right;\n\t\tfont-size: 18px;\n\n\t\t.llms-sep {\n\t\t\tmargin: 0 5px;\n\t\t}\n\t}\n\n\t.llms-quiz-nav {\n\t\tmargin-top: 20px;\n\t\tbutton {\n\t\t\tmargin: 0 10px 0 0;\n\t\t}\n\t}\n\n}\n\n// single question wrapper\n.llms-question-wrapper {\n\n\t.llms-question-text {\n\t\tfont-size: 30px;\n\t\tfont-weight: 400;\n\t\tmargin-bottom: 15px;\n\t}\n\n\t.llms-question-choices {\n\t\tborder: none;\n\n\t\t&.type--picture {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: auto auto auto;\n\t\t\tgap: 20px;\n\t\t}\n\n\t\tlist-style-type: none;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\n\t\t.llms-choice {\n\t\t\tborder-bottom: 1px solid #e8e8e8;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t\tposition: relative;\n\n\t\t\t&:last-child {\n\t\t\t\tborder-bottom: none;\n\t\t\t}\n\n\t\t\tlabel {\n\t\t\t\tdisplay: block;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 10px 20px;\n\t\t\t\tposition: relative;\n\t\t\t}\n\n\t\t\tinput {\n\t\t\t\topacity: 0;\n\n\t\t\t\t+ label {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\twidth: calc( 100% - 90px );\n\n\t\t\t\t\t.llms-choice-text {\n\t\t\t\t\t\tline-height: 1.6;\n\t\t\t\t\t\tvertical-align: middle;\n\t\t\t\t\t\tfont-size: 18px;\n\t\t\t\t\t\tfont-weight: 400;\n\t\t\t\t\t\tmargin: 5px 0 0 0;\n\t\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t}\n\n\t\t\t\t\t&:before {\n\t\t\t\t\t\tcontent: attr(data-marker);\n\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t\tbackground: $color-white;\n\t\t\t\t\t\tcolor: $color-black;\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\tleft: -32px;\n\t\t\t\t\t\twidth: 40px;\n\t\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\t\tfont-weight: inherit;\n\t\t\t\t\t\ttext-align: center;\n\t\t\t\t\t\theight: 40px;\n\t\t\t\t\t\talign-items: center;\n\t\t\t\t\t\tjustify-content: center;\n\t\t\t\t\t\ttransition: all 0.2s ease;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&[type=\"checkbox\"] {\n\t\t\t\t\t+ label:before {\n\t\t\t\t\t\tborder-radius: 4px;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&[type=\"radio\"] {\n\t\t\t\t\t+ label:before {\n\t\t\t\t\t\tborder-radius: 50%;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&:checked {\n\t\t\t\t\t+ label:before {\n\t\t\t\t\t\tbackground: $color-brand-pink;\n\t\t\t\t\t\tcolor: #fff;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&:focus {\n\t\t\t\t\t+ label:before {\n\t\t\t\t\t\tbox-shadow: 0 0px 8px $color-brand-pink;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&:not(:checked) {\n\t\t\t\t\t+ label.hovered:before {\n\t\t\t\t\t\tbox-shadow: 0 0px 8px $color-brand-pink;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t&.type--picture {\n\t\t\t\tborder-bottom: none;\n\t\t\t\tlabel {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\theight: auto;\n\n\t\t\t\t\t&:before {\n\t\t\t\t\t\tleft: auto;\n\t\t\t\t\t\tright: 10px;\n\t\t\t\t\t\tbottom: 10px;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.llms-choice-image {\n\t\t\t\t\tmargin: 2px;\n\t\t\t\t\tpadding: 20px;\n\t\t\t\t\ttransition: background 0.4s ease;\n\t\t\t\t\timg {\n\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t\theight: auto;\n\t\t\t\t\t\twidth: 100%;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinput:checked {\n\t\t\t\t\t+ label .llms-choice-image {\n\t\t\t\t\t\tbackground: #efefef\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-marker {\n\n\t\t\t\tbackground: $color-white;\n\t\t\t\tcolor: $color-black;\n\t\t\t\tdisplay: inline-block;\n\t\t\t\tfont-size: 20px;\n\t\t\t\theight: 40px;\n\t\t\t\tline-height: 40px;\n\t\t\t\tmargin-right: 10px;\n\t\t\t\ttext-align: center;\n\t\t\t\ttransition: all 0.2s ease;\n\t\t\t\tvertical-align: middle;\n\t\t\t\twidth: 40px;\n\n\t\t\t\t.fa {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t}\n\n\t\t\t\t&.type--lister,\n\t\t\t\t&.type--checkbox { border-radius: 4px; }\n\t\t\t\t&.type--radio { border-radius: 50%; }\n\n\t\t\t}\n\n\n\n\n\t\t}\n\t}\n\n}\n\n.llms-quiz-timer {\n\tbackground: #fff;\n\tborder: 1px solid $color-green;\n\tborder-radius: 4px;\n\tcolor: $color-green;\n\tfloat: right;\n\tfont-size: 18px;\n\tline-height: 1;\n\tmargin-left: 20px;\n\tpadding: 8px 12px;\n\tposition: relative;\n\twhite-space: nowrap;\n\tz-index: 1;\n\n\t&.color-half {\n\t\tborder-color: $color-orange;\n\t\tcolor: $color-orange\n\t}\n\n\t&.color-empty {\n\t\tborder-color: $color-danger;\n\t\tcolor: $color-danger\n\t}\n\n\t.llms-tiles {\n\t\tdisplay: inline-block;\n\t\tmargin-left: 5px;\n\t}\n}\n\n\n// /* My Quizzes */\n// .llms-quiz-results {\n//   @extend %cf;\n//   font-family: \"Open Sans\",Verdana,Geneva,sans-serif,sans-serif;\n//   position: relative;\n// }\n// .llms-quiz-results > h3 {\n//   background-color: #f5f5f5;\n//   padding: 4px;\n// }\n\n// .llms-quiz-result-details {\n//   float: left;\n//   ul {\n//     list-style-type: none;\n//     float: left;\n//     li {\n//       list-style-type: none;\n//       font-size: 20px;\n//     }\n//   }\n// }\n// .llms-attempts {\n//   font-weight: bold;\n// }\n\n// .llms-pass-perc {\n//   font-weight: bold;\n// }\n// .llms-content-block {\n//   margin: 6px 0;\n// }\n// .llms-question-wrapper {\n//   margin: 40px 0 20px 0;\n// }\n// .llms-question-count {\n//   margin-bottom: 20px;\n// }\n\n\n"
  },
  {
    "path": "assets/scss/frontend/_llms-table.scss",
    "content": ".llms-table {\n\tborder: 1px solid $color-border;\n\tborder-spacing: 0;\n\twidth: 100%;\n\n\tthead {\n\t\tth,td {\n\t\t\tfont-weight: 700;\n\t\t}\n\t}\n\n\ttbody {\n\t\ttr:nth-child( odd ) {\n\t\t\ttd, th {\n\t\t\t\tbackground: #f9f9f9;\n\t\t\t}\n\t\t}\n\t\ttr:last-child {\n\t\t\tborder-bottom-width: 0;\n\t\t}\n\t}\n\n\ttfoot {\n\t\ttr {\n\t\t\tbackground: #f9f9f9;\n\t\t\t.llms-pagination .page-numbers {\n\t\t\t\tmargin: 0;\n\t\t\t}\n\t\t\t.llms-table-sort {\n\t\t\t\ttext-align: right;\n\t\t\t\tform, select, input, button {\n\t\t\t\t\tmargin: 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tth {\n\t\tfont-weight: 700;\n\t}\n\n\tth, td {\n\t\tborder-bottom: 1px solid $color-border;\n\t\tpadding: 15px 20px;\n\n\t\t// launchpad compat...\n\t\t&:first-child { padding-left: 12px; }\n\t\t&:last-child { padding-right: 12px; }\n\n\t}\n\n}\n\n// launchpad compat...\n#page .llms-table tfoot label {\n\tdisplay: inline;\n}\n#page .llms-table tfoot select {\n\theight: auto;\n}\n"
  },
  {
    "path": "assets/scss/frontend/_loop.scss",
    "content": ".llms-loop-list {\n\tdisplay: grid;\n\n\t// Default, the cols-X classes below should override.\n\tgrid-template-columns: repeat(3,minmax(0, 1fr));\n\n\tgap: 20px;\n\n\t.llms-loop-item {\n\t\tlist-style: none;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t}\n\n\t@media (max-width: 768px) {\n\t\tgrid-template-columns: repeat(1, 1fr);\n\t}\n\n\tlist-style: none;\n\tmargin: 0 -10px;\n\tpadding: 0;\n\n\t@media all and (min-width: 600px) {\n\t\t$cols: 1;\n\t\t@while $cols <= 6 {\n\t\t\t&.cols-#{$cols} {\n\t\t\t\tgrid-template-columns: repeat($cols, minmax(0, 1fr));\n\t\t\t}\n\t\t\t$cols: $cols + 1;\n\t\t}\n\t}\n\n\n}\n\n.llms-loop-item {\n\tlist-style: none;\n\tmargin: 10px;\n\tpadding: 0;\n\tbackground: $color-white;\n\tborder: 1px solid $color-border;\n\tborder-radius: $radius-small;\n}\n\n\n\t.llms-loop-item-content {\n\t\toverflow: hidden;\n\t\tpadding-bottom: 15px;\n\t\tmargin: 0;\n\t\theight: auto;\n\n\t\tiframe {\n\t\t\tmax-width: 100%;\n\t\t}\n\n\t\t.llms-loop-link {\n\t\t\tcolor: $color-black;\n\t\t\tdisplay: block;\n\t\t\ttext-decoration: none;\n\t\t\t&:visited {\n\t\t\t\tcolor: $color-black;\n\t\t\t}\n\t\t}\n\n\t\t.llms-featured-image {\n\t\t\tdisplay: block;\n\t\t\tmax-width: 100%;\n\t\t}\n\n\t\t.llms-loop-title {\n\t\t\tcolor: $color-black;\n\t\t\tmargin: 15px 0;\n\t\t\t&:hover {\n\t\t\t\tcolor: $color-brand-blue;\n\t\t\t}\n\t\t}\n\n\t\t.llms-meta,\n\t\t.llms-author,\n\t\t.llms-loop-title,\n\t\t.llms-featured-pricing\n\t\t{\n\t\t\tpadding: 0 15px;\n\t\t}\n\n\t\t.llms-meta,\n\t\t.llms-author,\n\t\t.llms-featured-pricing\n\t\t{\n\t\t\tcolor: $color-cinder;\n\t\t\tdisplay: block;\n\t\t\tfont-size: 14px;\n\t\t\tmargin-bottom: 10px;\n\t\t\t&:last-child {\n\t\t\t\tmargin-bottom: 0;\n\t\t\t}\n\t\t}\n\n\t\t.llms-author {\n\t\t\talign-items: center;\n\t\t\tdisplay: flex;\n\t\t\tgap: 5px;\n\t\t}\n\n\t\t.llms-featured-img-wrap {\n\t\t\toverflow: hidden;\n\t\t}\n\n\t\tp {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\n\t\t.llms-progress {\n\t\t\tmargin: 0;\n\t\t\theight: .4em;\n\n\t\t\t.progress__indicator {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\n\t\t\t.llms-progress-bar {\n\t\t\t\tbackground-color: #f6f6f6;\n\t\t\t\tright: 0;\n\t\t\t\ttop: 0;\n\t\t\t}\n\t\t}\n\n\t}\n\n\n\n// .llms-membership-list .memberships {\n//   border-top: 1px solid #f6f6f6;\n//   width: 100%;\n//   display: inline-block;\n//   text-align: center;\n//   list-style: none;\n//   clear: both;\n//   margin: 0;\n//   padding: 0;\n// }\n\n\n\n// .llms-course-list {\n\n//   .llms-membership-link {\n//     @extend %llms-element;\n\n//     display: block\n//   }\n\n//   .llms-membership-footer {\n//     border-top: 3px solid $color-white;\n//     margin: 15px -15px 0;\n//     padding: 15px 15px 0;\n//     text-align: center;\n//   }\n\n// }\n\n\n\n\n// .llms-membership-list .memberships li {\n//   width: 300px;\n//   margin: 15px;\n//   list-style: none;\n//   vertical-align: top;\n//   display: inline-block;\n//   text-align: left;\n// }\n\n// .llms-membership-list .memberships li.first {\n//   margin-left: 0;\n// }\n\n// .llms-membership-list .memberships li.last {\n//   margin-right: 15px;\n// }\n\n// .llms-membership-list .memberships li .llms-title {\n//   display: block;\n//   font-weight: 700;\n//   margin-bottom: .5em;\n//   font-size: 18px;\n//   text-decoration: none;\n//   line-height: 30px;\n// }\n\n// .llms-membership-list .memberships li .llms-price {\n//   display: block;\n//   font-weight: 700;\n//   // margin-bottom: .5em;\n//   // font-size: 24px;\n//   text-decoration: none;\n//   line-height: 30px;\n// }\n\n// .llms-course-list {\n//   //margin: 30px 0;\n//   padding: 30px;\n//   //background: #FFF;\n//   // border-radius: 2px;\n//   display: inline-block;\n//   width: 100%;\n//   box-sizing: border-box;\n\n//   .llms-course-link {\n//     @extend %llms-element;\n\n//     display: block\n//   }\n\n//   .llms-course-footer {\n//     border-top: 3px solid $color-white;\n//     margin: 15px -15px 0;\n//     padding: 15px 15px 0;\n//     text-align: center;\n//   }\n\n//   .llms-progress {\n//     margin-top: 0;\n//     // .progress-bar {\n//     // background-color: $color-white;\n//     // }\n//   }\n\n// }\n\n// .llms-course-list .courses {\n//   //border-top: 1px solid #f6f6f6;\n//   width: 100%;\n//   display: inline-block;\n//   text-align: center;\n//   list-style: none;\n//   clear: both;\n//   margin: 0;\n//   padding: 0;\n// }\n\n// .llms-course-list .courses li {\n//   width: 300px;\n//   padding-top: 0; // twentyfifteen compat\n//   margin: 15px;\n//   list-style: none;\n//   vertical-align: top;\n//   display: inline-block;\n//   text-align: left;\n// }\n// @media screen and (max-width: $break-small) {\n//   .llms-course-list {\n//     padding: 30px 10px;\n\n//     .courses li {\n//       width: auto;\n//     }\n//   }\n// }\n\n// // .llms-course-list .courses li.first {\n// // \tmargin-left: 0;\n// // }\n\n// .llms-course-list .courses li.last {\n//   margin-right: 15px;\n// }\n\n// .llms-course-list .courses li .llms-title {\n//   display: block;\n//   font-weight: 700;\n//   margin-bottom: .5em;\n//   font-size: 18px;\n//   text-decoration: none;\n//   line-height: 30px;\n// }\n\n// .llms-course-list .courses li .llms-price {\n//   display: block;\n//   font-weight: 700;\n//   // margin-bottom: .5em;\n//   // font-size: 24px;\n//   text-decoration: none;\n//   line-height: 30px;\n// }\n\n\n\n\n// .courses a.llms-course-link:hover {\n//   text-decoration: none;\n// }\n"
  },
  {
    "path": "assets/scss/frontend/_main.scss",
    "content": "\n\n\n\n.llms-membership-image {\n  display: block;\n  margin: 0 auto;\n}\n\n\n\n.llms-course-image {\n  display: block;\n  margin: 0 auto;\n  max-width: 100%;\n}\n.llms-featured-image {\n  display: block;\n  margin: 0 auto;\n}\n.llms-image-thumb {\n  width: 150px;\n}\n\n// Responsive Videos.\n.llms-video-wrapper {\n\n\t.center-video {\n\t\theight: auto;\n\t\tmax-width: 100%;\n\t\toverflow: hidden;\n\t\tposition: relative;\n\t\tpadding-top: 56.25%;\n\t\ttext-align: center;\n\n\t\t& > .wp-video,\n\t\t.fluid-width-video-wrapper,\n\t\tiframe, object, embed {\n\t\t\theight: 100%;\n\t\t\tleft: 0;\n\t\t\tposition: absolute;\n\t\t\ttop: 0;\n\t\t\twidth: 100%;\n\t\t}\n\n\t\t& > .wp-video {\n\t\t\twidth: 100% !important;\n\t\t}\n\t\t.fluid-width-video-wrapper {\n\t\t\tpadding-top: 0 !important;\n\t\t}\n\t}\n\n}\n\n\n\n\n\n\n\n\n\n\n\n.clear {\n  clear: both;\n  width: 100%;\n}\n.llms-featured-image {\n  text-align: center;\n}\n\n#main-content .llms-payment-options p {\n  margin: 0;\n  font-size: 16px;\n}\n\n.llms-option {\n  display: block;\n  position: relative;\n  margin: 20px 0;\n  padding-left:40px;\n  font-size: 16px;\n\n  label {\n    cursor: pointer;\n    position: static;\n  }\n}\n.llms-option:first-child {\n  margin-top:0;\n}\n.llms-option:last-child {\n  margin-bottom:0;\n}\n#main-content .llms-option:last-child {\n  margin-bottom:0;\n}\n\n.llms-option input[type=\"radio\"] {\n  display: block;\n  position: absolute;\n  top:3px;\n  left:0;\n  z-index: 0;\n}\n\n.llms-option input[type=\"radio\"] {\n  display: inline-block;\n}\n.llms-option input[type=\"radio\"] + label span.llms-radio {\n  display: none;\n}\n\n.llms-option input[type=\"radio\"] + label span.llms-radio {\n  appearance: none;\n\n  z-index: 20;\n  position: absolute;\n  top: 0;\n  left: -2px;\n  display: inline-block;\n  width: 24px;\n  height: 24px;\n  border-radius: 50%;\n  cursor: pointer;\n  vertical-align: middle;\n  box-shadow: hsla(0,0%,100%,.15) 0 1px 1px, inset hsla(0,0%,0%,.5) 0 0 0 1px;\n\n  background: #efefef;\n  background-image: radial-gradient(ellipse at center,  $color-red 0%,$color-red 40%,#efefef 45%);\n  background-repeat: no-repeat;\n\n  transition: background-position .15s cubic-bezier(.8, 0, 1, 1);\n}\n.llms-option input[type=\"radio\"]:checked + label span.llms-radio {\n  transition: background-position .2s .15s cubic-bezier(0, 0, .2, 1);\n}\n\n.llms-option input[type=\"radio\"] + label span.llms-radio {\n  background-position: -24px 0;\n}\n.llms-option input[type=\"radio\"]:checked + label span.llms-radio {\n  background-position: 0 0;\n}\n\n.llms-option input[type=\"submit\"] {\n  border:none;\n  background:$color-red;\n  color:#fff;\n  font-size:20px;\n  padding:10px 0;\n  border-radius:3px;\n  cursor:pointer;\n  width:100%;\n}\n.llms-styled-text {\n  padding: 14px 0;\n}\n.llms-notice-box {\n  border-radius: 3px;\n  z-index: 10;\n  margin: 10px 0;\n  padding: 15px 20px;\n  //background: #fffef4;\n  border: 1px solid #ccc;\n  list-style-type: none;\n  width: 100%;\n  overflow: auto;\n  input[type=\"text\"] {\n    height: auto;\n  }\n  .col-1-1 {\n    width: 100%;\n    input[type=text] {\n      width: 100%;\n    }\n  }\n  .col-1-2 {\n    width: 50%;\n    float: left;\n    @media screen and (max-width: $break-medium) {\n      width: 100%;\n    }\n  }\n  .col-1-3 {\n    width: 33%;\n    float: left;\n    margin-right: 10px;\n  }\n  .col-1-4 {\n    width: 25%;\n    float: left;\n    margin-right: 10px;\n    @media screen and (max-width: $break-medium) {\n      width: 100%;\n    }\n  }\n  .col-1-6 {\n    width: 16.6%;\n    float: left;\n    margin-right: 10px;\n  }\n  .col-1-8 {\n    width: 11%;\n    float: right;\n  }\n  .llms-pad-right {\n    padding-right: 10px;\n    @media screen and (max-width: $break-medium) {\n      padding-right: 0;\n    }\n  }\n}\ninput[type=\"text\"].cc_cvv,\n#cc_cvv {\n  margin-right: 0;\n  width: 23%;\n  float: right;\n}\n.llms-clear-box {\n  border-radius: 3px;\n  z-index: 10;\n  margin: 10px 0;\n  padding: 15px 20px;\n  list-style-type: none;\n  width: 100%;\n  overflow: auto;\n}\n.llms-price-label {\n  font-weight: normal;\n}\n.llms-final-price {\n  font-weight: bold;\n  float: right;\n}\n.llms-center-content {\n  text-align: center;\n}\n.llms-input-text {\n  background-color: #fff;\n  border: 1px solid #ddd;\n  color: #333;\n  font-size: 18px;\n  font-weight: 300;\n  padding: 16px;\n  width: 100%;\n}\n.llms-price {\n  margin-bottom: 0;\n  font-weight: bold;\n}\n.llms-price-loop {\n  margin-bottom: 0;\n  font-weight: bold;\n}\n\n// hentry overrides\n.courses .entry {\n  padding: 0\n}\n.list-view .site-content .llms-course-list .hentry, .list-view .site-content .llms-membership-list .hentry {\n  border-top: 0;\n  padding-top: 0;\n}\n.llms-content {\n  width: 100%;\n}\n\n.llms-lesson-button-wrapper {\n  width: 100%;\n  display: block;\n  clear: both;\n  text-align: center;\n}\n.llms-template-wrapper {\n  width: 100%;\n  display: block;\n  clear: both;\n}\n.llms-button-wrapper {\n  width: 100%;\n  display: block;\n  clear: both;\n  text-align: center;\n}\n\n\n//custom select box\n.llms-styled-select {\n  border: 1px solid #ccc;\n  box-sizing: border-box;\n  border-radius: 3px;\n  overflow: hidden;\n  position: relative;\n}\n.llms-styled-select, .llms-styled-select select {\n  width: 100%;\n}\nselect:focus { outline: none; }\n.llms-styled-select select {\n  height: 34px;\n  padding: 5px 0 5px 5px;\n  background: transparent;\n  border: none;\n  -webkit-appearance: none;\n  font-size: 16px;\n  color: #444444;\n}\n\n@-moz-document url-prefix(){\n  .--ms-styled-select select { width: 110%; }\n}\n\n.llms-styled-select .fa-sort-desc {\n  position: absolute;\n  top: 0;\n  right: 12px;\n  font-size: 24px;\n  color: #ccc;\n}\n\nselect::-ms-expand { display: none; }\n\n_:-o-prefocus, .selector {\n  .llms-styled-select { background: none; }\n}\n\n.llms-form-item-wrapper {\n  margin-bottom: 1em;\n}\n\n/* Circle Graph */\n.llms-progress-circle {\n  position: relative;\n  width: 200px;\n  height: 200px;\n  float: left;\n}\n\n.llms-progress-circle-count {\n  top: 27%;\n  position: absolute;\n  width: 94%;\n  text-align: center;\n  color: #666;\n  font-size:44px;\n}\n.llms-progress-circle svg {\n  position: absolute;\n  width: 200px;\n  height: 200px;\n}\n.llms-progress-circle circle {\n  fill: transparent;\n}\nsvg .llms-background-circle {\n  fill: transparent;\n  stroke-width: 10px;\n  stroke: #f1f2f1;\n  stroke-dasharray: 430;\n}\n\nsvg .llms-animated-circle {\n  fill: transparent;\n  stroke-width: 10px;\n  stroke: #e5554e;\n  stroke-dasharray: 430;\n  stroke-dashoffset: 430 - 20\n}\n\n\n\n\n\n\n\n.llms-widget-syllabus {\n\n  .llms-lesson.current-lesson .lesson-title {\n  \tfont-weight: 700;\n  }\n\n  .section-header {\n    display: flex;\n    gap: 5px;\n\n    .llms-collapse-caret {\n      text-align: left;\n      width: 14px;\n    }\n  }\n\n  .llms-lesson-complete, .lesson-complete-placeholder {\n    font-size: 18px;\n    margin-right: 5px;\n    color: #ccc;\n    &.done {\n      color: $color-brand-blue;\n    }\n  }.section-title {\n     font-weight: bold;\n   }.lesson-title {\n      a {\n        text-decoration: underline;\n      }\n      &.done {\n        a {\n          color: $color-darkgrey;\n          text-decoration: line-through;\n        }\n      }\n    }\n  ul {\n    list-style-type: none;\n    margin: 0;\n    padding: 0;\n    li {\n      list-style-type: none;\n      margin: 0 0 10px 0;\n      ul {\n        margin: 0 0 15px 0;\n        li {\n          align-items: flex-start;\n          display: flex;\n          margin: 0 0 10px 0;\n          padding: 0;\n        }\n      }\n    }\n  }\n}\n\n\n\n.llms-remove-coupon {\n  float: right;\n}\n\n\n\n\n\n/*.llms-lesson-link-locked, .llms-lesson-link-locked:hover   {\n  background: #f1f1f1;\n  display: block;\n  color: #a6a6a6;\n  min-height: 85px;\n  padding: 15px;\n  text-decoration: none;\n  position: relative;\n}*/\n\n.llms-lesson-preview.is-complete .llms-lesson-link-locked {\n  padding-left: 75px;\n}\n\n.llms-lesson-tooltip { \tdisplay: none;\n\t\t\t\tposition:absolute;\n\t\t\t\tcolor: #000000;\n\t\t\t\tbackground-color: #c0c0c0;\n\t\t\t\tpadding:.25em;\n\t\t\t\tborder-radius: 2px;\n\t\t\t\tz-index: 100;\n \t\t\t\ttop:0;\n    \t\t\tleft:50%;\n    \t\t\ttext-align: center;\n    \t\t\t-webkit-transform: translateX(-50%) translateY(-100%);\n    \t\t\t        transform: translateX(-50%) translateY(-100%);\n\t\t\t}\n\n/* arrows - :after */\n.llms-lesson-tooltip:after {\n    content: \"\";\n    width: 0;\n    height: 0;\n    border-top: 8px solid #c0c0c0;\n    border-left: 8px solid transparent;\n    border-right: 8px solid transparent;\n    position:absolute;\n    bottom:-8px;\n    left:50%;\n    transform: translateX(-50%);\n}\n\n.llms-lesson-tooltip.active {\n  display: inline-block;\n}\n\n// Favorites.\n.llms-favorite-wrapper {\n  cursor: pointer;\n\n  .fa-heart {\n    color: #EF476F;\n  }\n\n  button {\n\tbackground: none;\n\tborder: none;\n\tcursor: pointer;\n\tpadding: 0;\n  }\n\n  button:focus {\n\toutline: 2px dashed #333;\n\toutline-offset: 2px;\n  }\n}\n\n.llms-has-favorite .llms-parent-course-link {\n  display: inline-block;\n  margin-bottom: 20px;\n\n  + .llms-favorite-wrapper {\n    float: right;\n    margin: 0;\n  }\n}\n\n.llms-syllabus-wrapper .llms-has-favorite {\n  text-align: left;\n\n  .llms-favorite-wrapper {\n    display: inline-block;\n  }\n\n}\n"
  },
  {
    "path": "assets/scss/frontend/_notices.scss",
    "content": ".llms-notice {\n\tbackground: rgba( $color-brand-blue, .3 );\n\tborder-color: $color-brand-blue;\n\tborder-style: solid;\n\tborder-width: 1px;\n\tborder-radius: $radius-small;\n\tpadding: 15px;\n\tmargin-bottom: 40px;\n\n\ta {\n\t\tcolor: inherit;\n\t}\n\n\tp, ul {\n\t\t&:last-child { margin-bottom: 0; }\n\t}\n\n\tli {\n\t\tlist-style-type: none;\n\t}\n\n\t&.llms-debug {\n\t\tbackground: rgba( #cacaca, .3 );\n\t\tborder-color: #cacaca;\n\t}\n\n\t&.llms-error {\n\t\tbackground: rgba( $color-red, .15 );\n\t\tborder-color: $color-red;\n\t}\n\n\t&.llms-success {\n\t\tbackground: rgba( $color-green, .15 );\n\t\tborder-color: $color-green;\n\t}\n\n}\n\n// this helps genesis and numerous other themes out a bit\n// by being slightly more specific\n.entry-content .llms-notice {\n\tmargin: 0 0 40px;\n\tli {\n\t\tlist-style-type: none;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_reviews.scss",
    "content": ".llms_review {\n\tmargin: 20px 0px;\n\tpadding: 10px;\n\n\th5 {\n\t\tfont-size: 17px;\n\t\tmargin: 3px 0px;\n\t}\n\n\th6 {\n\t\tfont-size: 13px;\n\t}\n\n\tp {\n\t\tfont-size: 15px;\n\t}\n}\n\n.review_box {\n\n\t[type=text] {\n\t\tmargin: 10px 0px\n\t}\n\n\t.review_error {\n\t\tcolor: red;\n\t\tdisplay: none;\n\t}\n\n\t+ .thank_you_box {\n\t\tdisplay: none;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_student-dashboard.scss",
    "content": ".llms-student-dashboard {\n\n\t.llms-sd-nav select {\n\n\t\tdisplay: none;\n\n\t\t@media all and ( max-width: 600px ) {\n\t\t\tdisplay: block;\n\t\t\tmargin-bottom: 30px;\n\t\t\twidth: 100%;\n\t\t}\n\t}\n\n\t.llms-sd-title {\n\t\talign-items: center;\n\t\tdisplay: flex;\n\t\tgap: 15px;\n\t\tmargin: 40px 0 20px 0;\n\n\t\tsmall {\n\t\t\tfont-size: 18px;\n\n\t\t\t+ a {\n\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-sd-items { // ul\n\t\tlist-style-type: none;\n\t\tmargin: 0 0 25px 0;\n\t\tpadding: 0;\n\t}\n\t\t.llms-sd-item { // li\n\t\t\tdisplay: inline-block;\n\t\t\tlist-style-type: none;\n\t\t\tmargin: 5px 0;\n\t\t\tpadding: 0;\n\n\t\t&:last-child {\n\t\t\t.llms-sep {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\t\t}\n\n\t\t\t.llms-sep {\n\t\t\t\tcolor: $color-darkgrey;\n\t\t\t\tmargin: 0 5px;\n\t\t\t}\n\t\t}\n\n\t.llms-sd-section {\n\t\th2 {\n\t\t\tmargin: 0 0 15px 0;\n\t\t}\n\n\t\t.llms-sd-section-title {\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tmargin-bottom: 40px;\n\n\t\t.llms-sd-section-title:first-child {\n\t\t\tmargin: 0 0 10px 0;\n\t\t}\n\n\t\t.llms-sd-section-footer {\n\t\t\tmargin-top: 10px;\n\n\t\t\ta.llms-button-secondary {\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\t\t}\n\n\t\t.llms-certificates-loop {\n\t\t\tmargin: 0 0 0 -10px;\n\t\t}\n\n\t\t.llms-certificate {\n\t\t\tpadding: 20px 0;\n\t\t}\n\t}\n\n\t.orders-table {\n\t\tborder: 1px solid $color-border;\n\t\tborder-spacing: 0;\n\t\twidth: 100%;\n\t\tmargin: 10px 0;\n\n\t\tthead {\n\t\t\tdisplay: none;\n\t\t\tth,td {\n\t\t\t\tfont-weight: 700;\n\t\t\t}\n\t\t\t@media all and ( min-width: 600px ) {\n\t\t\t\tdisplay: table-header-group;\n\t\t\t}\n\t\t}\n\n\t\ttbody {\n\t\t\ttr:nth-child( odd ) {\n\t\t\t\ttd, th {\n\t\t\t\t\tbackground: #f9f9f9;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttr:last-child {\n\t\t\t\tborder-bottom-width: 0;\n\t\t\t}\n\t\t}\n\n\t\ttfoot {\n\t\t\ttr {\n\t\t\t\tbackground: #f9f9f9;\n\t\t\t}\n\t\t\tth, td {\n\t\t\t\tpadding: 10px;\n\t\t\t\ttext-align: right;\n\t\t\t\t&:last-child { border-bottom-width: 0; }\n\t\t\t}\n\t\t}\n\n\t\tth {\n\t\t\tfont-weight: 700;\n\t\t}\n\n\t\tth, td {\n\t\t\tborder-color: $color-border;\n\t\t\tborder-style: solid;\n\t\t\tborder-width: 0;\n\t\t\tdisplay: block;\n\t\t\tpadding: 15px 20px;\n\t\t\ttext-align: center;\n\n\t\t\t.llms-button-primary {\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\n\t\t\t&:last-child {\n\t\t\t\tborder-bottom-width: 1px;\n\t\t\t}\n\n\t\t\t&:before {\n\t\t\t\tcontent: attr( data-label );\n\t\t\t}\n\n\t\t\t@media all and ( min-width: 600px ) {\n\t\t\t\tborder-bottom-width: 1px;\n\t\t\t\tdisplay: table-cell;\n\t\t\t\ttext-align: left;\n\t\t\t\t&:first-child { width: 220px; }\n\t\t\t\t&:before { display: none; }\n\t\t\t}\n\n\t\t}\n\n\t\t@media all and ( min-width: 600px ) {\n\t\t\t&.transactions th:first-child {width: auto; }\n\t\t}\n\n\t}\n\n\t@include order_status_badges();\n\n\t.order-title {\n\t\t.llms-status {\n\t\t\tfont-size: 18px;\n\t\t}\n\t}\n\n\t.llms-person-form-wrapper {\n\t\t.llms-change-password { display: none; }\n\t}\n\n\t.order-primary, .order-secondary {\n\t\twidth: 100%;\n\t}\n\n\t.order-secondary {\n\t\t.llms-form-field {\n\t\t\tpadding: 10px 0;\n\t\t}\n\t}\n\n\t.llms-switch-payment-source {\n\t\t.llms-notice,\n\t\t.entry-content .llms-notice {\n\t\t\tmargin-left: 10px;\n\t\t\tmargin-right: 10px;\n\t\t}\n\t}\n\n\t.llms-switch-payment-source-main {\n\t\tborder: none;\n\t\tdisplay: none;\n\t\tmargin: 0;\n\t\tul.llms-payment-gateways {\n\t\t\tpadding: 10px 15px 0;\n\t\t\tmargin: 0;\n\t\t}\n\t\t.llms-payment-method,\n\t\tul.llms-order-summary {\n\t\t\tpadding: 0 25px 10px;\n\t\t\tmargin: 0;\n\t\t\tlist-style-type: none;\n\t\t\tli { list-style-type: none; }\n\t\t}\n\t}\n\n\t.llms-featured-pricing {\n\t\tdisplay: none;\n\t}\n\n\t/**\n\t * Dashboard Home\n\t */\n\t&.dashboard {\n\t\t.llms-sd-section {\n\t\t\tborder: 1px solid $color-border;\n\t\t\tborder-radius: $radius-small;\n\t\t\tpadding: 20px;\n\t\t\tmargin-bottom: 40px;\n\t\t}\n\t}\n\t.llms-loop-list {\n\t\tmargin: 20px 0;\n\t}\n\n}\n\n.logged-in {\n\t.llms-sd-layout-columns {\n\t\talign-items: start;\n\n\t\t@media all and ( min-width: 600px ) {\n\t\t\tdisplay: grid;\n\t\t\tgrid-gap: 25px;\n\t\t\tgrid-template-areas: \"nav header\" \"nav tab\";\n\t\t\tgrid-template-columns: 200px 1fr;\n\t\t\tgrid-template-rows: auto 1fr;\n\n\t\t\t&:has(> .llms-notice) {\n\t\t\t\tgrid-template-areas: \"nav header\" \"nav notice\" \"nav tab\";\n\t\t\t\tgrid-template-rows: auto auto 1fr;\n\t\t\t}\n\t\t}\n\n\t\t.llms-notice {\n\t\t\tgrid-area: notice;\n\t\t}\n\n\t\t.llms-sd-nav {\n\t\t\tgrid-area: nav;\n\n\t\t\t.llms-sd-items {\n\n\t\t\t\tdisplay: none;\n\n\t\t\t\t@media all and ( min-width: 600px ) {\n\t\t\t\t\tdisplay: block;\n\n\t\t\t\t\t.llms-sd-item {\n\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t\tfloat: none;\n\n\t\t\t\t\t\ta {\n\t\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t\t\tpadding: 10px 0;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t.llms-sep {\n\t\t\t\t\t\t\tdisplay: none;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t.llms-sd-header {\n\t\t\tgrid-area: header;\n\t\t}\n\n\t\t.llms-sd-tab {\n\t\t\tgrid-area: tab;\n\t\t}\n\n\t}\n\n}\n\n// My Grades course list\n.llms-sd-grades {\n\t.llms-table {\n\t\t.llms-progress {\n\t\t\tdisplay: block;\n\t\t\tmargin: 0;\n\t\t\t.llms-progress-bar {\n\t\t\t\ttop: 0;\n\t\t\t\theight: 1.4em;\n\t\t\t}\n\t\t\t.progress__indicator {\n\t\t\t\tfont-size: 1em;\n\t\t\t\tposition: relative;\n\t\t\t\tright: 0.4em;\n\t\t\t\ttop: 0.2em;\n\t\t\t\tz-index: 1;\n\t\t\t}\n\t\t}\n\t}\n}\n\n// grades table for a single course\n.llms-table.llms-single-course-grades {\n\n\ttbody {\n\t\ttr:first-child td, tr:first-child th {\n\t\t\tbackground-color: #eaeaea;\n\t\t}\n\t}\n\n\tth {\n\t\tfont-weight: 400;\n\t\ttext-align: left;\n\t}\n\n\ttd {\n\t\t.llms-donut {\n\t\t\tdisplay: inline-block;\n\t\t\tvertical-align: middle;\n\t\t}\n\t\t.llms-status {\n\t\t\tmargin-right: 4px;\n\t\t}\n\t\t.llms-donut + .llms-status {\n\t\t\tmargin-left: 4px;\n\t\t}\n\t}\n\n\tth.llms-section_title {\n\t\tfont-size: 110%;\n\t\tfont-weight: 700;\n\t}\n\n\ttd.llms-lesson_title {\n\t\tmax-width: 40%;\n\t}\n\ttd.llms-associated_quiz {\n\t\t.llms-donut {\n\t\t\tdisplay: inline-block;\n\t\t\tmargin-right: 5px;\n\t\t\tvertical-align: middle;\n\t\t}\n\t}\n\ttd.llms-lesson_title {\n\t\ta[href=\"#\"] {\n\t\t\tpointer-events: none;\n\t\t}\n\t\ta[href^=\"#\"] {\n\t\t\tcolor: inherit;\n\t\t\tposition: relative;\n\t\t\t.llms-tooltip {\n\t\t\t\tmax-width: 380px;\n\t\t\t\twidth: 380px;\n\t\t\t\t&.show {\n\t\t\t\t\ttop: -54px;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n.llms-sd-widgets {\n\tdisplay: flex;\n\n\t.llms-sd-widget {\n\t\tbackground: $color-white;\n\t\tborder: 1px solid $color-border;\n\t\tborder-bottom-right-radius: $radius-small;\n\t\tborder-bottom-left-radius: $radius-small;\n\t\tflex: 1;\n\t\tmargin: 10px 10px 20px;\n\t\tpadding: 0 0 20px;\n\t\t&:first-child {\n\t\t\tmargin-left: 0;\n\t\t}\n\t\t&:last-child {\n\t\t\tmargin-right: 0;\n\t\t}\n\n\t\t.llms-sd-widget-title {\n\t\t\tbackground: $color-brand-blue;\n\t\t\tcolor: #fff;\n\t\t\tfont-size: 18px;\n\t\t\tline-height: 1;\n\t\t\tmargin: 0 0 20px;\n\t\t\tpadding: 10px;\n\t\t}\n\n\t\t.llms-sd-widget-empty {\n\t\t\tfont-size: 14px;\n\t\t\tfont-style: italic;\n\t\t\topacity: 0.5;\n\t\t\ttext-align: center;\n\t\t}\n\n\t\t.llms-donut {\n\t\t\tmargin: 0 auto;\n\t\t}\n\n\t\t.llms-sd-date {\n\t\t\topacity: 0.8;\n\t\t\ttext-align: center;\n\t\t\tfont-size: 22px;\n\t\t\tline-height: 1.1;\n\t\t\tspan {\n\t\t\t\tdisplay: block;\n\t\t\t\t&.day {\n\t\t\t\t\tfont-size: 52px;\n\t\t\t\t}\n\t\t\t\t&.diff {\n\t\t\t\t\tfont-size: 12px;\n\t\t\t\t\tfont-style: italic;\n\t\t\t\t\tmargin-top: 8px;\n\t\t\t\t\topacity: 0.75;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t.llms-achievement {\n\t\t\tbackground: transparent;\n\t\t\tmargin: 0 auto;\n\t\t\tmax-width: 120px;\n\t\t\t.llms-achievement-title {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\t\t}\n\n\t}\n\n\n}\n\n.redeem-voucher {\n\t.form-row {\n\t\tlabel {\n\t\t\tdisplay: block;\n\t\t\tfont-weight: 700;\n\t\t}\n\t\tinput[type=\"text\"] {\n\t\t\tbackground-color: $color-white;\n\t\t\tbackground-clip: padding-box;\n\t\t\tborder: 1px solid $color-grey;\n\t\t\tborder-radius: $radius-small;\n\t\t\tbox-sizing: border-box;\n\t\t\tfont-size: 16px;\n\t\t\tline-height: 1;\n\t\t\tpadding: 8px 12px;\n\t\t\ttransition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n\t\t}\n\t}\n}\n\n\n.llms-sd-pagination {\n\tmargin-top: 24px;\n\t@include clearfix;\n\t.llms-button-secondary {\n\t\tdisplay: inline-block;\n\t\t&.prev { float: left; }\n\t\t&.next { float: right; }\n\t}\n}\n\n\n.llms-sd-notification-center {\n\tbackground: $color-white;\n\tborder: 1px solid $color-border;\n\tborder-radius: $radius-small;\n\tpadding: 20px;\n\n\t.llms-notification-list-item {\n\t\t.llms-notification {\n\t\t\tz-index: 1;\n\t\t\t&:hover {\n\t\t\t\tbackground-color: inherit;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_syllabus.scss",
    "content": ".llms-syllabus-wrapper {\n\n\tmargin: 20px 0;\n\ttext-align: left;\n\n\t.llms-section-title {\n\t\tmargin: 40px 0 10px;\n\n\t\t+ .llms-lesson-preview {\n\t\t\tborder-top: 1px solid $color-border;\n\t\t}\n\t}\n\n}\n\n.llms-lesson-preview {\n\tborder-right: 1px solid $color-border;\n\tborder-bottom: 1px solid $color-border;\n\tborder-left: 1px solid $color-border;\n\tdisplay: block;\n\tmargin: 0;\n\tmax-width: 100%;\n\tposition: relative;\n\n\tsection {\n\t\tbackground: $color-white;\n\t}\n\n\t&.current-lesson {\n\t\t.llms-lesson-title {\n\t\t\tfont-weight: 400;\n\t\t}\n\t}\n\n\t.llms-lesson-link {\n\t\tcolor: $color-black;\n\t\tdisplay: block;\n\t\tpadding: 15px;\n\t\ttext-decoration: none;\n\t\ttransition: background-color 0.3s ease;\n\n\t\t&:hover {\n\t\t\tbackground: $el-background-hover;\n\t\t}\n\n\t\t&:visited {\n\t\t\tcolor: #212121;\n\t\t}\n\n\t\t.llms-lesson-preview-row {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row-reverse;\n\t\t}\n\t}\n\n\t.llms-lesson-locked {\n\t\tbackground: #EFEFEF;\n\t\tcolor: $color-darkgrey;\n\n\t\t.llms-lesson-link {\n\t\t\tcolor: $color-darkgrey;\n\t\t}\n\n\t\t&:hover {\n\t\t\tbackground: $color-border;\n\t\t}\n\t}\n\n\t.llms-lesson-thumbnail {\n\t\tmargin-bottom: 15px;\n\t\timg {\n\t\t\tdisplay: block;\n\t\t\theight: auto;\n\t\t\twidth: 100%;\n\t\t}\n\t}\n\n\t.llms-lesson-title {\n\t\tfont-weight: 700;\n\t\tmargin: 0 auto 5px;\n\t\t&:last-child {\n\t\t\tmargin-bottom: 0;\n\t\t}\n\t}\n\n\t.llms-lesson-excerpt {\n\t\tmargin-top: 05px;\n\n\t\tp {\n\t\t\tmargin: 0 0 10px 0;\n\t\t\tpadding: 0;\n\t\t\t&:last-of-type {\n\t\t\t\tmargin: 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-lesson-meta {\n\t\tdisplay: flex;\n\t\tpadding: 5px 15px;\n\t\tfont-size: 14px;\n\n\t\tdiv.llms-favorite-wrapper + span.llms-lesson-has-quiz {\n\t\t\tmargin-left: 10px;\n\t\t}\n\n\t\tdiv.llms-favorite-wrapper + span.llms-lesson-has-assignment {\n\t\t\tmargin-left: 10px;\n\t\t}\n\n\t\tspan.llms-lesson-has-quiz + span.llms-lesson-has-assignment {\n\t\t\tmargin-left: 10px;\n\t\t}\n\t}\n\n\t.llms-main {\n\t    flex-grow: 1;\n\t}\n\t.llms-extra {\n\t\tmin-width: 50px;\n\t\ttext-align: right;\n\t}\n\n\t.llms-lesson-counter,\n\t.llms-free-lesson-svg,\n\t.llms-lesson-complete,\n\t.llms-lesson-complete-placeholder {\n\t\tdisplay: block;\n\t}\n\n\t&.is-free,\n\t&.is-complete {\n\t\t.llms-lesson-complete {\n\t\t\tcolor: $color-brand-blue;\n\t\t}\n\t}\n\n\t&.is-complete {\n\t\t.llms-lesson-title:not(.llms-student-dashboard .llms-lesson-title):not(.llms-course-navigation .llms-lesson-title) {\n\t\t\ttext-decoration: line-through;\n\t\t}\n\t}\n\n\t.llms-icon-free {\n\t\tbackground: $color-brand-blue;\n\t\tborder-radius: $radius-small;\n\t\tcolor: $color-white;\n\t\tdisplay: inline-block;\n\t\tfont-size: 14px;\n\t\tfont-weight: bold;\n\t\tline-height: 1;\n\t\tpadding: 5px 8px 4px;\n\t\twhite-space: nowrap;\n\t}\n\n\t&.is-incomplete {\n\t\t.llms-lesson-complete {\n\t\t\tcolor: #cacaca;\n\t\t}\n\t}\n\n\t.llms-lesson-counter {\n\t\tfont-size: 16px;\n\t\tmargin-bottom: 5px;\n\t}\n\n\t.llms-free-lesson-svg {\n\t\tfill: currentColor;\n\t\theight: 23px;\n\t\twidth: 50px;\n\t}\n\n}\n\n.llms-course-navigation {\n\tdisplay: flex;\n\tgap: 20px;\n\t.llms-prev-lesson,\n\t.llms-next-lesson,\n\t.llms-back-to-course {\n\t\tflex: 1;\n\t}\n\n\t.llms-prev-lesson {\n\t\t& + .llms-back-to-course {\n\t\t\ttext-align: right;\n\t\t}\n\t}\n\n\t.llms-next-lesson {\n\t\t.llms-lesson-preview {\n\t\t\t.llms-lesson-link {\n\t\t\t\ttext-align: right;\n\t\t\t}\n\t\t}\n\t}\n\n\t.llms-lesson-preview {\n\t\tborder: 1px solid $color-border;\n\t}\n\n\t.llms-lesson-link {\n\t\tdisplay: block;\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_tooltip.scss",
    "content": ".llms-tooltip {\n\n\tbackground: #2a2a2a;\n\tborder-radius: 4px;\n\tcolor: #fff;\n\tfont-size: 14px;\n\tline-height: 1.2;\n\topacity: 0;\n\ttop: -20px;\n\tpadding: 8px 12px;\n\tleft: 50%;\n\tposition: absolute;\n\tpointer-events: none;\n\ttransform: translateX( -50% );\n\ttransition: opacity .2s ease, top .2s ease;\n\tmax-width: 320px;\n\n\t&.show {\n\t\ttop: -28px;\n\t\topacity: 1;\n\t}\n\n\t&:after {\n\n\t\tbottom: -8px;\n\t\tborder-top: 8px solid #2a2a2a;\n\t\tborder-left: 8px solid transparent;\n\t\tborder-right: 8px solid transparent;\n\t\tcontent: '';\n\t\theight: 0;\n\t\tleft: 50%;\n\t\tposition: absolute;\n\t\ttransform: translateX( -50% );\n\t\twidth: 0;\n\n\t}\n\n}\n\n\n\n.webui-popover-title {\n\tfont-size: initial;\n\tfont-weight: initial;\n\tline-height: initial;\n}\n.webui-popover-inverse {\n\t.webui-popover-inner .close {\n\t\tcolor: #fff;\n\t\topacity: 0.6;\n\t\ttext-shadow: none;\n\t\t&:hover {\n\t\t\topacity: 0.8;\n\t\t}\n\t}\n\t.webui-popover-content a {\n\t\tcolor: #fff;\n\t\ttext-decoration: underline;\n\t\t&:hover {\n\t\t\ttext-decoration: none;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/scss/frontend/_voucher.scss",
    "content": ".voucher-expand {\n\tdisplay: none;\n}"
  },
  {
    "path": "assets/scss/lifterlms.scss",
    "content": "//\n// Main Frontend CSS File\n//\n\n@import \"_includes/vars\";\n@import \"_includes/extends\";\n@import \"_includes/grid\";\n@import \"_includes/mixins\";\n@import \"_includes/buttons\";\n@import \"_includes/llms-donut\";\n@import \"_includes/tooltip\";\n\n@import \"frontend/main\";\n@import \"frontend/loop\";\n@import \"frontend/course\";\n@import \"frontend/syllabus\";\n@import \"frontend/llms-progress\";\n@import \"frontend/llms-author\";\n@import \"frontend/reviews\";\n\n@import \"frontend/notices\";\n@import \"frontend/llms-achievements-certs\";\n@import \"frontend/llms-notifications\";\n@import \"frontend/llms-pagination\";\n@import \"frontend/tooltip\";\n\n\n@import \"frontend/llms-quizzes\";\n\n@import \"frontend/voucher\";\n@import \"frontend/llms-access-plans\";\n@import \"frontend/checkout\";\n@import \"_includes/llms-form-field\";\n\n@import \"frontend/llms-outline-collapse\";\n\n@import \"frontend/student-dashboard\";\n@import \"frontend/llms-table\";\n\n@import \"_includes/vendor/_font-awesome\";\n"
  },
  {
    "path": "assets/scss/llms-focus-mode.scss",
    "content": "@import \"frontend/focus-mode\";\n"
  },
  {
    "path": "assets/vendor/a11y-dialog/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2024 Kitty Giraudel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "assets/vendor/datetimepicker/jquery.datetimepicker.full.js",
    "content": "/*!\n * @copyright Copyright &copy; Kartik Visweswaran, Krajee.com, 2014 - 2016\n * @version 1.3.4\n *\n * Date formatter utility library that allows formatting date/time variables or Date objects using PHP DateTime format.\n * @see http://php.net/manual/en/function.date.php\n *\n * For more JQuery plugins visit http://plugins.krajee.com\n * For more Yii related demos visit http://demos.krajee.com\n */var DateFormatter;!function(){\"use strict\";var t,e,r,n,a,u,i;u=864e5,i=3600,t=function(t,e){return\"string\"==typeof t&&\"string\"==typeof e&&t.toLowerCase()===e.toLowerCase()},e=function(t,r,n){var a=n||\"0\",u=t.toString();return u.length<r?e(a+u,r):u},r=function(t){var e,n;for(t=t||{},e=1;e<arguments.length;e++)if(n=arguments[e])for(var a in n)n.hasOwnProperty(a)&&(\"object\"==typeof n[a]?r(t[a],n[a]):t[a]=n[a]);return t},n=function(t,e){for(var r=0;r<e.length;r++)if(e[r].toLowerCase()===t.toLowerCase())return r;return-1},a={dateSettings:{days:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],daysShort:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],months:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],monthsShort:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],meridiem:[\"AM\",\"PM\"],ordinal:function(t){var e=t%10,r={1:\"st\",2:\"nd\",3:\"rd\"};return 1!==Math.floor(t%100/10)&&r[e]?r[e]:\"th\"}},separators:/[ \\-+\\/\\.T:@]/g,validParts:/[dDjlNSwzWFmMntLoYyaABgGhHisueTIOPZcrU]/g,intParts:/[djwNzmnyYhHgGis]/g,tzParts:/\\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\\d{4})?)\\b/g,tzClip:/[^-+\\dA-Z]/g},DateFormatter=function(t){var e=this,n=r(a,t);e.dateSettings=n.dateSettings,e.separators=n.separators,e.validParts=n.validParts,e.intParts=n.intParts,e.tzParts=n.tzParts,e.tzClip=n.tzClip},DateFormatter.prototype={constructor:DateFormatter,getMonth:function(t){var e,r=this;return e=n(t,r.dateSettings.monthsShort)+1,0===e&&(e=n(t,r.dateSettings.months)+1),e},parseDate:function(e,r){var n,a,u,i,s,o,c,f,l,h,d=this,g=!1,m=!1,p=d.dateSettings,y={date:null,year:null,month:null,day:null,hour:0,min:0,sec:0};if(!e)return null;if(e instanceof Date)return e;if(\"U\"===r)return u=parseInt(e),u?new Date(1e3*u):e;switch(typeof e){case\"number\":return new Date(e);case\"string\":break;default:return null}if(n=r.match(d.validParts),!n||0===n.length)throw new Error(\"Invalid date format definition.\");for(a=e.replace(d.separators,\"\\x00\").split(\"\\x00\"),u=0;u<a.length;u++)switch(i=a[u],s=parseInt(i),n[u]){case\"y\":case\"Y\":if(!s)return null;l=i.length,y.year=2===l?parseInt((70>s?\"20\":\"19\")+i):s,g=!0;break;case\"m\":case\"n\":case\"M\":case\"F\":if(isNaN(s)){if(o=d.getMonth(i),!(o>0))return null;y.month=o}else{if(!(s>=1&&12>=s))return null;y.month=s}g=!0;break;case\"d\":case\"j\":if(!(s>=1&&31>=s))return null;y.day=s,g=!0;break;case\"g\":case\"h\":if(c=n.indexOf(\"a\")>-1?n.indexOf(\"a\"):n.indexOf(\"A\")>-1?n.indexOf(\"A\"):-1,h=a[c],c>-1)f=t(h,p.meridiem[0])?0:t(h,p.meridiem[1])?12:-1,s>=1&&12>=s&&f>-1?y.hour=s+f-1:s>=0&&23>=s&&(y.hour=s);else{if(!(s>=0&&23>=s))return null;y.hour=s}m=!0;break;case\"G\":case\"H\":if(!(s>=0&&23>=s))return null;y.hour=s,m=!0;break;case\"i\":if(!(s>=0&&59>=s))return null;y.min=s,m=!0;break;case\"s\":if(!(s>=0&&59>=s))return null;y.sec=s,m=!0}if(g===!0&&y.year&&y.month&&y.day)y.date=new Date(y.year,y.month-1,y.day,y.hour,y.min,y.sec,0);else{if(m!==!0)return null;y.date=new Date(0,0,0,y.hour,y.min,y.sec,0)}return y.date},guessDate:function(t,e){if(\"string\"!=typeof t)return t;var r,n,a,u,i,s,o=this,c=t.replace(o.separators,\"\\x00\").split(\"\\x00\"),f=/^[djmn]/g,l=e.match(o.validParts),h=new Date,d=0;if(!f.test(l[0]))return t;for(a=0;a<c.length;a++){if(d=2,i=c[a],s=parseInt(i.substr(0,2)),isNaN(s))return null;switch(a){case 0:\"m\"===l[0]||\"n\"===l[0]?h.setMonth(s-1):h.setDate(s);break;case 1:\"m\"===l[0]||\"n\"===l[0]?h.setDate(s):h.setMonth(s-1);break;case 2:if(n=h.getFullYear(),r=i.length,d=4>r?r:4,n=parseInt(4>r?n.toString().substr(0,4-r)+i:i.substr(0,4)),!n)return null;h.setFullYear(n);break;case 3:h.setHours(s);break;case 4:h.setMinutes(s);break;case 5:h.setSeconds(s)}u=i.substr(d),u.length>0&&c.splice(a+1,0,u)}return h},parseFormat:function(t,r){var n,a=this,s=a.dateSettings,o=/\\\\?(.?)/gi,c=function(t,e){return n[t]?n[t]():e};return n={d:function(){return e(n.j(),2)},D:function(){return s.daysShort[n.w()]},j:function(){return r.getDate()},l:function(){return s.days[n.w()]},N:function(){return n.w()||7},w:function(){return r.getDay()},z:function(){var t=new Date(n.Y(),n.n()-1,n.j()),e=new Date(n.Y(),0,1);return Math.round((t-e)/u)},W:function(){var t=new Date(n.Y(),n.n()-1,n.j()-n.N()+3),r=new Date(t.getFullYear(),0,4);return e(1+Math.round((t-r)/u/7),2)},F:function(){return s.months[r.getMonth()]},m:function(){return e(n.n(),2)},M:function(){return s.monthsShort[r.getMonth()]},n:function(){return r.getMonth()+1},t:function(){return new Date(n.Y(),n.n(),0).getDate()},L:function(){var t=n.Y();return t%4===0&&t%100!==0||t%400===0?1:0},o:function(){var t=n.n(),e=n.W(),r=n.Y();return r+(12===t&&9>e?1:1===t&&e>9?-1:0)},Y:function(){return r.getFullYear()},y:function(){return n.Y().toString().slice(-2)},a:function(){return n.A().toLowerCase()},A:function(){var t=n.G()<12?0:1;return s.meridiem[t]},B:function(){var t=r.getUTCHours()*i,n=60*r.getUTCMinutes(),a=r.getUTCSeconds();return e(Math.floor((t+n+a+i)/86.4)%1e3,3)},g:function(){return n.G()%12||12},G:function(){return r.getHours()},h:function(){return e(n.g(),2)},H:function(){return e(n.G(),2)},i:function(){return e(r.getMinutes(),2)},s:function(){return e(r.getSeconds(),2)},u:function(){return e(1e3*r.getMilliseconds(),6)},e:function(){var t=/\\((.*)\\)/.exec(String(r))[1];return t||\"Coordinated Universal Time\"},I:function(){var t=new Date(n.Y(),0),e=Date.UTC(n.Y(),0),r=new Date(n.Y(),6),a=Date.UTC(n.Y(),6);return t-e!==r-a?1:0},O:function(){var t=r.getTimezoneOffset(),n=Math.abs(t);return(t>0?\"-\":\"+\")+e(100*Math.floor(n/60)+n%60,4)},P:function(){var t=n.O();return t.substr(0,3)+\":\"+t.substr(3,2)},T:function(){var t=(String(r).match(a.tzParts)||[\"\"]).pop().replace(a.tzClip,\"\");return t||\"UTC\"},Z:function(){return 60*-r.getTimezoneOffset()},c:function(){return\"Y-m-d\\\\TH:i:sP\".replace(o,c)},r:function(){return\"D, d M Y H:i:s O\".replace(o,c)},U:function(){return r.getTime()/1e3||0}},c(t,t)},formatDate:function(t,e){var r,n,a,u,i,s=this,o=\"\",c=\"\\\\\";if(\"string\"==typeof t&&(t=s.parseDate(t,e),!t))return null;if(t instanceof Date){for(a=e.length,r=0;a>r;r++)i=e.charAt(r),\"S\"!==i&&i!==c&&(r>0&&e.charAt(r-1)===c?o+=i:(u=s.parseFormat(i,t),r!==a-1&&s.intParts.test(i)&&\"S\"===e.charAt(r+1)&&(n=parseInt(u)||0,u+=s.dateSettings.ordinal(n)),o+=u));return o}return\"\"}}}();\n/**\n * @preserve jQuery DateTimePicker\n * @homepage http://xdsoft.net/jqplugins/datetimepicker/\n * @author Chupurnov Valeriy (<chupurnov@gmail.com>)\n */\n\n/**\n * @param {jQuery} $\n */\nvar datetimepickerFactory = function ($) {\n\t'use strict';\n\n\tvar default_options  = {\n\t\ti18n: {\n\t\t\tar: { // Arabic\n\t\t\t\tmonths: [\n\t\t\t\t\t\"كانون الثاني\", \"شباط\", \"آذار\", \"نيسان\", \"مايو\", \"حزيران\", \"تموز\", \"آب\", \"أيلول\", \"تشرين الأول\", \"تشرين الثاني\", \"كانون الأول\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"ن\", \"ث\", \"ع\", \"خ\", \"ج\", \"س\", \"ح\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"الأحد\", \"الاثنين\", \"الثلاثاء\", \"الأربعاء\", \"الخميس\", \"الجمعة\", \"السبت\", \"الأحد\"]\n\t\t\t},\n\t\t\tro: { // Romanian\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Ianuarie\", \"Februarie\", \"Martie\", \"Aprilie\", \"Mai\", \"Iunie\", \"Iulie\", \"August\", \"Septembrie\", \"Octombrie\", \"Noiembrie\", \"Decembrie\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Du\", \"Lu\", \"Ma\", \"Mi\", \"Jo\", \"Vi\", \"Sâ\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Duminică\", \"Luni\", \"Marţi\", \"Miercuri\", \"Joi\", \"Vineri\", \"Sâmbătă\"]\n\t\t\t},\n\t\t\tid: { // Indonesian\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januari\", \"Februari\", \"Maret\", \"April\", \"Mei\", \"Juni\", \"Juli\", \"Agustus\", \"September\", \"Oktober\", \"November\", \"Desember\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Min\", \"Sen\", \"Sel\", \"Rab\", \"Kam\", \"Jum\", \"Sab\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Minggu\", \"Senin\", \"Selasa\", \"Rabu\", \"Kamis\", \"Jumat\", \"Sabtu\"]\n\t\t\t},\n\t\t\tis: { // Icelandic\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Janúar\", \"Febrúar\", \"Mars\", \"Apríl\", \"Maí\", \"Júní\", \"Júlí\", \"Ágúst\", \"September\", \"Október\", \"Nóvember\", \"Desember\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Sun\", \"Mán\", \"Þrið\", \"Mið\", \"Fim\", \"Fös\", \"Lau\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Sunnudagur\", \"Mánudagur\", \"Þriðjudagur\", \"Miðvikudagur\", \"Fimmtudagur\", \"Föstudagur\", \"Laugardagur\"]\n\t\t\t},\n\t\t\tbg: { // Bulgarian\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Януари\", \"Февруари\", \"Март\", \"Април\", \"Май\", \"Юни\", \"Юли\", \"Август\", \"Септември\", \"Октомври\", \"Ноември\", \"Декември\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Нд\", \"Пн\", \"Вт\", \"Ср\", \"Чт\", \"Пт\", \"Сб\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Неделя\", \"Понеделник\", \"Вторник\", \"Сряда\", \"Четвъртък\", \"Петък\", \"Събота\"]\n\t\t\t},\n\t\t\tfa: { // Persian/Farsi\n\t\t\t\tmonths: [\n\t\t\t\t\t'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t'یکشنبه', 'دوشنبه', 'سه شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه'\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"یک‌شنبه\", \"دوشنبه\", \"سه‌شنبه\", \"چهارشنبه\", \"پنج‌شنبه\", \"جمعه\", \"شنبه\", \"یک‌شنبه\"]\n\t\t\t},\n\t\t\tru: { // Russian\n\t\t\t\tmonths: [\n\t\t\t\t\t'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Вс\", \"Пн\", \"Вт\", \"Ср\", \"Чт\", \"Пт\", \"Сб\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Воскресенье\", \"Понедельник\", \"Вторник\", \"Среда\", \"Четверг\", \"Пятница\", \"Суббота\"]\n\t\t\t},\n\t\t\tuk: { // Ukrainian\n\t\t\t\tmonths: [\n\t\t\t\t\t'Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ндл\", \"Пнд\", \"Втр\", \"Срд\", \"Чтв\", \"Птн\", \"Сбт\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Неділя\", \"Понеділок\", \"Вівторок\", \"Середа\", \"Четвер\", \"П'ятниця\", \"Субота\"]\n\t\t\t},\n\t\t\ten: { // English\n\t\t\t\tmonths: [\n\t\t\t\t\t\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"]\n\t\t\t},\n\t\t\tel: { // Ελληνικά\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Ιανουάριος\", \"Φεβρουάριος\", \"Μάρτιος\", \"Απρίλιος\", \"Μάιος\", \"Ιούνιος\", \"Ιούλιος\", \"Αύγουστος\", \"Σεπτέμβριος\", \"Οκτώβριος\", \"Νοέμβριος\", \"Δεκέμβριος\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Κυρ\", \"Δευ\", \"Τρι\", \"Τετ\", \"Πεμ\", \"Παρ\", \"Σαβ\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Κυριακή\", \"Δευτέρα\", \"Τρίτη\", \"Τετάρτη\", \"Πέμπτη\", \"Παρασκευή\", \"Σάββατο\"]\n\t\t\t},\n\t\t\tde: { // German\n\t\t\t\tmonths: [\n\t\t\t\t\t'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"So\", \"Mo\", \"Di\", \"Mi\", \"Do\", \"Fr\", \"Sa\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Sonntag\", \"Montag\", \"Dienstag\", \"Mittwoch\", \"Donnerstag\", \"Freitag\", \"Samstag\"]\n\t\t\t},\n\t\t\tnl: { // Dutch\n\t\t\t\tmonths: [\n\t\t\t\t\t\"januari\", \"februari\", \"maart\", \"april\", \"mei\", \"juni\", \"juli\", \"augustus\", \"september\", \"oktober\", \"november\", \"december\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"zo\", \"ma\", \"di\", \"wo\", \"do\", \"vr\", \"za\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"zondag\", \"maandag\", \"dinsdag\", \"woensdag\", \"donderdag\", \"vrijdag\", \"zaterdag\"]\n\t\t\t},\n\t\t\ttr: { // Turkish\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Ocak\", \"Şubat\", \"Mart\", \"Nisan\", \"Mayıs\", \"Haziran\", \"Temmuz\", \"Ağustos\", \"Eylül\", \"Ekim\", \"Kasım\", \"Aralık\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Paz\", \"Pts\", \"Sal\", \"Çar\", \"Per\", \"Cum\", \"Cts\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Pazar\", \"Pazartesi\", \"Salı\", \"Çarşamba\", \"Perşembe\", \"Cuma\", \"Cumartesi\"]\n\t\t\t},\n\t\t\tfr: { //French\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Janvier\", \"Février\", \"Mars\", \"Avril\", \"Mai\", \"Juin\", \"Juillet\", \"Août\", \"Septembre\", \"Octobre\", \"Novembre\", \"Décembre\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Dim\", \"Lun\", \"Mar\", \"Mer\", \"Jeu\", \"Ven\", \"Sam\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"dimanche\", \"lundi\", \"mardi\", \"mercredi\", \"jeudi\", \"vendredi\", \"samedi\"]\n\t\t\t},\n\t\t\tes: { // Spanish\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Enero\", \"Febrero\", \"Marzo\", \"Abril\", \"Mayo\", \"Junio\", \"Julio\", \"Agosto\", \"Septiembre\", \"Octubre\", \"Noviembre\", \"Diciembre\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Dom\", \"Lun\", \"Mar\", \"Mié\", \"Jue\", \"Vie\", \"Sáb\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Domingo\", \"Lunes\", \"Martes\", \"Miércoles\", \"Jueves\", \"Viernes\", \"Sábado\"]\n\t\t\t},\n\t\t\tth: { // Thai\n\t\t\t\tmonths: [\n\t\t\t\t\t'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t'อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"อาทิตย์\", \"จันทร์\", \"อังคาร\", \"พุธ\", \"พฤหัส\", \"ศุกร์\", \"เสาร์\", \"อาทิตย์\"]\n\t\t\t},\n\t\t\tpl: { // Polish\n\t\t\t\tmonths: [\n\t\t\t\t\t\"styczeń\", \"luty\", \"marzec\", \"kwiecień\", \"maj\", \"czerwiec\", \"lipiec\", \"sierpień\", \"wrzesień\", \"październik\", \"listopad\", \"grudzień\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"nd\", \"pn\", \"wt\", \"śr\", \"cz\", \"pt\", \"sb\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"niedziela\", \"poniedziałek\", \"wtorek\", \"środa\", \"czwartek\", \"piątek\", \"sobota\"]\n\t\t\t},\n\t\t\tpt: { // Portuguese\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Janeiro\", \"Fevereiro\", \"Março\", \"Abril\", \"Maio\", \"Junho\", \"Julho\", \"Agosto\", \"Setembro\", \"Outubro\", \"Novembro\", \"Dezembro\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Dom\", \"Seg\", \"Ter\", \"Qua\", \"Qui\", \"Sex\", \"Sab\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Domingo\", \"Segunda\", \"Terça\", \"Quarta\", \"Quinta\", \"Sexta\", \"Sábado\"]\n\t\t\t},\n\t\t\tch: { // Simplified Chinese\n\t\t\t\tmonths: [\n\t\t\t\t\t\"一月\", \"二月\", \"三月\", \"四月\", \"五月\", \"六月\", \"七月\", \"八月\", \"九月\", \"十月\", \"十一月\", \"十二月\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"日\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\"\n\t\t\t\t]\n\t\t\t},\n\t\t\tse: { // Swedish\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januari\", \"Februari\", \"Mars\", \"April\", \"Maj\", \"Juni\", \"Juli\", \"Augusti\", \"September\",  \"Oktober\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Sön\", \"Mån\", \"Tis\", \"Ons\", \"Tor\", \"Fre\", \"Lör\"\n\t\t\t\t]\n\t\t\t},\n\t\t\tkm: { // Khmer (ភាសាខ្មែរ)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"មករា​\", \"កុម្ភៈ\", \"មិនា​\", \"មេសា​\", \"ឧសភា​\", \"មិថុនា​\", \"កក្កដា​\", \"សីហា​\", \"កញ្ញា​\", \"តុលា​\", \"វិច្ឆិកា\", \"ធ្នូ​\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\"អាទិ​\", \"ច័ន្ទ​\", \"អង្គារ​\", \"ពុធ​\", \"ព្រហ​​\", \"សុក្រ​\", \"សៅរ៍\"],\n\t\t\t\tdayOfWeek: [\"អាទិត្យ​\", \"ច័ន្ទ​\", \"អង្គារ​\", \"ពុធ​\", \"ព្រហស្បតិ៍​\", \"សុក្រ​\", \"សៅរ៍\"]\n\t\t\t},\n\t\t\tkr: { // Korean\n\t\t\t\tmonths: [\n\t\t\t\t\t\"1월\", \"2월\", \"3월\", \"4월\", \"5월\", \"6월\", \"7월\", \"8월\", \"9월\", \"10월\", \"11월\", \"12월\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"일\", \"월\", \"화\", \"수\", \"목\", \"금\", \"토\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"일요일\", \"월요일\", \"화요일\", \"수요일\", \"목요일\", \"금요일\", \"토요일\"]\n\t\t\t},\n\t\t\tit: { // Italian\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Gennaio\", \"Febbraio\", \"Marzo\", \"Aprile\", \"Maggio\", \"Giugno\", \"Luglio\", \"Agosto\", \"Settembre\", \"Ottobre\", \"Novembre\", \"Dicembre\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Dom\", \"Lun\", \"Mar\", \"Mer\", \"Gio\", \"Ven\", \"Sab\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Domenica\", \"Lunedì\", \"Martedì\", \"Mercoledì\", \"Giovedì\", \"Venerdì\", \"Sabato\"]\n\t\t\t},\n\t\t\tda: { // Dansk\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januar\", \"Februar\", \"Marts\", \"April\", \"Maj\", \"Juni\", \"Juli\", \"August\", \"September\", \"Oktober\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Søn\", \"Man\", \"Tir\", \"Ons\", \"Tor\", \"Fre\", \"Lør\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"søndag\", \"mandag\", \"tirsdag\", \"onsdag\", \"torsdag\", \"fredag\", \"lørdag\"]\n\t\t\t},\n\t\t\tno: { // Norwegian\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januar\", \"Februar\", \"Mars\", \"April\", \"Mai\", \"Juni\", \"Juli\", \"August\", \"September\", \"Oktober\", \"November\", \"Desember\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Søn\", \"Man\", \"Tir\", \"Ons\", \"Tor\", \"Fre\", \"Lør\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: ['Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag']\n\t\t\t},\n\t\t\tja: { // Japanese\n\t\t\t\tmonths: [\n\t\t\t\t\t\"1月\", \"2月\", \"3月\", \"4月\", \"5月\", \"6月\", \"7月\", \"8月\", \"9月\", \"10月\", \"11月\", \"12月\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"日\", \"月\", \"火\", \"水\", \"木\", \"金\", \"土\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"日曜\", \"月曜\", \"火曜\", \"水曜\", \"木曜\", \"金曜\", \"土曜\"]\n\t\t\t},\n\t\t\tvi: { // Vietnamese\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Tháng 1\", \"Tháng 2\", \"Tháng 3\", \"Tháng 4\", \"Tháng 5\", \"Tháng 6\", \"Tháng 7\", \"Tháng 8\", \"Tháng 9\", \"Tháng 10\", \"Tháng 11\", \"Tháng 12\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"CN\", \"T2\", \"T3\", \"T4\", \"T5\", \"T6\", \"T7\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Chủ nhật\", \"Thứ hai\", \"Thứ ba\", \"Thứ tư\", \"Thứ năm\", \"Thứ sáu\", \"Thứ bảy\"]\n\t\t\t},\n\t\t\tsl: { // Slovenščina\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januar\", \"Februar\", \"Marec\", \"April\", \"Maj\", \"Junij\", \"Julij\", \"Avgust\", \"September\", \"Oktober\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ned\", \"Pon\", \"Tor\", \"Sre\", \"Čet\", \"Pet\", \"Sob\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Nedelja\", \"Ponedeljek\", \"Torek\", \"Sreda\", \"Četrtek\", \"Petek\", \"Sobota\"]\n\t\t\t},\n\t\t\tcs: { // Čeština\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Leden\", \"Únor\", \"Březen\", \"Duben\", \"Květen\", \"Červen\", \"Červenec\", \"Srpen\", \"Září\", \"Říjen\", \"Listopad\", \"Prosinec\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ne\", \"Po\", \"Út\", \"St\", \"Čt\", \"Pá\", \"So\"\n\t\t\t\t]\n\t\t\t},\n\t\t\thu: { // Hungarian\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Január\", \"Február\", \"Március\", \"Április\", \"Május\", \"Június\", \"Július\", \"Augusztus\", \"Szeptember\", \"Október\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Va\", \"Hé\", \"Ke\", \"Sze\", \"Cs\", \"Pé\", \"Szo\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"vasárnap\", \"hétfő\", \"kedd\", \"szerda\", \"csütörtök\", \"péntek\", \"szombat\"]\n\t\t\t},\n\t\t\taz: { //Azerbaijanian (Azeri)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Yanvar\", \"Fevral\", \"Mart\", \"Aprel\", \"May\", \"Iyun\", \"Iyul\", \"Avqust\", \"Sentyabr\", \"Oktyabr\", \"Noyabr\", \"Dekabr\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"B\", \"Be\", \"Ça\", \"Ç\", \"Ca\", \"C\", \"Ş\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Bazar\", \"Bazar ertəsi\", \"Çərşənbə axşamı\", \"Çərşənbə\", \"Cümə axşamı\", \"Cümə\", \"Şənbə\"]\n\t\t\t},\n\t\t\tbs: { //Bosanski\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januar\", \"Februar\", \"Mart\", \"April\", \"Maj\", \"Jun\", \"Jul\", \"Avgust\", \"Septembar\", \"Oktobar\", \"Novembar\", \"Decembar\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ned\", \"Pon\", \"Uto\", \"Sri\", \"Čet\", \"Pet\", \"Sub\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Nedjelja\",\"Ponedjeljak\", \"Utorak\", \"Srijeda\", \"Četvrtak\", \"Petak\", \"Subota\"]\n\t\t\t},\n\t\t\tca: { //Català\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Gener\", \"Febrer\", \"Març\", \"Abril\", \"Maig\", \"Juny\", \"Juliol\", \"Agost\", \"Setembre\", \"Octubre\", \"Novembre\", \"Desembre\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Dg\", \"Dl\", \"Dt\", \"Dc\", \"Dj\", \"Dv\", \"Ds\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Diumenge\", \"Dilluns\", \"Dimarts\", \"Dimecres\", \"Dijous\", \"Divendres\", \"Dissabte\"]\n\t\t\t},\n\t\t\t'en-GB': { //English (British)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"]\n\t\t\t},\n\t\t\tet: { //\"Eesti\"\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Jaanuar\", \"Veebruar\", \"Märts\", \"Aprill\", \"Mai\", \"Juuni\", \"Juuli\", \"August\", \"September\", \"Oktoober\", \"November\", \"Detsember\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"P\", \"E\", \"T\", \"K\", \"N\", \"R\", \"L\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Pühapäev\", \"Esmaspäev\", \"Teisipäev\", \"Kolmapäev\", \"Neljapäev\", \"Reede\", \"Laupäev\"]\n\t\t\t},\n\t\t\teu: { //Euskara\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Urtarrila\", \"Otsaila\", \"Martxoa\", \"Apirila\", \"Maiatza\", \"Ekaina\", \"Uztaila\", \"Abuztua\", \"Iraila\", \"Urria\", \"Azaroa\", \"Abendua\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ig.\", \"Al.\", \"Ar.\", \"Az.\", \"Og.\", \"Or.\", \"La.\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: ['Igandea', 'Astelehena', 'Asteartea', 'Asteazkena', 'Osteguna', 'Ostirala', 'Larunbata']\n\t\t\t},\n\t\t\tfi: { //Finnish (Suomi)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Tammikuu\", \"Helmikuu\", \"Maaliskuu\", \"Huhtikuu\", \"Toukokuu\", \"Kesäkuu\", \"Heinäkuu\", \"Elokuu\", \"Syyskuu\", \"Lokakuu\", \"Marraskuu\", \"Joulukuu\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Su\", \"Ma\", \"Ti\", \"Ke\", \"To\", \"Pe\", \"La\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"sunnuntai\", \"maanantai\", \"tiistai\", \"keskiviikko\", \"torstai\", \"perjantai\", \"lauantai\"]\n\t\t\t},\n\t\t\tgl: { //Galego\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Xan\", \"Feb\", \"Maz\", \"Abr\", \"Mai\", \"Xun\", \"Xul\", \"Ago\", \"Set\", \"Out\", \"Nov\", \"Dec\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Dom\", \"Lun\", \"Mar\", \"Mer\", \"Xov\", \"Ven\", \"Sab\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Domingo\", \"Luns\", \"Martes\", \"Mércores\", \"Xoves\", \"Venres\", \"Sábado\"]\n\t\t\t},\n\t\t\thr: { //Hrvatski\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Siječanj\", \"Veljača\", \"Ožujak\", \"Travanj\", \"Svibanj\", \"Lipanj\", \"Srpanj\", \"Kolovoz\", \"Rujan\", \"Listopad\", \"Studeni\", \"Prosinac\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ned\", \"Pon\", \"Uto\", \"Sri\", \"Čet\", \"Pet\", \"Sub\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Nedjelja\", \"Ponedjeljak\", \"Utorak\", \"Srijeda\", \"Četvrtak\", \"Petak\", \"Subota\"]\n\t\t\t},\n\t\t\tko: { //Korean (한국어)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"1월\", \"2월\", \"3월\", \"4월\", \"5월\", \"6월\", \"7월\", \"8월\", \"9월\", \"10월\", \"11월\", \"12월\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"일\", \"월\", \"화\", \"수\", \"목\", \"금\", \"토\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"일요일\", \"월요일\", \"화요일\", \"수요일\", \"목요일\", \"금요일\", \"토요일\"]\n\t\t\t},\n\t\t\tlt: { //Lithuanian (lietuvių)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Sausio\", \"Vasario\", \"Kovo\", \"Balandžio\", \"Gegužės\", \"Birželio\", \"Liepos\", \"Rugpjūčio\", \"Rugsėjo\", \"Spalio\", \"Lapkričio\", \"Gruodžio\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Sek\", \"Pir\", \"Ant\", \"Tre\", \"Ket\", \"Pen\", \"Šeš\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Sekmadienis\", \"Pirmadienis\", \"Antradienis\", \"Trečiadienis\", \"Ketvirtadienis\", \"Penktadienis\", \"Šeštadienis\"]\n\t\t\t},\n\t\t\tlv: { //Latvian (Latviešu)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Janvāris\", \"Februāris\", \"Marts\", \"Aprīlis \", \"Maijs\", \"Jūnijs\", \"Jūlijs\", \"Augusts\", \"Septembris\", \"Oktobris\", \"Novembris\", \"Decembris\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Sv\", \"Pr\", \"Ot\", \"Tr\", \"Ct\", \"Pk\", \"St\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Svētdiena\", \"Pirmdiena\", \"Otrdiena\", \"Trešdiena\", \"Ceturtdiena\", \"Piektdiena\", \"Sestdiena\"]\n\t\t\t},\n\t\t\tmk: { //Macedonian (Македонски)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"јануари\", \"февруари\", \"март\", \"април\", \"мај\", \"јуни\", \"јули\", \"август\", \"септември\", \"октомври\", \"ноември\", \"декември\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"нед\", \"пон\", \"вто\", \"сре\", \"чет\", \"пет\", \"саб\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Недела\", \"Понеделник\", \"Вторник\", \"Среда\", \"Четврток\", \"Петок\", \"Сабота\"]\n\t\t\t},\n\t\t\tmn: { //Mongolian (Монгол)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"1-р сар\", \"2-р сар\", \"3-р сар\", \"4-р сар\", \"5-р сар\", \"6-р сар\", \"7-р сар\", \"8-р сар\", \"9-р сар\", \"10-р сар\", \"11-р сар\", \"12-р сар\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Дав\", \"Мяг\", \"Лха\", \"Пүр\", \"Бсн\", \"Бям\", \"Ням\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Даваа\", \"Мягмар\", \"Лхагва\", \"Пүрэв\", \"Баасан\", \"Бямба\", \"Ням\"]\n\t\t\t},\n\t\t\t'pt-BR': { //Português(Brasil)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Janeiro\", \"Fevereiro\", \"Março\", \"Abril\", \"Maio\", \"Junho\", \"Julho\", \"Agosto\", \"Setembro\", \"Outubro\", \"Novembro\", \"Dezembro\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Dom\", \"Seg\", \"Ter\", \"Qua\", \"Qui\", \"Sex\", \"Sáb\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Domingo\", \"Segunda\", \"Terça\", \"Quarta\", \"Quinta\", \"Sexta\", \"Sábado\"]\n\t\t\t},\n\t\t\tsk: { //Slovenčina\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Január\", \"Február\", \"Marec\", \"Apríl\", \"Máj\", \"Jún\", \"Júl\", \"August\", \"September\", \"Október\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ne\", \"Po\", \"Ut\", \"St\", \"Št\", \"Pi\", \"So\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Nedeľa\", \"Pondelok\", \"Utorok\", \"Streda\", \"Štvrtok\", \"Piatok\", \"Sobota\"]\n\t\t\t},\n\t\t\tsq: { //Albanian (Shqip)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Janar\", \"Shkurt\", \"Mars\", \"Prill\", \"Maj\", \"Qershor\", \"Korrik\", \"Gusht\", \"Shtator\", \"Tetor\", \"Nëntor\", \"Dhjetor\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Die\", \"Hën\", \"Mar\", \"Mër\", \"Enj\", \"Pre\", \"Shtu\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"E Diel\", \"E Hënë\", \"E Martē\", \"E Mërkurë\", \"E Enjte\", \"E Premte\", \"E Shtunë\"]\n\t\t\t},\n\t\t\t'sr-YU': { //Serbian (Srpski)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januar\", \"Februar\", \"Mart\", \"April\", \"Maj\", \"Jun\", \"Jul\", \"Avgust\", \"Septembar\", \"Oktobar\", \"Novembar\", \"Decembar\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Ned\", \"Pon\", \"Uto\", \"Sre\", \"čet\", \"Pet\", \"Sub\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Nedelja\",\"Ponedeljak\", \"Utorak\", \"Sreda\", \"Četvrtak\", \"Petak\", \"Subota\"]\n\t\t\t},\n\t\t\tsr: { //Serbian Cyrillic (Српски)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"јануар\", \"фебруар\", \"март\", \"април\", \"мај\", \"јун\", \"јул\", \"август\", \"септембар\", \"октобар\", \"новембар\", \"децембар\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"нед\", \"пон\", \"уто\", \"сре\", \"чет\", \"пет\", \"суб\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Недеља\",\"Понедељак\", \"Уторак\", \"Среда\", \"Четвртак\", \"Петак\", \"Субота\"]\n\t\t\t},\n\t\t\tsv: { //Svenska\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Januari\", \"Februari\", \"Mars\", \"April\", \"Maj\", \"Juni\", \"Juli\", \"Augusti\", \"September\", \"Oktober\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Sön\", \"Mån\", \"Tis\", \"Ons\", \"Tor\", \"Fre\", \"Lör\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Söndag\", \"Måndag\", \"Tisdag\", \"Onsdag\", \"Torsdag\", \"Fredag\", \"Lördag\"]\n\t\t\t},\n\t\t\t'zh-TW': { //Traditional Chinese (繁體中文)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"一月\", \"二月\", \"三月\", \"四月\", \"五月\", \"六月\", \"七月\", \"八月\", \"九月\", \"十月\", \"十一月\", \"十二月\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"日\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"星期日\", \"星期一\", \"星期二\", \"星期三\", \"星期四\", \"星期五\", \"星期六\"]\n\t\t\t},\n\t\t\tzh: { //Simplified Chinese (简体中文)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"一月\", \"二月\", \"三月\", \"四月\", \"五月\", \"六月\", \"七月\", \"八月\", \"九月\", \"十月\", \"十一月\", \"十二月\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"日\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"星期日\", \"星期一\", \"星期二\", \"星期三\", \"星期四\", \"星期五\", \"星期六\"]\n\t\t\t},\n\t\t\tug:{ // Uyghur(ئۇيغۇرچە)\n\t\t\t\tmonths: [\n\t\t\t\t\t\"1-ئاي\",\"2-ئاي\",\"3-ئاي\",\"4-ئاي\",\"5-ئاي\",\"6-ئاي\",\"7-ئاي\",\"8-ئاي\",\"9-ئاي\",\"10-ئاي\",\"11-ئاي\",\"12-ئاي\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\n\t\t\t\t\t\"يەكشەنبە\", \"دۈشەنبە\",\"سەيشەنبە\",\"چارشەنبە\",\"پەيشەنبە\",\"جۈمە\",\"شەنبە\"\n\t\t\t\t]\n\t\t\t},\n\t\t\the: { //Hebrew (עברית)\n\t\t\t\tmonths: [\n\t\t\t\t\t'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t'א\\'', 'ב\\'', 'ג\\'', 'ד\\'', 'ה\\'', 'ו\\'', 'שבת'\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"ראשון\", \"שני\", \"שלישי\", \"רביעי\", \"חמישי\", \"שישי\", \"שבת\", \"ראשון\"]\n\t\t\t},\n\t\t\thy: { // Armenian\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Հունվար\", \"Փետրվար\", \"Մարտ\", \"Ապրիլ\", \"Մայիս\", \"Հունիս\", \"Հուլիս\", \"Օգոստոս\", \"Սեպտեմբեր\", \"Հոկտեմբեր\", \"Նոյեմբեր\", \"Դեկտեմբեր\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Կի\", \"Երկ\", \"Երք\", \"Չոր\", \"Հնգ\", \"Ուրբ\", \"Շբթ\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"Կիրակի\", \"Երկուշաբթի\", \"Երեքշաբթի\", \"Չորեքշաբթի\", \"Հինգշաբթի\", \"Ուրբաթ\", \"Շաբաթ\"]\n\t\t\t},\n\t\t\tkg: { // Kyrgyz\n\t\t\t\tmonths: [\n\t\t\t\t\t'Үчтүн айы', 'Бирдин айы', 'Жалган Куран', 'Чын Куран', 'Бугу', 'Кулжа', 'Теке', 'Баш Оона', 'Аяк Оона', 'Тогуздун айы', 'Жетинин айы', 'Бештин айы'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Жек\", \"Дүй\", \"Шей\", \"Шар\", \"Бей\", \"Жум\", \"Ише\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\n\t\t\t\t\t\"Жекшемб\", \"Дүйшөмб\", \"Шейшемб\", \"Шаршемб\", \"Бейшемби\", \"Жума\", \"Ишенб\"\n\t\t\t\t]\n\t\t\t},\n\t\t\trm: { // Romansh\n\t\t\t\tmonths: [\n\t\t\t\t\t\"Schaner\", \"Favrer\", \"Mars\", \"Avrigl\", \"Matg\", \"Zercladur\", \"Fanadur\", \"Avust\", \"Settember\", \"October\", \"November\", \"December\"\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"Du\", \"Gli\", \"Ma\", \"Me\", \"Gie\", \"Ve\", \"So\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\n\t\t\t\t\t\"Dumengia\", \"Glindesdi\", \"Mardi\", \"Mesemna\", \"Gievgia\", \"Venderdi\", \"Sonda\"\n\t\t\t\t]\n\t\t\t},\n\t\t\tka: { // Georgian\n\t\t\t\tmonths: [\n\t\t\t\t\t'იანვარი', 'თებერვალი', 'მარტი', 'აპრილი', 'მაისი', 'ივნისი', 'ივლისი', 'აგვისტო', 'სექტემბერი', 'ოქტომბერი', 'ნოემბერი', 'დეკემბერი'\n\t\t\t\t],\n\t\t\t\tdayOfWeekShort: [\n\t\t\t\t\t\"კვ\", \"ორშ\", \"სამშ\", \"ოთხ\", \"ხუთ\", \"პარ\", \"შაბ\"\n\t\t\t\t],\n\t\t\t\tdayOfWeek: [\"კვირა\", \"ორშაბათი\", \"სამშაბათი\", \"ოთხშაბათი\", \"ხუთშაბათი\", \"პარასკევი\", \"შაბათი\"]\n\t\t\t}\n\t\t},\n\n\t\townerDocument: document,\n\t\tcontentWindow: window,\n\n\t\tvalue: '',\n\t\trtl: false,\n\n\t\tformat:\t'Y/m/d H:i',\n\t\tformatTime:\t'H:i',\n\t\tformatDate:\t'Y/m/d',\n\n\t\tstartDate:\tfalse, // new Date(), '1986/12/08', '-1970/01/05','-1970/01/05',\n\t\tstep: 60,\n\t\tmonthChangeSpinner: true,\n\n\t\tcloseOnDateSelect: false,\n\t\tcloseOnTimeSelect: true,\n\t\tcloseOnWithoutClick: true,\n\t\tcloseOnInputClick: true,\n\t\topenOnFocus: true,\n\n\t\ttimepicker: true,\n\t\tdatepicker: true,\n\t\tweeks: false,\n\n\t\tdefaultTime: false,\t// use formatTime format (ex. '10:00' for formatTime:\t'H:i')\n\t\tdefaultDate: false,\t// use formatDate format (ex new Date() or '1986/12/08' or '-1970/01/05' or '-1970/01/05')\n\n\t\tminDate: false,\n\t\tmaxDate: false,\n\t\tminTime: false,\n\t\tmaxTime: false,\n        minDateTime: false,\n\n\t\tdisabledMinTime: false,\n\t\tdisabledMaxTime: false,\n\n\t\tallowTimes: [],\n\t\topened: false,\n\t\tinitTime: true,\n\t\tinline: false,\n\t\ttheme: '',\n\t\ttouchMovedThreshold: 5,\n\n\t\tonSelectDate: function () {},\n\t\tonSelectTime: function () {},\n\t\tonChangeMonth: function () {},\n\t\tonGetWeekOfYear: function () {},\n\t\tonChangeYear: function () {},\n\t\tonChangeDateTime: function () {},\n\t\tonShow: function () {},\n\t\tonClose: function () {},\n\t\tonGenerate: function () {},\n\n\t\twithoutCopyright: true,\n\t\tinverseButton: false,\n\t\thours12: false,\n\t\tnext: 'xdsoft_next',\n\t\tprev : 'xdsoft_prev',\n\t\tdayOfWeekStart: 0,\n\t\tparentID: 'body',\n\t\ttimeHeightInTimePicker: 25,\n\t\ttimepickerScrollbar: true,\n\t\ttodayButton: true,\n\t\tprevButton: true,\n\t\tnextButton: true,\n\t\tdefaultSelect: true,\n\n\t\tscrollMonth: true,\n\t\tscrollTime: true,\n\t\tscrollInput: true,\n\n\t\tlazyInit: false,\n\t\tmask: false,\n\t\tvalidateOnBlur: true,\n\t\tallowBlank: true,\n\t\tyearStart: 1950,\n\t\tyearEnd: 2050,\n\t\tmonthStart: 0,\n\t\tmonthEnd: 11,\n\t\tstyle: '',\n\t\tid: '',\n\t\tfixed: false,\n\t\troundTime: 'round', // ceil, floor\n\t\tclassName: '',\n\t\tweekends: [],\n\t\thighlightedDates: [],\n\t\thighlightedPeriods: [],\n\t\tallowDates : [],\n\t\tallowDateRe : null,\n\t\tdisabledDates : [],\n\t\tdisabledWeekDays: [],\n\t\tyearOffset: 0,\n\t\tbeforeShowDay: null,\n\n\t\tenterLikeTab: true,\n\t\tshowApplyButton: false\n\t};\n\n\tvar dateHelper = null,\n\t\tdefaultDateHelper = null,\n\t\tglobalLocaleDefault = 'en',\n\t\tglobalLocale = 'en';\n\n\tvar dateFormatterOptionsDefault = {\n\t\tmeridiem: ['AM', 'PM']\n\t};\n\n\tvar initDateFormatter = function(){\n\t\tvar locale = default_options.i18n[globalLocale],\n\t\t\topts = {\n\t\t\t\tdays: locale.dayOfWeek,\n\t\t\t\tdaysShort: locale.dayOfWeekShort,\n\t\t\t\tmonths: locale.months,\n\t\t\t\tmonthsShort: $.map(locale.months, function(n){ return n.substring(0, 3) })\n\t\t\t};\n\n\t\tif (typeof DateFormatter === 'function') {\n\t\t\tdateHelper = defaultDateHelper = new DateFormatter({\n\t\t\t\tdateSettings: $.extend({}, dateFormatterOptionsDefault, opts)\n\t\t\t});\n\t\t}\n\t};\n\n\tvar dateFormatters = {\n\t\tmoment: {\n\t\t\tdefault_options:{\n\t\t\t\tformat: 'YYYY/MM/DD HH:mm',\n\t\t\t\tformatDate: 'YYYY/MM/DD',\n\t\t\t\tformatTime: 'HH:mm',\n\t\t\t},\n\t\t\tformatter: {\n\t\t\t\tparseDate: function (date, format) {\n\t\t\t\t\tif(isFormatStandard(format)){\n\t\t\t\t\t\treturn defaultDateHelper.parseDate(date, format);\n\t\t\t\t\t} \n\t\t\t\t\tvar d = moment(date, format);\n\t\t\t\t\treturn d.isValid() ? d.toDate() : false;\n\t\t\t\t},\n\n\t\t\t\tformatDate: function (date, format) {\n\t\t\t\t\tif(isFormatStandard(format)){\n\t\t\t\t\t\treturn defaultDateHelper.formatDate(date, format);\n\t\t\t\t\t} \n\t\t\t\t\treturn moment(date).format(format);\n\t\t\t\t},\n\n\t\t\t\tformatMask: function(format){\n\t\t\t\t\treturn format\n\t\t\t\t\t\t.replace(/Y{4}/g, '9999')\n\t\t\t\t\t\t.replace(/Y{2}/g, '99')\n\t\t\t\t\t\t.replace(/M{2}/g, '19')\n\t\t\t\t\t\t.replace(/D{2}/g, '39')\n\t\t\t\t\t\t.replace(/H{2}/g, '29')\n\t\t\t\t\t\t.replace(/m{2}/g, '59')\n\t\t\t\t\t\t.replace(/s{2}/g, '59');\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t}\n\n\t// for locale settings\n\t$.datetimepicker = {\n\t\tsetLocale: function(locale){\n\t\t\tvar newLocale = default_options.i18n[locale] ? locale : globalLocaleDefault;\n\t\t\tif (globalLocale !== newLocale) {\n\t\t\t\tglobalLocale = newLocale;\n\t\t\t\t// reinit date formatter\n\t\t\t\tinitDateFormatter();\n\t\t\t}\n\t\t},\n\n\t\tsetDateFormatter: function(dateFormatter) {\n\t\t\tif(typeof dateFormatter === 'string' && dateFormatters.hasOwnProperty(dateFormatter)){\n\t\t\t\tvar df = dateFormatters[dateFormatter];\n\t\t\t\t$.extend(default_options, df.default_options);\n\t\t\t\tdateHelper = df.formatter; \n\t\t\t}\n\t\t\telse {\n\t\t\t\tdateHelper = dateFormatter;\n\t\t\t}\n\t\t},\n\t};\n\n\tvar standardFormats = {\n\t\tRFC_2822: 'D, d M Y H:i:s O',\n\t\tATOM: 'Y-m-d\\TH:i:sP',\n\t\tISO_8601: 'Y-m-d\\TH:i:sO',\n\t\tRFC_822: 'D, d M y H:i:s O',\n\t\tRFC_850: 'l, d-M-y H:i:s T',\n\t\tRFC_1036: 'D, d M y H:i:s O',\n\t\tRFC_1123: 'D, d M Y H:i:s O',\n\t\tRSS: 'D, d M Y H:i:s O',\n\t\tW3C: 'Y-m-d\\TH:i:sP'\n\t}\n\n\tvar isFormatStandard = function(format){\n\t\treturn Object.values(standardFormats).indexOf(format) === -1 ? false : true;\n\t}\n\n\t$.extend($.datetimepicker, standardFormats);\n\n\t// first init date formatter\n\tinitDateFormatter();\n\n\t// fix for ie8\n\tif (!window.getComputedStyle) {\n\t\twindow.getComputedStyle = function (el) {\n\t\t\tthis.el = el;\n\t\t\tthis.getPropertyValue = function (prop) {\n\t\t\t\tvar re = /(-([a-z]))/g;\n\t\t\t\tif (prop === 'float') {\n\t\t\t\t\tprop = 'styleFloat';\n\t\t\t\t}\n\t\t\t\tif (re.test(prop)) {\n\t\t\t\t\tprop = prop.replace(re, function (a, b, c) {\n\t\t\t\t\t\treturn c.toUpperCase();\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn el.currentStyle[prop] || null;\n\t\t\t};\n\t\t\treturn this;\n\t\t};\n\t}\n\tif (!Array.prototype.indexOf) {\n\t\tArray.prototype.indexOf = function (obj, start) {\n\t\t\tvar i, j;\n\t\t\tfor (i = (start || 0), j = this.length; i < j; i += 1) {\n\t\t\t\tif (this[i] === obj) { return i; }\n\t\t\t}\n\t\t\treturn -1;\n\t\t};\n\t}\n\n\tDate.prototype.countDaysInMonth = function () {\n\t\treturn new Date(this.getFullYear(), this.getMonth() + 1, 0).getDate();\n\t};\n\n\t$.fn.xdsoftScroller = function (options, percent) {\n\t\treturn this.each(function () {\n\t\t\tvar timeboxparent = $(this),\n\t\t\t\tpointerEventToXY = function (e) {\n\t\t\t\t\tvar out = {x: 0, y: 0},\n\t\t\t\t\t\ttouch;\n\t\t\t\t\tif (e.type === 'touchstart' || e.type === 'touchmove' || e.type === 'touchend' || e.type === 'touchcancel') {\n\t\t\t\t\t\ttouch  = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];\n\t\t\t\t\t\tout.x = touch.clientX;\n\t\t\t\t\t\tout.y = touch.clientY;\n\t\t\t\t\t} else if (e.type === 'mousedown' || e.type === 'mouseup' || e.type === 'mousemove' || e.type === 'mouseover' || e.type === 'mouseout' || e.type === 'mouseenter' || e.type === 'mouseleave') {\n\t\t\t\t\t\tout.x = e.clientX;\n\t\t\t\t\t\tout.y = e.clientY;\n\t\t\t\t\t}\n\t\t\t\t\treturn out;\n\t\t\t\t},\n\t\t\t\ttimebox,\n\t\t\t\tparentHeight,\n\t\t\t\theight,\n\t\t\t\tscrollbar,\n\t\t\t\tscroller,\n\t\t\t\tmaximumOffset = 100,\n\t\t\t\tstart = false,\n\t\t\t\tstartY = 0,\n\t\t\t\tstartTop = 0,\n\t\t\t\th1 = 0,\n\t\t\t\ttouchStart = false,\n\t\t\t\tstartTopScroll = 0,\n\t\t\t\tcalcOffset = function () {};\n\n\t\t\tif (percent === 'hide') {\n\t\t\t\ttimeboxparent.find('.xdsoft_scrollbar').hide();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!$(this).hasClass('xdsoft_scroller_box')) {\n\t\t\t\ttimebox = timeboxparent.children().eq(0);\n\t\t\t\tparentHeight = timeboxparent[0].clientHeight;\n\t\t\t\theight = timebox[0].offsetHeight;\n\t\t\t\tscrollbar = $('<div class=\"xdsoft_scrollbar\"></div>');\n\t\t\t\tscroller = $('<div class=\"xdsoft_scroller\"></div>');\n\t\t\t\tscrollbar.append(scroller);\n\n\t\t\t\ttimeboxparent.addClass('xdsoft_scroller_box').append(scrollbar);\n\t\t\t\tcalcOffset = function calcOffset(event) {\n\t\t\t\t\tvar offset = pointerEventToXY(event).y - startY + startTopScroll;\n\t\t\t\t\tif (offset < 0) {\n\t\t\t\t\t\toffset = 0;\n\t\t\t\t\t}\n\t\t\t\t\tif (offset + scroller[0].offsetHeight > h1) {\n\t\t\t\t\t\toffset = h1 - scroller[0].offsetHeight;\n\t\t\t\t\t}\n\t\t\t\t\ttimeboxparent.trigger('scroll_element.xdsoft_scroller', [maximumOffset ? offset / maximumOffset : 0]);\n\t\t\t\t};\n\n\t\t\t\tscroller\n\t\t\t\t\t.on('touchstart.xdsoft_scroller mousedown.xdsoft_scroller', function (event) {\n\t\t\t\t\t\tif (!parentHeight) {\n\t\t\t\t\t\t\ttimeboxparent.trigger('resize_scroll.xdsoft_scroller', [percent]);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstartY = pointerEventToXY(event).y;\n\t\t\t\t\t\tstartTopScroll = parseInt(scroller.css('margin-top'), 10);\n\t\t\t\t\t\th1 = scrollbar[0].offsetHeight;\n\n\t\t\t\t\t\tif (event.type === 'mousedown' || event.type === 'touchstart') {\n\t\t\t\t\t\t\tif (options.ownerDocument) {\n\t\t\t\t\t\t\t\t$(options.ownerDocument.body).addClass('xdsoft_noselect');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow]).on('touchend mouseup.xdsoft_scroller', function arguments_callee() {\n\t\t\t\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow]).off('touchend mouseup.xdsoft_scroller', arguments_callee)\n\t\t\t\t\t\t\t\t\t.off('mousemove.xdsoft_scroller', calcOffset)\n\t\t\t\t\t\t\t\t\t.removeClass('xdsoft_noselect');\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t$(options.ownerDocument.body).on('mousemove.xdsoft_scroller', calcOffset);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttouchStart = true;\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.on('touchmove', function (event) {\n\t\t\t\t\t\tif (touchStart) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tcalcOffset(event);\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.on('touchend touchcancel', function () {\n\t\t\t\t\t\ttouchStart =  false;\n\t\t\t\t\t\tstartTopScroll = 0;\n\t\t\t\t\t});\n\n\t\t\t\ttimeboxparent\n\t\t\t\t\t.on('scroll_element.xdsoft_scroller', function (event, percentage) {\n\t\t\t\t\t\tif (!parentHeight) {\n\t\t\t\t\t\t\ttimeboxparent.trigger('resize_scroll.xdsoft_scroller', [percentage, true]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpercentage = percentage > 1 ? 1 : (percentage < 0 || isNaN(percentage)) ? 0 : percentage;\n\n\t\t\t\t\t\tscroller.css('margin-top', maximumOffset * percentage);\n\n\t\t\t\t\t\tsetTimeout(function () {\n\t\t\t\t\t\t\ttimebox.css('marginTop', -parseInt((timebox[0].offsetHeight - parentHeight) * percentage, 10));\n\t\t\t\t\t\t}, 10);\n\t\t\t\t\t})\n\t\t\t\t\t.on('resize_scroll.xdsoft_scroller', function (event, percentage, noTriggerScroll) {\n\t\t\t\t\t\tvar percent, sh;\n\t\t\t\t\t\tparentHeight = timeboxparent[0].clientHeight;\n\t\t\t\t\t\theight = timebox[0].offsetHeight;\n\t\t\t\t\t\tpercent = parentHeight / height;\n\t\t\t\t\t\tsh = percent * scrollbar[0].offsetHeight;\n\t\t\t\t\t\tif (percent > 1) {\n\t\t\t\t\t\t\tscroller.hide();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tscroller.show();\n\t\t\t\t\t\t\tscroller.css('height', parseInt(sh > 10 ? sh : 10, 10));\n\t\t\t\t\t\t\tmaximumOffset = scrollbar[0].offsetHeight - scroller[0].offsetHeight;\n\t\t\t\t\t\t\tif (noTriggerScroll !== true) {\n\t\t\t\t\t\t\t\ttimeboxparent.trigger('scroll_element.xdsoft_scroller', [percentage || Math.abs(parseInt(timebox.css('marginTop'), 10)) / (height - parentHeight)]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\ttimeboxparent.on('mousewheel', function (event) {\n\t\t\t\t\tvar top = Math.abs(parseInt(timebox.css('marginTop'), 10));\n\n\t\t\t\t\ttop = top - (event.deltaY * 20);\n\t\t\t\t\tif (top < 0) {\n\t\t\t\t\t\ttop = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\ttimeboxparent.trigger('scroll_element.xdsoft_scroller', [top / (height - parentHeight)]);\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\n\t\t\t\ttimeboxparent.on('touchstart', function (event) {\n\t\t\t\t\tstart = pointerEventToXY(event);\n\t\t\t\t\tstartTop = Math.abs(parseInt(timebox.css('marginTop'), 10));\n\t\t\t\t});\n\n\t\t\t\ttimeboxparent.on('touchmove', function (event) {\n\t\t\t\t\tif (start) {\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\tvar coord = pointerEventToXY(event);\n\t\t\t\t\t\ttimeboxparent.trigger('scroll_element.xdsoft_scroller', [(startTop - (coord.y - start.y)) / (height - parentHeight)]);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\ttimeboxparent.on('touchend touchcancel', function () {\n\t\t\t\t\tstart = false;\n\t\t\t\t\tstartTop = 0;\n\t\t\t\t});\n\t\t\t}\n\t\t\ttimeboxparent.trigger('resize_scroll.xdsoft_scroller', [percent]);\n\t\t});\n\t};\n\n\t$.fn.datetimepicker = function (opt, opt2) {\n\t\tvar result = this,\n\t\t\tKEY0 = 48,\n\t\t\tKEY9 = 57,\n\t\t\t_KEY0 = 96,\n\t\t\t_KEY9 = 105,\n\t\t\tCTRLKEY = 17,\n\t\t\tDEL = 46,\n\t\t\tENTER = 13,\n\t\t\tESC = 27,\n\t\t\tBACKSPACE = 8,\n\t\t\tARROWLEFT = 37,\n\t\t\tARROWUP = 38,\n\t\t\tARROWRIGHT = 39,\n\t\t\tARROWDOWN = 40,\n\t\t\tTAB = 9,\n\t\t\tF5 = 116,\n\t\t\tAKEY = 65,\n\t\t\tCKEY = 67,\n\t\t\tVKEY = 86,\n\t\t\tZKEY = 90,\n\t\t\tYKEY = 89,\n\t\t\tctrlDown\t=\tfalse,\n\t\t\toptions = ($.isPlainObject(opt) || !opt) ? $.extend(true, {}, default_options, opt) : $.extend(true, {}, default_options),\n\n\t\t\tlazyInitTimer = 0,\n\t\t\tcreateDateTimePicker,\n\t\t\tdestroyDateTimePicker,\n\n\t\t\tlazyInit = function (input) {\n\t\t\t\tinput\n\t\t\t\t\t.on('open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart', function initOnActionCallback() {\n\t\t\t\t\t\tif (input.is(':disabled') || input.data('xdsoft_datetimepicker')) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tclearTimeout(lazyInitTimer);\n\t\t\t\t\t\tlazyInitTimer = setTimeout(function () {\n\n\t\t\t\t\t\t\tif (!input.data('xdsoft_datetimepicker')) {\n\t\t\t\t\t\t\t\tcreateDateTimePicker(input);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tinput\n\t\t\t\t\t\t\t\t.off('open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart', initOnActionCallback)\n\t\t\t\t\t\t\t\t.trigger('open.xdsoft');\n\t\t\t\t\t\t}, 100);\n\t\t\t\t\t});\n\t\t\t};\n\n\t\tcreateDateTimePicker = function (input) {\n\t\t\tvar datetimepicker = $('<div class=\"xdsoft_datetimepicker xdsoft_noselect\"></div>'),\n\t\t\t\txdsoft_copyright = $('<div class=\"xdsoft_copyright\"><a target=\"_blank\" href=\"http://xdsoft.net/jqplugins/datetimepicker/\">xdsoft.net</a></div>'),\n\t\t\t\tdatepicker = $('<div class=\"xdsoft_datepicker active\"></div>'),\n\t\t\t\tmonth_picker = $('<div class=\"xdsoft_monthpicker\"><button type=\"button\" class=\"xdsoft_prev\"></button><button type=\"button\" class=\"xdsoft_today_button\"></button>' +\n\t\t\t\t\t'<div class=\"xdsoft_label xdsoft_month\"><span></span><i></i></div>' +\n\t\t\t\t\t'<div class=\"xdsoft_label xdsoft_year\"><span></span><i></i></div>' +\n\t\t\t\t\t'<button type=\"button\" class=\"xdsoft_next\"></button></div>'),\n\t\t\t\tcalendar = $('<div class=\"xdsoft_calendar\"></div>'),\n\t\t\t\ttimepicker = $('<div class=\"xdsoft_timepicker active\"><button type=\"button\" class=\"xdsoft_prev\"></button><div class=\"xdsoft_time_box\"></div><button type=\"button\" class=\"xdsoft_next\"></button></div>'),\n\t\t\t\ttimeboxparent = timepicker.find('.xdsoft_time_box').eq(0),\n\t\t\t\ttimebox = $('<div class=\"xdsoft_time_variant\"></div>'),\n\t\t\t\tapplyButton = $('<button type=\"button\" class=\"xdsoft_save_selected blue-gradient-button\">Save Selected</button>'),\n\n\t\t\t\tmonthselect = $('<div class=\"xdsoft_select xdsoft_monthselect\"><div></div></div>'),\n\t\t\t\tyearselect = $('<div class=\"xdsoft_select xdsoft_yearselect\"><div></div></div>'),\n\t\t\t\ttriggerAfterOpen = false,\n\t\t\t\tXDSoft_datetime,\n\n\t\t\t\txchangeTimer,\n\t\t\t\ttimerclick,\n\t\t\t\tcurrent_time_index,\n\t\t\t\tsetPos,\n\t\t\t\ttimer = 0,\n\t\t\t\t_xdsoft_datetime,\n\t\t\t\tforEachAncestorOf;\n\n\t\t\tif (options.id) {\n\t\t\t\tdatetimepicker.attr('id', options.id);\n\t\t\t}\n\t\t\tif (options.style) {\n\t\t\t\tdatetimepicker.attr('style', options.style);\n\t\t\t}\n\t\t\tif (options.weeks) {\n\t\t\t\tdatetimepicker.addClass('xdsoft_showweeks');\n\t\t\t}\n\t\t\tif (options.rtl) {\n\t\t\t\tdatetimepicker.addClass('xdsoft_rtl');\n\t\t\t}\n\n\t\t\tdatetimepicker.addClass('xdsoft_' + options.theme);\n\t\t\tdatetimepicker.addClass(options.className);\n\n\t\t\tmonth_picker\n\t\t\t\t.find('.xdsoft_month span')\n\t\t\t\t.after(monthselect);\n\t\t\tmonth_picker\n\t\t\t\t.find('.xdsoft_year span')\n\t\t\t\t.after(yearselect);\n\n\t\t\tmonth_picker\n\t\t\t\t.find('.xdsoft_month,.xdsoft_year')\n\t\t\t\t.on('touchstart mousedown.xdsoft', function (event) {\n\t\t\t\t\tvar select = $(this).find('.xdsoft_select').eq(0),\n\t\t\t\t\t\tval = 0,\n\t\t\t\t\t\ttop = 0,\n\t\t\t\t\t\tvisible = select.is(':visible'),\n\t\t\t\t\t\titems,\n\t\t\t\t\t\ti;\n\n\t\t\t\t\tmonth_picker\n\t\t\t\t\t\t.find('.xdsoft_select')\n\t\t\t\t\t\t.hide();\n\t\t\t\t\tif (_xdsoft_datetime.currentTime) {\n\t\t\t\t\t\tval = _xdsoft_datetime.currentTime[$(this).hasClass('xdsoft_month') ? 'getMonth' : 'getFullYear']();\n\t\t\t\t\t}\n\n\t\t\t\t\tselect[visible ? 'hide' : 'show']();\n\t\t\t\t\tfor (items = select.find('div.xdsoft_option'), i = 0; i < items.length; i += 1) {\n\t\t\t\t\t\tif (items.eq(i).data('value') === val) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttop += items[0].offsetHeight;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tselect.xdsoftScroller(options, top / (select.children()[0].offsetHeight - (select[0].clientHeight)));\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\n\t\t\tvar handleTouchMoved = function (event) {\n\t\t\t\tthis.touchStartPosition = this.touchStartPosition || event.originalEvent.touches[0]\n\t\t\t\tvar touchPosition = event.originalEvent.touches[0]\n\t\t\t\tvar xMovement = Math.abs(this.touchStartPosition.clientX - touchPosition.clientX)\n\t\t\t\tvar yMovement = Math.abs(this.touchStartPosition.clientY - touchPosition.clientY)\n\t\t\t\tvar distance = Math.sqrt(xMovement * xMovement + yMovement * yMovement)\n\t\t\t\tif(distance > options.touchMovedThreshold) {\n\t\t\t\t\tthis.touchMoved = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmonth_picker\n\t\t\t\t.find('.xdsoft_select')\n\t\t\t\t.xdsoftScroller(options)\n\t\t\t\t.on('touchstart mousedown.xdsoft', function (event) {\n\t\t\t\t\tthis.touchMoved = false;\n\t\t\t\t\tthis.touchStartPosition = event.originalEvent.touches[0]\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t})\n\t\t\t\t.on('touchmove', '.xdsoft_option', handleTouchMoved)\n\t\t\t\t.on('touchend mousedown.xdsoft', '.xdsoft_option', function () {\n\t\t\t\t\tif (!this.touchMoved) {\n\t\t\t\t\t\tif (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) {\n\t\t\t\t\t\t\t_xdsoft_datetime.currentTime = _xdsoft_datetime.now();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar year = _xdsoft_datetime.currentTime.getFullYear();\n\t\t\t\t\t\tif (_xdsoft_datetime && _xdsoft_datetime.currentTime) {\n\t\t\t\t\t\t\t_xdsoft_datetime.currentTime[$(this).parent().parent().hasClass('xdsoft_monthselect') ? 'setMonth' : 'setFullYear']($(this).data('value'));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$(this).parent().parent().hide();\n\n\t\t\t\t\t\tdatetimepicker.trigger('xchange.xdsoft');\n\t\t\t\t\t\tif (options.onChangeMonth && $.isFunction(options.onChangeMonth)) {\n\t\t\t\t\t\t\toptions.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (year !== _xdsoft_datetime.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) {\n\t\t\t\t\t\t\toptions.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\tdatetimepicker.getValue = function () {\n\t\t\t\treturn _xdsoft_datetime.getCurrentTime();\n\t\t\t};\n\n\t\t\tdatetimepicker.setOptions = function (_options) {\n\t\t\t\tvar highlightedDates = {};\n\n\t\t\t\toptions = $.extend(true, {}, options, _options);\n\n\t\t\t\tif (_options.allowTimes && $.isArray(_options.allowTimes) && _options.allowTimes.length) {\n\t\t\t\t\toptions.allowTimes = $.extend(true, [], _options.allowTimes);\n\t\t\t\t}\n\n\t\t\t\tif (_options.weekends && $.isArray(_options.weekends) && _options.weekends.length) {\n\t\t\t\t\toptions.weekends = $.extend(true, [], _options.weekends);\n\t\t\t\t}\n\n\t\t\t\tif (_options.allowDates && $.isArray(_options.allowDates) && _options.allowDates.length) {\n\t\t\t\t\toptions.allowDates = $.extend(true, [], _options.allowDates);\n\t\t\t\t}\n\n\t\t\t\tif (_options.allowDateRe && Object.prototype.toString.call(_options.allowDateRe)===\"[object String]\") {\n\t\t\t\t\toptions.allowDateRe = new RegExp(_options.allowDateRe);\n\t\t\t\t}\n\n\t\t\t\tif (_options.highlightedDates && $.isArray(_options.highlightedDates) && _options.highlightedDates.length) {\n\t\t\t\t\t$.each(_options.highlightedDates, function (index, value) {\n\t\t\t\t\t\tvar splitData = $.map(value.split(','), $.trim),\n\t\t\t\t\t\t\texDesc,\n\t\t\t\t\t\t\thDate = new HighlightedDate(dateHelper.parseDate(splitData[0], options.formatDate), splitData[1], splitData[2]), // date, desc, style\n\t\t\t\t\t\t\tkeyDate = dateHelper.formatDate(hDate.date, options.formatDate);\n\t\t\t\t\t\tif (highlightedDates[keyDate] !== undefined) {\n\t\t\t\t\t\t\texDesc = highlightedDates[keyDate].desc;\n\t\t\t\t\t\t\tif (exDesc && exDesc.length && hDate.desc && hDate.desc.length) {\n\t\t\t\t\t\t\t\thighlightedDates[keyDate].desc = exDesc + \"\\n\" + hDate.desc;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thighlightedDates[keyDate] = hDate;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\toptions.highlightedDates = $.extend(true, [], highlightedDates);\n\t\t\t\t}\n\n\t\t\t\tif (_options.highlightedPeriods && $.isArray(_options.highlightedPeriods) && _options.highlightedPeriods.length) {\n\t\t\t\t\thighlightedDates = $.extend(true, [], options.highlightedDates);\n\t\t\t\t\t$.each(_options.highlightedPeriods, function (index, value) {\n\t\t\t\t\t\tvar dateTest, // start date\n\t\t\t\t\t\t\tdateEnd,\n\t\t\t\t\t\t\tdesc,\n\t\t\t\t\t\t\thDate,\n\t\t\t\t\t\t\tkeyDate,\n\t\t\t\t\t\t\texDesc,\n\t\t\t\t\t\t\tstyle;\n\t\t\t\t\t\tif ($.isArray(value)) {\n\t\t\t\t\t\t\tdateTest = value[0];\n\t\t\t\t\t\t\tdateEnd = value[1];\n\t\t\t\t\t\t\tdesc = value[2];\n\t\t\t\t\t\t\tstyle = value[3];\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tvar splitData = $.map(value.split(','), $.trim);\n\t\t\t\t\t\t\tdateTest = dateHelper.parseDate(splitData[0], options.formatDate);\n\t\t\t\t\t\t\tdateEnd = dateHelper.parseDate(splitData[1], options.formatDate);\n\t\t\t\t\t\t\tdesc = splitData[2];\n\t\t\t\t\t\t\tstyle = splitData[3];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\twhile (dateTest <= dateEnd) {\n\t\t\t\t\t\t\thDate = new HighlightedDate(dateTest, desc, style);\n\t\t\t\t\t\t\tkeyDate = dateHelper.formatDate(dateTest, options.formatDate);\n\t\t\t\t\t\t\tdateTest.setDate(dateTest.getDate() + 1);\n\t\t\t\t\t\t\tif (highlightedDates[keyDate] !== undefined) {\n\t\t\t\t\t\t\t\texDesc = highlightedDates[keyDate].desc;\n\t\t\t\t\t\t\t\tif (exDesc && exDesc.length && hDate.desc && hDate.desc.length) {\n\t\t\t\t\t\t\t\t\thighlightedDates[keyDate].desc = exDesc + \"\\n\" + hDate.desc;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\thighlightedDates[keyDate] = hDate;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\toptions.highlightedDates = $.extend(true, [], highlightedDates);\n\t\t\t\t}\n\n\t\t\t\tif (_options.disabledDates && $.isArray(_options.disabledDates) && _options.disabledDates.length) {\n\t\t\t\t\toptions.disabledDates = $.extend(true, [], _options.disabledDates);\n\t\t\t\t}\n\n\t\t\t\tif (_options.disabledWeekDays && $.isArray(_options.disabledWeekDays) && _options.disabledWeekDays.length) {\n\t\t\t\t\toptions.disabledWeekDays = $.extend(true, [], _options.disabledWeekDays);\n\t\t\t\t}\n\n\t\t\t\tif ((options.open || options.opened) && (!options.inline)) {\n\t\t\t\t\tinput.trigger('open.xdsoft');\n\t\t\t\t}\n\n\t\t\t\tif (options.inline) {\n\t\t\t\t\ttriggerAfterOpen = true;\n\t\t\t\t\tdatetimepicker.addClass('xdsoft_inline');\n\t\t\t\t\tinput.after(datetimepicker).hide();\n\t\t\t\t}\n\n\t\t\t\tif (options.inverseButton) {\n\t\t\t\t\toptions.next = 'xdsoft_prev';\n\t\t\t\t\toptions.prev = 'xdsoft_next';\n\t\t\t\t}\n\n\t\t\t\tif (options.datepicker) {\n\t\t\t\t\tdatepicker.addClass('active');\n\t\t\t\t} else {\n\t\t\t\t\tdatepicker.removeClass('active');\n\t\t\t\t}\n\n\t\t\t\tif (options.timepicker) {\n\t\t\t\t\ttimepicker.addClass('active');\n\t\t\t\t} else {\n\t\t\t\t\ttimepicker.removeClass('active');\n\t\t\t\t}\n\n\t\t\t\tif (options.value) {\n\t\t\t\t\t_xdsoft_datetime.setCurrentTime(options.value);\n\t\t\t\t\tif (input && input.val) {\n\t\t\t\t\t\tinput.val(_xdsoft_datetime.str);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (isNaN(options.dayOfWeekStart)) {\n\t\t\t\t\toptions.dayOfWeekStart = 0;\n\t\t\t\t} else {\n\t\t\t\t\toptions.dayOfWeekStart = parseInt(options.dayOfWeekStart, 10) % 7;\n\t\t\t\t}\n\n\t\t\t\tif (!options.timepickerScrollbar) {\n\t\t\t\t\ttimeboxparent.xdsoftScroller(options, 'hide');\n\t\t\t\t}\n\n\t\t\t\tif (options.minDate && /^[\\+\\-](.*)$/.test(options.minDate)) {\n\t\t\t\t\toptions.minDate = dateHelper.formatDate(_xdsoft_datetime.strToDateTime(options.minDate), options.formatDate);\n\t\t\t\t}\n\n\t\t\t\tif (options.maxDate &&  /^[\\+\\-](.*)$/.test(options.maxDate)) {\n\t\t\t\t\toptions.maxDate = dateHelper.formatDate(_xdsoft_datetime.strToDateTime(options.maxDate), options.formatDate);\n\t\t\t\t}\n\n                if (options.minDateTime &&  /^\\+(.*)$/.test(options.minDateTime)) {\n                \toptions.minDateTime = _xdsoft_datetime.strToDateTime(options.minDateTime).dateFormat(options.formatDate);\n                }\n\n\t\t\t\tapplyButton.toggle(options.showApplyButton);\n\n\t\t\t\tmonth_picker\n\t\t\t\t\t.find('.xdsoft_today_button')\n\t\t\t\t\t.css('visibility', !options.todayButton ? 'hidden' : 'visible');\n\n\t\t\t\tmonth_picker\n\t\t\t\t\t.find('.' + options.prev)\n\t\t\t\t\t.css('visibility', !options.prevButton ? 'hidden' : 'visible');\n\n\t\t\t\tmonth_picker\n\t\t\t\t\t.find('.' + options.next)\n\t\t\t\t\t.css('visibility', !options.nextButton ? 'hidden' : 'visible');\n\n\t\t\t\tsetMask(options);\n\n\t\t\t\tif (options.validateOnBlur) {\n\t\t\t\t\tinput\n\t\t\t\t\t\t.off('blur.xdsoft')\n\t\t\t\t\t\t.on('blur.xdsoft', function () {\n\t\t\t\t\t\t\tif (options.allowBlank && (!$.trim($(this).val()).length ||\n\t\t\t\t\t\t\t\t\t(typeof options.mask === \"string\" && $.trim($(this).val()) === options.mask.replace(/[0-9]/g, '_')))) {\n\t\t\t\t\t\t\t\t$(this).val(null);\n\t\t\t\t\t\t\t\tdatetimepicker.data('xdsoft_datetime').empty();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tvar d = dateHelper.parseDate($(this).val(), options.format);\n\t\t\t\t\t\t\t\tif (d) { // parseDate() may skip some invalid parts like date or time, so make it clear for user: show parsed date/time\n\t\t\t\t\t\t\t\t\t$(this).val(dateHelper.formatDate(d, options.format));\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tvar splittedHours   = +([$(this).val()[0], $(this).val()[1]].join('')),\n\t\t\t\t\t\t\t\t\t\tsplittedMinutes = +([$(this).val()[2], $(this).val()[3]].join(''));\n\n\t\t\t\t\t\t\t\t\t// parse the numbers as 0312 => 03:12\n\t\t\t\t\t\t\t\t\tif (!options.datepicker && options.timepicker && splittedHours >= 0 && splittedHours < 24 && splittedMinutes >= 0 && splittedMinutes < 60) {\n\t\t\t\t\t\t\t\t\t\t$(this).val([splittedHours, splittedMinutes].map(function (item) {\n\t\t\t\t\t\t\t\t\t\t\treturn item > 9 ? item : '0' + item;\n\t\t\t\t\t\t\t\t\t\t}).join(':'));\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t$(this).val(dateHelper.formatDate(_xdsoft_datetime.now(), options.format));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdatetimepicker.data('xdsoft_datetime').setCurrentTime($(this).val());\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tdatetimepicker.trigger('changedatetime.xdsoft');\n\t\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\toptions.dayOfWeekStartPrev = (options.dayOfWeekStart === 0) ? 6 : options.dayOfWeekStart - 1;\n\n\t\t\t\tdatetimepicker\n\t\t\t\t\t.trigger('xchange.xdsoft')\n\t\t\t\t\t.trigger('afterOpen.xdsoft');\n\t\t\t};\n\n\t\t\tdatetimepicker\n\t\t\t\t.data('options', options)\n\t\t\t\t.on('touchstart mousedown.xdsoft', function (event) {\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tyearselect.hide();\n\t\t\t\t\tmonthselect.hide();\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\n\t\t\t//scroll_element = timepicker.find('.xdsoft_time_box');\n\t\t\ttimeboxparent.append(timebox);\n\t\t\ttimeboxparent.xdsoftScroller(options);\n\n\t\t\tdatetimepicker.on('afterOpen.xdsoft', function () {\n\t\t\t\ttimeboxparent.xdsoftScroller(options);\n\t\t\t});\n\n\t\t\tdatetimepicker\n\t\t\t\t.append(datepicker)\n\t\t\t\t.append(timepicker);\n\n\t\t\tif (options.withoutCopyright !== true) {\n\t\t\t\tdatetimepicker\n\t\t\t\t\t.append(xdsoft_copyright);\n\t\t\t}\n\n\t\t\tdatepicker\n\t\t\t\t.append(month_picker)\n\t\t\t\t.append(calendar)\n\t\t\t\t.append(applyButton);\n\n\t\t\t$(options.parentID)\n\t\t\t\t.append(datetimepicker);\n\n\t\t\tXDSoft_datetime = function () {\n\t\t\t\tvar _this = this;\n\t\t\t\t_this.now = function (norecursion) {\n\t\t\t\t\tvar d = new Date(),\n\t\t\t\t\t\tdate,\n\t\t\t\t\t\ttime;\n\n\t\t\t\t\tif (!norecursion && options.defaultDate) {\n\t\t\t\t\t\tdate = _this.strToDateTime(options.defaultDate);\n\t\t\t\t\t\td.setFullYear(date.getFullYear());\n\t\t\t\t\t\td.setMonth(date.getMonth());\n\t\t\t\t\t\td.setDate(date.getDate());\n\t\t\t\t\t}\n\n\t\t\t\t\td.setFullYear(d.getFullYear());\n\n\t\t\t\t\tif (!norecursion && options.defaultTime) {\n\t\t\t\t\t\ttime = _this.strtotime(options.defaultTime);\n\t\t\t\t\t\td.setHours(time.getHours());\n\t\t\t\t\t\td.setMinutes(time.getMinutes());\n\t\t\t\t\t\td.setSeconds(time.getSeconds());\n\t\t\t\t\t\td.setMilliseconds(time.getMilliseconds());\n\t\t\t\t\t}\n\t\t\t\t\treturn d;\n\t\t\t\t};\n\n\t\t\t\t_this.isValidDate = function (d) {\n\t\t\t\t\tif (Object.prototype.toString.call(d) !== \"[object Date]\") {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn !isNaN(d.getTime());\n\t\t\t\t};\n\n\t\t\t\t_this.setCurrentTime = function (dTime, requireValidDate) {\n\t\t\t\t\tif (typeof dTime === 'string') {\n\t\t\t\t\t\t_this.currentTime = _this.strToDateTime(dTime);\n\t\t\t\t\t}\n\t\t\t\t\telse if (_this.isValidDate(dTime)) {\n\t\t\t\t\t\t_this.currentTime = dTime;\n\t\t\t\t\t}\n\t\t\t\t\telse if (!dTime && !requireValidDate && options.allowBlank && !options.inline) {\n\t\t\t\t\t\t_this.currentTime = null;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t_this.currentTime = _this.now();\n\t\t\t\t\t}\n\n\t\t\t\t\tdatetimepicker.trigger('xchange.xdsoft');\n\t\t\t\t};\n\n\t\t\t\t_this.empty = function () {\n\t\t\t\t\t_this.currentTime = null;\n\t\t\t\t};\n\n\t\t\t\t_this.getCurrentTime = function () {\n\t\t\t\t\treturn _this.currentTime;\n\t\t\t\t};\n\n\t\t\t\t_this.nextMonth = function () {\n\n\t\t\t\t\tif (_this.currentTime === undefined || _this.currentTime === null) {\n\t\t\t\t\t\t_this.currentTime = _this.now();\n\t\t\t\t\t}\n\n\t\t\t\t\tvar month = _this.currentTime.getMonth() + 1,\n\t\t\t\t\t\tyear;\n\t\t\t\t\tif (month === 12) {\n\t\t\t\t\t\t_this.currentTime.setFullYear(_this.currentTime.getFullYear() + 1);\n\t\t\t\t\t\tmonth = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\tyear = _this.currentTime.getFullYear();\n\n\t\t\t\t\t_this.currentTime.setDate(\n\t\t\t\t\t\tMath.min(\n\t\t\t\t\t\t\tnew Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(),\n\t\t\t\t\t\t\t_this.currentTime.getDate()\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t_this.currentTime.setMonth(month);\n\n\t\t\t\t\tif (options.onChangeMonth && $.isFunction(options.onChangeMonth)) {\n\t\t\t\t\t\toptions.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'));\n\t\t\t\t\t}\n\n\t\t\t\t\tif (year !== _this.currentTime.getFullYear() && $.isFunction(options.onChangeYear)) {\n\t\t\t\t\t\toptions.onChangeYear.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'));\n\t\t\t\t\t}\n\n\t\t\t\t\tdatetimepicker.trigger('xchange.xdsoft');\n\t\t\t\t\treturn month;\n\t\t\t\t};\n\n\t\t\t\t_this.prevMonth = function () {\n\n\t\t\t\t\tif (_this.currentTime === undefined || _this.currentTime === null) {\n\t\t\t\t\t\t_this.currentTime = _this.now();\n\t\t\t\t\t}\n\n\t\t\t\t\tvar month = _this.currentTime.getMonth() - 1;\n\t\t\t\t\tif (month === -1) {\n\t\t\t\t\t\t_this.currentTime.setFullYear(_this.currentTime.getFullYear() - 1);\n\t\t\t\t\t\tmonth = 11;\n\t\t\t\t\t}\n\t\t\t\t\t_this.currentTime.setDate(\n\t\t\t\t\t\tMath.min(\n\t\t\t\t\t\t\tnew Date(_this.currentTime.getFullYear(), month + 1, 0).getDate(),\n\t\t\t\t\t\t\t_this.currentTime.getDate()\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t_this.currentTime.setMonth(month);\n\t\t\t\t\tif (options.onChangeMonth && $.isFunction(options.onChangeMonth)) {\n\t\t\t\t\t\toptions.onChangeMonth.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'));\n\t\t\t\t\t}\n\t\t\t\t\tdatetimepicker.trigger('xchange.xdsoft');\n\t\t\t\t\treturn month;\n\t\t\t\t};\n\n\t\t\t\t_this.getWeekOfYear = function (datetime) {\n\t\t\t\t\tif (options.onGetWeekOfYear && $.isFunction(options.onGetWeekOfYear)) {\n\t\t\t\t\t\tvar week = options.onGetWeekOfYear.call(datetimepicker, datetime);\n\t\t\t\t\t\tif (typeof week !== 'undefined') {\n\t\t\t\t\t\t\treturn week;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tvar onejan = new Date(datetime.getFullYear(), 0, 1);\n\n\t\t\t\t\t//First week of the year is th one with the first Thursday according to ISO8601\n\t\t\t\t\tif (onejan.getDay() !== 4) {\n\t\t\t\t\t\tonejan.setMonth(0, 1 + ((4 - onejan.getDay()+ 7) % 7));\n\t\t\t\t\t}\n\n\t\t\t\t\treturn Math.ceil((((datetime - onejan) / 86400000) + onejan.getDay() + 1) / 7);\n\t\t\t\t};\n\n\t\t\t\t_this.strToDateTime = function (sDateTime) {\n\t\t\t\t\tvar tmpDate = [], timeOffset, currentTime;\n\n\t\t\t\t\tif (sDateTime && sDateTime instanceof Date && _this.isValidDate(sDateTime)) {\n\t\t\t\t\t\treturn sDateTime;\n\t\t\t\t\t}\n\n\t\t\t\t\ttmpDate = /^([+-]{1})(.*)$/.exec(sDateTime);\n\n\t\t\t\t\tif (tmpDate) {\n\t\t\t\t\t\ttmpDate[2] = dateHelper.parseDate(tmpDate[2], options.formatDate);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (tmpDate  && tmpDate[2]) {\n\t\t\t\t\t\ttimeOffset = tmpDate[2].getTime() - (tmpDate[2].getTimezoneOffset()) * 60000;\n\t\t\t\t\t\tcurrentTime = new Date((_this.now(true)).getTime() + parseInt(tmpDate[1] + '1', 10) * timeOffset);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcurrentTime = sDateTime ? dateHelper.parseDate(sDateTime, options.format) : _this.now();\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!_this.isValidDate(currentTime)) {\n\t\t\t\t\t\tcurrentTime = _this.now();\n\t\t\t\t\t}\n\n\t\t\t\t\treturn currentTime;\n\t\t\t\t};\n\n\t\t\t\t_this.strToDate = function (sDate) {\n\t\t\t\t\tif (sDate && sDate instanceof Date && _this.isValidDate(sDate)) {\n\t\t\t\t\t\treturn sDate;\n\t\t\t\t\t}\n\n\t\t\t\t\tvar currentTime = sDate ? dateHelper.parseDate(sDate, options.formatDate) : _this.now(true);\n\t\t\t\t\tif (!_this.isValidDate(currentTime)) {\n\t\t\t\t\t\tcurrentTime = _this.now(true);\n\t\t\t\t\t}\n\t\t\t\t\treturn currentTime;\n\t\t\t\t};\n\n\t\t\t\t_this.strtotime = function (sTime) {\n\t\t\t\t\tif (sTime && sTime instanceof Date && _this.isValidDate(sTime)) {\n\t\t\t\t\t\treturn sTime;\n\t\t\t\t\t}\n\t\t\t\t\tvar currentTime = sTime ? dateHelper.parseDate(sTime, options.formatTime) : _this.now(true);\n\t\t\t\t\tif (!_this.isValidDate(currentTime)) {\n\t\t\t\t\t\tcurrentTime = _this.now(true);\n\t\t\t\t\t}\n\t\t\t\t\treturn currentTime;\n\t\t\t\t};\n\n\t\t\t\t_this.str = function () {\n\t\t\t\t\tvar format = options.format;\n\t\t\t\t\tif (options.yearOffset) {\n\t\t\t\t\t\tformat = format.replace('Y', _this.currentTime.getFullYear() + options.yearOffset);\n\t\t\t\t\t\tformat = format.replace('y', String(_this.currentTime.getFullYear() + options.yearOffset).substring(2, 4));\n\t\t\t\t\t}\n\t\t\t\t\treturn dateHelper.formatDate(_this.currentTime, format);\n\t\t\t\t};\n\t\t\t\t_this.currentTime = this.now();\n\t\t\t};\n\n\t\t\t_xdsoft_datetime = new XDSoft_datetime();\n\n\t\t\tapplyButton.on('touchend click', function (e) {//pathbrite\n\t\t\t\te.preventDefault();\n\t\t\t\tdatetimepicker.data('changed', true);\n\t\t\t\t_xdsoft_datetime.setCurrentTime(getCurrentValue());\n\t\t\t\tinput.val(_xdsoft_datetime.str());\n\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t});\n\t\t\tmonth_picker\n\t\t\t\t.find('.xdsoft_today_button')\n\t\t\t\t.on('touchend mousedown.xdsoft', function () {\n\t\t\t\t\tdatetimepicker.data('changed', true);\n\t\t\t\t\t_xdsoft_datetime.setCurrentTime(0, true);\n\t\t\t\t\tdatetimepicker.trigger('afterOpen.xdsoft');\n\t\t\t\t}).on('dblclick.xdsoft', function () {\n\t\t\t\tvar currentDate = _xdsoft_datetime.getCurrentTime(), minDate, maxDate;\n\t\t\t\tcurrentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());\n\t\t\t\tminDate = _xdsoft_datetime.strToDate(options.minDate);\n\t\t\t\tminDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());\n\t\t\t\tif (currentDate < minDate) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tmaxDate = _xdsoft_datetime.strToDate(options.maxDate);\n\t\t\t\tmaxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate());\n\t\t\t\tif (currentDate > maxDate) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tinput.val(_xdsoft_datetime.str());\n\t\t\t\tinput.trigger('change');\n\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t});\n\t\t\tmonth_picker\n\t\t\t\t.find('.xdsoft_prev,.xdsoft_next')\n\t\t\t\t.on('touchend mousedown.xdsoft', function () {\n\t\t\t\t\tvar $this = $(this),\n\t\t\t\t\t\ttimer = 0,\n\t\t\t\t\t\tstop = false;\n\n\t\t\t\t\t(function arguments_callee1(v) {\n\t\t\t\t\t\tif ($this.hasClass(options.next)) {\n\t\t\t\t\t\t\t_xdsoft_datetime.nextMonth();\n\t\t\t\t\t\t} else if ($this.hasClass(options.prev)) {\n\t\t\t\t\t\t\t_xdsoft_datetime.prevMonth();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (options.monthChangeSpinner) {\n\t\t\t\t\t\t\tif (!stop) {\n\t\t\t\t\t\t\t\ttimer = setTimeout(arguments_callee1, v || 100);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}(500));\n\n\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow]).on('touchend mouseup.xdsoft', function arguments_callee2() {\n\t\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\t\tstop = true;\n\t\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow]).off('touchend mouseup.xdsoft', arguments_callee2);\n\t\t\t\t\t});\n\t\t\t\t});\n\n\t\t\ttimepicker\n\t\t\t\t.find('.xdsoft_prev,.xdsoft_next')\n\t\t\t\t.on('touchend mousedown.xdsoft', function () {\n\t\t\t\t\tvar $this = $(this),\n\t\t\t\t\t\ttimer = 0,\n\t\t\t\t\t\tstop = false,\n\t\t\t\t\t\tperiod = 110;\n\t\t\t\t\t(function arguments_callee4(v) {\n\t\t\t\t\t\tvar pheight = timeboxparent[0].clientHeight,\n\t\t\t\t\t\t\theight = timebox[0].offsetHeight,\n\t\t\t\t\t\t\ttop = Math.abs(parseInt(timebox.css('marginTop'), 10));\n\t\t\t\t\t\tif ($this.hasClass(options.next) && (height - pheight) - options.timeHeightInTimePicker >= top) {\n\t\t\t\t\t\t\ttimebox.css('marginTop', '-' + (top + options.timeHeightInTimePicker) + 'px');\n\t\t\t\t\t\t} else if ($this.hasClass(options.prev) && top - options.timeHeightInTimePicker >= 0) {\n\t\t\t\t\t\t\ttimebox.css('marginTop', '-' + (top - options.timeHeightInTimePicker) + 'px');\n\t\t\t\t\t\t}\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Fixed bug:\n\t\t\t\t\t\t * When using css3 transition, it will cause a bug that you cannot scroll the timepicker list.\n\t\t\t\t\t\t * The reason is that the transition-duration time, if you set it to 0, all things fine, otherwise, this\n\t\t\t\t\t\t * would cause a bug when you use jquery.css method.\n\t\t\t\t\t\t * Let's say: * { transition: all .5s ease; }\n\t\t\t\t\t\t * jquery timebox.css('marginTop') will return the original value which is before you clicking the next/prev button,\n\t\t\t\t\t\t * meanwhile the timebox[0].style.marginTop will return the right value which is after you clicking the\n\t\t\t\t\t\t * next/prev button.\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * What we should do:\n\t\t\t\t\t\t * Replace timebox.css('marginTop') with timebox[0].style.marginTop.\n\t\t\t\t\t\t */\n\t\t\t\t\t\ttimeboxparent.trigger('scroll_element.xdsoft_scroller', [Math.abs(parseInt(timebox[0].style.marginTop, 10) / (height - pheight))]);\n\t\t\t\t\t\tperiod = (period > 10) ? 10 : period - 10;\n\t\t\t\t\t\tif (!stop) {\n\t\t\t\t\t\t\ttimer = setTimeout(arguments_callee4, v || period);\n\t\t\t\t\t\t}\n\t\t\t\t\t}(500));\n\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow]).on('touchend mouseup.xdsoft', function arguments_callee5() {\n\t\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\t\tstop = true;\n\t\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow])\n\t\t\t\t\t\t\t.off('touchend mouseup.xdsoft', arguments_callee5);\n\t\t\t\t\t});\n\t\t\t\t});\n\n\t\t\txchangeTimer = 0;\n\t\t\t// base handler - generating a calendar and timepicker\n\t\t\tdatetimepicker\n\t\t\t\t.on('xchange.xdsoft', function (event) {\n\t\t\t\t\tclearTimeout(xchangeTimer);\n\t\t\t\t\txchangeTimer = setTimeout(function () {\n\n\t\t\t\t\t\tif (_xdsoft_datetime.currentTime === undefined || _xdsoft_datetime.currentTime === null) {\n\t\t\t\t\t\t\t_xdsoft_datetime.currentTime = _xdsoft_datetime.now();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar table =\t'',\n\t\t\t\t\t\t\tstart = new Date(_xdsoft_datetime.currentTime.getFullYear(), _xdsoft_datetime.currentTime.getMonth(), 1, 12, 0, 0),\n\t\t\t\t\t\t\ti = 0,\n\t\t\t\t\t\t\tj,\n\t\t\t\t\t\t\ttoday = _xdsoft_datetime.now(),\n\t\t\t\t\t\t\tmaxDate = false,\n\t\t\t\t\t\t\tminDate = false,\n                            minDateTime = false,\n\t\t\t\t\t\t\thDate,\n\t\t\t\t\t\t\tday,\n\t\t\t\t\t\t\td,\n\t\t\t\t\t\t\ty,\n\t\t\t\t\t\t\tm,\n\t\t\t\t\t\t\tw,\n\t\t\t\t\t\t\tclasses = [],\n\t\t\t\t\t\t\tcustomDateSettings,\n\t\t\t\t\t\t\tnewRow = true,\n\t\t\t\t\t\t\ttime = '',\n\t\t\t\t\t\t\th,\n\t\t\t\t\t\t\tline_time,\n\t\t\t\t\t\t\tdescription;\n\n\t\t\t\t\t\twhile (start.getDay() !== options.dayOfWeekStart) {\n\t\t\t\t\t\t\tstart.setDate(start.getDate() - 1);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttable += '<table><thead><tr>';\n\n\t\t\t\t\t\tif (options.weeks) {\n\t\t\t\t\t\t\ttable += '<th></th>';\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (j = 0; j < 7; j += 1) {\n\t\t\t\t\t\t\ttable += '<th>' + options.i18n[globalLocale].dayOfWeekShort[(j + options.dayOfWeekStart) % 7] + '</th>';\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttable += '</tr></thead>';\n\t\t\t\t\t\ttable += '<tbody>';\n\n\t\t\t\t\t\tif (options.maxDate !== false) {\n\t\t\t\t\t\t\tmaxDate = _xdsoft_datetime.strToDate(options.maxDate);\n\t\t\t\t\t\t\tmaxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (options.minDate !== false) {\n\t\t\t\t\t\t\tminDate = _xdsoft_datetime.strToDate(options.minDate);\n\t\t\t\t\t\t\tminDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());\n\t\t\t\t\t\t}\n\n                        if (options.minDateTime !== false) {\n\t\t\t\t\t\t\tminDateTime = _xdsoft_datetime.strToDate(options.minDateTime);\n\t\t\t\t\t\t\tminDateTime = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), minDateTime.getHours(), minDateTime.getMinutes(), minDateTime.getSeconds());\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\twhile (i < _xdsoft_datetime.currentTime.countDaysInMonth() || start.getDay() !== options.dayOfWeekStart || _xdsoft_datetime.currentTime.getMonth() === start.getMonth()) {\n\t\t\t\t\t\t\tclasses = [];\n\t\t\t\t\t\t\ti += 1;\n\n\t\t\t\t\t\t\tday = start.getDay();\n\t\t\t\t\t\t\td = start.getDate();\n\t\t\t\t\t\t\ty = start.getFullYear();\n\t\t\t\t\t\t\tm = start.getMonth();\n\t\t\t\t\t\t\tw = _xdsoft_datetime.getWeekOfYear(start);\n\t\t\t\t\t\t\tdescription = '';\n\n\t\t\t\t\t\t\tclasses.push('xdsoft_date');\n\n\t\t\t\t\t\t\tif (options.beforeShowDay && $.isFunction(options.beforeShowDay.call)) {\n\t\t\t\t\t\t\t\tcustomDateSettings = options.beforeShowDay.call(datetimepicker, start);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcustomDateSettings = null;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif(options.allowDateRe && Object.prototype.toString.call(options.allowDateRe) === \"[object RegExp]\"){\n\t\t\t\t\t\t\t\tif(!options.allowDateRe.test(dateHelper.formatDate(start, options.formatDate))){\n\t\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif(options.allowDates && options.allowDates.length>0){\n\t\t\t\t\t\t\t\tif(options.allowDates.indexOf(dateHelper.formatDate(start, options.formatDate)) === -1){\n\t\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif ((maxDate !== false && start > maxDate) || (minDateTime !== false && start < minDateTime)  || (minDate !== false && start < minDate) || (customDateSettings && customDateSettings[0] === false)) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (options.disabledDates.indexOf(dateHelper.formatDate(start, options.formatDate)) !== -1) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (options.disabledWeekDays.indexOf(day) !== -1) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif (input.is('[disabled]')) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (customDateSettings && customDateSettings[1] !== \"\") {\n\t\t\t\t\t\t\t\tclasses.push(customDateSettings[1]);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (_xdsoft_datetime.currentTime.getMonth() !== m) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_other_month');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ((options.defaultSelect || datetimepicker.data('changed')) && dateHelper.formatDate(_xdsoft_datetime.currentTime, options.formatDate) === dateHelper.formatDate(start, options.formatDate)) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_current');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (dateHelper.formatDate(today, options.formatDate) === dateHelper.formatDate(start, options.formatDate)) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_today');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (start.getDay() === 0 || start.getDay() === 6 || options.weekends.indexOf(dateHelper.formatDate(start, options.formatDate)) !== -1) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_weekend');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (options.highlightedDates[dateHelper.formatDate(start, options.formatDate)] !== undefined) {\n\t\t\t\t\t\t\t\thDate = options.highlightedDates[dateHelper.formatDate(start, options.formatDate)];\n\t\t\t\t\t\t\t\tclasses.push(hDate.style === undefined ? 'xdsoft_highlighted_default' : hDate.style);\n\t\t\t\t\t\t\t\tdescription = hDate.desc === undefined ? '' : hDate.desc;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (options.beforeShowDay && $.isFunction(options.beforeShowDay)) {\n\t\t\t\t\t\t\t\tclasses.push(options.beforeShowDay(start));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (newRow) {\n\t\t\t\t\t\t\t\ttable += '<tr>';\n\t\t\t\t\t\t\t\tnewRow = false;\n\t\t\t\t\t\t\t\tif (options.weeks) {\n\t\t\t\t\t\t\t\t\ttable += '<th>' + w + '</th>';\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttable += '<td data-date=\"' + d + '\" data-month=\"' + m + '\" data-year=\"' + y + '\"' + ' class=\"xdsoft_date xdsoft_day_of_week' + start.getDay() + ' ' + classes.join(' ') + '\" title=\"' + description + '\">' +\n\t\t\t\t\t\t\t\t'<div>' + d + '</div>' +\n\t\t\t\t\t\t\t\t'</td>';\n\n\t\t\t\t\t\t\tif (start.getDay() === options.dayOfWeekStartPrev) {\n\t\t\t\t\t\t\t\ttable += '</tr>';\n\t\t\t\t\t\t\t\tnewRow = true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tstart.setDate(d + 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttable += '</tbody></table>';\n\n\t\t\t\t\t\tcalendar.html(table);\n\n\t\t\t\t\t\tmonth_picker.find('.xdsoft_label span').eq(0).text(options.i18n[globalLocale].months[_xdsoft_datetime.currentTime.getMonth()]);\n\t\t\t\t\t\tmonth_picker.find('.xdsoft_label span').eq(1).text(_xdsoft_datetime.currentTime.getFullYear() + options.yearOffset);\n\n\t\t\t\t\t\t// generate timebox\n\t\t\t\t\t\ttime = '';\n\t\t\t\t\t\th = '';\n\t\t\t\t\t\tm = '';\n\n\t\t\t\t\t\tline_time = function line_time(h, m) {\n\t\t\t\t\t\t\tvar now = _xdsoft_datetime.now(), optionDateTime, current_time,\n\t\t\t\t\t\t\t\tisALlowTimesInit = options.allowTimes && $.isArray(options.allowTimes) && options.allowTimes.length;\n\t\t\t\t\t\t\tnow.setHours(h);\n\t\t\t\t\t\t\th = parseInt(now.getHours(), 10);\n\t\t\t\t\t\t\tnow.setMinutes(m);\n\t\t\t\t\t\t\tm = parseInt(now.getMinutes(), 10);\n\t\t\t\t\t\t\toptionDateTime = new Date(_xdsoft_datetime.currentTime);\n\t\t\t\t\t\t\toptionDateTime.setHours(h);\n\t\t\t\t\t\t\toptionDateTime.setMinutes(m);\n\t\t\t\t\t\t\tclasses = [];\n\t\t\t\t\t\t\tif ((options.minDateTime !== false && options.minDateTime > optionDateTime) || (options.maxTime !== false && _xdsoft_datetime.strtotime(options.maxTime).getTime() < now.getTime()) || (options.minTime !== false && _xdsoft_datetime.strtotime(options.minTime).getTime() > now.getTime())) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t} else if ((options.minDateTime !== false && options.minDateTime > optionDateTime) || ((options.disabledMinTime !== false && now.getTime() > _xdsoft_datetime.strtotime(options.disabledMinTime).getTime()) && (options.disabledMaxTime !== false && now.getTime() < _xdsoft_datetime.strtotime(options.disabledMaxTime).getTime()))) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t} else if (input.is('[disabled]')) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_disabled');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcurrent_time = new Date(_xdsoft_datetime.currentTime);\n\t\t\t\t\t\t\tcurrent_time.setHours(parseInt(_xdsoft_datetime.currentTime.getHours(), 10));\n\n\t\t\t\t\t\t\tif (!isALlowTimesInit) {\n\t\t\t\t\t\t\t\tcurrent_time.setMinutes(Math[options.roundTime](_xdsoft_datetime.currentTime.getMinutes() / options.step) * options.step);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ((options.initTime || options.defaultSelect || datetimepicker.data('changed')) && current_time.getHours() === parseInt(h, 10) && ((!isALlowTimesInit && options.step > 59) || current_time.getMinutes() === parseInt(m, 10))) {\n\t\t\t\t\t\t\t\tif (options.defaultSelect || datetimepicker.data('changed')) {\n\t\t\t\t\t\t\t\t\tclasses.push('xdsoft_current');\n\t\t\t\t\t\t\t\t} else if (options.initTime) {\n\t\t\t\t\t\t\t\t\tclasses.push('xdsoft_init_time');\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (parseInt(today.getHours(), 10) === parseInt(h, 10) && parseInt(today.getMinutes(), 10) === parseInt(m, 10)) {\n\t\t\t\t\t\t\t\tclasses.push('xdsoft_today');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttime += '<div class=\"xdsoft_time ' + classes.join(' ') + '\" data-hour=\"' + h + '\" data-minute=\"' + m + '\">' + dateHelper.formatDate(now, options.formatTime) + '</div>';\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tif (!options.allowTimes || !$.isArray(options.allowTimes) || !options.allowTimes.length) {\n\t\t\t\t\t\t\tfor (i = 0, j = 0; i < (options.hours12 ? 12 : 24); i += 1) {\n\t\t\t\t\t\t\t\tfor (j = 0; j < 60; j += options.step) {\n\t\t\t\t\t\t\t\t\th = (i < 10 ? '0' : '') + i;\n\t\t\t\t\t\t\t\t\tm = (j < 10 ? '0' : '') + j;\n\t\t\t\t\t\t\t\t\tline_time(h, m);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfor (i = 0; i < options.allowTimes.length; i += 1) {\n\t\t\t\t\t\t\t\th = _xdsoft_datetime.strtotime(options.allowTimes[i]).getHours();\n\t\t\t\t\t\t\t\tm = _xdsoft_datetime.strtotime(options.allowTimes[i]).getMinutes();\n\t\t\t\t\t\t\t\tline_time(h, m);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttimebox.html(time);\n\n\t\t\t\t\t\topt = '';\n\n\t\t\t\t\t\tfor (i = parseInt(options.yearStart, 10); i <= parseInt(options.yearEnd, 10); i += 1) {\n\t\t\t\t\t\t\topt += '<div class=\"xdsoft_option ' + (_xdsoft_datetime.currentTime.getFullYear() === i ? 'xdsoft_current' : '') + '\" data-value=\"' + i + '\">' + (i + options.yearOffset) + '</div>';\n\t\t\t\t\t\t}\n\t\t\t\t\t\tyearselect.children().eq(0)\n\t\t\t\t\t\t\t.html(opt);\n\n\t\t\t\t\t\tfor (i = parseInt(options.monthStart, 10), opt = ''; i <= parseInt(options.monthEnd, 10); i += 1) {\n\t\t\t\t\t\t\topt += '<div class=\"xdsoft_option ' + (_xdsoft_datetime.currentTime.getMonth() === i ? 'xdsoft_current' : '') + '\" data-value=\"' + i + '\">' + options.i18n[globalLocale].months[i] + '</div>';\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmonthselect.children().eq(0).html(opt);\n\t\t\t\t\t\t$(datetimepicker)\n\t\t\t\t\t\t\t.trigger('generate.xdsoft');\n\t\t\t\t\t}, 10);\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t})\n\t\t\t\t.on('afterOpen.xdsoft', function () {\n\t\t\t\t\tif (options.timepicker) {\n\t\t\t\t\t\tvar classType, pheight, height, top;\n\t\t\t\t\t\tif (timebox.find('.xdsoft_current').length) {\n\t\t\t\t\t\t\tclassType = '.xdsoft_current';\n\t\t\t\t\t\t} else if (timebox.find('.xdsoft_init_time').length) {\n\t\t\t\t\t\t\tclassType = '.xdsoft_init_time';\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (classType) {\n\t\t\t\t\t\t\tpheight = timeboxparent[0].clientHeight;\n\t\t\t\t\t\t\theight = timebox[0].offsetHeight;\n\t\t\t\t\t\t\ttop = timebox.find(classType).index() * options.timeHeightInTimePicker + 1;\n\t\t\t\t\t\t\tif ((height - pheight) < top) {\n\t\t\t\t\t\t\t\ttop = height - pheight;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttimeboxparent.trigger('scroll_element.xdsoft_scroller', [parseInt(top, 10) / (height - pheight)]);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttimeboxparent.trigger('scroll_element.xdsoft_scroller', [0]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\ttimerclick = 0;\n\t\t\tcalendar\n\t\t\t\t.on('touchend click.xdsoft', 'td', function (xdevent) {\n\t\t\t\t\txdevent.stopPropagation();  // Prevents closing of Pop-ups, Modals and Flyouts in Bootstrap\n\t\t\t\t\ttimerclick += 1;\n\t\t\t\t\tvar $this = $(this),\n\t\t\t\t\t\tcurrentTime = _xdsoft_datetime.currentTime;\n\n\t\t\t\t\tif (currentTime === undefined || currentTime === null) {\n\t\t\t\t\t\t_xdsoft_datetime.currentTime = _xdsoft_datetime.now();\n\t\t\t\t\t\tcurrentTime = _xdsoft_datetime.currentTime;\n\t\t\t\t\t}\n\n\t\t\t\t\tif ($this.hasClass('xdsoft_disabled')) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrentTime.setDate(1);\n\t\t\t\t\tcurrentTime.setFullYear($this.data('year'));\n\t\t\t\t\tcurrentTime.setMonth($this.data('month'));\n\t\t\t\t\tcurrentTime.setDate($this.data('date'));\n\n\t\t\t\t\tdatetimepicker.trigger('select.xdsoft', [currentTime]);\n\n\t\t\t\t\tinput.val(_xdsoft_datetime.str());\n\n\t\t\t\t\tif (options.onSelectDate &&\t$.isFunction(options.onSelectDate)) {\n\t\t\t\t\t\toptions.onSelectDate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent);\n\t\t\t\t\t}\n\n\t\t\t\t\tdatetimepicker.data('changed', true);\n\t\t\t\t\tdatetimepicker.trigger('xchange.xdsoft');\n\t\t\t\t\tdatetimepicker.trigger('changedatetime.xdsoft');\n\t\t\t\t\tif ((timerclick > 1 || (options.closeOnDateSelect === true || (options.closeOnDateSelect === false && !options.timepicker))) && !options.inline) {\n\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t}\n\t\t\t\t\tsetTimeout(function () {\n\t\t\t\t\t\ttimerclick = 0;\n\t\t\t\t\t}, 200);\n\t\t\t\t});\n\n\t\t\ttimebox\n\t\t\t\t.on('touchstart', 'div', function (xdevent) {\n\t\t\t\t\tthis.touchMoved = false;\n\t\t\t\t})\n\t\t\t\t.on('touchmove', 'div', handleTouchMoved)\n\t\t\t\t.on('touchend click.xdsoft', 'div', function (xdevent) {\n\t\t\t\t\tif (!this.touchMoved) {\n\t\t\t\t\t\txdevent.stopPropagation();\n\t\t\t\t\t\tvar $this = $(this),\n\t\t\t\t\t\t\tcurrentTime = _xdsoft_datetime.currentTime;\n\n\t\t\t\t\t\tif (currentTime === undefined || currentTime === null) {\n\t\t\t\t\t\t\t_xdsoft_datetime.currentTime = _xdsoft_datetime.now();\n\t\t\t\t\t\t\tcurrentTime = _xdsoft_datetime.currentTime;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ($this.hasClass('xdsoft_disabled')) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTime.setHours($this.data('hour'));\n\t\t\t\t\t\tcurrentTime.setMinutes($this.data('minute'));\n\t\t\t\t\t\tdatetimepicker.trigger('select.xdsoft', [currentTime]);\n\n\t\t\t\t\t\tdatetimepicker.data('input').val(_xdsoft_datetime.str());\n\n\t\t\t\t\t\tif (options.onSelectTime && $.isFunction(options.onSelectTime)) {\n\t\t\t\t\t\t\toptions.onSelectTime.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), xdevent);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdatetimepicker.data('changed', true);\n\t\t\t\t\t\tdatetimepicker.trigger('xchange.xdsoft');\n\t\t\t\t\t\tdatetimepicker.trigger('changedatetime.xdsoft');\n\t\t\t\t\t\tif (options.inline !== true && options.closeOnTimeSelect === true) {\n\t\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\tdatepicker\n\t\t\t\t.on('mousewheel.xdsoft', function (event) {\n\t\t\t\t\tif (!options.scrollMonth) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.deltaY < 0) {\n\t\t\t\t\t\t_xdsoft_datetime.nextMonth();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_xdsoft_datetime.prevMonth();\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\n\t\t\tinput\n\t\t\t\t.on('mousewheel.xdsoft', function (event) {\n\t\t\t\t\tif (!options.scrollInput) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif (!options.datepicker && options.timepicker) {\n\t\t\t\t\t\tcurrent_time_index = timebox.find('.xdsoft_current').length ? timebox.find('.xdsoft_current').eq(0).index() : 0;\n\t\t\t\t\t\tif (current_time_index + event.deltaY >= 0 && current_time_index + event.deltaY < timebox.children().length) {\n\t\t\t\t\t\t\tcurrent_time_index += event.deltaY;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (timebox.children().eq(current_time_index).length) {\n\t\t\t\t\t\t\ttimebox.children().eq(current_time_index).trigger('mousedown');\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tif (options.datepicker && !options.timepicker) {\n\t\t\t\t\t\tdatepicker.trigger(event, [event.deltaY, event.deltaX, event.deltaY]);\n\t\t\t\t\t\tif (input.val) {\n\t\t\t\t\t\t\tinput.val(_xdsoft_datetime.str());\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdatetimepicker.trigger('changedatetime.xdsoft');\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\tdatetimepicker\n\t\t\t\t.on('changedatetime.xdsoft', function (event) {\n\t\t\t\t\tif (options.onChangeDateTime && $.isFunction(options.onChangeDateTime)) {\n\t\t\t\t\t\tvar $input = datetimepicker.data('input');\n\t\t\t\t\t\toptions.onChangeDateTime.call(datetimepicker, _xdsoft_datetime.currentTime, $input, event);\n\t\t\t\t\t\tdelete options.value;\n\t\t\t\t\t\t$input.trigger('change');\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.on('generate.xdsoft', function () {\n\t\t\t\t\tif (options.onGenerate && $.isFunction(options.onGenerate)) {\n\t\t\t\t\t\toptions.onGenerate.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'));\n\t\t\t\t\t}\n\t\t\t\t\tif (triggerAfterOpen) {\n\t\t\t\t\t\tdatetimepicker.trigger('afterOpen.xdsoft');\n\t\t\t\t\t\ttriggerAfterOpen = false;\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.on('click.xdsoft', function (xdevent) {\n\t\t\t\t\txdevent.stopPropagation();\n\t\t\t\t});\n\n\t\t\tcurrent_time_index = 0;\n\n\t\t\t/**\n\t\t\t * Runs the callback for each of the specified node's ancestors.\n\t\t\t *\n\t\t\t * Return FALSE from the callback to stop ascending.\n\t\t\t *\n\t\t\t * @param {DOMNode} node\n\t\t\t * @param {Function} callback\n\t\t\t * @returns {undefined}\n\t\t\t */\n\t\t\tforEachAncestorOf = function (node, callback) {\n\t\t\t\tdo {\n\t\t\t\t\tnode = node.parentNode;\n\n\t\t\t\t\tif (!node || callback(node) === false) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} while (node.nodeName !== 'HTML');\n\t\t\t};\n\n\t\t\t/**\n\t\t\t * Sets the position of the picker.\n\t\t\t *\n\t\t\t * @returns {undefined}\n\t\t\t */\n\t\t\tsetPos = function () {\n\t\t\t\tvar dateInputOffset,\n\t\t\t\t\tdateInputElem,\n\t\t\t\t\tverticalPosition,\n\t\t\t\t\tleft,\n\t\t\t\t\tposition,\n\t\t\t\t\tdatetimepickerElem,\n\t\t\t\t\tdateInputHasFixedAncestor,\n\t\t\t\t\t$dateInput,\n\t\t\t\t\twindowWidth,\n\t\t\t\t\tverticalAnchorEdge,\n\t\t\t\t\tdatetimepickerCss,\n\t\t\t\t\twindowHeight,\n\t\t\t\t\twindowScrollTop;\n\n\t\t\t\t$dateInput = datetimepicker.data('input');\n\t\t\t\tdateInputOffset = $dateInput.offset();\n\t\t\t\tdateInputElem = $dateInput[0];\n\n\t\t\t\tverticalAnchorEdge = 'top';\n\t\t\t\tverticalPosition = (dateInputOffset.top + dateInputElem.offsetHeight) - 1;\n\t\t\t\tleft = dateInputOffset.left;\n\t\t\t\tposition = \"absolute\";\n\n\t\t\t\twindowWidth = $(options.contentWindow).width();\n\t\t\t\twindowHeight = $(options.contentWindow).height();\n\t\t\t\twindowScrollTop = $(options.contentWindow).scrollTop();\n\n\t\t\t\tif ((options.ownerDocument.documentElement.clientWidth - dateInputOffset.left) < datepicker.parent().outerWidth(true)) {\n\t\t\t\t\tvar diff = datepicker.parent().outerWidth(true) - dateInputElem.offsetWidth;\n\t\t\t\t\tleft = left - diff;\n\t\t\t\t}\n\n\t\t\t\tif ($dateInput.parent().css('direction') === 'rtl') {\n\t\t\t\t\tleft -= (datetimepicker.outerWidth() - $dateInput.outerWidth());\n\t\t\t\t}\n\n\t\t\t\tif (options.fixed) {\n\t\t\t\t\tverticalPosition -= windowScrollTop;\n\t\t\t\t\tleft -= $(options.contentWindow).scrollLeft();\n\t\t\t\t\tposition = \"fixed\";\n\t\t\t\t} else {\n\t\t\t\t\tdateInputHasFixedAncestor = false;\n\n\t\t\t\t\tforEachAncestorOf(dateInputElem, function (ancestorNode) {\n\t\t\t\t\t\tif (ancestorNode === null) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (options.contentWindow.getComputedStyle(ancestorNode).getPropertyValue('position') === 'fixed') {\n\t\t\t\t\t\t\tdateInputHasFixedAncestor = true;\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tif (dateInputHasFixedAncestor) {\n\t\t\t\t\t\tposition = 'fixed';\n\n\t\t\t\t\t\t//If the picker won't fit entirely within the viewport then display it above the date input.\n\t\t\t\t\t\tif (verticalPosition + datetimepicker.outerHeight() > windowHeight + windowScrollTop) {\n\t\t\t\t\t\t\tverticalAnchorEdge = 'bottom';\n\t\t\t\t\t\t\tverticalPosition = (windowHeight + windowScrollTop) - dateInputOffset.top;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tverticalPosition -= windowScrollTop;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (verticalPosition + datetimepicker[0].offsetHeight > windowHeight + windowScrollTop) {\n\t\t\t\t\t\t\tverticalPosition = dateInputOffset.top - datetimepicker[0].offsetHeight + 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (verticalPosition < 0) {\n\t\t\t\t\t\tverticalPosition = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (left + dateInputElem.offsetWidth > windowWidth) {\n\t\t\t\t\t\tleft = windowWidth - dateInputElem.offsetWidth;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tdatetimepickerElem = datetimepicker[0];\n\n\t\t\t\tforEachAncestorOf(datetimepickerElem, function (ancestorNode) {\n\t\t\t\t\tvar ancestorNodePosition;\n\n\t\t\t\t\tancestorNodePosition = options.contentWindow.getComputedStyle(ancestorNode).getPropertyValue('position');\n\n\t\t\t\t\tif (ancestorNodePosition === 'relative' && windowWidth >= ancestorNode.offsetWidth) {\n\t\t\t\t\t\tleft = left - ((windowWidth - ancestorNode.offsetWidth) / 2);\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tdatetimepickerCss = {\n\t\t\t\t\tposition: position,\n\t\t\t\t\tleft: left,\n\t\t\t\t\ttop: '',  //Initialize to prevent previous values interfering with new ones.\n\t\t\t\t\tbottom: ''  //Initialize to prevent previous values interfering with new ones.\n\t\t\t\t};\n\n\t\t\t\tdatetimepickerCss[verticalAnchorEdge] = verticalPosition;\n\n\t\t\t\tdatetimepicker.css(datetimepickerCss);\n\t\t\t};\n\n\t\t\tdatetimepicker\n\t\t\t\t.on('open.xdsoft', function (event) {\n\t\t\t\t\tvar onShow = true;\n\t\t\t\t\tif (options.onShow && $.isFunction(options.onShow)) {\n\t\t\t\t\t\tonShow = options.onShow.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event);\n\t\t\t\t\t}\n\t\t\t\t\tif (onShow !== false) {\n\t\t\t\t\t\tdatetimepicker.show();\n\t\t\t\t\t\tsetPos();\n\t\t\t\t\t\t$(options.contentWindow)\n\t\t\t\t\t\t\t.off('resize.xdsoft', setPos)\n\t\t\t\t\t\t\t.on('resize.xdsoft', setPos);\n\n\t\t\t\t\t\tif (options.closeOnWithoutClick) {\n\t\t\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow]).on('touchstart mousedown.xdsoft', function arguments_callee6() {\n\t\t\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t\t\t\t$([options.ownerDocument.body, options.contentWindow]).off('touchstart mousedown.xdsoft', arguments_callee6);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.on('close.xdsoft', function (event) {\n\t\t\t\t\tvar onClose = true;\n\t\t\t\t\tmonth_picker\n\t\t\t\t\t\t.find('.xdsoft_month,.xdsoft_year')\n\t\t\t\t\t\t.find('.xdsoft_select')\n\t\t\t\t\t\t.hide();\n\t\t\t\t\tif (options.onClose && $.isFunction(options.onClose)) {\n\t\t\t\t\t\tonClose = options.onClose.call(datetimepicker, _xdsoft_datetime.currentTime, datetimepicker.data('input'), event);\n\t\t\t\t\t}\n\t\t\t\t\tif (onClose !== false && !options.opened && !options.inline) {\n\t\t\t\t\t\tdatetimepicker.hide();\n\t\t\t\t\t}\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t})\n\t\t\t\t.on('toggle.xdsoft', function () {\n\t\t\t\t\tif (datetimepicker.is(':visible')) {\n\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdatetimepicker.trigger('open.xdsoft');\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.data('input', input);\n\n\t\t\ttimer = 0;\n\n\t\t\tdatetimepicker.data('xdsoft_datetime', _xdsoft_datetime);\n\t\t\tdatetimepicker.setOptions(options);\n\n\t\t\tfunction getCurrentValue() {\n\t\t\t\tvar ct = false, time;\n\n\t\t\t\tif (options.startDate) {\n\t\t\t\t\tct = _xdsoft_datetime.strToDate(options.startDate);\n\t\t\t\t} else {\n\t\t\t\t\tct = options.value || ((input && input.val && input.val()) ? input.val() : '');\n\t\t\t\t\tif (ct) {\n\t\t\t\t\t\tct = _xdsoft_datetime.strToDateTime(ct);\n\t\t\t\t\t\tif (options.yearOffset) {\n\t\t\t\t\t\t\tct = new Date(ct.getFullYear() - options.yearOffset, ct.getMonth(), ct.getDate(), ct.getHours(), ct.getMinutes(), ct.getSeconds(), ct.getMilliseconds());\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (options.defaultDate) {\n\t\t\t\t\t\tct = _xdsoft_datetime.strToDateTime(options.defaultDate);\n\t\t\t\t\t\tif (options.defaultTime) {\n\t\t\t\t\t\t\ttime = _xdsoft_datetime.strtotime(options.defaultTime);\n\t\t\t\t\t\t\tct.setHours(time.getHours());\n\t\t\t\t\t\t\tct.setMinutes(time.getMinutes());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (ct && _xdsoft_datetime.isValidDate(ct)) {\n\t\t\t\t\tdatetimepicker.data('changed', true);\n\t\t\t\t} else {\n\t\t\t\t\tct = '';\n\t\t\t\t}\n\n\t\t\t\treturn ct || 0;\n\t\t\t}\n\n\t\t\tfunction setMask(options) {\n\n\t\t\t\tvar isValidValue = function (mask, value) {\n\t\t\t\t\t\tvar reg = mask\n\t\t\t\t\t\t\t.replace(/([\\[\\]\\/\\{\\}\\(\\)\\-\\.\\+]{1})/g, '\\\\$1')\n\t\t\t\t\t\t\t.replace(/_/g, '{digit+}')\n\t\t\t\t\t\t\t.replace(/([0-9]{1})/g, '{digit$1}')\n\t\t\t\t\t\t\t.replace(/\\{digit([0-9]{1})\\}/g, '[0-$1_]{1}')\n\t\t\t\t\t\t\t.replace(/\\{digit[\\+]\\}/g, '[0-9_]{1}');\n\t\t\t\t\t\treturn (new RegExp(reg)).test(value);\n\t\t\t\t\t},\n\t\t\t\t\tgetCaretPos = function (input) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (options.ownerDocument.selection && options.ownerDocument.selection.createRange) {\n\t\t\t\t\t\t\t\tvar range = options.ownerDocument.selection.createRange();\n\t\t\t\t\t\t\t\treturn range.getBookmark().charCodeAt(2) - 2;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (input.setSelectionRange) {\n\t\t\t\t\t\t\t\treturn input.selectionStart;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\treturn 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tsetCaretPos = function (node, pos) {\n\t\t\t\t\t\tnode = (typeof node === \"string\" || node instanceof String) ? options.ownerDocument.getElementById(node) : node;\n\t\t\t\t\t\tif (!node) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (node.createTextRange) {\n\t\t\t\t\t\t\tvar textRange = node.createTextRange();\n\t\t\t\t\t\t\ttextRange.collapse(true);\n\t\t\t\t\t\t\ttextRange.moveEnd('character', pos);\n\t\t\t\t\t\t\ttextRange.moveStart('character', pos);\n\t\t\t\t\t\t\ttextRange.select();\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (node.setSelectionRange) {\n\t\t\t\t\t\t\tnode.setSelectionRange(pos, pos);\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t};\n\n\t\t\t\tif(options.mask) {\n\t\t\t\t\tinput.off('keydown.xdsoft');\n\t\t\t\t}\n\n\t\t\t\tif (options.mask === true) {\n\t\t\t\t\tif (dateHelper.formatMask) {\n\t\t\t\t\t\toptions.mask = dateHelper.formatMask(options.format)\n\t\t\t\t\t} else {\n\t\t\t\t\t\toptions.mask = options.format\n\t\t\t\t\t\t\t.replace(/Y/g, '9999')\n\t\t\t\t\t\t\t.replace(/F/g, '9999')\n\t\t\t\t\t\t\t.replace(/m/g, '19')\n\t\t\t\t\t\t\t.replace(/d/g, '39')\n\t\t\t\t\t\t\t.replace(/H/g, '29')\n\t\t\t\t\t\t\t.replace(/i/g, '59')\n\t\t\t\t\t\t\t.replace(/s/g, '59');\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ($.type(options.mask) === 'string') {\n\t\t\t\t\tif (!isValidValue(options.mask, input.val())) {\n\t\t\t\t\t\tinput.val(options.mask.replace(/[0-9]/g, '_'));\n\t\t\t\t\t\tsetCaretPos(input[0], 0);\n\t\t\t\t\t}\n\n\t\t\t\t\tinput.on('paste.xdsoft', function (event) {\n\t\t\t\t\t    // couple options here\n\t\t\t\t\t    // 1. return false - tell them they can't paste\n\t\t\t\t\t    // 2. insert over current characters - minimal validation\n\t\t\t\t\t    // 3. full fledged parsing and validation\n\t\t\t\t\t    // let's go option 2 for now\n\n\t\t\t\t\t    // fires multiple times for some reason\n\n\t\t\t\t\t    // https://stackoverflow.com/a/30496488/1366033\n\t\t\t\t\t    var clipboardData = event.clipboardData || event.originalEvent.clipboardData || window.clipboardData,\n\t\t\t\t\t\tpastedData = clipboardData.getData('text'),\n\t\t\t\t\t\tval = this.value,\n\t\t\t\t\t\tpos = this.selectionStart\n\n\t\t\t\t\t    var valueBeforeCursor = val.substr(0, pos);\n\t\t\t\t\t    var valueAfterPaste = val.substr(pos + pastedData.length);\n\n\t\t\t\t\t    val = valueBeforeCursor + pastedData + valueAfterPaste;           \n\t\t\t\t\t    pos += pastedData.length;\n\n\t\t\t\t\t    if (isValidValue(options.mask, val)) {\n\t\t\t\t\t\tthis.value = val;\n\t\t\t\t\t\tsetCaretPos(this, pos);\n\t\t\t\t\t    } else if ($.trim(val) === '') {\n\t\t\t\t\t\tthis.value = options.mask.replace(/[0-9]/g, '_');\n\t\t\t\t\t    } else {\n\t\t\t\t\t\tinput.trigger('error_input.xdsoft');\n\t\t\t\t\t    }\n\n\t\t\t\t\t    event.preventDefault();\n\t\t\t\t\t    return false;\n\t\t\t\t\t  });\n\n\t\t\t\t\t  input.on('keydown.xdsoft', function (event) {\n\t\t\t\t\t    var val = this.value,\n\t\t\t\t\t\tkey = event.which,\n\t\t\t\t\t\tpos = this.selectionStart,\n\t\t\t\t\t\tselEnd = this.selectionEnd,\n\t\t\t\t\t\thasSel = pos !== selEnd,\n\t\t\t\t\t\tdigit;\n\n\t\t\t\t\t    // only alow these characters\n\t\t\t\t\t    if (((key >=  KEY0 && key <=  KEY9)  ||\n\t\t\t\t\t\t (key >= _KEY0 && key <= _KEY9)) || \n\t\t\t\t\t\t (key === BACKSPACE || key === DEL)) {\n\n\t\t\t\t\t      // get char to insert which is new character or placeholder ('_')\n\t\t\t\t\t      digit = (key === BACKSPACE || key === DEL) ? '_' :\n\t\t\t\t\t\t\t  String.fromCharCode((_KEY0 <= key && key <= _KEY9) ? key - KEY0 : key);\n\n\t\t\t\t\t\t// we're deleting something, we're not at the start, and have normal cursor, move back one\n\t\t\t\t\t\t// if we have a selection length, cursor actually sits behind deletable char, not in front\n\t\t\t\t\t\tif (key === BACKSPACE && pos && !hasSel) {\n\t\t\t\t\t\t    pos -= 1;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// don't stop on a separator, continue whatever direction you were going\n\t\t\t\t\t\t//   value char - keep incrementing position while on separator char and we still have room\n\t\t\t\t\t\t//   del char   - keep decrementing position while on separator char and we still have room\n\t\t\t\t\t\twhile (true) {\n\t\t\t\t\t\t  var maskValueAtCurPos = options.mask.substr(pos, 1);\n\t\t\t\t\t\t  var posShorterThanMaskLength = pos < options.mask.length;\n\t\t\t\t\t\t  var posGreaterThanZero = pos > 0;\n\t\t\t\t\t\t  var notNumberOrPlaceholder = /[^0-9_]/;\n\t\t\t\t\t\t  var curPosOnSep = notNumberOrPlaceholder.test(maskValueAtCurPos);\n\t\t\t\t\t\t  var continueMovingPosition = curPosOnSep && posShorterThanMaskLength && posGreaterThanZero\n\n\t\t\t\t\t\t  // if we hit a real char, stay where we are\n\t\t\t\t\t\t  if (!continueMovingPosition) break;\n\n\t\t\t\t\t\t  // hitting backspace in a selection, you can possibly go back any further - go forward\n\t\t\t\t\t\t  pos += (key === BACKSPACE && !hasSel) ? -1 : 1;\n\n\t\t\t\t\t\t}\n\n\n\t\t\t\t\t\tif (hasSel) {\n\t\t\t\t\t\t  // pos might have moved so re-calc length\n\t\t\t\t\t\t  var selLength = selEnd - pos\n\n\t\t\t\t\t\t  // if we have a selection length we will wipe out entire selection and replace with default template for that range\n\t\t\t\t\t\t  var defaultBlank = options.mask.replace(/[0-9]/g, '_');\n\t\t\t\t\t\t  var defaultBlankSelectionReplacement = defaultBlank.substr(pos, selLength); \n\t\t\t\t\t\t  var selReplacementRemainder = defaultBlankSelectionReplacement.substr(1) // might be empty\n\n\t\t\t\t\t\t  var valueBeforeSel = val.substr(0, pos);\n\t\t\t\t\t\t  var insertChars = digit + selReplacementRemainder;\n\t\t\t\t\t\t  var charsAfterSelection = val.substr(pos + selLength);\n\n\t\t\t\t\t\t  val = valueBeforeSel + insertChars + charsAfterSelection\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t  var valueBeforeCursor = val.substr(0, pos);\n\t\t\t\t\t\t  var insertChar = digit;\n\t\t\t\t\t\t  var valueAfterNextChar = val.substr(pos + 1);\n\n\t\t\t\t\t\t  val = valueBeforeCursor + insertChar + valueAfterNextChar\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ($.trim(val) === '') {\n\t\t\t\t\t\t  // if empty, set to default\n\t\t\t\t\t\t    val = defaultBlank\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t  // if at the last character don't need to do anything\n\t\t\t\t\t\t    if (pos === options.mask.length) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t    }\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// resume cursor location\n\t\t\t\t\t\tpos += (key === BACKSPACE) ? 0 : 1;\n\t\t\t\t\t\t// don't stop on a separator, continue whatever direction you were going\n\t\t\t\t\t\twhile (/[^0-9_]/.test(options.mask.substr(pos, 1)) && pos < options.mask.length && pos > 0) {\n\t\t\t\t\t\t    pos += (key === BACKSPACE) ? 0 : 1;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (isValidValue(options.mask, val)) {\n\t\t\t\t\t\t    this.value = val;\n\t\t\t\t\t\t    setCaretPos(this, pos);\n\t\t\t\t\t\t} else if ($.trim(val) === '') {\n\t\t\t\t\t\t    this.value = options.mask.replace(/[0-9]/g, '_');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t    input.trigger('error_input.xdsoft');\n\t\t\t\t\t\t}\n\t\t\t\t\t    } else {\n\t\t\t\t\t\tif (([AKEY, CKEY, VKEY, ZKEY, YKEY].indexOf(key) !== -1 && ctrlDown) || [ESC, ARROWUP, ARROWDOWN, ARROWLEFT, ARROWRIGHT, F5, CTRLKEY, TAB, ENTER].indexOf(key) !== -1) {\n\t\t\t\t\t\t    return true;\n\t\t\t\t\t\t}\n\t\t\t\t\t    }\n\n\t\t\t\t\t    event.preventDefault();\n\t\t\t\t\t    return false;\n\t\t\t\t\t  });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_xdsoft_datetime.setCurrentTime(getCurrentValue());\n\n\t\t\tinput\n\t\t\t\t.data('xdsoft_datetimepicker', datetimepicker)\n\t\t\t\t.on('open.xdsoft focusin.xdsoft mousedown.xdsoft touchstart', function () {\n\t\t\t\t\tif (input.is(':disabled') || (input.data('xdsoft_datetimepicker').is(':visible') && options.closeOnInputClick)) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (!options.openOnFocus) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\ttimer = setTimeout(function () {\n\t\t\t\t\t\tif (input.is(':disabled')) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttriggerAfterOpen = true;\n\t\t\t\t\t\t_xdsoft_datetime.setCurrentTime(getCurrentValue(), true);\n\t\t\t\t\t\tif(options.mask) {\n\t\t\t\t\t\t\tsetMask(options);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdatetimepicker.trigger('open.xdsoft');\n\t\t\t\t\t}, 100);\n\t\t\t\t})\n\t\t\t\t.on('keydown.xdsoft', function (event) {\n\t\t\t\t\tvar elementSelector,\n\t\t\t\t\t\tkey = event.which;\n\t\t\t\t\tif ([ENTER].indexOf(key) !== -1 && options.enterLikeTab) {\n\t\t\t\t\t\telementSelector = $(\"input:visible,textarea:visible,button:visible,a:visible\");\n\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t\telementSelector.eq(elementSelector.index(this) + 1).focus();\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tif ([TAB].indexOf(key) !== -1) {\n\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.on('blur.xdsoft', function () {\n\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t});\n\t\t};\n\t\tdestroyDateTimePicker = function (input) {\n\t\t\tvar datetimepicker = input.data('xdsoft_datetimepicker');\n\t\t\tif (datetimepicker) {\n\t\t\t\tdatetimepicker.data('xdsoft_datetime', null);\n\t\t\t\tdatetimepicker.remove();\n\t\t\t\tinput\n\t\t\t\t\t.data('xdsoft_datetimepicker', null)\n\t\t\t\t\t.off('.xdsoft');\n\t\t\t\t$(options.contentWindow).off('resize.xdsoft');\n\t\t\t\t$([options.contentWindow, options.ownerDocument.body]).off('mousedown.xdsoft touchstart');\n\t\t\t\tif (input.unmousewheel) {\n\t\t\t\t\tinput.unmousewheel();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\t$(options.ownerDocument)\n\t\t\t.off('keydown.xdsoftctrl keyup.xdsoftctrl')\n\t\t\t.on('keydown.xdsoftctrl', function (e) {\n\t\t\t\tif (e.keyCode === CTRLKEY) {\n\t\t\t\t\tctrlDown = true;\n\t\t\t\t}\n\t\t\t})\n\t\t\t.on('keyup.xdsoftctrl', function (e) {\n\t\t\t\tif (e.keyCode === CTRLKEY) {\n\t\t\t\t\tctrlDown = false;\n\t\t\t\t}\n\t\t\t});\n\n\t\tthis.each(function () {\n\t\t\tvar datetimepicker = $(this).data('xdsoft_datetimepicker'), $input;\n\t\t\tif (datetimepicker) {\n\t\t\t\tif ($.type(opt) === 'string') {\n\t\t\t\t\tswitch (opt) {\n\t\t\t\t\t\tcase 'show':\n\t\t\t\t\t\t\t$(this).select().focus();\n\t\t\t\t\t\t\tdatetimepicker.trigger('open.xdsoft');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'hide':\n\t\t\t\t\t\t\tdatetimepicker.trigger('close.xdsoft');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'toggle':\n\t\t\t\t\t\t\tdatetimepicker.trigger('toggle.xdsoft');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'destroy':\n\t\t\t\t\t\t\tdestroyDateTimePicker($(this));\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'reset':\n\t\t\t\t\t\t\tthis.value = this.defaultValue;\n\t\t\t\t\t\t\tif (!this.value || !datetimepicker.data('xdsoft_datetime').isValidDate(dateHelper.parseDate(this.value, options.format))) {\n\t\t\t\t\t\t\t\tdatetimepicker.data('changed', false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdatetimepicker.data('xdsoft_datetime').setCurrentTime(this.value);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'validate':\n\t\t\t\t\t\t\t$input = datetimepicker.data('input');\n\t\t\t\t\t\t\t$input.trigger('blur.xdsoft');\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tif (datetimepicker[opt] && $.isFunction(datetimepicker[opt])) {\n\t\t\t\t\t\t\t\tresult = datetimepicker[opt](opt2);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tdatetimepicker\n\t\t\t\t\t\t.setOptions(opt);\n\t\t\t\t}\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t\tif ($.type(opt) !== 'string') {\n\t\t\t\tif (!options.lazyInit || options.open || options.inline) {\n\t\t\t\t\tcreateDateTimePicker($(this));\n\t\t\t\t} else {\n\t\t\t\t\tlazyInit($(this));\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\treturn result;\n\t};\n\n\t$.fn.datetimepicker.defaults = default_options;\n\n\tfunction HighlightedDate(date, desc, style) {\n\t\t\"use strict\";\n\t\tthis.date = date;\n\t\tthis.desc = desc;\n\t\tthis.style = style;\n\t}\n};\n;(function (factory) {\n\tif ( typeof define === 'function' && define.amd ) {\n\t\t// AMD. Register as an anonymous module.\n\t\tdefine(['jquery', 'jquery-mousewheel'], factory);\n\t} else if (typeof exports === 'object') {\n\t\t// Node/CommonJS style for Browserify\n\t\tmodule.exports = factory(require('jquery'));;\n\t} else {\n\t\t// Browser globals\n\t\tfactory(jQuery);\n\t}\n}(datetimepickerFactory));\n\n\n\n/*!\n * jQuery Mousewheel 3.1.13\n *\n * Copyright jQuery Foundation and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n */\n\n(function (factory) {\n    if ( typeof define === 'function' && define.amd ) {\n        // AMD. Register as an anonymous module.\n        define(['jquery'], factory);\n    } else if (typeof exports === 'object') {\n        // Node/CommonJS style for Browserify\n        module.exports = factory;\n    } else {\n        // Browser globals\n        factory(jQuery);\n    }\n}(function ($) {\n\n    var toFix  = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'],\n        toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ?\n                    ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],\n        slice  = Array.prototype.slice,\n        nullLowestDeltaTimeout, lowestDelta;\n\n    if ( $.event.fixHooks ) {\n        for ( var i = toFix.length; i; ) {\n            $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks;\n        }\n    }\n\n    var special = $.event.special.mousewheel = {\n        version: '3.1.12',\n\n        setup: function() {\n            if ( this.addEventListener ) {\n                for ( var i = toBind.length; i; ) {\n                    this.addEventListener( toBind[--i], handler, false );\n                }\n            } else {\n                this.onmousewheel = handler;\n            }\n            // Store the line height and page height for this particular element\n            $.data(this, 'mousewheel-line-height', special.getLineHeight(this));\n            $.data(this, 'mousewheel-page-height', special.getPageHeight(this));\n        },\n\n        teardown: function() {\n            if ( this.removeEventListener ) {\n                for ( var i = toBind.length; i; ) {\n                    this.removeEventListener( toBind[--i], handler, false );\n                }\n            } else {\n                this.onmousewheel = null;\n            }\n            // Clean up the data we added to the element\n            $.removeData(this, 'mousewheel-line-height');\n            $.removeData(this, 'mousewheel-page-height');\n        },\n\n        getLineHeight: function(elem) {\n            var $elem = $(elem),\n                $parent = $elem['offsetParent' in $.fn ? 'offsetParent' : 'parent']();\n            if (!$parent.length) {\n                $parent = $('body');\n            }\n            return parseInt($parent.css('fontSize'), 10) || parseInt($elem.css('fontSize'), 10) || 16;\n        },\n\n        getPageHeight: function(elem) {\n            return $(elem).height();\n        },\n\n        settings: {\n            adjustOldDeltas: true, // see shouldAdjustOldDeltas() below\n            normalizeOffset: true  // calls getBoundingClientRect for each event\n        }\n    };\n\n    $.fn.extend({\n        mousewheel: function(fn) {\n            return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel');\n        },\n\n        unmousewheel: function(fn) {\n            return this.unbind('mousewheel', fn);\n        }\n    });\n\n\n    function handler(event) {\n        var orgEvent   = event || window.event,\n            args       = slice.call(arguments, 1),\n            delta      = 0,\n            deltaX     = 0,\n            deltaY     = 0,\n            absDelta   = 0,\n            offsetX    = 0,\n            offsetY    = 0;\n        event = $.event.fix(orgEvent);\n        event.type = 'mousewheel';\n\n        // Old school scrollwheel delta\n        if ( 'detail'      in orgEvent ) { deltaY = orgEvent.detail * -1;      }\n        if ( 'wheelDelta'  in orgEvent ) { deltaY = orgEvent.wheelDelta;       }\n        if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY;      }\n        if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; }\n\n        // Firefox < 17 horizontal scrolling related to DOMMouseScroll event\n        if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {\n            deltaX = deltaY * -1;\n            deltaY = 0;\n        }\n\n        // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy\n        delta = deltaY === 0 ? deltaX : deltaY;\n\n        // New school wheel delta (wheel event)\n        if ( 'deltaY' in orgEvent ) {\n            deltaY = orgEvent.deltaY * -1;\n            delta  = deltaY;\n        }\n        if ( 'deltaX' in orgEvent ) {\n            deltaX = orgEvent.deltaX;\n            if ( deltaY === 0 ) { delta  = deltaX * -1; }\n        }\n\n        // No change actually happened, no reason to go any further\n        if ( deltaY === 0 && deltaX === 0 ) { return; }\n\n        // Need to convert lines and pages to pixels if we aren't already in pixels\n        // There are three delta modes:\n        //   * deltaMode 0 is by pixels, nothing to do\n        //   * deltaMode 1 is by lines\n        //   * deltaMode 2 is by pages\n        if ( orgEvent.deltaMode === 1 ) {\n            var lineHeight = $.data(this, 'mousewheel-line-height');\n            delta  *= lineHeight;\n            deltaY *= lineHeight;\n            deltaX *= lineHeight;\n        } else if ( orgEvent.deltaMode === 2 ) {\n            var pageHeight = $.data(this, 'mousewheel-page-height');\n            delta  *= pageHeight;\n            deltaY *= pageHeight;\n            deltaX *= pageHeight;\n        }\n\n        // Store lowest absolute delta to normalize the delta values\n        absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );\n\n        if ( !lowestDelta || absDelta < lowestDelta ) {\n            lowestDelta = absDelta;\n\n            // Adjust older deltas if necessary\n            if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {\n                lowestDelta /= 40;\n            }\n        }\n\n        // Adjust older deltas if necessary\n        if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {\n            // Divide all the things by 40!\n            delta  /= 40;\n            deltaX /= 40;\n            deltaY /= 40;\n        }\n\n        // Get a whole, normalized value for the deltas\n        delta  = Math[ delta  >= 1 ? 'floor' : 'ceil' ](delta  / lowestDelta);\n        deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);\n        deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);\n\n        // Normalise offsetX and offsetY properties\n        if ( special.settings.normalizeOffset && this.getBoundingClientRect ) {\n            var boundingRect = this.getBoundingClientRect();\n            offsetX = event.clientX - boundingRect.left;\n            offsetY = event.clientY - boundingRect.top;\n        }\n\n        // Add information to the event object\n        event.deltaX = deltaX;\n        event.deltaY = deltaY;\n        event.deltaFactor = lowestDelta;\n        event.offsetX = offsetX;\n        event.offsetY = offsetY;\n        // Go ahead and set deltaMode to 0 since we converted to pixels\n        // Although this is a little odd since we overwrite the deltaX/Y\n        // properties with normalized deltas.\n        event.deltaMode = 0;\n\n        // Add event and delta to the front of the arguments\n        args.unshift(event, delta, deltaX, deltaY);\n\n        // Clearout lowestDelta after sometime to better\n        // handle multiple device types that give different\n        // a different lowestDelta\n        // Ex: trackpad = 3 and mouse wheel = 120\n        if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }\n        nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);\n\n        return ($.event.dispatch || $.event.handle).apply(this, args);\n    }\n\n    function nullLowestDelta() {\n        lowestDelta = null;\n    }\n\n    function shouldAdjustOldDeltas(orgEvent, absDelta) {\n        // If this is an older event and the delta is divisable by 120,\n        // then we are assuming that the browser is treating this as an\n        // older mouse wheel event and that we should divide the deltas\n        // by 40 to try and get a more usable deltaFactor.\n        // Side note, this actually impacts the reported scroll distance\n        // in older browsers and can cause scrolling to be slower than native.\n        // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.\n        return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0;\n    }\n\n}));\n"
  },
  {
    "path": "assets/vendor/izimodal/iziModal.css",
    "content": "/*\n* iziModal | v1.5.1\n* http://izimodal.marcelodolce.com\n* by Marcelo Dolce.\n* slight modifications made for better WP theme compat\n*/\n.iziModal {\n    display: none;\n    position: fixed;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    margin: auto;\n    background: #FFF;\n    box-shadow: 0 0 8px rgba(0,0,0,.3);\n    transition: margin-top 0.3s ease, height 0.3s ease;\n    transform: translateZ(0);\n}\n.iziModal *{\n    -webkit-font-smoothing: antialiased;\n}\n.iziModal::after{\n    content: '';\n    width: 100%;\n    height: 0px;\n    opacity: 0;\n    position: absolute;\n    left: 0;\n    bottom: 0;\n    z-index: 1;\n    background: -moz-linear-gradient(top,  rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 100%);\n    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,0.35)));\n    background: -webkit-linear-gradient(top,  rgba(0,0,0,0) 0%,rgba(0,0,0,0.35) 100%);\n    background: -o-linear-gradient(top,  rgba(0,0,0,0) 0%,rgba(0,0,0,0.35) 100%);\n    background: -ms-linear-gradient(top,  rgba(0,0,0,0) 0%,rgba(0,0,0,0.35) 100%);\n    background: linear-gradient(to bottom,  rgba(0,0,0,0) 0%,rgba(0,0,0,0.35) 100%);\n    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00000000', endColorstr='#59000000',GradientType=0 );\n    transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out;\n    pointer-events: none;\n}\n.iziModal.hasShadow::after{\n    height: 30px;\n    opacity: 1;\n}\n.iziModal .iziModal-progressbar{\n    position: absolute;\n    left: 0;\n    top: 0px;\n    width: 100%;\n    z-index: 1;\n}\n.iziModal .iziModal-progressbar > div{\n    height: 2px;\n    width: 100%;\n}\n\n\n.iziModal .iziModal-header {\n    background: #88A0B9;\n    padding: 14px 18px 15px 18px;\n    box-shadow: inset 0 -10px 15px -12px rgba(0, 0, 0, 0.3), 0 0 0px #555;\n    overflow: hidden;\n    position: relative;\n    z-index: 10;\n}\n.iziModal .iziModal-header-icon{\n    font-size: 40px;\n    color: rgba(255, 255, 255, 0.5);\n    padding: 0 15px 0 0;\n    margin: 0;\n    float: left;\n}\n.iziModal .iziModal-header-title {\n    color: #FFF;\n    font-size: 18px;\n    font-weight: 600;\n    line-height: 1.3;\n}\n.iziModal .iziModal-header-subtitle {\n    color: rgba(255, 255, 255, 0.6);\n    font-size: 12px;\n    line-height: 1.45;\n}\n.iziModal .iziModal-header-title, .iziModal .iziModal-header-subtitle{\n    display: block;\n    margin: 0;\n    padding: 0;\n    /*font-family*/: 'Lato', Arial;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    text-align: left;\n}\n.iziModal .iziModal-header-buttons {\n    position: absolute;\n    top: 50%;\n    right: 10px;\n    margin: -17px 0 0 0;\n}\n\n.iziModal .iziModal-button{\n    display: block;\n    float: right;\n    z-index: 2;\n    outline: none;\n    height: 34px;\n    width: 34px;\n    border: 0;\n    padding: 0;\n    margin: 0;\n    opacity: 0.3;\n    border-radius: 50%;\n    transition: transform 0.5s cubic-bezier(.16,.81,.32,1), opacity 0.5s ease;\n    background-size: 67% !important;\n    -webkit-tap-highlight-color: rgba(0,0,0,0);\n    -webkit-tap-highlight-color: transparent; /* For some Androids */\n}\n.iziModal .iziModal-button-close{\n    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTMyIDc5LjE1OTI4NCwgMjAxNi8wNC8xOS0xMzoxMzo0MCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODZCQkIzQ0I0RTg0MTFFNjlBODI4QTFBRTRBMkFCMDQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODZCQkIzQ0M0RTg0MTFFNjlBODI4QTFBRTRBMkFCMDQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4NkJCQjNDOTRFODQxMUU2OUE4MjhBMUFFNEEyQUIwNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4NkJCQjNDQTRFODQxMUU2OUE4MjhBMUFFNEEyQUIwNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsgTJLcAAALJSURBVHja3JnLS1VBHMfvQ7g9dBXRRrwEFRciAhMi1JRW1aIHVEIYEkW0iVpUhOD/ICK6cFMgSbUpC6VFkQa9NtpjkauriRY9Noa3pHT8/mIODMM5Or85o87pC5/NPf5mvmc8M7+Z36SFEKkY2gj2gUawF2wHW8A6+fwv+A6KYAQMg+dg2rbDtKXhGnAaHJIms4zYz9J4HxgAf1g9k2EGteAhWBBuNApaQNrUg6nRTaAbzIuV0RCocWW4DoyJlVcJXI5ruFk2tJqi/2TWxvA5sXbqA2Ucw01i7dVjargazAo/dE33p6/DlAheg50pP0SJpwG8CH7IaH/Q5pFZUhnoArkwwwVwJeWfdoMLYYZvqG+yTGo9CerAoIWBT+A4qAdPDWOugwo1NVcxJtpFZRLkwH3GJCqCghJfxVjnz1JMMMKnwAbGRAg0B5rAA4O4CblZ+qj8tkBjZthvSzDCtFIMM0ZpQhslk5Eej4jpZ/T7G+ygwG1ghrk+jjNMFy1eMPJzpOAzlou6iWmXZkm91EBHjEwUZXoQTDk2SxqhRh7HTJ9hpstB3rFZ0ldq6J2DnB9m2rXZfxOPlrX1DrJRXiaBXSHPaMHvB0cd9JPLpBImMvzLQTuUFA6A9yHPfoIjhsllOc1l5N4grtmDWgYrl5+JTUZcSjNkeMyxWdpA3ZN72IJj01OJTByJS82J2/wQVxmB5y1HK8x0JWMf/kzdD98FJcY5S51gdwyTQl6eUAraspo27PeWXgy8afim0+CELAwOWHyH9EkdkyWwJ4Yxk6BCP+bTm48anutWW5dAp34IpbW03UOzb0FPVEHbx0LKfvAyqpAyKw97JU8Mt6pml6rAJ6oY6Eu5NfvfF7QTeWWQyEsZr6694lwsNoPD8mKRo29gCNwGj7gXi7aGA1EBcY+8vq0GW8FmJb3Pgx9gEnwAr8Ab8MW2w0UBBgAVyyyaohV7ewAAAABJRU5ErkJggg==') no-repeat 50% 50%;\n}\n.iziModal .iziModal-button-fullscreen{\n    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTMyIDc5LjE1OTI4NCwgMjAxNi8wNC8xOS0xMzoxMzo0MCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RTBBOUI4RUM0RTg0MTFFNjk0NTY4NUNFRkZFNEFEQzIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RTBBOUI4RUQ0RTg0MTFFNjk0NTY4NUNFRkZFNEFEQzIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFMEE5QjhFQTRFODQxMUU2OTQ1Njg1Q0VGRkU0QURDMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFMEE5QjhFQjRFODQxMUU2OTQ1Njg1Q0VGRkU0QURDMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrQO6gAAAANmSURBVHjazJlbSBRRGMd3x92i0ForRRMiKiUoX4ouiFlJkRVBDxW9GJERwUasvdRT9FD00osRQtAFqegGBUHRBY0uaCVKEkSRpVR0tSwrQtp1+p/4Bk7D7M45M/Ot/uGHu+Psmf+c+eY753wnbJpmyIfGgvmgiv6WgkKQBwzwE3wBr0AnuAta6ZgnhT0aFuY2ghoyGdH4bS+4Dc6CZjCkdWVhWIPF4JoZnB6CDToeVE8sBidNPt0E5UEZrgG9Jr8GwHa/huMgaWZXDSDsxfBuc/jUBAwdw3Fz+NWoang5SJkjQwm7P3seLqQEX2LLfgfBdZcMORMcBqNDwekPqASP0uXhpjR3Ok0x/fUw9HIHGGVdw5DuRtzJpgxDsJui2qOWmuaAOuuLbHivz4YLwLgQj/aAXNmwuItlHhtbA7pAG5jEZHgKWCcbrhUTIY+NPQVjqFFObbYMi/hc6aOhl2AJ9TKnFoIyYXgemKEzJQXVVkyR3oFVzKZFuqw2qHdyFPKhrHPgMoWC3fRjRtNVVg+7SR5IiqmXxUt60cG0CK/vTIZniZVCmcKJF0C3ZNjKBqvJ9Hrwm46tsN1EkCoRQ/M3fBjvs6GrYAvdwHEfGcd1qBaGkwoxrKI+xjz83yJ0iLFHApd46X4xX+M+WECh4lepCNUIcpnMijrEWtAvTRHrbOd8FZNG8uA2Nf0hpmwtjBPwpQ5T0GPS/+tBAZhIq+b3Lu09EyHRwRgO+0C+7dhWcII+PwCf6Sk/Aa9d2vtn+A7nyASugJiD6YSDQcOlvVbxiCaAN8xrs3sgprBiac/QhlhnzjUo6JuZM0UlDS5FPtoQIdNlPYJTWUihFaDex+9Pg6T1KHJAJ2NI7ASllA28hEQ/KJIXoSlwgKlnh+jFe+GjLtwIPtjfyktUt+UaUZWqvw7H3oJD1peI7eQdoF1xWa+zQikHH13OmwqmOxxP0EiZtgK/DRwNuIcHwSeXc2K01WAPhbhKBb5hBNTVbskVH7fqpZGhbJUNtYF83fqwQSXPbOsGjb6etwx2gcEsmT3iFAZeNmUqaMeHSz2qu0k6W15Rqsx3B2i0D+xXGAHTFrRVlEeFuVoqH+ku6VNUbDkPzlAtg30nVK66i8rRIjAbTKaSQVQyN0DD6nOqcLZQld9TLfmvAAMAeMcvp3eCFqQAAAAASUVORK5CYII=') no-repeat 50% 50%;\n}\n.iziModal.isFullscreen .iziModal-button-fullscreen{\n    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTMyIDc5LjE1OTI4NCwgMjAxNi8wNC8xOS0xMzoxMzo0MCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MkFFRTU5NDA0RTg1MTFFNjk0NEZFQzBGMkVBMDYyRDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MkFFRTU5NDE0RTg1MTFFNjk0NEZFQzBGMkVBMDYyRDkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoyQUVFNTkzRTRFODUxMUU2OTQ0RkVDMEYyRUEwNjJEOSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoyQUVFNTkzRjRFODUxMUU2OTQ0RkVDMEYyRUEwNjJEOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuDFfX8AAANASURBVHjazJlZSBVRGMfHcWlB0xZM68GKukQLYaGkmEUR2EsvRfQS+BSJPUQE+lTR8hqIZY8hFS0ERVCRoW3gpUApghYpszLTVnCB3O70/+K7MAwzc78Z58z4hx8XzpzvzJ+Zc+d85ztphmFoU9BsUAoq+XcFyAc5QAfD4BfoBp3gCWjnNl9K82mYzO0FVWwyw0NsD3gIroBWkPB0ZzLsgc3grhGcnoE9XjxIOxaCC4Y6tYC1QRmuAj2Geg2CA1M1XAsmjHDVANL8GK4zolMz0L0YrjWiV5PU8HYw6TBIf8imD6UynA96HYKPg3mgMUTDY6DUzXCzQ+AxSz+r6QEQZz4HbLoDZNkZrnAIoOlRZjN1Gk3XS0zty/gTFaRq7Ay3uAR8BcU2ps/z9QJTWw74HrDhTyDbbHg9SKQI+sb9rKa3mV8ZmAt+KJjP1TS+zinFPkqEUqQdBeAOKLa0UwIzpqlXtcYpIKWIO4RBZPoRKNfC10YQI8MlYLkwaAB8ABsiMDwDbKU8dgtIFwRMgJ3guRadKpNPWBMa7tOi1WoyHJPuTsC4oN+IQsOLM3gPJlEWqOE/neMGBqwDeYoMz6G8c0I4h6eFyHBC8A2eVoaH8JutaPwuUA/+uvSht1sHKgTjTWZwjUCVYdrK3xT0iwkND+lc5FClUQ9fINHCRYY7FBrWPSz5Er2lAR9H9P+hpfYGl64OCmPadQ7ojcDwOJetysBMQX/6mrWS4d+cIoYtMnAEnBT2fwVeJufYxZBMFoKFlrajQtOX/uczvEtIB50Kdgn1lt3JGdANltjsXE64jPMnuQ1LPuFJcFrBE11gzQXAUnAPFNk86esO4zSBfmu5lVa9toCf8DC4Ba6C22DEdO01KDLdP5fLr1Z94X2ibV1ilWVQ1XrDpvPAU4c+u1KVqvaHXI7q43ltp3PSYmDDNCgGPrCUD1wN6y5lqzAUN89baX1Y55Jn2LrPRUffRwaHwWhIZs/aTQM/hzLlDp+coPRReprk5cgrkyvz7wM0+hOcAvOlPvwcLNIp526ux1H5aJbHeFpVX4Br4LLXWoffk9CkVnLlaBNYAxaBXJBpMjfIy+o7EAdtfIyb8HPDfwIMAM1WPs8F9tcxAAAAAElFTkSuQmCC') no-repeat 50% 50%;\n}\n.iziModal .iziModal-button-close:hover{\n    transform: rotate(180deg);\n}\n.iziModal .iziModal-button:hover{\n    opacity: 0.8;\n}\n\n\n    .iziModal .iziModal-header.iziModal-noSubtitle{\n        height: auto;\n        padding: 10px 15px 12px 15px;\n    }\n    .iziModal .iziModal-header.iziModal-noSubtitle .iziModal-header-icon{\n        font-size: 23px;\n        padding-right: 13px;\n    }\n    .iziModal .iziModal-header.iziModal-noSubtitle .iziModal-header-title{\n        font-size: 15px;\n        margin: 3px 0 0 0;\n        font-weight: 400;\n    }\n    .iziModal .iziModal-header.iziModal-noSubtitle .iziModal-header-buttons{\n        right: 6px;\n        margin: -16px 0 0 0;\n    }\n    .iziModal .iziModal-header.iziModal-noSubtitle .iziModal-button{\n        height: 30px;\n        width: 30px;\n    }\n\n\n    /* RTL */\n\n    .iziModal-rtl {\n        direction: rtl;\n    }\n    .iziModal-rtl .iziModal-header {\n        padding: 14px 18px 15px 40px;\n    }\n    .iziModal-rtl .iziModal-header-icon {\n        float: right;\n        padding: 0 0 0 15px;\n    }\n    .iziModal-rtl .iziModal-header-buttons{\n        right: initial;\n        left: 10px;\n    }\n    .iziModal-rtl .iziModal-button{\n        float: left;\n    }\n    .iziModal-rtl .iziModal-header-title, .iziModal-rtl .iziModal-header-subtitle{\n        text-align: right;\n        /*font-family*/: Tahoma, 'Lato', Arial;\n        font-weight: 500;\n    }\n    .iziModal-rtl .iziModal-header.iziModal-noSubtitle {\n        padding: 10px 15px 12px 40px;\n    }\n    .iziModal-rtl .iziModal-header.iziModal-noSubtitle .iziModal-header-icon {\n        padding: 0 0 0 13px;\n    }\n\n    /* LIGHT THEME */\n\n    .iziModal.iziModal-light .iziModal-header-icon{\n        color: rgba(0, 0, 0, 0.5);\n    }\n    .iziModal.iziModal-light .iziModal-header-title{\n        color: #000;\n    }\n    .iziModal.iziModal-light .iziModal-header-subtitle{\n        color: rgba(0, 0, 0, 0.6);\n    }\n    .iziModal.iziModal-light .iziModal-button-close{\n        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4JpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTM4IDc5LjE1OTgyNCwgMjAxNi8wOS8xNC0wMTowOTowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyQTU1RUZDNzRFODQxMUU2ODAxOEUwQzg0QjBDQjI3OSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1NEM4MTU1MEI4QUExMUU2QjNGOEVBMjg4OTRBRTg2NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0RTNFNENDMkI4QUExMUU2QjNGOEVBMjg4OTRBRTg2NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNyAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjZjYzMwMmE1LWFlMjEtNDI3ZS1hMmE4LTJlYjhlMmZlY2E3NSIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjdmYmU3NGE3LTAxMDUtMTE3YS1hYmM3LWEzNWNkOWU1Yzc4NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Po24QssAAANtSURBVHja3JlJaBRBFIa7ZxyTSXADHUkikuAawZNLEOOGGrwJQYko8R4RBQ+OICoqghJQUVwPYjzFY0QUBQU1kogoKO6CG0pcIwbiNibj/8JraNvu6Xo9NTOtP3xzSKe6/65+Ve9VlWlkp2IwGUwFE0E5GA4G8/U+0APegWfgHrgPuq0bpNNp0QPNgEYngHlgGpuMCNp2s+kr4BYM/8ql4WqwHEzP4mXteg7awOW0YlerPnQIaARLNBl1ikLlBDw/1WF4ClgHKozc6idogekz2RheANbaBlE+dB4chfF+qeHF3LOF0FWwF6b7nBe8RvecApolzQVr3C64GR4H1huFV51pmvV+hikRbABFRji0GqarMxluAGON8CgKmmA65mZ4DFhqhE9VPP//ZXgZiCmm1t1gI6XWAAY+gF0gCe4qtqlHL8fthkeBWsXGreA6eMgPviEw+x5sBZ3gAdjPCcNPI8Fsu+FawUCzz40psEfRNJndBl7b/pZmVLTQMkzJo0bQSys43iWm3cxS+DUJOmoSwqKCRmEZWKkYv6RSMBPc5lqXRGm0A1Q6XiaT2aSwo8jrK/qZwZlFIlXTusxa6iXDddTdARpnMj2ek9AWjWYH7h/lubcs4A28THdyAdOl0ezAmKNBNyLLiT0Btjti9zuHg06zpJKIprohwXNypcu1OIdGjYbnxCLGPyYy/EPDfejzbwYvXK59AzuFGdFLKTL8WYNZ59RVzGESJCNm0teI40E6zNIA2wSaA2REP32iaW0omKXRbJKTUVyYEVV0J8oxvEiQmiUZrFSz6XNkuJe3nBKCelaSbjOZrhLsd1BInYxweSeJq9YA6dYtuZCBI4JZ6jGW/W+sebhd0DAaMIO5mTYFW1+X6GeQ7TO3W0WyQj3cw0ulBg4nSUbcAY7zPVYp7ip95FXOH29Hb35AOPjypWMIh7PORSjFZVsIzdKW7AWvfYnTVNWHyCytHw+jd1Nehqks3KepvtChUzD7yGvE2/cduqxldQF1EWZb/PbWLF3jAVgo0WrlkN+c6hSd+rzlaSuaR7O0oX0wyIa2pVAdGaj0HCUVOqIq4dVwrg5lmmG2w+8f/9tjL6foYHE+Gy8Xtv3CPUpf7WauDxadKuIwoeNbOmoYDYbZ0ns/1wxUC7ykigs8sS/LpEe3vwUYALiKDDDSgEiSAAAAAElFTkSuQmCC') no-repeat 50% 50%;\n    }\n    .iziModal.iziModal-light .iziModal-button-fullscreen{\n        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4JpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTM4IDc5LjE1OTgyNCwgMjAxNi8wOS8xNC0wMTowOTowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpEQTg1NTA2NTRFODQxMUU2OTQ0N0VERjY2Q0M5ODYwRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0RTNFNENCQkI4QUExMUU2QjNGOEVBMjg4OTRBRTg2NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0RTNFNENCQUI4QUExMUU2QjNGOEVBMjg4OTRBRTg2NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNyAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjFlNTQwYzczLTVhZmEtNDJlYi04YzJlLWMwMzFlYmFiYmIyNiIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOmVkYmRiMzM1LTAxMDUtMTE3YS1hYmM3LWEzNWNkOWU1Yzc4NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvIicdUAAAOvSURBVHjaxJlZbA1hFMe/qaItUUsspakg1laJ7UUisQuRvvTFA15sQSRCLBFrQryhHqxNHxEPtaQ8CCUkIrVVRbVBJdZYSrXVonr9/3pGxnTunZk78/X+k1+aO+1899/vnnvO+c4YKpi6ghEgW34OBD1BKjBAM6gH78Fz8BhUyrW/ikQivt7QiNMozU0DE8RkJx/3fgCPwA1QHvHp2K/hHJAPJqpwVA2K4flW2IZ7gyVgptKjh6AQxl+GYZi7uRr0U3rVBIpg+nIQwwvACpCkOk4XwYlosR3LMGN1qUqMroGDTqaNGDu7SiVWl+D3iP2i00c9HqxUidd8wzDy3HY4HRwCfWzXz4L7Lm+QKfHeOUTTLWAzdro6muH1YIbDjculWrmpUEM2YYXcCNMt9pAYE8WsWYLdlAxaNYTGMDDHKYYXBVy4B0jTFM/5iOcUc1fM/2JcnItNAYtBNzGtQ33BVHDV3OHpARqhV6CLLKpTs8yQYHxOCrDQO7AV1Gg2PBJhMYiGh4MMnx1eLkixXKsFuzSbZrrMpeGxHnqFFtvrTWCbhILd9AuNpnPMHXaTtZD0kl1mRdwSxXSjJsNZfONjcmqIJR5p3lp6Y+sXrAzsBz/lNXvmtZYMFKbqafi0pKQgKpOSPhmsC5BxXEs1Fz4fUr/7TWMe/q9bC2s3tJs1Df/Q/B5PwAZwJYS1WpPlo0zRZJZziL2gQU7I1GyHL7QSD26taVOytI26DpinxKypApvpk+C6dHlMnXskbUbT1yTpN3WJHWB327UCS3hUoc+tA/VyxP/ost5rGq7QWZnAdoe0eZgnYweDbgmgkoafgk8aTfNgsMNmmqfhC+Czj3V4T3mSBH255kxB0ztd4tNNDJkas2CUdkAKHQ3yAtxfijj/bdb7Cumyhmoyexzcs6Qwv2qUbPKvJDOtnNFklrF3R5qneA2XYHe/2A+ht1Xb3FZXRY1XTAjFTgtxJ45qKtWDpZK1g6dhIQuvBzjcy8FgQ6y8Nw+sCdnwL1Dn8jdMe6m2a+3ma9ESNUdOC1VixSH3bnPiYyraswnO0fqDIQkyW8WmCWab7b+I9TCF3+x0j2e+MPUA7LPGrVfD1F3VNsrPVR0zhS8BB5x21muzYa1Sy1Tb4y4d4qOwIi9Pk/wcj1gV50p5zQjJKAsJH8KcY4vpdYrjV0w9HMxxHjfKNpfwdMyRNuAmyy2M1vq5OegBNFMmR9lSHDizSLPMJGjuO2BZfSOtLKvpMylUvh/d/hFgAOH4+ibxGTZuAAAAAElFTkSuQmCC') no-repeat 50% 50%;\n    }\n    .iziModal.iziModal-light.isFullscreen .iziModal-button-fullscreen{\n        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3BpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTM4IDc5LjE1OTgyNCwgMjAxNi8wOS8xNC0wMTowOTowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyRUUxMkYxODRFODUxMUU2Qjc3RDk0MUUzMzJDRjBEOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0RTNFNENCRkI4QUExMUU2QjNGOEVBMjg4OTRBRTg2NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0RTNFNENCRUI4QUExMUU2QjNGOEVBMjg4OTRBRTg2NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNyAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjgzM2MwOWZiLWJjOTEtNGVlZS05MDM1LTRkMmU2ZmE1ZjBmMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoyRUUxMkYxODRFODUxMUU2Qjc3RDk0MUUzMzJDRjBEOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv1Q9Z8AAAOXSURBVHjaxJlLbA1RGMfPjIs+EvoIRYt4FVUl2EkkRTxKUqQbG0SEho2FjUQ8YtEICbEgTdFYeK1KaGvVeoUltyStt0UlNE17aWhV2+v/9X5XJpMzc8/0zpn5kl+aO3Nm7r/fnPu9xhDp2URQDJbw3xkgB2QCAwyAPvANfARvQDsfG7V4PO7pC40xCiVxa8AKFjnOw7VdoA08BtG4R8VeBZeCKrBS+GPvQAM0P/NbcB7YBdYJPfYKXIXwL34IJm8eBFOFXusH9RDdnI7gLWA/MEVwdh/UOe1tN8G0V3eLcKwFXJCJNl08G5ZYsrWgWnZCJng5OOBwo1iAoisMw6hMJXgyOOywVW7xj+9BgKL3QHSxm+C9IF9y4U2GMlStRPQP8Jbp9lFwhJwE0RHrgaSV8N6xG238l7Zjtfx3K58/Bd7zsWngIqdnP2we2ACa7B7e6RL6joK5EtHNfL7b5u1Bn7dGFbycYRVM/8WyFJnuJK+z2iVwzFrMcF1h+Cx4ClhtFVyu8CW54ITE01EwFMAPcH1SMJWIqxQvItE1YHEIsXkhtkUhCV4ApiteFOPadn4IgseDMooSSxVrhWFwmkvCsKw06WGhKLhHhGuzSHChh9pZ5cc1oFFwfoTTsWrWqQCvXdZQEpkDsjUJziSv3Qu43k3LTA1BXqvRY/4DMjTd/yu4niJVm9wslCjcb4QE/9Qo+Al44baAmgpKCIqC+01OBLrsr8/de8zkiYwuUxWSq7iuM8JhantIqfYItkOepKBysnbycIfPXYKqURL6DhaBCQrrKcZHTa5loyEIJgHXwG3F9TQV+pxMGK0BiaTHn2OLEjcURbdi7XBSMO3jTxoEjtg+7wDnhG3spSD6F3hk7Tjoxnc0CJ5k+5wFCrhplYl2mmI24nyvvWumAE9z2zIfBW8WifnxIHc2yb6xiHtEoms0/hlGtpAPHCkgNDjFyZngPN88COvkPpEe+XGHbFcD7z53C+ybwKEAo0UPZ8QCybkmiL3sNvkheygSI08RYOSQiaUhd52sUpIZLWwJsYqkkdcZeHfIS66nc9XcZQRpNBY7C7F9Yy1OtonErDgSgNhGcEXmWa/VFA1O9onE6y4dRqGtXuVtkpf2iDy8EVR6GLykMnrsNFC867QF0hH8v3MVicFcuYdKy56uqQx4SukWQj3NOtJtQIt4ckSvbmdziMqy7HcS9xv0cn/Xwdn0A1drnl/d/hNgAGQa6Lgarp6BAAAAAElFTkSuQmCC') no-repeat 50% 50%;\n    }\n\n\n.iziModal .iziModal-loader{\n    background: #FFF url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDQiIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCA0NCA0NCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBzdHJva2U9IiM5OTkiPiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS13aWR0aD0iMiI+ICAgICAgICA8Y2lyY2xlIGN4PSIyMiIgY3k9IjIyIiByPSIxIj4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJyIiAgICAgICAgICAgICAgICBiZWdpbj0iMHMiIGR1cj0iMS40cyIgICAgICAgICAgICAgICAgdmFsdWVzPSIxOyAyMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMTY1LCAwLjg0LCAwLjQ0LCAxIiAgICAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdHJva2Utb3BhY2l0eSIgICAgICAgICAgICAgICAgYmVnaW49IjBzIiBkdXI9IjEuNHMiICAgICAgICAgICAgICAgIHZhbHVlcz0iMTsgMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMywgMC42MSwgMC4zNTUsIDEiICAgICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPiAgICAgICAgPC9jaXJjbGU+ICAgICAgICA8Y2lyY2xlIGN4PSIyMiIgY3k9IjIyIiByPSIxIj4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJyIiAgICAgICAgICAgICAgICBiZWdpbj0iLTAuOXMiIGR1cj0iMS40cyIgICAgICAgICAgICAgICAgdmFsdWVzPSIxOyAyMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMTY1LCAwLjg0LCAwLjQ0LCAxIiAgICAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdHJva2Utb3BhY2l0eSIgICAgICAgICAgICAgICAgYmVnaW49Ii0wLjlzIiBkdXI9IjEuNHMiICAgICAgICAgICAgICAgIHZhbHVlcz0iMTsgMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMywgMC42MSwgMC4zNTUsIDEiICAgICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPiAgICAgICAgPC9jaXJjbGU+ICAgIDwvZz48L3N2Zz4=) no-repeat 50% 50%;\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: 0;\n    bottom: 0;\n    z-index: 9;\n}\n\n.iziModal .iziModal-content-loader{\n    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDQiIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCA0NCA0NCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBzdHJva2U9IiM5OTkiPiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS13aWR0aD0iMiI+ICAgICAgICA8Y2lyY2xlIGN4PSIyMiIgY3k9IjIyIiByPSIxIj4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJyIiAgICAgICAgICAgICAgICBiZWdpbj0iMHMiIGR1cj0iMS40cyIgICAgICAgICAgICAgICAgdmFsdWVzPSIxOyAyMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMTY1LCAwLjg0LCAwLjQ0LCAxIiAgICAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdHJva2Utb3BhY2l0eSIgICAgICAgICAgICAgICAgYmVnaW49IjBzIiBkdXI9IjEuNHMiICAgICAgICAgICAgICAgIHZhbHVlcz0iMTsgMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMywgMC42MSwgMC4zNTUsIDEiICAgICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPiAgICAgICAgPC9jaXJjbGU+ICAgICAgICA8Y2lyY2xlIGN4PSIyMiIgY3k9IjIyIiByPSIxIj4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJyIiAgICAgICAgICAgICAgICBiZWdpbj0iLTAuOXMiIGR1cj0iMS40cyIgICAgICAgICAgICAgICAgdmFsdWVzPSIxOyAyMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMTY1LCAwLjg0LCAwLjQ0LCAxIiAgICAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdHJva2Utb3BhY2l0eSIgICAgICAgICAgICAgICAgYmVnaW49Ii0wLjlzIiBkdXI9IjEuNHMiICAgICAgICAgICAgICAgIHZhbHVlcz0iMTsgMCIgICAgICAgICAgICAgICAgY2FsY01vZGU9InNwbGluZSIgICAgICAgICAgICAgICAga2V5VGltZXM9IjA7IDEiICAgICAgICAgICAgICAgIGtleVNwbGluZXM9IjAuMywgMC42MSwgMC4zNTUsIDEiICAgICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPiAgICAgICAgPC9jaXJjbGU+ICAgIDwvZz48L3N2Zz4=) no-repeat 50% 50%;\n}\n\n.iziModal .iziModal-content:before,\n.iziModal .iziModal-content:after { content:''; display:table }\n.iziModal .iziModal-content:after { clear:both }\n.iziModal .iziModal-content{\n    zoom:1;\n    width: 100%;\n    -webkit-overflow-scrolling: touch;\n    /*overflow-y: scroll;*/\n}\n.iziModal .iziModal-wrap{\n    width: 100%;\n    position: relative;\n    -webkit-overflow-scrolling: touch;\n    overflow-scrolling: touch;\n}\n.iziModal .iziModal-iframe{\n    border: 0;\n    margin: 0 0 -6px 0;\n    width: 100%;\n    transition: height 0.3s ease;\n}\n.iziModal-overlay{\n    display: block;\n    position: fixed;\n    top: 0;\n    left: 0;\n    height: 100%;\n    width: 100%;\n}\n\n.iziModal-navigate{\n    position: fixed;\n    left: 0;\n    right: 0;\n    top: 0;\n    bottom: 0;\n    pointer-events: none;\n}\n.iziModal-navigate-caption{\n    position: absolute;\n    left: 10px;\n    top: 10px;\n    color: white;\n    line-height: 16px;\n    font-size: 9px;\n    /*font-family*/: 'Lato', Arial;\n    letter-spacing: 0.1em;\n    text-indent: 0;\n    text-align: center;\n    width: 70px;\n    padding: 5px 0;\n    text-transform: uppercase;\n    display: none;\n}\n.iziModal-navigate-caption::before, .iziModal-navigate-caption::after {\n    position: absolute;\n    top: 2px;\n    width: 20px;\n    height: 20px;\n    text-align: center;\n    line-height: 14px;\n    font-size: 12px;\n    content: '';\n    background-size: 100% !important;\n}\n.iziModal-navigate-caption:before{\n    left: 0;\n    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAoCAYAAACFFRgXAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTMyIDc5LjE1OTI4NCwgMjAxNi8wNC8xOS0xMzoxMzo0MCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyNmFjNjAyMy04OWU0LWE0NDAtYmMxMy1kOTA5MTQ3MmYzYjAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDREQ0YwRjA1MzQzMTFFNkE5NUNDRDkyQzEwMzM5RTMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDREQ0YwRUY1MzQzMTFFNkE5NUNDRDkyQzEwMzM5RTMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cykiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpmNmM0Nzk3Ni1mNzE3LTk5NDAtYTgyYS1mNTdjNmNiYmU0NWMiIHN0UmVmOmRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDowZGVmYTEyZC01MzM0LTExZTYtYWRkYi04Y2NmYjI5ZTAxNjYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7oo0ptAAACWklEQVR42uyZTWsTYRSFZybxo4kWk5g2NC5qTAU3Kq30A9udi1oXolV/hWuhv6R/Q6utioi4LbbVFHemamlRU0OCEk0wZjwXzwtDoBDopHMHcuFJMplZnLm5ue+589qu61qeOApyYAjEgG0FEyLqN/gKiqBuTtgewWlwCZw056xgwwirgU3wxSv4NJgCUV5YBRXQDEhsBJwCSSauBVZFdJRlIJk9Av7wbj577jDIOENtRmPVwcsw6KfAAvikRKzEDlhnhuU/lRPBWaa9wsxqC6ndPX7OiOA4D8qW3vjO9z7H0w3+KhZstNmOFbLoCQ6DYGmL+bAInmGfLFC4asFXwRJIgB+goVmw+I7HXO+/gevGnGgUPEGxktkSmAMbWmt4HDwBKS6XN1jDKrvEFYoVK7oLroE3h93Woh1eNwqWafJ/gQV65vM+ail34mc6EZwBK2CAx8fAIjjeBYMzDT4cVHCEXtRbRvEu/Nr9HCIOnGGp15vgEec9KYn74B0nAT/CZnv86FcNvwK3wENwAjwAs2Bbs5d4CW5zir0AXvv8p+tKH34B5lkW4h2egRHtbu05uMMHHWfB0zC4NRF5l09kzvE4rd2tyUJyjy4tz7akZqXbL8QETbJ/FsMgWOJtb6brCQ5YsBsC8Uab63DVkkgqFpzie93h8OhScFah2LTHi5ccWroaLd5l6//+hpYQoWP05LKqFs2WQYbTsNxAi+5fxpWmdfh7HS7XhwSzG+H3a2JnvZsyktmLbdOFhpDMvrf4sN1u2/aK0cwMcmYLcturweceW+CnOfFPgAEA8uWFFylBJYoAAAAASUVORK5CYII=') no-repeat 50% 50%;\n}\n.iziModal-navigate-caption:after{\n    right: 0;\n    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAoCAYAAACFFRgXAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAADhmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMzIgNzkuMTU5Mjg0LCAyMDE2LzA0LzE5LTEzOjEzOjQwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjI2YWM2MDIzLTg5ZTQtYTQ0MC1iYzEzLWQ5MDkxNDcyZjNiMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0NERDRjBGMDUzNDMxMUU2QTk1Q0NEOTJDMTAzMzlFMyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0NERDRjBFRjUzNDMxMUU2QTk1Q0NEOTJDMTAzMzlFMyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNS41IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOmY2YzQ3OTc2LWY3MTctOTk0MC1hODJhLWY1N2M2Y2JiZTQ1YyIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjBkZWZhMTJkLTUzMzQtMTFlNi1hZGRiLThjY2ZiMjllMDE2NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuijSm0AAAKbSURBVFhH7ZnJj0xRGEerzFoIMTaCZmOIedhaiJj55yz8DaYdNhIJEUMQbCTG3rQ02hDSiEY553XdTpHS3nv96taV9ElO6lVt6peb7933fffVG41GrYW5uBaX4EysYzcw1Fd8hc/wM2a0Bl6Nm3BW9i0dDPsQX/olBF6FO72AH/gG3+N3jL3KBpqGC3ERTsGfeAsHDTyHi71oCXzBe/gaU2A5bscZOIxXTb8OLQNX9i6mElYsg/voqruwfQb2BhODWgqpMYDv0NLsNXC4yd42P1PEwNJj4HBTWdipErLVDfxfMRm408QMvBu3jV6WJ1Zg9/rbeBOP+UNZYgX+iE/Rp+lpPIKliBXYB9IhtPNy3z/T/F6YmDXsChvyBc7Gs3gACxEzsDzBg9iPPXgO92NuYgeWx2h3+AhtaM7jPsyF7aV37XR8gNZYO/pwKY51+xPkG27Fk2joT3gCr2A7NuJ6HMkTeAPadlp3VeMChF7G0P6X3dmfjAXOUxIj6LZkv1ylNuStDZejkL+PS96ScFzRqnDAtI5PoTefvbg7iNNOOwqVRCfYghdxBbpHH8Y7+DcKlUTV7MLLaNghPIrjhf2N2IF34AVcjE44hrXHyE3MwE6/loEzpEcIlqKjeyFiBe7FS+he/gENewMLEyuwXdo8dGWP43UsRazA9g7uDNbwNX8oS8watlsz+ISIGbgSJgN3GgOHlnFq8zNFQraGgT1iFc9iUyU0XsMGHhy9zh6XbvCp4ZuBBWglDBj4OdqLeu0+uRJTwMZ+Dbp/e21P3m97yWe2snsw1LTHmz5C/9lQdwhfGbiq89GwvrrwUT4UAouhN6MzloTRpVuEYI5O9urZYXtrYPGQw2OlZegM163QhrJMfWVgyTq0Qq32C/N7uPz9OknWAAAAAElFTkSuQmCC') no-repeat 50% 50%;\n}\n    .iziModal-navigate > button{\n        position: fixed;\n        bottom: 0;\n        top: 0;\n        border:0;\n        height: 100%;\n        width: 84px;\n        background-color: transparent !important;\n        background-size: 100% !important;\n        cursor: pointer;\n        padding: 0;\n        opacity: 0.2;\n        transition: opacity 0.3s ease;\n        pointer-events: all;\n        margin: 0;\n        outline: none;\n    }\n    .iziModal-navigate > button:hover{\n        background-color: transparent !important;\n        opacity: 1;\n    }\n    .iziModal-navigate-prev{\n        left: 50%;\n        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAC8CAYAAADCScSrAAAACXBIWXMAAAsTAAALEwEAmpwYAAA5sGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMzIgNzkuMTU5Mjg0LCAyMDE2LzA0LzE5LTEzOjEzOjQwICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgICAgICAgICB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDo2NDkyYzcxMy05ZDM0LTZlNGQtYmUwNi1hMDMyY2Q4NDVjNGU8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDo1QjIzMUMxODU3RjcxMUU2ODUzRkRBRjE5RDhDQjZBRDwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkluc3RhbmNlSUQ+eG1wLmlpZDpjZmMwNzVmNC1kODA3LWI0NDMtYWIwYS02YWVhZjRjMDgxZWE8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx4bXBNTTpEZXJpdmVkRnJvbSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgIDxzdFJlZjppbnN0YW5jZUlEPnhtcC5paWQ6NjQ5MmM3MTMtOWQzNC02ZTRkLWJlMDYtYTAzMmNkODQ1YzRlPC9zdFJlZjppbnN0YW5jZUlEPgogICAgICAgICAgICA8c3RSZWY6ZG9jdW1lbnRJRD54bXAuZGlkOjY0OTJjNzEzLTlkMzQtNmU0ZC1iZTA2LWEwMzJjZDg0NWM0ZTwvc3RSZWY6ZG9jdW1lbnRJRD4KICAgICAgICAgPC94bXBNTTpEZXJpdmVkRnJvbT4KICAgICAgICAgPHhtcE1NOkhpc3Rvcnk+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmNmYzA3NWY0LWQ4MDctYjQ0My1hYjBhLTZhZWFmNGMwODFlYTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNi0wOC0wMVQxMTo1ODowNC0wMzowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICA8L3JkZjpTZXE+CiAgICAgICAgIDwveG1wTU06SGlzdG9yeT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNS41IChXaW5kb3dzKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxNi0wOC0wMVQwOTo0MDo1Ni0wMzowMDwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4yMDE2LTA4LTAxVDExOjU4OjA0LTAzOjAwPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNi0wOC0wMVQxMTo1ODowNC0wMzowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9wbmc8L2RjOmZvcm1hdD4KICAgICAgICAgPHBob3Rvc2hvcDpDb2xvck1vZGU+MzwvcGhvdG9zaG9wOkNvbG9yTW9kZT4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzIwMDAwLzEwMDAwPC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WVJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+NjU1MzU8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjE4ODwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4xODg8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PvAvv7QAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAmdJREFUeNrs1LsJQkEQhtH/mtmBgQ8QA7tQK1e7MBBBMbADwzUZEyuQveeDCXbD4TBDay3SWJpYgYCXgJeAl4CXgJeAl4CXgJeAl4CXgJeAF/AS8BLwEvAS8BLwEvAS8BLwEvAS8BLwAl4CXgJeAl4CXv/WJskpyQJ4jQH7Mcmu0C+BV+/Y5/VeF/oV8Ood+7dpDfDqHvsrySHJBXjBDrxgB16wAy/YgRfswAt24AU78IIdeMEOPOywAw+7gIcdeMEOvGAHXrADL9iBF+zAC3bgBTvwsMMOPOwCHnYBD7uAhx14wQ68YAdesAMv2IEX7MDDDjvwsAt42AU87AIedgEPu4CHXcDDDrxgB16wAw877MDDDjvwsAt42AU87AIedgEPu4CHXcDDLuBhB16wAw877MDDLuBhF/CwC3jYBTzsAh52AQ+7gIddwEtjB3+tS/78+Z/V5d9iATz0Ah56AQ+9gIdewEMv4KEX8NALeOgFPPQCHnoBDz3wgh54QQ889NADDz30wEMv4KEX8NALeOgFPPQCHnoBD72Ahx54QQ+8oAde0AMv6IEX9MBDDz3w0EMPPPQCHnoBD72Ah17AQw+8FUAPvKAHXtADL+iBF/TAC3rgBT3wgh546KEHHnrogYdewEMv4KEHXtADL+iBF/TAC3rgBT3wgh54QQ+8oAde0AMv6IGHHnrgoU/yrgFe3aO/JdknuQOv3tGfC/tjjEsYWmsoyIWXgJeAl4CXgJeAl4CXgJeAl4CXgJeAF/AS8BLwEvAS8BLwEvAS8BLwEvAS8BLwAl4CXgJeAl4CXvqnPgAAAP//AwCEcoCBRabYzAAAAABJRU5ErkJggg==') no-repeat 50% 50%;\n    }\n    .iziModal-navigate-next{\n        right: 50%;\n        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAC8CAYAAADCScSrAAAACXBIWXMAAB3SAAAd0gEUasEwAAA7pGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMzIgNzkuMTU5Mjg0LCAyMDE2LzA0LzE5LTEzOjEzOjQwICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNS41IChXaW5kb3dzKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxNi0wOC0wMVQwOTo0MDoxNC0wMzowMDwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4yMDE2LTA4LTAxVDExOjU4OjEyLTAzOjAwPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNi0wOC0wMVQxMTo1ODoxMi0wMzowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9wbmc8L2RjOmZvcm1hdD4KICAgICAgICAgPHBob3Rvc2hvcDpDb2xvck1vZGU+MzwvcGhvdG9zaG9wOkNvbG9yTW9kZT4KICAgICAgICAgPHhtcE1NOkluc3RhbmNlSUQ+eG1wLmlpZDphZjljN2Q2MC00MTg2LWE3NGQtYTBiMS1mMGU5ODUwYzg2ZGY8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnhtcC5kaWQ6NjQ5MmM3MTMtOWQzNC02ZTRkLWJlMDYtYTAzMmNkODQ1YzRlPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06T3JpZ2luYWxEb2N1bWVudElEPnhtcC5kaWQ6NjQ5MmM3MTMtOWQzNC02ZTRkLWJlMDYtYTAzMmNkODQ1YzRlPC94bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpIaXN0b3J5PgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y3JlYXRlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjY0OTJjNzEzLTlkMzQtNmU0ZC1iZTA2LWEwMzJjZDg0NWM0ZTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNi0wOC0wMVQwOTo0MDoxNC0wMzowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjAxNjJjMmE3LWZmMjYtYzE0ZC05Yjg4LTc2MGM2NzAxYjYzNzwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNi0wOC0wMVQxMTo1MTowNy0wMzowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmFmOWM3ZDYwLTQxODYtYTc0ZC1hMGIxLWYwZTk4NTBjODZkZjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNi0wOC0wMVQxMTo1ODoxMi0wMzowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICA8L3JkZjpTZXE+CiAgICAgICAgIDwveG1wTU06SGlzdG9yeT4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTkzOTAzNi8xMDAwMDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+MTkzOTAzNi8xMDAwMDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT42NTUzNTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MTg4PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjE4ODwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSJ3Ij8+nbt1mgAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAACQklEQVR42uzSsQ3CQAAEQTdiOyGg/wrciJ0QUMYSECEKAP3PSdvAaZZqkWbJCQJeAl4CXgJeAl4CXgJeAl4CXgJeAl4CXsBLwEvAS8BLwEvAS8BLwEvAS8BLwEvAC3gJeAl4CXgJ+D9vrY7qBgLwo7dVZ+89oAd+5Pbq6nPQAz9s9+rZ96AHHnoBD72Ah17AQy/goRfw0At46AU89AIeegEPvYCHHnhBD7ygBx566IGHHnrgoRfw0At46AU89AIeegEPvYCHXsBDL+ChB17QAy/ogRf0wAt64KGHHnjooQceegEPvYCHXsBDL+ChF/DQAy/ogRf0wAt64AU98IIeeEEPvKAHXtADDz30wEPvI+ChF/DQAy/ogRf0wAt64AU98IIeeEEPvKAHXtADL+iBF/TAC3rgoZ8ePRDAAy/YgRfswAt24AU78IIdeMEOvGAHXrADL9iBhx124GEX8LADL9iBF+zAC3bgBTvwgh14wQ68YAcedtiBh13Awy7gYRfwsAMv2IEX7MALduAFO/CCHXjYYQcedgEPu4CHXcDDLuBhF/CwA+8E2IEX7MALduAFO/Cwww487AIedgEPu4CHXcDDLuBhF/CwC3jYgRfswMMOO/CwC3jYBTzsAh52AQ+7gIddwMMu4GEX8LBravB7dcEO/Ext1Qk78DO1VgfswEvAS8BLwEvAS8BLwEvAS8BLwEvAS8ALeAl4CXgJeAl4CXgJeAl4CXgJeAl4CXgBLwEvAS8BLwEvAS/9shcAAAD//wMAtAygvJrkwJUAAAAASUVORK5CYII=') no-repeat 50% 50%;\n    }\n\n.iziModal.isAttachedTop .iziModal-header{\n    border-top-left-radius: 0;\n    border-top-right-radius: 0;\n}\n.iziModal.isAttachedTop{\n    margin-top: 0 !important;\n    margin-bottom: auto !important;\n    border-top-left-radius: 0 !important;\n    border-top-right-radius: 0 !important;\n}\n.iziModal.isAttachedBottom{\n    margin-top: auto !important;\n    margin-bottom: 0 !important;\n    border-bottom-left-radius: 0 !important;\n    border-bottom-right-radius: 0 !important;\n}\n.iziModal.isFullscreen{\n    max-width: 100% !important;\n    margin: 0 !important;\n    height: 100% !important;\n    border-radius: 0 !important;\n}\n.iziModal.isAttached{\n    border-radius: 0 !important;\n}\n.iziModal.hasScroll .iziModal-wrap{\n    overflow-y: auto;\n    overflow-x: hidden;\n}\n\nhtml.iziModal-isOverflow{\n    overflow: hidden;\n}\nhtml.iziModal-isOverflow body, html.iziModal-isAttached body{\n    overflow-y: scroll;\n    position: relative;\n}\nhtml.iziModal-isAttached{\n    overflow: hidden;\n}\n\n/* SCROLL */\n\n.iziModal ::-webkit-scrollbar {\n    overflow: visible;\n    height: 7px;\n    width: 7px;\n}\n.iziModal ::-webkit-scrollbar-thumb {\n    background-color: rgba(0,0,0,.2);\n    background-clip: padding-box;\n    border: solid transparent;\n    border-width: 0px;\n    min-height: 28px;\n    padding: 100px 0 0;\n    box-shadow: inset 1px 1px 0 rgba(0,0,0,.1),inset 0 -1px 0 rgba(0,0,0,.07);\n}\n.iziModal ::-webkit-scrollbar-thumb:active {\n    background-color: rgba(0,0,0,.4);\n}\n.iziModal ::-webkit-scrollbar-button {\n    height: 0;\n    width: 0;\n}\n.iziModal ::-webkit-scrollbar-track {\n    background-clip: padding-box;\n    border: solid transparent;\n    border-width: 0 0 0 2px;\n}\n\n/* MODAL ANIMATIONS */\n\n.iziModal.transitionIn .iziModal-header{\n    -webkit-animation: iziM-slideDown 0.7s cubic-bezier(0.7,0,0.3,1);\n    -moz-animation: iziM-slideDown 0.7s cubic-bezier(0.7,0,0.3,1);\n    animation: iziM-slideDown 0.7s cubic-bezier(0.7,0,0.3,1);\n}\n    .iziModal.transitionIn .iziModal-header .iziModal-header-icon {\n        -webkit-animation: iziM-revealIn 1s cubic-bezier(.16,.81,.32,1) both;\n        -moz-animation: iziM-revealIn 1s cubic-bezier(.16,.81,.32,1) both;\n        animation: iziM-revealIn 1s cubic-bezier(.16,.81,.32,1) both;\n    }\n    .iziModal.transitionIn .iziModal-header .iziModal-header-title, .iziModal.transitionIn .iziModal-header .iziModal-header-subtitle {\n        -webkit-animation: iziM-slideIn 1s cubic-bezier(.16,.81,.32,1) both;\n        -moz-animation: iziM-slideIn 1s cubic-bezier(.16,.81,.32,1) both;\n        animation: iziM-slideIn 1s cubic-bezier(.16,.81,.32,1) both;\n    }\n\n    .iziModal.transitionIn .iziModal-header .iziModal-button{\n        -webkit-animation: iziM-revealIn 1.2s cubic-bezier(0.7,0,0.3,1);\n        -moz-animation: iziM-revealIn 1.2s cubic-bezier(0.7,0,0.3,1);\n        animation: iziM-revealIn 1.2s cubic-bezier(0.7,0,0.3,1);\n    }\n\n.iziModal.transitionIn .iziModal-iframe, .iziModal.transitionIn .iziModal-wrap{\n    -webkit-animation: iziM-fadeIn 1.3s;\n    -moz-animation: iziM-fadeIn 1.3s;\n    animation: iziM-fadeIn 1.3s;\n}\n.iziModal.transitionIn .iziModal-header {\n    -webkit-animation-delay: 0.0s;\n    -moz-animation: 0.0s;\n    animation-delay: 0.0s;\n}\n.iziModal.transitionIn .iziModal-header .iziModal-header-icon,\n.iziModal.transitionIn .iziModal-header .iziModal-header-title {\n    -webkit-animation-delay: 0.4s;\n    -moz-animation: 0.4s;\n    animation-delay: 0.4s;\n}\n.iziModal.transitionIn .iziModal-header .iziModal-header-subtitle {\n    -webkit-animation-delay: 0.5s;\n    -moz-animation: 0.5s;\n    animation-delay: 0.5s;\n}\n\n    .iziModal.transitionOut .iziModal-header, .iziModal.transitionOut .iziModal-header *{\n        transition: none !important;\n    }\n\n/* ANIMATIONS */\n\n.iziModal.fadeOut, .iziModal-overlay.fadeOut, .iziModal-navigate.fadeOut, .iziModal .fadeOut{\n    -webkit-animation: iziM-fadeOut 0.5s;\n    -moz-animation: iziM-fadeOut 0.5s;\n    animation: iziM-fadeOut 0.5s;\n    animation-fill-mode: forwards;\n}\n.iziModal.fadeIn, .iziModal-overlay.fadeIn, .iziModal-navigate.fadeIn, .iziModal .fadeIn {\n    -webkit-animation: iziM-fadeIn 0.5s;\n    -moz-animation: iziM-fadeIn 0.5s;\n    animation: iziM-fadeIn 0.5s;\n}\n.iziModal.comingIn, .iziModal-overlay.comingIn {\n    -webkit-animation: iziM-comingIn 0.5s ease;\n    -moz-animation: iziM-comingIn 0.5s ease;\n    animation: iziM-comingIn 0.5s ease;\n}\n.iziModal.comingOut, .iziModal-overlay.comingOut {\n    -webkit-animation: iziM-comingOut 0.5s cubic-bezier(.16,.81,.32,1);\n    -moz-animation: iziM-comingOut 0.5s cubic-bezier(.16,.81,.32,1);\n    animation: iziM-comingOut 0.5s cubic-bezier(.16,.81,.32,1);\n    animation-fill-mode: forwards;\n}\n.iziModal.bounceInDown, .iziModal-overlay.bounceInDown {\n    -webkit-animation: iziM-bounceInDown 0.7s ease;\n    animation: iziM-bounceInDown 0.7s ease;\n}\n.iziModal.bounceOutDown, .iziModal-overlay.bounceOutDown {\n    -webkit-animation: iziM-bounceOutDown 0.7s ease;\n    animation: iziM-bounceOutDown 0.7s ease;\n}\n.iziModal.bounceInUp, .iziModal-overlay.bounceInUp {\n    -webkit-animation: iziM-bounceInUp 0.7s ease;\n    animation: iziM-bounceInUp 0.7s ease;\n}\n.iziModal.bounceOutUp, .iziModal-overlay.bounceOutUp {\n    -webkit-animation: iziM-bounceOutUp 0.7s ease;\n    animation: iziM-bounceOutUp 0.7s ease;\n}\n.iziModal.fadeInDown, .iziModal-overlay.fadeInDown {\n    -webkit-animation: iziM-fadeInDown 0.7s cubic-bezier(.16,.81,.32,1);\n    animation: iziM-fadeInDown 0.7s cubic-bezier(.16,.81,.32,1);\n}\n.iziModal.fadeOutDown, .iziModal-overlay.fadeOutDown {\n    -webkit-animation: iziM-fadeOutDown 0.5s ease;\n    animation: iziM-fadeOutDown 0.5s ease;\n}\n.iziModal.fadeInUp, .iziModal-overlay.fadeInUp {\n    -webkit-animation: iziM-fadeInUp 0.7s cubic-bezier(.16,.81,.32,1);\n    animation: iziM-fadeInUp 0.7s cubic-bezier(.16,.81,.32,1);\n}\n.iziModal.fadeOutUp, .iziModal-overlay.fadeOutUp {\n    -webkit-animation: iziM-fadeOutUp 0.5s ease;\n    animation: iziM-fadeOutUp 0.5s ease;\n}\n.iziModal.fadeInLeft, .iziModal-overlay.fadeInLeft {\n    -webkit-animation: iziM-fadeInLeft 0.7s cubic-bezier(.16,.81,.32,1);\n    animation: iziM-fadeInLeft 0.7s cubic-bezier(.16,.81,.32,1);\n}\n.iziModal.fadeOutLeft, .iziModal-overlay.fadeOutLeft {\n    -webkit-animation: iziM-fadeOutLeft 0.5s ease;\n    animation: iziM-fadeOutLeft 0.5s ease;\n}\n.iziModal.fadeInRight, .iziModal-overlay.fadeInRight {\n    -webkit-animation: iziM-fadeInRight 0.7s cubic-bezier(.16,.81,.32,1);\n    animation: iziM-fadeInRight 0.7s cubic-bezier(.16,.81,.32,1);\n}\n.iziModal.fadeOutRight, .iziModal-overlay.fadeOutRight {\n    -webkit-animation: iziM-fadeOutRight 0.5s ease;\n    animation: iziM-fadeOutRight 0.5s ease;\n}\n.iziModal.flipInX, .iziModal-overlay.flipInX {\n    -webkit-animation: iziM-flipInX 0.7s ease;\n    animation: iziM-flipInX 0.7s ease;\n}\n.iziModal.flipOutX, .iziModal-overlay.flipOutX {\n    -webkit-animation: iziM-flipOutX 0.7s ease;\n    animation: iziM-flipOutX 0.7s ease;\n}\n\n@-webkit-keyframes iziM-comingIn {\n    0% {\n        opacity: 0;\n        transform: scale(0.9) translateY(-20px) perspective( 600px ) rotateX( 10deg );\n    }\n    100% {\n        opacity: 1;\n        transform: scale(1) translateY(0) perspective( 600px ) rotateX( 0 );\n    }\n}\n@-moz-keyframes iziM-comingIn {\n    0% {\n        opacity: 0;\n        transform: scale(0.9) translateY(-20px) perspective( 600px ) rotateX( 10deg );\n    }\n    100% {\n        opacity: 1;\n        transform: scale(1) translateY(0) perspective( 600px ) rotateX( 0 );\n    }\n}\n@keyframes iziM-comingIn {\n    0% {\n        opacity: 0;\n        /*transform: translateY(-20px) perspective( 600px ) rotateX( 10deg );*/\n        transform: scale(0.9) translateY(-20px) perspective( 600px ) rotateX( 10deg );\n    }\n    100% {\n        opacity: 1;\n        /*transform: scale(1) translateY(0) perspective( 600px ) rotateX( 0 ); */\n        transform: scale(1) translateY(0) perspective( 600px ) rotateX( 0 );\n    }\n}\n\n@-webkit-keyframes iziM-comingOut {\n    0% {\n        opacity: 1;\n        transform: scale(1);\n    }\n    100% {\n        opacity: 0;\n        transform: scale(0.9);\n    }\n}\n@-moz-keyframes iziM-comingOut {\n    0% {\n        opacity: 1;\n        transform: scale(1);\n    }\n    100% {\n        opacity: 0;\n        transform: scale(0.9);\n    }\n}\n@keyframes iziM-comingOut {\n    0% {\n        opacity: 1;\n        transform: scale(1);\n    }\n    100% {\n        opacity: 0;\n        transform: scale(0.9);\n    }\n}\n@-webkit-keyframes iziM-fadeOut {\n    0% {opacity: 1;}\n    100% {opacity: 0;}\n}\n@-moz-keyframes iziM-fadeOut {\n    0% {opacity: 1;}\n    100% {opacity: 0;}\n}\n@keyframes iziM-fadeOut {\n    0% {opacity: 1;}\n    100% {opacity: 0;}\n}\n\n@-webkit-keyframes iziM-fadeIn {\n    0% {opacity: 0;}\n    100% {opacity: 1;}\n}\n@-moz-keyframes iziM-fadeIn {\n    0% {opacity: 0;}\n    100% {opacity: 1;}\n}\n@keyframes iziM-fadeIn {\n    0% {opacity: 0;}\n    100% {opacity: 1;}\n}\n\n@-webkit-keyframes iziM-slideIn {\n    0% {\n        opacity: 0;\n        -webkit-transform: translateX(50px);\n    }\n    100% {\n        opacity: 1;\n        -webkit-transform: translateX(0);\n    }\n}\n@-moz-keyframes iziM-slideIn {\n    0% {\n        opacity: 0;\n        -moz-transform: translateX(50px);\n    }\n    100% {\n        opacity: 1;\n        -moz-transform: translateX(0);\n    }\n}\n@keyframes iziM-slideIn {\n    0% {\n        opacity: 0;\n        transform: translateX(50px);\n    }\n    100% {\n        opacity: 1;\n        transform: translateX(0);\n    }\n}\n\n@-webkit-keyframes iziM-slideDown {\n    0% { opacity: 0; -webkit-transform: scale(1,0) translateY(-40px); -webkit-transform-origin: center top; }\n}\n@-moz-keyframes iziM-slideDown {\n    0% { opacity: 0; -moz-transform: scale(1,0) translateY(-40px); -moz-transform-origin: center top; }\n}\n@keyframes iziM-slideDown {\n    0% { opacity: 0; transform: scale(1,0) translateY(-40px); transform-origin: center top; }\n}\n\n@-webkit-keyframes iziM-revealIn {\n    0% { opacity: 0; -webkit-transform: scale3d(0.3,0.3,1); }\n}\n@-moz-keyframes iziM-revealIn {\n    0% { opacity: 0; -moz-transform: scale3d(0.3,0.3,1); }\n}\n@keyframes iziM-revealIn {\n    0% { opacity: 0; transform: scale3d(0.3,0.3,1); }\n}\n\n@-webkit-keyframes iziM-bounceInDown {\n    from, 60%, 75%, 90%, to {\n        -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n        animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n    }\n    0% {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -1000px, 0);\n        transform: translate3d(0, -1000px, 0);\n    }\n    60% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, 25px, 0);\n        transform: translate3d(0, 25px, 0);\n    }\n    75% {\n        -webkit-transform: translate3d(0, -10px, 0);\n        transform: translate3d(0, -10px, 0);\n    }\n    90% {\n        -webkit-transform: translate3d(0, 5px, 0);\n        transform: translate3d(0, 5px, 0);\n    }\n    to {\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n@keyframes iziM-bounceInDown {\n    from, 60%, 75%, 90%, to {\n        -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n        animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n    }\n    0% {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -1000px, 0);\n        transform: translate3d(0, -1000px, 0);\n    }\n    60% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, 25px, 0);\n        transform: translate3d(0, 25px, 0);\n    }\n    75% {\n        -webkit-transform: translate3d(0, -10px, 0);\n        transform: translate3d(0, -10px, 0);\n    }\n    90% {\n        -webkit-transform: translate3d(0, 5px, 0);\n        transform: translate3d(0, 5px, 0);\n    }\n    to {\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n@-webkit-keyframes iziM-bounceOutDown {\n    20% {\n        -webkit-transform: translate3d(0, 10px, 0);\n        transform: translate3d(0, 10px, 0);\n    }\n    40%, 45% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, -20px, 0);\n        transform: translate3d(0, -20px, 0);\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 1000px, 0);\n        transform: translate3d(0, 1000px, 0);\n    }\n}\n@keyframes iziM-bounceOutDown {\n    20% {\n        -webkit-transform: translate3d(0, 10px, 0);\n        transform: translate3d(0, 10px, 0);\n    }\n    40%, 45% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, -20px, 0);\n        transform: translate3d(0, -20px, 0);\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 1000px, 0);\n        transform: translate3d(0, 1000px, 0);\n    }\n}\n\n@-webkit-keyframes iziM-bounceInUp {\n    from, 60%, 75%, 90%, to {\n        -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n        animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n    }\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 1000px, 0);\n        transform: translate3d(0, 1000px, 0);\n    }\n    60% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, -20px, 0);\n        transform: translate3d(0, -20px, 0);\n    }\n    75% {\n        -webkit-transform: translate3d(0, 10px, 0);\n        transform: translate3d(0, 10px, 0);\n    }\n    90% {\n        -webkit-transform: translate3d(0, -5px, 0);\n        transform: translate3d(0, -5px, 0);\n    }\n    to {\n        -webkit-transform: translate3d(0, 0, 0);\n        transform: translate3d(0, 0, 0);\n    }\n}\n@keyframes iziM-bounceInUp {\n    from, 60%, 75%, 90%, to {\n        -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n        animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);\n    }\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 1000px, 0);\n        transform: translate3d(0, 1000px, 0);\n    }\n    60% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, -20px, 0);\n        transform: translate3d(0, -20px, 0);\n    }\n    75% {\n        -webkit-transform: translate3d(0, 10px, 0);\n        transform: translate3d(0, 10px, 0);\n    }\n    90% {\n        -webkit-transform: translate3d(0, -5px, 0);\n        transform: translate3d(0, -5px, 0);\n    }\n    to {\n        -webkit-transform: translate3d(0, 0, 0);\n        transform: translate3d(0, 0, 0);\n    }\n}\n\n@-webkit-keyframes iziM-bounceOutUp {\n    20% {\n        -webkit-transform: translate3d(0, -10px, 0);\n        transform: translate3d(0, -10px, 0);\n    }\n    40%, 45% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, 20px, 0);\n        transform: translate3d(0, 20px, 0);\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -2000px, 0);\n        transform: translate3d(0, -2000px, 0);\n    }\n}\n@keyframes iziM-bounceOutUp {\n    20% {\n        -webkit-transform: translate3d(0, -10px, 0);\n        transform: translate3d(0, -10px, 0);\n    }\n    40%, 45% {\n        opacity: 1;\n        -webkit-transform: translate3d(0, 20px, 0);\n        transform: translate3d(0, 20px, 0);\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -1000px, 0);\n        transform: translate3d(0, -1000px, 0);\n    }\n}\n\n@-webkit-keyframes iziM-fadeInDown {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -100px, 0);\n        transform: translate3d(0, -100px, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n@keyframes iziM-fadeInDown {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -100px, 0);\n        transform: translate3d(0, -100px, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n\n@-webkit-keyframes iziM-fadeOutDown {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 100px, 0);\n        transform: translate3d(0, 100px, 0);\n    }\n}\n@keyframes iziM-fadeOutDown {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 100px, 0);\n        transform: translate3d(0, 100px, 0);\n    }\n}\n\n@-webkit-keyframes iziM-fadeInUp {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 100px, 0);\n        transform: translate3d(0, 100px, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n@keyframes iziM-fadeInUp {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(0, 100px, 0);\n        transform: translate3d(0, 100px, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n\n@-webkit-keyframes iziM-fadeOutUp {\n    from {\n        opacity: 1;\n    }\n\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -100px, 0);\n        transform: translate3d(0, -100px, 0);\n    }\n}\n@keyframes iziM-fadeOutUp {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(0, -100px, 0);\n        transform: translate3d(0, -100px, 0);\n    }\n}\n\n@-webkit-keyframes iziM-fadeInLeft {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(-200px, 0, 0);\n        transform: translate3d(-200px, 0, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n@keyframes iziM-fadeInLeft {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(-200px, 0, 0);\n        transform: translate3d(-200px, 0, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n\n@-webkit-keyframes iziM-fadeOutLeft {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(-200px, 0, 0);\n        transform: translate3d(-200px, 0, 0);\n    }\n}\n@keyframes iziM-fadeOutLeft {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(-200px, 0, 0);\n        transform: translate3d(-200px, 0, 0);\n    }\n}\n\n@-webkit-keyframes iziM-fadeInRight {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(200px, 0, 0);\n        transform: translate3d(200px, 0, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n@keyframes iziM-fadeInRight {\n    from {\n        opacity: 0;\n        -webkit-transform: translate3d(200px, 0, 0);\n        transform: translate3d(200px, 0, 0);\n    }\n    to {\n        opacity: 1;\n        -webkit-transform: none;\n        transform: none;\n    }\n}\n\n@-webkit-keyframes iziM-fadeOutRight {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(200px, 0, 0);\n        transform: translate3d(200px, 0, 0);\n    }\n}\n@keyframes iziM-fadeOutRight {\n    from {\n        opacity: 1;\n    }\n    to {\n        opacity: 0;\n        -webkit-transform: translate3d(200px, 0, 0);\n        transform: translate3d(200px, 0, 0);\n    }\n}\n\n@-webkit-keyframes iziM-flipInX {\n    0% {\n        -webkit-transform: perspective(400px) rotateX(60deg);\n        opacity: 0;\n    }\n    40% {\n        -webkit-transform: perspective(400px) rotateX(-10deg);\n    }\n    70% {\n        -webkit-transform: perspective(400px) rotateX(10deg);\n    }\n    100% {\n        -webkit-transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n}\n@keyframes iziM-flipInX {\n    0% {\n        transform: perspective(400px) rotateX(60deg);\n        opacity: 0;\n    }\n    40% {\n        transform: perspective(400px) rotateX(-10deg);\n    }\n    70% {\n        transform: perspective(400px) rotateX(10deg);\n    }\n    100% {\n        transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n}\n\n@-webkit-keyframes iziM-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, 40deg);\n        transform: perspective(400px) rotate3d(1, 0, 0, 40deg);\n        opacity: 0;\n    }\n}\n@keyframes iziM-flipOutX {\n    from {\n        -webkit-transform: perspective(400px);\n        transform: perspective(400px);\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    to {\n        -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 40deg);\n        transform: perspective(400px) rotate3d(1, 0, 0, 40deg);\n        opacity: 0;\n    }\n}\n"
  },
  {
    "path": "assets/vendor/izimodal/iziModal.js",
    "content": "/*\r\n* iziModal | v1.5.1\r\n* http://izimodal.marcelodolce.com\r\n* by Marcelo Dolce.\r\n*/\r\n(function (factory) {\r\n    if (typeof define === 'function' && define.amd) {\r\n        define(['jquery'], factory);\r\n    } else if (typeof module === 'object' && module.exports) {\r\n        module.exports = function( root, jQuery ) {\r\n            if ( jQuery === undefined ) {\r\n                if ( typeof window !== 'undefined' ) {\r\n                    jQuery = require('jquery');\r\n                }\r\n                else {\r\n                    jQuery = require('jquery')(root);\r\n                }\r\n            }\r\n            factory(jQuery);\r\n            return jQuery;\r\n        };\r\n    } else {\r\n        factory(jQuery);\r\n    }\r\n}(function ($) {\r\n\r\n\t\tvar $window = $(window),\r\n\t    \t$document = $(document),\r\n\t\t\tPLUGIN_NAME = 'iziModal',\r\n\t\t\tSTATES = {\r\n\t\t\tCLOSING: 'closing',\r\n\t\t\tCLOSED: 'closed',\r\n\t\t\tOPENING: 'opening',\r\n\t\t\tOPENED: 'opened',\r\n\t\t\tDESTROYED: 'destroyed'\r\n\t\t};\r\n\r\n\t\tfunction whichAnimationEvent(){\r\n\t\t\tvar t,\r\n\t\t\t\tel = document.createElement(\"fakeelement\"),\r\n\t\t\t\tanimations = {\r\n\t\t\t\t\"animation\"      : \"animationend\",\r\n\t\t\t\t\"OAnimation\"     : \"oAnimationEnd\",\r\n\t\t\t\t\"MozAnimation\"   : \"animationend\",\r\n\t\t\t\t\"WebkitAnimation\": \"webkitAnimationEnd\"\r\n\t\t\t};\r\n\t\t\tfor (t in animations){\r\n\t\t\t\tif (el.style[t] !== undefined){\r\n\t\t\t\t\treturn animations[t];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfunction isIE(version) {\r\n\t\t\tif(version === 9){\r\n\t\t\t\treturn navigator.appVersion.indexOf(\"MSIE 9.\") !== -1;\r\n\t\t\t} else {\r\n\t\t\t\tuserAgent = navigator.userAgent;\r\n\t\t\t\treturn userAgent.indexOf(\"MSIE \") > -1 || userAgent.indexOf(\"Trident/\") > -1;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfunction clearValue(value){\r\n\t\t\tvar separators = /%|px|em|cm|vh|vw/;\r\n\t\t\treturn parseInt(String(value).split(separators)[0]);\r\n\t\t}\r\n\r\n\t\tvar animationEvent = whichAnimationEvent(),\r\n\t\t\tisMobile = (/Mobi/.test(navigator.userAgent)) ? true : false;\r\n\r\n\t\twindow.$iziModal = {};\r\n\t\twindow.$iziModal.autoOpen = 0;\r\n\t\twindow.$iziModal.history = false;\r\n\r\n\t\tvar iziModal = function (element, options) {\r\n\t\t\tthis.init(element, options);\r\n\t\t};\r\n\r\n\t\tiziModal.prototype = {\r\n\r\n\t\t\tconstructor: iziModal,\r\n\r\n\t\t\tinit: function (element, options) {\r\n\r\n\t\t\t\tvar that = this;\r\n\t\t\t\tthis.$element = $(element);\r\n\r\n\t\t\t\tif(this.$element[0].id !== undefined && this.$element[0].id !== ''){\r\n\t\t\t\t\tthis.id = this.$element[0].id;\t\r\n\t\t\t\t} else {\r\n\t\t\t\t\tthis.id = PLUGIN_NAME+Math.floor((Math.random() * 10000000) + 1);\r\n\t\t\t\t\tthis.$element.attr('id', this.id);\r\n\t\t\t\t}\r\n\t\t\t\tthis.classes = ( this.$element.attr('class') !== undefined ) ? this.$element.attr('class') : '';\r\n\t\t\t\tthis.content = this.$element.html();\r\n\t\t\t\tthis.state = STATES.CLOSED;\r\n\t\t\t\tthis.options = options;\r\n\t\t\t\tthis.width = 0;\r\n\t\t\t\tthis.timer = null;\r\n\t\t\t\tthis.timerTimeout = null;\r\n\t\t\t\tthis.progressBar = null;\r\n\t            this.isPaused = false;\r\n\t\t\t\tthis.isFullscreen = false;\r\n\t            this.headerHeight = 0;\r\n\t            this.modalHeight = 0;\r\n\t            this.$overlay = $('<div class=\"'+PLUGIN_NAME+'-overlay\" style=\"background-color:'+options.overlayColor+'\"></div>');\r\n\t\t\t\tthis.$navigate = $('<div class=\"'+PLUGIN_NAME+'-navigate\"><div class=\"'+PLUGIN_NAME+'-navigate-caption\">Use</div><button class=\"'+PLUGIN_NAME+'-navigate-prev\"></button><button class=\"'+PLUGIN_NAME+'-navigate-next\"></button></div>');\r\n\t            this.group = {\r\n\t            \tname: this.$element.attr('data-'+PLUGIN_NAME+'-group'),\r\n\t            \tindex: null,\r\n\t            \tids: []\r\n\t            };\r\n\t\t\t\tthis.$element.attr('aria-hidden', 'true');\r\n\t\t\t\tthis.$element.attr('aria-labelledby', this.id);\r\n\t\t\t\tthis.$element.attr('role', 'dialog');\r\n\r\n\t\t\t\tif( !this.$element.hasClass('iziModal') ){\r\n\t\t\t\t\tthis.$element.addClass('iziModal');\r\n\t\t\t\t}\r\n\r\n\t            if(this.group.name === undefined && options.group !== \"\"){\r\n\t            \tthis.group.name = options.group;\r\n\t            \tthis.$element.attr('data-'+PLUGIN_NAME+'-group', options.group);\r\n\t            }\r\n\t            if(this.options.loop === true){\r\n\t            \tthis.$element.attr('data-'+PLUGIN_NAME+'-loop', true);\r\n\t            }\r\n\r\n\t            $.each( this.options , function(index, val) {\r\n\t\t\t\t\tvar attr = that.$element.attr('data-'+PLUGIN_NAME+'-'+index);\r\n\t            \ttry {\r\n\t\t\t            if(typeof attr !== typeof undefined){\r\n\r\n\t\t\t\t\t\t\tif(attr === \"\" || attr == \"true\"){\r\n\t\t\t\t\t\t\t\toptions[index] = true;\r\n\t\t\t\t\t\t\t} else if (attr == \"false\") {\r\n\t\t\t\t\t\t\t\toptions[index] = false;\r\n\t\t\t\t\t\t\t} else if (typeof val == 'function') {\r\n\t\t\t\t\t\t\t\toptions[index] = new Function(attr);\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\toptions[index] = attr;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t            }\r\n\t            \t} catch(exc){}\r\n\t            });\r\n\r\n\t            if(options.appendTo !== false){\r\n\t\t\t\t\tthis.$element.appendTo(options.appendTo);            \t\r\n\t            }\r\n\r\n\t            if (options.iframe === true) {\r\n\t                this.$element.html('<div class=\"'+PLUGIN_NAME+'-wrap\"><div class=\"'+PLUGIN_NAME+'-content\"><iframe class=\"'+PLUGIN_NAME+'-iframe\"></iframe>' + this.content + \"</div></div>\");\r\n\t                \r\n\t\t            if (options.iframeHeight !== null) {\r\n\t\t                this.$element.find('.'+PLUGIN_NAME+'-iframe').css('height', options.iframeHeight);\r\n\t\t            }\r\n\t            } else {\r\n\t            \tthis.$element.html('<div class=\"'+PLUGIN_NAME+'-wrap\"><div class=\"'+PLUGIN_NAME+'-content\">' + this.content + '</div></div>');\r\n\t            }\r\n\r\n\t\t\t\tif (this.options.background !== null) {\r\n\t\t\t\t\tthis.$element.css('background', this.options.background);\r\n\t\t\t\t}\r\n\r\n\t            this.$wrap = this.$element.find('.'+PLUGIN_NAME+'-wrap');\r\n\r\n\t\t\t\tif(options.zindex !== null && !isNaN(parseInt(options.zindex)) ){\r\n\t\t\t\t \tthis.$element.css('z-index', options.zindex);\r\n\t\t\t\t \tthis.$navigate.css('z-index', options.zindex-1);\r\n\t\t\t\t \tthis.$overlay.css('z-index', options.zindex-2);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif(options.radius !== \"\"){\r\n\t                this.$element.css('border-radius', options.radius);\r\n\t            }\r\n\r\n\t            if(options.padding !== \"\"){\r\n\t                this.$element.find('.'+PLUGIN_NAME+'-content').css('padding', options.padding);\r\n\t            }\r\n\r\n\t            if(options.theme !== \"\"){\r\n\t\t\t\t\tif(options.theme === \"light\"){\r\n\t\t\t\t\t\tthis.$element.addClass(PLUGIN_NAME+'-light');\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tthis.$element.addClass(options.theme);\r\n\t\t\t\t\t}\r\n\t            }\r\n\r\n\t\t\t\tif(options.rtl === true) {\r\n\t\t\t\t\tthis.$element.addClass(PLUGIN_NAME+'-rtl');\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif(options.openFullscreen === true){\r\n\t\t\t\t    this.isFullscreen = true;\r\n\t\t\t\t    this.$element.addClass('isFullscreen');\r\n\t\t\t\t}\r\n\r\n\t\t\t\tthis.createHeader();\r\n\t\t\t\tthis.recalcWidth();\r\n\t\t\t\tthis.recalcVerticalPos();\r\n\r\n\t\t\t\tif (that.options.afterRender && ( typeof(that.options.afterRender) === \"function\" || typeof(that.options.afterRender) === \"object\" ) ) {\r\n\t\t\t        that.options.afterRender(that);\r\n\t\t\t    }\r\n\r\n\t\t\t},\r\n\r\n\t\t\tcreateHeader: function(){\r\n\r\n\t\t\t\tthis.$header = $('<div class=\"'+PLUGIN_NAME+'-header\"><h2 class=\"'+PLUGIN_NAME+'-header-title\">' + this.options.title + '</h2><p class=\"'+PLUGIN_NAME+'-header-subtitle\">' + this.options.subtitle + '</p><div class=\"'+PLUGIN_NAME+'-header-buttons\"></div></div>');\r\n\r\n\t\t\t\tif (this.options.closeButton === true) {\r\n\t\t\t\t\tthis.$header.find('.'+PLUGIN_NAME+'-header-buttons').append('<a href=\"javascript:void(0)\" class=\"'+PLUGIN_NAME+'-button '+PLUGIN_NAME+'-button-close\" data-'+PLUGIN_NAME+'-close></a>');\r\n\t\t\t\t}\r\n\t            \r\n\t            if (this.options.fullscreen === true) {\r\n\t            \tthis.$header.find('.'+PLUGIN_NAME+'-header-buttons').append('<a href=\"javascript:void(0)\" class=\"'+PLUGIN_NAME+'-button '+PLUGIN_NAME+'-button-fullscreen\" data-'+PLUGIN_NAME+'-fullscreen></a>');\r\n\t            }\r\n\r\n\t\t\t\tif (this.options.timeoutProgressbar === true && !isNaN(parseInt(this.options.timeout)) && this.options.timeout !== false && this.options.timeout !== 0) {\r\n\t\t\t\t\tthis.$header.prepend('<div class=\"'+PLUGIN_NAME+'-progressbar\"><div style=\"background-color:'+this.options.timeoutProgressbarColor+'\"></div></div>');\r\n\t            }\r\n\r\n\t            if (this.options.subtitle === '') {\r\n\t        \t\tthis.$header.addClass(PLUGIN_NAME+'-noSubtitle');\r\n\t            }\r\n\r\n\t            if (this.options.title !== \"\") {\r\n\r\n\t                if (this.options.headerColor !== null) {\r\n\t                \tif(this.options.borderBottom === true){\r\n\t                    \tthis.$element.css('border-bottom', '3px solid ' + this.options.headerColor + '');\t                \t\t\r\n\t                \t}\r\n\t                    this.$header.css('background', this.options.headerColor);\r\n\t                }\r\n\t\t\t\t\tif (this.options.icon !== null || this.options.iconText !== null){\r\n\r\n\t                    this.$header.prepend('<i class=\"'+PLUGIN_NAME+'-header-icon\"></i>');\r\n\r\n\t\t                if (this.options.icon !== null) {\r\n\t\t                    this.$header.find('.'+PLUGIN_NAME+'-header-icon').addClass(this.options.icon).css('color', this.options.iconColor);\r\n\t\t\t\t\t\t}\r\n\t\t                if (this.options.iconText !== null){\r\n\t\t                \tthis.$header.find('.'+PLUGIN_NAME+'-header-icon').html(this.options.iconText);\r\n\t\t                }\r\n\t\t\t\t\t}\r\n\t                this.$element.css('overflow', 'hidden').prepend(this.$header);\r\n\t            }\r\n\t\t\t},\r\n\r\n\t\t\tsetGroup: function(groupName){\r\n\r\n\t\t\t\tvar that = this,\r\n\t\t\t\t\tgroup = this.group.name || groupName;\r\n\t\t\t\t\tthis.group.ids = [];\r\n\r\n\t\t\t\tif( groupName !== undefined && groupName !== this.group.name){\r\n\t\t\t\t\tgroup = groupName;\r\n\t\t\t\t\tthis.group.name = group;\r\n\t\t\t\t\tthis.$element.attr('data-'+PLUGIN_NAME+'-group', group);\t\t\t\t\r\n\t\t\t\t}\r\n\t\t\t\tif(group !== undefined && group !== \"\"){\r\n\r\n\t            \tvar count = 0;\r\n\t            \t$.each( $('.'+PLUGIN_NAME+'[data-'+PLUGIN_NAME+'-group='+group+']') , function(index, val) {\r\n\r\n\t\t\t\t\t\tthat.group.ids.push($(this)[0].id);\r\n\r\n\t\t\t\t\t\tif(that.id == $(this)[0].id){\r\n\t\t\t\t\t\t\tthat.group.index = count;\r\n\t\t\t\t\t\t}\r\n\t        \t\t\tcount++;\r\n\t            \t});\r\n\t            }\r\n\t\t\t},\r\n\r\n\t\t\ttoggle: function () {\r\n\r\n\t\t\t\tif(this.state == STATES.OPENED){\r\n\t\t\t\t\tthis.close();\r\n\t\t\t\t}\r\n\t\t\t\tif(this.state == STATES.CLOSED){\r\n\t\t\t\t\tthis.open();\r\n\t\t\t\t}\r\n\t\t\t},\r\n\r\n\t\t\topen: function (param) {\r\n\r\n\t\t\t\tvar that = this;\r\n\r\n\t\t\t\t$.each( $('.'+PLUGIN_NAME) , function(index, modal) {\r\n\t\t\t\t\tif( $(modal).data().iziModal !== undefined ){\r\n\t\t\t\t\t\tvar state = $(modal).iziModal('getState');\r\n\t\t\t\t\t\tif(state == 'opened' || state == 'opening'){\r\n\t\t\t\t\t\t\t$(modal).iziModal('close');\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t});\r\n\r\n\t            (function urlHash(){\r\n\t\t\t\t\tif(that.options.history){\r\n\t\t            \tvar oldTitle = document.title;\r\n\t\t\t            document.title = oldTitle + \" - \" + that.options.title;\r\n\t\t\t\t\t\tdocument.location.hash = that.id;\r\n\t\t\t\t\t\tdocument.title = oldTitle;\r\n\t\t\t\t\t\t//history.pushState({}, that.options.title, \"#\"+that.id);\r\n\t\t\t\t\t\twindow.$iziModal.history = true;\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\twindow.$iziModal.history = false;\r\n\t\t\t\t\t}\r\n\t            })();\r\n\r\n\t\t\t\tfunction opened(){\r\n\t\t\t\t    \r\n\t\t\t\t    // console.info('[ '+PLUGIN_NAME+' | '+that.id+' ] Opened.');\r\n\r\n\t\t\t\t\tthat.state = STATES.OPENED;\r\n\t\t\t    \tthat.$element.trigger(STATES.OPENED);\r\n\r\n\t\t\t\t\tif (that.options.onOpened && ( typeof(that.options.onOpened) === \"function\" || typeof(that.options.onOpened) === \"object\" ) ) {\r\n\t\t\t\t        that.options.onOpened(that);\r\n\t\t\t\t    }\r\n\t\t\t\t}\r\n\r\n\t\t\t\tfunction bindEvents(){\r\n\r\n\t\t            // Close when button pressed\r\n\t\t            that.$element.off('click', '[data-'+PLUGIN_NAME+'-close]').on('click', '[data-'+PLUGIN_NAME+'-close]', function (e) {\r\n\t\t                e.preventDefault();\r\n\r\n\t\t                var transition = $(e.currentTarget).attr('data-'+PLUGIN_NAME+'-transitionOut');\r\n\r\n\t\t                if(transition !== undefined){\r\n\t\t                \tthat.close({transition:transition});\r\n\t\t                } else {\r\n\t\t                \tthat.close();\r\n\t\t                }\r\n\t\t            });\r\n\r\n\t\t            // Expand when button pressed\r\n\t\t            that.$element.off('click', '[data-'+PLUGIN_NAME+'-fullscreen]').on('click', '[data-'+PLUGIN_NAME+'-fullscreen]', function (e) {\r\n\t\t                e.preventDefault();\r\n\t\t                if(that.isFullscreen === true){\r\n\t\t\t\t\t\t\tthat.isFullscreen = false;\r\n\t\t\t                that.$element.removeClass('isFullscreen');\r\n\t\t                } else {\r\n\t\t\t                that.isFullscreen = true;\r\n\t\t\t                that.$element.addClass('isFullscreen');\r\n\t\t                }\r\n\t\t\t\t\t\tif (that.options.onFullscreen && typeof(that.options.onFullscreen) === \"function\") {\r\n\t\t\t\t\t        that.options.onFullscreen(that);\r\n\t\t\t\t\t    }\r\n\t\t\t\t\t    that.$element.trigger('fullscreen', that);\r\n\t\t            });\r\n\r\n\t\t            // Next modal\r\n\t\t            that.$navigate.off('click', '.'+PLUGIN_NAME+'-navigate-next').on('click', '.'+PLUGIN_NAME+'-navigate-next', function (e) {\r\n\t\t            \tthat.next(e);\r\n\t\t            });\r\n\t\t            that.$element.off('click', '[data-'+PLUGIN_NAME+'-next]').on('click', '[data-'+PLUGIN_NAME+'-next]', function (e) {\r\n\t\t            \tthat.next(e);\r\n\t\t            });\r\n\r\n\t\t            // Previous modal\r\n\t\t            that.$navigate.off('click', '.'+PLUGIN_NAME+'-navigate-prev').on('click', '.'+PLUGIN_NAME+'-navigate-prev', function (e) {\r\n\t\t            \tthat.prev(e);\r\n\t\t            });\r\n\t\t\t\t\tthat.$element.off('click', '[data-'+PLUGIN_NAME+'-prev]').on('click', '[data-'+PLUGIN_NAME+'-prev]', function (e) {\r\n\t\t            \tthat.prev(e);\r\n\t\t            });\r\n\t\t\t\t}\r\n\r\n\t\t\t    if(this.state == STATES.CLOSED){\r\n\r\n\t\t\t    \tbindEvents();\r\n\r\n\t\t\t\t\tthis.setGroup();\r\n\t\t\t\t\tthis.state = STATES.OPENING;\r\n\t\t            this.$element.trigger(STATES.OPENING);\r\n\t\t\t\t\tthis.$element.attr('aria-hidden', 'false');\r\n\r\n\t\t\t\t\t// console.info('[ '+PLUGIN_NAME+' | '+this.id+' ] Opening...');\r\n\r\n\t\t\t\t\tif(this.options.iframe === true){\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-content').addClass(PLUGIN_NAME+'-content-loader');\r\n\r\n\t\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-iframe').on('load', function(){\r\n\t\t\t\t\t\t\t$(this).parent().removeClass(PLUGIN_NAME+'-content-loader');\r\n\t\t\t\t\t\t});\r\n\r\n\t\t\t\t\t\tvar href = null;\r\n\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\thref = $(param.currentTarget).attr('href') !== \"\" ? $(param.currentTarget).attr('href') : null;\r\n\t\t\t\t\t\t} catch(e) {\r\n\t\t\t\t\t\t\t// console.warn(e);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tif( (this.options.iframeURL !== null) && (href === null || href === undefined)){\r\n\t\t\t\t\t\t\thref = this.options.iframeURL;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tif(href === null || href === undefined){\r\n\t\t\t\t\t\t\tthrow new Error(\"Failed to find iframe URL\");\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t    this.$element.find('.'+PLUGIN_NAME+'-iframe').attr('src', href);\r\n\t\t\t\t\t}\r\n\r\n\r\n\t\t\t\t\tif (this.options.bodyOverflow || isMobile){\r\n\t\t\t\t\t\t$('html').addClass(PLUGIN_NAME+'-isOverflow');\r\n\t\t\t\t\t\tif(isMobile){\r\n\t\t\t\t\t\t\t$('body').css('overflow', 'hidden');\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (this.options.onOpening && typeof(this.options.onOpening) === \"function\") {\r\n\t\t\t\t        this.options.onOpening(this);\r\n\t\t\t\t    }\t\t\t    \r\n\t\t\t\t\t(function open(){\r\n\r\n\t\t\t\t    \tif(that.group.ids.length > 1 ){\r\n\r\n\t\t\t\t    \t\tthat.$navigate.appendTo('body');\r\n\t\t\t\t    \t\tthat.$navigate.addClass('fadeIn');\r\n\r\n\t\t\t\t    \t\tif(that.options.navigateCaption === true){\r\n\t\t\t\t    \t\t\tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-caption').show();\r\n\t\t\t\t    \t\t}\r\n\r\n\t\t\t\t    \t\tvar modalWidth = that.$element.outerWidth();\r\n\t\t\t\t    \t\tif(that.options.navigateArrows !== false){\r\n\t\t\t\t\t\t    \tif (that.options.navigateArrows === 'closeScreenEdge'){\r\n\t\t\t\t\t    \t\t\tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-prev').css('left', 0).show();\r\n\t\t\t\t\t    \t\t\tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-next').css('right', 0).show();\r\n\t\t\t\t\t\t    \t} else {\r\n\t\t\t\t\t\t\t    \tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-prev').css('margin-left', -((modalWidth/2)+84)).show();\r\n\t\t\t\t\t\t\t    \tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-next').css('margin-right', -((modalWidth/2)+84)).show();\t\t\t\t\t    \t\t\r\n\t\t\t\t\t\t    \t}\r\n\t\t\t\t    \t\t} else {\r\n\t\t\t\t    \t\t\tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-prev').hide();\r\n\t\t\t\t    \t\t\tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-next').hide();\r\n\t\t\t\t    \t\t}\r\n\t\t\t\t    \t\t\r\n\t\t\t\t    \t\tvar loop;\r\n\t\t\t\t\t\t\tif(that.group.index === 0){\r\n\r\n\t\t\t\t\t\t\t\tloop = $('.'+PLUGIN_NAME+'[data-'+PLUGIN_NAME+'-group=\"'+that.group.name+'\"][data-'+PLUGIN_NAME+'-loop]').length;\r\n\r\n\t\t\t\t\t\t\t\tif(loop === 0 && that.options.loop === false)\r\n\t\t\t\t\t\t\t\t\tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-prev').hide();\r\n\t\t\t\t\t    \t}\r\n\t\t\t\t\t    \tif(that.group.index+1 === that.group.ids.length){\r\n\r\n\t\t\t\t\t    \t\tloop = $('.'+PLUGIN_NAME+'[data-'+PLUGIN_NAME+'-group=\"'+that.group.name+'\"][data-'+PLUGIN_NAME+'-loop]').length;\r\n\r\n\t\t\t\t\t\t\t\tif(loop === 0 && that.options.loop === false)\r\n\t\t\t\t\t\t\t\t\tthat.$navigate.find('.'+PLUGIN_NAME+'-navigate-next').hide();\r\n\t\t\t\t\t    \t}\r\n\t\t\t\t    \t}\r\n\r\n\t\t\t\t\t\tif(that.options.overlay === true) {\r\n\r\n\t\t\t\t\t\t\tif(that.options.appendToOverlay === false){\r\n\t\t\t\t\t\t\t\tthat.$overlay.appendTo('body');\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\tthat.$overlay.appendTo( that.options.appendToOverlay );\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif (that.options.transitionInOverlay) {\r\n\t\t\t\t\t\t\tthat.$overlay.addClass(that.options.transitionInOverlay);\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tvar transitionIn = that.options.transitionIn;\r\n\r\n\t\t\t\t\t\tif( typeof param == 'object' ){\r\n\t\t\t\t\t\t\tif(param.transition !== undefined || param.transitionIn !== undefined){\r\n\t\t\t\t\t\t\t\ttransitionIn = param.transition || param.transitionIn;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif (transitionIn !== '' && animationEvent !== undefined) {\r\n\r\n\t\t\t\t\t\t\tthat.$element.addClass(\"transitionIn \"+transitionIn).show();\r\n\t\t\t\t\t\t\tthat.$wrap.one(animationEvent, function () {\r\n\r\n\t\t\t\t\t\t\t    that.$element.removeClass(transitionIn + \" transitionIn\");\r\n\t\t\t\t\t\t\t    that.$overlay.removeClass(that.options.transitionInOverlay);\r\n\t\t\t\t\t\t\t    that.$navigate.removeClass('fadeIn');\r\n\r\n\t\t\t\t\t\t\t\topened();\r\n\t\t\t\t\t\t\t});\r\n\r\n\t\t\t\t\t\t} else {\r\n\r\n\t\t\t\t\t\t\tthat.$element.show();\r\n\t\t\t\t\t\t\topened();\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif(that.options.pauseOnHover === true && that.options.pauseOnHover === true && that.options.timeout !== false && !isNaN(parseInt(that.options.timeout)) && that.options.timeout !== false && that.options.timeout !== 0){\r\n\r\n\t\t\t\t\t\t\tthat.$element.off('mouseenter').on('mouseenter', function(event) {\r\n\t\t\t\t\t\t\t\tevent.preventDefault();\r\n\t\t\t\t\t\t\t\tthat.isPaused = true;\r\n\t\t\t\t\t\t\t});\r\n\t\t\t\t\t\t\tthat.$element.off('mouseleave').on('mouseleave', function(event) {\r\n\t\t\t\t\t\t\t\tevent.preventDefault();\r\n\t\t\t\t\t\t\t\tthat.isPaused = false;\r\n\t\t\t\t\t\t\t});\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t})();\r\n\r\n\t\t\t\t\tif (this.options.timeout !== false && !isNaN(parseInt(this.options.timeout)) && this.options.timeout !== false && this.options.timeout !== 0) {\r\n\r\n\t\t\t\t\t\tif (this.options.timeoutProgressbar === true) {\r\n\r\n\t\t\t\t\t\t\tthis.progressBar = {\r\n\t\t\t                    hideEta: null,\r\n\t\t\t                    maxHideTime: null,\r\n\t\t\t                    currentTime: new Date().getTime(),\r\n\t\t\t                    el: this.$element.find('.'+PLUGIN_NAME+'-progressbar > div'),\r\n\t\t\t                    updateProgress: function()\r\n\t\t\t                    {\r\n\t\t\t\t\t\t\t\t\tif(!that.isPaused){\r\n\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\tthat.progressBar.currentTime = that.progressBar.currentTime+10;\r\n\r\n\t\t\t\t\t                    var percentage = ((that.progressBar.hideEta - (that.progressBar.currentTime)) / that.progressBar.maxHideTime) * 100;\r\n\t\t\t\t\t                    that.progressBar.el.width(percentage + '%');\r\n\t\t\t\t\t                    if(percentage < 0){\r\n\t\t\t\t\t                    \tthat.close();\r\n\t\t\t\t\t                    }\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t                    }\r\n\t\t\t                };\r\n\t\t\t\t\t\t\tif (this.options.timeout > 0) {\r\n\r\n\t\t                        this.progressBar.maxHideTime = parseFloat(this.options.timeout);\r\n\t\t                        this.progressBar.hideEta = new Date().getTime() + this.progressBar.maxHideTime;\r\n\t\t                        this.timerTimeout = setInterval(this.progressBar.updateProgress, 10);\r\n\t\t                    }\r\n\r\n\t\t\t\t\t\t} else {\r\n\r\n\t\t\t\t\t\t\tthis.timerTimeout = setTimeout(function(){\r\n\t\t\t\t\t\t\t\tthat.close();\r\n\t\t\t\t\t\t\t}, that.options.timeout);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t            // Close on overlay click\r\n\t\t            if (this.options.overlayClose && !this.$element.hasClass(this.options.transitionOut)) {\r\n\t\t            \tthis.$overlay.click(function () {\r\n\t\t                    that.close();\r\n\t\t            \t});\r\n\t\t            }\r\n\r\n\t\t\t\t\tif (this.options.focusInput){\r\n\t\t\t\t    \tthis.$element.find(':input:not(button):enabled:visible:first').focus(); // Focus on the first field\r\n\t\t\t\t\t}\r\n\t\t\t\t\t\r\n\t\t\t\t\t(function updateTimer(){\r\n\t\t\t\t    \tthat.recalcLayout();\r\n\t\t\t\t\t    that.timer = setTimeout(updateTimer, 300);\r\n\t\t\t\t\t})();\r\n\r\n\t\t            // Close when the Escape key is pressed\r\n\t\t            $document.on('keydown.'+PLUGIN_NAME, function (e) {\r\n\t\t                if (that.options.closeOnEscape && e.keyCode === 27) {\r\n\t\t                    that.close();\r\n\t\t                }\r\n\t\t            });\r\n\r\n\t\t\t    }\r\n\r\n\t\t\t},\r\n\r\n\t\t\tclose: function (param) {\r\n\r\n\t\t\t\tvar that = this;\r\n\r\n\t\t\t\tfunction closed(){\r\n\t                \r\n\t                // console.info('[ '+PLUGIN_NAME+' | '+that.id+' ] Closed.');\r\n\r\n\t                that.state = STATES.CLOSED;\r\n\t                that.$element.trigger(STATES.CLOSED);\r\n\r\n\t                if (that.options.iframe === true) {\r\n\t                    that.$element.find('.'+PLUGIN_NAME+'-iframe').attr('src', \"\");\r\n\t                }\r\n\r\n\t\t\t\t\tif (that.options.bodyOverflow || isMobile){\r\n\t\t\t\t\t\t$('html').removeClass(PLUGIN_NAME+'-isOverflow');\r\n\t\t\t\t\t\tif(isMobile){\r\n\t\t\t\t\t\t\t$('body').css('overflow','auto');\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}                \r\n\t\t\t\t\t\r\n\t\t\t\t\tif (that.options.onClosed && typeof(that.options.onClosed) === \"function\") {\r\n\t\t\t\t        that.options.onClosed(that);\r\n\t\t\t\t    }\r\n\r\n\t\t\t\t\tif(that.options.restoreDefaultContent === true){\r\n\t\t\t\t\t    that.$element.find('.'+PLUGIN_NAME+'-content').html( that.content );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif( $('.'+PLUGIN_NAME+':visible').length === 0 ){\r\n\t\t\t\t\t\t$('html').removeClass(PLUGIN_NAME+'-isAttached');\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t            if(this.state == STATES.OPENED || this.state == STATES.OPENING){\r\n\r\n\t            \t$document.off('keydown.'+PLUGIN_NAME);\r\n\r\n\t\t\t\t\tthis.state = STATES.CLOSING;\r\n\t\t\t\t\tthis.$element.trigger(STATES.CLOSING);\r\n\t\t\t\t\tthis.$element.attr('aria-hidden', 'true');\r\n\r\n\t\t\t\t\t// console.info('[ '+PLUGIN_NAME+' | '+this.id+' ] Closing...');\r\n\r\n\t\t            clearTimeout(this.timer);\r\n\t\t            clearTimeout(this.timerTimeout);\r\n\r\n\t\t\t\t\tif (that.options.onClosing && typeof(that.options.onClosing) === \"function\") {\r\n\t\t\t\t        that.options.onClosing(this);\r\n\t\t\t\t    }\r\n\r\n\t\t\t\t\tvar transitionOut = this.options.transitionOut;\r\n\r\n\t\t\t\t\tif( typeof param == 'object' ){\r\n\t\t\t\t\t\tif(param.transition !== undefined || param.transitionOut !== undefined){\r\n\t\t\t\t\t\t\ttransitionOut = param.transition || param.transitionOut;\r\n\t\t\t\t\t\t} \r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif( (transitionOut === false || transitionOut === '' ) || animationEvent === undefined){\r\n\r\n\t\t                this.$element.hide();\r\n\t\t                this.$overlay.remove();\r\n\t                \tthis.$navigate.remove();\r\n\t\t                closed();\r\n\r\n\t\t\t\t\t} else {\r\n\r\n\t\t                this.$element.attr('class', [\r\n\t\t\t\t\t\t\tthis.classes,\r\n\t\t\t\t\t\t\tPLUGIN_NAME,\r\n\t\t\t\t\t\t\ttransitionOut,\r\n\t\t\t\t\t\t\tthis.options.theme == 'light' ? PLUGIN_NAME+'-light' : this.options.theme,\r\n\t\t\t\t\t\t\tthis.isFullscreen === true ? 'isFullscreen' : '',\r\n\t\t\t\t\t\t\tthis.options.rtl ? PLUGIN_NAME+'-rtl' : ''\r\n\t\t\t\t\t\t].join(' '));\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tthis.$overlay.attr('class', PLUGIN_NAME + \"-overlay \" + this.options.transitionOutOverlay);\r\n\r\n\t\t\t\t\t\tif(that.options.navigateArrows !== false){\r\n\t\t\t\t\t\t\tthis.$navigate.attr('class', PLUGIN_NAME + \"-navigate fadeOut\");\r\n\t\t\t\t\t\t}\r\n\r\n\t\t                this.$element.one(animationEvent, function () {\r\n\t\t                    \r\n\t\t                    if( that.$element.hasClass(transitionOut) ){\r\n\t\t                        that.$element.removeClass(transitionOut + \" transitionOut\").hide();\r\n\t\t                    }\r\n\t                        that.$overlay.removeClass(that.options.transitionOutOverlay).remove();\r\n\t\t\t\t\t\t\tthat.$navigate.removeClass('fadeOut').remove();\r\n\t\t\t\t\t\t\tclosed();\r\n\t\t                });\r\n\r\n\t\t\t\t\t}\r\n\r\n\t            }\r\n\t\t\t},\r\n\r\n\t\t\tnext: function (e){\r\n\r\n\t            var that = this;\r\n\t            var transitionIn = 'fadeInRight';\r\n\t            var transitionOut = 'fadeOutLeft';\r\n\t\t\t\tvar modal = $('.'+PLUGIN_NAME+':visible');\r\n\t            var modals = {};\r\n\t\t\t\t\tmodals.out = this;\r\n\r\n\t\t\t\tif(e !== undefined && typeof e !== 'object'){\r\n\t            \te.preventDefault();\r\n\t            \tmodal = $(e.currentTarget);\r\n\t            \ttransitionIn = modal.attr('data-'+PLUGIN_NAME+'-transitionIn');\r\n\t            \ttransitionOut = modal.attr('data-'+PLUGIN_NAME+'-transitionOut');\r\n\t\t\t\t} else if(e !== undefined){\r\n\t\t\t\t\tif(e.transitionIn !== undefined){\r\n\t\t\t\t\t\ttransitionIn = e.transitionIn;\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif(e.transitionOut !== undefined){\r\n\t\t\t\t\t\ttransitionOut = e.transitionOut;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t        \tthis.close({transition:transitionOut});\r\n\t            \r\n\t\t\t\tsetTimeout(function(){\r\n\r\n\t\t\t\t\tvar loop = $('.'+PLUGIN_NAME+'[data-'+PLUGIN_NAME+'-group=\"'+that.group.name+'\"][data-'+PLUGIN_NAME+'-loop]').length;\r\n\t\t\t\t\tfor (var i = that.group.index+1; i <= that.group.ids.length; i++) {\r\n\r\n\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\tmodals.in = $(\"#\"+that.group.ids[i]).data().iziModal;\r\n\t\t\t\t\t\t} catch(log) {\r\n\t\t\t\t\t\t\t// console.info('[ '+PLUGIN_NAME+' ] No next modal.');\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tif(typeof modals.in !== 'undefined'){\r\n\r\n\t\t\t\t\t\t\t$(\"#\"+that.group.ids[i]).iziModal('open', { transition: transitionIn });\r\n\t\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\t\t} else {\r\n\r\n\t\t\t\t\t\t\tif(i == that.group.ids.length && loop > 0 || that.options.loop === true){\r\n\r\n\t\t\t\t\t\t\t\tfor (var index = 0; index <= that.group.ids.length; index++) {\r\n\r\n\t\t\t\t\t\t\t\t\tmodals.in = $(\"#\"+that.group.ids[index]).data().iziModal;\r\n\t\t\t\t\t\t\t\t\tif(typeof modals.in !== 'undefined'){\r\n\t\t\t\t\t\t\t\t\t\t$(\"#\"+that.group.ids[index]).iziModal('open', { transition: transitionIn });\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t}, 200);\r\n\r\n\t\t\t\t$(document).trigger( PLUGIN_NAME + \"-group-change\", modals );\r\n\t\t\t},\r\n\r\n\t\t\tprev: function (e){\r\n\t            var that = this;\r\n\t            var transitionIn = 'fadeInLeft';\r\n\t            var transitionOut = 'fadeOutRight';\r\n\t\t\t\tvar modal = $('.'+PLUGIN_NAME+':visible');\r\n\t            var modals = {};\r\n\t\t\t\t\tmodals.out = this;\r\n\r\n\t\t\t\tif(e !== undefined && typeof e !== 'object'){\r\n\t            \te.preventDefault();\r\n\t            \tmodal = $(e.currentTarget);\r\n\t            \ttransitionIn = modal.attr('data-'+PLUGIN_NAME+'-transitionIn');\r\n\t            \ttransitionOut = modal.attr('data-'+PLUGIN_NAME+'-transitionOut');\r\n\t            \t\r\n\t\t\t\t} else if(e !== undefined){\r\n\r\n\t\t\t\t\tif(e.transitionIn !== undefined){\r\n\t\t\t\t\t\ttransitionIn = e.transitionIn;\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif(e.transitionOut !== undefined){\r\n\t\t\t\t\t\ttransitionOut = e.transitionOut;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tthis.close({transition:transitionOut});\r\n\r\n\t\t\t\tsetTimeout(function(){\r\n\r\n\t\t\t\t\tvar loop = $('.'+PLUGIN_NAME+'[data-'+PLUGIN_NAME+'-group=\"'+that.group.name+'\"][data-'+PLUGIN_NAME+'-loop]').length;\r\n\r\n\t\t\t\t\tfor (var i = that.group.index; i >= 0; i--) {\r\n\r\n\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\tmodals.in = $(\"#\"+that.group.ids[i-1]).data().iziModal;\r\n\t\t\t\t\t\t} catch(log) {\r\n\t\t\t\t\t\t\t// console.info('[ '+PLUGIN_NAME+' ] No previous modal.');\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tif(typeof modals.in !== 'undefined'){\r\n\r\n\t\t\t\t\t\t\t$(\"#\"+that.group.ids[i-1]).iziModal('open', { transition: transitionIn });\r\n\t\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\t\t} else {\r\n\r\n\t\t\t\t\t\t\tif(i === 0 && loop > 0 || that.options.loop === true){\r\n\r\n\t\t\t\t\t\t\t\tfor (var index = that.group.ids.length-1; index >= 0; index--) {\r\n\r\n\t\t\t\t\t\t\t\t\tmodals.in = $(\"#\"+that.group.ids[index]).data().iziModal;\r\n\t\t\t\t\t\t\t\t\tif(typeof modals.in !== 'undefined'){\r\n\t\t\t\t\t\t\t\t\t\t$(\"#\"+that.group.ids[index]).iziModal('open', { transition: transitionIn });\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t}, 200);\r\n\r\n\t\t\t\t$(document).trigger( PLUGIN_NAME + \"-group-change\", modals );\r\n\t\t\t},\r\n\r\n\t\t\tdestroy: function () {\r\n\t\t\t\tvar e = $.Event('destroy');\r\n\r\n\t\t\t\tthis.$element.trigger(e);\r\n\r\n\t            $document.off('keydown.'+PLUGIN_NAME);\r\n\r\n\t\t\t\tclearTimeout(this.timer);\r\n\t\t\t\tclearTimeout(this.timerTimeout);\r\n\r\n\t\t\t\tif (this.options.iframe === true) {\r\n\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-iframe').remove();\r\n\t\t\t\t}\r\n\t\t\t\tthis.$element.html(this.$element.find('.'+PLUGIN_NAME+'-content').html());\r\n\r\n\t\t\t\tthis.$element.off('click', '[data-'+PLUGIN_NAME+'-close]');\r\n\t\t\t\tthis.$element.off('click', '[data-'+PLUGIN_NAME+'-fullscreen]');\r\n\r\n\t\t\t\tthis.$element\r\n\t\t\t\t\t.off('.'+PLUGIN_NAME)\r\n\t\t\t\t\t.removeData(PLUGIN_NAME)\r\n\t\t\t\t\t.attr('style', '');\r\n\t\t\t\t\r\n\t\t\t\tthis.$overlay.remove();\r\n\t\t\t\tthis.$navigate.remove();\r\n\t\t\t\tthis.$element.trigger(STATES.DESTROYED);\r\n\t\t\t\tthis.$element = null;\r\n\t\t\t},\r\n\r\n\t\t\tgetState: function(){\r\n\r\n\t\t\t\treturn this.state;\r\n\t\t\t},\r\n\r\n\t\t\tgetGroup: function(){\r\n\r\n\t\t\t\treturn this.group;\r\n\t\t\t},\r\n\r\n\t\t\tsetWidth: function(width){\r\n\r\n\t\t\t\tthis.options.width = width;\r\n\r\n\t\t\t\tthis.recalcWidth();\r\n\r\n\t\t\t\tvar modalWidth = this.$element.outerWidth();\r\n\t    \t\tif(this.options.navigateArrows === true || this.options.navigateArrows == 'closeToModal'){\r\n\t\t\t    \tthis.$navigate.find('.'+PLUGIN_NAME+'-navigate-prev').css('margin-left', -((modalWidth/2)+84)).show();\r\n\t\t\t    \tthis.$navigate.find('.'+PLUGIN_NAME+'-navigate-next').css('margin-right', -((modalWidth/2)+84)).show();\t\t\t\t\t    \t\t\r\n\t    \t\t}\r\n\r\n\t\t\t},\r\n\r\n\t\t\tsetTop: function(top){\r\n\r\n\t\t\t\tthis.options.top = top;\r\n\r\n\t\t\t\tthis.recalcVerticalPos(false);\r\n\t\t\t},\r\n\r\n\t\t\tsetBottom: function(bottom){\r\n\r\n\t\t\t\tthis.options.bottom = bottom;\r\n\r\n\t\t\t\tthis.recalcVerticalPos(false);\r\n\r\n\t\t\t},\r\n\r\n\t\t\tsetHeader: function(status){\r\n\r\n\t\t\t\tif(status){\r\n\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-header').show();\r\n\t\t\t\t} else {\r\n\t\t\t\t\tthis.headerHeight = 0;\r\n\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-header').hide();\r\n\t\t\t\t}\r\n\t\t\t},\r\n\r\n\t\t\tsetTitle: function(title){\r\n\r\n\t\t\t\tthis.options.title = title;\r\n\r\n\t\t\t\tif(this.headerHeight === 0){\r\n\t\t\t\t\tthis.createHeader();\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif( this.$header.find('.'+PLUGIN_NAME+'-header-title').length === 0 ){\r\n\t\t\t\t\tthis.$header.append('<h2 class=\"'+PLUGIN_NAME+'-header-title\"></h2>');\r\n\t\t\t\t}\r\n\r\n\t\t\t\tthis.$header.find('.'+PLUGIN_NAME+'-header-title').html(title);\r\n\t\t\t},\r\n\r\n\t\t\tsetSubtitle: function(subtitle){\r\n\r\n\t\t\t\tif(subtitle === ''){\r\n\t\t\t\t\t\r\n\t\t\t\t\tthis.$header.find('.'+PLUGIN_NAME+'-header-subtitle').remove();\r\n\t\t\t\t\tthis.$header.addClass(PLUGIN_NAME+'-noSubtitle');\r\n\r\n\t\t\t\t} else {\r\n\r\n\t\t\t\t\tif( this.$header.find('.'+PLUGIN_NAME+'-header-subtitle').length === 0 ){\r\n\t\t\t\t\t\tthis.$header.append('<p class=\"'+PLUGIN_NAME+'-header-subtitle\"></p>');\r\n\t\t\t\t\t}\r\n\t\t\t\t\tthis.$header.removeClass(PLUGIN_NAME+'-noSubtitle');\r\n\r\n\t\t\t\t}\r\n\r\n\t\t\t\tthis.$header.find('.'+PLUGIN_NAME+'-header-subtitle').html(subtitle);\r\n\t\t\t\tthis.options.subtitle = subtitle;\r\n\t\t\t},\r\n\r\n\t\t\tsetIcon: function(icon){\r\n\r\n\t\t\t\tif( this.$header.find('.'+PLUGIN_NAME+'-header-icon').length === 0 ){\r\n\t\t\t\t\tthis.$header.prepend('<i class=\"'+PLUGIN_NAME+'-header-icon\"></i>');\r\n\t\t\t\t}\r\n\t\t\t\tthis.$header.find('.'+PLUGIN_NAME+'-header-icon').attr('class', PLUGIN_NAME+'-header-icon ' + icon);\r\n\t\t\t\tthis.options.icon = icon;\r\n\t\t\t},\r\n\r\n\t\t\tsetIconText: function(iconText){\r\n\r\n\t\t\t\tthis.$header.find('.'+PLUGIN_NAME+'-header-icon').html(iconText);\r\n\t\t\t\tthis.options.iconText = iconText;\r\n\t\t\t},\r\n\r\n\t\t\tsetHeaderColor: function(headerColor){\r\n\t\t\t\tif(this.options.borderBottom === true){\r\n\t            \tthis.$element.css('border-bottom', '3px solid ' + headerColor + '');\r\n\t        \t}\r\n\t            this.$header.css('background', headerColor);\r\n\t            this.options.headerColor = headerColor;\r\n\t\t\t},\r\n\r\n\t\t\tsetBackground: function(background){\r\n\t\t\t\tif(background === false){\r\n\t\t\t\t\tthis.options.background = null;\r\n\t\t\t\t\tthis.$element.css('background', '');\r\n\t\t\t\t} else{\r\n\t            \tthis.$element.css('background', background);\r\n\t            \tthis.options.background = background;\t\t\t\t\t\r\n\t\t\t\t}\r\n\t\t\t},\r\n\r\n\t\t\tsetZindex: function(zIndex){\r\n\r\n\t\t        if (!isNaN(parseInt(this.options.zindex))) {\r\n\t\t        \tthis.options.zindex = zIndex;\r\n\t\t\t\t \tthis.$element.css('z-index', zIndex);\r\n\t\t\t\t \tthis.$navigate.css('z-index', zIndex-1);\r\n\t\t\t\t \tthis.$overlay.css('z-index', zIndex-2);\r\n\t\t        }\r\n\t\t\t},\r\n\r\n\t\t\tsetFullscreen: function(value){\r\n\r\n\t\t\t\tif(value){\r\n\t\t\t\t    this.isFullscreen = true;\r\n\t\t\t\t    this.$element.addClass('isFullscreen');\r\n\t\t\t\t} else {\r\n\t\t\t\t\tthis.isFullscreen = false;\r\n\t\t\t\t    this.$element.removeClass('isFullscreen');\r\n\t\t\t\t}\r\n\r\n\t\t\t},\r\n\r\n\t\t\tsetContent: function(content){\r\n\r\n\t\t\t\tif( typeof content == \"object\" ){\r\n\t\t\t\t\tvar replace = content.default || false;\r\n\t\t\t\t\tif(replace === true){\r\n\t\t\t\t\t\tthis.content = content.content;\r\n\t\t\t\t\t}\r\n\t\t\t\t\tcontent = content.content;\r\n\t\t\t\t}\r\n\t            if (this.options.iframe === false) {\r\n            \t\tthis.$element.find('.'+PLUGIN_NAME+'-content').html(content);\r\n\t            }\r\n\r\n\t\t\t},\r\n\r\n\t\t\tsetTransitionIn: function(transition){\r\n\t\t\t\t\r\n\t\t\t\tthis.options.transitionIn = transition;\r\n\t\t\t},\r\n\r\n\t\t\tsetTransitionOut: function(transition){\r\n\r\n\t\t\t\tthis.options.transitionOut = transition;\r\n\t\t\t},\r\n\r\n\t\t\tresetContent: function(){\r\n\r\n\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-content').html(this.content);\r\n\r\n\t\t\t},\r\n\r\n\t\t\tstartLoading: function(){\r\n\r\n\t\t\t\tif( !this.$element.find('.'+PLUGIN_NAME+'-loader').length ){\r\n\t\t\t\t\tthis.$element.append('<div class=\"'+PLUGIN_NAME+'-loader fadeIn\"></div>');\r\n\t\t\t\t}\r\n\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-loader').css({\r\n\t\t\t\t\ttop: this.headerHeight,\r\n        \t\t\tborderRadius: this.options.radius\r\n\t\t\t\t});\r\n\t\t\t},\r\n\r\n\t\t\tstopLoading: function(){\r\n\t\t\t\t\r\n\t\t\t\tvar $loader = this.$element.find('.'+PLUGIN_NAME+'-loader');\r\n\r\n\t\t\t\tif( !$loader.length ){\r\n\t\t\t\t\tthis.$element.prepend('<div class=\"'+PLUGIN_NAME+'-loader fadeIn\"></div>');\r\n\t\t\t\t\t$loader = this.$element.find('.'+PLUGIN_NAME+'-loader').css('border-radius', this.options.radius);\r\n\t\t\t\t}\r\n\t\t\t\t$loader.removeClass('fadeIn').addClass('fadeOut');\r\n\t\t\t\tsetTimeout(function(){\r\n\t\t\t\t\t$loader.remove();\r\n\t\t\t\t},600);\r\n\t\t\t},\r\n\r\n\t\t\trecalcWidth: function(){\r\n\r\n\t\t\t\tvar that = this;\r\n\r\n\t            this.$element.css('max-width', this.options.width);\r\n\r\n\t            if(isIE()){\r\n\t            \tvar modalWidth = that.options.width;\r\n\r\n\t            \tif(modalWidth.toString().split(\"%\").length > 1){\r\n\t\t\t\t\t\tmodalWidth = that.$element.outerWidth();\r\n\t            \t}\r\n\t            \tthat.$element.css({\r\n\t            \t\tleft: '50%',\r\n\t            \t\tmarginLeft: -(modalWidth/2)\r\n\t            \t});\r\n\t            }\r\n\t\t\t},\r\n\r\n\t\t\trecalcVerticalPos: function(first){\r\n\r\n\t\t\t\tif(this.options.top !== null && this.options.top !== false){\r\n\t            \tthis.$element.css('margin-top', this.options.top);\r\n\t            \tif(this.options.top === 0){\r\n\t            \t\tthis.$element.css({\r\n\t            \t\t\tborderTopRightRadius: 0,\r\n\t            \t\t\tborderTopLeftRadius: 0\r\n\t            \t\t});\r\n\t            \t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\tif(first === false){\r\n\t\t\t\t\t\tthis.$element.css({\r\n\t\t\t\t\t\t\tmarginTop: '',\r\n\t            \t\t\tborderRadius: this.options.radius\r\n\t            \t\t});\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tif (this.options.bottom !== null && this.options.bottom !== false){\r\n\t            \tthis.$element.css('margin-bottom', this.options.bottom);\r\n\t            \tif(this.options.bottom === 0){\r\n\t            \t\tthis.$element.css({\r\n\t            \t\t\tborderBottomRightRadius: 0,\r\n\t            \t\t\tborderBottomLeftRadius: 0\r\n\t            \t\t});\r\n\t            \t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\tif(first === false){\r\n\t\t\t\t\t\tthis.$element.css({\r\n\t\t\t\t\t\t\tmarginBottom: '',\r\n\t            \t\t\tborderRadius: this.options.radius\r\n\t            \t\t});\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t},\r\n\r\n\t\t\trecalcLayout: function(){\r\n\r\n\t\t\t\tvar that = this,\r\n\t        \t\twindowHeight = $window.height(),\r\n\t                modalHeight = this.$element.outerHeight(),\r\n\t                modalWidth = this.$element.outerWidth(),\r\n\t                contentHeight = this.$element.find('.'+PLUGIN_NAME+'-content')[0].scrollHeight,\r\n                \touterHeight = contentHeight + this.headerHeight,\r\n                \twrapperHeight = this.$element.innerHeight() - this.headerHeight,\r\n\t                modalMargin = parseInt(-((this.$element.innerHeight() + 1) / 2)) + 'px',\r\n                \tscrollTop = this.$wrap.scrollTop(),\r\n                \tborderSize = 0;\r\n\r\n\t\t\t\tif(isIE()){\r\n\t\t\t\t\tif( modalWidth >= $window.width() || this.isFullscreen === true ){\r\n\t\t\t\t\t\tthis.$element.css({\r\n\t\t\t\t\t\t\tleft: '0',\r\n\t\t\t\t\t\t\tmarginLeft: ''\r\n\t\t\t\t\t\t});\r\n\t\t\t\t\t} else {\r\n\t\t            \tthis.$element.css({\r\n\t\t            \t\tleft: '50%',\r\n\t\t            \t\tmarginLeft: -(modalWidth/2)\r\n\t\t            \t});\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif(this.options.borderBottom === true && this.options.title !== \"\"){\r\n\t\t\t\t\tborderSize = 3;\r\n\t\t\t\t}\r\n\r\n\t            if(this.$element.find('.'+PLUGIN_NAME+'-header').length && this.$element.find('.'+PLUGIN_NAME+'-header').is(':visible') ){\r\n\t            \tthis.headerHeight = parseInt(this.$element.find('.'+PLUGIN_NAME+'-header').innerHeight());\r\n\t            \tthis.$element.css('overflow', 'hidden');\r\n\t            } else {\r\n\t            \tthis.headerHeight = 0;\r\n\t            \tthis.$element.css('overflow', '');\r\n\t            }\r\n\r\n\t\t\t\tif(this.$element.find('.'+PLUGIN_NAME+'-loader').length){\r\n\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-loader').css('top', this.headerHeight);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif(modalHeight !== this.modalHeight){\r\n\t\t\t\t\tthis.modalHeight = modalHeight;\r\n\r\n\t\t\t\t\tif (this.options.onResize && typeof(this.options.onResize) === \"function\") {\r\n\t\t\t\t        this.options.onResize(this);\r\n\t\t\t\t    }\r\n\t\t\t\t}\r\n\r\n\t            if(this.state == STATES.OPENED || this.state == STATES.OPENING){\r\n\r\n\t\t\t\t\tif (this.options.iframe === true) {\r\n\r\n\t\t\t\t\t\t// If the height of the window is smaller than the modal with iframe\r\n\t\t\t\t\t\tif(windowHeight < (this.options.iframeHeight + this.headerHeight+borderSize) || this.isFullscreen === true){\r\n\t\t\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-iframe').css( 'height', windowHeight - (this.headerHeight+borderSize));\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tthis.$element.find('.'+PLUGIN_NAME+'-iframe').css( 'height', this.options.iframeHeight);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif(modalHeight == windowHeight){\r\n\t\t\t\t\t\tthis.$element.addClass('isAttached');\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tthis.$element.removeClass('isAttached');\r\n\t\t\t\t\t}\r\n\r\n\t        \t\tif(this.isFullscreen === false && this.$element.width() >= $window.width() ){\r\n\t        \t\t\tthis.$element.find('.'+PLUGIN_NAME+'-button-fullscreen').hide();\r\n\t        \t\t} else {\r\n\t        \t\t\tthis.$element.find('.'+PLUGIN_NAME+'-button-fullscreen').show();\r\n\t        \t\t}\r\n\t\t\t\t\tthis.recalcButtons();\r\n\r\n\t\t\t\t\tif(this.isFullscreen === false){\r\n \t                \twindowHeight = windowHeight - (clearValue(this.options.top) || 0) - (clearValue(this.options.bottom) || 0);\r\n\t\t\t\t\t}\r\n\t                // If the modal is larger than the height of the window..\r\n\t                if (outerHeight > windowHeight) {\r\n\t\t\t\t\t\tif(this.options.top > 0 && this.options.bottom === null && contentHeight < $window.height()){\r\n\t\t\t\t\t    \tthis.$element.addClass('isAttachedBottom');\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tif(this.options.bottom > 0 && this.options.top === null && contentHeight < $window.height()){\r\n\t\t\t\t\t    \tthis.$element.addClass('isAttachedTop');\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t$('html').addClass(PLUGIN_NAME+'-isAttached');\r\n\t\t\t\t\t\tthis.$element.css( 'height', windowHeight );\r\n\r\n\t                } else {\r\n\t                \tthis.$element.css('height', contentHeight + (this.headerHeight+borderSize));\r\n\t\t\t    \t\tthis.$element.removeClass('isAttachedTop isAttachedBottom');\r\n\t\t\t    \t\t$('html').removeClass(PLUGIN_NAME+'-isAttached');\r\n\t                }\r\n\r\n\t                (function applyScroll(){\r\n\t                \tif(contentHeight > wrapperHeight && outerHeight > windowHeight){\r\n\t                \t\tthat.$element.addClass('hasScroll');\r\n\t                \t\tthat.$wrap.css('height', modalHeight - (that.headerHeight+borderSize));\r\n\t                \t} else {\r\n\t                \t\tthat.$element.removeClass('hasScroll');\r\n\t                \t\tthat.$wrap.css('height', 'auto');\r\n\t                \t}\r\n                \t})();\r\n\r\n\t\t            (function applyShadow(){\r\n\t\t                if (wrapperHeight + scrollTop < (contentHeight - 30)) {\r\n\t\t                    that.$element.addClass('hasShadow');\r\n\t\t                } else {\r\n\t\t                    that.$element.removeClass('hasShadow');\r\n\t\t                }\r\n\t\t\t\t\t})();\r\n\r\n\t        \t}\r\n\t\t\t},\r\n\r\n\t\t\trecalcButtons: function(){\r\n\t\t\t\tvar widthButtons = this.$header.find('.'+PLUGIN_NAME+'-header-buttons').innerWidth()+10;\r\n\t\t\t\tif(this.options.rtl === true){\r\n\t\t\t\t\tthis.$header.css('padding-left', widthButtons);\r\n\t\t\t\t} else {\r\n\t\t\t\t\tthis.$header.css('padding-right', widthButtons);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t};\r\n\r\n\r\n\t\t$window.off('load.'+PLUGIN_NAME).on('load.'+PLUGIN_NAME, function(e) {\r\n\r\n\t\t\tvar modalHash = document.location.hash;\r\n\r\n\t\t\tif(window.$iziModal.autoOpen === 0 && !$('.'+PLUGIN_NAME).is(\":visible\")){\r\n\r\n\t\t\t\ttry {\r\n\t\t\t\t\tvar data = $(modalHash).data();\r\n\t\t\t\t\tif(typeof data !== 'undefined'){\r\n\t\t\t\t\t\tif(data.iziModal.options.autoOpen !== false){\r\n\t\t\t\t\t\t\t$(modalHash).iziModal(\"open\");\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t} catch(exc) { /* console.warn(exc); */ }\r\n\t\t\t}\r\n\r\n\t\t});\r\n\r\n\t\t$window.off('hashchange.'+PLUGIN_NAME).on('hashchange.'+PLUGIN_NAME, function(e) {\r\n\r\n\t\t\tvar modalHash = document.location.hash;\r\n\t\t\tvar data = $(modalHash).data();\r\n\r\n\t\t\tif(modalHash !== \"\"){\r\n\t\t\t\ttry {\r\n\t\t\t\t\tif(typeof data !== 'undefined' && $(modalHash).iziModal('getState') !== 'opening'){\r\n\r\n\t\t\t\t\t\tsetTimeout(function(){\r\n\t\t\t\t\t\t\t$(modalHash).iziModal(\"open\");\r\n\t\t\t\t\t\t},200);\r\n\t\t\t\t\t}\r\n\t\t\t\t} catch(exc) { /* console.warn(exc); */ }\r\n\r\n\t\t\t} else {\r\n\r\n\t\t\t\tif(window.$iziModal.history){\r\n\t\t\t\t\t$.each( $('.'+PLUGIN_NAME) , function(index, modal) {\r\n\t\t\t\t\t\tif( $(modal).data().iziModal !== undefined ){\r\n\t\t\t\t\t\t\tvar state = $(modal).iziModal('getState');\r\n\t\t\t\t\t\t\tif(state == 'opened' || state == 'opening'){\r\n\t\t\t\t\t\t\t\t$(modal).iziModal('close');\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t});\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\r\n\t\t});\r\n\r\n\t\t$document.off('click', '[data-'+PLUGIN_NAME+'-open]').on('click', '[data-'+PLUGIN_NAME+'-open]', function(e) {\r\n\t\t\te.preventDefault();\r\n\r\n\t\t\tvar modal = $('.'+PLUGIN_NAME+':visible');\r\n\t\t\tvar openModal = $(e.currentTarget).attr('data-'+PLUGIN_NAME+'-open');\r\n\t\t\tvar transitionIn = $(e.currentTarget).attr('data-'+PLUGIN_NAME+'-transitionIn');\r\n\t\t\tvar transitionOut = $(e.currentTarget).attr('data-'+PLUGIN_NAME+'-transitionOut');\r\n\r\n\t\t\tif(transitionOut !== undefined){\r\n\t\t\t\tmodal.iziModal('close', {\r\n\t\t\t\t\ttransition: transitionOut\r\n\t\t\t\t});\r\n\t\t\t} else {\r\n\t\t\t\tmodal.iziModal('close');\r\n\t\t\t}\r\n\r\n\t\t\tsetTimeout(function(){\r\n\t\t\t\tif(transitionIn !== undefined){\r\n\t\t\t\t\t$(openModal).iziModal('open', {\r\n\t\t\t\t\t\ttransition: transitionIn\r\n\t\t\t\t\t});\r\n\t\t\t\t} else {\r\n\t\t\t\t\t$(openModal).iziModal('open');\r\n\t\t\t\t}\r\n\t\t\t}, 200);\r\n\t\t});\r\n\r\n\t\t$document.off('keyup.'+PLUGIN_NAME).on('keyup.'+PLUGIN_NAME, function(event) {\r\n\r\n\t\t\tif( $('.'+PLUGIN_NAME+':visible').length ){\r\n\t\t\t\tvar modal = $('.'+PLUGIN_NAME+':visible')[0].id,\r\n\t\t\t\t\tgroup = $(\"#\"+modal).iziModal('getGroup'),\r\n\t\t\t\t\te = event || window.event,\r\n\t\t\t\t\ttarget = e.target || e.srcElement,\r\n\t\t\t\t\tmodals = {};\r\n\r\n\t\t\t\tif(modal !== undefined && group.name !== undefined && !e.ctrlKey && !e.metaKey && !e.altKey && target.tagName.toUpperCase() !== 'INPUT' && target.tagName.toUpperCase() != 'TEXTAREA'){ //&& $(e.target).is('body')\r\n\r\n\t\t\t\t\tif(e.keyCode === 37) { // left\r\n\r\n\t\t\t\t\t\t$(\"#\"+modal).iziModal('prev', e);\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse if(e.keyCode === 39 ) { // right\r\n\r\n\t\t\t\t\t\t$(\"#\"+modal).iziModal('next', e);\r\n\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t});\r\n\r\n\t\t$.fn[PLUGIN_NAME] = function(option, args) {\r\n\r\n\r\n\t\t\tif( !$(this).length && typeof option == \"object\"){\r\n\r\n\t\t\t\tvar newEL = {\r\n\t\t\t\t\t$el: document.createElement(\"div\"),\r\n\t\t\t\t\tid: this.selector.split('#'),\r\n\t\t\t\t\tclass: this.selector.split('.')\r\n\t\t\t\t};\r\n\t\t\t\t\t\r\n\t\t\t\tif(newEL.id.length > 1){\r\n\t\t\t\t\ttry{\r\n\t\t\t\t\t\tnewEL.$el = document.createElement(id[0]);\r\n\t\t\t\t\t} catch(exc){ }\r\n\r\n\t\t\t\t\tnewEL.$el.id = this.selector.split('#')[1].trim();\r\n\r\n\t\t\t\t} else if(newEL.class.length > 1){\r\n\t\t\t\t\ttry{\r\n\t\t\t\t\t\tnewEL.$el = document.createElement(newEL.class[0]);\r\n\t\t\t\t\t} catch(exc){ }\r\n\r\n\t\t\t\t\tfor (var x=1; x<newEL.class.length; x++) {\r\n\t\t\t\t\t\tnewEL.$el.classList.add(newEL.class[x].trim());\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tdocument.body.appendChild(newEL.$el);\r\n\r\n\t\t\t\tthis.push($(this.selector));\r\n\t\t\t}\r\n\t\t\tvar objs = this;\r\n\r\n\t\t\tfor (var i=0; i<objs.length; i++) {\r\n\t\t\t\t\r\n\t\t\t\tvar $this = $(objs[i]);\r\n\t\t\t\tvar data = $this.data(PLUGIN_NAME);\r\n\t\t\t\tvar options = $.extend({}, $.fn[PLUGIN_NAME].defaults, $this.data(), typeof option == 'object' && option);\r\n\r\n\t\t\t\tif (!data && (!option || typeof option == 'object')){\r\n\r\n\t\t\t\t\t$this.data(PLUGIN_NAME, (data = new iziModal($this, options)));\r\n\t\t\t\t}\r\n\t\t\t\telse if (typeof option == 'string' && typeof data != 'undefined'){\r\n\r\n\t\t\t\t\treturn data[option].apply(data, [].concat(args));\r\n\t\t\t\t}\r\n\t\t\t\tif (options.autoOpen){ // Automatically open the modal if autoOpen setted true or ms\r\n\r\n\t\t\t\t\tif( !isNaN(parseInt(options.autoOpen)) ){\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tsetTimeout(function(){\r\n\t\t\t\t\t\t\tdata.open();\r\n\t\t\t\t\t\t}, options.autoOpen);\r\n\r\n\t\t\t\t\t} else if(options.autoOpen === true ) {\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tdata.open();\r\n\t\t\t\t\t}\r\n\t\t\t\t\twindow.$iziModal.autoOpen++;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t        return this;\r\n\t    };\r\n\r\n\t\t$.fn[PLUGIN_NAME].defaults = {\r\n\t\t    title: '',\r\n\t\t    subtitle: '',\r\n\t\t    headerColor: '#88A0B9',\r\n\t\t    background: null,\r\n\t\t    theme: '',  // light\r\n\t\t    icon: null,\r\n\t\t    iconText: null,\r\n\t\t    iconColor: '',\r\n\t\t    rtl: false,\r\n\t\t    width: 600,\r\n\t\t    top: null,\r\n\t\t    bottom: null,\r\n\t\t    borderBottom: true,\r\n\t\t    padding: 0,\r\n\t\t    radius: 3,\r\n\t\t    zindex: 999,\r\n\t\t    iframe: false,\r\n\t\t    iframeHeight: 400,\r\n\t\t    iframeURL: null,\r\n\t\t    focusInput: true,\r\n\t\t    group: '',\r\n\t\t    loop: false,\r\n\t\t    navigateCaption: true,\r\n\t\t    navigateArrows: true, // Boolean, 'closeToModal', 'closeScreenEdge'\r\n\t\t    history: false,\r\n\t\t    restoreDefaultContent: false,\r\n\t\t    autoOpen: 0, // Boolean, Number\r\n\t\t    bodyOverflow: false,\r\n\t\t    fullscreen: false,\r\n\t\t    openFullscreen: false,\r\n\t\t    closeOnEscape: true,\r\n\t\t    closeButton: true,\r\n\t\t    appendTo: 'body', // or false\r\n\t\t    appendToOverlay: 'body', // or false\r\n\t\t    overlay: true,\r\n\t\t    overlayClose: true,\r\n\t\t    overlayColor: 'rgba(0, 0, 0, 0.4)',\r\n\t\t    timeout: false,\r\n\t\t    timeoutProgressbar: false,\r\n\t\t    pauseOnHover: false,\r\n\t\t    timeoutProgressbarColor: 'rgba(255,255,255,0.5)',\r\n\t\t    transitionIn: 'comingIn',   // comingIn, bounceInDown, bounceInUp, fadeInDown, fadeInUp, fadeInLeft, fadeInRight, flipInX\r\n\t\t    transitionOut: 'comingOut', // comingOut, bounceOutDown, bounceOutUp, fadeOutDown, fadeOutUp, , fadeOutLeft, fadeOutRight, flipOutX\r\n\t\t    transitionInOverlay: 'fadeIn',\r\n\t\t    transitionOutOverlay: 'fadeOut',\r\n\t\t    onFullscreen: function(){},\r\n\t\t    onResize: function(){},\r\n\t        onOpening: function(){},\r\n\t        onOpened: function(){},\r\n\t        onClosing: function(){},\r\n\t        onClosed: function(){},\r\n\t        afterRender: function(){}\r\n\t\t};\r\n\r\n\t$.fn[PLUGIN_NAME].Constructor = iziModal;\r\n\r\n    return $.fn.iziModal;\r\n\r\n}));"
  },
  {
    "path": "assets/vendor/jquery-ui-flick/jquery-ui-flick.css",
    "content": "/*! jQuery UI - v1.11.2 - 2014-10-16\n* https://code.jquery.com/ui/1.11.2/themes/flick/jquery-ui.css\n* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css\n* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Helvetica%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=2px&bgColorHeader=dddddd&bgTextureHeader=highlight_soft&bgImgOpacityHeader=50&borderColorHeader=dddddd&fcHeader=444444&iconColorHeader=0073ea&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=dddddd&fcContent=444444&iconColorContent=ff0084&bgColorDefault=f6f6f6&bgTextureDefault=highlight_soft&bgImgOpacityDefault=100&borderColorDefault=dddddd&fcDefault=0073ea&iconColorDefault=666666&bgColorHover=0073ea&bgTextureHover=highlight_soft&bgImgOpacityHover=25&borderColorHover=0073ea&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=dddddd&fcActive=ff0084&iconColorActive=454545&bgColorHighlight=ffffff&bgTextureHighlight=flat&bgImgOpacityHighlight=55&borderColorHighlight=cccccc&fcHighlight=444444&iconColorHighlight=0073ea&bgColorError=ffffff&bgTextureError=flat&bgImgOpacityError=55&borderColorError=ff0084&fcError=222222&iconColorError=ff0084&bgColorOverlay=eeeeee&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=80&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=60&thicknessShadow=4px&offsetTopShadow=-4px&offsetLeftShadow=-4px&cornerRadiusShadow=0px\n* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */\n\n/* Layout helpers\n----------------------------------*/\n.ui-helper-hidden {\n\tdisplay: none;\n}\n.ui-helper-hidden-accessible {\n\tborder: 0;\n\tclip: rect(0 0 0 0);\n\theight: 1px;\n\tmargin: -1px;\n\toverflow: hidden;\n\tpadding: 0;\n\tposition: absolute;\n\twidth: 1px;\n}\n.ui-helper-reset {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\toutline: 0;\n\tline-height: 1.3;\n\ttext-decoration: none;\n\tfont-size: 100%;\n\tlist-style: none;\n}\n.ui-helper-clearfix:before,\n.ui-helper-clearfix:after {\n\tcontent: \"\";\n\tdisplay: table;\n\tborder-collapse: collapse;\n}\n.ui-helper-clearfix:after {\n\tclear: both;\n}\n.ui-helper-clearfix {\n\tmin-height: 0; /* support: IE7 */\n}\n.ui-helper-zfix {\n\twidth: 100%;\n\theight: 100%;\n\ttop: 0;\n\tleft: 0;\n\tposition: absolute;\n\topacity: 0;\n\tfilter:Alpha(Opacity=0); /* support: IE8 */\n}\n\n.ui-front {\n\tz-index: 100;\n}\n\n\n/* Interaction Cues\n----------------------------------*/\n.ui-state-disabled {\n\tcursor: default !important;\n}\n\n\n/* Icons\n----------------------------------*/\n\n/* states and images */\n.ui-icon {\n\tdisplay: block;\n\ttext-indent: -99999px;\n\toverflow: hidden;\n\tbackground-repeat: no-repeat;\n}\n\n\n/* Misc visuals\n----------------------------------*/\n\n/* Overlays */\n.ui-widget-overlay {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n}\n.ui-accordion .ui-accordion-header {\n\tdisplay: block;\n\tcursor: pointer;\n\tposition: relative;\n\tmargin: 2px 0 0 0;\n\tpadding: .5em .5em .5em .7em;\n\tmin-height: 0; /* support: IE7 */\n\tfont-size: 100%;\n}\n.ui-accordion .ui-accordion-icons {\n\tpadding-left: 2.2em;\n}\n.ui-accordion .ui-accordion-icons .ui-accordion-icons {\n\tpadding-left: 2.2em;\n}\n.ui-accordion .ui-accordion-header .ui-accordion-header-icon {\n\tposition: absolute;\n\tleft: .5em;\n\ttop: 50%;\n\tmargin-top: -8px;\n}\n.ui-accordion .ui-accordion-content {\n\tpadding: 1em 2.2em;\n\tborder-top: 0;\n\toverflow: auto;\n}\n.ui-autocomplete {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tcursor: default;\n}\n.ui-button {\n\tdisplay: inline-block;\n\tposition: relative;\n\tpadding: 0;\n\tline-height: normal;\n\tmargin-right: .1em;\n\tcursor: pointer;\n\tvertical-align: middle;\n\ttext-align: center;\n\toverflow: visible; /* removes extra width in IE */\n}\n.ui-button,\n.ui-button:link,\n.ui-button:visited,\n.ui-button:hover,\n.ui-button:active {\n\ttext-decoration: none;\n}\n/* to make room for the icon, a width needs to be set here */\n.ui-button-icon-only {\n\twidth: 2.2em;\n}\n/* button elements seem to need a little more width */\nbutton.ui-button-icon-only {\n\twidth: 2.4em;\n}\n.ui-button-icons-only {\n\twidth: 3.4em;\n}\nbutton.ui-button-icons-only {\n\twidth: 3.7em;\n}\n\n/* button text element */\n.ui-button .ui-button-text {\n\tdisplay: block;\n\tline-height: normal;\n}\n.ui-button-text-only .ui-button-text {\n\tpadding: .4em 1em;\n}\n.ui-button-icon-only .ui-button-text,\n.ui-button-icons-only .ui-button-text {\n\tpadding: .4em;\n\ttext-indent: -9999999px;\n}\n.ui-button-text-icon-primary .ui-button-text,\n.ui-button-text-icons .ui-button-text {\n\tpadding: .4em 1em .4em 2.1em;\n}\n.ui-button-text-icon-secondary .ui-button-text,\n.ui-button-text-icons .ui-button-text {\n\tpadding: .4em 2.1em .4em 1em;\n}\n.ui-button-text-icons .ui-button-text {\n\tpadding-left: 2.1em;\n\tpadding-right: 2.1em;\n}\n/* no icon support for input elements, provide padding by default */\ninput.ui-button {\n\tpadding: .4em 1em;\n}\n\n/* button icon element(s) */\n.ui-button-icon-only .ui-icon,\n.ui-button-text-icon-primary .ui-icon,\n.ui-button-text-icon-secondary .ui-icon,\n.ui-button-text-icons .ui-icon,\n.ui-button-icons-only .ui-icon {\n\tposition: absolute;\n\ttop: 50%;\n\tmargin-top: -8px;\n}\n.ui-button-icon-only .ui-icon {\n\tleft: 50%;\n\tmargin-left: -8px;\n}\n.ui-button-text-icon-primary .ui-button-icon-primary,\n.ui-button-text-icons .ui-button-icon-primary,\n.ui-button-icons-only .ui-button-icon-primary {\n\tleft: .5em;\n}\n.ui-button-text-icon-secondary .ui-button-icon-secondary,\n.ui-button-text-icons .ui-button-icon-secondary,\n.ui-button-icons-only .ui-button-icon-secondary {\n\tright: .5em;\n}\n\n/* button sets */\n.ui-buttonset {\n\tmargin-right: 7px;\n}\n.ui-buttonset .ui-button {\n\tmargin-left: 0;\n\tmargin-right: -.3em;\n}\n\n/* workarounds */\n/* reset extra padding in Firefox, see h5bp.com/l */\ninput.ui-button::-moz-focus-inner,\nbutton.ui-button::-moz-focus-inner {\n\tborder: 0;\n\tpadding: 0;\n}\n.ui-datepicker {\n\twidth: 17em;\n\tpadding: .2em .2em 0;\n\tdisplay: none;\n}\n.ui-datepicker .ui-datepicker-header {\n\tposition: relative;\n\tpadding: .2em 0;\n}\n.ui-datepicker .ui-datepicker-prev,\n.ui-datepicker .ui-datepicker-next {\n\tposition: absolute;\n\ttop: 2px;\n\twidth: 1.8em;\n\theight: 1.8em;\n}\n.ui-datepicker .ui-datepicker-prev-hover,\n.ui-datepicker .ui-datepicker-next-hover {\n\ttop: 1px;\n}\n.ui-datepicker .ui-datepicker-prev {\n\tleft: 2px;\n}\n.ui-datepicker .ui-datepicker-next {\n\tright: 2px;\n}\n.ui-datepicker .ui-datepicker-prev-hover {\n\tleft: 1px;\n}\n.ui-datepicker .ui-datepicker-next-hover {\n\tright: 1px;\n}\n.ui-datepicker .ui-datepicker-prev span,\n.ui-datepicker .ui-datepicker-next span {\n\tdisplay: block;\n\tposition: absolute;\n\tleft: 50%;\n\tmargin-left: -8px;\n\ttop: 50%;\n\tmargin-top: -8px;\n}\n.ui-datepicker .ui-datepicker-title {\n\tmargin: 0 2.3em;\n\tline-height: 1.8em;\n\ttext-align: center;\n}\n.ui-datepicker .ui-datepicker-title select {\n\tfont-size: 1em;\n\tmargin: 1px 0;\n}\n.ui-datepicker select.ui-datepicker-month,\n.ui-datepicker select.ui-datepicker-year {\n\twidth: 45%;\n}\n.ui-datepicker table {\n\twidth: 100%;\n\tfont-size: .9em;\n\tborder-collapse: collapse;\n\tmargin: 0 0 .4em;\n}\n.ui-datepicker th {\n\tpadding: .7em .3em;\n\ttext-align: center;\n\tfont-weight: bold;\n\tborder: 0;\n}\n.ui-datepicker td {\n\tborder: 0;\n\tpadding: 1px;\n}\n.ui-datepicker td span,\n.ui-datepicker td a {\n\tdisplay: block;\n\tpadding: .2em;\n\ttext-align: right;\n\ttext-decoration: none;\n}\n.ui-datepicker .ui-datepicker-buttonpane {\n\tbackground-image: none;\n\tmargin: .7em 0 0 0;\n\tpadding: 0 .2em;\n\tborder-left: 0;\n\tborder-right: 0;\n\tborder-bottom: 0;\n}\n.ui-datepicker .ui-datepicker-buttonpane button {\n\tfloat: right;\n\tmargin: .5em .2em .4em;\n\tcursor: pointer;\n\tpadding: .2em .6em .3em .6em;\n\twidth: auto;\n\toverflow: visible;\n}\n.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {\n\tfloat: left;\n}\n\n/* with multiple calendars */\n.ui-datepicker.ui-datepicker-multi {\n\twidth: auto;\n}\n.ui-datepicker-multi .ui-datepicker-group {\n\tfloat: left;\n}\n.ui-datepicker-multi .ui-datepicker-group table {\n\twidth: 95%;\n\tmargin: 0 auto .4em;\n}\n.ui-datepicker-multi-2 .ui-datepicker-group {\n\twidth: 50%;\n}\n.ui-datepicker-multi-3 .ui-datepicker-group {\n\twidth: 33.3%;\n}\n.ui-datepicker-multi-4 .ui-datepicker-group {\n\twidth: 25%;\n}\n.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,\n.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {\n\tborder-left-width: 0;\n}\n.ui-datepicker-multi .ui-datepicker-buttonpane {\n\tclear: left;\n}\n.ui-datepicker-row-break {\n\tclear: both;\n\twidth: 100%;\n\tfont-size: 0;\n}\n\n/* RTL support */\n.ui-datepicker-rtl {\n\tdirection: rtl;\n}\n.ui-datepicker-rtl .ui-datepicker-prev {\n\tright: 2px;\n\tleft: auto;\n}\n.ui-datepicker-rtl .ui-datepicker-next {\n\tleft: 2px;\n\tright: auto;\n}\n.ui-datepicker-rtl .ui-datepicker-prev:hover {\n\tright: 1px;\n\tleft: auto;\n}\n.ui-datepicker-rtl .ui-datepicker-next:hover {\n\tleft: 1px;\n\tright: auto;\n}\n.ui-datepicker-rtl .ui-datepicker-buttonpane {\n\tclear: right;\n}\n.ui-datepicker-rtl .ui-datepicker-buttonpane button {\n\tfloat: left;\n}\n.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,\n.ui-datepicker-rtl .ui-datepicker-group {\n\tfloat: right;\n}\n.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,\n.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {\n\tborder-right-width: 0;\n\tborder-left-width: 1px;\n}\n.ui-dialog {\n\toverflow: hidden;\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tpadding: .2em;\n\toutline: 0;\n}\n.ui-dialog .ui-dialog-titlebar {\n\tpadding: .4em 1em;\n\tposition: relative;\n}\n.ui-dialog .ui-dialog-title {\n\tfloat: left;\n\tmargin: .1em 0;\n\twhite-space: nowrap;\n\twidth: 90%;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n.ui-dialog .ui-dialog-titlebar-close {\n\tposition: absolute;\n\tright: .3em;\n\ttop: 50%;\n\twidth: 20px;\n\tmargin: -10px 0 0 0;\n\tpadding: 1px;\n\theight: 20px;\n}\n.ui-dialog .ui-dialog-content {\n\tposition: relative;\n\tborder: 0;\n\tpadding: .5em 1em;\n\tbackground: none;\n\toverflow: auto;\n}\n.ui-dialog .ui-dialog-buttonpane {\n\ttext-align: left;\n\tborder-width: 1px 0 0 0;\n\tbackground-image: none;\n\tmargin-top: .5em;\n\tpadding: .3em 1em .5em .4em;\n}\n.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {\n\tfloat: right;\n}\n.ui-dialog .ui-dialog-buttonpane button {\n\tmargin: .5em .4em .5em 0;\n\tcursor: pointer;\n}\n.ui-dialog .ui-resizable-se {\n\twidth: 12px;\n\theight: 12px;\n\tright: -5px;\n\tbottom: -5px;\n\tbackground-position: 16px 16px;\n}\n.ui-draggable .ui-dialog-titlebar {\n\tcursor: move;\n}\n.ui-draggable-handle {\n\t-ms-touch-action: none;\n\ttouch-action: none;\n}\n.ui-menu {\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 0;\n\tdisplay: block;\n\toutline: none;\n}\n.ui-menu .ui-menu {\n\tposition: absolute;\n}\n.ui-menu .ui-menu-item {\n\tposition: relative;\n\tmargin: 0;\n\tpadding: 3px 1em 3px .4em;\n\tcursor: pointer;\n\tmin-height: 0; /* support: IE7 */\n\t/* support: IE10, see #8844 */\n\tlist-style-image: url(\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\");\n}\n.ui-menu .ui-menu-divider {\n\tmargin: 5px 0;\n\theight: 0;\n\tfont-size: 0;\n\tline-height: 0;\n\tborder-width: 1px 0 0 0;\n}\n.ui-menu .ui-state-focus,\n.ui-menu .ui-state-active {\n\tmargin: -1px;\n}\n\n/* icon support */\n.ui-menu-icons {\n\tposition: relative;\n}\n.ui-menu-icons .ui-menu-item {\n\tpadding-left: 2em;\n}\n\n/* left-aligned */\n.ui-menu .ui-icon {\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tleft: .2em;\n\tmargin: auto 0;\n}\n\n/* right-aligned */\n.ui-menu .ui-menu-icon {\n\tleft: auto;\n\tright: 0;\n}\n.ui-progressbar {\n\theight: 2em;\n\ttext-align: left;\n\toverflow: hidden;\n}\n.ui-progressbar .ui-progressbar-value {\n\tmargin: -1px;\n\theight: 100%;\n}\n.ui-progressbar .ui-progressbar-overlay {\n\tbackground: url(\"data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==\");\n\theight: 100%;\n\tfilter: alpha(opacity=25); /* support: IE8 */\n\topacity: 0.25;\n}\n.ui-progressbar-indeterminate .ui-progressbar-value {\n\tbackground-image: none;\n}\n.ui-resizable {\n\tposition: relative;\n}\n.ui-resizable-handle {\n\tposition: absolute;\n\tfont-size: 0.1px;\n\tdisplay: block;\n\t-ms-touch-action: none;\n\ttouch-action: none;\n}\n.ui-resizable-disabled .ui-resizable-handle,\n.ui-resizable-autohide .ui-resizable-handle {\n\tdisplay: none;\n}\n.ui-resizable-n {\n\tcursor: n-resize;\n\theight: 7px;\n\twidth: 100%;\n\ttop: -5px;\n\tleft: 0;\n}\n.ui-resizable-s {\n\tcursor: s-resize;\n\theight: 7px;\n\twidth: 100%;\n\tbottom: -5px;\n\tleft: 0;\n}\n.ui-resizable-e {\n\tcursor: e-resize;\n\twidth: 7px;\n\tright: -5px;\n\ttop: 0;\n\theight: 100%;\n}\n.ui-resizable-w {\n\tcursor: w-resize;\n\twidth: 7px;\n\tleft: -5px;\n\ttop: 0;\n\theight: 100%;\n}\n.ui-resizable-se {\n\tcursor: se-resize;\n\twidth: 12px;\n\theight: 12px;\n\tright: 1px;\n\tbottom: 1px;\n}\n.ui-resizable-sw {\n\tcursor: sw-resize;\n\twidth: 9px;\n\theight: 9px;\n\tleft: -5px;\n\tbottom: -5px;\n}\n.ui-resizable-nw {\n\tcursor: nw-resize;\n\twidth: 9px;\n\theight: 9px;\n\tleft: -5px;\n\ttop: -5px;\n}\n.ui-resizable-ne {\n\tcursor: ne-resize;\n\twidth: 9px;\n\theight: 9px;\n\tright: -5px;\n\ttop: -5px;\n}\n.ui-selectable {\n\t-ms-touch-action: none;\n\ttouch-action: none;\n}\n.ui-selectable-helper {\n\tposition: absolute;\n\tz-index: 100;\n\tborder: 1px dotted black;\n}\n.ui-selectmenu-menu {\n\tpadding: 0;\n\tmargin: 0;\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tdisplay: none;\n}\n.ui-selectmenu-menu .ui-menu {\n\toverflow: auto;\n\t/* Support: IE7 */\n\toverflow-x: hidden;\n\tpadding-bottom: 1px;\n}\n.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {\n\tfont-size: 1em;\n\tfont-weight: bold;\n\tline-height: 1.5;\n\tpadding: 2px 0.4em;\n\tmargin: 0.5em 0 0 0;\n\theight: auto;\n\tborder: 0;\n}\n.ui-selectmenu-open {\n\tdisplay: block;\n}\n.ui-selectmenu-button {\n\tdisplay: inline-block;\n\toverflow: hidden;\n\tposition: relative;\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n.ui-selectmenu-button span.ui-icon {\n\tright: 0.5em;\n\tleft: auto;\n\tmargin-top: -8px;\n\tposition: absolute;\n\ttop: 50%;\n}\n.ui-selectmenu-button span.ui-selectmenu-text {\n\ttext-align: left;\n\tpadding: 0.4em 2.1em 0.4em 1em;\n\tdisplay: block;\n\tline-height: 1.4;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n.ui-slider {\n\tposition: relative;\n\ttext-align: left;\n}\n.ui-slider .ui-slider-handle {\n\tposition: absolute;\n\tz-index: 2;\n\twidth: 1.2em;\n\theight: 1.2em;\n\tcursor: default;\n\t-ms-touch-action: none;\n\ttouch-action: none;\n}\n.ui-slider .ui-slider-range {\n\tposition: absolute;\n\tz-index: 1;\n\tfont-size: .7em;\n\tdisplay: block;\n\tborder: 0;\n\tbackground-position: 0 0;\n}\n\n/* support: IE8 - See #6727 */\n.ui-slider.ui-state-disabled .ui-slider-handle,\n.ui-slider.ui-state-disabled .ui-slider-range {\n\tfilter: inherit;\n}\n\n.ui-slider-horizontal {\n\theight: .8em;\n}\n.ui-slider-horizontal .ui-slider-handle {\n\ttop: -.3em;\n\tmargin-left: -.6em;\n}\n.ui-slider-horizontal .ui-slider-range {\n\ttop: 0;\n\theight: 100%;\n}\n.ui-slider-horizontal .ui-slider-range-min {\n\tleft: 0;\n}\n.ui-slider-horizontal .ui-slider-range-max {\n\tright: 0;\n}\n\n.ui-slider-vertical {\n\twidth: .8em;\n\theight: 100px;\n}\n.ui-slider-vertical .ui-slider-handle {\n\tleft: -.3em;\n\tmargin-left: 0;\n\tmargin-bottom: -.6em;\n}\n.ui-slider-vertical .ui-slider-range {\n\tleft: 0;\n\twidth: 100%;\n}\n.ui-slider-vertical .ui-slider-range-min {\n\tbottom: 0;\n}\n.ui-slider-vertical .ui-slider-range-max {\n\ttop: 0;\n}\n.ui-sortable-handle {\n\t-ms-touch-action: none;\n\ttouch-action: none;\n}\n.ui-spinner {\n\tposition: relative;\n\tdisplay: inline-block;\n\toverflow: hidden;\n\tpadding: 0;\n\tvertical-align: middle;\n}\n.ui-spinner-input {\n\tborder: none;\n\tbackground: none;\n\tcolor: inherit;\n\tpadding: 0;\n\tmargin: .2em 0;\n\tvertical-align: middle;\n\tmargin-left: .4em;\n\tmargin-right: 22px;\n}\n.ui-spinner-button {\n\twidth: 16px;\n\theight: 50%;\n\tfont-size: .5em;\n\tpadding: 0;\n\tmargin: 0;\n\ttext-align: center;\n\tposition: absolute;\n\tcursor: default;\n\tdisplay: block;\n\toverflow: hidden;\n\tright: 0;\n}\n/* more specificity required here to override default borders */\n.ui-spinner a.ui-spinner-button {\n\tborder-top: none;\n\tborder-bottom: none;\n\tborder-right: none;\n}\n/* vertically center icon */\n.ui-spinner .ui-icon {\n\tposition: absolute;\n\tmargin-top: -8px;\n\ttop: 50%;\n\tleft: 0;\n}\n.ui-spinner-up {\n\ttop: 0;\n}\n.ui-spinner-down {\n\tbottom: 0;\n}\n\n/* TR overrides */\n.ui-spinner .ui-icon-triangle-1-s {\n\t/* need to fix icons sprite */\n\tbackground-position: -65px -16px;\n}\n.ui-tabs {\n\tposition: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as \"fixed\") */\n\tpadding: .2em;\n}\n.ui-tabs .ui-tabs-nav {\n\tmargin: 0;\n\tpadding: .2em .2em 0;\n}\n.ui-tabs .ui-tabs-nav li {\n\tlist-style: none;\n\tfloat: left;\n\tposition: relative;\n\ttop: 0;\n\tmargin: 1px .2em 0 0;\n\tborder-bottom-width: 0;\n\tpadding: 0;\n\twhite-space: nowrap;\n}\n.ui-tabs .ui-tabs-nav .ui-tabs-anchor {\n\tfloat: left;\n\tpadding: .5em 1em;\n\ttext-decoration: none;\n}\n.ui-tabs .ui-tabs-nav li.ui-tabs-active {\n\tmargin-bottom: -1px;\n\tpadding-bottom: 1px;\n}\n.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,\n.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,\n.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {\n\tcursor: text;\n}\n.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {\n\tcursor: pointer;\n}\n.ui-tabs .ui-tabs-panel {\n\tdisplay: block;\n\tborder-width: 0;\n\tpadding: 1em 1.4em;\n\tbackground: none;\n}\n.ui-tooltip {\n\tpadding: 8px;\n\tposition: absolute;\n\tz-index: 9999;\n\tmax-width: 300px;\n\t-webkit-box-shadow: 0 0 5px #aaa;\n\tbox-shadow: 0 0 5px #aaa;\n}\nbody .ui-tooltip {\n\tborder-width: 2px;\n}\n\n/* Component containers\n----------------------------------*/\n.ui-widget {\n\tfont-family: Helvetica,Arial,sans-serif;\n\tfont-size: 1.1em;\n}\n.ui-widget .ui-widget {\n\tfont-size: 1em;\n}\n.ui-widget input,\n.ui-widget select,\n.ui-widget textarea,\n.ui-widget button {\n\tfont-family: Helvetica,Arial,sans-serif;\n\tfont-size: 1em;\n}\n.ui-widget-content {\n\tborder: 1px solid #dddddd;\n\tbackground: #ffffff url(\"images/ui-bg_flat_75_ffffff_40x100.png\") 50% 50% repeat-x;\n\tcolor: #444444;\n}\n.ui-widget-content a {\n\tcolor: #444444;\n}\n.ui-widget-header {\n\tborder: 1px solid #dddddd;\n\tbackground: #dddddd url(\"images/ui-bg_highlight-soft_50_dddddd_1x100.png\") 50% 50% repeat-x;\n\tcolor: #444444;\n\tfont-weight: bold;\n}\n.ui-widget-header a {\n\tcolor: #444444;\n}\n\n/* Interaction states\n----------------------------------*/\n.ui-state-default,\n.ui-widget-content .ui-state-default,\n.ui-widget-header .ui-state-default {\n\tborder: 1px solid #dddddd;\n\tbackground: #f6f6f6 url(\"images/ui-bg_highlight-soft_100_f6f6f6_1x100.png\") 50% 50% repeat-x;\n\tfont-weight: bold;\n\tcolor: #0073ea;\n}\n.ui-state-default a,\n.ui-state-default a:link,\n.ui-state-default a:visited {\n\tcolor: #0073ea;\n\ttext-decoration: none;\n}\n.ui-state-hover,\n.ui-widget-content .ui-state-hover,\n.ui-widget-header .ui-state-hover,\n.ui-state-focus,\n.ui-widget-content .ui-state-focus,\n.ui-widget-header .ui-state-focus {\n\tborder: 1px solid #0073ea;\n\tbackground: #0073ea url(\"images/ui-bg_highlight-soft_25_0073ea_1x100.png\") 50% 50% repeat-x;\n\tfont-weight: bold;\n\tcolor: #ffffff;\n}\n.ui-state-hover a,\n.ui-state-hover a:hover,\n.ui-state-hover a:link,\n.ui-state-hover a:visited,\n.ui-state-focus a,\n.ui-state-focus a:hover,\n.ui-state-focus a:link,\n.ui-state-focus a:visited {\n\tcolor: #ffffff;\n\ttext-decoration: none;\n}\n.ui-state-active,\n.ui-widget-content .ui-state-active,\n.ui-widget-header .ui-state-active {\n\tborder: 1px solid #dddddd;\n\tbackground: #ffffff url(\"images/ui-bg_glass_65_ffffff_1x400.png\") 50% 50% repeat-x;\n\tfont-weight: bold;\n\tcolor: #ff0084;\n}\n.ui-state-active a,\n.ui-state-active a:link,\n.ui-state-active a:visited {\n\tcolor: #ff0084;\n\ttext-decoration: none;\n}\n\n/* Interaction Cues\n----------------------------------*/\n.ui-state-highlight,\n.ui-widget-content .ui-state-highlight,\n.ui-widget-header .ui-state-highlight {\n\tborder: 1px solid #cccccc;\n\tbackground: #ffffff url(\"images/ui-bg_flat_55_ffffff_40x100.png\") 50% 50% repeat-x;\n\tcolor: #444444;\n}\n.ui-state-highlight a,\n.ui-widget-content .ui-state-highlight a,\n.ui-widget-header .ui-state-highlight a {\n\tcolor: #444444;\n}\n.ui-state-error,\n.ui-widget-content .ui-state-error,\n.ui-widget-header .ui-state-error {\n\tborder: 1px solid #ff0084;\n\tbackground: #ffffff url(\"images/ui-bg_flat_55_ffffff_40x100.png\") 50% 50% repeat-x;\n\tcolor: #222222;\n}\n.ui-state-error a,\n.ui-widget-content .ui-state-error a,\n.ui-widget-header .ui-state-error a {\n\tcolor: #222222;\n}\n.ui-state-error-text,\n.ui-widget-content .ui-state-error-text,\n.ui-widget-header .ui-state-error-text {\n\tcolor: #222222;\n}\n.ui-priority-primary,\n.ui-widget-content .ui-priority-primary,\n.ui-widget-header .ui-priority-primary {\n\tfont-weight: bold;\n}\n.ui-priority-secondary,\n.ui-widget-content .ui-priority-secondary,\n.ui-widget-header .ui-priority-secondary {\n\topacity: .7;\n\tfilter:Alpha(Opacity=70); /* support: IE8 */\n\tfont-weight: normal;\n}\n.ui-state-disabled,\n.ui-widget-content .ui-state-disabled,\n.ui-widget-header .ui-state-disabled {\n\topacity: .35;\n\tfilter:Alpha(Opacity=35); /* support: IE8 */\n\tbackground-image: none;\n}\n.ui-state-disabled .ui-icon {\n\tfilter:Alpha(Opacity=35); /* support: IE8 - See #6059 */\n}\n\n/* Icons\n----------------------------------*/\n\n/* states and images */\n.ui-icon {\n\twidth: 16px;\n\theight: 16px;\n}\n.ui-icon,\n.ui-widget-content .ui-icon {\n\tbackground-image: url(\"images/ui-icons_ff0084_256x240.png\");\n}\n.ui-widget-header .ui-icon {\n\tbackground-image: url(\"images/ui-icons_0073ea_256x240.png\");\n}\n.ui-state-default .ui-icon {\n\tbackground-image: url(\"images/ui-icons_666666_256x240.png\");\n}\n.ui-state-hover .ui-icon,\n.ui-state-focus .ui-icon {\n\tbackground-image: url(\"images/ui-icons_ffffff_256x240.png\");\n}\n.ui-state-active .ui-icon {\n\tbackground-image: url(\"images/ui-icons_454545_256x240.png\");\n}\n.ui-state-highlight .ui-icon {\n\tbackground-image: url(\"images/ui-icons_0073ea_256x240.png\");\n}\n.ui-state-error .ui-icon,\n.ui-state-error-text .ui-icon {\n\tbackground-image: url(\"images/ui-icons_ff0084_256x240.png\");\n}\n\n/* positioning */\n.ui-icon-blank { background-position: 16px 16px; }\n.ui-icon-carat-1-n { background-position: 0 0; }\n.ui-icon-carat-1-ne { background-position: -16px 0; }\n.ui-icon-carat-1-e { background-position: -32px 0; }\n.ui-icon-carat-1-se { background-position: -48px 0; }\n.ui-icon-carat-1-s { background-position: -64px 0; }\n.ui-icon-carat-1-sw { background-position: -80px 0; }\n.ui-icon-carat-1-w { background-position: -96px 0; }\n.ui-icon-carat-1-nw { background-position: -112px 0; }\n.ui-icon-carat-2-n-s { background-position: -128px 0; }\n.ui-icon-carat-2-e-w { background-position: -144px 0; }\n.ui-icon-triangle-1-n { background-position: 0 -16px; }\n.ui-icon-triangle-1-ne { background-position: -16px -16px; }\n.ui-icon-triangle-1-e { background-position: -32px -16px; }\n.ui-icon-triangle-1-se { background-position: -48px -16px; }\n.ui-icon-triangle-1-s { background-position: -64px -16px; }\n.ui-icon-triangle-1-sw { background-position: -80px -16px; }\n.ui-icon-triangle-1-w { background-position: -96px -16px; }\n.ui-icon-triangle-1-nw { background-position: -112px -16px; }\n.ui-icon-triangle-2-n-s { background-position: -128px -16px; }\n.ui-icon-triangle-2-e-w { background-position: -144px -16px; }\n.ui-icon-arrow-1-n { background-position: 0 -32px; }\n.ui-icon-arrow-1-ne { background-position: -16px -32px; }\n.ui-icon-arrow-1-e { background-position: -32px -32px; }\n.ui-icon-arrow-1-se { background-position: -48px -32px; }\n.ui-icon-arrow-1-s { background-position: -64px -32px; }\n.ui-icon-arrow-1-sw { background-position: -80px -32px; }\n.ui-icon-arrow-1-w { background-position: -96px -32px; }\n.ui-icon-arrow-1-nw { background-position: -112px -32px; }\n.ui-icon-arrow-2-n-s { background-position: -128px -32px; }\n.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }\n.ui-icon-arrow-2-e-w { background-position: -160px -32px; }\n.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }\n.ui-icon-arrowstop-1-n { background-position: -192px -32px; }\n.ui-icon-arrowstop-1-e { background-position: -208px -32px; }\n.ui-icon-arrowstop-1-s { background-position: -224px -32px; }\n.ui-icon-arrowstop-1-w { background-position: -240px -32px; }\n.ui-icon-arrowthick-1-n { background-position: 0 -48px; }\n.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }\n.ui-icon-arrowthick-1-e { background-position: -32px -48px; }\n.ui-icon-arrowthick-1-se { background-position: -48px -48px; }\n.ui-icon-arrowthick-1-s { background-position: -64px -48px; }\n.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }\n.ui-icon-arrowthick-1-w { background-position: -96px -48px; }\n.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }\n.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }\n.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }\n.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }\n.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }\n.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }\n.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }\n.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }\n.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }\n.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }\n.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }\n.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }\n.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }\n.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }\n.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }\n.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }\n.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }\n.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }\n.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }\n.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }\n.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }\n.ui-icon-arrow-4 { background-position: 0 -80px; }\n.ui-icon-arrow-4-diag { background-position: -16px -80px; }\n.ui-icon-extlink { background-position: -32px -80px; }\n.ui-icon-newwin { background-position: -48px -80px; }\n.ui-icon-refresh { background-position: -64px -80px; }\n.ui-icon-shuffle { background-position: -80px -80px; }\n.ui-icon-transfer-e-w { background-position: -96px -80px; }\n.ui-icon-transferthick-e-w { background-position: -112px -80px; }\n.ui-icon-folder-collapsed { background-position: 0 -96px; }\n.ui-icon-folder-open { background-position: -16px -96px; }\n.ui-icon-document { background-position: -32px -96px; }\n.ui-icon-document-b { background-position: -48px -96px; }\n.ui-icon-note { background-position: -64px -96px; }\n.ui-icon-mail-closed { background-position: -80px -96px; }\n.ui-icon-mail-open { background-position: -96px -96px; }\n.ui-icon-suitcase { background-position: -112px -96px; }\n.ui-icon-comment { background-position: -128px -96px; }\n.ui-icon-person { background-position: -144px -96px; }\n.ui-icon-print { background-position: -160px -96px; }\n.ui-icon-trash { background-position: -176px -96px; }\n.ui-icon-locked { background-position: -192px -96px; }\n.ui-icon-unlocked { background-position: -208px -96px; }\n.ui-icon-bookmark { background-position: -224px -96px; }\n.ui-icon-tag { background-position: -240px -96px; }\n.ui-icon-home { background-position: 0 -112px; }\n.ui-icon-flag { background-position: -16px -112px; }\n.ui-icon-calendar { background-position: -32px -112px; }\n.ui-icon-cart { background-position: -48px -112px; }\n.ui-icon-pencil { background-position: -64px -112px; }\n.ui-icon-clock { background-position: -80px -112px; }\n.ui-icon-disk { background-position: -96px -112px; }\n.ui-icon-calculator { background-position: -112px -112px; }\n.ui-icon-zoomin { background-position: -128px -112px; }\n.ui-icon-zoomout { background-position: -144px -112px; }\n.ui-icon-search { background-position: -160px -112px; }\n.ui-icon-wrench { background-position: -176px -112px; }\n.ui-icon-gear { background-position: -192px -112px; }\n.ui-icon-heart { background-position: -208px -112px; }\n.ui-icon-star { background-position: -224px -112px; }\n.ui-icon-link { background-position: -240px -112px; }\n.ui-icon-cancel { background-position: 0 -128px; }\n.ui-icon-plus { background-position: -16px -128px; }\n.ui-icon-plusthick { background-position: -32px -128px; }\n.ui-icon-minus { background-position: -48px -128px; }\n.ui-icon-minusthick { background-position: -64px -128px; }\n.ui-icon-close { background-position: -80px -128px; }\n.ui-icon-closethick { background-position: -96px -128px; }\n.ui-icon-key { background-position: -112px -128px; }\n.ui-icon-lightbulb { background-position: -128px -128px; }\n.ui-icon-scissors { background-position: -144px -128px; }\n.ui-icon-clipboard { background-position: -160px -128px; }\n.ui-icon-copy { background-position: -176px -128px; }\n.ui-icon-contact { background-position: -192px -128px; }\n.ui-icon-image { background-position: -208px -128px; }\n.ui-icon-video { background-position: -224px -128px; }\n.ui-icon-script { background-position: -240px -128px; }\n.ui-icon-alert { background-position: 0 -144px; }\n.ui-icon-info { background-position: -16px -144px; }\n.ui-icon-notice { background-position: -32px -144px; }\n.ui-icon-help { background-position: -48px -144px; }\n.ui-icon-check { background-position: -64px -144px; }\n.ui-icon-bullet { background-position: -80px -144px; }\n.ui-icon-radio-on { background-position: -96px -144px; }\n.ui-icon-radio-off { background-position: -112px -144px; }\n.ui-icon-pin-w { background-position: -128px -144px; }\n.ui-icon-pin-s { background-position: -144px -144px; }\n.ui-icon-play { background-position: 0 -160px; }\n.ui-icon-pause { background-position: -16px -160px; }\n.ui-icon-seek-next { background-position: -32px -160px; }\n.ui-icon-seek-prev { background-position: -48px -160px; }\n.ui-icon-seek-end { background-position: -64px -160px; }\n.ui-icon-seek-start { background-position: -80px -160px; }\n/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */\n.ui-icon-seek-first { background-position: -80px -160px; }\n.ui-icon-stop { background-position: -96px -160px; }\n.ui-icon-eject { background-position: -112px -160px; }\n.ui-icon-volume-off { background-position: -128px -160px; }\n.ui-icon-volume-on { background-position: -144px -160px; }\n.ui-icon-power { background-position: 0 -176px; }\n.ui-icon-signal-diag { background-position: -16px -176px; }\n.ui-icon-signal { background-position: -32px -176px; }\n.ui-icon-battery-0 { background-position: -48px -176px; }\n.ui-icon-battery-1 { background-position: -64px -176px; }\n.ui-icon-battery-2 { background-position: -80px -176px; }\n.ui-icon-battery-3 { background-position: -96px -176px; }\n.ui-icon-circle-plus { background-position: 0 -192px; }\n.ui-icon-circle-minus { background-position: -16px -192px; }\n.ui-icon-circle-close { background-position: -32px -192px; }\n.ui-icon-circle-triangle-e { background-position: -48px -192px; }\n.ui-icon-circle-triangle-s { background-position: -64px -192px; }\n.ui-icon-circle-triangle-w { background-position: -80px -192px; }\n.ui-icon-circle-triangle-n { background-position: -96px -192px; }\n.ui-icon-circle-arrow-e { background-position: -112px -192px; }\n.ui-icon-circle-arrow-s { background-position: -128px -192px; }\n.ui-icon-circle-arrow-w { background-position: -144px -192px; }\n.ui-icon-circle-arrow-n { background-position: -160px -192px; }\n.ui-icon-circle-zoomin { background-position: -176px -192px; }\n.ui-icon-circle-zoomout { background-position: -192px -192px; }\n.ui-icon-circle-check { background-position: -208px -192px; }\n.ui-icon-circlesmall-plus { background-position: 0 -208px; }\n.ui-icon-circlesmall-minus { background-position: -16px -208px; }\n.ui-icon-circlesmall-close { background-position: -32px -208px; }\n.ui-icon-squaresmall-plus { background-position: -48px -208px; }\n.ui-icon-squaresmall-minus { background-position: -64px -208px; }\n.ui-icon-squaresmall-close { background-position: -80px -208px; }\n.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }\n.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }\n.ui-icon-grip-solid-vertical { background-position: -32px -224px; }\n.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }\n.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }\n.ui-icon-grip-diagonal-se { background-position: -80px -224px; }\n\n\n/* Misc visuals\n----------------------------------*/\n\n/* Corner radius */\n.ui-corner-all,\n.ui-corner-top,\n.ui-corner-left,\n.ui-corner-tl {\n\tborder-top-left-radius: 2px;\n}\n.ui-corner-all,\n.ui-corner-top,\n.ui-corner-right,\n.ui-corner-tr {\n\tborder-top-right-radius: 2px;\n}\n.ui-corner-all,\n.ui-corner-bottom,\n.ui-corner-left,\n.ui-corner-bl {\n\tborder-bottom-left-radius: 2px;\n}\n.ui-corner-all,\n.ui-corner-bottom,\n.ui-corner-right,\n.ui-corner-br {\n\tborder-bottom-right-radius: 2px;\n}\n\n/* Overlays */\n.ui-widget-overlay {\n\tbackground: #eeeeee url(\"images/ui-bg_flat_0_eeeeee_40x100.png\") 50% 50% repeat-x;\n\topacity: .8;\n\tfilter: Alpha(Opacity=80); /* support: IE8 */\n}\n.ui-widget-shadow {\n\tmargin: -4px 0 0 -4px;\n\tpadding: 4px;\n\tbackground: #aaaaaa url(\"images/ui-bg_flat_0_aaaaaa_40x100.png\") 50% 50% repeat-x;\n\topacity: .6;\n\tfilter: Alpha(Opacity=60); /* support: IE8 */\n\tborder-radius: 0px;\n}\n"
  },
  {
    "path": "assets/vendor/quill/quill.bubble.css",
    "content": "/*!\n * Quill Editor v1.3.5\n * https://quilljs.com/\n * Copyright (c) 2014, Jason Chen\n * Copyright (c) 2013, salesforce.com\n */\n.ql-container {\n  box-sizing: border-box;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 13px;\n  height: 100%;\n  margin: 0px;\n  position: relative;\n}\n.ql-container.ql-disabled .ql-tooltip {\n  visibility: hidden;\n}\n.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {\n  pointer-events: none;\n}\n.ql-clipboard {\n  left: -100000px;\n  height: 1px;\n  overflow-y: hidden;\n  position: absolute;\n  top: 50%;\n}\n.ql-clipboard p {\n  margin: 0;\n  padding: 0;\n}\n.ql-editor {\n  box-sizing: border-box;\n  line-height: 1.42;\n  height: 100%;\n  outline: none;\n  overflow-y: auto;\n  padding: 12px 15px;\n  tab-size: 4;\n  -moz-tab-size: 4;\n  text-align: left;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n.ql-editor > * {\n  cursor: text;\n}\n.ql-editor p,\n.ql-editor ol,\n.ql-editor ul,\n.ql-editor pre,\n.ql-editor blockquote,\n.ql-editor h1,\n.ql-editor h2,\n.ql-editor h3,\n.ql-editor h4,\n.ql-editor h5,\n.ql-editor h6 {\n  margin: 0;\n  padding: 0;\n  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;\n}\n.ql-editor ol,\n.ql-editor ul {\n  padding-left: 1.5em;\n}\n.ql-editor ol > li,\n.ql-editor ul > li {\n  list-style-type: none;\n}\n.ql-editor ul > li::before {\n  content: '\\2022';\n}\n.ql-editor ul[data-checked=true],\n.ql-editor ul[data-checked=false] {\n  pointer-events: none;\n}\n.ql-editor ul[data-checked=true] > li *,\n.ql-editor ul[data-checked=false] > li * {\n  pointer-events: all;\n}\n.ql-editor ul[data-checked=true] > li::before,\n.ql-editor ul[data-checked=false] > li::before {\n  color: #777;\n  cursor: pointer;\n  pointer-events: all;\n}\n.ql-editor ul[data-checked=true] > li::before {\n  content: '\\2611';\n}\n.ql-editor ul[data-checked=false] > li::before {\n  content: '\\2610';\n}\n.ql-editor li::before {\n  display: inline-block;\n  white-space: nowrap;\n  width: 1.2em;\n}\n.ql-editor li:not(.ql-direction-rtl)::before {\n  margin-left: -1.5em;\n  margin-right: 0.3em;\n  text-align: right;\n}\n.ql-editor li.ql-direction-rtl::before {\n  margin-left: 0.3em;\n  margin-right: -1.5em;\n}\n.ql-editor ol li:not(.ql-direction-rtl),\n.ql-editor ul li:not(.ql-direction-rtl) {\n  padding-left: 1.5em;\n}\n.ql-editor ol li.ql-direction-rtl,\n.ql-editor ul li.ql-direction-rtl {\n  padding-right: 1.5em;\n}\n.ql-editor ol li {\n  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;\n  counter-increment: list-0;\n}\n.ql-editor ol li:before {\n  content: counter(list-0, decimal) '. ';\n}\n.ql-editor ol li.ql-indent-1 {\n  counter-increment: list-1;\n}\n.ql-editor ol li.ql-indent-1:before {\n  content: counter(list-1, lower-alpha) '. ';\n}\n.ql-editor ol li.ql-indent-1 {\n  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;\n}\n.ql-editor ol li.ql-indent-2 {\n  counter-increment: list-2;\n}\n.ql-editor ol li.ql-indent-2:before {\n  content: counter(list-2, lower-roman) '. ';\n}\n.ql-editor ol li.ql-indent-2 {\n  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;\n}\n.ql-editor ol li.ql-indent-3 {\n  counter-increment: list-3;\n}\n.ql-editor ol li.ql-indent-3:before {\n  content: counter(list-3, decimal) '. ';\n}\n.ql-editor ol li.ql-indent-3 {\n  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;\n}\n.ql-editor ol li.ql-indent-4 {\n  counter-increment: list-4;\n}\n.ql-editor ol li.ql-indent-4:before {\n  content: counter(list-4, lower-alpha) '. ';\n}\n.ql-editor ol li.ql-indent-4 {\n  counter-reset: list-5 list-6 list-7 list-8 list-9;\n}\n.ql-editor ol li.ql-indent-5 {\n  counter-increment: list-5;\n}\n.ql-editor ol li.ql-indent-5:before {\n  content: counter(list-5, lower-roman) '. ';\n}\n.ql-editor ol li.ql-indent-5 {\n  counter-reset: list-6 list-7 list-8 list-9;\n}\n.ql-editor ol li.ql-indent-6 {\n  counter-increment: list-6;\n}\n.ql-editor ol li.ql-indent-6:before {\n  content: counter(list-6, decimal) '. ';\n}\n.ql-editor ol li.ql-indent-6 {\n  counter-reset: list-7 list-8 list-9;\n}\n.ql-editor ol li.ql-indent-7 {\n  counter-increment: list-7;\n}\n.ql-editor ol li.ql-indent-7:before {\n  content: counter(list-7, lower-alpha) '. ';\n}\n.ql-editor ol li.ql-indent-7 {\n  counter-reset: list-8 list-9;\n}\n.ql-editor ol li.ql-indent-8 {\n  counter-increment: list-8;\n}\n.ql-editor ol li.ql-indent-8:before {\n  content: counter(list-8, lower-roman) '. ';\n}\n.ql-editor ol li.ql-indent-8 {\n  counter-reset: list-9;\n}\n.ql-editor ol li.ql-indent-9 {\n  counter-increment: list-9;\n}\n.ql-editor ol li.ql-indent-9:before {\n  content: counter(list-9, decimal) '. ';\n}\n.ql-editor .ql-indent-1:not(.ql-direction-rtl) {\n  padding-left: 3em;\n}\n.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {\n  padding-left: 4.5em;\n}\n.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {\n  padding-right: 3em;\n}\n.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {\n  padding-right: 4.5em;\n}\n.ql-editor .ql-indent-2:not(.ql-direction-rtl) {\n  padding-left: 6em;\n}\n.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {\n  padding-left: 7.5em;\n}\n.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {\n  padding-right: 6em;\n}\n.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {\n  padding-right: 7.5em;\n}\n.ql-editor .ql-indent-3:not(.ql-direction-rtl) {\n  padding-left: 9em;\n}\n.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {\n  padding-left: 10.5em;\n}\n.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {\n  padding-right: 9em;\n}\n.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {\n  padding-right: 10.5em;\n}\n.ql-editor .ql-indent-4:not(.ql-direction-rtl) {\n  padding-left: 12em;\n}\n.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {\n  padding-left: 13.5em;\n}\n.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {\n  padding-right: 12em;\n}\n.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {\n  padding-right: 13.5em;\n}\n.ql-editor .ql-indent-5:not(.ql-direction-rtl) {\n  padding-left: 15em;\n}\n.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {\n  padding-left: 16.5em;\n}\n.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {\n  padding-right: 15em;\n}\n.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {\n  padding-right: 16.5em;\n}\n.ql-editor .ql-indent-6:not(.ql-direction-rtl) {\n  padding-left: 18em;\n}\n.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {\n  padding-left: 19.5em;\n}\n.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {\n  padding-right: 18em;\n}\n.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {\n  padding-right: 19.5em;\n}\n.ql-editor .ql-indent-7:not(.ql-direction-rtl) {\n  padding-left: 21em;\n}\n.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {\n  padding-left: 22.5em;\n}\n.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {\n  padding-right: 21em;\n}\n.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {\n  padding-right: 22.5em;\n}\n.ql-editor .ql-indent-8:not(.ql-direction-rtl) {\n  padding-left: 24em;\n}\n.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {\n  padding-left: 25.5em;\n}\n.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {\n  padding-right: 24em;\n}\n.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {\n  padding-right: 25.5em;\n}\n.ql-editor .ql-indent-9:not(.ql-direction-rtl) {\n  padding-left: 27em;\n}\n.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {\n  padding-left: 28.5em;\n}\n.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {\n  padding-right: 27em;\n}\n.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {\n  padding-right: 28.5em;\n}\n.ql-editor .ql-video {\n  display: block;\n  max-width: 100%;\n}\n.ql-editor .ql-video.ql-align-center {\n  margin: 0 auto;\n}\n.ql-editor .ql-video.ql-align-right {\n  margin: 0 0 0 auto;\n}\n.ql-editor .ql-bg-black {\n  background-color: #000;\n}\n.ql-editor .ql-bg-red {\n  background-color: #e60000;\n}\n.ql-editor .ql-bg-orange {\n  background-color: #f90;\n}\n.ql-editor .ql-bg-yellow {\n  background-color: #ff0;\n}\n.ql-editor .ql-bg-green {\n  background-color: #008a00;\n}\n.ql-editor .ql-bg-blue {\n  background-color: #06c;\n}\n.ql-editor .ql-bg-purple {\n  background-color: #93f;\n}\n.ql-editor .ql-color-white {\n  color: #fff;\n}\n.ql-editor .ql-color-red {\n  color: #e60000;\n}\n.ql-editor .ql-color-orange {\n  color: #f90;\n}\n.ql-editor .ql-color-yellow {\n  color: #ff0;\n}\n.ql-editor .ql-color-green {\n  color: #008a00;\n}\n.ql-editor .ql-color-blue {\n  color: #06c;\n}\n.ql-editor .ql-color-purple {\n  color: #93f;\n}\n.ql-editor .ql-font-serif {\n  font-family: Georgia, Times New Roman, serif;\n}\n.ql-editor .ql-font-monospace {\n  font-family: Monaco, Courier New, monospace;\n}\n.ql-editor .ql-size-small {\n  font-size: 0.75em;\n}\n.ql-editor .ql-size-large {\n  font-size: 1.5em;\n}\n.ql-editor .ql-size-huge {\n  font-size: 2.5em;\n}\n.ql-editor .ql-direction-rtl {\n  direction: rtl;\n  text-align: inherit;\n}\n.ql-editor .ql-align-center {\n  text-align: center;\n}\n.ql-editor .ql-align-justify {\n  text-align: justify;\n}\n.ql-editor .ql-align-right {\n  text-align: right;\n}\n.ql-editor.ql-blank::before {\n  color: rgba(0,0,0,0.6);\n  content: attr(data-placeholder);\n  font-style: italic;\n  left: 15px;\n  pointer-events: none;\n  position: absolute;\n  right: 15px;\n}\n.ql-bubble.ql-toolbar:after,\n.ql-bubble .ql-toolbar:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n.ql-bubble.ql-toolbar button,\n.ql-bubble .ql-toolbar button {\n  background: none;\n  border: none;\n  cursor: pointer;\n  display: inline-block;\n  float: left;\n  height: 24px;\n  padding: 3px 5px;\n  width: 28px;\n}\n.ql-bubble.ql-toolbar button svg,\n.ql-bubble .ql-toolbar button svg {\n  float: left;\n  height: 100%;\n}\n.ql-bubble.ql-toolbar button:active:hover,\n.ql-bubble .ql-toolbar button:active:hover {\n  outline: none;\n}\n.ql-bubble.ql-toolbar input.ql-image[type=file],\n.ql-bubble .ql-toolbar input.ql-image[type=file] {\n  display: none;\n}\n.ql-bubble.ql-toolbar button:hover,\n.ql-bubble .ql-toolbar button:hover,\n.ql-bubble.ql-toolbar button:focus,\n.ql-bubble .ql-toolbar button:focus,\n.ql-bubble.ql-toolbar button.ql-active,\n.ql-bubble .ql-toolbar button.ql-active,\n.ql-bubble.ql-toolbar .ql-picker-label:hover,\n.ql-bubble .ql-toolbar .ql-picker-label:hover,\n.ql-bubble.ql-toolbar .ql-picker-label.ql-active,\n.ql-bubble .ql-toolbar .ql-picker-label.ql-active,\n.ql-bubble.ql-toolbar .ql-picker-item:hover,\n.ql-bubble .ql-toolbar .ql-picker-item:hover,\n.ql-bubble.ql-toolbar .ql-picker-item.ql-selected,\n.ql-bubble .ql-toolbar .ql-picker-item.ql-selected {\n  color: #fff;\n}\n.ql-bubble.ql-toolbar button:hover .ql-fill,\n.ql-bubble .ql-toolbar button:hover .ql-fill,\n.ql-bubble.ql-toolbar button:focus .ql-fill,\n.ql-bubble .ql-toolbar button:focus .ql-fill,\n.ql-bubble.ql-toolbar button.ql-active .ql-fill,\n.ql-bubble .ql-toolbar button.ql-active .ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill,\n.ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill,\n.ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill,\n.ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill,\n.ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill,\n.ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill,\n.ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,\n.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,\n.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {\n  fill: #fff;\n}\n.ql-bubble.ql-toolbar button:hover .ql-stroke,\n.ql-bubble .ql-toolbar button:hover .ql-stroke,\n.ql-bubble.ql-toolbar button:focus .ql-stroke,\n.ql-bubble .ql-toolbar button:focus .ql-stroke,\n.ql-bubble.ql-toolbar button.ql-active .ql-stroke,\n.ql-bubble .ql-toolbar button.ql-active .ql-stroke,\n.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke,\n.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke,\n.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke,\n.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke,\n.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke,\n.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke,\n.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,\n.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,\n.ql-bubble.ql-toolbar button:hover .ql-stroke-miter,\n.ql-bubble .ql-toolbar button:hover .ql-stroke-miter,\n.ql-bubble.ql-toolbar button:focus .ql-stroke-miter,\n.ql-bubble .ql-toolbar button:focus .ql-stroke-miter,\n.ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter,\n.ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter,\n.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,\n.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,\n.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,\n.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,\n.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,\n.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,\n.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,\n.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {\n  stroke: #fff;\n}\n@media (pointer: coarse) {\n  .ql-bubble.ql-toolbar button:hover:not(.ql-active),\n  .ql-bubble .ql-toolbar button:hover:not(.ql-active) {\n    color: #ccc;\n  }\n  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill,\n  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill,\n  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,\n  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {\n    fill: #ccc;\n  }\n  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke,\n  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke,\n  .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,\n  .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {\n    stroke: #ccc;\n  }\n}\n.ql-bubble {\n  box-sizing: border-box;\n}\n.ql-bubble * {\n  box-sizing: border-box;\n}\n.ql-bubble .ql-hidden {\n  display: none;\n}\n.ql-bubble .ql-out-bottom,\n.ql-bubble .ql-out-top {\n  visibility: hidden;\n}\n.ql-bubble .ql-tooltip {\n  position: absolute;\n  transform: translateY(10px);\n}\n.ql-bubble .ql-tooltip a {\n  cursor: pointer;\n  text-decoration: none;\n}\n.ql-bubble .ql-tooltip.ql-flip {\n  transform: translateY(-10px);\n}\n.ql-bubble .ql-formats {\n  display: inline-block;\n  vertical-align: middle;\n}\n.ql-bubble .ql-formats:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n.ql-bubble .ql-stroke {\n  fill: none;\n  stroke: #ccc;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  stroke-width: 2;\n}\n.ql-bubble .ql-stroke-miter {\n  fill: none;\n  stroke: #ccc;\n  stroke-miterlimit: 10;\n  stroke-width: 2;\n}\n.ql-bubble .ql-fill,\n.ql-bubble .ql-stroke.ql-fill {\n  fill: #ccc;\n}\n.ql-bubble .ql-empty {\n  fill: none;\n}\n.ql-bubble .ql-even {\n  fill-rule: evenodd;\n}\n.ql-bubble .ql-thin,\n.ql-bubble .ql-stroke.ql-thin {\n  stroke-width: 1;\n}\n.ql-bubble .ql-transparent {\n  opacity: 0.4;\n}\n.ql-bubble .ql-direction svg:last-child {\n  display: none;\n}\n.ql-bubble .ql-direction.ql-active svg:last-child {\n  display: inline;\n}\n.ql-bubble .ql-direction.ql-active svg:first-child {\n  display: none;\n}\n.ql-bubble .ql-editor h1 {\n  font-size: 2em;\n}\n.ql-bubble .ql-editor h2 {\n  font-size: 1.5em;\n}\n.ql-bubble .ql-editor h3 {\n  font-size: 1.17em;\n}\n.ql-bubble .ql-editor h4 {\n  font-size: 1em;\n}\n.ql-bubble .ql-editor h5 {\n  font-size: 0.83em;\n}\n.ql-bubble .ql-editor h6 {\n  font-size: 0.67em;\n}\n.ql-bubble .ql-editor a {\n  text-decoration: underline;\n}\n.ql-bubble .ql-editor blockquote {\n  border-left: 4px solid #ccc;\n  margin-bottom: 5px;\n  margin-top: 5px;\n  padding-left: 16px;\n}\n.ql-bubble .ql-editor code,\n.ql-bubble .ql-editor pre {\n  background-color: #f0f0f0;\n  border-radius: 3px;\n}\n.ql-bubble .ql-editor pre {\n  white-space: pre-wrap;\n  margin-bottom: 5px;\n  margin-top: 5px;\n  padding: 5px 10px;\n}\n.ql-bubble .ql-editor code {\n  font-size: 85%;\n  padding: 2px 4px;\n}\n.ql-bubble .ql-editor pre.ql-syntax {\n  background-color: #23241f;\n  color: #f8f8f2;\n  overflow: visible;\n}\n.ql-bubble .ql-editor img {\n  max-width: 100%;\n}\n.ql-bubble .ql-picker {\n  color: #ccc;\n  display: inline-block;\n  float: left;\n  font-size: 14px;\n  font-weight: 500;\n  height: 24px;\n  position: relative;\n  vertical-align: middle;\n}\n.ql-bubble .ql-picker-label {\n  cursor: pointer;\n  display: inline-block;\n  height: 100%;\n  padding-left: 8px;\n  padding-right: 2px;\n  position: relative;\n  width: 100%;\n}\n.ql-bubble .ql-picker-label::before {\n  display: inline-block;\n  line-height: 22px;\n}\n.ql-bubble .ql-picker-options {\n  background-color: #444;\n  display: none;\n  min-width: 100%;\n  padding: 4px 8px;\n  position: absolute;\n  white-space: nowrap;\n}\n.ql-bubble .ql-picker-options .ql-picker-item {\n  cursor: pointer;\n  display: block;\n  padding-bottom: 5px;\n  padding-top: 5px;\n}\n.ql-bubble .ql-picker.ql-expanded .ql-picker-label {\n  color: #777;\n  z-index: 2;\n}\n.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill {\n  fill: #777;\n}\n.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke {\n  stroke: #777;\n}\n.ql-bubble .ql-picker.ql-expanded .ql-picker-options {\n  display: block;\n  margin-top: -1px;\n  top: 100%;\n  z-index: 1;\n}\n.ql-bubble .ql-color-picker,\n.ql-bubble .ql-icon-picker {\n  width: 28px;\n}\n.ql-bubble .ql-color-picker .ql-picker-label,\n.ql-bubble .ql-icon-picker .ql-picker-label {\n  padding: 2px 4px;\n}\n.ql-bubble .ql-color-picker .ql-picker-label svg,\n.ql-bubble .ql-icon-picker .ql-picker-label svg {\n  right: 4px;\n}\n.ql-bubble .ql-icon-picker .ql-picker-options {\n  padding: 4px 0px;\n}\n.ql-bubble .ql-icon-picker .ql-picker-item {\n  height: 24px;\n  width: 24px;\n  padding: 2px 4px;\n}\n.ql-bubble .ql-color-picker .ql-picker-options {\n  padding: 3px 5px;\n  width: 152px;\n}\n.ql-bubble .ql-color-picker .ql-picker-item {\n  border: 1px solid transparent;\n  float: left;\n  height: 16px;\n  margin: 2px;\n  padding: 0px;\n  width: 16px;\n}\n.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {\n  position: absolute;\n  margin-top: -9px;\n  right: 0;\n  top: 50%;\n  width: 18px;\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,\n.ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,\n.ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,\n.ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,\n.ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {\n  content: attr(data-label);\n}\n.ql-bubble .ql-picker.ql-header {\n  width: 98px;\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item::before {\n  content: 'Normal';\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value=\"1\"]::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"1\"]::before {\n  content: 'Heading 1';\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value=\"2\"]::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"2\"]::before {\n  content: 'Heading 2';\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value=\"3\"]::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"3\"]::before {\n  content: 'Heading 3';\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value=\"4\"]::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"4\"]::before {\n  content: 'Heading 4';\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value=\"5\"]::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"5\"]::before {\n  content: 'Heading 5';\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value=\"6\"]::before,\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"6\"]::before {\n  content: 'Heading 6';\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"1\"]::before {\n  font-size: 2em;\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"2\"]::before {\n  font-size: 1.5em;\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"3\"]::before {\n  font-size: 1.17em;\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"4\"]::before {\n  font-size: 1em;\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"5\"]::before {\n  font-size: 0.83em;\n}\n.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value=\"6\"]::before {\n  font-size: 0.67em;\n}\n.ql-bubble .ql-picker.ql-font {\n  width: 108px;\n}\n.ql-bubble .ql-picker.ql-font .ql-picker-label::before,\n.ql-bubble .ql-picker.ql-font .ql-picker-item::before {\n  content: 'Sans Serif';\n}\n.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,\n.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {\n  content: 'Serif';\n}\n.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,\n.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {\n  content: 'Monospace';\n}\n.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {\n  font-family: Georgia, Times New Roman, serif;\n}\n.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {\n  font-family: Monaco, Courier New, monospace;\n}\n.ql-bubble .ql-picker.ql-size {\n  width: 98px;\n}\n.ql-bubble .ql-picker.ql-size .ql-picker-label::before,\n.ql-bubble .ql-picker.ql-size .ql-picker-item::before {\n  content: 'Normal';\n}\n.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before,\n.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {\n  content: 'Small';\n}\n.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before,\n.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {\n  content: 'Large';\n}\n.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,\n.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {\n  content: 'Huge';\n}\n.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before {\n  font-size: 10px;\n}\n.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before {\n  font-size: 18px;\n}\n.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {\n  font-size: 32px;\n}\n.ql-bubble .ql-color-picker.ql-background .ql-picker-item {\n  background-color: #fff;\n}\n.ql-bubble .ql-color-picker.ql-color .ql-picker-item {\n  background-color: #000;\n}\n.ql-bubble .ql-toolbar .ql-formats {\n  margin: 8px 12px 8px 0px;\n}\n.ql-bubble .ql-toolbar .ql-formats:first-child {\n  margin-left: 12px;\n}\n.ql-bubble .ql-color-picker svg {\n  margin: 1px;\n}\n.ql-bubble .ql-color-picker .ql-picker-item.ql-selected,\n.ql-bubble .ql-color-picker .ql-picker-item:hover {\n  border-color: #fff;\n}\n.ql-bubble .ql-tooltip {\n  background-color: #444;\n  border-radius: 25px;\n  color: #fff;\n}\n.ql-bubble .ql-tooltip-arrow {\n  border-left: 6px solid transparent;\n  border-right: 6px solid transparent;\n  content: \" \";\n  display: block;\n  left: 50%;\n  margin-left: -6px;\n  position: absolute;\n}\n.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow {\n  border-bottom: 6px solid #444;\n  top: -6px;\n}\n.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow {\n  border-top: 6px solid #444;\n  bottom: -6px;\n}\n.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor {\n  display: block;\n}\n.ql-bubble .ql-tooltip.ql-editing .ql-formats {\n  visibility: hidden;\n}\n.ql-bubble .ql-tooltip-editor {\n  display: none;\n}\n.ql-bubble .ql-tooltip-editor input[type=text] {\n  background: transparent;\n  border: none;\n  color: #fff;\n  font-size: 13px;\n  height: 100%;\n  outline: none;\n  padding: 10px 20px;\n  position: absolute;\n  width: 100%;\n}\n.ql-bubble .ql-tooltip-editor a {\n  top: 10px;\n  position: absolute;\n  right: 20px;\n}\n.ql-bubble .ql-tooltip-editor a:before {\n  color: #ccc;\n  content: \"\\D7\";\n  font-size: 16px;\n  font-weight: bold;\n}\n.ql-container.ql-bubble:not(.ql-disabled) a {\n  position: relative;\n  white-space: nowrap;\n}\n.ql-container.ql-bubble:not(.ql-disabled) a::before {\n  background-color: #444;\n  border-radius: 15px;\n  top: -5px;\n  font-size: 12px;\n  color: #fff;\n  content: attr(href);\n  font-weight: normal;\n  overflow: hidden;\n  padding: 5px 15px;\n  text-decoration: none;\n  z-index: 1;\n}\n.ql-container.ql-bubble:not(.ql-disabled) a::after {\n  border-top: 6px solid #444;\n  border-left: 6px solid transparent;\n  border-right: 6px solid transparent;\n  top: 0;\n  content: \" \";\n  height: 0;\n  width: 0;\n}\n.ql-container.ql-bubble:not(.ql-disabled) a::before,\n.ql-container.ql-bubble:not(.ql-disabled) a::after {\n  left: 0;\n  margin-left: 50%;\n  position: absolute;\n  transform: translate(-50%, -100%);\n  transition: visibility 0s ease 200ms;\n  visibility: hidden;\n}\n.ql-container.ql-bubble:not(.ql-disabled) a:hover::before,\n.ql-container.ql-bubble:not(.ql-disabled) a:hover::after {\n  visibility: visible;\n}\n"
  },
  {
    "path": "assets/vendor/quill/quill.js",
    "content": "/*! For license information please see quill.js.LICENSE.txt */\n!function(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports.Quill=e():t.Quill=e()}(self,(function(){return function(){var t={9698:function(t,e,n){\"use strict\";n.d(e,{Ay:function(){return c},Ji:function(){return d},mG:function(){return h},zo:function(){return u}});var r=n(6003),i=n(5232),s=n.n(i),o=n(3036),l=n(4850),a=n(5508);class c extends r.BlockBlot{cache={};delta(){return null==this.cache.delta&&(this.cache.delta=h(this)),this.cache.delta}deleteAt(t,e){super.deleteAt(t,e),this.cache={}}formatAt(t,e,n,i){e<=0||(this.scroll.query(n,r.Scope.BLOCK)?t+e===this.length()&&this.format(n,i):super.formatAt(t,Math.min(e,this.length()-t-1),n,i),this.cache={})}insertAt(t,e,n){if(null!=n)return super.insertAt(t,e,n),void(this.cache={});if(0===e.length)return;const r=e.split(\"\\n\"),i=r.shift();i.length>0&&(t<this.length()-1||null==this.children.tail?super.insertAt(Math.min(t,this.length()-1),i):this.children.tail.insertAt(this.children.tail.length(),i),this.cache={});let s=this;r.reduce(((t,e)=>(s=s.split(t,!0),s.insertAt(0,e),e.length)),t+i.length)}insertBefore(t,e){const{head:n}=this.children;super.insertBefore(t,e),n instanceof o.A&&n.remove(),this.cache={}}length(){return null==this.cache.length&&(this.cache.length=super.length()+1),this.cache.length}moveChildren(t,e){super.moveChildren(t,e),this.cache={}}optimize(t){super.optimize(t),this.cache={}}path(t){return super.path(t,!0)}removeChild(t){super.removeChild(t),this.cache={}}split(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(e&&(0===t||t>=this.length()-1)){const e=this.clone();return 0===t?(this.parent.insertBefore(e,this),this):(this.parent.insertBefore(e,this.next),e)}const n=super.split(t,e);return this.cache={},n}}c.blotName=\"block\",c.tagName=\"P\",c.defaultChild=o.A,c.allowedChildren=[o.A,l.A,r.EmbedBlot,a.A];class u extends r.EmbedBlot{attach(){super.attach(),this.attributes=new r.AttributorStore(this.domNode)}delta(){return(new(s())).insert(this.value(),{...this.formats(),...this.attributes.values()})}format(t,e){const n=this.scroll.query(t,r.Scope.BLOCK_ATTRIBUTE);null!=n&&this.attributes.attribute(n,e)}formatAt(t,e,n,r){this.format(n,r)}insertAt(t,e,n){if(null!=n)return void super.insertAt(t,e,n);const r=e.split(\"\\n\"),i=r.pop(),s=r.map((t=>{const e=this.scroll.create(c.blotName);return e.insertAt(0,t),e})),o=this.split(t);s.forEach((t=>{this.parent.insertBefore(t,o)})),i&&this.parent.insertBefore(this.scroll.create(\"text\",i),o)}}function h(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return t.descendants(r.LeafBlot).reduce(((t,n)=>0===n.length()?t:t.insert(n.value(),d(n,{},e))),new(s())).insert(\"\\n\",d(t))}function d(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];return null==t?e:(\"formats\"in t&&\"function\"==typeof t.formats&&(e={...e,...t.formats()},n&&delete e[\"code-token\"]),null==t.parent||\"scroll\"===t.parent.statics.blotName||t.parent.statics.scope!==t.statics.scope?e:d(t.parent,e,n))}u.scope=r.Scope.BLOCK_BLOT},3036:function(t,e,n){\"use strict\";var r=n(6003);class i extends r.EmbedBlot{static value(){}optimize(){(this.prev||this.next)&&this.remove()}length(){return 0}value(){return\"\"}}i.blotName=\"break\",i.tagName=\"BR\",e.A=i},580:function(t,e,n){\"use strict\";var r=n(6003);class i extends r.ContainerBlot{}e.A=i},4541:function(t,e,n){\"use strict\";var r=n(6003),i=n(5508);class s extends r.EmbedBlot{static blotName=\"cursor\";static className=\"ql-cursor\";static tagName=\"span\";static CONTENTS=\"\\ufeff\";static value(){}constructor(t,e,n){super(t,e),this.selection=n,this.textNode=document.createTextNode(s.CONTENTS),this.domNode.appendChild(this.textNode),this.savedLength=0}detach(){null!=this.parent&&this.parent.removeChild(this)}format(t,e){if(0!==this.savedLength)return void super.format(t,e);let n=this,i=0;for(;null!=n&&n.statics.scope!==r.Scope.BLOCK_BLOT;)i+=n.offset(n.parent),n=n.parent;null!=n&&(this.savedLength=s.CONTENTS.length,n.optimize(),n.formatAt(i,s.CONTENTS.length,t,e),this.savedLength=0)}index(t,e){return t===this.textNode?0:super.index(t,e)}length(){return this.savedLength}position(){return[this.textNode,this.textNode.data.length]}remove(){super.remove(),this.parent=null}restore(){if(this.selection.composing||null==this.parent)return null;const t=this.selection.getNativeRange();for(;null!=this.domNode.lastChild&&this.domNode.lastChild!==this.textNode;)this.domNode.parentNode.insertBefore(this.domNode.lastChild,this.domNode);const e=this.prev instanceof i.A?this.prev:null,n=e?e.length():0,r=this.next instanceof i.A?this.next:null,o=r?r.text:\"\",{textNode:l}=this,a=l.data.split(s.CONTENTS).join(\"\");let c;if(l.data=s.CONTENTS,e)c=e,(a||r)&&(e.insertAt(e.length(),a+o),r&&r.remove());else if(r)c=r,r.insertAt(0,a);else{const t=document.createTextNode(a);c=this.scroll.create(t),this.parent.insertBefore(c,this)}if(this.remove(),t){const i=(t,i)=>e&&t===e.domNode?i:t===l?n+i-1:r&&t===r.domNode?n+a.length+i:null,s=i(t.start.node,t.start.offset),o=i(t.end.node,t.end.offset);if(null!==s&&null!==o)return{startNode:c.domNode,startOffset:s,endNode:c.domNode,endOffset:o}}return null}update(t,e){if(t.some((t=>\"characterData\"===t.type&&t.target===this.textNode))){const t=this.restore();t&&(e.range=t)}}optimize(t){super.optimize(t);let{parent:e}=this;for(;e;){if(\"A\"===e.domNode.tagName){this.savedLength=s.CONTENTS.length,e.isolate(this.offset(e),this.length()).unwrap(),this.savedLength=0;break}e=e.parent}}value(){return\"\"}}e.A=s},746:function(t,e,n){\"use strict\";var r=n(6003),i=n(5508);const s=\"\\ufeff\";class o extends r.EmbedBlot{constructor(t,e){super(t,e),this.contentNode=document.createElement(\"span\"),this.contentNode.setAttribute(\"contenteditable\",\"false\"),Array.from(this.domNode.childNodes).forEach((t=>{this.contentNode.appendChild(t)})),this.leftGuard=document.createTextNode(s),this.rightGuard=document.createTextNode(s),this.domNode.appendChild(this.leftGuard),this.domNode.appendChild(this.contentNode),this.domNode.appendChild(this.rightGuard)}index(t,e){return t===this.leftGuard?0:t===this.rightGuard?1:super.index(t,e)}restore(t){let e,n=null;const r=t.data.split(s).join(\"\");if(t===this.leftGuard)if(this.prev instanceof i.A){const t=this.prev.length();this.prev.insertAt(t,r),n={startNode:this.prev.domNode,startOffset:t+r.length}}else e=document.createTextNode(r),this.parent.insertBefore(this.scroll.create(e),this),n={startNode:e,startOffset:r.length};else t===this.rightGuard&&(this.next instanceof i.A?(this.next.insertAt(0,r),n={startNode:this.next.domNode,startOffset:r.length}):(e=document.createTextNode(r),this.parent.insertBefore(this.scroll.create(e),this.next),n={startNode:e,startOffset:r.length}));return t.data=s,n}update(t,e){t.forEach((t=>{if(\"characterData\"===t.type&&(t.target===this.leftGuard||t.target===this.rightGuard)){const n=this.restore(t.target);n&&(e.range=n)}}))}}e.A=o},4850:function(t,e,n){\"use strict\";var r=n(6003),i=n(3036),s=n(5508);class o extends r.InlineBlot{static allowedChildren=[o,i.A,r.EmbedBlot,s.A];static order=[\"cursor\",\"inline\",\"link\",\"underline\",\"strike\",\"italic\",\"bold\",\"script\",\"code\"];static compare(t,e){const n=o.order.indexOf(t),r=o.order.indexOf(e);return n>=0||r>=0?n-r:t===e?0:t<e?-1:1}formatAt(t,e,n,i){if(o.compare(this.statics.blotName,n)<0&&this.scroll.query(n,r.Scope.BLOT)){const r=this.isolate(t,e);i&&r.wrap(n,i)}else super.formatAt(t,e,n,i)}optimize(t){if(super.optimize(t),this.parent instanceof o&&o.compare(this.statics.blotName,this.parent.statics.blotName)>0){const t=this.parent.isolate(this.offset(),this.length());this.moveChildren(t),t.wrap(this)}}}e.A=o},5508:function(t,e,n){\"use strict\";n.d(e,{A:function(){return i},X:function(){return s}});var r=n(6003);class i extends r.TextBlot{}function s(t){return t.replace(/[&<>\"']/g,(t=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\"}[t])))}},3729:function(t,e,n){\"use strict\";n.d(e,{default:function(){return R}});var r=n(6142),i=n(9698),s=n(3036),o=n(580),l=n(4541),a=n(746),c=n(4850),u=n(6003),h=n(5232),d=n.n(h),f=n(5374);function p(t){return t instanceof i.Ay||t instanceof i.zo}function g(t){return\"function\"==typeof t.updateContent}class m extends u.ScrollBlot{static blotName=\"scroll\";static className=\"ql-editor\";static tagName=\"DIV\";static defaultChild=i.Ay;static allowedChildren=[i.Ay,i.zo,o.A];constructor(t,e,n){let{emitter:r}=n;super(t,e),this.emitter=r,this.batch=!1,this.optimize(),this.enable(),this.domNode.addEventListener(\"dragstart\",(t=>this.handleDragStart(t)))}batchStart(){Array.isArray(this.batch)||(this.batch=[])}batchEnd(){if(!this.batch)return;const t=this.batch;this.batch=!1,this.update(t)}emitMount(t){this.emitter.emit(f.A.events.SCROLL_BLOT_MOUNT,t)}emitUnmount(t){this.emitter.emit(f.A.events.SCROLL_BLOT_UNMOUNT,t)}emitEmbedUpdate(t,e){this.emitter.emit(f.A.events.SCROLL_EMBED_UPDATE,t,e)}deleteAt(t,e){const[n,r]=this.line(t),[o]=this.line(t+e);if(super.deleteAt(t,e),null!=o&&n!==o&&r>0){if(n instanceof i.zo||o instanceof i.zo)return void this.optimize();const t=o.children.head instanceof s.A?null:o.children.head;n.moveChildren(o,t),n.remove()}this.optimize()}enable(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.domNode.setAttribute(\"contenteditable\",t?\"true\":\"false\")}formatAt(t,e,n,r){super.formatAt(t,e,n,r),this.optimize()}insertAt(t,e,n){if(t>=this.length())if(null==n||null==this.scroll.query(e,u.Scope.BLOCK)){const t=this.scroll.create(this.statics.defaultChild.blotName);this.appendChild(t),null==n&&e.endsWith(\"\\n\")?t.insertAt(0,e.slice(0,-1),n):t.insertAt(0,e,n)}else{const t=this.scroll.create(e,n);this.appendChild(t)}else super.insertAt(t,e,n);this.optimize()}insertBefore(t,e){if(t.statics.scope===u.Scope.INLINE_BLOT){const n=this.scroll.create(this.statics.defaultChild.blotName);n.appendChild(t),super.insertBefore(n,e)}else super.insertBefore(t,e)}insertContents(t,e){const n=this.deltaToRenderBlocks(e.concat((new(d())).insert(\"\\n\"))),r=n.pop();if(null==r)return;this.batchStart();const s=n.shift();if(s){const e=\"block\"===s.type&&(0===s.delta.length()||!this.descendant(i.zo,t)[0]&&t<this.length()),n=\"block\"===s.type?s.delta:(new(d())).insert({[s.key]:s.value});b(this,t,n);const r=\"block\"===s.type?1:0,o=t+n.length()+r;e&&this.insertAt(o-1,\"\\n\");const l=(0,i.Ji)(this.line(t)[0]),a=h.AttributeMap.diff(l,s.attributes)||{};Object.keys(a).forEach((t=>{this.formatAt(o-1,1,t,a[t])})),t=o}let[o,l]=this.children.find(t);n.length&&(o&&(o=o.split(l),l=0),n.forEach((t=>{if(\"block\"===t.type)b(this.createBlock(t.attributes,o||void 0),0,t.delta);else{const e=this.create(t.key,t.value);this.insertBefore(e,o||void 0),Object.keys(t.attributes).forEach((n=>{e.format(n,t.attributes[n])}))}}))),\"block\"===r.type&&r.delta.length()&&b(this,o?o.offset(o.scroll)+l:this.length(),r.delta),this.batchEnd(),this.optimize()}isEnabled(){return\"true\"===this.domNode.getAttribute(\"contenteditable\")}leaf(t){const e=this.path(t).pop();if(!e)return[null,-1];const[n,r]=e;return n instanceof u.LeafBlot?[n,r]:[null,-1]}line(t){return t===this.length()?this.line(t-1):this.descendant(p,t)}lines(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Number.MAX_VALUE;const n=(t,e,r)=>{let i=[],s=r;return t.children.forEachAt(e,r,((t,e,r)=>{p(t)?i.push(t):t instanceof u.ContainerBlot&&(i=i.concat(n(t,e,s))),s-=r})),i};return n(this,t,e)}optimize(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.batch||(super.optimize(t,e),t.length>0&&this.emitter.emit(f.A.events.SCROLL_OPTIMIZE,t,e))}path(t){return super.path(t).slice(1)}remove(){}update(t){if(this.batch)return void(Array.isArray(t)&&(this.batch=this.batch.concat(t)));let e=f.A.sources.USER;\"string\"==typeof t&&(e=t),Array.isArray(t)||(t=this.observer.takeRecords()),(t=t.filter((t=>{let{target:e}=t;const n=this.find(e,!0);return n&&!g(n)}))).length>0&&this.emitter.emit(f.A.events.SCROLL_BEFORE_UPDATE,e,t),super.update(t.concat([])),t.length>0&&this.emitter.emit(f.A.events.SCROLL_UPDATE,e,t)}updateEmbedAt(t,e,n){const[r]=this.descendant((t=>t instanceof i.zo),t);r&&r.statics.blotName===e&&g(r)&&r.updateContent(n)}handleDragStart(t){t.preventDefault()}deltaToRenderBlocks(t){const e=[];let n=new(d());return t.forEach((t=>{const r=t?.insert;if(r)if(\"string\"==typeof r){const i=r.split(\"\\n\");i.slice(0,-1).forEach((r=>{n.insert(r,t.attributes),e.push({type:\"block\",delta:n,attributes:t.attributes??{}}),n=new(d())}));const s=i[i.length-1];s&&n.insert(s,t.attributes)}else{const i=Object.keys(r)[0];if(!i)return;this.query(i,u.Scope.INLINE)?n.push(t):(n.length()&&e.push({type:\"block\",delta:n,attributes:{}}),n=new(d()),e.push({type:\"blockEmbed\",key:i,value:r[i],attributes:t.attributes??{}}))}})),n.length()&&e.push({type:\"block\",delta:n,attributes:{}}),e}createBlock(t,e){let n;const r={};Object.entries(t).forEach((t=>{let[e,i]=t;null!=this.query(e,u.Scope.BLOCK&u.Scope.BLOT)?n=e:r[e]=i}));const i=this.create(n||this.statics.defaultChild.blotName,n?t[n]:void 0);this.insertBefore(i,e||void 0);const s=i.length();return Object.entries(r).forEach((t=>{let[e,n]=t;i.formatAt(0,s,e,n)})),i}}function b(t,e,n){n.reduce(((e,n)=>{const r=h.Op.length(n);let s=n.attributes||{};if(null!=n.insert)if(\"string\"==typeof n.insert){const r=n.insert;t.insertAt(e,r);const[o]=t.descendant(u.LeafBlot,e),l=(0,i.Ji)(o);s=h.AttributeMap.diff(l,s)||{}}else if(\"object\"==typeof n.insert){const r=Object.keys(n.insert)[0];if(null==r)return e;if(t.insertAt(e,r,n.insert[r]),null!=t.scroll.query(r,u.Scope.INLINE)){const[n]=t.descendant(u.LeafBlot,e),r=(0,i.Ji)(n);s=h.AttributeMap.diff(r,s)||{}}}return Object.keys(s).forEach((n=>{t.formatAt(e,r,n,s[n])})),e+r}),e)}var y=m,v=n(5508),A=n(584),x=n(4266);class N extends x.A{static DEFAULTS={delay:1e3,maxStack:100,userOnly:!1};lastRecorded=0;ignoreChange=!1;stack={undo:[],redo:[]};currentRange=null;constructor(t,e){super(t,e),this.quill.on(r.Ay.events.EDITOR_CHANGE,((t,e,n,i)=>{t===r.Ay.events.SELECTION_CHANGE?e&&i!==r.Ay.sources.SILENT&&(this.currentRange=e):t===r.Ay.events.TEXT_CHANGE&&(this.ignoreChange||(this.options.userOnly&&i!==r.Ay.sources.USER?this.transform(e):this.record(e,n)),this.currentRange=w(this.currentRange,e))})),this.quill.keyboard.addBinding({key:\"z\",shortKey:!0},this.undo.bind(this)),this.quill.keyboard.addBinding({key:[\"z\",\"Z\"],shortKey:!0,shiftKey:!0},this.redo.bind(this)),/Win/i.test(navigator.platform)&&this.quill.keyboard.addBinding({key:\"y\",shortKey:!0},this.redo.bind(this)),this.quill.root.addEventListener(\"beforeinput\",(t=>{\"historyUndo\"===t.inputType?(this.undo(),t.preventDefault()):\"historyRedo\"===t.inputType&&(this.redo(),t.preventDefault())}))}change(t,e){if(0===this.stack[t].length)return;const n=this.stack[t].pop();if(!n)return;const i=this.quill.getContents(),s=n.delta.invert(i);this.stack[e].push({delta:s,range:w(n.range,s)}),this.lastRecorded=0,this.ignoreChange=!0,this.quill.updateContents(n.delta,r.Ay.sources.USER),this.ignoreChange=!1,this.restoreSelection(n)}clear(){this.stack={undo:[],redo:[]}}cutoff(){this.lastRecorded=0}record(t,e){if(0===t.ops.length)return;this.stack.redo=[];let n=t.invert(e),r=this.currentRange;const i=Date.now();if(this.lastRecorded+this.options.delay>i&&this.stack.undo.length>0){const t=this.stack.undo.pop();t&&(n=n.compose(t.delta),r=t.range)}else this.lastRecorded=i;0!==n.length()&&(this.stack.undo.push({delta:n,range:r}),this.stack.undo.length>this.options.maxStack&&this.stack.undo.shift())}redo(){this.change(\"redo\",\"undo\")}transform(t){E(this.stack.undo,t),E(this.stack.redo,t)}undo(){this.change(\"undo\",\"redo\")}restoreSelection(t){if(t.range)this.quill.setSelection(t.range,r.Ay.sources.USER);else{const e=function(t,e){const n=e.reduce(((t,e)=>t+(e.delete||0)),0);let r=e.length()-n;return function(t,e){const n=e.ops[e.ops.length-1];return null!=n&&(null!=n.insert?\"string\"==typeof n.insert&&n.insert.endsWith(\"\\n\"):null!=n.attributes&&Object.keys(n.attributes).some((e=>null!=t.query(e,u.Scope.BLOCK))))}(t,e)&&(r-=1),r}(this.quill.scroll,t.delta);this.quill.setSelection(e,r.Ay.sources.USER)}}}function E(t,e){let n=e;for(let e=t.length-1;e>=0;e-=1){const r=t[e];t[e]={delta:n.transform(r.delta,!0),range:r.range&&w(r.range,n)},n=r.delta.transform(n),0===t[e].delta.length()&&t.splice(e,1)}}function w(t,e){if(!t)return t;const n=e.transformPosition(t.index);return{index:n,length:e.transformPosition(t.index+t.length)-n}}var q=n(8123);class k extends x.A{constructor(t,e){super(t,e),t.root.addEventListener(\"drop\",(e=>{e.preventDefault();let n=null;if(document.caretRangeFromPoint)n=document.caretRangeFromPoint(e.clientX,e.clientY);else if(document.caretPositionFromPoint){const t=document.caretPositionFromPoint(e.clientX,e.clientY);n=document.createRange(),n.setStart(t.offsetNode,t.offset),n.setEnd(t.offsetNode,t.offset)}const r=n&&t.selection.normalizeNative(n);if(r){const n=t.selection.normalizedToRange(r);e.dataTransfer?.files&&this.upload(n,e.dataTransfer.files)}}))}upload(t,e){const n=[];Array.from(e).forEach((t=>{t&&this.options.mimetypes?.includes(t.type)&&n.push(t)})),n.length>0&&this.options.handler.call(this,t,n)}}k.DEFAULTS={mimetypes:[\"image/png\",\"image/jpeg\"],handler(t,e){if(!this.quill.scroll.query(\"image\"))return;const n=e.map((t=>new Promise((e=>{const n=new FileReader;n.onload=()=>{e(n.result)},n.readAsDataURL(t)}))));Promise.all(n).then((e=>{const n=e.reduce(((t,e)=>t.insert({image:e})),(new(d())).retain(t.index).delete(t.length));this.quill.updateContents(n,f.A.sources.USER),this.quill.setSelection(t.index+e.length,f.A.sources.SILENT)}))}};var _=k;const L=[\"insertText\",\"insertReplacementText\"];class S extends x.A{constructor(t,e){super(t,e),t.root.addEventListener(\"beforeinput\",(t=>{this.handleBeforeInput(t)})),/Android/i.test(navigator.userAgent)||t.on(r.Ay.events.COMPOSITION_BEFORE_START,(()=>{this.handleCompositionStart()}))}deleteRange(t){(0,q.Xo)({range:t,quill:this.quill})}replaceText(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:\"\";if(0===t.length)return!1;if(e){const n=this.quill.getFormat(t.index,1);this.deleteRange(t),this.quill.updateContents((new(d())).retain(t.index).insert(e,n),r.Ay.sources.USER)}else this.deleteRange(t);return this.quill.setSelection(t.index+e.length,0,r.Ay.sources.SILENT),!0}handleBeforeInput(t){if(this.quill.composition.isComposing||t.defaultPrevented||!L.includes(t.inputType))return;const e=t.getTargetRanges?t.getTargetRanges()[0]:null;if(!e||!0===e.collapsed)return;const n=function(t){return\"string\"==typeof t.data?t.data:t.dataTransfer?.types.includes(\"text/plain\")?t.dataTransfer.getData(\"text/plain\"):null}(t);if(null==n)return;const r=this.quill.selection.normalizeNative(e),i=r?this.quill.selection.normalizedToRange(r):null;i&&this.replaceText(i,n)&&t.preventDefault()}handleCompositionStart(){const t=this.quill.getSelection();t&&this.replaceText(t)}}var O=S;const T=/Mac/i.test(navigator.platform);class j extends x.A{isListening=!1;selectionChangeDeadline=0;constructor(t,e){super(t,e),this.handleArrowKeys(),this.handleNavigationShortcuts()}handleArrowKeys(){this.quill.keyboard.addBinding({key:[\"ArrowLeft\",\"ArrowRight\"],offset:0,shiftKey:null,handler(t,e){let{line:n,event:i}=e;if(!(n instanceof u.ParentBlot&&n.uiNode))return!0;const s=\"rtl\"===getComputedStyle(n.domNode).direction;return!!(s&&\"ArrowRight\"!==i.key||!s&&\"ArrowLeft\"!==i.key)||(this.quill.setSelection(t.index-1,t.length+(i.shiftKey?1:0),r.Ay.sources.USER),!1)}})}handleNavigationShortcuts(){this.quill.root.addEventListener(\"keydown\",(t=>{!t.defaultPrevented&&(t=>\"ArrowLeft\"===t.key||\"ArrowRight\"===t.key||\"ArrowUp\"===t.key||\"ArrowDown\"===t.key||\"Home\"===t.key||!(!T||\"a\"!==t.key||!0!==t.ctrlKey))(t)&&this.ensureListeningToSelectionChange()}))}ensureListeningToSelectionChange(){this.selectionChangeDeadline=Date.now()+100,this.isListening||(this.isListening=!0,document.addEventListener(\"selectionchange\",(()=>{this.isListening=!1,Date.now()<=this.selectionChangeDeadline&&this.handleSelectionChange()}),{once:!0}))}handleSelectionChange(){const t=document.getSelection();if(!t)return;const e=t.getRangeAt(0);if(!0!==e.collapsed||0!==e.startOffset)return;const n=this.quill.scroll.find(e.startContainer);if(!(n instanceof u.ParentBlot&&n.uiNode))return;const r=document.createRange();r.setStartAfter(n.uiNode),r.setEndAfter(n.uiNode),t.removeAllRanges(),t.addRange(r)}}var C=j;r.Ay.register({\"blots/block\":i.Ay,\"blots/block/embed\":i.zo,\"blots/break\":s.A,\"blots/container\":o.A,\"blots/cursor\":l.A,\"blots/embed\":a.A,\"blots/inline\":c.A,\"blots/scroll\":y,\"blots/text\":v.A,\"modules/clipboard\":A.Ay,\"modules/history\":N,\"modules/keyboard\":q.Ay,\"modules/uploader\":_,\"modules/input\":O,\"modules/uiNode\":C});var R=r.Ay},5374:function(t,e,n){\"use strict\";n.d(e,{A:function(){return o}});var r=n(8920),i=n(7356);const s=(0,n(6078).A)(\"quill:events\");[\"selectionchange\",\"mousedown\",\"mouseup\",\"click\"].forEach((t=>{document.addEventListener(t,(function(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];Array.from(document.querySelectorAll(\".ql-container\")).forEach((t=>{const n=i.A.get(t);n&&n.emitter&&n.emitter.handleDOM(...e)}))}))}));var o=class extends r{static events={EDITOR_CHANGE:\"editor-change\",SCROLL_BEFORE_UPDATE:\"scroll-before-update\",SCROLL_BLOT_MOUNT:\"scroll-blot-mount\",SCROLL_BLOT_UNMOUNT:\"scroll-blot-unmount\",SCROLL_OPTIMIZE:\"scroll-optimize\",SCROLL_UPDATE:\"scroll-update\",SCROLL_EMBED_UPDATE:\"scroll-embed-update\",SELECTION_CHANGE:\"selection-change\",TEXT_CHANGE:\"text-change\",COMPOSITION_BEFORE_START:\"composition-before-start\",COMPOSITION_START:\"composition-start\",COMPOSITION_BEFORE_END:\"composition-before-end\",COMPOSITION_END:\"composition-end\"};static sources={API:\"api\",SILENT:\"silent\",USER:\"user\"};constructor(){super(),this.domListeners={},this.on(\"error\",s.error)}emit(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return s.log.call(s,...e),super.emit(...e)}handleDOM(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];(this.domListeners[t.type]||[]).forEach((e=>{let{node:r,handler:i}=e;(t.target===r||r.contains(t.target))&&i(t,...n)}))}listenDOM(t,e,n){this.domListeners[t]||(this.domListeners[t]=[]),this.domListeners[t].push({node:e,handler:n})}}},7356:function(t,e){\"use strict\";e.A=new WeakMap},6078:function(t,e){\"use strict\";const n=[\"error\",\"warn\",\"log\",\"info\"];let r=\"warn\";function i(t){if(r&&n.indexOf(t)<=n.indexOf(r)){for(var e=arguments.length,i=new Array(e>1?e-1:0),s=1;s<e;s++)i[s-1]=arguments[s];console[t](...i)}}function s(t){return n.reduce(((e,n)=>(e[n]=i.bind(console,n,t),e)),{})}s.level=t=>{r=t},i.level=s.level,e.A=s},4266:function(t,e){\"use strict\";e.A=class{static DEFAULTS={};constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.quill=t,this.options=e}}},6142:function(t,e,n){\"use strict\";n.d(e,{Ay:function(){return I}});var r=n(8347),i=n(6003),s=n(5232),o=n.n(s),l=n(3707),a=n(5123),c=n(9698),u=n(3036),h=n(4541),d=n(5508),f=n(8298);const p=/^[ -~]*$/;function g(t,e,n){if(0===t.length){const[t]=y(n.pop());return e<=0?`</li></${t}>`:`</li></${t}>${g([],e-1,n)}`}const[{child:r,offset:i,length:s,indent:o,type:l},...a]=t,[c,u]=y(l);if(o>e)return n.push(l),o===e+1?`<${c}><li${u}>${m(r,i,s)}${g(a,o,n)}`:`<${c}><li>${g(t,e+1,n)}`;const h=n[n.length-1];if(o===e&&l===h)return`</li><li${u}>${m(r,i,s)}${g(a,o,n)}`;const[d]=y(n.pop());return`</li></${d}>${g(t,e-1,n)}`}function m(t,e,n){let r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(\"html\"in t&&\"function\"==typeof t.html)return t.html(e,n);if(t instanceof d.A)return(0,d.X)(t.value().slice(e,e+n));if(t instanceof i.ParentBlot){if(\"list-container\"===t.statics.blotName){const r=[];return t.children.forEachAt(e,n,((t,e,n)=>{const i=\"formats\"in t&&\"function\"==typeof t.formats?t.formats():{};r.push({child:t,offset:e,length:n,indent:i.indent||0,type:i.list})})),g(r,-1,[])}const i=[];if(t.children.forEachAt(e,n,((t,e,n)=>{i.push(m(t,e,n))})),r||\"list\"===t.statics.blotName)return i.join(\"\");const{outerHTML:s,innerHTML:o}=t.domNode,[l,a]=s.split(`>${o}<`);return\"<table\"===l?`<table style=\"border: 1px solid #000;\">${i.join(\"\")}<${a}`:`${l}>${i.join(\"\")}<${a}`}return t.domNode instanceof Element?t.domNode.outerHTML:\"\"}function b(t,e){return Object.keys(e).reduce(((n,r)=>{if(null==t[r])return n;const i=e[r];return i===t[r]?n[r]=i:Array.isArray(i)?i.indexOf(t[r])<0?n[r]=i.concat([t[r]]):n[r]=i:n[r]=[i,t[r]],n}),{})}function y(t){const e=\"ordered\"===t?\"ol\":\"ul\";switch(t){case\"checked\":return[e,' data-list=\"checked\"'];case\"unchecked\":return[e,' data-list=\"unchecked\"'];default:return[e,\"\"]}}function v(t){return t.reduce(((t,e)=>{if(\"string\"==typeof e.insert){const n=e.insert.replace(/\\r\\n/g,\"\\n\").replace(/\\r/g,\"\\n\");return t.insert(n,e.attributes)}return t.push(e)}),new(o()))}function A(t,e){let{index:n,length:r}=t;return new f.Q(n+e,r)}var x=class{constructor(t){this.scroll=t,this.delta=this.getDelta()}applyDelta(t){this.scroll.update();let e=this.scroll.length();this.scroll.batchStart();const n=v(t),l=new(o());return function(t){const e=[];return t.forEach((t=>{\"string\"==typeof t.insert?t.insert.split(\"\\n\").forEach(((n,r)=>{r&&e.push({insert:\"\\n\",attributes:t.attributes}),n&&e.push({insert:n,attributes:t.attributes})})):e.push(t)})),e}(n.ops.slice()).reduce(((t,n)=>{const o=s.Op.length(n);let a=n.attributes||{},u=!1,h=!1;if(null!=n.insert){if(l.retain(o),\"string\"==typeof n.insert){const o=n.insert;h=!o.endsWith(\"\\n\")&&(e<=t||!!this.scroll.descendant(c.zo,t)[0]),this.scroll.insertAt(t,o);const[l,u]=this.scroll.line(t);let d=(0,r.A)({},(0,c.Ji)(l));if(l instanceof c.Ay){const[t]=l.descendant(i.LeafBlot,u);t&&(d=(0,r.A)(d,(0,c.Ji)(t)))}a=s.AttributeMap.diff(d,a)||{}}else if(\"object\"==typeof n.insert){const o=Object.keys(n.insert)[0];if(null==o)return t;const l=null!=this.scroll.query(o,i.Scope.INLINE);if(l)(e<=t||this.scroll.descendant(c.zo,t)[0])&&(h=!0);else if(t>0){const[e,n]=this.scroll.descendant(i.LeafBlot,t-1);e instanceof d.A?\"\\n\"!==e.value()[n]&&(u=!0):e instanceof i.EmbedBlot&&e.statics.scope===i.Scope.INLINE_BLOT&&(u=!0)}if(this.scroll.insertAt(t,o,n.insert[o]),l){const[e]=this.scroll.descendant(i.LeafBlot,t);if(e){const t=(0,r.A)({},(0,c.Ji)(e));a=s.AttributeMap.diff(t,a)||{}}}}e+=o}else if(l.push(n),null!==n.retain&&\"object\"==typeof n.retain){const e=Object.keys(n.retain)[0];if(null==e)return t;this.scroll.updateEmbedAt(t,e,n.retain[e])}Object.keys(a).forEach((e=>{this.scroll.formatAt(t,o,e,a[e])}));const f=u?1:0,p=h?1:0;return e+=f+p,l.retain(f),l.delete(p),t+o+f+p}),0),l.reduce(((t,e)=>\"number\"==typeof e.delete?(this.scroll.deleteAt(t,e.delete),t):t+s.Op.length(e)),0),this.scroll.batchEnd(),this.scroll.optimize(),this.update(n)}deleteText(t,e){return this.scroll.deleteAt(t,e),this.update((new(o())).retain(t).delete(e))}formatLine(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};this.scroll.update(),Object.keys(n).forEach((r=>{this.scroll.lines(t,Math.max(e,1)).forEach((t=>{t.format(r,n[r])}))})),this.scroll.optimize();const r=(new(o())).retain(t).retain(e,(0,l.A)(n));return this.update(r)}formatText(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};Object.keys(n).forEach((r=>{this.scroll.formatAt(t,e,r,n[r])}));const r=(new(o())).retain(t).retain(e,(0,l.A)(n));return this.update(r)}getContents(t,e){return this.delta.slice(t,t+e)}getDelta(){return this.scroll.lines().reduce(((t,e)=>t.concat(e.delta())),new(o()))}getFormat(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=[],r=[];0===e?this.scroll.path(t).forEach((t=>{const[e]=t;e instanceof c.Ay?n.push(e):e instanceof i.LeafBlot&&r.push(e)})):(n=this.scroll.lines(t,e),r=this.scroll.descendants(i.LeafBlot,t,e));const[s,o]=[n,r].map((t=>{const e=t.shift();if(null==e)return{};let n=(0,c.Ji)(e);for(;Object.keys(n).length>0;){const e=t.shift();if(null==e)return n;n=b((0,c.Ji)(e),n)}return n}));return{...s,...o}}getHTML(t,e){const[n,r]=this.scroll.line(t);if(n){const i=n.length();return n.length()>=r+e&&(0!==r||e!==i)?m(n,r,e,!0):m(this.scroll,t,e,!0)}return\"\"}getText(t,e){return this.getContents(t,e).filter((t=>\"string\"==typeof t.insert)).map((t=>t.insert)).join(\"\")}insertContents(t,e){const n=v(e),r=(new(o())).retain(t).concat(n);return this.scroll.insertContents(t,n),this.update(r)}insertEmbed(t,e,n){return this.scroll.insertAt(t,e,n),this.update((new(o())).retain(t).insert({[e]:n}))}insertText(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return e=e.replace(/\\r\\n/g,\"\\n\").replace(/\\r/g,\"\\n\"),this.scroll.insertAt(t,e),Object.keys(n).forEach((r=>{this.scroll.formatAt(t,e.length,r,n[r])})),this.update((new(o())).retain(t).insert(e,(0,l.A)(n)))}isBlank(){if(0===this.scroll.children.length)return!0;if(this.scroll.children.length>1)return!1;const t=this.scroll.children.head;if(t?.statics.blotName!==c.Ay.blotName)return!1;const e=t;return!(e.children.length>1)&&e.children.head instanceof u.A}removeFormat(t,e){const n=this.getText(t,e),[r,i]=this.scroll.line(t+e);let s=0,l=new(o());null!=r&&(s=r.length()-i,l=r.delta().slice(i,i+s-1).insert(\"\\n\"));const a=this.getContents(t,e+s).diff((new(o())).insert(n).concat(l)),c=(new(o())).retain(t).concat(a);return this.applyDelta(c)}update(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;const r=this.delta;if(1===e.length&&\"characterData\"===e[0].type&&e[0].target.data.match(p)&&this.scroll.find(e[0].target)){const i=this.scroll.find(e[0].target),s=(0,c.Ji)(i),l=i.offset(this.scroll),a=e[0].oldValue.replace(h.A.CONTENTS,\"\"),u=(new(o())).insert(a),d=(new(o())).insert(i.value()),f=n&&{oldRange:A(n.oldRange,-l),newRange:A(n.newRange,-l)};t=(new(o())).retain(l).concat(u.diff(d,f)).reduce(((t,e)=>e.insert?t.insert(e.insert,s):t.push(e)),new(o())),this.delta=r.compose(t)}else this.delta=this.getDelta(),t&&(0,a.A)(r.compose(t),this.delta)||(t=r.diff(this.delta,n));return t}},N=n(5374),E=n(7356),w=n(6078),q=n(4266),k=n(746),_=class{isComposing=!1;constructor(t,e){this.scroll=t,this.emitter=e,this.setupListeners()}setupListeners(){this.scroll.domNode.addEventListener(\"compositionstart\",(t=>{this.isComposing||this.handleCompositionStart(t)})),this.scroll.domNode.addEventListener(\"compositionend\",(t=>{this.isComposing&&queueMicrotask((()=>{this.handleCompositionEnd(t)}))}))}handleCompositionStart(t){const e=t.target instanceof Node?this.scroll.find(t.target,!0):null;!e||e instanceof k.A||(this.emitter.emit(N.A.events.COMPOSITION_BEFORE_START,t),this.scroll.batchStart(),this.emitter.emit(N.A.events.COMPOSITION_START,t),this.isComposing=!0)}handleCompositionEnd(t){this.emitter.emit(N.A.events.COMPOSITION_BEFORE_END,t),this.scroll.batchEnd(),this.emitter.emit(N.A.events.COMPOSITION_END,t),this.isComposing=!1}},L=n(9609);const S=t=>{const e=t.getBoundingClientRect(),n=\"offsetWidth\"in t&&Math.abs(e.width)/t.offsetWidth||1,r=\"offsetHeight\"in t&&Math.abs(e.height)/t.offsetHeight||1;return{top:e.top,right:e.left+t.clientWidth*n,bottom:e.top+t.clientHeight*r,left:e.left}},O=t=>{const e=parseInt(t,10);return Number.isNaN(e)?0:e},T=(t,e,n,r,i,s)=>t<n&&e>r?0:t<n?-(n-t+i):e>r?e-t>r-n?t+i-n:e-r+s:0;const j=[\"block\",\"break\",\"cursor\",\"inline\",\"scroll\",\"text\"];const C=(0,w.A)(\"quill\"),R=new i.Registry;i.ParentBlot.uiClass=\"ql-ui\";class I{static DEFAULTS={bounds:null,modules:{clipboard:!0,keyboard:!0,history:!0,uploader:!0},placeholder:\"\",readOnly:!1,registry:R,theme:\"default\"};static events=N.A.events;static sources=N.A.sources;static version=\"2.0.2\";static imports={delta:o(),parchment:i,\"core/module\":q.A,\"core/theme\":L.A};static debug(t){!0===t&&(t=\"log\"),w.A.level(t)}static find(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return E.A.get(t)||R.find(t,e)}static import(t){return null==this.imports[t]&&C.error(`Cannot import ${t}. Are you sure it was registered?`),this.imports[t]}static register(){if(\"string\"!=typeof(arguments.length<=0?void 0:arguments[0])){const t=arguments.length<=0?void 0:arguments[0],e=!!(arguments.length<=1?void 0:arguments[1]),n=\"attrName\"in t?t.attrName:t.blotName;\"string\"==typeof n?this.register(`formats/${n}`,t,e):Object.keys(t).forEach((n=>{this.register(n,t[n],e)}))}else{const t=arguments.length<=0?void 0:arguments[0],e=arguments.length<=1?void 0:arguments[1],n=!!(arguments.length<=2?void 0:arguments[2]);null==this.imports[t]||n||C.warn(`Overwriting ${t} with`,e),this.imports[t]=e,(t.startsWith(\"blots/\")||t.startsWith(\"formats/\"))&&e&&\"boolean\"!=typeof e&&\"abstract\"!==e.blotName&&R.register(e),\"function\"==typeof e.register&&e.register(R)}}constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(this.options=function(t,e){const n=B(t);if(!n)throw new Error(\"Invalid Quill container\");const s=!e.theme||e.theme===I.DEFAULTS.theme?L.A:I.import(`themes/${e.theme}`);if(!s)throw new Error(`Invalid theme ${e.theme}. Did you register it?`);const{modules:o,...l}=I.DEFAULTS,{modules:a,...c}=s.DEFAULTS;let u=M(e.modules);null!=u&&u.toolbar&&u.toolbar.constructor!==Object&&(u={...u,toolbar:{container:u.toolbar}});const h=(0,r.A)({},M(o),M(a),u),d={...l,...U(c),...U(e)};let f=e.registry;return f?e.formats&&C.warn('Ignoring \"formats\" option because \"registry\" is specified'):f=e.formats?((t,e,n)=>{const r=new i.Registry;return j.forEach((t=>{const n=e.query(t);n&&r.register(n)})),t.forEach((t=>{let i=e.query(t);i||n.error(`Cannot register \"${t}\" specified in \"formats\" config. Are you sure it was registered?`);let s=0;for(;i;)if(r.register(i),i=\"blotName\"in i?i.requiredContainer??null:null,s+=1,s>100){n.error(`Cycle detected in registering blot requiredContainer: \"${t}\"`);break}})),r})(e.formats,d.registry,C):d.registry,{...d,registry:f,container:n,theme:s,modules:Object.entries(h).reduce(((t,e)=>{let[n,i]=e;if(!i)return t;const s=I.import(`modules/${n}`);return null==s?(C.error(`Cannot load ${n} module. Are you sure you registered it?`),t):{...t,[n]:(0,r.A)({},s.DEFAULTS||{},i)}}),{}),bounds:B(d.bounds)}}(t,e),this.container=this.options.container,null==this.container)return void C.error(\"Invalid Quill container\",t);this.options.debug&&I.debug(this.options.debug);const n=this.container.innerHTML.trim();this.container.classList.add(\"ql-container\"),this.container.innerHTML=\"\",E.A.set(this.container,this),this.root=this.addContainer(\"ql-editor\"),this.root.classList.add(\"ql-blank\"),this.emitter=new N.A;const s=i.ScrollBlot.blotName,l=this.options.registry.query(s);if(!l||!(\"blotName\"in l))throw new Error(`Cannot initialize Quill without \"${s}\" blot`);if(this.scroll=new l(this.options.registry,this.root,{emitter:this.emitter}),this.editor=new x(this.scroll),this.selection=new f.A(this.scroll,this.emitter),this.composition=new _(this.scroll,this.emitter),this.theme=new this.options.theme(this,this.options),this.keyboard=this.theme.addModule(\"keyboard\"),this.clipboard=this.theme.addModule(\"clipboard\"),this.history=this.theme.addModule(\"history\"),this.uploader=this.theme.addModule(\"uploader\"),this.theme.addModule(\"input\"),this.theme.addModule(\"uiNode\"),this.theme.init(),this.emitter.on(N.A.events.EDITOR_CHANGE,(t=>{t===N.A.events.TEXT_CHANGE&&this.root.classList.toggle(\"ql-blank\",this.editor.isBlank())})),this.emitter.on(N.A.events.SCROLL_UPDATE,((t,e)=>{const n=this.selection.lastRange,[r]=this.selection.getRange(),i=n&&r?{oldRange:n,newRange:r}:void 0;D.call(this,(()=>this.editor.update(null,e,i)),t)})),this.emitter.on(N.A.events.SCROLL_EMBED_UPDATE,((t,e)=>{const n=this.selection.lastRange,[r]=this.selection.getRange(),i=n&&r?{oldRange:n,newRange:r}:void 0;D.call(this,(()=>{const n=(new(o())).retain(t.offset(this)).retain({[t.statics.blotName]:e});return this.editor.update(n,[],i)}),I.sources.USER)})),n){const t=this.clipboard.convert({html:`${n}<p><br></p>`,text:\"\\n\"});this.setContents(t)}this.history.clear(),this.options.placeholder&&this.root.setAttribute(\"data-placeholder\",this.options.placeholder),this.options.readOnly&&this.disable(),this.allowReadOnlyEdits=!1}addContainer(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;if(\"string\"==typeof t){const e=t;(t=document.createElement(\"div\")).classList.add(e)}return this.container.insertBefore(t,e),t}blur(){this.selection.setRange(null)}deleteText(t,e,n){return[t,e,,n]=P(t,e,n),D.call(this,(()=>this.editor.deleteText(t,e)),n,t,-1*e)}disable(){this.enable(!1)}editReadOnly(t){this.allowReadOnlyEdits=!0;const e=t();return this.allowReadOnlyEdits=!1,e}enable(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.scroll.enable(t),this.container.classList.toggle(\"ql-disabled\",!t)}focus(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.selection.focus(),t.preventScroll||this.scrollSelectionIntoView()}format(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:N.A.sources.API;return D.call(this,(()=>{const n=this.getSelection(!0);let r=new(o());if(null==n)return r;if(this.scroll.query(t,i.Scope.BLOCK))r=this.editor.formatLine(n.index,n.length,{[t]:e});else{if(0===n.length)return this.selection.format(t,e),r;r=this.editor.formatText(n.index,n.length,{[t]:e})}return this.setSelection(n,N.A.sources.SILENT),r}),n)}formatLine(t,e,n,r,i){let s;return[t,e,s,i]=P(t,e,n,r,i),D.call(this,(()=>this.editor.formatLine(t,e,s)),i,t,0)}formatText(t,e,n,r,i){let s;return[t,e,s,i]=P(t,e,n,r,i),D.call(this,(()=>this.editor.formatText(t,e,s)),i,t,0)}getBounds(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=null;if(n=\"number\"==typeof t?this.selection.getBounds(t,e):this.selection.getBounds(t.index,t.length),!n)return null;const r=this.container.getBoundingClientRect();return{bottom:n.bottom-r.top,height:n.height,left:n.left-r.left,right:n.right-r.left,top:n.top-r.top,width:n.width}}getContents(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.getLength()-t;return[t,e]=P(t,e),this.editor.getContents(t,e)}getFormat(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.getSelection(!0),e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return\"number\"==typeof t?this.editor.getFormat(t,e):this.editor.getFormat(t.index,t.length)}getIndex(t){return t.offset(this.scroll)}getLength(){return this.scroll.length()}getLeaf(t){return this.scroll.leaf(t)}getLine(t){return this.scroll.line(t)}getLines(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Number.MAX_VALUE;return\"number\"!=typeof t?this.scroll.lines(t.index,t.length):this.scroll.lines(t,e)}getModule(t){return this.theme.modules[t]}getSelection(){return arguments.length>0&&void 0!==arguments[0]&&arguments[0]&&this.focus(),this.update(),this.selection.getRange()[0]}getSemanticHTML(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1?arguments[1]:void 0;return\"number\"==typeof t&&(e=e??this.getLength()-t),[t,e]=P(t,e),this.editor.getHTML(t,e)}getText(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1?arguments[1]:void 0;return\"number\"==typeof t&&(e=e??this.getLength()-t),[t,e]=P(t,e),this.editor.getText(t,e)}hasFocus(){return this.selection.hasFocus()}insertEmbed(t,e,n){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:I.sources.API;return D.call(this,(()=>this.editor.insertEmbed(t,e,n)),r,t)}insertText(t,e,n,r,i){let s;return[t,,s,i]=P(t,0,n,r,i),D.call(this,(()=>this.editor.insertText(t,e,s)),i,t,e.length)}isEnabled(){return this.scroll.isEnabled()}off(){return this.emitter.off(...arguments)}on(){return this.emitter.on(...arguments)}once(){return this.emitter.once(...arguments)}removeFormat(t,e,n){return[t,e,,n]=P(t,e,n),D.call(this,(()=>this.editor.removeFormat(t,e)),n,t)}scrollRectIntoView(t){((t,e)=>{const n=t.ownerDocument;let r=e,i=t;for(;i;){const t=i===n.body,e=t?{top:0,right:window.visualViewport?.width??n.documentElement.clientWidth,bottom:window.visualViewport?.height??n.documentElement.clientHeight,left:0}:S(i),o=getComputedStyle(i),l=T(r.left,r.right,e.left,e.right,O(o.scrollPaddingLeft),O(o.scrollPaddingRight)),a=T(r.top,r.bottom,e.top,e.bottom,O(o.scrollPaddingTop),O(o.scrollPaddingBottom));if(l||a)if(t)n.defaultView?.scrollBy(l,a);else{const{scrollLeft:t,scrollTop:e}=i;a&&(i.scrollTop+=a),l&&(i.scrollLeft+=l);const n=i.scrollLeft-t,s=i.scrollTop-e;r={left:r.left-n,top:r.top-s,right:r.right-n,bottom:r.bottom-s}}i=t||\"fixed\"===o.position?null:(s=i).parentElement||s.getRootNode().host||null}var s})(this.root,t)}scrollIntoView(){console.warn(\"Quill#scrollIntoView() has been deprecated and will be removed in the near future. Please use Quill#scrollSelectionIntoView() instead.\"),this.scrollSelectionIntoView()}scrollSelectionIntoView(){const t=this.selection.lastRange,e=t&&this.selection.getBounds(t.index,t.length);e&&this.scrollRectIntoView(e)}setContents(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:N.A.sources.API;return D.call(this,(()=>{t=new(o())(t);const e=this.getLength(),n=this.editor.deleteText(0,e),r=this.editor.insertContents(0,t),i=this.editor.deleteText(this.getLength()-1,1);return n.compose(r).compose(i)}),e)}setSelection(t,e,n){null==t?this.selection.setRange(null,e||I.sources.API):([t,e,,n]=P(t,e,n),this.selection.setRange(new f.Q(Math.max(0,t),e),n),n!==N.A.sources.SILENT&&this.scrollSelectionIntoView())}setText(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:N.A.sources.API;const n=(new(o())).insert(t);return this.setContents(n,e)}update(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:N.A.sources.USER;const e=this.scroll.update(t);return this.selection.update(t),e}updateContents(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:N.A.sources.API;return D.call(this,(()=>(t=new(o())(t),this.editor.applyDelta(t))),e,!0)}}function B(t){return\"string\"==typeof t?document.querySelector(t):t}function M(t){return Object.entries(t??{}).reduce(((t,e)=>{let[n,r]=e;return{...t,[n]:!0===r?{}:r}}),{})}function U(t){return Object.fromEntries(Object.entries(t).filter((t=>void 0!==t[1])))}function D(t,e,n,r){if(!this.isEnabled()&&e===N.A.sources.USER&&!this.allowReadOnlyEdits)return new(o());let i=null==n?null:this.getSelection();const s=this.editor.delta,l=t();if(null!=i&&(!0===n&&(n=i.index),null==r?i=z(i,l,e):0!==r&&(i=z(i,n,r,e)),this.setSelection(i,N.A.sources.SILENT)),l.length()>0){const t=[N.A.events.TEXT_CHANGE,l,s,e];this.emitter.emit(N.A.events.EDITOR_CHANGE,...t),e!==N.A.sources.SILENT&&this.emitter.emit(...t)}return l}function P(t,e,n,r,i){let s={};return\"number\"==typeof t.index&&\"number\"==typeof t.length?\"number\"!=typeof e?(i=r,r=n,n=e,e=t.length,t=t.index):(e=t.length,t=t.index):\"number\"!=typeof e&&(i=r,r=n,n=e,e=0),\"object\"==typeof n?(s=n,i=r):\"string\"==typeof n&&(null!=r?s[n]=r:i=n),[t,e,s,i=i||N.A.sources.API]}function z(t,e,n,r){const i=\"number\"==typeof n?n:0;if(null==t)return null;let s,o;return e&&\"function\"==typeof e.transformPosition?[s,o]=[t.index,t.index+t.length].map((t=>e.transformPosition(t,r!==N.A.sources.USER))):[s,o]=[t.index,t.index+t.length].map((t=>t<e||t===e&&r===N.A.sources.USER?t:i>=0?t+i:Math.max(e,t+i))),new f.Q(s,o-s)}},8298:function(t,e,n){\"use strict\";n.d(e,{Q:function(){return a}});var r=n(6003),i=n(5123),s=n(3707),o=n(5374);const l=(0,n(6078).A)(\"quill:selection\");class a{constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;this.index=t,this.length=e}}function c(t,e){try{e.parentNode}catch(t){return!1}return t.contains(e)}e.A=class{constructor(t,e){this.emitter=e,this.scroll=t,this.composing=!1,this.mouseDown=!1,this.root=this.scroll.domNode,this.cursor=this.scroll.create(\"cursor\",this),this.savedRange=new a(0,0),this.lastRange=this.savedRange,this.lastNative=null,this.handleComposition(),this.handleDragging(),this.emitter.listenDOM(\"selectionchange\",document,(()=>{this.mouseDown||this.composing||setTimeout(this.update.bind(this,o.A.sources.USER),1)})),this.emitter.on(o.A.events.SCROLL_BEFORE_UPDATE,(()=>{if(!this.hasFocus())return;const t=this.getNativeRange();null!=t&&t.start.node!==this.cursor.textNode&&this.emitter.once(o.A.events.SCROLL_UPDATE,((e,n)=>{try{this.root.contains(t.start.node)&&this.root.contains(t.end.node)&&this.setNativeRange(t.start.node,t.start.offset,t.end.node,t.end.offset);const r=n.some((t=>\"characterData\"===t.type||\"childList\"===t.type||\"attributes\"===t.type&&t.target===this.root));this.update(r?o.A.sources.SILENT:e)}catch(t){}}))})),this.emitter.on(o.A.events.SCROLL_OPTIMIZE,((t,e)=>{if(e.range){const{startNode:t,startOffset:n,endNode:r,endOffset:i}=e.range;this.setNativeRange(t,n,r,i),this.update(o.A.sources.SILENT)}})),this.update(o.A.sources.SILENT)}handleComposition(){this.emitter.on(o.A.events.COMPOSITION_BEFORE_START,(()=>{this.composing=!0})),this.emitter.on(o.A.events.COMPOSITION_END,(()=>{if(this.composing=!1,this.cursor.parent){const t=this.cursor.restore();if(!t)return;setTimeout((()=>{this.setNativeRange(t.startNode,t.startOffset,t.endNode,t.endOffset)}),1)}}))}handleDragging(){this.emitter.listenDOM(\"mousedown\",document.body,(()=>{this.mouseDown=!0})),this.emitter.listenDOM(\"mouseup\",document.body,(()=>{this.mouseDown=!1,this.update(o.A.sources.USER)}))}focus(){this.hasFocus()||(this.root.focus({preventScroll:!0}),this.setRange(this.savedRange))}format(t,e){this.scroll.update();const n=this.getNativeRange();if(null!=n&&n.native.collapsed&&!this.scroll.query(t,r.Scope.BLOCK)){if(n.start.node!==this.cursor.textNode){const t=this.scroll.find(n.start.node,!1);if(null==t)return;if(t instanceof r.LeafBlot){const e=t.split(n.start.offset);t.parent.insertBefore(this.cursor,e)}else t.insertBefore(this.cursor,n.start.node);this.cursor.attach()}this.cursor.format(t,e),this.scroll.optimize(),this.setNativeRange(this.cursor.textNode,this.cursor.textNode.data.length),this.update()}}getBounds(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;const n=this.scroll.length();let r;t=Math.min(t,n-1),e=Math.min(t+e,n-1)-t;let[i,s]=this.scroll.leaf(t);if(null==i)return null;if(e>0&&s===i.length()){const[e]=this.scroll.leaf(t+1);if(e){const[n]=this.scroll.line(t),[r]=this.scroll.line(t+1);n===r&&(i=e,s=0)}}[r,s]=i.position(s,!0);const o=document.createRange();if(e>0)return o.setStart(r,s),[i,s]=this.scroll.leaf(t+e),null==i?null:([r,s]=i.position(s,!0),o.setEnd(r,s),o.getBoundingClientRect());let l,a=\"left\";if(r instanceof Text){if(!r.data.length)return null;s<r.data.length?(o.setStart(r,s),o.setEnd(r,s+1)):(o.setStart(r,s-1),o.setEnd(r,s),a=\"right\"),l=o.getBoundingClientRect()}else{if(!(i.domNode instanceof Element))return null;l=i.domNode.getBoundingClientRect(),s>0&&(a=\"right\")}return{bottom:l.top+l.height,height:l.height,left:l[a],right:l[a],top:l.top,width:0}}getNativeRange(){const t=document.getSelection();if(null==t||t.rangeCount<=0)return null;const e=t.getRangeAt(0);if(null==e)return null;const n=this.normalizeNative(e);return l.info(\"getNativeRange\",n),n}getRange(){const t=this.scroll.domNode;if(\"isConnected\"in t&&!t.isConnected)return[null,null];const e=this.getNativeRange();return null==e?[null,null]:[this.normalizedToRange(e),e]}hasFocus(){return document.activeElement===this.root||null!=document.activeElement&&c(this.root,document.activeElement)}normalizedToRange(t){const e=[[t.start.node,t.start.offset]];t.native.collapsed||e.push([t.end.node,t.end.offset]);const n=e.map((t=>{const[e,n]=t,i=this.scroll.find(e,!0),s=i.offset(this.scroll);return 0===n?s:i instanceof r.LeafBlot?s+i.index(e,n):s+i.length()})),i=Math.min(Math.max(...n),this.scroll.length()-1),s=Math.min(i,...n);return new a(s,i-s)}normalizeNative(t){if(!c(this.root,t.startContainer)||!t.collapsed&&!c(this.root,t.endContainer))return null;const e={start:{node:t.startContainer,offset:t.startOffset},end:{node:t.endContainer,offset:t.endOffset},native:t};return[e.start,e.end].forEach((t=>{let{node:e,offset:n}=t;for(;!(e instanceof Text)&&e.childNodes.length>0;)if(e.childNodes.length>n)e=e.childNodes[n],n=0;else{if(e.childNodes.length!==n)break;e=e.lastChild,n=e instanceof Text?e.data.length:e.childNodes.length>0?e.childNodes.length:e.childNodes.length+1}t.node=e,t.offset=n})),e}rangeToNative(t){const e=this.scroll.length(),n=(t,n)=>{t=Math.min(e-1,t);const[r,i]=this.scroll.leaf(t);return r?r.position(i,n):[null,-1]};return[...n(t.index,!1),...n(t.index+t.length,!0)]}setNativeRange(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:t,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:e,i=arguments.length>4&&void 0!==arguments[4]&&arguments[4];if(l.info(\"setNativeRange\",t,e,n,r),null!=t&&(null==this.root.parentNode||null==t.parentNode||null==n.parentNode))return;const s=document.getSelection();if(null!=s)if(null!=t){this.hasFocus()||this.root.focus({preventScroll:!0});const{native:o}=this.getNativeRange()||{};if(null==o||i||t!==o.startContainer||e!==o.startOffset||n!==o.endContainer||r!==o.endOffset){t instanceof Element&&\"BR\"===t.tagName&&(e=Array.from(t.parentNode.childNodes).indexOf(t),t=t.parentNode),n instanceof Element&&\"BR\"===n.tagName&&(r=Array.from(n.parentNode.childNodes).indexOf(n),n=n.parentNode);const i=document.createRange();i.setStart(t,e),i.setEnd(n,r),s.removeAllRanges(),s.addRange(i)}}else s.removeAllRanges(),this.root.blur()}setRange(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:o.A.sources.API;if(\"string\"==typeof e&&(n=e,e=!1),l.info(\"setRange\",t),null!=t){const n=this.rangeToNative(t);this.setNativeRange(...n,e)}else this.setNativeRange(null);this.update(n)}update(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o.A.sources.USER;const e=this.lastRange,[n,r]=this.getRange();if(this.lastRange=n,this.lastNative=r,null!=this.lastRange&&(this.savedRange=this.lastRange),!(0,i.A)(e,this.lastRange)){if(!this.composing&&null!=r&&r.native.collapsed&&r.start.node!==this.cursor.textNode){const t=this.cursor.restore();t&&this.setNativeRange(t.startNode,t.startOffset,t.endNode,t.endOffset)}const n=[o.A.events.SELECTION_CHANGE,(0,s.A)(this.lastRange),(0,s.A)(e),t];this.emitter.emit(o.A.events.EDITOR_CHANGE,...n),t!==o.A.sources.SILENT&&this.emitter.emit(...n)}}}},9609:function(t,e){\"use strict\";class n{static DEFAULTS={modules:{}};static themes={default:n};modules={};constructor(t,e){this.quill=t,this.options=e}init(){Object.keys(this.options.modules).forEach((t=>{null==this.modules[t]&&this.addModule(t)}))}addModule(t){const e=this.quill.constructor.import(`modules/${t}`);return this.modules[t]=new e(this.quill,this.options.modules[t]||{}),this.modules[t]}}e.A=n},8276:function(t,e,n){\"use strict\";n.d(e,{Hu:function(){return l},gS:function(){return s},qh:function(){return o}});var r=n(6003);const i={scope:r.Scope.BLOCK,whitelist:[\"right\",\"center\",\"justify\"]},s=new r.Attributor(\"align\",\"align\",i),o=new r.ClassAttributor(\"align\",\"ql-align\",i),l=new r.StyleAttributor(\"align\",\"text-align\",i)},9541:function(t,e,n){\"use strict\";n.d(e,{l:function(){return s},s:function(){return o}});var r=n(6003),i=n(8638);const s=new r.ClassAttributor(\"background\",\"ql-bg\",{scope:r.Scope.INLINE}),o=new i.a2(\"background\",\"background-color\",{scope:r.Scope.INLINE})},9404:function(t,e,n){\"use strict\";n.d(e,{Ay:function(){return h},Cy:function(){return d},EJ:function(){return u}});var r=n(9698),i=n(3036),s=n(4541),o=n(4850),l=n(5508),a=n(580),c=n(6142);class u extends a.A{static create(t){const e=super.create(t);return e.setAttribute(\"spellcheck\",\"false\"),e}code(t,e){return this.children.map((t=>t.length()<=1?\"\":t.domNode.innerText)).join(\"\\n\").slice(t,t+e)}html(t,e){return`<pre>\\n${(0,l.X)(this.code(t,e))}\\n</pre>`}}class h extends r.Ay{static TAB=\"  \";static register(){c.Ay.register(u)}}class d extends o.A{}d.blotName=\"code\",d.tagName=\"CODE\",h.blotName=\"code-block\",h.className=\"ql-code-block\",h.tagName=\"DIV\",u.blotName=\"code-block-container\",u.className=\"ql-code-block-container\",u.tagName=\"DIV\",u.allowedChildren=[h],h.allowedChildren=[l.A,i.A,s.A],h.requiredContainer=u},8638:function(t,e,n){\"use strict\";n.d(e,{JM:function(){return o},a2:function(){return i},g3:function(){return s}});var r=n(6003);class i extends r.StyleAttributor{value(t){let e=super.value(t);return e.startsWith(\"rgb(\")?(e=e.replace(/^[^\\d]+/,\"\").replace(/[^\\d]+$/,\"\"),`#${e.split(\",\").map((t=>`00${parseInt(t,10).toString(16)}`.slice(-2))).join(\"\")}`):e}}const s=new r.ClassAttributor(\"color\",\"ql-color\",{scope:r.Scope.INLINE}),o=new i(\"color\",\"color\",{scope:r.Scope.INLINE})},7912:function(t,e,n){\"use strict\";n.d(e,{Mc:function(){return s},VL:function(){return l},sY:function(){return o}});var r=n(6003);const i={scope:r.Scope.BLOCK,whitelist:[\"rtl\"]},s=new r.Attributor(\"direction\",\"dir\",i),o=new r.ClassAttributor(\"direction\",\"ql-direction\",i),l=new r.StyleAttributor(\"direction\",\"direction\",i)},6772:function(t,e,n){\"use strict\";n.d(e,{q:function(){return s},z:function(){return l}});var r=n(6003);const i={scope:r.Scope.INLINE,whitelist:[\"serif\",\"monospace\"]},s=new r.ClassAttributor(\"font\",\"ql-font\",i);class o extends r.StyleAttributor{value(t){return super.value(t).replace(/[\"']/g,\"\")}}const l=new o(\"font\",\"font-family\",i)},664:function(t,e,n){\"use strict\";n.d(e,{U:function(){return i},r:function(){return s}});var r=n(6003);const i=new r.ClassAttributor(\"size\",\"ql-size\",{scope:r.Scope.INLINE,whitelist:[\"small\",\"large\",\"huge\"]}),s=new r.StyleAttributor(\"size\",\"font-size\",{scope:r.Scope.INLINE,whitelist:[\"10px\",\"18px\",\"32px\"]})},584:function(t,e,n){\"use strict\";n.d(e,{Ay:function(){return S},hV:function(){return I}});var r=n(6003),i=n(5232),s=n.n(i),o=n(9698),l=n(6078),a=n(4266),c=n(6142),u=n(8276),h=n(9541),d=n(9404),f=n(8638),p=n(7912),g=n(6772),m=n(664),b=n(8123);const y=/font-weight:\\s*normal/,v=[\"P\",\"OL\",\"UL\"],A=t=>t&&v.includes(t.tagName),x=/\\bmso-list:[^;]*ignore/i,N=/\\bmso-list:[^;]*\\bl(\\d+)/i,E=/\\bmso-list:[^;]*\\blevel(\\d+)/i,w=[function(t){\"urn:schemas-microsoft-com:office:word\"===t.documentElement.getAttribute(\"xmlns:w\")&&(t=>{const e=Array.from(t.querySelectorAll(\"[style*=mso-list]\")),n=[],r=[];e.forEach((t=>{(t.getAttribute(\"style\")||\"\").match(x)?n.push(t):r.push(t)})),n.forEach((t=>t.parentNode?.removeChild(t)));const i=t.documentElement.innerHTML,s=r.map((t=>((t,e)=>{const n=t.getAttribute(\"style\"),r=n?.match(N);if(!r)return null;const i=Number(r[1]),s=n?.match(E),o=s?Number(s[1]):1,l=new RegExp(`@list l${i}:level${o}\\\\s*\\\\{[^\\\\}]*mso-level-number-format:\\\\s*([\\\\w-]+)`,\"i\"),a=e.match(l);return{id:i,indent:o,type:a&&\"bullet\"===a[1]?\"bullet\":\"ordered\",element:t}})(t,i))).filter((t=>t));for(;s.length;){const t=[];let e=s.shift();for(;e;)t.push(e),e=s.length&&s[0]?.element===e.element.nextElementSibling&&s[0].id===e.id?s.shift():null;const n=document.createElement(\"ul\");t.forEach((t=>{const e=document.createElement(\"li\");e.setAttribute(\"data-list\",t.type),t.indent>1&&e.setAttribute(\"class\",\"ql-indent-\"+(t.indent-1)),e.innerHTML=t.element.innerHTML,n.appendChild(e)}));const r=t[0]?.element,{parentNode:i}=r??{};r&&i?.replaceChild(n,r),t.slice(1).forEach((t=>{let{element:e}=t;i?.removeChild(e)}))}})(t)},function(t){t.querySelector('[id^=\"docs-internal-guid-\"]')&&((t=>{Array.from(t.querySelectorAll('b[style*=\"font-weight\"]')).filter((t=>t.getAttribute(\"style\")?.match(y))).forEach((e=>{const n=t.createDocumentFragment();n.append(...e.childNodes),e.parentNode?.replaceChild(n,e)}))})(t),(t=>{Array.from(t.querySelectorAll(\"br\")).filter((t=>A(t.previousElementSibling)&&A(t.nextElementSibling))).forEach((t=>{t.parentNode?.removeChild(t)}))})(t))}];const q=(0,l.A)(\"quill:clipboard\"),k=[[Node.TEXT_NODE,function(t,e,n){let r=t.data;if(\"O:P\"===t.parentElement?.tagName)return e.insert(r.trim());if(!R(t)){if(0===r.trim().length&&r.includes(\"\\n\")&&!function(t,e){return t.previousElementSibling&&t.nextElementSibling&&!j(t.previousElementSibling,e)&&!j(t.nextElementSibling,e)}(t,n))return e;const i=(t,e)=>{const n=e.replace(/[^\\u00a0]/g,\"\");return n.length<1&&t?\" \":n};r=r.replace(/\\r\\n/g,\" \").replace(/\\n/g,\" \"),r=r.replace(/\\s\\s+/g,i.bind(i,!0)),(null==t.previousSibling&&null!=t.parentElement&&j(t.parentElement,n)||t.previousSibling instanceof Element&&j(t.previousSibling,n))&&(r=r.replace(/^\\s+/,i.bind(i,!1))),(null==t.nextSibling&&null!=t.parentElement&&j(t.parentElement,n)||t.nextSibling instanceof Element&&j(t.nextSibling,n))&&(r=r.replace(/\\s+$/,i.bind(i,!1)))}return e.insert(r)}],[Node.TEXT_NODE,M],[\"br\",function(t,e){return T(e,\"\\n\")||e.insert(\"\\n\"),e}],[Node.ELEMENT_NODE,M],[Node.ELEMENT_NODE,function(t,e,n){const i=n.query(t);if(null==i)return e;if(i.prototype instanceof r.EmbedBlot){const e={},r=i.value(t);if(null!=r)return e[i.blotName]=r,(new(s())).insert(e,i.formats(t,n))}else if(i.prototype instanceof r.BlockBlot&&!T(e,\"\\n\")&&e.insert(\"\\n\"),\"blotName\"in i&&\"formats\"in i&&\"function\"==typeof i.formats)return O(e,i.blotName,i.formats(t,n),n);return e}],[Node.ELEMENT_NODE,function(t,e,n){const i=r.Attributor.keys(t),s=r.ClassAttributor.keys(t),o=r.StyleAttributor.keys(t),l={};return i.concat(s).concat(o).forEach((e=>{let i=n.query(e,r.Scope.ATTRIBUTE);null!=i&&(l[i.attrName]=i.value(t),l[i.attrName])||(i=_[e],null==i||i.attrName!==e&&i.keyName!==e||(l[i.attrName]=i.value(t)||void 0),i=L[e],null==i||i.attrName!==e&&i.keyName!==e||(i=L[e],l[i.attrName]=i.value(t)||void 0))})),Object.entries(l).reduce(((t,e)=>{let[r,i]=e;return O(t,r,i,n)}),e)}],[Node.ELEMENT_NODE,function(t,e,n){const r={},i=t.style||{};return\"italic\"===i.fontStyle&&(r.italic=!0),\"underline\"===i.textDecoration&&(r.underline=!0),\"line-through\"===i.textDecoration&&(r.strike=!0),(i.fontWeight?.startsWith(\"bold\")||parseInt(i.fontWeight,10)>=700)&&(r.bold=!0),e=Object.entries(r).reduce(((t,e)=>{let[r,i]=e;return O(t,r,i,n)}),e),parseFloat(i.textIndent||0)>0?(new(s())).insert(\"\\t\").concat(e):e}],[\"li\",function(t,e,n){const r=n.query(t);if(null==r||\"list\"!==r.blotName||!T(e,\"\\n\"))return e;let i=-1,o=t.parentNode;for(;null!=o;)[\"OL\",\"UL\"].includes(o.tagName)&&(i+=1),o=o.parentNode;return i<=0?e:e.reduce(((t,e)=>e.insert?e.attributes&&\"number\"==typeof e.attributes.indent?t.push(e):t.insert(e.insert,{indent:i,...e.attributes||{}}):t),new(s()))}],[\"ol, ul\",function(t,e,n){const r=t;let i=\"OL\"===r.tagName?\"ordered\":\"bullet\";const s=r.getAttribute(\"data-checked\");return s&&(i=\"true\"===s?\"checked\":\"unchecked\"),O(e,\"list\",i,n)}],[\"pre\",function(t,e,n){const r=n.query(\"code-block\");return O(e,\"code-block\",!r||!(\"formats\"in r)||\"function\"!=typeof r.formats||r.formats(t,n),n)}],[\"tr\",function(t,e,n){const r=\"TABLE\"===t.parentElement?.tagName?t.parentElement:t.parentElement?.parentElement;return null!=r?O(e,\"table\",Array.from(r.querySelectorAll(\"tr\")).indexOf(t)+1,n):e}],[\"b\",B(\"bold\")],[\"i\",B(\"italic\")],[\"strike\",B(\"strike\")],[\"style\",function(){return new(s())}]],_=[u.gS,p.Mc].reduce(((t,e)=>(t[e.keyName]=e,t)),{}),L=[u.Hu,h.s,f.JM,p.VL,g.z,m.r].reduce(((t,e)=>(t[e.keyName]=e,t)),{});class S extends a.A{static DEFAULTS={matchers:[]};constructor(t,e){super(t,e),this.quill.root.addEventListener(\"copy\",(t=>this.onCaptureCopy(t,!1))),this.quill.root.addEventListener(\"cut\",(t=>this.onCaptureCopy(t,!0))),this.quill.root.addEventListener(\"paste\",this.onCapturePaste.bind(this)),this.matchers=[],k.concat(this.options.matchers??[]).forEach((t=>{let[e,n]=t;this.addMatcher(e,n)}))}addMatcher(t,e){this.matchers.push([t,e])}convert(t){let{html:e,text:n}=t,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(r[d.Ay.blotName])return(new(s())).insert(n||\"\",{[d.Ay.blotName]:r[d.Ay.blotName]});if(!e)return(new(s())).insert(n||\"\",r);const i=this.convertHTML(e);return T(i,\"\\n\")&&(null==i.ops[i.ops.length-1].attributes||r.table)?i.compose((new(s())).retain(i.length()-1).delete(1)):i}normalizeHTML(t){(t=>{t.documentElement&&w.forEach((e=>{e(t)}))})(t)}convertHTML(t){const e=(new DOMParser).parseFromString(t,\"text/html\");this.normalizeHTML(e);const n=e.body,r=new WeakMap,[i,s]=this.prepareMatching(n,r);return I(this.quill.scroll,n,i,s,r)}dangerouslyPasteHTML(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:c.Ay.sources.API;if(\"string\"==typeof t){const n=this.convert({html:t,text:\"\"});this.quill.setContents(n,e),this.quill.setSelection(0,c.Ay.sources.SILENT)}else{const r=this.convert({html:e,text:\"\"});this.quill.updateContents((new(s())).retain(t).concat(r),n),this.quill.setSelection(t+r.length(),c.Ay.sources.SILENT)}}onCaptureCopy(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(t.defaultPrevented)return;t.preventDefault();const[n]=this.quill.selection.getRange();if(null==n)return;const{html:r,text:i}=this.onCopy(n,e);t.clipboardData?.setData(\"text/plain\",i),t.clipboardData?.setData(\"text/html\",r),e&&(0,b.Xo)({range:n,quill:this.quill})}normalizeURIList(t){return t.split(/\\r?\\n/).filter((t=>\"#\"!==t[0])).join(\"\\n\")}onCapturePaste(t){if(t.defaultPrevented||!this.quill.isEnabled())return;t.preventDefault();const e=this.quill.getSelection(!0);if(null==e)return;const n=t.clipboardData?.getData(\"text/html\");let r=t.clipboardData?.getData(\"text/plain\");if(!n&&!r){const e=t.clipboardData?.getData(\"text/uri-list\");e&&(r=this.normalizeURIList(e))}const i=Array.from(t.clipboardData?.files||[]);if(!n&&i.length>0)this.quill.uploader.upload(e,i);else{if(n&&i.length>0){const t=(new DOMParser).parseFromString(n,\"text/html\");if(1===t.body.childElementCount&&\"IMG\"===t.body.firstElementChild?.tagName)return void this.quill.uploader.upload(e,i)}this.onPaste(e,{html:n,text:r})}}onCopy(t){const e=this.quill.getText(t);return{html:this.quill.getSemanticHTML(t),text:e}}onPaste(t,e){let{text:n,html:r}=e;const i=this.quill.getFormat(t.index),o=this.convert({text:n,html:r},i);q.log(\"onPaste\",o,{text:n,html:r});const l=(new(s())).retain(t.index).delete(t.length).concat(o);this.quill.updateContents(l,c.Ay.sources.USER),this.quill.setSelection(l.length()-t.length,c.Ay.sources.SILENT),this.quill.scrollSelectionIntoView()}prepareMatching(t,e){const n=[],r=[];return this.matchers.forEach((i=>{const[s,o]=i;switch(s){case Node.TEXT_NODE:r.push(o);break;case Node.ELEMENT_NODE:n.push(o);break;default:Array.from(t.querySelectorAll(s)).forEach((t=>{if(e.has(t)){const n=e.get(t);n?.push(o)}else e.set(t,[o])}))}})),[n,r]}}function O(t,e,n,r){return r.query(e)?t.reduce(((t,r)=>{if(!r.insert)return t;if(r.attributes&&r.attributes[e])return t.push(r);const i=n?{[e]:n}:{};return t.insert(r.insert,{...i,...r.attributes})}),new(s())):t}function T(t,e){let n=\"\";for(let r=t.ops.length-1;r>=0&&n.length<e.length;--r){const e=t.ops[r];if(\"string\"!=typeof e.insert)break;n=e.insert+n}return n.slice(-1*e.length)===e}function j(t,e){if(!(t instanceof Element))return!1;const n=e.query(t);return!(n&&n.prototype instanceof r.EmbedBlot)&&[\"address\",\"article\",\"blockquote\",\"canvas\",\"dd\",\"div\",\"dl\",\"dt\",\"fieldset\",\"figcaption\",\"figure\",\"footer\",\"form\",\"h1\",\"h2\",\"h3\",\"h4\",\"h5\",\"h6\",\"header\",\"iframe\",\"li\",\"main\",\"nav\",\"ol\",\"output\",\"p\",\"pre\",\"section\",\"table\",\"td\",\"tr\",\"ul\",\"video\"].includes(t.tagName.toLowerCase())}const C=new WeakMap;function R(t){return null!=t&&(C.has(t)||(\"PRE\"===t.tagName?C.set(t,!0):C.set(t,R(t.parentNode))),C.get(t))}function I(t,e,n,r,i){return e.nodeType===e.TEXT_NODE?r.reduce(((n,r)=>r(e,n,t)),new(s())):e.nodeType===e.ELEMENT_NODE?Array.from(e.childNodes||[]).reduce(((s,o)=>{let l=I(t,o,n,r,i);return o.nodeType===e.ELEMENT_NODE&&(l=n.reduce(((e,n)=>n(o,e,t)),l),l=(i.get(o)||[]).reduce(((e,n)=>n(o,e,t)),l)),s.concat(l)}),new(s())):new(s())}function B(t){return(e,n,r)=>O(n,t,!0,r)}function M(t,e,n){if(!T(e,\"\\n\")){if(j(t,n)&&(t.childNodes.length>0||t instanceof HTMLParagraphElement))return e.insert(\"\\n\");if(e.length()>0&&t.nextSibling){let r=t.nextSibling;for(;null!=r;){if(j(r,n))return e.insert(\"\\n\");const t=n.query(r);if(t&&t.prototype instanceof o.zo)return e.insert(\"\\n\");r=r.firstChild}}}return e}},8123:function(t,e,n){\"use strict\";n.d(e,{Ay:function(){return f},Xo:function(){return v}});var r=n(5123),i=n(3707),s=n(5232),o=n.n(s),l=n(6003),a=n(6142),c=n(6078),u=n(4266);const h=(0,c.A)(\"quill:keyboard\"),d=/Mac/i.test(navigator.platform)?\"metaKey\":\"ctrlKey\";class f extends u.A{static match(t,e){return![\"altKey\",\"ctrlKey\",\"metaKey\",\"shiftKey\"].some((n=>!!e[n]!==t[n]&&null!==e[n]))&&(e.key===t.key||e.key===t.which)}constructor(t,e){super(t,e),this.bindings={},Object.keys(this.options.bindings).forEach((t=>{this.options.bindings[t]&&this.addBinding(this.options.bindings[t])})),this.addBinding({key:\"Enter\",shiftKey:null},this.handleEnter),this.addBinding({key:\"Enter\",metaKey:null,ctrlKey:null,altKey:null},(()=>{})),/Firefox/i.test(navigator.userAgent)?(this.addBinding({key:\"Backspace\"},{collapsed:!0},this.handleBackspace),this.addBinding({key:\"Delete\"},{collapsed:!0},this.handleDelete)):(this.addBinding({key:\"Backspace\"},{collapsed:!0,prefix:/^.?$/},this.handleBackspace),this.addBinding({key:\"Delete\"},{collapsed:!0,suffix:/^.?$/},this.handleDelete)),this.addBinding({key:\"Backspace\"},{collapsed:!1},this.handleDeleteRange),this.addBinding({key:\"Delete\"},{collapsed:!1},this.handleDeleteRange),this.addBinding({key:\"Backspace\",altKey:null,ctrlKey:null,metaKey:null,shiftKey:null},{collapsed:!0,offset:0},this.handleBackspace),this.listen()}addBinding(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const r=function(t){if(\"string\"==typeof t||\"number\"==typeof t)t={key:t};else{if(\"object\"!=typeof t)return null;t=(0,i.A)(t)}return t.shortKey&&(t[d]=t.shortKey,delete t.shortKey),t}(t);null!=r?(\"function\"==typeof e&&(e={handler:e}),\"function\"==typeof n&&(n={handler:n}),(Array.isArray(r.key)?r.key:[r.key]).forEach((t=>{const i={...r,key:t,...e,...n};this.bindings[i.key]=this.bindings[i.key]||[],this.bindings[i.key].push(i)}))):h.warn(\"Attempted to add invalid keyboard binding\",r)}listen(){this.quill.root.addEventListener(\"keydown\",(t=>{if(t.defaultPrevented||t.isComposing)return;if(229===t.keyCode&&(\"Enter\"===t.key||\"Backspace\"===t.key))return;const e=(this.bindings[t.key]||[]).concat(this.bindings[t.which]||[]).filter((e=>f.match(t,e)));if(0===e.length)return;const n=a.Ay.find(t.target,!0);if(n&&n.scroll!==this.quill.scroll)return;const i=this.quill.getSelection();if(null==i||!this.quill.hasFocus())return;const[s,o]=this.quill.getLine(i.index),[c,u]=this.quill.getLeaf(i.index),[h,d]=0===i.length?[c,u]:this.quill.getLeaf(i.index+i.length),p=c instanceof l.TextBlot?c.value().slice(0,u):\"\",g=h instanceof l.TextBlot?h.value().slice(d):\"\",m={collapsed:0===i.length,empty:0===i.length&&s.length()<=1,format:this.quill.getFormat(i),line:s,offset:o,prefix:p,suffix:g,event:t};e.some((t=>{if(null!=t.collapsed&&t.collapsed!==m.collapsed)return!1;if(null!=t.empty&&t.empty!==m.empty)return!1;if(null!=t.offset&&t.offset!==m.offset)return!1;if(Array.isArray(t.format)){if(t.format.every((t=>null==m.format[t])))return!1}else if(\"object\"==typeof t.format&&!Object.keys(t.format).every((e=>!0===t.format[e]?null!=m.format[e]:!1===t.format[e]?null==m.format[e]:(0,r.A)(t.format[e],m.format[e]))))return!1;return!(null!=t.prefix&&!t.prefix.test(m.prefix)||null!=t.suffix&&!t.suffix.test(m.suffix)||!0===t.handler.call(this,i,m,t))}))&&t.preventDefault()}))}handleBackspace(t,e){const n=/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]$/.test(e.prefix)?2:1;if(0===t.index||this.quill.getLength()<=1)return;let r={};const[i]=this.quill.getLine(t.index);let l=(new(o())).retain(t.index-n).delete(n);if(0===e.offset){const[e]=this.quill.getLine(t.index-1);if(e&&!(\"block\"===e.statics.blotName&&e.length()<=1)){const e=i.formats(),n=this.quill.getFormat(t.index-1,1);if(r=s.AttributeMap.diff(e,n)||{},Object.keys(r).length>0){const e=(new(o())).retain(t.index+i.length()-2).retain(1,r);l=l.compose(e)}}}this.quill.updateContents(l,a.Ay.sources.USER),this.quill.focus()}handleDelete(t,e){const n=/^[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/.test(e.suffix)?2:1;if(t.index>=this.quill.getLength()-n)return;let r={};const[i]=this.quill.getLine(t.index);let l=(new(o())).retain(t.index).delete(n);if(e.offset>=i.length()-1){const[e]=this.quill.getLine(t.index+1);if(e){const n=i.formats(),o=this.quill.getFormat(t.index,1);r=s.AttributeMap.diff(n,o)||{},Object.keys(r).length>0&&(l=l.retain(e.length()-1).retain(1,r))}}this.quill.updateContents(l,a.Ay.sources.USER),this.quill.focus()}handleDeleteRange(t){v({range:t,quill:this.quill}),this.quill.focus()}handleEnter(t,e){const n=Object.keys(e.format).reduce(((t,n)=>(this.quill.scroll.query(n,l.Scope.BLOCK)&&!Array.isArray(e.format[n])&&(t[n]=e.format[n]),t)),{}),r=(new(o())).retain(t.index).delete(t.length).insert(\"\\n\",n);this.quill.updateContents(r,a.Ay.sources.USER),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),this.quill.focus()}}const p={bindings:{bold:b(\"bold\"),italic:b(\"italic\"),underline:b(\"underline\"),indent:{key:\"Tab\",format:[\"blockquote\",\"indent\",\"list\"],handler(t,e){return!(!e.collapsed||0===e.offset)||(this.quill.format(\"indent\",\"+1\",a.Ay.sources.USER),!1)}},outdent:{key:\"Tab\",shiftKey:!0,format:[\"blockquote\",\"indent\",\"list\"],handler(t,e){return!(!e.collapsed||0===e.offset)||(this.quill.format(\"indent\",\"-1\",a.Ay.sources.USER),!1)}},\"outdent backspace\":{key:\"Backspace\",collapsed:!0,shiftKey:null,metaKey:null,ctrlKey:null,altKey:null,format:[\"indent\",\"list\"],offset:0,handler(t,e){null!=e.format.indent?this.quill.format(\"indent\",\"-1\",a.Ay.sources.USER):null!=e.format.list&&this.quill.format(\"list\",!1,a.Ay.sources.USER)}},\"indent code-block\":g(!0),\"outdent code-block\":g(!1),\"remove tab\":{key:\"Tab\",shiftKey:!0,collapsed:!0,prefix:/\\t$/,handler(t){this.quill.deleteText(t.index-1,1,a.Ay.sources.USER)}},tab:{key:\"Tab\",handler(t,e){if(e.format.table)return!0;this.quill.history.cutoff();const n=(new(o())).retain(t.index).delete(t.length).insert(\"\\t\");return this.quill.updateContents(n,a.Ay.sources.USER),this.quill.history.cutoff(),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),!1}},\"blockquote empty enter\":{key:\"Enter\",collapsed:!0,format:[\"blockquote\"],empty:!0,handler(){this.quill.format(\"blockquote\",!1,a.Ay.sources.USER)}},\"list empty enter\":{key:\"Enter\",collapsed:!0,format:[\"list\"],empty:!0,handler(t,e){const n={list:!1};e.format.indent&&(n.indent=!1),this.quill.formatLine(t.index,t.length,n,a.Ay.sources.USER)}},\"checklist enter\":{key:\"Enter\",collapsed:!0,format:{list:\"checked\"},handler(t){const[e,n]=this.quill.getLine(t.index),r={...e.formats(),list:\"checked\"},i=(new(o())).retain(t.index).insert(\"\\n\",r).retain(e.length()-n-1).retain(1,{list:\"unchecked\"});this.quill.updateContents(i,a.Ay.sources.USER),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),this.quill.scrollSelectionIntoView()}},\"header enter\":{key:\"Enter\",collapsed:!0,format:[\"header\"],suffix:/^$/,handler(t,e){const[n,r]=this.quill.getLine(t.index),i=(new(o())).retain(t.index).insert(\"\\n\",e.format).retain(n.length()-r-1).retain(1,{header:null});this.quill.updateContents(i,a.Ay.sources.USER),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),this.quill.scrollSelectionIntoView()}},\"table backspace\":{key:\"Backspace\",format:[\"table\"],collapsed:!0,offset:0,handler(){}},\"table delete\":{key:\"Delete\",format:[\"table\"],collapsed:!0,suffix:/^$/,handler(){}},\"table enter\":{key:\"Enter\",shiftKey:null,format:[\"table\"],handler(t){const e=this.quill.getModule(\"table\");if(e){const[n,r,i,s]=e.getTable(t),l=function(t,e,n,r){return null==e.prev&&null==e.next?null==n.prev&&null==n.next?0===r?-1:1:null==n.prev?-1:1:null==e.prev?-1:null==e.next?1:null}(0,r,i,s);if(null==l)return;let c=n.offset();if(l<0){const e=(new(o())).retain(c).insert(\"\\n\");this.quill.updateContents(e,a.Ay.sources.USER),this.quill.setSelection(t.index+1,t.length,a.Ay.sources.SILENT)}else if(l>0){c+=n.length();const t=(new(o())).retain(c).insert(\"\\n\");this.quill.updateContents(t,a.Ay.sources.USER),this.quill.setSelection(c,a.Ay.sources.USER)}}}},\"table tab\":{key:\"Tab\",shiftKey:null,format:[\"table\"],handler(t,e){const{event:n,line:r}=e,i=r.offset(this.quill.scroll);n.shiftKey?this.quill.setSelection(i-1,a.Ay.sources.USER):this.quill.setSelection(i+r.length(),a.Ay.sources.USER)}},\"list autofill\":{key:\" \",shiftKey:null,collapsed:!0,format:{\"code-block\":!1,blockquote:!1,table:!1},prefix:/^\\s*?(\\d+\\.|-|\\*|\\[ ?\\]|\\[x\\])$/,handler(t,e){if(null==this.quill.scroll.query(\"list\"))return!0;const{length:n}=e.prefix,[r,i]=this.quill.getLine(t.index);if(i>n)return!0;let s;switch(e.prefix.trim()){case\"[]\":case\"[ ]\":s=\"unchecked\";break;case\"[x]\":s=\"checked\";break;case\"-\":case\"*\":s=\"bullet\";break;default:s=\"ordered\"}this.quill.insertText(t.index,\" \",a.Ay.sources.USER),this.quill.history.cutoff();const l=(new(o())).retain(t.index-i).delete(n+1).retain(r.length()-2-i).retain(1,{list:s});return this.quill.updateContents(l,a.Ay.sources.USER),this.quill.history.cutoff(),this.quill.setSelection(t.index-n,a.Ay.sources.SILENT),!1}},\"code exit\":{key:\"Enter\",collapsed:!0,format:[\"code-block\"],prefix:/^$/,suffix:/^\\s*$/,handler(t){const[e,n]=this.quill.getLine(t.index);let r=2,i=e;for(;null!=i&&i.length()<=1&&i.formats()[\"code-block\"];)if(i=i.prev,r-=1,r<=0){const r=(new(o())).retain(t.index+e.length()-n-2).retain(1,{\"code-block\":null}).delete(1);return this.quill.updateContents(r,a.Ay.sources.USER),this.quill.setSelection(t.index-1,a.Ay.sources.SILENT),!1}return!0}},\"embed left\":m(\"ArrowLeft\",!1),\"embed left shift\":m(\"ArrowLeft\",!0),\"embed right\":m(\"ArrowRight\",!1),\"embed right shift\":m(\"ArrowRight\",!0),\"table down\":y(!1),\"table up\":y(!0)}};function g(t){return{key:\"Tab\",shiftKey:!t,format:{\"code-block\":!0},handler(e,n){let{event:r}=n;const i=this.quill.scroll.query(\"code-block\"),{TAB:s}=i;if(0===e.length&&!r.shiftKey)return this.quill.insertText(e.index,s,a.Ay.sources.USER),void this.quill.setSelection(e.index+s.length,a.Ay.sources.SILENT);const o=0===e.length?this.quill.getLines(e.index,1):this.quill.getLines(e);let{index:l,length:c}=e;o.forEach(((e,n)=>{t?(e.insertAt(0,s),0===n?l+=s.length:c+=s.length):e.domNode.textContent.startsWith(s)&&(e.deleteAt(0,s.length),0===n?l-=s.length:c-=s.length)})),this.quill.update(a.Ay.sources.USER),this.quill.setSelection(l,c,a.Ay.sources.SILENT)}}}function m(t,e){return{key:t,shiftKey:e,altKey:null,[\"ArrowLeft\"===t?\"prefix\":\"suffix\"]:/^$/,handler(n){let{index:r}=n;\"ArrowRight\"===t&&(r+=n.length+1);const[i]=this.quill.getLeaf(r);return!(i instanceof l.EmbedBlot&&(\"ArrowLeft\"===t?e?this.quill.setSelection(n.index-1,n.length+1,a.Ay.sources.USER):this.quill.setSelection(n.index-1,a.Ay.sources.USER):e?this.quill.setSelection(n.index,n.length+1,a.Ay.sources.USER):this.quill.setSelection(n.index+n.length+1,a.Ay.sources.USER),1))}}}function b(t){return{key:t[0],shortKey:!0,handler(e,n){this.quill.format(t,!n.format[t],a.Ay.sources.USER)}}}function y(t){return{key:t?\"ArrowUp\":\"ArrowDown\",collapsed:!0,format:[\"table\"],handler(e,n){const r=t?\"prev\":\"next\",i=n.line,s=i.parent[r];if(null!=s){if(\"table-row\"===s.statics.blotName){let t=s.children.head,e=i;for(;null!=e.prev;)e=e.prev,t=t.next;const r=t.offset(this.quill.scroll)+Math.min(n.offset,t.length()-1);this.quill.setSelection(r,0,a.Ay.sources.USER)}}else{const e=i.table()[r];null!=e&&(t?this.quill.setSelection(e.offset(this.quill.scroll)+e.length()-1,0,a.Ay.sources.USER):this.quill.setSelection(e.offset(this.quill.scroll),0,a.Ay.sources.USER))}return!1}}}function v(t){let{quill:e,range:n}=t;const r=e.getLines(n);let i={};if(r.length>1){const t=r[0].formats(),e=r[r.length-1].formats();i=s.AttributeMap.diff(e,t)||{}}e.deleteText(n,a.Ay.sources.USER),Object.keys(i).length>0&&e.formatLine(n.index,1,i,a.Ay.sources.USER),e.setSelection(n.index,a.Ay.sources.SILENT)}f.DEFAULTS=p},8920:function(t){\"use strict\";var e=Object.prototype.hasOwnProperty,n=\"~\";function r(){}function i(t,e,n){this.fn=t,this.context=e,this.once=n||!1}function s(t,e,r,s,o){if(\"function\"!=typeof r)throw new TypeError(\"The listener must be a function\");var l=new i(r,s||t,o),a=n?n+e:e;return t._events[a]?t._events[a].fn?t._events[a]=[t._events[a],l]:t._events[a].push(l):(t._events[a]=l,t._eventsCount++),t}function o(t,e){0==--t._eventsCount?t._events=new r:delete t._events[e]}function l(){this._events=new r,this._eventsCount=0}Object.create&&(r.prototype=Object.create(null),(new r).__proto__||(n=!1)),l.prototype.eventNames=function(){var t,r,i=[];if(0===this._eventsCount)return i;for(r in t=this._events)e.call(t,r)&&i.push(n?r.slice(1):r);return Object.getOwnPropertySymbols?i.concat(Object.getOwnPropertySymbols(t)):i},l.prototype.listeners=function(t){var e=n?n+t:t,r=this._events[e];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,s=r.length,o=new Array(s);i<s;i++)o[i]=r[i].fn;return o},l.prototype.listenerCount=function(t){var e=n?n+t:t,r=this._events[e];return r?r.fn?1:r.length:0},l.prototype.emit=function(t,e,r,i,s,o){var l=n?n+t:t;if(!this._events[l])return!1;var a,c,u=this._events[l],h=arguments.length;if(u.fn){switch(u.once&&this.removeListener(t,u.fn,void 0,!0),h){case 1:return u.fn.call(u.context),!0;case 2:return u.fn.call(u.context,e),!0;case 3:return u.fn.call(u.context,e,r),!0;case 4:return u.fn.call(u.context,e,r,i),!0;case 5:return u.fn.call(u.context,e,r,i,s),!0;case 6:return u.fn.call(u.context,e,r,i,s,o),!0}for(c=1,a=new Array(h-1);c<h;c++)a[c-1]=arguments[c];u.fn.apply(u.context,a)}else{var d,f=u.length;for(c=0;c<f;c++)switch(u[c].once&&this.removeListener(t,u[c].fn,void 0,!0),h){case 1:u[c].fn.call(u[c].context);break;case 2:u[c].fn.call(u[c].context,e);break;case 3:u[c].fn.call(u[c].context,e,r);break;case 4:u[c].fn.call(u[c].context,e,r,i);break;default:if(!a)for(d=1,a=new Array(h-1);d<h;d++)a[d-1]=arguments[d];u[c].fn.apply(u[c].context,a)}}return!0},l.prototype.on=function(t,e,n){return s(this,t,e,n,!1)},l.prototype.once=function(t,e,n){return s(this,t,e,n,!0)},l.prototype.removeListener=function(t,e,r,i){var s=n?n+t:t;if(!this._events[s])return this;if(!e)return o(this,s),this;var l=this._events[s];if(l.fn)l.fn!==e||i&&!l.once||r&&l.context!==r||o(this,s);else{for(var a=0,c=[],u=l.length;a<u;a++)(l[a].fn!==e||i&&!l[a].once||r&&l[a].context!==r)&&c.push(l[a]);c.length?this._events[s]=1===c.length?c[0]:c:o(this,s)}return this},l.prototype.removeAllListeners=function(t){var e;return t?(e=n?n+t:t,this._events[e]&&o(this,e)):(this._events=new r,this._eventsCount=0),this},l.prototype.off=l.prototype.removeListener,l.prototype.addListener=l.prototype.on,l.prefixed=n,l.EventEmitter=l,t.exports=l},5090:function(t){var e=-1,n=1,r=0;function i(t,g,m,b,y){if(t===g)return t?[[r,t]]:[];if(null!=m){var A=function(t,e,n){var r=\"number\"==typeof n?{index:n,length:0}:n.oldRange,i=\"number\"==typeof n?null:n.newRange,s=t.length,o=e.length;if(0===r.length&&(null===i||0===i.length)){var l=r.index,a=t.slice(0,l),c=t.slice(l),u=i?i.index:null,h=l+o-s;if((null===u||u===h)&&!(h<0||h>o)){var d=e.slice(0,h);if((g=e.slice(h))===c){var f=Math.min(l,h);if((b=a.slice(0,f))===(A=d.slice(0,f)))return v(b,a.slice(f),d.slice(f),c)}}if(null===u||u===l){var p=l,g=(d=e.slice(0,p),e.slice(p));if(d===a){var m=Math.min(s-p,o-p);if((y=c.slice(c.length-m))===(x=g.slice(g.length-m)))return v(a,c.slice(0,c.length-m),g.slice(0,g.length-m),y)}}}if(r.length>0&&i&&0===i.length){var b=t.slice(0,r.index),y=t.slice(r.index+r.length);if(!(o<(f=b.length)+(m=y.length))){var A=e.slice(0,f),x=e.slice(o-m);if(b===A&&y===x)return v(b,t.slice(f,s-m),e.slice(f,o-m),y)}}return null}(t,g,m);if(A)return A}var x=o(t,g),N=t.substring(0,x);x=a(t=t.substring(x),g=g.substring(x));var E=t.substring(t.length-x),w=function(t,l){var c;if(!t)return[[n,l]];if(!l)return[[e,t]];var u=t.length>l.length?t:l,h=t.length>l.length?l:t,d=u.indexOf(h);if(-1!==d)return c=[[n,u.substring(0,d)],[r,h],[n,u.substring(d+h.length)]],t.length>l.length&&(c[0][0]=c[2][0]=e),c;if(1===h.length)return[[e,t],[n,l]];var f=function(t,e){var n=t.length>e.length?t:e,r=t.length>e.length?e:t;if(n.length<4||2*r.length<n.length)return null;function i(t,e,n){for(var r,i,s,l,c=t.substring(n,n+Math.floor(t.length/4)),u=-1,h=\"\";-1!==(u=e.indexOf(c,u+1));){var d=o(t.substring(n),e.substring(u)),f=a(t.substring(0,n),e.substring(0,u));h.length<f+d&&(h=e.substring(u-f,u)+e.substring(u,u+d),r=t.substring(0,n-f),i=t.substring(n+d),s=e.substring(0,u-f),l=e.substring(u+d))}return 2*h.length>=t.length?[r,i,s,l,h]:null}var s,l,c,u,h,d=i(n,r,Math.ceil(n.length/4)),f=i(n,r,Math.ceil(n.length/2));return d||f?(s=f?d&&d[4].length>f[4].length?d:f:d,t.length>e.length?(l=s[0],c=s[1],u=s[2],h=s[3]):(u=s[0],h=s[1],l=s[2],c=s[3]),[l,c,u,h,s[4]]):null}(t,l);if(f){var p=f[0],g=f[1],m=f[2],b=f[3],y=f[4],v=i(p,m),A=i(g,b);return v.concat([[r,y]],A)}return function(t,r){for(var i=t.length,o=r.length,l=Math.ceil((i+o)/2),a=l,c=2*l,u=new Array(c),h=new Array(c),d=0;d<c;d++)u[d]=-1,h[d]=-1;u[a+1]=0,h[a+1]=0;for(var f=i-o,p=f%2!=0,g=0,m=0,b=0,y=0,v=0;v<l;v++){for(var A=-v+g;A<=v-m;A+=2){for(var x=a+A,N=(_=A===-v||A!==v&&u[x-1]<u[x+1]?u[x+1]:u[x-1]+1)-A;_<i&&N<o&&t.charAt(_)===r.charAt(N);)_++,N++;if(u[x]=_,_>i)m+=2;else if(N>o)g+=2;else if(p&&(q=a+f-A)>=0&&q<c&&-1!==h[q]&&_>=(w=i-h[q]))return s(t,r,_,N)}for(var E=-v+b;E<=v-y;E+=2){for(var w,q=a+E,k=(w=E===-v||E!==v&&h[q-1]<h[q+1]?h[q+1]:h[q-1]+1)-E;w<i&&k<o&&t.charAt(i-w-1)===r.charAt(o-k-1);)w++,k++;if(h[q]=w,w>i)y+=2;else if(k>o)b+=2;else if(!p){var _;if((x=a+f-E)>=0&&x<c&&-1!==u[x])if(N=a+(_=u[x])-x,_>=(w=i-w))return s(t,r,_,N)}}}return[[e,t],[n,r]]}(t,l)}(t=t.substring(0,t.length-x),g=g.substring(0,g.length-x));return N&&w.unshift([r,N]),E&&w.push([r,E]),p(w,y),b&&function(t){for(var i=!1,s=[],o=0,g=null,m=0,b=0,y=0,v=0,A=0;m<t.length;)t[m][0]==r?(s[o++]=m,b=v,y=A,v=0,A=0,g=t[m][1]):(t[m][0]==n?v+=t[m][1].length:A+=t[m][1].length,g&&g.length<=Math.max(b,y)&&g.length<=Math.max(v,A)&&(t.splice(s[o-1],0,[e,g]),t[s[o-1]+1][0]=n,o--,m=--o>0?s[o-1]:-1,b=0,y=0,v=0,A=0,g=null,i=!0)),m++;for(i&&p(t),function(t){function e(t,e){if(!t||!e)return 6;var n=t.charAt(t.length-1),r=e.charAt(0),i=n.match(c),s=r.match(c),o=i&&n.match(u),l=s&&r.match(u),a=o&&n.match(h),p=l&&r.match(h),g=a&&t.match(d),m=p&&e.match(f);return g||m?5:a||p?4:i&&!o&&l?3:o||l?2:i||s?1:0}for(var n=1;n<t.length-1;){if(t[n-1][0]==r&&t[n+1][0]==r){var i=t[n-1][1],s=t[n][1],o=t[n+1][1],l=a(i,s);if(l){var p=s.substring(s.length-l);i=i.substring(0,i.length-l),s=p+s.substring(0,s.length-l),o=p+o}for(var g=i,m=s,b=o,y=e(i,s)+e(s,o);s.charAt(0)===o.charAt(0);){i+=s.charAt(0),s=s.substring(1)+o.charAt(0),o=o.substring(1);var v=e(i,s)+e(s,o);v>=y&&(y=v,g=i,m=s,b=o)}t[n-1][1]!=g&&(g?t[n-1][1]=g:(t.splice(n-1,1),n--),t[n][1]=m,b?t[n+1][1]=b:(t.splice(n+1,1),n--))}n++}}(t),m=1;m<t.length;){if(t[m-1][0]==e&&t[m][0]==n){var x=t[m-1][1],N=t[m][1],E=l(x,N),w=l(N,x);E>=w?(E>=x.length/2||E>=N.length/2)&&(t.splice(m,0,[r,N.substring(0,E)]),t[m-1][1]=x.substring(0,x.length-E),t[m+1][1]=N.substring(E),m++):(w>=x.length/2||w>=N.length/2)&&(t.splice(m,0,[r,x.substring(0,w)]),t[m-1][0]=n,t[m-1][1]=N.substring(0,N.length-w),t[m+1][0]=e,t[m+1][1]=x.substring(w),m++),m++}m++}}(w),w}function s(t,e,n,r){var s=t.substring(0,n),o=e.substring(0,r),l=t.substring(n),a=e.substring(r),c=i(s,o),u=i(l,a);return c.concat(u)}function o(t,e){if(!t||!e||t.charAt(0)!==e.charAt(0))return 0;for(var n=0,r=Math.min(t.length,e.length),i=r,s=0;n<i;)t.substring(s,i)==e.substring(s,i)?s=n=i:r=i,i=Math.floor((r-n)/2+n);return g(t.charCodeAt(i-1))&&i--,i}function l(t,e){var n=t.length,r=e.length;if(0==n||0==r)return 0;n>r?t=t.substring(n-r):n<r&&(e=e.substring(0,n));var i=Math.min(n,r);if(t==e)return i;for(var s=0,o=1;;){var l=t.substring(i-o),a=e.indexOf(l);if(-1==a)return s;o+=a,0!=a&&t.substring(i-o)!=e.substring(0,o)||(s=o,o++)}}function a(t,e){if(!t||!e||t.slice(-1)!==e.slice(-1))return 0;for(var n=0,r=Math.min(t.length,e.length),i=r,s=0;n<i;)t.substring(t.length-i,t.length-s)==e.substring(e.length-i,e.length-s)?s=n=i:r=i,i=Math.floor((r-n)/2+n);return m(t.charCodeAt(t.length-i))&&i--,i}var c=/[^a-zA-Z0-9]/,u=/\\s/,h=/[\\r\\n]/,d=/\\n\\r?\\n$/,f=/^\\r?\\n\\r?\\n/;function p(t,i){t.push([r,\"\"]);for(var s,l=0,c=0,u=0,h=\"\",d=\"\";l<t.length;)if(l<t.length-1&&!t[l][1])t.splice(l,1);else switch(t[l][0]){case n:u++,d+=t[l][1],l++;break;case e:c++,h+=t[l][1],l++;break;case r:var f=l-u-c-1;if(i){if(f>=0&&y(t[f][1])){var g=t[f][1].slice(-1);if(t[f][1]=t[f][1].slice(0,-1),h=g+h,d=g+d,!t[f][1]){t.splice(f,1),l--;var m=f-1;t[m]&&t[m][0]===n&&(u++,d=t[m][1]+d,m--),t[m]&&t[m][0]===e&&(c++,h=t[m][1]+h,m--),f=m}}b(t[l][1])&&(g=t[l][1].charAt(0),t[l][1]=t[l][1].slice(1),h+=g,d+=g)}if(l<t.length-1&&!t[l][1]){t.splice(l,1);break}if(h.length>0||d.length>0){h.length>0&&d.length>0&&(0!==(s=o(d,h))&&(f>=0?t[f][1]+=d.substring(0,s):(t.splice(0,0,[r,d.substring(0,s)]),l++),d=d.substring(s),h=h.substring(s)),0!==(s=a(d,h))&&(t[l][1]=d.substring(d.length-s)+t[l][1],d=d.substring(0,d.length-s),h=h.substring(0,h.length-s)));var v=u+c;0===h.length&&0===d.length?(t.splice(l-v,v),l-=v):0===h.length?(t.splice(l-v,v,[n,d]),l=l-v+1):0===d.length?(t.splice(l-v,v,[e,h]),l=l-v+1):(t.splice(l-v,v,[e,h],[n,d]),l=l-v+2)}0!==l&&t[l-1][0]===r?(t[l-1][1]+=t[l][1],t.splice(l,1)):l++,u=0,c=0,h=\"\",d=\"\"}\"\"===t[t.length-1][1]&&t.pop();var A=!1;for(l=1;l<t.length-1;)t[l-1][0]===r&&t[l+1][0]===r&&(t[l][1].substring(t[l][1].length-t[l-1][1].length)===t[l-1][1]?(t[l][1]=t[l-1][1]+t[l][1].substring(0,t[l][1].length-t[l-1][1].length),t[l+1][1]=t[l-1][1]+t[l+1][1],t.splice(l-1,1),A=!0):t[l][1].substring(0,t[l+1][1].length)==t[l+1][1]&&(t[l-1][1]+=t[l+1][1],t[l][1]=t[l][1].substring(t[l+1][1].length)+t[l+1][1],t.splice(l+1,1),A=!0)),l++;A&&p(t,i)}function g(t){return t>=55296&&t<=56319}function m(t){return t>=56320&&t<=57343}function b(t){return m(t.charCodeAt(0))}function y(t){return g(t.charCodeAt(t.length-1))}function v(t,i,s,o){return y(t)||b(o)?null:function(t){for(var e=[],n=0;n<t.length;n++)t[n][1].length>0&&e.push(t[n]);return e}([[r,t],[e,i],[n,s],[r,o]])}function A(t,e,n,r){return i(t,e,n,r,!0)}A.INSERT=n,A.DELETE=e,A.EQUAL=r,t.exports=A},9629:function(t,e,n){t=n.nmd(t);var r=\"__lodash_hash_undefined__\",i=9007199254740991,s=\"[object Arguments]\",o=\"[object Boolean]\",l=\"[object Date]\",a=\"[object Function]\",c=\"[object GeneratorFunction]\",u=\"[object Map]\",h=\"[object Number]\",d=\"[object Object]\",f=\"[object Promise]\",p=\"[object RegExp]\",g=\"[object Set]\",m=\"[object String]\",b=\"[object Symbol]\",y=\"[object WeakMap]\",v=\"[object ArrayBuffer]\",A=\"[object DataView]\",x=\"[object Float32Array]\",N=\"[object Float64Array]\",E=\"[object Int8Array]\",w=\"[object Int16Array]\",q=\"[object Int32Array]\",k=\"[object Uint8Array]\",_=\"[object Uint8ClampedArray]\",L=\"[object Uint16Array]\",S=\"[object Uint32Array]\",O=/\\w*$/,T=/^\\[object .+?Constructor\\]$/,j=/^(?:0|[1-9]\\d*)$/,C={};C[s]=C[\"[object Array]\"]=C[v]=C[A]=C[o]=C[l]=C[x]=C[N]=C[E]=C[w]=C[q]=C[u]=C[h]=C[d]=C[p]=C[g]=C[m]=C[b]=C[k]=C[_]=C[L]=C[S]=!0,C[\"[object Error]\"]=C[a]=C[y]=!1;var R=\"object\"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,I=\"object\"==typeof self&&self&&self.Object===Object&&self,B=R||I||Function(\"return this\")(),M=e&&!e.nodeType&&e,U=M&&t&&!t.nodeType&&t,D=U&&U.exports===M;function P(t,e){return t.set(e[0],e[1]),t}function z(t,e){return t.add(e),t}function F(t,e,n,r){var i=-1,s=t?t.length:0;for(r&&s&&(n=t[++i]);++i<s;)n=e(n,t[i],i,t);return n}function H(t){var e=!1;if(null!=t&&\"function\"!=typeof t.toString)try{e=!!(t+\"\")}catch(t){}return e}function $(t){var e=-1,n=Array(t.size);return t.forEach((function(t,r){n[++e]=[r,t]})),n}function V(t,e){return function(n){return t(e(n))}}function K(t){var e=-1,n=Array(t.size);return t.forEach((function(t){n[++e]=t})),n}var W,Z=Array.prototype,G=Function.prototype,X=Object.prototype,Q=B[\"__core-js_shared__\"],J=(W=/[^.]+$/.exec(Q&&Q.keys&&Q.keys.IE_PROTO||\"\"))?\"Symbol(src)_1.\"+W:\"\",Y=G.toString,tt=X.hasOwnProperty,et=X.toString,nt=RegExp(\"^\"+Y.call(tt).replace(/[\\\\^$.*+?()[\\]{}|]/g,\"\\\\$&\").replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g,\"$1.*?\")+\"$\"),rt=D?B.Buffer:void 0,it=B.Symbol,st=B.Uint8Array,ot=V(Object.getPrototypeOf,Object),lt=Object.create,at=X.propertyIsEnumerable,ct=Z.splice,ut=Object.getOwnPropertySymbols,ht=rt?rt.isBuffer:void 0,dt=V(Object.keys,Object),ft=Bt(B,\"DataView\"),pt=Bt(B,\"Map\"),gt=Bt(B,\"Promise\"),mt=Bt(B,\"Set\"),bt=Bt(B,\"WeakMap\"),yt=Bt(Object,\"create\"),vt=zt(ft),At=zt(pt),xt=zt(gt),Nt=zt(mt),Et=zt(bt),wt=it?it.prototype:void 0,qt=wt?wt.valueOf:void 0;function kt(t){var e=-1,n=t?t.length:0;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function _t(t){var e=-1,n=t?t.length:0;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function Lt(t){var e=-1,n=t?t.length:0;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function St(t){this.__data__=new _t(t)}function Ot(t,e,n){var r=t[e];tt.call(t,e)&&Ft(r,n)&&(void 0!==n||e in t)||(t[e]=n)}function Tt(t,e){for(var n=t.length;n--;)if(Ft(t[n][0],e))return n;return-1}function jt(t,e,n,r,i,f,y){var T;if(r&&(T=f?r(t,i,f,y):r(t)),void 0!==T)return T;if(!Wt(t))return t;var j=Ht(t);if(j){if(T=function(t){var e=t.length,n=t.constructor(e);return e&&\"string\"==typeof t[0]&&tt.call(t,\"index\")&&(n.index=t.index,n.input=t.input),n}(t),!e)return function(t,e){var n=-1,r=t.length;for(e||(e=Array(r));++n<r;)e[n]=t[n];return e}(t,T)}else{var R=Ut(t),I=R==a||R==c;if(Vt(t))return function(t,e){if(e)return t.slice();var n=new t.constructor(t.length);return t.copy(n),n}(t,e);if(R==d||R==s||I&&!f){if(H(t))return f?t:{};if(T=function(t){return\"function\"!=typeof t.constructor||Pt(t)?{}:Wt(e=ot(t))?lt(e):{};var e}(I?{}:t),!e)return function(t,e){return Rt(t,Mt(t),e)}(t,function(t,e){return t&&Rt(e,Zt(e),t)}(T,t))}else{if(!C[R])return f?t:{};T=function(t,e,n,r){var i,s=t.constructor;switch(e){case v:return Ct(t);case o:case l:return new s(+t);case A:return function(t,e){var n=e?Ct(t.buffer):t.buffer;return new t.constructor(n,t.byteOffset,t.byteLength)}(t,r);case x:case N:case E:case w:case q:case k:case _:case L:case S:return function(t,e){var n=e?Ct(t.buffer):t.buffer;return new t.constructor(n,t.byteOffset,t.length)}(t,r);case u:return function(t,e,n){return F(e?n($(t),!0):$(t),P,new t.constructor)}(t,r,n);case h:case m:return new s(t);case p:return function(t){var e=new t.constructor(t.source,O.exec(t));return e.lastIndex=t.lastIndex,e}(t);case g:return function(t,e,n){return F(e?n(K(t),!0):K(t),z,new t.constructor)}(t,r,n);case b:return i=t,qt?Object(qt.call(i)):{}}}(t,R,jt,e)}}y||(y=new St);var B=y.get(t);if(B)return B;if(y.set(t,T),!j)var M=n?function(t){return function(t,e,n){var r=e(t);return Ht(t)?r:function(t,e){for(var n=-1,r=e.length,i=t.length;++n<r;)t[i+n]=e[n];return t}(r,n(t))}(t,Zt,Mt)}(t):Zt(t);return function(t,e){for(var n=-1,r=t?t.length:0;++n<r&&!1!==e(t[n],n););}(M||t,(function(i,s){M&&(i=t[s=i]),Ot(T,s,jt(i,e,n,r,s,t,y))})),T}function Ct(t){var e=new t.constructor(t.byteLength);return new st(e).set(new st(t)),e}function Rt(t,e,n,r){n||(n={});for(var i=-1,s=e.length;++i<s;){var o=e[i],l=r?r(n[o],t[o],o,n,t):void 0;Ot(n,o,void 0===l?t[o]:l)}return n}function It(t,e){var n,r,i=t.__data__;return(\"string\"==(r=typeof(n=e))||\"number\"==r||\"symbol\"==r||\"boolean\"==r?\"__proto__\"!==n:null===n)?i[\"string\"==typeof e?\"string\":\"hash\"]:i.map}function Bt(t,e){var n=function(t,e){return null==t?void 0:t[e]}(t,e);return function(t){return!(!Wt(t)||(e=t,J&&J in e))&&(Kt(t)||H(t)?nt:T).test(zt(t));var e}(n)?n:void 0}kt.prototype.clear=function(){this.__data__=yt?yt(null):{}},kt.prototype.delete=function(t){return this.has(t)&&delete this.__data__[t]},kt.prototype.get=function(t){var e=this.__data__;if(yt){var n=e[t];return n===r?void 0:n}return tt.call(e,t)?e[t]:void 0},kt.prototype.has=function(t){var e=this.__data__;return yt?void 0!==e[t]:tt.call(e,t)},kt.prototype.set=function(t,e){return this.__data__[t]=yt&&void 0===e?r:e,this},_t.prototype.clear=function(){this.__data__=[]},_t.prototype.delete=function(t){var e=this.__data__,n=Tt(e,t);return!(n<0||(n==e.length-1?e.pop():ct.call(e,n,1),0))},_t.prototype.get=function(t){var e=this.__data__,n=Tt(e,t);return n<0?void 0:e[n][1]},_t.prototype.has=function(t){return Tt(this.__data__,t)>-1},_t.prototype.set=function(t,e){var n=this.__data__,r=Tt(n,t);return r<0?n.push([t,e]):n[r][1]=e,this},Lt.prototype.clear=function(){this.__data__={hash:new kt,map:new(pt||_t),string:new kt}},Lt.prototype.delete=function(t){return It(this,t).delete(t)},Lt.prototype.get=function(t){return It(this,t).get(t)},Lt.prototype.has=function(t){return It(this,t).has(t)},Lt.prototype.set=function(t,e){return It(this,t).set(t,e),this},St.prototype.clear=function(){this.__data__=new _t},St.prototype.delete=function(t){return this.__data__.delete(t)},St.prototype.get=function(t){return this.__data__.get(t)},St.prototype.has=function(t){return this.__data__.has(t)},St.prototype.set=function(t,e){var n=this.__data__;if(n instanceof _t){var r=n.__data__;if(!pt||r.length<199)return r.push([t,e]),this;n=this.__data__=new Lt(r)}return n.set(t,e),this};var Mt=ut?V(ut,Object):function(){return[]},Ut=function(t){return et.call(t)};function Dt(t,e){return!!(e=null==e?i:e)&&(\"number\"==typeof t||j.test(t))&&t>-1&&t%1==0&&t<e}function Pt(t){var e=t&&t.constructor;return t===(\"function\"==typeof e&&e.prototype||X)}function zt(t){if(null!=t){try{return Y.call(t)}catch(t){}try{return t+\"\"}catch(t){}}return\"\"}function Ft(t,e){return t===e||t!=t&&e!=e}(ft&&Ut(new ft(new ArrayBuffer(1)))!=A||pt&&Ut(new pt)!=u||gt&&Ut(gt.resolve())!=f||mt&&Ut(new mt)!=g||bt&&Ut(new bt)!=y)&&(Ut=function(t){var e=et.call(t),n=e==d?t.constructor:void 0,r=n?zt(n):void 0;if(r)switch(r){case vt:return A;case At:return u;case xt:return f;case Nt:return g;case Et:return y}return e});var Ht=Array.isArray;function $t(t){return null!=t&&function(t){return\"number\"==typeof t&&t>-1&&t%1==0&&t<=i}(t.length)&&!Kt(t)}var Vt=ht||function(){return!1};function Kt(t){var e=Wt(t)?et.call(t):\"\";return e==a||e==c}function Wt(t){var e=typeof t;return!!t&&(\"object\"==e||\"function\"==e)}function Zt(t){return $t(t)?function(t,e){var n=Ht(t)||function(t){return function(t){return function(t){return!!t&&\"object\"==typeof t}(t)&&$t(t)}(t)&&tt.call(t,\"callee\")&&(!at.call(t,\"callee\")||et.call(t)==s)}(t)?function(t,e){for(var n=-1,r=Array(t);++n<t;)r[n]=e(n);return r}(t.length,String):[],r=n.length,i=!!r;for(var o in t)!e&&!tt.call(t,o)||i&&(\"length\"==o||Dt(o,r))||n.push(o);return n}(t):function(t){if(!Pt(t))return dt(t);var e=[];for(var n in Object(t))tt.call(t,n)&&\"constructor\"!=n&&e.push(n);return e}(t)}t.exports=function(t){return jt(t,!0,!0)}},4162:function(t,e,n){t=n.nmd(t);var r=\"__lodash_hash_undefined__\",i=1,s=2,o=9007199254740991,l=\"[object Arguments]\",a=\"[object Array]\",c=\"[object AsyncFunction]\",u=\"[object Boolean]\",h=\"[object Date]\",d=\"[object Error]\",f=\"[object Function]\",p=\"[object GeneratorFunction]\",g=\"[object Map]\",m=\"[object Number]\",b=\"[object Null]\",y=\"[object Object]\",v=\"[object Promise]\",A=\"[object Proxy]\",x=\"[object RegExp]\",N=\"[object Set]\",E=\"[object String]\",w=\"[object Undefined]\",q=\"[object WeakMap]\",k=\"[object ArrayBuffer]\",_=\"[object DataView]\",L=/^\\[object .+?Constructor\\]$/,S=/^(?:0|[1-9]\\d*)$/,O={};O[\"[object Float32Array]\"]=O[\"[object Float64Array]\"]=O[\"[object Int8Array]\"]=O[\"[object Int16Array]\"]=O[\"[object Int32Array]\"]=O[\"[object Uint8Array]\"]=O[\"[object Uint8ClampedArray]\"]=O[\"[object Uint16Array]\"]=O[\"[object Uint32Array]\"]=!0,O[l]=O[a]=O[k]=O[u]=O[_]=O[h]=O[d]=O[f]=O[g]=O[m]=O[y]=O[x]=O[N]=O[E]=O[q]=!1;var T=\"object\"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,j=\"object\"==typeof self&&self&&self.Object===Object&&self,C=T||j||Function(\"return this\")(),R=e&&!e.nodeType&&e,I=R&&t&&!t.nodeType&&t,B=I&&I.exports===R,M=B&&T.process,U=function(){try{return M&&M.binding&&M.binding(\"util\")}catch(t){}}(),D=U&&U.isTypedArray;function P(t,e){for(var n=-1,r=null==t?0:t.length;++n<r;)if(e(t[n],n,t))return!0;return!1}function z(t){var e=-1,n=Array(t.size);return t.forEach((function(t,r){n[++e]=[r,t]})),n}function F(t){var e=-1,n=Array(t.size);return t.forEach((function(t){n[++e]=t})),n}var H,$,V,K=Array.prototype,W=Function.prototype,Z=Object.prototype,G=C[\"__core-js_shared__\"],X=W.toString,Q=Z.hasOwnProperty,J=(H=/[^.]+$/.exec(G&&G.keys&&G.keys.IE_PROTO||\"\"))?\"Symbol(src)_1.\"+H:\"\",Y=Z.toString,tt=RegExp(\"^\"+X.call(Q).replace(/[\\\\^$.*+?()[\\]{}|]/g,\"\\\\$&\").replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g,\"$1.*?\")+\"$\"),et=B?C.Buffer:void 0,nt=C.Symbol,rt=C.Uint8Array,it=Z.propertyIsEnumerable,st=K.splice,ot=nt?nt.toStringTag:void 0,lt=Object.getOwnPropertySymbols,at=et?et.isBuffer:void 0,ct=($=Object.keys,V=Object,function(t){return $(V(t))}),ut=It(C,\"DataView\"),ht=It(C,\"Map\"),dt=It(C,\"Promise\"),ft=It(C,\"Set\"),pt=It(C,\"WeakMap\"),gt=It(Object,\"create\"),mt=Dt(ut),bt=Dt(ht),yt=Dt(dt),vt=Dt(ft),At=Dt(pt),xt=nt?nt.prototype:void 0,Nt=xt?xt.valueOf:void 0;function Et(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function wt(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function qt(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function kt(t){var e=-1,n=null==t?0:t.length;for(this.__data__=new qt;++e<n;)this.add(t[e])}function _t(t){var e=this.__data__=new wt(t);this.size=e.size}function Lt(t,e){for(var n=t.length;n--;)if(Pt(t[n][0],e))return n;return-1}function St(t){return null==t?void 0===t?w:b:ot&&ot in Object(t)?function(t){var e=Q.call(t,ot),n=t[ot];try{t[ot]=void 0;var r=!0}catch(t){}var i=Y.call(t);return r&&(e?t[ot]=n:delete t[ot]),i}(t):function(t){return Y.call(t)}(t)}function Ot(t){return Wt(t)&&St(t)==l}function Tt(t,e,n,r,o){return t===e||(null==t||null==e||!Wt(t)&&!Wt(e)?t!=t&&e!=e:function(t,e,n,r,o,c){var f=Ft(t),p=Ft(e),b=f?a:Mt(t),v=p?a:Mt(e),A=(b=b==l?y:b)==y,w=(v=v==l?y:v)==y,q=b==v;if(q&&Ht(t)){if(!Ht(e))return!1;f=!0,A=!1}if(q&&!A)return c||(c=new _t),f||Zt(t)?jt(t,e,n,r,o,c):function(t,e,n,r,o,l,a){switch(n){case _:if(t.byteLength!=e.byteLength||t.byteOffset!=e.byteOffset)return!1;t=t.buffer,e=e.buffer;case k:return!(t.byteLength!=e.byteLength||!l(new rt(t),new rt(e)));case u:case h:case m:return Pt(+t,+e);case d:return t.name==e.name&&t.message==e.message;case x:case E:return t==e+\"\";case g:var c=z;case N:var f=r&i;if(c||(c=F),t.size!=e.size&&!f)return!1;var p=a.get(t);if(p)return p==e;r|=s,a.set(t,e);var b=jt(c(t),c(e),r,o,l,a);return a.delete(t),b;case\"[object Symbol]\":if(Nt)return Nt.call(t)==Nt.call(e)}return!1}(t,e,b,n,r,o,c);if(!(n&i)){var L=A&&Q.call(t,\"__wrapped__\"),S=w&&Q.call(e,\"__wrapped__\");if(L||S){var O=L?t.value():t,T=S?e.value():e;return c||(c=new _t),o(O,T,n,r,c)}}return!!q&&(c||(c=new _t),function(t,e,n,r,s,o){var l=n&i,a=Ct(t),c=a.length;if(c!=Ct(e).length&&!l)return!1;for(var u=c;u--;){var h=a[u];if(!(l?h in e:Q.call(e,h)))return!1}var d=o.get(t);if(d&&o.get(e))return d==e;var f=!0;o.set(t,e),o.set(e,t);for(var p=l;++u<c;){var g=t[h=a[u]],m=e[h];if(r)var b=l?r(m,g,h,e,t,o):r(g,m,h,t,e,o);if(!(void 0===b?g===m||s(g,m,n,r,o):b)){f=!1;break}p||(p=\"constructor\"==h)}if(f&&!p){var y=t.constructor,v=e.constructor;y==v||!(\"constructor\"in t)||!(\"constructor\"in e)||\"function\"==typeof y&&y instanceof y&&\"function\"==typeof v&&v instanceof v||(f=!1)}return o.delete(t),o.delete(e),f}(t,e,n,r,o,c))}(t,e,n,r,Tt,o))}function jt(t,e,n,r,o,l){var a=n&i,c=t.length,u=e.length;if(c!=u&&!(a&&u>c))return!1;var h=l.get(t);if(h&&l.get(e))return h==e;var d=-1,f=!0,p=n&s?new kt:void 0;for(l.set(t,e),l.set(e,t);++d<c;){var g=t[d],m=e[d];if(r)var b=a?r(m,g,d,e,t,l):r(g,m,d,t,e,l);if(void 0!==b){if(b)continue;f=!1;break}if(p){if(!P(e,(function(t,e){if(i=e,!p.has(i)&&(g===t||o(g,t,n,r,l)))return p.push(e);var i}))){f=!1;break}}else if(g!==m&&!o(g,m,n,r,l)){f=!1;break}}return l.delete(t),l.delete(e),f}function Ct(t){return function(t,e,n){var r=e(t);return Ft(t)?r:function(t,e){for(var n=-1,r=e.length,i=t.length;++n<r;)t[i+n]=e[n];return t}(r,n(t))}(t,Gt,Bt)}function Rt(t,e){var n,r,i=t.__data__;return(\"string\"==(r=typeof(n=e))||\"number\"==r||\"symbol\"==r||\"boolean\"==r?\"__proto__\"!==n:null===n)?i[\"string\"==typeof e?\"string\":\"hash\"]:i.map}function It(t,e){var n=function(t,e){return null==t?void 0:t[e]}(t,e);return function(t){return!(!Kt(t)||function(t){return!!J&&J in t}(t))&&($t(t)?tt:L).test(Dt(t))}(n)?n:void 0}Et.prototype.clear=function(){this.__data__=gt?gt(null):{},this.size=0},Et.prototype.delete=function(t){var e=this.has(t)&&delete this.__data__[t];return this.size-=e?1:0,e},Et.prototype.get=function(t){var e=this.__data__;if(gt){var n=e[t];return n===r?void 0:n}return Q.call(e,t)?e[t]:void 0},Et.prototype.has=function(t){var e=this.__data__;return gt?void 0!==e[t]:Q.call(e,t)},Et.prototype.set=function(t,e){var n=this.__data__;return this.size+=this.has(t)?0:1,n[t]=gt&&void 0===e?r:e,this},wt.prototype.clear=function(){this.__data__=[],this.size=0},wt.prototype.delete=function(t){var e=this.__data__,n=Lt(e,t);return!(n<0||(n==e.length-1?e.pop():st.call(e,n,1),--this.size,0))},wt.prototype.get=function(t){var e=this.__data__,n=Lt(e,t);return n<0?void 0:e[n][1]},wt.prototype.has=function(t){return Lt(this.__data__,t)>-1},wt.prototype.set=function(t,e){var n=this.__data__,r=Lt(n,t);return r<0?(++this.size,n.push([t,e])):n[r][1]=e,this},qt.prototype.clear=function(){this.size=0,this.__data__={hash:new Et,map:new(ht||wt),string:new Et}},qt.prototype.delete=function(t){var e=Rt(this,t).delete(t);return this.size-=e?1:0,e},qt.prototype.get=function(t){return Rt(this,t).get(t)},qt.prototype.has=function(t){return Rt(this,t).has(t)},qt.prototype.set=function(t,e){var n=Rt(this,t),r=n.size;return n.set(t,e),this.size+=n.size==r?0:1,this},kt.prototype.add=kt.prototype.push=function(t){return this.__data__.set(t,r),this},kt.prototype.has=function(t){return this.__data__.has(t)},_t.prototype.clear=function(){this.__data__=new wt,this.size=0},_t.prototype.delete=function(t){var e=this.__data__,n=e.delete(t);return this.size=e.size,n},_t.prototype.get=function(t){return this.__data__.get(t)},_t.prototype.has=function(t){return this.__data__.has(t)},_t.prototype.set=function(t,e){var n=this.__data__;if(n instanceof wt){var r=n.__data__;if(!ht||r.length<199)return r.push([t,e]),this.size=++n.size,this;n=this.__data__=new qt(r)}return n.set(t,e),this.size=n.size,this};var Bt=lt?function(t){return null==t?[]:(t=Object(t),function(e,n){for(var r=-1,i=null==e?0:e.length,s=0,o=[];++r<i;){var l=e[r];a=l,it.call(t,a)&&(o[s++]=l)}var a;return o}(lt(t)))}:function(){return[]},Mt=St;function Ut(t,e){return!!(e=null==e?o:e)&&(\"number\"==typeof t||S.test(t))&&t>-1&&t%1==0&&t<e}function Dt(t){if(null!=t){try{return X.call(t)}catch(t){}try{return t+\"\"}catch(t){}}return\"\"}function Pt(t,e){return t===e||t!=t&&e!=e}(ut&&Mt(new ut(new ArrayBuffer(1)))!=_||ht&&Mt(new ht)!=g||dt&&Mt(dt.resolve())!=v||ft&&Mt(new ft)!=N||pt&&Mt(new pt)!=q)&&(Mt=function(t){var e=St(t),n=e==y?t.constructor:void 0,r=n?Dt(n):\"\";if(r)switch(r){case mt:return _;case bt:return g;case yt:return v;case vt:return N;case At:return q}return e});var zt=Ot(function(){return arguments}())?Ot:function(t){return Wt(t)&&Q.call(t,\"callee\")&&!it.call(t,\"callee\")},Ft=Array.isArray,Ht=at||function(){return!1};function $t(t){if(!Kt(t))return!1;var e=St(t);return e==f||e==p||e==c||e==A}function Vt(t){return\"number\"==typeof t&&t>-1&&t%1==0&&t<=o}function Kt(t){var e=typeof t;return null!=t&&(\"object\"==e||\"function\"==e)}function Wt(t){return null!=t&&\"object\"==typeof t}var Zt=D?function(t){return function(e){return t(e)}}(D):function(t){return Wt(t)&&Vt(t.length)&&!!O[St(t)]};function Gt(t){return null!=(e=t)&&Vt(e.length)&&!$t(e)?function(t,e){var n=Ft(t),r=!n&&zt(t),i=!n&&!r&&Ht(t),s=!n&&!r&&!i&&Zt(t),o=n||r||i||s,l=o?function(t,e){for(var n=-1,r=Array(t);++n<t;)r[n]=e(n);return r}(t.length,String):[],a=l.length;for(var c in t)!e&&!Q.call(t,c)||o&&(\"length\"==c||i&&(\"offset\"==c||\"parent\"==c)||s&&(\"buffer\"==c||\"byteLength\"==c||\"byteOffset\"==c)||Ut(c,a))||l.push(c);return l}(t):function(t){if(n=(e=t)&&e.constructor,e!==(\"function\"==typeof n&&n.prototype||Z))return ct(t);var e,n,r=[];for(var i in Object(t))Q.call(t,i)&&\"constructor\"!=i&&r.push(i);return r}(t);var e}t.exports=function(t,e){return Tt(t,e)}},1270:function(t,e,n){\"use strict\";Object.defineProperty(e,\"__esModule\",{value:!0});const r=n(9629),i=n(4162);var s;!function(t){t.compose=function(t={},e={},n=!1){\"object\"!=typeof t&&(t={}),\"object\"!=typeof e&&(e={});let i=r(e);n||(i=Object.keys(i).reduce(((t,e)=>(null!=i[e]&&(t[e]=i[e]),t)),{}));for(const n in t)void 0!==t[n]&&void 0===e[n]&&(i[n]=t[n]);return Object.keys(i).length>0?i:void 0},t.diff=function(t={},e={}){\"object\"!=typeof t&&(t={}),\"object\"!=typeof e&&(e={});const n=Object.keys(t).concat(Object.keys(e)).reduce(((n,r)=>(i(t[r],e[r])||(n[r]=void 0===e[r]?null:e[r]),n)),{});return Object.keys(n).length>0?n:void 0},t.invert=function(t={},e={}){t=t||{};const n=Object.keys(e).reduce(((n,r)=>(e[r]!==t[r]&&void 0!==t[r]&&(n[r]=e[r]),n)),{});return Object.keys(t).reduce(((n,r)=>(t[r]!==e[r]&&void 0===e[r]&&(n[r]=null),n)),n)},t.transform=function(t,e,n=!1){if(\"object\"!=typeof t)return e;if(\"object\"!=typeof e)return;if(!n)return e;const r=Object.keys(e).reduce(((n,r)=>(void 0===t[r]&&(n[r]=e[r]),n)),{});return Object.keys(r).length>0?r:void 0}}(s||(s={})),e.default=s},5232:function(t,e,n){\"use strict\";Object.defineProperty(e,\"__esModule\",{value:!0}),e.AttributeMap=e.OpIterator=e.Op=void 0;const r=n(5090),i=n(9629),s=n(4162),o=n(1270);e.AttributeMap=o.default;const l=n(4123);e.Op=l.default;const a=n(7033);e.OpIterator=a.default;const c=String.fromCharCode(0),u=(t,e)=>{if(\"object\"!=typeof t||null===t)throw new Error(\"cannot retain a \"+typeof t);if(\"object\"!=typeof e||null===e)throw new Error(\"cannot retain a \"+typeof e);const n=Object.keys(t)[0];if(!n||n!==Object.keys(e)[0])throw new Error(`embed types not matched: ${n} != ${Object.keys(e)[0]}`);return[n,t[n],e[n]]};class h{constructor(t){Array.isArray(t)?this.ops=t:null!=t&&Array.isArray(t.ops)?this.ops=t.ops:this.ops=[]}static registerEmbed(t,e){this.handlers[t]=e}static unregisterEmbed(t){delete this.handlers[t]}static getHandler(t){const e=this.handlers[t];if(!e)throw new Error(`no handlers for embed type \"${t}\"`);return e}insert(t,e){const n={};return\"string\"==typeof t&&0===t.length?this:(n.insert=t,null!=e&&\"object\"==typeof e&&Object.keys(e).length>0&&(n.attributes=e),this.push(n))}delete(t){return t<=0?this:this.push({delete:t})}retain(t,e){if(\"number\"==typeof t&&t<=0)return this;const n={retain:t};return null!=e&&\"object\"==typeof e&&Object.keys(e).length>0&&(n.attributes=e),this.push(n)}push(t){let e=this.ops.length,n=this.ops[e-1];if(t=i(t),\"object\"==typeof n){if(\"number\"==typeof t.delete&&\"number\"==typeof n.delete)return this.ops[e-1]={delete:n.delete+t.delete},this;if(\"number\"==typeof n.delete&&null!=t.insert&&(e-=1,n=this.ops[e-1],\"object\"!=typeof n))return this.ops.unshift(t),this;if(s(t.attributes,n.attributes)){if(\"string\"==typeof t.insert&&\"string\"==typeof n.insert)return this.ops[e-1]={insert:n.insert+t.insert},\"object\"==typeof t.attributes&&(this.ops[e-1].attributes=t.attributes),this;if(\"number\"==typeof t.retain&&\"number\"==typeof n.retain)return this.ops[e-1]={retain:n.retain+t.retain},\"object\"==typeof t.attributes&&(this.ops[e-1].attributes=t.attributes),this}}return e===this.ops.length?this.ops.push(t):this.ops.splice(e,0,t),this}chop(){const t=this.ops[this.ops.length-1];return t&&\"number\"==typeof t.retain&&!t.attributes&&this.ops.pop(),this}filter(t){return this.ops.filter(t)}forEach(t){this.ops.forEach(t)}map(t){return this.ops.map(t)}partition(t){const e=[],n=[];return this.forEach((r=>{(t(r)?e:n).push(r)})),[e,n]}reduce(t,e){return this.ops.reduce(t,e)}changeLength(){return this.reduce(((t,e)=>e.insert?t+l.default.length(e):e.delete?t-e.delete:t),0)}length(){return this.reduce(((t,e)=>t+l.default.length(e)),0)}slice(t=0,e=1/0){const n=[],r=new a.default(this.ops);let i=0;for(;i<e&&r.hasNext();){let s;i<t?s=r.next(t-i):(s=r.next(e-i),n.push(s)),i+=l.default.length(s)}return new h(n)}compose(t){const e=new a.default(this.ops),n=new a.default(t.ops),r=[],i=n.peek();if(null!=i&&\"number\"==typeof i.retain&&null==i.attributes){let t=i.retain;for(;\"insert\"===e.peekType()&&e.peekLength()<=t;)t-=e.peekLength(),r.push(e.next());i.retain-t>0&&n.next(i.retain-t)}const l=new h(r);for(;e.hasNext()||n.hasNext();)if(\"insert\"===n.peekType())l.push(n.next());else if(\"delete\"===e.peekType())l.push(e.next());else{const t=Math.min(e.peekLength(),n.peekLength()),r=e.next(t),i=n.next(t);if(i.retain){const a={};if(\"number\"==typeof r.retain)a.retain=\"number\"==typeof i.retain?t:i.retain;else if(\"number\"==typeof i.retain)null==r.retain?a.insert=r.insert:a.retain=r.retain;else{const t=null==r.retain?\"insert\":\"retain\",[e,n,s]=u(r[t],i.retain),o=h.getHandler(e);a[t]={[e]:o.compose(n,s,\"retain\"===t)}}const c=o.default.compose(r.attributes,i.attributes,\"number\"==typeof r.retain);if(c&&(a.attributes=c),l.push(a),!n.hasNext()&&s(l.ops[l.ops.length-1],a)){const t=new h(e.rest());return l.concat(t).chop()}}else\"number\"==typeof i.delete&&(\"number\"==typeof r.retain||\"object\"==typeof r.retain&&null!==r.retain)&&l.push(i)}return l.chop()}concat(t){const e=new h(this.ops.slice());return t.ops.length>0&&(e.push(t.ops[0]),e.ops=e.ops.concat(t.ops.slice(1))),e}diff(t,e){if(this.ops===t.ops)return new h;const n=[this,t].map((e=>e.map((n=>{if(null!=n.insert)return\"string\"==typeof n.insert?n.insert:c;throw new Error(\"diff() called \"+(e===t?\"on\":\"with\")+\" non-document\")})).join(\"\"))),i=new h,l=r(n[0],n[1],e,!0),u=new a.default(this.ops),d=new a.default(t.ops);return l.forEach((t=>{let e=t[1].length;for(;e>0;){let n=0;switch(t[0]){case r.INSERT:n=Math.min(d.peekLength(),e),i.push(d.next(n));break;case r.DELETE:n=Math.min(e,u.peekLength()),u.next(n),i.delete(n);break;case r.EQUAL:n=Math.min(u.peekLength(),d.peekLength(),e);const t=u.next(n),l=d.next(n);s(t.insert,l.insert)?i.retain(n,o.default.diff(t.attributes,l.attributes)):i.push(l).delete(n)}e-=n}})),i.chop()}eachLine(t,e=\"\\n\"){const n=new a.default(this.ops);let r=new h,i=0;for(;n.hasNext();){if(\"insert\"!==n.peekType())return;const s=n.peek(),o=l.default.length(s)-n.peekLength(),a=\"string\"==typeof s.insert?s.insert.indexOf(e,o)-o:-1;if(a<0)r.push(n.next());else if(a>0)r.push(n.next(a));else{if(!1===t(r,n.next(1).attributes||{},i))return;i+=1,r=new h}}r.length()>0&&t(r,{},i)}invert(t){const e=new h;return this.reduce(((n,r)=>{if(r.insert)e.delete(l.default.length(r));else{if(\"number\"==typeof r.retain&&null==r.attributes)return e.retain(r.retain),n+r.retain;if(r.delete||\"number\"==typeof r.retain){const i=r.delete||r.retain;return t.slice(n,n+i).forEach((t=>{r.delete?e.push(t):r.retain&&r.attributes&&e.retain(l.default.length(t),o.default.invert(r.attributes,t.attributes))})),n+i}if(\"object\"==typeof r.retain&&null!==r.retain){const i=t.slice(n,n+1),s=new a.default(i.ops).next(),[l,c,d]=u(r.retain,s.insert),f=h.getHandler(l);return e.retain({[l]:f.invert(c,d)},o.default.invert(r.attributes,s.attributes)),n+1}}return n}),0),e.chop()}transform(t,e=!1){if(e=!!e,\"number\"==typeof t)return this.transformPosition(t,e);const n=t,r=new a.default(this.ops),i=new a.default(n.ops),s=new h;for(;r.hasNext()||i.hasNext();)if(\"insert\"!==r.peekType()||!e&&\"insert\"===i.peekType())if(\"insert\"===i.peekType())s.push(i.next());else{const t=Math.min(r.peekLength(),i.peekLength()),n=r.next(t),l=i.next(t);if(n.delete)continue;if(l.delete)s.push(l);else{const r=n.retain,i=l.retain;let a=\"object\"==typeof i&&null!==i?i:t;if(\"object\"==typeof r&&null!==r&&\"object\"==typeof i&&null!==i){const t=Object.keys(r)[0];if(t===Object.keys(i)[0]){const n=h.getHandler(t);n&&(a={[t]:n.transform(r[t],i[t],e)})}}s.retain(a,o.default.transform(n.attributes,l.attributes,e))}}else s.retain(l.default.length(r.next()));return s.chop()}transformPosition(t,e=!1){e=!!e;const n=new a.default(this.ops);let r=0;for(;n.hasNext()&&r<=t;){const i=n.peekLength(),s=n.peekType();n.next(),\"delete\"!==s?(\"insert\"===s&&(r<t||!e)&&(t+=i),r+=i):t-=Math.min(i,t-r)}return t}}h.Op=l.default,h.OpIterator=a.default,h.AttributeMap=o.default,h.handlers={},e.default=h,t.exports=h,t.exports.default=h},4123:function(t,e){\"use strict\";var n;Object.defineProperty(e,\"__esModule\",{value:!0}),function(t){t.length=function(t){return\"number\"==typeof t.delete?t.delete:\"number\"==typeof t.retain?t.retain:\"object\"==typeof t.retain&&null!==t.retain?1:\"string\"==typeof t.insert?t.insert.length:1}}(n||(n={})),e.default=n},7033:function(t,e,n){\"use strict\";Object.defineProperty(e,\"__esModule\",{value:!0});const r=n(4123);e.default=class{constructor(t){this.ops=t,this.index=0,this.offset=0}hasNext(){return this.peekLength()<1/0}next(t){t||(t=1/0);const e=this.ops[this.index];if(e){const n=this.offset,i=r.default.length(e);if(t>=i-n?(t=i-n,this.index+=1,this.offset=0):this.offset+=t,\"number\"==typeof e.delete)return{delete:t};{const r={};return e.attributes&&(r.attributes=e.attributes),\"number\"==typeof e.retain?r.retain=t:\"object\"==typeof e.retain&&null!==e.retain?r.retain=e.retain:\"string\"==typeof e.insert?r.insert=e.insert.substr(n,t):r.insert=e.insert,r}}return{retain:1/0}}peek(){return this.ops[this.index]}peekLength(){return this.ops[this.index]?r.default.length(this.ops[this.index])-this.offset:1/0}peekType(){const t=this.ops[this.index];return t?\"number\"==typeof t.delete?\"delete\":\"number\"==typeof t.retain||\"object\"==typeof t.retain&&null!==t.retain?\"retain\":\"insert\":\"retain\"}rest(){if(this.hasNext()){if(0===this.offset)return this.ops.slice(this.index);{const t=this.offset,e=this.index,n=this.next(),r=this.ops.slice(this.index);return this.offset=t,this.index=e,[n].concat(r)}}return[]}}},8820:function(t,e,n){\"use strict\";n.d(e,{A:function(){return l}});var r=n(8138),i=function(t,e){for(var n=t.length;n--;)if((0,r.A)(t[n][0],e))return n;return-1},s=Array.prototype.splice;function o(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}o.prototype.clear=function(){this.__data__=[],this.size=0},o.prototype.delete=function(t){var e=this.__data__,n=i(e,t);return!(n<0||(n==e.length-1?e.pop():s.call(e,n,1),--this.size,0))},o.prototype.get=function(t){var e=this.__data__,n=i(e,t);return n<0?void 0:e[n][1]},o.prototype.has=function(t){return i(this.__data__,t)>-1},o.prototype.set=function(t,e){var n=this.__data__,r=i(n,t);return r<0?(++this.size,n.push([t,e])):n[r][1]=e,this};var l=o},2461:function(t,e,n){\"use strict\";var r=n(2281),i=n(5507),s=(0,r.A)(i.A,\"Map\");e.A=s},3558:function(t,e,n){\"use strict\";n.d(e,{A:function(){return d}});var r=(0,n(2281).A)(Object,\"create\"),i=Object.prototype.hasOwnProperty,s=Object.prototype.hasOwnProperty;function o(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}o.prototype.clear=function(){this.__data__=r?r(null):{},this.size=0},o.prototype.delete=function(t){var e=this.has(t)&&delete this.__data__[t];return this.size-=e?1:0,e},o.prototype.get=function(t){var e=this.__data__;if(r){var n=e[t];return\"__lodash_hash_undefined__\"===n?void 0:n}return i.call(e,t)?e[t]:void 0},o.prototype.has=function(t){var e=this.__data__;return r?void 0!==e[t]:s.call(e,t)},o.prototype.set=function(t,e){var n=this.__data__;return this.size+=this.has(t)?0:1,n[t]=r&&void 0===e?\"__lodash_hash_undefined__\":e,this};var l=o,a=n(8820),c=n(2461),u=function(t,e){var n,r,i=t.__data__;return(\"string\"==(r=typeof(n=e))||\"number\"==r||\"symbol\"==r||\"boolean\"==r?\"__proto__\"!==n:null===n)?i[\"string\"==typeof e?\"string\":\"hash\"]:i.map};function h(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}h.prototype.clear=function(){this.size=0,this.__data__={hash:new l,map:new(c.A||a.A),string:new l}},h.prototype.delete=function(t){var e=u(this,t).delete(t);return this.size-=e?1:0,e},h.prototype.get=function(t){return u(this,t).get(t)},h.prototype.has=function(t){return u(this,t).has(t)},h.prototype.set=function(t,e){var n=u(this,t),r=n.size;return n.set(t,e),this.size+=n.size==r?0:1,this};var d=h},2673:function(t,e,n){\"use strict\";n.d(e,{A:function(){return l}});var r=n(8820),i=n(2461),s=n(3558);function o(t){var e=this.__data__=new r.A(t);this.size=e.size}o.prototype.clear=function(){this.__data__=new r.A,this.size=0},o.prototype.delete=function(t){var e=this.__data__,n=e.delete(t);return this.size=e.size,n},o.prototype.get=function(t){return this.__data__.get(t)},o.prototype.has=function(t){return this.__data__.has(t)},o.prototype.set=function(t,e){var n=this.__data__;if(n instanceof r.A){var o=n.__data__;if(!i.A||o.length<199)return o.push([t,e]),this.size=++n.size,this;n=this.__data__=new s.A(o)}return n.set(t,e),this.size=n.size,this};var l=o},439:function(t,e,n){\"use strict\";var r=n(5507).A.Symbol;e.A=r},7218:function(t,e,n){\"use strict\";var r=n(5507).A.Uint8Array;e.A=r},6753:function(t,e,n){\"use strict\";n.d(e,{A:function(){return c}});var r=n(8412),i=n(723),s=n(776),o=n(3767),l=n(5755),a=Object.prototype.hasOwnProperty,c=function(t,e){var n=(0,i.A)(t),c=!n&&(0,r.A)(t),u=!n&&!c&&(0,s.A)(t),h=!n&&!c&&!u&&(0,l.A)(t),d=n||c||u||h,f=d?function(t,e){for(var n=-1,r=Array(t);++n<t;)r[n]=e(n);return r}(t.length,String):[],p=f.length;for(var g in t)!e&&!a.call(t,g)||d&&(\"length\"==g||u&&(\"offset\"==g||\"parent\"==g)||h&&(\"buffer\"==g||\"byteLength\"==g||\"byteOffset\"==g)||(0,o.A)(g,p))||f.push(g);return f}},802:function(t,e){\"use strict\";e.A=function(t,e){for(var n=-1,r=e.length,i=t.length;++n<r;)t[i+n]=e[n];return t}},6437:function(t,e,n){\"use strict\";var r=n(6770),i=n(8138),s=Object.prototype.hasOwnProperty;e.A=function(t,e,n){var o=t[e];s.call(t,e)&&(0,i.A)(o,n)&&(void 0!==n||e in t)||(0,r.A)(t,e,n)}},6770:function(t,e,n){\"use strict\";var r=n(7889);e.A=function(t,e,n){\"__proto__\"==e&&r.A?(0,r.A)(t,e,{configurable:!0,enumerable:!0,value:n,writable:!0}):t[e]=n}},1381:function(t,e,n){\"use strict\";var r=n(802),i=n(723);e.A=function(t,e,n){var s=e(t);return(0,i.A)(t)?s:(0,r.A)(s,n(t))}},2159:function(t,e,n){\"use strict\";n.d(e,{A:function(){return u}});var r=n(439),i=Object.prototype,s=i.hasOwnProperty,o=i.toString,l=r.A?r.A.toStringTag:void 0,a=Object.prototype.toString,c=r.A?r.A.toStringTag:void 0,u=function(t){return null==t?void 0===t?\"[object Undefined]\":\"[object Null]\":c&&c in Object(t)?function(t){var e=s.call(t,l),n=t[l];try{t[l]=void 0;var r=!0}catch(t){}var i=o.call(t);return r&&(e?t[l]=n:delete t[l]),i}(t):function(t){return a.call(t)}(t)}},5771:function(t,e){\"use strict\";e.A=function(t){return function(e){return t(e)}}},2899:function(t,e,n){\"use strict\";var r=n(7218);e.A=function(t){var e=new t.constructor(t.byteLength);return new r.A(e).set(new r.A(t)),e}},3812:function(t,e,n){\"use strict\";var r=n(5507),i=\"object\"==typeof exports&&exports&&!exports.nodeType&&exports,s=i&&\"object\"==typeof module&&module&&!module.nodeType&&module,o=s&&s.exports===i?r.A.Buffer:void 0,l=o?o.allocUnsafe:void 0;e.A=function(t,e){if(e)return t.slice();var n=t.length,r=l?l(n):new t.constructor(n);return t.copy(r),r}},1827:function(t,e,n){\"use strict\";var r=n(2899);e.A=function(t,e){var n=e?(0,r.A)(t.buffer):t.buffer;return new t.constructor(n,t.byteOffset,t.length)}},4405:function(t,e){\"use strict\";e.A=function(t,e){var n=-1,r=t.length;for(e||(e=Array(r));++n<r;)e[n]=t[n];return e}},9601:function(t,e,n){\"use strict\";var r=n(6437),i=n(6770);e.A=function(t,e,n,s){var o=!n;n||(n={});for(var l=-1,a=e.length;++l<a;){var c=e[l],u=s?s(n[c],t[c],c,n,t):void 0;void 0===u&&(u=t[c]),o?(0,i.A)(n,c,u):(0,r.A)(n,c,u)}return n}},7889:function(t,e,n){\"use strict\";var r=n(2281),i=function(){try{var t=(0,r.A)(Object,\"defineProperty\");return t({},\"\",{}),t}catch(t){}}();e.A=i},9646:function(t,e){\"use strict\";var n=\"object\"==typeof global&&global&&global.Object===Object&&global;e.A=n},2816:function(t,e,n){\"use strict\";var r=n(1381),i=n(9844),s=n(3169);e.A=function(t){return(0,r.A)(t,s.A,i.A)}},2281:function(t,e,n){\"use strict\";n.d(e,{A:function(){return m}});var r,i=n(7572),s=n(5507).A[\"__core-js_shared__\"],o=(r=/[^.]+$/.exec(s&&s.keys&&s.keys.IE_PROTO||\"\"))?\"Symbol(src)_1.\"+r:\"\",l=n(659),a=n(1543),c=/^\\[object .+?Constructor\\]$/,u=Function.prototype,h=Object.prototype,d=u.toString,f=h.hasOwnProperty,p=RegExp(\"^\"+d.call(f).replace(/[\\\\^$.*+?()[\\]{}|]/g,\"\\\\$&\").replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g,\"$1.*?\")+\"$\"),g=function(t){return!(!(0,l.A)(t)||(e=t,o&&o in e))&&((0,i.A)(t)?p:c).test((0,a.A)(t));var e},m=function(t,e){var n=function(t,e){return null==t?void 0:t[e]}(t,e);return g(n)?n:void 0}},8769:function(t,e,n){\"use strict\";var r=(0,n(2217).A)(Object.getPrototypeOf,Object);e.A=r},9844:function(t,e,n){\"use strict\";n.d(e,{A:function(){return o}});var r=n(6935),i=Object.prototype.propertyIsEnumerable,s=Object.getOwnPropertySymbols,o=s?function(t){return null==t?[]:(t=Object(t),function(t,e){for(var n=-1,r=null==t?0:t.length,i=0,s=[];++n<r;){var o=t[n];e(o,n,t)&&(s[i++]=o)}return s}(s(t),(function(e){return i.call(t,e)})))}:r.A},7995:function(t,e,n){\"use strict\";n.d(e,{A:function(){return E}});var r=n(2281),i=n(5507),s=(0,r.A)(i.A,\"DataView\"),o=n(2461),l=(0,r.A)(i.A,\"Promise\"),a=(0,r.A)(i.A,\"Set\"),c=(0,r.A)(i.A,\"WeakMap\"),u=n(2159),h=n(1543),d=\"[object Map]\",f=\"[object Promise]\",p=\"[object Set]\",g=\"[object WeakMap]\",m=\"[object DataView]\",b=(0,h.A)(s),y=(0,h.A)(o.A),v=(0,h.A)(l),A=(0,h.A)(a),x=(0,h.A)(c),N=u.A;(s&&N(new s(new ArrayBuffer(1)))!=m||o.A&&N(new o.A)!=d||l&&N(l.resolve())!=f||a&&N(new a)!=p||c&&N(new c)!=g)&&(N=function(t){var e=(0,u.A)(t),n=\"[object Object]\"==e?t.constructor:void 0,r=n?(0,h.A)(n):\"\";if(r)switch(r){case b:return m;case y:return d;case v:return f;case A:return p;case x:return g}return e});var E=N},1683:function(t,e,n){\"use strict\";n.d(e,{A:function(){return a}});var r=n(659),i=Object.create,s=function(){function t(){}return function(e){if(!(0,r.A)(e))return{};if(i)return i(e);t.prototype=e;var n=new t;return t.prototype=void 0,n}}(),o=n(8769),l=n(501),a=function(t){return\"function\"!=typeof t.constructor||(0,l.A)(t)?{}:s((0,o.A)(t))}},3767:function(t,e){\"use strict\";var n=/^(?:0|[1-9]\\d*)$/;e.A=function(t,e){var r=typeof t;return!!(e=null==e?9007199254740991:e)&&(\"number\"==r||\"symbol\"!=r&&n.test(t))&&t>-1&&t%1==0&&t<e}},501:function(t,e){\"use strict\";var n=Object.prototype;e.A=function(t){var e=t&&t.constructor;return t===(\"function\"==typeof e&&e.prototype||n)}},8795:function(t,e,n){\"use strict\";var r=n(9646),i=\"object\"==typeof exports&&exports&&!exports.nodeType&&exports,s=i&&\"object\"==typeof module&&module&&!module.nodeType&&module,o=s&&s.exports===i&&r.A.process,l=function(){try{return s&&s.require&&s.require(\"util\").types||o&&o.binding&&o.binding(\"util\")}catch(t){}}();e.A=l},2217:function(t,e){\"use strict\";e.A=function(t,e){return function(n){return t(e(n))}}},5507:function(t,e,n){\"use strict\";var r=n(9646),i=\"object\"==typeof self&&self&&self.Object===Object&&self,s=r.A||i||Function(\"return this\")();e.A=s},1543:function(t,e){\"use strict\";var n=Function.prototype.toString;e.A=function(t){if(null!=t){try{return n.call(t)}catch(t){}try{return t+\"\"}catch(t){}}return\"\"}},3707:function(t,e,n){\"use strict\";n.d(e,{A:function(){return H}});var r=n(2673),i=n(6437),s=n(9601),o=n(3169),l=n(2624),a=n(3812),c=n(4405),u=n(9844),h=n(802),d=n(8769),f=n(6935),p=Object.getOwnPropertySymbols?function(t){for(var e=[];t;)(0,h.A)(e,(0,u.A)(t)),t=(0,d.A)(t);return e}:f.A,g=n(2816),m=n(1381),b=function(t){return(0,m.A)(t,l.A,p)},y=n(7995),v=Object.prototype.hasOwnProperty,A=n(2899),x=/\\w*$/,N=n(439),E=N.A?N.A.prototype:void 0,w=E?E.valueOf:void 0,q=n(1827),k=function(t,e,n){var r,i,s,o=t.constructor;switch(e){case\"[object ArrayBuffer]\":return(0,A.A)(t);case\"[object Boolean]\":case\"[object Date]\":return new o(+t);case\"[object DataView]\":return function(t,e){var n=e?(0,A.A)(t.buffer):t.buffer;return new t.constructor(n,t.byteOffset,t.byteLength)}(t,n);case\"[object Float32Array]\":case\"[object Float64Array]\":case\"[object Int8Array]\":case\"[object Int16Array]\":case\"[object Int32Array]\":case\"[object Uint8Array]\":case\"[object Uint8ClampedArray]\":case\"[object Uint16Array]\":case\"[object Uint32Array]\":return(0,q.A)(t,n);case\"[object Map]\":case\"[object Set]\":return new o;case\"[object Number]\":case\"[object String]\":return new o(t);case\"[object RegExp]\":return(s=new(i=t).constructor(i.source,x.exec(i))).lastIndex=i.lastIndex,s;case\"[object Symbol]\":return r=t,w?Object(w.call(r)):{}}},_=n(1683),L=n(723),S=n(776),O=n(7948),T=n(5771),j=n(8795),C=j.A&&j.A.isMap,R=C?(0,T.A)(C):function(t){return(0,O.A)(t)&&\"[object Map]\"==(0,y.A)(t)},I=n(659),B=j.A&&j.A.isSet,M=B?(0,T.A)(B):function(t){return(0,O.A)(t)&&\"[object Set]\"==(0,y.A)(t)},U=\"[object Arguments]\",D=\"[object Function]\",P=\"[object Object]\",z={};z[U]=z[\"[object Array]\"]=z[\"[object ArrayBuffer]\"]=z[\"[object DataView]\"]=z[\"[object Boolean]\"]=z[\"[object Date]\"]=z[\"[object Float32Array]\"]=z[\"[object Float64Array]\"]=z[\"[object Int8Array]\"]=z[\"[object Int16Array]\"]=z[\"[object Int32Array]\"]=z[\"[object Map]\"]=z[\"[object Number]\"]=z[P]=z[\"[object RegExp]\"]=z[\"[object Set]\"]=z[\"[object String]\"]=z[\"[object Symbol]\"]=z[\"[object Uint8Array]\"]=z[\"[object Uint8ClampedArray]\"]=z[\"[object Uint16Array]\"]=z[\"[object Uint32Array]\"]=!0,z[\"[object Error]\"]=z[D]=z[\"[object WeakMap]\"]=!1;var F=function t(e,n,h,d,f,m){var A,x=1&n,N=2&n,E=4&n;if(h&&(A=f?h(e,d,f,m):h(e)),void 0!==A)return A;if(!(0,I.A)(e))return e;var w=(0,L.A)(e);if(w){if(A=function(t){var e=t.length,n=new t.constructor(e);return e&&\"string\"==typeof t[0]&&v.call(t,\"index\")&&(n.index=t.index,n.input=t.input),n}(e),!x)return(0,c.A)(e,A)}else{var q=(0,y.A)(e),O=q==D||\"[object GeneratorFunction]\"==q;if((0,S.A)(e))return(0,a.A)(e,x);if(q==P||q==U||O&&!f){if(A=N||O?{}:(0,_.A)(e),!x)return N?function(t,e){return(0,s.A)(t,p(t),e)}(e,function(t,e){return t&&(0,s.A)(e,(0,l.A)(e),t)}(A,e)):function(t,e){return(0,s.A)(t,(0,u.A)(t),e)}(e,function(t,e){return t&&(0,s.A)(e,(0,o.A)(e),t)}(A,e))}else{if(!z[q])return f?e:{};A=k(e,q,x)}}m||(m=new r.A);var T=m.get(e);if(T)return T;m.set(e,A),M(e)?e.forEach((function(r){A.add(t(r,n,h,r,e,m))})):R(e)&&e.forEach((function(r,i){A.set(i,t(r,n,h,i,e,m))}));var j=E?N?b:g.A:N?l.A:o.A,C=w?void 0:j(e);return function(t,e){for(var n=-1,r=null==t?0:t.length;++n<r&&!1!==e(t[n],n,t););}(C||e,(function(r,s){C&&(r=e[s=r]),(0,i.A)(A,s,t(r,n,h,s,e,m))})),A},H=function(t){return F(t,5)}},8138:function(t,e){\"use strict\";e.A=function(t,e){return t===e||t!=t&&e!=e}},8412:function(t,e,n){\"use strict\";n.d(e,{A:function(){return u}});var r=n(2159),i=n(7948),s=function(t){return(0,i.A)(t)&&\"[object Arguments]\"==(0,r.A)(t)},o=Object.prototype,l=o.hasOwnProperty,a=o.propertyIsEnumerable,c=s(function(){return arguments}())?s:function(t){return(0,i.A)(t)&&l.call(t,\"callee\")&&!a.call(t,\"callee\")},u=c},723:function(t,e){\"use strict\";var n=Array.isArray;e.A=n},3628:function(t,e,n){\"use strict\";var r=n(7572),i=n(1628);e.A=function(t){return null!=t&&(0,i.A)(t.length)&&!(0,r.A)(t)}},776:function(t,e,n){\"use strict\";n.d(e,{A:function(){return l}});var r=n(5507),i=\"object\"==typeof exports&&exports&&!exports.nodeType&&exports,s=i&&\"object\"==typeof module&&module&&!module.nodeType&&module,o=s&&s.exports===i?r.A.Buffer:void 0,l=(o?o.isBuffer:void 0)||function(){return!1}},5123:function(t,e,n){\"use strict\";n.d(e,{A:function(){return S}});var r=n(2673),i=n(3558);function s(t){var e=-1,n=null==t?0:t.length;for(this.__data__=new i.A;++e<n;)this.add(t[e])}s.prototype.add=s.prototype.push=function(t){return this.__data__.set(t,\"__lodash_hash_undefined__\"),this},s.prototype.has=function(t){return this.__data__.has(t)};var o=s,l=function(t,e){for(var n=-1,r=null==t?0:t.length;++n<r;)if(e(t[n],n,t))return!0;return!1},a=function(t,e,n,r,i,s){var a=1&n,c=t.length,u=e.length;if(c!=u&&!(a&&u>c))return!1;var h=s.get(t),d=s.get(e);if(h&&d)return h==e&&d==t;var f=-1,p=!0,g=2&n?new o:void 0;for(s.set(t,e),s.set(e,t);++f<c;){var m=t[f],b=e[f];if(r)var y=a?r(b,m,f,e,t,s):r(m,b,f,t,e,s);if(void 0!==y){if(y)continue;p=!1;break}if(g){if(!l(e,(function(t,e){if(o=e,!g.has(o)&&(m===t||i(m,t,n,r,s)))return g.push(e);var o}))){p=!1;break}}else if(m!==b&&!i(m,b,n,r,s)){p=!1;break}}return s.delete(t),s.delete(e),p},c=n(439),u=n(7218),h=n(8138),d=function(t){var e=-1,n=Array(t.size);return t.forEach((function(t,r){n[++e]=[r,t]})),n},f=function(t){var e=-1,n=Array(t.size);return t.forEach((function(t){n[++e]=t})),n},p=c.A?c.A.prototype:void 0,g=p?p.valueOf:void 0,m=n(2816),b=Object.prototype.hasOwnProperty,y=n(7995),v=n(723),A=n(776),x=n(5755),N=\"[object Arguments]\",E=\"[object Array]\",w=\"[object Object]\",q=Object.prototype.hasOwnProperty,k=function(t,e,n,i,s,o){var l=(0,v.A)(t),c=(0,v.A)(e),p=l?E:(0,y.A)(t),k=c?E:(0,y.A)(e),_=(p=p==N?w:p)==w,L=(k=k==N?w:k)==w,S=p==k;if(S&&(0,A.A)(t)){if(!(0,A.A)(e))return!1;l=!0,_=!1}if(S&&!_)return o||(o=new r.A),l||(0,x.A)(t)?a(t,e,n,i,s,o):function(t,e,n,r,i,s,o){switch(n){case\"[object DataView]\":if(t.byteLength!=e.byteLength||t.byteOffset!=e.byteOffset)return!1;t=t.buffer,e=e.buffer;case\"[object ArrayBuffer]\":return!(t.byteLength!=e.byteLength||!s(new u.A(t),new u.A(e)));case\"[object Boolean]\":case\"[object Date]\":case\"[object Number]\":return(0,h.A)(+t,+e);case\"[object Error]\":return t.name==e.name&&t.message==e.message;case\"[object RegExp]\":case\"[object String]\":return t==e+\"\";case\"[object Map]\":var l=d;case\"[object Set]\":var c=1&r;if(l||(l=f),t.size!=e.size&&!c)return!1;var p=o.get(t);if(p)return p==e;r|=2,o.set(t,e);var m=a(l(t),l(e),r,i,s,o);return o.delete(t),m;case\"[object Symbol]\":if(g)return g.call(t)==g.call(e)}return!1}(t,e,p,n,i,s,o);if(!(1&n)){var O=_&&q.call(t,\"__wrapped__\"),T=L&&q.call(e,\"__wrapped__\");if(O||T){var j=O?t.value():t,C=T?e.value():e;return o||(o=new r.A),s(j,C,n,i,o)}}return!!S&&(o||(o=new r.A),function(t,e,n,r,i,s){var o=1&n,l=(0,m.A)(t),a=l.length;if(a!=(0,m.A)(e).length&&!o)return!1;for(var c=a;c--;){var u=l[c];if(!(o?u in e:b.call(e,u)))return!1}var h=s.get(t),d=s.get(e);if(h&&d)return h==e&&d==t;var f=!0;s.set(t,e),s.set(e,t);for(var p=o;++c<a;){var g=t[u=l[c]],y=e[u];if(r)var v=o?r(y,g,u,e,t,s):r(g,y,u,t,e,s);if(!(void 0===v?g===y||i(g,y,n,r,s):v)){f=!1;break}p||(p=\"constructor\"==u)}if(f&&!p){var A=t.constructor,x=e.constructor;A==x||!(\"constructor\"in t)||!(\"constructor\"in e)||\"function\"==typeof A&&A instanceof A&&\"function\"==typeof x&&x instanceof x||(f=!1)}return s.delete(t),s.delete(e),f}(t,e,n,i,s,o))},_=n(7948),L=function t(e,n,r,i,s){return e===n||(null==e||null==n||!(0,_.A)(e)&&!(0,_.A)(n)?e!=e&&n!=n:k(e,n,r,i,t,s))},S=function(t,e){return L(t,e)}},7572:function(t,e,n){\"use strict\";var r=n(2159),i=n(659);e.A=function(t){if(!(0,i.A)(t))return!1;var e=(0,r.A)(t);return\"[object Function]\"==e||\"[object GeneratorFunction]\"==e||\"[object AsyncFunction]\"==e||\"[object Proxy]\"==e}},1628:function(t,e){\"use strict\";e.A=function(t){return\"number\"==typeof t&&t>-1&&t%1==0&&t<=9007199254740991}},659:function(t,e){\"use strict\";e.A=function(t){var e=typeof t;return null!=t&&(\"object\"==e||\"function\"==e)}},7948:function(t,e){\"use strict\";e.A=function(t){return null!=t&&\"object\"==typeof t}},5755:function(t,e,n){\"use strict\";n.d(e,{A:function(){return u}});var r=n(2159),i=n(1628),s=n(7948),o={};o[\"[object Float32Array]\"]=o[\"[object Float64Array]\"]=o[\"[object Int8Array]\"]=o[\"[object Int16Array]\"]=o[\"[object Int32Array]\"]=o[\"[object Uint8Array]\"]=o[\"[object Uint8ClampedArray]\"]=o[\"[object Uint16Array]\"]=o[\"[object Uint32Array]\"]=!0,o[\"[object Arguments]\"]=o[\"[object Array]\"]=o[\"[object ArrayBuffer]\"]=o[\"[object Boolean]\"]=o[\"[object DataView]\"]=o[\"[object Date]\"]=o[\"[object Error]\"]=o[\"[object Function]\"]=o[\"[object Map]\"]=o[\"[object Number]\"]=o[\"[object Object]\"]=o[\"[object RegExp]\"]=o[\"[object Set]\"]=o[\"[object String]\"]=o[\"[object WeakMap]\"]=!1;var l=n(5771),a=n(8795),c=a.A&&a.A.isTypedArray,u=c?(0,l.A)(c):function(t){return(0,s.A)(t)&&(0,i.A)(t.length)&&!!o[(0,r.A)(t)]}},3169:function(t,e,n){\"use strict\";n.d(e,{A:function(){return a}});var r=n(6753),i=n(501),s=(0,n(2217).A)(Object.keys,Object),o=Object.prototype.hasOwnProperty,l=n(3628),a=function(t){return(0,l.A)(t)?(0,r.A)(t):function(t){if(!(0,i.A)(t))return s(t);var e=[];for(var n in Object(t))o.call(t,n)&&\"constructor\"!=n&&e.push(n);return e}(t)}},2624:function(t,e,n){\"use strict\";n.d(e,{A:function(){return c}});var r=n(6753),i=n(659),s=n(501),o=Object.prototype.hasOwnProperty,l=function(t){if(!(0,i.A)(t))return function(t){var e=[];if(null!=t)for(var n in Object(t))e.push(n);return e}(t);var e=(0,s.A)(t),n=[];for(var r in t)(\"constructor\"!=r||!e&&o.call(t,r))&&n.push(r);return n},a=n(3628),c=function(t){return(0,a.A)(t)?(0,r.A)(t,!0):l(t)}},8347:function(t,e,n){\"use strict\";n.d(e,{A:function(){return $}});var r,i,s,o,l=n(2673),a=n(6770),c=n(8138),u=function(t,e,n){(void 0!==n&&!(0,c.A)(t[e],n)||void 0===n&&!(e in t))&&(0,a.A)(t,e,n)},h=function(t,e,n){for(var r=-1,i=Object(t),s=n(t),o=s.length;o--;){var l=s[++r];if(!1===e(i[l],l,i))break}return t},d=n(3812),f=n(1827),p=n(4405),g=n(1683),m=n(8412),b=n(723),y=n(3628),v=n(7948),A=n(776),x=n(7572),N=n(659),E=n(2159),w=n(8769),q=Function.prototype,k=Object.prototype,_=q.toString,L=k.hasOwnProperty,S=_.call(Object),O=n(5755),T=function(t,e){if((\"constructor\"!==e||\"function\"!=typeof t[e])&&\"__proto__\"!=e)return t[e]},j=n(9601),C=n(2624),R=function(t,e,n,r,i,s,o){var l,a=T(t,n),c=T(e,n),h=o.get(c);if(h)u(t,n,h);else{var q=s?s(a,c,n+\"\",t,e,o):void 0,k=void 0===q;if(k){var R=(0,b.A)(c),I=!R&&(0,A.A)(c),B=!R&&!I&&(0,O.A)(c);q=c,R||I||B?(0,b.A)(a)?q=a:(l=a,(0,v.A)(l)&&(0,y.A)(l)?q=(0,p.A)(a):I?(k=!1,q=(0,d.A)(c,!0)):B?(k=!1,q=(0,f.A)(c,!0)):q=[]):function(t){if(!(0,v.A)(t)||\"[object Object]\"!=(0,E.A)(t))return!1;var e=(0,w.A)(t);if(null===e)return!0;var n=L.call(e,\"constructor\")&&e.constructor;return\"function\"==typeof n&&n instanceof n&&_.call(n)==S}(c)||(0,m.A)(c)?(q=a,(0,m.A)(a)?q=function(t){return(0,j.A)(t,(0,C.A)(t))}(a):(0,N.A)(a)&&!(0,x.A)(a)||(q=(0,g.A)(c))):k=!1}k&&(o.set(c,q),i(q,c,r,s,o),o.delete(c)),u(t,n,q)}},I=function t(e,n,r,i,s){e!==n&&h(n,(function(o,a){if(s||(s=new l.A),(0,N.A)(o))R(e,n,a,r,t,i,s);else{var c=i?i(T(e,a),o,a+\"\",e,n,s):void 0;void 0===c&&(c=o),u(e,a,c)}}),C.A)},B=function(t){return t},M=Math.max,U=n(7889),D=U.A?function(t,e){return(0,U.A)(t,\"toString\",{configurable:!0,enumerable:!1,value:(n=e,function(){return n}),writable:!0});var n}:B,P=Date.now,z=(r=D,i=0,s=0,function(){var t=P(),e=16-(t-s);if(s=t,e>0){if(++i>=800)return arguments[0]}else i=0;return r.apply(void 0,arguments)}),F=function(t,e){return z(function(t,e,n){return e=M(void 0===e?t.length-1:e,0),function(){for(var r=arguments,i=-1,s=M(r.length-e,0),o=Array(s);++i<s;)o[i]=r[e+i];i=-1;for(var l=Array(e+1);++i<e;)l[i]=r[i];return l[e]=n(o),function(t,e,n){switch(n.length){case 0:return t.call(e);case 1:return t.call(e,n[0]);case 2:return t.call(e,n[0],n[1]);case 3:return t.call(e,n[0],n[1],n[2])}return t.apply(e,n)}(t,this,l)}}(t,e,B),t+\"\")},H=n(3767),$=(o=function(t,e,n){I(t,e,n)},F((function(t,e){var n=-1,r=e.length,i=r>1?e[r-1]:void 0,s=r>2?e[2]:void 0;for(i=o.length>3&&\"function\"==typeof i?(r--,i):void 0,s&&function(t,e,n){if(!(0,N.A)(n))return!1;var r=typeof e;return!!(\"number\"==r?(0,y.A)(n)&&(0,H.A)(e,n.length):\"string\"==r&&e in n)&&(0,c.A)(n[e],t)}(e[0],e[1],s)&&(i=r<3?void 0:i,r=1),t=Object(t);++n<r;){var l=e[n];l&&o(t,l,n)}return t})))},6935:function(t,e){\"use strict\";e.A=function(){return[]}},6003:function(t,e,n){\"use strict\";n.r(e),n.d(e,{Attributor:function(){return i},AttributorStore:function(){return d},BlockBlot:function(){return w},ClassAttributor:function(){return c},ContainerBlot:function(){return k},EmbedBlot:function(){return _},InlineBlot:function(){return N},LeafBlot:function(){return m},ParentBlot:function(){return A},Registry:function(){return l},Scope:function(){return r},ScrollBlot:function(){return O},StyleAttributor:function(){return h},TextBlot:function(){return j}});var r=(t=>(t[t.TYPE=3]=\"TYPE\",t[t.LEVEL=12]=\"LEVEL\",t[t.ATTRIBUTE=13]=\"ATTRIBUTE\",t[t.BLOT=14]=\"BLOT\",t[t.INLINE=7]=\"INLINE\",t[t.BLOCK=11]=\"BLOCK\",t[t.BLOCK_BLOT=10]=\"BLOCK_BLOT\",t[t.INLINE_BLOT=6]=\"INLINE_BLOT\",t[t.BLOCK_ATTRIBUTE=9]=\"BLOCK_ATTRIBUTE\",t[t.INLINE_ATTRIBUTE=5]=\"INLINE_ATTRIBUTE\",t[t.ANY=15]=\"ANY\",t))(r||{});class i{constructor(t,e,n={}){this.attrName=t,this.keyName=e;const i=r.TYPE&r.ATTRIBUTE;this.scope=null!=n.scope?n.scope&r.LEVEL|i:r.ATTRIBUTE,null!=n.whitelist&&(this.whitelist=n.whitelist)}static keys(t){return Array.from(t.attributes).map((t=>t.name))}add(t,e){return!!this.canAdd(t,e)&&(t.setAttribute(this.keyName,e),!0)}canAdd(t,e){return null==this.whitelist||(\"string\"==typeof e?this.whitelist.indexOf(e.replace(/[\"']/g,\"\"))>-1:this.whitelist.indexOf(e)>-1)}remove(t){t.removeAttribute(this.keyName)}value(t){const e=t.getAttribute(this.keyName);return this.canAdd(t,e)&&e?e:\"\"}}class s extends Error{constructor(t){super(t=\"[Parchment] \"+t),this.message=t,this.name=this.constructor.name}}const o=class t{constructor(){this.attributes={},this.classes={},this.tags={},this.types={}}static find(t,e=!1){if(null==t)return null;if(this.blots.has(t))return this.blots.get(t)||null;if(e){let n=null;try{n=t.parentNode}catch{return null}return this.find(n,e)}return null}create(e,n,r){const i=this.query(n);if(null==i)throw new s(`Unable to create ${n} blot`);const o=i,l=n instanceof Node||n.nodeType===Node.TEXT_NODE?n:o.create(r),a=new o(e,l,r);return t.blots.set(a.domNode,a),a}find(e,n=!1){return t.find(e,n)}query(t,e=r.ANY){let n;return\"string\"==typeof t?n=this.types[t]||this.attributes[t]:t instanceof Text||t.nodeType===Node.TEXT_NODE?n=this.types.text:\"number\"==typeof t?t&r.LEVEL&r.BLOCK?n=this.types.block:t&r.LEVEL&r.INLINE&&(n=this.types.inline):t instanceof Element&&((t.getAttribute(\"class\")||\"\").split(/\\s+/).some((t=>(n=this.classes[t],!!n))),n=n||this.tags[t.tagName]),null==n?null:\"scope\"in n&&e&r.LEVEL&n.scope&&e&r.TYPE&n.scope?n:null}register(...t){return t.map((t=>{const e=\"blotName\"in t,n=\"attrName\"in t;if(!e&&!n)throw new s(\"Invalid definition\");if(e&&\"abstract\"===t.blotName)throw new s(\"Cannot register abstract class\");const r=e?t.blotName:n?t.attrName:void 0;return this.types[r]=t,n?\"string\"==typeof t.keyName&&(this.attributes[t.keyName]=t):e&&(t.className&&(this.classes[t.className]=t),t.tagName&&(Array.isArray(t.tagName)?t.tagName=t.tagName.map((t=>t.toUpperCase())):t.tagName=t.tagName.toUpperCase(),(Array.isArray(t.tagName)?t.tagName:[t.tagName]).forEach((e=>{(null==this.tags[e]||null==t.className)&&(this.tags[e]=t)})))),t}))}};o.blots=new WeakMap;let l=o;function a(t,e){return(t.getAttribute(\"class\")||\"\").split(/\\s+/).filter((t=>0===t.indexOf(`${e}-`)))}const c=class extends i{static keys(t){return(t.getAttribute(\"class\")||\"\").split(/\\s+/).map((t=>t.split(\"-\").slice(0,-1).join(\"-\")))}add(t,e){return!!this.canAdd(t,e)&&(this.remove(t),t.classList.add(`${this.keyName}-${e}`),!0)}remove(t){a(t,this.keyName).forEach((e=>{t.classList.remove(e)})),0===t.classList.length&&t.removeAttribute(\"class\")}value(t){const e=(a(t,this.keyName)[0]||\"\").slice(this.keyName.length+1);return this.canAdd(t,e)?e:\"\"}};function u(t){const e=t.split(\"-\"),n=e.slice(1).map((t=>t[0].toUpperCase()+t.slice(1))).join(\"\");return e[0]+n}const h=class extends i{static keys(t){return(t.getAttribute(\"style\")||\"\").split(\";\").map((t=>t.split(\":\")[0].trim()))}add(t,e){return!!this.canAdd(t,e)&&(t.style[u(this.keyName)]=e,!0)}remove(t){t.style[u(this.keyName)]=\"\",t.getAttribute(\"style\")||t.removeAttribute(\"style\")}value(t){const e=t.style[u(this.keyName)];return this.canAdd(t,e)?e:\"\"}},d=class{constructor(t){this.attributes={},this.domNode=t,this.build()}attribute(t,e){e?t.add(this.domNode,e)&&(null!=t.value(this.domNode)?this.attributes[t.attrName]=t:delete this.attributes[t.attrName]):(t.remove(this.domNode),delete this.attributes[t.attrName])}build(){this.attributes={};const t=l.find(this.domNode);if(null==t)return;const e=i.keys(this.domNode),n=c.keys(this.domNode),s=h.keys(this.domNode);e.concat(n).concat(s).forEach((e=>{const n=t.scroll.query(e,r.ATTRIBUTE);n instanceof i&&(this.attributes[n.attrName]=n)}))}copy(t){Object.keys(this.attributes).forEach((e=>{const n=this.attributes[e].value(this.domNode);t.format(e,n)}))}move(t){this.copy(t),Object.keys(this.attributes).forEach((t=>{this.attributes[t].remove(this.domNode)})),this.attributes={}}values(){return Object.keys(this.attributes).reduce(((t,e)=>(t[e]=this.attributes[e].value(this.domNode),t)),{})}},f=class{constructor(t,e){this.scroll=t,this.domNode=e,l.blots.set(e,this),this.prev=null,this.next=null}static create(t){if(null==this.tagName)throw new s(\"Blot definition missing tagName\");let e,n;return Array.isArray(this.tagName)?(\"string\"==typeof t?(n=t.toUpperCase(),parseInt(n,10).toString()===n&&(n=parseInt(n,10))):\"number\"==typeof t&&(n=t),e=\"number\"==typeof n?document.createElement(this.tagName[n-1]):n&&this.tagName.indexOf(n)>-1?document.createElement(n):document.createElement(this.tagName[0])):e=document.createElement(this.tagName),this.className&&e.classList.add(this.className),e}get statics(){return this.constructor}attach(){}clone(){const t=this.domNode.cloneNode(!1);return this.scroll.create(t)}detach(){null!=this.parent&&this.parent.removeChild(this),l.blots.delete(this.domNode)}deleteAt(t,e){this.isolate(t,e).remove()}formatAt(t,e,n,i){const s=this.isolate(t,e);if(null!=this.scroll.query(n,r.BLOT)&&i)s.wrap(n,i);else if(null!=this.scroll.query(n,r.ATTRIBUTE)){const t=this.scroll.create(this.statics.scope);s.wrap(t),t.format(n,i)}}insertAt(t,e,n){const r=null==n?this.scroll.create(\"text\",e):this.scroll.create(e,n),i=this.split(t);this.parent.insertBefore(r,i||void 0)}isolate(t,e){const n=this.split(t);if(null==n)throw new Error(\"Attempt to isolate at end\");return n.split(e),n}length(){return 1}offset(t=this.parent){return null==this.parent||this===t?0:this.parent.children.offset(this)+this.parent.offset(t)}optimize(t){this.statics.requiredContainer&&!(this.parent instanceof this.statics.requiredContainer)&&this.wrap(this.statics.requiredContainer.blotName)}remove(){null!=this.domNode.parentNode&&this.domNode.parentNode.removeChild(this.domNode),this.detach()}replaceWith(t,e){const n=\"string\"==typeof t?this.scroll.create(t,e):t;return null!=this.parent&&(this.parent.insertBefore(n,this.next||void 0),this.remove()),n}split(t,e){return 0===t?this:this.next}update(t,e){}wrap(t,e){const n=\"string\"==typeof t?this.scroll.create(t,e):t;if(null!=this.parent&&this.parent.insertBefore(n,this.next||void 0),\"function\"!=typeof n.appendChild)throw new s(`Cannot wrap ${t}`);return n.appendChild(this),n}};f.blotName=\"abstract\";let p=f;const g=class extends p{static value(t){return!0}index(t,e){return this.domNode===t||this.domNode.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_CONTAINED_BY?Math.min(e,1):-1}position(t,e){let n=Array.from(this.parent.domNode.childNodes).indexOf(this.domNode);return t>0&&(n+=1),[this.parent.domNode,n]}value(){return{[this.statics.blotName]:this.statics.value(this.domNode)||!0}}};g.scope=r.INLINE_BLOT;const m=g;class b{constructor(){this.head=null,this.tail=null,this.length=0}append(...t){if(this.insertBefore(t[0],null),t.length>1){const e=t.slice(1);this.append(...e)}}at(t){const e=this.iterator();let n=e();for(;n&&t>0;)t-=1,n=e();return n}contains(t){const e=this.iterator();let n=e();for(;n;){if(n===t)return!0;n=e()}return!1}indexOf(t){const e=this.iterator();let n=e(),r=0;for(;n;){if(n===t)return r;r+=1,n=e()}return-1}insertBefore(t,e){null!=t&&(this.remove(t),t.next=e,null!=e?(t.prev=e.prev,null!=e.prev&&(e.prev.next=t),e.prev=t,e===this.head&&(this.head=t)):null!=this.tail?(this.tail.next=t,t.prev=this.tail,this.tail=t):(t.prev=null,this.head=this.tail=t),this.length+=1)}offset(t){let e=0,n=this.head;for(;null!=n;){if(n===t)return e;e+=n.length(),n=n.next}return-1}remove(t){this.contains(t)&&(null!=t.prev&&(t.prev.next=t.next),null!=t.next&&(t.next.prev=t.prev),t===this.head&&(this.head=t.next),t===this.tail&&(this.tail=t.prev),this.length-=1)}iterator(t=this.head){return()=>{const e=t;return null!=t&&(t=t.next),e}}find(t,e=!1){const n=this.iterator();let r=n();for(;r;){const i=r.length();if(t<i||e&&t===i&&(null==r.next||0!==r.next.length()))return[r,t];t-=i,r=n()}return[null,0]}forEach(t){const e=this.iterator();let n=e();for(;n;)t(n),n=e()}forEachAt(t,e,n){if(e<=0)return;const[r,i]=this.find(t);let s=t-i;const o=this.iterator(r);let l=o();for(;l&&s<t+e;){const r=l.length();t>s?n(l,t-s,Math.min(e,s+r-t)):n(l,0,Math.min(r,t+e-s)),s+=r,l=o()}}map(t){return this.reduce(((e,n)=>(e.push(t(n)),e)),[])}reduce(t,e){const n=this.iterator();let r=n();for(;r;)e=t(e,r),r=n();return e}}function y(t,e){const n=e.find(t);if(n)return n;try{return e.create(t)}catch{const n=e.create(r.INLINE);return Array.from(t.childNodes).forEach((t=>{n.domNode.appendChild(t)})),t.parentNode&&t.parentNode.replaceChild(n.domNode,t),n.attach(),n}}const v=class t extends p{constructor(t,e){super(t,e),this.uiNode=null,this.build()}appendChild(t){this.insertBefore(t)}attach(){super.attach(),this.children.forEach((t=>{t.attach()}))}attachUI(e){null!=this.uiNode&&this.uiNode.remove(),this.uiNode=e,t.uiClass&&this.uiNode.classList.add(t.uiClass),this.uiNode.setAttribute(\"contenteditable\",\"false\"),this.domNode.insertBefore(this.uiNode,this.domNode.firstChild)}build(){this.children=new b,Array.from(this.domNode.childNodes).filter((t=>t!==this.uiNode)).reverse().forEach((t=>{try{const e=y(t,this.scroll);this.insertBefore(e,this.children.head||void 0)}catch(t){if(t instanceof s)return;throw t}}))}deleteAt(t,e){if(0===t&&e===this.length())return this.remove();this.children.forEachAt(t,e,((t,e,n)=>{t.deleteAt(e,n)}))}descendant(e,n=0){const[r,i]=this.children.find(n);return null==e.blotName&&e(r)||null!=e.blotName&&r instanceof e?[r,i]:r instanceof t?r.descendant(e,i):[null,-1]}descendants(e,n=0,r=Number.MAX_VALUE){let i=[],s=r;return this.children.forEachAt(n,r,((n,r,o)=>{(null==e.blotName&&e(n)||null!=e.blotName&&n instanceof e)&&i.push(n),n instanceof t&&(i=i.concat(n.descendants(e,r,s))),s-=o})),i}detach(){this.children.forEach((t=>{t.detach()})),super.detach()}enforceAllowedChildren(){let e=!1;this.children.forEach((n=>{e||this.statics.allowedChildren.some((t=>n instanceof t))||(n.statics.scope===r.BLOCK_BLOT?(null!=n.next&&this.splitAfter(n),null!=n.prev&&this.splitAfter(n.prev),n.parent.unwrap(),e=!0):n instanceof t?n.unwrap():n.remove())}))}formatAt(t,e,n,r){this.children.forEachAt(t,e,((t,e,i)=>{t.formatAt(e,i,n,r)}))}insertAt(t,e,n){const[r,i]=this.children.find(t);if(r)r.insertAt(i,e,n);else{const t=null==n?this.scroll.create(\"text\",e):this.scroll.create(e,n);this.appendChild(t)}}insertBefore(t,e){null!=t.parent&&t.parent.children.remove(t);let n=null;this.children.insertBefore(t,e||null),t.parent=this,null!=e&&(n=e.domNode),(this.domNode.parentNode!==t.domNode||this.domNode.nextSibling!==n)&&this.domNode.insertBefore(t.domNode,n),t.attach()}length(){return this.children.reduce(((t,e)=>t+e.length()),0)}moveChildren(t,e){this.children.forEach((n=>{t.insertBefore(n,e)}))}optimize(t){if(super.optimize(t),this.enforceAllowedChildren(),null!=this.uiNode&&this.uiNode!==this.domNode.firstChild&&this.domNode.insertBefore(this.uiNode,this.domNode.firstChild),0===this.children.length)if(null!=this.statics.defaultChild){const t=this.scroll.create(this.statics.defaultChild.blotName);this.appendChild(t)}else this.remove()}path(e,n=!1){const[r,i]=this.children.find(e,n),s=[[this,e]];return r instanceof t?s.concat(r.path(i,n)):(null!=r&&s.push([r,i]),s)}removeChild(t){this.children.remove(t)}replaceWith(e,n){const r=\"string\"==typeof e?this.scroll.create(e,n):e;return r instanceof t&&this.moveChildren(r),super.replaceWith(r)}split(t,e=!1){if(!e){if(0===t)return this;if(t===this.length())return this.next}const n=this.clone();return this.parent&&this.parent.insertBefore(n,this.next||void 0),this.children.forEachAt(t,this.length(),((t,r,i)=>{const s=t.split(r,e);null!=s&&n.appendChild(s)})),n}splitAfter(t){const e=this.clone();for(;null!=t.next;)e.appendChild(t.next);return this.parent&&this.parent.insertBefore(e,this.next||void 0),e}unwrap(){this.parent&&this.moveChildren(this.parent,this.next||void 0),this.remove()}update(t,e){const n=[],r=[];t.forEach((t=>{t.target===this.domNode&&\"childList\"===t.type&&(n.push(...t.addedNodes),r.push(...t.removedNodes))})),r.forEach((t=>{if(null!=t.parentNode&&\"IFRAME\"!==t.tagName&&document.body.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_CONTAINED_BY)return;const e=this.scroll.find(t);null!=e&&(null==e.domNode.parentNode||e.domNode.parentNode===this.domNode)&&e.detach()})),n.filter((t=>t.parentNode===this.domNode&&t!==this.uiNode)).sort(((t,e)=>t===e?0:t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?1:-1)).forEach((t=>{let e=null;null!=t.nextSibling&&(e=this.scroll.find(t.nextSibling));const n=y(t,this.scroll);(n.next!==e||null==n.next)&&(null!=n.parent&&n.parent.removeChild(this),this.insertBefore(n,e||void 0))})),this.enforceAllowedChildren()}};v.uiClass=\"\";const A=v,x=class t extends A{static create(t){return super.create(t)}static formats(e,n){const r=n.query(t.blotName);if(null==r||e.tagName!==r.tagName){if(\"string\"==typeof this.tagName)return!0;if(Array.isArray(this.tagName))return e.tagName.toLowerCase()}}constructor(t,e){super(t,e),this.attributes=new d(this.domNode)}format(e,n){if(e!==this.statics.blotName||n){const t=this.scroll.query(e,r.INLINE);if(null==t)return;t instanceof i?this.attributes.attribute(t,n):n&&(e!==this.statics.blotName||this.formats()[e]!==n)&&this.replaceWith(e,n)}else this.children.forEach((e=>{e instanceof t||(e=e.wrap(t.blotName,!0)),this.attributes.copy(e)})),this.unwrap()}formats(){const t=this.attributes.values(),e=this.statics.formats(this.domNode,this.scroll);return null!=e&&(t[this.statics.blotName]=e),t}formatAt(t,e,n,i){null!=this.formats()[n]||this.scroll.query(n,r.ATTRIBUTE)?this.isolate(t,e).format(n,i):super.formatAt(t,e,n,i)}optimize(e){super.optimize(e);const n=this.formats();if(0===Object.keys(n).length)return this.unwrap();const r=this.next;r instanceof t&&r.prev===this&&function(t,e){if(Object.keys(t).length!==Object.keys(e).length)return!1;for(const n in t)if(t[n]!==e[n])return!1;return!0}(n,r.formats())&&(r.moveChildren(this),r.remove())}replaceWith(t,e){const n=super.replaceWith(t,e);return this.attributes.copy(n),n}update(t,e){super.update(t,e),t.some((t=>t.target===this.domNode&&\"attributes\"===t.type))&&this.attributes.build()}wrap(e,n){const r=super.wrap(e,n);return r instanceof t&&this.attributes.move(r),r}};x.allowedChildren=[x,m],x.blotName=\"inline\",x.scope=r.INLINE_BLOT,x.tagName=\"SPAN\";const N=x,E=class t extends A{static create(t){return super.create(t)}static formats(e,n){const r=n.query(t.blotName);if(null==r||e.tagName!==r.tagName){if(\"string\"==typeof this.tagName)return!0;if(Array.isArray(this.tagName))return e.tagName.toLowerCase()}}constructor(t,e){super(t,e),this.attributes=new d(this.domNode)}format(e,n){const s=this.scroll.query(e,r.BLOCK);null!=s&&(s instanceof i?this.attributes.attribute(s,n):e!==this.statics.blotName||n?n&&(e!==this.statics.blotName||this.formats()[e]!==n)&&this.replaceWith(e,n):this.replaceWith(t.blotName))}formats(){const t=this.attributes.values(),e=this.statics.formats(this.domNode,this.scroll);return null!=e&&(t[this.statics.blotName]=e),t}formatAt(t,e,n,i){null!=this.scroll.query(n,r.BLOCK)?this.format(n,i):super.formatAt(t,e,n,i)}insertAt(t,e,n){if(null==n||null!=this.scroll.query(e,r.INLINE))super.insertAt(t,e,n);else{const r=this.split(t);if(null==r)throw new Error(\"Attempt to insertAt after block boundaries\");{const t=this.scroll.create(e,n);r.parent.insertBefore(t,r)}}}replaceWith(t,e){const n=super.replaceWith(t,e);return this.attributes.copy(n),n}update(t,e){super.update(t,e),t.some((t=>t.target===this.domNode&&\"attributes\"===t.type))&&this.attributes.build()}};E.blotName=\"block\",E.scope=r.BLOCK_BLOT,E.tagName=\"P\",E.allowedChildren=[N,E,m];const w=E,q=class extends A{checkMerge(){return null!==this.next&&this.next.statics.blotName===this.statics.blotName}deleteAt(t,e){super.deleteAt(t,e),this.enforceAllowedChildren()}formatAt(t,e,n,r){super.formatAt(t,e,n,r),this.enforceAllowedChildren()}insertAt(t,e,n){super.insertAt(t,e,n),this.enforceAllowedChildren()}optimize(t){super.optimize(t),this.children.length>0&&null!=this.next&&this.checkMerge()&&(this.next.moveChildren(this),this.next.remove())}};q.blotName=\"container\",q.scope=r.BLOCK_BLOT;const k=q,_=class extends m{static formats(t,e){}format(t,e){super.formatAt(0,this.length(),t,e)}formatAt(t,e,n,r){0===t&&e===this.length()?this.format(n,r):super.formatAt(t,e,n,r)}formats(){return this.statics.formats(this.domNode,this.scroll)}},L={attributes:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0},S=class extends A{constructor(t,e){super(null,e),this.registry=t,this.scroll=this,this.build(),this.observer=new MutationObserver((t=>{this.update(t)})),this.observer.observe(this.domNode,L),this.attach()}create(t,e){return this.registry.create(this,t,e)}find(t,e=!1){const n=this.registry.find(t,e);return n?n.scroll===this?n:e?this.find(n.scroll.domNode.parentNode,!0):null:null}query(t,e=r.ANY){return this.registry.query(t,e)}register(...t){return this.registry.register(...t)}build(){null!=this.scroll&&super.build()}detach(){super.detach(),this.observer.disconnect()}deleteAt(t,e){this.update(),0===t&&e===this.length()?this.children.forEach((t=>{t.remove()})):super.deleteAt(t,e)}formatAt(t,e,n,r){this.update(),super.formatAt(t,e,n,r)}insertAt(t,e,n){this.update(),super.insertAt(t,e,n)}optimize(t=[],e={}){super.optimize(e);const n=e.mutationsMap||new WeakMap;let r=Array.from(this.observer.takeRecords());for(;r.length>0;)t.push(r.pop());const i=(t,e=!0)=>{null==t||t===this||null!=t.domNode.parentNode&&(n.has(t.domNode)||n.set(t.domNode,[]),e&&i(t.parent))},s=t=>{n.has(t.domNode)&&(t instanceof A&&t.children.forEach(s),n.delete(t.domNode),t.optimize(e))};let o=t;for(let e=0;o.length>0;e+=1){if(e>=100)throw new Error(\"[Parchment] Maximum optimize iterations reached\");for(o.forEach((t=>{const e=this.find(t.target,!0);null!=e&&(e.domNode===t.target&&(\"childList\"===t.type?(i(this.find(t.previousSibling,!1)),Array.from(t.addedNodes).forEach((t=>{const e=this.find(t,!1);i(e,!1),e instanceof A&&e.children.forEach((t=>{i(t,!1)}))}))):\"attributes\"===t.type&&i(e.prev)),i(e))})),this.children.forEach(s),o=Array.from(this.observer.takeRecords()),r=o.slice();r.length>0;)t.push(r.pop())}}update(t,e={}){t=t||this.observer.takeRecords();const n=new WeakMap;t.map((t=>{const e=this.find(t.target,!0);return null==e?null:n.has(e.domNode)?(n.get(e.domNode).push(t),null):(n.set(e.domNode,[t]),e)})).forEach((t=>{null!=t&&t!==this&&n.has(t.domNode)&&t.update(n.get(t.domNode)||[],e)})),e.mutationsMap=n,n.has(this.domNode)&&super.update(n.get(this.domNode),e),this.optimize(t,e)}};S.blotName=\"scroll\",S.defaultChild=w,S.allowedChildren=[w,k],S.scope=r.BLOCK_BLOT,S.tagName=\"DIV\";const O=S,T=class t extends m{static create(t){return document.createTextNode(t)}static value(t){return t.data}constructor(t,e){super(t,e),this.text=this.statics.value(this.domNode)}deleteAt(t,e){this.domNode.data=this.text=this.text.slice(0,t)+this.text.slice(t+e)}index(t,e){return this.domNode===t?e:-1}insertAt(t,e,n){null==n?(this.text=this.text.slice(0,t)+e+this.text.slice(t),this.domNode.data=this.text):super.insertAt(t,e,n)}length(){return this.text.length}optimize(e){super.optimize(e),this.text=this.statics.value(this.domNode),0===this.text.length?this.remove():this.next instanceof t&&this.next.prev===this&&(this.insertAt(this.length(),this.next.value()),this.next.remove())}position(t,e=!1){return[this.domNode,t]}split(t,e=!1){if(!e){if(0===t)return this;if(t===this.length())return this.next}const n=this.scroll.create(this.domNode.splitText(t));return this.parent.insertBefore(n,this.next||void 0),this.text=this.statics.value(this.domNode),n}update(t,e){t.some((t=>\"characterData\"===t.type&&t.target===this.domNode))&&(this.text=this.statics.value(this.domNode))}value(){return this.text}};T.blotName=\"text\",T.scope=r.INLINE_BLOT;const j=T}},e={};function n(r){var i=e[r];if(void 0!==i)return i.exports;var s=e[r]={id:r,loaded:!1,exports:{}};return t[r](s,s.exports,n),s.loaded=!0,s.exports}n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,{a:e}),e},n.d=function(t,e){for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.g=function(){if(\"object\"==typeof globalThis)return globalThis;try{return this||new Function(\"return this\")()}catch(t){if(\"object\"==typeof window)return window}}(),n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.r=function(t){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})},n.nmd=function(t){return t.paths=[],t.children||(t.children=[]),t};var r={};return function(){\"use strict\";n.d(r,{default:function(){return It}});var t=n(3729),e=n(8276),i=n(7912),s=n(6003);class o extends s.ClassAttributor{add(t,e){let n=0;if(\"+1\"===e||\"-1\"===e){const r=this.value(t)||0;n=\"+1\"===e?r+1:r-1}else\"number\"==typeof e&&(n=e);return 0===n?(this.remove(t),!0):super.add(t,n.toString())}canAdd(t,e){return super.canAdd(t,e)||super.canAdd(t,parseInt(e,10))}value(t){return parseInt(super.value(t),10)||void 0}}var l=new o(\"indent\",\"ql-indent\",{scope:s.Scope.BLOCK,whitelist:[1,2,3,4,5,6,7,8]}),a=n(9698);class c extends a.Ay{static blotName=\"blockquote\";static tagName=\"blockquote\"}var u=c;class h extends a.Ay{static blotName=\"header\";static tagName=[\"H1\",\"H2\",\"H3\",\"H4\",\"H5\",\"H6\"];static formats(t){return this.tagName.indexOf(t.tagName)+1}}var d=h,f=n(580),p=n(6142);class g extends f.A{}g.blotName=\"list-container\",g.tagName=\"OL\";class m extends a.Ay{static create(t){const e=super.create();return e.setAttribute(\"data-list\",t),e}static formats(t){return t.getAttribute(\"data-list\")||void 0}static register(){p.Ay.register(g)}constructor(t,e){super(t,e);const n=e.ownerDocument.createElement(\"span\"),r=n=>{if(!t.isEnabled())return;const r=this.statics.formats(e,t);\"checked\"===r?(this.format(\"list\",\"unchecked\"),n.preventDefault()):\"unchecked\"===r&&(this.format(\"list\",\"checked\"),n.preventDefault())};n.addEventListener(\"mousedown\",r),n.addEventListener(\"touchstart\",r),this.attachUI(n)}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute(\"data-list\",e):super.format(t,e)}}m.blotName=\"list\",m.tagName=\"LI\",g.allowedChildren=[m],m.requiredContainer=g;var b=n(9541),y=n(8638),v=n(6772),A=n(664),x=n(4850);class N extends x.A{static blotName=\"bold\";static tagName=[\"STRONG\",\"B\"];static create(){return super.create()}static formats(){return!0}optimize(t){super.optimize(t),this.domNode.tagName!==this.statics.tagName[0]&&this.replaceWith(this.statics.blotName)}}var E=N;class w extends x.A{static blotName=\"link\";static tagName=\"A\";static SANITIZED_URL=\"about:blank\";static PROTOCOL_WHITELIST=[\"http\",\"https\",\"mailto\",\"tel\",\"sms\"];static create(t){const e=super.create(t);return e.setAttribute(\"href\",this.sanitize(t)),e.setAttribute(\"rel\",\"noopener noreferrer\"),e.setAttribute(\"target\",\"_blank\"),e}static formats(t){return t.getAttribute(\"href\")}static sanitize(t){return q(t,this.PROTOCOL_WHITELIST)?t:this.SANITIZED_URL}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute(\"href\",this.constructor.sanitize(e)):super.format(t,e)}}function q(t,e){const n=document.createElement(\"a\");n.href=t;const r=n.href.slice(0,n.href.indexOf(\":\"));return e.indexOf(r)>-1}class k extends x.A{static blotName=\"script\";static tagName=[\"SUB\",\"SUP\"];static create(t){return\"super\"===t?document.createElement(\"sup\"):\"sub\"===t?document.createElement(\"sub\"):super.create(t)}static formats(t){return\"SUB\"===t.tagName?\"sub\":\"SUP\"===t.tagName?\"super\":void 0}}var _=k;class L extends x.A{static blotName=\"underline\";static tagName=\"U\"}var S=L,O=n(746);class T extends O.A{static blotName=\"formula\";static className=\"ql-formula\";static tagName=\"SPAN\";static create(t){if(null==window.katex)throw new Error(\"Formula module requires KaTeX.\");const e=super.create(t);return\"string\"==typeof t&&(window.katex.render(t,e,{throwOnError:!1,errorColor:\"#f00\"}),e.setAttribute(\"data-value\",t)),e}static value(t){return t.getAttribute(\"data-value\")}html(){const{formula:t}=this.value();return`<span>${t}</span>`}}var j=T;const C=[\"alt\",\"height\",\"width\"];class R extends s.EmbedBlot{static blotName=\"image\";static tagName=\"IMG\";static create(t){const e=super.create(t);return\"string\"==typeof t&&e.setAttribute(\"src\",this.sanitize(t)),e}static formats(t){return C.reduce(((e,n)=>(t.hasAttribute(n)&&(e[n]=t.getAttribute(n)),e)),{})}static match(t){return/\\.(jpe?g|gif|png)$/.test(t)||/^data:image\\/.+;base64/.test(t)}static sanitize(t){return q(t,[\"http\",\"https\",\"data\"])?t:\"//:0\"}static value(t){return t.getAttribute(\"src\")}format(t,e){C.indexOf(t)>-1?e?this.domNode.setAttribute(t,e):this.domNode.removeAttribute(t):super.format(t,e)}}var I=R;const B=[\"height\",\"width\"];class M extends a.zo{static blotName=\"video\";static className=\"ql-video\";static tagName=\"IFRAME\";static create(t){const e=super.create(t);return e.setAttribute(\"frameborder\",\"0\"),e.setAttribute(\"allowfullscreen\",\"true\"),e.setAttribute(\"src\",this.sanitize(t)),e}static formats(t){return B.reduce(((e,n)=>(t.hasAttribute(n)&&(e[n]=t.getAttribute(n)),e)),{})}static sanitize(t){return w.sanitize(t)}static value(t){return t.getAttribute(\"src\")}format(t,e){B.indexOf(t)>-1?e?this.domNode.setAttribute(t,e):this.domNode.removeAttribute(t):super.format(t,e)}html(){const{video:t}=this.value();return`<a href=\"${t}\">${t}</a>`}}var U=M,D=n(9404),P=n(5232),z=n.n(P),F=n(4266),H=n(3036),$=n(4541),V=n(5508),K=n(584);const W=new s.ClassAttributor(\"code-token\",\"hljs\",{scope:s.Scope.INLINE});class Z extends x.A{static formats(t,e){for(;null!=t&&t!==e.domNode;){if(t.classList&&t.classList.contains(D.Ay.className))return super.formats(t,e);t=t.parentNode}}constructor(t,e,n){super(t,e,n),W.add(this.domNode,n)}format(t,e){t!==Z.blotName?super.format(t,e):e?W.add(this.domNode,e):(W.remove(this.domNode),this.domNode.classList.remove(this.statics.className))}optimize(){super.optimize(...arguments),W.value(this.domNode)||this.unwrap()}}Z.blotName=\"code-token\",Z.className=\"ql-token\";class G extends D.Ay{static create(t){const e=super.create(t);return\"string\"==typeof t&&e.setAttribute(\"data-language\",t),e}static formats(t){return t.getAttribute(\"data-language\")||\"plain\"}static register(){}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute(\"data-language\",e):super.format(t,e)}replaceWith(t,e){return this.formatAt(0,this.length(),Z.blotName,!1),super.replaceWith(t,e)}}class X extends D.EJ{attach(){super.attach(),this.forceNext=!1,this.scroll.emitMount(this)}format(t,e){t===G.blotName&&(this.forceNext=!0,this.children.forEach((n=>{n.format(t,e)})))}formatAt(t,e,n,r){n===G.blotName&&(this.forceNext=!0),super.formatAt(t,e,n,r)}highlight(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(null==this.children.head)return;const n=`${Array.from(this.domNode.childNodes).filter((t=>t!==this.uiNode)).map((t=>t.textContent)).join(\"\\n\")}\\n`,r=G.formats(this.children.head.domNode);if(e||this.forceNext||this.cachedText!==n){if(n.trim().length>0||null==this.cachedText){const e=this.children.reduce(((t,e)=>t.concat((0,a.mG)(e,!1))),new(z())),i=t(n,r);e.diff(i).reduce(((t,e)=>{let{retain:n,attributes:r}=e;return n?(r&&Object.keys(r).forEach((e=>{[G.blotName,Z.blotName].includes(e)&&this.formatAt(t,n,e,r[e])})),t+n):t}),0)}this.cachedText=n,this.forceNext=!1}}html(t,e){const[n]=this.children.find(t);return`<pre data-language=\"${n?G.formats(n.domNode):\"plain\"}\">\\n${(0,V.X)(this.code(t,e))}\\n</pre>`}optimize(t){if(super.optimize(t),null!=this.parent&&null!=this.children.head&&null!=this.uiNode){const t=G.formats(this.children.head.domNode);t!==this.uiNode.value&&(this.uiNode.value=t)}}}X.allowedChildren=[G],G.requiredContainer=X,G.allowedChildren=[Z,$.A,V.A,H.A];class Q extends F.A{static register(){p.Ay.register(Z,!0),p.Ay.register(G,!0),p.Ay.register(X,!0)}constructor(t,e){if(super(t,e),null==this.options.hljs)throw new Error(\"Syntax module requires highlight.js. Please include the library on the page before Quill.\");this.languages=this.options.languages.reduce(((t,e)=>{let{key:n}=e;return t[n]=!0,t}),{}),this.highlightBlot=this.highlightBlot.bind(this),this.initListener(),this.initTimer()}initListener(){this.quill.on(p.Ay.events.SCROLL_BLOT_MOUNT,(t=>{if(!(t instanceof X))return;const e=this.quill.root.ownerDocument.createElement(\"select\");this.options.languages.forEach((t=>{let{key:n,label:r}=t;const i=e.ownerDocument.createElement(\"option\");i.textContent=r,i.setAttribute(\"value\",n),e.appendChild(i)})),e.addEventListener(\"change\",(()=>{t.format(G.blotName,e.value),this.quill.root.focus(),this.highlight(t,!0)})),null==t.uiNode&&(t.attachUI(e),t.children.head&&(e.value=G.formats(t.children.head.domNode)))}))}initTimer(){let t=null;this.quill.on(p.Ay.events.SCROLL_OPTIMIZE,(()=>{t&&clearTimeout(t),t=setTimeout((()=>{this.highlight(),t=null}),this.options.interval)}))}highlight(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(this.quill.selection.composing)return;this.quill.update(p.Ay.sources.USER);const n=this.quill.getSelection();(null==t?this.quill.scroll.descendants(X):[t]).forEach((t=>{t.highlight(this.highlightBlot,e)})),this.quill.update(p.Ay.sources.SILENT),null!=n&&this.quill.setSelection(n,p.Ay.sources.SILENT)}highlightBlot(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:\"plain\";if(e=this.languages[e]?e:\"plain\",\"plain\"===e)return(0,V.X)(t).split(\"\\n\").reduce(((t,n,r)=>(0!==r&&t.insert(\"\\n\",{[D.Ay.blotName]:e}),t.insert(n))),new(z()));const n=this.quill.root.ownerDocument.createElement(\"div\");return n.classList.add(D.Ay.className),n.innerHTML=((t,e,n)=>{if(\"string\"==typeof t.versionString){const r=t.versionString.split(\".\")[0];if(parseInt(r,10)>=11)return t.highlight(n,{language:e}).value}return t.highlight(e,n).value})(this.options.hljs,e,t),(0,K.hV)(this.quill.scroll,n,[(t,e)=>{const n=W.value(t);return n?e.compose((new(z())).retain(e.length(),{[Z.blotName]:n})):e}],[(t,n)=>t.data.split(\"\\n\").reduce(((t,n,r)=>(0!==r&&t.insert(\"\\n\",{[D.Ay.blotName]:e}),t.insert(n))),n)],new WeakMap)}}Q.DEFAULTS={hljs:window.hljs,interval:1e3,languages:[{key:\"plain\",label:\"Plain\"},{key:\"bash\",label:\"Bash\"},{key:\"cpp\",label:\"C++\"},{key:\"cs\",label:\"C#\"},{key:\"css\",label:\"CSS\"},{key:\"diff\",label:\"Diff\"},{key:\"xml\",label:\"HTML/XML\"},{key:\"java\",label:\"Java\"},{key:\"javascript\",label:\"JavaScript\"},{key:\"markdown\",label:\"Markdown\"},{key:\"php\",label:\"PHP\"},{key:\"python\",label:\"Python\"},{key:\"ruby\",label:\"Ruby\"},{key:\"sql\",label:\"SQL\"}]};class J extends a.Ay{static blotName=\"table\";static tagName=\"TD\";static create(t){const e=super.create();return t?e.setAttribute(\"data-row\",t):e.setAttribute(\"data-row\",nt()),e}static formats(t){if(t.hasAttribute(\"data-row\"))return t.getAttribute(\"data-row\")}cellOffset(){return this.parent?this.parent.children.indexOf(this):-1}format(t,e){t===J.blotName&&e?this.domNode.setAttribute(\"data-row\",e):super.format(t,e)}row(){return this.parent}rowOffset(){return this.row()?this.row().rowOffset():-1}table(){return this.row()&&this.row().table()}}class Y extends f.A{static blotName=\"table-row\";static tagName=\"TR\";checkMerge(){if(super.checkMerge()&&null!=this.next.children.head){const t=this.children.head.formats(),e=this.children.tail.formats(),n=this.next.children.head.formats(),r=this.next.children.tail.formats();return t.table===e.table&&t.table===n.table&&t.table===r.table}return!1}optimize(t){super.optimize(t),this.children.forEach((t=>{if(null==t.next)return;const e=t.formats(),n=t.next.formats();if(e.table!==n.table){const e=this.splitAfter(t);e&&e.optimize(),this.prev&&this.prev.optimize()}}))}rowOffset(){return this.parent?this.parent.children.indexOf(this):-1}table(){return this.parent&&this.parent.parent}}class tt extends f.A{static blotName=\"table-body\";static tagName=\"TBODY\"}class et extends f.A{static blotName=\"table-container\";static tagName=\"TABLE\";balanceCells(){const t=this.descendants(Y),e=t.reduce(((t,e)=>Math.max(e.children.length,t)),0);t.forEach((t=>{new Array(e-t.children.length).fill(0).forEach((()=>{let e;null!=t.children.head&&(e=J.formats(t.children.head.domNode));const n=this.scroll.create(J.blotName,e);t.appendChild(n),n.optimize()}))}))}cells(t){return this.rows().map((e=>e.children.at(t)))}deleteColumn(t){const[e]=this.descendant(tt);null!=e&&null!=e.children.head&&e.children.forEach((e=>{const n=e.children.at(t);null!=n&&n.remove()}))}insertColumn(t){const[e]=this.descendant(tt);null!=e&&null!=e.children.head&&e.children.forEach((e=>{const n=e.children.at(t),r=J.formats(e.children.head.domNode),i=this.scroll.create(J.blotName,r);e.insertBefore(i,n)}))}insertRow(t){const[e]=this.descendant(tt);if(null==e||null==e.children.head)return;const n=nt(),r=this.scroll.create(Y.blotName);e.children.head.children.forEach((()=>{const t=this.scroll.create(J.blotName,n);r.appendChild(t)}));const i=e.children.at(t);e.insertBefore(r,i)}rows(){const t=this.children.head;return null==t?[]:t.children.map((t=>t))}}function nt(){return`row-${Math.random().toString(36).slice(2,6)}`}et.allowedChildren=[tt],tt.requiredContainer=et,tt.allowedChildren=[Y],Y.requiredContainer=tt,Y.allowedChildren=[J],J.requiredContainer=Y;class rt extends F.A{static register(){p.Ay.register(J),p.Ay.register(Y),p.Ay.register(tt),p.Ay.register(et)}constructor(){super(...arguments),this.listenBalanceCells()}balanceTables(){this.quill.scroll.descendants(et).forEach((t=>{t.balanceCells()}))}deleteColumn(){const[t,,e]=this.getTable();null!=e&&(t.deleteColumn(e.cellOffset()),this.quill.update(p.Ay.sources.USER))}deleteRow(){const[,t]=this.getTable();null!=t&&(t.remove(),this.quill.update(p.Ay.sources.USER))}deleteTable(){const[t]=this.getTable();if(null==t)return;const e=t.offset();t.remove(),this.quill.update(p.Ay.sources.USER),this.quill.setSelection(e,p.Ay.sources.SILENT)}getTable(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.quill.getSelection();if(null==t)return[null,null,null,-1];const[e,n]=this.quill.getLine(t.index);if(null==e||e.statics.blotName!==J.blotName)return[null,null,null,-1];const r=e.parent;return[r.parent.parent,r,e,n]}insertColumn(t){const e=this.quill.getSelection();if(!e)return;const[n,r,i]=this.getTable(e);if(null==i)return;const s=i.cellOffset();n.insertColumn(s+t),this.quill.update(p.Ay.sources.USER);let o=r.rowOffset();0===t&&(o+=1),this.quill.setSelection(e.index+o,e.length,p.Ay.sources.SILENT)}insertColumnLeft(){this.insertColumn(0)}insertColumnRight(){this.insertColumn(1)}insertRow(t){const e=this.quill.getSelection();if(!e)return;const[n,r,i]=this.getTable(e);if(null==i)return;const s=r.rowOffset();n.insertRow(s+t),this.quill.update(p.Ay.sources.USER),t>0?this.quill.setSelection(e,p.Ay.sources.SILENT):this.quill.setSelection(e.index+r.children.length,e.length,p.Ay.sources.SILENT)}insertRowAbove(){this.insertRow(0)}insertRowBelow(){this.insertRow(1)}insertTable(t,e){const n=this.quill.getSelection();if(null==n)return;const r=new Array(t).fill(0).reduce((t=>{const n=new Array(e).fill(\"\\n\").join(\"\");return t.insert(n,{table:nt()})}),(new(z())).retain(n.index));this.quill.updateContents(r,p.Ay.sources.USER),this.quill.setSelection(n.index,p.Ay.sources.SILENT),this.balanceTables()}listenBalanceCells(){this.quill.on(p.Ay.events.SCROLL_OPTIMIZE,(t=>{t.some((t=>!![\"TD\",\"TR\",\"TBODY\",\"TABLE\"].includes(t.target.tagName)&&(this.quill.once(p.Ay.events.TEXT_CHANGE,((t,e,n)=>{n===p.Ay.sources.USER&&this.balanceTables()})),!0)))}))}}var it=rt;const st=(0,n(6078).A)(\"quill:toolbar\");class ot extends F.A{constructor(t,e){if(super(t,e),Array.isArray(this.options.container)){const e=document.createElement(\"div\");e.setAttribute(\"role\",\"toolbar\"),function(t,e){Array.isArray(e[0])||(e=[e]),e.forEach((e=>{const n=document.createElement(\"span\");n.classList.add(\"ql-formats\"),e.forEach((t=>{if(\"string\"==typeof t)lt(n,t);else{const e=Object.keys(t)[0],r=t[e];Array.isArray(r)?function(t,e,n){const r=document.createElement(\"select\");r.classList.add(`ql-${e}`),n.forEach((t=>{const e=document.createElement(\"option\");!1!==t?e.setAttribute(\"value\",String(t)):e.setAttribute(\"selected\",\"selected\"),r.appendChild(e)})),t.appendChild(r)}(n,e,r):lt(n,e,r)}})),t.appendChild(n)}))}(e,this.options.container),t.container?.parentNode?.insertBefore(e,t.container),this.container=e}else\"string\"==typeof this.options.container?this.container=document.querySelector(this.options.container):this.container=this.options.container;this.container instanceof HTMLElement?(this.container.classList.add(\"ql-toolbar\"),this.controls=[],this.handlers={},this.options.handlers&&Object.keys(this.options.handlers).forEach((t=>{const e=this.options.handlers?.[t];e&&this.addHandler(t,e)})),Array.from(this.container.querySelectorAll(\"button, select\")).forEach((t=>{this.attach(t)})),this.quill.on(p.Ay.events.EDITOR_CHANGE,(()=>{const[t]=this.quill.selection.getRange();this.update(t)}))):st.error(\"Container required for toolbar\",this.options)}addHandler(t,e){this.handlers[t]=e}attach(t){let e=Array.from(t.classList).find((t=>0===t.indexOf(\"ql-\")));if(!e)return;if(e=e.slice(3),\"BUTTON\"===t.tagName&&t.setAttribute(\"type\",\"button\"),null==this.handlers[e]&&null==this.quill.scroll.query(e))return void st.warn(\"ignoring attaching to nonexistent format\",e,t);const n=\"SELECT\"===t.tagName?\"change\":\"click\";t.addEventListener(n,(n=>{let r;if(\"SELECT\"===t.tagName){if(t.selectedIndex<0)return;const e=t.options[t.selectedIndex];r=!e.hasAttribute(\"selected\")&&(e.value||!1)}else r=!t.classList.contains(\"ql-active\")&&(t.value||!t.hasAttribute(\"value\")),n.preventDefault();this.quill.focus();const[i]=this.quill.selection.getRange();if(null!=this.handlers[e])this.handlers[e].call(this,r);else if(this.quill.scroll.query(e).prototype instanceof s.EmbedBlot){if(r=prompt(`Enter ${e}`),!r)return;this.quill.updateContents((new(z())).retain(i.index).delete(i.length).insert({[e]:r}),p.Ay.sources.USER)}else this.quill.format(e,r,p.Ay.sources.USER);this.update(i)})),this.controls.push([e,t])}update(t){const e=null==t?{}:this.quill.getFormat(t);this.controls.forEach((n=>{const[r,i]=n;if(\"SELECT\"===i.tagName){let n=null;if(null==t)n=null;else if(null==e[r])n=i.querySelector(\"option[selected]\");else if(!Array.isArray(e[r])){let t=e[r];\"string\"==typeof t&&(t=t.replace(/\"/g,'\\\\\"')),n=i.querySelector(`option[value=\"${t}\"]`)}null==n?(i.value=\"\",i.selectedIndex=-1):n.selected=!0}else if(null==t)i.classList.remove(\"ql-active\"),i.setAttribute(\"aria-pressed\",\"false\");else if(i.hasAttribute(\"value\")){const t=e[r],n=t===i.getAttribute(\"value\")||null!=t&&t.toString()===i.getAttribute(\"value\")||null==t&&!i.getAttribute(\"value\");i.classList.toggle(\"ql-active\",n),i.setAttribute(\"aria-pressed\",n.toString())}else{const t=null!=e[r];i.classList.toggle(\"ql-active\",t),i.setAttribute(\"aria-pressed\",t.toString())}}))}}function lt(t,e,n){const r=document.createElement(\"button\");r.setAttribute(\"type\",\"button\"),r.classList.add(`ql-${e}`),r.setAttribute(\"aria-pressed\",\"false\"),null!=n?(r.value=n,r.setAttribute(\"aria-label\",`${e}: ${n}`)):r.setAttribute(\"aria-label\",e),t.appendChild(r)}ot.DEFAULTS={},ot.DEFAULTS={container:null,handlers:{clean(){const t=this.quill.getSelection();if(null!=t)if(0===t.length){const t=this.quill.getFormat();Object.keys(t).forEach((t=>{null!=this.quill.scroll.query(t,s.Scope.INLINE)&&this.quill.format(t,!1,p.Ay.sources.USER)}))}else this.quill.removeFormat(t.index,t.length,p.Ay.sources.USER)},direction(t){const{align:e}=this.quill.getFormat();\"rtl\"===t&&null==e?this.quill.format(\"align\",\"right\",p.Ay.sources.USER):t||\"right\"!==e||this.quill.format(\"align\",!1,p.Ay.sources.USER),this.quill.format(\"direction\",t,p.Ay.sources.USER)},indent(t){const e=this.quill.getSelection(),n=this.quill.getFormat(e),r=parseInt(n.indent||0,10);if(\"+1\"===t||\"-1\"===t){let e=\"+1\"===t?1:-1;\"rtl\"===n.direction&&(e*=-1),this.quill.format(\"indent\",r+e,p.Ay.sources.USER)}},link(t){!0===t&&(t=prompt(\"Enter link URL:\")),this.quill.format(\"link\",t,p.Ay.sources.USER)},list(t){const e=this.quill.getSelection(),n=this.quill.getFormat(e);\"check\"===t?\"checked\"===n.list||\"unchecked\"===n.list?this.quill.format(\"list\",!1,p.Ay.sources.USER):this.quill.format(\"list\",\"unchecked\",p.Ay.sources.USER):this.quill.format(\"list\",t,p.Ay.sources.USER)}}};const at='<svg viewbox=\"0 0 18 18\"><polyline class=\"ql-even ql-stroke\" points=\"5 7 3 9 5 11\"/><polyline class=\"ql-even ql-stroke\" points=\"13 7 15 9 13 11\"/><line class=\"ql-stroke\" x1=\"10\" x2=\"8\" y1=\"5\" y2=\"13\"/></svg>';var ct={align:{\"\":'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"3\" x2=\"15\" y1=\"9\" y2=\"9\"/><line class=\"ql-stroke\" x1=\"3\" x2=\"13\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"3\" x2=\"9\" y1=\"4\" y2=\"4\"/></svg>',center:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"15\" x2=\"3\" y1=\"9\" y2=\"9\"/><line class=\"ql-stroke\" x1=\"14\" x2=\"4\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"12\" x2=\"6\" y1=\"4\" y2=\"4\"/></svg>',right:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"15\" x2=\"3\" y1=\"9\" y2=\"9\"/><line class=\"ql-stroke\" x1=\"15\" x2=\"5\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"15\" x2=\"9\" y1=\"4\" y2=\"4\"/></svg>',justify:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"15\" x2=\"3\" y1=\"9\" y2=\"9\"/><line class=\"ql-stroke\" x1=\"15\" x2=\"3\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"15\" x2=\"3\" y1=\"4\" y2=\"4\"/></svg>'},background:'<svg viewbox=\"0 0 18 18\"><g class=\"ql-fill ql-color-label\"><polygon points=\"6 6.868 6 6 5 6 5 7 5.942 7 6 6.868\"/><rect height=\"1\" width=\"1\" x=\"4\" y=\"4\"/><polygon points=\"6.817 5 6 5 6 6 6.38 6 6.817 5\"/><rect height=\"1\" width=\"1\" x=\"2\" y=\"6\"/><rect height=\"1\" width=\"1\" x=\"3\" y=\"5\"/><rect height=\"1\" width=\"1\" x=\"4\" y=\"7\"/><polygon points=\"4 11.439 4 11 3 11 3 12 3.755 12 4 11.439\"/><rect height=\"1\" width=\"1\" x=\"2\" y=\"12\"/><rect height=\"1\" width=\"1\" x=\"2\" y=\"9\"/><rect height=\"1\" width=\"1\" x=\"2\" y=\"15\"/><polygon points=\"4.63 10 4 10 4 11 4.192 11 4.63 10\"/><rect height=\"1\" width=\"1\" x=\"3\" y=\"8\"/><path d=\"M10.832,4.2L11,4.582V4H10.708A1.948,1.948,0,0,1,10.832,4.2Z\"/><path d=\"M7,4.582L7.168,4.2A1.929,1.929,0,0,1,7.292,4H7V4.582Z\"/><path d=\"M8,13H7.683l-0.351.8a1.933,1.933,0,0,1-.124.2H8V13Z\"/><rect height=\"1\" width=\"1\" x=\"12\" y=\"2\"/><rect height=\"1\" width=\"1\" x=\"11\" y=\"3\"/><path d=\"M9,3H8V3.282A1.985,1.985,0,0,1,9,3Z\"/><rect height=\"1\" width=\"1\" x=\"2\" y=\"3\"/><rect height=\"1\" width=\"1\" x=\"6\" y=\"2\"/><rect height=\"1\" width=\"1\" x=\"3\" y=\"2\"/><rect height=\"1\" width=\"1\" x=\"5\" y=\"3\"/><rect height=\"1\" width=\"1\" x=\"9\" y=\"2\"/><rect height=\"1\" width=\"1\" x=\"15\" y=\"14\"/><polygon points=\"13.447 10.174 13.469 10.225 13.472 10.232 13.808 11 14 11 14 10 13.37 10 13.447 10.174\"/><rect height=\"1\" width=\"1\" x=\"13\" y=\"7\"/><rect height=\"1\" width=\"1\" x=\"15\" y=\"5\"/><rect height=\"1\" width=\"1\" x=\"14\" y=\"6\"/><rect height=\"1\" width=\"1\" x=\"15\" y=\"8\"/><rect height=\"1\" width=\"1\" x=\"14\" y=\"9\"/><path d=\"M3.775,14H3v1H4V14.314A1.97,1.97,0,0,1,3.775,14Z\"/><rect height=\"1\" width=\"1\" x=\"14\" y=\"3\"/><polygon points=\"12 6.868 12 6 11.62 6 12 6.868\"/><rect height=\"1\" width=\"1\" x=\"15\" y=\"2\"/><rect height=\"1\" width=\"1\" x=\"12\" y=\"5\"/><rect height=\"1\" width=\"1\" x=\"13\" y=\"4\"/><polygon points=\"12.933 9 13 9 13 8 12.495 8 12.933 9\"/><rect height=\"1\" width=\"1\" x=\"9\" y=\"14\"/><rect height=\"1\" width=\"1\" x=\"8\" y=\"15\"/><path d=\"M6,14.926V15H7V14.316A1.993,1.993,0,0,1,6,14.926Z\"/><rect height=\"1\" width=\"1\" x=\"5\" y=\"15\"/><path d=\"M10.668,13.8L10.317,13H10v1h0.792A1.947,1.947,0,0,1,10.668,13.8Z\"/><rect height=\"1\" width=\"1\" x=\"11\" y=\"15\"/><path d=\"M14.332,12.2a1.99,1.99,0,0,1,.166.8H15V12H14.245Z\"/><rect height=\"1\" width=\"1\" x=\"14\" y=\"15\"/><rect height=\"1\" width=\"1\" x=\"15\" y=\"11\"/></g><polyline class=\"ql-stroke\" points=\"5.5 13 9 5 12.5 13\"/><line class=\"ql-stroke\" x1=\"11.63\" x2=\"6.38\" y1=\"11\" y2=\"11\"/></svg>',blockquote:'<svg viewbox=\"0 0 18 18\"><rect class=\"ql-fill ql-stroke\" height=\"3\" width=\"3\" x=\"4\" y=\"5\"/><rect class=\"ql-fill ql-stroke\" height=\"3\" width=\"3\" x=\"11\" y=\"5\"/><path class=\"ql-even ql-fill ql-stroke\" d=\"M7,8c0,4.031-3,5-3,5\"/><path class=\"ql-even ql-fill ql-stroke\" d=\"M14,8c0,4.031-3,5-3,5\"/></svg>',bold:'<svg viewbox=\"0 0 18 18\"><path class=\"ql-stroke\" d=\"M5,4H9.5A2.5,2.5,0,0,1,12,6.5v0A2.5,2.5,0,0,1,9.5,9H5A0,0,0,0,1,5,9V4A0,0,0,0,1,5,4Z\"/><path class=\"ql-stroke\" d=\"M5,9h5.5A2.5,2.5,0,0,1,13,11.5v0A2.5,2.5,0,0,1,10.5,14H5a0,0,0,0,1,0,0V9A0,0,0,0,1,5,9Z\"/></svg>',clean:'<svg class=\"\" viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"5\" x2=\"13\" y1=\"3\" y2=\"3\"/><line class=\"ql-stroke\" x1=\"6\" x2=\"9.35\" y1=\"12\" y2=\"3\"/><line class=\"ql-stroke\" x1=\"11\" x2=\"15\" y1=\"11\" y2=\"15\"/><line class=\"ql-stroke\" x1=\"15\" x2=\"11\" y1=\"11\" y2=\"15\"/><rect class=\"ql-fill\" height=\"1\" rx=\"0.5\" ry=\"0.5\" width=\"7\" x=\"2\" y=\"14\"/></svg>',code:at,\"code-block\":at,color:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-color-label ql-stroke ql-transparent\" x1=\"3\" x2=\"15\" y1=\"15\" y2=\"15\"/><polyline class=\"ql-stroke\" points=\"5.5 11 9 3 12.5 11\"/><line class=\"ql-stroke\" x1=\"11.63\" x2=\"6.38\" y1=\"9\" y2=\"9\"/></svg>',direction:{\"\":'<svg viewbox=\"0 0 18 18\"><polygon class=\"ql-stroke ql-fill\" points=\"3 11 5 9 3 7 3 11\"/><line class=\"ql-stroke ql-fill\" x1=\"15\" x2=\"11\" y1=\"4\" y2=\"4\"/><path class=\"ql-fill\" d=\"M11,3a3,3,0,0,0,0,6h1V3H11Z\"/><rect class=\"ql-fill\" height=\"11\" width=\"1\" x=\"11\" y=\"4\"/><rect class=\"ql-fill\" height=\"11\" width=\"1\" x=\"13\" y=\"4\"/></svg>',rtl:'<svg viewbox=\"0 0 18 18\"><polygon class=\"ql-stroke ql-fill\" points=\"15 12 13 10 15 8 15 12\"/><line class=\"ql-stroke ql-fill\" x1=\"9\" x2=\"5\" y1=\"4\" y2=\"4\"/><path class=\"ql-fill\" d=\"M5,3A3,3,0,0,0,5,9H6V3H5Z\"/><rect class=\"ql-fill\" height=\"11\" width=\"1\" x=\"5\" y=\"4\"/><rect class=\"ql-fill\" height=\"11\" width=\"1\" x=\"7\" y=\"4\"/></svg>'},formula:'<svg viewbox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M11.759,2.482a2.561,2.561,0,0,0-3.53.607A7.656,7.656,0,0,0,6.8,6.2C6.109,9.188,5.275,14.677,4.15,14.927a1.545,1.545,0,0,0-1.3-.933A0.922,0.922,0,0,0,2,15.036S1.954,16,4.119,16s3.091-2.691,3.7-5.553c0.177-.826.36-1.726,0.554-2.6L8.775,6.2c0.381-1.421.807-2.521,1.306-2.676a1.014,1.014,0,0,0,1.02.56A0.966,0.966,0,0,0,11.759,2.482Z\"/><rect class=\"ql-fill\" height=\"1.6\" rx=\"0.8\" ry=\"0.8\" width=\"5\" x=\"5.15\" y=\"6.2\"/><path class=\"ql-fill\" d=\"M13.663,12.027a1.662,1.662,0,0,1,.266-0.276q0.193,0.069.456,0.138a2.1,2.1,0,0,0,.535.069,1.075,1.075,0,0,0,.767-0.3,1.044,1.044,0,0,0,.314-0.8,0.84,0.84,0,0,0-.238-0.619,0.8,0.8,0,0,0-.594-0.239,1.154,1.154,0,0,0-.781.3,4.607,4.607,0,0,0-.781,1q-0.091.15-.218,0.346l-0.246.38c-0.068-.288-0.137-0.582-0.212-0.885-0.459-1.847-2.494-.984-2.941-0.8-0.482.2-.353,0.647-0.094,0.529a0.869,0.869,0,0,1,1.281.585c0.217,0.751.377,1.436,0.527,2.038a5.688,5.688,0,0,1-.362.467,2.69,2.69,0,0,1-.264.271q-0.221-.08-0.471-0.147a2.029,2.029,0,0,0-.522-0.066,1.079,1.079,0,0,0-.768.3A1.058,1.058,0,0,0,9,15.131a0.82,0.82,0,0,0,.832.852,1.134,1.134,0,0,0,.787-0.3,5.11,5.11,0,0,0,.776-0.993q0.141-.219.215-0.34c0.046-.076.122-0.194,0.223-0.346a2.786,2.786,0,0,0,.918,1.726,2.582,2.582,0,0,0,2.376-.185c0.317-.181.212-0.565,0-0.494A0.807,0.807,0,0,1,14.176,15a5.159,5.159,0,0,1-.913-2.446l0,0Q13.487,12.24,13.663,12.027Z\"/></svg>',header:{1:'<svg viewBox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M10,4V14a1,1,0,0,1-2,0V10H3v4a1,1,0,0,1-2,0V4A1,1,0,0,1,3,4V8H8V4a1,1,0,0,1,2,0Zm6.06787,9.209H14.98975V7.59863a.54085.54085,0,0,0-.605-.60547h-.62744a1.01119,1.01119,0,0,0-.748.29688L11.645,8.56641a.5435.5435,0,0,0-.022.8584l.28613.30762a.53861.53861,0,0,0,.84717.0332l.09912-.08789a1.2137,1.2137,0,0,0,.2417-.35254h.02246s-.01123.30859-.01123.60547V13.209H12.041a.54085.54085,0,0,0-.605.60547v.43945a.54085.54085,0,0,0,.605.60547h4.02686a.54085.54085,0,0,0,.605-.60547v-.43945A.54085.54085,0,0,0,16.06787,13.209Z\"/></svg>',2:'<svg viewBox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M16.73975,13.81445v.43945a.54085.54085,0,0,1-.605.60547H11.855a.58392.58392,0,0,1-.64893-.60547V14.0127c0-2.90527,3.39941-3.42187,3.39941-4.55469a.77675.77675,0,0,0-.84717-.78125,1.17684,1.17684,0,0,0-.83594.38477c-.2749.26367-.561.374-.85791.13184l-.4292-.34082c-.30811-.24219-.38525-.51758-.1543-.81445a2.97155,2.97155,0,0,1,2.45361-1.17676,2.45393,2.45393,0,0,1,2.68408,2.40918c0,2.45312-3.1792,2.92676-3.27832,3.93848h2.79443A.54085.54085,0,0,1,16.73975,13.81445ZM9,3A.99974.99974,0,0,0,8,4V8H3V4A1,1,0,0,0,1,4V14a1,1,0,0,0,2,0V10H8v4a1,1,0,0,0,2,0V4A.99974.99974,0,0,0,9,3Z\"/></svg>',3:'<svg viewBox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M16.65186,12.30664a2.6742,2.6742,0,0,1-2.915,2.68457,3.96592,3.96592,0,0,1-2.25537-.6709.56007.56007,0,0,1-.13232-.83594L11.64648,13c.209-.34082.48389-.36328.82471-.1543a2.32654,2.32654,0,0,0,1.12256.33008c.71484,0,1.12207-.35156,1.12207-.78125,0-.61523-.61621-.86816-1.46338-.86816H13.2085a.65159.65159,0,0,1-.68213-.41895l-.05518-.10937a.67114.67114,0,0,1,.14307-.78125l.71533-.86914a8.55289,8.55289,0,0,1,.68213-.7373V8.58887a3.93913,3.93913,0,0,1-.748.05469H11.9873a.54085.54085,0,0,1-.605-.60547V7.59863a.54085.54085,0,0,1,.605-.60547h3.75146a.53773.53773,0,0,1,.60547.59375v.17676a1.03723,1.03723,0,0,1-.27539.748L14.74854,10.0293A2.31132,2.31132,0,0,1,16.65186,12.30664ZM9,3A.99974.99974,0,0,0,8,4V8H3V4A1,1,0,0,0,1,4V14a1,1,0,0,0,2,0V10H8v4a1,1,0,0,0,2,0V4A.99974.99974,0,0,0,9,3Z\"/></svg>',4:'<svg viewBox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M10,4V14a1,1,0,0,1-2,0V10H3v4a1,1,0,0,1-2,0V4A1,1,0,0,1,3,4V8H8V4a1,1,0,0,1,2,0Zm7.05371,7.96582v.38477c0,.39648-.165.60547-.46191.60547h-.47314v1.29785a.54085.54085,0,0,1-.605.60547h-.69336a.54085.54085,0,0,1-.605-.60547V12.95605H11.333a.5412.5412,0,0,1-.60547-.60547v-.15332a1.199,1.199,0,0,1,.22021-.748l2.56348-4.05957a.7819.7819,0,0,1,.72607-.39648h1.27637a.54085.54085,0,0,1,.605.60547v3.7627h.33008A.54055.54055,0,0,1,17.05371,11.96582ZM14.28125,8.7207h-.022a4.18969,4.18969,0,0,1-.38525.81348l-1.188,1.80469v.02246h1.5293V9.60059A7.04058,7.04058,0,0,1,14.28125,8.7207Z\"/></svg>',5:'<svg viewBox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M16.74023,12.18555a2.75131,2.75131,0,0,1-2.91553,2.80566,3.908,3.908,0,0,1-2.25537-.68164.54809.54809,0,0,1-.13184-.8252L11.73438,13c.209-.34082.48389-.36328.8252-.1543a2.23757,2.23757,0,0,0,1.1001.33008,1.01827,1.01827,0,0,0,1.1001-.96777c0-.61621-.53906-.97949-1.25439-.97949a2.15554,2.15554,0,0,0-.64893.09961,1.15209,1.15209,0,0,1-.814.01074l-.12109-.04395a.64116.64116,0,0,1-.45117-.71484l.231-3.00391a.56666.56666,0,0,1,.62744-.583H15.541a.54085.54085,0,0,1,.605.60547v.43945a.54085.54085,0,0,1-.605.60547H13.41748l-.04395.72559a1.29306,1.29306,0,0,1-.04395.30859h.022a2.39776,2.39776,0,0,1,.57227-.07715A2.53266,2.53266,0,0,1,16.74023,12.18555ZM9,3A.99974.99974,0,0,0,8,4V8H3V4A1,1,0,0,0,1,4V14a1,1,0,0,0,2,0V10H8v4a1,1,0,0,0,2,0V4A.99974.99974,0,0,0,9,3Z\"/></svg>',6:'<svg viewBox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M14.51758,9.64453a1.85627,1.85627,0,0,0-1.24316.38477H13.252a1.73532,1.73532,0,0,1,1.72754-1.4082,2.66491,2.66491,0,0,1,.5498.06641c.35254.05469.57227.01074.70508-.40723l.16406-.5166a.53393.53393,0,0,0-.373-.75977,4.83723,4.83723,0,0,0-1.17773-.14258c-2.43164,0-3.7627,2.17773-3.7627,4.43359,0,2.47559,1.60645,3.69629,3.19043,3.69629A2.70585,2.70585,0,0,0,16.96,12.19727,2.43861,2.43861,0,0,0,14.51758,9.64453Zm-.23047,3.58691c-.67187,0-1.22168-.81445-1.22168-1.45215,0-.47363.30762-.583.72559-.583.96875,0,1.27734.59375,1.27734,1.12207A.82182.82182,0,0,1,14.28711,13.23145ZM10,4V14a1,1,0,0,1-2,0V10H3v4a1,1,0,0,1-2,0V4A1,1,0,0,1,3,4V8H8V4a1,1,0,0,1,2,0Z\"/></svg>'},italic:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"7\" x2=\"13\" y1=\"4\" y2=\"4\"/><line class=\"ql-stroke\" x1=\"5\" x2=\"11\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"8\" x2=\"10\" y1=\"14\" y2=\"4\"/></svg>',image:'<svg viewbox=\"0 0 18 18\"><rect class=\"ql-stroke\" height=\"10\" width=\"12\" x=\"3\" y=\"4\"/><circle class=\"ql-fill\" cx=\"6\" cy=\"7\" r=\"1\"/><polyline class=\"ql-even ql-fill\" points=\"5 12 5 11 7 9 8 10 11 7 13 9 13 12 5 12\"/></svg>',indent:{\"+1\":'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"3\" x2=\"15\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"3\" x2=\"15\" y1=\"4\" y2=\"4\"/><line class=\"ql-stroke\" x1=\"9\" x2=\"15\" y1=\"9\" y2=\"9\"/><polyline class=\"ql-fill ql-stroke\" points=\"3 7 3 11 5 9 3 7\"/></svg>',\"-1\":'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"3\" x2=\"15\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"3\" x2=\"15\" y1=\"4\" y2=\"4\"/><line class=\"ql-stroke\" x1=\"9\" x2=\"15\" y1=\"9\" y2=\"9\"/><polyline class=\"ql-stroke\" points=\"5 7 5 11 3 9 5 7\"/></svg>'},link:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"7\" x2=\"11\" y1=\"7\" y2=\"11\"/><path class=\"ql-even ql-stroke\" d=\"M8.9,4.577a3.476,3.476,0,0,1,.36,4.679A3.476,3.476,0,0,1,4.577,8.9C3.185,7.5,2.035,6.4,4.217,4.217S7.5,3.185,8.9,4.577Z\"/><path class=\"ql-even ql-stroke\" d=\"M13.423,9.1a3.476,3.476,0,0,0-4.679-.36,3.476,3.476,0,0,0,.36,4.679c1.392,1.392,2.5,2.542,4.679.36S14.815,10.5,13.423,9.1Z\"/></svg>',list:{bullet:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"6\" x2=\"15\" y1=\"4\" y2=\"4\"/><line class=\"ql-stroke\" x1=\"6\" x2=\"15\" y1=\"9\" y2=\"9\"/><line class=\"ql-stroke\" x1=\"6\" x2=\"15\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke\" x1=\"3\" x2=\"3\" y1=\"4\" y2=\"4\"/><line class=\"ql-stroke\" x1=\"3\" x2=\"3\" y1=\"9\" y2=\"9\"/><line class=\"ql-stroke\" x1=\"3\" x2=\"3\" y1=\"14\" y2=\"14\"/></svg>',check:'<svg class=\"\" viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"9\" x2=\"15\" y1=\"4\" y2=\"4\"/><polyline class=\"ql-stroke\" points=\"3 4 4 5 6 3\"/><line class=\"ql-stroke\" x1=\"9\" x2=\"15\" y1=\"14\" y2=\"14\"/><polyline class=\"ql-stroke\" points=\"3 14 4 15 6 13\"/><line class=\"ql-stroke\" x1=\"9\" x2=\"15\" y1=\"9\" y2=\"9\"/><polyline class=\"ql-stroke\" points=\"3 9 4 10 6 8\"/></svg>',ordered:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke\" x1=\"7\" x2=\"15\" y1=\"4\" y2=\"4\"/><line class=\"ql-stroke\" x1=\"7\" x2=\"15\" y1=\"9\" y2=\"9\"/><line class=\"ql-stroke\" x1=\"7\" x2=\"15\" y1=\"14\" y2=\"14\"/><line class=\"ql-stroke ql-thin\" x1=\"2.5\" x2=\"4.5\" y1=\"5.5\" y2=\"5.5\"/><path class=\"ql-fill\" d=\"M3.5,6A0.5,0.5,0,0,1,3,5.5V3.085l-0.276.138A0.5,0.5,0,0,1,2.053,3c-0.124-.247-0.023-0.324.224-0.447l1-.5A0.5,0.5,0,0,1,4,2.5v3A0.5,0.5,0,0,1,3.5,6Z\"/><path class=\"ql-stroke ql-thin\" d=\"M4.5,10.5h-2c0-.234,1.85-1.076,1.85-2.234A0.959,0.959,0,0,0,2.5,8.156\"/><path class=\"ql-stroke ql-thin\" d=\"M2.5,14.846a0.959,0.959,0,0,0,1.85-.109A0.7,0.7,0,0,0,3.75,14a0.688,0.688,0,0,0,.6-0.736,0.959,0.959,0,0,0-1.85-.109\"/></svg>'},script:{sub:'<svg viewbox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M15.5,15H13.861a3.858,3.858,0,0,0,1.914-2.975,1.8,1.8,0,0,0-1.6-1.751A1.921,1.921,0,0,0,12.021,11.7a0.50013,0.50013,0,1,0,.957.291h0a0.914,0.914,0,0,1,1.053-.725,0.81,0.81,0,0,1,.744.762c0,1.076-1.16971,1.86982-1.93971,2.43082A1.45639,1.45639,0,0,0,12,15.5a0.5,0.5,0,0,0,.5.5h3A0.5,0.5,0,0,0,15.5,15Z\"/><path class=\"ql-fill\" d=\"M9.65,5.241a1,1,0,0,0-1.409.108L6,7.964,3.759,5.349A1,1,0,0,0,2.192,6.59178Q2.21541,6.6213,2.241,6.649L4.684,9.5,2.241,12.35A1,1,0,0,0,3.71,13.70722q0.02557-.02768.049-0.05722L6,11.036,8.241,13.65a1,1,0,1,0,1.567-1.24277Q9.78459,12.3777,9.759,12.35L7.316,9.5,9.759,6.651A1,1,0,0,0,9.65,5.241Z\"/></svg>',super:'<svg viewbox=\"0 0 18 18\"><path class=\"ql-fill\" d=\"M15.5,7H13.861a4.015,4.015,0,0,0,1.914-2.975,1.8,1.8,0,0,0-1.6-1.751A1.922,1.922,0,0,0,12.021,3.7a0.5,0.5,0,1,0,.957.291,0.917,0.917,0,0,1,1.053-.725,0.81,0.81,0,0,1,.744.762c0,1.077-1.164,1.925-1.934,2.486A1.423,1.423,0,0,0,12,7.5a0.5,0.5,0,0,0,.5.5h3A0.5,0.5,0,0,0,15.5,7Z\"/><path class=\"ql-fill\" d=\"M9.651,5.241a1,1,0,0,0-1.41.108L6,7.964,3.759,5.349a1,1,0,1,0-1.519,1.3L4.683,9.5,2.241,12.35a1,1,0,1,0,1.519,1.3L6,11.036,8.241,13.65a1,1,0,0,0,1.519-1.3L7.317,9.5,9.759,6.651A1,1,0,0,0,9.651,5.241Z\"/></svg>'},strike:'<svg viewbox=\"0 0 18 18\"><line class=\"ql-stroke ql-thin\" x1=\"15.5\" x2=\"2.5\" y1=\"8.5\" y2=\"9.5\"/><path class=\"ql-fill\" d=\"M9.007,8C6.542,7.791,6,7.519,6,6.5,6,5.792,7.283,5,9,5c1.571,0,2.765.679,2.969,1.309a1,1,0,0,0,1.9-.617C13.356,4.106,11.354,3,9,3,6.2,3,4,4.538,4,6.5a3.2,3.2,0,0,0,.5,1.843Z\"/><path class=\"ql-fill\" d=\"M8.984,10C11.457,10.208,12,10.479,12,11.5c0,0.708-1.283,1.5-3,1.5-1.571,0-2.765-.679-2.969-1.309a1,1,0,1,0-1.9.617C4.644,13.894,6.646,15,9,15c2.8,0,5-1.538,5-3.5a3.2,3.2,0,0,0-.5-1.843Z\"/></svg>',table:'<svg viewbox=\"0 0 18 18\"><rect class=\"ql-stroke\" height=\"12\" width=\"12\" x=\"3\" y=\"3\"/><rect class=\"ql-fill\" height=\"2\" width=\"3\" x=\"5\" y=\"5\"/><rect class=\"ql-fill\" height=\"2\" width=\"4\" x=\"9\" y=\"5\"/><g class=\"ql-fill ql-transparent\"><rect height=\"2\" width=\"3\" x=\"5\" y=\"8\"/><rect height=\"2\" width=\"4\" x=\"9\" y=\"8\"/><rect height=\"2\" width=\"3\" x=\"5\" y=\"11\"/><rect height=\"2\" width=\"4\" x=\"9\" y=\"11\"/></g></svg>',underline:'<svg viewbox=\"0 0 18 18\"><path class=\"ql-stroke\" d=\"M5,3V9a4.012,4.012,0,0,0,4,4H9a4.012,4.012,0,0,0,4-4V3\"/><rect class=\"ql-fill\" height=\"1\" rx=\"0.5\" ry=\"0.5\" width=\"12\" x=\"3\" y=\"15\"/></svg>',video:'<svg viewbox=\"0 0 18 18\"><rect class=\"ql-stroke\" height=\"12\" width=\"12\" x=\"3\" y=\"3\"/><rect class=\"ql-fill\" height=\"12\" width=\"1\" x=\"5\" y=\"3\"/><rect class=\"ql-fill\" height=\"12\" width=\"1\" x=\"12\" y=\"3\"/><rect class=\"ql-fill\" height=\"2\" width=\"8\" x=\"5\" y=\"8\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"3\" y=\"5\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"3\" y=\"7\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"3\" y=\"10\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"3\" y=\"12\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"12\" y=\"5\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"12\" y=\"7\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"12\" y=\"10\"/><rect class=\"ql-fill\" height=\"1\" width=\"3\" x=\"12\" y=\"12\"/></svg>'};let ut=0;function ht(t,e){t.setAttribute(e,`${!(\"true\"===t.getAttribute(e))}`)}var dt=class{constructor(t){this.select=t,this.container=document.createElement(\"span\"),this.buildPicker(),this.select.style.display=\"none\",this.select.parentNode.insertBefore(this.container,this.select),this.label.addEventListener(\"mousedown\",(()=>{this.togglePicker()})),this.label.addEventListener(\"keydown\",(t=>{switch(t.key){case\"Enter\":this.togglePicker();break;case\"Escape\":this.escape(),t.preventDefault()}})),this.select.addEventListener(\"change\",this.update.bind(this))}togglePicker(){this.container.classList.toggle(\"ql-expanded\"),ht(this.label,\"aria-expanded\"),ht(this.options,\"aria-hidden\")}buildItem(t){const e=document.createElement(\"span\");e.tabIndex=\"0\",e.setAttribute(\"role\",\"button\"),e.classList.add(\"ql-picker-item\");const n=t.getAttribute(\"value\");return n&&e.setAttribute(\"data-value\",n),t.textContent&&e.setAttribute(\"data-label\",t.textContent),e.addEventListener(\"click\",(()=>{this.selectItem(e,!0)})),e.addEventListener(\"keydown\",(t=>{switch(t.key){case\"Enter\":this.selectItem(e,!0),t.preventDefault();break;case\"Escape\":this.escape(),t.preventDefault()}})),e}buildLabel(){const t=document.createElement(\"span\");return t.classList.add(\"ql-picker-label\"),t.innerHTML='<svg viewbox=\"0 0 18 18\"><polygon class=\"ql-stroke\" points=\"7 11 9 13 11 11 7 11\"/><polygon class=\"ql-stroke\" points=\"7 7 9 5 11 7 7 7\"/></svg>',t.tabIndex=\"0\",t.setAttribute(\"role\",\"button\"),t.setAttribute(\"aria-expanded\",\"false\"),this.container.appendChild(t),t}buildOptions(){const t=document.createElement(\"span\");t.classList.add(\"ql-picker-options\"),t.setAttribute(\"aria-hidden\",\"true\"),t.tabIndex=\"-1\",t.id=`ql-picker-options-${ut}`,ut+=1,this.label.setAttribute(\"aria-controls\",t.id),this.options=t,Array.from(this.select.options).forEach((e=>{const n=this.buildItem(e);t.appendChild(n),!0===e.selected&&this.selectItem(n)})),this.container.appendChild(t)}buildPicker(){Array.from(this.select.attributes).forEach((t=>{this.container.setAttribute(t.name,t.value)})),this.container.classList.add(\"ql-picker\"),this.label=this.buildLabel(),this.buildOptions()}escape(){this.close(),setTimeout((()=>this.label.focus()),1)}close(){this.container.classList.remove(\"ql-expanded\"),this.label.setAttribute(\"aria-expanded\",\"false\"),this.options.setAttribute(\"aria-hidden\",\"true\")}selectItem(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const n=this.container.querySelector(\".ql-selected\");t!==n&&(null!=n&&n.classList.remove(\"ql-selected\"),null!=t&&(t.classList.add(\"ql-selected\"),this.select.selectedIndex=Array.from(t.parentNode.children).indexOf(t),t.hasAttribute(\"data-value\")?this.label.setAttribute(\"data-value\",t.getAttribute(\"data-value\")):this.label.removeAttribute(\"data-value\"),t.hasAttribute(\"data-label\")?this.label.setAttribute(\"data-label\",t.getAttribute(\"data-label\")):this.label.removeAttribute(\"data-label\"),e&&(this.select.dispatchEvent(new Event(\"change\")),this.close())))}update(){let t;if(this.select.selectedIndex>-1){const e=this.container.querySelector(\".ql-picker-options\").children[this.select.selectedIndex];t=this.select.options[this.select.selectedIndex],this.selectItem(e)}else this.selectItem(null);const e=null!=t&&t!==this.select.querySelector(\"option[selected]\");this.label.classList.toggle(\"ql-active\",e)}},ft=class extends dt{constructor(t,e){super(t),this.label.innerHTML=e,this.container.classList.add(\"ql-color-picker\"),Array.from(this.container.querySelectorAll(\".ql-picker-item\")).slice(0,7).forEach((t=>{t.classList.add(\"ql-primary\")}))}buildItem(t){const e=super.buildItem(t);return e.style.backgroundColor=t.getAttribute(\"value\")||\"\",e}selectItem(t,e){super.selectItem(t,e);const n=this.label.querySelector(\".ql-color-label\"),r=t&&t.getAttribute(\"data-value\")||\"\";n&&(\"line\"===n.tagName?n.style.stroke=r:n.style.fill=r)}},pt=class extends dt{constructor(t,e){super(t),this.container.classList.add(\"ql-icon-picker\"),Array.from(this.container.querySelectorAll(\".ql-picker-item\")).forEach((t=>{t.innerHTML=e[t.getAttribute(\"data-value\")||\"\"]})),this.defaultItem=this.container.querySelector(\".ql-selected\"),this.selectItem(this.defaultItem)}selectItem(t,e){super.selectItem(t,e);const n=t||this.defaultItem;if(null!=n){if(this.label.innerHTML===n.innerHTML)return;this.label.innerHTML=n.innerHTML}}},gt=class{constructor(t,e){this.quill=t,this.boundsContainer=e||document.body,this.root=t.addContainer(\"ql-tooltip\"),this.root.innerHTML=this.constructor.TEMPLATE,(t=>{const{overflowY:e}=getComputedStyle(t,null);return\"visible\"!==e&&\"clip\"!==e})(this.quill.root)&&this.quill.root.addEventListener(\"scroll\",(()=>{this.root.style.marginTop=-1*this.quill.root.scrollTop+\"px\"})),this.hide()}hide(){this.root.classList.add(\"ql-hidden\")}position(t){const e=t.left+t.width/2-this.root.offsetWidth/2,n=t.bottom+this.quill.root.scrollTop;this.root.style.left=`${e}px`,this.root.style.top=`${n}px`,this.root.classList.remove(\"ql-flip\");const r=this.boundsContainer.getBoundingClientRect(),i=this.root.getBoundingClientRect();let s=0;if(i.right>r.right&&(s=r.right-i.right,this.root.style.left=`${e+s}px`),i.left<r.left&&(s=r.left-i.left,this.root.style.left=`${e+s}px`),i.bottom>r.bottom){const e=i.bottom-i.top,r=t.bottom-t.top+e;this.root.style.top=n-r+\"px\",this.root.classList.add(\"ql-flip\")}return s}show(){this.root.classList.remove(\"ql-editing\"),this.root.classList.remove(\"ql-hidden\")}},mt=n(8347),bt=n(5374),yt=n(9609);const vt=[!1,\"center\",\"right\",\"justify\"],At=[\"#000000\",\"#e60000\",\"#ff9900\",\"#ffff00\",\"#008a00\",\"#0066cc\",\"#9933ff\",\"#ffffff\",\"#facccc\",\"#ffebcc\",\"#ffffcc\",\"#cce8cc\",\"#cce0f5\",\"#ebd6ff\",\"#bbbbbb\",\"#f06666\",\"#ffc266\",\"#ffff66\",\"#66b966\",\"#66a3e0\",\"#c285ff\",\"#888888\",\"#a10000\",\"#b26b00\",\"#b2b200\",\"#006100\",\"#0047b2\",\"#6b24b2\",\"#444444\",\"#5c0000\",\"#663d00\",\"#666600\",\"#003700\",\"#002966\",\"#3d1466\"],xt=[!1,\"serif\",\"monospace\"],Nt=[\"1\",\"2\",\"3\",!1],Et=[\"small\",!1,\"large\",\"huge\"];class wt extends yt.A{constructor(t,e){super(t,e);const n=e=>{document.body.contains(t.root)?(null==this.tooltip||this.tooltip.root.contains(e.target)||document.activeElement===this.tooltip.textbox||this.quill.hasFocus()||this.tooltip.hide(),null!=this.pickers&&this.pickers.forEach((t=>{t.container.contains(e.target)||t.close()}))):document.body.removeEventListener(\"click\",n)};t.emitter.listenDOM(\"click\",document.body,n)}addModule(t){const e=super.addModule(t);return\"toolbar\"===t&&this.extendToolbar(e),e}buildButtons(t,e){Array.from(t).forEach((t=>{(t.getAttribute(\"class\")||\"\").split(/\\s+/).forEach((n=>{if(n.startsWith(\"ql-\")&&(n=n.slice(3),null!=e[n]))if(\"direction\"===n)t.innerHTML=e[n][\"\"]+e[n].rtl;else if(\"string\"==typeof e[n])t.innerHTML=e[n];else{const r=t.value||\"\";null!=r&&e[n][r]&&(t.innerHTML=e[n][r])}}))}))}buildPickers(t,e){this.pickers=Array.from(t).map((t=>{if(t.classList.contains(\"ql-align\")&&(null==t.querySelector(\"option\")&&kt(t,vt),\"object\"==typeof e.align))return new pt(t,e.align);if(t.classList.contains(\"ql-background\")||t.classList.contains(\"ql-color\")){const n=t.classList.contains(\"ql-background\")?\"background\":\"color\";return null==t.querySelector(\"option\")&&kt(t,At,\"background\"===n?\"#ffffff\":\"#000000\"),new ft(t,e[n])}return null==t.querySelector(\"option\")&&(t.classList.contains(\"ql-font\")?kt(t,xt):t.classList.contains(\"ql-header\")?kt(t,Nt):t.classList.contains(\"ql-size\")&&kt(t,Et)),new dt(t)})),this.quill.on(bt.A.events.EDITOR_CHANGE,(()=>{this.pickers.forEach((t=>{t.update()}))}))}}wt.DEFAULTS=(0,mt.A)({},yt.A.DEFAULTS,{modules:{toolbar:{handlers:{formula(){this.quill.theme.tooltip.edit(\"formula\")},image(){let t=this.container.querySelector(\"input.ql-image[type=file]\");null==t&&(t=document.createElement(\"input\"),t.setAttribute(\"type\",\"file\"),t.setAttribute(\"accept\",this.quill.uploader.options.mimetypes.join(\", \")),t.classList.add(\"ql-image\"),t.addEventListener(\"change\",(()=>{const e=this.quill.getSelection(!0);this.quill.uploader.upload(e,t.files),t.value=\"\"})),this.container.appendChild(t)),t.click()},video(){this.quill.theme.tooltip.edit(\"video\")}}}}});class qt extends gt{constructor(t,e){super(t,e),this.textbox=this.root.querySelector('input[type=\"text\"]'),this.listen()}listen(){this.textbox.addEventListener(\"keydown\",(t=>{\"Enter\"===t.key?(this.save(),t.preventDefault()):\"Escape\"===t.key&&(this.cancel(),t.preventDefault())}))}cancel(){this.hide(),this.restoreFocus()}edit(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:\"link\",e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;if(this.root.classList.remove(\"ql-hidden\"),this.root.classList.add(\"ql-editing\"),null==this.textbox)return;null!=e?this.textbox.value=e:t!==this.root.getAttribute(\"data-mode\")&&(this.textbox.value=\"\");const n=this.quill.getBounds(this.quill.selection.savedRange);null!=n&&this.position(n),this.textbox.select(),this.textbox.setAttribute(\"placeholder\",this.textbox.getAttribute(`data-${t}`)||\"\"),this.root.setAttribute(\"data-mode\",t)}restoreFocus(){this.quill.focus({preventScroll:!0})}save(){let{value:t}=this.textbox;switch(this.root.getAttribute(\"data-mode\")){case\"link\":{const{scrollTop:e}=this.quill.root;this.linkRange?(this.quill.formatText(this.linkRange,\"link\",t,bt.A.sources.USER),delete this.linkRange):(this.restoreFocus(),this.quill.format(\"link\",t,bt.A.sources.USER)),this.quill.root.scrollTop=e;break}case\"video\":t=function(t){let e=t.match(/^(?:(https?):\\/\\/)?(?:(?:www|m)\\.)?youtube\\.com\\/watch.*v=([a-zA-Z0-9_-]+)/)||t.match(/^(?:(https?):\\/\\/)?(?:(?:www|m)\\.)?youtu\\.be\\/([a-zA-Z0-9_-]+)/);return e?`${e[1]||\"https\"}://www.youtube.com/embed/${e[2]}?showinfo=0`:(e=t.match(/^(?:(https?):\\/\\/)?(?:www\\.)?vimeo\\.com\\/(\\d+)/))?`${e[1]||\"https\"}://player.vimeo.com/video/${e[2]}/`:t}(t);case\"formula\":{if(!t)break;const e=this.quill.getSelection(!0);if(null!=e){const n=e.index+e.length;this.quill.insertEmbed(n,this.root.getAttribute(\"data-mode\"),t,bt.A.sources.USER),\"formula\"===this.root.getAttribute(\"data-mode\")&&this.quill.insertText(n+1,\" \",bt.A.sources.USER),this.quill.setSelection(n+2,bt.A.sources.USER)}break}}this.textbox.value=\"\",this.hide()}}function kt(t,e){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];e.forEach((e=>{const r=document.createElement(\"option\");e===n?r.setAttribute(\"selected\",\"selected\"):r.setAttribute(\"value\",String(e)),t.appendChild(r)}))}var _t=n(8298);const Lt=[[\"bold\",\"italic\",\"link\"],[{header:1},{header:2},\"blockquote\"]];class St extends qt{static TEMPLATE=['<span class=\"ql-tooltip-arrow\"></span>','<div class=\"ql-tooltip-editor\">','<input type=\"text\" data-formula=\"e=mc^2\" data-link=\"https://quilljs.com\" data-video=\"Embed URL\">','<a class=\"ql-close\"></a>',\"</div>\"].join(\"\");constructor(t,e){super(t,e),this.quill.on(bt.A.events.EDITOR_CHANGE,((t,e,n,r)=>{if(t===bt.A.events.SELECTION_CHANGE)if(null!=e&&e.length>0&&r===bt.A.sources.USER){this.show(),this.root.style.left=\"0px\",this.root.style.width=\"\",this.root.style.width=`${this.root.offsetWidth}px`;const t=this.quill.getLines(e.index,e.length);if(1===t.length){const t=this.quill.getBounds(e);null!=t&&this.position(t)}else{const n=t[t.length-1],r=this.quill.getIndex(n),i=Math.min(n.length()-1,e.index+e.length-r),s=this.quill.getBounds(new _t.Q(r,i));null!=s&&this.position(s)}}else document.activeElement!==this.textbox&&this.quill.hasFocus()&&this.hide()}))}listen(){super.listen(),this.root.querySelector(\".ql-close\").addEventListener(\"click\",(()=>{this.root.classList.remove(\"ql-editing\")})),this.quill.on(bt.A.events.SCROLL_OPTIMIZE,(()=>{setTimeout((()=>{if(this.root.classList.contains(\"ql-hidden\"))return;const t=this.quill.getSelection();if(null!=t){const e=this.quill.getBounds(t);null!=e&&this.position(e)}}),1)}))}cancel(){this.show()}position(t){const e=super.position(t),n=this.root.querySelector(\".ql-tooltip-arrow\");return n.style.marginLeft=\"\",0!==e&&(n.style.marginLeft=-1*e-n.offsetWidth/2+\"px\"),e}}class Ot extends wt{constructor(t,e){null!=e.modules.toolbar&&null==e.modules.toolbar.container&&(e.modules.toolbar.container=Lt),super(t,e),this.quill.container.classList.add(\"ql-bubble\")}extendToolbar(t){this.tooltip=new St(this.quill,this.options.bounds),null!=t.container&&(this.tooltip.root.appendChild(t.container),this.buildButtons(t.container.querySelectorAll(\"button\"),ct),this.buildPickers(t.container.querySelectorAll(\"select\"),ct))}}Ot.DEFAULTS=(0,mt.A)({},wt.DEFAULTS,{modules:{toolbar:{handlers:{link(t){t?this.quill.theme.tooltip.edit():this.quill.format(\"link\",!1,p.Ay.sources.USER)}}}}});const Tt=[[{header:[\"1\",\"2\",\"3\",!1]}],[\"bold\",\"italic\",\"underline\",\"link\"],[{list:\"ordered\"},{list:\"bullet\"}],[\"clean\"]];class jt extends qt{static TEMPLATE=['<a class=\"ql-preview\" rel=\"noopener noreferrer\" target=\"_blank\" href=\"about:blank\"></a>','<input type=\"text\" data-formula=\"e=mc^2\" data-link=\"https://quilljs.com\" data-video=\"Embed URL\">','<a class=\"ql-action\"></a>','<a class=\"ql-remove\"></a>'].join(\"\");preview=this.root.querySelector(\"a.ql-preview\");listen(){super.listen(),this.root.querySelector(\"a.ql-action\").addEventListener(\"click\",(t=>{this.root.classList.contains(\"ql-editing\")?this.save():this.edit(\"link\",this.preview.textContent),t.preventDefault()})),this.root.querySelector(\"a.ql-remove\").addEventListener(\"click\",(t=>{if(null!=this.linkRange){const t=this.linkRange;this.restoreFocus(),this.quill.formatText(t,\"link\",!1,bt.A.sources.USER),delete this.linkRange}t.preventDefault(),this.hide()})),this.quill.on(bt.A.events.SELECTION_CHANGE,((t,e,n)=>{if(null!=t){if(0===t.length&&n===bt.A.sources.USER){const[e,n]=this.quill.scroll.descendant(w,t.index);if(null!=e){this.linkRange=new _t.Q(t.index-n,e.length());const r=w.formats(e.domNode);this.preview.textContent=r,this.preview.setAttribute(\"href\",r),this.show();const i=this.quill.getBounds(this.linkRange);return void(null!=i&&this.position(i))}}else delete this.linkRange;this.hide()}}))}show(){super.show(),this.root.removeAttribute(\"data-mode\")}}class Ct extends wt{constructor(t,e){null!=e.modules.toolbar&&null==e.modules.toolbar.container&&(e.modules.toolbar.container=Tt),super(t,e),this.quill.container.classList.add(\"ql-snow\")}extendToolbar(t){null!=t.container&&(t.container.classList.add(\"ql-snow\"),this.buildButtons(t.container.querySelectorAll(\"button\"),ct),this.buildPickers(t.container.querySelectorAll(\"select\"),ct),this.tooltip=new jt(this.quill,this.options.bounds),t.container.querySelector(\".ql-link\")&&this.quill.keyboard.addBinding({key:\"k\",shortKey:!0},((e,n)=>{t.handlers.link.call(t,!n.format.link)})))}}Ct.DEFAULTS=(0,mt.A)({},wt.DEFAULTS,{modules:{toolbar:{handlers:{link(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;let e=this.quill.getText(t);/^\\S+@\\S+\\.\\S+$/.test(e)&&0!==e.indexOf(\"mailto:\")&&(e=`mailto:${e}`);const{tooltip:n}=this.quill.theme;n.edit(\"link\",e)}else this.quill.format(\"link\",!1,p.Ay.sources.USER)}}}}});var Rt=Ct;t.default.register({\"attributors/attribute/direction\":i.Mc,\"attributors/class/align\":e.qh,\"attributors/class/background\":b.l,\"attributors/class/color\":y.g3,\"attributors/class/direction\":i.sY,\"attributors/class/font\":v.q,\"attributors/class/size\":A.U,\"attributors/style/align\":e.Hu,\"attributors/style/background\":b.s,\"attributors/style/color\":y.JM,\"attributors/style/direction\":i.VL,\"attributors/style/font\":v.z,\"attributors/style/size\":A.r},!0),t.default.register({\"formats/align\":e.qh,\"formats/direction\":i.sY,\"formats/indent\":l,\"formats/background\":b.s,\"formats/color\":y.JM,\"formats/font\":v.q,\"formats/size\":A.U,\"formats/blockquote\":u,\"formats/code-block\":D.Ay,\"formats/header\":d,\"formats/list\":m,\"formats/bold\":E,\"formats/code\":D.Cy,\"formats/italic\":class extends E{static blotName=\"italic\";static tagName=[\"EM\",\"I\"]},\"formats/link\":w,\"formats/script\":_,\"formats/strike\":class extends E{static blotName=\"strike\";static tagName=[\"S\",\"STRIKE\"]},\"formats/underline\":S,\"formats/formula\":j,\"formats/image\":I,\"formats/video\":U,\"modules/syntax\":Q,\"modules/table\":it,\"modules/toolbar\":ot,\"themes/bubble\":Ot,\"themes/snow\":Rt,\"ui/icons\":ct,\"ui/picker\":dt,\"ui/icon-picker\":pt,\"ui/color-picker\":ft,\"ui/tooltip\":gt},!0);var It=t.default}(),r.default}()}));\n//# sourceMappingURL=quill.js.map\n"
  },
  {
    "path": "assets/vendor/quill/quill.js.LICENSE.txt",
    "content": "Copyright (c) 2017-2024, Slab\nCopyright (c) 2014, Jason Chen\nCopyright (c) 2013, salesforce.com\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "assets/vendor/quill/quill.module.wordcount.js",
    "content": "console.warn( 'Direct loading of this file is deprecated and it will be removed in the next major release of LifterLMS!' );\nconsole.warn( 'Instead, enqueue the registered script `llms-quill-wordcount`' );\n!function(){var n={54:function(n){n.exports=(()=>{\"use strict\";var n={314:(n,t,e)=>{e.r(t),e.d(t,{default:()=>a,wordsCount:()=>u,wordsSplit:()=>l,wordsDetect:()=>i});var r=[\",\",\"，\",\".\",\"。\",\":\",\"：\",\";\",\"；\",\"[\",\"]\",\"【\",\"]\",\"】\",\"{\",\"｛\",\"}\",\"｝\",\"(\",\"（\",\")\",\"）\",\"<\",\"《\",\">\",\"》\",\"$\",\"￥\",\"!\",\"！\",\"?\",\"？\",\"~\",\"～\",\"'\",\"’\",'\"',\"“\",\"”\",\"*\",\"/\",\"\\\\\",\"&\",\"%\",\"@\",\"#\",\"^\",\"、\",\"、\",\"、\",\"、\"],o={words:[],count:0},i=function(n){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!n)return o;var e=String(n);if(\"\"===e.trim())return o;var i=t.punctuationAsBreaker?\" \":\"\",u=t.disableDefaultPunctuation?[]:r,l=t.punctuation||[];u.concat(l).forEach((function(n){var t=new RegExp(\"\\\\\"+n,\"g\");e=e.replace(t,i)})),e=(e=(e=(e=e.replace(/[\\uFF00-\\uFFEF\\u2000-\\u206F]/g,\"\")).replace(/\\s+/,\" \")).split(\" \")).filter((function(n){return n.trim()}));var a=new RegExp(\"(\\\\d+)|[a-zA-ZÀ-ÿĀ-ſƀ-ɏɐ-ʯḀ-ỿЀ-ӿԀ-ԯഀ-ൿ]+|[⺀-⻿⼀-⿟　-〿㇀-㇯㈀-㋿㌀-㏿㐀-㿿䀀-䶿一-俿倀-忿怀-濿瀀-翿耀-迿退-鿿豈-﫿぀-ゟ゠-ヿㇰ-ㇿ㆐-㆟ᄀ-ᇿ㄰-㆏ꥠ-꥿가-꿿뀀-뿿쀀-쿿퀀-힯ힰ-퟿]\",\"g\"),c=[];return e.forEach((function(n){var t,e=[];do{(t=a.exec(n))&&e.push(t[0])}while(t);0===e.length?c.push(n):c=c.concat(e)})),{words:c,count:c.length}},u=function(n){return i(n,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).count},l=function(n){return i(n,arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).words};const a=u}},t={};function e(r){if(t[r])return t[r].exports;var o=t[r]={exports:{}};return n[r](o,o.exports,e),o.exports}return e.d=(n,t)=>{for(var r in t)e.o(t,r)&&!e.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:t[r]})},e.o=(n,t)=>Object.prototype.hasOwnProperty.call(n,t),e.r=n=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(n,\"__esModule\",{value:!0})},e(314)})()}},t={};function e(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return n[r].call(i.exports,i,i.exports,e),i.exports}e.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(t,{a:t}),t},e.d=function(n,t){for(var r in t)e.o(t,r)&&!e.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:t[r]})},e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},function(){\"use strict\";var n=e(54),t=e.n(n);function r(n){return(new Intl.NumberFormat).format(n)}function o(n,t,e){const o=document.createElement(\"i\");return o.className=`ql-wordcount-${n}`,o.style.opacity=\"0.5\",o.style.marginRight=\"10px\",o.innerHTML=`${t}: ${r(e)}`,o}function i(n){const{l10n:t,min:e,max:r}=n,i=document.createElement(\"div\");return i.className=\"ql-wordcount ql-toolbar ql-snow\",i.style.marginTop=\"-1px\",i.style.fontSize=\"85%\",e&&i.appendChild(o(\"min\",t.min,e)),r&&i.appendChild(o(\"max\",t.max,r)),i}function u(n,t){const{min:e,max:r,colorWarning:o,colorError:i}=t;let u=\"initial\";return e&&n<e||r&&n>r?u=i:r&&n>=.9*r&&(u=o),u}function l(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return n={min:null,max:null,colorWarning:\"#ff922b\",colorError:\"#e5554e\",onChange:(n,t,e)=>{},l10n:{},...n},n.l10n={singular:\"word\",plural:\"words\",min:\"Minimum\",max:\"Maximum\",...n.l10n},n}function a(n){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};e=l(e);const o=i(e),a=document.createElement(\"span\");a.className=\"ql-wordcount-counter\",a.style.float=\"right\",o.appendChild(a);const c=()=>{const o=t()(n.getText());a.style.color=u(o,e);const i=1===o?e.l10n.singular:e.l10n.plural;a.innerHTML=r(o)+\" \"+i,e.onChange(n,e,o)};c(),n.container.parentNode.insertBefore(o,n.container.nextSibling),n.on(\"text-change\",c)}!function(){const{Quill:n}=window;void 0!==n&&n.register(\"modules/wordcount\",a)}()}()}();"
  },
  {
    "path": "assets/vendor/select2/css/select2.css",
    "content": ".select2-container {\n  box-sizing: border-box;\n  display: inline-block;\n  margin: 0;\n  position: relative;\n  vertical-align: middle; }\n  .select2-container .select2-selection--single {\n    box-sizing: border-box;\n    cursor: pointer;\n    display: block;\n    height: 28px;\n    user-select: none;\n    -webkit-user-select: none; }\n    .select2-container .select2-selection--single .select2-selection__rendered {\n      display: block;\n      padding-left: 8px;\n      padding-right: 20px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap; }\n    .select2-container .select2-selection--single .select2-selection__clear {\n      position: relative; }\n  .select2-container[dir=\"rtl\"] .select2-selection--single .select2-selection__rendered {\n    padding-right: 8px;\n    padding-left: 20px; }\n  .select2-container .select2-selection--multiple {\n    box-sizing: border-box;\n    cursor: pointer;\n    display: block;\n    min-height: 32px;\n    user-select: none;\n    -webkit-user-select: none; }\n    .select2-container .select2-selection--multiple .select2-selection__rendered {\n      display: inline-block;\n      overflow: hidden;\n      padding-left: 8px;\n      text-overflow: ellipsis;\n      white-space: nowrap; }\n  .select2-container .select2-search--inline {\n    float: left; }\n    .select2-container .select2-search--inline .select2-search__field {\n      box-sizing: border-box;\n      border: none;\n      font-size: 100%;\n      margin-top: 5px;\n      padding: 0; }\n      .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {\n        -webkit-appearance: none; }\n\n.select2-dropdown {\n  background-color: white;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  box-sizing: border-box;\n  display: block;\n  position: absolute;\n  left: -100000px;\n  width: 100%;\n  z-index: 1051; }\n\n.select2-results {\n  display: block; }\n\n.select2-results__options {\n  list-style: none;\n  margin: 0;\n  padding: 0; }\n\n.select2-results__option {\n  padding: 6px;\n  user-select: none;\n  -webkit-user-select: none; }\n  .select2-results__option[aria-selected] {\n    cursor: pointer; }\n\n.select2-container--open .select2-dropdown {\n  left: 0; }\n\n.select2-container--open .select2-dropdown--above {\n  border-bottom: none;\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0; }\n\n.select2-container--open .select2-dropdown--below {\n  border-top: none;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0; }\n\n.select2-search--dropdown {\n  display: block;\n  padding: 4px; }\n  .select2-search--dropdown .select2-search__field {\n    padding: 4px;\n    width: 100%;\n    box-sizing: border-box; }\n    .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {\n      -webkit-appearance: none; }\n  .select2-search--dropdown.select2-search--hide {\n    display: none; }\n\n.select2-close-mask {\n  border: 0;\n  margin: 0;\n  padding: 0;\n  display: block;\n  position: fixed;\n  left: 0;\n  top: 0;\n  min-height: 100%;\n  min-width: 100%;\n  height: auto;\n  width: auto;\n  opacity: 0;\n  z-index: 99;\n  background-color: #fff;\n  filter: alpha(opacity=0); }\n\n.select2-hidden-accessible {\n  border: 0 !important;\n  clip: rect(0 0 0 0) !important;\n  -webkit-clip-path: inset(50%) !important;\n  clip-path: inset(50%) !important;\n  height: 1px !important;\n  overflow: hidden !important;\n  padding: 0 !important;\n  position: absolute !important;\n  width: 1px !important;\n  white-space: nowrap !important; }\n\n.select2-container--default .select2-selection--single {\n  background-color: #fff;\n  border: 1px solid #aaa;\n  border-radius: 4px; }\n  .select2-container--default .select2-selection--single .select2-selection__rendered {\n    color: #444;\n    line-height: 28px; }\n  .select2-container--default .select2-selection--single .select2-selection__clear {\n    cursor: pointer;\n    float: right;\n    font-weight: bold; }\n  .select2-container--default .select2-selection--single .select2-selection__placeholder {\n    color: #999; }\n  .select2-container--default .select2-selection--single .select2-selection__arrow {\n    height: 26px;\n    position: absolute;\n    top: 1px;\n    right: 1px;\n    width: 20px; }\n    .select2-container--default .select2-selection--single .select2-selection__arrow b {\n      border-color: #888 transparent transparent transparent;\n      border-style: solid;\n      border-width: 5px 4px 0 4px;\n      height: 0;\n      left: 50%;\n      margin-left: -4px;\n      margin-top: -2px;\n      position: absolute;\n      top: 50%;\n      width: 0; }\n\n.select2-container--default[dir=\"rtl\"] .select2-selection--single .select2-selection__clear {\n  float: left; }\n\n.select2-container--default[dir=\"rtl\"] .select2-selection--single .select2-selection__arrow {\n  left: 1px;\n  right: auto; }\n\n.select2-container--default.select2-container--disabled .select2-selection--single {\n  background-color: #eee;\n  cursor: default; }\n  .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {\n    display: none; }\n\n.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {\n  border-color: transparent transparent #888 transparent;\n  border-width: 0 4px 5px 4px; }\n\n.select2-container--default .select2-selection--multiple {\n  background-color: white;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  cursor: text; }\n  .select2-container--default .select2-selection--multiple .select2-selection__rendered {\n    box-sizing: border-box;\n    list-style: none;\n    margin: 0;\n    padding: 0 5px;\n    width: 100%; }\n    .select2-container--default .select2-selection--multiple .select2-selection__rendered li {\n      list-style: none; }\n  .select2-container--default .select2-selection--multiple .select2-selection__clear {\n    cursor: pointer;\n    float: right;\n    font-weight: bold;\n    margin-top: 5px;\n    margin-right: 10px;\n    padding: 1px; }\n  .select2-container--default .select2-selection--multiple .select2-selection__choice {\n    background-color: #e4e4e4;\n    border: 1px solid #aaa;\n    border-radius: 4px;\n    cursor: default;\n    float: left;\n    margin-right: 5px;\n    margin-top: 5px;\n    padding: 0 5px; }\n  .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {\n    color: #999;\n    cursor: pointer;\n    display: inline-block;\n    font-weight: bold;\n    margin-right: 2px; }\n    .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {\n      color: #333; }\n\n.select2-container--default[dir=\"rtl\"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir=\"rtl\"] .select2-selection--multiple .select2-search--inline {\n  float: right; }\n\n.select2-container--default[dir=\"rtl\"] .select2-selection--multiple .select2-selection__choice {\n  margin-left: 5px;\n  margin-right: auto; }\n\n.select2-container--default[dir=\"rtl\"] .select2-selection--multiple .select2-selection__choice__remove {\n  margin-left: 2px;\n  margin-right: auto; }\n\n.select2-container--default.select2-container--focus .select2-selection--multiple {\n  border: solid black 1px;\n  outline: 0; }\n\n.select2-container--default.select2-container--disabled .select2-selection--multiple {\n  background-color: #eee;\n  cursor: default; }\n\n.select2-container--default.select2-container--disabled .select2-selection__choice__remove {\n  display: none; }\n\n.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0; }\n\n.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0; }\n\n.select2-container--default .select2-search--dropdown .select2-search__field {\n  border: 1px solid #aaa; }\n\n.select2-container--default .select2-search--inline .select2-search__field {\n  background: transparent;\n  border: none;\n  outline: 0;\n  box-shadow: none;\n  -webkit-appearance: textfield; }\n\n.select2-container--default .select2-results > .select2-results__options {\n  max-height: 200px;\n  overflow-y: auto; }\n\n.select2-container--default .select2-results__option[role=group] {\n  padding: 0; }\n\n.select2-container--default .select2-results__option[aria-disabled=true] {\n  color: #999; }\n\n.select2-container--default .select2-results__option[aria-selected=true] {\n  background-color: #ddd; }\n\n.select2-container--default .select2-results__option .select2-results__option {\n  padding-left: 1em; }\n  .select2-container--default .select2-results__option .select2-results__option .select2-results__group {\n    padding-left: 0; }\n  .select2-container--default .select2-results__option .select2-results__option .select2-results__option {\n    margin-left: -1em;\n    padding-left: 2em; }\n    .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {\n      margin-left: -2em;\n      padding-left: 3em; }\n      .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {\n        margin-left: -3em;\n        padding-left: 4em; }\n        .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {\n          margin-left: -4em;\n          padding-left: 5em; }\n          .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {\n            margin-left: -5em;\n            padding-left: 6em; }\n\n.select2-container--default .select2-results__option--highlighted[aria-selected] {\n  background-color: #5897fb;\n  color: white; }\n\n.select2-container--default .select2-results__group {\n  cursor: default;\n  display: block;\n  padding: 6px; }\n\n.select2-container--classic .select2-selection--single {\n  background-color: #f7f7f7;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  outline: 0;\n  background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);\n  background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);\n  background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }\n  .select2-container--classic .select2-selection--single:focus {\n    border: 1px solid #5897fb; }\n  .select2-container--classic .select2-selection--single .select2-selection__rendered {\n    color: #444;\n    line-height: 28px; }\n  .select2-container--classic .select2-selection--single .select2-selection__clear {\n    cursor: pointer;\n    float: right;\n    font-weight: bold;\n    margin-right: 10px; }\n  .select2-container--classic .select2-selection--single .select2-selection__placeholder {\n    color: #999; }\n  .select2-container--classic .select2-selection--single .select2-selection__arrow {\n    background-color: #ddd;\n    border: none;\n    border-left: 1px solid #aaa;\n    border-top-right-radius: 4px;\n    border-bottom-right-radius: 4px;\n    height: 26px;\n    position: absolute;\n    top: 1px;\n    right: 1px;\n    width: 20px;\n    background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);\n    background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);\n    background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);\n    background-repeat: repeat-x;\n    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }\n    .select2-container--classic .select2-selection--single .select2-selection__arrow b {\n      border-color: #888 transparent transparent transparent;\n      border-style: solid;\n      border-width: 5px 4px 0 4px;\n      height: 0;\n      left: 50%;\n      margin-left: -4px;\n      margin-top: -2px;\n      position: absolute;\n      top: 50%;\n      width: 0; }\n\n.select2-container--classic[dir=\"rtl\"] .select2-selection--single .select2-selection__clear {\n  float: left; }\n\n.select2-container--classic[dir=\"rtl\"] .select2-selection--single .select2-selection__arrow {\n  border: none;\n  border-right: 1px solid #aaa;\n  border-radius: 0;\n  border-top-left-radius: 4px;\n  border-bottom-left-radius: 4px;\n  left: 1px;\n  right: auto; }\n\n.select2-container--classic.select2-container--open .select2-selection--single {\n  border: 1px solid #5897fb; }\n  .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {\n    background: transparent;\n    border: none; }\n    .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {\n      border-color: transparent transparent #888 transparent;\n      border-width: 0 4px 5px 4px; }\n\n.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {\n  border-top: none;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);\n  background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);\n  background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }\n\n.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {\n  border-bottom: none;\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0;\n  background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);\n  background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);\n  background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }\n\n.select2-container--classic .select2-selection--multiple {\n  background-color: white;\n  border: 1px solid #aaa;\n  border-radius: 4px;\n  cursor: text;\n  outline: 0; }\n  .select2-container--classic .select2-selection--multiple:focus {\n    border: 1px solid #5897fb; }\n  .select2-container--classic .select2-selection--multiple .select2-selection__rendered {\n    list-style: none;\n    margin: 0;\n    padding: 0 5px; }\n  .select2-container--classic .select2-selection--multiple .select2-selection__clear {\n    display: none; }\n  .select2-container--classic .select2-selection--multiple .select2-selection__choice {\n    background-color: #e4e4e4;\n    border: 1px solid #aaa;\n    border-radius: 4px;\n    cursor: default;\n    float: left;\n    margin-right: 5px;\n    margin-top: 5px;\n    padding: 0 5px; }\n  .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {\n    color: #888;\n    cursor: pointer;\n    display: inline-block;\n    font-weight: bold;\n    margin-right: 2px; }\n    .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {\n      color: #555; }\n\n.select2-container--classic[dir=\"rtl\"] .select2-selection--multiple .select2-selection__choice {\n  float: right;\n  margin-left: 5px;\n  margin-right: auto; }\n\n.select2-container--classic[dir=\"rtl\"] .select2-selection--multiple .select2-selection__choice__remove {\n  margin-left: 2px;\n  margin-right: auto; }\n\n.select2-container--classic.select2-container--open .select2-selection--multiple {\n  border: 1px solid #5897fb; }\n\n.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {\n  border-top: none;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0; }\n\n.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {\n  border-bottom: none;\n  border-bottom-left-radius: 0;\n  border-bottom-right-radius: 0; }\n\n.select2-container--classic .select2-search--dropdown .select2-search__field {\n  border: 1px solid #aaa;\n  outline: 0; }\n\n.select2-container--classic .select2-search--inline .select2-search__field {\n  outline: 0;\n  box-shadow: none; }\n\n.select2-container--classic .select2-dropdown {\n  background-color: white;\n  border: 1px solid transparent; }\n\n.select2-container--classic .select2-dropdown--above {\n  border-bottom: none; }\n\n.select2-container--classic .select2-dropdown--below {\n  border-top: none; }\n\n.select2-container--classic .select2-results > .select2-results__options {\n  max-height: 200px;\n  overflow-y: auto; }\n\n.select2-container--classic .select2-results__option[role=group] {\n  padding: 0; }\n\n.select2-container--classic .select2-results__option[aria-disabled=true] {\n  color: grey; }\n\n.select2-container--classic .select2-results__option--highlighted[aria-selected] {\n  background-color: #3875d7;\n  color: white; }\n\n.select2-container--classic .select2-results__group {\n  cursor: default;\n  display: block;\n  padding: 6px; }\n\n.select2-container--classic.select2-container--open .select2-dropdown {\n  border-color: #5897fb; }\n"
  },
  {
    "path": "assets/vendor/select2/js/select2.js",
    "content": "/*!\n * Select2 4.0.13\n * https://select2.github.io\n *\n * Released under the MIT license\n * https://github.com/select2/select2/blob/master/LICENSE.md\n */\n;(function (factory) {\n\n  // customized for LifterLMS\n  // http://stackoverflow.com/a/36815607/400568\n  var existingVersion = jQuery.fn.select2 || null;\n  if (existingVersion) {\n    delete jQuery.fn.select2;\n  }\n  // end customization\n\n  if (typeof define === 'function' && define.amd) {\n    // AMD. Register as an anonymous module.\n    define(['jquery'], factory);\n  } else if (typeof module === 'object' && module.exports) {\n    // Node/CommonJS\n    module.exports = function (root, jQuery) {\n      if (jQuery === undefined) {\n        // require('jQuery') returns a factory that requires window to\n        // build a jQuery instance, we normalize how we use modules\n        // that require this pattern but the window provided is a noop\n        // if it's defined (how jquery works)\n        if (typeof window !== 'undefined') {\n          jQuery = require('jquery');\n        }\n        else {\n          jQuery = require('jquery')(root);\n        }\n      }\n      factory(jQuery);\n      return jQuery;\n    };\n  } else {\n    // Browser globals\n    factory(jQuery);\n  }\n\n  // customized for LifterLMS\n  // http://stackoverflow.com/a/36815607/400568\n  jQuery.fn.llmsSelect2 = jQuery.fn.select2;\n  if (existingVersion) {\n    delete jQuery.fn.select2;\n    jQuery.fn.select2 = existingVersion;\n  }\n  // end customization\n\n} (function (jQuery) {\n  // This is needed so we can catch the AMD loader configuration and use it\n  // The inner file should be wrapped (by `banner.start.js`) in a function that\n  // returns the AMD loader references.\n  var S2 =(function () {\n  // Restore the Select2 AMD loader so it can be used\n  // Needed mostly in the language files, where the loader is not inserted\n  if (jQuery && jQuery.fn && jQuery.fn.select2 && jQuery.fn.select2.amd) {\n    var S2 = jQuery.fn.select2.amd;\n  }\nvar S2;(function () { if (!S2 || !S2.requirejs) {\nif (!S2) { S2 = {}; } else { require = S2; }\n/**\n * @license almond 0.3.3 Copyright jQuery Foundation and other contributors.\n * Released under MIT license, http://github.com/requirejs/almond/LICENSE\n */\n//Going sloppy to avoid 'use strict' string cost, but strict practices should\n//be followed.\n/*global setTimeout: false */\n\nvar requirejs, require, define;\n(function (undef) {\n    var main, req, makeMap, handlers,\n        defined = {},\n        waiting = {},\n        config = {},\n        defining = {},\n        hasOwn = Object.prototype.hasOwnProperty,\n        aps = [].slice,\n        jsSuffixRegExp = /\\.js$/;\n\n    function hasProp(obj, prop) {\n        return hasOwn.call(obj, prop);\n    }\n\n    /**\n     * Given a relative module name, like ./something, normalize it to\n     * a real name that can be mapped to a path.\n     * @param {String} name the relative name\n     * @param {String} baseName a real name that the name arg is relative\n     * to.\n     * @returns {String} normalized name\n     */\n    function normalize(name, baseName) {\n        var nameParts, nameSegment, mapValue, foundMap, lastIndex,\n            foundI, foundStarMap, starI, i, j, part, normalizedBaseParts,\n            baseParts = baseName && baseName.split(\"/\"),\n            map = config.map,\n            starMap = (map && map['*']) || {};\n\n        //Adjust any relative paths.\n        if (name) {\n            name = name.split('/');\n            lastIndex = name.length - 1;\n\n            // If wanting node ID compatibility, strip .js from end\n            // of IDs. Have to do this here, and not in nameToUrl\n            // because node allows either .js or non .js to map\n            // to same file.\n            if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {\n                name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');\n            }\n\n            // Starts with a '.' so need the baseName\n            if (name[0].charAt(0) === '.' && baseParts) {\n                //Convert baseName to array, and lop off the last part,\n                //so that . matches that 'directory' and not name of the baseName's\n                //module. For instance, baseName of 'one/two/three', maps to\n                //'one/two/three.js', but we want the directory, 'one/two' for\n                //this normalization.\n                normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);\n                name = normalizedBaseParts.concat(name);\n            }\n\n            //start trimDots\n            for (i = 0; i < name.length; i++) {\n                part = name[i];\n                if (part === '.') {\n                    name.splice(i, 1);\n                    i -= 1;\n                } else if (part === '..') {\n                    // If at the start, or previous value is still ..,\n                    // keep them so that when converted to a path it may\n                    // still work when converted to a path, even though\n                    // as an ID it is less than ideal. In larger point\n                    // releases, may be better to just kick out an error.\n                    if (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') {\n                        continue;\n                    } else if (i > 0) {\n                        name.splice(i - 1, 2);\n                        i -= 2;\n                    }\n                }\n            }\n            //end trimDots\n\n            name = name.join('/');\n        }\n\n        //Apply map config if available.\n        if ((baseParts || starMap) && map) {\n            nameParts = name.split('/');\n\n            for (i = nameParts.length; i > 0; i -= 1) {\n                nameSegment = nameParts.slice(0, i).join(\"/\");\n\n                if (baseParts) {\n                    //Find the longest baseName segment match in the config.\n                    //So, do joins on the biggest to smallest lengths of baseParts.\n                    for (j = baseParts.length; j > 0; j -= 1) {\n                        mapValue = map[baseParts.slice(0, j).join('/')];\n\n                        //baseName segment has  config, find if it has one for\n                        //this name.\n                        if (mapValue) {\n                            mapValue = mapValue[nameSegment];\n                            if (mapValue) {\n                                //Match, update name to the new value.\n                                foundMap = mapValue;\n                                foundI = i;\n                                break;\n                            }\n                        }\n                    }\n                }\n\n                if (foundMap) {\n                    break;\n                }\n\n                //Check for a star map match, but just hold on to it,\n                //if there is a shorter segment match later in a matching\n                //config, then favor over this star map.\n                if (!foundStarMap && starMap && starMap[nameSegment]) {\n                    foundStarMap = starMap[nameSegment];\n                    starI = i;\n                }\n            }\n\n            if (!foundMap && foundStarMap) {\n                foundMap = foundStarMap;\n                foundI = starI;\n            }\n\n            if (foundMap) {\n                nameParts.splice(0, foundI, foundMap);\n                name = nameParts.join('/');\n            }\n        }\n\n        return name;\n    }\n\n    function makeRequire(relName, forceSync) {\n        return function () {\n            //A version of a require function that passes a moduleName\n            //value for items that may need to\n            //look up paths relative to the moduleName\n            var args = aps.call(arguments, 0);\n\n            //If first arg is not require('string'), and there is only\n            //one arg, it is the array form without a callback. Insert\n            //a null so that the following concat is correct.\n            if (typeof args[0] !== 'string' && args.length === 1) {\n                args.push(null);\n            }\n            return req.apply(undef, args.concat([relName, forceSync]));\n        };\n    }\n\n    function makeNormalize(relName) {\n        return function (name) {\n            return normalize(name, relName);\n        };\n    }\n\n    function makeLoad(depName) {\n        return function (value) {\n            defined[depName] = value;\n        };\n    }\n\n    function callDep(name) {\n        if (hasProp(waiting, name)) {\n            var args = waiting[name];\n            delete waiting[name];\n            defining[name] = true;\n            main.apply(undef, args);\n        }\n\n        if (!hasProp(defined, name) && !hasProp(defining, name)) {\n            throw new Error('No ' + name);\n        }\n        return defined[name];\n    }\n\n    //Turns a plugin!resource to [plugin, resource]\n    //with the plugin being undefined if the name\n    //did not have a plugin prefix.\n    function splitPrefix(name) {\n        var prefix,\n            index = name ? name.indexOf('!') : -1;\n        if (index > -1) {\n            prefix = name.substring(0, index);\n            name = name.substring(index + 1, name.length);\n        }\n        return [prefix, name];\n    }\n\n    //Creates a parts array for a relName where first part is plugin ID,\n    //second part is resource ID. Assumes relName has already been normalized.\n    function makeRelParts(relName) {\n        return relName ? splitPrefix(relName) : [];\n    }\n\n    /**\n     * Makes a name map, normalizing the name, and using a plugin\n     * for normalization if necessary. Grabs a ref to plugin\n     * too, as an optimization.\n     */\n    makeMap = function (name, relParts) {\n        var plugin,\n            parts = splitPrefix(name),\n            prefix = parts[0],\n            relResourceName = relParts[1];\n\n        name = parts[1];\n\n        if (prefix) {\n            prefix = normalize(prefix, relResourceName);\n            plugin = callDep(prefix);\n        }\n\n        //Normalize according\n        if (prefix) {\n            if (plugin && plugin.normalize) {\n                name = plugin.normalize(name, makeNormalize(relResourceName));\n            } else {\n                name = normalize(name, relResourceName);\n            }\n        } else {\n            name = normalize(name, relResourceName);\n            parts = splitPrefix(name);\n            prefix = parts[0];\n            name = parts[1];\n            if (prefix) {\n                plugin = callDep(prefix);\n            }\n        }\n\n        //Using ridiculous property names for space reasons\n        return {\n            f: prefix ? prefix + '!' + name : name, //fullName\n            n: name,\n            pr: prefix,\n            p: plugin\n        };\n    };\n\n    function makeConfig(name) {\n        return function () {\n            return (config && config.config && config.config[name]) || {};\n        };\n    }\n\n    handlers = {\n        require: function (name) {\n            return makeRequire(name);\n        },\n        exports: function (name) {\n            var e = defined[name];\n            if (typeof e !== 'undefined') {\n                return e;\n            } else {\n                return (defined[name] = {});\n            }\n        },\n        module: function (name) {\n            return {\n                id: name,\n                uri: '',\n                exports: defined[name],\n                config: makeConfig(name)\n            };\n        }\n    };\n\n    main = function (name, deps, callback, relName) {\n        var cjsModule, depName, ret, map, i, relParts,\n            args = [],\n            callbackType = typeof callback,\n            usingExports;\n\n        //Use name if no relName\n        relName = relName || name;\n        relParts = makeRelParts(relName);\n\n        //Call the callback to define the module, if necessary.\n        if (callbackType === 'undefined' || callbackType === 'function') {\n            //Pull out the defined dependencies and pass the ordered\n            //values to the callback.\n            //Default to [require, exports, module] if no deps\n            deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;\n            for (i = 0; i < deps.length; i += 1) {\n                map = makeMap(deps[i], relParts);\n                depName = map.f;\n\n                //Fast path CommonJS standard dependencies.\n                if (depName === \"require\") {\n                    args[i] = handlers.require(name);\n                } else if (depName === \"exports\") {\n                    //CommonJS module spec 1.1\n                    args[i] = handlers.exports(name);\n                    usingExports = true;\n                } else if (depName === \"module\") {\n                    //CommonJS module spec 1.1\n                    cjsModule = args[i] = handlers.module(name);\n                } else if (hasProp(defined, depName) ||\n                           hasProp(waiting, depName) ||\n                           hasProp(defining, depName)) {\n                    args[i] = callDep(depName);\n                } else if (map.p) {\n                    map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});\n                    args[i] = defined[depName];\n                } else {\n                    throw new Error(name + ' missing ' + depName);\n                }\n            }\n\n            ret = callback ? callback.apply(defined[name], args) : undefined;\n\n            if (name) {\n                //If setting exports via \"module\" is in play,\n                //favor that over return value and exports. After that,\n                //favor a non-undefined return value over exports use.\n                if (cjsModule && cjsModule.exports !== undef &&\n                        cjsModule.exports !== defined[name]) {\n                    defined[name] = cjsModule.exports;\n                } else if (ret !== undef || !usingExports) {\n                    //Use the return value from the function.\n                    defined[name] = ret;\n                }\n            }\n        } else if (name) {\n            //May just be an object definition for the module. Only\n            //worry about defining if have a module name.\n            defined[name] = callback;\n        }\n    };\n\n    requirejs = require = req = function (deps, callback, relName, forceSync, alt) {\n        if (typeof deps === \"string\") {\n            if (handlers[deps]) {\n                //callback in this case is really relName\n                return handlers[deps](callback);\n            }\n            //Just return the module wanted. In this scenario, the\n            //deps arg is the module name, and second arg (if passed)\n            //is just the relName.\n            //Normalize module name, if it contains . or ..\n            return callDep(makeMap(deps, makeRelParts(callback)).f);\n        } else if (!deps.splice) {\n            //deps is a config object, not an array.\n            config = deps;\n            if (config.deps) {\n                req(config.deps, config.callback);\n            }\n            if (!callback) {\n                return;\n            }\n\n            if (callback.splice) {\n                //callback is an array, which means it is a dependency list.\n                //Adjust args if there are dependencies\n                deps = callback;\n                callback = relName;\n                relName = null;\n            } else {\n                deps = undef;\n            }\n        }\n\n        //Support require(['a'])\n        callback = callback || function () {};\n\n        //If relName is a function, it is an errback handler,\n        //so remove it.\n        if (typeof relName === 'function') {\n            relName = forceSync;\n            forceSync = alt;\n        }\n\n        //Simulate async callback;\n        if (forceSync) {\n            main(undef, deps, callback, relName);\n        } else {\n            //Using a non-zero value because of concern for what old browsers\n            //do, and latest browsers \"upgrade\" to 4 if lower value is used:\n            //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:\n            //If want a value immediately, use require('id') instead -- something\n            //that works in almond on the global level, but not guaranteed and\n            //unlikely to work in other AMD implementations.\n            setTimeout(function () {\n                main(undef, deps, callback, relName);\n            }, 4);\n        }\n\n        return req;\n    };\n\n    /**\n     * Just drops the config on the floor, but returns req in case\n     * the config return value is used.\n     */\n    req.config = function (cfg) {\n        return req(cfg);\n    };\n\n    /**\n     * Expose module registry for debugging and tooling\n     */\n    requirejs._defined = defined;\n\n    define = function (name, deps, callback) {\n        if (typeof name !== 'string') {\n            throw new Error('See almond README: incorrect module build, no module name');\n        }\n\n        //This module may not have dependencies\n        if (!deps.splice) {\n            //deps is not an array, so probably means\n            //an object literal or factory function for\n            //the value. Adjust args.\n            callback = deps;\n            deps = [];\n        }\n\n        if (!hasProp(defined, name) && !hasProp(waiting, name)) {\n            waiting[name] = [name, deps, callback];\n        }\n    };\n\n    define.amd = {\n        jQuery: true\n    };\n}());\n\nS2.requirejs = requirejs;S2.require = require;S2.define = define;\n}\n}());\nS2.define(\"almond\", function(){});\n\n/* global jQuery:false, $:false */\nS2.define('jquery',[],function () {\n  var _$ = jQuery || $;\n\n  if (_$ == null && console && console.error) {\n    console.error(\n      'Select2: An instance of jQuery or a jQuery-compatible library was not ' +\n      'found. Make sure that you are including jQuery before Select2 on your ' +\n      'web page.'\n    );\n  }\n\n  return _$;\n});\n\nS2.define('select2/utils',[\n  'jquery'\n], function ($) {\n  var Utils = {};\n\n  Utils.Extend = function (ChildClass, SuperClass) {\n    var __hasProp = {}.hasOwnProperty;\n\n    function BaseConstructor () {\n      this.constructor = ChildClass;\n    }\n\n    for (var key in SuperClass) {\n      if (__hasProp.call(SuperClass, key)) {\n        ChildClass[key] = SuperClass[key];\n      }\n    }\n\n    BaseConstructor.prototype = SuperClass.prototype;\n    ChildClass.prototype = new BaseConstructor();\n    ChildClass.__super__ = SuperClass.prototype;\n\n    return ChildClass;\n  };\n\n  function getMethods (theClass) {\n    var proto = theClass.prototype;\n\n    var methods = [];\n\n    for (var methodName in proto) {\n      var m = proto[methodName];\n\n      if (typeof m !== 'function') {\n        continue;\n      }\n\n      if (methodName === 'constructor') {\n        continue;\n      }\n\n      methods.push(methodName);\n    }\n\n    return methods;\n  }\n\n  Utils.Decorate = function (SuperClass, DecoratorClass) {\n    var decoratedMethods = getMethods(DecoratorClass);\n    var superMethods = getMethods(SuperClass);\n\n    function DecoratedClass () {\n      var unshift = Array.prototype.unshift;\n\n      var argCount = DecoratorClass.prototype.constructor.length;\n\n      var calledConstructor = SuperClass.prototype.constructor;\n\n      if (argCount > 0) {\n        unshift.call(arguments, SuperClass.prototype.constructor);\n\n        calledConstructor = DecoratorClass.prototype.constructor;\n      }\n\n      calledConstructor.apply(this, arguments);\n    }\n\n    DecoratorClass.displayName = SuperClass.displayName;\n\n    function ctr () {\n      this.constructor = DecoratedClass;\n    }\n\n    DecoratedClass.prototype = new ctr();\n\n    for (var m = 0; m < superMethods.length; m++) {\n      var superMethod = superMethods[m];\n\n      DecoratedClass.prototype[superMethod] =\n        SuperClass.prototype[superMethod];\n    }\n\n    var calledMethod = function (methodName) {\n      // Stub out the original method if it's not decorating an actual method\n      var originalMethod = function () {};\n\n      if (methodName in DecoratedClass.prototype) {\n        originalMethod = DecoratedClass.prototype[methodName];\n      }\n\n      var decoratedMethod = DecoratorClass.prototype[methodName];\n\n      return function () {\n        var unshift = Array.prototype.unshift;\n\n        unshift.call(arguments, originalMethod);\n\n        return decoratedMethod.apply(this, arguments);\n      };\n    };\n\n    for (var d = 0; d < decoratedMethods.length; d++) {\n      var decoratedMethod = decoratedMethods[d];\n\n      DecoratedClass.prototype[decoratedMethod] = calledMethod(decoratedMethod);\n    }\n\n    return DecoratedClass;\n  };\n\n  var Observable = function () {\n    this.listeners = {};\n  };\n\n  Observable.prototype.on = function (event, callback) {\n    this.listeners = this.listeners || {};\n\n    if (event in this.listeners) {\n      this.listeners[event].push(callback);\n    } else {\n      this.listeners[event] = [callback];\n    }\n  };\n\n  Observable.prototype.trigger = function (event) {\n    var slice = Array.prototype.slice;\n    var params = slice.call(arguments, 1);\n\n    this.listeners = this.listeners || {};\n\n    // Params should always come in as an array\n    if (params == null) {\n      params = [];\n    }\n\n    // If there are no arguments to the event, use a temporary object\n    if (params.length === 0) {\n      params.push({});\n    }\n\n    // Set the `_type` of the first object to the event\n    params[0]._type = event;\n\n    if (event in this.listeners) {\n      this.invoke(this.listeners[event], slice.call(arguments, 1));\n    }\n\n    if ('*' in this.listeners) {\n      this.invoke(this.listeners['*'], arguments);\n    }\n  };\n\n  Observable.prototype.invoke = function (listeners, params) {\n    for (var i = 0, len = listeners.length; i < len; i++) {\n      listeners[i].apply(this, params);\n    }\n  };\n\n  Utils.Observable = Observable;\n\n  Utils.generateChars = function (length) {\n    var chars = '';\n\n    for (var i = 0; i < length; i++) {\n      var randomChar = Math.floor(Math.random() * 36);\n      chars += randomChar.toString(36);\n    }\n\n    return chars;\n  };\n\n  Utils.bind = function (func, context) {\n    return function () {\n      func.apply(context, arguments);\n    };\n  };\n\n  Utils._convertData = function (data) {\n    for (var originalKey in data) {\n      var keys = originalKey.split('-');\n\n      var dataLevel = data;\n\n      if (keys.length === 1) {\n        continue;\n      }\n\n      for (var k = 0; k < keys.length; k++) {\n        var key = keys[k];\n\n        // Lowercase the first letter\n        // By default, dash-separated becomes camelCase\n        key = key.substring(0, 1).toLowerCase() + key.substring(1);\n\n        if (!(key in dataLevel)) {\n          dataLevel[key] = {};\n        }\n\n        if (k == keys.length - 1) {\n          dataLevel[key] = data[originalKey];\n        }\n\n        dataLevel = dataLevel[key];\n      }\n\n      delete data[originalKey];\n    }\n\n    return data;\n  };\n\n  Utils.hasScroll = function (index, el) {\n    // Adapted from the function created by @ShadowScripter\n    // and adapted by @BillBarry on the Stack Exchange Code Review website.\n    // The original code can be found at\n    // http://codereview.stackexchange.com/q/13338\n    // and was designed to be used with the Sizzle selector engine.\n\n    var $el = $(el);\n    var overflowX = el.style.overflowX;\n    var overflowY = el.style.overflowY;\n\n    //Check both x and y declarations\n    if (overflowX === overflowY &&\n        (overflowY === 'hidden' || overflowY === 'visible')) {\n      return false;\n    }\n\n    if (overflowX === 'scroll' || overflowY === 'scroll') {\n      return true;\n    }\n\n    return ($el.innerHeight() < el.scrollHeight ||\n      $el.innerWidth() < el.scrollWidth);\n  };\n\n  Utils.escapeMarkup = function (markup) {\n    var replaceMap = {\n      '\\\\': '&#92;',\n      '&': '&amp;',\n      '<': '&lt;',\n      '>': '&gt;',\n      '\"': '&quot;',\n      '\\'': '&#39;',\n      '/': '&#47;'\n    };\n\n    // Do not try to escape the markup if it's not a string\n    if (typeof markup !== 'string') {\n      return markup;\n    }\n\n    return String(markup).replace(/[&<>\"'\\/\\\\]/g, function (match) {\n      return replaceMap[match];\n    });\n  };\n\n  // Append an array of jQuery nodes to a given element.\n  Utils.appendMany = function ($element, $nodes) {\n    // jQuery 1.7.x does not support $.fn.append() with an array\n    // Fall back to a jQuery object collection using $.fn.add()\n    if ($.fn.jquery.substr(0, 3) === '1.7') {\n      var $jqNodes = $();\n\n      $.map($nodes, function (node) {\n        $jqNodes = $jqNodes.add(node);\n      });\n\n      $nodes = $jqNodes;\n    }\n\n    $element.append($nodes);\n  };\n\n  // Cache objects in Utils.__cache instead of $.data (see #4346)\n  Utils.__cache = {};\n\n  var id = 0;\n  Utils.GetUniqueElementId = function (element) {\n    // Get a unique element Id. If element has no id,\n    // creates a new unique number, stores it in the id\n    // attribute and returns the new id.\n    // If an id already exists, it simply returns it.\n\n    var select2Id = element.getAttribute('data-select2-id');\n    if (select2Id == null) {\n      // If element has id, use it.\n      if (element.id) {\n        select2Id = element.id;\n        element.setAttribute('data-select2-id', select2Id);\n      } else {\n        element.setAttribute('data-select2-id', ++id);\n        select2Id = id.toString();\n      }\n    }\n    return select2Id;\n  };\n\n  Utils.StoreData = function (element, name, value) {\n    // Stores an item in the cache for a specified element.\n    // name is the cache key.\n    var id = Utils.GetUniqueElementId(element);\n    if (!Utils.__cache[id]) {\n      Utils.__cache[id] = {};\n    }\n\n    Utils.__cache[id][name] = value;\n  };\n\n  Utils.GetData = function (element, name) {\n    // Retrieves a value from the cache by its key (name)\n    // name is optional. If no name specified, return\n    // all cache items for the specified element.\n    // and for a specified element.\n    var id = Utils.GetUniqueElementId(element);\n    if (name) {\n      if (Utils.__cache[id]) {\n        if (Utils.__cache[id][name] != null) {\n          return Utils.__cache[id][name];\n        }\n        return $(element).data(name); // Fallback to HTML5 data attribs.\n      }\n      return $(element).data(name); // Fallback to HTML5 data attribs.\n    } else {\n      return Utils.__cache[id];\n    }\n  };\n\n  Utils.RemoveData = function (element) {\n    // Removes all cached items for a specified element.\n    var id = Utils.GetUniqueElementId(element);\n    if (Utils.__cache[id] != null) {\n      delete Utils.__cache[id];\n    }\n\n    element.removeAttribute('data-select2-id');\n  };\n\n  return Utils;\n});\n\nS2.define('select2/results',[\n  'jquery',\n  './utils'\n], function ($, Utils) {\n  function Results ($element, options, dataAdapter) {\n    this.$element = $element;\n    this.data = dataAdapter;\n    this.options = options;\n\n    Results.__super__.constructor.call(this);\n  }\n\n  Utils.Extend(Results, Utils.Observable);\n\n  Results.prototype.render = function () {\n    var $results = $(\n      '<ul class=\"select2-results__options\" role=\"listbox\"></ul>'\n    );\n\n    if (this.options.get('multiple')) {\n      $results.attr('aria-multiselectable', 'true');\n    }\n\n    this.$results = $results;\n\n    return $results;\n  };\n\n  Results.prototype.clear = function () {\n    this.$results.empty();\n  };\n\n  Results.prototype.displayMessage = function (params) {\n    var escapeMarkup = this.options.get('escapeMarkup');\n\n    this.clear();\n    this.hideLoading();\n\n    var $message = $(\n      '<li role=\"alert\" aria-live=\"assertive\"' +\n      ' class=\"select2-results__option\"></li>'\n    );\n\n    var message = this.options.get('translations').get(params.message);\n\n    $message.append(\n      escapeMarkup(\n        message(params.args)\n      )\n    );\n\n    $message[0].className += ' select2-results__message';\n\n    this.$results.append($message);\n  };\n\n  Results.prototype.hideMessages = function () {\n    this.$results.find('.select2-results__message').remove();\n  };\n\n  Results.prototype.append = function (data) {\n    this.hideLoading();\n\n    var $options = [];\n\n    if (data.results == null || data.results.length === 0) {\n      if (this.$results.children().length === 0) {\n        this.trigger('results:message', {\n          message: 'noResults'\n        });\n      }\n\n      return;\n    }\n\n    data.results = this.sort(data.results);\n\n    for (var d = 0; d < data.results.length; d++) {\n      var item = data.results[d];\n\n      var $option = this.option(item);\n\n      $options.push($option);\n    }\n\n    this.$results.append($options);\n  };\n\n  Results.prototype.position = function ($results, $dropdown) {\n    var $resultsContainer = $dropdown.find('.select2-results');\n    $resultsContainer.append($results);\n  };\n\n  Results.prototype.sort = function (data) {\n    var sorter = this.options.get('sorter');\n\n    return sorter(data);\n  };\n\n  Results.prototype.highlightFirstItem = function () {\n    var $options = this.$results\n      .find('.select2-results__option[aria-selected]');\n\n    var $selected = $options.filter('[aria-selected=true]');\n\n    // Check if there are any selected options\n    if ($selected.length > 0) {\n      // If there are selected options, highlight the first\n      $selected.first().trigger('mouseenter');\n    } else {\n      // If there are no selected options, highlight the first option\n      // in the dropdown\n      $options.first().trigger('mouseenter');\n    }\n\n    this.ensureHighlightVisible();\n  };\n\n  Results.prototype.setClasses = function () {\n    var self = this;\n\n    this.data.current(function (selected) {\n      var selectedIds = $.map(selected, function (s) {\n        return s.id.toString();\n      });\n\n      var $options = self.$results\n        .find('.select2-results__option[aria-selected]');\n\n      $options.each(function () {\n        var $option = $(this);\n\n        var item = Utils.GetData(this, 'data');\n\n        // id needs to be converted to a string when comparing\n        var id = '' + item.id;\n\n        if ((item.element != null && item.element.selected) ||\n            (item.element == null && $.inArray(id, selectedIds) > -1)) {\n          $option.attr('aria-selected', 'true');\n        } else {\n          $option.attr('aria-selected', 'false');\n        }\n      });\n\n    });\n  };\n\n  Results.prototype.showLoading = function (params) {\n    this.hideLoading();\n\n    var loadingMore = this.options.get('translations').get('searching');\n\n    var loading = {\n      disabled: true,\n      loading: true,\n      text: loadingMore(params)\n    };\n    var $loading = this.option(loading);\n    $loading.className += ' loading-results';\n\n    this.$results.prepend($loading);\n  };\n\n  Results.prototype.hideLoading = function () {\n    this.$results.find('.loading-results').remove();\n  };\n\n  Results.prototype.option = function (data) {\n    var option = document.createElement('li');\n    option.className = 'select2-results__option';\n\n    var attrs = {\n      'role': 'option',\n      'aria-selected': 'false'\n    };\n\n    var matches = window.Element.prototype.matches ||\n      window.Element.prototype.msMatchesSelector ||\n      window.Element.prototype.webkitMatchesSelector;\n\n    if ((data.element != null && matches.call(data.element, ':disabled')) ||\n        (data.element == null && data.disabled)) {\n      delete attrs['aria-selected'];\n      attrs['aria-disabled'] = 'true';\n    }\n\n    if (data.id == null) {\n      delete attrs['aria-selected'];\n    }\n\n    if (data._resultId != null) {\n      option.id = data._resultId;\n    }\n\n    if (data.title) {\n      option.title = data.title;\n    }\n\n    if (data.children) {\n      attrs.role = 'group';\n      attrs['aria-label'] = data.text;\n      delete attrs['aria-selected'];\n    }\n\n    for (var attr in attrs) {\n      var val = attrs[attr];\n\n      option.setAttribute(attr, val);\n    }\n\n    if (data.children) {\n      var $option = $(option);\n\n      var label = document.createElement('strong');\n      label.className = 'select2-results__group';\n\n      var $label = $(label);\n      this.template(data, label);\n\n      var $children = [];\n\n      for (var c = 0; c < data.children.length; c++) {\n        var child = data.children[c];\n\n        var $child = this.option(child);\n\n        $children.push($child);\n      }\n\n      var $childrenContainer = $('<ul></ul>', {\n        'class': 'select2-results__options select2-results__options--nested'\n      });\n\n      $childrenContainer.append($children);\n\n      $option.append(label);\n      $option.append($childrenContainer);\n    } else {\n      this.template(data, option);\n    }\n\n    Utils.StoreData(option, 'data', data);\n\n    return option;\n  };\n\n  Results.prototype.bind = function (container, $container) {\n    var self = this;\n\n    var id = container.id + '-results';\n\n    this.$results.attr('id', id);\n\n    container.on('results:all', function (params) {\n      self.clear();\n      self.append(params.data);\n\n      if (container.isOpen()) {\n        self.setClasses();\n        self.highlightFirstItem();\n      }\n    });\n\n    container.on('results:append', function (params) {\n      self.append(params.data);\n\n      if (container.isOpen()) {\n        self.setClasses();\n      }\n    });\n\n    container.on('query', function (params) {\n      self.hideMessages();\n      self.showLoading(params);\n    });\n\n    container.on('select', function () {\n      if (!container.isOpen()) {\n        return;\n      }\n\n      self.setClasses();\n\n      if (self.options.get('scrollAfterSelect')) {\n        self.highlightFirstItem();\n      }\n    });\n\n    container.on('unselect', function () {\n      if (!container.isOpen()) {\n        return;\n      }\n\n      self.setClasses();\n\n      if (self.options.get('scrollAfterSelect')) {\n        self.highlightFirstItem();\n      }\n    });\n\n    container.on('open', function () {\n      // When the dropdown is open, aria-expended=\"true\"\n      self.$results.attr('aria-expanded', 'true');\n      self.$results.attr('aria-hidden', 'false');\n\n      self.setClasses();\n      self.ensureHighlightVisible();\n    });\n\n    container.on('close', function () {\n      // When the dropdown is closed, aria-expended=\"false\"\n      self.$results.attr('aria-expanded', 'false');\n      self.$results.attr('aria-hidden', 'true');\n      self.$results.removeAttr('aria-activedescendant');\n    });\n\n    container.on('results:toggle', function () {\n      var $highlighted = self.getHighlightedResults();\n\n      if ($highlighted.length === 0) {\n        return;\n      }\n\n      $highlighted.trigger('mouseup');\n    });\n\n    container.on('results:select', function () {\n      var $highlighted = self.getHighlightedResults();\n\n      if ($highlighted.length === 0) {\n        return;\n      }\n\n      var data = Utils.GetData($highlighted[0], 'data');\n\n      if ($highlighted.attr('aria-selected') == 'true') {\n        self.trigger('close', {});\n      } else {\n        self.trigger('select', {\n          data: data\n        });\n      }\n    });\n\n    container.on('results:previous', function () {\n      var $highlighted = self.getHighlightedResults();\n\n      var $options = self.$results.find('[aria-selected]');\n\n      var currentIndex = $options.index($highlighted);\n\n      // If we are already at the top, don't move further\n      // If no options, currentIndex will be -1\n      if (currentIndex <= 0) {\n        return;\n      }\n\n      var nextIndex = currentIndex - 1;\n\n      // If none are highlighted, highlight the first\n      if ($highlighted.length === 0) {\n        nextIndex = 0;\n      }\n\n      var $next = $options.eq(nextIndex);\n\n      $next.trigger('mouseenter');\n\n      var currentOffset = self.$results.offset().top;\n      var nextTop = $next.offset().top;\n      var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset);\n\n      if (nextIndex === 0) {\n        self.$results.scrollTop(0);\n      } else if (nextTop - currentOffset < 0) {\n        self.$results.scrollTop(nextOffset);\n      }\n    });\n\n    container.on('results:next', function () {\n      var $highlighted = self.getHighlightedResults();\n\n      var $options = self.$results.find('[aria-selected]');\n\n      var currentIndex = $options.index($highlighted);\n\n      var nextIndex = currentIndex + 1;\n\n      // If we are at the last option, stay there\n      if (nextIndex >= $options.length) {\n        return;\n      }\n\n      var $next = $options.eq(nextIndex);\n\n      $next.trigger('mouseenter');\n\n      var currentOffset = self.$results.offset().top +\n        self.$results.outerHeight(false);\n      var nextBottom = $next.offset().top + $next.outerHeight(false);\n      var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset;\n\n      if (nextIndex === 0) {\n        self.$results.scrollTop(0);\n      } else if (nextBottom > currentOffset) {\n        self.$results.scrollTop(nextOffset);\n      }\n    });\n\n    container.on('results:focus', function (params) {\n      params.element.addClass('select2-results__option--highlighted');\n    });\n\n    container.on('results:message', function (params) {\n      self.displayMessage(params);\n    });\n\n    if ($.fn.mousewheel) {\n      this.$results.on('mousewheel', function (e) {\n        var top = self.$results.scrollTop();\n\n        var bottom = self.$results.get(0).scrollHeight - top + e.deltaY;\n\n        var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0;\n        var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height();\n\n        if (isAtTop) {\n          self.$results.scrollTop(0);\n\n          e.preventDefault();\n          e.stopPropagation();\n        } else if (isAtBottom) {\n          self.$results.scrollTop(\n            self.$results.get(0).scrollHeight - self.$results.height()\n          );\n\n          e.preventDefault();\n          e.stopPropagation();\n        }\n      });\n    }\n\n    this.$results.on('mouseup', '.select2-results__option[aria-selected]',\n      function (evt) {\n      var $this = $(this);\n\n      var data = Utils.GetData(this, 'data');\n\n      if ($this.attr('aria-selected') === 'true') {\n        if (self.options.get('multiple')) {\n          self.trigger('unselect', {\n            originalEvent: evt,\n            data: data\n          });\n        } else {\n          self.trigger('close', {});\n        }\n\n        return;\n      }\n\n      self.trigger('select', {\n        originalEvent: evt,\n        data: data\n      });\n    });\n\n    this.$results.on('mouseenter', '.select2-results__option[aria-selected]',\n      function (evt) {\n      var data = Utils.GetData(this, 'data');\n\n      self.getHighlightedResults()\n          .removeClass('select2-results__option--highlighted');\n\n      self.trigger('results:focus', {\n        data: data,\n        element: $(this)\n      });\n    });\n  };\n\n  Results.prototype.getHighlightedResults = function () {\n    var $highlighted = this.$results\n    .find('.select2-results__option--highlighted');\n\n    return $highlighted;\n  };\n\n  Results.prototype.destroy = function () {\n    this.$results.remove();\n  };\n\n  Results.prototype.ensureHighlightVisible = function () {\n    var $highlighted = this.getHighlightedResults();\n\n    if ($highlighted.length === 0) {\n      return;\n    }\n\n    var $options = this.$results.find('[aria-selected]');\n\n    var currentIndex = $options.index($highlighted);\n\n    var currentOffset = this.$results.offset().top;\n    var nextTop = $highlighted.offset().top;\n    var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset);\n\n    var offsetDelta = nextTop - currentOffset;\n    nextOffset -= $highlighted.outerHeight(false) * 2;\n\n    if (currentIndex <= 2) {\n      this.$results.scrollTop(0);\n    } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) {\n      this.$results.scrollTop(nextOffset);\n    }\n  };\n\n  Results.prototype.template = function (result, container) {\n    var template = this.options.get('templateResult');\n    var escapeMarkup = this.options.get('escapeMarkup');\n\n    var content = template(result, container);\n\n    if (content == null) {\n      container.style.display = 'none';\n    } else if (typeof content === 'string') {\n      container.innerHTML = escapeMarkup(content);\n    } else {\n      $(container).append(content);\n    }\n  };\n\n  return Results;\n});\n\nS2.define('select2/keys',[\n\n], function () {\n  var KEYS = {\n    BACKSPACE: 8,\n    TAB: 9,\n    ENTER: 13,\n    SHIFT: 16,\n    CTRL: 17,\n    ALT: 18,\n    ESC: 27,\n    SPACE: 32,\n    PAGE_UP: 33,\n    PAGE_DOWN: 34,\n    END: 35,\n    HOME: 36,\n    LEFT: 37,\n    UP: 38,\n    RIGHT: 39,\n    DOWN: 40,\n    DELETE: 46\n  };\n\n  return KEYS;\n});\n\nS2.define('select2/selection/base',[\n  'jquery',\n  '../utils',\n  '../keys'\n], function ($, Utils, KEYS) {\n  function BaseSelection ($element, options) {\n    this.$element = $element;\n    this.options = options;\n\n    BaseSelection.__super__.constructor.call(this);\n  }\n\n  Utils.Extend(BaseSelection, Utils.Observable);\n\n  BaseSelection.prototype.render = function () {\n    var $selection = $(\n      '<span class=\"select2-selection\" role=\"combobox\" ' +\n      ' aria-haspopup=\"true\" aria-expanded=\"false\">' +\n      '</span>'\n    );\n\n    this._tabindex = 0;\n\n    if (Utils.GetData(this.$element[0], 'old-tabindex') != null) {\n      this._tabindex = Utils.GetData(this.$element[0], 'old-tabindex');\n    } else if (this.$element.attr('tabindex') != null) {\n      this._tabindex = this.$element.attr('tabindex');\n    }\n\n    $selection.attr('title', this.$element.attr('title'));\n    $selection.attr('tabindex', this._tabindex);\n    $selection.attr('aria-disabled', 'false');\n\n    this.$selection = $selection;\n\n    return $selection;\n  };\n\n  BaseSelection.prototype.bind = function (container, $container) {\n    var self = this;\n\n    var resultsId = container.id + '-results';\n\n    this.container = container;\n\n    this.$selection.on('focus', function (evt) {\n      self.trigger('focus', evt);\n    });\n\n    this.$selection.on('blur', function (evt) {\n      self._handleBlur(evt);\n    });\n\n    this.$selection.on('keydown', function (evt) {\n      self.trigger('keypress', evt);\n\n      if (evt.which === KEYS.SPACE) {\n        evt.preventDefault();\n      }\n    });\n\n    container.on('results:focus', function (params) {\n      self.$selection.attr('aria-activedescendant', params.data._resultId);\n    });\n\n    container.on('selection:update', function (params) {\n      self.update(params.data);\n    });\n\n    container.on('open', function () {\n      // When the dropdown is open, aria-expanded=\"true\"\n      self.$selection.attr('aria-expanded', 'true');\n      self.$selection.attr('aria-owns', resultsId);\n\n      self._attachCloseHandler(container);\n    });\n\n    container.on('close', function () {\n      // When the dropdown is closed, aria-expanded=\"false\"\n      self.$selection.attr('aria-expanded', 'false');\n      self.$selection.removeAttr('aria-activedescendant');\n      self.$selection.removeAttr('aria-owns');\n\n      self.$selection.trigger('focus');\n\n      self._detachCloseHandler(container);\n    });\n\n    container.on('enable', function () {\n      self.$selection.attr('tabindex', self._tabindex);\n      self.$selection.attr('aria-disabled', 'false');\n    });\n\n    container.on('disable', function () {\n      self.$selection.attr('tabindex', '-1');\n      self.$selection.attr('aria-disabled', 'true');\n    });\n  };\n\n  BaseSelection.prototype._handleBlur = function (evt) {\n    var self = this;\n\n    // This needs to be delayed as the active element is the body when the tab\n    // key is pressed, possibly along with others.\n    window.setTimeout(function () {\n      // Don't trigger `blur` if the focus is still in the selection\n      if (\n        (document.activeElement == self.$selection[0]) ||\n        ($.contains(self.$selection[0], document.activeElement))\n      ) {\n        return;\n      }\n\n      self.trigger('blur', evt);\n    }, 1);\n  };\n\n  BaseSelection.prototype._attachCloseHandler = function (container) {\n\n    $(document.body).on('mousedown.select2.' + container.id, function (e) {\n      var $target = $(e.target);\n\n      var $select = $target.closest('.select2');\n\n      var $all = $('.select2.select2-container--open');\n\n      $all.each(function () {\n        if (this == $select[0]) {\n          return;\n        }\n\n        var $element = Utils.GetData(this, 'element');\n\n        $element.select2('close');\n      });\n    });\n  };\n\n  BaseSelection.prototype._detachCloseHandler = function (container) {\n    $(document.body).off('mousedown.select2.' + container.id);\n  };\n\n  BaseSelection.prototype.position = function ($selection, $container) {\n    var $selectionContainer = $container.find('.selection');\n    $selectionContainer.append($selection);\n  };\n\n  BaseSelection.prototype.destroy = function () {\n    this._detachCloseHandler(this.container);\n  };\n\n  BaseSelection.prototype.update = function (data) {\n    throw new Error('The `update` method must be defined in child classes.');\n  };\n\n  /**\n   * Helper method to abstract the \"enabled\" (not \"disabled\") state of this\n   * object.\n   *\n   * @return {true} if the instance is not disabled.\n   * @return {false} if the instance is disabled.\n   */\n  BaseSelection.prototype.isEnabled = function () {\n    return !this.isDisabled();\n  };\n\n  /**\n   * Helper method to abstract the \"disabled\" state of this object.\n   *\n   * @return {true} if the disabled option is true.\n   * @return {false} if the disabled option is false.\n   */\n  BaseSelection.prototype.isDisabled = function () {\n    return this.options.get('disabled');\n  };\n\n  return BaseSelection;\n});\n\nS2.define('select2/selection/single',[\n  'jquery',\n  './base',\n  '../utils',\n  '../keys'\n], function ($, BaseSelection, Utils, KEYS) {\n  function SingleSelection () {\n    SingleSelection.__super__.constructor.apply(this, arguments);\n  }\n\n  Utils.Extend(SingleSelection, BaseSelection);\n\n  SingleSelection.prototype.render = function () {\n    var $selection = SingleSelection.__super__.render.call(this);\n\n    $selection.addClass('select2-selection--single');\n\n    $selection.html(\n      '<span class=\"select2-selection__rendered\"></span>' +\n      '<span class=\"select2-selection__arrow\" role=\"presentation\">' +\n        '<b role=\"presentation\"></b>' +\n      '</span>'\n    );\n\n    return $selection;\n  };\n\n  SingleSelection.prototype.bind = function (container, $container) {\n    var self = this;\n\n    SingleSelection.__super__.bind.apply(this, arguments);\n\n    var id = container.id + '-container';\n\n    this.$selection.find('.select2-selection__rendered')\n      .attr('id', id)\n      .attr('role', 'textbox')\n      .attr('aria-readonly', 'true');\n    this.$selection.attr('aria-labelledby', id);\n\n    this.$selection.on('mousedown', function (evt) {\n      // Only respond to left clicks\n      if (evt.which !== 1) {\n        return;\n      }\n\n      self.trigger('toggle', {\n        originalEvent: evt\n      });\n    });\n\n    this.$selection.on('focus', function (evt) {\n      // User focuses on the container\n    });\n\n    this.$selection.on('blur', function (evt) {\n      // User exits the container\n    });\n\n    container.on('focus', function (evt) {\n      if (!container.isOpen()) {\n        self.$selection.trigger('focus');\n      }\n    });\n  };\n\n  SingleSelection.prototype.clear = function () {\n    var $rendered = this.$selection.find('.select2-selection__rendered');\n    $rendered.empty();\n    $rendered.removeAttr('title'); // clear tooltip on empty\n  };\n\n  SingleSelection.prototype.display = function (data, container) {\n    var template = this.options.get('templateSelection');\n    var escapeMarkup = this.options.get('escapeMarkup');\n\n    return escapeMarkup(template(data, container));\n  };\n\n  SingleSelection.prototype.selectionContainer = function () {\n    return $('<span></span>');\n  };\n\n  SingleSelection.prototype.update = function (data) {\n    if (data.length === 0) {\n      this.clear();\n      return;\n    }\n\n    var selection = data[0];\n\n    var $rendered = this.$selection.find('.select2-selection__rendered');\n    var formatted = this.display(selection, $rendered);\n\n    $rendered.empty().append(formatted);\n\n    var title = selection.title || selection.text;\n\n    if (title) {\n      $rendered.attr('title', title);\n    } else {\n      $rendered.removeAttr('title');\n    }\n  };\n\n  return SingleSelection;\n});\n\nS2.define('select2/selection/multiple',[\n  'jquery',\n  './base',\n  '../utils'\n], function ($, BaseSelection, Utils) {\n  function MultipleSelection ($element, options) {\n    MultipleSelection.__super__.constructor.apply(this, arguments);\n  }\n\n  Utils.Extend(MultipleSelection, BaseSelection);\n\n  MultipleSelection.prototype.render = function () {\n    var $selection = MultipleSelection.__super__.render.call(this);\n\n    $selection.addClass('select2-selection--multiple');\n\n    $selection.html(\n      '<ul class=\"select2-selection__rendered\"></ul>'\n    );\n\n    return $selection;\n  };\n\n  MultipleSelection.prototype.bind = function (container, $container) {\n    var self = this;\n\n    MultipleSelection.__super__.bind.apply(this, arguments);\n\n    this.$selection.on('click', function (evt) {\n      self.trigger('toggle', {\n        originalEvent: evt\n      });\n    });\n\n    this.$selection.on(\n      'click',\n      '.select2-selection__choice__remove',\n      function (evt) {\n        // Ignore the event if it is disabled\n        if (self.isDisabled()) {\n          return;\n        }\n\n        var $remove = $(this);\n        var $selection = $remove.parent();\n\n        var data = Utils.GetData($selection[0], 'data');\n\n        self.trigger('unselect', {\n          originalEvent: evt,\n          data: data\n        });\n      }\n    );\n  };\n\n  MultipleSelection.prototype.clear = function () {\n    var $rendered = this.$selection.find('.select2-selection__rendered');\n    $rendered.empty();\n    $rendered.removeAttr('title');\n  };\n\n  MultipleSelection.prototype.display = function (data, container) {\n    var template = this.options.get('templateSelection');\n    var escapeMarkup = this.options.get('escapeMarkup');\n\n    return escapeMarkup(template(data, container));\n  };\n\n  MultipleSelection.prototype.selectionContainer = function () {\n    var $container = $(\n      '<li class=\"select2-selection__choice\">' +\n        '<span class=\"select2-selection__choice__remove\" role=\"presentation\">' +\n          '&times;' +\n        '</span>' +\n      '</li>'\n    );\n\n    return $container;\n  };\n\n  MultipleSelection.prototype.update = function (data) {\n    this.clear();\n\n    if (data.length === 0) {\n      return;\n    }\n\n    var $selections = [];\n\n    for (var d = 0; d < data.length; d++) {\n      var selection = data[d];\n\n      var $selection = this.selectionContainer();\n      var formatted = this.display(selection, $selection);\n\n      $selection.append(formatted);\n\n      var title = selection.title || selection.text;\n\n      if (title) {\n        $selection.attr('title', title);\n      }\n\n      Utils.StoreData($selection[0], 'data', selection);\n\n      $selections.push($selection);\n    }\n\n    var $rendered = this.$selection.find('.select2-selection__rendered');\n\n    Utils.appendMany($rendered, $selections);\n  };\n\n  return MultipleSelection;\n});\n\nS2.define('select2/selection/placeholder',[\n  '../utils'\n], function (Utils) {\n  function Placeholder (decorated, $element, options) {\n    this.placeholder = this.normalizePlaceholder(options.get('placeholder'));\n\n    decorated.call(this, $element, options);\n  }\n\n  Placeholder.prototype.normalizePlaceholder = function (_, placeholder) {\n    if (typeof placeholder === 'string') {\n      placeholder = {\n        id: '',\n        text: placeholder\n      };\n    }\n\n    return placeholder;\n  };\n\n  Placeholder.prototype.createPlaceholder = function (decorated, placeholder) {\n    var $placeholder = this.selectionContainer();\n\n    $placeholder.html(this.display(placeholder));\n    $placeholder.addClass('select2-selection__placeholder')\n                .removeClass('select2-selection__choice');\n\n    return $placeholder;\n  };\n\n  Placeholder.prototype.update = function (decorated, data) {\n    var singlePlaceholder = (\n      data.length == 1 && data[0].id != this.placeholder.id\n    );\n    var multipleSelections = data.length > 1;\n\n    if (multipleSelections || singlePlaceholder) {\n      return decorated.call(this, data);\n    }\n\n    this.clear();\n\n    var $placeholder = this.createPlaceholder(this.placeholder);\n\n    this.$selection.find('.select2-selection__rendered').append($placeholder);\n  };\n\n  return Placeholder;\n});\n\nS2.define('select2/selection/allowClear',[\n  'jquery',\n  '../keys',\n  '../utils'\n], function ($, KEYS, Utils) {\n  function AllowClear () { }\n\n  AllowClear.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n\n    decorated.call(this, container, $container);\n\n    if (this.placeholder == null) {\n      if (this.options.get('debug') && window.console && console.error) {\n        console.error(\n          'Select2: The `allowClear` option should be used in combination ' +\n          'with the `placeholder` option.'\n        );\n      }\n    }\n\n    this.$selection.on('mousedown', '.select2-selection__clear',\n      function (evt) {\n        self._handleClear(evt);\n    });\n\n    container.on('keypress', function (evt) {\n      self._handleKeyboardClear(evt, container);\n    });\n  };\n\n  AllowClear.prototype._handleClear = function (_, evt) {\n    // Ignore the event if it is disabled\n    if (this.isDisabled()) {\n      return;\n    }\n\n    var $clear = this.$selection.find('.select2-selection__clear');\n\n    // Ignore the event if nothing has been selected\n    if ($clear.length === 0) {\n      return;\n    }\n\n    evt.stopPropagation();\n\n    var data = Utils.GetData($clear[0], 'data');\n\n    var previousVal = this.$element.val();\n    this.$element.val(this.placeholder.id);\n\n    var unselectData = {\n      data: data\n    };\n    this.trigger('clear', unselectData);\n    if (unselectData.prevented) {\n      this.$element.val(previousVal);\n      return;\n    }\n\n    for (var d = 0; d < data.length; d++) {\n      unselectData = {\n        data: data[d]\n      };\n\n      // Trigger the `unselect` event, so people can prevent it from being\n      // cleared.\n      this.trigger('unselect', unselectData);\n\n      // If the event was prevented, don't clear it out.\n      if (unselectData.prevented) {\n        this.$element.val(previousVal);\n        return;\n      }\n    }\n\n    this.$element.trigger('input').trigger('change');\n\n    this.trigger('toggle', {});\n  };\n\n  AllowClear.prototype._handleKeyboardClear = function (_, evt, container) {\n    if (container.isOpen()) {\n      return;\n    }\n\n    if (evt.which == KEYS.DELETE || evt.which == KEYS.BACKSPACE) {\n      this._handleClear(evt);\n    }\n  };\n\n  AllowClear.prototype.update = function (decorated, data) {\n    decorated.call(this, data);\n\n    if (this.$selection.find('.select2-selection__placeholder').length > 0 ||\n        data.length === 0) {\n      return;\n    }\n\n    var removeAll = this.options.get('translations').get('removeAllItems');\n\n    var $remove = $(\n      '<span class=\"select2-selection__clear\" title=\"' + removeAll() +'\">' +\n        '&times;' +\n      '</span>'\n    );\n    Utils.StoreData($remove[0], 'data', data);\n\n    this.$selection.find('.select2-selection__rendered').prepend($remove);\n  };\n\n  return AllowClear;\n});\n\nS2.define('select2/selection/search',[\n  'jquery',\n  '../utils',\n  '../keys'\n], function ($, Utils, KEYS) {\n  function Search (decorated, $element, options) {\n    decorated.call(this, $element, options);\n  }\n\n  Search.prototype.render = function (decorated) {\n    var $search = $(\n      '<li class=\"select2-search select2-search--inline\">' +\n        '<input class=\"select2-search__field\" type=\"search\" tabindex=\"-1\"' +\n        ' autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"none\"' +\n        ' spellcheck=\"false\" role=\"searchbox\" aria-autocomplete=\"list\" />' +\n      '</li>'\n    );\n\n    this.$searchContainer = $search;\n    this.$search = $search.find('input');\n\n    var $rendered = decorated.call(this);\n\n    this._transferTabIndex();\n\n    return $rendered;\n  };\n\n  Search.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n\n    var resultsId = container.id + '-results';\n\n    decorated.call(this, container, $container);\n\n    container.on('open', function () {\n      self.$search.attr('aria-controls', resultsId);\n      self.$search.trigger('focus');\n    });\n\n    container.on('close', function () {\n      self.$search.val('');\n      self.$search.removeAttr('aria-controls');\n      self.$search.removeAttr('aria-activedescendant');\n      self.$search.trigger('focus');\n    });\n\n    container.on('enable', function () {\n      self.$search.prop('disabled', false);\n\n      self._transferTabIndex();\n    });\n\n    container.on('disable', function () {\n      self.$search.prop('disabled', true);\n    });\n\n    container.on('focus', function (evt) {\n      self.$search.trigger('focus');\n    });\n\n    container.on('results:focus', function (params) {\n      if (params.data._resultId) {\n        self.$search.attr('aria-activedescendant', params.data._resultId);\n      } else {\n        self.$search.removeAttr('aria-activedescendant');\n      }\n    });\n\n    this.$selection.on('focusin', '.select2-search--inline', function (evt) {\n      self.trigger('focus', evt);\n    });\n\n    this.$selection.on('focusout', '.select2-search--inline', function (evt) {\n      self._handleBlur(evt);\n    });\n\n    this.$selection.on('keydown', '.select2-search--inline', function (evt) {\n      evt.stopPropagation();\n\n      self.trigger('keypress', evt);\n\n      self._keyUpPrevented = evt.isDefaultPrevented();\n\n      var key = evt.which;\n\n      if (key === KEYS.BACKSPACE && self.$search.val() === '') {\n        var $previousChoice = self.$searchContainer\n          .prev('.select2-selection__choice');\n\n        if ($previousChoice.length > 0) {\n          var item = Utils.GetData($previousChoice[0], 'data');\n\n          self.searchRemoveChoice(item);\n\n          evt.preventDefault();\n        }\n      }\n    });\n\n    this.$selection.on('click', '.select2-search--inline', function (evt) {\n      if (self.$search.val()) {\n        evt.stopPropagation();\n      }\n    });\n\n    // Try to detect the IE version should the `documentMode` property that\n    // is stored on the document. This is only implemented in IE and is\n    // slightly cleaner than doing a user agent check.\n    // This property is not available in Edge, but Edge also doesn't have\n    // this bug.\n    var msie = document.documentMode;\n    var disableInputEvents = msie && msie <= 11;\n\n    // Workaround for browsers which do not support the `input` event\n    // This will prevent double-triggering of events for browsers which support\n    // both the `keyup` and `input` events.\n    this.$selection.on(\n      'input.searchcheck',\n      '.select2-search--inline',\n      function (evt) {\n        // IE will trigger the `input` event when a placeholder is used on a\n        // search box. To get around this issue, we are forced to ignore all\n        // `input` events in IE and keep using `keyup`.\n        if (disableInputEvents) {\n          self.$selection.off('input.search input.searchcheck');\n          return;\n        }\n\n        // Unbind the duplicated `keyup` event\n        self.$selection.off('keyup.search');\n      }\n    );\n\n    this.$selection.on(\n      'keyup.search input.search',\n      '.select2-search--inline',\n      function (evt) {\n        // IE will trigger the `input` event when a placeholder is used on a\n        // search box. To get around this issue, we are forced to ignore all\n        // `input` events in IE and keep using `keyup`.\n        if (disableInputEvents && evt.type === 'input') {\n          self.$selection.off('input.search input.searchcheck');\n          return;\n        }\n\n        var key = evt.which;\n\n        // We can freely ignore events from modifier keys\n        if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) {\n          return;\n        }\n\n        // Tabbing will be handled during the `keydown` phase\n        if (key == KEYS.TAB) {\n          return;\n        }\n\n        self.handleSearch(evt);\n      }\n    );\n  };\n\n  /**\n   * This method will transfer the tabindex attribute from the rendered\n   * selection to the search box. This allows for the search box to be used as\n   * the primary focus instead of the selection container.\n   *\n   * @private\n   */\n  Search.prototype._transferTabIndex = function (decorated) {\n    this.$search.attr('tabindex', this.$selection.attr('tabindex'));\n    this.$selection.attr('tabindex', '-1');\n  };\n\n  Search.prototype.createPlaceholder = function (decorated, placeholder) {\n    this.$search.attr('placeholder', placeholder.text);\n  };\n\n  Search.prototype.update = function (decorated, data) {\n    var searchHadFocus = this.$search[0] == document.activeElement;\n\n    this.$search.attr('placeholder', '');\n\n    decorated.call(this, data);\n\n    this.$selection.find('.select2-selection__rendered')\n                   .append(this.$searchContainer);\n\n    this.resizeSearch();\n    if (searchHadFocus) {\n      this.$search.trigger('focus');\n    }\n  };\n\n  Search.prototype.handleSearch = function () {\n    this.resizeSearch();\n\n    if (!this._keyUpPrevented) {\n      var input = this.$search.val();\n\n      this.trigger('query', {\n        term: input\n      });\n    }\n\n    this._keyUpPrevented = false;\n  };\n\n  Search.prototype.searchRemoveChoice = function (decorated, item) {\n    this.trigger('unselect', {\n      data: item\n    });\n\n    this.$search.val(item.text);\n    this.handleSearch();\n  };\n\n  Search.prototype.resizeSearch = function () {\n    this.$search.css('width', '25px');\n\n    var width = '';\n\n    if (this.$search.attr('placeholder') !== '') {\n      width = this.$selection.find('.select2-selection__rendered').width();\n    } else {\n      var minimumWidth = this.$search.val().length + 1;\n\n      width = (minimumWidth * 0.75) + 'em';\n    }\n\n    this.$search.css('width', width);\n  };\n\n  return Search;\n});\n\nS2.define('select2/selection/eventRelay',[\n  'jquery'\n], function ($) {\n  function EventRelay () { }\n\n  EventRelay.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n    var relayEvents = [\n      'open', 'opening',\n      'close', 'closing',\n      'select', 'selecting',\n      'unselect', 'unselecting',\n      'clear', 'clearing'\n    ];\n\n    var preventableEvents = [\n      'opening', 'closing', 'selecting', 'unselecting', 'clearing'\n    ];\n\n    decorated.call(this, container, $container);\n\n    container.on('*', function (name, params) {\n      // Ignore events that should not be relayed\n      if ($.inArray(name, relayEvents) === -1) {\n        return;\n      }\n\n      // The parameters should always be an object\n      params = params || {};\n\n      // Generate the jQuery event for the Select2 event\n      var evt = $.Event('select2:' + name, {\n        params: params\n      });\n\n      self.$element.trigger(evt);\n\n      // Only handle preventable events if it was one\n      if ($.inArray(name, preventableEvents) === -1) {\n        return;\n      }\n\n      params.prevented = evt.isDefaultPrevented();\n    });\n  };\n\n  return EventRelay;\n});\n\nS2.define('select2/translation',[\n  'jquery',\n  'require'\n], function ($, require) {\n  function Translation (dict) {\n    this.dict = dict || {};\n  }\n\n  Translation.prototype.all = function () {\n    return this.dict;\n  };\n\n  Translation.prototype.get = function (key) {\n    return this.dict[key];\n  };\n\n  Translation.prototype.extend = function (translation) {\n    this.dict = $.extend({}, translation.all(), this.dict);\n  };\n\n  // Static functions\n\n  Translation._cache = {};\n\n  Translation.loadPath = function (path) {\n    if (!(path in Translation._cache)) {\n      var translations = require(path);\n\n      Translation._cache[path] = translations;\n    }\n\n    return new Translation(Translation._cache[path]);\n  };\n\n  return Translation;\n});\n\nS2.define('select2/diacritics',[\n\n], function () {\n  var diacritics = {\n    '\\u24B6': 'A',\n    '\\uFF21': 'A',\n    '\\u00C0': 'A',\n    '\\u00C1': 'A',\n    '\\u00C2': 'A',\n    '\\u1EA6': 'A',\n    '\\u1EA4': 'A',\n    '\\u1EAA': 'A',\n    '\\u1EA8': 'A',\n    '\\u00C3': 'A',\n    '\\u0100': 'A',\n    '\\u0102': 'A',\n    '\\u1EB0': 'A',\n    '\\u1EAE': 'A',\n    '\\u1EB4': 'A',\n    '\\u1EB2': 'A',\n    '\\u0226': 'A',\n    '\\u01E0': 'A',\n    '\\u00C4': 'A',\n    '\\u01DE': 'A',\n    '\\u1EA2': 'A',\n    '\\u00C5': 'A',\n    '\\u01FA': 'A',\n    '\\u01CD': 'A',\n    '\\u0200': 'A',\n    '\\u0202': 'A',\n    '\\u1EA0': 'A',\n    '\\u1EAC': 'A',\n    '\\u1EB6': 'A',\n    '\\u1E00': 'A',\n    '\\u0104': 'A',\n    '\\u023A': 'A',\n    '\\u2C6F': 'A',\n    '\\uA732': 'AA',\n    '\\u00C6': 'AE',\n    '\\u01FC': 'AE',\n    '\\u01E2': 'AE',\n    '\\uA734': 'AO',\n    '\\uA736': 'AU',\n    '\\uA738': 'AV',\n    '\\uA73A': 'AV',\n    '\\uA73C': 'AY',\n    '\\u24B7': 'B',\n    '\\uFF22': 'B',\n    '\\u1E02': 'B',\n    '\\u1E04': 'B',\n    '\\u1E06': 'B',\n    '\\u0243': 'B',\n    '\\u0182': 'B',\n    '\\u0181': 'B',\n    '\\u24B8': 'C',\n    '\\uFF23': 'C',\n    '\\u0106': 'C',\n    '\\u0108': 'C',\n    '\\u010A': 'C',\n    '\\u010C': 'C',\n    '\\u00C7': 'C',\n    '\\u1E08': 'C',\n    '\\u0187': 'C',\n    '\\u023B': 'C',\n    '\\uA73E': 'C',\n    '\\u24B9': 'D',\n    '\\uFF24': 'D',\n    '\\u1E0A': 'D',\n    '\\u010E': 'D',\n    '\\u1E0C': 'D',\n    '\\u1E10': 'D',\n    '\\u1E12': 'D',\n    '\\u1E0E': 'D',\n    '\\u0110': 'D',\n    '\\u018B': 'D',\n    '\\u018A': 'D',\n    '\\u0189': 'D',\n    '\\uA779': 'D',\n    '\\u01F1': 'DZ',\n    '\\u01C4': 'DZ',\n    '\\u01F2': 'Dz',\n    '\\u01C5': 'Dz',\n    '\\u24BA': 'E',\n    '\\uFF25': 'E',\n    '\\u00C8': 'E',\n    '\\u00C9': 'E',\n    '\\u00CA': 'E',\n    '\\u1EC0': 'E',\n    '\\u1EBE': 'E',\n    '\\u1EC4': 'E',\n    '\\u1EC2': 'E',\n    '\\u1EBC': 'E',\n    '\\u0112': 'E',\n    '\\u1E14': 'E',\n    '\\u1E16': 'E',\n    '\\u0114': 'E',\n    '\\u0116': 'E',\n    '\\u00CB': 'E',\n    '\\u1EBA': 'E',\n    '\\u011A': 'E',\n    '\\u0204': 'E',\n    '\\u0206': 'E',\n    '\\u1EB8': 'E',\n    '\\u1EC6': 'E',\n    '\\u0228': 'E',\n    '\\u1E1C': 'E',\n    '\\u0118': 'E',\n    '\\u1E18': 'E',\n    '\\u1E1A': 'E',\n    '\\u0190': 'E',\n    '\\u018E': 'E',\n    '\\u24BB': 'F',\n    '\\uFF26': 'F',\n    '\\u1E1E': 'F',\n    '\\u0191': 'F',\n    '\\uA77B': 'F',\n    '\\u24BC': 'G',\n    '\\uFF27': 'G',\n    '\\u01F4': 'G',\n    '\\u011C': 'G',\n    '\\u1E20': 'G',\n    '\\u011E': 'G',\n    '\\u0120': 'G',\n    '\\u01E6': 'G',\n    '\\u0122': 'G',\n    '\\u01E4': 'G',\n    '\\u0193': 'G',\n    '\\uA7A0': 'G',\n    '\\uA77D': 'G',\n    '\\uA77E': 'G',\n    '\\u24BD': 'H',\n    '\\uFF28': 'H',\n    '\\u0124': 'H',\n    '\\u1E22': 'H',\n    '\\u1E26': 'H',\n    '\\u021E': 'H',\n    '\\u1E24': 'H',\n    '\\u1E28': 'H',\n    '\\u1E2A': 'H',\n    '\\u0126': 'H',\n    '\\u2C67': 'H',\n    '\\u2C75': 'H',\n    '\\uA78D': 'H',\n    '\\u24BE': 'I',\n    '\\uFF29': 'I',\n    '\\u00CC': 'I',\n    '\\u00CD': 'I',\n    '\\u00CE': 'I',\n    '\\u0128': 'I',\n    '\\u012A': 'I',\n    '\\u012C': 'I',\n    '\\u0130': 'I',\n    '\\u00CF': 'I',\n    '\\u1E2E': 'I',\n    '\\u1EC8': 'I',\n    '\\u01CF': 'I',\n    '\\u0208': 'I',\n    '\\u020A': 'I',\n    '\\u1ECA': 'I',\n    '\\u012E': 'I',\n    '\\u1E2C': 'I',\n    '\\u0197': 'I',\n    '\\u24BF': 'J',\n    '\\uFF2A': 'J',\n    '\\u0134': 'J',\n    '\\u0248': 'J',\n    '\\u24C0': 'K',\n    '\\uFF2B': 'K',\n    '\\u1E30': 'K',\n    '\\u01E8': 'K',\n    '\\u1E32': 'K',\n    '\\u0136': 'K',\n    '\\u1E34': 'K',\n    '\\u0198': 'K',\n    '\\u2C69': 'K',\n    '\\uA740': 'K',\n    '\\uA742': 'K',\n    '\\uA744': 'K',\n    '\\uA7A2': 'K',\n    '\\u24C1': 'L',\n    '\\uFF2C': 'L',\n    '\\u013F': 'L',\n    '\\u0139': 'L',\n    '\\u013D': 'L',\n    '\\u1E36': 'L',\n    '\\u1E38': 'L',\n    '\\u013B': 'L',\n    '\\u1E3C': 'L',\n    '\\u1E3A': 'L',\n    '\\u0141': 'L',\n    '\\u023D': 'L',\n    '\\u2C62': 'L',\n    '\\u2C60': 'L',\n    '\\uA748': 'L',\n    '\\uA746': 'L',\n    '\\uA780': 'L',\n    '\\u01C7': 'LJ',\n    '\\u01C8': 'Lj',\n    '\\u24C2': 'M',\n    '\\uFF2D': 'M',\n    '\\u1E3E': 'M',\n    '\\u1E40': 'M',\n    '\\u1E42': 'M',\n    '\\u2C6E': 'M',\n    '\\u019C': 'M',\n    '\\u24C3': 'N',\n    '\\uFF2E': 'N',\n    '\\u01F8': 'N',\n    '\\u0143': 'N',\n    '\\u00D1': 'N',\n    '\\u1E44': 'N',\n    '\\u0147': 'N',\n    '\\u1E46': 'N',\n    '\\u0145': 'N',\n    '\\u1E4A': 'N',\n    '\\u1E48': 'N',\n    '\\u0220': 'N',\n    '\\u019D': 'N',\n    '\\uA790': 'N',\n    '\\uA7A4': 'N',\n    '\\u01CA': 'NJ',\n    '\\u01CB': 'Nj',\n    '\\u24C4': 'O',\n    '\\uFF2F': 'O',\n    '\\u00D2': 'O',\n    '\\u00D3': 'O',\n    '\\u00D4': 'O',\n    '\\u1ED2': 'O',\n    '\\u1ED0': 'O',\n    '\\u1ED6': 'O',\n    '\\u1ED4': 'O',\n    '\\u00D5': 'O',\n    '\\u1E4C': 'O',\n    '\\u022C': 'O',\n    '\\u1E4E': 'O',\n    '\\u014C': 'O',\n    '\\u1E50': 'O',\n    '\\u1E52': 'O',\n    '\\u014E': 'O',\n    '\\u022E': 'O',\n    '\\u0230': 'O',\n    '\\u00D6': 'O',\n    '\\u022A': 'O',\n    '\\u1ECE': 'O',\n    '\\u0150': 'O',\n    '\\u01D1': 'O',\n    '\\u020C': 'O',\n    '\\u020E': 'O',\n    '\\u01A0': 'O',\n    '\\u1EDC': 'O',\n    '\\u1EDA': 'O',\n    '\\u1EE0': 'O',\n    '\\u1EDE': 'O',\n    '\\u1EE2': 'O',\n    '\\u1ECC': 'O',\n    '\\u1ED8': 'O',\n    '\\u01EA': 'O',\n    '\\u01EC': 'O',\n    '\\u00D8': 'O',\n    '\\u01FE': 'O',\n    '\\u0186': 'O',\n    '\\u019F': 'O',\n    '\\uA74A': 'O',\n    '\\uA74C': 'O',\n    '\\u0152': 'OE',\n    '\\u01A2': 'OI',\n    '\\uA74E': 'OO',\n    '\\u0222': 'OU',\n    '\\u24C5': 'P',\n    '\\uFF30': 'P',\n    '\\u1E54': 'P',\n    '\\u1E56': 'P',\n    '\\u01A4': 'P',\n    '\\u2C63': 'P',\n    '\\uA750': 'P',\n    '\\uA752': 'P',\n    '\\uA754': 'P',\n    '\\u24C6': 'Q',\n    '\\uFF31': 'Q',\n    '\\uA756': 'Q',\n    '\\uA758': 'Q',\n    '\\u024A': 'Q',\n    '\\u24C7': 'R',\n    '\\uFF32': 'R',\n    '\\u0154': 'R',\n    '\\u1E58': 'R',\n    '\\u0158': 'R',\n    '\\u0210': 'R',\n    '\\u0212': 'R',\n    '\\u1E5A': 'R',\n    '\\u1E5C': 'R',\n    '\\u0156': 'R',\n    '\\u1E5E': 'R',\n    '\\u024C': 'R',\n    '\\u2C64': 'R',\n    '\\uA75A': 'R',\n    '\\uA7A6': 'R',\n    '\\uA782': 'R',\n    '\\u24C8': 'S',\n    '\\uFF33': 'S',\n    '\\u1E9E': 'S',\n    '\\u015A': 'S',\n    '\\u1E64': 'S',\n    '\\u015C': 'S',\n    '\\u1E60': 'S',\n    '\\u0160': 'S',\n    '\\u1E66': 'S',\n    '\\u1E62': 'S',\n    '\\u1E68': 'S',\n    '\\u0218': 'S',\n    '\\u015E': 'S',\n    '\\u2C7E': 'S',\n    '\\uA7A8': 'S',\n    '\\uA784': 'S',\n    '\\u24C9': 'T',\n    '\\uFF34': 'T',\n    '\\u1E6A': 'T',\n    '\\u0164': 'T',\n    '\\u1E6C': 'T',\n    '\\u021A': 'T',\n    '\\u0162': 'T',\n    '\\u1E70': 'T',\n    '\\u1E6E': 'T',\n    '\\u0166': 'T',\n    '\\u01AC': 'T',\n    '\\u01AE': 'T',\n    '\\u023E': 'T',\n    '\\uA786': 'T',\n    '\\uA728': 'TZ',\n    '\\u24CA': 'U',\n    '\\uFF35': 'U',\n    '\\u00D9': 'U',\n    '\\u00DA': 'U',\n    '\\u00DB': 'U',\n    '\\u0168': 'U',\n    '\\u1E78': 'U',\n    '\\u016A': 'U',\n    '\\u1E7A': 'U',\n    '\\u016C': 'U',\n    '\\u00DC': 'U',\n    '\\u01DB': 'U',\n    '\\u01D7': 'U',\n    '\\u01D5': 'U',\n    '\\u01D9': 'U',\n    '\\u1EE6': 'U',\n    '\\u016E': 'U',\n    '\\u0170': 'U',\n    '\\u01D3': 'U',\n    '\\u0214': 'U',\n    '\\u0216': 'U',\n    '\\u01AF': 'U',\n    '\\u1EEA': 'U',\n    '\\u1EE8': 'U',\n    '\\u1EEE': 'U',\n    '\\u1EEC': 'U',\n    '\\u1EF0': 'U',\n    '\\u1EE4': 'U',\n    '\\u1E72': 'U',\n    '\\u0172': 'U',\n    '\\u1E76': 'U',\n    '\\u1E74': 'U',\n    '\\u0244': 'U',\n    '\\u24CB': 'V',\n    '\\uFF36': 'V',\n    '\\u1E7C': 'V',\n    '\\u1E7E': 'V',\n    '\\u01B2': 'V',\n    '\\uA75E': 'V',\n    '\\u0245': 'V',\n    '\\uA760': 'VY',\n    '\\u24CC': 'W',\n    '\\uFF37': 'W',\n    '\\u1E80': 'W',\n    '\\u1E82': 'W',\n    '\\u0174': 'W',\n    '\\u1E86': 'W',\n    '\\u1E84': 'W',\n    '\\u1E88': 'W',\n    '\\u2C72': 'W',\n    '\\u24CD': 'X',\n    '\\uFF38': 'X',\n    '\\u1E8A': 'X',\n    '\\u1E8C': 'X',\n    '\\u24CE': 'Y',\n    '\\uFF39': 'Y',\n    '\\u1EF2': 'Y',\n    '\\u00DD': 'Y',\n    '\\u0176': 'Y',\n    '\\u1EF8': 'Y',\n    '\\u0232': 'Y',\n    '\\u1E8E': 'Y',\n    '\\u0178': 'Y',\n    '\\u1EF6': 'Y',\n    '\\u1EF4': 'Y',\n    '\\u01B3': 'Y',\n    '\\u024E': 'Y',\n    '\\u1EFE': 'Y',\n    '\\u24CF': 'Z',\n    '\\uFF3A': 'Z',\n    '\\u0179': 'Z',\n    '\\u1E90': 'Z',\n    '\\u017B': 'Z',\n    '\\u017D': 'Z',\n    '\\u1E92': 'Z',\n    '\\u1E94': 'Z',\n    '\\u01B5': 'Z',\n    '\\u0224': 'Z',\n    '\\u2C7F': 'Z',\n    '\\u2C6B': 'Z',\n    '\\uA762': 'Z',\n    '\\u24D0': 'a',\n    '\\uFF41': 'a',\n    '\\u1E9A': 'a',\n    '\\u00E0': 'a',\n    '\\u00E1': 'a',\n    '\\u00E2': 'a',\n    '\\u1EA7': 'a',\n    '\\u1EA5': 'a',\n    '\\u1EAB': 'a',\n    '\\u1EA9': 'a',\n    '\\u00E3': 'a',\n    '\\u0101': 'a',\n    '\\u0103': 'a',\n    '\\u1EB1': 'a',\n    '\\u1EAF': 'a',\n    '\\u1EB5': 'a',\n    '\\u1EB3': 'a',\n    '\\u0227': 'a',\n    '\\u01E1': 'a',\n    '\\u00E4': 'a',\n    '\\u01DF': 'a',\n    '\\u1EA3': 'a',\n    '\\u00E5': 'a',\n    '\\u01FB': 'a',\n    '\\u01CE': 'a',\n    '\\u0201': 'a',\n    '\\u0203': 'a',\n    '\\u1EA1': 'a',\n    '\\u1EAD': 'a',\n    '\\u1EB7': 'a',\n    '\\u1E01': 'a',\n    '\\u0105': 'a',\n    '\\u2C65': 'a',\n    '\\u0250': 'a',\n    '\\uA733': 'aa',\n    '\\u00E6': 'ae',\n    '\\u01FD': 'ae',\n    '\\u01E3': 'ae',\n    '\\uA735': 'ao',\n    '\\uA737': 'au',\n    '\\uA739': 'av',\n    '\\uA73B': 'av',\n    '\\uA73D': 'ay',\n    '\\u24D1': 'b',\n    '\\uFF42': 'b',\n    '\\u1E03': 'b',\n    '\\u1E05': 'b',\n    '\\u1E07': 'b',\n    '\\u0180': 'b',\n    '\\u0183': 'b',\n    '\\u0253': 'b',\n    '\\u24D2': 'c',\n    '\\uFF43': 'c',\n    '\\u0107': 'c',\n    '\\u0109': 'c',\n    '\\u010B': 'c',\n    '\\u010D': 'c',\n    '\\u00E7': 'c',\n    '\\u1E09': 'c',\n    '\\u0188': 'c',\n    '\\u023C': 'c',\n    '\\uA73F': 'c',\n    '\\u2184': 'c',\n    '\\u24D3': 'd',\n    '\\uFF44': 'd',\n    '\\u1E0B': 'd',\n    '\\u010F': 'd',\n    '\\u1E0D': 'd',\n    '\\u1E11': 'd',\n    '\\u1E13': 'd',\n    '\\u1E0F': 'd',\n    '\\u0111': 'd',\n    '\\u018C': 'd',\n    '\\u0256': 'd',\n    '\\u0257': 'd',\n    '\\uA77A': 'd',\n    '\\u01F3': 'dz',\n    '\\u01C6': 'dz',\n    '\\u24D4': 'e',\n    '\\uFF45': 'e',\n    '\\u00E8': 'e',\n    '\\u00E9': 'e',\n    '\\u00EA': 'e',\n    '\\u1EC1': 'e',\n    '\\u1EBF': 'e',\n    '\\u1EC5': 'e',\n    '\\u1EC3': 'e',\n    '\\u1EBD': 'e',\n    '\\u0113': 'e',\n    '\\u1E15': 'e',\n    '\\u1E17': 'e',\n    '\\u0115': 'e',\n    '\\u0117': 'e',\n    '\\u00EB': 'e',\n    '\\u1EBB': 'e',\n    '\\u011B': 'e',\n    '\\u0205': 'e',\n    '\\u0207': 'e',\n    '\\u1EB9': 'e',\n    '\\u1EC7': 'e',\n    '\\u0229': 'e',\n    '\\u1E1D': 'e',\n    '\\u0119': 'e',\n    '\\u1E19': 'e',\n    '\\u1E1B': 'e',\n    '\\u0247': 'e',\n    '\\u025B': 'e',\n    '\\u01DD': 'e',\n    '\\u24D5': 'f',\n    '\\uFF46': 'f',\n    '\\u1E1F': 'f',\n    '\\u0192': 'f',\n    '\\uA77C': 'f',\n    '\\u24D6': 'g',\n    '\\uFF47': 'g',\n    '\\u01F5': 'g',\n    '\\u011D': 'g',\n    '\\u1E21': 'g',\n    '\\u011F': 'g',\n    '\\u0121': 'g',\n    '\\u01E7': 'g',\n    '\\u0123': 'g',\n    '\\u01E5': 'g',\n    '\\u0260': 'g',\n    '\\uA7A1': 'g',\n    '\\u1D79': 'g',\n    '\\uA77F': 'g',\n    '\\u24D7': 'h',\n    '\\uFF48': 'h',\n    '\\u0125': 'h',\n    '\\u1E23': 'h',\n    '\\u1E27': 'h',\n    '\\u021F': 'h',\n    '\\u1E25': 'h',\n    '\\u1E29': 'h',\n    '\\u1E2B': 'h',\n    '\\u1E96': 'h',\n    '\\u0127': 'h',\n    '\\u2C68': 'h',\n    '\\u2C76': 'h',\n    '\\u0265': 'h',\n    '\\u0195': 'hv',\n    '\\u24D8': 'i',\n    '\\uFF49': 'i',\n    '\\u00EC': 'i',\n    '\\u00ED': 'i',\n    '\\u00EE': 'i',\n    '\\u0129': 'i',\n    '\\u012B': 'i',\n    '\\u012D': 'i',\n    '\\u00EF': 'i',\n    '\\u1E2F': 'i',\n    '\\u1EC9': 'i',\n    '\\u01D0': 'i',\n    '\\u0209': 'i',\n    '\\u020B': 'i',\n    '\\u1ECB': 'i',\n    '\\u012F': 'i',\n    '\\u1E2D': 'i',\n    '\\u0268': 'i',\n    '\\u0131': 'i',\n    '\\u24D9': 'j',\n    '\\uFF4A': 'j',\n    '\\u0135': 'j',\n    '\\u01F0': 'j',\n    '\\u0249': 'j',\n    '\\u24DA': 'k',\n    '\\uFF4B': 'k',\n    '\\u1E31': 'k',\n    '\\u01E9': 'k',\n    '\\u1E33': 'k',\n    '\\u0137': 'k',\n    '\\u1E35': 'k',\n    '\\u0199': 'k',\n    '\\u2C6A': 'k',\n    '\\uA741': 'k',\n    '\\uA743': 'k',\n    '\\uA745': 'k',\n    '\\uA7A3': 'k',\n    '\\u24DB': 'l',\n    '\\uFF4C': 'l',\n    '\\u0140': 'l',\n    '\\u013A': 'l',\n    '\\u013E': 'l',\n    '\\u1E37': 'l',\n    '\\u1E39': 'l',\n    '\\u013C': 'l',\n    '\\u1E3D': 'l',\n    '\\u1E3B': 'l',\n    '\\u017F': 'l',\n    '\\u0142': 'l',\n    '\\u019A': 'l',\n    '\\u026B': 'l',\n    '\\u2C61': 'l',\n    '\\uA749': 'l',\n    '\\uA781': 'l',\n    '\\uA747': 'l',\n    '\\u01C9': 'lj',\n    '\\u24DC': 'm',\n    '\\uFF4D': 'm',\n    '\\u1E3F': 'm',\n    '\\u1E41': 'm',\n    '\\u1E43': 'm',\n    '\\u0271': 'm',\n    '\\u026F': 'm',\n    '\\u24DD': 'n',\n    '\\uFF4E': 'n',\n    '\\u01F9': 'n',\n    '\\u0144': 'n',\n    '\\u00F1': 'n',\n    '\\u1E45': 'n',\n    '\\u0148': 'n',\n    '\\u1E47': 'n',\n    '\\u0146': 'n',\n    '\\u1E4B': 'n',\n    '\\u1E49': 'n',\n    '\\u019E': 'n',\n    '\\u0272': 'n',\n    '\\u0149': 'n',\n    '\\uA791': 'n',\n    '\\uA7A5': 'n',\n    '\\u01CC': 'nj',\n    '\\u24DE': 'o',\n    '\\uFF4F': 'o',\n    '\\u00F2': 'o',\n    '\\u00F3': 'o',\n    '\\u00F4': 'o',\n    '\\u1ED3': 'o',\n    '\\u1ED1': 'o',\n    '\\u1ED7': 'o',\n    '\\u1ED5': 'o',\n    '\\u00F5': 'o',\n    '\\u1E4D': 'o',\n    '\\u022D': 'o',\n    '\\u1E4F': 'o',\n    '\\u014D': 'o',\n    '\\u1E51': 'o',\n    '\\u1E53': 'o',\n    '\\u014F': 'o',\n    '\\u022F': 'o',\n    '\\u0231': 'o',\n    '\\u00F6': 'o',\n    '\\u022B': 'o',\n    '\\u1ECF': 'o',\n    '\\u0151': 'o',\n    '\\u01D2': 'o',\n    '\\u020D': 'o',\n    '\\u020F': 'o',\n    '\\u01A1': 'o',\n    '\\u1EDD': 'o',\n    '\\u1EDB': 'o',\n    '\\u1EE1': 'o',\n    '\\u1EDF': 'o',\n    '\\u1EE3': 'o',\n    '\\u1ECD': 'o',\n    '\\u1ED9': 'o',\n    '\\u01EB': 'o',\n    '\\u01ED': 'o',\n    '\\u00F8': 'o',\n    '\\u01FF': 'o',\n    '\\u0254': 'o',\n    '\\uA74B': 'o',\n    '\\uA74D': 'o',\n    '\\u0275': 'o',\n    '\\u0153': 'oe',\n    '\\u01A3': 'oi',\n    '\\u0223': 'ou',\n    '\\uA74F': 'oo',\n    '\\u24DF': 'p',\n    '\\uFF50': 'p',\n    '\\u1E55': 'p',\n    '\\u1E57': 'p',\n    '\\u01A5': 'p',\n    '\\u1D7D': 'p',\n    '\\uA751': 'p',\n    '\\uA753': 'p',\n    '\\uA755': 'p',\n    '\\u24E0': 'q',\n    '\\uFF51': 'q',\n    '\\u024B': 'q',\n    '\\uA757': 'q',\n    '\\uA759': 'q',\n    '\\u24E1': 'r',\n    '\\uFF52': 'r',\n    '\\u0155': 'r',\n    '\\u1E59': 'r',\n    '\\u0159': 'r',\n    '\\u0211': 'r',\n    '\\u0213': 'r',\n    '\\u1E5B': 'r',\n    '\\u1E5D': 'r',\n    '\\u0157': 'r',\n    '\\u1E5F': 'r',\n    '\\u024D': 'r',\n    '\\u027D': 'r',\n    '\\uA75B': 'r',\n    '\\uA7A7': 'r',\n    '\\uA783': 'r',\n    '\\u24E2': 's',\n    '\\uFF53': 's',\n    '\\u00DF': 's',\n    '\\u015B': 's',\n    '\\u1E65': 's',\n    '\\u015D': 's',\n    '\\u1E61': 's',\n    '\\u0161': 's',\n    '\\u1E67': 's',\n    '\\u1E63': 's',\n    '\\u1E69': 's',\n    '\\u0219': 's',\n    '\\u015F': 's',\n    '\\u023F': 's',\n    '\\uA7A9': 's',\n    '\\uA785': 's',\n    '\\u1E9B': 's',\n    '\\u24E3': 't',\n    '\\uFF54': 't',\n    '\\u1E6B': 't',\n    '\\u1E97': 't',\n    '\\u0165': 't',\n    '\\u1E6D': 't',\n    '\\u021B': 't',\n    '\\u0163': 't',\n    '\\u1E71': 't',\n    '\\u1E6F': 't',\n    '\\u0167': 't',\n    '\\u01AD': 't',\n    '\\u0288': 't',\n    '\\u2C66': 't',\n    '\\uA787': 't',\n    '\\uA729': 'tz',\n    '\\u24E4': 'u',\n    '\\uFF55': 'u',\n    '\\u00F9': 'u',\n    '\\u00FA': 'u',\n    '\\u00FB': 'u',\n    '\\u0169': 'u',\n    '\\u1E79': 'u',\n    '\\u016B': 'u',\n    '\\u1E7B': 'u',\n    '\\u016D': 'u',\n    '\\u00FC': 'u',\n    '\\u01DC': 'u',\n    '\\u01D8': 'u',\n    '\\u01D6': 'u',\n    '\\u01DA': 'u',\n    '\\u1EE7': 'u',\n    '\\u016F': 'u',\n    '\\u0171': 'u',\n    '\\u01D4': 'u',\n    '\\u0215': 'u',\n    '\\u0217': 'u',\n    '\\u01B0': 'u',\n    '\\u1EEB': 'u',\n    '\\u1EE9': 'u',\n    '\\u1EEF': 'u',\n    '\\u1EED': 'u',\n    '\\u1EF1': 'u',\n    '\\u1EE5': 'u',\n    '\\u1E73': 'u',\n    '\\u0173': 'u',\n    '\\u1E77': 'u',\n    '\\u1E75': 'u',\n    '\\u0289': 'u',\n    '\\u24E5': 'v',\n    '\\uFF56': 'v',\n    '\\u1E7D': 'v',\n    '\\u1E7F': 'v',\n    '\\u028B': 'v',\n    '\\uA75F': 'v',\n    '\\u028C': 'v',\n    '\\uA761': 'vy',\n    '\\u24E6': 'w',\n    '\\uFF57': 'w',\n    '\\u1E81': 'w',\n    '\\u1E83': 'w',\n    '\\u0175': 'w',\n    '\\u1E87': 'w',\n    '\\u1E85': 'w',\n    '\\u1E98': 'w',\n    '\\u1E89': 'w',\n    '\\u2C73': 'w',\n    '\\u24E7': 'x',\n    '\\uFF58': 'x',\n    '\\u1E8B': 'x',\n    '\\u1E8D': 'x',\n    '\\u24E8': 'y',\n    '\\uFF59': 'y',\n    '\\u1EF3': 'y',\n    '\\u00FD': 'y',\n    '\\u0177': 'y',\n    '\\u1EF9': 'y',\n    '\\u0233': 'y',\n    '\\u1E8F': 'y',\n    '\\u00FF': 'y',\n    '\\u1EF7': 'y',\n    '\\u1E99': 'y',\n    '\\u1EF5': 'y',\n    '\\u01B4': 'y',\n    '\\u024F': 'y',\n    '\\u1EFF': 'y',\n    '\\u24E9': 'z',\n    '\\uFF5A': 'z',\n    '\\u017A': 'z',\n    '\\u1E91': 'z',\n    '\\u017C': 'z',\n    '\\u017E': 'z',\n    '\\u1E93': 'z',\n    '\\u1E95': 'z',\n    '\\u01B6': 'z',\n    '\\u0225': 'z',\n    '\\u0240': 'z',\n    '\\u2C6C': 'z',\n    '\\uA763': 'z',\n    '\\u0386': '\\u0391',\n    '\\u0388': '\\u0395',\n    '\\u0389': '\\u0397',\n    '\\u038A': '\\u0399',\n    '\\u03AA': '\\u0399',\n    '\\u038C': '\\u039F',\n    '\\u038E': '\\u03A5',\n    '\\u03AB': '\\u03A5',\n    '\\u038F': '\\u03A9',\n    '\\u03AC': '\\u03B1',\n    '\\u03AD': '\\u03B5',\n    '\\u03AE': '\\u03B7',\n    '\\u03AF': '\\u03B9',\n    '\\u03CA': '\\u03B9',\n    '\\u0390': '\\u03B9',\n    '\\u03CC': '\\u03BF',\n    '\\u03CD': '\\u03C5',\n    '\\u03CB': '\\u03C5',\n    '\\u03B0': '\\u03C5',\n    '\\u03CE': '\\u03C9',\n    '\\u03C2': '\\u03C3',\n    '\\u2019': '\\''\n  };\n\n  return diacritics;\n});\n\nS2.define('select2/data/base',[\n  '../utils'\n], function (Utils) {\n  function BaseAdapter ($element, options) {\n    BaseAdapter.__super__.constructor.call(this);\n  }\n\n  Utils.Extend(BaseAdapter, Utils.Observable);\n\n  BaseAdapter.prototype.current = function (callback) {\n    throw new Error('The `current` method must be defined in child classes.');\n  };\n\n  BaseAdapter.prototype.query = function (params, callback) {\n    throw new Error('The `query` method must be defined in child classes.');\n  };\n\n  BaseAdapter.prototype.bind = function (container, $container) {\n    // Can be implemented in subclasses\n  };\n\n  BaseAdapter.prototype.destroy = function () {\n    // Can be implemented in subclasses\n  };\n\n  BaseAdapter.prototype.generateResultId = function (container, data) {\n    var id = container.id + '-result-';\n\n    id += Utils.generateChars(4);\n\n    if (data.id != null) {\n      id += '-' + data.id.toString();\n    } else {\n      id += '-' + Utils.generateChars(4);\n    }\n    return id;\n  };\n\n  return BaseAdapter;\n});\n\nS2.define('select2/data/select',[\n  './base',\n  '../utils',\n  'jquery'\n], function (BaseAdapter, Utils, $) {\n  function SelectAdapter ($element, options) {\n    this.$element = $element;\n    this.options = options;\n\n    SelectAdapter.__super__.constructor.call(this);\n  }\n\n  Utils.Extend(SelectAdapter, BaseAdapter);\n\n  SelectAdapter.prototype.current = function (callback) {\n    var data = [];\n    var self = this;\n\n    this.$element.find(':selected').each(function () {\n      var $option = $(this);\n\n      var option = self.item($option);\n\n      data.push(option);\n    });\n\n    callback(data);\n  };\n\n  SelectAdapter.prototype.select = function (data) {\n    var self = this;\n\n    data.selected = true;\n\n    // If data.element is a DOM node, use it instead\n    if ($(data.element).is('option')) {\n      data.element.selected = true;\n\n      this.$element.trigger('input').trigger('change');\n\n      return;\n    }\n\n    if (this.$element.prop('multiple')) {\n      this.current(function (currentData) {\n        var val = [];\n\n        data = [data];\n        data.push.apply(data, currentData);\n\n        for (var d = 0; d < data.length; d++) {\n          var id = data[d].id;\n\n          if ($.inArray(id, val) === -1) {\n            val.push(id);\n          }\n        }\n\n        self.$element.val(val);\n        self.$element.trigger('input').trigger('change');\n      });\n    } else {\n      var val = data.id;\n\n      this.$element.val(val);\n      this.$element.trigger('input').trigger('change');\n    }\n  };\n\n  SelectAdapter.prototype.unselect = function (data) {\n    var self = this;\n\n    if (!this.$element.prop('multiple')) {\n      return;\n    }\n\n    data.selected = false;\n\n    if ($(data.element).is('option')) {\n      data.element.selected = false;\n\n      this.$element.trigger('input').trigger('change');\n\n      return;\n    }\n\n    this.current(function (currentData) {\n      var val = [];\n\n      for (var d = 0; d < currentData.length; d++) {\n        var id = currentData[d].id;\n\n        if (id !== data.id && $.inArray(id, val) === -1) {\n          val.push(id);\n        }\n      }\n\n      self.$element.val(val);\n\n      self.$element.trigger('input').trigger('change');\n    });\n  };\n\n  SelectAdapter.prototype.bind = function (container, $container) {\n    var self = this;\n\n    this.container = container;\n\n    container.on('select', function (params) {\n      self.select(params.data);\n    });\n\n    container.on('unselect', function (params) {\n      self.unselect(params.data);\n    });\n  };\n\n  SelectAdapter.prototype.destroy = function () {\n    // Remove anything added to child elements\n    this.$element.find('*').each(function () {\n      // Remove any custom data set by Select2\n      Utils.RemoveData(this);\n    });\n  };\n\n  SelectAdapter.prototype.query = function (params, callback) {\n    var data = [];\n    var self = this;\n\n    var $options = this.$element.children();\n\n    $options.each(function () {\n      var $option = $(this);\n\n      if (!$option.is('option') && !$option.is('optgroup')) {\n        return;\n      }\n\n      var option = self.item($option);\n\n      var matches = self.matches(params, option);\n\n      if (matches !== null) {\n        data.push(matches);\n      }\n    });\n\n    callback({\n      results: data\n    });\n  };\n\n  SelectAdapter.prototype.addOptions = function ($options) {\n    Utils.appendMany(this.$element, $options);\n  };\n\n  SelectAdapter.prototype.option = function (data) {\n    var option;\n\n    if (data.children) {\n      option = document.createElement('optgroup');\n      option.label = data.text;\n    } else {\n      option = document.createElement('option');\n\n      if (option.textContent !== undefined) {\n        option.textContent = data.text;\n      } else {\n        option.innerText = data.text;\n      }\n    }\n\n    if (data.id !== undefined) {\n      option.value = data.id;\n    }\n\n    if (data.disabled) {\n      option.disabled = true;\n    }\n\n    if (data.selected) {\n      option.selected = true;\n    }\n\n    if (data.title) {\n      option.title = data.title;\n    }\n\n    var $option = $(option);\n\n    var normalizedData = this._normalizeItem(data);\n    normalizedData.element = option;\n\n    // Override the option's data with the combined data\n    Utils.StoreData(option, 'data', normalizedData);\n\n    return $option;\n  };\n\n  SelectAdapter.prototype.item = function ($option) {\n    var data = {};\n\n    data = Utils.GetData($option[0], 'data');\n\n    if (data != null) {\n      return data;\n    }\n\n    if ($option.is('option')) {\n      data = {\n        id: $option.val(),\n        text: $option.text(),\n        disabled: $option.prop('disabled'),\n        selected: $option.prop('selected'),\n        title: $option.prop('title')\n      };\n    } else if ($option.is('optgroup')) {\n      data = {\n        text: $option.prop('label'),\n        children: [],\n        title: $option.prop('title')\n      };\n\n      var $children = $option.children('option');\n      var children = [];\n\n      for (var c = 0; c < $children.length; c++) {\n        var $child = $($children[c]);\n\n        var child = this.item($child);\n\n        children.push(child);\n      }\n\n      data.children = children;\n    }\n\n    data = this._normalizeItem(data);\n    data.element = $option[0];\n\n    Utils.StoreData($option[0], 'data', data);\n\n    return data;\n  };\n\n  SelectAdapter.prototype._normalizeItem = function (item) {\n    if (item !== Object(item)) {\n      item = {\n        id: item,\n        text: item\n      };\n    }\n\n    item = $.extend({}, {\n      text: ''\n    }, item);\n\n    var defaults = {\n      selected: false,\n      disabled: false\n    };\n\n    if (item.id != null) {\n      item.id = item.id.toString();\n    }\n\n    if (item.text != null) {\n      item.text = item.text.toString();\n    }\n\n    if (item._resultId == null && item.id && this.container != null) {\n      item._resultId = this.generateResultId(this.container, item);\n    }\n\n    return $.extend({}, defaults, item);\n  };\n\n  SelectAdapter.prototype.matches = function (params, data) {\n    var matcher = this.options.get('matcher');\n\n    return matcher(params, data);\n  };\n\n  return SelectAdapter;\n});\n\nS2.define('select2/data/array',[\n  './select',\n  '../utils',\n  'jquery'\n], function (SelectAdapter, Utils, $) {\n  function ArrayAdapter ($element, options) {\n    this._dataToConvert = options.get('data') || [];\n\n    ArrayAdapter.__super__.constructor.call(this, $element, options);\n  }\n\n  Utils.Extend(ArrayAdapter, SelectAdapter);\n\n  ArrayAdapter.prototype.bind = function (container, $container) {\n    ArrayAdapter.__super__.bind.call(this, container, $container);\n\n    this.addOptions(this.convertToOptions(this._dataToConvert));\n  };\n\n  ArrayAdapter.prototype.select = function (data) {\n    var $option = this.$element.find('option').filter(function (i, elm) {\n      return elm.value == data.id.toString();\n    });\n\n    if ($option.length === 0) {\n      $option = this.option(data);\n\n      this.addOptions($option);\n    }\n\n    ArrayAdapter.__super__.select.call(this, data);\n  };\n\n  ArrayAdapter.prototype.convertToOptions = function (data) {\n    var self = this;\n\n    var $existing = this.$element.find('option');\n    var existingIds = $existing.map(function () {\n      return self.item($(this)).id;\n    }).get();\n\n    var $options = [];\n\n    // Filter out all items except for the one passed in the argument\n    function onlyItem (item) {\n      return function () {\n        return $(this).val() == item.id;\n      };\n    }\n\n    for (var d = 0; d < data.length; d++) {\n      var item = this._normalizeItem(data[d]);\n\n      // Skip items which were pre-loaded, only merge the data\n      if ($.inArray(item.id, existingIds) >= 0) {\n        var $existingOption = $existing.filter(onlyItem(item));\n\n        var existingData = this.item($existingOption);\n        var newData = $.extend(true, {}, item, existingData);\n\n        var $newOption = this.option(newData);\n\n        $existingOption.replaceWith($newOption);\n\n        continue;\n      }\n\n      var $option = this.option(item);\n\n      if (item.children) {\n        var $children = this.convertToOptions(item.children);\n\n        Utils.appendMany($option, $children);\n      }\n\n      $options.push($option);\n    }\n\n    return $options;\n  };\n\n  return ArrayAdapter;\n});\n\nS2.define('select2/data/ajax',[\n  './array',\n  '../utils',\n  'jquery'\n], function (ArrayAdapter, Utils, $) {\n  function AjaxAdapter ($element, options) {\n    this.ajaxOptions = this._applyDefaults(options.get('ajax'));\n\n    if (this.ajaxOptions.processResults != null) {\n      this.processResults = this.ajaxOptions.processResults;\n    }\n\n    AjaxAdapter.__super__.constructor.call(this, $element, options);\n  }\n\n  Utils.Extend(AjaxAdapter, ArrayAdapter);\n\n  AjaxAdapter.prototype._applyDefaults = function (options) {\n    var defaults = {\n      data: function (params) {\n        return $.extend({}, params, {\n          q: params.term\n        });\n      },\n      transport: function (params, success, failure) {\n        var $request = $.ajax(params);\n\n        $request.then(success);\n        $request.fail(failure);\n\n        return $request;\n      }\n    };\n\n    return $.extend({}, defaults, options, true);\n  };\n\n  AjaxAdapter.prototype.processResults = function (results) {\n    return results;\n  };\n\n  AjaxAdapter.prototype.query = function (params, callback) {\n    var matches = [];\n    var self = this;\n\n    if (this._request != null) {\n      // JSONP requests cannot always be aborted\n      if ($.isFunction(this._request.abort)) {\n        this._request.abort();\n      }\n\n      this._request = null;\n    }\n\n    var options = $.extend({\n      type: 'GET'\n    }, this.ajaxOptions);\n\n    if (typeof options.url === 'function') {\n      options.url = options.url.call(this.$element, params);\n    }\n\n    if (typeof options.data === 'function') {\n      options.data = options.data.call(this.$element, params);\n    }\n\n    function request () {\n      var $request = options.transport(options, function (data) {\n        var results = self.processResults(data, params);\n\n        if (self.options.get('debug') && window.console && console.error) {\n          // Check to make sure that the response included a `results` key.\n          if (!results || !results.results || !$.isArray(results.results)) {\n            console.error(\n              'Select2: The AJAX results did not return an array in the ' +\n              '`results` key of the response.'\n            );\n          }\n        }\n\n        callback(results);\n      }, function () {\n        // Attempt to detect if a request was aborted\n        // Only works if the transport exposes a status property\n        if ('status' in $request &&\n            ($request.status === 0 || $request.status === '0')) {\n          return;\n        }\n\n        self.trigger('results:message', {\n          message: 'errorLoading'\n        });\n      });\n\n      self._request = $request;\n    }\n\n    if (this.ajaxOptions.delay && params.term != null) {\n      if (this._queryTimeout) {\n        window.clearTimeout(this._queryTimeout);\n      }\n\n      this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay);\n    } else {\n      request();\n    }\n  };\n\n  return AjaxAdapter;\n});\n\nS2.define('select2/data/tags',[\n  'jquery'\n], function ($) {\n  function Tags (decorated, $element, options) {\n    var tags = options.get('tags');\n\n    var createTag = options.get('createTag');\n\n    if (createTag !== undefined) {\n      this.createTag = createTag;\n    }\n\n    var insertTag = options.get('insertTag');\n\n    if (insertTag !== undefined) {\n        this.insertTag = insertTag;\n    }\n\n    decorated.call(this, $element, options);\n\n    if ($.isArray(tags)) {\n      for (var t = 0; t < tags.length; t++) {\n        var tag = tags[t];\n        var item = this._normalizeItem(tag);\n\n        var $option = this.option(item);\n\n        this.$element.append($option);\n      }\n    }\n  }\n\n  Tags.prototype.query = function (decorated, params, callback) {\n    var self = this;\n\n    this._removeOldTags();\n\n    if (params.term == null || params.page != null) {\n      decorated.call(this, params, callback);\n      return;\n    }\n\n    function wrapper (obj, child) {\n      var data = obj.results;\n\n      for (var i = 0; i < data.length; i++) {\n        var option = data[i];\n\n        var checkChildren = (\n          option.children != null &&\n          !wrapper({\n            results: option.children\n          }, true)\n        );\n\n        var optionText = (option.text || '').toUpperCase();\n        var paramsTerm = (params.term || '').toUpperCase();\n\n        var checkText = optionText === paramsTerm;\n\n        if (checkText || checkChildren) {\n          if (child) {\n            return false;\n          }\n\n          obj.data = data;\n          callback(obj);\n\n          return;\n        }\n      }\n\n      if (child) {\n        return true;\n      }\n\n      var tag = self.createTag(params);\n\n      if (tag != null) {\n        var $option = self.option(tag);\n        $option.attr('data-select2-tag', true);\n\n        self.addOptions([$option]);\n\n        self.insertTag(data, tag);\n      }\n\n      obj.results = data;\n\n      callback(obj);\n    }\n\n    decorated.call(this, params, wrapper);\n  };\n\n  Tags.prototype.createTag = function (decorated, params) {\n    var term = $.trim(params.term);\n\n    if (term === '') {\n      return null;\n    }\n\n    return {\n      id: term,\n      text: term\n    };\n  };\n\n  Tags.prototype.insertTag = function (_, data, tag) {\n    data.unshift(tag);\n  };\n\n  Tags.prototype._removeOldTags = function (_) {\n    var $options = this.$element.find('option[data-select2-tag]');\n\n    $options.each(function () {\n      if (this.selected) {\n        return;\n      }\n\n      $(this).remove();\n    });\n  };\n\n  return Tags;\n});\n\nS2.define('select2/data/tokenizer',[\n  'jquery'\n], function ($) {\n  function Tokenizer (decorated, $element, options) {\n    var tokenizer = options.get('tokenizer');\n\n    if (tokenizer !== undefined) {\n      this.tokenizer = tokenizer;\n    }\n\n    decorated.call(this, $element, options);\n  }\n\n  Tokenizer.prototype.bind = function (decorated, container, $container) {\n    decorated.call(this, container, $container);\n\n    this.$search =  container.dropdown.$search || container.selection.$search ||\n      $container.find('.select2-search__field');\n  };\n\n  Tokenizer.prototype.query = function (decorated, params, callback) {\n    var self = this;\n\n    function createAndSelect (data) {\n      // Normalize the data object so we can use it for checks\n      var item = self._normalizeItem(data);\n\n      // Check if the data object already exists as a tag\n      // Select it if it doesn't\n      var $existingOptions = self.$element.find('option').filter(function () {\n        return $(this).val() === item.id;\n      });\n\n      // If an existing option wasn't found for it, create the option\n      if (!$existingOptions.length) {\n        var $option = self.option(item);\n        $option.attr('data-select2-tag', true);\n\n        self._removeOldTags();\n        self.addOptions([$option]);\n      }\n\n      // Select the item, now that we know there is an option for it\n      select(item);\n    }\n\n    function select (data) {\n      self.trigger('select', {\n        data: data\n      });\n    }\n\n    params.term = params.term || '';\n\n    var tokenData = this.tokenizer(params, this.options, createAndSelect);\n\n    if (tokenData.term !== params.term) {\n      // Replace the search term if we have the search box\n      if (this.$search.length) {\n        this.$search.val(tokenData.term);\n        this.$search.trigger('focus');\n      }\n\n      params.term = tokenData.term;\n    }\n\n    decorated.call(this, params, callback);\n  };\n\n  Tokenizer.prototype.tokenizer = function (_, params, options, callback) {\n    var separators = options.get('tokenSeparators') || [];\n    var term = params.term;\n    var i = 0;\n\n    var createTag = this.createTag || function (params) {\n      return {\n        id: params.term,\n        text: params.term\n      };\n    };\n\n    while (i < term.length) {\n      var termChar = term[i];\n\n      if ($.inArray(termChar, separators) === -1) {\n        i++;\n\n        continue;\n      }\n\n      var part = term.substr(0, i);\n      var partParams = $.extend({}, params, {\n        term: part\n      });\n\n      var data = createTag(partParams);\n\n      if (data == null) {\n        i++;\n        continue;\n      }\n\n      callback(data);\n\n      // Reset the term to not include the tokenized portion\n      term = term.substr(i + 1) || '';\n      i = 0;\n    }\n\n    return {\n      term: term\n    };\n  };\n\n  return Tokenizer;\n});\n\nS2.define('select2/data/minimumInputLength',[\n\n], function () {\n  function MinimumInputLength (decorated, $e, options) {\n    this.minimumInputLength = options.get('minimumInputLength');\n\n    decorated.call(this, $e, options);\n  }\n\n  MinimumInputLength.prototype.query = function (decorated, params, callback) {\n    params.term = params.term || '';\n\n    if (params.term.length < this.minimumInputLength) {\n      this.trigger('results:message', {\n        message: 'inputTooShort',\n        args: {\n          minimum: this.minimumInputLength,\n          input: params.term,\n          params: params\n        }\n      });\n\n      return;\n    }\n\n    decorated.call(this, params, callback);\n  };\n\n  return MinimumInputLength;\n});\n\nS2.define('select2/data/maximumInputLength',[\n\n], function () {\n  function MaximumInputLength (decorated, $e, options) {\n    this.maximumInputLength = options.get('maximumInputLength');\n\n    decorated.call(this, $e, options);\n  }\n\n  MaximumInputLength.prototype.query = function (decorated, params, callback) {\n    params.term = params.term || '';\n\n    if (this.maximumInputLength > 0 &&\n        params.term.length > this.maximumInputLength) {\n      this.trigger('results:message', {\n        message: 'inputTooLong',\n        args: {\n          maximum: this.maximumInputLength,\n          input: params.term,\n          params: params\n        }\n      });\n\n      return;\n    }\n\n    decorated.call(this, params, callback);\n  };\n\n  return MaximumInputLength;\n});\n\nS2.define('select2/data/maximumSelectionLength',[\n\n], function (){\n  function MaximumSelectionLength (decorated, $e, options) {\n    this.maximumSelectionLength = options.get('maximumSelectionLength');\n\n    decorated.call(this, $e, options);\n  }\n\n  MaximumSelectionLength.prototype.bind =\n    function (decorated, container, $container) {\n      var self = this;\n\n      decorated.call(this, container, $container);\n\n      container.on('select', function () {\n        self._checkIfMaximumSelected();\n      });\n  };\n\n  MaximumSelectionLength.prototype.query =\n    function (decorated, params, callback) {\n      var self = this;\n\n      this._checkIfMaximumSelected(function () {\n        decorated.call(self, params, callback);\n      });\n  };\n\n  MaximumSelectionLength.prototype._checkIfMaximumSelected =\n    function (_, successCallback) {\n      var self = this;\n\n      this.current(function (currentData) {\n        var count = currentData != null ? currentData.length : 0;\n        if (self.maximumSelectionLength > 0 &&\n          count >= self.maximumSelectionLength) {\n          self.trigger('results:message', {\n            message: 'maximumSelected',\n            args: {\n              maximum: self.maximumSelectionLength\n            }\n          });\n          return;\n        }\n\n        if (successCallback) {\n          successCallback();\n        }\n      });\n  };\n\n  return MaximumSelectionLength;\n});\n\nS2.define('select2/dropdown',[\n  'jquery',\n  './utils'\n], function ($, Utils) {\n  function Dropdown ($element, options) {\n    this.$element = $element;\n    this.options = options;\n\n    Dropdown.__super__.constructor.call(this);\n  }\n\n  Utils.Extend(Dropdown, Utils.Observable);\n\n  Dropdown.prototype.render = function () {\n    var $dropdown = $(\n      '<span class=\"select2-dropdown\">' +\n        '<span class=\"select2-results\"></span>' +\n      '</span>'\n    );\n\n    $dropdown.attr('dir', this.options.get('dir'));\n\n    this.$dropdown = $dropdown;\n\n    return $dropdown;\n  };\n\n  Dropdown.prototype.bind = function () {\n    // Should be implemented in subclasses\n  };\n\n  Dropdown.prototype.position = function ($dropdown, $container) {\n    // Should be implemented in subclasses\n  };\n\n  Dropdown.prototype.destroy = function () {\n    // Remove the dropdown from the DOM\n    this.$dropdown.remove();\n  };\n\n  return Dropdown;\n});\n\nS2.define('select2/dropdown/search',[\n  'jquery',\n  '../utils'\n], function ($, Utils) {\n  function Search () { }\n\n  Search.prototype.render = function (decorated) {\n    var $rendered = decorated.call(this);\n\n    var $search = $(\n      '<span class=\"select2-search select2-search--dropdown\">' +\n        '<input class=\"select2-search__field\" type=\"search\" tabindex=\"-1\"' +\n        ' autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"none\"' +\n        ' spellcheck=\"false\" role=\"searchbox\" aria-autocomplete=\"list\" />' +\n      '</span>'\n    );\n\n    this.$searchContainer = $search;\n    this.$search = $search.find('input');\n\n    $rendered.prepend($search);\n\n    return $rendered;\n  };\n\n  Search.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n\n    var resultsId = container.id + '-results';\n\n    decorated.call(this, container, $container);\n\n    this.$search.on('keydown', function (evt) {\n      self.trigger('keypress', evt);\n\n      self._keyUpPrevented = evt.isDefaultPrevented();\n    });\n\n    // Workaround for browsers which do not support the `input` event\n    // This will prevent double-triggering of events for browsers which support\n    // both the `keyup` and `input` events.\n    this.$search.on('input', function (evt) {\n      // Unbind the duplicated `keyup` event\n      $(this).off('keyup');\n    });\n\n    this.$search.on('keyup input', function (evt) {\n      self.handleSearch(evt);\n    });\n\n    container.on('open', function () {\n      self.$search.attr('tabindex', 0);\n      self.$search.attr('aria-controls', resultsId);\n\n      self.$search.trigger('focus');\n\n      window.setTimeout(function () {\n        self.$search.trigger('focus');\n      }, 0);\n    });\n\n    container.on('close', function () {\n      self.$search.attr('tabindex', -1);\n      self.$search.removeAttr('aria-controls');\n      self.$search.removeAttr('aria-activedescendant');\n\n      self.$search.val('');\n      self.$search.trigger('blur');\n    });\n\n    container.on('focus', function () {\n      if (!container.isOpen()) {\n        self.$search.trigger('focus');\n      }\n    });\n\n    container.on('results:all', function (params) {\n      if (params.query.term == null || params.query.term === '') {\n        var showSearch = self.showSearch(params);\n\n        if (showSearch) {\n          self.$searchContainer.removeClass('select2-search--hide');\n        } else {\n          self.$searchContainer.addClass('select2-search--hide');\n        }\n      }\n    });\n\n    container.on('results:focus', function (params) {\n      if (params.data._resultId) {\n        self.$search.attr('aria-activedescendant', params.data._resultId);\n      } else {\n        self.$search.removeAttr('aria-activedescendant');\n      }\n    });\n  };\n\n  Search.prototype.handleSearch = function (evt) {\n    if (!this._keyUpPrevented) {\n      var input = this.$search.val();\n\n      this.trigger('query', {\n        term: input\n      });\n    }\n\n    this._keyUpPrevented = false;\n  };\n\n  Search.prototype.showSearch = function (_, params) {\n    return true;\n  };\n\n  return Search;\n});\n\nS2.define('select2/dropdown/hidePlaceholder',[\n\n], function () {\n  function HidePlaceholder (decorated, $element, options, dataAdapter) {\n    this.placeholder = this.normalizePlaceholder(options.get('placeholder'));\n\n    decorated.call(this, $element, options, dataAdapter);\n  }\n\n  HidePlaceholder.prototype.append = function (decorated, data) {\n    data.results = this.removePlaceholder(data.results);\n\n    decorated.call(this, data);\n  };\n\n  HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) {\n    if (typeof placeholder === 'string') {\n      placeholder = {\n        id: '',\n        text: placeholder\n      };\n    }\n\n    return placeholder;\n  };\n\n  HidePlaceholder.prototype.removePlaceholder = function (_, data) {\n    var modifiedData = data.slice(0);\n\n    for (var d = data.length - 1; d >= 0; d--) {\n      var item = data[d];\n\n      if (this.placeholder.id === item.id) {\n        modifiedData.splice(d, 1);\n      }\n    }\n\n    return modifiedData;\n  };\n\n  return HidePlaceholder;\n});\n\nS2.define('select2/dropdown/infiniteScroll',[\n  'jquery'\n], function ($) {\n  function InfiniteScroll (decorated, $element, options, dataAdapter) {\n    this.lastParams = {};\n\n    decorated.call(this, $element, options, dataAdapter);\n\n    this.$loadingMore = this.createLoadingMore();\n    this.loading = false;\n  }\n\n  InfiniteScroll.prototype.append = function (decorated, data) {\n    this.$loadingMore.remove();\n    this.loading = false;\n\n    decorated.call(this, data);\n\n    if (this.showLoadingMore(data)) {\n      this.$results.append(this.$loadingMore);\n      this.loadMoreIfNeeded();\n    }\n  };\n\n  InfiniteScroll.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n\n    decorated.call(this, container, $container);\n\n    container.on('query', function (params) {\n      self.lastParams = params;\n      self.loading = true;\n    });\n\n    container.on('query:append', function (params) {\n      self.lastParams = params;\n      self.loading = true;\n    });\n\n    this.$results.on('scroll', this.loadMoreIfNeeded.bind(this));\n  };\n\n  InfiniteScroll.prototype.loadMoreIfNeeded = function () {\n    var isLoadMoreVisible = $.contains(\n      document.documentElement,\n      this.$loadingMore[0]\n    );\n\n    if (this.loading || !isLoadMoreVisible) {\n      return;\n    }\n\n    var currentOffset = this.$results.offset().top +\n      this.$results.outerHeight(false);\n    var loadingMoreOffset = this.$loadingMore.offset().top +\n      this.$loadingMore.outerHeight(false);\n\n    if (currentOffset + 50 >= loadingMoreOffset) {\n      this.loadMore();\n    }\n  };\n\n  InfiniteScroll.prototype.loadMore = function () {\n    this.loading = true;\n\n    var params = $.extend({}, {page: 1}, this.lastParams);\n\n    params.page++;\n\n    this.trigger('query:append', params);\n  };\n\n  InfiniteScroll.prototype.showLoadingMore = function (_, data) {\n    return data.pagination && data.pagination.more;\n  };\n\n  InfiniteScroll.prototype.createLoadingMore = function () {\n    var $option = $(\n      '<li ' +\n      'class=\"select2-results__option select2-results__option--load-more\"' +\n      'role=\"option\" aria-disabled=\"true\"></li>'\n    );\n\n    var message = this.options.get('translations').get('loadingMore');\n\n    $option.html(message(this.lastParams));\n\n    return $option;\n  };\n\n  return InfiniteScroll;\n});\n\nS2.define('select2/dropdown/attachBody',[\n  'jquery',\n  '../utils'\n], function ($, Utils) {\n  function AttachBody (decorated, $element, options) {\n    this.$dropdownParent = $(options.get('dropdownParent') || document.body);\n\n    decorated.call(this, $element, options);\n  }\n\n  AttachBody.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n\n    decorated.call(this, container, $container);\n\n    container.on('open', function () {\n      self._showDropdown();\n      self._attachPositioningHandler(container);\n\n      // Must bind after the results handlers to ensure correct sizing\n      self._bindContainerResultHandlers(container);\n    });\n\n    container.on('close', function () {\n      self._hideDropdown();\n      self._detachPositioningHandler(container);\n    });\n\n    this.$dropdownContainer.on('mousedown', function (evt) {\n      evt.stopPropagation();\n    });\n  };\n\n  AttachBody.prototype.destroy = function (decorated) {\n    decorated.call(this);\n\n    this.$dropdownContainer.remove();\n  };\n\n  AttachBody.prototype.position = function (decorated, $dropdown, $container) {\n    // Clone all of the container classes\n    $dropdown.attr('class', $container.attr('class'));\n\n    $dropdown.removeClass('select2');\n    $dropdown.addClass('select2-container--open');\n\n    $dropdown.css({\n      position: 'absolute',\n      top: -999999\n    });\n\n    this.$container = $container;\n  };\n\n  AttachBody.prototype.render = function (decorated) {\n    var $container = $('<span></span>');\n\n    var $dropdown = decorated.call(this);\n    $container.append($dropdown);\n\n    this.$dropdownContainer = $container;\n\n    return $container;\n  };\n\n  AttachBody.prototype._hideDropdown = function (decorated) {\n    this.$dropdownContainer.detach();\n  };\n\n  AttachBody.prototype._bindContainerResultHandlers =\n      function (decorated, container) {\n\n    // These should only be bound once\n    if (this._containerResultsHandlersBound) {\n      return;\n    }\n\n    var self = this;\n\n    container.on('results:all', function () {\n      self._positionDropdown();\n      self._resizeDropdown();\n    });\n\n    container.on('results:append', function () {\n      self._positionDropdown();\n      self._resizeDropdown();\n    });\n\n    container.on('results:message', function () {\n      self._positionDropdown();\n      self._resizeDropdown();\n    });\n\n    container.on('select', function () {\n      self._positionDropdown();\n      self._resizeDropdown();\n    });\n\n    container.on('unselect', function () {\n      self._positionDropdown();\n      self._resizeDropdown();\n    });\n\n    this._containerResultsHandlersBound = true;\n  };\n\n  AttachBody.prototype._attachPositioningHandler =\n      function (decorated, container) {\n    var self = this;\n\n    var scrollEvent = 'scroll.select2.' + container.id;\n    var resizeEvent = 'resize.select2.' + container.id;\n    var orientationEvent = 'orientationchange.select2.' + container.id;\n\n    var $watchers = this.$container.parents().filter(Utils.hasScroll);\n    $watchers.each(function () {\n      Utils.StoreData(this, 'select2-scroll-position', {\n        x: $(this).scrollLeft(),\n        y: $(this).scrollTop()\n      });\n    });\n\n    $watchers.on(scrollEvent, function (ev) {\n      var position = Utils.GetData(this, 'select2-scroll-position');\n      $(this).scrollTop(position.y);\n    });\n\n    $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent,\n      function (e) {\n      self._positionDropdown();\n      self._resizeDropdown();\n    });\n  };\n\n  AttachBody.prototype._detachPositioningHandler =\n      function (decorated, container) {\n    var scrollEvent = 'scroll.select2.' + container.id;\n    var resizeEvent = 'resize.select2.' + container.id;\n    var orientationEvent = 'orientationchange.select2.' + container.id;\n\n    var $watchers = this.$container.parents().filter(Utils.hasScroll);\n    $watchers.off(scrollEvent);\n\n    $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent);\n  };\n\n  AttachBody.prototype._positionDropdown = function () {\n    var $window = $(window);\n\n    var isCurrentlyAbove = this.$dropdown.hasClass('select2-dropdown--above');\n    var isCurrentlyBelow = this.$dropdown.hasClass('select2-dropdown--below');\n\n    var newDirection = null;\n\n    var offset = this.$container.offset();\n\n    offset.bottom = offset.top + this.$container.outerHeight(false);\n\n    var container = {\n      height: this.$container.outerHeight(false)\n    };\n\n    container.top = offset.top;\n    container.bottom = offset.top + container.height;\n\n    var dropdown = {\n      height: this.$dropdown.outerHeight(false)\n    };\n\n    var viewport = {\n      top: $window.scrollTop(),\n      bottom: $window.scrollTop() + $window.height()\n    };\n\n    var enoughRoomAbove = viewport.top < (offset.top - dropdown.height);\n    var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height);\n\n    var css = {\n      left: offset.left,\n      top: container.bottom\n    };\n\n    // Determine what the parent element is to use for calculating the offset\n    var $offsetParent = this.$dropdownParent;\n\n    // For statically positioned elements, we need to get the element\n    // that is determining the offset\n    if ($offsetParent.css('position') === 'static') {\n      $offsetParent = $offsetParent.offsetParent();\n    }\n\n    var parentOffset = {\n      top: 0,\n      left: 0\n    };\n\n    if (\n      $.contains(document.body, $offsetParent[0]) ||\n      $offsetParent[0].isConnected\n      ) {\n      parentOffset = $offsetParent.offset();\n    }\n\n    css.top -= parentOffset.top;\n    css.left -= parentOffset.left;\n\n    if (!isCurrentlyAbove && !isCurrentlyBelow) {\n      newDirection = 'below';\n    }\n\n    if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) {\n      newDirection = 'above';\n    } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) {\n      newDirection = 'below';\n    }\n\n    if (newDirection == 'above' ||\n      (isCurrentlyAbove && newDirection !== 'below')) {\n      css.top = container.top - parentOffset.top - dropdown.height;\n    }\n\n    if (newDirection != null) {\n      this.$dropdown\n        .removeClass('select2-dropdown--below select2-dropdown--above')\n        .addClass('select2-dropdown--' + newDirection);\n      this.$container\n        .removeClass('select2-container--below select2-container--above')\n        .addClass('select2-container--' + newDirection);\n    }\n\n    this.$dropdownContainer.css(css);\n  };\n\n  AttachBody.prototype._resizeDropdown = function () {\n    var css = {\n      width: this.$container.outerWidth(false) + 'px'\n    };\n\n    if (this.options.get('dropdownAutoWidth')) {\n      css.minWidth = css.width;\n      css.position = 'relative';\n      css.width = 'auto';\n    }\n\n    this.$dropdown.css(css);\n  };\n\n  AttachBody.prototype._showDropdown = function (decorated) {\n    this.$dropdownContainer.appendTo(this.$dropdownParent);\n\n    this._positionDropdown();\n    this._resizeDropdown();\n  };\n\n  return AttachBody;\n});\n\nS2.define('select2/dropdown/minimumResultsForSearch',[\n\n], function () {\n  function countResults (data) {\n    var count = 0;\n\n    for (var d = 0; d < data.length; d++) {\n      var item = data[d];\n\n      if (item.children) {\n        count += countResults(item.children);\n      } else {\n        count++;\n      }\n    }\n\n    return count;\n  }\n\n  function MinimumResultsForSearch (decorated, $element, options, dataAdapter) {\n    this.minimumResultsForSearch = options.get('minimumResultsForSearch');\n\n    if (this.minimumResultsForSearch < 0) {\n      this.minimumResultsForSearch = Infinity;\n    }\n\n    decorated.call(this, $element, options, dataAdapter);\n  }\n\n  MinimumResultsForSearch.prototype.showSearch = function (decorated, params) {\n    if (countResults(params.data.results) < this.minimumResultsForSearch) {\n      return false;\n    }\n\n    return decorated.call(this, params);\n  };\n\n  return MinimumResultsForSearch;\n});\n\nS2.define('select2/dropdown/selectOnClose',[\n  '../utils'\n], function (Utils) {\n  function SelectOnClose () { }\n\n  SelectOnClose.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n\n    decorated.call(this, container, $container);\n\n    container.on('close', function (params) {\n      self._handleSelectOnClose(params);\n    });\n  };\n\n  SelectOnClose.prototype._handleSelectOnClose = function (_, params) {\n    if (params && params.originalSelect2Event != null) {\n      var event = params.originalSelect2Event;\n\n      // Don't select an item if the close event was triggered from a select or\n      // unselect event\n      if (event._type === 'select' || event._type === 'unselect') {\n        return;\n      }\n    }\n\n    var $highlightedResults = this.getHighlightedResults();\n\n    // Only select highlighted results\n    if ($highlightedResults.length < 1) {\n      return;\n    }\n\n    var data = Utils.GetData($highlightedResults[0], 'data');\n\n    // Don't re-select already selected resulte\n    if (\n      (data.element != null && data.element.selected) ||\n      (data.element == null && data.selected)\n    ) {\n      return;\n    }\n\n    this.trigger('select', {\n        data: data\n    });\n  };\n\n  return SelectOnClose;\n});\n\nS2.define('select2/dropdown/closeOnSelect',[\n\n], function () {\n  function CloseOnSelect () { }\n\n  CloseOnSelect.prototype.bind = function (decorated, container, $container) {\n    var self = this;\n\n    decorated.call(this, container, $container);\n\n    container.on('select', function (evt) {\n      self._selectTriggered(evt);\n    });\n\n    container.on('unselect', function (evt) {\n      self._selectTriggered(evt);\n    });\n  };\n\n  CloseOnSelect.prototype._selectTriggered = function (_, evt) {\n    var originalEvent = evt.originalEvent;\n\n    // Don't close if the control key is being held\n    if (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)) {\n      return;\n    }\n\n    this.trigger('close', {\n      originalEvent: originalEvent,\n      originalSelect2Event: evt\n    });\n  };\n\n  return CloseOnSelect;\n});\n\nS2.define('select2/i18n/en',[],function () {\n  // English\n  return {\n    errorLoading: function () {\n      return 'The results could not be loaded.';\n    },\n    inputTooLong: function (args) {\n      var overChars = args.input.length - args.maximum;\n\n      var message = 'Please delete ' + overChars + ' character';\n\n      if (overChars != 1) {\n        message += 's';\n      }\n\n      return message;\n    },\n    inputTooShort: function (args) {\n      var remainingChars = args.minimum - args.input.length;\n\n      var message = 'Please enter ' + remainingChars + ' or more characters';\n\n      return message;\n    },\n    loadingMore: function () {\n      return 'Loading more results…';\n    },\n    maximumSelected: function (args) {\n      var message = 'You can only select ' + args.maximum + ' item';\n\n      if (args.maximum != 1) {\n        message += 's';\n      }\n\n      return message;\n    },\n    noResults: function () {\n      return 'No results found';\n    },\n    searching: function () {\n      return 'Searching…';\n    },\n    removeAllItems: function () {\n      return 'Remove all items';\n    }\n  };\n});\n\nS2.define('select2/defaults',[\n  'jquery',\n  'require',\n\n  './results',\n\n  './selection/single',\n  './selection/multiple',\n  './selection/placeholder',\n  './selection/allowClear',\n  './selection/search',\n  './selection/eventRelay',\n\n  './utils',\n  './translation',\n  './diacritics',\n\n  './data/select',\n  './data/array',\n  './data/ajax',\n  './data/tags',\n  './data/tokenizer',\n  './data/minimumInputLength',\n  './data/maximumInputLength',\n  './data/maximumSelectionLength',\n\n  './dropdown',\n  './dropdown/search',\n  './dropdown/hidePlaceholder',\n  './dropdown/infiniteScroll',\n  './dropdown/attachBody',\n  './dropdown/minimumResultsForSearch',\n  './dropdown/selectOnClose',\n  './dropdown/closeOnSelect',\n\n  './i18n/en'\n], function ($, require,\n\n             ResultsList,\n\n             SingleSelection, MultipleSelection, Placeholder, AllowClear,\n             SelectionSearch, EventRelay,\n\n             Utils, Translation, DIACRITICS,\n\n             SelectData, ArrayData, AjaxData, Tags, Tokenizer,\n             MinimumInputLength, MaximumInputLength, MaximumSelectionLength,\n\n             Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll,\n             AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect,\n\n             EnglishTranslation) {\n  function Defaults () {\n    this.reset();\n  }\n\n  Defaults.prototype.apply = function (options) {\n    options = $.extend(true, {}, this.defaults, options);\n\n    if (options.dataAdapter == null) {\n      if (options.ajax != null) {\n        options.dataAdapter = AjaxData;\n      } else if (options.data != null) {\n        options.dataAdapter = ArrayData;\n      } else {\n        options.dataAdapter = SelectData;\n      }\n\n      if (options.minimumInputLength > 0) {\n        options.dataAdapter = Utils.Decorate(\n          options.dataAdapter,\n          MinimumInputLength\n        );\n      }\n\n      if (options.maximumInputLength > 0) {\n        options.dataAdapter = Utils.Decorate(\n          options.dataAdapter,\n          MaximumInputLength\n        );\n      }\n\n      if (options.maximumSelectionLength > 0) {\n        options.dataAdapter = Utils.Decorate(\n          options.dataAdapter,\n          MaximumSelectionLength\n        );\n      }\n\n      if (options.tags) {\n        options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags);\n      }\n\n      if (options.tokenSeparators != null || options.tokenizer != null) {\n        options.dataAdapter = Utils.Decorate(\n          options.dataAdapter,\n          Tokenizer\n        );\n      }\n\n      if (options.query != null) {\n        var Query = require(options.amdBase + 'compat/query');\n\n        options.dataAdapter = Utils.Decorate(\n          options.dataAdapter,\n          Query\n        );\n      }\n\n      if (options.initSelection != null) {\n        var InitSelection = require(options.amdBase + 'compat/initSelection');\n\n        options.dataAdapter = Utils.Decorate(\n          options.dataAdapter,\n          InitSelection\n        );\n      }\n    }\n\n    if (options.resultsAdapter == null) {\n      options.resultsAdapter = ResultsList;\n\n      if (options.ajax != null) {\n        options.resultsAdapter = Utils.Decorate(\n          options.resultsAdapter,\n          InfiniteScroll\n        );\n      }\n\n      if (options.placeholder != null) {\n        options.resultsAdapter = Utils.Decorate(\n          options.resultsAdapter,\n          HidePlaceholder\n        );\n      }\n\n      if (options.selectOnClose) {\n        options.resultsAdapter = Utils.Decorate(\n          options.resultsAdapter,\n          SelectOnClose\n        );\n      }\n    }\n\n    if (options.dropdownAdapter == null) {\n      if (options.multiple) {\n        options.dropdownAdapter = Dropdown;\n      } else {\n        var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch);\n\n        options.dropdownAdapter = SearchableDropdown;\n      }\n\n      if (options.minimumResultsForSearch !== 0) {\n        options.dropdownAdapter = Utils.Decorate(\n          options.dropdownAdapter,\n          MinimumResultsForSearch\n        );\n      }\n\n      if (options.closeOnSelect) {\n        options.dropdownAdapter = Utils.Decorate(\n          options.dropdownAdapter,\n          CloseOnSelect\n        );\n      }\n\n      if (\n        options.dropdownCssClass != null ||\n        options.dropdownCss != null ||\n        options.adaptDropdownCssClass != null\n      ) {\n        var DropdownCSS = require(options.amdBase + 'compat/dropdownCss');\n\n        options.dropdownAdapter = Utils.Decorate(\n          options.dropdownAdapter,\n          DropdownCSS\n        );\n      }\n\n      options.dropdownAdapter = Utils.Decorate(\n        options.dropdownAdapter,\n        AttachBody\n      );\n    }\n\n    if (options.selectionAdapter == null) {\n      if (options.multiple) {\n        options.selectionAdapter = MultipleSelection;\n      } else {\n        options.selectionAdapter = SingleSelection;\n      }\n\n      // Add the placeholder mixin if a placeholder was specified\n      if (options.placeholder != null) {\n        options.selectionAdapter = Utils.Decorate(\n          options.selectionAdapter,\n          Placeholder\n        );\n      }\n\n      if (options.allowClear) {\n        options.selectionAdapter = Utils.Decorate(\n          options.selectionAdapter,\n          AllowClear\n        );\n      }\n\n      if (options.multiple) {\n        options.selectionAdapter = Utils.Decorate(\n          options.selectionAdapter,\n          SelectionSearch\n        );\n      }\n\n      if (\n        options.containerCssClass != null ||\n        options.containerCss != null ||\n        options.adaptContainerCssClass != null\n      ) {\n        var ContainerCSS = require(options.amdBase + 'compat/containerCss');\n\n        options.selectionAdapter = Utils.Decorate(\n          options.selectionAdapter,\n          ContainerCSS\n        );\n      }\n\n      options.selectionAdapter = Utils.Decorate(\n        options.selectionAdapter,\n        EventRelay\n      );\n    }\n\n    // If the defaults were not previously applied from an element, it is\n    // possible for the language option to have not been resolved\n    options.language = this._resolveLanguage(options.language);\n\n    // Always fall back to English since it will always be complete\n    options.language.push('en');\n\n    var uniqueLanguages = [];\n\n    for (var l = 0; l < options.language.length; l++) {\n      var language = options.language[l];\n\n      if (uniqueLanguages.indexOf(language) === -1) {\n        uniqueLanguages.push(language);\n      }\n    }\n\n    options.language = uniqueLanguages;\n\n    options.translations = this._processTranslations(\n      options.language,\n      options.debug\n    );\n\n    return options;\n  };\n\n  Defaults.prototype.reset = function () {\n    function stripDiacritics (text) {\n      // Used 'uni range + named function' from http://jsperf.com/diacritics/18\n      function match(a) {\n        return DIACRITICS[a] || a;\n      }\n\n      return text.replace(/[^\\u0000-\\u007E]/g, match);\n    }\n\n    function matcher (params, data) {\n      // Always return the object if there is nothing to compare\n      if ($.trim(params.term) === '') {\n        return data;\n      }\n\n      // Do a recursive check for options with children\n      if (data.children && data.children.length > 0) {\n        // Clone the data object if there are children\n        // This is required as we modify the object to remove any non-matches\n        var match = $.extend(true, {}, data);\n\n        // Check each child of the option\n        for (var c = data.children.length - 1; c >= 0; c--) {\n          var child = data.children[c];\n\n          var matches = matcher(params, child);\n\n          // If there wasn't a match, remove the object in the array\n          if (matches == null) {\n            match.children.splice(c, 1);\n          }\n        }\n\n        // If any children matched, return the new object\n        if (match.children.length > 0) {\n          return match;\n        }\n\n        // If there were no matching children, check just the plain object\n        return matcher(params, match);\n      }\n\n      var original = stripDiacritics(data.text).toUpperCase();\n      var term = stripDiacritics(params.term).toUpperCase();\n\n      // Check if the text contains the term\n      if (original.indexOf(term) > -1) {\n        return data;\n      }\n\n      // If it doesn't contain the term, don't return anything\n      return null;\n    }\n\n    this.defaults = {\n      amdBase: './',\n      amdLanguageBase: './i18n/',\n      closeOnSelect: true,\n      debug: false,\n      dropdownAutoWidth: false,\n      escapeMarkup: Utils.escapeMarkup,\n      language: {},\n      matcher: matcher,\n      minimumInputLength: 0,\n      maximumInputLength: 0,\n      maximumSelectionLength: 0,\n      minimumResultsForSearch: 0,\n      selectOnClose: false,\n      scrollAfterSelect: false,\n      sorter: function (data) {\n        return data;\n      },\n      templateResult: function (result) {\n        return result.text;\n      },\n      templateSelection: function (selection) {\n        return selection.text;\n      },\n      theme: 'default',\n      width: 'resolve'\n    };\n  };\n\n  Defaults.prototype.applyFromElement = function (options, $element) {\n    var optionLanguage = options.language;\n    var defaultLanguage = this.defaults.language;\n    var elementLanguage = $element.prop('lang');\n    var parentLanguage = $element.closest('[lang]').prop('lang');\n\n    var languages = Array.prototype.concat.call(\n      this._resolveLanguage(elementLanguage),\n      this._resolveLanguage(optionLanguage),\n      this._resolveLanguage(defaultLanguage),\n      this._resolveLanguage(parentLanguage)\n    );\n\n    options.language = languages;\n\n    return options;\n  };\n\n  Defaults.prototype._resolveLanguage = function (language) {\n    if (!language) {\n      return [];\n    }\n\n    if ($.isEmptyObject(language)) {\n      return [];\n    }\n\n    if ($.isPlainObject(language)) {\n      return [language];\n    }\n\n    var languages;\n\n    if (!$.isArray(language)) {\n      languages = [language];\n    } else {\n      languages = language;\n    }\n\n    var resolvedLanguages = [];\n\n    for (var l = 0; l < languages.length; l++) {\n      resolvedLanguages.push(languages[l]);\n\n      if (typeof languages[l] === 'string' && languages[l].indexOf('-') > 0) {\n        // Extract the region information if it is included\n        var languageParts = languages[l].split('-');\n        var baseLanguage = languageParts[0];\n\n        resolvedLanguages.push(baseLanguage);\n      }\n    }\n\n    return resolvedLanguages;\n  };\n\n  Defaults.prototype._processTranslations = function (languages, debug) {\n    var translations = new Translation();\n\n    for (var l = 0; l < languages.length; l++) {\n      var languageData = new Translation();\n\n      var language = languages[l];\n\n      if (typeof language === 'string') {\n        try {\n          // Try to load it with the original name\n          languageData = Translation.loadPath(language);\n        } catch (e) {\n          try {\n            // If we couldn't load it, check if it wasn't the full path\n            language = this.defaults.amdLanguageBase + language;\n            languageData = Translation.loadPath(language);\n          } catch (ex) {\n            // The translation could not be loaded at all. Sometimes this is\n            // because of a configuration problem, other times this can be\n            // because of how Select2 helps load all possible translation files\n            if (debug && window.console && console.warn) {\n              console.warn(\n                'Select2: The language file for \"' + language + '\" could ' +\n                'not be automatically loaded. A fallback will be used instead.'\n              );\n            }\n          }\n        }\n      } else if ($.isPlainObject(language)) {\n        languageData = new Translation(language);\n      } else {\n        languageData = language;\n      }\n\n      translations.extend(languageData);\n    }\n\n    return translations;\n  };\n\n  Defaults.prototype.set = function (key, value) {\n    var camelKey = $.camelCase(key);\n\n    var data = {};\n    data[camelKey] = value;\n\n    var convertedData = Utils._convertData(data);\n\n    $.extend(true, this.defaults, convertedData);\n  };\n\n  var defaults = new Defaults();\n\n  return defaults;\n});\n\nS2.define('select2/options',[\n  'require',\n  'jquery',\n  './defaults',\n  './utils'\n], function (require, $, Defaults, Utils) {\n  function Options (options, $element) {\n    this.options = options;\n\n    if ($element != null) {\n      this.fromElement($element);\n    }\n\n    if ($element != null) {\n      this.options = Defaults.applyFromElement(this.options, $element);\n    }\n\n    this.options = Defaults.apply(this.options);\n\n    if ($element && $element.is('input')) {\n      var InputCompat = require(this.get('amdBase') + 'compat/inputData');\n\n      this.options.dataAdapter = Utils.Decorate(\n        this.options.dataAdapter,\n        InputCompat\n      );\n    }\n  }\n\n  Options.prototype.fromElement = function ($e) {\n    var excludedData = ['select2'];\n\n    if (this.options.multiple == null) {\n      this.options.multiple = $e.prop('multiple');\n    }\n\n    if (this.options.disabled == null) {\n      this.options.disabled = $e.prop('disabled');\n    }\n\n    if (this.options.dir == null) {\n      if ($e.prop('dir')) {\n        this.options.dir = $e.prop('dir');\n      } else if ($e.closest('[dir]').prop('dir')) {\n        this.options.dir = $e.closest('[dir]').prop('dir');\n      } else {\n        this.options.dir = 'ltr';\n      }\n    }\n\n    $e.prop('disabled', this.options.disabled);\n    $e.prop('multiple', this.options.multiple);\n\n    if (Utils.GetData($e[0], 'select2Tags')) {\n      if (this.options.debug && window.console && console.warn) {\n        console.warn(\n          'Select2: The `data-select2-tags` attribute has been changed to ' +\n          'use the `data-data` and `data-tags=\"true\"` attributes and will be ' +\n          'removed in future versions of Select2.'\n        );\n      }\n\n      Utils.StoreData($e[0], 'data', Utils.GetData($e[0], 'select2Tags'));\n      Utils.StoreData($e[0], 'tags', true);\n    }\n\n    if (Utils.GetData($e[0], 'ajaxUrl')) {\n      if (this.options.debug && window.console && console.warn) {\n        console.warn(\n          'Select2: The `data-ajax-url` attribute has been changed to ' +\n          '`data-ajax--url` and support for the old attribute will be removed' +\n          ' in future versions of Select2.'\n        );\n      }\n\n      $e.attr('ajax--url', Utils.GetData($e[0], 'ajaxUrl'));\n      Utils.StoreData($e[0], 'ajax-Url', Utils.GetData($e[0], 'ajaxUrl'));\n    }\n\n    var dataset = {};\n\n    function upperCaseLetter(_, letter) {\n      return letter.toUpperCase();\n    }\n\n    // Pre-load all of the attributes which are prefixed with `data-`\n    for (var attr = 0; attr < $e[0].attributes.length; attr++) {\n      var attributeName = $e[0].attributes[attr].name;\n      var prefix = 'data-';\n\n      if (attributeName.substr(0, prefix.length) == prefix) {\n        // Get the contents of the attribute after `data-`\n        var dataName = attributeName.substring(prefix.length);\n\n        // Get the data contents from the consistent source\n        // This is more than likely the jQuery data helper\n        var dataValue = Utils.GetData($e[0], dataName);\n\n        // camelCase the attribute name to match the spec\n        var camelDataName = dataName.replace(/-([a-z])/g, upperCaseLetter);\n\n        // Store the data attribute contents into the dataset since\n        dataset[camelDataName] = dataValue;\n      }\n    }\n\n    // Prefer the element's `dataset` attribute if it exists\n    // jQuery 1.x does not correctly handle data attributes with multiple dashes\n    if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) {\n      dataset = $.extend(true, {}, $e[0].dataset, dataset);\n    }\n\n    // Prefer our internal data cache if it exists\n    var data = $.extend(true, {}, Utils.GetData($e[0]), dataset);\n\n    data = Utils._convertData(data);\n\n    for (var key in data) {\n      if ($.inArray(key, excludedData) > -1) {\n        continue;\n      }\n\n      if ($.isPlainObject(this.options[key])) {\n        $.extend(this.options[key], data[key]);\n      } else {\n        this.options[key] = data[key];\n      }\n    }\n\n    return this;\n  };\n\n  Options.prototype.get = function (key) {\n    return this.options[key];\n  };\n\n  Options.prototype.set = function (key, val) {\n    this.options[key] = val;\n  };\n\n  return Options;\n});\n\nS2.define('select2/core',[\n  'jquery',\n  './options',\n  './utils',\n  './keys'\n], function ($, Options, Utils, KEYS) {\n  var Select2 = function ($element, options) {\n    if (Utils.GetData($element[0], 'select2') != null) {\n      Utils.GetData($element[0], 'select2').destroy();\n    }\n\n    this.$element = $element;\n\n    this.id = this._generateId($element);\n\n    options = options || {};\n\n    this.options = new Options(options, $element);\n\n    Select2.__super__.constructor.call(this);\n\n    // Set up the tabindex\n\n    var tabindex = $element.attr('tabindex') || 0;\n    Utils.StoreData($element[0], 'old-tabindex', tabindex);\n    $element.attr('tabindex', '-1');\n\n    // Set up containers and adapters\n\n    var DataAdapter = this.options.get('dataAdapter');\n    this.dataAdapter = new DataAdapter($element, this.options);\n\n    var $container = this.render();\n\n    this._placeContainer($container);\n\n    var SelectionAdapter = this.options.get('selectionAdapter');\n    this.selection = new SelectionAdapter($element, this.options);\n    this.$selection = this.selection.render();\n\n    this.selection.position(this.$selection, $container);\n\n    var DropdownAdapter = this.options.get('dropdownAdapter');\n    this.dropdown = new DropdownAdapter($element, this.options);\n    this.$dropdown = this.dropdown.render();\n\n    this.dropdown.position(this.$dropdown, $container);\n\n    var ResultsAdapter = this.options.get('resultsAdapter');\n    this.results = new ResultsAdapter($element, this.options, this.dataAdapter);\n    this.$results = this.results.render();\n\n    this.results.position(this.$results, this.$dropdown);\n\n    // Bind events\n\n    var self = this;\n\n    // Bind the container to all of the adapters\n    this._bindAdapters();\n\n    // Register any DOM event handlers\n    this._registerDomEvents();\n\n    // Register any internal event handlers\n    this._registerDataEvents();\n    this._registerSelectionEvents();\n    this._registerDropdownEvents();\n    this._registerResultsEvents();\n    this._registerEvents();\n\n    // Set the initial state\n    this.dataAdapter.current(function (initialData) {\n      self.trigger('selection:update', {\n        data: initialData\n      });\n    });\n\n    // Hide the original select\n    $element.addClass('select2-hidden-accessible');\n    $element.attr('aria-hidden', 'true');\n\n    // Synchronize any monitored attributes\n    this._syncAttributes();\n\n    Utils.StoreData($element[0], 'select2', this);\n\n    // Ensure backwards compatibility with $element.data('select2').\n    $element.data('select2', this);\n  };\n\n  Utils.Extend(Select2, Utils.Observable);\n\n  Select2.prototype._generateId = function ($element) {\n    var id = '';\n\n    if ($element.attr('id') != null) {\n      id = $element.attr('id');\n    } else if ($element.attr('name') != null) {\n      id = $element.attr('name') + '-' + Utils.generateChars(2);\n    } else {\n      id = Utils.generateChars(4);\n    }\n\n    id = id.replace(/(:|\\.|\\[|\\]|,)/g, '');\n    id = 'select2-' + id;\n\n    return id;\n  };\n\n  Select2.prototype._placeContainer = function ($container) {\n    $container.insertAfter(this.$element);\n\n    var width = this._resolveWidth(this.$element, this.options.get('width'));\n\n    if (width != null) {\n      $container.css('width', width);\n    }\n  };\n\n  Select2.prototype._resolveWidth = function ($element, method) {\n    var WIDTH = /^width:(([-+]?([0-9]*\\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;\n\n    if (method == 'resolve') {\n      var styleWidth = this._resolveWidth($element, 'style');\n\n      if (styleWidth != null) {\n        return styleWidth;\n      }\n\n      return this._resolveWidth($element, 'element');\n    }\n\n    if (method == 'element') {\n      var elementWidth = $element.outerWidth(false);\n\n      if (elementWidth <= 0) {\n        return 'auto';\n      }\n\n      return elementWidth + 'px';\n    }\n\n    if (method == 'style') {\n      var style = $element.attr('style');\n\n      if (typeof(style) !== 'string') {\n        return null;\n      }\n\n      var attrs = style.split(';');\n\n      for (var i = 0, l = attrs.length; i < l; i = i + 1) {\n        var attr = attrs[i].replace(/\\s/g, '');\n        var matches = attr.match(WIDTH);\n\n        if (matches !== null && matches.length >= 1) {\n          return matches[1];\n        }\n      }\n\n      return null;\n    }\n\n    if (method == 'computedstyle') {\n      var computedStyle = window.getComputedStyle($element[0]);\n\n      return computedStyle.width;\n    }\n\n    return method;\n  };\n\n  Select2.prototype._bindAdapters = function () {\n    this.dataAdapter.bind(this, this.$container);\n    this.selection.bind(this, this.$container);\n\n    this.dropdown.bind(this, this.$container);\n    this.results.bind(this, this.$container);\n  };\n\n  Select2.prototype._registerDomEvents = function () {\n    var self = this;\n\n    this.$element.on('change.select2', function () {\n      self.dataAdapter.current(function (data) {\n        self.trigger('selection:update', {\n          data: data\n        });\n      });\n    });\n\n    this.$element.on('focus.select2', function (evt) {\n      self.trigger('focus', evt);\n    });\n\n    this._syncA = Utils.bind(this._syncAttributes, this);\n    this._syncS = Utils.bind(this._syncSubtree, this);\n\n    if (this.$element[0].attachEvent) {\n      this.$element[0].attachEvent('onpropertychange', this._syncA);\n    }\n\n    var observer = window.MutationObserver ||\n      window.WebKitMutationObserver ||\n      window.MozMutationObserver\n    ;\n\n    if (observer != null) {\n      this._observer = new observer(function (mutations) {\n        self._syncA();\n        self._syncS(null, mutations);\n      });\n      this._observer.observe(this.$element[0], {\n        attributes: true,\n        childList: true,\n        subtree: false\n      });\n    } else if (this.$element[0].addEventListener) {\n      this.$element[0].addEventListener(\n        'DOMAttrModified',\n        self._syncA,\n        false\n      );\n      this.$element[0].addEventListener(\n        'DOMNodeInserted',\n        self._syncS,\n        false\n      );\n      this.$element[0].addEventListener(\n        'DOMNodeRemoved',\n        self._syncS,\n        false\n      );\n    }\n  };\n\n  Select2.prototype._registerDataEvents = function () {\n    var self = this;\n\n    this.dataAdapter.on('*', function (name, params) {\n      self.trigger(name, params);\n    });\n  };\n\n  Select2.prototype._registerSelectionEvents = function () {\n    var self = this;\n    var nonRelayEvents = ['toggle', 'focus'];\n\n    this.selection.on('toggle', function () {\n      self.toggleDropdown();\n    });\n\n    this.selection.on('focus', function (params) {\n      self.focus(params);\n    });\n\n    this.selection.on('*', function (name, params) {\n      if ($.inArray(name, nonRelayEvents) !== -1) {\n        return;\n      }\n\n      self.trigger(name, params);\n    });\n  };\n\n  Select2.prototype._registerDropdownEvents = function () {\n    var self = this;\n\n    this.dropdown.on('*', function (name, params) {\n      self.trigger(name, params);\n    });\n  };\n\n  Select2.prototype._registerResultsEvents = function () {\n    var self = this;\n\n    this.results.on('*', function (name, params) {\n      self.trigger(name, params);\n    });\n  };\n\n  Select2.prototype._registerEvents = function () {\n    var self = this;\n\n    this.on('open', function () {\n      self.$container.addClass('select2-container--open');\n    });\n\n    this.on('close', function () {\n      self.$container.removeClass('select2-container--open');\n    });\n\n    this.on('enable', function () {\n      self.$container.removeClass('select2-container--disabled');\n    });\n\n    this.on('disable', function () {\n      self.$container.addClass('select2-container--disabled');\n    });\n\n    this.on('blur', function () {\n      self.$container.removeClass('select2-container--focus');\n    });\n\n    this.on('query', function (params) {\n      if (!self.isOpen()) {\n        self.trigger('open', {});\n      }\n\n      this.dataAdapter.query(params, function (data) {\n        self.trigger('results:all', {\n          data: data,\n          query: params\n        });\n      });\n    });\n\n    this.on('query:append', function (params) {\n      this.dataAdapter.query(params, function (data) {\n        self.trigger('results:append', {\n          data: data,\n          query: params\n        });\n      });\n    });\n\n    this.on('keypress', function (evt) {\n      var key = evt.which;\n\n      if (self.isOpen()) {\n        if (key === KEYS.ESC || key === KEYS.TAB ||\n            (key === KEYS.UP && evt.altKey)) {\n          self.close(evt);\n\n          evt.preventDefault();\n        } else if (key === KEYS.ENTER) {\n          self.trigger('results:select', {});\n\n          evt.preventDefault();\n        } else if ((key === KEYS.SPACE && evt.ctrlKey)) {\n          self.trigger('results:toggle', {});\n\n          evt.preventDefault();\n        } else if (key === KEYS.UP) {\n          self.trigger('results:previous', {});\n\n          evt.preventDefault();\n        } else if (key === KEYS.DOWN) {\n          self.trigger('results:next', {});\n\n          evt.preventDefault();\n        }\n      } else {\n        if (key === KEYS.ENTER || key === KEYS.SPACE ||\n            (key === KEYS.DOWN && evt.altKey)) {\n          self.open();\n\n          evt.preventDefault();\n        }\n      }\n    });\n  };\n\n  Select2.prototype._syncAttributes = function () {\n    this.options.set('disabled', this.$element.prop('disabled'));\n\n    if (this.isDisabled()) {\n      if (this.isOpen()) {\n        this.close();\n      }\n\n      this.trigger('disable', {});\n    } else {\n      this.trigger('enable', {});\n    }\n  };\n\n  Select2.prototype._isChangeMutation = function (evt, mutations) {\n    var changed = false;\n    var self = this;\n\n    // Ignore any mutation events raised for elements that aren't options or\n    // optgroups. This handles the case when the select element is destroyed\n    if (\n      evt && evt.target && (\n        evt.target.nodeName !== 'OPTION' && evt.target.nodeName !== 'OPTGROUP'\n      )\n    ) {\n      return;\n    }\n\n    if (!mutations) {\n      // If mutation events aren't supported, then we can only assume that the\n      // change affected the selections\n      changed = true;\n    } else if (mutations.addedNodes && mutations.addedNodes.length > 0) {\n      for (var n = 0; n < mutations.addedNodes.length; n++) {\n        var node = mutations.addedNodes[n];\n\n        if (node.selected) {\n          changed = true;\n        }\n      }\n    } else if (mutations.removedNodes && mutations.removedNodes.length > 0) {\n      changed = true;\n    } else if ($.isArray(mutations)) {\n      $.each(mutations, function(evt, mutation) {\n        if (self._isChangeMutation(evt, mutation)) {\n          // We've found a change mutation.\n          // Let's escape from the loop and continue\n          changed = true;\n          return false;\n        }\n      });\n    }\n    return changed;\n  };\n\n  Select2.prototype._syncSubtree = function (evt, mutations) {\n    var changed = this._isChangeMutation(evt, mutations);\n    var self = this;\n\n    // Only re-pull the data if we think there is a change\n    if (changed) {\n      this.dataAdapter.current(function (currentData) {\n        self.trigger('selection:update', {\n          data: currentData\n        });\n      });\n    }\n  };\n\n  /**\n   * Override the trigger method to automatically trigger pre-events when\n   * there are events that can be prevented.\n   */\n  Select2.prototype.trigger = function (name, args) {\n    var actualTrigger = Select2.__super__.trigger;\n    var preTriggerMap = {\n      'open': 'opening',\n      'close': 'closing',\n      'select': 'selecting',\n      'unselect': 'unselecting',\n      'clear': 'clearing'\n    };\n\n    if (args === undefined) {\n      args = {};\n    }\n\n    if (name in preTriggerMap) {\n      var preTriggerName = preTriggerMap[name];\n      var preTriggerArgs = {\n        prevented: false,\n        name: name,\n        args: args\n      };\n\n      actualTrigger.call(this, preTriggerName, preTriggerArgs);\n\n      if (preTriggerArgs.prevented) {\n        args.prevented = true;\n\n        return;\n      }\n    }\n\n    actualTrigger.call(this, name, args);\n  };\n\n  Select2.prototype.toggleDropdown = function () {\n    if (this.isDisabled()) {\n      return;\n    }\n\n    if (this.isOpen()) {\n      this.close();\n    } else {\n      this.open();\n    }\n  };\n\n  Select2.prototype.open = function () {\n    if (this.isOpen()) {\n      return;\n    }\n\n    if (this.isDisabled()) {\n      return;\n    }\n\n    this.trigger('query', {});\n  };\n\n  Select2.prototype.close = function (evt) {\n    if (!this.isOpen()) {\n      return;\n    }\n\n    this.trigger('close', { originalEvent : evt });\n  };\n\n  /**\n   * Helper method to abstract the \"enabled\" (not \"disabled\") state of this\n   * object.\n   *\n   * @return {true} if the instance is not disabled.\n   * @return {false} if the instance is disabled.\n   */\n  Select2.prototype.isEnabled = function () {\n    return !this.isDisabled();\n  };\n\n  /**\n   * Helper method to abstract the \"disabled\" state of this object.\n   *\n   * @return {true} if the disabled option is true.\n   * @return {false} if the disabled option is false.\n   */\n  Select2.prototype.isDisabled = function () {\n    return this.options.get('disabled');\n  };\n\n  Select2.prototype.isOpen = function () {\n    return this.$container.hasClass('select2-container--open');\n  };\n\n  Select2.prototype.hasFocus = function () {\n    return this.$container.hasClass('select2-container--focus');\n  };\n\n  Select2.prototype.focus = function (data) {\n    // No need to re-trigger focus events if we are already focused\n    if (this.hasFocus()) {\n      return;\n    }\n\n    this.$container.addClass('select2-container--focus');\n    this.trigger('focus', {});\n  };\n\n  Select2.prototype.enable = function (args) {\n    if (this.options.get('debug') && window.console && console.warn) {\n      console.warn(\n        'Select2: The `select2(\"enable\")` method has been deprecated and will' +\n        ' be removed in later Select2 versions. Use $element.prop(\"disabled\")' +\n        ' instead.'\n      );\n    }\n\n    if (args == null || args.length === 0) {\n      args = [true];\n    }\n\n    var disabled = !args[0];\n\n    this.$element.prop('disabled', disabled);\n  };\n\n  Select2.prototype.data = function () {\n    if (this.options.get('debug') &&\n        arguments.length > 0 && window.console && console.warn) {\n      console.warn(\n        'Select2: Data can no longer be set using `select2(\"data\")`. You ' +\n        'should consider setting the value instead using `$element.val()`.'\n      );\n    }\n\n    var data = [];\n\n    this.dataAdapter.current(function (currentData) {\n      data = currentData;\n    });\n\n    return data;\n  };\n\n  Select2.prototype.val = function (args) {\n    if (this.options.get('debug') && window.console && console.warn) {\n      console.warn(\n        'Select2: The `select2(\"val\")` method has been deprecated and will be' +\n        ' removed in later Select2 versions. Use $element.val() instead.'\n      );\n    }\n\n    if (args == null || args.length === 0) {\n      return this.$element.val();\n    }\n\n    var newVal = args[0];\n\n    if ($.isArray(newVal)) {\n      newVal = $.map(newVal, function (obj) {\n        return obj.toString();\n      });\n    }\n\n    this.$element.val(newVal).trigger('input').trigger('change');\n  };\n\n  Select2.prototype.destroy = function () {\n    this.$container.remove();\n\n    if (this.$element[0].detachEvent) {\n      this.$element[0].detachEvent('onpropertychange', this._syncA);\n    }\n\n    if (this._observer != null) {\n      this._observer.disconnect();\n      this._observer = null;\n    } else if (this.$element[0].removeEventListener) {\n      this.$element[0]\n        .removeEventListener('DOMAttrModified', this._syncA, false);\n      this.$element[0]\n        .removeEventListener('DOMNodeInserted', this._syncS, false);\n      this.$element[0]\n        .removeEventListener('DOMNodeRemoved', this._syncS, false);\n    }\n\n    this._syncA = null;\n    this._syncS = null;\n\n    this.$element.off('.select2');\n    this.$element.attr('tabindex',\n    Utils.GetData(this.$element[0], 'old-tabindex'));\n\n    this.$element.removeClass('select2-hidden-accessible');\n    this.$element.attr('aria-hidden', 'false');\n    Utils.RemoveData(this.$element[0]);\n    this.$element.removeData('select2');\n\n    this.dataAdapter.destroy();\n    this.selection.destroy();\n    this.dropdown.destroy();\n    this.results.destroy();\n\n    this.dataAdapter = null;\n    this.selection = null;\n    this.dropdown = null;\n    this.results = null;\n  };\n\n  Select2.prototype.render = function () {\n    var $container = $(\n      '<span class=\"select2 select2-container\">' +\n        '<span class=\"selection\"></span>' +\n        '<span class=\"dropdown-wrapper\" aria-hidden=\"true\"></span>' +\n      '</span>'\n    );\n\n    $container.attr('dir', this.options.get('dir'));\n\n    this.$container = $container;\n\n    this.$container.addClass('select2-container--' + this.options.get('theme'));\n\n    Utils.StoreData($container[0], 'element', this.$element);\n\n    return $container;\n  };\n\n  return Select2;\n});\n\nS2.define('select2/compat/utils',[\n  'jquery'\n], function ($) {\n  function syncCssClasses ($dest, $src, adapter) {\n    var classes, replacements = [], adapted;\n\n    classes = $.trim($dest.attr('class'));\n\n    if (classes) {\n      classes = '' + classes; // for IE which returns object\n\n      $(classes.split(/\\s+/)).each(function () {\n        // Save all Select2 classes\n        if (this.indexOf('select2-') === 0) {\n          replacements.push(this);\n        }\n      });\n    }\n\n    classes = $.trim($src.attr('class'));\n\n    if (classes) {\n      classes = '' + classes; // for IE which returns object\n\n      $(classes.split(/\\s+/)).each(function () {\n        // Only adapt non-Select2 classes\n        if (this.indexOf('select2-') !== 0) {\n          adapted = adapter(this);\n\n          if (adapted != null) {\n            replacements.push(adapted);\n          }\n        }\n      });\n    }\n\n    $dest.attr('class', replacements.join(' '));\n  }\n\n  return {\n    syncCssClasses: syncCssClasses\n  };\n});\n\nS2.define('select2/compat/containerCss',[\n  'jquery',\n  './utils'\n], function ($, CompatUtils) {\n  // No-op CSS adapter that discards all classes by default\n  function _containerAdapter (clazz) {\n    return null;\n  }\n\n  function ContainerCSS () { }\n\n  ContainerCSS.prototype.render = function (decorated) {\n    var $container = decorated.call(this);\n\n    var containerCssClass = this.options.get('containerCssClass') || '';\n\n    if ($.isFunction(containerCssClass)) {\n      containerCssClass = containerCssClass(this.$element);\n    }\n\n    var containerCssAdapter = this.options.get('adaptContainerCssClass');\n    containerCssAdapter = containerCssAdapter || _containerAdapter;\n\n    if (containerCssClass.indexOf(':all:') !== -1) {\n      containerCssClass = containerCssClass.replace(':all:', '');\n\n      var _cssAdapter = containerCssAdapter;\n\n      containerCssAdapter = function (clazz) {\n        var adapted = _cssAdapter(clazz);\n\n        if (adapted != null) {\n          // Append the old one along with the adapted one\n          return adapted + ' ' + clazz;\n        }\n\n        return clazz;\n      };\n    }\n\n    var containerCss = this.options.get('containerCss') || {};\n\n    if ($.isFunction(containerCss)) {\n      containerCss = containerCss(this.$element);\n    }\n\n    CompatUtils.syncCssClasses($container, this.$element, containerCssAdapter);\n\n    $container.css(containerCss);\n    $container.addClass(containerCssClass);\n\n    return $container;\n  };\n\n  return ContainerCSS;\n});\n\nS2.define('select2/compat/dropdownCss',[\n  'jquery',\n  './utils'\n], function ($, CompatUtils) {\n  // No-op CSS adapter that discards all classes by default\n  function _dropdownAdapter (clazz) {\n    return null;\n  }\n\n  function DropdownCSS () { }\n\n  DropdownCSS.prototype.render = function (decorated) {\n    var $dropdown = decorated.call(this);\n\n    var dropdownCssClass = this.options.get('dropdownCssClass') || '';\n\n    if ($.isFunction(dropdownCssClass)) {\n      dropdownCssClass = dropdownCssClass(this.$element);\n    }\n\n    var dropdownCssAdapter = this.options.get('adaptDropdownCssClass');\n    dropdownCssAdapter = dropdownCssAdapter || _dropdownAdapter;\n\n    if (dropdownCssClass.indexOf(':all:') !== -1) {\n      dropdownCssClass = dropdownCssClass.replace(':all:', '');\n\n      var _cssAdapter = dropdownCssAdapter;\n\n      dropdownCssAdapter = function (clazz) {\n        var adapted = _cssAdapter(clazz);\n\n        if (adapted != null) {\n          // Append the old one along with the adapted one\n          return adapted + ' ' + clazz;\n        }\n\n        return clazz;\n      };\n    }\n\n    var dropdownCss = this.options.get('dropdownCss') || {};\n\n    if ($.isFunction(dropdownCss)) {\n      dropdownCss = dropdownCss(this.$element);\n    }\n\n    CompatUtils.syncCssClasses($dropdown, this.$element, dropdownCssAdapter);\n\n    $dropdown.css(dropdownCss);\n    $dropdown.addClass(dropdownCssClass);\n\n    return $dropdown;\n  };\n\n  return DropdownCSS;\n});\n\nS2.define('select2/compat/initSelection',[\n  'jquery'\n], function ($) {\n  function InitSelection (decorated, $element, options) {\n    if (options.get('debug') && window.console && console.warn) {\n      console.warn(\n        'Select2: The `initSelection` option has been deprecated in favor' +\n        ' of a custom data adapter that overrides the `current` method. ' +\n        'This method is now called multiple times instead of a single ' +\n        'time when the instance is initialized. Support will be removed ' +\n        'for the `initSelection` option in future versions of Select2'\n      );\n    }\n\n    this.initSelection = options.get('initSelection');\n    this._isInitialized = false;\n\n    decorated.call(this, $element, options);\n  }\n\n  InitSelection.prototype.current = function (decorated, callback) {\n    var self = this;\n\n    if (this._isInitialized) {\n      decorated.call(this, callback);\n\n      return;\n    }\n\n    this.initSelection.call(null, this.$element, function (data) {\n      self._isInitialized = true;\n\n      if (!$.isArray(data)) {\n        data = [data];\n      }\n\n      callback(data);\n    });\n  };\n\n  return InitSelection;\n});\n\nS2.define('select2/compat/inputData',[\n  'jquery',\n  '../utils'\n], function ($, Utils) {\n  function InputData (decorated, $element, options) {\n    this._currentData = [];\n    this._valueSeparator = options.get('valueSeparator') || ',';\n\n    if ($element.prop('type') === 'hidden') {\n      if (options.get('debug') && console && console.warn) {\n        console.warn(\n          'Select2: Using a hidden input with Select2 is no longer ' +\n          'supported and may stop working in the future. It is recommended ' +\n          'to use a `<select>` element instead.'\n        );\n      }\n    }\n\n    decorated.call(this, $element, options);\n  }\n\n  InputData.prototype.current = function (_, callback) {\n    function getSelected (data, selectedIds) {\n      var selected = [];\n\n      if (data.selected || $.inArray(data.id, selectedIds) !== -1) {\n        data.selected = true;\n        selected.push(data);\n      } else {\n        data.selected = false;\n      }\n\n      if (data.children) {\n        selected.push.apply(selected, getSelected(data.children, selectedIds));\n      }\n\n      return selected;\n    }\n\n    var selected = [];\n\n    for (var d = 0; d < this._currentData.length; d++) {\n      var data = this._currentData[d];\n\n      selected.push.apply(\n        selected,\n        getSelected(\n          data,\n          this.$element.val().split(\n            this._valueSeparator\n          )\n        )\n      );\n    }\n\n    callback(selected);\n  };\n\n  InputData.prototype.select = function (_, data) {\n    if (!this.options.get('multiple')) {\n      this.current(function (allData) {\n        $.map(allData, function (data) {\n          data.selected = false;\n        });\n      });\n\n      this.$element.val(data.id);\n      this.$element.trigger('input').trigger('change');\n    } else {\n      var value = this.$element.val();\n      value += this._valueSeparator + data.id;\n\n      this.$element.val(value);\n      this.$element.trigger('input').trigger('change');\n    }\n  };\n\n  InputData.prototype.unselect = function (_, data) {\n    var self = this;\n\n    data.selected = false;\n\n    this.current(function (allData) {\n      var values = [];\n\n      for (var d = 0; d < allData.length; d++) {\n        var item = allData[d];\n\n        if (data.id == item.id) {\n          continue;\n        }\n\n        values.push(item.id);\n      }\n\n      self.$element.val(values.join(self._valueSeparator));\n      self.$element.trigger('input').trigger('change');\n    });\n  };\n\n  InputData.prototype.query = function (_, params, callback) {\n    var results = [];\n\n    for (var d = 0; d < this._currentData.length; d++) {\n      var data = this._currentData[d];\n\n      var matches = this.matches(params, data);\n\n      if (matches !== null) {\n        results.push(matches);\n      }\n    }\n\n    callback({\n      results: results\n    });\n  };\n\n  InputData.prototype.addOptions = function (_, $options) {\n    var options = $.map($options, function ($option) {\n      return Utils.GetData($option[0], 'data');\n    });\n\n    this._currentData.push.apply(this._currentData, options);\n  };\n\n  return InputData;\n});\n\nS2.define('select2/compat/matcher',[\n  'jquery'\n], function ($) {\n  function oldMatcher (matcher) {\n    function wrappedMatcher (params, data) {\n      var match = $.extend(true, {}, data);\n\n      if (params.term == null || $.trim(params.term) === '') {\n        return match;\n      }\n\n      if (data.children) {\n        for (var c = data.children.length - 1; c >= 0; c--) {\n          var child = data.children[c];\n\n          // Check if the child object matches\n          // The old matcher returned a boolean true or false\n          var doesMatch = matcher(params.term, child.text, child);\n\n          // If the child didn't match, pop it off\n          if (!doesMatch) {\n            match.children.splice(c, 1);\n          }\n        }\n\n        if (match.children.length > 0) {\n          return match;\n        }\n      }\n\n      if (matcher(params.term, data.text, data)) {\n        return match;\n      }\n\n      return null;\n    }\n\n    return wrappedMatcher;\n  }\n\n  return oldMatcher;\n});\n\nS2.define('select2/compat/query',[\n\n], function () {\n  function Query (decorated, $element, options) {\n    if (options.get('debug') && window.console && console.warn) {\n      console.warn(\n        'Select2: The `query` option has been deprecated in favor of a ' +\n        'custom data adapter that overrides the `query` method. Support ' +\n        'will be removed for the `query` option in future versions of ' +\n        'Select2.'\n      );\n    }\n\n    decorated.call(this, $element, options);\n  }\n\n  Query.prototype.query = function (_, params, callback) {\n    params.callback = callback;\n\n    var query = this.options.get('query');\n\n    query.call(null, params);\n  };\n\n  return Query;\n});\n\nS2.define('select2/dropdown/attachContainer',[\n\n], function () {\n  function AttachContainer (decorated, $element, options) {\n    decorated.call(this, $element, options);\n  }\n\n  AttachContainer.prototype.position =\n    function (decorated, $dropdown, $container) {\n    var $dropdownContainer = $container.find('.dropdown-wrapper');\n    $dropdownContainer.append($dropdown);\n\n    $dropdown.addClass('select2-dropdown--below');\n    $container.addClass('select2-container--below');\n  };\n\n  return AttachContainer;\n});\n\nS2.define('select2/dropdown/stopPropagation',[\n\n], function () {\n  function StopPropagation () { }\n\n  StopPropagation.prototype.bind = function (decorated, container, $container) {\n    decorated.call(this, container, $container);\n\n    var stoppedEvents = [\n    'blur',\n    'change',\n    'click',\n    'dblclick',\n    'focus',\n    'focusin',\n    'focusout',\n    'input',\n    'keydown',\n    'keyup',\n    'keypress',\n    'mousedown',\n    'mouseenter',\n    'mouseleave',\n    'mousemove',\n    'mouseover',\n    'mouseup',\n    'search',\n    'touchend',\n    'touchstart'\n    ];\n\n    this.$dropdown.on(stoppedEvents.join(' '), function (evt) {\n      evt.stopPropagation();\n    });\n  };\n\n  return StopPropagation;\n});\n\nS2.define('select2/selection/stopPropagation',[\n\n], function () {\n  function StopPropagation () { }\n\n  StopPropagation.prototype.bind = function (decorated, container, $container) {\n    decorated.call(this, container, $container);\n\n    var stoppedEvents = [\n      'blur',\n      'change',\n      'click',\n      'dblclick',\n      'focus',\n      'focusin',\n      'focusout',\n      'input',\n      'keydown',\n      'keyup',\n      'keypress',\n      'mousedown',\n      'mouseenter',\n      'mouseleave',\n      'mousemove',\n      'mouseover',\n      'mouseup',\n      'search',\n      'touchend',\n      'touchstart'\n    ];\n\n    this.$selection.on(stoppedEvents.join(' '), function (evt) {\n      evt.stopPropagation();\n    });\n  };\n\n  return StopPropagation;\n});\n\n/*!\n * jQuery Mousewheel 3.1.13\n *\n * Copyright jQuery Foundation and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n */\n\n(function (factory) {\n    if ( typeof S2.define === 'function' && S2.define.amd ) {\n        // AMD. Register as an anonymous module.\n        S2.define('jquery-mousewheel',['jquery'], factory);\n    } else if (typeof exports === 'object') {\n        // Node/CommonJS style for Browserify\n        module.exports = factory;\n    } else {\n        // Browser globals\n        factory(jQuery);\n    }\n}(function ($) {\n\n    var toFix  = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'],\n        toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ?\n                    ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],\n        slice  = Array.prototype.slice,\n        nullLowestDeltaTimeout, lowestDelta;\n\n    if ( $.event.fixHooks ) {\n        for ( var i = toFix.length; i; ) {\n            $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks;\n        }\n    }\n\n    var special = $.event.special.mousewheel = {\n        version: '3.1.12',\n\n        setup: function() {\n            if ( this.addEventListener ) {\n                for ( var i = toBind.length; i; ) {\n                    this.addEventListener( toBind[--i], handler, false );\n                }\n            } else {\n                this.onmousewheel = handler;\n            }\n            // Store the line height and page height for this particular element\n            $.data(this, 'mousewheel-line-height', special.getLineHeight(this));\n            $.data(this, 'mousewheel-page-height', special.getPageHeight(this));\n        },\n\n        teardown: function() {\n            if ( this.removeEventListener ) {\n                for ( var i = toBind.length; i; ) {\n                    this.removeEventListener( toBind[--i], handler, false );\n                }\n            } else {\n                this.onmousewheel = null;\n            }\n            // Clean up the data we added to the element\n            $.removeData(this, 'mousewheel-line-height');\n            $.removeData(this, 'mousewheel-page-height');\n        },\n\n        getLineHeight: function(elem) {\n            var $elem = $(elem),\n                $parent = $elem['offsetParent' in $.fn ? 'offsetParent' : 'parent']();\n            if (!$parent.length) {\n                $parent = $('body');\n            }\n            return parseInt($parent.css('fontSize'), 10) || parseInt($elem.css('fontSize'), 10) || 16;\n        },\n\n        getPageHeight: function(elem) {\n            return $(elem).height();\n        },\n\n        settings: {\n            adjustOldDeltas: true, // see shouldAdjustOldDeltas() below\n            normalizeOffset: true  // calls getBoundingClientRect for each event\n        }\n    };\n\n    $.fn.extend({\n        mousewheel: function(fn) {\n            return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel');\n        },\n\n        unmousewheel: function(fn) {\n            return this.unbind('mousewheel', fn);\n        }\n    });\n\n\n    function handler(event) {\n        var orgEvent   = event || window.event,\n            args       = slice.call(arguments, 1),\n            delta      = 0,\n            deltaX     = 0,\n            deltaY     = 0,\n            absDelta   = 0,\n            offsetX    = 0,\n            offsetY    = 0;\n        event = $.event.fix(orgEvent);\n        event.type = 'mousewheel';\n\n        // Old school scrollwheel delta\n        if ( 'detail'      in orgEvent ) { deltaY = orgEvent.detail * -1;      }\n        if ( 'wheelDelta'  in orgEvent ) { deltaY = orgEvent.wheelDelta;       }\n        if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY;      }\n        if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; }\n\n        // Firefox < 17 horizontal scrolling related to DOMMouseScroll event\n        if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {\n            deltaX = deltaY * -1;\n            deltaY = 0;\n        }\n\n        // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy\n        delta = deltaY === 0 ? deltaX : deltaY;\n\n        // New school wheel delta (wheel event)\n        if ( 'deltaY' in orgEvent ) {\n            deltaY = orgEvent.deltaY * -1;\n            delta  = deltaY;\n        }\n        if ( 'deltaX' in orgEvent ) {\n            deltaX = orgEvent.deltaX;\n            if ( deltaY === 0 ) { delta  = deltaX * -1; }\n        }\n\n        // No change actually happened, no reason to go any further\n        if ( deltaY === 0 && deltaX === 0 ) { return; }\n\n        // Need to convert lines and pages to pixels if we aren't already in pixels\n        // There are three delta modes:\n        //   * deltaMode 0 is by pixels, nothing to do\n        //   * deltaMode 1 is by lines\n        //   * deltaMode 2 is by pages\n        if ( orgEvent.deltaMode === 1 ) {\n            var lineHeight = $.data(this, 'mousewheel-line-height');\n            delta  *= lineHeight;\n            deltaY *= lineHeight;\n            deltaX *= lineHeight;\n        } else if ( orgEvent.deltaMode === 2 ) {\n            var pageHeight = $.data(this, 'mousewheel-page-height');\n            delta  *= pageHeight;\n            deltaY *= pageHeight;\n            deltaX *= pageHeight;\n        }\n\n        // Store lowest absolute delta to normalize the delta values\n        absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );\n\n        if ( !lowestDelta || absDelta < lowestDelta ) {\n            lowestDelta = absDelta;\n\n            // Adjust older deltas if necessary\n            if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {\n                lowestDelta /= 40;\n            }\n        }\n\n        // Adjust older deltas if necessary\n        if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {\n            // Divide all the things by 40!\n            delta  /= 40;\n            deltaX /= 40;\n            deltaY /= 40;\n        }\n\n        // Get a whole, normalized value for the deltas\n        delta  = Math[ delta  >= 1 ? 'floor' : 'ceil' ](delta  / lowestDelta);\n        deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);\n        deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);\n\n        // Normalise offsetX and offsetY properties\n        if ( special.settings.normalizeOffset && this.getBoundingClientRect ) {\n            var boundingRect = this.getBoundingClientRect();\n            offsetX = event.clientX - boundingRect.left;\n            offsetY = event.clientY - boundingRect.top;\n        }\n\n        // Add information to the event object\n        event.deltaX = deltaX;\n        event.deltaY = deltaY;\n        event.deltaFactor = lowestDelta;\n        event.offsetX = offsetX;\n        event.offsetY = offsetY;\n        // Go ahead and set deltaMode to 0 since we converted to pixels\n        // Although this is a little odd since we overwrite the deltaX/Y\n        // properties with normalized deltas.\n        event.deltaMode = 0;\n\n        // Add event and delta to the front of the arguments\n        args.unshift(event, delta, deltaX, deltaY);\n\n        // Clearout lowestDelta after sometime to better\n        // handle multiple device types that give different\n        // a different lowestDelta\n        // Ex: trackpad = 3 and mouse wheel = 120\n        if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }\n        nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);\n\n        return ($.event.dispatch || $.event.handle).apply(this, args);\n    }\n\n    function nullLowestDelta() {\n        lowestDelta = null;\n    }\n\n    function shouldAdjustOldDeltas(orgEvent, absDelta) {\n        // If this is an older event and the delta is divisable by 120,\n        // then we are assuming that the browser is treating this as an\n        // older mouse wheel event and that we should divide the deltas\n        // by 40 to try and get a more usable deltaFactor.\n        // Side note, this actually impacts the reported scroll distance\n        // in older browsers and can cause scrolling to be slower than native.\n        // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.\n        return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0;\n    }\n\n}));\n\nS2.define('jquery.select2',[\n  'jquery',\n  'jquery-mousewheel',\n\n  './select2/core',\n  './select2/defaults',\n  './select2/utils'\n], function ($, _, Select2, Defaults, Utils) {\n  if ($.fn.select2 == null) {\n    // All methods that should return the element\n    var thisMethods = ['open', 'close', 'destroy'];\n\n    $.fn.select2 = function (options) {\n      options = options || {};\n\n      if (typeof options === 'object') {\n        this.each(function () {\n          var instanceOptions = $.extend(true, {}, options);\n\n          var instance = new Select2($(this), instanceOptions);\n        });\n\n        return this;\n      } else if (typeof options === 'string') {\n        var ret;\n        var args = Array.prototype.slice.call(arguments, 1);\n\n        this.each(function () {\n          var instance = Utils.GetData(this, 'select2');\n\n          if (instance == null && window.console && console.error) {\n            console.error(\n              'The select2(\\'' + options + '\\') method was called on an ' +\n              'element that is not using Select2.'\n            );\n          }\n\n          ret = instance[options].apply(instance, args);\n        });\n\n        // Check if we should be returning `this`\n        if ($.inArray(options, thisMethods) > -1) {\n          return this;\n        }\n\n        return ret;\n      } else {\n        throw new Error('Invalid arguments for Select2: ' + options);\n      }\n    };\n  }\n\n  if ($.fn.select2.defaults == null) {\n    $.fn.select2.defaults = Defaults;\n  }\n\n  return Select2;\n});\n\n  // Return the AMD loader configuration so it can be used outside of this file\n  return {\n    define: S2.define,\n    require: S2.require\n  };\n}());\n\n  // Autoload the jQuery bindings\n  // We know that all of the modules exist above this, so we're safe\n  var select2 = S2.require('jquery.select2');\n\n  // Hold the AMD module references on the jQuery function that was just loaded\n  // This allows Select2 to use the internal loader outside of this file, such\n  // as in the language files.\n  jQuery.fn.select2.amd = S2;\n\n  // Return the Select2 instance for anyone who is importing it.\n  return select2;\n}));\n"
  },
  {
    "path": "assets/vendor/webui-popover/jquery.webui-popover.css",
    "content": "/*\n *  webui popover plugin  - v1.2.15\n *  A lightWeight popover plugin with jquery ,enchance the  popover plugin of bootstrap with some awesome new features. It works well with bootstrap ,but bootstrap is not necessary!\n *  https://github.com/sandywalker/webui-popover\n *\n *  Made by Sandy Duan\n *  Under MIT License\n */\n.webui-popover-content {\n  display: none;\n}\n.webui-popover-rtl {\n  direction: rtl;\n  text-align: right;\n}\n/*  webui popover  */\n.webui-popover {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 9999;\n  display: none;\n  min-width: 50px;\n  min-height: 32px;\n  padding: 1px;\n  text-align: left;\n  white-space: normal;\n  background-color: #ffffff;\n  background-clip: padding-box;\n  border: 1px solid #cccccc;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  border-radius: 6px;\n  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.webui-popover.top,\n.webui-popover.top-left,\n.webui-popover.top-right {\n  margin-top: -10px;\n}\n.webui-popover.right,\n.webui-popover.right-top,\n.webui-popover.right-bottom {\n  margin-left: 10px;\n}\n.webui-popover.bottom,\n.webui-popover.bottom-left,\n.webui-popover.bottom-right {\n  margin-top: 10px;\n}\n.webui-popover.left,\n.webui-popover.left-top,\n.webui-popover.left-bottom {\n  margin-left: -10px;\n}\n.webui-popover.pop {\n  -webkit-transform: scale(0.8);\n  -o-transform: scale(0.8);\n  transform: scale(0.8);\n  -webkit-transition: transform 0.15s cubic-bezier(0.3, 0, 0, 1.5);\n  -o-transition: transform 0.15s cubic-bezier(0.3, 0, 0, 1.5);\n  transition: transform 0.15s cubic-bezier(0.3, 0, 0, 1.5);\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.webui-popover.pop-out {\n  -webkit-transition-property: \"opacity,transform\";\n  -o-transition-property: \"opacity,transform\";\n  transition-property: \"opacity,transform\";\n  -webkit-transition: 0.15s linear;\n  -o-transition: 0.15s linear;\n  transition: 0.15s linear;\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.webui-popover.fade,\n.webui-popover.fade-out {\n  -webkit-transition: opacity 0.15s linear;\n  -o-transition: opacity 0.15s linear;\n  transition: opacity 0.15s linear;\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.webui-popover.out {\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.webui-popover.in {\n  -webkit-transform: none;\n  -o-transform: none;\n  transform: none;\n  opacity: 1;\n  filter: alpha(opacity=100);\n}\n.webui-popover .webui-popover-content {\n  padding: 9px 14px;\n  overflow: auto;\n  display: block;\n}\n.webui-popover .webui-popover-content > div:first-child {\n  width: 99%;\n}\n.webui-popover-inner .close {\n  font-family: arial;\n  margin: 8px 10px 0 0;\n  float: right;\n  font-size: 16px;\n  font-weight: bold;\n  line-height: 16px;\n  color: #000000;\n  text-shadow: 0 1px 0 #fff;\n  opacity: 0.2;\n  filter: alpha(opacity=20);\n  text-decoration: none;\n}\n.webui-popover-inner .close:hover,\n.webui-popover-inner .close:focus {\n  opacity: 0.5;\n  filter: alpha(opacity=50);\n}\n.webui-popover-inner .close:after {\n  content: \"\\00D7\";\n  width: 0.8em;\n  height: 0.8em;\n  padding: 4px;\n  position: relative;\n}\n.webui-popover-title {\n  padding: 8px 14px;\n  margin: 0;\n  font-size: 14px;\n  font-weight: bold;\n  line-height: 18px;\n  background-color: #ffffff;\n  border-bottom: 1px solid #f2f2f2;\n  border-radius: 5px 5px 0 0;\n}\n.webui-popover-content {\n  padding: 9px 14px;\n  overflow: auto;\n  display: none;\n}\n.webui-popover-inverse {\n  background-color: #333333;\n  color: #eeeeee;\n}\n.webui-popover-inverse .webui-popover-title {\n  background: #333333;\n  border-bottom: 1px solid #3b3b3b;\n  color: #eeeeee;\n}\n.webui-no-padding .webui-popover-content {\n  padding: 0;\n}\n.webui-no-padding .list-group-item {\n  border-right: none;\n  border-left: none;\n}\n.webui-no-padding .list-group-item:first-child {\n  border-top: 0;\n}\n.webui-no-padding .list-group-item:last-child {\n  border-bottom: 0;\n}\n.webui-popover > .webui-arrow,\n.webui-popover > .webui-arrow:after {\n  position: absolute;\n  display: block;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.webui-popover > .webui-arrow {\n  border-width: 11px;\n}\n.webui-popover > .webui-arrow:after {\n  border-width: 10px;\n  content: \"\";\n}\n.webui-popover.top > .webui-arrow,\n.webui-popover.top-right > .webui-arrow,\n.webui-popover.top-left > .webui-arrow {\n  bottom: -11px;\n  left: 50%;\n  margin-left: -11px;\n  border-top-color: #999999;\n  border-top-color: rgba(0, 0, 0, 0.25);\n  border-bottom-width: 0;\n}\n.webui-popover.top > .webui-arrow:after,\n.webui-popover.top-right > .webui-arrow:after,\n.webui-popover.top-left > .webui-arrow:after {\n  content: \" \";\n  bottom: 1px;\n  margin-left: -10px;\n  border-top-color: #ffffff;\n  border-bottom-width: 0;\n}\n.webui-popover.right > .webui-arrow,\n.webui-popover.right-top > .webui-arrow,\n.webui-popover.right-bottom > .webui-arrow {\n  top: 50%;\n  left: -11px;\n  margin-top: -11px;\n  border-left-width: 0;\n  border-right-color: #999999;\n  border-right-color: rgba(0, 0, 0, 0.25);\n}\n.webui-popover.right > .webui-arrow:after,\n.webui-popover.right-top > .webui-arrow:after,\n.webui-popover.right-bottom > .webui-arrow:after {\n  content: \" \";\n  left: 1px;\n  bottom: -10px;\n  border-left-width: 0;\n  border-right-color: #ffffff;\n}\n.webui-popover.bottom > .webui-arrow,\n.webui-popover.bottom-right > .webui-arrow,\n.webui-popover.bottom-left > .webui-arrow {\n  top: -11px;\n  left: 50%;\n  margin-left: -11px;\n  border-bottom-color: #999999;\n  border-bottom-color: rgba(0, 0, 0, 0.25);\n  border-top-width: 0;\n}\n.webui-popover.bottom > .webui-arrow:after,\n.webui-popover.bottom-right > .webui-arrow:after,\n.webui-popover.bottom-left > .webui-arrow:after {\n  content: \" \";\n  top: 1px;\n  margin-left: -10px;\n  border-bottom-color: #ffffff;\n  border-top-width: 0;\n}\n.webui-popover.left > .webui-arrow,\n.webui-popover.left-top > .webui-arrow,\n.webui-popover.left-bottom > .webui-arrow {\n  top: 50%;\n  right: -11px;\n  margin-top: -11px;\n  border-right-width: 0;\n  border-left-color: #999999;\n  border-left-color: rgba(0, 0, 0, 0.25);\n}\n.webui-popover.left > .webui-arrow:after,\n.webui-popover.left-top > .webui-arrow:after,\n.webui-popover.left-bottom > .webui-arrow:after {\n  content: \" \";\n  right: 1px;\n  border-right-width: 0;\n  border-left-color: #ffffff;\n  bottom: -10px;\n}\n.webui-popover-inverse.top > .webui-arrow,\n.webui-popover-inverse.top-left > .webui-arrow,\n.webui-popover-inverse.top-right > .webui-arrow,\n.webui-popover-inverse.top > .webui-arrow:after,\n.webui-popover-inverse.top-left > .webui-arrow:after,\n.webui-popover-inverse.top-right > .webui-arrow:after {\n  border-top-color: #333333;\n}\n.webui-popover-inverse.right > .webui-arrow,\n.webui-popover-inverse.right-top > .webui-arrow,\n.webui-popover-inverse.right-bottom > .webui-arrow,\n.webui-popover-inverse.right > .webui-arrow:after,\n.webui-popover-inverse.right-top > .webui-arrow:after,\n.webui-popover-inverse.right-bottom > .webui-arrow:after {\n  border-right-color: #333333;\n}\n.webui-popover-inverse.bottom > .webui-arrow,\n.webui-popover-inverse.bottom-left > .webui-arrow,\n.webui-popover-inverse.bottom-right > .webui-arrow,\n.webui-popover-inverse.bottom > .webui-arrow:after,\n.webui-popover-inverse.bottom-left > .webui-arrow:after,\n.webui-popover-inverse.bottom-right > .webui-arrow:after {\n  border-bottom-color: #333333;\n}\n.webui-popover-inverse.left > .webui-arrow,\n.webui-popover-inverse.left-top > .webui-arrow,\n.webui-popover-inverse.left-bottom > .webui-arrow,\n.webui-popover-inverse.left > .webui-arrow:after,\n.webui-popover-inverse.left-top > .webui-arrow:after,\n.webui-popover-inverse.left-bottom > .webui-arrow:after {\n  border-left-color: #333333;\n}\n.webui-popover i.icon-refresh:before {\n  content: \"\";\n}\n.webui-popover i.icon-refresh {\n  display: block;\n  width: 30px;\n  height: 30px;\n  font-size: 20px;\n  top: 50%;\n  left: 50%;\n  position: absolute;\n  margin-left: -15px;\n  margin-right: -15px;\n  background: url(../img/loading.gif) no-repeat;\n}\n@-webkit-keyframes rotate {\n  100% {\n    -webkit-transform: rotate(360deg);\n  }\n}\n@keyframes rotate {\n  100% {\n    transform: rotate(360deg);\n  }\n}\n.webui-popover-backdrop {\n  background-color: rgba(0, 0, 0, 0.65);\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  top: 0;\n  left: 0;\n  z-index: 9998;\n}\n.webui-popover .dropdown-menu {\n  display: block;\n  position: relative;\n  top: 0;\n  border: none;\n  box-shadow: none;\n  float: none;\n}\n"
  },
  {
    "path": "assets/vendor/webui-popover/jquery.webui-popover.js",
    "content": "/*\n *  webui popover plugin  - v1.2.15\n *  A lightWeight popover plugin with jquery ,enchance the  popover plugin of bootstrap with some awesome new features. It works well with bootstrap ,but bootstrap is not necessary!\n *  https://github.com/sandywalker/webui-popover\n *\n *  Made by Sandy Duan\n *  Under MIT License\n */\n(function(window, document, undefined) {\n    'use strict';\n    (function(factory) {\n        if (typeof define === 'function' && define.amd) {\n            // Register as an anonymous AMD module.\n            define(['jquery'], factory);\n        } else if (typeof exports === 'object') {\n            // Node/CommonJS\n            module.exports = factory(require('jquery'));\n        } else {\n            // Browser globals\n            factory(window.jQuery);\n        }\n    }(function($) {\n\t\tvar closeLabel = 'Close';\n\t\tif ( 'undefined' !== typeof LLMS && 'undefined' !== typeof LLMS.l10n && 'undefined' !== typeof LLMS.l10n.translate ) {\n\t\t\tcloseLabel = LLMS.l10n.translate( 'Close' );\n\t\t}\n\n\t\t// Create the defaults once\n        var pluginName = 'webuiPopover';\n        var pluginClass = 'webui-popover';\n        var pluginType = 'webui.popover';\n        var defaults = {\n            placement: 'auto',\n            container: null,\n            width: 'auto',\n            height: 'auto',\n            trigger: 'click', //hover,click,sticky,manual\n            style: '',\n            selector: false, // jQuery selector, if a selector is provided, popover objects will be delegated to the specified.\n            delay: {\n                show: null,\n                hide: 300\n            },\n            async: {\n                type: 'GET',\n                before: null, //function(that, xhr){}\n                success: null, //function(that, xhr){}\n                error: null //function(that, xhr, data){}\n            },\n            cache: true,\n            multi: false,\n            arrow: true,\n            title: '',\n            content: '',\n            closeable: false,\n            padding: true,\n            url: '',\n            type: 'html',\n            direction: '', // ltr,rtl\n            animation: null,\n            template: '<div class=\"webui-popover\">' +\n                '<div class=\"webui-arrow\"></div>' +\n                '<div class=\"webui-popover-inner\">' +\n                '<a href=\"#\" aria-label=\"' + closeLabel + '\" class=\"close\"></a>' +\n                '<h3 class=\"webui-popover-title\"></h3>' +\n                '<div class=\"webui-popover-content\"><i class=\"icon-refresh\"></i> <p>&nbsp;</p></div>' +\n                '</div>' +\n                '</div>',\n            backdrop: false,\n            dismissible: true,\n            onShow: null,\n            onHide: null,\n            abortXHR: true,\n            autoHide: false,\n            offsetTop: 0,\n            offsetLeft: 0,\n            iframeOptions: {\n                frameborder: '0',\n                allowtransparency: 'true',\n                id: '',\n                name: '',\n                scrolling: '',\n                onload: '',\n                height: '',\n                width: ''\n            },\n            hideEmpty: false\n        };\n\n        var rtlClass = pluginClass + '-rtl';\n        var _srcElements = [];\n        var backdrop = $('<div class=\"webui-popover-backdrop\"></div>');\n        var _globalIdSeed = 0;\n        var _isBodyEventHandled = false;\n        var _offsetOut = -2000; // the value offset  out of the screen\n        var $document = $(document);\n\n        var toNumber = function(numeric, fallback) {\n            return isNaN(numeric) ? (fallback || 0) : Number(numeric);\n        };\n\n        var getPopFromElement = function($element) {\n            return $element.data('plugin_' + pluginName);\n        };\n\n        var hideAllPop = function() {\n            var pop = null;\n            for (var i = 0; i < _srcElements.length; i++) {\n                pop = getPopFromElement(_srcElements[i]);\n                if (pop) {\n                    pop.hide(true);\n                }\n            }\n            $document.trigger('hiddenAll.' + pluginType);\n        };\n\n        var hideOtherPops = function(currentPop) {\n            var pop = null;\n            for (var i = 0; i < _srcElements.length; i++) {\n                pop = getPopFromElement(_srcElements[i]);\n                if (pop && pop.id !== currentPop.id) {\n                    pop.hide(true);\n                }\n            }\n            $document.trigger('hiddenAll.' + pluginType);\n        };\n\n        var isMobile = ('ontouchstart' in document.documentElement) && (/Mobi/.test(navigator.userAgent));\n\n        var pointerEventToXY = function(e) {\n            var out = {\n                x: 0,\n                y: 0\n            };\n            if (e.type === 'touchstart' || e.type === 'touchmove' || e.type === 'touchend' || e.type === 'touchcancel') {\n                var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];\n                out.x = touch.pageX;\n                out.y = touch.pageY;\n            } else if (e.type === 'mousedown' || e.type === 'mouseup' || e.type === 'click') {\n                out.x = e.pageX;\n                out.y = e.pageY;\n            }\n            return out;\n        };\n\n\n\n        // The actual plugin constructor\n        function WebuiPopover(element, options) {\n            this.$element = $(element);\n            if (options) {\n                if ($.type(options.delay) === 'string' || $.type(options.delay) === 'number') {\n                    options.delay = {\n                        show: options.delay,\n                        hide: options.delay\n                    }; // bc break fix\n                }\n            }\n            this.options = $.extend({}, defaults, options);\n            this._defaults = defaults;\n            this._name = pluginName;\n            this._targetclick = false;\n            this.init();\n            _srcElements.push(this.$element);\n            return this;\n\n        }\n\n        WebuiPopover.prototype = {\n            //init webui popover\n            init: function() {\n                if (this.$element[0] instanceof document.constructor && !this.options.selector) {\n                    throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!');\n                }\n\n                if (this.getTrigger() !== 'manual') {\n                    //init the event handlers\n                    if (this.getTrigger() === 'click' || isMobile) {\n                        this.$element.off('click touchend', this.options.selector).on('click touchend', this.options.selector, $.proxy(this.toggle, this));\n                    } else if (this.getTrigger() === 'hover') {\n                        this.$element\n                            .off('mouseenter mouseleave click', this.options.selector)\n                            .on('mouseenter', this.options.selector, $.proxy(this.mouseenterHandler, this))\n                            .on('mouseleave', this.options.selector, $.proxy(this.mouseleaveHandler, this));\n                    }\n                }\n                this._poped = false;\n                this._inited = true;\n                this._opened = false;\n                this._idSeed = _globalIdSeed;\n                this.id = pluginName + this._idSeed;\n                // normalize container\n                this.options.container = $(this.options.container || document.body).first();\n\n                if (this.options.backdrop) {\n                    backdrop.appendTo(this.options.container).hide();\n                }\n                _globalIdSeed++;\n                if (this.getTrigger() === 'sticky') {\n                    this.show();\n                }\n\n                if (this.options.selector) {\n                    this._options = $.extend({}, this.options, {\n                        selector: ''\n                    });\n                }\n\n            },\n            /* api methods and actions */\n            destroy: function() {\n                var index = -1;\n\n                for (var i = 0; i < _srcElements.length; i++) {\n                    if (_srcElements[i] === this.$element) {\n                        index = i;\n                        break;\n                    }\n                }\n\n                _srcElements.splice(index, 1);\n\n\n                this.hide();\n                this.$element.data('plugin_' + pluginName, null);\n                if (this.getTrigger() === 'click') {\n                    this.$element.off('click');\n                } else if (this.getTrigger() === 'hover') {\n                    this.$element.off('mouseenter mouseleave');\n                }\n                if (this.$target) {\n                    this.$target.remove();\n                }\n            },\n            getDelegateOptions: function() {\n                var options = {};\n\n                this._options && $.each(this._options, function(key, value) {\n                    if (defaults[key] !== value) {\n                        options[key] = value;\n                    }\n                });\n                return options;\n            },\n            /*\n                param: force    boolean value, if value is true then force hide the popover\n                param: event    dom event,\n            */\n            hide: function(force, event) {\n\n                if (!force && this.getTrigger() === 'sticky') {\n                    return;\n                }\n                if (!this._opened) {\n                    return;\n                }\n                if (event) {\n                    event.preventDefault();\n                    event.stopPropagation();\n                }\n\n                if (this.xhr && this.options.abortXHR === true) {\n                    this.xhr.abort();\n                    this.xhr = null;\n                }\n\n\n                var e = $.Event('hide.' + pluginType);\n                this.$element.trigger(e, [this.$target]);\n                if (this.$target) {\n                    this.$target.removeClass('in').addClass(this.getHideAnimation());\n                    var that = this;\n                    setTimeout(function() {\n                        that.$target.hide();\n                        if (!that.getCache()) {\n                            that.$target.remove();\n                        }\n                    }, that.getHideDelay());\n                }\n                if (this.options.backdrop) {\n                    backdrop.hide();\n                }\n                this._opened = false;\n                this.$element.trigger('hidden.' + pluginType, [this.$target]);\n\n                if (this.options.onHide) {\n                    this.options.onHide(this.$target);\n                }\n\n            },\n            resetAutoHide: function() {\n                var that = this;\n                var autoHide = that.getAutoHide();\n                if (autoHide) {\n                    if (that.autoHideHandler) {\n                        clearTimeout(that.autoHideHandler);\n                    }\n                    that.autoHideHandler = setTimeout(function() {\n                        that.hide();\n                    }, autoHide);\n                }\n            },\n            delegate: function(eventTarget) {\n                var self = $(eventTarget).data('plugin_' + pluginName);\n                if (!self) {\n                    self = new WebuiPopover(eventTarget, this.getDelegateOptions());\n                    $(eventTarget).data('plugin_' + pluginName, self);\n                }\n                return self;\n            },\n            toggle: function(e) {\n                var self = this;\n                if (e) {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    if (this.options.selector) {\n                        self = this.delegate(e.currentTarget);\n                    }\n                }\n                self[self.getTarget().hasClass('in') ? 'hide' : 'show']();\n            },\n            hideAll: function() {\n                hideAllPop();\n            },\n            hideOthers: function() {\n                hideOtherPops(this);\n            },\n            /*core method ,show popover */\n            show: function() {\n                if (this._opened) {\n                    return;\n                }\n                //removeAllTargets();\n                var\n                    $target = this.getTarget().removeClass().addClass(pluginClass).addClass(this._customTargetClass);\n                if (!this.options.multi) {\n                    this.hideOthers();\n                }\n\n                // use cache by default, if not cache setted  , reInit the contents\n                if (!this.getCache() || !this._poped || this.content === '') {\n                    this.content = '';\n                    this.setTitle(this.getTitle());\n                    if (!this.options.closeable) {\n                        $target.find('.close').off('click').remove();\n                    }\n                    if (!this.isAsync()) {\n                        this.setContent(this.getContent());\n                    } else {\n                        this.setContentASync(this.options.content);\n                    }\n\n                    if (this.canEmptyHide() && this.content === '') {\n                        return;\n                    }\n                    $target.show();\n                }\n\n                this.displayContent();\n\n                if (this.options.onShow) {\n                    this.options.onShow($target);\n                }\n\n                this.bindBodyEvents();\n                if (this.options.backdrop) {\n                    backdrop.show();\n                }\n                this._opened = true;\n                this.resetAutoHide();\n            },\n            displayContent: function() {\n                var\n                //element postion\n                    elementPos = this.getElementPosition(),\n                    //target postion\n                    $target = this.getTarget().removeClass().addClass(pluginClass).addClass(this._customTargetClass),\n                    //target content\n                    $targetContent = this.getContentElement(),\n                    //target Width\n                    targetWidth = $target[0].offsetWidth,\n                    //target Height\n                    targetHeight = $target[0].offsetHeight,\n                    //placement\n                    placement = 'bottom',\n                    e = $.Event('show.' + pluginType);\n\n                if (this.canEmptyHide()) {\n\n                    var content = $targetContent.children().html();\n                    if (content !== null && content.trim().length === 0) {\n                        return;\n                    }\n                }\n\n                //if (this.hasContent()){\n                this.$element.trigger(e, [$target]);\n                //}\n                // support width as data attribute\n                var optWidth = this.$element.data('width') || this.options.width;\n                if (optWidth === '') {\n                    optWidth = this._defaults.width;\n                }\n\n                if (optWidth !== 'auto') {\n                    $target.width(optWidth);\n                }\n\n                // support height as data attribute\n                var optHeight = this.$element.data('height') || this.options.height;\n                if (optHeight === '') {\n                    optHeight = this._defaults.height;\n                }\n\n                if (optHeight !== 'auto') {\n                    $targetContent.height(optHeight);\n                }\n\n                if (this.options.style) {\n                    this.$target.addClass(pluginClass + '-' + this.options.style);\n                }\n\n                //check rtl\n                if (this.options.direction === 'rtl' && !$targetContent.hasClass(rtlClass)) {\n                    $targetContent.addClass(rtlClass);\n                }\n\n                //init the popover and insert into the document body\n                if (!this.options.arrow) {\n                    $target.find('.webui-arrow').remove();\n                }\n                $target.detach().css({\n                    top: _offsetOut,\n                    left: _offsetOut,\n                    display: 'block'\n                });\n\n                if (this.getAnimation()) {\n                    $target.addClass(this.getAnimation());\n                }\n                $target.appendTo(this.options.container);\n\n\n                placement = this.getPlacement(elementPos);\n\n                //This line is just for compatible with knockout custom binding\n                this.$element.trigger('added.' + pluginType);\n\n                this.initTargetEvents();\n\n                if (!this.options.padding) {\n                    if (this.options.height !== 'auto') {\n                        $targetContent.css('height', $targetContent.outerHeight());\n                    }\n                    this.$target.addClass('webui-no-padding');\n                }\n                targetWidth = $target[0].offsetWidth;\n                targetHeight = $target[0].offsetHeight;\n\n                var postionInfo = this.getTargetPositin(elementPos, placement, targetWidth, targetHeight);\n\n                this.$target.css(postionInfo.position).addClass(placement).addClass('in');\n\n                if (this.options.type === 'iframe') {\n                    var $iframe = $target.find('iframe');\n                    var iframeWidth = $target.width();\n                    var iframeHeight = $iframe.parent().height();\n\n                    if (this.options.iframeOptions.width !== '' && this.options.iframeOptions.width !== 'auto') {\n                        iframeWidth = this.options.iframeOptions.width;\n                    }\n\n                    if (this.options.iframeOptions.height !== '' && this.options.iframeOptions.height !== 'auto') {\n                        iframeHeight = this.options.iframeOptions.height;\n                    }\n\n                    $iframe.width(iframeWidth).height(iframeHeight);\n                }\n\n                if (!this.options.arrow) {\n                    this.$target.css({\n                        'margin': 0\n                    });\n                }\n                if (this.options.arrow) {\n                    var $arrow = this.$target.find('.webui-arrow');\n                    $arrow.removeAttr('style');\n\n                    //prevent arrow change by content size\n                    if (placement === 'left' || placement === 'right') {\n                        $arrow.css({\n                            top: this.$target.height() / 2\n                        });\n                    } else if (placement === 'top' || placement === 'bottom') {\n                        $arrow.css({\n                            left: this.$target.width() / 2\n                        });\n                    }\n\n                    if (postionInfo.arrowOffset) {\n                        //hide the arrow if offset is negative\n                        if (postionInfo.arrowOffset.left === -1 || postionInfo.arrowOffset.top === -1) {\n                            $arrow.hide();\n                        } else {\n                            $arrow.css(postionInfo.arrowOffset);\n                        }\n                    }\n\n                }\n                this._poped = true;\n                this.$element.trigger('shown.' + pluginType, [this.$target]);\n            },\n\n            isTargetLoaded: function() {\n                return this.getTarget().find('i.glyphicon-refresh').length === 0;\n            },\n\n            /*getter setters */\n            getTriggerElement: function() {\n                return this.$element;\n            },\n            getTarget: function() {\n                if (!this.$target) {\n                    var id = pluginName + this._idSeed;\n                    this.$target = $(this.options.template)\n                        .attr('id', id);\n                    this._customTargetClass = this.$target.attr('class') !== pluginClass ? this.$target.attr('class') : null;\n                    this.getTriggerElement().attr('data-target', id);\n                }\n                if (!this.$target.data('trigger-element')) {\n                    this.$target.data('trigger-element', this.getTriggerElement());\n                }\n                return this.$target;\n            },\n            removeTarget: function() {\n                this.$target.remove();\n                this.$target = null;\n                this.$contentElement = null;\n            },\n            getTitleElement: function() {\n                return this.getTarget().find('.' + pluginClass + '-title');\n            },\n            getContentElement: function() {\n                if (!this.$contentElement) {\n                    this.$contentElement = this.getTarget().find('.' + pluginClass + '-content');\n                }\n                return this.$contentElement;\n            },\n            getTitle: function() {\n                return this.$element.attr('data-title') || this.options.title || this.$element.attr('title');\n            },\n            getUrl: function() {\n                return this.$element.attr('data-url') || this.options.url;\n            },\n            getAutoHide: function() {\n                return this.$element.attr('data-auto-hide') || this.options.autoHide;\n            },\n            getOffsetTop: function() {\n                return toNumber(this.$element.attr('data-offset-top')) || this.options.offsetTop;\n            },\n            getOffsetLeft: function() {\n                return toNumber(this.$element.attr('data-offset-left')) || this.options.offsetLeft;\n            },\n            getCache: function() {\n                var dataAttr = this.$element.attr('data-cache');\n                if (typeof(dataAttr) !== 'undefined') {\n                    switch (dataAttr.toLowerCase()) {\n                        case 'true':\n                        case 'yes':\n                        case '1':\n                            return true;\n                        case 'false':\n                        case 'no':\n                        case '0':\n                            return false;\n                    }\n                }\n                return this.options.cache;\n            },\n            getTrigger: function() {\n                return this.$element.attr('data-trigger') || this.options.trigger;\n            },\n            getDelayShow: function() {\n                var dataAttr = this.$element.attr('data-delay-show');\n                if (typeof(dataAttr) !== 'undefined') {\n                    return dataAttr;\n                }\n                return this.options.delay.show === 0 ? 0 : this.options.delay.show || 100;\n            },\n            getHideDelay: function() {\n                var dataAttr = this.$element.attr('data-delay-hide');\n                if (typeof(dataAttr) !== 'undefined') {\n                    return dataAttr;\n                }\n                return this.options.delay.hide === 0 ? 0 : this.options.delay.hide || 100;\n            },\n            getAnimation: function() {\n                var dataAttr = this.$element.attr('data-animation');\n                return dataAttr || this.options.animation;\n            },\n            getHideAnimation: function() {\n                var ani = this.getAnimation();\n                return ani ? ani + '-out' : 'out';\n            },\n            setTitle: function(title) {\n                var $titleEl = this.getTitleElement();\n                if (title) {\n                    //check rtl\n                    if (this.options.direction === 'rtl' && !$titleEl.hasClass(rtlClass)) {\n                        $titleEl.addClass(rtlClass);\n                    }\n                    $titleEl.html(title);\n                } else {\n                    $titleEl.remove();\n                }\n            },\n            hasContent: function() {\n                return this.getContent();\n            },\n            canEmptyHide: function() {\n                return this.options.hideEmpty && this.options.type === 'html';\n            },\n            getIframe: function() {\n                var $iframe = $('<iframe></iframe>').attr('src', this.getUrl());\n                var self = this;\n                $.each(this._defaults.iframeOptions, function(opt) {\n                    if (typeof self.options.iframeOptions[opt] !== 'undefined') {\n                        $iframe.attr(opt, self.options.iframeOptions[opt]);\n                    }\n                });\n\n                return $iframe;\n            },\n            getContent: function() {\n                if (this.getUrl()) {\n                    switch (this.options.type) {\n                        case 'iframe':\n                            this.content = this.getIframe();\n                            break;\n                        case 'html':\n                            try {\n                                this.content = $(this.getUrl());\n                                if (!this.content.is(':visible')) {\n                                    this.content.show();\n                                }\n                            } catch (error) {\n                                throw new Error('Unable to get popover content. Invalid selector specified.');\n                            }\n                            break;\n                    }\n                } else if (!this.content) {\n                    var content = '';\n                    if ($.isFunction(this.options.content)) {\n                        content = this.options.content.apply(this.$element[0], [this]);\n                    } else {\n                        content = this.options.content;\n                    }\n                    this.content = this.$element.attr('data-content') || content;\n                    if (!this.content) {\n                        var $next = this.$element.next();\n\n                        if ($next && $next.hasClass(pluginClass + '-content')) {\n                            this.content = $next;\n                        }\n                    }\n                }\n                return this.content;\n            },\n            setContent: function(content) {\n                var $target = this.getTarget();\n                var $ct = this.getContentElement();\n                if (typeof content === 'string') {\n                    $ct.html(content);\n                } else if (content instanceof $) {\n                    $ct.html('');\n                    //Don't want to clone too many times.\n                    if (!this.options.cache) {\n                        content.clone(true, true).removeClass(pluginClass + '-content').appendTo($ct);\n                    } else {\n                        content.removeClass(pluginClass + '-content').appendTo($ct);\n                    }\n                }\n                this.$target = $target;\n            },\n            isAsync: function() {\n                return this.options.type === 'async';\n            },\n            setContentASync: function(content) {\n                var that = this;\n                if (this.xhr) {\n                    return;\n                }\n                this.xhr = $.ajax({\n                    url: this.getUrl(),\n                    type: this.options.async.type,\n                    cache: this.getCache(),\n                    beforeSend: function(xhr) {\n                        if (that.options.async.before) {\n                            that.options.async.before(that, xhr);\n                        }\n                    },\n                    success: function(data) {\n                        that.bindBodyEvents();\n                        if (content && $.isFunction(content)) {\n                            that.content = content.apply(that.$element[0], [data]);\n                        } else {\n                            that.content = data;\n                        }\n                        that.setContent(that.content);\n                        var $targetContent = that.getContentElement();\n                        $targetContent.removeAttr('style');\n                        that.displayContent();\n                        if (that.options.async.success) {\n                            that.options.async.success(that, data);\n                        }\n                    },\n                    complete: function() {\n                        that.xhr = null;\n                    },\n                    error: function(xhr, data) {\n                        if (that.options.async.error) {\n                            that.options.async.error(that, xhr, data);\n                        }\n                    }\n                });\n            },\n\n            bindBodyEvents: function() {\n                if (_isBodyEventHandled) {\n                    return;\n                }\n                if (this.options.dismissible && this.getTrigger() === 'click') {\n                    $document.off('keyup.webui-popover').on('keyup.webui-popover', $.proxy(this.escapeHandler, this));\n                    $document.off('click.webui-popover touchend.webui-popover')\n                        .on('click.webui-popover touchend.webui-popover', $.proxy(this.bodyClickHandler, this));\n                } else if (this.getTrigger() === 'hover') {\n                    $document.off('touchend.webui-popover')\n                        .on('touchend.webui-popover', $.proxy(this.bodyClickHandler, this));\n                }\n            },\n\n            /* event handlers */\n            mouseenterHandler: function(e) {\n                var self = this;\n\n                if (e && this.options.selector) {\n                    self = this.delegate(e.currentTarget);\n                }\n\n                if (self._timeout) {\n                    clearTimeout(self._timeout);\n                }\n                self._enterTimeout = setTimeout(function() {\n                    if (!self.getTarget().is(':visible')) {\n                        self.show();\n                    }\n                }, this.getDelayShow());\n            },\n            mouseleaveHandler: function() {\n                var self = this;\n                clearTimeout(self._enterTimeout);\n                //key point, set the _timeout  then use clearTimeout when mouse leave\n                self._timeout = setTimeout(function() {\n                    self.hide();\n                }, this.getHideDelay());\n            },\n            escapeHandler: function(e) {\n                if (e.keyCode === 27) {\n                    this.hideAll();\n                }\n            },\n\n            bodyClickHandler: function(e) {\n                _isBodyEventHandled = true;\n                var canHide = true;\n                for (var i = 0; i < _srcElements.length; i++) {\n                    var pop = getPopFromElement(_srcElements[i]);\n                    if (pop && pop._opened) {\n                        var offset = pop.getTarget().offset();\n                        var popX1 = offset.left;\n                        var popY1 = offset.top;\n                        var popX2 = offset.left + pop.getTarget().width();\n                        var popY2 = offset.top + pop.getTarget().height();\n                        var pt = pointerEventToXY(e);\n                        var inPop = pt.x >= popX1 && pt.x <= popX2 && pt.y >= popY1 && pt.y <= popY2;\n                        if (inPop) {\n                            canHide = false;\n                            break;\n                        }\n                    }\n                }\n                if (canHide) {\n                    hideAllPop();\n                }\n            },\n\n            /*\n            targetClickHandler: function() {\n                this._targetclick = true;\n            },\n            */\n\n            //reset and init the target events;\n            initTargetEvents: function() {\n                if (this.getTrigger() === 'hover') {\n                    this.$target\n                        .off('mouseenter mouseleave')\n                        .on('mouseenter', $.proxy(this.mouseenterHandler, this))\n                        .on('mouseleave', $.proxy(this.mouseleaveHandler, this));\n                }\n                this.$target.find('.close').off('click').on('click', $.proxy(this.hide, this, true));\n                //this.$target.off('click.webui-popover').on('click.webui-popover', $.proxy(this.targetClickHandler, this));\n            },\n            /* utils methods */\n            //caculate placement of the popover\n            getPlacement: function(pos) {\n                var\n                    placement,\n                    container = this.options.container,\n                    clientWidth = container.innerWidth(),\n                    clientHeight = container.innerHeight(),\n                    scrollTop = container.scrollTop(),\n                    scrollLeft = container.scrollLeft(),\n                    pageX = Math.max(0, pos.left - scrollLeft),\n                    pageY = Math.max(0, pos.top - scrollTop);\n                //arrowSize = 20;\n\n                //if placement equals auto，caculate the placement by element information;\n                if (typeof(this.options.placement) === 'function') {\n                    placement = this.options.placement.call(this, this.getTarget()[0], this.$element[0]);\n                } else {\n                    placement = this.$element.data('placement') || this.options.placement;\n                }\n\n                var isH = placement === 'horizontal';\n                var isV = placement === 'vertical';\n                var detect = placement === 'auto' || isH || isV;\n\n                if (detect) {\n                    if (pageX < clientWidth / 3) {\n                        if (pageY < clientHeight / 3) {\n                            placement = isH ? 'right-bottom' : 'bottom-right';\n                        } else if (pageY < clientHeight * 2 / 3) {\n                            if (isV) {\n                                placement = pageY <= clientHeight / 2 ? 'bottom-right' : 'top-right';\n                            } else {\n                                placement = 'right';\n                            }\n                        } else {\n                            placement = isH ? 'right-top' : 'top-right';\n                        }\n                        //placement= pageY>targetHeight+arrowSize?'top-right':'bottom-right';\n                    } else if (pageX < clientWidth * 2 / 3) {\n                        if (pageY < clientHeight / 3) {\n                            if (isH) {\n                                placement = pageX <= clientWidth / 2 ? 'right-bottom' : 'left-bottom';\n                            } else {\n                                placement = 'bottom';\n                            }\n                        } else if (pageY < clientHeight * 2 / 3) {\n                            if (isH) {\n                                placement = pageX <= clientWidth / 2 ? 'right' : 'left';\n                            } else {\n                                placement = pageY <= clientHeight / 2 ? 'bottom' : 'top';\n                            }\n                        } else {\n                            if (isH) {\n                                placement = pageX <= clientWidth / 2 ? 'right-top' : 'left-top';\n                            } else {\n                                placement = 'top';\n                            }\n                        }\n                    } else {\n                        //placement = pageY>targetHeight+arrowSize?'top-left':'bottom-left';\n                        if (pageY < clientHeight / 3) {\n                            placement = isH ? 'left-bottom' : 'bottom-left';\n                        } else if (pageY < clientHeight * 2 / 3) {\n                            if (isV) {\n                                placement = pageY <= clientHeight / 2 ? 'bottom-left' : 'top-left';\n                            } else {\n                                placement = 'left';\n                            }\n                        } else {\n                            placement = isH ? 'left-top' : 'top-left';\n                        }\n                    }\n                } else if (placement === 'auto-top') {\n                    if (pageX < clientWidth / 3) {\n                        placement = 'top-right';\n                    } else if (pageX < clientWidth * 2 / 3) {\n                        placement = 'top';\n                    } else {\n                        placement = 'top-left';\n                    }\n                } else if (placement === 'auto-bottom') {\n                    if (pageX < clientWidth / 3) {\n                        placement = 'bottom-right';\n                    } else if (pageX < clientWidth * 2 / 3) {\n                        placement = 'bottom';\n                    } else {\n                        placement = 'bottom-left';\n                    }\n                } else if (placement === 'auto-left') {\n                    if (pageY < clientHeight / 3) {\n                        placement = 'left-top';\n                    } else if (pageY < clientHeight * 2 / 3) {\n                        placement = 'left';\n                    } else {\n                        placement = 'left-bottom';\n                    }\n                } else if (placement === 'auto-right') {\n                    if (pageY < clientHeight / 3) {\n                        placement = 'right-bottom';\n                    } else if (pageY < clientHeight * 2 / 3) {\n                        placement = 'right';\n                    } else {\n                        placement = 'right-top';\n                    }\n                }\n                return placement;\n            },\n            getElementPosition: function() {\n                // If the container is the body or normal conatiner, just use $element.offset()\n                var elRect = this.$element[0].getBoundingClientRect();\n                var container = this.options.container;\n                var cssPos = container.css('position');\n\n                if (container.is(document.body) || cssPos === 'static') {\n                    return $.extend({}, this.$element.offset(), {\n                        width: this.$element[0].offsetWidth || elRect.width,\n                        height: this.$element[0].offsetHeight || elRect.height\n                    });\n                    // Else fixed container need recalculate the  position\n                } else if (cssPos === 'fixed') {\n                    var containerRect = container[0].getBoundingClientRect();\n                    return {\n                        top: elRect.top - containerRect.top + container.scrollTop(),\n                        left: elRect.left - containerRect.left + container.scrollLeft(),\n                        width: elRect.width,\n                        height: elRect.height\n                    };\n                } else if (cssPos === 'relative') {\n                    return {\n                        top: this.$element.offset().top - container.offset().top,\n                        left: this.$element.offset().left - container.offset().left,\n                        width: this.$element[0].offsetWidth || elRect.width,\n                        height: this.$element[0].offsetHeight || elRect.height\n                    };\n                }\n            },\n\n            getTargetPositin: function(elementPos, placement, targetWidth, targetHeight) {\n                var pos = elementPos,\n                    container = this.options.container,\n                    //clientWidth = container.innerWidth(),\n                    //clientHeight = container.innerHeight(),\n                    elementW = this.$element.outerWidth(),\n                    elementH = this.$element.outerHeight(),\n                    scrollTop = document.documentElement.scrollTop + container.scrollTop(),\n                    scrollLeft = document.documentElement.scrollLeft + container.scrollLeft(),\n                    position = {},\n                    arrowOffset = null,\n                    arrowSize = this.options.arrow ? 20 : 0,\n                    padding = 10,\n                    fixedW = elementW < arrowSize + padding ? arrowSize : 0,\n                    fixedH = elementH < arrowSize + padding ? arrowSize : 0,\n                    refix = 0,\n                    pageH = document.documentElement.clientHeight + scrollTop,\n                    pageW = document.documentElement.clientWidth + scrollLeft;\n\n                var validLeft = pos.left + pos.width / 2 - fixedW > 0;\n                var validRight = pos.left + pos.width / 2 + fixedW < pageW;\n                var validTop = pos.top + pos.height / 2 - fixedH > 0;\n                var validBottom = pos.top + pos.height / 2 + fixedH < pageH;\n\n\n                switch (placement) {\n                    case 'bottom':\n                        position = {\n                            top: pos.top + pos.height,\n                            left: pos.left + pos.width / 2 - targetWidth / 2\n                        };\n                        break;\n                    case 'top':\n                        position = {\n                            top: pos.top - targetHeight,\n                            left: pos.left + pos.width / 2 - targetWidth / 2\n                        };\n                        break;\n                    case 'left':\n                        position = {\n                            top: pos.top + pos.height / 2 - targetHeight / 2,\n                            left: pos.left - targetWidth\n                        };\n                        break;\n                    case 'right':\n                        position = {\n                            top: pos.top + pos.height / 2 - targetHeight / 2,\n                            left: pos.left + pos.width\n                        };\n                        break;\n                    case 'top-right':\n                        position = {\n                            top: pos.top - targetHeight,\n                            left: validLeft ? pos.left - fixedW : padding\n                        };\n                        arrowOffset = {\n                            left: validLeft ? Math.min(elementW, targetWidth) / 2 + fixedW : _offsetOut\n                        };\n                        break;\n                    case 'top-left':\n                        refix = validRight ? fixedW : -padding;\n                        position = {\n                            top: pos.top - targetHeight,\n                            left: pos.left - targetWidth + pos.width + refix\n                        };\n                        arrowOffset = {\n                            left: validRight ? targetWidth - Math.min(elementW, targetWidth) / 2 - fixedW : _offsetOut\n                        };\n                        break;\n                    case 'bottom-right':\n                        position = {\n                            top: pos.top + pos.height,\n                            left: validLeft ? pos.left - fixedW : padding\n                        };\n                        arrowOffset = {\n                            left: validLeft ? Math.min(elementW, targetWidth) / 2 + fixedW : _offsetOut\n                        };\n                        break;\n                    case 'bottom-left':\n                        refix = validRight ? fixedW : -padding;\n                        position = {\n                            top: pos.top + pos.height,\n                            left: pos.left - targetWidth + pos.width + refix\n                        };\n                        arrowOffset = {\n                            left: validRight ? targetWidth - Math.min(elementW, targetWidth) / 2 - fixedW : _offsetOut\n                        };\n                        break;\n                    case 'right-top':\n                        refix = validBottom ? fixedH : -padding;\n                        position = {\n                            top: pos.top - targetHeight + pos.height + refix,\n                            left: pos.left + pos.width\n                        };\n                        arrowOffset = {\n                            top: validBottom ? targetHeight - Math.min(elementH, targetHeight) / 2 - fixedH : _offsetOut\n                        };\n                        break;\n                    case 'right-bottom':\n                        position = {\n                            top: validTop ? pos.top - fixedH : padding,\n                            left: pos.left + pos.width\n                        };\n                        arrowOffset = {\n                            top: validTop ? Math.min(elementH, targetHeight) / 2 + fixedH : _offsetOut\n                        };\n                        break;\n                    case 'left-top':\n                        refix = validBottom ? fixedH : -padding;\n                        position = {\n                            top: pos.top - targetHeight + pos.height + refix,\n                            left: pos.left - targetWidth\n                        };\n                        arrowOffset = {\n                            top: validBottom ? targetHeight - Math.min(elementH, targetHeight) / 2 - fixedH : _offsetOut\n                        };\n                        break;\n                    case 'left-bottom':\n                        position = {\n                            top: validTop ? pos.top - fixedH : padding,\n                            left: pos.left - targetWidth\n                        };\n                        arrowOffset = {\n                            top: validTop ? Math.min(elementH, targetHeight) / 2 + fixedH : _offsetOut\n                        };\n                        break;\n\n                }\n                position.top += this.getOffsetTop();\n                position.left += this.getOffsetLeft();\n\n                return {\n                    position: position,\n                    arrowOffset: arrowOffset\n                };\n            }\n        };\n        $.fn[pluginName] = function(options, noInit) {\n            var results = [];\n            var $result = this.each(function() {\n\n                var webuiPopover = $.data(this, 'plugin_' + pluginName);\n                if (!webuiPopover) {\n                    if (!options) {\n                        webuiPopover = new WebuiPopover(this, null);\n                    } else if (typeof options === 'string') {\n                        if (options !== 'destroy') {\n                            if (!noInit) {\n                                webuiPopover = new WebuiPopover(this, null);\n                                results.push(webuiPopover[options]());\n                            }\n                        }\n                    } else if (typeof options === 'object') {\n                        webuiPopover = new WebuiPopover(this, options);\n                    }\n                    $.data(this, 'plugin_' + pluginName, webuiPopover);\n                } else {\n                    if (options === 'destroy') {\n                        webuiPopover.destroy();\n                    } else if (typeof options === 'string') {\n                        results.push(webuiPopover[options]());\n                    }\n                }\n            });\n            return (results.length) ? results : $result;\n        };\n\n        //Global object exposes to window.\n        var webuiPopovers = (function() {\n            var _hideAll = function() {\n                hideAllPop();\n            };\n            var _create = function(selector, options) {\n                options = options || {};\n                $(selector).webuiPopover(options);\n            };\n            var _isCreated = function(selector) {\n                var created = true;\n                $(selector).each(function(item) {\n                    created = created && $(item).data('plugin_' + pluginName) !== undefined;\n                });\n                return created;\n            };\n            var _show = function(selector, options) {\n                if (options) {\n                    $(selector).webuiPopover(options).webuiPopover('show');\n                } else {\n                    $(selector).webuiPopover('show');\n                }\n            };\n            var _hide = function(selector) {\n                $(selector).webuiPopover('hide');\n            };\n            var _updateContent = function(selector, content) {\n                var pop = $(selector).data('plugin_' + pluginName);\n                if (pop) {\n                    var cache = pop.getCache();\n                    pop.options.cache = false;\n                    pop.options.content = content;\n                    if (pop._opened) {\n                        pop._opened = false;\n                        pop.show();\n                    } else {\n                        if (pop.isAsync()) {\n                            pop.setContentASync(content);\n                        } else {\n                            pop.setContent(content);\n                        }\n                    }\n                    pop.options.cache = cache;\n                }\n            };\n\n            return {\n                show: _show,\n                hide: _hide,\n                create: _create,\n                isCreated: _isCreated,\n                hideAll: _hideAll,\n                updateContent: _updateContent\n            };\n        })();\n        window.WebuiPopovers = webuiPopovers;\n    }));\n})(window, document);\n"
  },
  {
    "path": "babel.config.js",
    "content": "/**\n * Babel config\n *\n * @package LifterLMS/Dev/Scripts\n *\n * @since Unknown\n * @version 6.0.0\n */\n\nconst\n\tpresets = [ '@wordpress/default' ],\n\tplugins = [ '@babel/plugin-proposal-class-properties' ];\n\nmodule.exports = { plugins, presets };\n"
  },
  {
    "path": "class-lifterlms.php",
    "content": "<?php\n/**\n * Main LifterLMS class\n *\n * @package LifterLMS/Main\n *\n * @since 1.0.0\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Main LifterLMS Class\n *\n * @since 1.0.0\n * @since 3.32.0 Update action-scheduler to latest version; load staging class on the admin panel.\n * @since 3.34.0 Include the LLMS_Admin_Users_Table class.\n * @since 3.36.0 Added events classes and methods.\n * @since 3.36.1 Include SendWP Connector.\n * @since 3.37.0 Move theme support methods to LLMS_Theme_Support.\n * @since 3.38.1 Include LLMS_Mime_Type_Extractor class.\n * @since 4.0.0 Update session management.\n *              Remove deprecated class files and variables.\n *              Move includes (file loading) into the LLMS_Loader class.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n */\nfinal class LifterLMS {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * LifterLMS Plugin Version.\n\t *\n\t * @var string\n\t */\n\tpublic $version = '10.0.0';\n\n\t/**\n\t * LLMS_Assets instance\n\t *\n\t * @var LLMS_Assets\n\t */\n\tpublic $assets = null;\n\n\t/**\n\t * LLMS_Query instance\n\t *\n\t * @var LLMS_Query\n\t */\n\tpublic $query = null;\n\n\t/**\n\t * Session instance\n\t *\n\t * @var LLMS_Session\n\t */\n\tpublic $session = null;\n\n\t/**\n\t * LifterLMS Constructor.\n\t *\n\t * @since 1.0.0\n\t * @since 3.21.1 Unknown\n\t * @since 4.0.0 Load `$this->session` at `plugins_loaded` in favor of during class construction.\n\t *               Remove deprecated `__autoload()` & initialize new file loader class.\n\t * @since 4.13.0 Check site duplicate status on `admin_init`.\n\t * @since 5.3.0 Move the loading of the LifterLMS autoloader to the main `lifterlms.php` file.\n\t * @since 6.1.0 Automatically load payment gateways.\n\t * @since 6.4.0 Moved registration of `LLMS_Shortcodes::init()` with the 'init' hook to `LLMS_Shortcodes::__construct()`.\n\t * @since 7.6.0 Lood locale textdomain on `init` instead of immediately\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t$this->define_constants();\n\n\t\t$this->init_assets();\n\n\t\t$this->query = new LLMS_Query();\n\n\t\t// Hooks.\n\t\tadd_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'add_action_links' ), 10, 1 );\n\n\t\tadd_action( 'init', array( $this, 'localize' ), 0 );\n\t\tadd_action( 'init', array( $this, 'init' ), 0 );\n\t\tadd_action( 'init', array( $this, 'integrations' ), 1 );\n\t\tadd_action( 'init', array( $this, 'processors' ), 5 );\n\t\tadd_action( 'init', array( $this, 'events' ), 5 );\n\t\tadd_action( 'init', array( $this, 'init_session' ), 6 ); // After table installation which happens at init 5.\n\t\tadd_action( 'init', array( $this, 'payment_gateways' ) );\n\t\tadd_action( 'init', array( $this, 'include_template_functions' ) );\n\n\t\tadd_action( 'admin_init', array( 'LLMS_Site', 'check_status' ) );\n\n\t\t// Tracking.\n\t\tif ( defined( 'DOING_CRON' ) && DOING_CRON && 'yes' === get_option( 'llms_allow_tracking', 'no' ) ) {\n\t\t\tLLMS_Tracker::init();\n\t\t}\n\n\t\t/**\n\t\t * Action fired after LifterLMS is fully loaded.\n\t\t *\n\t\t * @since Unknown\n\t\t */\n\t\tdo_action( 'lifterlms_loaded' );\n\t}\n\n\t/**\n\t * Define LifterLMS Constants\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.8 Added `LLMS_PLUGIN_URL` && `LLMS_ASSETS_SUFFIX`.\n\t * @since 4.0.0 Moved definitions of `LLMS_PLUGIN_FILE` and `LLMS_PLUGIN_DIR` to the main `lifterlms.php` file.\n\t *              Use `llms_maybe_define_constant()` to reduce code complexity.\n\t * @since 7.2.0 Added `LLMS_ASSETS_VERSION` constant.\n\t * @since 7.7.0 Added `LLMS_ALLOWED_HTML_PRICES` constant.\n\t *\n\t * @return void\n\t */\n\tprivate function define_constants() {\n\n\t\tllms_maybe_define_constant( 'LLMS_VERSION', $this->version );\n\t\tllms_maybe_define_constant( 'LLMS_TEMPLATE_PATH', $this->template_path() );\n\t\tllms_maybe_define_constant( 'LLMS_PLUGIN_URL', plugin_dir_url( LLMS_PLUGIN_FILE ) );\n\n\t\t$upload_dir = wp_upload_dir();\n\t\tllms_maybe_define_constant( 'LLMS_LOG_DIR', $upload_dir['basedir'] . '/llms-logs/' );\n\t\tllms_maybe_define_constant( 'LLMS_TMP_DIR', $upload_dir['basedir'] . '/llms-tmp/' );\n\n\t\t// If we're loading in debug mode.\n\t\t$script_debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;\n\t\t$wp_debug     = defined( 'WP_DEBUG' ) && WP_DEBUG;\n\n\t\t// If debugging, load the unminified version otherwise load minified.\n\t\tif ( ! defined( 'LLMS_ASSETS_SUFFIX' ) ) {\n\t\t\tdefine( 'LLMS_ASSETS_SUFFIX', $script_debug ? '' : '.min' );\n\t\t}\n\n\t\t// If debugging, use time for asset version otherwise use plugin version.\n\t\tif ( ! defined( 'LLMS_ASSETS_VERSION' ) ) {\n\t\t\tdefine( 'LLMS_ASSETS_VERSION', ( $script_debug || $wp_debug ) ? time() : $this->version );\n\t\t}\n\n\t\t$allowed_atts = array(\n\t\t\t'label'           => true,\n\t\t\t'align'           => true,\n\t\t\t'checked'         => true,\n\t\t\t'border'          => true,\n\t\t\t'decoding'        => true,\n\t\t\t'disabled'        => true,\n\t\t\t'required'        => true,\n\t\t\t'allowfullscreen' => true,\n\t\t\t'allow'           => true,\n\t\t\t'frameborder'     => true,\n\t\t\t'class'           => true,\n\t\t\t'type'            => true,\n\t\t\t'id'              => true,\n\t\t\t'dir'             => true,\n\t\t\t'lang'            => true,\n\t\t\t'style'           => true,\n\t\t\t'xml:lang'        => true,\n\t\t\t'src'             => true,\n\t\t\t'alt'             => true,\n\t\t\t'href'            => true,\n\t\t\t'rel'             => true,\n\t\t\t'rev'             => true,\n\t\t\t'onclick'         => true,\n\t\t\t'target'          => true,\n\t\t\t'novalidate'      => true,\n\t\t\t'value'           => true,\n\t\t\t'name'            => true,\n\t\t\t'tabindex'        => true,\n\t\t\t'action'          => true,\n\t\t\t'method'          => true,\n\t\t\t'for'             => true,\n\t\t\t'width'           => true,\n\t\t\t'height'          => true,\n\t\t\t'data-*'          => true,\n\t\t\t'aria-label'      => true,\n\t\t\t'aria-live'       => true,\n\t\t\t'aria-hidden'     => true,\n\t\t\t'aria-*'          => true,\n\t\t\t'title'           => true,\n\t\t\t'placeholder'     => true,\n\t\t\t'readonly'        => true,\n\t\t\t'rows'            => true,\n\t\t\t'cols'            => true,\n\t\t\t'minlength'       => true,\n\t\t\t'maxlength'       => true,\n\t\t\t'pattern'         => true,\n\t\t\t'enctype'         => true,\n\t\t\t'role'            => true,\n\t\t\t'selected'        => true,\n\t\t\t'srcset'          => true,\n\t\t\t'accept'          => true,\n\t\t\t'accept-charset'  => true,\n\t\t\t'accesskey'       => true,\n\t\t\t'autocomplete'    => true,\n\t\t\t'autofocus'       => true,\n\t\t\t'colspan'         => true,\n\t\t\t'contenteditable' => true,\n\t\t\t'contextmenu'     => true,\n\t\t\t'controls'        => true,\n\t\t\t'coords'          => true,\n\t\t\t'datetime'        => true,\n\t\t\t'dirname'         => true,\n\t\t\t'download'        => true,\n\t\t\t'draggable'       => true,\n\t\t\t'dropzone'        => true,\n\t\t\t'form'            => true,\n\t\t\t'formaction'      => true,\n\t\t\t'formenctype'     => true,\n\t\t\t'formmethod'      => true,\n\t\t\t'formnovalidate'  => true,\n\t\t\t'formtarget'      => true,\n\t\t\t'headers'         => true,\n\t\t\t'hidden'          => true,\n\t\t\t'high'            => true,\n\t\t\t'hreflang'        => true,\n\t\t\t'http-equiv'      => true,\n\t\t\t'ismap'           => true,\n\t\t\t'list'            => true,\n\t\t\t'loop'            => true,\n\t\t\t'low'             => true,\n\t\t\t'max'             => true,\n\t\t\t'media'           => true,\n\t\t\t'min'             => true,\n\t\t\t'multiple'        => true,\n\t\t\t'muted'           => true,\n\t\t\t'open'            => true,\n\t\t\t'optimum'         => true,\n\t\t\t'poster'          => true,\n\t\t\t'preload'         => true,\n\t\t\t'reversed'        => true,\n\t\t\t'rowspan'         => true,\n\t\t\t'scope'           => true,\n\t\t\t'shape'           => true,\n\t\t\t'size'            => true,\n\t\t\t'span'            => true,\n\t\t\t'spellcheck'      => true,\n\t\t\t'srcdoc'          => true,\n\t\t\t'srclang'         => true,\n\t\t\t'start'           => true,\n\t\t\t'step'            => true,\n\t\t\t'translate'       => true,\n\t\t\t'usemap'          => true,\n\t\t\t'wrap'            => true,\n\t\t\t'ping'            => true,\n\t\t\t'referrerpolicy'  => true,\n\t\t\t'sandbox'         => true,\n\t\t\t'sizes'           => true,\n\t\t);\n\n\t\t// For use in escaping and sanitizing.\n\t\tllms_maybe_define_constant(\n\t\t\t'LLMS_ALLOWED_HTML_PRICES',\n\t\t\tarray(\n\t\t\t\t'div'    => $allowed_atts,\n\t\t\t\t'span'   => $allowed_atts,\n\t\t\t\t'strong' => $allowed_atts,\n\t\t\t\t'sup'    => $allowed_atts,\n\t\t\t\t'sub'    => $allowed_atts,\n\t\t\t\t'del'    => $allowed_atts,\n\t\t\t\t'ins'    => $allowed_atts,\n\t\t\t\t'em'     => $allowed_atts,\n\t\t\t\t'bdi'    => $allowed_atts,\n\t\t\t\t's'      => $allowed_atts,\n\t\t\t\t'u'      => $allowed_atts,\n\t\t\t)\n\t\t);\n\n\t\t// Defining ourselves rather than relying on wp_kses_allowed_html( 'post' ) because it could be filtered.\n\t\t$allowed_post_fields = array(\n\t\t\t'address'    => array(),\n\t\t\t'a'          => array(\n\t\t\t\t'href'     => true,\n\t\t\t\t'rel'      => true,\n\t\t\t\t'rev'      => true,\n\t\t\t\t'name'     => true,\n\t\t\t\t'target'   => true,\n\t\t\t\t'download' => array(\n\t\t\t\t\t'valueless' => 'y',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'abbr'       => array(),\n\t\t\t'acronym'    => array(),\n\t\t\t'area'       => array(\n\t\t\t\t'alt'    => true,\n\t\t\t\t'coords' => true,\n\t\t\t\t'href'   => true,\n\t\t\t\t'nohref' => true,\n\t\t\t\t'shape'  => true,\n\t\t\t\t'target' => true,\n\t\t\t),\n\t\t\t'article'    => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'aside'      => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'audio'      => array(\n\t\t\t\t'autoplay' => true,\n\t\t\t\t'controls' => true,\n\t\t\t\t'loop'     => true,\n\t\t\t\t'muted'    => true,\n\t\t\t\t'preload'  => true,\n\t\t\t\t'src'      => true,\n\t\t\t),\n\t\t\t'b'          => array(),\n\t\t\t'bdo'        => array(),\n\t\t\t'big'        => array(),\n\t\t\t'blockquote' => array(\n\t\t\t\t'cite' => true,\n\t\t\t),\n\t\t\t'br'         => array(),\n\t\t\t'button'     => array(\n\t\t\t\t'disabled' => true,\n\t\t\t\t'name'     => true,\n\t\t\t\t'type'     => true,\n\t\t\t\t'value'    => true,\n\t\t\t),\n\t\t\t'caption'    => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'cite'       => array(),\n\t\t\t'code'       => array(),\n\t\t\t'col'        => array(\n\t\t\t\t'align'   => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'span'    => true,\n\t\t\t\t'valign'  => true,\n\t\t\t\t'width'   => true,\n\t\t\t),\n\t\t\t'colgroup'   => array(\n\t\t\t\t'align'   => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'span'    => true,\n\t\t\t\t'valign'  => true,\n\t\t\t\t'width'   => true,\n\t\t\t),\n\t\t\t'del'        => array(\n\t\t\t\t'datetime' => true,\n\t\t\t),\n\t\t\t'dd'         => array(),\n\t\t\t'dfn'        => array(),\n\t\t\t'details'    => array(\n\t\t\t\t'align' => true,\n\t\t\t\t'open'  => true,\n\t\t\t),\n\t\t\t'div'        => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'dl'         => array(),\n\t\t\t'dt'         => array(),\n\t\t\t'em'         => array(),\n\t\t\t'fieldset'   => array(),\n\t\t\t'figure'     => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'figcaption' => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'font'       => array(\n\t\t\t\t'color' => true,\n\t\t\t\t'face'  => true,\n\t\t\t\t'size'  => true,\n\t\t\t),\n\t\t\t'footer'     => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'h1'         => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'h2'         => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'h3'         => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'h4'         => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'h5'         => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'h6'         => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'header'     => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'hgroup'     => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'hr'         => array(\n\t\t\t\t'align'   => true,\n\t\t\t\t'noshade' => true,\n\t\t\t\t'size'    => true,\n\t\t\t\t'width'   => true,\n\t\t\t),\n\t\t\t'i'          => array(),\n\t\t\t'img'        => array(\n\t\t\t\t'alt'      => true,\n\t\t\t\t'align'    => true,\n\t\t\t\t'border'   => true,\n\t\t\t\t'height'   => true,\n\t\t\t\t'hspace'   => true,\n\t\t\t\t'loading'  => true,\n\t\t\t\t'longdesc' => true,\n\t\t\t\t'vspace'   => true,\n\t\t\t\t'src'      => true,\n\t\t\t\t'usemap'   => true,\n\t\t\t\t'width'    => true,\n\t\t\t),\n\t\t\t'ins'        => array(\n\t\t\t\t'datetime' => true,\n\t\t\t\t'cite'     => true,\n\t\t\t),\n\t\t\t'kbd'        => array(),\n\t\t\t'label'      => array(\n\t\t\t\t'for' => true,\n\t\t\t),\n\t\t\t'legend'     => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'li'         => array(\n\t\t\t\t'align' => true,\n\t\t\t\t'value' => true,\n\t\t\t),\n\t\t\t'main'       => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'map'        => array(\n\t\t\t\t'name' => true,\n\t\t\t),\n\t\t\t'mark'       => array(),\n\t\t\t'menu'       => array(\n\t\t\t\t'type' => true,\n\t\t\t),\n\t\t\t'nav'        => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'object'     => array(\n\t\t\t\t'data' => array(\n\t\t\t\t\t'required'       => true,\n\t\t\t\t\t'value_callback' => '_wp_kses_allow_pdf_objects',\n\t\t\t\t),\n\t\t\t\t'type' => array(\n\t\t\t\t\t'required' => true,\n\t\t\t\t\t'values'   => array( 'application/pdf' ),\n\t\t\t\t),\n\t\t\t),\n\t\t\t'p'          => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'pre'        => array(\n\t\t\t\t'width' => true,\n\t\t\t),\n\t\t\t'q'          => array(\n\t\t\t\t'cite' => true,\n\t\t\t),\n\t\t\t'rb'         => array(),\n\t\t\t'rp'         => array(),\n\t\t\t'rt'         => array(),\n\t\t\t'rtc'        => array(),\n\t\t\t'ruby'       => array(),\n\t\t\t's'          => array(),\n\t\t\t'samp'       => array(),\n\t\t\t'span'       => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'section'    => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'small'      => array(),\n\t\t\t'strike'     => array(),\n\t\t\t'strong'     => array(),\n\t\t\t'sub'        => array(),\n\t\t\t'summary'    => array(\n\t\t\t\t'align' => true,\n\t\t\t),\n\t\t\t'sup'        => array(),\n\t\t\t'table'      => array(\n\t\t\t\t'align'       => true,\n\t\t\t\t'bgcolor'     => true,\n\t\t\t\t'border'      => true,\n\t\t\t\t'cellpadding' => true,\n\t\t\t\t'cellspacing' => true,\n\t\t\t\t'rules'       => true,\n\t\t\t\t'summary'     => true,\n\t\t\t\t'width'       => true,\n\t\t\t),\n\t\t\t'tbody'      => array(\n\t\t\t\t'align'   => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'valign'  => true,\n\t\t\t),\n\t\t\t'td'         => array(\n\t\t\t\t'abbr'    => true,\n\t\t\t\t'align'   => true,\n\t\t\t\t'axis'    => true,\n\t\t\t\t'bgcolor' => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'colspan' => true,\n\t\t\t\t'headers' => true,\n\t\t\t\t'height'  => true,\n\t\t\t\t'nowrap'  => true,\n\t\t\t\t'rowspan' => true,\n\t\t\t\t'scope'   => true,\n\t\t\t\t'valign'  => true,\n\t\t\t\t'width'   => true,\n\t\t\t),\n\t\t\t'textarea'   => array(\n\t\t\t\t'cols'     => true,\n\t\t\t\t'rows'     => true,\n\t\t\t\t'disabled' => true,\n\t\t\t\t'name'     => true,\n\t\t\t\t'readonly' => true,\n\t\t\t),\n\t\t\t'tfoot'      => array(\n\t\t\t\t'align'   => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'valign'  => true,\n\t\t\t),\n\t\t\t'th'         => array(\n\t\t\t\t'abbr'    => true,\n\t\t\t\t'align'   => true,\n\t\t\t\t'axis'    => true,\n\t\t\t\t'bgcolor' => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'colspan' => true,\n\t\t\t\t'headers' => true,\n\t\t\t\t'height'  => true,\n\t\t\t\t'nowrap'  => true,\n\t\t\t\t'rowspan' => true,\n\t\t\t\t'scope'   => true,\n\t\t\t\t'valign'  => true,\n\t\t\t\t'width'   => true,\n\t\t\t),\n\t\t\t'thead'      => array(\n\t\t\t\t'align'   => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'valign'  => true,\n\t\t\t),\n\t\t\t'title'      => array(),\n\t\t\t'tr'         => array(\n\t\t\t\t'align'   => true,\n\t\t\t\t'bgcolor' => true,\n\t\t\t\t'char'    => true,\n\t\t\t\t'charoff' => true,\n\t\t\t\t'valign'  => true,\n\t\t\t),\n\t\t\t'track'      => array(\n\t\t\t\t'default' => true,\n\t\t\t\t'kind'    => true,\n\t\t\t\t'label'   => true,\n\t\t\t\t'src'     => true,\n\t\t\t\t'srclang' => true,\n\t\t\t),\n\t\t\t'tt'         => array(),\n\t\t\t'u'          => array(),\n\t\t\t'ul'         => array(\n\t\t\t\t'type' => true,\n\t\t\t),\n\t\t\t'ol'         => array(\n\t\t\t\t'start'    => true,\n\t\t\t\t'type'     => true,\n\t\t\t\t'reversed' => true,\n\t\t\t),\n\t\t\t'var'        => array(),\n\t\t\t'video'      => array(\n\t\t\t\t'autoplay'    => true,\n\t\t\t\t'controls'    => true,\n\t\t\t\t'height'      => true,\n\t\t\t\t'loop'        => true,\n\t\t\t\t'muted'       => true,\n\t\t\t\t'playsinline' => true,\n\t\t\t\t'poster'      => true,\n\t\t\t\t'preload'     => true,\n\t\t\t\t'src'         => true,\n\t\t\t\t'width'       => true,\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $allowed_post_fields as $field => $attributes ) {\n\t\t\tif ( ! is_array( $attributes ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$allowed_post_fields[ $field ] = array_merge( $attributes, $allowed_atts );\n\t\t}\n\n\t\tllms_maybe_define_constant(\n\t\t\t'LLMS_ALLOWED_HTML_FORM_FIELDS',\n\t\t\tarray_merge(\n\t\t\t\t$allowed_post_fields,\n\t\t\t\tarray(\n\t\t\t\t\t'bdi'      => $allowed_atts,\n\t\t\t\t\t'iframe'   => $allowed_atts,\n\t\t\t\t\t'form'     => $allowed_atts,\n\t\t\t\t\t'input'    => $allowed_atts,\n\t\t\t\t\t'select'   => $allowed_atts,\n\t\t\t\t\t'option'   => $allowed_atts,\n\t\t\t\t\t'checkbox' => $allowed_atts,\n\t\t\t\t\t'radio'    => $allowed_atts,\n\t\t\t\t\t'optgroup' => $allowed_atts,\n\t\t\t\t\t'datalist' => $allowed_atts,\n\t\t\t\t\t'output'   => $allowed_atts,\n\t\t\t\t\t'progress' => $allowed_atts,\n\t\t\t\t\t'meter'    => $allowed_atts,\n\t\t\t\t\t'source'   => $allowed_atts,\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t\tllms_maybe_define_constant( 'LLMS_CONFIRMATION_FIELDS', array( 'email_address_confirm', 'password_confirm' ) );\n\t}\n\n\t/**\n\t * Load Hooks\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function include_template_functions() {\n\t\tinclude_once 'includes/llms.template.functions.php';\n\t}\n\n\t/**\n\t * Init LifterLMS when WordPress Initialises.\n\t *\n\t * @since 1.0.0\n\t * @since 3.21.1 Unknown.\n\t * @since 4.0.0 Don't initialize removed `LLMS_Person()` class.\n\t * @since 4.12.0 Check site staging/duplicate status & trigger associated actions.\n\t * @since 4.13.0 Remove site staging/duplicate check and run only on `admin_init`.\n\t * @since 5.8.0 Initialize block templates.\n\t * @since 7.7.0 Initialize Elementor migration.\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\n\t\tdo_action( 'before_lifterlms_init' );\n\n\t\t$this->block_templates();\n\t\t$this->engagements();\n\t\t$this->notifications();\n\t\t( new LLMS_Media_Protector() )->register_callbacks();\n\n\t\tinclude_once 'includes/class-llms-elementor-migrate.php';\n\t\tinclude_once 'includes/class-llms-bricks.php';\n\t\tinclude_once 'includes/class-llms-beaver-builder.php';\n\t\tinclude_once 'includes/class-llms-beaver-builder-migrate.php';\n\n\t\tdo_action( 'lifterlms_init' );\n\t}\n\n\t/**\n\t * Initialize the core asset handler class.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return LLMS_Assets\n\t */\n\tprivate function init_assets() {\n\n\t\t$this->assets = new LLMS_Assets( 'llms-core' );\n\n\t\t$this->assets->define( 'scripts', require LLMS_PLUGIN_DIR . 'includes/assets/llms-assets-scripts.php' );\n\t\t$this->assets->define( 'styles', require LLMS_PLUGIN_DIR . 'includes/assets/llms-assets-styles.php' );\n\n\t\treturn $this->assets;\n\t}\n\n\t/**\n\t * Initializes an LLMS_Session() into the $session variable\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return LLMS_Session\n\t */\n\tpublic function init_session() {\n\n\t\tif ( is_null( $this->session ) ) {\n\t\t\t$this->session = new LLMS_Session();\n\t\t}\n\n\t\treturn $this->session;\n\t}\n\n\t/**\n\t * Get the plugin url.\n\t *\n\t * @return string\n\t */\n\tpublic function plugin_url() {\n\t\treturn untrailingslashit( plugins_url( '/', __FILE__ ) );\n\t}\n\n\t/**\n\t * Get the plugin path.\n\t *\n\t * @return string\n\t */\n\tpublic function plugin_path() {\n\t\treturn untrailingslashit( plugin_dir_path( __FILE__ ) );\n\t}\n\n\t/**\n\t * Get the template path.\n\t *\n\t * @return string\n\t */\n\tpublic function template_path() {\n\t\treturn apply_filters( 'llms_template_path', 'lifterlms/' );\n\t}\n\n\t/**\n\t * Retrieve the LLMS_Emails singleton.\n\t *\n\t * @since Unknown\n\t *\n\t * @return LLMS_Emails\n\t */\n\tpublic function mailer() {\n\t\treturn LLMS_Emails::instance();\n\t}\n\n\t/**\n\t * Retrieve the LLMS_Achievements singleton.\n\t *\n\t * @since Unknown\n\t *\n\t * @return LLMS_Achievements\n\t */\n\tpublic function achievements() {\n\t\treturn LLMS_Achievements::instance();\n\t}\n\n\t/**\n\t * Retrieve the LLMS_Certificates singleton.\n\t *\n\t * @since Unknown\n\t *\n\t * @return LLMS_Certificates\n\t */\n\tpublic function certificates() {\n\t\treturn LLMS_Certificates::instance();\n\t}\n\n\t/**\n\t * Retrieve the LLMS_Engagements singleton.\n\t *\n\t * @since Unknown\n\t *\n\t * @return LLMS_Engagements\n\t */\n\tpublic function engagements() {\n\t\treturn LLMS_Engagements::instance();\n\t}\n\n\t/**\n\t * Block templates instance.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return LLMS_Block_Templates\n\t */\n\tpublic function block_templates() {\n\t\treturn LLMS_Block_Templates::instance();\n\t}\n\n\t/**\n\t * Events instance.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return LLMS_Events\n\t */\n\tpublic function events() {\n\t\treturn LLMS_Events::instance();\n\t}\n\n\t/**\n\t * Grading instance\n\t *\n\t * @since    3.24.0\n\t *\n\t * @return   LLMS_Grades\n\t */\n\tpublic function grades() {\n\t\treturn LLMS_Grades::instance();\n\t}\n\n\t/**\n\t * Get integrations\n\t *\n\t * @return LLMS_Integrations instance\n\t */\n\tpublic function integrations() {\n\t\treturn LLMS_Integrations::instance();\n\t}\n\n\t/**\n\t * Retrieve an instance of the notifications class\n\t *\n\t * @return   LLMS_Notifications\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function notifications() {\n\t\treturn LLMS_Notifications::instance();\n\t}\n\n\t/**\n\t * Get payment gateways.\n\t *\n\t * @return LLMS_Payment_Gateways\n\t */\n\tpublic function payment_gateways() {\n\t\treturn LLMS_Payment_Gateways::instance();\n\t}\n\n\t/**\n\t * Load all background processors.\n\t *\n\t * @since    3.15.0\n\t *\n\t * @return   LLMS_Processors\n\t */\n\tpublic function processors() {\n\t\treturn LLMS_Processors::instance();\n\t}\n\n\t/**\n\t * Add plugin settings Action Links\n\t *\n\t * @since Unknown\n\t *\n\t * @param string[] $links Existing action links.\n\t * @return string[]\n\t */\n\tpublic function add_action_links( $links ) {\n\n\t\t$lifter_links = array(\n\t\t\t'<a href=\"' . admin_url( 'admin.php?page=llms-settings' ) . '\">' . __( 'Settings', 'lifterlms' ) . '</a>',\n\t\t);\n\n\t\tif ( 3 === count( $links ) ) {\n\t\t\treturn $links;\n\t\t}\n\n\t\treturn array_merge( $links, $lifter_links );\n\t}\n\n\t/**\n\t * Localize the plugin\n\t *\n\t * Language files can be found in the following locations (The first loaded file takes priority):\n\t *\n\t *   1. wp-content/languages/lifterlms/lifterlms-{LOCALE}.mo\n\t *\n\t *      This is recommended \"safe\" location where custom language files can be stored. A file\n\t *      stored in this directory will never be automatically overwritten.\n\t *\n\t *   2. wp-content/languages/plugins/lifterlms-{LOCALE}.mo\n\t *\n\t *      This is the default directory where WordPress will download language files from the\n\t *      WordPress GlotPress server during updates. If you store a custom language file in this\n\t *      directory it will be overwritten during updates.\n\t *\n\t *   3. wp-content/plugins/lifterlms/languages/lifterlms-{LOCALE}.mo\n\t *\n\t *      This is the the LifterLMS plugin directory. A language file stored in this directory will\n\t *      be removed from the server during a LifterLMS plugin update.\n\t *\n\t * @since Unknown\n\t * @since 4.9.0 Use `llms_load_textdomain()`.\n\t *\n\t * @return void\n\t */\n\tpublic function localize() {\n\n\t\tllms_load_textdomain( 'lifterlms' );\n\t}\n}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"gocodebox/lifterlms\",\n  \"description\": \"LifterLMS, the #1 WordPress LMS solution, makes it easy to create, sell, and protect engaging online courses.\",\n  \"keywords\": [\n    \"WordPress\",\n    \"LMS\"\n  ],\n  \"homepage\": \"https://lifterlms.com\",\n  \"license\": \"GPL-3.0+\",\n  \"authors\": [\n    {\n      \"name\": \"LifterLMS\",\n      \"email\": \"help@lifterlms.com\",\n      \"homepage\": \"https://lifterlms.com\"\n    }\n  ],\n  \"type\": \"wordpress-plugin\",\n  \"support\": {\n    \"forum\": \"https://wordpress.org/support/plugin/lifterlms\",\n    \"issues\": \"https://github.com/gocodebox/lifterlms/issues\",\n    \"source\": \"https://github.com/gocodebox/lifterlms\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"LLMS\\\\\": \"includes\"\n    }\n  },\n  \"require\": {\n    \"php\": \">=7.4\",\n    \"composer/installers\": \"~1.9.0\",\n    \"deliciousbrains/wp-background-processing\": \"1.0.2\",\n    \"lifterlms/lifterlms-blocks\": \"2.7.2\",\n    \"lifterlms/lifterlms-cli\": \"0.0.5\",\n    \"lifterlms/lifterlms-helper\": \"3.5.9\",\n    \"lifterlms/lifterlms-rest\": \"1.0.5\",\n    \"woocommerce/action-scheduler\": \"3.5.4\",\n    \"gocodebox/banner-notifications\": \"1.1.1\"\n  },\n  \"require-dev\": {\n    \"lifterlms/lifterlms-tests\": \"^4.4\",\n    \"lifterlms/lifterlms-cs\": \"dev-trunk\"\n  },\n  \"archive\": {\n    \"exclude\": [\n      \".*\",\n      \"*.lock\",\n      \"*.xml\",\n      \"*.xml.dist\",\n      \"*.config.js\",\n\n      \"CHANGELOG.md\",\n      \"composer.json\",\n      \"docker-compose.yml\",\n      \"lerna.json\",\n      \"package.json\",\n      \"package-lock.json\",\n      \"README.md\",\n\n      \"/assets/scss\",\n\n      \"_private\",\n      \"dist\",\n      \"docs\",\n      \"gulpfile.js\",\n      \"node_modules\",\n      \"packages\",\n      \"src\",\n      \"tests\",\n      \"tmp\",\n      \"wordpress\",\n      \"!/vendor\",\n\n      \"!/libraries\",\n      \"/libraries/README.md\",\n      \"/libraries/**/composer.*\",\n      \"/libraries/**/i18n\",\n      \"/libraries/banner-notifications/.github\",\n      \"/libraries/banner-notifications/.gitignore\",\n      \"/libraries/banner-notifications/phpunit.xml\",\n      \"/libraries/banner-notifications/.editorconfig\",\n      \"/libraries/banner-notifications/tests\",\n\n      \"/vendor/bin\",\n      \"/vendor/**/**/composer.*\",\n      \"/vendor/**/**/*.md\",\n      \"/vendor/**/**/.*\",\n      \"/vendor/composer/installers\",\n      \"/vendor/composer/lifters\",\n\n      \"!/assets/maps/js/vendor\",\n      \"!/assets/vendor\",\n      \"!/assets/js/vendor\",\n      \"!/assets/js/builder/vendor\"\n    ]\n  },\n  \"scripts\": {\n    \"check-cs\": \"\\\"vendor/bin/phpcs\\\" --colors\",\n    \"check-cs-errors\": \"\\\"vendor/bin/phpcs\\\" --colors --error-severity=1 --warning-severity=6\",\n    \"config-cs\": [\n      \"\\\"vendor/bin/phpcs\\\" --config-set default_standard 'LifterLMS Core'\",\n      \"\\\"vendor/bin/phpcs\\\" --config-set ignore_warnings_on_exit 1\"\n    ],\n    \"env\": \"\\\"vendor/bin/llms-env\\\"\",\n    \"env:setup\": \"./tests/bin/setup-e2e.sh\",\n    \"fix-cs\": \"\\\"vendor/bin/phpcbf\\\"\",\n    \"post-install-cmd\": \"@post-update-install-cmd\",\n    \"post-update-cmd\": \"@post-update-install-cmd\",\n    \"post-update-install-cmd\": [\n      \"@config-cs\",\n      \"rm -rf ./wp-content/\",\n      \"rm composer.lock\"\n    ],\n    \"tests-remove\": \"\\\"vendor/bin/llms-tests\\\" teardown ${TESTS_DB_NAME:-llms_tests} ${TESTS_DB_USER:-root} \\\"${TESTS_DB_PASS:-password}\\\" ${TESTS_DB_HOST:-127.0.0.1}\",\n    \"tests-install\": \"\\\"vendor/bin/llms-tests\\\" install ${TESTS_DB_NAME:-llms_tests} ${TESTS_DB_USER:-root} \\\"${TESTS_DB_PASS:-password}\\\" ${TESTS_DB_HOST:-127.0.0.1} ${WP_VERSION:-latest} false \\\"${WP_TESTS_VERSION:-false}\\\"\",\n    \"tests-reinstall\": [\n      \"@tests-remove\",\n      \"@tests-install\"\n    ],\n    \"tests\": [\n      \"Composer\\\\Config::disableProcessTimeout\",\n      \"\\\"vendor/bin/phpunit\\\"\"\n    ],\n    \"tests-run\": [\n      \"Composer\\\\Config::disableProcessTimeout\",\n      \"\\\"vendor/bin/phpunit\\\"\"\n    ],\n    \"install-php8\": \"composer install --ignore-platform-reqs\"\n  },\n  \"extra\": {\n    \"installer-paths\": {\n      \"libraries/{$name}\": [\n        \"lifterlms/lifterlms-blocks\",\n        \"lifterlms/lifterlms-cli\",\n        \"lifterlms/lifterlms-helper\",\n        \"lifterlms/lifterlms-rest\",\n        \"gocodebox/banner-notifications\"\n      ],\n      \"vendor/{$vendor}/{$name}\": [\n        \"type:wordpress-plugin\"\n      ]\n    }\n  },\n  \"config\": {\n    \"allow-plugins\": {\n      \"dealerdirect/phpcodesniffer-composer-installer\": true,\n      \"composer/installers\": true\n    }\n  },\n  \"minimum-stability\": \"dev\"\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.1'\nservices:\n  wordpress:\n    volumes:\n      - ./:/var/www/html/wp-content/plugins/lifterlms:rw\n"
  },
  {
    "path": "docs/block-development.md",
    "content": "# Block Development\n\nBelow are the steps for creating and registering a new block for LifterLMS.\n\nPlease note that before beginning you will need to have Node and NPM installed on your machine. Please see [https://github.com/gocodebox/lifterlms/blob/trunk/docs/installing.md](https://github.com/gocodebox/lifterlms/blob/trunk/docs/installing.md) for installation details.\n\n#### Table of Contents\n- [1. Create block files](#1-create-block-files)\n- [2. Add block JSON data](#2-add-block-json-data)\n- [3. Adding Block JS](#3-adding-block-js)\n- [4. Compiling blocks](#4-compiling-blocks)\n- [5. Register with PHP](#5-register-with-php)\n- [6. Block design guidelines](#6-block-design-guidelines)\n\n### 1. Create block files\n\nCreate a new folder in the `src/blocks` directory for your block. E.g. `/src/blocks/example-block/`. Then, add a `block.json` file and an `index.jsx` file to the new folder.\n\nThe block directory structure should now look like this:\n\n```shell\n.\n└── project/\n    ├── src/\n    │   └── blocks/\n    │       └── block/\n    │           ├── block.json\n    │           ├── index.jsx\n    │           └── index.scss # optional.\n    ├── package.json\n    └── webpack.congif.js\n```\n\n### 2. Add block JSON data\n\nNext, add block information to the `block.json` file. Below is an example of a block.json file. Note that the category should be `lifterlms` to match the other LifterLMS blocks:\n\n```json\n{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/example-block\",\n  \"title\": \"Example\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Block description\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {},\n  \"supports\": {},\n  \"editorScript\": \"file:./index.js\"\n}\n```\n\n### 3. Adding Block JS\n\nNext, add the block’s JavaScript to the `index.jsx` file. We use the JSX file extension to indicate that the file contains JSX code.\n\nBelow is an example of how to register a new block and access the block.json data to set the block’s name and attributes:\n\n```jsx\nimport { registerBlockType } from '@wordpress/blocks';\nimport blockJson from './block.json';\n\nregisterBlockType( blockJson, {\n    edit: ( props ) => {\n        return <p>{ props.name }</p>;\n    },\n    save: ( props ) => {\n        return <p>{ props.name }</p>;\n    },\n} );\n```\n\n*Note that while it is common practise to separate the `edit` and `save` functions into separate files, this is not necessary unless the code becomes too complex to manage in a single file. We prefer to keep the code in a single file where possible.*\n\n### 4. Compiling blocks\n\nTo compile the block, open the Terminal and run the following NPM script from the plugin root directory. This will compile all blocks to the main `/blocks/` directory:\n\n`npm run build:blocks`\n\n### 5. Register with PHP\n\nThe last step is to register the block with PHP. This should be added to a PHP file or class where it makes sense. For example, shortcode blocks are registered in the `/includes/shortcodes/class.llms.shortcodes.blocks.php` file. Below is an example of how to register a block with PHP and allow WordPress to handle the loading of scripts and styles:\n\n```php\nadd_action( 'init', 'llms_register_example_block' );\n/**\n * Register the example block.\n *\n * @since 1.0.0\n *\n * @return void\n */\nfunction llms_register_example_block() {\n    register_block_type( LLMS_PLUGIN_DIR . 'blocks/example-block' );\n}\n```\n\n### 6. Block design guidelines\n\nBlocks should be designed to be as simple as possible.\n\n#### Icons\n\nBlocks should use FontAwesome icons. The complete list of icons can be found at [https://fontawesome.com/icons](https://fontawesome.com/icons). SVG icons need to be converted to React components and added to blocks with the `registerBlockType` function:\n\n```jsx\nimport { registerBlockType } from '@wordpress/blocks';\nimport { SVG, Path } from '@wordpress/primitives';\n\nconst Icon = () => (\n\t<SVG xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\">\n\t\t<Path\n\t\t\td=\"M592 416H48c-26.5 0-48-21.5-48-48V144c0-26.5 21.5-48 48-48h544c26.5 0 48 21.5 48 48v224c0 26.5-21.5 48-48 48z\"\n\t\t/>\n\t</SVG>\n);\n\nregisterBlockType( blockJson, {\n  icon: Icon,\n  edit: Edit\n} );\n```\n\n#### Colors\n\nBlocks use the default core admin color palette. This ensures that hover and active states are consistent with other blocks.\n\n## Shortcodes\n\nFor blocks with Server Side Rendering functionality, a shortcode should also be registered to support users who are not using the block editor. The shortcode should follow LifterLMS shortcode naming conventions and be registered in the `/includes/shortcodes/` directory and extend the `LLMS_Shortcode` class.\n\nShortcode blocks can use the `llms_shortcode_blocks` filter provided by the  `LLMS_Shortcode_Block` class to handle the block registration and rendering. Below is an example of how to register a block with the class from within a LifterLMS add-on plugin:\n\n```php\nadd_filter( 'llms_shortcode_blocks', register_blocks( array $config ): array {\n    $config['group-list'] = array(\n        'render' => array( 'LLMS_Groups_Shortcode_Group_List', 'output' ),\n        'path'   => LLMS_GROUPS_PLUGIN_DIR . 'assets/blocks/group-list',\n    );\n\n    return $config;\n} );\n```\n"
  },
  {
    "path": "docs/coding-standards.md",
    "content": "LifterLMS Coding Standards\n==========================\n\nThe purpose of the LifterLMS Coding Standards is to create a baseline for collaboration and review within the open source LifterLMS codebase, project, and community.\n\nThe WordPress community has developed coding standards and documented them in the [WordPress codex](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/). Wherever possible, the LifterLMS Coding Standards aim to obey these coding standards.\n\n## Naming Conventions\n\n### camelCase should not be used.\n\nLifterLMS avoids `camelCase` for class names, class methods, functions, and variables. Words should instead be separated by underscores.\n\n### Class Names\n\nClass names should use capitalized words separated by underscores.\nLifterLMS core class names should be prefixed with `LLMS_`.\n\n\n```php\nclass LLMS_Student extends LLMS_Abstract_User_Data { [...] }\nclass LLMS_Data { [...] }\n```\n\nLifterLMS add-on class names should be prefixed with `LLMS_` as well as an additional add-on prefix.\n\n```php\nclass LLMS_AQ_Question_Types { [...] }\nclass LLMS_SL_Story extends LLMS_Abstract_Database_Store { [...] }\n```\n\n### Trait Names\n\nTrait names should use capitalized words separated by underscores.\nLifterLMS core trait names should be prefixed with `LLMS_Trait`.\n\n```php\ntrait LLMS_Trait_Singleton { [...] }\n```\n\n### Constants\n\nConstants should be in all upper-case with underscores separating words.\nLifterLMS core constants should be prefixed with `LLMS_`.\n\n```php\ndefine( 'LLMS_PLUGIN_FILE', __FILE__ );\n```\n\nLifterLMS add-on class names should be prefixed with `LLMS_` as well as an additional add-on prefix.\n\n```php\ndefine( 'LLMS_FORMIDABLE_FORMS_PLUGIN_FILE', __FILE__ );\n```\n\n### File names\n\nFiles should be named descriptively using lower case letters. Hyphens should be used to separate words.\n\n```\nmy-plugin-file.php\n```\n\nClass file names should be based on the class name with `class-` prepended and the underscores in the class name replaced with hyphens, for example `LLMS_Data` becomes:\n\n```\nclass-llms-data.php\n```\n\nFiles containing model classes should prepend `model-` instead of `class-`. For example the `LLMS_Student` model class becomes:\n\n```\nmodel-llms-student.php\n```\n\nTrait file names should be based on the trait name with underscores replaced by hyphens and the file stored in the\n`includes/traits` directory. For example `LLMS_Trait_Singleton` becomes:\n\n```\nincludes/traits/llms-trait-singleton.php\n```\n\n### Functions & Variables\n\nLowercase letters should be used for function names and variables. Separate words with underscores.\nLifterLMS core functions should be prepended with the prefix `llms_`.\n\n```php\nllms_current_time( $type, $gmt = 0 ) { [...] }\n```\n\nLifterLMS add-on function names should be prefixed with `llms_` as well as an additional add-on prefix.\n\n```php\nllms_ck_consent_form_field() { [...] }\n```\n\n### Hooks: Actions & Filters\n\nLowercase letters should be used for hook names. Separate words with underscores.\nLifterLMS core hooks should be prepended with the prefix `llms_`.\n\n```php\ndo_action( 'llms_user_enrolled_in_course', [...] );\napply_filters( 'llms_get_enrollment_status', [...] );\n```\n\nLifterLMS add-on hook names should be prefixed with `llms_` as well as an additional add-on prefix.\n\n```php\ndo_action( 'llms_pa_post_created_from_automation', [...] );\napply_filters( 'llms_sl_story_can_user_manage', [...] );\n```\n\nWhen actions are set to run before and after items (templates, as an example) it is acceptable to use additional prefixes `before_` and `after_` prior to the `llms_` prefix.\n\nThere are a number of legacy hooks which use the prefix `lifterlms_` instead of `llms_`. These are retained for backwards compatibility but should not be used as an example of an acceptable naming convention for new code.\n\n### CSS Classes and IDs\n\nClass names and IDs should be lowercase and prefixed with `llms-`.\n\nWords should be separated with hyphens (AKA \"kebab case\").\n\n```html\n<div class=\"llms-element-name\" id=\"llms-element-id\"></div>\n```\n\n### Form Element `name` attributes\n\nThe `name` attribute of HTML form elements should be prefixed with `llms_`.\n\nLowercase letters should be used and words should be separated by underscores.\n\n```html\n<form>\n   <input name=\"llms_text_field\" type=\"text\">\n</form>\n```\n\n\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "Contributor Guidelines\n----------------------\n\nSee contributing guidelines at https://github.com/gocodebox/lifterlms/blob/trunk/.github/CONTRIBUTING.md\n"
  },
  {
    "path": "docs/documentation-standards.md",
    "content": "LifterLMS Inline Documentation Standards\n========================================\n\nThe LifterLMS documentation standard is heavily inspired by the [WordPress core's documentation standards][wp-core-docs]. We have made customizations to these standards in areas where it aids our core team's development and release workflows. By using the WordPress core documentation standard as a starting point any contributor already familiar with the WordPress core should be able to quickly add inline documentation to LifterLMS without the need to study our standards at length.\n\n## What should be documented\n\nThe following elements should be documented using formatted documentation blocks (DocBlocks):\n\n+ Functions\n+ Classes\n+ Class methods\n+ Class members (including properties and constants)\n+ Requires and includes\n+ Hooks (actions and filters)\n+ File headers\n\n## DocBlock Formatting Guidelines\n\nInline documentation in the LifterLMS code base is automatically parsed and output to the code reference [developer.lifterlms.com][llms-dev]. Adhering to these guidelines is essential to ensure optimum readability via the code reference.\n\n\n### Spacing\n\nDocBlocks should directly precede the element (hook, function, method, class, etc...). There should not be any opening/closing tags, white space, or anything else between the DocBlock and the declarations. This will ensure the parser can correctly associate the DocBlock with it's element.\n\n\n### Summary\n\nA short piece of text, usually one line, providing the basic function of the associated element. A good summary concisely describes what the element does and should not attempt to describe why the element exists.\n\nHTML may not be used in the summary. For example, if the function outputs an `<img>` tag, the summary should read ```Outputs an image tag.``` instead of ```Outputs an `<img>` tag.```.\n\n\n### Description\n\nAn optional longer piece of text providing more details on the associated element’s function.\n\nHTML may not be used in the summary but markdown can be used to format a complicated description.\n\n**1. Lists**\n\nUse a hyphen (`-`) to create an unordered list, with a blank line before and after.\n\n```\n * Description which includes an unordered list:\n *\n * - This is item 1.\n * - This is item 2.\n * - This is item 3.\n *\n * The description continues on ...\n```\n\nUse numbers to create an ordered list, with a blank line before and after.\n\n```\n * Description which includes an ordered list:\n *\n * 1. This is item 1.\n * 2. This is item 2.\n * 3. This is item 3.\n *\n * The description continues on ...\n```\n\n**2. Code Samples**\n\nA code sample may be created by indenting every line of the code by 4 spaces, with a blank line before and after. Blank lines in code samples also need to be indented by four spaces. Note that examples added in this way will be output in `<pre>` tags and are not syntax-highlighted in the code reference.\n\n```\n  * Description including a code sample:\n  *\n  *    $status = array(\n  *        'draft'   => __( 'Draft' ),\n  *        'pending' => __( 'Pending Review' ),\n  *        'private' => __( 'Private' ),\n  *        'publish' => __( 'Published' )\n  *    );\n  *\n  * The description continues on ...\n```\n\n**3. Links**\n\nA link in the form of a URL, such as related GitHub issue or other documentation, should be added in the appropriate place in the DocBlock using the `@link` tag.\n\n```\n * Description text.\n *\n * @link https://github.com/gocodebox/lifterlms/issues/1234567890\n```\n\n### Changelogs\n\nWhenever any code is changed within an element, a `@since`, `@version`, or `@deprecated` tag should be added to the element to document the change(s) which have been made.\n\nNo HTML should be used in the descriptions for these tags, though limited Markdown can be used as necessary, such as for adding backticks around variables, e.g. `$variable`.\n\nAll descriptions for any of these tags should be a full sentence ending with a full stop (a period, for example).\n\n#### Changes Warranting a Changelog Entry\n\nMost code changes warrant a changelog entry to be recorded for the element but there are some exceptions.\n\n+ **Classes**: Any breaking changes, deprecations, or the introduction of new class elements (elements which do not have their own changelog, such as class properties) require an accompanying `@since` tag entry. Changes to a class method should be recorded on the method's changelog, not on the class changelog.\n+ **Functions and class methods**: Any change made requires an accompanying `@since` tag entry\n\nChanges which do not affect the functionality or execution of the element *should not* be recorded on the element's changelog. For example, a coding standards change such as alignment or spacing should not be recorded.\n\n#### Recording the Version Number\n\nVersions should be expressed in the 3-digit `x.x.x` style.\n\n```\n * @since 3.29.0\n```\n\nWhen any change has been made to the element an additional `@since` tag can be added with a short description of the changes which were made.\n\n```\n * @since 3.3.0\n * @since 3.5.0 Added optional 3rd argument.\n```\n\n#### Deprecations\n\nWhen an element is marked for deprecation this should be recorded at the end of the changelog with an `@deprecated` tag.\n\nA short description may be added to provide additional information about the deprecation. If a replacement function has been added in it's place, note as much with an `@see` tag.\n\n```\n * @since 3.3.0\n * @since 3.5.0 Added optional 3rd argument.\n * @deprecated 3.10.0 Use `llms_new_function_name()` instead.\n *\n * @see llms_new_function_name()\n```\n\nWhen adding documentation on an existing element which does not yet have a changelog (common in code added prior to the creation and enforcement of these standards) if it is impossible to determine when the element was added the version may be expressed with `Unknown` instead of the `x.x.x` version number.\n\n#### File Headers\n\nWhenever an element within a file is updated, the `@version` tag in the header should be updated to the current version of the codebase.\n\n#### Tag alignment and order\n\nAll changelog tags, `@since`, `@version`, and `@deprecated` should be grouped together with a space before the first `@since` tag and after the last tag in the group.\n\n```\n * @since 3.3.0\n * @since 3.5.0 Changelog entry description.\n * @deprecated 3.10.0 Use `llms_new_function_name()` instead.\n```\n\nWhen multiple lines are required for a single entry, subsequent lines should be indented to match the starting point of the description.\n\n```\n * @since 3.3.0\n * @since 3.5.0 Changelog entry description.\n                A second entry aligned to with the first entry.\n```\n\nMultiple logs with version numbers of differing lengths should not be aligned to one another.\n\n```\n * @since 3.3.0\n * @since 3.25.0 Changelog entry description.\n * @since 4.0.0 This entry should not be aligned with the 3.25.0 entry above it.\n```\n\n#### Using Placeholders\n\nWhen contributing code we recommend using the placeholder `[version]` in favor of trying to guess what version the element will be released with.\n\nOur release workflow automatically replaces with `@since`, `@version`, and `@deprecated` followed by `[version]` with the actual version of the release being packaged.\n\nFor a new element:\n\n```\n * @since [version]\n```\n\nWhen updating an existing element:\n\n```\n * @since 3.5.0\n * @since [version] Updated element.\n```\n\n\n### Additional Tags\n\n#### 1. Parameters and Returns\n\nFunctions and methods should define all parameter arguments and returns with the `@param` and `@return` tags.\n\nNo HTML should be used in the descriptions for these tags, though limited Markdown can be used as necessary, such as for adding backticks around variables, e.g. `$variable`.\n\nAll descriptions for any of these tags should be a full sentence ending with a full stop (a period, for example).\n\n```\n * @param string $var1 Description of the argument.\n * @param bool   $var2 Description of the argument.\n * @return string\n */\nfunction my_function( $var1, $var2 = false ) {\n    ...\n    return $var1;\n}\n```\n\nParameters that are arrays should be documented using WordPress’ flavor of hash notation style, each array value beginning with the `@type` tag, and and describing the value as follows:\n\n```\n *     @type type $key Description. Default 'value'. Accepts 'value', 'value'.\n *                     (aligned with Description, if wraps to a new line)\n```\n\nA full array parameter would look like this:\n\n```\n * @param array $args {\n *     Optional. An array of arguments.\n *\n *     @type type $key Description. Default 'value'. Accepts 'value', 'value'.\n *                     (aligned with Description, if wraps to a new line)\n *     @type type $key Description.\n * }\n```\n\n#### 2. Types\n\nVariables, constants, and class members should use the `@var` tag to describe the member's type.\n\n```\n * @var string\n */\npublic $var = 'text';\n```\n\n#### 3. Relations and References\n\nUse `@see` to perform automatic links to other areas of the codebase. For example `{@see 'is_lifterlms'}` to link to the filter `is_lifterlms`.\n\n\n#### 4. Thrown Exceptions\n\nA function or method which throws an exception should document the thrown exception using an `@throws` tag.\n\nWhen present, the `@throws` tag should be added to the end of the docblock below the `@return` tag. An empty line should separate the `@return` and `@throws` tag.\n\n```\n * @return string\n *\n * @throws Exception A description of the raised exception.\n */\n```\n\n## DocBlock Examples\n\n\n### Functions and Class Methods\n\nFunctions and class methods should be formatted as follows:\n\n+ Summary\n+ Description (optional)\n+ Changelog\n+ Links and References (where appropriate)\n+ Parameters\n+ Return\n\n```\n/**\n * Summary.\n *\n * Description.\n *\n * @since x.x.x\n * @since x.x.x Description of function/method changes.\n *\n * @see Function/method/class relied on\n * @link URL\n *\n * @param type $var Description.\n * @param type $var Optional. Description. Default.\n * @return type Description.\n */\n```\n\n\n### Classes\n\nClass DocBlocks should be formatted as follows:\n\n+ Summary\n+ Description (Optional)\n+ Links and References (as an example use `@see` to reference a super class when documenting a sub class)\n+ Changelog\n\n```\n/**\n * Summary.\n *\n * Description.\n *\n * @see Super_Class\n *\n * @since x.x.x\n * @since x.x.x Description of class changes.\n */\n```\n\n\n### Class Members\n\nClass properties and constants should be formatted as follows:\n\n+ Summary\n+ Changelog\n+ Type\n\n```\n/**\n * Summary.\n *\n * @since x.x.x\n * @since x.x.x Description of member changes.\n * @var type Optional description.\n */\n```\n\n\n### Hooks (Actions and Filters)\n\nBoth action and filter hooks should be documented on the line immediately preceding the call to `do_action()` or `do_action_ref_array()`, `apply_filters()`, or `apply_filters_ref_array()`, and formatted as follows:\n\n+ Summary\n+ Description (Optional)\n+ Changelog\n+ Parameters\n\nNote that `@return` is not used for hook documentation, because action hooks return nothing, and filter hooks always return their first parameter.\n\n```\n/**\n * Summary.\n *\n * Description.\n *\n * @since x.x.x\n * @since x.x.x Description of hook changes.\n *\n * @param type  $var Description.\n * @param array $args {\n *     Short description about this hash.\n *\n *     @type type $var Description.\n *     @type type $var Description.\n * }\n * @param type  $var Description.\n */\n```\n\n\n### File Headers\n\nThe file header DocBlock is used to give an overview of what is contained in the file and should be formatted as follows:\n\n+ Summary\n+ Description (optional)\n+ Links and references\n+ Package\n+ Changelog\n\n```\n/**\n * Summary (no period for file headers)\n *\n * Description. (use period)\n *\n * @link URL\n *\n * @package LifterLMS/SecondaryPackage/TertiaryPackage\n *\n * @since x.x.x\n * @since x.x.x Description of file changes.\n * @version x.x.x\n */\n```\n\n\n[llms-dev]: https://developer.lifterlms.com/reference/\n[wp-core-docs]: https://developer.wordpress.org/coding-standards/inline-documentation-standards/\n"
  },
  {
    "path": "docs/e2e-tests-real.md",
    "content": "Running E2E (End to End) Tests Against a Real Website\n=====================================================\n\n_The core E2E test suite is primarily designed to be run locally against managed Docker containers. However, it is possible to run the test suite against any WordPress website with a publicly accessible URL by following this guide._\n\n_To run tests locally against managed Docker containers, see the [E2E Testing README](../tests/e2e/README.md)._\n\n**NOTE: This is an experimental process! Proceed with caution. We are developing this process for internal use and thought it might be useful to some other folks.**\n\n**Another note: This process will import courses, create fake users, and add other data to your website and there is no cleanup proccess. If you choose to use this against a live production site that means that the database will have a bunch of fake test data added to it. So don't run this against a real production website. Use a staging website instead!**\n\n## Prerequisites\n\n+ Ability to use a terminal\n+ git\n+ node.js\n+ npm\n\n\n## Setup your local environment\n\n+ Install the LifterLMS repo: `git clone https://github.com/gocodebox/lifterlms`\n+ Move into the cloned directory: `cd liferlms`\n+ Install node packages: `npm ci`\n+ Create a new file in the created directory named `.llmsenv`.\n+ Use your favorite text editor to edit the file and add the following to the file (replacing the example data with your site's information):\n\n```\nWP_BASE_URL=https://yourwebsiteurl.tld\nWP_USERNAME=adminusername\nWP_PASSWORD=adminpassword\n```\n\n**This will store a password in a PLAIN TEXT which we know is wrong. Our internal use case uses this process with temporary sites which are regularly destroyed so the danger is acceptable to our use case. If you decide to use this process on a real website with real user information you have been warned that storing your production site's WP admin password in a plain text file in order to use this process is a bad idea. We recommend instead using environment variables to pass your password to the script later and removing the WP_PASSWORD from the `.llmsenv` file.**\n\n+ Save the file\n\n\n## Setup your production site\n\n+ Install and activate the LifterLMS plugin on your site\n\n\n## Run the tests\n\nThere are two ways to run the E2E tests:\n\n### Headless mode\n\nRuns the tests and shows you the results.\n\nIf errors are encountered, a screenshot of the page will be taken and saved in the `tmp/e2e-screenshots/` directory so you can see what the page looked like when things went sour.\n\nError logs will be output in your terminal to review.\n\nRun headless tests by executing `npm run tests` in your terminal.\n\n\n### Interactive mode\n\nLaunches an automated Chromium browser and runs the tests in \"slow motion\" so you can watch as the tests run.\n\nNo screenshots are takeng in interactive mode.\n\nError logs are output to the terminal for review.\n\nRun interactive tests by executing `npm run tests:dev` in your terminal.\n\n\n### Using environment variables\n\nIf you don't want to store you admin password in a plaintext file you can define the WP_PASSWORD variable at runtime `WP_PASSWORD=yourpassword npm run tests`\n"
  },
  {
    "path": "docs/installing.md",
    "content": "Installing for Development\n==========================\n\n## Requirements\n\nIn order to build and develop LifterLMS locally, you'll need the following:\n\n+ PHP\n+ MySQL / MariaDB\n+ [Composer](https://getcomposer.org/download/)\n+ [Node.js](https://nodejs.org/en/download/)\n+ [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)\n\n\n## Building LifterLMS\n\n### 1. Clone source from GitHub\n\n```sh\n$ git clone https://github.com/gocodebox/lifterlms\n$ cd lifterlms\n```\n\nIf you're planning to contribute code, you should fork this repository and clone your fork instead and switch to the dev branch before continuing the install.\n\n```sh\n$ git checkout dev\n```\n\n### 2. Install composer dependencies:\n\n```sh\n$ composer install\n```\n\n### 3. Install npm dependencies:\n\n```sh\n$ npm install --global gulp\n$ npm install\n```\n\n### 4. Build static assets\n\n```sh\n$ npm run build\n```\n\nThe `lifterlms` directory is now an installable plugin that can be moved into your local server's `wp-content/plugins` directory.\n\n\n## Running PHPCS\n\nWhen contributing you should ensure your contributions follow our [coding](./coding-standards.md) and [documentation](./documentation-standards.md) standards.\n\nTo check for errors and warnings in your code, run PHPCS:\n\n```sh\n$ composer run check-cs\n```\n\nTo check for errors only:\n\n```sh\n$ composer run check-cs-errors\n```\n\nThese reports may include issues that can be automatically fixed using PHPCBF:\n\n```sh\n$ composer run fix-cs\n```\n\n## Running Test Suites\n\nNew code should also strive to be covered by automated tests.\n\nLifterLMS has unit and integration tests via phpunit and End-to-End tests via Jest and Puppeteer.\n\nFor guides on running and contributing tests, see the relevant guides:\n\n+ [phpunit](../tests/phpunit/README.md)\n+ [e2e](../tests/e2e/README.md)\n"
  },
  {
    "path": "gulpfile.js/index.js",
    "content": "/**\n * Main Gulp File\n *\n * Requires all task files\n */\nvar gulp = require('gulp');\n\n// All custom tasks.\nrequire( './tasks/hacky-clean' );\nrequire( './tasks/js-additional' );\nrequire( './tasks/js-builder' );\n\n// All tasks from lib-tasks.\nrequire( 'lifterlms-lib-tasks' )( gulp );\n"
  },
  {
    "path": "gulpfile.js/tasks/hacky-clean.js",
    "content": "const gulp = require( 'gulp' ),\n\t{ unlinkSync } = require( 'fs' ),\n\tfilesToRemove = [\n\t\t'assets/css/dancing-script-rtl.css',\n\t\t'assets/css/imperial-script-rtl.css',\n\t\t'assets/css/pirata-one-rtl.css',\n\t\t'assets/css/unifraktur-maguntia-rtl.css',\n\t];\n\n/**\n * A hacky clean script that deletes RTL css files generated by the legacy styles-rtl tasks.\n *\n * The deleted files are webfont definition files which we don't need RTL stylesheets for.\n */\ngulp.task( 'hacky-clean', function( cb ) {\n\n\tfilesToRemove.forEach( file => {\n\t\tunlinkSync( file );\n\t} );\n\n\treturn cb();\n} );\n"
  },
  {
    "path": "gulpfile.js/tasks/js-additional.js",
    "content": "var    gulp = require( 'gulp' )\n\t,  header = require( 'gulp-header' )\n\t, include = require( 'gulp-include' )\n\t,    maps = require( 'gulp-sourcemaps' )\n\t,    pump = require( 'pump' )\n\t,  rename = require( 'gulp-rename' )\n\t,  uglify = require( 'gulp-uglify' )\n\t, gulpignore = require( 'gulp-ignore' )\n\n    ,         path = require( 'path' )\n;\n\ngulp.task( 'js-additional', function( cb ) {\n\n\tvar notice = [\n\t\t'/****************************************************************',\n \t\t' *',\n \t\t' * Contributor\\'s Notice',\n \t\t' * ',\n \t\t' * This is a compiled file and should not be edited directly!',\n \t\t' * The uncompiled script is located in the \"assets/private\" directory',\n \t\t' * ',\n \t\t' ****************************************************************/',\n \t\t'',\n \t\t'',\n \t];\n\n\tpump( [\n\t\tgulp.src( 'assets/js/private/**/*.js' ),\n\t\t\tinclude(),\n\t\t\tmaps.init(),\n\t\t\theader( notice.join( '\\n' ) ),\n\t\t\tmaps.write('../maps/js', { destPath: 'assets/js' } ),\n\t\t\tgulp.dest( 'assets/js' ),\n\n\t        // Don't pass maps any further.\n\t        gulpignore.exclude( file => '.js' !== path.extname( file.basename ) ),\n\n\t\t\tuglify(),\n\t\t\trename( {\n\t\t\t\tsuffix: '.min',\n\t\t\t} ),\n\t\t\tmaps.write('../maps/js', { destPath: 'assets/js' } ),\n\t\t\tgulp.dest( 'assets/js' )\n\t\t],\n\t\tcb\n\t);\n\n} );\n"
  },
  {
    "path": "gulpfile.js/tasks/js-builder.js",
    "content": "/**\n * -----------------------------------------------------------\n * js-builder\n * -----------------------------------------------------------\n * Compile Admin builder Javascript\n */\n\nvar   gulp              = require( 'gulp' )\n\t, requirejsOptimize = require( 'gulp-requirejs-optimize' )\n\t, rename            = require( 'gulp-rename' )\n\t, sourcemaps        = require( 'gulp-sourcemaps' )\n;\n\ngulp.task( 'js-builder', function( cb ) {\n\n\tgulp.src( 'assets/js/builder/main.js' )\n\t\t// unminified\n\t\t.pipe( sourcemaps.init() )\n\t\t.pipe( requirejsOptimize( function( file ) {\n\t\t\treturn {\n\t\t\t\tname: 'vendor/almond',\n\t\t\t\toptimize: 'none',\n\t\t\t\twrap: {\n\t\t\t\t\tstart: \"(function($){\",\n\t\t\t\t\tend: \"}(jQuery));\"\n\t\t\t\t},\n\t\t\t\tbaseUrl: 'assets/js/builder/',\n\t\t\t\tinclude: [ 'main' ],\n\t\t\t\tpreserveLicenseComments: false\n\t\t\t};\n\t\t} ).on( 'error', ( err ) => console.log( err ) ) )\n\t\t.pipe( rename( 'llms-builder.js' ) )\n\t\t.pipe( sourcemaps.write( '../maps/js', { destPath: 'assets/js' } ) )\n\t\t.pipe( gulp.dest( 'assets/js/' ) )\n\n\t\t// minified\n\t\t.pipe( sourcemaps.init() )\n\t\t.pipe( requirejsOptimize( function( file ) {\n\t\t\treturn {\n\t\t\t\tname: 'vendor/almond',\n\t\t\t\toptimize: 'uglify2',\n\t\t\t\twrap: {\n\t\t\t\t\tstart: \"(function($){\",\n\t\t\t\t\tend: \"}(jQuery));\"\n\t\t\t\t},\n\t\t\t\tbaseUrl: 'assets/js/builder/',\n\t\t\t\tinclude: [ 'main' ],\n\t\t\t\tpreserveLicenseComments: false\n\t\t\t};\n\t\t} ).on( 'error', ( err ) => console.log( err ) ) )\n\t\t.pipe( rename( 'llms-builder.min.js' ) )\n\t\t.pipe( sourcemaps.write( '../maps/js', { destPath: 'assets/js' } ) )\n\t\t.pipe( gulp.dest( 'assets/js/' ) );\n\n\tcb();\n\n});\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.admin.metabox.php",
    "content": "<?php\n/**\n * Admin Metabox Abstract.\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.0.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin metabox abstract class.\n *\n * @since 3.0.0\n * @since 3.35.0 Sanitize and verify nonce when saving metabox data.\n * @since 3.36.0 Allow quotes to be saved without being encoded for some special fields that store a shortcode.\n * @since 3.36.1 Improve `save()` method.\n * @since 3.37.12 Simplify `save()` by moving logic to sanitize and update posted data to `save_field()`.\n *                Add field sanitize option \"no_encode_quotes\" which functions like previous \"shortcode\" but is more semantically accurate.\n * @since 3.37.19 Bail if the global `$post` is empty, before registering our meta boxes.\n * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n */\nabstract class LLMS_Admin_Metabox {\n\n\t/**\n\t * Metabox ID.\n\t *\n\t * Define this in extending class's $this->configure() method.\n\t *\n\t * @var string\n\t */\n\tpublic $id;\n\n\t/**\n\t * Post Types this metabox should be added to.\n\t *\n\t * Can be a string of a single post type or an indexed array of multiple post types.\n\t * Define this in extending class's $this->configure() method.\n\t *\n\t * @var array\n\t */\n\tpublic $screens = array();\n\n\t/**\n\t * Title of the metabox.\n\t *\n\t * Define this in extending class's $this->configure() method.\n\t *\n\t * @var string\n\t */\n\tpublic $title;\n\n\t/**\n\t * Capability to check in order to display the metabox to the user.\n\t *\n\t * @var string\n\t */\n\tpublic $capability = 'edit_post';\n\n\t/**\n\t * Optional context to register the metabox with.\n\t *\n\t * Accepts anything that can be passed to WP core add_meta_box() function: 'normal', 'side', 'advanced'.\n\t *\n\t * Define this in extending class's $this->configure() method.\n\t *\n\t * @var string\n\t */\n\tpublic $context = 'normal';\n\n\t/**\n\t * Optional priority for the metabox.\n\t *\n\t * Accepts anything that can be passed to WP core add_meta_box() function: 'default', 'high', 'low'.\n\t *\n\t * Define this in extending class's $this->configure() method.\n\t *\n\t * @var string\n\t */\n\tpublic $priority = 'default';\n\n\t/**\n\t * Array of callback arguments passed to `add_meta_box()`.\n\t *\n\t * @var null\n\t */\n\tpublic $callback_args = null;\n\n\t/**\n\t * Instance of WP_Post for the current post.\n\t *\n\t * @var WP_Post\n\t */\n\tpublic $post;\n\n\t/**\n\t * Meta Key Prefix for all elements in the metabox.\n\t *\n\t * @var string\n\t */\n\tpublic $prefix = '_llms_';\n\n\t/**\n\t * Array of error messages to be displayed after an update attempt.\n\t *\n\t * @var string[]|WP_Error[]\n\t */\n\tprivate $errors = array();\n\n\t/**\n\t * Option keyname where error options are stored.\n\t *\n\t * @var string\n\t */\n\tprotected $error_opt_key = '';\n\n\t/**\n\t * HTML for the Metabox Content.\n\t *\n\t * Content handled by $this->process_fields().\n\t *\n\t * @var string\n\t */\n\tprivate $content = '';\n\n\t/**\n\t * HTML for the Metabox Navigation.\n\t *\n\t * Content handled by $this->process_fields().\n\t *\n\t * @var string\n\t */\n\tprivate $navigation = '';\n\n\t/**\n\t * The number of tabs registered to the metabox.\n\t *\n\t * This will be calculated automatically.\n\t *\n\t * Navigation will not display unless there's 2 or more tabs.\n\t *\n\t * @var integer\n\t */\n\tprivate $total_tabs = 0;\n\n\t/**\n\t * Metabox Version Number.\n\t *\n\t * @var integer\n\t */\n\tprivate $version = 1;\n\n\t/**\n\t * Used to prevent save action from running\n\t * multiple times on a single load.\n\t *\n\t * @since 7.5.0\n\t * @var bool\n\t */\n\tprivate $_saved;\n\n\t/**\n\t * Constructor.\n\t *\n\t * Configure the metabox and automatically add required actions.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Use `$this->error_opt_key()` in favor of hardcoded option name.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Allow child classes to configure variables.\n\t\t$this->configure();\n\n\t\t// Set the error option key.\n\t\t$this->error_opt_key = sprintf( 'lifterlms_metabox_errors%s', $this->id );\n\n\t\t// Register the metabox.\n\t\tadd_action( 'add_meta_boxes', array( $this, 'register' ) );\n\n\t\t// Register save actions for applicable screens (post types).\n\t\tforeach ( $this->get_screens() as $screen ) {\n\t\t\tadd_action( 'save_post_' . $screen, array( $this, 'save_actions' ), 10, 1 );\n\t\t}\n\n\t\t// Display errors.\n\t\tadd_action( 'admin_notices', array( $this, 'output_errors' ) );\n\n\t\t// Save errors.\n\t\tadd_action( 'shutdown', array( $this, 'save_errors' ) );\n\t}\n\n\t/**\n\t * Add an Error Message.\n\t *\n\t * @since 3.0.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @param string|WP_Error $error Error message text.\n\t * @return void\n\t */\n\tpublic function add_error( $error ) {\n\t\t$this->errors[] = $error;\n\t}\n\n\t/**\n\t * This function allows extending classes to configure required class properties.\n\t *\n\t * Properties $id, $title, and $screens should be configured in this function.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tabstract public function configure();\n\n\t/**\n\t * Retrieve stored metabox errors.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return string[]|WP_Error[]\n\t */\n\tpublic function get_errors() {\n\t\treturn get_option( $this->error_opt_key, array() );\n\t}\n\n\t/**\n\t * This function is where extending classes can configure all the fields within the metabox.\n\t *\n\t * The function must return an array which can be consumed by the \"output\" function.\n\t *\n\t * @return array\n\t */\n\tabstract public function get_fields();\n\n\t/**\n\t * Normalizes $this->screens to ensure it's an array.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Remove unnecessary `else` condition.\n\t *\n\t * @return array\n\t */\n\tprivate function get_screens() {\n\t\tif ( is_string( $this->screens ) ) {\n\t\t\treturn array( $this->screens );\n\t\t}\n\t\treturn $this->screens;\n\t}\n\n\t/**\n\t * Determine if any errors have been added to the metabox.\n\t *\n\t * @since Unknown\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_errors() {\n\t\treturn count( $this->errors ) ? true : false;\n\t}\n\n\t/**\n\t * Generate and output the HTML for the metabox.\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t// Setup html for nav and content.\n\t\t$this->process_fields();\n\n\t\t// output the html.\n\t\techo '<div class=\"llms-mb-container\">';\n\t\t// only show tabbed navigation when there's more than 1 tab.\n\t\tif ( $this->total_tabs > 1 ) {\n\t\t\techo '<nav class=\"llms-nav-tab-wrapper llms-nav-style-tabs\"><ul class=\"tabs llms-nav-items\">' . wp_kses_post( $this->navigation ) . '</ul></nav>';\n\t\t}\n\t\tdo_action( 'llms_metabox_before_content', $this->id );\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped via process_fields().\n\t\techo $this->content;\n\t\tdo_action( 'llms_metabox_after_content', $this->id );\n\t\techo '</div>';\n\t\twp_nonce_field( 'lifterlms_save_data', 'lifterlms_meta_nonce' );\n\t}\n\n\t/**\n\t * Display the messages as a WP Admin Notice.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Load errors using `$this->get_errors()` instead of `get_option()`.\n\t * @since 6.0.0 Handle WP_Error objects.\n\t *\n\t * @return void\n\t */\n\tpublic function output_errors() {\n\n\t\t$errors = $this->get_errors();\n\n\t\tif ( empty( $errors ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tforeach ( $errors as $error ) {\n\t\t\tif ( is_wp_error( $error ) ) {\n\t\t\t\t$error = $error->get_error_message();\n\t\t\t}\n\t\t\techo '<div id=\"lifterlms_errors\" class=\"error\"><p>' . wp_kses_post( $error ) . '</p></div>';\n\t\t}\n\n\t\tdelete_option( $this->error_opt_key );\n\t}\n\n\t/**\n\t * Process fields to setup navigation and content with minimal PHP loops.\n\t *\n\t * Called by `$this->output()` before actually outputting html.\n\t *\n\t * @since 3.0.0\n\t * @since 3.16.14 Unknown.\n\t * @since 6.0.0 Move single field processing logic to a specific method {@see LLMS_Admin_Metabox::process_field()}.\n\t *\n\t * @return void\n\t */\n\tprivate function process_fields() {\n\n\t\t// Create a filter-safe ID that conforms to WordPress coding standards for hooks.\n\t\t$id = str_replace( '-', '_', $this->id );\n\n\t\t/**\n\t\t * Customize metabox fields prior to field processing.\n\t\t *\n\t\t * The dynamic portion of this filter, `$id`, corresponds to the classes `$id` property with\n\t\t * dashes (`-`) replaced with underscores (`_`). If the class id is \"my-metabox\" the filter would be\n\t\t * \"llms_metabox_fields_my_metabox\".\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array $fields Array of metabox fields.\n\t\t */\n\t\t$fields = apply_filters( \"llms_metabox_fields_{$id}\", $this->get_fields() );\n\n\t\t$this->total_tabs = count( $fields );\n\n\t\tforeach ( $fields as $i => $tab ) {\n\n\t\t\t++$i;\n\t\t\t$current = 1 === $i ? ' llms-active' : '';\n\n\t\t\t$this->navigation .= '<li class=\"llms-nav-item tab-link ' . esc_attr( $current ) . '\" data-tab=\"' . $this->id . '-tab-' . esc_attr( $i ) . '\"><span class=\"llms-nav-link\">' . wp_kses_post( $tab['title'] ) . '</span></li>';\n\n\t\t\t$this->content .= '<div id=\"' . $this->id . '-tab-' . $i . '\" class=\"tab-content' . esc_attr( $current ) . '\"><ul>';\n\n\t\t\tforeach ( $tab['fields'] as $field ) {\n\t\t\t\t$this->content .= $this->process_field( $field );\n\t\t\t}\n\n\t\t\t$this->content .= '</ul></div>';\n\n\t\t}\n\t}\n\n\t/**\n\t * Process single field.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $field Metabox field.\n\t * @return string\n\t */\n\tprotected function process_field( $field ) {\n\n\t\t$name = ucfirst(\n\t\t\tstrtr(\n\t\t\t\tpreg_replace_callback(\n\t\t\t\t\t'/(\\w+)/',\n\t\t\t\t\tfunction ( $m ) {\n\t\t\t\t\t\treturn ucfirst( $m[1] );\n\t\t\t\t\t},\n\t\t\t\t\t$field['type']\n\t\t\t\t),\n\t\t\t\t'-',\n\t\t\t\t'_'\n\t\t\t)\n\t\t);\n\n\t\t$field_class_name = str_replace( '{TOKEN}', $name, 'LLMS_Metabox_{TOKEN}_Field' );\n\t\t$field_class      = new $field_class_name( $field );\n\t\tob_start();\n\t\t$field_class->Output();\n\t\t$field_html = ob_get_clean();\n\t\tunset( $field_class );\n\n\t\treturn $field_html;\n\t}\n\n\t/**\n\t * Register the Metabox using WP Functions.\n\t *\n\t * This is called automatically by constructor.\n\t *\n\t * Utilizes class properties for registration.\n\t *\n\t * @since 3.0.0\n\t * @since 3.13.0 Unknown.\n\t * @since 3.37.19 Early bail if the global `$post` is empty.\n\t * @since 6.0.0 Pass callback arguments to `add_meta_box()`.\n\t *\n\t * @return void\n\t */\n\tpublic function register() {\n\n\t\tglobal $post;\n\n\t\tif ( empty( $post ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->post = $post;\n\n\t\tif ( current_user_can( $this->capability, $this->post->ID ) ) {\n\n\t\t\tadd_meta_box(\n\t\t\t\t$this->id,\n\t\t\t\t$this->title,\n\t\t\t\tarray( $this, 'output' ),\n\t\t\t\t$this->get_screens(),\n\t\t\t\t$this->context,\n\t\t\t\t$this->priority,\n\t\t\t\tis_callable( $this->callback_args ) ? ( $this->callback_args )() : $this->callback_args\n\t\t\t);\n\n\t\t}\n\t}\n\n\t/**\n\t * Save field data.\n\t *\n\t * Loops through fields and saves the data to postmeta.\n\t *\n\t * Called by $this->save_actions().\n\t *\n\t * This function is dumb. If the fields need to output error messages or do validation override\n\t * this method and create a custom save method to accommodate the validations or conditions.\n\t *\n\t * @since 3.0.0\n\t * @since 3.14.1 Unknown.\n\t * @since 3.35.0 Added nonce verification before processing data; only access `$_POST` data via `llms_filter_input()`.\n\t * @since 3.36.0 Allow quotes when sanitizing some special fields that store a shortcode.\n\t * @since 3.36.1 Check metabox capability during saves.\n\t *               Return an `int` depending on return condition.\n\t *               Automatically add `FILTER_REQUIRE_ARRAY` flag when sanitizing a `multi` field.\n\t * @since 3.37.12 Move field sanitization and updates to the `save_field()` method.\n\t * @since 6.0.0 Allow skipping the saving of a field.\n\t *\n\t * @param int $post_id WP Post ID of the post being saved.\n\t * @return int `-1` When no user or user is missing required capabilities or when there's no or invalid nonce.\n\t *             `0` during inline saves or ajax requests or when no fields are found for the metabox.\n\t *             `1` if fields were found. This doesn't mean there weren't errors during saving.\n\t */\n\tprotected function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) || ! current_user_can( $this->capability, $post_id ) ) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t// Return early during quick saves and ajax requests.\n\t\tif ( ( isset( $_POST['action'] ) && 'inline-save' === $_POST['action'] ) || llms_is_ajax() ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Get all defined fields.\n\t\t$id     = str_replace( '-', '_', $this->id );\n\t\t$fields = apply_filters( \"llms_metabox_fields_{$id}\", $this->get_fields() );\n\n\t\tif ( ! is_array( $fields ) ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Loop through the fields.\n\t\tforeach ( $fields as $group => $data ) {\n\n\t\t\t// Find the fields in each tab.\n\t\t\tif ( isset( $data['fields'] ) && is_array( $data['fields'] ) ) {\n\n\t\t\t\t// Loop through the fields.\n\t\t\t\tforeach ( $data['fields'] as $field ) {\n\t\t\t\t\t// Don't save things that don't have an ID or that are set to be skipped.\n\t\t\t\t\tif ( isset( $field['id'] ) && empty( $field['skip_save'] ) ) {\n\t\t\t\t\t\t$this->save_field( $post_id, $field );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn 1;\n\t}\n\n\t/**\n\t * Save a metabox field.\n\t *\n\t * @since 3.37.12\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 6.0.0 Move the DB saving in another method.\n\t *\n\t * @param int   $post_id WP_Post ID.\n\t * @param array $field   Metabox field array.\n\t * @return boolean\n\t */\n\tprotected function save_field( $post_id, $field ) {\n\n\t\t$val = '';\n\n\t\tif ( isset( $field['handler'] ) && method_exists( $this, 'handler_' . $field['handler'] ) ) {\n\t\t\treturn $this->{'handler_' . $field['handler']}( $post_id, $field, $_POST );\n\t\t}\n\n\t\tif ( ! isset( $_POST[ $field['id'] ] ) ) {\n\t\t\treturn $this->save_field_db( $post_id, $field['id'], $val );\n\t\t}\n\n\t\tif ( 'basic-editor' === $field['type'] ) {\n\t\t\t$val = wp_kses( $_POST[ $field['id'] ], LLMS_ALLOWED_HTML_PRICES );\n\t\t\treturn $this->save_field_db( $post_id, $field['id'], $val );\n\t\t}\n\n\t\t$flags = array();\n\n\t\tif ( isset( $field['sanitize'] ) && in_array( $field['sanitize'], array( 'shortcode', 'no_encode_quotes' ), true ) ) {\n\t\t\t$flags[] = FILTER_FLAG_NO_ENCODE_QUOTES;\n\t\t} elseif ( ! empty( $field['multi'] ) ) {\n\t\t\t$flags[] = FILTER_REQUIRE_ARRAY;\n\t\t}\n\n\t\t$val = llms_filter_input_sanitize_string( INPUT_POST, $field['id'], $flags );\n\n\t\treturn $this->save_field_db( $post_id, $field['id'], $val );\n\t}\n\n\tprotected function handler_instructors_mb_store( $post_id, $field, $request ) {\n\t\tif ( ! llms_current_user_can_edit_product( $post_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$post = llms_get_post( $post_id );\n\n\t\t$instructors = array();\n\n\t\t// TODO: This is assuming only one repeater on the page. Make the repeater use unique field names instead based on the ID.\n\n\t\t// We're going in order of the request array to get the order of the instructors, vs. going 1, 2, 3 for ID.\n\t\tforeach ( $request as $key => $val ) {\n\n\t\t\t// Check if key is in the format llms_id_{number}.\n\t\t\tif ( ! preg_match( '/^_llms_id_[0-9]+$/', $key ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$key_id = str_replace( '_llms_id_', '', $key );\n\n\t\t\t$instructor = array(\n\t\t\t\t'id'         => absint( $request[ '_llms_id_' . $key_id ] ),\n\t\t\t\t'label'      => $request[ '_llms_label_' . $key_id ],\n\t\t\t\t'visibility' => $request[ '_llms_visibility_' . $key_id ],\n\t\t\t);\n\n\t\t\t$instructors[] = $instructor;\n\t\t}\n\n\t\t$post->set_instructors( $instructors );\n\n\t\treturn true;\n\t}\n\n\n\t/**\n\t * Save field in the db.\n\t *\n\t * Expects an already sanitized value.\n\t *\n\t * @param int   $post_id  The WP Post ID.\n\t * @param int   $field_id The field identifier.\n\t * @param mixed $val      Value to save.\n\t * @return bool\n\t */\n\tprotected function save_field_db( $post_id, $field_id, $val ) {\n\t\treturn update_post_meta( $post_id, $field_id, $val ) ? true : false;\n\t}\n\n\t/**\n\t * Allows extending classes to perform additional save methods before the default save.\n\t *\n\t * Called before `$this->save()` during `$this->save_actions()`.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param int $post_id WP Post ID of the post being saved.\n\t * @return void\n\t */\n\tprotected function save_before( $post_id ) {}\n\n\t/**\n\t * Allows extending classes to perform additional save methods after the default save.\n\t *\n\t * Called after `$this->save()` during `$this->save_actions()`.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param int $post_id WP Post ID of the post being saved.\n\t * @return void\n\t */\n\tprotected function save_after( $post_id ) {}\n\n\t/**\n\t * Perform Save Actions.\n\t *\n\t * Triggers actions for before and after save and calls the save method which actually saves metadata.\n\t *\n\t * This is called automatically on save_post_{$post_type} for all screens defined in `$this->screens`.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param int $post_id WP Post ID of the post being saved.\n\t * @return void\n\t */\n\tpublic function save_actions( $post_id ) {\n\n\t\t// Prevent save action from running multiple times on a single load.\n\t\tif ( isset( $this->_saved ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->post = get_post( $post_id );\n\n\t\t$this->_saved = true;\n\t\tdo_action( 'llms_metabox_before_save_' . $this->id, $post_id, $this );\n\t\t$this->save_before( $post_id );\n\t\t$this->save( $post_id );\n\t\t$this->save_after( $post_id );\n\t\tdo_action( 'llms_metabox_after_save_' . $this->id, $post_id, $this );\n\t}\n\n\t/**\n\t * Save messages to the database.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Use `$this->error_opt_key()` in favor of hardcoded option name.\n\t *                Only save errors if errors have been added.\n\t *\n\t * @return void\n\t */\n\tpublic function save_errors() {\n\t\tif ( $this->has_errors() ) {\n\t\t\tupdate_option( $this->error_opt_key, $this->errors );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.admin.table.php",
    "content": "<?php\n/**\n * Admin Table Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.2.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Table abstract class.\n *\n * @since 3.2.0\n * @since 3.34.0 Added get_table_classes().\n * @since 3.37.7 Fix PHP 7.4 deprecation notice.\n */\nabstract class LLMS_Admin_Table extends LLMS_Abstract_Exportable_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table.\n\t *\n\t * @var string\n\t */\n\tprotected $id = '';\n\n\t/**\n\t * When pagination is enabled, the current page.\n\t *\n\t * @var integer\n\t */\n\tprotected $current_page = 1;\n\n\t/**\n\t * Value of the field being filtered by.\n\t *\n\t * @var string Only applicable if $filterby is set.\n\t */\n\tprotected $filter = '';\n\n\t/**\n\t * Field results are filtered by.\n\t *\n\t * @var string\n\t */\n\tprotected $filterby = '';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var bool\n\t */\n\tprotected $is_exportable = false;\n\n\t/**\n\t * When pagination enabled, determines if this is the last page of results.\n\t *\n\t * @var bool\n\t */\n\tprotected $is_last_page = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links.\n\t *\n\t * @var bool\n\t */\n\tprotected $is_paginated = false;\n\n\t/**\n\t * Determine if the table is filterable.\n\t *\n\t * @var bool\n\t */\n\tprotected $is_filterable = false;\n\n\t/**\n\t * If true will be a table with a larger font size.\n\t *\n\t * @var bool\n\t */\n\tprotected $is_large = false;\n\n\t/**\n\t * Determine of the table is searchable.\n\t *\n\t * @var bool\n\t */\n\tprotected $is_searchable = false;\n\n\t/**\n\t * If true, tbody will be zebra striped.\n\t *\n\t * @var bool\n\t */\n\tprotected $is_zebra = true;\n\n\t/**\n\t * If an integer supplied, used to jump to last page.\n\t *\n\t * @var int\n\t */\n\tprotected $max_pages = null;\n\n\t/**\n\t * Results sort order.\n\t *\n\t * @var string 'ASC' or 'DESC'.\n\t *             Only applicable of $orderby is not set.\n\t */\n\tprotected $order = '';\n\n\t/**\n\t * Field results are sorted by.\n\t *\n\t * @var string\n\t */\n\tprotected $orderby = '';\n\n\t/**\n\t * Number of records to display per page.\n\t *\n\t * @var int\n\t */\n\tprotected $per_page = -1;\n\n\t/**\n\t * The search query submitted for a searchable table.\n\t *\n\t * @var string\n\t */\n\tprotected $search = '';\n\n\t/**\n\t * Table Data.\n\t *\n\t * @var array Array of objects or arrays.\n\t *            Each item represents as row in the table's body, each item is a cell.\n\t */\n\tprotected $tbody_data = array();\n\n\t/**\n\t * Table Title Displayed on Screen.\n\t *\n\t * @var string\n\t */\n\tprotected $title = '';\n\n\t/**\n\t * Retrieve data for a cell.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @param string $key  The column ID/key.\n\t * @param mixed  $data Object/array of data that the function can use to extract the data.\n\t * @return mixed\n\t */\n\tabstract protected function get_data( $key, $data );\n\n\t/**\n\t * Execute a query to retrieve results from the table.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @param array $args Array of query args.\n\t * @return mixed\n\t */\n\tabstract public function get_results( $args = array() );\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method.\n\t *\n\t * @since 2.3.0\n\t *\n\t * @return array\n\t */\n\tabstract public function set_args();\n\n\t/**\n\t * Define the structure of the table.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return array\n\t */\n\tabstract protected function set_columns();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\t$this->title = $this->set_title();\n\t\t$this->register_hooks();\n\t}\n\n\t/**\n\t * Ensure that a valid array of data is passed to a query.\n\t *\n\t * Used by AJAX methods to clean unnecessary parameters before passing the request data\n\t * to the get_results function.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @param array $args Array of arguments\n\t * @return array\n\t */\n\tprotected function clean_args( $args = array() ) {\n\n\t\t$allowed = array_keys( $this->get_args() );\n\n\t\tforeach ( $args as $key => $val ) {\n\t\t\tif ( ! in_array( $key, $allowed ) ) {\n\t\t\t\tunset( $args[ $key ] );\n\t\t\t}\n\t\t}\n\n\t\treturn $args;\n\t}\n\n\t/**\n\t * Ensures that all data requested by $this->get_data is filterable\n\t * before being output on screen / in the export file.\n\t *\n\t * @since 3.2.0\n\t * @since 3.17.6 Unknown.\n\t *\n\t * @param mixed  $value   Value to be displayed.\n\t * @param string $key     Column key/ID.\n\t * @param mixed  $data    Original data object/array.\n\t * @param string $context Display context [display|export].\n\t * @return mixed\n\t */\n\tprotected function filter_get_data( $value, $key, $data, $context = 'display' ) {\n\t\t/**\n\t\t * Filters the table data.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.2.0\n\t\t *\n\t\t * @param mixed            $value        Value to be displayed.\n\t\t * @param string           $key          Column key/ID.\n\t\t * @param mixed            $data         Original data object/array.\n\t\t * @param string           $context      Display context [display|export].\n\t\t * @param LLMS_Admin_Table $table_object Instance of the class extending `LLMS_Admin_Table`.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_data_{$this->id}\", $value, $key, $data, $context, $this );\n\t}\n\n\t/**\n\t * Retrieve the arguments defined in `set_args`.\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Fix filter name.\n\t *\n\t * @return array\n\t */\n\tpublic function get_args() {\n\n\t\t$default = array(\n\t\t\t'page'    => $this->get_current_page(),\n\t\t\t'order'   => $this->get_order(),\n\t\t\t'orderby' => $this->get_orderby(),\n\t\t);\n\n\t\tif ( $this->is_filterable ) {\n\t\t\t$default['filter']   = $this->get_filter();\n\t\t\t$default['filterby'] = $this->get_filterby();\n\t\t}\n\n\t\tif ( $this->is_searchable ) {\n\t\t\t$default['search'] = $this->get_search();\n\t\t}\n\n\t\t$args = wp_parse_args( $this->set_args(), $default );\n\n\t\t/**\n\t\t * Filters the arguments used to build the query.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param array $args Arguments to build the query whose results will populate the table.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_args_{$this->id}\", $args );\n\t}\n\n\t/**\n\t * Retrieve the array of columns defined by set_columns.\n\n\t * @since 3.2.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $context Display context [display|export].\n\t * @return array\n\t */\n\tpublic function get_columns( $context = 'display' ) {\n\n\t\t/**\n\t\t * Filters the array of table columns.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.2.0\n\t\t *\n\t\t * @param array  $columns The array of table columns.\n\t\t * @param string $context Display context [display|export].\n\t\t */\n\t\t$cols = apply_filters( \"llms_table_get_{$this->id}_columns\", $this->set_columns(), $context );\n\n\t\tif ( $this->is_exportable ) {\n\n\t\t\tforeach ( $cols as $id => $data ) {\n\n\t\t\t\tif ( ! $this->is_col_visible( $data, $context ) ) {\n\t\t\t\t\tunset( $cols[ $id ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $cols;\n\t}\n\n\t/**\n\t * Get the current page.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_current_page() {\n\t\treturn $this->current_page;\n\t}\n\n\t/**\n\t * Get `$this->empty_msg` string.\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Fix filter name.\n\t *\n\t * @return string\n\t */\n\tpublic function get_empty_message() {\n\t\t/**\n\t\t * Filters the message displayed when the table is empty.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param string $columns The message displayed when the table is empty.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_{$this->id}_empty_message\", $this->set_empty_message() );\n\t}\n\n\t/**\n\t * Get the text for the default/placeholder for a filterable column.\n\t *\n\t * @since 3.4.0\n\t * @since 3.15.0 Fix filter name.\n\t * @since 7.3.0 Fixed typo in function name (`is_strinp` => `is_string` ).\n\t *\n\t * @param string $column_id The ID of the column.\n\t * @return string\n\t */\n\tpublic function get_filter_placeholder( $column_id, $column_data ) {\n\t\t$placeholder = __( 'Any', 'lifterlms' );\n\t\tif ( is_array( $column_data ) && isset( $column_data['title'] ) ) {\n\t\t\t/* translators: %s: Column title. */\n\t\t\t$placeholder = sprintf( __( 'Any %s', 'lifterlms' ), $column_data['title'] );\n\t\t} elseif ( is_string( $column_data ) ) {\n\t\t\t/* translators: %s: Column title. */\n\t\t\t$placeholder = sprintf( __( 'Any %s', 'lifterlms' ), $column_data );\n\t\t}\n\t\t/**\n\t\t * Filters the placeholder string for a filterable column.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param string $placeholder Placeholder string.\n\t\t * @param string $column_id   The ID of the column.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_{$this->id}_filter_placeholder\", $placeholder, $column_id );\n\t}\n\n\t/**\n\t * Get the current filter.\n\t *\n\t * @since 3.4.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_filter() {\n\t\treturn $this->filter;\n\t}\n\n\t/**\n\t * Get the current field results are filtered by.\n\t *\n\t * @since 3.4.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_filterby() {\n\t\treturn $this->filterby;\n\t}\n\n\t/**\n\t * Retrieve a modified classname that can be passed via AJAX for new queries.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_handler() {\n\t\treturn str_replace( 'LLMS_Table_', '', get_class( $this ) );\n\t}\n\n\t/**\n\t * Retrieve the max number of pages for the table.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_max_pages() {\n\t\treturn $this->max_pages;\n\t}\n\n\t/**\n\t * Get the current sort order.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_order() {\n\t\treturn $this->order;\n\t}\n\n\t/**\n\t * Get the current field results are ordered by.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_orderby() {\n\t\treturn $this->orderby;\n\t}\n\n\t/**\n\t * Get the current number of results to display per page.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_per_page() {\n\t\treturn $this->per_page;\n\t}\n\n\t/**\n\t * Gets the opposite of the current order.\n\t *\n\t * Used to determine what order should be displayed when resorting.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_new_order( $orderby = '' ) {\n\n\t\t// Current order matches submitted order, return opposite.\n\t\tif ( $this->orderby === $orderby ) {\n\t\t\treturn ( 'ASC' === $this->order ) ? 'DESC' : 'ASC';\n\t\t} else {\n\t\t\treturn 'ASC';\n\t\t}\n\t}\n\n\t/**\n\t * Retrieves the current search query.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_search() {\n\t\treturn esc_attr( trim( $this->search ) );\n\t}\n\n\t/**\n\t * Returns an array of CSS class names to use on this table.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_table_classes() {\n\t\t$classes = array(\n\t\t\t'llms-table',\n\t\t\t'llms-gb-table',\n\t\t\t'llms-gb-table-' . $this->id,\n\t\t);\n\n\t\tif ( $this->is_zebra ) {\n\t\t\t$classes[] = 'zebra';\n\t\t}\n\n\t\tif ( $this->is_large ) {\n\t\t\t$classes[] = 'size-large';\n\t\t}\n\n\t\t/**\n\t\t * Filters the CSS classes to use on the table.\n\t\t *\n\t\t * @since 3.34.0\n\t\t *\n\t\t * @param array $classes  CSS class names.\n\t\t * @param array $table_id Id property of this table object.\n\t\t */\n\t\treturn apply_filters( 'llms_table_get_table_classes', $classes, $this->id );\n\t}\n\n\t/**\n\t * Get HTML for the filters displayed in the head of the table.\n\t *\n\t * @since 3.4.0\n\t *\n\t * @return string\n\t * @deprecated 7.7.0 Use output_table_filters_html() instead.\n\t */\n\tpublic function get_table_filters_html() {\n\t\tob_start();\n\t\t$this->output_table_filters_html();\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output HTML for the filters displayed in the head of the table.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return string\n\t */\n\tpublic function output_table_filters_html() {\n\t\t?>\n\t\t<div class=\"llms-table-filters\">\n\t\t\t<?php foreach ( $this->get_columns() as $id => $data ) : ?>\n\t\t\t\t<?php if ( is_array( $data ) && isset( $data['filterable'] ) && is_array( $data['filterable'] ) ) : ?>\n\t\t\t\t\t<div class=\"llms-table-filter-wrap\">\n\t\t\t\t\t\t<select class=\"llms-select2 llms-table-filter\" id=\"<?php echo esc_attr( sprintf( '%1$s-%2$s-filter', $this->id, $id ) ); ?>\" name=\"<?php echo esc_attr( $id ); ?>\">\n\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $this->get_filter() ); ?>\"><?php echo esc_html( $this->get_filter_placeholder( $id, $data ) ); ?></option>\n\t\t\t\t\t\t\t<?php foreach ( $data['filterable'] as $val => $name ) : ?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $val ); ?>\"><?php echo esc_html( $name ); ?></option>\n\t\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</div>\n\t\t\t\t<?php endif; ?>\n\t\t\t<?php endforeach; ?>\n\t\t</div>\n\t\t<?php\n\t}\n\n\n\t/**\n\t * Return the HTML for the entire table.\n\t *\n\t * @since 3.2.0\n\t * @since 3.17.8 Unknown.\n\t * @since 3.37.7 Use correct argument order for implode to fix php 7.4 deprecation.\n\t * @since 7.8.0 Added button for clearing resumable attempts.\n\t *\n\t * @return string\n\t * @deprecated 7.7.0 Use output_table_html() instead.\n\t */\n\tpublic function get_table_html() {\n\t\tob_start();\n\t\t$this->output_table_html();\n\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output the HTML for the entire table.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_table_html() {\n\n\t\t$classes = $this->get_table_classes();\n\n\t\t?>\n\t\t<div class=\"llms-table-wrap\">\n\t\t\t<header class=\"llms-table-header\">\n\t\t\t\t<?php $this->output_table_title_html(); ?>\n\t\t\t\t<?php if ( $this->is_searchable ) : ?>\n\t\t\t\t\t<?php $this->output_table_search_form_html(); ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php if ( $this->is_filterable ) : ?>\n\t\t\t\t\t<?php $this->output_table_filters_html(); ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t</header>\n\t\t\t<table\n\t\t\t\tclass=\"<?php echo esc_attr( implode( ' ', $classes ) ); ?>\"\n\t\t\t\tdata-args='<?php echo esc_attr( wp_json_encode( $this->get_args() ) ); ?>'\n\t\t\t\tdata-handler=\"<?php echo esc_attr( $this->get_handler() ); ?>\"\n\t\t\t\tid=\"llms-gb-table-<?php echo esc_attr( $this->id ); ?>\"\n\t\t\t>\n\t\t\t\t<?php $this->output_thead_html(); ?>\n\t\t\t\t<?php $this->output_tbody_html(); ?>\n\t\t\t\t<?php $this->output_tfoot_html(); ?>\n\t\t\t</table>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Get the HTML of the search form for a searchable table.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t * @deprecated 7.7.0 Use output_table_search_form_html() instead.\n\t */\n\tpublic function get_table_search_form_html() {\n\t\tob_start();\n\t\t$this->output_table_search_form_html();\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output the HTML of the search form for a searchable table.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t */\n\tpublic function output_table_search_form_html() {\n\t\t?>\n\t\t<div class=\"llms-table-search\">\n\t\t\t<input class=\"regular-text\" id=\"<?php echo esc_attr( $this->id ); ?>-search-input\" placeholder=\"<?php echo esc_attr( $this->get_table_search_form_placeholder() ); ?>\" type=\"text\">\n\t\t</div>\n\t\t<?php\n\t}\n\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input.\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Fix filter name.\n\t *\n\t * @return string\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\t\t/**\n\t\t * Filters the text to be used as the placeholder in a searchable tables search input.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param string $text Text to be used as the placeholder in a searchable tables search input.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_{$this->id}_search_placeholder\", __( 'Search', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Get the HTML for the table's title.\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Unknown.\n\t *\n\t * @return string\n\t * @deprecated 7.7.0 Use output_table_title_html() instead.\n\t */\n\tpublic function get_table_title_html() {\n\t\tob_start();\n\t\t$this->output_table_title_html();\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output the HTML for the table's title.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_table_title_html() {\n\t\t$title = $this->get_title();\n\t\tif ( $title ) {\n\t\t\techo '<h2 class=\"llms-table-title\">' . esc_html( $title ) . '</h2>';\n\t\t}\n\t}\n\n\t/**\n\t * Get `$this->tbody_data` array.\n\n\t * @since 3.2.0\n\t * @since 3.15.0 Fix filter name.\n\t *\n\t * @return array\n\t */\n\tpublic function get_tbody_data() {\n\t\t/**\n\t\t * Filters the array of tbody data.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param array $tbody_data Array of data that will be used to create the table body.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_{$this->id}_tbody_data\", $this->tbody_data );\n\t}\n\n\t/**\n\t * Get a tbody element for the table.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t * @deprecated 7.7.0 Use output_tbody_html() instead.\n\t */\n\tpublic function get_tbody_html() {\n\t\tob_start();\n\t\t$this->output_tbody_html();\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output a tbody element for the table.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_tbody_html() {\n\t\t$data = $this->get_tbody_data();\n\t\t?>\n\t\t<tbody>\n\t\t<?php if ( $data ) : ?>\n\t\t\t<?php foreach ( $data as $row ) : ?>\n\t\t\t\t<?php $this->output_tr_html( $row ); ?>\n\t\t\t<?php endforeach; ?>\n\t\t<?php else : ?>\n\t\t\t<tr><td class=\"llms-gb-table-empty\" colspan=\"<?php echo esc_attr( $this->get_columns_count() ); ?>\"><p><?php echo esc_html( $this->get_empty_message() ); ?></p></td></tr>\n\t\t<?php endif; ?>\n\t\t</tbody>\n\t\t<?php\n\t}\n\n\t/**\n\t * Get a tfoot element for the table.\n\t *\n\t * @since 3.2.0\n\t * @since 3.28.0 Unknown.\n\t *\n\t * @return string\n\t * @deprecated 7.7.0 Use output_tfoot_html() instead.\n\t */\n\tpublic function get_tfoot_html() {\n\t\tob_start();\n\t\t$this->output_tfoot_html();\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output a tfoot element for the table.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_tfoot_html() {\n\t\t?>\n\t\t<tfoot>\n\t\t<tr>\n\t\t\t<th colspan=\"<?php echo esc_attr( $this->get_columns_count() ); ?>\">\n\t\t\t\t<?php if ( $this->has_resumable_attempts() ) : ?>\n\t\t\t\t\t<div class=\"llms-clear-resumable-attempts\">\n\t\t\t\t\t\t<form action=\"\" method=\"POST\">\n\t\t\t\t\t\t\t<button class=\"llms-button-primary small\" name=\"llms_quiz_resumable_attempt_action\" type=\"submit\" value=\"llms_clear_resumable_attempts\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-trash-o\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t\t<?php echo esc_html( __( 'Clear resumable attempts', 'lifterlms' ) ); ?>\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<input type=\"hidden\" name=\"llms_quiz_id\" value=\"<?php echo esc_attr( $this->quiz_id ); ?>\">\n\t\t\t\t\t\t\t<?php wp_nonce_field( 'llms_quiz_attempt_actions', '_llms_quiz_attempt_nonce' ); ?>\n\t\t\t\t\t\t</form>\n\t\t\t\t\t</div>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( $this->is_exportable ) : ?>\n\t\t\t\t\t<div class=\"llms-table-export\">\n\t\t\t\t\t\t<button class=\"llms-button-primary small\" name=\"llms-table-export\">\n\t\t\t\t\t\t\t<span class=\"dashicons dashicons-download\"></span> <?php esc_html_e( 'Export', 'lifterlms' ); ?>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<?php $this->output_progress_bar_html( 0 ); ?>\n\t\t\t\t\t\t<em><small class=\"llms-table-export-msg\"></small></em>\n\t\t\t\t\t</div>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( $this->is_paginated ) : ?>\n\t\t\t\t\t<div class=\"llms-table-pagination\">\n\t\t\t\t\t\t<?php if ( $this->max_pages ) : ?>\n\t\t\t\t\t\t\t<span class=\"llms-table-page-count\">\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t/* translators: %1$d: Current page, %2$d: Total page count. */\n\t\t\t\t\t\t\t\techo esc_html( sprintf( esc_html_x( '%1$d of %2$d', 'pagination', 'lifterlms' ), $this->current_page, $this->max_pages ) );\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php if ( 1 !== $this->get_current_page() ) : ?>\n\t\t\t\t\t\t\t<?php if ( $this->max_pages ) : ?>\n\t\t\t\t\t\t\t\t<button class=\"llms-button-primary small\" data-page=\"1\" name=\"llms-table-paging\"><span class=\"dashicons dashicons-arrow-left-alt\"></span> <?php esc_html_e( 'First', 'lifterlms' ); ?></button>\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t\t<button class=\"llms-button-primary small\" data-page=\"<?php echo esc_attr( $this->current_page - 1 ); ?>\" name=\"llms-table-paging\"><span class=\"dashicons dashicons-arrow-left-alt2\"></span> <?php esc_html_e( 'Back', 'lifterlms' ); ?></button>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php if ( ! $this->is_last_page ) : ?>\n\t\t\t\t\t\t\t<button class=\"llms-button-primary small\" data-page=\"<?php echo esc_attr( $this->current_page + 1 ); ?>\" name=\"llms-table-paging\"><?php esc_html_e( 'Next', 'lifterlms' ); ?> <span class=\"dashicons dashicons-arrow-right-alt2\"></span></button>\n\t\t\t\t\t\t\t<?php if ( $this->max_pages ) : ?>\n\t\t\t\t\t\t\t\t<button class=\"llms-button-primary small\" data-page=\"<?php echo esc_attr( $this->max_pages ); ?>\" name=\"llms-table-paging\"><?php esc_html_e( 'Last', 'lifterlms' ); ?> <span class=\"dashicons dashicons-arrow-right-alt\"></span></button>\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</div>\n\t\t\t\t<?php endif; ?>\n\t\t\t</th>\n\t\t</tr>\n\t\t</tfoot>\n\t\t<?php\n\t}\n\n\t/**\n\t * Check if any quiz has resumable attempts.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @return bool\n\t */\n\tpublic function has_resumable_attempts() {\n\t\tif ( 'quizzes' === llms_filter_input( INPUT_GET, 'tab' ) && 'attempts' === llms_filter_input( INPUT_GET, 'stab' ) ) {\n\t\t\t$admin_quiz_attempts = new LLMS_Controller_Admin_Quiz_Attempts();\n\t\t\t$quizzes             = $admin_quiz_attempts->get_resumable_attempts( $this->quiz_id );\n\t\t\treturn ! empty( $quizzes );\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get a thead element for the table.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return string\n\t * @deprecated 7.7.0 Use output_thead_html() instead.\n\t */\n\tpublic function get_thead_html() {\n\t\tob_start();\n\t\t$this->output_thead_html();\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output the thead element for the table.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_thead_html() {\n\t\t?>\n\t\t<thead>\n\t\t<tr>\n\t\t\t<?php foreach ( $this->get_columns() as $id => $data ) : ?>\n\t\t\t\t<th class=\"<?php echo esc_attr( $id ); ?>\">\n\t\t\t\t\t<?php if ( is_array( $data ) ) : ?>\n\t\t\t\t\t\t<?php if ( isset( $data['sortable'] ) && $data['sortable'] ) : ?>\n\t\t\t\t\t\t\t<a class=\"llms-sortable<?php echo ( $this->get_orderby() === $id ) ? ' active' : ''; ?>\" data-order=\"<?php echo esc_attr( $this->get_new_order( $id ) ); ?>\" data-orderby=\"<?php echo esc_attr( $id ); ?>\" href=\"#llms-gb-table-resort\">\n\t\t\t\t\t\t\t\t<?php echo esc_html( $data['title'] ); ?>\n\t\t\t\t\t\t\t\t<span class=\"dashicons dashicons-arrow-up asc\"></span>\n\t\t\t\t\t\t\t\t<span class=\"dashicons dashicons-arrow-down desc\"></span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t<?php echo esc_html( $data['title'] ); ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t<?php echo esc_html( $data ); ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</th>\n\t\t\t<?php endforeach; ?>\n\t\t</tr>\n\t\t</thead>\n\t\t<?php\n\t}\n\n\t/**\n\t * Get a CSS class list (as a string) for each TR.\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param mixed $row Object/array of data that the function can use to extract the data.\n\t * @return string\n\t */\n\tprotected function get_tr_classes( $row ) {\n\t\t/**\n\t\t * Filters the CSS class of a table row.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.24.0\n\t\t *\n\t\t * @param string $class CSS class list (as a string) for a given TR.\n\t\t * @param mixed  $row   Object/array of data that the function can use to extract the data.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_{$this->id}_tr_classes\", 'llms-table-tr', $row );\n\t}\n\n\t/**\n\t * Get the HTML for a single row in the body of the table.\n\t *\n\t * @since 3.2.0\n\t * @since 3.21.0 Fix action hooks names.\n\t *\n\t * @param mixed $row Array/object of data describing a single row in the table.\n\t * @return string\n\t * @deprecated 7.7.0 Use output_tr_html() instead.\n\t */\n\tpublic function get_tr_html( $row ) {\n\t\tob_start();\n\t\t$this->output_tr_html( $row );\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output the HTML for a single row in the body of the table.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param mixed $row Array/object of data describing a single row in the table.\n\t * @return void\n\t */\n\tpublic function output_tr_html( $row ) {\n\t\t/**\n\t\t * Fired before a table `<tr>`.\n\t\t *\n\t\t * @since 3.21.0\n\t\t *\n\t\t * @param string           $row          Array/object of data describing a single row in the table.\n\t\t * @param LLMS_Admin_Table $table_object Instance of the class extending `LLMS_Admin_Table`.\n\t\t */\n\t\tdo_action( 'llms_table_before_tr', $row, $this );\n\t\t?>\n\t\t<tr class=\"<?php echo esc_attr( $this->get_tr_classes( $row ) ); ?>\">\n\t\t\t<?php foreach ( $this->get_columns() as $id => $title ) : ?>\n\t\t\t\t<td class=\"<?php echo esc_attr( $id ); ?>\">\n\t\t\t\t\t<?php echo $this->get_data( $id, $row ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>\n\t\t\t\t</td>\n\t\t\t<?php endforeach; ?>\n\t\t</tr>\n\t\t<?php\n\t\t/**\n\t\t * Fired after a table `<tr>`.\n\t\t *\n\t\t * @since 3.21.0\n\t\t *\n\t\t * @param string           $row          Array/object of data describing a single row in the table.\n\t\t * @param LLMS_Admin_Table $table_object Instance of the class extending `LLMS_Admin_Table`.\n\t\t */\n\t\tdo_action( 'llms_table_after_tr', $row, $this );\n\t}\n\n\n\n\t/**\n\t * Get the total number of columns in the table.\n\t *\n\t * Useful for creating full width tds via colspan.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_columns_count() {\n\t\treturn count( $this->get_columns() );\n\t}\n\n\t/**\n\t * Get the HTML to output a progress bar within a td.\n\t *\n\t * Improve ugly tables with a small visual flourish.\n\t * Useful when displaying a percentage within a table!\n\t * Bonus if the table sorts by that percentage column.\n\t *\n\t * @since 3.4.1\n\t *\n\t * @param float  $percentage The percentage to be displayed.\n\t * @param string $text       Text to display over the progress bar, defaults to $percentage.\n\t * @return string\n\t * @deprecated 7.7.0 Use output_progress_bar_html() instead.\n\t */\n\tpublic function get_progress_bar_html( $percentage, $text = '' ) {\n\t\tob_start();\n\t\t$this->output_progress_bar_html( $percentage, $text );\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Output the HTML to output a progress bar within a td.\n\t *\n\t * Improve ugly tables with a small visual flourish.\n\t * Useful when displaying a percentage within a table!\n\t * Bonus if the table sorts by that percentage column.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param float  $percentage The percentage to be displayed.\n\t * @param string $text       Text to display over the progress bar, defaults to $percentage.\n\t * @return void\n\t */\n\tpublic function output_progress_bar_html( $percentage, $text = '' ) {\n\t\t$text = $text ? $text : $percentage . '%';\n\t\t?>\n\t\t<div class=\"llms-table-progress\">\n\t\t\t<div class=\"llms-table-progress-bar\"><div class=\"llms-table-progress-inner\" style=\"width:<?php echo esc_attr( $percentage ); ?>%\"></div></div>\n\t\t\t<span class=\"llms-table-progress-text\"><?php echo esc_html( $text ); ?></span>\n\t\t</div>\n\t\t<?php\n\t}\n\n\n\t/**\n\t * Get the HTML for a WP Post Link.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @param int    $post_id WP Post ID.\n\t * @param string $text    Optional text to display within the anchor, if none supplied $post_id if used.\n\t * @return string\n\t */\n\tpublic function get_post_link( $post_id, $text = '' ) {\n\t\tif ( ! $text ) {\n\t\t\t$text = $post_id;\n\t\t}\n\t\treturn '<a href=\"' . esc_url( get_edit_post_link( $post_id ) ) . '\">' . $text . '</a>';\n\t}\n\n\t/**\n\t * Get the title of the table.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\t/**\n\t\t * Filters the table title.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param string $title The title of the table.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_{$this->id}_table_title\", $this->title );\n\t}\n\n\t/**\n\t * Get the HTML for a WP User Link.\n\t *\n\t * @since 3.17.2\n\t *\n\t * @param int    $user_id WP User ID.\n\t * @param string $text    Optional text to display within the anchor, if none supplied $user_id if used.\n\t * @return string\n\t */\n\tpublic function get_user_link( $user_id, $text = '' ) {\n\t\tif ( ! $text ) {\n\t\t\t$text = $user_id;\n\t\t}\n\t\treturn '<a href=\"' . esc_url( get_edit_user_link( $user_id ) ) . '\">' . $text . '</a>';\n\t}\n\n\t/**\n\t * Determine if a column is visible based on the current context.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param array  $data    Array of a single column's data from `set_columns()`.\n\t * @param string $context Context [display|export].\n\t * @return bool\n\t */\n\tprivate function is_col_visible( $data, $context = 'display' ) {\n\n\t\t// Display if 'export_only' does not exist or it does exist and is false.\n\t\tif ( 'display' === $context ) {\n\t\t\treturn ( ! isset( $data['export_only'] ) || ! $data['export_only'] );\n\n\t\t\t// Display if exportable is set and is true.\n\t\t} elseif ( 'export' === $context ) {\n\t\t\treturn ( isset( $data['exportable'] ) && $data['exportable'] );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return protected is_last_page var.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_last_page() {\n\t\treturn $this->is_last_page;\n\t}\n\n\t/**\n\t * Allow custom hooks to be registered for use within the class.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return void\n\t */\n\tprotected function register_hooks() {}\n\n\t/**\n\t * Setter.\n\t *\n\t * @since 2.3.0\n\t *\n\t * @param string $key Variable name.\n\t * @param mixed  $val Variable data.\n\t * @return void\n\t */\n\tpublic function set( $key, $val ) {\n\t\t$this->$key = $val;\n\t}\n\n\t/**\n\t * Empty message displayed when no results are found.\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Fix filter name.\n\t *\n\t * @return string\n\t */\n\tprotected function set_empty_message() {\n\t\t/**\n\t\t * Filters the default message displayed when the table is empty.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param string $columns The default message displayed when the table is empty.\n\t\t */\n\t\treturn apply_filters( 'llms_table_default_empty_message', __( 'No results were found.', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Stub used to set the title during table construction.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\treturn '';\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.analytics.widget.php",
    "content": "<?php\n/**\n * Analytics Widget Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.0.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Analytics Widget abstract class.\n *\n * @since 3.0.0\n * @since 3.30.3 Define undefined properties.\n * @since 3.33.1 In `set_order_data_query()` always set $order_clause variable to avoid PHP notices.\n * @since 3.35.0 Sanitize input data from reporting filters.\n * @since 3.36.3 Avoid warnings on using wpdb::prepare without placeholders.\n * @since 4.0.0 Remove previously deprecated class properties: `$date_end`, `$date_start`, & `$date_end`.\n */\nabstract class LLMS_Analytics_Widget {\n\n\t/**\n\t * @var array\n\t * @since 3.0.0\n\t */\n\tpublic $chart_data;\n\n\t/**\n\t * @var bool\n\t * @since 3.0.0\n\t */\n\tpublic $charts = false;\n\n\t/**\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tpublic $message = '';\n\n\t/**\n\t * One of the wpdb constants: OBJECT, OBJECT_K, ARRAY_A, or ARRAY_N\n\t *\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tprotected $output_type;\n\n\t/**\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tprotected $prepared_query;\n\t/**\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tprotected $query;\n\n\t/**\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tprotected $query_function;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tprotected $query_vars;\n\n\t/**\n\t * @var int\n\t * @since 3.0.0\n\t */\n\tpublic $response;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tpublic $results = array();\n\n\t/**\n\t * @var bool\n\t * @since 3.0.0\n\t */\n\tpublic $success = false;\n\n\tabstract protected function format_response();\n\tabstract protected function set_query();\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count',\n\t\t\t'header' => array(\n\t\t\t\t'id'    => '',\n\t\t\t\t'label' => '',\n\t\t\t\t'type'  => 'string',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function __construct() {}\n\n\t/**\n\t * Retrieve posted dates.\n\t *\n\t * @since 3.0.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string[]|string\n\t */\n\tprotected function get_posted_dates() {\n\n\t\t$dates = llms_filter_input_sanitize_string( INPUT_POST, 'dates', array( FILTER_REQUIRE_ARRAY ) );\n\t\treturn $dates ? $dates : '';\n\t}\n\n\tprotected function get_posted_courses() {\n\n\t\t$courses = llms_filter_input( INPUT_POST, 'courses', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\treturn $courses ? $courses : array();\n\t}\n\n\tprotected function get_posted_memberships() {\n\n\t\t$memberships = llms_filter_input( INPUT_POST, 'memberships', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\treturn $memberships ? $memberships : array();\n\t}\n\n\tprotected function get_posted_posts() {\n\t\treturn array_merge( $this->get_posted_courses(), $this->get_posted_memberships() );\n\t}\n\n\tprotected function get_posted_students() {\n\t\t$students = llms_filter_input( INPUT_POST, 'students', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\treturn $students ? $students : array();\n\t}\n\n\tprotected function get_prepared_query() {\n\t\treturn $this->prepared_query;\n\t}\n\n\tprotected function get_query() {\n\t\treturn $this->query;\n\t}\n\n\tprotected function get_query_vars() {\n\t\treturn $this->query_vars;\n\t}\n\n\tprotected function get_results() {\n\t\treturn $this->results;\n\t}\n\n\tprotected function format_date( $date, $type ) {\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'start':\n\t\t\t\t$date .= ' 00:00:00';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'end':\n\t\t\t\t/**\n\t\t\t\t * Return 00:00:00 on the next day after this date, using PHP datetime functions to avoid issues with daylight savings time or leap years.\n\t\t\t\t *\n\t\t\t\t * 23:59:59 is not a safe way to capture the end of day in newer versions of MySQL, if the transaction happened at (say) 23:59:59.999.\n\t\t\t\t */\n\t\t\t\t$end_date = new DateTime( $date );\n\t\t\t\t$end_date->modify( '+1 day' );\n\n\t\t\t\t$date = $end_date->format( 'Y-m-d' ) . ' 00:00:00';\n\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $date;\n\t}\n\n\tprotected function is_error() {\n\n\t\treturn ( $this->success ) ? false : true;\n\t}\n\n\tprotected function set_order_data_query( $args = array() ) {\n\n\t\textract(\n\t\t\twp_parse_args(\n\t\t\t\t$args,\n\t\t\t\tarray(\n\t\t\t\t\t'select'         => array( '*' ),\n\t\t\t\t\t'date_range'     => true, // whether or not to add a \"where\" for the posted date range.\n\t\t\t\t\t'date_field'     => 'post_date',\n\t\t\t\t\t'query_function' => 'get_results', // query function to pass to $wpdb->query().\n\t\t\t\t\t'output_type'    => OBJECT,\n\t\t\t\t\t'joins'          => array(), // array of JOIN statements.\n\t\t\t\t\t'statuses'       => array(), // array of order statuses to query.\n\t\t\t\t\t'wheres'         => array(), // array of \"WHERE\" statements.\n\t\t\t\t\t'order'          => 'ASC',\n\t\t\t\t\t'orderby'        => '',\n\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->query_function = $query_function;\n\t\t$this->query_vars     = array();\n\t\t$this->output_type    = $output_type;\n\n\t\tglobal $wpdb;\n\n\t\t// setup student join & where clauses.\n\t\t$students       = $this->get_posted_students();\n\t\t$students_join  = '';\n\t\t$students_where = '';\n\t\tif ( $students ) {\n\t\t\t$students_join   = \"JOIN {$wpdb->postmeta} AS m1 ON orders.ID = m1.post_id\";\n\t\t\t$students_where .= \"AND m1.meta_key = '_llms_user_id'\";\n\t\t\t$students_where .= ' AND m1.meta_value IN ( ' . implode( ', ', $students ) . ' )';\n\t\t}\n\n\t\t// setup post (product) joins & where clauses.\n\t\t$posts          = $this->get_posted_posts();\n\t\t$products_join  = '';\n\t\t$products_where = '';\n\t\tif ( $posts ) {\n\t\t\t$products_join   = \"JOIN {$wpdb->postmeta} AS m2 ON orders.ID = m2.post_id\";\n\t\t\t$products_where .= \"AND m2.meta_key = '_llms_product_id'\";\n\t\t\t$products_where .= ' AND m2.meta_value IN ( ' . implode( ', ', $posts ) . ' )';\n\t\t}\n\n\t\t$order_dates = '';\n\t\tif ( $date_range ) {\n\t\t\t$dates              = $this->get_posted_dates();\n\t\t\t$order_dates        = \"AND orders.{$date_field} >= CAST( %s as DATETIME ) AND orders.{$date_field} < CAST( %s as DATETIME )\";\n\t\t\t$this->query_vars[] = $this->format_date( $dates['start'], 'start' );\n\t\t\t$this->query_vars[] = $this->format_date( $dates['end'], 'end' );\n\t\t}\n\n\t\t// setup post status conditions in the where clause.\n\t\t$post_statuses = '';\n\t\tif ( $statuses ) {\n\t\t\t$post_statuses .= ' AND ( ';\n\t\t\tforeach ( $statuses as $i => $status ) {\n\t\t\t\tif ( $i > 0 ) {\n\t\t\t\t\t$post_statuses .= ' OR ';\n\t\t\t\t}\n\t\t\t\t$post_statuses     .= 'post_status = %s';\n\t\t\t\t$this->query_vars[] = $status;\n\t\t\t}\n\t\t\t$post_statuses .= ' )';\n\t\t}\n\n\t\t// setup the select clause.\n\t\t$select_clause = '';\n\t\tforeach ( $select as $i => $s ) {\n\t\t\tif ( $i > 0 ) {\n\t\t\t\t$select_clause .= ', ';\n\t\t\t}\n\t\t\t$select_clause .= $s;\n\t\t}\n\n\t\t$joins_clause = '';\n\t\tforeach ( $joins as $join ) {\n\t\t\t$joins_clause .= $join . \"\\r\\n\";\n\t\t}\n\n\t\t$wheres_clause = '';\n\t\tforeach ( $wheres as $where ) {\n\t\t\t$wheres_clause .= $where . \"\\r\\n\";\n\t\t}\n\n\t\t$order_clause = '';\n\t\tif ( $order && $orderby ) {\n\t\t\t$order_clause = 'ORDER BY ' . $orderby . ' ' . $order;\n\t\t}\n\n\t\t$this->query = \"SELECT {$select_clause}\n\t\t\t\t\t\tFROM {$wpdb->posts} AS orders\n\t\t\t\t\t\t{$students_join}\n\t\t\t\t\t\t{$products_join}\n\t\t\t\t\t\t{$joins_clause}\n\t\t\t\t\t\tWHERE orders.post_type = 'llms_order'\n\t\t\t\t\t\t\t{$order_dates}\n\t\t\t\t\t\t\t{$post_statuses}\n\t\t\t\t\t\t\t{$students_where}\n\t\t\t\t\t\t\t{$products_where}\n\t\t\t\t\t\t\t{$wheres_clause}\n\t\t\t\t\t\t{$order_clause}\n\t\t\t\t\t\t;\";\n\t}\n\n\t/**\n\t * Perform the query.\n\t *\n\t * @since unknown.\n\t * @since 3.36.3 Avoid warnings on using wpdb::prepare without placeholders.\n\t */\n\tprotected function query() {\n\n\t\tglobal $wpdb;\n\n\t\t// Roughly avoid warnings on using wpdb::prepare without placeholders.\n\t\t// The following strpos simple check is the same wpdb::prepare() does to check the correct usage.\n\t\tif ( strpos( $this->query, '%' ) === false || empty( $this->query_vars ) ) {\n\t\t\t$query = $this->query;\n\t\t} else {\n\t\t\t// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- It is prepared.\n\t\t\t$query = $wpdb->prepare( $this->query, $this->query_vars );\n\t\t\t// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared\n\t\t}\n\n\t\t// no output options.\n\t\tif ( in_array( $this->query_function, array( 'get_var', 'get_col' ), true ) ) {\n\t\t\t$this->results = $wpdb->{$this->query_function}( $query );\n\t\t} else {\n\t\t\t$this->results = $wpdb->{$this->query_function}( $query, $this->output_type );\n\t\t}\n\n\t\t$this->prepared_query = trim( str_replace( array( \"\\r\", \"\\n\", \"\\t\", '  ' ), ' ', $wpdb->last_query ) );\n\n\t\tif ( ! $wpdb->last_error ) {\n\n\t\t\t$this->success = true;\n\t\t\t$this->message = 'success';\n\n\t\t} else {\n\n\t\t\t$this->message = $wpdb->last_error;\n\n\t\t}\n\t}\n\n\t/**\n\t * Whether or not the current widget can be processed/displayed.\n\t *\n\t * @since 7.3.0\n\t *\n\t * @return true|WP_Error True if the widget can be processed, `WP_Error` otherwise.\n\t */\n\tprotected function _can_be_processed() { // phpcs:ignore -- PSR2.Methods.MethodDeclaration.Underscore.\n\n\t\t$can_be_processed = true;\n\t\tif ( ! current_user_can( 'view_others_lifterlms_reports' ) ) {\n\t\t\t$can_be_processed = new WP_Error(\n\t\t\t\tWP_Http::FORBIDDEN, // 403.\n\t\t\t\tesc_html__( 'You are not authorized to access the requested widget', 'lifterlms' )\n\t\t\t);\n\t\t}\n\n\t\treturn $can_be_processed;\n\t}\n\n\t/**\n\t * Whether or not the current widget can be processed/displayed.\n\t *\n\t * @since 7.3.0\n\t *\n\t * @return true|WP_Error\n\t */\n\tpublic function can_be_processed() {\n\n\t\t$widget_name = str_replace(\n\t\t\tarray( 'llms_analytics_', '_widget' ),\n\t\t\t'',\n\t\t\tstrtolower( get_class( $this ) )\n\t\t);\n\n\t\t/**\n\t\t * Whether or not the current widget can be processed/displayed.\n\t\t *\n\t\t * @param true|WP_Error         True if the widget can be processed, `WP_Error` otherwise.\n\t\t * @param string                The widget name.\n\t\t * @param LLMS_Analytics_Widget The instance extending `LLMS_Analytics_Widget`.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_can_analytics_widget_be_processed',\n\t\t\t$this->_can_be_processed(),\n\t\t\t$widget_name,\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Output widget.\n\t *\n\t * @since 3.0.0\n\t * @since 7.3.0 Use `wp_json_encode` in place of the deprecated `json_encode`.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t$this->set_query();\n\t\t$this->query();\n\n\t\t$this->response = $this->format_response();\n\n\t\tif ( $this->charts ) {\n\t\t\t$this->chart_data = $this->get_chart_data();\n\t\t}\n\n\t\theader( 'Content-Type: application/json' );\n\t\techo wp_json_encode( $this );\n\t\twp_die();\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.database.query.php",
    "content": "<?php\n/**\n * Database Query Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.8.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Database Query abstract class.\n *\n * @since 3.8.0\n * @since 3.30.3 `is_last_page()` method returns `true` when no results are found.\n * @since 3.34.0 Sanitizes sort parameters.\n */\nabstract class LLMS_Database_Query extends LLMS_Abstract_Query {\n\n\t/**\n\t * Identify the extending query.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'database';\n\n\t/**\n\t * SQL query used to count total found results.\n\t *\n\t * Set by subclasses in prepare_query() from the same clause\n\t * variables (FROM, JOIN, WHERE) used for the main query.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $count_query = '';\n\n\t/**\n\t * Retrieve query argument default values.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function default_arguments() {\n\n\t\treturn wp_parse_args(\n\t\t\tarray(\n\t\t\t\t'per_page' => 25,\n\t\t\t\t'sort'     => array(\n\t\t\t\t\t'id' => 'ASC',\n\t\t\t\t),\n\t\t\t),\n\t\t\tparent::default_arguments()\n\t\t);\n\t}\n\n\t/**\n\t * Escape and add quotes to a string, useful for array mapping when building queries.\n\t *\n\t * @since 3.8.0\n\t * @since 6.0.0 Use {@see llms_esc_and_quote_str()}.\n\t *\n\t * @param mixed $input Input data.\n\t * @return string\n\t */\n\tpublic function escape_and_quote_string( $input ) {\n\t\treturn llms_esc_and_quote_str( $input );\n\t}\n\n\t/**\n\t * Retrieve default arguments for the query.\n\t *\n\t * @since 3.8.0\n\t * @since 4.5.1 Added new default arg `no_found_rows` set to false.\n\t * @since 6.0.0 Call parent method.\n\t *\n\t * @todo This should be removed in favor of the parent method only when the\n\t *       `llms_db_query_get_default_args` hook is removed.\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_args() {\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $this->default_arguments();\n\t\t}\n\n\t\t// Get them from the parent with the new replacement filter.\n\t\t$args = parent::get_default_args();\n\n\t\t/**\n\t\t * Filters the query default args.\n\t\t *\n\t\t * @since 3.8.0\n\t\t * @deprecated 6.0.0 Filter `llms_db_query_get_default_args` is deprecated in favor of `llms_{$this->id}_query_get_default_args`.\n\t\t *\n\t\t * @param array $args Array of default arguments to set up the query with.\n\t\t */\n\t\treturn apply_filters_deprecated( 'llms_db_query_get_default_args', array( $args ), '6.0.0', \"llms_{$this->id}_query_get_default_args\" );\n\t}\n\n\t/**\n\t * Get a string used as filter names unique to the extending query.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @todo Deprecate.\n\t *\n\t * @param string $filter Filter name.\n\t * @return string\n\t */\n\tprotected function get_filter( $filter ) {\n\t\treturn 'llms_' . $this->id . '_query_' . $filter;\n\t}\n\n\t/**\n\t * Get the number of results to skip for the query based on the current page and per_page vars.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return int\n\t */\n\tprotected function get_skip() {\n\t\treturn absint( ( $this->get( 'page' ) - 1 ) * $this->get( 'per_page' ) );\n\t}\n\n\t/**\n\t * Performs the SQL query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array An integer-keyed array of row objects.\n\t */\n\tprotected function perform_query() {\n\n\t\tglobal $wpdb;\n\t\treturn $wpdb->get_results( $this->query ); // phpcs:ignore: WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared\n\t}\n\n\t/**\n\t * Set variables related to total number of results and pages possible with supplied arguments.\n\t *\n\t * @since 3.8.0\n\t * @since 4.5.1 Bail early if the query arg `no_found_rows` is true, b/c no reason to calculate anything.\n\t * @deprecated 6.0.0 `LLMS_Database_Query::set_found_results()` is deprecated.\n\t *\n\t * @return void\n\t */\n\tprotected function set_found_results() {\n\n\t\t_deprecated_function( 'LLMS_Database_Query::set_found_results()', '6.0.0' );\n\n\t\t// If no results, or found rows not required, bail early b/c no reason to calculate anything.\n\t\tif ( ! $this->number_results || $this->get( 'no_found_rows' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->found_results = $this->found_results();\n\t\t$this->max_pages     = absint( ceil( $this->found_results / $this->get( 'per_page' ) ) );\n\t}\n\n\t/**\n\t * Retrieve the total number of found results for the given query.\n\t *\n\t * Uses a separate COUNT(*) query built from the same SQL clauses as the\n\t * main query, set by subclasses in prepare_query().\n\t *\n\t * @since 6.0.0\n\t * @since 10.0.0 Replaced FOUND_ROWS() with $this->count_query.\n\t *\n\t * @return int\n\t */\n\tprotected function found_results() {\n\n\t\tglobal $wpdb;\n\n\t\tif ( empty( $this->count_query ) ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn (int) $wpdb->get_var( $this->count_query ); // db call ok; no-cache ok.\n\t}\n\n\t/**\n\t * Retrieve the prepared SQL for the SELECT clause.\n\t *\n\t * @since 4.5.1\n\t * @since 10.0.0 Removed SQL_CALC_FOUND_ROWS; found results are now counted via a separate query.\n\t *\n\t * @param string $select_columns Optional. Columns to select. Default '*'.\n\t * @return string\n\t */\n\tprotected function sql_select_columns( $select_columns = '*' ) {\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $select_columns;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query SELECT columns.\n\t\t *\n\t\t * The dynamic part of the filter `$this->id` identifies the extending query.\n\t\t *\n\t\t * @since 4.5.1\n\t\t *\n\t\t * @param string              $select_columns Columns to select.\n\t\t * @param LLMS_Database_Query $db_query       Instance of LLMS_Database_Query.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->id}_query_select_columns\", $select_columns, $this );\n\t}\n\n\t/**\n\t * Retrieve the prepared SQL for the LIMIT clause.\n\t *\n\t * @since 3.16.0\n\t * @since 4.5.1 Drop use of `$this->get_filter('limit')` in favor of `\"llms_{$this->id}_query_limit\"`.\n\t * @since 10.0.0 Returns empty string for count_only queries.\n\t *\n\t * @return string\n\t */\n\tprotected function sql_limit() {\n\n\t\tif ( $this->get( 'count_only' ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tglobal $wpdb;\n\n\t\t$sql = $wpdb->prepare( 'LIMIT %d, %d', $this->get_skip(), $this->get( 'per_page' ) );\n\n\t\t/**\n\t\t * Filters the query LIMIT clause.\n\t\t *\n\t\t * The dynamic part of the filter `$this->id` identifies the extending query.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param string              $sql      The LIMIT clause of the query.\n\t\t * @param LLMS_Database_Query $db_query The LLMS_Database_Query instance.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->id}_query_limit\", $sql, $this );\n\t}\n\n\t/**\n\t * Retrieve the prepared SQL for the ORDER BY clause.\n\t *\n\t * @since 3.8.0\n\t * @since 3.34.0 Returns an empty string if no sort fields are available.\n\t * @since 4.5.1 Drop use of `$this->get_filter('orderby')` in favor of `\"llms_{$this->id}_query_orderby\"`.\n\t *\n\t * @return string\n\t */\n\tprotected function sql_orderby() {\n\t\t$sql = '';\n\n\t\t// No point in ordering if we're just counting.\n\t\tif ( $this->get( 'count_only' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t$sort = $this->get( 'sort' );\n\t\tif ( $sort ) {\n\n\t\t\t$sql = 'ORDER BY';\n\n\t\t\t$comma = false;\n\n\t\t\tforeach ( $sort as $orderby => $order ) {\n\t\t\t\t$pre   = ( $comma ) ? ', ' : ' ';\n\t\t\t\t$sql  .= $pre . sanitize_sql_orderby( \"{$orderby} {$order}\" );\n\t\t\t\t$comma = true;\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query ORDER BY clause.\n\t\t *\n\t\t * The dynamic part of the filter `$this->id` identifies the extending query.\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param string              $sql      The ORDER BY clause of the query.\n\t\t * @param LLMS_Database_Query $db_query The LLMS_Database_Query instance.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->id}_query_orderby\", $sql, $this );\n\t}\n\n\t/**\n\t * Execute a query.\n\t *\n\t * Overrides the parent to detect if a filter re-added SQL_CALC_FOUND_ROWS\n\t * to the query, and falls back to FOUND_ROWS() if so.\n\t *\n\t * Also warns when a subclass does not set $this->count_query, which means\n\t * get_found_results() and get_max_pages() will return 0.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function query() {\n\n\t\tparent::query();\n\n\t\t$has_sql_calc = ! $this->get( 'suppress_filters' ) &&\n\t\t\tis_string( $this->query ) &&\n\t\t\tstr_contains( $this->query, 'SQL_CALC_FOUND_ROWS' );\n\n\t\tif ( $has_sql_calc ) {\n\t\t\t_deprecated_argument(\n\t\t\t\t\"llms_{$this->id}_query_prepare_query\",\n\t\t\t\t'[version]',\n\t\t\t\t'SQL_CALC_FOUND_ROWS should no longer be added via filters. Results are now counted with a separate COUNT query.'\n\t\t\t);\n\n\t\t\tglobal $wpdb;\n\t\t\t$this->found_results = (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ); // db call ok; no-cache ok.\n\t\t\t$this->max_pages     = absint( ceil( $this->found_results / $this->get( 'per_page' ) ) );\n\t\t}\n\n\t\tif (\n\t\t\t$this->number_results &&\n\t\t\t! $this->get( 'no_found_rows' ) &&\n\t\t\t! $this->get( 'count_only' ) &&\n\t\t\tempty( $this->count_query ) &&\n\t\t\t! $has_sql_calc\n\t\t) {\n\t\t\t_doing_it_wrong(\n\t\t\t\tget_class( $this ) . '::prepare_query',\n\t\t\t\tsprintf(\n\t\t\t\t\t/* translators: %s: The query subclass name. */\n\t\t\t\t\t'Subclasses of LLMS_Database_Query should set $this->count_query in prepare_query() when no_found_rows is not true. %s does not set count_query, so get_found_results() and get_max_pages() will return 0.',\n\t\t\t\t\tget_class( $this )\n\t\t\t\t),\n\t\t\t\t'[version]'\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Gets information about properties that used to be public and have been replaced with public getters.\n\t *\n\t * Used by `__get()` and `__set()` and will be removed when these are properly removed in the next\n\t * major release.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function legacy_public_props() {\n\n\t\treturn array(\n\t\t\t// Property      => $0 = alternative prop or method, $1 = has replacement.\n\t\t\t'found_results'  => array( 'get_found_results', true ),\n\t\t\t'max_pages'      => array( 'get_max_pages', true ),\n\t\t\t'number_results' => array( 'get_number_results', true ),\n\t\t\t'query_vars'     => array( 'query_vars', false ),\n\t\t\t'results'        => array( 'get_results', true ),\n\t\t);\n\t}\n\n\t/**\n\t * Throws a deprecation message when a formerly public property is accessed directly.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $prop Property name.\n\t * @return void\n\t */\n\tprivate function public_prop_deprecation( $prop ) {\n\n\t\t$legacy_props = $this->legacy_public_props();\n\n\t\tlist( $val, $has_replacement ) = $legacy_props[ $prop ];\n\n\t\t$class     = get_called_class();\n\t\t$is_method = method_exists( $this, $val );\n\t\t$suffix    = $is_method ? '()' : '';\n\t\t_deprecated_function( esc_html( \"Public access to property {$class}::{$prop}\" ), '6.0.0', $has_replacement ? esc_html( \"{$class}::{$val}{$suffix}\" ) : '' );\n\t}\n\n\t/**\n\t * Preserve backwards compat for read access to formerly public and removed class properties.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $key Property key name.\n\t * @return mixed\n\t */\n\tpublic function __get( $key ) {\n\n\t\t// Handle formerly public properties.\n\t\t$legacy_props = $this->legacy_public_props();\n\t\tif ( array_key_exists( $key, $legacy_props ) ) {\n\t\t\t$this->public_prop_deprecation( $key );\n\t\t\t$val = $legacy_props[ $key ][0];\n\t\t\treturn method_exists( $this, $val ) ? $this->$val() : $this->$val;\n\t\t} elseif ( 'sql' === $key ) {\n\t\t\t$class = get_called_class();\n\t\t\t_deprecated_function( esc_html( \"Property {$class}::sql\" ), '6.0.0', esc_html( \"{$class}::get_query()\" ) );\n\t\t\treturn $this->query;\n\t\t}\n\t}\n\n\t/**\n\t * Preserve backwards compat for write access to formerly public and removed class properties.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $key Property name.\n\t * @param mixed  $val Property value.\n\t * @return void\n\t */\n\tpublic function __set( $key, $val ) {\n\n\t\t$legacy_props = $this->legacy_public_props();\n\t\tif ( array_key_exists( $key, $legacy_props ) ) {\n\t\t\t$this->public_prop_deprecation( $key );\n\t\t\t$this->$key = $val;\n\t\t} elseif ( 'sql' === $key ) {\n\t\t\t$class = get_called_class();\n\t\t\t_deprecated_function( esc_html( \"Property {$class}::sql\" ), '6.0.0', esc_html( \"{$class}::query\" ) );\n\t\t\t$this->query = $val;\n\t\t}\n\t}\n\n\t/**\n\t * Handle backwards compatibility for the misspelled (and removed) method `preprare_query()`.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $name Method name.\n\t * @param array  $args Arguments passed to the method.\n\t * @return void|string\n\t */\n\tpublic function __call( $name, $args ) {\n\t\tif ( 'preprare_query' === $name ) {\n\t\t\t$class = get_called_class();\n\t\t\t_deprecated_function( esc_html( \"{$class}::preprare_query()\" ), '6.0.0', esc_html( \"{$class}::prepare_query()\" ) );\n\t\t\treturn $this->prepare_query();\n\t\t}\n\t}\n\n\t/**\n\t * Prepare the query.\n\t *\n\t * Should return the query which will be used by `query()`.\n\t *\n\t * This *should* be an abstract method but is defined here for backwards compatibility\n\t * to preserve the previous method, `preprare_query()` (notice the misspelling).\n\t *\n\t * Once the `preprare_query()` method is fully removed in the next major release this\n\t * method can be removed in favor of the abstract from the parent class.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return mixed\n\t */\n\tprotected function prepare_query() {\n\t\tif ( method_exists( $this, 'preprare_query' ) ) {\n\t\t\t$class = get_called_class();\n\t\t\t_deprecated_function( esc_html( \"{$class}::preprare_query()\" ), '6.0.0', esc_html( \"{$class}::prepare_query()\" ) );\n\t\t\treturn $this->preprare_query();\n\t\t} else {\n\t\t\t_doing_it_wrong(\n\t\t\t\t__METHOD__,\n\t\t\t\t/* translators: %s: Method name. */\n\t\t\t\tesc_html( sprintf( __( \"Method '%s' not implemented. Must be overridden in subclass.\", 'lifterlms' ), __METHOD__ ) ),\n\t\t\t\t'6.0.0'\n\t\t\t);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.payment.gateway.php",
    "content": "<?php\n/**\n * LifterLMS Payment Gateways Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Payment Gateways abstract class\n *\n * @since 3.0.0\n * @since 4.0.0 Removed deprecated completed transaction message parameter output.\n * @since 5.3.0 Extend LLMS_Abstract_Options_Data for improved options interactions.\n */\nabstract class LLMS_Payment_Gateway extends LLMS_Abstract_Options_Data {\n\n\t/**\n\t * Optional gateway description for the admin panel\n\t *\n\t * @var string\n\t */\n\tpublic $admin_description = '';\n\n\t/**\n\t * Fields the gateway uses on the admin panel when displaying/editing an order\n\t *\n\t * @var array\n\t */\n\tpublic $admin_order_fields = array(\n\t\t'customer'     => false,\n\t\t'subscription' => false,\n\t\t'source'       => false,\n\t);\n\n\t/**\n\t * Optional gateway title for the admin panel\n\t *\n\t * @var string\n\t */\n\tpublic $admin_title = '';\n\n\t/**\n\t * Optional gateway description for the frontend\n\t *\n\t * @var string\n\t */\n\tpublic $description = '';\n\n\t/**\n\t * Order to display the gateway in on the frontend\n\t *\n\t * @var integer\n\t */\n\tpublic $display_order = 1;\n\n\t/**\n\t * Is the gateway enabled for payment processing?\n\t *\n\t * @var string\n\t */\n\tpublic $enabled = 'no';\n\n\t/**\n\t * Optional icon displayed on the frontend\n\t *\n\t * @var string\n\t */\n\tpublic $icon = '';\n\n\t/**\n\t * ID of the Payment Gateway, used internally\n\t *\n\t * @var string\n\t */\n\tpublic $id;\n\n\t/**\n\t * Logging status\n\t *\n\t * @var string\n\t */\n\tpublic $logging_enabled = '';\n\n\t/**\n\t * Option name prefix.\n\t *\n\t * @var string\n\t */\n\tprotected $option_prefix = 'llms_gateway_';\n\n\t/**\n\t * Array of supported gateway features.\n\t *\n\t * @var array\n\t */\n\tpublic $supports = array(\n\t\t'checkout_fields'           => false,\n\t\t'cc_save'                   => false,\n\t\t'refunds'                   => false,\n\t\t'single_payments'           => false,\n\t\t'recurring_payments'        => false,\n\t\t'recurring_retry'           => false,\n\t\t'test_mode'                 => false,\n\t\t'modify_recurring_payments' => null,\n\t);\n\n\t/**\n\t * Description of the gateway's test mode (if supported)\n\t *\n\t * @var string\n\t */\n\tpublic $test_mode_description = '';\n\n\t/**\n\t * Is test mode enabled?\n\t *\n\t * Can be modified by user on settings page if gateway supports \"test_mode\".\n\t *\n\t * @var string\n\t */\n\tpublic $test_mode_enabled = 'no';\n\n\t/**\n\t * Title of the gateway's test mode (if supported)\n\t *\n\t * @var string\n\t */\n\tpublic $test_mode_title = '';\n\n\t/**\n\t * Gateway title for the frontend\n\t *\n\t * @var string\n\t */\n\tpublic $title = '';\n\n\t/**\n\t * Strings to mask when writing debug logs.\n\t *\n\t * @var string[]\n\t */\n\tprotected $secure_strings = array();\n\n\t/**\n\t * Option's data version\n\t *\n\t * @var integer\n\t */\n\tprotected $version = 2;\n\n\t/**\n\t * Adds a string to the gateway's list of secure strings.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $string The string to add.\n\t * @return void\n\t */\n\tpublic function add_secure_string( $string ) {\n\t\t$this->secure_strings[] = (string) $string;\n\t}\n\n\t/**\n\t * This should be called by the gateway after verifying the transaction was completed successfully\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.0 Added access plan and query string checkout redirect settings.\n\t * @since 3.34.3 Use `llms_redirect_and_exit()` instead of `wp_redirect()` and `exit()`.\n\t * @since 3.37.18 Allow redirection to external domains by disabling \"safe\" redirects.\n\t *\n\t * @param LLMS_Order $order      Instance of an LLMS_Order object.\n\t * @param null       $deprecated Deprecated.\n\t * @return void\n\t */\n\tpublic function complete_transaction( $order, $deprecated = null ) {\n\n\t\t$this->log( $this->get_admin_title() . ' `complete_transaction()` started', $order );\n\n\t\t$redirect = $this->get_complete_transaction_redirect_url( $order );\n\n\t\t$this->log( $this->get_admin_title() . ' `complete_transaction()` finished', $redirect, $order );\n\n\t\t// Ensure notification processors get dispatched since shutdown wont be called.\n\t\tdo_action( 'llms_dispatch_notification_processors' );\n\n\t\t// Execute a redirect.\n\t\tllms_redirect_and_exit(\n\t\t\t$redirect,\n\t\t\tarray(\n\t\t\t\t'safe' => false,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * This should be called by AJAX-powered gateways after verifying that a transaction was completed successfully.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param LLMS_Order $order The order being processed.\n\t * @param array      $data  Data to add to the default success return array.\n\t * @return array {\n\t *     An array of return data. The actual return array may include additional data from the payment gateway.\n\t *\n\t *     @type string $redirect The complete transaction redirect URL.\n\t *     @type string $status   The status code, always 'SUCCESS'.\n\t * }\n\t */\n\tpublic function complete_transaction_ajax( $order, $data = array() ) {\n\n\t\t$data = wp_parse_args(\n\t\t\t$data,\n\t\t\tarray(\n\t\t\t\t'redirect' => $this->get_complete_transaction_redirect_url( $order ),\n\t\t\t\t'status'   => 'SUCCESS',\n\t\t\t)\n\t\t);\n\n\t\t// Ensure notification processors get dispatched since shutdown won't be called.\n\t\tdo_action( 'llms_dispatch_notification_processors' );\n\n\t\treturn $data;\n\t}\n\n\t/**\n\t * Whether the payment details are entered on an external payment page, or entered inline with the checkout form.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_external_payment_entry() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * Confirms a Payment.\n\t *\n\t * Called by {@see LLMS_Controller_Orders::confirm_pending_order} on confirm form submission.\n\t *\n\t * Some validation is performed before passing to this function, gateways should do further validation\n\t * on their own.\n\t *\n\t * This stub is not necessary to implement if the gateway doesn't have a payment confirmation step.\n\t *\n\t * For gateways which implement AJAX order processing, this function should return either a WP_Error or\n\t * a success array from {@see LLMS_Payment_Gateway::complete_transaction_ajax()}.\n\t *\n\t * For gateways which implement synchronous order processing through form submission, this function should\n\t * not return and should instead perform a redirect and / or output notices using {@see llms_add_notice()}.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param LLMS_Order $order Instance of the order being processed.\n\t * @return void|WP_Error|array\n\t */\n\tpublic function confirm_pending_order( $order ) {}\n\n\t/**\n\t * Get admin description for the gateway\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_admin_description() {\n\n\t\t/**\n\t\t * Filters a payment gateway's admin description.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $admin_description The admin description.\n\t\t * @param string $gateway_id        The payment gateway ID.\n\t\t */\n\t\treturn apply_filters( 'llms_get_gateway_admin_description', $this->admin_description, $this->id );\n\t}\n\n\t/**\n\t * Get the admin title for the gateway\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_admin_title() {\n\n\t\t/**\n\t\t * Filters a payment gateway's admin title.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $admin_title The admin title.\n\t\t * @param string $gateway_id  The payment gateway ID.\n\t\t */\n\t\treturn apply_filters( 'llms_get_gateway_admin_title', $this->admin_title, $this->id );\n\t}\n\n\t/**\n\t * Get data about the fields displayed on the admin panel when viewing an order\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return array[]\n\t */\n\tpublic function get_admin_order_fields() {\n\n\t\t$fields = array(\n\t\t\t'customer'     => array(\n\t\t\t\t'label'   => __( 'Customer ID', 'lifterlms' ),\n\t\t\t\t'enabled' => $this->admin_order_fields['customer'],\n\t\t\t\t'name'    => 'gateway_customer_id',\n\t\t\t),\n\t\t\t'source'       => array(\n\t\t\t\t'label'   => __( 'Source ID', 'lifterlms' ),\n\t\t\t\t'enabled' => $this->admin_order_fields['source'],\n\t\t\t\t'name'    => 'gateway_source_id',\n\t\t\t),\n\t\t\t'subscription' => array(\n\t\t\t\t'label'   => __( 'Subscription ID', 'lifterlms' ),\n\t\t\t\t'enabled' => $this->admin_order_fields['subscription'],\n\t\t\t\t'name'    => 'gateway_subscription_id',\n\t\t\t),\n\t\t);\n\n\t\t/**\n\t\t * Filters a payment gateway's admin title.\n\t\t *\n\t\t * @since 3.10.0\n\t\t *\n\t\t * @param array[] $fields     Array of admin order fields.\n\t\t * @param string  $gateway_id The payment gateway ID.\n\t\t */\n\t\treturn apply_filters( 'llms_get_gateway_admin_order_fields', $fields, $this->id );\n\t}\n\n\t/**\n\t * Get default gateway admin settings fields\n\t *\n\t * @since 3.0.0\n\t * @since 3.29.0 Unknown.\n\t *\n\t * @return array[]\n\t */\n\tpublic function get_admin_settings_fields() {\n\n\t\t$fields = array();\n\n\t\t$fields[] = array(\n\t\t\t'type'  => 'custom-html',\n\t\t\t'value' => '\n\t\t\t\t<h1>' . $this->get_admin_title() . '</h1>\n\t\t\t\t<p>' . $this->get_admin_description() . '</p>\n\t\t\t',\n\t\t);\n\n\t\t$fields[] = array(\n\t\t\t'autoload'     => true,\n\t\t\t'id'           => $this->get_option_name( 'enabled' ),\n\t\t\t/* translators: %s: Payment gateway title. */\n\t\t\t'desc'         => sprintf( _x( 'Enable %s', 'Payment gateway title', 'lifterlms' ), $this->get_admin_title() ),\n\t\t\t'desc_tooltip' => __( 'Checking this box will allow users to use this payment gateway.', 'lifterlms' ),\n\t\t\t'default'      => $this->get_enabled(),\n\t\t\t'title'        => __( 'Enable / Disable', 'lifterlms' ),\n\t\t\t'type'         => 'checkbox',\n\t\t);\n\n\t\t$fields[] = array(\n\t\t\t'id'      => $this->get_option_name( 'title' ),\n\t\t\t'desc'    => '<br>' . __( 'The title the user sees during checkout.', 'lifterlms' ),\n\t\t\t'default' => $this->get_title(),\n\t\t\t'title'   => __( 'Title', 'lifterlms' ),\n\t\t\t'type'    => 'text',\n\t\t);\n\n\t\t$fields[] = array(\n\t\t\t'id'      => $this->get_option_name( 'description' ),\n\t\t\t'desc'    => '<br>' . __( 'The description the user sees during checkout.', 'lifterlms' ),\n\t\t\t'default' => $this->get_description(),\n\t\t\t'title'   => __( 'Description', 'lifterlms' ),\n\t\t\t'type'    => 'text',\n\t\t);\n\n\t\tif ( $this->supports( 'test_mode' ) ) {\n\n\t\t\t$fields[] = array(\n\t\t\t\t'id'           => $this->get_option_name( 'test_mode_enabled' ),\n\t\t\t\t/* translators: %s: Payment gateway test mode title. */\n\t\t\t\t'desc'         => sprintf( _x( 'Enable %s', 'Payment gateway test mode title', 'lifterlms' ), $this->get_test_mode_title() ),\n\t\t\t\t'desc_tooltip' => $this->get_test_mode_description(),\n\t\t\t\t'default'      => $this->get_test_mode_enabled(),\n\t\t\t\t'title'        => $this->get_test_mode_title(),\n\t\t\t\t'type'         => 'checkbox',\n\t\t\t);\n\n\t\t}\n\n\t\t$fields[] = array(\n\t\t\t'id'           => $this->get_option_name( 'logging_enabled' ),\n\t\t\t'desc'         => __( 'Enable debug logging', 'lifterlms' ),\n\t\t\t/* Translators: %s: Debug log location. */\n\t\t\t'desc_tooltip' => sprintf( __( 'When enabled, debugging information will be logged to \"%s\"', 'lifterlms' ), llms_get_log_path( $this->get_id() ) ),\n\t\t\t'title'        => __( 'Debug Log', 'lifterlms' ),\n\t\t\t'type'         => 'checkbox',\n\t\t);\n\n\t\t/**\n\t\t * Filters the gateway's settings fields displayed on the admin panel\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param array[] $fields     Array of settings fields.\n\t\t * @param string  $gateway_id The payment gateway ID.\n\t\t */\n\t\treturn apply_filters( 'llms_get_gateway_settings_fields', $fields, $this->id );\n\t}\n\n\t/**\n\t * Get API mode\n\t *\n\t * If test is not supported will return \"live\".\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_api_mode() {\n\t\tif ( $this->supports( 'test_mode' ) && $this->is_test_mode_enabled() ) {\n\t\t\treturn 'test';\n\t\t}\n\t\treturn 'live';\n\t}\n\n\t/**\n\t * Calculates the url to redirect to on transaction completion.\n\t *\n\t * @since 3.30.0\n\t * @since 7.0.0 Retrieve the redirect URL from the INPUT_POST if not passed via INPUT_GET.\n\t *\n\t * @param LLMS_Order $order The order object.\n\t * @return string\n\t */\n\tprotected function get_complete_transaction_redirect_url( $order ) {\n\n\t\t// Get the redirect parameter from INPUT_GET.\n\t\t$redirect = urldecode( llms_filter_input( INPUT_GET, 'redirect', FILTER_VALIDATE_URL ) ?? '' );\n\n\t\t// Get the redirect parameter from INPUT_POST if not INPUT_GET redirect pased.\n\t\t$redirect = $redirect ? $redirect : llms_filter_input( INPUT_POST, 'redirect', FILTER_VALIDATE_URL );\n\n\t\t// Redirect to the product's permalink, if no redirect found yet.\n\t\t$redirect = $redirect ? $redirect : get_permalink( $order->get( 'product_id' ) );\n\n\t\t// Fallback to the account page if we don't have a url for some reason.\n\t\t$redirect = $redirect ? $redirect : get_permalink( llms_get_page_id( 'myaccount' ) );\n\n\t\t// Add order key to the url.\n\t\t$redirect = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'order-complete' => $order->get( 'order_key' ),\n\t\t\t),\n\t\t\tesc_url( $redirect )\n\t\t);\n\n\t\t// Redirection url on free checkout form.\n\t\t$quick_enroll_form      = llms_filter_input( INPUT_POST, 'form' );\n\t\t$free_checkout_redirect = llms_filter_input( INPUT_POST, 'free_checkout_redirect', FILTER_VALIDATE_URL );\n\n\t\tif ( get_current_user_id() && ( 'free_enroll' === $quick_enroll_form ) && $free_checkout_redirect ) {\n\t\t\t$redirect = $free_checkout_redirect;\n\t\t}\n\n\t\t/**\n\t\t * Filters the redirect on order completion.\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param string     $redirect The URL to redirect user to.\n\t\t * @param LLMS_Order $order    The order object.\n\t\t */\n\t\treturn esc_url( apply_filters( 'lifterlms_completed_transaction_redirect', $redirect, $order ) );\n\t}\n\n\t/**\n\t * Gateways can override this to return a URL to a customer permalink on the gateway's website\n\t *\n\t * If this is not defined, it will just return the supplied ID.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $customer_id Gateway's customer ID.\n\t * @param string $api_mode    Link to either the live or test site for the gateway, where applicable.\n\t * @return string\n\t */\n\tpublic function get_customer_url( $customer_id, $api_mode = 'live' ) {\n\t\treturn $customer_id;\n\t}\n\n\t/**\n\t * Get the frontend description setting\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_description() {\n\t\treturn $this->get_option( 'description' );\n\t}\n\n\t/**\n\t * Get the display order setting\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_display_order() {\n\t\treturn absint( $this->get_option( 'display_order' ) );\n\t}\n\n\t/**\n\t * Get the value of the enabled setting\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_enabled() {\n\t\treturn $this->get_option( 'enabled' );\n\t}\n\n\t/**\n\t * Get fields displayed on the checkout form\n\t *\n\t * Gateways should define this function if the gateway supports fields.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_fields() {\n\t\t/**\n\t\t * Filters the HTML of the gateway's checkout fields\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $fields     Fields HTML string.\n\t\t * @param string $gateway_id The payment gateway's ID.\n\t\t */\n\t\treturn apply_filters( 'llms_get_gateway_fields', '', $this->id );\n\t}\n\n\t/**\n\t * Get the icon displayed on the checkout form\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_icon() {\n\t\t/**\n\t\t * Filters the HTML of the gateway's checkout icon\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $icon       Icon HTML string.\n\t\t * @param string $gateway_id The payment gateway's ID.\n\t\t */\n\t\treturn apply_filters( 'llms_get_gateway_icon', $this->icon, $this->id );\n\t}\n\n\t/**\n\t * Get the gateway's ID\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_id() {\n\t\treturn $this->id;\n\t}\n\n\t/**\n\t * Retrieve an HTML link to a customer, subscription, or source URL\n\t *\n\t * If no URL provided returns the item value as string.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param string $item_key   The key of the item to retrieve a URL for.\n\t * @param string $item_value The value of the item to retrieve.\n\t * @param string $api_mode   The current api mode to retrieve the URL for.\n\t * @return string\n\t */\n\tpublic function get_item_link( $item_key, $item_value, $api_mode = 'live' ) {\n\n\t\tswitch ( $item_key ) {\n\n\t\t\tcase 'customer':\n\t\t\t\t$url = $this->get_customer_url( $item_value, $api_mode );\n\t\t\t\tbreak;\n\n\t\t\tcase 'subscription':\n\t\t\t\t$url = $this->get_subscription_url( $item_value, $api_mode );\n\t\t\t\tbreak;\n\n\t\t\tcase 'source':\n\t\t\t\t$url = $this->get_source_url( $item_value, $api_mode );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$url = $item_value;\n\n\t\t}\n\n\t\tif ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) {\n\t\t\treturn $item_value;\n\t\t}\n\n\t\treturn sprintf( '<a href=\"%1$s\" target=\"_blank\">%2$s</a>', $url, $item_value );\n\t}\n\n\t/**\n\t * Get the value of the logging setting\n\t *\n\t * @since 3.0.0\n\t * @since 7.0.0 Added the force filter, `llms_gateway_{$this->id}_logging_enabled`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_logging_enabled() {\n\t\t/**\n\t\t * Enables forcing the logging status for the gateway on or off.\n\t\t *\n\t\t * The dynamic portion of this hook, `{$this->id}`, refers to the gateway's ID.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param null|bool $forced The forced status. If `null`, the default status derived from the gateway options will be used.\n\t\t */\n\t\t$forced = apply_filters( \"llms_gateway_{$this->id}_logging_enabled\", null );\n\t\tif ( ! is_null( $forced ) ) {\n\t\t\treturn $forced ? 'yes' : 'no';\n\t\t}\n\t\treturn $this->get_option( 'logging_enabled' );\n\t}\n\n\t/**\n\t * Adds the gateway's registered secured strings to the default list of site-wide secure strings.\n\t *\n\t * This is the callback for the `llms_secure_strings` filter (called via `llms_log()`).\n\t *\n\t * @since 6.4.0\n\t * @since 7.0.0 Load strings from `retrieve_secure_strings()`.\n\t *\n\t * @param string[] $strings Array of secure strings.\n\t * @param string   $handle  The log handle.\n\t * @return string[]\n\t */\n\tpublic function get_secure_strings( $strings, $handle ) {\n\n\t\t// Don't add our strings to other log files.\n\t\tif ( $this->id !== $handle ) {\n\t\t\treturn $strings;\n\t\t}\n\n\t\treturn array_merge( $strings, $this->retrieve_secure_strings() );\n\t}\n\n\t/**\n\t * Gateways can override this to return a URL to a source permalink on the gateway's website\n\t *\n\t * If this is not defined, it will just return the supplied ID.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $source_id Gateway's source ID.\n\t * @param string $api_mode  Link to either the live or test site for the gateway, where applicable.\n\t * @return string\n\t */\n\tpublic function get_source_url( $source_id, $api_mode = 'live' ) {\n\t\treturn $source_id;\n\t}\n\n\t/**\n\t * Gateways can override this to return a URL to a subscription permalink on the gateway's website\n\t *\n\t * If this is not defined, it will just return the supplied ID.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $subscription_id Gateway's subscription ID.\n\t * @param string $api_mode        Link to either the live or test site for the gateway, where applicable.\n\t * @return string\n\t */\n\tpublic function get_subscription_url( $subscription_id, $api_mode = 'live' ) {\n\t\treturn $subscription_id;\n\t}\n\n\t/**\n\t * Get an array of features the gateway supports.\n\t *\n\t * @since 3.0.0\n\t * @since 7.0.0 Handle `modify_recurring_payments` depending on `recurring_payments`.\n\t *\n\t * @return array\n\t */\n\tpublic function get_supported_features() {\n\n\t\tif ( ! isset( $this->supports['modify_recurring_payments'] ) || is_null( $this->supports['modify_recurring_payments'] ) ) {\n\t\t\t$this->supports['modify_recurring_payments'] = $this->supports['recurring_payments'] ?? false;\n\t\t}\n\n\t\t/**\n\t\t * Filters the gateway's supported features array\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param array  $supports   Array of feature support.\n\t\t * @param string $gateway_id The payment gateway's ID.\n\t\t */\n\t\treturn apply_filters( 'llms_get_gateway_supported_features', $this->supports, $this->id );\n\t}\n\n\t/**\n\t * Get the description of test mode displayed on the admin panel\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_test_mode_description() {\n\t\treturn $this->test_mode_description;\n\t}\n\n\t/**\n\t * Get value of the test mode enabled setting\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_test_mode_enabled() {\n\t\treturn $this->get_option( 'test_mode_enabled' );\n\t}\n\n\t/**\n\t * Get the title of test mode displayed on the admin panel\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_test_mode_title() {\n\t\treturn $this->test_mode_title;\n\t}\n\n\t/**\n\t * Get gateway title setting\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\treturn $this->get_option( 'title' );\n\t}\n\n\t/**\n\t * Gateways can override this to return a URL to a transaction permalink on the gateway's website\n\t *\n\t * If this is not defined, it will just return the supplied ID.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $transaction_id Gateway's transaction ID.\n\t * @param string $api_mode       Link to either the live or test site for the gateway, where applicable.\n\t *\n\t * @return string\n\t */\n\tpublic function get_transaction_url( $transaction_id, $api_mode = 'live' ) {\n\t\treturn $transaction_id;\n\t}\n\n\t/**\n\t * Called when the Update Payment Method form is submitted from a single order view on the student dashboard\n\t *\n\t * Gateways should do whatever the gateway needs to do to validate the new payment method and save it to the order\n\t * so that future payments on the order will use this new source.\n\t *\n\t * This should be an abstract function but experience has taught me that no one will upgrade follow our instructions\n\t * and they'll end up with 500 errors and debug mode disabled and send me giant frustrated question marks.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param LLMS_Order $order     Order object.\n\t * @param array      $form_data Additional data passed from the submitted form (EG $_POST).\n\t *\n\t * @return null\n\t */\n\tpublic function handle_payment_source_switch( $order, $form_data = array() ) {\n\t\treturn llms_add_notice(\n\t\t\tsprintf(\n\t\t\t\t/* translators: %s: the title of the payment gateway. */\n\t\t\t\tesc_html__( 'The selected payment gateway \"%s\" does not support payment method switching.', 'lifterlms' ),\n\t\t\t\t$this->get_title()\n\t\t\t),\n\t\t\t'error'\n\t\t);\n\t}\n\n\t/**\n\t * Handle a Pending Order\n\t *\n\t * Called by LLMS_Controller_Orders->create_pending_order() on checkout form submission.\n\t *\n\t * All data will be validated before it's passed to this function.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param LLMS_Order        $order  The order being processed.\n\t * @param LLMS_Access_Plan  $plan   Access plan the order is built from.\n\t * @param LLMS_Student      $person The purchasing customer.\n\t * @param LLMS_Coupon|false $coupon Coupon used during order processing or `false` if none supplied.\n\t * @return void\n\t */\n\tabstract public function handle_pending_order( $order, $plan, $person, $coupon = false );\n\n\t/**\n\t * Called by scheduled actions to charge an order for a scheduled recurring transaction\n\t *\n\t * This function must be defined by gateways which support recurring transactions.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param LLMS_Order $order The order being processed.\n\t * @return mixed\n\t */\n\tpublic function handle_recurring_transaction( $order ) {}\n\n\t/**\n\t * Determine if the gateway is the default gateway\n\t *\n\t * This will be the FIRST gateway in the gateways that are enabled.\n\t *\n\t * @since 3.0.0\n\t * @since 5.3.0 Use `llms()` in favor of deprecated `LLMS()`.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_default_gateway() {\n\t\treturn ( $this->get_id() === llms()->payment_gateways()->get_default_gateway() );\n\t}\n\n\t/**\n\t * Determine if the gateway is enabled according to admin settings checkbox\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_enabled() {\n\t\treturn ( 'yes' === $this->get_enabled() ) ? true : false;\n\t}\n\n\t/**\n\t * Determine if test mode is enabled\n\t *\n\t * Returns false if gateway doesn't support test mode.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_test_mode_enabled() {\n\t\treturn ( 'yes' === $this->get_test_mode_enabled() ) ? true : false;\n\t}\n\n\t/**\n\t * Log messages if logging is enabled\n\t *\n\t * @since 3.0.0\n\t * @since 6.4.0 Load the gateway's `secure_option` settings into `llms_secure_strings` hook when logging.\n\t *\n\t * @return void\n\t */\n\tpublic function log() {\n\n\t\tif ( ! llms_parse_bool( $this->get_logging_enabled() ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tadd_filter( 'llms_secure_strings', array( $this, 'get_secure_strings' ), 10, 2 );\n\n\t\tforeach ( func_get_args() as $data ) {\n\t\t\tllms_log( $data, $this->get_id() );\n\t\t}\n\n\t\tremove_filter( 'llms_secure_strings', array( $this, 'get_secure_strings' ), 10, 2 );\n\t}\n\n\t/**\n\t * Get the value of an option from the database & fallback to default value if none found\n\t *\n\t * Optionally attempts to retrieve a secure key first, if secure key is provided.\n\t *\n\t * The behavior of this function differs slightly from the parent method in that the second argument\n\t * in this method allows lookup of the secure key value.\n\t *\n\t * Default options are autoloaded via the get_option_default_value() method.\n\t *\n\t * @since 3.0.0\n\t * @since 3.29.0 Added secure option lookup via option second parameter.\n\t *\n\t * @param string $key option Option name / key, eg: \"title\".\n\t * @param string $secure_key Secure option name / key, eg: \"TITLE\".\n\t * @return mixed\n\t */\n\tpublic function get_option( $key, $secure_key = false ) {\n\n\t\tif ( $secure_key ) {\n\t\t\t$secure_val = llms_get_secure_option( $secure_key );\n\t\t\tif ( false !== $secure_val ) {\n\t\t\t\treturn $secure_val; // Intentionally not filtered here.\n\t\t\t}\n\t\t}\n\n\t\t$val = parent::get_option( $key );\n\n\t\t/**\n\t\t * Filters the value of a gateway option\n\t\t *\n\t\t * The dynamic portion of the hook, `{$key}`, refers to the unprefixed\n\t\t * option name.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param mixed  $val        Option value.\n\t\t * @param string $gateway_id Payment gateway ID.\n\t\t */\n\t\treturn apply_filters( \"llms_get_gateway_{$key}\", $val, $this->id );\n\t}\n\n\t/**\n\t * Option default value autoloader\n\t *\n\t * This is a callback function for the WP core filter `default_option_{$option}`.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param mixed  $default_value        The default value. If no value is passed to `get_option()`, this will be an empty string.\n\t *                                     Otherwise it will be the default value passed to the method.\n\t * @param string $full_option_name     The full (prefixed) option name.\n\t * @param bool   $passed_default_value Whether or not a default value was passed to `get_option()`.\n\t * @return mixed The default option value.\n\t */\n\tpublic function get_option_default_value( $default_value, $full_option_name, $passed_default_value ) {\n\t\t$unprefixed = str_replace( $this->get_option_prefix(), '', $full_option_name );\n\t\treturn isset( $this->$unprefixed ) ? $this->$unprefixed : '';\n\t}\n\n\t/**\n\t * Retrieve a prefix for options\n\t *\n\t * Appends the gateway's ID to the default option prefix, eg: \"llms_gateway_manual_\".\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_option_prefix() {\n\t\treturn parent::get_option_prefix() . $this->id . '_';\n\t}\n\n\t/**\n\t * Called when refunding via a Gateway\n\t *\n\t * This function must be defined by gateways which support refunds.\n\t *\n\t * This function is called by LLMS_Transaction->process_refund().\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param LLMS_Transaction $transaction The transaction being refunded.\n\t * @param float            $amount      Amount to refund.\n\t * @param string           $note        Optional refund note to pass to the gateway.\n\t * @return mixed\n\t */\n\tpublic function process_refund( $transaction, $amount = 0, $note = '' ) {}\n\n\t/**\n\t * Retrieves a list of \"secure\" strings which should be anonymized if they're found within debug logs.\n\t *\n\t * This method will load the values of any gateway options with a `secure_option` declaration. Additional\n\t * strings can be added to the list using the `llms_get_gateway_secure_strings` filter or via the\n\t * gateway's `add_secure_string()` method.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return string[]\n\t */\n\tpublic function retrieve_secure_strings() {\n\n\t\t$gateway_strings = $this->secure_strings;\n\t\tforeach ( $this->get_admin_settings_fields() as $field ) {\n\n\t\t\tif ( empty( $field['id'] ) || empty( $field['secure_option'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$string = llms_get_secure_option( $field['secure_option'], '', $field['id'] );\n\t\t\tif ( empty( $string ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$gateway_strings[] = $string;\n\n\t\t}\n\n\t\t/**\n\t\t * Filters the list of the gateway's secure strings.\n\t\t *\n\t\t * @since 6.4.0\n\t\t *\n\t\t * @param string[] $gateway_strings List of secure strings for the payment gateway.\n\t\t * @param string   $id              The gateway ID.\n\t\t */\n\t\t$gateway_strings = apply_filters( 'llms_get_gateway_secure_strings', $gateway_strings, $this->id );\n\n\t\treturn array_values( array_unique( $gateway_strings ) );\n\t}\n\n\t/**\n\t * Determine if a feature is supported by the gateway.\n\t *\n\t * Looks at the $this->supports and ensures the submitted feature exists and is true.\n\t *\n\t * @since 3.0.0\n\t * @since 7.0.0 Added `$order` param, to be used when the feature also depends on an order property.\n\t *\n\t * @param string     $feature Name of the supported feature.\n\t * @param LLMS_Order $order   Instance of an LLMS_Order.\n\t * @return boolean\n\t */\n\tpublic function supports( $feature, $order = null ) {\n\n\t\t$supports = $this->get_supported_features();\n\n\t\tif ( isset( $supports[ $feature ] ) && $supports[ $feature ] ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determine if an access plan can be processed by the gateway.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param LLMS_Access_Plan $plan  Instance of an LLMS_Access_Plan.\n\t * @param LLMS_Order       $order Instance of an LLMS_Order. Used to check whether a payment can be switched using this gateway.\n\t *                                In that case, in fact, we have to rely on the access plan information contained in the order\n\t *                                at the moment of its creation.\n\t * @return boolean\n\t */\n\tpublic function can_process_access_plan( $plan, $order = null ) {\n\t\t/**\n\t\t * Filters whether or not a gateway can process a specific access plan.\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @param bool             $can_process_plan Whether or not the gateway can process a specific access plan.\n\t\t * @param LLMS_Access_Plan $plan             Access plan object.\n\t\t * @param LLMS_Order       $plan             Order object.\n\t\t * @param string           $id               The gateway ID.\n\t\t */\n\t\treturn apply_filters( 'llms_can_gateway_process_access_plan', $plan || $order, $plan, $order, $this->id );\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.post.model.php",
    "content": "<?php\n/**\n * LLMS_Post_Model abstract class file\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.0.0\n * @version 7.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Post_Model abstract class.\n *\n * Defines base methods and properties for programmatically interfacing with LifterLMS Custom Post Types.\n *\n * @property      string  $author           ID of post author.\n * @property      string  $content          The post's content.\n * @property      string  $date             The post's local publication time.\n * @property-read string  $db_post_type     Name of the post type as stored in the database\n *                                          This will be prefixed (where applicable)\n *                                          ie: \"llms_order\" for the \"llms_order\" post type\n * @property      string  $excerpt          The post's excerpt.\n * @property-read int     $id               Post ID.\n * @property      int     $menu_order       A field used for ordering posts.\n * @property-read string  $meta_prefix      A prefix to add to all meta properties\n *                                          Child classes can redefine this\n * @property-read string  $model_post_type  Define this in extending classes\n *                                          Allows models to use unprefixed post type names for filters and more\n *                                          ie: \"order\" for the \"llms_order\" post type\n * @property      string  $modified         The post's local modified time.\n * @property      string  $name             The post's slug.\n * @property      int     $parent           WP_Post ID of the post's parent post.\n * @property-read WP_Post $post             Instance of WP_Post\n * @property      string  $status           The post's status.\n * @property      string  $title            The post's title.\n * @property      string  $type             The post's type, like post or page.\n *\n * @since 3.0.0\n * @since 3.30.0 Improve handling of custom field data to `toArrayCustom()`.\n * @since 3.30.2 Add filter to allow 3rd parties to prevent a field from being added to the custom field array.\n * @since 3.30.3 Use `wp_slash()` when creating new posts.\n * @since 3.31.0 Treat `post_excerpt` fields as HTML instead of plain text.\n * @since 3.34.0 Add parameter to the `get()` method in order to get raw properties.\n * @since 3.34.0 Add `comment_status`, `ping_status`, `date_gmt`, `modified_gmt`, `menu_order`, 'post_password` as gettable\\settable post properties.\n * @since 3.34.0 Add `set_bulk()` method that will allow to update an object at once given an array of properties.\n * @since 3.34.0 Refresh the whole $post property with the just updated instance of WP_Post after updating it.\n * @since 3.36.1 In `set_bulk()` method, use WP_Error::$errors in place of WP_Error::has_errors() to support WordPress version prior to 5.1.\n */\nabstract class LLMS_Post_Model implements JsonSerializable {\n\n\t/**\n\t * Name of the post type as stored in the database\n\t * This will be prefixed (where applicable)\n\t * ie: \"llms_order\" for the \"llms_order\" post type\n\t *\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tprotected $db_post_type;\n\n\t/**\n\t * WP Post ID\n\t *\n\t * @var int\n\t * @since 3.0.0\n\t */\n\tprotected $id;\n\n\t/**\n\t * Define this in extending classes\n\t *\n\t * Allows models to use unprefixed post type names for filters and more\n\t * ie: \"order\" for the \"llms_order\" post type.\n\t *\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tprotected $model_post_type;\n\n\t/**\n\t * A prefix to add to all meta properties\n\t *\n\t * Child classes can redefine this.\n\t *\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tprotected $meta_prefix = '_llms_';\n\n\t/**\n\t * Instance of WP_Post\n\t *\n\t * @var WP_Post\n\t * @since 3.0.0\n\t */\n\tprotected $post;\n\n\t/**\n\t * Array of meta properties and their property type\n\t *\n\t * @var array\n\t * @since 3.3.0\n\t */\n\tprotected $properties = array();\n\n\t/**\n\t * Array of default property values\n\t *\n\t * In the form of key => default value.\n\t *\n\t * @var array\n\t * @since 3.24.0\n\t */\n\tprotected $property_defaults = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * Setup ID and related post property.\n\t *\n\t * @since 3.0.0\n\t * @since 3.13.0 Unknown.\n\t *\n\t * @param string|int|LLMS_Post_Model|WP_Post $model 'new', WP post id, instance of an extending class, instance of WP_Post.\n\t * @param array                              $args  Args to create the post, only applies when $model is 'new'.\n\t * @return void\n\t */\n\tpublic function __construct( $model, $args = array() ) {\n\n\t\tif ( 'new' === $model ) {\n\t\t\t$model = $this->create( $args );\n\t\t\tif ( ! is_wp_error( $model ) ) {\n\t\t\t\t$created = true;\n\t\t\t}\n\t\t} else {\n\t\t\t$created = false;\n\t\t}\n\n\t\tif ( empty( $model ) || is_wp_error( $model ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_numeric( $model ) ) {\n\n\t\t\t$this->id   = absint( $model );\n\t\t\t$this->post = get_post( $this->id );\n\n\t\t} elseif ( is_subclass_of( $model, 'LLMS_Post_Model' ) ) {\n\n\t\t\t$this->id   = absint( $model->id );\n\t\t\t$this->post = $model->post;\n\n\t\t} elseif ( $model instanceof WP_Post && isset( $model->ID ) ) {\n\n\t\t\t$this->id   = absint( $model->ID );\n\t\t\t$this->post = $model;\n\n\t\t}\n\n\t\tif ( $created ) {\n\t\t\t$this->after_create();\n\t\t}\n\t}\n\n\n\t/**\n\t * Magic Getter\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $key Key to retrieve.\n\t * @return mixed\n\t */\n\tpublic function __get( $key ) {\n\t\treturn $this->___get( $key );\n\t}\n\n\t/**\n\t * Magic Isset\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $key Check if a key exists in the database.\n\t * @return boolean\n\t */\n\tpublic function __isset( $key ) {\n\t\treturn metadata_exists( 'post', $this->id, $this->meta_prefix . $key );\n\t}\n\n\t/**\n\t * Magic Setter\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $key Key of the property.\n\t * @param mixed  $val Value to set the property with.\n\t * @return void\n\t */\n\tpublic function __set( $key, $val ) {\n\t\t$this->$key = $val;\n\t}\n\n\t/**\n\t * Allow extending classes to add custom meta properties to the object\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array $props Key val array of prop key => prop type (see $this->properties).\n\t */\n\tprotected function add_properties( $props = array() ) {\n\n\t\t$this->properties = array_merge( $this->properties, $props );\n\t}\n\n\t/**\n\t * Modify allowed post tags for wp_kses for this post\n\t *\n\t * @since 3.19.2\n\t *\n\t * @return void\n\t */\n\tprotected function allowed_post_tags_set() {\n\t\tglobal $allowedposttags;\n\t\t$allowedposttags['iframe'] = array(\n\t\t\t'allowfullscreen' => true,\n\t\t\t'frameborder'     => true,\n\t\t\t'height'          => true,\n\t\t\t'src'             => true,\n\t\t\t'width'           => true,\n\t\t\t'style'           => true,\n\t\t);\n\t}\n\n\t/**\n\t * Remove modified allowed post tags for wp_kses for this post\n\t *\n\t * @since 3.19.2\n\t *\n\t * @return void\n\t */\n\tprotected function allowed_post_tags_unset() {\n\t\tglobal $allowedposttags;\n\t\tunset( $allowedposttags['iframe'] );\n\t}\n\n\t/**\n\t * Wrapper for $this-get() which allows translation of the database value before outputting on screen\n\t *\n\t * Extending classes should define this and translate any possible strings\n\t * with a switch statement or something.\n\t * This will return the untranslated string if a translation isn't defined.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $key Key to retrieve.\n\t * @return string\n\t */\n\tpublic function translate( $key ) {\n\t\t$val = $this->get( $key );\n\t\t// ******* example *******\n\t\t// switch( $key ) {\n\t\t// case 'example_key':\n\t\t// if ( 'example-val' === $val ) {\n\t\t// return translate( 'Example Key', 'lifterlms' );\n\t\t// }\n\t\t// break;\n\t\t// default:\n\t\t// return $val;\n\t\t// }\n\t\t// ******* example *******\n\t\treturn $val;\n\t}\n\n\t/**\n\t * Wrapper for the $this->translate() that echos the result rather than returning it\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $key Key to translate.\n\t * @return void\n\t */\n\tpublic function _e( $key ) { // phpcs:ignore -- This is to mimic localization functions.\n\t\techo esc_html( $this->translate( $key ) );\n\t}\n\n\t/**\n\t * Called immediately after creating / inserting a new post into the database\n\t *\n\t * This stub can be overwritten by child classes.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return  void\n\t */\n\tprotected function after_create() {}\n\n\t/**\n\t * Create a new post of the Instantiated Model\n\t *\n\t * This can be called by instantiating an instance with \"new\"\n\t * as the value passed to the constructor.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.3 Use `wp_slash()` for the post title.\n\t *\n\t * @param string $title Title to create the post with.\n\t * @return int WP Post ID of the new Post on success or 0 on error.\n\t */\n\tprivate function create( $title = '' ) {\n\t\treturn wp_insert_post(\n\t\t\twp_slash(\n\t\t\t\t/**\n\t\t\t\t * Filters the creation arguments used to create a new post.\n\t\t\t\t *\n\t\t\t\t * The return array is passed through {@see wp_slash} and ultimately\n\t\t\t\t * passed directly to {@see wp_insert_post}.\n\t\t\t\t *\n\t\t\t\t * The dynamic portion of this hook, `{$this->model_post_type}`, refers to the post\n\t\t\t\t * model's `$model_post_type` property.\n\t\t\t\t *\n\t\t\t\t * @since 3.0.0\n\t\t\t\t *\n\t\t\t\t * @param array $creation_args An array of arguments passed.\n\t\t\t\t */\n\t\t\t\tapply_filters(\n\t\t\t\t\t\"llms_new_{$this->model_post_type}\",\n\t\t\t\t\t$this->get_creation_args( $title )\n\t\t\t\t)\n\t\t\t),\n\t\t\ttrue\n\t\t);\n\t}\n\n\t/**\n\t * Clones the Post if the post is cloneable\n\t *\n\t * @since 3.3.0\n\t * @since 4.7.0 Use `LLMS_Generator::get_generated_content()` in favor of deprecated `LLMS_Generator::get_generated_posts()`.\n\t *\n\t * @return WP_Error|int|null WP_Error, WP Post ID of the clone (new) post, or null if post is not cloneable.\n\t */\n\tpublic function clone_post() {\n\n\t\t// If post type doesn't support cloning, don't proceed.\n\t\tif ( ! $this->is_cloneable() ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$this->allowed_post_tags_set();\n\n\t\t$generator = new LLMS_Generator( $this->toArray() );\n\t\t$generator->set_generator( 'LifterLMS/Single' . ucwords( $this->model_post_type ) . 'Cloner' );\n\t\tif ( ! $generator->is_error() ) {\n\t\t\t$generator->generate();\n\t\t}\n\n\t\t$this->allowed_post_tags_unset();\n\n\t\t$generated = $generator->get_generated_content();\n\t\tif ( isset( $generated[ $this->db_post_type ] ) ) {\n\t\t\treturn $generated[ $this->db_post_type ][0];\n\t\t}\n\n\t\treturn new WP_Error( 'generator-error', __( 'An unknown error occurred during post cloning. Please try again.', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Trigger an export download of the given post type\n\t *\n\t * @since 3.3.0\n\t * @since 3.19.2 Unknown.\n\t * @since 4.8.0 Made sure extra data are added to the posts model array representation during export.\n\t *\n\t * @return void\n\t */\n\tpublic function export() {\n\t\t// If post type doesn't support exporting don't proceed.\n\t\tif ( ! $this->is_exportable() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$title = str_replace( ' ', '-', $this->get( 'title' ) );\n\t\t$title = preg_replace( '/[^a-zA-Z0-9-]/', '', $title );\n\n\t\t/**\n\t\t * Filters the export file name\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string          $title     The exported file name. Doesn't include the extension.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\t$filename = apply_filters( 'llms_post_model_export_filename', $title . '_' . current_time( 'Ymd' ), $this );\n\n\t\theader( 'Content-type: application/json' );\n\t\theader( 'Content-Disposition: attachment; filename=\"' . $filename . '.json\"' );\n\t\theader( 'Pragma: no-cache' );\n\t\theader( 'Expires: 0' );\n\n\t\t$this->allowed_post_tags_set();\n\n\t\tadd_filter( 'llms_post_model_to_array_add_extras', '__return_true', 99 );\n\t\t$arr = $this->toArray();\n\t\tremove_filter( 'llms_post_model_to_array_add_extras', '__return_true', 99 );\n\n\t\t$arr['_generator'] = 'LifterLMS/Single' . ucwords( $this->model_post_type ) . 'Exporter';\n\t\t$arr['_source']    = get_site_url();\n\t\t$arr['_version']   = llms()->version;\n\n\t\tksort( $arr );\n\n\t\techo json_encode( $arr );\n\n\t\t$this->allowed_post_tags_unset();\n\n\t\tdie();\n\t}\n\n\t/**\n\t * Private getter.\n\t *\n\t * @since 3.34.0\n\t * @since 4.10.0 Add `post_name` as a property to skip scrubbing and add a filter on the list of properties to skip scrubbing.\n\t * @since 5.1.2 Pass second parameter to the `get_the_excerpt` filter hook (the WP_Post object), introduced in WordPress 4.5.0.\n\t *\n\t * @param string  $key The property key.\n\t * @param boolean $raw Optional. Whether or not we need to get the raw value. Default false.\n\t * @return mixed\n\t */\n\tprivate function ___get( $key, $raw = false ) {\n\n\t\t// Force numeric id and prevent filtering on the id.\n\t\tif ( 'id' === $key ) {\n\n\t\t\treturn absint( $this->$key );\n\n\t\t} elseif ( in_array( $key, array_keys( $this->get_post_properties() ) ) ) {\n\t\t\t$post_key = 'post_' . $key;\n\n\t\t\t// Ensure post is set globally for filters below.\n\t\t\tglobal $post;\n\t\t\t$temp = $post;\n\t\t\t$post = $this->post;\n\n\t\t\tswitch ( $key ) {\n\n\t\t\t\tcase 'content':\n\t\t\t\t\t$val = $raw ? $this->post->$post_key : llms_content( $this->post->$post_key );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'excerpt':\n\t\t\t\t\t/* This is a WordPress filter. */\n\t\t\t\t\t$val = $raw ? $this->post->$post_key : apply_filters( 'get_the_excerpt', $this->post->$post_key, $this->post );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'ping_status':\n\t\t\t\tcase 'comment_status':\n\t\t\t\tcase 'menu_order':\n\t\t\t\t\t$val = $this->post->$key;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'title':\n\t\t\t\t\t/* This is a WordPress filter. */\n\t\t\t\t\t$val = $raw ? $this->post->$post_key : apply_filters( 'the_title', $this->post->$post_key, $this->get( 'id' ) );\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t$val = $this->post->$post_key;\n\n\t\t\t}\n\n\t\t\t// Return the original global.\n\t\t\t$post = $temp;\n\n\t\t} elseif ( ! in_array( $key, $this->get_unsettable_properties() ) ) {\n\n\t\t\tif ( metadata_exists( 'post', $this->id, $this->meta_prefix . $key ) ) {\n\t\t\t\t$val = get_post_meta( $this->id, $this->meta_prefix . $key, true );\n\t\t\t} else {\n\t\t\t\t$val = $this->get_default_value( $key );\n\t\t\t}\n\t\t} else {\n\n\t\t\treturn $this->$key;\n\t\t}\n\n\t\t// If we found a value, apply default llms get filter and return the value.\n\t\tif ( isset( $val ) ) {\n\n\t\t\t/**\n\t\t\t * Filters the list of properties which should be excluded from scrubbing during a property read.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `{$this->model_post_type}`, refers to the post's model type,\n\t\t\t * for example \"course\" for an `LLMS_Course`, \"membership\" for an `LLMS_Membership`, etc...\n\t\t\t *\n\t\t\t * @since 4.10.0\n\t\t\t *\n\t\t\t * @param string[]        $props An array of property keys to be excluded from scrubbing.\n\t\t\t * @param LLMS_Post_Model $this  Instance of the post object.\n\t\t\t */\n\t\t\t$exclude = apply_filters( \"llms_get_{$this->model_post_type}_no_scrub_props\", array( 'content', 'name' ), $this );\n\t\t\tif ( ! $raw && ! in_array( $key, $exclude, true ) ) {\n\t\t\t\t$val = $this->scrub( $key, $val );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filters the property value\n\t\t\t *\n\t\t\t * The first dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t\t * \"lesson\", \"membership\", etc...\n\t\t\t * The second dynamic part of this hook, `$key`, refers to the property name.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param mixed           $val       The property value.\n\t\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t\t */\n\t\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_{$key}\", $val, $this );\n\n\t\t}\n\n\t\t// Shouldn't ever get here.\n\t\treturn false;\n\t}\n\n\t/**\n\t * Getter\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string  $key The property key.\n\t * @param boolean $raw Optional. Whether or not we need to get the raw value. Default is `false`.\n\t * @return mixed\n\t */\n\tpublic function get( $key, $raw = false ) {\n\n\t\tif ( $raw ) {\n\t\t\treturn $this->___get( $key, $raw );\n\t\t}\n\n\t\treturn $this->$key;\n\t}\n\n\t/**\n\t * Getter for array values\n\t *\n\t * Ensures that even empty values return an array.\n\t *\n\t * @since 3.0.0 Unknown.\n\t *\n\t * @param string $key Property key.\n\t * @return array\n\t */\n\tpublic function get_array( $key ) {\n\t\t$val = $this->get( $key );\n\t\tif ( ! is_array( $val ) ) {\n\t\t\t$val = array( $val );\n\t\t}\n\t\treturn $val;\n\t}\n\n\t/**\n\t * Getter for date strings with optional date format conversion\n\t *\n\t * If no format is supplied, the default format available via $this->get_date_format() will be used.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $key    Property key.\n\t * @param string $format Any valid date format that can be passed to date().\n\t * @return string\n\t */\n\tpublic function get_date( $key, $format = null ) {\n\t\t$format = ( ! $format ) ? $this->get_date_format() : $format;\n\t\t$raw    = $this->get( $key );\n\t\t// Only convert the date if we actually have something stored, otherwise we'll return the current date, which we probably aren't expecting.\n\t\t$date = $raw ? date_i18n( $format, strtotime( $raw ) ) : '';\n\n\t\t/**\n\t\t * Filters the date(s)\n\t\t *\n\t\t * The first dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t * The second dynamic part of this hook, `$key`, refers to the date property name.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string          $date      The formatted date.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_{$key}_date\", $date, $this );\n\t}\n\n\t/**\n\t * Retrieve the default date format for the post model\n\t *\n\t * This *can* be overridden by child classes if the post type requires a different default date format.\n\t *\n\t * If no format is supplied by the child class, the default WP date & time formats available\n\t * via General Settings will be combined and used.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_date_format() {\n\t\t$format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );\n\n\t\t/**\n\t\t * Filters the date format\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $format The date format.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_date_format\", $format );\n\t}\n\n\t/**\n\t * Get the default value of a property\n\t *\n\t * If defaults don't exist returns an empty string in accordance with the return of get_post_meta() when no metadata exists.\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param string $key Property key/name.\n\t * @return mixed\n\t */\n\tpublic function get_default_value( $key ) {\n\t\t$defaults = $this->get_property_defaults();\n\t\treturn isset( $defaults[ $key ] ) ? $defaults[ $key ] : '';\n\t}\n\n\t/**\n\t * Retrieve URL for an image associated with the post\n\t *\n\t * Currently, only retrieves the featured image if the post type supports it.\n\t * In the future, this will allow retrieval of custom post images as well.\n\t *\n\t * @since 3.3.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @param string|array $size Registered image size or a numeric array with width/height.\n\t * @param string       $key  Currently unused but here for forward compatibility if\n\t *                           additional custom images are added\n\t * @return string Empty string if no image or not supported.\n\t */\n\tpublic function get_image( $size = 'full', $key = '' ) {\n\t\tif ( 'thumbnail' === $key && post_type_supports( $this->db_post_type, 'thumbnail' ) ) {\n\t\t\t$url = get_the_post_thumbnail_url( $this->get( 'id' ), $size );\n\t\t} else {\n\t\t\t$id = $this->get( $key );\n\t\t\tif ( is_numeric( $id ) ) {\n\t\t\t\t$src = wp_get_attachment_image_src( $id, $size );\n\t\t\t\tif ( $src ) {\n\t\t\t\t\t$url = $src[0];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn ! empty( $url ) ? $url : '';\n\t}\n\n\t/**\n\t * Retrieve the Post's post type data object\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return WP_Post_Type|null\n\t */\n\tpublic function get_post_type_data() {\n\t\treturn get_post_type_object( $this->get( 'type' ) );\n\t}\n\n\t/**\n\t * Retrieve a label from the post type data object's labels object\n\t *\n\t * @since 3.0.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @param string $label Key for the label.\n\t * @return string\n\t */\n\tpublic function get_post_type_label( $label = 'singular_name' ) {\n\t\t$obj = $this->get_post_type_data();\n\t\tif ( property_exists( $obj, 'labels' ) && property_exists( $obj->labels, $label ) ) {\n\t\t\treturn $obj->labels->$label;\n\t\t}\n\t\treturn '';\n\t}\n\n\t/**\n\t * Getter for price strings with optional formatting options\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.0 Unknown.\n\t * @since 4.8.0 Use strict type comparison where possible.\n\t *\n\t * @param string $key        Property key.\n\t * @param array  $price_args Optional. Array of arguments that can be passed to llms_price(). Default is empty array.\n\t * @param string $format     Optional. Format conversion method [html|raw|float]. Default is 'html'.\n\t * @return mixed\n\t */\n\tpublic function get_price( $key, $price_args = array(), $format = 'html' ) {\n\n\t\t$price = $this->get( $key );\n\n\t\t// Handle empty or unset values gracefully.\n\t\tif ( '' === $price ) {\n\t\t\t$price = 0;\n\t\t}\n\n\t\t/**\n\t\t * Filter the price before formatting the price for display.\n\t\t *\n\t\t * @since 7.8.0\n\t\t */\n\t\t$price = apply_filters( \"llms_{$this->model_post_type}_get_price_before_formatting\", $price, $key, $price_args, $this );\n\n\t\tif ( 'html' === $format || 'raw' === $format ) {\n\t\t\t$price = llms_price( $price, $price_args );\n\t\t\tif ( 'raw' === $format ) {\n\t\t\t\t$price = wp_strip_all_tags( $price );\n\t\t\t}\n\t\t} elseif ( 'float' === $format ) {\n\t\t\t$price = floatval( number_format( $price, get_lifterlms_decimals(), '.', '' ) );\n\t\t} else {\n\t\t\t/**\n\t\t\t* Allows applying custom formatting to price(s).\n\t\t\t*\n\t\t\t* This is only fired when the `get_price()`'s `$format` passed param is not one of html|raw|float.\n\t\t\t*\n\t\t\t* @since Unknown\n\t\t\t*\n\t\t\t* The first dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t\t* \"lesson\", \"membership\", etc...\n\t\t\t* The second dynamic part of this hook, `$key`, refers to the price property name.\n\t\t\t* The third dynamic part of this hook, `$format`, refers to the custom format conversion method.\n\t\t\t*/\n\t\t\t$price = apply_filters( \"llms_get_{$this->model_post_type}_{$key}_{$format}\", $price, $key, $price_args, $format, $this );\n\t\t}\n\n\t\t/**\n\t\t * Filters the price(s)\n\t\t *\n\t\t * The first dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t * The second dynamic part of this hook, `$key`, refers to the price property name.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string          $price      The maybe formatted price.\n\t\t * @param string          $key        The price property name.\n\t\t * @param array           $price_args Array of arguments that can be passed to llms_price().\n\t\t * @param string          $format     Format conversion method.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_{$key}_price\", $price, $key, $price_args, $format, $this );\n\t}\n\n\t/**\n\t * Retrieve the default values for properties\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_property_defaults() {\n\t\t/**\n\t\t * Filters the defaults properties.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since 3.24.0\n\t\t *\n\t\t * @param array           $property_defaults Array of default property values.\n\t\t * @param LLMS_Post_Model $llms_post         The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_property_defaults\", $this->property_defaults, $this );\n\t}\n\n\t/**\n\t * An array of default arguments to pass to $this->create() when creating a new post\n\t *\n\t * This *should* be overridden by child classes.\n\t *\n\t * @since 3.0.0\n\t * @since 3.18.0 Unknown.\n\t *\n\t * @param array $args Args of data to be passed to wp_insert_post.\n\t * @return array\n\t */\n\tprotected function get_creation_args( $args = null ) {\n\n\t\t// Allow nothing to be passed in.\n\t\tif ( empty( $args ) ) {\n\t\t\t$args = array();\n\t\t}\n\n\t\t// Backwards compat to original 3.0.0 format when just a title was passed in.\n\t\tif ( is_string( $args ) ) {\n\t\t\t$args = array(\n\t\t\t\t'post_title' => $args,\n\t\t\t);\n\t\t}\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'comment_status' => 'closed',\n\t\t\t\t'ping_status'    => 'closed',\n\t\t\t\t'post_author'    => get_current_user_id(),\n\t\t\t\t'post_content'   => '',\n\t\t\t\t'post_excerpt'   => '',\n\t\t\t\t'post_status'    => 'draft',\n\t\t\t\t'post_title'     => '',\n\t\t\t\t'post_type'      => $this->get( 'db_post_type' ),\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Filters the llms post creation args\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since 3.24.0\n\t\t *\n\t\t * @param array           $args      Array of default creation args to be passed to `wp_insert_post()`.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->model_post_type}_get_creation_args\", $args, $this );\n\t}\n\n\t/**\n\t * Get media embeds.\n\t *\n\t * @since 3.17.0\n\t * @since 3.17.5 Unknown.\n\t * @since 7.7.0 Added function to get provider support.\n\t *\n\t * @param string $type Optional. Embed type [video|audio]. Default is 'video'.\n\t * @param string $prop Optional. Postmeta property name. Default is empty string.\n\t *                     If not supplied it will default to {$type}_embed.\n\t * @return string\n\t */\n\tprotected function get_embed( $type = 'video', $prop = '' ) {\n\n\t\t$ret = '';\n\n\t\t$prop = $prop ? $prop : $type . '_embed';\n\t\t$url  = $this->get( $prop );\n\t\tif ( trim( $url ) && wp_parse_url( $url ) ) {\n\t\t\t$this->get_provider_support( $url );\n\n\t\t\t$ret = wp_oembed_get( sanitize_url( $url ) );\n\n\t\t\tif ( ! $ret ) {\n\t\t\t\t$ret = apply_filters( 'llms_embed_shortcode_output', do_shortcode( sprintf( '[%1$s src=\"%2$s\"]', $type, $url ) ), $type, $this );\n\t\t\t}\n\t\t}\n\t\t/**\n\t\t * Filters the embed html\n\t\t *\n\t\t * The first dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t * The second dynamic portion of this hook, `$type`, refers to the embed type [video|audio].\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array           $embed     The embed html.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t * @param string          $type      Embed type [video|audio].\n\t\t * @param string          $prop      Postmeta property name.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->model_post_type}_{$type}\", $ret, $this, $type, $prop );\n\t}\n\n\t/**\n\t * Get a property's data type for scrubbing\n\t *\n\t * Used by $this->scrub() to determine how to scrub the property.\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param string $key Property key.\n\t * @return string\n\t */\n\tprotected function get_property_type( $key ) {\n\n\t\t$props = $this->get_properties();\n\n\t\t// Check against the properties array.\n\t\tif ( in_array( $key, array_keys( $props ) ) ) {\n\t\t\t$type = $props[ $key ];\n\t\t} else {\n\t\t\t$type = 'text';\n\t\t}\n\n\t\treturn $type;\n\t}\n\n\t/**\n\t * Retrieve an array of post properties\n\t *\n\t * These properties need to be get/set with alternate methods.\n\t *\n\t * @since 3.0.0\n\t * @since 3.31.0 Treat excerpts as HTML instead of plain text.\n\t * @since 3.34.0 Add date and modified dates GMT version, comment and ping status, post password and menu_order.\n\t *\n\t * @return array\n\t */\n\tprotected function get_post_properties() {\n\t\t/**\n\t\t * Filters the properties of the model that are properties of WP_Post.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array           $post_properties Associative array of the type $post_property_name => type.\n\t\t * @param LLMS_Post_Model $llms_post       The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_post_model_get_post_properties',\n\t\t\tarray(\n\t\t\t\t'author'         => 'absint',\n\t\t\t\t'content'        => 'html',\n\t\t\t\t'date'           => 'text',\n\t\t\t\t'date_gmt'       => 'text',\n\t\t\t\t'excerpt'        => 'html',\n\t\t\t\t'password'       => 'text',\n\t\t\t\t'parent'         => 'absint',\n\t\t\t\t'menu_order'     => 'absint',\n\t\t\t\t'modified'       => 'text',\n\t\t\t\t'modified_gmt'   => 'text',\n\t\t\t\t'name'           => 'text',\n\t\t\t\t'status'         => 'text',\n\t\t\t\t'title'          => 'text',\n\t\t\t\t'type'           => 'text',\n\t\t\t\t'comment_status' => 'text',\n\t\t\t\t'ping_status'    => 'text',\n\t\t\t),\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an array of properties defined by the model\n\t *\n\t * @since 3.3.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @return array\n\t */\n\tpublic function get_properties() {\n\t\t$props = array_merge( $this->get_post_properties(), $this->properties );\n\t\t/**\n\t\t * Filters the llms post properties\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array           $properties Array of properties defined by the model\n\t\t * @param LLMS_Post_Model $llms_post  The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_properties\", $props, $this );\n\t}\n\n\t/**\n\t * Get the properties that will be used to generate the array representation of the model.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return string[] Array of property keys to be used by {@see toArray}.\n\t */\n\tprotected function get_to_array_properties() {\n\n\t\t$all_props = array_keys( $this->get_properties() );\n\n\t\t/**\n\t\t * Filters the properties which will excluded form the array representation of the model\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string[]        $excluded  Array of property names.\n\t\t * @param string[]        $all_props The full property list without the applied exclusions.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\t$excluded = apply_filters(\n\t\t\t\"llms_get_{$this->model_post_type}_excluded_to_array_properties\",\n\t\t\t$this->get_to_array_excluded_properties(),\n\t\t\t$all_props,\n\t\t\t$this\n\t\t);\n\n\t\t$props = array_diff(\n\t\t\t$all_props,\n\t\t\t$excluded\n\t\t);\n\n\t\t/**\n\t\t * Filters the properties which will populate the array representation of the model.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string[]        $props     Array of property names.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t\"llms_get_{$this->model_post_type}_to_array_properties\",\n\t\t\t$props,\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Get the properties that will be explicitly excluded from the array representation of the model.\n\t *\n\t * This stub can be overloaded by an extending class and the property list is filterable via the\n\t * {@see llms_get_{$this->model_post_type}_excluded_to_array_properties} filter.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return string[]\n\t */\n\tprotected function get_to_array_excluded_properties() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Retrieve the registered Label of the post's current status\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_status_name() {\n\t\t$obj = get_post_status_object( $this->get( 'status' ) );\n\t\t/**\n\t\t * Filters the registered label of the post's current status.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $label The registered label of the post's current status.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_status_name\", $obj->label );\n\t}\n\n\t/**\n\t * Get an array of terms for a given taxonomy for the post\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string  $tax    Taxonomy name.\n\t * @param boolean $single Return only one term as an int, useful for taxes which\n\t *                        Can only have one term (eg: visibilities and difficulties and such)\n\t * @return mixed When single a single term object or null.\n\t *               When not single an array of term objects.\n\t */\n\tpublic function get_terms( $tax, $single = false ) {\n\n\t\t$terms = get_the_terms( $this->get( 'id' ), $tax );\n\n\t\tif ( $single ) {\n\t\t\treturn $terms ? $terms[0] : null;\n\t\t}\n\n\t\treturn $terms ? $terms : array();\n\t}\n\n\t/**\n\t * Array of properties which *cannot* be set\n\t *\n\t * If a child class adds any properties which should not be settable\n\t * the class should override this property and add their custom\n\t * properties to the array.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_unsettable_properties() {\n\t\t/**\n\t\t * Filters the properties of the model that *cannot* be set\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array           $unsettable_properties Array of property names.\n\t\t * @param LLMS_Post_Model $llms_post             The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_post_model_get_unsettable_properties',\n\t\t\tarray(\n\t\t\t\t'db_post_type',\n\t\t\t\t'id',\n\t\t\t\t'meta_prefix',\n\t\t\t\t'model_post_type',\n\t\t\t\t'post',\n\t\t\t),\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Determine if the associated post is exportable\n\t *\n\t * @since 3.3.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_cloneable() {\n\t\treturn post_type_supports( $this->db_post_type, 'llms-clone-post' );\n\t}\n\n\t/**\n\t * Determine if the associated post is exportable\n\t *\n\t * @since 3.3.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_exportable() {\n\t\treturn post_type_supports( $this->db_post_type, 'llms-export-post' );\n\t}\n\n\t/**\n\t * Format the object for json serialization\n\t *\n\t * Encodes the results of $this->toArray().\n\t *\n\t * @todo The `mixed` return type declared by the parent method, which should be defined here as well,\n\t *       is not available until PHP 8.0. Once support is dropped for 7.4 we can add the return type declaration\n\t *       and remove the `#[ReturnTypeWillChange]` attribute. This *must* happen before the release of PHP 9.0.\n\t *\n\t * @since 3.3.0\n\t *\n\t * @return array\n\t */\n\t#[ReturnTypeWillChange]\n\tpublic function jsonSerialize() {\n\t\t/**\n\t\t * Filters the properties of the model that *cannot* be set\n\t\t *\n\t\t * @since 3.3.0\n\t\t *\n\t\t * @param array           $model     Array representation of the LLMS_Post_Model object.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\treturn apply_filters( 'llms_post_model_json_serialize', $this->toArray(), $this );\n\t}\n\n\t/**\n\t * Scrub field according to it's type\n\t *\n\t * This is automatically called by set() method before anything is actually set.\n\t *\n\t * @since 3.0.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param string $key Property key.\n\t * @param mixed  $val Property value.\n\t * @return mixed\n\t */\n\tprotected function scrub( $key, $val ) {\n\t\t/**\n\t\t * Filters the property type being scrubbed.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string          $type      The property type.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t */\n\t\t$type = apply_filters( \"llms_get_{$this->model_post_type}_property_type\", $this->get_property_type( $key ), $this );\n\n\t\t/**\n\t\t * Filters the scrubbed property.\n\t\t *\n\t\t * The first dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t * The second dynamic part of this hook, `$key`, refers to the property name.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param mixed           $scrubbed  The scrubbed property value.\n\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t * @param string          $key       The property name.\n\t\t * @param mixed           $val       The original property value.\n\t\t */\n\t\treturn apply_filters( \"llms_scrub_{$this->model_post_type}_field_{$key}\", $this->scrub_field( $val, $type ), $this, $key, $val );\n\t}\n\n\t/**\n\t * Scrub fields according to datatype\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.2 Unknown.\n\t * @since 5.9.0 Use `wp_strip_all_tags()` in favor of `strip_tags()`.\n\t *              Only strip tags from string values.\n\t *              Coerce `null` html input to an empty string.\n\t *\n\t * @param mixed  $val  Property value to scrub.\n\t * @param string $type Data type.\n\t * @return mixed\n\t */\n\tprotected function scrub_field( $val, $type ) {\n\n\t\tif ( is_string( $val ) && 'html' !== $type ) {\n\t\t\t$val = wp_strip_all_tags( $val );\n\t\t}\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'absint':\n\t\t\t\t$val = absint( $val );\n\t\t\t\tbreak;\n\n\t\t\tcase 'array':\n\t\t\t\tif ( '' === $val ) {\n\t\t\t\t\t$val = array();\n\t\t\t\t}\n\t\t\t\t$val = (array) $val;\n\t\t\t\tbreak;\n\n\t\t\tcase 'bool':\n\t\t\tcase 'boolean':\n\t\t\t\t$val = boolval( $val );\n\t\t\t\tbreak;\n\n\t\t\tcase 'float':\n\t\t\t\t$val = floatval( $val );\n\t\t\t\tbreak;\n\n\t\t\tcase 'html':\n\t\t\t\t$this->allowed_post_tags_set();\n\t\t\t\t$val = wp_kses_post( $val ?? '' );\n\t\t\t\t$this->allowed_post_tags_unset();\n\t\t\t\tbreak;\n\n\t\t\tcase 'int':\n\t\t\t\t$val = intval( $val );\n\t\t\t\tbreak;\n\n\t\t\tcase 'yesno':\n\t\t\t\t$val = 'yes' === $val ? 'yes' : 'no';\n\t\t\t\tbreak;\n\n\t\t\tcase 'url':\n\t\t\t\t$val = sanitize_url( $val );\n\t\t\t\tbreak;\n\n\t\t\tcase 'text':\n\t\t\tcase 'string':\n\t\t\tdefault:\n\t\t\t\t$val = sanitize_text_field( $val );\n\n\t\t}\n\n\t\treturn $val;\n\t}\n\n\t/**\n\t * Setter.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.3 Use `wp_slash()` when setting properties.\n\t * @since 3.34.0 Turned to be only a wrapper for the set_bulk() method.\n\t * @since 6.5.0 Introduced `$allow_same_meta_value` param.\n\t *\n\t * @param string|array $key_or_array          Key of the property or an associative array of key/val pairs.\n\t * @param mixed        $val                   Value to set the property with.\n\t *                                            This parameter will be ignored when the first parameter is an associative array of key/val pairs.\n\t * @param boolean      $allow_same_meta_value Whether or not updating a meta with the same value as stored in the db is allowed.\n\t * @return boolean true on success, false on error or if the submitted value is the same as what's in the database and `$allow_same_meta_value` is `false`.\n\t */\n\tpublic function set( $key_or_array, $val = '', $allow_same_meta_value = false ) {\n\n\t\t$model_array = $key_or_array;\n\n\t\tif ( ! is_array( $key_or_array ) ) {\n\t\t\t$model_array = array(\n\t\t\t\t$key_or_array => $val,\n\t\t\t);\n\t\t}\n\n\t\treturn $this->set_bulk( $model_array, false, $allow_same_meta_value );\n\t}\n\n\n\t/**\n\t * Bulk setter.\n\t *\n\t * @since 3.34.0\n\t * @since 3.36.1 Use WP_Error::$errors in place of WP_Error::has_errors() to support WordPress version prior to 5.1.\n\t * @since 5.3.1 Fix quote slashing when the user is not an admin.\n\t * @since 6.5.0 Introduced `$allow_same_meta_value` param.\n\t *               Code reorganization.\n\t *\n\t * @param array   $model_array           Associative array of key/val pairs.\n\t * @param array   $wp_error              Whether or not return a WP_Error.\n\t * @param boolean $allow_same_meta_value Whether or not updating a meta with the same value as stored in the db is allowed.\n\t * @return boolean|WP_Error True on success. If the param $wp_error is set to false this will be false on error or if there was nothing to update.\n\t *                          Otherwise, this will be a WP_Error object collecting all the errors encountered along the way.\n\t */\n\tpublic function set_bulk( $model_array, $wp_error = false, $allow_same_meta_value = false ) {\n\n\t\tif ( empty( $model_array ) ) {\n\t\t\treturn $wp_error ? new WP_Error( 'empty_data', __( 'Empty data', 'lifterlms' ) ) : false;\n\t\t}\n\n\t\t$llms_post = $this->parse_properties_to_set( $model_array );\n\n\t\tif ( empty( $llms_post ) ) {\n\t\t\treturn $wp_error ? new WP_Error( 'invalid_data', __( 'Invalid data', 'lifterlms' ) ) : false;\n\t\t}\n\n\t\t$update_post_properties = $this->update_post_properties( $llms_post['post'] );\n\t\t$update_meta_properties = $this->update_meta_properties( $llms_post['meta'], $allow_same_meta_value );\n\n\t\t$error = is_wp_error( $update_post_properties ) ? $update_post_properties : new WP_Error();\n\t\tif ( is_wp_error( $update_meta_properties ) ) {\n\t\t\tforeach ( $update_meta_properties->get_error_messages( 'invalid_meta' ) as $message ) {\n\t\t\t\t$error->add( 'invalid_meta', $message );\n\t\t\t}\n\t\t}\n\n\t\tif ( ! empty( $error->has_errors() ) ) {\n\t\t\treturn $wp_error ? $error : false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Parse the LifterLMS post properties to set.\n\t *\n\t * Logic moved from `set_bulk()` method.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @param array $model_array Associative array of key/val pairs.\n\t * @return array|bool Returns `false` if nothing to set or an array that contains all the post properties and all the metas to set.\n\t */\n\tprivate function parse_properties_to_set( $model_array ) {\n\n\t\t$llms_post = array(\n\t\t\t'post' => array(),\n\t\t\t'meta' => array(),\n\t\t);\n\n\t\t$post_properties       = array_keys( $this->get_post_properties() );\n\t\t$unsettable_properties = $this->get_unsettable_properties();\n\n\t\tforeach ( $model_array as $key => $val ) {\n\n\t\t\t// Sanitize the post properties keys by removing the 'post_' prefix.\n\t\t\tif ( 'post_' === substr( $key, 0, 5 ) ) {\n\t\t\t\t$_key = substr( $key, 5 );\n\t\t\t\tif ( in_array( $_key, $post_properties, true ) ) {\n\t\t\t\t\t$key = $_key;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$val = $this->scrub( $key, $val );\n\n\t\t\t/**\n\t\t\t * WordPress Post properties to be updated using the wp_insert_post() function.\n\t\t\t *\n\t\t\t * The 'edit_date' must be passed to the wp_update_post() function in order\n\t\t\t * to allow 'drafty' posts' creation date to be modified.\n\t\t\t */\n\t\t\tif ( in_array( $key, $post_properties, true ) || 'edit_date' === $key ) {\n\n\t\t\t\t$type          = 'post';\n\t\t\t\t$llms_post_key = \"post_{$key}\";\n\n\t\t\t\tswitch ( $key ) {\n\n\t\t\t\t\tcase 'content':\n\t\t\t\t\t\t/** This is a WordPress core filter. {@see kses_init_filters()}*/\n\t\t\t\t\t\t$val = stripslashes( apply_filters( 'content_save_pre', addslashes( $val ) ) );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'excerpt':\n\t\t\t\t\t\t/** This is a WordPress core filter. {@see kses_init_filters()}*/\n\t\t\t\t\t\t$val = stripslashes( apply_filters( 'excerpt_save_pre', addslashes( $val ) ) );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'edit_date':\n\t\t\t\t\tcase 'ping_status':\n\t\t\t\t\tcase 'comment_status':\n\t\t\t\t\tcase 'menu_order':\n\t\t\t\t\t\t$llms_post_key = $key;\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'title':\n\t\t\t\t\t\t/** This is a WordPress core filter. {@see kses_init_filters()}*/\n\t\t\t\t\t\t$val = stripslashes( apply_filters( 'title_save_pre', addslashes( $val ) ) );\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} elseif ( ! in_array( $key, $unsettable_properties, true ) ) {\n\t\t\t\t$type          = 'meta';\n\t\t\t\t$llms_post_key = $key;\n\t\t\t} else {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filters the property value prior to be set.\n\t\t\t *\n\t\t\t * The first dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t\t * \"lesson\", \"membership\", etc...\n\t\t\t * The second dynamic part of this hook, `$key`, refers to the property name.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param mixed           $val       The property value.\n\t\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t\t */\n\t\t\t$llms_post[ $type ][ $llms_post_key ] = apply_filters( \"llms_set_{$this->model_post_type}_{$key}\", $val, $this );\n\n\t\t}\n\n\t\treturn empty( $llms_post['post'] ) && empty( $llms_post['meta'] ) ? false : $llms_post;\n\t}\n\n\t/**\n\t * Update post properties.\n\t *\n\t * Logic moved from `set_bulk()` method.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @param array $post_properties Array of post properties to set.\n\t * @return void|WP_Error\n\t */\n\tprivate function update_post_properties( $post_properties ) {\n\n\t\tif ( empty( $post_properties ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$args = array_merge(\n\t\t\t$post_properties,\n\t\t\tarray(\n\t\t\t\t'ID' => $this->get( 'id' ),\n\t\t\t)\n\t\t);\n\n\t\t$update_post = wp_update_post( wp_slash( $args ), true );\n\n\t\tif ( is_wp_error( $update_post ) ) {\n\t\t\treturn $update_post;\n\t\t}\n\n\t\t// Update this post.\n\t\t$this->post = get_post( $this->get( 'id' ) );\n\t}\n\n\n\t/**\n\t * Update post meta properties.\n\t *\n\t * Logic moved from `set_bulk()` method.\n\t *\n\t * @param array   $post_meta_properties  Array of post meta properties to set.\n\t * @param boolean $allow_same_meta_value Whether or not updating a meta with the same value as stored in the db is allowed.\n\t *                                       By default `update_post_meta` doesn't allow that.\n\t * @return void|WP_Error\n\t */\n\tprivate function update_meta_properties( $post_meta_properties, $allow_same_meta_value ) {\n\n\t\tif ( empty( $post_meta_properties ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$error = new WP_Error();\n\n\t\tforeach ( $post_meta_properties as $key => $val ) {\n\n\t\t\tif ( $allow_same_meta_value ) {\n\t\t\t\t/**\n\t\t\t\t * Do pretty much(*) the same check for a duplicate value as in `update_metadata()`\n\t\t\t\t * to avoid `update_post_meta()` returning false.\n\t\t\t\t * {@see WP_REST_Meta_Fields::update_meta_value()}.\n\t\t\t\t *\n\t\t\t\t * If the new value to be set equals the old one don't update it.\n\t\t\t\t *\n\t\t\t\t * (*) This is not exactly the same check you can find in `update_metadata()` as that\n\t\t\t\t * account for multiple meta values for the same key, while we don't.\n\t\t\t\t */\n\t\t\t\t$old_value = get_post_meta( $this->id, $this->meta_prefix . $key, true );\n\t\t\t\tif ( $this->is_meta_value_same_as_stored_value( $key, $old_value, $val ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$u = update_post_meta( $this->id, $this->meta_prefix . $key, wp_slash( $val ) );\n\n\t\t\tif ( ! ( is_numeric( $u ) || true === $u ) ) {\n\t\t\t\t/* translators: %s: Meta key. */\n\t\t\t\t$error->add( 'invalid_meta', sprintf( __( 'Cannot insert/update the %s meta', 'lifterlms' ), $key ) );\n\t\t\t}\n\t\t}\n\n\t\tif ( $error->has_errors() ) {\n\t\t\treturn $error;\n\t\t}\n\t}\n\n\t/**\n\t * Checks if the user provided value is equivalent to a stored value for the given meta key.\n\t *\n\t * {@see WP_REST_Meta_Fields::is_meta_value_same_as_stored_value()}.\n\t *\n\t * @param string $key          The un-prefixed meta key being checked.\n\t * @param mixed  $stored_value The currently stored value retrieved from get_metadata().\n\t * @param mixed  $new_value    The new value.\n\t * @return bool\n\t */\n\tprivate function is_meta_value_same_as_stored_value( $key, $stored_value, $new_value ) {\n\n\t\t$sanitized = sanitize_meta( $this->meta_prefix . $key, $new_value, 'post', $this->db_post_type );\n\n\t\t// The return value of get_metadata will always be a string for scalar types.\n\t\t$scalar_types = array(\n\t\t\t'string',\n\t\t\t'text',\n\t\t\t'absint',\n\t\t\t'yesno',\n\t\t\t'html',\n\t\t\t'float',\n\t\t\t'int',\n\t\t\t'bool',\n\t\t\t'boolean',\n\t\t);\n\n\t\tif ( in_array( $this->get_property_type( $key ), $scalar_types, true ) ) {\n\t\t\t$sanitized = (string) $sanitized;\n\t\t}\n\n\t\treturn $sanitized === $stored_value;\n\t}\n\n\t/**\n\t * Update terms for the post for a given taxonomy\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param array   $terms  Array of terms (name or ids).\n\t * @param string  $tax    The name of the tax.\n\t * @param boolean $append Optional. If true, will append the terms, false will replace existing terms. Default is `false`.\n\t * @return bool\n\t */\n\tpublic function set_terms( $terms, $tax, $append = false ) {\n\t\t$set = wp_set_object_terms( $this->get( 'id' ), $terms, $tax, $append );\n\t\t// wp_set_object_terms has 3 options when unsuccessful and only 1 for success\n\t\t// an array of terms when successful, let's keep it simple...\n\t\treturn is_array( $set );\n\t}\n\n\t/**\n\t * Coverts the object to an associative array\n\t *\n\t * Any property returned by $this->get_properties() will be retrieved\n\t * via $this->get() and added to the array.\n\t *\n\t * Extending classes can add additional properties to the array\n\t * by overriding $this->toArrayAfter().\n\t *\n\t * This function is also utilized to serialize the object to JSON.\n\t *\n\t * @since 3.3.0\n\t * @since 3.17.0 Unknown.\n\t * @since 4.7.0 Add exporting of extra data (images and blocks).\n\t * @since 4.8.0 Exclude extra data by default. Added `llms_post_model_to_array_add_extras` filter.\n\t * @since 5.4.1 Load properties to be used to generate the array from the new `get_to_array_properties()` method.\n\t *\n\t * @return array\n\t */\n\tpublic function toArray() {\n\n\t\t$arr = array(\n\t\t\t'id' => $this->get( 'id' ),\n\t\t);\n\n\t\tforeach ( $this->get_to_array_properties() as $prop ) {\n\n\t\t\tif ( in_array( $prop, array( 'content', 'excerpt', 'title' ), true ) ) {\n\t\t\t\t$post_prop    = \"post_{$prop}\";\n\t\t\t\t$arr[ $prop ] = $this->post->$post_prop;\n\t\t\t} else {\n\t\t\t\t$arr[ $prop ] = $this->get( $prop );\n\t\t\t}\n\t\t}\n\n\t\t// Add the featured image if the post type supports it.\n\t\tif ( post_type_supports( $this->db_post_type, 'thumbnail' ) ) {\n\t\t\t$arr['featured_image'] = $this->get_image( 'full', 'thumbnail' );\n\t\t}\n\n\t\t// Expand instructors if instructors are supported.\n\t\tif ( ! empty( $arr['instructors'] ) && method_exists( $this, 'instructors' ) ) {\n\n\t\t\tforeach ( $arr['instructors'] as &$data ) {\n\t\t\t\t$instructor = llms_get_instructor( $data['id'] );\n\t\t\t\tif ( $instructor ) {\n\t\t\t\t\t$data = array_merge( $data, $instructor->toArray() );\n\t\t\t\t}\n\t\t\t}\n\t\t} elseif ( ! empty( $arr['author'] ) ) {\n\n\t\t\t$instructor = llms_get_instructor( $arr['author'] );\n\t\t\tif ( $instructor ) {\n\t\t\t\t$arr['author'] = $instructor->toArray();\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter whether or not \"extra\" content should be included in the post array\n\t\t *\n\t\t * `__return_true` (with priority 99) is used to force the filter on during exports.\n\t\t *\n\t\t * @since 4.8.0\n\t\t *\n\t\t * @param boolean         $include Whether or not to include extra data. Default is `false`, except on during exports.\n\t\t * @param LLMS_Post_Model $model   Post model instance.\n\t\t */\n\t\t$add_array_extra = apply_filters( 'llms_post_model_to_array_add_extras', false, $this );\n\n\t\t/**\n\t\t * Filter whether or not \"extra\" content should be included in the post array\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param boolean         $include Whether or not to include extra data.\n\t\t * @param LLMS_Post_Model $model   Post model instance.\n\t\t */\n\t\t$add_array_extra = apply_filters( \"llms_{$this->model_post_type}_to_array_add_extras\", $add_array_extra, $this );\n\n\t\tif ( $add_array_extra ) {\n\t\t\t$arr = $this->to_array_extra( $arr );\n\t\t}\n\n\t\t// Add custom fields.\n\t\t$arr = $this->toArrayCustom( $arr );\n\n\t\t// Allow extending classes to add properties easily without overriding the class.\n\t\t$arr = $this->toArrayAfter( $arr );\n\n\t\t$cpt_data = $this->get_post_type_data();\n\t\tif ( $cpt_data->public ) {\n\t\t\t$arr['permalink'] = get_permalink( $this->get( 'id' ) );\n\t\t}\n\n\t\tksort( $arr ); // Because i'm anal...\n\n\t\t/**\n\t\t * Filter the final post array created when converting the object to an array\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t * \"lesson\", \"membership\", etc...\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param array           $arr   Associative array of the model.\n\t\t * @param LLMS_Post_Model $model Post model instance.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->model_post_type}_to_array\", $arr, $this );\n\t}\n\n\t/**\n\t * Enqueues provider scripts for the URL.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $url URL to check.\n\t * @return null If no provider is found.\n\t */\n\tpublic function get_provider_support( $url ) {\n\n\t\t$host = wp_parse_url( $url, PHP_URL_HOST );\n\n\t\t// VideoPress Provider.\n\t\tif ( is_plugin_active( 'jetpack-videopress/jetpack-videopress.php' ) ) {\n\t\t\tif ( strpos( $host, 'videopress.com' ) !== false || strpos( $host, 'video.wordpress.com' ) !== false ) {\n\t\t\t\twp_enqueue_script( 'videopress-token-bridge', plugins_url() . '/jetpack-videopress/jetpack_vendor/automattic/jetpack-videopress/src/../build/lib/token-bridge.js', array(), llms()->version, true );\n\n\t\t\t\twp_localize_script( 'videopress-token-bridge', 'videopressAjax', array() );\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\n\n\t/**\n\t * Called before data is sorted and returned by $this->toArray()\n\t *\n\t * Extending classes should override this data if custom data should\n\t * be added when object is converted to an array or json.\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param array $arr Array of data to be serialized.\n\t * @return array\n\t */\n\tprotected function toArrayAfter( $arr ) {\n\t\treturn $arr;\n\t}\n\n\t/**\n\t * Add \"extra\" data to the post array during export/serialization\n\t *\n\t * This method adds two arrays of data, \"blocks\" and \"images\".\n\t *\n\t * The \"blocks\" array is an array of reusable blocks used in the post's content. During\n\t * an import these blocks will be imported into the site.\n\t *\n\t * The \"images\" array is an array of image element source URLs found in the post's content. During\n\t * an import these images will be imported into the new site via media sideloading.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param array $arr Post array from `toArray()`.\n\t * @return array[]\n\t */\n\tprotected function to_array_extra( $arr ) {\n\n\t\t$arr['_extras'] = array(\n\t\t\t'blocks' => empty( $arr['content'] ) ? array() : $this->to_array_extra_blocks( $arr['content'] ),\n\t\t\t'images' => empty( $arr['content'] ) ? array() : $this->to_array_extra_images( $arr['content'] ),\n\t\t);\n\n\t\treturn $arr;\n\t}\n\n\t/**\n\t * Add reusable blocks found in the post's content to the post's array\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param string $content Raw `post_content` string.\n\t * @return array[] {\n\t *     Array of reusable block information arrays. The array key is the WP_Post ID of the reusable block.\n\t *\n\t *     @type string $title   Reusable block title.\n\t *     @type string $content Reusable block content.\n\t * }\n\t */\n\tprotected function to_array_extra_blocks( $content ) {\n\n\t\t$blocks = array();\n\n\t\tforeach ( parse_blocks( $content ) as $block ) {\n\n\t\t\tif ( 'core/block' !== $block['blockName'] ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$post = get_post( $block['attrs']['ref'] );\n\t\t\tif ( ! $post ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$blocks[ $post->ID ] = array(\n\t\t\t\t'title'   => $post->post_title,\n\t\t\t\t'content' => $post->post_content,\n\t\t\t);\n\t\t}\n\n\t\treturn $blocks;\n\t}\n\n\t/**\n\t * Add images found in the post's content to the post's array\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param string $content Raw `post_content` string.\n\t * @return string[] Array of image source URLs.\n\t */\n\tprotected function to_array_extra_images( $content ) {\n\n\t\t$images = array();\n\t\t$dom    = llms_get_dom_document( $content );\n\t\tif ( is_wp_error( $dom ) ) {\n\t\t\treturn $images;\n\t\t}\n\n\t\t$site_url = get_site_url();\n\t\tforeach ( $dom->getElementsByTagName( 'img' ) as $img ) {\n\t\t\t$src = $img->getAttribute( 'src' );\n\t\t\t// Only include images stored in this site's media library.\n\t\t\tif ( 0 !== strpos( $src, $site_url ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$images[] = $src;\n\t\t}\n\n\t\treturn array_values( array_unique( $images ) );\n\t}\n\n\t/**\n\t * Called by toArray to add custom fields via get_post_meta()\n\t *\n\t * Removes all custom props registered to the $this->properties automatically.\n\t * Also removes some fields used by the WordPress core that don't hold necessary data.\n\t * Extending classes may override this class to exclude, extend, or modify the custom fields for a post type.\n\t *\n\t * @since 3.16.11\n\t * @since 3.30.0 Use `maybe_unserialize()` to ensure array data is accessible as an array.\n\t * @since 3.30.2 Add filter to allow 3rd parties to prevent a field from being added to the custom field array.\n\t *\n\t * @param array $arr Existing post array.\n\t * @return array\n\t */\n\tprotected function toArrayCustom( $arr ) {\n\n\t\t// Build an array of keys that are registered or can be excluded as a custom field.\n\t\t$props = array_keys( $this->get_properties() );\n\t\tforeach ( $props as &$prop ) {\n\t\t\t$prop = $this->meta_prefix . $prop;\n\t\t}\n\t\t$props[] = '_edit_lock';\n\t\t$props[] = '_edit_last';\n\n\t\t// Get all meta data.\n\t\t$custom = array();\n\t\tforeach ( get_post_meta( $this->get( 'id' ) ) as $key => $vals ) {\n\n\t\t\t// Skip registered fields or fields 3rd parties want to skip.\n\t\t\t/**\n\t\t\t * Filters whether the custom field should be excluded in the array representation of the post model\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to the model's post type. For example \"course\",\n\t\t\t * \"lesson\", \"membership\", etc...\n\t\t\t *\n\t\t\t * @since 3.30.2\n\t\t\t *\n\t\t\t * @param boolean         $exclude   Whether the custom field should be excluded. Default is `false`.\n\t\t\t * @param string          $key       The custom field name.\n\t\t\t * @param LLMS_Post_Model $llms_post The LLMS_Post_Model instance.\n\t\t\t */\n\t\t\tif ( in_array( $key, $props, true ) || apply_filters( \"llms_{$this->model_post_type}_skip_custom_field\", false, $key, $this ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Add it.\n\t\t\t$custom[ $key ] = array_map( 'maybe_unserialize', $vals );\n\n\t\t}\n\n\t\t// Add the compiled custom array.\n\t\t$arr['custom'] = $custom;\n\n\t\treturn $arr;\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.shortcode.course.element.php",
    "content": "<?php\n/**\n * Common Shortcode for course element templates\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.6.0\n * @version 3.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Common Shortcode for course element templates abstract class\n *\n * @since 3.6.0\n */\nabstract class LLMS_Shortcode_Course_Element extends LLMS_Shortcode {\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @return   void\n\t * @since    3.6.0\n\t * @version  3.6.0\n\t */\n\tabstract protected function template_function();\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @return   array\n\t * @since    3.6.0\n\t * @version  3.6.0\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'course_id' => get_the_ID(),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @return   string\n\t * @since    3.6.0\n\t * @version  3.6.0\n\t */\n\tprotected function get_output() {\n\n\t\t// Get a reference to the current page where the shortcode is displayed.\n\t\tglobal $post;\n\t\t$current_post = $post;\n\n\t\t$course = get_post( $this->get_attribute( 'course_id' ) );\n\n\t\t// We don't have a post object to proceed with.\n\t\tif ( ! $course ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tif ( 'course' !== $course->post_type ) {\n\t\t\t// Get the parent.\n\t\t\t$parent = llms_get_post_parent_course( $course );\n\n\t\t\t// Post type doesn't have a parent so we can't display a syllabus.\n\t\t\tif ( ! $parent ) {\n\t\t\t\treturn '';\n\t\t\t}\n\n\t\t\t// We have a course.\n\t\t\t$course = $parent->post;\n\n\t\t}\n\n\t\tob_start();\n\n\t\t// Hack the global so our syllabus template works.\n\t\tif ( $course->ID != $current_post->ID ) {\n\t\t\t$post = $course;\n\t\t}\n\n\t\t$this->template_function();\n\n\t\t// Restore the global.\n\t\tif ( $course->ID != $current_post->ID ) {\n\t\t\t$post = $current_post;\n\t\t}\n\n\t\treturn ob_get_clean();\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.shortcode.php",
    "content": "<?php\n/**\n * Base Shortcode\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.4.3\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode Abstract.\n *\n * @since 3.4.3\n */\nabstract class LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var  string\n\t */\n\tpublic $tag = '';\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @return   string\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tabstract protected function get_output();\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @return   array\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Retrieves a string used for default content which is used if no content is supplied\n\t *\n\t * @return   string\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tprotected function get_default_content() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Holds singletons for extending classes\n\t *\n\t * @var  array\n\t */\n\tprivate static $_instances = array();\n\n\tprivate $attributes = array();\n\tprivate $content    = '';\n\n\t/**\n\t * Get the singleton instance for the extending class\n\t *\n\t * @return   obj\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tpublic static function instance() {\n\n\t\t$class = get_called_class();\n\n\t\tif ( ! isset( self::$_instances[ $class ] ) ) {\n\t\t\tself::$_instances[ $class ] = new $class();\n\t\t}\n\n\t\treturn self::$_instances[ $class ];\n\n\t}\n\n\t/**\n\t * Private constructor\n\t *\n\t * @since 3.4.3\n\t */\n\tprivate function __construct() {\n\t\tadd_shortcode( $this->tag, array( $this, 'output' ) );\n\t}\n\n\t/**\n\t * Allow shortcodes to enqueue scripts only when the shortcode is used\n\t * Enqueues a registered script IF that script isn't already enqueued\n\t *\n\t * @param    string $handle  script handle\n\t * @return   void\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tprotected function enqueue_script( $handle ) {\n\n\t\tif ( wp_script_is( $handle, 'registered' ) && ! wp_script_is( $handle, 'enqueued' ) ) {\n\n\t\t\twp_enqueue_script( $handle );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Get the array of attributes\n\t *\n\t * @return   array\n\t * @since    3.4.3\n\t * @version  3.5.1\n\t */\n\tpublic function get_attributes() {\n\t\treturn apply_filters( $this->get_filter( 'get_attributes' ), $this->attributes, $this );\n\t}\n\n\t/**\n\t * Get a specific attribute from the attributes array\n\t *\n\t * @param    string $key      attribute key to retrieve\n\t * @param    string $default  if no attribute is set, this value will be used\n\t * @return   mixed\n\t * @since    3.4.3\n\t * @version  3.5.1\n\t */\n\tpublic function get_attribute( $key, $default = '' ) {\n\t\t$attributes = $this->get_attributes();\n\t\tif ( isset( $attributes[ $key ] ) ) {\n\t\t\treturn $attributes[ $key ];\n\t\t}\n\t\treturn $default;\n\t}\n\n\t/**\n\t * Retrieve the content of the shortcode\n\t *\n\t * @return   string\n\t * @since    3.4.3\n\t * @version  3.5.1\n\t */\n\tpublic function get_content() {\n\t\treturn apply_filters( $this->get_filter( 'get_content' ), $this->content, $this );\n\t}\n\n\t/**\n\t * Retrieve a string that can be used for apply_filters()\n\t * Ensures that all shortcode related filters follow the same naming convention\n\t *\n\t * @param    string $filter  filter name / suffix\n\t * @return   string\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tprotected function get_filter( $filter ) {\n\t\treturn $this->tag . '_' . $filter;\n\t}\n\n\t/**\n\t * Output the actual content of the shortcode\n\t * This is the callback function used by add_shortcode\n\t * and can also be used programmatically, used in some widgets\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @since 3.4.3\n\t * @since 3.5.1 Unknown.\n\t * @since 5.0.0 Merge attributes in a separate method.\n\t *\n\t * @param    array  $atts     user submitted shortcode attributes\n\t * @param    string $content  user submitted content\n\t * @return   string\n\t */\n\tpublic function output( $atts = array(), $content = '' ) {\n\n\t\t$this->attributes = $this->set_attributes( $atts );\n\n\t\tif ( ! $content ) {\n\t\t\t$content = apply_filters( $this->get_filter( 'get_default_content' ), $this->get_default_content(), $this );\n\t\t}\n\n\t\t$this->content = $content;\n\n\t\treturn apply_filters( $this->get_filter( 'output' ), $this->get_output(), $this );\n\n\t}\n\n\t/**\n\t * Merge user attributes with default attributes.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $atts User-submitted shortcode attributes.\n\t *\n\t * @return array\n\t */\n\tprotected function set_attributes( $atts = array() ) {\n\n\t\treturn shortcode_atts(\n\t\t\tapply_filters( $this->get_filter( 'get_default_attributes' ), $this->get_default_attributes(), $this ),\n\t\t\t$atts,\n\t\t\t$this->tag\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/abstract.llms.update.php",
    "content": ""
  },
  {
    "path": "includes/abstracts/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-admin-tool.php",
    "content": "<?php\n/**\n * Base for tools listed on the LifterLMS -> Status -> Tools & Utilities screen\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.37.19\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Abstract_Admin_Tool\n *\n * @since 3.37.19\n */\nabstract class LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID\n\t *\n\t * @var string\n\t */\n\tprotected $id = '';\n\n\t/**\n\t * Tool Priority\n\t *\n\t * Passed to the `llms_status_tools` filter when registering the tool.\n\t *\n\t * @var integer\n\t */\n\tprotected $priority = 10;\n\n\t/**\n\t * Process the tool.\n\t *\n\t * This method should do whatever the tool actually does.\n\t *\n\t * By the time this tool is called a nonce and the user's capabilities have already been checked.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return mixed\n\t */\n\tabstract protected function handle();\n\n\t/**\n\t * Retrieve a description of the tool\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return string\n\t */\n\tabstract protected function get_description();\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return string\n\t */\n\tabstract protected function get_label();\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return string\n\t */\n\tabstract protected function get_text();\n\n\t/**\n\t * Static constructor.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'llms_status_tool', array( $this, 'maybe_handle' ) );\n\t\tadd_filter( 'llms_status_tools', array( $this, 'register' ), $this->priority );\n\n\t}\n\n\t/**\n\t * Processes the tool if the submitted tool matches the tool's ID.\n\t *\n\t * @since 3.37.19\n\t * @since 5.0.0 Add before and after action hooks.\n\t *\n\t * @param string tool_id ID of the submitted tool.\n\t * @return mixed|false\n\t */\n\tpublic function maybe_handle( $tool_id ) {\n\n\t\tif ( $this->should_load() && $this->id === $tool_id ) {\n\n\t\t\t/**\n\t\t\t * Action run prior to running an admin tool's main `handle()` method.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook `{$tool_id}` refers to the unique ID\n\t\t\t * of the admin tool.\n\t\t\t *\n\t\t\t * @since 5.0.0\n\t\t\t *\n\t\t\t * @param object $tool_class Instance of the extending tool class.\n\t\t\t */\n\t\t\tdo_action( \"llms_before_handle_tool_{$tool_id}\", $this );\n\n\t\t\t$handled = $this->handle();\n\n\t\t\t/**\n\t\t\t * Action run prior to running an admin tool's main `handle()` method.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook `{$tool_id}` refers to the unique ID\n\t\t\t * of the admin tool.\n\t\t\t *\n\t\t\t * @since 5.0.0\n\t\t\t *\n\t\t\t * @param object $tool_class Instance of the extending tool class.\n\t\t\t */\n\t\t\tdo_action( \"llms_after_handle_tool_{$tool_id}\", $this );\n\n\t\t\treturn $handled;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Register the tool.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @see llms_status_tools (filter)\n\t *\n\t * @param array[] $tools Array of tool definitions.\n\t * @return array[]\n\t */\n\tpublic function register( $tools ) {\n\n\t\tif ( ! $this->should_load() ) {\n\t\t\treturn $tools;\n\t\t}\n\n\t\t$tools[ $this->id ] = array(\n\t\t\t'description' => $this->get_description(),\n\t\t\t'label'       => $this->get_label(),\n\t\t\t'text'        => $this->get_text(),\n\t\t);\n\n\t\treturn $tools;\n\n\t}\n\n\t/**\n\t * Conditionally load the tool\n\t *\n\t * This stub can be overridden by the tool to provide custom logic to determine\n\t * whether or not the tool should be loaded and registered.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return boolean Return `true` to load the tool and `false` to not load it.\n\t */\n\tprotected function should_load() {\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-admin-wizard.php",
    "content": "<?php\n/**\n * Display a Wizard\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 7.4.0\n * @version 7.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Display a Wizard class.\n *\n * @since 7.4.0\n */\nabstract class LLMS_Abstract_Admin_Wizard {\n\n\t/**\n\t * Wizard ID.\n\t *\n\t * @since 7.4.0\n\t * @var string\n\t */\n\tprotected string $id;\n\n\t/**\n\t * Views directory.\n\t *\n\t * @since 7.4.0\n\t * @var string\n\t */\n\tprotected string $views_dir;\n\n\t/**\n\t * Steps.\n\t *\n\t * @since 7.4.0\n\t * @var array\n\t */\n\tprotected array $steps;\n\n\t/**\n\t * Page title.\n\t *\n\t * @since 7.4.0\n\t * @var string\n\t */\n\tprotected string $title;\n\n\t/**\n\t * Error message.\n\t *\n\t * @since 7.4.0\n\t * @var WP_Error|null\n\t */\n\tpublic ?WP_Error $error = null;\n\n\t/**\n\t * Add hooks.\n\t *\n\t * @since 7.4.0\n\t * @return void\n\t */\n\tprotected function add_hooks(): void {\n\n\t\t/**\n\t\t * Whether the LifterLMS Wizard is enabled.\n\t\t *\n\t\t * This filter may be used to entirely disable the setup wizard.\n\t\t *\n\t\t * The dynamic portion of this filter, `{$this->id}`, refers to the wizard type. E.g. \"setup\".\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param bool $enabled Whether the wizard is enabled.\n\t\t */\n\t\tif ( ! apply_filters( \"llms_enable_{$this->id}_wizard\", true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );\n\t\tadd_action( 'admin_menu', array( $this, 'admin_menu' ) );\n\t\tadd_action( 'admin_init', array( $this, 'save' ) );\n\t\tadd_filter( 'llms_admin_show_header', array( $this, 'hide_admin_header' ), 10, 3 );\n\t}\n\n\t/**\n\t * Hide the admin header on the wizard pages.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param bool      $show   Whether to show the admin header.\n\t * @param WP_Screen $screen Current admin screen.\n\t * @param string    $page   Current admin page.\n\t * @return bool\n\t */\n\tpublic function hide_admin_header( bool $show, WP_Screen $screen, string $page ): bool {\n\t\tif ( \"llms-{$this->id}\" === $page ) {\n\t\t\t$show = false;\n\t\t}\n\n\t\treturn $show;\n\t}\n\n\t/**\n\t * Register wizard setup page.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return string|bool The hook suffix of the setup wizard page (\"admin_page_llms-setup\"), or `false` if the user does not have the capability required.\n\t */\n\tpublic function admin_menu() {\n\n\t\t/**\n\t\t * Filter the WP User capability required to access and run the setup wizard.\n\t\t *\n\t\t * The dynamic portion of this filter, `{$this->id}`, refers to the wizard type. E.g. \"setup\".\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param string $cap Required user capability. Default value is `install_plugins`.\n\t\t */\n\t\t$cap = apply_filters( \"llms_{$this->id}_wizard_access\", 'install_plugins' );\n\n\t\t$hook = add_dashboard_page(\n\t\t\t$this->title,\n\t\t\t'',\n\t\t\t$cap,\n\t\t\t'llms-' . $this->id,\n\t\t\tarray( $this, 'output' )\n\t\t);\n\n\t\tupdate_option( 'lifterlms_first_time_' . $this->id, 'yes' );\n\n\t\treturn $hook;\n\t}\n\n\t/**\n\t * Enqueue static assets for the setup wizard screens.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return bool\n\t */\n\tpublic function enqueue(): bool {\n\n\t\tif ( ! isset( $_GET['page'] ) || 'llms-' . $this->id !== $_GET['page'] ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn llms()->assets->enqueue_script( 'llms-admin-wizard' ) && llms()->assets->enqueue_style( 'llms-admin-wizard' );\n\n\t}\n\n\t/**\n\t * Retrieve the redirect URL at the conclusion of the wizard.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param int[] $course_ids WP_Post IDs of the course(s) generated during the import.\n\t * @return string\n\t */\n\tabstract protected function get_completed_url( array $course_ids ): string;\n\n\t/**\n\t * Retrieve the current step and default to the intro.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_current_step(): string {\n\t\t$step_keys = array_keys( $this->get_steps() );\n\n\t\t$step = llms_filter_input_sanitize_string( INPUT_GET, 'step' );\n\n\t\treturn $step ?? $step_keys[0] ?? '';\n\t}\n\n\t/**\n\t * Get slug if next step.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param string|bool $step Step to use as current.\n\t * @return string|bool\n\t */\n\tpublic function get_next_step( string $step = null ) {\n\t\t$step = $step ?? $this->get_current_step();\n\t\t$keys = array_keys( $this->get_steps() );\n\t\t$i    = array_search( $step, $keys, true );\n\n\t\t// Next step doesn't exist or the next step would be greater than the index of the last step.\n\t\tif ( false === $i || $i + 1 >= count( $keys ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $keys[ ++$i ] ?? false;\n\t}\n\n\t/**\n\t * Get slug if prev step.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param string|bool $step Step to use as current.\n\t * @return string|bool\n\t */\n\tpublic function get_prev_step( string $step = null ) {\n\t\t$step = $step ?? $this->get_current_step();\n\t\t$keys = array_keys( $this->get_steps() );\n\t\t$i    = array_search( $step, $keys, true );\n\n\t\tif ( false === $i || $i - 1 < 0 ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $keys[ $i - 1 ] ?? false;\n\t}\n\n\t/**\n\t * Get the text to display on the \"save\" buttons.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param string $step Step to get text for.\n\t * @return string The translated text.\n\t */\n\tprivate function get_save_text( string $step ): string {\n\n\t\t/**\n\t\t * Filter the Save button text for a given step in the setup wizard.\n\t\t *\n\t\t * The first dynamic portion of this hook, `$this->id`, refers to the type of wizard being displayed.\n\t\t *\n\t\t * The second dynamic portion of this hook, `$step`, refers to the slug of the current step.\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param string $text Button text string.\n\t\t */\n\t\t$text = apply_filters(\n\t\t\t\"llms_{$this->id}_wizard_get_{$step}_save_text\",\n\t\t\t$this->get_steps()[ $step ]['save'] ?? __( 'Save & Continue', 'lifterlms' )\n\t\t);\n\n\t\treturn esc_html( $text );\n\t}\n\n\t/**\n\t * Get the text to display on the \"skip\" buttons.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param string $step Step to get text for.\n\t * @return string Translated text.\n\t */\n\tprivate function get_skip_text( string $step ): string {\n\n\t\t/**\n\t\t * Filter the skip button text for a given step in the setup wizard.\n\t\t *\n\t\t * The first dynamic portion of this hook, `$this->id`, refers to the type of wizard being displayed.\n\t\t *\n\t\t * The second dynamic portion of this hook, `$step`, refers to the slug of the current step.\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param string $text Button text string.\n\t\t */\n\t\t$text = apply_filters(\n\t\t\t\"llms_{$this->id}_wizard_get_{$step}_skip_text\",\n\t\t\t$this->get_steps()[ $step ]['skip'] ?? __( 'Skip this step', 'lifterlms' )\n\t\t);\n\n\t\treturn esc_html( $text );\n\n\t}\n\n\t/**\n\t * Get the URL to a step.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param string $step Step slug.\n\t * @return string\n\t */\n\tprotected function get_step_url( string $step ): string {\n\n\t\t$args = array(\n\t\t\t'page' => 'llms-' . $this->id,\n\t\t\t'step' => $step,\n\t\t);\n\n\t\treturn add_query_arg( $args, admin_url() );\n\t}\n\n\t/**\n\t * Get an array of step slugs => titles.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_steps(): array {\n\n\t\t/**\n\t\t * Filter the steps included in the setup wizard.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->id`, refers to the type of wizard being displayed.\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param string[] $steps Array of setup wizard steps. The array key is the slug/id of the step and the array value\n\t\t *                        is the step's title displayed in the wizard's navigation.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->id}_wizard_steps\", $this->steps );\n\n\t}\n\n\t/**\n\t * Output the HTML content of the setup page.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function output(): void {\n\t\t$views_dir = trailingslashit( esc_attr( $this->views_dir ) );\n\t\t$step_html = '';\n\t\t$steps     = $this->get_steps();\n\t\t$current   = $this->get_current_step() ?? 'intro';\n\t\t$prev      = $this->get_prev_step();\n\t\t$next      = $this->get_next_step();\n\t\t$transient = $this->get_transient();\n\n\t\tif ( in_array( $current, array_keys( $steps ), true ) ) {\n\n\t\t\tob_start();\n\t\t\tinclude $views_dir . 'step-' . $current . '.php';\n\t\t\t$step_html = ob_get_clean();\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the HTML of a step within the setup wizard.\n\t\t *\n\t\t * The first dynamic portion of this hook, `$this->id`, refers to the type of wizard being displayed.\n\t\t *\n\t\t * The second dynamic portion of this hook, `$current`, refers to the slug of the current step.\n\t\t *\n\t\t * This filter can be used to output the HTML for a custom step in the setup wizard.\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param string                  $step_html HTML of the step.\n\t\t * @param LLMS_Admin_Setup_Wizard $wizard    Setup wizard class instance.\n\t\t */\n\t\t$step_html = apply_filters( \"llms_{$this->id}_wizard_{$current}_html\", $step_html, $this );\n\n\t\tinclude $views_dir . 'main.php';\n\n\t}\n\n\t/**\n\t * Handle saving data during setup.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @throws Exception If the current user does not have permission to save data.\n\t *\n\t * @return null|WP_Error\n\t */\n\tpublic function save(): ?WP_Error {\n\t\t$nonce  = \"llms_{$this->id}_nonce\";\n\t\t$action = \"llms_{$this->id}_save\";\n\n\t\tif ( ! isset( $_POST[ $nonce ] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ $nonce ] ) ), $action ) || ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$response = new WP_Error( \"llms-{$this->id}-save-invalid\", esc_html__( 'There was an error saving your data, please try again.', 'lifterlms' ) );\n\n\t\t$step = llms_filter_input( INPUT_POST, $action );\n\n\t\tif ( method_exists( $this, 'save_' . $step ) ) {\n\t\t\t$response = $this->{\"save_{$step}\"}();\n\t\t}\n\n\t\tif ( is_wp_error( $response ) ) {\n\t\t\t$this->error = $response;\n\t\t\treturn $response;\n\t\t}\n\n\t\tif ( ! is_array( $response ) ) {\n\t\t\t$response = array( $response );\n\t\t}\n\n\t\t$url = ( 'finish' === $step ) ? $this->get_completed_url( $response ) : $this->get_step_url( $this->get_next_step() );\n\n\t\tllms_redirect_and_exit( $url );\n\n\t\treturn null;\n\n\t}\n\n\t/**\n\t * Returns wizard transient if set.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_transient(): array {\n\t\treturn array();\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-controller-user-engagements.php",
    "content": "<?php\n/**\n * LLMS_Abstract_Controller_User_Engagements class\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Base class that handles awarded user engagements (achievements and certificates).\n *\n * @since 6.0.0\n */\nabstract class LLMS_Abstract_Controller_User_Engagements {\n\n\tuse LLMS_Trait_User_Engagement_Type;\n\n\t/**\n\t * A text type for a sync operation error message when the user can not edit an awarded engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_AWARDED_ENGAGEMENT_INSUFFICIENT_PERMISSIONS = 0;\n\n\t/**\n\t * A text type for a sync operation error message about an awarded engagement not having a valid engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_AWARDED_ENGAGEMENT_INVALID_TEMPLATE = 1;\n\n\t/**\n\t * A text type for a sync operation error message about the user not being able to edit awarded engagements.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_AWARDED_ENGAGEMENTS_INSUFFICIENT_PERMISSIONS = 2;\n\n\t/**\n\t * A text type for a sync operation error message about an invalid nonce.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_AWARDED_ENGAGEMENTS_INVALID_NONCE = 3;\n\n\t/**\n\t * A text type for a sync operation error message about a missing awarded engagement ID.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_MISSING_AWARDED_ENGAGEMENT_ID = 4;\n\n\t/**\n\t * A text type for a sync operation error message about a missing engagement template ID.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_MISSING_ENGAGEMENT_TEMPLATE_ID = 5;\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'init', array( $this, 'maybe_handle_awarded_engagement_sync_actions' ) );\n\t}\n\n\t/**\n\t * Delete an awarded user engagement.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Permanently delete user engagement via wp_delete_post().\n\t *              Refactored from LLMS_Controller_Achievements::delete() and LLMS_Controller_Certificates::delete().\n\t *\n\t * @param int $post_id WP Post ID of the awarded engagement.\n\t * @return void\n\t */\n\tprotected function delete( $post_id ) {\n\n\t\t// Only allow LLMS admins to delete. is_admin() check also makes sure call is made from the dashboard.\n\t\tif ( ! is_admin() || ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$post = get_post( $post_id );\n\t\tif ( ! $post || ! in_array( $post->post_type, array( 'llms_my_achievement', 'llms_my_certificate' ), true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\twp_delete_post( $post_id, true );\n\t}\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Controller_User_Engagements::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\treturn __( 'Invalid text type.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Handle awarded engagement sync actions.\n\t *\n\t * Errors are added to {@see LLMS_Admin_Metabox::add_error()} to be displayed as an admin notice\n\t * and also returned for unit tests.\n\t *\n\t * If the sync is successful, the {@see llms_redirect_and_exit()} function is called and this method does not return.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return null|WP_Error\n\t */\n\tpublic function maybe_handle_awarded_engagement_sync_actions() {\n\n\t\t// Validate action.\n\t\t// Invalid actions return a WP_Error for testing purposes and are not displayed to the user.\n\t\t$actions = array(\n\t\t\t'sync_one'  => \"sync_awarded_{$this->engagement_type}\",\n\t\t\t'sync_many' => \"sync_awarded_{$this->engagement_type}s\",\n\t\t);\n\t\t$action  = llms_filter_input( INPUT_GET, 'action' );\n\t\tif ( ! $action ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t\"llms-sync-awarded-{$this->engagement_type}s-missing-action\",\n\t\t\t\t__( 'Sorry, you have not provided any actions.', 'lifterlms' )\n\t\t\t);\n\t\t} elseif ( ! in_array( $action, $actions, true ) ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t\"llms-sync-awarded-{$this->engagement_type}s-invalid-action\",\n\t\t\t\t__( \"You're trying to perform an invalid action.\", 'lifterlms' )\n\t\t\t);\n\t\t}\n\n\t\t// Verify nonce.\n\t\t$nonce_field = \"_llms_{$this->engagement_type}_sync_actions_nonce\";\n\t\tif ( ! isset( $_REQUEST[ $nonce_field ] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_field ] ) ), \"llms-{$this->engagement_type}-sync-actions\" ) ) {\n\t\t\t$result = new WP_Error(\n\t\t\t\t\"llms-sync-awarded-{$this->engagement_type}s-invalid-nonce\",\n\t\t\t\t$this->get_text( self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INVALID_NONCE )\n\t\t\t);\n\t\t\t( new LLMS_Meta_Box_Award_Engagement_Submit() )->add_error( $result );\n\n\t\t\treturn $result;\n\t\t}\n\n\t\t$engagement_id  = llms_filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT );\n\t\t$is_syncing_one = $action === $actions['sync_one'];\n\n\t\tif ( empty( $engagement_id ) ) {\n\t\t\tif ( $is_syncing_one ) {\n\t\t\t\t$code    = \"llms-sync-missing-awarded-{$this->engagement_type}-id\";\n\t\t\t\t$message = $this->get_text( self::TEXT_SYNC_MISSING_AWARDED_ENGAGEMENT_ID );\n\t\t\t} else {\n\t\t\t\t$code    = \"llms-sync-missing-{$this->engagement_type}-template-id\";\n\t\t\t\t$message = $this->get_text( self::TEXT_SYNC_MISSING_ENGAGEMENT_TEMPLATE_ID );\n\t\t\t}\n\t\t\t$result = new WP_Error( $code, $message );\n\t\t} elseif ( $is_syncing_one ) {\n\t\t\t$result = $this->sync_awarded_engagement( $engagement_id );\n\t\t} else {\n\t\t\t$result = $this->sync_awarded_engagements( $engagement_id );\n\t\t}\n\n\t\tif ( is_wp_error( $result ) ) {\n\t\t\t( new LLMS_Meta_Box_Award_Engagement_Submit() )->add_error( $result );\n\t\t}\n\n\t\treturn $result;\n\t}\n\n\t/**\n\t * Sync an awarded engagement with its template.\n\t *\n\t * If the sync is successful, the {@see llms_redirect_and_exit()} function is called and this method does not return.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $engagement_id Awarded engagement id.\n\t * @return void|WP_Error\n\t */\n\tprivate function sync_awarded_engagement( $engagement_id ) {\n\n\t\tif ( ! current_user_can( 'edit_post', $engagement_id ) ) {\n\t\t\t$variables = compact( 'engagement_id' );\n\t\t\treturn new WP_Error(\n\t\t\t\t\"llms-sync-awarded-{$this->engagement_type}-insufficient-permissions\",\n\t\t\t\t$this->get_text( self::TEXT_SYNC_AWARDED_ENGAGEMENT_INSUFFICIENT_PERMISSIONS, $variables ),\n\t\t\t\t$variables\n\t\t\t);\n\t\t}\n\n\t\t$sync = $this->get_user_engagement( $engagement_id, true )->sync();\n\t\tif ( ! $sync ) {\n\t\t\t$variables = compact( 'engagement_id' );\n\t\t\treturn new WP_Error(\n\t\t\t\t\"llms-sync-awarded-{$this->engagement_type}-invalid-template\",\n\t\t\t\t$this->get_text( self::TEXT_SYNC_AWARDED_ENGAGEMENT_INVALID_TEMPLATE, $variables ),\n\t\t\t\t$variables\n\t\t\t);\n\t\t}\n\n\t\t$redirect_url = get_edit_post_link( $engagement_id, 'raw' );\n\t\t$redirect_url = add_query_arg( 'message', 1, $redirect_url );\n\t\tllms_redirect_and_exit( $redirect_url );\n\t}\n\n\t/**\n\t * Sync all the awarded engagements with their template.\n\t *\n\t * If the preflight checks are successful, the {@see llms_redirect_and_exit()} function is called after the sync is\n\t * triggered and this method does not return.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $user_engagement_template_id User engagement template ID.\n\t * @return void|WP_Error\n\t */\n\tprivate function sync_awarded_engagements( $user_engagement_template_id ) {\n\n\t\tif ( ! current_user_can( get_post_type_object( \"llms_my_{$this->engagement_type}\" )->cap->edit_posts ) ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t\"llms-sync-awarded-{$this->engagement_type}s-insufficient-permissions\",\n\t\t\t\t$this->get_text( self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INSUFFICIENT_PERMISSIONS )\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * Fires an action to trigger the bulk sync of awarded engagements.\n\t\t *\n\t\t * The dynamic portion of this hook, `{$this->engagement_type}`, refers to the type of awarded engagement,\n\t\t * either \"achievement\" or \"certificate\".\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @see LLMS_Processor_Certificate_Sync.\n\t\t *\n\t\t * @param int $user_engagement_template_id The user engagement template post ID.\n\t\t */\n\t\tdo_action( \"llms_do_awarded_{$this->engagement_type}s_bulk_sync\", $user_engagement_template_id );\n\n\t\tif ( empty( $_SERVER['HTTP_REFERER'] ) ) {\n\t\t\tllms_redirect_and_exit( get_edit_post_link( $user_engagement_template_id, 'raw' ) );\n\t\t} else {\n\t\t\tllms_redirect_and_exit( sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-email-provider.php",
    "content": "<?php\n/**\n * Base class used by email delivery provider \"connector\" classes\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.40.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Abstract_Email_Provider\n *\n * @since 3.40.0\n * @since 6.0.0 Removed the deprecated `LLMS_Abstract_Email_Provider::output_css()` method.\n */\nabstract class LLMS_Abstract_Email_Provider {\n\n\t/**\n\t * Connector's ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = '';\n\n\t/**\n\t * Array of supported providers.\n\t *\n\t * @var array\n\t */\n\tprotected $providers = array(\n\t\t'mailhawk',\n\t\t'sendwp',\n\t);\n\n\t/**\n\t * Configures the response returned when `do_remote_install()` is successful.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return array\n\t */\n\tabstract protected function do_remote_install_success();\n\n\t/**\n\t * Retrieve the settings area HTML for the connect button\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function get_connect_setting();\n\n\t/**\n\t * Retrieve description text to be used in the settings area.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function get_description();\n\n\t/**\n\t * Retrieve the connector's name / title.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function get_title();\n\n\t/**\n\t * Determines if connector plugin is connected for sending.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean\n\t */\n\tabstract protected function is_connected();\n\n\t/**\n\t * Determines if connector plugin is installed\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean\n\t */\n\tabstract protected function is_installed();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t/**\n\t\t * Filter the available email providers\n\t\t *\n\t\t * @since 3.40.0\n\t\t *\n\t\t * @param string[] $this->providers List of email provider ids.\n\t\t */\n\t\t$this->providers = apply_filters( 'llms_email_delivery_providers', $this->providers );\n\n\t\t/**\n\t\t * Dynamically adjust the priority.\n\t\t *\n\t\t * A \"connected\" provider will always load first, ensuring\n\t\t * that it can disable the other providers.\n\t\t *\n\t\t * When no providers are connected, they'll all load at 10\n\t\t * and display in alphabetical order as a result of the order\n\t\t * the files are included.\n\t\t */\n\t\t$priority = $this->is_connected() ? 5 : 10;\n\t\tadd_action( 'admin_init', array( $this, 'init' ), $priority );\n\t}\n\n\t/**\n\t * Initialize the Connector\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\n\t\t// Disable other email delivery services if the current connector is already connected.\n\t\tif ( $this->is_connected() ) {\n\t\t\t$this->disable_other_providers();\n\t\t}\n\n\t\t/**\n\t\t * Disable the Connector class and settings\n\t\t *\n\t\t * The dynamic portion of this filter, `{$this->id}`, refers\n\t\t * to the id of the email provider. See `$this->providers` for a list of supported providers.\n\t\t *\n\t\t * @since 3.40.0\n\t\t *\n\t\t * @param bool $disabled Whether or not this class is disabled.\n\t\t */\n\t\tif ( apply_filters( \"llms_disable_{$this->id}\", false ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tadd_filter( 'llms_email_delivery_services', array( $this, 'add_settings' ) );\n\t\tadd_action( 'wp_ajax_llms_' . $this->id . '_remote_install', array( $this, 'ajax_callback_remote_install' ) );\n\t\tadd_action( 'wp_ajax_llms_' . $this->id . '_remote_install_verify', array( $this, 'ajax_callback_remote_install_verify' ) );\n\n\t\tadd_action( 'admin_print_footer_scripts', array( $this, 'output_js' ) );\n\t}\n\n\t/**\n\t * Determines if the plugin is already installed and activates it if it is\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean|WP_Error `true` when plugin is installed and successfully activated.\n\t *                           `WP_Error` when plugin is installed and there was an error activating it.\n\t *                           `false` when plugin is not installed.\n\t */\n\tprotected function activate_already_installed_plugin() {\n\n\t\t$is_plugin_installed = false;\n\n\t\tforeach ( get_plugins() as $path => $details ) {\n\t\t\tif ( false === strpos( $path, '/' . $this->id . '.php' ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$is_plugin_installed = true;\n\t\t\t$activate            = activate_plugin( $path );\n\t\t\tif ( is_wp_error( $activate ) ) {\n\t\t\t\treturn $activate;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\treturn $is_plugin_installed;\n\t}\n\n\t/**\n\t * Add Settings.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @param array $settings Existing settings.\n\t * @return array\n\t */\n\tpublic function add_settings( $settings ) {\n\n\t\t// Short circuit if missing authorization.\n\t\tif ( ! current_user_can( 'install_plugins' ) ) {\n\t\t\treturn $settings;\n\t\t}\n\n\t\t$new_settings = array(\n\t\t\tarray(\n\t\t\t\t'id'    => $this->id . '_title',\n\t\t\t\t'type'  => 'subtitle',\n\t\t\t\t'title' => $this->get_title(),\n\t\t\t\t'desc'  => $this->is_connected() ? '' : $this->get_description(),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'    => $this->id . '_connect',\n\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t'value' => $this->get_connect_setting(),\n\t\t\t),\n\t\t);\n\n\t\treturn array_merge( $settings, $new_settings );\n\t}\n\n\t/**\n\t * Ajax callback for installing the connector's plugin.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function ajax_callback_remote_install() {\n\n\t\t$ret = $this->do_remote_install();\n\t\tob_clean();\n\t\twp_send_json( $ret, ! empty( $ret['status'] ) ? $ret['status'] : 200 );\n\t}\n\n\t/**\n\t * Ajax callback called after doing the initial install, so the plugin is loaded and available.\n\t *\n\t * @since 8.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function ajax_callback_remote_install_verify() {\n\n\t\t$ret = $this->do_remote_install_verify();\n\t\tob_clean();\n\t\twp_send_json( $ret, ! empty( $ret['status'] ) ? $ret['status'] : 200 );\n\t}\n\n\t/**\n\t * Determines if the current user can perform the remote installation.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return true|array\n\t */\n\tprotected function can_remote_install() {\n\n\t\t$nonce_field = '_llms_' . $this->id . '_nonce';\n\t\tif ( ! isset( $_REQUEST[ $nonce_field ] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_field ] ) ), 'llms-' . $this->id . '-install' ) ) {\n\t\t\treturn array(\n\t\t\t\t'code'    => 'llms_' . $this->id . '_install_nonce_failure',\n\t\t\t\t'message' => esc_html__( 'Security check failed.', 'lifterlms' ),\n\t\t\t\t'status'  => 401,\n\t\t\t);\n\t\t} elseif ( ! current_user_can( 'install_plugins' ) ) {\n\t\t\treturn array(\n\t\t\t\t'code'    => 'llms_' . $this->id . '_install_unauthorized',\n\t\t\t\t'message' => esc_html__( 'You do not have permission to perform this action.', 'lifterlms' ),\n\t\t\t\t'status'  => 403,\n\t\t\t);\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Automatically disables other providers when the current provider is connected.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void.\n\t */\n\tprotected function disable_other_providers() {\n\n\t\t$disable = array_diff( $this->providers, array( $this->id ) );\n\t\tforeach ( $disable as $id ) {\n\t\t\tadd_filter( 'llms_disable_' . $id, '__return_true' );\n\t\t}\n\t}\n\n\t/**\n\t * Validate installation request and perform the plugin install or return errors.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return array\n\t */\n\tprotected function do_remote_install() {\n\n\t\t$can_install = $this->can_remote_install();\n\t\tif ( true !== $can_install ) {\n\t\t\treturn $can_install;\n\t\t}\n\n\t\t$install = $this->install();\n\n\t\tif ( is_wp_error( $install ) ) {\n\t\t\treturn array(\n\t\t\t\t'code'    => $install->get_error_code(),\n\t\t\t\t'message' => $install->get_error_message(),\n\t\t\t\t'status'  => 400,\n\t\t\t);\n\t\t}\n\n\t\treturn array( 'success' => true );\n\t}\n\n\t/**\n\t * Verify the remote install, and then perform the post-install response. Otherwise the plugin isn't available yet.\n\t *\n\t * @return array\n\t */\n\tprotected function do_remote_install_verify() {\n\n\t\t$ret = $this->do_remote_install();\n\n\t\tif ( is_wp_error( $ret ) ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\tif ( ! $this->is_installed() ) {\n\t\t\treturn array(\n\t\t\t\t'code'    => 'llms_' . $this->id . '_not_found',\n\t\t\t\t/* translators: %s: title of the email delivery plugin. */\n\t\t\t\t'message' => sprintf( __( '%s plugin not found. Please try again.', 'lifterlms' ), $this->get_title() ),\n\t\t\t\t'status'  => 400,\n\t\t\t);\n\t\t}\n\n\t\treturn $this->do_remote_install_success();\n\t}\n\n\t/**\n\t * Install the plugin via the WP plugin installer.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean|WP_Error Error object or `true` when successful.\n\t */\n\tprotected function install() {\n\n\t\t// Check if the plugin already exists and activate it if it is.\n\t\t$ret = $this->activate_already_installed_plugin();\n\n\t\t// Plugin doesn't exist, install it.\n\t\tif ( false === $ret ) {\n\t\t\t$ret = $this->install_plugin();\n\t\t}\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Install the plugin via the WP Plugin Repo.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean|WP_Error `true` on success, error object otherwise.\n\t */\n\tprotected function install_plugin() {\n\n\t\tinclude_once ABSPATH . 'wp-admin/includes/plugin-install.php';\n\t\tinclude_once ABSPATH . 'wp-admin/includes/file.php';\n\t\tinclude_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';\n\n\t\t// Use the WordPress Plugins API to get the plugin download link.\n\t\t$api = plugins_api(\n\t\t\t'plugin_information',\n\t\t\tarray(\n\t\t\t\t'slug' => $this->id,\n\t\t\t)\n\t\t);\n\t\tif ( is_wp_error( $api ) ) {\n\t\t\treturn $api;\n\t\t}\n\n\t\t// Use the AJAX upgrader skin to quietly install the plugin.\n\t\t$upgrader = new Plugin_Upgrader( new WP_Ajax_Upgrader_Skin() );\n\t\t$install  = $upgrader->install( $api->download_link );\n\t\tif ( is_wp_error( $install ) ) {\n\t\t\treturn $install;\n\t\t}\n\n\t\t$activate = activate_plugin( $upgrader->plugin_info() );\n\t\tif ( is_wp_error( $activate ) ) {\n\t\t\treturn $activate;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Determine if inline scripts and styles should be output.\n\t *\n\t * @since 3.40.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return bool\n\t */\n\tprotected function should_output_inline() {\n\n\t\t// Short circuit if unauthorized.\n\t\tif ( ! current_user_can( 'install_plugins' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$screen = get_current_screen();\n\t\treturn ( 'lifterlms_page_llms-settings' === $screen->id && 'engagements' === llms_filter_input( INPUT_GET, 'tab' ) && ! $this->is_connected() );\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-generator-posts.php",
    "content": "<?php\n/**\n * Generate LMS Content from export files or raw arrays of data.\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 4.7.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Abstract_Generator_Posts class.\n *\n * Many methods in this class were moved from `LLMS_Generator`. The move has been\n * noted on these methods and their preexisting changelogs have been preserved.\n *\n * @since 4.7.0\n * @since 6.0.0 Removed the deprecated `LLMS_Abstract_Generator_Posts::increment()` method.\n */\nabstract class LLMS_Abstract_Generator_Posts {\n\n\t/**\n\t * Exception code: WP_Post creation error\n\t *\n\t * @var int\n\t */\n\tconst ERROR_CREATE_POST = 1000;\n\n\t/**\n\t * Exception code: WP_Term creation error\n\t *\n\t * @var int\n\t */\n\tconst ERROR_CREATE_TERM = 1001;\n\n\t/**\n\t * Exception code: WP_User creation error\n\t *\n\t * @var int\n\t */\n\tconst ERROR_CREATE_USER = 1002;\n\n\t/**\n\t * Exception code: Requested LLMS_Post_Model subclass does not exist.\n\t *\n\t * @var int\n\t */\n\tconst ERROR_INVALID_POST = 1100;\n\n\t/**\n\t * Default post status when status isn't set in $raw for a given post\n\t *\n\t * @var string\n\t */\n\tprivate $default_post_status = 'draft';\n\n\t/**\n\t * Array of images that have been sideloaded during generation\n\t *\n\t * Each array key will be the original source URL and the array value will be the new\n\t * attachment post ID of the image that has been sideloaded into the current site.\n\t *\n\t * This array is checked prior to sideloading an image to ensure that if the same image is\n\t * used multiple times throughout an import, the image is only sideloaded a single time.\n\t *\n\t * @var array\n\t */\n\tprotected $images = array();\n\n\t/**\n\t * Array of reusable blocks that have been imported during generation\n\t *\n\t * Each array key will be the original block ID and the array value will be the new\n\t * block ID.\n\t *\n\t * This array is checked prior to importing a reusable block to ensure that if the same\n\t * block is used multiple times throughout an import, it will only be imported once.\n\t *\n\t * @var array\n\t */\n\tprotected $reusable_blocks = array();\n\n\t/**\n\t * Associate raw tempids with actual created ids\n\t *\n\t * @var array\n\t */\n\tprotected $tempids = array();\n\n\t/**\n\t * Construct a new generator instance with data\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Load deps.\n\t\t$this->load_dependencies();\n\t}\n\n\t/**\n\t * Add custom data to a post based on the 'custom' array\n\t *\n\t * @since 3.16.11\n\t * @since 3.28.3 Add extra slashes around JSON strings.\n\t * @since 3.30.2 Skip JSON evaluation for non-string values; make publicly accessible.\n\t * @since 4.7.0 Moved from `LLMS_Generator`.\n\t *\n\t * @param int   $post_id WP Post ID.\n\t * @param array $raw     Raw data.\n\t * @return void\n\t */\n\tpublic function add_custom_values( $post_id, $raw ) {\n\n\t\t// No custom data, return early.\n\t\tif ( empty( $raw['custom'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tforeach ( $raw['custom'] as $custom_key => $custom_vals ) {\n\t\t\tforeach ( $custom_vals as $val ) {\n\t\t\t\t$this->add_custom_value( $post_id, $custom_key, $val );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Add a \"custom\" post meta data for a given post\n\t *\n\t * Automatically slashes JSON data when supplied.\n\t *\n\t * Automatically unserializes serialized data so `add_post_meta()` can re-serialize.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param int    $post_id WP_Post ID.\n\t * @param string $key     Meta key.\n\t * @param mixed  $val     Meta value.\n\t * @return void\n\t */\n\tprotected function add_custom_value( $post_id, $key, $val ) {\n\n\t\t// If $val is a JSON string, add slashes before saving.\n\t\tif ( is_string( $val ) && null !== json_decode( $val, true ) ) {\n\t\t\t$val = wp_slash( $val );\n\t\t}\n\n\t\tadd_post_meta( $post_id, $key, maybe_unserialize( $val ) );\n\t}\n\n\t/**\n\t * Generate a new LLMS_Post_Model.\n\t *\n\t * @since 4.7.0\n\t * @since 4.7.1 Set the post's excerpt during the initial insert instead of during metadata updates after creation.\n\t * @since 7.3.0 Skip adding the `generated_from_id` meta from the original post: this is the case when cloning a cloned post.\n\t *              Also skip creating revisions.\n\t *\n\t * @param string $type      The LLMS_Post_Model post type type. For example \"course\" for an `LLMS_Course` or `membership` for `LLMS_Membership`.\n\t * @param array  $raw       Array of raw, used to create the post.\n\t * @param int    $author_id Fallback author ID, used when now author data can be found in `$raw`.\n\t * @return LLMS_Post_Model\n\t *\n\t * @throws Exception When the class identified by `$type` is not found or when an error is encountered during post creation.\n\t */\n\tprotected function create_post( $type, $raw = array(), $author_id = null ) {\n\n\t\t$class_name = sprintf( 'LLMS_%s', implode( '_', array_map( 'ucfirst', explode( '_', $type ) ) ) );\n\t\tif ( ! class_exists( $class_name ) ) {\n\t\t\t/* translators: %s: Name of class. */\n\t\t\tthrow new Exception( esc_html( sprintf( __( 'The class \"%s\" does not exist.', 'lifterlms' ), $class_name ) ), intval( self::ERROR_INVALID_POST ) );\n\t\t}\n\n\t\t// Don't create useless revision on \"cloning\".\n\t\tadd_filter( 'wp_revisions_to_keep', '__return_zero', 999 );\n\n\t\t// Insert the object.\n\t\t$post = new $class_name(\n\t\t\t'new',\n\t\t\t/**\n\t\t\t * Filter the data used to generate a new post.\n\t\t\t *\n\t\t\t * @since 7.4.0\n\t\t\t *\n\t\t\t * @param array $new_post_data New post data array.\n\t\t\t * @param array $raw           Original raw post data array.\n\t\t\t */\n\t\t\tapply_filters(\n\t\t\t\t'llms_generator_new_post_data',\n\t\t\t\tarray(\n\t\t\t\t\t'post_author'   => $this->get_author_id_from_raw( $raw, $author_id ),\n\t\t\t\t\t'post_content'  => isset( $raw['content'] ) ? $raw['content'] : '',\n\t\t\t\t\t'post_date'     => isset( $raw['date'] ) ? $this->format_date( $raw['date'] ) : null,\n\t\t\t\t\t'post_excerpt'  => isset( $raw['excerpt'] ) ? $raw['excerpt'] : '',\n\t\t\t\t\t'post_modified' => isset( $raw['modified'] ) ? $this->format_date( $raw['modified'] ) : null,\n\t\t\t\t\t'post_status'   => isset( $raw['status'] ) ? $raw['status'] : $this->get_default_post_status(),\n\t\t\t\t\t'post_title'    => $raw['title'],\n\t\t\t\t),\n\t\t\t\t$raw\n\t\t\t)\n\t\t);\n\n\t\tif ( ! $post->get( 'id' ) ) {\n\t\t\t// Translators: %s = post type name.\n\t\t\tthrow new Exception( esc_html( sprintf( __( 'Error creating the %s post object.', 'lifterlms' ), $type ) ), intval( self::ERROR_CREATE_POST ) );\n\t\t}\n\n\t\t// Store the temp id if it exists.\n\t\t$this->store_temp_id( $raw, $post );\n\n\t\t// Don't set these values again.\n\t\tunset( $raw['id'], $raw['author'], $raw['content'], $raw['date'], $raw['excerpt'], $raw['modified'], $raw['name'], $raw['status'], $raw['title'] );\n\t\t/**\n\t\t * Skip adding the `generated_from_id` meta from the original post:\n\t\t * this is the case when cloning a cloned post.\n\t\t */\n\t\tunset( $raw['custom'][ $post->get( 'meta_prefix' ) . 'generated_from_id' ] );\n\n\t\t$this->set_metadata( $post, $raw );\n\t\t$this->set_featured_image( $raw, $post->get( 'id' ) );\n\t\t$this->add_custom_values( $post->get( 'id' ), $raw );\n\t\t$this->sideload_images( $post, $raw );\n\t\t$this->handle_reusable_blocks( $post, $raw );\n\n\t\t// Remove revision prevention.\n\t\tremove_filter( 'wp_revisions_to_keep', '__return_zero', 999 );\n\n\t\treturn $post;\n\t}\n\n\t/**\n\t * Creates a reusable block\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param int   $block_id WP_Post ID of the block being imported. This will be the ID as found on the original site.\n\t * @param array $block    {\n\t *     Array of block data.\n\t *\n\t *     @type string $title   Title of the reusable block.\n\t *     @type string $content Content of the reusable block.\n\t * }\n\t * @return bool|int The WP_Post ID of the new block on success or `false` on error.\n\t */\n\tprotected function create_reusable_block( $block_id, $block ) {\n\n\t\t$block_id = absint( $block_id );\n\n\t\t// Check if the block was previously imported.\n\t\t$id = empty( $this->reusable_blocks[ $block_id ] ) ? false : $this->reusable_blocks[ $block_id ];\n\t\tif ( ! $id ) {\n\n\t\t\t// If the block already exists, don't create it again.\n\t\t\t$existing = get_post( $block_id );\n\t\t\tif ( $existing && 'wp_block' === $existing->post_type && $block['title'] === $existing->post_title && $block['content'] === $existing->post_content ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t$id = $this->insert_resuable_block( $block_id, $block );\n\t\t}\n\n\t\t// Don't return 0 if `wp_insert_post()` fails.\n\t\treturn $id ? $id : false;\n\t}\n\n\t/**\n\t * Create a new WP_User from raw data\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param array $raw Raw data.\n\t * @return int|WP_Error WP_User ID on success or error on failure.\n\t */\n\tprotected function create_user( $raw ) {\n\n\t\t/**\n\t\t * Filter the default role used to create a new user during generator imports\n\t\t *\n\t\t * This role is used a role isn't supplied in the raw data.\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param string $role WP_User role. Default role is 'administrator'.\n\t\t * @param array  $raw  Original raw author data.\n\t\t */\n\t\t$raw['role'] = empty( $raw['role'] ) ? apply_filters( 'llms_generator_new_user_default_role', 'administrator', $raw ) : $raw['role'];\n\n\t\t$data = array(\n\t\t\t'role'       => $raw['role'],\n\t\t\t'user_email' => $raw['email'],\n\t\t\t'user_login' => LLMS_Person_Handler::generate_username( $raw['email'] ),\n\t\t\t'user_pass'  => wp_generate_password(),\n\t\t);\n\n\t\tif ( isset( $raw['first_name'] ) && isset( $raw['last_name'] ) ) {\n\t\t\t$data['display_name'] = $raw['first_name'] . ' ' . $raw['last_name'];\n\t\t\t$data['first_name']   = $raw['first_name'];\n\t\t\t$data['last_name']    = $raw['last_name'];\n\t\t}\n\n\t\tif ( isset( $raw['description'] ) ) {\n\t\t\t$data['description'] = $raw['description'];\n\t\t}\n\n\t\t/**\n\t\t * Filter user data used to create a new user during generator imports\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array $data Prepared user data to be passed to `wp_insert_user()`.\n\t\t * @param array $raw  Original raw author data.\n\t\t */\n\t\t$data      = apply_filters( 'llms_generator_new_author_data', $data, $raw );\n\t\t$author_id = wp_insert_user( $data );\n\n\t\tif ( ! is_wp_error( $author_id ) ) {\n\t\t\t/**\n\t\t\t * Action fired after creation of a new user during generation\n\t\t\t *\n\t\t\t *  @since 4.7.0\n\t\t\t *\n\t\t\t * @param int   $author_id WP_User ID.\n\t\t\t * @param array $data      User creation data passed to `wp_insert_user()`.\n\t\t\t * @param array $raw       Original raw author data.\n\t\t\t */\n\t\t\tdo_action( 'llms_generator_new_user', $author_id, $data, $raw );\n\t\t}\n\n\t\treturn $author_id;\n\t}\n\n\t/**\n\t * Ensure raw dates are correctly formatted to create a post date\n\t *\n\t * Falls back to current date if no date is supplied.\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Made publicly accessible.\n\t * @since 4.7.0 Use `llms_current_time()` in favor of `current_time()`.\n\t *\n\t * @param string $raw_date Raw date from raw object.\n\t * @return string\n\t */\n\tpublic function format_date( $raw_date = null ) {\n\n\t\tif ( ! $raw_date ) {\n\t\t\treturn llms_current_time( 'mysql' );\n\t\t}\n\n\t\treturn date( 'Y-m-d H:i:s', strtotime( $raw_date ) );\n\t}\n\n\t/**\n\t * Accepts raw author data and locates an existing author by email or id or creates one\n\t *\n\t * @since 3.3.0\n\t * @since 4.3.3 Use strict string comparator.\n\t * @since 4.7.0 Moved from `LLMS_Generator` and made `protected` instead of `private`.\n\t *\n\t * @param array $raw Author data.\n\t *                   If id and email are provided will use id only if it matches the email for user matching that id in the database.\n\t *                   If no id found, attempts to locate by email.\n\t *                   If no author found and email provided, creates new user using email.\n\t *                   Falls back to current user id.\n\t *                   First_name, last_name, and description can be optionally provided.\n\t *                   When provided will be used only when creating a new user.\n\t * @return int WP_User ID\n\t *\n\t * @throws Exception When an error is encountered creating a new user.\n\t */\n\tprotected function get_author_id( $raw ) {\n\n\t\t$author_id = 0;\n\n\t\t// If raw is missing an ID and Email, use current user id.\n\t\tif ( ! isset( $raw['id'] ) && ! isset( $raw['email'] ) ) {\n\t\t\t$author_id = get_current_user_id();\n\t\t} else {\n\n\t\t\t// If id is set, check if the id matches a user in the DB.\n\t\t\tif ( isset( $raw['id'] ) && is_numeric( $raw['id'] ) ) {\n\n\t\t\t\t$user = get_user_by( 'ID', $raw['id'] );\n\n\t\t\t\t// User exists.\n\t\t\t\tif ( $user ) {\n\n\t\t\t\t\t// We have a raw email.\n\t\t\t\t\tif ( isset( $raw['email'] ) ) {\n\n\t\t\t\t\t\t// Raw email matches found user's email.\n\t\t\t\t\t\tif ( $user->user_email === $raw['email'] ) {\n\t\t\t\t\t\t\t$author_id = $user->ID;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$author_id = $user->ID;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( ! $author_id ) {\n\n\t\t\t\tif ( isset( $raw['email'] ) ) {\n\n\t\t\t\t\t// See if we have a user that matches by email.\n\t\t\t\t\t$user = get_user_by( 'email', $raw['email'] );\n\n\t\t\t\t\t// User exists, use this user.\n\t\t\t\t\tif ( $user ) {\n\t\t\t\t\t\t$author_id = $user->ID;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// No author id, create a new one using the email.\n\t\t\tif ( ! $author_id && isset( $raw['email'] ) ) {\n\n\t\t\t\t$author_id = $this->create_user( $raw );\n\n\t\t\t\tif ( is_wp_error( $author_id ) ) {\n\t\t\t\t\tthrow new Exception( esc_html( $author_id->get_error_message() ), intval( self::ERROR_CREATE_USER ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter the author ID prior to it being used for the generation of new posts\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param int   $author_id WP_User ID of the author.\n\t\t * @param array $raw       Original raw author data.\n\t\t */\n\t\treturn apply_filters( 'llms_generator_get_author_id', $author_id, $raw );\n\t}\n\n\t/**\n\t * Receives a raw array of course, plan, section, lesson, etc data and gets an author id\n\t *\n\t * Falls back to optionally supplied fallback id.\n\t * Falls back to current user id.\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Made publicly accessible.\n\t * @since 4.7.0 Moved from `LLMS_Generators`.\n\t *\n\t * @param array $raw                Raw data.\n\t * @param int   $fallback_author_id Optional. WP User ID. Default is `null`.\n\t *                                  If not supplied, if no author is set, the current user ID will be used.\n\t * @return int|WP_Error\n\t */\n\tpublic function get_author_id_from_raw( $raw, $fallback_author_id = null ) {\n\n\t\t// If author is set, get the author id.\n\t\tif ( isset( $raw['author'] ) ) {\n\t\t\t$author_id = $this->get_author_id( $raw['author'] );\n\t\t}\n\n\t\t// Fallback to current user.\n\t\tif ( empty( $author_id ) ) {\n\t\t\t$author_id = ! empty( $fallback_author_id ) ? $fallback_author_id : get_current_user_id();\n\t\t}\n\n\t\treturn $author_id;\n\t}\n\n\t/**\n\t * Retrieve the default post status for the generated set of posts\n\t *\n\t * @since 3.7.3\n\t * @since 3.30.2 Made publicly accessible.\n\t * @since 4.7.0 Moved from `LLMS_Generators`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_default_post_status() {\n\n\t\t/**\n\t\t * Filter the default status used for generating posts\n\t\t *\n\t\t * @since 3.7.3\n\t\t *\n\t\t * @param string         $post_status The default post status.\n\t\t * @param LLMS_Generator $generator   Generator instance.\n\t\t */\n\t\treturn apply_filters( 'llms_generator_default_post_status', $this->default_post_status, $this );\n\t}\n\n\t/**\n\t * Get a WP Term ID for a term by taxonomy and term name\n\t *\n\t * Attempts to find a given term by name first to prevent duplicates during imports.\n\t *\n\t * @since 3.3.0\n\t * @since 4.7.0 Moved from `LLMS_Generator` and updated method access from `private` to `protected`.\n\t *               Throws an exception in favor of returning `null` when an error is encountered.\n\t *\n\t * @param string $term_name Term name.\n\t * @param string $tax       Taxonomy slug.\n\t * @return int The created WP_Term `term_id`.\n\t *\n\t * @throws Exception When an error is encountered during taxonomy term creation.\n\t */\n\tprotected function get_term_id( $term_name, $tax ) {\n\n\t\t$term = get_term_by( 'name', $term_name, $tax, ARRAY_A );\n\n\t\t// Not found, create it.\n\t\tif ( ! $term ) {\n\n\t\t\t$term = wp_insert_term( $term_name, $tax );\n\n\t\t\tif ( is_wp_error( $term ) ) {\n\t\t\t\t/* translators: %s: name of term. */\n\t\t\t\tthrow new Exception( esc_html( sprintf( __( 'Error creating new term \"%s\".', 'lifterlms' ), $term_name ) ), intval( self::ERROR_CREATE_TERM ) );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Triggered when a new term is generated during an import\n\t\t\t *\n\t\t\t * @since 4.7.0\n\t\t\t *\n\t\t\t * @param array  $term Term information array from `wp_insert_term()`.\n\t\t\t * @param string $tax  Taxonomy name.\n\t\t\t */\n\t\t\tdo_action( 'llms_generator_new_term', $term, $tax );\n\n\t\t}\n\n\t\treturn $term['term_id'];\n\t}\n\n\t/**\n\t * Handle importing of reusable blocks stored in post content\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param LLMS_Post_Model $post Instance of a post model.\n\t * @param array           $raw  Array of raw data.\n\t * @return null|bool Returns `null` when importing is disabled, `false` when there are no blocks to import, and `true` on success.\n\t */\n\tprotected function handle_reusable_blocks( $post, $raw ) {\n\n\t\t// Importing blocks is disabled.\n\t\tif ( ! $this->is_reusable_block_importing_enabled() ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// No blocks to import.\n\t\tif ( empty( $raw['_extras']['blocks'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$find    = array();\n\t\t$replace = array();\n\t\tforeach ( $raw['_extras']['blocks'] as $block_id => $block ) {\n\n\t\t\t$new_id = $this->create_reusable_block( $block_id, $block );\n\t\t\tif ( ! is_wp_error( $new_id ) && is_numeric( $new_id ) ) {\n\t\t\t\t$find[]    = sprintf( '<!-- wp:block {\"ref\":%d}', absint( $block_id ) );\n\t\t\t\t$replace[] = sprintf( '<!-- wp:block {\"ref\":%d}', $new_id );\n\t\t\t}\n\t\t}\n\n\t\tif ( $find && $replace ) {\n\t\t\t$args = array(\n\t\t\t\t'ID'           => $post->get( 'id' ),\n\t\t\t\t'post_content' => str_replace( $find, $replace, $post->get( 'content', true ) ),\n\t\t\t);\n\t\t\treturn wp_update_post( $args ) ? true : false;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Insert a reusable block into the database\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param int   $block_id WP_Post ID of the block being imported. This will be the ID as found on the original site.\n\t * @param array $block    {\n\t *     Array of block data.\n\t *\n\t *     @type string $title   Title of the reusable block.\n\t *     @type string $content Content of the reusable block.\n\t * }\n\t * @return int WP_Post ID on success or `0 on error.\n\t */\n\tprotected function insert_resuable_block( $block_id, $block ) {\n\n\t\t$id = wp_insert_post(\n\t\t\tarray(\n\t\t\t\t'post_content' => $block['content'],\n\t\t\t\t'post_title'   => $block['title'],\n\t\t\t\t'post_type'    => 'wp_block',\n\t\t\t\t'post_status'  => 'publish',\n\t\t\t)\n\t\t);\n\n\t\tif ( $id ) {\n\n\t\t\t$this->reusable_blocks[ $block_id ] = $id;\n\n\t\t\t/**\n\t\t\t * Triggered when a new reusable block is created during an import\n\t\t\t *\n\t\t\t * @since 4.7.0\n\t\t\t *\n\t\t\t * @param int   $id    WP_Post ID of the block.\n\t\t\t * @param array $block Array of block information from the import.\n\t\t\t */\n\t\t\tdo_action( 'llms_generator_new_reusable_block', $id, $block );\n\n\t\t}\n\n\t\treturn $id;\n\t}\n\n\t/**\n\t * Determines if image sideloading is enabled for the generator\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return boolean If `true`, sideloading is enabled, otherwise sideloading is disabled.\n\t */\n\tpublic function is_image_sideloading_enabled() {\n\n\t\t/**\n\t\t * Filter the status of image sideloading for the generator.\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param boolean        $enabled   Whether or not sideloading is enabled.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\treturn apply_filters( 'llms_generator_is_image_sideloading_enabled', true, $this );\n\t}\n\n\t/**\n\t * Determines if reusable block importing is enabled generator\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return boolean If `true`, importing is enabled, otherwise importing is disabled.\n\t */\n\tpublic function is_reusable_block_importing_enabled() {\n\n\t\t/**\n\t\t * Filter the status of reusable block importing for the generator.\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param boolean        $enabled   Whether or not block importing is enabled.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\treturn apply_filters( 'llms_generator_is_reusable_block_importing_enabled', true, $this );\n\t}\n\n\t/**\n\t * Load additional generator classes and other dependencies\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tprotected function load_dependencies() {\n\n\t\t// For featured image creation via `media_sideload_image()`.\n\t\trequire_once ABSPATH . 'wp-admin/includes/media.php';\n\t\trequire_once ABSPATH . 'wp-admin/includes/file.php';\n\t\trequire_once ABSPATH . 'wp-admin/includes/image.php';\n\t}\n\n\t/**\n\t * Saves an image (from URL) to the media library and sets it as the featured image for a given post\n\t *\n\t * @since 3.3.0\n\t * @since 4.7.0 Moved from `LLMS_Generator` and made `protected` instead of `private`.\n\t *               Add a return instead of `void`; Don't import if sideloading is disabled; Use `$this->sideload_image()` sideloading.\n\t *\n\t * @param string $url_or_raw Array of raw data or URL to an image.\n\t * @param int    $post_id    WP Post ID.\n\t * @return null|false|int Returns `null` if sideloading is disabled, WP Post ID of the attachment on success, `false` on error.\n\t */\n\tprotected function set_featured_image( $url_or_raw, $post_id ) {\n\n\t\t// Sideloading is disabled.\n\t\tif ( ! $this->is_image_sideloading_enabled() ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$image_url = ( is_array( $url_or_raw ) && ! empty( $url_or_raw['featured_image'] ) ) ? $url_or_raw['featured_image'] : $url_or_raw;\n\n\t\tif ( $image_url && is_string( $image_url ) ) {\n\n\t\t\t$id = $this->sideload_image( $post_id, $image_url, 'id' );\n\t\t\tif ( ! is_wp_error( $id ) ) {\n\t\t\t\tset_post_thumbnail( $post_id, $id );\n\t\t\t\treturn $id;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Configure the default post status for generated posts at runtime\n\t *\n\t * @since 3.7.3\n\t *\n\t * @param string $status Any valid WP Post Status.\n\t * @return void\n\t */\n\tpublic function set_default_post_status( $status ) {\n\t\t$this->default_post_status = $status;\n\t}\n\n\t/**\n\t * Set all metadata for a given post object\n\t *\n\t * This method will only set metadata for registered LLMS_Post_Model properties.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param LLMS_Post_Model $post An LLMS post object.\n\t * @param array           $raw  Array of raw data.\n\t * @return void\n\t */\n\tprotected function set_metadata( $post, $raw ) {\n\n\t\t// Set all metadata.\n\t\tforeach ( array_keys( $post->get_properties() ) as $key ) {\n\t\t\tif ( isset( $raw[ $key ] ) ) {\n\t\t\t\t$post->set( $key, $raw[ $key ] );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Sideload an image from a url\n\t *\n\t * @since 4.7.0\n\t *\n\t * @link https://developer.wordpress.org/reference/hooks/http_request_host_is_external/ If exporting from a local site and importing into another local site, images *will not* be side loaded as a result of this condition in the WP Core\n\t *\n\t * @param int    $post_id WP_Post ID of the post where the image will be attached.\n\t * @param string $url     The image's URL.\n\t * @return string|int|WP_Error Returns a WP_Error on failure, the image's new URL when `$return` is \"src\", otherwise returns the image's attachment ID.\n\t */\n\tprotected function sideload_image( $post_id, $url, $return = 'src' ) {\n\n\t\t// Check if the image was previously sideloaded.\n\t\t$id = empty( $this->images[ $url ] ) ? false : $this->images[ $url ];\n\n\t\t// Image was not previously sideloaded.\n\t\tif ( ! $id ) {\n\n\t\t\t$id = media_sideload_image( $url, $post_id, null, 'id' );\n\t\t\tif ( is_wp_error( $id ) ) {\n\t\t\t\treturn $id;\n\t\t\t}\n\n\t\t\t// Store the ID for future usage.\n\t\t\t$this->images[ $url ] = $id;\n\n\t\t}\n\n\t\treturn 'src' === $return ? wp_get_attachment_url( $id ) : $id;\n\t}\n\n\t/**\n\t * Sideload images found in a given post\n\t *\n\t * This attempts to sideload the `src` attribute of every <img> element\n\t * found in the `post_content` of the supplied post.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param LLMS_Post_Model $post Post object.\n\t * @param array           $raw  Array of raw data.\n\t * @return null|boolean Returns `true` on success, `false` if there were no images to update, or `null` if sideloading is disabled.\n\t */\n\tprotected function sideload_images( $post, $raw ) {\n\n\t\t// Sideloading is disabled.\n\t\tif ( ! $this->is_image_sideloading_enabled() ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// No images to sideload.\n\t\tif ( empty( $raw['_extras']['images'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t/**\n\t\t * List of hostnames from which sideloading is explicitly disabled\n\t\t *\n\t\t * If the source url of an image is from a host in this list, the image will not be sideloaded\n\t\t * during generation.\n\t\t *\n\t\t * By default the current site is included in the blocklist ensuring that images aren't\n\t\t * sideloaded into the same site.\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param string[] $blocked_hosts Array of hostnames.\n\t\t */\n\t\t$blocked_hosts = apply_filters(\n\t\t\t'llms_generator_sideload_hosts_blocklist',\n\t\t\tarray(\n\t\t\t\twp_parse_url( get_site_url(), PHP_URL_HOST ),\n\t\t\t)\n\t\t);\n\n\t\t$post_id = $post->get( 'id' );\n\t\t$find    = array();\n\t\t$replace = array();\n\t\tforeach ( $raw['_extras']['images'] as $src ) {\n\n\t\t\t// Don't sideload images from blocked hosts.\n\t\t\tif ( in_array( wp_parse_url( $src, PHP_URL_HOST ), $blocked_hosts, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$new_src = $this->sideload_image( $post_id, $src );\n\t\t\tif ( ! is_wp_error( $new_src ) ) {\n\t\t\t\t$find[]    = $src;\n\t\t\t\t$replace[] = $new_src;\n\t\t\t}\n\t\t}\n\n\t\tif ( $find && $replace ) {\n\t\t\t$content = str_replace( $find, $replace, $post->get( 'content', true ) );\n\t\t\treturn $post->set( 'content', $content );\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Accepts a raw object, finds the raw id and stores it\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param array           $raw Array of raw data.\n\t * @param LLMS_Post_Model $obj The LLMS Post Object generated from the raw data.\n\t * @return int|false Raw id when present or `false` if no raw id was found.\n\t */\n\tprotected function store_temp_id( $raw, $obj ) {\n\n\t\tif ( empty( $raw['id'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Ensure the object post type array exists.\n\t\tif ( ! isset( $this->tempids[ $obj->get( 'type' ) ] ) ) {\n\t\t\t$this->tempids[ $obj->get( 'type' ) ] = array();\n\t\t}\n\n\t\t// Store the id on the meta table.\n\t\t$obj->set( 'generated_from_id', $raw['id'] );\n\n\t\t// Store it in the object for prereq handling later.\n\t\t$this->tempids[ $obj->get( 'type' ) ][ $raw['id'] ] = $obj->get( 'id' );\n\n\t\treturn $raw['id'];\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-meta-box-user-engagement-sync.php",
    "content": "<?php\n/**\n * LLMS_Abstract_Meta_Box_User_Engagement_Sync class\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Base meta box class for syncing between awarded engagements (certificates or achievements) and templates.\n *\n * @since 6.0.0\n */\nabstract class LLMS_Abstract_Meta_Box_User_Engagement_Sync extends LLMS_Admin_Metabox {\n\n\tuse LLMS_Trait_User_Engagement_Type;\n\n\t/**\n\t * A text type for a sync alert about many awarded engagements being synced to the current engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_ALERT_MANY_AWARDED_ENGAGEMENTS = 0;\n\n\t/**\n\t * A text type for a sync alert about one awarded engagement being synced to the current engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_ALERT_ONE_AWARDED_ENGAGEMENT = 1;\n\n\t/**\n\t * A text type for a sync alert about this awarded engagement being synced to its engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_ALERT_THIS_AWARDED_ENGAGEMENT = 2;\n\n\t/**\n\t * A text type for a sync description about many awarded engagements being synced to the current engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_DESCRIPTION_MANY_AWARDED_ENGAGEMENTS = 3;\n\n\t/**\n\t * A text type for a sync description about one awarded engagement being synced to the current engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_DESCRIPTION_ONE_AWARDED_ENGAGEMENT = 4;\n\n\t/**\n\t * A text type for a sync description about this awarded engagement being synced to its engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_DESCRIPTION_THIS_AWARDED_ENGAGEMENT = 5;\n\n\t/**\n\t * A text type for the content of a \"sync awarded engagements\" meta box when there are no awarded engagements to sync with.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_ENGAGEMENT_TEMPLATE_NO_AWARDED_ENGAGEMENTS = 6;\n\n\t/**\n\t * A text type for the title of a \"sync awarded engagement\" meta box.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_TITLE_AWARDED_ENGAGEMENT = 7;\n\n\t/**\n\t * A text type for the title of a \"sync awarded engagements\" meta box.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_TITLE_AWARDED_ENGAGEMENTS = 8;\n\n\t/**\n\t * The context to register the meta box with.\n\t *\n\t * Accepts anything that can be passed to WP core add_meta_box() function: 'normal', 'side', 'advanced'.\n\t *\n\t * @var string\n\t */\n\tpublic $context = 'side';\n\n\t/**\n\t * If true, we are syncing all awarded engagements with their template,\n\t * else we are syncing a single awarded engagement with its template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var bool\n\t */\n\tprotected $is_current_post_a_template;\n\n\t/**\n\t * The post type of an awarded engagement, e.g. 'llms_my_achievement' or 'llms_my_certificate'.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $post_type_awarded;\n\n\t/**\n\t * The post type of an engagement template, e.g. 'llms_achievement' or 'llms_certificate'.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $post_type_template;\n\n\t/**\n\t * Configure the meta box settings.\n\t *\n\t * @since 6.0.0 Refactored from LLMS_Meta_Box_Award_Certificate_Sync::configure() and\n\t *              LLMS_Meta_Box_Certificate_Template_Sync::configure().\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t// Try to load the post being edited.\n\t\tif ( is_null( $this->post ) ) {\n\t\t\t$this->post = get_post( llms_filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT ) );\n\t\t}\n\n\t\t// There is no need to configure this meta box if we're not editing an engagement template or awarded engagement.\n\t\tif (\n\t\t\tis_null( $this->post ) ||\n\t\t\t! in_array( $this->post->post_type, array( $this->post_type_awarded, $this->post_type_template ), true )\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->id = \"{$this->engagement_type}_sync\";\n\n\t\tif ( $this->post->post_type === $this->post_type_template ) {\n\t\t\t$this->is_current_post_a_template = true;\n\t\t\t$this->title                      = $this->get_text( self::TEXT_SYNC_TITLE_AWARDED_ENGAGEMENTS );\n\t\t} else {\n\t\t\t$this->is_current_post_a_template = false;\n\t\t\t$this->title                      = $this->get_text( self::TEXT_SYNC_TITLE_AWARDED_ENGAGEMENT );\n\t\t}\n\t}\n\n\t/**\n\t * Not used because our meta box doesn't use the standard fields API.\n\t *\n\t * @since 6.0.0 Refactored from LLMS_Meta_Box_Award_Certificate_Sync::get_fields() and\n\t *              LLMS_Meta_Box_Certificate_Template_Sync::get_fields().\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\treturn array();\n\t}\n\n\t/**\n\t * Returns the sync alert and sync description texts for the sync action button, or an empty array if the sync\n\t * button should not be displayed.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_sync_action_texts() {\n\n\t\tif ( $this->is_current_post_a_template ) {\n\t\t\t$awarded_number = $this->count_awarded_engagements( $this->post->ID );\n\n\t\t\tif ( ! $awarded_number ) {\n\t\t\t\treturn array();\n\t\t\t}\n\n\t\t\t$variables = compact( 'awarded_number' );\n\t\t\tif ( $awarded_number > 1 ) {\n\t\t\t\t$sync_alert       = $this->get_text( self::TEXT_SYNC_ALERT_MANY_AWARDED_ENGAGEMENTS, $variables );\n\t\t\t\t$sync_description = $this->get_text( self::TEXT_SYNC_DESCRIPTION_MANY_AWARDED_ENGAGEMENTS, $variables );\n\t\t\t} else {\n\t\t\t\t$sync_alert       = $this->get_text( self::TEXT_SYNC_ALERT_ONE_AWARDED_ENGAGEMENT, $variables );\n\t\t\t\t$sync_description = $this->get_text( self::TEXT_SYNC_DESCRIPTION_ONE_AWARDED_ENGAGEMENT, $variables );\n\t\t\t}\n\t\t} else {\n\t\t\t$awarded_model = $this->get_user_engagement( $this->post->ID, true );\n\t\t\t$template_id   = $awarded_model ? $awarded_model->get( 'parent' ) : false;\n\n\t\t\tif ( empty( $template_id ) || ! $this->get_user_engagement( $template_id, false ) ) {\n\t\t\t\treturn array();\n\t\t\t}\n\n\t\t\t$sync_alert       = $this->get_text( self::TEXT_SYNC_ALERT_THIS_AWARDED_ENGAGEMENT );\n\t\t\t$sync_description = $this->get_text(\n\t\t\t\tself::TEXT_SYNC_DESCRIPTION_THIS_AWARDED_ENGAGEMENT,\n\t\t\t\tarray( 'template_id' => $template_id )\n\t\t\t);\n\t\t}\n\n\t\treturn compact( 'sync_alert', 'sync_description' );\n\t}\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Meta_Box_User_Engagement_Sync::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\treturn __( 'Invalid text type.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Function to field WP::output() method call.\n\t *\n\t * @see LLMS_Admin_Metabox::register()\n\t * @see do_meta_boxes()\n\t *\n\t * @since 6.0.0 Refactored from LLMS_Meta_Box_Award_Certificate_Sync::output() and\n\t *              LLMS_Meta_Box_Certificate_Template_Sync::output().\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t$sync_action = $this->sync_action();\n\n\t\t// Output the HTML.\n\t\techo '<div class=\"llms-mb-container\">';\n\t\tdo_action( 'llms_metabox_before_content', $this->id );\n\t\techo wp_kses( $sync_action, LLMS_ALLOWED_HTML_FORM_FIELDS );\n\t\tdo_action( 'llms_metabox_after_content', $this->id );\n\t\techo '</div>';\n\t}\n\n\t/**\n\t * Returns the sync action description and button HTML for a meta box on an engagement template or an awarded engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string\n\t */\n\tprivate function sync_action() {\n\n\t\t$texts = $this->get_sync_action_texts();\n\t\tif ( empty( $texts ) ) {\n\t\t\treturn $this->get_text( self::TEXT_SYNC_ENGAGEMENT_TEMPLATE_NO_AWARDED_ENGAGEMENTS );\n\t\t}\n\n\t\t$base_url = remove_query_arg( 'action' ); // Current URL without 'action' arg.\n\t\t$sync_url = add_query_arg(\n\t\t\t'action',\n\t\t\t\"sync_awarded_{$this->engagement_type}\" . ( $this->is_current_post_a_template ? 's' : '' ),\n\t\t\twp_nonce_url(\n\t\t\t\t$base_url,\n\t\t\t\t\"llms-{$this->engagement_type}-sync-actions\",\n\t\t\t\t\"_llms_{$this->engagement_type}_sync_actions_nonce\"\n\t\t\t)\n\t\t);\n\n\t\t$sync_alert   = str_replace( \"'\", \"\\'\", $texts['sync_alert'] );\n\t\t$button_label = __( 'Sync', 'lifterlms' );\n\n\t\tob_start();\n\t\t?>\n<p><?php echo wp_kses_post( $texts['sync_description'] ); ?></p>\n<p style=\"text-align: right; margin: 1em 0;\">\n<a href=\"<?php echo esc_url( $sync_url ); ?>\" class=\"llms-button-primary sync-action full small\" onclick=\"return confirm('<?php echo esc_js( $sync_alert ); ?>')\" style=\"box-sizing:border-box;\"><?php echo wp_kses_post( $button_label ); ?></a>\n</p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-posts-query.php",
    "content": "<?php\n/**\n * LLMS_Abstract_Posts_Query class file.\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Abstract WP_Posts query class.\n *\n * This class is meant to perform custom queries that ultimately\n * get passed into a `WP_Query`, ideally for a specific post type\n * or list of post types.\n *\n * @since 6.0.0\n *\n * Valid query arguments\n *\n * {@see LLMS_Abstract_Query} for inherited query arguments.\n *\n * @param string          $fields     WP_Post fields to return for each result. Accepts \"all\", \"ids\", or \"id=>parent\". Default: \"all\".\n * @param string[]|string $status     Limit results by WP_Post `$post_status`. Default: \"publish\".\n * @param string[]        $post_types Limit results to the specified post type(s).\n */\nabstract class LLMS_Abstract_Posts_Query extends LLMS_Abstract_Query {\n\n\t/**\n\t * Defines fields that can be sorted on via ORDER BY.\n\t *\n\t * @var string[]\n\t */\n\tprotected $allowed_sort_fields = array(\n\t\t'ID',\n\t\t'author',\n\t\t'title',\n\t\t'name',\n\t\t'type',\n\t\t'date',\n\t\t'modified',\n\t\t'parent',\n\t\t'menu_order',\n\t);\n\n\t/**\n\t * Specify the post types allowed to be queried by this class\n\t *\n\t * This array should be a list of one or more post type names.\n\t *\n\t * @var string[]\n\t */\n\tprotected $allowed_post_types = array();\n\n\t/**\n\t * The WP_Query instance.\n\t *\n\t * @var null\n\t */\n\tprotected $wp_query = null;\n\n\t/**\n\t * Set result counts and pagination properties.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function count_results() {\n\n\t\t$this->number_results = $this->wp_query->post_count;\n\t\t$this->found_results  = $this->found_results();\n\t\t$this->max_pages      = (int) $this->wp_query->max_num_pages;\n\n\t}\n\n\t/**\n\t * Retrieve query argument default values.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function default_arguments() {\n\n\t\treturn wp_parse_args(\n\t\t\tarray(\n\t\t\t\t'fields'     => 'all',\n\t\t\t\t'status'     => 'publish',\n\t\t\t\t'post_types' => $this->allowed_post_types,\n\t\t\t\t'sort'       => array(\n\t\t\t\t\t'date' => 'DESC',\n\t\t\t\t\t'ID'   => 'DESC',\n\t\t\t\t),\n\t\t\t),\n\t\t\tparent::default_arguments()\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve total found results for the query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int\n\t */\n\tprotected function found_results() {\n\t\treturn $this->wp_query->found_posts;\n\t}\n\n\t/**\n\t * Map input arguments to WP_Query arguments.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_arg_map() {\n\n\t\treturn array(\n\t\t\t'page'       => 'paged',\n\t\t\t'per_page'   => 'posts_per_page',\n\t\t\t'post_types' => 'post_type',\n\t\t\t'search'     => 's',\n\t\t\t'sort'       => 'orderby',\n\t\t\t'status'     => 'post_status',\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the WP_Query object for the query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return WP_Query\n\t */\n\tpublic function get_wp_query() {\n\t\treturn $this->wp_query;\n\t}\n\n\t/**\n\t * Performs the query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return WP_Post[]|int[] Array of results corresponding to the value specified in the `$fields` query argument.\n\t */\n\tprotected function perform_query() {\n\n\t\t$this->wp_query = new WP_Query( $this->query );\n\t\treturn $this->wp_query->posts;\n\n\t}\n\n\t/**\n\t * Prepare the query.\n\t *\n\t * Should return the query which will be used by `query()`.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return mixed\n\t */\n\tprotected function prepare_query() {\n\n\t\t$map = $this->get_arg_map();\n\n\t\t$args = array();\n\t\tforeach ( $this->query_vars as $var => $val ) {\n\n\t\t\t$var          = array_key_exists( $var, $map ) ? $map[ $var ] : $var;\n\t\t\t$args[ $var ] = $val;\n\t\t}\n\n\t\treturn $args;\n\n\t}\n\n\t/**\n\t * Sets a query variable.\n\t *\n\t * Overrides parent method to ensure only allowed post types can be queried.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $key Variable key.\n\t * @param mixed  $val Variable value.\n\t * @return void\n\t */\n\tpublic function set( $key, $val ) {\n\n\t\tif ( 'post_types' === $key ) {\n\t\t\t$val = $this->sanitize_post_types( $val );\n\t\t}\n\n\t\tparent::set( $key, $val );\n\n\t}\n\n\t/**\n\t * Sanitize the `post_types` query argument.\n\t *\n\t * Any post types not explicitly included in the `$allowed_post_types` list are\n\t * removed from the input.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string[] $val Array of post types to query.\n\t * @return string[] Cleaned array.\n\t */\n\tprotected function sanitize_post_types( $val ) {\n\n\t\tif ( ! is_array( $val ) ) {\n\t\t\treturn array();\n\t\t}\n\n\t\tforeach ( $val as $index => $post_type ) {\n\t\t\tif ( ! in_array( $post_type, $this->allowed_post_types, true ) ) {\n\t\t\t\tunset( $val[ $index ] );\n\t\t\t}\n\t\t}\n\n\t\treturn array_values( $val );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-processor-user-engagement-sync.php",
    "content": "<?php\n/**\n * LLMS_Abstract_Processor_User_Engagement_Sync class\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Base processor class for syncing awarded engagements (certificates or achievements) to their engagement template.\n *\n * @since 6.0.0\n */\nabstract class LLMS_Abstract_Processor_User_Engagement_Sync extends LLMS_Abstract_Processor {\n\n\tuse LLMS_Trait_User_Engagement_Type;\n\n\t/**\n\t * A text type for an admin notice that the sync of awarded engagements to an engagement template is already scheduled.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_NOTICE_ALREADY_SCHEDULED = 0;\n\n\t/**\n\t * A text type for an admin notice that the sync of awarded engagements to an engagement template is complete.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_NOTICE_AWARDED_ENGAGEMENTS_COMPLETE = 1;\n\n\t/**\n\t * A text type for an admin notice that there are no awarded engagements to sync the template with.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_NOTICE_NO_AWARDED_ENGAGEMENTS = 2;\n\n\t/**\n\t * A text type for an admin notice that the sync of awarded engagements to an engagement template is scheduled.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprotected const TEXT_SYNC_NOTICE_SCHEDULED = 3;\n\n\t/**\n\t * Clear notices.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $engagement_template_id WP Post ID of the user engagement template.\n\t * @return void\n\t */\n\tprivate function clear_notices( $engagement_template_id ) {\n\t\t$notices = array(\n\t\t\t'awarded-%1$ss-sync-%2$d-scheduled',\n\t\t\t'awarded-%1$ss-sync-%2$d-already-scheduled',\n\t\t\t'awarded-%1$ss-sync-%2$d-done',\n\t\t);\n\n\t\tforeach ( $notices as $notice ) {\n\t\t\tLLMS_Admin_Notices::delete_notice(\n\t\t\t\tsprintf( $notice, $this->engagement_type, $engagement_template_id )\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Action triggered to sync all the awarded engagements that need to be updated.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $engagement_template_id WP Post ID of the engagement template.\n\t * @return void\n\t */\n\tpublic function dispatch_sync( $engagement_template_id ) {\n\n\t\t$this->log(\n\t\t\tsprintf(\n\t\t\t\t'awarded %1$ss bulk sync dispatched for the %1$s template %2$s (#%3$d)',\n\t\t\t\t$this->engagement_type,\n\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t$engagement_template_id\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Filter the query arguments used when retrieving the awarded engagements to sync.\n\t\t *\n\t\t * The dynamic portion of the hook name,\n\t\t * {@see LLMS_Abstract_Processor_User_Engagement_Sync::$engagement_type `$this->engagement_type`},\n\t\t * refers to the engagement type, either 'achievement' or 'certificate'.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param array $args Query arguments passed to LLMS_Awards_Query.\n\t\t */\n\t\t$args = apply_filters(\n\t\t\t\"llms_processor_sync_awarded_{$this->engagement_type}s_query_args\",\n\t\t\tarray(\n\t\t\t\t'templates' => $engagement_template_id,\n\t\t\t\t'per_page'  => 20,\n\t\t\t\t'page'      => 1,\n\t\t\t\t'status'    => array(\n\t\t\t\t\t'publish',\n\t\t\t\t\t'future',\n\t\t\t\t),\n\t\t\t\t'type'      => $this->engagement_type,\n\t\t\t)\n\t\t);\n\n\t\t$query = new LLMS_Awards_Query( $args );\n\n\t\tif ( ! $query->get_found_results() ) {\n\t\t\treturn;\n\t\t}\n\n\t\twhile ( $args['page'] <= $query->get_max_pages() ) {\n\t\t\t$this->push_to_queue(\n\t\t\t\tarray(\n\t\t\t\t\t'query_args' => $args,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$args['page'] ++;\n\t\t}\n\n\t\t// Save queue and dispatch the process.\n\t\t$this->save()->dispatch();\n\t}\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Processor_User_Engagement_Sync::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\treturn __( 'Invalid text type.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Initializer.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function init() {\n\n\t\t// For the cron.\n\t\tadd_action( $this->schedule_hook, array( $this, 'dispatch_sync' ), 10, 1 );\n\n\t\t// For LifterLMS actions which trigger bulk enrollment.\n\t\t$this->actions = array(\n\t\t\t\"llms_do_awarded_{$this->engagement_type}s_bulk_sync\" => array(\n\t\t\t\t'arguments' => 1,\n\t\t\t\t/** @see LLMS_Abstract_Processor_User_Engagement_Sync::schedule_sync() */\n\t\t\t\t'callback'  => 'schedule_sync',\n\t\t\t\t'priority'  => 10,\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Perform actions when the process is completed.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $args Array of processing data.\n\t * @return void\n\t */\n\tprivate function process_completed( $args ) {\n\n\t\t$this->log(\n\t\t\tsprintf(\n\t\t\t\t'awarded %1$s bulk sync completed for the %1$s template %2$s (#%3$d)',\n\t\t\t\t$this->engagement_type,\n\t\t\t\tget_the_title( $args['query_args']['templates'] ),\n\t\t\t\t$args['query_args']['templates']\n\t\t\t)\n\t\t);\n\n\t\t$this->clear_notices( $args['query_args']['templates'] );\n\n\t\tLLMS_Admin_Notices::add_notice(\n\t\t\tsprintf( 'awarded-%1$ss-sync-%2$d-done', $this->engagement_type, $args['query_args']['templates'] ),\n\t\t\t$this->get_text(\n\t\t\t\tself::TEXT_SYNC_NOTICE_AWARDED_ENGAGEMENTS_COMPLETE,\n\t\t\t\tarray( 'engagement_template_id' => $args['query_args']['templates'] )\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'dismissible'      => true,\n\t\t\t\t'dismiss_for_days' => 0,\n\t\t\t\t'type'             => 'success',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Schedule sync.\n\t *\n\t * This will schedule an event that will setup the queue of items for the background process.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $engagement_template_id WP Post ID of the user engagement template.\n\t * @return void\n\t */\n\tpublic function schedule_sync( $engagement_template_id ) {\n\n\t\t$this->log(\n\t\t\tsprintf(\n\t\t\t\t'awarded %1$ss bulk sync for the %1$s template %2$s (#%3$d)',\n\t\t\t\t$this->engagement_type,\n\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t$engagement_template_id\n\t\t\t)\n\t\t);\n\n\t\t$this->clear_notices( $engagement_template_id );\n\n\t\t$args          = array( $engagement_template_id );\n\t\t$awarded_count = $this->count_awarded_engagements( $engagement_template_id );\n\n\t\tif ( 0 === $awarded_count ) {\n\n\t\t\t$log_message    = 'no awarded %1$ss to bulk sync with the %1$s template %2$s (#%3$d)';\n\t\t\t$notice_message = $this->get_text( self::TEXT_SYNC_NOTICE_NO_AWARDED_ENGAGEMENTS, compact( 'engagement_template_id' ) );\n\t\t\t$notice_id      = 'awarded-%1$ss-sync-%2$d-no-awarded';\n\t\t\t$notice_type    = 'error';\n\n\t\t} elseif ( wp_next_scheduled( $this->schedule_hook, $args ) ) {\n\n\t\t\t$log_message    = 'awarded %1$ss bulk sync already scheduled for the %1$s template %2$s (#%3$d)';\n\t\t\t$notice_message = $this->get_text( self::TEXT_SYNC_NOTICE_ALREADY_SCHEDULED, compact( 'engagement_template_id' ) );\n\t\t\t$notice_id      = 'awarded-%1$ss-sync-%2$d-already-scheduled';\n\t\t\t$notice_type    = 'warning';\n\t\t} else {\n\n\t\t\twp_schedule_single_event( time(), $this->schedule_hook, $args );\n\t\t\t$log_message    = 'awarded %1$ss bulk sync scheduled for the %1$s template %2$s (#%3$d)';\n\t\t\t$notice_message = $this->get_text( self::TEXT_SYNC_NOTICE_SCHEDULED, compact( 'engagement_template_id' ) );\n\t\t\t$notice_id      = 'awarded-%1$ss-sync-%2$d-scheduled';\n\t\t\t$notice_type    = 'info';\n\t\t}\n\n\t\t$this->log(\n\t\t\tsprintf(\n\t\t\t\t$log_message,\n\t\t\t\t$this->engagement_type,\n\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t$engagement_template_id\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Notices::add_notice(\n\t\t\tsprintf( $notice_id, $this->engagement_type, $engagement_template_id ),\n\t\t\t$notice_message,\n\t\t\tarray(\n\t\t\t\t'dismissible'      => true,\n\t\t\t\t'dismiss_for_days' => 0,\n\t\t\t\t'type'             => $notice_type,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Sync awarded engagements.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_Abstract_User_Engagement[] $engagements Array of awarded engagements to sync.\n\t * @param int                             $template_id Engagement template ID.\n\t * @return void\n\t */\n\tprivate function sync_awarded_engagements( $engagements, $template_id ) {\n\n\t\t$success_log_message = 'awarded %1$s %2$s (#%3$d) successfully synced with template %4$s (#%5$d)';\n\t\t$error_log_message   = 'an error occurred while trying to sync awarded %1$s %2$s (#%3$d) from template %4$s (#%5$d)';\n\n\t\tforeach ( $engagements as $awarded_engagement ) {\n\t\t\t$this->log(\n\t\t\t\tsprintf(\n\t\t\t\t\t$awarded_engagement->sync() ? $success_log_message : $error_log_message,\n\t\t\t\t\t$this->engagement_type,\n\t\t\t\t\t$awarded_engagement->get( 'title', true ),\n\t\t\t\t\t$awarded_engagement->get( 'id' ),\n\t\t\t\t\tget_the_title( $template_id ),\n\t\t\t\t\t$template_id\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Execute sync for each item in the queue until all awarded engagements are synced.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $args Array of processing data.\n\t * @return boolean `true` to keep the item in the queue and process again.\n\t *                 `false` to remove the item from the queue.\n\t */\n\tpublic function task( $args ) {\n\n\t\t$this->log(\n\t\t\tsprintf(\n\t\t\t\t'awarded %1$ss bulk sync task started for the %1$s template %2$s (#%3$d) - chunk %4$d',\n\t\t\t\t$this->engagement_type,\n\t\t\t\tget_the_title( $args['query_args']['templates'] ),\n\t\t\t\t$args['query_args']['templates'],\n\t\t\t\t$args['query_args']['page']\n\t\t\t)\n\t\t);\n\n\t\t// Ensure the item has all the data we need to process it.\n\t\tif ( empty( $args['query_args']['templates'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$query = new LLMS_Awards_Query( $args['query_args'] );\n\n\t\tif ( $query->has_results() ) {\n\t\t\t$this->sync_awarded_engagements( $query->get_awards(), $args['query_args']['templates'] );\n\t\t}\n\n\t\tif ( $query->is_last_page() ) {\n\t\t\t$this->process_completed( $args );\n\t\t}\n\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-query.php",
    "content": "<?php\n/**\n * LLMS_Abstract_Query class file.\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Database Query abstract class.\n *\n * @since 6.0.0\n *\n * Query Arguments\n *\n * @param int $page             Results page number. Default: `1`.\n * @param int $per_page         Number of results to retrieve per page. Default: `10`.\n * @param int $search           Text search term or query. Default: `''`.\n * @param int $sort             Result order and orderby. Default: `array()`.\n * @param int $suppress_filters Whether or not to allow filter hooks to run. Default: `false`.\n * @param int $no_found_rows    Whether or not to disable the total number of found results for the query. Default: `false`.\n */\nabstract class LLMS_Abstract_Query {\n\n\t/**\n\t * Identify the extending query.\n\t *\n\t * @var string\n\t */\n\tprotected $id = '';\n\n\t/**\n\t * Defines fields that can be sorted on via ORDER BY.\n\t *\n\t * If this is not defined by extending classes, the sort data\n\t * will not be validated.\n\t *\n\t * @var string[]|null\n\t */\n\tprotected $allowed_sort_fields = array();\n\n\t/**\n\t * Combined arguments prior to sanitization.\n\t *\n\t * This is the final resulting arguments used to generate the query,\n\t * the result of the default arguments merged into the submitted arguments.\n\t *\n\t * @var array\n\t */\n\tprotected $arguments = array();\n\n\t/**\n\t * Default arguments before merging with original.\n\t *\n\t * @var array\n\t */\n\tprotected $arguments_default = array();\n\n\t/**\n\t * Original arguments before merging with defaults.\n\t *\n\t * @var array\n\t */\n\tprotected $arguments_original = array();\n\n\t/**\n\t * Total number of results matching query parameters.\n\t *\n\t * @var integer\n\t */\n\tprotected $found_results = 0;\n\n\t/**\n\t * Maximum number of pages of results based off per_page & found_results.\n\t *\n\t * @var integer\n\t */\n\tprotected $max_pages = 0;\n\n\t/**\n\t * Number of results on the current page.\n\t *\n\t * @var integer\n\t */\n\tprotected $number_results = 0;\n\n\t/**\n\t * The final query used to retrieve results.\n\t *\n\t * For a raw database query, this is the SQL passed to `$wpdb->get_results()`. For a WP_Posts query\n\t * this is the array of query arguments passed into the `WP_Query`. Other extending queries\n\t * may used this as they see fit.\n\t *\n\t * @var mixed\n\t */\n\tprotected $query = array();\n\n\t/**\n\t * The parsed and sanitized arguments ultimately used by the query.\n\t *\n\t * @var array\n\t */\n\tprotected $query_vars = array();\n\n\t/**\n\t * Array of results retrieved by the query.\n\t *\n\t * @var array\n\t */\n\tprotected $results = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $args Query arguments. When not provided the default arguments will be used.\n\t * @return void\n\t */\n\tpublic function __construct( $args = array() ) {\n\n\t\t$this->arguments_original = $args;\n\t\t$this->arguments_default  = $this->get_default_args();\n\n\t\t$this->setup_args();\n\n\t\t$this->query();\n\t}\n\n\t/**\n\t * Set result counts and pagination properties.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function count_results() {\n\n\t\t$this->number_results = count( (array) $this->results );\n\n\t\t// If we have results and found rounds isn't disabled.\n\t\tif ( $this->number_results && ! $this->get( 'no_found_rows' ) ) {\n\t\t\t$this->found_results = $this->found_results();\n\t\t\t$this->max_pages     = absint( ceil( $this->found_results / $this->get( 'per_page' ) ) );\n\t\t}\n\t}\n\n\t/**\n\t * Determines the default arguments for the query.\n\t *\n\t * Extending classes can override or extend this method to customize the default query\n\t * arguments for the query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function default_arguments() {\n\n\t\treturn array(\n\t\t\t'page'             => 1,\n\t\t\t'per_page'         => 10,\n\t\t\t'search'           => '',\n\t\t\t'sort'             => array(),\n\t\t\t'suppress_filters' => false,\n\t\t\t'no_found_rows'    => false,\n\t\t\t'count_only'       => false,\n\t\t);\n\t}\n\n\t/**\n\t * Determines the total number of found results for the given query and returns it.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int\n\t */\n\tabstract protected function found_results();\n\n\t/**\n\t * Retrieve a query variable with an optional fallback value when the value is not set.\n\t *\n\t * @since 3.8.0\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` and updated to use the null coalesce operator.\n\t *\n\t * @param string $key     Variable key.\n\t * @param mixed  $default Default value.\n\t * @return mixed\n\t */\n\tpublic function get( $key, $default = '' ) {\n\t\treturn $this->query_vars[ $key ] ?? $default;\n\t}\n\n\t/**\n\t * Get the final combined arguments used to generate the query.\n\t *\n\t * Retrieves the value of the protected `$arguments` variable.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_arguments() {\n\t\treturn $this->arguments;\n\t}\n\n\t/**\n\t * Get the default arguments used for the query.\n\t *\n\t * Retrieves the value of the protected `$arguments_default` variable.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_arguments_default() {\n\t\treturn $this->arguments_default;\n\t}\n\n\t/**\n\t * Get the original, uncleaned, arguments submitted to the query.\n\t *\n\t * Retrieves the value of the protected `$arguments_original` variable.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_arguments_original() {\n\t\treturn $this->arguments_original;\n\t}\n\n\t/**\n\t * Get the query.\n\t *\n\t * Retrieves the value of the protected `$query` variable.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return mixed\n\t */\n\tpublic function get_query() {\n\t\treturn $this->query;\n\t}\n\n\t/**\n\t * Retrieve a list of fields that are allowed to be used for result sorting.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string[]\n\t */\n\tprotected function get_allowed_sort_fields() {\n\n\t\t$allowed_fields = $this->allowed_sort_fields;\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $allowed_fields;\n\t\t}\n\n\t\t/**\n\t\t * Filters the allowed sort fields.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->id`, refers to ID of the extending\n\t\t * query class.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param array $allowed_fields Default arguments.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->id}_query_allowed_sort_fields\", $allowed_fields );\n\t}\n\n\t/**\n\t * Retrieve default arguments for the query.\n\t *\n\t * @since 3.8.0\n\t * @since 4.5.1 Added new default arg `no_found_rows` set to false.\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_args() {\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $this->default_arguments();\n\t\t}\n\n\t\t/**\n\t\t * Filters the query default args.\n\t\t * The dynamic part of the filter `$this->id` identifies the extending query.\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param array $args Array of default arguments to set up the query with.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->id}_query_get_default_args\", $this->default_arguments() );\n\t}\n\n\t/**\n\t * Get the total results found for the query.\n\t *\n\t * If the query was instantiated with `$no_found_rows=true` this will always\n\t * return `0`.\n\t *\n\t * Retrieves the value of the protected property `$found_results`.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_found_results() {\n\t\treturn $this->found_results;\n\t}\n\n\t/**\n\t * Get the total number of pages available for the given query.\n\t *\n\t * If the query was instantiated with `$no_found_rows=true` this will always\n\t * return `0`.\n\t *\n\t * Retrieves the value of the protected property `$max_pages`.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_max_pages() {\n\t\treturn $this->max_pages;\n\t}\n\n\t/**\n\t * Get the number of results on the current page.\n\t *\n\t * Retrieves the value of the protected property `$number_results`.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_number_results() {\n\t\treturn $this->number_results;\n\t}\n\n\t/**\n\t * Retrieve an array of results for the given query.\n\t *\n\t * @since 3.8.0\n\t * @since 4.5.1 Drop use of `this->get_filter('get_results')` in favor of `\"llms_{$this->id}_query_get_results\"`.\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @return array\n\t */\n\tpublic function get_results() {\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $this->results;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query results.\n\t\t *\n\t\t * The dynamic part of the filter `$this->id` identifies the extending query.\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param array $results Array of results retrieved by the query.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->id}_query_get_results\", $this->results );\n\t}\n\n\t/**\n\t * Determine if the query has at least one result.\n\t *\n\t * @since 3.16.0\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @return bool\n\t */\n\tpublic function has_results() {\n\t\treturn $this->number_results > 0;\n\t}\n\n\t/**\n\t * Determine if we're on the first page of results.\n\t *\n\t * @since 3.8.0\n\t * @since 3.14.0 Unknown.\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_first_page() {\n\t\treturn 1 === absint( $this->get( 'page' ) );\n\t}\n\n\t/**\n\t * Determine if we're on the last page of results.\n\t *\n\t * @since 3.8.0\n\t * @since 3.30.3 Return true if there are no results.\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_last_page() {\n\t\treturn ! $this->has_results() || ( absint( $this->get( 'page' ) ) === $this->max_pages );\n\t}\n\n\t/**\n\t * Parse arguments needed for the query.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tabstract protected function parse_args();\n\n\t/**\n\t * Perform the query and return the results.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tabstract protected function perform_query();\n\n\t/**\n\t * Prepare the query.\n\t *\n\t * Should return the query which will be used by `query()`.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return mixed\n\t */\n\tabstract protected function prepare_query();\n\n\t/**\n\t * Execute a query.\n\t *\n\t * @since 3.8.0\n\t * @since 4.5.1 Drop use of `$this->get_filter('prepare_query')` in favor of `\"llms_{$this->id}_query_prepare_query\"`.\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @return void\n\t */\n\tpublic function query() {\n\n\t\t$this->query = $this->prepare_query();\n\t\tif ( ! $this->get( 'suppress_filters' ) ) {\n\t\t\t/**\n\t\t\t * Filters the query SQL.\n\t\t\t *\n\t\t\t * The dynamic part of the filter `$this->id` identifies the extending query.\n\t\t\t *\n\t\t\t * @since 3.8.0\n\t\t\t *\n\t\t\t * @param string              $sql      The SQL query.\n\t\t\t * @param LLMS_Database_Query $db_query The LLMS_Database_Query instance.\n\t\t\t */\n\t\t\t$this->query = apply_filters( \"llms_{$this->id}_query_prepare_query\", $this->query, $this );\n\t\t}\n\n\t\t$this->results = $this->perform_query();\n\n\t\t$this->count_results();\n\t}\n\n\t/**\n\t * Sanitize input to ensure an array of absolute integers.\n\t *\n\t * @since 3.15.0\n\t * @since 3.24.0 Unknown.\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @param string|int|array $ids String/Int or array of strings/ints.\n\t * @return array\n\t */\n\tprotected function sanitize_id_array( $ids = array() ) {\n\n\t\tif ( empty( $ids ) ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t// Allow numeric strings & ints to be passed instead of an array.\n\t\t$ids = ! is_array( $ids ) ? array( $ids ) : $ids;\n\n\t\t// Force positive ints.\n\t\t$ids = array_map( 'absint', $ids );\n\n\t\t// Remove empty values.\n\t\treturn array_values( array_filter( $ids ) );\n\t}\n\n\t/**\n\t * Removes any invalid sort fields before preparing a query.\n\t *\n\t * @since 3.34.0\n\t * @since 6.0.0 Moved from `LLMS_Database_Query`.\n\t *              Use `get_allowed_sort_fields()`.\n\t *\n\t * @return void\n\t */\n\tprotected function sanitize_sort( $sort ) {\n\n\t\t$allowed_fields = $this->get_allowed_sort_fields();\n\n\t\tif ( empty( $allowed_fields ) ) {\n\t\t\treturn $sort;\n\t\t}\n\n\t\tforeach ( (array) $sort as $orderby => $order ) {\n\t\t\tif ( ! in_array( $orderby, $allowed_fields, true ) || ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {\n\t\t\t\tunset( $sort[ $orderby ] );\n\t\t\t}\n\t\t}\n\n\t\treturn $sort;\n\t}\n\n\t/**\n\t * Sets a query variable.\n\t *\n\t * @since 3.8.0\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @param string $key Variable key.\n\t * @param mixed  $val Variable value.\n\t * @return void\n\t */\n\tpublic function set( $key, $val ) {\n\t\t$this->query_vars[ $key ] = $val;\n\t}\n\n\t/**\n\t * Setup arguments prior to a query.\n\t *\n\t * @since 3.8.0\n\t * @since 3.34.0 Sanitizes sort parameters.\n\t * @since 4.5.1 Added filter `\"llms_{$this->id}_query_parse_args\"`.\n\t * @since 6.0.0 Moved from `LLMS_Database_Query` abstract.\n\t *\n\t * @return void\n\t */\n\tprotected function setup_args() {\n\n\t\t$this->arguments = wp_parse_args( $this->arguments_original, $this->arguments_default );\n\n\t\t$this->parse_args();\n\n\t\tif ( ! $this->get( 'suppress_filters' ) ) {\n\t\t\t/**\n\t\t\t * Filters the parsed query arguments.\n\t\t\t *\n\t\t\t * The dynamic part of the filter `$this->id` identifies the extending query.\n\t\t\t *\n\t\t\t * @since 4.5.1\n\t\t\t *\n\t\t\t * @param array               $ars           The query parse arguments.\n\t\t\t * @param LLMS_Database_Query $db_query      The LLMS_Database_Query instance.\n\t\t\t * @param array               $original_args Original arguments before merging with defaults.\n\t\t\t * @param array               $default_args  Default arguments before merging with original.\n\t\t\t */\n\t\t\t$this->arguments = apply_filters( \"llms_{$this->id}_query_parse_args\", $this->arguments, $this, $this->arguments_original, $this->arguments_default );\n\t\t}\n\n\t\tforeach ( $this->arguments as $arg => $val ) {\n\n\t\t\t$val = 'sort' === $arg ? $this->sanitize_sort( $this->arguments['sort'] ) : $val;\n\t\t\t$this->set( $arg, $val );\n\n\t\t}\n\t}\n\n\tpublic function get_count_only_result() {\n\t\t$results = $this->get_results();\n\t\tif ( ! is_array( $results ) || ! count( $results ) || ! isset( $results[0] ) || ! property_exists(\n\t\t\t$results[0],\n\t\t\t'total'\n\t\t) ) {\n\t\t\t/* Translators: %s - name of class. */\n\t\t\terror_log(\n\t\t\t\tsprintf(\n\t\t\t\t\t__( '[LifterLMS] Found results with count_only has no result rows in %s.', 'lifterlms' ),\n\t\t\t\t\tget_class( $this )\n\t\t\t\t)\n\t\t\t);\n\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn intval( $results[0]->total );\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-session-data.php",
    "content": "<?php\n/**\n * Base session data class\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 4.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Abstract_Session\n *\n * @since 4.0.0\n */\nabstract class LLMS_Abstract_Session_Data {\n\n\t/**\n\t * Session data\n\t *\n\t * @var array\n\t */\n\tprotected $data = array();\n\n\t/**\n\t * Session ID\n\t *\n\t * The session ID is the logged-in user ID\n\t * or a unique ID for an anonymous user.\n\t *\n\t * @var string\n\t */\n\tprotected $id = '';\n\n\t/**\n\t * Determines if there's session data to be saved.\n\t *\n\t * If `true` no data needs to be saved, if `false` data\n\t * needs to be saved.\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_clean = true;\n\n\t/**\n\t * Generate a session key for the current user/visitor.\n\t *\n\t * A logged-in user will use their WP_User ID while logged-out\n\t * users will be assigned a random string.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function generate_id() {\n\n\t\t// Use the current user id if the user is logged in.\n\t\tif ( is_user_logged_in() ) {\n\t\t\treturn strval( get_current_user_id() );\n\t\t}\n\n\t\t// Generate a random id.\n\t\trequire_once ABSPATH . 'wp-includes/class-phpass.php';\n\t\t$hasher = new PasswordHash( 8, false );\n\t\treturn md5( $hasher->get_random_bytes( 32 ) );\n\n\t}\n\n\t/**\n\t * Retrieve session ID.\n\t *\n\t * @since 1.0.0\n\t * @since 4.0.0 Moved from `LLMS_Sessions`, automatically generates an ID if it doesn't exist.\n\t *\n\t * @return string Session ID.\n\t */\n\tpublic function get_id() {\n\t\tif ( empty( $this->id ) ) {\n\t\t\t$this->id = $this->generate_id();\n\t\t}\n\t\treturn $this->id;\n\t}\n\n\t/**\n\t * Retrieve a session variable.\n\t *\n\t * @since 1.0.0\n\t * @since 3.37.7 Added the `$default` parameter that represents the default value\n\t *               to return if the session variable requested doesn't exist.\n\t * @since 4.0.0 Moved from `LLMS_Session`.\n\t *\n\t * @param string $key     The key of the session variable.\n\t * @param mixed  $default Optional. The default value to return if no session variable is found with the provided key. Default `false`.\n\t * @return mixed\n\t */\n\tpublic function get( $key, $default = false ) {\n\n\t\t$key = sanitize_key( $key );\n\t\treturn isset( $this->data[ $key ] ) ? maybe_unserialize( $this->data[ $key ] ) : $default;\n\n\t}\n\n\t/**\n\t * Set a session variable.\n\t *\n\t * @since 1.0.0\n\t * @since 4.0.0 Moved from `LLMS_Session`.\n\t *\n\t * @param string $key   The key of the session variable.\n\t * @param mixed  $value The value of the session variable.\n\t * @return mixed\n\t */\n\tpublic function set( $key, $value ) {\n\n\t\t/**\n\t\t * Using `isset()` allows us to explicitly save a value of `false`\n\t\t * since the `get()` method will return the default value `false` making it look\n\t\t * as if the value hasn't changed (when it actually has).\n\t\t */\n\t\tif ( ! isset( $this->$key ) || $value !== $this->get( $key ) ) {\n\t\t\t$this->data[ sanitize_key( $key ) ] = maybe_serialize( $value );\n\t\t\t$this->is_clean                     = false;\n\t\t}\n\n\t\treturn $this->get( $key );\n\n\t}\n\n\t/**\n\t * Magic get\n\t *\n\t * @since 1.0.0\n\t * @since 4.0.0 Moved from `LLMS_Session`.\n\t *\n\t * @param string $key The key of the session variable.\n\t * @return mixed\n\t */\n\tpublic function __get( $key ) {\n\t\treturn $this->get( $key );\n\t}\n\n\t/**\n\t * Magic set\n\t *\n\t * @since 1.0.0\n\t * @since 4.0.0 Moved from `LLMS_Session`.\n\t *\n\t * @param string $key   The key of the session variable.\n\t * @param string $value The value of the session variable.\n\t * @return void\n\t */\n\tpublic function __set( $key, $value ) {\n\t\t$this->set( $key, $value );\n\t}\n\n\t/**\n\t * Magic isset\n\t *\n\t * @since 1.0.0\n\t * @since 4.0.0 Use `sanitize_key()` (like other methods in this class) instead of `sanitize_title()`.\n\t *\n\t * @param string $key The key of the session variable.\n\t * @return bool\n\t */\n\tpublic function __isset( $key ) {\n\t\treturn isset( $this->data[ sanitize_key( $key ) ] );\n\t}\n\n\t/**\n\t * Magic unset\n\t *\n\t * @since 1.0.0\n\t * @since 4.0.0 Use `sanitize_key()` when removing session var.\n\t *\n\t * @param string $key The key of the session variable.\n\t * @return void\n\t */\n\tpublic function __unset( $key ) {\n\t\tif ( isset( $this->$key ) ) {\n\t\t\tunset( $this->data[ sanitize_key( $key ) ] );\n\t\t\t$this->is_clean = false;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-session-database-handler.php",
    "content": "<?php\n/**\n * Base session data class\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 4.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Abstract_Session\n *\n * @since 4.0.0\n */\nabstract class LLMS_Abstract_Session_Database_Handler extends LLMS_Abstract_Session_Data {\n\n\t/**\n\t * Cache group name, used for WP caching functions\n\t *\n\t * @var string\n\t */\n\tprotected $cache_group = 'llms_session_id';\n\n\t/**\n\t * Delete all sessions from the database\n\t *\n\t * This method is the callback function for the `llms_delete_expired_session_data` cron event, which\n\t * deletes expired sessions hourly.\n\t *\n\t * This method is also used by the admin tool to remove *all* sessions on demand.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param boolean $expired_only If `true`, only delete expired sessions, otherwise deletes all events.\n\t * @return int\n\t */\n\tpublic function clean( $expired_only = true ) {\n\n\t\tglobal $wpdb;\n\n\t\t$query = \"DELETE FROM {$wpdb->prefix}lifterlms_sessions\";\n\t\tif ( $expired_only ) {\n\t\t\t$query .= $wpdb->prepare( ' WHERE expires < %d', time() );\n\t\t}\n\n\t\tLLMS_Cache_Helper::invalidate_group( $this->cache_group );\n\n\t\treturn $wpdb->query( $query ); // phpcs:ignore: WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared\n\n\t}\n\n\t/**\n\t * Delete a session from the database\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param string $id Session key.\n\t * @return boolean\n\t */\n\tpublic function delete( $id ) {\n\n\t\twp_cache_delete( $this->get_cache_key( $id ), $this->cache_group );\n\n\t\tglobal $wpdb;\n\t\treturn (bool) $wpdb->delete(  // phpcs:ignore: WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\t\t$wpdb->prefix . 'lifterlms_sessions',\n\t\t\tarray(\n\t\t\t\t'session_key' => $id,\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve a prefixed cache key\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param string $key Unprefixed cache key.\n\t * @return string\n\t */\n\tprotected function get_cache_key( $key ) {\n\t\treturn LLMS_Cache_Helper::get_prefix( $this->cache_group ) . $key;\n\t}\n\n\t/**\n\t * Save the session to the database\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param int $expires Timestamp of the session expiration.\n\t * @return boolean\n\t */\n\tpublic function save( $expires ) {\n\n\t\t// Only save if we have data to save.\n\t\tif ( $this->is_clean ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tglobal $wpdb;\n\t\t$save = $wpdb->query(  // phpcs:ignore: WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"INSERT INTO {$wpdb->prefix}lifterlms_sessions ( `session_key`, `data`, `expires` ) VALUES ( %s, %s, %d )\n\t\t\t\tON DUPLICATE KEY UPDATE `data` = VALUES ( `data` ), `expires` = VALUES ( `expires` )\",\n\t\t\t\t$this->get_id(),\n\t\t\t\tmaybe_serialize( $this->data ),\n\t\t\t\t$expires\n\t\t\t)\n\t\t);\n\n\t\twp_cache_set( $this->get_cache_key( $this->get_id() ), $this->data, $this->cache_group, $expires - time() );\n\t\t$this->is_clean = true;\n\n\t\treturn (bool) $save;\n\n\t}\n\n\t/**\n\t * Retrieve session data from the database\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param string $key     Session key.\n\t * @param array  $default Default value used when no data exists.\n\t * @return string|array\n\t */\n\tpublic function read( $key, $default = array() ) {\n\n\t\t$cache_key = $this->get_cache_key( $key );\n\t\t$data      = wp_cache_get( $cache_key, $this->cache_group );\n\n\t\tif ( false === $data ) {\n\n\t\t\tglobal $wpdb;\n\n\t\t\t$data = $wpdb->get_var( $wpdb->prepare( \"SELECT `data` FROM {$wpdb->prefix}lifterlms_sessions WHERE `session_key` = %s\", $key ) );  // phpcs:ignore: WordPress.DB.DirectDatabaseQuery.DirectQuery\n\n\t\t\tif ( is_null( $data ) ) {\n\t\t\t\t$data = $default;\n\t\t\t}\n\n\t\t\t$duration = $this->expires - time();\n\t\t\tif ( 0 < $duration ) {\n\t\t\t\twp_cache_set( $cache_key, $data, $this->cache_group, $duration );\n\t\t\t}\n\t\t}\n\n\t\treturn maybe_unserialize( $data );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms-abstract-user-engagement.php",
    "content": "<?php\n/**\n * LLMS_Abstract_User_Engagement class\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 6.0.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Base model class for awarded engagements (certificates and achievements).\n *\n * @since 6.0.0\n */\nabstract class LLMS_Abstract_User_Engagement extends LLMS_Post_Model {\n\n\tuse LLMS_Trait_User_Engagement_Type;\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string|int|LLMS_Post_Model|WP_Post $model 'new', WP post id, instance of an extending class, instance of WP_Post.\n\t * @param array                              $args  Args to create the post, only applies when $model is 'new'.\n\t * @return void\n\t */\n\tpublic function __construct( $model, $args = array() ) {\n\n\t\t$this->engagement_type = $this->model_post_type;\n\t\tparent::__construct( $model, $args );\n\t}\n\n\t/**\n\t * Called immediately after creating / inserting a new post into the database\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function after_create() {\n\n\t\t$this->sync( 'create' );\n\t}\n\n\t/**\n\t * Delete the engagement\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Migrated from LLMS_User_Certificate and LLMS_User_Achievement.\n\t *\n\t * @return void\n\t */\n\tpublic function delete() {\n\n\t\t/**\n\t\t * Action fired immediately prior to the deletion of a user's awarded engagement.\n\t\t *\n\t\t * The dynamic portion of the hook name, `$this->model_post_type`,\n\t\t * refers to the engagement type, either \"achievement\" or \"certificate\".\n\t\t *\n\t\t * @since 3.18.0\n\t\t * @since 6.0.0 Migrated from LLMS_User_Certificate and LLMS_User_Achievement.\n\t\t *\n\t\t * @param LLMS_Abstract_User_Engagement $User_Engagement Achievement or certificate class object.\n\t\t */\n\t\tdo_action( \"llms_before_delete_{$this->model_post_type}\", $this );\n\n\t\tglobal $wpdb;\n\t\t$id = $this->get( 'id' );\n\t\t$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\t\t\"{$wpdb->prefix}lifterlms_user_postmeta\",\n\t\t\tarray(\n\t\t\t\t'user_id'    => $this->get_user_id(),\n\t\t\t\t'meta_key'   => $this->get_user_post_meta_key(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value\n\t\t\t),\n\t\t\tarray( '%d', '%s', '%d' )\n\t\t);\n\t\twp_delete_post( $id, true );\n\n\t\t/**\n\t\t * Action fired immediately after the deletion of a user's awarded engagement.\n\t\t *\n\t\t * The dynamic portion of the hook name, `$this->model_post_type`,\n\t\t * refers to the engagement type, either \"achievement\" or \"certificate\".\n\t\t *\n\t\t * @since 3.18.0\n\t\t * @since 6.0.0 Migrated from LLMS_User_Certificate and LLMS_User_Achievement.\n\t\t *\n\t\t * @param LLMS_Abstract_User_Engagement $User_Engagement Achievement or certificate class object.\n\t\t */\n\t\tdo_action( \"llms_delete_{$this->model_post_type}\", $this );\n\t}\n\n\t/**\n\t * Retrieve the date the achievement was earned (created)\n\t *\n\t * @since 3.14.0\n\t * @since 6.0.0 Migrated from LLMS_User_Certificate and LLMS_User_Achievement.\n\t *\n\t * @param string $format Date format string.\n\t * @return string\n\t */\n\tpublic function get_earned_date( $format = null ) {\n\n\t\t$format = $format ? $format : get_option( 'date_format' );\n\n\t\treturn $this->get_date( 'date', $format );\n\t}\n\n\t/**\n\t * Get the WP Post ID of the post which triggered the earning of the certificate\n\t *\n\t * This would be a lesson, course, section, track, etc...\n\t *\n\t * @since 3.8.0\n\t * @since 4.5.0 Force return to an integer.\n\t * @since 6.0.0 Migrated from LLMS_User_Certificate and LLMS_User_Achievement.\n\t *\n\t * @return int\n\t */\n\tpublic function get_related_post_id() {\n\n\t\t$meta = $this->get_user_postmeta();\n\n\t\treturn isset( $meta->post_id ) ? absint( $meta->post_id ) : $this->get( 'related' );\n\t}\n\n\t/**\n\t * Retrieve the user ID of the user who earned the certificate\n\t *\n\t * @since 3.8.0\n\t * @since 3.9.0 Unknown.\n\t * @since 4.5.0 Force return to an integer.\n\t * @since 6.0.0 Migrated from LLMS_User_Certificate and LLMS_User_Achievement.\n\t *\n\t * @return int\n\t */\n\tpublic function get_user_id() {\n\n\t\t$meta = $this->get_user_postmeta();\n\n\t\treturn isset( $meta->user_id ) ? absint( $meta->user_id ) : $this->get( 'author' );\n\t}\n\n\t/**\n\t * Retrieve user postmeta data for the achievement or certificate.\n\t *\n\t * @since 3.8.0\n\t * @since 6.0.0 Migrated from LLMS_User_Certificate and LLMS_User_Achievement.\n\t *\n\t * @return stdClass\n\t */\n\tpublic function get_user_postmeta() {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT user_id, post_id FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_value = %d AND meta_key = %s\",\n\t\t\t\t$this->get( 'id' ),\n\t\t\t\t$this->get_user_post_meta_key()\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the user postmeta key recorded when the engagement is earned.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_user_post_meta_key() {\n\n\t\treturn sprintf( '_%s_earned', $this->model_post_type );\n\t}\n\n\t/**\n\t * Determines if the achievement or certificate has been awarded.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_awarded() {\n\n\t\tif ( 'publish' !== $this->get( 'status' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn (bool) $this->get( 'awarded' );\n\t}\n\n\t/**\n\t * Allow child classes to merge the post content based on content from the template.\n\t *\n\t * @since 6.0.0\n\t * @since 6.4.0 Added optional `$content` and `$load_reusable_blocks` parameters.\n\t *\n\t * @param string $content              Optionally use the given content instead of `$this->content`.\n\t * @param bool   $load_reusable_blocks Optionally replace reusable blocks with their actual blocks.\n\t * @return string\n\t */\n\tpublic function merge_content( $content = null, $load_reusable_blocks = false ) {\n\n\t\tif ( is_null( $content ) ) {\n\t\t\t$content = $this->get( 'content', true );\n\t\t}\n\n\t\tif ( $load_reusable_blocks ) {\n\t\t\t$blocks  = parse_blocks( $content );\n\t\t\t$blocks  = LLMS_Forms::instance()->load_reusable_blocks( $blocks );\n\t\t\t$content = serialize_blocks( $blocks );\n\t\t}\n\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Update the awarded engagement by regenerating it from its template.\n\t *\n\t * @since 6.0.0\n\t * @since 6.4.0 Added replacement of references to reusable blocks with their actual blocks.\n\t *\n\t * @param string $context Sync context. Either \"update\" for an update to an existing awarded engagement\n\t *                        or \"create\" when the awarded engagement is being created.\n\t * @return boolean Returns false if the parent doesn't exist, otherwise returns true.\n\t */\n\tpublic function sync( $context = 'update' ) {\n\n\t\t$template_id = $this->get( 'parent' );\n\t\t$template    = $this->get_user_engagement( $template_id, false );\n\t\tif ( ! $template ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$this->set( 'title', get_post_meta( $template_id, \"_llms_{$this->model_post_type}_title\", true ) );\n\t\tif ( get_post_thumbnail_id( $template_id ) !== get_post_thumbnail_id( $this->get( 'post' ) ) &&\n\t\t\t! set_post_thumbnail( $this->get( 'post' ), get_post_thumbnail_id( $template_id ) )\n\t\t) {\n\t\t\tdelete_post_thumbnail( $this->get( 'post' ) );\n\t\t}\n\n\t\t// Copy the content with optional merge codes, shortcodes, and optional block editor layout meta properties\n\t\t// from the template to this awarded engagement.\n\t\t$content = $template->get( 'content', true );\n\t\t$this->set( 'content', $this->merge_content( $content, true ) );\n\t\t$this->sync_meta( $template );\n\n\t\t/**\n\t\t * Action run after an awarded engagement is synchronized with its template.\n\t\t *\n\t\t * The dynamic portion of the hook name, `$this->model_post_type`,\n\t\t * refers to the engagement type, either \"achievement\" or \"certificate\".\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param LLMS_Abstract_User_Engagement $engagement Awarded engagement object.\n\t\t * @param LLMS_Abstract_User_Engagement $template   Engagement template object.\n\t\t * @param string                        $context    The context within which the synchronization is run.\n\t\t *                                                  Either \"create\" or \"update\".\n\t\t */\n\t\tdo_action( \"llms_{$this->model_post_type}_synchronized\", $this, $template, $context );\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * This is a stub that allows extending classes to sync additional data from the template during a sync operation.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_Abstract_User_Engagement $template\n\t * @return void\n\t */\n\tprotected function sync_meta( $template ) {\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.api.handler.php",
    "content": "<?php\n/**\n * 3rd Party API request handler abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.11.2\n * @version 4.21.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * 3rd Party API request handler abstract class\n *\n * @since 3.11.2\n * @since 3.30.1 self::set_request_body() may respond with `null` in order to send a request with no `body`.\n */\nabstract class LLMS_Abstract_API_Handler {\n\n\t/**\n\t * Determines if an empty response body should be interpreted as an error\n\t *\n\t * @var bool\n\t */\n\tprotected $allow_empty_response = false;\n\n\t/**\n\t * Default request method\n\t *\n\t * @var  string\n\t */\n\tprotected $default_request_method = 'POST';\n\n\t/**\n\t * Determine if the request should be made as JSON\n\t *\n\t * @var bool\n\t */\n\tprotected $is_json = true;\n\n\t/**\n\t * Request timeout in seconds\n\t *\n\t * @var integer\n\t */\n\tprotected $request_timeout = 60;\n\n\tprivate $result        = null;\n\tprivate $error_message = null;\n\tprivate $error_object  = null;\n\tprivate $error_type    = null;\n\n\t/**\n\t * Construct an API call, parameters are passed to private `call()` function\n\t *\n\t * @param    stirng $resource  url endpoint or resource to make a request to\n\t * @param    array  $data      array of data to pass in the body of the request\n\t * @param    string $method    method of request (POST, GET, DELETE, PUT, etc...)\n\t * @return   void\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tpublic function __construct( $resource, $data, $method = null ) {\n\n\t\t$this->call( $resource, $data, $method );\n\n\t}\n\n\t/**\n\t * Execute an API request.\n\t *\n\t * @since 3.11.2\n\t * @since 3.30.1 self::set_request_body() may respond with `null` in order to send a request with no `body`\n\t * @since 4.21.3 Use `wp_json_encode()` in favor of `json_encode()`.\n\t *                Updated the API connection error message.\n\t *\n\t * @param    string $resource  url endpoint or resource to make a request to.\n\t * @param    array  $data      array of data to pass in the body of the request.\n\t * @param    string $method    method of request (POST, GET, DELETE, PUT, etc...).\n\t * @return   null\n\t */\n\tprivate function call( $resource, $data, $method = null ) {\n\n\t\t$method = is_null( $method ) ? $this->default_request_method : $method;\n\n\t\t// setup headers.\n\t\t$content_type = $this->is_json ? 'application/json; charset=utf-8' : 'application/x-www-form-urlencoded';\n\t\t$headers      = $this->set_request_headers(\n\t\t\tarray(\n\t\t\t\t'content-type' => $content_type,\n\t\t\t),\n\t\t\t$resource,\n\t\t\t$method\n\t\t);\n\n\t\t$args = array(\n\t\t\t'headers'    => $headers,\n\t\t\t'method'     => $method,\n\t\t\t'timeout'    => $this->request_timeout,\n\t\t\t'user-agent' => $this->set_user_agent( 'LifterLMS ' . LLMS_VERSION, $resource, $method ),\n\t\t);\n\n\t\t// setup body.\n\t\t$body = $this->set_request_body( $data, $method, $resource );\n\n\t\t// if \"null\" if passed to body, don't send a body at all.\n\t\tif ( ! is_null( $body ) ) {\n\t\t\t$args['body'] = $this->is_json && $body ? wp_json_encode( $body ) : $body;\n\t\t}\n\n\t\t// Attempt to call the API.\n\t\t$response = wp_safe_remote_request(\n\t\t\t$this->set_request_url( $resource, $method ),\n\t\t\t$args\n\t\t);\n\n\t\t// Connection error.\n\t\tif ( is_wp_error( $response ) ) {\n\t\t\treturn $this->set_error( __( 'There was a problem connecting to the external API.', 'lifterlms' ), 'api_connection', $response );\n\t\t}\n\n\t\t// Empty body.\n\t\tif ( ! $this->allow_empty_response && empty( $response['body'] ) ) {\n\n\t\t\treturn $this->set_error( __( 'Empty Response.', 'lifterlms' ), 'empty_response', $response );\n\n\t\t}\n\n\t\t$this->parse_response( $response );\n\n\t}\n\n\t/**\n\t * Retrieve the private \"error_message\" variable\n\t *\n\t * @return   string\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tpublic function get_error_message() {\n\n\t\treturn $this->error_message;\n\n\t}\n\n\t/**\n\t * Get the private \"error_object\" variable\n\t *\n\t * @return   mixed\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tpublic function get_error_object() {\n\n\t\treturn $this->error_object;\n\n\t}\n\n\t/**\n\t * Retrieve the private \"error_type\" variable\n\t *\n\t * @return   string\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tpublic function get_error_type() {\n\n\t\treturn $this->error_type;\n\n\t}\n\n\t/**\n\t * Retrieve the private \"result\" variable\n\t *\n\t * @return   mixed\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tpublic function get_result() {\n\n\t\treturn $this->result;\n\n\t}\n\n\t/**\n\t * Determine if the response is an error\n\t *\n\t * @return   boolean\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tpublic function is_error() {\n\n\t\treturn is_wp_error( $this->get_result() );\n\n\t}\n\n\t/**\n\t * Parse the body of the response and set a success/error\n\t *\n\t * @param    array $response  response data\n\t * @return   array\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tabstract protected function parse_response( $response );\n\n\t/**\n\t * Set an Error\n\t * Sets all error variables and sets the result as a WP_Error so the result can always be tested with `is_wp_error()`\n\t *\n\t * @param    string $message  error message\n\t * @param    string $type     error code or type\n\t * @param    object $obj      full error object or api response\n\t * @return   void\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tprotected function set_error( $message, $type, $obj ) {\n\n\t\t$this->result        = new WP_Error( $type, $message, $obj );\n\t\t$this->error_type    = $type;\n\t\t$this->error_message = $message;\n\t\t$this->error_object  = $obj;\n\n\t}\n\n\t/**\n\t * Set the result\n\t *\n\t * @param    mixed $result  result data\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tprotected function set_result( $result ) {\n\t\t$this->result = $result;\n\t}\n\n\t/**\n\t * Set request body\n\t *\n\t * @param    array  $data      request body\n\t * @param    string $method    request method\n\t * @param    string $resource  requested resource\n\t * @return   array\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tabstract protected function set_request_body( $data, $method, $resource );\n\n\t/**\n\t * Set request headers\n\t *\n\t * @param    array  $headers   default request headers\n\t * @param    string $resource  request resource\n\t * @param    string $method    request method\n\t * @return   array\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tprotected function set_request_headers( $headers, $resource, $method ) {\n\t\treturn $headers;\n\t}\n\n\t/**\n\t * Set the request URL\n\t *\n\t * @param    string $resource  requested resource\n\t * @param    string $method    request method\n\t * @return   string\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tabstract protected function set_request_url( $resource, $method );\n\n\t/**\n\t * Set the request User Agent\n\t * Can be overridden by extending classes when necessary\n\t *\n\t * @param    string $user_agent  default user agent (LifterLMS {$version})\n\t * @param    string $resource    requested resource\n\t * @param    string $method      request method\n\t * @return   string\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tprotected function set_user_agent( $user_agent, $resource, $method ) {\n\t\treturn $user_agent;\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.database.store.php",
    "content": "<?php\n/**\n * WPDB database interactions\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.14.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * WPDB database interactions abstract class\n *\n * @since 3.14.0\n * @since 3.33.0 setup() method returns self instead of void.\n * @since 3.34.0 to_array() method returns value of the primary key instead of the format.\n * @since 3.36.0 Prevent undefined index error when attempting to retrieve an unset value from an unsaved object.\n *               Hydrate before returning an array via the `to_array()` method.\n * @since 4.3.0 Add deprecated hook calls to preserve backwards compatibility for extending classes which have no `$type` property declaration.\n *              Updated the `$type` property to have a default placeholder value.\n */\nabstract class LLMS_Abstract_Database_Store {\n\n\t/**\n\t * The Database ID of the record\n\t *\n\t * @var int\n\t */\n\tprotected $id = null;\n\n\t/**\n\t * Object properties\n\t *\n\t * @var array\n\t */\n\tprivate $data = array();\n\n\t/**\n\t * Column name of the record's \"created\" date\n\t *\n\t * This can be set to an empty string if the extending\n\t * class does not utilize or require created date storage.\n\t *\n\t * @var string\n\t */\n\tprotected $date_created = 'created';\n\n\t/**\n\t * Column name of the record's \"updated\" date\n\t *\n\t * This can be set to an empty string if the extending\n\t * class does not utilize or require updated date storage.\n\t *\n\t * @var string\n\t */\n\tprotected $date_updated = 'updated';\n\n\t/**\n\t * Array of table column name => format\n\t *\n\t * @var array\n\t */\n\tprotected $columns = array();\n\n\t/**\n\t * Primary Key column name => format\n\t *\n\t * @var array\n\t */\n\tprotected $primary_key = array(\n\t\t'id' => '%d',\n\t);\n\n\t/**\n\t * Database Table Name\n\t *\n\t * @var string\n\t */\n\tprotected $table = '';\n\n\t/**\n\t * Database Table Prefix\n\t *\n\t * @var string\n\t */\n\tprotected $table_prefix = 'lifterlms_';\n\n\t/**\n\t * The record type\n\t *\n\t * Used for filters/actions.\n\t *\n\t * This is a placeholder which should be redefined in any extending classes.\n\t *\n\t * @var string\n\t */\n\tprotected $type = '_db_record_';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.14.0\n\t * @since 3.21.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tif ( ! $this->id ) {\n\n\t\t\t// If created dates supported, add current time to the data on construction.\n\t\t\tif ( $this->date_created ) {\n\t\t\t\t$this->set( $this->date_created, llms_current_time( 'mysql' ), false );\n\t\t\t}\n\n\t\t\tif ( $this->date_updated ) {\n\t\t\t\t$this->set( $this->date_updated, llms_current_time( 'mysql' ), false );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Get object data\n\t *\n\t * @since 3.14.0\n\t *\n\t * @param string $key Key to retrieve.\n\t * @return mixed\n\t */\n\tpublic function __get( $key ) {\n\t\treturn $this->data[ $key ];\n\t}\n\n\t/**\n\t * Determine if the item exists in the database\n\t *\n\t * @since 3.14.7\n\t * @since 3.15.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function exists() {\n\n\t\tif ( $this->primary_key ) {\n\t\t\treturn $this->read( $this->get_primary_key() ) ? true : false;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Get object data\n\t *\n\t * @since 3.14.0\n\t * @since 3.16.0 Unknown.\n\t * @since 3.36.0 Prevent undefined index error when attempting to retrieve an unset value from an unsaved object.\n\t *\n\t * @param string  $key   Key to retrieve.\n\t * @param boolean $cache If true, save data to to the object for future gets.\n\t * @return mixed\n\t */\n\tpublic function get( $key, $cache = true ) {\n\n\t\t$key_exists = isset( $this->data[ $key ] );\n\t\tif ( ! $key_exists && $this->id ) {\n\t\t\t$res = $this->read( $key )[ $key ];\n\t\t\tif ( $cache ) {\n\t\t\t\t$this->set( $key, $res );\n\t\t\t}\n\t\t\treturn $res;\n\t\t}\n\t\treturn $key_exists ? $this->$key : null;\n\n\t}\n\n\t/**\n\t * Set object data\n\t *\n\t * @since 3.14.0\n\t *\n\t * @param string $key Column name.\n\t * @param mixed  $val Column value.\n\t * @return void\n\t */\n\tpublic function __set( $key, $val ) {\n\t\t$this->data[ $key ] = $val;\n\t}\n\n\t/**\n\t * General setter\n\t *\n\t * @since 3.14.0\n\t * @since 3.21.0 Unknown.\n\t *\n\t * @param string  $key  Column name.\n\t * @param mixed   $val  Column value.\n\t * @param boolean $save If true, immediately persists to database.\n\t * @return LLMS_Abstract_Database_Store Instance of the current object, useful for chaining.\n\t */\n\tpublic function set( $key, $val, $save = false ) {\n\n\t\t$this->$key = $val;\n\t\tif ( $save ) {\n\t\t\t$update = array(\n\t\t\t\t$key => $val,\n\t\t\t);\n\t\t\t// If update date supported, add an updated date.\n\t\t\tif ( $this->date_updated ) {\n\t\t\t\t$update[ $this->date_updated ] = llms_current_time( 'mysql' );\n\t\t\t}\n\t\t\t$this->update( $update );\n\t\t}\n\n\t\treturn $this;\n\n\t}\n\n\t/**\n\t * Setup an object with an array of data\n\t *\n\t * @since 3.14.0\n\t * @since 3.33.0 Return self for chaining instead of void.\n\t *\n\t * @param array $data key => val\n\t * @return LLMS_Abstract_Database_Store Instance of the current object, useful for chaining.\n\t */\n\tpublic function setup( $data ) {\n\n\t\tforeach ( $data as $key => $val ) {\n\t\t\t$this->set( $key, $val, false );\n\t\t}\n\n\t\treturn $this;\n\n\t}\n\n\t/**\n\t * Create the item in the database\n\t *\n\t * @since 3.14.0\n\t * @since 3.24.0 Unknown.\n\t * @since 4.3.0 Added deprecated hook call to `llms__created` action to preserve backwards compatibility.\n\t * @since 6.0.0 Removed deprecated `llms__created` action hook.\n\t *\n\t * @return int|false Record ID on success, false on error or when there's nothing to save.\n\t */\n\tprivate function create() {\n\n\t\tif ( ! $this->data ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tglobal $wpdb;\n\t\t$format = array_map( array( $this, 'get_column_format' ), array_keys( $this->data ) );\n\t\t$res    = $wpdb->insert( $this->get_table(), $this->data, $format ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\tif ( 1 === $res ) {\n\n\t\t\t$this->id = $wpdb->insert_id;\n\n\t\t\t/**\n\t\t\t * Fires when a new database record is created.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$this->type`, refers to the record type.\n\t\t\t *\n\t\t\t * @since Unknown.\n\t\t\t *\n\t\t\t * @param int                          $id  Record ID.\n\t\t\t * @param LLMS_Abstract_Database_Store $obj Instance of the record object.\n\t\t\t */\n\t\t\tdo_action( \"llms_{$this->type}_created\", $this->id, $this );\n\n\t\t\treturn $this->id;\n\t\t}\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Delete the object from the database\n\t *\n\t * @since 3.14.0\n\t * @since 3.24.0 Unknown.\n\t * @since 4.3.0 Added deprecated hook call to `llms__deleted` action to preserve backwards compatibility.\n\t * @since 6.0.0 Removed deprecated `llms__deleted` action hook.\n\t *\n\t * @return boolean `true` on success, `false` otherwise.\n\t */\n\tpublic function delete() {\n\n\t\tif ( ! $this->id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$id = $this->id;\n\t\tglobal $wpdb;\n\t\t$where = array_combine( array_keys( $this->primary_key ), array( $this->id ) );\n\t\t$res   = $wpdb->delete( $this->get_table(), $where, array_values( $this->primary_key ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\tif ( $res ) {\n\t\t\t$this->id   = null;\n\t\t\t$this->data = array();\n\n\t\t\t/**\n\t\t\t * Fires when a new database record is created.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$this->type`, refers to the record type.\n\t\t\t *\n\t\t\t * @since Unknown.\n\t\t\t *\n\t\t\t * @param int                          $id  Record ID.\n\t\t\t * @param LLMS_Abstract_Database_Store $obj Instance of the record object.\n\t\t\t */\n\t\t\tdo_action( \"llms_{$this->type}_deleted\", $id, $this );\n\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Read object data from the database\n\t *\n\t * @since 3.14.0\n\t *\n\t * @param string[]|string $keys Key name (or array of keys) to retrieve from the database.\n\t * @return array|false Returns a key=>val array of data or `false` when record not found.\n\t */\n\tprivate function read( $keys ) {\n\n\t\tglobal $wpdb;\n\t\tif ( is_array( $keys ) ) {\n\t\t\t$keys = implode( ', ', $keys );\n\t\t}\n\t\t$res = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\t\t$wpdb->prepare( \"SELECT {$keys} FROM {$this->get_table()} WHERE {$this->get_primary_key()} = %d\", $this->id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- This query is safe.\n\t\t\tARRAY_A\n\t\t);\n\t\treturn ! $res ? false : $res;\n\n\t}\n\n\t/**\n\t * Update object data in the database\n\t *\n\t * @since 3.14.0\n\t * @since 3.24.0 Unknown.\n\t * @since 4.3.0 Added deprecated hook call to `llms__updated` action to preserve backwards compatibility.\n\t * @since 6.0.0 Removed deprecated `llms__updated` action hook.\n\t *\n\t * @param array $data Data to update as key=>val.\n\t * @return boolean\n\t */\n\tprivate function update( $data ) {\n\n\t\tglobal $wpdb;\n\t\t$format = array_map( array( $this, 'get_column_format' ), array_keys( $data ) );\n\t\t$where  = array_combine( array_keys( $this->primary_key ), array( $this->id ) );\n\t\t$res    = $wpdb->update( $this->get_table(), $data, $where, $format, array_values( $this->primary_key ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\tif ( $res ) {\n\n\t\t\t/**\n\t\t\t * Fires when a new database record is updated.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$this->type`, refers to the record type.\n\t\t\t *\n\t\t\t * @since Unknown.\n\t\t\t *\n\t\t\t * @param int                          $id  Record ID.\n\t\t\t * @param LLMS_Abstract_Database_Store $obj Instance of the record object.\n\t\t\t */\n\t\t\tdo_action( \"llms_{$this->type}_updated\", $this->id, $this );\n\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Load the whole object from the database\n\t *\n\t * @since 3.14.0\n\t *\n\t * @return LLMS_Abstract_Database_Store instance of the current object, useful for chaining.\n\t */\n\tprotected function hydrate() {\n\n\t\tif ( $this->id ) {\n\t\t\t$res = $this->read( array_keys( $this->columns ) );\n\t\t\tif ( $res ) {\n\t\t\t\t$this->data = array_merge( $this->data, $res );\n\t\t\t}\n\t\t}\n\n\t\treturn $this;\n\n\t}\n\n\t/**\n\t * Save object to the database\n\t *\n\t * Creates it if doesn't already exist, updates if it does.\n\t *\n\t * @since 3.14.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function save() {\n\n\t\tif ( ! $this->id ) {\n\t\t\t$id = $this->create();\n\t\t\tif ( $id ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t} else {\n\t\t\treturn $this->update( $this->data );\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieve the format for a column\n\t *\n\t * @since 3.14.0\n\t *\n\t * @param string $key Column name.\n\t * @return string\n\t */\n\tprivate function get_column_format( $key ) {\n\n\t\tif ( isset( $this->columns[ $key ] ) ) {\n\t\t\treturn $this->columns[ $key ];\n\t\t}\n\t\treturn '%s';\n\n\t}\n\n\t/**\n\t * Retrieve the primary key column name\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_primary_key() {\n\t\t$primary_key = array_keys( $this->primary_key );\n\t\treturn preg_replace( '/[^a-zA-Z0-9_]/', '', $primary_key[0] );\n\t}\n\n\t/**\n\t * Get the ID of the object\n\t *\n\t * @since 3.14.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_id() {\n\t\treturn $this->id;\n\t}\n\n\t/**\n\t * Get the table Name\n\t *\n\t * @since 3.14.0\n\t *\n\t * @return string\n\t */\n\tprivate function get_table() {\n\n\t\tglobal $wpdb;\n\t\treturn $wpdb->prefix . $this->table_prefix . $this->table;\n\n\t}\n\n\t/**\n\t * Retrieve object as an array\n\t *\n\t * @since 3.14.0\n\t * @since 3.34.0 Return the item ID instead of item format as the value of the primary key.\n\t * @since 3.36.0 Hydrate before returning the array.\n\t *\n\t * @return array\n\t */\n\tpublic function to_array() {\n\n\t\t$this->hydrate();\n\t\treturn array_merge( array_combine( array_keys( $this->primary_key ), array( $this->id ) ), $this->data );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.exportable.admin.table.php",
    "content": "<?php\n/**\n * Admin Table Export Functions\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.28.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Exportable admin table abstract class\n *\n * @since 3.28.0\n * @since 3.30.3 Explicitly define undefined properties.\n * @since 3.37.15 Ensure filenames of generated export files are for supported filetypes.\n * @since 4.0.0 Removed previously deprecated method `LLMS_Admin_Table::queue_export()`.\n */\nabstract class LLMS_Abstract_Exportable_Admin_Table {\n\n\t/**\n\t * The current page.\n\t *\n\t * @var int\n\t */\n\tprotected $current_page;\n\n\t/**\n\t * Unique ID for the table\n\t *\n\t * @var string\n\t */\n\tprotected $id;\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_exportable = true;\n\n\t/**\n\t * Export download nonce action.\n\t *\n\t * @var string\n\t */\n\tpublic const EXPORT_NONCE_ACTION = 'llms_export_table';\n\n\t/**\n\t * Generate an export file for the current table.\n\t *\n\t * @since 3.28.0\n\t * @since 3.28.1 Unknown.\n\t * @since 3.37.15 \"Sanitize\" submitted filename.\n\t *\n\t * @param array  $args     Arguments to pass get_results().\n\t * @param string $filename Filename of the existing file, if omitted creates a new file, if passed, will continue adding to existing file.\n\t * @param string $type     Export file type for forward compatibility. Currently only accepts 'csv'.\n\t * @return WP_Error|array\n\t */\n\tpublic function generate_export_file( $args = array(), $filename = null, $type = 'csv' ) {\n\n\t\t// We only support CSVs and don't allow fakers.\n\t\tif ( ! empty( $filename ) && pathinfo( $filename, PATHINFO_EXTENSION ) !== $type ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Always force page 1 regardless of what is requested. Pagination is handled below.\n\t\t$args['page'] = 1;\n\n\t\t/**\n\t\t * Customize the number of records per page when generating an export file.\n\t\t *\n\t\t * @since 3.28.0\n\t\t *\n\t\t * @param int $per_page Number of records per page.\n\t\t */\n\t\t$args['per_page'] = apply_filters( 'llms_table_generate_export_file_per_page_boost', 250 );\n\n\t\t$filename    = $filename ? $filename : $this->get_export_file_name() . '.' . $type;\n\t\t$file_path   = LLMS_TMP_DIR . $filename;\n\t\t$option_name = 'llms_gen_export_' . basename( $filename, '.' . $type );\n\t\t$args        = get_option( $option_name, $args );\n\n\t\t$handle = @fopen( $file_path, 'a+' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Yea but we handle the error alright I think.\n\t\tif ( ! $handle ) {\n\t\t\treturn new WP_Error( 'file_error', __( 'Unable to generate export file, could not open file for writing.', 'lifterlms' ) );\n\t\t}\n\n\t\t/**\n\t\t * Customize the delimiter used when generating CSV export files.\n\t\t *\n\t\t * @since 3.28.0\n\t\t *\n\t\t * @param int                                  $delim Delimiter.\n\t\t * @param LLMS_Abstract_Exportable_Admin_Table $table Instance of the table.\n\t\t * @param array                                $args  Array of arguments.\n\t\t */\n\t\t$delim = apply_filters( 'llms_table_generate_export_file_delimiter', ',', $this, $args );\n\n\t\tforeach ( $this->get_export( $args ) as $row ) {\n\t\t\tfputcsv( $handle, $row, $delim );\n\t\t}\n\n\t\tif ( ! $this->is_last_page() ) {\n\n\t\t\t$args['page'] = $this->get_current_page() + 1;\n\t\t\tupdate_option( $option_name, $args );\n\t\t\t$progress = round( ( $this->get_current_page() / $this->get_max_pages() ) * 100, 2 );\n\n\t\t} else {\n\n\t\t\tdelete_option( $option_name );\n\t\t\t$progress = 100;\n\n\t\t}\n\n\t\treturn array(\n\t\t\t'filename' => $filename,\n\t\t\t'progress' => $progress,\n\t\t\t'url'      => $this->get_export_file_url( $file_path ),\n\t\t);\n\t}\n\n\t/**\n\t * Gets data prepared for an export\n\t *\n\t * @since 3.15.0\n\t * @since 3.15.1 Unknown.\n\t *\n\t * @param array $args Query arguments to be passed to get_results().\n\t * @return array\n\t */\n\tpublic function get_export( $args = array() ) {\n\n\t\t$this->get_results( $args );\n\n\t\t$export = array();\n\t\tif ( 1 === $this->current_page ) {\n\t\t\t$export[] = $this->get_export_header();\n\t\t}\n\n\t\tforeach ( $this->get_tbody_data() as $row ) {\n\t\t\t$row_data = array();\n\t\t\tforeach ( array_keys( $this->get_columns( 'export' ) ) as $row_key ) {\n\t\t\t\t$row_data[ $row_key ] = html_entity_decode( $this->get_export_data( $row_key, $row ) );\n\t\t\t}\n\t\t\t$export[] = $row_data;\n\t\t}\n\n\t\treturn $export;\n\t}\n\n\t/**\n\t * Retrieve data for a cell in an export file\n\t * Should be overridden in extending classes\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $key  The column id / key.\n\t * @param mixed  $data Object / array of data that the function can use to extract the data.\n\t * @return mixed\n\t */\n\tpublic function get_export_data( $key, $data ) {\n\t\treturn trim( wp_strip_all_tags( $this->get_data( $key, $data ) ) );\n\t}\n\n\t/**\n\t * Retrieve the download URL to an export file\n\t *\n\t * @since 3.28.0\n\t * @since 3.28.1 Unknown.\n\t * @since 7.5.0 Add nonce to export file url.\n\t *\n\t * @param string $file_path Full path to a download file.\n\t * @return string\n\t */\n\tprotected function get_export_file_url( $file_path ) {\n\t\treturn add_query_arg(\n\t\t\tarray(\n\t\t\t\t'llms-dl-export'       => basename( $file_path ),\n\t\t\t\t'llms_dl_export_nonce' => wp_create_nonce( self::EXPORT_NONCE_ACTION ),\n\t\t\t),\n\t\t\tadmin_url( 'admin.php' )\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the header row for generating an export file\n\t *\n\t * @since 3.15.0\n\t * @since 3.17.3 Fixed SYLK warning generated when importing into Excel.\n\t *\n\t * @return array\n\t */\n\tpublic function get_export_header() {\n\n\t\t$cols = wp_list_pluck( $this->get_columns( 'export' ), 'title' );\n\n\t\t/**\n\t\t * If the first column is \"ID\" force it to lowercase\n\t\t * to prevent Excel from attempting to interpret the .csv as SYLK\n\t\t *\n\t\t * @see https://github.com/gocodebox/lifterlms/issues/397\n\t\t */\n\t\tforeach ( $cols as &$title ) {\n\t\t\tif ( 'id' === strtolower( $title ) ) {\n\t\t\t\t$title = strtolower( $title );\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t/**\n\t\t * Customize the export file header columns.\n\t\t *\n\t\t * The dynamic portion of this hook `$this->id` refers to the ID of the table.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param string[] $cols Array of file headers.\n\t\t */\n\t\treturn apply_filters( \"llms_table_get_{$this->id}_export_header\", $cols );\n\t}\n\n\t/**\n\t * Retrieves the file name for an export file.\n\t *\n\t * @since 3.15.0\n\t * @since 3.28.0 Unknown.\n\t * @since 7.0.1 Fixed issue encountered when special characters are present in the table's title.\n\t *\n\t * @param array $args Optional arguments passed from table to csv processor.\n\t * @return string\n\t */\n\tpublic function get_export_file_name( $args = array() ) {\n\n\t\t$parts = array(\n\t\t\tsanitize_file_name( strtolower( $this->get_export_title( $args ) ) ),\n\t\t\t_x( 'export', 'Used in export filenames', 'lifterlms' ),\n\t\t\tllms_current_time( 'Y-m-d' ),\n\t\t\twp_generate_password( 8, false, false ),\n\t\t);\n\n\t\t$filename = implode( '_', $parts );\n\n\t\t/**\n\t\t * Filters the file name for an export file.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->id`, refers to the table's\n\t\t * `$id` property.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 7.0.1 Added the `$parts` and `$table` parameters.\n\t\t *\n\t\t * @param string                               $filename The generated filename.\n\t\t * @param string[]                             $parts    An array of strings that makeup the generated filename\n\t\t *                                                       when joined with the underscore separator character.\n\t\t * @param LLMS_Abstract_Exportable_Admin_Table $table    Instance of the table object.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t\"llms_table_get_{$this->id}_export_file_name\",\n\t\t\t$filename,\n\t\t\t$parts,\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Get a lock key unique to the table & user for locking the table during export generation\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_export_lock_key() {\n\t\treturn sprintf( '%1$s:%2$d', $this->id, get_current_user_id() );\n\t}\n\n\t/**\n\t * Allow customization of the title for export files\n\t *\n\t * @since 3.15.0\n\t * @since 3.28.0 Unknown.\n\t *\n\t * @param array $args Optional arguments passed from table to csv processor.\n\t * @return string\n\t */\n\tpublic function get_export_title( $args = array() ) {\n\t\treturn apply_filters( 'llms_table_get_' . $this->id . '_export_title', $this->get_title(), $args );\n\t}\n\n\t/**\n\t * Retrieves the table's title.\n\t *\n\t * This method must be overwritten by extending classes.\n\t *\n\t * @since 7.0.1\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\t_doing_it_wrong(\n\t\t\t__METHOD__,\n\t\t\tesc_html(\n\t\t\t\tsprintf(\n\t\t\t\t// Translators: %s = the name of the method.\n\t\t\t\t\t__( \"Method '%s' must be overridden.\", 'lifterlms' ),\n\t\t\t\t\t__METHOD__\n\t\t\t\t)\n\t\t\t),\n\t\t\t'[version]'\n\t\t);\n\t\treturn $this->id;\n\t}\n\n\t/**\n\t * Determine if the table is currently locked due to export generation.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_export_locked() {\n\t\treturn llms()->processors()->get( 'table_to_csv' )->is_table_locked( $this->get_export_lock_key() );\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.integration.php",
    "content": "<?php\n/**\n * LifterLMS Integration Abstract\n *\n * @package LifterLMS/Abstracts\n *\n * @since 3.0.0\n * @version 4.21.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Integration abstract class\n *\n * @since 3.0.0\n */\nabstract class LLMS_Abstract_Integration extends LLMS_Abstract_Options_Data {\n\n\t/**\n\t * Integration ID\n\t *\n\t * Defined by extending class as a variable.\n\t *\n\t * @var string\n\t */\n\tpublic $id = '';\n\n\t/**\n\t * Integration Title\n\t *\n\t * Should be defined by extending class in configure() function (so it can be translated).\n\t *\n\t * @var string\n\t */\n\tpublic $title = '';\n\n\t/**\n\t * Integration Description\n\t *\n\t * Should be defined by extending class in configure() function (so it can be translated).\n\t *\n\t * @var string\n\t */\n\tpublic $description = '';\n\n\t/**\n\t * Integration Missing Dependencies Description\n\t *\n\t * Should be defined by extending class in configure() function (so it can be translated).\n\t *\n\t * Displays on the settings screen when `$this->is_installed()` is `false` to help users\n\t * identify what requirements are missing.\n\t *\n\t * @var string\n\t */\n\tpublic $description_missing = '';\n\n\t/**\n\t * Reference to the integration plugin's main plugin file basename\n\t *\n\t * In the `configure()` method call `plugin_basename()` on the main plugin file.\n\t *\n\t * @var string\n\t */\n\tprotected $plugin_basename = '';\n\n\t/**\n\t * Integration Priority\n\t *\n\t * Determines the order of the settings on the Integrations settings table.\n\t *\n\t * Built-in core integrations fire at 5.\n\t *\n\t * @var integer\n\t */\n\tprotected $priority = 20;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.8.0\n\t * @since 3.18.2 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->configure();\n\n\t\tadd_filter( 'lifterlms_integrations_settings_' . $this->id, array( $this, 'add_settings' ), $this->priority, 1 );\n\n\t\t/**\n\t\t * Trigger an action when the integration is initialized.\n\t\t *\n\t\t * The dynamic portion of this hook, `{$this->id}`, refers to the integration's unique ID.\n\t\t *\n\t\t * @since 4.21.0\n\t\t *\n\t\t * @param object $instance Class instance of the class extending the `LLMS_Abstract_Integration` abstract.\n\t\t */\n\t\tdo_action( \"llms_integration_{$this->id}_init\", $this );\n\n\t\tif ( ! empty( $this->plugin_basename ) ) {\n\t\t\tadd_action( \"plugin_action_links_{$this->plugin_basename}\", array( $this, 'plugin_action_links' ), 100, 4 );\n\t\t}\n\n\t}\n\n\t/**\n\t * Configure the integration\n\t *\n\t * Set required class properties and so on.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tabstract protected function configure();\n\n\t/**\n\t * Merge the default abstract settings with the actual integration settings\n\t *\n\t * Automatically called via filter upon construction.\n\t *\n\t * @since 3.17.8\n\t *\n\t * @param array $settings Existing settings from other integrations.\n\t * @return array\n\t */\n\tpublic function add_settings( $settings ) {\n\t\treturn array_merge( $settings, $this->get_settings() );\n\t}\n\n\t/**\n\t * Get additional settings specific to the integration\n\t *\n\t * Extending classes should override this with the settings\n\t * specific to the integration.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_integration_settings() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Retrieve the integration priority property.\n\t *\n\t * @since 3.33.1\n\t *\n\t * @return int\n\t */\n\tpublic function get_priority() {\n\t\treturn $this->priority;\n\t}\n\n\t/**\n\t * Retrieve an array of integration related settings\n\t *\n\t * @since 3.8.0\n\t * @since 3.21.1 Automatically output the `$description_missing` message when requirements are not met.\n\t * @since 4.21.0 Add an 'id' to the missing description HTML setting.\n\t *\n\t * @return array\n\t */\n\tprotected function get_settings() {\n\n\t\t$settings   = array();\n\t\t$settings[] = array(\n\t\t\t'type' => 'sectionstart',\n\t\t\t'id'   => 'llms_integration_' . $this->id . '_start',\n\t\t);\n\t\t$settings[] = array(\n\t\t\t'desc'  => $this->description,\n\t\t\t'id'    => 'llms_integration_' . $this->id . '_title',\n\t\t\t'title' => $this->title,\n\t\t\t'type'  => 'title',\n\t\t);\n\t\t$settings[] = array(\n\t\t\t'desc'    => __( 'Check to enable this integration.', 'lifterlms' ),\n\t\t\t'default' => 'no',\n\t\t\t'id'      => $this->get_option_name( 'enabled' ),\n\t\t\t'type'    => 'checkbox',\n\t\t\t'title'   => __( 'Enable / Disable', 'lifterlms' ),\n\t\t);\n\n\t\tif ( ! $this->is_installed() && ! empty( $this->description_missing ) ) {\n\t\t\t$settings[] = array(\n\t\t\t\t'id'    => 'llms_integration_' . $this->id . '_missing_requirements_desc',\n\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t'value' => '<em>' . $this->description_missing . '</em>',\n\t\t\t);\n\t\t}\n\n\t\t$settings   = array_merge( $settings, $this->get_integration_settings() );\n\t\t$settings[] = array(\n\t\t\t'type' => 'sectionend',\n\t\t\t'id'   => 'llms_integration_' . $this->id . '_end',\n\t\t);\n\n\t\t/**\n\t\t * Filters the integration's settings\n\t\t *\n\t\t * The dynamic portion of this hook, `{$this->id}`, refers to the integration's ID.\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param array[] $settings Array of settings arrays.\n\t\t * @param object  $instance Class instance of the class extending the `LLMS_Abstract_Integration` abstract.\n\t\t */\n\t\treturn apply_filters( \"llms_integration_{$this->id}_get_settings\", $settings, $this );\n\t}\n\n\t/**\n\t * Retrieve the option name prefix.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_option_prefix() {\n\t\treturn $this->option_prefix . 'integration_' . $this->id . '_';\n\t}\n\n\t/**\n\t * Autoload default option values from values defined in the integration settings array\n\t *\n\t * This will only run when extending integration classes define a version property greater than 1.\n\t *\n\t * This is a callback function for the WP core filter `default_option_{$option}`.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param mixed  $default_value        The default value. If no value is passed to `get_option()`, this will be an empty string.\n\t *                                     Otherwise it will be the default value passed to the method.\n\t * @param string $full_option_name     The full (prefixed) option name.\n\t * @param bool   $passed_default_value Whether or not a default value was passed to `get_option()`.\n\t * @return mixed The default option value.\n\t */\n\tpublic function get_option_default_value( $default_value, $full_option_name, $passed_default_value ) {\n\n\t\t// If a default value is explicitly passed, use it.\n\t\tif ( $passed_default_value ) {\n\t\t\treturn $default_value;\n\t\t}\n\n\t\tforeach ( $this->get_settings() as $setting ) {\n\n\t\t\tif ( ! empty( $setting['id'] ) && $full_option_name === $setting['id'] ) {\n\t\t\t\treturn isset( $setting['default'] ) ? $setting['default'] : $default_value;\n\t\t\t}\n\t\t}\n\n\t\treturn $default_value;\n\n\t}\n\n\t/**\n\t * Determine if the integration is enabled via the checkbox on the admin panel\n\t * and the necessary plugin (if any) is installed and activated\n\t *\n\t * @since 3.0.0\n\t * @since 3.17.8 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_available() {\n\t\treturn ( $this->is_installed() && $this->is_enabled() );\n\t}\n\n\t/**\n\t * Determine if the integration had been enabled via checkbox\n\t *\n\t * @since 3.0.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_enabled() {\n\t\treturn ( 'yes' === $this->get_option( 'enabled', 'no' ) );\n\t}\n\n\t/**\n\t * Determine if required dependencies are installed.\n\t *\n\t * Extending classes should override this to perform dependency checks.\n\t *\n\t * @since 3.0.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_installed() {\n\t\treturn true;\n\t}\n\n\t/**\n\t * Add plugin settings Action Links\n\t *\n\t * @since 3.37.9\n\t * @since 4.21.0 Don't check `$context`. If the plugin isn't active this won't run anyway so it's a useless check.\n\t *\n\t * @param string[] $links   Existing action links.\n\t * @param string   $file    Path to the plugin file, relative to the plugin directory.\n\t * @param array    $data    Plugin data.\n\t * @param string   $context Plugin's content (eg: active, invactive, etc...).\n\t * @return string[]\n\t */\n\tpublic function plugin_action_links( $links, $file, $data, $context ) {\n\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'page'    => 'llms-settings',\n\t\t\t\t'tab'     => 'integrations',\n\t\t\t\t'section' => $this->id,\n\t\t\t),\n\t\t\tadmin_url( 'admin.php' )\n\t\t);\n\n\t\t$links[] = '<a href=\"' . esc_url( $url ) . '\">' . _x( 'Settings', 'Link text for integration plugin settings', 'lifterlms' ) . '</a>';\n\n\t\treturn $links;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.notification.controller.php",
    "content": "<?php\n/**\n * Notification Controller Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.8.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller abstract class\n *\n * @since 3.8.0\n * @since 3.30.3 Explicitly define undefined properties & fixed typo in output string.\n */\nabstract class LLMS_Abstract_Notification_Controller extends LLMS_Abstract_Options_Data implements LLMS_Interface_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id;\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var integer\n\t */\n\tprotected $action_accepted_args = 1;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var array\n\t */\n\tprotected $action_hooks = array();\n\n\t/**\n\t * Priority used when adding action hook\n\t *\n\t * @var integer\n\t */\n\tprotected $action_priority = 15;\n\n\t/**\n\t * If true, will automatically dupcheck before sending\n\t *\n\t * @var boolean\n\t */\n\tprotected $auto_dupcheck = false;\n\n\t/**\n\t * Course associated with the notification\n\t *\n\t * @var LLMS_Course\n\t * @since 3.8.0\n\t */\n\tpublic $course;\n\n\t/**\n\t * WP Post ID associated with the triggering action\n\t *\n\t * @var null\n\t */\n\tprotected $post_id = null;\n\n\t/**\n\t * WP Post ID of the post which triggered the achievement to be awarded\n\t *\n\t * @var int\n\t * @since 3.8.0\n\t */\n\tpublic $related_post_id;\n\n\t/**\n\t * Array of subscriptions for the notification\n\t *\n\t * @var array\n\t */\n\tprotected $subscriptions = array();\n\n\t/**\n\t * Array of supported notification types\n\t *\n\t * @var array\n\t */\n\tprotected $supported_types = array();\n\n\t/**\n\t * Determines if test notifications can be sent\n\t *\n\t * @var bool\n\t */\n\tprotected $testable = array(\n\t\t'basic' => false,\n\t\t'email' => false,\n\t);\n\n\t/**\n\t * WP User ID associated with the triggering action\n\t *\n\t * @var null\n\t */\n\tprotected $user_id = null;\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t *  @since 3.8.0\n\t *\n\t * @param string $subscriber Subscriber type string.\n\t * @return int|false\n\t */\n\tabstract protected function get_subscriber( $subscriber );\n\n\t/**\n\t * Get the translatable title for the notification\n\t *\n\t * Used on settings screens.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tabstract public function get_title();\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type Notification type id.\n\t * @return array\n\t */\n\tabstract protected function set_subscriber_options( $type );\n\n\n\t/**\n\t * Holds singletons for extending classes\n\t *\n\t * @var LLMS_Abstract_Notification_Controller[]\n\t */\n\tprivate static $_instances = array();\n\n\t/**\n\t * Get the singleton instance for the extending class\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return LLMS_Abstract_Notification_Controller\n\t */\n\tpublic static function instance() {\n\n\t\t$class = get_called_class();\n\n\t\tif ( ! isset( self::$_instances[ $class ] ) ) {\n\t\t\tself::$_instances[ $class ] = new $class();\n\t\t}\n\n\t\treturn self::$_instances[ $class ];\n\t}\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t$this->add_actions();\n\t}\n\n\t/**\n\t * Add an action to trigger the notification to send\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tprotected function add_actions() {\n\n\t\tforeach ( $this->action_hooks as $hook ) {\n\t\t\tadd_action( $hook, array( $this, 'action_callback' ), $this->action_accepted_args, $this->action_priority );\n\t\t}\n\t}\n\n\t/**\n\t * Add custom subscriptions\n\t *\n\t * @since Unknown.\n\t *\n\t * @return void\n\t */\n\tprivate function add_custom_subscriptions( $type ) {\n\t\t$option      = $this->get_option( $type . '_custom_subscribers' );\n\t\t$subscribers = explode( ',', $option );\n\t\tforeach ( $subscribers as $subscriber ) {\n\t\t\t$subscriber = trim( $subscriber );\n\t\t\tif ( $subscriber ) {\n\t\t\t\t$this->subscribe( $subscriber, $type );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Adds subscribers before sending a notifications\n\t *\n\t * @since 3.8.0\n\t * @since 5.2.0 Added parameter to only send notifications of specific types.\n\t *\n\t * @param null|string[] $filter_types Optional. Array of notification types to be sent. Default is `null`.\n\t *                                    When not provided (`null`) all the types.\n\t * @return void\n\t */\n\tprivate function add_subscriptions( $filter_types = null ) {\n\n\t\tforeach ( array_keys( $this->get_supported_types() ) as $type ) {\n\t\t\tif ( ! is_null( $filter_types ) && ! in_array( $type, $filter_types, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tforeach ( $this->get_subscribers_settings( $type ) as $subscriber_key => $enabled ) {\n\n\t\t\t\tif ( 'no' === $enabled ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t} elseif ( 'custom' === $subscriber_key ) {\n\t\t\t\t\t$this->add_custom_subscriptions( $type );\n\t\t\t\t}\n\n\t\t\t\t$subscriber = $this->get_subscriber( $subscriber_key );\n\n\t\t\t\tif ( $subscriber ) {\n\n\t\t\t\t\t$this->subscribe( $subscriber, $type );\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get a fake instance of a view, used for managing options & customization on the admin panel\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type       Optional. Notification type. Default is 'basic'.\n\t * @param int    $subscriber Optional. Subscriber id. When not provided the current user id will be used.\n\t * @param int    $user_id    Optional. User id. When not provided the current user id will be used.\n\t * @param int    $post_id    Optional. Post id. When not provided the post_id (`$this->post_id`) associated with the notification will be used.\n\t * @return LLMS_Abstract_Notification_View|false\n\t */\n\tpublic function get_mock_view( $type = 'basic', $subscriber = null, $user_id = null, $post_id = null ) {\n\n\t\t$notification = new LLMS_Notification();\n\t\t$notification->set( 'type', $type );\n\t\t$notification->set( 'subscriber', $subscriber ? $subscriber : get_current_user_id() );\n\t\t$notification->set( 'user_id', $user_id ? $user_id : get_current_user_id() );\n\t\t$notification->set( 'post_id', $post_id );\n\t\t$notification->set( 'trigger_id', $this->id );\n\n\t\treturn llms()->notifications()->get_view( $notification );\n\t}\n\n\t/**\n\t * Retrieve a prefix for options related to the notification\n\t *\n\t * This overrides the LLMS_Abstract_Options_Data method.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_option_prefix() {\n\t\treturn sprintf( '%1$snotification_%2$s_', $this->option_prefix, $this->id );\n\t}\n\n\t/**\n\t * Retrieve get an array of subscriber options for the current notification by type\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @return array\n\t */\n\tpublic function get_subscriber_options( $type ) {\n\t\t/**\n\t\t * Filters the subscriber options\n\t\t *\n\t\t * The dynamic portion of this filter, `$this->id`, refers to the notification trigger identifier.\n\t\t *\n\t\t * @param array                                 An array of subscriber options for this notification.\n\t\t * @param string                                The notification type [basic|email].\n\t\t * @param LLMS_Abstract_Notification_Controller The notification controller instance.\n\t\t */\n\t\treturn apply_filters( \"llms_notification_{$this->id}_subscriber_options\", $this->set_subscriber_options( $type ), $type, $this );\n\t}\n\n\t/**\n\t * Get an array of saved subscriber settings prefilled with defaults for the current notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @return array\n\t */\n\tpublic function get_subscribers_settings( $type ) {\n\t\t$defaults = wp_list_pluck( $this->get_subscriber_options( $type ), 'enabled', 'id' );\n\t\treturn $this->get_option( $type . '_subscribers', $defaults );\n\t}\n\n\t/**\n\t * Get an array of prebuilt subscriber option settings for common subscriptions\n\t *\n\t * @since 3.8.0\n\t * @since 3.30.3 Fixed typo in default description string.\n\t *\n\t * @param string $id      Id of the subscriber type.\n\t * @param string $enabled Optional. Whether or not the subscription should be enabled by default [yes|no]. Default is 'yes'.\n\t * @return array\n\t */\n\tpublic function get_subscriber_option_array( $id, $enabled = 'yes' ) {\n\n\t\t$defaults = array(\n\t\t\t'author'        => array(\n\t\t\t\t'title' => __( 'Author', 'lifterlms' ),\n\t\t\t),\n\t\t\t'student'       => array(\n\t\t\t\t'title' => __( 'Student', 'lifterlms' ),\n\t\t\t),\n\t\t\t'lesson_author' => array(\n\t\t\t\t'title' => __( 'Lesson Author', 'lifterlms' ),\n\t\t\t),\n\t\t\t'course_author' => array(\n\t\t\t\t'title' => __( 'Course Author', 'lifterlms' ),\n\t\t\t),\n\t\t\t'custom'        => array(\n\t\t\t\t'description' => __( 'Enter additional email addresses which will receive this notification. Separate multiple addresses with commas.', 'lifterlms' ),\n\t\t\t\t'title'       => __( 'Additional Recipients', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\n\t\tif ( isset( $defaults[ $id ] ) ) {\n\t\t\t$arr            = $defaults[ $id ];\n\t\t\t$arr['id']      = $id;\n\t\t\t$arr['enabled'] = $enabled;\n\t\t\treturn $arr;\n\t\t}\n\t}\n\n\t/**\n\t * Get a subscriptions array for a specific subscriber\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param mixed $subscriber WP User ID, email address, etc...\n\t * @return array\n\t */\n\tpublic function get_subscriber_subscriptions( $subscriber ) {\n\t\t$subscriptions = $this->get_subscriptions();\n\t\treturn isset( $subscriptions[ $subscriber ] ) ? $subscriptions[ $subscriber ] : array();\n\t}\n\n\t/**\n\t * Retrieve subscribers\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_subscriptions() {\n\t\treturn $this->subscriptions;\n\t}\n\n\t/**\n\t * Get an array of supported notification types\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_supported_types() {\n\t\t/**\n\t\t * Filters the supported notification types\n\t\t *\n\t\t * The dynamic portion of this filter, `$this->id`, refers to the notification trigger identifier.\n\t\t *\n\t\t * @param string[]                              An array of supported types for the notification.\n\t\t * @param string                                The notification type [basic|email].\n\t\t * @param LLMS_Abstract_Notification_Controller The notification controller instance.\n\t\t */\n\t\treturn apply_filters( \"llms_notification_{$this->id}_supported_types\", $this->set_supported_types(), $this );\n\t}\n\n\t/**\n\t * Get an array of additional options to be added to the notification view in the admin panel\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $type Type of the notification.\n\t * @return array\n\t */\n\tpublic function get_additional_options( $type ) {\n\t\t/**\n\t\t * Filters the notification additional options\n\t\t *\n\t\t * The dynamic portion of this filter, `$this->id`, refers to the notification trigger identifier.\n\t\t *\n\t\t * @param array                                 An array of additional options for the notification.\n\t\t * @param LLMS_Abstract_Notification_Controller The notification controller instance.\n\t\t * @param string                                The notification type [basic|email].\n\t\t */\n\t\treturn apply_filters( \"llms_notification_{$this->id}_additional_options\", $this->set_additional_options( $type ), $this, $type );\n\t}\n\n\t/**\n\t * Get an array of LifterLMS Admin Page settings to send test notifications\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @return array\n\t */\n\tpublic function get_test_settings( $type ) {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Determine if the notification is a potential duplicate.\n\t *\n\t * @since 3.11.0\n\t * @since 6.0.0 Fixed how the protected {@see LLMS_Notifications_Query::$found_results} property is accessed.\n\t * @since 7.1.0 Improve the query performance by fetching only one result.\n\t *\n\t * @param string $type       Notification type id.\n\t * @param mixed  $subscriber WP User ID for the subscriber, email address, phone number, etc...\n\t * @return boolean\n\t */\n\tpublic function has_subscriber_received( $type, $subscriber ) {\n\n\t\t$query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'post_id'       => $this->post_id,\n\t\t\t\t'subscriber'    => $subscriber,\n\t\t\t\t'types'         => $type,\n\t\t\t\t'trigger_id'    => $this->id,\n\t\t\t\t'user_id'       => $this->user_id,\n\t\t\t\t'per_page'      => 1,\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\treturn $query->has_results();\n\t}\n\n\t/**\n\t * Determine if the notification type support tests\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param string $type Notification type [email|basic].\n\t * @return bool\n\t */\n\tpublic function is_testable( $type ) {\n\n\t\tif ( empty( $this->testable[ $type ] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Send all the subscriptions\n\t *\n\t * @since 3.8.0\n\t * @since 3.11.0 Unknown.\n\t * @since 5.2.0 Added parameter to only send notifications of specific types.\n\t *\n\t * @param bool          $force        Optional. If true, will force a send even if duplicates. Default is `false`.\n\t *                                    Only applies to controllers that flag $this->auto_dupcheck to true.\n\t * @param null|string[] $filter_types Optional. Array of notification types to be sent. Default is `null`.\n\t *                                    When not provided (`null`) all the types.\n\t * @return void\n\t */\n\tpublic function send( $force = false, $filter_types = null ) {\n\n\t\t$this->add_subscriptions( $filter_types );\n\n\t\tforeach ( $this->get_subscriptions() as $subscriber => $types ) {\n\t\t\tforeach ( $types as $type ) {\n\t\t\t\t/**\n\t\t\t\t * Filter to determine if a notification should be sent to a subscriber.\n\t\t\t\t *\n\t\t\t\t * @since 9.0.0\n\t\t\t\t */\n\t\t\t\tif ( ! apply_filters( 'llms_send_notification_to_subscriber', true, $this, $subscriber, $type, $this->post_id ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$this->send_one( $type, $subscriber, $force );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Cleanup subscriptions so if the notification\n\t\t * is triggered again we don't have incorrect subscribers\n\t\t * on the next trigger.\n\t\t * This happens when receipts are triggered in bulk by action scheduler.\n\t\t */\n\t\t$this->unset_subscriptions();\n\t}\n\n\t/**\n\t * Send a notification for a subscriber\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $type       Notification type id.\n\t * @param mixed  $subscriber WP User ID for the subscriber, email address, phone number, etc...\n\t * @param bool   $force      Optional. If true, will force a send even if duplicates. Default is `false`.\n\t *                           Only applies to controllers that flag $this->auto_dupcheck to true.\n\t * @return int|false\n\t */\n\tprotected function send_one( $type, $subscriber, $force = false ) {\n\n\t\t/**\n\t\t * If autodupcheck is set\n\t\t * and the send function doesn't override the dupcheck\n\t\t * and the subscriber has already received the notification\n\t\t * skip it.\n\t\t */\n\t\tif ( $this->auto_dupcheck && ! $force && $this->has_subscriber_received( $type, $subscriber ) ) {\n\t\t\t// phpcs:ignore -- commented out code\n\t\t\t// llms_log( sprintf( 'Skipped %1$s to subscriber \"%2$s\" bc of dupcheck', $type, $subscriber ), 'notifications' );\n\t\t\treturn false;\n\t\t}\n\n\t\t$notification = new LLMS_Notification();\n\t\t$id           = $notification->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $this->post_id,\n\t\t\t\t'subscriber' => $subscriber,\n\t\t\t\t'type'       => $type,\n\t\t\t\t'trigger_id' => $this->id,\n\t\t\t\t'user_id'    => $this->user_id,\n\t\t\t)\n\t\t);\n\n\t\t// If successful, push to the processor where processing is supported.\n\t\tif ( $id ) {\n\n\t\t\t$processor = llms()->notifications()->get_processor( $type );\n\t\t\tif ( $processor ) {\n\n\t\t\t\t$processor->log( sprintf( 'Queuing %1$s notification ID #%2$d', $type, $id ) );\n\t\t\t\t$processor->push_to_queue( $id );\n\t\t\t\tllms()->notifications()->schedule_processing( $type );\n\n\t\t\t}\n\t\t}\n\n\t\treturn $id;\n\t}\n\n\t/**\n\t * Send a test notification to the currently logged in users\n\t *\n\t * Extending classes should redefine this in order to properly setup the controller with post_id and user_id data.\n\n\t * @since 3.24.0\n\t *\n\t * @param string $type Notification type [basic|email]\n\t * @param array  $data Optional. Array of test notification data as specified by $this->get_test_data(). Defalt is empty array.\n\t * @return int|false\n\t */\n\tpublic function send_test( $type, $data = array() ) {\n\t\treturn $this->send_one( $type, get_current_user_id(), true );\n\t}\n\n\t/**\n\t * Determine what types are supported\n\t *\n\t * @since 3.8.0\n\t *\n\t * Extending classes can override this function in order to add or remove support.\n\t * 3rd parties should add support via filter on $this->get_supported_types().\n\t *\n\t * @return array Associative array, keys are the ID/db type, values should be translated display types.\n\t */\n\tprotected function set_supported_types() {\n\t\treturn array(\n\t\t\t'basic' => __( 'Popup', 'lifterlms' ),\n\t\t\t'email' => __( 'Email', 'lifterlms' ),\n\t\t);\n\t}\n\n\n\t/**\n\t * Set additional options to be added to the notification view in the admin panel\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $type Type of the notification.\n\t * @return array\n\t */\n\tprotected function set_additional_options( $type ) {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Subscribe a user to a notification type\n\t *\n\t * @since 3.8.0\n\t * @since 5.2.0 Use strict type comparison.\n\t *\n\t * @param mixed  $subscriber WP User ID, email address, etc...\n\t * @param string $type       Identifier for a subscription type eg: basic.\n\t * @return void\n\t */\n\tpublic function subscribe( $subscriber, $type ) {\n\n\t\t// Prevent unsupported types from being subscribed.\n\t\tif ( ! $this->supports( $type ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$subscriptions = $this->get_subscriber_subscriptions( $subscriber );\n\n\t\tif ( ! in_array( $type, $subscriptions, true ) ) {\n\t\t\tarray_push( $subscriptions, $type );\n\t\t}\n\n\t\t$this->subscriptions[ $subscriber ] = $subscriptions;\n\t}\n\n\t/**\n\t * Determine if a given notification type is supported\n\t *\n\t * @since 3.8.0\n\t * @since 5.2.0 Use strict type comparison.\n\t *\n\t * @param string $type Notification type id.\n\t * @return boolean\n\t */\n\tpublic function supports( $type ) {\n\t\treturn in_array( $type, array_keys( $this->get_supported_types() ), true );\n\t}\n\n\t/**\n\t * Reset the subscriptions array\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function unset_subscriptions() {\n\t\t$this->subscriptions = array();\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.notification.processor.php",
    "content": "<?php\n/**\n * LifterLMS Notification Background Processor Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.8.0\n * @version 6.10.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Notification Background Processor abstract class\n *\n * @since 3.8.0\n * @since 3.38.0 Modified return of `dispatch()` override to return the return value of the parent method.\n */\nabstract class LLMS_Abstract_Notification_Processor extends WP_Background_Process {\n\n\t/**\n\t * Enables event logging\n\t *\n\t * @var boolean\n\t */\n\tprivate $enable_logging = true;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function __construct() {\n\n\t\tparent::__construct();\n\n\t\tif ( ! defined( 'LLMS_NOTIFICATIONS_LOGGING' ) ) {\n\t\t\tdefine( 'LLMS_NOTIFICATIONS_LOGGING', true );\n\t\t}\n\n\t\t$this->enable_logging = ( defined( 'LLMS_NOTIFICATIONS_LOGGING' ) && LLMS_NOTIFICATIONS_LOGGING );\n\n\t}\n\n\t/**\n\t * Called when queue is emptied and action is complete\n\t *\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function complete() {\n\n\t\t$this->log( sprintf( 'Processing for %s finished', $this->action ) );\n\t\tparent::complete();\n\n\t}\n\n\t/**\n\t * Starts the queue.\n\t *\n\t * @since 3.8.0\n\t * @since 3.38.0 Added return from parent method.\n\t * @since 6.10.1 Fixed malformed sprintf when logging dispatch errors.\n\t *\n\t * @return array|WP_Error Response from `wp_remote_post()`.\n\t */\n\tpublic function dispatch() {\n\n\t\t$this->log(\n\t\t\tsprintf(\n\t\t\t\t'Dispatching %s',\n\t\t\t\t$this->action\n\t\t\t)\n\t\t);\n\n\t\t$dispatched = parent::dispatch();\n\n\t\tif ( is_wp_error( $dispatched ) ) {\n\t\t\t$this->log(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Unable to dispatch %1$s: %2$s',\n\t\t\t\t\t$this->action,\n\t\t\t\t\t$dispatched->get_error_message()\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn $dispatched;\n\n\t}\n\n\t/**\n\t * Handle cron healthcheck\n\t *\n\t * Restart the background process if not already running\n\t * and data exists in the queue.\n\t *\n\t * Overridden to enable the \"force\" option to work, replaces \"exit\" with \"return\"\n\t * so that we can redirect and manually call the cronjob\n\t *\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function handle_cron_healthcheck() {\n\t\tif ( $this->is_process_running() ) {\n\t\t\t// Background process already running.\n\t\t\treturn;\n\t\t}\n\t\tif ( $this->is_queue_empty() ) {\n\t\t\t// No data to process.\n\t\t\t$this->clear_scheduled_event();\n\t\t\treturn;\n\t\t}\n\t\t$this->handle();\n\t}\n\n\t/**\n\t * Returns true if the processor is running\n\t *\n\t * @return   boolean\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function is_processing() {\n\t\treturn ( false === $this->is_queue_empty() );\n\t}\n\n\t/**\n\t * Log event data to an update file when logging enabled\n\t *\n\t * @param    mixed $data  data to log\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function log( $data ) {\n\n\t\tif ( $this->enable_logging ) {\n\t\t\tllms_log( $data, 'notifications' );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.notification.view.php",
    "content": "<?php\n/**\n * Notification View Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.8.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Abstract_Notification_View class.\n *\n * @since 3.8.0\n * @since 3.30.3 Explicitly define undefined properties.\n * @since 3.31.0 Add filter on `$basic_options` class property.\n * @since 3.37.19 Introduced the method `get_object()`. It'll allow extending classes\n *                 defining the way the object associated to the notification should be retrieved.\n *                 Use `in_array` with strict comparison where possible.\n */\nabstract class LLMS_Abstract_Notification_View extends LLMS_Abstract_Options_Data {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it.\n\t\t */\n\t\t'auto_dismiss' => 0,\n\t\t/**\n\t\t * Enables manual dismissal of notifications.\n\t\t */\n\t\t'dismissible'  => false,\n\t);\n\n\t/**\n\t * @var string\n\t * @since 3.8.0\n\t */\n\tpublic $id;\n\n\t/**\n\t * Instance of the LLMS_Post_Model for the triggering post\n\t *\n\t * @var LLMS_Post_Model\n\t */\n\tprotected $post;\n\n\t/**\n\t * Supported fields for notification types\n\t *\n\t * @var array\n\t */\n\tprotected $supported_fields = array();\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var int\n\t */\n\tpublic $trigger_id;\n\n\t/**\n\t * Instance of the current LLMS_Notification\n\t *\n\t * @var LLMS_Notification\n\t */\n\tprotected $notification;\n\n\t/**\n\t * Instance of LLMS_Student for the subscriber\n\t *\n\t * @var LLMS_Student\n\t */\n\tprotected $subscriber;\n\n\t/**\n\t * Instance of an LLMS_Student for the triggering user\n\t *\n\t * @var LLMS_Student\n\t */\n\tprotected $user;\n\n\t/**\n\t * Merge codes.\n\t *\n\t * @var string[]\n\t */\n\tprotected $merge_codes;\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tabstract protected function set_merge_data( $code );\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function set_body();\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function set_footer();\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function set_icon();\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tabstract protected function set_merge_codes();\n\n\t/**\n\t * Setup notification subject line for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function set_subject();\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * On an email the title acts as the \"heading\" element.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tabstract protected function set_title();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.8.0\n\t * @since 3.31.0 Add filter on `$basic_options` class class property.\n\t * @since 3.37.19 Moved the retrieval of the associated llms post into a protected method.\n\t * @since 5.0.0 Force [llms-user] shortocde to the user ID of the user who triggered the notification.\n\t *\n\t * @param mixed $notification Notification id, instance of LLMS_Notification\n\t *                            or an object containing at least an 'id'.\n\t * @return void\n\t */\n\tpublic function __construct( $notification ) {\n\n\t\tif ( is_numeric( $notification ) ) {\n\t\t\t$this->id           = $notification;\n\t\t\t$this->notification = new LLMS_Notification( $notification );\n\t\t} elseif ( is_a( $notification, 'LLMS_Notification' ) ) {\n\t\t\t$this->id           = $notification->get( 'id' );\n\t\t\t$this->notification = $notification;\n\t\t} elseif ( is_object( $notification ) && isset( $notification->id ) ) {\n\t\t\t$this->id           = $notification->id;\n\t\t\t$this->notification = new LLMS_Notification( $notification->id );\n\t\t}\n\n\t\t$this->subscriber = new LLMS_Student( $this->notification->get( 'subscriber' ) );\n\t\t$this->user       = new LLMS_Student( $this->notification->get( 'user_id' ) );\n\t\t$this->post       = $this->get_object();\n\n\t\t$this->basic_options = apply_filters( $this->get_filter( 'basic_options' ), $this->basic_options, $this );\n\n\t\tadd_filter( 'llms_user_info_shortcode_user_id', array( $this, 'set_shortcode_user' ) );\n\t}\n\n\t/**\n\t * Destructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __destruct() {\n\t\tremove_filter( 'llms_user_info_shortcode_user_id', array( $this, 'set_shortcode_user' ) );\n\t}\n\n\t/**\n\t * Set the user ID used by [llms-user] to the user triggering the notification.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param int $uid WP_User ID of the current user.\n\t * @return int\n\t */\n\tpublic function set_shortcode_user( $uid ) {\n\t\treturn $this->user->get( 'id' );\n\t}\n\n\t/**\n\t * Get the object associated to the notification\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return object\n\t */\n\tprotected function get_object() {\n\t\treturn llms_get_post( $this->notification->get( 'post_id' ), 'post' );\n\t}\n\n\t/**\n\t * Get the html for a basic notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprivate function get_basic_html() {\n\n\t\t// Setup html classes.\n\t\t$classes = array(\n\t\t\t'llms-notification',\n\t\t);\n\n\t\t// Setup html attributes.\n\t\t$attributes = array(\n\t\t\t'id'      => $this->id,\n\t\t\t'trigger' => $this->trigger_id,\n\t\t\t'type'    => $this->notification->get( 'type' ),\n\t\t);\n\n\t\tif ( $this->basic_options['dismissible'] ) {\n\t\t\t$classes[] = 'is-dismissible';\n\t\t}\n\t\tif ( $this->basic_options['auto_dismiss'] ) {\n\t\t\t$classes[]                  = 'auto-dismiss';\n\t\t\t$attributes['auto-dismiss'] = $this->basic_options['auto_dismiss'];\n\t\t}\n\n\t\t$atts = '';\n\t\tforeach ( $attributes as $att => $val ) {\n\t\t\t$atts .= sprintf( ' data-%1$s=\"%2$s\"', $att, $val );\n\t\t}\n\n\t\t// Get variables.\n\t\t$title  = $this->get_title();\n\t\t$icon   = ( 'yes' === $this->get_option( 'icon_hide', 'no' ) ) ? '' : $this->get_icon_src();\n\t\t$body   = $this->get_body();\n\t\t$footer = $this->get_footer();\n\n\t\tob_start();\n\t\tllms_get_template(\n\t\t\t'notifications/basic.php',\n\t\t\tarray(\n\t\t\t\t'atts'        => $atts,\n\t\t\t\t'attributes'  => $attributes,\n\t\t\t\t'body'        => $body,\n\t\t\t\t'classes'     => implode( ' ', $classes ),\n\t\t\t\t'date'        => $this->get_date_display( 5 ),\n\t\t\t\t'dismissible' => $this->basic_options['dismissible'],\n\t\t\t\t'footer'      => $footer,\n\t\t\t\t'icon'        => $icon,\n\t\t\t\t'id'          => $this->id,\n\t\t\t\t'status'      => $this->notification->get( 'status' ),\n\t\t\t\t'title'       => $title,\n\t\t\t)\n\t\t);\n\t\t$html = trim( preg_replace( '/\\s+/S', ' ', ob_get_clean() ) );\n\n\t\treturn apply_filters( $this->get_filter( 'get_basic_html' ), $html, $this );\n\t}\n\n\t/**\n\t * Retrieve the body for the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_body( $merge = true ) {\n\t\t$body = $this->get_option( 'body', apply_filters( $this->get_filter( 'set_body' ), $this->set_body(), $this ) );\n\t\tif ( $merge ) {\n\t\t\t$body = $this->get_merged_string( $body );\n\t\t}\n\t\treturn apply_filters( $this->get_filter( 'get_body' ), wpautop( $body ), $this );\n\t}\n\n\t/**\n\t * Retrieve a formatted date\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $date   Created or updated.\n\t * @param string $format Valid PHP date format, defaults to WP date format options.\n\t * @return string\n\t */\n\tpublic function get_date( $date = 'created', $format = null ) {\n\n\t\tif ( ! $format ) {\n\t\t\t$format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );\n\t\t}\n\n\t\treturn date_i18n( $format, strtotime( $this->notification->get( $date ) ) );\n\t}\n\n\t/**\n\t * Get relative or absolute date\n\t *\n\t * Returns relative if relative date is less than $max_days\n\t * otherwise returns the absolute date.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param int $max_days Max age of notification to display relative date for.\n\t * @return string\n\t */\n\tpublic function get_date_display( $max_days = 5 ) {\n\n\t\t$now     = current_time( 'timestamp' );\n\t\t$created = $this->get_date( 'created', 'U' );\n\n\t\tif ( ( $now - $created ) <= ( $max_days * DAY_IN_SECONDS ) ) {\n\n\t\t\t/* translators: %s: Relative date display. */\n\t\t\treturn sprintf( _x( 'About %s ago', 'relative date display', 'lifterlms' ), $this->get_date_relative( 'created' ) );\n\n\t\t}\n\n\t\treturn $this->get_date( 'created' );\n\t}\n\n\t/**\n\t * Retrieve a date relative to the current time\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $date Created or updated.\n\t * @return string\n\t */\n\tpublic function get_date_relative( $date = 'created' ) {\n\t\treturn llms_get_date_diff( current_time( 'timestamp' ), $this->get_date( $date, 'U' ), 1 );\n\t}\n\n\t/**\n\t * Get the html for an email notification\n\t *\n\t * @since 3.28.2 Unknown.\n\t *\n\t * @return string\n\t * @since 3.8.0\n\t */\n\tprivate function get_email_html() {\n\t\treturn apply_filters( $this->get_filter( 'get_email_html' ), $this->get_body(), $this );\n\t}\n\n\t/**\n\t * Get a filter hook string prefixed for the current view\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $hook Hook name.\n\t * @return string\n\t */\n\tprotected function get_filter( $hook ) {\n\t\treturn 'llms_notification_view' . $this->trigger_id . '_' . $hook;\n\t}\n\n\t/**\n\t * Get an array of field-related options to be add to the notifications view config page on the admin panel\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type Type of the field.\n\t * @return array\n\t */\n\tpublic function get_field_options( $type ) {\n\n\t\t$options = array();\n\n\t\tif ( $this->has_field_support( $type, 'subject' ) ) {\n\t\t\t$options[] = array(\n\t\t\t\t'after_html' => llms_merge_code_button( '#' . $this->get_option_name( 'subject' ), false, $this->get_merge_codes() ),\n\t\t\t\t'id'         => $this->get_option_name( 'subject' ),\n\t\t\t\t'title'      => __( 'Subject', 'lifterlms' ),\n\t\t\t\t'type'       => 'text',\n\t\t\t\t'value'      => $this->get_subject( false ),\n\t\t\t);\n\t\t}\n\n\t\tif ( $this->has_field_support( $type, 'title' ) ) {\n\t\t\t$options[] = array(\n\t\t\t\t'after_html' => llms_merge_code_button( '#' . $this->get_option_name( 'title' ), false, $this->get_merge_codes() ),\n\t\t\t\t'id'         => $this->get_option_name( 'title' ),\n\t\t\t\t'title'      => ( 'email' === $type ) ? __( 'Heading', 'lifterlms' ) : __( 'Title', 'lifterlms' ),\n\t\t\t\t'type'       => 'text',\n\t\t\t\t'value'      => $this->get_title( false ),\n\t\t\t);\n\t\t}\n\n\t\tif ( $this->has_field_support( $type, 'body' ) ) {\n\t\t\t$options[] = array(\n\t\t\t\t'editor_settings' => array(\n\t\t\t\t\t'teeny' => true,\n\t\t\t\t),\n\t\t\t\t'id'              => $this->get_option_name( 'body' ),\n\t\t\t\t'title'           => __( 'Body', 'lifterlms' ),\n\t\t\t\t'type'            => 'wpeditor',\n\t\t\t\t'value'           => $this->get_body( false ),\n\t\t\t);\n\t\t}\n\n\t\tif ( $this->has_field_support( $type, 'icon' ) ) {\n\t\t\t$options[] = array(\n\t\t\t\t'id'         => $this->get_option_name( 'icon' ),\n\t\t\t\t'image_size' => 'llms_notification_icon',\n\t\t\t\t'title'      => __( 'Icon', 'lifterlms' ),\n\t\t\t\t'type'       => 'image',\n\t\t\t\t'value'      => $this->get_icon(),\n\t\t\t);\n\t\t\t$options[] = array(\n\t\t\t\t'default'     => 'no',\n\t\t\t\t'description' => __( 'When checked the icon will not be displayed when showing this notification.', 'lifterlms' ),\n\t\t\t\t'id'          => $this->get_option_name( 'icon_hide' ),\n\t\t\t\t'title'       => __( 'Disable Icon', 'lifterlms' ),\n\t\t\t\t'type'        => 'checkbox',\n\t\t\t);\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'get_field_options' ), $options, $this );\n\t}\n\n\t/**\n\t * Retrieve the footer for the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_footer() {\n\t\treturn apply_filters( $this->get_filter( 'get_footer' ), $this->set_footer(), $this );\n\t}\n\n\t/**\n\t * Retrieve the full HTML to be output for the notification type\n\t *\n\t * @since 3.8.0\n\t * @since 4.16.0 Pass `null` to the 3rd-party filter.\n\t *\n\t * @return string|WP_Error If the notification type is not supported, returns an error.\n\t */\n\tpublic function get_html() {\n\n\t\t$type = $this->notification->get( 'type' );\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'email':\n\t\t\t\t$html = $this->get_email_html();\n\t\t\t\tbreak;\n\n\t\t\tcase 'basic':\n\t\t\t\t$html = $this->get_basic_html();\n\t\t\t\tbreak;\n\n\t\t\t// 3rd party/custom types.\n\t\t\tdefault:\n\t\t\t\t$html = apply_filters( $this->get_filter( 'get_' . $type . '_html' ), null, $this );\n\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'get_html' ), $html, $this );\n\t}\n\n\t/**\n\t * Retrieve the icon id for the notification\n\t *\n\t * Returns an attachment id for the image.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return mixed\n\t */\n\tpublic function get_icon() {\n\t\t$icon = $this->get_option( 'icon', apply_filters( $this->get_filter( 'set_icon' ), $this->set_icon(), $this ) );\n\t\treturn apply_filters( $this->get_filter( 'get_icon' ), $icon, $this );\n\t}\n\n\t/**\n\t * Retrieve a default icon for the notification based on the notification type\n\t *\n\t * @since 3.8.0\n\t * @since 3.10.0 Unknown.\n\t * @since 3.37.19 Use `in_array` with strict comparison.\n\t *\n\t * @param string $type Type of icon [positive|negative].\n\t * @return string\n\t */\n\tpublic function get_icon_default( $type ) {\n\t\tif ( ! in_array( $type, array( 'negative', 'positive', 'warning' ), true ) ) {\n\t\t\t$ret = '';\n\t\t} else {\n\t\t\t$ret = llms()->plugin_url() . '/assets/images/notifications/icon-' . $type . '.png';\n\t\t}\n\t\treturn apply_filters( 'llms_notification_get_icon_default', $ret, $type, $this );\n\t}\n\n\t/**\n\t * Retrieve the icon src for the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_icon_src() {\n\t\t$src = '';\n\t\t$val = $this->get_icon();\n\t\tif ( is_numeric( $val ) ) {\n\t\t\t$src = wp_get_attachment_image_src( $val, 'llms_notification_icon' );\n\t\t\tif ( is_array( $src ) ) {\n\t\t\t\t$src = $src[0];\n\t\t\t}\n\t\t} else {\n\t\t\t$src = $val;\n\t\t}\n\t\treturn apply_filters( $this->get_filter( 'get_icon_src' ), $src, $this );\n\t}\n\n\t/**\n\t * Get available merge codes for the current notification.\n\t *\n\t * @since 3.8.0\n\t * @since 3.11.0 Unknown.\n\t * @since 6.4.0 Cache merge codes.\n\t *\n\t * @return array\n\t */\n\tpublic function get_merge_codes() {\n\n\t\tif ( ! isset( $this->merge_codes ) ) {\n\t\t\t$codes = array_merge( $this->get_merge_code_defaults(), $this->set_merge_codes() );\n\t\t\tasort( $codes );\n\t\t\t$this->merge_codes = $codes;\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'get_merge_codes' ), $this->merge_codes, $this );\n\t}\n\n\t/**\n\t * Get default merge codes available to all notifications of a given type\n\t *\n\t * @since 3.11.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_merge_code_defaults() {\n\n\t\tswitch ( $this->notification->get( 'type' ) ) {\n\n\t\t\tcase 'email':\n\t\t\t\t$codes = array(\n\t\t\t\t\t'{{DIVIDER}}' => __( 'Divider Line', 'lifterlms' ),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$codes = array();\n\t\t}\n\n\t\treturn $codes;\n\t}\n\n\t/**\n\t * Merge a string.\n\t *\n\t * @since 3.8.0\n\t * @since 3.37.19 Use `in_array` with strict comparison.\n\t * @since 6.4.0 Only populate effectively used merged data.\n\t *\n\t * @param string $string An unmerged string.\n\t * @return string\n\t */\n\tprivate function get_merged_string( $string ) {\n\n\t\t// Only merge if there are codes in the string.\n\t\tif ( false !== strpos( $string, '{{' ) ) {\n\n\t\t\t$merge_code_defaults = $this->get_merge_code_defaults();\n\n\t\t\tforeach ( $this->get_used_merge_codes( $string ) as $code ) {\n\n\t\t\t\t// Set defaults.\n\t\t\t\tif ( array_key_exists( $code, $merge_code_defaults ) ) {\n\n\t\t\t\t\t$func = 'set_merge_data_default';\n\n\t\t\t\t\t// Set customs with extended class func.\n\t\t\t\t} else {\n\n\t\t\t\t\t$func = 'set_merge_data';\n\n\t\t\t\t}\n\n\t\t\t\t$string = str_replace( $code, $this->$func( $code ), $string );\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'get_merged_string' ), $this->sentence_case( $string ), $this );\n\t}\n\n\t/**\n\t * Retrieve merge codes used in a given string.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @param string $string Text string whereto look for merge codes.\n\t * @return array Returns a list of merge codes actually used in the passed string.\n\t */\n\tprivate function get_used_merge_codes( $string ) {\n\n\t\treturn array_keys(\n\t\t\tarray_filter(\n\t\t\t\t$this->get_merge_codes(),\n\t\t\t\tfunction ( $code ) use ( $string ) {\n\t\t\t\t\treturn false !== strpos( $string, $code );\n\t\t\t\t},\n\t\t\t\tARRAY_FILTER_USE_KEY\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Access the protected notification object\n\t *\n\t * @since 3.18.2\n\t *\n\t * @return LLMS_Notification\n\t */\n\tpublic function get_notification() {\n\t\treturn $this->notification;\n\t}\n\n\t/**\n\t * Retrieve a prefix for options related to the notification\n\t *\n\t * This overrides the LLMS_Abstract_Options_Data method.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_option_prefix() {\n\t\treturn sprintf( '%1$snotification_%2$s_%3$s_', $this->option_prefix, $this->trigger_id, $this->notification->get( 'type' ) );\n\t}\n\n\t/**\n\t * Retrieve the subject for the notification (if supported)\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_subject( $merge = true ) {\n\t\t$subject = $this->get_option( 'subject', apply_filters( $this->get_filter( 'set_subject' ), $this->set_subject(), $this ) );\n\t\tif ( $merge ) {\n\t\t\t$subject = $this->get_merged_string( $subject );\n\t\t}\n\t\treturn apply_filters( $this->get_filter( 'get_subject' ), $subject, $this );\n\t}\n\n\t/**\n\t * Get supported fields and allow filtering for 3rd parties\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_supported_fields() {\n\t\treturn apply_filters( $this->get_filter( 'get_supported_fields' ), $this->set_supported_fields(), $this );\n\t}\n\n\t/**\n\t * Retrieve the title for the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_title( $merge = true ) {\n\t\t$title = $this->get_option( 'title', apply_filters( $this->get_filter( 'set_title' ), $this->set_title(), $this ) );\n\t\tif ( $merge ) {\n\t\t\t$title = $this->get_merged_string( $title );\n\t\t}\n\t\treturn apply_filters( $this->get_filter( 'get_title' ), $title, $this );\n\t}\n\n\t/**\n\t * Determine if the current view supports a field by ID\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type  Notification type [email|basic].\n\t * @param string $field Field id.\n\t * @return bool\n\t */\n\tprotected function has_field_support( $type, $field ) {\n\t\t$fields = $this->get_supported_fields();\n\t\tif ( ! isset( $fields[ $type ] ) ) {\n\t\t\treturn false;\n\t\t}\n\t\t$type_fields = $fields[ $type ];\n\t\tif ( ! isset( $type_fields[ $field ] ) ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn $type_fields[ $field ];\n\t}\n\n\t/**\n\t * Determine if the notification subscriber is the user who triggered the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return bool\n\t */\n\tprotected function is_for_self() {\n\t\treturn ( $this->subscriber->get_id() === $this->user->get_id() );\n\t}\n\n\t/**\n\t * Convert a string to sentence case\n\t *\n\t * Useful for handling lowercased merged data like \"you\" which may appear at the beginning or middle of a sentence.\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $string A string.\n\t * @return string\n\t */\n\tprivate function sentence_case( $string ) {\n\n\t\t$sentences  = preg_split( '/(\\.|\\?|\\!)(\\s|$)+/', $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );\n\t\t$new_string = '';\n\t\tforeach ( $sentences as $sentence ) {\n\t\t\t$new_string .= strlen( $sentence ) === 1 ? $sentence . ' ' : ucfirst( trim( $sentence ) );\n\t\t}\n\n\t\treturn trim( $new_string );\n\t}\n\n\t/**\n\t * Replace default merge codes with actual values\n\t *\n\t * @since 3.11.0\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data_default( $code ) {\n\n\t\t$mailer = llms()->mailer();\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{DIVIDER}}':\n\t\t\t\t$code = $mailer->get_divider_html();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Define field support for the view\n\t *\n\t * Extending classes can override this\n\t * 3rd parties should filter $this->get_supported_fields().\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_supported_fields() {\n\t\treturn array(\n\t\t\t'basic' => array(\n\t\t\t\t'body'  => true,\n\t\t\t\t'title' => true,\n\t\t\t\t'icon'  => true,\n\t\t\t),\n\t\t\t'email' => array(\n\t\t\t\t'body'    => true,\n\t\t\t\t'subject' => true,\n\t\t\t\t'title'   => true,\n\t\t\t),\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.notification.view.quiz.completion.php",
    "content": "<?php\n/**\n * Shared Notification View for quiz completions abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.24.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Shared Notification View for quiz completions abstract class\n *\n * @since 3.24.0\n * @since 4.0.0 Remove usage of deprecated class `LLMS_Quiz_Legacy`.\n */\nabstract class LLMS_Abstract_Notification_View_Quiz_Completion extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var  array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Setup body for email notification\n\t *\n\t * @since 3.24.0\n\t * @since 5.2.0 Build the table with mailer helper.\n\t *\n\t * @return string\n\t */\n\tprotected function set_body_email() {\n\n\t\t$mailer = llms()->mailer();\n\n\t\t$btn_style = $mailer->get_button_style();\n\n\t\t$rows = array(\n\t\t\t'STUDENT_NAME' => __( 'Student', 'lifterlms' ),\n\t\t\t'QUIZ_TITLE'   => __( 'Quiz', 'lifterlms' ),\n\t\t\t'LESSON_TITLE' => __( 'Lesson', 'lifterlms' ),\n\t\t\t'COURSE_TITLE' => __( 'Course', 'lifterlms' ),\n\t\t\t'GRADE'        => __( 'Grade', 'lifterlms' ),\n\t\t\t'STATUS'       => __( 'Status', 'lifterlms' ),\n\t\t);\n\n\t\tob_start();\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped by the mailer.\n\t\techo $mailer->get_table_html( $rows );\n\t\t?>\n\t\t<p><a href=\"{{REVIEW_URL}}\" style=\"<?php echo esc_attr( $btn_style ); ?>\"><?php esc_html_e( 'View the quiz attempt and leave remarks', 'lifterlms' ); ?></a></p>\n\t\t<p><small><?php esc_html_e( 'Trouble clicking? Copy and paste this URL into your browser:', 'lifterlms' ); ?><br><a href=\"{{REVIEW_URL}}\">{{REVIEW_URL}}</a></small></p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{COURSE_PROGRESS}}' => __( 'Course Progress Bar', 'lifterlms' ),\n\t\t\t'{{COURSE_TITLE}}'    => __( 'Course Title', 'lifterlms' ),\n\t\t\t'{{GRADE}}'           => __( 'Grade', 'lifterlms' ),\n\t\t\t'{{GRADE_BAR}}'       => __( 'Grade Bar', 'lifterlms' ),\n\t\t\t'{{LESSON_TITLE}}'    => __( 'Lesson Title', 'lifterlms' ),\n\t\t\t'{{QUIZ_TITLE}}'      => __( 'Quiz Title', 'lifterlms' ),\n\t\t\t'{{REVIEW_URL}}'      => __( 'Review URL', 'lifterlms' ),\n\t\t\t'{{STATUS}}'          => __( 'Quiz Status', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}'    => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values.\n\t *\n\t * @since 3.24.0\n\t * @since 4.0.0 Remove usage of deprecated class `LLMS_Quiz_Legacy`.\n\t * @since 7.8.0 Don't try to round nulls.\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$quiz_id = $this->notification->get( 'post_id' );\n\t\t$attempt = $this->user->quizzes()->get_last_completed_attempt( $quiz_id );\n\t\tif ( ! $attempt ) {\n\t\t\treturn '';\n\t\t}\n\t\t$lesson = llms_get_post( $attempt->get( 'lesson_id' ) );\n\t\tif ( ! $lesson ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{COURSE_TITLE}}':\n\t\t\t\t$course = $lesson->get_course();\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$code = $course->get( 'title' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = '';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{GRADE}}':\n\t\t\t\t$code = round( $attempt->get( 'grade' ) ?? 0, 2 ) . '%';\n\t\t\t\tbreak;\n\n\t\t\tcase '{{GRADE_BAR}}':\n\t\t\t\t$code = lifterlms_course_progress_bar( $attempt->get( 'grade' ), false, false, false );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{LESSON_TITLE}}':\n\t\t\t\t$code = $lesson->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{QUIZ_TITLE}}':\n\t\t\t\t$code = get_the_title( $quiz_id );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{REVIEW_URL}}':\n\t\t\t\t$code = add_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'tab'        => 'quizzes',\n\t\t\t\t\t\t'stab'       => 'attempts',\n\t\t\t\t\t\t'quiz_id'    => $attempt->get( 'quiz_id' ),\n\t\t\t\t\t\t'attempt_id' => $attempt->get( 'id' ),\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php?page=llms-reporting' )\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STATUS}}':\n\t\t\t\t$code = $attempt->l10n( 'status' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.options.data.php",
    "content": "<?php\n/**\n * LifterLMS Options Table Data Store abstract class\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.8.0\n * @version 4.21.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Options Table Data Store abstract\n *\n * @since 3.8.0\n */\nabstract class LLMS_Abstract_Options_Data {\n\n\t/**\n\t * Option name prefix.\n\t *\n\t * @var string\n\t */\n\tprotected $option_prefix = 'llms_';\n\n\t/**\n\t * Options data abstract version\n\t *\n\t * This is used to determine the behavior of the `get_option()` method.\n\t *\n\t * Concrete classes should use version 2 in order to use the new (future default)\n\t * behavior of the method.\n\t *\n\t * @var int\n\t */\n\tprotected $version = 1;\n\n\t/**\n\t * Retrieve the value of an option from the database\n\t *\n\t * @since 3.8.0\n\t * @since 4.21.0 Changed the behavior of the function when the concrete class defines `$this->version` greater than 1.\n\t *\n\t * @param string $name     Option name (unprefixed).\n\t * @param mixed  $default  Default value to use if no option is found.\n\t * @return mixed The option value.\n\t */\n\tpublic function get_option( $name, $default = false ) {\n\n\t\t$full_name = $this->get_option_name( $name );\n\n\t\t// If the class is version 1, use the old method.\n\t\tif ( 1 === $this->version ) {\n\t\t\t// If only one argument is passed switch the default to the old argument default (an empty string).\n\t\t\t$default = 1 === func_num_args() ? '' : $default;\n\t\t\treturn $this->get_option_deprecated( $full_name, $default );\n\t\t}\n\n\t\tadd_filter( \"default_option_{$full_name}\", array( $this, 'get_option_default_value' ), 10, 3 );\n\n\t\t// Call this way so that the `$passed_default_value` of the filter is accurate based on the number of arguments actually passed.\n\t\t$args = func_num_args() > 1 ? array( $full_name, $default ) : array( $full_name );\n\t\t$val  = get_option( ...$args );\n\n\t\tremove_filter( \"default_option_{$full_name}\", array( $this, 'get_option_default_value' ), 10, 3 );\n\n\t\treturn $val;\n\n\t}\n\n\t/**\n\t * Retrieve the value of an option from the database\n\t *\n\t * This is the \"old\" (to be deprecated) version of the function.\n\t *\n\t * We will transition extending classes little by little to use the new behavior and deprecate this once\n\t * all classes are fully transitioned.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param string $name     Full (prefixed) option name.\n\t * @param mixed  $default  Default value to use if no option is found.\n\t * @return mixed The option value.\n\t */\n\tprivate function get_option_deprecated( $name, $default = '' ) {\n\t\t$val = get_option( $name, '' );\n\t\tif ( '' === $val ) {\n\t\t\treturn $default;\n\t\t}\n\t\treturn $val;\n\t}\n\n\t/**\n\t * Option default value autoloader\n\t *\n\t * By default, this method does nothing but extending classes can implement an autoloader to pull\n\t * default values from other sources.\n\t *\n\t * This is a callback function for the WP core filter `default_option_{$option}`.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param mixed  $default_value        The default value. If no value is passed to `get_option()`, this will be an empty string.\n\t *                                     Otherwise it will be the default value passed to the method.\n\t * @param string $full_option_name     The full (prefixed) option name.\n\t * @param bool   $passed_default_value Whether or not a default value was passed to `get_option()`.\n\t * @return mixed The default option value.\n\t */\n\tpublic function get_option_default_value( $default_value, $full_option_name, $passed_default_value ) {\n\t\treturn $default_value;\n\t}\n\n\t/**\n\t * Retrieve a prefix for options\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_option_prefix() {\n\t\treturn $this->option_prefix;\n\t}\n\n\t/**\n\t * Retrieve a prefixed option name from the database\n\t * Prefix automatically adds a trigger and type to the option name\n\t * in addition to llms_notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $name Option name (unprefixed).\n\t * @return string\n\t */\n\tpublic function get_option_name( $name ) {\n\t\treturn $this->get_option_prefix() . $name;\n\t}\n\n\t/**\n\t * Set the value of an option\n\t *\n\t * @since 3.17.8\n\t *\n\t * @param string $name  Option name (unprefixed).\n\t * @param mixed  $value Option value.\n\t * @return bool Returns `true` if option value has changed and `false` if no update or the update failed.\n\t */\n\tpublic function set_option( $name, $value ) {\n\t\treturn update_option( $this->get_option_name( $name ), $value );\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.post.data.php",
    "content": "<?php\n/**\n * Defines base methods and properties for querying data about LifterLMS Custom Post Types\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.31.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS Post Data abstract class\n *\n * @since 3.31.0\n */\nabstract class LLMS_Abstract_Post_Data {\n\n\t/**\n\t * LLMS Post instance.\n\t *\n\t * @since 3.31.0\n\t * @var LLMS_Post_Model\n\t */\n\tprotected $post;\n\n\t/**\n\t * LLMS Post ID.\n\t *\n\t * @since 3.31.0\n\t * @var int\n\t */\n\tprotected $post_id;\n\n\t/**\n\t * @since 3.31.0\n\t * @var array\n\t */\n\tprotected $dates = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.31.0\n\t *\n\t * @param int $post_id WP Post ID of the LLMS Post.\n\t */\n\tpublic function __construct( $post_id ) {\n\n\t\t$this->post_id = $post_id;\n\t\t$this->post    = llms_get_post( $this->post_id );\n\n\t}\n\n\t/**\n\t * Retrieve the instance of the LLMS_Post_Model.\n\t *\n\t * @since 3.31.0\n\t *\n\t * @return LLMS_Post_Model The instance of the LLMS_Post_Model.\n\t */\n\tpublic function get_post() {\n\t\treturn $this->post;\n\t}\n\n\t/**\n\t * Retrieve the LLMS_Post_Model ID.\n\t *\n\t * @since 3.31.0\n\t *\n\t * @return int The LLMS_Post_Model ID.\n\t */\n\tpublic function get_post_id() {\n\t\treturn $this->post_id;\n\t}\n\n\t/**\n\t * Allow dates and timestamps to be passed into various data functions.\n\t *\n\t * @since 3.31.0\n\t *\n\t * @param  mixed $date A date string or timestamp.\n\t * @return int The Unix timestamp of the given date.\n\t */\n\tprotected function strtotime( $date ) {\n\t\tif ( ! is_numeric( $date ) ) {\n\t\t\t$date = date( 'U', strtotime( $date ) );\n\t\t}\n\t\treturn $date;\n\t}\n\n\t/**\n\t * Retrieve a start or end date based on the period.\n\t *\n\t * @since 3.31.0\n\t *\n\t * @param  string $period Period [current|previous].\n\t * @param  string $date   The date type [start|end].\n\t * @return string The start or end date in the format 'Y-m-d H:i:s'.\n\t */\n\tprotected function get_date( $period, $date ) {\n\n\t\treturn date( 'Y-m-d H:i:s', $this->dates[ $period ][ $date ] );\n\n\t}\n\n\t/**\n\t * Retrieves the selected period from `$_GET` and validates it.\n\t *\n\t * If there is no period set or it's set to an invalid period, defaults to \"today\".\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return string\n\t */\n\tpublic function parse_period() {\n\t\t$periods = LLMS_Admin_Reporting::get_period_filters();\n\t\t$period  = llms_filter_input( INPUT_GET, 'period' );\n\t\tif ( ! $period || ! array_key_exists( $period, $periods ) ) {\n\t\t\t$period = 'today';\n\t\t}\n\t\treturn $period;\n\t}\n\n\t/**\n\t * Set the dates passed on a date range period\n\t *\n\t * @since 3.31.0\n\t *\n\t * @param  string $period Date range period.\n\t * @return void\n\t */\n\tpublic function set_period( $period = 'today' ) {\n\n\t\t$now = current_time( 'timestamp' );\n\n\t\tswitch ( $period ) {\n\n\t\t\tcase 'all_time':\n\t\t\t\t$curr_start = 0;\n\t\t\t\t$curr_end   = $now;\n\n\t\t\t\t$prev_start = 0;\n\t\t\t\t$prev_end   = $now;\n\t\t\t\tbreak;\n\n\t\t\tcase 'last_year':\n\t\t\t\t$curr_start = strtotime( 'first day of january last year', $now );\n\t\t\t\t$curr_end   = strtotime( 'last day of december last year', $now );\n\n\t\t\t\t$prev_start = strtotime( 'first day of january last year', $curr_start );\n\t\t\t\t$prev_end   = strtotime( 'last day of december last year', $curr_start );\n\t\t\t\tbreak;\n\n\t\t\tcase 'year':\n\t\t\t\t$curr_start = strtotime( 'first day of january this year', $now );\n\t\t\t\t$curr_end   = strtotime( 'last day of december this year', $now );\n\n\t\t\t\t$prev_start = strtotime( 'first day of january last year', $now );\n\t\t\t\t$prev_end   = strtotime( 'last day of december last year', $now );\n\t\t\t\tbreak;\n\n\t\t\tcase 'last_month':\n\t\t\t\t$curr_start = strtotime( 'first day of previous month', $now );\n\t\t\t\t$curr_end   = strtotime( 'last day of previous month', $now );\n\n\t\t\t\t$prev_start = strtotime( 'first day of previous month', $curr_start );\n\t\t\t\t$prev_end   = strtotime( 'last day of previous month', $curr_start );\n\t\t\t\tbreak;\n\n\t\t\tcase 'month':\n\t\t\t\t$curr_start = strtotime( 'first day of this month', $now );\n\t\t\t\t$curr_end   = strtotime( 'last day of this month', $now );\n\n\t\t\t\t$prev_start = strtotime( 'first day of previous month', $now );\n\t\t\t\t$prev_end   = strtotime( 'last day of previous month', $now );\n\t\t\t\tbreak;\n\n\t\t\tcase 'last_week':\n\t\t\t\t$curr_start = strtotime( 'monday this week', $now - WEEK_IN_SECONDS );\n\t\t\t\t$curr_end   = $now;\n\n\t\t\t\t$prev_start = strtotime( 'monday previous week', $curr_start - WEEK_IN_SECONDS );\n\t\t\t\t$prev_end   = $curr_start - DAY_IN_SECONDS;\n\t\t\t\tbreak;\n\n\t\t\tcase 'week':\n\t\t\t\t$curr_start = strtotime( 'monday this week', $now );\n\t\t\t\t$curr_end   = $now;\n\n\t\t\t\t$prev_start = strtotime( 'monday previous week', $now );\n\t\t\t\t$prev_end   = $curr_start - DAY_IN_SECONDS;\n\t\t\t\tbreak;\n\n\t\t\tcase 'yesterday':\n\t\t\t\t$curr_start = $now - DAY_IN_SECONDS;\n\t\t\t\t$curr_end   = $curr_start;\n\n\t\t\t\t$prev_start = $curr_start - DAY_IN_SECONDS;\n\t\t\t\t$prev_end   = $prev_start;\n\t\t\t\tbreak;\n\n\t\t\tcase 'today':\n\t\t\tdefault:\n\t\t\t\t$curr_start = $now;\n\t\t\t\t$curr_end   = $now;\n\n\t\t\t\t$prev_start = $now - DAY_IN_SECONDS;\n\t\t\t\t$prev_end   = $prev_start;\n\n\t\t}// End switch().\n\n\t\t$this->dates = array(\n\t\t\t'current'  => array(\n\t\t\t\t'start' => strtotime( 'midnight', $curr_start ),\n\t\t\t\t'end'   => strtotime( 'tomorrow', $curr_end ) - 1,\n\t\t\t),\n\t\t\t'previous' => array(\n\t\t\t\t'start' => strtotime( 'midnight', $prev_start ),\n\t\t\t\t'end'   => strtotime( 'tomorrow', $prev_end ) - 1,\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve recent LLMS_User_Postmeta for the quiz\n\t *\n\t * @since 3.31.0\n\t *\n\t * @param array $args {\n\t *     Optional. An array of arguments to feed the LLMS_Query_User_Postmeta with.\n\t *\n\t *     @type int          $per_page The number of posts to query for. Default 10.\n\t *     @type array|string $types    Array of strings for the type of events to fetch, or a string to fetch them all. Default 'all'.\n\t *                                  @see LLMS_Query_User_Postmeta::parse_args()\n\t * }\n\t * @return array Array of LLMS_User_Postmetas.\n\t */\n\tpublic function recent_events( $args = array() ) {\n\n\t\t$query_args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'per_page' => 10,\n\t\t\t\t'types'    => 'all',\n\t\t\t)\n\t\t);\n\n\t\t$query_args['post_id']        = $this->post_id;\n\t\t$query_args['no_found_rows'] = true;\n\n\t\t$query = new LLMS_Query_User_Postmeta( $query_args );\n\n\t\treturn $query->get_metas();\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.privacy.php",
    "content": "<?php\n/**\n * LifterLMS Privacy Export / Eraser Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.18.0\n * @version 3.18.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Privacy Export / Eraser abstract class.\n *\n * Thanks WooCommerce.\n *\n * @since 3.18.0\n */\nabstract class LLMS_Abstract_Privacy {\n\n\t/**\n\t * Plugin name.\n\t *\n\t * @var string\n\t */\n\tpublic $name;\n\n\t/**\n\t * Registered erasers.\n\t *\n\t * @var array\n\t */\n\tprotected $erasers = array();\n\n\t/**\n\t * Registered exporters.\n\t *\n\t * @var array\n\t */\n\tprotected $exporters = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param string $name Plugin name.\n\t * @return void\n\t */\n\tpublic function __construct( $name = '' ) {\n\n\t\t$this->name = $name;\n\t\t$this->add_hooks();\n\t}\n\n\t/**\n\t * Add filters for the registered exporters & erasers.\n\t *\n\t * @since 3.18.0\n\t *\n\t * @return void\n\t */\n\tprotected function add_hooks() {\n\n\t\tadd_action( 'admin_init', array( $this, 'add_privacy_message' ) );\n\n\t\tadd_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_erasers' ) );\n\t\tadd_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporters' ), 5 );\n\t}\n\n\t/**\n\t * Add privacy message sample content.\n\t *\n\t * @since 3.18.0\n\t *\n\t * @return void\n\t */\n\tpublic function add_privacy_message() {\n\n\t\tif ( function_exists( 'wp_add_privacy_policy_content' ) ) {\n\t\t\t$content = $this->get_privacy_message();\n\t\t\tif ( $content ) {\n\t\t\t\twp_add_privacy_policy_content( $this->name, $this->get_privacy_message() );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the privacy message sample content.\n\t *\n\t * This stub can be overloaded.\n\t *\n\t * @since 3.18.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_privacy_message() {\n\n\t\treturn '';\n\t}\n\n\t/**\n\t * Retrieve an instance of an LLMS_Student from email address.\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param string $email Email address.\n\t * @return false|LLMS_Student\n\t */\n\tprotected static function get_student_by_email( $email ) {\n\n\t\t$user = get_user_by( 'email', $email );\n\t\tif ( is_a( $user, 'WP_User' ) ) {\n\t\t\treturn llms_get_student( $user );\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Add all registered erasers to the array of existing erasers.\n\t *\n\t * @filter wp_privacy_personal_data_erasers\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param array $erasers Existing erasers.\n\t * @return array\n\t */\n\tpublic function register_erasers( $erasers = array() ) {\n\n\t\tforeach ( $this->erasers as $id => $eraser ) {\n\t\t\t$erasers[ $id ] = $eraser;\n\t\t}\n\t\treturn $erasers;\n\t}\n\n\t/**\n\t * Add all registered erasers to the array of existing exporters.\n\t *\n\t * @filter wp_privacy_personal_data_exporters\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param array $exporters Existing exporters.\n\t * @return array\n\t */\n\tpublic function register_exporters( $exporters = array() ) {\n\n\t\tforeach ( $this->exporters as $id => $exporter ) {\n\t\t\t$exporters[ $id ] = $exporter;\n\t\t}\n\t\treturn $exporters;\n\t}\n\n\t/**\n\t * Register an eraser.\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param string $id       Eraser ID.\n\t * @param string $name     Human-readable eraser name.\n\t * @param mixed  $callback Callback function (callable).\n\t * @return array\n\t */\n\tpublic function add_eraser( $id, $name, $callback ) {\n\n\t\t$this->erasers[ $id ] = array(\n\t\t\t'eraser_friendly_name' => $name,\n\t\t\t'callback'             => $callback,\n\t\t);\n\t\treturn $this->erasers;\n\t}\n\n\t/**\n\t * Register an exporter.\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param string   $id       Exporter ID.\n\t * @param string   $name     Human-readable exporter name.\n\t * @param callable $callback Callback function.\n\t * @return array\n\t */\n\tpublic function add_exporter( $id, $name, $callback ) {\n\n\t\t$this->exporters[ $id ] = array(\n\t\t\t'exporter_friendly_name' => $name,\n\t\t\t'callback'               => $callback,\n\t\t);\n\t\treturn $this->exporters;\n\t}\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.processor.php",
    "content": "<?php\n/**\n * Background Processor abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.15.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Background Processor abstract class\n *\n * @since 3.15.0\n */\nabstract class LLMS_Abstract_Processor extends WP_Background_Process {\n\n\t/**\n\t * Prefix\n\t *\n\t * @var string\n\t */\n\tprotected $prefix = 'llms';\n\n\t/**\n\t * Unique identifier for the processor\n\t *\n\t * @var  string\n\t */\n\tprotected $id;\n\n\t/**\n\t * Initializer\n\t *\n\t * Acts as a constructor that extending processors should implement\n\t * at the very least should populate the $this->actions array.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return void\n\t */\n\tabstract protected function init();\n\n\t/**\n\t * Array of actions that should be watched to trigger\n\t * the process(es)\n\t *\n\t * @var  array\n\t */\n\tprotected $actions = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->action .= '_' . $this->id;\n\n\t\tparent::__construct();\n\n\t\t// Setup.\n\t\t$this->init();\n\n\t\t// Add trigger actions.\n\t\t$this->add_actions();\n\n\t}\n\n\t/**\n\t * Add actions defined in $this->actions\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function add_actions() {\n\n\t\tforeach ( $this->get_actions() as $action => $data ) {\n\n\t\t\t$data = wp_parse_args(\n\t\t\t\t$data,\n\t\t\t\tarray(\n\t\t\t\t\t'arguments' => 1,\n\t\t\t\t\t'priority'  => 10,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tadd_action( $action, array( $this, $data['callback'] ), $data['priority'], $data['arguments'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Disable a processor\n\t *\n\t * Useful when bulk enrolling into a membership (for example)\n\t * so we don't trigger course data calculations a few hundred times.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function disable() {\n\n\t\tremove_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );\n\t\tforeach ( $this->get_actions() as $action => $data ) {\n\n\t\t\t$data = wp_parse_args(\n\t\t\t\t$data,\n\t\t\t\tarray(\n\t\t\t\t\t'arguments' => 1,\n\t\t\t\t\t'priority'  => 10,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tremove_action( $action, array( $this, $data['callback'] ), $data['priority'], $data['arguments'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Dispatch\n\t *\n\t * Overrides the parent method to reset the (saved) `$data` property and\n\t * prevent duplicate data being pushed into future batches.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return array|WP_Error Result of wp_remote_post()\n\t */\n\tpublic function dispatch() {\n\n\t\t// Perform the parent method.\n\t\t$ret = parent::dispatch();\n\n\t\t/**\n\t\t * Empty the (saved) data to prevent duplicate data in future batches.\n\t\t *\n\t\t * @link https://github.com/gocodebox/lifterlms/issues/1602\n\t\t */\n\t\t$this->data = array();\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Retrieve a filtered array of actions to be added by $this->add_actions\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_actions() {\n\n\t\treturn apply_filters( 'llms_data_processor_' . $this->id . '_actions', $this->actions, $this );\n\n\t}\n\n\t/**\n\t * Retrieve data for the current processor that can be used\n\t * in future processes\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $key     If set, return a specific piece of data rather than the whole array.\n\t * @param string $default When returning a specific piece of data, allows a default value to be passed.\n\t * @return array|mixed\n\t */\n\tpublic function get_data( $key = null, $default = '' ) {\n\n\t\t// Get the array of processor data.\n\t\t$all_data = get_option( 'llms_processor_data', array() );\n\n\t\t// Get data for current processor.\n\t\t$data = isset( $all_data[ $this->id ] ) ? $all_data[ $this->id ] : array();\n\n\t\t// Get a specific piece of data.\n\t\tif ( $key ) {\n\t\t\treturn isset( $data[ $key ] ) ? $data[ $key ] : $default;\n\t\t}\n\n\t\t// Return all the data.\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Returns the edit post link for a post.\n\t *\n\t * This is based on the WordPress {@see get_edit_post_link()} function, but does not check if the user can\n\t * edit the post or if the post's post type has an edit link defined.\n\t *\n\t * When the background processor is running, the current user ID is 0. This prevents {@see current_user_can()}\n\t * from ever returning true and also causes the post's post type edit link to be set to an empty string in\n\t * {@see WP_Post_Type::set_props()}.\n\t *\n\t * This method is useful when the processor has completed and creates an admin notice that contains an edit post link.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int|WP_Post $id      Optional. Post ID or post object. Default is the global `$post`.\n\t * @param string      $context Optional. How to output the '&' character. Default '&amp;'.\n\t * @return string|null The edit post link for the given post. Null if the post type does not exist\n\t *                     or does not allow an editing UI.\n\t */\n\tprotected function get_edit_post_link( $id = 0, $context = 'display' ) {\n\n\t\t$post = get_post( $id );\n\t\tif ( ! $post ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$post_type_object = get_post_type_object( $post->post_type );\n\t\tif ( ! $post_type_object ) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif ( 'revision' === $post->post_type ) {\n\t\t\t$action = '';\n\t\t} elseif ( 'display' === $context ) {\n\t\t\t$action = '&amp;action=edit';\n\t\t} else {\n\t\t\t$action = '&action=edit';\n\t\t}\n\t\t$link = admin_url( sprintf( 'post.php?post=%d%s', $post->ID, $action ) );\n\n\t\t/**\n\t\t * Filters the post edit link.\n\t\t *\n\t\t * This is identical to the `get_edit_post_link` filter hook in {@see get_edit_post_link()}.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param string $link    The edit link.\n\t\t * @param int    $post_id Post ID.\n\t\t * @param string $context The link context. If set to 'display' then ampersands are encoded.\n\t\t */\n\t\treturn apply_filters( 'get_edit_post_link', $link, $post->ID, $context );\n\t}\n\n\t/**\n\t * Log data to the processors log when processors debugging is enabled\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param mixed $data Data to log.\n\t * @return void\n\t */\n\tprotected function log( $data ) {\n\n\t\tif ( defined( 'LLMS_PROCESSORS_DEBUG' ) && LLMS_PROCESSORS_DEBUG ) {\n\t\t\tllms_log( $data, 'processors' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Persist data to the database related to the processor\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param array $data Data to save.\n\t * @return void\n\t */\n\tprivate function save_data( $data ) {\n\n\t\t// Merge the current data with all processor data.\n\t\t$all_data = wp_parse_args(\n\t\t\tarray(\n\t\t\t\t$this->id => $data,\n\t\t\t),\n\t\t\tget_option( 'llms_processor_data', array() )\n\t\t);\n\n\t\t// Save it.\n\t\tupdate_option( 'llms_processor_data', $all_data );\n\n\t}\n\n\t/**\n\t * Update data to the database related to the processor\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $key   Key name.\n\t * @param mixed  $value Value.\n\t * @return void\n\t */\n\tpublic function set_data( $key, $value ) {\n\n\t\t// Get the array of processor data.\n\t\t$data         = $this->get_data();\n\t\t$data[ $key ] = $value;\n\n\t\t$this->save_data( $data );\n\n\t}\n\n\t/**\n\t * Delete a piece of data from the database by key\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $key Key name to remove.\n\t * @return void\n\t */\n\tpublic function unset_data( $key ) {\n\n\t\t$data = $this->get_data();\n\t\tif ( isset( $data[ $key ] ) ) {\n\t\t\tunset( $data[ $key ] );\n\t\t}\n\n\t\t$this->save_data( $data );\n\n\t}\n\n\n}\n"
  },
  {
    "path": "includes/abstracts/llms.abstract.user.data.php",
    "content": "<?php\n/**\n * LifterLMS User Data Abstract\n *\n * @package LifterLMS/Abstracts/Classes\n *\n * @since 3.9.0\n * @version 4.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS User Data Abstract class\n *\n * @since 3.9.0\n * @since 3.17.0 Unknown.\n * @since 3.34.0 Allow `user_url` to be retrieved by `get()`.\n * @since 4.2.0 The `get_id()` always returns an int.\n */\nabstract class LLMS_Abstract_User_Data {\n\n\t/**\n\t * Student's WordPress User ID\n\t *\n\t * @var int\n\t */\n\tprotected $id;\n\n\t/**\n\t * User postmeta key prefix\n\t *\n\t * @var  string\n\t */\n\tprotected $meta_prefix = 'llms_';\n\n\t/**\n\t * Instance of the WP_User\n\t *\n\t * @var obj\n\t */\n\tprotected $user;\n\n\t/**\n\t * Constructor.\n\t *\n\t * By default, the current user is loaded if no `$user` is supplied. This behavior can be disabled by providing `$autoload = false`.\n\t *\n\t * @since 2.2.3\n\t * @since 3.9.0 Unknown.\n\t * @since 7.0.0 Added `$autoload` parameter.\n\t *\n\t * @param int|null|WP_User|LLMS_Abstract_User_Data $user     A `WP_User` ID, instance of a `WP_User`, or instance of any class extending this class.\n\t * @param boolean                                  $autoload If `true` and `$user` input is empty, the user will be loaded from `get_current_user_id()`.\n\t *                                                           If `$user` is not empty then this parameter has no impact.\n\t * @return void\n\t */\n\tpublic function __construct( $user = null, $autoload = true ) {\n\n\t\t$user = ( $user || $autoload ) ? $this->get_user_id( $user ) : false;\n\t\tif ( false !== $user ) {\n\t\t\t$this->id   = $user;\n\t\t\t$this->user = get_user_by( 'ID', $user );\n\t\t}\n\n\t}\n\n\t/**\n\t * Magic Getter for User Data\n\t *\n\t * Mapped directly to the WP_User class.\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.1 Unknown.\n\t * @since 3.34.0 Allow `user_url` to be retrieved.\n\t *\n\t * @param    string $key key of the property to get a value for\n\t * @return   mixed\n\t */\n\tpublic function __get( $key ) {\n\n\t\t// Array of items we should *not* add the $this->meta_prefix to.\n\t\t$unprefixed = apply_filters(\n\t\t\t'llms_student_unprefixed_metas',\n\t\t\tarray(\n\t\t\t\t'description',\n\t\t\t\t'display_name',\n\t\t\t\t'first_name',\n\t\t\t\t'last_name',\n\t\t\t\t'nickname',\n\t\t\t\t'user_login',\n\t\t\t\t'user_nicename',\n\t\t\t\t'user_email',\n\t\t\t\t'user_registered',\n\t\t\t\t'user_url',\n\t\t\t),\n\t\t\t$this\n\t\t);\n\n\t\t/**\n\t\t * Add the meta prefix to things that aren't in the above array\n\t\t * only if the meta prefix isn't already there\n\t\t * this means that the following will output the same data\n\t\t * $this->get( 'llms_billing_address_1')\n\t\t * $this->get( 'billing_address_1')\n\t\t */\n\t\tif ( false === strpos( $key, $this->meta_prefix ) && ! in_array( $key, $unprefixed ) ) {\n\t\t\t$key = $this->meta_prefix . $key;\n\t\t}\n\n\t\tif ( ! $this->exists() ) {\n\t\t\treturn '';\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_student_meta_' . $key, $this->user->get( $key ), $this );\n\n\t}\n\n\t/**\n\t * Retrieve an item from the cache\n\t *\n\t * @param    string $key   cache key\n\t * @return   false|mixed       false on failure\n\t * @since    3.17.0\n\t * @version  3.17.0\n\t */\n\tprotected function cache_get( $key ) {\n\t\treturn wp_cache_get( $key, $this->get_cache_group() );\n\t}\n\n\t/**\n\t * Delete an item from the cache\n\t *\n\t * @param    string $key  cache key\n\t * @return   bool\n\t * @since    3.17.0\n\t * @version  3.17.0\n\t */\n\tprotected function cache_delete( $key ) {\n\t\treturn wp_cache_delete( $key, $this->get_cache_group() );\n\t}\n\n\t/**\n\t * Add an item to the cache cache\n\t *\n\t * @param    string $key  cache key\n\t * @param    mixed  $val  value to cache\n\t * @return   boolean\n\t * @since    3.17.0\n\t * @version  3.17.0\n\t */\n\tprotected function cache_set( $key, $val ) {\n\t\treturn wp_cache_set( $key, $val, $this->get_cache_group() );\n\t}\n\n\t/**\n\t * Determine if the user exists\n\t *\n\t * @return   boolean\n\t * @since    3.9.0\n\t * @version  3.9.0\n\t */\n\tpublic function exists() {\n\t\treturn ( $this->user && $this->user->exists() );\n\t}\n\n\t/**\n\t * Allows direct access to WP_User object for retrieving user data from the user or usermeta tables\n\t *\n\t * @since   3.0.0\n\t * @version 3.0.0\n\t * @param   string $key key of the property to get a value for\n\t * @return  mixed\n\t */\n\tpublic function get( $key ) {\n\t\treturn $this->$key;\n\t}\n\n\t/**\n\t * Retrieve the group name used by cache functions\n\t *\n\t * @return   string\n\t * @since    3.17.0\n\t * @version  3.17.0\n\t */\n\tprotected function get_cache_group() {\n\t\treturn sprintf( 'llms_user_%d', $this->get( 'id' ) );\n\t}\n\n\t/**\n\t * Retrieve the user id\n\t *\n\t * @since 3.9.0\n\t * @since 4.2.0 Always return an absolute integer.\n\t *\n\t * @return int\n\t */\n\tpublic function get_id() {\n\t\treturn absint( $this->id );\n\t}\n\n\t/**\n\t * Allow extending classes to access the main student class\n\t *\n\t * @return   LLMS_Student|false\n\t * @since    3.9.0\n\t * @version  3.9.0\n\t */\n\tprotected function get_student() {\n\t\treturn llms_get_student( $this->get_id() );\n\t}\n\n\t/**\n\t * Retrieve the instance of the WP User for the student\n\t *\n\t * @return   WP_User\n\t * @since    3.9.0\n\t * @version  3.9.0\n\t */\n\tpublic function get_user() {\n\t\treturn $this->user;\n\t}\n\n\t/**\n\t * Retrieve the User ID based on object\n\t *\n\t * @param    mixed $user  WP_User ID, instance of WP_User, or instance of any student class extending this class\n\t * @return   mixed            int if a user id can be found, otherwise false\n\t * @since    3.9.0\n\t * @version  3.9.0\n\t */\n\tprotected function get_user_id( $user ) {\n\n\t\tif ( ! $user && get_current_user_id() ) {\n\t\t\treturn get_current_user_id();\n\t\t} elseif ( is_numeric( $user ) ) {\n\t\t\treturn $user;\n\t\t} elseif ( is_a( $user, 'WP_User' ) && isset( $user->ID ) ) {\n\t\t\treturn $user->ID;\n\t\t} elseif ( $user instanceof LLMS_Abstract_User_Data ) {\n\t\t\treturn $user->get_id();\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Update a meta property for the user\n\t *\n\t * @param    string  $key     meta key\n\t * @param    mixed   $value   meta value\n\t * @param    boolean $prefix  include the meta prefix when setting\n\t *                            passing false will allow 3rd parties to update fields with a custom prefix\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function set( $key, $value, $prefix = true ) {\n\t\t$key = $prefix ? $this->meta_prefix . $key : $key;\n\t\tupdate_user_meta( $this->get_id(), $key, $value );\n\t}\n\n}\n"
  },
  {
    "path": "includes/achievements/class.llms.achievement.user.php",
    "content": "<?php\n/**\n * User Achievement class, inherits methods from LLMS_Achievement\n *\n * Generates achievements for users.\n *\n * @package LifterLMS/Classes/Achievements\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Achievement_User class\n *\n * @since 1.0.0\n * @since 3.17.4 Unknown.\n * @since 3.24.0 Unknown.\n * @since 3.30.3 Explicitly define undefined properties.\n * @deprecated 6.0.0 Class `LLMS_Achievement_User` is deprecated with no direct replacement.\n */\nclass LLMS_Achievement_User extends LLMS_Achievement {\n\n\t/**\n\t * @var string|false\n\t * @since 1.0.0\n\t */\n\tpublic $account_link;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $recipient;\n\n\t/**\n\t * partial path and file name of HTML template\n\t *\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $template_html;\n\n\t/**\n\t * user meta fields\n\t *\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tpublic $user = array();\n\n\t/**\n\t * @var WP_User|false\n\t * @since 1.0.0\n\t */\n\tpublic $user_data;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_email;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_firstname;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_lastname;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_login;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_pass;\n\n\t/**\n\t * Alert when deprecated methods are used.\n\t *\n\t * This class as well as core classes extending it have been deprecated. All public and protected methods\n\t * have been changed to private and will be made accessible through this magic method which also emits a\n\t * deprecation warning.\n\t *\n\t * This public method has been intentionally marked as private to denote it's temporary lifespan. It will be\n\t * removed alongside this class in the next major release.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @access private\n\t *\n\t * @param string $name Name of the method being called.\n\t * @param array  $args Arguments provided to the method.\n\t * @return void\n\t */\n\tpublic function __call( $name, $args ) {\n\t\t_deprecated_function( __CLASS__ . '::' . esc_html( $name ), '6.0.0' );\n\t\tif ( method_exists( $this, $name ) ) {\n\t\t\t$this->$name( ...$args );\n\t\t}\n\t}\n\n\t/**\n\t * Constructor\n\t */\n\tpublic function __construct() {\n\n\t\tparent::__construct();\n\t}\n\n\t/**\n\t * Check if the user has already earned this achievement used to prevent duplicates\n\t *\n\t * @since 3.4.1\n\t * @since 3.17.4 Unknown.\n\t * @deprecated 6.0.0 `LLMS_Achievement_User::has_user_earned()` is deprecated with no replacement.\n\t *\n\t * @return boolean\n\t */\n\tprivate function has_user_earned() {\n\n\t\tglobal $wpdb;\n\n\t\t$count = (int) $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT COUNT( pm.meta_id )\n\t\t\tFROM {$wpdb->postmeta} AS pm\n\t\t\tJOIN {$wpdb->prefix}lifterlms_user_postmeta AS upm ON pm.post_id = upm.meta_value\n\t\t\tWHERE pm.meta_key = '_llms_achievement_template'\n\t\t\t  AND pm.meta_value = %d\n\t\t\t  AND upm.meta_key = '_achievement_earned'\n\t\t\t  AND upm.user_id = %d\n\t\t\t  AND upm.post_id = %d\n\t\t\t  LIMIT 1\n\t\t\t;\",\n\t\t\t\tarray( $this->achievement_template_id, $this->userid, $this->lesson_id )\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * @filter llms_achievement_has_user_earned\n\t\t * Allow 3rd parties to override default dupcheck functionality for achievements\n\t\t */\n\t\treturn apply_filters( 'llms_achievement_has_user_earned', ( $count >= 1 ), $this );\n\t}\n\n\t/**\n\t * Initializes all of the variables needed to create the achievement post.\n\t *\n\t * @since 1.0.0\n\t * @since 3.24.0 Unknown.\n\t * @deprecated 6.0.0 `LLMS_Achievement_User::init()` is deprecated with no replacement.\n\t *\n\t * @param int $id        Id of post.\n\t * @param int $person_id Id of user.\n\t * @param int $lesson_id Id of associated lesson.\n\t * @return void\n\t */\n\tprivate function init( $id, $person_id, $lesson_id ) {\n\t\tglobal $wpdb;\n\n\t\t$content = get_post( $id );\n\t\t$meta    = get_post_meta( $content->ID );\n\n\t\t$this->achievement_template_id = $id;\n\t\t$this->lesson_id               = $lesson_id;\n\t\t$this->title                   = $content->post_title;\n\t\t$this->achievement_title       = $meta['_llms_achievement_title'][0];\n\t\t$this->content                 = ( ! empty( $content->post_content ) ) ? $content->post_content : $meta['_llms_achievement_content'][0];\n\t\t$this->image                   = $meta['_llms_achievement_image'][0];\n\t\t$this->userid                  = $person_id;\n\t\t$this->user                    = get_user_meta( $person_id );\n\t\t$this->user_data               = get_userdata( $person_id );\n\t\t$this->user_firstname          = ( '' != $this->user['first_name'][0] ? $this->user['first_name'][0] : $this->user['nickname'][0] );\n\t\t$this->user_lastname           = ( '' != $this->user['last_name'][0] ? $this->user['last_name'][0] : '' );\n\t\t$this->user_email              = $this->user_data->data->user_email;\n\t\t$this->template_html           = 'achievements/template.php';\n\t\t$this->account_link            = get_permalink( llms_get_page_id( 'myaccount' ) );\n\t}\n\n\t/**\n\t * Creates new instance of WP_User and calls parent method create\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Achievement_User::trigger()` is deprecated with no replacement.\n\t *\n\t * @param int $user_id   ID of user.\n\t * @param int $id        ID of post.\n\t * @param int $lesson_id ID of associated lesson.\n\t * @return void\n\t */\n\tprivate function trigger( $user_id, $id, $lesson_id ) {\n\n\t\t$this->init( $id, $user_id, $lesson_id );\n\n\t\t// Only award achievement if the user hasn't already earned it.\n\t\tif ( $this->has_user_earned() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( $user_id ) {\n\n\t\t\t$this->object     = new WP_User( $user_id );\n\t\t\t$this->user_login = stripslashes( $this->object->user_login );\n\t\t\t$this->user_email = stripslashes( $this->object->user_email );\n\t\t\t$this->recipient  = $this->user_email;\n\n\t\t}\n\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->create( $this->get_content() );\n\t}\n\n\t/**\n\t * Gets post content and replaces merge fields with user meta-data\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Achievement_User::get_content_html()` is deprecated with no replacement.\n\t *\n\t * @return string\n\t */\n\tprivate function get_content_html() {\n\n\t\t$this->find    = array(\n\t\t\t'{site_title}',\n\t\t\t'{user_login}',\n\t\t\t'{site_url}',\n\t\t\t'{first_name}',\n\t\t\t'{last_name}',\n\t\t\t'{email_address}',\n\t\t\t'{current_date}',\n\t\t);\n\t\t$this->replace = array(\n\t\t\t$this->get_blogname(),\n\t\t\t$this->user_login,\n\t\t\t$this->account_link,\n\t\t\t$this->user_firstname,\n\t\t\t$this->user_lastname,\n\t\t\t$this->user_email,\n\t\t\tdate( 'M d, Y', strtotime( current_time( 'mysql' ) ) ),\n\t\t);\n\n\t\t$content = $this->format_string( $this->content );\n\n\t\tob_start();\n\t\tllms_get_template(\n\t\t\t$this->template_html,\n\t\t\tarray(\n\t\t\t\t'content' => $this->content,\n\t\t\t\t'title'   => $this->format_string( $this->title ),\n\t\t\t\t'image'   => $this->image,\n\t\t\t)\n\t\t);\n\t\treturn ob_get_clean();\n\t}\n}\n\nreturn new LLMS_Achievement_User();\n"
  },
  {
    "path": "includes/achievements/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/admin/class-llms-admin-events-promo.php",
    "content": "<?php\n/**\n * Lightweight Events add-on promotion in the LifterLMS core admin.\n *\n * Shows an \"Events\" tab in Course Options and Membership meta boxes\n * with a CTA to install the Events add-on when it is not active.\n *\n * Also adds an \"Events\" link in Course Builder lesson settings.\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Events_Promo class.\n *\n * @since 7.8.0\n */\nclass LLMS_Admin_Events_Promo {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.8.0\n\t */\n\tpublic function __construct() {\n\t\t// Only show promo if the events plugin is not active.\n\t\tif ( class_exists( 'LLMS_Events_Plugin' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tadd_filter( 'llms_metabox_fields_lifterlms_course_options', array( $this, 'add_course_promo_tab' ) );\n\t\tadd_filter( 'llms_metabox_fields_lifterlms_membership', array( $this, 'add_membership_promo_tab' ) );\n\t\tadd_filter( 'llms_metabox_fields_lifterlms_lesson', array( $this, 'add_lesson_promo_tab' ) );\n\t}\n\n\t/**\n\t * Add an Events promo tab to Course Options.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param array $tabs Existing tabs.\n\t * @return array\n\t */\n\tpublic function add_course_promo_tab( $tabs ) {\n\t\t$tabs[] = array(\n\t\t\t'title'  => __( 'Events', 'lifterlms' ),\n\t\t\t'fields' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => '_llms_events_promo',\n\t\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t\t'label' => '',\n\t\t\t\t\t'value' => $this->get_promo_html(),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t\treturn $tabs;\n\t}\n\n\t/**\n\t * Add an Events promo tab to Membership.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param array $tabs Existing tabs.\n\t * @return array\n\t */\n\tpublic function add_membership_promo_tab( $tabs ) {\n\t\t$tabs[] = array(\n\t\t\t'title'  => __( 'Events', 'lifterlms' ),\n\t\t\t'fields' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => '_llms_events_promo',\n\t\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t\t'label' => '',\n\t\t\t\t\t'value' => $this->get_promo_html(),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t\treturn $tabs;\n\t}\n\n\t/**\n\t * Add an Events promo tab to Lesson Settings.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param array $tabs Existing tabs.\n\t * @return array\n\t */\n\tpublic function add_lesson_promo_tab( $tabs ) {\n\t\t$tabs[] = array(\n\t\t\t'title'  => __( 'Events', 'lifterlms' ),\n\t\t\t'fields' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => '_llms_events_promo',\n\t\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t\t'label' => '',\n\t\t\t\t\t'value' => $this->get_promo_html(),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t\treturn $tabs;\n\t}\n\n\t/**\n\t * Get the promo HTML.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @return string\n\t */\n\tprivate function get_promo_html() {\n\t\t$html  = '<div class=\"llms-metabox\" style=\"padding:20px;text-align:center;\">';\n\t\t$html .= '<div class=\"dashicons dashicons-calendar-alt\" style=\"color:#2271b1;margin-bottom:12px;\"></div>';\n\t\t$html .= '<h3>' . esc_html__( 'Schedule Events for Your Students', 'lifterlms' ) . '</h3>';\n\t\t$html .= '<p>' . esc_html__( 'Add live events, webinars, and in-person sessions to your courses and memberships. Students can subscribe to calendar feeds and never miss an event.', 'lifterlms' ) . '</p>';\n\t\t$html .= '<a href=\"https://lifterlms.com/product/lifterlms-events/?utm_source=LifterLMS%20Plugin&utm_medium=Course%20Editor&utm_campaign=Events%20Promo\" target=\"_blank\" class=\"llms-button-primary\">';\n\t\t$html .= esc_html__( 'Get LifterLMS Events', 'lifterlms' );\n\t\t$html .= '</a>';\n\t\t$html .= '</div>';\n\n\t\treturn $html;\n\t}\n}\n\nnew LLMS_Admin_Events_Promo();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-export-download.php",
    "content": "<?php\n/**\n * Serves Export CSVs on the admin panel\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.28.1\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Export_Download class\n *\n * @since 3.28.1\n */\nclass LLMS_Admin_Export_Download {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since   3.28.1\n\t * @version 3.28.1\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'admin_init', array( $this, 'maybe_serve_export' ) );\n\t}\n\n\t/**\n\t * Serve an export file as a download.\n\t *\n\t * @since 3.28.1\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.5.0 Check nonce and only consider the basename of the file to be downloaded.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_serve_export() {\n\n\t\t$export = llms_filter_input( INPUT_GET, 'llms-dl-export', FILTER_SANITIZE_FULL_SPECIAL_CHARS );\n\t\tif ( ! $export ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Verify nonce.\n\t\tif ( ! isset( $_REQUEST['llms_dl_export_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['llms_dl_export_nonce'] ) ), LLMS_Abstract_Exportable_Admin_Table::EXPORT_NONCE_ACTION ) ) {\n\t\t\twp_die( esc_html__( 'Cheatin&#8217; huh?', 'lifterlms' ) );\n\t\t}\n\n\t\t// Only allow people who can view reports view exports.\n\t\tif ( ! current_user_can( 'view_others_lifterlms_reports' ) && ! current_user_can( 'view_lifterlms_reports' ) ) {\n\t\t\twp_die( esc_html__( 'Cheatin&#8217; huh?', 'lifterlms' ) );\n\t\t}\n\n\t\t$path = LLMS_TMP_DIR . basename( $export );\n\t\tif ( ! file_exists( $path ) ) {\n\t\t\twp_die( esc_html__( 'Cheatin&#8217; huh?', 'lifterlms' ) );\n\t\t}\n\n\t\t$info = pathinfo( $path );\n\t\tif ( 'csv' !== $info['extension'] ) {\n\t\t\twp_die( esc_html__( 'Cheatin&#8217; huh?', 'lifterlms' ) );\n\t\t}\n\n\t\theader( 'Content-Type: text/csv' );\n\t\theader( 'Content-Disposition: attachment; filename=\"' . $export . '\"' );\n\n\t\t$file = file_get_contents( $path );\n\t\tunlink( $path );\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo $file;\n\t\texit;\n\t}\n}\n\nreturn new LLMS_Admin_Export_Download();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-header.php",
    "content": "<?php\n/**\n * LLMS_Admin_Header class file\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 7.1.0\n * @version 7.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Header UI.\n *\n * @since 7.1.0\n */\nclass LLMS_Admin_Header {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'in_admin_header', array( $this, 'admin_header' ) );\n\t}\n\n\t/**\n\t * Show admin header banner on LifterLMS admin screens.\n\t *\n\t * @since 7.1.0\n\t * @since 7.1.2 Making the LifterLMS logo link to the LifterLMS.com site.\n\t * @since 7.1.3 Using strpos instead of str_starts_with for compatibility.\n\t * @since 7.4.0 Added `llms_admin_show_header` filter and move wizard check.\n\t *\n\t * @return void\n\t */\n\tpublic function admin_header() {\n\n\t\t// Assume we should not show our header.\n\t\t$show_header = false;\n\n\t\t// Get the current screen and determine if we should show the header.\n\t\t$current_screen = get_current_screen();\n\n\t\t// Show header on our custom post types in admin, but not on the block editor.\n\t\tif (\n\t\t\tisset( $current_screen->post_type ) &&\n\t\t\tin_array( $current_screen->post_type, array( 'course', 'lesson', 'llms_review', 'llms_membership', 'llms_engagement', 'llms_order', 'llms_coupon', 'llms_voucher', 'llms_form', 'llms_achievement', 'llms_my_achievement', 'llms_certificate', 'llms_my_certificate', 'llms_email' ), true ) &&\n\t\t\tfalse === $current_screen->is_block_editor\n\t\t) {\n\t\t\t$show_header = true;\n\t\t}\n\n\t\t// Get the current page if available.\n\t\t$page = (string) llms_filter_input( INPUT_GET, 'page' );\n\n\t\t// Show header on our settings pages.\n\t\tif (\n\t\t\t( strpos( $page, 'llms-' ) === 0 ) ||\n\t\t\t( ! empty( $current_screen->id ) && strpos( $current_screen->id, 'lifterlms' ) === 0 )\n\t\t) {\n\t\t\t$show_header = true;\n\t\t}\n\n\t\t// Don't show header on the Course Builder.\n\t\tif ( isset( $current_screen->base ) && 'admin_page_llms-course-builder' === $current_screen->base ) {\n\t\t\t$show_header = false;\n\t\t}\n\n\t\t/**\n\t\t * Allow developers to filter the show header value.\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param bool      $show_header    Whether to show the header.\n\t\t * @param WP_Screen $current_screen The current screen object.\n\t\t * @param string    $page           The current page if available.\n\t\t */\n\t\t$show_header = apply_filters( 'llms_admin_show_header', $show_header, $current_screen, $page );\n\n\t\t// Conditionally show our header.\n\t\tif ( ! empty( $show_header ) ) { ?>\n\t\t\t<header class=\"llms-header\">\n\t\t\t\t<div class=\"llms-inside-wrap\">\n\t\t\t\t\t<a href=\"https://lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Admin%20Header&utm_content=LifterLMS%20Logo\" target=\"_blank\"><img class=\"lifterlms-logo\" src=\"<?php echo esc_url( llms()->plugin_url() ); ?>/assets/images/lifterlms-logo-black.png\" alt=\"<?php esc_attr_e( 'LifterLMS Logo', 'lifterlms' ); ?>\"></a>\n\t\t\t\t\t<div class=\"llms-meta\">\n\t\t\t\t\t\t<div class=\"llms-meta-left\">\n\t\t\t\t\t\t\t<span class=\"llms-version\"><?php echo esc_html( sprintf( __( 'Version: %s', 'lifterlms' ), llms()->version ) ); ?></span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"llms-meta-right\">\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t// Show a license link in header if we aren't on the Add-ons screen.\n\t\t\t\t\t\t\t$screen = get_current_screen();\n\t\t\t\t\t\t\tif ( 'lifterlms_page_llms-add-ons' !== $screen->id ) {\n\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t<span class=\"llms-license\">\n\t\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t\t// Get active keys for this site.\n\t\t\t\t\t\t\t\t\t$my_keys = llms_helper_options()->get_license_keys();\n\n\t\t\t\t\t\t\t\t\tif ( empty( $my_keys ) ) {\n\t\t\t\t\t\t\t\t\t\t$license_class = 'llms-license-none';\n\t\t\t\t\t\t\t\t\t\t$license_label = __( 'No License', 'lifterlms' );\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t$license_class = 'llms-license-active';\n\t\t\t\t\t\t\t\t\t\t$license_label = __( 'My License Keys', 'lifterlms' );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t\t<a class=\"<?php echo esc_attr( $license_class ); ?>\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-add-ons' ) ); ?>\"><?php echo esc_html( $license_label ); ?></a>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t<span class=\"llms-support\">\n\t\t\t\t\t\t\t\t<a href=\"https://lifterlms.com/my-account/my-tickets/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Admin%20Header&utm_content=LifterLMS%20Support\" target=\"_blank\"><?php esc_html_e( 'Get Support', 'lifterlms' ); ?></a>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</header>\n\t\t\t<?php\n\t\t}\n\t}\n}\n\nreturn new LLMS_Admin_Header();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-media-protection-attachment-settings.php",
    "content": "<?php\n/**\n * LifterLMS Admin Media Protection Attachment Settings.\n *\n * @package LifterLMS/Classes/Admin\n */\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\nclass LLMS_Admin_Media_Protection_Attachment_Settings {\n\n\tpublic function __construct() {\n\n\t\tadd_filter( 'attachment_fields_to_edit', array( $this, 'attachment_fields_to_edit' ), 10, 2 );\n\t\tadd_filter( 'attachment_fields_to_save', array( $this, 'attachment_fields_to_save' ), 10, 2 );\n\t}\n\n\n\t/**\n\t * Add the media protection settings to the attachment edit screen\n\t *\n\t * @param   array  $form_fields  Array of form fields\n\t * @param   object $post         WP_Post object\n\t * @return  array\n\t */\n\tpublic function attachment_fields_to_edit( $form_fields, $post ) {\n\n\t\t$selected_product_html = $protection_warning_html = '';\n\t\t$selected_product_id   = get_post_meta( $post->ID, '_llms_media_protection_product_id', true );\n\t\tif ( $selected_product_id && ( $selected_product      = get_post( $selected_product_id ) ) ) {\n\t\t\t$selected_product_html = sprintf( '<option value=\"%d\" selected=\"selected\">%s</option>', $selected_product->ID, $selected_product->post_title );\n\t\t}\n\n\t\t$protector = new LLMS_Media_Protector();\n\t\tif ( ! $protector->is_media_protected( $post->ID ) ) {\n\t\t\t// translators: %s is a link to the LifterLMS documentation.\n\t\t\t$protection_warning_html = '<div class=\"llms-media-protection-warning\">' . sprintf( __( 'This media is not protected. If you select a product here, the media will be moved to the protected uploads directory and existing links to the media will no longer work. %1$sLearn More%2$s', 'lifterlms' ), '<a target=\"_blank\" href=\"https://lifterlms.com/docs/how-protected-media-files-work/?utm_source=LifterLMS%20Plugin&utm_medium=Media&utm_campaign=Backend%20Help%20Page\">', '</a>' ) . '</div>';\n\t\t}\n\n\t\t$form_fields['llms_media_protection_post'] = array(\n\t\t\t'label' => __( 'LifterLMS Media Protection:', 'lifterlms' ),\n\t\t\t'input' => 'html',\n\t\t\t// TODO: Add selected course/membership to the select2 dropdown if known for this attachment post.\n\t\t\t'html'  => \"$protection_warning_html<select id='attachments-\" . $post->ID . \"-llms_media_protection_post' class='llms-posts-select2' data-no-view-button='true' data-allow_clear='false' data-post-type='course,llms_membership' name='attachments[\" . $post->ID . \"][llms_media_protection_post]'>$selected_product_html</select>\",\n\t\t\t'helps' => $protector->is_media_protected( $post->ID ) ? sprintf( __( 'Access is restricted to the selected course/membership. %1$sLearn More%2$s', 'lifterlms' ), '<a target=\"_blank\" href=\"https://lifterlms.com/docs/how-protected-media-files-work/?utm_source=LifterLMS%20Plugin&utm_medium=Media&utm_campaign=Backend%20Help%20Page\">', '</a>' ) : '',\n\t\t);\n\n\t\t/**\n\t\t * Filter the LifterLMS media protection attachment field.\n\t\t *\n\t\t * @since 10.0.0\n\t\t *\n\t\t * @param array                $field     Attachment field definition.\n\t\t * @param WP_Post              $post      Attachment post object.\n\t\t * @param LLMS_Media_Protector $protector Media protector instance.\n\t\t */\n\t\t$form_fields['llms_media_protection_post'] = apply_filters( 'llms_media_protection_attachment_field', $form_fields['llms_media_protection_post'], $post, $protector );\n\n\t\treturn $form_fields;\n\t}\n\n\t/**\n\t * Save the media protection settings\n\t *\n\t * @param   array $post     Array of post data\n\t * @param   array $attachment  Array of attachment data\n\t * @return  array\n\t */\n\tpublic function attachment_fields_to_save( $post, $attachment ) {\n\n\t\tif ( ! empty( $attachment['llms_media_protection_post'] ) ) {\n\t\t\tif ( $this->move_attachment_to_protected_dir( $post['ID'] ) ) {\n\t\t\t\tupdate_post_meta( $post['ID'], '_llms_media_protection_product_id', absint( $attachment['llms_media_protection_post'] ) );\n\t\t\t}\n\t\t}\n\n\t\treturn $post;\n\t}\n\n\t/**\n\t * Move an existing media attachment over to the protected folder.\n\t *\n\t * @param $attachment_id\n\t * @since 9.0.0\n\t *\n\t * @return bool\n\t */\n\tfunction move_attachment_to_protected_dir( $attachment_id ) {\n\t\t// Get attachment metadata.\n\t\t$metadata = wp_get_attachment_metadata( $attachment_id );\n\t\t$file     = get_attached_file( $attachment_id );\n\n\t\t// Get the protected upload directory.\n\t\t$protector = new LLMS_Media_Protector();\n\n\t\t// We could check that the file is in the protected folder, but currently there's no \"unprotect\" method.\n\t\tif ( $protector->is_media_protected( $attachment_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$protected_dir = $protector->get_upload_basedir();\n\n\t\tglobal $wp_filesystem;\n\t\tif ( empty( $wp_filesystem ) ) {\n\t\t\trequire_once ABSPATH . '/wp-admin/includes/file.php';\n\t\t\tWP_Filesystem();\n\t\t}\n\n\t\t$new_file = str_replace( wp_upload_dir()['basedir'], wp_upload_dir()['basedir'] . untrailingslashit( $protected_dir ), $file );\n\t\tif ( ! $wp_filesystem->is_dir( dirname( $new_file ) ) ) {\n\t\t\twp_mkdir_p( dirname( $new_file ) );\n\t\t}\n\t\tif ( $wp_filesystem->move( $file, $new_file ) ) {\n\t\t\t// Move thumbnails if they exist.\n\t\t\tif ( ! empty( $metadata['sizes'] ) ) {\n\t\t\t\t$base_dir     = dirname( $file );\n\t\t\t\t$new_base_dir = dirname( $new_file );\n\n\t\t\t\t// Multiple registered sizes can share the same physical file.\n\t\t\t\t$moved_files = array();\n\n\t\t\t\tforeach ( $metadata['sizes'] as $size => $size_info ) {\n\t\t\t\t\tif ( in_array( $size_info['file'], $moved_files, true ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t$old_thumb = $base_dir . '/' . $size_info['file'];\n\t\t\t\t\t$new_thumb = $new_base_dir . '/' . $size_info['file'];\n\t\t\t\t\tif ( ! $wp_filesystem->exists( $old_thumb ) ) {\n\t\t\t\t\t\terror_log( 'Registered metadata thumbnail file does not exist. Skipping. ' . $old_thumb );\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif ( ! $wp_filesystem->move( $old_thumb, $new_thumb ) ) {\n\t\t\t\t\t\terror_log( 'Unable to move protected file. Thumbnail moving failed: ' . $old_thumb . ' to ' . $new_thumb );\n\n\t\t\t\t\t\t// Move the file back along with any thumbnails we already moved.\n\t\t\t\t\t\t$wp_filesystem->move( $new_file, $file );\n\t\t\t\t\t\tforeach ( $moved_files as $moved_file ) {\n\t\t\t\t\t\t\t$old_thumb = $base_dir . '/' . $moved_file;\n\t\t\t\t\t\t\t$new_thumb = $new_base_dir . '/' . $moved_file;\n\t\t\t\t\t\t\tif ( $wp_filesystem->exists( $new_thumb ) ) {\n\t\t\t\t\t\t\t\t$wp_filesystem->move( $new_thumb, $old_thumb );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\t$moved_files[] = $size_info['file'];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Update attachment location in database.\n\t\t\tupdate_attached_file( $attachment_id, $new_file );\n\n\t\t\t// This only exists with images it seems.\n\t\t\tif ( array_key_exists( 'file', $metadata ) ) {\n\t\t\t\t$metadata['file'] = ltrim( $protected_dir, '/' ) . $metadata['file'];\n\t\t\t\twp_update_attachment_metadata( $attachment_id, $metadata );\n\t\t\t}\n\n\t\t\t$protector->add_authorization_meta_to_media_post( $attachment_id );\n\n\t\t\treturn true;\n\t\t}\n\n\t\terror_log( 'Unable to move protected file, check permissions on the protected directory or existing file with the same name: ' . $file );\n\t\treturn false;\n\t}\n}\n\nnew LLMS_Admin_Media_Protection_Attachment_Settings();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-permalinks.php",
    "content": "<?php\n/**\n * LLMS_Admin_Header class file\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 7.6.0\n * @version 7.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Permalink settings class\n */\nclass LLMS_Admin_Permalinks {\n\n\t/**\n\t * Permalink settings.\n\t *\n\t * @var array\n\t */\n\tprivate $permalinks = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'current_screen', array( $this, 'load_on_permalinks_screen' ) );\n\t}\n\n\t/**\n\t * Ensure we're on the permalinks screen.\n\t *\n\t * @since 7.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function load_on_permalinks_screen() {\n\t\t$screen = get_current_screen();\n\n\t\tif ( $screen && 'options-permalink' === $screen->id ) {\n\t\t\t$this->settings_init();\n\t\t\t$this->settings_save();\n\t\t}\n\t}\n\n\t/**\n\t * Show the available permalink settings\n\t */\n\tpublic function settings_init() {\n\t\tadd_settings_section( 'lifterlms-permalink', __( 'LifterLMS Permalinks', 'lifterlms' ), array( $this, 'settings' ), 'permalink' );\n\n\t\t$this->permalinks = llms_get_permalink_structure();\n\t}\n\n\tpublic function settings() {\n\t\t?>\n\t\t<p><?php esc_html_e( 'LifterLMS uses custom post types and taxonomies to organize your courses and memberships. You can customize the URLs for these items here.', 'lifterlms' ); ?></p>\n\n\t\t<?php\n\t\t$course_catalog_id = llms_get_page_id( 'courses' );\n\t\tif ( $course_catalog_id && get_post( $course_catalog_id ) ) {\n\t\t\t?>\n\t\t\t<p>\n\t\t\t\t<?php echo esc_html__( 'Note: The Courses Catalog is currently set to a static page.', 'lifterlms' ); ?>\n\t\t\t\t<a href=\"<?php echo esc_url( get_edit_post_link( $course_catalog_id ) ); ?>\">\n\t\t\t\t<?php echo esc_html__( 'You can edit the page slug to change its location.', 'lifterlms' ); ?>\n\t\t\t\t</a>\n\t\t\t</p>\n\t\t\t<?php\n\t\t}\n\n\t\t$memberships_catalog_id = llms_get_page_id( 'memberships' );\n\t\tif ( $memberships_catalog_id && get_post( $memberships_catalog_id ) ) {\n\t\t\t?>\n\t\t\t<p>\n\t\t\t\t<?php echo esc_html__( 'Note: The Memberships Catalog is currently set to a static page.', 'lifterlms' ); ?>\n\t\t\t\t<a href=\"<?php echo esc_url( get_edit_post_link( $memberships_catalog_id ) ); ?>\">\n\t\t\t\t\t<?php echo esc_html__( 'You can edit the page slug to change its location.', 'lifterlms' ); ?>\n\t\t\t\t</a>\n\t\t\t</p>\n\t\t\t<?php\n\t\t}\n\t\t?>\n\n\t\t<table class=\"form-table\" role=\"presentation\">\n\t\t\t<tbody>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"course_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Course Post Type', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_course_base\" id=\"course_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['course_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<?php if ( ! $course_catalog_id || ! get_post( $course_catalog_id ) ) : ?>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"courses_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Course Archive base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_courses_base\" id=\"courses_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['courses_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( ! $memberships_catalog_id || ! get_post( $memberships_catalog_id ) ) : ?>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"memberships_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Memberships Archive base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_memberships_base\" id=\"memberships_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['memberships_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<?php endif; ?>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"lesson_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Lesson Post Type', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_lesson_base\" id=\"lesson_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['lesson_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"quiz_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Quiz Post Type', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_quiz_base\" id=\"quiz_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['quiz_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"certificate_template_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Certificate Template Post Type', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_certificate_template_base\" id=\"certificate_template_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['certificate_template_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"certificate_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Earned Certificate Post Type', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_certificate_base\" id=\"certificate_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['certificate_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"course_category_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Course Category base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_course_category_base\" id=\"course_category_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['course_category_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"course_tag_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Course Tag base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_course_tag_base\" id=\"course_tag_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['course_tag_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"course_track_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Course Track base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_course_track_base\" id=\"course_track_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['course_track_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"course_difficulty_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Course Difficulty base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_course_difficulty_base\" id=\"course_difficulty_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['course_difficulty_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"membership_category_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Membership Category base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_membership_category_base\" id=\"membership_category_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['membership_category_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"membership_tag_base\">\n\t\t\t\t\t\t<?php esc_html_e( 'Membership Tag base', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input name=\"llms_membership_tag_base\" id=\"membership_tag_base\" type=\"text\" value=\"<?php echo esc_attr( $this->permalinks['membership_tag_base'] ); ?>\" class=\"regular-text code\" required>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<?php do_action( 'llms_permalink_setting_fields' ); ?>\n\t\t\t</tbody>\n\t\t</table>\n\n\t\t<?php wp_nonce_field( 'llms-permalinks', 'llms-permalinks-nonce' ); ?>\n\t\t<?php\n\t}\n\n\t/**\n\t * Save the permalink settings\n\t */\n\tpublic function settings_save() {\n\t\tif ( ! is_admin() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! current_user_can( 'manage_options' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isset( $_POST['llms-permalinks-nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['llms-permalinks-nonce'] ), 'llms-permalinks' ) ) {\n\t\t\tllms_switch_to_site_locale();\n\n\t\t\t$permalinks = llms_get_permalink_structure();\n\n\t\t\t$permalinks['course_base']               = isset( $_POST['llms_course_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_course_base'] ) ) : $permalinks['course_base'];\n\t\t\t$permalinks['courses_base']              = isset( $_POST['llms_courses_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_courses_base'] ) ) : $permalinks['courses_base'];\n\t\t\t$permalinks['memberships_base']          = isset( $_POST['llms_memberships_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_memberships_base'] ) ) : $permalinks['memberships_base'];\n\t\t\t$permalinks['lesson_base']               = isset( $_POST['llms_lesson_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_lesson_base'] ) ) : $permalinks['lesson_base'];\n\t\t\t$permalinks['quiz_base']                 = isset( $_POST['llms_quiz_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_quiz_base'] ) ) : $permalinks['quiz_base'];\n\t\t\t$permalinks['certificate_template_base'] = isset( $_POST['llms_certificate_template_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_certificate_template_base'] ) ) : $permalinks['certificate_template_base'];\n\t\t\t$permalinks['certificate_base']          = isset( $_POST['llms_certificate_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_certificate_base'] ) ) : $permalinks['certificate_base'];\n\t\t\t$permalinks['course_category_base']      = isset( $_POST['llms_course_category_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_course_category_base'] ) ) : $permalinks['course_category_base'];\n\t\t\t$permalinks['course_tag_base']           = isset( $_POST['llms_course_tag_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_course_tag_base'] ) ) : $permalinks['course_tag_base'];\n\t\t\t$permalinks['course_track_base']         = isset( $_POST['llms_course_track_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_course_track_base'] ) ) : $permalinks['course_track_base'];\n\t\t\t$permalinks['course_difficulty_base']    = isset( $_POST['llms_course_difficulty_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_course_difficulty_base'] ) ) : $permalinks['course_difficulty_base'];\n\t\t\t$permalinks['membership_category_base']  = isset( $_POST['llms_membership_category_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_membership_category_base'] ) ) : $permalinks['membership_category_base'];\n\t\t\t$permalinks['membership_tag_base']       = isset( $_POST['llms_membership_tag_base'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_membership_tag_base'] ) ) : $permalinks['membership_tag_base'];\n\n\t\t\tllms_set_permalink_structure( $permalinks );\n\n\t\t\tllms_restore_locale();\n\t\t}\n\t}\n}\n\nreturn new LLMS_Admin_Permalinks();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-plugins.php",
    "content": "<?php\n/**\n * LLMS_Admin_Plugins class\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 7.5.1\n * @version 7.5.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Modifications to the plugins page.\n *\n * @since 7.5.1\n */\nclass LLMS_Admin_Plugins {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 7.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_filter( 'plugin_action_links_' . plugin_basename( LLMS_PLUGIN_DIR . '/lifterlms.php' ), array( $this, 'plugin_action_links' ) );\n\t\tadd_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 );\n\t}\n\n\t/**\n\t * Add links to the plugins page.\n\t */\n\tpublic function plugin_action_links( $links ) {\n\t\t$new_links = array(\n\t\t\t'dashboard' => '<a href=\"' . esc_url( admin_url( 'admin.php?page=llms-dashboard' ) ) . '\">' . __( 'Dashboard', 'lifterlms' ) . '</a>',\n\t\t\t'settings' => '<a href=\"' . esc_url( admin_url( 'admin.php?page=llms-settings' ) ) . '\">' . __( 'Settings', 'lifterlms' ) . '</a>',\n\t\t);\n\n\t\t$links = array_merge( $new_links, $links );\n\t\treturn $links;\n\t}\n\n\t/**\n\t * Add links to plugin description.\n\t */\n\tpublic function plugin_row_meta( $links, $file ) {\n\t\tif ( plugin_basename( LLMS_PLUGIN_DIR . '/lifterlms.php' ) === $file ) {\n\t\t\t$row_meta = array(\n\t\t\t\t'docs'      => '<a href=\"' . esc_url( 'https://lifterlms.com/docs/?utm_source=LifterLMS%20Plugin&utm_medium=Plugins%20Screen&utm_campaign=Plugin%20to%20Sale&utm_content=Documentation' ) . '\" aria-label=\"' . esc_attr__( 'Docs', 'lifterlms' ) . '\" target=\"_blank\">' . esc_html__( 'Documentation', 'lifterlms' ) . '</a>',\n\t\t\t\t'support'   => '<a href=\"' . esc_url( 'https://lifterlms.com/my-account/my-tickets/?utm_source=LifterLMS%20Plugin&utm_medium=Plugins%20Screen&utm_campaign=Plugin%20to%20Sale&utm_content=Support' ) . '\" aria-label=\"' . esc_attr__( 'Support', 'lifterlms' ) . '\" target=\"_blank\">' . esc_html__( 'Support', 'lifterlms' ) . '</a>',\n\t\t\t\t'pricing'   => '<a href=\"' . esc_url( 'https://lifterlms.com/pricing/?utm_source=LifterLMS%20Plugin&utm_medium=Plugins%20Screen&utm_campaign=Plugin%20to%20Sale&utm_content=Premium%20Plans' ) . '\" aria-label=\"' . esc_attr__( 'Premium Plans', 'lifterlms' ) . '\" target=\"_blank\">' . esc_html__( 'Premium Plans', 'lifterlms' ) . '</a>',\n\t\t\t);\n\n\t\t\t$links = array_merge( $links, $row_meta );\n\t\t}\n\n\t\treturn $links;\n\t}\n}\n\nreturn new LLMS_Admin_Plugins();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-profile.php",
    "content": "<?php\n/**\n * Handle extra profile fields for users in admin\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since [verson]\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Handle extra profile fields for users in admin\n *\n * Applies to edit-user.php & profile.php.\n *\n * @since 5.0.0\n */\nclass LLMS_Admin_Profile {\n\n\t/**\n\t * Array of user profile fields\n\t *\n\t * @var array\n\t */\n\tprivate $fields;\n\n\t/**\n\t * Submission errors\n\t *\n\t * @var null|WP_Error\n\t */\n\tprivate $errors;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'show_user_profile', array( $this, 'add_user_meta_fields' ) );\n\t\tadd_action( 'edit_user_profile', array( $this, 'add_user_meta_fields' ) );\n\n\t\tadd_action( 'personal_options_update', array( $this, 'save_user_meta_fields' ) );\n\t\tadd_action( 'edit_user_profile_update', array( $this, 'save_user_meta_fields' ) );\n\n\t\t// Allow errors to be output.\n\t\tadd_action( 'user_profile_update_errors', array( $this, 'add_errors' ) );\n\t}\n\n\t/**\n\t * Add user meta fields to the profile screens\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param WP_User $user Instance of WP_User for the user being updated.\n\t * @return bool `true` if fields were added, `false` otherwise.\n\t */\n\tpublic function add_user_meta_fields( $user ) {\n\n\t\tif ( ! $this->current_user_can_edit_admin_custom_fields() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$fields = $this->get_fields();\n\n\t\tif ( empty( $fields ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t/**\n\t\t * Enqueue select2 scripts and styles.\n\t\t */\n\t\twp_enqueue_script( 'llms-metaboxes' );\n\t\twp_enqueue_script( 'llms-select2' );\n\t\tllms()->assets->enqueue_style( 'llms-select2-styles' );\n\t\twp_add_inline_script(\n\t\t\t'llms',\n\t\t\t\"window.llms.address_info = '\" . wp_json_encode( llms_get_countries_address_info() ) . \"';\"\n\t\t);\n\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/views/user-edit-fields.php';\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Maybe save user meta fields\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param int $user_id WP_User ID for the user being updated.\n\t * @return void\n\t */\n\tpublic function save_user_meta_fields( $user_id ) {\n\n\t\tif ( ! $this->current_user_can_edit_admin_custom_fields() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$fields      = $this->get_fields();\n\t\t$posted_data = array();\n\n\t\tforeach ( $this->fields as $field ) {\n\t\t\tif ( in_array( $field['name'], LLMS_CONFIRMATION_FIELDS, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t//phpcs:disable WordPress.Security.NonceVerification.Missing  -- nonce is verified prior to reaching this method.\n\t\t\tif ( isset( $_POST[ $field['name'] ] ) &&\n\t\t\t\t\tisset( $field['data_store_key'] ) &&\n\t\t\t\t\t\t$field['data_store'] && 'usermeta' === $field['data_store'] ) {\n\t\t\t\t//phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- sanitization and unslashing happens in `LLMS_Form_Handler::instance()->submit_form_fields()` below.\n\t\t\t\t$posted_data[ $field['name'] ] = $_POST[ $field['name'] ];\n\t\t\t}\n\t\t\t//phpcs:disable WordPress.Security.NonceVerification.Missing\n\t\t}\n\n\t\tif ( empty( $posted_data ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$posted_data['user_id'] = $user_id;\n\n\t\t$submit = LLMS_Form_Handler::instance()->submit_fields( $posted_data, 'admin-profile', $fields, 'update' );\n\n\t\tif ( isset( $_POST['llms_allow_unlimited_quiz_time'] ) && 'yes' === $_POST['llms_allow_unlimited_quiz_time'] ) {\n\t\t\tupdate_user_option( $user_id, 'llms_allow_unlimited_quiz_time', 'yes' );\n\t\t} else {\n\t\t\tupdate_user_option( $user_id, 'llms_allow_unlimited_quiz_time', 'no' );\n\t\t}\n\n\t\tif ( is_wp_error( $submit ) ) {\n\t\t\t$this->errors = $submit;\n\t\t}\n\t}\n\n\t/**\n\t * Maybe print validation errors\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param WP_Error $errors Instance of WP_Error, passed by reference.\n\t * @return void\n\t */\n\tpublic function add_errors( &$errors ) {\n\n\t\tif ( is_wp_error( $this->errors ) && $this->errors->has_errors() ) {\n\t\t\t$this->merge_llms_fields_errors( $errors );\n\t\t}\n\t}\n\n\t/**\n\t * Check whether the current user can edit users custom fields\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return boolean\n\t */\n\tprivate function current_user_can_edit_admin_custom_fields() {\n\t\treturn current_user_can( 'manage_lifterlms' ) && current_user_can( 'edit_users' );\n\t}\n\n\t/**\n\t * Merge llms fields errors into the passed WP_Error\n\t *\n\t * @since 5.0.0\n\t * @todo Remove the fallback when minimum required WP version will be 5.6+.\n\t *\n\t * @param WP_Error $errors Instance of WP_Error, passed by reference.\n\t * @return void\n\t */\n\tprivate function merge_llms_fields_errors( &$errors ) {\n\n\t\tforeach ( $this->errors->get_error_codes() as $code ) {\n\t\t\tforeach ( $this->errors->get_error_messages( $code ) as $error_message ) {\n\t\t\t\t$errors->add(\n\t\t\t\t\t$code,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t// Translators: %1$s = Opening strong tag; %2$s = Closing strong tag; %3$s = The error message.\n\t\t\t\t\t\tesc_html__( '%1$sError%2$s: %3$s', 'lifterlms' ),\n\t\t\t\t\t\t'<strong>',\n\t\t\t\t\t\t'</strong>',\n\t\t\t\t\t\t$error_message\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// `WP_Error::get_all_error_data()` has been introduced in WP 5.6.0.\n\t\t\t$error_data = method_exists( $this->errors, 'get_all_error_data' ) ?\n\t\t\t\t\t$this->errors->get_all_error_data( $code ) : $this->errors->get_error_data( $code );\n\n\t\t\tforeach ( $error_data as $data ) {\n\t\t\t\t$errors->add_data( $data, $code );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get fields to be added in the profile screen\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_fields() {\n\n\t\tif ( ! isset( $this->fields ) ) {\n\t\t\t$this->fields = $this->prepare_fields();\n\t\t}\n\n\t\treturn $this->fields;\n\t}\n\n\t/**\n\t * Setup fields to be added to the profile screen\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function prepare_fields() {\n\n\t\t$fields   = llms_get_user_information_fields();\n\t\t$prepared = array();\n\n\t\t/**\n\t\t * Filters the list of user information fields which are excluded from the admin profile.\n\t\t *\n\t\t * By default WP core fields are excluded as they are automatically rendered on the screen\n\t\t * by the WP core.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string[] $fields A list of field ids to be excluded.\n\t\t */\n\t\t$excluded = apply_filters(\n\t\t\t'llms_admin_profile_excluded_fields',\n\t\t\tarray_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'user_login',\n\t\t\t\t\t'email_address',\n\t\t\t\t\t'password',\n\t\t\t\t\t'first_name',\n\t\t\t\t\t'last_name',\n\t\t\t\t\t'display_name',\n\t\t\t\t),\n\t\t\t\tLLMS_CONFIRMATION_FIELDS\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $fields as $field ) {\n\n\t\t\t// Skip excluded fields.\n\t\t\tif ( in_array( $field['name'], $excluded, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// For display purposes.\n\t\t\t$field['columns'] = 6;\n\n\t\t\t// Handle weird exception.\n\t\t\t$field['label'] = ( 'llms_billing_address_2' === $field['name'] ) ? __( 'Address line 2', 'lifterlms' ) : $field['label'];\n\n\t\t\t$prepared[] = $field;\n\n\t\t}\n\n\t\t/**\n\t\t * Fields to be added in the profile screen\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array[] $fields Array of fields.\n\t\t */\n\t\treturn apply_filters( 'llms_admin_profile_fields', $prepared );\n\t}\n}\n\nreturn new LLMS_Admin_Profile();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-review.php",
    "content": "<?php\n/**\n * LLMS_Admin_Review class file\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.24.0\n * @version 7.1.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin review request.\n *\n * Handles UI updates to the admin panel which request users to rate & review the\n * LifterLMS plugin on WordPress.org.\n *\n * Please say nice things about us.\n *\n * @since 3.24.0\n */\nclass LLMS_Admin_Review {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'admin_footer_text', array( $this, 'admin_footer' ), 1 );\n\t\tadd_action( 'admin_notices', array( $this, 'maybe_show_notice' ) );\n\n\t\tadd_action( 'wp_ajax_llms_review_dismiss', array( $this, 'dismiss' ) );\n\t}\n\n\t/**\n\t * On LifterLMS admin screens replace the default footer text with a review request.\n\t *\n\t * @since 3.24.0\n\t * @since 7.1.0 Show footer on our custom post types in admin, but not on the block editor.\n\t * @since 7.1.3 Using strpos instead of str_starts_with for compatibility.\n\t *\n\t * @param string $text Default footer text.\n\t * @return string\n\t */\n\tpublic function admin_footer( $text ) {\n\n\t\tglobal $current_screen;\n\n\t\t// Show footer on our custom post types in admin, but not on the block editor.\n\t\tif (\n\t\t\tisset( $current_screen->post_type ) &&\n\t\t\tin_array( $current_screen->post_type, array( 'course', 'lesson', 'llms_review', 'llms_membership', 'llms_engagement', 'llms_order', 'llms_coupon', 'llms_voucher', 'llms_form', 'llms_achievement', 'llms_my_achievement', 'llms_certificate', 'llms_my_certificate', 'llms_email' ), true ) &&\n\t\t\tfalse === $current_screen->is_block_editor\n\t\t) {\n\t\t\t$show_footer = true;\n\t\t}\n\n\t\t// Show footer on our settings pages.\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Recommended -- No nonce verification needed here\n\t\t// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- No sanitization needed here, we're not gonna use this value other than for checks\n\t\t// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- No unslash needed here, we're not gonna use this value other than for checks\n\t\tif (\n\t\t\t( ! empty( $_GET['page'] ) && strpos( $_GET['page'], 'llms-' ) === 0 ) ||\n\t\t\t( ! empty( $current_screen->id ) && strpos( $current_screen->id, 'lifterlms' ) === 0 )\n\t\t) {\n\t\t\t$show_footer = true;\n\t\t}\n\t\t// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized\n\t\t// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash\n\n\t\t// Exclude the wizard.\n\t\tif ( ! empty( $_GET['page'] ) && 'llms-setup' === $_GET['page'] ) {\n\t\t\t$show_footer = false;\n\t\t}\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Recommended\n\n\t\t// Don't show footer on the Course Builder.\n\t\tif ( isset( $current_screen->base ) && 'admin_page_llms-course-builder' === $current_screen->base ) {\n\t\t\t$show_footer = false;\n\t\t}\n\n\t\t// Conditionally filter footer text with our content.\n\t\tif ( ! empty( $show_footer ) ) {\n\n\t\t\t$url  = 'https://wordpress.org/support/plugin/lifterlms/reviews/#new-post';\n\t\t\t$text = sprintf(\n\t\t\t\twp_kses(\n\t\t\t\t\t/* Translators: %1$s = LifterLMS plugin name; %2$s = WP.org review link; %3$s = WP.org review link. */\n\t\t\t\t\t__( 'Please rate %1$s <a class=\"llms-rating-stars\" href=\"%2$s\" target=\"_blank\" rel=\"noopener noreferrer\">&#9733;&#9733;&#9733;&#9733;&#9733;</a> on <a href=\"%3$s\" target=\"_blank\" rel=\"noopener\">WordPress.org</a> to help us spread the word. Thank you from the LifterLMS team!', 'lifterlms' ),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'a' => array(\n\t\t\t\t\t\t\t'class'  => array(),\n\t\t\t\t\t\t\t'href'   => array(),\n\t\t\t\t\t\t\t'target' => array(),\n\t\t\t\t\t\t\t'rel'    => array(),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t'<strong>LifterLMS</strong>',\n\t\t\t\t$url,\n\t\t\t\t$url\n\t\t\t);\n\n\t\t}\n\n\t\treturn $text;\n\t}\n\n\t/**\n\t * AJAX callback for dismissing the notice\n\t *\n\t * @since 3.24.0\n\t * @since 4.14.0 Only users with `manager_lifterlms` caps can dismiss and added nonce verification.\n\t *               Use `llms_filter_input()` in favor of `filter_input()`.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function dismiss() {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) || ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'llms-admin-review-request-dismiss' ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$success = llms_parse_bool( llms_filter_input( INPUT_POST, 'success' ) );\n\n\t\tupdate_option(\n\t\t\t'llms_review',\n\t\t\tarray(\n\t\t\t\t'time'      => time(),\n\t\t\t\t'dismissed' => true,\n\t\t\t\t'success'   => $success ? 'yes' : 'no',\n\t\t\t)\n\t\t);\n\n\t\twp_die();\n\t}\n\n\t/**\n\t * Determine if the notice should be displayed and display it\n\t *\n\t * @since 3.24.0\n\t * @since 4.14.0 Only show to users with `manage_lifterlms` instead of only admins.\n\t *\n\t * @return null|false|void Returns `null` when there are permission issues, `false` when the notification is not set to be\n\t *                         displayed, and has no return when the notice is successfully displayed.\n\t */\n\tpublic function maybe_show_notice() {\n\n\t\t// Only show review request to admins.\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Verify that we can do a check for reviews.\n\t\t$review      = get_option( 'llms_review' );\n\t\t$time        = time();\n\t\t$enrollments = 0;\n\n\t\t// No review info stored, create a stub.\n\t\tif ( ! $review ) {\n\n\t\t\tupdate_option(\n\t\t\t\t'llms_review',\n\t\t\t\tarray(\n\t\t\t\t\t'time'      => $time,\n\t\t\t\t\t'dismissed' => false,\n\t\t\t\t)\n\t\t\t);\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// Review has not been dismissed and LifterLMS has been installed at least a week.\n\t\tif ( ( isset( $review['dismissed'] ) && ! $review['dismissed'] ) && isset( $review['time'] ) && ( $review['time'] + WEEK_IN_SECONDS <= $time ) ) {\n\n\t\t\t// Show if the enrollments threshold is reached.\n\t\t\tglobal $wpdb;\n\t\t\t$enrollments = $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_status' AND meta_value = 'enrolled'\" ); // no-cache ok.\n\n\t\t}\n\n\t\t// Only load if we have 30 or more enrollments.\n\t\t// This will be 0 if the review time/dismissed check above fails.\n\t\tif ( $enrollments < 30 ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$enrollments = self::round_down( $enrollments );\n\n\t\tinclude 'views/notices/review-request.php';\n\t}\n\n\t/**\n\t * Round a number down to a big-ish round number\n\t *\n\t * @since 3.24.0\n\t * @since 4.14.0 Numbers less than 10 are not rounded & numbers less than 100 are rounded to the nearest 10.\n\t *\n\t * @param int $number Input number.\n\t * @return int\n\t */\n\tpublic static function round_down( $number ) {\n\n\t\tif ( $number < 10 ) {\n\t\t\treturn $number;\n\t\t}\n\n\t\tif ( $number < 100 ) {\n\t\t\t$number = floor( $number / 10 ) * 10;\n\t\t} elseif ( $number < 1000 ) {\n\t\t\t$number = floor( $number / 100 ) * 100;\n\t\t} elseif ( $number < 10000 ) {\n\t\t\t$number = floor( $number / 1000 ) * 1000;\n\t\t} else {\n\t\t\t$number = 10000;\n\t\t}\n\n\t\treturn $number;\n\t}\n}\n\nreturn new LLMS_Admin_Review();\n"
  },
  {
    "path": "includes/admin/class-llms-admin-users-table.php",
    "content": "<?php\n/**\n * Manage WP Admin users table\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.34.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Users_Table class\n *\n * @since 3.34.0\n * @since 4.0.0 Add custom user table columns and action links.\n * @since 6.0.0 Removed the deprecated `LLMS_Admin_Users_Table::load_dependencies()` method.\n */\nclass LLMS_Admin_Users_Table {\n\n\t/**\n\t * Date/time format used to format last login timestamps\n\t *\n\t * This \"caches\" the data on the instance so that multiple requests\n\t * to get_option() / wp_cache_get() don't need to be made when outputting\n\t * a user table view.\n\t *\n\t * @var string\n\t */\n\tprotected $login_date_format = '';\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.34.0\n\t * @since 4.0.0 Add custom user table columns and action links.\n\t * @since 4.7.0 Remove `load_dependencies()` method hook.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'manage_users_columns', array( $this, 'add_cols' ) );\n\t\tadd_filter( 'manage_users_custom_column', array( $this, 'output_col' ), 10, 3 );\n\n\t\tadd_filter( 'users_list_table_query_args', array( $this, 'modify_query_args' ) );\n\t\tadd_filter( 'views_users', array( $this, 'modify_views' ) );\n\n\t\tadd_filter( 'user_row_actions', array( $this, 'add_actions' ), 20, 2 );\n\n\t}\n\n\t/**\n\t * Add custom actions links\n\t *\n\t * Outputs a \"Reports\" action link seen when hovering over a user in the table.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param string[] $actions Array of existing action links.\n\t * @param WP_User  $user    User object.\n\t * @return string[]\n\t */\n\tpublic function add_actions( $actions, $user ) {\n\t\t$url                       = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\tarray(\n\t\t\t\t'student_id' => $user->ID,\n\t\t\t)\n\t\t);\n\t\t$actions['llms-reporting'] = '<a href=\"' . esc_url( $url ) . '\">' . __( 'Reports', 'lifterlms' ) . '</a>';\n\t\treturn $actions;\n\n\t}\n\n\t/**\n\t * Add Custom Columns to the Admin Users Table Screen\n\t *\n\t * @param  array $columns key=>val array of existing columns\n\t *\n\t * @return array $columns updated columns\n\t */\n\tpublic function add_cols( $columns ) {\n\t\t$columns['llms-last-login']  = __( 'Last Login', 'lifterlms' );\n\t\t$columns['llms-enrollments'] = __( 'Enrollments', 'lifterlms' );\n\t\treturn $columns;\n\t}\n\n\t/**\n\t * Retrieve the date/time format used to display a user's last login.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_login_column_date_format() {\n\n\t\tif ( ! $this->login_date_format ) {\n\t\t\t$this->login_date_format = get_option( 'date_format', 'Y-m-d' ) . ' ' . get_option( 'time_format', ' h:i:s a' );\n\t\t}\n\n\t\treturn $this->login_date_format;\n\n\t}\n\n\t/**\n\t * Retrieves the output for the \"enrollments\" column.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param LLMS_Student $student Student object.\n\t * @return string\n\t */\n\tprotected function get_enrollments_column_output( $student ) {\n\n\t\t$info = array();\n\n\t\t$types = array(\n\t\t\t'courses'     => __( 'Courses', 'lifterlms' ),\n\t\t\t'memberships' => __( 'Memberships', 'lifterlms' ),\n\t\t);\n\n\t\tforeach ( $types as $type => $name ) {\n\n\t\t\t$url = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\tarray(\n\t\t\t\t\t'stab'       => $type,\n\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$query = call_user_func( array( $student, 'get_' . $type ), array( 'limit' => 1 ) );\n\n\t\t\t$info[] = sprintf( '%1$s: <a href=\"%2$s\">%3$d</a>', $name, esc_url( $url ), $query['found'] );\n\n\t\t}\n\n\t\treturn implode( '<br>', $info );\n\n\t}\n\n\t/**\n\t * Modify the query arguments of the users table query.\n\t *\n\t * If the current user is an instructor and no `role` argument is provided will limit the query to users\n\t * with the `instructors_assistant` and `instructor` roles.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @param array $args Array of arguments to be passed to a WP_User_Query.\n\t * @return array\n\t */\n\tpublic function modify_query_args( $args ) {\n\n\t\tif ( LLMS_User_Permissions::is_current_user_instructor() && empty( $args['role'] ) ) {\n\t\t\t$args['role__in'] = array( 'instructors_assistant', 'instructor' );\n\t\t}\n\n\t\treturn $args;\n\t}\n\n\t/**\n\t * Modify the list of role \"view\" filter links at the top of the user table.\n\t *\n\t * An instructor can only manage instructors and instructor's assistants so we'll remove these links from the list\n\t * and additionally modify the count on the \"All\" filter to reflect the total number of users who are visible\n\t * to the current instructor.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @param array $views Associative array of views where the key is the role name and the value is the HTML for the view link.\n\t * @return array\n\t */\n\tpublic function modify_views( $views ) {\n\n\t\tif ( LLMS_User_Permissions::is_current_user_instructor() ) {\n\n\t\t\t$users = count_users();\n\n\t\t\t// Allow the instructor to see roles they're allowed to edit.\n\t\t\t$all_roles = LLMS_User_Permissions::get_editable_roles();\n\t\t\t$roles     = array_merge( array( 'all', 'instructor' ), $all_roles['instructor'] );\n\n\t\t\t$all = 0;\n\n\t\t\tforeach ( array_keys( $views ) as $view ) {\n\t\t\t\tif ( ! in_array( $view, $roles, true ) ) {\n\t\t\t\t\t// Unset any views they're not allowed to edit.\n\t\t\t\t\tunset( $views[ $view ] );\n\t\t\t\t} elseif ( ! empty( $users['avail_roles'][ $view ] ) ) {\n\t\t\t\t\t// Add roles they're allowed to view to the new all count.\n\t\t\t\t\t$all += $users['avail_roles'][ $view ];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Replace the count on the \"All\" link with our updated count.\n\t\t\t$format       = '<span class=\"count\">(%s)</span>';\n\t\t\t$views['all'] = str_replace( sprintf( $format, $users['total_users'] ), sprintf( $format, $all ), $views['all'] );\n\n\t\t}\n\n\t\treturn $views;\n\t}\n\n\t/**\n\t * Register custom columns\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param string $output   Column output value to display (defaults to empty).\n\t * @param string $col_name Column name/id.\n\t * @param int    $user_id  WP_User ID.\n\t * @return string\n\t */\n\tpublic function output_col( $output, $col_name, $user_id ) {\n\n\t\tswitch ( $col_name ) {\n\n\t\t\tcase 'llms-enrollments':\n\t\t\t\t$student = llms_get_student( $user_id );\n\t\t\t\tif ( $student ) {\n\t\t\t\t\t$output = $this->get_enrollments_column_output( $student );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms-last-login':\n\t\t\t\t$last   = get_user_meta( $user_id, 'llms_last_login', true );\n\t\t\t\t$last   = is_numeric( $last ) ? $last : strtotime( $last );\n\t\t\t\t$output = $last ? date_i18n( $this->get_login_column_date_format(), $last ) : __( 'Never', 'lifterlms' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $output;\n\n\t}\n\n}\n\nreturn new LLMS_Admin_Users_Table();\n"
  },
  {
    "path": "includes/admin/class-llms-export-api.php",
    "content": "<?php\n/**\n * Manage imports from lifterlms.com export API\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 4.8.0\n * @version 4.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Export API Class\n *\n * @since 4.8.0\n */\nclass LLMS_Export_API {\n\n\t/**\n\t * Make an GET request to the exports API\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param array $args Array of query string arguments formatted as an associative array.\n\t * @return array|WP_Error\n\t */\n\tprotected static function call_api( $args ) {\n\n\t\t/**\n\t\t * Filter the url used to make requests to the LifterLMS.com \"exports\" api.\n\t\t *\n\t\t * @since 4.8.0\n\t\t *\n\t\t * @param string $url API request url.\n\t\t */\n\t\t$base_url = apply_filters(\n\t\t\t'llms_export_api_url',\n\t\t\t'https://academy.lifterlms.com/wp-json/llms-academy/v1/exports'\n\t\t);\n\n\t\t$req  = wp_safe_remote_get(\n\t\t\tadd_query_arg( $args, $base_url ),\n\t\t\tarray(\n\t\t\t\t'timeout' => 15,\n\t\t\t)\n\t\t);\n\t\t$body = json_decode( wp_remote_retrieve_body( $req ), true );\n\t\tif ( 200 === wp_remote_retrieve_response_code( $req ) ) {\n\t\t\treturn $body;\n\t\t}\n\n\t\t// If there's a body it's a json encoded error object, otherwise it's already an error object.\n\t\treturn $body && ! empty( $body['code'] ) ? new WP_Error( $body['code'], $body['message'], $body['data'] ) : $req;\n\n\t}\n\n\t/**\n\t * Retrieve an import array by export IDs.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param int[] $ids Array of export IDs.\n\t * @return array|WP_Error\n\t */\n\tpublic static function get( $ids ) {\n\n\t\t$ids = implode( ',', array_map( 'absint', $ids ) );\n\t\treturn self::call_api( compact( 'ids' ) );\n\n\t}\n\n\n\t/**\n\t * Retrieve a list of available exports\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param int $page     Results page.\n\t * @param int $per_page Results per page.\n\t * @return array[]|WP_Error\n\t */\n\tpublic static function list( $page = 1, $per_page = 10 ) {\n\t\treturn self::call_api( compact( 'page', 'per_page' ) );\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/class-llms-mailhawk.php",
    "content": "<?php\n/**\n * MailHawk Connect\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.40.0\n * @version 3.40.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_MailHawk class\n *\n * @since 3.40.0\n */\nclass LLMS_MailHawk extends LLMS_Abstract_Email_Provider {\n\n\t/**\n\t * LifterLMS MailHawk Partner ID.\n\t *\n\t * @var int\n\t */\n\tconst PARTNER_ID = 3;\n\n\t/**\n\t * Connector's ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'mailhawk';\n\n\t/**\n\t * Configures the response returned when `do_remote_install()` is successful.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return array\n\t */\n\tprotected function do_remote_install_success() {\n\t\treturn array(\n\t\t\t'partner_id'   => self::PARTNER_ID,\n\t\t\t'register_url' => esc_url( trailingslashit( MAILHAWK_LICENSE_SERVER_URL ) ),\n\t\t\t'client_state' => esc_html( \\MailHawk\\Keys::instance()->state() ),\n\t\t\t'redirect_uri' => esc_url( \\MailHawk\\mailhawk_admin_page() ),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the settings area HTML for the connect button\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_connect_setting() {\n\n\t\tif ( $this->is_connected() ) {\n\n\t\t\t$ret = array(\n\t\t\t\t__( 'Your site is connected to MailHawk.', 'lifterlms' ),\n\t\t\t);\n\n\t\t\t$settings_url = esc_url( admin_url( '/tools.php?page=mailhawk' ) );\n\n\t\t\tif ( function_exists( '\\MailHawk\\mailhawk_is_suspended' ) && ! \\MailHawk\\mailhawk_is_suspended() ) {\n\t\t\t\t$ret[] = sprintf(\n\t\t\t\t\t// Translators: %1$s = Opening anchor tag to WP MailHawk Settings; Opening anchor tag to MailHawk.io account page; %2$s = Closing anchor tag.\n\t\t\t\t\t__( '%1$sView settings%3$s or %2$smanage your account%3$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . $settings_url . '\">',\n\t\t\t\t\t'<a href=\"https://mailhawk.io/account/\" target=\"_blank\" rel=\"noopener noreferrer\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t$ret[] = sprintf(\n\t\t\t\t\t// Translators: %1$s = Opening anchor tag; %2$s = Closing anchor tag.\n\t\t\t\t\t'<em>' . __( 'Email sending is currently disabled. %1$sVisit MailHawk Settings%2$s to enable sending.', 'lifterlms' ) . '</em>',\n\t\t\t\t\t'<a href=\"' . $settings_url . '\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn '<p>' . implode( ' ', $ret ) . '</p>';\n\n\t\t}\n\n\t\treturn sprintf( '<button type=\"button\" class=\"llms-button-outline\" id=\"llms-mailhawk-connect\"><span class=\"dashicons dashicons-email-alt\"></span> %s</button>', __( 'Connect MailHawk', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Retrieve description text to be used in the settings area.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\n\t\treturn sprintf(\n\t\t\t// Translators: %s = Anchor tag html linking to MailHawk.io.\n\t\t\t__( 'Never worry about sending email again. %s takes care of everything for you starting for a small monthly fee.', 'lifterlms' ),\n\t\t\t'<a href=\"https://lifterlikes.com/mailhawk\" target=\"_blank\">' . $this->get_title() . '</a>'\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the connector's name / title.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_title() {\n\t\treturn __( 'MailHawk', 'lifterlms' );\n\t}\n\n\t/**\n\t * Determines if MailHawk is installed and connected for sending.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function is_connected() {\n\t\treturn ( function_exists( '\\MailHawk\\mailhawk_is_connected' ) && \\MailHawk\\mailhawk_is_connected() );\n\t}\n\n\t/**\n\t * Determines if connector plugin is installed\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function is_installed() {\n\t\treturn function_exists( '\\MailHawk\\mailhawk_admin_page' );\n\t}\n\n\t/**\n\t * Output some quick and dirty inline JS.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_js( $additional_js = '' ) {\n\n\t\tif ( ! $this->should_output_inline() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t?>\n\t\t<script>\n\t\t\tjQuery( '#llms-mailhawk-connect' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\tLLMS.Spinner.start( jQuery( this ), 'small' );\n\n\t\t\t\tvar data = {\n\t\t\t\t\taction: 'llms_mailhawk_remote_install',\n\t\t\t\t\t_llms_mailhawk_nonce: '<?php echo esc_js( wp_create_nonce( 'llms-mailhawk-install' ) ); ?>',\n\t\t\t\t};\n\n\t\t\t\twindow.llms.emailConnectors.remoteInstall( jQuery( this ), data, function( res ) {\n\n\t\t\t\t\twindow.llms.emailConnectors.registerClient( res.register_url, {\n\t\t\t\t\t\t'mailhawk_plugin_signup': 'yes',\n\t\t\t\t\t\t'state': res.client_state,\n\t\t\t\t\t\t'redirect_uri': res.redirect_uri,\n\t\t\t\t\t\t'partner_id': res.partner_id\n\t\t\t\t\t} );\n\n\t\t\t\t} );\n\n\t\t\t} );\n\t\t</script>\n\t\t<?php\n\t}\n}\n\nreturn new LLMS_MailHawk();\n"
  },
  {
    "path": "includes/admin/class-llms-sendwp.php",
    "content": "<?php\n/**\n * SendWP Connect\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.36.1\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_SendWP class\n *\n * @since 3.36.1\n * @since 3.37.0 Sanitize URLs, clean up jQuery references, add loading feedback when connector button is clicked.\n * @since 3.37.3 Modify the ID used to determine where to splice in SendWP Options.\n * @since 3.40.0 Refactor to utilize `LLMS_Abstract_Email_Provider`.\n * @since 6.0.0 Removed `LLMS_SendWP::do_remote_install()` in favor of `LLMS_Abstract_Email_Provider::do_remote_install()`.\n */\nclass LLMS_SendWP extends LLMS_Abstract_Email_Provider {\n\n\t/**\n\t * LifterLMS SendWP Partner ID.\n\t *\n\t * @var int\n\t */\n\tconst PARTNER_ID = 2007;\n\n\t/**\n\t * Connector's ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'sendwp';\n\n\t/**\n\t * Configures the response returned when `do_remote_install()` is successful.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return array\n\t */\n\tprotected function do_remote_install_success() {\n\t\treturn array(\n\t\t\t'partner_id'      => self::PARTNER_ID,\n\t\t\t'register_url'    => esc_url( sendwp_get_server_url() . '_/signup' ),\n\t\t\t'client_name'     => esc_url( sendwp_get_client_name() ),\n\t\t\t'client_secret'   => esc_url( sendwp_get_client_secret() ),\n\t\t\t'client_redirect' => esc_url( sendwp_get_client_redirect() ),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve description text to be used in the settings area.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\n\t\treturn sprintf(\n\t\t\t// Translators: %s = Anchor tag html linking to SendWP.com.\n\t\t\t__( '%s makes WordPress email delivery as simple as a few clicks so you can relax, knowing your important emails are being delivered on time.', 'lifterlms' ),\n\t\t\t'<a href=\"https://lifterlikes.com/sendwp\" target=\"_blank\" rel=\"noopener noreferrer\">' . $this->get_title() . '</a>'\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the connector's name / title.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_title() {\n\t\treturn __( 'SendWP', 'lifterlms' );\n\t}\n\n\t/**\n\t * Determine if SendWP is installed and connected for sending.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function is_connected() {\n\t\treturn ( function_exists( 'sendwp_client_connected' ) && sendwp_client_connected() );\n\t}\n\n\t/**\n\t * Determines if connector plugin is installed\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function is_installed() {\n\t\treturn function_exists( 'sendwp_get_server_url' );\n\t}\n\n\t/**\n\t * Get the \"Connect\" Setting field html.\n\t *\n\t * @since 3.36.1\n\t * @since 3.40.0 Abstract methods used to determine if SendWP is connected.\n\t * @since 5.3.2 Update the URL for managing an account.\n\t *\n\t * @return string\n\t */\n\tprotected function get_connect_setting() {\n\n\t\tif ( $this->is_connected() ) {\n\n\t\t\t$ret = array(\n\t\t\t\t__( 'Your site is connected to SendWP.', 'lifterlms' ),\n\t\t\t);\n\n\t\t\tif ( function_exists( 'sendwp_forwarding_enabled' ) && sendwp_forwarding_enabled() ) {\n\t\t\t\t$ret[] = sprintf(\n\t\t\t\t\t// Translators: %1$s = Opening anchor tag; %2$s = Closing anchor tag.\n\t\t\t\t\t__( '%1$sManage your account%2$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"https://app.sendwp.com/dashboard\" target=\"_blank\" rel=\"noopener noreferrer\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t$ret[] = sprintf(\n\t\t\t\t\t// Translators: %1$s = Opening anchor tag; %2$s = Closing anchor tag.\n\t\t\t\t\t'<em>' . __( 'Email sending is currently disabled. %1$sVisit the SendWP Settings%2$s to enable sending..', 'lifterlms' ) . '</em>',\n\t\t\t\t\t'<a href=\"' . admin_url( '/tools.php?page=sendwp' ) . '\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn '<p>' . implode( ' ', $ret ) . '</p>';\n\n\t\t}\n\n\t\treturn '<button class=\"llms-button-outline\" id=\"llms-sendwp-connect\"><i class=\"fa fa-paper-plane-o\" aria-hidden=\"true\"></i> Connect SendWP</button>';\n\t}\n\n\t/**\n\t * Output some quick and dirty inline JS.\n\t *\n\t * @since 3.36.1\n\t * @since 3.37.0 Add nonce and replace references to `$` with `jQuery`.\n\t * @since 3.40.0 Refactored to utilize `window.llms.emailConnectors`.\n\t *\n\t * @return void\n\t */\n\tpublic function output_js() {\n\n\t\tif ( ! $this->should_output_inline() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t?>\n\t\t<script>\n\t\t\tjQuery( '#llms-sendwp-connect' ).on( 'click', function( e ) {\n\n\t\t\t\te.preventDefault();\n\n\t\t\t\tLLMS.Spinner.start( jQuery( this ), 'small' );\n\n\t\t\t\tvar data = {\n\t\t\t\t\taction: 'llms_sendwp_remote_install',\n\t\t\t\t\t_llms_sendwp_nonce: '<?php echo esc_js( wp_create_nonce( 'llms-sendwp-install' ) ); ?>',\n\t\t\t\t};\n\n\t\t\t\twindow.llms.emailConnectors.remoteInstall( jQuery( this ), data, function( res ) {\n\n\t\t\t\t\twindow.llms.emailConnectors.registerClient( res.register_url, {\n\t\t\t\t\t\tclient_name: res.client_name,\n\t\t\t\t\t\tclient_secret: res.client_secret,\n\t\t\t\t\t\tclient_redirect: res.client_redirect,\n\t\t\t\t\t\tpartner_id: res.partner_id,\n\t\t\t\t\t} );\n\n\t\t\t\t} );\n\n\t\t\t} );\n\t\t</script>\n\t\t<?php\n\t}\n}\n\nreturn new LLMS_SendWP();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.addons.php",
    "content": "<?php\n/**\n * LifterLMS Add-On browser\n *\n * This is where the adds are, if you don't like it that's okay but i don't want to hear your complaints!\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.5.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_AddOns class\n *\n * @since 3.5.0\n * @since 3.30.3 Explicitly define undefined properties.\n * @since 3.35.0 Sanitize input data.\n */\nclass LLMS_Admin_AddOns {\n\n\t/**\n\t * Data from `llms_get_add_ons()`.\n\t *\n\t * @var array\n\t * @since 3.5.0\n\t */\n\tpublic $data = array();\n\n\t/**\n\t * Retrieves the current section from the query string.\n\t *\n\t * @since 3.5.0\n\t * @since 3.22.0 Unknown.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string\n\t */\n\tprivate function get_current_section() {\n\n\t\t$section = 'all';\n\n\t\tif ( isset( $_GET['page'] ) && 'llms-dashboard' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\t$section = 'featured';\n\t\t} elseif ( isset( $_GET['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\t$section = llms_filter_input_sanitize_string( INPUT_GET, 'section' );\n\t\t}\n\n\t\treturn apply_filters( 'llms_admin_add_ons_get_current_section', $section );\n\t}\n\n\t/**\n\t * Retrieve addon data for the current section (tab) based off query string variables\n\t *\n\t * @return   array\n\t * @since    3.5.0\n\t * @version  3.22.0\n\t */\n\tprivate function get_current_section_content() {\n\n\t\t$sec = $this->get_current_section();\n\n\t\t$content = apply_filters( 'llms_admin_add_ons_get_current_section_default_content', array(), $sec );\n\n\t\tif ( ! $content ) {\n\n\t\t\tif ( 'all' === $sec ) {\n\t\t\t\t$content = $this->get_all();\n\t\t\t} elseif ( 'featured' === $sec ) {\n\t\t\t\t$content = $this->get_features();\n\t\t\t} else {\n\t\t\t\t$content = $this->get_products_for_cat( $sec );\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_admin_add_ons_get_current_section_content', $content, $sec );\n\t}\n\n\t/**\n\t * Retrieve remote json data.\n\t *\n\t * @since 3.5.0\n\t * @since 3.22.2 Unknown.\n\t * @since 7.1.0 Use strict comparisons for `in_array()`.\n\t *\n\t * @return array|WP_Error\n\t */\n\tprivate function get_data() {\n\n\t\t$this->data = llms_get_add_ons();\n\n\t\tif ( ! is_wp_error( $this->data ) ) {\n\t\t\tforeach ( $this->data['items'] as $key => $addon ) {\n\t\t\t\t// Exclude the core plugin and helper plugin.\n\t\t\t\tif ( in_array( $addon['id'], array( 'lifterlms-com-lifterlms', 'lifterlms-com-lifterlms-helper' ), true ) ) {\n\t\t\t\t\tunset( $this->data['items'][ $key ] );\n\t\t\t\t}\n\n\t\t\t\t// Exclude uncategorized Add-ons.\n\t\t\t\tif ( array_key_exists( 'uncategorized', $addon['categories'] ) ) {\n\t\t\t\t\tunset( $this->data['items'][ $key ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $this->data;\n\t}\n\n\t/**\n\t * Retrieve a list of addons for use on the All section\n\t *\n\t * @return   array\n\t * @since    7.5.0\n\t * @version  7.5.0\n\t */\n\tprivate function get_all() {\n\n\t\t$all = array();\n\n\t\t$addons = $this->data['items'];\n\n\t\tforeach ( $addons as $addon ) {\n\t\t\t// Exclude third-party Addons from the All section.\n\t\t\tif ( in_array( 'third-party', array_keys( $addon['categories'] ), true ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$all[] = $addon;\n\t\t}\n\n\t\treturn $all;\n\t}\n\n\t/**\n\t * Retrieve a list of 'featured' addons for use on the general settings screen\n\t * Excludes already available products from current site's activations\n\t *\n\t * @return   array\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tprivate function get_features() {\n\n\t\t$features = array();\n\n\t\t// Addons to exclude.\n\t\t// Helper will filter this based on existing activations.\n\t\t$exclude = apply_filters( 'llms_admin_addon_features_exclude_ids', array() );\n\n\t\t$cats = array(\n\t\t\t'e-commerce',\n\t\t\t'bundles',\n\t\t\t'resources',\n\t\t\t'courses',\n\t\t\t'courses',\n\t\t);\n\n\t\tforeach ( $cats as $cat ) {\n\t\t\t$addon = $this->get_product_from_cat( $cat, $exclude );\n\t\t\tif ( $addon ) {\n\t\t\t\t$features[] = $addon;\n\t\t\t\t$exclude[]  = $addon['id'];\n\t\t\t}\n\t\t\tif ( 3 === count( $features ) ) {\n\t\t\t\treturn $features;\n\t\t\t}\n\t\t}\n\n\t\treturn $features;\n\t}\n\n\t/**\n\t * Get a random product from a category that doesn't exist in the list of excluded product ids.\n\t *\n\t * @since 3.22.0\n\t * @since 7.1.0 Use strict comparisons for `in_array()`.\n\t *\n\t * @param  string $cat      Category slug.\n\t * @param  array  $excludes List of product ids to exclude.\n\t * @return array|false\n\t */\n\tpublic function get_product_from_cat( $cat, $excludes ) {\n\n\t\t$addons = $this->get_products_for_cat( $cat, true );\n\t\tshuffle( $addons );\n\n\t\tforeach ( $addons as $addon ) {\n\n\t\t\tif ( in_array( 'third-party', array_keys( $addon['categories'] ), true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( ! in_array( $addon['id'], $excludes, true ) ) {\n\t\t\t\treturn $addon;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve products for a specific category.\n\t *\n\t * @since 3.22.0\n\t * @since 7.1.0 Use strict comparisons for `in_array()`.\n\t *\n\t * @param string $cat Category slug.\n\t * @return array\n\t */\n\tprivate function get_products_for_cat( $cat, $include_bundles = true ) {\n\n\t\t$products = array();\n\n\t\tforeach ( $this->data['items'] as $item ) {\n\n\t\t\t$cats = array_keys( $item['categories'] );\n\n\t\t\t// Exclude bundles if bundles are not being included or requested.\n\t\t\tif ( 'bundles' !== $cat && ! $include_bundles && in_array( 'bundles', $cats, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( in_array( $cat, $cats, true ) ) {\n\t\t\t\t$products[] = $item;\n\t\t\t}\n\t\t}\n\n\t\treturn $products;\n\t}\n\n\t/**\n\t * Handle form submissions for managing license keys\n\t *\n\t * @return   void\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tpublic function handle_actions() {\n\n\t\t// Activate & deactivate addons.\n\t\tif ( isset( $_REQUEST['_llms_manage_addon_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_manage_addon_nonce'] ) ), 'llms_manage_addon' ) ) {\n\n\t\t\t$this->handle_manage_addons();\n\t\t\tLLMS_Admin_Notices::output_notices();\n\t\t}\n\t}\n\n\t/**\n\t * Handle activation and deactivation of addons\n\t *\n\t * @since 3.22.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tprivate function handle_manage_addons() {\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce is verified in $this->handle_actions() method.\n\n\t\t$actions = apply_filters(\n\t\t\t'llms_admin_add_ons_manage_actions',\n\t\t\tarray(\n\t\t\t\t'activate',\n\t\t\t\t'deactivate',\n\t\t\t)\n\t\t);\n\n\t\t$errors  = array();\n\t\t$success = array();\n\n\t\tforeach ( $actions as $action ) {\n\n\t\t\tif ( empty( $_POST[ 'llms_' . $action ] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tforeach ( llms_filter_input( INPUT_POST, 'llms_' . $action, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ) as $id ) {\n\n\t\t\t\t$addon = llms_get_add_on( $id );\n\t\t\t\tif ( ! method_exists( $addon, $action ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$ret = call_user_func( array( $addon, $action ) );\n\t\t\t\tif ( is_wp_error( $ret ) ) {\n\t\t\t\t\tLLMS_Admin_Notices::flash_notice( $ret->get_error_message(), 'error' );\n\t\t\t\t} else {\n\t\t\t\t\tLLMS_Admin_Notices::flash_notice( $ret );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Missing\n\t}\n\n\t/**\n\t * Output HTML for the current screen\n\t *\n\t * @since 3.5.0\n\t * @since 3.28.0 Unknown.\n\t * @since 4.10.1 Use `hr.wp-header-end` in favor of a second (hidden) <h1> to \"catch\" admin notices.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tif ( is_wp_error( $this->get_data() ) ) {\n\t\t\tesc_html_e( 'There was an error retrieving add-ons. Please try again.', 'lifterlms' );\n\t\t\treturn;\n\t\t}\n\t\t?>\n\t\t<div class=\"wrap lifterlms lifterlms-settings lifterlms-addons\">\n\n\t\t\t<div class=\"llms-subheader\">\n\n\t\t\t\t<h1><?php esc_html_e( 'LifterLMS Add-Ons, Courses, and Resources', 'lifterlms' ); ?></h1>\n\t\t\t\t<?php do_action( 'llms_addons_page_after_title' ); ?>\n\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-inside-wrap\">\n\n\t\t\t\t<?php $this->output_navigation(); ?>\n\n\t\t\t\t<hr class=\"wp-header-end\">\n\n\t\t\t\t<form action=\"\" method=\"POST\">\n\n\t\t\t\t\t<?php $this->output_content(); ?>\n\n\t\t\t\t\t<?php wp_nonce_field( 'llms_manage_addon', '_llms_manage_addon_nonce' ); ?>\n\n\t\t\t\t\t<div class=\"llms-addons-bulk-actions\" id=\"llms-addons-bulk-actions\">\n\n\t\t\t\t\t\t<a class=\"llms-bulk-close\" href=\"#\">\n\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Close', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<i class=\"fa fa-times-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t</a>\n\n\t\t\t\t\t\t<div class=\"llms-bulk-desc update\">\n\t\t\t\t\t\t\t<i class=\"fa fa-cloud-download\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t<?php esc_html_e( 'Update', 'lifterlms' ); ?> <span></span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"llms-bulk-desc install\">\n\t\t\t\t\t\t\t<i class=\"fa fa-cloud-download\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t<?php esc_html_e( 'Install', 'lifterlms' ); ?> <span></span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"llms-bulk-desc activate\">\n\t\t\t\t\t\t\t<i class=\"fa fa-plug\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t<?php esc_html_e( 'Activate', 'lifterlms' ); ?> <span></span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"llms-bulk-desc deactivate\">\n\t\t\t\t\t\t\t<i class=\"fa fa-plug\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t<?php esc_html_e( 'Deactivate', 'lifterlms' ); ?> <span></span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<button class=\"llms-button-primary\" name=\"llms_bulk_actions_submit\" value=\"\" type=\"submit\"><?php esc_html_e( 'Apply', 'lifterlms' ); ?></button>\n\n\t\t\t\t\t</div>\n\n\t\t\t\t</form>\n\n\t\t\t</div>\n\n\t\t</div>\n\n\t\t<?php\n\t}\n\n\t/**\n\t * Output HTML for a single addon\n\t *\n\t * @param    array $addon  associative array of add-on data\n\t * @return   void\n\t * @since    3.5.0\n\t * @version  3.22.0\n\t */\n\tprivate function output_addon( $addon ) {\n\t\t$current_tab = $this->get_current_section();\n\t\tinclude 'views/addons/addon-item.php';\n\t}\n\n\t/**\n\t * Output the addon list for the current section\n\t *\n\t * @return   void\n\t * @since    3.5.0\n\t * @version  3.22.0\n\t */\n\tprivate function output_content() {\n\t\t?>\n\t\t<ul class=\"llms-addons-wrap section--<?php echo esc_attr( $this->get_current_section() ); ?>\">\n\n\t\t\t<?php do_action( 'lifterlms_before_addons' ); ?>\n\n\t\t\t<?php\n\t\t\tforeach ( $this->get_current_section_content() as $addon ) {\n\t\t\t\t$addon = llms_get_add_on( $addon );\n\t\t\t\t$this->output_addon( $addon );\n\t\t\t}\n\t\t\t?>\n\n\t\t\t<?php do_action( 'lifterlms_after_addons' ); ?>\n\n\t\t</ul>\n\t\t<?php\n\t}\n\n\t/**\n\t * Outputs most popular resources\n\t * used on general settings screen\n\t *\n\t * @return   void\n\t * @since    3.7.6\n\t * @version  3.7.6\n\t */\n\tpublic function output_for_settings() {\n\n\t\tif ( is_wp_error( $this->get_data() ) ) {\n\n\t\t\tesc_html_e( 'There was an error retrieving add-ons. Please try again.', 'lifterlms' );\n\t\t\treturn;\n\n\t\t}\n\t\t$this->output_content();\n\t}\n\n\t/**\n\t * Output the navigation bar\n\t *\n\t * @return   void\n\t * @since    3.5.0\n\t * @version  3.22.0\n\t */\n\tprivate function output_navigation() {\n\t\t$curr_section = $this->get_current_section();\n\t\t?>\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t<?php do_action( 'lifterlms_before_addons_nav', $curr_section ); ?>\n\t\t\t\t<?php\n\t\t\t\tforeach ( $this->data['categories'] as $name => $title ) :\n\t\t\t\t\t$name   = sanitize_title( $name );\n\t\t\t\t\t$title  = sanitize_text_field( $title );\n\t\t\t\t\t$active = ( $this->get_current_section() === $name ) ? ' llms-active' : '';\n\t\t\t\t\t?>\n\t\t\t\t\t<li class=\"llms-nav-item<?php echo esc_attr( $active ); ?>\"><a class=\"llms-nav-link\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-add-ons&section=' . $name ) ); ?>\"><?php echo esc_html( $title ); ?></a></li>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( 'all' === $curr_section ) ? ' llms-active' : ''; ?>\"><a class=\"llms-nav-link\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-add-ons&section=all' ) ); ?>\"><?php esc_html_e( 'All', 'lifterlms' ); ?></a></li>\n\n\t\t\t<?php do_action( 'lifterlms_after_addons_nav', $curr_section ); ?>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<?php\n\t}\n}\n"
  },
  {
    "path": "includes/admin/class.llms.admin.assets.php",
    "content": "<?php\n/**\n * LLMS_Admin_Assets class\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 1.0.0\n * @version 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Register and enqueue admin assets.\n *\n * @since 1.0.0\n */\nclass LLMS_Admin_Assets {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.5 Unknown.\n\t * @since 6.0.0 Add hooks for admin inline footer scripts, inline header styles, and block editor assets.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );\n\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );\n\t\tadd_action( 'wp_enqueue_media', array( $this, 'admin_media' ) );\n\t\tadd_action( 'admin_print_styles', array( $this, 'admin_print_styles' ) );\n\t\tadd_action( 'admin_print_scripts', array( $this, 'admin_print_scripts' ) );\n\t\tadd_action( 'admin_print_footer_scripts', array( $this, 'admin_print_footer_scripts' ) );\n\t\tadd_action( 'enqueue_block_editor_assets', array( $this, 'block_editor_assets' ) );\n\t\tadd_action( 'elementor/editor/before_enqueue_scripts', array( $this, 'elementor_editor_assets' ) );\n\t}\n\n\t/**\n\t * Output inline scripts in the admin footer.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function admin_print_footer_scripts() {\n\t\tllms()->assets->output_inline( 'footer' );\n\t}\n\n\t/**\n\t * Output inline styles in the header.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function admin_print_styles() {\n\t\tllms()->assets->output_inline( 'style' );\n\t}\n\n\t/**\n\t * Enqueue assets for the block editor.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function block_editor_assets() {\n\n\t\t$screen = get_current_screen();\n\t\tif ( $screen && $screen->is_block_editor && in_array( $screen->post_type, array( 'llms_certificate', 'llms_my_certificate' ), true ) ) {\n\t\t\t$this->block_editor_assets_for_certificates();\n\t\t}\n\n\t\tif ( $screen && $screen->is_block_editor && current_user_can( 'edit_courses' ) ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-admin-media-protection-block-protect' );\n\t\t}\n\t}\n\n\tpublic function elementor_editor_assets() {\n\t\tif ( isset( $_REQUEST['post'] ) && is_numeric( $_REQUEST['post'] ) && 'course' === get_post_type( intval( $_REQUEST['post'] ) ) ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-admin-elementor-editor' );\n\t\t\twp_localize_script( 'llms-admin-elementor-editor', 'llms_elementor', array( 'builder_url' => admin_url( 'admin.php?page=llms-course-builder&course_id=' . intval( $_REQUEST['post'] ) ) ) );\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue block editor assets for certificate post types.\n\t *\n\t * @since 6.0.0\n\t * @since 6.5.0 Use `wp_slash()` after `wp_json_encode()` to prevent issues encountered when strings contain single quotes.\n\t *\n\t * @return void\n\t */\n\tprivate function block_editor_assets_for_certificates() {\n\n\t\tllms()->assets->enqueue_script( 'llms-admin-certificate-editor' );\n\n\t\t$settings = array(\n\t\t\t'default_image' => llms()->certificates()->get_default_image( get_the_ID() ),\n\t\t\t'sizes'         => llms_get_certificate_sizes(),\n\t\t\t'orientations'  => llms_get_certificate_orientations(),\n\t\t\t'units'         => llms_get_certificate_units(),\n\t\t\t'colors'        => array(\n\t\t\t\tarray(\n\t\t\t\t\t'name'  => __( 'White', 'lifterlms' ),\n\t\t\t\t\t'slug'  => 'white',\n\t\t\t\t\t'color' => '#ffffff',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'name'  => __( 'White Smoke', 'lifterlms' ),\n\t\t\t\t\t'slug'  => 'white-smoke',\n\t\t\t\t\t'color' => '#f5f5f5',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'name'  => __( 'Ivory', 'lifterlms' ),\n\t\t\t\t\t'slug'  => 'ivory',\n\t\t\t\t\t'color' => '#fffff0',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'merge_codes'   => llms_get_certificate_merge_codes(),\n\t\t);\n\t\tllms()->assets->enqueue_inline(\n\t\t\t'llms-admin-certificate-settings',\n\t\t\t\"window.llms = window.llms || {};window.llms.certificates=JSON.parse( '\" . wp_slash( wp_json_encode( $settings ) ) . \"' );\",\n\t\t\t'footer'\n\t\t);\n\n\t\tglobal $wp_version;\n\t\t$supports_fonts = version_compare( $wp_version, '5.9-src', '>=' );\n\n\t\t$fonts = $supports_fonts ? llms_get_certificate_fonts() : new stdClass();\n\n\t\t$styles = '';\n\t\tforeach ( $fonts as $id => $data ) {\n\n\t\t\tif ( ! empty( $data['href'] ) ) {\n\t\t\t\twp_enqueue_style( 'llms-font-' . $id, $data['href'], array(), LLMS_VERSION );\n\t\t\t}\n\n\t\t\t$css     = $data['fontFamily'];\n\t\t\t$styles .= \".editor-styles-wrapper .has-{$id}-font-family { font-family: {$css} !important }\\n\";\n\t\t}\n\n\t\tllms()->assets->enqueue_inline(\n\t\t\t'llms-admin-certificate-styles',\n\t\t\t$styles,\n\t\t\t'style'\n\t\t);\n\t}\n\n\t/**\n\t * Determine if the current screen should load LifterLMS assets\n\t *\n\t * @since 3.7.0\n\t * @since 3.19.4 Unknown.\n\t *\n\t * @return bool\n\t */\n\tpublic function is_llms_page() {\n\n\t\t$screen = get_current_screen();\n\n\t\t$id = str_replace( 'edit-', '', $screen->id );\n\n\t\tif ( false !== strpos( $id, 'lifterlms' ) ) {\n\t\t\treturn true;\n\t\t} elseif ( false !== strpos( $id, 'llms' ) ) {\n\t\t\treturn true;\n\t\t} elseif ( in_array( $id, array( 'course', 'lesson' ), true ) ) {\n\t\t\treturn true;\n\t\t} elseif ( ! empty( $screen->post_type ) && post_type_supports( $screen->post_type, 'llms-membership-restrictions' ) ) {\n\t\t\treturn true;\n\t\t} elseif ( in_array( $screen->id, array( 'users' ), true ) ) {\n\t\t\treturn true;\n\t\t} elseif ( 'attachment' === $id || 'upload' === $id ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Enqueue stylesheets\n\t *\n\t * @since 1.0.0\n\t * @since 3.29.0 Unknown.\n\t * @since 3.35.0 Explicitly set asset versions.\n\t * @since 5.0.0 Use `LLMS_Assets` for registration/enqueue of styles.\n\t * @since 5.5.0 Use `LLMS_Assets` for the enqueue of `llms-addons`.\n\t * @since 7.2.0 Use `LLMS_ASSETS_VERSION` for asset versions.\n\t *\n\t * @return void\n\t */\n\tpublic function admin_styles() {\n\n\t\twp_enqueue_style( 'llms-admin-styles', LLMS_PLUGIN_URL . 'assets/css/admin' . LLMS_ASSETS_SUFFIX . '.css', array(), LLMS_ASSETS_VERSION );\n\t\twp_style_add_data( 'llms-admin-styles', 'rtl', 'replace' );\n\t\twp_style_add_data( 'llms-admin-styles', 'suffix', LLMS_ASSETS_SUFFIX );\n\n\t\tif ( ! $this->is_llms_page() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tllms()->assets->enqueue_style( 'llms-select2-styles' );\n\n\t\t$screen = get_current_screen();\n\n\t\tif ( 'lifterlms_page_llms-add-ons' === $screen->id || 'lifterlms_page_llms-dashboard' === $screen->id ) {\n\t\t\tllms()->assets->enqueue_style( 'llms-admin-add-ons' );\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue scripts.\n\t *\n\t * @since 1.0.0\n\t * @since 3.22.0 Unknown.\n\t * @since 3.35.0 Explicitly set asset versions.\n\t * @since 3.35.1 Don't reference external scripts & styles.\n\t * @since 4.3.3 Move logic for reporting/analytics scripts to `maybe_enqueue_reporting()`.\n\t * @since 4.4.0 Enqueue the main `llms` script.\n\t * @since 5.0.0 Clean up duplicate references to llms-select2 and register the script using `LLMS_Assets`.\n\t *              Remove topModal vendor dependency.\n\t *              Add `llms-admin-forms` on the forms post table screen.\n\t * @since 5.5.0 Use `LLMS_Assets` for the enqueue of `llms-admin-add-ons`.\n\t * @since 6.0.0 Enqueue certificate and achievement related js in `llms_my_certificate`, `llms_my_achievement` post types as well.\n\t * @since 7.1.0 Enqueue `postbox` script on the new dashboard page.\n\t * @since 7.2.0 Use `LLMS_ASSETS_VERSION` for asset versions.\n\t *              Enqueue reporting scripts on dashboard page.\n\t * @since 7.4.1 Enqueue `postbox` script on the new resources page.\n\t *\n\t * @return void\n\t */\n\tpublic function admin_scripts() {\n\n\t\tglobal $post_type, $post;\n\t\t$screen = get_current_screen();\n\n\t\tif ( 'widgets' === $screen->id ) {\n\n\t\t\twp_enqueue_script( 'llms-widget-syllabus', LLMS_PLUGIN_URL . 'assets/js/llms-widget-syllabus' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\n\t\t}\n\n\t\tllms()->assets->register_script( 'llms-select2' );\n\t\twp_register_script( 'llms-metaboxes', LLMS_PLUGIN_URL . 'assets/js/llms-metaboxes' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'jquery-ui-datepicker', 'llms-admin-scripts', 'llms-select2' ), LLMS_ASSETS_VERSION, true );\n\n\t\tif ( ( post_type_exists( $screen->id ) && post_type_supports( $screen->id, 'llms-membership-restrictions' ) ) || 'dashboard_page_llms-setup' === $screen->id ) {\n\t\t\twp_enqueue_script( 'llms-metaboxes' );\n\t\t}\n\n\t\t$tables = apply_filters(\n\t\t\t'llms_load_table_resources_pages',\n\t\t\tarray(\n\t\t\t\t'course',\n\t\t\t\t'lifterlms_page_llms-reporting',\n\t\t\t\t'llms_membership',\n\t\t\t)\n\t\t);\n\t\tif ( in_array( $screen->id, $tables, true ) ) {\n\t\t\twp_register_script( 'llms-admin-tables', LLMS_PLUGIN_URL . 'assets/js/llms-admin-tables' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\twp_enqueue_script( 'llms-admin-tables' );\n\t\t}\n\n\t\twp_register_script( 'llms', LLMS_PLUGIN_URL . 'assets/js/llms' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\twp_register_script( 'llms-admin-scripts', LLMS_PLUGIN_URL . 'assets/js/llms-admin' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'llms', 'llms-select2' ), LLMS_ASSETS_VERSION, true );\n\n\t\twp_register_script( 'llms-admin-media-protection-attachment-settings', LLMS_PLUGIN_URL . 'assets/js/llms-admin-media-protection-attachment-settings' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'media-views', 'wp-i18n', 'llms-admin-scripts' ), LLMS_ASSETS_VERSION, true );\n\n\t\tif ( $this->is_llms_page() ) {\n\n\t\t\tllms()->assets->enqueue_script( 'llms' );\n\n\t\t\twp_enqueue_script( 'jquery-ui-datepicker' );\n\t\t\twp_enqueue_script( 'jquery-ui-sortable' );\n\n\t\t\twp_enqueue_script( 'llms-admin-scripts' );\n\n\t\t\twp_register_style( 'jquery-ui-flick', LLMS_PLUGIN_URL . 'assets/vendor/jquery-ui-flick/jquery-ui-flick' . LLMS_ASSETS_SUFFIX . '.css', array(), '1.11.2' );\n\t\t\twp_enqueue_style( 'jquery-ui-flick' );\n\n\t\t\twp_enqueue_script( 'llms-ajax', LLMS_PLUGIN_URL . 'assets/js/llms-ajax' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\n\t\t\twp_enqueue_media();\n\n\t\t\tif ( 'course' === $post_type ) {\n\n\t\t\t\twp_enqueue_script( 'llms-metabox-fields', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-fields' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\t\tif ( ! use_block_editor_for_post_type( 'course' ) && $post ) {\n\t\t\t\t\twp_enqueue_script( 'llms-launch-course-button', LLMS_PLUGIN_URL . 'assets/js/llms-launch-course-button' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\t\t\twp_localize_script( 'llms-launch-course-button', 'llms_launch_course', array( 'builder_url' => admin_url( 'admin.php?page=llms-course-builder&course_id=' . intval( $post->ID ) ) ) );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( 'course' === $post_type || 'llms_membership' === $post_type ) {\n\n\t\t\t\tself::register_quill();\n\t\t\t\tself::register_a11y_dialog();\n\n\t\t\t\twp_enqueue_script( 'llms-metabox-students', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-students' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'llms-select2' ), LLMS_ASSETS_VERSION, true );\n\t\t\t\twp_enqueue_script( 'llms-metabox-product', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-product' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'llms', 'llms-a11y-dialog' ), LLMS_ASSETS_VERSION, true );\n\t\t\t\twp_enqueue_script( 'llms-metabox-instructors', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-instructors' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'llms' ), LLMS_ASSETS_VERSION, true );\n\t\t\t\twp_enqueue_script( 'llms-metabox-options', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-options' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'llms', 'llms-quill' ), LLMS_ASSETS_VERSION, true );\n\n\t\t\t\twp_enqueue_style( 'llms-quill-bubble' );\n\t\t\t}\n\n\t\t\tif ( 'lesson' === $post_type ) {\n\t\t\t\twp_enqueue_script( 'llms-metabox-fields', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-fields' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\t}\n\t\t\tif ( in_array( $post_type, array( 'llms_certificate', 'llms_my_certificate' ), true ) ) {\n\n\t\t\t\twp_enqueue_script( 'llms-metabox-certificate', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-certificate' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\t}\n\t\t\tif ( in_array( $post_type, array( 'llms_achievement', 'llms_my_achievement' ), true ) ) {\n\n\t\t\t\twp_enqueue_script( 'llms-metabox-achievement', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-achievement' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\t}\n\t\t\tif ( 'llms_membership' === $post_type ) {\n\t\t\t\twp_enqueue_script( 'llms-metabox-fields', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-fields' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\t}\n\t\t\tif ( 'llms_voucher' === $post_type ) {\n\n\t\t\t\twp_enqueue_script( 'llms-metabox-voucher', LLMS_PLUGIN_URL . 'assets/js/llms-metabox-voucher' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\t}\n\n\t\t\t$this->maybe_enqueue_reporting( $screen );\n\n\t\t\twp_enqueue_script( 'llms' );\n\t\t\twp_enqueue_script( 'llms-metaboxes' );\n\n\t\t\t// Load forms advert/compat script.\n\t\t\tif ( 'edit-llms_form' === $screen->id ) {\n\t\t\t\tllms()->assets->enqueue_script( 'llms-admin-forms' );\n\t\t\t}\n\t\t}\n\n\t\tif ( 'dashboard' === $screen->base ) {\n\t\t\t$this->maybe_enqueue_reporting( $screen );\n\t\t}\n\n\t\tif ( 'lifterlms_page_llms-settings' === $screen->id ) {\n\n\t\t\twp_enqueue_media();\n\t\t\twp_enqueue_script( 'llms-admin-settings', LLMS_PLUGIN_URL . 'assets/js/llms-admin-settings' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'jquery-ui-sortable' ), LLMS_ASSETS_VERSION, true );\n\n\t\t} elseif ( 'admin_page_llms-course-builder' === $screen->id ) {\n\n\t\t\tself::register_quill();\n\n\t\t\twp_enqueue_editor();\n\n\t\t\twp_enqueue_style( 'llms-builder-styles', LLMS_PLUGIN_URL . 'assets/css/builder' . LLMS_ASSETS_SUFFIX . '.css', array( 'llms-quill-bubble' ), LLMS_ASSETS_VERSION, 'screen' );\n\t\t\twp_style_add_data( 'llms-builder-styles', 'rtl', 'replace' );\n\t\t\twp_style_add_data( 'llms-builder-styles', 'suffix', LLMS_ASSETS_SUFFIX );\n\n\t\t\twp_enqueue_style( 'webui-popover', LLMS_PLUGIN_URL . 'assets/vendor/webui-popover/jquery.webui-popover' . LLMS_ASSETS_SUFFIX . '.css', array(), '1.2.15' );\n\t\t\twp_style_add_data( 'webui-popover', 'suffix', LLMS_ASSETS_SUFFIX );\n\n\t\t\twp_enqueue_script( 'webui-popover', LLMS_PLUGIN_URL . 'assets/vendor/webui-popover/jquery.webui-popover' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), LLMS_ASSETS_VERSION, true );\n\t\t\twp_enqueue_style( 'llms-datetimepicker', LLMS_PLUGIN_URL . 'assets/vendor/datetimepicker/jquery.datetimepicker.min.css', array(), '1.3.4' );\n\t\t\twp_enqueue_script( 'llms-datetimepicker', LLMS_PLUGIN_URL . 'assets/vendor/datetimepicker/jquery.datetimepicker.full' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), '1.3.4', true );\n\n\t\t\tif ( apply_filters( 'llms_builder_use_heartbeat', true ) ) {\n\t\t\t\twp_enqueue_script( 'heartbeat' );\n\t\t\t}\n\n\t\t\twp_enqueue_script( 'llms-builder', LLMS_PLUGIN_URL . 'assets/js/llms-builder' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-draggable', 'backbone', 'underscore', 'post', 'llms-quill' ), LLMS_ASSETS_VERSION, true );\n\n\t\t} elseif ( 'lifterlms_page_llms-add-ons' === $screen->id ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-addons' );\n\t\t} elseif ( in_array( $screen->id, array( 'lifterlms_page_llms-dashboard', 'lifterlms_page_llms-resources' ), true ) ) {\n\t\t\twp_enqueue_script( 'postbox' );\n\t\t}\n\n\t\tif (\n\t\t\t'edit-llms_my_certificate' === $screen->id ||\n\t\t\t(\n\t\t\t\t'lifterlms_page_llms-reporting' === $screen->id &&\n\t\t\t\t'students' === llms_filter_input( INPUT_GET, 'tab' ) &&\n\t\t\t\t'certificates' === llms_filter_input( INPUT_GET, 'stab' )\n\t\t\t)\n\t\t) {\n\t\t\tllms()->assets->enqueue_script( 'llms-admin-award-certificate' );\n\t\t\twp_enqueue_style( 'wp-editor' );\n\t\t}\n\t}\n\n\t/**\n\t * Register the media protection scripts when the media is enqueued.\n\t *\n\t * @since 9.0.6\n\t *\n\t * @return void\n\t */\n\tpublic function admin_media() {\n\t\tif ( ! is_admin() ) {\n\t\t\treturn;\n\t\t}\n\t\twp_enqueue_script( 'llms-admin-media-protection-attachment-settings' );\n\t}\n\n\t/**\n\t * Initialize the \"llms\" object for other scripts to hook into\n\t *\n\t * @since 1.0.0\n\t * @since 3.7.5 Unknown.\n\t * @since 4.4.0 Add `ajax_nonce`.\n\t * @since 4.5.1 Add an analytics localization object.\n\t * @since 5.0.0 Output Form location information as a window variable for block editor utilization.\n\t * @since 5.9.0 Use `wp_slash()` after `wp_json_encode()` to prevent issues encountered when strings contain single quotes.\n\t * @since 7.1.1 Add `home_url`.\n\t *\n\t * @return void\n\t */\n\tpublic function admin_print_scripts() {\n\n\t\t$screen = get_current_screen();\n\n\t\tglobal $post;\n\n\t\t$postdata = array();\n\n\t\tif ( ! empty( $post ) ) {\n\n\t\t\t$postdata = array(\n\t\t\t\t'id'        => $post->ID,\n\t\t\t\t'post_type' => $post->post_type,\n\t\t\t);\n\n\t\t}\n\n\t\techo '\n\t\t\t<script type=\"text/javascript\">\n\t\t\t\twindow.llms = window.llms || {};\n\t\t\t\twindow.llms.ajax_nonce = \"' . esc_attr( wp_create_nonce( LLMS_AJAX::NONCE ) ) . '\";\n\t\t\t\twindow.llms.admin_url = \"' . esc_url( admin_url() ) . '\";\n\t\t\t\twindow.llms.home_url = \"' . esc_url( home_url() ) . '\";\n\t\t\t\twindow.llms.post = ' . wp_json_encode( $postdata ) . ';\n\t\t\t\twindow.llms.analytics = ' . wp_json_encode( $this->get_analytics_options() ) . ';\n\t\t\t</script>\n\t\t';\n\n\t\techo '<script type=\"text/javascript\">window.LLMS = window.LLMS || {};</script>';\n\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_js_strings returns json_encoded strings.\n\t\techo '<script type=\"text/javascript\">window.LLMS.l10n = window.LLMS.l10n || {}; window.LLMS.l10n.strings = ' . wp_json_encode( LLMS_L10n::get_js_strings( false ) ) . ';</script>';\n\n\t\t$forms = LLMS_Forms::instance()->get_post_type();\n\n\t\tif ( $forms === $screen->id ) {\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped in `wp_slash()`.and wp_json_encode()\n\t\t\techo \"<script>window.llms.formLocations = JSON.parse( '\" . wp_slash( wp_json_encode( LLMS_Forms::instance()->get_locations() ) ) . \"' );</script>\";\n\t\t}\n\n\t\tif ( ! empty( $screen->is_block_editor ) || 'customize' === $screen->base ) {\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped in `wp_slash()`.and wp_json_encode()\n\t\t\techo \"<script>window.llms.userInfoFields = JSON.parse( '\" . wp_slash( wp_json_encode( llms_get_user_information_fields_for_editor() ) ) . \"' );</script>\";\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve an array of options used to localize the `llms.analytics` JS instance.\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return array\n\t */\n\tprotected function get_analytics_options() {\n\n\t\t/**\n\t\t * Create a number format string readable by google charts\n\t\t *\n\t\t * Replacing `9.9` with `9,9` and `0,0` with `0.0` to prevent loading errors encountered\n\t\t * as a result of the chart pattern not allowing usage of a comma for the decimal separator.\n\t\t *\n\t\t * @see https://stackoverflow.com/a/18204679/400568\n\t\t */\n\t\t$currency_format = str_replace( array( '9.9', '0,0', '9' ), array( '9,9', '0.0', '#' ), llms_price_raw( 9990.00 ) );\n\n\t\t/**\n\t\t * Customize Javascript localization options passed to the `llms.analytics` JS instance.\n\t\t *\n\t\t * @since 4.5.1\n\t\t *\n\t\t * @param array $opts Associative array of option data.\n\t\t */\n\t\treturn apply_filters( 'llms_get_analytics_js_options', compact( 'currency_format' ) );\n\t}\n\n\t/**\n\t * Register and enqueue scripts used on and related-to reporting and analytics\n\t *\n\t * @since 4.3.3\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.2.0 Load on dashboard screen.\n\t *              Use `LLMS_ASSETS_VERSION` for asset versions.\n\t *\n\t * @param WP_Sreen $screen Screen object from WP `get_current_screen()`.\n\t * @return void\n\t */\n\tprotected function maybe_enqueue_reporting( $screen ) {\n\n\t\tif ( in_array( $screen->base, array( 'lifterlms_page_llms-reporting', 'lifterlms_page_llms-dashboard', 'dashboard' ), true ) ) {\n\n\t\t\t$current_tab = llms_filter_input( INPUT_GET, 'tab' );\n\n\t\t\twp_register_script( 'llms-google-charts', LLMS_PLUGIN_URL . 'assets/js/vendor/gcharts-loader.min.js', array(), '2019-09-04', false );\n\t\t\twp_register_script( 'llms-analytics', LLMS_PLUGIN_URL . 'assets/js/llms-analytics' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'llms', 'llms-admin-scripts', 'llms-google-charts' ), LLMS_ASSETS_VERSION, true );\n\n\t\t\t// Dashboard page where we have analytics widgets.\n\t\t\tif ( in_array( $screen->base, array( 'lifterlms_page_llms-dashboard', 'dashboard' ), true ) ) {\n\n\t\t\t\twp_enqueue_script( 'llms-analytics' );\n\n\t\t\t} elseif ( 'lifterlms_page_llms-reporting' === $screen->base ) {\n\n\t\t\t\tif ( in_array( $current_tab, array( 'enrollments', 'sales' ), true ) ) {\n\t\t\t\t\twp_enqueue_script( 'llms-analytics' );\n\t\t\t\t} elseif ( 'quizzes' === $current_tab && 'attempts' === llms_filter_input( INPUT_GET, 'stab' ) ) {\n\t\t\t\t\twp_enqueue_script( 'llms-quiz-attempt-review', LLMS_PLUGIN_URL . 'assets/js/llms-quiz-attempt-review' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery', 'llms' ), LLMS_ASSETS_VERSION, true );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Register Quill CSS & JS\n\t *\n\t * @since 3.16.0\n\t * @since 3.17.8 Unknown.\n\t * @since 6.10.0 Load modules using `llms()->assets`.\n\t *\n\t * @return void\n\t */\n\tpublic static function register_quill( $modules = array() ) {\n\n\t\tif ( ! wp_script_is( 'llms-quill', 'registered' ) ) {\n\t\t\twp_register_script( 'llms-quill', LLMS_PLUGIN_URL . 'assets/vendor/quill/quill' . LLMS_ASSETS_SUFFIX . '.js', array(), '2.0.2', true );\n\t\t\twp_register_style( 'llms-quill-bubble', LLMS_PLUGIN_URL . 'assets/vendor/quill/quill.bubble' . LLMS_ASSETS_SUFFIX . '.css', array(), '1.3.5', 'screen' );\n\t\t}\n\n\t\tforeach ( $modules as $module ) {\n\t\t\tllms()->assets->register_script( \"llms-quill-{$module}\" );\n\t\t}\n\t}\n\n\t/**\n\t * Register the accessible dialog JS\n\t *\n\t * @since 8.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function register_a11y_dialog() {\n\t\tif ( ! wp_script_is( 'llms-a11y-dialog', 'registered' ) ) {\n\t\t\twp_register_script( 'llms-a11y-dialog', LLMS_PLUGIN_URL . 'assets/vendor/a11y-dialog/a11y-dialog.min.js', array(), '8.1.1', true );\n\t\t}\n\t}\n}\n\n\nreturn new LLMS_Admin_Assets();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.builder.php",
    "content": "<?php\n/**\n * LifterLMS Admin Course Builder\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.13.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Builder class\n *\n * @since 3.13.0\n * @since 3.30.0 Fixed issues related to custom field sanitization.\n * @since 3.37.11 Made method `get_existing_posts_where()` static.\n * @since 3.37.12 Refactored the `process_trash()` method.\n *                Added new filter, `llms_builder_{$post_type}_force_delete` to allow control of how post type deletion is handled\n *                when deleted via the builder.\n * @since 3.38.0 Improve backwards compatibility handling for the `llms_get_quiz_theme_settings` filter.\n * @since 3.38.2 On quiz saving, made sure that a question as a type set, otherwise set it by default to `'choice'`.\n */\nclass LLMS_Admin_Builder {\n\n\t/**\n\t * Search term string used by `get_existing_posts_where()` when querying for existing posts to clone/add to a course.\n\t *\n\t * @var string\n\t */\n\tprivate static $search_term = '';\n\n\t/**\n\t * Add menu items to the WP Admin Bar to allow quiz returns to the dashboard from the course builder\n\t *\n\t * @since 3.16.7\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param  WP_Admin_Bar $wp_admin_bar Instance of WP_Admin_Bar\n\t * @return void\n\t */\n\tpublic static function admin_bar_menu( $wp_admin_bar ) {\n\n\t\t// Partially lifted from `wp_admin_bar_site_menu()` in wp-includes/admin-bar.php.\n\t\tif ( current_user_can( 'read' ) ) {\n\n\t\t\t$wp_admin_bar->add_menu(\n\t\t\t\tarray(\n\t\t\t\t\t'parent' => 'site-name',\n\t\t\t\t\t'id'     => 'dashboard',\n\t\t\t\t\t'title'  => __( 'Dashboard', 'lifterlms' ),\n\t\t\t\t\t'href'   => admin_url(),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$wp_admin_bar->add_menu(\n\t\t\t\tarray(\n\t\t\t\t\t'parent' => 'site-name',\n\t\t\t\t\t'id'     => 'llms-courses',\n\t\t\t\t\t'title'  => __( 'Courses', 'lifterlms' ),\n\t\t\t\t\t'href'   => admin_url( 'edit.php?post_type=course' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\twp_admin_bar_appearance_menu( $wp_admin_bar );\n\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve the current user's builder autosave preferences\n\t *\n\t * Defaults to enabled for users who have never configured a setting value.\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return string Either \"yes\" or \"no\".\n\t */\n\tprotected static function get_autosave_status() {\n\n\t\t$autosave = get_user_option( 'llms_builder_autosave' );\n\t\t$autosave = empty( $autosave ) ? 'no' : $autosave;\n\n\t\t/**\n\t\t * Gets the status of autosave for the builder\n\t\t *\n\t\t * This can be configured on a per-user basis in the user's profile screen on the WP Admin Panel.\n\t\t *\n\t\t * @since 4.14.0\n\t\t *\n\t\t * @param string $autosave Status of autosave for the current user. Either \"yes\" or \"no\".\n\t\t */\n\t\treturn apply_filters( 'llms_builder_autosave_enabled', $autosave );\n\t}\n\n\t/**\n\t * Retrieve custom field schemas\n\t *\n\t * @since 3.17.0\n\t * @since 3.17.6 Add backwards compatibility for the deprecated `llms_get_quiz_theme_settings` filter.\n\t * @since 3.38.0 Only run backwards compatibility for `llms_get_quiz_theme_settings` when the filter is being used.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_custom_schemas() {\n\n\t\t$quiz_fields = array();\n\n\t\t/**\n\t\t * Handle old quiz layout compatibility API:\n\t\t * Translate the old filter into the new one for quizzes.\n\t\t */\n\t\tif ( get_theme_support( 'lifterlms-quizzes' ) && has_filter( 'llms_get_quiz_theme_settings' ) ) {\n\n\t\t\t$theme = wp_get_theme();\n\n\t\t\t$old = llms_get_quiz_theme_setting( 'layout' );\n\n\t\t\t$field = array(\n\t\t\t\t'attribute' => $old['id'],\n\t\t\t\t'id'        => $old['id'],\n\t\t\t\t'label'     => $old['name'],\n\t\t\t\t'type'      => ( 'select' === $old['type'] ) ? 'select' : 'radio',\n\t\t\t\t'options'   => $old['options'],\n\t\t\t);\n\n\t\t\tif ( isset( $old['id_prefix'] ) ) {\n\t\t\t\t$field['attribute_prefix'] = $old['id_prefix'];\n\t\t\t}\n\n\t\t\t$quiz_fields[ sprintf( '%s_backwards_theme_group', $theme->get_stylesheet() ) ] = array(\n\t\t\t\t// Translators: %s = Theme name.\n\t\t\t\t'title'      => sprintf( __( '%s Theme Settings', 'lifterlms' ), $theme->get( 'Name' ) ),\n\t\t\t\t'toggleable' => true,\n\t\t\t\t'fields'     => array( array( $field ) ),\n\t\t\t);\n\n\t\t}\n\n\t\t/**\n\t\t * Add custom fields to the LifterLMS Builder.\n\t\t *\n\t\t * @since 3.17.0\n\t\t *\n\t\t * @link https://lifterlms.com/docs/course-builder-custom-fields-for-developers\n\t\t *\n\t\t * @param array[] $fields Array of post types containing arrays of custom field data.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_builder_register_custom_fields',\n\t\t\tarray(\n\t\t\t\t'lesson' => array(),\n\t\t\t\t'quiz'   => $quiz_fields,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve a list of lessons the current user is allowed to clone/attach\n\t *\n\t * Used for ajax searching to add existing lessons.\n\t *\n\t * @since 3.14.8\n\t * @since 3.16.12 Unknown.\n\t * @since 5.8.0 Allow LMS managers to get all lessons. {@link https://github.com/gocodebox/lifterlms/issues/1849}.\n\t *              Removed unused `$course_id` parameter.\n\t *\n\t * @param string $post_type   Optional. Search specific post type(s). By default searches for all post types.\n\t * @param string $search_term Optional. Search term (searches post_title). Default is empty string.\n\t * @param int    $page        Optional. Used when paginating search results. Default is `1`.\n\t * @return array\n\t */\n\tprivate static function get_existing_posts( $post_type = '', $search_term = '', $page = 1 ) {\n\n\t\t$args = array(\n\t\t\t'order'          => 'ASC',\n\t\t\t'orderby'        => 'post_title',\n\t\t\t'paged'          => $page,\n\t\t\t'post_status'    => array( 'publish', 'draft', 'pending' ),\n\t\t\t'posts_per_page' => 10,\n\t\t);\n\n\t\tif ( $post_type ) {\n\t\t\t$args['post_type'] = $post_type;\n\t\t}\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\n\t\t\t$instructor = llms_get_instructor();\n\t\t\t$parents    = $instructor->get( 'parent_instructors' );\n\t\t\tif ( ! $parents ) {\n\t\t\t\t$parents = array();\n\t\t\t}\n\n\t\t\t$args['author__in'] = array_unique(\n\t\t\t\tarray_merge(\n\t\t\t\t\tarray( get_current_user_id() ),\n\t\t\t\t\t$instructor->get_assistants(),\n\t\t\t\t\t$parents\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\n\t\tself::$search_term = $search_term;\n\t\tadd_filter( 'posts_where', array( __CLASS__, 'get_existing_posts_where' ), 10, 2 );\n\t\t$query = new WP_Query( $args );\n\t\tremove_filter( 'posts_where', array( __CLASS__, 'get_existing_posts_where' ), 10, 2 );\n\n\t\t$posts = array();\n\n\t\tif ( $query->have_posts() ) {\n\n\t\t\tforeach ( $query->posts as $post ) {\n\n\t\t\t\t$post = llms_get_post( $post );\n\n\t\t\t\t$parents = array();\n\n\t\t\t\tif ( method_exists( $post, 'is_orphan' ) && $post->is_orphan() ) {\n\n\t\t\t\t\t$action = 'attach';\n\n\t\t\t\t} else {\n\n\t\t\t\t\t$action = 'clone';\n\n\t\t\t\t\t$course_id = false;\n\t\t\t\t\t$lesson_id = false;\n\n\t\t\t\t\tif ( 'lesson' === $post->get( 'type' ) ) {\n\t\t\t\t\t\t$course_id = $post->get( 'parent_course' );\n\t\t\t\t\t} elseif ( 'llms_quiz' === $post->get( 'type' ) ) {\n\t\t\t\t\t\t$lesson_id = $post->get( 'lesson_id' );\n\t\t\t\t\t\t$course    = $post->get_course();\n\t\t\t\t\t\tif ( $course ) {\n\t\t\t\t\t\t\t$course_id = $course->get( 'id' );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( $lesson_id ) {\n\t\t\t\t\t\t// Translators: %1$s = Lesson title; %2$d = Lesson id.\n\t\t\t\t\t\t$parents['lesson'] = sprintf( __( 'Lesson: %1$s (#%2$d)', 'lifterlms' ), '<em>' . get_the_title( $lesson_id ) . '</em>', $lesson_id );\n\t\t\t\t\t}\n\t\t\t\t\tif ( $course_id ) {\n\t\t\t\t\t\t// Translators: %1$s = Course title; %2$d - Course id.\n\t\t\t\t\t\t$parents['course'] = sprintf( __( 'Course: %1$s (#%2$d)', 'lifterlms' ), '<em>' . get_the_title( $course_id ) . '</em>', $course_id );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$posts[] = array(\n\t\t\t\t\t'action'  => $action,\n\t\t\t\t\t'data'    => $post,\n\t\t\t\t\t'id'      => $post->get( 'id' ),\n\t\t\t\t\t'parents' => $parents,\n\t\t\t\t\t'text'    => sprintf( '%1$s (#%2$d)', $post->get( 'title' ), $post->get( 'id' ) ),\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t\t$ret = array(\n\t\t\t'results'    => $posts,\n\t\t\t'pagination' => array(\n\t\t\t\t'more' => ( $page < $query->max_num_pages ),\n\t\t\t),\n\t\t);\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Search lessons by search term during existing lesson lookups\n\t *\n\t * @since 3.14.8\n\t * @since 3.16.12 Unknown.\n\t * @since 3.37.11 Made method static.\n\t *\n\t * @param string   $where    Existing sql where clause.\n\t * @param WP_QUery $wp_query Query object.\n\t * @return string\n\t */\n\tpublic static function get_existing_posts_where( $where, $wp_query ) {\n\n\t\tif ( self::$search_term ) {\n\t\t\tglobal $wpdb;\n\t\t\t$where .= ' AND ' . $wpdb->posts . '.post_title LIKE \"%' . esc_sql( $wpdb->esc_like( self::$search_term ) ) . '%\"';\n\t\t}\n\n\t\treturn $where;\n\t}\n\n\t/**\n\t * Retrieve the HTML of a JS template\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $template Template file slug.\n\t * @return string\n\t */\n\tprivate static function get_template( $template, $vars = array() ) {\n\n\t\tob_start();\n\t\textract( $vars );\n\t\tinclude 'views/builder/' . $template . '.php';\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * A terrible Rest API for the course builder\n\t *\n\t * @since 3.13.0\n\t * @since 3.19.2 Unknown.\n\t * @since 4.16.0 Remove all filters/actions applied to the title/content when handling the ajax_save by deafault.\n\t *               This is specially to prevent plugin conflicts, see https://github.com/gocodebox/lifterlms/issues/1530.\n\t * @since 4.17.0 Remove `remove_all_*` hooks added in version 4.16.0.\n\t *\n\t * @param array $request $_REQUEST\n\t * @return array\n\t */\n\tpublic static function handle_ajax( $request ) {\n\n\t\tif ( ! $request['course_id'] || ! current_user_can( 'edit_course', $request['course_id'] ) ) {\n\t\t\treturn array();\n\t\t}\n\n\t\tswitch ( $request['action_type'] ) {\n\n\t\t\tcase 'ajax_save':\n\t\t\t\tif ( isset( $request['llms_builder'] ) ) {\n\n\t\t\t\t\t$request['llms_builder'] = stripslashes( $request['llms_builder'] );\n\t\t\t\t\twp_send_json( self::heartbeat_received( array(), $request ) );\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'get_permalink':\n\t\t\t\t$id = isset( $request['id'] ) ? absint( $request['id'] ) : false;\n\t\t\t\tif ( ! $id ) {\n\t\t\t\t\treturn array();\n\t\t\t\t}\n\t\t\t\t$title = isset( $request['title'] ) ? sanitize_title( $request['title'] ) : null;\n\t\t\t\t$slug  = isset( $request['slug'] ) ? sanitize_title( $request['slug'] ) : null;\n\t\t\t\t$link  = get_sample_permalink( $id, $title, $slug );\n\t\t\t\twp_send_json(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'slug'      => $link[1],\n\t\t\t\t\t\t'permalink' => str_replace( '%pagename%', $link[1], $link[0] ),\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'lazy_load':\n\t\t\t\t$ret = array();\n\t\t\t\tif ( isset( $request['load_id'] ) ) {\n\t\t\t\t\t$post = llms_get_post( absint( $request['load_id'] ) );\n\t\t\t\t\t$ret  = $post->toArray();\n\t\t\t\t}\n\t\t\t\twp_send_json( $ret );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'search':\n\t\t\t\t$page      = isset( $request['page'] ) ? $request['page'] : 1;\n\t\t\t\t$term      = isset( $request['term'] ) ? sanitize_text_field( $request['term'] ) : '';\n\t\t\t\t$post_type = '';\n\t\t\t\tif ( isset( $request['post_type'] ) ) {\n\t\t\t\t\tif ( is_array( $request['post_type'] ) ) {\n\t\t\t\t\t\t$post_type = array_map( 'sanitize_text_field', $request['post_type'] );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$post_type = sanitize_text_field( $request['post_type'] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twp_send_json( self::get_existing_posts( $post_type, $term, $page ) );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn array();\n\t}\n\n\t/**\n\t * Do post locking stuff on the builder\n\t *\n\t * Locking the course edit main screen will lock the builder and vice versa... probably need to find a way\n\t * to fix that but for now this'll work just fine and if you're unhappy about it, well, sorry...\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param int $course_id WP Post ID.\n\t * @return void\n\t */\n\tprivate static function handle_post_locking( $course_id ) {\n\n\t\tif ( ! wp_check_post_lock( $course_id ) ) {\n\t\t\t$active_post_lock = wp_set_post_lock( $course_id );\n\t\t}\n\n\t\t?><input type=\"hidden\" id=\"post_ID\" value=\"<?php echo absint( $course_id ); ?>\">\n\t\t<?php\n\n\t\tif ( ! empty( $active_post_lock ) ) {\n\t\t\t?>\n\t<input type=\"hidden\" id=\"active_post_lock\" value=\"<?php echo esc_attr( implode( ':', $active_post_lock ) ); ?>\" />\n\t\t\t<?php\n\t\t}\n\n\t\tadd_filter( 'get_edit_post_link', array( __CLASS__, 'modify_take_over_link' ), 10, 3 );\n\t\tadd_action( 'admin_footer', '_admin_notice_post_locked' );\n\t}\n\n\t/**\n\t * Handle AJAX Heartbeat received calls\n\t *\n\t * All builder data is sent through the heartbeat.\n\n\t * @since 3.16.0\n\t * @since 3.24.2 Unknown.\n\t *\n\t * @param array $res  Response data.\n\t * @param array $data Data from the heartbeat api.\n\t *                    Builder data will be in the \"llms_builder\" array.\n\t * @return array\n\t */\n\tpublic static function heartbeat_received( $res, $data ) {\n\n\t\t// Exit if there's no builder data in the heartbeat data.\n\t\tif ( empty( $data['llms_builder'] ) ) {\n\t\t\treturn $res;\n\t\t}\n\n\t\t// Isolate builder data & ensure slashes aren't removed.\n\t\t$data = $data['llms_builder'];\n\n\t\t// Escape slashes.\n\t\t$data = json_decode( $data, true );\n\n\t\t// Setup our return.\n\t\t$ret = array(\n\t\t\t'status'  => 'success',\n\t\t\t'message' => esc_html__( 'Success', 'lifterlms' ),\n\t\t);\n\n\t\t// Need a numeric ID for a course post type!\n\t\tif ( empty( $data['id'] ) || ! is_numeric( $data['id'] ) || 'course' !== get_post_type( $data['id'] ) ) {\n\n\t\t\t$ret['status']  = 'error';\n\t\t\t$ret['message'] = esc_html__( 'Error: Invalid or missing course ID.', 'lifterlms' );\n\n\t\t} elseif ( ! current_user_can( 'edit_course', $data['id'] ) ) {\n\n\t\t\t$ret['status']  = 'error';\n\t\t\t$ret['message'] = esc_html__( 'Error: You do not have permission to edit this course.', 'lifterlms' );\n\n\t\t} else {\n\n\t\t\tif ( ! empty( $data['detach'] ) && is_array( $data['detach'] ) ) {\n\n\t\t\t\t$ret['detach'] = self::process_detachments( $data );\n\n\t\t\t}\n\n\t\t\tif ( current_user_can( 'delete_course', $data['id'] ) ) {\n\n\t\t\t\tif ( ! empty( $data['trash'] ) && is_array( $data['trash'] ) ) {\n\n\t\t\t\t\t$ret['trash'] = self::process_trash( $data );\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( ! empty( $data['updates'] ) && is_array( $data['updates'] ) ) {\n\n\t\t\t\t$ret['updates']['sections'] = self::process_updates( $data );\n\n\t\t\t}\n\t\t}\n\n\t\t// Unescape slashes after saved.\n\t\t// This ensures that updates are recognized as successful during Sync comparisons.\n\t\t// phpcs:ignore -- commented out code\n\t\t// $ret = json_decode( str_replace( '\\\\\\\\', '\\\\', json_encode( $ret ) ), true );\n\n\t\t// Return our data.\n\t\t$res['llms_builder'] = $ret;\n\n\t\treturn $res;\n\t}\n\n\t/**\n\t * Determine if an ID submitted via heartbeat data is a temporary id.\n\t *\n\t * If so the object must be created rather than updated\n\t *\n\t * @since 3.16.0\n\t * @since 3.17.0\n\t *\n\t * @param string $id An ID string.\n\t * @return bool\n\t */\n\tpublic static function is_temp_id( $id ) {\n\n\t\treturn ( ! is_numeric( $id ) && 0 === strpos( $id, 'temp_' ) );\n\t}\n\n\t/**\n\t * Modify the \"Take Over\" link on the post locked modal to send users to the builder when taking over a course\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param string $link    Default post edit link.\n\t * @param int    $post_id WP Post ID of the course.\n\t * @param string $context Display context.\n\t * @return string\n\t */\n\tpublic static function modify_take_over_link( $link, $post_id, $context ) {\n\n\t\treturn add_query_arg(\n\t\t\tarray(\n\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t'course_id' => $post_id,\n\t\t\t),\n\t\t\tadmin_url( 'admin.php' )\n\t\t);\n\t}\n\n\t/**\n\t * Output the page content\n\t *\n\t * @since 3.13.0\n\t * @since 3.19.2 Unknown.\n\t * @since 4.14.0 Added builder autosave preference defaults.\n\t * @since 7.2.0 Added video explainer template.\n\t * @since 7.6.0 Removed video explainer template.\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\n\t\tglobal $post;\n\n\t\t$course_id = isset( $_GET['course_id'] ) ? absint( $_GET['course_id'] ) : null;\n\t\tif ( ! $course_id || ( $course_id && 'course' !== get_post_type( $course_id ) ) ) {\n\t\t\tesc_html_e( 'Invalid course ID', 'lifterlms' );\n\t\t\treturn;\n\t\t}\n\n\t\t$post = get_post( $course_id );\n\n\t\tif ( ! current_user_can( 'edit_course', $course_id ) ) {\n\t\t\tesc_html_e( 'You cannot edit this course!', 'lifterlms' );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( 'auto-draft' === $post->post_status ) {\n\t\t\twp_update_post(\n\t\t\t\tarray(\n\t\t\t\t\t'ID'          => $course_id,\n\t\t\t\t\t'post_status' => 'draft',\n\t\t\t\t\t'post_title'  => __( 'New Course', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$post = get_post( $course_id );\n\t\t}\n\n\t\t$course = llms_get_post( $post );\n\n\t\tremove_all_actions( 'the_title' );\n\t\tremove_all_actions( 'the_content' );\n\n\t\tglobal $llms_builder_lazy_load;\n\t\t$llms_builder_lazy_load = true;\n\t\t?>\n\n\t\t<div class=\"wrap lifterlms llms-builder\">\n\n\t\t\t<?php do_action( 'llms_before_builder', $course_id ); ?>\n\n\t\t\t<div class=\"llms-builder-main\" id=\"llms-builder-main\"></div>\n\n\t\t\t<aside class=\"llms-builder-sidebar\" id=\"llms-builder-sidebar\"></aside>\n\n\t\t\t<?php\n\t\t\t\t$templates = array(\n\t\t\t\t\t'assignment',\n\t\t\t\t\t'course',\n\t\t\t\t\t'editor',\n\t\t\t\t\t'elements',\n\t\t\t\t\t'lesson',\n\t\t\t\t\t'lesson-settings',\n\t\t\t\t\t'quiz',\n\t\t\t\t\t'question',\n\t\t\t\t\t'question-choice',\n\t\t\t\t\t'question-type',\n\t\t\t\t\t'section',\n\t\t\t\t\t'settings-fields',\n\t\t\t\t\t'sidebar',\n\t\t\t\t\t'utilities',\n\t\t\t\t);\n\n\t\t\t\tforeach ( $templates as $template ) {\n\t\t\t\t\t// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\t\techo self::get_template(\n\t\t\t\t\t\t$template,\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'course_id' => $course_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t// phpcs:enable\n\t\t\t\t}\n\n\t\t\t\t?>\n\n\t\t\t<script>window.llms_builder =\n\t\t\t<?php\n\t\t\techo json_encode(\n\t\t\t\t/**\n\t\t\t\t * Filters the settings passed to the builder.\n\t\t\t\t *\n\t\t\t\t * @since 7.2.0\n\t\t\t\t *\n\t\t\t\t * @param array $settings Associative array of settings passed to the LifterLMS course builder.\n\t\t\t\t */\n\t\t\t\tapply_filters(\n\t\t\t\t\t'llms_builder_settings',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'autosave'               => self::get_autosave_status(),\n\t\t\t\t\t\t'admin_url'              => admin_url(),\n\t\t\t\t\t\t'course'                 => $course->toArray(),\n\t\t\t\t\t\t'debug'                  => array(\n\t\t\t\t\t\t\t'enabled' => ( defined( 'LLMS_BUILDER_DEBUG' ) && LLMS_BUILDER_DEBUG ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'questions'              => array_values( llms_get_question_types() ),\n\t\t\t\t\t\t'schemas'                => self::get_custom_schemas(),\n\t\t\t\t\t\t'sync'                   => apply_filters(\n\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t * Filters the sync builder settings.\n\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t * @since 3.16.0\n\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t * @param array $settings Associative array of settings passed to the LifterLMS course builder used for the sync.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t'llms_builder_sync_settings',\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'check_interval_ms' => ( 'yes' === self::get_autosave_status() ? 10000 : 1000 ),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'enable_video_explainer' => true,\n\t\t\t\t\t\t'home_url'               => home_url(),\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t\t</script>\n\n\t\t\t<?php do_action( 'llms_after_builder', $course_id ); ?>\n\n\t\t</div>\n\n\t\t<?php\n\t\t$llms_builder_lazy_load = false;\n\t\tself::handle_post_locking( $course_id );\n\t}\n\n\t/**\n\t * Process lesson detachments from the heartbeat data\n\t *\n\t * @since 3.16.0\n\t * @since 3.27.0 Unknown.\n\t *\n\t * @param array $data Array of lesson ids.\n\t * @return array\n\t */\n\tprivate static function process_detachments( $data ) {\n\n\t\t$ret = array();\n\n\t\tforeach ( $data['detach'] as $id ) {\n\n\t\t\t$res = array(\n\t\t\t\t// Translators: %s = Item id.\n\t\t\t\t'error' => sprintf( esc_html__( 'Unable to detach \"%s\". Invalid ID.', 'lifterlms' ), $id ),\n\t\t\t\t'id'    => $id,\n\t\t\t);\n\n\t\t\t$type = get_post_type( $id );\n\n\t\t\t$post_types = apply_filters( 'llms_builder_detachable_post_types', array( 'lesson', 'llms_question', 'llms_quiz' ) );\n\t\t\tif ( ! is_numeric( $id ) || ! in_array( $type, $post_types ) ) {\n\t\t\t\tarray_push( $ret, $res );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$post = llms_get_post( $id );\n\t\t\tif ( ! is_a( $post, 'LLMS_Post_Model' ) ) {\n\t\t\t\tarray_push( $ret, $res );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( 'lesson' === $type ) {\n\t\t\t\t$post->set( 'parent_course', '' );\n\t\t\t\t$post->set( 'parent_section', '' );\n\t\t\t} elseif ( 'llms_question' === $type ) {\n\t\t\t\t$post->set( 'parent_id', '' );\n\t\t\t} elseif ( 'llms_quiz' === $type ) {\n\t\t\t\t$parent = $post->get_lesson();\n\t\t\t\tif ( $parent ) {\n\t\t\t\t\t$parent->set( 'quiz_enabled', 'no' );\n\t\t\t\t\t$parent->set( 'quiz', '' );\n\t\t\t\t\t$post->set( 'lesson_id', 0 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdo_action( 'llms_builder_detach_' . $type, $post );\n\n\t\t\tunset( $res['error'] );\n\t\t\tarray_push( $ret, $res );\n\n\t\t}\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Delete/trash elements from heartbeat data\n\t *\n\t * @since 3.16.0\n\t * @since 3.17.1 Unknown.\n\t * @since 3.37.12 Refactored method to reduce method complexity.\n\t *\n\t * @param array $data Array of ids to trash/delete.\n\t * @return array[] Array of arrays containing information about the deleted items.\n\t */\n\tprivate static function process_trash( $data ) {\n\n\t\t$ret = array();\n\n\t\tforeach ( $data['trash'] as $id ) {\n\t\t\t$ret[] = self::process_trash_item( $id );\n\t\t}\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Trash (or delete) a single item\n\t *\n\t * @since 3.37.12\n\t *\n\t * @param mixed $id Item id. Usually a WP_Post ID but can also be custom ID strings.\n\t * @return array Associative array containing information about the trashed item.\n\t *               On success returns an array with an `id` key corresponding to the item's id.\n\t *               On failure returns the `id` as well as an `error` key which is a string describing the error.\n\t */\n\tprivate static function process_trash_item( $id ) {\n\n\t\t// Default response.\n\t\t$res = array(\n\t\t\t// Translators: %s = Item id.\n\t\t\t'error' => sprintf( esc_html__( 'Unable to delete \"%s\". Invalid ID.', 'lifterlms' ), $id ),\n\t\t\t'id'    => $id,\n\t\t);\n\n\t\t/**\n\t\t * Custom or 3rd party items can perform custom deletion actions using this filter.\n\t\t *\n\t\t * Return an associative array containing at least the `$id` to cease execution and have\n\t\t * the custom item returned via the `process_trash()` method.\n\t\t *\n\t\t * A successful deletion return should be: `array( 'id' => $id )`.\n\t\t *\n\t\t * A failure should contain an error message in a second array member:\n\t\t * `array( 'id' => $id, 'error' => esc_html__( 'My error message', 'my-domain' ) )`.\n\t\t *\n\t\t * @since Unknown.\n\t\t *\n\t\t * @param null|array $trash_response Denotes the trash response. See description above for details.\n\t\t * @param array      $res            The initial default error response which can be modified for your needs and then returned.\n\t\t * @param mixed      $id             The ID of the course element. Usually a WP_Post id.\n\t\t */\n\t\t$custom = apply_filters( 'llms_builder_trash_custom_item', null, $res, $id );\n\t\tif ( $custom ) {\n\t\t\treturn $custom;\n\t\t}\n\n\t\t// Determine the element's post type.\n\t\t$type = is_numeric( $id ) ? get_post_type( $id ) : false;\n\n\t\tif ( $type ) {\n\t\t\t$status = self::process_trash_item_post_type( $id, $type );\n\t\t} else {\n\t\t\t$status = self::process_trash_item_non_post_type( $id );\n\t\t}\n\n\t\t// Error deleting.\n\t\tif ( is_wp_error( $status ) ) {\n\t\t\t$res['error'] = $status->get_error_message();\n\n\t\t} elseif ( true === $status ) {\n\t\t\t// Success.\n\t\t\tunset( $res['error'] );\n\n\t\t}\n\n\t\treturn $res;\n\t}\n\n\t/**\n\t * Delete non-post type elements\n\t *\n\t * Currently handles deletion of question choices. In the future additional non-post type elements\n\t * may be handled by this method.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @param string $id Custom item ID. This should be a question choice id in the format of \"{$question_id}:{$choice_id}\".\n\t * @return null|true|WP_Error `null` when the $id cannot be parsed into a question choice id.\n\t *                            `true` on success.\n\t *                            `WP_Error` when an error is encountered.\n\t */\n\tprivate static function process_trash_item_non_post_type( $id ) {\n\n\t\t// Can't process.\n\t\tif ( false === strpos( $id, ':' ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$split    = explode( ':', $id );\n\t\t$question = llms_get_post( $split[0] );\n\n\t\t// Not a question choice.\n\t\tif ( ! $question || ! is_a( $question, 'LLMS_Question' ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Error.\n\t\tif ( ! $question->delete_choice( $split[1] ) ) {\n\t\t\t// Translators: %s = Question choice ID.\n\t\t\treturn new WP_Error( 'llms_builder_trash_custom_item', sprintf( esc_html__( 'Error deleting the question choice \"%s\"', 'lifterlms' ), $id ) );\n\t\t}\n\n\t\t// Success.\n\t\treturn true;\n\t}\n\n\t/**\n\t * Delete / Trash a post type\n\t *\n\t * @since 3.37.12\n\t *\n\t * @param int    $id        WP_Post ID.\n\t * @param string $post_type Post type name.\n\t * @return boolean|WP_Error `true` when successfully deleted or trashed.\n\t *                          `WP_Error` for unsupported post types or when a deletion error is encountered.\n\t */\n\tprivate static function process_trash_item_post_type( $id, $post_type ) {\n\n\t\t// Used for errors.\n\t\t$obj = get_post_type_object( $post_type );\n\n\t\t/**\n\t\t * Filter course elements that can be deleted or trashed via the course builder.\n\t\t *\n\t\t * Note that the use of \"trash\" in the filter name is not semantically correct as this filter does not guarantee\n\t\t * that the element will be sent to the trash. Use the filter `llms_builder_trash_{$post_type}_force_delete` to\n\t\t * determine if the element is sent to the trash or deleted immediately.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 3.37.12 The \"question_choice\" item was removed from the default list and is being handled as a \"custom item\".\n\t\t *\n\t\t * @param string[] $post_types Array of post type names.\n\t\t */\n\t\t$post_types = apply_filters( 'llms_builder_trashable_post_types', array( 'lesson', 'llms_quiz', 'llms_question', 'section' ) );\n\t\tif ( ! in_array( $post_type, $post_types, true ) ) {\n\t\t\t// Translators: %s = Post type name.\n\t\t\treturn new WP_Error( 'llms_builder_trash_unsupported_post_type', sprintf( esc_html__( '%s cannot be deleted via the Course Builder.', 'lifterlms' ), $obj->labels->name ) );\n\t\t}\n\n\t\t// Default force value: these post types are force deleted and others are moved to the trash.\n\t\t$force = in_array( $post_type, array( 'section', 'llms_question', 'llms_quiz' ), true );\n\n\t\t/**\n\t\t * Determine whether or not a post type should be moved to the trash or deleted when trashed via the Course Builder.\n\t\t *\n\t\t * The dynamic portion of this hook, `$post_type`, refers to the post type name of the post that's being trashed.\n\t\t *\n\t\t * By default all post types are moved to trash except for `section`, `llms_question`, and `llms_quiz` post types.\n\t\t *\n\t\t * @since 3.37.12\n\t\t *\n\t\t * @param boolean $force If `true` the post is deleted, if `false` it will be moved to the trash.\n\t\t * @param int     $id    WP_Post ID of the post being trashed.\n\t\t */\n\t\t$force = apply_filters( \"llms_builder_{$post_type}_force_delete\", $force, $id );\n\n\t\t// Delete or trash the post.\n\t\t$res = $force ? wp_delete_post( $id, true ) : wp_trash_post( $id );\n\t\tif ( ! $res ) {\n\t\t\t// Translators: %1$s = Post type singular name; %2$d = Post id.\n\t\t\treturn new WP_Error( 'llms_builder_trash_post_type', sprintf( esc_html__( 'Error deleting the %1$s \"%2$d\".', 'lifterlms' ), $obj->labels->singular_name, $id ) );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Process all the update data from the heartbeat\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array $data Array of course updates (all the way down the tree).\n\t * @return array\n\t */\n\tprivate static function process_updates( $data ) {\n\n\t\t$ret = array();\n\n\t\tif ( ! empty( $data['updates']['sections'] ) && is_array( $data['updates']['sections'] ) ) {\n\n\t\t\tforeach ( $data['updates']['sections'] as $section_data ) {\n\n\t\t\t\tif ( ! isset( $section_data['id'] ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$ret[] = self::update_section( $section_data, $data['id'] );\n\n\t\t\t}\n\t\t}\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Handle updating custom schema data\n\t *\n\t * @since 3.17.0\n\t * @since 3.30.0 Fixed typo preventing fields specifying a custom callback from working.\n\t * @since 3.30.0 Array fields will run field values through `sanitize_text_field()` instead of requiring a custom sanitization callback.\n\t *\n\t * @param string          $type Model type (lesson, quiz, etc...).\n\t * @param LLMS_Post_Model $post LLMS_Post_Model object for the model being updated.\n\t * @param array           $post_data Assoc array of raw data to update the model with.\n\t * @return void\n\t */\n\tpublic static function update_custom_schemas( $type, $post, $post_data ) {\n\n\t\t$schemas = self::get_custom_schemas();\n\t\tif ( empty( $schemas[ $type ] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$groups = $schemas[ $type ];\n\n\t\tforeach ( $groups as $name => $group ) {\n\n\t\t\t// Allow 3rd parties to manage their own custom save methods.\n\t\t\tif ( apply_filters( 'llms_builder_update_custom_fields_group_' . $name, false, $post, $post_data, $groups ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tforeach ( $group['fields'] as $fields ) {\n\n\t\t\t\tforeach ( $fields as $field ) {\n\n\t\t\t\t\t$keys = array( $field['attribute'] );\n\t\t\t\t\tif ( isset( $field['switch_attribute'] ) ) {\n\t\t\t\t\t\t$keys[] = $field['switch_attribute'];\n\t\t\t\t\t}\n\n\t\t\t\t\tforeach ( $keys as $attr ) {\n\n\t\t\t\t\t\tif ( isset( $post_data[ $attr ] ) ) {\n\n\t\t\t\t\t\t\tif ( isset( $field['sanitize_callback'] ) ) {\n\t\t\t\t\t\t\t\t$val = call_user_func( $field['sanitize_callback'], $post_data[ $attr ] );\n\t\t\t\t\t\t\t} elseif ( is_array( $post_data[ $attr ] ) ) {\n\t\t\t\t\t\t\t\t\t$val = array_map( 'sanitize_text_field', $post_data[ $attr ] );\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t$val = sanitize_text_field( $post_data[ $attr ] );\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t$attr = isset( $field['attribute_prefix'] ) ? $field['attribute_prefix'] . $attr : $attr;\n\t\t\t\t\t\t\tupdate_post_meta( $post_data['id'], $attr, $val );\n\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Update lesson from heartbeat data.\n\t *\n\t * @since 3.16.0\n\t * @since 5.1.3 Made sure a lesson moved in a just created section is correctly assigned to it.\n\t * @since 7.3.0 Skip revision creation when creating a brand new lesson.\n\t *\n\t * @param array        $lessons Lesson data from heartbeat.\n\t * @param LLMS_Section $section instance of the parent LLMS_Section.\n\t * @return array\n\t */\n\tprivate static function update_lessons( $lessons, $section ) {\n\n\t\t$ret = array();\n\n\t\tforeach ( $lessons as $lesson_data ) {\n\n\t\t\tif ( ! isset( $lesson_data['id'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$res = array_merge(\n\t\t\t\t$lesson_data,\n\t\t\t\tarray(\n\t\t\t\t\t'orig_id' => $lesson_data['id'],\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Create a new lesson.\n\t\t\tif ( self::is_temp_id( $lesson_data['id'] ) ) {\n\n\t\t\t\t$lesson = new LLMS_Lesson(\n\t\t\t\t\t'new',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'post_title' => isset( $lesson_data['title'] ) ? $lesson_data['title'] : __( 'New Lesson', 'lifterlms' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$created = true;\n\n\t\t\t} else {\n\n\t\t\t\t$lesson  = llms_get_post( $lesson_data['id'] );\n\t\t\t\t$created = false;\n\n\t\t\t}\n\n\t\t\tif ( empty( $lesson ) || ! is_a( $lesson, 'LLMS_Lesson' ) ) {\n\n\t\t\t\t// Translators: %s = Lesson post id.\n\t\t\t\t$res['error'] = sprintf( esc_html__( 'Unable to update lesson \"%s\". Invalid lesson ID.', 'lifterlms' ), $lesson_data['id'] );\n\n\t\t\t} else {\n\n\t\t\t\t// Don't create useless revision on \"creating\".\n\t\t\t\tadd_filter( 'wp_revisions_to_keep', '__return_zero', 999 );\n\n\t\t\t\t/**\n\t\t\t\t * If the parent section was just created the lesson will have a temp id\n\t\t\t\t * replace it with the newly created section's real ID.\n\t\t\t\t */\n\t\t\t\tif ( ! isset( $lesson_data['parent_section'] ) || self::is_temp_id( $lesson_data['parent_section'] ) ) {\n\t\t\t\t\t$lesson_data['parent_section'] = $section->get( 'id' );\n\t\t\t\t}\n\n\t\t\t\t// Return the real ID (important when creating a new lesson).\n\t\t\t\t$res['id'] = $lesson->get( 'id' );\n\n\t\t\t\t$properties = array_merge(\n\t\t\t\t\tarray_keys( $lesson->get_properties() ),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'content',\n\t\t\t\t\t\t'title',\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$skip_props = apply_filters( 'llms_builder_update_lesson_skip_props', array( 'quiz' ) );\n\n\t\t\t\t// Don't overwrite content if the content editor doesn't display.\n\t\t\t\tif ( ! $created && '' !== $lesson->get( 'content' ) && ! llms_parse_bool( $lesson->get( 'content_added_in_builder' ) ) ) {\n\t\t\t\t\t$skip_props[] = 'content';\n\t\t\t\t}\n\n\t\t\t\tif ( '' === $lesson->get( 'content' ) && isset( $lesson_data['content'] ) && '' !== $lesson_data['content']\n\t\t\t\t\t&& ! isset( $lesson_data['content_added_in_builder'] ) ) {\n\t\t\t\t\t// We're adding content via the builder for the first time; add a flag saying so.\n\t\t\t\t\t$lesson_data['content_added_in_builder'] = 'yes';\n\t\t\t\t}\n\n\t\t\t\t// Update all updatable properties.\n\t\t\t\tforeach ( $properties as $prop ) {\n\t\t\t\t\tif ( isset( $lesson_data[ $prop ] ) && ! in_array( $prop, $skip_props, true ) ) {\n\t\t\t\t\t\t$lesson->set( $prop, $lesson_data[ $prop ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Update all custom fields.\n\t\t\t\tself::update_custom_schemas( 'lesson', $lesson, $lesson_data );\n\n\t\t\t\t// During clone's we want to ensure custom field data comes with the lesson.\n\t\t\t\tif ( $created && isset( $lesson_data['custom'] ) ) {\n\t\t\t\t\tforeach ( $lesson_data['custom'] as $custom_key => $custom_vals ) {\n\t\t\t\t\t\tforeach ( $custom_vals as $val ) {\n\t\t\t\t\t\t\tadd_post_meta( $lesson->get( 'id' ), $custom_key, maybe_unserialize( $val ) );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Ensure slug gets updated when changing title from default \"New Lesson\".\n\t\t\t\tif ( isset( $lesson_data['title'] ) && ! $lesson->has_modified_slug() ) {\n\t\t\t\t\t$lesson->set( 'name', sanitize_title( $lesson_data['title'] ) );\n\t\t\t\t}\n\n\t\t\t\t// Include permalink, slug, and editor type in the response so the builder can update the model.\n\t\t\t\t$res['permalink']                = get_permalink( $lesson->get( 'id' ) );\n\t\t\t\t$res['name']                     = $lesson->get( 'name' );\n\t\t\t\t$res['content_added_in_builder'] = $lesson->get( 'content_added_in_builder' );\n\n\t\t\t\t// Remove revision prevention.\n\t\t\t\tremove_filter( 'wp_revisions_to_keep', '__return_zero', 999 );\n\n\t\t\t\tif ( ! empty( $lesson_data['quiz'] ) && is_array( $lesson_data['quiz'] ) ) {\n\t\t\t\t\t$res['quiz'] = self::update_quiz( $lesson_data['quiz'], $lesson );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Allow 3rd parties to update custom data.\n\t\t\t$res = apply_filters( 'llms_builder_update_lesson', $res, $lesson_data, $lesson, $created );\n\n\t\t\tarray_push( $ret, $res );\n\n\t\t}\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Update quiz questions from heartbeat data\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.11 Unknown.\n\t * @since 3.38.2 Make sure that a question as a type set, otherwise set it by default to `'choice'`.\n\t *\n\t * @param array                   $questions Question data array.\n\t * @param LLMS_Quiz|LLMS_Question $parent    Instance of an LLMS_Quiz or LLMS_Question (group).\n\t * @return array\n\t */\n\tprivate static function update_questions( $questions, $parent ) {\n\n\t\t$res = array();\n\n\t\tforeach ( $questions as $q_data ) {\n\n\t\t\t$ret = array_merge(\n\t\t\t\t$q_data,\n\t\t\t\tarray(\n\t\t\t\t\t'orig_id' => $q_data['id'],\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Remove temp id if we have one so we'll create a new question.\n\t\t\tif ( self::is_temp_id( $q_data['id'] ) ) {\n\t\t\t\tunset( $q_data['id'] );\n\t\t\t}\n\n\t\t\t// Remove choices because we'll add them individually after creation.\n\t\t\t$choices = ( isset( $q_data['choices'] ) && is_array( $q_data['choices'] ) ) ? $q_data['choices'] : false;\n\t\t\tunset( $q_data['choices'] );\n\n\t\t\t// Remove child questions if it's a question group.\n\t\t\t$questions = ( isset( $q_data['questions'] ) && is_array( $q_data['questions'] ) ) ? $q_data['questions'] : false;\n\t\t\tunset( $q_data['questions'] );\n\n\t\t\t$question_id = $parent->questions()->update_question( $q_data );\n\n\t\t\tif ( ! $question_id ) {\n\n\t\t\t\t// Translators: %s = Question post id.\n\t\t\t\t$ret['error'] = sprintf( esc_html__( 'Unable to update question \"%s\". Invalid question ID.', 'lifterlms' ), $q_data['id'] );\n\n\t\t\t} else {\n\n\t\t\t\t$ret['id'] = $question_id;\n\n\t\t\t\t$question = $parent->questions()->get_question( $question_id );\n\n\t\t\t\t/**\n\t\t\t\t * When saving a question, make sure that it has a question type set\n\t\t\t\t * otherwise set it by default to `'choice'`.\n\t\t\t\t */\n\t\t\t\tif ( ! $question->get( 'question_type', true ) ) {\n\t\t\t\t\t$question->set( 'question_type', 'choice' );\n\t\t\t\t}\n\n\t\t\t\tif ( $choices ) {\n\n\t\t\t\t\t$ret['choices'] = array();\n\n\t\t\t\t\tforeach ( $choices as $c_data ) {\n\n\t\t\t\t\t\t$choice_res = array_merge(\n\t\t\t\t\t\t\t$c_data,\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'orig_id' => $c_data['id'],\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tunset( $c_data['question_id'] );\n\n\t\t\t\t\t\t// Remove the temp ID so that we create it if it's new.\n\t\t\t\t\t\tif ( self::is_temp_id( $c_data['id'] ) ) {\n\t\t\t\t\t\t\tunset( $c_data['id'] );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$choice_id = $question->update_choice( $c_data );\n\t\t\t\t\t\tif ( ! $choice_id ) {\n\t\t\t\t\t\t\t// Translators: %s = Question choice ID.\n\t\t\t\t\t\t\t$choice_res['error'] = sprintf( esc_html__( 'Unable to update choice \"%s\". Invalid choice ID.', 'lifterlms' ), $c_data['id'] );\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t$choice_res['id'] = $choice_id;\n\n\t\t\t\t\t\t\tif ( isset( $c_data['choice']['id'] ) ) {\n\t\t\t\t\t\t\t\t// The quiz IDs are needed for later verification of access by the protected media filters.\n\t\t\t\t\t\t\t\t$quiz_ids = get_post_meta( $c_data['choice']['id'], '_llms_quiz_id', true );\n\t\t\t\t\t\t\t\tif ( ! is_array( $quiz_ids ) ) {\n\t\t\t\t\t\t\t\t\t$quiz_ids = array();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t$quiz_id = $parent->get( 'parent_id' ) ? $parent->get( 'parent_id' ) : $parent->get( 'id' );\n\t\t\t\t\t\t\t\tif ( ! in_array( $quiz_id, $quiz_ids ) ) {\n\t\t\t\t\t\t\t\t\t$quiz_ids[] = $quiz_id;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tupdate_post_meta( $c_data['choice']['id'], '_llms_quiz_id', $quiz_ids );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tarray_push( $ret['choices'], $choice_res );\n\n\t\t\t\t\t}\n\t\t\t\t} elseif ( $questions ) {\n\n\t\t\t\t\t$ret['questions'] = self::update_questions( $questions, $question );\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tarray_push( $res, $ret );\n\n\t\t}\n\n\t\treturn $res;\n\t}\n\n\t/**\n\t * Update quizzes during heartbeats\n\t *\n\t * @since 3.16.0\n\t * @since 3.17.6 Unknown.\n\t *\n\t * @param array       $quiz_data Array of quiz updates.\n\t * @param LLMS_Lesson $lesson    Instance of the parent LLMS_Lesson.\n\t * @return array\n\t */\n\tprivate static function update_quiz( $quiz_data, $lesson ) {\n\n\t\t$res = array_merge(\n\t\t\t$quiz_data,\n\t\t\tarray(\n\t\t\t\t'orig_id' => $quiz_data['id'],\n\t\t\t)\n\t\t);\n\n\t\t// Create a quiz.\n\t\tif ( self::is_temp_id( $quiz_data['id'] ) ) {\n\n\t\t\t$quiz = new LLMS_Quiz(\n\t\t\t\t'new',\n\t\t\t\tarray(\n\t\t\t\t\t'post_title' => isset( $quiz_data['title'] ) ? $quiz_data['title'] : __( 'New Quiz', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Update existing quiz.\n\t\t} else {\n\n\t\t\t$quiz = llms_get_post( $quiz_data['id'] );\n\n\t\t}\n\n\t\t$lesson->set( 'quiz', $quiz->get( 'id' ) );\n\t\t$lesson->set( 'quiz_enabled', 'yes' );\n\n\t\t// We don't have a proper quiz to work with...\n\t\tif ( empty( $quiz ) || ! is_a( $quiz, 'LLMS_Quiz' ) ) {\n\n\t\t\t// Translators: %s = Quiz post id.\n\t\t\t$res['error'] = sprintf( esc_html__( 'Unable to update quiz \"%s\". Invalid quiz ID.', 'lifterlms' ), $quiz_data['id'] );\n\n\t\t} else {\n\n\t\t\t// Return the real ID (important when creating a new quiz).\n\t\t\t$res['id'] = $quiz->get( 'id' );\n\n\t\t\t/**\n\t\t\t * If the parent lesson was just created the lesson will have a temp id\n\t\t\t * replace it with the newly created lessons's real ID.\n\t\t\t */\n\t\t\tif ( ! isset( $quiz_data['lesson_id'] ) || self::is_temp_id( $quiz_data['lesson_id'] ) ) {\n\t\t\t\t$quiz_data['lesson_id'] = $lesson->get( 'id' );\n\t\t\t}\n\n\t\t\t$properties = array_merge(\n\t\t\t\tarray_keys( $quiz->get_properties() ),\n\t\t\t\tarray(\n\t\t\t\t\t// phpcs:ignore -- commented out code\n\t\t\t\t\t// 'content',\n\t\t\t\t\t'status',\n\t\t\t\t\t'title',\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Update all updatable properties.\n\t\t\tforeach ( $properties as $prop ) {\n\t\t\t\tif ( isset( $quiz_data[ $prop ] ) ) {\n\t\t\t\t\t$quiz->set( $prop, $quiz_data[ $prop ] );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Include permalink and slug in the response so the builder can update the model.\n\t\t\t$res['permalink'] = get_permalink( $quiz->get( 'id' ) );\n\t\t\t$res['name']      = $quiz->get( 'name' );\n\n\t\t\tif ( isset( $quiz_data['questions'] ) && is_array( $quiz_data['questions'] ) ) {\n\t\t\t\t$res['questions'] = self::update_questions( $quiz_data['questions'], $quiz );\n\t\t\t}\n\n\t\t\t// Update all custom fields.\n\t\t\tself::update_custom_schemas( 'quiz', $quiz, $quiz_data );\n\n\t\t}\n\n\t\treturn $res;\n\t}\n\n\t/**\n\t * Update a section with data from the heartbeat\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.11 Unknown.\n\t *\n\t * @param array       $section_data Array of section data.\n\t * @param LLMS_Course $course_id    Instance of the parent LLMS_Course.\n\t * @return array\n\t */\n\tprivate static function update_section( $section_data, $course_id ) {\n\n\t\t$res = array_merge(\n\t\t\t$section_data,\n\t\t\tarray(\n\t\t\t\t'orig_id' => $section_data['id'],\n\t\t\t)\n\t\t);\n\n\t\t// Create a new section.\n\t\tif ( self::is_temp_id( $section_data['id'] ) ) {\n\n\t\t\t$section = new LLMS_Section( 'new' );\n\t\t\t$section->set( 'parent_course', $course_id );\n\n\t\t\t// Update existing section.\n\t\t} else {\n\n\t\t\t$section = llms_get_post( $section_data['id'] );\n\n\t\t}\n\n\t\t// We don't have a proper section to work with...\n\t\tif ( empty( $section ) || ! is_a( $section, 'LLMS_Section' ) ) {\n\t\t\t// Translators: %s = Section post id.\n\t\t\t$res['error'] = sprintf( esc_html__( 'Unable to update section \"%s\". Invalid section ID.', 'lifterlms' ), $section_data['id'] );\n\t\t} else {\n\n\t\t\t// Return the real ID (important when creating a new section).\n\t\t\t$res['id'] = $section->get( 'id' );\n\n\t\t\t// Run through all possible updated fields.\n\t\t\tforeach ( array( 'order', 'title' ) as $key ) {\n\n\t\t\t\t// Update those that were sent through.\n\t\t\t\tif ( isset( $section_data[ $key ] ) ) {\n\n\t\t\t\t\t$section->set( $key, $section_data[ $key ] );\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( isset( $section_data['lessons'] ) && is_array( $section_data['lessons'] ) ) {\n\n\t\t\t\t$res['lessons'] = self::update_lessons( $section_data['lessons'], $section );\n\n\t\t\t}\n\t\t}\n\n\t\treturn $res;\n\t}\n}\n"
  },
  {
    "path": "includes/admin/class.llms.admin.dashboard-widget.php",
    "content": "<?php\n/**\n * Admin Dashboard Widget\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 7.2.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Dashboard Widget class.\n *\n * @since 7.2.0\n */\nclass LLMS_Admin_Dashboard_Widget {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) );\n\t}\n\n\t/**\n\t * Add the dashboard widget.\n\t *\n\t * @since 7.2.0\n\t * @since 7.3.0 Add dashboard widget only if the current user can `manage_lifterlms`.\n\t *\n\t * @return void\n\t */\n\tpublic function add_dashboard_widget() {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\twp_add_dashboard_widget(\n\t\t\t'llms_dashboard_widget',\n\t\t\t'LifterLMS ' . __( 'Quick Links', 'lifterlms' ),\n\t\t\tarray( $this, 'output' )\n\t\t);\n\t}\n\n\t/**\n\t * Output the dashboard widget.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\t\t?>\n\t\t<div class=\"llms-dashboard-widget-wrap\">\n\t\t\t<h3><?php esc_html_e( 'Activity this week:', 'lifterlms' ); ?></h3>\n\t\t\t<a class=\"llms-button-primary\" href=\"<?php echo esc_url( admin_url( 'post-new.php?post_type=course' ) ); ?>\">\n\t\t\t\t<i class=\"fa fa-graduation-cap\" aria-hidden=\"true\"></i>\n\t\t\t\t<?php esc_html_e( 'Create a New Course', 'lifterlms' ); ?>\n\t\t\t</a>\n\t\t</div>\n\t\t<div class=\"activity-block\">\n\t\t\t<?php echo $this->get_widgets(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in template file. ?>\n\t\t</div>\n\t\t<div class=\"activity-block\">\n\t\t\t<h3><?php esc_html_e( 'LifterLMS News & Podcasts', 'lifterlms' ); ?></h3>\n\t\t</div>\n\t\t<ul class=\"llms-dashboard-widget-feed\">\n\t\t\t<?php foreach ( $this->get_feed() as $item ) : ?>\n\t\t\t\t<li>\n\t\t\t\t\t<a href=\"<?php echo esc_url( $item->get_permalink() ); ?>\" target=\"_blank\" rel=\"noopener\">\n\t\t\t\t\t\t<?php echo esc_html( $item->get_title() ); ?>\n\t\t\t\t\t</a>\n\t\t\t\t\t<span class=\"llms-dashboard-widget-feed-date\">\n\t\t\t\t\t\t<?php echo esc_html( date_i18n( get_option( 'date_format' ), $item->get_date( 'U' ) ) ); ?>\n\t\t\t\t\t\t|\n\t\t\t\t\t\t<?php echo strpos( $item->get_permalink(), '//podcast' ) !== false ? esc_html__( 'Podcast', 'lifterlms' ) : esc_html__( 'Blog', 'lifterlms' ); ?>\n\t\t\t\t\t</span>\n\t\t\t\t</li>\n\t\t\t<?php endforeach; ?>\n\t\t</ul>\n\t\t<ul class=\"subsubsub\">\n\t\t\t<li>\n\t\t\t\t<a href=\"https://lifterlms.com/blog/\" target=\"_blank\" rel=\"noopener\" aria-label=\"<?php esc_attr_e( 'Opens in a new tab', 'lifterlms' ); ?>\">\n\t\t\t\t\t<?php esc_html_e( 'View all blog posts', 'lifterlms' ); ?>\n\t\t\t\t\t<span aria-hidden=\"true\" class=\"dashicons dashicons-external\"></span>\n\t\t\t\t</a>\n\t\t\t\t|\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<a href=\"https://podcast.lifterlms.com/\" target=\"_blank\" rel=\"noopener\" aria-label=\"<?php esc_attr_e( 'Opens in a new tab', 'lifterlms' ); ?>\">\n\t\t\t\t\t<?php esc_html_e( 'View all podcasts', 'lifterlms' ); ?>\n\t\t\t\t\t<span aria-hidden=\"true\" class=\"dashicons dashicons-external\"></span>\n\t\t\t\t</a>\n\t\t\t\t|\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<a href=\"https://lifterlms.com/help/\" target=\"_blank\" rel=\"noopener\" aria-label=\"<?php esc_attr_e( 'Opens in a new tab', 'lifterlms' ); ?>\">\n\t\t\t\t\t<?php esc_html_e( 'Get support', 'lifterlms' ); ?>\n\t\t\t\t\t<span aria-hidden=\"true\" class=\"dashicons dashicons-external\"></span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<?php\n\t}\n\n\t/**\n\t * Get the widget HTML.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return string\n\t */\n\tprivate function get_widgets(): string {\n\t\treturn llms_get_template(\n\t\t\t'admin/reporting/tabs/widgets.php',\n\t\t\tarray(\n\t\t\t\t'json'        => wp_json_encode(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'current_tab'         => 'settings',\n\t\t\t\t\t\t'current_range'       => 'last-7-days',\n\t\t\t\t\t\t'current_students'    => array(),\n\t\t\t\t\t\t'current_courses'     => array(),\n\t\t\t\t\t\t'current_memberships' => array(),\n\t\t\t\t\t\t'dates'               => array(\n\t\t\t\t\t\t\t'start' => date( 'Y-m-d', strtotime( '-1 week' ) ),\n\t\t\t\t\t\t\t'end'   => current_time( 'Y-m-d' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t'widget_data' => array( self::get_dashboard_widget_data() ),\n\t\t\t)\n\t\t) ?? '';\n\t}\n\n\t/**\n\t * Get blog and podcast feed.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_feed(): array {\n\t\t$blog    = fetch_feed( 'https://lifterlms.com/feed' );\n\t\t$podcast = fetch_feed( 'https://podcast.lifterlms.com/feed/' );\n\n\t\tif ( ! is_wp_error( $blog ) ) {\n\t\t\t$blog_max   = $blog->get_item_quantity( 3 );\n\t\t\t$blog_items = $blog->get_items( 0, $blog_max );\n\t\t}\n\n\t\tif ( ! is_wp_error( $podcast ) ) {\n\t\t\t$podcast_max   = $podcast->get_item_quantity( 3 );\n\t\t\t$podcast_items = $podcast->get_items( 0, $podcast_max );\n\t\t}\n\n\t\t$merged = array_merge(\n\t\t\t$blog_items ?? array(),\n\t\t\t$podcast_items ?? array()\n\t\t);\n\n\t\tusort(\n\t\t\t$merged,\n\t\t\tfunction ( $a, $b ) {\n\t\t\t\treturn $b->get_date( 'U' ) - $a->get_date( 'U' );\n\t\t\t}\n\t\t);\n\n\t\treturn array_slice( $merged, 0, 5 );\n\t}\n\n\t/**\n\t * Get dashboard widget data.\n\t *\n\t * @since 7.3.0\n\t *\n\t * @return array $widget_data Array of data that will feed the dashboard widget.\n\t */\n\tpublic static function get_dashboard_widget_data() {\n\t\treturn apply_filters(\n\t\t\t/**\n\t\t\t * Filters the dashboard widget data.\n\t\t\t *\n\t\t\t * @since 7.3.0\n\t\t\t *\n\t\t\t * @param array $widget_data Array of data that will feed the dashboard widget.\n\t\t\t */\n\t\t\t'llms_dashboard_widget_data',\n\t\t\tarray(\n\t\t\t\t'enrollments'       => array(\n\t\t\t\t\t'title'   => __( 'Enrollments', 'lifterlms' ),\n\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t'info'    => __( 'Number of total enrollments during the selected period', 'lifterlms' ),\n\t\t\t\t\t'link'    => admin_url( 'admin.php?page=llms-reporting&tab=enrollments' ),\n\t\t\t\t),\n\t\t\t\t'registrations'     => array(\n\t\t\t\t\t'title'   => __( 'Registrations', 'lifterlms' ),\n\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t'info'    => __( 'Number of total user registrations during the selected period', 'lifterlms' ),\n\t\t\t\t\t'link'    => admin_url( 'admin.php?page=llms-reporting&tab=students' ),\n\t\t\t\t),\n\t\t\t\t'sold'              => array(\n\t\t\t\t\t'title'   => __( 'Net Sales', 'lifterlms' ),\n\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t'info'    => __( 'Total of all successful transactions during this period', 'lifterlms' ),\n\t\t\t\t\t'link'    => admin_url( 'admin.php?page=llms-reporting&tab=sales' ),\n\t\t\t\t),\n\t\t\t\t'lessoncompletions' => array(\n\t\t\t\t\t'title'   => __( 'Lessons Completed', 'lifterlms' ),\n\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t'info'    => __( 'Number of total lessons completed during the selected period', 'lifterlms' ),\n\t\t\t\t\t'link'    => admin_url( 'admin.php?page=llms-reporting&tab=courses' ),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n}\nreturn new LLMS_Admin_Dashboard_Widget();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.dashboard.php",
    "content": "<?php\n/**\n * Admin Dashboard Screen\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 7.1.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Dashboard Screen class.\n *\n * @since 7.1.0\n */\nclass LLMS_Admin_Dashboard {\n\n\t/**\n\t * Retrieve an instance of the WP_Screen for the dashboard screen.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return WP_Screen|boolean Returns a `WP_Screen` object when on the dashboard screen, otherwise returns `false`.\n\t */\n\tpublic static function get_screen() {\n\n\t\t$screen = get_current_screen();\n\t\tif ( $screen instanceof WP_Screen && 'lifterlms_page_llms-dashboard' === $screen->id ) {\n\t\t\treturn $screen;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Register Dashboard's meta boxes.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic static function register_meta_boxes() {\n\n\t\tadd_meta_box(\n\t\t\t'llms_dashboard_quick_links',\n\t\t\t__( 'Quick Links', 'lifterlms' ),\n\t\t\tarray( __CLASS__, 'meta_box' ),\n\t\t\t'toplevel_page_llms-dashboard',\n\t\t\t'normal',\n\t\t\t'default',\n\t\t\tarray( 'view' => 'quick-links' )\n\t\t);\n\n\t\tadd_meta_box(\n\t\t\t'llms_dashboard_addons',\n\t\t\t__( 'Most Popular Add-ons, Courses, and Resources', 'lifterlms' ),\n\t\t\tarray( __CLASS__, 'meta_box' ),\n\t\t\t'toplevel_page_llms-dashboard',\n\t\t\t'normal',\n\t\t\t'default',\n\t\t\tarray( 'view' => 'addons' )\n\t\t);\n\n\t\tadd_meta_box(\n\t\t\t'llms_dashboard_blog',\n\t\t\t__( 'LifterLMS Blog', 'lifterlms' ),\n\t\t\tarray( __CLASS__, 'meta_box' ),\n\t\t\t'toplevel_page_llms-dashboard',\n\t\t\t'side',\n\t\t\t'default',\n\t\t\tarray( 'view' => 'blog' )\n\t\t);\n\n\t\tadd_meta_box(\n\t\t\t'llms_dashboard_podcast',\n\t\t\t__( 'LifterLMS Podcast', 'lifterlms' ),\n\t\t\tarray( __CLASS__, 'meta_box' ),\n\t\t\t'toplevel_page_llms-dashboard',\n\t\t\t'side',\n\t\t\t'default',\n\t\t\tarray( 'view' => 'podcast' )\n\t\t);\n\n\t\t/**\n\t\t * Fired after adding the meta boxes on the LifterLMS admin dashboard page.\n\t\t *\n\t\t * Third parties can hook here to remove LifterLMS core meta boxes.\n\t\t *\n\t\t * @since 7.1.0\n\t\t */\n\t\tdo_action( 'llms_dashboard_meta_boxes_added' );\n\t}\n\n\t/**\n\t * Prints the dashboard's meta box html.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @param mixed $data_object Often this is the object that's the focus of the current screen,\n\t *                           for example a `WP_Post` or `WP_Comment` object.\n\t * @param array $box         Meta Box configuration array.\n\t * @return void\n\t */\n\tpublic static function meta_box( $data_object, $box ) {\n\n\t\tif ( isset( $box['args']['view'] ) ) {\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in template files.\n\t\t\techo self::get_view( $box['args']['view'] );\n\t\t}\n\t}\n\n\t/**\n\t * Handle HTML output on the screen.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\t\tinclude 'views/dashboard.php';\n\t}\n\n\t/**\n\t * Retrieves the HTML of a view from the views/dashboard directory.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @param string $file The file basename of the view to retrieve.\n\t * @return string The HTML content of the view.\n\t */\n\tprivate static function get_view( $file ) {\n\n\t\tob_start();\n\t\tinclude 'views/dashboard/' . $file . '.php';\n\t\treturn ob_get_clean();\n\t}\n}\n"
  },
  {
    "path": "includes/admin/class.llms.admin.import.php",
    "content": "<?php\n/**\n * Admin Import Screen and form submission handling\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.3.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Import Screen and form submission handling class\n *\n * @since 3.3.0\n * @since 3.30.1 Explicitly include template functions during imports.\n * @since 3.35.0 Initialize at `admin_init` instead of `init`.\n *               Import template from the admin views directory instead of the frontend templates directory.\n *               Improve error handling.\n * @since 3.36.3 Fixed a typo where \"$generator\" was spelled \"$generater\".\n * @since 3.37.3 Don't unslash uploaded file `tmp_name`.\n * @since 6.0.0 Removed the deprecated `LLMS_Admin_Import::localize_stat()` method.\n */\nclass LLMS_Admin_Import {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.3.0\n\t * @since 3.35.0 Initialize at `admin_init` instead of `init`.\n\t * @since 4.8.0 Added hooks handling cloud imports and outputting WP help tabs.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'admin_init', array( $this, 'cloud_import' ) );\n\t\tadd_action( 'admin_init', array( $this, 'upload_import' ) );\n\n\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );\n\n\t\tadd_action( 'current_screen', array( $this, 'add_help_tabs' ) );\n\n\t}\n\n\t/**\n\t * Add WP_Screen help tabs\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return WP_Screen|boolean Returns the WP_Screen on success or false if called outside of the intended screen context.\n\t */\n\tpublic function add_help_tabs() {\n\n\t\t$screen = $this->get_screen();\n\t\tif ( ! $screen ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$screen->add_help_tab(\n\t\t\tarray(\n\t\t\t\t'id'      => 'llms_import_overview',\n\t\t\t\t'title'   => __( 'Overview', 'lifterlms' ),\n\t\t\t\t'content' => $this->get_view( 'help-tab-overview' ),\n\t\t\t)\n\t\t);\n\n\t\t$screen->set_help_sidebar( $this->get_view( 'help-sidebar' ) );\n\n\t\treturn $screen;\n\n\t}\n\n\t/**\n\t * Handle form submission of a cloud import file\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return WP_Error|boolean Returns `false` for nonce or user permission errors, `true` on success, or an error object.\n\t */\n\tpublic function cloud_import() {\n\n\t\tif ( ! isset( $_REQUEST['llms_cloud_importer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['llms_cloud_importer_nonce'] ) ), 'llms-cloud-importer' ) || ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$course_id = llms_filter_input( INPUT_POST, 'llms_cloud_import_course_id', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( ! $course_id ) {\n\t\t\treturn $this->show_error( new WP_Error( 'llms-cloud-import-missing-id', __( 'Error: Missing course ID.', 'lifterlms' ) ) );\n\t\t}\n\n\t\t$res = LLMS_Export_API::get( array( $course_id ) );\n\t\tif ( is_wp_error( $res ) ) {\n\t\t\treturn $this->show_error( $res );\n\t\t}\n\n\t\treturn $this->handle_generation( $res );\n\n\t}\n\n\t/**\n\t * Enqueue static assets used on the screen\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return null|boolean Returns `null` when called outside of the intended screen context, `true` on success, or `false` on error.\n\t */\n\tpublic function enqueue() {\n\n\t\tif ( ! $this->get_screen() ) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn llms()->assets->enqueue_style( 'llms-admin-importer' );\n\n\t}\n\n\t/**\n\t * Convert an array of generated content IDs to a list of anchor tags to edit the generated content\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param int[]  $ids  Array of object IDs. Either WP_Post IDs or WP_User IDs.\n\t * @param string $type Object's type. Either \"post\" or \"user\".\n\t * @return string A comma-separated list of HTML anchor tags.\n\t */\n\tprotected function get_generated_content_list( $ids, $type ) {\n\n\t\t$list = array();\n\t\tforeach ( $ids as $id ) {\n\n\t\t\tif ( 'post' === $type ) {\n\t\t\t\t$link = get_edit_post_link( $id );\n\t\t\t\t$text = get_the_title( $id );\n\t\t\t} elseif ( 'user' === $type ) {\n\t\t\t\t$link = get_edit_user_link( $id );\n\t\t\t\t$text = get_user_by( 'ID', $id )->display_name;\n\t\t\t}\n\n\t\t\t$list[] = sprintf( '<a href=\"%1$s\">%2$s</a>', esc_url( $link ), $text );\n\n\t\t}\n\n\t\treturn implode( ', ', $list );\n\n\t}\n\n\t/**\n\t * Retrieve an instance of the WP_Screen for the import screen\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return WP_Screen|boolean Returns a `WP_Screen` object when on the import screen, otherwise returns `false`.\n\t */\n\tprotected function get_screen() {\n\n\t\t$screen = get_current_screen();\n\t\tif ( $screen instanceof WP_Screen && 'lifterlms_page_llms-import' === $screen->id ) {\n\t\t\treturn $screen;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Retrieves the HTML of a view from the views/import directory.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param string $file The file basename of the view to retrieve.\n\t * @return string The HTML content of the view.\n\t */\n\tprotected function get_view( $file ) {\n\n\t\tob_start();\n\t\tinclude 'views/import/' . $file . '.php';\n\t\treturn ob_get_clean();\n\n\t}\n\n\t/**\n\t * Retrieves a \"Success\" message providing information about the imported content.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param LLMS_Generator $generator Generator instance.\n\t * @return string\n\t */\n\tprotected function get_success_message( $generator ) {\n\n\t\t$msg  = '<strong>' . __( 'Import Successful!', 'lifterlms' ) . '</strong><br>';\n\t\t$msg .= '<ul>';\n\n\t\t$generated = $generator->get_generated_content();\n\n\t\tif ( ! empty( $generated['course'] ) ) {\n\t\t\t// Translators: %s = comma-separated list of anchors to the imported courses.\n\t\t\t$msg .= '<li>' . sprintf( __( 'Imported courses: %s', 'lifterlms' ), $this->get_generated_content_list( $generated['course'], 'post' ) ) . '</li>';\n\t\t}\n\t\tif ( ! empty( $generated['user'] ) ) {\n\t\t\t// Translators: %s = comma-separated list of anchors to the imported users.\n\t\t\t$msg .= '<li>' . sprintf( __( 'Imported users: %s', 'lifterlms' ), $this->get_generated_content_list( $generated['user'], 'user' ) ) . '</li>';\n\t\t}\n\n\t\t$msg .= '</ul>';\n\n\t\treturn $msg;\n\n\t}\n\n\t/**\n\t * Instantiate and generate raw data via LLMS_Generator\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param string|array $raw A JSON string or array or raw data which can be parsed by an LLMS_Generator instance.\n\t * @return WP_Error|boolean On success, returns true or an error object on failure.\n\t */\n\tprotected function handle_generation( $raw ) {\n\n\t\t$generator = new LLMS_Generator( $raw );\n\t\tif ( is_wp_error( $generator->set_generator() ) ) {\n\t\t\treturn $this->show_error( $generator->error );\n\t\t}\n\n\t\t$generator->generate();\n\t\tif ( $generator->is_error() ) {\n\t\t\treturn $this->show_error( $generator->error );\n\t\t}\n\n\t\tLLMS_Admin_Notices::flash_notice( $this->get_success_message( $generator ), 'success' );\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Handle HTML output on the screen\n\t *\n\t * @since 3.3.0\n\t * @since 3.35.0 Import template from the admin views directory instead of the frontend templates directory.\n\t * @since 4.7.0 Moved logic for generating success message into its own method.\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\t\tinclude 'views/import.php';\n\t}\n\n\t/**\n\t * Output an admin notice from a WP_Error object\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param WP_Error $error WP_Error object.\n\t * @return WP_Error Returns the same error passed into the object.\n\t */\n\tprotected function show_error( $error ) {\n\t\tLLMS_Admin_Notices::flash_notice( $error->get_error_message(), 'error' );\n\t\treturn $error;\n\t}\n\n\t/**\n\t * Handle form submission\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.1 Explicitly include template functions.\n\t * @since 3.35.0 Validate nonce and user permissions before processing import data.\n\t *               Moved statistic localization into its own function.\n\t *               Updated return signature.\n\t * @since 3.36.3 Fixed a typo where \"$generator\" was spelled \"$generater\".\n\t * @since 3.37.3 Don't unslash uploaded file `tmp_name`.\n\t * @since 4.8.0 Use helper methods `show_error()` and `handle_generation()`.\n\t *\n\t * @return boolean|WP_Error false for nonce or permission errors, WP_Error when an error is encountered, true on success.\n\t */\n\tpublic function upload_import() {\n\n\t\tif ( ! isset( $_REQUEST['llms_importer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['llms_importer_nonce'] ) ), 'llms-importer' ) || ! current_user_can( 'manage_lifterlms' ) || empty( $_FILES['llms_import'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Fixes an issue where hooks are loaded in an unexpected order causing template functions required to parse an import aren't available.\n\t\tllms()->include_template_functions();\n\n\t\t// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash\n\t\t// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized\n\t\t$validate = $this->validate_upload( $_FILES['llms_import'] );\n\n\t\t// File upload error.\n\t\tif ( is_wp_error( $validate ) ) {\n\t\t\treturn $this->show_error( $validate );\n\t\t}\n\n\t\t$raw = ! empty( $_FILES['llms_import']['tmp_name'] ) ? file_get_contents( sanitize_text_field( $_FILES['llms_import']['tmp_name'] ) ) : array(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents\n\t\t// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash\n\t\t// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitizedr\n\n\t\treturn $this->handle_generation( $raw );\n\n\t}\n\n\t/**\n\t * Validate the uploaded file\n\t *\n\t * @since 3.3.0\n\t * @since 3.35.0 Fix undefined variable error.\n\t *\n\t * @link https://www.php.net/manual/en/features.file-upload.errors.php\n\t *\n\t * @param array $file  array of file data.\n\t * @return WP_Error|true\n\t */\n\tprivate function validate_upload( $file ) {\n\n\t\tif ( ! empty( $file['error'] ) ) {\n\n\t\t\tswitch ( $file['error'] ) {\n\n\t\t\t\tcase UPLOAD_ERR_INI_SIZE:\n\t\t\t\t\t$msg = __( 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase UPLOAD_ERR_FORM_SIZE:\n\t\t\t\t\t$msg = __( 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase UPLOAD_ERR_PARTIAL:\n\t\t\t\t\t$msg = __( 'The uploaded file was only partially uploaded.', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase UPLOAD_ERR_NO_FILE:\n\t\t\t\t\t$msg = __( 'No file was uploaded.', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase UPLOAD_ERR_NO_TMP_DIR:\n\t\t\t\t\t$msg = __( 'Missing a temporary folder.', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase UPLOAD_ERR_CANT_WRITE:\n\t\t\t\t\t$msg = __( 'Failed to write file to disk.', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase UPLOAD_ERR_EXTENSION:\n\t\t\t\t\t$msg = __( 'File upload stopped by extension.', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t$msg = __( 'Unknown upload error.', 'lifterlms' );\n\n\t\t\t}\n\t\t} else {\n\n\t\t\t$info = pathinfo( $file['name'] );\n\n\t\t\tif ( 'json' !== strtolower( $info['extension'] ) ) {\n\t\t\t\t$msg = __( 'Only valid JSON files can be imported.', 'lifterlms' );\n\t\t\t}\n\t\t}\n\n\t\tif ( ! empty( $msg ) ) {\n\t\t\treturn new WP_Error( 'llms_import_file_error', $msg );\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n}\n\nreturn new LLMS_Admin_Import();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.menus.php",
    "content": "<?php\n/**\n * Admin Menu Items.\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 1.0.0\n * @version 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Menus class\n *\n * @since 1.0.0\n * @since 3.19.0 Added action scheduler posts table.\n * @since 3.35.0 Sanitize input data.\n * @since 3.37.19 Load tools on the status page.\n * @since 3.35.0 Sanitize input data.\n * @since 5.0.0 Add custom LifterLMS submenu item sorting.\n */\nclass LLMS_Admin_Menus {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 1.0.0\n\t * @since 3.19.0 Add action scheduler posts table.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'admin_init', array( $this, 'status_page_actions' ) );\n\t\tadd_action( 'admin_init', array( $this, 'builder_page_actions' ) );\n\t\tadd_action( 'load-admin_page_llms-course-builder', array( $this, 'builder_title' ) );\n\n\t\tadd_filter( 'custom_menu_order', array( $this, 'submenu_order' ) );\n\t\tadd_action( 'admin_menu', array( $this, 'display_admin_menu' ) );\n\t\tadd_action( 'admin_menu', array( $this, 'display_admin_menu_late' ), 7777 );\n\n\t\t// Shame shame shame.\n\t\tadd_action( 'admin_menu', array( $this, 'instructor_menu_hack' ) );\n\n\t\tadd_filter( 'action_scheduler_post_type_args', array( $this, 'action_scheduler_menu' ) );\n\t}\n\n\t/**\n\t * If WP_DEBUG is not enabled, expose the schedule-action post type management via direct link\n\t *\n\t * EG: site.com/wp-admin/edit.php?post_type=scheduled-action\n\t *\n\t * @since 3.19.0\n\t *\n\t * @param array $args Default custom post type arguments.\n\t * @return array\n\t */\n\tpublic function action_scheduler_menu( $args ) {\n\n\t\t// If WP_DEBUG is enabled the menu item will already be displayed under \"tools.php\".\n\t\tif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {\n\t\t\treturn $args;\n\t\t}\n\n\t\t// Otherwise we'll add a hidden menu accessible via direct link only.\n\t\treturn array_merge(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'show_ui'           => true,\n\t\t\t\t'show_in_menu'      => '',\n\t\t\t\t'show_in_admin_bar' => false,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Remove the default menu page from the submenu.\n\t *\n\t * @since 1.0.0\n\t * @since 3.2.0 Unknown.\n\t * @since 5.0.0 Adds custom sorting for LifterLMS submenu items.\n\t * @since 7.1.0 Added `llms-dashboard` to the order array in first position.\n\t *\n\t * @param bool $flag Flag from core filter (always false).\n\t * @return bool\n\t */\n\tpublic function submenu_order( $flag ) {\n\n\t\tglobal $submenu;\n\n\t\tif ( isset( $submenu['lifterlms'] ) ) {\n\n\t\t\t// Our desired order.\n\t\t\t$order = array( 'llms-dashboard', 'llms-settings', 'llms-reporting', 'edit.php?post_type=llms_form' );\n\n\t\t\t// Temporary array to hold our submenu items.\n\t\t\t$new_submenu = array();\n\n\t\t\t// Any items not defined in the $order array will be added at the end of the new array.\n\t\t\t$num_items = count( $submenu['lifterlms'] );\n\n\t\t\tforeach ( $submenu['lifterlms'] as $item ) {\n\n\t\t\t\t// Locate the desired order.\n\t\t\t\t$key = array_search( $item[2], $order, true );\n\n\t\t\t\t// Not found, increment the number of items to add it to the end of the array in its original order.\n\t\t\t\tif ( false === $key ) {\n\t\t\t\t\t$key = ++$num_items;\n\t\t\t\t}\n\n\t\t\t\t// Add the item to the new submenu.\n\t\t\t\t$new_submenu[ $key ] = $item;\n\n\t\t\t}\n\n\t\t\t// Sort.\n\t\t\tksort( $new_submenu );\n\n\t\t\t// Remove the keys so the new array doesn't skip any numbers.\n\t\t\t$submenu['lifterlms'] = array_values( $new_submenu );\n\n\t\t}\n\n\t\treturn $flag;\n\t}\n\n\t/**\n\t * Handle init actions on the course builder page\n\t *\n\t * Used for post-locking redirects when taking over from another user\n\t * on the course builder page.\n\t *\n\t * @since 3.13.0\n\t * @since 3.16.7 Unknown.\n\t * @since 9.2.3 Add capability check before setting post lock.\n\t *\n\t * @return void\n\t */\n\tpublic function builder_page_actions() {\n\n\t\tif ( ! isset( $_GET['page'] ) || 'llms-course-builder' !== $_GET['page'] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! empty( $_GET['get-post-lock'] ) && ! empty( $_GET['course_id'] ) ) {\n\t\t\t$post_id = absint( $_GET['course_id'] );\n\t\t\tcheck_admin_referer( 'lock-post_' . $post_id );\n\t\t\tif ( ! current_user_can( 'edit_post', $post_id ) ) {\n\t\t\t\twp_die( esc_html__( 'You are not authorized to edit this course.', 'lifterlms' ) );\n\t\t\t}\n\t\t\twp_set_post_lock( $post_id );\n\t\t\twp_safe_redirect(\n\t\t\t\tadd_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t\t\t'course_id' => $post_id,\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t)\n\t\t\t);\n\t\t\texit();\n\n\t\t}\n\n\t\tadd_action( 'admin_bar_menu', array( 'LLMS_Admin_Builder', 'admin_bar_menu' ), 100, 1 );\n\t}\n\n\t/**\n\t * Set the global $title variable for the builder\n\t *\n\t * Prevents the <title> in the admin head being partially empty on builder screen.\n\t *\n\t * @since 3.14.9\n\t *\n\t * @return void\n\t */\n\tpublic function builder_title() {\n\t\tglobal $title;\n\t\t$title = __( 'Course Builder', 'lifterlms' );\n\t}\n\n\t/**\n\t * Admin Menu.\n\t *\n\t * @since 1.0.0\n\t * @since 3.13.0 Unknown.\n\t * @since 5.3.1 Use encoded SVG LifterLMS icon so that WordPress can \"paint\" it.\n\t *              submenu page in place of NULL.\n\t * @since 7.1.0 Added the 'Dashboard' submenu page.\n\t * @since 7.4.1 Added the 'Resources' submenu page.\n\t *\n\t * @return void\n\t */\n\tpublic function display_admin_menu() {\n\n\t\tglobal $menu;\n\n\t\t$menu[51] = array( '', 'read', 'llms-separator', '', 'wp-menu-separator' );\n\n\t\t$icon_url = 'data:image/svg+xml;base64,' . base64_encode( file_get_contents( LLMS_PLUGIN_DIR . 'assets/images/lifterlms-icon-grey.svg' ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode\n\t\tadd_menu_page( 'lifterlms', 'LifterLMS', 'read', 'lifterlms', '__return_empty_string', $icon_url, 51 );\n\n\t\tadd_submenu_page( 'lifterlms', __( 'LifterLMS Dashboard', 'lifterlms' ), __( 'Dashboard', 'lifterlms' ), 'manage_lifterlms', 'llms-dashboard', array( $this, 'dashboard_page_init' ) );\n\n\t\tadd_submenu_page( 'lifterlms', __( 'LifterLMS Settings', 'lifterlms' ), __( 'Settings', 'lifterlms' ), 'manage_lifterlms', 'llms-settings', array( $this, 'settings_page_init' ) );\n\n\t\tadd_submenu_page( 'lifterlms', __( 'LifterLMS Reporting', 'lifterlms' ), __( 'Reporting', 'lifterlms' ), 'view_lifterlms_reports', 'llms-reporting', array( $this, 'reporting_page_init' ) );\n\n\t\tadd_submenu_page( 'lifterlms', __( 'LifterLMS Import', 'lifterlms' ), __( 'Import', 'lifterlms' ), 'manage_lifterlms', 'llms-import', array( $this, 'import_page_init' ) );\n\n\t\tadd_submenu_page( 'lifterlms', __( 'LifterLMS Status', 'lifterlms' ), __( 'Status', 'lifterlms' ), 'manage_lifterlms', 'llms-status', array( $this, 'status_page_init' ) );\n\n\t\tadd_submenu_page( 'lifterlms', __( 'LifterLMS Resources', 'lifterlms' ), __( 'Resources', 'lifterlms' ), 'manage_lifterlms', 'llms-resources', array( $this, 'resources_page_init' ) );\n\n\t\t// Passing '' to register the page without actually adding a menu item.\n\t\tadd_submenu_page( '', __( 'LifterLMS Course Builder', 'lifterlms' ), __( 'Course Builder', 'lifterlms' ), 'edit_courses', 'llms-course-builder', array( $this, 'builder_init' ) );\n\t}\n\n\t/**\n\t * Add items to the admin menu with a later priority\n\t *\n\t * @since 3.5.0\n\t * @since 3.22.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function display_admin_menu_late() {\n\n\t\t/**\n\t\t * Disable the display and output of LifterLMS Add-ons screen.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param boolean $display Whether or not to display the screen. Defaults to `false` which shows the screen.\n\t\t */\n\t\tif ( apply_filters( 'lifterlms_disable_addons_screen', false ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tadd_submenu_page( 'lifterlms', __( 'LifterLMS Add-ons, Courses, and Resources', 'lifterlms' ), __( 'Add-ons & more', 'lifterlms' ), 'manage_lifterlms', 'llms-add-ons', array( $this, 'add_ons_page_init' ) );\n\t}\n\n\t/**\n\t * Output the add-ons screen.\n\t *\n\t * @since 3.5.0\n\t * @since 3.22.0 Unknown.\n\t * @since 6.0.0 Removed loading the LLMS_Admin_AddOns class file that is now handled by the autoloader.\n\t *\n\t * @return void\n\t */\n\tpublic function add_ons_page_init() {\n\n\t\t$view = new LLMS_Admin_AddOns();\n\t\t$view->handle_actions();\n\t\t$view->output();\n\t}\n\n\t/**\n\t * Output the HTML for the Course Builder\n\t *\n\t * @since 3.13.0\n\t * @since 3.16.0 Unknown.\n\t * @since 6.0.0 Removed loading the LLMS_Admin_Builder class file that is now handled by the autoloader.\n\t *\n\t * @return void\n\t */\n\tpublic function builder_init() {\n\n\t\tLLMS_Admin_Builder::output();\n\t}\n\n\t/**\n\t * Outputs the LifterLMS Importer Screen HTML\n\t *\n\t * @since 3.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function import_page_init() {\n\t\tLLMS_Admin_Import::output();\n\t}\n\n\t/**\n\t * Removes edit.php from the admin menu for instructors/asst instructors\n\t *\n\t * Note: The post screen is still technically accessible.\n\t *\n\t * Posts will need to be submitted for review as the instructors only actually have\n\t * the capability of a contributor with regards to posts\n\t * but this hack will allow instructors to publish new lessons, quizzes, & questions.\n\t *\n\t * @since 3.13.0\n\t * @since 7.0.1 Added filterable early return allowing 3rd parties to modify\n\t *               the user roles affected by this hack.\n\t *\n\t * @link https://core.trac.wordpress.org/ticket/22895\n\t * @link https://core.trac.wordpress.org/ticket/16808\n\t *\n\t * @return void\n\t */\n\tpublic function instructor_menu_hack() {\n\n\t\t/**\n\t\t * Filters the WP_User roles should receive the instructor admin menu hack.\n\t\t *\n\t\t * If you wish to provide explicit access to the `post` post type, to the\n\t\t * instrutor or instructor's assistant role, the role will need to be\n\t\t * removed from this array so they can access to the post type edit.php\n\t\t * screen.\n\t\t *\n\t\t * @see LLMS_Admin_Menus::instructor_menu_hack\n\t\t *\n\t\t * @since 7.0.1\n\t\t *\n\t\t * @param string[] $roles The list of WP_User roles which need the hack.\n\t\t */\n\t\t$roles = apply_filters( 'llms_instructor_menu_hack_roles', array( 'instructor', 'instructors_assistant' ) );\n\n\t\t$user = wp_get_current_user();\n\t\tif ( array_intersect( $roles, $user->roles ) ) {\n\t\t\tremove_menu_page( 'edit.php' );\n\t\t}\n\t}\n\n\t/**\n\t * Output the HTML for admin dashboard screen.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function dashboard_page_init() {\n\t\tLLMS_Admin_Dashboard::register_meta_boxes();\n\t\tLLMS_Admin_Dashboard::output();\n\t}\n\n\t/**\n\t * Output the HTML for admin settings screens\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Removed loading the LLMS_Admin_Settings class file that is now handled by the autoloader.\n\t *\n\t * @return void\n\t */\n\tpublic function settings_page_init() {\n\t\tLLMS_Admin_Settings::output();\n\t}\n\n\t/**\n\t * Output the HTML for the reporting screens\n\t *\n\t * @since 3.2.0\n\t * @since 3.13.0 Unknown.\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 4.7.0 Removed inclusion of `LLMS_Admin_Reporting` which is now loaded automatically.\n\t *\n\t * @return void\n\t */\n\tpublic function reporting_page_init() {\n\n\t\tif ( isset( $_GET['student_id'] ) && ! llms_current_user_can( 'view_lifterlms_reports', llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\twp_die( esc_html__( 'You do not have permission to access this content.', 'lifterlms' ) );\n\t\t}\n\n\t\t$reporting = new LLMS_Admin_Reporting();\n\t\t$reporting->output();\n\t}\n\n\t/**\n\t * Include files used on the Status page.\n\t *\n\t * @since 3.37.19\n\t * @since 4.12.0 Added `llms_load_admin_tools` action.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tprotected function status_page_includes() {\n\n\t\t// Tools.\n\t\tforeach ( glob( LLMS_PLUGIN_DIR . 'includes/admin/tools/class-llms-admin-tool-*.php' ) as $tool_path ) {\n\t\t\trequire_once $tool_path;\n\t\t}\n\n\t\t/**\n\t\t * Action which can be used by 3rd parties to load custom admin page tools.\n\t\t *\n\t\t * @since 4.12.0\n\t\t */\n\t\tdo_action( 'llms_load_admin_tools' );\n\t}\n\n\t/**\n\t * Handle form submission actions on the status pages\n\t *\n\t * @since 3.11.2\n\t * @since 3.37.19 Load tools-related files.\n\t *\n\t * @return void\n\t */\n\tpublic function status_page_actions() {\n\t\t$this->status_page_includes();\n\t\tLLMS_Admin_Page_Status::handle_actions();\n\t}\n\n\t/**\n\t * Output the HTML for the Status Pages\n\t *\n\t * @since Unknown\n\t * @since 3.11.2 Unknown.\n\t * @since 3.37.19 Load tools-related files.\n\t *\n\t * @return void\n\t */\n\tpublic function status_page_init() {\n\t\t$this->status_page_includes();\n\t\tLLMS_Admin_Page_Status::output();\n\t}\n\n\t/**\n\t * Output the HTML for admin resources screen.\n\t *\n\t * @since 7.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function resources_page_init() {\n\t\tLLMS_Admin_Resources::register_meta_boxes();\n\t\tLLMS_Admin_Resources::output();\n\t}\n}\n\nreturn new LLMS_Admin_Menus();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.notices.core.php",
    "content": "<?php\n/**\n * Manage core admin notices\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.0.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Manage core admin notices class.\n *\n * @since 3.0.0\n * @since 6.0.0 Removed the deprecated `LLMS_Admin_Notices_Core::check_staging()` method.\n */\nclass LLMS_Admin_Notices_Core {\n\n\t/**\n\t * Init.\n\t *\n\t * @since 3.0.0\n\t * @since 3.14.8 Add handler for removing dismissed notices.\n\t * @since 7.1.0 Do not add a callback to remove sidebar notice on `switch_theme` anymore.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tadd_action( 'admin_head', array( __CLASS__, 'maybe_hide_notices' ), 1 );\n\t\tadd_action( 'current_screen', array( __CLASS__, 'maybe_hide_notices' ), 999 );\n\n\t\tadd_action( 'current_screen', array( __CLASS__, 'add_init_actions' ) );\n\t}\n\n\t/**\n\t * Add actions on different hooks depending on the current screen.\n\t *\n\t * Adds later for LLMS Settings screens to accommodate for settings that are updated later in the load cycle.\n\t *\n\t * @since 3.0.0\n\t * @since 4.12.0 Remove hook for deprecated `check_staging()` notice.\n\t * @since 7.1.0 Do not add a callback to show the missing sidebar support anymore.\n\t * @since 7.7.0 Add notice for media protection on certain hosting servers.\n\t *\n\t * @return void\n\t */\n\tpublic static function add_init_actions() {\n\n\t\t$screen = get_current_screen();\n\t\tif ( ! empty( $screen->base ) && 'lifterlms_page_llms-settings' === $screen->base ) {\n\t\t\t$action   = 'lifterlms_settings_notices';\n\t\t\t$priority = 5;\n\t\t} else {\n\t\t\t$action   = 'current_screen';\n\t\t\t$priority = 77;\n\t\t}\n\n\t\tadd_action( $action, array( __CLASS__, 'gateways' ), $priority );\n\t\tadd_action( $action, array( __CLASS__, 'media_protection' ), $priority );\n\t\tadd_action( $action, array( __CLASS__, 'beaver_builder' ), $priority );\n\t}\n\n\tpublic static function beaver_builder() {\n\t\t$id = 'beaver-builder-lab';\n\n\t\tif ( class_exists( 'LifterLMS_Labs' ) && llms_parse_bool( get_option( 'llms_lab_beaver-builder_enabled' ) ) && current_user_can( 'manage_lifterlms' ) ) {\n\t\t\t$html = sprintf(\n\t\t\t\t__( 'Improved Beaver Builder support is now included in core! To use it, %1$sdisable the Beaver Builder lab%2$s.', 'lifterlms' ),\n\t\t\t\t'<a href=\"' . admin_url( 'admin.php?page=llms-labs' ) . '\">',\n\t\t\t\t'</a>',\n\t\t\t);\n\n\t\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t\t$id,\n\t\t\t\t$html,\n\t\t\t\tarray(\n\t\t\t\t\t'type'             => 'warning',\n\t\t\t\t\t'dismiss_for_days' => 730, // @TODO: there should be a \"forever\" setting here.\n\t\t\t\t\t'remindable'       => false,\n\t\t\t\t)\n\t\t\t);\n\t\t} elseif ( LLMS_Admin_Notices::has_notice( $id ) ) {\n\t\t\tLLMS_Admin_Notices::delete_notice( $id );\n\t\t}\n\t}\n\n\t/**\n\t * Check for gateways and output gateway notice\n\t *\n\t * @since 3.0.0\n\t * @since 3.13.0 Unknown.\n\t * @since 4.5.0 Dismiss notice for 2 years instead of 7 days.\n\t *\n\t * @return void\n\t */\n\tpublic static function gateways() {\n\t\t$id = 'no-gateways';\n\n\t\tif ( ! apply_filters( 'llms_admin_notice_no_payment_gateways', llms()->payment_gateways()->has_gateways( true ) ) ) {\n\t\t\t$html  = __( 'No LifterLMS Payment Gateways are currently enabled. Students will only be able to enroll in courses or memberships with free access plans.', 'lifterlms' ) . '<br><br>';\n\t\t\t$html .= sprintf(\n\t\t\t\t__( 'For starters you can configure manual payments on the %1$sCheckout Settings tab%2$s. Be sure to check out all the available %3$sLifterLMS Payment Gateways%4$s and install one later so that you can start selling your courses and memberships.', 'lifterlms' ),\n\t\t\t\t'<a href=\"' . add_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'page' => 'llms-settings',\n\t\t\t\t\t\t'tab'  => 'checkout',\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t) . '\">',\n\t\t\t\t'</a>',\n\t\t\t\t'<a href=\"https://lifterlms.com/product-category/plugins/payment-gateways/\" target=\"_blank\">',\n\t\t\t\t'</a>'\n\t\t\t);\n\t\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t\t$id,\n\t\t\t\t$html,\n\t\t\t\tarray(\n\t\t\t\t\t'type'             => 'warning',\n\t\t\t\t\t'dismiss_for_days' => 730, // @TODO: there should be a \"forever\" setting here.\n\t\t\t\t\t'remindable'       => true,\n\t\t\t\t)\n\t\t\t);\n\t\t} elseif ( LLMS_Admin_Notices::has_notice( $id ) ) {\n\t\t\tLLMS_Admin_Notices::delete_notice( $id );\n\t\t}\n\t}\n\n\t/**\n\t * Check for Nginx and output a notice about media protection.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic static function media_protection() {\n\t\t$id = 'using-nginx';\n\t\tif (\n\t\t\tapply_filters(\n\t\t\t\t'llms_admin_notice_using_nginx',\n\t\t\t\t( ! empty( $GLOBALS['is_nginx'] && $GLOBALS['is_nginx'] ) )\n\t\t\t\t||\n\t\t\t\t( function_exists( 'is_wpe' ) && is_wpe() )\n\t\t\t) ) {\n\t\t\t$html = sprintf(\n\t\t\t\t/* translators: 1. opening link tag; 2. closing link tag */\n\t\t\t\t__( 'For the best protection for your media files, you should use this doc to add this %1$sNGINX redirect rule%2$s.', 'lifterlms' ),\n\t\t\t\t'<a href=\"https://lifterlms.com/docs/protected-media-files-on-nginx/\" target=\"_blank\">',\n\t\t\t\t'</a>'\n\t\t\t);\n\t\t\t$html .= '<br><br>' . __( 'If you have already reviewed these instructions you may dismiss this notice.', 'lifterlms' );\n\n\t\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t\t$id,\n\t\t\t\t$html,\n\t\t\t\tarray(\n\t\t\t\t\t'type'             => 'warning',\n\t\t\t\t\t'dismiss_for_days' => 10000,\n\t\t\t\t\t'remindable'       => true,\n\t\t\t\t)\n\t\t\t);\n\t\t} elseif ( LLMS_Admin_Notices::has_notice( $id ) ) {\n\t\t\tLLMS_Admin_Notices::delete_notice( $id );\n\t\t}\n\t}\n\n\t/**\n\t * Don't display notices on specific pages\n\t *\n\t * @since 3.14.8\n\t * @since 3.16.14 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function maybe_hide_notices() {\n\n\t\t$screen = get_current_screen();\n\n\t\tif ( $screen && 'admin_page_llms-course-builder' === $screen->id ) {\n\n\t\t\tremove_all_actions( 'admin_notices' ); // 3rd party notices.\n\t\t\tremove_action( 'admin_print_styles', array( 'LLMS_Admin_Notices', 'output_notices' ) ); // Notices output by LifterLMS.\n\n\t\t}\n\t}\n\n\t/**\n\t * Check theme support for LifterLMS Sidebars.\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.4 Unknown.\n\t * @since 4.5.0 Use strict comparison for `in_array()`.\n\t * @deprecated 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic static function sidebar_support() {\n\n\t\t_deprecated_function( __METHOD__, '7.1.0' );\n\n\t\t$theme = wp_get_theme();\n\n\t\t$id = 'sidebars';\n\n\t\tif ( ! current_theme_supports( 'lifterlms-sidebars' ) && ! in_array( $theme->get_template(), llms_get_core_supported_themes(), true ) ) {\n\n\t\t\t$msg = sprintf(\n\t\t\t\t__( '<strong>The current theme, %1$s, does not declare support for LifterLMS Sidebars.</strong> Course and Lesson sidebars may not work as expected. Please see our %2$sintegration guide%3$s or check out our %4$sLaunchPad%5$s theme which is designed specifically for use with LifterLMS.', 'lifterlms' ),\n\t\t\t\t$theme->get( 'Name' ),\n\t\t\t\t'<a href=\"https://lifterlms.com/docs/lifterlms-sidebar-support/?utm_source=notice&utm_medium=product&utm_content=sidebarsupport&utm_campaign=lifterlmsplugin\" target=\"_blank\">',\n\t\t\t\t'</a>',\n\t\t\t\t'<a href=\"https://lifterlms.com/product/launchpad/?utm_source=notice&utm_medium=product&utm_content=launchpad&utm_campaign=lifterlmsplugin\" target=\"_blank\">',\n\t\t\t\t'</a>'\n\t\t\t);\n\n\t\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t\t$id,\n\t\t\t\t$msg,\n\t\t\t\tarray(\n\t\t\t\t\t'dismissible'      => true,\n\t\t\t\t\t'dismiss_for_days' => 730, // @TODO: there should be a \"forever\" setting here.\n\t\t\t\t\t'remindable'       => false,\n\t\t\t\t\t'type'             => 'warning',\n\t\t\t\t)\n\t\t\t);\n\n\t\t} elseif ( LLMS_Admin_Notices::has_notice( $id ) ) {\n\n\t\t\tLLMS_Admin_Notices::delete_notice( $id );\n\n\t\t}\n\t}\n\n\t/**\n\t * Removes the current sidebar notice (if present) and clears notice delay transients.\n\t *\n\t * Called when theme is switched.\n\t *\n\t * @since 3.14.7\n\t * @deprecated 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic static function clear_sidebar_notice() {\n\n\t\t_deprecated_function( __METHOD__, '7.1.0' );\n\n\t\tif ( LLMS_Admin_Notices::has_notice( 'sidebars' ) ) {\n\t\t\tLLMS_Admin_Notices::delete_notice( 'sidebars' );\n\t\t} else {\n\t\t\tdelete_transient( 'llms_admin_notice_sidebars_delay' );\n\t\t}\n\t}\n}\n\nLLMS_Admin_Notices_Core::init();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.notices.php",
    "content": "<?php\n/**\n * LLMS_Admin_Notices class file.\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.0.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Admin Notices.\n *\n * @since 3.0.0\n */\nclass LLMS_Admin_Notices {\n\n\t/**\n\t * Array of messages to display.\n\t *\n\t * @var array\n\t */\n\tprivate static $notices = array();\n\n\t/**\n\t * Array of messages already displayed in the current request.\n\t *\n\t * @var array\n\t */\n\tprivate static $printed_notices = array();\n\n\t/**\n\t * Static constructor\n\t *\n\t * @since 3.0.0\n\t * @since 4.13.0 Populate the `self::$notices` using `self::load_notices()`.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tself::$notices = self::load_notices();\n\n\t\tadd_action( 'wp_loaded', array( __CLASS__, 'hide_notices' ) );\n\t\tadd_action( 'current_screen', array( __CLASS__, 'add_output_actions' ) );\n\t\tadd_action( 'shutdown', array( __CLASS__, 'save_notices' ) );\n\t}\n\n\t/**\n\t * Add output notice actions depending on the current screen.\n\t *\n\t * Notices are added later for LifterLMS settings screens to accommodate\n\t * settings that are updated later in the load cycle.\n\t *\n\t * @since 3.0.0\n\t * @since 5.9.0 Output notices at `admin_notices` in favor of `admin_print_styles`.\n\t *\n\t * @return void\n\t */\n\tpublic static function add_output_actions() {\n\n\t\t$screen = get_current_screen();\n\t\tif ( ! empty( $screen->base ) && 'lifterlms_page_llms-settings' === $screen->base ) {\n\t\t\tadd_action( 'lifterlms_settings_notices', array( __CLASS__, 'output_notices' ) );\n\t\t} else {\n\t\t\tadd_action( 'admin_notices', array( __CLASS__, 'output_notices' ) );\n\t\t}\n\t}\n\n\t/**\n\t * Add a notice\n\t *\n\t * Saves options to the database to be output later\n\t *\n\t * @since 3.0.0\n\t * @since 3.3.0 Added \"flash\" option.\n\t *\n\t * @param string $notice_id       Unique id of the notice.\n\t * @param string $html_or_options Html content of the notice for short notices that don't need a template\n\t *                                or an array of options, html of the notice will be in a template\n\t *                                passed as the \"template\" param of this array.\n\t * @param array  $options         Array of options, when passing html directly via $html_or_options.\n\t *                                Notice options should be passed in this array.\n\t * @return void\n\t */\n\tpublic static function add_notice( $notice_id, $html_or_options = '', $options = array() ) {\n\n\t\t// Don't add the notice if we've already dismissed or delayed it.\n\t\tif ( get_transient( 'llms_admin_notice_' . $notice_id . '_delay' ) ||\n\t\t\t( is_numeric( get_option( 'llms_admin_notice_' . $notice_id . '_delay' ) ) &&\n\t\t\t\ttime() < get_option( 'llms_admin_notice_' . $notice_id . '_delay' )\n\t\t\t) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_array( $html_or_options ) ) {\n\n\t\t\t$options = $html_or_options;\n\n\t\t} else {\n\n\t\t\t$options['html'] = $html_or_options;\n\t\t}\n\n\t\t$options = wp_parse_args(\n\t\t\t$options,\n\t\t\tarray(\n\t\t\t\t'dismissible'      => true,\n\t\t\t\t'dismiss_for_days' => 7,\n\t\t\t\t'flash'            => false, // If true, will delete the notice after displaying it.\n\t\t\t\t'html'             => '',\n\t\t\t\t'remind_in_days'   => 7,\n\t\t\t\t'remindable'       => false,\n\t\t\t\t'type'             => 'info', // Info, warning, success, error.\n\t\t\t\t'template'         => false, // Template name, eg \"admin/notices/notice.php\".\n\t\t\t\t'template_path'    => '', // Allow override of default llms()->template_path().\n\t\t\t\t'default_path'     => '', // Allow override of default path llms()->plugin_path() . '/templates/'. An addon may add a notice and pass it's own path in here.\n\t\t\t)\n\t\t);\n\n\t\tself::$notices = array_unique( array_merge( self::get_notices(), array( $notice_id ) ) );\n\t\tupdate_option( 'llms_admin_notice_' . $notice_id, $options );\n\t}\n\n\t/**\n\t * Delete a notice by id\n\t *\n\t * @since 3.0.0\n\t * @since 3.4.3 Unknown.\n\t *\n\t * @param string $notice_id Unique id of the notice.\n\t * @param string $trigger   Deletion action/trigger, accepts 'delete' (default), 'hide', or 'remind'.\n\t * @return void\n\t */\n\tpublic static function delete_notice( $notice_id, $trigger = 'delete' ) {\n\t\tself::$notices = array_diff( self::get_notices(), array( $notice_id ) );\n\t\t$notice        = self::get_notice( $notice_id );\n\t\tdelete_option( 'llms_admin_notice_' . $notice_id );\n\t\tif ( $notice ) {\n\t\t\t$delay = 0;\n\t\t\tif ( 'remind' === $trigger && $notice['remindable'] ) {\n\t\t\t\t$delay = isset( $notice['remind_in_days'] ) ? $notice['remind_in_days'] : 0;\n\t\t\t}\n\t\t\tif ( 'hide' === $trigger && $notice['dismissible'] ) {\n\t\t\t\t$delay = isset( $notice['dismiss_for_days'] ) ? $notice['dismiss_for_days'] : 7;\n\t\t\t}\n\t\t\tif ( $delay ) {\n\t\t\t\tupdate_option( 'llms_admin_notice_' . $notice_id . '_delay', time() + ( DAY_IN_SECONDS * $delay ) );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Hook run when a notice is dismissed.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook `{$trigger}` refers to the deletion trigger, either 'delete',\n\t\t\t * 'hide', or 'remind'.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `{$notice_id}` refers to the ID of the notice being dismissed.\n\t\t\t *\n\t\t\t * @since 4.10.0\n\t\t\t */\n\t\t\tdo_action( \"lifterlms_{$trigger}_{$notice_id}_notice\" );\n\t\t}\n\t}\n\n\t/**\n\t * Flash a notice on screen, isn't saved and is automatically deleted after being displayed\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param string $message Message text / html to display onscreen.\n\t * @param string $type    Notice type [info|warning|success|error].\n\t * @return void\n\t */\n\tpublic static function flash_notice( $message, $type = 'info' ) {\n\n\t\t$id = 'llms-flash-notice-';\n\t\t$i  = 0;\n\n\t\t// Increment the notice id so we can flash multiple notices on screen in one load if necessary.\n\t\twhile ( self::has_notice( $id . $i ) ) {\n\t\t\t++$i;\n\t\t}\n\n\t\t$id = $id . $i;\n\n\t\tself::add_notice(\n\t\t\t$id,\n\t\t\t$message,\n\t\t\tarray(\n\t\t\t\t'dismissible' => false,\n\t\t\t\t'flash'       => true,\n\t\t\t\t'type'        => $type,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Get notice details array from the DB\n\t *\n\t * @since 3.0.0\n\t * @since 4.13.0 When the notice cannot be found, return an empty array in favor of an empty string.\n\t *\n\t * @param string $notice_id Notice id.\n\t * @return array\n\t */\n\tpublic static function get_notice( $notice_id ) {\n\t\treturn get_option( 'llms_admin_notice_' . $notice_id, array() );\n\t}\n\n\t/**\n\t * Get notices\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_notices() {\n\t\treturn self::$notices;\n\t}\n\n\t/**\n\t * Determine if a notice is already set\n\t *\n\t * @since 3.0.0\n\t * @since 4.10.0 Use a strict comparison.\n\t *\n\t * @param string $notice_id Id of the notice.\n\t * @return boolean\n\t */\n\tpublic static function has_notice( $notice_id ) {\n\t\treturn in_array( $notice_id, self::get_notices(), true );\n\t}\n\n\t/**\n\t * Called when \"Dismiss X\" or \"Remind Me\" is clicked on a notice\n\t *\n\t * Validates request and deletes the notice.\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Unslash input data.\n\t * @since 5.2.0 Remove notice and notice query string vars and redirect after clearing.\n\t *\n\t * @return void\n\t */\n\tpublic static function hide_notices() {\n\t\tif ( ( isset( $_GET['llms-hide-notice'] ) || isset( $_GET['llms-remind-notice'] ) ) && isset( $_GET['_llms_notice_nonce'] ) ) {\n\t\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_llms_notice_nonce'] ) ), 'llms_hide_notices_nonce' ) ) {\n\t\t\t\twp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'lifterlms' ) );\n\t\t\t}\n\t\t\tif ( ! current_user_can( 'manage_options' ) ) {\n\t\t\t\twp_die( esc_html__( 'Cheatin&#8217; huh?', 'lifterlms' ) );\n\t\t\t}\n\t\t\tif ( isset( $_GET['llms-hide-notice'] ) ) {\n\t\t\t\t$notice = sanitize_text_field( wp_unslash( $_GET['llms-hide-notice'] ) );\n\t\t\t\t$action = 'hide';\n\t\t\t} elseif ( isset( $_GET['llms-remind-notice'] ) ) {\n\t\t\t\t$notice = sanitize_text_field( wp_unslash( $_GET['llms-remind-notice'] ) );\n\t\t\t\t$action = 'remind';\n\t\t\t}\n\t\t\tself::delete_notice( $notice, $action );\n\t\t\tllms_redirect_and_exit( remove_query_arg( array( 'llms-hide-notice', 'llms-remind-notice', '_llms_notice_nonce' ) ) );\n\t\t}\n\t}\n\n\t/**\n\t * Loads stored notice IDs from the database\n\t *\n\t * Handles potentially malformed data by ensuring that only an array of strings\n\t * can be loaded.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return string[]\n\t */\n\tprotected static function load_notices() {\n\n\t\t$notices = get_option( 'llms_admin_notices', array() );\n\n\t\tif ( ! is_array( $notices ) ) {\n\t\t\t$notices = array( $notices );\n\t\t}\n\n\t\t// Remove empty and non-string values.\n\t\treturn array_filter(\n\t\t\t$notices,\n\t\t\tfunction ( $notice ) {\n\t\t\t\treturn ( ! empty( $notice ) && is_string( $notice ) );\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * Output a single notice by ID\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.4 Unknown.\n\t * @since 5.2.0 Ensure `template_path` and `default_path` are properly passed to `llms_get_template()`.\n\t * @since 5.3.1 Delete empty notices and do not display them.\n\t *\n\t * @param string $notice_id Notice id.\n\t * @return void\n\t */\n\tpublic static function output_notice( $notice_id ) {\n\n\t\tif ( current_user_can( 'manage_options' ) ) {\n\n\t\t\t$notice = self::get_notice( $notice_id );\n\n\t\t\t// Don't output those rogue empty notices I can't find.\n\t\t\t// @todo find the source.\n\t\t\tif ( empty( $notice ) || ( empty( $notice['template'] ) && empty( $notice['html'] ) ) ) {\n\t\t\t\tself::delete_notice( $notice_id );\n\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t?>\n\t\t\t<div class=\"notice notice-<?php echo esc_attr( $notice['type'] ); ?> llms-admin-notice\" id=\"llms-notice<?php echo esc_attr( $notice_id ); ?>\" style=\"position:relative;\">\n\t\t\t\t<div class=\"llms-admin-notice-icon\"></div>\n\t\t\t\t<div class=\"llms-admin-notice-content\">\n\t\t\t\t\t<?php if ( $notice['dismissible'] ) : ?>\n\t\t\t\t\t\t<a class=\"notice-dismiss\" href=\"<?php echo esc_url( wp_nonce_url( add_query_arg( 'llms-hide-notice', $notice_id ), 'llms_hide_notices_nonce', '_llms_notice_nonce' ) ); ?>\">\n\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Dismiss', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php if ( ! empty( $notice['template'] ) ) : ?>\n\n\t\t\t\t\t\t<?php llms_get_template( $notice['template'], array(), $notice['template_path'], $notice['default_path'] ); ?>\n\n\t\t\t\t\t<?php elseif ( ! empty( $notice['html'] ) ) : ?>\n\n\t\t\t\t\t\t<?php echo wpautop( wp_kses_post( $notice['html'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already using wp_kses_post() ?>\n\n\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t<?php if ( $notice['remindable'] ) : ?>\n\t\t\t\t\t\t<p style=\"text-align:right;\"><a class=\"button\" href=\"<?php echo esc_url( wp_nonce_url( add_query_arg( 'llms-remind-notice', $notice_id ), 'llms_hide_notices_nonce', '_llms_notice_nonce' ) ); ?>\"><?php esc_html_e( 'Remind me later', 'lifterlms' ); ?></a></p>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<?php\n\n\t\t\tif ( isset( $notice['flash'] ) && $notice['flash'] ) {\n\t\t\t\tself::delete_notice( $notice_id, 'delete' );\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic static function output_global_notices() {\n\n\t\tif ( ! current_user_can( 'manage_options' ) ) {\n\t\t\treturn;\n\t\t}\n\t\tglobal $lifterlms_banner_notifications;\n\n\t\t// Default to showing notification banners.\n\t\t$show_notifications = true;\n\n\t\t// Hide notifications if the user has disabled them.\n\t\tif ( $lifterlms_banner_notifications->get_max_notification_priority() < 1 ) {\n\t\t\t$show_notifications = false;\n\t\t}\n\n\t\tif ( $show_notifications ) :\n\t\t\t?>\n\t\t\t<div id=\"lifterlms-notifications\" style=\"position:relative;\">\n\t\t\t</div>\n\t\t\t<?php\n\t\t\t// To debug a specific notification.\n\t\t\tif ( ! empty( $_REQUEST['lifterlms_notification'] ) ) {\n\t\t\t\t$specific_notification = '&lifterlms_notification=' . intval( $_REQUEST['lifterlms_notification'] );\n\t\t\t} else {\n\t\t\t\t$specific_notification = '';\n\t\t\t}\n\t\t\t?>\n\t\t\t<script>\n\t\t\t\tjQuery(document).ready( function() {\n\t\t\t\t\tjQuery.get('<?php echo esc_url_raw( admin_url( 'admin-ajax.php?action=lifterlms_notifications' . $specific_notification ) ); ?>', function(data) {\n\t\t\t\t\t\tif ( data && data != 'NULL' )\n\t\t\t\t\t\t\tjQuery( '#lifterlms-notifications' ).html( data );\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t</script>\n\t\t\t<?php\n\t\tendif;\n\t}\n\n\t/**\n\t * Output all saved notices.\n\t *\n\t * @since 3.0.0\n\t * @since 7.1.0 Made sure to print the notices only once.\n\t *\n\t * @return void\n\t */\n\tpublic static function output_notices() {\n\n\t\t$notices_to_print = array_diff( self::get_notices(), self::$printed_notices );\n\n\t\tforeach ( $notices_to_print as $notice_id ) {\n\t\t\tself::output_notice( $notice_id );\n\t\t\tself::$printed_notices[] = $notice_id;\n\t\t}\n\n\t\tself::output_global_notices();\n\t}\n\n\t/**\n\t * Save notices in the database\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function save_notices() {\n\t\tupdate_option( 'llms_admin_notices', self::get_notices() );\n\t}\n}\n\nLLMS_Admin_Notices::init();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.page.status.php",
    "content": "<?php\n/**\n * Admin Status Pages\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.11.2\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Page_Status class\n *\n * @since 3.11.2\n * @since 3.32.0 Add \"Scheduled Actions\" tab.\n * @since 3.33.1 Read log files using `llms_filter_input`.\n * @since 3.33.2 Fix undefined index when viewing log files.\n * @since 3.35.0 Sanitize input data.\n * @since 3.37.14 Added the WP Core `debug.log` file as log that's viewable via the log viewer.\n * @since 4.0.0 The `clear-sessions` tool has been moved to `LLMS_Admin_Tool_Clear_Sessions`.\n */\nclass LLMS_Admin_Page_Status {\n\n\t/**\n\t * Register \"unclassed\" core tools\n\t *\n\t * @since 4.13.0\n\t *\n\t * @param array[] $tools List of tool definitions.\n\t * @return array[]\n\t */\n\tpublic static function add_core_tools( $tools ) {\n\n\t\treturn array_merge(\n\t\t\t$tools,\n\t\t\tarray(\n\n\t\t\t\t'reset-tracking' => array(\n\t\t\t\t\t'description' => __( 'If you opted into LifterLMS Tracking and no longer wish to participate, you may opt out here.', 'lifterlms' ),\n\t\t\t\t\t'label'       => __( 'Reset Tracking Settings', 'lifterlms' ),\n\t\t\t\t\t'text'        => __( 'Reset Tracking Settings', 'lifterlms' ),\n\t\t\t\t),\n\n\t\t\t\t'clear-cache'    => array(\n\t\t\t\t\t'description' => __( 'Clears the cached data displayed on various reporting screens. This does not affect actual student progress, it only clears cached progress data. This data will be regenerated the next time it is accessed.', 'lifterlms' ),\n\t\t\t\t\t'label'       => __( 'Student Progress Cache', 'lifterlms' ),\n\t\t\t\t\t'text'        => __( 'Clear cache', 'lifterlms' ),\n\t\t\t\t),\n\n\t\t\t\t'setup-wizard'   => array(\n\t\t\t\t\t'description' => __( 'If you want to run the LifterLMS Setup Wizard again or skipped it and want to return now, click below.', 'lifterlms' ),\n\t\t\t\t\t'label'       => __( 'Setup Wizard', 'lifterlms' ),\n\t\t\t\t\t'text'        => __( 'Return to Setup Wizard', 'lifterlms' ),\n\t\t\t\t),\n\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Handle tools actions\n\t *\n\t * @since 3.11.2\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.37.14 Verify user capabilities when doing a tool action.\n\t *                Use `llms_redirect_and_exit()` in favor of `wp_safe_redirect()`.\n\t * @since 4.0.0 The `clear-sessions` tool has been moved to `LLMS_Admin_Tool_Clear_Sessions`.\n\t * @since 4.13.0 The `automatic-payments` tool has been moved to `LLMS_Admin_Tool_Reset_Automatic_Payments`.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tprivate static function do_tool() {\n\n\t\tif ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'llms_tool' ) || ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\twp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'lifterlms' ) );\n\t\t}\n\n\t\t$tool = llms_filter_input_sanitize_string( INPUT_POST, 'llms_tool' );\n\n\t\t/**\n\t\t * Custom and 3rd party tools can use this action to perform the tool's action\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @see llms_status_tools For the filter used to register tools.\n\t\t *\n\t\t * @param string $tool Tool name or ID.\n\t\t */\n\t\tdo_action( 'llms_status_tool', $tool );\n\n\t\tswitch ( $tool ) {\n\n\t\t\tcase 'clear-cache':\n\t\t\t\tglobal $wpdb;\n\t\t\t\t$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\t\t\t\t\"DELETE FROM {$wpdb->usermeta} WHERE meta_key = 'llms_overall_progress' or meta_key = 'llms_overall_grade';\"\n\t\t\t\t);\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'reset-tracking':\n\t\t\t\tupdate_option( 'llms_allow_tracking', 'no' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'setup-wizard':\n\t\t\t\tllms_redirect_and_exit( esc_url_raw( admin_url( 'admin.php?page=llms-setup' ) ) );\n\t\t\t\tbreak;\n\n\t\t}\n\t}\n\n\t/**\n\t * Handle form / link actions on the status pages\n\t *\n\t * @since 3.11.2\n\t *\n\t * @return void\n\t */\n\tpublic static function handle_actions() {\n\n\t\tif ( ! empty( $_REQUEST['llms_delete_log'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonces are verified elsewhere.\n\t\t\tself::remove_log_file();\n\t\t} elseif ( ! empty( $_REQUEST['llms_tool'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonces are verified elsewhere.\n\t\t\tself::do_tool();\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve the URL to the status page\n\t *\n\t * @since 3.11.2\n\t *\n\t * @param string $tab Optionally add a tab.\n\t * @return string\n\t */\n\tpublic static function get_url( $tab = null ) {\n\t\t$args = array(\n\t\t\t'page' => 'llms-status',\n\t\t);\n\t\tif ( $tab ) {\n\t\t\t$args['tab'] = $tab;\n\t\t}\n\t\treturn add_query_arg( $args, admin_url( 'admin.php' ) );\n\t}\n\n\t/**\n\t * Retrieve an array of log files\n\t *\n\t * @since 3.11.2\n\t * @since 3.37.14 Add the WP debug.log file to the array if `WP_DEBUG_LOG` is enabled.\n\t *\n\t * @return array[] Associative array of log files. The array key is the file \"slug\" and the value is the file's absolute path.\n\t */\n\tprivate static function get_logs() {\n\n\t\t$result = array();\n\n\t\t// Retrieve all the files in our log directory.\n\t\t$files = @scandir( LLMS_LOG_DIR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- It's okay though.\n\t\tif ( ! empty( $files ) ) {\n\t\t\tforeach ( $files as $key => $value ) {\n\n\t\t\t\t// Ignore directory dots, directories, and non .log files.\n\t\t\t\tif ( in_array( $value, array( '.', '..' ), true ) || is_dir( $value ) || ! strstr( $value, '.log' ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$result[ sanitize_title( $value ) ] = LLMS_LOG_DIR . $value;\n\n\t\t\t}\n\t\t}\n\n\t\t// Add the site's debug.log or native error log file if it exists.\n\t\t$err_path = ini_get( 'error_log' );\n\t\tif ( $err_path ) {\n\t\t\t$result['debug-log'] = $err_path;\n\t\t}\n\n\t\treturn $result;\n\t}\n\n\t/**\n\t * Output the system report\n\t *\n\t * @since 2.1.0\n\t * @since 3.32.0 Add \"Scheduled Actions\" tab output.\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.37.14 Use strict comparators.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\n\t\t$tabs = apply_filters(\n\t\t\t'llms_admin_page_status_tabs',\n\t\t\tarray(\n\t\t\t\t'report'           => __( 'System Report', 'lifterlms' ),\n\t\t\t\t'tools'            => __( 'Tools & Utilities', 'lifterlms' ),\n\t\t\t\t'logs'             => __( 'Logs', 'lifterlms' ),\n\t\t\t\t'action-scheduler' => __( 'Scheduled Actions', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\t$current_tab = empty( $_GET['tab'] ) ? 'report' : llms_filter_input_sanitize_string( INPUT_GET, 'tab' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We're not processing the form data.\n\t\t?>\n\n\t\t<div class=\"wrap lifterlms llms-status llms-status--<?php echo esc_attr( $current_tab ); ?>\">\n\n\t\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\t\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t\t<?php\n\t\t\t\tforeach ( $tabs as $name => $label ) :\n\t\t\t\t\t$active = ( $current_tab === $name ) ? ' llms-active' : '';\n\t\t\t\t\t?>\n\t\t\t\t\t<li class=\"llms-nav-item<?php echo esc_attr( $active ); ?>\"><a class=\"llms-nav-link\" href=\"<?php echo esc_url( self::get_url( $name ) ); ?>\"><?php echo esc_html( $label ); ?></a></li>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</ul>\n\t\t\t</nav>\n\n\t\t\t<h1 style=\"display:none;\"></h1>\n\n\t\t\t<?php\n\t\t\tdo_action( 'llms_before_admin_page_status', $current_tab );\n\n\t\t\tswitch ( $current_tab ) {\n\n\t\t\t\tcase 'action-scheduler':\n\t\t\t\t\tActionScheduler_AdminView::instance()->render_admin_ui();\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'logs':\n\t\t\t\t\tself::output_logs_content();\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'report':\n\t\t\t\t\tLLMS_Admin_System_Report::output();\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'tools':\n\t\t\t\t\tself::output_tools_content();\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tdo_action( 'llms_after_admin_page_status', $current_tab );\n\t\t\t?>\n\n\t\t</div>\n\n\t\t<?php\n\t}\n\n\t/**\n\t * Delete a log file\n\t *\n\t * @since 3.11.2\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.37.14 Added user capability check.\n\t *\n\t * @return void\n\t */\n\tprivate static function remove_log_file() {\n\n\t\tif ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'delete_log' ) || ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\twp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'lifterlms' ) );\n\t\t}\n\n\t\tif ( ! empty( $_REQUEST['llms_delete_log'] ) ) {\n\n\t\t\t$logs   = self::get_logs();\n\t\t\t$handle = sanitize_title( wp_unslash( $_REQUEST['llms_delete_log'] ) );\n\t\t\t$log    = isset( $logs[ $handle ] ) ? $logs[ $handle ] : false;\n\n\t\t\tif ( $log && is_file( $log ) && is_writable( $log ) ) {\n\t\t\t\tunlink( $log );\n\t\t\t\tllms_redirect_and_exit( esc_url_raw( self::get_url( 'logs' ) ) );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Output the HTML for the Logs tab\n\t *\n\t * @since 3.11.2\n\t * @since 3.33.1 Use `llms_filter_input` to read current log file.\n\t * @since 3.33.2 Fix undefined variable notice.\n\t * @since 3.37.14 Moved HTML output to the view file located at includes/admin/views/status/view-log.php.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tprivate static function output_logs_content() {\n\n\t\t$logs        = self::get_logs();\n\t\t$date_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );\n\n\t\t$log_file = llms_filter_input_sanitize_string( INPUT_POST, 'llms_log_file' );\n\t\t$current  = $log_file ? sanitize_title( $log_file ) : null;\n\n\t\tif ( $logs && ! $current ) {\n\t\t\t$log_keys = array_keys( $logs );\n\t\t\t$current  = array_shift( $log_keys );\n\t\t}\n\n\t\tif ( $logs ) {\n\n\t\t\t// Nonce URL to delete a log file.\n\t\t\t$delete_url = 'debug-log' === $current ? '' : wp_nonce_url(\n\t\t\t\tadd_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'llms_delete_log' => $current,\n\t\t\t\t\t),\n\t\t\t\t\tself::get_url( 'logs' )\n\t\t\t\t),\n\t\t\t\t'delete_log'\n\t\t\t);\n\n\t\t\tinclude_once 'views/status/view-log.php';\n\n\t\t} else {\n\t\t\techo '<div class=\"llms-log-viewer\">' . esc_html__( 'There are currently no logs to view.', 'lifterlms' ) . '</div>';\n\t\t}\n\t}\n\n\t/**\n\t * Output the HTML for the tools tab\n\t *\n\t * @since 3.11.2\n\t * @since 4.0.0 The `clear-sessions` tool has been moved to `LLMS_Admin_Tool_Clear_Sessions`.\n\t * @since 4.13.0 Move \"unclassed\" core actions to be added to the `llms_status_tools` filter at priority 5 via `LLMS_Admin_Page_Status::add_core_tools()`.\n\t *\n\t * @return void\n\t */\n\tprivate static function output_tools_content() {\n\n\t\t// Load unclassed core tools at priority 5 to \"preserve\" their original order before we started classing tools.\n\t\tadd_filter( 'llms_status_tools', array( __CLASS__, 'add_core_tools' ), 5 );\n\n\t\t/**\n\t\t * Register tools with the LifterLMS core\n\t\t *\n\t\t * When registering a custom tool you should additionally have an action triggered for the tool using the action\n\t\t * `llms_status_tool` which will be called to process or handle the action.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @see llms_status_tool For the action called to handle a tool.\n\t\t *\n\t\t * @param array[] $tools {\n\t\t *     Associative array of status tool definitions.\n\t\t *\n\t\t *     The array key is a unique \"id\" for the tool and the array value should be an associative array\n\t\t *     as described below:\n\t\t *\n\t\t *     @type string $description Description of what the tool does.\n\t\t *     @type string $label       The title of the tool.\n\t\t *     @type string $text        The text displayed on the tool's button.\n\t\t * }\n\t\t */\n\t\t$tools = apply_filters( 'llms_status_tools', array() );\n\n\t\t?>\n\t\t<form action=\"<?php echo esc_url( self::get_url( 'tools' ) ); ?>\" method=\"POST\">\n\t\t\t<div class=\"llms-setting-group top\">\n\t\t\t\t<p class=\"llms-label\"><?php esc_html_e( 'Tools & Utilities', 'lifterlms' ); ?></p>\n\t\t\t\t<table class=\"llms-table text-left zebra\">\n\t\t\t\t<?php foreach ( $tools as $slug => $data ) : ?>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th><?php echo esc_html( $data['label'] ); ?></th>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<p><?php echo wp_kses_post( $data['description'] ); ?></p>\n\t\t\t\t\t\t\t<button class=\"llms-button-secondary small\" name=\"llms_tool\" type=\"submit\" value=\"<?php echo esc_attr( $slug ); ?>\"><?php echo esc_html( $data['text'] ); ?></button>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</table>\n\t\t\t\t<?php wp_nonce_field( 'llms_tool' ); ?>\n\t\t\t</div>\n\t\t</form>\n\t\t<?php\n\t}\n}\n"
  },
  {
    "path": "includes/admin/class.llms.admin.post-types.php",
    "content": "<?php\n/**\n * LLMS_Admin_Post_Types class.\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since Unknown\n * @version 6.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Post Types.\n *\n * Sets up post type custom messages and includes base metabox class.\n *\n * @since Unknown.\n * @since 6.0.0 Removed LLMS_Admin_Post_Types::meta_metabox_init() in favor of autoloading.\n */\nclass LLMS_Admin_Post_Types {\n\n\t/**\n\t * Constructor\n\t *\n\t * Adds functions to actions and sets filter on post_updated_messages.\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Disable the block editor for legacy certificates.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'use_block_editor_for_post', array( $this, 'use_block_editor_for_post' ), 20, 2 );\n\n\t\tadd_action( 'admin_init', array( $this, 'include_post_type_metabox_class' ) );\n\n\t\tadd_filter( 'post_updated_messages', array( $this, 'llms_post_updated_messages' ) );\n\n\t}\n\n\t/**\n\t * Admin Menu\n\t *\n\t * Includes base metabox class\n\t *\n\t * @return void\n\t */\n\tpublic function include_post_type_metabox_class() {\n\t\tinclude 'post-types/class.llms.meta.boxes.php';\n\t}\n\n\t/**\n\t * Disables the block editor for legacy certificates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param boolean $use_block_editor Whether or not to use the block editor.\n\t * @param WP_Post $post             Post object.\n\t * @return boolean\n\t */\n\tpublic function use_block_editor_for_post( $use_block_editor, $post ) {\n\t\t$cert = llms_get_certificate( $post, true );\n\t\tif ( $cert && 1 === $cert->get_template_version() ) {\n\t\t\t$use_block_editor = false;\n\t\t}\n\n\t\treturn $use_block_editor;\n\n\t}\n\n\t/**\n\t * Initializes core for metaboxes.\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Admin_Post_Types::meta_metabox_init()` is deprecated with no replacement.\n\t *\n\t * @return void\n\t */\n\tpublic function meta_metabox_init() {\n\n\t\tllms_deprecated_function( __METHOD__, '6.0.0' );\n\n\t\tinclude_once 'llms.class.admin.metabox.php';\n\t}\n\n\t/**\n\t * Customize post type messages.\n\t *\n\t * @since Unknown.\n\t * @since 3.35.0 Fix l10n calls.\n\t * @since 4.7.0 Added `publicly_queryable` check for permalink and preview.\n\t * @since 6.0.0 Handle `llms_my_certificate` and `llms_my_achievement` post types.\n\t * @since 6.7.0 Fixed too few arguments passed to sprintf, when building restore from revision message.\n\t *\n\t * @return array $messages Post updated messages.\n\t */\n\tpublic function llms_post_updated_messages( $messages ) {\n\n\t\tglobal $post;\n\n\t\t$llms_post_types = array(\n\t\t\t'course',\n\t\t\t'section',\n\t\t\t'lesson',\n\t\t\t'llms_order',\n\t\t\t'llms_email',\n\t\t\t'llms_email',\n\t\t\t'llms_certificate',\n\t\t\t'llms_my_certificate',\n\t\t\t'llms_achievement',\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_engagement',\n\t\t\t'llms_quiz',\n\t\t\t'llms_question',\n\t\t\t'llms_coupon',\n\t\t);\n\n\t\tforeach ( $llms_post_types as $type ) {\n\n\t\t\t$obj  = get_post_type_object( $type );\n\t\t\t$name = $obj->labels->singular_name;\n\n\t\t\t$permalink_html    = '';\n\t\t\t$preview_link_html = '';\n\n\t\t\tif ( $obj->publicly_queryable ) {\n\n\t\t\t\t$permalink    = get_permalink( $post->ID );\n\t\t\t\t$preview_link = add_query_arg( 'preview', 'true', $permalink );\n\n\t\t\t\t$link_format = ' <a href=\"%1$s\">%2$s</a>.';\n\n\t\t\t\t$permalink_html    = sprintf( $link_format, $permalink, sprintf( __( 'View %s', 'lifterlms' ), $name ) );\n\t\t\t\t$preview_link_html = sprintf( $link_format, $permalink, sprintf( __( 'Preview %s', 'lifterlms' ), $name ) );\n\t\t\t}\n\n\t\t\t$messages[ $type ] = array(\n\t\t\t\t0  => '',\n\t\t\t\t1  => sprintf( __( '%s updated.', 'lifterlms' ), $name ) . $permalink_html,\n\t\t\t\t2  => __( 'Custom field updated.', 'lifterlms' ),\n\t\t\t\t3  => __( 'Custom field deleted.', 'lifterlms' ),\n\t\t\t\t4  => sprintf( __( '%s updated.', 'lifterlms' ), $name ),\n\t\t\t\t5  => isset( $_GET['revision'] ) ? // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No need to verify the nonce here.\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t__( '%1$s restored to revision from %2$s.', 'lifterlms' ),\n\t\t\t\t\t\t$name,\n\t\t\t\t\t\twp_post_revision_title( llms_filter_input( INPUT_GET, 'revision', FILTER_SANITIZE_NUMBER_INT ), false )\n\t\t\t\t\t)\n\t\t\t\t\t:\n\t\t\t\t\tfalse,\n\t\t\t\t6  => sprintf( __( '%s published.', 'lifterlms' ), $name ) . $permalink_html,\n\t\t\t\t7  => sprintf( __( '%s saved.', 'lifterlms' ), $name ),\n\t\t\t\t8  => sprintf( __( '%s submitted.', 'lifterlms' ), $name ) . $preview_link_html,\n\t\t\t\t9  => sprintf(\n\t\t\t\t\t__( '%1$s scheduled for: <strong>%2$s</strong>.', 'lifterlms' ),\n\t\t\t\t\t$name,\n\t\t\t\t\tdate_i18n( __( 'M j, Y @ G:i', 'lifterlms' ), strtotime( $post->post_date ) )\n\t\t\t\t) . $preview_link_html,\n\t\t\t\t10 => sprintf( __( '%1$s draft updated.', 'lifterlms' ), $name ) . $preview_link_html,\n\t\t\t);\n\n\t\t}\n\n\t\treturn $messages;\n\t}\n\n}\n\nreturn new LLMS_Admin_Post_Types();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.resources.php",
    "content": "<?php\n/**\n * Admin Resources Screen\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 7.4.1\n * @version 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Resources Screen class.\n *\n * @since 7.4.1\n */\nclass LLMS_Admin_Resources {\n\n\t/**\n\t * Retrieve an instance of the WP_Screen for the resources screen.\n\t *\n\t * @since 7.4.1\n\t *\n\t * @return WP_Screen|boolean Returns a `WP_Screen` object when on the resources screen, otherwise returns `false`.\n\t */\n\tpublic static function get_screen() {\n\n\t\t$screen = get_current_screen();\n\t\tif ( $screen instanceof WP_Screen && 'lifterlms_page_llms-resources' === $screen->id ) {\n\t\t\treturn $screen;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Register Resource's meta boxes.\n\t *\n\t * @since 7.4.1\n\t *\n\t * @return void\n\t */\n\tpublic static function register_meta_boxes() {\n\n\t\tadd_meta_box(\n\t\t\t'llms_dashboard_welcome_video',\n\t\t\t__( 'Welcome to LifterLMS', 'lifterlms' ),\n\t\t\tarray( __CLASS__, 'meta_box' ),\n\t\t\t'toplevel_page_llms-resources',\n\t\t\t'normal',\n\t\t\t'default',\n\t\t\tarray( 'view' => 'welcome-video' )\n\t\t);\n\n\t\tadd_meta_box(\n\t\t\t'llms_dashboard_resource_links',\n\t\t\t__( 'Resource Links', 'lifterlms' ),\n\t\t\tarray( __CLASS__, 'meta_box' ),\n\t\t\t'toplevel_page_llms-resources',\n\t\t\t'normal',\n\t\t\t'default',\n\t\t\tarray( 'view' => 'resource-links' )\n\t\t);\n\n\t\tadd_meta_box(\n\t\t\t'llms_dashboard_getting_started',\n\t\t\t__( 'Getting Started', 'lifterlms' ),\n\t\t\tarray( __CLASS__, 'meta_box' ),\n\t\t\t'toplevel_page_llms-resources',\n\t\t\t'side',\n\t\t\t'default',\n\t\t\tarray( 'view' => 'getting-started' )\n\t\t);\n\n\t\t/**\n\t\t * Fired after adding the meta boxes on the LifterLMS admin resources page.\n\t\t *\n\t\t * Third parties can hook here to remove LifterLMS core meta boxes.\n\t\t *\n\t\t * @since 7.4.1\n\t\t */\n\t\tdo_action( 'llms_resources_meta_boxes_added' );\n\t}\n\n\t/**\n\t * Prints the resource's meta box html.\n\t *\n\t * @since 7.4.1\n\t *\n\t * @param mixed $data_object Often this is the object that's the focus of the current screen,\n\t *                           for example a `WP_Post` or `WP_Comment` object.\n\t * @param array $box         Meta Box configuration array.\n\t * @return void\n\t */\n\tpublic static function meta_box( $data_object, $box ) {\n\n\t\tif ( isset( $box['args']['view'] ) ) {\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped in the view file.\n\t\t\techo self::get_view( $box['args']['view'] );\n\t\t}\n\t}\n\n\t/**\n\t * Handle HTML output on the screen.\n\t *\n\t * @since 7.4.1\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\t\tinclude 'views/resources.php';\n\t}\n\n\t/**\n\t * Retrieves the HTML of a view from the views/dashboard directory.\n\t *\n\t * @since 7.4.1\n\t *\n\t * @param string $file The file basename of the view to retrieve.\n\t * @return string The HTML content of the view.\n\t */\n\tprivate static function get_view( $file ) {\n\n\t\tob_start();\n\t\tinclude 'views/resources/' . $file . '.php';\n\t\treturn ob_get_clean();\n\t}\n}\n"
  },
  {
    "path": "includes/admin/class.llms.admin.reviews.php",
    "content": "<?php\n/**\n * Admin Reviews\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Reviews class\n *\n * This class handles the admin side of the reviews.\n * It is responsible for creating the meta box on the course\n * page (and in the future the membership page).\n *\n * @since Unknown\n */\nclass LLMS_Admin_Reviews {\n\n\tpublic static $prefix = '_';\n\n\t/**\n\t * The constructor for the class. It adds the methods here to the appropriate\n\t * actions. The actions are for:\n\t * 1) Creating the custom column set in the llms_review post screen\n\t * 2) Making a column sortable\n\t * 3) Adding content to the column\n\t * 4) Outputting the content.\n\t * 5) Adding the meta boxes to the course page\n\t * 6) Handling the saving of the data\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'manage_llms_review_posts_columns', array( $this, 'init' ) );\n\t\tadd_action( 'manage_edit-llms_review_sortable_columns', array( $this, 'make_columns_sortable' ) );\n\t\tadd_action( 'manage_llms_review_posts_custom_column', array( $this, 'generate_column_data' ), 10, 2 );\n\t\tadd_filter( 'llms_metabox_fields_lifterlms_course_options', array( $this, 'add_review_meta_boxes' ) );\n\t\tadd_action( 'save_post', array( $this, 'save_review_meta_boxes' ) );\n\t}\n\n\t/**\n\t * This function generates the custom column set. It takes in\n\t * the array of standard columns, then modifies that set to\n\t * contain the needed fields.\n\t *\n\t * @param array $columns The array of standard WP columns\n\t *\n\t * @return array The updated array of columns.\n\t */\n\tpublic function init( $columns ) {\n\n\t\tunset( $columns['date'] );\n\t\tunset( $columns['comments'] );\n\t\t$columns['title']  = __( 'Review Title', 'lifterlms' );\n\t\t$columns['course'] = __( 'Course Reviewed', 'lifterlms' );\n\t\t$columns['author'] = __( 'Review Author', 'lifterlms' );\n\t\t$columns['date']   = __( 'Review Date', 'lifterlms' );\n\t\treturn $columns;\n\t}\n\n\t/**\n\t * This function makes the 'Course' column sortable\n\t *\n\t * @param array $columns Array of sortable columns\n\t * @return array Updated column array.\n\t */\n\tpublic function make_columns_sortable( $columns ) {\n\n\t\t$columns['course'] = 'course';\n\t\treturn $columns;\n\t}\n\n\t/**\n\t * This function entered the information into the course section\n\t * of the llms_review post page. It takes the column that is being\n\t * worked on, as well as the comment ID, then echoes the content\n\t * required.\n\t *\n\t * @param string $column  Type of column being worked on\n\t * @param int    $post_id ID of comment\n\t *\n\t * @return void\n\t */\n\tpublic function generate_column_data( $column, $post_id ) {\n\n\t\tswitch ( $column ) {\n\t\t\tcase 'course':\n\t\t\t\techo ( wp_get_post_parent_id( $post_id ) != 0 ) ? esc_html( get_the_title( wp_get_post_parent_id( $post_id ) ) ) : '';\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * This function builds the additional content that is added\n\t * to the course meta box. It builds the additional fields and\n\t * then returns the updated array of fields\n\t *\n\t * @param array $content Array of meta fields\n\t *\n\t * @return array Updated array of meta fields\n\t */\n\tpublic function add_review_meta_boxes( $content ) {\n\n\t\t/**\n\t\t * This array is what holds the updated fields.\n\t\t * It is created in such a way so that a plugin\n\t\t * can latch onto it to extend the review functionality\n\t\t *\n\t\t * @var array\n\t\t */\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t'label'      => __( 'Enable Reviews', 'lifterlms' ),\n\t\t\t\t'desc'       => __( 'Select to enable reviews.', 'lifterlms' ),\n\t\t\t\t'id'         => self::$prefix . 'llms_reviews_enabled',\n\t\t\t\t'class'      => '',\n\t\t\t\t'value'      => '1',\n\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t'group'      => '',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t'label'      => __( 'Display Reviews', 'lifterlms' ),\n\t\t\t\t'desc'       => __( 'Select to display reviews on the page.', 'lifterlms' ),\n\t\t\t\t'id'         => self::$prefix . 'llms_display_reviews',\n\t\t\t\t'class'      => 'llms-num-reviews-top',\n\t\t\t\t'value'      => '1',\n\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t'group'      => 'llms-num-reviews-top',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'       => 'number',\n\t\t\t\t'min'        => '0',\n\t\t\t\t'label'      => __( 'Number of Reviews', 'lifterlms' ),\n\t\t\t\t'desc'       => __( 'Number of reviews to display on the page.', 'lifterlms' ),\n\t\t\t\t'id'         => self::$prefix . 'llms_num_reviews',\n\t\t\t\t'class'      => 'input-full',\n\t\t\t\t'value'      => '',\n\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t'group'      => 'bottom llms-num-reviews-bottom',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t'label'      => __( 'Prevent Multiple Reviews', 'lifterlms' ),\n\t\t\t\t'desc'       => __( 'Select to prevent a user from submitting more than one review.', 'lifterlms' ),\n\t\t\t\t'id'         => self::$prefix . 'llms_multiple_reviews_disabled',\n\t\t\t\t'class'      => '',\n\t\t\t\t'value'      => '1',\n\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t'group'      => '',\n\t\t\t),\n\t\t);\n\n\t\tif ( has_filter( 'llms_review_fields' ) ) {\n\t\t\t$fields = apply_filters( 'llms_review_fields', $fields );\n\t\t}\n\n\t\t$metaboxtab = array(\n\t\t\t'title'  => __( 'Reviews', 'lifterlms' ),\n\t\t\t'fields' => $fields,\n\t\t);\n\t\tarray_push( $content, $metaboxtab );\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Save metabox fields.\n\t *\n\t * @since Unknown\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function save_review_meta_boxes() {\n\t\t$post_id = llms_filter_input( INPUT_POST, 'post_ID', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( ! $post_id ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$post_type = get_post_type( $post_id );\n\t\tif ( 'course' !== $post_type ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified by core before triggering hook.\n\n\t\t$enabled  = ( isset( $_POST['_llms_reviews_enabled'] ) ) ? llms_filter_input_sanitize_string( INPUT_POST, '_llms_reviews_enabled' ) : '';\n\t\t$display  = ( isset( $_POST['_llms_display_reviews'] ) ) ? llms_filter_input_sanitize_string( INPUT_POST, '_llms_display_reviews' ) : '';\n\t\t$num      = ( isset( $_POST['_llms_num_reviews'] ) ) ? llms_filter_input_sanitize_string( INPUT_POST, '_llms_num_reviews' ) : 0;\n\t\t$multiple = ( isset( $_POST['_llms_multiple_reviews_disabled'] ) ) ? llms_filter_input_sanitize_string( INPUT_POST, '_llms_multiple_reviews_disabled' ) : '';\n\n\t\tupdate_post_meta( $post_id, '_llms_reviews_enabled', $enabled );\n\t\tupdate_post_meta( $post_id, '_llms_display_reviews', $display );\n\t\tupdate_post_meta( $post_id, '_llms_num_reviews', $num );\n\t\tupdate_post_meta( $post_id, '_llms_multiple_reviews_disabled', $multiple );\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Missing\n\t}\n}\n\nreturn new LLMS_Admin_Reviews();\n"
  },
  {
    "path": "includes/admin/class.llms.admin.settings.php",
    "content": "<?php\n/**\n * Admin Settings and fields\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 1.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin settings and fields class\n *\n * @since 1.0.0\n * @since 3.29.0 Unknown.\n * @since 3.34.4 Add \"keyval\" field for displaying custom html next to a setting key.\n * @since 3.35.0 Sanitize input data.\n * @since 3.35.1 Fix saving issue.\n * @since 3.35.2 Don't strip tags on editor and textarea fields that allow HTML.\n * @since 3.37.9 Add option for fields to show an asterisk for required fields.\n * @since 4.2.0 Use dashicons for tooltip icon display.\n */\nclass LLMS_Admin_Settings {\n\n\t/**\n\t * Settings array\n\t *\n\t * @var array\n\t */\n\tprivate static $settings = array();\n\n\t/**\n\t * Errors array\n\t *\n\t * @var array\n\t */\n\tprivate static $errors = array();\n\n\t/**\n\t * Messages array\n\t *\n\t * @var array\n\t */\n\tprivate static $messages = array();\n\n\t/**\n\t * Instantiates setting page objects, if not already done, and returns them.\n\t *\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return LLMS_Settings_Page[] self::$settings\n\t */\n\tpublic static function get_settings_tabs() {\n\n\t\tif ( empty( self::$settings ) ) {\n\t\t\t$settings = array();\n\n\t\t\t$settings[] = include 'settings/class.llms.settings.general.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.courses.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.memberships.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.security.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.accounts.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.checkout.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.engagements.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.notifications.php';\n\t\t\t$settings[] = include 'settings/class.llms.settings.integrations.php';\n\n\t\t\t/**\n\t\t\t * Allow 3rd parties to add or remove setting pages.\n\t\t\t *\n\t\t\t * @since 1.0.0\n\t\t\t *\n\t\t\t * @param LLMS_Settings_Page[] $settings Setting page objects.\n\t\t\t */\n\t\t\tself::$settings = apply_filters( 'lifterlms_get_settings_pages', $settings );\n\t\t}\n\n\t\treturn self::$settings;\n\t}\n\n\t/**\n\t * Save method. Saves all fields on current tab\n\t *\n\t * @return void\n\t */\n\tpublic static function save() {\n\n\t\tglobal $current_tab;\n\t\tif ( isset( $_POST['_wpnonce'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'lifterlms-settings' ) ) {\n\t\t\tdie( esc_html__( 'Whoa! something went wrong there!. Please refresh the page and retry.', 'lifterlms' ) );\n\t\t}\n\n\t\tdo_action( 'lifterlms_settings_save_' . $current_tab );\n\t\tdo_action( 'lifterlms_update_options_' . $current_tab );\n\t\tdo_action( 'lifterlms_update_options' );\n\n\t\tself::set_message( __( 'Your settings have been saved.', 'lifterlms' ) );\n\n\t\tdo_action( 'lifterlms_settings_saved' );\n\t\tdo_action( 'lifterlms_settings_saved_' . $current_tab );\n\t}\n\n\t/**\n\t * set message to messages array\n\t *\n\t * @param string $message\n\t * @return void\n\t */\n\tpublic static function set_message( $message ) {\n\t\tself::$messages[] = $message;\n\t}\n\n\t/**\n\t * set message to messages array\n\t *\n\t * @param string $message\n\t * @return void\n\t */\n\tpublic static function set_error( $message ) {\n\t\tself::$errors[] = $message;\n\t}\n\n\t/**\n\t * display messages in settings\n\t *\n\t * @return void\n\t */\n\tpublic static function display_messages_html() {\n\n\t\tif ( count( self::$errors ) > 0 ) {\n\n\t\t\tforeach ( self::$errors as $error ) {\n\t\t\t\techo '<div class=\"error\"><p><strong>' . wp_kses_post( $error ) . '</strong></p></div>';\n\t\t\t}\n\t\t} elseif ( count( self::$messages ) > 0 ) {\n\n\t\t\tforeach ( self::$messages as $message ) {\n\t\t\t\techo '<div class=\"updated\"><p><strong>' . wp_kses_post( $message ) . '</strong></p></div>';\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Settings Page output tabs\n\t *\n\t * @since 1.0.0\n\t * @since 3.29.0 Unknown.\n\t * @since 3.35.0 Sanitize `$_GET` data.\n\t * @since 3.35.1 Fix issue causing data to be saved on every page load.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\n\t\tglobal $current_tab;\n\n\t\tdo_action( 'lifterlms_settings_start' );\n\n\t\tself::get_settings_tabs();\n\n\t\t// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce is checked in self::save().\n\t\t$current_tab = empty( $_GET['tab'] ) ? 'general' : llms_filter_input_sanitize_string( INPUT_GET, 'tab' );\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce is checked in self::save().\n\t\tif ( ! empty( $_POST ) ) {\n\t\t\tself::save();\n\t\t}\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Missing.\n\n\t\t$err = llms_filter_input_sanitize_string( INPUT_GET, 'llms_error' );\n\t\tif ( $err ) {\n\t\t\tself::set_error( $err );\n\t\t}\n\n\t\t$msg = llms_filter_input_sanitize_string( INPUT_GET, 'llms_message' );\n\t\tif ( $msg ) {\n\t\t\tself::set_message( $msg );\n\t\t}\n\n\t\tself::display_messages_html();\n\n\t\t$tabs = apply_filters( 'lifterlms_settings_tabs_array', array() );\n\n\t\tinclude 'views/settings.php';\n\t}\n\n\t/**\n\t * Output fields for settings tabs. Dynamically generates fields.\n\t *\n\t * Needs to be refactored! Sets up all of the fields..gross...\n\t *\n\t * @return void\n\t */\n\tpublic static function output_fields( $settings ) {\n\n\t\tforeach ( $settings as $field ) {\n\n\t\t\t// Skip item if no field type is set.\n\t\t\tif ( ! isset( $field['type'] ) ) {\n\t\t\t\tcontinue; }\n\n\t\t\t// Output the field.\n\t\t\tself::output_field( $field );\n\n\t\t}\n\t}\n\n\n\t/**\n\t * Output fields\n\t *\n\t * @since Unknown.\n\t * @since 3.29.0 Unknown.\n\t * @since 3.34.4 Add \"keyval\" field for displaying custom html next to a setting key.\n\t * @since 3.37.9 Add option for fields to show an asterisk for required fields.\n\t * @since 5.0.2 Pass any option value sanitized as a \"slug\" through `urldecode()` prior to displaying it.\n\t * @since 7.0.0 Add `$after_html` to all field types.\n\t *\n\t * @param array $field {\n\t *     Array of field settings.\n\t *\n\t *     @type string $id                The setting ID. Used as the from element's `name` and `id` attributes and\n\t *                                     automatically correspond to an option key using the WP options API.\n\t *     @type string $type              The field type. Accepts: 'title', 'table', 'subtitle', 'desc', 'custom-html',\n\t *                                     'custom-html-no-wrap', 'sectionstart', 'sectionend', 'button', 'hidden', 'keyval',\n\t *                                     'text', 'email', 'number', 'password', 'textarea', 'wpeditor', 'select', 'multiselect',\n\t *                                     'radio', 'checkbox', 'image', 'single_select_page', 'single_select_membership'.\n\t *     @type string $title             The title / name of the option as displayed to the user.\n\t *     @type string $name              For \"button\" fields only: used as HTML `name` attribute. If not supplied the default\n\t *                                     value \"save\" will be used. For other field types used as a fallback for `$title` if\n\t *                                     no value is supplied.\n\t *     @type string $class             A space-separated list of CSS class names to apply the setting form element (the\n\t *                                     `<input>`, `<select>` etc...).\n\t *     @type string $css               An inline CSS style string.\n\t *     @type string $default           The default value of the setting.\n\t *     @type string $desc              The setting's description.\n\t *     @type bool   $desc_tooltip      If `true`, displays `$desc` in a hoverable tooltip.\n\t *     @type string $value             The value of the setting. If supplied this will override the automatic setting retrieval\n\t *                                     using `get_option( $id, $default )`.\n\t *     @type array  $custom_attributes An associative array of custom HTML attributes to be added to the form element (the\n\t *                                     `<input>`, `<select>` etc...).\n\t *     @type bool   $disabled          If `true` adds the `llms-disabled-field` class to the settings field wrapper.\n\t *     @type bool   $required          If `true`, text, email, number, and password fields will require user input.\n\t *     @type string $secure_option     The name of settings secure option equivalent. If specified, the fields value will be\n\t *                                     automatically removed from the database and the value will be masked when displayed on\n\t *                                     on screen. See {@see llms_get_secure_option()} for more information.\n\t *     @type string $sanitize          Automatically apply the specified sanitization to the value before storing and outputting\n\t *                                     the stored value. Supported filters:\n\t *                                       + \"slug\": Uses `sanitize_title()` on the value when storing and `urldecode()` when displaying.\n\t *     @type string $after_html        Additional HTML to add after the field's form element.\n\t *     @type array  $editor_settings   Used with \"wpeditor\" field type only. An array of options to pass to `wp_editor()` as the `$settings` argument.\n\t *     @type array  $options           For \"select\", \"multiselect\", and \"radio\" fields, an array of key/value pairs where the\n\t *                                     key is the setting value stored in database and the value is the setting label displayed\n\t *                                     on screen.\n\t * }\n\t * @return void\n\t */\n\tpublic static function output_field( $field ) {\n\n\t\t// Set missing values with defaults.\n\t\t$field = self::set_field_defaults( $field );\n\n\t\t// Setup custom attributes.\n\t\t$custom_attributes = self::format_field_custom_attributes( $field['custom_attributes'] ?? array() );\n\n\t\t// Setup field description and tooltip.\n\t\textract( self::set_field_descriptions( $field ) );\n\t\t$description .= ' ' . $field['after_html'];\n\n\t\t// Get the field value.\n\t\t$option_value = isset( $field['value'] ) ? $field['value'] : self::get_option( $field['id'], $field['default'] );\n\n\t\t// Setup the disabled CSS class.\n\t\t$disabled_class = ( isset( $field['disabled'] ) && true === $field['disabled'] ) ? 'llms-disabled-field' : '';\n\n\t\t// Switch based on type.\n\t\tswitch ( $field['type'] ) {\n\n\t\t\t// Section Titles.\n\t\t\tcase 'title':\n\t\t\t\tif ( ! empty( $field['title'] ) ) {\n\t\t\t\t\techo '<p class=\"llms-label\">' . esc_html( $field['title'] ) . '</p>';\n\t\t\t\t}\n\t\t\t\tif ( ! empty( $field['desc'] ) ) {\n\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in wp_kses_post()\n\t\t\t\t\techo '<p class=\"llms-description\">' . wpautop( wptexturize( wp_kses_post( $field['desc'] ) ) ) . '</p>';\n\t\t\t\t}\n\n\t\t\t\techo '<table class=\"form-table\">' . \"\\n\\n\";\n\n\t\t\t\tif ( ! empty( $field['id'] ) ) {\n\n\t\t\t\t\tdo_action( 'lifterlms_settings_' . sanitize_title( $field['id'] ) );\n\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'table':\n\t\t\t\techo '<tr valign=\"top\" class=\"' . esc_attr( $disabled_class ) . '\"><td>';\n\n\t\t\t\t\t$field['table']->get_results();\n\t\t\t\t\t$field['table']->output_table_html();\n\n\t\t\t\techo '</td></tr>';\n\t\t\t\tbreak;\n\n\t\t\tcase 'subtitle':\n\t\t\t\tif ( ! empty( $field['title'] ) ) {\n\t\t\t\t\techo '<tr valign=\"top\" class=\"' . esc_attr( $disabled_class ) . '\"><td colspan=\"2\">\n\t\t\t\t    \t<h3 class=\"llms-subtitle\">' . esc_html( $field['title'] ) . '</h3>';\n\t\t\t\t\tif ( ! empty( $field['desc'] ) ) {\n\t\t\t\t\t\techo '<p>' . wp_kses_post( $field['desc'] ) . '</p>';\n\t\t\t\t\t}\n\t\t\t\t\techo '</tr></td>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'desc':\n\t\t\t\tif ( ! empty( $field['desc'] ) ) {\n\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in wp_kses_post()\n\t\t\t\t\techo '<th colspan=\"2\" style=\"font-weight: normal;\">' . wpautop( wptexturize( wp_kses_post( $field['desc'] ) ) ) . '</th>';\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'custom-html':\n\t\t\t\tif ( ! empty( $field['value'] ) ) {\n\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in value / template file..\n\t\t\t\t\techo '<tr valign=\"top\" class=\"' . esc_attr( $disabled_class ) . '\"><td colspan=\"2\">' . $field['value'] . '</tr></td>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'custom-html-no-wrap':\n\t\t\t\tif ( ! empty( $field['value'] ) ) {\n\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in value / template file..\n\t\t\t\t\techo $field['value'];\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'sectionstart':\n\t\t\t\tif ( ! empty( $field['id'] ) ) {\n\n\t\t\t\t\tdo_action( 'lifterlms_settings_' . sanitize_title( $field['id'] ) . '_before' );\n\n\t\t\t\t\techo '<div class=\"llms-setting-group ' . esc_attr( $field['class'] ) . '\">';\n\n\t\t\t\t\tdo_action( 'lifterlms_settings_' . sanitize_title( $field['id'] ) . '_start' );\n\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'sectionend':\n\t\t\t\tif ( ! empty( $field['id'] ) ) {\n\n\t\t\t\t\tdo_action( 'lifterlms_settings_' . sanitize_title( $field['id'] ) . '_end' );\n\n\t\t\t\t}\n\n\t\t\t\techo '</table>';\n\t\t\t\techo '</div>';\n\n\t\t\t\tif ( ! empty( $field['id'] ) ) {\n\n\t\t\t\t\tdo_action( 'lifterlms_settings_' . sanitize_title( $field['id'] ) . '_after' );\n\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'button':\n\t\t\t\t$name = isset( $field['name'] ) ? $field['name'] : 'save';\n\t\t\t\techo '<tr valign=\"top\" class=\"' . esc_attr( $disabled_class ) . '\"><th><label for=\"' . esc_attr( $field['id'] ) . '\">' . esc_html( $field['title'] ) . '</label>';\n\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $tooltip escaped in set_field_descriptions().\n\t\t\t\techo $tooltip;\n\t\t\t\techo '</th>';\n\n\t\t\t\techo '<td class=\"forminp forminp-' . esc_attr( sanitize_title( $field['type'] ) ) . '\">';\n\t\t\t\techo '<div id=\"llms-form-wrapper\">';\n\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $description escaped in set_field_descriptions().\n\t\t\t\techo $description . '<br><br>';\n\t\t\t\techo '<input name=\"' . esc_attr( $name ) . '\" class=\"llms-button-primary\" type=\"submit\" value=\"' . esc_attr( $field['value'] ) . '\" />';\n\t\t\t\techo '</div>';\n\t\t\t\techo '</td></tr>';\n\t\t\t\t// phpcs:ignore -- commented out code\n\t\t\t\t// get_submit_button( 'Filter Results', 'primary', 'llms_search', true, array( 'id' => 'llms_analytics_search' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'hidden':\n\t\t\t\techo '<th></th>';\n\t\t\t\techo '<td><input type=\"hidden\"\n\t\t\t\t\tname=\"' . esc_attr( $field['id'] ) . '\"\n\t\t\t\t\tid=\"' . esc_attr( $field['id'] ) . '\"\n\t\t\t\t\tvalue=\"' . esc_attr( $field['value'] ) . '\">';\n\t\t\t\tbreak;\n\n\t\t\tcase 'keyval':\n\t\t\t\t?><tr valign=\"top\">\n\t\t\t\t\t<th>\n\t\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\"><?php echo esc_html( $field['title'] ); ?></label>\n\t\t\t\t\t\t<?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions(). ?>\n\t\t\t\t\t</th>\n\t\t\t\t\t<td class=\"forminp forminp-<?php echo esc_attr( sanitize_title( $field['type'] ) ); ?>\">\n\t\t\t\t\t\t<div id=\"<?php echo esc_attr( $field['id'] ); ?>\"><?php echo wp_kses_post( $field['value'] ); ?></div>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'text':\n\t\t\tcase 'email':\n\t\t\tcase 'number':\n\t\t\tcase 'password':\n\t\t\t\t$type     = $field['type'];\n\t\t\t\t$class    = '';\n\t\t\t\t$required = ! empty( $field['required'] );\n\n\t\t\t\t$secure_val   = isset( $field['secure_option'] ) ? llms_get_secure_option( $field['secure_option'], false ) : false;\n\t\t\t\t$option_value = ( false !== $secure_val ) ? str_repeat( '*', strlen( $secure_val ) ) : $option_value;\n\n\t\t\t\t// Ensure slugs with non-latin characters are not displayed as urlencoded strings.\n\t\t\t\tif ( ! empty( $field['sanitize'] ) && 'slug' === $field['sanitize'] ) {\n\t\t\t\t\t$option_value = urldecode( $option_value );\n\t\t\t\t}\n\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"<?php echo esc_attr( $disabled_class ); ?>\">\n\t\t\t\t\t<th>\n\t\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\">\n\t\t\t\t\t\t\t<?php echo esc_html( $field['title'] ); ?>\n\t\t\t\t\t\t\t<?php echo $required ? '<span class=\"llms-required\">*</span>' : ''; ?>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions(). ?>\n\t\t\t\t\t</th>\n\t\t\t\t\t<td class=\"forminp forminp-<?php echo esc_attr( sanitize_title( $field['type'] ) ); ?>\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tname=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\tid=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\ttype=\"<?php echo esc_attr( $type ); ?>\"\n\t\t\t\t\t\t\tstyle=\"<?php echo esc_attr( $field['css'] ); ?>\"\n\t\t\t\t\t\t\tvalue=\"<?php echo esc_attr( $option_value ); ?>\"\n\t\t\t\t\t\t\tclass=\"<?php echo esc_attr( $field['class'] ); ?>\"\n\t\t\t\t\t\t\t<?php echo $secure_val ? 'disabled=\"disabled\"' : ''; ?>\n\t\t\t\t\t\t\t<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in format_field_custom_attributes. ?>\n\t\t\t\t\t\t\t<?php echo $required ? 'required=\"required\"' : ''; ?>\n\t\t\t\t\t\t\t/> <?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\t// Textarea.\n\t\t\tcase 'textarea':\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"<?php echo esc_attr( $disabled_class ); ?>\">\n\t\t\t\t\t<th>\n\t\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\"><?php echo esc_html( $field['title'] ); ?></label>\n\t\t\t\t\t\t<?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?>\n\t\t\t\t\t</th>\n\t\t\t\t\t<td class=\"forminp forminp-<?php echo esc_attr( sanitize_title( $field['type'] ) ); ?>\">\n\t\t\t\t\t\t<textarea\n\t\t\t\t\t\t\tname=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\tid=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\tstyle=\"<?php echo esc_attr( $field['css'] ); ?>\"\n\t\t\t\t\t\t\tclass=\"<?php echo esc_attr( $field['class'] ); ?>\"\n\t\t\t\t\t\t\t<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in format_field_custom_attributes. ?>\n\t\t\t\t\t\t\t><?php echo esc_textarea( $option_value ); ?></textarea>\n\t\t\t\t\t\t<?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\tcase 'wpeditor':\n\t\t\t\t$editor_settings = isset( $field['editor_settings'] ) ? $field['editor_settings'] : array();\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"<?php echo esc_attr( $disabled_class ); ?>\">\n\t\t\t\t\t<th>\n\t\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\"><?php echo esc_html( $field['title'] ); ?></label>\n\t\t\t\t\t\t<?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?>\n\t\t\t\t\t</th>\n\t\t\t\t\t<td class=\"forminp forminp-<?php echo esc_attr( sanitize_title( $field['type'] ) ); ?>\">\n\t\t\t\t\t\t<?php wp_editor( $option_value, $field['id'], $editor_settings ); ?>\n\t\t\t\t\t\t<?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\t// Select boxes.\n\t\t\tcase 'select':\n\t\t\tcase 'multiselect':\n\t\t\t\t$field_name = esc_attr( $field['id'] );\n\t\t\t\tif ( 'multiselect' === $field['type'] ) {\n\t\t\t\t\t$field_name .= '[]';\n\t\t\t\t}\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"<?php echo esc_attr( $disabled_class ); ?>\">\n\t\t\t\t\t<th>\n\t\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\"><?php echo esc_html( $field['title'] ); ?></label>\n\t\t\t\t\t\t<?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?>\n\t\t\t\t\t</th>\n\t\t\t\t\t<td class=\"forminp forminp-<?php echo esc_attr( sanitize_title( $field['type'] ) ); ?>\">\n\t\t\t\t\t\t<select\n\t\t\t\t\t\t\tname=\"<?php echo esc_attr( $field_name ); ?>\"\n\t\t\t\t\t\t\tid=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\tstyle=\"<?php echo esc_attr( $field['css'] ); ?>\"\n\t\t\t\t\t\t\tclass=\"<?php echo esc_attr( $field['class'] ); ?>\"\n\t\t\t\t\t\t\t<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in format_field_custom_attributes. ?>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\tif ( 'multiselect' === $field['type'] ) {\n\t\t\t\t\t\t\t\techo 'multiple=\"multiple\"'; }\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\tforeach ( $field['options'] as $key => $val ) {\n\n\t\t\t\t\t\t\t\t// Convert an array from llms_make_select2_post_array().\n\t\t\t\t\t\t\t\tif ( is_array( $val ) ) {\n\t\t\t\t\t\t\t\t\t$key = $val['key'];\n\t\t\t\t\t\t\t\t\t$val = $val['title'];\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $key ); ?>\"\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\tif ( is_array( $option_value ) ) {\n\t\t\t\t\t\t\t\t\tselected( in_array( $key, $option_value ), true );\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselected( $option_value, $key );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t><?php echo wp_kses_post( $val ); ?></option>\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\t// Radio inputs.\n\t\t\tcase 'radio':\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"<?php echo esc_attr( $disabled_class ); ?>\">\n\t\t\t\t\t<th>\n\t\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\"><?php echo esc_html( $field['title'] ); ?></label>\n\t\t\t\t\t\t<?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?>\n\t\t\t\t\t</th>\n\t\t\t\t\t<td class=\"forminp forminp-<?php echo esc_attr( sanitize_title( $field['type'] ) ); ?>\">\n\t\t\t\t\t\t<fieldset>\n\t\t\t\t\t\t\t<?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\tforeach ( $field['options'] as $key => $val ) {\n\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t\t\t<label><input\n\t\t\t\t\t\t\t\t\t\tname=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\t\t\t\tvalue=\"<?php echo esc_attr( $key ); ?>\"\n\t\t\t\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t\t\t\tstyle=\"<?php echo esc_attr( $field['css'] ); ?>\"\n\t\t\t\t\t\t\t\t\t\tclass=\"<?php echo esc_attr( $field['class'] ); ?>\"\n\t\t\t\t\t\t\t\t\t\t<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in format_field_custom_attributes. ?>\n\t\t\t\t\t\t\t\t\t\t<?php checked( $key, $option_value ); ?>\n\t\t\t\t\t\t\t\t\t\t/> <?php echo esc_html( $val ); ?></label>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t</fieldset>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\t// Checkbox input.\n\t\t\tcase 'checkbox':\n\t\t\t\t$visbility_class = array();\n\n\t\t\t\tif ( ! isset( $field['hide_if_checked'] ) ) {\n\t\t\t\t\t$field['hide_if_checked'] = false;\n\t\t\t\t}\n\t\t\t\tif ( ! isset( $field['show_if_checked'] ) ) {\n\t\t\t\t\t$field['show_if_checked'] = false;\n\t\t\t\t}\n\t\t\t\tif ( 'yes' === $field['hide_if_checked'] || 'yes' === $field['show_if_checked'] ) {\n\t\t\t\t\t$visbility_class[] = 'hidden_option';\n\t\t\t\t}\n\t\t\t\tif ( 'option' === $field['hide_if_checked'] ) {\n\t\t\t\t\t$visbility_class[] = 'hide_options_if_checked';\n\t\t\t\t}\n\t\t\t\tif ( 'option' === $field['show_if_checked'] ) {\n\t\t\t\t\t$visbility_class[] = 'show_options_if_checked';\n\t\t\t\t}\n\t\t\t\tif ( ! isset( $field['checkboxgroup'] ) || 'start' === $field['checkboxgroup'] ) {\n\t\t\t\t\t?>\n\t\t\t\t\t\t<tr valign=\"top\" class=\"<?php echo esc_attr( implode( ' ', $visbility_class ) ); ?> <?php echo esc_attr( $disabled_class ); ?>\">\n\t\t\t\t\t\t\t<th><?php echo esc_html( $field['title'] ); ?></th>\n\t\t\t\t\t\t\t<td class=\"forminp forminp-checkbox\">\n\t\t\t\t\t\t\t\t<fieldset>\n\t\t\t\t\t<?php\n\t\t\t\t} else {\n\t\t\t\t\t?>\n\t\t\t\t\t\t<fieldset class=\"<?php echo esc_attr( implode( ' ', $visbility_class ) ); ?>\">\n\t\t\t\t\t<?php\n\t\t\t\t}\n\n\t\t\t\tif ( ! empty( $field['title'] ) ) {\n\t\t\t\t\t?>\n\t\t\t\t\t\t<legend class=\"screen-reader-text\"><span><?php echo esc_html( $field['title'] ); ?></span></legend>\n\t\t\t\t\t<?php\n\t\t\t\t}\n\n\t\t\t\t?>\n\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tname=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\tid=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tvalue=\"1\"\n\t\t\t\t\t\t\t<?php checked( $option_value, 'yes' ); ?>\n\t\t\t\t\t\t\t<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in format_field_custom_attributes. ?>\n\t\t\t\t\t\t/> <?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t</label> <?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?>\n\t\t\t\t<?php\n\n\t\t\t\tif ( ! isset( $field['checkboxgroup'] ) || 'end' === $field['checkboxgroup'] ) {\n\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t</fieldset>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t<?php\n\t\t\t\t} else {\n\t\t\t\t\t?>\n\t\t\t\t\t\t</fieldset>\n\t\t\t\t\t<?php\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'image':\n\t\t\t\t$type  = $field['type'];\n\t\t\t\t$class = '';\n\n\t\t\t\tif ( $option_value ) {\n\t\t\t\t\t// Media lib object ID.\n\t\t\t\t\tif ( is_numeric( $option_value ) ) {\n\t\t\t\t\t\t$size       = isset( $field['image_size'] ) ? $field['image_size'] : 'medium';\n\t\t\t\t\t\t$attachment = wp_get_attachment_image_src( $option_value, $size );\n\t\t\t\t\t\t$src        = $attachment[0];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Raw img src.\n\t\t\t\t\t\t$src = $option_value;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t$src = '';\n\t\t\t\t}\n\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"<?php echo esc_attr( $disabled_class ); ?>\">\n\t\t\t\t\t<th>\n\t\t\t\t\t\t<label for=\"<?php echo esc_attr( $field['id'] ); ?>\"><?php echo esc_html( $field['title'] ); ?></label>\n\t\t\t\t\t\t<?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?>\n\t\t\t\t\t</th>\n\t\t\t\t\t<td class=\"forminp forminp-<?php echo esc_attr( sanitize_title( $field['type'] ) ); ?>\">\n\n\t\t\t\t\t\t<img class=\"llms-image-field-preview\" src=\"<?php echo esc_url( $src ); ?>\">\n\t\t\t\t\t\t<button class=\"llms-button-secondary llms-image-field-upload\" data-id=\"<?php echo esc_attr( $field['id'] ); ?>\" type=\"button\">\n\t\t\t\t\t\t\t<span class=\"dashicons dashicons-admin-media\"></span>\n\t\t\t\t\t\t\t<?php esc_html_e( 'Upload', 'lifterlms' ); ?>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button class=\"llms-button-danger llms-image-field-remove<?php echo ( ! $src ) ? ' hidden' : ''; ?>\" data-id=\"<?php echo esc_attr( $field['id'] ); ?>\" type=\"button\">\n\t\t\t\t\t\t\t<span class=\"dashicons dashicons-no\"></span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tname=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\tid=\"<?php echo esc_attr( $field['id'] ); ?>\"\n\t\t\t\t\t\t\ttype=\"hidden\"\n\t\t\t\t\t\t\tstyle=\"<?php echo esc_attr( $field['css'] ); ?>\"\n\t\t\t\t\t\t\tvalue=\"<?php echo esc_attr( $option_value ); ?>\"\n\t\t\t\t\t\t\tclass=\"<?php echo esc_attr( $field['class'] ); ?>\"\n\t\t\t\t\t\t\t<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in format_field_custom_attributes. ?>\n\t\t\t\t\t\t\t/> <?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\t// Single page selects.\n\t\t\tcase 'single_select_page':\n\t\t\t\t$args = array(\n\t\t\t\t\t'name'             => $field['id'],\n\t\t\t\t\t'id'               => $field['id'],\n\t\t\t\t\t'sort_column'      => 'menu_order',\n\t\t\t\t\t'sort_order'       => 'ASC',\n\t\t\t\t\t'show_option_none' => ' ',\n\t\t\t\t\t'class'            => $field['class'],\n\t\t\t\t\t'echo'             => false,\n\t\t\t\t\t'selected'         => absint( self::get_option( $field['id'] ) ),\n\t\t\t\t);\n\n\t\t\t\tif ( isset( $field['args'] ) ) {\n\t\t\t\t\t$args = wp_parse_args( $field['args'], $args );\n\t\t\t\t}\n\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"single_select_page\">\n\t\t\t\t\t<th><?php echo esc_html( $field['title'] ); ?> <?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?></th>\n\t\t\t\t\t<td class=\"forminp\">\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t// PHPCS ignore reason: This is a dropdown and the output is escaped in wp_dropdown_pages.\n\t\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\t\t\techo str_replace( ' id=', \" data-placeholder='\" . esc_html__( 'Select a page&hellip;', 'lifterlms' ) . \"' style='\" . esc_attr( $field['css'] ) . \"' class='\" . esc_attr( $field['class'] ) . \"' id=\", wp_dropdown_pages( $args ) );\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<?php echo wp_kses_post( $description ); ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\t// Single page selects.\n\t\t\tcase 'single_select_membership':\n\t\t\t\t$args  = array(\n\t\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t\t'post_type'      => 'llms_membership',\n\t\t\t\t\t'nopaging'       => true,\n\t\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t\t'class'          => $field['class'],\n\t\t\t\t\t'selected'       => absint( self::get_option( $field['id'] ) ),\n\t\t\t\t);\n\t\t\t\t$posts = get_posts( $args );\n\n\t\t\t\tif ( isset( $field['args'] ) ) {\n\t\t\t\t\t$args = wp_parse_args( $field['args'], $args );\n\t\t\t\t}\n\n\t\t\t\t?>\n\t\t\t\t<tr valign=\"top\" class=\"single_select_membership\">\n\t\t\t\t\t<th><?php echo esc_html( $field['title'] ); ?> <?php echo $tooltip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in set_field_descriptions. ?></th>\n\t\t\t\t\t<td class=\"forminp\">\n\t\t\t\t\t\t<select class=\"<?php echo esc_attr( $args['class'] ); ?>\" style=\"<?php echo esc_attr( $field['css'] ); ?>\" name=\"lifterlms_membership_required\" id=\"lifterlms_membership_required\">\n\t\t\t\t\t\t\t<option value=\"\"> <?php esc_html_e( 'None', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\tforeach ( $posts as $post ) :\n\t\t\t\t\t\t\t\tsetup_postdata( $post );\n\t\t\t\t\t\t\t\tif ( $args['selected'] === $post->ID ) {\n\t\t\t\t\t\t\t\t\t$selected = 'selected';\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t$selected = '';\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $post->ID ); ?>\" <?php echo esc_attr( $selected ); ?> ><?php echo esc_html( $post->post_title ); ?></option>\n\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<?php\n\t\t\t\tbreak;\n\n\t\t\t// Default: run an action.\n\t\t\tdefault:\n\t\t\t\tdo_action( 'lifterlms_admin_field_' . $field['type'], $field, $option_value, $description, $tooltip, $custom_attributes );\n\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * Add and set default values for a field when looping\n\t *\n\t * @since 1.4.5\n\t * @since 7.0.0 Use `wp_parse_args()` to simplify method logic & add `after_html` default.\n\t *\n\t * @param array $field Associative array of field data, {@see LLMS_Admin_Settings::output_field()} for a full description.\n\t * @return array\n\t */\n\tpublic static function set_field_defaults( $field = array() ) {\n\n\t\t$field = wp_parse_args(\n\t\t\t$field,\n\t\t\tarray(\n\t\t\t\t'id'           => '',\n\t\t\t\t'title'        => $field['name'] ?? '',\n\t\t\t\t'class'        => '',\n\t\t\t\t'css'          => '',\n\t\t\t\t'default'      => '',\n\t\t\t\t'desc'         => '',\n\t\t\t\t'desc_tooltip' => '',\n\t\t\t\t'after_html'   => '',\n\t\t\t)\n\t\t);\n\n\t\treturn $field;\n\t}\n\n\t/**\n\t * Setup a field's tooltip and description based on supplied values\n\t *\n\t * @since 1.4.5\n\t * @since 3.24.0 Unknown.\n\t * @since 4.2.0 Use a dashicon in place of image for tooltip icon.\n\t *\n\t * @param array $field Associative array of field data.\n\t * @return array {\n\t *     Associative array containing field description and tooltip HTML.\n\t *\n\t *     @type string $description Description element HTML.\n\t *     @type string $tooltip     Tooltip element HTML.\n\t * }\n\t */\n\tpublic static function set_field_descriptions( $field = array() ) {\n\n\t\t$description = '';\n\t\t$tooltip     = '';\n\n\t\tif ( true === $field['desc_tooltip'] ) {\n\n\t\t\t$description = '';\n\t\t\t$tooltip     = $field['desc'];\n\n\t\t} elseif ( ! empty( $field['desc_tooltip'] ) ) {\n\n\t\t\t$description = $field['desc'];\n\t\t\t$tooltip     = $field['desc_tooltip'];\n\n\t\t} elseif ( ! empty( $field['desc'] ) ) {\n\n\t\t\t$description = $field['desc'];\n\t\t\t$tooltip     = '';\n\n\t\t}\n\n\t\tif ( $description && in_array( $field['type'], array( 'radio' ), true ) ) {\n\n\t\t\t$description = '<p style=\"margin-top:0\">' . wp_kses_post( $description ) . '</p>';\n\n\t\t} elseif ( $description && in_array( $field['type'], array( 'checkbox' ), true ) ) {\n\n\t\t\t$description = wp_kses_post( $description );\n\n\t\t} elseif ( $description ) {\n\n\t\t\t$description = '<p class=\"description\">' . wp_kses_post( $description ) . '</p>';\n\t\t}\n\n\t\tif ( $tooltip && in_array( $field['type'], array( 'checkbox' ), true ) ) {\n\n\t\t\t$tooltip = '<p class=\"description\">' . $tooltip . '</p>';\n\n\t\t} elseif ( $tooltip ) {\n\n\t\t\t$position = isset( $field['tooltip_position'] ) ? $field['tooltip_position'] : 'top-right';\n\t\t\t$tooltip  = '<span class=\"llms-help-tooltip tip--' . esc_attr( $position ) . '\" data-tip=\"' . esc_attr( $tooltip ) . '\"><span class=\"dashicons dashicons-editor-help\"></span></span>';\n\n\t\t}\n\n\t\treturn compact( 'description', 'tooltip' );\n\t}\n\n\t/**\n\t * Formats an associative array of custom field attributes as an array of HTML strings\n\t *\n\t * @param  array $attributes   associative array of attributes\n\t * @return array\n\t *\n\t * @since  1.4.5\n\t */\n\tpublic static function format_field_custom_attributes( $attributes = array() ) {\n\n\t\t// Custom attribute handling.\n\t\t$custom_attributes = array();\n\t\tforeach ( $attributes as $attribute => $attribute_value ) {\n\n\t\t\t$custom_attributes[] = esc_attr( $attribute ) . '=\"' . esc_attr( $attribute_value ) . '\"';\n\n\t\t}\n\n\t\treturn $custom_attributes;\n\t}\n\n\n\t/**\n\t * Get a setting from the settings API.\n\t *\n\t * @since Unknown\n\t * @since 3.7.5 Unknown\n\t *\n\t * @param string $option_name Option name.\n\t * @param mixed  $default     Optional default value.\n\t * @return string\n\t */\n\tpublic static function get_option( $option_name, $default = '' ) {\n\t\t// Array value.\n\t\tif ( strstr( $option_name, '[' ) ) {\n\n\t\t\tparse_str( $option_name, $option_array );\n\n\t\t\t// Option name is first key.\n\t\t\t$option_name = current( array_keys( $option_array ) );\n\n\t\t\t// Get value.\n\t\t\t$option_values = get_option( $option_name, '' );\n\n\t\t\t$key = key( $option_array[ $option_name ] );\n\n\t\t\tif ( isset( $option_values[ $key ] ) ) {\n\t\t\t\t$option_value = $option_values[ $key ];\n\t\t\t} else {\n\t\t\t\t$option_value = null;\n\t\t\t}\n\t\t} else {\n\t\t\t$option_value = get_option( $option_name, null );\n\t\t}\n\n\t\tif ( is_array( $option_value ) ) {\n\t\t\t$option_value = stripslashes_deep( $option_value );\n\t\t} elseif ( ! is_null( $option_value ) ) {\n\t\t\t$option_value = stripslashes( $option_value );\n\t\t}\n\n\t\treturn null === $option_value ? $default : $option_value;\n\t}\n\n\t/**\n\t * Save admin fields.\n\t *\n\t * Loops through a LifterLMS settings field options array and saves the values via `update_option()`.\n\t *\n\t * @since 1.0.0\n\t * @since 3.29.0 Unknown.\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 3.35.2 Don't strip tags on editor and textarea fields that allow HTML.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.0.0 Add handling for array fields for standard input types.\n\t *              Account for the `maxlength` input text and textarea attribute.\n\t *\n\t * @param array $settings Opens array to output\n\t * @return boolean\n\t */\n\tpublic static function save_fields( $settings ) {\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce is checked in self::save().\n\t\tif ( empty( $_POST ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Options to update will be stored here.\n\t\t$update_options = array();\n\n\t\t// Loop options and get values to save.\n\t\tforeach ( $settings as $field ) {\n\n\t\t\tif ( ! isset( $field['id'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$type = isset( $field['type'] ) ? sanitize_title( $field['type'] ) : '';\n\n\t\t\t// Remove secure options from the database.\n\t\t\tif ( isset( $field['secure_option'] ) && llms_get_secure_option( $field['secure_option'] ) ) {\n\t\t\t\tdelete_option( $field['id'] );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Get the option name.\n\t\t\t$option_value = null;\n\n\t\t\t// Determines if the option value is an array.\n\t\t\t$is_array_option = false !== strpos( $field['id'], '[' );\n\n\t\t\tswitch ( $type ) {\n\n\t\t\t\tcase 'checkbox':\n\t\t\t\t\tif ( $is_array_option ) {\n\t\t\t\t\t\t$option_value = self::get_array_field_posted_value( $field['id'] ) ? 'yes' : 'no';\n\t\t\t\t\t} elseif ( isset( $_POST[ $field['id'] ] ) ) {\n\t\t\t\t\t\t$option_value = 'yes';\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$option_value = 'no';\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'textarea':\n\t\t\t\tcase 'wpeditor':\n\t\t\t\t\tif ( isset( $_POST[ $field['id'] ] ) ) {\n\t\t\t\t\t\t$option_value = wp_kses_post( trim( llms_filter_input( INPUT_POST, $field['id'], FILTER_DEFAULT ) ) );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$option_value = '';\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'password':\n\t\t\t\tcase 'text':\n\t\t\t\tcase 'email':\n\t\t\t\tcase 'number':\n\t\t\t\tcase 'select':\n\t\t\t\tcase 'single_select_page':\n\t\t\t\tcase 'single_select_membership':\n\t\t\t\tcase 'radio':\n\t\t\t\tcase 'hidden':\n\t\t\t\tcase 'image':\n\t\t\t\t\tif ( $is_array_option ) {\n\t\t\t\t\t\t$option_value = self::get_array_field_posted_value( $field['id'] );\n\t\t\t\t\t} elseif ( isset( $_POST[ $field['id'] ] ) ) {\n\t\t\t\t\t\t$option_value = llms_filter_input_sanitize_string( INPUT_POST, $field['id'] );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$option_value = '';\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( isset( $field['sanitize'] ) && 'slug' === $field['sanitize'] ) {\n\t\t\t\t\t\t$option_value = sanitize_title( $option_value );\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'multiselect':\n\t\t\t\t\tif ( isset( $_POST[ $field['id'] ] ) ) {\n\t\t\t\t\t\t$option_value = llms_filter_input_sanitize_string( INPUT_POST, $field['id'], array( FILTER_REQUIRE_ARRAY ) );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$option_value = '';\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t/**\n\t\t\t\t\t * Action run for external field types.\n\t\t\t\t\t *\n\t\t\t\t\t * @since Unknown\n\t\t\t\t\t * @deprecated 7.0.0 Use `llms_update_option_{$type}` filter hook instead.\n\t\t\t\t\t *\n\t\t\t\t\t * @param type $arg Description.\n\t\t\t\t\t */\n\t\t\t\t\tdo_action_deprecated( \"lifterlms_update_option_{$type}\", array( $field ), '7.0.0' );\n\n\t\t\t}\n\n\t\t\t// Special treatment for the 'maxlength' attribute.\n\t\t\tif ( in_array( $type, array( 'text', 'textarea' ), true ) && isset( $field['custom_attributes']['maxlength'] ) ) {\n\t\t\t\t$option_value = llms_trim_string( $option_value, (int) $field['custom_attributes']['maxlength'], '' );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filters the value of a settings field after it has been parsed and sanitized\n\t\t\t * and before it is saved to the database.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `{$type}` refers to the setting field type:\n\t\t\t * email, text, checkbox, etc...\n\t\t\t *\n\t\t\t * @since 7.0.0\n\t\t\t *\n\t\t\t * @param string|null $option_value The sanitized option value or `null`.\n\t\t\t * @param array       $field        The settings field array.\n\t\t\t */\n\t\t\t$option_value = apply_filters( \"llms_update_option_{$type}\", $option_value, $field );\n\n\t\t\tif ( ! is_null( $option_value ) ) {\n\n\t\t\t\tif ( $is_array_option ) {\n\n\t\t\t\t\tparse_str( $field['id'], $option_array );\n\n\t\t\t\t\t// Option name is first key.\n\t\t\t\t\t$option_name = current( array_keys( $option_array ) );\n\n\t\t\t\t\t// Get old option value.\n\t\t\t\t\tif ( ! isset( $update_options[ $option_name ] ) ) {\n\t\t\t\t\t\t$update_options[ $option_name ] = get_option( $option_name, array() );\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! is_array( $update_options[ $option_name ] ) ) {\n\t\t\t\t\t\t$update_options[ $option_name ] = array();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Set keys and value.\n\t\t\t\t\t$key = key( $option_array[ $option_name ] );\n\n\t\t\t\t\t$update_options[ $option_name ][ $key ] = $option_value;\n\n\t\t\t\t} else {\n\t\t\t\t\t$update_options[ $field['id'] ] = $option_value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Action run prior to the update of a LifterLMS setting field option.\n\t\t\t *\n\t\t\t * An update isn't guaranteed after this action if the method's logic can't\n\t\t\t * find a valid posted valued to persist to the database.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param array $field The admin setting field array to be updated.\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_update_option', $field );\n\n\t\t}\n\n\t\t// Now save the options.\n\t\tforeach ( $update_options as $name => $value ) {\n\t\t\tupdate_option( $name, $value );\n\t\t}\n\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Missing\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Retrieves the posted value for an array type setting field.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $id The field ID, eg: \"my_setting[field_one]\".\n\t * @return string Returns the (sanitized) posted value or an empty string if it wasn't posted.\n\t */\n\tprivate static function get_array_field_posted_value( $id ) {\n\n\t\tparse_str( $id, $parsed_id );\n\t\t$opt_id  = key( $parsed_id );\n\t\t$opt_key = key( $parsed_id[ $opt_id ] );\n\t\t$posted  = llms_filter_input_sanitize_string( INPUT_POST, $opt_id, array( FILTER_REQUIRE_ARRAY ) );\n\n\t\treturn $posted[ $opt_key ] ?? '';\n\t}\n}\n"
  },
  {
    "path": "includes/admin/class.llms.admin.setup.wizard.php",
    "content": "<?php\n/**\n * Display a Setup Wizard\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.0.0\n * @version 7.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Display a Setup Wizard class.\n *\n * @since 3.0.0\n * @since 3.30.3 Fixed spelling error.\n * @since 3.35.0 Sanitize input data.\n * @since 3.37.14 Ensure redirect to the imported course when a course is imported at setup completion.\n * @since 4.4.4 Method `LLMS_Admin_Setup_Wizard::scripts()` & `LLMS_Admin_Setup_Wizard::output_step_html()` are deprecated with no replacements.\n * @since 4.8.0 Removed private class property \"generated_course_id\".\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Admin_Setup_Wizard::generator_course_status()` method\n *              - `LLMS_Admin_Setup_Wizard::output_step_html()` method\n *              - `LLMS_Admin_Setup_Wizard::scripts()` method\n *              - `LLMS_Admin_Setup_Wizard::watch_course_generation()` method\n * @since 7.4.0 Abstracted: {@see LLMS_Abstract_Admin_Wizard}.\n */\nclass LLMS_Admin_Setup_Wizard extends LLMS_Abstract_Admin_Wizard {\n\n\t/**\n\t * Configure wizard.\n\t *\n\t * @since 3.0.0\n\t * @since 4.4.4 Remove output of inline scripts.\n\t * @since 7.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\t$this->id        = 'setup';\n\t\t$this->views_dir = LLMS_PLUGIN_DIR . 'includes/admin/views/setup-wizard/';\n\t\t$this->title     = esc_html__( 'LifterLMS Setup Wizard', 'lifterlms' );\n\t\t$this->steps     = array(\n\t\t\t'intro'    => array(\n\t\t\t\t'title' => esc_html__( 'Welcome!', 'lifterlms' ),\n\t\t\t\t'save'  => esc_html__( 'Save & Continue', 'lifterlms' ),\n\t\t\t\t'skip'  => esc_html__( 'Skip this step', 'lifterlms' ),\n\t\t\t),\n\t\t\t'pages'    => array(\n\t\t\t\t'title' => esc_html__( 'Page Setup', 'lifterlms' ),\n\t\t\t\t'save'  => esc_html__( 'Save & Continue', 'lifterlms' ),\n\t\t\t\t'skip'  => esc_html__( 'Skip this step', 'lifterlms' ),\n\t\t\t),\n\t\t\t'payments' => array(\n\t\t\t\t'title' => esc_html__( 'Payments', 'lifterlms' ),\n\t\t\t\t'save'  => esc_html__( 'Save & Continue', 'lifterlms' ),\n\t\t\t\t'skip'  => esc_html__( 'Skip this step', 'lifterlms' ),\n\t\t\t),\n\t\t\t'coupon'   => array(\n\t\t\t\t'title' => esc_html__( 'Coupon', 'lifterlms' ),\n\t\t\t\t'save'  => esc_html__( 'Allow', 'lifterlms' ),\n\t\t\t\t'skip'  => esc_html__( 'No thanks', 'lifterlms' ),\n\t\t\t),\n\t\t\t'finish'   => array(\n\t\t\t\t'title' => esc_html__( 'Finish!', 'lifterlms' ),\n\t\t\t\t'save'  => esc_html__( 'Import Courses', 'lifterlms' ),\n\t\t\t\t'skip'  => esc_html__( 'Skip this step', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\n\t\t$this->add_hooks();\n\n\t\t// Add HTML around importable courses on last step.\n\t\tadd_action( 'llms_before_importable_course', array( $this, 'output_before_importable_course' ) );\n\t\tadd_action( 'llms_after_importable_course', array( $this, 'output_after_importable_course' ) );\n\n\t\t// Hide action buttons on importable courses during last step.\n\t\tadd_filter( 'llms_importable_course_show_action', '__return_false' );\n\n\t\t// Enqueue importer styles.\n\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'enqueue_importer' ) );\n\t}\n\n\t/**\n\t * Enqueue importer styles.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return bool\n\t */\n\tpublic function enqueue_importer(): bool {\n\t\tif ( 'finish' === $this->get_current_step() ) {\n\t\t\treturn llms()->assets->enqueue_style( 'llms-admin-importer' );\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Output HTML prior to each importable course\n\t *\n\t * Adds an opening label wrapper and adds HTML data to turn the element into a toggleable form element.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param array $course Importable course data array.\n\t * @return void\n\t */\n\tpublic function output_before_importable_course( array $course ): void {\n\n\t\t$id = absint( $course['id'] ?? null );\n\t\t?>\n\t\t<label>\n\t\t<div class=\"llms-switch\">\n\t\t\t<input class=\"llms-toggle llms-toggle-round\" id=\"llms-setup-import-course-<?php echo esc_attr( $id ); ?>\" name=\"llms_setup_course_import_ids[]\" value=\"<?php echo esc_attr( $id ); ?>\" type=\"checkbox\">\n\t\t\t<label for=\"llms-setup-import-course-<?php echo esc_attr( $id ); ?>\"><span class=\"screen-reader-text\"><?php esc_attr_e( 'Toggle to import course', 'lifterlms' ); ?>\n\t\t\t</label>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Output HTML after to each importable course\n\t *\n\t * Closes the label element opened in `output_before_importable_course()`.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param array $course Importable course data array.\n\t * @return void\n\t */\n\tpublic function output_after_importable_course( array $course ): void {\n\t\techo '</label>';\n\t}\n\n\t/**\n\t * Retrieve the redirect URL to use after an import is complete at the conclusion of the wizard.\n\t *\n\t * If a single course is imported, redirects to that course's edit page, otherwise redirects\n\t * to the course post table list sorted by created date with the most recent courses first.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @param int[] $course_ids WP_Post IDs of the course(s) generated during the import.\n\t * @return string\n\t */\n\tprotected function get_completed_url( array $course_ids ): string {\n\n\t\t$count = count( $course_ids );\n\n\t\tif ( 1 === $count ) {\n\t\t\treturn get_edit_post_link( $course_ids[0], 'not-display' ) ?? '';\n\t\t}\n\n\t\treturn admin_url( 'edit.php?post_type=course&orderby=date&order=desc' );\n\t}\n\n\t/**\n\t * Save the \"Coupon\" step\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return WP_Error|boolean Returns `true` on success otherwise returns a WP_Error.\n\t */\n\tprotected function save_coupon() {\n\n\t\tupdate_option( 'llms_allow_tracking', 'yes' );\n\n\t\t$req = LLMS_Tracker::send_data( true );\n\t\t$ret = new WP_Error( 'llms-setup-coupon-save-unknown', esc_html__( 'There was an error saving your data, please try again.', 'lifterlms' ) );\n\n\t\tif ( is_wp_error( $req ) ) {\n\t\t\t$ret = $req;\n\t\t} elseif ( empty( $req['success'] ) && isset( $req['message'] ) ) {\n\t\t\t$ret = new WP_Error( 'llms-setup-coupon-save-tracking-api', $req['message'] );\n\t\t} elseif ( ! empty( $req['success'] ) && true === $req['success'] ) {\n\t\t\t$ret = true;\n\t\t}\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Save the \"Pages\" creation step\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return WP_Error|boolean Returns `true` on success otherwise returns a WP_Error.\n\t */\n\tprotected function save_pages() {\n\n\t\treturn LLMS_Install::create_pages() ? true : new WP_Error( 'llms-setup-pages-save', esc_html__( 'There was an error saving your data, please try again.', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Save the \"Payments\" step.\n\t *\n\t * @since 4.8.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return bool Always returns true.\n\t */\n\tprotected function save_payments(): bool {\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce is verified in `save()`.\n\t\t$country = isset( $_POST['country'] ) ? llms_filter_input_sanitize_string( INPUT_POST, 'country' ) : get_lifterlms_country();\n\t\tupdate_option( 'lifterlms_country', $country );\n\n\t\t$currency = isset( $_POST['currency'] ) ? llms_filter_input_sanitize_string( INPUT_POST, 'currency' ) : get_lifterlms_currency();\n\t\tupdate_option( 'lifterlms_currency', $currency );\n\n\t\t$manual = isset( $_POST['manual_payments'] ) ? llms_filter_input_sanitize_string( INPUT_POST, 'manual_payments' ) : 'no';\n\t\tupdate_option( 'llms_gateway_manual_enabled', $manual );\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Missing\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Save the \"Finish\" step.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return WP_Error|int[]|bool Returns an array of generated WP_Post IDs on success, `false` when no import IDs are posted, otherwise returns a WP_Error.\n\t */\n\tprotected function save_finish() {\n\n\t\t$ids = (array) llms_filter_input( INPUT_POST, 'llms_setup_course_import_ids', FILTER_DEFAULT, FILTER_FORCE_ARRAY );\n\t\t$ids = array_filter( array_map( 'absint', $ids ) );\n\t\tif ( ! $ids ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$res = LLMS_Export_API::get( $ids );\n\t\tif ( is_wp_error( $res ) ) {\n\t\t\treturn $res;\n\t\t}\n\n\t\t$gen = new LLMS_Generator( $res );\n\t\t$gen->set_generator();\n\t\t$gen->generate();\n\n\t\tif ( $gen->is_error() ) {\n\t\t\treturn $gen->get_results();\n\t\t}\n\n\t\treturn $gen->get_generated_courses();\n\t}\n}\n\nfunction llms_load_admin_setup_wizard() {\n\treturn new LLMS_Admin_Setup_Wizard();\n}\nadd_action( 'init', 'llms_load_admin_setup_wizard' );\n"
  },
  {
    "path": "includes/admin/class.llms.admin.system-report.php",
    "content": "<?php\n/**\n * Admin System Report\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 2.1.0\n * @version 7.1.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin System Report Class.\n *\n * @since 2.1.0\n */\nclass LLMS_Admin_System_Report {\n\n\t/**\n\t * Output the system report\n\t *\n\t * @since 2.1.0\n\t * @since 3.0.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\n\t\techo '<div class=\"wrap lifterlms\">';\n\n\t\tself::output_copy_box();\n\n\t\tforeach ( LLMS_Data::get_data( 'system_report' ) as $key => $data ) {\n\n\t\t\tif ( is_array( $data ) ) {\n\n\t\t\t\tself::output_section( $key, $data );\n\n\t\t\t}\n\t\t}\n\n\t\techo '</div>';\n\t}\n\n\t/**\n\t * Output the copy for support box.\n\t *\n\t * @since 2.1.0\n\t * @since 3.11.2 Unknown.\n\t * @since 7.1.0 Style and HTML structure update.\n\t * @since 7.1.1 Use the right CSS selector to target the elements to include into the system's report copy.\n\t *\n\t * @return void\n\t */\n\tpublic static function output_copy_box() {\n\t\t?>\n\t\t<div class=\"llms-setting-group top\">\n\t\t\t<p class=\"llms-label\"><?php esc_html_e( 'Support', 'lifterlms' ); ?></p>\n\t\t\t<div id=\"llms-debug-report\">\n\t\t\t\t<textarea style=\"display:none;width: 100%\" rows=\"12\" readonly=\"readonly\"></textarea>\n\t\t\t\t<p class=\"submit\">\n\t\t\t\t\t<button id=\"copy-for-support\" class=\"llms-button-primary\"><?php esc_html_e( 'Copy for Support', 'lifterlms' ); ?></button>\n\t\t\t\t\t<a class=\"llms-button-secondary\" href=\"https://lifterlms.com/my-account/my-tickets/?utm_source=LifterLMS%20Plugin&utm_medium=System%20Report&utm_campaign=Get%20Help&utm_content=button001\" target=\"_blank\"><?php esc_html_e( 'Get Help', 'lifterlms' ); ?></a>\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\t\t<script>\n\t\t\tjQuery( document ).ready( function( $ ) {\n\t\t\t\tvar $textarea = $( '#llms-debug-report textarea' );\n\n\t\t\t\t$( '.llms-setting-group' ).each( function( index, element ) {\n\t\t\t\t\tvar title = $( this ).find( '.llms-label' ).text();\n\t\t\t\t\ttitle = title + '\\n' + '-------------------------------------------';\n\t\t\t\t\tvar val = $( this ).find( 'li' ).text().replace(/  /g, '').replace(/\\t/g, '').replace(/\\n\\n/g, '\\n');\n\t\t\t\t\t$textarea.val( $textarea.val() + title + '\\n' + val + '\\n\\n' );\n\t\t\t\t} );\n\n\t\t\t\t$( '#copy-for-support' ).on( 'click', function() {\n\t\t\t\t\t$textarea.show().select();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif ( ! document.execCommand( 'copy' ) ) {\n\t\t\t\t\t\t\tthrow 'Not allowed.';\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch( e ) {\n\t\t\t\t\t\talert( 'copy the text below' );\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t\t$textarea.on( 'click', function() {\n\t\t\t\t\t$( this ).select();\n\t\t\t\t} );\n\t\t\t});\n\t\t</script>\n\t\t<?php\n\t}\n\n\t/**\n\t * Output a section of data in the system report\n\t *\n\t * @since 3.0.0\n\t * @since 3.11.2 Unknown.\n\t * @since 4.13.0 Don't strip underscores when outputting the constant keys.\n\t * @since 7.1.0 Style and HTML structure update.\n\t *\n\t * @param string $section_title Title / key of the section.\n\t * @param arry   $data          Array of data for the section.\n\t * @return void\n\t */\n\tpublic static function output_section( $section_title, $data ) {\n\n\t\tif ( 'plugins' === $section_title ) {\n\n\t\t\t$data = $data['active'];\n\n\t\t}\n\n\t\t?>\n\t\t<div class=\"llms-setting-group\">\n\t\t\t<p class=\"llms-label\"><?php echo esc_html( self::title( $section_title ) ); ?></p>\n\t\t\t<div class=\"llms-list\">\n\t\t\t\t<ul>\n\t\t\t\t\t<?php foreach ( $data as $key => $val ) : ?>\n\t\t\t\t\t\t<li><p>\n\t\t\t\t\t\t<?php if ( 'plugins' === $section_title ) : ?>\n\t\t\t\t\t\t\t<?php self::plugin_item( $val ); ?>\n\t\t\t\t\t\t<?php elseif ( 'template_overrides' === $section_title ) : ?>\n\t\t\t\t\t\t\t<?php self::template_item( $val ); ?>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t<?php echo 'constants' === $section_title ? esc_html( $key ) : esc_html( self::title( $key ) ); ?>: <strong><?php echo esc_html( self::value( $val ) ); ?></strong>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t</p></li>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Output data related to an active plugin in the system report\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $data Array of plugin data.\n\t * @return void\n\t */\n\tprivate static function plugin_item( $data ) {\n\t\t?>\n\t\t<a href=\"<?php echo esc_url( $data['PluginURI'] ); ?>\"><?php echo esc_html( $data['Name'] ); ?></a>: <strong><?php echo esc_html( $data['Version'] ); ?></strong>\n\t\t<?php\n\t}\n\n\t/**\n\t * Output data related to an overridden template system report\n\t *\n\t * @param array $data Array of template data.\n\t * @return   void\n\t * @since    3.11.2\n\t * @version  3.11.2\n\t */\n\tprivate static function template_item( $data ) {\n\t\techo '<strong>' . esc_html( $data['template'] ) . ' (ver: ' . esc_html( $data['core_version'] ) . ')</strong>: ';\n\t\techo '<code>' . esc_html( $data['location'] ) . '</code> (ver: ' . esc_html( $data['version'] ) . ')';\n\t}\n\n\n\t/**\n\t * Return the title for an item in the system report.\n\t *\n\t * @since 3.0.0\n\t * @since 7.1.0 Fixed misspelled WordPress.\n\t * @since 7.7.0 Return the title.\n\t *\n\t * @param string $key Title.\n\t * @return string\n\t */\n\tprivate static function title( $key ) {\n\n\t\t$key = ucwords( str_replace( '_', ' ', $key ) );\n\n\t\t// Fix for capital P.\n\t\tif ( 'Wordpress' === $key ) { // phpcs:ignore\n\t\t\t$key = 'WordPress';\n\t\t}\n\n\t\treturn $key;\n\t}\n\n\t/**\n\t * Return the value of an item in the system report\n\t *\n\t * @since 3.0.0\n\t * @since 7.7.0 Return the value.\n\t *\n\t * @param string $val Value.\n\t * @return string\n\t */\n\tprivate static function value( $val ) {\n\n\t\treturn $val;\n\t}\n}\n"
  },
  {
    "path": "includes/admin/class.llms.admin.user.custom.fields.php",
    "content": "<?php\n/**\n * LLMS_Admin_User_Custom_Fields class file\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 2.7.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Add custom user fields to user admin panel screens\n *\n * Applies to edit-user.php, user-new.php, & profile.php.\n *\n * @since 2.7.0\n * @since 3.35.0 Sanitize input data.\n * @since 3.37.15 Fix error encountered when errors encountered validating custom fields.\n */\nclass LLMS_Admin_User_Custom_Fields {\n\n\tprivate $fields = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 2.7.0\n\t * @since 3.13.0 Unknown.\n\t * @since 4.14.0 Add personal options hook.\n\t * @since 5.0.0 Custom fields (legacy), are now printed with priority 11 instead of 10.\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Output custom fields on edit screens.\n\t\t$field_actions = array(\n\t\t\t'show_user_profile',\n\t\t\t'edit_user_profile',\n\t\t\t'user_new_form',\n\t\t);\n\n\t\tforeach ( $field_actions as $action ) {\n\t\t\tadd_action( $action, array( $this, 'output_custom_fields' ), 11, 1 );\n\t\t\tadd_action( $action, array( $this, 'output_instructors_assistant_fields' ), 10, 1 );\n\t\t}\n\n\t\t// Allow errors to be output before saving field data.\n\t\t// Save the data if no errors are encountered.\n\t\tadd_action( 'user_profile_update_errors', array( $this, 'add_errors' ), 10, 3 );\n\n\t\t// Save data when a new user is created.\n\t\tadd_action( 'edit_user_created_user', array( $this, 'save' ) );\n\n\t\t// Add personal options.\n\t\tadd_action( 'personal_options', array( $this, 'output_personal_options' ) );\n\t}\n\n\n\t/**\n\t * Validate custom fields\n\t *\n\t * During updates will save data, creation is saved during a different action.\n\t *\n\t * @since 2.7.0\n\t * @since 3.13.0 Unknown.\n\t * @since 3.37.15 Correctly pass `$user` to `$this->save()`.\n\t *\n\t * @param obj     $errors Instance of WP_Error, passed by reference.\n\t * @param bool    $update `true` if updating a profile, `false` if a new user.\n\t * @param WP_User $user   Instance of WP_User for the user being updated.\n\t * @return void\n\t */\n\tpublic function add_errors( &$errors, $update, $user ) {\n\n\t\t$this->get_fields();\n\n\t\t$error = $this->validate_fields( $user );\n\n\t\tif ( $error ) {\n\n\t\t\t$errors->add( '', $error, '' );\n\n\t\t\tif ( $update ) {\n\t\t\t\t$this->save( $user );\n\t\t\t}\n\n\t\t\t// Don't save.\n\t\t\tremove_action( 'edit_user_created_user', array( $this, 'save' ) );\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t// If updating, save here since there's no other save specific admin action (that I could find).\n\t\tif ( $update ) {\n\t\t\t$this->save( $user );\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve an associative array of custom fields and custom field data\n\t *\n\t * @since 2.7.0\n\t * @since 3.13.0 Unknown.\n\t * @since 5.0.0 Removed LLMS core fields and deprecate the filter usage.\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\t$this->fields = apply_filters_deprecated(\n\t\t\t'lifterlms_get_user_custom_fields',\n\t\t\tarray(\n\t\t\t\tarray(),\n\t\t\t),\n\t\t\t'5.0.0',\n\t\t\t'llms_admin_profile_fields'\n\t\t);\n\n\t\treturn $this->fields;\n\t}\n\n\t/**\n\t * Load usermeta data into the array of fields retrieved from $this->get_fields\n\t *\n\t * Meta data is added to the array under the key \"value\" for each field.\n\t *\n\t * If no data is found for a particular field the value is still added as an empty string.\n\t *\n\t * @since 2.7.0\n\t *\n\t * @param WP_User|int $user Instance of WP_User or WP User ID\n\t * @return array\n\t */\n\tpublic function get_fields_with_data( $user ) {\n\n\t\tif ( is_numeric( $user ) ) {\n\t\t\t$user = new WP_User( $user );\n\t\t}\n\n\t\t$this->get_fields();\n\n\t\tforeach ( $this->fields as $field => $data ) {\n\n\t\t\t$this->fields[ $field ]['value'] = apply_filters( 'lifterlms_get_user_custom_field_value_' . $field, $user->get( $field ), $user, $field );\n\n\t\t}\n\n\t\treturn $this->fields;\n\t}\n\n\t/**\n\t * Output custom field data fields as HTML inputs\n\t *\n\t * @since 2.7.0\n\t * @since 3.24.0 Unknown.\n\t * @since 5.0.0 Do not include user-edit template if no fields to show.\n\t *\n\t * @param WP_User|int $user Instance of WP_User or WP User ID.\n\t * @return void\n\t */\n\tpublic function output_custom_fields( $user ) {\n\n\t\tif ( is_numeric( $user ) || is_a( $user, 'WP_User' ) ) {\n\t\t\t$this->get_fields_with_data( $user );\n\t\t} else {\n\t\t\t$this->get_fields();\n\t\t}\n\n\t\tif ( empty( $this->fields ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tllms_get_template(\n\t\t\t'admin/user-edit.php',\n\t\t\tarray(\n\t\t\t\t'section_title' => __( 'LifterLMS Profile (legacy fields)', 'lifterlms' ),\n\t\t\t\t'fields'        => $this->fields,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Output personal option fields\n\t *\n\t * Currently adds a single option row for controlling auto-save behavior on the course builder.\n\t *\n\t * @since 4.14.0\n\t *\n\t * @param WP_User $user Viewed user object.\n\t * @return void\n\t */\n\tpublic function output_personal_options( $user ) {\n\n\t\tif ( ! user_can( $user, 'edit_courses' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$autosave = get_user_option( 'llms_builder_autosave', $user->ID );\n\t\t$autosave = empty( $autosave ) ? 'no' : $autosave;\n\n\t\t?>\n\t\t<tr class=\"llms-builder-autosave llms-builder-autosave-wrap\">\n\t\t\t<th scope=\"row\"><?php esc_html_e( 'Course Builder Autosave', 'lifterlms' ); ?></th>\n\t\t\t<td>\n\t\t\t\t<label for=\"llms_builder_autosave\">\n\t\t\t\t\t<input name=\"llms_builder_autosave\" type=\"checkbox\" id=\"llms_builder_autosave\" value=\"yes\"<?php checked( 'yes', $autosave ); ?>>\n\t\t\t\t\t<?php esc_html_e( 'Automatically save changes when using the course builder', 'lifterlms' ); ?>\n\t\t\t\t</label><br>\n\t\t\t</td>\n\t\t</tr>\n\t\t<?php\n\t}\n\n\t/**\n\t * Add instructor parent fields for use when creating instructor's assistants\n\t *\n\t * @since 3.13.0\n\t * @since 3.23.0 Unknown.\n\t * @since 3.37.15 Use strict comparisons.\n\t *\n\t * @param WP_User|int $user Instance of WP_User or WP User ID\n\t * @return void\n\t */\n\tpublic function output_instructors_assistant_fields( $user ) {\n\n\t\tif ( is_numeric( $user ) || is_a( $user, 'WP_User' ) ) {\n\t\t\t$instructor = llms_get_instructor( $user );\n\t\t\t$selected   = $instructor->get( 'parent_instructors' );\n\t\t\tif ( empty( $selected ) && ! is_array( $selected ) ) {\n\t\t\t\t$selected = array();\n\t\t\t}\n\t\t} else {\n\t\t\t$selected = array( get_current_user_id() );\n\t\t}\n\n\t\t$selected = array_map( 'absint', $selected );\n\n\t\t// Only let admins & lms managers select the parent for an instructor's assistant.\n\t\tif ( current_user_can( 'manage_lifterlms' ) ) {\n\n\t\t\t$users = get_users(\n\t\t\t\tarray(\n\t\t\t\t\t'role__in' => array( 'administrator', 'lms_manager', 'instructor' ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t\t<table class=\"form-table\" id=\"llms-parent-instructors-table\" style=\"display:none;\">\n\t\t\t\t<tr class=\"form-field\">\n\t\t\t\t\t<th scope=\"row\"><label for=\"llms-parent-instructors\"><?php esc_html_e( 'Parent Instructor(s)', 'lifterlms' ); ?></label></th>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<select class=\"regular-text\" id=\"llms-parent-instructors\" name=\"llms_parent_instructors[]\" multiple=\"multiple\">\n\t\t\t\t\t\t\t<?php foreach ( $users as $user ) : ?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $user->ID ); ?>\"<?php selected( in_array( $user->ID, $selected, true ) ); ?>>\n\t\t\t\t\t\t\t\t\t<?php echo esc_html( $user->display_name ); ?>\n\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t</table>\n\t\t\t<?php\n\n\t\t\tadd_action( 'admin_print_footer_scripts', array( $this, 'output_instructors_assistant_scripts' ) );\n\n\t\t} elseif ( 'add-new-user' === $user ) {\n\t\t\t/**\n\t\t\t * This will be the case for Instructors only:\n\t\t\t *\n\t\t\t * Show a hidden field with the current user's info\n\t\t\t *\n\t\t\t * When saving it will only save if the created user's role is instructor's assistant.\n\t\t\t */\n\t\t\techo '<input type=\"hidden\" name=\"llms_parent_instructors[]\" value=\"' . esc_attr( get_current_user_id() ) . '\">';\n\t\t}\n\t}\n\n\t/**\n\t * Output JS to handle user interaction with the instructor's parent field\n\t *\n\t * Display custom field ONLY when creating/editing an instructor's assistant.\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_instructors_assistant_scripts() {\n\t\t?>\n\t\t<script>\n\t\t\t( function( $ ) {\n\t\t\t\tvar $role = $( '#role' ),\n\t\t\t\t\t$parent = $( '#llms-parent-instructors-table' );\n\t\t\t\t$role.closest( '.form-table' ).after( $parent );\n\t\t\t\t$role.on( 'change', function() {\n\t\t\t\t\tif ( 'instructors_assistant' === $( this ).val() ) {\n\t\t\t\t\t\t$parent.show();\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$parent.hide();\n\t\t\t\t\t}\n\t\t\t\t} ).trigger( 'change' );\n\t\t\t} )( jQuery );\n\t\t</script>\n\t\t<?php\n\t}\n\n\t/**\n\t * Save custom field data for a user\n\t *\n\t * @since 3.13.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.37.15 Use strict comparisons.\n\t * @since 4.14.0 Save builder autosave personal options.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param WP_User|int|obj $user User object or id.\n\t * @return void\n\t */\n\tpublic function save( $user ) {\n\n\t\tif ( is_numeric( $user ) ) {\n\n\t\t\t// Numeric ID is passed in during creations.\n\t\t\t$user   = new WP_User( $user );\n\t\t\t$action = 'create';\n\n\t\t} elseif ( isset( $user->ID ) ) {\n\n\t\t\t// An object that's not a WP_User gets passed in during updates.\n\t\t\t$user   = new WP_User( $user->ID );\n\t\t\t$action = 'update';\n\t\t}\n\n\t\t// Saves custom fields.\n\t\tforeach ( $this->fields as $field => $data ) {\n\n\t\t\t$value = apply_filters( 'lifterlms_save_custom_user_field_' . $field, llms_filter_input_sanitize_string( INPUT_POST, $field ), $user, $field );\n\t\t\tupdate_user_meta( $user->ID, $field, $value );\n\n\t\t}\n\n\t\t// Save instructor assistant's parent instructor.\n\t\tif ( in_array( 'instructors_assistant', $user->roles, true ) && ! empty( $_POST['llms_parent_instructors'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Missing\n\n\t\t\t$instructor = llms_get_instructor( $user );\n\t\t\t$instructor->add_parent( llms_filter_input( INPUT_POST, 'llms_parent_instructors', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ) );\n\n\t\t}\n\n\t\t// Save personal options.\n\t\tif ( user_can( $user, 'edit_courses' ) && 'create' !== $action ) {\n\t\t\t$autosave = empty( $_POST['llms_builder_autosave'] ) ? 'no' : 'yes';\n\t\t\tupdate_user_meta( $user->ID, 'llms_builder_autosave', $autosave );\n\t\t}\n\t}\n\n\t/**\n\t * Validate custom fields\n\t *\n\t * By default only checks for valid as core fields don't have any special validation.\n\t *\n\t * If adding custom fields, hook into the action run after required validation\n\t * to add special validation rules for your field.\n\t *\n\t * @since 2.7.0\n\t *\n\t * @param WP_User|int $user Instance of WP_User or WP User ID.\n\t * @return string|bool `false` if no validation errors or the error message (as a sttring) if validation errors occurred.\n\t */\n\tpublic function validate_fields( $user ) {\n\n\t\t// Ensure there's no missing required fields.\n\t\tforeach ( $this->fields as $field => $data ) {\n\n\t\t\t// Return an error message for empty required fields.\n\t\t\tif ( empty( $_POST[ $field ] ) && $data['required'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing\n\n\t\t\t\treturn sprintf( __( 'Required field \"%s\" is missing.', 'lifterlms' ), $data['label'] );\n\n\t\t\t} else {\n\n\t\t\t\t/**\n\t\t\t\t * Run custom validation against the field\n\t\t\t\t *\n\t\t\t\t * If filter function returns a truthy, validation will stop, fields will not be saved,\n\t\t\t\t * and an error message will be displayed on screen.\n\t\t\t\t *\n\t\t\t\t * This should return `false` or a string which will be used as the error message.\n\t\t\t\t *\n\t\t\t\t * @since 2.7.0\n\t\t\t\t *\n\t\t\t\t * @param boolean     $error_message The error message when validation issues are encountered. Return `false` when no validation issues.\n\t\t\t\t * @param string      $field         Field id.\n\t\t\t\t * @param WP_User|int $user          Instance of WP_User or WP User ID.\n\t\t\t\t */\n\t\t\t\t$error_msg = apply_filters( \"lifterlms_validate_custom_user_field_{$field}\", false, $field, $user );\n\n\t\t\t\tif ( $error_msg ) {\n\n\t\t\t\t\treturn $error_msg;\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n}\n\nreturn new LLMS_Admin_User_Custom_Fields();\n"
  },
  {
    "path": "includes/admin/class.llms.student.bulk.enroll.php",
    "content": "<?php\n/**\n * Bulk Enrollment\n *\n * @package LifterLMS/Admin/Classes\n *\n * @since 3.20.0\n * @version 9.2.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Bulk Enrollment class\n *\n * @since 3.20.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 9.2.3 Added CSRF protection via nonce verification on bulk enrollment.\n */\nclass LLMS_Student_Bulk_Enroll {\n\n\t/**\n\t * Admin notices\n\t *\n\t * @var string[]\n\t * @since 3.19.4\n\t */\n\tpublic $admin_notices = array();\n\n\t/**\n\t * Product (Course/Membership) ID\n\t *\n\t * @var int\n\t */\n\tpublic $product_id = 0;\n\n\t/**\n\t * Product Post Title\n\t *\n\t * @var string\n\t */\n\tpublic $product_title = '';\n\n\t/**\n\t * User IDs\n\t *\n\t * @var int\n\t */\n\tpublic $user_ids = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since   3.20.0\n\t * @version 3.20.0\n\t */\n\tpublic function __construct() {\n\t\t// Hook into extra ui on users table to display product selection.\n\t\tadd_action( 'manage_users_extra_tablenav', array( $this, 'display_product_selection_for_bulk_users' ) );\n\n\t\t// Hook into users table screen to process bulk enrollment.\n\t\tadd_action( 'admin_head-users.php', array( $this, 'maybe_enroll_users_in_product' ) );\n\n\t\t// Display enrollment results as notices.\n\t\tadd_action( 'admin_notices', array( $this, 'display_notices' ) );\n\t}\n\n\t/**\n\t * Displays ui for selecting product to bulk enroll users into\n\t *\n\t * @since 3.20.0\n\t * @since 9.2.3 Added nonce field for CSRF protection.\n\t *\n\t * @param string $which Whether this is the 'top' or 'bottom' tablenav.\n\t * @return void\n\t */\n\tpublic function display_product_selection_for_bulk_users( $which ) {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// The attributes need to be different for top and bottom of the table.\n\t\t$id     = 'bottom' === $which ? 'llms_bulk_enroll_product2' : 'llms_bulk_enroll_product';\n\t\t$submit = 'bottom' === $which ? 'llms_bulk_enroll2' : 'llms_bulk_enroll';\n\t\t?>\n\t\t<div class=\"alignleft actions\">\n\t\t\t<label class=\"screen-reader-text\" for=\"_llms_bulk_enroll_product\">\n\t\t\t\t<?php esc_html_e( 'Choose Course/Membership', 'lifterlms' ); ?>\n\t\t\t</label>\n\t\t\t<select id=\"<?php echo esc_attr( $id ); ?>\" class=\"llms-posts-select2 llms-bulk-enroll-product\" data-post-type=\"llms_membership,course\" name=\"<?php echo esc_attr( $id ); ?>\" style=\"min-width:200px;max-width:auto;\">\n\t\t\t</select>\n\t\t\t<input type=\"submit\" name=\"<?php echo esc_attr( $submit ); ?>\" id=\"<?php echo esc_attr( $submit ); ?>\" class=\"button\" value=\"<?php esc_attr_e( 'Enroll', 'lifterlms' ); ?>\">\n\t\t\t<?php if ( 'top' === $which ) : ?>\n\t\t\t\t<?php wp_nonce_field( 'llms_bulk_enroll', '_llms_bulk_enroll_nonce', false ); ?>\n\t\t\t<?php endif; ?>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Conditionally enrolls multiple users into a product\n\t *\n\t * @since 3.20.0\n\t * @since 9.2.3 Added nonce verification for CSRF protection.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_enroll_users_in_product() {\n\n\t\t// Verify bulk enrollment request.\n\t\t$do_bulk_enroll = $this->_bottom_else_top( 'llms_bulk_enroll' );\n\n\t\t// Bail if this is not a bulk enrollment request.\n\t\tif ( empty( $do_bulk_enroll ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tcheck_admin_referer( 'llms_bulk_enroll', '_llms_bulk_enroll_nonce' );\n\n\t\t// Get the product (course/membership) to enroll users in.\n\t\t$this->product_id = $this->_bottom_else_top( 'llms_bulk_enroll_product', FILTER_VALIDATE_INT );\n\n\t\tif ( empty( $this->product_id ) ) {\n\t\t\t$message = __( 'Please select a Course or Membership to enroll users into!', 'lifterlms' );\n\t\t\t$this->generate_notice( 'error', $message );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! current_user_can( 'enroll', $this->product_id ) ) {\n\t\t\t$message = __( 'You do not have permission to enroll users into this course or membership.', 'lifterlms' );\n\t\t\t$this->generate_notice( 'error', $message );\n\t\t\treturn;\n\t\t}\n\n\t\t// Get the product title for notices.\n\t\t$this->product_title = get_the_title( $this->product_id );\n\n\t\t// Get all the user ids to enroll.\n\t\t$this->user_ids = filter_input( INPUT_GET, 'users', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );\n\n\t\tif ( empty( $this->user_ids ) ) {\n\t\t\t$message = sprintf( __( 'Please select users to enroll into <em>%s</em>.', 'lifterlms' ), $this->product_title );\n\t\t\t$this->generate_notice( 'error', $message );\n\t\t\treturn;\n\t\t}\n\n\t\t$this->enroll_users_in_product();\n\t}\n\n\t/**\n\t * Retrieves submitted inputs\n\t *\n\t * @param   string $param The input key\n\t * @param   mixed  $validation Validation filter constant\n\t * @return  mixed The submitted input value\n\t * @since   3.20.0\n\t * @version 3.20.0\n\t */\n\tprivate function _bottom_else_top( $param, $validation = FILTER_DEFAULT ) {\n\n\t\t$return_val = false;\n\n\t\t// Get the value of the input displayed at the bottom of users table.\n\t\t$bottom_value = filter_input( INPUT_GET, $param . '2', $validation );\n\n\t\t// Get the value of input displayed at the top of users table.\n\t\t$top_value = filter_input( INPUT_GET, $param, $validation );\n\n\t\t// Prefer top over bottom, just like WordPress does.\n\t\tif ( ! empty( $bottom_value ) ) {\n\t\t\t$return_val = $bottom_value;\n\t\t}\n\t\tif ( ! empty( $top_value ) ) {\n\t\t\t$return_val = $top_value;\n\t\t}\n\n\t\treturn $return_val;\n\t}\n\n\t/**\n\t * Enrolls multiple users into a product\n\t *\n\t * @since   3.20.0\n\t * @version 3.20.0\n\t */\n\tprivate function enroll_users_in_product() {\n\n\t\t// Get user information from user ids.\n\t\t$users = $this->get_users( $this->user_ids );\n\n\t\t// Bail if for some reason, no users are found (because they were deleted in the bg?).\n\t\tif ( empty( $users ) ) {\n\t\t\t$message = sprintf( __( 'No such users found. Cannot enroll into <em>%s</em>.', 'lifterlms' ), $this->product_title );\n\t\t\t$this->generate_notice( 'error', $message );\n\t\t\treturn;\n\t\t}\n\n\t\t// Create manual enrollment trigger.\n\t\t$trigger = 'admin_' . get_current_user_id();\n\n\t\tforeach ( $users as $user ) {\n\n\t\t\t$this->enroll( $user, $trigger );\n\t\t}\n\t}\n\n\t/**\n\t * Get user details from user IDs\n\n\t * @param   array $user_ids WP user IDs\n\t * @return  array User details\n\t * @since   3.20.0\n\t * @version 3.21.0\n\t */\n\tprivate function get_users( $user_ids ) {\n\n\t\t// Prepare query arguments.\n\t\t$user_query_args = array(\n\t\t\t'include' => $user_ids,\n\t\t\t// We need display names for notices.\n\t\t\t'fields'  => array( 'ID', 'display_name' ),\n\t\t);\n\n\t\t$user_query = new WP_User_Query( $user_query_args );\n\n\t\t$results = $user_query->get_results();\n\n\t\treturn empty( $results ) ? false : $results;\n\t}\n\n\t/**\n\t * Enrolls a user into the selected product\n\t *\n\t * @param   WP_User $user User object\n\t * @param   string  $trigger Enrollment trigger string\n\t * @since   3.20.0\n\t * @version 3.20.0\n\t */\n\tprivate function enroll( $user, $trigger ) {\n\n\t\t// Enroll into LifterLMS product.\n\t\t$enrolled = llms_enroll_student( $user->ID, $this->product_id, $trigger );\n\n\t\t// Figure out notice type based on enrollment success.\n\t\t$type = ( ! $enrolled ) ? 'error' : 'success';\n\n\t\t// Figure out notice message string based on notice type.\n\t\t$success_fail_string = ( ! $enrolled ) ? __( 'Failed to enroll <em>%1$1s</em> into <em>%2$2s</em>.', 'lifterlms' ) : __( 'Successfully enrolled <em>%1$1s</em> into <em>%2$2s</em>.', 'lifterlms' );\n\n\t\t// Get formatted message with username and product title.\n\t\t$message = sprintf( $success_fail_string, $user->display_name, $this->product_title );\n\n\t\t// Generate a notice for display.\n\t\t$this->generate_notice( $type, $message );\n\t}\n\n\t/**\n\t * Generates admin notice markup\n\t *\n\t * @param   string $type Type of notice 'error' or 'success'\n\t * @param   string $message Notice message\n\t * @since   3.20.0\n\t * @version 3.20.0\n\t */\n\tpublic function generate_notice( $type, $message ) {\n\t\tob_start();\n\t\t?>\n\t\t<div class=\"notice notice-<?php echo esc_attr( $type ); ?> is-dismissible\">\n\t\t\t<p><?php echo wp_kses_post( $message ); ?></p>\n\t\t</div>\n\t\t<?php\n\t\t$notice                = ob_get_clean();\n\t\t$this->admin_notices[] = $notice;\n\t}\n\n\t/**\n\t * Displays all generated notices\n\t *\n\t * @return  void\n\t * @since   3.20.0\n\t * @version 3.20.0\n\t */\n\tpublic function display_notices() {\n\t\tif ( empty( $this->admin_notices ) ) {\n\t\t\treturn;\n\t\t}\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Notices are escaped in generate_notice().\n\t\techo implode( \"\\n\", $this->admin_notices );\n\t}\n}\n\nreturn new LLMS_Student_Bulk_Enroll();\n"
  },
  {
    "path": "includes/admin/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/llms.functions.admin.php",
    "content": "<?php\n/**\n * Core functions used exclusively on the admin panel\n *\n * @package LifterLMS/Admin/Functions\n *\n * @since 3.0.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Create a Page & save it's id as an option.\n *\n * @since 3.0.0\n * @since 3.7.5 Unknown.\n * @since 7.3.0 Strip all tags from the page title, slash the page data prior to inserting the page in the db via `wp_insert_post`.\n *              Prefer strict type comparison when using `in_array()`.\n *\n * @param string $slug    Page slug.\n * @param string $title   Page title.\n * @param string $content Page content\n * @param string $option  Option name.\n * @return int Page id.\n */\nfunction llms_create_page( $slug, $title = '', $content = '', $option = '' ) {\n\n\t$option_val = get_option( $option );\n\n\t// See if there's a valid page already stored for the option we're trying to create.\n\tif ( $option_val && is_numeric( $option_val ) ) {\n\t\t$page_object = get_post( $option_val );\n\t\tif ( $page_object && 'page' === $page_object->post_type &&\n\t\t\t\t! in_array( $page_object->post_status, array( 'pending', 'trash', 'future', 'auto-draft' ), true ) ) {\n\t\t\treturn $page_object->ID;\n\t\t}\n\t}\n\n\tglobal $wpdb;\n\n\t// Search for an existing page with the specified page content like a shortcode.\n\tif ( strlen( $content ) > 0 ) {\n\t\t$page_id = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_content LIKE %s LIMIT 1;\",\n\t\t\t\t\"%{$content}%\"\n\t\t\t)\n\t\t);// no-cache ok.\n\t} else {\n\t\t$page_id = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' )  AND post_name = %s LIMIT 1;\",\n\t\t\t\t$slug\n\t\t\t)\n\t\t);// no-cache ok.\n\t}\n\n\t/**\n\t * Filters the ID of the page to be created.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param int     $page_id The WP_Post ID of the page.\n\t * @param string  $slug    The page slug.\n\t * @param string  $content THe content of the page.\n\t */\n\t$page_id = apply_filters( 'llms_create_page_id', $page_id, $slug, $content );\n\tif ( $page_id ) {\n\t\tif ( $option ) {\n\t\t\tupdate_option( $option, $page_id );\n\t\t}\n\t\treturn $page_id;\n\t}\n\n\t// Look in the trashed page by content.\n\tif ( strlen( $content ) > 0 ) {\n\t\t$trashed_id = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_content LIKE %s LIMIT 1;\",\n\t\t\t\t\"%{$content}%\"\n\t\t\t)\n\t\t);// no-cache ok.\n\t} else {\n\t\t$trashed_id = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_name = %s LIMIT 1;\",\n\t\t\t\t$slug\n\t\t\t)\n\t\t);// no-cache ok.\n\t}\n\n\t// If we find it in the trash move it out of the trash.\n\tif ( $trashed_id ) {\n\t\t$page_id   = $trashed_id;\n\t\t$page_data = array(\n\t\t\t'ID'          => $page_id,\n\t\t\t'post_status' => 'publish',\n\t\t);\n\t\twp_update_post( $page_data );\n\t} else {\n\t\t$page_data = array(\n\t\t\t'post_status'    => 'publish',\n\t\t\t'post_type'      => 'page',\n\t\t\t'post_author'    => get_current_user_id() ? get_current_user_id() : 1,\n\t\t\t'post_name'      => $slug,\n\t\t\t'post_title'     => wp_strip_all_tags( $title ),\n\t\t\t'post_content'   => $content,\n\t\t\t'comment_status' => 'closed',\n\t\t);\n\t\t$page_id   = wp_insert_post(\n\t\t\twp_slash(\n\t\t\t\t/**\n\t\t\t\t * Filters the page data passed to create a page.\n\t\t\t\t *\n\t\t\t\t * The output of this filter will be slashed via `wp_slash` prior\n\t\t\t\t * to being passed to `wp_insert_post` to prevent slashes from\n\t\t\t\t * being stripped from the page title.\n\t\t\t\t *\n\t\t\t\t * @since 3.0.0\n\t\t\t\t *\n\t\t\t\t * @param array $page_data Array of page data.\n\t\t\t\t */\n\t\t\t\tapply_filters( 'llms_create_page', $page_data )\n\t\t\t)\n\t\t);\n\t}\n\tif ( $option ) {\n\t\tupdate_option( $option, $page_id );\n\t}\n\n\treturn $page_id;\n}\n\n/**\n * Retrieve available products from the LifterLMS.com API\n *\n * @since 3.22.0\n *\n * @return array {\n *     Array of LifterLMS add-on data from the LifterLMS.com products api.\n *\n *     @type array   $categories Associative array of add-on category information, mapping ID to Title.\n *     @type array[] $items      List of add-ons definition arrays.\n * }\n */\nfunction llms_get_add_ons( $use_cache = true ) {\n\n\t$data = $use_cache ? get_transient( 'llms_products_api_result' ) : false;\n\n\tif ( false === $data ) {\n\n\t\t$req  = new LLMS_Dot_Com_API( '/products', array(), 'GET' );\n\t\t$data = $req->get_result();\n\n\t\tif ( $req->is_error() ) {\n\t\t\treturn $data;\n\t\t}\n\n\t\tset_transient( 'llms_products_api_result', $data, DAY_IN_SECONDS );\n\n\t}\n\n\treturn $data;\n}\n\n/**\n * Instantiate a new LLMS_Add_On object\n *\n * @since 3.22.0\n *\n * @param string|array $addon      Add-on data array or a string (such as an ID or update file path) used to lookup the addon.\n * @param string       $lookup_key If $addon is a string, this determines how to lookup the addon from the available list of addons.\n * @return LLMS_Add_On|LLMS_Helper_Add_On\n */\nfunction llms_get_add_on( $addon = array(), $lookup_key = 'id' ) {\n\tif ( class_exists( 'LLMS_Helper_Add_On' ) ) {\n\t\treturn new LLMS_Helper_Add_On( $addon, $lookup_key );\n\t}\n\treturn new LLMS_Add_On( $addon, $lookup_key );\n}\n\n/**\n * Retrieves HTML for a Dashicon wrapped in an anchor.\n *\n * A utility for adding links to external documentation.\n *\n * @since 7.0.0\n *\n * @param string $url The URL of the anchor tag.\n * @param array  $args {\n *     An array of optional configuration options.\n *\n *     @type integer $size  The size of the icon. Default 18.\n *     @type string  $title The title attribute of the anchor tag. Default: \"More information\".\n *     @type string  $icon  The Dashicon icon to use, {@see @link https://developer.wordpress.org/resource/dashicons/}. Default: \"external\".\n * }\n * @return string\n */\nfunction llms_get_dashicon_link( $url, $args = array() ) {\n\n\t$args = wp_parse_args(\n\t\t$args,\n\t\tarray(\n\t\t\t'size'  => 18,\n\t\t\t'title' => esc_attr__( 'More information', 'lifterlms' ),\n\t\t\t'icon'  => 'external',\n\t\t)\n\t);\n\n\t$dashicon = sprintf(\n\t\t'<span class=\"dashicons dashicons-%1$s\" style=\"font-size:%2$dpx;width:%2$dpx;height:%2$dpx\"></span>',\n\t\tesc_attr( $args['icon'] ),\n\t\t$args['size']\n\t);\n\n\treturn sprintf(\n\t\t'<a href=\"%1$s\" style=\"text-decoration:none;\" target=\"_blank\" rel=\"noreferrer\" title=\"%2$s\">%3$s</a>',\n\t\tesc_url( $url ),\n\t\tesc_attr( $args['title'] ),\n\t\t$dashicon\n\t);\n}\n\n/**\n * Get an array of available course/membership sales page options\n *\n * @return   array\n * @since    3.23.0\n * @version  3.23.0\n */\nfunction llms_get_sales_page_types() {\n\treturn apply_filters(\n\t\t'llms_sales_page_types',\n\t\tarray(\n\t\t\t'none'    => __( 'Display default course content', 'lifterlms' ),\n\t\t\t'content' => __( 'Show custom content', 'lifterlms' ),\n\t\t\t'page'    => __( 'Redirect to WordPress Page', 'lifterlms' ),\n\t\t\t'url'     => __( 'Redirect to custom URL', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Get an array of available course/membership checkout redirection options\n *\n * @since    3.30.0\n * @version  3.30.0\n *\n * @param    string $product_type The product type, Course or Membership\n * @return   array\n */\nfunction llms_get_checkout_redirection_types( $product_type = '' ) {\n\n\t$product_type = empty( $product_type ) ? __( 'Course/Membership', 'lifterlms' ) : $product_type;\n\n\treturn apply_filters(\n\t\t'llms_checkout_redirection_types',\n\t\tarray(\n\t\t\t'self' => sprintf( __( '(Default) Return to %s', 'lifterlms' ), $product_type ),\n\t\t\t'page' => __( 'Redirect to a WordPress Page', 'lifterlms' ),\n\t\t\t'url'  => __( 'Redirect to a custom URL', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Add a \"merge code\" button that to auto-add merge codes to email & etc...\n *\n * @since 3.1.0\n * @since 3.17.4 Unknown.\n * @since 6.0.0 Move HTML into view file: `includes/admin/views/merge-code-editor-button.php`.\n *                Move certificate merge code list to `llms_get_certificate_merge_codes()`.\n *\n * @param string  $target Target to add the merge code to. Accepts the ID of a tinymce editor or a DOM ID (#element-id).\n * @param boolean $echo   If `true`, echos the HTML output.\n * @param array   $codes  Optional array of custom codes to pass in, otherwise the codes are determined\n *                        what is available for the post type.\n * @return string Returns the HTML for the merge code button.\n */\nfunction llms_merge_code_button( $target = 'content', $echo = true, $codes = array() ) {\n\n\t$screen = get_current_screen();\n\n\tif ( ! $codes && $screen && isset( $screen->post_type ) ) {\n\n\t\tswitch ( $screen->post_type ) {\n\n\t\t\tcase 'llms_certificate':\n\t\t\t\t$codes = llms_get_certificate_merge_codes();\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_email':\n\t\t\t\t$codes = array(\n\t\t\t\t\t'{site_title}'    => __( 'Website Title', 'lifterlms' ),\n\t\t\t\t\t'{site_url}'      => __( 'Website URL', 'lifterlms' ),\n\t\t\t\t\t'{email_address}' => __( 'Student Email Address', 'lifterlms' ),\n\t\t\t\t\t'{user_login}'    => __( 'Student Username', 'lifterlms' ),\n\t\t\t\t\t'{first_name}'    => __( 'Student First Name', 'lifterlms' ),\n\t\t\t\t\t'{last_name}'     => __( 'Student Last Name', 'lifterlms' ),\n\t\t\t\t\t'{current_date}'  => __( 'Current Date', 'lifterlms' ),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$codes = array();\n\n\t\t}\n\t}\n\n\t/**\n\t * Filters the list of available merge codes in the specified context.\n\t *\n\t * @since Unknown\n\t *\n\t * @param array[]        $codes  Associative array of merge codes where the array key is the merge code and the array value is a name / description of the merge code.\n\t * @param WP_Screen|null $screen The screen object from `get_current_screen().\n\t * @param string         $target Target to add the merge code to. Accepts the ID of a tinymce editor or a DOM ID (#element-id).\n\t */\n\t$codes = apply_filters( 'llms_merge_codes_for_button', $codes, $screen, $target );\n\n\t$html = '';\n\tif ( $codes ) {\n\t\tob_start();\n\t\tinclude LLMS_PLUGIN_DIR . 'includes/admin/views/merge-code-button.php';\n\t\t$html = ob_get_clean();\n\t}\n\n\tif ( $echo ) {\n\t\t// PHPCS ignore reason: Escaped in the view file.\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo $html;\n\t}\n\n\treturn $html;\n}\n\n/**\n * Retrieve the precision for round function for floating values.\n *\n * @since 7.1.3\n *\n * @return int\n */\nfunction llms_get_floats_rounding_precision() {\n\n\t// Used `static` to store precision value so `apply_filters()` run only once per request.\n\tstatic $precision = null;\n\n\tif ( is_null( $precision ) ) {\n\t\t/**\n\t\t * Filters the precision for round function for floating values.\n\t\t *\n\t\t * @since 7.1.3\n\t\t *\n\t\t * @param int $precision Precision for round function for floating values.\n\t\t */\n\t\t$precision = apply_filters( 'lifterlms_floats_rounding_precision', 2 );\n\t}\n\n\treturn $precision;\n}\n"
  },
  {
    "path": "includes/admin/post-types/class.llms.meta.boxes.php",
    "content": "<?php\n/**\n * Admin base Metabox.\n *\n * @package LifterLMS/Admin/PostTypes/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Meta_Boxes class\n *\n * Sets up base metabox functionality and global save.\n *\n * @since 1.0.0\n * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n */\nclass LLMS_Admin_Meta_Boxes {\n\n\t/**\n\t * Array of collected errors.\n\t *\n\t * @access public\n\t * @var string\n\t */\n\tprivate static $errors = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.0.0\n\t * @since 3.16.0 Unknown.\n\t * @since 6.0.0 Instantiate award engagement submit meta box.\n\t *               Instantiate meta boxes to sync awarded certificates and achievements with their templates.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Achievements.\n\t\tnew LLMS_Meta_Box_Achievement();\n\t\tnew LLMS_Meta_Box_Achievement_Sync();\n\n\t\t// Certs.\n\t\tnew LLMS_Meta_Box_Certificate();\n\t\tnew LLMS_Meta_Box_Certificate_Sync();\n\n\t\t// Emails.\n\t\tnew LLMS_Meta_Box_Email_Settings();\n\n\t\t// Engagements.\n\t\tnew LLMS_Meta_Box_Engagement();\n\n\t\t// Award Engagements.\n\t\tnew LLMS_Meta_Box_Award_Engagement_Submit();\n\n\t\t// Membership restriction metabox.\n\t\tnew LLMS_Meta_Box_Access();\n\n\t\t// Courses.\n\t\tnew LLMS_Meta_Box_Course_Options();\n\n\t\t// Memberships.\n\t\tnew LLMS_Meta_Box_Membership();\n\n\t\t// Courses & memberships.\n\t\trequire_once 'meta-boxes/class.llms.meta.box.course.builder.php';\n\t\trequire_once 'meta-boxes/class.llms.meta.box.visibility.php';\n\t\tnew LLMS_Meta_Box_Product();\n\t\tnew LLMS_Meta_Box_Students();\n\n\t\t// Lessons.\n\t\trequire_once 'meta-boxes/class.llms.meta.box.lesson.php';\n\n\t\t// Coupons.\n\t\tnew LLMS_Meta_Box_Coupon();\n\n\t\t// Orders.\n\t\tnew LLMS_Meta_Box_Order_Submit();\n\t\tnew LLMS_Meta_Box_Order_Details();\n\t\tnew LLMS_Meta_Box_Order_Transactions();\n\t\tnew LLMS_Meta_Box_Order_Enrollment();\n\t\tnew LLMS_Meta_Box_Order_Notes();\n\n\t\t// Vouchers.\n\t\tnew LLMS_Meta_Box_Voucher();\n\n\t\tadd_action( 'add_meta_boxes', array( $this, 'hide_meta_boxes' ), 10 );\n\t\tadd_action( 'add_meta_boxes', array( $this, 'refresh_meta_boxes' ), 10 );\n\t\tadd_action( 'add_meta_boxes', array( $this, 'get_meta_boxes' ), 10 );\n\t\tadd_action( 'save_post', array( $this, 'save_meta_boxes' ), 10, 2 );\n\n\t\tadd_action( 'lifterlms_process_llms_voucher_meta', 'LLMS_Meta_Box_Voucher_Export::export', 10, 2 );\n\n\t\t// Error handling.\n\t\tadd_action( 'admin_notices', array( $this, 'display_errors' ) );\n\t\tadd_action( 'shutdown', array( $this, 'set_errors' ) );\n\n\t\t// Modify the title placeholder text for achievement and certificate templates.\n\t\tadd_filter( 'enter_title_here', array( $this, 'maybe_modify_title_placeholder' ), 10, 2 );\n\n\t\t// Add default image information for achievement and certificate templates.\n\t\tadd_filter( 'admin_post_thumbnail_html', array( $this, 'maybe_modify_post_thumbnail_html' ), 10, 3 );\n\t}\n\n\t/**\n\t * Add error messages from metaboxes\n\t *\n\t * @param string $text\n\t */\n\tpublic static function add_error( $text ) {\n\t\tself::$errors[] = $text;\n\t}\n\n\t/**\n\t * Save messages to the database\n\t */\n\tpublic function set_errors() {\n\t\tupdate_option( 'lifterlms_errors', self::$errors );\n\t}\n\n\t/**\n\t * Display the messages in the error dialog box\n\t */\n\tpublic function display_errors() {\n\t\t$errors = get_option( 'lifterlms_errors' );\n\n\t\tif ( empty( $errors ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$errors = maybe_unserialize( $errors );\n\n\t\techo '<div id=\"lifterlms_errors\" class=\"error\"><p>';\n\n\t\tforeach ( $errors as $error ) {\n\t\t\techo esc_html( $error );\n\t\t}\n\n\t\techo '</p></div>';\n\n\t\tdelete_option( 'lifterlms_errors' );\n\t}\n\n\t/**\n\t * Add Metaboxes\n\t *\n\t * @return   void\n\t * @since    1.0.0\n\t * @version  3.16.0\n\t */\n\tpublic function get_meta_boxes() {\n\n\t\tadd_action( 'media_buttons', 'llms_merge_code_button' );\n\n\t\t/**\n\t\t * @todo Transition to new style metaboxes.\n\t\t */\n\t\tadd_meta_box( 'lifterlms-voucher-export', __( 'Export CSV', 'lifterlms' ), 'LLMS_Meta_Box_Voucher_Export::output', 'llms_voucher', 'side', 'default' );\n\t}\n\n\t/**\n\t * Remove Metaboxes\n\t *\n\t * @return void\n\t * @since    3.4.0\n\t * @version  3.13.0\n\t */\n\tpublic function hide_meta_boxes() {\n\n\t\t// Remove some defaults from orders.\n\t\tremove_meta_box( 'commentstatusdiv', 'llms_order', 'normal' );\n\t\tremove_meta_box( 'commentsdiv', 'llms_order', 'normal' );\n\t\tremove_meta_box( 'slugdiv', 'llms_order', 'normal' );\n\n\t\t// Remove the default submit box in favor of our custom box.\n\t\tremove_meta_box( 'submitdiv', 'llms_order', 'side' );\n\n\t\t// Remove some defaults from the course.\n\t\tremove_meta_box( 'postexcerpt', 'course', 'normal' );\n\t\tremove_meta_box( 'tagsdiv-course_difficulty', 'course', 'side' );\n\t}\n\n\t/**\n\t * Modifies the featured image metabox for achievement and certificate templates.\n\t *\n\t * Displays the default image, text denoting that the default image is being used,\n\t * and a link to the settings page where the default image can be changed.\n\t *\n\t * This additional content is only displayed when there's no featured image set\n\t * for the template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $content  Default metabox HTML.\n\t * @param int    $post_id  WP_Post ID of the post being edited.\n\t * @param int    $image_id Attachment ID for the saved featured image.\n\t * @return string\n\t */\n\tpublic function maybe_modify_post_thumbnail_html( $content, $post_id, $image_id ) {\n\n\t\t$post_types = array(\n\t\t\t'llms_achievement',\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_certificate',\n\t\t\t'llms_my_certificate',\n\t\t);\n\t\t$post_type  = get_post_type( $post_id );\n\t\tif ( ! $image_id && in_array( $post_type, $post_types, true ) ) {\n\n\t\t\t$add_content = '';\n\n\t\t\t$class = str_replace( array( 'llms_', 'my_' ), '', $post_type ) . 's';\n\n\t\t\t$image_id = llms()->$class()->get_default_image_id();\n\t\t\t$alt      = $image_id ? get_post_meta( $image_id, '_wp_attachment_image_alt', true ) : __( 'Default image', 'lifterlms' );\n\n\t\t\t$add_content = '<p><img alt=\"' . trim( wp_strip_all_tags( $alt ) ) . '\" src=\"' . esc_url( llms()->$class()->get_default_image( $post_id ) ) . '\" /></p>';\n\n\t\t\t$settings_url = admin_url( 'admin.php?page=llms-settings&tab=engagements' );\n\t\t\t$add_content .= '<p class=\"howto\">' . __( 'Using the global default.', 'lifterlms' ) . ' <a href=\"' . esc_url( $settings_url ) . '\">' . __( 'Edit', 'lifterlms' ) . '</a></p>';\n\n\t\t\t$content = $add_content . $content;\n\t\t}\n\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Modifies the placeholder text for the post title field.\n\t *\n\t * This is used to denote that the achievement and certificate template title fields\n\t * are for internal use only to help avoid confusion as to why there are two separate\n\t * titles.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string  $placeholder Default placeholder text.\n\t * @param WP_Post $post        Post object.\n\t * @return string\n\t */\n\tpublic function maybe_modify_title_placeholder( $placeholder, $post ) {\n\t\t$post_types = array(\n\t\t\t'llms_achievement',\n\t\t\t'llms_certificate',\n\t\t);\n\t\tif ( in_array( $post->post_type, $post_types, true ) ) {\n\t\t\t$placeholder = sprintf(\n\t\t\t\t'%1$s (%2$s)',\n\t\t\t\t$placeholder,\n\t\t\t\t_x( 'for internal use only', 'added achievement and certificate template post title placeholder', 'lifterlms' )\n\t\t\t);\n\t\t}\n\n\t\treturn $placeholder;\n\t}\n\n\t/**\n\t * Updates global $post variable\n\t *\n\t * @return void\n\t */\n\tpublic function refresh_meta_boxes() {\n\t\tglobal $post;\n\t}\n\n\t/**\n\t * Validates post and metabox data before saving.\n\t *\n\t * @since Unknown\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t *\n\t * @param int     $post_id WP Post ID.\n\t * @param WP_Post $post Post object.\n\t * @return bool\n\t */\n\tpublic function validate_post( $post_id, $post ) {\n\n\t\tif ( ! current_user_can( 'edit_post', $post_id ) ) {\n\t\t\treturn false;\n\t\t} elseif ( empty( $post_id ) || empty( $post ) ) {\n\t\t\treturn false;\n\t\t} elseif ( defined( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) {\n\t\t\treturn false;\n\t\t} elseif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn false;\n\t\t} elseif ( empty( $_POST['post_ID'] ) || $_POST['post_ID'] != $post_id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Check whether the post is a LifterLMS post type.\n\t *\n\t * @since unknown\n\t * @since 6.0.0 Added 'llms_my_achievement' and 'llms_my_certificate'.\n\t *\n\t * @param WP_Post $post WP_Post instance.\n\t * @return boolean\n\t */\n\tpublic function is_llms_post_type( $post ) {\n\t\t$post_types = array(\n\t\t\t'course',\n\t\t\t'section',\n\t\t\t'lesson',\n\t\t\t'llms_order',\n\t\t\t'llms_email',\n\t\t\t'llms_certificate',\n\t\t\t'llms_my_certificate',\n\t\t\t'llms_achievement',\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_engagement',\n\t\t\t'llms_membership',\n\t\t\t'llms_quiz',\n\t\t\t'llms_question',\n\t\t\t'llms_coupon',\n\t\t\t'llms_voucher',\n\t\t);\n\n\t\t/**\n\t\t * Filters the post type names that are secific of LifterLMS.\n\t\t *\n\t\t * Used to determine whether or not fire actions of the type \"lifterlms_process_{$post->post_type}_meta\" on save.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param string[] $post_types Array of post type names.\n\t\t * @param WP_Post  $post       WP_Post instance.\n\t\t */\n\t\t$post_types = apply_filters( 'llms_metaboxes_llms_post_types', $post_types, $post );\n\n\t\tif ( in_array( $post->post_type, $post_types, true ) ) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t/**\n\t * Global Metabox Save\n\t *\n\t * @return void\n\t * @param $post, $post_id\n\t */\n\tpublic function save_meta_boxes( $post_id, $post ) {\n\n\t\tif ( self::validate_post( $post_id, $post ) ) {\n\n\t\t\tif ( self::is_llms_post_type( $post ) ) {\n\n\t\t\t\tdo_action( 'lifterlms_process_' . $post->post_type . '_meta', $post_id, $post );\n\n\t\t\t}\n\t\t}\n\t}\n}\n\nnew LLMS_Admin_Meta_Boxes();\n"
  },
  {
    "path": "includes/admin/post-types/class.llms.post.tables.php",
    "content": "<?php\n/**\n * Post Table management for LifterLMS custom post types\n *\n * @package LifterLMS/Admin/PostTypes/Classes\n *\n * @since 3.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Tables class.\n *\n * @since 3.0.0\n * @since 3.13.0 Unknown.\n * @since 3.33.1 Use `llms_filter_input`\n * @since 3.33.1 Use specific caps (`edit_course`) instead of generic caps (`edit_post`) for exporting and cloning courses.\n */\nclass LLMS_Admin_Post_Tables {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.0.0\n\t */\n\tpublic function __construct() {\n\n\t\t// Load all post table classes.\n\t\tforeach ( glob( LLMS_PLUGIN_DIR . '/includes/admin/post-types/post-tables/*.php' ) as $filename ) {\n\t\t\tinclude_once $filename;\n\t\t}\n\n\t\tadd_filter( 'post_row_actions', array( $this, 'add_links' ), 777, 2 );\n\t\tadd_action( 'admin_init', array( $this, 'handle_link_actions' ) );\n\t}\n\n\t/**\n\t * Adds clone links to post types which support lifterlms post cloning\n\t *\n\t * @since 3.3.0\n\t * @since 3.13.0 Unknown.\n\t * @since 3.33.1 Use `edit_course` instead of `edit_post` when checking capabilities.\n\t *\n\t * @param array   $actions Existing actions.\n\t * @param WP_Post $post Post object.\n\t * @return string[]\n\t */\n\tpublic function add_links( $actions, $post ) {\n\n\t\tif ( current_user_can( 'edit_course', $post->ID ) && post_type_supports( $post->post_type, 'llms-clone-post' ) ) {\n\t\t\t$url                   = add_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => $post->post_type,\n\t\t\t\t\t'action'    => 'llms-clone-post',\n\t\t\t\t\t'post'      => $post->ID,\n\t\t\t\t),\n\t\t\t\tadmin_url( 'edit.php' )\n\t\t\t);\n\t\t\t$actions['llms-clone'] = '<a href=\"' . esc_url( wp_nonce_url( $url, 'llms_clone_post', 'llms_clone_post_nonce' ) ) . '\">' . __( 'Clone', 'lifterlms' ) . '</a>';\n\t\t}\n\n\t\tif ( current_user_can( 'edit_course', $post->ID ) && post_type_supports( $post->post_type, 'llms-export-post' ) ) {\n\t\t\t$url                    = add_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => $post->post_type,\n\t\t\t\t\t'action'    => 'llms-export-post',\n\t\t\t\t\t'post'      => $post->ID,\n\t\t\t\t),\n\t\t\t\tadmin_url( 'edit.php' )\n\t\t\t);\n\t\t\t$actions['llms-export'] = '<a href=\"' . esc_url( $url ) . '\">' . __( 'Export', 'lifterlms' ) . '</a>';\n\t\t}\n\n\t\tif ( current_user_can( 'edit_course', $post->ID ) && post_type_supports( $post->post_type, 'llms-detach-post' ) ) {\n\n\t\t\t$url = add_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => $post->post_type,\n\t\t\t\t\t'action'    => 'llms-detach-post',\n\t\t\t\t\t'post'      => $post->ID,\n\t\t\t\t),\n\t\t\t\tadmin_url( 'edit.php' )\n\t\t\t);\n\n\t\t\t$actions['llms-detach'] = '<a href=\"' . esc_url( wp_nonce_url( $url, 'llms_detach_post', 'llms_detach_post_nonce' ) ) . '\">' . __( 'Detach', 'lifterlms' ) . '</a>';\n\t\t}\n\n\t\treturn $actions;\n\t}\n\n\t/**\n\t * Handle events for our custom postrow actions\n\t *\n\t * @since 3.3.0\n\t * @since 3.33.1 Use `llms_filter_input` to access `$_GET` and `$_POST` data.\n\t * @since 3.33.1 Use `edit_course` cap instead of `edit_post` cap.\n\t * @since 7.5.1 Adding nonce to course clone links\n\t *\n\t * @return void\n\t */\n\tpublic function handle_link_actions() {\n\n\t\t$action = llms_filter_input( INPUT_GET, 'action' );\n\n\t\t// Bail early if request doesn't concern us.\n\t\tif ( empty( $action ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Bail early if it isn't a clone/ export request.\n\t\tif ( 'llms-clone-post' !== $action && 'llms-export-post' !== $action && 'llms-detach-post' !== $action ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$post_id = llms_filter_input( INPUT_GET, 'post' );\n\n\t\t// Bail if there's no post ID.\n\t\tif ( empty( $post_id ) ) {\n\t\t\twp_die( esc_html__( 'Missing post ID.', 'lifterlms' ) );\n\t\t}\n\n\t\t$post = get_post( $post_id );\n\n\t\t// Bail if post ID is invalid.\n\t\tif ( ! $post ) {\n\t\t\twp_die( esc_html__( 'Invalid post ID.', 'lifterlms' ) );\n\t\t}\n\n\t\t// Bail if the action isn't supported on post type.\n\t\tif ( ! post_type_supports( $post->post_type, $action ) ) {\n\t\t\twp_die( esc_html__( 'Action cannot be executed on the current post.', 'lifterlms' ) );\n\t\t}\n\n\t\t// Bail if user doesn't have permissions.\n\t\tif ( ! current_user_can( 'edit_course', $post->ID ) ) {\n\t\t\twp_die( esc_html__( 'You are not authorized to perform this action on the current post.', 'lifterlms' ) );\n\t\t}\n\n\t\t$post = llms_get_post( $post );\n\n\t\t// Run export or clone action as needed.\n\t\tswitch ( $action ) {\n\n\t\t\tcase 'llms-export-post':\n\t\t\t\t$post->export();\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms-detach-post':\n\t\t\t\tif ( ! isset( $_GET['llms_detach_post_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['llms_detach_post_nonce'] ), 'llms_detach_post' ) ) {\n\t\t\t\t\twp_die( esc_html__( 'You are not authorized to perform this action on the current post.', 'lifterlms' ) );\n\t\t\t\t}\n\t\t\t\t$r = delete_post_meta( $post->id, '_llms_parent_section' ) && delete_post_meta( $post->id, '_llms_parent_course' );\n\t\t\t\tif ( ! $r ) {\n\t\t\t\t\tLLMS_Admin_Notices::flash_notice( esc_html__( 'There was an error detaching the post.', 'lifterlms' ), 'error' );\n\t\t\t\t}\n\t\t\t\twp_safe_redirect( admin_url( 'edit.php?post_type=' . $post->get( 'type' ) ) );\n\t\t\t\texit;\n\n\t\t\tcase 'llms-clone-post':\n\t\t\t\tif ( ! isset( $_GET['llms_clone_post_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['llms_clone_post_nonce'] ), 'llms_clone_post' ) ) {\n\t\t\t\t\twp_die( esc_html__( 'You are not authorized to perform this action on the current post.', 'lifterlms' ) );\n\t\t\t\t}\n\t\t\t\t$r = $post->clone_post();\n\t\t\t\tif ( is_wp_error( $r ) ) {\n\t\t\t\t\tLLMS_Admin_Notices::flash_notice( $r->get_error_message(), 'error' );\n\t\t\t\t}\n\t\t\t\twp_safe_redirect( admin_url( 'edit.php?post_type=' . $post->get( 'type' ) ) );\n\t\t\t\texit;\n\n\t\t}\n\t}\n\n\t/**\n\t * Get the HTML for a post type select2 filter\n\t *\n\t * @since 3.12.0\n\t * @since 6.0.0 Don't display a dynamic view post button.\n\t *\n\t * @param string $name      Name of the select element.\n\t * @param string $post_type Post type to search by.\n\t * @param int[]  $selected  Array of POST IDs to use for the pre-selected options on page load.\n\t * @return string\n\t */\n\tpublic static function get_post_type_filter_html( $name, $post_type = 'course', $selected = array() ) {\n\n\t\t$id = sprintf( 'filter-by-llms-post-%s', $post_type );\n\n\t\t$obj = get_post_type_object( $post_type );\n\t\t// Translators: %s = the singular post type name.\n\t\t$label = sprintf( __( 'Filter by %s', 'lifterlms' ), $obj->labels->singular_name );\n\t\tob_start();\n\t\t?>\n\t\t<span class=\"llms-post-table-post-filter\">\n\t\t\t<label for=\"<?php echo esc_attr( $id ); ?>\" class=\"screen-reader-text\">\n\t\t\t\t<?php echo esc_html( $label ); ?>\n\t\t\t</label>\n\t\t\t<select\n\t\t\t\tclass=\"llms-select2-post\"\n\t\t\t\tdata-allow_clear=\"true\"\n\t\t\t\tdata-no-view-button=\"true\"\n\t\t\t\tdata-placeholder=\"<?php echo esc_attr( $label ); ?>\"\n\t\t\t\tdata-post-type=\"<?php echo esc_attr( $post_type ); ?>\"\n\t\t\t\tname=\"<?php echo esc_attr( $name ); ?>\"\n\t\t\t\tid=\"<?php echo esc_attr( $id ); ?>\"\n\t\t\t>\n\t\t\t\t<?php if ( $selected ) : ?>\n\t\t\t\t\t<?php foreach ( llms_make_select2_post_array( $selected ) as $data ) : ?>\n\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $data['key'] ); ?>\"><?php echo esc_html( $data['title'] ); ?></option>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t</select>\n\t\t</span>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n}\nreturn new LLMS_Admin_Post_Tables();\n"
  },
  {
    "path": "includes/admin/post-types/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class-llms-meta-box-achievement-sync.php",
    "content": "<?php\n/**\n * LLMS_Meta_Box_Achievement_Sync class\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta box to sync between awarded achievements and achievement templates.\n *\n * @since 6.0.0\n */\nclass LLMS_Meta_Box_Achievement_Sync extends LLMS_Abstract_Meta_Box_User_Engagement_Sync {\n\n\t/**\n\t * Type of user engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $engagement_type = 'achievement';\n\n\t/**\n\t * The post type of an awarded engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $post_type_awarded = 'llms_my_achievement';\n\n\t/**\n\t * The post type of an engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $post_type_template = 'llms_achievement';\n\n\t/**\n\t * Post types that this meta box should be added to.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string[]\n\t */\n\tpublic $screens = array(\n\t\t'llms_achievement', // Template.\n\t\t'llms_my_achievement', // Awarded.\n\t);\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Meta_Box_User_Engagement_Sync::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\tswitch ( $text_type ) {\n\t\t\tcase self::TEXT_SYNC_ALERT_MANY_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded achievements */\n\t\t\t\t\t__(\n\t\t\t\t\t\t'This action will replace the current title, content, background etc. of %1$d awarded achievements with the ones from this achievement template. Are you sure you want to proceed?',\n\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_ALERT_ONE_AWARDED_ENGAGEMENT:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded achievements */\n\t\t\t\t\t__(\n\t\t\t\t\t\t'This action will replace the current title, content, background etc. of %1$d awarded achievement with the ones from this achievement template. Are you sure you want to proceed?',\n\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_ALERT_THIS_AWARDED_ENGAGEMENT:\n\t\t\t\treturn __(\n\t\t\t\t\t'This action will replace the current title, content, background etc. of this awarded achievement with the ones from the achievement template. Are you sure you want to proceed?',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_DESCRIPTION_MANY_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded achievements */\n\t\t\t\t\t__( 'Sync %1$d awarded achievements with this achievement template.', 'lifterlms' ),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_DESCRIPTION_ONE_AWARDED_ENGAGEMENT:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded achievements */\n\t\t\t\t\t__( 'Sync %1$d awarded achievement with this achievement template.', 'lifterlms' ),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_DESCRIPTION_THIS_AWARDED_ENGAGEMENT:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: link to edit the achievement template, %2$s: closing anchor tag */\n\t\t\t\t\t__( 'Sync this awarded achievement with its %1$sachievement template%2$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( ( $variables['template_id'] ?? 0 ) ) . '\" target=\"_blank\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_ENGAGEMENT_TEMPLATE_NO_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn __( 'This achievement template has no awarded achievements to sync.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_TITLE_AWARDED_ENGAGEMENT:\n\t\t\t\treturn __( 'Sync Awarded Achievement', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_TITLE_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn __( 'Sync Awarded Achievements', 'lifterlms' );\n\t\t\tdefault:\n\t\t\t\treturn parent::get_text( $text_type );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class-llms-meta-box-certificate-sync.php",
    "content": "<?php\n/**\n * LLMS_Meta_Box_Certificate_Sync class\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta box to sync between awarded certificates and certificate templates.\n *\n * @since 6.0.0\n */\nclass LLMS_Meta_Box_Certificate_Sync extends LLMS_Abstract_Meta_Box_User_Engagement_Sync {\n\n\t/**\n\t * Type of user engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $engagement_type = 'certificate';\n\n\t/**\n\t * The post type of an awarded engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $post_type_awarded = 'llms_my_certificate';\n\n\t/**\n\t * The post type of an engagement template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $post_type_template = 'llms_certificate';\n\n\t/**\n\t * Post types that this meta box should be added to.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string[]\n\t */\n\tpublic $screens = array(\n\t\t'llms_certificate', // Template.\n\t\t'llms_my_certificate', // Awarded.\n\t);\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Meta_Box_User_Engagement_Sync::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\tswitch ( $text_type ) {\n\t\t\tcase self::TEXT_SYNC_ALERT_MANY_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded certificates */\n\t\t\t\t\t__(\n\t\t\t\t\t\t'This action will replace the current title, content, background etc. of %1$d awarded certificates with the ones from this certificate template. Are you sure you want to proceed?',\n\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_ALERT_ONE_AWARDED_ENGAGEMENT:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded certificates */\n\t\t\t\t\t__(\n\t\t\t\t\t\t'This action will replace the current title, content, background etc. of %1$d awarded certificate with the ones from this certificate template. Are you sure you want to proceed?',\n\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_ALERT_THIS_AWARDED_ENGAGEMENT:\n\t\t\t\treturn __(\n\t\t\t\t\t'This action will replace the current title, content, background etc. of this awarded certificate with the ones from the certificate template. Are you sure you want to proceed?',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_DESCRIPTION_MANY_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded certificates */\n\t\t\t\t\t__( 'Sync %1$d awarded certificates with this certificate template.', 'lifterlms' ),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_DESCRIPTION_ONE_AWARDED_ENGAGEMENT:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: number of awarded certificates */\n\t\t\t\t\t__( 'Sync %1$d awarded certificate with this certificate template.', 'lifterlms' ),\n\t\t\t\t\t( $variables['awarded_number'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_DESCRIPTION_THIS_AWARDED_ENGAGEMENT:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: link to edit the certificate template, %2$s: closing anchor tag */\n\t\t\t\t\t__( 'Sync this awarded certificate with its %1$scertificate template%2$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( ( $variables['template_id'] ?? 0 ) ) . '\" target=\"_blank\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_ENGAGEMENT_TEMPLATE_NO_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn __( 'This certificate template has no awarded certificates to sync.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_TITLE_AWARDED_ENGAGEMENT:\n\t\t\t\treturn __( 'Sync Awarded certificate', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_TITLE_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn __( 'Sync Awarded certificates', 'lifterlms' );\n\t\t\tdefault:\n\t\t\t\treturn parent::get_text( $text_type );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php",
    "content": "<?php\n/**\n * Membership Access Restrictions meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 3.36.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Access class\n *\n * @since 1.0.0\n * @since 3.0.0 Updated for 3.0.0 compatibility.\n */\nclass LLMS_Meta_Box_Access extends LLMS_Admin_Metabox {\n\n\n\t/**\n\t * Configure the metabox\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id      = 'lifterlms-membership-access';\n\t\t$this->title   = __( 'Membership Access', 'lifterlms' );\n\t\t$this->screens = $this->get_screens();\n\t\t$this->context = 'side';\n\n\t}\n\n\t/**\n\t * Define metabox fields\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\t$post_type = get_post_type_object( $this->post->post_type );\n\n\t\t$restrictions = get_post_meta( $this->post->ID, $this->prefix . 'restricted_levels', true );\n\n\t\tif ( ! $restrictions ) {\n\t\t\t$restrictions = array();\n\t\t}\n\n\t\treturn array(\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Membership Access', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controls'   => '#' . $this->prefix . 'restricted_levels',\n\t\t\t\t\t\t'desc_class' => 'd-1of2 t-1of2 m-1of2',\n\t\t\t\t\t\t'id'         => $this->prefix . 'is_restricted',\n\t\t\t\t\t\t'label'      => sprintf( _x( 'Restrict this %s', 'apply membership restriction to post type', 'lifterlms' ), $post_type->labels->singular_name ),\n\t\t\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t\t\t'value'      => 'yes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'           => 'input-full llms-select2-post',\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'post-type' => 'llms_membership',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'desc'            => sprintf( __( 'Visitors must belong to one of these memberships to access this %s', 'lifterlms' ), strtolower( $post_type->labels->singular_name ) ),\n\t\t\t\t\t\t'id'              => $this->prefix . 'restricted_levels',\n\t\t\t\t\t\t'label'           => __( 'Memberships', 'lifterlms' ),\n\t\t\t\t\t\t'multi'           => true,\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'value'           => llms_make_select2_post_array( $restrictions ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Determine the screens where the metabox should be rendered.\n\t *\n\t * This is determined by finding all public post types and checking if they support\n\t * the 'llms-membership-restrictions' feature.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_screens() {\n\n\t\t$screens = array();\n\n\t\t// Check against all public post types.\n\t\t$post_types = get_post_types(\n\t\t\tarray(\n\t\t\t\t'public' => true,\n\t\t\t),\n\t\t\t'names',\n\t\t\t'and'\n\t\t);\n\n\t\tforeach ( $post_types as $post_type ) {\n\n\t\t\t// check if the post type supports membership restrictions.\n\t\t\tif ( post_type_supports( $post_type, 'llms-membership-restrictions' ) ) {\n\n\t\t\t\t$screens[] = $post_type;\n\n\t\t\t}\n\t\t}\n\n\t\treturn $screens;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php",
    "content": "<?php\n/**\n * LLMS_Meta_Box_Achievement class file.\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Achievements meta box class.\n *\n * Generates the main metabox for the `llms_achievement` and `llms_my_achievement` post types.\n *\n * @since 1.0.0\n */\nclass LLMS_Meta_Box_Achievement extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings.\n\t *\n\t * @since 3.0.0\n\t * @since 6.0.0 Added support for the `llms_my_achievement` post type.\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-achievement';\n\t\t$this->title    = __( 'Achievement Settings', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_achievement',\n\t\t\t'llms_my_achievement',\n\t\t);\n\t\t$this->priority = 'high';\n\n\t}\n\n\t/**\n\t * Builds array of metabox options.\n\t *\n\t * Array is called in output method to display options.\n\t * Appropriate fields are generated based on type.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Allow some fields to store values with quotes.\n\t * @since 6.0.0 Removed the deprecated achievement background image meta field.\n\t *              Made the title field conditional based on viewed post type.\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\t$fields = array();\n\n\t\tif ( 'llms_achievement' === $this->post->post_type ) {\n\n\t\t\t$fields[] = array(\n\t\t\t\t'label'    => __( 'Achievement Title', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'The name of the achievement which will be shown to users', 'lifterlms' ),\n\t\t\t\t'id'       => $this->prefix . 'achievement_title',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'class'    => 'input-full',\n\t\t\t\t'sanitize' => 'no_encode_quotes',\n\t\t\t);\n\n\t\t}\n\n\t\t$fields[] = array(\n\t\t\t'label'    => __( 'Achievement Content', 'lifterlms' ),\n\t\t\t'desc'     => __( 'An optional short description of the achievement which will be shown to users', 'lifterlms' ),\n\t\t\t'id'       => $this->prefix . 'achievement_content',\n\t\t\t'type'     => 'textarea_w_tags',\n\t\t\t'sanitize' => 'no_encode_quotes',\n\t\t\t'cols'     => 80,\n\t\t\t'rows'     => 8,\n\t\t\t'meta'     => $this->post->post_content,\n\t\t);\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'General', 'lifterlms' ),\n\t\t\t\t'fields' => $fields,\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Save field in the db.\n\t *\n\t * Expects an already sanitized value.\n\t *\n\t * Stores the `achievement_content` field as `post_content` in favor of storing it in the postmeta table.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $post_id  The WP Post ID.\n\t * @param int   $field_id The field identifier.\n\t * @param mixed $val      Value to save.\n\t * @return bool\n\t */\n\tprotected function save_field_db( $post_id, $field_id, $val ) {\n\t\t// Save to the post content field.\n\t\tif ( $this->prefix . 'achievement_content' === $field_id && $this->post->ID === $post_id ) {\n\n\t\t\treturn wp_update_post(\n\t\t\t\tarray(\n\t\t\t\t\t'ID'           => $post_id,\n\t\t\t\t\t'post_content' => $val,\n\t\t\t\t)\n\t\t\t) ? true : false;\n\t\t}\n\n\t\treturn parent::save_field_db( $post_id, $field_id, $val );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.award.engagement.submit.php",
    "content": "<?php\n/**\n * Award engagement submit meta box.\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Award engagement submit meta box class.\n *\n * @since 6.0.0\n */\nclass LLMS_Meta_Box_Award_Engagement_Submit extends LLMS_Admin_Metabox {\n\n\t/**\n\t * ID of the student who earned (is about to earn) the engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var int\n\t */\n\tprivate $student_id;\n\n\t/**\n\t * Allowed post types.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string[]\n\t */\n\tprivate $post_types = array(\n\t\t'llms_my_achievement' => array(\n\t\t\t'model'          => 'LLMS_User_Achievement',\n\t\t\t'reporting_stab' => 'achievements',\n\t\t),\n\t\t'llms_my_certificate' => array(\n\t\t\t'model'          => 'LLMS_User_Certificate',\n\t\t\t'reporting_stab' => 'certificates',\n\t\t),\n\t);\n\n\t/**\n\t * Configure the metabox settings.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'submitdiv'; // Overrides the WordPress core one.\n\t\t$this->title    = __( 'Award', 'lifterlms' );\n\t\t$this->screens  = array_keys( $this->post_types );\n\t\t$this->context  = 'side';\n\t\t$this->priority = 'high';\n\n\t\t$this->callback_args = function () {\n\t\t\treturn 'llms_my_certificate' === get_post_type() ?\n\t\t\t\tarray(\n\t\t\t\t\t'__back_compat_meta_box' => true,\n\t\t\t\t)\n\t\t\t\t:\n\t\t\t\tarray();\n\t\t};\n\t}\n\n\t/**\n\t * Not used because our metabox doesn't use the standard fields api.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Function to field WP::output() method call.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $action;\n\n\t\tadd_action( 'admin_print_footer_scripts', array( __CLASS__, 'metabox_scripts' ), PHP_INT_MAX );\n\n\t\t$engagement             = $this->post;\n\t\t$engagement_id          = (int) $this->post->ID;\n\t\t$engagement_type_object = get_post_type_object( $this->post->post_type );\n\t\t$can_publish            = current_user_can( $engagement_type_object->cap->publish_posts );\n\t\t$fields                 = $this->student_fields();\n\n\t\tinclude LLMS_PLUGIN_DIR . 'includes/admin/views/metaboxes/view-award-engagement-submit.php';\n\t}\n\n\t/**\n\t * Student fields.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string\n\t */\n\tprivate function student_fields() {\n\n\t\t$fields = '';\n\n\t\t// Creating.\n\t\tif ( 'add' === get_current_screen()->action ) {\n\t\t\t$fields = $this->student_fields_on_creation();\n\t\t}\n\n\t\t$fields .= $this->student_information();\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Student fields on creation html.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string\n\t */\n\tprivate function student_fields_on_creation() {\n\n\t\t$student_id = $this->current_student_id( true );\n\n\t\t// The `post_author_override` is the same used in WP core for the author selector.\n\t\t$field_id = 'post_author_override';\n\n\t\t$field = array(\n\t\t\t'id'        => $field_id,\n\t\t\t'type'      => 'hidden',\n\t\t\t'value'     => $student_id,\n\t\t\t'skip_save' => true,\n\t\t\t'required'  => true,\n\t\t);\n\n\t\tif ( empty( $student_id ) ) {\n\t\t\t$field = array(\n\t\t\t\t'allow_null'      => false,\n\t\t\t\t'class'           => 'llms-select2-student',\n\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t'allow_clear' => false,\n\t\t\t\t\t'placeholder' => esc_attr__( 'Select a Student', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'id'              => $field_id,\n\t\t\t\t'label'           => ' ' . esc_html__( 'Select a Student', 'lifterlms' ),\n\t\t\t\t'type'            => 'select',\n\t\t\t\t'skip_save'       => true,\n\t\t\t\t'required'        => true,\n\t\t\t);\n\t\t}\n\n\t\treturn $this->process_field( $field );\n\t}\n\n\t/**\n\t * Current student information html.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string\n\t */\n\tprivate function student_information() {\n\n\t\t$post_type  = get_post_type( $this->post->ID );\n\t\t$student_id = $this->current_student_id();\n\t\t$student    = $student_id ? llms_get_student( $student_id ) : $student_id;\n\n\t\t// Bail if no student.\n\t\tif ( empty( $student ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$first = $student->get( 'first_name' );\n\t\t$last  = $student->get( 'last_name' );\n\n\t\tif ( ! $first || ! $last ) {\n\t\t\t$name = $student->get( 'display_name' );\n\t\t} else {\n\t\t\t$name = $last . ', ' . $first;\n\t\t}\n\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'page'       => 'llms-reporting',\n\t\t\t\t'tab'        => 'students',\n\t\t\t\t'student_id' => $student_id,\n\t\t\t\t'stab'       => $this->post_types[ $post_type ]['reporting_stab'],\n\t\t\t),\n\t\t\tadmin_url( 'admin.php' )\n\t\t);\n\n\t\treturn sprintf(\n\t\t\t'<li class=\"llms-mb-list student-info\"> <b>%1$s:</b>&nbsp;<span>%2$s</span></li>',\n\t\t\tesc_html__( 'Student', 'lifterlms' ),\n\t\t\tsprintf(\n\t\t\t\t'<a href=\"%1$s\" target=\"_blank\">%2$s &lt;%3$s&gt;</a>',\n\t\t\t\tesc_url( $url ),\n\t\t\t\tesc_html( $name ),\n\t\t\t\tesc_html( $student->get( 'user_email' ) )\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the current student id.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param null|bool $creating Whether or not we're awarding an engagement.\n\t *                            If not provided, it'll be dynamically retrieved if the current screen's action is 'add'.\n\t *                            See WordPress' `get_current_screen()`.\n\t * @return int\n\t */\n\tprivate function current_student_id( $creating = null ) {\n\n\t\tif ( isset( $this->student_id ) ) {\n\t\t\treturn $this->student_id;\n\t\t}\n\n\t\t$creating  = $creating ?? ( 'add' === get_current_screen()->action );\n\t\t$post_type = get_post_type( $this->post->ID );\n\t\t// If creating, take into account passed GET variable.\n\t\t$student = $creating && ! empty( $_GET['sid'] ) ? llms_filter_input( INPUT_GET, 'sid', FILTER_SANITIZE_NUMBER_INT ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no need to verify the nonce here.\n\t\t// If not creating, retrieve the earned engagement user id.\n\t\t$student          = ! $creating ? ( new $this->post_types[ $post_type ]['model']( $this->post->ID ) )->get_user_id() : $student;\n\t\t$this->student_id = $student;\n\n\t\treturn $this->student_id;\n\t}\n\n\t/**\n\t * Metabox specific scripts.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function metabox_scripts() {\n\t\t?>\n<script>\n\tdocument.addEventListener(\n\t\t\"DOMContentLoaded\",\n\t\t() => {\n\t\t\t// Localization.\n\t\t\tconst __  = window.wp.i18n.__,\n\t\t\t\t_i18n = {\n\t\t\t\t'Publish on:'  : __( 'Award on:', 'lifterlms' ),\n\t\t\t\t'Publish'      : __( 'Award', 'lifterlms' ),\n\t\t\t\t'Published'    : __( 'Awarded', 'lifterlms' ),\n\t\t\t\t'Published on:': __( 'Awarded on:', 'lifterlms' ),\n\t\t\t};\n\n\t\t\twindow.wp.hooks.addFilter(\n\t\t\t\t'i18n.gettext',\n\t\t\t\t'llms.awardEngagement.submitbox',\n\t\t\t\t( translation, text ) => {\n\t\t\t\t\treturn text in _i18n ? _i18n[text] : translation;\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t);\n</script>\n\t\t<?php\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php",
    "content": "<?php\n/**\n * Certificates meta box.\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Certificate template meta box class.\n *\n * @since 1.0.0\n * @since 3.37.12 Allow the certificate title field to store text with quotes.\n */\nclass LLMS_Meta_Box_Certificate extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings.\n\t *\n\t * @since 3.0.0\n\t * @since 6.0.0 Renamed from \"Certificate Settings\" to \"Settings\".\n\t *              Moved to the side context with default priority.\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id            = 'lifterlms-certificate';\n\t\t$this->title         = __( 'Settings', 'lifterlms' );\n\t\t$this->screens       = array(\n\t\t\t'llms_certificate',\n\t\t);\n\t\t$this->priority      = 'default';\n\t\t$this->context       = 'side';\n\t\t$this->callback_args = array(\n\t\t\t'__back_compat_meta_box' => true,\n\t\t);\n\n\t}\n\n\t/**\n\t * Builds array of metabox options.\n\t *\n\t * Array is called in output method to display options.\n\t * Appropriate fields are generated based on type.\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.4 Unknown.\n\t * @since 3.37.12 Allow the certificate title field to store text with quotes.\n\t * @since 6.0.0 Remove the background image option (in favor of featured image metabox).\n\t *              Expose the \"Next Sequential ID\" option.\n\t *\n\t * @return array Array of metabox fields.\n\t */\n\tpublic function get_fields() {\n\n\t\t$next_id = llms_get_certificate_sequential_id( $this->post->ID );\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'label'      => __( 'Certificate Title', 'lifterlms' ),\n\t\t\t\t'id'         => $this->prefix . 'certificate_title',\n\t\t\t\t'type'       => 'text',\n\t\t\t\t'class'      => 'input-full',\n\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t'sanitize'   => 'no_encode_quotes',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'label'      => __( 'Next Sequential ID', 'lifterlms' ),\n\t\t\t\t'id'         => $this->prefix . 'sequential_id',\n\t\t\t\t'type'       => 'number',\n\t\t\t\t'class'      => 'input-full',\n\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t'value'      => $next_id,\n\t\t\t\t'min'        => $next_id,\n\t\t\t\t'step'       => 1,\n\t\t\t),\n\t\t);\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'General', 'lifterlms' ),\n\t\t\t\t'fields' => $fields,\n\t\t\t),\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php",
    "content": "<?php\n/**\n * Coupon meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 6.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Coupon meta box class\n *\n * @since 1.0.0\n * @since 3.32.0 Coupons can now be restricted also to a draft or scheduled Course/Membership.\n * @since 3.35.0 Sanitize `$_POST` data and verify nonce.\n * @since 3.37.19 Localize strings that were missing translation functions.\n */\nclass LLMS_Meta_Box_Coupon extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @return void\n\t * @since  3.0.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-coupon';\n\t\t$this->title    = __( 'Coupon Settings', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_coupon',\n\t\t);\n\t\t$this->priority = 'high';\n\n\t}\n\n\t/**\n\t * This function is where extending classes can configure all the fields within the metabox.\n\t * The function must return an array which can be consumed by the \"output\" function.\n\t *\n\t * @since 3.0.0\n\t * @since 3.32.0 Coupons can now be restricted also to a draft or scheduled Course/Membership\n\t *               via the `<select />` data attribute 'post-statuses' (data-post-status).\n\t * @since 3.37.19 Localize strings that were missing translation functions.\n\t * @since 6.9.0 Add step definitions for discount amount and trial discount amount to allow float values to be used.\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\t$courses     = array();\n\t\t$memberships = array();\n\n\t\tif ( isset( $this->post ) ) {\n\n\t\t\t$c = new LLMS_Coupon( $this->post );\n\n\t\t\tforeach ( $c->get_array( 'coupon_courses' ) as $course_id ) {\n\t\t\t\t$courses[] = array(\n\t\t\t\t\t'key'   => $course_id,\n\t\t\t\t\t'title' => get_the_title( $course_id ) . ' (' . __( 'ID#', 'lifterlms' ) . ' ' . $course_id . ')',\n\t\t\t\t);\n\t\t\t}\n\t\t\tforeach ( $c->get_array( 'coupon_membership' ) as $membership_id ) {\n\t\t\t\t$memberships[] = array(\n\t\t\t\t\t'key'   => $membership_id,\n\t\t\t\t\t'title' => get_the_title( $membership_id ) . ' (' . __( 'ID#', 'lifterlms' ) . ' ' . $membership_id . ')',\n\t\t\t\t);\n\t\t\t}\n\t\t} else {\n\n\t\t\t$c = false;\n\n\t\t}\n\n\t\treturn array(\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'General', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'allow_null'      => false,\n\t\t\t\t\t\t'class'           => 'llms-select2',\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'minimum-results-for-search' => 5,\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'desc'            => __( 'Select a dollar or percentage discount.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'      => 'd-all',\n\t\t\t\t\t\t'id'              => $this->prefix . 'discount_type',\n\t\t\t\t\t\t'label'           => __( 'Discount Type', 'lifterlms' ),\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'value'           => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'percent',\n\t\t\t\t\t\t\t\t'title' => __( 'Percentage Discount', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'dollar',\n\t\t\t\t\t\t\t\t'title' => sprintf( __( '%s Discount', 'lifterlms' ), get_lifterlms_currency_symbol() ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'label'           => __( 'Access Plan Types', 'lifterlms' ),\n\t\t\t\t\t\t'desc'            => __( 'Select which type of access plans this coupon can be used with.', 'lifterlms' ),\n\t\t\t\t\t\t'id'              => $this->prefix . 'plan_type',\n\t\t\t\t\t\t'class'           => 'llms-select2',\n\t\t\t\t\t\t'value'           => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'any',\n\t\t\t\t\t\t\t\t'title' => __( 'Any Access Plan', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'one-time',\n\t\t\t\t\t\t\t\t'title' => __( 'Only One-time Payment Access Plans', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'recurring',\n\t\t\t\t\t\t\t\t'title' => sprintf( __( 'Only Recurring Access Plans', 'lifterlms' ), get_lifterlms_currency_symbol() ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'desc_class'      => 'd-all',\n\t\t\t\t\t\t'allow_null'      => false,\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'minimum-results-for-search' => 5,\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'number',\n\t\t\t\t\t\t'label'      => __( 'Discount Amount', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => sprintf( __( 'The amount to be subtracted from the \"Price\" of an applicable access plan. Do not include symbols such as %1$s.', 'lifterlms' ), get_lifterlms_currency_symbol() ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'coupon_amount',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'required'   => true,\n\t\t\t\t\t\t'step'       => '0.01',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t\t\t'label'      => __( 'Enable Trial Pricing Discount', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'When checked, the coupon can apply a discount to the \"Trial Price\" of an access plan.', 'lifterlms' ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'enable_trial_discount',\n\t\t\t\t\t\t'value'      => 'yes',\n\t\t\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'controls'   => '#' . $this->prefix . 'trial_amount',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'number',\n\t\t\t\t\t\t'label'      => __( 'Trial Discount Amount', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => sprintf( __( 'The amount to be subtracted from the \"Trial Price\" of an applicable access plan. Do not include symbols such as %1$s.', 'lifterlms' ), get_lifterlms_currency_symbol() ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'trial_amount',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => '',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'step'       => '0.01',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Restrictions', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'label'           => __( 'Courses', 'lifterlms' ),\n\t\t\t\t\t\t'desc'            => __( 'Limit coupon to the following courses.', 'lifterlms' ),\n\t\t\t\t\t\t'id'              => $this->prefix . 'coupon_courses',\n\t\t\t\t\t\t'class'           => 'input-full llms-select2-post',\n\t\t\t\t\t\t'value'           => $courses,\n\t\t\t\t\t\t'multi'           => true,\n\t\t\t\t\t\t'selected'        => $c ? $c->get_array( 'coupon_courses' ) : array(),\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'post-type'     => 'course',\n\t\t\t\t\t\t\t'post-statuses' => 'publish,draft,future',\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'label'           => __( 'Membership', 'lifterlms' ),\n\t\t\t\t\t\t'desc'            => __( 'Limit coupon to the following memberships.', 'lifterlms' ),\n\t\t\t\t\t\t'id'              => $this->prefix . 'coupon_membership',\n\t\t\t\t\t\t'class'           => 'input-full llms-select2-post',\n\t\t\t\t\t\t'value'           => $memberships,\n\t\t\t\t\t\t'multi'           => true,\n\t\t\t\t\t\t'selected'        => $c ? $c->get_array( 'coupon_membership' ) : array(),\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'post-type'     => 'llms_membership',\n\t\t\t\t\t\t\t'post-statuses' => 'publish,draft,future',\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'date',\n\t\t\t\t\t\t'label'      => __( 'Coupon Expiration Date', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'Coupon will no longer be usable after this date. Leave blank for no expiration.', 'lifterlms' ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'expiration_date',\n\t\t\t\t\t\t'class'      => 'llms-datepicker input-full',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => '',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'number',\n\t\t\t\t\t\t'label'      => __( 'Usage Limit', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'The amount of times this coupon can be used. Leave empty or enter 0 for unlimited uses.', 'lifterlms' ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'usage_limit',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => '',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Description', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'textarea',\n\t\t\t\t\t\t'label'      => __( 'Description', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'Optional description for internal notes. This is never displayed to your students.', 'lifterlms' ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'description',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => '',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'required'   => false,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Save all metadata\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Sanitize `$_POST` data and verify nonce.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id WP Post ID.\n\t * @return void\n\t */\n\tprotected function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$coupon = new LLMS_Coupon( $post_id );\n\n\t\t// Dupcheck the title.\n\t\t$exists = llms_find_coupon( $coupon->get( 'title' ), $post_id );\n\t\tif ( $exists ) {\n\t\t\t$this->add_error( __( 'Coupon code already exists. Customers will use the most recently created coupon with this code.', 'lifterlms' ) );\n\t\t}\n\n\t\t// Trial validation.\n\t\t$trial_discount = llms_filter_input_sanitize_string( INPUT_POST, $this->prefix . 'enable_trial_discount' );\n\t\t$trial_amount   = llms_filter_input( INPUT_POST, $this->prefix . 'trial_amount', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( ! $trial_discount ) {\n\t\t\t$trial_discount = 'no';\n\t\t} elseif ( 'yes' === $trial_discount && empty( $trial_amount ) ) {\n\t\t\t$this->add_error( __( 'A Trial Discount Amount was not supplied. Trial Pricing Discount has automatically been disabled. Please re-enable Trial Pricing Discount and enter a Trial Discount Amount, then save this coupon again.', 'lifterlms' ) );\n\t\t\t$trial_discount = 'no';\n\t\t}\n\n\t\t$coupon->set( 'enable_trial_discount', $trial_discount );\n\t\t$coupon->set( 'trial_amount', $trial_amount );\n\n\t\t$courses = llms_filter_input( INPUT_POST, $this->prefix . 'coupon_courses', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\tif ( empty( $courses ) ) {\n\t\t\t$courses = array();\n\t\t}\n\n\t\t$coupon->set( 'coupon_courses', $courses );\n\n\t\t$memberships = llms_filter_input( INPUT_POST, $this->prefix . 'coupon_membership', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\tif ( empty( $memberships ) ) {\n\t\t\t$memberships = array();\n\t\t}\n\n\t\t$coupon->set( 'coupon_membership', $memberships );\n\n\t\t// Save all the fields.\n\t\t$fields = array(\n\t\t\t'coupon_amount',\n\t\t\t'usage_limit',\n\t\t\t'discount_type',\n\t\t\t'description',\n\t\t\t'expiration_date',\n\t\t\t'plan_type',\n\t\t);\n\t\tforeach ( $fields as $field ) {\n\t\t\tif ( isset( $_POST[ $this->prefix . $field ] ) ) {\n\t\t\t\t$coupon->set( $field, llms_filter_input_sanitize_string( INPUT_POST, $this->prefix . $field ) );\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php",
    "content": "<?php\n/**\n * Course Builder meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 3.13.0\n * @version 3.30.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta box for the \"Course Builder\" launcher/browser\n *\n * @since 3.13.0\n * @since 3.30.1 Add `llms-mb-container` CSS class to container element in the `output()` method.\n */\nclass LLMS_Metabox_Course_Builder extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @return   void\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id         = 'course_builder';\n\t\t$this->title      = __( 'Course Builder', 'lifterlms' );\n\t\t$this->screens    = array(\n\t\t\t'course',\n\t\t\t'lesson',\n\t\t);\n\t\t$this->context    = 'side';\n\t\t$this->capability = 'edit_course';\n\t}\n\n\t/**\n\t * Get a URL to the course builder with an optional hash to a lesson/quiz/assignment\n\t *\n\t * @param   int    $course_id WP Post ID of a course.\n\t * @param   string $hash      Hash of the lesson & tab info (lesson:{$lesson_id}:tab).\n\t * @return  string\n\t * @since   3.27.0\n\t * @version 3.27.0\n\t */\n\tpublic function get_builder_url( $course_id, $hash = null ) {\n\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t'course_id' => $course_id,\n\t\t\t),\n\t\t\tadmin_url( 'admin.php' )\n\t\t);\n\n\t\tif ( $hash ) {\n\t\t\t$url = $url . '#' . $hash;\n\t\t}\n\n\t\treturn $url;\n\t}\n\n\t/**\n\t * This metabox has no options\n\t *\n\t * @return   array\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Get the HTML for a title, optionally as an anchor\n\t *\n\t * @param    string  $title  title to display\n\t * @param    boolean $url    url to link to\n\t * @return   string\n\t * @since    3.13.0\n\t * @version  3.27.0\n\t */\n\tpublic function get_title_html( $title, $url = false ) {\n\n\t\tif ( $url ) {\n\t\t\t$title = sprintf( '<a href=\"%1$s\">%2$s</a>', esc_url( $url ), $title );\n\t\t}\n\n\t\treturn $title;\n\t}\n\n\t/**\n\t * Override the output method to output a button\n\t *\n\t * @since 3.13.0\n\t * @since 3.30.1 Add `llms-mb-container` CSS class to container element.\n\t *\n\t * @return   void\n\t */\n\tpublic function output() {\n\n\t\t$post_id = $this->post->ID;\n\n\t\t$lesson  = false;\n\t\t$section = false;\n\t\tif ( 'lesson' === $this->post->post_type ) {\n\t\t\t$course = llms_get_post_parent_course( $post_id );\n\t\t\tif ( ! $course ) {\n\t\t\t\tesc_html_e( 'This lesson is not attached to a course.', 'lifterlms' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t$course_id = $course->get( 'id' );\n\t\t\t$lesson    = llms_get_post( $this->post );\n\t\t\t$section   = $lesson->get_parent_section() ? llms_get_post( $lesson->get_parent_section() ) : false;\n\t\t} else {\n\t\t\t$course = llms_get_post( $post_id );\n\t\t}\n\t\t?>\n\t\t<div class=\"llms-builder-launcher llms-mb-container\">\n\n\t\t\t<?php if ( $lesson && $section ) : ?>\n\n\t\t\t\t<p><strong><?php printf( esc_html__( 'Course: %s', 'lifterlms' ), wp_kses_post( $this->get_title_html( $course->get( 'title' ), get_edit_post_link( $course->get( 'id' ) ) ) ) ); ?></strong></p>\n\n\t\t\t\t<?php $this->output_section( $section, 'previous' ); ?>\n\n\t\t\t\t<?php $this->output_section( $section, 'current' ); ?>\n\n\t\t\t\t<?php $this->output_section( $section, 'next' ); ?>\n\n\t\t\t<?php endif; ?>\n\n\t\t\t<a class=\"llms-button-primary full\" href=\"<?php echo esc_url( $this->get_builder_url( $course->get( 'id' ) ) ); ?>\"><?php esc_html_e( 'Launch Course Builder', 'lifterlms' ); ?></a>\n\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * HTML helper to output info for a section\n\t *\n\t * @param    obj    $section  LLMS_Section object\n\t * @param    string $which    positioning [current|previous|next]\n\t * @return   void\n\t * @since    3.13.0\n\t * @version  3.28.0\n\t */\n\tprivate function output_section( $section, $which ) {\n\n\t\t$url = false;\n\n\t\tif ( 'previous' === $which ) {\n\t\t\t$section = $section->get_previous();\n\t\t} elseif ( 'next' === $which ) {\n\t\t\t$section = $section->get_next();\n\t\t}\n\n\t\tif ( ! $section ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( 'previous' === $which || 'next' === $which ) {\n\t\t\t$lessons = $section->get_lessons( 'ids' );\n\t\t\tif ( $lessons ) {\n\t\t\t\t$url = get_edit_post_link( $lessons[0] );\n\t\t\t}\n\t\t}\n\t\t?>\n\n\t\t<p><strong><?php printf( esc_html__( 'Section %1$d: %2$s', 'lifterlms' ), esc_html( $section->get( 'order' ) ), wp_kses_post( $this->get_title_html( $section->get( 'title' ), $url ) ) ); ?></strong></p>\n\n\t\t<?php if ( 'current' === $which ) : ?>\n\t\t\t<ol>\n\t\t\t<?php\n\t\t\tforeach ( $section->get_lessons() as $lesson ) :\n\t\t\t\t$hash = 'lesson:' . $lesson->get( 'id' );\n\t\t\t\t?>\n\t\t\t\t<li>\n\t\t\t\t\t<?php if ( $this->post->ID !== $lesson->get( 'id' ) ) : ?>\n\t\t\t\t\t\t<?php echo wp_kses_post( $this->get_title_html( $lesson->get( 'title' ), get_edit_post_link( $lesson->get( 'id' ) ) ) ); ?>\n\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t<?php echo wp_kses_post( $lesson->get( 'title' ) ); ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<a class=\"tip--top-left\" href=\"<?php echo esc_url( $this->get_builder_url( $lesson->get( 'parent_course' ), $hash ) ); ?>\" data-tip=\"<?php esc_attr_e( 'Edit lesson in builder', 'lifterlms' ); ?>\"><i class=\"fa fa-cog\"></i></a>\n\t\t\t\t\t<?php\n\t\t\t\t\tif ( $lesson->has_quiz() ) :\n\t\t\t\t\t\t$quiz = $lesson->get_quiz();\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<br>\n\t\t\t\t\t\t<?php printf( '<span class=\"tip--top-right\" data-tip=\"%1$s\"><i class=\"fa fa-question-circle\"></i></span> %2$s', esc_attr__( 'Quiz', 'lifterlms' ), wp_kses_post( $this->get_title_html( $quiz->get( 'title' ), $this->get_builder_url( $lesson->get( 'parent_course' ), $hash . ':quiz' ) ) ) ); ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php do_action( 'llms_builder_mb_after_lesson', $lesson, $this ); ?>\n\t\t\t\t</li>\n\t\t\t<?php endforeach; ?>\n\t\t\t</ol>\n\t\t\t<?php\n\t\tendif;\n\t}\n}\n\nreturn new LLMS_Metabox_Course_Builder();\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php",
    "content": "<?php\n/**\n * Course Options meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Course_Options class.\n *\n * Course Options meta box.\n *\n * @since 1.0.0\n * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n * @since 3.36.0 Allow some fields to store values with quotes.\n */\nclass LLMS_Meta_Box_Course_Options extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @return void\n\t * @since  3.0.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-course-options';\n\t\t$this->title    = __( 'Course Options', 'lifterlms' );\n\t\t$this->screens  = 'course';\n\t\t$this->priority = 'high';\n\t}\n\n\t/**\n\t * Setup fields.\n\t *\n\t * @since 1.0.0\n\t * @since 3.36.0 Allow some fields to store values with quotes.\n\t * @since 7.1.3 Fixed condition for unsetting fields when using Gutenberg.\n\t *              Replaced outdated URLs to WordPress' documentation about the list of sites you can embed from.\n\t * @since 7.1.4 Fixed issue that prevented the correct saving of the course length when using the block editor.\n\t * @since 7.2.0 Add function exists check for `llms_blocks_is_post_migrated`.\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\tglobal $post;\n\n\t\t$course = new LLMS_Course( $this->post );\n\n\t\t$course_tracks_options = get_terms( 'course_track', 'hide_empty=0' );\n\t\t$course_tracks         = array();\n\t\tforeach ( (array) $course_tracks_options as $term ) {\n\t\t\t$course_tracks[] = array(\n\t\t\t\t'key'   => $term->term_id,\n\t\t\t\t'title' => $term->name,\n\t\t\t);\n\t\t}\n\n\t\t// Setup course difficulty select options.\n\t\t$difficulty_terms   = get_terms( 'course_difficulty', 'hide_empty=0' );\n\t\t$difficulty_options = array();\n\t\tforeach ( $difficulty_terms as $term ) {\n\t\t\t$difficulty_options[] = array(\n\t\t\t\t'key'   => $term->slug,\n\t\t\t\t'title' => $term->name,\n\t\t\t);\n\t\t}\n\n\t\t$sales_page_content_type = 'none';\n\t\tif ( $post && 'auto-draft' !== $post->post_status && $post->post_excerpt ) {\n\t\t\t$sales_page_content_type = 'content';\n\t\t}\n\n\t\t$instructor_defaults = llms_get_instructors_defaults();\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Sales Page', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'allow_null'    => false,\n\t\t\t\t\t\t'class'         => 'llms-select2',\n\t\t\t\t\t\t'desc'          => __( 'Customize the content displayed to visitors and students who are not enrolled in the course.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'default'       => $sales_page_content_type,\n\t\t\t\t\t\t'id'            => $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'label'         => __( 'Sales Page Content', 'lifterlms' ),\n\t\t\t\t\t\t'type'          => 'select',\n\t\t\t\t\t\t'value'         => llms_get_sales_page_types(),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'controller_value' => 'content',\n\t\t\t\t\t\t'desc'             => __( 'This content will only be shown to visitors who are not enrolled in this course.', 'lifterlms' ),\n\t\t\t\t\t\t'id'               => '',\n\t\t\t\t\t\t'label'            => __( 'Sales Page Custom Content', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'post-excerpt',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'controller_value' => 'page',\n\t\t\t\t\t\t'data_attributes'  => array(\n\t\t\t\t\t\t\t'post-type'   => 'page',\n\t\t\t\t\t\t\t'placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'class'            => 'llms-select2-post',\n\t\t\t\t\t\t'id'               => $this->prefix . 'sales_page_content_page_id',\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'label'            => __( 'Select a Page', 'lifterlms' ),\n\t\t\t\t\t\t'value'            => $course->get( 'sales_page_content_page_id' ) ? llms_make_select2_post_array( array( $course->get( 'sales_page_content_page_id' ) ) ) : array(),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'controller_value' => 'url',\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t\t'label'            => __( 'Sales Page Redirect URL', 'lifterlms' ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'sales_page_content_url',\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'value'            => '',\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'group'            => 'top',\n\t\t\t\t\t),\n\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'General', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t\t'label'      => __( 'Course Length', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'Enter a description of the estimated length. IE: 3 days', 'lifterlms' ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'length',\n\t\t\t\t\t\t'class'      => 'input-full',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => 'top',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'      => 'llms-select2',\n\t\t\t\t\t\t'id'         => $this->prefix . 'post_course_difficulty',\n\t\t\t\t\t\t'desc'       => sprintf( __( 'Choose a course difficulty level. New difficulties can be added via %1$sCourses -> Difficulties%2$s.', 'lifterlms' ), '<a href=\"' . admin_url( 'edit-tags.php?taxonomy=course_difficulty&post_type=course' ) . '\">', '</a>' ),\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => 'bottom',\n\t\t\t\t\t\t'label'      => __( 'Course Difficulty Category', 'lifterlms' ),\n\t\t\t\t\t\t'selected'   => $course->get_difficulty( 'slug' ),\n\t\t\t\t\t\t'type'       => 'select',\n\t\t\t\t\t\t'value'      => $difficulty_options,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'allow_null'    => false,\n\t\t\t\t\t\t'class'         => 'llms-select2',\n\t\t\t\t\t\t'id'            => $this->prefix . 'focus_mode',\n\t\t\t\t\t\t'desc'          => __( 'Enable or disable Focus Mode for lessons in this course.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'    => 'd-all',\n\t\t\t\t\t\t'group'         => 'bottom',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'label'         => __( 'Focus Mode', 'lifterlms' ),\n\t\t\t\t\t\t'selected'      => $course->get( 'focus_mode' ) ? $course->get( 'focus_mode' ) : 'inherit',\n\t\t\t\t\t\t'type'          => 'select',\n\t\t\t\t\t\t'value'         => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'inherit',\n\t\t\t\t\t\t\t\t'title' => sprintf(\n\t\t\t\t\t\t\t\t\t/* translators: %s: current global setting label */\n\t\t\t\t\t\t\t\t\t__( 'Inherit Global Setting (%s)', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\t'yes' === get_option( 'lifterlms_enable_focus_mode', 'no' )\n\t\t\t\t\t\t\t\t\t\t? __( 'Enabled', 'lifterlms' )\n\t\t\t\t\t\t\t\t\t\t: __( 'Disabled', 'lifterlms' )\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'enable',\n\t\t\t\t\t\t\t\t'title' => __( 'Enable', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'disable',\n\t\t\t\t\t\t\t\t'title' => __( 'Disable', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'allow_null'       => false,\n\t\t\t\t\t\t'class'            => 'llms-select2',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'focus_mode',\n\t\t\t\t\t\t'controller_value' => 'enable,inherit',\n\t\t\t\t\t\t'id'               => $this->prefix . 'focus_mode_content_width',\n\t\t\t\t\t\t'desc'             => __( 'Set the maximum width of the lesson content area in focus mode.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'group'            => 'bottom',\n\t\t\t\t\t\t'label'            => __( 'Focus Mode Content Width', 'lifterlms' ),\n\t\t\t\t\t\t'selected'         => $course->get( 'focus_mode_content_width' ) ? $course->get( 'focus_mode_content_width' ) : 'inherit',\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'value'            => llms_get_focus_mode_content_width_options( true ),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'allow_null'       => false,\n\t\t\t\t\t\t'class'            => 'llms-select2',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'focus_mode',\n\t\t\t\t\t\t'controller_value' => 'enable,inherit',\n\t\t\t\t\t\t'id'               => $this->prefix . 'focus_mode_sidebar_position',\n\t\t\t\t\t\t'desc'             => __( 'Choose which side the course syllabus sidebar appears on.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'group'            => 'bottom',\n\t\t\t\t\t\t'label'            => __( 'Focus Mode Sidebar Position', 'lifterlms' ),\n\t\t\t\t\t\t'selected'         => $course->get( 'focus_mode_sidebar_position' ) ? $course->get( 'focus_mode_sidebar_position' ) : 'inherit',\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'value'            => llms_get_focus_mode_sidebar_position_options( true ),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'text',\n\t\t\t\t\t\t'label' => __( 'Featured Video', 'lifterlms' ),\n\t\t\t\t\t\t'desc'  => sprintf( __( 'Paste the url for a Wistia, Vimeo or Youtube video or a hosted video file. For a full list of supported providers see %s.', 'lifterlms' ), '<a href=\"https://wordpress.org/documentation/article/embeds/#list-of-sites-you-can-embed-from\" target=\"_blank\">WordPress oEmbeds</a>' ),\n\t\t\t\t\t\t'id'    => $this->prefix . 'video_embed',\n\t\t\t\t\t\t'class' => 'code input-full',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'desc'       => __( 'When enabled, the featured video will be displayed on the course tile in addition to the course page.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'         => $this->prefix . 'tile_featured_video',\n\t\t\t\t\t\t'label'      => __( 'Display Featured Video on Course Tile', 'lifterlms' ),\n\t\t\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t\t\t'value'      => 'yes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'text',\n\t\t\t\t\t\t'label' => __( 'Featured Audio', 'lifterlms' ),\n\t\t\t\t\t\t'desc'  => sprintf( __( 'Paste the url for a SoundCloud or Spotify song or a hosted audio file. For a full list of supported providers see %s.', 'lifterlms' ), '<a href=\"https://wordpress.org/documentation/article/embeds/#list-of-sites-you-can-embed-from\" target=\"_blank\">WordPress oEmbeds</a>' ),\n\t\t\t\t\t\t'id'    => $this->prefix . 'audio_embed',\n\t\t\t\t\t\t'class' => 'code input-full',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'basic-editor',\n\t\t\t\t\t\t'label' => __( 'Featured Pricing Information', 'lifterlms' ),\n\t\t\t\t\t\t'desc'  => __( 'Enter information on pricing for this course, to be displayed on the catalog page.', 'lifterlms' ),\n\t\t\t\t\t\t'id'    => $this->prefix . 'featured_pricing',\n\t\t\t\t\t\t'class' => 'code input-full',\n\t\t\t\t\t\t'value' => 'test',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'label'           => __( 'Course Completion Page', 'lifterlms' ),\n\t\t\t\t\t\t'class'           => 'llms-select2-post',\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'allow-clear' => true,\n\t\t\t\t\t\t\t'post-type'   => 'page',\n\t\t\t\t\t\t\t'placeholder' => get_option( 'lifterlms_course_completion_page_id', '' ) ?\n\t\t\t\t\t\t\t\tsprintf( __( 'Global setting (%s)', 'lifterlms' ), get_the_title( get_option( 'lifterlms_course_completion_page_id' ) ) ) :\n\t\t\t\t\t\t\t\t__( 'Global setting (none)', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'desc'            => sprintf( __( 'This page will be shown to students when they complete the course. %1$sMore Information%2$s', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/course-completion-page/\" target=\"_blank\">', '</a>' ),\n\t\t\t\t\t\t'id'              => $this->prefix . 'completion_page_id',\n\t\t\t\t\t\t'value'           => llms_make_select2_post_array( array( $course->get( 'completion_page_id' ) ) ),\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Restrictions', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'   => 'input-full',\n\t\t\t\t\t\t'default' => __( 'You must enroll in this course to access course content.', 'lifterlms' ),\n\t\t\t\t\t\t'desc'    => __( 'This message will be displayed when non-enrolled visitors attempt to access course content directly without enrolling first', 'lifterlms' ),\n\t\t\t\t\t\t'id'      => $this->prefix . 'content_restricted_message',\n\t\t\t\t\t\t'label'   => __( 'Content Restricted Message', 'lifterlms' ),\n\t\t\t\t\t\t'type'    => 'text',\n\t\t\t\t\t),\n\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'          => 'checkbox',\n\t\t\t\t\t\t'label'         => __( 'Enable Enrollment Period', 'lifterlms' ),\n\t\t\t\t\t\t'desc'          => __( 'Set registration start and end dates for this course', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'            => $this->prefix . 'enrollment_period',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'value'         => 'yes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'llms-datepicker input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'enrollment_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'desc'             => __( 'Registration opens on this date.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'id'               => $this->prefix . 'enrollment_start_date',\n\t\t\t\t\t\t'label'            => __( 'Enrollment Start Date', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'date',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'llms-datepicker input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'enrollment_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'desc'             => __( 'Registration closes on this date.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'id'               => $this->prefix . 'enrollment_end_date',\n\t\t\t\t\t\t'label'            => __( 'Enrollment End Date', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'date',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'enrollment_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'default'          => sprintf( __( 'Enrollment in this course opens on [lifterlms_course_info id=\"%d\" key=\"enrollment_start_date\"].', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'desc'             => sprintf( __( 'This message will be displayed to non-enrolled visitors before the Enrollment Start Date. You may use shortcodes like [lifterlms_course_info id=\"%d\" key=\"enrollment_start_date\"] in this message.', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'enrollment_opens_message',\n\t\t\t\t\t\t'label'            => __( 'Enrollment Opens Message', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t\t'sanitize'         => 'shortcode',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'enrollment_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'default'          => sprintf( __( 'Enrollment in this course closed on [lifterlms_course_info id=\"%d\" key=\"enrollment_end_date\"].', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'desc'             => sprintf( __( 'This message will be displayed to non-enrolled visitors once the Enrollment End Date has passed. You may use shortcodes like [lifterlms_course_info id=\"%d\" key=\"enrollment_end_date\"] in this message.', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'enrollment_closed_message',\n\t\t\t\t\t\t'label'            => __( 'Enrollment Closed Message', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t\t'sanitize'         => 'shortcode',\n\t\t\t\t\t),\n\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'          => 'checkbox',\n\t\t\t\t\t\t'label'         => __( 'Enable Course Time Period', 'lifterlms' ),\n\t\t\t\t\t\t'desc'          => __( 'Set start and end dates for this course. Content can only be viewed and completed within the selected range.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'            => $this->prefix . 'time_period',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'value'         => 'yes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'llms-datepicker input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'time_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'id'               => $this->prefix . 'start_date',\n\t\t\t\t\t\t'label'            => __( 'Course Start Date', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'date',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'llms-datepicker input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'time_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'id'               => $this->prefix . 'end_date',\n\t\t\t\t\t\t'label'            => __( 'Course End Date', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'date',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'time_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'default'          => sprintf( __( 'This course opens on [lifterlms_course_info id=\"%d\" key=\"start_date\"].', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'desc'             => sprintf( __( 'This message will be displayed to non-enrolled visitors before the Course Start Date. You may use shortcodes like [lifterlms_course_info id=\"%d\" key=\"start_date\"] in this message.', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'course_opens_message',\n\t\t\t\t\t\t'label'            => __( 'Course Opens Message', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t\t'sanitize'         => 'shortcode',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'time_period',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'default'          => sprintf( __( 'This course closed on [lifterlms_course_info id=\"%d\" key=\"end_date\"].', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'desc'             => sprintf( __( 'This message will be displayed to non-enrolled visitors once the Course End Date has passed. You may use shortcodes like [lifterlms_course_info id=\"%d\" key=\"end_date\"] in this message.', 'lifterlms' ), $this->post->ID ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'course_closed_message',\n\t\t\t\t\t\t'label'            => __( 'Course Closed Message', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t\t'sanitize'         => 'shortcode',\n\t\t\t\t\t),\n\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'type'          => 'checkbox',\n\t\t\t\t\t\t'label'         => __( 'Enable Prerequisite', 'lifterlms' ),\n\t\t\t\t\t\t'desc'          => __( 'Enable to choose a prerequisite course or course track', 'lifterlms' ),\n\t\t\t\t\t\t'id'            => $this->prefix . 'has_prerequisite',\n\t\t\t\t\t\t'class'         => '',\n\t\t\t\t\t\t'value'         => 'yes',\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'has_prerequisite',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'data_attributes'  => array(\n\t\t\t\t\t\t\t'post-type'   => 'course',\n\t\t\t\t\t\t\t'allow-clear' => true,\n\t\t\t\t\t\t\t'placeholder' => __( 'Select a course', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'class'            => 'llms-select2-post',\n\t\t\t\t\t\t'desc'             => __( 'Select a prerequisite course. Students must have completed the selected course before they can view or complete content in this course.', 'lifterlms' ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'prerequisite',\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'label'            => __( 'Choose Prerequisite Course', 'lifterlms' ),\n\t\t\t\t\t\t'value'            => llms_make_select2_post_array( array( $course->get( 'prerequisite' ) ) ),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'llms-select2',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'has_prerequisite',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'desc'             => __( 'Select the prerequisite course track. Students must have completed the select track before they can view or complete content in this course.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'id'               => $this->prefix . 'prerequisite_track',\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'label'            => __( 'Choose Prerequisite Course Track', 'lifterlms' ),\n\t\t\t\t\t\t'value'            => $course_tracks,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'          => 'checkbox',\n\t\t\t\t\t\t'label'         => __( 'Enable Lesson Drip', 'lifterlms' ),\n\t\t\t\t\t\t'desc'          => __( 'Set global drip restrictions so lesson content becomes available at an interval you define for the course.', 'lifterlms' ),\n\t\t\t\t\t\t'id'            => $this->prefix . 'lesson_drip',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'value'         => 'yes',\n\t\t\t\t\t\t'class'         => '',\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'llms-select2',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'lesson_drip',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'is_controller'    => true,\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'id'               => $this->prefix . 'drip_method',\n\t\t\t\t\t\t'label'            => __( 'Drip Method', 'lifterlms' ),\n\t\t\t\t\t\t'value'            => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'start',\n\t\t\t\t\t\t\t\t'title' => __( 'After course start or enrollment', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'lesson_drip',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'id'               => $this->prefix . 'ignore_lessons',\n\t\t\t\t\t\t'label'            => __( 'Number of lessons to make immediately available on course start', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'number',\n\t\t\t\t\t\t'step'             => 1,\n\t\t\t\t\t\t'min'              => 1,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'lesson_drip',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'id'               => $this->prefix . 'days_before_available',\n\t\t\t\t\t\t'label'            => __( 'Delay (in days) ', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'number',\n\t\t\t\t\t\t'step'             => 1,\n\t\t\t\t\t\t'min'              => 1,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'type'          => 'checkbox',\n\t\t\t\t\t\t'label'         => __( 'Enable Course Capacity', 'lifterlms' ),\n\t\t\t\t\t\t'desc'          => __( 'Limit the number of users that can enroll in this course.', 'lifterlms' ),\n\t\t\t\t\t\t'id'            => $this->prefix . 'enable_capacity',\n\t\t\t\t\t\t'class'         => '',\n\t\t\t\t\t\t'value'         => 'yes',\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'enable_capacity',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'id'               => $this->prefix . 'capacity',\n\t\t\t\t\t\t'type'             => 'number',\n\t\t\t\t\t\t'label'            => __( 'Course Capacity', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'enable_capacity',\n\t\t\t\t\t\t'controller_value' => 'yes',\n\t\t\t\t\t\t'default'          => __( 'Enrollment has closed because the maximum number of allowed students has been reached.', 'lifterlms' ),\n\t\t\t\t\t\t'desc'             => __( 'This message will be displayed to non-enrolled visitors once the Course Capacity has been reached. ', 'lifterlms' ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'capacity_message',\n\t\t\t\t\t\t'label'            => __( 'Capacity Reached Message', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Instructors', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'button'  => array(\n\t\t\t\t\t\t\t'text' => __( 'Add Instructor', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'handler' => 'instructors_mb_store',\n\t\t\t\t\t\t'header'  => array(\n\t\t\t\t\t\t\t'default' => __( 'New Instructor', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'id'      => $this->prefix . 'instructors_data',\n\t\t\t\t\t\t'label'   => '',\n\t\t\t\t\t\t'type'    => 'repeater',\n\t\t\t\t\t\t'fields'  => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'allow_null'      => false,\n\t\t\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t\t\t'placeholder' => esc_attr__( 'Select an Instructor', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\t'roles'       => 'administrator,lms_manager,instructor,instructors_assistant',\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t'class'           => 'llms-select2-student',\n\t\t\t\t\t\t\t\t'group'           => 'd-2of3',\n\t\t\t\t\t\t\t\t'id'              => $this->prefix . 'id',\n\t\t\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t\t\t'label'           => __( 'Instructor', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'group'   => 'd-1of6',\n\t\t\t\t\t\t\t\t'class'   => 'input-full',\n\t\t\t\t\t\t\t\t'default' => $instructor_defaults['label'],\n\t\t\t\t\t\t\t\t'id'      => $this->prefix . 'label',\n\t\t\t\t\t\t\t\t'type'    => 'text',\n\t\t\t\t\t\t\t\t'label'   => __( 'Label', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'allow_null' => false,\n\t\t\t\t\t\t\t\t'class'      => 'llms-select2',\n\t\t\t\t\t\t\t\t'default'    => $instructor_defaults['visibility'],\n\t\t\t\t\t\t\t\t'group'      => 'd-1of6',\n\t\t\t\t\t\t\t\t'id'         => $this->prefix . 'visibility',\n\t\t\t\t\t\t\t\t'type'       => 'select',\n\t\t\t\t\t\t\t\t'label'      => __( 'Visibility', 'lifterlms' ),\n\t\t\t\t\t\t\t\t'value'      => array(\n\t\t\t\t\t\t\t\t\t'visible' => esc_html__( 'Visible', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\t'hidden'  => esc_html__( 'Hidden', 'lifterlms' ),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$current_screen = get_current_screen();\n\t\t$is_gutenberg   = is_object( $current_screen ) && method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor();\n\n\t\t/**\n\t\t * Remove length and difficulty fields if\n\t\t * - the course is a new post and the editor is Gutenberg.\n\t\t * or\n\t\t * - the course is migrated to blocks used in the Gutenberg editor.\n\t\t */\n\t\tif (\n\t\t\t( $is_gutenberg && 'auto-draft' === get_post_status( $this->post->ID ) ) ||\n\t\t\tfunction_exists( 'llms_blocks_is_post_migrated' ) && llms_blocks_is_post_migrated( $this->post->ID )\n\t\t) {\n\t\t\tunset( $fields[1]['fields'][1] ); // difficulty.\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Update course difficulty on save\n\t *\n\t * @since 3.0.0\n\t * @since 3.26.3 Only save when using the classic editor.\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id  WP Post ID of the course\n\t * @return void\n\t */\n\tprotected function save_before( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! function_exists( 'register_block_type' ) || ! llms_blocks_is_post_migrated( $this->post->ID ) ) {\n\n\t\t\twp_set_object_terms( $post_id, llms_filter_input_sanitize_string( INPUT_POST, '_llms_post_course_difficulty' ), 'course_difficulty', false );\n\t\t\tunset( $_POST['_llms_post_course_difficulty'] ); // Don't save this to the postmeta table.\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.course.short.description.php",
    "content": "<?php\n/**\n * Course Short Description meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since unknown\n * @version unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Course Short Description meta box class\n *\n * Overrides WP short description.\n *\n * @since unknown\n */\nclass LLMS_Meta_Box_Course_Short_Description {\n\n\t/**\n\t * outputs tinymce\n\t *\n\t * @return mixed (wp_editor)\n\t * @param string $post\n\t */\n\tpublic static function output( $post ) {\n\t\t$settings = array(\n\t\t\t'textarea_name' => 'excerpt',\n\t\t\t'quicktags'     => array(\n\t\t\t\t'buttons' => 'em,strong,link',\n\t\t\t),\n\t\t\t'tinymce'       => array(\n\t\t\t\t'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator',\n\t\t\t\t'theme_advanced_buttons2' => '',\n\t\t\t),\n\t\t\t'editor_css'    => '<style>#wp-excerpt-editor-container .wp-editor-area{height:175px; width:100%;}</style>',\n\t\t);\n\n\t\twp_editor( htmlspecialchars_decode( $post->post_excerpt ), 'excerpt', apply_filters( 'lifterlms_course_short_description_editor_settings', $settings ) );\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php",
    "content": "<?php\n/**\n * Certificate Options meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 3.37.12\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta box Certificate Options class.\n *\n * Displays email settings meta box. Only displays on email post.\n *\n * @since 1.0.0\n * @since 3.1.0 Unknown.\n * @since 3.1.4 Unknown.\n * @since 3.37.12 Allow some fields to store values with quotes.\n */\nclass LLMS_Meta_Box_Email_Settings extends LLMS_Admin_Metabox {\n\n\n\t/**\n\t * Configure the metabox settings.\n\t *\n\t * @since 3.0.0\n\t * @since 3.1.4 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-email';\n\t\t$this->title    = __( 'Email Settings', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_email',\n\t\t);\n\t\t$this->priority = 'high';\n\n\t}\n\n\t/**\n\t * Builds array of metabox options.\n\t *\n\t * Array is called in output method to display options.\n\t * Appropriate fields are generated based on type.\n\t *\n\t * @since 3.0.0\n\t * @since 3.1.0 Unknown.\n\t * @since 3.37.12 Allow some fields to store values with quotes.\n\t *\n\t * @return array Array of metabox fields.\n\t */\n\tpublic function get_fields() {\n\n\t\t$email_merge = array(\n\t\t\t'{student_email}' => __( 'Student Email', 'lifterlms' ),\n\t\t\t'{admin_email}'   => __( 'Admin Email', 'lifterlms' ),\n\t\t);\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'title'  => 'Settings',\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t\t'label'      => __( 'Email Subject', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'This will be used for the subject line of your email.', 'lifterlms' ) . llms_merge_code_button( '#' . $this->prefix . 'email_subject', false ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'email_subject',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => 'top',\n\t\t\t\t\t\t'sanitize'   => 'no_encode_quotes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t\t'label'      => __( 'Email Heading', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'This is the heading for your email. It will display above the content.', 'lifterlms' ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'email_heading',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'group'      => 'bottom',\n\t\t\t\t\t\t'sanitize'   => 'no_encode_quotes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t\t'label'      => __( 'Email To:', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'Separate multiple address with a comma.', 'lifterlms' ) . llms_merge_code_button( '#' . $this->prefix . 'email_to', false, $email_merge ),\n\t\t\t\t\t\t'default'    => '{student_email}',\n\t\t\t\t\t\t'id'         => $this->prefix . 'email_to',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'required'   => true,\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t\t'label'      => __( 'Email CC:', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'Separate multiple address with a comma.', 'lifterlms' ) . llms_merge_code_button( '#' . $this->prefix . 'email_cc', false, $email_merge ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'email_cc',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t\t'label'      => __( 'Email BCC:', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => __( 'Separate multiple address with a comma.', 'lifterlms' ) . llms_merge_code_button( '#' . $this->prefix . 'email_bcc', false, $email_merge ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'email_bcc',\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'value'      => '',\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php",
    "content": "<?php\n/**\n * Engagements meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Engagements meta box class\n *\n * @since 1.0.0\n * @since 3.35.0 Verify nonce and access $_POST data via `llms_filter_input()`.\n */\nclass LLMS_Meta_Box_Engagement extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @return   void\n\t * @since    3.1.0\n\t * @version  3.1.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-engagement';\n\t\t$this->title    = __( 'Engagement Options', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_engagement',\n\t\t);\n\t\t$this->priority = 'high';\n\t}\n\n\t/**\n\t * Return an empty array because the metabox fields here are completely custom\n\t *\n\t * @return   array\n\t * @since    1.0.0\n\t * @version  3.11.0\n\t */\n\tpublic function get_fields() {\n\n\t\t$triggers = llms_get_engagement_triggers();\n\n\t\t$types = llms_get_engagement_types();\n\n\t\t$fields = array();\n\n\t\t$fields[] = array(\n\t\t\t'allow_null'    => false,\n\t\t\t'class'         => 'llms-select2',\n\t\t\t'desc'          => __( 'This engagement will be triggered when a student completes the selected action', 'lifterlms' ),\n\t\t\t'id'            => $this->prefix . 'trigger_type',\n\t\t\t'is_controller' => true,\n\t\t\t'type'          => 'select',\n\t\t\t'label'         => __( 'Triggering Event', 'lifterlms' ),\n\t\t\t'value'         => $triggers,\n\t\t);\n\n\t\t$trigger_post_fields = array(\n\n\t\t\t'course'           => array(\n\t\t\t\t'controller_value' => array(\n\t\t\t\t\t'course_completed',\n\t\t\t\t\t'course_enrollment',\n\t\t\t\t\t'course_purchased',\n\t\t\t\t),\n\t\t\t\t'id'               => '_faux_engagement_trigger_post_course',\n\t\t\t\t'label'            => __( 'Select a Course', 'lifterlms' ),\n\t\t\t\t'placeholder'      => __( 'Any Course', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'lesson'           => array(\n\t\t\t\t'controller_value' => array( 'lesson_completed' ),\n\t\t\t\t'id'               => '_faux_engagement_trigger_post_lesson',\n\t\t\t\t'label'            => __( 'Select a Lesson', 'lifterlms' ),\n\t\t\t\t'placeholder'      => __( 'Any Lesson', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'llms_access_plan' => array(\n\t\t\t\t'controller_value' => array(\n\t\t\t\t\t'access_plan_purchased',\n\t\t\t\t),\n\t\t\t\t'id'               => '_faux_engagement_trigger_post_access_plan',\n\t\t\t\t'label'            => __( 'Select an Access Plan', 'lifterlms' ),\n\t\t\t\t'placeholder'      => __( 'Any Access Plan', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'llms_membership'  => array(\n\t\t\t\t'controller_value' => array(\n\t\t\t\t\t'membership_enrollment',\n\t\t\t\t\t'membership_purchased',\n\t\t\t\t),\n\t\t\t\t'id'               => '_faux_engagement_trigger_post_membership',\n\t\t\t\t'label'            => __( 'Select a Membership', 'lifterlms' ),\n\t\t\t\t'placeholder'      => __( 'Any Membership', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'llms_quiz'        => array(\n\t\t\t\t'controller_value' => array(\n\t\t\t\t\t'quiz_completed',\n\t\t\t\t\t'quiz_passed',\n\t\t\t\t\t'quiz_failed',\n\t\t\t\t),\n\t\t\t\t'id'               => '_faux_engagement_trigger_post_quiz',\n\t\t\t\t'label'            => __( 'Select a Quiz', 'lifterlms' ),\n\t\t\t\t'placeholder'      => __( 'Any Quiz', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'section'          => array(\n\t\t\t\t'controller_value' => array( 'section_completed' ),\n\t\t\t\t'id'               => '_faux_engagement_trigger_post_section',\n\t\t\t\t'label'            => __( 'Select a Section', 'lifterlms' ),\n\t\t\t\t'placeholder'      => __( 'Any Section', 'lifterlms' ),\n\t\t\t),\n\n\t\t);\n\n\t\tforeach ( $trigger_post_fields as $post_type => $data ) {\n\n\t\t\t$data['controller_value'] = apply_filters( 'llms_engagement_controller_values_' . $post_type, $data['controller_value'] );\n\n\t\t\t$trigger_post_val = get_post_meta( $this->post->ID, $this->prefix . 'engagement_trigger_post', true );\n\t\t\tif ( 'any' === $trigger_post_val || empty( $trigger_post_val ) ) {\n\t\t\t\t$val = array();\n\t\t\t} elseif ( in_array( get_post_meta( $this->post->ID, $this->prefix . 'trigger_type', true ), $data['controller_value'] ) ) {\n\t\t\t\t$val = llms_make_select2_post_array( array( $trigger_post_val ) );\n\t\t\t} else {\n\t\t\t\t$val = array();\n\t\t\t}\n\n\t\t\t$placeholder = isset( $data['placeholder'] ) ? $data['placeholder'] : $data['label'];\n\n\t\t\t$fields[] = array(\n\t\t\t\t'allow_null'       => false,\n\t\t\t\t'class'            => 'llms-select2-post',\n\t\t\t\t'controller'       => '#' . $this->prefix . 'trigger_type',\n\t\t\t\t'controller_value' => implode( ',', $data['controller_value'] ),\n\t\t\t\t'data_attributes'  => array(\n\t\t\t\t\t'allow_clear' => true,\n\t\t\t\t\t'placeholder' => $placeholder,\n\t\t\t\t\t'post-type'   => $post_type,\n\t\t\t\t),\n\t\t\t\t'desc'             => __( 'Leave blank to apply to all.', 'lifterlms' ),\n\t\t\t\t'id'               => $data['id'],\n\t\t\t\t'label'            => $data['label'],\n\t\t\t\t'type'             => 'select',\n\t\t\t\t'value'            => $val,\n\t\t\t);\n\n\t\t}\n\n\t\t$track_options = array();\n\t\t$tracks        = get_terms(\n\t\t\t'course_track',\n\t\t\tarray(\n\t\t\t\t'hide_empty' => '0',\n\t\t\t)\n\t\t);\n\t\tforeach ( $tracks as $track ) {\n\t\t\t$track_options[] = array(\n\t\t\t\t'key'   => $track->term_id,\n\t\t\t\t'title' => $track->name . ' (ID# ' . $track->term_id . ')',\n\t\t\t);\n\t\t}\n\n\t\t$track_selected = get_post_meta( $this->post->ID, $this->prefix . 'engagement_trigger_post', true );\n\t\tif ( 'any' === $track_selected ) {\n\t\t\t$track_selected = '';\n\t\t}\n\n\t\t$fields[] = array(\n\t\t\t'allow_null'       => true,\n\t\t\t'class'            => 'llms-select2',\n\t\t\t'controller'       => '#' . $this->prefix . 'trigger_type',\n\t\t\t'controller_value' => implode( ',', apply_filters( 'llms_engagement_controller_values_track', array( 'course_track_completed' ) ) ),\n\t\t\t'data_attributes'  => array(\n\t\t\t\t'allow_clear' => true,\n\t\t\t\t'placeholder' => __( 'Any Course Track', 'lifterlms' ),\n\t\t\t),\n\t\t\t'id'               => '_faux_engagement_trigger_post_track',\n\t\t\t'label'            => __( 'Select a Course Track', 'lifterlms' ),\n\t\t\t'type'             => 'select',\n\t\t\t'selected'         => $track_selected,\n\t\t\t'value'            => $track_options,\n\t\t);\n\n\t\t$fields[] = array(\n\t\t\t'allow_null'    => false,\n\t\t\t'class'         => 'llms-select2',\n\t\t\t'desc'          => __( 'Determines the type of engagement', 'lifterlms' ),\n\t\t\t'id'            => $this->prefix . 'engagement_type',\n\t\t\t'is_controller' => true,\n\t\t\t'label'         => __( 'Engagement Type', 'lifterlms' ),\n\t\t\t'type'          => 'select',\n\t\t\t'value'         => $types,\n\t\t);\n\n\t\t$type    = get_post_meta( $this->post->ID, $this->prefix . 'engagement_type', true );\n\t\t$default = ( ! $type ) ? 'llms_achievement' : 'llms_' . $type;\n\n\t\t$fields[] = array(\n\t\t\t'allow_null'      => false,\n\t\t\t'class'           => 'llms-select2-post',\n\t\t\t'data_attributes' => array(\n\t\t\t\t'allow_clear' => true,\n\t\t\t\t'placeholder' => __( 'Select an Engagement', 'lifterlms' ),\n\t\t\t\t'post-type'   => $default,\n\t\t\t\t'edit-button' => true,\n\t\t\t),\n\t\t\t'id'              => $this->prefix . 'engagement',\n\t\t\t'label'           => __( 'Select an Engagement', 'lifterlms' ),\n\t\t\t'type'            => 'select',\n\t\t\t'value'           => llms_make_select2_post_array( array( get_post_meta( $this->post->ID, $this->prefix . 'engagement', true ) ) ),\n\t\t);\n\n\t\t$fields[] = array(\n\t\t\t'class'   => 'input-full',\n\t\t\t'default' => 0,\n\t\t\t'desc'    => __( 'Enter the number of days to wait before triggering this engagement. Enter 0 or leave blank to trigger immediately.', 'lifterlms' ),\n\t\t\t'id'      => $this->prefix . 'engagement_delay',\n\t\t\t'label'   => __( 'Engagement Delay', 'lifterlms' ),\n\t\t\t'min'     => 0,\n\t\t\t'type'    => 'number',\n\t\t);\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Engagement Settings', 'lifterlms' ),\n\t\t\t\t'fields' => $fields,\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Custom save method.\n\t *\n\t * Ensures that the faux fields are not saved to the postmeta table.\n\t *\n\t * @since 3.1.0\n\t * @since 3.11.0 Unknown.\n\t * @since 3.35.0 Verify nonce and access $_POST data via `llms_filter_input()`.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id WP Post ID of the engagement.\n\t * @return void\n\t */\n\tpublic function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Get all defined fields.\n\t\t$fields = $this->get_fields();\n\n\t\tif ( ! is_array( $fields ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Loop through the fields.\n\t\tforeach ( $fields as $group => $data ) {\n\n\t\t\t// Find the fields in each tab.\n\t\t\tif ( isset( $data['fields'] ) && is_array( $data['fields'] ) ) {\n\n\t\t\t\t// Loop through the fields.\n\t\t\t\tforeach ( $data['fields'] as $field ) {\n\n\t\t\t\t\t// Don't save things that don't have an ID.\n\t\t\t\t\tif ( isset( $field['id'] ) ) {\n\n\t\t\t\t\t\t// Skip our faux fields.\n\t\t\t\t\t\tif ( 0 === strpos( $field['id'], '_faux_engagement_trigger_post_' ) ) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Get the posted value.\n\t\t\t\t\t\tif ( isset( $_POST[ $field['id'] ] ) ) {\n\n\t\t\t\t\t\t\t$val = llms_filter_input_sanitize_string( INPUT_POST, $field['id'] );\n\n\t\t\t\t\t\t} elseif ( ! isset( $_POST[ $field['id'] ] ) ) {\n\n\t\t\t\t\t\t\t$val = '';\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Update the value if we have one.\n\t\t\t\t\t\tif ( isset( $val ) ) {\n\n\t\t\t\t\t\t\tupdate_post_meta( $post_id, $field['id'], $val );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tunset( $val );\n\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Locate and store the trigger post id.\n\t\t$type = llms_filter_input( INPUT_POST, $this->prefix . 'trigger_type' );\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'access_plan_purchased':\n\t\t\t\t$var = 'access_plan';\n\t\t\t\tbreak;\n\n\t\t\tcase 'course_completed':\n\t\t\tcase 'course_purchased':\n\t\t\tcase 'course_enrollment':\n\t\t\t\t$var = 'course';\n\t\t\t\tbreak;\n\n\t\t\tcase 'lesson_completed':\n\t\t\t\t$var = 'lesson';\n\t\t\t\tbreak;\n\n\t\t\tcase 'membership_purchased':\n\t\t\tcase 'membership_enrollment':\n\t\t\t\t$var = 'membership';\n\t\t\t\tbreak;\n\n\t\t\tcase 'quiz_completed':\n\t\t\tcase 'quiz_passed':\n\t\t\tcase 'quiz_failed':\n\t\t\t\t$var = 'quiz';\n\t\t\t\tbreak;\n\n\t\t\tcase 'section_completed':\n\t\t\t\t$var = 'section';\n\t\t\t\tbreak;\n\n\t\t\tcase 'course_track_completed':\n\t\t\t\t$var = 'track';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$var = false;\n\n\t\t}\n\n\t\tif ( $var ) {\n\n\t\t\t$val = llms_filter_input_sanitize_string( INPUT_POST, '_faux_engagement_trigger_post_' . $var );\n\n\t\t\t// An empty trigger post means \"any\" — store explicitly so the intent is clear.\n\t\t\tif ( empty( $val ) ) {\n\t\t\t\t$val = 'any';\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t$val = '';\n\n\t\t}\n\n\t\tupdate_post_meta( $post_id, $this->prefix . 'engagement_trigger_post', $val );\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php",
    "content": "<?php\n/**\n * Lesson Settings meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 7.1.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Lesson Settings meta box class\n *\n * @since 1.0.0\n * @since 3.30.3 Fixed spelling errors.\n * @since 3.36.2 'start' drip method made avialble only if the parent course has a start date set.\n */\nclass LLMS_Meta_Box_Lesson extends LLMS_Admin_Metabox {\n\n\t/**\n\t * This function allows extending classes to configure required class properties\n\t * $this->id, $this->title, and $this->screens should be configured in this function\n\t *\n\t * @return  void\n\t * @since   3.0.0\n\t * @version 3.0.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-lesson';\n\t\t$this->title    = __( 'Lesson Settings', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'lesson',\n\t\t);\n\t\t$this->priority = 'high';\n\t}\n\n\t/**\n\t * This function is where extending classes can configure all the fields within the metabox.\n\t *\n\t * The function must return an array which can be consumed by the \"output\" function.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.3 Fixed spelling errors.\n\t * @since 3.36.2 'start' drip method made available only if the parent course has a start date set.\n\t * @since 7.1.3 Replace outdated URLs to WordPress' documentation about the list of sites you can embed from.\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\t$lesson = llms_get_post( $this->post );\n\n\t\t$methods = array(\n\t\t\t'date'       => __( 'On a specific date', 'lifterlms' ),\n\t\t\t'enrollment' => __( 'After course enrollment', 'lifterlms' ),\n\t\t\t'start'      => __( 'After course start date', 'lifterlms' ),\n\t\t);\n\n\t\t$section = $lesson->get_section();\n\n\t\t// Tf the lesson isn't first, add previous completion method.\n\t\tif ( 1 !== $lesson->get( 'order' ) || ( $section && 1 !== $section->get( 'order' ) ) ) {\n\t\t\t$methods['prerequisite'] = __( 'After prerequisite completion', 'lifterlms' );\n\t\t}\n\n\t\t// If the parent course has no start date set, unset the 'start' drip method.\n\t\t$course = $lesson->get_course();\n\t\tif ( ! $course || ! $course->get_date( 'start_date' ) ) {\n\t\t\tunset( $methods['start'] );\n\t\t}\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'General', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'desc'       => sprintf( __( 'Paste the url for a Wistia, Vimeo or Youtube video or a hosted video file. For a full list of supported providers see %s.', 'lifterlms' ), '<a href=\"https://wordpress.org/documentation/article/embeds/#list-of-sites-you-can-embed-from\" target=\"_blank\">WordPress oEmbeds</a>' ),\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'id'         => $this->prefix . 'video_embed',\n\t\t\t\t\t\t'label'      => __( 'Video Embed Url', 'lifterlms' ),\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'      => 'code input-full',\n\t\t\t\t\t\t'desc'       => sprintf( __( 'Paste the url for a SoundCloud or Spotify song or a hosted audio file. For a full list of supported providers see %s.', 'lifterlms' ), '<a href=\"https://wordpress.org/documentation/article/embeds/#list-of-sites-you-can-embed-from\" target=\"_blank\">WordPress oEmbeds</a>' ),\n\t\t\t\t\t\t'desc_class' => 'd-all',\n\t\t\t\t\t\t'id'         => $this->prefix . 'audio_embed',\n\t\t\t\t\t\t'type'       => 'text',\n\t\t\t\t\t\t'label'      => __( 'Audio Embed Url', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'         => '',\n\t\t\t\t\t\t'desc'          => __( 'Checking this box will allow guests to view the content of this lesson without registering or signing up for the course.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'            => $this->prefix . 'free_lesson',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'label'         => __( 'Free Lesson', 'lifterlms' ),\n\t\t\t\t\t\t'type'          => 'checkbox',\n\t\t\t\t\t\t'value'         => 'yes',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Prerequisites', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'      => '',\n\t\t\t\t\t\t'controls'   => '#' . $this->prefix . 'prerequisite',\n\t\t\t\t\t\t'desc'       => __( 'Enable to choose a prerequisite Lesson', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'         => $this->prefix . 'has_prerequisite',\n\t\t\t\t\t\t'label'      => __( 'Enable Prerequisite', 'lifterlms' ),\n\t\t\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t\t\t'value'      => 'yes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'           => 'llms-select2-post',\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'allow-clear' => true,\n\t\t\t\t\t\t\t'placeholder' => __( 'Select a Prerequisite Lesson', 'lifterlms' ),\n\t\t\t\t\t\t\t'post-type'   => 'lesson',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'desc'            => __( 'Select the prerequisite lesson', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'      => 'd-all',\n\t\t\t\t\t\t'id'              => $this->prefix . 'prerequisite',\n\t\t\t\t\t\t'label'           => __( 'Choose Prerequisite', 'lifterlms' ),\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'value'           => llms_make_select2_post_array( array( get_post_meta( $this->post->ID, $this->prefix . 'prerequisite', true ) ) ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\t'drip' => array(\n\t\t\t\t'title'  => __( 'Drip Settings', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t\t\t'id'    => $this->prefix . 'drip_course_settings_info',\n\t\t\t\t\t\t'value' => $this->get_drip_course_settings_info_html( $course ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Quiz', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'free_lesson',\n\t\t\t\t\t\t'controller_value' => 'false',\n\t\t\t\t\t\t'desc'             => __( 'Checking this box will require students to get a passing score on the above quiz to complete the lesson.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'       => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'               => $this->prefix . 'require_passing_grade',\n\t\t\t\t\t\t'label'            => __( 'Require Passing Grade', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'checkbox',\n\t\t\t\t\t\t'value'            => 'yes',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\tif ( ! $course || 'yes' !== $course->get( 'lesson_drip' ) || ! $course->get( 'drip_method' ) ) {\n\t\t\t$fields['drip']['fields'][] = array(\n\t\t\t\t'class'         => 'llms-select2',\n\t\t\t\t'desc_class'    => 'd-all',\n\t\t\t\t'id'            => $this->prefix . 'drip_method',\n\t\t\t\t'is_controller' => true,\n\t\t\t\t'label'         => __( 'Method', 'lifterlms' ),\n\t\t\t\t'type'          => 'select',\n\t\t\t\t'value'         => $methods,\n\t\t\t);\n\t\t\t$fields['drip']['fields'][] = array(\n\t\t\t\t'controller'       => '#' . $this->prefix . 'drip_method',\n\t\t\t\t'controller_value' => 'lesson,enrollment,start,prerequisite',\n\t\t\t\t'class'            => 'input-full',\n\t\t\t\t'id'               => $this->prefix . 'days_before_available',\n\t\t\t\t'label'            => __( 'Delay (in days) ', 'lifterlms' ),\n\t\t\t\t'type'             => 'number',\n\t\t\t\t'step'             => 1,\n\t\t\t\t'min'              => 0,\n\t\t\t);\n\t\t\t$fields['drip']['fields'][] = array(\n\t\t\t\t'controller'       => '#' . $this->prefix . 'drip_method',\n\t\t\t\t'controller_value' => 'date',\n\t\t\t\t'class'            => 'llms-datepicker',\n\t\t\t\t'date_format'      => 'yy-mm-dd',\n\t\t\t\t'id'               => $this->prefix . 'date_available',\n\t\t\t\t'label'            => __( 'Date Available', 'lifterlms' ),\n\t\t\t\t'type'             => 'date',\n\t\t\t);\n\t\t\t$fields['drip']['fields'][] = array(\n\t\t\t\t'controller'       => '#' . $this->prefix . 'drip_method',\n\t\t\t\t'controller_value' => 'date',\n\t\t\t\t'class'            => '',\n\t\t\t\t'desc'             => __( 'Optionally enter a time when the lesson should become available. If no time supplied, lesson will be available at 12:00 AM. Format must be HH:MM AM', 'lifterlms' ),\n\t\t\t\t'id'               => $this->prefix . 'time_available',\n\t\t\t\t'label'            => __( 'Time Available', 'lifterlms' ),\n\t\t\t\t'type'             => 'text',\n\t\t\t);\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Helpful messaging depending on whether the course for this lesson has drip settings enabled or not.\n\t *\n\t * @since 7.6.0\n\t *\n\t * @param LLMS_Course $course Course object.\n\t * @return string\n\t */\n\tpublic function get_drip_course_settings_info_html( $course ) {\n\t\tif ( ! $course ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$output = 'yes' === $course->get( 'lesson_drip' ) && $course->get( 'drip_method' ) ?\n\t\t\tesc_html__( 'Drip settings are currently set at the course level, under the Restrictions settings tab. If you would like to set individual drip settings for each lesson, you must disable the course level drip settings first.', 'lifterlms' )\n\t\t:\n\t\t\tesc_html__( 'Drip settings can be set at the course level to release course content at a specified interval, in the Restrictions settings tab.', 'lifterlms' );\n\n\t\t$output .= ' <a href=\"' . esc_url( admin_url( 'post.php?post=' . $course->get( 'id' ) . '&action=edit#lifterlms-course-options' ) ) . '\">' . esc_html__( 'Edit Course', 'lifterlms' ) . '</a>';\n\n\t\treturn $output;\n\t}\n}\n\nnew LLMS_Meta_Box_Lesson();\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php",
    "content": "<?php\n/**\n * Membership Settings meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Membership Settings meta box class\n *\n * @since 1.0.0\n * @since 3.30.3 Fixed spelling errors; removed duplicate array keys.\n * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n * @since 3.36.0 Allow some fields to store values with quotes.\n * @since 3.36.3 In the `save() method Added logic to correctly sanitize fields of type\n *              'multi' (array) and 'shortcode' (preventing quotes encode).\n *               Also align the method return type to the parent `save()` method.\n */\nclass LLMS_Meta_Box_Membership extends LLMS_Admin_Metabox {\n\n\t/**\n\t * This function allows extending classes to configure required class properties\n\t * $this->id, $this->title, and $this->screens should be configured in this function.\n\t *\n\t * @return void\n\t * @since  3.0.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-membership';\n\t\t$this->title    = __( 'Membership Settings', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_membership',\n\t\t);\n\t\t$this->priority = 'high';\n\t}\n\n\t/**\n\t * Get array of data to pass to the auto enrollment courses table.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.0 Removed sorting by title.\n\t * @since 3.30.3 Fixed spelling errors.\n\t *\n\t * @param obj $membership instance of LLMS_Membership for the current post.\n\t * @return array\n\t */\n\tprivate function get_content_table( $membership ) {\n\n\t\t$data   = array();\n\t\t$data[] = array(\n\t\t\t'',\n\t\t\t'<br>' . __( 'No automatic enrollment courses found. Add a course below.', 'lifterlms' ) . '<br><br>',\n\t\t\t'',\n\t\t);\n\n\t\tforeach ( $membership->get_auto_enroll_courses() as $course_id ) {\n\n\t\t\t$course = new LLMS_Course( $course_id );\n\n\t\t\t$title = $course->get( 'title' );\n\n\t\t\t$data[] = array(\n\n\t\t\t\t'<span class=\"dashicons dashicons-menu llms-drag-handle ui-sortable-handle\"></span>',\n\t\t\t\t'<a href=\"' . get_edit_post_link( $course->get( 'id' ) ) . '\">' . $title . ' (ID#' . $course_id . ')</a>',\n\t\t\t\t'<a class=\"llms-button-danger small\" data-id=\"' . $course_id . '\" href=\"#llms-course-remove\" style=\"float:right;\">' . __( 'Remove course', 'lifterlms' ) . '</a>\n\t\t\t\t <a class=\"llms-button-secondary small\" data-id=\"' . $course_id . '\" href=\"#llms-course-bulk-enroll\" style=\"float:right;margin-right:5px;\">' . __( 'Enroll All Members', 'lifterlms' ) . '</a>',\n\n\t\t\t);\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_membership_get_content_table_data', $data, $membership );\n\t}\n\n\t/**\n\t * This function is where extending classes can configure all the fields within the metabox.\n\t * The function must return an array which can be consumed by the \"output\" function.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.0 Removed empty field settings. Modified settings to accommodate sortable auto-enrollment table.\n\t * @since 3.30.3 Removed duplicate array keys.\n\t * @since 3.36.0 Allow some fields to store values with quotes.\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\tglobal $post;\n\n\t\t$membership = new LLMS_Membership( $this->post );\n\n\t\t$redirect_options = array();\n\t\t$redirect_page_id = $membership->get( 'redirect_page_id' );\n\t\tif ( $redirect_page_id ) {\n\t\t\t$redirect_options[] = array(\n\t\t\t\t'key'   => $redirect_page_id,\n\t\t\t\t'title' => get_the_title( $redirect_page_id ) . '(ID#' . $redirect_page_id . ')',\n\t\t\t);\n\t\t}\n\n\t\t$sales_page_content_type = 'none';\n\t\tif ( $post && 'auto-draft' !== $post->post_status && $post->post_excerpt ) {\n\t\t\t$sales_page_content_type = 'content';\n\t\t}\n\n\t\t$instructor_defaults = llms_get_instructors_defaults();\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Sales Page', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'allow_null'    => false,\n\t\t\t\t\t\t'class'         => 'llms-select2',\n\t\t\t\t\t\t'desc'          => __( 'Customize the content displayed to visitors and students who are not enrolled in the membership.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class'    => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'default'       => $sales_page_content_type,\n\t\t\t\t\t\t'id'            => $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'label'         => __( 'Sales Page Content', 'lifterlms' ),\n\t\t\t\t\t\t'type'          => 'select',\n\t\t\t\t\t\t'value'         => llms_get_sales_page_types(),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'controller_value' => 'content',\n\t\t\t\t\t\t'desc'             => __( 'This content will only be shown to visitors who are not enrolled in this membership.', 'lifterlms' ),\n\t\t\t\t\t\t'id'               => '',\n\t\t\t\t\t\t'label'            => __( 'Sales Page Custom Content', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'post-excerpt',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'controller_value' => 'page',\n\t\t\t\t\t\t'data_attributes'  => array(\n\t\t\t\t\t\t\t'post-type'   => 'page',\n\t\t\t\t\t\t\t'placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'class'            => 'llms-select2-post',\n\t\t\t\t\t\t'id'               => $this->prefix . 'sales_page_content_page_id',\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'label'            => __( 'Select a Page', 'lifterlms' ),\n\t\t\t\t\t\t'value'            => $membership->get( 'sales_page_content_page_id' ) ? llms_make_select2_post_array( array( $membership->get( 'sales_page_content_page_id' ) ) ) : array(),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'sales_page_content_type',\n\t\t\t\t\t\t'controller_value' => 'url',\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t\t'label'            => __( 'Sales Page Redirect URL', 'lifterlms' ),\n\t\t\t\t\t\t'id'               => $this->prefix . 'sales_page_content_url',\n\t\t\t\t\t\t'class'            => 'input-full',\n\t\t\t\t\t\t'value'            => '',\n\t\t\t\t\t\t'desc_class'       => 'd-all',\n\t\t\t\t\t\t'group'            => 'top',\n\t\t\t\t\t),\n\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'General', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'text',\n\t\t\t\t\t\t'label' => __( 'Featured Video', 'lifterlms' ),\n\t\t\t\t\t\t'desc'  => sprintf( __( 'Paste the url for a Wistia, Vimeo or Youtube video or a hosted video file. For a full list of supported providers see %s.', 'lifterlms' ), '<a href=\"https://wordpress.org/documentation/article/embeds/#list-of-sites-you-can-embed-from\" target=\"_blank\">WordPress oEmbeds</a>' ),\n\t\t\t\t\t\t'id'    => $this->prefix . 'video_embed',\n\t\t\t\t\t\t'class' => 'code input-full',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'desc'       => __( 'When enabled, the featured video will be displayed on the membership tile in addition to the membership page.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'         => $this->prefix . 'tile_featured_video',\n\t\t\t\t\t\t'label'      => __( 'Display Featured Video on Membership Tile', 'lifterlms' ),\n\t\t\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t\t\t'value'      => 'yes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'text',\n\t\t\t\t\t\t'label' => __( 'Featured Audio', 'lifterlms' ),\n\t\t\t\t\t\t'desc'  => sprintf( __( 'Paste the url for a SoundCloud or Spotify song or a hosted audio file. For a full list of supported providers see %s.', 'lifterlms' ), '<a href=\"https://wordpress.org/documentation/article/embeds/#list-of-sites-you-can-embed-from\" target=\"_blank\">WordPress oEmbeds</a>' ),\n\t\t\t\t\t\t'id'    => $this->prefix . 'audio_embed',\n\t\t\t\t\t\t'class' => 'code input-full',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'basic-editor',\n\t\t\t\t\t\t'label' => __( 'Featured Pricing Information', 'lifterlms' ),\n\t\t\t\t\t\t'desc'  => __( 'Enter information on pricing for this membership, to be displayed on the catalog page.', 'lifterlms' ),\n\t\t\t\t\t\t'id'    => $this->prefix . 'featured_pricing',\n\t\t\t\t\t\t'class' => 'code input-full',\n\t\t\t\t\t\t'value' => 'test',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Restrictions', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'allow_null'    => false,\n\t\t\t\t\t\t'class'         => '',\n\t\t\t\t\t\t'desc'          => __( 'When a non-member attempts to access content restricted to this membership', 'lifterlms' ),\n\t\t\t\t\t\t'id'            => $this->prefix . 'restriction_redirect_type',\n\t\t\t\t\t\t'is_controller' => true,\n\t\t\t\t\t\t'type'          => 'select',\n\t\t\t\t\t\t'label'         => __( 'Restricted Access Redirect', 'lifterlms' ),\n\t\t\t\t\t\t'value'         => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'none',\n\t\t\t\t\t\t\t\t'title' => __( 'Stay on page', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'membership',\n\t\t\t\t\t\t\t\t'title' => __( 'Redirect to this membership page', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'page',\n\t\t\t\t\t\t\t\t'title' => __( 'Redirect to a WordPress page', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'key'   => 'custom',\n\t\t\t\t\t\t\t\t'title' => __( 'Redirect to a Custom URL', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => 'llms-select2-post',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'restriction_redirect_type',\n\t\t\t\t\t\t'controller_value' => 'page',\n\t\t\t\t\t\t'data_attributes'  => array(\n\t\t\t\t\t\t\t'post-type' => 'page',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'id'               => $this->prefix . 'redirect_page_id',\n\t\t\t\t\t\t'label'            => __( 'Select a WordPress Page', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'select',\n\t\t\t\t\t\t'value'            => $redirect_options,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'            => '',\n\t\t\t\t\t\t'controller'       => '#' . $this->prefix . 'restriction_redirect_type',\n\t\t\t\t\t\t'controller_value' => 'custom',\n\t\t\t\t\t\t'id'               => $this->prefix . 'redirect_custom_url',\n\t\t\t\t\t\t'label'            => __( 'Enter a Custom URL', 'lifterlms' ),\n\t\t\t\t\t\t'type'             => 'text',\n\t\t\t\t\t\t'value'            => 'test',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'      => '',\n\t\t\t\t\t\t'controls'   => '#' . $this->prefix . 'restriction_notice',\n\t\t\t\t\t\t'default'    => 'yes',\n\t\t\t\t\t\t'desc'       => __( 'Check this box to output a message after redirecting. If no redirect is selected this message will replace the normal content that would be displayed.', 'lifterlms' ),\n\t\t\t\t\t\t'desc_class' => 'd-3of4 t-3of4 m-1of2',\n\t\t\t\t\t\t'id'         => $this->prefix . 'restriction_add_notice',\n\t\t\t\t\t\t'label'      => __( 'Display a Message', 'lifterlms' ),\n\t\t\t\t\t\t'type'       => 'checkbox',\n\t\t\t\t\t\t'value'      => 'yes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'    => 'full-width',\n\t\t\t\t\t\t'desc'     => sprintf( __( 'Shortcodes like %s can be used in this message', 'lifterlms' ), '[lifterlms_membership_link id=\"' . $this->post->ID . '\"]' ),\n\t\t\t\t\t\t'default'  => sprintf( __( 'You must belong to the %s membership to access this content.', 'lifterlms' ), '[lifterlms_membership_link id=\"' . $this->post->ID . '\"]' ),\n\t\t\t\t\t\t'id'       => $this->prefix . 'restriction_notice',\n\t\t\t\t\t\t'label'    => __( 'Restricted Content Notice', 'lifterlms' ),\n\t\t\t\t\t\t'type'     => 'text',\n\t\t\t\t\t\t'sanitize' => 'shortcode',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Instructors', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'button'  => array(\n\t\t\t\t\t\t\t'text' => __( 'Add Instructor', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'handler' => 'instructors_mb_store',\n\t\t\t\t\t\t'header'  => array(\n\t\t\t\t\t\t\t'default' => __( 'New Instructor', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'id'      => $this->prefix . 'instructors_data',\n\t\t\t\t\t\t'label'   => '',\n\t\t\t\t\t\t'type'    => 'repeater',\n\t\t\t\t\t\t'fields'  => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'allow_null'      => false,\n\t\t\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t\t\t'placeholder' => esc_attr__( 'Select an Instructor', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\t'roles'       => 'administrator,lms_manager,instructor,instructors_assistant',\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t'class'           => 'llms-select2-student',\n\t\t\t\t\t\t\t\t'group'           => 'd-2of3',\n\t\t\t\t\t\t\t\t'id'              => $this->prefix . 'id',\n\t\t\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t\t\t'label'           => __( 'Instructor', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'group'   => 'd-1of6',\n\t\t\t\t\t\t\t\t'class'   => 'input-full',\n\t\t\t\t\t\t\t\t'default' => $instructor_defaults['label'],\n\t\t\t\t\t\t\t\t'id'      => $this->prefix . 'label',\n\t\t\t\t\t\t\t\t'type'    => 'text',\n\t\t\t\t\t\t\t\t'label'   => __( 'Label', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'allow_null' => false,\n\t\t\t\t\t\t\t\t'class'      => 'llms-select2',\n\t\t\t\t\t\t\t\t'default'    => $instructor_defaults['visibility'],\n\t\t\t\t\t\t\t\t'group'      => 'd-1of6',\n\t\t\t\t\t\t\t\t'id'         => $this->prefix . 'visibility',\n\t\t\t\t\t\t\t\t'type'       => 'select',\n\t\t\t\t\t\t\t\t'label'      => __( 'Visibility', 'lifterlms' ),\n\t\t\t\t\t\t\t\t'value'      => array(\n\t\t\t\t\t\t\t\t\t'visible' => esc_html__( 'Visible', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\t'hidden'  => esc_html__( 'Hidden', 'lifterlms' ),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Auto Enrollment', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'label'      => __( 'Automatic Enrollment', 'lifterlms' ),\n\t\t\t\t\t\t'desc'       => sprintf( __( 'When a student joins this membership they will be automatically enrolled in these courses. Click %1$shere%2$s for more information.', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/membership-auto-enrollment/\" target=\"_blank\">', '</a>' ),\n\t\t\t\t\t\t'id'         => $this->prefix . 'content_table',\n\t\t\t\t\t\t'titles'     => array( '', __( 'Course Name', 'lifterlms' ), '' ),\n\t\t\t\t\t\t'type'       => 'table',\n\t\t\t\t\t\t'table_data' => $this->get_content_table( $membership ),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'class'           => 'llms-select2-post',\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'placeholder'    => __( 'Select course(s)', 'lifterlms' ),\n\t\t\t\t\t\t\t'post-type'      => 'course',\n\t\t\t\t\t\t\t'no-view-button' => true,\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'id'              => $this->prefix . 'auto_enroll',\n\t\t\t\t\t\t'label'           => __( 'Add Course(s)', 'lifterlms' ),\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'value'           => array(),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Save field data.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.0 Autoenroll courses saved via AJAX and removed from this method.\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 3.36.3 Added logic to correctly sanitize fields of type 'multi' (array)\n\t *               and 'shortcode' (preventing quotes encode).\n\t *               Also align the return type to the parent `save()` method.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @see LLMS_Admin_Metabox::save_actions()\n\t *\n\t * @param int $post_id WP_Post ID of the post being saved.\n\t * @return int `-1` When no user or user is missing required capabilities or when there's no or invalid nonce.\n\t *             `0` during inline saves or ajax requests or when no fields are found for the metabox.\n\t *             `1` if fields were found. This doesn't mean there weren't errors during saving.\n\t */\n\tpublic function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) ) {\n\t\t\treturn -1;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t// Return early during quick saves and ajax requests.\n\t\tif ( isset( $_POST['action'] ) && 'inline-save' === $_POST['action'] ) {\n\t\t\treturn 0;\n\t\t} elseif ( llms_is_ajax() ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t$membership = new LLMS_Membership( $post_id );\n\n\t\tif ( ! isset( $_POST[ $this->prefix . 'restriction_add_notice' ] ) ) {\n\t\t\t$_POST[ $this->prefix . 'restriction_add_notice' ] = 'no';\n\t\t}\n\n\t\t// Get all defined fields.\n\t\t$fields = $this->get_fields();\n\n\t\t// save all the fields.\n\t\t$save_fields = array(\n\t\t\t$this->prefix . 'restriction_redirect_type',\n\t\t\t$this->prefix . 'redirect_page_id',\n\t\t\t$this->prefix . 'redirect_custom_url',\n\t\t\t$this->prefix . 'restriction_add_notice',\n\t\t\t$this->prefix . 'restriction_notice',\n\t\t\t$this->prefix . 'sales_page_content_page_id',\n\t\t\t$this->prefix . 'sales_page_content_type',\n\t\t\t$this->prefix . 'sales_page_content_url',\n\t\t\t$this->prefix . 'featured_pricing',\n\t\t\t$this->prefix . 'video_embed',\n\t\t\t$this->prefix . 'audio_embed',\n\t\t\t$this->prefix . 'tile_featured_video',\n\t\t\t$this->prefix . 'instructors_data',\n\t\t);\n\n\t\tif ( ! is_array( $fields ) ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t$to_return = 0;\n\n\t\t// Loop through the fields.\n\t\tforeach ( $fields as $group => $data ) {\n\n\t\t\t// Find the fields in each tab.\n\t\t\tif ( isset( $data['fields'] ) && is_array( $data['fields'] ) ) {\n\n\t\t\t\t// loop through the fields.\n\t\t\t\tforeach ( $data['fields'] as $field ) {\n\n\t\t\t\t\t// don't save things that don't have an ID or that are not listed in $save_fields.\n\t\t\t\t\tif ( isset( $field['id'] ) && in_array( $field['id'], $save_fields, true ) ) {\n\n\t\t\t\t\t\tif ( isset( $field['handler'] ) ) {\n\t\t\t\t\t\t\t$this->save_field( $post_id, $field );\n\t\t\t\t\t\t\t$to_return = 1;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( isset( $field['sanitize'] ) && 'shortcode' === $field['sanitize'] ) {\n\t\t\t\t\t\t\t$val = llms_filter_input_sanitize_string( INPUT_POST, $field['id'], array( FILTER_FLAG_NO_ENCODE_QUOTES ) );\n\t\t\t\t\t\t} elseif ( isset( $field['multi'] ) && $field['multi'] ) {\n\t\t\t\t\t\t\t$val = llms_filter_input_sanitize_string( INPUT_POST, $field['id'], array( FILTER_REQUIRE_ARRAY ) );\n\t\t\t\t\t\t} elseif ( $field['type'] === 'basic-editor' ) {\n\t\t\t\t\t\t\t$val = wp_kses( $_POST[ $field['id'] ], LLMS_ALLOWED_HTML_PRICES );\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t$val = llms_filter_input_sanitize_string( INPUT_POST, $field['id'] );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$membership->set( substr( $field['id'], strlen( $this->prefix ) ), $val );\n\t\t\t\t\t\t$to_return = 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $to_return;\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.order.details.php",
    "content": "<?php\n/**\n * LLMS_Meta_Box_Order_Details class\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 3.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Order Details meta box\n *\n * @since 3.0.0\n */\nclass LLMS_Meta_Box_Order_Details extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-order-details';\n\t\t$this->title    = __( 'Order Details', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_order',\n\t\t);\n\t\t$this->context  = 'normal';\n\t\t$this->priority = 'high';\n\t}\n\n\t/**\n\t * Not used because our metabox doesn't use the standard fields api\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Output metabox content\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Unknown.\n\t * @since 5.3.0 Use llms() in favor of deprecated LLMS().\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t$order = llms_get_post( $this->post );\n\t\tif ( ! $order || ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$gateway = $order->get_gateway();\n\n\t\t// Setup a list of gateways that this order can be switched to.\n\t\t$gateway_feature           = $order->is_recurring() ? 'recurring_payments' : 'single_payments';\n\t\t$switchable_gateways       = array();\n\t\t$switchable_gateway_fields = array();\n\t\tforeach ( llms()->payment_gateways()->get_supporting_gateways( $gateway_feature ) as $id => $gateway_obj ) {\n\t\t\t$switchable_gateways[ $id ]       = $gateway_obj->get_admin_title();\n\t\t\t$switchable_gateway_fields[ $id ] = $gateway_obj->get_admin_order_fields();\n\t\t}\n\n\t\tinclude LLMS_PLUGIN_DIR . 'includes/admin/views/metaboxes/view-order-details.php';\n\t}\n\n\t/**\n\t * Save method\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 5.3.0 Update return value from void to int (for testing conditions) and include update remaining payment data when necessary.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id Post ID of the Order.\n\t * @return int Returns `-1` on invalid or missing nonce, `0` when an order cannot be found, and\n\t *             `1` otherwise.\n\t */\n\tpublic function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) ) {\n\t\t\treturn -1;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t$order = llms_get_post( $post_id );\n\t\tif ( ! $order || ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif ( ! current_user_can( 'edit_post', $post_id ) ) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t$fields = array(\n\t\t\t'payment_gateway',\n\t\t\t'gateway_customer_id',\n\t\t\t'gateway_subscription_id',\n\t\t\t'gateway_source_id',\n\t\t);\n\n\t\tforeach ( $fields as $key ) {\n\n\t\t\tif ( isset( $_POST[ $key ] ) ) {\n\t\t\t\t$order->set( $key, llms_filter_input_sanitize_string( INPUT_POST, $key ) );\n\t\t\t}\n\t\t}\n\n\t\t// Only allow editing the total (for the next recurrence) if this is a recurring order.\n\t\tif ( $order->is_recurring() && isset( $_POST['total'] ) && is_numeric( $_POST['total'] ) ) {\n\t\t\t$total = floatval( $_POST['total'] );\n\t\t\t$order->set( 'total', $total );\n\t\t\t$order->add_note( sprintf( __( 'Order total for future payments updated to %s.', 'lifterlms' ), $order->get_price( 'total' ) ) );\n\t\t}\n\n\t\t$this->save_remaining_payments( $order );\n\n\t\treturn 1;\n\t}\n\n\t/**\n\t * Save remaining payment date for expiring recurring orders\n\t *\n\t * @since 5.3.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.0.0 Return `-1` if the recurring payment cannot be modified (the gateway doesn't support this feature).\n\t *\n\t * @param LLMS_Order $order Order object\n\t * @return int Returns `-1` when there's for invalid order types, `0` when there's no changes to save, and\n\t *             `1` when remaining payment data is updated.\n\t */\n\tprotected function save_remaining_payments( $order ) {\n\n\t\t// If it's not a payment plan or cannot modify recurring payment, don't proceed.\n\t\tif ( ! $order->has_plan_expiration() || ! $order->supports_modify_recurring_payments() ) {\n\t\t\treturn -1;\n\t\t}\n\n\t\t$payments  = (int) llms_filter_input( INPUT_POST, '_llms_remaining_payments', FILTER_SANITIZE_NUMBER_INT );\n\t\t$remaining = $order->get_remaining_payments();\n\n\t\t// Nothing to save.\n\t\tif ( $payments < 1 || $payments === $remaining ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Determine how to adjust the billing length.\n\t\t$adjustment = $payments - $remaining;\n\t\t$original   = $order->get( 'billing_length' );\n\t\t$new_length = max( $order->get( 'billing_length' ) + $adjustment, 1 );\n\t\t$period     = $order->get( 'billing_period' );\n\n\t\t// Update the payment plan.\n\t\t$order->set( 'billing_length', $new_length );\n\n\t\t// Record that the payment plan has been modified.\n\t\t$order->add_note(\n\t\t\tsprintf(\n\t\t\t\t// Translators: %1$d is the original billing length; %2$s is the billing period (adjusted for pluralization against the original billing length); %3$d is the new billing length; %4$s is the billing period (adjusted for pluralization against the new billing length).\n\t\t\t\t__( 'The billing length of the order has been modified from %1$d %2$s to %3$d %4$s.', 'lifterlms' ),\n\t\t\t\t$original,\n\t\t\t\tllms_get_time_period_l10n( $period, $original ),\n\t\t\t\t$new_length,\n\t\t\t\tllms_get_time_period_l10n( $period, $new_length )\n\t\t\t)\n\t\t);\n\n\t\t// Store a use note if one was entered.\n\t\t$note = llms_filter_input_sanitize_string( INPUT_POST, '_llms_remaining_note' );\n\t\tif ( $note ) {\n\t\t\t$order->add_note( wp_strip_all_tags( $note ), true );\n\t\t}\n\n\t\t// Restart scheduled payments.\n\t\tif ( ! $order->has_scheduled_payment() ) {\n\t\t\t$order->maybe_schedule_payment();\n\t\t}\n\n\t\treturn 1;\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php",
    "content": "<?php\n/**\n * Meta box for Student Enrollment Information via the Order interface\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 3.0.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Order_Enrollment class\n *\n * @since 3.0.0\n * @since 3.33.0 Added the logic to handle the Enrollment 'deleted' status on save.\n * @since 4.2.0 In ` save_delete_enrollment()` removed order cancellation instruction, moved elsewhere as reaction to the enrollment deletion.\n *              @see `LLMS_Controller_Orders->on_deleted_enrollment()` in `includes\\controllers\\class.llms.controller.orders.php`.\n *              Also, add order note about the enrollment deletion only if it actually occurred.\n */\nclass LLMS_Meta_Box_Order_Enrollment extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-order-enrollment-status';\n\t\t$this->title    = __( 'Student Enrollment', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_order',\n\t\t);\n\t\t$this->context  = 'side';\n\t\t$this->priority = 'default';\n\t}\n\n\t/**\n\t * Not used because our metabox doesn't use the standard fields api\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Function to field WP::output() method call\n\t *\n\t * Passes output instruction to parent.\n\t *\n\t * @since 3.0.0\n\t * @since 3.33.0 Added 'Delete Enrollment' button.\n\t *\n\t * @return null\n\t */\n\tpublic function output() {\n\n\t\t$order = llms_get_post( $this->post );\n\n\t\tif ( llms_parse_bool( $order->get( 'anonymized' ) ) ) {\n\t\t\tesc_html_e( 'Cannot manage enrollment status for anonymized orders.', 'lifterlms' );\n\t\t\treturn;\n\t\t}\n\n\t\t$student_id = $order->get( 'user_id' );\n\t\tif ( ! $student_id ) {// No user id, nothing to show.\n\t\t\treturn;\n\t\t}\n\n\t\t$student = llms_get_student( $student_id );\n\n\t\t// No student, show a message.\n\t\tif ( empty( $student ) ) {\n\t\t\tesc_html_e( \"The student who placed the order doesn't exist anymore.\", 'lifterlms' );\n\t\t\treturn;\n\t\t}\n\n\t\t$current_status = $student->get_enrollment_status( $order->get( 'product_id' ) );\n\n\t\t$select  = '<select name=\"llms_student_new_enrollment_status\">';\n\t\t$select .= '<option value=\"\">-- ' . esc_html__( 'Select', 'lifterlms' ) . ' --</option>';\n\n\t\tforeach ( llms_get_enrollment_statuses() as $val => $name ) {\n\t\t\t$select .= '<option value=\"' . esc_attr( $val ) . '\"' . selected( $val, strtolower( $current_status ), false ) . '>' . esc_html( $name ) . '</option>';\n\t\t}\n\t\t$select .= '</select>';\n\n\t\techo '<p>';\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above.\n\t\tprintf( esc_html_x( 'Status: %s', 'enrollment status', 'lifterlms' ), $select );\n\t\techo '</p>';\n\n\t\techo '<p>';\n\t\tprintf( esc_html_x( 'Enrolled: %s', 'enrollment trigger', 'lifterlms' ), esc_html( $student->get_enrollment_date( $order->get( 'product_id' ), 'enrolled', 'm/d/Y h:i:s A' ) ) );\n\t\techo '</p>';\n\t\techo '<p>';\n\t\tprintf( esc_html_x( 'Updated: %s', 'enrollment trigger', 'lifterlms' ), esc_html( $student->get_enrollment_date( $order->get( 'product_id' ), 'updated', 'm/d/Y h:i:s A' ) ) );\n\t\techo '</p>';\n\t\techo '<p>';\n\t\tprintf( esc_html_x( 'Trigger: %s', 'enrollment trigger', 'lifterlms' ), esc_html( $student->get_enrollment_trigger( $order->get( 'product_id' ) ) ) );\n\t\techo '</p>';\n\n\t\techo '<input name=\"llms_student_old_enrollment_status\" type=\"hidden\" value=\"' . esc_attr( $current_status ) . '\">';\n\n\t\techo '<input name=\"llms_update_enrollment_status\" type=\"submit\" class=\"llms-button-secondary small\" value=\"' . esc_html__( 'Update Status', 'lifterlms' ) . '\"> ';\n\t\tif ( $current_status && 'enrolled' !== $current_status ) {\n\t\t\techo '<input name=\"llms_delete_enrollment_status\" type=\"submit\" class=\"llms-button-danger small\" value=\"' . esc_html__( 'Delete Enrollment', 'lifterlms' ) . '\">';\n\t\t}\n\t}\n\n\t/**\n\t * Save method\n\t *\n\t * @since 3.0.0\n\t * @since 3.33.0 Added the logic to handle the Enrollment 'deleted' status.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id Post ID of the Order.\n\t * @return void\n\t */\n\tpublic function save( $post_id ) {\n\n\t\t$update = llms_filter_input( INPUT_POST, 'llms_update_enrollment_status' );\n\t\tif ( ! empty( $update ) ) {\n\t\t\t$this->save_update_enrollment( $post_id );\n\t\t}\n\n\t\t$delete = llms_filter_input( INPUT_POST, 'llms_delete_enrollment_status' );\n\t\tif ( ! empty( $delete ) ) {\n\t\t\t$this->save_delete_enrollment( $post_id );\n\t\t}\n\t}\n\n\t/**\n\t * Delete enrollment data based on posted values.\n\t *\n\t * @since 3.33.0\n\t * @since 4.2.0 Removed order cancellation instruction, moved elsewhere as reaction to the enrollment deletion.\n\t *              @see `LLMS_Controller_Orders->on_deleted_enrollment()` in `includes\\controllers\\class.llms.controller.orders.php`.\n\t *              Also, add order note about the enrollment deletion only if it actually occurred.\n\t *\n\t * @param int $post_id WP_Post ID of the order.\n\t * @return void\n\t */\n\tprivate function save_delete_enrollment( $post_id ) {\n\n\t\t$order = llms_get_post( $post_id );\n\n\t\t/**\n\t\t * Completely remove any enrollment records related to the given product & order.\n\t\t * Also note that, by design, at this stage the student has already been unenrolled,\n\t\t * as the delete button is only available when the enrollment status is NOT 'enrolled'.\n\t\t */\n\t\tif ( llms_delete_student_enrollment( $order->get( 'user_id' ), $order->get( 'product_id' ), 'order_' . $order->get( 'id' ) ) ) {\n\n\t\t\t$order->add_note( __( 'Student enrollment records have been deleted.', 'lifterlms' ), true );\n\n\t\t}\n\t}\n\n\t/**\n\t * Update enrollment data based on posted values.\n\t *\n\t * @since 3.33.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id WP_Post ID of the order.\n\t * @return void\n\t */\n\tprivate function save_update_enrollment( $post_id ) {\n\n\t\t$old_status = llms_filter_input_sanitize_string( INPUT_POST, 'llms_student_old_enrollment_status' );\n\t\t$new_status = llms_filter_input_sanitize_string( INPUT_POST, 'llms_student_new_enrollment_status' );\n\n\t\tif ( ! $new_status || $old_status === $new_status ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$order = llms_get_post( $post_id );\n\n\t\tif ( 'enrolled' === $new_status ) {\n\t\t\tllms_enroll_student( $order->get( 'user_id' ), $order->get( 'product_id' ), 'order_' . $order->get( 'id' ) );\n\t\t} else {\n\t\t\tllms_unenroll_student( $order->get( 'user_id' ), $order->get( 'product_id' ), $new_status, 'any' );\n\t\t}\n\n\t\t$new_status_name = llms_get_enrollment_status_name( $new_status );\n\n\t\tif ( ! $old_status ) {\n\n\t\t\t$note = sprintf( __( 'Student enrollment status changed to %s.', 'lifterlms' ), $new_status_name );\n\n\t\t} else {\n\n\t\t\t// Translators: %1$s = old enrollment status; %2$s = new enrollment status.\n\t\t\t$note = sprintf( __( 'Student enrollment status changed from %1$s to %2$s', 'lifterlms' ), llms_get_enrollment_status_name( $old_status ), $new_status_name );\n\n\t\t}\n\n\t\t$order->add_note( $note, true );\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php",
    "content": "<?php\n/**\n * Meta boxes for order notes\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 3.0.0\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta boxes for orders notes class\n *\n * @since 3.0.0\n * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n */\nclass LLMS_Meta_Box_Order_Notes extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @since  3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-order-notes';\n\t\t$this->title    = __( 'Order Notes', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_order',\n\t\t);\n\t\t$this->context  = 'side';\n\t\t$this->priority = 'default';\n\t}\n\n\t/**\n\t * Not used because our metabox doesn't use the standard fields api\n\t *\n\t * @return array\n\t *\n\t * @since  3.0.0\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Function to field WP::output() method call\n\t * Passes output instruction to parent\n\t *\n\t * @since  3.0.0\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t$order = new LLMS_Order( $this->post );\n\n\t\t$curr_page = isset( $_GET['notes-page'] ) ? absint( wp_unslash( $_GET['notes-page'] ) ) : 1;\n\t\t$per_page  = 10;\n\n\t\t$edit_link = get_edit_post_link( $this->post->ID );\n\n\t\t$notes     = $order->get_notes( $per_page, $curr_page );\n\t\t$next_page = ( count( $notes ) == $per_page ) ? count( $order->get_notes( $per_page, $curr_page + 1 ) ) : 0;\n\n\t\t$prev_url = ( $curr_page > 1 ) ? add_query_arg( 'notes-page', $curr_page - 1, $edit_link ) . '#' . $this->id : false;\n\t\t$next_url = ( $next_page ) ? add_query_arg( 'notes-page', $curr_page + 1, $edit_link ) . '#' . $this->id : false;\n\n\t\tif ( $notes ) {\n\t\t\techo '<ul class=\"llms-order-notes\">';\n\t\t\tforeach ( $notes  as $note ) {\n\t\t\t\t?>\n\n\t\t\t\t<li class=\"llms-order-note\" id=\"llms-order-note-<?php echo esc_attr( $note->comment_ID ); ?>\">\n\t\t\t\t\t<div class=\"llms-order-note-content\"><?php echo wp_kses_post( wpautop( get_comment_text( $note->comment_ID ) ) ); ?></div>\n\t\t\t\t\t<div class=\"llms-order-note-meta\">\n\t\t\t\t\t\t<?php printf( esc_html_x( 'by %s', 'order note author', 'lifterlms' ), esc_html( get_comment_author( $note->comment_ID ) ) ); ?>\n\t\t\t\t\t\t<?php printf( esc_html_x( 'on %s', 'order note date', 'lifterlms' ), esc_html( get_comment_date( 'M j, Y h:i a', $note->comment_ID ) ) ); ?>\n\t\t\t\t\t</div>\n\n\t\t\t\t</li>\n\n\t\t\t\t<?php\n\t\t\t}\n\t\t\techo '</ul>';\n\n\t\t\tif ( ! empty( $prev_url ) || ! empty( $next_url ) ) {\n\n\t\t\t\techo '<hr>';\n\n\t\t\t}\n\n\t\t\tif ( ! empty( $prev_url ) ) {\n\t\t\t\techo '<a class=\"button\" href=\"' . esc_url( $prev_url ) . '\">' . sprintf( esc_html__( '%s Newer', 'lifterlms' ), '&laquo;' ) . '</a> ';\n\t\t\t}\n\n\t\t\tif ( ! empty( $next_url ) ) {\n\t\t\t\techo '<a class=\"button\" href=\"' . esc_url( $next_url ) . '\">' . sprintf( esc_html__( 'Older %s', 'lifterlms' ), '&raquo;' ) . '</a>';\n\t\t\t}\n\t\t} else {\n\n\t\t\tesc_html_e( 'No order notes found.', 'lifterlms' );\n\n\t\t}// End if().\n\t}\n\n\t/**\n\t * Save method\n\t * Does nothing because there's no editable data in this metabox\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param int $post_id  Post ID of the Order.\n\t * @return  void\n\t */\n\tpublic function save( $post_id ) {}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.order.submit.php",
    "content": "<?php\n/**\n * Order update/submit box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Order_Submit class\n *\n * @since 1.0.0\n * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n * @since 3.36.0 Date fields require array when sanitized.\n */\nclass LLMS_Meta_Box_Order_Submit extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-order-submit';\n\t\t$this->title    = __( 'Order Information', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_order',\n\t\t);\n\t\t$this->context  = 'side';\n\t\t$this->priority = 'high';\n\t}\n\n\t/**\n\t * Retrieve json to be used by the llms-editable date fields\n\t *\n\t * @param    int $time  timestamp\n\t * @return   string\n\t * @since    3.10.0\n\t * @version  3.19.0\n\t */\n\tpublic function get_editable_date_json( $time ) {\n\n\t\treturn json_encode(\n\t\t\tarray(\n\t\t\t\t'date'   => date_i18n( 'Y-m-d', $time ),\n\t\t\t\t'hour'   => date_i18n( 'H', $time ),\n\t\t\t\t'minute' => date_i18n( 'i', $time ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Not used because our metabox doesn't use the standard fields api\n\t *\n\t * @since  3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Function to field WP::output() method call\n\t * Passes output instruction to parent\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @return string|null\n\t */\n\tpublic function output() {\n\n\t\t$order = new LLMS_Order( $this->post );\n\n\t\tif ( $order->is_legacy() ) {\n\t\t\treturn esc_html_e( 'The status of a Legacy order cannot be changed.', 'lifterlms' );\n\t\t}\n\n\t\tinclude LLMS_PLUGIN_DIR . 'includes/admin/views/metaboxes/view-order-submit.php';\n\n\t\twp_nonce_field( 'lifterlms_save_data', 'lifterlms_meta_nonce' );\n\t}\n\n\t/**\n\t * Save action, update order status\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 3.36.0 Date fields require array when sanitized.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.0.0 Do not save recurring payments related dates if order's gateway do not support recurring payments modification.\n\t * @since 10.0.0 Add order note when access expiration date is changed.\n\t *\n\t * @param int $post_id  WP Post ID of the Order\n\t * @return null\n\t */\n\tpublic function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$order = llms_get_post( $post_id );\n\n\t\tif ( isset( $_POST['_llms_order_status'] ) ) {\n\n\t\t\t$new_status = llms_filter_input_sanitize_string( INPUT_POST, '_llms_order_status' );\n\t\t\t$old_status = $order->get( 'status' );\n\n\t\t\tif ( $old_status !== $new_status ) {\n\n\t\t\t\t// Update the status.\n\t\t\t\t$order->set( 'status', $new_status );\n\n\t\t\t}\n\t\t}\n\n\t\t$editable_dates = array(\n\t\t\t'_llms_date_access_expires',\n\t\t);\n\n\t\t// Save recurring payments related dates if order's gateway supports recurring payments modification.\n\t\tif ( $order->supports_modify_recurring_payments() ) {\n\t\t\t/**\n\t\t\t * Order is important -- if both trial and next payment are updated\n\t\t\t * they should be saved in that order since next payment date\n\t\t\t * is automatically recalculated by trial end date update.\n\t\t\t */\n\t\t\tarray_push( $editable_dates, '_llms_date_trial_end', '_llms_date_next_payment' );\n\t\t}\n\n\t\tforeach ( $editable_dates as $id => $key ) {\n\n\t\t\tif ( isset( $_POST[ $key ] ) ) {\n\n\t\t\t\t// The array of date, hour, minute that was submitted.\n\t\t\t\t$dates = llms_filter_input_sanitize_string( INPUT_POST, $key, array( FILTER_REQUIRE_ARRAY ) );\n\n\t\t\t\t// Format the array of data as a datetime string.\n\t\t\t\t$new_date = $dates['date'] . ' ' . sprintf( '%02d', $dates['hour'] ) . ':' . sprintf( '%02d', $dates['minute'] );\n\n\t\t\t\t// Get the existing saved date without seconds (in the same format as $new_date).\n\t\t\t\t$saved_date = date_i18n( 'Y-m-d H:i', strtotime( get_post_meta( $post_id, $key, true ) ) );\n\n\t\t\t\t// If the dates are not equal, update the date.\n\t\t\t\tif ( $new_date !== $saved_date ) {\n\t\t\t\t\t$date_key = str_replace( '_llms_date_', '', $key );\n\t\t\t\t\t$order->set_date( $date_key, $new_date . ':00' );\n\n\t\t\t\t\tif ( 'access_expires' === $date_key ) {\n\t\t\t\t\t\t$old_date_display = $saved_date\n\t\t\t\t\t\t\t? date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $saved_date ) )\n\t\t\t\t\t\t\t: __( 'none', 'lifterlms' );\n\t\t\t\t\t\t$new_date_display = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $new_date ) );\n\n\t\t\t\t\t\t// Translators: %1$s = old access expiration date; %2$s = new access expiration date.\n\t\t\t\t\t\t$note = sprintf( __( 'Access expiration date changed from %1$s to %2$s', 'lifterlms' ), $old_date_display, $new_date_display );\n\t\t\t\t\t\t$order->add_note( $note, true );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php",
    "content": "<?php\n/**\n * Order transactions metabox\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 3.0.0\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Order_Transactions class\n *\n * @since 3.0.0\n * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n */\nclass LLMS_Meta_Box_Order_Transactions extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-order-transactions';\n\t\t$this->title    = __( 'Transactions', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_order',\n\t\t);\n\t\t$this->context  = 'normal';\n\t\t$this->priority = 'high';\n\n\t}\n\n\t/**\n\t * Not used because our metabox doesn't use the standard fields api\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Function to field WP::output() method call\n\t * Passes output instruction to parent\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Sanitize `$_GET` data.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t$order = new LLMS_Order( $this->post );\n\n\t\t$curr_page = isset( $_GET['txns-page'] ) ? absint( wp_unslash( $_GET['txns-page'] ) ) : 1;\n\t\t// Allow users to see all if they really want to.\n\t\t$per_page = isset( $_GET['txns-count'] ) ? absint( wp_unslash( $_GET['txns-count'] ) ) : 20;\n\n\t\t$transactions = $order->get_transactions(\n\t\t\tarray(\n\t\t\t\t'per_page' => $per_page,\n\t\t\t\t'paged'    => $curr_page,\n\t\t\t)\n\t\t);\n\n\t\t$edit_link = get_edit_post_link( $this->post->ID );\n\n\t\t$prev_url = ( $transactions['page'] > 1 ) ? add_query_arg( 'txns-page', $curr_page - 1, $edit_link ) . '#' . $this->id : false;\n\t\t$next_url = ( $transactions['page'] < $transactions['pages'] ) ? add_query_arg( 'txns-page', $curr_page + 1, $edit_link ) . '#' . $this->id : false;\n\t\t$all_url  = ( $next_url || $prev_url ) ? add_query_arg( 'txns-count', -1, $edit_link ) . '#' . $this->id : false;\n\n\t\tllms_get_template(\n\t\t\t'admin/post-types/order-transactions.php',\n\t\t\tarray(\n\t\t\t\t'all_url'      => $all_url,\n\t\t\t\t'next_url'     => $next_url,\n\t\t\t\t'prev_url'     => $prev_url,\n\t\t\t\t'transactions' => $transactions,\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Resend a receipt for a transaction\n\t *\n\t * @param    int $post_id  WP Post ID of the current order\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprivate function resend_receipt( $post_id ) {\n\n\t\t$txn_id = llms_filter_input( INPUT_POST, 'llms_resend_receipt', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( ! $txn_id ) {\n\t\t\treturn;\n\t\t}\n\t\tdo_action( 'lifterlms_resend_transaction_receipt', llms_get_post( $txn_id ) );\n\n\t}\n\n\t/**\n\t * Save method, processes refunds / records manual txns\n\t *\n\t * @since 3.0.0\n\t * @since 3.8.0 Unknown\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id Post ID of the Order.\n\t * @return void\n\t */\n\tpublic function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_meta_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$actions = array(\n\t\t\t'llms_process_refund' => 'save_refund',\n\t\t\t'llms_record_txn'     => 'save_transaction',\n\t\t\t'llms_resend_receipt' => 'resend_receipt',\n\t\t);\n\n\t\tforeach ( $actions as $action => $method ) {\n\t\t\t$action = llms_filter_input( INPUT_POST, $action );\n\t\t\tif ( $action ) {\n\t\t\t\t$this->$method( $post_id );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Save method, processes refunds\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id Post ID of the Order.\n\t * @return null\n\t */\n\tprivate function save_refund( $post_id ) {\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce is verified in the save() method of this class.\n\n\t\t$txn_id = llms_filter_input( INPUT_POST, 'llms_refund_txn_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t$amount = llms_filter_input_sanitize_string( INPUT_POST, 'llms_refund_amount' );\n\t\tif ( empty( $txn_id ) ) {\n\t\t\treturn $this->add_error( __( 'Refund Error: Missing a transaction ID', 'lifterlms' ) );\n\t\t} elseif ( empty( $amount ) ) {\n\t\t\treturn $this->add_error( __( 'Refund Error: Missing or invalid refund amount', 'lifterlms' ) );\n\t\t}\n\n\t\t$txn = new LLMS_Transaction( $txn_id );\n\n\t\t$refund = $txn->process_refund(\n\t\t\t$amount,\n\t\t\tllms_filter_input_sanitize_string( INPUT_POST, 'llms_refund_note' ),\n\t\t\tllms_filter_input_sanitize_string( INPUT_POST, 'llms_process_refund' )\n\t\t);\n\n\t\tif ( is_wp_error( $refund ) ) {\n\t\t\t$this->add_error( sprintf( _x( 'Refund Error: %s', 'admin error message', 'lifterlms' ), $refund->get_error_message() ) );\n\t\t}\n\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Missing\n\n\t}\n\n\n\t/**\n\t * Save method, records manual transactions\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id Post ID of the Order.\n\t * @return null\n\t */\n\tprivate function save_transaction( $post_id ) {\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce is verified in the save() method of this class.\n\n\t\tif ( empty( $_POST['llms_txn_amount'] ) ) {\n\t\t\treturn $this->add_error( __( 'Refund Error: Missing or invalid payment amount', 'lifterlms' ) );\n\t\t}\n\n\t\t$order = new LLMS_Order( $post_id );\n\n\t\t$txn = $order->record_transaction(\n\t\t\tarray(\n\t\t\t\t'amount'             => llms_filter_input_sanitize_string( INPUT_POST, 'llms_txn_amount' ),\n\t\t\t\t'source_description' => llms_filter_input_sanitize_string( INPUT_POST, 'llms_txn_source' ),\n\t\t\t\t'transaction_id'     => llms_filter_input_sanitize_string( INPUT_POST, 'llms_txn_id' ),\n\t\t\t\t'status'             => 'llms-txn-succeeded',\n\t\t\t\t'payment_gateway'    => 'manual',\n\t\t\t\t'payment_type'       => 'single',\n\t\t\t)\n\t\t);\n\n\t\tif ( ! empty( $_POST['llms_txn_note'] ) ) {\n\t\t\t$order->add_note( llms_filter_input_sanitize_string( INPUT_POST, 'llms_txn_note' ), true );\n\t\t}\n\n\t\tif ( is_wp_error( $txn ) ) {\n\t\t\t$this->add_error( sprintf( _x( 'Refund Error: %s', 'admin error message', 'lifterlms' ), $refund->get_error_message() ) );\n\t\t}\n\n\t\t// phpcs:enable WordPress.Security.NonceVerification.Missing\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php",
    "content": "<?php\n/**\n * Access Plan meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 1.0.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Product class\n *\n * @since 1.0.0\n * @since 3.30.0 Added checkout redirect settings\n * @since 3.30.3 Adjusted localization priority to 9.\n */\nclass LLMS_Meta_Box_Product extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.3 Adjusted localization priority to 9.\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-product';\n\t\t$this->title    = __( 'Access Plans', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'course',\n\t\t\t'llms_membership',\n\t\t);\n\t\t$this->priority = 'high';\n\n\t\t// Output PHP variables for JS access.\n\t\tadd_action( 'admin_print_footer_scripts', array( $this, 'localize_js' ), 9 );\n\t}\n\n\t/**\n\t * Return an empty array because the metabox fields here are completely custom\n\t *\n\t * @return array\n\t * @since  3.0.0\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Pass settings to JS\n\t *\n\t * @return  void\n\t * @since   3.0.0\n\t * @version 3.0.0\n\t */\n\tpublic function localize_js() {\n\t\t$p     = new LLMS_Product( $this->post );\n\t\t$limit = $p->get_access_plan_limit();\n\t\techo '<script>' . esc_js( 'window.llms = window.llms || {}; window.llms.product = { access_plan_limit: ' . $limit . ' };' ) . '</script>';\n\t}\n\n\t/**\n\t * Filter the available buttons in the Plan Description editors\n\t *\n\t * @param  array  $buttons array of default buttons\n\t * @param  string $id      editor id\n\t * @return array\n\t * @since   3.0.0\n\t * @version 3.0.0\n\t */\n\tpublic function mce_buttons( $buttons, $id ) {\n\n\t\tif ( strpos( $id, '_llms_plans_content' ) !== false ) {\n\n\t\t\t$buttons = array(\n\t\t\t\t'bold',\n\t\t\t\t'italic',\n\t\t\t\t'underline',\n\t\t\t\t'blockquote',\n\t\t\t\t'strikethrough',\n\t\t\t\t'bullist',\n\t\t\t\t'numlist',\n\t\t\t\t'alignleft',\n\t\t\t\t'aligncenter',\n\t\t\t\t'alignright',\n\t\t\t\t'undo',\n\t\t\t\t'redo',\n\t\t\t);\n\n\t\t}\n\n\t\treturn $buttons;\n\t}\n\n\t/**\n\t * Output metabox content\n\t * Overwrites abstract because of the requirements of the UI\n\t *\n\t * @return void\n\t * @since  3.0.0\n\t * @version 3.29.0\n\t */\n\tpublic function output() {\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in get_html().\n\t\techo $this->get_html();\n\t}\n\n\t/**\n\t * Retrieve the HTML for the metabox\n\t *\n\t * @return  string\n\t * @since   3.29.0\n\t * @since   3.30.0 Added checkout redirect settings.\n\t * @version 3.30.0\n\t */\n\tpublic function get_html() {\n\n\t\tob_start();\n\t\t$product = new LLMS_Product( $this->post );\n\t\tadd_filter( 'teeny_mce_buttons', array( $this, 'mce_buttons' ), 10, 2 );\n\t\t$course = ( 'course' === $product->get( 'type' ) ) ? new LLMS_Course( $product->post ) : false;\n\n\t\t// get available checkout redirection types.\n\t\t$product_type               = ( ! $course ) ? __( 'Membership', 'lifterlms' ) : __( 'Course', 'lifterlms' );\n\t\t$checkout_redirection_types = llms_get_checkout_redirection_types( $product_type );\n\n\t\tinclude LLMS_PLUGIN_DIR . 'includes/admin/views/access-plans/metabox.php';\n\t\tremove_filter( 'teeny_mce_buttons', array( $this, 'mce_buttons' ), 10, 2 );\n\t\treturn apply_filters( 'llms_metabox_product_output', ob_get_clean(), $this );\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.students.php",
    "content": "<?php\n/**\n * Students meta box for Courses & Memberships\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 3.0.0\n * @version 3.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Students class\n *\n * Add & remove students.\n *\n * @since 3.0.0\n * @version 3.13.0\n */\nclass LLMS_Meta_Box_Students extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Capability to check in order to display the metabox to the user\n\t *\n\t * @var    string\n\t * @since  3.13.0\n\t */\n\tpublic $capability = 'view_lifterlms_reports';\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @return void\n\t * @since  3.0.0\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-students';\n\t\t$this->title    = __( 'Student Management', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'course',\n\t\t\t'llms_membership',\n\t\t);\n\t\t$this->priority = 'default';\n\t}\n\n\t/**\n\t * Unused with our custom metabox output\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Custom metabox output function\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.4.0\n\t */\n\tpublic function output() {\n\n\t\t$screen = get_current_screen();\n\n\t\tif ( 'add' === $screen->action ) {\n\n\t\t\tesc_html_e( 'You must publish this post before you can manage students.', 'lifterlms' );\n\n\t\t} else {\n\n\t\t\tglobal $post;\n\n\t\t\tllms_get_template(\n\t\t\t\t'admin/post-types/students.php',\n\t\t\t\tarray(\n\t\t\t\t\t'post_id' => $post->ID,\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php",
    "content": "<?php\n/**\n * Product Visibility Settings meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since 3.6.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Meta_Box_Visibility class\n *\n * Adds radios to the publishing misc. actions box for courses and memberships.\n *\n * @since 3.6.0\n * @since 3.35.0 Sanitize `$_POST` data and add nonce verification.\n */\nclass LLMS_Meta_Box_Visibility {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.6.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'post_submitbox_misc_actions', array( $this, 'output' ) );\n\t\tadd_action( 'save_post_course', array( $this, 'save' ), 10, 1 );\n\t\tadd_action( 'save_post_llms_membership', array( $this, 'save' ), 10, 1 );\n\t}\n\n\t/**\n\t * Output HTML for the settings\n\t *\n\t * @since  3.6.0\n\t * @since 3.35.0 Add nonce verification.\n\t *\n\t * @return   void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tif ( ! in_array( $post->post_type, array( 'course', 'llms_membership' ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$product    = new LLMS_Product( $post );\n\t\t$visibility = $product->get_catalog_visibility();\n\t\t$options    = llms_get_product_visibility_options();\n\t\t$name       = isset( $options[ $visibility ] ) ? $options[ $visibility ] : $visibility;\n\t\t?>\n\t\t<div class=\"misc-pub-section\" id=\"llms-catalog-visibility\">\n\n\t\t<span style=\"color:#82878c;\" class=\"dashicons dashicons-welcome-view-site\"></span>\n\n\t\t\t<?php esc_html_e( 'Catalog visibility:', 'lifterlms' ); ?> <strong id=\"llms-catalog-visibility-display\"><?php echo esc_html( $name ); ?></strong>\n\n\t\t\t<a href=\"#llms-catalog-visibility\" class=\"llms-edit-catalog-visibility hide-if-no-js\"><?php esc_html_e( 'Edit', 'lifterlms' ); ?></a>\n\n\t\t\t<div id=\"llms-catalog-visibility-select\" class=\"hide-if-js\">\n\n\t\t\t\t<p><?php printf( esc_html__( 'Choose the visibility of the %s in your catalog. It will always be available directly.', 'lifterlms' ), esc_html( $product->get_post_type_label() ) ); ?></p>\n\t\t\t\t<?php foreach ( $options as $name => $label ) : ?>\n\t\t\t\t\t<input data-label=\"<?php echo esc_attr( $label ); ?>\" id=\"_llms_visibility_<?php echo esc_attr( $name ); ?>\" name=\"_llms_visibility\" type=\"radio\" value=\"<?php echo esc_attr( $name ); ?>\" <?php checked( $visibility, $name ); ?> />\n\t\t\t\t\t<label for=\"_llms_visibility_<?php echo esc_attr( $name ); ?>\" class=\"selectit\"><?php echo esc_attr( $label ); ?></label><br>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t<p>\n\t\t\t\t\t<a href=\"#llms-catalog-visibility\" class=\"llms-save-catalog-visibility hide-if-no-js button\"><?php esc_html_e( 'OK', 'lifterlms' ); ?></a>\n\t\t\t\t\t<a href=\"#llms-catalog-visibility\" class=\"llms-cancel-catalog-visibility hide-if-no-js\"><?php esc_html_e( 'Cancel', 'lifterlms' ); ?></a>\n\t\t\t\t</p>\n\n\t\t\t\t<?php wp_nonce_field( 'llms-catalog-visibility-nonce', 'llms_catalog_visibility_nonce' ); ?>\n\n\t\t\t</div>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Save the settings\n\t *\n\t * @since 3.6.0\n\t * @since 3.35.0 Sanitize `$_POST` data and verify nonce.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id WP Post ID.\n\t * @return void\n\t */\n\tpublic function save( $post_id ) {\n\n\t\tif ( ! isset( $_REQUEST['llms_catalog_visibility_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['llms_catalog_visibility_nonce'] ) ), 'llms-catalog-visibility-nonce' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$visibility = llms_filter_input_sanitize_string( INPUT_POST, '_llms_visibility' );\n\t\tif ( ! $visibility ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$product = new LLMS_Product( $post_id );\n\t\t$product->set_catalog_visibility( $visibility );\n\t}\n}\n\nreturn new LLMS_Meta_Box_Visibility();\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php",
    "content": "<?php\n/**\n * Meta box Voucher Export\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since Unknown\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta box Voucher Export class\n *\n * @since Unknown\n * @since 3.30.3 Fixed typo in export content-disposition header.\n * @since 3.35.0 Sanitize $_POST data, fix issue preventing emails from being properly sent.\n */\nclass LLMS_Meta_Box_Voucher_Export {\n\n\n\tpublic static $prefix = '_';\n\n\tpublic function __construct() {}\n\n\t/**\n\t * Function to field WP::output() method call\n\t * Passes output instruction to parent\n\t *\n\t * @param    object $post  WP global post object\n\t * @return   void\n\t * @since    ??\n\t * @version  3.24.0\n\t */\n\tpublic static function output( $post ) {\n\n\t\tglobal $post;\n\t\tif ( 'publish' !== $post->post_status ) {\n\t\t\tesc_html_e( 'You need to publish this post before you can generate a CSV.', 'lifterlms' );\n\t\t\treturn;\n\t\t}\n\t\t?>\n\t\t<div class=\"llms-voucher-export-wrapper\" id=\"llms-form-wrapper\">\n\n\t\t\t<div class=\"llms-voucher-export-type\">\n\t\t\t\t<input type=\"radio\" name=\"llms_voucher_export_type\" id=\"vouchers_only_type\" value=\"vouchers\">\n\t\t\t\t<label for=\"vouchers_only_type\"><strong><?php esc_html_e( 'Vouchers only', 'lifterlms' ); ?></strong></label>\n\t\t\t\t<p><?php esc_html_e( 'Generates a CSV of voucher codes, uses, and remaining uses.', 'lifterlms' ); ?></p>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-voucher-export-type\">\n\t\t\t\t<input type=\"radio\" name=\"llms_voucher_export_type\" id=\"redeemed_codes_type\" value=\"redeemed\">\n\t\t\t\t<label for=\"redeemed_codes_type\"><strong><?php esc_html_e( 'Redeemed codes', 'lifterlms' ); ?></strong></label>\n\t\t\t\t<p><?php esc_html_e( 'Generated a CSV of student emails, redemption date, and used code.', 'lifterlms' ); ?></p>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-voucher-email-wrapper\">\n\t\t\t\t<input type=\"checkbox\" name=\"llms_voucher_export_send_email\" id=\"llms_voucher_export_send_email\"\n\t\t\t\t\t\tvalue=\"true\">\n\t\t\t\t<label for=\"llms_voucher_export_send_email\"><?php esc_html_e( 'Email CSV', 'lifterlms' ); ?></label>\n\t\t\t\t<input type=\"text\" placeholder=\"Email\" name=\"llms_voucher_export_email\">\n\t\t\t\t<p><?php esc_html_e( 'Send to multiple emails by separating emails addresses with commas.', 'lifterlms' ); ?></p>\n\t\t\t</div>\n\n\t\t\t<button type=\"submit\" name=\"llms_generate_export\" value=\"generate\" class=\"button-primary\"><?php esc_html_e( 'Generate Export', 'lifterlms' ); ?></button>\n\t\t\t<?php wp_nonce_field( 'lifterlms_csv_export_data', 'lifterlms_export_nonce' ); ?>\n\t\t\t<div class=\"clear\"></div>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Export vouchers.\n\t *\n\t * @since Unknown.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return [type] [description]\n\t */\n\tpublic static function export() {\n\n\t\tif ( empty( llms_filter_input( INPUT_POST, 'llms_generate_export' ) ) || ! isset( $_REQUEST['lifterlms_export_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_export_nonce'] ) ), 'lifterlms_csv_export_data' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$type = llms_filter_input( INPUT_POST, 'llms_voucher_export_type' );\n\t\tif ( ! empty( $type ) ) {\n\n\t\t\tif ( 'vouchers' === $type || 'redeemed' === $type ) {\n\n\t\t\t\t// Export CSV.\n\n\t\t\t\t$csv       = array();\n\t\t\t\t$file_name = '';\n\n\t\t\t\tglobal $post;\n\t\t\t\t$voucher = new LLMS_Voucher( $post->ID );\n\n\t\t\t\tswitch ( $type ) {\n\t\t\t\t\tcase 'vouchers':\n\t\t\t\t\t\t$voucher = new LLMS_Voucher( $post->ID );\n\t\t\t\t\t\t$codes   = $voucher->get_voucher_codes( 'ARRAY_A' );\n\n\t\t\t\t\t\tif ( ! $codes ) {\n\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t * @todo  error handling here\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tforeach ( $codes as $k => $v ) {\n\t\t\t\t\t\t\tunset( $codes[ $k ]['id'] );\n\t\t\t\t\t\t\tunset( $codes[ $k ]['voucher_id'] );\n\t\t\t\t\t\t\t$codes[ $k ]['count']   = $codes[ $k ]['redemption_count'];\n\t\t\t\t\t\t\t$codes[ $k ]['used']    = $codes[ $k ]['used'];\n\t\t\t\t\t\t\t$codes[ $k ]['created'] = $codes[ $k ]['created_at'];\n\t\t\t\t\t\t\t$codes[ $k ]['updated'] = $codes[ $k ]['updated_at'];\n\t\t\t\t\t\t\tunset( $codes[ $k ]['redemption_count'] );\n\t\t\t\t\t\t\tunset( $codes[ $k ]['created_at'] );\n\t\t\t\t\t\t\tunset( $codes[ $k ]['updated_at'] );\n\t\t\t\t\t\t\tunset( $codes[ $k ]['is_deleted'] );\n\n\t\t\t\t\t\t}\n\t\t\t\t\t\t$csv = self::array_to_csv( $codes );\n\n\t\t\t\t\t\t$file_name = 'vouchers.csv';\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'redeemed':\n\t\t\t\t\t\t$redeemed_codes = $voucher->get_redeemed_codes( 'ARRAY_A' );\n\n\t\t\t\t\t\tif ( ! $redeemed_codes ) {\n\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t * @todo  error handling here\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tforeach ( $redeemed_codes as $k => $v ) {\n\t\t\t\t\t\t\tunset( $redeemed_codes[ $k ]['id'] );\n\t\t\t\t\t\t\tunset( $redeemed_codes[ $k ]['code_id'] );\n\t\t\t\t\t\t\tunset( $redeemed_codes[ $k ]['voucher_id'] );\n\t\t\t\t\t\t\tunset( $redeemed_codes[ $k ]['redemption_count'] );\n\t\t\t\t\t\t\tunset( $redeemed_codes[ $k ]['user_id'] );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$csv = self::array_to_csv( $redeemed_codes );\n\n\t\t\t\t\t\t$file_name = 'redeemed_codes.csv';\n\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t$send_email = llms_parse_bool( llms_filter_input( INPUT_POST, 'llms_voucher_export_send_email' ) );\n\t\t\t\tif ( $send_email ) {\n\n\t\t\t\t\t// Send email.\n\t\t\t\t\t$email_text = trim( llms_filter_input_sanitize_string( INPUT_POST, 'llms_voucher_export_email' ) );\n\t\t\t\t\tif ( ! empty( $email_text ) ) {\n\n\t\t\t\t\t\t$emails = array_filter( array_map( 'is_email', array_map( 'trim', explode( ',', $email_text ) ) ) );\n\n\t\t\t\t\t\tif ( ! empty( $emails ) ) {\n\n\t\t\t\t\t\t\t$voucher = new LLMS_Voucher( $post->ID );\n\n\t\t\t\t\t\t\tself::send_email( $csv, $emails, $voucher->get_voucher_title() );\n\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tself::download_csv( $csv, $file_name );\n\t\t\t}// End if().\n\t\t}// End if().\n\t}\n\n\tpublic static function array_to_csv( $data, $delimiter = ',', $enclosure = '\"' ) {\n\n\t\t$handle   = fopen( 'php://temp', 'r+' );\n\t\t$contents = '';\n\n\t\t$names = array();\n\n\t\tforeach ( $data[0] as $name => $item ) {\n\t\t\t$names[] = $name;\n\t\t}\n\n\t\tfputcsv( $handle, $names, $delimiter, $enclosure );\n\n\t\tforeach ( $data as $line ) {\n\t\t\tfputcsv( $handle, $line, $delimiter, $enclosure );\n\t\t}\n\t\trewind( $handle );\n\t\twhile ( ! feof( $handle ) ) {\n\t\t\t$contents .= fread( $handle, 8192 );\n\t\t}\n\t\tfclose( $handle );\n\t\treturn $contents;\n\t}\n\n\t/**\n\t * Serve the CSV as an attachment to be downloaded.\n\t *\n\t * @since Unknown\n\t * @since 3.30.3 Fixed typo in export content-disposition header.\n\t *\n\t * @param string $csv CSV content string.\n\t * @param string $name Filename.\n\t * @return void\n\t */\n\tpublic static function download_csv( $csv, $name ) {\n\n\t\theader( 'Content-Type: application/csv' );\n\t\theader( 'Content-Disposition: attachment; filename=\"' . $name . '\";' );\n\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV output.\n\t\techo $csv;\n\t\texit;\n\t}\n\n\tpublic static function send_email( $csv, $emails, $title ) {\n\n\t\t$subject = 'Your LifterLMS Voucher Export';\n\t\t$message = 'Please find the attached voucher csv export for ' . $title . '.';\n\n\t\t// Create temp file.\n\t\t$temp = tempnam( '/tmp', 'vouchers' );\n\n\t\t// Write CSV.\n\t\t$handle = fopen( $temp, 'w' );\n\t\tfwrite( $handle, $csv );\n\n\t\t// Prepare filename.\n\t\t$temp_data     = stream_get_meta_data( $handle );\n\t\t$temp_filename = $temp_data['uri'];\n\n\t\t$new_filename = substr_replace( $temp_filename, '', 13 ) . '.csv';\n\t\trename( $temp_filename, $new_filename );\n\n\t\t// Send email/s.\n\t\t$mail = wp_mail( $emails, $subject, $message, '', $new_filename );\n\n\t\t// And remove it.\n\t\tfclose( $handle );\n\t\tunlink( $new_filename );\n\n\t\treturn $mail;\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php",
    "content": "<?php\n/**\n * Vouchers meta box\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Classes\n *\n * @since Unknown\n * @version 7.1.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Vouchers Meta box class\n *\n * @since Unknown\n * @since 3.32.0 Vouchers can now be restricted also to a draft or scheduled Course/Membership.\n * @since 3.35.0 Sanitize `$_POST` data; add placeholder text.\n * @since 3.36.0 Remove superfluous code.\n * @since 4.0.0 Remove usage of `LLMS_Svg`.\n * @since 7.1.3 Added `esc_attr()` and `esc_html()` for HTML attributes and HTML.\n */\nclass LLMS_Meta_Box_Voucher extends LLMS_Admin_Metabox {\n\n\t/**\n\t * Configure the metabox settings\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function configure() {\n\n\t\t$this->id       = 'lifterlms-voucher';\n\t\t$this->title    = __( 'Voucher Settings', 'lifterlms' );\n\t\t$this->screens  = array(\n\t\t\t'llms_voucher',\n\t\t);\n\t\t$this->priority = 'high';\n\t}\n\n\t/**\n\t * Builds array of metabox options.\n\t *\n\t * Array is called in output method to display options.\n\t * Appropriate fields are generated based on type.\n\t *\n\t * @since 3.0.0\n\t * @since 3.32.0 Vouchers can now be restricted also to a draft or scheduled Course/Membership\n\t * @since 3.35.0 Add relevant placeholders on the course/membership select fields.\n\t *\n\t * @return array\n\t */\n\tpublic function get_fields() {\n\n\t\t$voucher = new LLMS_Voucher( $this->post->ID );\n\n\t\t$selected_couses      = $voucher->get_products( 'course' );\n\t\t$selected_memberships = $voucher->get_products( 'llms_membership' );\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'General', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'post-type'     => 'course',\n\t\t\t\t\t\t\t'post-statuses' => 'publish,draft,future',\n\t\t\t\t\t\t\t'placeholder'   => __( 'Courses', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'label'           => __( 'Courses', 'lifterlms' ),\n\t\t\t\t\t\t'id'              => $this->prefix . 'voucher_courses',\n\t\t\t\t\t\t'class'           => 'input-full llms-select2-post',\n\t\t\t\t\t\t'selected'        => $selected_couses,\n\t\t\t\t\t\t'value'           => llms_make_select2_post_array( $selected_couses ),\n\t\t\t\t\t\t'multi'           => true,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t\t'post-type'     => 'llms_membership',\n\t\t\t\t\t\t\t'post-statuses' => 'publish,draft,future',\n\t\t\t\t\t\t\t'placeholder'   => __( 'Memberships', 'lifterlms' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t\t'label'           => __( 'Membership', 'lifterlms' ),\n\t\t\t\t\t\t'id'              => $this->prefix . 'voucher_membership',\n\t\t\t\t\t\t'class'           => 'input-full llms-select2-post',\n\t\t\t\t\t\t'selected'        => $selected_memberships,\n\t\t\t\t\t\t'value'           => llms_make_select2_post_array( $selected_memberships ),\n\t\t\t\t\t\t'multi'           => true,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t\t\t'label' => __( 'Codes', 'lifterlms' ),\n\t\t\t\t\t\t'id'    => '',\n\t\t\t\t\t\t'class' => '',\n\t\t\t\t\t\t'value' => self::codes_section_html(),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'  => __( 'Redemptions', 'lifterlms' ),\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t\t\t'label' => __( 'Redemptions', 'lifterlms' ),\n\t\t\t\t\t\t'id'    => '',\n\t\t\t\t\t\t'class' => '',\n\t\t\t\t\t\t'value' => self::redemption_section_html(),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the HTML for the codes area.\n\t *\n\t * @since Unknown\n\t * @since 4.0.0 Replace SVG delete icon with a dashicon.\n\t * @since 7.1.3 Added `esc_attr()` for HTML attributes.\n\t *\n\t * @return string\n\t */\n\tprivate function codes_section_html() {\n\n\t\tglobal $post;\n\t\t$voucher = new LLMS_Voucher( $post->ID );\n\t\t$codes   = $voucher->get_voucher_codes();\n\n\t\tob_start(); ?>\n\t\t<div class=\"llms-voucher-codes-wrapper\" id=\"llms-form-wrapper\">\n\t\t\t<table>\n\n\t\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th></th>\n\t\t\t\t\t<th>Code</th>\n\t\t\t\t\t<th>Uses</th>\n\t\t\t\t\t<th>Actions</th>\n\t\t\t\t</tr>\n\t\t\t\t</thead>\n\n\t\t\t\t<script>var delete_icon = '<span class=\"dashicons dashicons-no\"></span><span class=\"screen-reader-text\"><?php echo esc_js( __( 'Delete', 'lifterlms' ) ); ?></span>';</script>\n\n\t\t\t\t<tbody id=\"llms_voucher_tbody\">\n\t\t\t\t<?php\n\t\t\t\tif ( ! empty( $codes ) ) :\n\t\t\t\t\tforeach ( $codes as $code ) :\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td></td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<input type=\"text\" maxlength=\"20\" placeholder=\"Code\" value=\"<?php echo esc_attr( $code->code ); ?>\" name=\"llms_voucher_code[]\">\n\t\t\t\t\t\t\t\t<input type=\"hidden\" name=\"llms_voucher_code_id[]\" value=\"<?php echo esc_attr( $code->id ); ?>\">\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<span><?php echo esc_html( $code->used ); ?> / </span><input type=\"number\" min=\"1\" value=\"<?php echo esc_attr( $code->redemption_count ); ?>\" placeholder=\"Uses\" class=\"llms-voucher-uses\" name=\"llms_voucher_uses[]\">\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<a href=\"#\" data-id=\"<?php echo esc_attr( $code->id ); ?>\" class=\"llms-voucher-delete\">\n\t\t\t\t\t\t\t\t\t<span class=\"dashicons dashicons-no\"></span><span class=\"screen-reader-text\"><?php echo esc_html__( 'Delete', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<?php\n\t\t\t\t\tendforeach;\n\t\t\t\tendif;\n\t\t\t\t?>\n\t\t\t\t</tbody>\n\n\t\t\t</table>\n\n\t\t\t<div class=\"llms-voucher-add-codes\">\n\t\t\t\t<p>Add <input type=\"number\" max=\"50\" placeholder=\"#\" id=\"llms_voucher_add_quantity\"> new code(s) with <input\n\t\t\t\t\t\ttype=\"number\" placeholder=\"#\" id=\"llms_voucher_add_uses\"> use(s) per code\n\t\t\t\t\t<button id=\"llms_voucher_add_codes\" class=\"button-primary\">Add</button>\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<input type=\"hidden\" name=\"delete_ids\" id=\"delete_ids\">\n\t\t</div>\n\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Retrieve the HTML for the redemption area.\n\t *\n\t * @since Unknown\n\t * @since 7.1.3 Added `esc_html()` for HTML output.\n\t *\n\t * @return string\n\t */\n\tprivate function redemption_section_html() {\n\n\t\tglobal $post;\n\n\t\t$pid            = $post->ID;\n\t\t$voucher        = new LLMS_Voucher( $pid );\n\t\t$redeemed_codes = $voucher->get_redeemed_codes();\n\t\tob_start();\n\t\t?>\n\n\t\t<div class=\"llms-voucher-redemption-wrapper\">\n\t\t\t<table>\n\n\t\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>Name</th>\n\t\t\t\t\t<th>Email</th>\n\t\t\t\t\t<th>Redemption Date</th>\n\t\t\t\t\t<th>Code</th>\n\t\t\t\t</tr>\n\t\t\t\t</thead>\n\n\t\t\t\t<tbody>\n\t\t\t\t<?php\n\t\t\t\tif ( ! empty( $redeemed_codes ) ) :\n\t\t\t\t\tforeach ( $redeemed_codes as $redeemed_code ) :\n\n\t\t\t\t\t\t$user = get_user_by( 'id', $redeemed_code->user_id );\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td><?php echo esc_html( $user->data->display_name ); ?></td>\n\t\t\t\t\t\t\t<td><?php echo esc_html( $user->data->user_email ); ?></td>\n\t\t\t\t\t\t\t<td><?php echo esc_html( $redeemed_code->redemption_date ); ?></td>\n\t\t\t\t\t\t\t<td><?php echo esc_html( $redeemed_code->code ); ?></td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<?php\n\t\t\t\t\tendforeach;\n\t\t\t\tendif;\n\t\t\t\t?>\n\t\t\t\t</tbody>\n\n\t\t\t</table>\n\t\t</div>\n\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Save method\n\t *\n\t * Cleans variables and saves using `update_post_meta()`.\n\t *\n\t * @version 3.0.0\n\t * @version 3.35.0 Sanitize `$_POST` data with `llms_filter_input()`.\n\t * @version 3.36.0 Remove superfluous code.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id [id of post object]\n\t * @return boolean|null\n\t */\n\tpublic function save( $post_id ) {\n\n\t\tif ( ! empty( llms_filter_input( INPUT_POST, 'llms_generate_export' ) ) || ! isset( $_REQUEST['lifterlms_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_meta_nonce'] ) ), 'lifterlms_save_data' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Codes save.\n\t\t$codes = array();\n\n\t\t$llms_codes           = llms_filter_input_sanitize_string( INPUT_POST, 'llms_voucher_code', array( FILTER_REQUIRE_ARRAY ) );\n\t\t$llms_uses            = llms_filter_input( INPUT_POST, 'llms_voucher_uses', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\t$llms_voucher_code_id = llms_filter_input( INPUT_POST, 'llms_voucher_code_id', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\n\t\t$voucher = new LLMS_Voucher( $post_id );\n\n\t\tif ( isset( $llms_codes ) && ! empty( $llms_codes ) && isset( $llms_uses ) && ! empty( $llms_uses ) ) {\n\n\t\t\tforeach ( $llms_codes as $k => $code ) {\n\t\t\t\tif ( isset( $code ) && ! empty( $code ) && isset( $llms_uses[ $k ] ) && ! empty( $llms_uses[ $k ] ) ) {\n\n\t\t\t\t\tif ( isset( $llms_voucher_code_id[ $k ] ) ) {\n\n\t\t\t\t\t\t$data = array(\n\t\t\t\t\t\t\t'code'             => $code,\n\t\t\t\t\t\t\t'redemption_count' => intval( $llms_uses[ $k ] ),\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif ( intval( $llms_voucher_code_id[ $k ] ) ) {\n\t\t\t\t\t\t\t$data['id'] = intval( $llms_voucher_code_id[ $k ] );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$codes[] = $data;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( ! empty( $codes ) ) {\n\t\t\tforeach ( $codes as $code ) {\n\n\t\t\t\tif ( isset( $code['id'] ) ) {\n\t\t\t\t\t$voucher->update_voucher_code( $code );\n\t\t\t\t} else {\n\t\t\t\t\t$voucher->save_voucher_code( $code );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Courses and membership save.\n\t\t$products = array();\n\n\t\tforeach ( array( 'courses', 'membership' ) as $type ) {\n\t\t\t$list = llms_filter_input( INPUT_POST, '_llms_voucher_' . $type, FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\t\tforeach ( (array) $list as $item ) {\n\t\t\t\t$products[] = absint( $item );\n\t\t\t}\n\t\t}\n\n\t\t// Remove old products.\n\t\t$voucher->delete_products();\n\n\t\t// Save new ones.\n\t\tif ( ! empty( $products ) ) {\n\t\t\tforeach ( $products as $item ) {\n\t\t\t\t$voucher->save_product( $item );\n\t\t\t}\n\t\t}\n\n\t\t// Set old codes as deleted.\n\t\t$ids = llms_filter_input( INPUT_POST, 'delete_ids' );\n\t\tif ( $ids ) {\n\t\t\t$delete_ids = array_map( 'absint', explode( ',', $ids ) );\n\n\t\t\tif ( ! empty( $delete_ids ) ) {\n\t\t\t\tforeach ( $delete_ids as $id ) {\n\t\t\t\t\t$voucher->delete_voucher_code( $id );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.basic.editor.php",
    "content": "<?php\n/**\n * Meta box Field: Basic Editor\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since 8.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nclass LLMS_Metabox_Basic_Editor_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tparent::output(); ?>\n\n\t\t<div\n\t\t\tdata-name=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tclass=\"llms-editable-title llms-basic-editor llms-input-formatting\"\n\t\t\tdata-attribute=\"title\"\n\t\t\t<?php if ( array_key_exists( 'placeholder', $this->field ) && $this->field['placeholder'] ) : ?>\n\t\t\t\tdata-placeholder=\"<?php echo esc_attr( $this->field['placeholder'] ); ?>\"\n\t\t\t<?php endif; ?>\n\t\t><?php echo wp_kses( $this->meta, LLMS_ALLOWED_HTML_PRICES ); ?></div>\n\n\t\t<input type=\"hidden\" name=\"<?php echo esc_attr( $this->field['id'] ); ?>\" value=\"<?php echo esc_attr( $this->meta ); ?>\" />\n\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.button.php",
    "content": "<?php\n/**\n * Meta box Field: Button\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Button_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Button_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output(); ?>\n\n\t\t<button\n\t\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\t\tclass=\"<?php echo esc_attr( $this->field['class'] ); ?>\"\n\t\t\t>\n\t\t\t\t<?php echo esc_attr( $this->field['value'] ); ?>\n\t\t\t</button>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.checkbox.php",
    "content": "<?php\n/**\n * Meta box Field: Checkbox\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Checkbox_Field class\n *\n * @since Unknown\n * @since 4.0.0 Remove reliance on `LLMS_Svg` class.\n */\nclass LLMS_Metabox_Checkbox_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $_field Array containing information about field\n\t * @return void\n\t */\n\tpublic function __construct( $_field ) {\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @since Unknown\n\t * @since 4.0.0 Remove reliance on `LLMS_Svg` class, refactor to closely match appearance of WP core block editor toggles.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output(); ?>\n\n\t\t<div class=\"llms-switch d-1of4 t-1of4 m-1of2\">\n\t\t\t<input\n\t\t\t\t<?php if ( isset( $this->field['controls'] ) ) : ?>\n\t\t\t\tdata-controls=\"<?php echo esc_attr( $this->field['controls'] ); ?>\"\n\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php if ( isset( $this->field['is_controller'] ) ) : ?>\n\t\t\t\tdata-is-controller=\"true\"\n\t\t\t\t<?php endif; ?>\n\t\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\t\tclass=\"llms-toggle llms-toggle-round\"\n\t\t\t\ttype=\"checkbox\"\n\t\t\t\tvalue=\"<?php echo esc_attr( $this->field['value'] ); ?>\"\n\t\t\t\t<?php echo ( $this->field['value'] === $this->meta ) ? 'checked' : ''; ?>\n\t\t\t/>\n\n\t\t\t<label for=\"<?php echo esc_attr( $this->field['id'] ); ?>\"></label>\n\n\t\t</div>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.color.php",
    "content": "<?php\n/**\n * Meta box Field: Color picker\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Color_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Color_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\tif ( ! $this->meta ) {\n\t\t\t$this->meta = $this->field['value'];\n\t\t}\n\t\t?>\n\t\t<input class=\"color-picker\" type=\"text\" name=\"<?php echo esc_attr( $this->field['id'] ); ?>\" id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" value=\"<?php echo esc_attr( $this->meta ); ?>\" data-default-color=\"<?php echo esc_attr( $this->field['value'] ); ?>\"/>\n\t\t\t<br /><span class=\"description\"><?php echo wp_kses_post( $this->field['desc'] ); ?></span>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.custom.html.php",
    "content": "<?php\n/**\n * Meta box Field: Custom HTML\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Custom_Html_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Custom_Html_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in the field value.\n\t\techo $this->field['value'];\n\t\tparent::close_output();\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.date.php",
    "content": "<?php\n/**\n * Meta box Field: Date Picker Field\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since  Unknown\n * @version  3.11.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Date_Field class\n *\n * Pass in 'llms-datepicker' for the class for the field to automatically use jQuery datepicker.\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Date_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param    array $_field Array containing information about field\n\t * @since    ??\n\t * @version  3.11.0\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$_field = wp_parse_args(\n\t\t\t$_field,\n\t\t\tarray(\n\t\t\t\t'date_format'        => 'mm/dd/yy', // jQuery datepicker formats (http://api.jqueryui.com/datepicker/#utility-formatDate).\n\t\t\t\t'date_max'           => '',\n\t\t\t\t'date_min'           => '',\n\t\t\t\t'date_displayformat' => $this->php_to_jquery_date_format( get_option( 'date_format' ) ),\n\t\t\t)\n\t\t);\n\n\t\t$this->field = $_field;\n\t}\n\n\tfunction php_to_jquery_date_format( $php_format ) {\n\t\t$replacements = array(\n\t\t\t// Day\n\t\t\t'd' => 'dd',\n\t\t\t'D' => 'D',\n\t\t\t'j' => 'd',\n\t\t\t'l' => 'DD',\n\t\t\t// Month\n\t\t\t'm' => 'mm',\n\t\t\t'n' => 'm',\n\t\t\t'M' => 'M',\n\t\t\t'F' => 'MM',\n\t\t\t// Year\n\t\t\t'Y' => 'yy',\n\t\t\t'y' => 'y',\n\t\t);\n\n\t\treturn strtr( $php_format, $replacements );\n\t}\n\n\tfunction jquery_date_to_php_format( $js_format ) {\n\t\t// Mapping of jQuery UI Datepicker tokens to PHP date format tokens\n\t\t$replacements = array(\n\t\t\t// Year tokens\n\t\t\t'yy' => 'Y',  // 4-digit year\n\t\t\t'y'  => 'y',  // 2-digit year\n\n\t\t\t// Month tokens\n\t\t\t'mm' => 'm',  // Month with leading zero\n\t\t\t'm'  => 'n',  // Month without leading zero\n\t\t\t'MM' => 'F',  // Full month name\n\t\t\t'M'  => 'M',  // Short month name\n\n\t\t\t// Day tokens\n\t\t\t'dd' => 'd',  // Day with leading zero\n\t\t\t'd'  => 'j',  // Day without leading zero\n\t\t\t'DD' => 'l',  // Full day name\n\t\t\t'D'  => 'D',   // Short day name\n\t\t);\n\n\t\t// Sort keys descending by length to avoid partial replacements.\n\t\tuksort(\n\t\t\t$replacements,\n\t\t\tfunction ( $a, $b ) {\n\t\t\t\treturn strlen( $b ) - strlen( $a );\n\t\t\t}\n\t\t);\n\n\t\treturn strtr( $js_format, $replacements );\n\t}\n\n\t/**\n\t * Construct data attributes for the field\n\t * sets up jQuery datepicker\n\t *\n\t * @return   [type]     [description]\n\t * @since    3.11.0\n\t * @version  3.11.0\n\t */\n\tpublic function get_data_attrs() {\n\n\t\t$attrs = array(\n\t\t\t'date_format' => 'data-format',\n\t\t\t'date_max'    => 'data-max-date',\n\t\t\t'date_min'    => 'data-min-date',\n\t\t);\n\n\t\t$data_attrs = '';\n\t\tforeach ( $attrs as $key => $attr ) {\n\t\t\t$val = ! empty( $this->field[ $key ] ) ? $this->field[ $key ] : null;\n\t\t\tif ( $val ) {\n\t\t\t\t$data_attrs .= sprintf( '%1$s=\"%2$s\"', $attr, $val );\n\t\t\t}\n\t\t}\n\t\treturn $data_attrs;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @since    ??\n\t * @version  3.11.0\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output(); ?>\n\t\t<?php\n\t\t// Convert the meta value into display format.\n\t\t$js_display_date = $this->meta;\n\t\tif ( ! empty( $this->meta ) ) {\n\t\t\t$meta_date = DateTime::createFromFormat( $this->jquery_date_to_php_format( $this->field['date_format'] ), $this->meta );\n\n\t\t\tif ( ! $meta_date ) {\n\t\t\t\terror_log( sprintf( 'Meta value %s for field %s is not in the expected format %s', $this->meta, $this->field['id'], $this->field['date_format'] ) );\n\t\t\t}\n\n\t\t\tif ( $meta_date ) {\n\t\t\t\t$js_display_date = $meta_date->format( $this->jquery_date_to_php_format( $this->field['date_displayformat'] ) );\n\t\t\t}\n\t\t}\n\t\t?>\n\t\t<input type=\"hidden\"\n\t\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>_alt_datefield\"\n\t\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\t\tvalue=\"<?php echo ! empty( $this->meta ) ? esc_attr( $this->meta ) : ''; ?>\"\n\t\t\t<?php if ( isset( $this->field['required'] ) && $this->field['required'] ) : ?>\n\t\t\t\trequired=\"required\"\n\t\t\t<?php endif; ?>\n\t\t/>\n\t\t<input type=\"text\"\n\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>_datepicker\"\n\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tclass=\"<?php echo esc_attr( $this->field['class'] ); ?>\"\n\t\t\tvalue=\"<?php echo ! empty( $this->meta ) ? esc_attr( $js_display_date ) : ''; ?>\"\n\t\t\tsize=\"30\"\n\t\t\t<?php if ( isset( $this->field['required'] ) && $this->field['required'] ) : ?>\n\t\t\trequired=\"required\"\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( isset( $this->field['placeholder'] ) ) : ?>\n\t\t\tplaceholder=\"<?php echo esc_attr( $this->field['placeholder'] ); ?>\"\n\t\t\t<?php endif; ?>\n\t\t\tdata-format=\"<?php echo esc_attr( $this->field['date_displayformat'] ); ?>\"\n\t\t\tdata-alt-format=\"<?php echo esc_attr( $this->field['date_format'] ); ?>\"\n\t\t\t<?php if ( ! empty( $this->field['date_max'] ) ) : ?>\n\t\t\t\tdata-max-date=\"<?php echo esc_attr( $this->field['date_max'] ); ?>\"\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( ! empty( $this->field['date_min'] ) ) : ?>\n\t\t\t\tdata-min-date=\"<?php echo esc_attr( $this->field['date_min'] ); ?>\"\n\t\t\t<?php endif; ?>\n\t\t\tdata-alt-field=\"#<?php echo esc_attr( $this->field['id'] ); ?>_alt_datefield\"\n\n\t\t/>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.editor.php",
    "content": "<?php\n/**\n * WP Editor meta box field\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * WP Editor meta box field class\n *\n * @since Unknown\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Metabox_Editor_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Array of editor arguments.\n\t *\n\t * @see _WP_Editors::parse_settings()\n\t * @var array\n\t * @since 3.11.0\n\t */\n\tpublic $settings;\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field    = $_field;\n\t\t$this->settings = isset( $this->field['settings'] ) ? $this->field['settings'] : array();\n\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\twp_editor( $this->meta, $this->field['id'], $this->settings );\n\n\t\tparent::close_output();\n\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.fields.php",
    "content": "<?php\n/**\n * Abstract Metabox_Field.\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since unknown\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Metabox_Field parent class.\n *\n * Contains base code for each of the Metabox Fields.\n *\n * @since unknown\n * @since 3.24.0 Unknown.\n */\nabstract class LLMS_Metabox_Field {\n\n\t/**\n\t * Global array used in class instance to store field information.\n\t *\n\t * @var array\n\t */\n\tpublic $field;\n\n\t/**\n\t * Global variable to contain meta information about $field.\n\t *\n\t * @var object\n\t */\n\tpublic $meta;\n\n\t/**\n\t * Outputs the head for each of the field types.\n\t *\n\t * @todo All the unset variables here should be defaulted somewhere else probably.\n\t *\n\t * @since unknown\n\t * @since 3.11.0 Unknown.\n\t * @since 6.0.0 Do not print empty labels; do not print the description block if both 'desc' and 'label' are empty.\n\t *               Avoid retrieving the meta from the db if passed.\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tif ( isset( $this->field['meta'] ) ) {\n\t\t\t$this->meta = $this->field['meta'];\n\t\t} elseif ( ( ! metadata_exists( 'post', $post->ID, $this->field['id'] ) || 'auto-draft' === $post->post_status ) && ! empty( $this->field['default'] ) ) {\n\t\t\t$this->meta = $this->field['default'];\n\t\t} else {\n\t\t\t$this->meta = self::get_post_meta( $post->ID, $this->field['id'] );\n\t\t}\n\n\t\tif ( ! isset( $this->field['group'] ) ) {\n\t\t\t$this->field['group'] = '';\n\t\t}\n\n\t\tif ( ! isset( $this->field['desc_class'] ) ) {\n\t\t\t$this->field['desc_class'] = '';\n\t\t}\n\n\t\tif ( ! isset( $this->field['desc'] ) ) {\n\t\t\t$this->field['desc'] = '';\n\t\t}\n\n\t\t$wrapper_classes   = array( 'llms-mb-list' );\n\t\t$wrapper_classes[] = $this->field['id'];\n\t\t$wrapper_classes[] = $this->field['type'];\n\t\t$wrapper_classes   = array_merge( $wrapper_classes, explode( ' ', $this->field['group'] ) );\n\n\t\t?>\n\t\t<li\n\t\t\tclass=\"<?php echo esc_attr( implode( ' ', $wrapper_classes ) ); ?>\"\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\techo isset( $this->field['controller'] ) ? ' data-controller=\"' . esc_attr( $this->field['controller'] ) . '\"' : '';\n\t\t\t\t\t\t\t\techo isset( $this->field['controller_value'] ) ? ' data-controller-value=\"' . esc_attr( $this->field['controller_value'] ) . '\"' : '';\n\t\t\t\t\t\t\t\t?>\n\t\t>\n\t\t<?php if ( ! empty( $this->field['desc'] ) || ! empty( $this->field['label'] ) ) : ?>\n\t\t\t<div class=\"description <?php echo esc_attr( $this->field['desc_class'] ); ?>\">\n\t\t\t<?php if ( ! empty( $this->field['label'] ) ) : ?>\n\t\t\t\t<label for=\"<?php echo esc_attr( $this->field['id'] ); ?>\"><?php echo esc_html( $this->field['label'] ); ?></label>\n\t\t\t<?php endif; ?>\n\t\t\t\t<?php echo wp_kses_post( $this->field['desc'] ); ?>\n\t\t\t\t<?php\n\t\t\t\tif ( isset( $this->field['required'] ) && $this->field['required'] ) :\n\t\t\t\t\t?>\n\t\t\t\t\t<em>(required)</em><?php endif; ?>\n\t\t\t</div>\n\t\t\t<?php\n\t\t\tendif;\n\t}\n\n\t/**\n\t * Outputs the tail for each of the field types.\n\t *\n\t * @since unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function close_output() {\n\n\t\techo '<div class=\"clear\"></div></li>';\n\t}\n\n\t/**\n\t * Set the default meta value of a field.\n\t *\n\t * @since 1.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param int    $post_id  WP Post ID.\n\t * @param string $field_id ID/name of the field.\n\t * @return mixed\n\t */\n\tpublic static function get_post_meta( $post_id, $field_id ) {\n\n\t\tif ( '_post_course_difficulty' === $field_id ) {\n\t\t\t$difficulties = wp_get_object_terms( $post_id, 'course_difficulty' );\n\n\t\t\tif ( $difficulties ) {\n\t\t\t\treturn $difficulties[0]->slug;\n\t\t\t}\n\t\t} else {\n\t\t\treturn get_post_meta( $post_id, $field_id, true );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.hidden.php",
    "content": "<?php\n/**\n * Meta box Field: Hidden.\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Hidden_Field class.\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Hidden_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor.\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * Outputs the Html for the given field.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tparent::output(); ?>\n\n\t\t<input\n\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\t<?php if ( isset( $this->field['required'] ) && $this->field['required'] ) : ?>\n\t\t\trequired=\"required\"\n\t\t\t<?php endif; ?>\n\t\t\ttype=\"hidden\"\n\t\t\tvalue=\"<?php echo esc_attr( $this->field['value'] ); ?>\"\n\t\t>\n\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.image.php",
    "content": "<?php\n/**\n * Meta box field: Image meta box field\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since ??\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Image meta box field class\n *\n * @since ??\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Metabox_Image_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t * @since    ??\n\t * @version  3.24.0\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\tif ( 'achievement_meta_box' === $this->field['section'] ) {\n\t\t\t$image = apply_filters( 'lifterlms_placeholder_img_src', llms()->plugin_url() . '/assets/images/optional_achievement.png' ); ?>\n\t\t\t<img id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" class=\"llms_achievement_default_image\" style=\"display:none\" src=\"<?php echo esc_url( $image ); ?>\">\n\t\t\t<?php\n\t\t\t$imgclass = 'llms_achievement_image';\n\t\t} else {\n\t\t\t$image = apply_filters( 'lifterlms_placeholder_img_src', llms()->plugin_url() . '/assets/images/optional_certificate.png' );\n\t\t\t?>\n\t\t\t<img id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" class=\"llms_certificate_default_image\" style=\"display:none\" src=\"<?php echo esc_url( $image ); ?>\">\n\t\t\t<?php\n\t\t\t$imgclass = 'llms_certificate_image';\n\t\t} // End if().\n\t\tif ( is_numeric( $this->meta ) ) {\n\t\t\t$image = wp_get_attachment_image_src( $this->meta, 'medium' );\n\t\t\t$image = $image[0];\n\t\t}\n\t\t?>\n\t\t\t\t<img src=\"<?php echo esc_url( $image ); ?>\" id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" class=\"<?php echo esc_attr( $imgclass ); ?>\" /><br />\n\t\t\t\t<input name=\"<?php echo esc_attr( $this->field['id'] ); ?>\" id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" type=\"hidden\" class=\"upload_<?php echo esc_attr( $this->field['class'] ); ?>_image\" type=\"text\" size=\"36\" name=\"ad_image\" value=\"<?php echo esc_attr( $this->meta ); ?>\" />\n\t\t\t\t<input id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" class=\"button <?php echo esc_attr( $this->field['class'] ); ?>_image_button\" type=\"button\" value=\"Upload Image\" />\n\t\t\t\t<small> <a href=\"#\" id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" class=\"llms_<?php echo esc_attr( $this->field['class'] ); ?>_clear_image_button\">Remove Image</a></small>\n\t\t\t\t<br /><span class=\"description\"><?php echo wp_kses_post( $this->field['desc'] ); ?></span>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.number.php",
    "content": "<?php\n/**\n * Meta box Field: Number\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Number_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Number_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t * @since    1.0.0\n\t * @version  3.16.0\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\t// Clear an invalid value, usually from a clone or import.\n\t\tif (\n\t\t\tis_numeric( $this->meta ) &&\n\t\t\tisset( $this->field['min'] ) &&\n\t\t\tis_numeric( $this->field['min'] ) &&\n\t\t\t$this->field['min'] > 0 &&\n\t\t\t$this->meta < $this->field['min'] ) {\n\t\t\t$this->meta = '';\n\t\t}\n\t\t?>\n\n\t\t<input type=\"number\"\n\t\t\t<?php if ( isset( $this->field['min'] ) ) : ?>\n\t\t\tmin=\"<?php echo esc_attr( $this->field['min'] ); ?>\"\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( isset( $this->field['max'] ) ) : ?>\n\t\t\tmax=\"<?php echo esc_attr( $this->field['max'] ); ?>\"\n\t\t\t<?php endif; ?>\n\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tclass=\"<?php echo esc_attr( $this->field['class'] ); ?>\"\n\t\t\tvalue=\"<?php echo esc_attr( $this->meta ); ?>\"\n\t\t\tsize=\"30\"\n\t\t\t<?php if ( isset( $this->field['step'] ) ) : ?>\n\t\t\tstep=\"<?php echo esc_attr( $this->field['step'] ); ?>\"\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( isset( $this->field['required'] ) && $this->field['required'] ) : ?>\n\t\t\trequired=\"required\"\n\t\t\t<?php endif; ?>\n\t\t/>\n\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.post.content.php",
    "content": "<?php\n/**\n * Meta box Field: Content editor\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Post_Content_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Post_Content_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\t$settings = array(\n\t\t\t'textarea_name'    => 'content',\n\t\t\t'quicktags'        => array(\n\t\t\t\t'buttons' => 'em,strong,link',\n\t\t\t),\n\t\t\t'tinymce'          => array(\n\t\t\t\t'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator',\n\t\t\t\t'theme_advanced_buttons2' => '',\n\t\t\t),\n\t\t\t'editor_css'       => '<style>#wp-content-editor-container .wp-editor-area{height:300px; width:100%;}</style>',\n\t\t\t'drag_drop_upload' => true,\n\t\t);\n\n\t\twp_editor( htmlspecialchars_decode( $post->post_content ), 'content', apply_filters( 'lifterlms_course_full_description_editor_settings', $settings ) );\n\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.post.excerpt.php",
    "content": "<?php\n/**\n * Meta box Field: Post Excerpt\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Post_Excerpt_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Post_Excerpt_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\t$settings = array(\n\t\t\t'textarea_name'    => 'excerpt',\n\t\t\t'quicktags'        => array(\n\t\t\t\t'buttons' => 'em,strong,link',\n\t\t\t),\n\t\t\t'tinymce'          => array(\n\t\t\t\t'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator',\n\t\t\t\t'theme_advanced_buttons2' => '',\n\t\t\t),\n\t\t\t'editor_class'     => 'llms-post-editor',\n\t\t\t'editor_css'       => '<style>#excerpt_ifr{height:300px}#wp-excerpt-editor-container .wp-editor-area{height:300px; width:100%;}</style>',\n\t\t\t'drag_drop_upload' => true,\n\t\t);\n\n\t\twp_editor( htmlspecialchars_decode( $post->post_excerpt ), 'excerpt', apply_filters( 'lifterlms_course_short_description_editor_settings', $settings ) );\n\n\t\techo '<div class=\"clear\"></div>';\n\n\t\tparent::close_output();\n\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.repeater.php",
    "content": "<?php\n/**\n * Meta box Field: Repeater\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since 3.11.0\n * @version 3.17.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta box Repeater Field class\n *\n * @since 3.11.0\n * @version 3.17.3\n */\nclass LLMS_Metabox_Repeater_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t * @since    3.11.0\n\t * @version  3.11.0\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$button_defaults = array(\n\t\t\t'classes' => '', // Array or space separated string.\n\t\t\t'icon'    => 'dashicons-plus', // dashicon classname or HTML/String.\n\t\t\t'id'      => $_field['id'] . '-add-new',\n\t\t\t'style'   => 'primary',\n\t\t\t'text'    => __( 'Add New', 'lifterlms' ),\n\t\t);\n\n\t\tif ( empty( $_field['button'] ) ) {\n\t\t\t$_field['button'] = $button_defaults;\n\t\t} else {\n\t\t\t$_field['button'] = wp_parse_args( $_field['button'], $button_defaults );\n\t\t}\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * Retrieve the HTML for the repeater add more button\n\t *\n\t * @return   string\n\t * @since    3.11.0\n\t * @version  3.11.0\n\t */\n\tprivate function output_button() {\n\n\t\t$btn = $this->field['button'];\n\n\t\t// Setup class list.\n\t\t$classes   = explode( ' ', $btn['classes'] );\n\t\t$classes[] = sprintf( 'llms-button-%s', $btn['style'] );\n\t\t$classes[] = 'llms-repeater-new-btn';\n\t\t$classes   = implode( ' ', $classes );\n\n\t\t// Setup icon.\n\t\tif ( $btn['icon'] && 0 === strpos( $btn['icon'], 'dashicons-' ) ) {\n\t\t\t$icon = '<span class=\"dashicons ' . $btn['icon'] . '\"></span>&nbsp;';\n\t\t} else {\n\t\t\t$icon = $btn['icon'];\n\t\t}\n\n\t\t?>\n\t\t<button class=\"<?php echo esc_attr( $classes ); ?>\" type=\"button\"><?php echo wp_kses_post( $icon ) . esc_html( $btn['text'] ); ?></button>\n\t\t<?php\n\t}\n\n\tprivate function output_row( $index ) {\n\n\t\t?>\n\n\t\t<div class=\"llms-collapsible llms-repeater-row\" data-row-order=\"<?php echo esc_attr( $index ); ?>\">\n\n\t\t\t<header class=\"llms-collapsible-header\">\n\t\t\t\t<div class=\"d-2of3\">\n\t\t\t\t\t<h3 class=\"llms-repeater-title\"><?php echo esc_html( $this->field['header']['default'] ); ?></h3>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"d-1of3 d-right\">\n\t\t\t\t\t<span class=\"dashicons dashicons-arrow-down\"></span>\n\t\t\t\t\t<span class=\"dashicons dashicons-arrow-up\"></span>\n\t\t\t\t\t<span class=\"dashicons dashicons-menu llms-drag-handle\"></span>\n\t\t\t\t\t<span class=\"dashicons dashicons-no llms-repeater-remove\"></span>\n\t\t\t\t</div>\n\t\t\t</header>\n\n\t\t\t<section class=\"llms-collapsible-body\">\n\n\t\t\t\t<ul class=\"llms-mb-repeater-fields\">\n\n\t\t\t\t\t<?php foreach ( $this->field['fields'] as $field ) : ?>\n\n\t\t\t\t\t\t<?php $this->output_sub_field( $field, $index ); ?>\n\n\t\t\t\t\t<?php endforeach; ?>\n\n\t\t\t\t</ul>\n\n\t\t\t</section>\n\n\t\t</div>\n\n\t\t<?php\n\t}\n\n\t/**\n\t * Get repeater sub field html output\n\t *\n\t * @return   string\n\t * @since    3.11.0\n\t * @version  3.17.3\n\t */\n\tprivate function output_sub_field( $field, $index ) {\n\n\t\t$field['id'] .= '_' . $index;\n\n\t\tif ( isset( $field['controller'] ) ) {\n\t\t\t$field['controller'] .= '_' . $index;\n\t\t}\n\n\t\t$name = ucfirst(\n\t\t\tstrtr(\n\t\t\t\tpreg_replace_callback(\n\t\t\t\t\t'/(\\w+)/',\n\t\t\t\t\tfunction ( $m ) {\n\t\t\t\t\t\treturn ucfirst( $m[1] );\n\t\t\t\t\t},\n\t\t\t\t\t$field['type']\n\t\t\t\t),\n\t\t\t\t'-',\n\t\t\t\t'_'\n\t\t\t)\n\t\t);\n\n\t\t$field_class_name = str_replace( '{TOKEN}', $name, 'LLMS_Metabox_{TOKEN}_Field' );\n\t\t$field_class      = new $field_class_name( $field );\n\t\t$field_class->output();\n\t}\n\n\t/**\n\t * Outputs the Html for the given field\n\t *\n\t * @return   void\n\t * @since    3.11.0\n\t * @version  3.11.0\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\t?>\n\t\t<div class=\"llms-repeater-model\" id=\"<?php echo esc_attr( $this->field['id'] ); ?>-model\" style=\"display:none;\">\n\t\t<?php $this->output_row( 'model' ); ?>\n\t\t</div>\n\n\t\t<div class=\"llms-collapsible-group llms-repeater-rows\"></div>\n\n\t\t<footer class=\"llms-mb-repeater-footer\">\n\t\t<?php $this->output_button(); ?>\n\t\t</footer>\n\n\t\t<input class=\"llms-repeater-field-handler\" type=\"hidden\" value=\"<?php echo esc_attr( $this->field['handler'] ); ?>\">\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.search.php",
    "content": "<?php\n/**\n * Meta box Field: Search\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Search_Field\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Search_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output(); ?>\n\n\t\t<select\n\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tclass=\"<?php echo esc_attr( $this->field['class'] ); ?>\"\n\t\t>\n\t\t</select>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.select.php",
    "content": "<?php\n/**\n * Meta box field: Select.\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Select_Field class.\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Select_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\n\t/**\n\t * Class constructor.\n\t *\n\t * @param array $_field Array containing information about field.\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * Outputs the Html for the given field.\n\t *\n\t * @since 1.0.0\n\t * @since 3.1.0 Allow regular key=>val arrays to be passed.\n\t * @since 6.0.0 Added required attribute when required :D.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output();\n\n\t\t$name = $this->field['id'];\n\n\t\t$allow_null = ( isset( $this->field['allow_null'] ) ) ? $this->field['allow_null'] : true;\n\n\t\tif ( array_key_exists( 'multi', $this->field ) ) {\n\t\t\t$name .= '[]';\n\t\t}\n\n\t\t$selected = $this->meta;\n\t\tif ( array_key_exists( 'selected', $this->field ) ) {\n\t\t\t$selected = $this->field['selected'];\n\t\t}\n\t\t$attrs = isset( $this->field['data_attributes'] ) ? $this->field['data_attributes'] : array();\n\t\t?>\n\n\t\t<select\n\t\t\t<?php echo isset( $this->field['is_controller'] ) ? 'data-is-controller=\"true\"' : ''; ?>\n\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tname=\"<?php echo esc_attr( $name ); ?>\"\n\t\t<?php if ( ! empty( $this->field['required'] ) && ! $allow_null ) : ?>\n\t\t\trequired=\"required\"\n\t\t<?php endif; ?>\n\t\t\tclass=\"<?php echo esc_attr( $this->field['class'] ); ?>\"\n\t\t\t<?php if ( array_key_exists( 'multi', $this->field ) && $this->field['multi'] ) : ?>\n\t\t\t\tmultiple=\"multiple\"\n\t\t\t<?php endif; ?>\n\t\t\t<?php\n\t\t\tforeach ( $attrs as $attr => $attr_val ) {\n\t\t\t\techo ' data-' . esc_attr( $attr ) . '=\"' . esc_attr( $attr_val ) . '\"'; }\n\t\t\t?>\n\t\t\t>\n\t\t\t<?php if ( $allow_null ) : ?>\n\t\t\t\t<option value=\"\">None</option>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php if ( isset( $this->field['value'] ) ) : ?>\n\n\t\t\t\t<?php\n\t\t\t\tforeach ( $this->field['value'] as $key => $option ) :\n\t\t\t\t\t$is_selected = false;\n\t\t\t\t\tif ( is_array( $selected ) ) {\n\t\t\t\t\t\tif ( in_array( $option['key'], $selected ) ) {\n\t\t\t\t\t\t\t$is_selected = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t} elseif ( isset( $option['key'] ) && $option['key'] == $selected ) {\n\t\t\t\t\t\t$is_selected = true;\n\t\t\t\t\t} elseif ( $key === $selected ) {\n\t\t\t\t\t\t$is_selected = true;\n\t\t\t\t\t}\n\t\t\t\t\t?>\n\t\t\t\t\t<option value=\"<?php echo isset( $option['key'] ) ? esc_attr( $option['key'] ) : esc_attr( $key ); ?>\"<?php echo ( $is_selected ? ' selected=\"selected\"' : '' ); ?>><?php echo isset( $option['title'] ) ? esc_html( $option['title'] ) : esc_html( $option ); ?></option>\n\n\t\t\t\t<?php endforeach; ?>\n\n\t\t\t<?php endif; ?>\n\t\t</select>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.table.php",
    "content": "<?php\n/**\n * Meta box Field: Table\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Table_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Table_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output(); ?>\n\t\t\t<table class=\"llms-table zebra text-left\">\n\t\t\t\t<thead>\n\t\t\t\t\t<?php foreach ( $this->field['titles'] as $title ) : ?>\n\t\t\t\t\t\t<th><?php echo wp_kses_post( $title ); ?></th>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t<?php if ( $this->field['table_data'] ) : ?>\n\t\t\t\t\t\t<?php foreach ( $this->field['table_data'] as $row ) : ?>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<?php foreach ( $row as $column ) : ?>\n\t\t\t\t\t\t\t\t\t<td><?php echo wp_kses_post( $column ); ?></td>\n\t\t\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t<?php elseif ( $this->field['empty_message'] ) : ?>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td colspan=\"<?php count( $this->field['titles'] ); ?>\">\n\t\t\t\t\t\t\t\t<?php echo wp_kses_post( $this->field['empty_message'] ); ?>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.text.php",
    "content": "<?php\n/**\n * Meta box Field: Text\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version 3.36.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Text_Field class\n *\n * @since Unknown\n * @since 3.36.0 When outputting the field's value convert quotes (double and single) HTML entities back to characters.\n */\nclass LLMS_Metabox_Text_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @since 3.36.0 Convert quotes (double and single) HTML entities back to characters.\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\t\tparent::output(); ?>\n\n\t\t<input type=\"text\"\n\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\t<?php if ( array_key_exists( 'required', $this->field ) && $this->field['required'] ) : ?>\n\t\t\t\trequired=\"required\"\n\t\t\t<?php endif; ?>\n\t\t\tclass=\"<?php echo esc_attr( $this->field['class'] ); ?>\"\n\t\t\tvalue=\"<?php echo esc_attr( $this->meta ); ?>\"\n\t\t\tsize=\"30\"\n\t\t\t<?php if ( isset( $this->field['required'] ) && $this->field['required'] ) : ?>\n\t\t\trequired=\"required\"\n\t\t\t<?php endif; ?>\n\t\t/>\n\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.textarea.php",
    "content": "<?php\n/**\n * Meta box Field: Textarea\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Textarea_Field class\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Textarea_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor\n\t *\n\t * @param array $_field Array containing information about field\n\t */\n\tpublic function __construct( $_field ) {\n\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * outputs the Html for the given field\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tglobal $post;\n\n\t\tparent::output(); ?>\n\n\t\t<textarea name=\"<?php echo esc_attr( $this->field['id'] ); ?>\" id=\"<?php echo esc_attr( $this->field['id'] ); ?>\" cols=\"60\" rows=\"4\"\n\t\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t\tif ( isset( $this->field['required'] ) && $this->field['required'] ) :\n\t\t\t\t\t\t\t\t\t\t?>\n\t\t\trequired=\"required\"<?php endif; ?>><?php echo esc_textarea( $this->meta ); ?></textarea>\n\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.textarea.tags.php",
    "content": "<?php\n/**\n * Meta box Field: Textarea with Tags.\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Classes\n *\n * @since Unknown\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Metabox_Textarea_W_Tags_Field class.\n *\n * @since Unknown\n */\nclass LLMS_Metabox_Textarea_W_Tags_Field extends LLMS_Metabox_Field implements Meta_Box_Field_Interface {\n\n\t/**\n\t * Class constructor.\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $_field Array containing information about field.\n\t * @return void\n\t */\n\tpublic function __construct( $_field ) {\n\t\t$this->field = $_field;\n\t}\n\n\t/**\n\t * Outputs the Html for the given field.\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Allow displaying a custom value.\n\t *               Added options for defining textarea rows and columns.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\t\tparent::output();\n\t\t$cols = $this->field['cols'] ?? 60;\n\t\t$rows = $this->field['rows'] ?? 4;\n\t\t?>\n\t\t<textarea\n\t\t\tname=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tid=\"<?php echo esc_attr( $this->field['id'] ); ?>\"\n\t\t\tcols=\"<?php echo esc_attr( $cols ); ?>\"\n\t\t\trows=\"<?php echo esc_attr( $rows ); ?>\"\n\t\t\t><?php echo ! empty( $this->field['value'] ) ? esc_textarea( $this->field['value'] ) : esc_textarea( $this->meta ); ?></textarea>\n\t\t<?php\n\t\tparent::close_output();\n\t}\n}\n\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/fields/llms.interface.meta.box.field.php",
    "content": "<?php\n/**\n * Meta box Field interface\n *\n * @package LifterLMS/Admin/PostTypes/MetaBoxes/Fields/Interfaces\n *\n * @since unknown\n * @version unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Meta_Box_Field_Interface interface\n *\n * @since Unknown\n */\ninterface Meta_Box_Field_Interface {\n\n\tpublic function output();\n}\n"
  },
  {
    "path": "includes/admin/post-types/meta-boxes/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class-llms-admin-post-table-achievements.php",
    "content": "<?php\n/**\n * LLMS_Admin_Post_Table_Achievement class\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// TODO: remove this when the new loader will be implemented.\nrequire_once LLMS_PLUGIN_DIR . '/includes/traits/llms-trait-award-templates-post-list-table.php';\n\n/**\n * Customize display of the achievement post table.\n *\n * @since 6.0.0\n */\nclass LLMS_Admin_Post_Table_Achievements {\n\n\tuse LLMS_Trait_Award_Templates_Post_List_Table;\n\tuse LLMS_Trait_User_Engagement_Type;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->engagement_type = 'achievement';\n\t\t$this->award_template_row_actions(); // defined in LLMS_Trait_Award_Templates_Post_List_Table.\n\n\t}\n\n}\n\nreturn new LLMS_Admin_Post_Table_Achievements();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class-llms-admin-post-table-awards.php",
    "content": "<?php\n/**\n * LLMS_Admin_Post_Table_Awards class\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 6.0.0\n * @version 6.10.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Post table customizations for awarded achievements and certificates.\n *\n * @since 6.0.0\n */\nclass LLMS_Admin_Post_Table_Awards {\n\n\t/**\n\t * Array of supported post types.\n\t *\n\t * @var string[]\n\t */\n\tprivate $post_types = array(\n\t\t'llms_my_achievement',\n\t\t'llms_my_certificate',\n\t);\n\n\t/**\n\t * Query string variable used when filtering by the post's parent.\n\t *\n\t * @var string\n\t */\n\tpublic const TEMPLATE_FILTER_QUERY_VAR = 'llms_filter_template';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'post_row_actions', array( $this, 'row_actions' ), 1, 2 );\n\t\tadd_filter( 'post_date_column_status', array( $this, 'date_col_status' ), 10, 3 );\n\n\t\tforeach ( $this->post_types as $post_type ) {\n\n\t\t\tadd_filter( \"manage_{$post_type}_posts_columns\", array( $this, 'add_cols' ), 10, 1 );\n\t\t\tadd_action( \"manage_{$post_type}_posts_custom_column\", array( $this, 'manage_cols' ), 10, 2 );\n\n\t\t\tadd_filter( \"bulk_actions-edit-{$post_type}\", array( $this, 'bulk_actions' ) );\n\n\t\t\tadd_filter( \"views_edit-{$post_type}\", array( $this, 'modify_views' ) );\n\t\t}\n\n\t\tadd_filter( 'parse_query', array( $this, 'parse_query' ), 10, 1 );\n\t\tadd_action( 'restrict_manage_posts', array( $this, 'add_filters' ), 10, 2 );\n\t}\n\n\t/**\n\t * Add post table columns.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $cols Array of post table columns.\n\t * @return array\n\t */\n\tpublic function add_cols( $cols ) {\n\n\t\t$cols = llms_assoc_array_insert( $cols, 'title', 'user', __( 'User', 'lifterlms' ) );\n\t\t$cols = llms_assoc_array_insert( $cols, 'user', 'template', __( 'Template', 'lifterlms' ) );\n\n\t\treturn $cols;\n\t}\n\n\t/**\n\t * Add filters to the top of the post table\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $post_type Post Type of the current posts table.\n\t * @param string $which     Positioning of the filters, either \"top\" or \"bottom\".\n\t * @return void\n\t */\n\tpublic function add_filters( $post_type, $which ) {\n\n\t\tif ( 'top' !== $which || ! $this->is_post_type( $post_type ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$template_post_type = str_replace( 'my_', '', $post_type );\n\n\t\t$selected = (int) llms_filter_input( INPUT_GET, self::TEMPLATE_FILTER_QUERY_VAR, FILTER_SANITIZE_NUMBER_INT );\n\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped in the function.\n\t\techo LLMS_Admin_Post_Tables::get_post_type_filter_html( self::TEMPLATE_FILTER_QUERY_VAR, $template_post_type, $selected );\n\t}\n\n\t/**\n\t * Manage bulk actions.\n\t *\n\t * Changes the language for \"Move to trash\" to \"Delete permanently\" since the post type doesn't support trash.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $actions Array of bulk actions.\n\t * @return array\n\t */\n\tpublic function bulk_actions( $actions ) {\n\n\t\tif ( ! empty( $actions['trash'] ) ) {\n\t\t\t$actions['trash'] = __( 'Delete Permanently', 'lifterlms' );\n\t\t}\n\n\t\treturn $actions;\n\t}\n\n\t/**\n\t * Modify the post status language.\n\t *\n\t * Displays \"Awarded\" in favor of \"Published\".\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string  $text        Default status text.\n\t * @param WP_Post $post        Post object.\n\t * @param string  $column_name Column name/id. Hardcoded to `date` in the WP core but\n\t *                             passing and checking it anyway in case that changes at\n\t *                             some point.\n\t * @return string\n\t */\n\tpublic function date_col_status( $text, $post, $column_name ) {\n\t\tif ( 'date' === $column_name && $this->is_post_type( $post->post_type ) && 'publish' === $post->post_status ) {\n\t\t\treturn __( 'Awarded', 'lifterlms' );\n\t\t}\n\t\treturn $text;\n\t}\n\n\t/**\n\t * Retrieves the post object given the current screen.\n\t *\n\t * @since 6.0.0\n\t * @since 6.4.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 6.10.0 When no INPUT_GET `post_type` variable set, retrieve the post_type from the `$id` (WP_Post ID) parameter.\n\t *\n\t * @param int     $id       WP_Post ID.\n\t * @param boolean $template Whether or not a template is being requested.\n\t * @return LLMS_User_Achievement|LLMS_User_Certificate|boolean Returns the object or `false` for invalid post types.\n\t */\n\tprivate function get_object( $id, $template = false ) {\n\n\t\t$post_type = llms_filter_input( INPUT_GET, 'post_type' );\n\t\t$post_type = $post_type ? $post_type : get_post_type( $id );\n\n\t\tif ( 'llms_my_achievement' === $post_type ) {\n\t\t\treturn new LLMS_User_Achievement( $id );\n\t\t} elseif ( 'llms_my_certificate' === $post_type ) {\n\t\t\treturn llms_get_certificate( $id, $template );\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determines if the specified post type is one of the post types affected by this class.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $post_type A post type to test.\n\t * @return boolean\n\t */\n\tprivate function is_post_type( $post_type ) {\n\t\treturn in_array( $post_type, $this->post_types, true );\n\t}\n\n\t/**\n\t * Manage content of awarded certificate columns.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $column  Column key/name.\n\t * @param int    $post_id WP Post ID of the llms_my_certificate for the row.\n\t * @return void\n\t */\n\tpublic function manage_cols( $column, $post_id ) {\n\n\t\tif ( 'template' === $column ) {\n\t\t\t$this->manage_cols_template( $post_id );\n\t\t} elseif ( 'user' === $column ) {\n\t\t\t$this->manage_cols_user( $post_id );\n\t\t}\n\t}\n\n\t/**\n\t * Output the content for a template column.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $post_id WP_Post ID for the row.\n\t * @return void\n\t */\n\tprivate function manage_cols_template( $post_id ) {\n\t\t$obj       = $this->get_object( $post_id );\n\t\t$parent_id = $obj->get( 'parent' );\n\t\t$parent    = $parent_id ? $this->get_object( $parent_id, true ) : false;\n\t\tif ( $parent ) {\n\t\t\tprintf(\n\t\t\t\t'<a href=\"%1$s\">%2$s</a>',\n\t\t\t\tesc_url( get_edit_post_link( $parent_id ) ),\n\t\t\t\t_draft_or_post_title( $parent_id ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped in the function.\n\t\t\t);\n\t\t} else {\n\t\t\techo '&mdash;';\n\t\t}\n\t}\n\n\t/**\n\t * Output the content for a user column.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $post_id WP_Post ID for the row.\n\t * @return void\n\t */\n\tprivate function manage_cols_user( $post_id ) {\n\t\t$obj  = $this->get_object( $post_id );\n\t\t$uid  = $obj->get_user_id();\n\t\t$user = $uid ? llms_get_student( $uid ) : false;\n\t\tif ( $user ) {\n\t\t\t$url = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\tarray(\n\t\t\t\t\t'student_id' => $uid,\n\t\t\t\t)\n\t\t\t);\n\t\t\tprintf(\n\t\t\t\t'<a href=\"%1$s\">%2$s</a>',\n\t\t\t\tesc_url( $url ),\n\t\t\t\tesc_html( $user->get_name() )\n\t\t\t);\n\t\t} else {\n\t\t\techo '&mdash;';\n\t\t}\n\t}\n\n\t/**\n\t * Removes the trash link for the post table.\n\t *\n\t * We intend these post types to be permanently deleted but due to issues with how the block editor handles\n\t * moving a post to the trash we cannot disable the trash for these post types. Instead, we allow items to be\n\t * moved into the trash and we're hiding the trash link. Items will be deleted automatically via the WP core's\n\t * cron and we've updated \"Move to trash\" language to \"Delete permanently\". Users will be able to navigate\n\t * to the trash page and restore from the trash if they can know/guess the link, and that's okay. I guess.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @link https://github.com/WordPress/gutenberg/issues/13024\n\t *\n\t * @param array $views Array of table views.\n\t * @return array\n\t */\n\tpublic function modify_views( $views ) {\n\t\tunset( $views['trash'] );\n\t\treturn $views;\n\t}\n\n\t/**\n\t * Modify the main WP Query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param WP_Query $query The WordPress Query.\n\t * @return WP_Query\n\t */\n\tpublic function parse_query( $query ) {\n\n\t\t// Only modify admin & main query.\n\t\tif ( ! ( is_admin() && $query->is_main_query() ) ) {\n\t\t\treturn $query;\n\t\t}\n\n\t\t// Don't proceed if it's not our post type.\n\t\tif ( ! isset( $query->query['post_type'] ) || ! $this->is_post_type( $query->query['post_type'] ) ) {\n\t\t\treturn $query;\n\t\t}\n\n\t\t$template_id = (int) llms_filter_input( INPUT_GET, self::TEMPLATE_FILTER_QUERY_VAR, FILTER_SANITIZE_NUMBER_INT );\n\n\t\t// Don't proceed if no template is being filtered.\n\t\tif ( ! $template_id ) {\n\t\t\treturn $query;\n\t\t}\n\n\t\t$query->set( 'post_parent', $template_id );\n\n\t\treturn $query;\n\t}\n\n\t/**\n\t * Modify post row actions.\n\t *\n\t * @since 6.0.0\n\t * @since 6.10.0 Added missing textdomain for the 'Move {post_title} to the Trash' string.\n\t *\n\t * @param array   $actions Existing actions.\n\t * @param WP_Post $post    Post object.\n\t * @return array\n\t */\n\tpublic function row_actions( $actions, $post ) {\n\n\t\tif ( $this->is_post_type( $post->post_type ) && ! empty( $actions['trash'] ) ) {\n\n\t\t\t$actions['trash'] = sprintf(\n\t\t\t\t'<a href=\"%s\" class=\"submitdelete\" aria-label=\"%s\">%s</a>',\n\t\t\t\tget_delete_post_link( $post->ID ),\n\t\t\t\t// Translators: %s: Post title.\n\t\t\t\tesc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash', 'lifterlms' ), _draft_or_post_title( $post ) ) ),\n\t\t\t\t__( 'Delete Permanently', 'lifterlms' )\n\t\t\t);\n\n\t\t}\n\n\t\treturn $actions;\n\t}\n}\n\nreturn new LLMS_Admin_Post_Table_Awards();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class-llms-admin-post-table-certificates.php",
    "content": "<?php\n/**\n * LLMS_Admin_Post_Table_Certificates class\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 6.0.0\n * @version 6.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// TODO: remove this when the new loader will be implemented.\nrequire_once LLMS_PLUGIN_DIR . '/includes/traits/llms-trait-award-templates-post-list-table.php';\n/**\n * Customize display of the certificate post tables.\n *\n * @since 6.0.0\n */\nclass LLMS_Admin_Post_Table_Certificates {\n\n\tuse LLMS_Trait_Award_Templates_Post_List_Table;\n\tuse LLMS_Trait_User_Engagement_Type;\n\n\t/**\n\t * Query string variable used to identify the migration action.\n\t *\n\t * @var string\n\t */\n\tconst MIGRATE_ACTION = 'llms-migrate-legacy-certificate';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->engagement_type = 'certificate';\n\t\t$this->award_template_row_actions(); // defined in LLMS_Trait_Award_Templates_Post_List_Table.\n\n\t\tif ( ! llms_is_block_editor_supported_for_certificates() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$post_types = array( 'llms_certificate', 'llms_my_certificate' );\n\t\tif ( in_array( llms_filter_input( INPUT_GET, 'post_type' ), $post_types, true ) ) {\n\t\t\tadd_filter( 'display_post_states', array( $this, 'add_states' ), 20, 2 );\n\t\t\tadd_filter( 'post_row_actions', array( $this, 'add_actions' ), 20, 2 );\n\t\t}\n\n\t\tif ( 1 === (int) llms_filter_input( INPUT_GET, self::MIGRATE_ACTION, FILTER_SANITIZE_NUMBER_INT ) ) {\n\t\t\tadd_filter( 'llms_certificate_template_version', array( $this, 'upgrade_template' ), 10 );\n\t\t}\n\n\t\tadd_filter( 'manage_llms_my_certificate_posts_columns', array( $this, 'mod_cols' ), 10, 1 );\n\n\t}\n\n\t/**\n\t * Add post row actions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array   $actions Array of post row actions.\n\t * @param WP_Post $post    Post object for the row.\n\t * @return array\n\t */\n\tpublic function add_actions( $actions, $post ) {\n\n\t\t$cert = llms_get_certificate( $post, true );\n\t\tif ( 1 === $cert->get_template_version() ) {\n\n\t\t\t$url                             = esc_url( add_query_arg( self::MIGRATE_ACTION, 1, get_edit_post_link( $post ) ) );\n\t\t\t$actions[ self::MIGRATE_ACTION ] = '<a href=\"' . $url . '\">' . __( 'Migrate legacy certificate', 'lifterlms' ) . '</a>';\n\n\t\t}\n\n\t\treturn $actions;\n\n\t}\n\n\t/**\n\t * Add state information denoting the usage of the legacy template.\n\t *\n\t * @since 6.0.0\n\t * @since 6.2.0 Made sure to only process certificates.\n\t *\n\t * @param string[] $states Array of post states.\n\t * @param WP_Post  $post   Post object.\n\t * @return string[]\n\t */\n\tpublic function add_states( $states, $post ) {\n\n\t\t$cert = llms_get_certificate( $post, true );\n\t\tif ( $cert && 1 === $cert->get_template_version() ) {\n\t\t\t$states['llms-legacy-template'] = __( 'Legacy', 'lifterlms' );\n\t\t}\n\n\t\treturn $states;\n\n\t}\n\n\t/**\n\t * Modify the columns list for the `llms_my_certificate` post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $cols Array of columns.\n\t * @return array\n\t */\n\tpublic function mod_cols( $cols ) {\n\t\tunset( $cols['author'] );\n\t\treturn $cols;\n\t}\n\n\t/**\n\t * Callback function for `llms_certificate_template_version` forcing an upgrade to version 2.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param integer $version Current template version.\n\t * @return integer\n\t */\n\tpublic function upgrade_template( $version ) {\n\t\treturn 2;\n\t}\n\n}\n\nreturn new LLMS_Admin_Post_Table_Certificates();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class-llms-admin-post-table-forms.php",
    "content": "<?php\n/**\n * Add, Customize, and Manage LifterLMS Forms Post Table Columns\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Table_Forms\n *\n * @since 5.0.0\n */\nclass LLMS_Admin_Post_Table_Forms {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return  void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'manage_llms_form_posts_columns', array( $this, 'add_columns' ), 10, 1 );\n\t\tadd_filter( 'bulk_actions-edit-llms_form', array( $this, 'manage_bulk_actions' ), 10, 1 );\n\t\tadd_filter( 'post_row_actions', array( $this, 'manage_post_row_actions' ), 10, 2 );\n\n\t\tadd_action( 'manage_llms_form_posts_custom_column', array( $this, 'manage_columns' ), 10, 2 );\n\n\t\tadd_action( 'pre_get_posts', array( 'LLMS_Admin_Post_Table_Forms', 'pre_get_posts' ) );\n\t}\n\n\t/**\n\t * Add Custom Columns\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $columns Array of default columns.\n\t * @return array\n\t */\n\tpublic function add_columns( $columns ) {\n\n\t\tif ( apply_filters( 'llms_forms_disable_post_table_cb', true ) ) {\n\t\t\tunset( $columns['cb'] );\n\t\t}\n\n\t\treturn llms_assoc_array_insert( $columns, 'title', 'location', __( 'Location', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Manage available bulk actions.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $actions Array of actions.\n\t * @return array\n\t */\n\tpublic function manage_bulk_actions( $actions ) {\n\t\tunset( $actions['edit'] );\n\t\treturn $actions;\n\t}\n\n\t/**\n\t * Manage content of custom columns\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $column Table column name.\n\t * @param int    $post_id WP Post ID of the form for the current row.\n\t * @return void\n\t */\n\tpublic function manage_columns( $column, $post_id ) {\n\n\t\tif ( 'location' === $column ) {\n\t\t\t$locs = LLMS_Forms::instance()->get_locations();\n\t\t\t$loc  = get_post_meta( $post_id, '_llms_form_location', true );\n\n\t\t\tif ( isset( $locs[ $loc ] ) ) {\n\t\t\t\tprintf( '<strong>%1$s</strong><br><em>%2$s</em>', esc_html( $locs[ $loc ]['name'] ), esc_html( $locs[ $loc ]['description'] ) );\n\t\t\t} else {\n\t\t\t\techo esc_html( $loc );\n\t\t\t}\n\t\t}\n\t}\n\n\n\t/**\n\t * Manage available bulk actions.\n\t *\n\t * @since 5.0.0\n\t * @since 6.4.0 Use `LLMS_Forms::is_a_core_form()` to determine whether a form is a core form and cannot be deleted.\n\t *\n\t * @param array $actions Array of actions.\n\t * @return array\n\t */\n\tpublic function manage_post_row_actions( $actions, $post ) {\n\n\t\tif ( 'llms_form' !== $post->post_type ) {\n\t\t\treturn $actions;\n\t\t}\n\n\t\t// Core forms cannot be deleted.\n\t\tif ( LLMS_Forms::instance()->is_a_core_form( $post ) ) {\n\t\t\tunset( $actions['trash'] );\n\t\t}\n\n\t\tunset( $actions['inline hide-if-no-js'] );\n\n\t\t$link = get_permalink( $post );\n\t\tif ( $link ) {\n\t\t\t$label           = sprintf( __( 'View \"%s\"', 'lifterlms' ), $post->post_title );\n\t\t\t$actions['view'] = sprintf( '<a href=\"%1$s\" rel=\"bookmark\" aria-label=\"%2$s\">%3$s</a>', esc_url( $link ), esc_attr( $label ), esc_html__( 'View', 'lifterlms' ) );\n\t\t}\n\n\t\treturn $actions;\n\t}\n\n\t/**\n\t * Ensure only core forms are displayed in the forms list.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param WP_Query $query Query object.\n\t * @return void\n\t */\n\tpublic static function pre_get_posts( $query ) {\n\n\t\tif ( ! function_exists( 'get_current_screen' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$screen = get_current_screen();\n\t\tif ( ! $screen || 'edit-llms_form' !== $screen->id || ! $query->is_main_query() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$query->set( 'meta_key', '_llms_form_is_core' );\n\t\t$query->set( 'meta_value', 'yes' );\n\t}\n}\nreturn new LLMS_Admin_Post_Table_Forms();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php",
    "content": "<?php\n/**\n * Add, Customize, and Manage LifterLMS Coupon Post Table Columns\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 3.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Table_Coupons class\n *\n * @since 3.0.0\n */\nclass LLMS_Admin_Post_Table_Coupons {\n\n\t/**\n\t * Constructor\n\t *\n\t * @return void\n\t *\n\t * @since 3.0.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'manage_llms_coupon_posts_columns', array( $this, 'add_columns' ), 10, 1 );\n\t\tadd_action( 'manage_llms_coupon_posts_custom_column', array( $this, 'manage_columns' ), 10, 2 );\n\t}\n\n\t/**\n\t * Add Custom Coupon Columns\n\t *\n\t * @param array $columns array of default columns\n\t * @return  array\n\t * @since  3.0.0\n\t */\n\tpublic function add_columns( $columns ) {\n\n\t\t$columns = array(\n\t\t\t'cb'     => '<input type=\"checkbox\" />',\n\t\t\t'title'  => __( 'Code', 'lifterlms' ),\n\t\t\t'amount' => __( 'Coupon Amount', 'lifterlms' ),\n\t\t\t'desc'   => __( 'Description', 'lifterlms' ),\n\t\t\t'usage'  => __( 'Usage / Limit', 'lifterlms' ),\n\t\t\t'expiry' => __( 'Expiration Date', 'lifterlms' ),\n\t\t);\n\n\t\treturn $columns;\n\t}\n\n\n\t/**\n\t * Manage content of custom coupon columns\n\t *\n\t * @param  string $column  column key/name\n\t * @param  int    $post_id WP Post ID of the coupon for the row\n\t * @return void\n\t */\n\tpublic function manage_columns( $column, $post_id ) {\n\n\t\tglobal $post;\n\t\t$c = new LLMS_Coupon( $post );\n\n\t\tswitch ( $column ) {\n\n\t\t\tcase 'amount':\n\t\t\t\tesc_html_e( 'Discount: ', 'lifterlms' );\n\t\t\t\techo wp_kses( $c->get_formatted_amount(), LLMS_ALLOWED_HTML_PRICES );\n\t\t\t\techo '<br>';\n\n\t\t\t\tif ( $c->has_trial_discount() ) {\n\t\t\t\t\tesc_html_e( 'Trial Discount: ', 'lifterlms' );\n\t\t\t\t\techo wp_kses( $c->get_formatted_amount( 'trial_amount' ), LLMS_ALLOWED_HTML_PRICES );\n\t\t\t\t\techo '<br>';\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'desc':\n\t\t\t\techo esc_html( $c->get( 'description' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'usage':\n\t\t\t\techo esc_html( $c->get_uses() );\n\t\t\t\techo ' / ';\n\t\t\t\techo ( $c->get( 'usage_limit' ) ) ? esc_html( $c->get( 'usage_limit' ) ) : '&infin;';\n\t\t\t\tbreak;\n\n\t\t\tcase 'expiry':\n\t\t\t\techo $c->get( 'expiration_date' ) ? esc_html( $c->get_date( 'expiration_date', 'F d, Y' ) ) : '&ndash;';\n\t\t\t\tbreak;\n\n\t\t}\n\t}\n}\nreturn new LLMS_Admin_Post_Table_Coupons();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class.llms.admin.post.table.courses.php",
    "content": "<?php\n/**\n * Add, Customize, and Manage LifterLMS Course Post Table Columns\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 3.3.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Table_Courses class\n *\n * @since 3.3.0\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Admin_Post_Table_Courses {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.3.0\n\t * @since 3.13.0 Unknown.\n\t * @since 7.1.0 Added new custom columns.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'post_row_actions', array( $this, 'add_links' ), 1, 2 );\n\n\t\tadd_filter( 'manage_course_posts_columns', array( $this, 'add_columns' ), 10, 1 );\n\t\tadd_action( 'manage_course_posts_custom_column', array( $this, 'manage_columns' ), 10, 2 );\n\n\t\tadd_filter( 'bulk_actions-edit-course', array( $this, 'register_bulk_actions' ) );\n\t\tadd_filter( 'handle_bulk_actions-edit-course', array( $this, 'handle_bulk_actions' ), 10, 3 );\n\t}\n\n\t/**\n\t * Add course builder edit link\n\t *\n\t * @param    array $actions  existing actions\n\t * @param    obj   $post     WP_Post object\n\t * @since    3.13.0\n\t * @version  3.13.1\n\t */\n\tpublic function add_links( $actions, $post ) {\n\n\t\tif ( 'course' === $post->post_type && current_user_can( 'edit_course', $post->ID ) ) {\n\n\t\t\t$url = add_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t\t'course_id' => $post->ID,\n\t\t\t\t),\n\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t);\n\n\t\t\t$actions = array_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'llms-builder' => '<a href=\"' . esc_url( $url ) . '\">' . __( 'Builder', 'lifterlms' ) . '</a>',\n\t\t\t\t),\n\t\t\t\t$actions\n\t\t\t);\n\n\t\t}\n\n\t\treturn $actions;\n\t}\n\n\t/**\n\t * Exports courses from the Bulk Actions menu on the courses post table\n\t *\n\t * @param    string $redirect_to  url to redirect to upon export completion (not used)\n\t * @param    string $doaction     action name called\n\t * @param    array  $post_ids     selected post ids\n\t * @return   null\n\t * @since    3.3.0\n\t * @version  3.24.0\n\t */\n\tpublic function handle_bulk_actions( $redirect_to, $doaction, $post_ids ) {\n\n\t\t// Ensure it's our custom action.\n\t\tif ( 'llms_export' !== $doaction ) {\n\t\t\treturn $redirect_to;\n\t\t}\n\n\t\t$data = array(\n\t\t\t'_generator' => 'LifterLMS/BulkCourseExporter',\n\t\t\t'_source'    => get_site_url(),\n\t\t\t'_version'   => llms()->version,\n\t\t\t'courses'    => array(),\n\t\t);\n\n\t\tforeach ( $post_ids as $post_id ) {\n\n\t\t\t$c                 = new LLMS_Course( $post_id );\n\t\t\t$data['courses'][] = $c->toArray();\n\n\t\t}\n\n\t\t$title = str_replace( ' ', '-', __( 'courses export', 'lifterlms' ) );\n\t\t$title = preg_replace( '/[^a-zA-Z0-9-]/', '', $title );\n\n\t\t$filename = apply_filters( 'llms_bulk_export_courses_filename', $title . '_' . current_time( 'Ymd' ), $this );\n\n\t\theader( 'Content-type: application/json' );\n\t\theader( 'Content-Disposition: attachment; filename=\"' . $filename . '.json\"' );\n\t\theader( 'Pragma: no-cache' );\n\t\theader( 'Expires: 0' );\n\n\t\techo json_encode( $data );\n\n\t\tdie;\n\t}\n\n\t/**\n\t * Register bulk actions\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param array $actions Existing bulk actions.\n\t * @return string[]\n\t */\n\tpublic function register_bulk_actions( $actions ) {\n\n\t\t$actions['llms_export'] = __( 'Export', 'lifterlms' );\n\t\treturn $actions;\n\t}\n\n\n\t/**\n\t * Add custom course columns.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @param array $columns Array of default columns.\n\t * @return array\n\t */\n\tpublic function add_columns( $columns ) {\n\n\t\t// Add a new column for Lessons.\n\t\t$new_columns            = array();\n\t\t$new_columns['lessons'] = __( 'Lessons', 'lifterlms' );\n\n\t\t// Insert column into third position in existing columns array.\n\t\t$columns = array_merge( array_slice( $columns, 0, 3 ), $new_columns, array_slice( $columns, 3 ) );\n\n\t\treturn $columns;\n\t}\n\n\n\t/**\n\t * Manage content of custom course columns.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @param string $column  Column key/name.\n\t * @param int    $post_id WP Post ID of the course for the row.\n\t * @return void\n\t */\n\tpublic function manage_columns( $column, $post_id ) {\n\n\t\tif ( 'lessons' !== $column ) {\n\t\t\treturn $column;\n\t\t}\n\n\t\t// Get a count of lessons in the course.\n\t\t$course       = llms_get_post( $post_id );\n\t\t$lesson_count = $course->get_lessons_count();\n\n\t\tif ( ! $lesson_count ) {\n\t\t\techo '&ndash;';\n\n\t\t\treturn;\n\t\t}\n\n\t\t// Build the URL to link to lesson post type filtered for course ID.\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'post_status'           => 'all',\n\t\t\t\t'post_type'             => 'lesson',\n\t\t\t\t'llms_filter_course_id' => $post_id,\n\t\t\t),\n\t\t\tadmin_url( 'edit.php' )\n\t\t);\n\n\t\t// Translators: %d = Number of lessons in the specified course.\n\t\t$label = sprintf( _n( '%d Lesson', '%d Lessons', $lesson_count, 'lifterlms' ), $lesson_count );\n\t\techo '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $label ) . '</a>';\n\t}\n}\n\nreturn new LLMS_Admin_Post_Table_Courses();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php",
    "content": "<?php\n/**\n * Add, Customize, and Manage LifterLMS Engagement Post Table Columns\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 3.1.0\n * @version 3.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Table_Engagements class\n *\n * @since 3.1.0\n * @since 3.7.0 Unknown.\n */\nclass LLMS_Admin_Post_Table_Engagements {\n\n\t/**\n\t * Constructor\n\t *\n\t * @return  void\n\t * @since    3.1.0\n\t * @version  3.1.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'manage_llms_engagement_posts_columns', array( $this, 'add_columns' ), 10, 1 );\n\t\tadd_action( 'manage_llms_engagement_posts_custom_column', array( $this, 'manage_columns' ), 10, 2 );\n\t}\n\n\t/**\n\t * Add Custom Coupon Columns\n\t *\n\t * @param    array $columns array of default columns\n\t * @return   array\n\t * @since    3.1.0\n\t * @version  3.1.0\n\t */\n\tpublic function add_columns( $columns ) {\n\n\t\t$date = $columns['date'];\n\t\tunset( $columns['date'] );\n\n\t\t$columns['trigger'] = __( 'Trigger', 'lifterlms' );\n\t\t$columns['type']    = __( 'Type', 'lifterlms' );\n\t\t$columns['delay']   = __( 'Delay', 'lifterlms' );\n\n\t\t$columns['date'] = $date;\n\n\t\treturn $columns;\n\t}\n\n\n\t/**\n\t * Manage content of custom coupon columns\n\t *\n\t * @param  string $column  column key/name\n\t * @param  int    $post_id WP Post ID of the coupon for the row\n\t * @return void\n\t * @since    3.1.0\n\t * @version  3.7.0\n\t */\n\tpublic function manage_columns( $column, $post_id ) {\n\n\t\tswitch ( $column ) {\n\n\t\t\tcase 'trigger':\n\t\t\t\t$triggers = llms_get_engagement_triggers();\n\n\t\t\t\t$trigger = get_post_meta( $post_id, '_llms_trigger_type', true );\n\n\t\t\t\techo isset( $triggers[ $trigger ] ) ? esc_html( $triggers[ $trigger ] ) : esc_html( $trigger );\n\n\t\t\t\t$tid = get_post_meta( $post_id, '_llms_engagement_trigger_post', true );\n\t\t\t\tif ( $tid && 'any' !== $tid ) {\n\n\t\t\t\t\techo '<br>';\n\n\t\t\t\t\tif ( 'course_track_completed' === $trigger ) {\n\t\t\t\t\t\t$term  = get_term( $tid, 'course_track' );\n\t\t\t\t\t\t$title = $term->name;\n\t\t\t\t\t\t$link  = get_edit_term_link( $tid, 'course_track', 'course' );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$title = get_the_title( $tid );\n\t\t\t\t\t\t$link  = get_edit_post_link( $tid );\n\t\t\t\t\t}\n\n\t\t\t\t\tprintf( '<a href=\"%s\">%s (ID# %d)</a>', esc_url( $link ), esc_html( $title ), esc_html( $tid ) );\n\n\t\t\t\t} elseif ( 'any' === $tid ) {\n\n\t\t\t\t\techo '<br><em>' . esc_html__( 'Any', 'lifterlms' ) . '</em>';\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'type':\n\t\t\t\t$types = llms_get_engagement_types();\n\n\t\t\t\t$type = get_post_meta( $post_id, '_llms_engagement_type', true );\n\n\t\t\t\techo isset( $types[ $type ] ) ? esc_html( $types[ $type ] ) : esc_html( $type );\n\n\t\t\t\t$eid = get_post_meta( $post_id, '_llms_engagement', true );\n\t\t\t\tif ( $eid ) {\n\n\t\t\t\t\techo '<br>';\n\t\t\t\t\tprintf( '<a href=\"%s\">%s (ID# %d)</a>', esc_url( get_edit_post_link( $eid ) ), esc_html( get_the_title( $eid ) ), esc_html( $eid ) );\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'delay':\n\t\t\t\t$delay = get_post_meta( $post_id, '_llms_engagement_delay', true );\n\n\t\t\t\tif ( $delay ) {\n\n\t\t\t\t\tprintf( esc_html__( '%d days', 'lifterlms' ), esc_html( $delay ) );\n\n\t\t\t\t} else {\n\n\t\t\t\t\techo '&ndash;';\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t}\n\t}\n}\nreturn new LLMS_Admin_Post_Table_Engagements();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class.llms.admin.post.table.instructors.php",
    "content": "<?php\n/**\n * Post table stuff for courses and memberships who have custom \"instructor\" stuff * which replaces \"Author\"\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 3.13.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n/**\n * LLMS_Admin_Post_Table_Instructors class\n *\n * @since 3.13.0\n * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n */\nclass LLMS_Admin_Post_Table_Instructors {\n\n\tprivate $post_types = array(\n\t\t'course',\n\t\t'llms_membership',\n\t);\n\n\t/**\n\t * Constructor\n\t *\n\t * @return  void\n\t * @since    3.3.0\n\t * @version  3.13.0\n\t */\n\tpublic function __construct() {\n\n\t\tforeach ( $this->post_types as $post_type ) {\n\t\t\tadd_filter( 'manage_' . $post_type . '_posts_columns', array( $this, 'add_columns' ), 10, 1 );\n\t\t\tadd_action( 'manage_' . $post_type . '_posts_custom_column', array( $this, 'manage_columns' ), 10, 2 );\n\t\t\tadd_filter( 'views_edit-' . $post_type, array( $this, 'get_views' ), 777, 1 );\n\t\t}\n\n\t\tadd_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );\n\t}\n\n\t/**\n\t * Add Custom Columns\n\t *\n\t * @param    array $columns array of default columns\n\t * @return   array\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function add_columns( $columns ) {\n\n\t\t$offset = array_search( 'title', array_keys( $columns ) );\n\n\t\t$add = array(\n\t\t\t'llms-instructors' => __( 'Instructors', 'lifterlms' ),\n\t\t);\n\n\t\treturn array_slice( $columns, 0, $offset + 1 ) + $add + array_slice( $columns, $offset );\n\t}\n\n\t/**\n\t * Create a string that can be used in a LIKE query for finding a student's id in the llms_instructors\n\t * meta field on the usermeta table\n\t *\n\t * @param    int $user_id  WP User ID\n\t * @return   string\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tprivate function get_serialized_id( $user_id ) {\n\t\t$val = serialize(\n\t\t\tarray(\n\t\t\t\t'id' => absint( $user_id ),\n\t\t\t)\n\t\t);\n\t\treturn str_replace( array( 'a:1:{', '}' ), '', $val );\n\t}\n\n\t/**\n\t * Ensure that the \"Mine\" view quick link at the top of the table displays the correct number\n\t * Most of this is based on WordPress core functions found in wp-admin/includes/class-wp-posts-list-table.php\n\t *\n\t * @since 3.13.0\n\t * @since 3.24.0 Unknown.\n\t * @since 3.35.0 Verify nonces and sanitize `$_POST` data.\n\t * @since 4.5.1 Use `$_GET` data instead of `$_POST`.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param    array $views  array of view link HTML string\n\t * @return   array\n\t */\n\tpublic function get_views( $views ) {\n\n\t\t$post_type       = llms_filter_input_sanitize_string( INPUT_GET, 'post_type' );\n\t\t$current_user_id = get_current_user_id();\n\t\t$exclude_states  = get_post_stati(\n\t\t\tarray(\n\t\t\t\t'show_in_admin_all_list' => false,\n\t\t\t)\n\t\t);\n\n\t\tglobal $wpdb;\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Statuses are sanitized.\n\n\t\t$count = intval(\n\t\t\t$wpdb->get_var(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"\n\t\t\tSELECT COUNT( 1 )\n\t\t\tFROM $wpdb->posts AS p\n\t\t\tJOIN $wpdb->postmeta AS m\n\t\t\t  ON p.ID = m.post_id\n\t\t\t AND m.meta_key = '_llms_instructors'\n\t\t\t AND m.meta_value LIKE %s\n\t\t\tWHERE p.post_type = %s\n\t\t\t  AND p.post_status NOT IN ( '\" . implode( \"','\", $exclude_states ) . \"' )\n\t\t\",\n\t\t\t\t\t'%' . $this->get_serialized_id( $current_user_id ) . '%',\n\t\t\t\t\t$post_type\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared\n\n\t\t$label = sprintf(\n\t\t\t_nx(\n\t\t\t\t'Mine <span class=\"count\">(%s)</span>',\n\t\t\t\t'Mine <span class=\"count\">(%s)</span>',\n\t\t\t\t$count,\n\t\t\t\t'posts',\n\t\t\t\t'lifterlms'\n\t\t\t),\n\t\t\tnumber_format_i18n( $count )\n\t\t);\n\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t\t'author'    => $current_user_id,\n\t\t\t),\n\t\t\t'edit.php'\n\t\t);\n\n\t\t$class = '';\n\t\tif ( isset( $_GET['author'] ) && ( $_GET['author'] == $current_user_id ) ) {\n\t\t\t$class = 'class=\"current\"';\n\t\t}\n\n\t\t/**\n\t\t * If mine doesn't already exist in views, we need to add it after \"All\" manually\n\t\t * to preserve the user experience.\n\t\t */\n\t\tif ( ! isset( $views['mine'] ) ) {\n\n\t\t\t$offset = array_search( 'all', array_keys( $views ) );\n\t\t\t$add    = array(\n\t\t\t\t'mine' => '',\n\t\t\t);\n\t\t\t$views  = array_slice( $views, 0, $offset + 1 ) + $add + array_slice( $views, $offset + 1 );\n\n\t\t}\n\n\t\t$views['mine'] = sprintf( '<a href=\"%1$s\"%2$s>%3$s</a>', esc_url( $url ), $class, $label );\n\n\t\treturn $views;\n\t}\n\n\t/**\n\t * Manage content of custom columns\n\t *\n\t * @param    string $column   column key/name\n\t * @param    int    $post_id  WP Post ID of the coupon for the row\n\t * @return   void\n\t * @since    3.13.0\n\t * @version  3.23.0\n\t */\n\tpublic function manage_columns( $column, $post_id ) {\n\n\t\t$post = llms_get_post( $post_id );\n\n\t\tswitch ( $column ) {\n\n\t\t\tcase 'llms-instructors':\n\t\t\t\t$instructors = $post->get_instructors();\n\t\t\t\t$htmls       = array();\n\t\t\t\tforeach ( $instructors as $user ) {\n\n\t\t\t\t\t$url = add_query_arg(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'post_type' => $post->get( 'type' ),\n\t\t\t\t\t\t\t'author'    => $user['id'],\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'edit.php'\n\t\t\t\t\t);\n\n\t\t\t\t\t$instructor = llms_get_instructor( $user['id'] );\n\n\t\t\t\t\tif ( $instructor ) {\n\t\t\t\t\t\t$htmls[] = sprintf( '<a href=\"%1$s\">%2$s</a>', esc_url( $url ), esc_html( $instructor->get( 'display_name' ) ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped before being put in array.\n\t\t\t\techo implode( ', ', $htmls );\n\t\t\t\tbreak;\n\n\t\t}\n\t}\n\n\t/**\n\t * Handle course & membership queries for searching by llms_instructors rather than author\n\t *\n\t * @param    obj $query  WP_Query\n\t * @return   void\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function pre_get_posts( $query ) {\n\n\t\tif ( ! is_admin() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! $query->is_main_query() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Don't run duplicates.\n\t\tif ( $query->get( 'llms_instructor_query' ) ) {\n\t\t\treturn;\n\t\t}\n\t\t// phpcs:ignore -- commented out code\n\t\t// var_dump( $query->query_vars );\n\n\t\tif ( isset( $query->query_vars['post_type'] ) && in_array( $query->query_vars['post_type'], $this->post_types ) && ! empty( $query->query_vars['author'] ) ) {\n\n\t\t\t// Get the query or a default to work with.\n\t\t\t$meta_query = $query->get( 'meta_query' );\n\t\t\tif ( ! $meta_query ) {\n\t\t\t\t$meta_query = array();\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Set an and relation for our filters\n\t\t\t * if other filters already exist, we'll ensure we obey them as well this way.\n\t\t\t */\n\t\t\t$meta_query['relation'] = 'AND';\n\n\t\t\t$meta_query[] = array(\n\t\t\t\t'compare' => 'LIKE',\n\t\t\t\t'key'     => '_llms_instructors',\n\t\t\t\t'value'   => $this->get_serialized_id( $query->query_vars['author'] ),\n\t\t\t);\n\n\t\t\t$query->set( 'meta_query', $meta_query );\n\n\t\t\t$query->set( 'llms_instructor_query', true );\n\n\t\t\t$query->set( 'author', '' );\n\n\t\t}\n\t}\n}\n\nreturn new LLMS_Admin_Post_Table_Instructors();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php",
    "content": "<?php\n/**\n * Add, Customize, and Manage LifterLMS Lesson posts table Columns\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 3.2.3\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Table_Lessons class\n *\n * @since 3.2.3\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Admin_Post_Table_Lessons {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.2.3\n\t * @since 3.12.0 Unknown.\n\t * @since 7.1.0 Added links to the course builder.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'post_row_actions', array( $this, 'add_links' ), 1, 2 );\n\n\t\tadd_filter( 'manage_lesson_posts_columns', array( $this, 'add_columns' ), 10, 1 );\n\t\tadd_action( 'manage_lesson_posts_custom_column', array( $this, 'manage_columns' ), 10, 2 );\n\n\t\tadd_action( 'restrict_manage_posts', array( $this, 'add_filters' ), 10, 2 );\n\t\tadd_filter( 'parse_query', array( $this, 'parse_query_filters' ), 10, 1 );\n\t}\n\n\t/**\n\t * Add course builder edit link.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @param array   $actions Existing actions.\n\t * @param WP_Post $post    Lesson's WP_Post object.\n\t * @return array\n\t */\n\tpublic function add_links( $actions, $post ) {\n\n\t\tif ( 'lesson' === $post->post_type && current_user_can( 'edit_lesson', $post->ID ) ) {\n\n\t\t\t$lesson = llms_get_post( $post->ID );\n\t\t\tif ( ! $lesson ) {\n\t\t\t\treturn $actions;\n\t\t\t}\n\n\t\t\t$course = $lesson->get( 'parent_course' );\n\t\t\t$url    = add_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t\t'course_id' => $course,\n\t\t\t\t),\n\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t);\n\t\t\t$url   .= sprintf( '#lesson:%d', $post->ID );\n\n\t\t\t$actions = array_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'llms-builder' => '<a href=\"' . esc_url( $url ) . '\">' . __( 'Builder', 'lifterlms' ) . '</a>',\n\t\t\t\t),\n\t\t\t\t$actions\n\t\t\t);\n\n\t\t}\n\n\t\treturn $actions;\n\t}\n\n\t/**\n\t * Add custom lesson columns.\n\t *\n\t * @since 3.2.3\n\t * @since 3.12.0 Unknown.\n\t * @since 7.1.0 Quiz column added.\n\t *\n\t * @param array $columns Array of default columns.\n\t * @return array\n\t */\n\tpublic function add_columns( $columns ) {\n\n\t\t$columns = array(\n\t\t\t'cb'      => '<input type=\"checkbox\" />',\n\t\t\t'title'   => __( 'Lesson Title', 'lifterlms' ),\n\t\t\t'course'  => __( 'Course', 'lifterlms' ),\n\t\t\t'section' => __( 'Section', 'lifterlms' ),\n\t\t\t'prereq'  => __( 'Prerequisite', 'lifterlms' ),\n\t\t\t'quiz'    => __( 'Quiz', 'lifterlms' ),\n\t\t\t'author'  => __( 'Author', 'lifterlms' ),\n\t\t\t'date'    => __( 'Date', 'lifterlms' ),\n\t\t);\n\n\t\treturn $columns;\n\t}\n\n\t/**\n\t * Add filters to the top of the post table\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param string $post_type Post Type of the current posts table.\n\t * @param string $which     Positioning of the filters [top|bottom].\n\t * @return void\n\t */\n\tpublic function add_filters( $post_type, $which ) {\n\n\t\t// Only for the correct post type & position.\n\t\tif ( 'lesson' !== $post_type || 'top' !== $which ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$selected = isset( $_GET['llms_filter_course_id'] ) ? absint( $_GET['llms_filter_course_id'] ) : false;\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped in the function.\n\t\techo LLMS_Admin_Post_Tables::get_post_type_filter_html( 'llms_filter_course_id', 'course', $selected );\n\t}\n\n\t/**\n\t * Manage content of custom lesson columns.\n\t *\n\t * @since 3.2.3\n\t * @since 3.24.0 Unknown.\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n\t * @since 7.1.0 Implemented content for the quiz column.\n\t *\n\t * @param string $column  Column key/name.\n\t * @param int    $post_id WP Post ID of the lesson for the row.\n\t * @return void\n\t */\n\tpublic function manage_columns( $column, $post_id ) {\n\n\t\t$lesson = llms_get_post( $post_id );\n\t\tif ( ! $lesson ) {\n\t\t\treturn;\n\t\t}\n\n\t\tswitch ( $column ) {\n\n\t\t\tcase 'course':\n\t\t\t\t$course    = $lesson->get( 'parent_course' );\n\t\t\t\t$edit_link = get_edit_post_link( $course );\n\n\t\t\t\tif ( ! empty( $course ) && get_post( $course ) ) {\n\t\t\t\t\tprintf( '<a href=\"%1$s\">%2$s</a>', esc_url( $edit_link ), esc_html( get_the_title( $course ) ) );\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'section':\n\t\t\t\t$section = $lesson->get_parent_section();\n\t\t\t\tif ( ! empty( $section ) ) {\n\t\t\t\t\techo esc_html( get_the_title( $section ) );\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'prereq':\n\t\t\t\tif ( $lesson->has_prerequisite() ) {\n\n\t\t\t\t\t$prereq    = $lesson->get( 'prerequisite' );\n\t\t\t\t\t$edit_link = get_edit_post_link( $prereq );\n\n\t\t\t\t\tif ( $prereq ) {\n\n\t\t\t\t\t\tprintf( '<a href=\"%1$s\">%2$s</a>', esc_url( $edit_link ), esc_html( get_the_title( $prereq ) ) );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\techo '&ndash;';\n\n\t\t\t\t\t}\n\t\t\t\t} else {\n\n\t\t\t\t\techo '&ndash;';\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'quiz':\n\t\t\t\t$course = $lesson->get( 'parent_course' );\n\t\t\t\t$url    = add_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t\t\t'course_id' => $course,\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t);\n\t\t\t\t$url   .= sprintf( '#lesson:%d:quiz', $post_id );\n\n\t\t\t\tif ( $lesson->has_quiz() ) {\n\n\t\t\t\t\t$label = __( 'Edit Quiz', 'lifterlms' );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t$label = __( 'Add Quiz', 'lifterlms' );\n\n\t\t\t\t}\n\n\t\t\t\techo '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $label ) . '</a>';\n\n\t\t\t\tbreak;\n\n\t\t}\n\t}\n\n\t/**\n\t * Modify the main WP Query\n\t *\n\t * @since 3.12.0\n\t * @since 4.5.1 Bail early if the query has no `post_type` property set.\n\t *\n\t * @param WP_Query $query The WordPress Query.\n\t * @return WP_Query\n\t */\n\tpublic function parse_query_filters( $query ) {\n\n\t\t// Only modify admin & main query.\n\t\tif ( ! ( is_admin() && $query->is_main_query() ) ) {\n\t\t\treturn $query;\n\t\t}\n\n\t\t// Don't proceed if it's not our post type.\n\t\tif ( ! isset( $query->query['post_type'] ) || 'lesson' !== $query->query['post_type'] ) {\n\t\t\treturn $query;\n\t\t}\n\n\t\t// If none of our custom filters are set, don't proceed.\n\t\tif ( ! isset( $_REQUEST['llms_filter_course_id'] ) ) {\n\t\t\treturn $query;\n\t\t}\n\n\t\t// Get the query or a default to work with.\n\t\t$meta_query = $query->get( 'meta_query' );\n\t\tif ( ! $meta_query ) {\n\t\t\t$meta_query = array();\n\t\t}\n\n\t\t/**\n\t\t * Set an and relation for our filters\n\t\t * if other filters already exist, we'll ensure we obey them as well this way.\n\t\t */\n\t\t$meta_query['relation'] = 'AND';\n\n\t\t$meta_query[] = array(\n\t\t\t'compare' => '=',\n\t\t\t'key'     => '_llms_parent_course',\n\t\t\t'value'   => absint( $_REQUEST['llms_filter_course_id'] ),\n\t\t);\n\n\t\t$query->set( 'meta_query', $meta_query );\n\n\t\treturn $query;\n\t}\n}\nreturn new LLMS_Admin_Post_Table_Lessons();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php",
    "content": "<?php\n/**\n * Add, Customize, and Manage LifterLMS Order Post Type Post Table Columns.\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 3.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Table_Orders class.\n *\n * @since 3.0.0\n */\nclass LLMS_Admin_Post_Table_Orders {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.0.0\n\t * @since 3.24.3 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'load-edit.php', array( $this, 'edit_load' ) );\n\t\tadd_filter( 'manage_llms_order_posts_columns', array( $this, 'add_columns' ), 10, 1 );\n\t\tadd_action( 'manage_llms_order_posts_custom_column', array( $this, 'manage_columns' ), 10, 2 );\n\t\tadd_filter( 'manage_edit-llms_order_sortable_columns', array( $this, 'sortable_columns' ) );\n\t\tadd_filter( 'pre_get_posts', array( $this, 'modify_admin_search' ), 10, 1 );\n\t\tadd_filter( 'post_row_actions', array( $this, 'modify_actions' ), 10, 2 );\n\t}\n\n\t/**\n\t * Order post. Appends custom columns to post grid.\n\t *\n\t * @since 3.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param array $columns Array of columns.\n\t * @return array\n\t */\n\tpublic function add_columns( $columns ) {\n\n\t\t$columns = array(\n\t\t\t'cb'             => '<input type=\"checkbox\" />',\n\t\t\t'order'          => __( 'Order', 'lifterlms' ),\n\t\t\t'payment_status' => __( 'Payment Status', 'lifterlms' ),\n\t\t\t'access_status'  => __( 'Access Status', 'lifterlms' ),\n\t\t\t'product'        => __( 'Product', 'lifterlms' ),\n\t\t\t'revenue'        => __( 'Revenue', 'lifterlms' ),\n\t\t\t'type'           => __( 'Order Type', 'lifterlms' ),\n\t\t\t'order_date'     => __( 'Date', 'lifterlms' ),\n\t\t);\n\n\t\treturn $columns;\n\t}\n\n\t/**\n\t * Order post: Queries data based on column name.\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t * @since 5.4.0 Inform about deleted products.\n\t * @since 7.0.0 Treat the case when the order has no WordPress user associated yet.\n\t *\n\t * @param string $column  Custom column name.\n\t * @param int    $post_id ID of the individual post.\n\t * @return void\n\t */\n\tpublic function manage_columns( $column, $post_id ) {\n\t\tglobal $post;\n\n\t\t$order = new LLMS_Order( $post_id );\n\n\t\tswitch ( $column ) {\n\n\t\t\tcase 'order':\n\t\t\t\techo '<a href=\"' . esc_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ) ) . '\">';\n\t\t\t\t\tprintf( esc_html_x( '#%d', 'order number display', 'lifterlms' ), esc_html( $post_id ) );\n\t\t\t\techo '</a> ';\n\n\t\t\t\tesc_html_e( 'by', 'lifterlms' );\n\t\t\t\techo ' ';\n\n\t\t\t\tif ( llms_parse_bool( $order->get( 'anonymized' ) ) || empty( llms_get_student( $order->get( 'user_id' ) ) ) ) {\n\t\t\t\t\techo esc_html( $order->get_customer_name() );\n\t\t\t\t} else {\n\t\t\t\t\t$edit_user_link = $order->get( 'user_id' ) ? get_edit_user_link( $order->get( 'user_id' ) ) : '';\n\t\t\t\t\techo ! $edit_user_link ? esc_html( $order->get_customer_name() ) . '<br>' : '<a href=\"' . esc_url( $edit_user_link ) . '\">' . esc_html( $order->get_customer_name() ) . '</a><br>';\n\t\t\t\t\techo '<a href=\"' . esc_url( 'mailto:' . $order->get( 'billing_email' ) ) . '\">' . esc_html( $order->get( 'billing_email' ) ) . '</a>';\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'payment_status':\n\t\t\t\t$status = $order->get( 'status' );\n\t\t\t\techo '<span class=\"llms-status llms-size--large ' . esc_attr( $status ) . ' \">' . esc_html( llms_get_order_status_name( $status ) ) . '</span>';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'access_status':\n\t\t\t\t$date = $order->get_access_expiration_date( 'F j, Y' );\n\t\t\t\t$ts   = strtotime( $date );\n\n\t\t\t\t// Timestamp will be false if date is not a date.\n\t\t\t\tif ( $ts ) {\n\n\t\t\t\t\tif ( $ts < current_time( 'timestamp' ) ) {\n\t\t\t\t\t\techo esc_html_x( 'Expired:', 'access plan expiration', 'lifterlms' );\n\t\t\t\t\t} else {\n\t\t\t\t\t\techo esc_html_x( 'Expires:', 'access plan expiration', 'lifterlms' );\n\t\t\t\t\t}\n\n\t\t\t\t\techo ' ' . esc_html( $date );\n\n\t\t\t\t} else {\n\n\t\t\t\t\techo esc_html( $date );\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'product':\n\t\t\t\tif ( llms_get_post( $order->get( 'product_id' ) ) ) {\n\t\t\t\t\techo '<a href=\"' . esc_url( get_edit_post_link( $order->get( 'product_id' ) ) ) . '\">' . esc_html( $order->get( 'product_title' ) ) . '</a>';\n\t\t\t\t} else {\n\t\t\t\t\techo esc_html__( '[DELETED]', 'lifterlms' ) . ' ' . esc_html( $order->get( 'product_title' ) );\n\t\t\t\t}\n\t\t\t\techo ' (' . esc_html( ucfirst( $order->get( 'product_type' ) ) ) . ')';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'revenue':\n\t\t\t\t$grosse = $order->get_revenue( 'grosse' );\n\t\t\t\t$net    = $order->get_revenue( 'net' );\n\n\t\t\t\tif ( $grosse !== $net ) {\n\t\t\t\t\techo '<del>' . wp_kses( llms_price( $grosse ), LLMS_ALLOWED_HTML_PRICES ) . '</del> ';\n\t\t\t\t}\n\n\t\t\t\techo wp_kses( llms_price( $net ), LLMS_ALLOWED_HTML_PRICES );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'type':\n\t\t\t\tif ( $order->is_recurring() ) {\n\t\t\t\t\tesc_html_e( 'Recurring', 'lifterlms' );\n\t\t\t\t} else {\n\t\t\t\t\tesc_html_e( 'One-time', 'lifterlms' );\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'order_date':\n\t\t\t\techo esc_html( $order->get_date( 'date' ) );\n\n\t\t\t\tbreak;\n\n\t\t}// End switch().\n\t}\n\n\t/**\n\t * Order post: Creates array of columns that will be sortable.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $columns Array of sortable columns.\n\t * @return array\n\t */\n\tpublic function sortable_columns( $columns ) {\n\n\t\t$columns['order']      = 'order';\n\t\t$columns['product']    = 'product';\n\t\t$columns['order_date'] = 'order_date';\n\n\t\treturn $columns;\n\t}\n\n\t/**\n\t * Order post: Adds custom sortable columns to WP request.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function edit_load() {\n\t\tadd_filter( 'request', array( $this, 'llms_sort_orders' ) );\n\t}\n\n\t/**\n\t * Order post: Applies custom query variables for sorting custom columns.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $vars Fost query args.\n\t * @return array\n\t */\n\tpublic function llms_sort_orders( $vars ) {\n\n\t\tif ( isset( $vars['post_type'] ) && 'llms_order' == $vars['post_type'] ) {\n\n\t\t\tif ( isset( $vars['orderby'] ) && 'order' == $vars['orderby'] ) {\n\t\t\t\t$vars = array_merge(\n\t\t\t\t\t$vars,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'orderby' => 'ID',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t} elseif ( isset( $vars['orderby'] ) && 'product' == $vars['orderby'] ) {\n\t\t\t\t$vars = array_merge(\n\t\t\t\t\t$vars,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'meta_key' => '_llms_product_title',\n\t\t\t\t\t\t'orderby'  => 'meta_value',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t} elseif ( isset( $vars['orderby'] ) && 'order_date' == $vars['orderby'] ) {\n\t\t\t\t$vars = array_merge(\n\t\t\t\t\t$vars,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'orderby' => 'date',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn $vars;\n\t}\n\n\t/**\n\t * Modify the actions for the orders.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array   $actions Existing actions.\n\t * @param WP_Post $post    Post object.\n\t * @return string[]\n\t */\n\tpublic function modify_actions( $actions, $post ) {\n\n\t\tif ( 'llms_order' !== $post->post_type ) {\n\t\t\treturn $actions;\n\t\t}\n\n\t\tunset( $actions['inline hide-if-no-js'] );\n\n\t\treturn $actions;\n\t}\n\n\n\t/**\n\t * Modify the search query for various post types before retrieving posts.\n\t *\n\t * @since 2.5.0\n\t * @since 3.24.3 Unknown\n\t * @since 3.35.0 Sanitize $_GET data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param WP_Query $query Query object.\n\t * @return WP_Query\n\t */\n\tpublic function modify_admin_search( $query ) {\n\n\t\t// On the admin posts order table.\n\t\t// Allow searching of custom fields.\n\t\tif ( is_admin() && ! empty( $query->query_vars['s'] ) && isset( $query->query_vars['post_type'] ) && 'llms_order' === $query->query_vars['post_type'] ) {\n\n\t\t\t// What we are searching for.\n\t\t\t$term = $query->query_vars['s'];\n\n\t\t\t// We have to kill this value so that the query actually works.\n\t\t\t$query->query_vars['s'] = '';\n\n\t\t\t// Add a filter back in so we don't have 'Search results for \"\"' on the top of the screen.\n\t\t\t// @note we're not super proud of this incredible piece of duct tape.\n\t\t\tadd_filter(\n\t\t\t\t'get_search_query',\n\t\t\t\tfunction ( $q ) {\n\t\t\t\t\tif ( '' === $q ) {\n\t\t\t\t\t\treturn llms_filter_input_sanitize_string( INPUT_GET, 's' );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tif ( is_numeric( $term ) ) {\n\t\t\t\t$query->query_vars['p'] = trim( intval( $term ) );\n\t\t\t\treturn $query;\n\t\t\t}\n\n\t\t\t// Search wp_users.\n\t\t\t$user_query = new WP_User_Query(\n\t\t\t\tarray(\n\t\t\t\t\t'search'         => '*' . esc_attr( $term ) . '*',\n\t\t\t\t\t'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ),\n\t\t\t\t\t'fields'         => 'ID',\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Search wp_usermeta for First and Last names.\n\t\t\t$user_query2 = new WP_User_Query(\n\t\t\t\tarray(\n\t\t\t\t\t'fields'     => 'ID',\n\t\t\t\t\t'meta_query' => array(\n\t\t\t\t\t\t'relation' => 'OR',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'key'     => 'first_name',\n\t\t\t\t\t\t\t'value'   => $term,\n\t\t\t\t\t\t\t'compare' => 'LIKE',\n\t\t\t\t\t\t),\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'key'     => 'last_name',\n\t\t\t\t\t\t\t'value'   => $term,\n\t\t\t\t\t\t\t'compare' => 'LIKE',\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$results = wp_parse_id_list( array_merge( (array) $user_query->get_results(), (array) $user_query2->get_results() ) );\n\n\t\t\t// Add metaquery for the user id.\n\t\t\t$meta_query = array(\n\t\t\t\t'relation' => 'OR',\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_user_id',\n\t\t\t\t\t'value'   => $results,\n\t\t\t\t\t'compare' => 'IN',\n\t\t\t\t),\n\t\t\t);\n\n\t\t\t// Set the query.\n\t\t\t$query->set( 'meta_query', $meta_query );\n\n\t\t}\n\n\t\treturn $query;\n\t}\n}\n\nreturn new LLMS_Admin_Post_Table_Orders();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php",
    "content": "<?php\n/**\n * Customize display of the \"Page\" post tables\n *\n * @package LifterLMS/Admin/PostTypes/PostTables/Classes\n *\n * @since 3.0.0\n * @version 3.7.5\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Post_Table_Pages class\n *\n * @since 3.0.0\n * @since 3.7.5 Unknown.\n */\nclass LLMS_Admin_Post_Table_Pages {\n\n\tpublic $pages = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function __construct() {\n\n\t\tif ( isset( $_GET['post_type'] ) && 'page' === $_GET['post_type'] ) {\n\n\t\t\tadd_action( 'init', array( $this, 'populate_pages' ) );\n\n\t\t\tadd_filter( 'display_post_states', array( $this, 'post_states' ), 10, 2 );\n\n\t\t}\n\t}\n\n\tpublic function populate_pages() {\n\t\t$pages = array(\n\t\t\t'checkout'    => __( 'LifterLMS Checkout', 'lifterlms' ),\n\t\t\t'courses'     => __( 'LifterLMS Course Catalog', 'lifterlms' ),\n\t\t\t'memberships' => __( 'LifterLMS Memberships Catalog', 'lifterlms' ),\n\t\t\t'myaccount'   => __( 'LifterLMS Student Dashboard', 'lifterlms' ),\n\t\t);\n\n\t\tforeach ( $pages as $key => $name ) {\n\t\t\t$id = llms_get_page_id( $key );\n\t\t\tif ( $id ) {\n\n\t\t\t\t$this->pages[ $id ] = $name;\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Add state information to pages that are set as LifterLMD pages\n\t *\n\t * @param    array $states  array of post states\n\t * @param    obj   $post    WP_Post object\n\t * @return   array\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function post_states( $states, $post ) {\n\n\t\tif ( isset( $this->pages[ $post->ID ] ) ) {\n\n\t\t\t$states[] = $this->pages[ $post->ID ];\n\n\t\t}\n\n\t\treturn $states;\n\t}\n}\n\nreturn new LLMS_Admin_Post_Table_Pages();\n"
  },
  {
    "path": "includes/admin/post-types/post-tables/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/post-types/tables/class.llms.table.student.management.php",
    "content": "<?php\n/**\n * Student Management table on Courses and Memberships\n *\n * @package LifterLMS/Admin/PostTypes/Tables/Classes\n *\n * @since 3.4.0\n * @version {version}\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Student Management table on Courses and Memberships class\n *\n * @since 3.4.0\n * @since 3.33.0 Added table action button to delete a cancelled enrollment.\n * @since 3.33.0 Added popover tooltip to the table action button icons via llms tooltip data attribute api.\n */\nclass LLMS_Table_StudentManagement extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'student-management';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'status';\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'name';\n\n\t/**\n\t * Post ID for the current table\n\t *\n\t * @var  int\n\t */\n\tprotected $post_id = null;\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @since 3.4.0\n\t * @since 3.33.0 Added action button to delete a cancelled enrollment.\n\t * @since 3.33.0 Added icon popover tooltip via llms tooltip data attribute api.\n\t *\n\t * @param string       $key     The column id / key.\n\t * @param LLMS_Student $student Student object.\n\t * @return mixed\n\t */\n\tpublic function get_data( $key, $student ) {\n\n\t\t$value = '';\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'actions':\n\t\t\t\tif ( $student->is_enrolled( $this->post_id ) ) {\n\t\t\t\t\t$trigger = $student->get_enrollment_trigger( $this->post_id );\n\t\t\t\t\tif ( false !== strpos( $trigger, 'order_' ) ) {\n\t\t\t\t\t\t$value = '<a class=\"llms-action-icon tip--top-left\" href=\"' . get_edit_post_link( $student->get_enrollment_trigger_id( $this->post_id ) ) . '\" target=\"_blank\" data-tip=\"' . __( 'Visit the triggering order to manage this student\\'s enrollment', 'lifterlms' ) . '\"><span class=\"dashicons dashicons-external\"></span></a>';\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif ( current_user_can( 'unenroll' ) ) {\n\t\t\t\t\t\t\t$value = '<a class=\"llms-action-icon llms-remove-student tip--top-left\" data-id=\"' . $student->get_id() . '\" href=\"#llms-student-remove\" data-tip=\"' . __( 'Cancel Enrollment', 'lifterlms' ) . '\"><span class=\"dashicons dashicons-no\"></span></a>';\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif ( current_user_can( 'enroll' ) ) {\n\t\t\t\t\t\t$value = '<a class=\"llms-action-icon llms-add-student tip--top-left\" data-id=\"' . $student->get_id() . '\" href=\"#llms-student-add\" data-tip=\"' . __( 'Reactivate Enrollment', 'lifterlms' ) . '\"><span class=\"dashicons dashicons-update\"></span></a>';\n\t\t\t\t\t}\n\t\t\t\t\tif ( current_user_can( 'unenroll' ) ) {\n\t\t\t\t\t\t$value .= '<a class=\"llms-action-icon danger llms-delete-enrollment tip--top-left\" data-id=\"' . $student->get_id() . '\" href=\"#llms-student-delete-enrollment\" data-tip=\"' . __( 'Delete Enrollment', 'lifterlms' ) . '\"><span class=\"dashicons dashicons-trash\"></span></a>';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'enrolled':\n\t\t\t\t$value = $student->get_enrollment_date( $this->post_id, 'updated' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'grade':\n\t\t\t\t$value = $student->get_grade( $this->post_id );\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$id = $student->get_id();\n\t\t\t\tif ( current_user_can( 'edit_users', $id ) ) {\n\t\t\t\t\t$value = '<a href=\"' . esc_url( get_edit_user_link( $id ) ) . '\">' . $id . '</a>';\n\t\t\t\t} else {\n\t\t\t\t\t$value = $id;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'last_lesson':\n\t\t\t\t$lid = $student->get_last_completed_lesson( $this->post_id );\n\t\t\t\tif ( $lid ) {\n\t\t\t\t\t$value = $this->get_post_link( $lid, llms_trim_string( get_the_title( $lid ), 30 ) );\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$first = $student->get( 'first_name' );\n\t\t\t\t$last  = $student->get( 'last_name' );\n\n\t\t\t\tif ( ! $first || ! $last ) {\n\t\t\t\t\t$value = $student->get( 'display_name' );\n\t\t\t\t} else {\n\t\t\t\t\t$value = $last . ', ' . $first;\n\t\t\t\t}\n\n\t\t\t\t$url   = add_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'page'       => 'llms-reporting',\n\t\t\t\t\t\t'tab'        => 'students',\n\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $value . '</a>';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'progress':\n\t\t\t\t$value = $student->get_progress( $this->post_id ) . '%';\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$value = llms_get_enrollment_status_name( $student->get_enrollment_status( $this->post_id ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'trigger':\n\t\t\t\t$trigger = $student->get_enrollment_trigger( $this->post_id );\n\t\t\t\tif ( $trigger && false !== strpos( $trigger, 'order_' ) ) {\n\t\t\t\t\t$tid   = $student->get_enrollment_trigger_id( $this->post_id );\n\t\t\t\t\t$value = $this->get_post_link( $tid, sprintf( __( 'Order #%d', 'lifterlms' ), $tid ) );\n\t\t\t\t} elseif ( $trigger && false !== strpos( $trigger, 'admin_' ) ) {\n\t\t\t\t\t$tid        = $student->get_enrollment_trigger_id( $this->post_id );\n\t\t\t\t\t$admin      = llms_get_student( $tid );\n\t\t\t\t\t$admin_name = $admin ? $admin->get_name() : __( '[Deleted]', 'lifterlms' );\n\t\t\t\t\t$value      = $this->get_user_link( $tid, sprintf( __( 'Admin: %1$s (#%2$d)', 'lifterlms' ), $admin_name, $tid ) );\n\t\t\t\t} else {\n\t\t\t\t\t$value = $trigger;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $this->filter_get_data( $value, $key, $student );\n\n\t}\n\n\t/**\n\t * Retrieve a list of IDs for all the users enrollments\n\t *\n\t * @param    obj $student  instance of LLMS_Student\n\t * @return   array             array of course ids\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tprivate function get_enrollments( $student ) {\n\n\t\t$r = array();\n\n\t\t$page = 1;\n\t\t$skip = 0;\n\n\t\twhile ( true ) {\n\n\t\t\t$courses = $student->get_courses(\n\t\t\t\tarray(\n\t\t\t\t\t'limit' => 5000,\n\t\t\t\t\t'skip'  => 5000 * ( $page - 1 ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$r = array_merge( $courses['results'] );\n\n\t\t\tif ( ! $courses['more'] ) {\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\t$page++;\n\t\t\t}\n\t\t}\n\n\t\treturn $r;\n\n\t}\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input\n\t *\n\t * @return   string\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\t\treturn apply_filters( 'llms_gradebook_get_' . $this->id . '_search_placeholder', __( 'Search students by name or email...', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @since 3.4.0\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Manage Existing Enrollments', 'lifterlms' );\n\n\t\tif ( ! $args ) {\n\t\t\t$args = $this->get_args();\n\t\t}\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$this->post_id = $args['post_id'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->get_order();\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->get_orderby();\n\n\t\t$sort = array();\n\t\tswitch ( $this->get_orderby() ) {\n\t\t\tcase 'enrolled':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'date'       => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'id' => $this->get_order(),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'last_name'  => $this->get_order(),\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'status'     => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t$query_args = array(\n\t\t\t'page'     => $this->get_current_page(),\n\t\t\t'post_id'  => $args['post_id'],\n\t\t\t'per_page' => apply_filters( 'llms_' . $this->id . '_table_students_per_page', 20 ),\n\t\t\t'sort'     => $sort,\n\t\t);\n\n\t\tif ( 'status' === $this->get_filterby() && 'any' !== $this->get_filter() ) {\n\n\t\t\t$query_args['statuses'] = array( $this->get_filter() );\n\n\t\t}\n\n\t\tif ( isset( $args['search'] ) ) {\n\n\t\t\t$this->search         = $args['search'];\n\t\t\t$query_args['search'] = $this->get_search();\n\n\t\t}\n\n\t\t$query = new LLMS_Student_Query( $query_args );\n\n\t\t$this->max_pages    = $query->get_max_pages();\n\t\t$this->is_last_page = $query->is_last_page();\n\n\t\t$this->tbody_data = $query->get_students();\n\n\t}\n\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @return   array\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function set_args() {\n\n\t\tif ( ! $this->post_id ) {\n\t\t\tglobal $post;\n\t\t\t$this->post_id = ! empty( $post->ID ) ? $post->ID : null;\n\t\t}\n\n\t\treturn array(\n\t\t\t'post_id' => $this->post_id,\n\t\t);\n\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function set_columns() {\n\t\t$cols = array(\n\t\t\t'id'          => array(\n\t\t\t\t'sortable' => true,\n\t\t\t\t'title'    => __( 'ID', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name'        => array(\n\t\t\t\t'sortable' => true,\n\t\t\t\t'title'    => __( 'Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'status'      => array(\n\t\t\t\t'filterable' => llms_get_enrollment_statuses(),\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Status', 'lifterlms' ),\n\t\t\t),\n\t\t\t'enrolled'    => array(\n\t\t\t\t'sortable' => true,\n\t\t\t\t'title'    => __( 'Enrollment Updated', 'lifterlms' ),\n\t\t\t),\n\t\t\t'progress'    => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Progress', 'lifterlms' ),\n\t\t\t),\n\t\t\t'grade'       => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Grade', 'lifterlms' ),\n\t\t\t),\n\t\t\t'last_lesson' => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Last Lesson', 'lifterlms' ),\n\t\t\t),\n\t\t\t'trigger'     => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Enrollment Trigger', 'lifterlms' ),\n\t\t\t),\n\t\t\t'actions'     => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => '&nbsp;',\n\t\t\t),\n\t\t);\n\t\t$args = $this->get_args();\n\t\tif ( 'llms_membership' === get_post_type( $args['post_id'] ) ) {\n\t\t\tunset( $cols['grade'] );\n\t\t\tunset( $cols['progress'] );\n\t\t\tunset( $cols['last_lesson'] );\n\t\t}\n\t\treturn $cols;\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/post-types/tables/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/reporting/class.llms.admin.reporting.php",
    "content": "<?php\n/**\n * Admin Reporting Base Class\n *\n * @package LifterLMS/Admin/Reporting/Classes\n *\n * @since 3.2.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Reporting Base class.\n *\n * @since 3.2.0\n * @since 3.31.0 Fix redundant `if` statement in the `output_widget` method.\n * @since 3.32.0 Added Memberships tab.\n * @since 3.32.0 The `output_event()` method now outputs the student's avatar whent in 'membership' context.\n * @since 3.35.0 Sanitize input data.\n * @since 3.36.3 Fixed sanitization for input data array.\n */\nclass LLMS_Admin_Reporting {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.2.0\n\t */\n\tpublic function __construct() {\n\n\t\tself::includes();\n\t}\n\n\t/**\n\t * Get array of course IDs selected according to applied filters.\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.36.3 Fixed sanitization for input data array.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return array\n\t */\n\tpublic static function get_current_courses() {\n\n\t\t$r = isset( $_GET['course_ids'] ) ? llms_filter_input( INPUT_GET, 'course_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ) : array();\n\n\t\tif ( '' === $r ) {\n\t\t\t$r = array();\n\t\t}\n\t\tif ( is_string( $r ) ) {\n\t\t\t$r = array_map( 'absint', explode( ',', $r ) );\n\t\t}\n\t\treturn $r;\n\t}\n\n\t/**\n\t * Get array of membership IDs selected according to applied filters.\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.36.3 Fixed sanitization for input data array.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return array\n\t */\n\tpublic static function get_current_memberships() {\n\n\t\t$r = isset( $_GET['membership_ids'] ) ? llms_filter_input( INPUT_GET, 'membership_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ) : array();\n\n\t\tif ( '' === $r ) {\n\t\t\t$r = array();\n\t\t}\n\t\tif ( is_string( $r ) ) {\n\t\t\t$r = array_map( 'absint', explode( ',', $r ) );\n\t\t}\n\t\treturn $r;\n\t}\n\n\t/**\n\t * Get the currently selected date range filter.\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string\n\t */\n\tpublic static function get_current_range() {\n\n\t\treturn ( isset( $_GET['range'] ) ) ? llms_filter_input_sanitize_string( INPUT_GET, 'range' ) : 'last-7-days';\n\t}\n\n\t/**\n\t * Get array of student IDs according to current filters.\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.36.3 Fixed sanitization for input data array.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return array\n\t */\n\tpublic static function get_current_students() {\n\n\t\t$r = isset( $_GET['student_ids'] ) ? llms_filter_input( INPUT_GET, 'student_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ) : array();\n\t\tif ( '' === $r ) {\n\t\t\t$r = array();\n\t\t}\n\t\tif ( is_string( $r ) ) {\n\t\t\t$r = array_map( 'absint', explode( ',', $r ) );\n\t\t}\n\t\treturn $r;\n\t}\n\n\t/**\n\t * Retrieve the current reporting tab.\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string\n\t */\n\tpublic static function get_current_tab() {\n\n\t\treturn isset( $_GET['tab'] ) ? llms_filter_input_sanitize_string( INPUT_GET, 'tab' ) : 'students';\n\t}\n\n\t/**\n\t * Get the current end date according to filters.\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string\n\t */\n\tpublic static function get_date_end() {\n\n\t\treturn ( isset( $_GET['date_end'] ) ) ? llms_filter_input_sanitize_string( INPUT_GET, 'date_end' ) : '';\n\t}\n\n\t/**\n\t * Get the current start date according to filters.\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string\n\t */\n\tpublic static function get_date_start() {\n\n\t\treturn ( isset( $_GET['date_start'] ) ) ? llms_filter_input_sanitize_string( INPUT_GET, 'date_start' ) : '';\n\t}\n\n\t/**\n\t * Get dates via the current date string.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @param string $range Date range string.\n\t * @return array\n\t */\n\tpublic static function get_dates( $range ) {\n\n\t\t$now = current_time( 'timestamp' );\n\n\t\t$dates = array(\n\t\t\t'start' => '',\n\t\t\t'end'   => date( 'Y-m-d', $now ),\n\t\t);\n\n\t\tswitch ( $range ) {\n\n\t\t\tcase 'this-year':\n\t\t\t\t$dates['start'] = date( 'Y', $now ) . '-01-01';\n\t\t\t\tbreak;\n\n\t\t\tcase 'last-month':\n\t\t\t\t$dates['start'] = date( 'Y-m-d', strtotime( 'first day of last month', $now ) );\n\t\t\t\t$dates['end']   = date( 'Y-m-d', strtotime( 'last day of last month', $now ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'this-month':\n\t\t\t\t$dates['start'] = date( 'Y-m', $now ) . '-01';\n\t\t\t\tbreak;\n\n\t\t\tcase 'last-7-days':\n\t\t\t\t$dates['start'] = date( 'Y-m-d', strtotime( '-7 days', $now ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'custom':\n\t\t\t\t$dates['start'] = self::get_date_start();\n\t\t\t\t$dates['end']   = self::get_date_end();\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn $dates;\n\t}\n\n\t/**\n\t * Returns an admin URL with the given arguments added as query variables.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @param array $args Arguments to add to the query string.\n\t * @return string\n\t */\n\tpublic static function get_current_tab_url( $args = array() ) {\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'page' => 'llms-reporting',\n\t\t\t\t'tab'  => self::get_current_tab(),\n\t\t\t)\n\t\t);\n\t\treturn add_query_arg( $args, admin_url( 'admin.php' ) );\n\t}\n\n\t/**\n\t * Retrieves arguments for {@see LifterLMS_Admin_Reporting::output_widget}.\n\t *\n\t * Merges the supplied arguments with the default args.\n\t *\n\t * @since 6.11.0\n\t *\n\t * @param array $args Widget settings and data, {@see LifterLMS_Adming_Reporting::output_widget}.\n\t * @return array Merged arguments.\n\t */\n\tprivate static function get_output_widget_args( $args = array() ) {\n\n\t\treturn wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'id'           => '',\n\t\t\t\t'text'         => '',\n\t\t\t\t'data'         => '',\n\t\t\t\t'data_compare' => '',\n\t\t\t\t'data_type'    => 'numeric', // Enum: numeric, monetary, text, percentage, or date.\n\t\t\t\t'icon'         => '',\n\t\t\t\t'impact'       => 'positive', // Enum: positive or negative.\n\t\t\t\t'cols'         => 'd-1of2',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an array of period filters used by self::output_widget_range_filter().\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_period_filters() {\n\n\t\treturn array(\n\t\t\t'today'      => esc_attr__( 'Today', 'lifterlms' ),\n\t\t\t'yesterday'  => esc_attr__( 'Yesterday', 'lifterlms' ),\n\t\t\t'week'       => esc_attr__( 'This Week', 'lifterlms' ),\n\t\t\t'last_week'  => esc_attr__( 'Last Week', 'lifterlms' ),\n\t\t\t'month'      => esc_attr__( 'This Month', 'lifterlms' ),\n\t\t\t'last_month' => esc_attr__( 'Last Month', 'lifterlms' ),\n\t\t\t'year'       => esc_attr__( 'This Year', 'lifterlms' ),\n\t\t\t'last_year'  => esc_attr__( 'Last Year', 'lifterlms' ),\n\t\t\t'all_time'   => esc_attr__( 'All Time', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Get the full URL to a sub-tab within a reporting screen.\n\t *\n\t * @since 3.2.0\n\t * @since 3.32.0 Added Memberships tab.\n\t * @since 3.35.0 Sanitize input data.\n\t *\n\t * @param string $stab Slug of the sub-tab.\n\t * @return string\n\t */\n\tpublic static function get_stab_url( $stab ) {\n\n\t\t$args = array(\n\t\t\t'page' => 'llms-reporting',\n\t\t\t'tab'  => self::get_current_tab(),\n\t\t\t'stab' => $stab,\n\t\t);\n\n\t\tswitch ( self::get_current_tab() ) {\n\t\t\tcase 'memberships':\n\t\t\t\t$args['membership_id'] = llms_filter_input( INPUT_GET, 'membership_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t\tbreak;\n\n\t\t\tcase 'courses':\n\t\t\t\t$args['course_id'] = llms_filter_input( INPUT_GET, 'course_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t\tbreak;\n\n\t\t\tcase 'students':\n\t\t\t\t$args['student_id'] = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t\tbreak;\n\n\t\t\tcase 'quizzes':\n\t\t\t\t$args['quiz_id'] = llms_filter_input( INPUT_GET, 'quiz_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn add_query_arg( $args, admin_url( 'admin.php' ) );\n\t}\n\n\t/**\n\t * Get an array of tabs to output in the main reporting menu.\n\t *\n\t * @since 3.2.0\n\t * @since 3.32.0 Added Memberships tab.\n\t *\n\t * @return array\n\t */\n\tprivate function get_tabs() {\n\n\t\t$tabs = array(\n\t\t\t'students'    => __( 'Students', 'lifterlms' ),\n\t\t\t'courses'     => __( 'Courses', 'lifterlms' ),\n\t\t\t'memberships' => __( 'Memberships', 'lifterlms' ),\n\t\t\t'quizzes'     => __( 'Quizzes', 'lifterlms' ),\n\t\t\t'sales'       => __( 'Sales', 'lifterlms' ),\n\t\t\t'enrollments' => __( 'Enrollments', 'lifterlms' ),\n\t\t);\n\t\tforeach ( $tabs as $slug => $tab ) {\n\t\t\tif ( ! current_user_can( $this->get_tab_cap( $slug ) ) ) {\n\t\t\t\tunset( $tabs[ $slug ] );\n\t\t\t}\n\t\t}\n\t\treturn apply_filters( 'lifterlms_reporting_tabs', $tabs );\n\t}\n\n\t/**\n\t * Get the WP capability required to access a reporting tab.\n\t *\n\t * Defaults to 'view_lifterlms_reports'. Most reports implement additional permissions within the view.\n\t * Sales & Enrollments tab requires 'view_others_lifterlms_reports' b/c they don't add any additional filters\n\t * within the view.\n\t *\n\t * @since 3.19.4\n\t * @since 7.3.0 Use `in_array()` with strict type comparison.\n\t *\n\t * @param string $tab ID/slug of the tab.\n\t * @return string\n\t */\n\tprivate function get_tab_cap( $tab = null ) {\n\n\t\t$tab = is_null( $tab ) ? self::get_current_tab() : $tab;\n\n\t\t$cap = 'view_lifterlms_reports';\n\t\tif ( in_array( $tab, array( 'sales', 'enrollments' ), true ) ) {\n\t\t\t$cap = 'view_others_lifterlms_reports';\n\t\t}\n\n\t\t/**\n\t\t * Filters the WP capability required to access a reporting tab.\n\t\t *\n\t\t * @since 3.19.4\n\t\t * @since 7.3.0 Added the `$tab` parameter.\n\t\t *\n\t\t * @param string      $cap The required WP capability.\n\t\t * @param string|null $tab ID/slug of the tab.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_reporting_tab_cap', $cap, $tab );\n\t}\n\n\t/**\n\t * Retrieve an array of data to pass to the reporting page template.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_template_data() {\n\n\t\treturn array(\n\t\t\t'current_tab' => self::get_current_tab(),\n\t\t\t'tabs'        => $this->get_tabs(),\n\t\t);\n\t}\n\n\t/**\n\t * Include all required classes & files for the Reporting screens.\n\t *\n\t * @since 3.2.0\n\t * @since 3.16.0 Unknown.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic static function includes() {\n\n\t\t// Include tab classes.\n\t\tforeach ( glob( LLMS_PLUGIN_DIR . '/includes/admin/reporting/tabs/*.php' ) as $filename ) {\n\t\t\tinclude_once $filename;\n\t\t}\n\t}\n\n\t/**\n\t * Output the reporting screen HTML.\n\t *\n\t * @since 3.2.0\n\t * @since 3.19.4 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\tif ( ! current_user_can( $this->get_tab_cap() ) ) {\n\t\t\twp_die( esc_html__( 'You don\\'t have permission to do that', 'lifterlms' ) );\n\t\t}\n\n\t\tllms_get_template( 'admin/reporting/reporting.php', $this->get_template_data() );\n\t}\n\n\t/**\n\t * Output the HTML for a postmeta event in the recent events sidebar of various reporting screens.\n\t *\n\t * @since 3.15.0\n\t * @since 3.32.0 Outputs the student's avatar when in 'membership' context.\n\t *\n\t * @param LLMS_User_Postmeta $event   Instance of an LLMS_User_Postmeta item.\n\t * @param string             $context Optional. Display context [course|student|quiz|membership]. Default 'course'.\n\t * @return void\n\t */\n\tpublic static function output_event( $event, $context = 'course' ) {\n\n\t\t$student = $event->get_student();\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$url = $event->get_link( $context );\n\n\t\t?>\n\t\t<div class=\"llms-reporting-event <?php echo esc_attr( $event->get( 'meta_key' ) ); ?> <?php echo esc_attr( $event->get( 'meta_value' ) ); ?>\">\n\n\t\t\t<?php if ( $url ) : ?>\n\t\t\t\t<a href=\"<?php echo esc_url( $url ); ?>\">\n\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( 'course' === $context || 'membership' === $context || 'quiz' === $context ) : ?>\n\t\t\t\t\t<?php echo wp_kses_post( $student->get_avatar( 24 ) ); ?>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php echo wp_kses_post( $event->get_description( $context ) ); ?>\n\t\t\t\t<time datetime=\"<?php echo esc_attr( $event->get( 'updated_date' ) ); ?>\"><?php echo esc_attr( llms_get_date_diff( current_time( 'timestamp' ), $event->get( 'updated_date' ), 1 ) ); ?></time>\n\n\t\t\t<?php if ( $url ) : ?>\n\t\t\t\t</a>\n\t\t\t<?php endif; ?>\n\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Outputs the HTML for a reporting widget.\n\t *\n\t * @since 3.15.0\n\t * @since 3.31.0 Remove redundant `if` statement.\n\t * @since 6.11.0 Moved HTML into a view file.\n\t *               Fixed division by zero error encountered during data comparisons when `$data` is `0`.\n\t *               Added a check to ensure only numeric, monetary, or percentage data types will generate comparison data.\n\t * @since 7.3.0 Better rounding of float values of percentage data types.\n\t *\n\t * @param array $args {\n\t *    Array of widget options and data to be displayed.\n\t *\n\t *    @type string           $id           Required. A unique identifier for the widget.\n\t *    @type string           $text         A short description of the widget's data.\n\t *    @type int|string|float $data         The value of the data to display.\n\t *    @type int|string|float $data_compare Additional data to compare $data against.\n\t *    @type string           $data_type    The type of data. Used to format displayed data. Accepts \"numeric\",\n\t *                                         \"monetary\", \"text\", \"percentage\", or \"date\".\n\t *    @type string           $icon         An optional Font Awesome icon used to help visually identify the widget.\n\t *                                         If supplied, should be supplied without the `fa-` icon prefix.\n\t *    @type string           $impact       The type of impact the data has, either \"positive\" or \"negative\". This\n\t *                                         is used when displaying comparisons to determine if the change was a positive\n\t *                                         change or negative change. For example: student enrollments has a positive\n\t *                                         impact while quiz failures has a negative impact. An increase in enrollments\n\t *                                         will be displayed in green while a decrease will be displayed in red. An\n\t *                                         increase in quiz failures will be displayed in red while a decrease will be\n\t *                                         displayed in green.\n\t *    @type string           $cols         Grid class widget width ID. See: assets/scss/admin/partials/_grid.scss.\n\t * }\n\t * @return void\n\t */\n\tpublic static function output_widget( $args = array() ) {\n\n\t\t$args = self::get_output_widget_args( $args );\n\n\t\t// Only these data types can make comparisons.\n\t\t$can_compare = in_array( $args['data_type'], array( 'numeric', 'monetary', 'percentage' ), true );\n\n\t\t// Adds a percentage symbol after data.\n\t\t$data_after = 'percentage' === $args['data_type'] && is_numeric( $args['data'] ) ? '<sup>%</sup>' : '';\n\n\t\t$change             = false;\n\t\t$compare_operator   = '';\n\t\t$compare_class      = '';\n\t\t$compare_title      = '';\n\t\t$floating_precision = llms_get_floats_rounding_precision();\n\n\t\tif ( $can_compare && $args['data_compare'] && floatval( $args['data'] ) ) {\n\t\t\t$change           = round( ( $args['data'] - $args['data_compare'] ) / $args['data'] * 100, $floating_precision );\n\t\t\t$compare_operator = ( $change <= 0 ) ? '' : '+';\n\t\t\t$compare_title    = sprintf(\n\t\t\t\t// Translators: %s = The value of the data from the previous data set.\n\t\t\t\tesc_attr__( 'Previously %s', 'lifterlms' ),\n\t\t\t\tround( $args['data_compare'], $floating_precision ) . wp_strip_all_tags( $data_after )\n\t\t\t);\n\n\t\t\t$compare_class = ( $change <= 0 ) ? 'negative' : 'positive';\n\t\t\tif ( 'negative' === $args['impact'] ) {\n\t\t\t\t$compare_class = ( $change <= 0 ) ? 'positive' : 'negative';\n\t\t\t}\n\t\t}\n\n\t\tif ( is_numeric( $args['data'] ?? '' ) ) {\n\t\t\tif ( 'percentage' === $args['data_type'] ) {\n\t\t\t\t$args['data'] = round( $args['data'], $floating_precision );\n\t\t\t} elseif ( 'monetary' === $args['data_type'] ) {\n\t\t\t\t$args['data']         = llms_price( $args['data'] );\n\t\t\t\t$args['data_compare'] = llms_price_raw( $args['data_compare'] );\n\t\t\t}\n\t\t}\n\n\t\t$args['id'] = esc_attr( $args['id'] );\n\n\t\tinclude LLMS_PLUGIN_DIR . 'includes/admin/views/reporting/widget.php';\n\t}\n\n\t/**\n\t * Output a range filter select.\n\t *\n\t * Used by overview data tabs\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $selected_period Currently selected period.\n\t * @param string $tab             Current tab name.\n\t * @param array  $args            Additional args to be passed when form is submitted.\n\t * @return void\n\t */\n\tpublic static function output_widget_range_filter( $selected_period, $tab, $args = array() ) {\n\t\t?>\n\t\t<div class=\"llms-reporting-tab-filter\">\n\t\t\t<form action=\"<?php echo esc_url( admin_url( 'admin.php' ) ); ?>\" method=\"GET\">\n\t\t\t\t<select class=\"llms-select2\" name=\"period\" onchange=\"this.form.submit();\">\n\t\t\t\t\t<?php foreach ( self::get_period_filters() as $val => $text ) : ?>\n\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $val ); ?>\"<?php selected( $val, $selected_period ); ?>><?php echo esc_html( $text ); ?></option>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</select>\n\t\t\t\t<input type=\"hidden\" name=\"page\" value=\"llms-reporting\">\n\t\t\t\t<input type=\"hidden\" name=\"tab\" value=\"<?php echo esc_attr( $tab ); ?>\">\n\t\t\t\t<?php foreach ( $args as $key => $val ) : ?>\n\t\t\t\t\t<input type=\"hidden\" name=\"<?php echo esc_attr( $key ); ?>\" value=\"<?php echo esc_attr( $val ); ?>\">\n\t\t\t\t<?php endforeach; ?>\n\t\t\t</form>\n\t\t</div>\n\t\t<?php\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/reporting/tables/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.achievements.php",
    "content": "<?php\n/**\n * LLMS_Table_Achievements class file\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.2.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Display the student achievements reporting table.\n *\n * @since 3.2.0\n * @since 6.0.0 Allow pagination.\n */\nclass LLMS_Table_Achievements extends LLMS_Admin_Table {\n\n\tuse LLMS_Trait_Earned_Engagement_Reporting_Table;\n\n\t/**\n\t * Unique ID for the Table.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'achievements';\n\n\t/**\n\t * Instance of LLMS_Student.\n\t *\n\t * @var null\n\t */\n\tprotected $student = null;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links.\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Get HTML for buttons in the actions cell of the table.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Show a button to edit earned achievements.\n\t *\n\t * @param int $achievement_id WP Post ID of the achievement post.\n\t * @return void\n\t */\n\tprivate function get_actions_html( $achievement_id ) {\n\t\tob_start();\n\t\t?>\n\t\t<?php if ( get_edit_post_link( $achievement_id ) ) : ?>\n\t\t<a class=\"llms-button-secondary small\" href=\"<?php echo esc_url( get_edit_post_link( $achievement_id ) ); ?>\">\n\t\t\t<?php esc_html_e( 'Edit', 'lifterlms' ); ?>\n\t\t\t<i class=\"fa fa-pencil\" aria-hidden=\"true\"></i>\n\t\t</a>\n\t\t<?php endif; ?>\n\t\t<form action=\"\" method=\"POST\" style=\"display:inline;\">\n\n\t\t\t<button type=\"submit\" class=\"llms-button-danger small\" id=\"llms_delete_achievement\" name=\"llms_delete_achievement\">\n\t\t\t\t<?php esc_html_e( 'Delete', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t\t<input type=\"hidden\" name=\"achievement_id\" value=\"<?php echo absint( $achievement_id ); ?>\">\n\t\t\t<?php wp_nonce_field( 'llms-achievement-actions', '_llms_achievement_actions_nonce' ); ?>\n\n\t\t</form>\n\n\t\t<script>document.getElementById( 'llms_delete_achievement' ).onclick = function( e ) {\n\t\t\treturn window.confirm( '<?php esc_attr_e( 'Are you sure you want to delete this achievement? This action cannot be undone!', 'lifterlms' ); ?>' );\n\t\t};</script>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Retrieve data for the columns.\n\t *\n\t * @since 3.2.0\n\t * @since 3.18.0 Unknown.\n\t * @since 6.0.0 Retrieve earned date using the LLMS_User_Achievement model.\n\t *\n\t * @param  string                $key         The column id / key.\n\t * @param  LLMS_User_Achievement $achievement Object of achievement data.\n\t * @return mixed\n\t */\n\tpublic function get_data( $key, $achievement ) {\n\n\t\t// Handle old object being passed in.\n\t\tif ( ! is_a( $achievement, 'LLMS_User_Achievement' ) && property_exists( $achievement, 'achievement_id' ) ) {\n\t\t\t$achievement = new LLMS_User_Achievement( $achievement->certificate_id );\n\t\t}\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'actions':\n\t\t\t\t$value = $this->get_actions_html( $achievement->get( 'id' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'related':\n\t\t\t\tif ( $achievement->get( 'related' ) && 'llms_achievement' !== get_post_type( $achievement->get( 'related' ) ) ) {\n\t\t\t\t\tif ( is_numeric( $achievement->get( 'related' ) ) ) {\n\t\t\t\t\t\t$value = $this->get_post_link( $achievement->get( 'related' ), get_the_title( $achievement->get( 'related' ) ) );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$value = $achievement->get( 'related' );\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'earned':\n\t\t\t\t$value = $achievement->get_earned_date();\n\t\t\t\t$value = 'future' === $achievement->get( 'status' ) ? $value . ' ' . __( '(scheduled)', 'lifterlms' ) : $value;\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $achievement->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'image':\n\t\t\t\t$src   = $achievement->get_image( array( 32, 32 ) );\n\t\t\t\t$value = '<img src=\"' . esc_url( $src ) . '\" alt=\"' . $achievement->get( 'title' ) . '\" width=\"32\" height=\"32\">';\n\t\t\t\tbreak;\n\n\t\t\tcase 'template_id':\n\t\t\t\t// Prior to 3.2 this data wasn't recorded.\n\t\t\t\t$template = $achievement->get( 'parent' );\n\t\t\t\tif ( $template ) {\n\t\t\t\t\t$value = $this->get_post_link( $template );\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$value = $achievement->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}\n\n\t\t// Pass the \"legacy\" object to the filter.\n\t\t$backwards_compat_obj = array(\n\t\t\t'post_id'        => $achievement->get( 'related' ),\n\t\t\t'achievement_id' => $achievement->get( 'id' ),\n\t\t\t'earned_date'    => $achievement->get_earned_date(),\n\t\t);\n\n\t\treturn $this->filter_get_data( $value, $key, (object) $backwards_compat_obj );\n\t}\n\n\t/**\n\t * Get table results.\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Don't use deprecated signature for retrieving achievements.\n\t *              Paginate results.\n\t *\n\t * @param array $args Table query arguments.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$student_id = is_numeric( $args['student'] ) ? absint( $args['student'] ) : ( $args['student'] ? $args['student']->get_id() : 0 );\n\t\tif ( $student_id && ! current_user_can( 'view_others_lifterlms_reports' ) && ! llms_current_user_can( 'view_lifterlms_reports', $student_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_numeric( $args['student'] ) ) {\n\t\t\t$args['student'] = new LLMS_Student( $args['student'] );\n\t\t}\n\n\t\t$this->student = $args['student'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$query = $this->student->get_achievements(\n\t\t\tarray(\n\t\t\t\t'per_page' => 10,\n\t\t\t\t'status'   => array( 'publish', 'future' ),\n\t\t\t\t'paged'    => $this->current_page,\n\t\t\t\t'sort'     => array(\n\t\t\t\t\t'date' => 'ASC',\n\t\t\t\t\t'ID'   => 'ASC',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\t$this->max_pages = $query->get_max_pages();\n\n\t\tif ( $this->max_pages > $this->current_page ) {\n\t\t\t$this->is_last_page = false;\n\t\t}\n\n\t\t$this->tbody_data = $query->get_awards();\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since 2.3.0\n\t * @since 3.35.0 Get student ID more reliably.\n\t *\n\t * @return array\n\t */\n\tpublic function set_args() {\n\n\t\t$student = false;\n\t\tif ( ! empty( $this->student ) ) {\n\t\t\t$student = $this->student->get_id();\n\t\t} elseif ( ! empty( $_GET['student_id'] ) ) {\n\t\t\t$student = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t}\n\n\t\treturn array(\n\t\t\t'student' => $student,\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.2.0\n\t * @version  3.18.0\n\t */\n\tprotected function set_columns() {\n\t\treturn array(\n\t\t\t'id'          => __( 'ID', 'lifterlms' ),\n\t\t\t'template_id' => __( 'Template ID', 'lifterlms' ),\n\t\t\t'name'        => __( 'Achievement Title', 'lifterlms' ),\n\t\t\t'image'       => __( 'Image', 'lifterlms' ),\n\t\t\t'earned'      => __( 'Earned Date', 'lifterlms' ),\n\t\t\t'related'     => __( 'Related Post', 'lifterlms' ),\n\t\t\t'actions'     => '',\n\t\t);\n\t}\n\n\t/**\n\t * Empty message displayed when no results are found\n\t *\n\t * @return   string\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tprotected function set_empty_message() {\n\t\treturn __( 'This student has not yet earned any achievements.', 'lifterlms' );\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.certificates.php",
    "content": "<?php\n/**\n * Admin Student Certificates Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.2.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Student_Certificates class.\n *\n * @since 3.2.0\n * @since 3.35.0 Get student ID more reliably.\n * @since 6.0.0 Allow pagination.\n */\nclass LLMS_Table_Student_Certificates extends LLMS_Admin_Table {\n\n\tuse LLMS_Trait_Earned_Engagement_Reporting_Table;\n\n\t/**\n\t * Unique ID for the Table.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'certificates';\n\n\t/**\n\t * Instance of LLMS_Student.\n\t *\n\t * @var null\n\t */\n\tprotected $student = null;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links.\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Get HTML for buttons in the actions cell of the table.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Show a button to edit earned certificates.\n\t *\n\t * @param int $certificate_id  WP Post ID of the llms_my_certificate\n\t * @return void\n\t */\n\tprivate function get_actions_html( $certificate_id ) {\n\t\tob_start();\n\t\t?>\n\t\t<a class=\"llms-button-secondary small\" href=\"<?php echo esc_url( get_permalink( $certificate_id ) ); ?>\" target=\"_blank\">\n\t\t\t<?php esc_html_e( 'View', 'lifterlms' ); ?>\n\t\t\t<i class=\"fa fa-external-link\" aria-hidden=\"true\"></i>\n\t\t</a>\n\t\t<?php if ( get_edit_post_link( $certificate_id ) ) : ?>\n\t\t<a class=\"llms-button-secondary small\" href=\"<?php echo esc_url( get_edit_post_link( $certificate_id ) ); ?>\">\n\t\t\t<?php esc_html_e( 'Edit', 'lifterlms' ); ?>\n\t\t\t<i class=\"fa fa-pencil\" aria-hidden=\"true\"></i>\n\t\t</a>\n\t\t<?php endif; ?>\n\t\t<form action=\"\" method=\"POST\" style=\"display:inline;\">\n\n\t\t\t<button type=\"submit\" class=\"llms-button-secondary small\" name=\"llms_generate_cert\">\n\t\t\t\t<?php esc_html_e( 'Download', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-cloud-download\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t\t<button type=\"submit\" class=\"llms-button-danger small\" id=\"llms_delete_cert\" name=\"llms_delete_cert\">\n\t\t\t\t<?php esc_html_e( 'Delete', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t\t<input type=\"hidden\" name=\"certificate_id\" value=\"<?php echo absint( $certificate_id ); ?>\">\n\t\t\t<?php wp_nonce_field( 'llms-cert-actions', '_llms_cert_actions_nonce' ); ?>\n\n\t\t</form>\n\n\t\t<script>document.getElementById( 'llms_delete_cert' ).onclick = function( e ) {\n\t\t\treturn window.confirm( '<?php esc_attr_e( 'Are you sure you want to delete this certificate? This action cannot be undone!', 'lifterlms' ); ?>' );\n\t\t};</script>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Retrieve data for the columns.\n\t *\n\t * @since 3.2.0\n\t * @since 3.18.0 Unknown.\n\t * @since 6.0.0 Retrieve date using the LLMS_User_Certificate model.\n\t *\n\t * @param  string                $key         The column id / key.\n\t * @param  LLMS_User_Certificate $certificate Object of certificate data.\n\t * @return mixed\n\t */\n\tpublic function get_data( $key, $certificate ) {\n\n\t\t// Handle old object being passed in.\n\t\tif ( ! is_a( $certificate, 'LLMS_User_Certificate' ) && property_exists( $certificate, 'certificate_id' ) ) {\n\t\t\t$certificate = llms_get_certificate( $certificate->certificate_id );\n\t\t}\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'actions':\n\t\t\t\t$value = $this->get_actions_html( $certificate->get( 'id' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'related':\n\t\t\t\t$related = $certificate->get( 'related' );\n\t\t\t\tif ( $related && 'llms_certificate' !== get_post_type( $related ) ) {\n\t\t\t\t\tif ( is_numeric( $related ) ) {\n\t\t\t\t\t\t$value = $this->get_post_link( $related, get_the_title( $related ) );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$value = $related;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'earned':\n\t\t\t\t$value = $certificate->get_earned_date();\n\t\t\t\t$value = 'future' === get_post_status( $certificate->get( 'id' ) ) ? $value . ' ' . __( '(scheduled)', 'lifterlms' ) : $value;\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $certificate->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$value = $certificate->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'template_id':\n\t\t\t\t$template = $certificate->get( 'parent' );\n\t\t\t\tif ( $template ) {\n\t\t\t\t\t$value = $this->get_post_link( $template );\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}\n\n\t\t// Pass the \"legacy\" object to the filter.\n\t\t$backwards_compat_obj = array(\n\t\t\t'post_id'        => $certificate->get( 'related' ),\n\t\t\t'certificate_id' => $certificate->get( 'id' ),\n\t\t\t'earned_date'    => $certificate->get_earned_date(),\n\t\t);\n\n\t\treturn $this->filter_get_data( $value, $key, $backwards_compat_obj );\n\t}\n\n\t/**\n\t * Get table results.\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Paginate results.\n\t *\n\t * @param array $args\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$student_id = is_numeric( $args['student'] ) ? absint( $args['student'] ) : ( $args['student'] ? $args['student']->get_id() : 0 );\n\t\tif ( $student_id && ! current_user_can( 'view_others_lifterlms_reports' ) && ! llms_current_user_can( 'view_lifterlms_reports', $student_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_numeric( $args['student'] ) ) {\n\t\t\t$args['student'] = new LLMS_Student( $args['student'] );\n\t\t}\n\n\t\t$this->student = $args['student'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$query = $this->student->get_certificates(\n\t\t\tarray(\n\t\t\t\t'per_page' => 10,\n\t\t\t\t'status'   => array( 'publish', 'future' ),\n\t\t\t\t'paged'    => $this->current_page,\n\t\t\t\t'sort'     => array(\n\t\t\t\t\t'date' => 'ASC',\n\t\t\t\t\t'ID'   => 'ASC',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\t$this->max_pages = $query->get_max_pages();\n\n\t\tif ( $this->max_pages > $this->current_page ) {\n\t\t\t$this->is_last_page = false;\n\t\t}\n\n\t\t$this->tbody_data = $query->get_awards();\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since    2.3.0\n\t * @since 3.35.0 Get student ID more reliably.\n\t *\n\t * @return   array\n\t */\n\tpublic function set_args() {\n\n\t\t$student = false;\n\t\tif ( ! empty( $this->student ) ) {\n\t\t\t$student = $this->student->get_id();\n\t\t} elseif ( ! empty( $_GET['student_id'] ) ) {\n\t\t\t$student = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t}\n\n\t\treturn array(\n\t\t\t'student' => $student,\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.2.0\n\t * @version  3.18.0\n\t */\n\tprotected function set_columns() {\n\t\treturn array(\n\t\t\t'id'          => __( 'ID', 'lifterlms' ),\n\t\t\t'template_id' => __( 'Template ID', 'lifterlms' ),\n\t\t\t'name'        => __( 'Certificate Title', 'lifterlms' ),\n\t\t\t'earned'      => __( 'Earned Date', 'lifterlms' ),\n\t\t\t'related'     => __( 'Related Post', 'lifterlms' ),\n\t\t\t'actions'     => '',\n\t\t);\n\t}\n\n\t/**\n\t * Empty message displayed when no results are found\n\t *\n\t * @return   string\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tprotected function set_empty_message() {\n\t\treturn __( 'This student has not yet earned any certificates.', 'lifterlms' );\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.course.students.php",
    "content": "<?php\n/**\n * Display students enrolled in a given course on the course students subtab\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.2.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Course_Students class\n *\n * @since 3.15.0\n * @version 3.17.6\n */\nclass LLMS_Table_Course_Students extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'course-students';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'status';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_exportable = true;\n\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'name';\n\n\t/**\n\t * Post ID for the current table\n\t *\n\t * @var  int\n\t */\n\tpublic $course_id = null;\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @since 3.15.0\n\t * @since 3.17.2 Unknown.\n\t *\n\t * @param string       $key     The column id / key.\n\t * @param LLMS_Student $student Student object.\n\t * @return mixed\n\t */\n\tpublic function get_data( $key, $student ) {\n\n\t\t$value = '';\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'completed':\n\t\t\t\t$date  = $student->get_completion_date( $this->course_id );\n\t\t\t\t$value = $date ? $date : '&mdash;';\n\t\t\t\tbreak;\n\n\t\t\tcase 'enrolled':\n\t\t\t\t$value = $student->get_enrollment_date( $this->course_id, 'updated' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'grade':\n\t\t\t\t$value = $student->get_grade( $this->course_id );\n\t\t\t\tif ( is_numeric( $value ) ) {\n\t\t\t\t\t$value .= '%';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$id = $student->get_id();\n\t\t\t\tif ( current_user_can( 'edit_users', $id ) ) {\n\t\t\t\t\t$value = '<a href=\"' . esc_url( get_edit_user_link( $id ) ) . '\">' . $id . '</a>';\n\t\t\t\t} else {\n\t\t\t\t\t$value = $id;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'last_lesson':\n\t\t\t\t$lid = $student->get_last_completed_lesson( $this->course_id );\n\t\t\t\tif ( $lid ) {\n\t\t\t\t\t$value = $this->get_post_link( $lid, llms_trim_string( get_the_title( $lid ), 30 ) );\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$first = $student->get( 'first_name' );\n\t\t\t\t$last  = $student->get( 'last_name' );\n\n\t\t\t\tif ( ! $first || ! $last ) {\n\t\t\t\t\t$value = $student->get( 'display_name' );\n\t\t\t\t} else {\n\t\t\t\t\t$value = $last . ', ' . $first;\n\t\t\t\t}\n\n\t\t\t\t$url   = add_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'page'       => 'llms-reporting',\n\t\t\t\t\t\t'tab'        => 'students',\n\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t\t'stab'       => 'courses',\n\t\t\t\t\t\t'course_id'  => $this->course_id,\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $value . '</a>';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'progress':\n\t\t\t\t$value = $this->get_progress_bar_html( $student->get_progress( $this->course_id ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$value = llms_get_enrollment_status_name( $student->get_enrollment_status( $this->course_id ) );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $this->filter_get_data( $value, $key, $student );\n\n\t}\n\n\t/**\n\t * Retrieve data for a cell in an export file\n\t * Should be overridden in extending classes\n\t *\n\t * @param    string $key        the column id / key\n\t * @param    obj    $student    Instance of the LLMS_Student\n\t * @return   mixed\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_export_data( $key, $student ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $student->get_id();\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$value = $student->get( 'user_email' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name_first':\n\t\t\t\t$value = $student->get( 'first_name' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name_last':\n\t\t\t\t$value = $student->get( 'last_name' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'progress':\n\t\t\t\t$value = $student->get_progress( $this->course_id ) . '%';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $this->get_data( $key, $student );\n\n\t\t}// End switch().\n\n\t\treturn $this->filter_get_data( $value, $key, $student, 'export' );\n\n\t}\n\n\n\t/**\n\t * Get a lock key unique to the table & user for locking the table during export generation\n\t *\n\t * @return   string\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_export_lock_key() {\n\t\t$args = $this->get_args();\n\t\treturn sprintf( '%1$s:%2$d:%3$d', $this->id, get_current_user_id(), $args['course_id'] );\n\t}\n\n\t/**\n\t * Allow customization of the title for export files\n\t *\n\t * @param    array $args   optional arguments passed from table to csv processor\n\t * @return   string\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_export_title( $args = array() ) {\n\t\t$title = $this->get_title();\n\t\tif ( isset( $args['course_id'] ) ) {\n\t\t\t$title = get_the_title( $args['course_id'] ) . ' ' . $title;\n\t\t}\n\t\treturn apply_filters( 'llms_table_get_' . $this->id . '_export_title', $title );\n\t}\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input\n\t *\n\t * @return   string\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\t\treturn apply_filters( 'llms_reporting_get_' . $this->id . '_search_placeholder', __( 'Search students by name or email...', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table.\n\t *\n\t * @since 3.15.0\n\t * @since 5.10.0 Add ability to sort by completion date.\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t * @since 9.2.3 Added object-level authorization check on the course.\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Students', 'lifterlms' );\n\n\t\tif ( ! $args ) {\n\t\t\t$args = $this->get_args();\n\t\t}\n\n\t\t$args = $this->clean_args( $args );\n\n\t\tif ( ! current_user_can( 'view_others_lifterlms_reports' ) && ! current_user_can( 'edit_post', absint( $args['course_id'] ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->course_id = $args['course_id'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->get_order();\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->get_orderby();\n\n\t\t$sort = array();\n\t\tswitch ( $this->get_orderby() ) {\n\n\t\t\tcase 'completed':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'completed'  => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'enrolled':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'date'       => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'id' => $this->get_order(),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'last_name'  => $this->get_order(),\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'status'     => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t$query_args = array(\n\t\t\t'page'     => $this->get_current_page(),\n\t\t\t'post_id'  => $args['course_id'],\n\t\t\t'per_page' => apply_filters( 'llms_' . $this->id . '_table_students_per_page', 25 ),\n\t\t\t'sort'     => $sort,\n\t\t);\n\n\t\tif ( 'status' === $this->get_filterby() && 'any' !== $this->get_filter() ) {\n\n\t\t\t$query_args['statuses'] = array( $this->get_filter() );\n\n\t\t}\n\n\t\tif ( isset( $args['search'] ) ) {\n\n\t\t\t$this->search         = $args['search'];\n\t\t\t$query_args['search'] = $this->get_search();\n\n\t\t}\n\n\t\t$query = new LLMS_Student_Query( $query_args );\n\n\t\t$this->max_pages    = $query->get_max_pages();\n\t\t$this->is_last_page = $query->is_last_page();\n\n\t\t$this->tbody_data = $query->get_students();\n\n\t}\n\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function set_args() {\n\n\t\tif ( ! $this->course_id ) {\n\t\t\t$this->course_id = ! empty( $_GET['course_id'] ) ? absint( $_GET['course_id'] ) : null;\n\t\t}\n\n\t\treturn array(\n\t\t\t'course_id' => $this->course_id,\n\t\t);\n\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.16.11\n\t */\n\tpublic function set_columns() {\n\t\t$cols = array(\n\t\t\t'id'          => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name'        => array(\n\t\t\t\t'sortable' => true,\n\t\t\t\t'title'    => __( 'Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name_last'   => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Last Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name_first'  => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'First Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'email'       => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Email', 'lifterlms' ),\n\t\t\t),\n\t\t\t'status'      => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'filterable' => llms_get_enrollment_statuses(),\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Status', 'lifterlms' ),\n\t\t\t),\n\t\t\t'enrolled'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Enrollment Updated', 'lifterlms' ),\n\t\t\t),\n\t\t\t'completed'   => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Completed', 'lifterlms' ),\n\t\t\t),\n\t\t\t'progress'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => false,\n\t\t\t\t'title'      => __( 'Progress', 'lifterlms' ),\n\t\t\t),\n\t\t\t'grade'       => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => false,\n\t\t\t\t'title'      => __( 'Grade', 'lifterlms' ),\n\t\t\t),\n\t\t\t'last_lesson' => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Last Lesson', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\n\t\treturn $cols;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.courses.php",
    "content": "<?php\n/**\n * Courses Reporting Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.15.0\n * @version 3.16.14\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Courses Reporting Table class\n *\n * @since 3.15.0\n * @version 3.16.14\n */\nclass LLMS_Table_Courses extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'courses';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'instructor';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_exportable = true;\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'title';\n\n\t/**\n\t * Retrieve data for a cell\n\t *\n\t * @param    string $key   the column id / key\n\t * @param    mixed  $data  object / array of data that the function can use to extract the data\n\t * @return   mixed\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tprotected function get_data( $key, $data ) {\n\n\t\t$course = llms_get_post( $data );\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'grade':\n\t\t\t\t$value = $course->get( 'average_grade' ) . '%';\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $this->get_post_link( $course->get( 'id' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'instructors':\n\t\t\t\t$data = array();\n\t\t\t\tforeach ( $course->get_instructors() as $info ) {\n\t\t\t\t\t$instructor = llms_get_instructor( $info['id'] );\n\t\t\t\t\tif ( $instructor ) {\n\t\t\t\t\t\t$data[] = sprintf( '%1$s (%2$s)', $instructor->get( 'display_name' ), $info['label'] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$value = implode( ', ', $data );\n\t\t\t\tbreak;\n\n\t\t\tcase 'progress':\n\t\t\t\t$value = $this->get_progress_bar_html( $course->get( 'average_progress' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'students':\n\t\t\t\t$value = number_format_i18n( $course->get_student_count(), 0 );\n\t\t\t\tbreak;\n\n\t\t\tcase 'title':\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'tab'       => 'courses',\n\t\t\t\t\t\t'course_id' => $course->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $course->get( 'title' ) . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $value;\n\t}\n\n\t/**\n\t * Retrieve a list of Instructors to be used for Filtering\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tprivate function get_instructor_filters() {\n\n\t\t$query = get_users(\n\t\t\tarray(\n\t\t\t\t'fields'   => array( 'ID', 'display_name' ),\n\t\t\t\t'meta_key' => 'last_name',\n\t\t\t\t'orderby'  => 'meta_value',\n\t\t\t\t'role__in' => array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ),\n\t\t\t)\n\t\t);\n\n\t\t$instructors = wp_list_pluck( $query, 'display_name', 'ID' );\n\n\t\treturn $instructors;\n\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @param    array $args  array of query args\n\t * @return   void\n\t * @since    3.15.0\n\t * @version  3.16.14\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Courses', 'lifterlms' );\n\n\t\t$args = $this->clean_args( $args );\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$per = apply_filters( 'llms_reporting_' . $this->id . '_per_page', 25 );\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->order;\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->orderby;\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$query_args = array(\n\t\t\t'order'          => $this->order,\n\t\t\t'orderby'        => $this->orderby,\n\t\t\t'paged'          => $this->current_page,\n\t\t\t'post_status'    => array( 'publish', 'private' ),\n\t\t\t'post_type'      => 'course',\n\t\t\t'posts_per_page' => $per,\n\t\t);\n\n\t\tif ( 'any' !== $this->filter ) {\n\n\t\t\t$serialized_id = serialize(\n\t\t\t\tarray(\n\t\t\t\t\t'id' => absint( $this->filter ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$serialized_id = str_replace( array( 'a:1:{', '}' ), '', $serialized_id );\n\n\t\t\t$query_args['meta_query'] = array(\n\t\t\t\tarray(\n\t\t\t\t\t'compare' => 'LIKE',\n\t\t\t\t\t'key'     => '_llms_instructors',\n\t\t\t\t\t'value'   => $serialized_id,\n\t\t\t\t),\n\t\t\t);\n\n\t\t}\n\n\t\tif ( 'progress' === $this->orderby ) {\n\t\t\t$query_args['meta_key'] = '_llms_average_progress';\n\t\t\t$query_args['orderby']  = 'meta_value_num';\n\t\t} elseif ( 'grade' === $this->orderby ) {\n\t\t\t$query_args['meta_key'] = '_llms_average_progress';\n\t\t\t$query_args['orderby']  = 'meta_value_num';\n\t\t}\n\n\t\tif ( isset( $args['search'] ) ) {\n\t\t\t$query_args['s'] = sanitize_text_field( $args['search'] );\n\t\t}\n\n\t\t// If you can view others reports, make a regular query.\n\t\tif ( current_user_can( 'view_others_lifterlms_reports' ) ) {\n\n\t\t\t$query = new WP_Query( $query_args );\n\n\t\t\t// User can only see their own reports, get a list of their students.\n\t\t} elseif ( current_user_can( 'view_lifterlms_reports' ) ) {\n\n\t\t\t$instructor = llms_get_instructor();\n\t\t\tif ( ! $instructor ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t$query = $instructor->get_courses( $query_args, 'query' );\n\n\t\t} else {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t$this->max_pages = $query->max_num_pages;\n\n\t\tif ( $this->max_pages > $this->current_page ) {\n\t\t\t$this->is_last_page = false;\n\t\t}\n\n\t\t$this->tbody_data = $query->posts;\n\n\t}\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input\n\t *\n\t * @return   string\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\t\treturn apply_filters( 'llms_table_get_' . $this->id . '_search_placeholder', __( 'Search courses...', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function set_args() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tprotected function set_columns() {\n\t\treturn array(\n\t\t\t'id'          => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'title'       => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Title', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'instructors' => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'filterable' => current_user_can( 'view_others_lifterlms_reports' ) ? $this->get_instructor_filters() : false,\n\t\t\t\t'title'      => __( 'Instructors', 'lifterlms' ),\n\t\t\t),\n\t\t\t'students'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Students', 'lifterlms' ),\n\t\t\t),\n\t\t\t'progress'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Average Progress', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'grade'       => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Average Grade', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.membership.students.php",
    "content": "<?php\n/**\n * Display students enrolled in a given membership on the membership students subtab\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.32.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Membership_Students class\n *\n * @since 3.32.0\n */\nclass LLMS_Table_Membership_Students extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'membership-students';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set.\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by.\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'status';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_exportable = true;\n\n\n\t/**\n\t *\n\t * @since   3.32.0\n\t *\n\t * Determine if the table is filterable.\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links.\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable.\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order 'ASC' or 'DESC'.\n\t * Only applicable of $orderby is not set.\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by.\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'name';\n\n\t/**\n\t * Post ID for the current table.\n\t *\n\t * @since   3.32.0\n\t *\n\t * @var  int\n\t */\n\tpublic $membership_id = null;\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string       $key     The column id / key.\n\t * @param LLMS_Student $student Student object.\n\t * @return mixed\n\t */\n\tpublic function get_data( $key, $student ) {\n\n\t\t$value = '';\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'enrolled':\n\t\t\t\t$value = $student->get_enrollment_date( $this->membership_id, 'updated' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$id = $student->get_id();\n\t\t\t\tif ( current_user_can( 'edit_users', $id ) ) {\n\t\t\t\t\t$value = '<a href=\"' . esc_url( get_edit_user_link( $id ) ) . '\">' . $id . '</a>';\n\t\t\t\t} else {\n\t\t\t\t\t$value = $id;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$first = $student->get( 'first_name' );\n\t\t\t\t$last  = $student->get( 'last_name' );\n\n\t\t\t\tif ( ! $first || ! $last ) {\n\t\t\t\t\t$value = $student->get( 'display_name' );\n\t\t\t\t} else {\n\t\t\t\t\t$value = $last . ', ' . $first;\n\t\t\t\t}\n\n\t\t\t\t$url   = add_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'page'          => 'llms-reporting',\n\t\t\t\t\t\t'tab'           => 'students',\n\t\t\t\t\t\t'student_id'    => $student->get_id(),\n\t\t\t\t\t\t'stab'          => 'memberships',\n\t\t\t\t\t\t'membership_id' => $this->membership_id,\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $value . '</a>';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$value = llms_get_enrollment_status_name( $student->get_enrollment_status( $this->membership_id ) );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $this->filter_get_data( $value, $key, $student );\n\n\t}\n\n\t/**\n\t * Retrieve data for a cell in an export file.\n\t * Should be overridden in extending classes.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $key     The column id / key.\n\t * @param obj    $student Instance of the LLMS_Student.\n\t * @return mixed\n\t */\n\tpublic function get_export_data( $key, $student ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $student->get_id();\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$value = $student->get( 'user_email' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name_first':\n\t\t\t\t$value = $student->get( 'first_name' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name_last':\n\t\t\t\t$value = $student->get( 'last_name' );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $this->get_data( $key, $student );\n\n\t\t}// End switch().\n\n\t\treturn $this->filter_get_data( $value, $key, $student, 'export' );\n\n\t}\n\n\n\t/**\n\t * Get a lock key unique to the table & user for locking the table during export generation.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_export_lock_key() {\n\t\t$args = $this->get_args();\n\t\treturn sprintf( '%1$s:%2$d:%3$d', $this->id, get_current_user_id(), $args['membership_id'] );\n\t}\n\n\t/**\n\t * Allow customization of the title for export files.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param  array $args Optional. Array of arguments passed from table to csv processor. Default empty array.\n\t * @return string\n\t */\n\tpublic function get_export_title( $args = array() ) {\n\t\t$title = $this->get_title();\n\t\tif ( isset( $args['membership_id'] ) ) {\n\t\t\t$title = get_the_title( $args['membership_id'] ) . ' ' . $title;\n\t\t}\n\t\treturn apply_filters( 'llms_table_get_' . $this->id . '_export_title', $title );\n\t}\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\t\treturn apply_filters( 'llms_reporting_get_' . $this->id . '_search_placeholder', __( 'Search students by name or email...', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table.\n\t *\n\t * @since 3.32.0\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t * @since 9.2.3 Added object-level authorization check on the membership.\n\t *\n\t * @param array $args Optional. Array of query args. Default empty array.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Students', 'lifterlms' );\n\n\t\tif ( ! $args ) {\n\t\t\t$args = $this->get_args();\n\t\t}\n\n\t\t$args = $this->clean_args( $args );\n\n\t\tif ( ! current_user_can( 'view_others_lifterlms_reports' ) && ! current_user_can( 'edit_post', absint( $args['membership_id'] ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->membership_id = $args['membership_id'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->get_order();\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->get_orderby();\n\n\t\t$sort = array();\n\t\tswitch ( $this->get_orderby() ) {\n\n\t\t\tcase 'enrolled':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'date'       => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'id' => $this->get_order(),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'last_name'  => $this->get_order(),\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'status'     => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t$query_args = array(\n\t\t\t'page'     => $this->get_current_page(),\n\t\t\t'post_id'  => $args['membership_id'],\n\t\t\t'per_page' => apply_filters( 'llms_' . $this->id . '_table_students_per_page', 25 ),\n\t\t\t'sort'     => $sort,\n\t\t);\n\n\t\tif ( 'status' === $this->get_filterby() && 'any' !== $this->get_filter() ) {\n\n\t\t\t$query_args['statuses'] = array( $this->get_filter() );\n\n\t\t}\n\n\t\tif ( isset( $args['search'] ) ) {\n\n\t\t\t$this->search         = $args['search'];\n\t\t\t$query_args['search'] = $this->get_search();\n\n\t\t}\n\n\t\t$query = new LLMS_Student_Query( $query_args );\n\n\t\t$this->max_pages    = $query->get_max_pages();\n\t\t$this->is_last_page = $query->is_last_page();\n\n\t\t$this->tbody_data = $query->get_students();\n\n\t}\n\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return array\n\t */\n\tpublic function set_args() {\n\n\t\tif ( ! $this->membership_id ) {\n\t\t\t$this->membership_id = ! empty( $_GET['membership_id'] ) ? absint( $_GET['membership_id'] ) : null;\n\t\t}\n\n\t\treturn array(\n\t\t\t'membership_id' => $this->membership_id,\n\t\t);\n\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return array\n\t */\n\tpublic function set_columns() {\n\t\t$cols = array(\n\t\t\t'id'         => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name'       => array(\n\t\t\t\t'sortable' => true,\n\t\t\t\t'title'    => __( 'Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name_last'  => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Last Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name_first' => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'First Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'email'      => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Email', 'lifterlms' ),\n\t\t\t),\n\t\t\t'status'     => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'filterable' => llms_get_enrollment_statuses(),\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Status', 'lifterlms' ),\n\t\t\t),\n\t\t\t'enrolled'   => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Enrollment Updated', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\n\t\treturn $cols;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.memberships.php",
    "content": "<?php\n/**\n * Memberships Reporting Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.32.0\n * @version 3.32.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Memberships Reporting Table class\n *\n * @since 3.32.0\n */\nclass LLMS_Table_Memberships extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'memberships';\n\n\t/**\n\t * Value of the field being filtered by.\n\t * Only applicable if $filterby is set.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'instructor';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_exportable = true;\n\n\t/**\n\t * Determine if the table is filterable.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order 'ASC' or 'DESC'.\n\t * Only applicable of $orderby is not set.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'title';\n\n\t/**\n\t * Retrieve data for a cell.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $key   The column id / key.\n\t * @param mixed  $data  Object / array of data that the function can use to extract the data.\n\t * @return mixed\n\t */\n\tprotected function get_data( $key, $data ) {\n\n\t\t$membership = llms_get_post( $data );\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $this->get_post_link( $membership->get( 'id' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'instructors':\n\t\t\t\t$data = array();\n\t\t\t\tforeach ( $membership->get_instructors() as $info ) {\n\t\t\t\t\t$instructor = llms_get_instructor( $info['id'] );\n\t\t\t\t\tif ( $instructor ) {\n\t\t\t\t\t\t$data[] = sprintf( '%1$s (%2$s)', $instructor->get( 'display_name' ), $info['label'] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$value = implode( ', ', $data );\n\t\t\t\tbreak;\n\n\t\t\tcase 'students':\n\t\t\t\t$value = number_format_i18n( $membership->get_student_count(), 0 );\n\t\t\t\tbreak;\n\n\t\t\tcase 'title':\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'tab'           => 'memberships',\n\t\t\t\t\t\t'membership_id' => $membership->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $membership->get( 'title' ) . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $value;\n\t}\n\n\t/**\n\t * Retrieve a list of Instructors to be used for Filtering.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_instructor_filters() {\n\n\t\t$query = get_users(\n\t\t\tarray(\n\t\t\t\t'fields'   => array( 'ID', 'display_name' ),\n\t\t\t\t'meta_key' => 'last_name',\n\t\t\t\t'orderby'  => 'meta_value',\n\t\t\t\t'role__in' => array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ),\n\t\t\t)\n\t\t);\n\n\t\t$instructors = wp_list_pluck( $query, 'display_name', 'ID' );\n\n\t\treturn $instructors;\n\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param  array $args  Optional. Array of query args. Default empty array.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Memberships', 'lifterlms' );\n\n\t\t$args = $this->clean_args( $args );\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$per = apply_filters( 'llms_reporting_' . $this->id . '_per_page', 25 );\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->order;\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->orderby;\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$query_args = array(\n\t\t\t'order'          => $this->order,\n\t\t\t'orderby'        => $this->orderby,\n\t\t\t'paged'          => $this->current_page,\n\t\t\t'post_status'    => array( 'publish', 'private' ),\n\t\t\t'post_type'      => 'llms_membership',\n\t\t\t'posts_per_page' => $per,\n\t\t);\n\n\t\tif ( 'any' !== $this->filter ) {\n\n\t\t\t$serialized_id = serialize(\n\t\t\t\tarray(\n\t\t\t\t\t'id' => absint( $this->filter ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$serialized_id = str_replace( array( 'a:1:{', '}' ), '', $serialized_id );\n\n\t\t\t$query_args['meta_query'] = array(\n\t\t\t\tarray(\n\t\t\t\t\t'compare' => 'LIKE',\n\t\t\t\t\t'key'     => '_llms_instructors',\n\t\t\t\t\t'value'   => $serialized_id,\n\t\t\t\t),\n\t\t\t);\n\n\t\t}\n\n\t\tif ( isset( $args['search'] ) ) {\n\t\t\t$query_args['s'] = sanitize_text_field( $args['search'] );\n\t\t}\n\n\t\t// If you can view others reports, make a regular query.\n\t\tif ( current_user_can( 'view_others_lifterlms_reports' ) ) {\n\n\t\t\t$query = new WP_Query( $query_args );\n\n\t\t\t// User can only see their own reports, get a list of their students.\n\t\t} elseif ( current_user_can( 'view_lifterlms_reports' ) ) {\n\n\t\t\t$instructor = llms_get_instructor();\n\t\t\tif ( ! $instructor ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t$query = $instructor->get_memberships( $query_args, 'query' );\n\n\t\t} else {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t$this->max_pages = $query->max_num_pages;\n\n\t\tif ( $this->max_pages > $this->current_page ) {\n\t\t\t$this->is_last_page = false;\n\t\t}\n\n\t\t$this->tbody_data = $query->posts;\n\n\t}\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\t\treturn apply_filters( 'llms_table_get_' . $this->id . '_search_placeholder', __( 'Search memberships...', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return array\n\t */\n\tpublic function set_args() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Define the structure of the table.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_columns() {\n\t\treturn array(\n\t\t\t'id'          => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'title'       => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Title', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'instructors' => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'filterable' => current_user_can( 'view_others_lifterlms_reports' ) ? $this->get_instructor_filters() : false,\n\t\t\t\t'title'      => __( 'Instructors', 'lifterlms' ),\n\t\t\t),\n\t\t\t'students'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Students', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.quiz.attempts.php",
    "content": "<?php\n/**\n * Quizzes Reporting Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.16.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Quiz_Attempts class.\n *\n * @since 3.16.0\n * @since 3.30.3 Fixed undefined variable notice in the `set_args()` method.\n */\nclass LLMS_Table_Quiz_Attempts extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'quiz_attempts';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'grade';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_exportable = false;\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'DESC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'id';\n\n\t/**\n\t * WP Post ID of the displayed quiz\n\t *\n\t * @var  null\n\t */\n\tprotected $quiz_id = null;\n\n\t/**\n\t * Retrieve data for a cell.\n\t *\n\t * @since 3.16.0\n\t * @since 3.26.3 Unknown.\n\t * @since 7.8.0 Added information about whether the attempt can be resumed.\n\t *\n\t * @param string            $key     The column id / key.\n\t * @param LLMS_Quiz_Attempt $attempt LLMS_Quiz_Attempt obj.\n\t * @return mixed\n\t */\n\tprotected function get_data( $key, $attempt ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'student':\n\t\t\t\t$value   = '&ndash;';\n\t\t\t\t$student = $attempt->get_student();\n\t\t\t\tif ( $student ) {\n\t\t\t\t\t$value = $student->get_name();\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'attempt':\n\t\t\t\t$value = '#' . $attempt->get( $key );\n\t\t\t\tbreak;\n\n\t\t\tcase 'grade':\n\t\t\t\t$value      = $attempt->get( $key ) ? $attempt->get( $key ) . '%' : '0%';\n\t\t\t\t$additional = $attempt->l10n( 'status' );\n\t\t\t\tif ( $attempt->can_be_resumed() && $attempt->is_last_attempt() ) {\n\t\t\t\t\t$additional .= ' - ' . esc_html__( 'Can be resumed', 'lifterlms' );\n\t\t\t\t}\n\t\t\t\t$value .= ' (' . $additional . ')';\n\t\t\t\tbreak;\n\n\t\t\tcase 'start_date':\n\t\t\tcase 'end_date':\n\t\t\t\t$value = '&ndash;';\n\t\t\t\t$date  = $attempt->get( $key );\n\t\t\t\tif ( $date ) {\n\t\t\t\t\t$value = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $date ) );\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$value = sprintf( '%2$d (%1$s)', $attempt->get_key(), $attempt->get( 'id' ) );\n\n\t\t\t\t$url = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'tab'        => 'quizzes',\n\t\t\t\t\t\t'stab'       => 'attempts',\n\t\t\t\t\t\t'quiz_id'    => $attempt->get( 'quiz_id' ),\n\t\t\t\t\t\t'attempt_id' => $attempt->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $value . '</a>';\n\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $value;\n\t}\n\n\t/**\n\t * Retrieve a list of Instructors to be used for Filtering\n\t *\n\t * @return   array\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tprivate function get_instructor_filters() {\n\n\t\t$query = get_users(\n\t\t\tarray(\n\t\t\t\t'fields'   => array( 'ID', 'display_name' ),\n\t\t\t\t'meta_key' => 'last_name',\n\t\t\t\t'orderby'  => 'meta_value',\n\t\t\t\t'role__in' => array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ),\n\t\t\t)\n\t\t);\n\n\t\t$instructors = wp_list_pluck( $query, 'display_name', 'ID' );\n\n\t\treturn $instructors;\n\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @since 3.16.0\n\t * @since 3.25.0 Unknown.\n\t * @since 6.0.0 Don't access `LLMS_Query_Quiz_Attempt` properties directly.\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Quiz Attempts', 'lifterlms' );\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$this->quiz_id = $args['quiz_id'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$per = apply_filters( 'llms_reporting_' . $this->id . '_per_page', 25 );\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->order;\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->orderby;\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$query_args = array(\n\t\t\t'sort'       => array(\n\t\t\t\t$this->orderby => $this->order,\n\t\t\t),\n\t\t\t'page'       => $this->current_page,\n\t\t\t'per_page'   => $per,\n\t\t\t'quiz_id'    => $args['quiz_id'],\n\t\t\t'student_id' => isset( $args['student_id'] ) ? $args['student_id'] : null,\n\t\t);\n\n\t\t// Add search functionality\n\t\tif ( isset( $args['search'] ) && ! empty( $args['search'] ) ) {\n\t\t\t$query_args['search'] = sanitize_text_field( $args['search'] );\n\t\t}\n\n\t\tif ( 'any' !== $this->filter ) {\n\t\t\t$query_args['status'] = $this->filter;\n\t\t}\n\n\t\tif ( current_user_can( 'view_others_lifterlms_reports' ) || ( current_user_can( 'view_lifterlms_reports' ) && current_user_can( 'edit_post', $args['quiz_id'] ) ) ) {\n\n\t\t\t$query = new LLMS_Query_Quiz_Attempt( $query_args );\n\n\t\t} else {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t$this->max_pages    = $query->get_max_pages();\n\t\t$this->is_last_page = $query->is_last_page();\n\n\t\t$this->tbody_data = $query->get_attempts();\n\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since 3.16.0\n\t * @version 3.30.3 Fallback to `null` when `$_GET` param is not set.\n\t *\n\t * @return   array\n\t */\n\tpublic function set_args() {\n\t\treturn array(\n\t\t\t'quiz_id'    => ! empty( $this->quiz_id ) ? $this->quiz_id : ( isset( $_GET['quiz_id'] ) ? absint( $_GET['quiz_id'] ) : null ),\n\t\t\t'student_id' => 0,\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.16.0\n\t * @version  3.19.2\n\t */\n\tprotected function set_columns() {\n\n\t\t$cols = array(\n\t\t\t'id'         => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'attempt'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Attempt #', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'student'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Student', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'grade'      => array(\n\t\t\t\t'filterable' => llms_get_quiz_attempt_statuses(),\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Grade', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'start_date' => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Start Date', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'end_date'   => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'End Date', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t);\n\n\t\treturn $cols;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.quiz.non.attempts.php",
    "content": "<?php\n/**\n * Quiz Non-Attempts Reporting Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 9.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Quiz_Non_Attempts class.\n *\n * Displays students enrolled in courses containing a quiz but who have not attempted the quiz.\n *\n * @since 9.1.0\n */\nclass LLMS_Table_Quiz_Non_Attempts extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'quiz_non_attempts';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'status';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_exportable = true;\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'name';\n\n\t/**\n\t * WP Post ID of the displayed quiz\n\t *\n\t * @var  null\n\t */\n\tprotected $quiz_id = null;\n\n\t/**\n\t * Retrieve data for a cell.\n\t *\n\t * @since 9.1.0\n\t *\n\t * @param string       $key     The column id / key.\n\t * @param LLMS_Student $student LLMS_Student obj.\n\t * @return mixed\n\t */\n\tprotected function get_data( $key, $student ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'id':\n\t\t\t\t$id = $student->get_id();\n\t\t\t\tif ( current_user_can( 'edit_users', $id ) ) {\n\t\t\t\t\t$value = '<a href=\"' . esc_url( get_edit_user_link( $id ) ) . '\">' . $id . '</a>';\n\t\t\t\t} else {\n\t\t\t\t\t$value = $id;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$first = $student->get( 'first_name' );\n\t\t\t\t$last  = $student->get( 'last_name' );\n\n\t\t\t\tif ( ! $first || ! $last ) {\n\t\t\t\t\t$value = $student->get( 'display_name' );\n\t\t\t\t} else {\n\t\t\t\t\t$value = $last . ', ' . $first;\n\t\t\t\t}\n\n\t\t\t\t$id = $student->get_id();\n\t\t\t\tif ( current_user_can( 'edit_users', $id ) ) {\n\t\t\t\t\t$value = '<a href=\"' . esc_url( get_edit_user_link( $id ) ) . '\">' . $value . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$value = $student->get( 'user_email' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'enrolled_courses':\n\t\t\t\t$quiz   = llms_get_post( $this->quiz_id );\n\t\t\t\t$course = $quiz ? $quiz->get_course() : false;\n\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$enrollment_date = $student->get_enrollment_date( $course->get( 'id' ) );\n\t\t\t\t\t$value           = $enrollment_date ? $enrollment_date : '&mdash;';\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&mdash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$quiz   = llms_get_post( $this->quiz_id );\n\t\t\t\t$course = $quiz ? $quiz->get_course() : false;\n\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$status = $student->get_enrollment_status( $course->get( 'id' ) );\n\t\t\t\t\t$value  = ucfirst( $status );\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&mdash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $value;\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table.\n\t *\n\t * @since 9.1.0\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Students Without Quiz Attempts', 'lifterlms' );\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$this->quiz_id = $args['quiz_id'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$per         = apply_filters( 'llms_reporting_' . $this->id . '_per_page', 25 );\n\t\t$order       = isset( $args['order'] ) ? $args['order'] : $this->order;\n\t\t$this->order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'ASC';\n\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->orderby;\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\tif ( ! ( current_user_can( 'view_others_lifterlms_reports' ) || ( current_user_can( 'view_lifterlms_reports' ) && current_user_can( 'edit_post', $args['quiz_id'] ) ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$quiz = llms_get_post( $this->quiz_id );\n\t\tif ( ! $quiz ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$course = $quiz->get_course();\n\t\tif ( ! $course ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Use a single optimized database query.\n\t\tglobal $wpdb;\n\n\t\t$search_sql = '';\n\t\tif ( isset( $args['search'] ) && ! empty( $args['search'] ) ) {\n\t\t\t$search_term = sanitize_text_field( $args['search'] );\n\t\t\t$search_sql  = $wpdb->prepare(\n\t\t\t\t'AND (\n\t\t\t\t\tu.user_login LIKE %s\n\t\t\t\t\tOR u.user_email LIKE %s\n\t\t\t\t\tOR u.display_name LIKE %s\n\t\t\t\t\tOR m_first.meta_value LIKE %s\n\t\t\t\t\tOR m_last.meta_value LIKE %s\n\t\t\t\t)',\n\t\t\t\t'%' . $search_term . '%',\n\t\t\t\t'%' . $search_term . '%',\n\t\t\t\t'%' . $search_term . '%',\n\t\t\t\t'%' . $search_term . '%',\n\t\t\t\t'%' . $search_term . '%'\n\t\t\t);\n\t\t}\n\n\t\tif ( 'any' !== $this->filter ) {\n\t\t\t$status_sql = $wpdb->prepare( 'AND upm.meta_value = %s', $this->filter );\n\t\t} else {\n\t\t\t$status_sql = \"AND upm.meta_value IN ('enrolled', 'expired', 'cancelled')\";\n\t\t}\n\n\t\tswitch ( $this->orderby ) {\n\t\t\tcase 'name':\n\t\t\t\t$order_sql = 'ORDER BY m_last.meta_value ' . $this->order . ', m_first.meta_value ' . $this->order;\n\t\t\t\tbreak;\n\t\t\tcase 'id':\n\t\t\t\t$order_sql = 'ORDER BY u.ID ' . $this->order;\n\t\t\t\tbreak;\n\t\t\tcase 'email':\n\t\t\t\t$order_sql = 'ORDER BY u.user_email ' . $this->order;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t$order_sql = 'ORDER BY u.display_name ' . $this->order;\n\t\t}\n\n\t\t$offset = ( $this->current_page - 1 ) * $per;\n\n\t\t$from_joins_where = \"FROM {$wpdb->users} u\n\t\t\t\tINNER JOIN {$wpdb->prefix}lifterlms_user_postmeta upm\n\t\t\t\t\tON u.ID = upm.user_id\n\t\t\t\t\tAND upm.post_id = %d\n\t\t\t\t\tAND upm.meta_key = '_status'\n\t\t\t\t\t{$status_sql}\n\t\t\t\tLEFT JOIN {$wpdb->usermeta} m_first\n\t\t\t\t\tON u.ID = m_first.user_id\n\t\t\t\t\tAND m_first.meta_key = 'first_name'\n\t\t\t\tLEFT JOIN {$wpdb->usermeta} m_last\n\t\t\t\t\tON u.ID = m_last.user_id\n\t\t\t\t\tAND m_last.meta_key = 'last_name'\n\t\t\t\tLEFT JOIN {$wpdb->prefix}lifterlms_quiz_attempts qa\n\t\t\t\t\tON u.ID = qa.student_id\n\t\t\t\t\tAND qa.quiz_id = %d\n\t\t\t\tWHERE upm.updated_date = (\n\t\t\t\t\tSELECT MAX(upm2.updated_date)\n\t\t\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta upm2\n\t\t\t\t\tWHERE upm2.user_id = u.ID\n\t\t\t\t\tAND upm2.post_id = %d\n\t\t\t\t\tAND upm2.meta_key = '_status'\n\t\t\t\t)\n\t\t\t\tAND qa.id IS NULL\n\t\t\t\t{$search_sql}\";\n\n\t\t$prepare_args = array(\n\t\t\t$course->get( 'id' ),\n\t\t\t$this->quiz_id,\n\t\t\t$course->get( 'id' ),\n\t\t);\n\n\t\t$total_results = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT COUNT(DISTINCT u.ID) {$from_joins_where}\",\n\t\t\t\t$prepare_args\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t\t$results = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT DISTINCT\n\t\t\t\t\tu.ID as user_id,\n\t\t\t\t\tu.user_email,\n\t\t\t\t\tu.display_name,\n\t\t\t\t\tu.user_registered,\n\t\t\t\t\tm_first.meta_value as first_name,\n\t\t\t\t\tm_last.meta_value as last_name,\n\t\t\t\t\tupm.meta_value as enrollment_status,\n\t\t\t\t\tupm.updated_date as enrollment_date\n\t\t\t\t{$from_joins_where}\n\t\t\t\t{$order_sql}\n\t\t\t\tLIMIT %d, %d\",\n\t\t\t\tarray_merge( $prepare_args, array( $offset, $per ) )\n\t\t\t)\n\t\t);\n\n\t\t$this->max_pages    = ceil( $total_results / $per );\n\t\t$this->is_last_page = ( $this->current_page >= $this->max_pages );\n\n\t\t$this->tbody_data = array();\n\t\tif ( ! empty( $results ) ) {\n\t\t\tforeach ( $results as $result ) {\n\t\t\t\t$this->tbody_data[] = llms_get_student( $result->user_id );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since 9.1.0\n\t *\n\t * @return array\n\t */\n\tpublic function set_args() {\n\t\treturn array(\n\t\t\t'quiz_id' => ! empty( $this->quiz_id ) ? $this->quiz_id : ( isset( $_GET['quiz_id'] ) ? absint( $_GET['quiz_id'] ) : null ),\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return array\n\t * @since 9.1.0\n\t */\n\tprotected function set_columns() {\n\n\t\t$cols = array(\n\t\t\t'id'               => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'name'             => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Name', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'email'            => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Email', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'enrolled_courses' => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Enrollment Date', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'status'           => array(\n\t\t\t\t'filterable' => array(\n\t\t\t\t\t'enrolled'  => __( 'Enrolled', 'lifterlms' ),\n\t\t\t\t\t'expired'   => __( 'Expired', 'lifterlms' ),\n\t\t\t\t\t'cancelled' => __( 'Cancelled', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Status', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t);\n\n\t\treturn $cols;\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.quizzes.php",
    "content": "<?php\n/**\n * Quizzes Reporting Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.16.0\n * @version 7.1.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Quizzes Reporting Table class\n *\n * @since 3.16.0\n * @since 3.36.1 Fixed an issue that allow instructors, who can only see their own reports,\n *               to see all the quizzes when they had no courses or courses with no lessons.\n * @since 3.37.8 Allow orphaned quizzes to be deleted.\n *               Output quiz IDs as plain text (no link) when they cannot be edited and link to the quiz within the course builder when they can.\n * @since 3.37.12 Fixed the 'actions' column name.\n * @since 4.2.0 Added deep checks on whether the quiz is associated to a lesson.\n */\nclass LLMS_Table_Quizzes extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'quizzes';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set\n\t *\n\t * @var string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @var string\n\t */\n\tprotected $filterby = 'instructor';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_exportable = true;\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'.\n\t * Only applicable of $orderby is not set.\n\t *\n\t * @var string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var string\n\t */\n\tprotected $orderby = 'title';\n\n\t/**\n\t * Get HTML for buttons in the actions cell of the table\n\t *\n\t * @since 3.37.8\n\t * @since 4.2.0 Added a deep check on whether the quiz is associated to a lesson.\n\t * @since 6.0.0 Don't access `LLMS_Query_Quiz_Attempt` properties directly.\n\t *\n\t * @param LLMS_Quiz $quiz Quiz object.\n\t * @return string\n\t */\n\tprivate function get_actions_html( $quiz ) {\n\t\tif ( ! $quiz->is_orphan( true ) && $quiz->get_course() ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t// If there are quiz attempts for the quiz let the admin know they're going to delete the attempts also.\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'quiz_id'  => $quiz->get( 'id' ),\n\t\t\t\t'per_page' => 1,\n\t\t\t)\n\t\t);\n\n\t\t$msg  = $query->has_results() ? __( 'Are you sure you want to delete this quiz and all associated student attempts?', 'lifterlms' ) : __( 'Are you sure you want to delete this quiz?', 'lifterlms' );\n\t\t$msg .= ' ' . __( 'This action cannot be undone!', 'lifterlms' );\n\n\t\tob_start();\n\t\t?>\n\t\t<form action=\"\" method=\"POST\" style=\"display:inline;\">\n\n\t\t\t<button type=\"submit\" class=\"llms-button-danger small\" id=\"llms-del-quiz-<?php echo esc_attr( $quiz->get( 'id' ) ); ?>\" name=\"llms_del_quiz\" value=\"<?php echo esc_attr( $quiz->get( 'id' ) ); ?>\">\n\t\t\t\t<?php esc_html_e( 'Delete', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t\t<input type=\"hidden\" name=\"_llms_quiz_actions_nonce\" value=\"<?php echo esc_attr( wp_create_nonce( 'llms-quiz-actions' ) ); ?>\">\n\n\t\t</form>\n\n\t\t<script>document.getElementById( 'llms-del-quiz-<?php echo esc_attr( $quiz->get( 'id' ) ); ?>' ).onclick = function( e ) {\n\t\t\treturn window.confirm( '<?php echo esc_attr( $msg ); ?>' );\n\t\t};</script>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Retrieve data for a cell\n\t *\n\t * @since 3.16.0\n\t * @since 3.24.0 Unknown.\n\t * @since 3.37.8 Add actions column that allows deletion of orphaned quizzes.\n\t *               ID column displays as plain text if the quiz is not editable and directs to the quiz within the course builder when it is.\n\t * @since 4.2.0 Added a deep check on whether the quiz is associated to a lesson.\n\t * @since 6.0.0 Don't access `LLMS_Query_Quiz_Attempt` properties directly.\n\t * @since 7.1.3 Added `round()` method for 'average' column values with precision from `llms_get_floats_rounding_precision()` helper.\n\t *\n\t * @param string $key  The column id / key.\n\t * @param mixed  $data Object / array of data that the function can use to extract the data.\n\t * @return mixed\n\t */\n\tprotected function get_data( $key, $data ) {\n\n\t\t$quiz = llms_get_post( $data );\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'actions':\n\t\t\t\t$value = $this->get_actions_html( $quiz );\n\t\t\t\tbreak;\n\n\t\t\tcase 'attempts':\n\t\t\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'quiz_id'    => $quiz->get( 'id' ),\n\t\t\t\t\t\t'count_only' => true,\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'tab'     => 'quizzes',\n\t\t\t\t\t\t'stab'    => 'attempts',\n\t\t\t\t\t\t'quiz_id' => $quiz->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . $url . '\">' . $query->get_count_only_result() . '</a>';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'average':\n\t\t\t\t$grade = 0;\n\t\t\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'quiz_id'  => $quiz->get( 'id' ),\n\t\t\t\t\t\t'per_page' => 1000,\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$attempts = $query->get_number_results();\n\n\t\t\t\tif ( ! $attempts ) {\n\t\t\t\t\t$value = 0;\n\t\t\t\t} else {\n\n\t\t\t\t\tforeach ( $query->get_attempts() as $attempt ) {\n\t\t\t\t\t\t$grade += $attempt->get( 'grade' );\n\t\t\t\t\t}\n\n\t\t\t\t\t$value = round( $grade / $attempts, llms_get_floats_rounding_precision() ) . '%';\n\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'course':\n\t\t\t\t$value  = '&mdash;';\n\t\t\t\t$course = $quiz->get_course();\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'tab'       => 'courses',\n\t\t\t\t\t\t\t'course_id' => $course->get( 'id' ),\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $course->get( 'title' ) . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$id    = $quiz->get( 'id' );\n\t\t\t\t$value = $id;\n\n\t\t\t\t$course = $quiz->get_course();\n\t\t\t\tif ( ! $quiz->is_orphan( true ) && $course ) {\n\n\t\t\t\t\t$url = add_query_arg(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t\t\t\t'course_id' => $course->get( 'id' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t\t);\n\n\t\t\t\t\t$url  .= sprintf( '#lesson:%d:quiz', $quiz->get( 'lesson_id' ) );\n\t\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $id . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'lesson':\n\t\t\t\t$value  = '&mdash;';\n\t\t\t\t$lesson = $quiz->get_lesson();\n\t\t\t\tif ( $lesson ) {\n\t\t\t\t\t$value = $lesson->get( 'title' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'title':\n\t\t\t\t$value = $quiz->get( 'title' );\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'tab'     => 'quizzes',\n\t\t\t\t\t\t'quiz_id' => $quiz->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $quiz->get( 'title' ) . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}\n\n\t\treturn $this->filter_get_data( $value, $key, $data );\n\t}\n\n\t/**\n\t * Retrieve a list of Instructors to be used for Filtering\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_instructor_filters() {\n\n\t\t$query = get_users(\n\t\t\tarray(\n\t\t\t\t'fields'   => array( 'ID', 'display_name' ),\n\t\t\t\t'meta_key' => 'last_name', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'orderby'  => 'meta_value',\n\t\t\t\t'role__in' => array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ),\n\t\t\t)\n\t\t);\n\n\t\t$instructors = wp_list_pluck( $query, 'display_name', 'ID' );\n\n\t\treturn $instructors;\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @since 3.16.0\n\t * @since 3.36.1 Fixed an issue that allow instructors, who can only see their own reports,\n\t *               to see all the quizzes when they had no courses or courses with no lessons.\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Quizzes', 'lifterlms' );\n\n\t\t$args = $this->clean_args( $args );\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$per = apply_filters( 'llms_reporting_' . $this->id . '_per_page', 25 );\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->order;\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->orderby;\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$query_args = array(\n\t\t\t'order'          => $this->order,\n\t\t\t'orderby'        => $this->orderby,\n\t\t\t'paged'          => $this->current_page,\n\t\t\t'post_status'    => 'publish',\n\t\t\t'post_type'      => 'llms_quiz',\n\t\t\t'posts_per_page' => $per,\n\t\t);\n\n\t\tif ( isset( $args['search'] ) ) {\n\t\t\t$query_args['s'] = sanitize_text_field( $args['search'] );\n\t\t}\n\n\t\t// if you can view others reports, make a regular query.\n\t\tif ( current_user_can( 'view_others_lifterlms_reports' ) ) {\n\n\t\t\t$query = new WP_Query( $query_args );\n\n\t\t\t// user can only see their own reports, get a list of their students.\n\t\t} elseif ( current_user_can( 'view_lifterlms_reports' ) ) {\n\n\t\t\t$instructor = llms_get_instructor();\n\t\t\tif ( ! $instructor ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$lessons = array();\n\t\t\t$courses = $instructor->get_courses(\n\t\t\t\tarray(\n\t\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t)\n\t\t\t);\n\t\t\tforeach ( $courses as $course ) {\n\t\t\t\t$lessons = array_merge( $lessons, $course->get_lessons( 'ids' ) );\n\t\t\t}\n\n\t\t\tif ( empty( $lessons ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$query_args['meta_query'] = array(  // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query\n\t\t\t\tarray(\n\t\t\t\t\t'compare' => 'IN',\n\t\t\t\t\t'key'     => '_llms_lesson_id',\n\t\t\t\t\t'value'   => $lessons,\n\t\t\t\t),\n\t\t\t);\n\n\t\t\t$query = new WP_Query( $query_args );\n\n\t\t} else {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t$this->max_pages = $query->max_num_pages;\n\n\t\tif ( $this->max_pages > $this->current_page ) {\n\t\t\t$this->is_last_page = false;\n\t\t}\n\n\t\t$this->tbody_data = $query->posts;\n\t}\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\n\t\t/**\n\t\t * Filter the placeholder used in the search input on the quizzes reporting table.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param string $placeholder The placeholder string.\n\t\t */\n\t\treturn apply_filters( 'llms_table_get_quizzes_search_placeholder', __( 'Search quizzes...', 'lifterlms' ) );\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return array\n\t */\n\tpublic function set_args() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.10 Unknown.\n\t * @since 3.37.8 Added the 'actions' column.\n\t * @since 3.37.12 Fixed the 'actions' column name.\n\t *\n\t * @return array\n\t */\n\tprotected function set_columns() {\n\t\treturn array(\n\t\t\t'id'       => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'title'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Title', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'course'   => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Course', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'lesson'   => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Lesson', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'attempts' => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Total Attempts', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'average'  => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Average Grade', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'actions'  => array(\n\t\t\t\t'exportable' => false,\n\t\t\t\t'title'      => __( 'Actions', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.student.course.php",
    "content": "<?php\n/**\n * Individual Student's Course Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.2.0\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Student_Course class\n *\n * @since 3.2.0\n * @since 3.21.0 Unknown.\n * @since 3.35.0 Get student ID more reliably.\n */\nclass LLMS_Table_Student_Course extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'student-course';\n\n\t/**\n\t * Stores the current section while building the table\n\t * used by $this->output_section_row_html() to determine\n\t * if a new section header needs to be output\n\t *\n\t * @var  int\n\t */\n\tprivate $current_section = null;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = false;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'name';\n\n\t/**\n\t * Instance of LLMS_Student\n\t *\n\t * @var  null\n\t */\n\tpublic $student = null;\n\n\t/**\n\t * Get the HTML for the actions column on the table\n\t *\n\t * @param   obj $lesson LLMS_Lesson..\n\t * @return  string\n\t * @since   3.29.0\n\t * @version 10.0.0\n\t */\n\tprivate function get_actions_html( $lesson ) {\n\t\t$html = '';\n\n\t\t// Evaluate against the student's progress, not the current (admin) user's.\n\t\t// is_quiz_enabled() requires publish status, so unpublished quizzes are ignored.\n\t\t$show_button = true;\n\n\t\tif ( $lesson->is_quiz_enabled() ) {\n\t\t\t$attempt          = $this->student->quizzes()->get_best_attempt( $lesson->get( 'quiz' ) );\n\t\t\t$passing_required = llms_parse_bool( $lesson->get( 'require_passing_grade' ) );\n\n\t\t\tif ( ! $attempt || ( $passing_required && ! $attempt->is_passing() ) ) {\n\t\t\t\t$show_button = false;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters whether the lesson action (mark complete/incomplete) button\n\t\t * should display in the admin student course reporting table.\n\t\t *\n\t\t * @since 10.0.0\n\t\t *\n\t\t * @param bool         $show_button Whether to show the button.\n\t\t * @param LLMS_Lesson  $lesson      Lesson instance.\n\t\t * @param LLMS_Student $student     Student being reported on.\n\t\t */\n\t\t$show_button = apply_filters( 'llms_admin_report_show_lesson_action_button', $show_button, $lesson, $this->student );\n\n\t\tif ( $show_button ) {\n\n\t\t\tif ( $this->student->is_complete( $lesson->get( 'id' ) ) ) {\n\t\t\t\t$action = 'incomplete';\n\t\t\t\t$icon   = 'exclamation-triangle';\n\t\t\t\t$text   = __( 'Mark Incomplete', 'lifterlms' );\n\t\t\t} else {\n\t\t\t\t$action = 'complete';\n\t\t\t\t$icon   = 'check';\n\t\t\t\t$text   = __( 'Mark Complete', 'lifterlms' );\n\t\t\t}\n\t\t\t$html = '\n\t\t\t\t<form action=\"\" method=\"POST\">\n\t\t\t\t\t<input name=\"student_id\" type=\"hidden\" value=\"' . $this->student->get( 'id' ) . '\">\n\t\t\t\t\t<input name=\"lesson_id\" type=\"hidden\" value=\"' . $lesson->get( 'id' ) . '\">\n\t\t\t\t\t<button class=\"llms-button-secondary square small tip--bottom-left\" data-tip=\"' . esc_attr( $text ) . '\" name=\"llms-lesson-action\" type=\"submit\" value=\"' . $action . '\">\n\t\t\t\t\t\t<i class=\"fa fa-' . $icon . '\" aria-hidden=\"true\"></i>\n\t\t\t\t\t</button>\n\t\t\t\t\t' . wp_nonce_field( 'llms-admin-lesson-progression', 'llms-admin-progression-nonce', false, false ) . '\n\t\t\t\t</form>\n\t\t\t';\n\n\t\t}\n\t\treturn $html;\n\t}\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @param    string $key        the column id / key\n\t * @param    int    $lesson     Instance of an LLMS_Lesson\n\t * @return   mixed\n\t * @since    3.2.0\n\t * @version  3.29.0\n\t */\n\tpublic function get_data( $key, $lesson ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'actions':\n\t\t\t\t$value = $this->get_actions_html( $lesson );\n\t\t\t\tbreak;\n\n\t\t\tcase 'completed':\n\t\t\t\t$date  = $this->student->get_completion_date( $lesson->get( 'id' ) );\n\t\t\t\t$value = $date ? $date : '&ndash;';\n\t\t\t\tbreak;\n\n\t\t\tcase 'grade':\n\t\t\t\t$grade = $this->student->get_grade( $lesson->get( 'id' ) );\n\t\t\t\t$value = is_numeric( $grade ) ? $grade . '%' : $grade;\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $this->get_post_link( $lesson->get( 'id' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$value = $lesson->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'quiz':\n\t\t\t\t$q = $lesson->get( 'quiz' );\n\n\t\t\t\tif ( $q ) {\n\n\t\t\t\t\t$url   = esc_url(\n\t\t\t\t\t\tadd_query_arg(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'quiz_id'   => $q,\n\t\t\t\t\t\t\t\t'lesson_id' => $lesson->get( 'id' ),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$value = '<a href=\"' . $url . '\">' . get_the_title( $q ) . '</a>';\n\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch.\n\n\t\treturn $this->filter_get_data( $value, $key, $lesson );\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @param    array $args  array of query args\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$student_id = is_numeric( $args['student'] ) ? absint( $args['student'] ) : ( $args['student'] ? $args['student']->get_id() : 0 );\n\t\tif ( $student_id && ! current_user_can( 'view_others_lifterlms_reports' ) && ! llms_current_user_can( 'view_lifterlms_reports', $student_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$course = new LLMS_Course( absint( $args['course_id'] ) );\n\n\t\tif ( is_numeric( $args['student'] ) ) {\n\t\t\t$args['student'] = new LLMS_Student( $args['student'] );\n\t\t}\n\n\t\t$this->student = $args['student'];\n\n\t\t$this->tbody_data = $course->get_lessons();\n\t}\n\n\t/**\n\t * Output a section title row for each course Section\n\t *\n\t * @param    obj $lesson  the current lesson instance\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.21.0\n\t */\n\tpublic function output_section_row_html( $lesson ) {\n\n\t\tif ( $lesson instanceof LLMS_Lesson ) {\n\n\t\t\t$sid = $lesson->get_parent_section();\n\n\t\t\tif ( $this->current_section !== $sid ) {\n\t\t\t\techo '<tr><td class=\"id\">' . esc_html( $sid ) . '</td><td class=\"section-title\" colspan=\"' . esc_attr( $this->get_columns_count() - 1 ) . '\">' . esc_html( sprintf( _x( 'Section: %s', 'section title', 'lifterlms' ), get_the_title( $sid ) ) ) . '</td></tr>';\n\t\t\t\t$this->current_section = $sid;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Allow custom hooks to be registered for use within the class\n\t *\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.21.0\n\t */\n\tprotected function register_hooks() {\n\t\tadd_action( 'llms_table_before_tr', array( $this, 'output_section_row_html' ), 10, 1 );\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since    2.3.0\n\t * @since 3.35.0 Get student ID more reliably.\n\t *\n\t * @return   array\n\t */\n\tpublic function set_args() {\n\n\t\t$student = false;\n\t\tif ( ! empty( $this->student ) ) {\n\t\t\t$student = $this->student->get_id();\n\t\t} elseif ( ! empty( $_GET['student_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\t$student = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t}\n\n\t\treturn array(\n\t\t\t'page'    => $this->get_current_page(),\n\t\t\t'student' => $student,\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.2.0\n\t * @version  3.29.0\n\t */\n\tpublic function set_columns() {\n\t\treturn array(\n\t\t\t'id'        => array(\n\t\t\t\t'title' => __( 'ID', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name'      => array(\n\t\t\t\t'title' => __( 'Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'quiz'      => array(\n\t\t\t\t'title' => __( 'Quiz', 'lifterlms' ),\n\t\t\t),\n\t\t\t'grade'     => array(\n\t\t\t\t'title' => __( 'Grade', 'lifterlms' ),\n\t\t\t),\n\t\t\t'completed' => array(\n\t\t\t\t'title' => __( 'Completed', 'lifterlms' ),\n\t\t\t),\n\t\t\t'actions'   => array(\n\t\t\t\t'exportable' => false,\n\t\t\t\t'title'      => __( 'Actions', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.student.courses.php",
    "content": "<?php\n/**\n * Individual Student's Courses Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.2.0\n * @version 3.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Student_Courses class\n *\n * @since 3.2.0\n * @since 3.13.0 Unknown.\n */\nclass LLMS_Table_Student_Courses extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'student-courses';\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'name';\n\n\t/**\n\t * Instance of LLMS_Student\n\t *\n\t * @var  null\n\t */\n\tprotected $student = null;\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @param    string $key        the column id / key\n\t * @param    int    $course_id  ID of the course\n\t * @return   mixed\n\t * @since    3.2.0\n\t * @version  3.13.0\n\t */\n\tpublic function get_data( $key, $course_id ) {\n\n\t\t$course = new LLMS_Course( $course_id );\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'progress':\n\t\t\t\t$value = $this->student->get_progress( $course->get( 'id' ), 'course' ) . '%';\n\t\t\t\tbreak;\n\n\t\t\tcase 'completed':\n\t\t\t\t$date  = $this->student->get_completion_date( $course->get( 'id' ) );\n\t\t\t\t$value = $date ? $date : '&ndash;';\n\t\t\t\tbreak;\n\n\t\t\tcase 'grade':\n\t\t\t\t$grade = $this->student->get_grade( $course->get( 'id' ) );\n\t\t\t\t$value = is_numeric( $grade ) ? $grade . '%' : $grade;\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $course->get( 'id' );\n\t\t\t\tif ( current_user_can( 'edit_post', $value ) ) {\n\t\t\t\t\t$value = $this->get_post_link( $value );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$id = $course->get( 'id' );\n\t\t\t\tif ( current_user_can( 'edit_post', $id ) ) {\n\t\t\t\t\t$url   = esc_url(\n\t\t\t\t\t\tadd_query_arg(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'course_id'  => $course->get( 'id' ),\n\t\t\t\t\t\t\t\t'page'       => 'llms-reporting',\n\t\t\t\t\t\t\t\t'stab'       => 'courses',\n\t\t\t\t\t\t\t\t'student_id' => $this->student->get_id(),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$value = '<a href=\"' . $url . '\">' . $course->get( 'title' ) . '</a>';\n\t\t\t\t} else {\n\t\t\t\t\t$value = $course->get( 'title' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$value = llms_get_enrollment_status_name( $this->student->get_enrollment_status( $course->get( 'id' ) ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'updated':\n\t\t\t\t$value = $this->student->get_enrollment_date( $course->get( 'id' ), 'updated' );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $this->filter_get_data( $value, $key, $course_id );\n\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @since 3.2.0\n\t * @since 9.2.3 Added object-level authorization check on the student.\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$student_id = is_numeric( $args['student'] ) ? absint( $args['student'] ) : ( $args['student'] ? $args['student']->get_id() : 0 );\n\t\tif ( $student_id && ! current_user_can( 'view_others_lifterlms_reports' ) && ! llms_current_user_can( 'view_lifterlms_reports', $student_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_numeric( $args['student'] ) ) {\n\t\t\t$args['student'] = new LLMS_Student( $args['student'] );\n\t\t}\n\n\t\t$this->student = $args['student'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$per = apply_filters( 'llms_gradebook_' . $this->id . '_per_page', 20 );\n\n\t\t$order = ! empty( $args['order'] ) ? $args['order'] : 'ASC';\n\t\t$order = in_array( $order, array( 'ASC', 'DESC' ) ) ? $order : 'ASC';\n\n\t\tif ( isset( $args['order'] ) ) {\n\t\t\t$this->order = $order;\n\t\t}\n\t\tif ( isset( $args['orderby'] ) ) {\n\t\t\t$this->orderby = $args['orderby'];\n\t\t}\n\n\t\tswitch ( $this->orderby ) {\n\n\t\t\tcase 'updated':\n\t\t\t\t$orderby = 'upm.updated_date';\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\tdefault:\n\t\t\t\t$orderby = 'p.post_title';\n\n\t\t}\n\n\t\t$courses = $this->student->get_courses(\n\t\t\tarray(\n\t\t\t\t'limit'   => $per,\n\t\t\t\t'skip'    => ( $this->current_page - 1 ) * $per,\n\t\t\t\t'orderby' => $orderby,\n\t\t\t\t'order'   => $order,\n\t\t\t)\n\t\t);\n\n\t\tif ( $courses['more'] ) {\n\t\t\t$this->is_last_page = false;\n\t\t}\n\n\t\t$this->tbody_data = $courses['results'];\n\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since 2.3.0\n\t * @since 3.35.0 Sanitize `$_GET` data.\n\t *\n\t * @return   array\n\t */\n\tpublic function set_args() {\n\n\t\t$student = false;\n\t\tif ( ! empty( $this->student ) ) {\n\t\t\t$student = $this->student->get_id();\n\t\t} elseif ( ! empty( $_GET['student_id'] ) ) {\n\t\t\t$student = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t}\n\n\t\treturn array(\n\t\t\t'page'    => $this->get_current_page(),\n\t\t\t'student' => $student,\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function set_columns() {\n\t\treturn array(\n\t\t\t'id'        => array(\n\t\t\t\t'title' => __( 'ID', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name'      => array(\n\t\t\t\t'title'    => __( 'Name', 'lifterlms' ),\n\t\t\t\t'sortable' => true,\n\t\t\t),\n\t\t\t'status'    => array(\n\t\t\t\t'title' => __( 'Status', 'lifterlms' ),\n\t\t\t),\n\t\t\t'grade'     => array(\n\t\t\t\t'title' => __( 'Grade', 'lifterlms' ),\n\t\t\t),\n\t\t\t'progress'  => array(\n\t\t\t\t'title' => __( 'Progress', 'lifterlms' ),\n\t\t\t),\n\t\t\t'updated'   => array(\n\t\t\t\t'title'    => __( 'Updated', 'lifterlms' ),\n\t\t\t\t'sortable' => true,\n\t\t\t),\n\t\t\t'completed' => array(\n\t\t\t\t'title' => __( 'Completed', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Empty message displayed when no results are found\n\t *\n\t * @return   string\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tprotected function set_empty_message() {\n\t\treturn __( 'This student is not enrolled in any courses.', 'lifterlms' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.student.memberships.php",
    "content": "<?php\n/**\n * Individual Student's Memberships Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.2.0\n * @version 3.7.5\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Student_Memberships\n *\n * @since 3.2.0\n * @since 3.7.5 Unknown.\n * @since 3.35.0 Get student ID more reliably.\n */\nclass LLMS_Table_Student_Memberships extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'student-memberships';\n\n\t/**\n\t * Instance of LLMS_Student\n\t *\n\t * @var  null\n\t */\n\tprotected $student = null;\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @param    string $key            the column id / key\n\t * @param    int    $membership_id  ID of the membership\n\t * @return   mixed\n\t * @since    3.2.0\n\t * @version  3.7.5\n\t */\n\tpublic function get_data( $key, $membership_id ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $this->get_post_link( $membership_id );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$value = get_the_title( $membership_id );\n\t\t\t\tbreak;\n\n\t\t\tcase 'status':\n\t\t\t\t$value = llms_get_enrollment_status_name( $this->student->get_enrollment_status( $membership_id ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'enrolled':\n\t\t\t\t$value = $this->student->get_enrollment_date( $membership_id, 'enrolled' );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}\n\n\t\treturn $this->filter_get_data( $value, $key, $membership_id );\n\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @param    array $args  array of query args\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$student_id = is_numeric( $args['student'] ) ? absint( $args['student'] ) : ( $args['student'] ? $args['student']->get_id() : 0 );\n\t\tif ( $student_id && ! current_user_can( 'view_others_lifterlms_reports' ) && ! llms_current_user_can( 'view_lifterlms_reports', $student_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_numeric( $args['student'] ) ) {\n\t\t\t$args['student'] = new LLMS_Student( $args['student'] );\n\t\t}\n\n\t\t$this->student = $args['student'];\n\n\t\t$this->tbody_data = $this->student->get_membership_levels();\n\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since    2.3.0\n\t * @since 3.35.0 Get student ID more reliably.\n\t *\n\t * @return   array\n\t */\n\tpublic function set_args() {\n\n\t\t$student = false;\n\t\tif ( ! empty( $this->student ) ) {\n\t\t\t$student = $this->student->get_id();\n\t\t} elseif ( ! empty( $_GET['student_id'] ) ) {\n\t\t\t$student = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t}\n\n\t\treturn array(\n\t\t\t'student' => $student,\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function set_columns() {\n\t\treturn array(\n\t\t\t'id'       => array(\n\t\t\t\t'title' => __( 'ID', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name'     => array(\n\t\t\t\t'title' => __( 'Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'status'   => array(\n\t\t\t\t'title' => __( 'Status', 'lifterlms' ),\n\t\t\t),\n\t\t\t'enrolled' => array(\n\t\t\t\t'title' => __( 'Enrolled', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Empty message displayed when no results are found\n\t *\n\t * @return   string\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tprotected function set_empty_message() {\n\t\treturn __( 'This student is not enrolled in any memberships.', 'lifterlms' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.student.quiz.attempts.php",
    "content": "<?php\n/**\n * Student Quiz Attempts Reporting Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 9.1.0\n * @version 9.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Student_Quiz_Attempts class.\n *\n * Displays all quiz attempts for a specific student across all courses.\n *\n * @since 9.1.0\n */\nclass LLMS_Table_Student_Quiz_Attempts extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'student_quiz_attempts';\n\n\t/**\n\t * Value of the field being filtered by\n\t * Only applicable if $filterby is set\n\t *\n\t * @var  string\n\t */\n\tprotected $filter = 'any';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @var  string\n\t */\n\tprotected $filterby = 'grade';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_exportable = true;\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var  boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set\n\t *\n\t * @var  string\n\t */\n\tprotected $order = 'DESC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var  string\n\t */\n\tprotected $orderby = 'id';\n\n\t/**\n\t * Student ID for the displayed student\n\t *\n\t * @var  null\n\t */\n\tprotected $student_id = null;\n\n\t/**\n\t * Retrieve data for a cell.\n\t *\n\t * @since 9.1.0\n\t *\n\t * @param string            $key     The column id / key.\n\t * @param LLMS_Quiz_Attempt $attempt LLMS_Quiz_Attempt obj.\n\t * @return mixed\n\t */\n\tprotected function get_data( $key, $attempt ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'quiz':\n\t\t\t\t$quiz = $attempt->get_quiz();\n\t\t\t\tif ( $quiz ) {\n\t\t\t\t\t$value = $quiz->get( 'title' );\n\n\t\t\t\t\t// Add link to quiz attempts if user has permission\n\t\t\t\t\tif ( current_user_can( 'edit_post', $quiz->get( 'id' ) ) ) {\n\t\t\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'tab'     => 'quizzes',\n\t\t\t\t\t\t\t\t'stab'    => 'attempts',\n\t\t\t\t\t\t\t\t'quiz_id' => $quiz->get( 'id' ),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $value ) . '</a>';\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t$value = __( '[Deleted Quiz]', 'lifterlms' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'course':\n\t\t\t\t$value = '&mdash;';\n\t\t\t\t$quiz  = $attempt->get_quiz();\n\t\t\t\tif ( $quiz ) {\n\t\t\t\t\t$course = $quiz->get_course();\n\t\t\t\t\tif ( $course ) {\n\t\t\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'tab'       => 'courses',\n\t\t\t\t\t\t\t\t'stab'      => 'overview',\n\t\t\t\t\t\t\t\t'course_id' => $course->get( 'id' ),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $course->get( 'title' ) ) . '</a>';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'lesson':\n\t\t\t\t$quiz = $attempt->get_quiz();\n\t\t\t\tif ( $quiz ) {\n\t\t\t\t\t$lesson = $quiz->get_lesson();\n\t\t\t\t\tif ( $lesson ) {\n\t\t\t\t\t\t$value = $lesson->get( 'title' );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$value = __( '[Deleted Lesson]', 'lifterlms' );\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t$value = '&ndash;';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'attempt':\n\t\t\t\t$value = '#' . $attempt->get( $key );\n\t\t\t\tbreak;\n\n\t\t\tcase 'grade':\n\t\t\t\t$value      = $attempt->get( $key ) ? $attempt->get( $key ) . '%' : '0%';\n\t\t\t\t$additional = $attempt->l10n( 'status' );\n\t\t\t\tif ( $attempt->can_be_resumed() && $attempt->is_last_attempt() ) {\n\t\t\t\t\t$additional .= ' - ' . esc_html__( 'Can be resumed', 'lifterlms' );\n\t\t\t\t}\n\t\t\t\t$value .= ' (' . $additional . ')';\n\t\t\t\tbreak;\n\n\t\t\tcase 'start_date':\n\t\t\tcase 'end_date':\n\t\t\t\t$value = '&ndash;';\n\t\t\t\t$date  = $attempt->get( $key );\n\t\t\t\tif ( $date ) {\n\t\t\t\t\t$value = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $date ) );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$value = sprintf( '%2$d (%1$s)', $attempt->get_key(), $attempt->get( 'id' ) );\n\n\t\t\t\t$url = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'tab'        => 'quizzes',\n\t\t\t\t\t\t'stab'       => 'attempts',\n\t\t\t\t\t\t'quiz_id'    => $attempt->get( 'quiz_id' ),\n\t\t\t\t\t\t'attempt_id' => $attempt->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $value . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}// End switch().\n\n\t\treturn $value;\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @since 9.1.0\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$this->title = __( 'Quiz Attempts', 'lifterlms' );\n\n\t\t$args = $this->clean_args( $args );\n\n\t\t$this->student_id = $args['student_id'];\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$per = apply_filters( 'llms_reporting_' . $this->id . '_per_page', 25 );\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->order;\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->orderby;\n\n\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\n\t\t$query_args = array(\n\t\t\t'sort'       => array(\n\t\t\t\t$this->orderby => $this->order,\n\t\t\t),\n\t\t\t'page'       => $this->current_page,\n\t\t\t'per_page'   => $per,\n\t\t\t'student_id' => $this->student_id,\n\t\t);\n\n\t\t// Add search functionality\n\t\tif ( isset( $args['search'] ) && ! empty( $args['search'] ) ) {\n\t\t\t$query_args['search'] = $args['search'];\n\t\t}\n\n\t\tif ( 'any' !== $this->filter ) {\n\t\t\t$query_args['status'] = $this->filter;\n\t\t}\n\n\t\t// Check permissions\n\t\tif ( ! current_user_can( 'view_others_lifterlms_reports' ) && ! llms_current_user_can( 'view_lifterlms_reports', $this->student_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$query = new LLMS_Query_Quiz_Attempt( $query_args );\n\n\t\t$this->max_pages    = $query->get_max_pages();\n\t\t$this->is_last_page = $query->is_last_page();\n\n\t\t$this->tbody_data = $query->get_attempts();\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since 9.1.0\n\t *\n\t * @return array\n\t */\n\tpublic function set_args() {\n\t\treturn array(\n\t\t\t'student_id' => ! empty( $this->student_id ) ? $this->student_id : ( isset( $_GET['student_id'] ) ? absint( $_GET['student_id'] ) : null ),\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return array\n\t * @since 9.1.0\n\t */\n\tprotected function set_columns() {\n\n\t\t$cols = array(\n\t\t\t'id'         => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'quiz'       => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Quiz', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'course'     => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Course', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'lesson'     => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Lesson', 'lifterlms' ),\n\t\t\t\t'sortable'   => false,\n\t\t\t),\n\t\t\t'attempt'    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Attempt #', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'grade'      => array(\n\t\t\t\t'filterable' => llms_get_quiz_attempt_statuses(),\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Grade', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'start_date' => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'Start Date', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t\t'end_date'   => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'title'      => __( 'End Date', 'lifterlms' ),\n\t\t\t\t'sortable'   => true,\n\t\t\t),\n\t\t);\n\n\t\treturn $cols;\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/tables/llms.table.students.php",
    "content": "<?php\n/**\n * Students Reporting Table\n *\n * @package LifterLMS/Admin/Reporting/Tables/Classes\n *\n * @since 3.2.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_Students class\n *\n * @since 3.2.0\n * @since 3.28.0 Unknown.\n * @since 3.31.0 Allow filtering the table by Course or Membership\n * @since 3.36.0 Add \"Last Seen\" column.\n * @since 3.36.1 Fixed \"Last Seen\" column displaying wrong date when the student last login date was saved as timestamp.\n * @since 3.37.2 The post filter on the students table now limits post results based on instructor access.\n */\nclass LLMS_Table_Students extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'students';\n\n\t/**\n\t * Value of the field being filtered by\n\t *\n\t * Only applicable if $filterby is set.\n\t *\n\t * @since 3.31.0\n\t * @var string\n\t */\n\tprotected $filter = '';\n\n\t/**\n\t * Field results are filtered by\n\t *\n\t * @since 3.31.0\n\t * @var string\n\t */\n\tprotected $filterby = 'course_membership';\n\n\t/**\n\t * Is the Table Exportable?\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_exportable = true;\n\n\t/**\n\t * Determine if the table is filterable\n\t *\n\t * @since 3.31.0\n\t * @var boolean\n\t */\n\tprotected $is_filterable = true;\n\n\t/**\n\t * If true, tfoot will add ajax pagination links\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_paginated = true;\n\n\t/**\n\t * Determine of the table is searchable\n\t *\n\t * @var boolean\n\t */\n\tprotected $is_searchable = true;\n\n\t/**\n\t * Results sort order\n\t *\n\t * 'ASC' or 'DESC'\n\t * Only applicable of $orderby is not set.\n\t *\n\t * @var string\n\t */\n\tprotected $order = 'ASC';\n\n\t/**\n\t * Field results are sorted by\n\t *\n\t * @var string\n\t */\n\tprotected $orderby = 'name';\n\n\t/**\n\t * Number of records to display per page\n\t *\n\t * @var int\n\t */\n\tprotected $per_page = 25;\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Unknown.\n\t * @since 3.36.0 Added \"Last Seen\" column.\n\t * @since 3.36.1 Fixed \"Last Seen\" column displaying wrong date when the student last login date was saved as timestamp.\n\t * @since 4.7.0 Speed up the query used to retrieve the last seen column by avoiding the found rows calculation.\n\t * @since 6.0.0 Don't access `LLMS_Events_Query` properties directly\n\t *              Use `LLMS_Student::get_awards_count()` for retrieving the number of earned achievements and certificates.\n\t *\n\t * @param string       $key     The column id / key.\n\t * @param LLMS_Student $student Instance of the LLMS_Student.\n\t * @return mixed\n\t */\n\tpublic function get_data( $key, $student ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'achievements':\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'stab'       => 'achievements',\n\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $student->get_awards_count( 'achievement' ) . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tcase 'certificates':\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'stab'       => 'certificates',\n\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $student->get_awards_count( 'certificate' ) . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tcase 'completions':\n\t\t\t\t$courses = $student->get_completed_courses();\n\t\t\t\t$value   = count( $courses['results'] );\n\t\t\t\tbreak;\n\n\t\t\tcase 'enrollments':\n\t\t\t\t$url         = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'stab'       => 'courses',\n\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$enrollments = $student->get_courses(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'limit' => 1,\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value       = '<a href=\"' . esc_url( $url ) . '\">' . $enrollments['found'] . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tcase 'id':\n\t\t\t\t$id = $student->get_id();\n\t\t\t\tif ( current_user_can( 'list_users' ) ) {\n\t\t\t\t\t$value = '<a href=\"' . esc_url( get_edit_user_link( $id ) ) . '\">' . $id . '</a>';\n\t\t\t\t} else {\n\t\t\t\t\t$value = $id;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'last_seen':\n\t\t\t\t$query = new LLMS_Events_Query(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'actor'         => $student->get_id(),\n\t\t\t\t\t\t'per_page'      => 1,\n\t\t\t\t\t\t'sort'          => array(\n\t\t\t\t\t\t\t'date' => 'DESC',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'no_found_rows' => true,\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\tif ( $query->has_results() ) {\n\t\t\t\t\t$events = $query->get_events();\n\t\t\t\t\t$last   = array_shift( $events );\n\t\t\t\t\t$value  = $last->get( 'date' );\n\t\t\t\t} else {\n\t\t\t\t\t$value = $student->get( 'last_login' );\n\t\t\t\t}\n\n\t\t\t\t$value = $value ? date_i18n( get_option( 'date_format' ), is_numeric( $value ) ? $value : strtotime( $value ) ) : '&ndash;';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'memberships':\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'stab'       => 'memberships',\n\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . count( $student->get_membership_levels() ) . '</a>';\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$first = $student->get( 'first_name' );\n\t\t\t\t$last  = $student->get( 'last_name' );\n\n\t\t\t\tif ( ! $first || ! $last ) {\n\t\t\t\t\t$value = $student->get( 'display_name' );\n\t\t\t\t} else {\n\t\t\t\t\t$value = $last . ', ' . $first;\n\t\t\t\t}\n\n\t\t\t\t$url   = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$value = '<a href=\"' . esc_url( $url ) . '\">' . $value . '</a>';\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'overall_grade':\n\t\t\t\t$value = $student->get_overall_grade( true );\n\t\t\t\tif ( is_numeric( $value ) ) {\n\t\t\t\t\t$value .= '%';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'overall_progress':\n\t\t\t\t$value = $this->get_progress_bar_html( $student->get_overall_progress( true ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'registered':\n\t\t\t\t$value = $student->get_registration_date();\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $key;\n\n\t\t}\n\n\t\treturn $this->filter_get_data( $value, $key, $student );\n\t}\n\n\t/**\n\t * Retrieve data for a cell in an export file\n\t *\n\t * Should be overridden in extending classes.\n\t *\n\t * @since 3.15.0\n\t * @since 3.26.1 Unknown.\n\t *\n\t * @param string       $key     The column id / key.\n\t * @param LLMS_Student $student Instance of the LLMS_Student.\n\t * @return mixed\n\t */\n\tpublic function get_export_data( $key, $student ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'id':\n\t\t\t\t$value = $student->get_id();\n\t\t\t\tbreak;\n\n\t\t\tcase 'courses_cancelled':\n\t\t\tcase 'courses_enrolled':\n\t\t\tcase 'courses_expired':\n\t\t\t\t$status  = explode( '_', $key );\n\t\t\t\t$status  = array_pop( $status );\n\t\t\t\t$courses = $student->get_courses(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'status' => $status,\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$titles  = array();\n\t\t\t\tforeach ( $courses['results'] as $id ) {\n\t\t\t\t\t$titles[] = get_the_title( $id );\n\t\t\t\t}\n\t\t\t\t$value = implode( ', ', $titles );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$value = $student->get( 'user_email' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'memberships_cancelled':\n\t\t\tcase 'memberships_enrolled':\n\t\t\tcase 'memberships_expired':\n\t\t\t\t$status      = explode( '_', $key );\n\t\t\t\t$status      = array_pop( $status );\n\t\t\t\t$memberships = $student->get_memberships(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'status' => $status,\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$titles      = array();\n\t\t\t\tforeach ( $memberships['results'] as $id ) {\n\t\t\t\t\t$titles[] = get_the_title( $id );\n\t\t\t\t}\n\t\t\t\t$value = implode( ', ', $titles );\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'name_first':\n\t\t\t\t$value = $student->get( 'first_name' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'name_last':\n\t\t\t\t$value = $student->get( 'last_name' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'overall_grade':\n\t\t\t\t$value = $student->get_overall_grade( false );\n\t\t\t\tif ( is_numeric( $value ) ) {\n\t\t\t\t\t$value .= '%';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'overall_progress':\n\t\t\t\t$value = $student->get_overall_progress( false ) . '%';\n\t\t\t\tbreak;\n\n\t\t\tcase 'billing_address_1':\n\t\t\tcase 'billing_address_2':\n\t\t\tcase 'billing_city':\n\t\t\tcase 'billing_state':\n\t\t\tcase 'billing_zip':\n\t\t\tcase 'billing_country':\n\t\t\tcase 'phone':\n\t\t\t\t$value = $student->get( $key );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $this->get_data( $key, $student );\n\n\t\t}\n\n\t\treturn $this->filter_get_data( $value, $key, $student, 'export' );\n\t}\n\n\t/**\n\t * Get the Text to be used as the placeholder in a searchable tables search input\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_table_search_form_placeholder() {\n\t\treturn apply_filters( 'llms_table_get_' . $this->id . '_search_placeholder', __( 'Search students by name or email...', 'lifterlms' ) );\n\t}\n\n\tpublic function output_table_title_html() {\n\t\tparent::output_table_title_html();\n\t\t?>\n\t\t<form id=\"llms-clear-student-progress-cache\" action=\"<?php echo esc_url( LLMS_Admin_Page_Status::get_url( 'tools' ) ); ?>\" method=\"POST\">\n\t\t\t<button class=\"button button-secondary\" id=\"llms-clear-reporting-cache\" name=\"llms_tool\" type=\"submit\" value=\"clear-cache\"><?php echo esc_html__( 'Clear Reporting Cache', 'lifterlms' ); ?></button>\n\t\t\t<?php wp_nonce_field( 'llms_tool' ); ?>\n\t\t\t<label for=\"llms-clear-reporting-cache\">\n\t\t\t\t<span class=\"screen-reader-text\"><?php echo esc_html__( 'Click to refresh the data and display the most current reporting information', 'lifterlms' ); ?></span>\n\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php echo esc_html__( 'Click to refresh the data and display the most current reporting information', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t</label>\n\t\t</form>\n\t\t<?php\n\t}\n\n\t/**\n\t * Get HTML for the filters displayed in the head of the table\n\t *\n\t * This overrides the LLMS_Admin_Table method.\n\t *\n\t * @since 3.31.0\n\t * @since 3.37.2 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function output_table_filters_html() {\n\t\t$select_id     = sprintf( '%1$s-%2$s-filter', $this->id, 'course-membership' );\n\t\t$instructor_id = null;\n\t\t// Limit Course/Membership results based on instructor access.\n\t\tif ( ! current_user_can( 'view_others_lifterlms_reports' ) && current_user_can( 'view_lifterlms_reports' ) ) {\n\t\t\t$instructor = llms_get_instructor();\n\t\t\tif ( $instructor ) {\n\t\t\t\t$instructor_id = $instructor->get( 'id' );\n\t\t\t}\n\t\t}\n\t\t?>\n\t\t<div class=\"llms-table-filters\">\n\t\t\t<div class=\"llms-table-filter-wrap\">\n\t\t\t\t<label class=\"screen-reader-text\" for=\"<?php echo esc_attr( $select_id ); ?>\">\n\t\t\t\t\t<?php esc_html_e( 'Choose Course/Membership', 'lifterlms' ); ?>\n\t\t\t\t</label>\n\t\t\t\t<select\n\t\t\t\t\tdata-post-type=\"llms_membership,course\"\n\t\t\t\t\tclass=\"llms-posts-select2 llms-table-filter\"\n\t\t\t\t\tid=\"<?php echo esc_attr( $select_id ); ?>\"\n\t\t\t\t\tname=\"course_membership\"\n\t\t\t\t\tstyle=\"min-width:200px;max-width:500px;\"\n\t\t\t\t\t<?php if ( $instructor_id ) : ?>\n\t\t\t\t\tdata-instructor-id=\"<?php echo esc_attr( $instructor_id ); ?>\"\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t></select>\n\t\t\t</div>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Retrieve an array of query arguments to pass to the LLMS_Student_Query\n\t *\n\t * @since 3.28.0\n\t * @since 3.31.0 Added logic to setup the query args in order to allow the filtering by Course or Membership.\n\t *\n\t * @return array\n\t */\n\tprivate function get_query_args() {\n\n\t\t$query_args = array(\n\t\t\t'page'     => $this->get_current_page(),\n\t\t\t'post_id'  => array(),\n\t\t\t'per_page' => $this->get_per_page(),\n\t\t\t'sort'     => $this->get_sort(),\n\t\t);\n\n\t\tif ( 'status' === $this->get_filterby() && 'any' !== $this->get_filter() ) {\n\n\t\t\t$query_args['statuses'] = array( $this->get_filter() );\n\n\t\t} elseif ( 'course_membership' === $this->get_filterby() && '' !== $this->get_filter() ) {\n\n\t\t\t$query_args['post_id']  = absint( $this->get_filter() );\n\t\t\t$query_args['statuses'] = 'enrolled';\n\n\t\t}\n\n\t\tif ( $this->get_search() ) {\n\t\t\t$query_args['search'] = $this->get_search();\n\t\t}\n\n\t\treturn $query_args;\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @since 3.2.0\n\t * @since 3.28.0 Unknown.\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @param array $args Array of query args.\n\t * @return void\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t// Current user can't access this report.\n\t\tif ( ! current_user_can( 'view_others_lifterlms_reports' ) && ! current_user_can( 'view_lifterlms_reports' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->parse_args( $args );\n\n\t\t$query_args = $this->get_query_args();\n\n\t\tif ( current_user_can( 'view_others_lifterlms_reports' ) ) {\n\n\t\t\t$query = new LLMS_Student_Query( $query_args );\n\n\t\t} elseif ( current_user_can( 'view_lifterlms_reports' ) ) {\n\n\t\t\t$instructor = llms_get_instructor();\n\t\t\tif ( ! $instructor ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t$query = $instructor->get_students( $query_args );\n\n\t\t}\n\n\t\t$this->max_pages    = $query->get_max_pages();\n\t\t$this->is_last_page = $query->is_last_page();\n\n\t\t$this->tbody_data = $query->get_students();\n\t}\n\n\t/**\n\t * Setup the array of sort arguments to pass to the LLMS_Student_Query for the table\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_sort() {\n\n\t\t$sort = array();\n\t\tswitch ( $this->get_orderby() ) {\n\n\t\t\tcase 'id':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'id' => $this->get_order(),\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'name':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'last_name'  => $this->get_order(),\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'overall_grade':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'overall_grade' => $this->get_order(),\n\t\t\t\t\t'last_name'     => 'ASC',\n\t\t\t\t\t'first_name'    => 'ASC',\n\t\t\t\t\t'id'            => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'overall_progress':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'overall_progress' => $this->get_order(),\n\t\t\t\t\t'last_name'        => 'ASC',\n\t\t\t\t\t'first_name'       => 'ASC',\n\t\t\t\t\t'id'               => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'registered':\n\t\t\t\t$sort = array(\n\t\t\t\t\t'registered' => $this->get_order(),\n\t\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t\t'id'         => 'ASC',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $sort;\n\t}\n\n\t/**\n\t * Parse arguments passed to get_results() method & setup table class variables.\n\t *\n\t * @since 3.28.0\n\t * @since 3.31.0 Added logic to parse 'filterby' and 'filter' args when this table is filterable.\n\t *\n\t * @param array $args Array of arguments.\n\t * @return void\n\t */\n\tprotected function parse_args( $args = array() ) {\n\n\t\tif ( ! $args ) {\n\t\t\t$args = $this->get_args();\n\t\t}\n\n\t\t$args = $this->clean_args( $args );\n\n\t\tif ( isset( $args['page'] ) ) {\n\t\t\t$this->current_page = absint( $args['page'] );\n\t\t}\n\n\t\t$this->order   = isset( $args['order'] ) ? $args['order'] : $this->get_order();\n\t\t$this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->get_orderby();\n\n\t\t$this->per_page = isset( $args['per_page'] ) ? $args['per_page'] : $this->get_per_page();\n\n\t\tif ( $this->is_filterable ) {\n\t\t\t$this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby();\n\t\t\t$this->filter   = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter();\n\t\t}\n\n\t\tif ( isset( $args['search'] ) ) {\n\t\t\t$this->search = $args['search'];\n\t\t}\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @since 2.3.0\n\t * @since 3.28.0 Unknown.\n\t *\n\t * @return array\n\t */\n\tpublic function set_args() {\n\t\treturn array(\n\t\t\t'per_page' => apply_filters( 'llms_table_' . $this->id . '_per_page', $this->per_page ),\n\t\t);\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @since 3.2.0\n\t * @since 3.15.0 Unknown.\n\t * @since 3.36.0 Add \"Last Seen\" column.\n\t *\n\t * @return array\n\t */\n\tpublic function set_columns() {\n\t\treturn array(\n\t\t\t'id'                    => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'ID', 'lifterlms' ),\n\t\t\t),\n\t\t\t'email'                 => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Email', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name'                  => array(\n\t\t\t\t'sortable' => true,\n\t\t\t\t'title'    => __( 'Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name_last'             => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Last Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'name_first'            => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'First Name', 'lifterlms' ),\n\t\t\t),\n\t\t\t'registered'            => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Registration Date', 'lifterlms' ),\n\t\t\t),\n\t\t\t'last_seen'             => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => false,\n\t\t\t\t'title'      => __( 'Last Seen', 'lifterlms' ),\n\t\t\t),\n\t\t\t'overall_progress'      => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Progress', 'lifterlms' ),\n\t\t\t),\n\t\t\t'overall_grade'         => array(\n\t\t\t\t'exportable' => true,\n\t\t\t\t'sortable'   => true,\n\t\t\t\t'title'      => __( 'Grade', 'lifterlms' ),\n\t\t\t),\n\t\t\t'enrollments'           => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Enrollments', 'lifterlms' ),\n\t\t\t),\n\t\t\t'completions'           => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Completions', 'lifterlms' ),\n\t\t\t),\n\t\t\t'certificates'          => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Certificates', 'lifterlms' ),\n\t\t\t),\n\t\t\t'achievements'          => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Achievements', 'lifterlms' ),\n\t\t\t),\n\t\t\t'memberships'           => array(\n\t\t\t\t'sortable' => false,\n\t\t\t\t'title'    => __( 'Memberships', 'lifterlms' ),\n\t\t\t),\n\t\t\t'billing_address_1'     => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Billing Address 1', 'lifterlms' ),\n\t\t\t),\n\t\t\t'billing_address_2'     => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Billing Address 2', 'lifterlms' ),\n\t\t\t),\n\t\t\t'billing_city'          => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Billing City', 'lifterlms' ),\n\t\t\t),\n\t\t\t'billing_state'         => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Billing State', 'lifterlms' ),\n\t\t\t),\n\t\t\t'billing_zip'           => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Billing Zip', 'lifterlms' ),\n\t\t\t),\n\t\t\t'billing_country'       => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Billing Country', 'lifterlms' ),\n\t\t\t),\n\t\t\t'phone'                 => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Phone', 'lifterlms' ),\n\t\t\t),\n\t\t\t'courses_enrolled'      => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Courses (Enrolled)', 'lifterlms' ),\n\t\t\t),\n\t\t\t'courses_cancelled'     => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Courses (Cancelled)', 'lifterlms' ),\n\t\t\t),\n\t\t\t'courses_expired'       => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Courses (Expired)', 'lifterlms' ),\n\t\t\t),\n\t\t\t'memberships_enrolled'  => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Memberships (Enrolled)', 'lifterlms' ),\n\t\t\t),\n\t\t\t'memberships_cancelled' => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Memberships (Cancelled)', 'lifterlms' ),\n\t\t\t),\n\t\t\t'memberships_expired'   => array(\n\t\t\t\t'exportable'  => true,\n\t\t\t\t'export_only' => true,\n\t\t\t\t'title'       => __( 'Memberships (Expired)', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Set the table's title.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\treturn __( 'Students', 'lifterlms' );\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/tabs/class.llms.admin.reporting.tab.courses.php",
    "content": "<?php\n/**\n * Courses Tab on Reporting Screen\n *\n * @package LifterLMS/Admin/Reporting/Tabs/Classes\n *\n * @since 3.15.0\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Reporting_Tab_Courses\n *\n * @since 3.15.0\n * @since 3.35.0 Sanitize input data.\n */\nclass LLMS_Admin_Reporting_Tab_Courses {\n\n\t/**\n\t * Constructor\n\t *\n\t * @return   void\n\t * @since    3.15.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'llms_reporting_content_courses', array( $this, 'output' ) );\n\t\tadd_action( 'llms_reporting_course_tab_breadcrumbs', array( $this, 'breadcrumbs' ) );\n\t}\n\n\t/**\n\t * Add breadcrumb links to the tab depending on current view\n\t *\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function breadcrumbs() {\n\n\t\t$links = array();\n\n\t\t// Single student.\n\t\tif ( isset( $_GET['course_id'] ) ) {\n\t\t\t$course = llms_get_post( absint( $_GET['course_id'] ) );\n\t\t\t$links[ LLMS_Admin_Reporting::get_stab_url( 'overview' ) ] = $course->get( 'title' );\n\t\t}\n\n\t\tforeach ( $links as $url => $title ) {\n\n\t\t\techo '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $title ) . '</a>';\n\n\t\t}\n\t}\n\n\t/**\n\t * Output tab content\n\t *\n\t * @since 3.15.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t// Single course.\n\t\tif ( isset( $_GET['course_id'] ) ) {\n\n\t\t\tif ( ! current_user_can( 'edit_post', llms_filter_input( INPUT_GET, 'course_id', FILTER_SANITIZE_NUMBER_INT ) ) ) {\n\t\t\t\twp_die( esc_html__( 'You do not have permission to access this content.', 'lifterlms' ) );\n\t\t\t}\n\n\t\t\t$tabs = apply_filters(\n\t\t\t\t'llms_reporting_tab_course_tabs',\n\t\t\t\tarray(\n\t\t\t\t\t'overview' => __( 'Overview', 'lifterlms' ),\n\t\t\t\t\t'students' => __( 'Students', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/courses/course.php',\n\t\t\t\tarray(\n\t\t\t\t\t'current_tab' => isset( $_GET['stab'] ) ? esc_attr( llms_filter_input_sanitize_string( INPUT_GET, 'stab' ) ) : 'overview',\n\t\t\t\t\t'tabs'        => $tabs,\n\t\t\t\t\t'course'      => llms_get_post( intval( $_GET['course_id'] ) ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t} else {\n\n\t\t\t$table = new LLMS_Table_Courses();\n\t\t\t$table->get_results();\n\t\t\t$table->output_table_html();\n\n\t\t}\n\t}\n}\nreturn new LLMS_Admin_Reporting_Tab_Courses();\n"
  },
  {
    "path": "includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php",
    "content": "<?php\n/**\n * Enrollments Tab on Reporting Screen\n *\n * @package LifterLMS/Admin/Reporting/Tabs/Classes\n *\n * @since 3.2.0\n * @version 3.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Reporting_Tab_Enrollments class\n *\n * @since 3.2.0\n * @since 3.5.0 Unknown.\n */\nclass LLMS_Admin_Reporting_Tab_Enrollments {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'llms_reporting_after_nav', array( $this, 'output_filters' ), 10, 1 );\n\t\tadd_action( 'llms_reporting_content_enrollments', array( $this, 'output' ) );\n\n\t}\n\n\tpublic static function get_filter_data() {\n\n\t\t$data = array();\n\n\t\t$data['current_tab'] = LLMS_Admin_Reporting::get_current_tab();\n\n\t\t$data['current_range'] = LLMS_Admin_Reporting::get_current_range();\n\n\t\t$data['current_students'] = LLMS_Admin_Reporting::get_current_students();\n\n\t\t$data['current_courses'] = LLMS_Admin_Reporting::get_current_courses();\n\n\t\t$data['current_memberships'] = LLMS_Admin_Reporting::get_current_memberships();\n\n\t\t$data['dates']      = LLMS_Admin_Reporting::get_dates( $data['current_range'] );\n\t\t$data['date_start'] = $data['dates']['start'];\n\t\t$data['date_end']   = $data['dates']['end'];\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get an array of ajax widgets to load on page load\n\t *\n\t * @return   array\n\t * @since    3.2.0\n\t * @version  3.5.0\n\t */\n\tpublic function get_widget_data() {\n\t\treturn apply_filters(\n\t\t\t'llms_reporting_tab_enrollments_widgets',\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'registrations'     => array(\n\t\t\t\t\t\t'title'   => __( 'Registrations', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of total user registrations during the selected period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'enrollments'       => array(\n\t\t\t\t\t\t'title'   => __( 'Enrollments', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of total enrollments during the selected period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'coursecompletions' => array(\n\t\t\t\t\t\t'title'   => __( 'Courses Completed', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of total courses completed during the selected period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'lessoncompletions' => array(\n\t\t\t\t\t\t'title'   => __( 'Lessons Completed', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of total lessons completed during the selected period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Output the template for the sales tab\n\t *\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function output() {\n\n\t\tllms_get_template(\n\t\t\t'admin/reporting/tabs/widgets.php',\n\t\t\tarray(\n\t\t\t\t'json'        => json_encode( self::get_filter_data() ),\n\t\t\t\t'widget_data' => $this->get_widget_data(),\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Output filters navigation\n\t *\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function output_filters( $tab ) {\n\n\t\tif ( 'enrollments' === $tab ) {\n\n\t\t\tllms_get_template( 'admin/reporting/nav-filters.php', self::get_filter_data() );\n\n\t\t}\n\n\t}\n\n}\nreturn new LLMS_Admin_Reporting_Tab_Enrollments();\n"
  },
  {
    "path": "includes/admin/reporting/tabs/class.llms.admin.reporting.tab.memberships.php",
    "content": "<?php\n/**\n * Memberships Tab on Reporting Screen\n *\n * @package LifterLMS/Admin/Reporting/Tabs/Classes\n *\n * @since 3.32.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Memberships Tab on Reporting Screen class\n *\n * @since 3.32.0\n * @since 3.35.0 Sanitize input data.\n */\nclass LLMS_Admin_Reporting_Tab_Memberships {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'llms_reporting_content_memberships', array( $this, 'output' ) );\n\t\tadd_action( 'llms_reporting_membership_tab_breadcrumbs', array( $this, 'breadcrumbs' ) );\n\t}\n\n\t/**\n\t * Add breadcrumb links to the tab depending on current view.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return void\n\t */\n\tpublic function breadcrumbs() {\n\n\t\t$links = array();\n\n\t\t// Single student.\n\t\tif ( isset( $_GET['membership_id'] ) ) {\n\t\t\t$membership = llms_get_post( absint( $_GET['membership_id'] ) );\n\t\t\t$links[ LLMS_Admin_Reporting::get_stab_url( 'overview' ) ] = $membership->get( 'title' );\n\t\t}\n\n\t\tforeach ( $links as $url => $title ) {\n\n\t\t\techo '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $title ) . '</a>';\n\n\t\t}\n\t}\n\n\t/**\n\t * Output tab content.\n\t *\n\t * @since 3.32.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t// Single membership.\n\t\tif ( isset( $_GET['membership_id'] ) ) {\n\n\t\t\tif ( ! current_user_can( 'edit_post', llms_filter_input( INPUT_GET, 'membership_id', FILTER_SANITIZE_NUMBER_INT ) ) ) {\n\t\t\t\twp_die( esc_html__( 'You do not have permission to access this content.', 'lifterlms' ) );\n\t\t\t}\n\n\t\t\t$tabs = apply_filters(\n\t\t\t\t'llms_reporting_tab_membership_tabs',\n\t\t\t\tarray(\n\t\t\t\t\t'overview' => __( 'Overview', 'lifterlms' ),\n\t\t\t\t\t'students' => __( 'Students', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/memberships/membership.php',\n\t\t\t\tarray(\n\t\t\t\t\t'current_tab' => isset( $_GET['stab'] ) ? esc_attr( llms_filter_input_sanitize_string( INPUT_GET, 'stab' ) ) : 'overview',\n\t\t\t\t\t'tabs'        => $tabs,\n\t\t\t\t\t'membership'  => llms_get_post( intval( $_GET['membership_id'] ) ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t} else {\n\n\t\t\t$table = new LLMS_Table_Memberships();\n\t\t\t$table->get_results();\n\t\t\t$table->output_table_html();\n\n\t\t}\n\t}\n}\n\nreturn new LLMS_Admin_Reporting_Tab_Memberships();\n"
  },
  {
    "path": "includes/admin/reporting/tabs/class.llms.admin.reporting.tab.quizzes.php",
    "content": "<?php\n/**\n * Quizzes Tab on Reporting Screen\n *\n * @package LifterLMS/Admin/Reporting/Tabs/Classes\n *\n * @since 3.16.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Reporting_Tab_Quizzes\n *\n * @since 3.16.0\n * @since 3.35.0 Sanitize input data.\n */\nclass LLMS_Admin_Reporting_Tab_Quizzes {\n\n\t/**\n\t * Constructor\n\t *\n\t * @return   void\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'llms_reporting_content_quizzes', array( $this, 'output' ) );\n\t\tadd_action( 'llms_reporting_quiz_tab_breadcrumbs', array( $this, 'breadcrumbs' ) );\n\t}\n\n\t/**\n\t * Add breadcrumb links to the tab depending on current view\n\t *\n\t * @return   void\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function breadcrumbs() {\n\n\t\t$links = array();\n\n\t\t// Single quiz.\n\t\tif ( isset( $_GET['quiz_id'] ) ) {\n\t\t\t$quiz = llms_get_post( absint( $_GET['quiz_id'] ) );\n\t\t\t$links[ LLMS_Admin_Reporting::get_stab_url( 'overview' ) ] = $quiz->get( 'title' );\n\t\t}\n\n\t\tif ( isset( $_GET['attempt_id'] ) ) {\n\n\t\t\t$attempt = new LLMS_Quiz_Attempt( absint( $_GET['attempt_id'] ) );\n\t\t\t$links[ LLMS_Admin_Reporting::get_stab_url( 'attempts' ) ] = $attempt->get_title();\n\n\t\t}\n\n\t\tforeach ( $links as $url => $title ) {\n\n\t\t\techo '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $title ) . '</a>';\n\n\t\t}\n\t}\n\n\t/**\n\t * Output tab content\n\t *\n\t * @since 3.16.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t// Single quiz.\n\t\tif ( isset( $_GET['quiz_id'] ) ) {\n\n\t\t\tif ( ! current_user_can( 'edit_post', llms_filter_input( INPUT_GET, 'quiz_id', FILTER_SANITIZE_NUMBER_INT ) ) ) {\n\t\t\t\twp_die( esc_html__( 'You do not have permission to access this content.', 'lifterlms' ) );\n\t\t\t}\n\n\t\t\t$tabs = apply_filters(\n\t\t\t\t'llms_reporting_tab_quiz_tabs',\n\t\t\t\tarray(\n\t\t\t\t\t'overview'     => __( 'Overview', 'lifterlms' ),\n\t\t\t\t\t'attempts'     => __( 'Attempts', 'lifterlms' ),\n\t\t\t\t\t'non-attempts' => __( 'Students Without Attempts', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/quizzes/quiz.php',\n\t\t\t\tarray(\n\t\t\t\t\t'current_tab' => isset( $_GET['stab'] ) ? esc_attr( llms_filter_input_sanitize_string( INPUT_GET, 'stab' ) ) : 'overview',\n\t\t\t\t\t'tabs'        => $tabs,\n\t\t\t\t\t'quiz'        => llms_get_post( intval( $_GET['quiz_id'] ) ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Quiz table.\n\t\t} else {\n\n\t\t\t$table = new LLMS_Table_Quizzes();\n\t\t\t$table->get_results();\n\t\t\t$table->output_table_html();\n\n\t\t}\n\t}\n}\nreturn new LLMS_Admin_Reporting_Tab_Quizzes();\n"
  },
  {
    "path": "includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php",
    "content": "<?php\n/**\n * Sales Tab on Reporting Screen\n *\n * @package LifterLMS/Admin/Reporting/Tabs/Classes\n *\n * @since 3.2.0\n * @version 3.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Reporting_Tab_Sales class\n *\n * @since 3.2.0\n */\nclass LLMS_Admin_Reporting_Tab_Sales {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'llms_reporting_after_nav', array( $this, 'output_filters' ), 10, 1 );\n\t\tadd_action( 'llms_reporting_content_sales', array( $this, 'output' ) );\n\t}\n\n\tpublic static function get_filter_data() {\n\n\t\t$data = array();\n\n\t\t$data['current_tab'] = LLMS_Admin_Reporting::get_current_tab();\n\n\t\t$data['current_range'] = LLMS_Admin_Reporting::get_current_range();\n\n\t\t$data['current_students'] = LLMS_Admin_Reporting::get_current_students();\n\n\t\t$data['current_courses'] = LLMS_Admin_Reporting::get_current_courses();\n\n\t\t$data['current_memberships'] = LLMS_Admin_Reporting::get_current_memberships();\n\n\t\t$data['dates']      = LLMS_Admin_Reporting::get_dates( $data['current_range'] );\n\t\t$data['date_start'] = $data['dates']['start'];\n\t\t$data['date_end']   = $data['dates']['end'];\n\n\t\treturn $data;\n\t}\n\n\t/**\n\t * Get an array of ajax widgets to load on page load\n\t *\n\t * @return   array\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function get_widget_data() {\n\t\treturn apply_filters(\n\t\t\t'llms_reporting_tab_sales_widgets',\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'sales'    => array(\n\t\t\t\t\t\t'title'   => __( '# of New Sales', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of new non-refunded orders placed within this period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'sold'     => array(\n\t\t\t\t\t\t'title'   => __( 'Net Revenue', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Total of all successful transactions during this period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'refunds'  => array(\n\t\t\t\t\t\t'title'   => __( '# of Refunds', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of orders refunded during this period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'refunded' => array(\n\t\t\t\t\t\t'title'   => __( 'Amount Refunded', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Total of all transactions refunded during this period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'coupons'      => array(\n\t\t\t\t\t\t'title'   => __( '# of Coupons Used', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of orders completed using coupons during this period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'discounts'    => array(\n\t\t\t\t\t\t'title'   => __( 'Amount of Coupons', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Total amount of coupons used during this period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'transactions' => array(\n\t\t\t\t\t\t'title'   => __( '# of Transactions', 'lifterlms' ),\n\t\t\t\t\t\t'cols'    => '1-4',\n\t\t\t\t\t\t'content' => __( 'loading...', 'lifterlms' ),\n\t\t\t\t\t\t'info'    => __( 'Number of transactions within this period', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Output the template for the sales tab\n\t *\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function output() {\n\n\t\tllms_get_template(\n\t\t\t'admin/reporting/tabs/widgets.php',\n\t\t\tarray(\n\t\t\t\t'json'        => json_encode( self::get_filter_data() ),\n\t\t\t\t'widget_data' => $this->get_widget_data(),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Output filters navigation\n\t *\n\t * @return   void\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function output_filters( $tab ) {\n\n\t\tif ( 'sales' === $tab ) {\n\n\t\t\tllms_get_template( 'admin/reporting/nav-filters.php', self::get_filter_data() );\n\n\t\t}\n\t}\n}\nreturn new LLMS_Admin_Reporting_Tab_Sales();\n"
  },
  {
    "path": "includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php",
    "content": "<?php\n/**\n * LLMS_Admin_Reporting_Tab_Students class file\n *\n * @package LifterLMS/Admin/Reporting/Tabs/Classes\n *\n * @since 3.2.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Students Tab on Reporting Screen\n *\n * @since 3.2.0\n */\nclass LLMS_Admin_Reporting_Tab_Students {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'llms_reporting_content_students', array( $this, 'output' ) );\n\t\tadd_action( 'llms_reporting_student_tab_breadcrumbs', array( $this, 'breadcrumbs' ) );\n\t}\n\n\t/**\n\t * Add breadcrumb links to the tab depending on current view\n\t *\n\t * @since 3.2.0\n\t * @since 3.35.0 Sanitize input data.\n\t *\n\t * @return void\n\t */\n\tpublic function breadcrumbs() {\n\n\t\t$links = array();\n\n\t\t// Single student.\n\t\tif ( isset( $_GET['student_id'] ) ) {\n\t\t\t$student = new LLMS_Student( absint( $_GET['student_id'] ) );\n\t\t\t$links[ LLMS_Admin_Reporting::get_stab_url( 'information' ) ] = $student->get_name();\n\n\t\t\tif ( isset( $_GET['stab'] ) && 'courses' === $_GET['stab'] ) {\n\t\t\t\t$links[ LLMS_Admin_Reporting::get_stab_url( 'courses' ) ] = __( 'All Courses', 'lifterlms' );\n\n\t\t\t\tif ( isset( $_GET['course_id'] ) ) {\n\n\t\t\t\t\t$course_id     = llms_filter_input( INPUT_GET, 'course_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t\t\t$student_id    = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t\t\t$url           = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'stab'       => 'courses',\n\t\t\t\t\t\t\t'student_id' => $student_id,\n\t\t\t\t\t\t\t'course_id'  => $course_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$links[ $url ] = get_the_title( $course_id );\n\n\t\t\t\t\tif ( isset( $_GET['quiz_id'] ) ) {\n\t\t\t\t\t\t$quiz_id       = llms_filter_input( INPUT_GET, 'quiz_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t\t\t\t$url           = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'stab'       => 'courses',\n\t\t\t\t\t\t\t\t'student_id' => $student_id,\n\t\t\t\t\t\t\t\t'course_id'  => $course_id,\n\t\t\t\t\t\t\t\t'quiz_id'    => $quiz_id,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t\t$links[ $url ] = get_the_title( $quiz_id );\n\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tforeach ( $links as $url => $title ) {\n\n\t\t\techo '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $title ) . '</a>';\n\n\t\t}\n\t}\n\n\t/**\n\t * Output HTML for the current view within the students tab\n\t *\n\t * @since 3.2.0\n\t * @since 4.20.0 Added a report permission check and a user existence check.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t// Single student.\n\t\tif ( isset( $_GET['student_id'] ) ) {\n\n\t\t\t$student_id = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t\tif ( ! llms_current_user_can( 'view_lifterlms_reports', $student_id ) ) {\n\t\t\t\twp_die( esc_html__( \"You do not have permission to access this student's reports\", 'lifterlms' ) );\n\t\t\t}\n\t\t\t$student = llms_get_student( $student_id );\n\t\t\tif ( ! $student ) {\n\t\t\t\twp_die( esc_html__( \"This student doesn't exist.\", 'lifterlms' ) );\n\t\t\t}\n\n\t\t\t$tabs = apply_filters(\n\t\t\t\t'llms_reporting_tab_student_tabs',\n\t\t\t\tarray(\n\t\t\t\t\t'information'            => __( 'Information', 'lifterlms' ),\n\t\t\t\t\t'courses'                => __( 'Courses', 'lifterlms' ),\n\t\t\t\t\t'memberships'            => __( 'Memberships', 'lifterlms' ),\n\t\t\t\t\t'achievements'           => __( 'Achievements', 'lifterlms' ),\n\t\t\t\t\t'certificates'           => __( 'Certificates', 'lifterlms' ),\n\t\t\t\t\t'quiz_attempts'          => __( 'Quiz Attempts', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/students/student.php',\n\t\t\t\tarray(\n\t\t\t\t\t'current_tab' => isset( $_GET['stab'] ) ? esc_attr( llms_filter_input_sanitize_string( INPUT_GET, 'stab' ) ) : 'information',\n\t\t\t\t\t'tabs'        => $tabs,\n\t\t\t\t\t'student'     => $student,\n\t\t\t\t)\n\t\t\t);\n\n\t\t} else {\n\n\t\t\tllms_get_template( 'admin/reporting/tabs/students/students.php' );\n\n\t\t}\n\t}\n}\nreturn new LLMS_Admin_Reporting_Tab_Students();\n"
  },
  {
    "path": "includes/admin/reporting/tabs/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.ajax.php",
    "content": "<?php\n/**\n * Register WordPress AJAX methods for Analytics Widgets\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Analytics_Widget_Ajax class\n *\n * @since 3.0.0\n * @since 3.35.0 Sanitize `$_REQUEST` data.\n */\nclass LLMS_Analytics_Widget_Ajax {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.0.0\n\t * @since 3.16.8 Unknown.\n\t * @since 3.35.0 Sanitize `$_REQUEST` data.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t * @since 7.3.0 Ajax calls are now handled by `LLMS_Analytics_Widget_Ajax::handle()` method.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Only proceed if we're doing ajax.\n\t\tif ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX || ! isset( $_REQUEST['action'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$methods = array(\n\n\t\t\t// Sales.\n\t\t\t'coupons',\n\t\t\t'discounts',\n\t\t\t'refunded',\n\t\t\t'refunds',\n\t\t\t'revenue',\n\t\t\t'sales',\n\t\t\t'sold',\n\n\t\t\t// Enrollments.\n\t\t\t'enrollments',\n\t\t\t'registrations',\n\t\t\t'lessoncompletions',\n\t\t\t'coursecompletions',\n\t\t);\n\n\t\t$method = str_replace( 'llms_widget_', '', sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) );\n\n\t\t$file = LLMS_PLUGIN_DIR . 'includes/admin/reporting/widgets/class.llms.analytics.widget.' . $method . '.php';\n\n\t\tif ( file_exists( $file ) ) {\n\t\t\tadd_action( 'wp_ajax_llms_widget_' . $method, array( __CLASS__, 'handle' ) );\n\t\t}\n\t}\n\n\t/**\n\t * Handles the AJAX request.\n\t *\n\t * @since 7.3.0\n\t *\n\t * @return void\n\t */\n\tpublic static function handle() {\n\n\t\t// Make sure we are getting a valid AJAX request.\n\t\tcheck_ajax_referer( LLMS_Ajax::NONCE );\n\n\t\t$method = str_replace(\n\t\t\t'llms_widget_',\n\t\t\t'',\n\t\t\tsanitize_text_field( wp_unslash( $_REQUEST['action'] ?? '' ) )\n\t\t);\n\t\t$class  = 'LLMS_Analytics_' . ucwords( $method ) . '_Widget';\n\n\t\tif ( ! class_exists( $class ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$widget           = new $class();\n\t\t$can_be_processed = $widget->can_be_processed();\n\n\t\tif ( is_wp_error( $can_be_processed ) ) {\n\t\t\twp_send_json_error( $can_be_processed );\n\t\t\twp_die();\n\t\t}\n\n\t\t$widget->output();\n\n\t}\n\n}\n\nreturn new LLMS_Analytics_Widget_Ajax();\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.coupons.php",
    "content": "<?php\n/**\n * Coupons analytics widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.18.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Coupons analytics widget class\n *\n * Locates number of active / completed orders from a given date range\n * by a given group of students.\n *\n * @since 3.0.0\n * @since 3.18.0 Unknown.\n */\nclass LLMS_Analytics_Coupons_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\t/**\n\t * Retrieve data for chart\n\t *\n\t * @return   array\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count',\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'coupons',\n\t\t\t\t'label' => __( '# of Coupons Used', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Setup the query\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$this->set_order_data_query(\n\t\t\tarray(\n\t\t\t\t'query_function' => 'get_results',\n\t\t\t\t'select'         => array(\n\t\t\t\t\t'orders.post_date AS date',\n\t\t\t\t),\n\t\t\t\t'joins'          => array(\n\t\t\t\t\t\"JOIN {$wpdb->postmeta} AS coupons ON orders.ID = coupons.post_id\",\n\t\t\t\t),\n\t\t\t\t'statuses'       => array(\n\t\t\t\t\t'llms-active',\n\t\t\t\t\t'llms-completed',\n\t\t\t\t),\n\t\t\t\t'wheres'         => array(\n\t\t\t\t\t\" AND coupons.meta_key = '_llms_coupon_used'\",\n\t\t\t\t\t\" AND coupons.meta_value = 'yes'\",\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Format the response\n\t *\n\t * @return   int\n\t * @since    3.0.0\n\t * @version  3.18.0\n\t */\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.coursecompletions.php",
    "content": "<?php\n/**\n * Course Completions analytics widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.5.0\n * @version 3.5.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Course Completions analytics widget class\n *\n * @since 3.5.0\n * @version 3.5.3\n */\nclass LLMS_Analytics_Coursecompletions_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count', // Type of field.\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'coursecompletions',\n\t\t\t\t'label' => __( '# of Courses Completed', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$dates = $this->get_posted_dates();\n\n\t\t$student_ids = '';\n\t\t$students    = $this->get_posted_students();\n\t\tif ( $students ) {\n\t\t\t$student_ids .= 'AND user_id IN ( ' . implode( ', ', $students ) . ' )';\n\t\t}\n\n\t\t$lesson_ids = '';\n\t\t$products   = $this->get_posted_posts();\n\n\t\tif ( $products ) {\n\t\t\t$lesson_ids .= 'AND post_id IN ( ' . implode( ', ', $products ) . ' )';\n\t\t}\n\n\t\t$this->query_function = 'get_results';\n\t\t$this->output_type    = OBJECT;\n\n\t\t$this->query = \"SELECT updated_date AS date\n\t\t\t\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta AS upm\n\t\t\t\t\t\tJOIN {$wpdb->posts} AS p ON p.ID = upm.post_id\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\t    upm.meta_key = '_is_complete'\n\t\t\t\t\t\t\tAND p.post_type = 'course'\n\t\t\t\t\t\t\tAND upm.meta_value = 'yes'\n\t\t\t\t\t\t\tAND upm.updated_date >= CAST( %s as DATETIME )\n\t\t\t\t\t\t    AND upm.updated_date < CAST( %s as DATETIME )\n\t\t\t\t\t\t\t{$student_ids}\n\t\t\t\t\t\t\t{$lesson_ids}\n\t\t\t\t\t\t;\";\n\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.discounts.php",
    "content": "<?php\n/**\n * Total amount of coupon discount savings\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Analytics_Discounts_Widget class\n *\n * Totals all coupon discounts applied to orders in the given filters.\n *\n * @since 3.0.0\n */\nclass LLMS_Analytics_Discounts_Widget extends LLMS_Analytics_Widget {\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$this->set_order_data_query(\n\t\t\tarray(\n\t\t\t\t'query_function' => 'get_var',\n\t\t\t\t'select'         => array(\n\t\t\t\t\t'SUM( cp_val.meta_value )',\n\t\t\t\t),\n\t\t\t\t'joins'          => array(\n\t\t\t\t\t\"JOIN {$wpdb->postmeta} AS cp ON orders.ID = cp.post_id\",\n\t\t\t\t\t\"JOIN {$wpdb->postmeta} AS cp_val ON orders.ID = cp_val.post_id\",\n\t\t\t\t),\n\t\t\t\t'statuses'       => array(\n\t\t\t\t\t'llms-active',\n\t\t\t\t\t'llms-completed',\n\t\t\t\t),\n\t\t\t\t'wheres'         => array(\n\t\t\t\t\t\" AND cp.meta_key = '_llms_coupon_used'\",\n\t\t\t\t\t\" AND cp.meta_value = 'yes'\",\n\t\t\t\t\t\" AND cp_val.meta_key = '_llms_coupon_value'\",\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn llms_price_raw( floatval( $this->get_results() ) );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.enrollments.php",
    "content": "<?php\n/**\n * Enrollments analytics widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Analytics_Enrollments_Widget class\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n */\nclass LLMS_Analytics_Enrollments_Widget extends LLMS_Analytics_Widget {\n\n\n\tpublic $charts = true;\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count', // Type of field.\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'enrollments',\n\t\t\t\t'label' => __( '# of Enrollments', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$dates = $this->get_posted_dates();\n\n\t\t$student_ids = '';\n\t\t$students    = $this->get_posted_students();\n\t\tif ( $students ) {\n\t\t\t$student_ids .= 'AND user_id IN ( ' . implode( ', ', $students ) . ' )';\n\t\t}\n\n\t\t$product_ids = '';\n\t\t$products    = $this->get_posted_posts();\n\t\tif ( $products ) {\n\t\t\t$product_ids .= 'AND post_id IN ( ' . implode( ', ', $products ) . ' )';\n\t\t}\n\n\t\t$this->query_function = 'get_results';\n\t\t$this->output_type    = OBJECT;\n\n\t\t$this->query = \"SELECT updated_date AS date\n\t\t\t\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\t    meta_key = '_status'\n\t\t\t\t\t\t\tAND ( meta_value = 'Enrolled' OR meta_value = 'enrolled' )\n\t\t\t\t\t\t\tAND updated_date >= CAST( %s as DATETIME )\n\t\t\t\t\t\t    AND updated_date < CAST( %s as DATETIME )\n\t\t\t\t\t\t\t{$student_ids}\n\t\t\t\t\t\t\t{$product_ids}\n\t\t\t\t\t\t;\";\n\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.lessoncompletions.php",
    "content": "<?php\n/**\n * Lesson Completions analytics widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.5.0\n * @version 3.5.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Analytics_Lessoncompletions_Widget class\n *\n * @since 3.5.0\n * @since 3.5.3 Unknown.\n */\nclass LLMS_Analytics_Lessoncompletions_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count', // Type of field.\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'lessoncompletions',\n\t\t\t\t'label' => __( '# of Lessons Completed', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an array of lesson ids for all the products in the current filter\n\t *\n\t * @param    array $products  array of product ids\n\t * @return   array\n\t * @since    3.5.0\n\t * @version  3.5.0\n\t */\n\tprivate function get_lesson_ids( $products ) {\n\n\t\t$lessons = array();\n\n\t\t// Loop through all products.\n\t\tforeach ( $products as $product ) {\n\n\t\t\t// Ignore the memberships.\n\t\t\tif ( 'llms_membership' === get_post_type( $product ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Get the course.\n\t\t\t$course  = llms_get_post( $product );\n\t\t\t$lessons = array_merge( $course->get_lessons( 'ids' ) );\n\n\t\t}\n\n\t\treturn $lessons;\n\t}\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$dates = $this->get_posted_dates();\n\n\t\t$student_ids = '';\n\t\t$students    = $this->get_posted_students();\n\t\tif ( $students ) {\n\t\t\t$student_ids .= 'AND user_id IN ( ' . implode( ', ', $students ) . ' )';\n\t\t}\n\n\t\t$lesson_ids = '';\n\t\t$products   = $this->get_posted_posts();\n\n\t\tif ( $products ) {\n\t\t\t$lesson_ids .= 'AND post_id IN ( ' . implode( ', ', $this->get_lesson_ids( $products ) ) . ' )';\n\t\t}\n\n\t\t$this->query_function = 'get_results';\n\t\t$this->output_type    = OBJECT;\n\n\t\t$this->query = \"SELECT updated_date AS date\n\t\t\t\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta AS upm\n\t\t\t\t\t\tJOIN {$wpdb->posts} AS p ON p.ID = upm.post_id\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\t    upm.meta_key = '_is_complete'\n\t\t\t\t\t\t\tAND p.post_type = 'lesson'\n\t\t\t\t\t\t\tAND upm.meta_value = 'yes'\n\t\t\t\t\t\t\tAND upm.updated_date >= CAST( %s as DATETIME )\n\t\t\t\t\t\t    AND upm.updated_date < CAST( %s as DATETIME )\n\t\t\t\t\t\t\t{$student_ids}\n\t\t\t\t\t\t\t{$lesson_ids}\n\t\t\t\t\t\t;\";\n\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.refunded.php",
    "content": "<?php\n/**\n * Refunded Amount Widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.36.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Refunded Amount Widget class\n *\n * Retrieves the total amount of all refunded transactions\n * according to active filters.\n *\n * @since 3.0.0\n * @since 3.36.3 In `format_response()` method avoid running `wp_list_pluck()` on non arrays.\n */\nclass LLMS_Analytics_Refunded_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'amount', // Type of field.\n\t\t\t'key'    => 'amount', // Key of result field to add when counting.\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'refunded',\n\t\t\t\t'label' => __( 'Amount Refunded', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$txn_meta_join  = '';\n\t\t$txn_meta_where = '';\n\t\t// create an \"IN\" clause that can be used for later in WHERE clauses.\n\t\tif ( $this->get_posted_students() || $this->get_posted_posts() ) {\n\n\t\t\t// get an array of order based on posted students & products.\n\t\t\t$this->set_order_data_query(\n\t\t\t\tarray(\n\t\t\t\t\t'date_range'     => false,\n\t\t\t\t\t'query_function' => 'get_col',\n\t\t\t\t\t'select'         => array(\n\t\t\t\t\t\t'orders.ID',\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$this->query();\n\t\t\t$order_ids = $this->get_results();\n\n\t\t\tif ( $order_ids ) {\n\n\t\t\t\t$txn_meta_join   = \"JOIN {$wpdb->postmeta} AS txn_meta ON txn_meta.post_id = txns.ID\";\n\t\t\t\t$txn_meta_where .= \" AND txn_meta.meta_key = '_llms_order_id'\";\n\t\t\t\t$txn_meta_where .= ' AND txn_meta.meta_value IN ( ' . implode( ', ', $order_ids ) . ' )';\n\t\t\t} else {\n\n\t\t\t\t$this->query_function = 'get_var';\n\t\t\t\t$this->query          = 'SELECT 0';\n\t\t\t\treturn;\n\n\t\t\t}\n\t\t}\n\n\t\t// date range will be used to get transactions between given dates.\n\t\t$dates            = $this->get_posted_dates();\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\n\t\t$this->query_function = 'get_results';\n\t\t$this->output_type    = OBJECT;\n\n\t\t$this->query = \"SELECT\n\t\t\t\t\t\t\t  txns.post_modified AS date\n\t\t\t\t\t\t\t, refund.meta_value AS amount\n\t\t\t\t\t\tFROM {$wpdb->posts} AS txns\n\t\t\t\t\t\t{$txn_meta_join}\n\t\t\t\t\t\tJOIN {$wpdb->postmeta} AS refund ON refund.post_id = txns.ID\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t        ( txns.post_status = 'llms-txn-succeeded' OR txns.post_status = 'llms-txn-refunded' )\n\t\t\t\t\t\t    AND txns.post_type = 'llms_transaction'\n\t\t\t\t\t\t\tAND txns.post_modified >= CAST( %s as DATETIME )\n\t\t\t\t\t\t\tAND txns.post_modified < CAST( %s as DATETIME )\n\t\t\t\t\t\t\tAND refund.meta_key = '_llms_refund_amount'\n\t\t\t\t\t\t\t{$txn_meta_where}\n\t\t\t\t\t\t\tORDER BY txns.post_modified ASC\n\t\t\t\t\t\t;\";\n\t}\n\n\t/**\n\t * Format response.\n\t *\n\t * @since unknown\n\t * @since 3.36.3 Avoid running `wp_list_pluck()` on non arrays.\n\t */\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\t$results = $this->get_results();\n\t\t\treturn llms_price_raw( floatval( is_array( $results ) ? array_sum( wp_list_pluck( $results, 'amount' ) ) : $results ) );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.refunds.php",
    "content": "<?php\n/**\n * Refunds analytics widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Refunds analytics widget class\n *\n * Locates number of refunded orders from a given date range\n * by a given group of students.\n *\n * Uses \"post_modified\" rather than \"post_date\" for date query.\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since  3.0.0\n * @version 3.0.0\n */\nclass LLMS_Analytics_Refunds_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count',\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'refunds',\n\t\t\t\t'label' => __( '# of Refunds', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\t$this->set_order_data_query(\n\t\t\tarray(\n\t\t\t\t'date_field'     => 'post_modified',\n\t\t\t\t'query_function' => 'get_results',\n\t\t\t\t'select'         => array(\n\t\t\t\t\t'orders.post_modified AS date',\n\t\t\t\t),\n\t\t\t\t'statuses'       => array(\n\t\t\t\t\t'llms-refunded',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.registrations.php",
    "content": "<?php\n/**\n * Registrations analytics widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.5.0\n * @version 3.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Registrations analytics widget class\n *\n * @since 3.5.0\n */\nclass LLMS_Analytics_Registrations_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count', // Type of field.\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'registrations',\n\t\t\t\t'label' => __( '# of Registrations', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$dates = $this->get_posted_dates();\n\n\t\t$student_ids = '';\n\t\t$students    = $this->get_posted_students();\n\t\tif ( $students ) {\n\t\t\t$student_ids .= 'AND ID IN ( ' . implode( ', ', $students ) . ' )';\n\t\t}\n\n\t\t$this->query_function = 'get_results';\n\t\t$this->output_type    = OBJECT;\n\n\t\t$this->query = \"SELECT user_registered AS date\n\t\t\t\t\t\tFROM {$wpdb->users}\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\tuser_registered >= CAST( %s as DATETIME )\n\t\t\t\t\t\t    AND user_registered < CAST( %s as DATETIME )\n\t\t\t\t\t\t\t{$student_ids}\n\t\t\t\t\t\t;\";\n\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.revenue.php",
    "content": "<?php\n/**\n * Revenue widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Analytics_Revenue_Widget class\n *\n * Retrieves the total amount of all succeeded transactions\n * according to active filters.\n *\n * @since 3.0.0\n */\nclass LLMS_Analytics_Revenue_Widget extends LLMS_Analytics_Widget {\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$txn_meta_join  = '';\n\t\t$txn_meta_where = '';\n\t\t// Create an \"IN\" clause that can be used for later in WHERE clauses.\n\t\tif ( $this->get_posted_students() || $this->get_posted_posts() ) {\n\n\t\t\t// Get an array of order based on posted students & products.\n\t\t\t$this->set_order_data_query(\n\t\t\t\tarray(\n\t\t\t\t\t'date_range'     => false,\n\t\t\t\t\t'query_function' => 'get_col',\n\t\t\t\t\t'select'         => array(\n\t\t\t\t\t\t'orders.ID',\n\t\t\t\t\t),\n\t\t\t\t\t'statuses'       => array(\n\t\t\t\t\t\t'llms-active',\n\t\t\t\t\t\t'llms-completed',\n\t\t\t\t\t\t'llms-refunded',\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$this->query();\n\t\t\t$order_ids = $this->get_results();\n\n\t\t\tif ( $order_ids ) {\n\n\t\t\t\t$txn_meta_join   = \"JOIN {$wpdb->postmeta} AS txn_meta ON txn_meta.post_id = txns.ID\";\n\t\t\t\t$txn_meta_where .= \" AND txn_meta.meta_key = '_llms_order_id'\";\n\t\t\t\t$txn_meta_where .= ' AND txn_meta.meta_value IN ( ' . implode( ', ', $order_ids ) . ' )';\n\t\t\t} else {\n\n\t\t\t\t$this->query_function = 'get_var';\n\t\t\t\t$this->query          = 'SELECT 0';\n\t\t\t\treturn;\n\n\t\t\t}\n\t\t}\n\n\t\t// Date range will be used to get transactions between given dates.\n\t\t$dates            = $this->get_posted_dates();\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\n\t\t$this->query_function = 'get_var';\n\n\t\t$this->query = \"SELECT\n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\tIFNULL( SUM( (\n\t\t\t\t\t\t\t\t\tSELECT price.meta_value\n\t\t\t\t\t\t\t\t\tFROM {$wpdb->postmeta} AS price\n\t\t\t\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\t\t\t\t  price.meta_key = '_llms_amount'\n\t\t\t\t\t\t\t\t\t  AND price.post_id IN( txns.ID )\n\t\t\t\t\t\t\t\t) ), 0 ) - IFNULL( SUM((\n\t\t\t\t\t\t\t\t\tSELECT refund.meta_value\n\t\t\t\t\t\t\t\t\tFROM {$wpdb->postmeta} AS refund\n\t\t\t\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\t\t\t\t  refund.meta_key = '_llms_refund_amount'\n\t\t\t\t\t\t\t\t\t  AND refund.post_id IN( txns.ID )\n\t\t\t\t\t\t\t\t) ), 0 )\n\t\t\t\t\t\t\t) AS revenue\n\t\t\t\t\t\tFROM {$wpdb->posts} AS txns\n\t\t\t\t\t\t{$txn_meta_join}\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t        ( txns.post_status = 'llms-txn-succeeded' OR txns.post_status = 'llms-txn-refunded' )\n\t\t\t\t\t\t    AND txns.post_type = 'llms_transaction'\n\t\t\t\t\t\t\tAND txns.post_date >= CAST( %s as DATETIME )\n\t\t\t\t\t\t\tAND txns.post_date < CAST( %s as DATETIME )\n\t\t\t\t\t\t\t{$txn_meta_where}\n\t\t\t\t\t\t;\";\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn llms_price_raw( floatval( $this->get_results() ) );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.sales.php",
    "content": "<?php\n/**\n * Sales analytics widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Analytics_Sales_Widget class\n *\n * Locates number of active / completed orders from a given date range\n * by a given group of students.\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n */\nclass LLMS_Analytics_Sales_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count',\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'sales',\n\t\t\t\t'label' => __( '# of New Sales', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\t$this->set_order_data_query(\n\t\t\tarray(\n\t\t\t\t'query_function' => 'get_results',\n\t\t\t\t'select'         => array(\n\t\t\t\t\t'orders.post_date AS date',\n\t\t\t\t),\n\t\t\t\t'statuses'       => array(\n\t\t\t\t\t'llms-active',\n\t\t\t\t\t'llms-completed',\n\t\t\t\t\t'llms-on-hold',\n\t\t\t\t\t'llms-pending-cancel',\n\t\t\t\t\t'llms-cancelled',\n\t\t\t\t\t'llms-expired',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.sold.php",
    "content": "<?php\n/**\n * Sold Amount Widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 3.0.0\n * @version 3.36.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Sold Amount Widget class\n *\n * Retrieves the total amount of all successful transactions\n * according to active filters.\n *\n * @since 3.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 3.36.3 In `format_response()` method avoid running `wp_list_pluck()` on non arrays.\n */\nclass LLMS_Analytics_Sold_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\t/**\n\t * temporary order ids\n\t *\n\t * @var array\n\t * @since 3.0.0\n\t */\n\tpublic $temp = array();\n\n\t/**\n\t * temporary query\n\t *\n\t * @since 3.0.0\n\t * @var array\n\t */\n\tpublic $temp_q = array();\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'amount', // Type of field.\n\t\t\t'key'    => 'amount', // Key of result field to add when counting.\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'sold',\n\t\t\t\t'label' => __( 'Net Revenue', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$txn_meta_join  = '';\n\t\t$txn_meta_where = '';\n\t\t// Create an \"IN\" clause that can be used for later in WHERE clauses.\n\t\tif ( $this->get_posted_students() || $this->get_posted_posts() ) {\n\n\t\t\t// Get an array of order based on posted students & products.\n\t\t\t$this->set_order_data_query(\n\t\t\t\tarray(\n\t\t\t\t\t'date_range'     => false,\n\t\t\t\t\t'query_function' => 'get_col',\n\t\t\t\t\t'select'         => array(\n\t\t\t\t\t\t'orders.ID',\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$this->query();\n\t\t\t$order_ids = $this->get_results();\n\n\t\t\t$this->temp_q = $wpdb->last_query;\n\t\t\t$this->temp   = $order_ids;\n\n\t\t\tif ( $order_ids ) {\n\t\t\t\t$txn_meta_join   = \"JOIN {$wpdb->postmeta} AS txn_meta ON txn_meta.post_id = txns.ID\";\n\t\t\t\t$txn_meta_where .= \" AND txn_meta.meta_key = '_llms_order_id'\";\n\t\t\t\t$txn_meta_where .= ' AND txn_meta.meta_value IN ( ' . implode( ', ', $order_ids ) . ' )';\n\t\t\t} else {\n\n\t\t\t\t$this->query_function = 'get_var';\n\t\t\t\t$this->query          = 'SELECT 0';\n\t\t\t\treturn;\n\n\t\t\t}\n\t\t}\n\n\t\t// Date range will be used to get transactions between given dates.\n\t\t$dates            = $this->get_posted_dates();\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\n\t\t$this->query_function = 'get_results';\n\t\t$this->output_type    = OBJECT;\n\n\t\t$this->query = \"SELECT txns.post_date AS date,\n       \t\t\t\t\t(sales.meta_value - COALESCE(refunds.meta_value, 0)) AS amount\n\t\t\t\t\t\tFROM {$wpdb->posts} AS txns\n\t\t\t\t\t\t{$txn_meta_join}\n\t\t\t\t\t\tJOIN {$wpdb->postmeta} AS sales ON sales.post_id = txns.ID AND sales.meta_key = '_llms_amount'\n\t\t\t\t\t\tLEFT JOIN {$wpdb->postmeta} AS refunds ON refunds.post_id = txns.ID AND refunds.meta_key = '_llms_refund_amount'\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t        ( txns.post_status = 'llms-txn-succeeded' OR txns.post_status = 'llms-txn-refunded' )\n\t\t\t\t\t\t    AND txns.post_type = 'llms_transaction'\n\t\t\t\t\t\t\tAND txns.post_date >= CAST( %s as DATETIME )\n\t\t\t\t\t\t\tAND txns.post_date < CAST( %s as DATETIME )\n\t\t\t\t\t\t\t{$txn_meta_where}\n\t\t\t\t\t\t\tORDER BY txns.post_date ASC\n\t\t\t\t\t\t;\";\n\t}\n\n\t/**\n\t * Format response.\n\t *\n\t * @since unknown\n\t * @since 3.36.3 Avoid running `wp_list_pluck()` on non arrays.\n\t * @since 8.0.3 Using aggregate sum of net sales rather than summing up the txn records.\n\t */\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\t\t\t$results = $this->get_results();\n\t\t\treturn llms_price_raw( floatval( is_array( $results ) ? array_sum( wp_list_pluck( $results, 'amount' ) ) : $results ) );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/class.llms.analytics.widget.transactions.php",
    "content": "<?php\n/**\n * Transaction Count Widget\n *\n * @package LifterLMS/Admin/Reporting/Widgets/Classes\n *\n * @since 8.0.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Transaction Count Widget class\n *\n * Locates number of transactions from a given date range\n * by a given group of students.\n */\nclass LLMS_Analytics_Transactions_Widget extends LLMS_Analytics_Widget {\n\n\tpublic $charts = true;\n\n\t/**\n\t * temporary order ids\n\t *\n\t * @var array\n\t * @since 3.0.0\n\t */\n\tpublic $temp = array();\n\n\t/**\n\t * temporary query\n\t *\n\t * @since 3.0.0\n\t * @var array\n\t */\n\tpublic $temp_q = array();\n\n\tprotected function get_chart_data() {\n\t\treturn array(\n\t\t\t'type'   => 'count',\n\t\t\t'header' => array(\n\t\t\t\t'id'    => 'sold',\n\t\t\t\t'label' => __( '# of Transactions', 'lifterlms' ),\n\t\t\t\t'type'  => 'number',\n\t\t\t),\n\t\t);\n\t}\n\n\tpublic function set_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$txn_meta_join  = '';\n\t\t$txn_meta_where = '';\n\t\t// Create an \"IN\" clause that can be used for later in WHERE clauses.\n\t\tif ( $this->get_posted_students() || $this->get_posted_posts() ) {\n\n\t\t\t// Get an array of order based on posted students & products.\n\t\t\t$this->set_order_data_query(\n\t\t\t\tarray(\n\t\t\t\t\t'date_range'     => false,\n\t\t\t\t\t'query_function' => 'get_col',\n\t\t\t\t\t'select'         => array(\n\t\t\t\t\t\t'orders.ID',\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$this->query();\n\t\t\t$order_ids = $this->get_results();\n\n\t\t\t$this->temp_q = $wpdb->last_query;\n\t\t\t$this->temp   = $order_ids;\n\n\t\t\tif ( $order_ids ) {\n\t\t\t\t$txn_meta_join   = \"JOIN {$wpdb->postmeta} AS txn_meta ON txn_meta.post_id = txns.ID\";\n\t\t\t\t$txn_meta_where .= \" AND txn_meta.meta_key = '_llms_order_id'\";\n\t\t\t\t$txn_meta_where .= ' AND txn_meta.meta_value IN ( ' . implode( ', ', array_map( 'absint', $order_ids ) ) . ' )';\n\t\t\t} else {\n\n\t\t\t\t$this->query_function = 'get_var';\n\t\t\t\t$this->query          = 'SELECT 0';\n\t\t\t\treturn;\n\n\t\t\t}\n\t\t}\n\n\t\t// Date range will be used to get transactions between given dates.\n\t\t$dates            = $this->get_posted_dates();\n\t\t$this->query_vars = array(\n\t\t\t$this->format_date( $dates['start'], 'start' ),\n\t\t\t$this->format_date( $dates['end'], 'end' ),\n\t\t);\n\n\t\t$this->query_function = 'get_results';\n\t\t$this->output_type    = OBJECT;\n\n\t\t$this->query = \"SELECT\n\t\t\t\t\t\t\t  txns.post_date as date\n\t\t\t\t\t\tFROM {$wpdb->posts} AS txns\n\t\t\t\t\t\t{$txn_meta_join}\n\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t        ( txns.post_status = 'llms-txn-succeeded' OR txns.post_status = 'llms-txn-refunded' )\n\t\t\t\t\t\t    AND txns.post_type = 'llms_transaction'\n\t\t\t\t\t\t\tAND txns.post_date >= CAST( %s as DATETIME )\n\t\t\t\t\t\t\tAND txns.post_date < CAST( %s as DATETIME )\n\t\t\t\t\t\t\t{$txn_meta_where}\n\t\t\t\t\t\t\tORDER BY txns.post_modified ASC\n\t\t\t\t\t\t;\";\n\t}\n\n\tprotected function format_response() {\n\n\t\tif ( ! $this->is_error() ) {\n\n\t\t\treturn count( $this->get_results() );\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/reporting/widgets/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.accounts.php",
    "content": "<?php\n/**\n * Admin Settings Page, Accounts Tab\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page, Accounts Tab class\n *\n * @since 1.0.0\n * @since 3.30.3 Fixed spelling errors.\n * @since 3.37.3 Renamed setting field IDs to be unique.\n *               Removed redundant functions defined in the `LLMS_Settings_Page` class.\n *               Removed constructor and added `get_label()` method to be compatible with changes in `LLMS_Settings_Page`.\n * @since 3.37.4 Revert $id to \"account\".\n */\nclass LLMS_Settings_Accounts extends LLMS_Settings_Page {\n\n\t/**\n\t * Settings identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id = 'account';\n\n\t/**\n\t * Should permalinks be flushed on save?\n\t *\n\t * @var boolean\n\t */\n\tprotected $flush = true;\n\n\t/**\n\t * Get settings array\n\t *\n\t * @since 1.0.0\n\t * @since 3.30.3 Fixed spelling errors.\n\t * @since 3.37.3 Renamed duplicate field id for section close (`user_info_field_options` to `user_info_field_options_end`)\n\t * @since 5.0.0 Removed field display settings.\n\t *              Reorganized open registration setting.\n\t *              Renamed \"User Information Options\" to \"User Privacy Options\".\n\t * @since 5.6.0 Added options to disable concurrent logins.\n\t * @since 7.5.0 Added settings for favorites endpoint.\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\t\t$account_settings = array(\n\t\t\tarray(\n\t\t\t\t'class' => 'top',\n\t\t\t\t'id'    => 'course_account_options',\n\t\t\t\t'type'  => 'sectionstart',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'    => 'account_page_options_start',\n\t\t\t\t'title' => __( 'Student Dashboard', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'             => __( 'Dashboard Page', 'lifterlms' ),\n\t\t\t\t'desc'              => __( 'Page where students can view and manage their current enrollments, earned certificates and achievements, account information, and purchase history.', 'lifterlms' ) . ' ' . sprintf( __( 'Requires the %1$s[lifterlms_my_account]%2$s shortcode or the \"My Account\" block.', 'lifterlms' ), '<code>', '</code>' ),\n\t\t\t\t'id'                => 'lifterlms_myaccount_page_id',\n\t\t\t\t'default'           => '',\n\t\t\t\t'desc_tip'          => true,\n\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t'type'              => 'select',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-post-type'   => 'page',\n\t\t\t\t\t'data-placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'lifterlms_myaccount_page_id', '' ) ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Courses Sorting', 'lifterlms' ),\n\t\t\t\t'default' => 'order,ASC',\n\t\t\t\t'desc'    => __( 'Determines the order of the courses in-progress listed on the student dashboard.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_myaccount_courses_in_progress_sorting',\n\t\t\t\t'type'    => 'select',\n\t\t\t\t'options' => array(\n\t\t\t\t\t'title,ASC'  => __( 'Course Title (A to Z)', 'lifterlms' ),\n\t\t\t\t\t'title,DESC' => __( 'Course Title (Z to A)', 'lifterlms' ),\n\t\t\t\t\t'date,DESC'  => __( 'Enrollment Date (Most Recent to Least Recent)', 'lifterlms' ),\n\t\t\t\t\t'order,ASC'  => __( 'Order (Low to High)', 'lifterlms' ),\n\t\t\t\t\t'order,DESC' => __( 'Order (High to Low)', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'default' => 'no',\n\t\t\t\t'desc'    => sprintf(\n\t\t\t\t\t// Translators: %1$s = opening anchor tag; %2$s = closing anchor tag.\n\t\t\t\t\t__( 'Enable new user registration on the Student Dashboard. %1$sLearn More%2$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"https://lifterlms.com/docs/open-registration/\" target=\"_blank\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t),\n\t\t\t\t'id'      => 'lifterlms_enable_myaccount_registration',\n\t\t\t\t'title'   => __( 'Open Registration', 'lifterlms' ),\n\t\t\t\t'type'    => 'checkbox',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'default'           => 'no',\n\t\t\t\t'desc'              => __( 'Only allow the most recent login for each user account.', 'lifterlms' ),\n\t\t\t\t'id'                => 'lifterlms_prevent_concurrent_logins',\n\t\t\t\t'title'             => __( 'Prevent concurrent logins', 'lifterlms' ),\n\t\t\t\t'type'              => 'checkbox',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'class'         => 'llms-conditional-controller',\n\t\t\t\t\t'data-controls' => '#lifterlms_prevent_concurrent_logins_roles',\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'class'             => 'llms-select2',\n\t\t\t\t'default'           => array( 'student' ),\n\t\t\t\t'desc'              => __( 'Prevent concurrent logins for users with the selected user roles.', 'lifterlms' ),\n\t\t\t\t'id'                => 'lifterlms_prevent_concurrent_logins_roles',\n\t\t\t\t'options'           => LLMS_Roles::get_all_role_names(),\n\t\t\t\t'title'             => '',\n\t\t\t\t'type'              => 'multiselect',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-placeholder' => __( 'Select user roles', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'   => 'course_account_options_end',\n\t\t\t\t'type' => 'sectionend',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'class' => 'top',\n\t\t\t\t'id'    => 'course_account_endpoint_options_start',\n\t\t\t\t'type'  => 'sectionstart',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'    => 'account_page_endpoint_options_title',\n\t\t\t\t'title' => __( 'Student Dashboard Endpoints', 'lifterlms' ),\n\t\t\t\t'desc'  => __( 'Each endpoint allows students to view more information or manage parts of their account. Each endpoint should be unique, URL-safe, and can be left blank to disable the endpoint completely.', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'View Grades', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Student grade and progress reporting', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_grades_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'my-grades',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'View Courses', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'List of all the student\\'s courses', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_courses_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'my-courses',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'View Memberships', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'List of all the student\\'s memberships', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_memberships_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'my-memberships',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'View Achievements', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'List of all the student\\'s achievements', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_achievements_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'my-achievements',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'View Certificates', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'List of all the student\\'s certificates', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_certificates_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'my-certificates',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Notifications', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'View Notifications and adjust notification settings', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_notifications_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'notifications',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Edit Account', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Edit Account page', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_edit_account_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'edit-account',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Lost Password', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Lost Password page', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_lost_password_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'lost-password',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Redeem Vouchers', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Redeem vouchers page', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_redeem_vouchers_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'redeem-voucher',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Orders History', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Students can review order history on this page', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_orders_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'orders',\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'   => 'course_account_endpoint_options_end',\n\t\t\t\t'type' => 'sectionend',\n\t\t\t),\n\n\t\t\t// Start user info fields options.\n\t\t\tarray(\n\t\t\t\t'id'   => 'user_info_field_options',\n\t\t\t\t'type' => 'sectionstart',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title' => __( 'User Information & Privacy Options', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'id'    => 'user_info_field_options_title',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title' => __( 'User Information Field Settings', 'lifterlms' ),\n\t\t\t\t'type'  => 'subtitle',\n\t\t\t\t'desc'  => __( 'Since version 5.0, all user information fields are customized using the form editor.', 'lifterlms' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t'value' => '<p><a class=\"button-primary\" href=\"' . admin_url( 'edit.php?post_type=llms_form' ) . '\">' . __( 'Edit Forms', 'lifterlms' ) . '</a></p>',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title' => __( 'Terms and Conditions', 'lifterlms' ),\n\t\t\t\t'type'  => 'subtitle',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload'          => false,\n\t\t\t\t'default'           => 'no',\n\t\t\t\t'id'                => 'lifterlms_registration_require_agree_to_terms',\n\t\t\t\t'desc'              => __( 'When enabled users must agree to your site\\'s Terms and Conditions to register for an account.', 'lifterlms' ),\n\t\t\t\t'title'             => __( 'Enable / Disable', 'lifterlms' ),\n\t\t\t\t'type'              => 'checkbox',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'class'         => 'llms-conditional-controller',\n\t\t\t\t\t'data-controls' => '#lifterlms_terms_page_id,#llms_terms_notice',\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload'          => false,\n\t\t\t\t'desc'              => __( 'Select a page where your site\\'s Terms and Conditions are described.', 'lifterlms' ),\n\t\t\t\t'id'                => 'lifterlms_terms_page_id',\n\t\t\t\t'default'           => '',\n\t\t\t\t'desc_tip'          => true,\n\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t'title'             => __( 'Terms and Conditions Page', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-post-type'   => 'page',\n\t\t\t\t\t'data-placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'lifterlms_terms_page_id', '' ) ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => llms_get_terms_notice(),\n\t\t\t\t'id'       => 'llms_terms_notice',\n\t\t\t\t'desc'     => __( 'Customize the text used to display the Terms and Conditions checkbox that students must accept.', 'lifterlms' ),\n\t\t\t\t'title'    => __( 'Terms and Conditions Notice', 'lifterlms' ),\n\t\t\t\t'type'     => 'textarea',\n\t\t\t\t'value'    => llms_get_terms_notice(),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title' => __( 'Privacy Policy', 'lifterlms' ),\n\t\t\t\t'type'  => 'subtitle',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload'          => false,\n\t\t\t\t'desc'              => sprintf(\n\t\t\t\t\t__( 'Select a page where your site\\'s Privacy Policy is described. See %1$sWordPress Privacy Settings%2$s for more information', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . esc_url( admin_url( 'privacy.php' ) ) . '\">',\n\t\t\t\t\t'</a>'\n\t\t\t\t),\n\t\t\t\t'id'                => 'wp_page_for_privacy_policy',\n\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t'title'             => __( 'Privacy Policy Page', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-post-type'   => 'page',\n\t\t\t\t\t'data-placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'wp_page_for_privacy_policy' ) ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => llms_get_privacy_notice(),\n\t\t\t\t'id'       => 'llms_privacy_notice',\n\t\t\t\t'desc'     => __( 'Optionally display a privacy policy notice during registration and checkout.', 'lifterlms' ),\n\t\t\t\t'title'    => __( 'Privacy Policy Notice', 'lifterlms' ),\n\t\t\t\t'type'     => 'textarea',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title' => __( 'Account Erasure Requests', 'lifterlms' ),\n\t\t\t\t/* Translators: %$1s = opening anchor to account erasure screen; %2$s closing anchor */\n\t\t\t\t'desc'  => sprintf( __( 'Customize data retention during %1$saccount erasure requests%2$s.', 'lifterlms' ), '<a href=\"' . esc_url( admin_url( 'tools.php?page=remove_personal_data' ) ) . '\">', '</a>' ),\n\t\t\t\t'type'  => 'subtitle',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => 'no',\n\t\t\t\t'id'       => 'llms_erasure_request_removes_order_data',\n\t\t\t\t'desc'     => __( 'When enabled orders will be anonymized during a personal data erasure.', 'lifterlms' ),\n\t\t\t\t'title'    => __( 'Remove Order Data', 'lifterlms' ),\n\t\t\t\t'type'     => 'checkbox',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => 'no',\n\t\t\t\t'id'       => 'llms_erasure_request_removes_lms_data',\n\t\t\t\t'desc'     => __( 'When enabled all student data related to course and membership activities will be removed.', 'lifterlms' ),\n\t\t\t\t'title'    => __( 'Remove Student LMS Data', 'lifterlms' ),\n\t\t\t\t'type'     => 'checkbox',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'   => 'user_info_field_options_end',\n\t\t\t\t'type' => 'sectionend',\n\t\t\t),\n\n\t\t);\n\n\t\tif ( llms_is_favorites_enabled() ) {\n\t\t\tarray_splice(\n\t\t\t\t$account_settings,\n\t\t\t\t15,\n\t\t\t\t0,\n\t\t\t\tarray(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'title'    => __( 'View Favorites', 'lifterlms' ),\n\t\t\t\t\t\t'desc'     => __( 'List of all the student\\'s favorites', 'lifterlms' ),\n\t\t\t\t\t\t'id'       => 'lifterlms_myaccount_favorites_endpoint',\n\t\t\t\t\t\t'type'     => 'text',\n\t\t\t\t\t\t'default'  => 'my-favorites',\n\t\t\t\t\t\t'sanitize' => 'slug',\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * Filters the account settings.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the settings page.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array $account_settings The account page settings.\n\t\t */\n\t\treturn apply_filters( \"lifterlms_{$this->id}_settings\", $account_settings );\n\t}\n\n\t/**\n\t * Retrieve the page label.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return string\n\t */\n\tprotected function set_label() {\n\t\treturn __( 'Accounts', 'lifterlms' );\n\t}\n}\n\nreturn new LLMS_Settings_Accounts();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.checkout.php",
    "content": "<?php\n/**\n * Admin Settings Page, Checkout Tab\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 3.0.0\n * @version 3.35.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page, Checkout Tab class\n *\n * @since 3.0.0\n * @since 3.30.3 Fixed spelling errors.\n * @since 3.35.1 Verify nonce when saving.\n */\nclass LLMS_Settings_Checkout extends LLMS_Settings_Page {\n\n\t/**\n\t * Allow settings page to determine if a rewrite flush is required\n\t *\n\t * @var      boolean\n\t * @since    3.0.4\n\t * @version  3.10.0\n\t */\n\tprotected $flush = true;\n\n\t/**\n\t * Constructor\n\t * executes settings tab actions\n\t *\n\t * @since    3.0.4\n\t * @version  3.17.5\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id    = 'checkout';\n\t\t$this->label = __( 'Checkout', 'lifterlms' );\n\n\t\tadd_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );\n\t\tadd_action( 'lifterlms_sections_' . $this->id, array( $this, 'output_sections_nav' ) );\n\t\tadd_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'save' ) );\n\t}\n\n\t/**\n\t * Get HTML for the payment gateways table\n\t *\n\t * @return   string\n\t * @since    3.17.5\n\t * @version  3.17.5\n\t */\n\tpublic function get_gateway_table_html() {\n\n\t\t$gateways = llms()->payment_gateways()->get_payment_gateways();\n\n\t\tusort( $gateways, array( $this, 'sort_gateways' ) );\n\n\t\tob_start();\n\t\t?>\n\n\t\t<table class=\"llms-table zebra text-left size-large llms-gateway-table\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th class=\"sort\"></th>\n\t\t\t\t\t<th><?php esc_html_e( 'Gateway', 'lifterlms' ); ?></th>\n\t\t\t\t\t<th><?php esc_html_e( 'Gateway ID', 'lifterlms' ); ?></th>\n\t\t\t\t\t<th><?php esc_html_e( 'Enabled', 'lifterlms' ); ?></th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t<?php foreach ( $gateways as $gateway ) : ?>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"sort\">\n\t\t\t\t\t\t<i class=\"fa fa-bars llms-action-icon\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t<input type=\"hidden\" name=\"<?php echo esc_attr( $gateway->get_option_name( 'display_order' ) ); ?>\" value=\"<?php echo esc_attr( $gateway->get_display_order() ); ?>\">\n\t\t\t\t\t</td>\n\t\t\t\t\t<td><a href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-settings&tab=' . $this->id . '&section=' . $gateway->get_id() ) ); ?>\"><?php echo esc_html( $gateway->get_admin_title() ); ?></a></td>\n\t\t\t\t\t<td><?php echo esc_html( $gateway->get_id() ); ?></td>\n\t\t\t\t\t<td class=\"status\">\n\t\t\t\t\t\t<?php if ( $gateway->is_enabled() ) : ?>\n\t\t\t\t\t\t\t<span class=\"tip--bottom-right\" data-tip=\"<?php esc_attr_e( 'Enabled', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Enabled', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t\t<i class=\"fa fa-check-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t&ndash;\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t<?php endforeach; ?>\n\t\t\t</tbody>\n\t\t</table>\n\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Get the page sections\n\t *\n\t * @return   array\n\t * @since    3.17.5\n\t * @version  3.17.5\n\t */\n\tpublic function get_sections() {\n\n\t\t$sections = array();\n\n\t\t$gateways = llms()->payment_gateways()->get_payment_gateways();\n\n\t\tforeach ( $gateways as $id => $gateway ) {\n\t\t\t$sections[ $id ] = $gateway->get_admin_title();\n\t\t}\n\n\t\tasort( $sections );\n\n\t\t$sections = array_merge(\n\t\t\tarray(\n\t\t\t\t'main' => __( 'Checkout Settings', 'lifterlms' ),\n\t\t\t),\n\t\t\t$sections\n\t\t);\n\n\t\treturn apply_filters( 'llms_checkout_settings_sections', $sections );\n\t}\n\n\t/**\n\t * Get settings array\n\t *\n\t * @return   array\n\t * @since    3.0.4\n\t * @version  3.17.5\n\t */\n\tpublic function get_settings() {\n\n\t\t$curr_section = $this->get_current_section();\n\n\t\tif ( 'main' === $curr_section ) {\n\n\t\t\treturn apply_filters( 'lifterlms_checkout_settings', $this->get_settings_default() );\n\n\t\t}\n\n\t\treturn apply_filters( 'lifterlms_gateway_settings_' . $curr_section, $this->get_settings_gateway( $curr_section ) );\n\t}\n\n\t/**\n\t * Retrieve the default checkout settings for the main section\n\t *\n\t * @since 3.17.5\n\t * @since 3.30.3 Fixed spelling errors.\n\t *\n\t * @return array\n\t */\n\tprivate function get_settings_default() {\n\n\t\t$currency_code_options = get_lifterlms_currencies();\n\t\tforeach ( $currency_code_options as $code => $name ) {\n\t\t\t$currency_code_options[ $code ] = $name . ' (' . get_lifterlms_currency_symbol( $code ) . ')';\n\t\t}\n\n\t\t$country_options = get_lifterlms_countries();\n\t\tforeach ( $country_options as $code => $name ) {\n\t\t\t$country_options[ $code ] = $name . ' (' . $code . ')';\n\t\t}\n\n\t\treturn array(\n\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionstart',\n\t\t\t\t'id'   => 'checkout_settings_gateways_list_start',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title' => __( 'Payment Gateways', 'lifterlms' ),\n\t\t\t\t'desc'  => sprintf( __( 'Gateways allow you to accept payments on your site. %1$1sLearn More%2$2s', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/what-payment-gateways-can-i-use-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\">', '</a>' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'id'    => 'checkout_settings_gateways_list_title',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'value' => $this->get_gateway_table_html(),\n\t\t\t\t'type'  => 'custom-html',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionend',\n\t\t\t\t'id'   => 'checkout_settings_gateways_list_end',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'class' => 'top',\n\t\t\t\t'id'    => 'course_archive_options',\n\t\t\t\t'type'  => 'sectionstart',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'id'    => 'course_options',\n\t\t\t\t'title' => __( 'Checkout Settings', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'             => __( 'Checkout Page', 'lifterlms' ),\n\t\t\t\t'desc'              => __( 'Page used for displaying the checkout form.', 'lifterlms' ) . ' ' . sprintf( __( 'Requires the %1$s[lifterlms_checkout]%2$s shortcode or the \"Checkout\" block.', 'lifterlms' ), '<code>', '</code>' ),\n\t\t\t\t'id'                => 'lifterlms_checkout_page_id',\n\t\t\t\t'type'              => 'select',\n\t\t\t\t'default'           => '',\n\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-post-type' => 'page',\n\t\t\t\t),\n\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'lifterlms_checkout_page_id', '' ) ),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Confirm Payment', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Payment confirmation endpoint slug', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_myaccount_confirm_payment_endpoint',\n\t\t\t\t'type'     => 'text',\n\t\t\t\t'default'  => 'confirm-payment',\n\t\t\t\t'desc_tip' => true,\n\t\t\t\t'sanitize' => 'slug',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Force SSL', 'lifterlms' ),\n\t\t\t\t'desc'    => __( 'Force secure checkout via SSL (https) on the checkout page(s).', 'lifterlms' ) .\n\t\t\t\t\t\t\t\t'<p class=\"description\">' . sprintf( __( 'Requires an SSL certificate. %1$sLearn More%2$s', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/ssl-and-https/\" target=\"_blank\">', '</a>' ) . '</p>',\n\t\t\t\t'id'      => 'lifterlms_checkout_force_ssl',\n\t\t\t\t'type'    => 'checkbox',\n\t\t\t\t'default' => 'no',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'default' => 'yes',\n\t\t\t\t'desc'    => __( 'Enable automatic retry of failed recurring payments.', 'lifterlms' ) .\n\t\t\t\t\t\t\t\t'<p class=\"description\">' . sprintf( esc_html__( 'Recover lost revenue from temporarily declined payment methods. %1$sLearn More%2$s', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/automatic-retry-failed-payments/\" target=\"_blank\">', '</a>' ) . '</p>',\n\t\t\t\t'id'      => 'lifterlms_recurring_payment_retry',\n\t\t\t\t'title'   => __( 'Retry Failed Payments', 'lifterlms' ),\n\t\t\t\t'type'    => 'checkbox',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionend',\n\t\t\t\t'id'   => 'course_archive_options',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionstart',\n\t\t\t\t'id'   => 'general_options',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title' => __( 'Currency Options', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'desc'  => __( 'The following options affect how prices are displayed on the frontend.', 'lifterlms' ),\n\t\t\t\t'id'    => 'pricing_options',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'class'    => 'llms-select2',\n\t\t\t\t'title'    => __( 'Country', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Select the country LifterLMS should use as the default during transactions and registrations.', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_country',\n\t\t\t\t'default'  => 'US',\n\t\t\t\t'type'     => 'select',\n\t\t\t\t'desc_tip' => false,\n\t\t\t\t'options'  => $country_options,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'class'    => 'llms-select2',\n\t\t\t\t'title'    => __( 'Currency', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'Select the currency LifterLMS should use to display prices and process transactions.', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_currency',\n\t\t\t\t'default'  => 'USD',\n\t\t\t\t'type'     => 'select',\n\t\t\t\t'desc_tip' => false,\n\t\t\t\t'options'  => $currency_code_options,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Currency Position', 'lifterlms' ),\n\t\t\t\t'desc'    => __( 'Customize the position and formatting of the currency symbol for displayed prices.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_currency_position',\n\t\t\t\t'default' => 'left',\n\t\t\t\t'type'    => 'select',\n\t\t\t\t'options' => array(\n\t\t\t\t\t'left'        => 'Left (' . sprintf( '%1$s%2$s', get_lifterlms_currency_symbol(), 99.99 ) . ')',\n\t\t\t\t\t'right'       => 'Right (' . sprintf( '%2$s%1$s', get_lifterlms_currency_symbol(), 99.99 ) . ')',\n\t\t\t\t\t'left_space'  => 'Left with Space (' . sprintf( '%1$s&nbsp;%2$s', get_lifterlms_currency_symbol(), 99.99 ) . ')',\n\t\t\t\t\t'right_space' => 'Right with Space (' . sprintf( '%2$s&nbsp;%1$s', get_lifterlms_currency_symbol(), 99.99 ) . ')',\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Thousand Separator', 'lifterlms' ),\n\t\t\t\t'class'   => 'tiny',\n\t\t\t\t'desc'    => __( 'Choose the character to display as the thousand\\'s place separator for displayed prices.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_thousand_separator',\n\t\t\t\t'type'    => 'text',\n\t\t\t\t'default' => ',',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Decimal Separator', 'lifterlms' ),\n\t\t\t\t'class'   => 'tiny',\n\t\t\t\t'desc'    => __( 'Choose the character to display as the decimal separator for displayed prices.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_decimal_separator',\n\t\t\t\t'type'    => 'text',\n\t\t\t\t'default' => '.',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Decimal Places', 'lifterlms' ),\n\t\t\t\t'class'   => 'tiny',\n\t\t\t\t'desc'    => __( 'Customize the number of decimal places for prices.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_decimals',\n\t\t\t\t'type'    => 'number',\n\t\t\t\t'default' => '2',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Hide Zero Decimals', 'lifterlms' ),\n\t\t\t\t'desc'    => __( 'Automatically remove zero decimals from the end of displayed prices.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_trim_zero_decimals',\n\t\t\t\t'default' => 'no',\n\t\t\t\t'type'    => 'checkbox',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionend',\n\t\t\t\t'id'   => 'general_options',\n\t\t\t),\n\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve settings for a gateway section\n\t *\n\t * @param    string $curr_section  gateway ID string\n\t * @return   array\n\t * @since    3.17.5\n\t * @version  3.17.5\n\t */\n\tprivate function get_settings_gateway( $curr_section ) {\n\n\t\t$settings = array();\n\n\t\t$settings[] = array(\n\t\t\t'type'  => 'sectionstart',\n\t\t\t'id'    => 'start_gateway_settings_' . $curr_section,\n\t\t\t'class' => 'top',\n\t\t);\n\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( $curr_section );\n\t\tif ( ! $gateway ) {\n\n\t\t\t$settings[] = array(\n\t\t\t\t'title' => __( 'Payment Gateway Settings', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'id'    => 'title_gateway_settings_' . $curr_section,\n\t\t\t);\n\n\t\t\t$settings[] = array(\n\t\t\t\t'title' => sprintf( __( 'Error: \"%s\" is not a valid payment gateway', 'lifterlms' ), $curr_section ),\n\t\t\t\t'type'  => 'subtitle',\n\t\t\t\t'id'    => 'title_gateway_settings_' . $curr_section,\n\t\t\t);\n\n\t\t} else {\n\n\t\t\t$settings[] = array(\n\t\t\t\t'title' => sprintf( __( '%s Payment Gateway Settings', 'lifterlms' ), $gateway->get_admin_title() ),\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'id'    => 'title_gateway_settings_' . $curr_section,\n\t\t\t);\n\n\t\t\t$settings = array_merge( $settings, $gateway->get_admin_settings_fields() );\n\n\t\t}\n\n\t\t$settings[] = array(\n\t\t\t'type' => 'sectionend',\n\t\t\t'id'   => 'end_gateway_settings_' . $curr_section,\n\t\t);\n\n\t\treturn $settings;\n\t}\n\n\t/**\n\t * Override default save method to save the display order of payment gateways\n\t *\n\t * @since 3.17.5\n\t * @since 3.35.1 Verify nonce.\n\t *\n\t * @return   void\n\t */\n\tpublic function save() {\n\n\t\tif ( ! isset( $_REQUEST['_wpnonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'lifterlms-settings' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Save all custom fields.\n\t\tparent::save();\n\n\t\t// Save display order of gateways.\n\t\tforeach ( llms()->payment_gateways()->get_payment_gateways() as $id => $gateway ) {\n\t\t\t$option = $gateway->get_option_name( 'display_order' );\n\t\t\tif ( isset( $_POST[ $option ] ) ) {\n\t\t\t\tupdate_option( $option, absint( $_POST[ $option ] ) );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * usort function used to ensure gateways are sorted by display order on the gateways table\n\t *\n\t * @param    obj $gateway_a  Payment Gateway instance\n\t * @param    obj $gateway_b  Payment Gateway instance\n\t * @return   int\n\t * @since    3.17.5\n\t * @version  3.17.5\n\t */\n\tpublic function sort_gateways( $gateway_a, $gateway_b ) {\n\n\t\t$a_order = $gateway_a->get_display_order();\n\t\t$b_order = $gateway_b->get_display_order();\n\n\t\tif ( $a_order == $b_order ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn $a_order < $b_order ? -1 : 1;\n\t}\n}\n\nreturn new LLMS_Settings_Checkout();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.courses.php",
    "content": "<?php\n/**\n * Admin Settings Page \"Courses\" Tab\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 3.5.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page \"Courses\" Tab class\n *\n * @since 3.5.0\n */\nclass LLMS_Settings_Courses extends LLMS_Settings_Page {\n\n\t/**\n\t * Constructor\n\t *\n\t * Executes settings tab actions.\n\t *\n\t * @since 3.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id    = 'courses';\n\t\t$this->label = __( 'Courses', 'lifterlms' );\n\n\t\tadd_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );\n\t\tadd_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'save' ) );\n\t}\n\n\t/**\n\t * Get settings array.\n\t *\n\t * @since 3.5.0\n\t * @since 7.5.0 Added settings for enabling/disabling favorites.\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\n\t\t/**\n\t\t * Filter the course settings.\n\t\t *\n\t\t * @since 3.5.0\n\t\t *\n\t\t * @param array $course_settings THe course Settings.\n\t\t */\n\t\t$course = apply_filters(\n\t\t\t'lifterlms_course_settings',\n\t\t\tarray(\n\n\t\t\t\tarray(\n\t\t\t\t\t'class' => 'top',\n\t\t\t\t\t'id'    => 'course_general_options',\n\t\t\t\t\t'type'  => 'sectionstart',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => 'course_general_options_title',\n\t\t\t\t\t'title' => __( 'Course Settings', 'lifterlms' ),\n\t\t\t\t\t'type'  => 'title',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'desc'    => __( 'Enabling this setting allows students to mark a lesson as \"incomplete\" after they have completed a lesson.', 'lifterlms' ),\n\t\t\t\t\t'default' => 'no',\n\t\t\t\t\t'id'      => 'lifterlms_retake_lessons',\n\t\t\t\t\t'title'   => __( 'Retake Lessons', 'lifterlms' ),\n\t\t\t\t\t'type'    => 'checkbox',\n\t\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'desc'    => __( 'Enabling this setting will display lessons in a distraction-free focus mode.', 'lifterlms' ),\n\t\t\t\t'default' => 'no',\n\t\t\t\t'id'      => 'lifterlms_enable_focus_mode',\n\t\t\t\t'title'   => __( 'Focus Mode', 'lifterlms' ),\n\t\t\t\t'type'    => 'checkbox',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'default' => '960',\n\t\t\t\t'desc'    => __( 'Set the maximum width of the lesson content area in focus mode.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_focus_mode_content_width',\n\t\t\t\t'options' => array(\n\t\t\t\t\t'full' => __( 'Full Width', 'lifterlms' ),\n\t\t\t\t\t'1600' => __( 'Extra Wide (1600px)', 'lifterlms' ),\n\t\t\t\t\t'1180' => __( 'Wide (1180px)', 'lifterlms' ),\n\t\t\t\t\t'960'  => __( 'Default (960px)', 'lifterlms' ),\n\t\t\t\t\t'768'  => __( 'Narrow (768px)', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'title'   => __( 'Focus Mode Content Width', 'lifterlms' ),\n\t\t\t\t'type'    => 'select',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'default' => 'left',\n\t\t\t\t'desc'    => __( 'Choose which side the course syllabus sidebar appears on.', 'lifterlms' ),\n\t\t\t\t'id'      => 'lifterlms_focus_mode_sidebar_position',\n\t\t\t\t'options' => array(\n\t\t\t\t\t'left'  => __( 'Left', 'lifterlms' ),\n\t\t\t\t\t'right' => __( 'Right', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'title'   => __( 'Focus Mode Sidebar Position', 'lifterlms' ),\n\t\t\t\t'type'    => 'select',\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'title'   => __( 'Lesson Favorites', 'lifterlms' ),\n\t\t\t\t\t'desc'    => __( 'Enabling this setting allows students to mark a lesson as \"favorite\".', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_favorites',\n\t\t\t\t\t'default' => 'yes',\n\t\t\t\t\t'type'    => 'checkbox',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t\t'data-post-type'   => 'page',\n\t\t\t\t\t\t'data-placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'desc'              => sprintf( __( 'This page will be shown to students when they complete the course. %1$sMore Information%2$s', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/course-completion-page/\" target=\"_blank\">', '</a>' ),\n\t\t\t\t\t'id'                => 'lifterlms_course_completion_page_id',\n\t\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'lifterlms_course_completion_page_id', '' ) ),\n\t\t\t\t\t'title'             => __( 'Course Completion', 'lifterlms' ),\n\t\t\t\t\t'type'              => 'select',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'type' => 'sectionend',\n\t\t\t\t\t'id'   => 'course_general_options',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'class' => 'top',\n\t\t\t\t\t'id'    => 'course_archive_options',\n\t\t\t\t\t'type'  => 'sectionstart',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => 'course_options',\n\t\t\t\t\t'title' => __( 'Course Catalog Settings', 'lifterlms' ),\n\t\t\t\t\t'type'  => 'title',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t\t'data-post-type'   => 'page',\n\t\t\t\t\t\t'data-placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'desc'              => sprintf( __( 'This page is where your visitors will find a list of all your available courses. %1$sMore Information%2$s', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/course-catalog/\" target=\"_blank\">', '</a>' ),\n\t\t\t\t\t'id'                => 'lifterlms_shop_page_id',\n\t\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'lifterlms_shop_page_id', '' ) ),\n\t\t\t\t\t'title'             => __( 'Course Catalog', 'lifterlms' ),\n\t\t\t\t\t'type'              => 'select',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'default' => 9,\n\t\t\t\t\t'desc'    => __( 'To show all courses on one page, enter -1.', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_shop_courses_per_page',\n\t\t\t\t\t'title'   => __( 'Courses per page', 'lifterlms' ),\n\t\t\t\t\t'type'    => 'number',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'default' => 'menu_order',\n\t\t\t\t\t'desc'    => __( 'Determines the display order for courses on the courses page.', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_shop_ordering',\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'menu_order,ASC'  => __( 'Order (Low to High)', 'lifterlms' ),\n\t\t\t\t\t\t'menu_order,DESC' => __( 'Order (High to Low)', 'lifterlms' ),\n\t\t\t\t\t\t'title,ASC'       => __( 'Title (A - Z)', 'lifterlms' ),\n\t\t\t\t\t\t'title,DESC'      => __( 'Title (Z - A)', 'lifterlms' ),\n\t\t\t\t\t\t'date,DESC'       => __( 'Most Recent', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'title'   => __( 'Catalog Sorting', 'lifterlms' ),\n\t\t\t\t\t'type'    => 'select',\n\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'type' => 'sectionend',\n\t\t\t\t\t'id'   => 'course_archive_options',\n\t\t\t\t),\n\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Ensure deprecated filter sticks around for a while\n\t\t *\n\t\t * @todo  deprecate this filter\n\t\t */\n\t\t$deprecated = apply_filters( 'lifterlms_catalog_settings', array() );\n\n\t\treturn array_merge( $course, $deprecated );\n\t}\n\n\t/**\n\t * Save settings\n\t *\n\t * @since   3.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function save() {\n\t\t$settings = $this->get_settings();\n\t\tLLMS_Admin_Settings::save_fields( $settings );\n\t}\n\n\t/**\n\t * Output settings on screen\n\t *\n\t * @since   3.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\t\t$settings = $this->get_settings();\n\t\tLLMS_Admin_Settings::output_fields( $settings );\n\t\tadd_action( 'shutdown', array( $this, 'flush_rewrite_rules' ) );\n\t}\n}\n\nreturn new LLMS_Settings_Courses();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.engagements.php",
    "content": "<?php\n/**\n * Admin Settings Page: Engagements\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Settings_Engagements class\n *\n * @since 1.0.0\n * @since 3.8.0 Unknown.\n * @since 3.37.3 Renamed setting field IDs to be unique.\n *              Removed redundant functions defined in the `LLMS_Settings_Page` class.\n *              Removed constructor and added `get_label()` method to be compatible with changes in `LLMS_Settings_Page`.\n * @since 3.40.0 Add a section that displays conditionally for email delivery provider connections.\n */\nclass LLMS_Settings_Engagements extends LLMS_Settings_Page {\n\n\t/**\n\t * Settings identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id = 'engagements';\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct();\n\t\tadd_action( \"lifterlms_settings_{$this->id}\", array( $this, 'output_js' ) );\n\t}\n\n\t/**\n\t * Retrieve the page label.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return string\n\t */\n\tprotected function set_label() {\n\t\treturn __( 'Engagements', 'lifterlms' );\n\t}\n\n\t/**\n\t * Get settings array\n\t *\n\t * @since 1.0.0\n\t * @since 3.8.0 Unknown.\n\t * @since 3.37.3 Refactor to pull each settings group from its own method.\n\t * @since 3.40.0 Include an email delivery section.\n\t * @since 6.0.0 Include achievements section.\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\n\t\t/**\n\t\t * Modify LifterLMS Admin Settings on the \"Engagements\" tab,\n\t\t *\n\t\t * @since 1.0.0\n\t\t *\n\t\t * @param array[] $settings Array of settings fields arrays.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'lifterlms_engagements_settings',\n\t\t\tarray_merge(\n\t\t\t\t$this->get_settings_group_email(),\n\t\t\t\t$this->get_settings_group_email_delivery(),\n\t\t\t\t$this->get_settings_group_achievements(),\n\t\t\t\t$this->get_settings_group_certs()\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve fields for the achievements settings group.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array[]\n\t */\n\tprotected function get_settings_group_achievements() {\n\n\t\treturn $this->generate_settings_group(\n\t\t\t'achievement_options',\n\t\t\t__( 'Achievement Settings', 'lifterlms' ),\n\t\t\t'',\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'title'    => __( 'Placeholder Image', 'lifterlms' ),\n\t\t\t\t\t'desc'     => $this->get_award_image_desc( __( 'achievement', 'lifterlms' ) ),\n\t\t\t\t\t'id'       => 'lifterlms_achievement_default_img',\n\t\t\t\t\t'type'     => 'image',\n\t\t\t\t\t'value'    => llms()->achievements()->get_default_image( 0 ),\n\t\t\t\t\t'autoload' => false,\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve fields for the certificates settings group.\n\t *\n\t * @since 3.37.3\n\t * @since 6.0.0 Add background image options.\n\t *               Only load legacy certificate options when the legacy option is enabled.\n\t *\n\t * @return array[]\n\t */\n\tprotected function get_settings_group_certs() {\n\n\t\t$certificate_sizes = llms_get_certificate_sizes();\n\n\t\t$settings = array(\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Placeholder Background Image', 'lifterlms' ),\n\t\t\t\t'desc'     => $this->get_award_image_desc( __( 'certificate', 'lifterlms' ) ),\n\t\t\t\t'id'       => 'lifterlms_certificate_default_img',\n\t\t\t\t'type'     => 'image',\n\t\t\t\t'value'    => llms()->certificates()->get_default_image( 0 ),\n\t\t\t\t'autoload' => false,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Default Size', 'lifterlms' ),\n\t\t\t\t'desc'     => __( 'The default size used when creating new certificates.', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_certificate_default_size',\n\t\t\t\t'type'     => 'select',\n\t\t\t\t'options'  => $this->get_certificate_size_opts(),\n\t\t\t\t'default'  => 'LETTER',\n\t\t\t\t'autoload' => false,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title' => __( 'User Defined Certificate Size', 'lifterlms' ),\n\t\t\t\t'desc'  => __( 'Use these settings to customize the User Defined Certificate size.', 'lifterlms' ),\n\t\t\t\t'id'    => 'cert_user_defined_size_settings',\n\t\t\t\t'type'  => 'subtitle',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'             => __( 'Width', 'lifterlms' ),\n\t\t\t\t'id'                => 'lifterlms_certificate_default_user_defined_width',\n\t\t\t\t'type'              => 'number',\n\t\t\t\t'default'           => $certificate_sizes['USER_DEFINED']['width'],\n\t\t\t\t'autoload'          => false,\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'step' => '0.01',\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'             => __( 'Height', 'lifterlms' ),\n\t\t\t\t'id'                => 'lifterlms_certificate_default_user_defined_height',\n\t\t\t\t'type'              => 'number',\n\t\t\t\t'default'           => $certificate_sizes['USER_DEFINED']['height'],\n\t\t\t\t'autoload'          => false,\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'step' => '0.01',\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title'    => __( 'Unit', 'lifterlms' ),\n\t\t\t\t'id'       => 'lifterlms_certificate_default_user_defined_unit',\n\t\t\t\t'type'     => 'select',\n\t\t\t\t'options'  => $this->get_certificate_units_opts(),\n\t\t\t\t'default'  => $certificate_sizes['USER_DEFINED']['unit'],\n\t\t\t\t'autoload' => false,\n\t\t\t),\n\t\t);\n\n\t\tif ( $this->has_legacy_certificates() ) {\n\n\t\t\t$settings = array_merge(\n\t\t\t\t$settings,\n\t\t\t\tarray(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'title' => __( 'Legacy Certificate Background Image Settings', 'lifterlms' ),\n\t\t\t\t\t\t'type'  => 'subtitle',\n\t\t\t\t\t\t'desc'  => __( 'Use these settings to determine the dimensions of legacy certificate background images created using the classic editor. These settings have no effect on certificates created using the block editor. After changing these settings, you may need to <a href=\"http://wordpress.org/extend/plugins/regenerate-thumbnails/\" target=\"_blank\">regenerate your thumbnails</a>.', 'lifterlms' ),\n\t\t\t\t\t\t'id'    => 'cert_bg_image_settings',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'title'    => __( 'Image Width', 'lifterlms' ),\n\t\t\t\t\t\t'desc'     => __( 'in pixels', 'lifterlms' ),\n\t\t\t\t\t\t'id'       => 'lifterlms_certificate_bg_img_width',\n\t\t\t\t\t\t'default'  => '800',\n\t\t\t\t\t\t'type'     => 'number',\n\t\t\t\t\t\t'autoload' => false,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'title'    => __( 'Image Height', 'lifterlms' ),\n\t\t\t\t\t\t'id'       => 'lifterlms_certificate_bg_img_height',\n\t\t\t\t\t\t'desc'     => __( 'in pixels', 'lifterlms' ),\n\t\t\t\t\t\t'default'  => '616',\n\t\t\t\t\t\t'type'     => 'number',\n\t\t\t\t\t\t'autoload' => false,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'title'    => __( 'Legacy compatibility', 'lifterlms' ),\n\t\t\t\t\t\t'desc'     => __( 'Use legacy certificate image sizes.', 'lifterlms' ) .\n\t\t\t\t\t\t\t\t\t\t'<br><em>' . __( 'Enabling this will override the above dimension settings and set the image dimensions to match the dimensions of the uploaded image.', 'lifterlms' ) . '</em>',\n\t\t\t\t\t\t'id'       => 'lifterlms_certificate_legacy_image_size',\n\t\t\t\t\t\t'default'  => 'no',\n\t\t\t\t\t\t'type'     => 'checkbox',\n\t\t\t\t\t\t'autoload' => false,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\n\t\treturn $this->generate_settings_group(\n\t\t\t'certificates_options',\n\t\t\t__( 'Certificate Settings', 'lifterlms' ),\n\t\t\t'',\n\t\t\t$settings\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve fields for the email settings group.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return array[]\n\t */\n\tprotected function get_settings_group_email() {\n\n\t\treturn $this->generate_settings_group(\n\t\t\t'email_options',\n\t\t\t__( 'Email Settings', 'lifterlms' ),\n\t\t\t__( 'Settings for all emails sent by LifterLMS. Notification and engagement emails will adhere to these settings.', 'lifterlms' ),\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'title'   => __( 'Sender Name', 'lifterlms' ),\n\t\t\t\t\t'desc'    => __( 'Name to be displayed in From field.', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_email_from_name',\n\t\t\t\t\t'type'    => 'text',\n\t\t\t\t\t'default' => esc_attr( get_bloginfo( 'title' ) ),\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'title'   => __( 'Sender Email', 'lifterlms' ),\n\t\t\t\t\t'desc'    => __( 'Email Address displayed in the From field.', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_email_from_address',\n\t\t\t\t\t'type'    => 'email',\n\t\t\t\t\t'default' => get_option( 'admin_email' ),\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'title'    => __( 'Header Image', 'lifterlms' ),\n\t\t\t\t\t'id'       => 'lifterlms_email_header_image',\n\t\t\t\t\t'type'     => 'image',\n\t\t\t\t\t'default'  => '',\n\t\t\t\t\t'autoload' => false,\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'title'   => __( 'Email Footer Text', 'lifterlms' ),\n\t\t\t\t\t'desc'    => __( 'Text you would like displayed in the footer of all emails.', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_email_footer_text',\n\t\t\t\t\t'type'    => 'textarea',\n\t\t\t\t\t'default' => '',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve email delivery partner settings groups.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_settings_group_email_delivery() {\n\n\t\t/**\n\t\t * Filter settings for available email delivery services.\n\t\t *\n\t\t * @since 3.40.0\n\t\t *\n\t\t * @param array[] $settings Array of settings arrays.\n\t\t */\n\t\t$services = apply_filters( 'llms_email_delivery_services', array() );\n\n\t\t// If there's no services respond with an empty array so we don't output the whole section.\n\t\tif ( ! $services ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t// Output the a section.\n\t\treturn $this->generate_settings_group(\n\t\t\t'email_delivery',\n\t\t\t__( 'Email Delivery', 'lifterlms' ),\n\t\t\t'',\n\t\t\t$services\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieves the options array for the `lifterlms_certificate_default_size` option.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_certificate_size_opts() {\n\n\t\t$units = llms_get_certificate_units();\n\n\t\t$sizes = array();\n\n\t\tforeach ( llms_get_certificate_sizes() as $size_id => $data ) {\n\n\t\t\t$unit = $units[ $data['unit'] ] ?? '';\n\n\t\t\t$sizes[ $size_id ] = sprintf(\n\t\t\t\t'%1$s (%2$s%4$s x %3$s%4$s)',\n\t\t\t\t$data['name'],\n\t\t\t\t$data['width'],\n\t\t\t\t$data['height'],\n\t\t\t\t$unit['symbol'] ?? ''\n\t\t\t);\n\n\t\t}\n\n\t\treturn $sizes;\n\n\t}\n\n\t/**\n\t * Retrieves the options array for the `lifterlms_certificate_default_user_defined_units` option.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_certificate_units_opts() {\n\n\t\t$units = llms_get_certificate_units();\n\t\t$opts  = array();\n\n\t\tforeach ( $units as $unit => $data ) {\n\t\t\t$opts[ $unit ] = sprintf(\n\t\t\t\t'%1$s (%2$s)',\n\t\t\t\t$unit,\n\t\t\t\t$data['name']\n\t\t\t);\n\t\t}\n\t\treturn $opts;\n\n\t}\n\n\t/**\n\t * Retrieves the award image setting description HTML.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $post_type Translated post type name.\n\t * @return string\n\t */\n\tprivate function get_award_image_desc( $post_type ) {\n\n\t\t$desc = sprintf(\n\t\t\t__( 'A default image used for any %1$s template or award which does not specify an image. Changing this setting will affect all existing templates and awards which do not specify their own image.', 'lifterlms' ),\n\t\t\t$post_type\n\t\t);\n\t\treturn '<p class=\"description\">' . $desc . '</p>';\n\n\t}\n\n\t/**\n\t * Determines if legacy certificate options should be displayed.\n\t *\n\t * The option used to determine if there are certificates is set during a migration to version from versions\n\t * earlier than 6.0.0. During the migration if at least one certificate template is migrated, the option\n\t * is set and the legacy options will be displayed.\n\t *\n\t * Even after all certificates have been individually migrated the option will still be set and should be\n\t * deleted via the db, set to 'no' via the options.php screen or disabled by returning `false` from the short\n\t * circuit filter {@see llms_has_legacy_certificates}.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return boolean\n\t */\n\tprivate function has_legacy_certificates() {\n\n\t\t/**\n\t\t * Short-circuits the legacy certificates check preventing a database call.\n\t\t *\n\t\t * This can be used to force-enable or force-disable legacy certificate settings regardless\n\t\t * of the value found in the database option.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param boolean $has_legacy_certificates Return `true` to force legacy certificate settings on\n\t\t *                                         and `false` to force them off.\n\t\t */\n\t\t$pre = apply_filters( 'llms_has_legacy_certificates', null );\n\t\tif ( ! is_null( $pre ) ) {\n\t\t\treturn $pre;\n\t\t}\n\n\t\treturn llms_parse_bool( get_option( 'llms_has_certificates_with_legacy_default_image', 'no' ) );\n\n\t}\n\n\t/**\n\t * Outputs inline Javascript utilized on the engagements settings tab.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_js() {\n\t\t?>\n\t\t<script>(function(){\n\t\t\tconst fields = {\n\t\t\t\theight: document.getElementById( 'lifterlms_certificate_default_user_defined_height' ),\n\t\t\t\twidth: document.getElementById( 'lifterlms_certificate_default_user_defined_width' ),\n\t\t\t\tunit: document.getElementById( 'lifterlms_certificate_default_user_defined_unit' ),\n\t\t\t};\n\t\t\t/**\n\t\t\t * Updates the USER_DEFINED <option> text when the values of the custom inputs change.\n\t\t\t *\n\t\t\t * @since 6.0.0\n\t\t\t *\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tfunction updateOptionText() {\n\t\t\t\tconst opt = document.getElementById( 'lifterlms_certificate_default_size' ).querySelector( 'option[value=\"USER_DEFINED\"]' ),\n\t\t\t\t\tnewStr = fields.width.value + fields.unit.value + ' x ' + fields.height.value + fields.unit.value;\n\t\t\t\tif ( opt ) {\n\t\t\t\t\topt.textContent = opt.textContent.replace( / \\(.*\\)/, ' (' + newStr + ')' );\n\t\t\t\t}\n\t\t\t}\n\t\t\t// When any of the fields change, update the value of the option.\n\t\t\tObject.values( fields ).map( function( el ) {\n\t\t\t\tel.addEventListener( 'change', updateOptionText );\n\t\t\t} );\n\t\t})();</script>\n\t\t<?php\n\t}\n\n}\n\nreturn new LLMS_Settings_Engagements();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.general.php",
    "content": "<?php\n/**\n * Admin Settings Page, General Tab.\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 1.0.0\n * @version 5.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page, General Tab class\n *\n * @since 1.0.0\n * @since 3.22.0 Unknown.\n */\nclass LLMS_Settings_General extends LLMS_Settings_Page {\n\n\t/**\n\t * Constructor\n\t *\n\t * Executes settings tab actions.\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id    = 'general';\n\t\t$this->label = __( 'General', 'lifterlms' );\n\n\t\tadd_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );\n\t\tadd_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'save' ) );\n\t}\n\n\t/**\n\t * Get settings array.\n\t *\n\t * @since 1.0.0\n\t * @since 3.13.0 Unknown.\n\t * @since 5.6.0 use LLMS_Roles::get_all_role_names() to retrieve the list of roles who can bypass enrollments.\n\t *              Add content protection setting.\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\n\t\t$settings = array();\n\n\t\t$settings[] = array(\n\t\t\t'id'    => 'section_features',\n\t\t\t'type'  => 'sectionstart',\n\t\t\t'class' => 'top',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'id'    => 'features',\n\t\t\t'title' => __( 'Features', 'lifterlms' ),\n\t\t\t'type'  => 'title',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'type'  => 'custom-html',\n\t\t\t'value' => sprintf(\n\t\t\t\t__( 'Automatic Recurring Payments: <strong>%s</strong>', 'lifterlms' ),\n\t\t\t\tLLMS_Site::get_feature( 'recurring_payments' ) ? __( 'Enabled', 'lifterlms' ) : __( 'Disabled', 'lifterlms' )\n\t\t\t),\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'id'   => 'section_features',\n\t\t\t'type' => 'sectionend',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'id'   => 'section_tools',\n\t\t\t'type' => 'sectionstart',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'id'    => 'general_settings',\n\t\t\t'title' => __( 'General Settings', 'lifterlms' ),\n\t\t\t'type'  => 'title',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'class'             => 'llms-select2',\n\t\t\t'custom_attributes' => array(\n\t\t\t\t'data-placeholder' => __( 'Select user roles', 'lifterlms' ),\n\t\t\t),\n\t\t\t'default'           => array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ),\n\t\t\t'desc'              => __( 'Users with the selected roles will bypass enrollment, drip, and prerequisite restrictions for courses and memberships.', 'lifterlms' ),\n\t\t\t'id'                => 'llms_grant_site_access',\n\t\t\t'options'           => array_filter(\n\t\t\t\tLLMS_Roles::get_all_role_names(),\n\t\t\t\tfunction ( $role ) {\n\t\t\t\t\treturn 'student' !== $role;\n\t\t\t\t},\n\t\t\t\tARRAY_FILTER_USE_KEY\n\t\t\t),\n\t\t\t'title'             => __( 'Unrestricted Preview Access', 'lifterlms' ),\n\t\t\t'type'              => 'multiselect',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'title'   => __( 'Content Protection', 'lifterlms' ),\n\t\t\t'desc'    => __( 'Prevent users from copying website content and downloading images.', 'lifterlms' ) . '<br><br>' . __( 'Users with Unrestricted Preview Access will not be affected by this setting.', 'lifterlms' ),\n\t\t\t'id'      => 'lifterlms_content_protection',\n\t\t\t'default' => 'no',\n\t\t\t'type'    => 'checkbox',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'title'   => __( 'Frequency of Saving Tracked Events', 'lifterlms' ),\n\t\t\t'default' => 'minimum',\n\t\t\t'desc'    => __( 'Specifies how often tracked events are sent to the server. \"Minimum\" sends only when local storage is almost full (fewer network calls). \"Always\" sends each event immediately (higher accuracy, more server load).', 'lifterlms' ),\n\t\t\t'id'      => 'lifterlms_tracked_event_saving_frequency',\n\t\t\t'type'    => 'select',\n\t\t\t'options' => array(\n\t\t\t\t'minimum' => __( 'Minimum', 'lifterlms' ),\n\t\t\t\t'always'  => __( 'Always', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'id'   => 'general_settings',\n\t\t\t'type' => 'sectionend',\n\t\t);\n\n\t\treturn apply_filters( 'lifterlms_general_settings', $settings );\n\t}\n\n\t/**\n\t * save settings to the database\n\t *\n\t * @return void\n\t */\n\tpublic function save() {\n\n\t\t$settings = $this->get_settings();\n\t\tLLMS_Admin_Settings::save_fields( $settings );\n\t}\n}\n\nreturn new LLMS_Settings_General();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.integrations.php",
    "content": "<?php\n/**\n * Admin Settings Page, Integrations Tab\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 1.0.0\n * @version 3.18.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page, Integrations Tab class\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 1.0.0\n * @since 3.18.2 Unknown.\n */\nclass LLMS_Settings_Integrations extends LLMS_Settings_Page {\n\n\t/**\n\t * Constructor\n\t * executes settings tab actions\n\t *\n\t * @since    1.0.0\n\t * @version  3.18.2\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id    = 'integrations';\n\t\t$this->label = __( 'Integrations', 'lifterlms' );\n\n\t\tadd_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );\n\t\tadd_action( 'lifterlms_sections_' . $this->id, array( $this, 'output_sections_nav' ) );\n\t\tadd_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'save' ) );\n\t}\n\n\t/**\n\t * Get default settings array for the main integrations tab\n\t *\n\t * @return   array\n\t * @since    3.18.2\n\t * @version  3.18.2\n\t */\n\tprivate function get_default_settings() {\n\n\t\t$settings = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionstart',\n\t\t\t\t'id'   => 'checkout_settings_integrations_list_start',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title' => __( 'Integrations', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'id'    => 'checkout_settings_integrations_list_title',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'value' => $this->get_table_html(),\n\t\t\t\t'type'  => 'custom-html',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionend',\n\t\t\t\t'id'   => 'checkout_settings_integrations_list_end',\n\t\t\t),\n\t\t);\n\n\t\treturn apply_filters( 'llms_integrations_settings_default', $settings );\n\t}\n\n\t/**\n\t * Get the page sections\n\t *\n\t * @return   array\n\t * @since    3.18.2\n\t * @version  3.18.2\n\t */\n\tpublic function get_sections() {\n\n\t\t$sections = array();\n\n\t\t$integrations = llms()->integrations()->get_integrations();\n\n\t\tforeach ( $integrations as $int ) {\n\t\t\t$sections[ $int->id ] = trim( str_replace( 'LifterLMS', '', $int->title ) );\n\t\t}\n\n\t\t$sections = array_merge(\n\t\t\tarray(\n\t\t\t\t'main' => __( 'Integrations', 'lifterlms' ),\n\t\t\t),\n\t\t\t$sections\n\t\t);\n\n\t\treturn apply_filters( 'llms_integration_settings_sections', $sections );\n\t}\n\n\t/**\n\t * Get settings array\n\t *\n\t * @return   array\n\t * @since    1.0.0\n\t * @version  3.18.2\n\t */\n\tpublic function get_settings() {\n\n\t\t$curr_section = $this->get_current_section();\n\n\t\tif ( 'main' === $curr_section ) {\n\n\t\t\treturn apply_filters( 'lifterlms_integrations_settings', $this->get_default_settings() );\n\n\t\t}\n\n\t\treturn apply_filters( 'lifterlms_integrations_settings_' . $curr_section, array() );\n\t}\n\n\t/**\n\t * Get HTML for the integrations table\n\t *\n\t * @return   string\n\t * @since    3.18.2\n\t * @version  3.18.2\n\t */\n\tprivate function get_table_html() {\n\n\t\t$integrations = llms()->integrations()->get_integrations();\n\t\tob_start();\n\t\t?>\n\n\t\t<table class=\"llms-table zebra text-left size-large llms-integrations-table\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th><?php esc_html_e( 'Integration', 'lifterlms' ); ?></th>\n\t\t\t\t\t<th><?php esc_html_e( 'Integration ID', 'lifterlms' ); ?></th>\n\t\t\t\t\t<th><?php esc_html_e( 'Installed', 'lifterlms' ); ?></th>\n\t\t\t\t\t<th><?php esc_html_e( 'Enabled', 'lifterlms' ); ?></th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t<?php\n\t\t\tforeach ( $integrations as $integration ) :\n\t\t\t\tif ( ! is_subclass_of( $integration, 'LLMS_Abstract_Integration' ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t?>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><a href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-settings&tab=' . $this->id . '&section=' . $integration->id ) ); ?>\"><?php echo esc_html( $integration->title ); ?></a></td>\n\t\t\t\t\t<td><?php echo esc_html( $integration->id ); ?></td>\n\t\t\t\t\t<td class=\"status available\">\n\t\t\t\t\t\t<?php if ( $integration->is_installed() ) : ?>\n\t\t\t\t\t\t\t<span class=\"tip--bottom-right\" data-tip=\"<?php esc_attr_e( 'Installed', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Installed', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t\t<i class=\"fa fa-check-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t&ndash;\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"status enabled\">\n\t\t\t\t\t\t<?php if ( $integration->is_enabled() ) : ?>\n\t\t\t\t\t\t\t<span class=\"tip--bottom-right\" data-tip=\"<?php esc_attr_e( 'Enabled', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Enabled', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t\t<i class=\"fa fa-check-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t&ndash;\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t<?php endforeach; ?>\n\t\t\t</tbody>\n\t\t</table>\n\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n}\n\nreturn new LLMS_Settings_Integrations();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.memberships.php",
    "content": "<?php\n/**\n * Admin Settings Page \"Memberships\" Tab\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 3.5.0\n * @version 3.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page \"Memberships\" Tab class\n *\n * @since 3.5.0\n */\nclass LLMS_Settings_Memberships extends LLMS_Settings_Page {\n\n\t/**\n\t * Constructor\n\t *\n\t * Executes settings tab actions.\n\t *\n\t * @since 3.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id    = 'memberships';\n\t\t$this->label = __( 'Memberships', 'lifterlms' );\n\n\t\tadd_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );\n\t\tadd_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'save' ) );\n\t}\n\n\t/**\n\t * Get settings array\n\t *\n\t * @return  array\n\t * @since   3.5.0\n\t * @version 3.5.0\n\t */\n\tpublic function get_settings() {\n\n\t\treturn apply_filters(\n\t\t\t'lifterlms_membership_settings',\n\t\t\tarray(\n\n\t\t\t\tarray(\n\t\t\t\t\t'class' => 'top',\n\t\t\t\t\t'id'    => 'membership_general_options',\n\t\t\t\t\t'type'  => 'sectionstart',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => 'membership_general_options_title',\n\t\t\t\t\t'title' => __( 'Membership Settings', 'lifterlms' ),\n\t\t\t\t\t'type'  => 'title',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t\t'data-post-type'   => 'llms_membership',\n\t\t\t\t\t\t'data-placeholder' => __( 'Select a membership', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'default'           => '',\n\t\t\t\t\t'desc'              => __( 'Only allow access to site to users with a specific membership level. Users will be able to view and purchase membership level.', 'lifterlms' ),\n\t\t\t\t\t'id'                => 'lifterlms_membership_required',\n\t\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'lifterlms_membership_required', '' ) ),\n\t\t\t\t\t'title'             => __( 'Restrict site by membership level', 'lifterlms' ),\n\t\t\t\t\t'type'              => 'select',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'id'   => 'membership_general_options',\n\t\t\t\t\t'type' => 'sectionend',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'class' => 'top',\n\t\t\t\t\t'id'    => 'membership_catalog_options',\n\t\t\t\t\t'type'  => 'sectionstart',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => 'membership_catalog_options_title',\n\t\t\t\t\t'title' => __( 'Memberships Catalog', 'lifterlms' ),\n\t\t\t\t\t'type'  => 'title',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'class'             => 'llms-select2-post',\n\t\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t\t'data-post-type'   => 'page',\n\t\t\t\t\t\t'data-placeholder' => __( 'Select a page', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'default'           => '',\n\t\t\t\t\t'desc'              => __( 'This page is where your visitors will find a list of all your available memberships.', 'lifterlms' ),\n\t\t\t\t\t'id'                => 'lifterlms_memberships_page_id',\n\t\t\t\t\t'options'           => llms_make_select2_post_array( get_option( 'lifterlms_memberships_page_id', '' ) ),\n\t\t\t\t\t'title'             => __( 'Memberships Page', 'lifterlms' ),\n\t\t\t\t\t'type'              => 'select',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'title'   => __( 'Memberships per page', 'lifterlms' ),\n\t\t\t\t\t'desc'    => __( 'To show all memberships on one page, enter -1', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_memberships_per_page',\n\t\t\t\t\t'type'    => 'number',\n\t\t\t\t\t'default' => 9,\n\t\t\t\t\t'css'     => 'min-width:200px;',\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'default' => 'menu_order',\n\t\t\t\t\t'desc'    => __( 'Determines the display order for items on the memberships page.', 'lifterlms' ),\n\t\t\t\t\t'id'      => 'lifterlms_memberships_ordering',\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'menu_order,ASC'  => __( 'Order (Low to High)', 'lifterlms' ),\n\t\t\t\t\t\t'menu_order,DESC' => __( 'Order (High to Low)', 'lifterlms' ),\n\t\t\t\t\t\t'title,ASC'       => __( 'Title (A - Z)', 'lifterlms' ),\n\t\t\t\t\t\t'title,DESC'      => __( 'Title (Z - A)', 'lifterlms' ),\n\t\t\t\t\t\t'date,DESC'       => __( 'Most Recent', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'title'   => __( 'Memberships Sorting', 'lifterlms' ),\n\t\t\t\t\t'type'    => 'select',\n\n\t\t\t\t),\n\n\t\t\t\tarray(\n\t\t\t\t\t'id'   => 'membership_catalog_options',\n\t\t\t\t\t'type' => 'sectionend',\n\t\t\t\t),\n\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * save settings to the database\n\t *\n\t * @since 3.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function save() {\n\n\t\t$settings = $this->get_settings();\n\t\tLLMS_Admin_Settings::save_fields( $settings );\n\t}\n\n\t/**\n\t * Output settings\n\t *\n\t * @since 3.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\t\t$settings = $this->get_settings();\n\t\tLLMS_Admin_Settings::output_fields( $settings );\n\t}\n}\n\nreturn new LLMS_Settings_Memberships();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.notifications.php",
    "content": "<?php\n/**\n * Admin Settings: Notifications Tab\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 3.8.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Settings_Notifications class\n *\n * Admin Settings: Notifications Tab.\n *\n * @since 3.8.0\n * @since 3.30.3 Explicitly define class properties; fix typo in title element id.\n * @since 3.35.0 Sanitize input data.\n */\nclass LLMS_Settings_Notifications extends LLMS_Settings_Page {\n\n\t/**\n\t * @var LLMS_Abstract_Notification_View\n\t *\n\t * @since 3.8.0\n\t */\n\tpublic $view;\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id    = 'notifications';\n\t\t$this->label = __( 'Notifications', 'lifterlms' );\n\n\t\tadd_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );\n\t\tadd_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'before_save' ), 5 );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'save' ) );\n\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'after_save' ), 15 );\n\t\tadd_filter( 'llms_settings_' . $this->id . '_has_save_button', array( $this, 'maybe_disable_save' ) );\n\n\t}\n\n\t/**\n\t * Get a breadcrumb custom html for use on notification settings screens (not on the table)\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $current_title The title of the current notification.\n\t * @return array\n\t */\n\tprivate function get_breadcrumbs( $current_title ) {\n\t\treturn array(\n\t\t\t'id'    => 'notification_options_breadcrumbs',\n\t\t\t'type'  => 'custom-html',\n\t\t\t'value' => '<a href=\"' . esc_url( admin_url( 'admin.php?page=llms-settings&tab=notifications' ) ) . '\">' . __( 'All Notifications', 'lifterlms' ) . '</a> <small>&gt;</small> <strong>' . $current_title . '</strong>',\n\t\t);\n\t}\n\n\t/**\n\t * Get settings specific to the current notification type\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t * @since 5.2.0 Merge controller additional options.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param LLMS_Abstract_Notification_Controller $controller Instance of an LLMS_Abstract_Notification_Controller extending class.\n\t * @return array\n\t */\n\tprivate function get_notification_settings( $controller ) {\n\n\t\t$settings = array();\n\n\t\t// Setup vars.\n\t\t$type  = llms_filter_input_sanitize_string( INPUT_GET, 'type' );\n\t\t$types = $controller->get_supported_types();\n\t\t$title = $controller->get_title() . ' (' . $types[ $type ] . ')';\n\t\t$view  = $controller->get_mock_view( $type );\n\n\t\t// So the merge code button can use it.\n\t\t$this->view = $view;\n\n\t\t// Output the merge code button for the WYSIWYG editor.\n\t\tadd_action( 'media_buttons', array( $this, 'merge_code_button' ) );\n\n\t\t// Add a breadcrumb on the top of the page.\n\t\t$settings[] = $this->get_breadcrumbs( $title );\n\n\t\t// Add field options for the view.\n\t\t$settings = array_merge( $settings, $view->get_field_options( $type ) );\n\n\t\t$subscribers = $controller->get_subscriber_options( $type );\n\n\t\tforeach ( $subscribers as $i => $data ) {\n\n\t\t\t$sub_settings = array(\n\t\t\t\t'default' => $data['enabled'],\n\t\t\t\t'desc'    => $data['title'],\n\t\t\t\t'id'      => sprintf( '%1$s[%2$s]', $controller->get_option_name( $type . '_subscribers' ), $data['id'] ),\n\t\t\t\t'type'    => 'checkbox',\n\t\t\t);\n\n\t\t\tif ( 0 === $i ) {\n\t\t\t\t$sub_settings['title']         = __( 'Subscribers', 'lifterlms' );\n\t\t\t\t$sub_settings['checkboxgroup'] = 'start';\n\t\t\t} elseif ( count( $subscribers ) - 1 === $i ) {\n\t\t\t\t$sub_settings['checkboxgroup'] = 'end';\n\t\t\t} else {\n\t\t\t\t$sub_settings['checkboxgroup'] = 'middle';\n\t\t\t}\n\n\t\t\t$settings[] = $sub_settings;\n\n\t\t\tif ( 'custom' === $data['id'] ) {\n\t\t\t\t$settings[] = array(\n\t\t\t\t\t'desc' => $data['description'],\n\t\t\t\t\t'id'   => $controller->get_option_name( $type . '_custom_subscribers' ),\n\t\t\t\t\t'type' => 'text',\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Add additional controller options.\n\t\t$settings = array_merge( $settings, $controller->get_additional_options( $type ) );\n\n\t\tif ( $controller->is_testable( $type ) ) {\n\t\t\tforeach ( $controller->get_test_settings( $type ) as $setting ) {\n\t\t\t\t$setting['id'] = 'llms_notification_test_data[' . $setting['id'] . ']';\n\t\t\t\t$settings[]    = $setting;\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_notification_settings_' . $controller->id . '_' . $type, $settings, $controller, $view );\n\n\t}\n\n\t/**\n\t * Get settings array\n\t *\n\t * @since 3.8.0\n\t * @since 3.30.3 Fixed typo in title id.\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\n\t\t$settings = array();\n\n\t\t$settings[] = array(\n\t\t\t'class' => 'top',\n\t\t\t'id'    => 'notification_options',\n\t\t\t'type'  => 'sectionstart',\n\t\t);\n\n\t\t$settings[] = array(\n\t\t\t'title' => __( 'Notification Settings', 'lifterlms' ),\n\t\t\t'type'  => 'title',\n\t\t\t'id'    => 'notification_options_title',\n\t\t);\n\n\t\tif ( isset( $_GET['notification'] ) ) {\n\n\t\t\t$controller = llms()->notifications()->get_controller( llms_filter_input_sanitize_string( INPUT_GET, 'notification' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\n\t\t\tif ( $controller ) {\n\n\t\t\t\t$settings = array_merge( $settings, $this->get_notification_settings( $controller ) );\n\n\t\t\t} else {\n\n\t\t\t\t$settings[] = array(\n\t\t\t\t\t'id'    => 'notification_options_invalid_error',\n\t\t\t\t\t'type'  => 'custom-html',\n\t\t\t\t\t'value' => __( 'Invalid notification', 'lifterlms' ),\n\t\t\t\t);\n\n\t\t\t}\n\t\t} else {\n\n\t\t\t$settings[] = array(\n\t\t\t\t'id'    => 'llms_notifications_table',\n\t\t\t\t'table' => new LLMS_Table_NotificationSettings(),\n\t\t\t\t'type'  => 'table',\n\t\t\t);\n\n\t\t}\n\n\t\t$settings[] = array(\n\t\t\t'id'   => 'notification_options',\n\t\t\t'type' => 'sectionend',\n\t\t);\n\n\t\treturn apply_filters( 'lifterlms_notifications_settings', $settings );\n\n\t}\n\n\t/**\n\t * Disable save button on the main notification tab (list)\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param bool $bool Default display value (true).\n\t * @return boolean\n\t */\n\tpublic function maybe_disable_save( $bool ) {\n\n\t\treturn ( isset( $_GET['notification'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\n\t}\n\n\t/**\n\t * Output a merge code button in the WYSIWYG editor\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function merge_code_button() {\n\n\t\tllms_merge_code_button( $this->view->get_option_name( 'body' ), true, $this->view->get_merge_codes() );\n\n\t}\n\n\t/**\n\t * Remove test data from $_POST so that it wont be saved to the DB\n\t *\n\t * @since 3.24.0\n\t * @since 3.35.0 Verify nonce & Sanitize input data.\n\t *\n\t * @return void\n\t */\n\tpublic function before_save() {\n\n\t\tif ( ! isset( $_REQUEST['_wpnonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'lifterlms-settings' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isset( $_POST['llms_notification_test_data'] ) ) {\n\n\t\t\t$_POST['llms_notification_test_data_temp'] = wp_unslash( $_POST['llms_notification_test_data'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized\n\t\t\tunset( $_POST['llms_notification_test_data'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Send a test notification after notification data is saved\n\t *\n\t * @since 3.24.0\n\t * @since 3.35.0 Verify nonce & Sanitize input data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function after_save() {\n\n\t\tif ( ! isset( $_REQUEST['_wpnonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'lifterlms-settings' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isset( $_GET['notification'] ) && isset( $_GET['type'] ) && isset( $_POST['llms_notification_test_data_temp'] ) ) {\n\n\t\t\tif ( ! empty( $_POST['llms_notification_test_data_temp'] ) ) {\n\n\t\t\t\t$controller = llms()->notifications()->get_controller( llms_filter_input_sanitize_string( INPUT_GET, 'notification' ) );\n\n\t\t\t\t$controller->send_test(\n\t\t\t\t\tllms_filter_input_sanitize_string( INPUT_GET, 'type' ),\n\t\t\t\t\twp_unslash( $_POST['llms_notification_test_data_temp'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n\nreturn new LLMS_Settings_Notifications();\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.page.php",
    "content": "<?php\n/**\n * Admin Settings Page Base\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 1.0.0\n * @version 3.37.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page Base class\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 3.35.0 Unslash input data.\n * @since 3.37.3 Add a constructor which registers the settings page and automatically saves and outputs settings content.\n *               Add public method stub `get_label()` which is used to automatically set the `$label` property on class initialization.\n *               Add utility method to generate a group of settings.\n */\nclass LLMS_Settings_Page {\n\n\t/**\n\t * Allow settings page to determine if a rewrite flush is required\n\t *\n\t * @var boolean\n\t */\n\tprotected $flush = false;\n\n\t/**\n\t * Settings identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id = '';\n\n\t/**\n\t * Settings page label / title.\n\t *\n\t * @var string\n\t */\n\tpublic $label = '';\n\n\t/**\n\t * Tab priority\n\t *\n\t * Determines the order of the page when registered with the core settings array.\n\t *\n\t * @var int\n\t */\n\tpublic $tab_priority = 20;\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->label = $this->set_label();\n\n\t\tadd_filter( 'lifterlms_settings_tabs_array', array( $this, 'add_settings_page' ), $this->tab_priority );\n\n\t\tif ( $this->id ) {\n\n\t\t\tadd_action( 'lifterlms_settings_' . $this->id, array( $this, 'output' ) );\n\t\t\tadd_action( 'lifterlms_settings_save_' . $this->id, array( $this, 'save' ) );\n\n\t\t}\n\t}\n\n\t/**\n\t * Add the settings page\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function add_settings_page( $pages ) {\n\t\t$pages[ $this->id ] = $this->label;\n\t\treturn $pages;\n\t}\n\n\t/**\n\t * Flushes rewrite rules when necessary\n\t *\n\t * @since 3.0.4\n\t *\n\t * @return void\n\t */\n\tpublic function flush_rewrite_rules() {\n\n\t\t// Add the updated endpoints.\n\t\t$query = new LLMS_Query();\n\t\t$query->add_endpoints();\n\n\t\t// Flush rewrite rules.\n\t\tflush_rewrite_rules();\n\t}\n\n\t/**\n\t * Retrieve current section from URL var\n\t *\n\t * @since 3.17.5\n\t * @since 3.35.0 Unslash input data.\n\t *\n\t * @return string\n\t */\n\tprotected function get_current_section() {\n\t\treturn isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : 'main';\n\t}\n\n\t/**\n\t * Retrieve the page label.\n\t *\n\t * Extending classes should override this to return a translated string used as the page's title.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return string\n\t */\n\tprotected function set_label() {\n\t\treturn $this->id;\n\t}\n\n\t/**\n\t * Generates a group of settings.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @param string  $id Group ID. Used to create IDs for the start, end, and title fields.\n\t * @param string  $title Title of the group (should be translatable).\n\t * @param string  $title_desc (Optional) title field description text.\n\t * @param array[] $settings Array of settings field arrays.\n\t * @return array[]\n\t */\n\tprotected function generate_settings_group( $id, $title, $title_desc = '', $settings = array() ) {\n\n\t\t$start = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionstart',\n\t\t\t\t'id'   => $id,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'id'    => sprintf( '%s_title', $id ),\n\t\t\t\t'title' => $title,\n\t\t\t\t'desc'  => $title_desc,\n\t\t\t),\n\t\t);\n\n\t\t$end = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'sectionend',\n\t\t\t\t'id'   => sprintf( '%s_end', $id ),\n\t\t\t),\n\t\t);\n\n\t\treturn array_merge( $start, $settings, $end );\n\t}\n\n\t/**\n\t * Get the page sections (stub)\n\t *\n\t * When overriding, this should return an associative array where the key is the\n\t * section id and the value is the (translated) section title. The \"default\" tab\n\t * should always use the id \"main\".\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.5 Return an array instead of void.\n\t *\n\t * @return array\n\t */\n\tpublic function get_sections() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Retrieve the page's settings (stub)\n\t *\n\t * @since 3.17.5\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Output the settings fields\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.5 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\t\tLLMS_Admin_Settings::output_fields( $this->get_settings() );\n\t}\n\n\t/**\n\t * Output settings sections as tabs and set post href\n\t *\n\t * @since 3.17.5\n\t *\n\t * @return void\n\t */\n\tpublic function output_sections_nav() {\n\n\t\t$sections = $this->get_sections();\n\n\t\tif ( empty( $sections ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$curr = $this->get_current_section();\n\t\t?>\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-text\">\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t\t<?php foreach ( $sections as $key => $title ) : ?>\n\t\t\t\t\t<li class=\"llms-nav-item<?php echo ( $key === $curr ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t\t<a class=\"llms-nav-link\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-settings&tab=' . $this->id . '&section=' . $key ) ); ?>\"><?php echo esc_html( $title ); ?></a>\n\t\t\t\t\t</li>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t</ul>\n\t\t</nav>\n\t\t<?php\n\t}\n\n\t/**\n\t * Save the settings field values\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.5 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function save() {\n\n\t\tLLMS_Admin_Settings::save_fields( $this->get_settings() );\n\t\tif ( $this->flush ) {\n\t\t\tadd_action( 'shutdown', array( $this, 'flush_rewrite_rules' ) );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/admin/settings/class.llms.settings.security.php",
    "content": "<?php\n/**\n * Admin Settings Page, Security Tab\n *\n * @package LifterLMS/Admin/Settings/Classes\n *\n * @since 9.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin Settings Page, Security Tab class\n *\n * @since 9.0.0\n */\nclass LLMS_Settings_Security extends LLMS_Settings_Page {\n\n\t/**\n\t * Settings identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id = 'security';\n\n\t/**\n\t * Get settings array\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\t\t$account_settings = array(\n\t\t\tarray(\n\t\t\t\t'class' => 'top',\n\t\t\t\t'id'    => 'course_account_options',\n\t\t\t\t'type'  => 'sectionstart',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title' => __( 'Website Security & Spam Prevention', 'lifterlms' ),\n\t\t\t\t'type'  => 'title',\n\t\t\t\t'id'    => 'security_and_spam_options_title',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload'          => false,\n\t\t\t\t'default'           => '',\n\t\t\t\t'id'                => 'lifterlms_captcha',\n\t\t\t\t'desc'              => __( 'Choose a captcha service to require at checkout.', 'lifterlms' ),\n\t\t\t\t'title'             => __( 'Captcha', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t\t'options'           => array(\n\t\t\t\t\t''          => __( 'None', 'lifterlms' ),\n\t\t\t\t\t'recaptcha' => __( 'reCAPTCHA', 'lifterlms' ),\n\t\t\t\t\t'turnstile' => __( 'Turnstile', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'class'             => 'llms-conditional-controller',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-controls-recaptcha' => '#lifterlms_recaptcha_site_key,#lifterlms_recaptcha_secret_key,#lifterlms_recaptcha_min_score',\n\t\t\t\t\t'data-controls-turnstile' => '#lifterlms_turnstile_site_key,#lifterlms_turnstile_secret_key',\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => '',\n\t\t\t\t'id'       => 'lifterlms_recaptcha_site_key',\n\t\t\t\t'desc'     => 'Requires reCAPTCHA v3 keys. <a href=\"https://lifterlms.com/docs/recaptcha\" target=\"_blank\">Learn More</a>.',\n\t\t\t\t'title'    => __( 'reCAPTCHA v3 Site Key', 'lifterlms' ),\n\t\t\t\t'type'     => 'text',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => '',\n\t\t\t\t'id'       => 'lifterlms_recaptcha_secret_key',\n\t\t\t\t'desc'     => '',\n\t\t\t\t'title'    => __( 'reCAPTCHA v3 Secret Key', 'lifterlms' ),\n\t\t\t\t'type'     => 'text',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload'          => false,\n\t\t\t\t'default'           => '5',\n\t\t\t\t'id'                => 'lifterlms_recaptcha_min_score',\n\t\t\t\t'desc'              => 'The minimum score required for reCAPTCHA validation, from 1-10. Google recommends 5 as a default. <a href=\"https://lifterlms.com/docs/recaptcha\" target=\"_blank\">Learn More</a>.',\n\t\t\t\t'title'             => __( 'reCAPTCHA Minimum Score', 'lifterlms' ),\n\t\t\t\t'type'              => 'number',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'min' => '1',\n\t\t\t\t\t'max' => '10',\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => '',\n\t\t\t\t'id'       => 'lifterlms_turnstile_site_key',\n\t\t\t\t'desc'     => 'Requires Cloudflare Turnstile keys. <a href=\"https://lifterlms.com/docs/turnstile\" target=\"_blank\">Learn More</a>.',\n\t\t\t\t'title'    => __( 'Turnstile Site Key', 'lifterlms' ),\n\t\t\t\t'type'     => 'text',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => '',\n\t\t\t\t'id'       => 'lifterlms_turnstile_secret_key',\n\t\t\t\t'desc'     => '',\n\t\t\t\t'title'    => __( 'Turnstile Secret Key', 'lifterlms' ),\n\t\t\t\t'type'     => 'text',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => 'yes',\n\t\t\t\t'id'       => 'lifterlms_spam_protection',\n\t\t\t\t'desc'     => __( 'Block IPs from checkout if there are more than 10 failures within 15 minutes.', 'lifterlms' ),\n\t\t\t\t'title'    => __( 'Spam Protection', 'lifterlms' ),\n\t\t\t\t'type'     => 'checkbox',\n\t\t\t),\n\t\t);\n\n\t\tif ( LLMS_Akismet::instance()->is_available() ) {\n\t\t\t$account_settings[] = array(\n\t\t\t\t'autoload' => false,\n\t\t\t\t'default'  => 'no',\n\t\t\t\t'id'       => 'lifterlms_akismet_enabled',\n\t\t\t\t'desc'     => __( 'Enable Akismet spam protection.', 'lifterlms' ),\n\t\t\t\t'title'    => __( 'Akismet Spam Protection', 'lifterlms' ),\n\t\t\t\t'type'     => 'checkbox',\n\t\t\t);\n\t\t}\n\n\t\t$account_settings[] =\n\t\t\tarray(\n\t\t\t\t'id'   => 'security_and_spam_options_end',\n\t\t\t\t'type' => 'sectionend',\n\t\t\t);\n\n\t\t/**\n\t\t * Filters the account settings.\n\t\t *\n\t\t * The dynamic portion of this filter `{$this->id}` refers to the unique ID for the settings page.\n\t\t *\n\t\t * @since 9.0.0\n\t\t *\n\t\t * @param array $account_settings The account page settings.\n\t\t */\n\t\treturn apply_filters( \"lifterlms_{$this->id}_settings\", $account_settings );\n\t}\n\n\t/**\n\t * Retrieve the page label.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_label() {\n\t\treturn __( 'Security', 'lifterlms' );\n\t}\n}\n\nreturn new LLMS_Settings_Security();\n"
  },
  {
    "path": "includes/admin/settings/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/settings/tables/class.llms.table.notification.settings.php",
    "content": "<?php\n/**\n * Student Management table on Courses and Memberships\n *\n * @package LifterLMS/Admin/Settings/Tables/Classes\n *\n * @since 3.8.0\n * @version 3.10.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Table_NotificationSettings class\n *\n * Student Management table on Courses and Memberships.\n *\n * @since 3.8.0\n * @since 3.10.0 Unknown.\n */\nclass LLMS_Table_NotificationSettings extends LLMS_Admin_Table {\n\n\t/**\n\t * Unique ID for the Table\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'notifications';\n\n\t/**\n\t * If true will be a table with a larger font size\n\t *\n\t * @var  bool\n\t */\n\tprotected $is_large = true;\n\n\t/**\n\t * Retrieve data for the columns\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $key  The column id / key.\n\t * @param array  $data Table data array.\n\t * @return mixed\n\t */\n\tpublic function get_data( $key, $data ) {\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'configure':\n\t\t\t\t$links = array();\n\t\t\t\tforeach ( $data['configure'] as $type => $name ) {\n\t\t\t\t\t$url     = esc_url(\n\t\t\t\t\t\tadd_query_arg(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'notification' => $data['id'],\n\t\t\t\t\t\t\t\t'type'         => $type,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$links[] = '<a href=\"' . $url . '\">' . $name . '</a>';\n\t\t\t\t}\n\t\t\t\t$value = implode( ', ', $links );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$value = $data[ $key ];\n\n\t\t}\n\n\t\treturn $this->filter_get_data( $value, $key, $data );\n\n\t}\n\n\t/**\n\t * Execute a query to retrieve results from the table\n\t *\n\t * @param    array $args  array of query args\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.10.0\n\t */\n\tpublic function get_results( $args = array() ) {\n\n\t\t$rows = array();\n\n\t\tforeach ( llms()->notifications()->get_controllers() as $controller ) {\n\n\t\t\t$rows[] = array(\n\t\t\t\t'id'           => $controller->id,\n\t\t\t\t'notification' => $controller->get_title(),\n\t\t\t\t'configure'    => $controller->get_supported_types(),\n\t\t\t);\n\n\t\t}\n\n\t\tusort( $rows, array( $this, 'sort_rows' ) );\n\n\t\t$this->tbody_data = $rows;\n\t}\n\n\t/**\n\t * Define the structure of arguments used to pass to the get_results method\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function set_args() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Define the structure of the table\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function set_columns() {\n\t\t$cols = array(\n\t\t\t'notification' => __( 'Notification', 'lifterlms' ),\n\t\t\t'configure'    => __( 'Configure', 'lifterlms' ),\n\t\t);\n\n\t\treturn $cols;\n\t}\n\n\t/**\n\t * Sorting function to display all loaded notifications in alphabetical order\n\t *\n\t * @param    array $row_a  first row to compare\n\t * @param    array $row_b  second row to compare\n\t * @return   int\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tpublic function sort_rows( $row_a, $row_b ) {\n\t\treturn strcmp( $row_a['notification'], $row_b['notification'] );\n\t}\n\n}\n"
  },
  {
    "path": "includes/admin/settings/tables/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-batch-eraser.php",
    "content": "<?php\n/**\n * Admin tool to delete pending batches created by a background processor\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 3.37.19\n * @version 3.37.19\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Tool_Batch_Eraser\n *\n * @since 3.37.19\n */\nclass LLMS_Admin_Tool_Batch_Eraser extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'batch-eraser';\n\n\t/**\n\t * Retrieve a description of the tool\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\n\t\t$count = $this->get_pending_batches();\n\n\t\t$desc  = __( 'Deletes pending batches generated by LifterLMS background processors.', 'lifterlms' );\n\t\t$desc .= ' ';\n\t\t// Translators: %d = the number of pending batches.\n\t\t$desc .= sprintf(\n\t\t\t_n(\n\t\t\t\t'There is currently %d pending batch that will be deleted.',\n\t\t\t\t'There are currently %d pending batches that will be deleted.',\n\t\t\t\t$count,\n\t\t\t\t'lifterlms'\n\t\t\t),\n\t\t\t$count\n\t\t);\n\n\t\treturn $desc;\n\n\t}\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'Delete processor batches', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Delete batches', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the number of pending batches.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return int\n\t */\n\tprotected function get_pending_batches() {\n\n\t\t$count = wp_cache_get( $this->id, 'llms_tool_data' );\n\t\tif ( false === $count ) {\n\n\t\t\tglobal $wpdb;\n\t\t\t$count = absint( $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '%llms_%_batch_%';\" ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\n\t\t\twp_cache_set( $this->id, $count, 'llms_tool_data' );\n\n\t\t}\n\n\t\treturn $count;\n\n\t}\n\n\t/**\n\t * Process the tool.\n\t *\n\t * This method should do whatever the tool actually does.\n\t *\n\t * By the time this tool is called a nonce and the user's capabilities have already been checked.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return mixed\n\t */\n\tprotected function handle() {\n\n\t\tglobal $wpdb;\n\t\t$res = $wpdb->query( \"DELETE FROM {$wpdb->options} WHERE option_name LIKE '%llms_%_batch_%';\" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\twp_cache_delete( $this->id, 'llms_tool_data' );\n\t\treturn $res > 0;\n\n\t}\n\n\t/**\n\t * Conditionally load the tool\n\t *\n\t * This tool should only load if there's batches in the database.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return boolean Return `true` to load the tool and `false` to not load it.\n\t */\n\tprotected function should_load() {\n\n\t\treturn $this->get_pending_batches() > 0;\n\n\t}\n\n}\n\nreturn new LLMS_Admin_Tool_Batch_Eraser();\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-clear-sessions.php",
    "content": "<?php\n/**\n * Admin tool to delete pending batches created by a background processor\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 4.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Tool_Clear_Sessions\n *\n * @since 4.0.0\n */\nclass LLMS_Admin_Tool_Clear_Sessions extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'clear-sessions';\n\n\t/**\n\t * Retrieve a description of the tool\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\t\treturn __( 'LifterLMS user sessions store temporary data related to error messages and order information during payment processing. Stale sessions are automatically deleted. This tool can be used to delete all existing user sessions.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'User Sessions', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Clear All User Sessions', 'lifterlms' );\n\t}\n\n\t/**\n\t * Process the tool.\n\t *\n\t * This method should do whatever the tool actually does.\n\t *\n\t * By the time this tool is called a nonce and the user's capabilities have already been checked.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return mixed\n\t */\n\tprotected function handle() {\n\n\t\tdo_action( 'llms_delete_expired_session_data', false );\n\t\treturn true;\n\n\t}\n\n}\n\nreturn new LLMS_Admin_Tool_Clear_Sessions();\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-course-data-lock-eraser.php",
    "content": "<?php\n/**\n * Admin tool to delete locked created by the Course Data background processor.\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 9.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Tool_Course_Data_Lock_Eraser\n *\n * @since 9.0.0\n */\nclass LLMS_Admin_Tool_Course_Data_Lock_Eraser extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'course-data-eraser';\n\n\t/**\n\t * Retrieve a description of the tool.\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\n\t\t$count = $this->get_locked_courses();\n\n\t\t$desc  = __( 'Deletes locks generated by LifterLMS course data background processor.', 'lifterlms' );\n\t\t$desc .= ' ';\n\t\t// Translators: %d = the number of pending batches.\n\t\t$desc .= sprintf(\n\t\t\t_n(\n\t\t\t\t'There is currently %d lock that will be deleted.',\n\t\t\t\t'There are currently %d locks that will be deleted.',\n\t\t\t\t$count,\n\t\t\t\t'lifterlms'\n\t\t\t),\n\t\t\t$count\n\t\t);\n\n\t\treturn $desc;\n\t}\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'Delete course data locks', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Delete locks', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the number of course data locks.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return int\n\t */\n\tprotected function get_locked_courses() {\n\n\t\t$count = wp_cache_get( $this->id, 'llms_tool_data' );\n\t\tif ( false === $count ) {\n\n\t\t\tglobal $wpdb;\n\t\t\t$count = absint( $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_llms_temp_calc_data_lock';\" ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\n\t\t\twp_cache_set( $this->id, $count, 'llms_tool_data' );\n\n\t\t}\n\n\t\treturn $count;\n\t}\n\n\t/**\n\t * Process the tool.\n\t *\n\t * This method should do whatever the tool actually does.\n\t *\n\t * By the time this tool is called a nonce and the user's capabilities have already been checked.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return bool\n\t */\n\tprotected function handle() {\n\n\t\t$res = delete_post_meta_by_key( '_llms_temp_calc_data_lock' );\n\t\twp_cache_delete( $this->id, 'llms_tool_data' );\n\t\treturn $res;\n\t}\n\n\t/**\n\t * Conditionally load the tool.\n\t *\n\t * This tool should only load if there's locks in the database.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return boolean Return `true` to load the tool and `false` to not load it.\n\t */\n\tprotected function should_load() {\n\n\t\treturn $this->get_locked_courses() > 0;\n\t}\n}\n\nreturn new LLMS_Admin_Tool_Course_Data_Lock_Eraser();\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-install-forms.php",
    "content": "<?php\n/**\n * LLMS_Admin_Tool_Install_Forms class file\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin tool to reinstall / revert user information forms to their default states\n *\n * @since 5.0.0\n */\nclass LLMS_Admin_Tool_Install_Forms extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'install-forms';\n\n\t/**\n\t * Retrieve a description of the tool\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\t\treturn __( 'Restores LifterLMS user information forms and reusable field blocks to their default versions. Caution: any existing form and field customizations will be lost!', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'Reinstall User Forms', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Reinstall Forms', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieves a list of core reusable blocks ordered by their field ID.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return int[] List of the WP_Post IDs.\n\t */\n\tpublic function get_reusable_blocks() {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t'no_found_rows'  => true,\n\t\t\t\t'post_type'      => 'wp_block',\n\t\t\t\t'meta_key'       => '_llms_field_id',\n\t\t\t\t'meta_compare'   => 'EXISTS',\n\t\t\t\t'orderby'        => 'meta_value',\n\t\t\t)\n\t\t);\n\n\t\treturn wp_list_pluck( $query->posts, 'ID' );\n\n\t}\n\n\t/**\n\t * Process the tool.\n\t *\n\t * Deletes all core reusable blocks and then recreates the core forms,\n\t * which additionally recreates the core reusable blocks.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function handle() {\n\n\t\t// Retrieve original reusable blocks.\n\t\t$original_blocks = $this->get_reusable_blocks();\n\n\t\t// Delete them all.\n\t\tforeach ( $original_blocks as $id ) {\n\t\t\twp_delete_post( $id, true );\n\t\t}\n\n\t\t// Recreate the forms (and the blocks).\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\treturn true;\n\n\t}\n\n}\n\nreturn new LLMS_Admin_Tool_Install_Forms();\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-limited-billing-order-locator.php",
    "content": "<?php\n/**\n * LLMS_Admin_Tool_Limited_Billing_Order_Locator class.\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 5.3.0\n * @version 5.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin tool which generates a report of limited billing orders affected by order end changes.\n *\n * @since 5.3.0\n *\n * @link https://github.com/gocodebox/lifterlms/pull/1744\n */\nclass LLMS_Admin_Tool_Limited_Billing_Order_Locator extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'limited-billing-order-locator';\n\n\t/**\n\t * Query the database for orders that may be affected by the change.\n\t *\n\t * @since 5.3.0\n\t * @since 5.4.0 Retrieve orders ordered by their unique ID (DESC) instead of the default `date_created`.\n\t *\n\t * @return array[] Returns an array of arrays where each array represents a line in the generated CSV file.\n\t */\n\tprotected function generate_csv() {\n\n\t\t$csv = array();\n\n\t\t$orders = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'llms_order',\n\t\t\t\t'post_status'    => array( 'llms-active', 'llms-on-hold' ),\n\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t'orderby'        => 'ID',\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => '_llms_billing_length',\n\t\t\t\t\t\t'value'   => 1,\n\t\t\t\t\t\t'compare' => '>=',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => '_llms_date_billing_end',\n\t\t\t\t\t\t'compare' => 'EXISTS',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $orders->posts as $order ) {\n\n\t\t\t$order = llms_get_post( $order );\n\t\t\tif ( ! $order || ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$csv[] = $this->get_order_csv( $order );\n\n\t\t}\n\n\t\treturn array_filter( $csv );\n\n\t}\n\n\t/**\n\t * Create a csv \"file\" via output buffering and return it as a string.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_csv_file() {\n\n\t\t$csv = $this->get_csv();\n\n\t\t// Add header row.\n\t\tarray_unshift(\n\t\t\t$csv,\n\t\t\tarray(\n\t\t\t\t'Order ID',\n\t\t\t\t'Expected Payments',\n\t\t\t\t'Total Payments',\n\t\t\t\t'Successful Payments',\n\t\t\t\t'Refunded Payments',\n\t\t\t\t'Edit Link',\n\t\t\t)\n\t\t);\n\n\t\t// Create the CSV file.\n\t\tob_start();\n\t\t$fh = fopen( 'php://output', 'w' );\n\t\tforeach ( $csv as $line ) {\n\t\t\tfputcsv( $fh, $line );\n\t\t}\n\t\tfclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose\n\n\t\treturn ob_get_clean();\n\n\t}\n\n\t/**\n\t * Retrieve a description of the tool.\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\n\t\t$count = count( $this->get_csv() );\n\n\t\t$desc = sprintf(\n\t\t\t// Translators: %1$s = opening anchor link to documentation; %2$s = closing anchor link.\n\t\t\t__( 'The method used to determine when a limited-billing recurring order has completed its payment plan changed during version 5.3.0. This tool provides a report of orders which may been affected by this change. %1$sRead more%2$s about this change.', 'lifterlms' ),\n\t\t\t'<a href=\"https://lifterlms.com/docs/payment-plan-orders-530/\" target=\"_blank\">',\n\t\t\t'</a>'\n\t\t);\n\t\t$desc .= ' ';\n\t\t// Translators: %d = the number of pending batches.\n\t\t$desc .= sprintf(\n\t\t\t_n(\n\t\t\t\t'There is %d order that should be reviewed.',\n\t\t\t\t'There are %d orders that should be reviewed.',\n\t\t\t\t$count,\n\t\t\t\t'lifterlms'\n\t\t\t),\n\t\t\t$count\n\t\t);\n\n\t\treturn $desc;\n\n\t}\n\n\t/**\n\t * Retrieve the tool's label.\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'Limited Billing Orders', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieves an array representing a line in generated CSV for the given order.\n\t *\n\t * An order is considered to be affected by the change if either of the following conditions are true:\n\t *\n\t *   + Any number of refunds exist for the order. Since we are now counting refunds towards the billing limit\n\t *     an active order with any number of refunds should be reviewed by the admin.\n\t *\n\t *   + The plan is marked as \"ended\" and the total number of successful payments is not equal to the billing length.\n\t *     In this scenario admins will likely want to add additional payments and start the order again.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param LLMS_Order $order The order object.\n\t * @return array Array of information on the order to be stored in the generated CSV or an empty array of the order\n\t *               was not affected by the change.\n\t */\n\tprotected function get_order_csv( $order ) {\n\n\t\t$refunds   = $this->get_txn_count_by_status( $order, 'llms-txn-refunded' );\n\t\t$successes = $this->get_txn_count_by_status( $order, 'llms-txn-succeeded' );\n\t\t$total     = $refunds + $successes;\n\n\t\t$ended    = llms_parse_bool( $order->get( 'plan_ended' ) );\n\t\t$expected = $order->get( 'billing_length' );\n\n\t\tif ( $refunds >= 1 || ( $ended && $total !== $expected ) ) {\n\n\t\t\t$id   = $order->get( 'id' );\n\t\t\t$link = get_edit_post_link( $id, 'raw' );\n\t\t\treturn array( $id, $expected, $total, $successes, $refunds, $link );\n\n\t\t}\n\n\t\treturn array();\n\n\t}\n\n\t/**\n\t * Helper to get the number of transactions on an order for a given status.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param LLMS_Order $order  The order object.\n\t * @param string     $status Transaction post status to query by.\n\t * @return int Number of transactions for the requested status.\n\t */\n\tprotected function get_txn_count_by_status( $order, $status ) {\n\n\t\t$txns = $order->get_transactions(\n\t\t\tarray(\n\t\t\t\t'per_page' => 1,\n\t\t\t\t'status'   => array( $status ),\n\t\t\t\t'type'     => array( 'recurring', 'single' ), // If a manual payment is recorded it's counted a single payment and that should count.\n\t\t\t)\n\t\t);\n\n\t\treturn $txns['total'];\n\n\t}\n\n\t/**\n\t * Retrieve the tool's button text.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Download CSV', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve a list of orders.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return int[]\n\t */\n\tprotected function get_csv() {\n\n\t\t$csv = wp_cache_get( $this->id, 'llms_tool_data' );\n\t\tif ( ! $csv ) {\n\t\t\t$csv = $this->generate_csv();\n\t\t\twp_cache_set( $this->id, $csv, 'llms_tool_data' );\n\t\t}\n\n\t\treturn $csv;\n\n\t}\n\n\t/**\n\t * Generate the CSV file an serve it as a downloadable attachment.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tprotected function handle() {\n\n\t\t$file = $this->get_csv_file();\n\n\t\tif ( ! headers_sent() ) { // This makes the method testable via phpunit.\n\t\t\theader( 'Content-Type: text/csv' );\n\t\t\theader( 'Content-Disposition: attachment; filename=orders.csv' );\n\t\t\theader( 'Content-Length: ' . strlen( $file ) );\n\t\t\tnocache_headers();\n\t\t}\n\n\t\tllms_exit( $file );\n\n\t}\n\n\t/**\n\t * Conditionally load the tool.\n\t *\n\t * This tool should only load if there are orders that can be handled by the tool.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return boolean Return `true` to load the tool and `false` to not load it.\n\t */\n\tprotected function should_load() {\n\t\treturn count( $this->get_csv() ) > 0;\n\t}\n\n}\n\nreturn new LLMS_Admin_Tool_Limited_Billing_Order_Locator();\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-recurring-payment-rescheduler.php",
    "content": "<?php\n/**\n * Admin tool used to automatically reschedule recurring orders missing a pending scheduled payment action\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 4.6.0\n * @version 4.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Tool_Recurring_Payment_Rescheduler class\n *\n * @since 4.6.0\n */\nclass LLMS_Admin_Tool_Recurring_Payment_Rescheduler extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'recurring-payment-rescheduler';\n\n\t/**\n\t * Retrieve a description of the tool\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 4.6.0\n\t * @since 4.7.0 Modified language and use count from `FOUND_ROWS()`.\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\n\t\t$orders = $this->get_orders();\n\t\t$count  = wp_cache_get( sprintf( '%s-total-results', $this->id ), 'llms_tool_data' );\n\n\t\t$desc  = __( 'Check active recurring orders to ensure their recurring payment action is properly scheduled for the next payment. If a recurring payment is due and not scheduled it will be rescheduled.', 'lifterlms' );\n\t\t$desc .= ' ';\n\t\t// Translators: %d = the number of pending batches.\n\t\t$desc .= sprintf(\n\t\t\t_n(\n\t\t\t\t'There is %d order that will be checked.',\n\t\t\t\t'There are %d orders that will be checked in batches of 50.',\n\t\t\t\t$count,\n\t\t\t\t'lifterlms'\n\t\t\t),\n\t\t\t$count\n\t\t);\n\n\t\treturn $desc;\n\n\t}\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'Reschedule Recurring Payments', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Reschedule Payments', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve a list of orders\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return int[]\n\t */\n\tprotected function get_orders() {\n\n\t\t$orders = wp_cache_get( $this->id, 'llms_tool_data' );\n\t\tif ( ! $orders ) {\n\t\t\t$orders = wp_list_pluck( $this->query_orders(), 'ID' );\n\t\t\twp_cache_set( $this->id, $orders, 'llms_tool_data' );\n\t\t}\n\n\t\treturn $orders;\n\n\t}\n\n\t/**\n\t * Schedules payments and expiration for an order\n\t *\n\t * Retrieves orders from the `get_orders()` method and schedules a recurring payment\n\t * and expiration action based on its existing calculated order data.\n\t *\n\t * @since 4.6.0\n\t * @since 4.7.0 Set `plan_ended` metadata for orders with an ended plan and don't attempt to process them.\n\t *\n\t * @return int[] Returns an array of WP_Post IDs for orders successfully rescheduled by the method.\n\t */\n\tprotected function handle() {\n\n\t\t$orders = array();\n\n\t\tforeach ( $this->get_orders() as $id ) {\n\t\t\t$order = llms_get_post( $id );\n\t\t\t$next  = $order->get_next_payment_due_date();\n\t\t\tif ( is_wp_error( $next ) && 'plan-ended' === $next->get_error_code() ) {\n\t\t\t\t$order->set( 'plan_ended', 'yes' );\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$order->maybe_schedule_payment( false );\n\t\t\t$order->maybe_schedule_expiration();\n\t\t\tif ( $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) ) {\n\t\t\t\t$orders[] = $id;\n\t\t\t}\n\t\t}\n\n\t\twp_cache_delete( $this->id, 'llms_tool_data' );\n\t\twp_cache_delete( sprintf( '%s-total-results', $this->id ), 'llms_tool_data' );\n\n\t\treturn $orders;\n\n\t}\n\n\t/**\n\t * Perform a DB query for orders to be handled by the tool\n\t *\n\t * @since 4.6.0\n\t * @since 4.7.0 Added `SQL_CALC_FOUND_ROWS` and improved query to exclude results with a completed payment plan.\n\t * @since 10.0.0 Replaced SQL_CALC_FOUND_ROWS with a separate COUNT(*) query.\n\t *\n\t * @return object[]\n\t */\n\tprotected function query_orders() {\n\n\t\tglobal $wpdb;\n\n\t\t$from_joins_where = \"FROM {$wpdb->posts} AS p\n\t\t  LEFT JOIN {$wpdb->postmeta} AS m\n\t\t\t     ON p.ID = m.post_ID\n\t\t\t    AND m.meta_key = '_llms_plan_ended'\n\t\t  LEFT JOIN {$wpdb->prefix}actionscheduler_actions AS a\n\t\t\t     ON a.args   = CONCAT( '{\\\"order_id\\\":', p.ID, '}' )\n\t\t\t    AND a.hook   = 'llms_charge_recurring_payment'\n\t\t\t    AND a.status = 'pending'\n\t\t\t  WHERE 1\n\t\t\t    AND p.post_type   = 'llms_order'\n\t\t\t    AND p.post_status = 'llms-active'\n\t\t\t    AND a.action_id IS NULL\n\t\t\t    AND m.meta_value IS NULL\";\n\n\t\t$total = $wpdb->get_var( \"SELECT COUNT(*) {$from_joins_where}\" ); // no-cache ok.\n\t\twp_cache_set( sprintf( '%s-total-results', $this->id ), $total, 'llms_tool_data' );\n\n\t\t$orders = $wpdb->get_results(\n\t\t\t\"SELECT p.ID\n\t\t\t   {$from_joins_where}\n\t\t   ORDER BY p.ID ASC\n\t\t\t  LIMIT 50\n\t\t\t;\"\n\t\t); // no-cache ok -- Caching implemented in `get_orders()`.\n\n\t\treturn $orders;\n\n\t}\n\n\t/**\n\t * Conditionally load the tool\n\t *\n\t * This tool should only load if there are orders that can be handled by the tool.\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return boolean Return `true` to load the tool and `false` to not load it.\n\t */\n\tprotected function should_load() {\n\t\treturn count( $this->get_orders() ) > 0;\n\t}\n\n}\n\nreturn new LLMS_Admin_Tool_Recurring_Payment_Rescheduler();\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-reset-automatic-payments.php",
    "content": "<?php\n/**\n * Admin tool to reset the status of the recurring payments site feature\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 4.13.0\n * @version 4.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Admin_Tool_Reset_Automatic_Payments\n *\n * @since 4.13.0\n */\nclass LLMS_Admin_Tool_Reset_Automatic_Payments extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'automatic-payments';\n\n\t/**\n\t * Tool Load Priority\n\t *\n\t * To preserve the \"original\" tool order, load this before unclassed core tools.\n\t *\n\t * @var integer\n\t */\n\tprotected $priority = 4;\n\n\t/**\n\t * Retrieve a description of the tool\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\t\treturn __( 'Allows you to choose to enable or disable automatic recurring payments which may be disabled on a staging site.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'Reset Automatic Payments Status', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Reset Automatic Payments Status', 'lifterlms' );\n\t}\n\n\t/**\n\t * Process the tool.\n\t *\n\t * This method should do whatever the tool actually does.\n\t *\n\t * By the time this tool is called a nonce and the user's capabilities have already been checked.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tprotected function handle() {\n\n\t\tLLMS_Site::clear_lock_url();\n\t\tupdate_option( 'llms_site_url_ignore', 'no' );\n\t\tLLMS_Site::check_status();\n\t\tllms_redirect_and_exit( esc_url_raw( admin_url( 'admin.php?page=llms-status&tab=tools' ) ) );\n\n\t}\n\n\t/**\n\t * Conditionally load the tool\n\t *\n\t * This tool should only load if the recurring payments site feature constant and the site clone status\n\t * constant are both NOT set.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return boolean Return `true` to load the tool and `false` to not load it.\n\t */\n\tprotected function should_load() {\n\n\t\treturn ! defined( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS' ) && ! defined( 'LLMS_SITE_IS_CLONE' );\n\n\t}\n\n}\n\nreturn new LLMS_Admin_Tool_Reset_Automatic_Payments();\n"
  },
  {
    "path": "includes/admin/tools/class-llms-admin-tool-wipe-legacy-account-options.php",
    "content": "<?php\n/**\n * LLMS_Admin_Tool_Wipe_Legacy_Account_Options\n *\n * @package LifterLMS/Admin/Tools/Classes\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Admin tool to wipe legacy account options\n *\n * @since 5.0.0\n */\nclass LLMS_Admin_Tool_Wipe_Legacy_Account_Options extends LLMS_Abstract_Admin_Tool {\n\n\t/**\n\t * Tool ID\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'wipe-legacy-account-options';\n\n\t/**\n\t * Skip cache when checking if should load\n\t *\n\t * @var boolean\n\t */\n\tprivate $skip_cache = false;\n\n\t/**\n\t * Retrieve a description of the tool\n\t *\n\t * This is displayed on the right side of the tool's list before the button.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description() {\n\t\treturn __( 'Removes all options used to control the visibility of user information fields prior to version 5.0. Since version 5.0 these options are only used when restoring forms to their original default values.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's label\n\t *\n\t * The label is the tool's title. It's displayed in the left column on the tool's list.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label() {\n\t\treturn __( 'Delete Legacy User Information Options', 'lifterlms' );\n\t}\n\n\t/**\n\t * Retrieve the tool's button text\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_text() {\n\t\treturn __( 'Delete Legacy Options', 'lifterlms' );\n\t}\n\n\t/**\n\t * Process the tool.\n\t *\n\t * Deletes all core reusable blocks and then recreates the core forms,\n\t * which additionally recreates the core reusable blocks.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function handle() {\n\n\t\t$options_to_wipe = array(\n\t\t\t'lifterlms_registration_generate_username',\n\t\t\t'lifterlms_registration_password_strength',\n\t\t\t'lifterlms_registration_password_min_strength',\n\t\t\t'lifterlms_user_info_field_names_checkout_visibility',\n\t\t\t'lifterlms_user_info_field_address_checkout_visibility',\n\t\t\t'lifterlms_user_info_field_phone_checkout_visibility',\n\t\t\t'lifterlms_user_info_field_email_confirmation_checkout_visibility',\n\t\t\t'lifterlms_user_info_field_names_registration_visibility',\n\t\t\t'lifterlms_user_info_field_address_registration_visibility',\n\t\t\t'lifterlms_user_info_field_phone_registration_visibility',\n\t\t\t'lifterlms_user_info_field_email_confirmation_registration_visibility',\n\t\t\t'lifterlms_voucher_field_registration_visibility',\n\t\t\t'lifterlms_user_info_field_names_account_visibility',\n\t\t\t'lifterlms_user_info_field_address_account_visibility',\n\t\t\t'lifterlms_user_info_field_phone_account_visibility',\n\t\t\t'lifterlms_user_info_field_email_confirmation_account_visibility',\n\t\t);\n\n\t\tglobal $wpdb;\n\n\t\t$sql = \"\n\t\tDELETE FROM {$wpdb->options}\n\t\tWHERE option_name IN (\" . implode( ', ', array_fill( 0, count( $options_to_wipe ), '%s' ) ) . ')';\n\n\t\t$wpdb->query(\n\t\t\t$wpdb->prepare(\n\t\t\t\t$sql,  // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared\n\t\t\t\t$options_to_wipe\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t\t$this->skip_cache = true;\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Conditionally load the tool\n\t *\n\t * This tool should only load if there are legacy options (we only check 'lifterlms_registration_generate_username').\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return boolean Return `true` to load the tool and `false` to not load it.\n\t */\n\tprotected function should_load() {\n\n\t\tif ( $this->skip_cache ) {\n\t\t\tglobal $wpdb;\n\n\t\t\treturn ! empty(\n\t\t\t\t$wpdb->get_var(\n\t\t\t\t\t\"SELECT COUNT(*) FROM {$wpdb->options}\n\t\t\t\tWHERE option_name='lifterlms_registration_generate_username'\"\n\t\t\t\t)\n\t\t\t); // no-cache ok.\n\n\t\t}\n\n\t\treturn ( 'not-set' !== get_option( 'lifterlms_registration_generate_username', 'not-set' ) );\n\n\t}\n}\n\nreturn new LLMS_Admin_Tool_Wipe_Legacy_Account_Options();\n"
  },
  {
    "path": "includes/admin/tools/index.php",
    "content": "<?php // Quiet.\n"
  },
  {
    "path": "includes/admin/views/access-plans/access-plan-dialog.php",
    "content": "<div\n\tid=\"llms-access-plan-dialog\"\n\tclass=\"llms-dialog-container\"\n\taria-labelledby=\"llms-access-plan-dialog-title\"\n\taria-hidden=\"true\"\n>\n\t<!-- 2. The dialog overlay -->\n\t<div class=\"llms-dialog-overlay\" data-a11y-dialog-hide></div>\n\t<!-- 3. The actual dialog -->\n\t<div class=\"llms-dialog-content\" role=\"document\">\n\t\t<button class=\"llms-dialog-close\" type=\"button\" data-a11y-dialog-hide aria-label=\"<?php echo esc_html( __( 'Close', 'lifterlms' ) ); ?>\">\n\t\t\t&times;\n\t\t</button>\n\t\t<h1 id=\"llms-access-plan-dialog-title\"><?php echo esc_html( __( 'What type of Access Plan do you want to create?', 'lifterlms' ) ); ?></h1>\n\n\t\t<div class=\"llms-access-plan-templates\">\n\t\t\t<button class=\"template\" data-template=\"free\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Free', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Free access that never expires.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"monthly\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Monthly', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Charge a recurring monthly subscription that never ends.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"annual\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Annual', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Charge a recurring annual subscription that never ends.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"one-time\">\n\t\t\t\t<strong><?php echo esc_html( __( 'One Time', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Charge a one-time payment for a fixed period.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"lifetime\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Lifetime', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Charge a one-time payment that never expires.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"paid-trial\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Paid Trial', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Charge a fee for trial access and capture recurring payment info with a future monthly subscription that will start in 1 week.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"hidden-access\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Hidden Access', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Grant free access without making this plan publicly available.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"sale\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Sale', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Discount a one-time payment for lifetime access.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<button class=\"template\" data-template=\"presell\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Pre-sale', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Offer lifetime access for a one-time payment with a future start date.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's dialog box pre-sale option.\n\t\t\t *\n\t\t\t * @since 8.0.0\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_dialog_after_pre_sale' );\n\t\t\t?>\n\t\t\t<?php if ( apply_filters( 'llms_access_plan_dialog_show_gifts_addon_option', true ) ) : ?>\n\t\t\t\t<a target=\"_blank\" href=\"https://lifterlms.com/product/lifterlms-gifts/?utm_source=LifterLMS%20Plugin&utm_medium=Access%20Plans&utm_campaign=Plugin%20to%20Sale\">\n\t\t\t\t\t<span class=\"add-on\"><?php echo esc_html( __( 'Add-on', 'lifterlms' ) ); ?></span>\n\t\t\t\t\t<strong><?php echo esc_html( __( 'Gift Purchases', 'lifterlms' ) ); ?></strong>\n\t\t\t\t\t<span><?php echo esc_html( __( 'Allow a buyer to purchase a voucher to gift access to someone else.', 'lifterlms' ) ); ?></span>\n\t\t\t\t</a>\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( apply_filters( 'llms_access_plan_dialog_show_group_addon_option', true ) ) : ?>\n\t\t\t\t<a target=\"_blank\" href=\"https://lifterlms.com/product/groups/?utm_source=LifterLMS%20Plugin&utm_medium=Access%20Plans&utm_campaign=Plugin%20to%20Sale\">\n\t\t\t\t\t<span class=\"add-on\"><?php echo esc_html( __( 'Add-on', 'lifterlms' ) ); ?></span>\n\t\t\t\t\t<strong><?php echo esc_html( __( 'Group Access', 'lifterlms' ) ); ?></strong>\n\t\t\t\t\t<span><?php echo esc_html( __( 'Allow a buyer to purchase lifetime access for a group of people.', 'lifterlms' ) ); ?></span>\n\t\t\t\t</a>\n\t\t\t<?php endif; ?>\n\t\t\t<button class=\"template\" data-template=\"advanced\">\n\t\t\t\t<strong><?php echo esc_html( __( 'Advanced', 'lifterlms' ) ); ?></strong>\n\t\t\t\t<span><?php echo esc_html( __( 'Show all settings to create an access plan from scratch.', 'lifterlms' ) ); ?></span>\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/admin/views/access-plans/access-plan.php",
    "content": "<?php\n/**\n * Individual Access Plan as displayed within the \"Product Options\" metabox.\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 3.0.0\n * @since 3.30.0 Added checkout redirect settings.\n * @since 3.31.0 Change sale_price input from text to number to ensure min value validation is properly enforced by browsers.\n * @since 3.37.18 Don't localize the price \"step\" html attribute.\n * @since 4.14.0 Get the access plan's raw content to display it in the wp_editor.\n * @since 7.3.0 Added another icon for possible issues with the access plan configuration.\n * @version 7.3.0\n *\n * @var LLMS_Course      $course                     LLMS_Course.\n * @var array            $checkout_redirection_types Checkout redirect setting options.\n * @var LLMS_Access_Plan $plan                       LLMS_Access_Plan.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// Create a \"step\" attribute for price fields according to LLMS settings.\n$price_step = number_format( 0.01, get_lifterlms_decimals() );\n\nif ( ! isset( $plan ) ) {\n\n\t$id    = 'llms-new-access-plan-model';\n\t$plan  = false;\n\t$order = 777;\n\n} else {\n\n\t$id                    = 'llms-access-plan-' . $plan->get( 'id' );\n\t$order                 = $plan->get( 'menu_order' );\n\t$visibility            = $plan->get_visibility();\n\t$frequency             = $plan->get( 'frequency' );\n\t$period                = $plan->get( 'period' );\n\t$access_expiration     = $plan->get( 'access_expiration' );\n\t$access_period         = $plan->get( 'access_period' );\n\t$trial_offer           = $plan->get( 'trial_offer' );\n\t$on_sale               = $plan->get( 'on_sale' );\n\t$availability          = $plan->get( 'availability' );\n\t$checkout_redirect_url = $plan->get( 'checkout_redirect_url' );\n\t$checkout_url          = apply_filters( 'llms_admin_plan_display_checkout_url', $plan->get_checkout_url( false ), $plan );\n\tif ( $checkout_url && false === strpos( $checkout_url, home_url() ) ) {\n\t\t$checkout_url = home_url( $checkout_url );\n\t}\n}\n?>\n\n<div class=\"llms-metabox-section d-all llms-collapsible llms-access-plan\" id=\"<?php echo esc_attr( $id ); ?>\"<?php echo $plan ? 'data-id=\"' . esc_attr( $plan->get( 'id' ) ) . '\"' : ''; ?>>\n\n\t<header class=\"llms-collapsible-header\">\n\t\t<div class=\"d-2of3\">\n\t\t\t<span class=\"fa fa-bars llms-drag-handle\"></span>\n\t\t\t<h3>\n\t\t\t\t<?php if ( $plan ) : ?>\n\t\t\t\t\t<span class=\"llms-plan-title\" data-default=\"<?php esc_attr_e( 'Unnamed Access Plan', 'lifterlms' ); ?>\"><?php echo esc_html( $plan->get( 'title' ) ); ?></span>\n\t\t\t\t\t<small>(<?php printf( esc_html_x( 'ID# %s', 'Product Access Plan ID', 'lifterlms' ), esc_html( $plan->get( 'id' ) ) ); ?>)</small>\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<span class=\"llms-plan-title\" data-default=\"<?php esc_attr_e( 'New Access Plan', 'lifterlms' ); ?>\"><?php esc_html_e( 'New Access Plan', 'lifterlms' ); ?></span>\n\t\t\t\t<?php endif; ?>\n\t\t\t</h3>\n\t\t</div>\n\t\t<div class=\"d-1of3 d-right\">\n\t\t\t<span class=\"tip--top-left\" data-tip=\"<?php esc_attr_e( 'This access plan requires attention for possible misconfigurations', 'lifterlms' ); ?>\">\n\t\t\t\t<span class=\"dashicons dashicons-warning medium-danger\"></span>\n\t\t\t</span>\n\t\t\t<span class=\"tip--top-left\" data-tip=\"<?php esc_attr_e( 'Errors were found during access plan validation', 'lifterlms' ); ?>\">\n\t\t\t\t<span class=\"dashicons dashicons-warning\"></span>\n\t\t\t</span>\n\t\t\t<?php if ( $plan ) : ?>\n\t\t\t\t<a target=\"_blank\" href=\"<?php echo esc_url( $checkout_url ); ?>\"><span class=\"dashicons dashicons-admin-links llms-plan-purchase-link\"></span></a>\n\t\t\t<?php endif; ?>\n\t\t\t<span class=\"dashicons dashicons-trash llms-plan-delete\"></span>\n\t\t\t<span class=\"dashicons dashicons-arrow-down\"></span>\n\t\t\t<span class=\"dashicons dashicons-arrow-up\"></span>\n\t\t</div>\n\t</header>\n\n\t<section class=\"llms-collapsible-body\">\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired before access plan's meta box row two\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_before_body', $plan, $id, $order );\n\t\t?>\n\n\t\t<h4><?php esc_html_e( 'General Plan Information', 'lifterlms' ); ?></h4>\n\n\t\t<?php\n\t\tif ( $plan && ! $checkout_url ) :\n\t\t\t?>\n\n\t\t\t<div>\n\n\t\t\t\t<div class=\"d-all\">\n\n\t\t\t\t\t<div class=\"notice notice-error inline llms-admin-notice llms-notice\">\n\n\t\t\t\t\t\t<div class=\"llms-admin-notice-content\">\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\techo wp_kses_post(\n\t\t\t\t\t\t\t\tsprintf(\n\t\t\t\t\t\t\t\t\t/* translators: %1$s - Open anchor tag to the checkout settings page, %2$s - Closing of anchor tag. */\n\t\t\t\t\t\t\t\t\t__( 'Your site does not have a checkout page configured. Configure a Checkout Page in the %1$sCheckout Settings%2$s.', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\t'<a href=\"' . esc_url( admin_url( 'admin.php?page=llms-settings&tab=checkout' ) ) . '\">',\n\t\t\t\t\t\t\t\t\t'</a>'\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t</div>\n\n\t\t\t\t</div>\n\n\t\t\t</div>\n\n\t\t<?php endif; ?>\n\n\t\t<?php if ( $plan && $checkout_url ) : ?>\n\t\t\t<p class=\"llms-plan-link\"><?php printf( esc_html__( 'Direct to Checkout Purchase Link: %s', 'lifterlms' ), '<code>' . esc_url( $checkout_url ) . '</code>' ); ?></p>\n\t\t<?php endif; ?>\n\n\t\t<div class=\"llms-plan-row-1\">\n\n\t\t\t<div class=\"llms-metabox-field d-1of2\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][title]\">\n\t\t\t\t\t<?php esc_html_e( 'Plan Title', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"llms-required\">*<span class=\"screen-reader-text\"> <?php esc_html_e( 'required', 'lifterlms' ); ?></span></span>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'The title of the access plan, displayed to users at the top of the plan.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'The title of the access plan, displayed to users at the top of the plan.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][title]\" class=\"llms-plan-title\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][title]\" required=\"required\" type=\"text\"<?php echo ( $plan ? ' value=\"' . esc_attr( $plan->get( 'title' ) ) . '\"' : ' disabled=\"disabled\"' ); ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][enroll_text]\">\n\t\t\t\t\t<?php esc_html_e( 'Enroll Button Text', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'The text displayed on the enrollment button for this access plan.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'The text displayed on the enrollment button for this access plan.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][enroll_text]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][enroll_text]\" type=\"text\"<?php echo ( $plan ) ? ' value=\"' . esc_attr( $plan->get( 'enroll_text' ) ) . '\"' : ' value=\"' . esc_attr__( 'Enroll Now', 'lifterlms' ) . '\" disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][visibility]\">\n\t\t\t\t\t<?php esc_html_e( 'Visibility', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Set whether this access plan is visible, hidden, or featured in the pricing table.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-left\" data-tip=\"<?php esc_attr_e( 'Set whether this access plan is visible, hidden, or featured in the pricing table.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][visibility]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][visibility]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t<?php foreach ( llms_get_access_plan_visibility_options() as $val => $name ) : ?>\n\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $val ); ?>\"<?php selected( $val, ( $plan ) ? $visibility : null ); ?>><?php echo esc_attr( $name ); ?></option>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</select>\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's meta box row two\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_after_row_one', $plan, $id, $order );\n\t\t?>\n\n\t\t<div class=\"llms-plan-row-2\">\n\n\t\t\t<div class=\"llms-metabox-field d-all\">\n\t\t\t\t<label for=\"_llms_plans_content_<?php echo esc_attr( $id ); ?>\">\n\t\t\t\t\t<?php esc_html_e( 'Plan Description', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Description text of the access plan shown on the pricing table. Bullet points of top plan benefits are often used here. ', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Description text of the access plan shown on the pricing table. Bullet points of top plan benefits are often used here. ', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<?php\n\t\t\t\twp_editor(\n\t\t\t\t\thtmlspecialchars_decode( $plan ? $plan->get( 'content', true ) : '' ),\n\t\t\t\t\t'_llms_plans_content_' . $id,\n\t\t\t\t\t/**\n\t\t\t\t\t * Filters the access plan editor settings\n\t\t\t\t\t *\n\t\t\t\t\t * @since Unknown\n\t\t\t\t\t *\n\t\t\t\t\t * @param array $settings See _WP_Editors::parse_settings() for description.\n\t\t\t\t\t */\n\t\t\t\t\tapply_filters(\n\t\t\t\t\t\t'llms_access_plan_editor_settings',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'drag_drop_upload' => true,\n\t\t\t\t\t\t\t'editor_height'    => 60,\n\t\t\t\t\t\t\t'media_buttons'    => false,\n\t\t\t\t\t\t\t'teeny'            => true,\n\t\t\t\t\t\t\t'textarea_name'    => '_llms_plans[' . $order . '][content]',\n\t\t\t\t\t\t\t'quicktags'        => array(\n\t\t\t\t\t\t\t\t'buttons' => 'strong,em,del,ul,ol,li,close',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's meta box row two\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_after_row_two', $plan, $id, $order );\n\t\t?>\n\n\t\t<h4><?php esc_html_e( 'Plan Pricing', 'lifterlms' ); ?></h4>\n\n\t\t<div class=\"llms-plan-row-3\">\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][is_free]\">\n\t\t\t\t\t<?php esc_html_e( 'Plan Type', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Specify if the plan is free or paid.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Specify if the plan is free or paid.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][is_free]\" data-controller-id=\"llms-is-free\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][is_free]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t<option value=\"no\"<?php selected( 'no', $plan ? $plan->get( 'is_free' ) : true ); ?>><?php esc_html_e( 'Paid', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"yes\"<?php selected( 'yes', $plan ? $plan->get( 'is_free' ) : '' ); ?>><?php esc_html_e( 'Free', 'lifterlms' ); ?></option>\n\t\t\t\t</select>\n\n\t\t\t</div>\n\n\t\t\t<div data-controller=\"llms-is-free\" data-value-is=\"no\">\n\n\t\t\t\t<div class=\"llms-metabox-field d-1of6\">\n\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][price]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Price', 'lifterlms' ); ?>\n\t\t\t\t\t\t<span class=\"llms-required\">*<span class=\"screen-reader-text\"><?php esc_html_e( 'required', 'lifterlms' ); ?></span></span>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Set the pricing for this access plan.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Set the pricing for this access plan.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"_llms_plans[<?php echo esc_attr( $order ); ?>][price]\"\n\t\t\t\t\t\tclass=\"llms-plan-price\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][price]\"\n\t\t\t\t\t\tplaceholder=\"<?php echo esc_attr( wp_strip_all_tags( llms_price( 1000 ) ) ); ?>\"\n\t\t\t\t\t\t<?php if ( apply_filters( 'llms_access_plan_price_required', true, $plan ) ) : ?>\n\t\t\t\t\t\tmin=\"<?php echo esc_attr( $price_step ); ?>\"\n\t\t\t\t\t\trequired=\"required\"\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\tstep=\"<?php echo esc_attr( $price_step ); ?>\"\n\t\t\t\t\t\ttype=\"number\"<?php echo ( $plan ? ' value=\"' . esc_attr( $plan->get( 'price' ) ) . '\"' : ' disabled=\"disabled\"' ); ?>\n\t\t\t\t\t>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][frequency]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Frequency', 'lifterlms' ); ?>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Choose how often the payment is charged: one-time or recurring.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Choose how often the payment is charged: one-time or recurring.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][frequency]\" data-controller-id=\"llms-plan-frequency\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][frequency]\"<?php echo ( $plan ? '' : ' disabled=\"disabled\"' ); ?>>\n\t\t\t\t\t\t<option value=\"0\"<?php selected( '0', ( $plan ) ? $frequency : null ); ?>><?php esc_html_e( 'one-time payment', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<option value=\"1\"<?php selected( '1', ( $plan ) ? $frequency : null ); ?>><?php esc_html_e( 'every', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<option value=\"2\"<?php selected( '2', ( $plan ) ? $frequency : null ); ?>><?php esc_html_e( 'every 2nd', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<option value=\"3\"<?php selected( '3', ( $plan ) ? $frequency : null ); ?>><?php esc_html_e( 'every 3rd', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<option value=\"4\"<?php selected( '4', ( $plan ) ? $frequency : null ); ?>><?php esc_html_e( 'every 4th', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<option value=\"5\"<?php selected( '5', ( $plan ) ? $frequency : null ); ?>><?php esc_html_e( 'every 5th', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<option value=\"6\"<?php selected( '6', ( $plan ) ? $frequency : null ); ?>><?php esc_html_e( 'every 6th', 'lifterlms' ); ?></option>\n\t\t\t\t\t</select>\n\t\t\t\t</div>\n\n\t\t\t\t<?php // Recurring plan options. ?>\n\t\t\t\t<div data-controller=\"llms-plan-frequency\" data-value-is-not=\"0\">\n\n\t\t\t\t\t<div class=\"llms-metabox-field d-1of6\">\n\t\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][period]\">\n\t\t\t\t\t\t\t<?php esc_html_e( 'Plan Period', 'lifterlms' ); ?>\n\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Define the billing cycle period for the recurring plan.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<span class=\"tip--top-left\" data-tip=\"<?php esc_attr_e( 'Define the billing cycle period for the recurring plan.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][period]\" data-controller-id=\"llms-plan-period\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][period]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t\t<option value=\"year\"<?php selected( 'year', ( $plan && 0 != $frequency ) ? $period : null ); ?>><?php esc_html_e( 'year', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<option value=\"month\"<?php selected( 'month', ( $plan && 0 != $frequency ) ? $period : null ); ?>><?php esc_html_e( 'month', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<option value=\"week\"<?php selected( 'week', ( $plan && 0 != $frequency ) ? $period : null ); ?>><?php esc_html_e( 'week', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<option value=\"day\"<?php selected( 'day', ( $plan && 0 != $frequency ) ? $period : null ); ?>><?php esc_html_e( 'day', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"llms-metabox-field d-1of6\">\n\t\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][length]\">\n\t\t\t\t\t\t\t<?php esc_html_e( 'Plan Length', 'lifterlms' ); ?>\n\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Specify the duration of the plan period.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<span class=\"tip--top-left\" data-tip=\"<?php esc_attr_e( 'Specify the duration of the plan period.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][length]\" data-controller=\"llms-plan-period\" data-value-is=\"year\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][length]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t\t<option value=\"0\"<?php selected( 0, ( $plan && 'year' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php esc_html_e( 'for all time', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<?php $i = 1; while ( $i <= 6 ) : ?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $i ); ?>\"<?php selected( $i, ( $plan && 'year' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php printf( esc_html( _n( 'for %s year', 'for %s years', $i, 'lifterlms' ) ), esc_html( $i ) ); ?></option>\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t++$i;\n\tendwhile;\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</select>\n\n\t\t\t\t\t\t<select data-controller=\"llms-plan-period\" data-value-is=\"month\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][length]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t\t<option value=\"0\"<?php selected( 0, ( $plan && 'month' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php esc_html_e( 'for all time', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<?php $i = 1; while ( $i <= 24 ) : ?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $i ); ?>\"<?php selected( $i, ( $plan && 'month' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php printf( esc_html( _n( 'for %s month', 'for %s months', $i, 'lifterlms' ) ), esc_html( $i ) ); ?></option>\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t++$i;\n\tendwhile;\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</select>\n\n\t\t\t\t\t\t<select data-controller=\"llms-plan-period\" data-value-is=\"week\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][length]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t\t<option value=\"0\"<?php selected( 0, ( $plan && 'week' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php esc_html_e( 'for all time', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<?php $i = 1; while ( $i <= 52 ) : ?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $i ); ?>\"<?php selected( $i, ( $plan && 'week' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php printf( esc_html( _n( 'for %s week', 'for %s weeks', $i, 'lifterlms' ) ), esc_html( $i ) ); ?></option>\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t++$i;\n\tendwhile;\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</select>\n\n\t\t\t\t\t\t<select data-controller=\"llms-plan-period\" data-value-is=\"day\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][length]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t\t<option value=\"0\"<?php selected( 0, ( $plan && 'day' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php esc_html_e( 'for all time', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<?php $i = 1; while ( $i <= 90 ) : ?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $i ); ?>\"<?php selected( $i, ( $plan && 'day' === $period ) ? $plan->get( 'length' ) : '' ); ?>><?php printf( esc_html( _n( 'for %s day', 'for %s days', $i, 'lifterlms' ) ), esc_html( $i ) ); ?></option>\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t++$i;\n\tendwhile;\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</select>\n\n\t\t\t\t\t</div>\n\n\t\t\t\t</div>\n\n\t\t\t</div>\n\n\t\t\t<?php\n\t\t\t\t// If only the manual gateway is enabled, show a notice and link to our Ecommerce Add-ons.\n\t\t\t\t$active_gateways = llms()->payment_gateways()->get_enabled_payment_gateways();\n\t\t\tif ( 1 === count( $active_gateways ) && array_key_exists( 'manual', $active_gateways ) ) :\n\t\t\t\t?>\n\t\t\t\t\t<div data-controller=\"llms-is-free\" data-value-is=\"no\">\n\n\t\t\t\t\t\t<div class=\"d-all\">\n\n\t\t\t\t\t\t\t<div class=\"notice notice-warning inline llms-admin-notice llms-payment-gateway-warning\">\n\n\t\t\t\t\t\t\t\t<div class=\"llms-admin-notice-content\">\n\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t\t$allowed_ecommerce_add_ons_html = array(\n\t\t\t\t\t\t\t\t\t\t'a'  => array(\n\t\t\t\t\t\t\t\t\t\t\t'href'   => array(),\n\t\t\t\t\t\t\t\t\t\t\t'target' => array(),\n\t\t\t\t\t\t\t\t\t\t\t'title'  => array(),\n\t\t\t\t\t\t\t\t\t\t\t'rel'    => array(),\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t'em' => array(),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tprintf(\n\t\t\t\t\t\t\t\t\t\twp_kses(\n\t\t\t\t\t\t\t\t\t\t\t/* translators: %s: URL to the LifterLMS Ecommerce Add-ons page */\n\t\t\t\t\t\t\t\t\t\t\t__( 'Your site is not set up to process payments. Check out the <a href=\"%s\" target=\"_blank\">Ecommerce Add-ons for LifterLMS</a> to enable live payments via credit card, PayPal, and more.', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\t\t\t$allowed_ecommerce_add_ons_html\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t'https://lifterlms.com/product-category/e-commerce/?utm_source=LifterLMS%20Plugin&utm_medium=Access%20Plans&utm_campaign=Plugin%20to%20Sale'\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t\t<a href=\"\n\t\t\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t\techo esc_url(\n\t\t\t\t\t\t\t\t\t\tadd_query_arg(\n\t\t\t\t\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t\t\t\t\t'page' => 'llms-settings',\n\t\t\t\t\t\t\t\t\t\t\t\t'tab'  => 'checkout',\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t\t\t\t\t\t\"><?php esc_html_e( 'View Payment Gateway Settings', 'lifterlms' ); ?>\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<?php\n\t\t\t\tendif;\n\t\t\t?>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's meta box row three\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_after_row_three', $plan, $id, $order );\n\t\t?>\n\n\t\t<div class=\"llms-plan-row-4\" data-controller=\"llms-plan-frequency\" data-value-is-not=\"0\">\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_offer]\">\n\t\t\t\t\t<?php esc_html_e( 'Trial Offer', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Enable or disable a free or paid trial period for this plan.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-left\" data-tip=\"<?php esc_attr_e( 'Enable or disable a free or paid trial period for this plan.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_offer]\" data-controller-id=\"llms-trial-offer\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_offer]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t<option value=\"no\"<?php selected( 'no', $plan ? $trial_offer : '' ); ?>><?php esc_html_e( 'No trial offer', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"yes\"<?php selected( 'yes', $plan ? $trial_offer : '' ); ?>><?php esc_html_e( 'Enable trial', 'lifterlms' ); ?></option>\n\t\t\t\t</select>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of6\" data-controller=\"llms-trial-offer\" data-value-is=\"yes\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_price]\">\n\t\t\t\t\t<?php esc_html_e( 'Trial Price', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Set the price for the trial period of this plan.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Set the price for the trial period of this plan.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_price]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_price]\" min=\"0\" placeholder=\"<?php echo esc_attr( wp_strip_all_tags( llms_price( 1000 ) ) ); ?>\" required=\"required\" step=\"<?php echo esc_attr( $price_step ); ?>\" type=\"text\"<?php echo ( $plan ) ? ' value=\"' . esc_attr( $plan->get( 'trial_price' ) ) . '\"' : ' disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\" data-controller=\"llms-trial-offer\" data-value-is=\"yes\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_length]\">\n\t\t\t\t\t<?php esc_html_e( 'Trial Length', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Specify the length of the trial period.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Specify the length of the trial period.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_length]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_length]\" min=\"1\" placeholder=\"1\" required=\"required\" type=\"text\"<?php echo ( $plan ) ? ' value=\"' . esc_attr( $plan->get( 'trial_length' ) ) . '\"' : ' value=\"1\" disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\" data-controller=\"llms-trial-offer\" data-value-is=\"yes\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_period]\">\n\t\t\t\t\t<?php esc_html_e( 'Trial Period', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Define the time length for the trial period (days, weeks, months, years).', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Define the time length for the trial period (days, weeks, months, years).', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_period]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][trial_period]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t<option value=\"year\"<?php selected( 'year', ( $plan && 'yes' === $trial_offer ) ? $plan->get( 'trial_period' ) : '' ); ?>><?php esc_html_e( 'year(s)', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"month\"<?php selected( 'month', ( $plan && 'yes' === $trial_offer ) ? $plan->get( 'trial_period' ) : '' ); ?>><?php esc_html_e( 'month(s)', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"week\"<?php selected( 'week', ( $plan && 'yes' === $trial_offer ) ? $plan->get( 'trial_period' ) : '' ); ?>><?php esc_html_e( 'week(s)', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"day\"<?php selected( 'day', ( $plan && 'yes' === $trial_offer ) ? $plan->get( 'trial_period' ) : '' ); ?>><?php esc_html_e( 'day(s)', 'lifterlms' ); ?></option>\n\t\t\t\t</select>\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's meta box row four\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_after_row_four', $plan, $id, $order );\n\t\t?>\n\n\t\t<div class=\"llms-plan-row-5\" data-controller=\"llms-is-free\" data-value-is=\"no\">\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][on_sale]\">\n\t\t\t\t\t<?php esc_html_e( 'Sale Pricing', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Indicate if the plan has a sale.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Indicate if the plan has a sale.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][on_sale]\" data-controller-id=\"llms-on-sale\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][on_sale]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t<option value=\"no\"<?php selected( 'no', $plan ? $on_sale : '' ); ?>><?php esc_html_e( 'Not on sale', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"yes\"<?php selected( 'yes', $plan ? $on_sale : '' ); ?>><?php esc_html_e( 'On Sale', 'lifterlms' ); ?></option>\n\t\t\t\t</select>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of6\" data-controller=\"llms-on-sale\" data-value-is=\"yes\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_price]\">\n\t\t\t\t\t<?php esc_html_e( 'Sale Price', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Set the discounted price for the sale period.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Set the discounted price for the sale period.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_price]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_price]\" min=\"0\" placeholder=\"<?php echo esc_attr( wp_strip_all_tags( llms_price( 1000 ) ) ); ?>\" required=\"required\" step=\"<?php echo esc_attr( $price_step ); ?>\" type=\"number\"<?php echo ( $plan && 'yes' === $on_sale ) ? ' value=\"' . esc_attr( $plan->get( 'sale_price' ) ) . '\"' : ' disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\" data-controller=\"llms-on-sale\" data-value-is=\"yes\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_start]\">\n\t\t\t\t\t<?php esc_html_e( 'Sale Start Date', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Specify when the sale period starts. ', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Specify when the sale period starts. ', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_start]\" class=\"llms-access-plan-datepicker\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_start]\" placeholder=\"MM/DD/YYYY\" type=\"text\"<?php echo ( $plan && 'yes' === $on_sale ) ? ' value=\"' . esc_attr( $plan->get_date( 'sale_start', 'm/d/Y' ) ) . '\"' : ' disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\" data-controller=\"llms-on-sale\" data-value-is=\"yes\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_end]\">\n\t\t\t\t\t<?php esc_html_e( 'Sale End Date', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Specify when the sale period ends.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-left\" data-tip=\"<?php esc_attr_e( 'Specify when the sale period ends.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_end]\" class=\"llms-access-plan-datepicker\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sale_end]\" placeholder=\"MM/DD/YYYY\" type=\"text\"<?php echo ( $plan && 'yes' === $on_sale ) ? ' value=\"' . esc_attr( $plan->get_date( 'sale_end', 'm/d/Y' ) ) . '\"' : ' disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's meta box row five\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_after_row_five', $plan, $id, $order );\n\t\t?>\n\n\t\t<?php\n\t\t\t// Do we have any memberships to restrict this plan to?\n\t\t\t$memberships_count = wp_count_posts( 'llms_membership' );\n\t\tif ( $course && $memberships_count->publish > 0 ) :\n\n\t\t\t/**\n\t\t\t * Filter to show/hide the Membership Settings for an Access Plan for a course.\n\t\t\t *\n\t\t\t * @param boolean          $show_membership_settings Show membership settings for access plans.\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t */\n\t\t\tif ( apply_filters( 'llms_show_membership_settings_for_access_plans', true, $plan, $id ) ) :\n\t\t\t\t?>\n\n\t\t\t\t<h4><?php esc_html_e( 'Membership Settings', 'lifterlms' ); ?></h4>\n\n\t\t\t\t<div class=\"llms-plan-row-6\">\n\n\t\t\t\t\t<div class=\"llms-metabox-field d-1of3\">\n\t\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][availability]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Plan Availability', 'lifterlms' ); ?>\n\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Choose who can purchase this plan: anyone or members only.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Choose who can purchase this plan: anyone or members only.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<select data-controller-id=\"llms-availability\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][availability]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t\t<option value=\"open\"<?php selected( 'open', $plan ? $availability : '' ); ?>><?php esc_html_e( 'Anyone', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<option value=\"members\"<?php selected( 'members', $plan ? $availability : '' ); ?>><?php esc_html_e( 'Members only', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"llms-metabox-field d-1of2\" data-controller=\"llms-availability\" data-value-is=\"members\">\n\t\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][availability_restrictions][]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Memberships', 'lifterlms' ); ?>\n\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Select the memberships required to access this plan.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Select the memberships required to access this plan.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][availability_restrictions][]\" class=\"llms-availability-restrictions\" data-post-type=\"llms_membership\" multiple=\"multiple\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][availability_restrictions][]\" required=\"required\" style=\"width:100%; height: 25px;\" <?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t<?php if ( $plan ) : ?>\n\t\t\t\t\t\t\t\t<?php foreach ( $plan->get_array( 'availability_restrictions' ) as $membership_id ) : ?>\n\t\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $membership_id ); ?>\" selected=\"selected\"><?php echo esc_html( get_the_title( $membership_id ) ); ?> (<?php printf( esc_html__( 'ID# %d', 'lifterlms' ), esc_html( $membership_id ) ); ?>)</option>\n\t\t\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"clear\"></div>\n\n\t\t\t\t</div>\n\n\t\t\t\t<?php\n\t\t\t\tendif;\n\t\t\tendif;\n\t\t?>\n\n\t\t<h4><?php esc_html_e( 'Expiration Settings', 'lifterlms' ); ?></h4>\n\n\t\t<div class=\"llms-plan-row-6\">\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_expiration]\">\n\t\t\t\t\t<?php esc_html_e( 'Access Expiration', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Define if and when the access to the plan expires.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Define if and when the access to the plan expires.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_expiration]\" data-controller-id=\"llms-access-expiration\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_expiration]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t<option value=\"lifetime\"<?php selected( 'lifetime', $plan ? $access_expiration : '' ); ?>><?php esc_html_e( 'Lifetime Access', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"limited-period\"<?php selected( 'limited-period', $plan ? $access_expiration : '' ); ?>><?php esc_html_e( 'Expires after', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"limited-date\"<?php selected( 'limited-date', $plan ? $access_expiration : '' ); ?>><?php esc_html_e( 'Expires on', 'lifterlms' ); ?></option>\n\t\t\t\t</select>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of6\" data-controller=\"llms-access-expiration\" data-value-is=\"limited-date\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_expires]\">\n\t\t\t\t\t<?php esc_html_e( 'Expiration Date', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Specify the exact date when the access from this plan expires.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Specify the exact date when the access from this plan expires.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_expires]\" class=\"llms-access-plan-datepicker\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_expires]\" placeholder=\"MM/DD/YYYY\" required=\"required\" type=\"text\"<?php echo ( $plan && 'limited-date' === $access_expiration ) ? ' value=\"' . esc_attr( $plan->get_date( 'access_expires', 'm/d/Y' ) ) . '\"' : ' value=\"' . esc_attr( date_i18n( 'm/d/y', current_time( 'timestamp' ) ) ) . '\" disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of6\" data-controller=\"llms-access-expiration\" data-value-is=\"limited-period\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_length]\">\n\t\t\t\t\t<?php esc_html_e( 'Access Length', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Set the length of the access period for limited-duration plans.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Set the length of the access period for limited-duration plans.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_length]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_length]\" min=\"1\" placeholder=\"1\" required=\"required\" type=\"number\"<?php echo ( $plan ) ? ' value=\"' . esc_attr( $plan->get( 'access_length' ) ) . '\"' : ' value=\"1\" disabled=\"disabled\"'; ?>>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\" data-controller=\"llms-access-expiration\" data-value-is=\"limited-period\">\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_period]\">\n\t\t\t\t\t<?php esc_html_e( 'Access Period', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Define the time unit for the access period (days, weeks, months, years).', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Define the time unit for the access period (days, weeks, months, years).', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_period]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][access_period]\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t<option value=\"year\"<?php selected( 'year', ( $plan && 'limited-period' === $access_expiration ) ? $access_period : '' ); ?>><?php esc_html_e( 'year(s)', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"month\"<?php selected( 'month', ( $plan && 'limited-period' === $access_expiration ) ? $access_period : '' ); ?>><?php esc_html_e( 'month(s)', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"week\"<?php selected( 'week', ( $plan && 'limited-period' === $access_expiration ) ? $access_period : '' ); ?>><?php esc_html_e( 'week(s)', 'lifterlms' ); ?></option>\n\t\t\t\t\t<option value=\"day\"<?php selected( 'day', ( $plan && 'limited-period' === $access_expiration ) ? $access_period : '' ); ?>><?php esc_html_e( 'day(s)', 'lifterlms' ); ?></option>\n\t\t\t\t</select>\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's meta box row six\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_after_row_six', $plan, $id, $order );\n\t\t?>\n\n\t\t<h4 class=\"llms-redirection-settings\"><?php esc_html_e( 'Redirection Settings', 'lifterlms' ); ?></h4>\n\n\t\t<div class=\"llms-plan-row-7 llms-redirection-settings\">\n\n\t\t\t<div class=\"llms-metabox-field d-all\" data-controller=\"llms-availability\" data-value-is=\"members\">\n\t\t\t\t<label>\n\t\t\t\t\t<?php esc_html_e( 'Override Membership Redirects', 'lifterlms' ); ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Enable this to override membership redirects with the settings below.', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Enable this to override membership redirects with the settings below.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_forced]\">\n\t\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_forced]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_forced]\" type=\"checkbox\" value=\"yes\"<?php checked( 'yes', $plan ? $plan->get( 'checkout_redirect_forced' ) : 'no' ); ?>>\n\t\t\t\t\t<?php esc_html_e( 'Any redirection set up on the Membership Access Plans will be overridden by the following settings.', 'lifterlms' ); ?>\n\t\t\t\t</label>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-checkout-redirect-settings\">\n\n\t\t\t\t<div class=\"llms-metabox-field d-1of2\">\n\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_type]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Checkout Redirect', 'lifterlms' ); ?>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Choose where users are redirected after checkout.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Choose where users are redirected after checkout.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_type]\" class=\"llms-checkout-redirect-type\" data-controller-id=\"llms-checkout-redirect-type\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_type]\" required=\"required\" style=\"width:100%; height: 25px;\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t<?php $saved_checkout_redirect_type = 'self'; ?>\n\t\t\t\t\t\t<?php if ( $plan ) : ?>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t$saved_checkout_redirect_type = ! empty( $plan->get( 'checkout_redirect_type' ) ) ? $plan->get( 'checkout_redirect_type' ) : 'self';\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php foreach ( $checkout_redirection_types as $checkout_redirection_type => $checkout_redirection_label ) : ?>\n\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $checkout_redirection_type ); ?>\"<?php selected( $checkout_redirection_type, $saved_checkout_redirect_type ); ?>><?php echo esc_html( $checkout_redirection_label ); ?></option>\n\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t</select>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"llms-metabox-field d-1of2\" data-controller=\"llms-checkout-redirect-type\" data-value-is=\"page\">\n\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_page]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Select a Page', 'lifterlms' ); ?>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Select a page to redirect users to after checkout.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Select a page to redirect users to after checkout.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<select id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_page]\" class=\"llms-checkout-redirect-page\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_page]\" data-post-type=\"page\" style=\"width:100%; height: 25px;\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t\t\t\t\t<?php if ( $plan ) : ?>\n\t\t\t\t\t\t\t<?php $llms_checkout_redirect_page = $plan->get( 'checkout_redirect_page' ); ?>\n\t\t\t\t\t\t\t<?php if ( ! empty( $llms_checkout_redirect_page ) ) : ?>\n\t\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $llms_checkout_redirect_page ); ?>\" selected=\"selected\"><?php echo esc_html( get_the_title( $llms_checkout_redirect_page ) ); ?> ( #<?php echo esc_html( $llms_checkout_redirect_page ); ?>)</option>\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</select>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"llms-metabox-field d-1of2\" data-controller=\"llms-checkout-redirect-type\" data-value-is=\"url\">\n\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_url]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Enter a URL', 'lifterlms' ); ?>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Enter a specific URL to redirect users to after checkout.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Enter a specific URL to redirect users to after checkout.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_url]\" type=\"text\" class=\"llms-checkout-redirect-url\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][checkout_redirect_url]\"<?php echo ( $plan ) ? ' value=\"' . esc_attr( $plan->get( 'checkout_redirect_url' ) ) . '\"' : ' disabled=\"disabled\"'; ?> value=\"<?php echo ( $plan ) ? esc_attr( $plan->get( 'checkout_redirect_url' ) ) : ''; ?>\" />\n\t\t\t\t</div>\n\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t\t<?php if ( llms_parse_bool( get_option( 'llms_access_plans_allow_skus', 'no' ) ) ) : ?>\n\n\t\t\t<h4><?php esc_html_e( 'Advanced Access Plan Settings', 'lifterlms' ); ?></h4>\n\n\t\t\t<div class=\"llms-plan-row-8\">\n\n\t\t\t\t<div class=\"llms-metabox-field d-1of4\">\n\t\t\t\t\t<label for=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sku]\">\n\t\t\t\t\t\t<?php esc_html_e( 'Plan SKU', 'lifterlms' ); ?>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Set a unique SKU for this access plan for inventory tracking.', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'Set a unique SKU for this access plan for inventory tracking.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<input id=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sku]\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][sku]\" type=\"text\"<?php echo ( $plan ? ' value=\"' . esc_attr( $plan->get( 'sku' ) ) . '\"' : ' disabled=\"disabled\"' ); ?>>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"clear\"></div>\n\n\t\t\t</div>\n\n\t\t<?php endif; ?>\n\n\t\t<div class=\"clear\"></div>\n\n\t\t<input class=\"plan-order\" name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][menu_order]\" type=\"hidden\" value=\"<?php echo ( $plan ) ? esc_attr( $plan->get( 'menu_order' ) ) : esc_attr( $order ); ?>\"<?php echo ( $plan ) ? '' : ' disabled=\"disabled\"'; ?>>\n\t\t<input name=\"_llms_plans[<?php echo esc_attr( $order ); ?>][id]\" type=\"hidden\"<?php echo ( $plan ) ? ' value=\"' . esc_attr( $plan->get( 'id' ) ) . '\"' : ' disabled=\"disabled\"'; ?>>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Action hook fired after access plan's meta box body\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param LLMS_Access_Plan $plan  LLMS_Access_Plan.\n\t\t\t * @param integer          $id    Access Plan ID.\n\t\t\t * @param integer          $order The order of the access plan.\n\t\t\t */\n\t\t\tdo_action( 'llms_access_plan_mb_after_body', $plan, $id, $order );\n\t\t?>\n\n\t</section>\n\n</div>\n"
  },
  {
    "path": "includes/admin/views/access-plans/index.php",
    "content": "<?php\n// quiet.\n"
  },
  {
    "path": "includes/admin/views/access-plans/metabox.php",
    "content": "<?php\n/**\n * Product Options Admin Metabox HTML\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 3.0.0\n * @since 6.0.0 Fix closing tag inside the `llms-no-plans-msg` div element.\n * @version 6.0.0\n *\n * @var LLMS_Course $course\n * @var array $checkout_redirection_types checkout redirect setting options.\n * @var LLMS_Product $product\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"llms-metabox\" id=\"llms-product-options-access-plans\">\n\t<p>\n\t\t<?php\n\t\t\t$access_plan_allowed_html = array(\n\t\t\t\t'a' => array(\n\t\t\t\t\t'href'   => array(),\n\t\t\t\t\t'target' => array(),\n\t\t\t\t),\n\t\t\t);\n\t\t\t// Translators: %1$s = Link to access plans documentation; %2$s = The singular label of the custom post type.\n\t\t\tprintf( wp_kses( __( '<a target=\"_blank\" href=\"%1$s\">Access plans</a> define the payment options and access time-periods available for this %2$s.', 'lifterlms' ), $access_plan_allowed_html ), esc_url( 'https://lifterlms.com/docs/what-is-an-access-plan/' ), esc_html( strtolower( $product->get_post_type_label( 'singular_name' ) ) ) );\n\t\t\t?>\n\t</p>\n\n\t<section class=\"llms-collapsible-group llms-access-plans\" id=\"llms-access-plans\">\n\t\t<div class=\"llms-no-plans-msg\">\n\t\t\t<div class=\"notice notice-warning inline\"><p><?php printf( esc_html__( 'No access plans exist for your %s, click \"Add New\" to get started.', 'lifterlms' ), esc_html( strtolower( $product->get_post_type_label( 'singular_name' ) ) ) ); ?></p></div>\n\t\t</div>\n\t\t<?php foreach ( $product->get_access_plans( false, false ) as $plan ) : ?>\n\t\t\t<?php include 'access-plan.php'; ?>\n\t\t<?php endforeach; ?>\n\t</section>\n\n\t<div class=\"llms-plans-actions\">\n\t\t<div class=\"d-all\">\n\t\t\t<button class=\"llms-button-secondary small\" id=\"llms-new-access-plan\" type=\"button\">\n\t\t\t\t<span class=\"fa fa-plus\"></span>\n\t\t\t\t<?php esc_html_e( 'Add New Plan', 'lifterlms' ); ?>\n\t\t\t</button>\n\t\t</div>\n\n\t\t<div class=\"clear\"></div>\n\n\t\t<div class=\"d-all d-right\">\n\t\t\t<button class=\"llms-button-primary\" id=\"llms-save-access-plans\" type=\"button\"><?php esc_html_e( 'Save All Plans', 'lifterlms' ); ?></button>\n\t\t</div>\n\t</div>\n\n\t<?php\n\t\t// unset $plan so it's not used for the model.\n\t\tunset( $plan );\n\t\t// model of an access plan we'll clone when clicking the \"add\" button.\n\t\trequire 'access-plan.php';\n\t?>\n\n\t<?php if ( apply_filters( 'llms_show_access_plan_dialog', true ) ) : ?>\n\t\t<?php include 'access-plan-dialog.php'; ?>\n\t<?php endif; ?>\n\n</div>\n"
  },
  {
    "path": "includes/admin/views/addons/addon-item.php",
    "content": "<?php\n/**\n * Single Add-on Item View.\n * Used on Add-Ons browser screen.\n *\n * @since 3.22.0\n * @since 7.5.0 Image URLs to use the local addon image directory with a fallback remote image and showing only LifterLMS author logo.\n * @version 7.5.0\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<li class=\"llms-add-on-item type--<?php echo esc_attr( $addon->get( 'type' ) ); ?>\" id=\"<?php echo esc_attr( $addon->get( 'id' ) ); ?>\">\n\n\t<div class=\"llms-add-on\">\n\n\t\t<a class=\"llms-add-on-link\" href=\"<?php echo esc_url( $addon->get_permalink() ); ?>\" target=\"_blank\">\n\t\t\t<header>\n\t\t\t\t<img alt=\"<?php echo esc_attr( $addon->get( 'title' ) ); ?> Banner\" src=\"<?php echo esc_url( $addon->get_image() ); ?>\">\n\t\t\t\t<h4><?php echo esc_html( $addon->get( 'title' ) ); ?></h4>\n\t\t\t</header>\n\n\t\t\t<section>\n\n\t\t\t\t<p><?php echo wp_kses_post( llms_trim_string( $addon->get( 'description' ), 180 ) ); ?></p>\n\n\t\t\t\t<ul>\n\t\t\t\t\t<?php if ( $addon->get( 'author' )['name'] ) : ?>\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t// Translators: %s = Author Name.\n\t\t\t\t\t\t\t\tprintf( esc_html__( 'Author: %s', 'lifterlms' ), esc_html( $addon->get( 'author' )['name'] ) );\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<?php if ( 'LifterLMS' === $addon->get( 'author' )['name'] && $addon->get( 'author' )['image'] ) : ?>\n\t\t\t\t\t\t\t\t<img src=\"<?php echo esc_url( $addon->get_image( 'author' ) ); ?>\" alt=\"<?php echo esc_attr( $addon->get( 'author' )['name'] ); ?> logo\">\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t<?php if ( $addon->is_installable() ) : ?>\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t// Translators: %s = Current Version Number.\n\t\t\t\t\t\t\tprintf( esc_html__( 'Version: %s', 'lifterlms' ), $addon->is_installed() ? esc_html( $addon->get_installed_version() ) : esc_html( $addon->get_latest_version() ) );\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<?php if ( $addon->is_installed() && $addon->has_available_update() ) : ?>\n\t\t\t\t\t\t\t<li><strong>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t// Translators: %s = Available Version Number.\n\t\t\t\t\t\t\t\tprintf( esc_html__( 'Update Available: %s', 'lifterlms' ), esc_html( $addon->get_latest_version() ) );\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t</strong></li>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</ul>\n\n\t\t\t</section>\n\n\t\t</a>\n\n\t\t<footer class=\"llms-actions\">\n\n\t\t\t<?php do_action( 'llms_add_ons_single_item_before_actions', $addon, $current_tab ); ?>\n\n\t\t\t<?php if ( in_array( $addon->get_type(), array( 'external', 'support' ), true ) || ( 'bundle' === $addon->get_type() && ! $addon->is_licensed() ) || ( $addon->is_installable() && ! $addon->is_installed() ) ) : ?>\n\t\t\t\t<a href=\"<?php echo esc_url( $addon->get_permalink() ); ?>\" class=\"llms-status-icon external status--<?php echo esc_attr( $addon->get_license_status() ); ?>\" target=\"_blank\">\n\t\t\t\t\t<i class=\"fa fa-info-circle hide-on-hover\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<i class=\"fa fa-external-link show-on-hover\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<span class=\"llms-status-text\"><?php esc_html_e( 'Learn more', 'lifterlms' ); ?></span>\n\t\t\t\t</a>\n\t\t\t\t<?php\n\t\t\telse :\n\t\t\t\t$url = $addon->is_licensed() ? 'https://lifterlms.com/my-account' : $addon->get_permalink();\n\t\t\t\t?>\n\t\t\t\t<a href=\"<?php echo esc_url( $url ); ?>\" class=\"llms-status-icon status--<?php echo esc_attr( $addon->get_license_status() ); ?>\" target=\"_blank\">\n\t\t\t\t\t<i class=\"fa fa-key hide-on-hover\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<i class=\"fa fa-external-link show-on-hover\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<span class=\"llms-status-text\"><?php echo wp_kses_post( $addon->get_license_status( true ) ); ?></span>\n\t\t\t\t</a>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php do_action( 'llms_add_ons_single_item_actions', $addon, $current_tab ); ?>\n\n\t\t\t<?php if ( 'featured' !== $current_tab ) : ?>\n\n\t\t\t\t<?php if ( $addon->is_installable() && $addon->is_installed() ) : ?>\n\t\t\t\t\t<?php if ( $addon->is_active() ) : ?>\n\t\t\t\t\t\t<?php if ( 'theme' !== $addon->get_type() ) : ?>\n\t\t\t\t\t\t\t<label class=\"llms-status-icon status--<?php echo esc_attr( $addon->get_status() ); ?>\" for=\"<?php echo esc_attr( sprintf( '%s-deactivate', $addon->get( 'id' ) ) ); ?>\">\n\t\t\t\t\t\t\t\t<input class=\"llms-bulk-check\" data-action=\"deactivate\" name=\"llms_deactivate[]\" id=\"<?php echo esc_attr( sprintf( '%s-deactivate', $addon->get( 'id' ) ) ); ?>\" type=\"checkbox\" value=\"<?php echo esc_attr( $addon->get( 'id' ) ); ?>\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-check-square-o\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t\t<i class=\"fa fa-plug\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t\t<span class=\"llms-status-text\"><?php esc_html_e( 'Deactivate', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t<label class=\"llms-status-icon status--<?php echo esc_attr( $addon->get_status() ); ?>\" for=\"<?php echo esc_attr( sprintf( '%s-activate', $addon->get( 'id' ) ) ); ?>\">\n\t\t\t\t\t\t\t<input class=\"llms-bulk-check\" data-action=\"activate\" name=\"llms_activate[]\" id=\"<?php echo esc_attr( sprintf( '%s-activate', $addon->get( 'id' ) ) ); ?>\" type=\"checkbox\" value=\"<?php echo esc_attr( $addon->get( 'id' ) ); ?>\">\n\t\t\t\t\t\t\t<i class=\"fa fa-check-square-o\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t<i class=\"fa fa-plug\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t<span class=\"llms-status-text\"><?php esc_html_e( 'Activate', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php do_action( 'llms_add_ons_single_item_after_actions', $addon, $current_tab ); ?>\n\n\t\t</footer>\n\n\t</div>\n\n</li>\n"
  },
  {
    "path": "includes/admin/views/addons/index.php",
    "content": "<?php // Quiet.\n"
  },
  {
    "path": "includes/admin/views/builder/assignment.php",
    "content": "<?php\n/**\n * Assignment template\n *\n * @since   3.17.0\n * @version 3.17.0\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-assignment-template\">\n\n\t<# if ( _.isEmpty( data ) ) { #>\n\n\t\t<div class=\"llms-quiz-empty\">\n\n\t\t\t<p><?php esc_html_e( 'There\\'s no assignment associated with this lesson.', 'lifterlms' ); ?></p>\n\n\t\t\t<button class=\"llms-element-button\" id=\"llms-new-assignment\" type=\"button\">\n\t\t\t\t<?php esc_html_e( 'Create New Assignment', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-file\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t\t<br>\n\n\t\t\t<button class=\"llms-element-button\" id=\"llms-existing-assignment\" type=\"button\">\n\t\t\t\t<?php esc_html_e( 'Add Existing Assignment', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-file-text\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t</div>\n\n\t<# } else { #>\n\n\t\t<?php do_action( 'llms_builder_assignment_settings' ); ?>\n\n\t<# } #>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/course.php",
    "content": "<?php\n/**\n * Builder main course view\n *\n * @since   3.16.0\n * @version 3.17.8\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-course-template\">\n\n\t<header class=\"llms-builder-header llms-course-header\">\n\n\t\t<h1 class=\"llms-headline\">\n\t\t\t<span data-original-content=\"{{{ data.get( 'title' ) }}}\" data-required=\"required\" type=\"text\">{{{ data.get( 'title' ) }}}</span>\n\t\t</h1>\n\n\t\t<div class=\"llms-action-icons static\">\n\t\t\t<# if ( data.get_edit_post_link() ) { #>\n\t\t\t\t<a class=\"llms-button-secondary small\" href=\"{{{ data.get_edit_post_link() }}}\">\n\t\t\t\t\t<i class=\"fa fa-pencil-square-o\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<?php esc_html_e( 'Edit Course Page', 'lifterlms' ); ?>\n\t\t\t\t</a>\n\t\t\t<# } #>\n\t\t\t<a class=\"llms-button-secondary small\" href=\"{{{ data.get( 'permalink' ) }}}\">\n\t\t\t\t<i class=\"fa fa-external-link\"></i>\n\t\t\t\t<?php esc_html_e( 'View Course', 'lifterlms' ); ?>\n\t\t\t</a>\n\t\t</div>\n\n\t</header>\n\n\t<section class=\"llms-outline\" id=\"llms-outline\">\n\t\t<div class=\"llms-builder-tutorial\" id=\"llms-builder-tutorial\"></div>\n\t\t<ul class=\"llms-sections\" id=\"llms-sections\"></ul>\n\t\t<button class=\"llms-button-secondary small new-section\">\n\t\t\t<span class=\"fa fa-file\"></span> <?php esc_html_e( 'Add New Section', 'lifterlms' ); ?>\n\t\t</button>\n\n\t</section>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/editor.php",
    "content": "<?php\n/**\n * Builder sidebar model editor view\n *\n * @since   3.16.0\n * @version 3.17.0\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-editor-template\">\n\n\t<nav class=\"llms-editor-nav\">\n\n\t\t<ul class=\"llms-editor-menu\">\n\n\t\t\t<li class=\"llms-editor-menu-item<# if ( 'lesson' === data.state ) { print( ' active' ); } #>\">\n\t\t\t\t<a href=\"#llms-editor-lesson\" data-view=\"lesson\"><?php esc_html_e( 'Lesson', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-editor-menu-item<# if ( 'assignment' === data.state ) { print( ' active' ); } #>\">\n\t\t\t\t<a href=\"#llms-editor-assignment\" data-view=\"assignment\"><?php esc_html_e( 'Assignment', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-editor-menu-item<# if ( 'quiz' === data.state ) { print( ' active' ); } #>\">\n\t\t\t\t<a href=\"#llms-editor-quiz\" data-view=\"quiz\"><?php esc_html_e( 'Quiz', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-editor-menu-item right\">\n\t\t\t\t<a href=\"#llms-editor-close\">\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Close', 'lifterlms' ); ?></span>\n\t\t\t\t\t<i class=\"fa fa-close\" aria-hidden=\"true\"></i>\n\t\t\t\t</a>\n\t\t\t</li>\n\n\t\t</ul>\n\n\t</nav>\n\n\t<section id=\"llms-editor-lesson\" class=\"llms-editor-tab tab--lesson<# if ( 'lesson' === data.state ) { print( ' active' ); } #>\"></section>\n\t<section id=\"llms-editor-quiz\" class=\"llms-editor-tab tab--quiz<# if ( 'quiz' === data.state ) { print( ' active' ); } #>\"></section>\n\t<section id=\"llms-editor-assignment\" class=\"llms-editor-tab tab--assignment<# if ( 'assignment' === data.state ) { print( ' active' ); } #>\"></section>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/elements.php",
    "content": "<?php\n/**\n * Builder sidebar course elements list\n *\n * @since   3.16.0\n * @version 3.16.0\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-elements-template\">\n\n\t<h2 class=\"llms-sidebar-headline\"><?php esc_html_e( 'Build Your Course', 'lifterlms' ); ?></h2>\n\t<p class=\"llms-sidebar-description\">\n\t\t<?php\n\t\t\t/* translators: %s: link to course builder tutorial */\n\t\t\techo wp_kses(\n\t\t\t\tsprintf(\n\t\t\t\t\t__( 'Drag or click on the different course elements below to build your course syllabus. <a href=\"%s\" target=\"_blank\">Visit the course builder tutorial here</a>.', 'lifterlms' ),\n\t\t\t\t\t'https://lifterlms.com/docs/using-course-builder/'\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'a' => array(\n\t\t\t\t\t\t'href'   => array(),\n\t\t\t\t\t\t'target' => array(),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t</p>\n\t<ul class=\"llms-elements-list llms-add-items\">\n\n\t\t<li>\n\t\t\t<button class=\"llms-element-button\" id=\"llms-new-section\">\n\t\t\t\t<span class=\"fa fa-puzzle-piece\"></span>\n\t\t\t\t<span class=\"llms-element-button-text\"><?php esc_html_e( 'Add New Section', 'lifterlms' ); ?></span>\n\t\t\t</button>\n\t\t</li>\n\n\t\t<li>\n\t\t\t<button class=\"llms-element-button\" id=\"llms-new-lesson\">\n\t\t\t\t<span class=\"fa fa-file\"></span>\n\t\t\t\t<span class=\"llms-element-button-text\"><?php esc_html_e( 'Add New Lesson', 'lifterlms' ); ?></span>\n\t\t\t</button>\n\t\t</li>\n\n\t\t<li>\n\t\t\t<button class=\"llms-element-button\" id=\"llms-existing-lesson\">\n\t\t\t\t<span class=\"fa fa-file-text\"></span>\n\t\t\t\t<span class=\"llms-element-button-text\"><?php esc_html_e( 'Add Existing Lesson', 'lifterlms' ); ?></span>\n\t\t\t</button>\n\t\t</li>\n\n\t</ul>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/index.php",
    "content": "<?php // Shhh.\n"
  },
  {
    "path": "includes/admin/views/builder/lesson-settings.php",
    "content": "<?php\n/**\n * Builder lesson settings template\n *\n * @since   3.17.0\n * @version 3.17.2\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-lesson-settings-template\">\n\n\t<header class=\"llms-model-header\" id=\"llms-lesson-header\">\n\n\t\t<h3 class=\"llms-headline llms-model-title\">\n\t\t\t<?php esc_html_e( 'Title', 'lifterlms' ); ?>: <span class=\"llms-input llms-editable-title\" contenteditable=\"true\" data-attribute=\"title\" data-original-content=\"{{{ data.get( 'title' ) }}}\" data-required=\"required\">{{{ data.get( 'title' ) }}}</span>\n\t\t</h3>\n\n\t\t<label class=\"llms-switch llms-model-status\">\n\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Published', 'lifterlms' ); ?></span>\n\t\t\t<input data-off=\"draft\" data-on=\"publish\" name=\"status\" type=\"checkbox\"<# if ( 'publish' === data.get( 'status' ) ) { print( ' checked' ) } #>>\n\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t</label>\n\n\t\t<div class=\"llms-action-icons\">\n\n\t\t\t<# if ( ! data.has_temp_id() ) { #>\n\t\t\t\t<a class=\"llms-action-icon tip--bottom-left\" data-tip=\"<?php esc_attr_e( 'Open WordPress lesson editor', 'lifterlms' ); ?>\" href=\"{{{ data.get_edit_post_link() }}}\" target=\"_blank\">\n\t\t\t\t\t<i class=\"fa fa-wordpress\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Open WordPress lesson editor', 'lifterlms' ); ?></span>\n\t\t\t\t</a>\n\n\t\t\t\t<a class=\"llms-action-icon danger tip--bottom-left\" data-tip=\"<?php esc_attr_e( 'Detach Lesson', 'lifterlms' ); ?>\" href=\"#llms-detach-model\">\n\t\t\t\t\t<i class=\"fa fa-chain-broken\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Detach Lesson', 'lifterlms' ); ?></span>\n\t\t\t\t</a>\n\t\t\t<# } #>\n\n\t\t\t<a class=\"llms-action-icon danger tip--bottom-left\" data-tip=\"<?php esc_attr_e( 'Delete Lesson', 'lifterlms' ); ?>\" href=\"#llms-trash-model\" tabindex=\"-1\">\n\t\t\t\t<i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Delete Lesson', 'lifterlms' ); ?></span>\n\t\t\t</a>\n\n\t\t</div>\n\n\t</header>\n\n\t<div id=\"llms-lesson-settings-fields\"></div>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/lesson.php",
    "content": "<?php\n/**\n * Builder lesson model view\n *\n * @since 3.16.0\n * @since 3.30.3 Fixed spelling errors.\n * @since 7.2.0 Added lesson id.\n * @version 7.2.0\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-lesson-template\">\n\n\t<span class=\"llms-drag-utility drag-lesson\"></span>\n\n\t<header class=\"llms-builder-header\">\n\t\t<h3 class=\"llms-headline\">\n\t\t\t<span>{{{ data.get( 'title' ) }}}</span>\n\t\t</h3>\n\n\t\t<div class=\"llms-action-icons\">\n\n\t\t\t<div class=\"llms-action-icons-left\">\n\n\t\t\t\t<span class=\"llms-item-id\"><?php esc_html_e( 'ID:', 'lifterlms' ); ?> {{{ data.get( 'id' ) }}}</span>\n\n\t\t\t\t<# if ( data.get_edit_post_link() ) { #>\n\t\t\t\t\t<a class=\"llms-action-icon tip--top-right\" data-tip=\"<?php esc_attr_e( 'Open WordPress lesson editor', 'lifterlms' ); ?>\" href=\"{{{ data.get_edit_post_link() }}}\" target=\"_blank\">\n\t\t\t\t\t\t<span class=\"fa fa-pencil-square-o\"></span>\n\t\t\t\t\t\t<?php esc_html_e( 'Edit', 'lifterlms' ); ?>\n\t\t\t\t\t</a>\n\t\t\t\t<# } #>\n\n\t\t\t\t<# if ( ! data.has_temp_id() ) { #>\n\t\t\t\t\t<a class=\"llms-action-icon tip--top-right\" data-tip=\"<?php esc_attr_e( 'View lesson', 'lifterlms' ); ?>\" href=\"{{{ data.get_view_post_link() }}}\" target=\"_blank\">\n\t\t\t\t\t\t<span class=\"fa fa-external-link\"></span>\n\t\t\t\t\t\t<?php esc_html_e( 'View', 'lifterlms' ); ?>\n\t\t\t\t\t</a>\n\t\t\t\t<# } #>\n\n\t\t\t\t<# if ( ! data.has_temp_id() ) { #>\n\t\t\t\t\t<button class=\"llms-action-icon llms-detach-model detach--lesson danger tip--top-right\" data-tip=\"<?php esc_attr_e( 'Detach lesson', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<span class=\"fa fa-chain-broken\"></span>\n\t\t\t\t\t\t<?php esc_html_e( 'Detach', 'lifterlms' ); ?>\n\t\t\t\t\t</button>\n\t\t\t\t<# } #>\n\n\t\t\t\t<?php if ( current_user_can( 'delete_course', $course_id ) ) : ?>\n\t\t\t\t\t<button class=\"llms-action-icon llms-trash-model trash--lesson danger tip--top-right\" data-tip=\"<?php esc_attr_e( 'Trash lesson', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<span class=\"fa fa-trash\"></span>\n\t\t\t\t\t\t<?php esc_html_e( 'Trash', 'lifterlms' ); ?>\n\t\t\t\t\t</button>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-action-icons-right\">\n\n\t\t\t\t<button id=\"llms-section-change\" class=\"llms-action-icon section-prev tip--top-right\" data-tip=\"<?php esc_attr_e( 'Move to previous section', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Move to previous section', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"fa fa-arrow-circle-o-up\"></span>\n\t\t\t\t</button>\n\n\t\t\t\t<button id=\"llms-section-change\" class=\"llms-action-icon section-next tip--top-right\" data-tip=\"<?php esc_attr_e( 'Move to next section', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Move to next section', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"fa fa-arrow-circle-o-down\"></span>\n\t\t\t\t</button>\n\n\t\t\t\t<button id=\"llms-shift\" class=\"llms-action-icon shift-up--lesson tip--top-right\" data-tip=\"<?php esc_attr_e( 'Shift up', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Shift up', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"fa fa-chevron-up\"></span>\n\t\t\t\t</button>\n\n\t\t\t\t<button id=\"llms-shift\" class=\"llms-action-icon shift-down--lesson tip--top-right\" data-tip=\"<?php esc_attr_e( 'Shift down', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Shift down', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"fa fa-chevron-down\"></span>\n\t\t\t\t</button>\n\n\t\t\t</div>\n\n\t\t</div>\n\n\t</header>\n\n\t<ul class=\"llms-info-list\">\n\n\t\t<?php\n\t\t$icons = array(\n\n\t\t\t'settings'    => array(\n\t\t\t\t'action'           => 'edit-lesson',\n\t\t\t\t'active_condition' => 'true',\n\t\t\t\t'tip'              => '',\n\t\t\t\t'tip_active'       => esc_attr__( 'Edit lesson settings', 'lifterlms' ),\n\t\t\t\t'icon'             => '',\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-cog\"></i>' . esc_html__( 'Settings', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'assignment'  => array(\n\t\t\t\t'action'           => 'edit-assignment',\n\t\t\t\t'active_condition' => \"'yes' === data.get( 'assignment_enabled' )\",\n\t\t\t\t'tip'              => esc_attr__( 'Add an assignment', 'lifterlms' ),\n\t\t\t\t'tip_active'       => sprintf( esc_attr__( 'Edit assignment: %s', 'lifterlms' ), \"{{{ _.isEmpty( data.get( 'assignment' ) ) ? '' : data.get( 'assignment' ).get( 'title' ) }}}\" ),\n\t\t\t\t'icon'             => '<i class=\"fa fa-check-square-o\"></i> ' . esc_html__( 'Add assignment', 'lifterlms' ),\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-check-square-o\"></i>' . esc_html__( 'Edit assignment', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'quiz'        => array(\n\t\t\t\t'action'           => 'edit-quiz',\n\t\t\t\t'active_condition' => \"'yes' === data.get( 'quiz_enabled' )\",\n\t\t\t\t'tip'              => esc_attr__( 'Add a quiz', 'lifterlms' ),\n\t\t\t\t'tip_active'       => sprintf( esc_attr__( 'Edit quiz: %s', 'lifterlms' ), \"{{{ ( 'yes' === data.get( 'quiz_enabled' ) ) ? data.get( 'quiz' ).get( 'title' ) : '' }}}\" ),\n\t\t\t\t'icon'             => '<i class=\"fa fa-question-circle\"></i> ' . esc_html__( 'Add quiz', 'lifterlms' ),\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-question-circle\"></i>' . esc_html__( 'Edit quiz', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'video'       => array(\n\t\t\t\t'action'           => 'edit-lesson',\n\t\t\t\t'active_condition' => \"data.get( 'video_embed' )\",\n\t\t\t\t'tip'              => esc_attr__( 'No video', 'lifterlms' ),\n\t\t\t\t'tip_active'       => esc_attr__( 'Has video', 'lifterlms' ),\n\t\t\t\t'icon'             => '',\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-play-circle\"></i>' . esc_html__( 'Video', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'audio'       => array(\n\t\t\t\t'action'           => false,\n\t\t\t\t'active_condition' => \"data.get( 'audio_embed' )\",\n\t\t\t\t'tip'              => esc_attr__( 'No audio', 'lifterlms' ),\n\t\t\t\t'tip_active'       => esc_attr__( 'Has audio', 'lifterlms' ),\n\t\t\t\t'icon'             => '',\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-volume-up\"></i>' . esc_html__( 'Audio', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'free'        => array(\n\t\t\t\t'action'           => 'edit-lesson',\n\t\t\t\t'active_condition' => \"'yes' === data.get( 'free_lesson' )\",\n\t\t\t\t'tip'              => esc_attr__( 'Enrolled students only', 'lifterlms' ),\n\t\t\t\t'tip_active'       => esc_attr__( 'Free lesson', 'lifterlms' ),\n\t\t\t\t'icon'             => '',\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-unlock\"></i>' . esc_html__( 'Free lesson', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'prereq'      => array(\n\t\t\t\t'action'           => 'edit-lesson',\n\t\t\t\t'active_condition' => \"'yes' === data.get( 'has_prerequisite' )\",\n\t\t\t\t'tip'              => esc_attr__( 'No prerequisite', 'lifterlms' ),\n\t\t\t\t'tip_active'       => esc_attr__( 'Prerequisite enabled', 'lifterlms' ),\n\t\t\t\t'icon'             => '',\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-link\"></i>' . esc_html__( 'Prerequisite enabled', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'drip_method' => array(\n\t\t\t\t'action'           => 'edit-lesson',\n\t\t\t\t'active_condition' => \"data.get( 'drip_method' )\",\n\t\t\t\t'tip'              => esc_attr__( 'Drip disabled', 'lifterlms' ),\n\t\t\t\t'tip_active'       => esc_attr__( 'Drip enabled', 'lifterlms' ),\n\t\t\t\t'icon'             => '',\n\t\t\t\t'icon_active'      => '<i class=\"fa fa-calendar\"></i>' . esc_html__( 'Drip enabled', 'lifterlms' ),\n\t\t\t),\n\n\t\t);\n\n\t\t// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above.\n\t\tforeach ( $icons as $icon ) :\n\t\t\t?>\n\n\t\t\t<?php // Hide the whole icon area if there is no icon set, and the active condition is not met. ?>\n\t\t\t<?php if ( ! $icon['icon'] ) : ?>\n\t\t\t\t<# if ( <?php echo $icon['active_condition']; ?> ) { #>\n\t\t\t<?php endif; ?>\n\t\t\t\t<li class=\"llms-info-item tip--top-right<# if ( <?php echo $icon['active_condition']; ?> ) { print( ' active') } #>\"\n\t\t\t\t\tdata-tip=\"<?php echo $icon['tip']; ?>\"\n\t\t\t\t\tdata-tip-active=\"<?php echo $icon['tip_active']; ?>\">\n\t\t\t\t\t<?php if ( $icon['action'] ) : ?>\n\t\t\t\t\t\t<?php printf( '<button class=\"llms-action-icon %1$s\" id=\"#llms-action--%1$s\">', $icon['action'] ); ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<# if ( <?php echo $icon['active_condition']; ?> ) { #>\n\t\t\t\t\t\t<?php echo $icon['icon_active']; ?>\n\t\t\t\t\t<# } else { #>\n\t\t\t\t\t\t<?php echo $icon['icon']; ?>\n\t\t\t\t\t<# } #>\n\t\t\t\t\t<?php if ( $icon['action'] ) : ?>\n\t\t\t\t\t</button>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</li>\n\t\t\t<?php if ( ! $icon['icon'] ) : ?>\n\t\t\t\t<# } #>\n\t\t\t<?php endif; ?>\n\n\t\t<?php endforeach; ?>\n\t\t<?php // phpcs:enable WordPress.XSS.EscapeOutput.OutputNotEscaped ?>\n\t</ul>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/question-choice.php",
    "content": "<?php\n/**\n * Builder question view\n *\n * @since   3.16.0\n * @version 3.17.8\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-question-choice-template\">\n\n\t<label class=\"llms-choice-id\">\n\t\t<# if ( data.is_selectable() ) { #>\n\t\t\t<input name=\"correct\" type=\"checkbox\"<# if ( data.get( 'correct' ) ) { print( ' checked=\"checked\"' ) ;} #>>\n\t\t<# } #>\n\t\t<span class=\"llms-marker<# if ( data.is_selectable() ) { print ( ' selectable' ) }#>\">\n\t\t\t<b>{{{ data.get( 'marker' ) }}}</b>\n\t\t\t<i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n\t\t</span>\n\t</label>\n\n\t<# if ( 'text' === data.get( 'choice_type' ) ) { #>\n\t\t<div class=\"llms-input-wrapper\">\n\t\t\t<span class=\"llms-input llms-editable-title\" contenteditable=\"true\" data-attribute=\"choice\" data-formatting=\"b,i,u,em,strong\" data-original-content=\"{{{ data.get( 'choice' ) }}}\" data-placeholder=\"<?php esc_attr_e( 'Enter a choice...', 'lifterlms' ); ?>\">{{{ data.get( 'choice' ) }}}</span>\n\t\t</div>\n\t<# } else if ( 'image' === data.get( 'choice_type' ) ) { #>\n\t\t<div class=\"llms-editable-image\">\n\t\t\t<# if ( data.get( 'choice' ).get( 'src' ) ) { #>\n\t\t\t\t<div class=\"llms-image\">\n\t\t\t\t\t<a class=\"llms-action-icon danger tip--top-left\" data-attribute=\"choice\" data-tip=\"<?php esc_attr_e( 'Remove image', 'lifterlms' ); ?>\" href=\"#llms-remove-image\">\n\t\t\t\t\t\t<i class=\"fa fa-times-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Remove image', 'lifterlms' ); ?></span>\n\t\t\t\t\t</a>\n\t\t\t\t\t<img alt=\"<?php esc_attr_e( 'image preview', 'lifterlms' ); ?>\" src=\"{{{ data.get( 'choice' ).get( 'src' ) }}}\">\n\t\t\t\t</div>\n\t\t\t<# } else { #>\n\t\t\t\t<button class=\"llms-element-button small llms-add-image\" data-attribute=\"choice\" data-image-size=\"full\">\n\t\t\t\t\t<span class=\"fa fa-picture-o\"></span> <?php esc_html_e( 'Add Image', 'lifterlms' ); ?>\n\t\t\t\t</button>\n\t\t\t<# } #>\n\t\t</div>\n\t<# } #>\n\n\t<div class=\"llms-action-icons\">\n\n\t\t<a class=\"llms-action-icon circle tip--top-left\" data-tip=\"<?php esc_attr_e( 'Add Choice', 'lifterlms' ); ?>\" href=\"#llms-add-choice\" tabindex=\"-1\">\n\t\t\t<i class=\"fa fa-plus\" aria-hidden=\"true\"></i>\n\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Add Choice', 'lifterlms' ); ?></span>\n\t\t</a>\n\n\t\t<a class=\"llms-action-icon circle danger tip--top-left\" data-tip=\"<?php esc_attr_e( 'Delete Choice', 'lifterlms' ); ?>\" href=\"#llms-del-choice\" tabindex=\"-1\">\n\t\t\t<i class=\"fa fa-minus\" aria-hidden=\"true\"></i>\n\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Delete Choice', 'lifterlms' ); ?></span>\n\t\t</a>\n\n\t</div>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/question-type.php",
    "content": "<?php\n/**\n * Builder quiz type template\n *\n * @since   3.16.0\n * @version 3.27.0\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-question-type-template\">\n\n\t<# if ( data.get( 'upgrade' ) ) { #>\n\t\t<a class=\"llms-type-unavailable tip--top-right\" href=\"{{{ data.get( 'upgrade' ) }}}\" data-tip=\"<?php esc_attr_e( 'Install the LifterLMS Advanced Quizzes add-on to enable this question type', 'lifterlms' ); ?>\" target=\"_blank\">\n\t<# } #>\n\t<button class=\"llms-element-button small llms-add-question\" data-id=\"{{{ data.get( 'id' ) }}}\" id=\"llms-add-question--{{{ data.get( 'id' ) }}}\">\n\t\t<i class=\"fa fa-{{{ data.get( 'icon' ) }}}\" aria-hidden=\"true\"></i> {{{ data.get( 'name' ) }}}\n\t</button>\n\t<# if ( data.get( 'upgrade' ) ) { #>\n\t\t</a>\n\t<# } #>\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/question.php",
    "content": "<?php\n/**\n * Builder question view.\n *\n * @since 3.16.0\n * @since 3.27.0 Unknown.\n * @since 7.4.0 Escaped strings in labels.\n * @version 7.4.0\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-question-template\">\n\n\t<header class=\"llms-builder-header\">\n\n\t\t<span class=\"llms-data-stamp {{{ data.get( 'question_type' ).get( 'id' ) }}} tip--top-right\" data-tip=\"{{{ data.get_qid() }}} &ndash; {{{ data.get( 'question_type' ).get( 'name' ) }}}\">\n\t\t\t<i class=\"fa fa-{{{ data.get( 'question_type' ).get( 'icon' ) }}}\" aria-hidden=\"true\"></i>\n\t\t\t<small>{{{ data.get_qid() }}}</small>\n\t\t</span>\n\n\t\t<h3 class=\"llms-headline llms-input-wrapper\">\n\t\t\t<div class=\"llms-editable-title llms-input-formatting\" data-attribute=\"title\" data-formatting=\"bold,italic,underline\" data-placeholder=\"{{{ data.get( 'question_type' ).get( 'placeholder' ) }}}\">{{{ data.get( 'title' ) }}}</div>\n\t\t</h3>\n\n\t\t<div class=\"llms-action-icons\">\n\n\t\t\t<# if ( ! data.get( '_expanded' ) ) { #>\n\t\t\t\t<a class=\"llms-action-icon expand--question tip--top-right\" data-tip=\"<?php esc_attr_e( 'Expand question', 'lifterlms' ); ?>\" href=\"#llms-expand\" tabindex=\"-1\">\n\t\t\t\t\t<i class=\"fa fa-plus-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t</a>\n\t\t\t<# } else { #>\n\t\t\t\t<a class=\"llms-action-icon collapse--question tip--top-right\" data-tip=\"<?php esc_attr_e( 'Collapse question', 'lifterlms' ); ?>\" href=\"#llms-collapse\" tabindex=\"-1\">\n\t\t\t\t\t<i class=\"fa fa-minus-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t</a>\n\t\t\t<# } #>\n\n\t\t\t<a class=\"llms-action-icon clone--question tip--top-right\" data-tip=\"<?php esc_attr_e( 'Clone question', 'lifterlms' ); ?>\" href=\"#llms-clone\" tabindex=\"-1\">\n\t\t\t\t<i class=\"fa fa-clone\" aria-hidden=\"true\"></i>\n\t\t\t</a>\n\n\t\t\t<# if ( ! data.has_temp_id() ) { #>\n\t\t\t\t<a class=\"llms-action-icon detach--question danger tip--top-right\" data-tip=\"<?php esc_attr_e( 'Detach question', 'lifterlms' ); ?>\" href=\"#llms-detach-model\">\n\t\t\t\t\t<span class=\"fa fa-chain-broken\"></span>\n\t\t\t\t</a>\n\t\t\t<# } #>\n\n\t\t\t<a class=\"llms-action-icon danger delete--question tip--top-right\" data-tip=\"<?php esc_attr_e( 'Delete question', 'lifterlms' ); ?>\" href=\"#llms-trash\" tabindex=\"-1\">\n\t\t\t\t<i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n\t\t\t</a>\n\t\t</div>\n\n\n\t\t<div class=\"llms-question-points llms-editable-number tip--top-left\" data-tip=\"{{{ data.get_points_percentage() }}}\">\n\t\t\t<input class=\"llms-input two-digits\" min=\"0\" max=\"99\" name=\"question_points\" type=\"number\" value=\"{{{ data.get( 'points' ) }}}\" tabindex=\"-1\"<# if ( ! data.get( 'question_type' ).get( 'points' ) ) { print( ' disabled'); }#>><small><?php esc_html_e( 'points', 'lifterlms' ); ?></small>\n\t\t</div>\n\n\t</header>\n\n\t<section class=\"llms-question-body <# if ( data.get( '_expanded' ) ) { print( ' active' ); }#>\">\n\n\t\t<div class=\"llms-question-features\">\n\n\t\t\t<?php do_action( 'llms_builder_question_before_features' ); ?>\n\n\t\t\t<div class=\"llms-settings-row\">\n\t\t\t\t<# if ( data.get( 'question_type' ).get( 'description' ) ) { #>\n\t\t\t\t\t<div class=\"llms-editable-toggle-group\">\n\t\t\t\t\t\t<label class=\"llms-switch\">\n\t\t\t\t\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Description', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<input type=\"checkbox\" name=\"description_enabled\"<# if ( 'yes' === data.get( 'description_enabled' ) ) { print( ' checked' ) } #>>\n\t\t\t\t\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t<# if ( 'yes' === data.get( 'description_enabled' ) ) { #>\n\t\t\t\t\t\t\t<div class=\"llms-editable-editor\">\n\t\t\t\t\t\t\t\t<textarea data-attribute=\"content\" id=\"question-desc--{{{ data.get( 'id' ) }}}\">{{{ data.get( 'content' ) }}}</textarea>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<# } #>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<# if ( 'yes' === data.get( 'description_enabled' ) ) { #>\n\t\t\t\t\t\t<div class=\"llms-breaker\"></div>\n\t\t\t\t\t<# } #>\n\t\t\t\t<# } #>\n\n\t\t\t\t<# if ( data.get( 'question_type' ).get( 'image' ) ) { #>\n\t\t\t\t\t<div class=\"llms-editable-toggle-group\">\n\t\t\t\t\t\t<label class=\"llms-switch\">\n\t\t\t\t\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Image', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<input type=\"checkbox\" name=\"image.enabled\"<# if ( 'yes' === data.get( 'image' ).get( 'enabled' ) ) { print( ' checked' ) } #>>\n\t\t\t\t\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t<# if ( 'yes' === data.get( 'image' ).get( 'enabled' ) ) { #>\n\t\t\t\t\t\t\t<div class=\"llms-editable-image\">\n\t\t\t\t\t\t\t\t<# if ( data.get( 'image' ).get( 'src' ) ) { #>\n\t\t\t\t\t\t\t\t\t<div class=\"llms-image\">\n\t\t\t\t\t\t\t\t\t\t<a class=\"llms-action-icon danger tip--top-left\" data-attribute=\"image\" data-tip=\"<?php esc_attr_e( 'Remove image', 'lifterlms' ); ?>\" href=\"#llms-remove-image\">\n\t\t\t\t\t\t\t\t\t\t\t<i class=\"fa fa-times-circle\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Remove image', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t<img alt=\"<?php esc_attr_e( 'image preview', 'lifterlms' ); ?>\" src=\"{{{ data.get( 'image' ).get( 'src' ) }}}\">\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<# } else { #>\n\t\t\t\t\t\t\t\t\t<button class=\"llms-element-button small llms-add-image\" data-attribute=\"image\" data-image-size=\"full\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"fa fa-picture-o\"></span> <?php esc_html_e( 'Add Image', 'lifterlms' ); ?>\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t<# } #>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<# } #>\n\t\t\t\t\t</div>\n\t\t\t\t<# } #>\n\n\t\t\t\t<# if ( data.get( 'question_type' ).get( 'video' ) ) { #>\n\t\t\t\t\t<div class=\"llms-editable-toggle-group\">\n\t\t\t\t\t\t<label class=\"llms-switch\">\n\t\t\t\t\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Video', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<input type=\"checkbox\" name=\"video_enabled\"<# if ( 'yes' === data.get( 'video_enabled' ) ) { print( ' checked' ) } #>>\n\t\t\t\t\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t<# if ( 'yes' === data.get( 'video_enabled' ) ) { #>\n\t\t\t\t\t\t\t<div class=\"llms-editable-video tip--top-right\" data-tip=\"<?php esc_attr_e( 'Use YouTube, Vimeo, or Wistia video URLS.', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t\t\t<input class=\"llms-input standard\" data-attribute=\"video_src\" data-original-content=\"{{{ data.get( 'video_src' ) }}}\" placeholder=\"<?php esc_attr_e( 'https://', 'lifterlms' ); ?>\" data-type=\"video\" name=\"video_src\" value=\"{{{ data.get( 'video_src' ) }}}\">\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<# } #>\n\t\t\t\t\t</div>\n\t\t\t\t<# } #>\n\t\t\t</div>\n\n\t\t\t<?php do_action( 'llms_builder_question_after_features' ); ?>\n\n\t\t</div>\n\n\t\t<# if ( data.get( 'question_type' ).get( 'choices' ) ) { #>\n\t\t\t<div class=\"llms-question-choices-wrapper\">\n\n\t\t\t\t<header class=\"llms-question-choices-list-header\">\n\n\t\t\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Choices', 'lifterlms' ); ?></span>\n\n\t\t\t\t\t<# if ( data.get( 'question_type' ).get_multi_choices() && data.get( 'question_type' ).get_choice_selectable() ) { #>\n\t\t\t\t\t\t<label class=\"llms-switch\">\n\t\t\t\t\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Multiple Correct Choices', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<input type=\"checkbox\" name=\"multi_choices\"<# if ( 'yes' === data.get( 'multi_choices' ) ) { print( ' checked' ) } #>>\n\t\t\t\t\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t<# } #>\n\n\t\t\t\t</header>\n\n\t\t\t\t<ul class=\"llms-question-choices<# if ( 'yes' === data.get( 'multi_choices' ) ) { print( ' multi-choices' ) } #>\"></ul>\n\t\t\t</div>\n\t\t<# } else if ( 'group' === data.get( 'question_type' ).get( 'id' ) ) { #>\n\t\t\t<ul class=\"llms-quiz-questions\" data-empty-msg=\"<?php esc_attr_e( 'Drag a question here to add it to the group.', 'lifterlms' ); ?>\"></ul>\n\t\t<# } #>\n\n\t\t<div class=\"llms-question-features\">\n\n\t\t\t<# if ( data.get( 'question_type' ).get( 'clarifications' ) ) { #>\n\t\t\t\t<div class=\"llms-settings-row\">\n\t\t\t\t\t<div class=\"llms-editable-toggle-group\">\n\t\t\t\t\t\t<label class=\"llms-switch\">\n\t\t\t\t\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Result Clarifications', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t<input type=\"checkbox\" name=\"clarifications_enabled\"<# if ( 'yes' === data.get( 'clarifications_enabled' ) ) { print( ' checked' ) } #>>\n\t\t\t\t\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<# if ( 'yes' === data.get( 'clarifications_enabled' ) ) { #>\n\t\t\t\t\t\t\t<div class=\"llms-editable-editor\">\n\t\t\t\t\t\t\t\t<textarea data-attribute=\"clarifications\" id=\"question-clarifications--{{{ data.get( 'id' ) }}}\">{{{ data.get( 'clarifications' ) }}}</textarea>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<# } #>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t<# } #>\n\n\t\t</div>\n\n\t</section>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/quiz.php",
    "content": "<?php\n/**\n * Builder quiz model view\n *\n * @since   3.16.0\n * @version 3.17.6\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-quiz-template\">\n\n\t<# if ( _.isEmpty( data ) ) { #>\n\n\t\t<div class=\"llms-quiz-empty\">\n\n\t\t\t<p><?php esc_html_e( 'There\\'s no quiz associated with this lesson.', 'lifterlms' ); ?></p>\n\n\t\t\t<button class=\"llms-element-button\" id=\"llms-new-quiz\" type=\"button\">\n\t\t\t\t<?php esc_html_e( 'Create New Quiz', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-file\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t\t<br>\n\n\t\t\t<button class=\"llms-element-button\" id=\"llms-existing-quiz\" type=\"button\">\n\t\t\t\t<?php esc_html_e( 'Add Existing Quiz', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-file-text\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t</div>\n\n\t<# } else { #>\n\n\t\t<header class=\"llms-model-header\" id=\"llms-lesson-header\">\n\n\t\t\t<h3 class=\"llms-headline llms-model-title\">\n\t\t\t\t<?php esc_html_e( 'Title', 'lifterlms' ); ?>: <span class=\"llms-input llms-editable-title\" contenteditable=\"true\" data-attribute=\"title\" data-original-content=\"{{{ data.get( 'title' ) }}}\" data-required=\"required\">{{{ data.get( 'title' ) }}}</span>\n\t\t\t</h3>\n\n\t\t\t<div class=\"llms-headline llms-quiz-points\">\n\t\t\t\t<?php esc_html_e( 'Total Points', 'lifterlms' ); ?>: <strong id=\"llms-quiz-total-points\">{{{ data.get( '_points' ) }}}</strong>\n\t\t\t</div>\n\n\t\t\t<label class=\"llms-switch llms-model-status\">\n\t\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Published', 'lifterlms' ); ?></span>\n\t\t\t\t<input data-off=\"draft\" data-on=\"publish\" name=\"status\" type=\"checkbox\"<# if ( 'publish' === data.get( 'status' ) ) { print( ' checked' ) } #>>\n\t\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t\t</label>\n\n\t\t\t<div class=\"llms-action-icons\">\n\n\t\t\t\t<# if ( ! data.has_temp_id() ) { #>\n\t\t\t\t\t<a class=\"llms-action-icon danger tip--bottom-left\" data-tip=\"<?php esc_attr_e( 'Detach Quiz', 'lifterlms' ); ?>\" href=\"#llms-detach-model\">\n\t\t\t\t\t\t<i class=\"fa fa-chain-broken\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Detach Quiz', 'lifterlms' ); ?></span>\n\t\t\t\t\t</a>\n\t\t\t\t<# } #>\n\n\t\t\t\t<a class=\"llms-action-icon danger tip--bottom-left\" data-tip=\"<?php esc_html_e( 'Delete Quiz', 'lifterlms' ); ?>\" href=\"#llms-trash-model\" tabindex=\"-1\">\n\t\t\t\t\t<i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Delete Quiz', 'lifterlms' ); ?></span>\n\t\t\t\t</a>\n\n\t\t\t</div>\n\n\t\t</header>\n\n\t\t<?php do_action( 'llms_builder_quiz_before_settings' ); ?>\n\n\t\t<div id=\"llms-quiz-settings-fields\"></div>\n\n\t\t<?php do_action( 'llms_builder_quiz_after_settings' ); ?>\n\n\t\t<ul class=\"llms-quiz-questions\" data-empty-msg=\"<?php esc_attr_e( 'Click \"Add Question\" below to start building your quiz!', 'lifterlms' ); ?>\" id=\"llms-quiz-questions\"></ul>\n\n\t\t<footer class=\"llms-quiz-footer\">\n\n\t\t\t<button class=\"llms-element-button secondary small right bulk-toggle\" data-action=\"collapse\" id=\"llms-question-collapse-all\">\n\t\t\t\t<?php esc_html_e( 'Collapse All', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-minus-circle\"></i>\n\t\t\t</button>\n\n\t\t\t<button class=\"llms-element-button secondary small right bulk-toggle\" data-action=\"expand\" id=\"llms-question-expand-all\">\n\t\t\t\t<?php esc_html_e( 'Expand All', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-plus-circle\"></i>\n\t\t\t</button>\n\n\t\t\t<button class=\"llms-element-button small right llms-show-question-bank\" id=\"llms-show-question-bank\">\n\t\t\t\t<?php esc_html_e( 'Add Question', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-plus-circle\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t</footer>\n\n\t\t<div class=\"llms-quiz-tools\" id=\"llms-quiz-tools\">\n\n\t\t\t<ul class=\"llms-question-bank\" id=\"llms-question-bank\"></ul>\n\n\t\t</div>\n\n\t<# } #>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/section.php",
    "content": "<?php\n/**\n * Builder section model\n *\n * @since   3.16.0\n * @version 3.17.2\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-section-template\">\n\n\t<span class=\"llms-drag-utility drag-section\"></span>\n\n\t<header class=\"llms-builder-header\">\n\n\t\t<h2 class=\"llms-headline\">\n\t\t\t<span class=\"llms-input\" contenteditable=\"true\" data-attribute=\"title\" data-original-content=\"{{{ data.title }}}\" data-required=\"required\">{{{ data.title }}}</span>\n\t\t</h2>\n\n\t\t<div class=\"llms-action-icons\">\n\n\t\t\t<div class=\"llms-action-icons-left\">\n\n\t\t\t\t<?php if ( current_user_can( 'delete_course', $course_id ) ) : ?>\n\t\t\t\t\t<button class=\"llms-action-icon llms-trash-model trash--section danger tip--top-right\" data-tip=\"<?php esc_attr_e( 'Delete section', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<span class=\"fa fa-trash\"></span>\n\t\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Delete section', 'lifterlms' ); ?></span>\n\t\t\t\t\t</button>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-action-icons-right\">\n\n\t\t\t\t<button class=\"llms-action-icon shift-up--section tip--top-right\" data-tip=\"<?php esc_attr_e( 'Shift up', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Shift up', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"fa fa-chevron-up\"></span>\n\t\t\t\t</button>\n\n\t\t\t\t<button class=\"llms-action-icon shift-down--section tip--top-right\" data-tip=\"<?php esc_attr_e( 'Shift down', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Shift down', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"fa fa-chevron-down\"></span>\n\t\t\t\t</button>\n\n\t\t\t\t<# if ( ! data._expanded ) { #>\n\t\t\t\t\t<button class=\"llms-action-icon expand tip--top-right\" data-tip=\"<?php esc_attr_e( 'Expand section', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Expand section', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<span class=\"fa fa-caret-down\"></span>\n\t\t\t\t\t</button>\n\t\t\t\t<# } #>\n\n\t\t\t\t<# if ( data._expanded ) { #>\n\t\t\t\t\t<button class=\"llms-action-icon collapse tip--top-right\" data-tip=\"<?php esc_attr_e( 'Collapse section', 'lifterlms' ); ?>\" aria-label=\"<?php esc_attr_e( 'Collapse section', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<span class=\"fa fa-caret-up\"></span>\n\t\t\t\t\t</button>\n\t\t\t\t<# } #>\n\n\t\t\t</div>\n\n\t\t</div>\n\n\t</header>\n\n\t<ul class=\"llms-lessons<# if ( data._expanded ) { #> expanded<# } #>\" id=\"llms-lessons-{{{ data.id }}}\"></ul>\n\n\t<# if ( data._expanded ) { #>\n\t\t<div class=\"llms-builder-footer\">\n\t\t\t<button class=\"llms-button-secondary small new-lesson\">\n\t\t\t\t<span class=\"fa fa-file\"></span> <?php esc_html_e( 'Add New Lesson', 'lifterlms' ); ?>\n\t\t\t</button>\n\t\t</div>\n\t<# } #>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/settings-fields.php",
    "content": "<?php\n/**\n * Model Field Settings Template.\n *\n * @since 3.17.0\n * @since 3.24.0 Unknown.\n * @since 7.4.0 Added support for `upsell` field type and multiple input fields.\n * @version 7.4.0\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n<script type=\"text/html\" id=\"tmpl-llms-settings-fields-template\">\n\n<# _.each( data.get_groups(), function( group_data, group_id ) { #>\n\t<section class=\"llms-model-settings active settings-group--{{{ group_id }}}{{ data.is_group_hidden( group_id ) ? ' hidden' : '' }}\" id=\"llms-{{{ data.model.get( 'type' ) }}}-settings-group--{{{ group_id }}}\">\n\n\t\t<# if ( group_data.title ) { #>\n\t\t\t<header class=\"llms-settings-group-header\">\n\t\t\t\t<h4 class=\"llms-settings-group-title\">{{{ group_data.title }}}</h4>\n\t\t\t\t<# if ( group_data.toggleable ) { #>\n\t\t\t\t\t<a class=\"llms-action-icon llms-settings-group-toggle\" href=\"#llms-group-toggle\">\n\t\t\t\t\t\t<i class=\"fa fa-caret-up\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t<i class=\"fa fa-caret-down\" aria-hidden=\"true\"></i>\n\t\t\t\t\t</a>\n\t\t\t\t<# } #>\n\t\t\t</header>\n\t\t<# } #>\n\n\t\t<div class=\"llms-settings-group-body\">\n\n\t\t<# _.each( group_data.fields, function( row, row_index ) { #>\n\t\t\t<div class=\"llms-settings-row\">\n\t\t\t<# _.each( row, function( orig_field, field_index ) { #>\n\n\t\t\t\t<#\n\t\t\t\t\tvar field = data.setup_field( orig_field, field_index );\n\t\t\t\t\tif ( ! field ) { return; }\n\t\t\t\t#>\n\n\t\t\t\t<div class=\"llms-settings-field settings-field--{{{ field.type }}}<# if ( field.label_after ) { #> has-label-after<# } #>\" id=\"llms-model-settings-field--{{{ field.id }}}\">\n\n\t\t\t\t\t<# if ( data.has_switch( field.type ) ) { #>\n\t\t\t\t\t\t<div class=\"llms-editable-select{{{ field.classes }}}\" >\n\t\t\t\t\t\t\t<label class=\"llms-switch\">\n\t\t\t\t\t\t\t\t<span class=\"llms-label\">\n\t\t\t\t\t\t\t\t\t{{{ field.label }}}\n\t\t\t\t\t\t\t\t\t<# if ( field.tip ) { #>\n\t\t\t\t\t\t\t\t\t\t<span class=\"tip--{{{ field.tip_position }}}\" data-tip=\"{{{ field.tip }}}\"><i class=\"fa fa-question-circle\"></i></span>\n\t\t\t\t\t\t\t\t\t<# } #>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<input data-on=\"{{{ field.switch_on }}}\" data-off=\"{{{ field.switch_off }}}\" data-rerender=\"{{{ data.should_rerender_on_toggle( field.type ) }}}\" name=\"{{{ data.get_switch_attribute( field ) }}}\" type=\"checkbox\"{{{ _.checked( field.switch_on, data.model.get( data.get_switch_attribute( field ) ) ) }}}>\n\t\t\t\t\t\t\t\t<div class=\"llms-switch-slider\"></div>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t<# } else if ( field.label ) { #>\n\t\t\t\t\t\t<span class=\"llms-label\">\n\t\t\t\t\t\t\t{{{ field.label }}}\n\t\t\t\t\t\t\t<# if ( field.tip ) { #>\n\t\t\t\t\t\t\t\t<span class=\"tip--{{{ field.tip_position }}}\" data-tip=\"{{{ field.tip }}}\"><i class=\"fa fa-question-circle\"></i></span>\n\t\t\t\t\t\t\t<# } #>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t<# } #>\n\n\t\t\t\t\t<# if ( 'permalink' === field.type ) { #>\n\n\t\t\t\t\t\t<a target=\"_blank\" href=\"{{{ data.model.get( 'permalink' ) }}}\">{{{ data.model.get( 'permalink' ) }}}</a>\n\t\t\t\t\t\t<input class=\"llms-input permalink\" data-attribute=\"name\" data-original-content=\"{{{ data.model.get( 'name' ) }}}\" data-type=\"permalink\" name=\"name\" type=\"text\" value=\"{{{ data.model.get( 'name' ) }}}\">\n\t\t\t\t\t\t<a class=\"llms-action-icon\" href=\"#llms-edit-slug\"><i class=\"fa fa-pencil\" aria-hidden=\"true\"></i></a>\n\n\t\t\t\t\t<# } else if ( 'page_builder_notice' === field.type ) { #>\n\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\tesc_html_e( \"This lesson's content was created outside of the Course Builder.\", 'lifterlms' );\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t<a href=\"{{{ data.model.get_edit_post_link() }}}\" target=\"_blank\"><?php esc_html_e( 'Edit in WordPress', 'lifterlms' ); ?></a>\n\t\t\t\t\t\t</p>\n\n\t\t\t\t\t<# } else if ( 'upsell' === field.type ) { #>\n\n\t\t\t\t\t\t<a target=\"_blank\" href=\"{{{ field.url }}}\">\n\t\t\t\t\t\t\t<span class=\"llms-disabled\">{{{ field.text }}}</span>\n\t\t\t\t\t\t</a>\n\n\t\t\t\t\t<# } else if ( 'select' === field.type || ( 'switch-select' === field.type && data.is_switch_condition_met( field ) ) ) { #>\n\n\t\t\t\t\t\t<div class=\"llms-editable-select{{{ field.classes }}}\" >\n\t\t\t\t\t\t\t<select name=\"{{{ field.attribute }}}\"{{{ field.multiple ? ' multiple' : '' }}}>{{{ data.render_select_options( field.options, field.attribute ) }}}</select>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t<# } else if ( 'radio' === field.type || ( 'switch-radio' === field.type && data.is_switch_condition_met( field ) ) ) { #>\n\n\t\t\t\t\t\t<div class=\"llms-editable-radio{{{ field.classes }}}\">\n\t\t\t\t\t\t\t<# _.each( field.options, function( label, val ) { #>\n\t\t\t\t\t\t\t\t<label for=\"{{{ field.id }}}_{{{ val }}}\" class=\"llms-radio\">\n\t\t\t\t\t\t\t\t\t<input id=\"{{{ field.id }}}_{{{ val }}}\" name=\"{{{ field.attribute }}}\" type=\"radio\" value=\"{{{ val }}}\"{{{ _.checked( val, data.model.get( field.attribute ) ) }}}>\n\t\t\t\t\t\t\t\t\t{{{ label }}}\n\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<# } ); #>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t<# } else if ( data.is_editor_field( field.type ) ) { #>\n\n\t\t\t\t\t\t<# if ( -1 === field.type.indexOf( 'switch-' ) || ( -1 !== field.type.indexOf( 'switch-' ) && data.is_switch_condition_met( field ) ) ) { #>\n\t\t\t\t\t\t\t<div class=\"llms-editable-editor{{{ field.classes }}}\">\n\t\t\t\t\t\t\t\t<textarea data-attribute=\"{{{ field.attribute }}}\" id=\"{{{ field.id }}}\">{{{ data.model.get( field.attribute ) }}}</textarea>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<# } #>\n\n\t\t\t\t\t<# } else if ( data.is_default_field( field.type ) ) { #>\n\n\t\t\t\t\t\t<# const field_inputs = field.inputs?.length ? field.inputs : [ field ]; #>\n\n\t\t\t\t\t\t<# if ( -1 === field.type.indexOf( 'switch-' ) || ( -1 !== field.type.indexOf( 'switch-' ) && data.is_switch_condition_met( field ) ) ) { #>\n\t\t\t\t\t\t\t<div class=\"llms-editable-input{{{ field.classes }}}\">\n\t\t\t\t\t\t\t\t<# field_inputs.forEach( input => { #>\n\t\t\t\t\t\t\t\t\t<div class=\"llms-input-wrapper\">\n\t\t\t\t\t\t\t\t\t\t<# if ( field_inputs.length > 1 && input.label ) { #>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"label\">{{{ input.label }}}</span>\n\t\t\t\t\t\t\t\t\t\t<# } #>\n\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\tclass=\"llms-input standard\"\n\t\t\t\t\t\t\t\t\t\t\tdata-attribute=\"{{{ input.attribute }}}\"\n\t\t\t\t\t\t\t\t\t\t\tdata-original-content=\"{{{ data.model.get( input.attribute ) }}}\"\n\t\t\t\t\t\t\t\t\t\t\t<# if ( 'datepicker' === input.type ) { #>\n\t\t\t\t\t\t\t\t\t\t\t\t<# if ( input.datepicker ) { #> data-date-datepicker=\"{{{ input.datepicker }}}\" <# } #>\n\t\t\t\t\t\t\t\t\t\t\t\t<# if ( input.timepicker ) { #> data-date-timepicker=\"{{{ input.timepicker }}}\" <# } #>\n\t\t\t\t\t\t\t\t\t\t\t\t<# if ( input.date_format ) { #> data-date-format=\"{{{ input.date_format }}}\" <# } #>\n\t\t\t\t\t\t\t\t\t\t\t<# } #>\n\t\t\t\t\t\t\t\t\t\t\t<# if ( input.hasOwnProperty( 'min' ) ) { #> min=\"{{{ input.min }}}\" <# } #>\n\t\t\t\t\t\t\t\t\t\t\t<# if ( input.hasOwnProperty( 'max' ) ) { #> max=\"{{{ input.max }}}\" <# } #>\n\t\t\t\t\t\t\t\t\t\t\tname=\"{{{ input.attribute }}}\"\n\t\t\t\t\t\t\t\t\t\t\t<# if ( input.placeholder ) { #> placeholder=\"{{{ input.placeholder }}}\" <# } #>\n\t\t\t\t\t\t\t\t\t\t\ttype=\"{{{ input.input_type }}}\"\n\t\t\t\t\t\t\t\t\t\t\tvalue=\"{{{ data.model.get( input.attribute ) }}}\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<# if ( input.input_description ) { #>\n\t\t\t\t\t\t\t\t\t\t\t<small class=\"llms-description\">{{{ input.input_description }}}</small>\n\t\t\t\t\t\t\t\t\t\t<# } #>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<# } ); #>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<# } #>\n\t\t\t\t\t<# } #>\n\n\t\t\t\t\t<# if ( field.label_after ) { #>\n\t\t\t\t\t\t<span class=\"llms-label llms-label--after\">{{{ field.label_after }}}</span>\n\t\t\t\t\t<# } #>\n\n\t\t\t\t\t<# if ( field.detail ) { #>\n\t\t\t\t\t\t<div class=\"llms-detail\">{{{ field.detail }}}</div>\n\t\t\t\t\t<# } #>\n\t\t\t\t</div>\n\t\t\t<# } ); #>\n\t\t\t</div>\n\t\t<# } ); #>\n\n\t\t</div>\n\n\t</section>\n<# } ); #>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/sidebar.php",
    "content": "<?php\n/**\n * Builder sidebar view template\n *\n * @since 3.16.0\n * @since 7.2.0 Added video explainer wrapper element.\n * @version 7.2.0\n */\n?>\n\n<script type=\"text/html\" id=\"tmpl-llms-sidebar-template\">\n\n\t<div class=\"llms-elements\" id=\"llms-elements\"></div>\n\t<div class=\"llms-utilities\" id=\"llms-utilities\"></div>\n\t<div class=\"llms-video-explainer\" id=\"llms-video-explainer\">\n\t\t<span class=\"llms-video-explainer-trigger\">\n\t\t\t<a href=\"https://lifterlms.com/docs/using-course-builder/\" target=\"_blank\">\n\t\t\t\t<img\n\t\t\t\t\tsrc=\"<?php echo esc_url( plugin_dir_url( LLMS_PLUGIN_FILE ) . 'assets/images/course-builder-video-thumbnail.jpg' ); ?>\"\n\t\t\t\t\talt=\"<?php esc_attr_e( 'How to Build Your Course Outline with the LifterLMS Course Builder', 'lifterlms' ); ?>\"\n\t\t\t\t/>\n\t\t\t</a>\n\t\t</span>\n\t</div>\n\n\t<div class=\"llms-editor\" id=\"llms-editor\"></div>\n\n\t<footer class=\"llms-builder-save\">\n\n\t\t<button class=\"llms-button-primary llms-save\" data-status=\"saved\" id=\"llms-save-button\" disabled=\"disabled\">\n\t\t\t<i></i><!-- placeholder for LLMS.Spinner -->\n\t\t\t<span class=\"llms-status-indicator status--saved\"><?php esc_html_e( 'Saved', 'lifterlms' ); ?></span>\n\t\t\t<span class=\"llms-status-indicator status--unsaved\"><?php esc_html_e( 'Save changes', 'lifterlms' ); ?></span>\n\t\t\t<span class=\"llms-status-indicator status--saving\"><?php esc_html_e( 'Saving changes...', 'lifterlms' ); ?></span>\n\t\t\t<span class=\"llms-status-indicator status--error\"><?php esc_html_e( 'Error saving changes...', 'lifterlms' ); ?></span>\n\t\t</button>\n\n\t\t<button class=\"llms-button-secondary llms-exit\" id=\"llms-exit-button\"><?php esc_html_e( 'Exit', 'lifterlms' ); ?></button>\n\n\t</footer class=\"llms-builder-save\">\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/builder/utilities.php",
    "content": "<?php\n/**\n * Builder utilities list view\n *\n * @since   3.16.0\n * @version 3.16.0\n */\n?>\n<script type=\"text/html\" id=\"tmpl-llms-utilities-template\">\n\n\t<ul class=\"llms-utilities-list\">\n\t\t<li>\n\t\t\t<button id=\"llms-expand-all\" class=\"llms-utility bulk-toggle\" data-action=\"expand\">\n\t\t\t\t<span class=\"fa fa-caret-down\"></span>\n\t\t\t\t<?php esc_html_e( 'Expand Sections', 'lifterlms' ); ?>\n\t\t\t</button>\n\t\t</li>\n\t\t<li>\n\t\t\t<button id=\"llms-collapse-all\" class=\"llms-utility bulk-toggle\" href=\"#llms-bulk-toggle\" data-action=\"collapse\">\n\t\t\t\t<span class=\"fa fa-caret-up\"></span>\n\t\t\t\t<?php esc_html_e( 'Collapse Sections', 'lifterlms' ); ?>\n\t\t\t</button>\n\t\t</li>\n\t</ul>\n\n</script>\n"
  },
  {
    "path": "includes/admin/views/dashboard/addons.php",
    "content": "<?php\n/**\n * Add-ons meta box HTML.\n *\n * @package LifterLMS/Admin/Views/Dashboard\n *\n * @since 7.1.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$view = new LLMS_Admin_AddOns();\n\n$view->output_for_settings();\n?>\n\n<p>\n\t<a\n\t\tclass=\"llms-button-primary\"\n\t\thref=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-add-ons' ) ); ?>\"><?php esc_html_e( 'View Add-ons & more', 'lifterlms' ); ?></a>\n</p>\n"
  },
  {
    "path": "includes/admin/views/dashboard/blog.php",
    "content": "<?php\n/**\n * Blog meta box HTML.\n *\n * @package LifterLMS/Admin/Views/Dashboard\n *\n * @since 7.1.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// Get RSS Feed(s).\nrequire_once ABSPATH . WPINC . '/feed.php';\n\n// Get a SimplePie feed object from the specified feed source.\n$rss = fetch_feed( 'https://lifterlms.com/feed' );\n\n$maxitems = 0;\n\nif ( ! is_wp_error( $rss ) ) : // Checks that the object is created correctly.\n\n\t// Figure out how many total items there are, but limit it to 3.\n\t$maxitems = $rss->get_item_quantity( 3 );\n\n\t// Build an array of all the items, starting with element 0 (first element).\n\t$rss_items = $rss->get_items( 0, $maxitems );\n\nendif;\n\n?>\n\n<ul>\n\t<?php if ( 0 === $maxitems ) : ?>\n\t\t<li><?php esc_html_e( 'No news found.', 'lifterlms' ); ?></li>\n\t<?php else : ?>\n\t\t<?php // Loop through each feed item and display each item as a hyperlink. ?>\n\t\t<?php foreach ( $rss_items as $item ) : ?>\n\t\t\t<li>\n\t\t\t\t<a\n\t\t\t\t\thref=\"<?php echo esc_url( $item->get_permalink() ); ?>\"\n\t\t\t\t\ttitle=\"<?php printf( esc_attr__( 'Posted %s', 'lifterlms' ), esc_attr( date_i18n( get_option( 'date_format' ), $item->get_date( 'U' ) ) ) ); ?>\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener\"><?php echo esc_html( $item->get_title() ); ?></a>\n\t\t\t\t\t<?php echo esc_html( date_i18n( get_option( 'date_format' ), $item->get_date( 'U' ) ) ); ?>\n\t\t\t</li>\n\t\t<?php endforeach; ?>\n\t<?php endif; ?>\n</ul>\n<p>\n\t<a\n\t\tclass=\"llms-button-secondary small\"\n\t\thref=\"https://lifterlms.com/blog/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Blog\"\n\t\ttarget=\"_blank\"\n\t\trel=\"noopener\"><i class=\"fa fa-external-link\" aria-hidden=\"true\"></i> <?php esc_html_e( 'View More', 'lifterlms' ); ?></a>\n</p>\n"
  },
  {
    "path": "includes/admin/views/dashboard/index.php",
    "content": "<?php // Quiet.\n"
  },
  {
    "path": "includes/admin/views/dashboard/podcast.php",
    "content": "<?php\n/**\n * Podcast meta box HTML.\n *\n * @package LifterLMS/Admin/Views/Dashboard\n *\n * @since 7.1.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// Get RSS Feed(s).\nrequire_once ABSPATH . WPINC . '/feed.php';\n\n// Get a SimplePie feed object from the specified feed source.\n$rss = fetch_feed( 'https://podcast.lifterlms.com/feed/' );\n\n$maxitems = 0;\n\nif ( ! is_wp_error( $rss ) ) : // Checks that the object is created correctly.\n\n\t// Figure out how many total items there are, but limit it to 3.\n\t$maxitems = $rss->get_item_quantity( 3 );\n\n\t// Build an array of all the items, starting with element 0 (first element).\n\t$rss_items = $rss->get_items( 0, $maxitems );\n\nendif;\n\n?>\n\n<ul>\n\t<?php if ( 0 === $maxitems ) : ?>\n\t\t<li><?php esc_html_e( 'No news found.', 'lifterlms' ); ?></li>\n\t<?php else : ?>\n\t\t<?php // Loop through each feed item and display each item as a hyperlink. ?>\n\t\t<?php foreach ( $rss_items as $item ) : ?>\n\t\t\t<li>\n\t\t\t\t<a\n\t\t\t\t\thref=\"<?php echo esc_url( $item->get_permalink() ); ?>\"\n\t\t\t\t\ttitle=\"<?php printf( esc_attr__( 'Posted %s', 'lifterlms' ), esc_attr( date_i18n( get_option( 'date_format' ), $item->get_date( 'U' ) ) ) ); ?>\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener\"><?php echo esc_html( $item->get_title() ); ?></a>\n\t\t\t\t\t<?php echo esc_html( date_i18n( get_option( 'date_format' ), $item->get_date( 'U' ) ) ); ?>\n\t\t\t</li>\n\t\t<?php endforeach; ?>\n\t<?php endif; ?>\n</ul>\n<p>\n\t<a\n\t\tclass=\"llms-button-secondary small\"\n\t\thref=\"https://lifterlms.com/blog/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Podcast\"\n\t\ttarget=\"_blank\"\n\t\trel=\"noopener\"><i class=\"fa fa-external-link\" aria-hidden=\"true\"></i> <?php esc_html_e( 'View More', 'lifterlms' ); ?></a>\n</p>\n"
  },
  {
    "path": "includes/admin/views/dashboard/quick-links.php",
    "content": "<?php\n/**\n * Quick links meta box HTML.\n *\n * @package LifterLMS/Admin/Views/Dashboard\n *\n * @since 7.1.0\n * @since 7.3.0 Added `llms_dashboard_checklist` filter.\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-quick-links\">\n\t<div class=\"llms-list\">\n\t\t<h3><?php esc_html_e( 'Build LMS Content', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"<?php echo esc_url( admin_url( 'post-new.php?post_type=llms_membership' ) ); ?>\"><?php esc_html_e( 'Add a New Membership', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"<?php echo esc_url( admin_url( 'post-new.php?post_type=llms_engagement' ) ); ?>\"><?php esc_html_e( 'Create an Engagement', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t\t<a class=\"llms-button-primary\" href=\"<?php echo esc_url( admin_url( 'post-new.php?post_type=course' ) ); ?>\"><i class=\"fa fa-graduation-cap\" aria-hidden=\"true\"></i>&nbsp;&nbsp;<?php esc_html_e( 'Create a New Course', 'lifterlms' ); ?></a>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><?php esc_html_e( 'Access Reports', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"<?php echo esc_url( admin_url( 'edit.php?post_type=llms_order' ) ); ?>\"><?php esc_html_e( 'View Orders', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=students' ) ); ?>\"><?php esc_html_e( 'View Students', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t\t<a class=\"llms-button-secondary\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=sales' ) ); ?>\"><i class=\"fa fa-line-chart\" aria-hidden=\"true\"></i>&nbsp;&nbsp;<?php esc_html_e( 'View Sales Report', 'lifterlms' ); ?></a>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><?php esc_html_e( 'Your Launch Checklist', 'lifterlms' ); ?></h3>\n\t\t<?php\n\t\t// Count access plans across the whole LMS.\n\t\t$ap_check = false;\n\t\t$ap_query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_type'              => 'llms_access_plan',\n\t\t\t\t'posts_per_page'         => 1,\n\t\t\t\t'post_status'            => 'any', // Retrieves any status except for 'inherit', 'trash' and 'auto-draft'.\n\t\t\t\t'no_found_rows'          => true,\n\t\t\t\t'update_post_meta_cache' => false,\n\t\t\t\t'update_post_term_cache' => false,\n\t\t\t)\n\t\t);\n\n\t\t// If more than 1 access plan, they are \"set up\".\n\t\tif ( $ap_query->post_count >= 1 ) {\n\t\t\t$ap_check = true;\n\t\t}\n\n\t\t// Count enrollments across the whole LMS.\n\t\tglobal $wpdb;\n\t\t$enrollments_check = false;\n\t\t$enrollments       = $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_status' AND meta_value = 'enrolled'\" ); // no-cache ok.\n\t\t// If more than 10 enrollments, they are \"set up\".\n\t\tif ( $enrollments >= 10 ) {\n\t\t\t$enrollments_check = true;\n\t\t}\n\n\t\t// Add checklist items to an array so we can filter.\n\t\t$checklist = array();\n\t\tif ( $ap_check ) {\n\t\t\t$checklist['access_plan'] = '<i class=\"fa fa-check\"></i> ' . esc_html__( 'Create Access Plan', 'lifterlms' );\n\t\t} else {\n\t\t\t$checklist['access_plan'] = '<i class=\"fa fa-times\"></i> <a href=\"https://lifterlms.com/docs/what-is-an-access-plan/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=Create%20Access%20Plan\" target=\"_blank\" rel=\"noopener\">' . esc_html__( 'Create Access Plan', 'lifterlms' ) . '</a>';\n\t\t}\n\t\tif ( $enrollments_check ) {\n\t\t\t$checklist['enrollments'] = '<i class=\"fa fa-check\"></i> ' . esc_html__( 'Get 10 Enrollments', 'lifterlms' );\n\t\t} else {\n\t\t\t$checklist['enrollments'] = '<i class=\"fa fa-times\"></i> <a href=\"https://academy.lifterlms.com/course/enroll/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=Get%2010%20Enrollments\" target=\"_blank\" rel=\"noopener\">' . esc_html__( 'Get 10 Enrollments', 'lifterlms' ) . '</a>';\n\t\t}\n\n\t\t/**\n\t\t * Filters the dashboard quick links checklist.\n\t\t *\n\t\t * @since 7.3.0\n\t\t *\n\t\t * @param array $checklist Dashboard quick links checklist.\n\t\t */\n\t\t$checklist = apply_filters( 'llms_dashboard_checklist', $checklist );\n\t\t?>\n\t\t<ul class=\"llms-checklist\">\n\t\t\t<?php\n\t\t\tforeach ( $checklist as $item ) {\n\t\t\t\techo '<li>' . wp_kses_post( $item ) . '</li>';\n\t\t\t}\n\t\t\t?>\n\t\t</ul>\n\t\t<a class=\"llms-button-action\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-add-ons' ) ); ?>\"><i class=\"fa fa-plug\" aria-hidden=\"true\"></i> <?php esc_html_e( 'Add Advanced Features', 'lifterlms' ); ?></a>\n\t</div>\n</div>\n<hr />\n<div class=\"llms-help-links\">\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-admin-users\"></span> <?php esc_html_e( 'Sales', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/pricing/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Pricing\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Pricing', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/store/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Add-ons\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Add-Ons', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/presales-contact/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Presales%20Contact\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Contact Sales', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-editor-help\"></span> <?php esc_html_e( 'Support', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Documentation\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Documentation', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://wordpress.org/support/plugin/lifterlms/\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'WordPress.org Support', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/my-account/my-tickets/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Support\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Premium Support', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-lightbulb\"></span> <?php esc_html_e( 'Learn', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://academy.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Academy\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Academy', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/community-events/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Events\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Events', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://developer.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Developers\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Developers', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-admin-site\"></span> <?php esc_html_e( 'Content', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/blog/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=LifterLMS%20Blog\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Blog', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://podcast.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Dashboard%20Screen&utm_content=Podcast\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Podcast', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://www.youtube.com/lifterlms\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'YouTube', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/admin/views/dashboard.php",
    "content": "<?php\n/**\n * Dashboard Page HTML.\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 7.1.0\n * @since 7.3.0 Leverage new `LLMS_Admin_Dashboard_Widget::get_dashboard_widget_data()` method.\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<div class=\"wrap lifterlms lifterlms-settings llms-dashboard\">\n\n\t<div class=\"llms-subheader\">\n\n\t\t<h1><?php esc_html_e( 'LifterLMS Dashboard', 'lifterlms' ); ?></h1>\n\n\t</div>\n\n\t<div class=\"llms-inside-wrap\">\n\n\t\t<hr class=\"wp-header-end\">\n\n\t\t<div class=\"llms-dashboard-activity\">\n\t\t\t<h2><?php printf( esc_html__( 'Recent Activity: %1$1s to %2$2s', 'lifterlms' ), esc_html( date( get_option( 'date_format' ), current_time( 'timestamp' ) - WEEK_IN_SECONDS ) ), esc_html( date( get_option( 'date_format' ), current_time( 'timestamp' ) ) ) ); ?></h2>\n\t\t\t<?php echo '<style type=\"text/css\">#llms-charts-wrapper{display:none;}</style>'; ?>\n\t\t\t<?php\n\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in template.\n\t\t\t\techo llms_get_template(\n\t\t\t\t\t'admin/reporting/tabs/widgets.php',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'json'        => wp_json_encode(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'current_tab'         => 'settings',\n\t\t\t\t\t\t\t\t'current_range'       => 'last-7-days',\n\t\t\t\t\t\t\t\t'current_students'    => array(),\n\t\t\t\t\t\t\t\t'current_courses'     => array(),\n\t\t\t\t\t\t\t\t'current_memberships' => array(),\n\t\t\t\t\t\t\t\t'dates'               => array(\n\t\t\t\t\t\t\t\t\t'start' => date( 'Y-m-d', current_time( 'timestamp' ) - WEEK_IN_SECONDS ),\n\t\t\t\t\t\t\t\t\t'end'   => current_time( 'Y-m-d' ),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'widget_data' => array( LLMS_Admin_Dashboard_Widget::get_dashboard_widget_data() ),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\t\t</div> <!-- end llms-dashboard-activity -->\n\n\t\t<form id=\"llms-dashboard-form\" method=\"post\" action=\"admin-post.php\">\n\t\t\t<div id=\"poststuff\">\n\t\t\t\t<div id=\"post-body\" class=\"metabox-holder columns-2\">\n\n\t\t\t\t\t<div id=\"postbox-container-1\" class=\"postbox-container\">\n\t\t\t\t\t\t<?php do_meta_boxes( 'toplevel_page_llms-dashboard', 'side', '' ); ?>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div id=\"postbox-container-2\" class=\"postbox-container\">\n\t\t\t\t\t\t<?php do_meta_boxes( 'toplevel_page_llms-dashboard', 'normal', '' ); ?>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<br class=\"clear\">\n\n\t\t\t\t</div> <!-- end dashboard-widgets -->\n\n\t\t\t\t<?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>\n\t\t\t\t<?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>\n\n\t\t\t</div> <!-- end dashboard-widgets-wrap -->\n\t\t</form>\n\t\t<script type=\"text/javascript\">\n\t\t\t//<![CDATA[\n\t\t\tjQuery(document).ready( function($) {\n\t\t\t\t// close postboxes that should be closed\n\t\t\t\t$('.if-js-closed').removeClass('if-js-closed').addClass('closed');\n\t\t\t\t// postboxes setup\n\t\t\t\tpostboxes.add_postbox_toggles('toplevel_page_llms-dashboard');\n\t\t\t});\n\t\t\t//]]>\n\t\t</script>\n\n\t</div>\n\n</div> <!-- end .wrap.llms-dashboard -->\n"
  },
  {
    "path": "includes/admin/views/import/help-sidebar.php",
    "content": "<?php\n/**\n * \"Overview\" Help Tab on the Admin Import screen\n *\n * @package LifterLMS/Admin/Views/Import\n *\n * @since 4.8.0\n * @version 4.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<p><strong><?php esc_html_e( 'For more information:', 'lifterlms' ); ?></strong></p>\n\n<p><a href=\"https://lifterlms.com/docs/importing-lifterlms-courses?utm_source=helptab&utm_medium=product&utm_content=importdocs&utm_campaign=lifterlmsplugin\" target=\"_blank\">\n\t<?php esc_html_e( 'Import Documentation', 'lifterlms' ); ?></a></p>\n"
  },
  {
    "path": "includes/admin/views/import/help-tab-overview.php",
    "content": "<?php\n/**\n * \"Overview\" Help Tab on the Admin Import screen\n *\n * @package LifterLMS/Admin/Views/Import\n *\n * @since 4.8.0\n * @version 4.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<h4><?php esc_html_e( 'Importing into LifterLMS', 'lifterlms' ); ?></h4>\n\n<p><?php esc_html_e( 'Importing allows you to copy courses from one LifterLMS site to another.', 'lifterlms' ); ?></p>\n\n<p><?php esc_html_e( 'You can upload <code>.json</code> files exported from another LifterLMS site using the \"Upload\" button.', 'lifterlms' ); ?></p>\n\n<p><?php esc_html_e( 'The courses listed below are samples and templates you can import into your site directly from LifterLMS.com. When importing these courses the content (including images) will be downloaded from LifterLMS.com and imported into your site.', 'lifterlms' ); ?></p>\n"
  },
  {
    "path": "includes/admin/views/import/index.php",
    "content": "<?php // Quiet.\n"
  },
  {
    "path": "includes/admin/views/import.php",
    "content": "<?php\n/**\n * Import & Export LLMS Content\n *\n * @since 3.3.0\n * @version 4.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$courses = LLMS_Export_API::list();\n?>\n\n<div class=\"wrap lifterlms lifterlms-settings llms-import-export\">\n\n\t<div class=\"llms-inside-wrap\">\n\n\t\t<div class=\"llms-setting-group top\">\n\t\t\t<div class=\"llms-label\">\n\t\t\t\t<?php esc_html_e( 'Import Courses', 'lifterlms' ); ?>\n\t\t\t\t<button class=\"llms-button-primary small\" role=\"button\"><?php esc_html_e( 'Upload', 'lifterlms' ); ?></button>\n\t\t\t</div>\n\n\t\t\t<hr class=\"wp-header-end\">\n\n\t\t\t<div class=\"llms-widget\" style=\"display: none;\" id=\"llms-import-uploader\">\n\n\t\t\t\t<form action=\"\" enctype=\"multipart/form-data\" method=\"POST\">\n\n\t\t\t\t\t<table class=\"form-table\">\n\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th><label for=\"llms-import-file\"><?php esc_html_e( 'Import Course(s)', 'lifterlms' ); ?></label></th>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<p><?php esc_html_e( 'Upload export files generated by LifterLMS. Must be a \".json\" file.', 'lifterlms' ); ?></p>\n\t\t\t\t\t\t\t\t<div class=\"llms-import-file-wrap\">\n\t\t\t\t\t\t\t\t\t<input accept=\"application/json\" name=\"llms_import\" id=\"llms-import-file\" type=\"file\">\n\t\t\t\t\t\t\t\t\t<button class=\"button\" id=\"llms-import-file-submit\" type=\"submit\"><?php esc_html_e( 'Import', 'lifterlms' ); ?></button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t * Fires after core importer(s) on the \"Import screen\".\n\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t * Allows 3rd parties to add their own importers to the table.\n\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t * @since 3.3.0\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\tdo_action( 'lifterlms_importer_tr' );\n\t\t\t\t\t\t?>\n\n\t\t\t\t\t</table>\n\n\t\t\t\t\t<?php wp_nonce_field( 'llms-importer', 'llms_importer_nonce' ); ?>\n\n\t\t\t\t</form>\n\n\t\t\t</div>\n\n\t\t\t<p>\n\t\t\t\t<?php\n\t\t\t\t\t// Translators: %s = anchor link HTML to LifterLMS.com.\n\t\t\t\t\tprintf( esc_html__( 'Download and import courses, templates, and more from %s.', 'lifterlms' ), '<a href=\"https://lifterlms.com\" target=\"_blank\">LifterLMS.com</a>' );\n\t\t\t\t?>\n\t\t\t\t<button class=\"llms-cloud-import-help button-link\" type=\"button\" title=\"<?php esc_attr_e( 'Help', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Help', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"dashicons dashicons-editor-help\"></span>\n\t\t\t\t</button>\n\t\t\t</p>\n\n\t\t\t<form action=\"\" method=\"POST\">\n\t\t\t\t<?php require LLMS_PLUGIN_DIR . 'includes/admin/views/importable-courses.php'; ?>\n\t\t\t\t<?php wp_nonce_field( 'llms-cloud-importer', 'llms_cloud_importer_nonce' ); ?>\n\t\t\t</form>\n\t\t</div>\n\n\t</div>\n\n</div>\n\n<script>\n( function() {\n\tdocument.querySelector( '.llms-button-primary' ).addEventListener( 'click', function() {\n\t\tconst el = document.getElementById( 'llms-import-uploader' );\n\t\tel.style.display = 'none' === el.style.display ? 'block' : 'none';\n\t} );\n\tdocument.querySelector( '.llms-cloud-import-help' ).addEventListener( 'click', function( e ) {\n\t\tdocument.getElementById( 'contextual-help-link' ).click();\n\t} );\n} )();\n</script>\n"
  },
  {
    "path": "includes/admin/views/importable-course.php",
    "content": "<?php\n/**\n * Display a single importable course\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 4.8.0\n * @version 4.8.0\n *\n * @property array $course A hash of importable course data.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Filters whether or not an action button should be displayed for an importable course.\n *\n * @since 4.8.0\n *\n * @param boolean $show_button Whether or not to show the button.\n * @param array   $course      Hash of the importable course data.\n */\n$show_button = apply_filters( 'llms_importable_course_show_action', true, $course );\n?>\n<li class=\"llms-importable-course<?php echo $show_button ? ' has-action-button' : ''; ?>\">\n\n\t<?php\n\t\t/**\n\t\t * Action run prior to the output of an importable course item\n\t\t *\n\t\t * @since 4.8.0\n\t\t *\n\t\t * @param array $course Hash of the importable course data.\n\t\t */\n\t\tdo_action( 'llms_before_importable_course', $course );\n\t?>\n\n\t<img alt=\"<?php printf( esc_attr__( '%s featured image', 'lifterlms' ), esc_attr( $course['title'] ) ); ?>\" src=\"<?php echo esc_url( $course['image'] ); ?>\">\n\n\t<h3><?php echo esc_html( $course['title'] ); ?></h3>\n\t<p><?php echo esc_html( $course['description'] ); ?></p>\n\n\t<?php\n\t\t/**\n\t\t * Action run after the output of an importable course item\n\t\t *\n\t\t * This runs after the item's content but before the item's action button.\n\t\t *\n\t\t * @since 4.8.0\n\t\t *\n\t\t * @param array $course Hash of the importable course data.\n\t\t */\n\t\tdo_action( 'llms_after_importable_course', $course );\n\t?>\n\n\t<?php if ( $show_button ) : ?>\n\t\t<p><button class=\"llms-button-secondary\" name=\"llms_cloud_import_course_id\" type=\"submit\" value=\"<?php echo absint( $course['id'] ); ?>\"><?php esc_html_e( 'Download & Import', 'lifterlms' ); ?></button></p>\n\t<?php endif; ?>\n\n</li>\n"
  },
  {
    "path": "includes/admin/views/importable-courses.php",
    "content": "<?php\n/**\n * List importable courses\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 4.8.0\n * @version 4.8.0\n *\n * @property array[] $courses List of importable course data.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php if ( is_wp_error( $courses ) ) : ?>\n\n\t<p class=\"llms-error\">\n\t\t<?php\n\t\t\t// Translators: %s = Text of the HTTP Request error message.\n\t\t\tprintf( esc_html__( 'There was an error loading importable courses. Please reload the page to try again. [%s]', 'lifterlms' ), wp_kses_post( $courses->get_error_message() ) );\n\t\t?>\n\t</p>\n\n<?php else : ?>\n\t<ul class=\"llms-importable-courses\">\n\t\t<?php\n\t\t/**\n\t\t * Action run prior to the output of an importable course list\n\t\t *\n\t\t * @since 4.8.0\n\t\t *\n\t\t * @param array[] $courses List of importable course data.\n\t\t */\n\t\tdo_action( 'llms_before_importable_courses', $courses );\n\n\t\tforeach ( $courses as $course ) {\n\t\t\tinclude LLMS_PLUGIN_DIR . 'includes/admin/views/importable-course.php';\n\t\t}\n\n\t\t/**\n\t\t * Action run after the output of an importable course list\n\t\t *\n\t\t * @since 4.8.0\n\t\t *\n\t\t * @param array[] $courses List of importable course data.\n\t\t */\n\t\tdo_action( 'llms_after_importable_courses', $courses );\n\t\t?>\n\t</ul>\n<?php endif; ?>\n"
  },
  {
    "path": "includes/admin/views/index.php",
    "content": "<?php // Shhhh.\n"
  },
  {
    "path": "includes/admin/views/merge-code-button.php",
    "content": "<?php\n/**\n * Display an MCE editor merge code button (and drop down).\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 6.0.0\n * @version 6.0.0\n *\n * @see llms_merge_code_button()\n *\n * @param array[]        $codes  Associative array of merge codes where the array key is the merge code and the array value is a name / description of the merge code.\n * @param WP_Screen|null $screen The screen object from `get_current_screen().\n * @param string         $target Target to add the merge code to. Accepts the ID of a tinymce editor or a DOM ID (#element-id).\n */\n\ndefined( 'ABSPATH' ) || exit;\n$svg = file_get_contents( LLMS_PLUGIN_DIR . '/assets/images/lifterlms-icon-grey.svg' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents\n?>\n\n<div class=\"llms-merge-code-wrapper\">\n\n\t<button class=\"button llms-merge-code-button\" type=\"button\">\n\t\t<?php echo $svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>\n\t\t<?php esc_html_e( 'Merge Codes', 'lifterlms' ); ?>\n\t</button>\n\n\t<div class=\"llms-merge-codes\" data-target=\"<?php echo esc_attr( $target ); ?>\">\n\t\t<ul>\n\t\t\t<?php foreach ( $codes as $code => $desc ) : ?>\n\t\t\t\t<li data-code=\"<?php echo esc_attr( $code ); ?>\"><?php echo wp_kses_post( $desc ); ?></li>\n\t\t\t<?php endforeach; ?>\n\t\t</ul>\n\t</div>\n\n</div><!-- .llms-merge-code-wrapper -->\n"
  },
  {
    "path": "includes/admin/views/metaboxes/index.php",
    "content": "<?php // Quiet you.\n"
  },
  {
    "path": "includes/admin/views/metaboxes/view-award-engagement-submit.php",
    "content": "<?php\n/**\n * Award Engagements submit meta box.\n *\n * Heavily based on WordPress `post_submit_meta_box()`.\n *\n * @package LifterLMS/Admin/Views/Metaboxes\n *\n * @since 6.0.0\n * @version 6.0.0\n *\n * @property WP_Post $engagement    WP_Post instance of the engagement.\n * @property int     $engagement_id WP_Post ID of the engagement.\n * @property string  $action        The action being performed.\n * @property bool    $can_publish   Whether the current user can publish the engagement.\n * @property string  $fields        Meta box fields such as student information ones.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"submitbox llms-award-engagement-submitbox\" id=\"submitpost\">\n\t<div id=\"minor-publishing\">\n\t\t<?php // Hidden submit button early on so that the browser chooses the right button when form is submitted with Return key. ?>\n\t\t<div style=\"display:none;\">\n\t\t\t<?php submit_button( __( 'Save', 'lifterlms' ), '', 'save' ); ?>\n\t\t</div>\n\t\t<div class=\"clear\"></div>\n\n\t\t<div id=\"misc-publishing-actions\">\n\t\t\t<div class=\"misc-pub-section misc-pub-post-status\">\n\t\t\t\t<?php esc_html_e( 'Status:', 'lifterlms' ); ?>\n\t\t\t\t<span id=\"post-status-display\">\n\t\t\t\t\t<?php\n\t\t\t\t\tswitch ( $engagement->post_status ) {\n\t\t\t\t\t\tcase 'publish':\n\t\t\t\t\t\t\tesc_html_e( 'Awarded', 'lifterlms' );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'future':\n\t\t\t\t\t\t\tesc_html_e( 'Scheduled', 'lifterlms' );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tcase 'draft':\n\t\t\t\t\t\tcase 'auto-draft':\n\t\t\t\t\t\t\tesc_html_e( 'Draft', 'lifterlms' );\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t?>\n\t\t\t\t</span>\n\t\t\t\t<?php if ( 'publish' === $engagement->post_status || $can_publish ) : /* Select needed because the core js takes the current status from this, when changing the publishing time */ ?>\n\t\t\t\t\t<div id=\"post-status-select\" class=\"hidden\">\n\t\t\t\t\t<select name=\"post_status\" id=\"post_status\">\n\t\t\t\t\t\t<?php if ( 'publish' === $engagement->post_status ) : ?>\n\t\t\t\t\t\t\t<option<?php selected( $engagement->post_status, 'publish' ); ?> value='publish'><?php esc_html_e( 'Awarded', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<?php elseif ( 'future' === $engagement->post_status ) : ?>\n\t\t\t\t\t\t\t<option<?php selected( $engagement->post_status, 'future' ); ?> value='future'><?php esc_html_e( 'Scheduled', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t<option<?php selected( $engagement->post_status, 'auto-draft' ); ?> value='draft'><?php esc_html_e( 'Draft', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</select>\n\t\t\t\t\t</div>\n\t\t\t\t<?php endif; ?>\n\t\t\t</div>\n\t\t</div>\n\t\t<?php\n\t\t/* translators: Award box date string. 1: Date, 2: Time. See https://www.php.net/manual/datetime.format.php */\n\t\t$date_string = __( '%1$s at %2$s', 'lifterlms' );\n\t\t/* translators: Award box date format, see https://www.php.net/manual/datetime.format.php */\n\t\t$date_format = _x( 'M j, Y', 'award box date format', 'lifterlms' );\n\t\t/* translators: Award box time format, see https://www.php.net/manual/datetime.format.php */\n\t\t$time_format = _x( 'H:i', 'award box time format', 'lifterlms' );\n\n\t\tif ( 0 !== $engagement_id ) {\n\t\t\tif ( 'future' === $engagement->post_status ) { // Scheduled for awarding at a future date.\n\t\t\t\t/* translators: Engagement date information. %s: Date on which the engagement is currently scheduled to be awarded. */\n\t\t\t\t$stamp = __( 'Scheduled for: %s', 'lifterlms' );\n\t\t\t} elseif ( 'publish' === $engagement->post_status ) { // Already awarded.\n\t\t\t\t/* translators: Post date information. %s: Date on which the engagement was awarded. */\n\t\t\t\t$stamp = __( 'Awarded on: %s', 'lifterlms' );\n\t\t\t} elseif ( '0000-00-00 00:00:00' === $engagement->post_date_gmt ) { // Draft, 1 or more saves, no date specified.\n\t\t\t\t$stamp = __( 'Award <b>immediately</b>', 'lifterlms' );\n\t\t\t} elseif ( llms_current_time( 'U', true ) < strtotime( $engagement->post_date_gmt . ' +0000' ) ) { // Draft, 1 or more saves, future date specified.\n\t\t\t\t/* translators: Post date information. %s: Date on which the post is to be awarded. */\n\t\t\t\t$stamp = __( 'Schedule for: %s', 'lifterlms' );\n\t\t\t} else { // Draft, 1 or more saves, date specified.\n\t\t\t\t/* translators: Post date information. %s: Date on which the post is to be awarded. */\n\t\t\t\t$stamp = __( 'Award on: %s', 'lifterlms' );\n\t\t\t}\n\t\t\t$date = sprintf(\n\t\t\t\t$date_string,\n\t\t\t\tdate_i18n( $date_format, strtotime( $engagement->post_date ) ),\n\t\t\t\tdate_i18n( $time_format, strtotime( $engagement->post_date ) )\n\t\t\t);\n\t\t} else { // Draft (no saves, and thus no date specified).\n\t\t\t$stamp = __( 'Award <b>immediately</b>', 'lifterlms' );\n\t\t\t$date  = sprintf(\n\t\t\t\t$date_string,\n\t\t\t\tdate_i18n( $date_format, strtotime( llms_current_time( 'mysql' ) ) ),\n\t\t\t\tdate_i18n( $time_format, strtotime( llms_current_time( 'mysql' ) ) )\n\t\t\t);\n\t\t}\n\n\t\tif ( $can_publish ) : // Contributors don't get to choose the date of awarding.\n\t\t\t?>\n\t\t\t<div class=\"misc-pub-section curtime misc-pub-curtime\">\n\t\t\t\t<span id=\"timestamp\">\n\t\t\t\t\t<?php echo wp_kses_post( sprintf( $stamp, '<b>' . $date . '</b>' ) ); ?>\n\t\t\t\t</span>\n\t\t\t\t<a href=\"#edit_timestamp\" class=\"edit-timestamp hide-if-no-js\" role=\"button\">\n\t\t\t\t\t<span aria-hidden=\"true\"><?php esc_html_e( 'Edit', 'lifterlms' ); ?></span>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Edit date and time', 'lifterlms' ); ?></span>\n\t\t\t\t</a>\n\t\t\t\t<fieldset id=\"timestampdiv\" class=\"hide-if-js\">\n\t\t\t\t\t<legend class=\"screen-reader-text\"><?php esc_html_e( 'Date and time', 'lifterlms' ); ?></legend>\n\t\t\t\t\t<?php touch_time( ( 'edit' === $action ), 1 ); ?>\n\t\t\t\t</fieldset>\n\t\t\t</div>\n\t\t\t<?php\n\t\tendif;\n\t\t?>\n\t\t<div class=\"clear\"></div>\n\t</div>\n\t<ul id=\"misc-fields\" class=\"misc-pub-section\" style=\"margin-top:0\">\n\t\t<?php\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\techo $fields;\n\t\t?>\n\t</ul>\n\t<div id=\"major-publishing-actions\">\n\t\t<div id=\"delete-action\">\n\t\t\t<?php\n\t\t\tif ( current_user_can( 'delete_post', $engagement_id ) ) {\n\t\t\t\t$delete_text = __( 'Delete permanently', 'lifterlms' );\n\t\t\t\t?>\n\t\t\t\t<a class=\"submitdelete deletion\" href=\"<?php echo get_delete_post_link( $engagement_id, '', true ); ?>\"><?php echo esc_html( $delete_text ); ?></a>\n\t\t\t\t<?php\n\t\t\t}\n\t\t\t?>\n\t\t</div>\n\n\t\t<div id=\"publishing-action\">\n\t\t\t<span class=\"spinner\"></span>\n\t\t\t<?php\n\t\t\tif ( ! in_array( $engagement->post_status, array( 'publish', 'future' ), true ) || 0 === $engagement_id ) {\n\t\t\t\tif ( $can_publish ) :\n\t\t\t\t\tif ( ! empty( $engagement->post_date_gmt ) && llms_current_time( 'U', true ) < strtotime( $engagement->post_date_gmt . ' +0000' ) ) :\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<input name=\"original_publish\" type=\"hidden\" id=\"original_publish\" value=\"<?php echo esc_attr_x( 'Schedule', 'post action/button label', 'lifterlms' ); ?>\" />\n\t\t\t\t\t\t<?php submit_button( _x( 'Schedule', 'post action/button label', 'lifterlms' ), 'primary large', 'publish', false ); ?>\n\t\t\t\t\t\t<?php\n\t\t\t\t\telse :\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<input name=\"original_publish\" type=\"hidden\" id=\"original_publish\" value=\"<?php esc_attr_e( 'Award', 'lifterlms' ); ?>\" />\n\t\t\t\t\t\t<?php submit_button( __( 'Award', 'lifterlms' ), 'primary large', 'publish', false ); ?>\n\t\t\t\t\t\t<?php\n\t\t\t\t\tendif;\n\t\t\t\tendif;\n\t\t\t} else {\n\t\t\t\t?>\n\t\t\t\t<input name=\"original_publish\" type=\"hidden\" id=\"original_publish\" value=\"<?php esc_attr_e( 'Update', 'lifterlms' ); ?>\" />\n\t\t\t\t<?php submit_button( __( 'Update', 'lifterlms' ), 'primary large', 'save', false, array( 'id' => 'publish' ) ); ?>\n\t\t\t\t<?php\n\t\t\t}\n\t\t\t?>\n\t\t</div>\n\t\t<div class=\"clear\"></div>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/admin/views/metaboxes/view-order-details.php",
    "content": "<?php\n/**\n * Order Details metabox for Order on Admin Panel\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 5.3.0\n * @since 5.4.0 Inform about deleted products.\n * @since 6.1.0 Add validation to the remaining payments input.\n *              Allow the number of remaining payments to be `0` for already-completed payment plans.\n * @version 7.0.0\n *\n * @property LLMS_Order           $order                     Order object.\n * @property LLMS_Payment_Gateway $gateway                   Instance of the order's payment gateway.\n * @property array                $switchable_gateways       List of gateways that the order can be switched to.\n * @property array                $switchable_gateway_fields List of admin fields for the available switchable gateways.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$supports_modify_recurring_payments = $order->supports_modify_recurring_payments();\n?>\n\n<div class=\"llms-metabox\">\n\n\t<?php if ( 'test' === $order->get( 'gateway_api_mode' ) ) : ?>\n\t\t<h6 class=\"llms-transaction-test-mode\"><?php esc_html_e( 'This order was processed in the gateway\\'s testing mode', 'lifterlms' ); ?></h6>\n\t<?php endif; ?>\n\n\t<?php do_action( 'lifterlms_before_order_meta_box', $order ); ?>\n\n\n\t<h2><?php echo esc_html( sprintf( __( 'Order #%s', 'lifterlms' ), $order->get( 'id' ) ) ); ?></h2>\n\t<h3><?php echo esc_html( sprintf( __( 'Processed by %s', 'lifterlms' ), is_wp_error( $gateway ) ? $order->get( 'payment_gateway' ) : $gateway->get_admin_title() ) ); ?></h3>\n\n\n\t<?php do_action( 'lifterlms_order_meta_box_after_header', $order ); ?>\n\n\t<div class=\"llms-metabox-section d-1of3\">\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * THIS ACTION HOOK TO BE DEPRECATED!\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_order_meta_box_after_order_information', $order );\n\t\t?>\n\n\t\t<?php do_action( 'lifterlms_order_meta_box_before_plan_information', $order ); ?>\n\n\t\t<?php if ( $order->get( 'plan_id' ) ) : ?>\n\n\t\t\t<h4><?php esc_html_e( 'Access Plan Information', 'lifterlms' ); ?></h4>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Name:', 'lifterlms' ); ?></label>\n\t\t\t\t<?php echo esc_html( $order->get( 'plan_title' ) ); ?>\n\t\t\t\t<small>(#<?php echo esc_html( $order->get( 'plan_id' ) ); ?>)</small>\n\t\t\t</div>\n\n\t\t\t<?php if ( llms_parse_bool( get_option( 'llms_access_plans_allow_skus', 'no' ) ) ) : ?>\n\t\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t\t<label><?php esc_html_e( 'SKU:', 'lifterlms' ); ?></label>\n\t\t\t\t\t<?php echo esc_html( $order->get( 'plan_sku' ) ); ?>\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t<?php endif; ?>\n\n\t\t<?php do_action( 'lifterlms_order_meta_box_after_plan_information', $order ); ?>\n\n\t\t<?php do_action( 'lifterlms_order_meta_box_before_product_information', $order ); ?>\n\n\t\t<h4><?php esc_html_e( 'Product Information', 'lifterlms' ); ?></h4>\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'Name:', 'lifterlms' ); ?></label>\n\t\t\t<?php if ( llms_get_post( $order->get( 'product_id' ) ) ) : ?>\n\t\t\t\t<a href=\"<?php echo esc_url( get_edit_post_link( $order->get( 'product_id' ) ) ); ?>\"><?php echo esc_html( $order->get( 'product_title' ) ); ?></a>\n\t\t\t<?php else : ?>\n\t\t\t\t<?php echo esc_html__( '[DELETED]', 'lifterlms' ) . ' ' . esc_html( $order->get( 'product_title' ) ); ?>\n\t\t\t<?php endif; ?>\n\t\t\t<small>(<?php echo esc_html( ucfirst( $order->get( 'product_type' ) ) ); ?>)</small>\n\t\t</div>\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'SKU:', 'lifterlms' ); ?></label>\n\t\t\t<?php echo esc_html( $order->get( 'product_sku' ) ); ?>\n\t\t</div>\n\n\t\t<?php do_action( 'lifterlms_order_meta_box_after_product_information', $order ); ?>\n\n\t</div>\n\n\t<?php do_action( 'lifterlms_order_meta_box_before_payment_information', $order ); ?>\n\n\t<div class=\"llms-metabox-section d-1of3\">\n\n\t\t<?php if ( $order->has_trial() ) : ?>\n\n\t\t\t<h4><?php esc_html_e( 'Trial Information', 'lifterlms' ); ?></h4>\n\n\t\t\t<?php if ( $order->has_coupon() && $order->get( 'coupon_amount_trial' ) ) : ?>\n\t\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t\t<label><?php esc_html_e( 'Original Total:', 'lifterlms' ); ?></label>\n\t\t\t\t\t<?php echo wp_kses( $order->get_price( 'trial_original_total' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t\t<label><?php esc_html_e( 'Coupon Discount:', 'lifterlms' ); ?></label>\n\t\t\t\t\t<?php echo wp_kses( $order->get_coupon_amount( 'trial' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t(<?php echo wp_kses( llms_price( $order->get_price( 'coupon_value_trial', array(), 'float' ) * - 1 ), LLMS_ALLOWED_HTML_PRICES ); ?>)\n\t\t\t\t\t[<a href=\"<?php echo esc_url( get_edit_post_link( $order->get( 'coupon_id' ) ) ); ?>\"><?php echo esc_html( $order->get( 'coupon_code' ) ); ?></a>]\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Total:', 'lifterlms' ); ?></label>\n\t\t\t\t<?php echo wp_kses( $order->get_price( 'trial_total' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t<?php echo esc_html( sprintf( _n( 'for %1$d %2$s', 'for %1$d %2$ss', $order->get( 'trial_length' ), 'lifterlms' ), $order->get( 'trial_length' ), $order->get( 'trial_period' ) ) ); ?>\n\t\t\t</div>\n\n\t\t<?php endif; ?>\n\n\t\t<h4>\n\t\t\t<?php esc_html_e( 'Payment Information', 'lifterlms' ); ?>\n\t\t\t<?php if ( $order->is_recurring() ) : ?>\n\t\t\t\t<button class=\"llms-editable\" title=\"<?php esc_attr_e( 'Edit payment information', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"dashicons dashicons-edit\"></span>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Edit payment information', 'lifterlms' ); ?></span>\n\t\t\t\t</button>\n\t\t\t<?php endif; ?>\n\t\t</h4>\n\n\t\t<?php if ( $order->has_discount() ) : ?>\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Original Total:', 'lifterlms' ); ?></label>\n\t\t\t\t<?php echo wp_kses( $order->get_price( 'original_total' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t</div>\n\n\t\t\t<?php if ( $order->has_sale() ) : ?>\n\t\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t\t<label><?php esc_html_e( 'Sale Discount:', 'lifterlms' ); ?></label>\n\t\t\t\t\t<?php echo wp_kses( $order->get_price( 'sale_price' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t(<?php echo wp_kses( llms_price( $order->get_price( 'sale_value', array(), 'float' ) * -1 ), LLMS_ALLOWED_HTML_PRICES ); ?>)\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php if ( $order->has_coupon() ) : ?>\n\t\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t\t<label><?php esc_html_e( 'Coupon Discount:', 'lifterlms' ); ?></label>\n\t\t\t\t\t<?php echo wp_kses( $order->get_coupon_amount( 'regular' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t(<?php echo wp_kses( llms_price( $order->get_price( 'coupon_value', array(), 'float' ) * - 1 ), LLMS_ALLOWED_HTML_PRICES ); ?>)\n\t\t\t\t\t[<a href=\"<?php echo esc_url( get_edit_post_link( $order->get( 'coupon_id' ) ) ); ?>\"><?php echo esc_html( $order->get( 'coupon_code' ) ); ?></a>]\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\t\t<?php endif; ?>\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'Total:', 'lifterlms' ); ?></label>\n\t\t\t<?php if ( $order->is_recurring() ) : ?>\n\t\t\t<div class=\"\" data-llms-editable=\"total\" data-llms-editable-required=\"yes\" data-llms-editable-type=\"price\" data-llms-editable-value=\"<?php echo esc_attr( $order->get( 'total' ) ); ?>\">\n\t\t\t<?php endif; ?>\n\t\t\t<?php echo wp_kses( $order->get_price( 'total' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t<?php if ( $order->is_recurring() ) : ?>\n\t\t\t</div>\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( $order->is_recurring() ) : ?>\n\t\t\t\t<?php\n\t\t\t\t//phpcs:disable WordPress.WP.I18n.MissingSingularPlaceholder -- We don't output the number so it's throwing an error but it's not broken.\n\t\t\t\techo esc_html(\n\t\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %1$d = The order billing period; %2$s = The order billing frequency.\n\t\t\t\t\t\t_n( 'Every %2$s', 'Every %1$d %2$ss', $order->get( 'billing_frequency' ), 'lifterlms' ), // phpcs:ignore: WordPress.WP.I18n.MismatchedPlaceholders\n\t\t\t\t\t\t$order->get( 'billing_frequency' ),\n\t\t\t\t\t\t$order->get( 'billing_period' )\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t//phpcs:enable WordPress.WP.I18n.MissingSingularPlaceholder\n\t\t\t\t?>\n\t\t\t\t<?php if ( $order->get( 'billing_length' ) > 0 ) : ?>\n\t\t\t\t\t<?php echo esc_html( sprintf( _n( 'for %1$d %2$s', 'for %1$d %2$ss', $order->get( 'billing_length' ), 'lifterlms' ), $order->get( 'billing_length' ), $order->get( 'billing_period' ) ) ); ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t<?php else : ?>\n\t\t\t\t<?php esc_html_e( 'One-time', 'lifterlms' ); ?>\n\t\t\t<?php endif; ?>\n\t\t</div>\n\n\t\t<?php\n\t\tif ( $order->has_plan_expiration() ) :\n\t\t\t$remaining               = $order->get_remaining_payments();\n\t\t\t$remaining_input_min_val = 0 === $remaining ? 0 : 1;\n\t\t\t?>\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Remaining Payments:', 'lifterlms' ); ?></label>\n\t\t\t\t<span id=\"llms-remaining-payments-view\"><?php echo esc_html( $remaining ); ?></span>\n\t\t\t\t<?php if ( $supports_modify_recurring_payments ) : ?>\n\t\t\t\t\t<?php add_thickbox(); ?>\n\t\t\t\t\t<div id=\"llms-remaining-edit\">\n\t\t\t\t\t\t<div class=\"llms-remaining-edit--content\">\n\t\t\t\t\t\t\t<h4><?php esc_html_e( 'Modify Remaining Payments', 'lifterlms' ); ?></h4>\n\n\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t\t\t<span><?php esc_html_e( 'Remaining payments', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t\t<input type=\"number\" id=\"llms-num-remaining-payments\" value=\"<?php echo esc_attr( $remaining ); ?>\" min=\"<?php echo esc_attr( $remaining_input_min_val ); ?>\" step=\"1\">\n\t\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t\t<label>\n\t\t\t\t\t\t\t\t<span><?php esc_html_e( 'Order Note', 'lifterlms' ); ?></span>\n\t\t\t\t\t\t\t\t<textarea id=\"llms-remaining-payments-note\" rows=\"3\"></textarea>\n\t\t\t\t\t\t\t\t<em><?php esc_html_e( 'For internal use only, not visible to the customer.', 'lifterlms' ); ?></em>\n\t\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t\t<button id=\"llms-save-remaining-payments\" class=\"button button-primary button-large\"><?php esc_html_e( 'Save', 'lifterlms' ); ?></button>\n\n\t\t\t\t\t\t\t<script>\n\t\t\t\t\t\t\t\t(function(){\n\t\t\t\t\t\t\t\t\tdocument.getElementById( 'llms-save-remaining-payments' ).addEventListener( 'click', function() {\n\t\t\t\t\t\t\t\t\t\tvar remainingEl = document.getElementById( 'llms-num-remaining-payments' ),\n\t\t\t\t\t\t\t\t\t\t\terrEl       = document.getElementById( 'llms-remaining-payments-err' ),\n\t\t\t\t\t\t\t\t\t\t\tremaining   = remainingEl.value,\n\t\t\t\t\t\t\t\t\t\t\tnote        = document.getElementById( 'llms-remaining-payments-note' ).value;\n\n\t\t\t\t\t\t\t\t\t\tif ( errEl ) {\n\t\t\t\t\t\t\t\t\t\t\terrEl.remove();\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif ( ! remainingEl.checkValidity() ) {\n\t\t\t\t\t\t\t\t\t\t\tremainingEl.insertAdjacentHTML( 'afterend', '<em id=\"llms-remaining-payments-err\" class=\"llms-error\">' + remainingEl.validationMessage + '</em>' );\n\t\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\ttb_remove();\n\n\t\t\t\t\t\t\t\t\t\tdocument.querySelector( 'input[name=\"_llms_remaining_payments\"]' ).value = remaining;\n\t\t\t\t\t\t\t\t\t\tdocument.querySelector( 'input[name=\"_llms_remaining_note\"]' ).value = note;\n\t\t\t\t\t\t\t\t\t\tdocument.getElementById( 'llms-remaining-payments-view' ).innerHTML = remaining;\n\t\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t\t})();\n\t\t\t\t\t\t\t</script>\n\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<a href=\"#TB_inline?&width=300&height=400&inlineId=llms-remaining-edit\" class=\"thickbox llms-metabox-icon\">\n\t\t\t\t\t\t<span class=\"dashicons dashicons-edit\" role=\"img\" aria-label=\"<?php esc_attr_e( 'Add additional payments', 'lifterlms' ); ?>\"></span>\n\t\t\t\t\t</a>\n\n\t\t\t\t\t<input type=\"hidden\" name=\"_llms_remaining_payments\" value=\"<?php echo esc_attr( $remaining ); ?>\">\n\t\t\t\t\t<input type=\"hidden\" name=\"_llms_remaining_note\">\n\t\t\t\t<?php endif; ?>\n\t\t\t</div>\n\t\t<?php endif; ?>\n\n\t\t<?php do_action( 'lifterlms_order_meta_box_after_payment_information', $order ); ?>\n\n\t</div>\n\n\t<?php do_action( 'lifterlms_order_meta_box_before_customer_information', $order ); ?>\n\n\t<div class=\"llms-metabox-section d-1of3\">\n\n\t\t<h4><?php esc_html_e( 'Customer Information', 'lifterlms' ); ?></h4>\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'Buyer Name:', 'lifterlms' ); ?></label>\n\t\t\t<?php if ( llms_parse_bool( $order->get( 'anonymized' ) ) || empty( llms_get_student( $order->get( 'user_id' ) ) ) ) : ?>\n\t\t\t\t<?php echo esc_html( $order->get_customer_name() ); ?>\n\t\t\t<?php else : ?>\n\t\t\t\t<?php\n\t\t\t\t$edit_user_link = $order->get( 'user_id' ) ? get_edit_user_link( $order->get( 'user_id' ) ) : '';\n\t\t\t\techo ! $edit_user_link ? esc_html( $order->get_customer_name() ) . '<br>' : '<a href=\"' . esc_url( $edit_user_link ) . '\">' . esc_html( $order->get_customer_name() ) . '</a>';\n\t\t\t\t?>\n\t\t\t<?php endif; ?>\n\t\t</div>\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'Buyer Email:', 'lifterlms' ); ?></label>\n\t\t\t<a href=\"<?php echo esc_url( 'mailto:' . $order->get( 'billing_email' ) ); ?>\"><?php echo esc_html( $order->get( 'billing_email' ) ); ?></a>\n\t\t</div>\n\n\t\t<?php if ( $order->get( 'billing_address_1' ) ) : ?>\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Buyer Address:', 'lifterlms' ); ?></label>\n\t\t\t\t<?php echo esc_html( $order->get( 'billing_address_1' ) ); ?><br>\n\t\t\t\t<?php if ( isset( $order->billing_address_2 ) ) : ?>\n\t\t\t\t\t<?php echo esc_html( $order->get( 'billing_address_2' ) ); ?><br>\n\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php echo esc_html( $order->get( 'billing_city' ) ); ?>,\n\t\t\t\t<?php echo esc_html( $order->get( 'billing_state' ) ); ?>,\n\t\t\t\t<?php echo esc_html( $order->get( 'billing_zip' ) ); ?><br>\n\t\t\t\t<?php echo esc_html( llms_get_country_name( $order->get( 'billing_country' ) ) ); ?>\n\t\t\t</div>\n\t\t<?php endif; ?>\n\n\t\t<?php if ( $order->get( 'billing_phone' ) ) : ?>\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'Buyer Phone:', 'lifterlms' ); ?></label>\n\t\t\t\t<?php echo esc_html( $order->get( 'billing_phone' ) ); ?>\n\t\t\t</div>\n\t\t<?php endif; ?>\n\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'Buyer IP Address:', 'lifterlms' ); ?></label>\n\t\t\t<?php echo esc_html( $order->get( 'user_ip_address' ) ); ?>\n\t\t</div>\n\n\t\t<?php do_action( 'lifterlms_order_meta_box_after_customer_information', $order ); ?>\n\n\t</div>\n\n\t<div class=\"clear\"></div>\n\n\n\t<?php do_action( 'lifterlms_order_meta_box_before_gateway_information', $order ); ?>\n\n\t<?php if ( $gateway ) : ?>\n\n\t\t<div class=\"llms-metabox-section d-all\">\n\n\t\t\t<h4>\n\t\t\t\t<?php esc_html_e( 'Gateway Information', 'lifterlms' ); ?>\n\t\t\t\t<button class=\"llms-editable\" title=\"<?php esc_attr_e( 'Edit gateway information', 'lifterlms' ); ?>\">\n\t\t\t\t\t<span class=\"dashicons dashicons-edit\"></span>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php esc_html_e( 'Edit gateway information', 'lifterlms' ); ?></span>\n\t\t\t\t</button>\n\t\t\t</h4>\n\n\t\t\t<div class=\"llms-metabox-field d-1of4\" data-gateway-fields='<?php echo wp_json_encode( $switchable_gateway_fields ); ?>' data-llms-editable=\"payment_gateway\" data-llms-editable-options='<?php echo wp_json_encode( $switchable_gateways ); ?>' data-llms-editable-type=\"select\" data-llms-editable-value=\"<?php echo esc_attr( $order->get( 'payment_gateway' ) ); ?>\">\n\t\t\t\t<label><?php esc_html_e( 'Name:', 'lifterlms' ); ?></label>\n\t\t\t\t<?php echo is_wp_error( $gateway ) ? esc_html( $order->get( 'payment_gateway' ) ) : esc_html( $gateway->get_admin_title() ); ?>\n\t\t\t</div>\n\n\t\t\t<?php if ( ! is_wp_error( $gateway ) ) : ?>\n\n\t\t\t\t<?php foreach ( $gateway->get_admin_order_fields() as $field => $data ) : ?>\n\n\t\t\t\t\t<div class=\"llms-metabox-field d-1of4\"<?php echo ! $data['enabled'] ? ' style=\"display:none;\"' : ' '; ?>data-llms-editable=\"<?php echo esc_attr( $data['name'] ); ?>\" data-llms-editable-required=\"yes\" data-llms-editable-type=\"text\" data-llms-editable-value=\"<?php echo esc_attr( $order->get( $data['name'] ) ); ?>\">\n\t\t\t\t\t\t<label><?php echo esc_html( $data['label'] ); ?></label>\n\t\t\t\t\t\t<?php echo wp_kses_post( $gateway->get_item_link( $field, $order->get( $data['name'] ), $order->get( 'gateway_api_mode' ) ) ); ?>\n\t\t\t\t\t</div>\n\n\t\t\t\t<?php endforeach; ?>\n\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php do_action( 'lifterlms_order_meta_box_after_gateway_information', $order ); ?>\n\n\t\t</div>\n\n\t<?php endif; ?>\n\n\t<?php do_action( 'lifterlms_after_order_meta_box', $order ); ?>\n\n</div>\n"
  },
  {
    "path": "includes/admin/views/metaboxes/view-order-submit.php",
    "content": "<?php\n/**\n * View for the LLMS_Meta_Box_Order_Submit metabox.\n *\n * @since 3.19.0\n * @since 5.4.0 The order status dropdown is now limited to a subset of possible status.\n * @version 7.0.0\n *\n * @property LLMS_Meta_Box_Order_Submit $this  LLMS_Meta_Box_Order_Submit instance.\n * @property LLMS_Order                 $order LLMS_Order instance.\n */\ndefined( 'ABSPATH' ) || exit;\n\n$current_status                     = $order->get( 'status' );\n$date_format                        = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );\n$statuses                           = llms_get_possible_order_statuses( $order );\n$supports_modify_recurring_payments = $order->supports_modify_recurring_payments();\n?>\n\n<div class=\"llms-metabox\">\n\n\t<div class=\"llms-metabox-section d-all no-top-margin\">\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label for=\"_llms_order_status\"><?php esc_html_e( 'Update Order Status:', 'lifterlms' ); ?></label>\n\t\t\t<select id=\"_llms_order_status\" name=\"_llms_order_status\">\n\t\t\t\t<?php foreach ( $statuses as $key => $val ) : ?>\n\t\t\t\t\t<option value=\"<?php echo esc_attr( $key ); ?>\"<?php selected( $key, $current_status ); ?>><?php echo esc_html( $val ); ?></option>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<div class=\"llms-metabox-field\">\n\t\t\t<label><?php esc_html_e( 'Order Date', 'lifterlms' ); ?>:</label>\n\t\t\t<?php echo esc_html( $order->get_date( 'date', $date_format ) ); ?>\n\t\t</div>\n\n\t\t<?php if ( $order->is_recurring() ) : ?>\n\n\t\t\t<?php $next_time = $order->get_next_payment_due_date( 'U' ); ?>\n\n\t\t\t<?php if ( $order->has_trial() ) : ?>\n\t\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t\t<label><?php esc_html_e( 'Trial End Date', 'lifterlms' ); ?>:</label>\n\t\t\t\t\t<?php if ( $supports_modify_recurring_payments ) : ?>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tid=\"llms-editable-trial-end-date\"\n\t\t\t\t\t\t\tdata-llms-editable=\"_llms_date_trial_end\"\n\t\t\t\t\t\t\tdata-llms-editable-date-format=\"yy-mm-dd\"\n\t\t\t\t\t\t\tdata-llms-editable-date-min=\"<?php echo esc_attr( $order->get_date( 'date', 'Y-m-d' ) ); ?>\"\n\t\t\t\t\t\t\tdata-llms-editable-type=\"datetime\"\n\t\t\t\t\t\t\tdata-llms-editable-value='<?php echo esc_attr( $this->get_editable_date_json( $order->get_trial_end_date( 'U' ) ) ); ?>'><?php echo esc_html( $order->get_trial_end_date( $date_format ) ); ?></span>\n\t\t\t\t\t\t<?php if ( ! $order->has_trial_ended() ) : ?>\n\t\t\t\t\t\t\t<a class=\"llms-editable\" data-fields=\"#llms-editable-trial-end-date\" href=\"#\"><span class=\"dashicons dashicons-edit\"></span></a>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t<span id=\"llms-trial-end-date\"><?php echo esc_html( $order->get_trial_end_date( $date_format ) ); ?></span>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php if ( $order->is_recurring() && 'llms-pending-cancel' !== $current_status ) : ?>\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Next Payment Date', 'lifterlms' ); ?>:</label>\n\t\t\t\t<?php if ( is_wp_error( $next_time ) ) : ?>\n\t\t\t\t\t<?php echo wp_kses_post( $next_time->get_error_message() ); ?>\n\t\t\t\t<?php elseif ( $supports_modify_recurring_payments ) : ?>\n\t\t\t\t\t<span\n\t\t\t\t\t\tid=\"llms-editable-next-payment-date\"\n\t\t\t\t\t\tdata-llms-editable=\"_llms_date_next_payment\"\n\t\t\t\t\t\tdata-llms-editable-date-format=\"yy-mm-dd\"\n\t\t\t\t\t\tdata-llms-editable-date-min=\"<?php echo esc_attr( current_time( 'Y-m-d' ) ); ?>\"\n\t\t\t\t\t\tdata-llms-editable-type=\"datetime\"\n\t\t\t\t\t\tdata-llms-editable-value='<?php echo esc_attr( $this->get_editable_date_json( $next_time ) ); ?>'><?php echo esc_html( date_i18n( $date_format, $next_time ) ); ?></span>\n\t\t\t\t\t<a class=\"llms-editable\" data-fields=\"#llms-editable-next-payment-date\" href=\"#\"><span class=\"dashicons dashicons-edit\"></span></a>\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<span id=\"llms-next-payment-date\"><?php echo esc_html( date_i18n( $date_format, $next_time ) ); ?></span>\n\t\t\t\t<?php endif; ?>\n\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t<?php endif; ?>\n\n\t\t<?php if ( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) ) : ?>\n\n\t\t\t<?php $expire_time = $order->get_access_expiration_date( 'U' ); ?>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Access Expiration', 'lifterlms' ); ?>:</label>\n\t\t\t\t<?php if ( ! is_numeric( $expire_time ) ) : ?>\n\t\t\t\t\t<?php echo esc_html( $expire_time ); ?>\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<span\n\t\t\t\t\t\tid=\"llms-editable-access-expires-date\"\n\t\t\t\t\t\tdata-llms-editable=\"_llms_date_access_expires\"\n\t\t\t\t\t\tdata-llms-editable-date-format=\"yy-mm-dd\"\n\t\t\t\t\t\tdata-llms-editable-date-min=\"<?php echo esc_attr( current_time( 'Y-m-d' ) ); ?>\"\n\t\t\t\t\t\tdata-llms-editable-type=\"datetime\"\n\t\t\t\t\t\tdata-llms-editable-value='<?php echo esc_attr( $this->get_editable_date_json( $expire_time ) ); ?>'><?php echo esc_html( date_i18n( $date_format, $expire_time ) ); ?></span>\n\t\t\t\t\t<a class=\"llms-editable\" data-fields=\"#llms-editable-access-expires-date\" href=\"#\"><span class=\"dashicons dashicons-edit\"></span></a>\n\t\t\t\t<?php endif; ?>\n\t\t\t</div>\n\t\t<?php endif; ?>\n\n\t\t<div class=\"clear\"></div>\n\n\t\t<div class=\"llms-metabox-field\" style=\"text-align: right;\">\n\t\t\t<input name=\"save\" type=\"submit\" class=\"button button-primary button-large\" id=\"publish\" value=\"<?php esc_attr_e( 'Update Order', 'lifterlms' ); ?>\">\n\t\t</div>\n\n\t</div>\n\n</div>\n"
  },
  {
    "path": "includes/admin/views/notices/db-update.php",
    "content": "<?php\n/**\n * Pending database update notice.\n *\n * @package LifterLMS/Admin/Views/Notices\n *\n * @since 5.2.0\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$base_url = ! empty( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : admin_url();\n$url      = wp_nonce_url( $base_url, 'do_db_updates', 'llms-db-update' );\n?>\n\n<p><strong><?php esc_html_e( 'The LifterLMS database needs to be updated to the latest version.', 'lifterlms' ); ?></strong></p>\n<p><?php esc_html_e( \"The update will only take a few minutes and it will run in the background. A notice like this will let you know when it's finished.\", 'lifterlms' ); ?></p>\n\n<p><?php printf( esc_html__( 'See the %1$sdatabase update log%2$s for a complete list of changes scheduled for each upgrade.', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/lifterlms-database-updates/\" target=\"_blank\">', '</a>' ); ?></p>\n\n<p><a class=\"button-primary\" id=\"llms-start-updater\" href=\"<?php echo esc_url( $url ); ?>\"><?php esc_html_e( 'Run the Updater', 'lifterlms' ); ?></a></p>\n\n<script>\n( function() {\n\tdocument.getElementById( 'llms-start-updater' ).onclick = function( e ) {\n\t\tvar confirm = window.confirm( '<?php echo esc_js( __( 'We strongly recommended that you backup your database before proceeding. Are you sure you wish to run the updater now?', 'lifterlms' ) ); ?>' );\n\t\tif ( ! confirm ) {\n\t\t\te.preventDefault();\n\t\t}\n\t};\n} )();\n</script>\n"
  },
  {
    "path": "includes/admin/views/notices/index.php",
    "content": "<?php // shhh.\n"
  },
  {
    "path": "includes/admin/views/notices/review-request.php",
    "content": "<?php\n/**\n * Review Request\n *\n * We're needy. Please tell us you like us, it means a lot.\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 3.24.0\n * @since 4.14.0 Added nonce to AJAX request.\n * @version 4.14.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<div class=\"notice notice-info is-dismissible llms-admin-notice llms-review-notice\">\n\t<div class=\"llms-admin-notice-icon\"></div>\n\t<div class=\"llms-admin-notice-content\">\n\t\t<?php // Translators: %s = number of active students. ?>\n\t\t<p><?php printf( esc_html__( 'Hey there, we noticed you have more than %s active students on your site - that’s really awesome!', 'lifterlms' ), esc_html( number_format_i18n( $enrollments ) ) ); ?></p>\n\t\t<p><?php esc_html_e( 'Could you please do us a BIG favor and give LifterLMS a 5-star rating on WordPress to help us grow?', 'lifterlms' ); ?></p>\n\t\t<p>&ndash; <?php esc_html_e( 'Chris Badgett, CEO of LifterLMS', 'lifterlms' ); ?></p>\n\t\t<p>\n\t\t\t<a href=\"https://wordpress.org/support/plugin/lifterlms/reviews/#new-post\" class=\"llms-button-primary small llms-review-notice-dismiss llms-review-notice-out\" target=\"_blank\" rel=\"noopener noreferrer\"><?php esc_html_e( 'Ok, you deserve it', 'lifterlms' ); ?></a>\n\t\t\t<button class=\"llms-button-secondary small llms-review-notice-dismiss\"><?php esc_html_e( 'Nope, maybe later', 'lifterlms' ); ?></button>\n\t\t\t<button class=\"llms-button-secondary small llms-review-notice-dismiss\"><?php esc_html_e( 'I already did', 'lifterlms' ); ?></button>\n\t\t</p>\n\t</div>\n</div>\n<script>\n\tjQuery( document ).ready( function ( $ ) {\n\t\t$( document ).on( 'click', '.llms-review-notice-dismiss, .llms-review-notice .notice-dismiss', function ( event ) {\n\t\t\tvar success = 'yes';\n\t\t\tif ( ! $( this ).hasClass( 'llms-review-notice-out' ) ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tsuccess = 'no';\n\t\t\t}\n\t\t\t$.post( ajaxurl, {\n\t\t\t\taction: 'llms_review_dismiss',\n\t\t\t\tsuccess: success,\n\t\t\t\tnonce: '<?php echo esc_js( wp_create_nonce( 'llms-admin-review-request-dismiss' ) ); ?>',\n\t\t\t} );\n\t\t\t$( '.llms-review-notice' ).remove();\n\t\t} );\n\t} );\n</script>\n"
  },
  {
    "path": "includes/admin/views/reporting/index.php",
    "content": "<?php // Shhhh.\n"
  },
  {
    "path": "includes/admin/views/reporting/widget.php",
    "content": "<?php\n/**\n * Reporting widget view.\n *\n * @package LifterLMS/Admin/Views/Reporting\n *\n * @since 6.11.0\n *\n * @var array      $args             Input arguments provided to {@see LLMS_Admin_Reporting::output_widget}.\n * @var string     $data_after       Additional HTML to render inside the data display element.\n * @var string     $compare_class    A CSS class name added to the compare element's class list.\n * @var string     $compare_operator The operator to display before the changed data value.\n * @var string     $compare_title    A description of the previous data when displaying comparison data.\n * @var bool|float $change           The percent-change value when displaying comparisons or `false` when a comparison\n *                                   should not be displayed.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<div class=\"<?php echo esc_attr( $args['cols'] ); ?>\">\n\t<div class=\"llms-reporting-widget <?php echo esc_attr( $args['id'] ); ?>\" id=\"<?php echo esc_attr( $args['id'] ); ?>\">\n\t\t<?php if ( $args['icon'] ) : ?>\n\t\t\t<i class=\"fa fa-<?php echo esc_attr( $args['icon'] ); ?>\" aria-hidden=\"true\"></i>\n\t\t<?php endif; ?>\n\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong><?php echo wp_kses_post( $args['data'] . $data_after ); ?></strong>\n\t\t\t<?php if ( $change ) : ?>\n\t\t\t\t<small class=\"compare tooltip <?php echo esc_attr( $compare_class ); ?>\" title=\"<?php echo esc_attr( $compare_title ); ?>\">\n\t\t\t\t\t<?php echo wp_kses_post( $compare_operator . $change ); ?>%\n\t\t\t\t</small>\n\t\t\t<?php endif; ?>\n\t\t</div>\n\t\t<small><?php echo wp_kses_post( $args['text'] ); ?></small>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/admin/views/resources/getting-started.php",
    "content": "<?php\n/**\n * Getting Started links meta box HTML.\n *\n * @package LifterLMS/Admin/Views/Resources\n *\n * @since 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-getting-started-links\">\n\t<div class=\"llms-list\">\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/how-do-i-add-my-license-key-to-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'How to Activate Your License Key', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/how-to-create-a-course-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'How to Create a Course', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/what-is-an-access-plan/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'How to Set Up an Access Plans', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/getting-started-with-creating-memberships/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'How to Create a Membership', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/doc-category/features/engagements/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'How to Create Certificates and Engagements', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t\t<a class=\"llms-button-primary\" href=\"https://lifterlms.com/start-in-style/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'All Getting Started Resources', 'lifterlms' ); ?></a>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/admin/views/resources/index.php",
    "content": "<?php // Quiet.\n"
  },
  {
    "path": "includes/admin/views/resources/resource-links.php",
    "content": "<?php\n/**\n * Resource links meta box HTML.\n *\n * @package LifterLMS/Admin/Views/Resources\n *\n * @since 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-resource-links\">\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-admin-post\"></span> <?php esc_html_e( 'Key Documentation', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/shortcodes/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Shortcodes', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/what-payment-gateways-can-i-use-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'LifterLMS Payment Gateways', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/order-management/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Managing Orders', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/getting-started-with-lifterlms-and-woocommerce/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Integrating WooCommerce', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/membership-auto-enrollment/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Selling Bundled Courses', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/use-drip-content-lifterlms-lessons/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Dripping Content', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/use-lifterlms-language-english/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Translating LifterLMS', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/changes-to-the-wordpress-admin-for-lifterlms-5-0/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Editing Registration Forms', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t\t<a class=\"llms-button-secondary\" href=\"https://lifterlms.com/docs/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Knowledge Base', 'lifterlms' ); ?></a>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-editor-code\"></span> <?php esc_html_e( 'For Developers', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://make.lifterlms.com/category/release-notes/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Changelogs', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://developer.lifterlms.com/reference/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Code Reference', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://developer.lifterlms.com/rest-api/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'REST API', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://developer.lifterlms.com/cli/commands?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'LLMS-CLI', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://github.com/gocodebox/lifterlms-cs?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Coding Standards', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://github.com/gocodebox/lifterlms?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Github', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/contributing-to-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Contribute', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/slack/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Developer Slack Community', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t\t<a class=\"llms-button-secondary\" href=\"https://developer.lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Developer Resources', 'lifterlms' ); ?></a>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-hammer\"></span> <?php esc_html_e( 'Common Hang-ups', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/using-the-lifterlms-my-account-page-as-the-wordpress-front-page/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Student Dashboard as Front Page', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/caching-issues-faqs/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Caching Conflicts', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/why-do-i-get-a-notice-saying-my-license-key-is-no-longer-valid-and-was-deactivated/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'License Key Deactivated', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/a-guide-to-understanding-and-fixing-email-issues-in-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Emails Not Sending', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/docs/rerun-lifterlms-setup-wizard/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Rerun Set Up Wizard', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t\t<a class=\"llms-button-action\" href=\"https://lifterlms.com/my-account/my-tickets/new/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Contact Support', 'lifterlms' ); ?></a>\n\t</div>\n</div>\n<hr />\n<div class=\"llms-resource-links\">\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-welcome-learn-more\"></span> <?php esc_html_e( 'Courses &amp; Case Studies', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://academy.lifterlms.com/course/how-to-build-a-learning-management-system-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'LifterLMS Quickstart Course', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://academy.lifterlms.com/course/the-complete-wordpress-for-beginners-masterclass/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'WordPress 101', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/success/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'LifterLMS Case Studies', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/lifterlms-webinars/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'LifterLMS Webinars', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-heart\"></span> <?php esc_html_e( 'Community Resources', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/community-events/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Virtual Events', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://www.facebook.com/lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Facebook Group', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/experts/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Experts for Hire', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://wordpress.org/support/plugin/lifterlms/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'WordPress Forums', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t</div>\n\t<div class=\"llms-list\">\n\t\t<h3><span class=\"dashicons dashicons-plugins-checked\"></span> <?php esc_html_e( 'Third Party Stuff', 'lifterlms' ); ?></h3>\n\t\t<ul>\n\t\t\t<li><a href=\"https://lifterlms.com/recommended-resources/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page#third-party-lifterlms-add-ons\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Third Party Add-ons', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/recommended-resources/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page#forms-plugins\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Form Plugins', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/recommended-resources/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page#email-marketing-crm\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Email Marketing', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/recommended-resources/?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page#backups-and-security\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'Backups and Security', 'lifterlms' ); ?></a></li>\n\t\t\t<li><a href=\"https://lifterlms.com/recommended-resources?utm_source=LifterLMS%20Plugin&utm_medium=Resource%20Screen&utm_campaign=Backend%20Help%20Page\" target=\"_blank\" rel=\"noopener\"><?php esc_html_e( 'More Recommendations', 'lifterlms' ); ?></a></li>\n\t\t</ul>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/admin/views/resources/welcome-video.php",
    "content": "<?php\n/**\n * Welcome video meta box HTML.\n *\n * @package LifterLMS/Admin/Views/Resources\n *\n * @since 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-welcome-video\">\n\t<p><?php esc_html_e( 'Thank you for choosing LifterLMS! This page is your command center, where you can find all of the basic information you need to start building your courses. Use the links on this page to quickly access support or any additional documentation you may need along the way.', 'lifterlms' ); ?></p>\n\t<div class=\"llms-welcome-video-container\">\n\t\t<iframe width=\"762\" height=\"428\" src=\"https://www.youtube.com/embed/SWJKl4hs99g\" title=\"How to Build an LMS Website - Getting Started with LifterLMS\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen></iframe>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/admin/views/resources.php",
    "content": "<?php\n/**\n * Resources Page HTML.\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 7.4.1\n * @version 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<div class=\"wrap lifterlms lifterlms-settings llms-resources\">\n\n\t<div class=\"llms-subheader\">\n\n\t\t<h1><?php esc_html_e( 'LifterLMS Resources', 'lifterlms' ); ?></h1>\n\n\t</div>\n\n\t<div class=\"llms-inside-wrap\">\n\n\t\t<hr class=\"wp-header-end\">\n\n\t\t<form id=\"llms-resources-form\" method=\"post\" action=\"admin-post.php\">\n\t\t\t<div id=\"poststuff\">\n\t\t\t\t<div id=\"post-body\" class=\"metabox-holder columns-2\">\n\n\t\t\t\t\t<div id=\"postbox-container-1\" class=\"postbox-container\">\n\t\t\t\t\t\t<?php do_meta_boxes( 'toplevel_page_llms-resources', 'side', '' ); ?>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div id=\"postbox-container-2\" class=\"postbox-container\">\n\t\t\t\t\t\t<?php do_meta_boxes( 'toplevel_page_llms-resources', 'normal', '' ); ?>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<br class=\"clear\">\n\n\t\t\t\t</div> <!-- end metabox-holder -->\n\n\t\t\t\t<?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>\n\t\t\t\t<?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>\n\n\t\t\t</div> <!-- end poststuff -->\n\t\t</form>\n\t\t<script type=\"text/javascript\">\n\t\t\t//<![CDATA[\n\t\t\tjQuery(document).ready( function($) {\n\t\t\t\t// close postboxes that should be closed\n\t\t\t\t$('.if-js-closed').removeClass('if-js-closed').addClass('closed');\n\t\t\t\t// postboxes setup\n\t\t\t\tpostboxes.add_postbox_toggles('toplevel_page_llms-resources');\n\t\t\t});\n\t\t\t//]]>\n\t\t</script>\n\n\t</div>\n\n</div> <!-- end .wrap.llms-resources -->\n"
  },
  {
    "path": "includes/admin/views/settings.php",
    "content": "<?php\n/**\n * Admin Settings Page HTML\n *\n * @since    1.0.0\n * @version  3.29.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"wrap lifterlms lifterlms-settings\">\n\n\t<form action=\"\" method=\"POST\" id=\"mainform\" enctype=\"multipart/form-data\">\n\n\t\t<div class=\"llms-subheader\">\n\n\t\t\t<h1><?php esc_html_e( 'Settings', 'lifterlms' ); ?></h1>\n\n\t\t\t<?php if ( apply_filters( 'llms_settings_' . $current_tab . '_has_save_button', true ) ) : ?>\n\n\t\t\t\t<div class=\"llms-save\">\n\n\t\t\t\t\t<?php do_action( 'llms_before_admin_settings_save_button', $current_tab ); ?>\n\n\t\t\t\t\t<input name=\"save\" class=\"llms-button-primary\" type=\"submit\" value=\"<?php echo esc_attr( apply_filters( 'llms_admin_settings_submit_button_text', __( 'Save Changes', 'lifterlms' ), $current_tab ) ); ?>\" />\n\n\t\t\t\t\t<?php wp_nonce_field( 'lifterlms-settings' ); ?>\n\n\t\t\t\t\t<?php do_action( 'llms_after_admin_settings_save_button', $current_tab ); ?>\n\n\t\t\t\t</div>\n\n\t\t\t<?php endif; ?>\n\n\t\t</div>\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\t\t\t<div class=\"llms-inside-wrap\">\n\n\t\t\t\t<?php do_action( 'lifterlms_before_settings_tabs' ); ?>\n\n\t\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t\t<?php\n\t\t\t\tforeach ( $tabs as $name => $label ) :\n\t\t\t\t\t$active = ( $current_tab === $name ) ? ' llms-active' : '';\n\t\t\t\t\t?>\n\n\t\t\t\t\t<li class=\"llms-nav-item<?php echo esc_attr( $active ); ?>\"><a class=\"llms-nav-link\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-settings&tab=' . $name ) ); ?>\"><?php echo wp_kses_post( $label ); ?></a></li>\n\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</ul>\n\n\t\t\t\t<?php do_action( 'lifterlms_after_settings_tabs' ); ?>\n\n\t\t\t</div>\n\t\t</nav>\n\n\t\t<div class=\"llms-inside-wrap\">\n\n\t\t\t<hr class=\"wp-header-end\">\n\n\t\t\t<h1 class=\"screen-reader-text\"><?php echo esc_html( $tabs[ $current_tab ] ); ?></h1>\n\n\t\t\t<?php do_action( 'lifterlms_settings_notices' ); ?>\n\n\t\t\t<?php\n\t\t\t\tdo_action( 'lifterlms_sections_' . $current_tab );\n\t\t\t\tdo_action( 'lifterlms_settings_' . $current_tab );\n\t\t\t\tdo_action( 'lifterlms_settings_tabs_' . $current_tab );\n\t\t\t?>\n\n\t\t</div>\n\n\t</form>\n\n</div>\n"
  },
  {
    "path": "includes/admin/views/setup-wizard/index.php",
    "content": "<?php // Be quiet.\n"
  },
  {
    "path": "includes/admin/views/setup-wizard/main.php",
    "content": "<?php\n/**\n * Setup Wizard main view\n *\n * @package LifterLMS/Views/Admin/SetupWizard\n *\n * @since 4.4.4\n * @since 4.8.0 Unknown.\n * @since 7.4.0 Escape output.\n * @version 7.4.0\n *\n * @property string[]                $steps     Array of setup wizard steps.\n * @property string                  $current   Slug of the current step.\n * @property string|boolean          $prev      Slug of the previous step or `false` if no previous step found.\n * @property string|boolean          $next      Slug of the next step or `false` if no next step found.\n * @property string                  $step_html HTML content for the current step.\n * @property LLMS_Admin_Setup_Wizard $this      Setup wizard class instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div id=\"llms-setup-wizard\">\n\n\t<div class=\"llms-setup-wrapper\">\n\n\t\t<h1 id=\"llms-logo\">\n\t\t\t<a href=\"https://lifterlms.com/\" target=\"_blank\">\n\t\t\t\t<img src=\"<?php echo esc_url( llms()->plugin_url() . '/assets/images/lifterlms-logo-black.png' ); ?>\" alt=\"LifterLMS\">\n\t\t\t</a>\n\t\t</h1>\n\n\t\t<ul class=\"llms-setup-progress\">\n\t\t\t<?php foreach ( $steps as $slug => $step ) : ?>\n\t\t\t\t<li<?php echo ( $slug === $current ) ? ' class=\"current\"' : ''; ?>>\n\t\t\t\t\t<a href=\"<?php echo esc_url( $this->get_step_url( $slug ) ); ?>\"><?php echo esc_html( $step['title'] ?? '' ); ?></a>\n\t\t\t\t</li>\n\t\t\t<?php endforeach; ?>\n\t\t</ul>\n\n\t\t<div class=\"llms-setup-content\">\n\t\t\t<form action=\"\" method=\"POST\">\n\n\t\t\t\t<?php\n\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in template files.\n\t\t\t\t\techo $step_html;\n\t\t\t\t?>\n\n\t\t\t\t<?php if ( is_wp_error( $this->error ) ) : ?>\n\t\t\t\t\t<p class=\"error\"><?php echo esc_html( $this->error->get_error_message() ); ?></p>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<p class=\"llms-setup-actions\">\n\t\t\t\t\t<?php if ( 'intro' === $current ) : ?>\n\t\t\t\t\t\t<a href=\"<?php echo esc_url( admin_url() ); ?>\" class=\"llms-button-secondary large\"><?php esc_html_e( 'Skip setup', 'lifterlms' ); ?></a>\n\t\t\t\t\t\t<a href=\"<?php echo esc_url( admin_url() . '?page=llms-setup&step=' . $this->get_next_step() ); ?>\" class=\"llms-button-primary large\"><?php esc_html_e( 'Get Started Now', 'lifterlms' ); ?></a>\n\t\t\t\t\t<?php else : ?>\n\n\t\t\t\t\t\t<a class=\"llms-exit-setup\" data-confirm=\"<?php esc_attr_e( 'The site setup is incomplete! Are you sure you wish to exit?', 'lifterlms' ); ?>\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-settings' ) ); ?>\"><?php esc_html_e( 'Exit Setup', 'lifterlms' ); ?></a>\n\n\t\t\t\t\t\t<?php if ( $next ) : ?>\n\t\t\t\t\t\t\t<a href=\"<?php echo esc_url( $this->get_step_url( $next ) ); ?>\" class=\"llms-button-secondary large\">\n\t\t\t\t\t\t\t\t<?php echo esc_html( $this->get_skip_text( $current ) ); ?>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t\t<?php if ( 'finish' === $current ) : ?>\n\t\t\t\t\t\t\t<a href=\"<?php echo esc_url( admin_url( 'post-new.php?post_type=course' ) ); ?>\" class=\"llms-button-secondary large\">\n\t\t\t\t\t\t\t\t<?php esc_html_e( 'Start from Scratch', 'lifterlms' ); ?>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t\t<button class=\"llms-button-primary large\" type=\"submit\" id=\"llms-setup-submit\">\n\t\t\t\t\t\t\t<?php echo esc_html( $this->get_save_text( $current ) ); ?>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<input id=\"llms-setup-current-step\" name=\"llms_setup_save\" type=\"hidden\" value=\"<?php echo esc_attr( $current ); ?>\">\n\t\t\t\t\t\t<?php wp_nonce_field( 'llms_setup_save', 'llms_setup_nonce' ); ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</p>\n\n\t\t\t</form>\n\t\t</div>\n\n\t</div>\n\n</div>\n\n<?php\n"
  },
  {
    "path": "includes/admin/views/setup-wizard/step-coupon.php",
    "content": "<?php\n/**\n * Setup Wizard step: Coupon\n *\n * @package LifterLMS/Views/Admin/SetupWizard\n *\n * @since 4.4.4\n * @since 4.8.0 Unknown.\n * @since 7.4.0 Escape output.\n * @version 7.4.0\n *\n * @property LLMS_Admin_Setup_Wizard $this Setup wizard class instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<h1><?php esc_html_e( 'Help Improve LifterLMS & Get a Coupon', 'lifterlms' ); ?></h1>\n<p><?php esc_html_e( 'By allowing us to collect non-sensitive usage information and diagnostic data, you\\'ll be providing us with information we can use to make the future of LifterLMS stronger and more powerful with every update!', 'lifterlms' ); ?></p>\n<p><?php esc_html_e( 'Click \"Allow\" to and we\\'ll send you a coupon immediately.', 'lifterlms' ); ?></p>\n<p><a href=\"https://lifterlms.com/usage-tracking/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Usage%20Tracking\" target=\"_blank\"><?php esc_html_e( 'Find out more information', 'lifterlms' ); ?></a>.</p>\n"
  },
  {
    "path": "includes/admin/views/setup-wizard/step-finish.php",
    "content": "<?php\n/**\n * Setup Wizard step: Finish\n *\n * @package LifterLMS/Views/Admin/SetupWizard\n *\n * @since 4.4.4\n * @since 4.8.0 Unknown.\n * @since 7.4.0 Escape output.\n * @version 7.4.0\n *\n * @property LLMS_Admin_Setup_Wizard $this Setup wizard class instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$courses = LLMS_Export_API::list( 1, 3 );\n?>\n<h1><?php esc_html_e( 'Setup Complete!', 'lifterlms' ); ?></h1>\n<p><?php esc_html_e( 'Here\\'s some resources to help you get familiar with LifterLMS:', 'lifterlms' ); ?></p>\n<ul>\n\t<li><span class=\"dashicons dashicons-format-video\"></span> <a href=\"https://demo.lifterlms.com/course/how-to-build-a-learning-management-system-with-lifterlms/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%Video%20Tutorials\" target=\"_blank\"><?php esc_html_e( 'Watch the LifterLMS video tutorials', 'lifterlms' ); ?></a></li>\n\t<li><span class=\"dashicons dashicons-admin-page\"></span> <a href=\"https://lifterlms.com/docs/getting-started-guide-with-lifterlms-resources/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%Getting%20Started\" target=\"_blank\"><?php esc_html_e( 'Read the LifterLMS Getting Started Guide', 'lifterlms' ); ?></a></li>\n</ul>\n<br>\n\n<?php if ( is_array( $courses ) && ! empty( $courses ) ) : ?>\n\n\t<?php\n\t// If there was an error fetching courses, the array might be details of the request vs. a WP_Error object.\n\tforeach ( $courses as $course ) {\n\t\tif ( ! is_array( $course ) || ! isset( $course['id'], $course['description'], $course['image'], $course['title'] ) ) {\n\t\t\t$courses = new WP_Error( 'llms_invalid_course_data', __( 'There was an error loading importable courses. Please reload the page to try again.', 'lifterlms' ) );\n\t\t\tbreak;\n\t\t}\n\t}\n\t?>\n\n<h1><?php esc_html_e( 'Import Sample Courses and Templates!', 'lifterlms' ); ?></h1>\n<p><?php esc_html_e( 'Accelerate your progress by installing a quick LifterLMS training course and useful course templates.', 'lifterlms' ); ?></p>\n\n\t<?php require LLMS_PLUGIN_DIR . 'includes/admin/views/importable-courses.php'; ?>\n\n<div class=\"llms-importing-msgs\">\n\t<p class=\"llms-importing-msg single\">\n\t\t<?php\n\t\tprintf(\n\t\t\t// Translators: %s = anchor link to LifterLMS.com.\n\t\t\tesc_html__( 'The selected course will be downloaded and imported into this site from %s.', 'lifterlms' ),\n\t\t\t'<a href=\"https://lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Home\" target=\"_blank\">LifterLMS.com</a>'\n\t\t);\n\t\t?>\n\t</p>\n\t<p class=\"llms-importing-msg multiple\">\n\t\t<?php\n\t\tprintf(\n\t\t\t// Translators: %1$s = The number of selected courses; %2$s = anchor link to LifterLMS.com.\n\t\t\tesc_html__( 'The %1$s selected courses will be downloaded and imported into this site from %2$s.', 'lifterlms' ),\n\t\t\t'<span id=\"llms-importing-number\">2</span>',\n\t\t\t'<a href=\"https://lifterlms.com/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Home\" target=\"_blank\">LifterLMS.com</a>'\n\t\t);\n\t\t?>\n\t</p>\n</div>\n\n<?php endif; ?>\n"
  },
  {
    "path": "includes/admin/views/setup-wizard/step-intro.php",
    "content": "<?php\n/**\n * Setup Wizard step: Welcome\n *\n * @package LifterLMS/Views/Admin/SetupWizard\n *\n * @since 4.4.4\n * @since 4.8.0 Unknown.\n * @since 7.4.0 Escape output.\n * @version 7.4.0\n *\n * @property LLMS_Admin_Setup_Wizard $this Setup wizard class instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<h1><?php esc_html_e( 'Welcome to LifterLMS!', 'lifterlms' ); ?></h1>\n<p><?php esc_html_e( 'Thanks for choosing LifterLMS to power your online courses! This short setup wizard will guide you through the basic settings and configure LifterLMS so you can get started creating courses faster!', 'lifterlms' ); ?></p>\n<p><?php esc_html_e( 'It will only take a few minutes and it is completely optional. If you don\\'t have the time now, come back later.', 'lifterlms' ); ?></p>\n<?php\n"
  },
  {
    "path": "includes/admin/views/setup-wizard/step-pages.php",
    "content": "<?php\n/**\n * Setup Wizard step: Page Setup\n *\n * @since 4.4.4\n * @since 7.3.0 Using the `LLMS_Install::get_pages()` method now.\n * @since 7.4.0 Escape remaining strings.\n * @version 7.4.0\n *\n * @property LLMS_Admin_Setup_Wizard $this Setup wizard class instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<h1><?php esc_html_e( 'Page Setup', 'lifterlms' ); ?></h1>\n\n<p><?php esc_html_e( 'LifterLMS has a few essential pages. The following will be created automatically if they don\\'t already exist.', 'lifterlms' ); ?>\n\n<table>\n\t<?php\n\t$pages = LLMS_Install::get_pages();\n\tforeach ( $pages as $page ) {\n\t\t// Skip pages that don't have all the info we want to show.\n\t\tif ( empty( $page['docs_url'] ) || empty( $page['description'] ) || empty( $page['wizard_title'] ) ) {\n\t\t\tcontinue;\n\t\t}\n\t\t?>\n\t\t<tr>\n\t\t<td><a href=\"<?php echo esc_url( $page['docs_url'] ); ?>\" target=\"_blank\"><?php echo esc_html( $page['wizard_title'] ); ?></a></td>\n\t\t<td><p><?php echo esc_html( $page['description'] ); ?></p></td>\n\t\t</tr>\n\t\t<?php\n\t}\n\t?>\n</table>\n\n<p>\n\t<?php\n\tprintf(\n\t\t/* Translators: 1: Link to the Pages screen in the WordPress admin 2: Closing link tag 3: Link to the Appearance > Menus screen in the WordPress admin 4: Closing link tag. */\n\t\tesc_html__( 'After setup, you can manage these pages from the admin dashboard on the %1$sPages screen%2$s and you can control which pages display on your menu(s) via %3$sAppearance > Menus%4$s.', 'lifterlms' ),\n\t\t'<a href=\"' . esc_url( admin_url( 'edit.php?post_type=page' ) ) . '\" target=\"_blank\">',\n\t\t'</a>',\n\t\t'<a href=\"' . esc_url( admin_url( 'nav-menus.php' ) ) . '\" target=\"_blank\">',\n\t\t'</a>'\n\t);\n\t?>\n</p>\n"
  },
  {
    "path": "includes/admin/views/setup-wizard/step-payments.php",
    "content": "<?php\n/**\n * Setup Wizard step: Payments\n *\n * @package LifterLMS/Views/Admin/SetupWizard\n *\n * @since 4.4.4\n * @since 4.8.0 Unknown.\n * @since 7.4.0 Escape output.\n * @version 7.4.0\n *\n * @property LLMS_Admin_Setup_Wizard $this Setup wizard class instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$country  = get_lifterlms_country();\n$currency = get_lifterlms_currency();\n$payments = get_option( 'llms_gateway_manual_enabled', 'no' );\n\n?>\n<h1><?php esc_html_e( 'Payments', 'lifterlms' ); ?></h1>\n\n<table>\n\t<tr>\n\t\t<td colspan=\"2\">\n\t\t\t<p><label for=\"llms_country\"><?php esc_html_e( 'Which country should be used as the default for student registrations?', 'lifterlms' ); ?></label></p>\n\t\t\t<p>\n\t\t\t\t<select id=\"llms_country\" name=\"country\" class=\"llms-select2\">\n\t\t\t\t<?php foreach ( get_lifterlms_countries() as $code => $name ) : ?>\n\t\t\t\t\t<option value=\"<?php echo esc_attr( $code ); ?>\"<?php selected( $code, $country ); ?>>\n\t\t\t\t\t\t<?php echo esc_html( $name . ' (' . $code . ')' ); ?>\n\t\t\t\t\t</option>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</select>\n\t\t\t</p>\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td colspan=\"2\">\n\t\t\t<p><label for=\"llms_currency\"><?php esc_html_e( 'Which currency should be used for payment processing?', 'lifterlms' ); ?></label></p>\n\t\t\t<p>\n\t\t\t\t<select id=\"llms_currency\" name=\"currency\" class=\"llms-select2\">\n\t\t\t\t<?php foreach ( get_lifterlms_currencies() as $code => $name ) : ?>\n\t\t\t\t\t<option value=\"<?php echo esc_attr( $code ); ?>\"<?php selected( $code, $currency ); ?>><?php echo esc_html( $name ); ?> (<?php echo esc_html( get_lifterlms_currency_symbol( $code ) ); ?>)</option>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</select>\n\t\t\t\t<i><?php printf( esc_html__( 'If your currency is not listed you can %1$sadd it later%2$s.', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/how-can-i-add-my-currency-to-lifterlms/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Add%20Currency\" target=\"_blank\">', '</a>' ); ?></i>\n\t\t\t</p>\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td colspan=\"2\">\n\t\t\t<p><?php printf( esc_html__( 'With LifterLMS you can accept both online and offline payments. Be sure to install a %1$spayment gateway%2$s to accept online payments.', 'lifterlms' ), '<a href=\"https://lifterlms.com/product-category/plugins/payment-gateways/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Payment%20Add-ons\" target=\"_blank\">', '</a>' ); ?></p>\n\t\t\t<p><label for=\"llms_manual\"><input id=\"llms_manual\" name=\"manual_payments\" type=\"checkbox\" value=\"yes\"<?php checked( 'yes', $payments ); ?>> <?php esc_html_e( 'Enable Offline Payments', 'lifterlms' ); ?></label></p>\n\t\t\t<p><?php echo esc_html__( 'Payment gateways may be configured under Settings in the \"Checkout\" tab.', 'lifterlms' ); ?></p>\n\t\t</td>\n\t</tr>\n</table>\n"
  },
  {
    "path": "includes/admin/views/status/index.php",
    "content": "<?php // be quiet.\n"
  },
  {
    "path": "includes/admin/views/status/view-log.php",
    "content": "<?php\n/**\n * Log file viewer\n *\n * Used on the LifterLMS Admin Status Logs tab to output a single log file.\n *\n * @package LifterLMS/Admin/Views\n *\n * @since 3.37.14\n * @version 3.37.14\n *\n * @property array[] $logs       Associative array of log files. The array key is the file \"slug\" and the value is the file's absolute path.\n * @property string  $current    Slug of the current log file.\n * @property string  $delete_url Nonce url to delete the log file (if the log is deletable).\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<div class=\"llms-setting-group top\">\n\n\t<p class=\"llms-label\"><?php esc_html_e( 'View and Manage Logs', 'lifterlms' ); ?></p>\n\n\t<form action=\"<?php echo esc_url( LLMS_Admin_Page_Status::get_url( 'logs' ) ); ?>\" method=\"POST\">\n\t\t<select name=\"llms_log_file\">\n\t\t\t<?php foreach ( $logs as $name => $file ) : ?>\n\t\t\t\t<option value=\"<?php echo esc_attr( $name ); ?>\" <?php selected( $current, $name ); ?>>\n\t\t\t\t\t<?php echo esc_html( basename( $file ) ); ?>\n\t\t\t\t\t(<?php echo esc_html( date_i18n( $date_format, filemtime( $file ) ) ); ?>)\n\t\t\t\t</option>\n\t\t\t<?php endforeach; ?>\n\t\t</select>\n\t\t<button class=\"llms-button-secondary small\" type=\"submit\"><?php esc_html_e( 'View Log', 'lifterlms' ); ?></button>\n\t</form>\n\t<hr />\n\t<h2>\n\t\t<?php\n\t\tprintf(\n\t\t\t// Translators: %s = File name of the log.\n\t\t\tesc_html__( 'Viewing: %s', 'lifterlms' ),\n\t\t\tesc_html(\n\t\t\t\tbasename(\n\t\t\t\t\t$logs[ $current ]\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t\t?>\n\t\t<?php if ( $delete_url ) : ?>\n\t\t\t<a class=\"llms-button-danger small\" href=\"<?php echo esc_url( $delete_url ); ?>\"><?php esc_html_e( 'Delete', 'lifterlms' ); ?></a>\n\t\t<?php endif; ?>\n\t</h2>\n\n\t<div class=\"llms-log-viewer\">\n\t\t<pre><?php echo esc_html( file_get_contents( $logs[ $current ] ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Not a remote URL. ?></pre>\n\t</div>\n\n</div>\n"
  },
  {
    "path": "includes/admin/views/user-edit-fields.php",
    "content": "<?php\n/**\n * Display LifterLMS Profile fields in admin user screen\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( empty( $fields ) ) {\n\treturn;\n}\n?>\n<div id=\"llms-profile-fields\">\n\t<h2><?php esc_html_e( 'LifterLMS Profile', 'lifterlms' ); ?></h2>\n\t<?php array_map( 'llms_form_field', $fields, array_fill( 0, count( $fields ), true ), array_fill( 0, count( $fields ), $user ) ); ?>\n\n\t<?php\n\t$allow_unlimited = get_user_option( 'llms_allow_unlimited_quiz_time', is_numeric( $user ) ? $user : $user->ID );\n\t$allow_unlimited = empty( $allow_unlimited ) ? 'no' : $allow_unlimited;\n\t?>\n\t<div class=\"llms-allow-unlimited-quiz-time llms-allow-unlimited-quiz-time-wrap\">\n\t\t<label for=\"llms_allow_unlimited_quiz_time\">\n\t\t\t<input name=\"llms_allow_unlimited_quiz_time\" type=\"checkbox\" id=\"llms_allow_unlimited_quiz_time\" value=\"yes\"<?php checked( 'yes', $allow_unlimited ); ?>>\n\t\t\t<?php esc_html_e( 'Allow unlimited quiz time for quizzes that have a time limit set', 'lifterlms' ); ?>\n\t\t</label>\n\t</div>\n</div>\n"
  },
  {
    "path": "includes/assets/index.php",
    "content": "<?php // Quiet.\n"
  },
  {
    "path": "includes/assets/llms-assets-scripts.php",
    "content": "<?php\n/**\n * The main LifterLMS Script Asset Definition list\n *\n * This file returns an array of script asset definition arrays.\n *\n * The array key of each definition is the asset's \"handle\" which\n * is used by both LifterLMS and WordPress to identify the asset\n * during registration and enqueue.\n *\n * The remaining items in each definition are optional and will be\n * automatically populated with default values. See `LLMS_Assets::get_defaults()`\n * for information on the default values of the asset.\n *\n * See `LLMS_Assets::get()` for full documentation on the properties\n * of an asset definition.\n *\n * @package LifterLMS/Assets\n *\n * @since 4.4.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Scripts assets list.\n *\n * @since 4.4.0\n * @since 4.8.0 Added llms-admin-setup.\n * @since 5.0.0 Added llms-select2.\n * @since 5.5.0 Added llms-addons.\n * @since 6.0.0 Added llms-admin-certificate-editor.\n * @since 6.10.0 Added llms-quill-wordcount.\n * @since 7.0.0 Added llms-spinner.\n * @since 7.4.0 Renamed llms-admin-setup to llms-admin-wizard.\n * @since 7.5.0 Added llms-favorites.\n */\nreturn array(\n\n\t// Core.\n\t'llms'                                      => array(\n\t\t'dependencies' => array( 'jquery', 'wp-i18n' ),\n\t),\n\t'llms-form-checkout'                        => array(\n\t\t'dependencies' => array( 'jquery' ),\n\t),\n\t'llms-notifications'                        => array(\n\t\t'dependencies' => array( 'jquery' ),\n\t),\n\t'llms-quiz'                                 => array(\n\t\t'dependencies' => array( 'jquery', 'llms', 'wp-mediaelement' ),\n\t),\n\t'llms-favorites'                            => array(\n\t\t'dependencies' => array( 'jquery', 'llms' ),\n\t),\n\n\t// Admin.\n\t'llms-addons'                               => array(\n\t\t'asset_file' => true,\n\t\t'file_name'  => 'llms-admin-addons',\n\t\t'suffix'     => '',\n\t),\n\t'llms-admin-award-certificate'              => array(\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\t'llms-admin-wizard'                         => array(\n\t\t'dependencies' => array( 'jquery' ),\n\t),\n\t'llms-admin-forms'                          => array(\n\t\t'dependencies' => array( 'wp-i18n' ),\n\t),\n\t'llms-admin-certificate-editor'             => array(\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\t'llms-admin-media-protection-block-protect' => array(\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\n\t'llms-admin-elementor-editor'               => array(\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\n\t// Modules.\n\t'llms-components'                           => array(\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\t'llms-icons'                                => array(\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\t'llms-spinner'                              => array(\n\t\t/**\n\t\t * This script is automatically included in the `llms` script file.\n\t\t *\n\t\t * If your JS already defines `llms` as a dependency and you wish to use the `llms-spinner` it's recommended\n\t\t * you don't also define this as a dependency as it will cause an superfluous HTTP request.\n\t\t */\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\t'llms-utils'                                => array(\n\t\t'asset_file' => true,\n\t\t'suffix'     => '',\n\t),\n\n\t// Quill Modules.\n\t'llms-quill-wordcount'                      => array(\n\t\t'asset_file'   => true,\n\t\t'suffix'       => '',\n\t\t'dependencies' => array( 'llms-quill' ),\n\t),\n\n\t// Vendor.\n\t'llms-iziModal'                             => array(\n\t\t'file_name' => 'iziModal',\n\t\t'path'      => 'assets/vendor/izimodal',\n\t\t'version'   => '1.5.1',\n\t),\n\t'llms-jquery-matchheight'                   => array(\n\t\t'file_name'    => 'jquery.matchHeight',\n\t\t'path'         => 'assets/js/vendor/',\n\t\t'suffix'       => '',\n\t\t'version'      => '0.7.0',\n\t\t'dependencies' => array( 'jquery' ),\n\t),\n\t'llms-select2'                              => array(\n\t\t'file_name'    => 'select2',\n\t\t'path'         => 'assets/vendor/select2/js',\n\t\t'version'      => '4.0.13',\n\t\t'dependencies' => array( 'jquery' ),\n\t),\n\t'webui-popover'                             => array(\n\t\t'file_name'    => 'jquery.webui-popover',\n\t\t'path'         => 'assets/vendor/webui-popover',\n\t\t'version'      => '1.2.15',\n\t\t'dependencies' => array( 'jquery' ),\n\t),\n\n);\n"
  },
  {
    "path": "includes/assets/llms-assets-styles.php",
    "content": "<?php\n/**\n * The main LifterLMS Stylesheet Asset Definition list\n *\n * This file returns an array of stylesheet asset definition arrays.\n *\n * The array key of each definition is the asset's \"handle\" which\n * is used by both LifterLMS and WordPress to identify the asset\n * during registration and enqueue.\n *\n * The remaining items in each definition are optional and will be\n * automatically populated with default values. See `LLMS_Assets::get_defaults()`\n * for information on the default values of the asset.\n *\n * See `LLMS_Assets::get()` for full documentation on the properties\n * of an asset definition.\n *\n * @package LifterLMS/Assets\n *\n * @since 4.4.0\n * @version 7.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Stylesheets assets list\n *\n * @since 4.4.0\n * @since 4.4.4 Added llms-admin-setup.\n * @since 4.8.0 Added llms-admin-importer.\n * @since 5.0.0 Added llms-select2.\n * @since 5.5.0 Added llms-admin-add-ons.\n * @since 7.4.0 Renamed llms-admin-setup to llms-admin-wizard.\n */\nreturn array(\n\n\t// Core.\n\t'lifterlms-styles'    => array(\n\t\t'file_name' => 'lifterlms',\n\t),\n\t'certificates'        => array(),\n\n\t// Admin.\n\t'llms-admin-add-ons'  => array(\n\t\t'file_name' => 'llms-admin-addons',\n\t\t'suffix'    => '',\n\t),\n\t'llms-admin-wizard'   => array(\n\t\t'file_name' => 'admin-wizard',\n\t),\n\t'llms-admin-importer' => array(\n\t\t'file_name' => 'admin-importer',\n\t),\n\n\t// Vendor.\n\t'llms-iziModal'       => array(\n\t\t'file_name' => 'iziModal',\n\t\t'path'      => 'assets/vendor/izimodal',\n\t\t'version'   => '1.5.1',\n\t\t'rtl'       => false,\n\t),\n\t'llms-select2-styles' => array(\n\t\t'file_name' => 'select2',\n\t\t'path'      => 'assets/vendor/select2/css',\n\t\t'version'   => '4.0.13',\n\t\t'rtl'       => false,\n\t),\n\t'webui-popover'       => array(\n\t\t'file_name' => 'jquery.webui-popover',\n\t\t'path'      => 'assets/vendor/webui-popover',\n\t\t'version'   => '1.2.15',\n\t\t'rtl'       => false,\n\t),\n\n);\n"
  },
  {
    "path": "includes/beaver-builder/index.php",
    "content": "<?php // shhhhh.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-author/class.llms.lab.course.author.module.php",
    "content": "<?php\n/**\n * LifterLMS Course Author Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseAuthor/Classes\n *\n * @since 1.3.0\n * @version 1.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Course Author Module class.\n *\n * @since 1.3.0\n */\nclass LLMS_Lab_Course_Author_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Course Author', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays the name, author, and bio for the author of a course.', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'course-author/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'course-author/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\t}\n}\n\nFLBuilder::register_module(\n\t'LLMS_Lab_Course_Author_Module',\n\tarray(\n\t\t'general' => array(\n\t\t\t'title'    => esc_html__( 'General', 'lifterlms' ),\n\t\t\t'sections' => array(\n\t\t\t\t'general' => array(\n\t\t\t\t\t'title'  => esc_html__( 'General', 'lifterlms' ),\n\t\t\t\t\t'fields' => array(\n\t\t\t\t\t\t'llms_course_id'   => array(\n\t\t\t\t\t\t\t'type'    => 'suggest',\n\t\t\t\t\t\t\t'action'  => 'fl_as_posts',\n\t\t\t\t\t\t\t'data'    => 'course',\n\t\t\t\t\t\t\t'limit'   => 1,\n\t\t\t\t\t\t\t'label'   => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t\t\t\t\t'help'    => esc_html__( 'Select the course to display the author from. Leave blank for the current course.', 'lifterlms' ),\n\t\t\t\t\t\t\t'preview' => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'llms_avatar_size' => array(\n\t\t\t\t\t\t\t'default'     => 48,\n\t\t\t\t\t\t\t'type'        => 'unit',\n\t\t\t\t\t\t\t'label'       => esc_html__( 'Avatar Size', 'lifterlms' ),\n\t\t\t\t\t\t\t'description' => 'px',\n\t\t\t\t\t\t\t'preview'     => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'llms_show_bio'    => array(\n\t\t\t\t\t\t\t'type'    => 'select',\n\t\t\t\t\t\t\t'label'   => esc_html__( 'Display Author Bio', 'lifterlms' ),\n\t\t\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t\t\t'no'  => esc_html__( 'No', 'lifterlms' ),\n\t\t\t\t\t\t\t\t'yes' => esc_html__( 'Yes', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t'preview' => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n);\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-author/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Course Author Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseAuthor/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped attributes.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$course_id = ! empty( $settings->llms_course_id ) ? $settings->llms_course_id : get_the_ID();\n?>\n\n<div class=\"llms-lab-course-author\">\n\t[lifterlms_course_author avatar_size=\"<?php echo esc_attr( $settings->llms_avatar_size ); ?>\" bio=\"<?php echo esc_attr( $settings->llms_show_bio ); ?>\" course_id=\"<?php echo esc_attr( $course_id ); ?>\"]\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-author/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-author/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-continue-button/class.llms.lab.course.continue.button.module.php",
    "content": "<?php\n/**\n * LifterLMS Course Continue Button Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseContinueButton/Classes\n *\n * @since 1.3.0\n * @version 1.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Course Continue Button Module class.\n *\n * @since 1.3.0\n */\nclass LLMS_Lab_Course_Continue_Button_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Course Continue Button', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays a course progress bar for the current course.', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'course-continue-button/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'course-continue-button/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\n\t\tadd_filter( 'llms_course_continue_button_next_lesson', array( $this, 'force_display' ) );\n\t}\n\n\t/**\n\t * Force display.\n\t *\n\t * @since 1.7.0\n\t *\n\t * @param int $lesson_id WP_Post ID of the lesson.\n\t * @return int\n\t */\n\tpublic function force_display( $lesson_id ) {\n\n\t\tif ( ! $lesson_id && FLBuilderModel::is_builder_active() ) {\n\t\t\treturn get_the_ID();\n\t\t}\n\n\t\treturn $lesson_id;\n\t}\n}\n\nFLBuilder::register_module(\n\t'LLMS_Lab_Course_Continue_Button_Module',\n\tarray(\n\t\t'general' => array(\n\t\t\t'title'    => esc_html__( 'General', 'lifterlms' ),\n\t\t\t'sections' => array(\n\t\t\t\t'general' => array(\n\t\t\t\t\t'title'  => esc_html__( 'General', 'lifterlms' ),\n\t\t\t\t\t'fields' => array(\n\t\t\t\t\t\t'llms_course_id' => array(\n\t\t\t\t\t\t\t'type'    => 'suggest',\n\t\t\t\t\t\t\t'action'  => 'fl_as_posts',\n\t\t\t\t\t\t\t'data'    => 'course',\n\t\t\t\t\t\t\t'limit'   => 1,\n\t\t\t\t\t\t\t'label'   => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t\t\t\t\t'help'    => esc_html__( 'Select the course to display a continue button for. Leave blank for the current course.', 'lifterlms' ),\n\t\t\t\t\t\t\t'preview' => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n);\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-continue-button/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Course Continue Button Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseContinueButton/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped attributes.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$course_id = ! empty( $settings->llms_course_id ) ? $settings->llms_course_id : get_the_ID();\n?>\n\n<div class=\"llms-lab-course-continue-button\">\n\t[lifterlms_course_continue_button course_id=\"<?php echo esc_attr( $course_id ); ?>\"]\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-continue-button/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-continue-button/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-instructors/class.llms.lab.course.instructors.module.php",
    "content": "<?php\n/**\n * LifterLMS Course Instructors Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseContinueButton/Classes\n *\n * @since 8.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nclass LLMS_Lab_Course_Instructors_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Course Instructors', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays instructors for the current course.', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'course-instructors/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'course-instructors/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\t}\n}\n\nFLBuilder::register_module( 'LLMS_Lab_Course_Instructors_Module', array() );\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-instructors/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Course Continue Button Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseContinueButton/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped attributes.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n\n<div class=\"llms-lab-course-instructors\">\n\t[lifterlms_course_instructors]\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-instructors/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-instructors/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-meta-info/class.llms.lab.course.meta.info.module.php",
    "content": "<?php\n/**\n * LifterLMS Course Meta Info Module Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseMetaInfo/Classes\n *\n * @since 1.3.0\n * @version 1.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLifterLMS Course Meta Info Module class.\n *\n * @since 1.3.0\n */\nclass LLMS_Lab_Course_Meta_Info_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Course Information', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays course information: length, difficulty, tracks, categories, and tags.', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'course-meta-info/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'course-meta-info/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\t}\n}\n\nFLBuilder::register_module(\n\t'LLMS_Lab_Course_Meta_Info_Module',\n\tarray(\n\t\t'general' => array(\n\t\t\t'title'    => esc_html__( 'General', 'lifterlms' ),\n\t\t\t'sections' => array(\n\t\t\t\t'general' => array(\n\t\t\t\t\t'title'  => esc_html__( 'General', 'lifterlms' ),\n\t\t\t\t\t'fields' => array(\n\t\t\t\t\t\t'llms_course_id' => array(\n\t\t\t\t\t\t\t'type'    => 'suggest',\n\t\t\t\t\t\t\t'action'  => 'fl_as_posts',\n\t\t\t\t\t\t\t'data'    => 'course',\n\t\t\t\t\t\t\t'limit'   => 1,\n\t\t\t\t\t\t\t'label'   => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t\t\t\t\t'help'    => esc_html__( 'Select the course to display the course information from. Leave blank for the current course.', 'lifterlms' ),\n\t\t\t\t\t\t\t'preview' => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n);\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-meta-info/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Course Meta Info Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseMetaInfo/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped attributes.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$course_id = ! empty( $settings->llms_course_id ) ? $settings->llms_course_id : get_the_ID();\n?>\n\n<div class=\"llms-lab-course-meta-info\">\n\t[lifterlms_course_meta_info course_id=\"<?php echo esc_attr( $course_id ); ?>\"]\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-meta-info/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-meta-info/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-progress-bar/class.llms.lab.course.progress.bar.module.php",
    "content": "<?php\n/**\n * LifterLMS Course Progress Bar Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseProgressBar/Classes\n *\n * @since 1.3.0\n * @version 1.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Course Progress Module class.\n *\n * @since 1.3.0\n */\nclass LLMS_Lab_Course_Progress_Bar_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Course Progress Bar', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays a course progress bar for the current course.', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'course-progress-bar/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'course-progress-bar/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\t}\n}\n\nFLBuilder::register_module( 'LLMS_Lab_Course_Progress_Bar_Module', array() );\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-progress-bar/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Course Progress Bar Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseProgressBar/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped attributes.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-lab-course-progress-bar\">\n\t[lifterlms_course_progress]\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-progress-bar/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-progress-bar/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-syllabus/class.llms.lab.course.syllabus.module.php",
    "content": "<?php\n/**\n * LifterLMS Course Syllabus Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseSyllabus/Classes\n *\n * @since 1.3.0\n * @version 1.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Course Syllabus Module class.\n *\n * @since 1.3.0\n */\nclass LLMS_Lab_Course_Syllabus_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Course Syllabus', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays a course syllabus current course.', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'course-syllabus/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'course-syllabus/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\t}\n}\n\nFLBuilder::register_module( 'LLMS_Lab_Course_Syllabus_Module', array() );\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-syllabus/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Course Syllabus Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/CourseSyllabus/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped attributes.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-lab-course-syllabus\">\n\t[lifterlms_course_syllabus]\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-syllabus/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/course-syllabus/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/lesson-mark-complete/class.llms.lab.lesson.mark.complete.module.php",
    "content": "<?php\n/**\n * LifterLMS Lesson Mark Complete Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/LessonMarkComplete/Classes\n *\n * @since 1.3.0\n * @version 1.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Lesson Mark Complete Module class.\n *\n * @since 1.3.0\n */\nclass LLMS_Lab_Lesson_Mark_Complete_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Lesson Mark Complete Button', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays the mark complete / incomplete button(s) for a lesson', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'lesson-mark-complete/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'lesson-mark-complete/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\t}\n}\n\nFLBuilder::register_module( 'LLMS_Lab_Lesson_Mark_Complete_Module', array() );\n"
  },
  {
    "path": "includes/beaver-builder/modules/lesson-mark-complete/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Lesson Mark Complete Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/LessonMarkComplete/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped strings.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-lab-lesson-mark-complete\">\n<?php if ( FLBuilderModel::is_builder_active() ) : ?>\n\t<div class=\"clear\"></div>\n\t<div class=\"llms-lesson-button-wrapper\">\n\t\t<?php\n\t\tllms_form_field(\n\t\t\tarray(\n\t\t\t\t'columns'     => 12,\n\t\t\t\t'classes'     => 'llms-button-primary auto button',\n\t\t\t\t'id'          => 'llms_mark_complete',\n\t\t\t\t'value'       => apply_filters( 'lifterlms_mark_lesson_complete_button_text', esc_html__( 'Mark Complete', 'lifterlms' ), llms_get_post( get_the_ID() ) ),\n\t\t\t\t'last_column' => true,\n\t\t\t\t'name'        => 'mark_complete',\n\t\t\t\t'required'    => false,\n\t\t\t\t'type'        => 'submit',\n\t\t\t)\n\t\t);\n\t\t?>\n\t</div>\n<?php else : ?>\n\t[lifterlms_lesson_mark_complete]\n<?php endif; ?>\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/lesson-mark-complete/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/lesson-mark-complete/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/membership-instructors/class.llms.lab.membership.instructors.module.php",
    "content": "<?php\n/**\n * LifterLMS Membership Instructors Module\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/MembershipInstructors/Classes\n *\n * @since 8.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nclass LLMS_Lab_Membership_Instructors_Module extends FLBUilderModule {\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Membership Instructors', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'Displays instructors for the current membership.', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'membership-instructors/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'membership-instructors/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\t}\n}\n\nFLBuilder::register_module( 'LLMS_Lab_Membership_Instructors_Module', array() );\n"
  },
  {
    "path": "includes/beaver-builder/modules/membership-instructors/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Membership Instructors HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/MembershipInstructors/Templates\n *\n * @since 8.0.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n\n<div class=\"llms-lab-membership-instructors\">\n\t[lifterlms_membership_instructors]\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/membership-instructors/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/membership-instructors/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/pricing-table/class.llms.lab.pricing.table.module.php",
    "content": "<?php\n/**\n * LifterLMS Course/Membership Pricing Table Module HTML\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/PricingTable/Classes\n *\n * @since 1.3.0\n * @version 1.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Course/Membership Pricing Table Module HTML class.\n *\n * @since 1.3.0\n */\nclass LLMS_Lab_Pricing_Table_Module extends FLBUilderModule {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Escape strings.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tparent::__construct(\n\t\t\tarray(\n\t\t\t\t'name'          => esc_html__( 'Pricing Table', 'lifterlms' ),\n\t\t\t\t'description'   => esc_html__( 'LifterLMS Course / Membership Pricing Table', 'lifterlms' ),\n\t\t\t\t'category'      => esc_html__( 'LifterLMS Modules', 'lifterlms' ),\n\t\t\t\t'dir'           => LLMS_BB_MODULES_DIR . 'pricing-table/',\n\t\t\t\t'url'           => LLMS_BB_MODULES_URL . 'pricing-table/',\n\t\t\t\t'editor_export' => false,\n\t\t\t\t'enabled'       => true,\n\t\t\t)\n\t\t);\n\n\t\t// Ensure pricing tables always display when used within a BB module.\n\t\tadd_action( 'llms_lab_bb_before_pricing_table', array( $this, 'add_force_show_table_filter' ) );\n\t\tadd_action( 'lifterlms_after_access_plans', array( $this, 'remove_force_show_table_filter' ) );\n\n\t\t// Ensure pricing tables always display when the frontend builder is active.\n\t\tadd_filter( 'llms_product_pricing_table_enrollment_status', array( $this, 'show_table' ) );\n\t}\n\n\t/**\n\t * Force display of pricing tables within BB modules.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function add_force_show_table_filter() {\n\t\tadd_filter( 'llms_product_pricing_table_enrollment_status', '__return_false' );\n\t}\n\n\t/**\n\t * Remove force display after pricing tables within BB modules.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function remove_force_show_table_filter() {\n\t\tremove_filter( 'llms_product_pricing_table_enrollment_status', '__return_false' );\n\t}\n\n\t/**\n\t * Get the product ID to be used based of BB module settings.\n\t *\n\t * @since 1.3.0\n\t * @since 1.3.0 Use strict comparison for `in_array`.\n\t *\n\t * @param obj $settings BB node settings object.\n\t * @return int|false\n\t */\n\tpublic function get_product_id( $settings ) {\n\n\t\t$type = $settings->llms_product_type;\n\t\tif ( ! $type ) {\n\t\t\t$id = get_the_ID();\n\t\t} elseif ( 'course' === $type || 'membership' === $type ) {\n\t\t\t$key = sprintf( 'llms_%s_id', $type );\n\t\t\t$id  = $settings->$key;\n\t\t}\n\n\t\tif ( in_array( get_post_type( $id ), array( 'lesson', 'llms_quiz' ), true ) ) {\n\t\t\t$course = llms_get_post_parent_course( $id );\n\t\t\t$id     = $course->get( 'id' );\n\t\t}\n\n\t\t// If the current id isn't a course or membership don't proceed.\n\t\tif ( ! in_array( get_post_type( $id ), array( 'course', 'llms_membership' ), true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $id;\n\t}\n\n\t/**\n\t * Always show the pricing table when the builder is active.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @param bool $enrollment Enrollment status of the current user.\n\t * @return bool\n\t */\n\tpublic function show_table( $enrollment ) {\n\n\t\tif ( FLBuilderModel::is_builder_active() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $enrollment;\n\t}\n}\n\nFLBuilder::register_module(\n\t'LLMS_Lab_Pricing_Table_Module',\n\tarray(\n\t\t'general' => array(\n\t\t\t'title'    => esc_html__( 'General', 'lifterlms' ),\n\t\t\t'sections' => array(\n\t\t\t\t'general' => array(\n\t\t\t\t\t'title'  => esc_html__( 'General', 'lifterlms' ),\n\t\t\t\t\t'fields' => array(\n\t\t\t\t\t\t'llms_product_type'  => array(\n\t\t\t\t\t\t\t'type'    => 'select',\n\t\t\t\t\t\t\t'label'   => esc_html__( 'Product Type', 'lifterlms' ),\n\t\t\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t\t\t''           => esc_html__( 'Current Course or Membership', 'lifterlms' ),\n\t\t\t\t\t\t\t\t'course'     => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t\t\t\t\t\t'membership' => esc_html__( 'Memebership', 'lifterlms' ),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t'toggle'  => array(\n\t\t\t\t\t\t\t\t'course'     => array(\n\t\t\t\t\t\t\t\t\t'fields' => array( 'llms_course_id' ),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t'membership' => array(\n\t\t\t\t\t\t\t\t\t'fields' => array( 'llms_membership_id' ),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t'preview' => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'llms_course_id'     => array(\n\t\t\t\t\t\t\t'type'    => 'suggest',\n\t\t\t\t\t\t\t'action'  => 'fl_as_posts',\n\t\t\t\t\t\t\t'data'    => 'course',\n\t\t\t\t\t\t\t'limit'   => 1,\n\t\t\t\t\t\t\t'label'   => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t\t\t\t\t'help'    => esc_html__( 'Choose which course to display a pricing table for.', 'lifterlms' ),\n\t\t\t\t\t\t\t'preview' => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'llms_membership_id' => array(\n\t\t\t\t\t\t\t'type'    => 'suggest',\n\t\t\t\t\t\t\t'action'  => 'fl_as_posts',\n\t\t\t\t\t\t\t'data'    => 'llms_membership',\n\t\t\t\t\t\t\t'limit'   => 1,\n\t\t\t\t\t\t\t'label'   => esc_html__( 'Membership', 'lifterlms' ),\n\t\t\t\t\t\t\t'help'    => esc_html__( 'Choose which membership to display a pricing table for.', 'lifterlms' ),\n\t\t\t\t\t\t\t'preview' => array(\n\t\t\t\t\t\t\t\t'type' => 'none',\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n);\n"
  },
  {
    "path": "includes/beaver-builder/modules/pricing-table/includes/frontend.php",
    "content": "<?php\n/**\n * LifterLMS Course/Membership Pricing Table Module HTML.\n *\n * @package LifterLMS_Labs/Labs/BeaverBuilder/Modules/PricingTable/Templates\n *\n * @since 1.3.0\n * @since 1.7.0 Escaped attributes.\n * @version 1.7.0\n *\n * @param $settings obj Beaver Builder module settings instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$product_id = $module->get_product_id( $settings );\nif ( ! $product_id ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-lab-pricing-table\">\n\t<?php do_action( 'llms_lab_bb_before_pricing_table', $product_id ); ?>\n\t[lifterlms_pricing_table product=\"<?php echo esc_attr( $product_id ); ?>\"]\n\t<?php do_action( 'llms_lab_bb_after_pricing_table', $product_id ); ?>\n</div>\n"
  },
  {
    "path": "includes/beaver-builder/modules/pricing-table/includes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/pricing-table/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/beaver-builder/modules/pricing-table/js/frontend.js",
    "content": "LLMS.Pricing_Tables.init();\n"
  },
  {
    "path": "includes/beaver-builder/templates/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-course-author.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Course Author class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Course_Author extends \\Bricks\\Element {\n\tpublic $block        = 'llms/course-author';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-course-author';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-course-author';\n\tpublic $css_selector = '.llms-course-author-wrapper';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Course Author', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t\t$this->controls['avatar_size'] = array(\n\t\t\t'tab'         => 'content',\n\t\t\t'label'       => esc_html__( 'Avatar size', 'lifterlms' ),\n\t\t\t'type'        => 'slider',\n\t\t\t'units'       => array(\n\t\t\t\t'px' => array(\n\t\t\t\t\t'min'  => 1,\n\t\t\t\t\t'max'  => 300,\n\t\t\t\t\t'step' => 1,\n\t\t\t\t),\n\t\t\t),\n\t\t\t'default'     => 48,\n\t\t\t'description' => esc_html__( 'The size of the avatar in pixels.', 'lifterlms' ),\n\t\t);\n\n\t\t$this->controls['bio'] = array(\n\t\t\t'tab'     => 'content',\n\t\t\t'label'   => esc_html__( 'Display Bio', 'lifterlms' ),\n\t\t\t'type'    => 'checkbox',\n\t\t\t'inline'  => false,\n\t\t\t'small'   => true,\n\t\t\t'default' => true, // Default: false\n\t\t);\n\n\t\t$courses_posts = get_posts(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'course',\n\t\t\t\t'posts_per_page' => -1,         // Retrieve all posts\n\t\t\t\t'post_status'    => 'publish',   // Only published posts\n\t\t\t)\n\t\t);\n\t\t$courses       = array(\n\t\t\t'inherit' => __( 'Inherit from current course', 'lifterlms' ),\n\t\t);\n\t\tforeach ( $courses_posts as $course ) {\n\t\t\t$courses[ $course->ID ] = $course->post_title;\n\t\t}\n\n\t\t$this->controls['course_id'] = array(\n\t\t\t'tab'         => 'content',\n\t\t\t'label'       => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t'type'        => 'select',\n\t\t\t'options'     => $courses,\n\t\t\t'inline'      => false,\n\t\t\t'clearable'   => false,\n\t\t\t'pasteStyles' => false,\n\t\t\t'default'     => 'inherit',\n\t\t);\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t$element_settings = array(\n\t\t\t'avatar_size' => isset( $attributes['avatar_size'] ) ? intval( $attributes['avatar_size'] ) : 48,\n\t\t\t'bio'         => ( isset( $attributes['bio'] ) && 'no' === $attributes['bio'] ) ? false : true,\n\t\t\t'course_id'   => isset( $attributes['course_id'] ) ? intval( $attributes['course_id'] ) : 'inherit',\n\t\t);\n\n\t\treturn $element_settings;\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-course-author-wrapper';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\t$avatar_size = isset( $this->settings['avatar_size'] ) && $this->settings['avatar_size'] ? intval( $this->settings['avatar_size'] ) : 48;\n\t\t$bio         = isset( $this->settings['bio'] ) && $this->settings['bio'] ? '' : 'no';\n\t\t$course_id   = isset( $this->settings['course_id'] ) && is_numeric( $this->settings['course_id'] ) ? intval( $this->settings['course_id'] ) : '';\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo do_shortcode( '[lifterlms_course_author avatar_size=\"' . esc_attr( $avatar_size ) . '\" bio=\"' . esc_attr( $bio ) . '\" course_id=\"' . $course_id . '\"]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-course-continue.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Course Progress with Continue Button class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Course_Continue extends \\Bricks\\Element {\n\tpublic $block        = 'llms/course-continue';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-course-continue';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-course-continue';\n\tpublic $css_selector = '.llms-course-continue';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Course Progress with Continue Button', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t\t$courses_posts = get_posts(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'course',\n\t\t\t\t'posts_per_page' => -1,         // Retrieve all posts\n\t\t\t\t'post_status'    => 'publish',   // Only published posts\n\t\t\t)\n\t\t);\n\t\t$courses       = array(\n\t\t\t'inherit' => __( 'Inherit from current course', 'lifterlms' ),\n\t\t);\n\t\tforeach ( $courses_posts as $course ) {\n\t\t\t$courses[ $course->ID ] = $course->post_title;\n\t\t}\n\n\t\t$this->controls['course_id'] = array(\n\t\t\t'tab'         => 'content',\n\t\t\t'label'       => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t'type'        => 'select',\n\t\t\t'options'     => $courses,\n\t\t\t'inline'      => false,\n\t\t\t'clearable'   => false,\n\t\t\t'pasteStyles' => false,\n\t\t\t'default'     => 'inherit',\n\t\t);\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t$element_settings = array(\n\t\t\t'course_id' => isset( $attributes['course_id'] ) ? intval( $attributes['course_id'] ) : 'inherit',\n\t\t);\n\n\t\treturn $element_settings;\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-course-meta-info';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\t$course_id = isset( $this->settings['course_id'] ) && is_numeric( $this->settings['course_id'] ) ? intval( $this->settings['course_id'] ) : '';\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo do_shortcode( '[lifterlms_course_continue course_id=\"' . $course_id . '\"]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-course-information.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Course Information class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Course_Information extends \\Bricks\\Element {\n\tpublic $block        = 'llms/course-information';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-course-information';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-course-information';\n\tpublic $css_selector = '.llms-course-information-wrapper';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Course Information', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t\t// Convert to nested elements.\n\t\t$this->controls['title'] = array(\n\t\t\t'tab'     => 'content',\n\t\t\t'label'   => esc_html__( 'Title', 'lifterlms' ),\n\t\t\t'type'    => 'text',\n\t\t\t'default' => esc_html__( 'Course Information', 'lifterlms' ),\n\t\t);\n\n\t\t$this->controls['title_size'] = array(\n\t\t\t'tab'         => 'content',\n\t\t\t// 'group' => 'settings',\n\t\t\t'label'       => esc_html__( 'Title Headline Size', 'lifterlms' ),\n\t\t\t'type'        => 'select',\n\t\t\t'options'     => array(\n\t\t\t\t'h1' => esc_html__( 'h1', 'lifterlms' ),\n\t\t\t\t'h2' => esc_html__( 'h2', 'lifterlms' ),\n\t\t\t\t'h3' => esc_html__( 'h3', 'lifterlms' ),\n\t\t\t\t'h4' => esc_html__( 'h4', 'lifterlms' ),\n\t\t\t\t'h5' => esc_html__( 'h5', 'lifterlms' ),\n\t\t\t\t'h6' => esc_html__( 'h6', 'lifterlms' ),\n\t\t\t),\n\t\t\t'inline'      => true,\n\t\t\t'clearable'   => false,\n\t\t\t'pasteStyles' => false,\n\t\t\t'default'     => 'h2',\n\t\t);\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t$element_settings = array(\n\t\t\t'title'      => isset( $attributes['title'] ) ? $attributes['title'] : __( 'Course Information', 'lifterlms' ),\n\t\t\t'title_size' => isset( $attributes['title_size'] ) ? $attributes['title_size'] : 'h2',\n\t\t);\n\n\t\treturn $element_settings;\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-course-information-wrapper';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\t$title      = $this->settings['title'] ? $this->settings['title'] : __( 'Course Information', 'lifterlms' );\n\t\t$title_size = $this->settings['title_size'] ? $this->settings['title_size'] : 'h2';\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo wp_kses_post( \"<{$title_size} class='llms-meta-title'>{$title}</{$title_size}>\" );\n\n\t\techo do_shortcode( '[lifterlms_course_meta_info]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-course-meta-info.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Course Meta Info class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Course_Meta_Info extends \\Bricks\\Element {\n\tpublic $block        = 'llms/course-meta-info';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-course-meta-info';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-course-meta-info';\n\tpublic $css_selector = '.llms-course-meta-info';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Course Meta Information', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t\t$courses_posts = get_posts(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'course',\n\t\t\t\t'posts_per_page' => -1,         // Retrieve all posts\n\t\t\t\t'post_status'    => 'publish',   // Only published posts\n\t\t\t)\n\t\t);\n\t\t$courses       = array(\n\t\t\t'inherit' => __( 'Inherit from current course', 'lifterlms' ),\n\t\t);\n\t\tforeach ( $courses_posts as $course ) {\n\t\t\t$courses[ $course->ID ] = $course->post_title;\n\t\t}\n\n\t\t$this->controls['course_id'] = array(\n\t\t\t'tab'         => 'content',\n\t\t\t'label'       => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t'type'        => 'select',\n\t\t\t'options'     => $courses,\n\t\t\t'inline'      => false,\n\t\t\t'clearable'   => false,\n\t\t\t'pasteStyles' => false,\n\t\t\t'default'     => 'inherit',\n\t\t);\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t$element_settings = array(\n\t\t\t'course_id' => isset( $attributes['course_id'] ) ? intval( $attributes['course_id'] ) : 'inherit',\n\t\t);\n\n\t\treturn $element_settings;\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-course-meta-info';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\t$course_id = isset( $this->settings['course_id'] ) && is_numeric( $this->settings['course_id'] ) ? intval( $this->settings['course_id'] ) : '';\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo do_shortcode( '[lifterlms_course_meta_info course_id=\"' . $course_id . '\"]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-course-progress.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Course Progress class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Course_Progress extends \\Bricks\\Element {\n\tpublic $block        = 'llms/course-progress';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-course-progress';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-course-progress';\n\tpublic $css_selector = '.llms-course-progress-wrapper';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Course Progress', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t// Need to return an array of something for it to be converted.\n\t\treturn array( 'setting' => true );\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-course-progress-wrapper';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo do_shortcode( '[lifterlms_course_progress]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-course-syllabus.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Course Syllabus class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Course_Syllabus extends \\Bricks\\Element {\n\tpublic $block        = 'llms/course-syllabus';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-course-syllabus';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-course-syllabus';\n\tpublic $css_selector = '.llms-course-syllabus';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Course Syllabus', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t\t$courses_posts = get_posts(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'course',\n\t\t\t\t'posts_per_page' => -1,         // Retrieve all posts\n\t\t\t\t'post_status'    => 'publish',   // Only published posts\n\t\t\t)\n\t\t);\n\t\t$courses       = array(\n\t\t\t'inherit' => __( 'Inherit from current course', 'lifterlms' ),\n\t\t);\n\t\tforeach ( $courses_posts as $course ) {\n\t\t\t$courses[ $course->ID ] = $course->post_title;\n\t\t}\n\n\t\t$this->controls['course_id'] = array(\n\t\t\t'tab'         => 'content',\n\t\t\t'label'       => esc_html__( 'Course', 'lifterlms' ),\n\t\t\t'type'        => 'select',\n\t\t\t'options'     => $courses,\n\t\t\t'inline'      => false,\n\t\t\t'clearable'   => false,\n\t\t\t'pasteStyles' => false,\n\t\t\t'default'     => 'inherit',\n\t\t);\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t$element_settings = array(\n\t\t\t'course_id' => isset( $attributes['course_id'] ) ? intval( $attributes['course_id'] ) : 'inherit',\n\t\t);\n\n\t\treturn $element_settings;\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-course-syllabus';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\t$course_id = isset( $this->settings['course_id'] ) && is_numeric( $this->settings['course_id'] ) ? intval( $this->settings['course_id'] ) : '';\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo do_shortcode( '[lifterlms_course_syllabus course_id=\"' . $course_id . '\"]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-instructors.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Instructors class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Instructors extends \\Bricks\\Element {\n\tpublic $block        = 'llms/instructors';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-instructors';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-instructors';\n\tpublic $css_selector = '.llms-instructors';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Instructors', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t// Need to return an array of something for it to be converted.\n\t\treturn array( 'setting' => true );\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-instructors';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\tif ( 'llms_membership' === get_post_type() ) {\n\t\t\techo do_shortcode( '[lifterlms_membership_instructors]' );\n\t\t} else {\n\t\t\techo do_shortcode( '[lifterlms_course_instructors]' );\n\t\t}\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-lesson-progression.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Lesson Progression class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Lesson_Progression extends \\Bricks\\Element {\n\tpublic $block        = 'llms/lesson-progression';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-lesson-progression';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-lesson-progression';\n\tpublic $css_selector = '.llms-lesson-progression-wrapper';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'Lesson Progression (Mark Complete)', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t// Need to return an array of something for it to be converted.\n\t\treturn array( 'setting' => true );\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-lesson-progression-wrapper';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo do_shortcode( '[lifterlms_lesson_mark_complete]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/bricks/class-llms-bricks-element-pricing-table.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n/**\n * LifterLMS Bricks Pricing Table class.\n *\n * @since 8.0.3\n */\nclass LLMS_Bricks_Element_Pricing_Table extends \\Bricks\\Element {\n\tpublic $block        = 'llms/pricing-table';\n\tpublic $category     = 'lifterlms';\n\tpublic $name         = 'llms-pricing-table';\n\tpublic $icon         = 'llms-bricks-icon llms-bricks-icon-pricing-table';\n\tpublic $css_selector = '.llms-pricing-table-wrapper';\n\tpublic $scripts      = array();\n\n\tpublic function get_label() {\n\t\treturn esc_html__( 'LifterLMS Pricing Table', 'lifterlms' );\n\t}\n\n\tpublic function set_control_groups() {\n\t}\n\n\tpublic function set_controls() {\n\t}\n\n\tpublic function enqueue_scripts() {\n\t}\n\n\tpublic function convert_block_to_element_settings( $block, $attributes ) {\n\t\t// TODO: Visibility settings.\n\t\t// Need to return an array of something for it to be converted.\n\t\treturn array( 'setting' => true );\n\t}\n\n\tpublic function render() {\n\t\t$root_classes[] = 'llms-pricing-table-wrapper';\n\n\t\t$this->set_attribute( '_root', 'class', $root_classes );\n\n\t\techo \"<div {$this->render_attributes( '_root' )}>\"; // Element root attributes\n\n\t\techo do_shortcode( '[lifterlms_pricing_table]' );\n\n\t\techo '</div>';\n\t}\n}\n"
  },
  {
    "path": "includes/certificates/class.llms.certificate.user.php",
    "content": "<?php\n/**\n * Certificate\n *\n * @package LifterLMS/Classes/Certificates\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Certificate class\n *\n * Generates certificate post for user, triggered from engagement.\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @deprecated 6.0.0 Class `LLMS_Certificate_User` is deprecated with no direct replacement.\n */\nclass LLMS_Certificate_User extends LLMS_Certificate {\n\n\t/**\n\t * @var string|false\n\t * @since 1.0.0\n\t */\n\tpublic $account_link;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $email_content;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $recipient;\n\n\t/**\n\t * partial path and file name of HTML template\n\t *\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $template_html;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tpublic $user = array();\n\n\t/**\n\t * @var WP_User|false\n\t * @since 1.0.0\n\t */\n\tpublic $user_data;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_email;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_firstname;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_lastname;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_login;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $user_pass;\n\n\t/**\n\t * Alert when deprecated methods are used.\n\t *\n\t * This class as well as core classes extending it have been deprecated. All public and protected methods\n\t * have been changed to private and will be made accessible through this magic method which also emits a\n\t * deprecation warning.\n\t *\n\t * This public method has been intentionally marked as private to denote it's temporary lifespan. It will be\n\t * removed alongside this class in the next major release.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @access private\n\t *\n\t * @param string $name Name of the method being called.\n\t * @param array  $args Arguments provided to the method.\n\t * @return void\n\t */\n\tpublic function __call( $name, $args ) {\n\t\t_deprecated_function( __CLASS__ . '::' . esc_html( $name ), '6.0.0' );\n\t\tif ( method_exists( $this, $name ) ) {\n\t\t\t$this->$name( ...$args );\n\t\t}\n\t}\n\n\t/**\n\t * Check if the user has already earned this achievement used to prevent duplicates\n\t *\n\t * @since 3.4.1\n\t * @since 3.17.4 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tprivate function has_user_earned() {\n\n\t\tglobal $wpdb;\n\n\t\t$count = (int) $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT COUNT( pm.meta_id )\n\t\t\tFROM {$wpdb->postmeta} AS pm\n\t\t\tJOIN {$wpdb->prefix}lifterlms_user_postmeta AS upm ON pm.post_id = upm.meta_value\n\t\t\tWHERE pm.meta_key = '_llms_certificate_template'\n\t\t\t  AND pm.meta_value = %d\n\t\t\t  AND upm.meta_key = '_certificate_earned'\n\t\t\t  AND upm.user_id = %d\n\t\t\t  AND upm.post_id = %d\n\t\t\t  LIMIT 1\n\t\t\t;\",\n\t\t\t\tarray( $this->certificate_template_id, $this->userid, $this->lesson_id )\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Deprecated.\n\t\t *\n\t\t * @since Unknown.\n\t\t * @deprecated 6.0.0 Filter `llms_certificate_has_user_earned` is deprecated in favor of `llms_earned_certificate_dupcheck`.\n\t\t *\n\t\t * @param boolean               $has_earned Whether or not the certificate has been earned.\n\t\t * @param LLMS_Certificate_User $user_cert  The user certificate object.\n\t\t */\n\t\treturn apply_filters_deprecated( 'llms_certificate_has_user_earned', array( ( $count >= 1 ), $this ), 'llms_earned_certificate_dupcheck' );\n\t}\n\n\t/**\n\t * Sets up data needed to generate certificate.\n\t *\n\t * @since Unknown\n\t * @since 3.24.0 Unknown.\n\t * @deprecated 6.0.0 `LLMS_Certificate_User::init()` is deprecated with no replacement.\n\t *\n\t * @param int $email_id  ID of Certificate.\n\t * @param int $person_id ID of the user receiving the certificate.\n\t * @param int $lesson_id ID of associated lesson.\n\t * @return void\n\t */\n\tprivate function init( $email_id, $person_id, $lesson_id ) {\n\n\t\tglobal $wpdb;\n\n\t\t$email_content = get_post( $email_id );\n\t\t$email_meta    = get_post_meta( $email_content->ID );\n\n\t\t$this->certificate_template_id = $email_id;\n\t\t$this->lesson_id               = $lesson_id;\n\t\t$this->title                   = $email_content->post_title;\n\t\t$this->certificate_title       = $email_meta['_llms_certificate_title'][0] ?? $email_content->post_title;\n\t\t$this->content                 = $email_content->post_content;\n\t\t$this->image                   = $email_meta['_llms_certificate_image'][0] ?? '';\n\t\t$this->userid                  = $person_id;\n\t\t$this->user                    = get_user_meta( $person_id );\n\t\t$this->user_data               = get_userdata( $person_id );\n\t\t$this->user_firstname          = ( '' != $this->user['first_name'][0] ? $this->user['first_name'][0] : $this->user['nickname'][0] );\n\t\t$this->user_lastname           = ( '' != $this->user['last_name'][0] ? $this->user['last_name'][0] : '' );\n\t\t$this->user_email              = $this->user_data->data->user_email;\n\t\t$this->template_html           = 'certificates/template.php';\n\t\t$this->email_content           = $email_content->post_content;\n\t\t$this->account_link            = get_permalink( llms_get_page_id( 'myaccount' ) );\n\n\t\t$this->user_login = $this->user_data->user_login;\n\t}\n\n\t/**\n\t * Award the cert to a user.\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Certificate_User::trigger()` is deprecated with no replacement.\n\t *\n\t * @param int $user_id   ID of the user receiving the certificate.\n\t * @param int $email_id  ID of the certificate.\n\t * @param int $lesson_id ID of the associated lesson.\n\t *\n\t * @return void\n\t */\n\tprivate function trigger( $user_id, $email_id, $lesson_id ) {\n\n\t\t$this->init( $email_id, $user_id, $lesson_id );\n\n\t\t// Only award cert if the user hasn't already earned it.\n\t\tif ( $this->has_user_earned() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( $user_id ) {\n\t\t\t$this->object     = new WP_User( $user_id );\n\t\t\t$this->user_email = stripslashes( $this->object->user_email );\n\t\t\t$this->recipient  = $this->user_email;\n\n\t\t}\n\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn; }\n\n\t\t$this->create( $this->get_content() );\n\t}\n\n\t/**\n\t * get_content_html function.\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.4 Unknown.\n\t * @since 5.0.0 Merge the [llms-user] (and others) shortcode.\n\t * @deprecated 6.0.0 `LLMS_Certificate_User::get_content_html()` is deprecated with no replacement.\n\t *\n\t * @return string\n\t */\n\tprivate function get_content_html() {\n\n\t\tadd_filter( 'llms_user_info_shortcode_user_id', array( $this, 'set_shortcode_user' ) );\n\n\t\t$codes = apply_filters(\n\t\t\t'llms_certificate_merge_codes',\n\t\t\tarray(\n\t\t\t\t'{site_title}'    => $this->get_blogname(),\n\t\t\t\t'{user_login}'    => $this->user_login,\n\t\t\t\t'{site_url}'      => $this->account_link,\n\t\t\t\t'{first_name}'    => $this->user_firstname,\n\t\t\t\t'{last_name}'     => $this->user_lastname,\n\t\t\t\t'{email_address}' => $this->user_email,\n\t\t\t\t'{student_id}'    => $this->userid,\n\t\t\t\t'{current_date}'  => date_i18n( get_option( 'date_format' ), current_time( 'timestamp' ) ),\n\t\t\t),\n\t\t\t$this\n\t\t);\n\n\t\t$this->find    = array_keys( $codes );\n\t\t$this->replace = array_values( $codes );\n\n\t\t$content = $this->format_string( $this->content );\n\n\t\t// In certain circumstances shortcodes won't be registered yet.\n\t\tLLMS_Shortcodes::init();\n\n\t\tob_start();\n\t\tllms_get_template(\n\t\t\t$this->template_html,\n\t\t\tarray(\n\t\t\t\t'email_message' => do_shortcode( $content ),\n\t\t\t\t'title'         => $this->title,\n\t\t\t\t'image'         => $this->image,\n\t\t\t)\n\t\t);\n\n\t\tremove_filter( 'llms_user_info_shortcode_user_id', array( $this, 'set_shortcode_user' ) );\n\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Set the user ID used by [llms-user] to the user earning the certificate.\n\t *\n\t * @since 5.0.0\n\t * @deprecated 6.0.0 `LLMS_Certificate_User::set_shortcode_user()` is deprecated with no replacement.\n\t *\n\t * @param int $uid WP_User ID of the current user.\n\t * @return int\n\t */\n\tprivate function set_shortcode_user( $uid ) {\n\t\treturn $this->userid;\n\t}\n}\n\nreturn new LLMS_Certificate_User();\n"
  },
  {
    "path": "includes/certificates/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/class-llms-assets.php",
    "content": "<?php\n/**\n * Methods for static asset registration and enqueueing\n *\n * These methods require assets to be \"defined\" in a structured format.\n *\n * A defined asset is then enqueued or registered with the WordPress core using this derivative\n * API that requires only script handles.\n *\n * This API also aims to reduce redundancy in asset registrations by allowing \"partial\" definitions\n * which are filled with default values. For example, every asset in LifterLMS shares the same base\n * plugin url. Using this API we define that URL one time, instead of defining it over and over for\n * each individual asset.\n *\n * @package LifterLMS/Classes\n *\n * @since 4.4.0\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Assets Class\n *\n * @since 4.4.0\n * @since 4.9.0 Added new default values related to script localization.\n * @since 5.5.0 Added new script default for `asset_file`.\n */\nclass LLMS_Assets {\n\n\t/**\n\t * An ID used to identify the originating package (plugin or theme) of the asset handler instance.\n\t *\n\t * @var string\n\t */\n\tprotected $package_id = '';\n\n\t/**\n\t * Determines if SCRIPT_DEBUG is enabled.\n\t *\n\t * @var boolean\n\t */\n\tprotected $debugging_assets = false;\n\n\t/**\n\t * List of default asset definitions.\n\t *\n\t * @since 7.2.0 Use `LLMS_ASSETS_VERSION` for default asset version.\n\t *\n\t * @var array[]\n\t */\n\tprotected $defaults = array(\n\t\t// Base defaults shared by all asset types.\n\t\t'base'   => array(\n\t\t\t'base_file'    => LLMS_PLUGIN_FILE,\n\t\t\t'base_url'     => LLMS_PLUGIN_URL,\n\t\t\t'suffix'       => LLMS_ASSETS_SUFFIX,\n\t\t\t'dependencies' => array(),\n\t\t\t'version'      => LLMS_ASSETS_VERSION,\n\t\t),\n\t\t// Script specific defaults.\n\t\t'script' => array(\n\t\t\t'path'       => 'assets/js',\n\t\t\t'extension'  => '.js',\n\t\t\t'in_footer'  => true,\n\t\t\t'translate'  => false,\n\t\t\t'asset_file' => false,\n\t\t),\n\t\t// Stylesheet specific defaults.\n\t\t'style'  => array(\n\t\t\t'path'      => 'assets/css',\n\t\t\t'extension' => '.css',\n\t\t\t'media'     => 'all',\n\t\t\t'rtl'       => true,\n\t\t),\n\t);\n\n\tprotected $inline = array();\n\n\t/**\n\t * List of defined scripts.\n\t *\n\t * The full list of core script definitions can be found at includes/assets/llms-assets-scripts.php\n\t *\n\t * @var array[]\n\t */\n\tprotected $scripts = array();\n\n\t/**\n\t * List of defined stylesheets.\n\t *\n\t * The full list of core stylesheet definitions can be found at includes/assets/llms-assets-styles.php\n\t *\n\t * @var array[]\n\t */\n\tprotected $styles = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 4.4.0\n\t * @since 4.9.0 Replace defaults instead of merging them.\n\t *\n\t * @param string  $package_id An ID used to identify the originating package (plugin or theme) of the asset handler instance.\n\t * @param array[] $defaults   Array of asset definitions values. Accepts a partial list of values that is merged with the default defaults.\n\t */\n\tpublic function __construct( $package_id, $defaults = array() ) {\n\n\t\t$this->package_id = $package_id;\n\t\t$this->defaults   = array_replace_recursive( $this->defaults, $defaults );\n\n\t\t/**\n\t\t * Filter asset debug mode.\n\t\t *\n\t\t * Asset debug mode is used only to help debug inline assets although the asset suffix is also controlled by the same\n\t\t * WP Core constants.g\n\t\t *\n\t\t * @since 4.4.0\n\t\t *\n\t\t * @param bool   $debugging  Whether or not debugging is enabled. Returns `true` when `SCRIPT_DEBUG` is on, and `false` otherwise.\n\t\t * @param string $package_id An ID used to identify the originating plugin or theme that defined the asset.\n\t\t */\n\t\t$this->debugging_assets = apply_filters( 'llms_assets_debug', ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ), $this->package_id );\n\t}\n\n\t/**\n\t * Define a list of assets by type so they can be enqueued or registered later.\n\t *\n\t * If an asset is already defined, redefining it will overwrite the previous definition.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string  $type   Asset type. Accepts 'scripts' or 'styles'.\n\t * @param array[] $assets List of assets to define. The array key is the asset's handle. Each array value is an array of asset definitions.\n\t * @return array[] Returns the updated list of defined assets.\n\t */\n\tpublic function define( $type, $assets ) {\n\n\t\tif ( ! in_array( $type, array( 'scripts', 'styles' ), true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tforeach ( $assets as $handle => $definition ) {\n\t\t\t$this->{$type}[ $handle ] = $definition;\n\t\t}\n\n\t\treturn $this->$type;\n\t}\n\n\t/**\n\t * Enqueue an inline script or style\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string    $handle   Inline asset ID.\n\t * @param string    $asset    The inline script or CSS rule. This should *not* be wrapped in <script> or <style> tags.\n\t * @param string    $location Output location of the inline asset. Accepts \"style\" (for stylesheets in the headr), \"header\" (for\n\t *                            scripts in the header), or \"footer\" (for scripts in the footer).\n\t * @param int|float $priority Output priority of the inline asset.\n\t * @return float Returns the priority of the enqueued script\n\t */\n\tpublic function enqueue_inline( $handle, $asset, $location, $priority = 10 ) {\n\n\t\t// If script already exists, remove it and re-enqueue.\n\t\tif ( $this->is_inline_enqueued( $handle ) ) {\n\t\t\tunset( $this->inline[ $handle ] );\n\t\t}\n\n\t\t$priority                = $this->get_inline_priority( $priority, $this->get_definitions_inline( $location ) );\n\t\t$this->inline[ $handle ] = compact( 'handle', 'asset', 'location', 'priority' );\n\n\t\treturn $priority;\n\t}\n\n\t/**\n\t * Enqueue (and maybe register) a defined script\n\t *\n\t * If the script has not yet been registered, it will be automatically registered.\n\t *\n\t * The script *must* be defined in one of the following places:\n\t *\n\t *   + The script definition list found at includes/assets/llms-assets-scripts.php\n\t *   + Added to the definitions list via the `LLMS_Assets::define()` method.\n\t *   + Added to the definition list via the `llms_get_script_asset_definitions` filter\n\t *   + Added \"just in time\" via the `llms_get_script_asset` filter.\n\t *\n\t * If the script is *not defined* this function will return `false` because registration\n\t * will fail.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $handle The script's handle.\n\t * @return boolean\n\t */\n\tpublic function enqueue_script( $handle ) {\n\n\t\t// Script was not registered and registration failed.\n\t\tif ( ! wp_script_is( $handle, 'registered' ) && ! $this->register_script( $handle ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\twp_enqueue_script( $handle );\n\n\t\treturn wp_script_is( $handle, 'enqueued' );\n\t}\n\n\t/**\n\t * Enqueue (and maybe register) a defined stylesheet\n\t *\n\t * If the stylesheet has not yet been registered, it will be automatically registered.\n\t *\n\t * The stylesheet *must* be defined in one of the following places:\n\t *\n\t *   + The stylesheet definition list found at includes/assets/llms-assets-styles.php\n\t *   + Added to the definitions list via the `LLMS_Assets::define()` method.\n\t *   + Added to the definition list via the `llms_get_style_asset_definitions` filter\n\t *   + Added \"just in time\" via the `llms_get_style_asset` filter.\n\t *\n\t * If the stylesheet is *not defined* this function will return `false` because registration\n\t * will fail.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $handle The stylesheets's handle.\n\t * @return boolean\n\t */\n\tpublic function enqueue_style( $handle ) {\n\n\t\t// Style was not registered and registration failed.\n\t\tif ( ! wp_style_is( $handle, 'registered' ) && ! $this->register_style( $handle ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\twp_enqueue_style( $handle );\n\n\t\treturn wp_style_is( $handle, 'enqueued' );\n\t}\n\n\t/**\n\t * Retrieve an asset definition by type and handle\n\t *\n\t * Locates the asset by type and handle and merges a potentially impartial asset definition\n\t * with default values from the `get_defaults()` method.\n\t *\n\t * @since 4.4.0\n\t * @since 4.4.1 Replace truthy check with an strict check against `false` to ensure assets defined with an empty array signifying all default values should be used.\n\t * @since 5.5.0 Load dependency and version info from an asset.php file when `$asset_file` is `true`.\n\t *\n\t * @param string $type   The asset type. Accepts either \"script\" or \"style\".\n\t * @param string $handle The asset handle.\n\t * @return array|false {\n\t *     An asset definition array or `false` if an asset definition could not be located.\n\t *\n\t *     @type string   $file_name    The file name of the asset. Excludes the path, suffix, and extension,  eg: 'llms' for 'llms.js'. Defaults to the asset's handle.\n\t *     @type string   $base_url     The base URL used to locate the asset on the server. Defaults to `LLMS_PLUGIN_URL`.\n\t *     @type string   $path         The relative path to the asset within the plugin directory. Defaults to `assets/js` for scripts and `assets/css` for styles.\n\t *     @type string   $extension    The filename extension for the asset. Defaults to `.js` for scripts and `.css` for styles.\n\t *     @type string   $suffix       The file suffix for the asset, for example `.min` for minified files. Defaults to `LLMS_ASSETS_SUFFIX`.\n\t *     @type string[] $dependencies An array of asset handles the asset depends on. These assets do not necessarily need to be assets defined by LifterLMS, for example WP Core scripts, such as `jquery`, can be used.\n\t *     @type string   $version      The asset version. Defaults to `LLMS_ASSETS_VERSION`.\n\t *     @type string   $package_id   An ID used to identify the originating plugin or theme that defined the asset.\n\t *     @type boolean  $in_footer    (For `script` assets only) Whether or not the script should be output in the footer. Defaults to `true`.\n\t *     @type boolean  $translate    (For `script` assets only) Whether or not script translations should be set. Defaults to `false`.\n\t *     @type boolean  $asset_file   (For `script` assets only) Whether or not the script has an asset file (generated via the @wordpress/dependency-extraction-webpack-plugin).\n\t *     @type boolean  $rtl          (For `style` assets only) Whether or not to automatically add RTL style data for the stylesheet. Defaults to `true`.\n\t *     @type boolean  $media        (For `style` assets only) The stylesheet's media type. Defaults to `all`.\n\t * }\n\t */\n\tprotected function get( $type, $handle ) {\n\n\t\t$list  = $this->get_definitions( $type );\n\t\t$asset = isset( $list[ $handle ] ) ? $list[ $handle ] : false;\n\n\t\t/**\n\t\t * Filter static asset data prior to preparing the definition\n\t\t *\n\t\t * The definition is \"prepared\" by merging its data with the default data and preparing its src.\n\t\t *\n\t\t * The dynamic portion of this filter, `{$type}`, refers to the asset type. Either \"script\" or \"style\".\n\t\t *\n\t\t * @since 4.4.0\n\t\t *\n\t\t * @param array|false $asset      Array of asset data or `false` if the asset has not been defined with LifterLMS.\n\t\t * @param string      $handle     The asset handle.\n\t\t * @param string      $package_id An ID used to identify the originating plugin or theme that defined the asset.\n\t\t */\n\t\t$asset = apply_filters( \"llms_get_{$type}_asset_before_prep\", $asset, $handle, $this->package_id );\n\n\t\tif ( false !== $asset && is_array( $asset ) ) {\n\n\t\t\t$asset = wp_parse_args( $asset, $this->get_defaults( $type ) );\n\n\t\t\t$asset['handle']     = $handle;\n\t\t\t$asset['package_id'] = $this->package_id;\n\t\t\t$asset['file_name']  = ! empty( $asset['file_name'] ) ? $asset['file_name'] : $handle;\n\t\t\t$asset['src']        = ! empty( $asset['src'] ) ? $asset['src'] : implode(\n\t\t\t\t'',\n\t\t\t\tarray(\n\t\t\t\t\ttrailingslashit( $asset['base_url'] ),\n\t\t\t\t\ttrailingslashit( $asset['path'] ),\n\t\t\t\t\t$asset['file_name'],\n\t\t\t\t\t$asset['suffix'],\n\t\t\t\t\t$asset['extension'],\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$asset = $this->merge_asset_file( $asset );\n\n\t\t}\n\n\t\t/**\n\t\t * Filter static asset data prior to enqueueing or registering it with the WordPress core\n\t\t *\n\t\t * The dynamic portion of this filter, `{$type}`, refers to the asset type. Either \"script\" or \"style\".\n\t\t *\n\t\t * @since 4.4.0\n\t\t *\n\t\t * @param array|false $asset  Array of asset data or `false` if the asset has not been defined with LifterLMS.\n\t\t * @param string      $handle The asset handle.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$type}_asset\", $asset, $handle );\n\t}\n\n\t/**\n\t * Retrieves an array of definition values based on asset type.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $type The asset type. Accepts either \"script\" or \"style\".\n\t * @return array\n\t */\n\tprotected function get_defaults( $type ) {\n\n\t\t$type_defaults = isset( $this->defaults[ $type ] ) ? $this->defaults[ $type ] : array();\n\t\t$defaults      = array_merge( $this->defaults['base'], $type_defaults );\n\n\t\t/**\n\t\t * Filter the default values used to register or enqueue an asset\n\t\t *\n\t\t * The dynamic portion of this filter, `{$type}`, refers to the asset type. Either \"script\" or \"style\".\n\t\t *\n\t\t * @since 4.4.0\n\t\t *\n\t\t * @param array  $defaults   Default definition values.\n\t\t * @param string $package_id An ID used to identify the originating plugin or theme that defined the asset.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$type}_asset_defaults\", $defaults, $this->package_id );\n\t}\n\n\t/**\n\t * Retrieve the asset definition list for a given asset type.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $type The asset type. Accepts either \"script\" or \"style\".\n\t * @return array[]\n\t */\n\tprotected function get_definitions( $type ) {\n\n\t\tswitch ( $type ) {\n\t\t\tcase 'script':\n\t\t\t\t$list = $this->scripts;\n\t\t\t\tbreak;\n\n\t\t\tcase 'style':\n\t\t\t\t$list = $this->styles;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$list = array();\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the definition list of static assets for the given type\n\t\t *\n\t\t * The dynamic portion of this filter, `{$type}`, refers to the asset type. Either \"script\" or \"style\".\n\t\t *\n\t\t * @since 4.4.0\n\t\t *\n\t\t * @param array[] $list       The definition list.\n\t\t * @param string  $package_id An ID used to identify the originating plugin or theme that defined the asset.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$type}_asset_definitions\", $list, $this->package_id );\n\t}\n\n\n\t/**\n\t * Retrieve a list of inline asset definitions by location.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $location Location of scripts to output. Accepts \"style\", \"header\", or \"footer\".\n\t *                         Inline header styles are output using \"style\".\n\t *                         Inline scripts are output using either \"header\" or \"footer\", output in their respective locations.\n\t * @return array[]\n\t */\n\tprotected function get_definitions_inline( $location ) {\n\n\t\t$assets = array();\n\n\t\tforeach ( $this->inline as $handle => $definition ) {\n\n\t\t\tif ( $location === $definition['location'] ) {\n\t\t\t\t$assets[ $handle ] = $definition;\n\t\t\t}\n\t\t}\n\n\t\t// Sort by priority.\n\t\tuasort(\n\t\t\t$assets,\n\t\t\tfunction ( $a, $b ) {\n\t\t\t\tif ( $a['priority'] === $b['priority'] ) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\treturn $a['priority'] < $b['priority'] ? -1 : 1;\n\t\t\t}\n\t\t);\n\n\t\treturn $assets;\n\t}\n\n\t/**\n\t * Auto-increment inline asset priority to prevent duplicates.\n\t *\n\t * This ensures that inline assets are always enqueued with a unique priority for their requested\n\t * location.\n\t *\n\t * @since 4.4.0\n\t * @since 7.0.0 When increasing priorities, round to the nearest two decimals.\n\t *\n\t * @param float $priority      Requested enqueue priority.\n\t * @param array $inline_assets List of existing inline assets for the requested location.\n\t * @return float\n\t */\n\tprotected function get_inline_priority( $priority, $inline_assets = array() ) {\n\n\t\t$priority = floatval( $priority );\n\n\t\tif ( $inline_assets ) {\n\n\t\t\t$priorities = wp_list_pluck( $inline_assets, 'priority' );\n\t\t\twhile ( in_array( $priority, $priorities, true ) ) {\n\t\t\t\t$priority = round( $priority + 0.01, 2 );\n\t\t\t}\n\t\t}\n\n\t\treturn $priority;\n\t}\n\n\t/**\n\t * Determines if an inline asset is enqueued\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $handle Inline asset handle.\n\t * @return boolean\n\t */\n\tpublic function is_inline_enqueued( $handle ) {\n\t\treturn in_array( $handle, array_keys( $this->inline ), true );\n\t}\n\n\t/**\n\t * Retrieve dependency and version info from a script asset's asset.php file\n\t *\n\t * Loads the asset.php file (generated via the @wordpress/dependency-extraction-webpack-plugin) and merges it\n\t * into an existing asset array.\n\t *\n\t * @since 5.5.0\n\t *\n\t * @param array $asset An asset definition array.\n\t * @return array\n\t */\n\tprotected function merge_asset_file( $asset ) {\n\n\t\tif ( empty( $asset['asset_file'] ) ) {\n\t\t\treturn $asset;\n\t\t}\n\n\t\t$asset_file_path = plugin_dir_path( $asset['base_file'] ) . trailingslashit( $asset['path'] ) . $asset['file_name'] . '.asset.php';\n\t\tif ( file_exists( $asset_file_path ) ) {\n\t\t\t$info                  = include $asset_file_path;\n\t\t\t$asset['dependencies'] = array_merge( $asset['dependencies'], $info['dependencies'] );\n\t\t\t$asset['version']      = $info['version'];\n\t\t}\n\n\t\treturn $asset;\n\t}\n\n\t/**\n\t * Output inline scripts\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $location Location of scripts to output. Accepts \"style\", \"header\", or \"footer\".\n\t *                         Inline header styles are output using \"style\".\n\t *                         Inline scripts are output using either \"header\" or \"footer\", output in their respective locations.\n\t * @return void\n\t */\n\tpublic function output_inline( $location ) {\n\n\t\t$defs = self::get_definitions_inline( $location );\n\n\t\tif ( $defs ) {\n\n\t\t\t$assets = array();\n\t\t\tforeach ( $defs as $def ) {\n\t\t\t\t$assets[] = $this->prepare_inline_asset_for_output( $def, $location );\n\t\t\t}\n\n\t\t\t$open  = 'style' === $location ? '<style id=\"llms-inline-styles\" type=\"text/css\">' : sprintf( '<script id=\"llms-inline-%s-scripts\" type=\"text/javascript\">', $location );\n\t\t\t$close = 'style' === $location ? '</style>' : '</script>';\n\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\techo $open . implode( '', $assets ) . $close;\n\n\t\t}\n\t}\n\n\t/**\n\t * Prepares an inline asset definition for being output.\n\t *\n\t * When `$this->debugging_assets` is `true` this will add line breaks between each inline asset\n\t * and output the asset's handle as a comment before the asset's script/style so that the\n\t * inline assets can be quickly located and reviewed in the generated source of the page.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param array  $asset    The inline asset definition array.\n\t * @param string $location The location of the asset. Accepts \"header\", \"footer\", or \"style\".\n\t * @return string\n\t */\n\tprotected function prepare_inline_asset_for_output( $asset, $location ) {\n\n\t\t$before = '';\n\t\t$after  = '';\n\n\t\t// Output inline asset handles and add line breaks when debugging.\n\t\tif ( $this->debugging_assets ) {\n\n\t\t\t// Setup the comment template.\n\t\t\t$before = 'style' === $location ? '/* %s. */' : '// %s.';\n\n\t\t\t// Add line breaks.\n\t\t\t$before .= \"\\n\";\n\t\t\t$after   = \"\\n\";\n\n\t\t}\n\n\t\treturn sprintf( $before, $asset['handle'] ) . $asset['asset'] . $after;\n\t}\n\n\t/**\n\t * Registers a defined script with WordPress\n\t *\n\t * The script *must* be defined in one of the following places:\n\t *\n\t *   + The script definition list found at includes/assets/llms-assets-scripts.php\n\t *   + Added to the definition list via the `llms_get_script_asset_definitions` filter\n\t *   + Added \"just in time\" via the `llms_get_script_asset` filter.\n\t *\n\t * If the script is *not defined* this function will return `false`.\n\t *\n\t * @since 4.4.0\n\t * @since 4.9.0 Automatically set script translations when `translate=true`.\n\t * @since 5.5.0 Automatically register all of the asset's dependencies.\n\t *\n\t * @param string $handle The script's handle.\n\t * @return boolean\n\t */\n\tpublic function register_script( $handle ) {\n\n\t\t$script = $this->get( 'script', $handle );\n\t\tif ( $script ) {\n\n\t\t\tarray_map( array( $this, 'register_script' ), $script['dependencies'] );\n\n\t\t\t$reg = wp_register_script( $handle, $script['src'], $script['dependencies'], $script['version'], $script['in_footer'] );\n\t\t\tif ( $reg && $script['translate'] ) {\n\t\t\t\t$this->set_script_translations( $script );\n\t\t\t}\n\n\t\t\treturn $reg;\n\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Register a defined stylesheet\n\t *\n\t * If the stylesheet has not yet been registered, it will be automatically registered.\n\t *\n\t * The stylesheet *must* be defined in one of the following places:\n\t *\n\t *   + The stylesheet definition list found at includes/assets/llms-assets-styles.php\n\t *   + Added to the definition list via the `llms_get_style_asset_definitions` filter\n\t *   + Added \"just in time\" via the `llms_get_style_asset` filter.\n\t *\n\t * If the stylesheet is *not defined* this function will return `false`.\n\t *\n\t * This method will also automatically add RTL style data unless explicitly told not to do so.\n\t *\n\t * The RTL stylesheet should have the same name (and suffix) with `-rtl` included prior to the suffix, for example\n\t * `llms.css` (or `llms.min.css`) would add the RTL stylesheet `llms-rtl.css` (or `llms-rtl.min.css`).\n\t *\n\t * @since 4.4.0\n\t * @since 5.5.0 Automatically register all of the asset's dependencies.\n\t *\n\t * @param string $handle The stylesheets's handle.\n\t * @return boolean\n\t */\n\tpublic function register_style( $handle ) {\n\n\t\t$style = $this->get( 'style', $handle );\n\t\tif ( $style ) {\n\n\t\t\tarray_map( array( $this, 'register_style' ), $style['dependencies'] );\n\n\t\t\t$reg = wp_register_style( $handle, $style['src'], $style['dependencies'], $style['version'], $style['media'] );\n\n\t\t\tif ( $reg && $style['rtl'] ) {\n\t\t\t\twp_style_add_data( $handle, 'rtl', 'replace' );\n\t\t\t\twp_style_add_data( $handle, 'suffix', $style['suffix'] );\n\t\t\t}\n\n\t\t\treturn $reg;\n\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Load JSON format localization files for a registered script\n\t *\n\t * This method mimics the behavior of PO/MO pot files loaded for PHP localization.\n\t *\n\t * Language files can be found in the following locations (The first loaded file takes priority):\n\t *\n\t *   1. wp-content/languages/{$textdomain}/{$textdomain}-{$locale}-{$file_md5_hash}.json\n\t *\n\t *      This is recommended \"safe\" location where custom language files can be stored. A file\n\t *      stored in this directory will never be automatically overwritten.\n\t *\n\t *   2. wp-content/languages/plugins/{$textdomain}-{$locale}-{$file_md5_hash}.json\n\t *\n\t *      This is the default directory where WordPress will download language files from the\n\t *      WordPress GlotPress server during updates. If you store a custom language file in this\n\t *      directory it will be overwritten during updates.\n\t *\n\t *   3. wp-content/plugins/{$textdomain}/languages/{$textdomain}-{$locale}-{$file_md5_hash}.json\n\t *\n\t *      This is the the LifterLMS plugin directory. A language file stored in this directory will\n\t *      be removed from the server during a LifterLMS plugin update.\n\t *\n\t * @since 4.9.0\n\t *\n\t * @param array $script An asset definition array from the return of `LLMS_Assets::get()`.\n\t * @return void\n\t */\n\tprotected function set_script_translations( $script ) {\n\n\t\t$plugin_data = get_plugin_data( $script['base_file'], false, false );\n\t\t$domain      = $plugin_data['TextDomain'];\n\n\t\t// Possible directories where the language files may be found.\n\t\t$dirs = array(\n\t\t\tllms_l10n_get_safe_directory(),\n\t\t\tWP_LANG_DIR . '/plugins', // Default language directory.\n\t\t\ttrailingslashit( plugin_dir_path( $script['base_file'] ) ) . untrailingslashit( ltrim( $plugin_data['DomainPath'], '/' ) ), // Language directory within the plugin.\n\t\t);\n\n\t\tforeach ( $dirs as $dir ) {\n\t\t\twp_set_script_translations( $script['handle'], $domain, $dir );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/class-llms-awards-query.php",
    "content": "<?php\n/**\n * LLMS_Awards_Query class file.\n *\n * @package LifterLMS/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Query awarded achievements and engagements.\n *\n * @since 6.0.0\n *\n * Valid query arguments\n *\n * {@see LLMS_Abstract_Query} and {@see LLMS_Abstract_Posts_Query} for inherited query arguments.\n *\n * @param int|int[] $users                  Include awards for the specified WP_User(s) by WP_User ID.\n * @param int|int[] $users__exclude         Exclude awards for the specified WP_User(s) by WP_User ID.\n * @param int|int[] $related_posts          Include awards related to the specified WP_Post(s) by WP_Post ID.\n * @param int|int[] $related_posts__exclude Exclude awards related to the specified WP_Post(s) by WP_Post ID.\n * @param int|int[] $engagements            Include awards created from the specified `llms_engagement` post(s) by WP_Post ID.\n * @param int|int[] $engagements__exclude   Exclude awards created from the specified `llms_engagement` post(s) by WP_Post ID.\n * @param string    $type                   Award type, accepts \"any\", \"achievement\" or \"certificate\".\n * @param int|int[] $templates              Include awards created from the specified `llms_achievement` or `llms_certificate` template post(s) by WP_Post ID.\n * @param int|int[] $templates__exclude     Exclude awards created from the specified `llms_achievement` or `llms_certificate` template  post(s) by WP_Post ID.\n * @param boolean   $manual_only            Include only awards created manually. If specified the `$related_posts`, `$related_posts__exclude`, `$engagements`, `$engagements__exclude`, `$templates`, and `$templates__exclude` arguments will be ignored.\n */\nclass LLMS_Awards_Query extends LLMS_Abstract_Posts_Query {\n\n\t/**\n\t * Identify the extending query.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'awards';\n\n\t/**\n\t * Specify the post types allowed to be queried by this class.\n\t *\n\t * @var string[]\n\t */\n\tprotected $allowed_post_types = array(\n\t\t'llms_my_achievement',\n\t\t'llms_my_certificate',\n\t);\n\n\t/**\n\t * Defines fields that can be sorted on via ORDER BY.\n\t *\n\t * @var string[]\n\t */\n\tprotected $allowed_sort_fields = array(\n\t\t'date',\n\t\t'ID',\n\t\t'user',\n\t);\n\n\t/**\n\t * Retrieve query argument default values.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function default_arguments() {\n\n\t\treturn wp_parse_args(\n\t\t\tarray(\n\t\t\t\t'sort'                   => array(\n\t\t\t\t\t'date' => 'DESC',\n\t\t\t\t\t'ID'   => 'DESC',\n\t\t\t\t),\n\t\t\t\t'users'                  => array(),\n\t\t\t\t'users__exclude'         => array(),\n\t\t\t\t'related_posts'          => array(),\n\t\t\t\t'related_posts__exclude' => array(),\n\t\t\t\t'engagements'            => array(),\n\t\t\t\t'engagements__exclude'   => array(),\n\t\t\t\t'templates'              => array(),\n\t\t\t\t'templates__exclude'     => array(),\n\t\t\t\t'type'                   => 'any',\n\t\t\t\t'manual_only'            => false,\n\t\t\t),\n\t\t\tparent::default_arguments()\n\t\t);\n\n\t}\n\n\t/**\n\t * Map input arguments to WP_Query arguments.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_arg_map() {\n\n\t\t$map = parent::get_arg_map();\n\n\t\treturn array_merge(\n\t\t\t$map,\n\t\t\tarray(\n\t\t\t\t'users'              => 'author__in',\n\t\t\t\t'users__exclude'     => 'author__not_in',\n\t\t\t\t'templates'          => 'post_parent__in',\n\t\t\t\t'templates__exclude' => 'post_parent__not_in',\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve an array of award objects for the given result set returned by the query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array Array of LLMS_User_Achievement and/or LLMS_User_Certificate objects.\n\t */\n\tpublic function get_awards() {\n\n\t\t$awards = array_filter( array_map( array( $this, 'get_object' ), $this->get_results() ) );\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $awards;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query results array.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param array             $awards Array of LLMS_User_Achievement and/or LLMS_User_Certificate objects.\n\t\t * @param LLMS_Awards_Query $query  Instance of the query class.\n\t\t */\n\t\treturn apply_filters( 'llms_awards_query_get_awards', $awards, $this );\n\n\t}\n\n\t/**\n\t * Retrieve the object for a given result.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int|WP_Post $post Post object or ID.\n\t * @return LLMS_User_Achievement|LLMS_User_Certificate|null Returns the award object or `null` for unexpected post types.\n\t */\n\tprotected function get_object( $post ) {\n\n\t\t$post_type = get_post_type( $post );\n\t\tif ( 'llms_my_achievement' === $post_type ) {\n\t\t\treturn new LLMS_User_Achievement( $post );\n\t\t} elseif ( 'llms_my_certificate' === $post_type ) {\n\t\t\treturn llms_get_certificate( $post );\n\t\t}\n\n\t\treturn null;\n\n\t}\n\n\t/**\n\t * Parse arguments needed for the query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function parse_args() {\n\n\t\t$int_arrays = array(\n\t\t\t'users',\n\t\t\t'users__exclude',\n\t\t\t'related_posts',\n\t\t\t'related_posts__exclude',\n\t\t\t'engagements',\n\t\t\t'engagements__exclude',\n\t\t\t'templates',\n\t\t\t'templates__exclude',\n\t\t);\n\t\tforeach ( $int_arrays as $key ) {\n\t\t\t$this->arguments[ $key ] = $this->sanitize_id_array( $this->arguments[ $key ] );\n\t\t}\n\n\t\t$this->arguments['manual_only'] = llms_parse_bool( $this->arguments['manual_only'] );\n\n\t\t$this->arguments['page']     = absint( $this->arguments['page'] );\n\t\t$this->arguments['per_page'] = intval( $this->arguments['per_page'] );\n\n\t\t$this->arguments['no_found_rows'] = llms_parse_bool( $this->arguments['no_found_rows'] );\n\n\t}\n\n\t/**\n\t * Retrieve the post type(s) based on the `$type` input.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string[]\n\t */\n\tprotected function post_types() {\n\n\t\t$type  = $this->get( 'type' );\n\t\t$types = array();\n\t\tif ( 'any' === $type ) {\n\t\t\t$types = $this->allowed_post_types;\n\t\t} elseif ( 'achievement' === $type ) {\n\t\t\t$types = array( 'llms_my_achievement' );\n\t\t} elseif ( 'certificate' === $type ) {\n\t\t\t$types = array( 'llms_my_certificate' );\n\t\t}\n\n\t\treturn $types;\n\n\t}\n\n\t/**\n\t * Prepares the `meta_query` ultimately passed to the WP_Query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array An array of meta query arrays.\n\t */\n\tprivate function prepare_meta_query() {\n\n\t\t// If a query for manual awards we skip all other relationships and return early.\n\t\tif ( $this->get( 'manual_only' ) ) {\n\n\t\t\treturn array(\n\t\t\t\t'relation' => 'OR',\n\t\t\t\t// Meta doesn't exist.\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_engagement',\n\t\t\t\t\t'compare' => 'NOT EXISTS',\n\t\t\t\t),\n\t\t\t\t// Or it's \"empty\".\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_engagement',\n\t\t\t\t\t'value'   => array( '', '0', 0 ),\n\t\t\t\t\t'compare' => 'IN',\n\t\t\t\t),\n\t\t\t);\n\n\t\t}\n\n\t\treturn $this->prepare_meta_query_for_relationships();\n\n\t}\n\n\t/**\n\t * Retrieve meta query parts for related posts, engagements, and templates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array An array of meta query arrays.\n\t */\n\tprivate function prepare_meta_query_for_relationships() {\n\n\t\t$meta_query = array();\n\n\t\t$relations = array(\n\t\t\t'related_posts' => '_llms_related',\n\t\t\t'engagements'   => '_llms_engagement',\n\t\t);\n\t\tforeach ( $relations as $arg => $meta_key ) {\n\n\t\t\t// Include.\n\t\t\tif ( ! empty( $this->get( $arg ) ) ) {\n\t\t\t\t$meta_query[] = array(\n\t\t\t\t\t'key'     => $meta_key,\n\t\t\t\t\t'value'   => $this->sanitize_id_array( $this->get( $arg ) ),\n\t\t\t\t\t'compare' => 'IN',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Exclude.\n\t\t\t$exclude_arg = $arg . '__exclude';\n\t\t\tif ( ! empty( $this->get( $exclude_arg ) ) ) {\n\n\t\t\t\t$meta_query[] = array(\n\t\t\t\t\t'relation' => 'OR',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => $meta_key,\n\t\t\t\t\t\t'value'   => $this->sanitize_id_array( $this->get( $exclude_arg ) ),\n\t\t\t\t\t\t'compare' => 'NOT IN',\n\t\t\t\t\t),\n\t\t\t\t\t// Ensure posts that don't have the metadata set will be returned.\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => $meta_key,\n\t\t\t\t\t\t'compare' => 'NOT EXISTS',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn $meta_query;\n\n\t}\n\n\t/**\n\t * Prepare the WP_Query arguments for the awards query.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array Array of arguments suitable to pass to a WP_Query.\n\t */\n\tprotected function prepare_query() {\n\n\t\t// Remove any extra arguments not found in the map.\n\t\t$args = array_intersect_key(\n\t\t\tparent::prepare_query(),\n\t\t\tarray_flip( $this->get_arg_map() )\n\t\t);\n\n\t\t// Add post type(s).\n\t\t$args['post_type'] = $this->post_types();\n\n\t\t// Add meta query.\n\t\t$args['meta_query'] = $this->prepare_meta_query();\n\n\t\t// Remove empty arrays.\n\t\treturn array_filter(\n\t\t\t$args,\n\t\t\tfunction( $val ) {\n\t\t\t\treturn ! is_array( $val ) || ! empty( $val );\n\t\t\t}\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-beaver-builder-migrate.php",
    "content": "<?php\n/**\n * Handle post migration to the Beaver Builder modules.\n *\n * @package LifterLMS/Classes\n *\n * @since 8.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nclass LLMS_Beaver_Builder_Migrate {\n\n\tpublic function __construct() {\n\n\t\tadd_action( 'wp', array( $this, 'maybe_migrate_post' ) );\n\t\tadd_action( 'wp', array( $this, 'remove_template_hooks' ) );\n\t\tadd_action( 'fl_builder_after_save_layout', array( $this, 'maybe_update_migration_status' ), 10, 2 );\n\t}\n\n\t/**\n\t * Migrate posts created prior to the elementor updates to have default LifterLMS widgets.\n\t *\n\t * @since 8.0.0\n\t *\n\t * @return  void\n\t */\n\tpublic function maybe_migrate_post() {\n\t\tglobal $post;\n\n\t\tif ( ! class_exists( 'FLBuilderModel' ) || ! method_exists( 'FLBuilderModel', 'is_builder_active' ) || ! FLBuilderModel::is_builder_active() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! $post ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! $this->is_migratable_post_type( $post->ID ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! $this->should_migrate_post( $post->ID ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->add_template_to_post();\n\t}\n\n\tprotected function is_migratable_post_type( $post_id ) {\n\t\treturn in_array( get_post_type( $post_id ), array( 'course', 'lesson', 'llms_membership' ) );\n\t}\n\n\tpublic function add_template_to_post() {\n\t\t// Get the existing layout data.\n\t\t$data = FLBuilderModel::get_layout_data();\n\n\t\tif ( ! $data ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$path = LLMS_PLUGIN_DIR . 'includes/beaver-builder/templates/default-' . get_post_type() . '-template.dat';\n\n\t\tif ( ! file_exists( $path ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$templates = maybe_unserialize( file_get_contents( $path ) );\n\t\t$template  = $templates['layout'][0];\n\n\t\t// TODO : Check if the data already has the template inserted.\n\n\t\t// Get the next top-level position.\n\t\t$position = FLBuilderModel::next_node_position( 'row' );\n\n\t\t// Adjust the position of template nodes.\n\t\tforeach ( $template->nodes as $node_id => $node ) {\n\t\t\tif ( ! $node->parent ) {\n\t\t\t\t$template->nodes[ $node_id ]->position += $position;\n\t\t\t}\n\t\t}\n\t\t// Merge the template nodes with the existing nodes.\n\t\t$data = array_merge( $data, $template->nodes );\n\n\t\tFLBuilderModel::update_layout_data( $data );\n\t}\n\n\t/**\n\t * Removes core template action hooks from posts which have been migrated to beaver builder widgets.\n\t *\n\t * @since 8.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function remove_template_hooks() {\n\t\tif ( ! function_exists( 'llms_is_beaver_builder_post' ) ||\n\t\t\t! llms_is_beaver_builder_post() ||\n\t\t\t( get_the_ID() && ! llms_parse_bool( get_post_meta( get_the_ID(), '_llms_beaver_builder_migrated', true ) ) ) ) {\n\n\t\t\tif ( ! $this->is_migratable_post_type( get_the_ID() ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Remove the bottom actions if the builder is currently active to avoid confusion with uneditable pieces.\n\t\t\tif ( ! class_exists( 'FLBuilderModel' ) || ! method_exists( 'FLBuilderModel', 'is_builder_active' ) || ! FLBuilderModel::is_builder_active() ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tswitch ( get_post_type() ) {\n\t\t\tcase 'course':\n\t\t\t\t$this->remove_course_template_hooks();\n\t\t\t\tbreak;\n\t\t\tcase 'lesson':\n\t\t\t\t$this->remove_lesson_template_hooks();\n\t\t\t\tbreak;\n\t\t\tcase 'llms_membership':\n\t\t\t\t$this->remove_membership_template_hooks();\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * Remove membership template hooks.\n\t *\n\t * @since 8.0.0\n\t */\n\tpublic function remove_membership_template_hooks() {\n\t\tremove_action( 'lifterlms_single_membership_after_summary', 'lifterlms_template_pricing_table', 10 );\n\t}\n\n\t/**\n\t * Remove lesson template hooks.\n\t *\n\t * @since 8.0.0\n\t */\n\tpublic function remove_lesson_template_hooks() {\n\t\tremove_action( 'lifterlms_single_lesson_after_summary', 'lifterlms_template_complete_lesson_link', 10 );\n\t}\n\n\t/**\n\t * Remove course template hooks.\n\t *\n\t * @since 8.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function remove_course_template_hooks() {\n\t\t// TODO: Refactor this so it's not duplicated between Elementor and Beaver Builder.\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_start', 5 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_length', 10 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_difficulty', 20 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tracks', 25 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_categories', 30 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tags', 35 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_end', 50 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_progress', 60 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_syllabus', 90 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_course_author', 40 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_pricing_table', 60 );\n\t}\n\n\t/**\n\t * Determine if a post should be migrated.\n\t *\n\t * @since 8.0.0\n\t *\n\t * @param int $post_id WP_Post ID.\n\t * @return bool\n\t */\n\tpublic function should_migrate_post( $post_id ) {\n\n\t\t$ret = ! llms_parse_bool( get_post_meta( $post_id, '_llms_beaver_builder_migrated', true ) );\n\n\t\t/**\n\t\t * Filters whether or not a post should be migrated\n\t\t *\n\t\t * @since 8.0.0\n\t\t *\n\t\t * @param bool $migrate Whether or not a post should be migrated.\n\t\t * @param int  $post_id WP_Post ID.\n\t\t */\n\t\treturn apply_filters( 'llms_beaver_builder_should_migrate_post', $ret, $post_id );\n\t}\n\n\t/**\n\t * Update post meta data to signal status of the editor migration.\n\t *\n\t * @since 8.0.0\n\t *\n\t * @param int  $post_id WP_Post ID.\n\t * @param bool $publish Whether or not the post is being published.\n\t * @return void\n\t */\n\tpublic function maybe_update_migration_status( $post_id, $publish ) {\n\t\tif ( ! $publish ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! $this->is_migratable_post_type( $post_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( llms_parse_bool( get_post_meta( $post_id, '_llms_beaver_builder_migrated', true ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tupdate_post_meta( $post_id, '_llms_beaver_builder_migrated', 'yes' );\n\t}\n}\n\nreturn new LLMS_Beaver_Builder_Migrate();\n"
  },
  {
    "path": "includes/class-llms-beaver-builder.php",
    "content": "<?php\n/**\n * BeaverBuilder Integration\n *\n * Lets you do all them sweet BeaverBuilder things to Courses, Lessons, and Memberships.\n *\n * @package LifterLMS/Classes\n *\n * @since 8.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\ndefine( 'LLMS_BB_MODULES_DIR', plugin_dir_path( __FILE__ ) . 'beaver-builder/modules/' );\ndefine( 'LLMS_BB_MODULES_URL', plugins_url( '/', __FILE__ ) . 'beaver-builder/modules/' );\n\nclass LLMS_Beaver_Builder {\n\n\tuse LLMS_Trait_Singleton;\n\n\tpublic function __construct() {\n\t\t$this->init();\n\t}\n\n\tpublic function is_available() {\n\t\treturn class_exists( 'FLBuilder' );\n\t}\n\n\tprotected function init() {\n\n\t\t// Break early if the LifterLMS Labs is installed and enabled.\n\t\tif ( class_exists( 'LifterLMS_Labs' ) && llms_parse_bool( get_option( 'llms_lab_beaver-builder_enabled' ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! class_exists( 'FLBuilder' ) || ! class_exists( 'FLBuilderModel' ) || ! class_exists( 'FLBUilderModule' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Prevent uneditable llms post types from being enabled for page building.\n\t\tadd_filter( 'fl_builder_admin_settings_post_types', array( $this, 'remove_uneditable_post_types' ) );\n\n\t\t// Add migrateable post types to the builder by default.\n\t\tadd_filter( 'fl_builder_post_types', array( $this, 'enable_post_types_by_default' ) );\n\n\t\tadd_action( 'wp', array( $this, 'load_modules' ), 1 );\n\t\tadd_action( 'init', array( $this, 'load_templates' ) );\n\n\t\tadd_filter( 'fl_builder_register_module', array( $this, 'register_module' ), 10, 2 );\n\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'mod_page_restrictions' ), 999, 2 );\n\n\t\tadd_filter( 'fl_builder_register_settings_form', array( $this, 'add_visibility_settings' ), 999, 2 );\n\n\t\tadd_filter( 'fl_builder_is_node_visible', array( $this, 'is_node_visible' ), 10, 2 );\n\n\t\t// Hide editors when builder is enabled for a post.\n\t\tadd_filter( 'llms_metabox_fields_lifterlms_course_options', array( $this, 'mod_metabox_fields' ) );\n\t\tadd_filter( 'llms_metabox_fields_lifterlms_membership', array( $this, 'mod_metabox_fields' ) );\n\n\t\tadd_filter( 'fl_builder_upgrade_url', array( $this, 'upgrade_url' ) );\n\n\t\t// LifterLMS Private Areas.\n\t\tadd_action( 'llms_pa_before_do_area_content', array( $this, 'llms_pa_before_content' ) );\n\t\tadd_action( 'llms_pa_after_do_area_content', array( $this, 'llms_pa_after_content' ) );\n\t}\n\n\tpublic function enable_post_types_by_default( $types ) {\n\t\t$types[] = 'course';\n\t\t$types[] = 'lesson';\n\t\t$types[] = 'llms_membership';\n\n\t\treturn $types;\n\t}\n\n\t/**\n\t * Add LLMS post types to the enabled builder post types.\n\t *\n\t * Stub function called during install.\n\t *\n\t * @since 8.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function install() {\n\t\tif ( ! $this->is_available() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$existing = get_option( '_fl_builder_post_types', array( 'page' ) );\n\t\t$types    = array_unique( array_merge( $existing, array( 'course', 'lesson', 'llms_membership' ) ) );\n\t\tupdate_option( '_fl_builder_post_types', $types );\n\t}\n\n\t/**\n\t * This function should return array of settings fields.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @return array\n\t */\n\tprotected function settings() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Add custom visibility settings for enrollments to the BB \"Visibility\" section.\n\t *\n\t * @since 1.5.0\n\t * @since 1.5.3 Fixed localization textdomain.\n\t * @since 1.7.0 Escaped strings.\n\t *\n\t * @param array  $form Settings form array.\n\t * @param string $id   ID of the row/module/col/etc.\n\t * @return array\n\t */\n\tpublic function add_visibility_settings( $form, $id ) {\n\n\t\t$options = array(\n\t\t\t'llms_enrolled'     => esc_html__( 'Enrolled Students', 'lifterlms' ),\n\t\t\t'llms_not_enrolled' => esc_html__( 'Non-Enrolled Students and Visitors', 'lifterlms' ),\n\t\t);\n\n\t\t$toggle = array(\n\t\t\t'llms_enrolled'     => array(\n\t\t\t\t'fields' => array( 'llms_enrollment_type' ),\n\t\t\t),\n\t\t\t'llms_not_enrolled' => array(\n\t\t\t\t'fields' => array( 'llms_enrollment_type' ),\n\t\t\t),\n\t\t);\n\n\t\t$fields = array(\n\t\t\t'llms_enrollment_type'  => array(\n\t\t\t\t'type'    => 'select',\n\t\t\t\t'label'   => esc_html__( 'In', 'lifterlms' ),\n\t\t\t\t'options' => array(\n\t\t\t\t\t''         => esc_html__( 'Current Course or Membership', 'lifterlms' ),\n\t\t\t\t\t'any'      => esc_html__( 'Any Course(s) or Membership(s)', 'lifterlms' ),\n\t\t\t\t\t'specific' => esc_html__( 'Specific Course(s) and/or Membership(s)', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'toggle'  => array(\n\t\t\t\t\t'specific' => array(\n\t\t\t\t\t\t'fields' => array( 'llms_enrollment_match', 'llms_course_ids', 'llms_membership_ids' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'help'    => esc_html__( 'Select how to check the enrollment status of the current student.', 'lifterlms' ),\n\t\t\t\t'preview' => array(\n\t\t\t\t\t'type' => 'none',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'llms_enrollment_match' => array(\n\t\t\t\t'type'    => 'select',\n\t\t\t\t'label'   => esc_html__( 'Match', 'lifterlms' ),\n\t\t\t\t'options' => array(\n\t\t\t\t\t''    => esc_html__( 'Any of the following', 'lifterlms' ),\n\t\t\t\t\t'all' => esc_html__( 'All of the following', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'help'    => esc_html__( 'Select how to check the enrollment status of the current student.', 'lifterlms' ),\n\t\t\t\t'preview' => array(\n\t\t\t\t\t'type' => 'none',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'llms_course_ids'       => array(\n\t\t\t\t'type'    => 'suggest',\n\t\t\t\t'action'  => 'fl_as_posts',\n\t\t\t\t'data'    => 'course',\n\t\t\t\t'label'   => esc_html__( 'Courses', 'lifterlms' ),\n\t\t\t\t'help'    => esc_html__( 'Choose which course(s) the student must be enrolled (or not enrolled) in to view this element.', 'lifterlms' ),\n\t\t\t\t'preview' => array(\n\t\t\t\t\t'type' => 'none',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'llms_membership_ids'   => array(\n\t\t\t\t'type'    => 'suggest',\n\t\t\t\t'action'  => 'fl_as_posts',\n\t\t\t\t'data'    => 'llms_membership',\n\t\t\t\t'label'   => esc_html__( 'Memberships', 'lifterlms' ),\n\t\t\t\t'help'    => esc_html__( 'Choose which membership(s) the student must be enrolled (or not enrolled) in to view this element.', 'lifterlms' ),\n\t\t\t\t'preview' => array(\n\t\t\t\t\t'type' => 'none',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t// Rows.\n\t\tif (\n\t\t\tisset( $form['tabs'] ) &&\n\t\t\tisset( $form['tabs']['advanced'] ) &&\n\t\t\tisset( $form['tabs']['advanced']['sections'] ) &&\n\t\t\tisset( $form['tabs']['advanced']['sections']['visibility'] )\n\t\t) {\n\n\t\t\t$form['tabs']['advanced']['sections']['visibility']['fields']['visibility_display']['options'] = array_merge( $form['tabs']['advanced']['sections']['visibility']['fields']['visibility_display']['options'], $options );\n\t\t\t$form['tabs']['advanced']['sections']['visibility']['fields']['visibility_display']['toggle']  = array_merge( $form['tabs']['advanced']['sections']['visibility']['fields']['visibility_display']['toggle'], $toggle );\n\t\t\t$form['tabs']['advanced']['sections']['visibility']['fields']                                  = array_merge( $form['tabs']['advanced']['sections']['visibility']['fields'], $fields );\n\n\t\t\t// Modules.\n\t\t} elseif (\n\t\t\tisset( $form['sections'] ) &&\n\t\t\tisset( $form['sections']['visibility'] )\n\t\t) {\n\n\t\t\t$form['sections']['visibility']['fields']['visibility_display']['options'] = array_merge( $form['sections']['visibility']['fields']['visibility_display']['options'], $options );\n\t\t\t$form['sections']['visibility']['fields']['visibility_display']['toggle']  = array_merge( $form['sections']['visibility']['fields']['visibility_display']['toggle'], $toggle );\n\t\t\t$form['sections']['visibility']['fields']                                  = array_merge( $form['sections']['visibility']['fields'], $fields );\n\n\t\t}\n\n\t\treturn $form;\n\t}\n\n\t/**\n\t * Create a single array of course & membership IDs from a BB node settings object.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @param obj $settings BB Node Settings.\n\t * @return array\n\t */\n\tprivate function get_related_posts_from_settings( $settings ) {\n\n\t\t$post_ids = array();\n\n\t\tforeach ( array( 'llms_course_ids', 'llms_membership_ids' ) as $key ) {\n\n\t\t\tif ( ! empty( $settings->$key ) ) {\n\n\t\t\t\t$ids      = explode( ',', $settings->$key );\n\t\t\t\t$post_ids = array_merge( $post_ids, $ids );\n\n\t\t\t}\n\t\t}\n\n\t\treturn $post_ids;\n\t}\n\n\t/**\n\t * Determine if a node is visible based on llms enrollments status visibility settings.\n\t *\n\t * @since 1.3.0\n\t * @since 1.5.0 Unknown.\n\t * @since 1.5.3 Fixed visibility conditional logic for `'specific'` enrollment type.\n\t * @since 1.7.0 Use `in_array()` strict comparisons.\n\t *\n\t * @param bool $visible Default visibility.\n\t * @param obj  $node    BB node object.\n\t * @return boolean\n\t */\n\tpublic function is_node_visible( $visible, $node ) {\n\n\t\tif ( isset( $node->settings ) && isset( $node->settings->visibility_display ) && false !== strpos( $node->settings->visibility_display, 'llms_' ) ) {\n\n\t\t\t$status = $node->settings->visibility_display;\n\n\t\t\t$uid  = get_current_user_id();\n\t\t\t$type = ! empty( $node->settings->llms_enrollment_type ) ? $node->settings->llms_enrollment_type : null;\n\n\t\t\tllms_log( $type );\n\n\t\t\tif ( ! $type || 'any' === $type ) {\n\n\t\t\t\t// No type means current course/membership.\n\t\t\t\tif ( ! $type ) {\n\n\t\t\t\t\t$current_id = get_the_ID();\n\t\t\t\t\t// Cascade up for lessons & quizzes.\n\t\t\t\t\tif ( in_array( get_post_type( $current_id ), array( 'lesson', 'llms_quiz' ), true ) ) {\n\t\t\t\t\t\t$course     = llms_get_post_parent_course( $current_id );\n\t\t\t\t\t\t$current_id = $course->get( 'id' );\n\t\t\t\t\t}\n\n\t\t\t\t\t// If the current id isn't a course or membership don't proceed.\n\t\t\t\t\tif ( ! in_array( get_post_type( $current_id ), array( 'course', 'llms_membership' ), true ) ) {\n\t\t\t\t\t\treturn $visibility;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get the enrollment status.\n\t\t\t\t\t$enrollment_status = llms_is_user_enrolled( $uid, $current_id );\n\t\t\t\t} elseif ( 'any' === $type ) { // Check if they're enrolled/not enrolled in anything.\n\t\t\t\t\t$enrollment_status = $this->is_student_enrolled_in_one_thing( $uid );\n\t\t\t\t}\n\n\t\t\t\tif ( 'llms_enrolled' === $status ) {\n\t\t\t\t\treturn $enrollment_status;\n\t\t\t\t} elseif ( 'llms_not_enrolled' === $status ) {\n\t\t\t\t\treturn ( ! $enrollment_status );\n\t\t\t\t}\n\t\t\t} elseif ( 'specific' === $type ) { // Check if they're enrolled / not enrolled in the specific courses/memberships.\n\n\t\t\t\t$match = $node->settings->llms_enrollment_match ? $node->settings->llms_enrollment_match : 'any';\n\t\t\t\t$ids   = $this->get_related_posts_from_settings( $node->settings );\n\n\t\t\t\tif ( empty( $ids ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tif ( 'llms_enrolled' === $status ) {\n\n\t\t\t\t\tif ( ! $uid ) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn llms_is_user_enrolled( $uid, $ids, $match );\n\n\t\t\t\t} elseif ( 'llms_not_enrolled' === $status ) {\n\n\t\t\t\t\tif ( ! $uid ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn ! llms_is_user_enrolled( $uid, $ids, $match );\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $visible;\n\t}\n\n\t/**\n\t * Detemine if a student is enrolled in at least one course or membership.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @param int $uid WP_User ID.\n\t * @return boolean\n\t */\n\tprivate function is_student_enrolled_in_one_thing( $uid ) {\n\n\t\tif ( ! $uid ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$student = llms_get_student( $uid );\n\t\tif ( ! $student->exists() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$courses = $student->get_courses(\n\t\t\tarray(\n\t\t\t\t'limit'  => 1,\n\t\t\t\t'status' => 'enrolled',\n\t\t\t)\n\t\t);\n\n\t\tif ( $courses['results'] ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t$memberships = $student->get_membership_levels();\n\t\tif ( $memberships ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Replace the BB filter after we've rendered our content.\n\t *\n\t * @since 1.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function llms_pa_after_content() {\n\t\tadd_filter( 'the_content', 'FLBuilder::render_content' );\n\t}\n\n\t/**\n\t * BB will replace PA Post content with course/membership pagebuilder content\n\t * so remove the filter and replace when we're done with our output.\n\t *\n\t * @since 1.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function llms_pa_before_content() {\n\t\tremove_filter( 'the_content', 'FLBuilder::render_content' );\n\t}\n\n\t/**\n\t * Loads LifterLMS modules.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function load_modules() {\n\t\tif ( ! class_exists( 'FLBUilderModule' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( file_exists( LLMS_BB_MODULES_DIR ) ) {\n\t\t\tforeach ( glob( LLMS_BB_MODULES_DIR . '**/*.php', GLOB_NOSORT ) as $file ) {\n\t\t\t\trequire_once $file;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Filter out LifterLMS modules if it's not the right post type.\n\t *\n\t * @param $enabled\n\t * @param $instance\n\t *\n\t * @return bool\n\t */\n\tpublic function register_module( $enabled, $instance ) {\n\t\t$post_type = get_post_type();\n\n\t\tif ( 'course' !== $post_type && in_array( $instance->slug, array( 'class.llms.lab.course.instructors.module', 'class.llms.lab.course.syllabus.module', 'class.llms.lab.course.progress.bar.module' ) ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( 'llms_membership' !== $post_type && in_array( $instance->slug, array( 'class.llms.lab.membership.instructors.module' ) ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( 'lesson' !== $post_type && in_array( $instance->slug, array( 'class.llms.lab.lesson.mark.complete.module' ) ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $enabled;\n\t}\n\n\n\t/**\n\t * Load LifterLMS layout templates.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function load_templates() {\n\n\t\tif ( ! class_exists( 'FLBuilderModel' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tFLBuilderModel::register_templates( LLMS_PLUGIN_DIR . 'includes/beaver-builder/templates/course-template.dat' );\n\t}\n\n\t/**\n\t * Modify LifterLMS metabox Fields to show the page builder is active.\n\t *\n\t * @since 1.3.0\n\t * @since 1.5.2 Unknown.\n\t * @since 1.7.0 Use strict comparison for `in_array`.\n\t *\n\t * @param array $fields Metabox fields.\n\t * @return array\n\t */\n\tpublic function mod_metabox_fields( $fields ) {\n\n\t\tglobal $post;\n\n\t\t$post_types = array( 'course', 'lesson', 'llms_membership' );\n\n\t\tif ( in_array( $post->post_type, $post_types, true ) && FLBuilderModel::is_builder_enabled() ) {\n\n\t\t\tunset( $fields[0]['fields'][0]['value']['content'] );\n\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Bypass restriction checks for courses and memberships when the builder is active.\n\t *\n\t * Allows the builder to use custom LifterLMS visibility settings when a student is not enrolled.\n\t *\n\t * @since 1.3.0\n\t * @since 1.7.0 Use `in_array` with strict comparison.\n\t *\n\t * @param array $results Restriction results data.\n\t * @param int   $post_id Current post id.\n\t * @return array\n\t */\n\tpublic function mod_page_restrictions( $results, $post_id ) {\n\n\t\tif (\n\t\t\tFLBuilderModel::is_builder_enabled() &&\n\t\t\t$results['is_restricted'] &&\n\t\t\tin_array( get_post_type( $post_id ), array( 'course', 'llms_membership' ), true )\n\t\t) {\n\t\t\t$results['is_restricted'] = false;\n\t\t\t$results['reason']        = 'bb-lab';\n\t\t}\n\n\t\treturn $results;\n\t}\n\n\t/**\n\t * Prevent page building of LifterLMS Post Types that can't actually be pagebuilt despite what the settings may assume.\n\t *\n\t * @since 1.5.0\n\t * @since 1.7.0 Removed unset parameters for `llms_certificate` and `llms_my_certificate` from the filter.\n\t *\n\t * @param array $post_types Post type objects as an array.\n\t * @return array\n\t */\n\tpublic function remove_uneditable_post_types( $post_types ) {\n\n\t\tunset( $post_types['llms_quiz'] );\n\t\tunset( $post_types['llms_question'] );\n\n\t\treturn $post_types;\n\t}\n\n\t/**\n\t * Upgrade url.\n\t *\n\t * @since 1.3.0\n\t *\n\t * @param string $url Default upgrade url.\n\t * @return string\n\t */\n\tpublic function upgrade_url( $url ) {\n\t\treturn 'https://www.wpbeaverbuilder.com/?fla=968';\n\t}\n}\n\nreturn new LLMS_Beaver_Builder();\n"
  },
  {
    "path": "includes/class-llms-block-library.php",
    "content": "<?php\n/**\n * LLMS_Block_Library class file.\n *\n * @package LifterLMS/Classes\n *\n * @since 6.0.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Load the LifterLMS block library.\n *\n * @since 6.0.0\n */\nclass LLMS_Block_Library {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'init', array( $this, 'register' ) );\n\n\t\tadd_filter( 'block_editor_settings_all', array( $this, 'modify_editor_settings' ), 100, 2 );\n\n\t}\n\n\t/**\n\t * Retrieves a list of blocks to register.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string[] A list of directory paths that can individually be passed to `register_block_type()`.\n\t */\n\tprivate function get_blocks() {\n\n\t\t$blocks = array();\n\n\t\tif ( llms_is_block_editor_supported_for_certificates() ) {\n\t\t\t$blocks['certificate-title'] = array(\n\t\t\t\t'path'       => null,\n\t\t\t\t'post_types' => array(\n\t\t\t\t\t'llms_certificate',\n\t\t\t\t\t'llms_my_certificate',\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Add default path to all blocks.\n\t\tforeach ( $blocks as $id => &$block ) {\n\t\t\t$block['path'] = is_null( $block['path'] ) ? LLMS_PLUGIN_DIR . 'blocks/' . $id : $block['path'];\n\t\t}\n\n\t\treturn $blocks;\n\n\t}\n\n\t/**\n\t * Loads custom fonts for the llms/certificate-title block.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array                   $settings Editor settings.\n\t * @param WP_Block_Editor_Context $context  Current block editor context.\n\t * @return array\n\t */\n\tpublic function modify_editor_settings( $settings, $context ) {\n\n\t\t// Only load fonts when in post editor context for a certificate post type.\n\t\tif ( ! empty( $context->post ) && in_array( $context->post->post_type, array( 'llms_certificate', 'llms_my_certificate' ), true ) ) {\n\n\t\t\t$theme_fonts = $settings['__experimentalFeatures']['typography']['fontFamilies']['theme'] ?? array();\n\n\t\t\t$fonts        = llms_get_certificate_fonts();\n\t\t\t$custom_fonts = array_map(\n\t\t\t\tfunction( $slug, $font_data ) {\n\t\t\t\t\tunset( $font_data['href'] );\n\t\t\t\t\t$font_data['slug'] = $slug;\n\t\t\t\t\treturn $font_data;\n\t\t\t\t},\n\t\t\t\tarray_keys( $fonts ),\n\t\t\t\t$fonts\n\t\t\t);\n\n\t\t\t_wp_array_set(\n\t\t\t\t$settings,\n\t\t\t\tarray(\n\t\t\t\t\t'__experimentalFeatures',\n\t\t\t\t\t'blocks',\n\t\t\t\t\t'llms/certificate-title',\n\t\t\t\t\t'typography',\n\t\t\t\t\t'fontFamilies',\n\t\t\t\t\t'custom',\n\t\t\t\t),\n\t\t\t\tarray_merge( $theme_fonts, array_filter( $custom_fonts ) )\n\t\t\t);\n\n\t\t}\n\n\t\treturn $settings;\n\n\t}\n\n\t/**\n\t * Register all blocks in the LifterLMS block library.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function register() {\n\n\t\tforeach ( $this->get_blocks() as $id => $block ) {\n\n\t\t\tif ( $this->should_register( $id, $block ) ) {\n\t\t\t\tregister_block_type( $block['path'] );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Determines whether or not the block should be registered.\n\t *\n\t * There's no \"good\" way to register a block only for a specific post type(s) or context (such\n\t * as the post editor only and not the widgets editor).\n\t *\n\t * This method uses the `$pagenow` global and query string variables to interpret the current\n\t * screen context and register the block only in the intended context.\n\t *\n\t * This creates issues if the block list is retrieve via the REST API. But we can't avoid this\n\t * given the current APIs. Especially since the WP core throw's a notice on the widgets screen\n\t * if a block is registered with a script that relies on `wp-editor` as a dependency.\n\t *\n\t * See related issue links below.\n\t *\n\t * @since 6.0.0\n\t * @since 6.4.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @link https://github.com/WordPress/gutenberg/issues/28517\n\t * @link https://github.com/WordPress/gutenberg/issues/12931\n\t *\n\t * @param string $id    The block's id (without the `llms/` prefix).\n\t * @param array  $block Array of block data.\n\t * @return boolean\n\t */\n\tprivate function should_register( $id, $block ) {\n\n\t\t// Prevent errors if the block is already registered.\n\t\t$registry = WP_Block_Type_Registry::get_instance();\n\t\tif ( $registry->is_registered( 'llms/' . $id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Ensure the block is only registered in the correct context.\n\t\tglobal $pagenow;\n\t\t$post_type = null;\n\t\tif ( 'post.php' === $pagenow ) {\n\t\t\t$id        = llms_filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT );\n\t\t\t$post_type = $id ? get_post_type( $id ) : $post_type;\n\t\t} elseif ( 'post-new.php' === $pagenow ) {\n\t\t\t$post_type = llms_filter_input( INPUT_GET, 'post_type' );\n\t\t\t$post_type = $post_type ? $post_type : 'post'; // If `$_GET` is not set it's because it's a basic post.\n\t\t}\n\n\t\tif ( ! is_null( $post_type ) && in_array( $post_type, $block['post_types'], true ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n}\n\nreturn new LLMS_Block_Library();\n"
  },
  {
    "path": "includes/class-llms-block-templates.php",
    "content": "<?php\n/**\n * LLMS_Block_Templates class file\n *\n * @package LifterLMS/Classes\n *\n * @since 5.8.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Handles the block templates.\n *\n * @since 5.8.0\n */\nclass LLMS_Block_Templates {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Directory name of the block templates.\n\t *\n\t * @var string\n\t */\n\tconst LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME = 'block-templates';\n\n\t/**\n\t * Block Template namespace.\n\t *\n\t * This is used to save templates to the DB which are stored against this value in the wp_terms table.\n\t *\n\t * @var string\n\t */\n\tconst LLMS_BLOCK_TEMPLATES_NAMESPACE = 'lifterlms/lifterlms';\n\n\t/**\n\t * Block Template slug prefix.\n\t *\n\t * @var string\n\t */\n\tconst LLMS_BLOCK_TEMPLATES_PREFIX = 'llms_';\n\n\t/**\n\t * Block templates configuration.\n\t *\n\t * @var array\n\t */\n\tprivate $block_templates_config;\n\n\t/**\n\t * Private Constructor.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t$this->configure_block_templates();\n\n\t\tadd_filter( 'get_block_templates', array( $this, 'add_llms_block_templates' ), 10, 3 );\n\t\tadd_filter( 'pre_get_block_file_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );\n\t\tadd_action( 'admin_enqueue_scripts', array( $this, 'localize_blocks' ), 9999 );\n\t}\n\n\t/**\n\t * Configure block templates.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function configure_block_templates() {\n\n\t\t$block_templates_config = array(\n\t\t\tllms()->plugin_path() . '/templates/' . self::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME => array(\n\t\t\t\t'slug_prefix'       => self::LLMS_BLOCK_TEMPLATES_PREFIX,\n\t\t\t\t'namespace'         => self::LLMS_BLOCK_TEMPLATES_NAMESPACE,\n\t\t\t\t'blocks_dir'        => self::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME, // Relative to the plugin's templates directory.\n\t\t\t\t'admin_blocks_l10n' => $this->block_editor_l10n(),\n\t\t\t\t'template_titles'   => $this->template_titles(),\n\t\t\t),\n\t\t);\n\n\t\t/**\n\t\t * Filters the block templates configuration.\n\t\t *\n\t\t * @since 5.8.0\n\t\t *\n\t\t * @param array $block_templates_config Block templates configuration array.\n\t\t */\n\t\t$this->block_templates_config = apply_filters( 'llms_block_templates_config', $block_templates_config );\n\n\t}\n\n\t/**\n\t * This function checks if there's a blocks template to return to pre_get_posts short-circuiting the query in Gutenberg.\n\t *\n\t * Ultimately it resolves either a saved blocks template from the\n\t * database or a template file in `lifterlms/templates/block-templates/`.\n\t * Without this it won't be possible to save llms templates customizations in the DB.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param WP_Block_Template|null $template      Return a block template object to short-circuit the default query,\n\t *                                              or null to allow WP to run its normal queries.\n\t * @param string                 $id            Template unique identifier (example: theme_slug//template_slug).\n\t * @param array                  $template_type wp_template or wp_template_part.\n\t * @return mixed|WP_Block_Template|WP_Error\n\t */\n\tpublic function maybe_return_blocks_template( $template, $id, $template_type ) {\n\n\t\t// Bail if 'get_block_template' (introduced in WP 5.9.) doesn't exist, or the requested template is not a 'wp_template' type.\n\t\tif ( ! function_exists( 'get_block_template' ) || 'wp_template' !== $template_type ) {\n\t\t\treturn $template;\n\t\t}\n\n\t\t$template_name_parts = explode( '//', $id );\n\t\tif ( count( $template_name_parts ) < 2 ) {\n\t\t\treturn $template;\n\t\t}\n\n\t\tlist( , $slug ) = $template_name_parts;\n\n\t\t// Remove the filter at this point because if we don't then this function will infinite loop.\n\t\tremove_filter( 'pre_get_block_file_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );\n\n\t\t// Check if the theme has a saved version of this template before falling back to the llms one.\n\t\t$maybe_template = get_block_template( $id, $template_type );\n\n\t\tif ( null !== $maybe_template ) {\n\t\t\tadd_filter( 'pre_get_block_file_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );\n\t\t\treturn $maybe_template;\n\t\t}\n\n\t\t// Theme-based template didn't exist, try switching the theme to lifterlms and try again. This function has\n\t\t// been unhooked so won't run again.\n\t\tadd_filter( 'get_block_file_template', array( $this, 'get_single_block_template' ), 10, 3 );\n\t\t$maybe_template = get_block_template( $id, $template_type );\n\n\t\t// Re-hook this function, it was only unhooked to stop recursion.\n\t\tadd_filter( 'pre_get_block_file_template', array( $this, 'maybe_return_blocks_template' ), 10, 3 );\n\t\tremove_filter( 'get_block_file_template', array( $this, 'get_single_block_template' ), 10, 3 );\n\t\tif ( null !== $maybe_template ) {\n\t\t\treturn $maybe_template;\n\t\t}\n\n\t\t// At this point we haven't had any luck finding a template. Give up and let Gutenberg take control again.\n\t\treturn $template;\n\n\t}\n\n\n\t/**\n\t * Runs on the get_block_template hook.\n\t *\n\t * If a template is already found and passed to this function, then return it and don't run.\n\t * If a template is *not* passed, try to look for one that matches the ID in the database, if that's not found defer\n\t * to Blocks templates files. Priority goes: DB-Theme, DB-Blocks, Filesystem-Theme, Filesystem-Blocks.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param WP_Block_Template $template      The found block template.\n\t * @param string            $id            Template unique identifier (example: theme_slug//template_slug).\n\t * @param array             $template_type wp_template or wp_template_part.\n\t *\n\t * @return mixed|null\n\t */\n\tpublic function get_single_block_template( $template, $id, $template_type ) {\n\n\t\t// The template was already found before the filter runs, or the requested template is not a 'wp_template' type, just return it immediately.\n\t\tif ( null !== $template || 'wp_template' !== $template_type ) {\n\t\t\treturn $template;\n\t\t}\n\n\t\t$template_name_parts = explode( '//', $id );\n\t\tif ( count( $template_name_parts ) < 2 ) {\n\t\t\treturn $template;\n\t\t}\n\t\tlist( , $slug ) = $template_name_parts;\n\n\t\t// Get available llms templates from the filesystem.\n\t\t$available_templates = $this->block_templates( array( $slug ), '', true );\n\n\t\t// If this blocks template doesn't exist then we should just skip the function and let Gutenberg handle it.\n\t\tif ( ! in_array( $slug, wp_list_pluck( $available_templates, 'slug' ), true ) ) {\n\t\t\treturn $template;\n\t\t}\n\n\t\t$template = ( is_array( $available_templates ) && count( $available_templates ) > 0 ) ?\n\t\t\t$available_templates[0] : $template;\n\n\t\treturn $template;\n\n\t}\n\n\t/**\n\t * Gets the templates.\n\t *\n\t * @since 5.8.0\n\t * @since 5.9.0 Filter template slugs array before checking if it's empty.\n\t *\n\t * @param array  $slugs     An array of slugs to retrieve templates for.\n\t * @param string $post_type Post Type.\n\t * @param bool   $fs_only   Retrieve templates from the filesystem ony.\n\t * @return WP_Block_Template[] Templates.\n\t */\n\tprivate function block_templates( $slugs = array(), $post_type = '', $fs_only = false ) {\n\n\t\t// Get paths where to look for block templates.\n\t\t$block_templates_paths = $this->block_templates_paths();\n\n\t\t// Get all the slugs.\n\t\t$template_slugs = array_map( array( $this, 'generate_template_slug_from_path' ), $block_templates_paths );\n\t\t// If specific slugs are required, filter them only.\n\t\t$template_slugs = empty( array_filter( $slugs ) ) ? $template_slugs : array_intersect( $slugs, $template_slugs );\n\n\t\tif ( empty( $template_slugs ) ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t$templates = $fs_only\n\t\t\t?\n\t\t\t$this->block_templates_from_fs( $block_templates_paths, $template_slugs )\n\t\t\t:\n\t\t\tarray_merge(\n\t\t\t\t$this->block_templates_from_db( $template_slugs ),\n\t\t\t\t$this->block_templates_from_fs( $block_templates_paths, $template_slugs )\n\t\t\t);\n\n\t\t// DB wins over fs, exclude not allowed post types.\n\t\t$templates = array_values(\n\t\t\tarray_filter(\n\t\t\t\t$templates,\n\t\t\t\tfunction( $template, $key ) use ( $templates, $post_type ) {\n\t\t\t\t\treturn ( ! ( $post_type && isset( $template->post_types ) && ! in_array( $post_type, $template->post_types, true ) ) ) &&\n\t\t\t\t\t\tarray_search( $template->slug, array_unique( wp_list_pluck( $templates, 'slug' ) ), true ) === $key;\n\t\t\t\t},\n\t\t\t\tARRAY_FILTER_USE_BOTH\n\t\t\t)\n\t\t);\n\n\t\treturn $templates;\n\n\t}\n\n\t/**\n\t * Get block templates from the file system.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param string[] $block_templates_paths Array of block templates paths to look for templates.\n\t * @param string[] $slugs                 Arrray of template slugs to be retrieved.\n\t * @return void\n\t */\n\tprivate function block_templates_from_fs( $block_templates_paths, $slugs = array() ) {\n\n\t\t$templates = array();\n\n\t\tforeach ( $block_templates_paths as $template_file ) {\n\t\t\t$template_slug = $this->generate_template_slug_from_path( $template_file );\n\t\t\tif ( ! empty( $slugs ) && ! in_array( $template_slug, $slugs, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$templates[] = $this->build_template_result_from_file( $template_file, $template_slug );\n\t\t}\n\n\t\treturn $templates;\n\n\t}\n\n\t/**\n\t * Gets the templates saved in the database.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param array $slugs An array of slugs to retrieve templates for.\n\t * @return int[]|WP_Post[] An array of found templates.\n\t */\n\tprivate function block_templates_from_db( $slugs = array() ) {\n\n\t\t$query_args = array(\n\t\t\t'post_status'    => array( 'auto-draft', 'draft', 'publish' ),\n\t\t\t'post_type'      => 'wp_template',\n\t\t\t'posts_per_page' => -1,\n\t\t\t'no_found_rows'  => true,\n\t\t\t'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query\n\t\t\t\tarray(\n\t\t\t\t\t'taxonomy' => 'wp_theme',\n\t\t\t\t\t'field'    => 'name',\n\t\t\t\t\t'terms'    => array_merge(\n\t\t\t\t\t\tarray( get_stylesheet(), get_template() ),\n\t\t\t\t\t\tarray_column( $this->block_templates_config, 'namespace' )\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\tif ( is_array( $slugs ) && count( $slugs ) > 0 ) {\n\t\t\t$query_args['post_name__in'] = $slugs;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query arguments to retrieve the templates saved in the db.\n\t\t *\n\t\t * @since 5.8.0\n\t\t *\n\t\t * @param array $query_args WQ_Query argiments to retrieve the templates saved in the db.\n\t\t */\n\t\t$query_args = apply_filters( 'llms_block_templates_from_db_query_args', $query_args );\n\n\t\t$templates = ( new WP_Query( $query_args ) )->posts;\n\n\t\treturn array_map(\n\t\t\tfunction( $template ) {\n\t\t\t\treturn $this->build_template_result_from_post( $template );\n\t\t\t},\n\t\t\t$templates\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the block templates directory paths.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return string[]\n\t */\n\tprivate function block_templates_paths() {\n\n\t\t$block_template_paths = array();\n\n\t\t$block_templates_base_paths = array_keys( $this->block_templates_config );\n\n\t\tforeach ( $block_templates_base_paths as $block_template_base_path ) {\n\t\t\t$block_template_paths = array_merge(\n\t\t\t\t_get_block_templates_paths( $block_template_base_path ),\n\t\t\t\t$block_template_paths\n\t\t\t);\n\t\t}\n\n\t\treturn $block_template_paths;\n\n\t}\n\n\t/**\n\t * Build a wp template from file.\n\t *\n\t * @since 5.8.0\n\t * @since 5.9.0 Allow template directory override when the block template comes from an add-on.\n\t * @since 7.5.0 Use `traverse_and_serialize_blocks` in place of deprecated (since wp 6.4.0) `_inject_theme_attribute_in_block_template_content`\n\t *\n\t * @param string $template_file Template file path.\n\t * @param string $template_slug Template slug.\n\t * @return WP_Block_Template\n\t */\n\tprivate function build_template_result_from_file( $template_file, $template_slug = '' ) {\n\n\t\t$template_slug = empty( $template_slug ) ? $this->generate_template_slug_from_path( $template_file ) : $template_slug;\n\t\t$namespace     = $this->generate_template_namespace_from_path( $template_file );  // Looks like 'lifterlms/lifterlms' or 'lifterlms-groups/lifterlms-groups', etc.\n\t\t$template_file = $this->get_maybe_overridden_block_template_file_path( $template_file );\n\n\t\t// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents\n\t\t$template_content = file_get_contents( $template_file );\n\n\t\t// Is the template from the theme/child-theme.\n\t\t$theme = false !== strpos( $template_file, get_template_directory() ) ? get_template() : get_stylesheet();\n\t\t$theme = false !== strpos( $template_file, get_stylesheet_directory() ) ? $theme : false;\n\n\t\t$template                 = new WP_Block_Template();\n\t\t$template->id             = $theme ? $theme . '//' . $template_slug : $namespace . '//' . $template_slug;\n\t\t$template->theme          = $theme ? $theme : $namespace;\n\t\t$template->content        = function_exists( 'traverse_and_serialize_blocks' ) ?\n\t\t\ttraverse_and_serialize_blocks( parse_blocks( $template_content ), '_inject_theme_attribute_in_template_part_block' ) :\n\t\t\t_inject_theme_attribute_in_block_template_content( $template_content );\n\t\t$template->source         = $theme ? 'theme' : 'plugin'; // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.\n\t\t$template->slug           = $template_slug;\n\t\t$template->type           = 'wp_template';\n\t\t$template->title          = $this->convert_slug_to_title( $template_slug );\n\t\t$template->status         = 'publish';\n\t\t$template->has_theme_file = true;\n\t\t$template->origin         = $theme ? 'theme' : 'plugin';\n\t\t$template->is_custom      = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.\n\t\t$template->post_types     = array(); // Don't appear in any Edit Post template selector dropdown.\n\n\t\treturn $template;\n\t}\n\n\t/**\n\t * Build a unified template object based on a WP_Post object.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param WP_Post $post Template post.\n\t * @return WP_Block_Template|WP_Error Template.\n\t */\n\tprivate function build_template_result_from_post( $post ) {\n\n\t\t$terms = get_the_terms( $post, 'wp_theme' );\n\n\t\tif ( is_wp_error( $terms ) ) {\n\t\t\treturn $terms;\n\t\t}\n\n\t\tif ( ! $terms ) {\n\t\t\treturn new \\WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'lifterlms' ) );\n\t\t}\n\n\t\t$theme = $terms[0]->name;\n\n\t\t$template                 = new WP_Block_Template();\n\t\t$template->wp_id          = $post->ID;\n\t\t$template->id             = $theme . '//' . $post->post_name;\n\t\t$template->theme          = $theme;\n\t\t$template->content        = $post->post_content;\n\t\t$template->slug           = $post->post_name;\n\t\t$template->source         = 'custom';\n\t\t$template->type           = $post->post_type;\n\t\t$template->description    = $post->post_excerpt;\n\t\t$template->title          = $post->post_title;\n\t\t$template->status         = $post->post_status;\n\t\t$template->has_theme_file = true;\n\t\t$template->is_custom      = false;\n\t\t$template->post_types     = array(); // Don't appear in any Edit Post template selector dropdown.\n\n\t\t/**\n\t\t * Set the 'plugin' origin\n\t\t * if it doesn't come from from the current theme (or its parent).\n\t\t */\n\t\tif ( ! in_array( $theme, array( get_template(), get_stylesheet() ), true ) ) {\n\t\t\t$template->origin = 'plugin';\n\t\t}\n\n\t\treturn $template;\n\n\t}\n\n\t/**\n\t * Retrieve the actual template file path, maybe overridden in the theme.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @param string $template_file The template's path.\n\t * @return string\n\t */\n\tprivate function get_maybe_overridden_block_template_file_path( $template_file ) {\n\n\t\t$template_path_info  = pathinfo( $template_file );\n\t\t$template_file_name  = $template_path_info['filename'];\n\t\t$template_blocks_dir = untrailingslashit( $this->generate_template_blocks_dir_from_path( $template_file ) ); // Looks like 'block-templates'.\n\n\t\t/**\n\t\t * Does this come from LifterLMS or from an add-on? In the latter case use the absolute path.\n\t\t *\n\t\t * $template_path_info['dirname'] looks like 'ABSPATH/wp-content/plugins/lifterlms/templates/block-templates' or\n\t\t * 'ABSPATH/wp-content/plugins/lifterlms-groups/templates/block-templates' for an add-on.\n\t\t */\n\t\treturn false !== strpos( $template_path_info['dirname'], trailingslashit( llms()->plugin_path() ) )\n\t\t\t?\n\t\t\tllms_template_file_path(\n\t\t\t\t$template_blocks_dir . '/' . $template_file_name . '.html'\n\t\t\t)\n\t\t\t:\n\t\t\tllms_template_file_path(\n\t\t\t\t$template_blocks_dir . '/' . $template_file_name . '.html', // Looks like 'block-templates/single-llms_group.html'.\n\t\t\t\tsubstr( $template_path_info['dirname'], 0, -1 * strlen( $template_blocks_dir ) ), // Looks like 'ABSPATH/wp-content/plugins/lifterlms-groups/templates/'.\n\t\t\t\ttrue\n\t\t\t);\n\n\t}\n\n\t/**\n\t * Convert the template paths into a slug.\n\t *\n\t * @since 5.8.0\n\t * @since 5.9.0 Return empty string if the passed path is not in the configuration.\n\t * @since 5.10.0 Use '/' in favor of DIRECTORY_SEPARATOR to avoid issues on Windows.\n\t * @since 7.2.0 Retrieve the slug by using `basename()` which also fixes issues on Windows filesystems.\n\t *\n\t * @param string $path The template's path.\n\t * @return string\n\t */\n\tprivate function generate_template_slug_from_path( $path ) {\n\n\t\t$prefix  = $this->block_template_config_property_from_path( $path, 'slug_prefix' );\n\n\t\treturn $prefix . basename( $path, '.html' );\n\n\t}\n\n\t/**\n\t * Generate the template namespace from the template path.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param string $path The template's path.\n\t * @return string\n\t */\n\tprivate function generate_template_namespace_from_path( $path ) {\n\n\t\treturn $this->block_template_config_property_from_path( $path, 'namespace' );\n\n\t}\n\n\t/**\n\t * Generate the template slug prefix from the template path.\n\t *\n\t * @since 5.8.0\n\t * @since 5.9.0 Fix property name.\n\t *\n\t * @param string $path The template's path.\n\t * @return string\n\t */\n\tprivate function generate_template_prefix_from_path( $path ) {\n\n\t\treturn $this->block_template_config_property_from_path( $path, 'slug_prefix' );\n\n\t}\n\n\t/**\n\t * Generate the block template directory (relative to the templates direcotry) from the template path.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @param string $path The template's path.\n\t * @return string\n\t */\n\tprivate function generate_template_blocks_dir_from_path( $path ) {\n\n\t\treturn $this->block_template_config_property_from_path( $path, 'blocks_dir' );\n\n\t}\n\n\t/**\n\t * Retrieve a template config property from path.\n\t *\n\t * @since 5.8.0\n\t * @since 5.9.0 Return an empty string if requesting a non existing property.\n\t *               Also removed unused var `$dirname`.\n\t *\n\t * @param string $path     The template's path.\n\t * @param string $property The template's config property to retrieve.\n\t * @return string\n\t */\n\tprivate function block_template_config_property_from_path( $path, $property ) {\n\n\t\t$prop_value = '';\n\t\tforeach ( $this->block_templates_config as $block_templates_base_path => $config ) {\n\t\t\tif ( false !== strpos( $path, $block_templates_base_path ) ) {\n\t\t\t\t$prop_value = $config[ $property ] ?? $prop_value;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn $prop_value;\n\n\t}\n\n\t/**\n\t * Converts template slugs into readable titles.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param string $template_slug The templates slug (e.g. single-product).\n\t * @return string Human friendly title converted from the slug.\n\t */\n\tprivate function convert_slug_to_title( $template_slug ) {\n\n\t\t$template_titles = array_merge( ...array_column( $this->block_templates_config, 'template_titles' ) );\n\n\t\treturn array_key_exists( $template_slug, $template_titles ) ?\n\t\t\t$template_titles[ $template_slug ]\n\t\t\t:\n\t\t\t// Replace all hyphens and underscores with spaces.\n\t\t\tucwords( preg_replace( '/[\\-_]/', ' ', $template_slug ) );\n\n\t}\n\n\t/**\n\t * Add lifterlms blocks templates.\n\t *\n\t * @since 5.8.0\n\t * @since 6.0.0 Use `llms_is_block_theme()` in favor of `wp_is_block_theme()`.\n\t *\n\t * @param WP_Block_Template[] $query_result Array of found block templates.\n\t * @param array               $query        {\n\t *     Optional. Arguments to retrieve templates.\n\t *\n\t *     @type array  $slug__in List of slugs to include.\n\t *     @type int    $wp_id    Post ID of customized template.\n\t * }\n\t * @param array               $template_type wp_template or wp_template_part.\n\t * @return WP_Block_Template[] Templates.\n\t */\n\tpublic function add_llms_block_templates( $query_result, $query, $template_type = 'wp_template' ) {\n\n\t\t// Bail it's not a block theme, or is being retrieved a non wp_template type requested.\n\t\tif ( ! llms_is_block_theme() || 'wp_template' !== $template_type ) {\n\t\t\treturn $query_result;\n\t\t}\n\n\t\t$post_type = $query['post_type'] ?? '';\n\t\t$slugs     = $query['slug__in'] ?? array();\n\n\t\t// Retrieve templates.\n\t\t$templates = $this->block_templates( $slugs, $post_type );\n\n\t\t/**\n\t\t * Remove theme override templates who have a customization in the db from $query_result:\n\t\t * those template blocks will be already retrieved by our LLMS_Block_Templates::block_templates_from_db().\n\t\t */\n\t\t$query_result = array_values(\n\t\t\tarray_filter(\n\t\t\t\t$query_result,\n\t\t\t\tfunction( $template ) use ( $templates ) {\n\t\t\t\t\t$slugs = wp_list_pluck( $templates, 'slug' );\n\t\t\t\t\treturn ( ! in_array( $template->slug, $slugs, true ) );\n\t\t\t\t}\n\t\t\t)\n\t\t);\n\n\t\treturn array_merge( $query_result, $templates );\n\n\t}\n\n\t/**\n\t * Returns an associative array of template titles.\n\t *\n\t * Keys are template slugs.\n\t * Values are template titles in a human readable form.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return array\n\t */\n\tprivate function template_titles() {\n\n\t\t$template_titles = array(\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'archive-course'             => esc_html__( 'Course Catalog', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'archive-llms_membership'    => esc_html__( 'Membership Catalog', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'single-certificate'         => esc_html__( 'Single Certificate', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'single-no-access'           => esc_html__( 'Single Access Restricted', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'taxonomy-course_cat'        => esc_html__( 'Taxonomy Course Category', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'taxonomy-course_difficulty' => esc_html__( 'Taxonomy Course Difficulty', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'taxonomy-course_tag'        => esc_html__( 'Taxonomy Course Tag', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'taxonomy-course_track'      => esc_html__( 'Taxonomy Course Track', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'taxonomy-membership_cat'    => esc_html__( 'Taxonomy Membership Category', 'lifterlms' ),\n\t\t\tself::LLMS_BLOCK_TEMPLATES_PREFIX . 'taxonomy-membership_tag'    => esc_html__( 'Taxonomy Membership Tag', 'lifterlms' ),\n\t\t);\n\n\t\t/**\n\t\t * Filters the block template titles.\n\t\t *\n\t\t * @since 5.8.0\n\t\t *\n\t\t * @param array $template_titles  {\n\t\t *     Associative array of template titles.\n\t\t *\n\t\t *     @type string $slug  The template slug.\n\t\t *     @type string $title The template readable titles.\n\t\t * }\n\t\t */\n\t\treturn apply_filters( 'lifterlms_block_templates_titles', $template_titles );\n\n\t}\n\n\t/**\n\t * Block Templates admin js strings.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return string[]\n\t */\n\tprivate function block_editor_l10n() {\n\n\t\treturn array(\n\t\t\t'archive-course'             => esc_html__( 'LifterLMS Course Catalog Template', 'lifterlms' ),\n\t\t\t'archive-llms_membership'    => esc_html__( 'LifterLMS Membership Catalog Template', 'lifterlms' ),\n\t\t\t'single-certificate'         => esc_html__( 'LifterLMS Certificate Template', 'lifterlms' ),\n\t\t\t'single-no-access'           => esc_html__( 'LifterLMS Single Template Access Restricted', 'lifterlms' ),\n\t\t\t'taxonomy-course_cat'        => esc_html__( 'LifterLMS Course Category Taxonomy Template', 'lifterlms' ),\n\t\t\t'taxonomy-course_difficulty' => esc_html__( 'LifterLMS Course Difficulty Taxonomy Template', 'lifterlms' ),\n\t\t\t'taxonomy-course_tag'        => esc_html__( 'LifterLMS Course Tag Taxonomy Template', 'lifterlms' ),\n\t\t\t'taxonomy-course_track'      => esc_html__( 'LifterLMS Course Track Taxonomy Template', 'lifterlms' ),\n\t\t\t'taxonomy-membership_cat'    => esc_html__( 'LifterLMS Membership Tag Taxonomy Template', 'lifterlms' ),\n\t\t\t'taxonomy-membership_tag'    => esc_html__( 'LifterLMS Membership Tag Taxonomy Template', 'lifterlms' ),\n\t\t);\n\n\t}\n\n\t/**\n\t * Localize block templates.\n\t *\n\t * @since 5.8.0\n\t * @since 5.9.0 Retuns the `wp_localize_script()` return value.\n\t *\n\t * @return bool\n\t */\n\tpublic function localize_blocks() {\n\t\treturn wp_localize_script(\n\t\t\t'llms-blocks-editor',\n\t\t\t'llmsBlockTemplatesL10n',\n\t\t\tarray_merge( ...array_column( $this->block_templates_config, 'admin_blocks_l10n' ) )\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-bricks.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\nclass LLMS_Bricks {\n\n\tuse LLMS_Trait_Singleton;\n\n\tpublic function __construct() {\n\t\t$this->init();\n\t}\n\n\tpublic function is_available() {\n\t\treturn class_exists( '\\Bricks\\Elements' ) && function_exists( 'bricks_is_builder' );\n\t}\n\n\tprotected function init() {\n\n\t\tif ( ! $this->is_available() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tadd_action( 'init', array( $this, 'register_elements' ), 11 );\n\t\tadd_action( 'init', array( $this, 'add_builder_css' ), 11 );\n\t\tadd_filter( 'bricks/builder/i18n', array( $this, 'i18n' ) );\n\t}\n\n\tpublic function register_elements() {\n\n\t\t$element_files = glob( LLMS_PLUGIN_DIR . 'includes/bricks/class-llms-bricks-element-*.php' );\n\n\t\tforeach ( $element_files as $file ) {\n\t\t\t\\Bricks\\Elements::register_element( $file );\n\t\t}\n\t}\n\n\tpublic function add_builder_css() {\n\t\tif ( ! bricks_is_builder() ) {\n\t\t\treturn;\n\t\t}\n\t\twp_enqueue_style( 'llms-bricks-editor', LLMS_PLUGIN_URL . 'assets/css/bricks-editor.css', array(), filemtime( LLMS_PLUGIN_DIR . 'assets/css/bricks-editor.css' ) );\n\t}\n\n\tpublic function i18n( $i18n ) {\n\t\t$i18n['lifterlms'] = esc_html__( 'LifterLMS', 'lifterlms' );\n\n\t\treturn $i18n;\n\t}\n}\n\nreturn new LLMS_Bricks();\n"
  },
  {
    "path": "includes/class-llms-course-completion-page.php",
    "content": "<?php\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\nclass LLMS_Course_Completion_Page {\n\n\tuse LLMS_Trait_Singleton;\n\n\tpublic function __construct() {\n\t\t$this->init();\n\t}\n\n\tprotected function init() {\n\n\t\t// Add it as the last action to ensure other handlers of course completion happen first.\n\t\tadd_action( 'lifterlms_course_completed', array( $this, 'maybe_redirect_to_course_completion_page' ), 9999, 2 );\n\t}\n\n\tpublic function maybe_redirect_to_course_completion_page( $student_id, $related_post_id ) {\n\t\tif ( is_admin() || $student_id !== get_current_user_id() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$course = new LLMS_Course( $related_post_id );\n\n\t\tif ( ! $course ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( $course->get( 'completion_page_id' ) ) {\n\t\t\twp_safe_redirect( get_permalink( $course->get( 'completion_page_id' ) ) );\n\t\t\texit();\n\t\t}\n\n\t\tif ( get_option( 'lifterlms_course_completion_page_id', '' ) ) {\n\t\t\twp_safe_redirect( get_permalink( get_option( 'lifterlms_course_completion_page_id' ) ) );\n\t\t\texit();\n\t\t}\n\t}\n}\n\nreturn new LLMS_Course_Completion_Page();\n"
  },
  {
    "path": "includes/class-llms-db-ugrader.php",
    "content": "<?php\n/**\n * LLMS_DB_Upgrader class file\n *\n * @package LifterLMS/Classes\n *\n * @since 5.2.0\n * @version 5.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Manage database updates and migrations\n *\n * @since 5.2.0\n */\nclass LLMS_DB_Upgrader {\n\n\t/**\n\t * DB Version that's being upgraded from.\n\t *\n\t * @var string\n\t */\n\tprotected $db_version = '';\n\n\t/**\n\t * Instance of the bg updater class\n\t *\n\t * @var LLMS_Background_Updater\n\t */\n\tprotected $updater = null;\n\n\t/**\n\t * Update list\n\t *\n\t * @var array\n\t */\n\tprotected $updates = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.2.0\n\t *\n\t * @see includes/schemas/llms-db-updates.php For an example updates schema.\n\t *\n\t * @param string       $db_version The DB version that is being upgraded from.\n\t * @param null|array[] $updates    A list of database updates conforming to the database updates schema\n\t *                                 or null to load the LifterLMS core schema.\n\t */\n\tpublic function __construct( $db_version, $updates = null ) {\n\n\t\tif ( ! LLMS_Install::$background_updater ) {\n\t\t\tLLMS_Install::init_background_updater();\n\t\t}\n\t\t$this->updater = LLMS_Install::$background_updater;\n\n\t\t// Background updates may trigger a notice during a cron and notices might not be available.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\t\tif ( is_null( $updates ) ) {\n\t\t\t$updates = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-db-updates.php';\n\t\t}\n\n\t\t$this->db_version = $db_version;\n\t\t$this->updates    = $updates;\n\t}\n\n\t/**\n\t * Determine if an auto-update is possible from the specified DB version\n\t *\n\t * Auto updating is possible as long as none of the required updates are marked as \"manual\".\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return boolean Returns `true` when an auto-update is possible and `false` if manual updating\n\t *                 is required.\n\t */\n\tpublic function can_auto_update() {\n\n\t\t$autoupdate = true;\n\n\t\tforeach ( $this->get_required_updates( $this->db_version ) as $update ) {\n\n\t\t\t// If we find a manual update we cannot auto-update.\n\t\t\tif ( 'manual' === $update['type'] ) {\n\t\t\t\t$autoupdate = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters the list of database updates.\n\t\t *\n\t\t * @since 5.2.0\n\t\t *\n\t\t * @param boolean          $autoupdate Whether or not an automatic update can be run.\n\t\t * @param string           $db_version The specified DB that's being upgraded from.\n\t\t * @param LLMS_DB_Upgrader $upgrader   Instance of the database upgrader.\n\t\t */\n\t\treturn apply_filters( 'llms_can_auto_update_db', $autoupdate, $this->db_version, $this );\n\t}\n\n\t/**\n\t * Retrieve the callback's prefix string based on the schema's namespace declaration.\n\t *\n\t * If `$info['namespace']` is empty, no prefix will be added.\n\t * If `$info['namespace']` is `true`, the namespace is assumed to be `LLMS\\Updates`.\n\t * If `$info['namespace']` is a string, that string will be used.\n\t *\n\t * If a namespace is found, `\\Version_X_X_X` will automatically be appended to the namespace. The\n\t * string `X_X_X` is the database version for the upgrade substituting underscores for dots.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @param array  $info    Upgrade schema array.\n\t * @param string $version Version string for the upgrade.\n\t * @return string\n\t */\n\tprotected function get_callback_prefix( $info, $version ) {\n\n\t\tif ( ! empty( $info['namespace'] ) ) {\n\n\t\t\t$ver = explode( '-', $version ); // Drop prerelease data.\n\t\t\t$ver = str_replace( '.', '_', $ver[0] );\n\t\t\t$ns  = true === $info['namespace'] ? 'LLMS\\Updates' : $info['namespace'];\n\t\t\treturn sprintf( '%1$s\\\\Version_%2$s\\\\', $ns, $ver );\n\n\t\t}\n\n\t\treturn '';\n\t}\n\n\t/**\n\t * Enqueue and dispatch required updates\n\t *\n\t * Adds callbacks for all required updates to the LLMS_Background_Updater and dispatches\n\t * the updater in the background.\n\t *\n\t * If the update group cannot be auto-updated the following admin notices will be included:\n\t * + The \"update started\" notice will be immediately displayed/added.\n\t * + The \"update complete\" notice will be added to the end of the queue (and then displayed when the update is complete).\n\t *\n\t * @since 5.2.0\n\t * @since 5.6.0 Add namespace prefix to qualifying callback functions.\n\t *\n\t * @return void\n\t */\n\tpublic function enqueue_updates() {\n\n\t\t$queued = false;\n\t\tforeach ( $this->get_required_updates() as $version => $info ) {\n\n\t\t\t$prefix = $this->get_callback_prefix( $info, $version );\n\t\t\tforeach ( $info['updates'] as $callback ) {\n\n\t\t\t\t$callback = $prefix . $callback;\n\n\t\t\t\t$this->updater->log( sprintf( 'Queuing %s - %s', $version, $callback ) );\n\t\t\t\t$this->updater->push_to_queue( $callback );\n\t\t\t\t$queued = true;\n\n\t\t\t}\n\t\t}\n\n\t\t// No updates to add, return early.\n\t\tif ( ! $queued ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Show a start and complete notice for manual updates.\n\t\tif ( ! $this->can_auto_update() ) {\n\t\t\t$this->show_notice_started();\n\t\t\t$this->updater->push_to_queue( array( $this, 'show_notice_complete' ) );\n\t\t}\n\n\t\t$this->updater->save();\n\n\t\tadd_action( 'shutdown', array( 'LLMS_Install', 'dispatch_db_updates' ) );\n\t}\n\n\t/**\n\t * Retrieves the updates list\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_updates() {\n\n\t\t/**\n\t\t * Filters the list of database updates.\n\t\t *\n\t\t * @since 5.2.0\n\t\t *\n\t\t * @param array            $updates  List of updates to be run.\n\t\t * @param LLMS_DB_Upgrader $upgrader Instance of the database upgrader.\n\t\t */\n\t\treturn apply_filters( 'llms_db_updates_list', $this->updates, $this );\n\t}\n\n\t/**\n\t * Retrieve a filtered list of updates as required by the specified DB version\n\t *\n\t * All updates greater than the specified version will be returned.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return array[]\n\t */\n\tpublic function get_required_updates() {\n\n\t\t$db_version = $this->db_version;\n\n\t\treturn array_filter(\n\t\t\t$this->get_updates(),\n\t\t\tfunction ( $update_version ) use ( $db_version ) {\n\t\t\t\treturn version_compare( $db_version, $update_version, '<' );\n\t\t\t},\n\t\t\tARRAY_FILTER_USE_KEY\n\t\t);\n\t}\n\n\t/**\n\t * Determine whether or not there are required updates for a specified DB version.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return boolean Returns `true` if there are updates to run, otherwise returns `false`.\n\t */\n\tpublic function has_required_updates() {\n\n\t\t$required = $this->get_required_updates( $this->db_version );\n\t\treturn ! empty( $required );\n\t}\n\n\t/**\n\t * Show the db upgrade admin notice.\n\t *\n\t * Users can click this notice to start the database upgrade(s).\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tprotected function show_notice_pending() {\n\n\t\t$notice_id = 'bg-db-update';\n\n\t\tif ( LLMS_Admin_Notices::has_notice( $notice_id ) ) {\n\t\t\tLLMS_Admin_Notices::delete_notice( $notice_id );\n\t\t}\n\n\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t$notice_id,\n\t\t\tarray(\n\t\t\t\t'dismissible'  => false,\n\t\t\t\t'template'     => 'db-update.php',\n\t\t\t\t'default_path' => LLMS_PLUGIN_DIR . 'includes/admin/views/notices/',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Show a notice when a manual update is started.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tprotected function show_notice_started() {\n\n\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t'bg-db-update-started',\n\t\t\t__( 'Your database is being upgraded in the background. Feel free to leave this page. A notice like this will appear when the update is complete.', 'lifterlms' ),\n\t\t\tarray(\n\t\t\t\t'dismissible'      => true,\n\t\t\t\t'dismiss_for_days' => 0,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Show a notice when the update is complete\n\t *\n\t * This will also delete the started notice. When short updates run quickly the started and completed notice\n\t * may show up on the same page load which is confusing to look at it. If we just started and it's already done\n\t * when the next page loads we only need to see that update is complete.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function show_notice_complete() {\n\n\t\t// If the update started notice exists, delete it to avoid confusing UX when the update finishes before the page loads.\n\t\tif ( LLMS_Admin_Notices::has_notice( 'bg-db-update-started' ) ) {\n\t\t\tLLMS_Admin_Notices::delete_notice( 'bg-db-update-started' );\n\t\t}\n\n\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t'bg-db-update-complete',\n\t\t\t__( 'The LifterLMS database update is complete.', 'lifterlms' ),\n\t\t\tarray(\n\t\t\t\t'dismissible'      => true,\n\t\t\t\t'dismiss_for_days' => 0,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Start the update\n\t *\n\t * If autoupdating is possible, will enqueue and dispatch the bg updater. Otherwise\n\t * it will show the update pending notice which will prompt an admin to manually\n\t * start the update.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return boolean Returns `false` if there are no updates to run and `true` otherwise.\n\t */\n\tpublic function update() {\n\n\t\tif ( ! $this->has_required_updates() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Auto update if it can.\n\t\tif ( $this->can_auto_update() ) {\n\t\t\t$this->enqueue_updates();\n\t\t} else {\n\t\t\t$this->show_notice_pending();\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "includes/class-llms-dom-document.php",
    "content": "<?php\n/**\n * A convenient wrapper for the DOMDocument Class\n *\n * @package LifterLMS/Classes\n *\n * @since 4.13.0\n * @version 4.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_DOM_Document Class\n *\n * @since 4.13.0\n */\nclass LLMS_DOM_Document {\n\n\t/**\n\t * Stores the load method name\n\t *\n\t * @var string\n\t */\n\tprivate $load_method = 'load_with_mb_convert_encoding';\n\n\t/**\n\t * Stores the HTML string to load\n\t *\n\t * @var string\n\t */\n\tprivate $source;\n\n\t/**\n\t * Stores the DOMDocument instance\n\t *\n\t * @var DOMDocument\n\t */\n\tprivate $dom;\n\n\t/**\n\t * Stores loading errors\n\t *\n\t * @var null|WP_Error\n\t */\n\tprivate $error;\n\n\t/**\n\t * This forces DOMDocument to convert non-utf8 characters into HTML entities and without relying on `mb_convert_encoding()`.\n\t *\n\t * @var string\n\t */\n\tprivate $utf8_fixer = '<meta id=\"llms-get-dom-doc-utf-fixer\" http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 4.13.0\n\t *\n\t * @param string $source An HTML string, either a full HTML document or a partial string.\n\t * @return void\n\t */\n\tpublic function __construct( $source ) {\n\n\t\tif ( ! class_exists( 'DOMDocument' ) ) {\n\t\t\t$this->error = new WP_Error( 'llms-dom-document-missing', __( 'DOMDocument not available.', 'lifterlms' ) );\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Filters the convert encoding method to be used when loading the source in the DOMDocument\n\t\t *\n\t\t * @param boolean $use_mb_convert_encoding Whether or not the convert encoding method should be used when loading the source in the DOMDocument.\n\t\t *                                         Default is `true`. Requires `mbstring` PHP extension.\n\t\t */\n\t\t$use_mb_convert_encoding = apply_filters( 'llms_dom_document_use_mb_convert_encoding', true );\n\t\tif ( ! ( $use_mb_convert_encoding && function_exists( 'mb_convert_encoding' ) ) ) {\n\t\t\t$this->load_method = 'load_with_meta_utf_fixer';\n\t\t}\n\n\t\t$this->source = $source;\n\t\t$this->dom    = new DOMDocument();\n\t}\n\n\t/**\n\t * Load the HTML string in the DOMDocument\n\t *\n\t * This function suppresses PHP warnings that would be thrown by DOMDocument when\n\t * loading a partial string or an HTML string with errors.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return boolean|WP_Error Returns `true` if the source is loaded fine.\n\t *                          Or an error object when DOMDocument isn't available or an error is encountered during loading.\n\t */\n\tpublic function load() {\n\n\t\tif ( is_wp_error( $this->error ) && $this->error->has_errors() ) {\n\t\t\treturn $this->error;\n\t\t}\n\n\t\t// Don't throw or log warnings.\n\t\t$libxml_state = libxml_use_internal_errors( true );\n\n\t\t$this->{$this->load_method}();\n\n\t\t// Clear and restore errors.\n\t\tlibxml_clear_errors();\n\t\tlibxml_use_internal_errors( $libxml_state );\n\n\t\treturn is_wp_error( $this->error ) && $this->error->has_errors() ? $this->error : true;\n\n\t}\n\n\t/**\n\t * Returns the DOMDocument\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return DOMDocument Returns an instance of DOMDocument.\n\t */\n\tpublic function dom() {\n\n\t\treturn $this->dom;\n\n\t}\n\n\t/**\n\t * Load the HTML string in the DOMDocument using mb_encode_numericentity\n\t *\n\t * @since 4.13.0\n\t * @since 9.2.2 Use `mb_encode_numericentity()` instead of deprecated `mb_convert_encoding()` with 'HTML-ENTITIES'.\n\t *\n\t * @return void\n\t */\n\tprivate function load_with_mb_convert_encoding() {\n\t\t$html = mb_encode_numericentity( $this->source, array( 0x80, 0x10FFFF, 0, 0x1FFFFF ), 'UTF-8' );\n\t\tif ( ! $this->dom->loadHTML( $html ) ) {\n\t\t\t$this->error = new WP_Error( 'llms-dom-document-error', __( 'DOMDocument XML Error encountered.', 'lifterlms' ), libxml_get_errors() );\n\t\t}\n\t}\n\n\t/**\n\t * Load the HTML string in the DOMDocument using the meta ut8 fixer\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tprivate function load_with_meta_utf_fixer() {\n\t\tif ( ! $this->dom->loadHTML( $this->utf8_fixer . $this->source ) ) {\n\t\t\t$this->error = new WP_Error( 'llms-dom-document-error', __( 'DOMDocument XML Error encountered.', 'lifterlms' ), libxml_get_errors() );\n\t\t\treturn;\n\t\t}\n\n\t\t// Remove the fixer meta element, if it's not removed it creates invalid HTML5 Markup.\n\t\t$meta = $this->dom->getElementById( 'llms-get-dom-doc-utf-fixer' );\n\t\tif ( $meta ) {\n\t\t\t$meta->parentNode->removeChild( $meta ); // phpcs:ignore: WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-elementor-migrate.php",
    "content": "<?php\n/**\n * Handle post migration to the Elementor widgets.\n *\n * @package LifterLMS/Classes\n *\n * @since 7.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Handle post migration to the new Elementor widgets.\n *\n * @since 7.7.0\n */\nclass LLMS_Elementor_Migrate {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.7.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'current_screen', array( $this, 'migrate_post' ) );\n\t\tadd_action( 'wp', array( $this, 'remove_template_hooks' ) );\n\t}\n\n\t/**\n\t * Retrieve the elementor data template.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_elementor_data_template() {\n\t\t$content = array();\n\n\t\t$content[] = array(\n\t\t\t'id'       => uniqid(),\n\t\t\t'elType'   => 'container',\n\t\t\t'settings' => array(),\n\t\t\t'elements' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t'settings'   => array(\n\t\t\t\t\t\t'content_width' => 'full',\n\t\t\t\t\t\t'html'          => '<h2>' . esc_attr__( 'Course Information', 'lifterlms' ) . '</h2>',\n\t\t\t\t\t),\n\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t'widgetType' => 'html',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'isInner'  => false,\n\t\t);\n\t\t$content[] = array(\n\t\t\t'id'       => uniqid(),\n\t\t\t'elType'   => 'container',\n\t\t\t'settings' => array(),\n\t\t\t'elements' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t'settings'   => array(),\n\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t'widgetType' => 'llms_course_meta_information_widget',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'isInner'  => false,\n\t\t);\n\t\t$content[] = array(\n\t\t\t'id'       => uniqid(),\n\t\t\t'elType'   => 'container',\n\t\t\t'settings' => array(),\n\t\t\t'elements' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t'settings'   => array(),\n\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t'widgetType' => 'llms_course_instructors_widget',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'isInner'  => false,\n\t\t);\n\t\t$content[] = array(\n\t\t\t'id'       => uniqid(),\n\t\t\t'elType'   => 'container',\n\t\t\t'settings' => array(),\n\t\t\t'elements' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t'settings'   => array(),\n\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t'widgetType' => 'llms_pricing_table_widget',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'isInner'  => false,\n\t\t);\n\t\t$content[] = array(\n\t\t\t'id'       => uniqid(),\n\t\t\t'elType'   => 'container',\n\t\t\t'settings' => array(),\n\t\t\t'elements' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t'settings'   => array(),\n\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t'widgetType' => 'llms_course_progress_widget',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'isInner'  => false,\n\t\t);\n\t\t$content[] = array(\n\t\t\t'id'       => uniqid(),\n\t\t\t'elType'   => 'container',\n\t\t\t'settings' => array(),\n\t\t\t'elements' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t'settings'   => array(),\n\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t'widgetType' => 'llms_course_continue_button_widget',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'isInner'  => false,\n\t\t);\n\t\t$content[] = array(\n\t\t\t'id'       => uniqid(),\n\t\t\t'elType'   => 'container',\n\t\t\t'settings' => array(),\n\t\t\t'elements' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t'settings'   => array(),\n\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t'widgetType' => 'llms_course_syllabus_widget',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'isInner'  => false,\n\t\t);\n\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Migrate posts created prior to the elementor updates to have default LifterLMS widgets.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return  void\n\t */\n\tpublic function migrate_post() {\n\n\t\tglobal $pagenow;\n\n\t\tif ( 'post.php' !== $pagenow ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! current_user_can( 'edit_posts' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$post_id = llms_filter_input( INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT );\n\t\t$post    = $post_id ? get_post( $post_id ) : false;\n\n\t\tif ( ! $post || ! isset( $_REQUEST['action'] ) || 'elementor' !== $_REQUEST['action'] || ! $this->should_migrate_post( $post_id ) || 'course' !== get_post_type( $post_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->ensure_elementor_data_present( $post_id );\n\t\t$this->add_template_to_post( $post_id );\n\t}\n\n\tpublic function add_template_to_post( $post_id ) {\n\t\t$content = get_post_meta( $post_id, '_elementor_data', true );\n\t\tif ( ! $content ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$decoded_content = json_decode( $content, true );\n\n\t\tif ( ! is_array( $decoded_content ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$decoded_content = array_merge( $decoded_content, $this->get_elementor_data_template() );\n\n\t\t$this->update_elementor_data( $post_id, $decoded_content );\n\t\t$this->update_migration_status( $post_id );\n\t}\n\n\t/**\n\t * Removes core template action hooks from posts which have been migrated to elementor widgets.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function remove_template_hooks() {\n\n\t\tif ( ! function_exists( 'llms_is_elementor_post' ) ||\n\t\t\t! llms_is_elementor_post() ||\n\t\t\t( get_the_ID() && ! llms_parse_bool( get_post_meta( get_the_ID(), '_llms_elementor_migrated', true ) ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_start', 5 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_length', 10 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_difficulty', 20 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tracks', 25 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_categories', 30 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tags', 35 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_end', 50 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_progress', 60 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_syllabus', 90 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_course_author', 40 );\n\t\tremove_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_pricing_table', 60 );\n\t}\n\n\t/**\n\t * Determine if a post should be migrated.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param int $post_id WP_Post ID.\n\t * @return bool\n\t */\n\tpublic function should_migrate_post( $post_id ) {\n\n\t\t$ret = ! llms_parse_bool( get_post_meta( $post_id, '_llms_elementor_migrated', true ) );\n\n\t\t/**\n\t\t * Filters whether or not a post should be migrated\n\t\t *\n\t\t * @since 7.7.0\n\t\t *\n\t\t * @param bool $migrate Whether or not a post should be migrated.\n\t\t * @param int  $post_id WP_Post ID.\n\t\t */\n\t\treturn apply_filters( 'llms_elementor_should_migrate_post', $ret, $post_id );\n\t}\n\n\t/**\n\t * Update post meta data to signal status of the editor migration.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param int    $post_id WP_Post ID.\n\t * @param string $status  Yes or no.\n\t * @return void\n\t */\n\tpublic function update_migration_status( $post_id, $status = 'yes' ) {\n\t\tupdate_post_meta( $post_id, '_llms_elementor_migrated', $status );\n\t}\n\n\tprivate function ensure_elementor_data_present( $post_id ): void {\n\t\t$content = json_decode( get_post_meta( $post_id, '_elementor_data', true ) );\n\n\t\tif ( ! is_array( $content ) && ( $post = get_post( $post_id ) ) ) {\n\t\t\t$content   = array();\n\t\t\t$content[] = array(\n\t\t\t\t'id'       => uniqid(),\n\t\t\t\t'elType'   => 'container',\n\t\t\t\t'settings' => array(),\n\t\t\t\t'elements' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'id'         => uniqid(),\n\t\t\t\t\t\t'elType'     => 'widget',\n\t\t\t\t\t\t'settings'   => array(\n\t\t\t\t\t\t\t'editor' => $post->post_content,\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'elements'   => array(),\n\t\t\t\t\t\t'widgetType' => 'text-editor',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'isInner'  => false,\n\t\t\t);\n\t\t\t$this->update_elementor_data( $post_id, $content );\n\t\t}\n\t}\n\n\tprivate function update_elementor_data( $post_id, $content ): void {\n\t\t// The trim and wp json encode are important. It doesn't seem to work with just json_encode, for example.\n\t\tupdate_post_meta( $post_id, '_elementor_data', trim( wp_slash( wp_json_encode( $content ) ), '\"' ) );\n\t}\n}\n\nglobal $llms_elementor_migrate;\n$llms_elementor_migrate = new LLMS_Elementor_Migrate();\nreturn $llms_elementor_migrate;\n"
  },
  {
    "path": "includes/class-llms-engagement-handler.php",
    "content": "<?php\n/**\n * LLMS_Engagement_Handler class file.\n *\n * @package LifterLMS/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Validate and generate or send engagement posts.\n *\n * Handles validation, dupchecking, and etc...\n *\n * For certificates and achievements the earned (\"_my_\") post type is created.\n *\n * For emails, the email is triggered and sending recorded in the user postmeta table.\n *\n * @since 6.0.0\n */\nclass LLMS_Engagement_Handler {\n\n\t/**\n\t * Create a new earned achievement or certificate.\n\t *\n\t * This method is called by handler callback functions run when engagements are triggered.\n\t *\n\t * Before arriving here the input data ($user_id, $template_id, etc...) has already been validated to ensure\n\t * that it exists and the engagement can be processed using this data.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string   $type          The engagement type, either \"achievement\" or \"certificate\".\n\t * @param int      $user_id       WP_User ID of the student earning the engagement.\n\t * @param int      $template_id   WP_Post ID of the template post (llms_achievement or llms_certificate).\n\t * @param string   $related_id    WP_Post ID of the triggering related post (course, lesson, etc...) or an empty string for user registration.\n\t * @param null|int $engagement_id WP_Post ID of the engagement post used to configure the trigger. A `null` value maybe be passed for legacy\n\t *                                delayed engagements which were created without an engagement ID or when manually awarding via the admin UI.\n\t * @return boolean|WP_Error[] $can_process An array of WP_Errors or true if the engagement can be processed.\n\t */\n\tprivate static function can_process( $type, $user_id, $template_id, $related_id = '', $engagement_id = null ) {\n\n\t\t/**\n\t\t * Skip engagement processing checks and force engagements to process.\n\t\t *\n\t\t * This filter is used internally to skip running checks for immediate engagements which cannot\n\t\t * suffer from the issues that these checks seek to avoid.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param boolean  $skip_checks   Whether or not to skip checks.\n\t\t * @param string   $type          The engagement type, either \"achievement\" or \"certificate\".\n\t\t * @param int      $user_id       WP_User ID of the student earning the engagement.\n\t\t * @param int      $template_id   WP_Post ID of the template post (llms_achievement or llms_certificate).\n\t\t * @param string   $related_id    WP_Post ID of the triggering related post (course, lesson, etc...) or an empty string for user registration.\n\t\t * @param null|int $engagement_id WP_Post ID of the engagement post used to configure the trigger. A `null` value maybe be passed for legacy\n\t\t *                                delayed engagements which were created without an engagement ID or when manually awarding via the admin UI.\n\t\t * }\n\t\t */\n\t\t$skip_checks = apply_filters( 'llms_skip_engagement_processing_checks', false, $type, $user_id, $template_id, $related_id, $engagement_id );\n\t\tif ( $skip_checks ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t$checks = array();\n\n\t\t// User must exist.\n\t\t$user_check = get_userdata( $user_id ) ? true : new WP_Error( 'llms-engagement-check-user--not-found', sprintf( __( 'User \"%d\" not found.', 'lifterlms' ), $user_id ) );\n\t\t$checks[]   = $user_check;\n\n\t\t// Template must be published and of the expected post type.\n\t\t$checks[] = self::check_post( $template_id, \"llms_{$type}\" );\n\n\t\t// Check related post (if one is passed).\n\t\tif ( ! empty( $related_id ) ) {\n\t\t\t$check_related = self::check_post( $related_id );\n\t\t\t$checks[]      = $check_related;\n\t\t\t// Check post enrollment if the check passed and there's no user issues.\n\t\t\tif ( ! is_wp_error( $check_related ) && ! is_wp_error( $user_check ) ) {\n\t\t\t\t$checks[] = self::check_post_enrollment( $related_id, $user_id );\n\t\t\t}\n\t\t}\n\n\t\t// Ensure we have an argument to check, engagements created prior to v6.0.0 will not have this argument.\n\t\tif ( ! empty( $engagement_id ) ) {\n\t\t\t$checks[] = self::check_post( $engagement_id, 'llms_engagement' );\n\t\t}\n\n\t\t// Find all the failed checks.\n\t\t$errors = array_values( array_filter( $checks, 'is_wp_error' ) );\n\n\t\t/**\n\t\t * Filters whether or not an engagement should be processed immediately prior to it being sent or awarded.\n\t\t *\n\t\t * The dynamic portion of this hook, `{$type}` refers to the type of engagement being processed, either \"email\",\n\t\t * \"certificate\", or \"achievement\".\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param boolean|WP_Error[] $can_process   An array of WP_Errors or true if the engagement can be processed.\n\t\t * @param int                $user_id       WP_User ID of the student earning the engagement.\n\t\t * @param int                $template_id   WP_Post ID of the template post (llms_achievement or llms_certificate).\n\t\t * @param string             $related_id    WP_Post ID of the triggering related post (course, lesson, etc...) or an empty string for user registration.\n\t\t * @param null|int           $engagement_id WP_Post ID of the engagement post used to configure the trigger. A `null` value maybe be passed for legacy\n\t\t *                                          delayed engagements which were created without an engagement ID or when manually awarding via the admin UI.\n\t\t * }\n\t\t */\n\t\treturn apply_filters( \"llms_proccess_{$type}_engagement\", count( $errors ) ? $errors : true, $user_id, $template_id, $related_id, $engagement_id );\n\n\t}\n\n\t/**\n\t * Apply deprecated creation filters based on the engagement type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array  $args Array of creation arguments.\n\t * @param string $type The engagement type, accepts \"achievement\" or \"certificate\".\n\t * @return array\n\t */\n\tprivate static function do_deprecated_creation_filters( $args, $type ) {\n\n\t\t$hooks = array(\n\t\t\t'achievement' => array( 'lifterlms_new_achievement', 'llms_achievement_get_creation_args' ),\n\t\t\t'certificate' => array( 'lifterlms_new_page', 'llms_certificate_get_creation_args' ),\n\t\t);\n\n\t\t$hook = $hooks[ $type ] ?? null;\n\t\tif ( ! $hook ) {\n\t\t\treturn $args;\n\t\t}\n\n\t\treturn apply_filters_deprecated( $hook[0], array( $args ), '6.0.0', $hook[1] );\n\n\t}\n\n\t/**\n\t * Handles deprecated filters which have additional parameters from now deprecated classes.\n\t *\n\t * If there are no callbacks attached to the deprecated hook the original $args is returned and no\n\t * warnings will be emitted.\n\t *\n\t * This instantiates an initialized instance of the deprecated class and passes it with the original filtered\n\t * argument through `apply_filters_deprecated`. This results in several deprecation warnings being emitted\n\t * but ensures that these filters can continue to work in a backwards compatible manner.\n\t *\n\t * This method is a public method but it is intentionally marked as private to denote its temporary lifespan. It will\n\t * be removed alongside the deprecated filters it calls as it will no longer be necessary when the deprecated\n\t * hooks are fully removed. As such, this method is considered private for the purposes of semantic versioning and\n\t * will removed in the next major release without being officially deprecated.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @access private\n\t *\n\t * @param mixed  $args         The filtered argument (not an array of arguments).\n\t * @param array  $init_args    {\n\t *      An array of arguments used to initialize the old object.\n\t *\n\t *     @type int        $0 WP_Post ID of the template post, either an `llms_certificate` or `llms_achievement`.\n\t *     @type int        $1 WP_User ID of the user.\n\t *     @type int|string $2 WP_Post ID of the related post or an empty string during user registration.\n\t * }\n\t * @param string $type        The engagement type, either \"achievement\" or \"certificate\".\n\t * @param string $deprecated  The deprecated filter to call.\n\t * @param string $replacement The replacement hook.\n\t * @return mixed\n\t */\n\tpublic static function do_deprecated_filter( $args, $init_args, $type, $deprecated, $replacement ) {\n\n\t\tif ( has_filter( $deprecated ) ) {\n\n\t\t\t$old_class = sprintf( 'LLMS_%s_User', strtoupper( $type ) );\n\n\t\t\t/**\n\t\t\t * Retains deprecated functionality where an instance of LLMS_Certificate_User is passed as a parameter to the filter.\n\t\t\t *\n\t\t\t * Since there's no good way to recreate that functionality we'll handle it in this manner\n\t\t\t * until `LLMS_Certificate_User` is removed.\n\t\t\t */\n\t\t\t$old_obj = new $old_class();\n\t\t\t$old_obj->init( ...$init_args );\n\t\t\t$args = apply_filters_deprecated( $deprecated, array( $args, $old_obj ), '6.0.0', $replacement );\n\t\t}\n\n\t\treturn $args;\n\n\t}\n\n\t/**\n\t * Create a new earned achievement or certificate.\n\t *\n\t * This method is called by handler callback functions run when engagements are triggered.\n\t *\n\t * Before arriving here the input data ($user_id, $template_id, etc...) has already been validated to ensure\n\t * that it exists and the engagement can be processed using this data.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string   $type          The engagement type, either \"achievement\" or \"certificate\".\n\t * @param int      $user_id       WP_User ID of the student earning the engagement.\n\t * @param int      $template_id   WP_Post ID of the template post (llms_achievement or llms_certificate).\n\t * @param string   $related_id    WP_Post ID of the triggering related post (course, lesson, etc...) or an empty string for user registration.\n\t * @param null|int $engagement_id WP_Post ID of the engagement post used to configure the trigger. A `null` value maybe be passed for legacy\n\t *                                delayed engagements which were created without an engagement ID or when manually awarding via the admin UI.\n\t * @return WP_Error|LLMS_User_Certificate|LLMS_User_Achievement\n\t */\n\tprivate static function create( $type, $user_id, $template_id, $related_id = '', $engagement_id = null ) {\n\n\t\t$title    = get_post_meta( $template_id, \"_llms_{$type}_title\", true );\n\t\t$template = get_post( $template_id );\n\n\t\t// Setup args, ultimately passed to `wp_insert_post()`.\n\t\t$post_args = array(\n\t\t\t'post_author'  => $user_id,\n\t\t\t'post_content' => $template->post_content,\n\t\t\t'post_date'    => llms_current_time( 'mysql' ),\n\t\t\t'post_name'    => 'certificate' === $type ? llms()->certificates()->get_unique_slug( $title ) : null,\n\t\t\t'post_parent'  => $template_id,\n\t\t\t'post_status'  => 'publish',\n\t\t\t'post_title'   => $title,\n\t\t\t'meta_input'   => array(\n\t\t\t\t'_thumbnail_id'    => self::get_image_id( $type, $template_id ),\n\t\t\t\t'_llms_engagement' => $engagement_id,\n\t\t\t\t'_llms_related'    => $related_id,\n\t\t\t),\n\t\t);\n\n\t\t// Do deprecated filters. No direct replacement added, instead use `LLMS_Post_Model` creation filters.\n\t\t$post_args = self::do_deprecated_creation_filters( $post_args, $type );\n\n\t\t$model_class = sprintf( 'LLMS_User_%s', ucwords( $type ) );\n\t\t$generated   = new $model_class( 'new', $post_args );\n\t\tif ( ! $generated || ! $generated->get( 'id' ) ) {\n\t\t\treturn new WP_Error( 'llms-engagement-init--create', __( 'An error was encountered during post creation.', 'lifterlms' ), compact( 'user_id', 'template_id', 'related_id', 'engagement_id', 'post_args', 'type', 'model_class' ) );\n\t\t}\n\n\t\t// Reinstantiate the class so the merged post_content will be retrieved if accessed immediately.\n\t\treturn new $model_class( $generated->get( 'id' ) );\n\n\t}\n\n\t/**\n\t * Runs post-creation actions when creating/awarding an achievement or certificate to a user.\n\t *\n\t * @param string          $type          The engagement type, either \"achievement\" or \"certificate\".\n\t * @param int             $user_id       WP_User ID of the student who earned the engagement.\n\t * @param int             $generated_id  WP_Post ID of the generated engagement post.\n\t * @param string|int|null $related_id    WP_Post ID of the related post triggering generation, an empty string (in the event of a user registration trigger) or null if not supplied.\n\t * @param int|null        $engagement_id WP_Post ID of the engagement post used to configure engagement triggering.\n\t *\n\t * @return void\n\t */\n\tpublic static function create_actions( $type, $user_id, $generated_id, $related_id = '', $engagement_id = null ) {\n\n\t\t// I think this should be removed but there's a lot of places where queries to _certificate_earned or _achievement_earned exist and it's the documented way of retrieving this data.\n\t\t// Internally we should switch to stop relying on this and figure out a way to phase out the usage of the user postmeta data but for now I think we'll continue storing it.\n\t\tllms_update_user_postmeta(\n\t\t\t$user_id,\n\t\t\t$related_id,\n\t\t\t\"_{$type}_earned\",\n\t\t\t$generated_id,\n\t\t\t// The earned engagement must be unique if a `$related_id` is present, otherwise it must be not.\n\t\t\t// Manual awarding have no `$related_id`, and if we force the uniquiness we will end up updating always the same earned engagement\n\t\t\t// every time we manually award a new one for the same user.\n\t\t\t(bool) $related_id\n\t\t);\n\n\t\t/**\n\t\t * Action run after a student has successfully earned an engagement.\n\t\t *\n\t\t * The dynamic portion of this hook, `{$type}`, refers to the engagement type,\n\t\t * either \"achievement\" or \"certificate\".\n\t\t *\n\t\t * @since 1.0.0\n\t\t * @since 6.0.0 Added the `$engagement_id` parameter.\n\t\t *\n\t\t * @param int             $user_id       WP_User ID of the student who earned the engagement.\n\t\t * @param int             $generated_id  WP_Post ID of the generated engagement post.\n\t\t * @param string|int|null $related_id    WP_Post ID of the related post triggering generation, an empty string (in the event of a user registration trigger) or null if not supplied.\n\t\t * @param int|null        $engagement_id WP_Post ID of the engagement post used to configure engagement triggering.\n\t\t */\n\t\tdo_action(\n\t\t\t\"llms_user_earned_{$type}\",\n\t\t\t$user_id,\n\t\t\t$generated_id,\n\t\t\t$related_id,\n\t\t\t$engagement_id\n\t\t);\n\n\t}\n\n\t/**\n\t * Validates a post id submitted to an engagement handler callback function.\n\t *\n\t * This ensures the following is true:\n\t *   + The post must exist\n\t *   + It must be published\n\t *   + Optionally, it must match the specified post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int    $post_id   WP_Post ID.\n\t * @param string $post_type The expected post type.\n\t * @return WP_Error|boolean Returns `true` if all checks pass, otherwise returns a `WP_Error`.\n\t */\n\tpublic static function check_post( $post_id, $post_type = null ) {\n\n\t\t$post = get_post( $post_id );\n\t\tif ( ! $post ) {\n\t\t\t// Translators: %d = the WP_Post ID.\n\t\t\treturn new WP_Error( 'llms-engagement-post--not-found', sprintf( __( 'Post \"%d\" not found.', 'lifterlms' ), $post_id ), compact( 'post_id' ) );\n\t\t}\n\n\t\tif ( 'publish' !== $post->post_status ) {\n\t\t\t// Translators: %d = the WP_Post ID.\n\t\t\treturn new WP_Error( 'llms-engagement-post--status', sprintf( __( 'Post \"%d\" is not published.', 'lifterlms' ), $post_id ), compact( 'post' ) );\n\t\t}\n\n\t\tif ( $post_type && $post_type !== $post->post_type ) {\n\t\t\t// Translators: %d = the WP_Post ID.\n\t\t\treturn new WP_Error( 'llms-engagement-post--type', sprintf( __( 'Post \"%d\" is not the expected post type.', 'lifterlms' ), $post_id ), compact( 'post' ) );\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Check that the specified user is enrolled in the given post.\n\t *\n\t * This check will return true when running against non-enrollable post types.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $post_id WP_Post ID.\n\t * @param int $user_id WP_User ID.\n\t * @return WP_Error|boolean Returns `true` if the check passes, otherwise returns a `WP_Error`.\n\t */\n\tprivate static function check_post_enrollment( $post_id, $user_id ) {\n\n\t\t$type  = get_post_type( $post_id );\n\t\t$types = llms_get_enrollable_status_check_post_types();\n\n\t\t// If the post type is an enrollable post type, check enrollment.\n\t\tif ( in_array( $type, $types, true ) && ! llms_is_user_enrolled( $user_id, $post_id ) ) {\n\t\t\t// Translators: %1$d = WP_User ID; %2$d = WP_Post ID.\n\t\t\treturn new WP_Error( 'llms-engagement-check-post--enrollment', sprintf( __( 'User \"%1$d\" is not enrolled in \"%2$d\".', 'lifterlms' ), $user_id, $post_id ), compact( 'post_id', 'user_id' ) );\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Check if the engagement for the specified template and related post has already been earned / awarded to a given user.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $type          Engagement type, either \"certificate\" or \"achievement\".\n\t * @param int    $user_id       WP_User ID of the user earning the engagement.\n\t * @param int    $template_id   WP_Post ID of the template post, either an `llms_certificate` or an `llms_achievement`.\n\t * @param string $related_id    WP_Post ID of the related post or an empty string during user registration.\n\t * @param int    $engagement_id WP_Post ID of the `llms_engagement` post type.\n\t * @return WP_Error|boolean Returns `true` if the dupcheck passes otherwise returns an error object.\n\t */\n\tprivate static function dupcheck( $type, $user_id, $template_id, $related_id = '', $engagement_id = null ) {\n\n\t\t$student = llms_get_student( $user_id );\n\n\t\t$query = new LLMS_Awards_Query(\n\t\t\tarray(\n\t\t\t\t'type'          => $type,\n\t\t\t\t'users'         => $user_id,\n\t\t\t\t'templates'     => $template_id,\n\t\t\t\t'related_posts' => $related_id,\n\t\t\t\t'fields'        => 'ids',\n\t\t\t\t'no_found_rows' => true,\n\t\t\t\t'per_page'      => 1,\n\t\t\t)\n\t\t);\n\n\t\t$is_duplicate = self::do_deprecated_filter(\n\t\t\t$query->has_results(),\n\t\t\tarray( $template_id, $user_id, $related_id ),\n\t\t\t$type,\n\t\t\t\"llms_{$type}_has_user_earned\",\n\t\t\t\"llms_earned_{$type}_dupcheck\"\n\t\t);\n\n\t\t/**\n\t\t * Filters whether or not the given user has already earned a certificate or achievement.\n\t\t *\n\t\t * The dynamic portion of this hook, `{$type}`, refers to the type of engagement, either\n\t\t * \"achievement\" or \"certificate\".\n\t\t *\n\t\t * This filter should return `true` or a `WP_Error` to denote the certificate has already been earned and\n\t\t * `false` to denote that it has not.\n\t\t *\n\t\t * If `true` is returned the default error message will be used.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param boolean $is_duplicate Whether or not the engagement has already been earned.\n\t\t */\n\t\t$is_duplicate = apply_filters(\n\t\t\t\"llms_earned_{$type}_dupcheck\",\n\t\t\t$is_duplicate,\n\t\t\t$user_id,\n\t\t\t$template_id,\n\t\t\t$related_id,\n\t\t\t$engagement_id\n\t\t);\n\n\t\tif ( true === $is_duplicate ) {\n\t\t\t$is_duplicate = new WP_Error(\n\t\t\t\t'llms-engagement--is-duplicate',\n\t\t\t\t// Translators: %s = the WP_User ID.\n\t\t\t\tsprintf( __( 'User \"%s\" has already earned this engagement.', 'lifterlms' ), $user_id ),\n\t\t\t\tcompact( 'type', 'user_id', 'template_id', 'related_id', 'engagement_id' )\n\t\t\t);\n\t\t}\n\n\t\treturn is_wp_error( $is_duplicate ) ? $is_duplicate : true;\n\n\t}\n\n\t/**\n\t * Retrieve the attachment id to use for the earned engagement thumbnail.\n\t *\n\t * Retrieves the template's featured image ID and validates and then falls back to the site's\n\t * global default image option.\n\t *\n\t * If no global option is found, returns `0`. During front-end display, the hardcoded image will be used\n\t * in the template if the earned engagement's thumbnail is set to a fasly.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $type        Type of engagement, either \"achievement\" or \"certificate\".\n\t * @param int    $template_id WP_Post ID of the template post.\n\t * @return int WP_Post ID of the attachment or `0` when none found.\n\t */\n\tpublic static function get_image_id( $type, $template_id ) {\n\n\t\t$img_id = get_post_meta( $template_id, '_thumbnail_id', true );\n\n\t\tif ( $img_id && get_post( $img_id ) ) {\n\t\t\treturn absint( $img_id );\n\t\t}\n\n\t\tif ( 'achievement' === $type ) {\n\t\t\treturn llms()->achievements()->get_default_image_id();\n\t\t}\n\n\t\tif ( 'certificate' === $type ) {\n\t\t\treturn llms()->certificates()->get_default_image_id();\n\t\t}\n\n\t\treturn 0;\n\n\t}\n\n\t/**\n\t * Handle validation and creation of an earned achievement or certificate.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $type Type of engagement, either \"achievement\" or \"certificate\".\n\t * @param array  $args {\n\t *      Indexed array of arguments.\n\t *\n\t *     @type int        $0 WP_User ID.\n\t *     @type int        $1 WP_Post ID of the achievement or certificate template post.\n\t *     @type int|string $2 WP_Post ID of the related post that triggered the award or an empty string.\n\t *     @type int        $3 WP_Post ID of the engagement post.\n\t * }\n\t * @return WP_Error[]|LLMS_User_Achiemvent|LLMS_User_Certificate An array of errors or the earned engagement object\n\t */\n\tprivate static function handle( $type, $args ) {\n\n\t\t$can_process = self::can_process( $type, ...$args );\n\t\tif ( true !== $can_process ) {\n\t\t\treturn $can_process;\n\t\t}\n\n\t\t$dupcheck = self::dupcheck( $type, ...$args );\n\t\tif ( true !== $dupcheck ) {\n\t\t\treturn array( $dupcheck );\n\t\t}\n\n\t\treturn self::create( $type, ...$args );\n\n\t}\n\n\t/**\n\t * Award an achievement\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $args {\n\t *     Indexed array of arguments.\n\t *\n\t *     @type int        $0 WP_User ID.\n\t *     @type int        $1 WP_Post ID of the achievement template post.\n\t *     @type int|string $2 WP_Post ID of the related post that triggered the award or an empty string.\n\t *     @type int        $3 WP_Post ID of the engagement post.\n\t * }\n\t * @return WP_Error[]|LLMS_User_Achievement Returns an array of error objects on failure or the generated achievement object on success.\n\t */\n\tpublic static function handle_achievement( $args ) {\n\t\treturn self::handle( 'achievement', $args );\n\t}\n\n\t/**\n\t * Award an certificate\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $args {\n\t *     Indexed array of arguments.\n\t *\n\t *     @type int        $0 WP_User ID.\n\t *     @type int        $1 WP_Post ID of the certificate template post.\n\t *     @type int|string $2 WP_Post ID of the related post that triggered the award or an empty string.\n\t *     @type int        $3 WP_Post ID of the engagement post.\n\t * }\n\t * @return WP_Error[]|LLMS_User_Certificate Returns an array of error objects on failure or the generated certificate object on success.\n\t */\n\tpublic static function handle_certificate( $args ) {\n\t\treturn self::handle( 'certificate', $args );\n\t}\n\n\t/**\n\t * Send an email engagement\n\t *\n\t * This is called via do_action() by the 'maybe_trigger_engagement' function in this class.\n\t *\n\t * @since 2.3.0\n\t * @since 3.8.0 Unknown.\n\t * @since 4.4.1 Use postmeta helpers for dupcheck and postmeta insertion.\n\t *              Add a return value in favor of `void`.\n\t *              Log successes and failures to the `engagement-emails` log file instead of the main `llms` log.\n\t * @since 4.4.3 Fixed different emails triggered by the same related post not sent because of a wrong duplicate check.\n\t *              Fixed dupcheck log message and error message which reversed the email and person order.\n\t * @since 6.0.0 Moved from `LLMS_Engagements` class.\n\t *                Removed engagement debug logging.\n\t *                Ensure related post, email template, and engagement all exist and are published before processing.\n\t *\n\t * @param mixed[] $args {\n\t *     An array of arguments from the triggering hook.\n\t *\n\t *     @type int        $0 WP_User ID.\n\t *     @type int        $1 WP_Post ID of the email.\n\t *     @type int|string $2 WP_Post ID of the related triggering post or an empty string for engagements with no related post.\n\t *     @type int        $3 WP_Post ID of the engagement post.\n\t * }\n\t * @return bool|WP_Error[] Returns `true` on success and array of error objects when the email has failed or is prevented.\n\t */\n\tpublic static function handle_email( $args ) {\n\n\t\t$can_process = self::can_process( 'email', ...$args );\n\t\tif ( true !== $can_process ) {\n\t\t\treturn $can_process;\n\t\t}\n\n\t\tlist( $person_id, $email_id, $related_id ) = $args;\n\n\t\t$meta_key = '_email_sent';\n\n\t\t$msg = sprintf( __( 'Email #%1$d to user #%2$d triggered by %3$s', 'lifterlms' ), $email_id, $person_id, $related_id ? '#' . $related_id : 'N/A' );\n\n\t\tif ( $related_id && absint( $email_id ) === absint( llms_get_user_postmeta( $person_id, $related_id, $meta_key ) ) ) {\n\n\t\t\t// User has already received this email, don't send it again.\n\t\t\tllms_log( $msg . ' ' . __( 'not sent because of dupcheck.', 'lifterlms' ), 'engagement-emails' );\n\t\t\treturn array( new WP_Error( 'llms_engagement_email_not_sent_dupcheck', $msg, $args ) );\n\n\t\t}\n\n\t\t// Setup the email.\n\t\t$email = llms()->mailer()->get_email( 'engagement', compact( 'person_id', 'email_id', 'related_id' ) );\n\t\tif ( $email && $email->send() ) {\n\n\t\t\tif ( $related_id ) {\n\t\t\t\tllms_update_user_postmeta( $person_id, $related_id, $meta_key, $email_id );\n\t\t\t}\n\n\t\t\tllms_log( $msg . ' ' . __( 'sent successfully.', 'lifterlms' ), 'engagement-emails' );\n\t\t\treturn true;\n\t\t}\n\n\t\t// Error sending email.\n\t\tllms_log( $msg . ' ' . __( 'not sent due to email sending issues.', 'lifterlms' ), 'engagement-emails' );\n\t\treturn array( new WP_Error( 'llms_engagement_email_not_sent_error', $msg, $args ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-events-core.php",
    "content": "<?php\n/**\n * Record events triggered by core/wp actions.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.36.0\n * @version 3.36.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Events_Core class\n *\n * @since 3.36.0\n */\nclass LLMS_Events_Core {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'wp_login', array( $this, 'on_signon' ), 10, 2 );\n\t\tadd_action( 'clear_auth_cookie', array( $this, 'on_signout' ) );\n\n\t}\n\n\t/**\n\t * Record account.signon event via `wp_login` hook.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param string  $username WP_Users's user_login.\n\t * @param WP_User $user     User object.\n\t * @return LLMS_Event\n\t */\n\tpublic function on_signon( $username, $user ) {\n\n\t\treturn llms()->events()->record(\n\t\t\tarray(\n\t\t\t\t'actor_id'     => $user->ID,\n\t\t\t\t'object_type'  => 'user',\n\t\t\t\t'object_id'    => $user->ID,\n\t\t\t\t'event_type'   => 'account',\n\t\t\t\t'event_action' => 'signon',\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Record an account.signout event via `wp_logout()`\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Return `false` without recording any event if no user was logged in.\n\t *\n\t * @return LLMS_Event|false The instance of the `LLMS_Event` recorded or `false` when no user was logged in.\n\t */\n\tpublic function on_signout() {\n\n\t\t$uid = get_current_user_id();\n\n\t\tif ( ! $uid ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn llms()->events()->record(\n\t\t\tarray(\n\t\t\t\t'actor_id'     => $uid,\n\t\t\t\t'object_type'  => 'user',\n\t\t\t\t'object_id'    => $uid,\n\t\t\t\t'event_type'   => 'account',\n\t\t\t\t'event_action' => 'signout',\n\t\t\t)\n\t\t);\n\n\t}\n\n}\n\nreturn new LLMS_Events_Core();\n"
  },
  {
    "path": "includes/class-llms-events-query.php",
    "content": "<?php\n/**\n * Perform db queries for events\n *\n * @package LifterLMS/Classes\n *\n * @since 3.36.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Events_Query class\n *\n * @since 3.36.0\n */\nclass LLMS_Events_Query extends LLMS_Database_Query {\n\n\t/**\n\t * Identify the Query\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'events';\n\n\t/**\n\t * Retrieve default arguments for a query\n\t *\n\t * @since 3.36.0\n\t * @since 4.7.0 Drop usage of `this->get_filter( 'default_args' )` in favor of `'llms_events_query_default_args'`.\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_args() {\n\n\t\t$args = array(\n\t\t\t'actor'         => array(),\n\t\t\t'actor_not_in'  => array(),\n\t\t\t'date_after'    => '',\n\t\t\t'date_before'   => '',\n\t\t\t'exclude'       => array(),\n\t\t\t'include'       => array(),\n\t\t\t'object_type'   => '',\n\t\t\t'object'        => array(),\n\t\t\t'object_not_in' => array(),\n\t\t\t'event_type'    => '',\n\t\t\t'event_action'  => '',\n\t\t\t'sort'          => array(\n\t\t\t\t'date' => 'DESC',\n\t\t\t),\n\t\t);\n\n\t\t$args = wp_parse_args( $args, parent::get_default_args() );\n\n\t\tif ( $args['suppress_filters'] ) {\n\t\t\treturn $args;\n\t\t}\n\n\t\t/**\n\t\t * Filters the events query default args\n\t\t *\n\t\t * @since 3.36.0\n\t\t *\n\t\t * @param array             $args         Array of default arguments to set up the query with.\n\t\t * @param LLMS_Events_Query $events_query Instance of LLMS_Events_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_events_query_default_args', $args, $this );\n\n\t}\n\n\t/**\n\t * Retrieve an array of LLMS_Event objects for the given result set returned by the query\n\t *\n\t * @since 3.36.0\n\t * @since 4.7.0 Drop usage of `$this->get_filter('get_events')` in favor of `'llms_events_query_get_events'`.\n\t *\n\t * @return array\n\t */\n\tpublic function get_events() {\n\n\t\t$events  = array();\n\t\t$results = $this->get_results();\n\n\t\tif ( $results ) {\n\n\t\t\tforeach ( $results as $result ) {\n\t\t\t\t$events[] = new LLMS_Event( $result->id, true );\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $events;\n\t\t}\n\n\t\t/**\n\t\t * Filters the list of events\n\t\t *\n\t\t * @since 3.36.0\n\t\t *\n\t\t * @param LLMS_Event[]      $events       Array of LLMS_Event instances.\n\t\t * @param LLMS_Events_Query $events_query Instance of LLMS_Events_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_events_query_get_events', $events, $this );\n\n\t}\n\n\t/**\n\t * Parses argument data\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tprotected function parse_args() {\n\n\t\t// Sanitize post & user ids.\n\t\tforeach ( array( 'actor', 'actor_not_in', 'object', 'object_not_in', 'include', 'exclude' ) as $key ) {\n\t\t\t$this->arguments[ $key ] = $this->sanitize_id_array( $this->arguments[ $key ] );\n\t\t}\n\n\t\tforeach ( array( 'date_before', 'date_after' ) as $key ) {\n\t\t\tif ( ! empty( $this->arguments[ $key ] ) ) {\n\t\t\t\t$date = $this->arguments[ $key ];\n\t\t\t\tif ( ! is_numeric( $date ) ) {\n\t\t\t\t\t$date = strtotime( $date );\n\t\t\t\t}\n\t\t\t\t$this->arguments[ $key ] = date( 'Y-m-d H:i:s', $date );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Prepare the SQL for the query.\n\t *\n\t * @since 3.36.0\n\t * @since 4.7.0 Use `$this->sql_select_columns({columns})` to determine the columns to select.\n\t * @since 6.0.0 Renamed from `preprare_query()`.\n\t * @since 10.0.0 Build count_query from shared clauses instead of using SQL_CALC_FOUND_ROWS.\n\t *\n\t * @return string\n\t */\n\tprotected function prepare_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$from  = \"FROM {$wpdb->prefix}lifterlms_events\";\n\t\t$where = $this->sql_where();\n\n\t\tif ( ! $this->get( 'no_found_rows' ) ) {\n\t\t\t$this->count_query = \"SELECT COUNT(*) {$from} {$where}\";\n\t\t}\n\n\t\treturn \"SELECT {$this->sql_select_columns( 'id' )}\n\t\t\t\t{$from}\n\t\t\t\t{$where}\n\t\t\t\t{$this->sql_orderby()}\n\t\t\t\t{$this->sql_limit()};\";\n\n\t}\n\n\t/**\n\t * SQL \"where\" clause for the query\n\t *\n\t * @since 3.36.0\n\t * @since 4.7.0 Drop usage of `$this->get_filter('where')` in favor of `'llms_events_query_where'`.\n\t *\n\t * @return string\n\t */\n\tprotected function sql_where() {\n\n\t\tglobal $wpdb;\n\n\t\t$sql = 'WHERE 1';\n\n\t\t// \"IN\" clauses for id fields.\n\t\t$ids_include = array(\n\t\t\t'actor'   => 'actor_id',\n\t\t\t'object'  => 'object_id',\n\t\t\t'include' => 'id',\n\t\t);\n\t\tforeach ( $ids_include as $query_key => $db_key ) {\n\t\t\t$ids = $this->get( $query_key );\n\t\t\tif ( $ids ) {\n\t\t\t\t$prepared = implode( ',', $ids );\n\t\t\t\t$sql     .= \" AND {$db_key} IN ({$prepared})\";\n\t\t\t}\n\t\t}\n\n\t\t// \"NOT IN\" clauses for id fields.\n\t\t$ids_exclude = array(\n\t\t\t'actor_not_in'  => 'actor_id',\n\t\t\t'object_not_in' => 'object_id',\n\t\t\t'exclude'       => 'id',\n\t\t);\n\t\tforeach ( $ids_exclude as $query_key => $db_key ) {\n\t\t\t$ids = $this->get( $query_key );\n\t\t\tif ( $ids ) {\n\t\t\t\t$prepared = implode( ',', $ids );\n\t\t\t\t$sql     .= \" AND {$db_key} NOT IN ({$prepared})\";\n\t\t\t}\n\t\t}\n\n\t\t// Matching fields.\n\t\t$matching = array( 'object_type', 'event_type', 'event_action' );\n\t\tforeach ( $matching as $key ) {\n\t\t\t$val = $this->get( $key );\n\t\t\tif ( $val ) {\n\t\t\t\t$sql .= sprintf( \" AND {$key} = '%s'\", esc_sql( $val ) );\n\t\t\t}\n\t\t}\n\n\t\t// Date fields.\n\t\t$before = $this->get( 'date_before' );\n\t\tif ( $before ) {\n\t\t\t$sql .= $wpdb->prepare( ' AND date < %s', $before );\n\t\t}\n\n\t\t$after = $this->get( 'date_after' );\n\t\tif ( $after ) {\n\t\t\t$sql .= $wpdb->prepare( ' AND date > %s', $after );\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query WHERE clause\n\t\t *\n\t\t * @since 3.36.0\n\t\t *\n\t\t * @param string            $sql          The WHERE clause of the query.\n\t\t * @param LLMS_Events_Query $events_query Instance of LLMS_Events_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_events_query_where', $sql, $this );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-events.php",
    "content": "<?php\n/**\n * LifterLMS Event management.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.36.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Events class.\n *\n * @since 3.36.0\n * @since 3.36.1 Improve performances when checking if an event is valid in `LLMS_Events->is_event_valid()`.\n *               Remove redundant check on `is_singular()` and `is_post_type_archive()` in `LLMS_Events->should_track_client_events()`.\n * @since 3.37.14 Added `store_tracking_events()` method.\n *                Moved most of the `store_cookie()` method's logic into `store_tracking_events()`.\n * @since 3.37.15 Excluded `page.*` events in order to keep the events table small.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Removed the deprecated `LLMS_Events::$_instance` property.\n */\nclass LLMS_Events {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * List of registered event types.\n\t *\n\t * @var array\n\t */\n\tprotected $registered_events = array();\n\n\t/**\n\t * Private Constructor\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Register events at `init` hook with priority 9 in place of 10.\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\tadd_action( 'init', array( $this, 'register_events' ), 9 );\n\t\tadd_action( 'init', array( $this, 'store_cookie' ) );\n\t}\n\n\t/**\n\t * Retrieves an array of client settings used to initialize the JS Tracking instance on the frontend.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_client_settings() {\n\n\t\t$events = ! $this->should_track_client_events() ? array() : array_keys( array_filter( $this->get_registered_events() ) );\n\n\t\t/**\n\t\t * Filter client-side tracking settings\n\t\t *\n\t\t * @since 3.36.0\n\t\t *\n\t\t * @param array $settings {\n\t\t *     Hash of client-side settings.\n\t\t *\n\t\t *     @type string $nonce Nonce used to verify client-side events.\n\t\t *     @type string[] $events Array of events that should be tracked.\n\t\t * }\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_events_get_client_settings',\n\t\t\tarray(\n\t\t\t\t'nonce'            => wp_create_nonce( 'llms-tracking' ),\n\t\t\t\t'events'           => $events,\n\t\t\t\t'saving_frequency' => get_option( 'lifterlms_tracked_event_saving_frequency', 'minimum' ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an array of valid events.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return array Array key is the event name and array value is used to determine if the key is a client-side event.\n\t */\n\tpublic function get_registered_events() {\n\t\treturn $this->registered_events;\n\t}\n\n\t/**\n\t * Determine if the event string is registered and valid.\n\t *\n\t * @since 3.36.0\n\t * @since 3.36.1 Use more performant `array_key_exists( $key, $array_assoc )` in place of `in_array( $key, array_keys( $array_assoc ), true )`.\n\t *\n\t * @param string $event Event string ({$event_type}.{$event_action}). EG: \"account.signon\".\n\t * @return bool\n\t */\n\tprotected function is_event_valid( $event ) {\n\n\t\treturn array_key_exists( $event, $this->get_registered_events() );\n\t}\n\n\t/**\n\t * Prepares partial events from client-side event data.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param array $raw_event Raw event from client-side data.\n\t * @return array\n\t */\n\tpublic function prepare_event( $raw_event = array() ) {\n\n\t\tif ( ! isset( $raw_event['event'] ) ) {\n\t\t\t// Translators: %s = Event field key.\n\t\t\treturn new WP_Error( 'llms_events_missing_event', sprintf( __( 'The event is missing the \"%s\" field.', 'lifterlms' ), 'event' ) );\n\t\t}\n\n\t\t$event    = explode( '.', $raw_event['event'] );\n\t\t$prepared = array(\n\t\t\t'actor_id'     => get_current_user_id(),\n\t\t\t'event_type'   => $event[0],\n\t\t\t'event_action' => $event[1],\n\t\t\t'meta'         => isset( $raw_event['meta'] ) ? $raw_event['meta'] : array(),\n\t\t);\n\n\t\t// Convert timestamps to MYSQL date.\n\t\tif ( isset( $raw_event['time'] ) && is_numeric( $raw_event['time'] ) ) {\n\t\t\t$prepared['date'] = date( 'Y-m-d H:i:s', $raw_event['time'] );\n\t\t}\n\n\t\tif ( isset( $raw_event['url'] ) ) {\n\t\t\t$id = url_to_postid( $raw_event['url'] );\n\t\t\tif ( ! $id ) {\n\t\t\t\t// Translators: %s = URL.\n\t\t\t\treturn new WP_Error( 'llms_events_invalid_url', sprintf( __( 'The URL \"%s\" cannot be mapped to a valid post object.', 'lifterlms' ), esc_url( $raw_event['url'] ) ) );\n\t\t\t}\n\t\t\t$prepared['object_id']   = $id;\n\t\t\t$prepared['object_type'] = str_replace( 'llms_', '', get_post_type( $id ) );\n\t\t} elseif ( isset( $raw_event['object_id'] ) && isset( $raw_event['object_type'] ) ) {\n\t\t\t$prepared['object_id']   = $raw_event['object_id'];\n\t\t\t$prepared['object_type'] = $raw_event['object_type'];\n\t\t}\n\n\t\treturn $prepared;\n\t}\n\n\t/**\n\t * Store an event in the database.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Fixed event session end not recorded on sign-out.\n\t *\n\t * @param array $args {\n\t *     Event data\n\t *\n\t *     @type int $actor_id WP_User ID.\n\t *     @type string $object_type Type of object being acted upon (post,user,comment,etc...).\n\t *     @type int $object_id WP_Post ID, WP_User ID, WP_Comment ID, etc...\n\t *     @type string $event_type Type of event (account, page, course, etc...).\n\t *     @type string $event_action The event action or verb (signon,viewed,launched,etc...).\n\t * }\n\t * @return LLMS_Event|WP_Error\n\t */\n\tpublic function record( $args = array() ) {\n\n\t\t$err = new WP_Error();\n\n\t\tforeach ( array( 'actor_id', 'object_type', 'object_id', 'event_type', 'event_action' ) as $key ) {\n\t\t\tif ( ! in_array( $key, array_keys( $args ), true ) ) {\n\t\t\t\t// Translators: %s = key name of the missing field.\n\t\t\t\t$err->add( 'llms_event_record_missing_field', sprintf( __( 'Missing required field: \"%s\".', 'lifterlms' ), $key ) );\n\t\t\t}\n\t\t}\n\n\t\tif ( $err->get_error_codes() ) {\n\t\t\treturn $err;\n\t\t}\n\n\t\t$event = sprintf( '%1$s.%2$s', $args['event_type'], $args['event_action'] );\n\n\t\tif ( ! $this->is_event_valid( $event ) ) {\n\t\t\t// Translators: %s = Submitted event string.\n\t\t\treturn new WP_Error( 'llms_event_record_invalid_event', sprintf( __( 'The event \"%s\" is invalid.', 'lifterlms' ), $event ) );\n\t\t}\n\n\t\t$args = $this->sanitize_raw_event( $args );\n\t\t$meta = isset( $args['meta'] ) ? $args['meta'] : null;\n\t\tunset( $args['meta'] );\n\n\t\tif ( ! in_array( $event, array( 'session.start', 'session.end' ), true ) ) {\n\n\t\t\t// Start a session if one isn't open.\n\t\t\t$sessions = LLMS_Sessions::instance();\n\t\t\t$user_id  = 'account.signon' === $event && isset( $args['actor_id'] ) ? $args['actor_id'] : null;\n\n\t\t\tif ( false === $sessions->get_current( $user_id ) ) {\n\t\t\t\t$sessions->start( $user_id );\n\t\t\t}\n\t\t}\n\n\t\t$llms_event = new LLMS_Event();\n\t\tif ( ! $llms_event->setup( $args )->save() ) {\n\t\t\t$err->add( 'llms_event_recored_unknown_error', __( 'An unknown error occurred during event creation.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\t\tif ( $meta && ! empty( $meta ) ) {\n\t\t\t$llms_event->set_metas( $meta, true );\n\t\t}\n\n\t\t// End the current session on signout.\n\t\tif ( 'account.signout' === $event ) {\n\t\t\tLLMS_Sessions::instance()->end_current();\n\t\t}\n\n\t\treturn $llms_event;\n\t}\n\n\t/**\n\t * Record multiple events.\n\t *\n\t * Events are recorded with an SQL transaction. If any errors are encountered the transaction is rolled back (not events are recorded).\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param array[] $events Array of event hashes. See LLMS_Events::record() for hash description.\n\t * @return LLMS_Event[]|WP_Error Array of recorded events on success or WP_Error on failure.\n\t */\n\tpublic function record_many( $events = array() ) {\n\n\t\tglobal $wpdb;\n\t\t$wpdb->query( 'START TRANSACTION' );\n\n\t\t$recorded = array();\n\t\t$errors   = array();\n\t\tforeach ( $events as $event ) {\n\n\t\t\t$stat = $this->record( $event );\n\t\t\tif ( is_wp_error( $stat ) ) {\n\t\t\t\t$stat->add_data( $event );\n\t\t\t\t$errors[] = $stat;\n\t\t\t} else {\n\t\t\t\t$recorded[] = $stat;\n\t\t\t}\n\t\t}\n\n\t\tif ( count( $errors ) ) {\n\t\t\t$wpdb->query( 'ROLLBACK' );\n\t\t\treturn new WP_Error( 'llms_events_record_many_errors', __( 'There was one or more errors encountered while recording the events.', 'lifterlms' ), $errors );\n\t\t}\n\n\t\t$wpdb->query( 'COMMIT' );\n\n\t\treturn $recorded;\n\t}\n\n\t/**\n\t * Register event types\n\t *\n\t * @since 3.36.0\n\t * @since 3.37.15 Excluded `page.*` events in order to keep the events table small.\n\t *\n\t * @return void\n\t */\n\tpublic function register_events() {\n\n\t\t$events = array(\n\t\t\t'account.signon'  => false,\n\t\t\t'account.signout' => false,\n\t\t\t'session.start'   => false,\n\t\t\t'session.end'     => false,\n\n\t\t\t/*\n\t\t\t'page.load'       => true,\n\t\t\t'page.exit'       => true,\n\t\t\t'page.focus'      => true,\n\t\t\t'page.blur'       => true,\n\t\t\t*/\n\t\t);\n\n\t\t/**\n\t\t * Filter the list of registered events.\n\t\t *\n\t\t * Allows 3rd parties to register (or unregister) tracked events.\n\t\t *\n\t\t * @since 3.36.0\n\t\t *\n\t\t * @param array $events Array of events. Array key is the event name and array value is used to determine if the key is a client-side event.\n\t\t */\n\t\t$this->registered_events = apply_filters( 'llms_get_registered_events', $events );\n\t}\n\n\t/**\n\t * Recursively sanitize event data.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param array $raw Event information array.\n\t * @return array\n\t */\n\tprotected function sanitize_raw_event( $raw ) {\n\n\t\t$clean = array();\n\n\t\tforeach ( $raw as $key => $val ) {\n\n\t\t\t// This will recursively handle any metadata submitted.\n\t\t\tif ( is_array( $val ) ) {\n\t\t\t\t$val = $this->sanitize_raw_event( $val );\n\t\t\t} elseif ( in_array( $key, array( 'actor_id', 'object_id' ), true ) ) {\n\t\t\t\t// cast id fields to int.\n\t\t\t\t$val = absint( $val );\n\t\t\t} else {\n\t\t\t\t// everything else is a text field.\n\t\t\t\t$val = sanitize_text_field( $val );\n\t\t\t}\n\n\t\t\t// Sanitize the key. This will ensure no dirty keys are submitted in metadata.\n\t\t\t$key = is_numeric( $key ) ? $key : sanitize_text_field( $key );\n\n\t\t\t$clean[ $key ] = $val;\n\n\t\t}\n\n\t\treturn $clean;\n\t}\n\n\t/**\n\t * Determine if client side events from the current page should be tracked.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function should_track_client_events() {\n\n\t\t$ret = false;\n\n\t\t/**\n\t\t * Filter the post types that should be tracked\n\t\t *\n\t\t * @since 3.36.0\n\t\t * @since 3.36.1 Remove redundant check on `is_singular()` and `is_post_type_archive()`.\n\t\t *\n\t\t * @param string[]|string $post_types An array of post type names or a pre-defined setting as a string.\n\t\t *                                    \"llms\" uses all public LifterLMS and LifterLMS Add-on post types.\n\t\t *                                    \"all\" tracks everything.\n\t\t */\n\t\t$post_types = apply_filters( 'llms_tracking_post_types', 'llms' );\n\n\t\tif ( 'all' === $post_types ) {\n\t\t\t$ret = true;\n\t\t} elseif ( 'llms' === $post_types ) {\n\n\t\t\t// Filter public post types to include LifterLMS public post types.\n\t\t\t$post_types = array_keys( get_post_types( array( 'public' => true ) ) );\n\t\t\tforeach ( $post_types as $key => $type ) {\n\t\t\t\tif ( ! in_array( $type, array( 'course', 'lesson' ), true ) && 0 !== strpos( $type, 'llms_' ) ) {\n\t\t\t\t\tunset( $post_types[ $key ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( ! is_array( $post_types ) ) {\n\t\t\t$ret = false;\n\t\t} elseif ( is_singular( $post_types ) ) {\n\t\t\t$ret = true;\n\t\t} elseif ( is_post_type_archive( $post_types ) ) {\n\t\t\t$ret = true;\n\t\t} elseif ( is_llms_account_page() || is_llms_checkout() ) {\n\t\t\t$ret = true;\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not the current page should track client-side events\n\t\t *\n\t\t * @since 3.36.0\n\t\t *\n\t\t * @param bool $ret Whether or not to track the current page.\n\t\t * @param string[] $post_types Array of post types that should be tracked.\n\t\t */\n\t\treturn apply_filters( 'llms_tracking_should_track_client_events', $ret, $post_types );\n\t}\n\n\t/**\n\t * Store event data saved in the tracking cookie.\n\t *\n\t * @since 3.36.0\n\t * @since 3.37.14 Moved most of the logic into `store_tracking_events()` method.\n\t *                Bail if we're sending the tracking events via ajax.\n\t * @since 4.3.1 Set a secure cookie when possible.\n\t *\n\t * @return void\n\t */\n\tpublic function store_cookie() {\n\n\t\tif ( wp_doing_ajax() && ! empty( $_POST['llms-tracking'] ) ) {// phpcs:ignore: WordPress.Security.NonceVerification.Missing -- Nonce verified in `$this->store_tracking_events()` method.\n\t\t\treturn;\n\t\t}\n\n\t\t// Bail if no `llms-tracking` cookie.\n\t\tif ( empty( $_COOKIE['llms-tracking'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->store_tracking_events( wp_unslash( $_COOKIE['llms-tracking'] ) ); // phpcs:ignore: WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via $this->sanitize_raw_event().\n\n\t\t// Cookie reset.\n\t\tllms_setcookie( 'llms-tracking', '', time() - 60, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, llms_is_site_https() && is_ssl() );\n\t}\n\n\t/**\n\t * Store event data saved in the tracking cookie.\n\t *\n\t * @since 3.37.14\n\t *\n\t * @param string $tracking The `llms-tracking` data in JSON format.\n\t * @return (boolean|WP_Error) Returns WP_Error when nonce verification fails or unauthenticated user, `true` otherwise.\n\t */\n\tpublic function store_tracking_events( $tracking ) {\n\n\t\t$tracking = json_decode( $tracking, true );\n\n\t\tif ( ! empty( $tracking['nonce'] ) && wp_verify_nonce( $tracking['nonce'], 'llms-tracking' ) && get_current_user_id() ) {\n\n\t\t\tif ( ! empty( $tracking['events'] ) && is_array( $tracking['events'] ) ) {\n\n\t\t\t\tforeach ( $tracking['events'] as $event ) {\n\n\t\t\t\t\t$event = $this->prepare_event( $event );\n\n\t\t\t\t\tif ( ! is_wp_error( $event ) ) {\n\t\t\t\t\t\t$this->record( $event );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\treturn new WP_Error( 'llms_events_tracking_unauthorized', __( 'You\\'re not allowed to store tracking events', 'lifterlms' ) );\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "includes/class-llms-generator-courses.php",
    "content": "<?php\n/**\n * Generate LMS Content from export files or raw arrays of data\n *\n * @package LifterLMS/Classes\n *\n * @since 4.7.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Generator_Courses class\n *\n * @since 4.7.0\n */\nclass LLMS_Generator_Courses extends LLMS_Abstract_Generator_Posts {\n\n\t/**\n\t * Exception code: Raw data missing required data.\n\t *\n\t * @var int\n\t */\n\tconst ERROR_GEN_MISSING_REQUIRED = 2000;\n\n\t/**\n\t * Exception code: Raw data in an invalid format.\n\t *\n\t * @var int\n\t */\n\tconst ERROR_GEN_INVALID_FORMAT = 2001;\n\n\t/**\n\t * Add taxonomy terms to a course\n\t *\n\t * @since 3.3.0\n\t * @since 3.7.5 Unknown.\n\t * @since 4.7.0 Moved from `LLMS_Generator` and made `protected` instead of `private`.\n\t *\n\t * @param obj   $course_id WP_Post ID of a Course.\n\t * @param array $raw_terms Array of raw term arrays.\n\t * @return void\n\t */\n\tprotected function add_course_terms( $course_id, $raw_terms ) {\n\n\t\t$taxes = array(\n\t\t\t'course_cat'        => 'categories',\n\t\t\t'course_difficulty' => 'difficulty',\n\t\t\t'course_tag'        => 'tags',\n\t\t\t'course_track'      => 'tracks',\n\t\t);\n\n\t\tforeach ( $taxes as $tax => $key ) {\n\n\t\t\tif ( ! empty( $raw_terms[ $key ] ) && is_array( $raw_terms[ $key ] ) ) {\n\n\t\t\t\t// We can only have one difficulty at a time.\n\t\t\t\t$append = ( 'difficulty' === $key ) ? false : true;\n\n\t\t\t\t$terms = array();\n\n\t\t\t\t// Find term id or create it.\n\t\t\t\tforeach ( $raw_terms[ $key ] as $term_name ) {\n\n\t\t\t\t\tif ( empty( $term_name ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t$term_id = $this->get_term_id( $term_name, $tax );\n\t\t\t\t\tif ( $term_id ) {\n\t\t\t\t\t\t$terms[] = $term_id;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\twp_set_post_terms( $course_id, $terms, $tax, $append );\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Generator called when cloning a course\n\t *\n\t * @since 4.13.0\n\t *\n\t * @param array $raw Raw course data array.\n\t * @return int|null WP_Post ID of the generated course or `null` on failure.\n\t */\n\tpublic function clone_course( $raw ) {\n\t\treturn $this->generate_course( $this->setup_raw_for_clone( $raw ) );\n\t}\n\n\t/**\n\t * Generator called when cloning a lesson\n\t *\n\t * @since 3.14.8\n\t * @since 4.7.0 Moved from `LLMS_Generator` and made `public` instead of `private`.\n\t * @since 4.13.0 Use `setup_raw_for_clone()` to normalize the\n\t *\n\t * @param array $raw Raw data array.\n\t * @return int|WP_Error WP_Post ID of the created lesson on success and an error object on failure.\n\t */\n\tpublic function clone_lesson( $raw ) {\n\t\treturn $this->create_lesson( $this->setup_raw_for_clone( $raw ), 0, '', '' );\n\t}\n\n\t/**\n\t * Generator called for single course imports\n\t *\n\t * Converts the single course into a format that can be handled by the bulk courses generator\n\t * and invokes that generator.\n\t *\n\t * @since 3.3.0\n\t * @since 4.7.0 Moved from `LLMS_Generator` and made `public` instead of `private`.\n\t *              Returns an int on success.\n\t * @param array $raw Raw data array.\n\t * @return int|null WP_Post ID of the generated course or `null` on failure.\n\t */\n\tpublic function generate_course( $raw ) {\n\n\t\t$new_raw = array();\n\n\t\tforeach ( array( '_generator', '_version', '_source' ) as $meta ) {\n\t\t\tif ( isset( $raw[ $meta ] ) ) {\n\t\t\t\t$new_raw[ $meta ] = $raw[ $meta ];\n\t\t\t\tunset( $raw[ $meta ] );\n\t\t\t}\n\t\t}\n\n\t\t$new_raw['courses'] = array( $raw );\n\t\t$courses            = $this->generate_courses( $new_raw );\n\n\t\treturn is_array( $courses ) ? $courses[0] : null;\n\t}\n\n\t/**\n\t * Generator called for bulk course imports\n\t *\n\t * @since 3.3.0\n\t * @since 4.7.0 Moved from `LLMS_Generator` to `LLMS_Abstract_Generator_Courses`.\n\t *               Updated method access from `private` to `public`.\n\t *               Throws an exception in favor of returning `null` when an error is encountered.\n\t *               Returns an array of generated course IDs on success.\n\t *\n\t * @param array $raw Raw data array.\n\t * @return void\n\t *\n\t * @throws Exception When invalid `$raw` data is submitted.\n\t */\n\tpublic function generate_courses( $raw ) {\n\n\t\tif ( empty( $raw['courses'] ) ) {\n\t\t\tthrow new Exception( esc_attr__( 'Raw data is missing the required \"courses\" array.', 'lifterlms' ), intval( self::ERROR_GEN_MISSING_REQUIRED ) );\n\t\t} elseif ( ! is_array( $raw['courses'] ) ) {\n\t\t\tthrow new Exception( esc_attr__( 'The raw \"courses\" item must be an array.', 'lifterlms' ), intval( self::ERROR_GEN_INVALID_FORMAT ) );\n\t\t}\n\n\t\t$courses = array();\n\n\t\tforeach ( $raw['courses'] as $raw_course ) {\n\t\t\tunset( $raw_course['_generator'], $raw_course['_version'] );\n\t\t\t$courses[] = $this->create_course( $raw_course );\n\t\t}\n\n\t\t$this->handle_prerequisites();\n\n\t\treturn $courses;\n\t}\n\n\t/**\n\t * Create a new access plan\n\t *\n\t * @since 3.3.0\n\t * @since 3.7.3 Unknown.\n\t * @since 4.3.3 Use an empty string in favor of `null` for an empty `post_content` field.\n\t * @since 4.7.0 Sideload images attached to the post, use `create_post()` from abstract, add hooks.\n\t *\n\t * @param array $raw                Raw Access Plan Data.\n\t * @param int   $course_id          WP Post ID of a LLMS Course to assign the access plan to.\n\t * @param int   $fallback_author_id Optional. WP User ID to use for the access plan author if no author is supplied in the raw data. Default is `null`.\n\t *                                  When not supplied the fall back will be on the current user ID.\n\t * @return int\n\t */\n\tprotected function create_access_plan( $raw, $course_id, $fallback_author_id = null ) {\n\n\t\t/**\n\t\t * Filter raw course import data prior to generation\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param array          $raw       Raw course data array.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\t$raw = apply_filters( 'llms_generator_before_new_access_plan', $raw, $this );\n\n\t\t// Force course relationship.\n\t\t$raw['product_id'] = $course_id;\n\n\t\t$plan = $this->create_post( 'access_plan', $raw, $fallback_author_id );\n\t\tif ( ! $plan ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t/**\n\t\t * Action triggered immediately following generation of a new acess plan\n\t\t *\n\t\t * @since 4.7.0\n\t\t *\n\t\t * @param LLMS_Access_Plan $plan      Generated access plan object.\n\t\t * @param array            $raw       Original raw course data array.\n\t\t * @param LLMS_Generator   $generator Generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_new_access_plan', $plan, $raw, $this );\n\n\t\treturn $plan->get( 'id' );\n\t}\n\n\t/**\n\t * Create a new course\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Added hooks.\n\t * @since 4.3.3 Use an empty string in favor of `null` for empty `post_content` and `post_excerpt` fields.\n\t * @since 4.7.0 Import images and reusable blocks found in the post's content and use `create_post()` from abstract.\n\t *\n\t * @param array $raw Raw course data.\n\t * @return int\n\t *\n\t * @throws Exception When an error is encountered during course creation.\n\t */\n\tprotected function create_course( $raw ) {\n\n\t\t/**\n\t\t * Filter raw course import data prior to generation\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param array          $raw       Raw course data array.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\t$raw = apply_filters( 'llms_generator_before_new_course', $raw, $this );\n\n\t\t// Create the course.\n\t\t$course = $this->create_post( 'course', $raw, get_current_user_id() );\n\n\t\t// Add terms to our course.\n\t\t$terms = array();\n\t\tif ( isset( $raw['difficulty'] ) ) {\n\t\t\t$terms['difficulty'] = array( $raw['difficulty'] );\n\t\t}\n\t\tforeach ( array( 'categories', 'tags', 'tracks' ) as $tax ) {\n\t\t\tif ( isset( $raw[ $tax ] ) ) {\n\t\t\t\t$terms[ $tax ] = $raw[ $tax ];\n\t\t\t}\n\t\t}\n\t\t$this->add_course_terms( $course->get( 'id' ), $terms );\n\n\t\t// Create all access plans.\n\t\tif ( isset( $raw['access_plans'] ) ) {\n\t\t\tforeach ( $raw['access_plans'] as $plan ) {\n\t\t\t\t$this->create_access_plan( $plan, $course->get( 'id' ), $course->get( 'author' ) );\n\t\t\t}\n\t\t}\n\n\t\t// Create all sections.\n\t\tif ( isset( $raw['sections'] ) ) {\n\t\t\tforeach ( $raw['sections'] as $order => $section ) {\n\t\t\t\t$this->create_section( $section, ++$order, $course->get( 'id' ), $course->get( 'author' ) );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Action triggered immediately following generation of a new course\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param LLMS_Course    $course    Generated course object.\n\t\t * @param array          $raw       Original raw course data array.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_new_course', $course, $raw, $this );\n\n\t\treturn $course->get( 'id' );\n\t}\n\n\t/**\n\t * Create a new lesson\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Added hooks.\n\t * @since 4.3.3 Use an empty string in favor of `null` for empty `post_content` and `post_excerpt` fields.\n\t * @since 4.7.0 Import images and reusable blocks found in the post's content and use `create_post()` from abstract.\n\t *\n\t * @param array $raw                Raw lesson data.\n\t * @param int   $order              Lesson order within the section (starts at 1).\n\t * @param int   $section_id         WP Post ID of the lesson's parent section.\n\t * @param int   $course_id          WP Post ID of the lesson's parent course.\n\t * @param int   $fallback_author_id Optional. Author ID to use as a fallback if no raw author data supplied for the lesson. Default is `null`.\n\t *                                  When not supplied the fall back will be on the current user ID.\n\t * @return int\n\t *\n\t * @throws Exception When an error is encountered during post creation.\n\t */\n\tprotected function create_lesson( $raw, $order, $section_id, $course_id, $fallback_author_id = null ) {\n\n\t\t/**\n\t\t * Filter raw lesson import data prior to generation\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param array          $raw                Raw lesson data array.\n\t\t * @param int            $order              Lesson order within the section (starts at 1).\n\t\t * @param int            $section_id         WP Post ID of the lesson's parent section.\n\t\t * @param int            $course_id          WP Post ID of the lesson's parent course.\n\t\t * @param int            $fallback_author_id Optional author ID to use as a fallback if no raw author data supplied for the lesson.\n\t\t * @param LLMS_Generator $generator          Generator instance.\n\t\t */\n\t\t$raw = apply_filters( 'llms_generator_before_new_lesson', $raw, $order, $section_id, $course_id, $fallback_author_id, $this );\n\n\t\t// Force some data.\n\t\t$raw['parent_course']  = $course_id;\n\t\t$raw['parent_section'] = $section_id;\n\t\t$raw['order']          = $order;\n\n\t\t$raw_quiz = ! empty( $raw['quiz'] ) ? $raw['quiz'] : false;\n\t\tunset( $raw['quiz'] );\n\n\t\t$lesson = $this->create_post( 'lesson', $raw, $fallback_author_id );\n\n\t\tif ( $raw_quiz ) {\n\t\t\t$raw_quiz['lesson_id'] = $lesson->get( 'id' );\n\t\t\t$lesson->set( 'quiz', $this->create_quiz( $raw_quiz, $lesson->get( 'author' ) ) );\n\t\t}\n\n\t\t/**\n\t\t * Action triggered immediately following generation of a new lesson\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param LLMS_Lesson    $lesson    Generated lesson object.\n\t\t * @param array          $raw       Original raw lesson data array.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_new_lesson', $lesson, $raw, $this );\n\n\t\treturn $lesson->get( 'id' );\n\t}\n\n\t/**\n\t * Creates a new quiz\n\t * Creates all questions within the quiz as well\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Added hooks.\n\t * @since 4.3.3 Use an empty string in favor of `null` for an empty `post_content` field.\n\t * @since 4.7.0 Sideload images attached to the post  and use `create_post()` from abstract.\n\t *\n\t * @param array $raw                Raw quiz data.\n\t * @param int   $fallback_author_id Optional. Author ID to use as a fallback if no raw author data supplied for the quiz. Default is `null`.\n\t *                                  When not supplied the fall back will be on the current user ID.\n\t * @return int\n\t *\n\t * @throws Exception When an error is encountered during post creation.\n\t */\n\tprotected function create_quiz( $raw, $fallback_author_id = null ) {\n\n\t\t/**\n\t\t * Filter raw quiz import data prior to generation\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param array          $raw                Raw quiz data array.\n\t\t * @param int            $fallback_author_id Optional author ID to use as a fallback if no raw author data supplied for the quiz.\n\t\t * @param LLMS_Generator $generator          Generator instance.\n\t\t */\n\t\t$raw = apply_filters( 'llms_generator_before_new_quiz', $raw, $fallback_author_id, $this );\n\n\t\t$quiz = $this->create_post( 'quiz', $raw, $fallback_author_id );\n\n\t\tif ( isset( $raw['questions'] ) ) {\n\t\t\t$manager = $quiz->questions();\n\t\t\tforeach ( $raw['questions'] as $question ) {\n\t\t\t\t$this->create_question( $question, $manager, $quiz->get( 'author' ) );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Action triggered immediately following generation of a new quiz\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param LLMS_Quiz      $quiz      Generated quiz object.\n\t\t * @param array          $raw       Original raw quiz data array.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_new_quiz', $quiz, $raw, $this );\n\n\t\treturn $quiz->get( 'id' );\n\t}\n\n\t/**\n\t * Creates a new question\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Added hooks.\n\t * @since 4.7.0 Attempt to sideload images found in the imported post's content and image choices.\n\t *\n\t * @param array $raw       Raw question data.\n\t * @param obj   $manager   Question manager instance.\n\t * @param int   $author_id Optional. Author ID to use as a fallback if no raw author data supplied for the question. Default is `null`.\n\t *                         When not supplied the fall back will be on the current user ID.\n\t * @return int\n\t *\n\t * @throws Exception When an error is encountered during course creation.\n\t */\n\tprotected function create_question( $raw, $manager, $author_id ) {\n\n\t\t/**\n\t\t * Filter raw question import data prior to generation\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param array          $raw       Raw quiz data array.\n\t\t * @param obj            $manager   Question manager instance.\n\t\t * @param int            $author_id Optional author ID to use as a fallback if no raw author data supplied for the question.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\t$raw = apply_filters( 'llms_generator_before_new_question', $raw, $manager, $author_id, $this );\n\n\t\tunset( $raw['parent_id'] );\n\n\t\t$question_id = $manager->create_question(\n\t\t\tarray_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'post_status' => 'publish',\n\t\t\t\t\t'post_author' => $author_id,\n\t\t\t\t),\n\t\t\t\t$raw\n\t\t\t)\n\t\t);\n\n\t\tif ( ! $question_id ) {\n\t\t\tthrow new Exception( esc_attr__( 'Error creating the question post object.', 'lifterlms' ), intval( self::ERROR_CREATE_POST ) );\n\t\t}\n\n\t\t$question = llms_get_post( $question_id );\n\n\t\t$this->store_temp_id( $raw, $question );\n\n\t\tif ( isset( $raw['choices'] ) ) {\n\t\t\tforeach ( $raw['choices'] as $choice ) {\n\t\t\t\tunset( $choice['question_id'] );\n\t\t\t\t$question->create_choice( $this->maybe_sideload_choice_image( $choice, $question_id ) );\n\t\t\t}\n\t\t}\n\n\t\t// Set all metadata.\n\t\tforeach ( array_keys( $question->get_properties() ) as $key ) {\n\t\t\tif ( isset( $raw[ $key ] ) ) {\n\t\t\t\t$question->set( $key, $raw[ $key ] );\n\t\t\t}\n\t\t}\n\n\t\t$this->sideload_images( $question, $raw );\n\n\t\t/**\n\t\t * Action triggered immediately following generation of a new question\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param LLMS_Question  $question  Generated question object.\n\t\t * @param array          $raw       Original raw question data array.\n\t\t * @param obj            $manager   Question manager instance.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_new_question', $question, $raw, $manager, $this );\n\n\t\treturn $question->get( 'id' );\n\t}\n\n\t/**\n\t * Creates a new section\n\t *\n\t * Creates all lessons within the section data.\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Added hooks.\n\t * @since 4.7.0 Use `create_post()` from abstract.\n\t *\n\t * @param array $raw                Raw section data.\n\t * @param int   $order              Order within the course (starts at 1).\n\t * @param int   $course_id          WP Post ID of the parent course.\n\t * @param int   $fallback_author_id Optional. Author ID to use as a fallback if no raw author data supplied for the section.\n\t *                                  When not supplied the fall back will be on the current user ID.\n\t * @return int\n\t *\n\t * @throws Exception When an error is encountered during course creation.\n\t */\n\tprotected function create_section( $raw, $order, $course_id, $fallback_author_id = null ) {\n\n\t\t/**\n\t\t * Filter raw section import data prior to generation\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param array          $raw                Raw quiz data array.\n\t\t * @param int            $order              Order within the course (starts at 1).\n\t\t * @param int            $course_id          WP Post ID of the parent course.\n\t\t * @param int            $fallback_author_id Optional author ID to use as a fallback if no raw author data supplied for the section.\n\t\t * @param LLMS_Generator $generator          Generator instance.\n\t\t */\n\t\t$raw = apply_filters( 'llms_generator_before_new_section', $raw, $order, $course_id, $fallback_author_id, $this );\n\n\t\t$raw['parent_course'] = $course_id;\n\t\t$raw['order']         = $order;\n\n\t\t$section = $this->create_post( 'section', $raw, $fallback_author_id );\n\n\t\tif ( isset( $raw['lessons'] ) ) {\n\t\t\tforeach ( $raw['lessons'] as $lesson_order => $lesson ) {\n\t\t\t\t$this->create_lesson( $lesson, ++$lesson_order, $section->get( 'id' ), $course_id, $section->get( 'author' ) );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Action triggered immediately following generation of a new section\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param LLMS_Section   $section   Generated section object.\n\t\t * @param array          $raw       Original raw section data array.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_new_section', $section, $raw, $this );\n\n\t\treturn $section->get( 'id' );\n\t}\n\n\t/**\n\t * Updates course and lesson prerequisites\n\t *\n\t * If the prerequisite was included in the import, updates to the new imported version.\n\t *\n\t * If the prereq is not included but the source matches, leaves the prereq intact as long as the prereq exists.\n\t *\n\t * Otherwise removes prerequisite data from the new course / lesson.\n\t *\n\t * Removes prereq track associations if there's no source or source doesn't match\n\t * or if the track doesn't exist.\n\t *\n\t * @since 3.3.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tprotected function handle_prerequisites() {\n\n\t\tforeach ( array( 'course', 'lesson' ) as $obj_type ) {\n\n\t\t\t$ids = ! empty( $this->tempids[ $obj_type ] ) ? $this->tempids[ $obj_type ] : array();\n\n\t\t\t// Courses have two kinds of prereqs.\n\t\t\t$has_prereq_param = ( 'course' === $obj_type ) ? 'course' : null;\n\n\t\t\t// Loop through all then created lessons.\n\t\t\tforeach ( $ids as $old_id => $new_id ) {\n\n\t\t\t\t// Instantiate the new instance of the object.\n\t\t\t\t$obj = llms_get_post( $new_id );\n\n\t\t\t\t// If this is a course and there isn't a source or the source doesn't match the current site.\n\t\t\t\t// We should remove the track prerequisites.\n\t\t\t\tif ( 'course' === $obj_type && ( ! isset( $raw['_source'] ) || get_site_url() !== $raw['_source'] ) ) {\n\n\t\t\t\t\t// Remove prereq track settings.\n\t\t\t\t\tif ( $obj->has_prerequisite( 'course_track' ) ) {\n\t\t\t\t\t\t$obj->set( 'prerequisite_track', 0 );\n\t\t\t\t\t\tif ( ! $obj->has_prerequisite( 'course' ) ) {\n\t\t\t\t\t\t\t$obj->set( 'has_prerequisite', 'no' );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If the object has a prereq.\n\t\t\t\tif ( $obj->has_prerequisite( $has_prereq_param ) ) {\n\n\t\t\t\t\t// Get the old preqeq's id.\n\t\t\t\t\t$old_prereq = $obj->get( 'prerequisite' );\n\n\t\t\t\t\t// If the old prereq is a key in the array of created objects.\n\t\t\t\t\t// We can replace it with the new id.\n\t\t\t\t\tif ( in_array( $old_prereq, array_keys( $ids ) ) ) {\n\n\t\t\t\t\t\t$obj->set( 'prerequisite', $ids[ $old_prereq ] );\n\n\t\t\t\t\t} elseif ( ! isset( $raw['_source'] ) || get_site_url() !== $raw['_source'] ) {\n\n\t\t\t\t\t\t$obj->set( 'has_prerequisite', 'no' );\n\t\t\t\t\t\t$obj->set( 'prerequisite', 0 );\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$post = get_post( $old_prereq );\n\t\t\t\t\t\t// Post doesn't exist or the post type doesn't match, get rid of it.\n\t\t\t\t\t\tif ( ! $post || $obj_type !== $post->post_type ) {\n\n\t\t\t\t\t\t\t$obj->set( 'has_prerequisite', 'no' );\n\t\t\t\t\t\t\t$obj->set( 'prerequisite', 0 );\n\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Determines if a raw question choice object contains image data that should be sideloaded\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param array $choice      Raw choice data array.\n\t * @param int   $question_id WP_Post ID of the parent question.\n\t * @return array Choice data array.\n\t */\n\tprotected function maybe_sideload_choice_image( $choice, $question_id ) {\n\n\t\tif ( empty( $choice['choice_type'] ) || 'image' !== $choice['choice_type'] || ! $this->is_image_sideloading_enabled() ) {\n\t\t\treturn $choice;\n\t\t}\n\n\t\tif ( ! isset( $choice['choice']['src'] ) ) {\n\t\t\treturn $choice;\n\t\t}\n\n\t\t$id = $this->sideload_image( $question_id, $choice['choice']['src'], 'id' );\n\t\tif ( is_wp_error( $id ) ) {\n\t\t\treturn $choice;\n\t\t}\n\n\t\t$choice['choice']['id']  = $id;\n\t\t$choice['choice']['src'] = wp_get_attachment_url( $id );\n\n\t\treturn $choice;\n\t}\n\n\n\t/**\n\t * Modifies incoming raw data when creating a clone of a course or lesson\n\t *\n\t * When a clone is created, it will automatically have \"(Clone)\" appended to the existing title\n\t * and will be created with the \"Draft\" status.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @param array $raw Raw data array for the course or lesson.\n\t * @return array\n\t */\n\tprotected function setup_raw_for_clone( $raw ) {\n\n\t\t/**\n\t\t * Filters the suffix appended to the WP_Post title of a duplicated post when cloning a course or lesson\n\t\t *\n\t\t * @since 4.13.0\n\t\t *\n\t\t * @param string         $status    The WP_Post status to use for the duplicate of the post. Default: \"draft\".\n\t\t * @param array          $raw       Raw data array passed into the generator.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\t$raw['title'] .= apply_filters( 'llms_generator_cloned_post_title_suffix', sprintf( ' (%s)', __( 'Clone', 'lifterlms' ) ), $raw, $this );\n\n\t\t/**\n\t\t * Filters the WP_Post status used for the duplicated post when cloning a course or lesson\n\t\t *\n\t\t * @since 4.13.0\n\t\t *\n\t\t * @param string         $status    The WP_Post status to use for the duplicate of the post. Default: \"draft\".\n\t\t * @param array          $raw       Raw data array passed into the generator.\n\t\t * @param LLMS_Generator $generator Generator instance.\n\t\t */\n\t\t$raw['status'] = apply_filters( 'llms_generator_cloned_post_status', 'draft', $raw, $this );\n\n\t\treturn $raw;\n\t}\n\n\t/**\n\t * Set all metadata for a given post object.\n\t *\n\t * This method will only set metadata for registered LLMS_Post_Model properties.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @param LLMS_Post_Model $post An LLMS post object.\n\t * @param array           $raw  Array of raw data.\n\t * @return void\n\t */\n\tprotected function set_metadata( $post, $raw ) {\n\n\t\t$generated_from_id = $post->get( 'generated_from_id' );\n\n\t\tif ( $generated_from_id ) {\n\t\t\t$replace_id_props = array(\n\t\t\t\t'course_closed_message',\n\t\t\t\t'course_opens_message',\n\t\t\t\t'enrollment_closed_message',\n\t\t\t\t'enrollment_opens_message',\n\t\t\t);\n\n\t\t\t$find    = '#(.*id=[\"\\'])' . $generated_from_id . '([\"\\'].*)#';\n\t\t\t$replace = '${1}' . $post->get( 'id' ) . '${2}';\n\n\t\t\t/**\n\t\t\t * Replace old post ID with new cloned post ID in course/enrollment\n\t\t\t * message shortcodes.\n\t\t\t */\n\t\t\tforeach ( $replace_id_props as $key ) {\n\t\t\t\tif ( isset( $raw[ $key ] ) ) {\n\t\t\t\t\t$raw[ $key ] = preg_replace( $find, $replace, $raw[ $key ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn parent::set_metadata( $post, $raw );\n\t}\n}\n"
  },
  {
    "path": "includes/class-llms-grades.php",
    "content": "<?php\n/**\n * Get & Set grades for gradable post types\n *\n * @package LifterLMS/Classes\n *\n * @since 3.24.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Grades\n *\n * @since 3.24.0\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Removed the deprecated `LLMS_Grades::$_instance` property.\n */\nclass LLMS_Grades {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Determines the rounding precision used by grading functions\n\t *\n\t * @var  int\n\t */\n\tprivate $rounding_precision = 2;\n\n\t/**\n\t * Private constructor\n\t *\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprivate function __construct() {\n\n\t\t$this->rounding_precision = apply_filters( 'llms_grade_rounding_precision', $this->rounding_precision );\n\n\t}\n\n\t/**\n\t * Calculates the grades for elements that have a list of children which are averaged / weighted to come up with the total grade\n\t *\n\t * @param    array        $children list of child objects\n\t * @param    LLMS_Student $student  A LLMS_Student object.\n\t * @return   float|null\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprivate function calculate_grade_from_children( $children, $student ) {\n\n\t\t$grade  = null;\n\t\t$grades = array();\n\n\t\t// Loop through all the children and compile the overall grade & points data.\n\t\tforeach ( $children as $child_id ) {\n\n\t\t\t$child = llms_get_post( $child_id );\n\t\t\t$grade = $this->get_grade( $child_id, $student, false );\n\n\t\t\t// Non numeric grade (null) hasn't been taken yet or no gradable elements exist on the child.\n\t\t\tif ( ! is_numeric( $grade ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$points = $child->get( 'points' );\n\n\t\t\t// If no points assigned to the child, the grade doesn't count towards the overall grade.\n\t\t\tif ( ! $points ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Add the grade & points for further processing after we have all the data.\n\t\t\t$grades[] = array(\n\t\t\t\t'grade'  => $grade,\n\t\t\t\t'points' => $points,\n\t\t\t);\n\n\t\t}\n\n\t\t// If we have at least one grade.\n\t\tif ( count( $grades ) ) {\n\n\t\t\t// Get the total available points for all children with a numeric grade & a points value.\n\t\t\t$total_points = array_sum( wp_list_pluck( $grades, 'points' ) );\n\n\t\t\t// If we don't have any points this element can't have an overall grade.\n\t\t\tif ( $total_points ) {\n\n\t\t\t\t// Sum up the adjusted grade.\n\t\t\t\t$grade = 0;\n\t\t\t\tforeach ( $grades as $data ) {\n\t\t\t\t\t// Calculate the adjusted the grade.\n\t\t\t\t\t// Grade multiplied by available points over total points.\n\t\t\t\t\t$grade += $data['grade'] * ( $data['points'] / $total_points );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $grade;\n\n\t}\n\n\t/**\n\t * Calculate the grade for a course\n\t *\n\t * @param    LLMS_Course  $course  A LLMS_Course object.\n\t * @param    LLMS_Student $student A LLMS_Student object.\n\t * @return   float|null\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprivate function calculate_course_grade( $course, $student ) {\n\n\t\treturn apply_filters(\n\t\t\t'llms_calculate_course_grade',\n\t\t\t$this->calculate_grade_from_children( $course->get_lessons( 'ids' ), $student ),\n\t\t\t$course,\n\t\t\t$student\n\t\t);\n\n\t}\n\n\t/**\n\t * Main grade calculation function\n\t * Calculates the grade for a gradable post model\n\t * DOES NOT CACHE RESULTS!\n\t * See get_grade() for a function which uses caching\n\t *\n\t * @param    LLMS_Post_Model $post    A LLMS_Post_Model object.\n\t * @param    LLMS_Student    $student A LLMS_Student object.\n\t * @return   float|null\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function calculate_grade( $post, $student ) {\n\n\t\t$grade = null;\n\n\t\t$post_type = $post->get( 'type' );\n\t\tswitch ( $post_type ) {\n\n\t\t\tcase 'course':\n\t\t\t\t/** @var LLMS_Course $post */\n\t\t\t\t$grade = $this->calculate_course_grade( $post, $student );\n\t\t\t\tbreak;\n\n\t\t\tcase 'lesson':\n\t\t\t\t/** @var LLMS_Lesson $post */\n\t\t\t\t$grade = $this->calculate_lesson_grade( $post, $student );\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_quiz':\n\t\t\t\t$attempt = $student->quizzes()->get_best_attempt( $post->get( 'id' ) );\n\t\t\t\tif ( $attempt ) {\n\t\t\t\t\t$grade = $attempt->get( 'grade' );\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\t// 3rd party / custom element grading.\n\t\t\tdefault:\n\t\t\t\t$grade = apply_filters( 'llms_calculate_' . $post_type . '_grade', $grade, $post, $student );\n\n\t\t}\n\n\t\t// Round numeric results.\n\t\tif ( is_numeric( $grade ) ) {\n\t\t\t$grade = $this->round( $grade );\n\t\t}\n\n\t\treturn apply_filters( 'llms_calculate_grade', $grade, $post, $student );\n\n\t}\n\n\t/**\n\t * Calculates the grade for a lesson\n\t *\n\t * @param    LLMS_Lesson  $lesson  A LLMS_Lesson object.\n\t * @param    LLMS_Student $student A LLMS_Student object.\n\t * @return   float|null\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprivate function calculate_lesson_grade( $lesson, $student ) {\n\n\t\t$grade = null;\n\n\t\tif ( $lesson->is_quiz_enabled() ) {\n\n\t\t\t$grade = $this->get_grade( $lesson->get( 'quiz' ), $student, false );\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_calculate_lesson_grade', $grade, $lesson, $student );\n\n\t}\n\n\t/**\n\t * Main grade getter function\n\t *\n\t * Uses caching by default and can bypass cache when requested\n\t *\n\t * @since 3.24.0\n\t * @since 4.4.4 Don't pass the `$use_cache` parameter to the `calculate_grade()` method.\n\t *\n\t * @param    WP_Post|int  $post_id   An instance of WP_Post or a WP Post ID.\n\t * @param    LLMS_Student $student   A LLMS_Student object.\n\t * @param    bool         $use_cache when true, retrieves from cache if available\n\t * @return   float|null\n\t */\n\tpublic function get_grade( $post_id, $student, $use_cache = true ) {\n\n\t\t$post    = llms_get_post( $post_id );\n\t\t$student = llms_get_student( $student );\n\n\t\t$grade = $use_cache ? $this->get_grade_from_cache( $post, $student ) : false;\n\n\t\t// Grade not found in cache or we're not using the cache.\n\t\tif ( false === $grade ) {\n\n\t\t\t$grade = $this->calculate_grade( $post, $student );\n\n\t\t\t// Store in the cache.\n\t\t\twp_cache_set(\n\t\t\t\tsprintf( '%d_grade', $post->get( 'id' ) ),\n\t\t\t\t$grade,\n\t\t\t\tsprintf( 'student_%d', $student->get( 'id' ) )\n\t\t\t);\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_grade', $grade, $post, $student );\n\n\t}\n\n\t/**\n\t * Retrieve a grade from the wp_cache\n\t *\n\t * @param    LLMS_Post_Model $post    A LLMS_Post_Model object.\n\t * @param    LLMS_Student    $student A LLMS_Student object.\n\t * @return   mixed             grade as a float\n\t *                             null if there's no grade for the post\n\t *                             false if the grade wasn't found in the cache\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprivate function get_grade_from_cache( $post, $student ) {\n\n\t\treturn wp_cache_get(\n\t\t\tsprintf( '%d_grade', $post->get( 'id' ) ),\n\t\t\tsprintf( 'student_%d', $student->get( 'id' ) )\n\t\t);\n\n\t}\n\n\t/**\n\t * Round grades according to filterable rounding options set during construction\n\t *\n\t * @param    float $grade  Grade to round\n\t * @return   float\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function round( $grade ) {\n\n\t\treturn round( $grade, $this->rounding_precision );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-loader.php",
    "content": "<?php\n/**\n * LifterLMS file loader\n *\n * @package LifterLMS/Classes\n *\n * @since 4.0.0\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Loader.\n *\n * @since 4.0.0\n * @since 5.3.0 Add traits to `autoload()`.\n */\nclass LLMS_Loader {\n\n\t/**\n\t * These classes do not conform to any of the LifterLMS class name or file name standards.\n\t *\n\t * @todo Rename these classes and/or add a namespace to them.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string[] [ $lowercase_class_name => $path_relative_to_LLMS_PLUGIN_DIR ]\n\t */\n\tprivate $non_standard_classes = array(\n\t\t// Missing \"_Abstract_\" from class name.\n\t\t'llms_admin_metabox'                 => 'includes/abstracts/abstract.llms.admin.metabox.php',\n\t\t'llms_admin_table'                   => 'includes/abstracts/abstract.llms.admin.table.php',\n\t\t'llms_analytics_widget'              => 'includes/abstracts/abstract.llms.analytics.widget.php',\n\t\t'llms_database_query'                => 'includes/abstracts/abstract.llms.database.query.php',\n\t\t'llms_payment_gateway'               => 'includes/abstracts/abstract.llms.payment.gateway.php',\n\t\t'llms_post_model'                    => 'includes/abstracts/abstract.llms.post.model.php',\n\t\t'llms_shortcode_course_element'      => 'includes/abstracts/abstract.llms.shortcode.course.element.php',\n\t\t'llms_shortcode'                     => 'includes/abstracts/abstract.llms.shortcode.php',\n\n\t\t// Missing \"_Admin_\" from class name.\n\t\t'llms_export_api'                    => 'includes/admin/class-llms-export-api.php',\n\n\t\t// Meta box fields.\n\t\t'llms_metabox_field'                 => 'includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.fields.php',\n\t\t'llms_metabox_textarea_w_tags_field' => 'includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.textarea.tags.php',\n\t\t'meta_box_field_interface'           => 'includes/admin/post-types/meta-boxes/fields/llms.interface.meta.box.field.php',\n\n\t\t// Missing \"Model\" from class name.\n\t\t'llms_access_plan'                   => 'includes/models/model.llms.access.plan.php',\n\t\t'llms_add_on'                        => 'includes/models/model.llms.add-on.php',\n\t\t'llms_coupon'                        => 'includes/models/model.llms.coupon.php',\n\t\t'llms_course'                        => 'includes/models/model.llms.course.php',\n\t\t'llms_event'                         => 'includes/models/class-llms-event.php',\n\t\t'llms_instructor'                    => 'includes/models/model.llms.instructor.php',\n\t\t'llms_lesson'                        => 'includes/models/model.llms.lesson.php',\n\t\t'llms_membership'                    => 'includes/models/model.llms.membership.php',\n\t\t'llms_notification'                  => 'includes/models/model.llms.notification.php',\n\t\t'llms_order'                         => 'includes/models/model.llms.order.php',\n\t\t'llms_post_instructors'              => 'includes/models/model.llms.post.instructors.php',\n\t\t'llms_product'                       => 'includes/models/model.llms.product.php',\n\t\t'llms_question_choice'               => 'includes/models/model.llms.question.choice.php',\n\t\t'llms_question'                      => 'includes/models/model.llms.question.php',\n\t\t'llms_quiz_attempt'                  => 'includes/models/model.llms.quiz.attempt.php',\n\t\t'llms_quiz_attempt_question'         => 'includes/models/model.llms.quiz.attempt.question.php',\n\t\t'llms_quiz'                          => 'includes/models/model.llms.quiz.php',\n\t\t'llms_section'                       => 'includes/models/model.llms.section.php',\n\t\t'llms_student'                       => 'includes/models/model.llms.student.php',\n\t\t'llms_student_quizzes'               => 'includes/models/model.llms.student.quizzes.php',\n\t\t'llms_transaction'                   => 'includes/models/model.llms.transaction.php',\n\t\t'llms_user_achievement'              => 'includes/models/model.llms.user.achievement.php',\n\t\t'llms_user_certificate'              => 'includes/models/model.llms.user.certificate.php',\n\t\t'llms_user_postmeta'                 => 'includes/models/model.llms.user.postmeta.php',\n\n\t\t// Miscellaneous.\n\t\t'llms_admin_reporting'               => 'includes/admin/reporting/class.llms.admin.reporting.php',\n\t\t'llms_admin_system_report'           => 'includes/admin/class.llms.admin.system-report.php',\n\t\t'llms_bbp_widget_course_forums_list' => 'includes/widgets/class.llms.bbp.widget.course.forums.list.php',\n\t\t'llms_media_protection'              => 'includes/class-llms-media-protection.php',\n\t\t'llms_db_upgrader'                   => 'includes/class-llms-db-ugrader.php',\n\t\t'llms_emails'                        => 'includes/class.llms.emails.php',\n\t\t'llms_payment_gateway_manual'        => 'includes/class.llms.gateway.manual.php',\n\t\t'llms_settings_page'                 => 'includes/admin/settings/class.llms.settings.page.php',\n\t\t'llms_table_notificationsettings'    => 'includes/admin/settings/tables/class.llms.table.notification.settings.php',\n\t\t'llms_table_student_certificates'    => 'includes/admin/reporting/tables/llms.table.certificates.php',\n\t\t'llms_table_studentmanagement'       => 'includes/admin/post-types/tables/class.llms.table.student.management.php',\n\n\t\t// Deprecated classes.\n\t\t'llms_achievement_user'              => 'includes/achievements/class.llms.achievement.user.php',\n\t\t'llms_certificate_user'              => 'includes/certificates/class.llms.certificate.user.php',\n\t);\n\n\t/**\n\t * An array of paths and what the class name starts with.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string[] [ $path_relative_to_LLMS_PLUGIN_DIR => $class_name_starts_with ]\n\t */\n\tprivate $class_paths = array(\n\t\t'includes/admin/tools/'                 => 'llms_admin_tool_',\n\t\t'includes/admin/'                       => 'llms_admin_',\n\t\t'includes/controllers/'                 => 'llms_controller_',\n\t\t'includes/emails/'                      => 'llms_email',\n\t\t'includes/forms/'                       => 'llms_form',\n\t\t'includes/integrations/'                => 'llms_integration_',\n\t\t'includes/admin/post-types/meta-boxes/' => 'llms_meta_box_',\n\t\t'includes/notifications/views/'         => 'llms_notification_view_',\n\t\t'includes/notifications/'               => 'llms_notification',\n\t\t'includes/privacy/'                     => 'llms_privacy',\n\t\t'includes/processors/'                  => 'llms_processor',\n\t\t'includes/shortcodes/'                  => 'llms_shortcode',\n\t\t'includes/widgets/'                     => 'llms_widget',\n\t\t'includes/'                             => 'llms_',\n\t);\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tspl_autoload_register( array( $this, 'autoload' ) );\n\n\t\t$this->includes_libraries();\n\n\t\t$this->includes();\n\n\t\tif ( is_admin() ) {\n\t\t\t$this->includes_admin();\n\t\t} else {\n\t\t\t$this->includes_frontend();\n\t\t}\n\t}\n\n\t/**\n\t * Auto-load LLMS classes.\n\t *\n\t * @todo Add a {@link https://www.php.net/manual/en/language.namespaces.php namespace} to every file to simplify autoloading.\n\t *\n\t * @since 1.0.0\n\t * @since 3.15.0 Unknown.\n\t * @since 4.0.0 Moved from `LifterLMS` class.\n\t * @since 5.3.0 Add traits.\n\t * @since 6.0.0 Increased the number of files that are autoloaded instead of manually loaded on every request.\n\t *              Return early if not a LifterLMS core class.\n\t *\n\t * @param string $class Class name being called.\n\t * @return void\n\t */\n\tpublic function autoload( $class ) {\n\n\t\t$class = strtolower( $class );\n\t\tif ( 0 !== strpos( $class, 'llms_' ) && 'lifterlms' !== $class && 'meta_box_field_interface' !== $class ) {\n\t\t\treturn;\n\t\t}\n\t\t$path    = null;\n\t\t$fileize = str_replace( '_', '-', $class );\n\t\t$file    = 'class-' . $fileize . '.php';\n\n\t\tif ( array_key_exists( $class, $this->non_standard_classes ) ) {\n\t\t\t$path = LLMS_PLUGIN_DIR . $this->non_standard_classes[ $class ];\n\t\t\t$file = null;\n\n\t\t} elseif ( 0 === strpos( $class, 'llms_abstract_' ) ) {\n\t\t\t$path = LLMS_PLUGIN_DIR . 'includes/abstracts/';\n\t\t\t$file = $fileize . '.php';\n\n\t\t} elseif (\n\t\t\t0 === strpos( $class, 'llms_analytics_' ) && false !== strrpos( $class, '_widget', - 7 )\n\t\t) {\n\t\t\t$path = LLMS_PLUGIN_DIR . 'includes/admin/reporting/widgets/';\n\t\t\t$file = 'class.llms.analytics.widget.' . substr( $class, 15, - 7 ) . '.php';\n\n\t\t} elseif ( 0 === strpos( $class, 'llms_interface_' ) ) {\n\t\t\t$path = LLMS_PLUGIN_DIR . 'includes/interfaces/';\n\t\t\t$file = $fileize . '.php';\n\n\t\t} elseif (\n\t\t\t0 === strpos( $class, 'llms_metabox_' ) && false !== strrpos( $class, '_field', - 6 )\n\t\t) {\n\t\t\t$path = LLMS_PLUGIN_DIR . 'includes/admin/post-types/meta-boxes/fields/';\n\t\t\t$file = 'llms-class-meta-box-' . substr( $fileize, 13, - 6 ) . '.php';\n\n\t\t} elseif ( 0 === strpos( $class, 'llms_table_' ) ) {\n\t\t\t/** @todo Prefix file names with 'class-' */\n\t\t\t$path = LLMS_PLUGIN_DIR . 'includes/admin/reporting/tables/';\n\t\t\t$file = $fileize . '.php';\n\n\t\t} elseif ( 0 === strpos( $class, 'llms_trait_' ) ) {\n\t\t\t$path = LLMS_PLUGIN_DIR . 'includes/traits/';\n\t\t\t$file = $fileize . '.php';\n\t\t}\n\n\t\tif ( is_null( $path ) ) {\n\t\t\tforeach ( $this->class_paths as $class_path => $class_name_starts_with ) {\n\t\t\t\tif ( 0 === strpos( $class, $class_name_starts_with ) ) {\n\t\t\t\t\t$path = LLMS_PLUGIN_DIR . $class_path;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( $path ) {\n\t\t\tif ( is_readable( $path . $file ) ) {\n\t\t\t\trequire_once $path . $file;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$file = str_replace( '-', '.', $file );\n\t\t\tif ( is_readable( $path . $file ) ) {\n\t\t\t\trequire_once $path . $file;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Includes that are included everywhere.\n\t *\n\t * @since 4.0.0\n\t * @since 4.4.0 Include `LLMS_Assets` class.\n\t * @since 4.12.0 Class `LLMS_Staging` always loaded instead of only loaded on admin panel.\n\t * @since 4.13.0 Include `LLMS_DOM_Document` class.\n\t * @since 5.0.0 Include `LLMS_Forms`, `LLMS_Form_Post_Type`, `LLMS_Form_Templates`, and `LLMS_Form_Handler`.\n\t * @since 5.2.0 Include `LLMS_DB_Upgrader`.\n\t * @since 5.6.0 Include `LLMS_Prevent_Concurrent_Logins`.\n\t * @since 6.0.0 Included `LLMS_Block_Library`, `LLMS_Controller_Awards`, and `LLMS_Engagement_Handler`.\n\t *              Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t * @since 6.4.0 Included `LLMS_Shortcodes` before `LLMS_Controller_Orders`.\n\t * @since 7.0.0 Include `LLMS_Controller_Checkout`.\n\t * @since 7.2.0 Include `LLMS_Shortcodes_Blocks`.\n\t *\n\t * @return void\n\t */\n\tpublic function includes() {\n\n\t\t// Instantiate LLMS_Shortcodes before LLMS_Controller_Orders.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/shortcodes/class.llms.shortcodes.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/shortcodes/class.llms.shortcodes.blocks.php';\n\n\t\t// Functions.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/llms.functions.core.php';\n\n\t\t// Classes.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-block-library.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-events-core.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-rest-fields.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-sessions.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-staging.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-prevent-concurrent-logins.php';\n\n\t\t// Forms.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms-admin-bar.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms-classic-editor.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms-data.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms-dynamic-fields.php';\n\n\t\t// Classes (files to be renamed).\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.assets.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.ajax.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.ajax.handler.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.cache.helper.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.comments.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.date.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.install.php';\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/class.llms.l10n.frontend.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.nav.menus.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.oembed.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.playnice.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.post.relationships.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.post-types.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.query.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.question.types.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.review.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.sidebars.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.student.dashboard.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.user.permissions.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.view.manager.php';\n\n\t\t// Controllers.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class.llms.controller.achievements.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class-llms-controller-awards.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class.llms.controller.certificates.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class.llms.controller.lesson.progression.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class-llms-controller-checkout.php'; // Added out of alpha order to preserve action load order.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class.llms.controller.orders.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class.llms.controller.quizzes.php';\n\n\t\t// Form controllers.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/controllers/class.llms.controller.account.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/controllers/class.llms.controller.login.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/controllers/class.llms.controller.registration.php';\n\n\t\t// Hooks.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/llms.template.hooks.php';\n\n\t\t// Privacy components.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/privacy/class-llms-privacy.php';\n\n\t\t// Spam stuff.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/llms.spam.functions.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/spam/class-llms-captcha.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/spam/class-llms-turnstile.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/spam/class-llms-recaptcha.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/spam/class-llms-akismet.php';\n\n\t\t// Theme support.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/theme-support/class-llms-theme-support.php';\n\n\t\t// Widgets.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/widgets/class.llms.widget.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/widgets/class.llms.widgets.php';\n\n\t\t// Elementor support.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widgets.php';\n\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-course-completion-page.php';\n\t}\n\n\t/**\n\t * Includes that are required only on the admin panel\n\t *\n\t * @since 4.0.0\n\t * @since 4.7.0 Always load `LLMS_Admin_Reporting`.\n\t * @since 4.8.0 Add `LLMS_Export_API`.\n\t * @since 4.12.0 Class `LLMS_Staging` always loaded instead of only loaded on admin panel.\n\t * @since 5.0.0 Include `LLMS_Forms_Unsupported_Versions` class.\n\t * @since 5.9.0 Drop usage of deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t * @since 7.2.0 Include `LLMS_Admin_Dashboard_Wigdet` class.\n\t *\n\t * @return void\n\t */\n\tpublic function includes_admin() {\n\n\t\t// Functions.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/llms.functions.admin.php';\n\n\t\t// Admin classes.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-header.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-export-download.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-plugins.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-review.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-users-table.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-sendwp.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-mailhawk.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms-unsupported-versions.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-permalinks.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-media-protection-attachment-settings.php';\n\n\t\t// Admin classes (files to be renamed).\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.dashboard.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.dashboard-widget.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.import.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.menus.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.core.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.post-types.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.reviews.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-events-promo.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.user.custom.fields.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-profile.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.student.bulk.enroll.php';\n\n\t\t// Post types.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/class.llms.post.tables.php';\n\n\t\t// Controllers.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/controllers/class.llms.controller.admin.quiz.attempts.php';\n\n\t\t// Reporting.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/widgets/class.llms.analytics.widget.ajax.php';\n\n\t\t// Load setup wizard conditionally.\n\t\tif ( 'llms-setup' === llms_filter_input( INPUT_GET, 'page' ) ) {\n\t\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.setup.wizard.php';\n\t\t}\n\t}\n\n\t/**\n\t * Include libraries\n\t *\n\t * @since 4.0.0\n\t * @since 4.9.0 Adds constants which can be used to identify when included libraries have been loaded.\n\t * @since 5.0.0 Load core libraries from new location, add WP Background Processing lib, add LLMS Helper.\n\t * @since 5.1.3 Add keys to the $libs array and pass them through a filter.\n\t * @since 5.5.0 Add LLMS-CLI to the list of included libraries.\n\t *\n\t * @return void\n\t */\n\tpublic function includes_libraries() {\n\n\t\t$libs = array(\n\t\t\t'blocks' => array(\n\t\t\t\t'const' => 'LLMS_BLOCKS_LIB',\n\t\t\t\t'test'  => function_exists( 'has_blocks' ) && ! defined( 'LLMS_BLOCKS_VERSION' ),\n\t\t\t\t'file'  => LLMS_PLUGIN_DIR . 'libraries/lifterlms-blocks/lifterlms-blocks.php',\n\t\t\t),\n\t\t\t'cli'    => array(\n\t\t\t\t'const' => 'LLMS_CLI_LIB',\n\t\t\t\t'test'  => ! function_exists( 'llms_cli' ),\n\t\t\t\t'file'  => LLMS_PLUGIN_DIR . 'libraries/lifterlms-cli/lifterlms-cli.php',\n\t\t\t),\n\t\t\t'rest'   => array(\n\t\t\t\t'const' => 'LLMS_REST_API_LIB',\n\t\t\t\t'test'  => ! class_exists( 'LifterLMS_REST_API' ),\n\t\t\t\t'file'  => LLMS_PLUGIN_DIR . 'libraries/lifterlms-rest/lifterlms-rest.php',\n\t\t\t),\n\t\t\t'helper' => array(\n\t\t\t\t'const' => 'LLMS_HELPER_LIB',\n\t\t\t\t'test'  => ! class_exists( 'LifterLMS_Helper' ),\n\t\t\t\t'file'  => LLMS_PLUGIN_DIR . 'libraries/lifterlms-helper/lifterlms-helper.php',\n\t\t\t),\n\t\t);\n\n\t\t/**\n\t\t * Filters the list of LifterLMS libraries to be loaded.\n\t\t *\n\t\t * @since 5.1.3\n\t\t *\n\t\t * @param array $libs {\n\t\t *     Array of library data. Each array key serves as a unique ID for the library.\n\t\t *\n\t\t *     @type string $const Name of the constant used to identify if the library is loaded as a library.\n\t\t *     @type bool   $test  A test which is evaluated to determine if the library should be loaded. Returning `false` causes the library not to load.\n\t\t *     @type string $file  Path to the main library file's location in the LifterLMS core plugin.\n\t\t * }\n\t\t */\n\t\t$libs = apply_filters( 'llms_included_libs', $libs );\n\t\tforeach ( $libs as $lib ) {\n\n\t\t\tif ( $lib['test'] ) {\n\t\t\t\tdefine( $lib['const'], true );\n\t\t\t\trequire_once $lib['file'];\n\t\t\t}\n\t\t}\n\n\t\t// Action Scheduler.\n\t\trequire_once LLMS_PLUGIN_DIR . 'vendor/woocommerce/action-scheduler/action-scheduler.php';\n\n\t\t// WP Background Processing.\n\t\trequire_once LLMS_PLUGIN_DIR . 'vendor/deliciousbrains/wp-background-processing/wp-background-processing.php';\n\t}\n\n\t/**\n\t * Includes that are required only on the frontend\n\t *\n\t * @since 4.0.0\n\t * @since 5.0.0 Removed deprecated classes: LLMS_Frontend_Forms & LLMS_Frontend_Password.\n\t *\n\t * @return void\n\t */\n\tpublic function includes_frontend() {\n\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.frontend.assets.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.https.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class.llms.template.loader.php';\n\t}\n}\n\nreturn new LLMS_Loader();\n"
  },
  {
    "path": "includes/class-llms-media-protector.php",
    "content": "<?php\n/**\n * LLMS_Media_Protector class\n *\n * @package LifterLMS/Classes\n *\n * @since 7.7.0\n * @version 7.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Media_Protector class.\n *\n * Allows uploaded media files to be protected from unauthorized downloading.\n *\n * WordPress uses the terms \"media\" and \"attachment\" interchangeably to describe uploaded files.\n * When a file is uploaded to WordPress, a post is created with type = 'attachment' and the file name and path relative\n * to the upload directory, normally `WP_CONTENT_DIR . '/uploads'`, is saved as '_wp_attached_file' metadata.\n *\n * Example of uploading a file:\n *\n *     $protector = new LLMS_Media_Protector( '/social-learning' );\n *     $id        = $protector->handle_upload( 'image', 0, 'llms_sl_authorize_media_view', $post_data );\n *\n * Example of protecting a file:\n *\n *     add_filter( 'llms_sl_authorize_media_view', array( $this, 'authorize_media_view' ), 10, 3 );\n *\n *     public function authorize_media_view( $is_authorized, $media_id, $url ) {\n *         $is_authorized = current_user_can( 'view_others_students' );\n *         return $is_authorized;\n *     }\n *\n * @since 7.7.0\n *\n * @todo Add handling of HTTP range requests. See {@see https://datatracker.ietf.org/doc/html/rfc7233} and\n *       {@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests}.\n * @todo Add WordPress multi-site capability.\n */\nclass LLMS_Media_Protector {\n\n\t/**\n\t * The meta key used to specify the filter hook name that authorizes viewing of a media file.\n\t *\n\t * The key is protected by prefixing it with an underscore '_', which causes WordPress to not display it in\n\t * a custom fields interface. {@see is_protected_meta()}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var string\n\t */\n\tpublic const AUTHORIZATION_FILTER_KEY = '_llms_media_authorization_filter';\n\n\t/**\n\t * Serve the media file by reading and outputting it with the readfile() function.\n\t *\n\t * This is the least efficient way to serve a file because it uses a PHP process instead of a HTTP server thread.\n\t * For small files or a small number of protected files on a page, this may not be noticeable. However, the server's\n\t * configuration may need to be changed to allow more PHP processes to run, which will use more memory.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var int\n\t */\n\tpublic const SERVE_READ_FILE = 1;\n\n\t/**\n\t * Serve the media file by redirecting the HTTP client with a \"Location\" header.\n\t *\n\t * This is the least secure way to serve a file because an unprotected URL is given to the HTTP client.\n\t * It is unlikely, yet possible, that the URL could then be used by an unauthorized user to view the file.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var int\n\t */\n\tpublic const SERVE_REDIRECT = 2;\n\n\t/**\n\t * Serve the media file by sending an \"X-Sendfile\" style header and let the HTTP server serve the file.\n\t *\n\t * This is the most efficient and most secure way to serve a file. It requires one of the following HTTP servers.\n\t * - {@see https://httpd.apache.org/ Apache httpd} with {@see https://tn123.org/mod_xsendfile/ mod_xsendfile}\n\t * - {@see http://cherokee-project.com/doc/other_goodies.html Cherokee}\n\t * - {@see https://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file lighttpd}\n\t * - {@see https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ NGINX}\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var int\n\t */\n\tpublic const SERVE_SEND_FILE = 3;\n\n\t/**\n\t * The name of the URL parameter for whether the media image should be treated as an icon.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var string\n\t */\n\tpublic const URL_PARAMETER_ICON = 'llms_media_icon';\n\n\t/**\n\t * The name of the URL parameter for the media post ID.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var string\n\t */\n\tpublic const URL_PARAMETER_ID = 'llms_media_id';\n\n\t/**\n\t * The name of the URL parameter for when the LifterLMS rewrite rule changes a URL that directly accesses the\n\t * 'llms-uploads' directory into '/index.php?llms_protected_url=llms-uploads/PATH_TO_FILE'.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var string\n\t */\n\tpublic const URL_PARAMETER_PROTECTED_URL = 'llms_protected_url';\n\n\t/**\n\t * The name of the URL parameter for the requested media image size.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var string\n\t */\n\tpublic const URL_PARAMETER_SIZE = 'llms_media_image_size';\n\n\t/**\n\t * An optional path added to the base upload path.\n\t *\n\t * If it is not empty, it will have a leading slash and will not have a trailing slash.\n\t * Normally, the full path is `WP_CONTENT_DIR . \"/uploads/$base/$additional/$year/$month/$file_name\"`.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var string\n\t */\n\tprotected $additional_upload_path = '';\n\n\t/**\n\t * A base path for uploaded LifterLMS files.\n\t *\n\t * If it is not empty, it will have a leading slash and will not have a trailing slash.\n\t * Normally, the full path is `WP_CONTENT_DIR . \"/uploads/$base/$additional/$year/$month/$file_name\"`.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @var string\n\t */\n\tprotected $base_upload_path = '';\n\n\t/**\n\t * Set up this class.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $additional_upload_path This path is added to the base upload path.\n\t * @param string $base_upload_path       This path is appended to the WordPress upload path, which defaults to\n\t *                                       `WP_CONTENT_DIR . '/uploads'` in {@see _wp_upload_dir()}.\n\t * @return void\n\t */\n\tpublic function __construct( $additional_upload_path = '', $base_upload_path = '/lifterlms' ) {\n\n\t\t$this->set_base_upload_path( $base_upload_path );\n\t\t$this->set_additional_upload_path( $additional_upload_path );\n\t}\n\n\t/**\n\t * Adds query parameters to a protected media URL.\n\t *\n\t * Hooked to the {@see 'wp_get_attachment_image_src'} filter in {@see wp_get_attachment_image_src()}\n\t * by {@see LLMS_Media_Protector::register_callbacks()}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param array|false  $image    {\n\t *     Array of image data, or boolean false if no image is available.\n\t *\n\t *     @type string $0 Image source URL.\n\t *     @type int    $1 Image width in pixels.\n\t *     @type int    $2 Image height in pixels.\n\t *     @type bool   $3 Whether the image is a resized image.\n\t * }\n\t * @param int          $media_id The post ID of the image.\n\t * @param string|int[] $size     Requested image size. Can be any registered image size name,\n\t *                               or an array of width and height values in pixels (in that order).\n\t * @param bool         $icon     Whether the image should be treated as an icon.\n\t * @return array|false\n\t */\n\tpublic function authorize_media_image_src( $image, $media_id, $size, $icon ) {\n\n\t\tif ( ! is_numeric( $media_id ) || ! intval( $media_id ) ) {\n\t\t\t// Nothing to verify.\n\t\t\treturn $image;\n\t\t}\n\n\t\t$is_authorized = $this->is_authorized_to_view( get_current_user_id(), $media_id );\n\t\tif ( is_null( $is_authorized ) ) {\n\t\t\t// The media file is not protected.\n\t\t\treturn $image;\n\t\t} elseif ( false === $is_authorized ) {\n\t\t\t// Return the same thing that wp_get_attachment_image_src would return if no image found.\n\t\t\treturn false;\n\t\t}\n\n\t\t$image[0] = add_query_arg(\n\t\t\tarray(\n\t\t\t\tself::URL_PARAMETER_ID   => $media_id,\n\t\t\t\tself::URL_PARAMETER_SIZE => rawurlencode( is_array( $size ) ? wp_json_encode( $size ) : $size ),\n\t\t\t\tself::URL_PARAMETER_ICON => $icon ? 1 : 0,\n\t\t\t),\n\t\t\ttrailingslashit( home_url() )\n\t\t);\n\n\t\treturn $image;\n\t}\n\n\t/**\n\t * Returns the unchanged URL if the media file is not protected,\n\t * else if the user is authorized, returns a URL that triggers {@see LLMS_Media_Protector::serve_file()} when requested,\n\t * else returns a URL to a placeholder file.\n\t *\n\t * The result of this filter is cached for the duration of the current HTTP request.\n\t *\n\t * Hooked to the {@see 'wp_get_attachment_url'} filter in {@see wp_get_attachment_url()}\n\t * by {@see LLMS_Media_Protector::register_callbacks()}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $url      URL for the given media file.\n\t * @param int    $media_id The post ID of the media file.\n\t * @return string\n\t */\n\tpublic function authorize_media_url( $url, $media_id ) {\n\n\t\t$is_authorized = $this->is_authorized_to_view( get_current_user_id(), $media_id );\n\t\tif ( true === $is_authorized ) {\n\t\t\t$url = add_query_arg(\n\t\t\t\tarray( self::URL_PARAMETER_ID => $media_id ),\n\t\t\t\ttrailingslashit( home_url() )\n\t\t\t);\n\t\t} elseif ( false === $is_authorized ) {\n\t\t\t$url = '';\n\t\t}\n\t\t// If $is_authorized is null, do not change $url because it is unprotected.\n\n\t\treturn $url;\n\t}\n\n\t/**\n\t * Modify the media upload directory if this is a LifterLMS request.\n\t *\n\t * @param $params\n\t *\n\t * @return array\n\t */\n\tpublic function change_media_upload_directory( $params ) {\n\t\tif ( isset( $_REQUEST['llms'] ) && '1' === $_REQUEST['llms'] ) {\n\t\t\t$params = $this->upload_dir( $params );\n\t\t}\n\n\t\treturn $params;\n\t}\n\n\t/**\n\t * Adds authorization meta after an attachment is added.\n\t *\n\t * @param $media_id\n\t *\n\t * @return void\n\t */\n\tpublic function add_authorization_meta_after_attachment_added( $media_id ) {\n\t\t$attachment = get_post( $media_id );\n\t\tif ( $attachment && 'attachment' === $attachment->post_type && isset( $_REQUEST['llms'] ) && '1' === $_REQUEST['llms'] ) {\n\t\t\t$this->add_authorization_meta_to_media_post( $media_id );\n\t\t}\n\t}\n\n\t/**\n\t * Returns a path path with a leading slash and without a trailing slash, or if the given path is empty, an empty string.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $path The path to be formatted.\n\t * @return string An empty string or a path with a leading slash and without a trailing slash.\n\t */\n\tprotected function format_path( $path ) {\n\n\t\tif ( '' === $path ) {\n\t\t\treturn $path;\n\t\t}\n\n\t\t// Add leading slash.\n\t\tif ( strpos( $path, '/' ) !== 0 ) {\n\t\t\t$path = '/' . $path;\n\t\t}\n\n\t\t// Strip trailing slash.\n\t\t$path = untrailingslashit( $path );\n\n\t\treturn $path;\n\t}\n\n\t/**\n\t * Returns the additional path that is added onto the base path.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_additional_upload_path() {\n\n\t\treturn $this->additional_upload_path;\n\t}\n\n\t/**\n\t * Returns the base upload path.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_base_upload_path() {\n\n\t\treturn $this->base_upload_path;\n\t}\n\n\t/**\n\t * Returns the absolute path to the media file in the upload directory.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param int $media_id The media post ID.\n\t * @return string\n\t */\n\tpublic function get_media_path( $media_id ) {\n\n\t\t$upload_dir = wp_upload_dir();\n\t\t$file_name  = get_post_meta( $media_id, '_wp_attached_file', true );\n\n\t\treturn $upload_dir['basedir'] . DIRECTORY_SEPARATOR . $file_name;\n\t}\n\n\t/**\n\t * Gets the size from the URL query parameter.\n\t *\n\t * @see wp_create_image_subsizes()\n\t * @since 7.7.0\n\t *\n\t * @return string|int[]|null\n\t */\n\tprotected function get_size() {\n\n\t\t$size = ( isset( $_GET[ self::URL_PARAMETER_SIZE ] ) ) ? sanitize_text_field( wp_unslash( $_GET[ self::URL_PARAMETER_SIZE ] ) ) : null;\n\t\tif ( false === $size ) {\n\t\t\t$size = null;\n\t\t} elseif ( is_string( $size ) && '[' === $size[0] ) {\n\t\t\t$size = json_decode( $size );\n\t\t\t// Sanitize untrusted external input.\n\t\t\tif ( isset( $size[0] ) ) {\n\t\t\t\t$size[0] = (int) $size[0];\n\t\t\t}\n\t\t\tif ( isset( $size[1] ) ) {\n\t\t\t\t$size[1] = (int) $size[1];\n\t\t\t}\n\t\t}\n\n\t\treturn $size;\n\t}\n\n\t/**\n\t * Saves a file submitted from a POST request and creates an attachment post for it.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $file_id   Index of the `$_FILES` array that the file was sent. Required.\n\t * @param int    $post_id   The post ID of a post to attach the media item to. Required, but can\n\t *                          be set to 0, creating a media item that has no relationship to a post.\n\t * @param string $hook_name The name of the filter that will be applied by {@see LLMS_Media_Protector::is_authorized_to_view()}.\n\t * @param array  $post_data Optional. Set attachment elements that are sent to {@see wp_insert_post()}.\n\t *                          The defaults are set in {@see media_handle_upload()}.\n\t * @param array  $overrides Optional. Override the {@see wp_handle_upload()} behavior.\n\t * @return int|WP_Error Post ID of the media file or a WP_Error object on failure.\n\t */\n\tpublic function handle_upload(\n\t\t$file_id,\n\t\t$post_id,\n\t\t$hook_name,\n\t\t$post_data = array(),\n\t\t$overrides = array( 'test_form' => false )\n\t) {\n\n\t\t$post_data['meta_input'][ self::AUTHORIZATION_FILTER_KEY ] = $hook_name;\n\t\tadd_filter( 'upload_dir', array( $this, 'upload_dir' ), 10, 1 );\n\t\t$media_id = media_handle_upload( $file_id, $post_id, $post_data, $overrides );\n\t\tremove_filter( 'upload_dir', array( $this, 'upload_dir' ), 10 );\n\t\t$this->add_authorization_meta_to_media_post( $media_id );\n\n\t\treturn $media_id;\n\t}\n\n\t/**\n\t * See if the media is protected with an authorization filter.\n\t *\n\t * @param $media_id\n\t *\n\t * @return bool\n\t */\n\tpublic function is_media_protected( $media_id ) {\n\t\treturn (bool) get_post_meta( $media_id, self::AUTHORIZATION_FILTER_KEY, true );\n\t}\n\n\t/**\n\t * Returns true if the user is authorized to view the requested media file, false if not authorized,\n\t * or null if the media file is not protected.\n\t *\n\t * Authorization is handled by the callback added to the filter hook name given to {@see LLMS_Media_Protector::handle_upload()}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param int $user_id  The user ID.\n\t * @param int $media_id The post ID of the media file.\n\t * @return bool|null\n\t */\n\tpublic function is_authorized_to_view( $user_id, $media_id ): ?bool {\n\t\tif ( ! is_numeric( $media_id ) || ! intval( $media_id ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$cache_key     = 'llms-media-authorization-' . $media_id . '-' . $user_id;\n\t\t$authorization = wp_cache_get( $cache_key, 'llms_media_authorization', false, $found );\n\t\tif ( $found ) {\n\t\t\treturn ( ( $authorization === 'null' ) ? null : $authorization );\n\t\t}\n\n\t\t$authorization_filter = get_post_meta( $media_id, self::AUTHORIZATION_FILTER_KEY, true );\n\t\tif ( ! $authorization_filter ) {\n\t\t\t// We need to use string of 'null' since on some hosting like wordpress.com the value of null comes back as bool false.\n\t\t\twp_cache_add( $cache_key, 'null', 'llms_media_authorization' );\n\n\t\t\treturn null;\n\t\t}\n\n\t\t// The default is to allow WordPress super admins and LifterLMS managers to view all protected media files.\n\t\t// @todo Consider allowing users with the some of the 'students' capabilities.\n\t\tif ( is_super_admin( $user_id ) ) {\n\t\t\t$is_authorized = true;\n\t\t} else {\n\t\t\t$user          = wp_get_current_user();\n\t\t\t$is_authorized = in_array( 'llms_manager', $user->roles, true ) || intval( get_post_field( 'post_author', $media_id ) ) === $user_id;\n\t\t}\n\n\t\t// Allow student to view if they have an incomplete attempt for a quiz this media is for.\n\t\tif ( ! $is_authorized && llms_get_student() ) {\n\t\t\t// Check if the student is enrolled in a course that has access to the media.\n\t\t\t$authorized_product_id = get_post_meta( $media_id, '_llms_media_protection_product_id', true );\n\t\t\tif ( $authorized_product_id && (\n\t\t\t\tllms_get_student()->is_enrolled( $authorized_product_id ) ||\n\t\t\t\tllms_current_user_can_edit_product( $authorized_product_id )\n\t\t\t\t) ) {\n\t\t\t\t$is_authorized = true;\n\t\t\t}\n\n\t\t\tif ( ! $is_authorized ) {\n\t\t\t\t$authorized_quiz_ids = (array) get_post_meta( $media_id, '_llms_quiz_id', true );\n\n\t\t\t\tif ( $authorized_quiz_ids ) {\n\t\t\t\t\t$student_quizzes = llms_get_student()->quizzes()->get_all( $authorized_quiz_ids );\n\t\t\t\t\tforeach ( $student_quizzes as $student_quiz_attempt ) {\n\t\t\t\t\t\t$quiz_id = $student_quiz_attempt->get( 'quiz_id' );\n\t\t\t\t\t\tif ( ! ( new LLMS_Quiz( $quiz_id ) )->is_open() ) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ( 'incomplete' === $student_quiz_attempt->get( 'status' ) ) {\n\t\t\t\t\t\t\t$is_authorized = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Allow the plugin that is protecting the file to authorize access to it.\n\t\t *\n\t\t * The default is to allow the user to view the file in case there is a not a callback for the authorization hook.\n\t\t *\n\t\t * @since 7.7.0\n\t\t *\n\t\t * @param bool|null $is_authorized True if the user is authorized to view the media file, false if not authorized,\n\t\t *                                 or null if the file is not protected.\n\t\t * @param int       $media_id      The post ID of the media file.\n\t\t * @param int       $user_id       The ID of the user wanting to view the media file.\n\t\t */\n\t\t$is_authorized = apply_filters( $authorization_filter, $is_authorized, $media_id, $user_id );\n\n\t\t// Sanitize value.\n\t\tif ( ! is_bool( $is_authorized ) && ! is_null( $is_authorized ) ) {\n\t\t\t$is_authorized = (bool) $is_authorized;\n\t\t}\n\n\t\t/**\n\t\t * Determine how long the media authorization is valid for.\n\t\t *\n\t\t * @since 7.7.0\n\t\t *\n\t\t * @param int   $cache_expiration    Time in seconds to cache the authorization for this media file and user.\n\t\t * @param int   $media_id            The post ID of the media file.\n\t\t * @param int   $user_id             The ID of the user wanting to view the media file.\n\t\t */\n\t\t$cache_expiration = apply_filters( 'llms_media_protection_cache_expiration_time', MINUTE_IN_SECONDS * 1, $media_id, $user_id );\n\n\t\twp_cache_add( $cache_key, $is_authorized, 'llms_media_authorization', $cache_expiration );\n\n\t\treturn $is_authorized;\n\t}\n\n\t/**\n\t * Returns true if the current request has a different modification date or entity tag than the requested file.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $file_name The complete path and file name that the request is for.\n\t * @param string $entity_tag {@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag}.\n\t * @return bool\n\t */\n\tprotected function is_requested_file_modified( $file_name, $entity_tag ): bool {\n\n\t\t$is_modified = true;\n\n\t\t$file_modified     = filemtime( $file_name );\n\t\t$if_modified_since = ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) : '';\n\t\tif ( strtotime( $if_modified_since ) === $file_modified ) {\n\t\t\t$is_modified = false;\n\t\t}\n\n\t\t$if_match = llms_filter_input( INPUT_SERVER, 'HTTP_IF_MATCH', FILTER_SANITIZE_URL );\n\t\tif ( $if_match === $entity_tag ) {\n\t\t\t$is_modified = false;\n\t\t}\n\n\t\treturn $is_modified;\n\t}\n\n\t/**\n\t * Changes the URLs for image attachments prepared for JavaScript.\n\t *\n\t * Hooked to the {@see 'wp_prepare_attachment_for_js'} filter in {@see wp_prepare_attachment_for_js()}\n\t * by {@see LLMS_Media_Protector::register_callbacks()}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param array       $response   Array of prepared attachment data.\n\t * @param WP_Post     $attachment Attachment object.\n\t * @param array|false $meta       Array of attachment meta data, or false if there is none.\n\t * @return array\n\t */\n\tpublic function prepare_attachment_for_js( $response, $attachment, $meta ) {\n\n\t\t$is_authorized = $this->is_authorized_to_view( get_current_user_id(), $attachment->ID );\n\t\tif ( is_null( $is_authorized ) || ! array_key_exists( 'sizes', $response ) ) {\n\t\t\treturn $response;\n\t\t}\n\n\t\tforeach ( $response['sizes'] as $size => &$size_meta ) {\n\t\t\t$size_meta['url'] = add_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\tself::URL_PARAMETER_ID   => $attachment->ID,\n\t\t\t\t\tself::URL_PARAMETER_SIZE => $size,\n\t\t\t\t),\n\t\t\t\ttrailingslashit( home_url() )\n\t\t\t);\n\t\t}\n\n\t\treturn $response;\n\t}\n\n\t/**\n\t * Reads and outputs the file.\n\t *\n\t * This method sends the entire file and does not handle\n\t * {@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests HTTP range requests}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $file_name The file path and name.\n\t * @return void\n\t */\n\tprotected function read_file( $file_name ): void {\n\n\t\t// @todo What about the web server time limit?\n\t\tset_time_limit( 0 );\n\n\t\t// Tell the HTTP client that we do not handle HTTP range requests.\n\t\theader( 'Accept-Ranges: none' );\n\n\t\t// Turn off all output buffers to avoid running out of memory with large files.\n\t\t// @see https://www.php.net/readfile#refsect1-function.readfile-notes.\n\t\twp_ob_end_flush_all();\n\n\t\t$result = readfile( $file_name ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile\n\t\tif ( false === $result ) {\n\t\t\t// Tell the HTTP client that something unspecific went wrong. readfile() outputs warnings to the PHP error log.\n\t\t\theader( 'HTTP/1.1 500 Internal Server Error' );\n\t\t}\n\t}\n\n\t/**\n\t * Registers the callback functions for action and filter hooks that allow this class to protect uploaded media files.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return self\n\t */\n\tpublic function register_callbacks() {\n\n\t\tif (\n\t\t\t// phpcs:disable WordPress.Security.NonceVerification.Recommended\n\t\t\tarray_key_exists( self::URL_PARAMETER_ID, $_GET ) ||\n\t\t\tarray_key_exists( self::URL_PARAMETER_PROTECTED_URL, $_GET )\n\t\t\t// phpcs:enable WordPress.Security.NonceVerification.Recommended\n\t\t) {\n\t\t\tadd_action( 'init', array( $this, 'serve_file' ), 10 );\n\t\t} else {\n\t\t\tadd_filter( 'admin_init', array( $this, 'save_mod_rewrite_rules' ), 10, 1 );\n\t\t\tadd_filter( 'wp_prepare_attachment_for_js', array( $this, 'prepare_attachment_for_js' ), 99, 3 );\n\t\t\tadd_filter( 'wp_get_attachment_image_src', array( $this, 'authorize_media_image_src' ), 10, 4 );\n\t\t\tadd_filter( 'wp_get_attachment_url', array( $this, 'authorize_media_url' ), 10, 2 );\n\t\t\tadd_filter( 'upload_dir', array( $this, 'change_media_upload_directory' ), 10, 1 );\n\t\t\tadd_action( 'add_attachment', array( $this, 'add_authorization_meta_after_attachment_added' ), 10, 1 );\n\t\t}\n\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Adds .htaccess and blank index.php/html files to the upload directory to protect the files from being listed.\n\t *\n\t * Hooked to the {@see 'flush_rewrite_rules_hard'} filter in {@see WP_Rewrite::flush_rules()}\n\t * by {@see LLMS_Media_Protector::register_callbacks()}.\n\t *\n\t * @since 7.7.0\n\t */\n\tpublic function save_mod_rewrite_rules() {\n\t\t// TODO: Different for multi-site?\n\n\t\tif ( false === get_transient( 'lifterlms_check_media_protection_files' ) ) {\n\t\t\tglobal $wp_filesystem;\n\t\t\t/** @var WP_Filesystem_Base $wp_filesystem */\n\n\t\t\t/** Load files that define {@see WP_Filesystem()}, {@see media_handle_sideload()}, and many image functions. */\n\t\t\trequire_once ABSPATH . 'wp-admin/includes/file.php';\n\n\t\t\tWP_Filesystem();\n\n\t\t\t$uploads = wp_get_upload_dir();\n\n\t\t\t$upload_path   = $uploads['basedir'] . $this->get_base_upload_path();\n\t\t\t$htaccess_file = $upload_path . '/.htaccess';\n\n\t\t\t$upload_path_writeable = $wp_filesystem->is_writable( $upload_path );\n\n\t\t\t$rules  = \"Options -Indexes\\n\";\n\t\t\t$rules .= \"deny from all\\n\";\n\n\t\t\tif ( $upload_path_writeable && ! $wp_filesystem->exists( $htaccess_file ) ) {\n\t\t\t\t$wp_filesystem->put_contents( $htaccess_file, $rules, 0644 );\n\t\t\t} elseif ( $upload_path_writeable ) {\n\t\t\t\t$contents = $wp_filesystem->get_contents( $htaccess_file );\n\t\t\t\tif ( $contents !== $rules ) {\n\t\t\t\t\t$wp_filesystem->put_contents( $htaccess_file, $rules, 0644 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( $upload_path_writeable && ! $wp_filesystem->exists( $upload_path . '/index.php' ) ) {\n\t\t\t\t$wp_filesystem->put_contents( $upload_path . '/index.php', '<?php' . PHP_EOL . '// Silence is golden.' );\n\t\t\t}\n\n\t\t\tif ( $upload_path_writeable && ! $wp_filesystem->exists( $upload_path . '/index.html' ) ) {\n\t\t\t\t$wp_filesystem->put_contents( $upload_path . '/index.html', '' );\n\t\t\t}\n\n\t\t\t// Get the main directories in the root of the directory we're scanning.\n\t\t\t$upload_root_dirs = glob( $upload_path . '/*', GLOB_ONLYDIR | GLOB_NOSORT | GLOB_MARK );\n\n\t\t\t// Now get all the recursive directories.\n\t\t\t$upload_sub_dirs = glob( $upload_path . '/*/**', GLOB_ONLYDIR | GLOB_NOSORT | GLOB_MARK );\n\n\t\t\t// Merge the two arrays together, and avoid any possible duplicates.\n\t\t\tforeach ( array_unique( array_merge( $upload_root_dirs, $upload_sub_dirs ) ) as $dir ) {\n\t\t\t\tif ( ! wp_is_writable( $dir ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Create index.php, if it doesn't exist.\n\t\t\t\tif ( ! $wp_filesystem->exists( $dir . 'index.php' ) ) {\n\t\t\t\t\t$wp_filesystem->put_contents( $dir . 'index.php', '<?php' . PHP_EOL . '// Silence is golden.' );\n\t\t\t\t}\n\n\t\t\t\tif ( ! $wp_filesystem->exists( $dir . 'index.html' ) ) {\n\t\t\t\t\t$wp_filesystem->put_contents( $dir . 'index.html', '' );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tset_transient( 'lifterlms_check_media_protection_files', true, DAY_IN_SECONDS );\n\t\t}\n\t}\n\n\t/**\n\t * Outputs an X-Sendfile or X-Accel-Redirect HTTP header which will instruct the HTTP server\n\t * to send the file so that PHP doesn't have to.\n\t *\n\t * If none of the following HTTP servers are detected, {@see LLMS_Media_Protector::read_file()} is called.\n\t * - {@see https://tn123.org/mod_xsendfile/ Apache mod_xsendfile}\n\t * - {@see https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModCGI Lighttpd}\n\t * - {@see https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ NGINX}\n\t * - {@see https://cherokee-project.com/doc/other_goodies.html#x-sendfile Cherokee}\n\t *\n\t * Add `$_SERVER['MOD_X_SENDFILE_ENABLED'] = '1';` in `wp-config.php` if web server auto-detection isn't working.\n\t *\n\t * IIS administrators may want to use {@see https://github.com/stakach/IIS-X-Sendfile-plugin}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $file_name The file path and name.\n\t * @param int    $media_id  The post ID of the media file. Not used in this implementation, but here for consistency\n\t *                          with the other \"serve\" methods and may be useful in an overriding this method.\n\t * @return void\n\t */\n\tprotected function send_file( $file_name, $media_id ) {\n\t\t$server_software = ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '' );\n\n\t\tif (\n\t\t\t( array_key_exists( 'MOD_X_SENDFILE_ENABLED', $_SERVER ) && '1' === $_SERVER['MOD_X_SENDFILE_ENABLED'] ) ||\n\t\t\t( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules(), true ) ) ||\n\t\t\tstristr( $server_software, 'cherokee' ) ||\n\t\t\tstristr( $server_software, 'lighttpd' )\n\t\t) {\n\t\t\theader( \"X-Sendfile: $file_name\" );\n\n\t\t} elseif ( stristr( $server_software, 'nginx' ) ) {\n\t\t\t/**\n\t\t\t * @see https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/\n\t\t\t * @see https://woocommerce.com/document/digital-downloadable-product-handling/#nginx-setting\n\t\t\t */\n\t\t\t// NGINX requires a URI without the server's root path.\n\t\t\t$wp_root = $this->find_wp_root();\n\n\t\t\tif ( $wp_root ) {\n\t\t\t\t$nginx_file_name = substr( $file_name, strlen( $this->find_wp_root() ) );\n\t\t\t\theader( 'X-Accel-Redirect: ' . urlencode( $nginx_file_name ) );\n\t\t\t} else {\n\t\t\t\t$this->read_file( $file_name );\n\t\t\t}\n\t\t} else {\n\t\t\t$this->read_file( $file_name );\n\t\t}\n\t}\n\n\tprotected function find_wp_root() {\n\t\t$dir = dirname( WP_CONTENT_DIR );\n\t\twhile ( $dir ) {\n\t\t\tif ( file_exists( $dir . '/wp-load.php' ) || file_exists( $dir . '/wp-config.php' ) ) {\n\t\t\t\treturn $dir;\n\t\t\t}\n\n\t\t\t$parent_dir = dirname( $dir );\n\t\t\tif ( $parent_dir === $dir ) {\n\t\t\t\t// We have reached the root directory\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t$dir = $parent_dir;\n\t\t}\n\t\treturn false; // Root directory not found\n\t}\n\n\n\t/**\n\t * Send headers for the download.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $file_name The file path and name.\n\t * @param int    $media_id  The post ID of the media file.\n\t * @return void\n\t */\n\tprotected function send_headers( $file_name, $media_id ) {\n\n\t\t$file_size = @filesize( $file_name ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\n\t\tif ( ! $file_size ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$media_file   = get_post( $media_id );\n\t\t$content_type = $media_file->post_mime_type;\n\n\t\theader( \"Content-Type: $content_type\" );\n\t\theader( \"Content-Length: $file_size\" );\n\t\theader( 'X-Robots-Tag: noindex, nofollow', true );\n\t}\n\n\t/**\n\t * Sends a header that redirects the HTTP client to the media file's URL.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param int               $media_id The post ID of the media file.\n\t * @param string|int[]|null $size     A registered image size name, or an array of width and height values in pixels.\n\t * @param bool|null         $icon     Whether the image should fall back to a mime type icon.\n\t * @return void\n\t */\n\tprotected function send_redirect( $media_id, $size, $icon ): void {\n\n\t\tif ( is_null( $size ) && is_null( $icon ) ) {\n\t\t\t$url = wp_get_attachment_url( $media_id );\n\t\t} else {\n\t\t\t$url = wp_get_attachment_image_url( $media_id, $size, $icon );\n\t\t}\n\n\t\theader( \"Location: $url\" );\n\t}\n\n\tprotected function strip_query_params( $file_name ) {\n\t\t$parsed_url = wp_parse_url( $file_name );\n\t\t$path       = isset( $parsed_url['path'] ) ? $parsed_url['path'] : $file_name;\n\t\treturn $path;\n\t}\n\n\t/**\n\t * Serves the requested media file to the HTTP client.\n\t *\n\t * This method calls the {@see llms_exit()} function and does not return.\n\t *\n\t * Hooked to the {@see 'init'} filter by {@see LLMS_Media_Protector::register_callbacks()}.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t * @throws LLMS_Unit_Test_Exception_Exit Thrown during unit testing instead of exiting.\n\t */\n\tpublic function serve_file() {\n\n\t\t$media_id = llms_filter_input( INPUT_GET, self::URL_PARAMETER_ID, FILTER_SANITIZE_NUMBER_INT );\n\n\t\t// Handle a rewritten URL.\n\t\t// e.g. `/wp-content/uploads/llms-uploads/2022/01/image.png` is changed by the LifterLMS mod_rewrite rule\n\t\t// into `/index.php?llms-uploads=llms_protected_url/2022/01/image.png`.\n\t\tif ( empty( $media_id ) ) {\n\t\t\t$attached_file = llms_filter_input( INPUT_GET, self::URL_PARAMETER_PROTECTED_URL, FILTER_SANITIZE_URL );\n\n\t\t\t/** Extract the optional size. {@see WP_Image_Editor::get_suffix()} and {@see WP_Image_Editor::generate_filename()} */\n\t\t\t$result = preg_match( '/^(.+?)-(\\d+x\\d+)(.+)$/', $attached_file, $matches );\n\t\t\tif ( $result ) {\n\t\t\t\t$attached_file = $matches[1] . $matches[3];\n\t\t\t\t$size          = explode( 'x', $matches[2] );\n\t\t\t\t$size          = array_map( 'intval', $size );\n\t\t\t}\n\n\t\t\t$query    = new WP_Query(\n\t\t\t\tarray(\n\t\t\t\t\t'fields'      => 'ids',\n\t\t\t\t\t'meta_key'    => '_wp_attached_file',\n\t\t\t\t\t'meta_value'  => $attached_file,\n\t\t\t\t\t'post_status' => 'any',\n\t\t\t\t\t'post_type'   => 'attachment',\n\t\t\t\t)\n\t\t\t);\n\t\t\t$media_id = reset( $query->posts );\n\t\t}\n\n\t\t// The auth filter meta needs to exist for the media file to be served by this method.\n\t\tif ( ! get_post_meta( $media_id, self::AUTHORIZATION_FILTER_KEY, true ) ) {\n\t\t\theader( 'HTTP/1.1 404 Not Found' );\n\t\t\tllms_exit();\n\t\t}\n\n\t\t$media_file = get_post( $media_id );\n\n\t\t// Validate that the attachment post exists.\n\t\tif ( is_null( $media_file ) || 'attachment' !== $media_file->post_type ) {\n\t\t\theader( 'HTTP/1.1 404 Not Found' );\n\t\t\tllms_exit();\n\t\t}\n\n\t\t$file_name = $this->get_media_path( $media_id );\n\n\t\t// Optionally, use an alternate image size.\n\t\tif ( ! isset( $size ) ) {\n\t\t\t$size = $this->get_size();\n\t\t}\n\t\t$icon = ( isset( $_GET[ self::URL_PARAMETER_ICON ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::URL_PARAMETER_ICON ] ) ) : null );\n\t\tif ( ! is_null( $size ) || $icon ) {\n\t\t\t$image     = wp_get_attachment_image_src( $media_id, $size, $icon );\n\t\t\t$file_name = dirname( $file_name ) . '/' . basename( $image[0] );\n\t\t}\n\n\t\t$file_name = $this->strip_query_params( $file_name );\n\n\t\t// Validate that the media file exists.\n\t\tif ( false === file_exists( $file_name ) ) {\n\t\t\theader( 'HTTP/1.1 404 Not Found' );\n\t\t\tllms_exit();\n\t\t}\n\n\t\t// Is the user authorized to view the file?\n\t\t$is_authorized = $this->is_authorized_to_view( get_current_user_id(), $media_id );\n\t\tif ( false === $is_authorized ) {\n\t\t\tstatus_header( 404 );\n\t\t\tnocache_headers();\n\t\t\tdie( 'File not found.' );\n\t\t}\n\n\t\t// An HTTP client, but not a proxy, is allowed to cache the file, but must check with the server before reuse.\n\t\t$entity_tag = '\"' . md5_file( $file_name ) . '\"';\n\t\tif ( false === $this->is_requested_file_modified( $file_name, $entity_tag ) ) {\n\t\t\theader( 'HTTP/1.1 304 Not Modified' );\n\t\t\tllms_exit();\n\t\t}\n\t\theader( 'Cache-Control: private, no-cache' );\n\t\theader( \"Etag: $entity_tag\" );\n\n\t\t$serve_method = self::SERVE_SEND_FILE;\n\n\t\t// If WPEngine, we need to use the slower read file method as send file does not work.\n\t\tif ( function_exists( 'is_wpe' ) && is_wpe() ) {\n\t\t\t$serve_method = self::SERVE_READ_FILE;\n\t\t}\n\n\t\t/**\n\t\t * Determine how the media file should be served.\n\t\t *\n\t\t * @since 7.7.0\n\t\t *\n\t\t * @param string    $serve_method  One of the LLMS_Media_Protector::SERVE_X constants, {@see LLMS_Media_Protector::SERVE_SEND_FILE}.\n\t\t * @param int       $media_id      The post ID of the media file.\n\t\t * @param bool|null $is_authorized True if the user is authorized to view the requested media file,\n\t\t *                                 false if not authorized, or null if the media file is not protected.\n\t\t */\n\t\t$serve_method = apply_filters( 'llms_media_serve_method', $serve_method, $media_id, $is_authorized );\n\n\t\t// Don't use 'llms-uploads=' rewrite + send_redirect() at the same time. Otherwise there will be an infinite loop\n\t\t// of HTTP requests for the file and HTTP responses with a '302 Found' redirect back to the same file.\n\t\tif ( self::SERVE_REDIRECT === $serve_method && isset( $attached_file ) ) {\n\t\t\t$serve_method = self::SERVE_READ_FILE;\n\t\t}\n\n\t\tswitch ( $serve_method ) {\n\t\t\tcase self::SERVE_READ_FILE:\n\t\t\t\t$this->send_headers( $file_name, $media_id );\n\t\t\t\t$this->read_file( $file_name );\n\t\t\t\tbreak;\n\t\t\tcase self::SERVE_SEND_FILE:\n\t\t\t\t$this->send_headers( $file_name, $media_id );\n\t\t\t\t$this->send_file( $file_name, $media_id );\n\t\t\t\tbreak;\n\t\t\tcase self::SERVE_REDIRECT:\n\t\t\tdefault:\n\t\t\t\t$this->send_redirect( $media_id, $size, $icon );\n\t\t\t\tbreak;\n\t\t}\n\n\t\tllms_exit();\n\t}\n\n\t/**\n\t * Sanitizes and sets the additional upload path that is appended to the base upload path.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $additional_upload_path\n\t * @return self\n\t */\n\tpublic function set_additional_upload_path( $additional_upload_path ): self {\n\n\t\t$this->additional_upload_path = $this->format_path( $additional_upload_path );\n\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Sanitizes and sets the base upload path relative to `WP_CONTENT_DIR . '/uploads'`.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param string $base_upload_path\n\t * @return self\n\t */\n\tpublic function set_base_upload_path( $base_upload_path ): self {\n\n\t\t$this->base_upload_path = $this->format_path( $base_upload_path );\n\n\t\treturn $this;\n\t}\n\n\tpublic function get_upload_basedir() {\n\t\treturn trailingslashit( $this->base_upload_path . $this->additional_upload_path );\n\t}\n\n\t/**\n\t * Filters the 'uploads' directory data.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @param array $uploads {\n\t *     Array of information about the upload directory.\n\t *\n\t *     @type string       $path    Base directory and subdirectory or full path to upload directory.\n\t *     @type string       $url     Base URL and subdirectory or absolute URL to upload directory.\n\t *     @type string       $subdir  Subdirectory if uploads use year/month folders option is on.\n\t *     @type string       $basedir Path without subdirectory.\n\t *     @type string       $baseurl URL path without subdirectory.\n\t *     @type string|false $error   False or error message.\n\t * }\n\t * @return array\n\t */\n\tpublic function upload_dir( $uploads ) {\n\t\t$uploads['subdir'] = trailingslashit( $this->get_upload_basedir() ) . date( 'Y/m' );\n\t\t$uploads['path']   = $uploads['basedir'] . $uploads['subdir'];\n\t\t$uploads['url']    = $uploads['baseurl'] . $uploads['subdir'];\n\n\t\treturn $uploads;\n\t}\n\n\t/**\n\t * Add authorization meta to the post.\n\t *\n\t * @param $post_id\n\t * @param string $hook_name The name of the filter that will be applied by {@see LLMS_Media_Protector::is_authorized_to_view()}.\n\t *\n\t * @return void\n\t */\n\tpublic function add_authorization_meta_to_media_post( $post_id, $hook_name = 'llms_attachment_is_access_allowed' ) {\n\t\tif ( ! is_numeric( $post_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tupdate_post_meta( $post_id, self::AUTHORIZATION_FILTER_KEY, $hook_name );\n\t}\n}\n"
  },
  {
    "path": "includes/class-llms-mime-type-extractor.php",
    "content": "<?php\n/**\n * LifterLMS Mime Type Extractor\n *\n * @package LifterLMS/Classes\n *\n * @since 3.38.1\n * @version 3.38.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Mime_Type_Extractor class\n *\n * @since 3.38.1\n */\nclass LLMS_Mime_Type_Extractor {\n\n\tconst DEFAULT_MIME_TYPE = 'application/octet-stream';\n\n\t/**\n\t * Mime Type List\n\t *\n\t * @var array\n\t */\n\tprivate static $mime_type_list = array(\n\t\t'zip'     => 'application/zip',\n\t\t'gz'      => 'application/gzip',\n\t\t'bz2'     => 'application/x-bzip',\n\t\t'tar'     => 'application/x-tar',\n\t\t'3gp'     => 'video/3gpp',\n\t\t'ai'      => 'application/postscript',\n\t\t'aif'     => 'audio/x-aiff',\n\t\t'aifc'    => 'audio/x-aiff',\n\t\t'aiff'    => 'audio/x-aiff',\n\t\t'asc'     => 'text/plain',\n\t\t'atom'    => 'application/atom+xml',\n\t\t'au'      => 'audio/basic',\n\t\t'avi'     => 'video/x-msvideo',\n\t\t'bcpio'   => 'application/x-bcpio',\n\t\t'bin'     => 'application/octet-stream',\n\t\t'bmp'     => 'image/bmp',\n\t\t'cdf'     => 'application/x-netcdf',\n\t\t'cgm'     => 'image/cgm',\n\t\t'class'   => 'application/octet-stream',\n\t\t'cpio'    => 'application/x-cpio',\n\t\t'cpt'     => 'application/mac-compactpro',\n\t\t'csh'     => 'application/x-csh',\n\t\t'css'     => 'text/css',\n\t\t'dcr'     => 'application/x-director',\n\t\t'dif'     => 'video/x-dv',\n\t\t'dir'     => 'application/x-director',\n\t\t'djv'     => 'image/vnd.djvu',\n\t\t'djvu'    => 'image/vnd.djvu',\n\t\t'dll'     => 'application/octet-stream',\n\t\t'dmg'     => 'application/octet-stream',\n\t\t'dms'     => 'application/octet-stream',\n\t\t'doc'     => 'application/msword',\n\t\t'dtd'     => 'application/xml-dtd',\n\t\t'dv'      => 'video/x-dv',\n\t\t'dvi'     => 'application/x-dvi',\n\t\t'dxr'     => 'application/x-director',\n\t\t'eps'     => 'application/postscript',\n\t\t'etx'     => 'text/x-setext',\n\t\t'exe'     => 'application/octet-stream',\n\t\t'ez'      => 'application/andrew-inset',\n\t\t'flv'     => 'video/x-flv',\n\t\t'gif'     => 'image/gif',\n\t\t'gram'    => 'application/srgs',\n\t\t'grxml'   => 'application/srgs+xml',\n\t\t'gtar'    => 'application/x-gtar',\n\t\t'hdf'     => 'application/x-hdf',\n\t\t'hqx'     => 'application/mac-binhex40',\n\t\t'htm'     => 'text/html',\n\t\t'html'    => 'text/html',\n\t\t'ice'     => 'x-conference/x-cooltalk',\n\t\t'ico'     => 'image/x-icon',\n\t\t'ics'     => 'text/calendar',\n\t\t'ief'     => 'image/ief',\n\t\t'ifb'     => 'text/calendar',\n\t\t'iges'    => 'model/iges',\n\t\t'igs'     => 'model/iges',\n\t\t'jnlp'    => 'application/x-java-jnlp-file',\n\t\t'jp2'     => 'image/jp2',\n\t\t'jpe'     => 'image/jpeg',\n\t\t'jpeg'    => 'image/jpeg',\n\t\t'jpg'     => 'image/jpeg',\n\t\t'js'      => 'application/x-javascript',\n\t\t'kar'     => 'audio/midi',\n\t\t'latex'   => 'application/x-latex',\n\t\t'lha'     => 'application/octet-stream',\n\t\t'lzh'     => 'application/octet-stream',\n\t\t'm3u'     => 'audio/x-mpegurl',\n\t\t'm4a'     => 'audio/mp4a-latm',\n\t\t'm4p'     => 'audio/mp4a-latm',\n\t\t'm4u'     => 'video/vnd.mpegurl',\n\t\t'm4v'     => 'video/x-m4v',\n\t\t'mac'     => 'image/x-macpaint',\n\t\t'man'     => 'application/x-troff-man',\n\t\t'mathml'  => 'application/mathml+xml',\n\t\t'me'      => 'application/x-troff-me',\n\t\t'mesh'    => 'model/mesh',\n\t\t'mid'     => 'audio/midi',\n\t\t'midi'    => 'audio/midi',\n\t\t'mif'     => 'application/vnd.mif',\n\t\t'mov'     => 'video/quicktime',\n\t\t'movie'   => 'video/x-sgi-movie',\n\t\t'mp2'     => 'audio/mpeg',\n\t\t'mp3'     => 'audio/mpeg',\n\t\t'mp4'     => 'video/mp4',\n\t\t'mpe'     => 'video/mpeg',\n\t\t'mpeg'    => 'video/mpeg',\n\t\t'mpg'     => 'video/mpeg',\n\t\t'mpga'    => 'audio/mpeg',\n\t\t'ms'      => 'application/x-troff-ms',\n\t\t'msh'     => 'model/mesh',\n\t\t'mxu'     => 'video/vnd.mpegurl',\n\t\t'nc'      => 'application/x-netcdf',\n\t\t'oda'     => 'application/oda',\n\t\t'ogg'     => 'application/ogg',\n\t\t'ogv'     => 'video/ogv',\n\t\t'pbm'     => 'image/x-portable-bitmap',\n\t\t'pct'     => 'image/pict',\n\t\t'pdb'     => 'chemical/x-pdb',\n\t\t'pdf'     => 'application/pdf',\n\t\t'pgm'     => 'image/x-portable-graymap',\n\t\t'pgn'     => 'application/x-chess-pgn',\n\t\t'pic'     => 'image/pict',\n\t\t'pict'    => 'image/pict',\n\t\t'png'     => 'image/png',\n\t\t'pnm'     => 'image/x-portable-anymap',\n\t\t'pnt'     => 'image/x-macpaint',\n\t\t'pntg'    => 'image/x-macpaint',\n\t\t'ppm'     => 'image/x-portable-pixmap',\n\t\t'ppt'     => 'application/vnd.ms-powerpoint',\n\t\t'ps'      => 'application/postscript',\n\t\t'qt'      => 'video/quicktime',\n\t\t'qti'     => 'image/x-quicktime',\n\t\t'qtif'    => 'image/x-quicktime',\n\t\t'ra'      => 'audio/x-pn-realaudio',\n\t\t'ram'     => 'audio/x-pn-realaudio',\n\t\t'ras'     => 'image/x-cmu-raster',\n\t\t'rdf'     => 'application/rdf+xml',\n\t\t'rgb'     => 'image/x-rgb',\n\t\t'rm'      => 'application/vnd.rn-realmedia',\n\t\t'roff'    => 'application/x-troff',\n\t\t'rtf'     => 'text/rtf',\n\t\t'rtx'     => 'text/richtext',\n\t\t'sgm'     => 'text/sgml',\n\t\t'sgml'    => 'text/sgml',\n\t\t'sh'      => 'application/x-sh',\n\t\t'shar'    => 'application/x-shar',\n\t\t'silo'    => 'model/mesh',\n\t\t'sit'     => 'application/x-stuffit',\n\t\t'skd'     => 'application/x-koan',\n\t\t'skm'     => 'application/x-koan',\n\t\t'skp'     => 'application/x-koan',\n\t\t'skt'     => 'application/x-koan',\n\t\t'smi'     => 'application/smil',\n\t\t'smil'    => 'application/smil',\n\t\t'snd'     => 'audio/basic',\n\t\t'so'      => 'application/octet-stream',\n\t\t'spl'     => 'application/x-futuresplash',\n\t\t'src'     => 'application/x-wais-source',\n\t\t'sv4cpio' => 'application/x-sv4cpio',\n\t\t'sv4crc'  => 'application/x-sv4crc',\n\t\t'svg'     => 'image/svg+xml',\n\t\t'swf'     => 'application/x-shockwave-flash',\n\t\t't'       => 'application/x-troff',\n\t\t'tcl'     => 'application/x-tcl',\n\t\t'tex'     => 'application/x-tex',\n\t\t'texi'    => 'application/x-texinfo',\n\t\t'texinfo' => 'application/x-texinfo',\n\t\t'tif'     => 'image/tiff',\n\t\t'tiff'    => 'image/tiff',\n\t\t'tr'      => 'application/x-troff',\n\t\t'tsv'     => 'text/tab-separated-values',\n\t\t'txt'     => 'text/plain',\n\t\t'ustar'   => 'application/x-ustar',\n\t\t'vcd'     => 'application/x-cdlink',\n\t\t'vrml'    => 'model/vrml',\n\t\t'vxml'    => 'application/voicexml+xml',\n\t\t'wav'     => 'audio/x-wav',\n\t\t'wbmp'    => 'image/vnd.wap.wbmp',\n\t\t'wbxml'   => 'application/vnd.wap.wbxml',\n\t\t'webm'    => 'video/webm',\n\t\t'wml'     => 'text/vnd.wap.wml',\n\t\t'wmlc'    => 'application/vnd.wap.wmlc',\n\t\t'wmls'    => 'text/vnd.wap.wmlscript',\n\t\t'wmlsc'   => 'application/vnd.wap.wmlscriptc',\n\t\t'wmv'     => 'video/x-ms-wmv',\n\t\t'wrl'     => 'model/vrml',\n\t\t'xbm'     => 'image/x-xbitmap',\n\t\t'xht'     => 'application/xhtml+xml',\n\t\t'xhtml'   => 'application/xhtml+xml',\n\t\t'xls'     => 'application/vnd.ms-excel',\n\t\t'xml'     => 'application/xml',\n\t\t'xpm'     => 'image/x-xpixmap',\n\t\t'xsl'     => 'application/xml',\n\t\t'xslt'    => 'application/xslt+xml',\n\t\t'xul'     => 'application/vnd.mozilla.xul+xml',\n\t\t'xwd'     => 'image/x-xwindowdump',\n\t\t'xyz'     => 'chemical/x-xyz',\n\t);\n\n\t/**\n\t * Retrieve the mime type from file path.\n\t *\n\t * @param string $file The file path string.\n\t * @return string\n\t */\n\tpublic static function from_file_path( $file ) {\n\n\t\tif ( ! is_readable( $file ) || is_dir( $file ) || is_link( $file ) ) {\n\t\t\treturn self::DEFAULT_MIME_TYPE;\n\t\t}\n\n\t\t$mime_type   = '';\n\t\t$file_suffix = pathinfo( $file, PATHINFO_EXTENSION );\n\t\t$suffix      = strtolower( $file_suffix );\n\n\t\tif ( isset( self::$mime_type_list[ $suffix ] ) ) {\n\t\t\t$mime_type = self::$mime_type_list[ $suffix ];\n\t\t}\n\n\t\tif ( ! $mime_type && function_exists( 'finfo_file' ) ) {\n\t\t\t$finfo     = finfo_open( FILEINFO_MIME_TYPE );\n\t\t\t$mime_type = finfo_file( $finfo, $file );\n\t\t\tfinfo_close( $finfo );\n\t\t}\n\n\t\tif ( ! $mime_type && function_exists( 'mime_content_type' ) ) {\n\t\t\t$mime_type = mime_content_type( $file );\n\t\t}\n\n\t\treturn $mime_type ? $mime_type : self::DEFAULT_MIME_TYPE;\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-order-generator.php",
    "content": "<?php\n/**\n * LLMS_Order_Generator class file.\n *\n * @package LifterLMS/Classes\n *\n * @since 7.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Validate and create LLMS_Order posts.\n *\n * @since 7.0.0\n */\nclass LLMS_Order_Generator {\n\n\t/**\n\t * Error code: invalid coupon code submitted.\n\t *\n\t * @var string\n\t */\n\tconst E_COUPON_INVALID = 'llms-order-gen-coupon-invalid';\n\n\t/**\n\t * Error code: coupon code not found.\n\t *\n\t * @var string\n\t */\n\tconst E_COUPON_NOT_FOUND = 'llms-order-gen-coupon-not-found';\n\n\t/**\n\t * Error code: issue encountered during order post creation.\n\t *\n\t * @var string\n\t */\n\tconst E_CREATE_ORDER = 'llms-order-gen-create-order';\n\n\t/**\n\t * Error code: payment gateway id not submitted.\n\t *\n\t * @var string\n\t */\n\tconst E_GATEWAY_REQUIRED = 'llms-order-gen-gateway-required';\n\n\t/**\n\t * Error code: missing or invalid order key during confirmation.\n\t *\n\t * @var string\n\t */\n\tconst E_ORDER_NOT_FOUND = 'llms-order-gen-order-not-found';\n\n\t/**\n\t * Error code: order cannot be confirmed.\n\t *\n\t * @var string\n\t */\n\tconst E_ORDER_NOT_CONFIRMABLE = 'llms-order-gen-order-not-confirmable';\n\n\t/**\n\t * Error code: required plan ID not submitted.\n\t *\n\t * @var string\n\t */\n\tconst E_PLAN_REQUIRED = 'llms-order-gen-plan-required';\n\n\t/**\n\t * Error code: access plan not found.\n\t *\n\t * @var string\n\t */\n\tconst E_PLAN_NOT_FOUND = 'llms-order-gen-plan-not-found';\n\n\t/**\n\t * Error code: site's terms not accepted.\n\t *\n\t * @var string\n\t */\n\tconst E_SITE_TERMS = 'llms-order-gen-site-terms';\n\n\t/**\n\t * Error code: user already enrolled.\n\t *\n\t * @var string\n\t */\n\tconst E_USER_ENROLLED = 'llms-order-gen-user-enrolled';\n\n\t/**\n\t * User Action: validate and then commit (register or update) the user.\n\t *\n\t * @var string\n\t */\n\tconst UA_COMMIT = 'commit';\n\n\t/**\n\t * User Action: perform user validation only.\n\t *\n\t * @var string\n\t */\n\tconst UA_VALIDATE = 'validate';\n\n\t/**\n\t * The coupon used to discount the order.\n\t *\n\t * Derived from `$this->data['llms_coupon_code']`.\n\t *\n\t * Will be empty until the coupon is validated.\n\t *\n\t * @var LLMS_Coupon|null\n\t */\n\tprotected $coupon = null;\n\n\t/**\n\t * Associative array of input data.\n\t *\n\t * Usually the $_POST superglobal.\n\t *\n\t * @var array\n\t */\n\tprotected $data = array();\n\n\t/**\n\t * The payment gateway used to process the order.\n\t *\n\t * Derived from `$this->data['llms_payment_gateway']` .\n\t *\n\t * Will be empty until the gateway is validated.\n\t *\n\t * @var LLMS_Payment_Gateway|null\n\t */\n\tprotected $gateway = null;\n\n\t/**\n\t * The access plan used to generate the order.\n\t *\n\t * Derived from `$this->data['llms_plan_id']`.\n\t *\n\t * Will be empty until the plan is validated.\n\t *\n\t * @var LLMS_Access_Plan|null\n\t */\n\tprotected $plan = null;\n\n\t/**\n\t * The order.\n\t *\n\t * Derived from `$this->data['llms_order_key']`.\n\t *\n\t * Will be empty until the order is validated.\n\t *\n\t * This is only used during confirmation of existing orders.\n\t *\n\t * @var LLMS_Order|null\n\t */\n\tprotected $order = null;\n\n\t/**\n\t * The student used to generate the order.\n\t *\n\t * Will be empty until the user is created / update following all validations.\n\t *\n\t * @var LLMS_Student|null\n\t */\n\tprotected $student = null;\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array $data {\n\t *     An associative array of input data used to generate the order, usually from $_POST.\n\t *\n\t *     @type integer $llms_plan_id         An LLMS_Access_Plan ID.\n\t *     @type string  $llms_agree_to_terms  A yes/no value determining whether or not the user has agreed to the site's terms.\n\t *     @type string  $llms_payment_gateway The ID of the payment gateway used to process the order.\n\t *     @type string  $llms_coupon_code     Optional. The coupon code string being used.\n\t *     @type string  $llms_order_key       Optional. An `LLMS_Order` key used to modify an existing pending order rather than creating a new one.\n\t *     @type array   ...$user_data         All remaining data is passed to the user creation functions.\n\t * }\n\t * @return void\n\t */\n\tpublic function __construct( $data ) {\n\t\t$this->data = $data;\n\t}\n\n\t/**\n\t * Confirms an existing pending order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return WP_Error|array Returns an array of data from the payment gateway's `confirm_pending_order()` method on success.\n\t */\n\tpublic function confirm() {\n\n\t\t$validate = $this->validate( true );\n\t\tif ( is_wp_error( $validate ) ) {\n\t\t\treturn $validate;\n\t\t}\n\n\t\t$gateway_confirm = $this->gateway->confirm_pending_order( $this->order );\n\t\tif ( is_wp_error( $gateway_confirm ) ) {\n\t\t\treturn $gateway_confirm;\n\t\t}\n\n\t\t$user = $this->commit_user();\n\t\tif ( is_wp_error( $user ) ) {\n\t\t\treturn $user;\n\t\t}\n\n\t\t// Save the user to the order.\n\t\t$this->order->set_user_data( $this->get_user_data() );\n\n\t\tif ( 'SUCCESS' === ( $gateway_confirm['status'] ?? null ) && ! empty( $gateway_confirm['transaction'] ) ) {\n\t\t\t// Record the transaction.\n\t\t\t$this->order->record_transaction( $gateway_confirm['transaction'] );\n\t\t}\n\n\t\treturn $gateway_confirm;\n\n\t}\n\n\n\t/**\n\t * Creates a new pending order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return WP_Error|LLMS_Order\n\t */\n\tprotected function create() {\n\n\t\t$order = new LLMS_Order( $this->get_order_id() );\n\n\t\t// If there's no id we can't proceed, return an error.\n\t\tif ( ! $order->get( 'id' ) ) {\n\t\t\treturn $this->error(\n\t\t\t\tself::E_CREATE_ORDER,\n\t\t\t\t__( 'There was an error creating your order, please try again.', 'lifterlms' )\n\t\t\t);\n\t\t}\n\n\t\t$order->init( $this->get_user_data(), $this->plan, $this->gateway, $this->coupon );\n\n\t\treturn $order;\n\n\t}\n\n\t/**\n\t * Registers or updates the user from the submitted data.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return integer|WP_Error Returns the `WP_User` ID on success or an error object.\n\t */\n\tprotected function commit_user() {\n\n\t\t$args = array(\n\t\t\t'plan' => $this->plan,\n\t\t);\n\n\t\t$user_id = get_current_user_id() ?\n\t\t\tllms_update_user( $this->data, 'checkout', $args ) :\n\t\t\tllms_register_user( $this->data, 'checkout', true, $args );\n\n\t\tif ( ! is_wp_error( $user_id ) ) {\n\t\t\t$this->student = llms_get_student( $user_id );\n\t\t}\n\n\t\treturn $user_id;\n\n\t}\n\n\t/**\n\t * Returns an error object.\n\t *\n\t * This method accepts an error code and message and passes them directly to `WP_Error` and\n\t * adds all class variables to the error objects `$data` parameter.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $code       Error code.\n\t * @param string $message    Error message.\n\t * @param array  $extra_data Additional data to pass to WP_Error's 3rd parameter.\n\t * @return WP_Error\n\t */\n\tprotected function error( $code, $message, $extra_data = array() ) {\n\n\t\t$data = get_class_vars( __CLASS__ );\n\t\tforeach ( $data as $key => &$val ) {\n\t\t\t$val = $this->{$key};\n\t\t}\n\n\t\treturn new WP_Error( $code, $message, array_merge( $data, $extra_data ) );\n\n\t}\n\n\t/**\n\t * Attempts to locate a user ID.\n\t *\n\t * Uses the logged in user's information and falls back to a lookup by email address if available.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string|null $email An email address, if available.\n\t * @return null|integer Returns the WP_User ID or null if not found.\n\t */\n\tprivate function find_user_id( $email = null ) {\n\n\t\tif ( is_user_logged_in() ) {\n\t\t\treturn get_current_user_id();\n\t\t}\n\n\t\tif ( $email ) {\n\t\t\t$user = get_user_by( 'email', $email );\n\t\t\treturn $user ? $user->ID : null;\n\t\t}\n\n\t\treturn null;\n\n\t}\n\n\t/**\n\t * Generates an order.\n\t *\n\t * Uses data submitted during class construction and performs all necessary\n\t * validations. If validations pass, creates the order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $user_action The user action, accepts `LLMS_Order_Generator::UA_COMMIT` or `LLMS_Order_Generator::UA_VALIDATE`.\n\t * @return WP_Error|LLMS_Order\n\t */\n\tpublic function generate( $user_action = self::UA_COMMIT ) {\n\n\t\t$validate = $this->validate();\n\t\tif ( is_wp_error( $validate ) ) {\n\t\t\treturn $validate;\n\t\t}\n\n\t\tif ( self::UA_COMMIT === $user_action ) {\n\t\t\t$user = $this->commit_user();\n\t\t\tif ( is_wp_error( $user ) ) {\n\t\t\t\treturn $user;\n\t\t\t}\n\t\t}\n\n\t\treturn $this->create();\n\n\t}\n\n\t/**\n\t * Retrieves the coupon object for the order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return LLMS_Coupon|null\n\t */\n\tpublic function get_coupon() {\n\t\treturn $this->coupon;\n\t}\n\n\t/**\n\t * Retrieves the payment gateway instance for the order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return LLMS_Payment_Gateway|null\n\t */\n\tpublic function get_gateway() {\n\t\treturn $this->gateway;\n\t}\n\n\t/**\n\t * Retrieves the order id to use for the order.\n\t *\n\t * Attempts to locate an existing pending order by order key if it was submitted,\n\t * otherwise returns `new` which denotes a new order should be created.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return integer|string\n\t */\n\tprotected function get_order_id() {\n\n\t\t$order_id = null;\n\t\t$key      = $this->data['llms_order_key'] ?? null;\n\t\t$email    = $this->data['email_address'] ?? null;\n\t\t$plan_id  = $this->data['llms_plan_id'] ?? null;\n\n\t\t// Try to lookup using the order key if it was supplied.\n\t\tif ( $key ) {\n\t\t\t$order_id = $this->sanitize_retrieved_order_id( llms_get_order_by_key( $key, 'id' ) );\n\t\t}\n\n\t\t// Try to lookup by user ID.\n\t\tif ( ! $order_id ) {\n\n\t\t\t$user_id  = $this->find_user_id( $email );\n\t\t\t$order_id = $user_id ? $this->sanitize_retrieved_order_id( llms_locate_order_for_user_and_plan( $user_id, $plan_id ) ) : null;\n\n\t\t}\n\n\t\t// Lookup by email address.\n\t\tif ( ! $order_id && $email ) {\n\t\t\t$order_id = $this->sanitize_retrieved_order_id( llms_locate_order_for_email_and_plan( $email, $plan_id ) );\n\t\t}\n\n\t\treturn $order_id ? $order_id : 'new';\n\n\t}\n\n\t/**\n\t * Retrieves the access plan for the order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return LLMS_Access_Plan|null\n\t */\n\tpublic function get_plan() {\n\t\treturn $this->plan;\n\t}\n\n\t/**\n\t * Retrieves the order object.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return LLMS_Order|null\n\t */\n\tpublic function get_order() {\n\t\treturn $this->order;\n\t}\n\n\t/**\n\t * Retrieves the student for the order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return LLMS_Student|null\n\t */\n\tpublic function get_student() {\n\t\treturn $this->student;\n\t}\n\n\t/**\n\t * Retrieves an array of data representing the student.\n\t *\n\t * The resulting array is intended to be used for setting up the `LLMS_Order` post's\n\t * user metadata, ideally passed to `LLMS_Order::init()`.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_user_data() {\n\n\t\t$map = array(\n\t\t\t'billing_email'      => 'email_address',\n\t\t\t'billing_first_name' => 'first_name',\n\t\t\t'billing_last_name'  => 'last_name',\n\t\t\t'billing_phone'      => 'llms_phone',\n\t\t);\n\n\t\t$data = array(\n\t\t\t'billing_email'      => '',\n\t\t\t'billing_first_name' => '',\n\t\t\t'billing_last_name'  => '',\n\t\t\t'billing_address_1'  => '',\n\t\t\t'billing_address_2'  => '',\n\t\t\t'billing_city'       => '',\n\t\t\t'billing_state'      => '',\n\t\t\t'billing_zip'        => '',\n\t\t\t'billing_country'    => '',\n\t\t\t'billing_phone'      => '',\n\t\t);\n\n\t\tforeach ( $data as $key => &$val ) {\n\t\t\t$data_key = $map[ $key ] ?? \"llms_{$key}\";\n\t\t\t$val      = $this->data[ $data_key ] ?? '';\n\t\t}\n\n\t\t$data['user_id'] = $this->student ? $this->student->get( 'id' ) : '';\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Sanitizes the order_id retrieved by {@see LLMS_Order_Generator::get_order_id()} to ensure it can be resumed or confirmed during checkout.\n\t *\n\t * Only orders with the `llms-pending` status can be resumed or confirmed.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param null|int $order_id The order ID or `null` if the lookup didn't yield a result.\n\t * @return int|null Returns the submitted order ID if it's valid or `null`.\n\t */\n\tprivate function sanitize_retrieved_order_id( $order_id ) {\n\t\treturn $order_id && 'llms-pending' === get_post_status( $order_id ) ? $order_id : null;\n\t}\n\n\t/**\n\t * Performs all required data validations necessary to create the order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param boolean $validate_order Whether or not order data should be validated. This is `true` when running `confirm()` and `false` otherwise.\n\t * @return boolean|WP_Error Returns `true` if all validations pass or an error object.\n\t */\n\tprotected function validate( $validate_order = false ) {\n\n\t\t/**\n\t\t * Allows 3rd party validation prior to generation of an order.\n\t\t *\n\t\t * This validation hook runs prior to all default validation.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param null|WP_Error $validation_error Halts checkout and returns the supplied error.\n\t\t */\n\t\t$before_validation = apply_filters( 'llms_before_generate_order_validation', null );\n\t\tif ( is_wp_error( $before_validation ) ) {\n\t\t\treturn $before_validation;\n\t\t}\n\n\t\t$validations = array(\n\t\t\t'validate_plan',\n\t\t\t'validate_coupon',\n\t\t\t'validate_gateway',\n\t\t\t'validate_terms',\n\t\t\t'validate_user',\n\t\t);\n\n\t\tif ( $validate_order ) {\n\t\t\tarray_unshift( $validations, 'validate_order' );\n\t\t}\n\n\t\tforeach ( $validations as $func ) {\n\t\t\t$res = $this->{$func}();\n\t\t\tif ( is_wp_error( $res ) ) {\n\t\t\t\treturn $res;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Allows 3rd party validation prior to generation of an order.\n\t\t *\n\t\t * This validation hook runs after all default validation.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param boolean|WP_Error $validation_error Halts checkout and returns the supplied error.\n\t\t */\n\t\treturn apply_filters( 'llms_after_generate_order_validation', true );\n\n\t}\n\n\t/**\n\t * Validates the coupon.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean|WP_Error Returns `true` on success or an error object.\n\t */\n\tprotected function validate_coupon() {\n\n\t\t// If a coupon is being used, validate it.\n\t\tif ( ! empty( $this->data['llms_coupon_code'] ) ) {\n\n\t\t\t$code = sanitize_text_field( $this->data['llms_coupon_code'] );\n\n\t\t\t// Locate the coupon post ID.\n\t\t\t$coupon_id = llms_find_coupon( $code );\n\t\t\tif ( ! $coupon_id ) {\n\t\t\t\treturn $this->error(\n\t\t\t\t\tself::E_COUPON_NOT_FOUND,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t// Translators: %s = The user-submitted coupon code.\n\t\t\t\t\t\t__( 'Coupon code \"%s\" not found.', 'lifterlms' ),\n\t\t\t\t\t\t$code\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validate the coupon for the current plan.\n\t\t\t$coupon = llms_get_post( $coupon_id );\n\t\t\t$valid  = $coupon->is_valid( $this->plan->get( 'id' ) );\n\t\t\tif ( is_wp_error( $valid ) ) {\n\t\t\t\treturn $this->error( self::E_COUPON_INVALID, $valid->get_error_message() );\n\t\t\t}\n\n\t\t\t$this->coupon = $coupon;\n\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Validates the payment gateway.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean|WP_Error Returns `true` on success or an error object.\n\t */\n\tprotected function validate_gateway() {\n\n\t\t$coupon_id = $this->coupon ? $this->coupon->get( 'id' ) : null;\n\n\t\t/**\n\t\t * If payment is required, verify we have a gateway.\n\t\t *\n\t\t * For free plans the manual gateway is automatically used, whether or not it's enabled.\n\t\t */\n\t\tif ( $this->plan->requires_payment( $coupon_id ) && empty( $this->data['llms_payment_gateway'] ) ) {\n\t\t\treturn $this->error( self::E_GATEWAY_REQUIRED, __( 'No payment method selected.', 'lifterlms' ) );\n\t\t}\n\n\t\t$gateway_id = $this->data['llms_payment_gateway'] ?? 'manual';\n\t\t$is_valid   = llms_can_gateway_be_used_for_plan( $gateway_id, $this->plan );\n\t\tif ( is_wp_error( $is_valid ) ) {\n\t\t\treturn $is_valid;\n\t\t}\n\n\t\t$this->gateway = llms()->payment_gateways()->get_gateway_by_id( $gateway_id );\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validates the order.\n\t *\n\t * Ensures the submitted order key is valid and that the order can be confirmed.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean|WP_Error Returns `true` on success or an error object.\n\t */\n\tprotected function validate_order() {\n\n\t\t$order_id = $this->get_order_id();\n\n\t\tif ( 'new' === $order_id || 'llms_order' !== get_post_type( $order_id ) ) {\n\t\t\treturn $this->error(\n\t\t\t\tself::E_ORDER_NOT_FOUND,\n\t\t\t\t__( 'Could not locate an order to confirm.', 'lifterlms' )\n\t\t\t);\n\t\t}\n\n\t\t$order = llms_get_post( $order_id );\n\t\tif ( ! $order->can_be_confirmed() ) {\n\t\t\treturn $this->error(\n\t\t\t\tself::E_ORDER_NOT_CONFIRMABLE,\n\t\t\t\t__( 'Could not locate an order to confirm.', 'lifterlms' )\n\t\t\t);\n\t\t}\n\n\t\t$this->order = $order;\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validates the access plan.\n\t *\n\t * Ensures the access plan data was submitted and that it's a valid plan.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean|WP_Error Returns `true` on success or an error object.\n\t */\n\tprotected function validate_plan() {\n\n\t\t$plan_id = $this->data['llms_plan_id'] ?? null;\n\t\tif ( ! $plan_id ) {\n\t\t\treturn $this->error( self::E_PLAN_REQUIRED, __( 'Missing access plan ID.', 'lifterlms' ) );\n\t\t}\n\n\t\t$plan = llms_get_post( $plan_id );\n\t\tif ( ! $plan || 'llms_access_plan' !== $plan->get( 'type' ) ) {\n\t\t\treturn $this->error( self::E_PLAN_NOT_FOUND, __( 'Access plan not found.', 'lifterlms' ) );\n\t\t}\n\n\t\t$this->plan = $plan;\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validates the site's terms and conditions were submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean|WP_Error Returns `true` on success or an error object.\n\t */\n\tprotected function validate_terms() {\n\n\t\tif ( llms_are_terms_and_conditions_required() && ! llms_parse_bool( $this->data['llms_agree_to_terms'] ?? 'no' ) ) {\n\t\t\treturn $this->error(\n\t\t\t\tself::E_SITE_TERMS,\n\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %s = The title of the site's LifterLMS Terms and Conditions page.\n\t\t\t\t\t__( 'You must agree to the %s.', 'lifterlms' ),\n\t\t\t\t\tget_the_title( get_option( 'lifterlms_terms_page_id' ) )\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validates the submitted user data.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean|WP_Error Returns `true` on success or an error object.\n\t */\n\tprotected function validate_user() {\n\n\t\t$validate = llms_validate_user( $this->data );\n\t\tif ( is_wp_error( $validate ) ) {\n\t\t\treturn $validate;\n\t\t}\n\n\t\t// If validation passes, determine if the user already exists and, if they do, validate their enrollment.\n\t\t$email = $this->data['email_address'] ?? null;\n\t\t$user  = $email ? get_user_by( 'email', $email ) : false;\n\t\tif ( $user && llms_is_user_enrolled( $user->ID, $this->plan->get( 'product_id' ) ) ) {\n\t\t\treturn $this->error(\n\t\t\t\tself::E_USER_ENROLLED,\n\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %s = The title of the course or membership.\n\t\t\t\t\t__( 'You already have access to %s.', 'lifterlms' ),\n\t\t\t\t\tget_the_title( $this->plan->get( 'product_id' ) )\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "includes/class-llms-prevent-concurrent-logins.php",
    "content": "<?php\n/**\n * LLMS_Prevent_Concurrent_Logins class file\n *\n * @package LifterLMS/Classes\n *\n * @since 5.6.0\n * @version 5.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Prevent_Concurrent_Logins class.\n *\n * @since 5.6.0\n */\nclass LLMS_Prevent_Concurrent_Logins {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Array of sessions for the current user.\n\t *\n\t * @var array\n\t */\n\tprivate $user_sessions;\n\n\t/**\n\t * Current user ID.\n\t *\n\t * @var int\n\t */\n\tprivate $user_id;\n\n\t/**\n\t * Private Constructor.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\tif ( llms_parse_bool( get_option( 'lifterlms_prevent_concurrent_logins', 'no' ) ) &&\n\t\t\t\t! empty( get_option( 'lifterlms_prevent_concurrent_logins_roles', array( 'student' ) ) ) ) {\n\n\t\t\tadd_action( 'init', array( $this, 'init' ) );\n\t\t\tadd_action( 'init', array( $this, 'maybe_prevent_concurrent_logins' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Initialize.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\n\t\t$this->user_id = get_current_user_id();\n\n\t\tif ( empty( $this->user_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->user_sessions = wp_get_all_sessions();\n\n\t}\n\n\t/**\n\t * Maybe prevent current logins.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return bool `true` if concurrent login prevented, `false` otherwise.\n\t */\n\tpublic function maybe_prevent_concurrent_logins() {\n\n\t\t// No logged in user or current user has only one active session: nothing to do.\n\t\tif ( empty( $this->user_sessions ) || count( $this->user_sessions ) < 2 ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not allowing a specific user to have concurrent sessions.\n\t\t *\n\t\t * @since 5.6.0\n\t\t *\n\t\t * @param bool $allow   Whether or not the user should be allowed to have concurrent sessions.\n\t\t * @param int  $user_id WP_User ID of the current use.\n\t\t */\n\t\tif ( (bool) apply_filters( 'llms_allow_user_concurrent_logins', false, $this->user_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Current user doesn't have any restricted role: nothing to do.\n\t\tif ( empty( array_intersect( get_userdata( $this->user_id )->roles, (array) get_option( 'lifterlms_prevent_concurrent_logins_roles', array( 'student' ) ) ) ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$this->destroy_all_sessions_but_newest();\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Prevent login by destroying all the user's sessions but the newest.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return int 1 if the kept session is the current one, 0 otherwise.\n\t */\n\tprivate function destroy_all_sessions_but_newest() {\n\n\t\t$is_current_session_newest_session = $this->current_user_newest_session_login_time() === $this->current_user_current_session_login_time();\n\n\t\t$is_current_session_newest_session\n\t\t\t?\n\t\t\twp_destroy_other_sessions()\n\t\t\t:\n\t\t\twp_destroy_current_session();\n\n\t\treturn (int) $is_current_session_newest_session;\n\n\t}\n\n\t/**\n\t * Retrieve current session for the current user.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return int\n\t */\n\tprivate function current_user_current_session_login_time() {\n\n\t\t$sessions = WP_Session_Tokens::get_instance( $this->user_id );\n\t\treturn $sessions->get( wp_get_session_token() )['login'];\n\n\t}\n\n\t/**\n\t * Retrieve newest session login time for the current user.\n\t *\n\t * The bigger the login time is the newest the session is.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return int\n\t */\n\tprivate function current_user_newest_session_login_time() {\n\n\t\treturn max( array_column( $this->user_sessions, 'login' ) );\n\n\t}\n\n}\n\nreturn LLMS_Prevent_Concurrent_Logins::instance();\n"
  },
  {
    "path": "includes/class-llms-rest-fields.php",
    "content": "<?php\n/**\n * LLMS_Rest_Fields class\n *\n * @package LifterLMS/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Registers REST fields used by LifterLMS objects.\n *\n * @since 6.0.0\n */\nclass LLMS_REST_Fields {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'rest_api_init', array( $this, 'register' ) );\n\t\tadd_filter( 'rest_prepare_llms_my_certificate', array( $this, 'remove_author_assign_link' ) );\n\t}\n\n\t/**\n\t * Retrieves an array of data used to register fields for certificates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_fields_for_certificates() {\n\n\t\treturn array(\n\t\t\t'size'        => array(\n\t\t\t\t'description' => __( 'Certificate size.', 'lifterlms' ),\n\t\t\t\t'type'        => 'string',\n\t\t\t\t'enum'        => array_merge(\n\t\t\t\t\tarray_keys( llms_get_certificate_sizes() ),\n\t\t\t\t\tarray( 'CUSTOM' )\n\t\t\t\t),\n\t\t\t),\n\t\t\t'width'       => array(\n\t\t\t\t'description' => __( 'Certificate width.', 'lifterlms' ),\n\t\t\t\t'type'        => 'number',\n\t\t\t),\n\t\t\t'height'      => array(\n\t\t\t\t'description' => __( 'Certificate height.', 'lifterlms' ),\n\t\t\t\t'type'        => 'number',\n\t\t\t),\n\t\t\t'unit'        => array(\n\t\t\t\t'description' => __( 'Certificate sizing unit applied to the width and height properties.', 'lifterlms' ),\n\t\t\t\t'type'        => 'string',\n\t\t\t\t'enum'        => array_keys( llms_get_certificate_units() ),\n\t\t\t),\n\t\t\t'orientation' => array(\n\t\t\t\t'description' => __( 'Certificate orientation.', 'lifterlms' ),\n\t\t\t\t'type'        => 'string',\n\t\t\t\t'enum'        => array_keys( llms_get_certificate_orientations() ),\n\t\t\t),\n\t\t\t'margins'     => array(\n\t\t\t\t'description' => __( 'Certificate margins.', 'lifterlms' ),\n\t\t\t\t'type'        => 'array',\n\t\t\t\t'minItems'    => 4,\n\t\t\t\t'maxItems'    => 4,\n\t\t\t\t'items'       => array(\n\t\t\t\t\t'type' => 'number',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'background'  => array(\n\t\t\t\t'description' => __( 'Certificate background color.', 'lifterlms' ),\n\t\t\t\t'type'        => 'string',\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Register the REST fields.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function register() {\n\n\t\tif ( llms_is_block_editor_supported_for_certificates() ) {\n\n\t\t\t$this->register_fields_for_certificates();\n\t\t\t$this->register_fields_for_certificate_awards();\n\t\t\t$this->register_fields_for_certificate_templates();\n\t\t\t$this->register_fields_for_attachments();\n\n\t\t}\n\t}\n\n\t/**\n\t * Register fields for attachments.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function register_fields_for_attachments() {\n\n\t\tregister_rest_field(\n\t\t\t'attachment',\n\t\t\t'_llms_media_protection_product_id',\n\t\t\tarray(\n\t\t\t\t'get_callback'    => function ( $object ) {\n\t\t\t\t\treturn get_post_meta( $object['id'], '_llms_media_protection_product_id', true );\n\t\t\t\t},\n\t\t\t\t'update_callback' => function ( $value, $object ) {\n\t\t\t\t\t$settings = new LLMS_Admin_Media_Protection_Attachment_Settings();\n\t\t\t\t\t$protector = new LLMS_Media_Protector();\n\n\t\t\t\t\tif ( $protector->is_media_protected( $object->ID ) ) {\n\t\t\t\t\t\tupdate_post_meta( $object->ID, '_llms_media_protection_product_id', absint( $value ) );\n\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( $settings->move_attachment_to_protected_dir( $object->ID ) ) {\n\t\t\t\t\t\tupdate_post_meta( $object->ID, '_llms_media_protection_product_id', absint( $value ) );\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t'schema'          => array(\n\t\t\t\t\t'description' => __( 'The ID of the product that protects this media.', 'lifterlms' ),\n\t\t\t\t\t'type'        => 'integer',\n\t\t\t\t\t'context'     => array( 'view', 'edit' ),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Register rest fields used for awarded certificates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function register_fields_for_certificate_awards() {\n\n\t\tregister_rest_field(\n\t\t\t'llms_my_certificate',\n\t\t\t'certificate_template',\n\t\t\tarray(\n\t\t\t\t'schema'          => array(\n\t\t\t\t\t'description' => __( 'Certificate template ID.', 'lifterlms' ),\n\t\t\t\t\t'type'        => 'integer',\n\t\t\t\t\t'arg_options' => array(\n\t\t\t\t\t\t'validate_callback' => function ( $value ) {\n\t\t\t\t\t\t\treturn ! $value || 'llms_certificate' === get_post_type( $value );\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'get_callback'    => function ( $object ) {\n\t\t\t\t\treturn wp_get_post_parent_id( $object['id'] );\n\t\t\t\t},\n\t\t\t\t'update_callback' => function ( $value, $post ) {\n\t\t\t\t\t$update = array(\n\t\t\t\t\t\t'ID'          => $post->ID,\n\t\t\t\t\t\t'post_parent' => $value,\n\t\t\t\t\t);\n\t\t\t\t\treturn wp_update_post( $update );\n\t\t\t\t},\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Register rest fields used for certificate templates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function register_fields_for_certificate_templates() {\n\n\t\tregister_rest_field(\n\t\t\t'llms_certificate',\n\t\t\t'certificate_title',\n\t\t\tarray(\n\t\t\t\t'schema'          => array(\n\t\t\t\t\t'description' => __( 'Certificate title.', 'lifterlms' ),\n\t\t\t\t\t'type'        => 'string',\n\t\t\t\t),\n\t\t\t\t'get_callback'    => function ( $object ) {\n\t\t\t\t\t$cert = llms_get_certificate( $object['id'], true );\n\t\t\t\t\treturn $cert ? $cert->get( 'certificate_title' ) : null;\n\t\t\t\t},\n\t\t\t\t'update_callback' => function ( $value, $post ) {\n\t\t\t\t\t$cert = llms_get_certificate( $post->ID, true );\n\t\t\t\t\treturn $cert ? $cert->set( 'certificate_title', $value ) : null;\n\t\t\t\t},\n\t\t\t)\n\t\t);\n\n\t\tregister_rest_field(\n\t\t\t'llms_certificate',\n\t\t\t'certificate_sequential_id',\n\t\t\tarray(\n\t\t\t\t'schema'          => array(\n\t\t\t\t\t'description' => __( 'Next sequential ID.', 'lifterlms' ),\n\t\t\t\t\t'type'        => 'integer',\n\t\t\t\t\t'arg_options' => array(\n\t\t\t\t\t\t'validate_callback' => function ( $value, $request ) {\n\t\t\t\t\t\t\treturn (int) $value >= llms_get_certificate_sequential_id( $request['id'] );\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'get_callback'    => function ( $object ) {\n\t\t\t\t\treturn llms_get_certificate_sequential_id( $object['id'] );\n\t\t\t\t},\n\t\t\t\t'update_callback' => function ( $value, $post ) {\n\t\t\t\t\t$cert = llms_get_certificate( $post->ID, true );\n\t\t\t\t\treturn $cert ? $cert->set( 'sequential_id', $value ) : null;\n\t\t\t\t},\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Register fields for template and earned certificates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function register_fields_for_certificates() {\n\n\t\tforeach ( $this->get_fields_for_certificates() as $key => $schema ) {\n\n\t\t\t$schema['context'] = array( 'view', 'edit' );\n\n\t\t\tregister_rest_field(\n\t\t\t\tarray( 'llms_certificate', 'llms_my_certificate' ),\n\t\t\t\t\"certificate_{$key}\",\n\t\t\t\tarray(\n\t\t\t\t\t'schema'          => $schema,\n\t\t\t\t\t'get_callback'    => function ( $object ) use ( $key ) {\n\t\t\t\t\t\t$cert = llms_get_certificate( $object['id'], true );\n\t\t\t\t\t\t$func = \"get_{$key}\";\n\t\t\t\t\t\treturn $cert ? $cert->$func() : null;\n\t\t\t\t\t},\n\t\t\t\t\t'update_callback' => function ( $value, $post ) use ( $key ) {\n\t\t\t\t\t\t$cert = llms_get_certificate( $post->ID, true );\n\t\t\t\t\t\treturn $cert ? $cert->set( $key, $value ) : null;\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\t}\n\n\t/**\n\t * Remove the author assign action link for llms_my_certificate REST responses.\n\t *\n\t * This is a hack put in place to prevent the default <PostAuthor> control component\n\t * cannot be disabled in any other way I can find, the check in place on it determines\n\t * if the control can be displayed based on the presence of this link in the REST response.\n\t *\n\t * Removing this probably isn't generally idea but I cannot conceive of any other way to handle this.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @link https://github.com/WordPress/gutenberg/tree/trunk/packages/editor/src/components/post-author\n\t *\n\t * @param WP_REST_Response $res Rest response.\n\t * @return WP_REST_Response\n\t */\n\tpublic function remove_author_assign_link( $res ) {\n\n\t\t$res->remove_link( 'https://api.w.org/action-assign-author' );\n\n\t\treturn $res;\n\t}\n}\n\nreturn new LLMS_REST_Fields();\n"
  },
  {
    "path": "includes/class-llms-sessions.php",
    "content": "<?php\n/**\n * User event session management\n *\n * @package LifterLMS/Classes\n *\n * @since 3.36.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Sessions class.\n *\n * @since 3.36.0\n * @since 3.37.2 Add filter `llms_sessions_end_idle_cron_recurrence` to allow customization of the recurrence of the idle session cleanup cronjob.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Removed the deprecated `LLMS_Sessions::$_instance` property.\n */\nclass LLMS_Sessions {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Current user id.\n\t *\n\t * @var null\n\t */\n\tprotected $user_id = null;\n\n\t/**\n\t * Private Constructor.\n\t *\n\t * @since 3.36.0\n\t * @since 3.37.2 Add filter to the cleanup cronjob interval.\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\tadd_filter( 'cron_schedules', array( $this, 'add_cron_schedule' ) );\n\n\t\tif ( ! wp_next_scheduled( 'llms_end_idle_sessions' ) ) {\n\t\t\t/**\n\t\t\t * Filter the recurrence interval at which LifterLMS closes idle sessions.\n\t\t\t *\n\t\t\t * @link https://developer.wordpress.org/reference/functions/wp_get_schedules/\n\t\t\t *\n\t\t\t * @since 3.37.2\n\t\t\t *\n\t\t\t * @param string $recurrence Cron job recurrence interval. Must be valid interval as retrieved from `wp_get_schedules()`. Default is \"every_five_mins\".\n\t\t\t */\n\t\t\t$recurrence = apply_filters( 'llms_sessions_end_idle_cron_recurrence', 'every_five_mins' );\n\t\t\twp_schedule_event( time(), $recurrence, 'llms_end_idle_sessions' );\n\t\t}\n\t\tadd_action( 'llms_end_idle_sessions', array( $this, 'end_idle_sessions' ) );\n\n\t}\n\n\t/**\n\t * Add cron schedule for session end interval checks.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param array $schedules Array of cron schedules.\n\t * @return array\n\t */\n\tpublic function add_cron_schedule( $schedules ) {\n\n\t\t// Adds every 5 minutes to the existing schedules.\n\t\t$schedules['every_five_mins'] = array(\n\t\t\t'interval' => MINUTE_IN_SECONDS * 5,\n\t\t\t'display'  => sprintf( __( 'Every %d Minutes', 'lifterlms' ), 5 ),\n\t\t);\n\t\treturn $schedules;\n\n\t}\n\n\t/**\n\t * End the 50 oldest idle sessions.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function end_idle_sessions() {\n\n\t\tforeach ( $this->get_open_sessions() as $i => $event ) {\n\t\t\tif ( $this->is_session_idle( $event ) ) {\n\t\t\t\t$this->end( $event );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * End a session.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Delete open session entry from the `wp_lifterlms_events_open_sessions` table.\n\t *\n\t * @param LLMS_Event $start Event object for a session start.\n\t * @return LLMS_Event|WP_Error\n\t */\n\tprotected function end( $start ) {\n\n\t\t$end = llms()->events()->record(\n\t\t\tarray(\n\t\t\t\t'actor_id'     => $start->get( 'actor_id' ),\n\t\t\t\t'object_type'  => 'session',\n\t\t\t\t'object_id'    => $start->get( 'object_id' ),\n\t\t\t\t'event_type'   => 'session',\n\t\t\t\t'event_action' => 'end',\n\t\t\t)\n\t\t);\n\n\t\tif ( ! is_wp_error( $end ) ) {\n\t\t\tglobal $wpdb;\n\t\t\t$wpdb->query(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"\n\t\t\t\t\tDELETE FROM {$wpdb->prefix}lifterlms_events_open_sessions\n\t\t\t\t\tWHERE `event_id` = %d\n\t\t\t\t\t\",\n\t\t\t\t\t$start->get( 'id' )\n\t\t\t\t)\n\t\t\t); // db call ok; no-cache ok.\n\t\t}\n\n\t\treturn $end;\n\t}\n\n\t/**\n\t * Ends the currently active session for the logged in user.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return LLMS_Event|WP_Error|false\n\t */\n\tpublic function end_current() {\n\n\t\t$current = $this->get_current();\n\t\tif ( ! $current ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $this->end( $current );\n\n\t}\n\n\t/**\n\t * Retrieve the current session start event record for a given user.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Added optional `$user_id` parameter.\n\t *\n\t * @param int $user_id Optional. WP_User ID of a student. Default `null`\n\t *                     If not provided, or a falsy is provided, will fall back on the current user id.\n\t * @return LLMS_Event|false\n\t */\n\tpublic function get_current( $user_id = null ) {\n\n\t\t$user_id = $user_id ? $user_id : get_current_user_id();\n\t\tif ( ! $user_id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$session = $this->get_last_session( $user_id );\n\t\tif ( ! $session ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$session = new LLMS_Event( $session->id );\n\n\t\tif ( ! $this->is_session_open( $session ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $session;\n\n\t}\n\n\t/**\n\t * Determine if a session is idle.\n\t *\n\t * A session is considered idle if it's open and no new events have been recorded\n\t * in the last 30 minutes.\n\t *\n\t * @since 3.36.0\n\t * @since 4.7.0 When retrieving the last event, instantiate the events query passing `no_found_rows` arg as `true`,\n\t *              to improve performance.\n\t *\n\t * @param LLMS_Event $start Event record for the start of the session.\n\t * @return bool\n\t */\n\tpublic function is_session_idle( $start ) {\n\n\t\t// Session is closed so it can't be idle.\n\t\tif ( ! $this->is_session_open( $start ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$now = llms_current_time( 'timestamp' );\n\n\t\t/**\n\t\t * Filter the time (in minutes) to allow a session to remain open before it's considered an \"idle\" session.\n\t\t *\n\t\t * @param int $minutes Number of minutes.\n\t\t */\n\t\t$timeout = absint( apply_filters( 'llms_idle_session_timeout', 30 ) ) * MINUTE_IN_SECONDS;\n\n\t\t// Session has started within the idle window, so it can't have expired yet.\n\t\tif ( ( $now - strtotime( $start->get( 'date' ) ) ) < $timeout ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$events = $this->get_session_events(\n\t\t\t$start,\n\t\t\tarray(\n\t\t\t\t'per_page'      => 1,\n\t\t\t\t'sort'          => array(\n\t\t\t\t\t'date' => 'DESC',\n\t\t\t\t),\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t// No events, the session is idle.\n\t\tif ( ! $events ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t$last_event = array_shift( $events );\n\t\treturn ( ( $now - strtotime( $last_event->get( 'date' ) ) ) > $timeout );\n\n\t}\n\n\t/**\n\t * Determines if the given session is open (has not ended)\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param LLMS_Event Event record for the start of the session.\n\t * @return bool\n\t */\n\tpublic function is_session_open( $start ) {\n\n\t\treturn is_null( $this->get_session_end( $start ) );\n\n\t}\n\n\t/**\n\t * Retrieve the last session object for the current user.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Added optional `$user_id` parameter.\n\t *\n\t * @param int $user_id Optional. WP_User ID of a student. Default `null`\n\t *                     If not provided, or a falsy is provided, will fall back on the current user id.\n\t * @return obj|null\n\t */\n\tprotected function get_last_session( $user_id = null ) {\n\t\t$user_id = $user_id ? $user_id : get_current_user_id();\n\n\t\tglobal $wpdb;\n\t\treturn $wpdb->get_row(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT *\n\t\t\t   FROM {$wpdb->prefix}lifterlms_events\n\t\t\t  WHERE actor_id = %d\n\t\t\t    AND object_type = 'session'\n\t\t\t    AND event_type = 'session'\n\t\t\t    AND event_action = 'start'\n\t\t   ORDER BY date DESC\n\t\t\t  LIMIT 1;\",\n\t\t\t\t$user_id\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t}\n\n\t/**\n\t * Retrieve open sessions.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Retrieve open sessions from the `wp_lifterlms_events_open_sessions` table.\n\t *\n\t * @param int $limit Number of sessions to return.\n\t * @param int $skip  Number of sessions to skip.\n\t * @return LLMS_Event[]\n\t */\n\tprotected function get_open_sessions( $limit = 50, $skip = 0 ) {\n\n\t\tglobal $wpdb;\n\t\t$sessions = $wpdb->get_col(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\t   SELECT event_id\n\t\t\t   FROM {$wpdb->prefix}lifterlms_events_open_sessions\n\t\t\t   ORDER BY event_id ASC\n\t\t\t   LIMIT %d, %d\n\t\t\",\n\t\t\t\t$skip,\n\t\t\t\t$limit\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t\t$ret = array();\n\t\tif ( count( $sessions ) ) {\n\t\t\tforeach ( $sessions as $id ) {\n\t\t\t\t$ret[] = new LLMS_Event( $id );\n\t\t\t}\n\t\t}\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Retrieve an array of events which occurred during a session.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param LLMS_Event $start Event record for the session.start event.\n\t * @param array      $args  Array of additional arguments to pass to the LLMS_Events_Query.\n\t * @return LLMS_Event[]\n\t */\n\tpublic function get_session_events( $start, $args = array() ) {\n\n\t\t$end = $this->get_session_end( $start );\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'date_after' => $start->get( 'date' ),\n\t\t\t\t'exclude'    => array( $start->get( 'id' ) ),\n\t\t\t\t'actor'      => $start->get( 'actor_id' ),\n\t\t\t\t'sort'       => array(\n\t\t\t\t\t'date' => 'ASC',\n\t\t\t\t),\n\t\t\t\t'per_page'   => 10,\n\t\t\t)\n\t\t);\n\n\t\tif ( $end ) {\n\t\t\t$args['date_before'] = $end->get( 'date' );\n\t\t\t$args['exclude'][]   = $end->get( 'id' );\n\t\t}\n\n\t\t$args['no_found_rows'] = true;\n\t\t$query = new LLMS_Events_Query( $args );\n\t\treturn $query->get_events();\n\n\t}\n\n\t/**\n\t * Retrieve session end record for by session id.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param LLMS_Event $start Event record for the session.start event.\n\t * @return LLMS_Event|end\n\t */\n\tpublic function get_session_end( $start ) {\n\n\t\tglobal $wpdb;\n\t\t$end = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT id\n\t\t\t   FROM {$wpdb->prefix}lifterlms_events\n\t\t\t  WHERE actor_id = %d\n\t\t\t    AND object_id = %d\n\t\t\t    AND object_type = 'session'\n\t\t\t    AND event_type = 'session'\n\t\t\t    AND event_action = 'end'\n\t\t   ORDER BY date DESC\n\t\t\t  LIMIT 1;\",\n\t\t\t\t$start->get( 'actor_id' ),\n\t\t\t\t$start->get( 'object_id' )\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t\tif ( ! $end ) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn new LLMS_Event( $end );\n\n\t}\n\n\t/**\n\t * Retrieve a new session ID.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Added optional `$user_id` parameter.\n\t *\n\t * @param int $user_id Optional. WP_User ID of a student. Default `null`\n\t *                     If not provided, or a falsy is provided, will fall back on the current user id.\n\t * @return int\n\t */\n\tprotected function get_new_id( $user_id = null ) {\n\n\t\t$user_id = $user_id ? $user_id : get_current_user_id();\n\n\t\t$last = $this->get_last_session( $user_id );\n\t\tif ( ! $last ) {\n\t\t\treturn 1;\n\t\t}\n\n\t\treturn ++$last->object_id;\n\n\t}\n\n\t/**\n\t * Start a new session for the current user.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Create open session entry in the `wp_lifterlms_events_open_sessions` table.\n\t *                  Added optional `$user_id` parameter.\n\t *\n\t * @param int $user_id Optional. WP_User ID of a student. Default `null`\n\t *                     If not provided, or a falsy is provided, will fall back on the current user id.\n\t * @return false|LLMS_Event|WP_Error\n\t */\n\tpublic function start( $user_id = null ) {\n\n\t\t$user_id = $user_id ? $user_id : get_current_user_id();\n\t\tif ( ! $user_id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$start = llms()->events()->record(\n\t\t\tarray(\n\t\t\t\t'actor_id'     => $user_id,\n\t\t\t\t'object_type'  => 'session',\n\t\t\t\t'object_id'    => $this->get_new_id( $user_id ),\n\t\t\t\t'event_type'   => 'session',\n\t\t\t\t'event_action' => 'start',\n\t\t\t)\n\t\t);\n\n\t\tif ( ! is_wp_error( $start ) ) {\n\t\t\tglobal $wpdb;\n\t\t\t$wpdb->query( // db call ok; no-cache ok.\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"\n\t\t\t\t\tINSERT INTO {$wpdb->prefix}lifterlms_events_open_sessions ( `event_id` ) VALUES ( %d )\n\t\t\t\t\t\",\n\t\t\t\t\t$start->get( 'id' )\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn $start;\n\n\t}\n\n}\n\nreturn LLMS_Sessions::instance();\n"
  },
  {
    "path": "includes/class-llms-staging.php",
    "content": "<?php\n/**\n * Handle warnings and notices when staging is enabled.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.32.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Staging class.\n *\n * @since 3.32.0\n */\nclass LLMS_Staging {\n\n\t/**\n\t * Static Constructor.\n\t *\n\t * @since 3.32.0\n\t * @since 4.12.0 Add hook on `llms_site_clone_detected` action.\n\t * @since 4.13.0 Only add actions when recurring payments constant is not defined.\n\t *               If `LLMS_SITE_IS_CLONE` is defined & true, automatically disable recurring payments.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tif ( defined( 'LLMS_SITE_IS_CLONE' ) && LLMS_SITE_IS_CLONE ) {\n\t\t\tllms_maybe_define_constant( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS', false );\n\t\t}\n\n\t\tif ( ! defined( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS' ) ) {\n\t\t\tadd_action( 'llms_site_clone_detected', array( __CLASS__, 'clone_detected' ) );\n\t\t\tadd_action( 'admin_init', array( __CLASS__, 'handle_staging_notice_actions' ) );\n\t\t}\n\n\t\tadd_action( 'admin_menu', array( __CLASS__, 'menu_warning' ) );\n\t}\n\n\t/**\n\t * Callback function to automatically disable site features when a clone is detected\n\t *\n\t * @since 4.12.0\n\t * @since 4.13.0 Only disable payments for logged in users on the admin panel when not processing ajax requests.\n\t *\n\t * @return void\n\t */\n\tpublic static function clone_detected() {\n\n\t\tif ( is_admin() && current_user_can( 'manage_lifterlms' ) && ! wp_doing_ajax() ) {\n\t\t\tself::notice();\n\t\t\tLLMS_Site::update_feature( 'recurring_payments', false );\n\t\t}\n\t}\n\n\t/**\n\t * Retrieves the HTML for the \"warning bubble\" displayed in the admin menu when staging mode is active\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return string\n\t */\n\tprotected static function get_menu_warning_bubble() {\n\t\treturn ' <span class=\"update-plugins\">' . esc_html__( 'Staging', 'lifterlms' ) . '</span>';\n\t}\n\n\t/**\n\t * Handle the action buttons present in the recurring payments staging notice.\n\t *\n\t * @since 3.32.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 4.12.0 Use `llms_filter_input()` for retrieval of `$_GET` data.\n\t * @since 5.9.0 Drop usage of deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic static function handle_staging_notice_actions() {\n\n\t\tif ( ! isset( $_GET['llms-staging-status'] ) || ! isset( $_GET['_llms_staging_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_llms_staging_nonce'] ) ), 'llms_staging_status' ) || ! current_user_can( 'manage_options' ) ) {\n\t\t\twp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'lifterlms' ) );\n\t\t}\n\n\t\t$action = llms_filter_input( INPUT_GET, 'llms-staging-status' );\n\t\tif ( 'enable' === $action ) {\n\t\t\tLLMS_Site::set_lock_url();\n\t\t\tLLMS_Site::update_feature( 'recurring_payments', true );\n\t\t} elseif ( 'disable' === $action ) {\n\t\t\tLLMS_Site::clear_lock_url();\n\t\t\tLLMS_Site::update_feature( 'recurring_payments', false );\n\t\t\tupdate_option( 'llms_site_url_ignore', 'yes' );\n\t\t}\n\n\t\tLLMS_Admin_Notices::delete_notice( 'maybe-staging' );\n\n\t\tif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) {\n\t\t\tllms_redirect_and_exit( sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) );\n\t\t}\n\t}\n\n\n\n\t/**\n\t * Adds a \"bubble\" to the \"Orders\" menu item when recurring payments are disabled.\n\t *\n\t * @since 3.32.0\n\t * @since 4.12.0 Moved HTML for the warning bubble into it's own method.\n\t *\n\t * @return void\n\t */\n\tpublic static function menu_warning() {\n\n\t\tif ( LLMS_Site::get_feature( 'recurring_payments' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tglobal $menu;\n\t\tforeach ( $menu as $index => $item ) {\n\n\t\t\tif ( 'edit.php?post_type=llms_order' === $item[2] ) {\n\t\t\t\t$menu[ $index ][0] .= self::get_menu_warning_bubble();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Output a notice informing the user the site was put into staging mode.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic static function notice() {\n\n\t\t$id = 'maybe-staging';\n\n\t\tif ( ! LLMS_Admin_Notices::has_notice( $id ) ) {\n\n\t\t\tLLMS_Admin_Notices::add_notice(\n\t\t\t\t$id,\n\t\t\t\tarray(\n\t\t\t\t\t'type'        => 'info',\n\t\t\t\t\t'dismissible' => false,\n\t\t\t\t\t'remindable'  => false,\n\t\t\t\t\t'template'    => 'admin/notices/staging.php',\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\t}\n}\n\nreturn LLMS_Staging::init();\n"
  },
  {
    "path": "includes/class.llms.achievement.php",
    "content": "<?php\n/**\n * Base Achievement Class\n *\n * Handles generating Achievement\n *\n * @package LifterLMS/Classes/Achievements\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Base Achievement Class\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @deprecated 6.0.0 Class `LLMS_Achievement` is deprecated with no direct replacement.\n */\nclass LLMS_Achievement {\n\n\t/**\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $achievement_template_id;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $achievement_title;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $content;\n\n\t/**\n\t * is the achievement enabled\n\t *\n\t * @var bool\n\t * @since 1.0.0\n\t */\n\tpublic $enabled;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tpublic $find = array();\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $id;\n\n\t/**\n\t * image id\n\t *\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $image;\n\n\t/**\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $lesson_id;\n\n\t/**\n\t * @var WP_User\n\t * @since 1.0.0\n\t */\n\tpublic $object;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tpublic $replace = array();\n\n\t/**\n\t * post title\n\t *\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $title;\n\n\t/**\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $userid;\n\n\t/**\n\t * Alert when deprecated methods are used.\n\t *\n\t * This class as well as core classes extending it have been deprecated. All public and protected methods\n\t * have been changed to private and will be made accessible through this magic method which also emits a\n\t * deprecation warning.\n\t *\n\t * This public method has been intentionally marked as private to denote it's temporary lifespan. It will be\n\t * removed alongside this class in the next major release.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @access private\n\t *\n\t * @param string $name Name of the method being called.\n\t * @param array  $args Arguments provided to the method.\n\t * @return void\n\t */\n\tpublic function __call( $name, $args ) {\n\t\t_deprecated_function( __CLASS__ . '::' . esc_html( $name ), '6.0.0' );\n\t\tif ( method_exists( $this, $name ) ) {\n\t\t\t$this->$name( ...$args );\n\t\t}\n\t}\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since Unknown.\n\t * @deprecated 6.0.0 `LLMS_Achievement::__construct()` is deprecated with no replacement.\n\t */\n\tpublic function __construct() {\n\n\t\t// Settings TODO Refactor: theses can come from the achievement post now.\n\t\t$this->enabled = get_option( 'enabled' );\n\n\t\t$this->find    = array( '{blogname}', '{site_title}' );\n\t\t$this->replace = array( $this->get_blogname(), $this->get_blogname() );\n\t}\n\n\t/**\n\t * Checks if achievement is enabled\n\t *\n\t * @since Unknown\n\t * @since 3.24.0 Unknown.\n\t * @deprecated 6.0.0 `LLMS_Achievement::is_enabled()` is deprecated with no replacement.\n\t *\n\t * @return bool\n\t */\n\tprivate function is_enabled() {\n\t\t$enabled = 'yes' == $this->enabled ? true : false;\n\t\treturn true;\n\t}\n\n\t/**\n\t * Get Blog name\n\t *\n\t * Used by achievement merge fields.\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Achievement::get_blogname()` is deprecated with no replacement.\n\t *\n\t * @return string\n\t */\n\tprivate function get_blogname() {\n\t\treturn wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );\n\t}\n\n\t/**\n\t * Format String\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Achievement::format_string()` is deprecated with no replacement.\n\t *\n\t * @param string $string Un-formatted string.\n\t * @return string Formatted string.\n\t */\n\tprivate function format_string( $string ) {\n\t\treturn str_replace( $this->find, $this->replace, $string );\n\t}\n\n\t/**\n\t * Queries Achievement title postmeta\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Achievement::get_title()` is deprecated with no replacement.\n\t *\n\t * @return string\n\t */\n\tprivate function get_title() {\n\t\treturn apply_filters( '_llms_achievement_title' . $this->id, $this->title, $this->object );\n\t}\n\n\t/**\n\t * Get the content of the Achievement\n\t *\n\t * @since   1.0.0\n\t * @version 1.4.1\n\t * @deprecated 6.0.0 `LLMS_Achievement::get_content()` is deprecated with no replacement.\n\t *\n\t * @return string Data needed to generate achievement.\n\t */\n\tprivate function get_content() {\n\t\t$achievement_content = $this->content;\n\t\treturn $achievement_content;\n\t}\n\n\t/**\n\t * Generate HTML output of achievement.\n\t *\n\t * Converts merge fields to raw data sources and wraps content in HTML\n\t * then saves new achievement post and updates user_postmeta table.\n\t *\n\t * @since 1.0.0\n\t * @deprecated 6.0.0 `LLMS_Achievement::get_content_html()` is deprecated with no replacement.\n\t *\n\t * @return void\n\t */\n\tprivate function get_content_html() {}\n\n\t/**\n\t * Create the achievement\n\t *\n\t * @since 1.0.0\n\t * @since 3.8.0 Unknown\n\t * @deprecated 6.0.0 `LLMS_Achievement::create()` is deprecated with no replacement.\n\t *\n\t * @param string $content Achievement body content.\n\t * @return void\n\t */\n\tprivate function create( $content ) {\n\t\tglobal $wpdb;\n\n\t\t$new_user_achievement = apply_filters(\n\t\t\t'lifterlms_new_achievement',\n\t\t\tarray(\n\t\t\t\t'post_type'    => 'llms_my_achievement',\n\t\t\t\t'post_title'   => $this->title,\n\t\t\t\t'post_content' => $content,\n\t\t\t\t'post_status'  => 'publish',\n\t\t\t\t'post_author'  => 1,\n\t\t\t)\n\t\t);\n\n\t\t$new_user_achievement_id = wp_insert_post( $new_user_achievement, true );\n\n\t\tupdate_post_meta( $new_user_achievement_id, '_llms_achievement_title', $this->achievement_title );\n\t\tupdate_post_meta( $new_user_achievement_id, '_llms_achievement_image', $this->image );\n\t\tupdate_post_meta( $new_user_achievement_id, '_llms_achievement_content', $this->content );\n\t\tupdate_post_meta( $new_user_achievement_id, '_llms_achievement_template', $this->achievement_template_id );\n\n\t\t$user_metadatas = array(\n\t\t\t'_achievement_earned' => $new_user_achievement_id,\n\t\t);\n\n\t\tforeach ( $user_metadatas as $key => $value ) {\n\t\t\t$update_user_postmeta = $wpdb->insert(\n\t\t\t\t$wpdb->prefix . 'lifterlms_user_postmeta',\n\t\t\t\tarray(\n\t\t\t\t\t'user_id'      => $this->userid,\n\t\t\t\t\t'post_id'      => $this->lesson_id,\n\t\t\t\t\t'meta_key'     => $key,\n\t\t\t\t\t'meta_value'   => $value,\n\t\t\t\t\t'updated_date' => current_time( 'mysql' ),\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// This hook is documented in includes/class-llms-engagement-handler.php.\n\t\tdo_action( 'llms_user_earned_achievement', $this->userid, $new_user_achievement_id, $this->lesson_id );\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.achievements.php",
    "content": "<?php\n/**\n * Achievements Base Class\n *\n * @package LifterLMS/Classes/Achievements\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Main Achievements singleton\n *\n * @see llms()->achievements()\n *\n * @since 1.0.0\n * @since 3.24.0 Unknown.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Changes:\n *              - Deprecated the unused public class property `LLMS_Achievements::$content` with no replacement.\n *              - Deprecated the `LLMS_Achievements::trigger_engagement()` method.\n *                Use the {@see LLMS_Engagement_Handler::handle_achievement()} method instead.\n *              - Removed the unused private `LLMS_Achievements::$_from_address` property.\n *              - Removed the unused private `LLMS_Achievements::$_from_name` property.\n *              - Removed the unused private `LLMS_Achievements::$_content_type` property.\n *              - Removed the deprecated `LLMS_Achievements::$_instance` property.\n */\nclass LLMS_Achievements {\n\n\tuse LLMS_Trait_Singleton,\n\t\tLLMS_Trait_Award_Default_Images;\n\n\t/**\n\t * List of available achievement types.\n\t *\n\t * @var array\n\t */\n\tpublic $achievements = array();\n\n\t/**\n\t * The ID for the award type.\n\t *\n\t * Used by {@see LLMS_Trait_Award_Default_Images}.\n\t *\n\t * @var string\n\t */\n\tprotected $award_type = 'achievement';\n\n\t/**\n\t * Deprecated.\n\t *\n\t * @deprecated 6.0.0 Unused public class property `LLMS_Achievements::$content` is deprecated with no replacement.\n\t *\n\t * @var null\n\t */\n\tpublic $content;\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\t\t$this->init();\n\t}\n\n\t/**\n\t * Includes achievement class.\n\t *\n\t * @since 1.0.0\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\n\t\t$this->achievements['LLMS_Achievement_User'] = include_once 'achievements/class.llms.achievement.user.php';\n\t}\n\n\t/**\n\t * Get a list of achievement Achievement Template IDs for a given post.\n\t *\n\t * @since 3.24.0\n\t * @since 5.3.3 Set the query limit to 500.\n\t *\n\t * @param array|int $post_ids         Post IDs or single post ID to look for achievements by.\n\t * @param bool      $include_children If true, will include course children (sections, lessons, and quizzes).\n\t * @return array\n\t */\n\tpublic function get_achievements_by_post( $post_ids, $include_children = true ) {\n\n\t\tif ( ! is_array( $post_ids ) ) {\n\t\t\t$post_ids = array( $post_ids );\n\t\t}\n\n\t\t$original_post_ids = $post_ids;\n\n\t\tif ( $include_children ) {\n\n\t\t\tforeach ( $post_ids as $post_id ) {\n\t\t\t\tif ( 'course' === get_post_type( $post_id ) ) {\n\t\t\t\t\t$course   = llms_get_post( $post_id );\n\t\t\t\t\t$post_ids = array_merge(\n\t\t\t\t\t\t$post_ids,\n\t\t\t\t\t\t$course->get_sections( 'ids' ),\n\t\t\t\t\t\t$course->get_lessons( 'ids' ),\n\t\t\t\t\t\t$course->get_quizzes()\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters the query args to retrieve the achievements by post.\n\t\t *\n\t\t * @since 5.3.3\n\t\t *\n\t\t * @param array     $args              The query args to retrieve the achievements by post.\n\t\t * @param array|int $post_ids          Post IDs or single post ID to look for achievements by.\n\t\t * @param bool      $include_children  If true, will include course children (sections, lessons, and quizzes).\n\t\t */\n\t\t$query_args = apply_filters(\n\t\t\t'llms_achievements_by_post_query_args',\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'llms_engagement',\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_engagement_type',\n\t\t\t\t\t\t'value' => 'achievement',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'compare' => 'IN',\n\t\t\t\t\t\t'key'     => '_llms_engagement_trigger_post',\n\t\t\t\t\t\t'value'   => $post_ids,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'posts_per_page' => 500,\n\t\t\t),\n\t\t\t$original_post_ids,\n\t\t\t$include_children\n\t\t);\n\n\t\t$query = new WP_Query( $query_args );\n\n\t\t$achievements = array();\n\n\t\tforeach ( $query->posts as $engagement ) {\n\t\t\t$achievements[] = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\t\t}\n\n\t\treturn $achievements;\n\n\t}\n\n\t/**\n\t * Award an achievement to a user\n\t *\n\t * Calls trigger method passing arguments.\n\t *\n\t * @since 1.0.0\n\t * @deprecated 6.0.0 `LLMS_Achievements::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement()`.\n\t *\n\t * @param int $person_id       WP_User ID.\n\t * @param int $achievement_id  WP_Post ID of the achievement template.\n\t * @param int $related_post_id WP_Post ID of the related post, for example a lesson id.\n\t * @return void\n\t */\n\tpublic function trigger_engagement( $person_id, $achievement_id, $related_post_id ) {\n\t\t_deprecated_function( 'LLMS_Achievements::trigger_engagement()', '6.0.0', 'LLMS_Engagement_Handler::handle_achievements()' );\n\t\tLLMS_Engagement_Handler::handle_achievement( array( $person_id, $achievement_id, $related_post_id, null ) );\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.ajax.handler.php",
    "content": "<?php\n/**\n * LifterLMS AJAX Event Handler.\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_AJAX_Handler class\n *\n * @since 1.0.0\n * @since 3.0.0 Added `bulk_enroll_students()` handler.\n * @since 3.2.0 Added `get_admin_table_data()` handler.\n * @since 3.4.0 Unknown.\n * @since 3.13.0 Added `instructors_mb_store()` handler.\n * @since 3.15.0 Added `export_admin_table()` handler, and other unknown changes.\n * @since 3.28.1 Unknown.\n * @since 3.30.0 Added `llms_save_membership_autoenroll_courses` method.\n * @since 3.30.3 Fixed spelling errors.\n * @since 3.32.0 Update `select2_query_posts` to use llms_filter_input() and allows for querying posts by post status(es).\n * @since 3.33.0 Update `update_student_enrollment` to handle enrollment deletion requests, make sure the input array param 'post_id' field is not empty.\n *               Also always return either a WP_Error on failure or a \"success\" array on requested action performed.\n * @since 3.33.1 Update `llms_update_access_plans` to use `wp_unslash()` before inserting access plan data.\n * @since 3.37.2 Update `select2_query_posts` to allow filtering posts by instructor.\n * @since 3.37.14 Added `persist_tracking_events()` handler.\n *                Used strict comparison where needed.\n * @since 3.37.15 Update `get_admin_table_data()` and `export_admin_table()` to verify user permissions before processing data.\n * @since 3.39.0 Minor code readability updates to the `validate_coupon_code()` method.\n * @since 5.7.0 Deprecated the `LLMS_AJAX_Handler::add_lesson_to_course()` method with no replacement.\n *              Deprecated the `LLMS_AJAX_Handler::create_lesson()` method with no replacement.\n *              Deprecated the `LLMS_AJAX_Handler::create_section()` method with no replacement.\n */\nclass LLMS_AJAX_Handler {\n\t/**\n\t * Queue all members of a membership to be enrolled into a specific course\n\t *\n\t * Triggered from the auto-enrollment tab of a membership.\n\t *\n\t * @since 3.4.0\n\t * @since 3.15.0 Unknown.\n\t *\n\t * @param array $request Array of request data.\n\t * @return array\n\t */\n\tpublic static function bulk_enroll_membership_into_course( $request ) {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( empty( $request['post_id'] ) || empty( $request['course_id'] ) ) {\n\t\t\treturn new WP_Error( 400, __( 'Missing required parameters', 'lifterlms' ) );\n\t\t}\n\n\t\tdo_action( 'llms_membership_do_bulk_course_enrollment', $request['post_id'], $request['course_id'] );\n\n\t\treturn array(\n\t\t\t'message' => __( 'Members are being enrolled in the background. You may leave this page.', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Add or remove a student from a course or membership\n\t *\n\t * @since 3.0.0\n\t * @since 3.4.0 Unknown.\n\t *\n\t * @param array $request $_REQUEST object.\n\t * @return (void|WP_Error)\n\t */\n\tpublic static function bulk_enroll_students( $request ) {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( empty( $request['post_id'] ) || empty( $request['student_ids'] ) || ! is_array( $request['student_ids'] ) ) {\n\t\t\treturn new WP_Error( 400, __( 'Missing required parameters', 'lifterlms' ) );\n\t\t}\n\n\t\t$post_id = intval( $request['post_id'] );\n\n\t\tforeach ( $request['student_ids'] as $id ) {\n\t\t\tllms_enroll_student( intval( $id ), $post_id, 'admin_' . get_current_user_id() );\n\t\t}\n\t}\n\n\t/**\n\t * Determines if voucher codes already exist.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic static function check_voucher_duplicate() {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$post_id = ! empty( $_REQUEST['postId'] ) ? absint( llms_filter_input( INPUT_POST, 'postId', FILTER_SANITIZE_NUMBER_INT ) ) : 0;\n\t\t$codes   = ! empty( $_REQUEST['codes'] ) ? llms_filter_input_sanitize_string( INPUT_POST, 'codes', array( FILTER_REQUIRE_ARRAY ) ) : array();\n\n\t\tif ( ! $post_id || ! $codes ) {\n\t\t\treturn new WP_Error( 400, __( 'Missing required parameters', 'lifterlms' ) );\n\t\t} elseif ( ! current_user_can( 'edit_post', $post_id ) ) {\n\t\t\treturn new WP_Error( 401, __( 'Missing required permissions to perform this action.', 'lifterlms' ) );\n\t\t}\n\n\t\t$codes = implode(\n\t\t\t',',\n\t\t\tarray_map(\n\t\t\t\tfunction ( $code ) {\n\t\t\t\t\treturn sprintf( \"'%s'\", esc_sql( $code ) );\n\t\t\t\t},\n\t\t\t\tarray_filter( $codes )\n\t\t\t)\n\t\t);\n\n\t\tglobal $wpdb;\n\t\t$table = $wpdb->prefix . 'lifterlms_vouchers_codes';\n\t\t$res   = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT code FROM $table WHERE code IN( $codes ) AND voucher_id != %d\", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\t\tarray( $post_id )\n\t\t\t),\n\t\t\tARRAY_A\n\t\t);\n\n\t\twp_send_json(\n\t\t\tarray(\n\t\t\t\t'success'    => true,\n\t\t\t\t'duplicates' => $res,\n\t\t\t)\n\t\t);\n\t\twp_die();\n\t}\n\n\t/**\n\t * Move a Product Access Plan to the trash\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $request $_REQUEST object.\n\t * @return bool|WP_Error WP_Error on error, true if successful.\n\t */\n\tpublic static function delete_access_plan( $request ) {\n\n\t\tif ( empty( $request['plan_id'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$access_plan = llms_get_post( $request['plan_id'] );\n\t\tif ( ! $access_plan->get( 'product_id' ) ) {\n\t\t\twp_die();\n\t\t}\n\t\tif ( ! llms_current_user_can_edit_product( $access_plan->get( 'product_id' ) ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( ! wp_trash_post( $request['plan_id'] ) ) {\n\n\t\t\t$err = new WP_Error();\n\t\t\t$err->add( 'error', __( 'There was a problem deleting your access plan, please try again.', 'lifterlms' ) );\n\t\t\treturn $err;\n\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Retrieve a new instance of admin table class from a handler string.\n\t *\n\t * @since 3.37.15\n\t * @since 4.7.0 Don't require `LLMS_Admin_Reporting`, it's loaded automatically.\n\t *\n\t * @param string $handler Unprefixed handler class string. For example \"Students\" or \"Course_Students\".\n\t * @return object|false Instance of the admin table class or false if the class can't be found.\n\t */\n\tprotected static function get_admin_table_instance( $handler ) {\n\n\t\tLLMS_Admin_Reporting::includes();\n\n\t\t$handler = 'LLMS_Table_' . $handler;\n\t\tif ( class_exists( $handler ) ) {\n\t\t\treturn new $handler();\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Queue a table export event\n\t *\n\t * @since 3.15.0\n\t * @since 3.28.1 Unknown.\n\t * @since 3.37.15 Verify user permissions before processing request data.\n\t *\n\t * @param array $request Post data ($_REQUEST).\n\t * @return array|bool\n\t */\n\tpublic static function export_admin_table( $request ) {\n\t\tif ( ! current_user_can( 'view_lifterlms_reports' ) || empty( $request['handler'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$table = self::get_admin_table_instance( $request['handler'] );\n\t\tif ( ! $table ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$file = isset( $request['filename'] ) ? $request['filename'] : null;\n\t\treturn $table->generate_export_file( $request, $file );\n\t}\n\n\t/**\n\t * Reload admin tables\n\t *\n\t * @since 3.2.0\n\t * @since 3.37.15 Verify user permissions before processing request data.\n\t *                Use `wp_json_encode()` in favor of `json_encode()`.\n\t *\n\t * @param array $request Post data ($_REQUEST).\n\t * @return array\n\t */\n\tpublic static function get_admin_table_data( $request ) {\n\n\t\tif ( ! current_user_can( 'view_lifterlms_reports' ) || empty( $request['handler'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$table = self::get_admin_table_instance( $request['handler'] );\n\t\tif ( ! $table ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$table->get_results( $request );\n\t\treturn array(\n\t\t\t'args'  => wp_json_encode( $table->get_args() ),\n\t\t\t'thead' => trim( $table->get_thead_html() ),\n\t\t\t'tbody' => trim( $table->get_tbody_html() ),\n\t\t\t'tfoot' => trim( $table->get_tfoot_html() ),\n\t\t);\n\t}\n\n\t/**\n\t * Store data for the instructors metabox\n\t *\n\t * @since 3.13.0\n\t * @since 3.30.3 Fixed typos.\n\t *\n\t * @param array $request $_REQUEST object.\n\t * @return array\n\t */\n\tpublic static function instructors_mb_store( $request ) {\n\n\t\t// validate required params.\n\t\tif ( ! isset( $request['store_action'] ) ||\n\t\t\t! isset( $request['post_id'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( ! llms_current_user_can_edit_product( $request['post_id'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$post = llms_get_post( $request['post_id'] );\n\n\t\tswitch ( $request['store_action'] ) {\n\n\t\t\tcase 'load':\n\t\t\t\t$instructors = $post->get_instructors();\n\t\t\t\tbreak;\n\n\t\t\tcase 'save':\n\t\t\t\t$instructors = array();\n\n\t\t\t\tforeach ( $request['rows'] as $instructor ) {\n\n\t\t\t\t\tforeach ( $instructor as $key => $val ) {\n\n\t\t\t\t\t\t$new_key                = str_replace( array( 'llms', '_' ), '', $key );\n\t\t\t\t\t\t$new_key                = preg_replace( '/[0-9]+/', '', $new_key );\n\t\t\t\t\t\t$instructor[ $new_key ] = $val;\n\t\t\t\t\t\tunset( $instructor[ $key ] );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t$instructors[] = $instructor;\n\n\t\t\t\t}\n\n\t\t\t\t$post->set_instructors( $instructors );\n\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t$data = array();\n\n\t\tforeach ( $instructors as $instructor ) {\n\n\t\t\t$new_instructor = array();\n\t\t\tforeach ( $instructor as $key => $val ) {\n\t\t\t\tif ( 'id' === $key ) {\n\t\t\t\t\t$val = llms_make_select2_student_array( array( $instructor['id'] ) );\n\t\t\t\t}\n\t\t\t\t$new_instructor[ '_llms_' . $key ] = $val;\n\t\t\t}\n\t\t\t$data[] = $new_instructor;\n\t\t}\n\n\t\twp_send_json(\n\t\t\tarray(\n\t\t\t\t'data'    => $data,\n\t\t\t\t'message' => 'success',\n\t\t\t\t'success' => true,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Handle notification display & dismissal.\n\t *\n\t * @since 3.8.0\n\t * @since 3.37.14 Use strict comparison.\n\t * @since 7.1.0 Improve notifications query performance by not calculating unneeded found rows.\n\t *\n\t * @param array $request $_POST data.\n\t * @return array\n\t */\n\tpublic static function notifications_heartbeart( $request ) {\n\n\t\tif ( ! is_user_logged_in() ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$ret = array(\n\t\t\t'new' => array(),\n\t\t);\n\n\t\tif ( ! empty( $request['dismissals'] ) ) {\n\t\t\tforeach ( $request['dismissals'] as $nid ) {\n\t\t\t\t$noti = new LLMS_Notification( $nid );\n\t\t\t\tif ( get_current_user_id() === absint( $noti->get( 'subscriber' ) ) ) {\n\t\t\t\t\t$noti->set( 'status', 'read' );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Get 5 most recent new notifications for the current user.\n\t\t$query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'per_page'      => 5,\n\t\t\t\t'statuses'      => 'new',\n\t\t\t\t'types'         => 'basic',\n\t\t\t\t'subscriber'    => get_current_user_id(),\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t$ret['new'] = $query->get_notifications();\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Remove a course from the list of membership auto enrollment courses\n\t *\n\t * Called from \"Auto Enrollment\" tab of LLMS Membership Metaboxes.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $request $_POST data.\n\t * @return (void|WP_Error)\n\t */\n\tpublic static function membership_remove_auto_enroll_course( $request ) {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( empty( $request['post_id'] ) || empty( $request['course_id'] ) ) {\n\t\t\treturn new WP_Error( 'error', __( 'Missing required parameters.', 'lifterlms' ) );\n\t\t}\n\n\t\t$membership = new LLMS_Membership( $request['post_id'] );\n\n\t\tif ( ! $membership->remove_auto_enroll_course( intval( $request['course_id'] ) ) ) {\n\t\t\treturn new WP_Error( 'error', __( 'There was an error removing the course, please try again.', 'lifterlms' ) );\n\t\t}\n\t}\n\n\t/**\n\t * Start a Quiz Attempt.\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.4 Unknown.\n\t * @since 6.4.0 Make sure attempts limit was not reached.\n\t * @since 7.8.0 Use `$attempt->get( 'status' )` instead of the not existing `$attempt->get_status()` method and added `can_be_resumed` param.\n\t *\n\t * @param array $request $_POST data.\n\t *                       required:\n\t *                           (string) attempt_key\n\t *                           or\n\t *                           (int) quiz_id\n\t *                           (int) lesson_id.\n\t * @return WP_Error|array WP_Error on error or array containing html template of the first question.\n\t */\n\tpublic static function quiz_start( $request ) {\n\n\t\t$err = new WP_Error();\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\t$err->add( 400, __( 'You must be logged in to take quizzes.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t// Limit reached?\n\t\tif ( isset( $request['quiz_id'] ) && ! ( new LLMS_Quiz( $request['quiz_id'] ) )->is_open() ) {\n\t\t\t$err->add( 400, __( \"You've reached the maximum number of attempts for this quiz.\", 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$attempt = false;\n\t\tif ( ! empty( $request['attempt_key'] ) ) {\n\t\t\t$attempt = $student->quizzes()->get_attempt_by_key( $request['attempt_key'] );\n\t\t}\n\n\t\tif ( ! $attempt || 'new' !== $attempt->get( 'status' ) ) {\n\n\t\t\tif ( ! isset( $request['quiz_id'] ) || ! isset( $request['lesson_id'] ) ) {\n\t\t\t\t$err->add( 400, __( 'There was an error starting the quiz. Please return to the lesson and begin again.', 'lifterlms' ) );\n\t\t\t\treturn $err;\n\t\t\t}\n\n\t\t\t// Mark the previous attempt as ended if it could be resumed but we're restarting instead.\n\t\t\t$previous_attempt_key = ( new LLMS_Quiz( $request['quiz_id'] ) )->get_student_last_attempt_key();\n\t\t\tif ( $previous_attempt_key ) {\n\t\t\t\t$previous_attempt = $student->quizzes()->get_attempt_by_key( $previous_attempt_key );\n\t\t\t\tif ( $previous_attempt && $previous_attempt->can_be_resumed() ) {\n\t\t\t\t\t$previous_attempt->end();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$attempt = LLMS_Quiz_Attempt::init( absint( $request['quiz_id'] ), absint( $request['lesson_id'] ), $student->get( 'id' ) );\n\n\t\t}\n\n\t\t$question_id = $attempt->get_first_question();\n\t\tif ( ! $question_id ) {\n\t\t\t$err->add( 404, __( 'Unable to start quiz because the quiz does not contain any questions.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$attempt->start();\n\t\t$html = llms_get_template_ajax(\n\t\t\t'content-single-question.php',\n\t\t\tarray(\n\t\t\t\t'attempt'  => $attempt,\n\t\t\t\t'question' => llms_get_post( $question_id ),\n\t\t\t)\n\t\t);\n\n\t\t$quiz  = $attempt->get_quiz();\n\t\t$limit = $quiz->has_time_limit() && ! $student->has_unlimited_quiz_time() ? $quiz->get( 'time_limit' ) : false;\n\n\t\treturn array(\n\t\t\t'attempt_key'    => $attempt->get_key(),\n\t\t\t'html'           => $html,\n\t\t\t'time_limit'     => $limit,\n\t\t\t'question_id'    => $question_id,\n\t\t\t'total'          => $attempt->get_count( 'questions' ),\n\t\t\t'can_be_resumed' => $attempt->can_be_resumed(),\n\t\t);\n\t}\n\n\t/**\n\t * Resume a Quiz Attempt.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param array $request $_POST data.\n\t *                       required:\n\t *                           (string) attempt_key\n\t * @return WP_Error|array WP_Error on error or array containing html template of the first question to be answered.\n\t */\n\tpublic static function quiz_resume( $request ) {\n\n\t\t$err = new WP_Error();\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\t$err->add( 400, __( 'You must be logged in to take quizzes.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\tif ( ! isset( $request['attempt_key'] ) ) {\n\t\t\t$err->add( 400, __( 'Attempt key is required.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$attempt = $student->quizzes()->get_attempt_by_key( $request['attempt_key'] );\n\n\t\tif ( empty( $attempt ) ) {\n\t\t\t$err->add( 404, __( 'The requested attempt could not be found.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$quiz = $attempt->get_quiz();\n\t\tif ( empty( $quiz ) ) {\n\t\t\t$err->add( 400, __( 'No quiz found.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\tif (\n\t\t\t! $attempt->can_be_resumed() ||\n\t\t\t! $attempt->is_last_attempt()\n\t\t) {\n\t\t\t$err->add(\n\t\t\t\t400,\n\t\t\t\t__(\n\t\t\t\t\t'There was an error resuming the quiz. Please return to the lesson and begin again.',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t)\n\t\t\t);\n\t\t\treturn $err;\n\t\t}\n\n\t\t$question_ids = array_column( $attempt->get_questions(), 'id' );\n\t\tif ( ! $question_ids ) {\n\t\t\t$err->add(\n\t\t\t\t404,\n\t\t\t\t__( 'Unable to resume quiz because the quiz does not contain any questions.', 'lifterlms' )\n\t\t\t);\n\t\t\treturn $err;\n\t\t}\n\n\t\t$question_id = $attempt->get( 'current_question_id' ) ? $attempt->get( 'current_question_id' ) : $attempt->get_next_question( null );\n\n\t\tif ( ! $question_id ) {\n\t\t\treturn self::quiz_end( $request, $attempt );\n\t\t}\n\n\t\t$html = llms_get_template_ajax(\n\t\t\t'content-single-question.php',\n\t\t\tarray(\n\t\t\t\t'attempt'  => $attempt,\n\t\t\t\t'question' => llms_get_post( $question_id ),\n\t\t\t)\n\t\t);\n\n\t\treturn array(\n\t\t\t'attempt_key'    => $attempt->get_key(),\n\t\t\t'html'           => $html,\n\t\t\t'question_id'    => $question_id,\n\t\t\t'total'          => $attempt->get_count( 'questions' ),\n\t\t\t'question_ids'   => $question_ids,\n\t\t\t'can_be_resumed' => $attempt->can_be_resumed(),\n\t\t);\n\t}\n\n\t/**\n\t * AJAX Quiz get question.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param array $request $_POST data.\n\t * @return WP_Error|array\n\t */\n\tpublic static function quiz_get_question( $request ) {\n\t\t$err = new WP_Error();\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\t$err->add( 400, __( 'You must be logged in to take quizzes.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$required = array( 'attempt_key', 'question_id' );\n\t\tforeach ( $required as $key ) {\n\t\t\tif ( empty( $request[ $key ] ) ) {\n\t\t\t\t$err->add( 400, __( 'Missing required parameters. Could not proceed.', 'lifterlms' ) );\n\t\t\t\treturn $err;\n\t\t\t}\n\t\t}\n\n\t\t$attempt_key     = sanitize_text_field( $request['attempt_key'] );\n\t\t$question_id     = absint( $request['question_id'] );\n\t\t$student_quizzes = $student->quizzes();\n\t\t$attempt         = $student_quizzes->get_attempt_by_key( $attempt_key );\n\n\t\t// Don't allow the question to be retrieved if the attempt is not open or can't be resumed.\n\t\tif ( ! $attempt || ( ! $attempt->get_quiz()->is_open() && ( ! $attempt->get_quiz()->can_be_resumed() || ! $attempt->can_be_resumed() ) ) ) {\n\t\t\t$err->add( 500, __( 'There was an error retrieving the question. Please return to the lesson and try again.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$question_id = $attempt->get_question( $question_id );\n\n\t\tif ( ! $question_id ) {\n\t\t\t$err->add( 404, __( 'Cannot find the requested question id. Please return to the lesson and try again.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$html = llms_get_template_ajax(\n\t\t\t'content-single-question.php',\n\t\t\tarray(\n\t\t\t\t'attempt'  => $attempt,\n\t\t\t\t'question' => llms_get_post( $question_id ),\n\t\t\t)\n\t\t);\n\n\t\treturn array(\n\t\t\t'html'        => $html,\n\t\t\t'question_id' => $question_id,\n\t\t);\n\t}\n\n\t/**\n\t * AJAX Quiz answer question.\n\t *\n\t * @since 3.9.0\n\t * @since 3.27.0 Unknown.\n\t * @since 6.4.0 Make sure attempts limit was not reached.\n\t *\n\t * @param array $request $_POST data.\n\t * @return WP_Error|string\n\t */\n\tpublic static function quiz_answer_question( $request ) {\n\n\t\t$err = new WP_Error();\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\t$err->add( 400, __( 'You must be logged in to take quizzes.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t$required = array( 'attempt_key', 'question_id', 'question_type' );\n\t\tforeach ( $required as $key ) {\n\t\t\tif ( ! isset( $request[ $key ] ) ) {\n\t\t\t\t$err->add( 400, __( 'Missing required parameters. Could not proceed.', 'lifterlms' ) );\n\t\t\t\treturn $err;\n\t\t\t}\n\t\t}\n\n\t\t$attempt_key = sanitize_text_field( $request['attempt_key'] );\n\t\t$question_id = absint( $request['question_id'] );\n\t\t$answer      = array_map( 'stripslashes_deep', isset( $request['answer'] ) ? $request['answer'] : array() );\n\n\t\t$student_quizzes = $student->quizzes();\n\t\t$attempt         = $student_quizzes->get_attempt_by_key( $attempt_key );\n\t\tif ( ! $attempt || 'incomplete' !== $attempt->get( 'status' ) || ( $attempt->get_quiz()->can_be_resumed() && ! $attempt->can_be_resumed() ) ) {\n\t\t\t$err->add( 500, __( 'There was an error recording your answer. Please return to the lesson and begin again.', 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t/**\n\t\t * Check limit not reached.\n\t\t *\n\t\t * First check whether the quiz is open (so to leverage the `llms_quiz_is_open` filter ),\n\t\t * if not, check also for remaining attempts.\n\t\t *\n\t\t * At this point the current attempt has already been counted (maybe the last allowed),\n\t\t * so we check that the remaining attempt is just greater than -1.\n\t\t */\n\t\t$quiz_id = $attempt->get( 'quiz_id' );\n\t\tif ( ! ( new LLMS_Quiz( $quiz_id ) )->is_open() &&\n\t\t\t\t$student_quizzes->get_attempts_remaining_for_quiz( $quiz_id, true ) < 0 ) {\n\t\t\t$err->add( 400, __( \"You've reached the maximum number of attempts for this quiz.\", 'lifterlms' ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t// record the answer.\n\t\t$attempt->answer_question( $question_id, $answer );\n\n\t\tif ( isset( $request['via_previous_question'] ) ) {\n\t\t\t$attempt->set( 'current_question_id', $attempt->get_previous_question( $question_id ) );\n\t\t\t$attempt->save();\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isset( $request['via_exit_quiz'] ) ) {\n\t\t\t$attempt->set( 'current_question_id', $question_id );\n\t\t\t$attempt->save();\n\n\t\t\treturn;\n\t\t}\n\n\t\t// get the next question.\n\t\t$question_id = $attempt->get_next_question( $question_id );\n\n\t\t// return html for the next question.\n\t\tif ( $question_id ) {\n\t\t\t$attempt->set( 'current_question_id', absint( $question_id ) );\n\t\t\t$attempt->save();\n\n\t\t\t$html = llms_get_template_ajax(\n\t\t\t\t'content-single-question.php',\n\t\t\t\tarray(\n\t\t\t\t\t'attempt'  => $attempt,\n\t\t\t\t\t'question' => llms_get_post( $question_id ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\treturn array(\n\t\t\t\t'html'        => $html,\n\t\t\t\t'question_id' => $question_id,\n\t\t\t);\n\n\t\t} else {\n\n\t\t\treturn self::quiz_end( $request, $attempt );\n\n\t\t}\n\t}\n\n\t/**\n\t * End a quiz attempt.\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param array                  $request $_POST data.\n\t * @param LLMS_Quiz_Attempt|null $attempt The quiz attempt.\n\t * @return array\n\t */\n\tpublic static function quiz_end( $request, $attempt = null ) {\n\n\t\t$err = new WP_Error();\n\n\t\tif ( ! $attempt ) {\n\n\t\t\t$student = llms_get_student();\n\t\t\tif ( ! $student ) {\n\t\t\t\t$err->add( 400, __( 'You must be logged in to take quizzes.', 'lifterlms' ) );\n\t\t\t\treturn $err;\n\t\t\t}\n\n\t\t\tif ( ! isset( $request['attempt_key'] ) ) {\n\t\t\t\t$err->add( 400, __( 'Missing required parameters. Could not proceed.', 'lifterlms' ) );\n\t\t\t\treturn $err;\n\t\t\t}\n\n\t\t\t$attempt = $student->quizzes()->get_attempt_by_key( sanitize_text_field( $request['attempt_key'] ) );\n\n\t\t\tif ( ! $attempt ) {\n\t\t\t\t$err->add( 404, __( 'The requested attempt could not be found.', 'lifterlms' ) );\n\t\t\t\treturn $err;\n\t\t\t}\n\t\t}\n\n\t\t// Record the attempt's completion.\n\t\t$attempt->end();\n\n\t\t// Setup a redirect.\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'attempt_key' => $attempt->get_key(),\n\t\t\t),\n\t\t\tget_permalink( $attempt->get( 'quiz_id' ) )\n\t\t);\n\n\t\treturn array(\n\t\t\t/**\n\t\t\t * Filter the quiz redirect URL on completion.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param string            $url     The quiz redirect URL on completion.\n\t\t\t * @param LLMS_Quiz_Attempt $attempt The quiz attempt.\n\t\t\t */\n\t\t\t'redirect' => apply_filters( 'llms_quiz_complete_redirect', $url, $attempt ),\n\t\t);\n\t}\n\n\t/**\n\t * Remove a coupon from an order during checkout\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $request $_POST data.\n\t * @return array\n\t */\n\tpublic static function remove_coupon_code( $request ) {\n\n\t\tllms()->session->set( 'llms_coupon', false );\n\n\t\t$plan = new LLMS_Access_Plan( $request['plan_id'] );\n\n\t\tob_start();\n\t\tllms_get_template( 'checkout/form-coupon.php' );\n\t\t$coupon_html = ob_get_clean();\n\n\t\tob_start();\n\t\tllms_get_template(\n\t\t\t'checkout/form-gateways.php',\n\t\t\tarray(\n\t\t\t\t'coupon'           => false,\n\t\t\t\t'gateways'         => llms()->payment_gateways()->get_enabled_payment_gateways(),\n\t\t\t\t'selected_gateway' => llms()->payment_gateways()->get_default_gateway(),\n\t\t\t\t'plan'             => $plan,\n\t\t\t)\n\t\t);\n\t\t$gateways_html = ob_get_clean();\n\n\t\tob_start();\n\t\tllms_get_template(\n\t\t\t'checkout/form-summary.php',\n\t\t\tarray(\n\t\t\t\t'coupon'  => false,\n\t\t\t\t'plan'    => $plan,\n\t\t\t\t'product' => $plan->get_product(),\n\t\t\t)\n\t\t);\n\t\t$summary_html = ob_get_clean();\n\n\t\treturn array(\n\t\t\t'coupon_html'   => $coupon_html,\n\t\t\t'gateways_html' => $gateways_html,\n\t\t\t'summary_html'  => $summary_html,\n\t\t);\n\t}\n\n\t/**\n\t * Handle Select2 Search boxes for WordPress Posts by Post Type and Post Status.\n\t *\n\t * @since 3.0.0\n\t * @since 3.32.0 Updated to use llms_filter_input().\n\t * @since 3.32.0 Posts can be queried by post status(es) via the `$_POST['post_statuses']`.\n\t *               By default only the published posts will be queried.\n\t * @since 3.37.2 Posts can be 'filtered' by instructor via the `$_POST['instructor_id']`.\n\t * @since 5.5.0 Do not encode quotes when sanitizing search term.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic static function select2_query_posts() {\n\n\t\tglobal $wpdb;\n\n\t\tif ( ! is_user_logged_in() ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t// Grab the search term if it exists.\n\t\t$term = llms_filter_input_sanitize_string( INPUT_POST, 'term', array( FILTER_FLAG_NO_ENCODE_QUOTES ) );\n\n\t\t// Get the page.\n\t\t$page = llms_filter_input( INPUT_POST, 'page', FILTER_SANITIZE_NUMBER_INT );\n\n\t\t// Get post type(s).\n\t\t$post_type        = sanitize_text_field( llms_filter_input_sanitize_string( INPUT_POST, 'post_type' ) );\n\t\t$post_types_array = explode( ',', $post_type );\n\t\tforeach ( $post_types_array as &$str ) {\n\t\t\t$str = \"'\" . esc_sql( trim( $str ) ) . \"'\";\n\t\t}\n\t\t$post_types = implode( ',', $post_types_array );\n\n\t\t// Get post status(es).\n\t\t$post_statuses       = llms_filter_input_sanitize_string( INPUT_POST, 'post_statuses' );\n\t\t$post_statuses       = empty( $post_statuses ) ? 'publish' : $post_statuses;\n\t\t$post_statuses_array = explode( ',', $post_statuses );\n\t\tforeach ( $post_statuses_array as &$str ) {\n\t\t\t$str = \"'\" . esc_sql( trim( $str ) ) . \"'\";\n\t\t}\n\t\t$post_statuses = implode( ',', $post_statuses_array );\n\n\t\t// Filter posts (llms posts) by instructor ID.\n\t\t$instructor_id = llms_filter_input( INPUT_POST, 'instructor_id', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( ! empty( $instructor_id ) ) {\n\t\t\t$serialized_iid = serialize(\n\t\t\t\tarray(\n\t\t\t\t\t'id' => absint( $instructor_id ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$serialized_iid = str_replace( array( 'a:1:{', '}' ), '', $serialized_iid );\n\n\t\t\t$join = $wpdb->prepare(\n\t\t\t\t\" JOIN $wpdb->postmeta AS m ON p.ID = m.post_id AND m.meta_key = '_llms_instructors' AND m.meta_value LIKE %s\",\n\t\t\t\t'%' . $wpdb->esc_like( $serialized_iid ) . '%'\n\t\t\t);\n\t\t} else {\n\t\t\t$join = '';\n\t\t}\n\n\t\t$limit = 30;\n\t\t$start = $limit * $page;\n\n\t\tif ( $term ) {\n\t\t\t$like = \" AND post_title LIKE '%s'\";\n\t\t\t$vars = array( '%' . $term . '%', $start, $limit );\n\t\t} else {\n\t\t\t$like = '';\n\t\t\t$vars = array( $start, $limit );\n\t\t}\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$posts = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT p.ID as ID, p.post_title as post_title, p.post_type as post_type\n\t\t\t FROM $wpdb->posts as p\n\t\t\t $join\n\t\t\t WHERE p.post_type IN ( $post_types )\n\t\t\t   AND p.post_status IN ( $post_statuses )\n\t\t\t       $like\n\t\t\t ORDER BY post_title\n\t\t\t LIMIT %d, %d\n\t\t\t\",\n\t\t\t\t$vars\n\t\t\t) // phpcs:ignore -- The number of params is correct, $vars is an array of two elements.\n\t\t);// no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t$items = array();\n\n\t\t$grouping = ( count( $post_types_array ) > 1 );\n\n\t\tforeach ( $posts as $post ) {\n\n\t\t\tif ( ! current_user_can( 'read_post', $post->ID ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$item = array(\n\t\t\t\t'id'   => $post->ID,\n\t\t\t\t'name' => $post->post_title . ' (' . __( 'ID#', 'lifterlms' ) . ' ' . $post->ID . ')',\n\t\t\t);\n\n\t\t\tif ( $grouping ) {\n\n\t\t\t\t// Setup an object for the optgroup if it's not already set up.\n\t\t\t\tif ( ! isset( $items[ $post->post_type ] ) ) {\n\t\t\t\t\t$obj                       = get_post_type_object( $post->post_type );\n\t\t\t\t\t$items[ $post->post_type ] = array(\n\t\t\t\t\t\t'label' => $obj->labels->name,\n\t\t\t\t\t\t'items' => array(),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t$items[ $post->post_type ]['items'][] = $item;\n\n\t\t\t} else {\n\n\t\t\t\t$items[] = $item;\n\n\t\t\t}\n\t\t}\n\n\t\techo json_encode(\n\t\t\tarray(\n\t\t\t\t'items'   => $items,\n\t\t\t\t'more'    => count( $items ) === $limit,\n\t\t\t\t'success' => true,\n\t\t\t)\n\t\t);\n\t\twp_die();\n\t}\n\n\t/**\n\t * Add or remove a student from a course or membership.\n\t *\n\t * @since 3.0.0\n\t * @since 3.33.0 Handle the delete enrollment request and make sure the $request['post_id'] is not empty.\n\t *               Also always return either a WP_Error on failure or a \"success\" array on action performed.\n\t * @since 3.37.14 Use strict comparison.\n\t *\n\t * @param array $request $_POST data.\n\t * @return (WP_Error|array)\n\t */\n\tpublic static function update_student_enrollment( $request ) {\n\n\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( empty( $request['student_id'] ) || empty( $request['status'] ) || empty( $request['post_id'] ) ) {\n\t\t\treturn new WP_Error( 400, __( 'Missing required parameters', 'lifterlms' ) );\n\t\t}\n\n\t\tif ( ! in_array( $request['status'], array( 'add', 'remove', 'delete' ), true ) ) {\n\t\t\treturn new WP_Error( 400, __( 'Invalid status', 'lifterlms' ) );\n\t\t}\n\n\t\t$student_id = intval( $request['student_id'] );\n\t\t$post_id    = intval( $request['post_id'] );\n\n\t\tswitch ( $request['status'] ) {\n\t\t\tcase 'add':\n\t\t\t\t$res = llms_enroll_student( $student_id, $post_id, 'admin_' . get_current_user_id() );\n\t\t\t\tbreak;\n\n\t\t\tcase 'remove':\n\t\t\t\t$res = llms_unenroll_student( $student_id, $post_id, 'cancelled', 'any' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'delete':\n\t\t\t\t$res = llms_delete_student_enrollment( $student_id, $post_id, 'any' );\n\t\t\t\tbreak;\n\t\t}\n\n\t\tif ( ! $res ) {\n\t\t\t// Translators: %s = action add|remove|delete.\n\t\t\treturn new WP_Error( 400, sprintf( __( 'Action \"%1$s\" failed. Please try again', 'lifterlms' ), $request['status'] ) );\n\t\t}\n\n\t\treturn array(\n\t\t\t'success' => true,\n\t\t);\n\t}\n\n\t/**\n\t * Validate a Coupon via the Checkout Form\n\t *\n\t * @since 3.0.0\n\t * @since 3.39.0 Minor changes to code for readability with no changes to function behavior.\n\t * @since 4.21.1 Sanitize user-submitted coupon code before outputting in error messages.\n\t *\n\t * @param array $request $_POST data.\n\t * @return array|WP_Error On success, returns an array containing HTML parts used to update the interface of the checkout screen.\n\t *                        On error, returns an error object with details of the encountered error.\n\t */\n\tpublic static function validate_coupon_code( $request ) {\n\n\t\t$error = new WP_Error();\n\n\t\t$request['code'] = ! empty( $request['code'] ) ? sanitize_text_field( $request['code'] ) : '';\n\n\t\tif ( empty( $request['code'] ) ) {\n\n\t\t\t$error->add( 'error', __( 'Please enter a coupon code.', 'lifterlms' ) );\n\n\t\t} elseif ( empty( $request['plan_id'] ) ) {\n\n\t\t\t$error->add( 'error', __( 'Please enter a plan ID.', 'lifterlms' ) );\n\n\t\t} else {\n\n\t\t\t$cid = llms_find_coupon( $request['code'] );\n\n\t\t\tif ( ! $cid ) {\n\n\t\t\t\t// Translators: %s = coupon code.\n\t\t\t\t$error->add( 'error', sprintf( __( 'Coupon code \"%s\" not found.', 'lifterlms' ), $request['code'] ) );\n\n\t\t\t} else {\n\n\t\t\t\t$coupon = new LLMS_Coupon( $cid );\n\t\t\t\t$valid  = $coupon->is_valid( $request['plan_id'] );\n\n\t\t\t\tif ( is_wp_error( $valid ) ) {\n\n\t\t\t\t\t$error = $valid;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tllms()->session->set(\n\t\t\t\t\t\t'llms_coupon',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'plan_id'   => $request['plan_id'],\n\t\t\t\t\t\t\t'coupon_id' => $coupon->get( 'id' ),\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\n\t\t\t\t\t$plan = new LLMS_Access_Plan( $request['plan_id'] );\n\n\t\t\t\t\tob_start();\n\t\t\t\t\tllms_get_template(\n\t\t\t\t\t\t'checkout/form-coupon.php',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'coupon' => $coupon,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$coupon_html = ob_get_clean();\n\n\t\t\t\t\tob_start();\n\t\t\t\t\tllms_get_template(\n\t\t\t\t\t\t'checkout/form-gateways.php',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'coupon'           => $coupon,\n\t\t\t\t\t\t\t'gateways'         => llms()->payment_gateways()->get_enabled_payment_gateways(),\n\t\t\t\t\t\t\t'selected_gateway' => llms()->payment_gateways()->get_default_gateway(),\n\t\t\t\t\t\t\t'plan'             => $plan,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$gateways_html = ob_get_clean();\n\n\t\t\t\t\tob_start();\n\t\t\t\t\tllms_get_template(\n\t\t\t\t\t\t'checkout/form-summary.php',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'coupon'  => $coupon,\n\t\t\t\t\t\t\t'plan'    => $plan,\n\t\t\t\t\t\t\t'product' => $plan->get_product(),\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t$summary_html = ob_get_clean();\n\n\t\t\t\t\treturn array(\n\t\t\t\t\t\t'code'          => $coupon->get( 'title' ),\n\t\t\t\t\t\t'coupon_html'   => $coupon_html,\n\t\t\t\t\t\t'gateways_html' => $gateways_html,\n\t\t\t\t\t\t'summary_html'  => $summary_html,\n\t\t\t\t\t);\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $error;\n\t}\n\n\t/**\n\t * \"API\" for the Admin Builder.\n\t *\n\t * @since 3.13.0\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @param array $request $_POST data.\n\t * @return array\n\t */\n\tpublic static function llms_builder( $request ) {\n\n\t\treturn LLMS_Admin_Builder::handle_ajax( $request );\n\t}\n\n\t/**\n\t * Save autoenroll courses list for a Membership\n\t *\n\t * @since 3.30.0\n\t *\n\t * @param array $request $_POST data.\n\t * @return null|true\n\t */\n\tpublic static function llms_save_membership_autoenroll_courses( $request ) {\n\n\t\t// Missing required fields.\n\t\tif ( empty( $request['post_id'] ) || ! isset( $request['courses'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( ! current_user_can( 'edit_membership', $request['post_id'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t// Not a membership.\n\t\t$membership = llms_get_post( $request['post_id'] );\n\t\tif ( ! $membership || ! is_a( $membership, 'LLMS_Membership' ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$courses = array_map( 'absint', (array) $request['courses'] );\n\t\t$membership->add_auto_enroll_courses( $courses, true );\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * AJAX handler for creating and updating access plans via the metabox on courses & memberships\n\t *\n\t * @since 3.29.0\n\t * @since 3.33.1 Use `wp_unslash()` before inserting access plan data.\n\t *\n\t * @param array $request $_POST data.\n\t * @return array\n\t */\n\tpublic static function llms_update_access_plans( $request ) {\n\n\t\tif ( empty( $request['plans'] ) || ! is_array( $request['plans'] ) || empty( $request['post_id'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\tif ( ! llms_current_user_can_edit_product( $request['post_id'] ) ) {\n\t\t\twp_die();\n\t\t}\n\n\t\t$metabox       = new LLMS_Meta_Box_Product();\n\t\t$post_id       = absint( $request['post_id'] );\n\t\t$metabox->post = get_post( $post_id );\n\n\t\t$errors = array();\n\n\t\tforeach ( $request['plans'] as $raw_plan_data ) {\n\n\t\t\tif ( empty( $raw_plan_data ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$raw_plan_data = wp_unslash( $raw_plan_data );\n\n\t\t\t// Ensure we can switch plans that used to be paid to free.\n\t\t\tif ( isset( $raw_plan_data['is_free'] ) && llms_parse_bool( $raw_plan_data['is_free'] ) && ! isset( $raw_plan_data['price'] ) ) {\n\t\t\t\t$raw_plan_data['price'] = 0;\n\t\t\t}\n\n\t\t\t// Ensure we can clear the memberships when they are cleared but \"Members only\" still selected.\n\t\t\tif ( isset( $raw_plan_data['availability'] ) && 'members' === $raw_plan_data['availability'] &&\n\t\t\t\t( ! isset( $raw_plan_data['availability_restrictions'] ) || ! $raw_plan_data['availability_restrictions'] ) ) {\n\t\t\t\t$raw_plan_data['availability_restrictions'] = array();\n\t\t\t\t$raw_plan_data['availability']              = 'open';\n\t\t\t}\n\n\t\t\t$raw_plan_data['product_id'] = $post_id;\n\n\t\t\t// retained filter for backwards compat.\n\t\t\t$raw_plan_data = apply_filters( 'llms_access_before_save_plan', $raw_plan_data, $metabox );\n\n\t\t\t$plan = llms_insert_access_plan( $raw_plan_data );\n\t\t\tif ( is_wp_error( $plan ) ) {\n\t\t\t\t$errors[ $raw_plan_data['menu_order'] ] = $plan;\n\t\t\t} else {\n\t\t\t\t// retained hook for backwards compat.\n\t\t\t\tdo_action( 'llms_access_plan_saved', $plan, $raw_plan_data, $metabox );\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\t'errors' => $errors,\n\t\t\t'html'   => $metabox->get_html(),\n\t\t);\n\t}\n\n\t/**\n\t * AJAX handler for persisting tracking events.\n\t *\n\t * @since 3.37.14\n\t *\n\t * @param array $request $_POST data.\n\t * @return array|WP_Error\n\t */\n\tpublic static function persist_tracking_events( $request ) {\n\n\t\tif ( empty( $request['llms-tracking'] ) ) {\n\t\t\treturn new WP_Error( 'error', __( 'Missing tracking data.', 'lifterlms' ) );\n\t\t}\n\n\t\t$success = llms()->events()->store_tracking_events( wp_unslash( $request['llms-tracking'] ) );\n\n\t\tif ( ! is_wp_error( $success ) ) {\n\t\t\t$success = array(\n\t\t\t\t'success' => true,\n\t\t\t);\n\t\t}\n\n\t\treturn $success;\n\t}\n}\n\nnew LLMS_AJAX_Handler();\n"
  },
  {
    "path": "includes/class.llms.ajax.php",
    "content": "<?php\n/**\n * AJAX Event Handler\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_AJAX\n *\n * @since 1.0.0\n * @since 3.35.0 Unknown.\n * @since 4.0.0 Removed previously deprecated ajax actions and related methods.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_AJAX::check_voucher_duplicate()` method.\n *              - `LLMS_AJAX::get_ajax_data()` method.\n *              - `LLMS_AJAX::register_script()` method.\n */\nclass LLMS_AJAX {\n\n\t/**\n\t * Nonce validation argument\n\t *\n\t * @var string\n\t */\n\tconst NONCE = 'llms-ajax';\n\n\t/**\n\t * Hook into ajax events.\n\t *\n\t * @since 1.0.0\n\t * @since 3.16.0 Unknown.\n\t * @since 4.0.0 Stop registering previously deprecated actions.\n\t * @since 5.9.0 Move `check_voucher_duplicate()` to `LLMS_AJAX_Handler`.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t * @since 7.5.0 Added `favorite_object` ajax event.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$ajax_events = array(\n\t\t\t'query_quiz_questions' => false,\n\t\t\t'favorite_object'      => false,\n\t\t);\n\n\t\tforeach ( $ajax_events as $ajax_event => $nopriv ) {\n\t\t\tadd_action( 'wp_ajax_' . $ajax_event, array( $this, $ajax_event ) );\n\n\t\t\tif ( $nopriv ) {\n\t\t\t\tadd_action( 'wp_ajax_nopriv_' . $ajax_event, array( $this, $ajax_event ) );\n\t\t\t}\n\t\t}\n\n\t\tself::register();\n\n\t\tadd_filter( 'heartbeat_received', array( 'LLMS_Admin_Builder', 'heartbeat_received' ), 10, 2 );\n\t}\n\n\t/**\n\t * Register the AJAX handler class with all the appropriate WordPress hooks.\n\t *\n\t * @since Unknown\n\t * @since 4.4.0 Move `register_script()` to script enqueue hook in favor of `wp_loaded`.\n\t * @since 6.0.0 Removed the `wp_enqueue_scripts` action callback to the deprecated `LLMS_AJAX::register_script()` method.\n\t *\n\t * @return void\n\t */\n\tpublic function register() {\n\n\t\t$handler = 'LLMS_AJAX';\n\t\t$methods = get_class_methods( 'LLMS_AJAX_Handler' );\n\n\t\tforeach ( $methods as $method ) {\n\t\t\tadd_action( 'wp_ajax_' . $method, array( $handler, 'handle' ) );\n\t\t\tadd_action( 'wp_ajax_nopriv_' . $method, array( $handler, 'handle' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Handles the AJAX request for my plugin.\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic static function handle() {\n\n\t\t// Make sure we are getting a valid AJAX request.\n\t\tcheck_ajax_referer( self::NONCE );\n\n\t\t$request = $_REQUEST;\n\n\t\t$response = call_user_func( 'LLMS_AJAX_Handler::' . $request['action'], $request );\n\n\t\tif ( $response instanceof WP_Error ) {\n\t\t\tself::send_error( $response );\n\t\t}\n\n\t\twp_send_json_success( $response );\n\n\t\tdie();\n\n\t}\n\n\tpublic static function scrub_request( $request ) {\n\n\t\tforeach ( $request as $key => $value ) {\n\n\t\t\tif ( is_array( $value ) ) {\n\t\t\t\t$request[ $key ] = self::scrub_request( $value );\n\t\t\t} else {\n\t\t\t\t$request[ $key ] = llms_clean( $value );\n\t\t\t}\n\t\t}\n\n\t\treturn $request;\n\n\t}\n\n\t/**\n\t * Sends a JSON response with the details of the given error.\n\t *\n\t * @param WP_Error $error\n\t */\n\tprivate static function send_error( $error ) {\n\t\twp_send_json(\n\t\t\tarray(\n\t\t\t\t'code'    => $error->get_error_code(),\n\t\t\t\t'message' => $error->get_error_message(),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve Quiz Questions\n\t *\n\t * Used by Select2 AJAX functions to load paginated quiz questions\n\t * Also allows querying by question title\n\t *\n\t * @since Unknown\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function query_quiz_questions() {\n\n\t\t// Grab the search term if it exists.\n\t\t$term = array_key_exists( 'term', $_REQUEST ) ? llms_filter_input_sanitize_string( INPUT_POST, 'term' ) : '';\n\t\t$page = array_key_exists( 'page', $_REQUEST ) ? llms_filter_input( INPUT_POST, 'page', FILTER_SANITIZE_NUMBER_INT ) : 0;\n\n\t\tglobal $wpdb;\n\n\t\t$limit = 30;\n\t\t$start = $limit * $page;\n\n\t\tif ( $term ) {\n\t\t\t$like = \" AND post_title LIKE '%s'\";\n\t\t\t$vars = array( '%' . $term . '%', $start, $limit );\n\t\t} else {\n\t\t\t$like = '';\n\t\t\t$vars = array( $start, $limit );\n\t\t}\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t$questions = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT ID, post_title\n\t\t\t FROM $wpdb->posts\n\t\t\t WHERE\n\t\t\t \t    post_type = 'llms_question'\n\t\t\t \tAND post_status = 'publish'\n\t\t\t \t$like\n\t\t\t ORDER BY post_title\n\t\t\t LIMIT %d, %d\n\t\t\t\",\n\t\t\t\t$vars\n\t\t\t)\n\t\t);\n\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t$r = array();\n\t\tforeach ( $questions as $q ) {\n\n\t\t\t$r[] = array(\n\t\t\t\t'id'   => $q->ID,\n\t\t\t\t'name' => $q->post_title . ' (' . $q->ID . ')',\n\t\t\t);\n\n\t\t}\n\n\t\techo json_encode(\n\t\t\tarray(\n\t\t\t\t'items'   => $r,\n\t\t\t\t'more'    => count( $r ) === $limit,\n\t\t\t\t'success' => true,\n\t\t\t)\n\t\t);\n\n\t\twp_die();\n\n\t}\n\n\t/**\n\t * Add Favorite / Unfavorite postmeta for an object.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function favorite_object() {\n\n\t\t// Grab the data if it exists.\n\t\t$user_action = llms_filter_input_sanitize_string( INPUT_POST, 'user_action' );\n\t\t$object_id   = llms_filter_input( INPUT_POST, 'object_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t$object_type = llms_filter_input_sanitize_string( INPUT_POST, 'object_type' );\n\t\t$user_id     = get_current_user_id();\n\t\t$student     = llms_get_student( $user_id );\n\t\tif ( is_null( $object_id ) || ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( 'favorite' === $user_action ) {\n\t\t\t// You can never mark favorite a non-free lesson of a course you're not enrolled into.\n\t\t\tif ( 'lesson' === $object_type ) {\n\t\t\t\t$lesson            = llms_get_post( $object_id );\n\t\t\t\t$can_mark_favorite = $lesson && ( $student->is_enrolled( $object_id ) || $lesson->is_free() );\n\t\t\t\tif ( ! $can_mark_favorite ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tllms_mark_favorite( $user_id, $object_id, $object_type );\n\t\t} elseif ( 'unfavorite' === $user_action ) {\n\t\t\tllms_mark_unfavorite( $user_id, $object_id, $object_type );\n\t\t}\n\n\t\techo wp_json_encode(\n\t\t\tarray(\n\t\t\t\t'total_favorites' => llms_get_object_total_favorites( $object_id ),\n\t\t\t\t'success'         => true,\n\t\t\t)\n\t\t);\n\n\t\twp_die();\n\n\t}\n\n}\n\nnew LLMS_AJAX();\n"
  },
  {
    "path": "includes/class.llms.background.updater.php",
    "content": "<?php\n/**\n * LLMS_Background_Updater\n *\n * @package LifterLMS/Classes\n *\n * @since 3.4.3\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Background database upgrader\n *\n * Process db updates in the background\n *\n * Replaces abstract updater and update classes from 3.4.2 and lower.\n *\n * @since 3.4.3\n */\nclass LLMS_Background_Updater extends WP_Background_Process {\n\n\t/**\n\t * Action name\n\t *\n\t * @var string\n\t */\n\tprotected $action = 'llms_bg_updater';\n\n\t/**\n\t * Enables event logging\n\t *\n\t * @var boolean\n\t */\n\tprivate $enable_logging = true;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.4.3\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tparent::__construct();\n\n\t\tif ( ! defined( 'LLMS_BG_UPDATE_LOG' ) ) {\n\t\t\tdefine( 'LLMS_BG_UPDATE_LOG', true );\n\t\t}\n\n\t\t$this->enable_logging = ( defined( 'LLMS_BG_UPDATE_LOG' ) && LLMS_BG_UPDATE_LOG );\n\n\t}\n\n\t/**\n\t * Called when queue is emptied and action is complete\n\t *\n\t * @since 3.4.3\n\t *\n\t * @return void\n\t */\n\tprotected function complete() {\n\t\t$this->log( 'Update complete' );\n\t\tLLMS_Install::update_db_version();\n\t\tparent::complete();\n\t}\n\n\t/**\n\t * Starts the queue\n\t *\n\t * @since 3.4.3\n\t *\n\t * @return void\n\t */\n\tpublic function dispatch() {\n\n\t\t$dispatched = parent::dispatch();\n\n\t\tif ( is_wp_error( $dispatched ) ) {\n\t\t\t$this->log( sprintf( 'Unable to dispatch updater: %s' ), $dispatched->get_error_message() );\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieve approximate progress of updates in the queue\n\t *\n\t * @since 3.4.3\n\t * @since 3.16.10 Unknown.\n\t *\n\t * @return int\n\t */\n\tpublic function get_progress() {\n\n\t\t// If the queue is empty we've already finished.\n\t\tif ( $this->is_queue_empty() ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t// Get the progress.\n\t\t$batch     = $this->get_batch();\n\t\t$total     = max( array_keys( $batch->data ) ) + 1;\n\t\t$remaining = count( $batch->data );\n\t\tif ( ! $total ) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn ceil( ( ( $total - $remaining ) / $total ) * 100 );\n\t}\n\n\t/**\n\t * Handle cron healthcheck\n\t *\n\t * Restart the background process if not already running\n\t * and data exists in the queue.\n\t *\n\t * Overridden to enable the \"force\" option to work, replaces \"exit\" with \"return\"\n\t * so that we can redirect and manually call the cronjob\n\t *\n\t * @since 3.4.3\n\t *\n\t * @return void\n\t */\n\tpublic function handle_cron_healthcheck() {\n\n\t\t// Background process already running.\n\t\tif ( $this->is_process_running() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// No data to process.\n\t\tif ( $this->is_queue_empty() ) {\n\t\t\t$this->clear_scheduled_event();\n\t\t\treturn;\n\t\t}\n\n\t\t$this->handle();\n\n\t}\n\n\t/**\n\t * Returns true if the updater is running\n\t *\n\t * @since 3.4.3\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_updating() {\n\t\treturn ( false === $this->is_queue_empty() );\n\t}\n\n\t/**\n\t * Log event data to an update file when logging enabled\n\t *\n\t * @since 3.4.3\n\t *\n\t * @param mixed $data Data to log.\n\t * @return void\n\t */\n\tpublic function log( $data ) {\n\n\t\tif ( $this->enable_logging ) {\n\t\t\tllms_log( $data, 'updater' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Processes an item in the queue\n\t *\n\t * @since 3.4.3\n\t * @since 3.16.10 Unknown.\n\t * @since 5.2.0 Use `llms_get_callable_name()` to log callback.\n\t *\n\t * @param mixed $callback PHP callable (function name, callable array, etc...).\n\t * @return mixed Returns `false` when the callback is complete (removes it from the queue).\n\t *               Returns $callback to leave it in the queue.\n\t */\n\tprotected function task( $callback ) {\n\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/llms.functions.updates.php';\n\n\t\t$callback_name = llms_get_callable_name( $callback );\n\t\tif ( is_callable( $callback ) ) {\n\t\t\t$this->log( sprintf( 'Running %s callback', $callback_name ) );\n\t\t\tif ( call_user_func( $callback ) ) {\n\t\t\t\treturn $callback;\n\t\t\t}\n\t\t\t$this->log( sprintf( 'Finished %s callback', $callback_name ) );\n\t\t} else {\n\t\t\t$this->log( sprintf( 'Could not find %s callback', $callback_name ) );\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Save queue\n\t *\n\t * Overwrites parent method to empty `$this->data` following a save.\n\t *\n\t * This ensures save() can be called multiple times without recording duplicates.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return LLMS_Background_Updater\n\t */\n\tpublic function save() {\n\n\t\tparent::save();\n\t\t// Reset data to avoid duplicates if save() is called more than once.\n\t\t$this->data = array();\n\n\t\treturn $this;\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.cache.helper.php",
    "content": "<?php\n/**\n * LifterLMS Caching Helper\n *\n * @package LifterLMS/Classes\n *\n * @since 3.15.0\n * @version 6.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Cache_Helper\n *\n * @since 3.15.0\n * @since 4.0.0 Add WP_Object_Cache API helper.\n */\nclass LLMS_Cache_Helper {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'wp', array( $this, 'maybe_no_cache' ) );\n\n\t}\n\n\t/**\n\t * Sets a browser cookie that tells WP Engine to exclude a page from server caching.\n\t *\n\t * @see https://wpengine.com/support/cache/#Default_Cache_Exclusions\n\t * @see https://wpengine.com/support/determining-wp-engine-environment/\n\t *\n\t * @since 6.6.0\n\t *\n\t * @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`.\n\t *\n\t * @return void\n\t */\n\tprivate function exclude_page_from_wpe_server_cache( $post = null ) {\n\n\t\tif ( function_exists( 'is_wpe' ) && is_wpe() ) {\n\t\t\t/*\n\t\t\t * If \"Settings -> Permalinks\" is \"Plain\", i.e. the `permalink_structure` option is '',\n\t\t\t * allow the entire site to be cached by WP Engine.\n\t\t\t * Note: This will prevent users from being able to successfully use the \"Lost your password?\" feature.\n\t\t\t */\n\t\t\tif ( isset( $GLOBALS['wp_rewrite'] ) && ! $GLOBALS['wp_rewrite']->using_permalinks() ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$path = wp_parse_url( get_permalink( $post ), PHP_URL_PATH );\n\t\t\tllms_setcookie( 'wordpress_wpe_no_cache', '1', 0, $path, COOKIE_DOMAIN, is_ssl(), true );\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve a cache prefix that can be used with WP_Object_Cache methods\n\t *\n\t * Using a cache prefix allows simple invalidation of all items with the same\n\t * prefix simply by updating the prefix.\n\t *\n\t * The \"prefix\" is microtime(), if we wish to invalidate all items in the prefix group\n\t * we call the method again with `$invalidate=true` which updates the prefix to the current\n\t * microtime(), thereby invalidating the entire cache group.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @link https://core.trac.wordpress.org/ticket/4476#comment:10\n\t *\n\t * @param string $group Cache group name.\n\t * @return string\n\t */\n\tpublic static function get_prefix( $group ) {\n\n\t\t$key    = sprintf( 'llms_%s_cache_prefix', $group );\n\t\t$prefix = wp_cache_get( $key, $group );\n\n\t\tif ( false === $prefix ) {\n\t\t\t$prefix = microtime();\n\t\t\twp_cache_set( $key, $prefix, $group );\n\t\t}\n\n\t\treturn sprintf( 'llms_cache_%s_', $prefix );\n\n\t}\n\n\t/**\n\t * Invalidate a cache group prefix.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @link https://core.trac.wordpress.org/ticket/4476#comment:10\n\t *\n\t * @param string $group Cache group name.\n\t * @return void\n\t */\n\tpublic static function invalidate_group( $group ) {\n\t\twp_cache_set( sprintf( 'llms_%s_cache_prefix', $group ), microtime(), $group );\n\t}\n\n\t/**\n\t * Define nocache constants and set nocache headers on specified pages\n\t *\n\t * This prevents caching for the Checkout & Student Dashboard pages.\n\t *\n\t * @since 3.15.0\n\t * @since 6.4.0 Force no caching on quiz pages.\n\t *              Added 'no-store' to the default WordPress nocache headers.\n\t * @since 6.6.0 Added WP Engine server-side cache exclusions.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_no_cache() {\n\n\t\tif ( ! is_blog_installed() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Filter the list of pages that LifterLMS will send nocache headers for.\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @param int[] $ids List of WP_Post IDs.\n\t\t */\n\t\t$ids = apply_filters(\n\t\t\t'llms_no_cache_page_ids',\n\t\t\tarray(\n\t\t\t\tllms_get_page_id( 'checkout' ),\n\t\t\t\tllms_get_page_id( 'myaccount' ),\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Filter whether or not LifterLMS will send nocache headers.\n\t\t *\n\t\t * @since 6.4.0\n\t\t *\n\t\t * @param bool $no_cache Whether or not LifterLMS will send nocache headers.\n\t\t */\n\t\t$do_not_cache = apply_filters( 'llms_no_cache', is_page( $ids ) || is_quiz() );\n\n\t\tif ( $do_not_cache ) {\n\n\t\t\tadd_filter( 'nocache_headers', array( __CLASS__, 'additional_nocache_headers' ), 99 );\n\n\t\t\tllms_maybe_define_constant( 'DONOTCACHEPAGE', true );\n\t\t\tllms_maybe_define_constant( 'DONOTCACHEOBJECT', true );\n\t\t\tllms_maybe_define_constant( 'DONOTCACHEDB', true );\n\t\t\tnocache_headers();\n\t\t\t$this->exclude_page_from_wpe_server_cache();\n\n\t\t\tremove_filter( 'nocache_headers', array( __CLASS__, 'additional_nocache_headers' ), 99 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Set additional nocache headers.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @see wp_get_nocache_headers()\n\t *\n\t * @param array $headers {\n\t *     Header names and field values.\n\t *\n\t *     @type string $Expires       Expires header.\n\t *     @type string $Cache-Control Cache-Control header.\n\t * }\n\t * @return array\n\t */\n\tpublic static function additional_nocache_headers( $headers ) {\n\n\t\t// First tree are the default ones.\n\t\t$nocache_headers_cache_control = array(\n\t\t\t'no-cache',\n\t\t\t'must-revalidate',\n\t\t\t'max-age=0',\n\t\t\t'no-store',\n\t\t);\n\n\t\tif ( ! empty( $headers['Cache-Control'] ) ) {\n\t\t\t$original_headers_cache_control = array_map( 'trim', explode( ',', $headers['Cache-Control'] ) );\n\t\t\t// Merge original headers with our nocache headers.\n\t\t\t$nocache_headers_cache_control = array_merge( $nocache_headers_cache_control, $original_headers_cache_control );\n\t\t\t// Avoid duplicates.\n\t\t\t$nocache_headers_cache_control = array_unique( $nocache_headers_cache_control );\n\t\t}\n\n\t\t$headers['Cache-Control'] = implode( ', ', $nocache_headers_cache_control );\n\n\t\treturn $headers;\n\n\t}\n\n}\n\nreturn new LLMS_Cache_Helper();\n"
  },
  {
    "path": "includes/class.llms.certificate.php",
    "content": "<?php\n/**\n * Base Certificate Class\n *\n * Handles generating certificates.\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Base Certificate Class\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 4.0.0 Remove previously deprecated class property `$enabled`.\n * @deprecated 6.0.0 Class `LLMS_Certificate` is deprecated with no direct replacement.\n */\nclass LLMS_Certificate {\n\n\t/**\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $certificate_template_id;\n\n\t/**\n\t * post title\n\t *\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $certificate_title;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $content;\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $email_type;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tpublic $find = array();\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $id;\n\n\t/**\n\t * image id\n\t *\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $image;\n\n\t/**\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $lesson_id;\n\n\t/**\n\t * @var WP_User\n\t * @since 1.0.0\n\t */\n\tpublic $object;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tpublic $replace = array();\n\n\t/**\n\t * @var bool\n\t * @since 1.0.0\n\t */\n\tpublic $sending;\n\n\t/**\n\t * post title\n\t *\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tpublic $title;\n\n\t/**\n\t * @var int\n\t * @since 1.0.0\n\t */\n\tpublic $userid;\n\n\t/**\n\t * Alert when deprecated methods are used.\n\t *\n\t * This class as well as core classes extending it have been deprecated. All public and protected methods\n\t * have been changed to private and will be made accessible through this magic method which also emits a\n\t * deprecation warning.\n\t *\n\t * This public method has been intentionally marked as private to denote it's temporary lifespan. It will be\n\t * removed alongside this class in the next major release.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @access private\n\t *\n\t * @param string $name Name of the method being called.\n\t * @param array  $args Arguments provided to the method.\n\t * @return void\n\t */\n\tpublic function __call( $name, $args ) {\n\t\t_deprecated_function( __CLASS__ . '::' . esc_html( $name ), '6.0.0' );\n\t\tif ( method_exists( $this, $name ) ) {\n\t\t\t$this->$name( ...$args );\n\t\t}\n\t}\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since Unknown.\n\t * @deprecated 6.0.0 `LLMS_Certificate::__construct()` is deprecated with no replacement.\n\t */\n\tpublic function __construct() {\n\n\t\t// Settings TODO Refactor: theses can come from the email post now.\n\t\t$this->email_type = 'html';\n\n\t\t$this->find    = array( '{blogname}', '{site_title}' );\n\t\t$this->replace = array( $this->get_blogname(), $this->get_blogname() );\n\t}\n\n\t/**\n\t * Is Enabled\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Certificate::is_enabled()` is deprecated with no replacement.\n\t *\n\t * @return boolean\n\t */\n\tprivate function is_enabled() {\n\t\treturn true;\n\t}\n\n\t/**\n\t * Get Blog Name\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Certificate::get_blogname()` is deprecated with no replacement.\n\t *\n\t * @return string [blog name]\n\t */\n\tprivate function get_blogname() {\n\t\treturn wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );\n\t}\n\n\t/**\n\t * Format String\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Certificate::format_string()` is deprecated with no replacement.\n\t *\n\t * @param  string $string [Find and replace merge fields]\n\t * @return string [formatted string]\n\t */\n\tprivate function format_string( $string ) {\n\t\treturn str_replace( $this->find, $this->replace, $string );\n\t}\n\n\t/**\n\t * Get Blog Title\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Certificate::get_title()` is deprecated with no replacement.\n\t *\n\t * @return string [Blog title]\n\t */\n\tprivate function get_title() {\n\t\treturn apply_filters( '_llms_certificate_title' . $this->id, $this->title, $this->object );\n\t}\n\n\t/**\n\t * Get Content\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Certificate::get_content()` is deprecated with no replacement.\n\t *\n\t * @return string [Post Content]\n\t */\n\tprivate function get_content() {\n\n\t\t$this->sending = true;\n\n\t\t$email_content = $this->get_content_html();\n\n\t\treturn $email_content;\n\t}\n\n\t/**\n\t * Get Content HTML\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Certificate::get_content_html()` is deprecated with no replacement.\n\t *\n\t * @return void\n\t */\n\tprivate function get_content_html() {}\n\n\t/**\n\t * Create Certificate\n\t *\n\t * @since 1.0.0\n\t * @since 3.8.0 Unknown.\n\t * @deprecated 6.0.0 `LLMS_Certificate::get_title()` is deprecated with no replacement.\n\t *\n\t * @param string $content HTML formatted post content.\n\t * @return void\n\t */\n\tprivate function create( $content ) {\n\t\tglobal $wpdb;\n\n\t\t$new_user_certificate = apply_filters(\n\t\t\t'lifterlms_new_page',\n\t\t\tarray(\n\t\t\t\t'post_type'    => 'llms_my_certificate',\n\t\t\t\t'post_title'   => $this->title,\n\t\t\t\t'post_content' => $content,\n\t\t\t\t'post_status'  => 'publish',\n\t\t\t\t'post_author'  => 1,\n\t\t\t)\n\t\t);\n\n\t\t$new_user_certificate_id = wp_insert_post( $new_user_certificate, true );\n\n\t\tupdate_post_meta( $new_user_certificate_id, '_llms_certificate_title', $this->certificate_title );\n\t\tupdate_post_meta( $new_user_certificate_id, '_llms_certificate_image', $this->image );\n\t\tupdate_post_meta( $new_user_certificate_id, '_llms_certificate_template', $this->certificate_template_id );\n\n\t\t$user_metadatas = array(\n\t\t\t'_certificate_earned' => $new_user_certificate_id,\n\t\t);\n\n\t\tforeach ( $user_metadatas as $key => $value ) {\n\t\t\t$update_user_postmeta = $wpdb->insert(\n\t\t\t\t$wpdb->prefix . 'lifterlms_user_postmeta',\n\t\t\t\tarray(\n\t\t\t\t\t'user_id'      => $this->userid,\n\t\t\t\t\t'post_id'      => $this->lesson_id,\n\t\t\t\t\t'meta_key'     => $key,\n\t\t\t\t\t'meta_value'   => $value,\n\t\t\t\t\t'updated_date' => current_time( 'mysql' ),\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// This hook is documented in includes/class-llms-engagement-handler.php.\n\t\tdo_action( 'llms_user_earned_certificate', $this->userid, $new_user_certificate_id, $this->lesson_id );\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.certificates.php",
    "content": "<?php\n/**\n * LLMS_Certificates class file\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Main LifterLMS Certificates \"factory\"\n *\n * Handles certificate generation and exports.\n *\n * @see llms()->certificates()\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 3.37.3 Refactored `get_export_html()` method.\n *               Added an action `llms_certificate_generate_export` to allow modification of certificate exports before being stored on the server.\n * @since 3.38.1 Use `LLMS_Mime_Type_Extractor::from_file_path()` when retrieving the certificate's images mime types during html export.\n * @since 4.3.1 When generating the certificate to export, if `$this->scrape_certificate()` generates a WP_Error early, return it to avoid fatal errors.\n * @since 4.21.0 Added new class properties: `$export_local_hosts`, `$export_blocked_stylesheet_hosts`, and `$export_blocked_image_hosts`.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Changes:\n *              - Deprecated the `LLMS_Certificates::trigger_engagement()` method.\n *                Use the {@see LLMS_Engagement_Handler::handle_certificate()} method instead.\n *              - Removed the deprecated `LLMS_Certificates::$_instance` property.\n */\nclass LLMS_Certificates {\n\n\tuse LLMS_Trait_Singleton,\n\t\tLLMS_Trait_Award_Default_Images;\n\n\t/**\n\t * The ID for the award type.\n\t *\n\t * Used by {@see LLMS_Trait_Award_Default_Images}.\n\t *\n\t * @var string\n\t */\n\tprotected $award_type = 'certificate';\n\n\t/**\n\t * Array of Certificate types.\n\t *\n\t * @var array\n\t */\n\tpublic $certs = array();\n\n\t/**\n\t * Array of local hosts\n\t *\n\t * @var string[]\n\t */\n\tprivate $export_local_hosts;\n\n\t/**\n\t * Array of hosts from which stylesheets won't be retrieved during the export\n\t *\n\t * @var string[]\n\t */\n\tprivate $export_blocked_stylesheet_hosts;\n\n\t/**\n\t * Array of hosts from which images won't be retrieved during the export\n\t *\n\t * @var string[]\n\t */\n\tprivate $export_blocked_image_hosts;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\t\t$this->init();\n\t}\n\n\t/**\n\t * Initialize class.\n\t *\n\t * @since 1.0.0\n\t * @since 4.21.0 Define useful class properties used when exporting.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\n\t\t$this->certs['LLMS_Certificate_User'] = isset( $this->certs['LLMS_Certificate_User'] ) ? $this->certs['LLMS_Certificate_User'] : include_once 'certificates/class.llms.certificate.user.php';\n\n\t\t$this->export_local_hosts = array_unique(\n\t\t\tarray(\n\t\t\t\twp_parse_url( get_home_url(), PHP_URL_HOST ),\n\t\t\t\twp_parse_url( get_site_url(), PHP_URL_HOST ),\n\t\t\t)\n\t\t);\n\n\t\t$this->export_blocked_stylesheet_hosts = array_unique(\n\t\t\t/**\n\t\t\t * Filters the blocked hosts for stylesheets in certificate exports\n\t\t\t *\n\t\t\t * @since 4.21.0\n\t\t\t *\n\t\t\t * @param string[] Array of hosts to block.\n\t\t\t */\n\t\t\tapply_filters(\n\t\t\t\t'llms_certificate_export_blocked_stylesheet_hosts',\n\t\t\t\tarray(\n\t\t\t\t\t'fonts.googleapis.com',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->export_blocked_image_hosts = array_unique(\n\t\t\t/**\n\t\t\t * Filters the blocked hosts for images in certificate exports\n\t\t\t *\n\t\t\t * @since 4.21.0\n\t\t\t *\n\t\t\t * @param string[] Array of hosts to block.\n\t\t\t */\n\t\t\tapply_filters(\n\t\t\t\t'llms_certificate_export_blocked_image_hosts',\n\t\t\t\tarray()\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Award a certificate to a user.\n\t *\n\t * Calls trigger method passing arguments\n\t *\n\t * @since 1.0.0\n\t * @deprecated 6.0.0 `LLMS_Certificates::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate()`.\n\t *\n\t * @param int $person_id       WP_User ID.\n\t * @param int $certificate_id  WP_Post ID of the certificate template.\n\t * @param int $related_post_id WP_Post ID of the related post, for example a lesson id.\n\t * @return void\n\t */\n\tpublic function trigger_engagement( $person_id, $certificate_id, $related_post_id ) {\n\t\t_deprecated_function( 'LLMS_Certificates::trigger_engagement()', '6.0.0', 'LLMS_Engagement_Handler::handle_certificate()' );\n\t\tLLMS_Engagement_Handler::handle_certificate( array( $person_id, $certificate_id, $related_post_id, null ) );\n\t}\n\n\t/**\n\t * Generate a downloadable HTML file for a certificate\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.3 Added action `llms_certificate_generate_export`.\n\t * @since 4.3.1 Introduce `llms_certificate_error` WP_Error code.\n\t *\n\t * @param string $filepath       Full path for the created file.\n\t * @param int    $certificate_id WP_Post ID of the earned certificate.\n\t * @return mixed WP_Error or full path to the generated export.\n\t */\n\tprivate function generate_export( $filepath, $certificate_id ) {\n\n\t\t$html = $this->get_export_html( $certificate_id );\n\n\t\tif ( is_wp_error( $html ) ) {\n\t\t\treturn $html;\n\t\t}\n\n\t\t/**\n\t\t * Run actions prior to certificate export generation.\n\t\t *\n\t\t * @param string $filepath       Full path where the created file will be stored. Passed as a reference.\n\t\t * @param string $html           Certificate HTML. Passed as a reference.\n\t\t * @param int    $certificate_id WP_Post ID of the earned certificate.\n\t\t */\n\t\tdo_action_ref_array( 'llms_certificate_generate_export', array( &$filepath, &$html, $certificate_id ) );\n\n\t\t$file = fopen( $filepath, 'w' );\n\t\tif ( false === $file ) {\n\t\t\treturn new WP_Error( 'llms_certificate_error', __( 'Unable to open export file (HTML certificate) for writing.', 'lifterlms' ) );\n\t\t}\n\n\t\tif ( false === fwrite( $file, $html ) ) {\n\t\t\treturn new WP_Error( 'llms_certificate_error', __( 'Unable to write to export file (HTML certificate).', 'lifterlms' ) );\n\t\t}\n\n\t\tfclose( $file );\n\n\t\treturn $filepath;\n\n\t}\n\n\t/**\n\t * Retrieve an existing or generate a downloadable HTML file for a certificate\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Use the certificate post title in favor of the deprecated meta value `_llms_certificate_title`.\n\t *\n\t * @param int  $certificate_id WP Post ID of the earned certificate.\n\t * @param bool $use_cache      If true will check for existence of a cached version of the file first.\n\t * @return mixed WP_Error or full path to the generated export.\n\t */\n\tpublic function get_export( $certificate_id, $use_cache = false ) {\n\n\t\tif ( $use_cache ) {\n\t\t\t$cached = get_post_meta( $certificate_id, '_llms_export_filepath', true );\n\t\t\tif ( $cached && file_exists( $cached ) ) {\n\t\t\t\treturn $cached;\n\t\t\t}\n\t\t}\n\n\t\t$cert = new LLMS_User_Certificate( $certificate_id );\n\n\t\t// Translators: %1$s = url-safe certificate title, %2$s = random alpha-numeric characters for filename obscurity.\n\t\t$filename  = sanitize_title( sprintf( esc_attr_x( 'certificate-%1$s-%2$s', 'certificate download filename', 'lifterlms' ), $cert->get( 'title' ), wp_generate_password( 12, false, false ) ) );\n\t\t$filename .= '.html';\n\t\t$filepath  = LLMS_TMP_DIR . $filename;\n\n\t\t// Generate the file.\n\t\t$filepath = $this->generate_export( $filepath, $certificate_id );\n\n\t\tif ( $use_cache && ! is_wp_error( $filepath ) ) {\n\t\t\tupdate_post_meta( $certificate_id, '_llms_export_filepath', $filepath );\n\t\t}\n\n\t\treturn $filepath;\n\n\t}\n\n\t/**\n\t * Retrieves the HTML of a certificate which can be used to create an exportable download\n\t *\n\t * @since 3.18.0\n\t * @since 3.24.3 Unknown.\n\t * @since 3.37.3 Refactored method into multiple functions.\n\t * @since 4.3.1 If `$this->scrape_certificate()` generates a `WP_Error` early return it.\n\t * @since 4.8.0 Remove redundant check for the presence of `DOMDocument`.\n\t *\n\t * @param int $certificate_id WP_Post ID of the earned certificate.\n\t * @return WP_Error|string HTML of the certificate on success, otherwise an error object.\n\t */\n\tprivate function get_export_html( $certificate_id ) {\n\n\t\t// Retrieve the raw HTML of the page.\n\t\t$html = $this->scrape_certificate( $certificate_id );\n\t\tif ( is_wp_error( $html ) ) {\n\t\t\treturn $html;\n\t\t}\n\n\t\t// Modify the DOM.\n\t\t$html = $this->modify_dom( $html );\n\n\t\t/**\n\t\t * Modify the HTML of a certificate export.\n\t\t *\n\t\t * @since  3.18.0\n\t\t *\n\t\t * @param string $html           HTML to be exported.\n\t\t * @param int    $certificate_id WP_Post ID of the earned certificate.\n\t\t */\n\t\treturn apply_filters( 'llms_get_certificate_export_html', $html, $certificate_id );\n\n\t}\n\n\t/**\n\t * Create a unique slug for earned certificates.\n\t *\n\t * When relying only on `wp_unique_post_slug()`, predictable URLs are created for earned certificates,\n\t * such as \"certificate-of-completion-1\", \"certificate-of-completion-2\", etc... this method creates\n\t * an obtuse and randomized suffix and appends it to the post slug.\n\t *\n\t * The unique suffix will be a randomized string at least 3 characters long and made up of lowercase letters and numbers.\n\t *\n\t * When ensuring uniqueness of the generated suffix, the length of the string will be increased by one for every 5\n\t * encountered collisions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $title The title of the certificate being created.\n\t * @return string\n\t */\n\tpublic function get_unique_slug( $title ) {\n\n\t\t$title = sanitize_title( $title ) . '-';\n\n\t\t/**\n\t\t * Filters the minimum length of the suffix used to create a unique earned certificate slug.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param int $min_strlen The minimum desired suffix string length.\n\t\t */\n\t\t$min_strlen = apply_filters( 'llms_certificate_unique_slug_suffix_min_length', 3 );\n\n\t\t$i = 0;\n\t\tdo {\n\t\t\t$length = $min_strlen + floor( $i / 5 );\n\t\t\t$slug   = $title . strtolower( wp_generate_password( absint( $length ), false ) );\n\t\t\t$i++;\n\t\t} while ( wp_unique_post_slug( $slug, 0, 'publish', 'llms_my_certificate', 0 ) !== $slug );\n\n\t\treturn $slug;\n\n\t}\n\n\t/**\n\t * Modify the HTML using DOMDocument.\n\t *\n\t * Preparations include:\n\t *\n\t *     1. Removing all `script` tags.\n\t *     2. Removes the WP Admin Bar.\n\t *     3. Converting all stylesheets into inline `style` tags.\n\t *     4. Removes all non stylesheet `link` tags.\n\t *     5. Converts `img` tags into data uris.\n\t *     6. Adds inline CSS to hide anything hidden in a print view.\n\t *\n\t * @since 3.37.3\n\t * @since 3.38.1 Use `LLMS_Mime_Type_Extractor::from_file_path()` in place of `mime_content_type()` to avoid issues with PHP installs that do not support it.\n\t * @since 4.8.0 Use `llms_get_dom_document()` in favor of loading `DOMDocument` directly.\n\t * @since 4.21.0 Allow external assets (e.g. images/stylesheets from CDN) to be embedded/inlined.\n\t *               Also, remove the WP Admin Bar earlier.\n\t *               Move the links and images modification in specific methods.\n\t *\n\t * @param string $html Certificate HTML.\n\t * @return string\n\t */\n\tprivate function modify_dom( $html ) {\n\n\t\t$dom = llms_get_dom_document( $html );\n\t\tif ( is_wp_error( $dom ) ) {\n\t\t\treturn $html;\n\t\t}\n\n\t\t// Don't throw or log warnings.\n\t\t$libxml_state = libxml_use_internal_errors( true );\n\n\t\t// Remove all <scripts>.\n\t\t$scripts = $dom->getElementsByTagName( 'script' );\n\t\twhile ( $scripts && $scripts->length ) {\n\t\t\t$scripts->item( 0 )->parentNode->removeChild( $scripts->item( 0 ) );\n\t\t}\n\n\t\t// Remove the admin bar (if found).\n\t\t$admin_bar = $dom->getElementById( 'wpadminbar' );\n\t\tif ( $admin_bar ) {\n\t\t\t$admin_bar->parentNode->removeChild( $admin_bar ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase\n\t\t}\n\n\t\t$this->modify_dom_links( $dom );\n\t\t$this->modify_dom_images( $dom );\n\n\t\t// Hide print stuff (this is faster than traversing the dom to remove the element).\n\t\t$header = $dom->getElementsByTagName( 'head' )->item( 0 );\n\t\t$header->appendChild( $dom->createELement( 'style', '.no-print { display: none !important; }' ) );\n\n\t\t$html = $dom->saveHTML();\n\n\t\t// Handle errors.\n\t\tlibxml_clear_errors();\n\n\t\t// Restore.\n\t\tlibxml_use_internal_errors( $libxml_state );\n\n\t\treturn $html;\n\n\t}\n\n\t/**\n\t * Modify head's <link>s of the DOMDocument.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param DOMDocument $dom The DOMDocument containing the certificate.\n\t * @return void\n\t */\n\tprivate function modify_dom_links( $dom ) {\n\n\t\t// Get all <links>.\n\t\t$links      = $dom->getElementsByTagName( 'link' );\n\t\t$to_replace = array();\n\n\t\t// Inline stylesheets.\n\t\tforeach ( $links as $link ) {\n\n\t\t\t// Only proceed for stylesheets.\n\t\t\tif ( 'stylesheet' !== $link->getAttribute( 'rel' ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$raw = $this->get_stylesheet_raw( $link->getAttribute( 'href' ) );\n\n\t\t\tif ( empty( $raw ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Add it to be inlined late.\n\t\t\t$tag          = $dom->createElement( 'style', $raw );\n\t\t\t$to_replace[] = array(\n\t\t\t\t'old' => $link,\n\t\t\t\t'new' => $tag,\n\t\t\t);\n\n\t\t}\n\n\t\t// Do replacements, ensures cascade order is retained.\n\t\tforeach ( $to_replace as $replacement ) {\n\t\t\t$replacement['old']->parentNode->replaceChild( $replacement['new'], $replacement['old'] );\n\t\t}\n\n\t\t// Remove all remaining non stylesheet <links>.\n\t\t$links = $dom->getElementsByTagName( 'link' );\n\t\twhile ( $links && $links->length ) {\n\t\t\t$links->item( 0 )->parentNode->removeChild( $links->item( 0 ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Get stylesheet raw content given its URL\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param string  $stylesheet_href The stylesheet href.\n\t * @param boolean $allowed_only    Optional. Get only stylesheet whose host is not in the `export_blocked_stylesheet_hosts` list.\n\t * @return string|false\n\t */\n\tprivate function get_stylesheet_raw( $stylesheet_href, $allowed_only = true ) {\n\n\t\t$href_host = wp_parse_url( $stylesheet_href, PHP_URL_HOST );\n\n\t\t// Only include stylesheets from non blocked hosts.\n\t\tif ( $allowed_only && in_array( $href_host, $this->export_blocked_stylesheet_hosts, true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Get the actual CSS.\n\t\tif ( in_array( $href_host, $this->export_local_hosts, true ) ) { // Is local?\n\t\t\t$raw = file_get_contents( untrailingslashit( ABSPATH ) . wp_parse_url( $stylesheet_href, PHP_URL_PATH ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- getting a local file.\n\t\t} else {\n\t\t\t$response = wp_remote_get( $stylesheet_href );\n\t\t\t$raw      = wp_remote_retrieve_body( $response );\n\t\t}\n\n\t\treturn $raw;\n\n\t}\n\n\t/**\n\t * Modify images of the DOMDocument\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param DOMDocument $dom The DOMDocument containing the certificate.\n\t * @return void\n\t */\n\tprivate function modify_dom_images( $dom ) {\n\n\t\t$images    = $dom->getElementsByTagName( 'img' );\n\t\t$to_remove = array();\n\n\t\t// Convert images to data uris.\n\t\tforeach ( $images as $img ) {\n\n\t\t\t$img_data_type = $this->get_image_data_and_type( $img->getAttribute( 'src' ) );\n\n\t\t\tif ( empty( $img_data_type['data'] ) || empty( $img_data_type['type'] ) ) {\n\t\t\t\t$to_remove[] = $img; // Save images to remove: removing them directly here will alter the collection iteration (skip).\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$data = base64_encode( $img_data_type['data'] );// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode\n\n\t\t\t$img->setAttribute( 'src', 'data:' . $img_data_type['type'] . ';base64,' . $data );\n\n\t\t\t// Remove srcset and sizes attributes.\n\t\t\t$img->removeAttribute( 'sizes' );\n\t\t\t$img->removeAttribute( 'srcset' );\n\t\t\t// Remove useless loading attribute.\n\t\t\t$img->removeAttribute( 'loading' );\n\t\t}\n\n\t\tforeach ( $to_remove as $img ) {\n\t\t\t$img->parentNode->removeChild( $img ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase\n\t\t}\n\n\t}\n\n\t/**\n\t * Get image data and type given its source URL\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param string  $image_src    The image src.\n\t * @param boolean $allowed_only Optional. Get only images whose host is not in the `export_blocked_image_hosts` list.\n\t * @return array|false\n\t */\n\tprivate function get_image_data_and_type( $image_src, $allowed_only = true ) {\n\n\t\t$src_host = wp_parse_url( $image_src, PHP_URL_HOST );\n\n\t\t// Only include images from non blocked hosts.\n\t\tif ( $allowed_only && in_array( $src_host, $this->export_blocked_image_hosts, true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( in_array( $src_host, $this->export_local_hosts, true ) ) { // Is local?\n\t\t\t$imgpath = untrailingslashit( ABSPATH ) . wp_parse_url( $image_src, PHP_URL_PATH );\n\t\t\t$data    = file_get_contents( $imgpath ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- getting a local file.\n\t\t\t$type    = LLMS_Mime_Type_Extractor::from_file_path( $imgpath );\n\t\t} else {\n\t\t\t$response = wp_remote_get( $image_src );\n\t\t\t$data     = wp_remote_retrieve_body( $response );\n\t\t\t$type     = wp_remote_retrieve_header( $response, 'content-type' );\n\t\t}\n\n\t\treturn compact( 'data', 'type' );\n\n\t}\n\n\t/**\n\t * Scrape a LifterLMS Certificate permalink and return the generated HTML.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @param int $certificate_id WP_Post ID of the earned certificate (an \"llms_my_certificate\" post).\n\t * @return WP_Error|string WP_Error on failure or the full page HTML on success.\n\t */\n\tprivate function scrape_certificate( $certificate_id ) {\n\n\t\t// Create a nonce for getting the export HTML.\n\t\t$token = wp_generate_password( 32, false );\n\t\tupdate_post_meta( $certificate_id, '_llms_auth_nonce', $token );\n\n\t\t/**\n\t\t * Modify the URL used to scrape the HTML of a certificate in preparation for a certificate export.\n\t\t *\n\t\t * @since 3.18.0\n\t\t *\n\t\t * @param string $url            Certificate permalink with a one-time use authorization token appended as a query string variable.\n\t\t * @param int    $certificate_id WP_Post ID of the earned certificate (an \"llms_my_certificate\" post).\n\t\t */\n\t\t$url = apply_filters(\n\t\t\t'llms_get_certificate_export_html_url',\n\t\t\tadd_query_arg(\n\t\t\t\t'_llms_cert_auth',\n\t\t\t\t$token,\n\t\t\t\tget_permalink( $certificate_id )\n\t\t\t),\n\t\t\t$certificate_id\n\t\t);\n\n\t\t// Perform the request.\n\t\t$req = wp_safe_remote_get(\n\t\t\t$url,\n\t\t\tarray(\n\t\t\t\t'sslverify' => false,\n\t\t\t)\n\t\t);\n\n\t\t// Delete the token after the request.\n\t\tdelete_post_meta( $certificate_id, '_llms_auth_nonce', $token );\n\n\t\t// Error.\n\t\tif ( is_wp_error( $req ) ) {\n\t\t\treturn $req;\n\t\t}\n\n\t\treturn wp_remote_retrieve_body( $req );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.comments.php",
    "content": "<?php\n/**\n * LLMS_Comments class file.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 6.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Custom filters & actions for LifterLMS comments.\n *\n * This class owes a great debt to WooCommerce.\n *\n * @since 3.0.0\n */\nclass LLMS_Comments {\n\n\t/**\n\t * Transient key where calculated comment stats are stored.\n\t *\n\t * @var string\n\t */\n\tprotected static $count_transient_key = 'llms_count_comments';\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.37.12\n\t * @since 6.6.0 Conditionally hook `wp_count_comments` filter.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Secure order notes.\n\t\tadd_filter( 'comments_clauses', array( __CLASS__, 'exclude_order_comments' ), 10, 1 );\n\t\tadd_action( 'comment_feed_join', array( __CLASS__, 'exclude_order_comments_from_feed_join' ) );\n\t\tadd_action( 'comment_feed_where', array( __CLASS__, 'exclude_order_comments_from_feed_where' ) );\n\n\t\t// Delete comments count cache whenever there is a new comment or a comment status changes.\n\t\tadd_action( 'wp_insert_comment', array( __CLASS__, 'delete_comments_count_cache' ) );\n\t\tadd_action( 'wp_set_comment_status', array( __CLASS__, 'delete_comments_count_cache' ) );\n\n\t\t/**\n\t\t * Remove order notes when counting comments on WP versions earlier than 6.0.\n\t\t *\n\t\t * @todo This filter can be safely deprecated once support is dropped for WordPress 6.0.\n\t\t */\n\t\tif ( self::should_modify_comment_counts() ) {\n\t\t\tadd_filter( 'wp_count_comments', array( __CLASS__, 'wp_count_comments' ), 999, 2 );\n\t\t}\n\n\t}\n\n\t/**\n\t * Delete transient data when inserting new comments or updating comment status\n\t *\n\t * Next time wp_count_comments is called it'll be automatically regenerated\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Use class variable to access the transient key name.\n\t *\n\t * @return void\n\t */\n\tpublic static function delete_comments_count_cache() {\n\t\tdelete_transient( self::$count_transient_key );\n\t}\n\n\t/**\n\t * Exclude order comments from queries and RSS.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Use strict comparison for `in_array()`.\n\t *\n\t * @param array $clauses Array of SQL clauses.\n\t * @return array\n\t */\n\tpublic static function exclude_order_comments( $clauses ) {\n\n\t\tglobal $wpdb, $typenow;\n\n\t\t// Allow queries when in the admin.\n\t\tif ( is_admin() && in_array( $typenow, array( 'llms_order' ), true ) && current_user_can( apply_filters( 'lifterlms_admin_order_access', 'manage_options' ) ) ) {\n\t\t\treturn $clauses;\n\t\t}\n\n\t\tif ( ! $clauses['join'] ) {\n\t\t\t$clauses['join'] = '';\n\t\t}\n\n\t\tif ( ! strstr( $clauses['join'], \"JOIN $wpdb->posts\" ) ) {\n\t\t\t$clauses['join'] .= \" LEFT JOIN $wpdb->posts ON comment_post_ID = $wpdb->posts.ID \";\n\t\t}\n\n\t\tif ( $clauses['where'] ) {\n\t\t\t$clauses['where'] .= ' AND ';\n\t\t}\n\n\t\t$clauses['where'] .= \" $wpdb->posts.post_type NOT IN ('\" . implode( \"','\", array( 'llms_order' ) ) . \"') \";\n\n\t\treturn $clauses;\n\n\t}\n\n\t/**\n\t * Exclude order comments from queries and RSS.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $join SQL join clause.\n\t * @return string\n\t */\n\tpublic static function exclude_order_comments_from_feed_join( $join ) {\n\t\tglobal $wpdb;\n\t\tif ( ! strstr( $join, $wpdb->posts ) ) {\n\t\t\t$join = \" LEFT JOIN $wpdb->posts ON $wpdb->comments.comment_post_ID = $wpdb->posts.ID \";\n\t\t}\n\t\treturn $join;\n\t}\n\n\t/**\n\t * Exclude order comments from queries and RSS.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $where SQL where clause.\n\t * @return string\n\t */\n\tpublic static function exclude_order_comments_from_feed_where( $where ) {\n\t\tglobal $wpdb;\n\t\tif ( $where ) {\n\t\t\t$where .= ' AND ';\n\t\t}\n\t\t$where .= \" $wpdb->posts.post_type NOT IN ('\" . implode( \"','\", array( 'llms_order' ) ) . \"') \";\n\t\treturn $where;\n\t}\n\n\t/**\n\t * Retrieve an array mapping database values to their human-readable meanings\n\t *\n\t * The array key is the value stored in the $wpdb->comments table for the `comment_approved` column.\n\t *\n\t * The array values are the equivalent value as expected by the return of the `wp_count_comments()` function.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return array\n\t */\n\tprotected static function get_approved_map() {\n\n\t\treturn array(\n\t\t\t'0'            => 'moderated',\n\t\t\t'1'            => 'approved',\n\t\t\t'spam'         => 'spam',\n\t\t\t'trash'        => 'trash',\n\t\t\t'post-trashed' => 'post-trashed',\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve order note comment counts.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return array[]\n\t */\n\tprotected static function get_note_counts() {\n\n\t\tglobal $wpdb;\n\t\treturn $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\t\t\"\n\t\t\tSELECT comment_approved, COUNT( * ) AS num_comments\n\t\t\tFROM {$wpdb->comments}\n\t\t\tWHERE comment_type = 'llms_order_note'\n\t\t\tGROUP BY comment_approved;\n\t\t\t\",\n\t\t\tARRAY_A\n\t\t);\n\n\t}\n\n\t/**\n\t * Remove order notes from an existing comment count stats object.\n\t *\n\t * This method accepts a stats object, generated by another plugin (like WooCommerce) or using core information\n\t * from `get_comment_counts()` and then subtracts LifterLMS order note comments from the existing comment counts\n\t * which would have included order notes in the counts.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @todo This method can be safely deprecated once support is dropped for WordPress 6.0.\n\t *\n\t * @param  stdClass $stats Comment stats object. See the return of LLMS_Comments::wp_comment_counts() for object details.\n\t * @return stdClass See LLMS_Comments::wp_comment_counts() for return object details.\n\t */\n\tprotected static function modify_comment_stats( $stats ) {\n\n\t\t$counts = self::get_note_counts();\n\t\t$map    = self::get_approved_map();\n\n\t\tforeach ( (array) $counts as $row ) {\n\n\t\t\tif ( ! in_array( $row['comment_approved'], array( 'post-trashed', 'trash', 'spam' ), true ) ) {\n\t\t\t\t$stats->all            -= $row['num_comments'];\n\t\t\t\t$stats->total_comments -= $row['num_comments'];\n\t\t\t}\n\n\t\t\tif ( isset( $map[ $row['comment_approved'] ] ) ) {\n\t\t\t\t$var          = $map[ $row['comment_approved'] ];\n\t\t\t\t$stats->$var -= $row['num_comments'];\n\t\t\t}\n\t\t}\n\n\t\tset_transient( self::$count_transient_key, $stats );\n\n\t\treturn $stats;\n\n\t}\n\n\t/**\n\t * Determines whether or not comment count modification is necessary.\n\t *\n\t * Since WordPress 6.0 the `get_comment_count()` function utilizes `get_comments()` whereas in earlier versions the counts\n\t * are retrieved by a direct SQL query. This change means that the filter in this class on `comments_clauses` ensures that\n\t * our comments we hide & don't count in the comments management UI are already excluded and we do not need to filter\n\t * `wp_count_comments` to subtract our comments.\n\t *\n\t * @since 6.6.0\n\t *\n\t * @return boolean Returns `true` on WP earlier than 6.0 and `false` on 6.0 and later.\n\t */\n\tprivate static function should_modify_comment_counts() {\n\t\tglobal $wp_version;\n\t\treturn version_compare( $wp_version, '6.0-src', '<' );\n\t}\n\n\t/**\n\t * Remove order notes from the count when counting comments\n\t *\n\t * This method is hooked to `wp_count_comments`, called by `wp_count_comments()`.\n\t *\n\t * It handles two potential scenarios:\n\t *\n\t * 1) No other plugins have run the filter and the incoming $stats is an empty array.\n\t * In this scenario we'll utilize `get_comment_count()` to create a new $stats object\n\t *\n\t * 2) Another plugin has already generated a stats object and then incoming $stats is a stdClass.\n\t *\n\t * In either scenario we query the number of order notes and subtract this number from the existing\n\t * comment counts.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.12 Use strict comparisons.\n\t *                Fix issue encountered when $stats is an empty array.\n\t *                Modify the stats generation method.\n\t * @since 6.6.0 Will throw `_doing_it_wrong()` when run on WP 6.0 or later and return the input `$stats` unchanged.\n\t *\n\t * @todo This method can be safely deprecated once support is dropped for WordPress 6.0.\n\t *\n\t * @param stdClass|array $stats   Empty array or a stdClass of stats from another plugin.\n\t * @param int            $post_id WP Post ID. `0` indicates comment stats for the entire site.\n\t * @return stdClass {\n\t *     The number of comments keyed by their status.\n\t *\n\t *     @type int $approved       The number of approved comments.\n\t *     @type int $moderated      The number of comments awaiting moderation (a.k.a. pending).\n\t *     @type int $spam           The number of spam comments.\n\t *     @type int $trash          The number of trashed comments.\n\t *     @type int $post-trashed   The number of comments for posts that are in the trash.\n\t *     @type int $total_comments The total number of non-trashed comments, including spam.\n\t *     @type int $all            The total number of pending or approved comments.\n\t * }\n\t */\n\tpublic static function wp_count_comments( $stats, $post_id ) {\n\n\t\t// If someone calls this directly on 6.0 or later notify them and return early.\n\t\tif ( ! self::should_modify_comment_counts() ) {\n\t\t\t_doing_it_wrong( __METHOD__, 'This method should not be called on WordPress 6.0 or later.', '6.6.0' );\n\t\t\treturn $stats;\n\t\t}\n\n\t\t// Don't modify when querying for a specific post.\n\t\tif ( 0 !== $post_id ) {\n\t\t\treturn $stats;\n\t\t}\n\n\t\t// Return cached object if available.\n\t\t$cached = get_transient( self::$count_transient_key );\n\t\tif ( $cached ) {\n\t\t\treturn $cached;\n\t\t}\n\n\t\t// If $stats is empty, get a new object from the WP Core that we can modify.\n\t\tif ( empty( $stats ) ) {\n\n\t\t\t$stats = get_comment_count( $post_id );\n\n\t\t\t// The keys in wp_count_comments() and get_comment_counts() don't match.\n\t\t\t$stats['moderated'] = $stats['awaiting_moderation'];\n\t\t\tunset( $stats['awaiting_moderation'] );\n\n\t\t\t// Cast to an object.\n\t\t\t$stats = (object) $stats;\n\n\t\t}\n\n\t\t// Otherwise modify the existing stats object.\n\t\treturn self::modify_comment_stats( $stats );\n\n\t}\n\n}\nreturn new LLMS_Comments();\n"
  },
  {
    "path": "includes/class.llms.course.data.php",
    "content": "<?php\n/**\n * Query data about a course\n *\n * @package LifterLMS/Classes\n *\n * @since 3.15.0\n * @version 5.10.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Query data about a course\n *\n * @since 3.15.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 3.31.0 Extends LLMS_Abstract_Post_Data.\n * @since 4.0.0 Remove previously deprecated class properties `$course` and `$course_id`.\n */\nclass LLMS_Course_Data extends LLMS_Abstract_Post_Data {\n\n\tpublic $course_id;\n\n\tpublic $course;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param int $course_id WP Post ID of the course\n\t */\n\tpublic function __construct( $course_id ) {\n\n\t\t$this->course_id = $course_id;\n\t\t$this->course    = llms_get_post( $this->course_id );\n\t\tparent::__construct( $course_id );\n\t}\n\n\t/**\n\t * Retrieve an array of all post ids in the course\n\t *\n\t * Includes course id, all section ids, all lesson ids, and all quiz ids.\n\t *\n\t * @since 3.15.0\n\t * @since 3.31.0 Use $this->post_id instead of deprecated $this->course_id.\n\t *\n\t * @return array\n\t */\n\tprivate function get_all_ids() {\n\t\treturn array_merge(\n\t\t\tarray( $this->post_id ),\n\t\t\t$this->post->get_sections( 'ids' ),\n\t\t\t$this->post->get_lessons( 'ids' ),\n\t\t\t$this->post->get_quizzes()\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve # of course completions within the period\n\t *\n\t * @since 3.15.0\n\t * @since 3.31.0 Use $this->post_id instead of deprecated $this->course_id.\n\t *\n\t * @param string $period Optional. Date period [current|previous]. Default is 'current'.\n\t * @return int\n\t */\n\tpublic function get_completions( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT DISTINCT COUNT( user_id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_value = 'yes'\n\t\t\t  AND meta_key = '_is_complete'\n\t\t\t  AND post_id = %d\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);// db call ok; no-cache ok.\n\t}\n\n\t/**\n\t * Retrieve # of course enrollments within the period\n\t *\n\t * @since 3.15.0\n\t * @since 3.31.0 Use $this->post_id instead of deprecated $this->course_id.\n\t *\n\t * @param string $period Optional. Date period [current|previous]. Default is 'current'.\n\t * @return int\n\t */\n\tpublic function get_enrollments( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT DISTINCT COUNT( user_id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_value = 'yes'\n\t\t\t  AND meta_key = '_start_date'\n\t\t\t  AND post_id = %d\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);// db call ok; no-cache ok.\n\t}\n\n\t/**\n\t * Retrieve # of engagements related to the course awarded within the period\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $type   Engagement type [email|certificate|achievement].\n\t * @param string $period Optional. Date period [current|previous]. Default is 'current'.\n\t * @return int\n\t */\n\tpublic function get_engagements( $type, $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$ids = implode( ',', array_map( 'absint', $this->get_all_ids() ) );\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT DISTINCT COUNT( user_id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_key = %s\n\t\t\t  AND post_id IN ( {$ids} )\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t'_' . $type,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);// db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t}\n\n\t/**\n\t * Retrieves and returns the number of lessons completed within the period.\n\t *\n\t * @since 3.15.0\n\t * @since 5.10.0 Fixed issue when the course has no lessons.\n\t *\n\t * @param string $period Optional. Date period [current|previous]. Default is 'current'.\n\t * @return int\n\t */\n\tpublic function get_lesson_completions( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$lesson_ids = $this->post->get_lessons( 'ids' );\n\n\t\t// Return early for courses without any lessons.\n\t\tif ( empty( $lesson_ids ) ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t$lessons = implode( ',', array_map( 'absint', $this->post->get_lessons( 'ids' ) ) );\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT COUNT( * )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_value = 'yes'\n\t\t\t  AND meta_key = '_is_complete'\n\t\t\t  AND post_id IN ( {$lessons} )\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);// db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t}\n\n\t/**\n\t * Retrieve # of orders placed for the course within the period\n\t *\n\t * @since 3.15.0\n\t * @since 4.21.0 Fixed order params passed to the `$this->orders_query()` method.\n\t *\n\t * @param string $period Optional. Date period [current|previous]. Default is 'current'.\n\t * @return int\n\t */\n\tpublic function get_orders( $period = 'current' ) {\n\n\t\t$query = $this->orders_query(\n\t\t\t1,\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'after'     => $this->get_date( $period, 'start' ),\n\t\t\t\t\t'before'    => $this->get_date( $period, 'end' ),\n\t\t\t\t\t'inclusive' => true,\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t\treturn $query->found_posts;\n\t}\n\n\t/**\n\t * Retrieve total amount of transactions related to orders for the course completed within the period\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $period Date period [current|previous].\n\t * @return float\n\t */\n\tpublic function get_revenue( $period ) {\n\n\t\t$query     = $this->orders_query( -1 );\n\t\t$order_ids = wp_list_pluck( $query->posts, 'ID' );\n\n\t\t$revenue = 0;\n\n\t\tif ( $order_ids ) {\n\n\t\t\t$order_ids = implode( ',', array_map( 'absint', $order_ids ) );\n\n\t\t\tglobal $wpdb;\n\t\t\t$revenue = $wpdb->get_var(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"SELECT SUM( m2.meta_value )\n\t\t\t\t FROM $wpdb->posts AS p\n\t\t\t\t LEFT JOIN $wpdb->postmeta AS m1 ON m1.post_id = p.ID AND m1.meta_key = '_llms_order_id' -- join for the ID\n\t\t\t\t LEFT JOIN $wpdb->postmeta AS m2 ON m2.post_id = p.ID AND m2.meta_key = '_llms_amount'-- get the actual amounts\n\t\t\t\t WHERE p.post_type = 'llms_transaction'\n\t\t\t\t   AND p.post_status = 'llms-txn-succeeded'\n\t\t\t\t   AND m1.meta_value IN ({$order_ids})\n\t\t\t\t   AND p.post_modified BETWEEN %s AND %s\n\t\t\t\t;\",\n\t\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t\t)\n\t\t\t);// db call ok; no-cache ok.\n\n\t\t\tif ( is_null( $revenue ) ) {\n\t\t\t\t$revenue = 0;\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_course_data_get_revenue', $revenue, $period, $this );\n\t}\n\n\t/**\n\t * Retrieve the number of unenrollments on a given date.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param  tring $period Optional. Date period [current|previous]. Default 'current'.\n\t * @return int\n\t */\n\tpublic function get_unenrollments( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT DISTINCT COUNT( user_id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_value != 'enrolled'\n\t\t\t  AND meta_key = '_status'\n\t\t\t  AND post_id = %d\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);// db call ok; no-cache ok.\n\t}\n\n\t/**\n\t * Execute a WP Query to retrieve orders within the given date range\n\t *\n\t * @since 3.15.0\n\t * @since 4.21.0 Fixed the post status for completed orders.\n\t *\n\t * @param int   $num_orders Optional. Number of orders to retrieve. Default is `1`.\n\t * @param array $dates      Optional. Date range (passed to WP_Query['date_query']). Default is empty array.\n\t * @return WP_Query\n\t */\n\tprivate function orders_query( $num_orders = 1, $dates = array() ) {\n\n\t\t$args = array(\n\t\t\t'post_type'      => 'llms_order',\n\t\t\t'post_status'    => array( 'llms-active', 'llms-completed' ),\n\t\t\t'posts_per_page' => $num_orders,\n\t\t\t'meta_key'       => '_llms_product_id',\n\t\t\t'meta_value'     => $this->post_id,\n\t\t);\n\n\t\tif ( ! empty( $dates ) ) {\n\t\t\t$args['date_query'] = $dates;\n\t\t}\n\n\t\treturn new WP_Query( $args );\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.data.php",
    "content": "<?php\n/**\n * Retrieve data sets used by various other classes and functions\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 4.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Data\n *\n * @since 3.0.0\n */\nclass LLMS_Data {\n\n\t/**\n\t * Get the data data\n\t *\n\t * @since 3.0.0\n\t * @since 3.17.0 Added browser/os data section.\n\t * @since 4.13.0 Added constant data.\n\t *\n\t * @param string $dataset Dataset to retrieve data for [tracker|system_report].\n\t * @param string $format  Data return format (unused for unrecalled reasons).\n\t * @return array\n\t */\n\tpublic static function get_data( $dataset, $format = 'array' ) {\n\n\t\t$data = array();\n\n\t\t// Add admin email for tracker requests.\n\t\tif ( 'tracker' === $dataset ) {\n\t\t\t$data['email'] = apply_filters( 'llms_get_data_admin_email', get_option( 'admin_email' ) );\n\t\t}\n\n\t\t// General data.\n\t\t$data['url'] = home_url();\n\n\t\t// Wp info.\n\t\t$data['wordpress'] = self::get_wp_data();\n\n\t\t// Llms settings.\n\t\t$data['settings'] = self::get_llms_settings();\n\n\t\tif ( 'system_report' === $dataset ) {\n\t\t\t// Constants.\n\t\t\t$data['constants'] = self::get_constants_data();\n\t\t}\n\n\t\t// Gateways.\n\t\t$data['gateways'] = self::get_gateway_data();\n\n\t\t// Server info.\n\t\t$data['server'] = self::get_server_data();\n\n\t\t// Browser / os.\n\t\t$data['browser'] = self::get_browser_data();\n\n\t\t// Theme info.\n\t\t$data['theme'] = self::get_theme_data();\n\n\t\t// Plugin info.\n\t\t$data['plugins'] = self::get_plugin_data();\n\n\t\tif ( 'tracker' === $dataset ) {\n\n\t\t\t// Published content type counts.\n\t\t\t$data['post_counts'] = self::get_post_type_counts();\n\n\t\t\t// User data.\n\t\t\t$data['user_counts'] = self::get_user_counts();\n\n\t\t\t// Count student engagements.\n\t\t\t$data['engagement_counts'] = self::get_engagement_counts();\n\n\t\t\t// Order data.\n\t\t\t$data['order_counts'] = self::get_order_counts();\n\n\t\t}\n\n\t\t$data['integrations'] = self::get_integrations_data();\n\n\t\t$data['template_overrides'] = self::get_templates_data();\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * add browser and os info to the system report\n\t *\n\t * @since 3.17.0\n\t * @since 3.35.0 Sanitize `$_SERVER` data.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_browser_data() {\n\n\t\t$data = array(\n\t\t\t'HTTP_USER_AGENT' => ! empty( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',\n\n\t\t);\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Retrieve data about LifterLMS constants\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_constants_data() {\n\n\t\t$data = array(\n\t\t\t'LLMS_REMOVE_ALL_DATA'                 => 'undefined',\n\t\t\t'LLMS_REST_DISABLE'                    => 'undefined',\n\t\t\t'LLMS_SITE_FEATURE_RECURRING_PAYMENTS' => 'undefined',\n\t\t\t'LLMS_SITE_IS_CLONE'                   => 'undefined',\n\t\t);\n\n\t\tforeach ( $data as $constant => &$value ) {\n\n\t\t\tif ( defined( $constant ) ) {\n\t\t\t\t$value = constant( $constant ) ? 'true' : 'false';\n\t\t\t}\n\t\t}\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get student engagement counts for various llms interactions\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_engagement_counts() {\n\n\t\tglobal $wpdb;\n\n\t\t$data = array();\n\n\t\t$data['certificates']       = absint( $wpdb->get_var( \"SELECT COUNT( * ) FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_certificate_earned'\" ) );\n\t\t$data['achievements']       = absint( $wpdb->get_var( \"SELECT COUNT( * ) FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_achievement_earned'\" ) );\n\t\t$enrollments                = $wpdb->get_results( \"SELECT meta_id FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_status' AND ( meta_value = 'Enrolled' OR meta_value = 'enrolled' ) GROUP BY user_id, post_id\" );\n\t\t$data['enrollments']        = count( $enrollments );\n\t\t$data['course_completions'] = absint( $wpdb->get_var( \"SELECT COUNT( * ) FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_is_complete' AND meta_value = 'yes' \" ) );\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Retrieve metadata from a file.\n\t * Copied from WCs get_file_version which is based on WP Core's get_file_data function.\n\t *\n\t * @since 3.11.2\n\t *\n\t * @param string $file Path to the file.\n\t * @return string\n\t */\n\tprivate static function get_file_version( $file ) {\n\n\t\t// Avoid notices if file does not exist.\n\t\tif ( ! file_exists( $file ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t// We don't need to write to the file, so just open for reading..\n\t\t$fp = fopen( $file, 'r' );\n\n\t\t// Pull only the first 8kiB of the file in..\n\t\t$file_data = fread( $fp, 8192 );\n\n\t\t// PHP will close file handle, but we are good citizens..\n\t\tfclose( $fp );\n\n\t\t// Make sure we catch CR-only line endings..\n\t\t$file_data = str_replace( \"\\r\", \"\\n\", $file_data );\n\t\t$version   = '';\n\n\t\tif ( preg_match( '/^[ \\t\\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) {\n\t\t\t$version = _cleanup_header_comment( $match[1] );\n\t\t}\n\n\t\treturn $version;\n\n\t}\n\n\t/**\n\t * Get data about llms payment gateways\n\t *\n\t * @since 3.0.0\n\t * @since 3.17.8 Unknown.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_gateway_data() {\n\n\t\t$data = array();\n\n\t\tforeach ( llms()->payment_gateways()->get_payment_gateways() as $obj ) {\n\n\t\t\t$data[ $obj->get_admin_title() ] = $obj->is_enabled() ? 'Enabled' : 'Disabled';\n\n\t\t\tif ( $obj->supports( 'test_mode' ) ) {\n\t\t\t\t$data[ $obj->get_admin_title() . '_test_mode' ] = $obj->is_test_mode_enabled() ? 'Enabled' : 'Disabled';\n\t\t\t}\n\n\t\t\t$data[ $obj->get_admin_title() . '_logging' ] = $obj->get_logging_enabled();\n\t\t\t$data[ $obj->get_admin_title() . '_order' ]   = $obj->get_display_order();\n\n\t\t}\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get data about existing llms integrations\n\t *\n\t * @since 3.0.0\n\t * @since 3.17.8 Unknown.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_integrations_data() {\n\n\t\t$data = array();\n\n\t\t$integrations = llms()->integrations();\n\n\t\tforeach ( $integrations->integrations() as $obj ) {\n\n\t\t\tif ( method_exists( $obj, 'is_available' ) ) {\n\n\t\t\t\t$data[ $obj->title ] = $obj->is_available() ? 'Yes' : 'No';\n\n\t\t\t}\n\t\t}\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get LifterLMS settings\n\t *\n\t * @since 3.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_llms_settings() {\n\n\t\t$data = array();\n\n\t\t$data['version']    = llms()->version;\n\t\t$data['db_version'] = get_option( 'lifterlms_db_version' );\n\n\t\t$data['course_catalog']     = self::get_page_data( 'lifterlms_shop_page_id' );\n\t\t$data['membership_catalog'] = self::get_page_data( 'lifterlms_memberships_page_id' );\n\t\t$data['student_dashboard']  = self::get_page_data( 'lifterlms_myaccount_page_id' );\n\t\t$data['checkout_page']      = self::get_page_data( 'lifterlms_checkout_page_id' );\n\n\t\t$data['course_catalog_per_page'] = get_option( 'lifterlms_shop_courses_per_page' );\n\t\t$data['course_catalog_sorting']  = get_option( 'lifterlms_shop_ordering' );\n\n\t\t$data['membership_catalog_per_page'] = get_option( 'lifterlms_memberships_per_page' );\n\t\t$data['membership_catalog_sorting']  = get_option( 'lifterlms_memberships_ordering' );\n\n\t\t$data['site_membership'] = self::get_page_data( 'lifterlms_membership_required' );\n\n\t\t$data['courses_endpoint']       = get_option( 'lifterlms_myaccount_courses_endpoint' );\n\t\t$data['edit_endpoint']          = get_option( 'lifterlms_myaccount_edit_account_endpoint' );\n\t\t$data['lost_password_endpoint'] = get_option( 'lifterlms_myaccount_lost_password_endpoint' );\n\t\t$data['vouchers_endpoint']      = get_option( 'lifterlms_myaccount_redeem_vouchers_endpoint' );\n\n\t\t$data['autogenerate_username'] = get_option( 'lifterlms_registration_generate_username', 'no' );\n\n\t\t$data['password_strength_meter']   = get_option( 'lifterlms_registration_password_strength', 'no' );\n\t\t$data['minimum_password_strength'] = get_option( 'lifterlms_registration_password_min_strength' );\n\n\t\t$data['terms_required'] = get_option( 'lifterlms_registration_require_agree_to_terms', 'no' );\n\t\t$data['terms_page']     = self::get_page_data( 'lifterlms_terms_page_id' );\n\n\t\t$data['checkout_names']              = get_option( 'lifterlms_user_info_field_names_checkout_visibility' );\n\t\t$data['checkout_address']            = get_option( 'lifterlms_user_info_field_address_checkout_visibility' );\n\t\t$data['checkout_phone']              = get_option( 'lifterlms_user_info_field_phone_checkout_visibility' );\n\t\t$data['checkout_email_confirmation'] = get_option( 'lifterlms_user_info_field_email_confirmation_checkout_visibility', 'no' );\n\n\t\t$data['open_registration']               = get_option( 'lifterlms_enable_myaccount_registration', 'no' );\n\t\t$data['registration_names']              = get_option( 'lifterlms_user_info_field_names_registration_visibility' );\n\t\t$data['registration_address']            = get_option( 'lifterlms_user_info_field_address_registration_visibility' );\n\t\t$data['registration_phone']              = get_option( 'lifterlms_user_info_field_phone_registration_visibility' );\n\t\t$data['registration_voucher']            = get_option( 'lifterlms_voucher_field_registration_visibility' );\n\t\t$data['registration_email_confirmation'] = get_option( 'lifterlms_user_info_field_email_confirmation_registration_visibility', 'no' );\n\n\t\t$data['account_names']              = get_option( 'lifterlms_user_info_field_names_account_visibility' );\n\t\t$data['account_address']            = get_option( 'lifterlms_user_info_field_address_account_visibility' );\n\t\t$data['account_phone']              = get_option( 'lifterlms_user_info_field_phone_account_visibility' );\n\t\t$data['account_email_confirmation'] = get_option( 'lifterlms_user_info_field_email_confirmation_account_visibility', 'no' );\n\n\t\t$data['confirmation_endpoint'] = get_option( 'lifterlms_myaccount_confirm_payment_endpoint' );\n\t\t$data['force_ssl_checkout']    = get_option( 'lifterlms_checkout_force_ssl' );\n\t\t$data['country']               = get_lifterlms_country();\n\t\t$data['currency']              = get_lifterlms_currency();\n\t\t$data['currency_position']     = get_option( 'lifterlms_currency_position' );\n\t\t$data['thousand_separator']    = get_option( 'lifterlms_thousand_separator' );\n\t\t$data['decimal_separator']     = get_option( 'lifterlms_decimal_separator' );\n\t\t$data['decimals']              = get_option( 'lifterlms_decimals' );\n\t\t$data['trim_zero_decimals']    = get_option( 'lifterlms_trim_zero_decimals', 'no' );\n\n\t\t$data['recurring_payments'] = ( LLMS_Site::get_feature( 'recurring_payments' ) ) ? 'yes' : 'no';\n\n\t\t$data['email_from_address'] = get_option( 'lifterlms_email_from_address' );\n\t\t$data['email_from_name']    = get_option( 'lifterlms_email_from_name' );\n\t\t$data['email_footer_text']  = get_option( 'lifterlms_email_footer_text' );\n\t\t$data['email_header_image'] = get_option( 'lifterlms_email_header_image' );\n\t\t$data['cert_bg_width']      = get_option( 'lifterlms_certificate_bg_img_width' );\n\t\t$data['cert_bg_height']     = get_option( 'lifterlms_certificate_bg_img_height' );\n\t\t$data['cert_legacy_compat'] = get_option( 'lifterlms_certificate_legacy_image_size' );\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get number of orders per order status\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_order_counts() {\n\n\t\t$data = array();\n\n\t\t$orders = wp_count_posts( 'llms_order' );\n\n\t\tforeach ( llms_get_order_statuses() as $status => $name ) {\n\n\t\t\t$data[ $status ] = absint( $orders->{$status} );\n\n\t\t}\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get an option that should return a page ID and return the page name and ID as a formatted string\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $option Option name in the wp_options table.\n\t * @return string\n\t */\n\tprivate static function get_page_data( $option ) {\n\t\t$id = get_option( $option );\n\t\tif ( absint( $id ) ) {\n\t\t\treturn sprintf( '%1$s (#%2$d) [%3$s]', get_the_title( $id ), $id, get_permalink( $id ) );\n\t\t}\n\t\treturn 'Not Set'; // Don't translate this or you won't be able to read it smartypants.\n\t}\n\n\t/**\n\t * get an array of plugin data, sorted into two arrays (active and inactive)\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_plugin_data() {\n\n\t\t// Ensure we have our plugin function.\n\t\tif ( ! function_exists( 'get_plugins' ) ) {\n\t\t\tinclude ABSPATH . '/wp-admin/includes/plugin.php';\n\t\t}\n\n\t\t$plugins = get_plugins();\n\n\t\t$active   = array();\n\t\t$inactive = array();\n\n\t\tforeach ( get_plugins() as $path => $data ) {\n\n\t\t\tif ( is_plugin_active( $path ) ) {\n\t\t\t\t$active[ $path ] = $data;\n\t\t\t} else {\n\t\t\t\t$inactive[ $path ] = $data;\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\t'active'   => $active,\n\t\t\t'inactive' => $inactive,\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the number of published posts for various LLMS post types\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_post_type_counts() {\n\n\t\t$data = array();\n\n\t\t$posts = array(\n\t\t\t'course',\n\t\t\t'section',\n\t\t\t'lesson',\n\t\t\t'llms_quiz',\n\t\t\t'llms_question',\n\t\t\t'llms_review',\n\n\t\t\t'llms_membership',\n\n\t\t\t'llms_access_plan',\n\t\t\t'llms_coupon',\n\t\t\t'llms_voucher',\n\n\t\t\t'llms_engagement',\n\t\t\t'llms_achievement',\n\t\t\t'llms_certificate',\n\t\t\t'llms_email',\n\t\t);\n\n\t\tforeach ( $posts as $post_type ) {\n\t\t\t$count = wp_count_posts( $post_type );\n\t\t\t$data[ str_replace( 'llms_', '', $post_type ) ] = absint( $count->publish );\n\t\t}\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get PHP & Server Data\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Sanitize `$_SERVER` data.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_server_data() {\n\n\t\tglobal $wpdb;\n\n\t\t$data = array();\n\n\t\tif ( function_exists( 'ini_get' ) ) {\n\t\t\t$data['php_max_input_vars'] = ini_get( 'max_input_vars' );\n\t\t\t$data['php_memory_limit']   = ini_get( 'memory_limit' );\n\t\t\t$data['php_post_max_size']  = ini_get( 'post_max_size' );\n\t\t\t$data['php_time_limt']      = ini_get( 'max_execution_time' );\n\t\t\t$data['php_suhosin']        = extension_loaded( 'suhosin' ) ? 'Yes' : 'No';\n\t\t}\n\n\t\t$data['mysql_version'] = $wpdb->db_version();\n\n\t\t$data['php_curl']             = function_exists( 'curl_init' ) ? 'Yes' : 'No';\n\t\t$data['php_default_timezone'] = date_default_timezone_get();\n\t\t$data['php_fsockopen']        = function_exists( 'fsockopen' ) ? 'Yes' : 'No';\n\t\t$data['php_max_upload_size']  = size_format( wp_max_upload_size() );\n\t\t$data['php_soap']             = class_exists( 'SoapClient' ) ? 'Yes' : 'No';\n\n\t\tif ( function_exists( 'phpversion' ) ) {\n\t\t\t$data['php_version'] = phpversion();\n\t\t}\n\n\t\tif ( isset( $_SERVER['SERVER_SOFTWARE'] ) && ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) {\n\t\t\t$data['software'] = ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '';\n\t\t}\n\n\t\t$data['wp_memory_limit'] = WP_MEMORY_LIMIT;\n\n\t\tksort( $data );\n\n\t\treturn $data;\n\t}\n\n\t/**\n\t * Retrieve information about template overrides\n\t *\n\t * @since 3.11.2\n\t *\n\t * @return array\n\t */\n\tprivate static function get_templates_data() {\n\n\t\t$path = llms()->plugin_path() . '/templates/';\n\n\t\t$templates = array_merge( glob( $path . '*.php' ), glob( $path . '**/*.php' ) );\n\n\t\t$overrides = array();\n\n\t\tforeach ( $templates as $file ) {\n\n\t\t\t$name  = str_replace( $path, '', $file );\n\t\t\t$found = llms_get_template_override( $name );\n\t\t\tif ( $found ) {\n\t\t\t\t$overrides[] = array(\n\t\t\t\t\t'core_version' => self::get_file_version( $file ),\n\t\t\t\t\t'location'     => $found,\n\t\t\t\t\t'version'      => self::get_file_version( $found . $name ),\n\t\t\t\t\t'template'     => $name,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn $overrides;\n\n\t}\n\n\t/**\n\t * Get an array of theme data\n\t *\n\t * @since 3.0.0\n\t * @since 3.11.2 Unknown.\n\t *\n\t * @return   array\n\t */\n\tprivate static function get_theme_data() {\n\n\t\t$data                 = array();\n\t\t$theme_data           = wp_get_theme();\n\t\t$data['name']         = $theme_data->get( 'Name' );\n\t\t$data['version']      = $theme_data->get( 'Version' );\n\t\t$data['themeuri']     = $theme_data->get( 'ThemeURI' );\n\t\t$data['authoruri']    = $theme_data->get( 'AuthorURI' );\n\t\t$data['template']     = $theme_data->get( 'Template' );\n\t\t$data['child_theme']  = is_child_theme() ? 'Yes' : 'No';\n\t\t$data['llms_support'] = ( ! current_theme_supports( 'lifterlms' ) ) ? 'No' : 'Yes';\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Det the number of users and users by role registered on the site\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_user_counts() {\n\n\t\t$data = array();\n\n\t\t$users = count_users();\n\n\t\t$data          = $users['avail_roles'];\n\t\t$data['total'] = $users['total_users'];\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get some WP core settings and info\n\t *\n\t * @since 3.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return array\n\t */\n\tprivate static function get_wp_data() {\n\n\t\t$data = array();\n\n\t\t$data['home_url']            = get_home_url();\n\t\t$data['site_url']            = get_site_url();\n\t\t$data['login_url']           = wp_login_url();\n\t\t$data['version']             = get_bloginfo( 'version' );\n\t\t$data['debug_mode']          = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No';\n\t\t$data['debug_log']           = ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) ? 'Yes' : 'No';\n\t\t$data['debug_display']       = ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) ? 'Yes' : 'No';\n\t\t$data['locale']              = get_locale();\n\t\t$data['multisite']           = is_multisite() ? 'Yes' : 'No';\n\t\t$data['page_for_posts']      = self::get_page_data( 'page_for_posts' );\n\t\t$data['page_on_front']       = self::get_page_data( 'page_on_front' );\n\t\t$data['permalink_structure'] = get_option( 'permalink_structure' );\n\t\t$data['show_on_front']       = get_option( 'show_on_front' );\n\t\t$data['wp_cron']             = ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ? 'Yes' : 'No';\n\n\t\treturn $data;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.date.php",
    "content": "<?php\n/**\n * Dates Class\n *\n * Manages formatting dates for I/O and display\n *\n * @package LifterLMS/Classes\n *\n * @since Unknown\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Date class\n *\n * @since Unknown\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Date {\n\n\t/**\n\t * Set date to dd/mm/yyyy\n\t * Optional type value for converting AU date format\n\t * Converts any type of date format\n\t *\n\t * @param  [date] $date [datestring]\n\t * @return [date]       [datestring]\n\t */\n\tpublic static function pretty_date( $date, $type = '' ) {\n\n\t\tif ( 'au' === $type ) {\n\t\t\treturn date( 'd/m/Y', strtotime( $date ) );\n\t\t} else {\n\t\t\treturn date( 'm/d/Y', strtotime( $date ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Date filter options for analytics\n\t *\n\t * @return [array] [array of date filters]\n\t */\n\tpublic static function date_filters() {\n\t\t$filters = array(\n\t\t\t'none'    => 'Enter Specific Dates',\n\t\t\t'week'    => 'Last 7 Days',\n\t\t\t'month'   => 'Current Month',\n\t\t\t'quarter' => 'Current Quarter',\n\t\t\t'year'    => 'Current Year',\n\t\t);\n\t\treturn $filters;\n\t}\n\n\t/**\n\t * Converts date to yyyymmdd\n\t * Converts any type of date format\n\t * Optional field type accepts 'au' to convert Australian dd/mm/yyyy date format\n\t * Used for date db storage\n\t *\n\t * @param  [date] $date [datestring]\n\t * @param  [type] $type [optional field for managing AU date conversions]\n\t * @return [date]       [datestring]\n\t */\n\tpublic static function db_date( $date, $type = '' ) {\n\n\t\tif ( 'au' === $type ) {\n\t\t\tlist($d, $m, $y) = preg_split( '/\\//', $date );\n\t\t\t$date            = sprintf( '%4d-%02d-%02d', $y, $m, $d );\n\t\t} else {\n\t\t\t$date = date( 'Y-m-d', strtotime( $date ) );\n\t\t}\n\n\t\treturn $date;\n\t}\n\n\t/**\n\t * Get date range by filter\n\t *\n\t * Calculates the date range based on the filter value selected.\n\t *\n\t * @param  string $filter\n\t * @return array  $date_range\n\t */\n\tpublic static function get_date_range_by_filter( $filter ) {\n\n\t\t$today         = current_time( 'Y-m-d' );\n\t\t$current_month = date( 'm', strtotime( $today ) );\n\t\t$current_year  = date( 'Y', strtotime( $today ) );\n\n\t\tif ( 'week' === $filter ) {\n\n\t\t\t$start_date = self::db_date( $today . '- 7 days' );\n\t\t\t$end_date   = self::db_date( $today );\n\n\t\t} elseif ( 'month' === $filter ) {\n\n\t\t\t$start_date = date( 'Y-m-01', strtotime( $today ) );\n\t\t\t$end_date   = date( 'Y-m-t', strtotime( $today ) );\n\n\t\t} elseif ( 'quarter' === $filter ) {\n\n\t\t\tif ( $current_month >= 1 && $current_month <= 3 ) {\n\t\t\t\t$start_date = $current_year . '-01-01';\n\t\t\t\t$end_date   = $current_year . '-03-31';\n\t\t\t} elseif ( $current_month >= 4 && $current_month <= 6 ) {\n\t\t\t\t$start_date = $current_year . '-04-01';\n\t\t\t\t$end_date   = $current_year . '-06-30';\n\t\t\t} elseif ( $current_month >= 7 && $current_month <= 9 ) {\n\t\t\t\t$start_date = $current_year . '-07-01';\n\t\t\t\t$end_date   = $current_year . '-09-30';\n\t\t\t} elseif ( $current_month >= 10 && $current_month <= 12 ) {\n\t\t\t\t$start_date = $current_year . '-10-01';\n\t\t\t\t$end_date   = ( $current_year + 1 ) . '-01-01';\n\t\t\t}\n\t\t} elseif ( 'year' === $filter ) {\n\n\t\t\t$start_date = $current_year . '-01-01';\n\t\t\t$end_date   = ( $current_year + 1 ) . '-01-01';\n\t\t}\n\n\t\t$date_range = array(\n\t\t\t'start_date' => $start_date,\n\t\t\t'end_date'   => $end_date,\n\t\t);\n\n\t\treturn $date_range;\n\n\t}\n\n\t/**\n\t * Query Filter for for last 7 days\n\t * Appends AND statement to WP_Query WHERE clause\n\t * Only retrieves posts created\n\t *\n\t * @param  string $where [WP_Query Where clause]\n\t * @return [string]        [modified where clause]\n\t */\n\tpublic function last_seven_days( $where = '' ) {\n\t\tglobal $wpdb;\n\n\t\t$where .= $wpdb->prepare( ' AND post_date > %s', date( 'Y-m-d', strtotime( '-7 days' ) ) );\n\n\t\treturn $where;\n\t}\n\n\tpublic static function get_last_login_date( $user_id ) {\n\n\t\t$date = get_user_meta( $user_id, 'llms_last_login', true );\n\n\t\tif ( $date ) {\n\t\t\treturn date( 'd.m.Y H:i:s', get_user_meta( $user_id, 'llms_last_login', true ) );\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\n\t}\n\n\t/**\n\t * @todo  deprecate\n\t */\n\tpublic static function get_localized_date_string() {\n\t\treturn strftime( _x( '%1$b %2$d, %3$Y @ %4$I:%5$M %6$p', 'Localized Order DateTime', 'lifterlms' ) ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated -- Unused deprecated function to be removed shortly.\n\t}\n\n\tpublic static function convert_to_hours_minutes_string( $time ) {\n\t\t$decimal_part = $time - floor( $time );\n\t\tsettype( $time, 'integer' );\n\t\tif ( $time < 1 ) {\n\t\t\treturn;\n\t\t}\n\t\t$hours   = floor( $time / 60 );\n\t\t$minutes = ( $time % 60 );\n\t\t$seconds = ( 60 * $decimal_part );\n\n\t\t$hours_string   = '';\n\t\t$minutes_string = '';\n\t\t$seconds_string = '';\n\n\t\t// Determine hours vs hour in string.\n\t\tif ( ! empty( $hours ) ) {\n\t\t\tif ( $hours > 1 ) {\n\t\t\t\t$hour_desc = __( 'hours', 'lifterlms' );\n\t\t\t} else {\n\t\t\t\t$hour_desc = __( 'hour', 'lifterlms' );\n\t\t\t}\n\n\t\t\t$hours_string = sprintf( __( '%1$d %2$s ', 'lifterlms' ), $hours, $hour_desc );\n\t\t} else {\n\t\t\tif ( ! empty( $seconds ) ) {\n\t\t\t\tif ( $seconds > 1 ) {\n\t\t\t\t\t$second_desc = __( 'seconds', 'lifterlms' );\n\t\t\t\t} else {\n\t\t\t\t\t$second_desc = __( 'second', 'lifterlms' );\n\t\t\t\t}\n\n\t\t\t\t$seconds_string = sprintf( ' %d %s', $seconds, $second_desc );\n\n\t\t\t}\n\t\t}\n\n\t\t// Determine minutes vs minute in string.\n\t\tif ( ! empty( $minutes ) ) {\n\t\t\tif ( $minutes > 1 ) {\n\t\t\t\t$minute_desc = __( 'minutes', 'lifterlms' );\n\t\t\t} else {\n\t\t\t\t$minute_desc = __( 'minute', 'lifterlms' );\n\t\t\t}\n\n\t\t\t$minutes_string = sprintf( ' %d %s', $minutes, $minute_desc );\n\n\t\t}\n\n\t\treturn $hours_string . $minutes_string . $seconds_string;\n\t}\n\n\n\n}\n\nreturn new LLMS_Date();\n"
  },
  {
    "path": "includes/class.llms.dot.com.api.php",
    "content": "<?php\n/**\n * Interact with the LifterLMS.com API\n *\n * @package LifterLMS/Classes\n *\n * @since 3.22.0\n * @version 3.22.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Dot_Com_API\n *\n * @since 3.22.0\n */\nclass LLMS_Dot_Com_API extends LLMS_Abstract_API_Handler {\n\n\t/**\n\t * Send requests in JSON format\n\t *\n\t * @var  bool\n\t */\n\tprotected $is_json = false;\n\n\t/**\n\t * Determines if it's a request to the .com REST api\n\t *\n\t * @var  bool\n\t */\n\tprotected $is_rest = true;\n\n\t/**\n\t * Construct an API call, parameters are passed to private `call()` function\n\t *\n\t * @param    stirng $resource  url endpoint or resource to make a request to\n\t * @param    array  $data      array of data to pass in the body of the request\n\t * @param    string $method    method of request (POST, GET, DELETE, PUT, etc...)\n\t * @param    bool   $is_rest   if true adds wp-json rest to request url, otherwise requests to site base\n\t * @return   void\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tpublic function __construct( $resource, $data, $method = null, $is_rest = true ) {\n\n\t\t$this->is_rest = $is_rest;\n\t\tparent::__construct( $resource, $data, $method );\n\n\t}\n\n\t/**\n\t * Determine if the current request is a rest request\n\t *\n\t * @return   bool\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tpublic function is_rest_request() {\n\t\treturn $this->is_rest;\n\t}\n\n\t/**\n\t * Parse the body of the response and set a success/error\n\t *\n\t * @param    array $response  response data\n\t * @return   void\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tprotected function parse_response( $response ) {\n\n\t\t$body = json_decode( wp_remote_retrieve_body( $response ), true );\n\n\t\tif ( isset( $response['response'] ) && isset( $response['response']['code'] ) && ! in_array( $response['response']['code'], array( 200, 201 ) ) ) {\n\n\t\t\t$msg = isset( $body['message'] ) ? $body['message'] : $response['response']['message'];\n\t\t\t$this->set_error( $msg, isset( $body['code'] ) ? $body['code'] : $response['response']['code'], $body );\n\n\t\t} else {\n\n\t\t\t$this->set_result( $body );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Set request body\n\t *\n\t * @param    array  $data      request body\n\t * @param    string $method    request method\n\t * @param    string $resource  requested resource\n\t * @return   array\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tprotected function set_request_body( $data, $method, $resource ) {\n\t\treturn apply_filters( 'llms_dot_com_api_request_body', $data, $method, $resource, $this );\n\t}\n\n\t/**\n\t * Set request headers\n\t *\n\t * @param    array  $headers   default request headers\n\t * @param    string $resource  request resource\n\t * @param    string $method    request method\n\t * @return   array\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tprotected function set_request_headers( $headers, $resource, $method ) {\n\t\treturn apply_filters( 'llms_dot_com_api_request_headers', $headers, $resource, $method, $this );\n\t}\n\n\t/**\n\t * Set the request URL\n\t *\n\t * @param    string $resource  requested resource\n\t * @param    string $method    request method\n\t * @return   string\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tprotected function set_request_url( $resource, $method ) {\n\n\t\t$url = 'https://lifterlms.com';\n\t\tif ( $this->is_rest_request() ) {\n\t\t\t$url .= '/wp-json/llms/v3';\n\t\t}\n\n\t\treturn apply_filters( 'llms_dot_com_api_request_url', $url . $resource, $resource, $method, $this );\n\t}\n\n\t/**\n\t * Set the request User Agent\n\t * Can be overridden by extending classes when necessary\n\t *\n\t * @param    string $user_agent  default user agent (LifterLMS {$version})\n\t * @param    string $resource    requested resource\n\t * @param    string $method      request method\n\t * @return   string\n\t * @since    3.22.0\n\t * @version  3.22.0\n\t */\n\tprotected function set_user_agent( $user_agent, $resource, $method ) {\n\t\treturn sprintf( 'LifterLMS/%1$s (%2$s)', LLMS_VERSION, get_site_url() );\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.emails.php",
    "content": "<?php\n/**\n * LifterLMS Emails Class\n *\n * Manages finding the appropriate email.\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Emails\n *\n * @since 1.0.0\n * @since 3.8.0 Unknown.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Removed the deprecated `LLMS_Emails::$_instance` property.\n */\nclass LLMS_Emails {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Class names of all emails\n\t *\n\t * @var string[]\n\t */\n\tpublic $emails;\n\n\t/**\n\t * Constructor\n\t *\n\t * Initializes class.\n\t * Adds actions to trigger emails off of events.\n\t *\n\t * @since 1.0.0\n\t * @since 3.8.0 Unknown.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t// Template functions.\n\t\tllms()->include_template_functions();\n\n\t\t// Email base class.\n\t\t$this->emails['generic'] = 'LLMS_Email';\n\n\t\t// Email child classes.\n\t\t$this->emails['engagement']     = 'LLMS_Email_Engagement';\n\t\t$this->emails['reset_password'] = 'LLMS_Email_Reset_Password';\n\n\t\t$this->emails = apply_filters( 'lifterlms_email_classes', $this->emails );\n\t}\n\n\t/**\n\t * Get a string of inline CSS to add to an email button\n\t *\n\t * Use {button_style} merge code to output in HTML emails.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_button_style() {\n\t\t/**\n\t\t * Filters the default email button CSS rules\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param array $email_button_css Associative array of the type css-property => definition.\n\t\t */\n\t\t$rules  = apply_filters(\n\t\t\t'llms_email_button_css',\n\t\t\tarray(\n\t\t\t\t'background-color' => $this->get_css( 'button-background-color', false ),\n\t\t\t\t'color'            => $this->get_css( 'button-font-color', false ),\n\t\t\t\t'display'          => 'inline-block',\n\t\t\t\t'padding'          => '10px 15px',\n\t\t\t\t'text-decoration'  => 'none',\n\t\t\t)\n\t\t);\n\t\t$styles = '';\n\t\tforeach ( $rules as $rule => $style ) {\n\t\t\t$styles .= sprintf( '%1$s:%2$s !important;', $rule, $style );\n\t\t}\n\t\treturn $styles;\n\t}\n\n\t/**\n\t * Get css rules specific to the the email templates\n\t *\n\t * @since 3.8.0\n\t * @since 5.2.0 Early bail if no rule is provided.\n\t *\n\t * @param string  $rule Optional. Name of the css rule. Default is empty string.\n\t *                      If not provided an empty string will be returned/echoed.\n\t * @param boolean $echo Optional. If true, echo the definition. Default is `true`.\n\t * @return string\n\t */\n\tpublic function get_css( $rule = '', $echo = true ) {\n\n\t\tif ( empty( $rule ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t/**\n\t\t * Filters the default email CSS rules\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param array $email_css Associative array of the type css-property => definition.\n\t\t */\n\t\t$css = apply_filters(\n\t\t\t'llms_email_css',\n\t\t\tarray(\n\t\t\t\t'background-color'         => '#f6f6f6',\n\t\t\t\t'border-radius'            => '3px',\n\t\t\t\t'button-background-color'  => '#2295ff',\n\t\t\t\t'button-font-color'        => '#ffffff',\n\t\t\t\t'divider-color'            => '#cecece',\n\t\t\t\t'font-color'               => '#222222',\n\t\t\t\t'font-family'              => 'sans-serif',\n\t\t\t\t'font-size'                => '15px',\n\t\t\t\t'font-size-small'          => '13px',\n\t\t\t\t'heading-background-color' => '#2295ff',\n\t\t\t\t'heading-font-color'       => '#ffffff',\n\t\t\t\t'main-color'               => '#2295ff',\n\t\t\t\t'max-width'                => '580px',\n\t\t\t)\n\t\t);\n\n\t\tif ( isset( $css[ $rule ] ) ) {\n\n\t\t\tif ( $echo ) {\n\t\t\t\techo esc_attr( $css[ $rule ] );\n\t\t\t}\n\n\t\t\treturn $css[ $rule ];\n\n\t\t}\n\t}\n\n\t/**\n\t * Get an HTML divider for use in HTML emails\n\t *\n\t * Can use shortcode {divider} to output in any email.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_divider_html() {\n\t\treturn '<div style=\"height:1px;width:100%;margin:15px auto;background-color:' . $this->get_css( 'divider-color', false ) . '\"></div>';\n\t}\n\n\t/**\n\t * Retrieve a new instance of an email\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $id   Email id.\n\t * @param array  $args Optional arguments to pass to the email.\n\t * @return LLMS_Email\n\t */\n\tpublic function get_email( $id, $args = array() ) {\n\n\t\t$emails = $this->get_emails();\n\n\t\t// If we have an email matching the ID, return an instance of that email class.\n\t\tif ( isset( $emails[ $id ] ) ) {\n\t\t\treturn new $emails[ $id ]( $args );\n\t\t}\n\n\t\t// Otherwise return a generic email and set the ID to be the requested ID.\n\t\t/** @var LLMS_Email $generic */\n\t\t$generic = new $emails['generic']( $args );\n\t\t$generic->set_id( $id );\n\t\treturn $generic;\n\t}\n\n\t/**\n\t * Get all email objects\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return string[] Array of all email class names.\n\t */\n\tpublic function get_emails() {\n\t\treturn $this->emails;\n\t}\n\n\t/**\n\t * Retrieve the source url of the header image as defined in LifterLMS settings\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_header_image_src() {\n\t\t$src = get_option( 'lifterlms_email_header_image', '' );\n\t\tif ( is_numeric( $src ) ) {\n\t\t\t$attachment = wp_get_attachment_image_src( $src, 'full' );\n\t\t\t$src        = $attachment ? $attachment[0] : '';\n\t\t}\n\t\t/**\n\t\t * Filters the header image src\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param string $src Image `src` attribute value.\n\t\t */\n\t\treturn apply_filters( 'llms_email_header_image_src', $src );\n\t}\n\n\t/**\n\t * Returns an array with the table's tags inline style\n\t *\n\t * It makes sure that all the required tags (table, tr, td) are set.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return array {\n\t *     Array of table style.\n\t *\n\t *     @type string $0 Style of the table tag.\n\t *     @type string $1 Style of the tr tag.\n\t *     @type string $2 Style of the td tag.\n\t * }\n\t */\n\tprivate function get_parsed_table_style() {\n\n\t\t$table_style = $this->get_table_style();\n\t\t$table_style = is_array( $table_style ) ? $table_style : array( $table_style );\n\n\t\t$table_style = wp_parse_args(\n\t\t\t$table_style,\n\t\t\tarray(\n\t\t\t\t'table' => '',\n\t\t\t\t'tr'    => '',\n\t\t\t\t'td'    => '',\n\t\t\t)\n\t\t);\n\n\t\treturn array_values( $table_style );\n\t}\n\n\t/**\n\t * Return an associative array with the table's tags inline style\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_table_style() {\n\t\treturn array(\n\t\t\t'table' => $this->get_table_table_style(),\n\t\t\t'tr'    => $this->get_table_tr_style(),\n\t\t\t'td'    => $this->get_table_td_style(),\n\t\t);\n\t}\n\n\t/**\n\t * Return the table's `table` tag inline style\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_table_table_style() {\n\t\treturn sprintf(\n\t\t\t'border-collapse:collapse;color:%1$s;font-family:%2$s;font-size:%3$s;Margin-bottom:15px;text-align:left;width:100%%;',\n\t\t\t$this->get_css( 'font-color', false ),\n\t\t\t$this->get_css( 'font-family', false ),\n\t\t\t$this->get_css( 'font-size', false )\n\t\t);\n\t}\n\n\t/**\n\t * Return the table's `tr` tag inline style\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_table_tr_style() {\n\t\treturn 'color:inherit;font-family:inherit;font-size:inherit;';\n\t}\n\n\t/**\n\t * Return the table's `td` tag inline style\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_table_td_style() {\n\t\treturn sprintf(\n\t\t\t'border-bottom:1px solid %s;color:inherit;font-family:inherit;font-size:inherit;padding:10px;',\n\t\t\t$this->get_css( 'divider-color', false )\n\t\t);\n\t}\n\n\t/**\n\t * Returns the table html\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param array $rows Array of rows to populate the table with.\n\t * @return string\n\t */\n\tpublic function get_table_html( $rows ) {\n\n\t\tif ( empty( $rows ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tob_start();\n\t\t$this->output_table_html( $rows );\n\n\t\treturn ob_get_clean();\n\t}\n\n\tpublic function output_table_html( $rows ) {\n\n\t\tif ( empty( $rows ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tlist( $table_style, $tr_style, $td_style ) = $this->get_parsed_table_style();\n\n\t\t?>\n\t\t<table style=\"<?php echo esc_attr( $table_style ); ?>\">\n\t\t\t<?php foreach ( $rows as $code => $name ) : ?>\n\t\t\t\t<tr style=\"<?php echo esc_attr( $tr_style ); ?>\">\n\t\t\t\t\t<th style=\"<?php echo esc_attr( $td_style ); ?>width:33.3333%;\"><?php echo esc_html( $name ); ?></th>\n\t\t\t\t\t<td style=\"<?php echo esc_attr( $td_style ); ?>\">{{<?php echo esc_html( $code ); ?>}}</td>\n\t\t\t\t</tr>\n\t\t\t<?php endforeach; ?>\n\t\t</table>\n\t\t<?php\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.engagements.php",
    "content": "<?php\n/**\n * LLMS_Engagements class file\n *\n * @package LifterLMS/Classes\n *\n * @since 2.3.0\n * @version 6.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Engagements Class\n *\n * @since 2.3.0\n * @since 3.30.3 Fixed spelling errors.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Changes:\n *              - Deprecated the `LLMS_Engagements::handle_achievement()` method.\n *                Use the {@see LLMS_Engagement_Handler::handle_achievement()} method instead.\n *              - Deprecated the `LLMS_Engagements::handle_certificate()` method.\n *                Use the {@see LLMS_Engagement_Handler::handle_certificate()} method instead.\n *              - Deprecated the `LLMS_Engagements::handle_email()` method.\n *                Use the {@see LLMS_Engagement_Handler::handle_email()} method instead.\n *              - Deprecated the `LLMS_Engagements::init()` method with no replacement.\n *              - Deprecated the `LLMS_Engagements::log()` method.\n *                Engagement debug logging is removed. Use the {@see llms_log()} function directly instead.\n *              - Removed the deprecated `LLMS_Engagements::$_instance` property.\n */\nclass LLMS_Engagements {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Enable debug logging\n\t *\n\t * @since 2.7.9\n\t * @var boolean\n\t */\n\tprivate $debug = false;\n\n\t/**\n\t * Constructor\n\t *\n\t * Adds actions to events that trigger engagements.\n\t *\n\t * @since 2.3.0\n\t * @since 6.0.0 Added deprecation warning when using constant `LLMS_ENGAGEMENT_DEBUG`.\n\t *              Don't call deprecated `init()` method.\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\tif ( defined( 'LLMS_ENGAGEMENT_DEBUG' ) && LLMS_ENGAGEMENT_DEBUG ) {\n\t\t\t_deprecated_function( 'Constant: LLMS_ENGAGEMENT_DEBUG', '6.0.0' );\n\t\t\t$this->debug = true;\n\t\t}\n\n\t\t$this->add_actions();\n\t}\n\n\t/**\n\t * Register all actions that trigger engagements\n\t *\n\t * @since 2.3.0\n\t * @since 3.11.0 Unknown.\n\t * @since 3.39.0 Added `llms_rest_student_registered` as action hook.\n\t * @since 6.0.0 Moved the list of hooks to the `get_trigger_hooks()` method.\n\t *\n\t * @return void\n\t */\n\tprivate function add_actions() {\n\n\t\tforeach ( $this->get_trigger_hooks() as $action ) {\n\t\t\tadd_action( $action, array( $this, 'maybe_trigger_engagement' ), 777, 3 );\n\t\t}\n\n\t\t// Handlers are in charge of processing (awarding/sending) the email/cert/achievement.\n\t\t$handlers = array(\n\t\t\t'lifterlms_engagement_send_email'        => 'handle_email',\n\t\t\t'lifterlms_engagement_award_achievement' => 'handle_achievement',\n\t\t\t'lifterlms_engagement_award_certificate' => 'handle_certificate',\n\t\t);\n\t\tforeach ( $handlers as $action => $method ) {\n\n\t\t\t/**\n\t\t\t * Adds an action for the deprecated method so that `remove_action()` calls\n\t\t\t * on the old method will continue to remove the new method.\n\t\t\t *\n\t\t\t * When we *remove* the deprecated methods we can remove this logic.\n\t\t\t */\n\t\t\tadd_action( $action, array( $this, $method ) );\n\n\t\t\t// If the above action has been completely removed this will be false and we won't add the new method callback.\n\t\t\t$priority = has_action( $action, array( $this, $method ) );\n\t\t\tif ( false !== $priority ) {\n\t\t\t\t// Remove the deprecated action.\n\t\t\t\tremove_action( $action, array( $this, $method ) );\n\t\t\t\t// Call the new action at the specified priority. If the old action was restored at a different priority this will retain that customization.\n\t\t\t\tadd_action( $action, array( 'LLMS_Engagement_Handler', $method ), $priority );\n\t\t\t}\n\t\t}\n\n\t\tadd_action( 'deleted_post', array( $this, 'unschedule_delayed_engagements' ), 20, 2 );\n\t}\n\n\t/**\n\t * Retrieve a group id used when scheduling delayed engagement action triggers.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $engagement_id WP_Post ID of the `llms_engagement` post type.\n\t * @return string\n\t */\n\tprivate function get_delayed_group_id( $engagement_id ) {\n\t\treturn sprintf( 'llms_engagement_%d', $engagement_id );\n\t}\n\n\t/**\n\t * Retrieve engagements based on the trigger type\n\t *\n\t * Joins rather than nested loops and sub queries ftw.\n\t *\n\t * @since 2.3.0\n\t * @since 3.13.1 Unknown.\n\t * @since 6.0.0 Removed engagement debug logging & moved filter onto the return instead of calling in `maybe_trigger_engagement()`.\n\t *\n\t * @param string     $trigger_type    Name of the trigger to look for.\n\t * @param int|string $related_post_id The WP_Post ID of the related post or an empty string.\n\t * @return object[] {\n\t *     Array of objects from the database.\n\t *\n\t *     @type int    $engagement_id WP_Post ID of the engagement post (email, certificate, achievement).\n\t *     @type int    $trigger_id    WP_Post ID of the llms_engagement post.\n\t *     @type string $trigger_event The triggering action (user_registration, course_completed, etc...).\n\t *     @type string $event_type    The engagement event action (certificate, achievement, email).\n\t *     @type int    $delay         The engagement send delay (in days).\n\t * }\n\t */\n\tprivate function get_engagements( $trigger_type, $related_post_id = '' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$related_select = '';\n\t\t$related_join   = '';\n\t\t$related_where  = '';\n\n\t\tif ( $related_post_id ) {\n\n\t\t\t$related_select = ', relation_meta.meta_value AS related_post_id';\n\t\t\t$related_join   = \"LEFT JOIN $wpdb->postmeta AS relation_meta ON triggers.ID = relation_meta.post_id\";\n\t\t\t$related_where  = $wpdb->prepare(\n\t\t\t\t\"AND relation_meta.meta_key = '_llms_engagement_trigger_post' AND ( relation_meta.meta_value = %d OR relation_meta.meta_value = 'any' )\",\n\t\t\t\t$related_post_id\n\t\t\t);\n\n\t\t}\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$results = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT\n\t\t\t\t  DISTINCT triggers.ID AS trigger_id\n\t\t\t\t, triggers_meta.meta_value AS engagement_id\n\t\t\t\t, engagements_meta.meta_value AS trigger_event\n\t\t\t\t, event_meta.meta_value AS event_type\n\t\t\t\t, delay.meta_value AS delay\n\t\t\t\t$related_select\n\n\t\t\tFROM $wpdb->postmeta AS engagements_meta\n\n\t\t\tLEFT JOIN $wpdb->posts AS triggers ON triggers.ID = engagements_meta.post_id\n\t\t\tLEFT JOIN $wpdb->postmeta AS triggers_meta ON triggers.ID = triggers_meta.post_id\n\t\t\tLEFT JOIN $wpdb->posts AS engagements ON engagements.ID = triggers_meta.meta_value\n\t\t\tLEFT JOIN $wpdb->postmeta AS event_meta ON triggers.ID = event_meta.post_id\n\t\t\tLEFT JOIN $wpdb->postmeta AS delay ON triggers.ID = delay.post_id\n\t\t\t$related_join\n\n\t\t\tWHERE\n\t\t\t\t    triggers.post_type = 'llms_engagement'\n\t\t\t\tAND triggers.post_status = 'publish'\n\t\t\t\tAND triggers_meta.meta_key = '_llms_engagement'\n\n\t\t\t\tAND engagements_meta.meta_key = '_llms_trigger_type'\n\t\t\t\tAND engagements_meta.meta_value = %s\n\t\t\t\tAND engagements.post_status = 'publish'\n\n\t\t\t\tAND event_meta.meta_key = '_llms_engagement_type'\n\n\t\t\t\tAND delay.meta_key = '_llms_engagement_delay'\n\n\t\t\t\t$related_where\n\t\t\t\",\n\t\t\t\t// Prepare variables.\n\t\t\t\t$trigger_type\n\t\t\t),\n\t\t\tOBJECT\n\t\t); // no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t/**\n\t\t * Filters the list of engagements to be triggered for a given trigger type and related post.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param object[] $results         Array of engagement objects.\n\t\t * @param string   $trigger_type    Name of the engagement trigger.\n\t\t * @param int      $related_post_id WP_Post ID of the related post.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_get_engagements', $results, $trigger_type, $related_post_id );\n\t}\n\n\t/**\n\t * Retrieve a list of hooks that trigger engagements to be awarded.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string[]\n\t */\n\tprotected function get_trigger_hooks() {\n\n\t\t$hooks = array(\n\t\t\t'lifterlms_access_plan_purchased',\n\t\t\t'lifterlms_course_completed',\n\t\t\t'lifterlms_course_track_completed',\n\t\t\t'lifterlms_lesson_completed',\n\t\t\t'lifterlms_product_purchased',\n\t\t\t'lifterlms_quiz_completed',\n\t\t\t'lifterlms_quiz_failed',\n\t\t\t'lifterlms_quiz_passed',\n\t\t\t'lifterlms_section_completed',\n\t\t\t'lifterlms_user_registered',\n\t\t\t'llms_rest_student_registered',\n\t\t\t'llms_user_added_to_membership_level',\n\t\t\t'llms_user_enrolled_in_course',\n\t\t);\n\n\t\t// If there are any actions registered to this deprecated hook, add it to the list.\n\t\tif ( has_action( 'lifterlms_created_person' ) ) {\n\t\t\t$hooks[] = 'lifterlms_created_person';\n\t\t}\n\n\t\t/**\n\t\t * Filters the list of hooks which can trigger engagements to be sent/awarded.\n\t\t *\n\t\t * @since 2.3.0\n\t\t *\n\t\t * @param string[] $hooks List of hook names.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_engagement_actions', $hooks );\n\t}\n\n\t/**\n\t * Include engagement types (excluding email)\n\t *\n\t * @since Unknown\n\t * @deprecated 6.0.0 `LLMS_Engagements::init()` is deprecated with no replacement.\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\t\t_deprecated_function( 'LLMS_Engagements::init()', '6.0.0' );\n\t}\n\n\t/**\n\t * Award an achievement\n\t *\n\t * @since 2.3.0\n\t * @deprecated 6.0.0 `LLMS_Engagements::handle_achievement` is deprecated in favor of `LLMS_Engagement_Handler::handle_achievement`.\n\t *\n\t * @param array $args {\n\t *     Indexed array of arguments.\n\t *\n\t *     @type int        $0 WP_User ID.\n\t *     @type int        $1 WP_Post ID of the achievement template post.\n\t *     @type int|string $2 WP_Post ID of the related post that triggered the award or an empty string.\n\t *     @type int        $3 WP_Post ID of the engagement post.\n\t * }\n\t * @return void\n\t */\n\tpublic function handle_achievement( $args ) {\n\t\t_deprecated_function( 'LLMS_Engagements::handle_achievement', '6.0.0', 'LLMS_Engagement_Handler::handle_achievement' );\n\t\tLLMS_Engagement_Handler::handle_achievement( $args );\n\t}\n\n\t/**\n\t * Award a certificate\n\t *\n\t * @since 2.3.0\n\t * @deprecated 6.0.0 `LLMS_Engagements::handle_certificate` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate`.\n\t *\n\t * @param array $args {\n\t *     Indexed array of arguments.\n\t *\n\t *     @type int        $0 WP_User ID.\n\t *     @type int        $1 WP_Post ID of the certificate template post.\n\t *     @type int|string $2 WP_Post ID of the related post that triggered the award or an empty string.\n\t *     @type int        $3 WP_Post ID of the engagement post.\n\t * }\n\t * @return void\n\t */\n\tpublic function handle_certificate( $args ) {\n\t\t_deprecated_function( 'LLMS_Engagements::handle_certificate', '6.0.0', 'LLMS_Engagement_Handler::handle_certificate' );\n\t\tLLMS_Engagement_Handler::handle_certificate( $args );\n\t}\n\n\t/**\n\t * Send an email engagement\n\t *\n\t * This is called via do_action() by the 'maybe_trigger_engagement' function in this class.\n\t *\n\t * @since 2.3.0\n\t * @since 3.8.0 Unknown.\n\t * @since 4.4.1 Use postmeta helpers for dupcheck and postmeta insertion.\n\t *              Add a return value in favor of `void`.\n\t *              Log successes and failures to the `engagement-emails` log file instead of the main `llms` log.\n\t * @since 4.4.3 Fixed different emails triggered by the same related post not sent because of a wrong duplicate check.\n\t *              Fixed dupcheck log message and error message which reversed the email and person order.\n\t * @deprecated 6.0.0 `LLMS_Engagements::handle_email` is deprecated in favor of `LLMS_Engagement_Handler::handle_email`.\n\t *\n\t * @param mixed[] $args {\n\t *     An array of arguments from the triggering hook.\n\t *\n\t *     @type int        $0 WP_User ID.\n\t *     @type int        $1 WP_Post ID of the email.\n\t *     @type int|string $2 WP_Post ID of the related triggering post or an empty string for engagements with no related post.\n\t *     @type int        $3 WP_Post ID of the engagement post.\n\t * }\n\t * @return bool|WP_Error Returns `true` on success, `false` when the email is skipped, and a `WP_Error` when\n\t *                       the email has failed or is prevented.\n\t */\n\tpublic function handle_email( $args ) {\n\t\t_deprecated_function( 'LLMS_Engagements::handle_email', '6.0.0', 'LLMS_Engagement_Handler::handle_email' );\n\t\t$res = LLMS_Engagement_Handler::handle_email( $args );\n\t\tif ( true === $res ) {\n\t\t\treturn $res;\n\t\t}\n\t\t// The new handler returns an array of errors in favor of a single error. Retain the initial return type for this deprecated version.\n\t\treturn $res[0];\n\t}\n\n\t/**\n\t * Parse incoming hook / callback data to determine if an engagement should be triggered from a given hook.\n\t *\n\t * @since 6.0.0\n\t * @since 6.6.0 Fixed an issue where the `lifterlms_external_engagement_query_arguments` filter\n\t *              would not trigger if a 3rd party registered a trigger hook.\n\t *\n\t * @param string $action Action hook name.\n\t * @param array  $args   Array of arguments passed to the callback function.\n\t * @return array {\n\t *     An associative array of parsed data used to trigger the engagement.\n\t *\n\t *     @type string $trigger_type    The name of the engagement trigger. See `llms_get_engagement_triggers()` for a list of valid triggers.\n\t *     @type int    $user_id         The WP_User ID of the user who the engagement is being awarded or sent to.\n\t *     @type int    $related_post_id The WP_Post ID of a related post.\n\t *  }\n\t */\n\tprivate function parse_hook( $action, $args ) {\n\n\t\t$parsed = array(\n\t\t\t'trigger_type'    => null,\n\t\t\t'user_id'         => null,\n\t\t\t'related_post_id' => null,\n\t\t);\n\n\t\t/**\n\t\t * Allows 3rd parties to hook into the core engagement system by parsing data passed to the hook.\n\t\t *\n\t\t * @since 2.3.0\n\t\t *\n\t\t * @param array $parsed {\n\t\t *     An associative array of parsed data used to trigger the engagement.\n\t\t *\n\t\t *     @type string $trigger_type    (Required) The name of the engagement trigger. See `llms_get_engagement_triggers()` for a list of valid triggers.\n\t\t *     @type int    $user_id         (Required) The WP_User ID of the user who the engagement is being awarded or sent to.\n\t\t *     @type int    $related_post_id (Optional) The WP_Post ID of a related post.\n\t\t *  }\n\t\t *  @param string $action The name of the hook which triggered the engagement.\n\t\t *  @param array  $args   The original arguments provided by the triggering hook.\n\t\t */\n\t\t$filtered_parsed = apply_filters(\n\t\t\t'lifterlms_external_engagement_query_arguments',\n\t\t\t$parsed,\n\t\t\t$action,\n\t\t\t$args\n\t\t);\n\t\t// If valid, return the filtered parsed data.\n\t\tif ( isset( $filtered_parsed['trigger_type'] ) && isset( $filtered_parsed['user_id'] ) ) {\n\t\t\treturn $filtered_parsed;\n\t\t}\n\n\t\t// Verify that the action is a supported hook.\n\t\tif ( ! in_array( $action, $this->get_trigger_hooks(), true ) ) {\n\t\t\treturn $parsed;\n\t\t}\n\n\t\t// The user registration action doesn't have a related post id.\n\t\t$related_post_id = isset( $args[1] ) && is_numeric( $args[1] ) ? absint( $args[1] ) : '';\n\n\t\t$parsed['user_id']         = absint( $args[0] );\n\t\t$parsed['trigger_type']    = $this->parse_hook_find_trigger_type( $action, $related_post_id );\n\t\t$parsed['related_post_id'] = $related_post_id;\n\n\t\treturn $parsed;\n\t}\n\n\t/**\n\t * Get the engagement trigger type based on the action and related post id\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string     $action          Name of the triggering action hook.\n\t * @param int|string $related_post_id WP_Post ID of the related post or an empty string.\n\t * @return string\n\t */\n\tprivate function parse_hook_find_trigger_type( $action, $related_post_id ) {\n\n\t\t$trigger_type = '';\n\n\t\tswitch ( $action ) {\n\t\t\tcase 'llms_rest_student_registered':\n\t\t\tcase 'lifterlms_created_person':\n\t\t\tcase 'lifterlms_user_registered':\n\t\t\t\t$trigger_type = 'user_registration';\n\t\t\t\tbreak;\n\n\t\t\tcase 'lifterlms_course_completed':\n\t\t\tcase 'lifterlms_course_track_completed':\n\t\t\tcase 'lifterlms_lesson_completed':\n\t\t\tcase 'lifterlms_section_completed':\n\t\t\tcase 'lifterlms_quiz_completed':\n\t\t\tcase 'lifterlms_quiz_passed':\n\t\t\tcase 'lifterlms_quiz_failed':\n\t\t\t\t$trigger_type = str_replace( 'lifterlms_', '', $action );\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_user_added_to_membership_level':\n\t\t\tcase 'llms_user_enrolled_in_course':\n\t\t\t\t$trigger_type = str_replace( 'llms_', '', get_post_type( $related_post_id ) ) . '_enrollment';\n\t\t\t\tbreak;\n\n\t\t\tcase 'lifterlms_access_plan_purchased':\n\t\t\tcase 'lifterlms_product_purchased':\n\t\t\t\t$trigger_type = str_replace( 'llms_', '', get_post_type( $related_post_id ) ) . '_purchased';\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn $trigger_type;\n\t}\n\n\t/**\n\t * Handles all actions that could potentially trigger an engagement\n\t *\n\t * It will fire or schedule the actions after gathering all necessary data.\n\t *\n\t * @since 2.3.0\n\t * @since 3.11.0 Unknown.\n\t * @since 3.39.0 Treat also `llms_rest_student_registered` action.\n\t * @since 6.0.0 Major refactor to reduce code complexity.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_trigger_engagement() {\n\n\t\t// Parse incoming hook data.\n\t\t$hook = $this->parse_hook( current_filter(), func_get_args() );\n\n\t\t// We need a user and a trigger to proceed, related_post is optional though.\n\t\tif ( ! $hook['user_id'] || ! $hook['trigger_type'] ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Gather triggerable engagements matching the supplied criteria.\n\t\t$engagements = $this->get_engagements( $hook['trigger_type'], $hook['related_post_id'] );\n\n\t\t// Loop through the retrieved engagements and trigger them.\n\t\tforeach ( $engagements as $engagement ) {\n\n\t\t\t$handler = $this->parse_engagement( $engagement, $hook );\n\t\t\t$this->trigger_engagement( $handler, $engagement->delay );\n\n\t\t}\n\t}\n\n\t/**\n\t * Parse engagement objects from the DB and return data needed to trigger the engagements.\n\t *\n\t * @since 6.0.0\n\t * @since 6.6.0 Fixed an issue where the `lifterlms_external_engagement_handler_arguments` filter\n\t *              would not trigger if a 3rd party registered an engagement type.\n\t *\n\t * @param object $engagement   The engagement object from the `get_engagements()` query.\n\t * @param array  $trigger_data Parsed hook data from `parse_hook()`.\n\t * @return array {\n\t *     An associative array of parsed data used to trigger the engagement.\n\t *\n\t *     @type string $handler_action Hook name of the action that will handle awarding the sending the engagement.\n\t *     @type array  $handler_args   Arguments passed to the `$handler_action` callback.\n\t *  }\n\t */\n\tprivate function parse_engagement( $engagement, $trigger_data ) {\n\n\t\t$parsed = array(\n\t\t\t'handler_action' => null,\n\t\t\t'handler_args'   => null,\n\t\t);\n\n\t\t/**\n\t\t * Enable 3rd parties to parse custom engagement types.\n\t\t *\n\t\t * @since 2.3.0\n\t\t *\n\t\t * @param array $parsed {\n\t\t *     An associative array of parsed data used to trigger the engagement.\n\t\t *\n\t\t *     @type string $handler_action (Required) Hook name of the action that will handle awarding the sending the engagement.\n\t\t *     @type array  $handler_args   (Required) Arguments passed to the `$handler_action` callback.\n\t\t * }\n\t\t * @param object $engagement      The engagement object from the `get_engagements()` query.\n\t\t * @param int    $user_id         WP_User ID who will be awarded the engagement.\n\t\t * @param int    $related_post_id WP_Post ID of the related post.\n\t\t * @param string $event_type      The type of engagement event.\n\t\t */\n\t\t$filtered_parsed = apply_filters(\n\t\t\t'lifterlms_external_engagement_handler_arguments',\n\t\t\t$parsed,\n\t\t\t$engagement,\n\t\t\t$trigger_data['user_id'],\n\t\t\t$trigger_data['related_post_id'],\n\t\t\t$engagement->event_type\n\t\t);\n\t\t// If valid, return the filtered parsed data.\n\t\tif ( isset( $filtered_parsed['handler_action'] ) && isset( $filtered_parsed['handler_args'] ) ) {\n\t\t\treturn $filtered_parsed;\n\t\t}\n\n\t\t// Verify that the engagement event type is supported.\n\t\tif ( ! array_key_exists( $engagement->event_type, llms_get_engagement_types() ) ) {\n\t\t\treturn $parsed;\n\t\t}\n\n\t\t$parsed['handler_args'] = array(\n\t\t\t$trigger_data['user_id'],\n\t\t\t$engagement->engagement_id,\n\t\t\t$trigger_data['related_post_id'],\n\t\t\tabsint( $engagement->trigger_id ),\n\t\t);\n\n\t\t/**\n\t\t * @todo Fix this\n\t\t *\n\t\t * If there's no related post id we have to send one anyway for certs to work.\n\t\t *\n\t\t * This would only be for registration events @ version 2.3.0 so we pass the engagement_id twice until we find a better solution.\n\t\t */\n\t\tif ( 'certificate' === $engagement->event_type && empty( $parsed['handler_args'][2] ) ) {\n\t\t\t$parsed['handler_args'][2] = $parsed['handler_args'][1];\n\t\t}\n\n\t\t$parsed['handler_action'] = sprintf(\n\t\t\t'lifterlms_engagement_%1$s_%2$s',\n\t\t\t'email' === $engagement->event_type ? 'send' : 'award',\n\t\t\t$engagement->event_type\n\t\t);\n\n\t\treturn $parsed;\n\t}\n\n\t/**\n\t * Triggers or schedules an engagement\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $data  Handler data from `parse_engagement()`.\n\t * @param int   $delay The engagement send delay (in days).\n\t * @return void\n\t */\n\tprivate function trigger_engagement( $data, $delay ) {\n\n\t\t// Can't proceed without an action and a handler.\n\t\tif ( empty( $data['handler_action'] ) || empty( $data['handler_args'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If we have a delay, schedule the engagement handler.\n\t\t$delay = absint( $delay );\n\t\tif ( $delay ) {\n\n\t\t\tas_schedule_single_action(\n\t\t\t\tcurrent_datetime()->modify( \"+{$delay} days\" )->getTimestamp(),\n\t\t\t\t$data['handler_action'],\n\t\t\t\tarray( $data['handler_args'] ),\n\t\t\t\t! empty( $data['handler_args'][3] ) ? $this->get_delayed_group_id( $data['handler_args'][3] ) : null\n\t\t\t);\n\n\t\t} else {\n\n\t\t\t/**\n\t\t\t * Skip processing checks for immediate engagements.\n\t\t\t *\n\t\t\t * We know the user exists (because they're currently logged in) and we don't have to run\n\t\t\t * publish/existence checks on all the related posts because the `get_engagement()` query takes care\n\t\t\t * of that already.\n\t\t\t */\n\t\t\tadd_filter( 'llms_skip_engagement_processing_checks', '__return_true' );\n\n\t\t\tdo_action( $data['handler_action'], $data['handler_args'] );\n\n\t\t\tremove_filter( 'llms_skip_engagement_processing_checks', '__return_true' );\n\n\t\t}\n\t}\n\n\t/**\n\t * Unschedule all scheduled actions for a delayed engagement\n\t *\n\t * This is the callback function for deleted engagement posts.\n\t *\n\t * The `deleted_post` action param `$post` has been added since WordPress 5.5.0.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int          $post_id WP_Post ID.\n\t * @param WP_Post|null $post    Post object of the deleted post.\n\t * @return void\n\t */\n\tpublic function unschedule_delayed_engagements( $post_id, $post = null ) {\n\n\t\t// @todo Remove compatibility with WP < 5.5 when bumping the minimum WP required version to 5.5+\n\t\t$post_type = $post ? $post->post_type : get_post_type( $post_id );\n\n\t\tif ( 'llms_engagement' === $post_type ) {\n\t\t\tas_unschedule_all_actions( '', array(), $this->get_delayed_group_id( $post_id ) );\n\t\t}\n\t}\n\n\t/**\n\t * Log debug data to the WordPress debug.log file\n\t *\n\t * @since 2.7.9\n\t * @since 3.12.0 Unknown.\n\t * @deprecated 6.0.0 Engagement debug logging is removed. Use `llms_log()` directly instead.\n\t *\n\t * @param mixed $log Data to write to the log.\n\t * @return void\n\t */\n\tpublic function log( $log ) {\n\n\t\t_deprecated_function( 'LLMS_Engagements::log()', '6.0.0', 'llms_log()' );\n\n\t\tif ( $this->debug ) {\n\t\t\tllms_log( $log, 'engagements' );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.frontend.assets.php",
    "content": "<?php\n/**\n * Frontend scripts class\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Frontend_Assets\n *\n * @since 1.0.0\n * @since 3.35.0 Explicitly define asset versions.\n * @since 3.36.0 Localize tracking with client-side settings.\n * @since 4.0.0 Remove JS dependencies \"collapse\" and \"transition\".\n * @since 4.4.0 Method `enqueue_inline_script()` is deprecated in favor of `LLMS_Assets::enqueue_inline()`.\n *              Method `is_inline_script_enqueued()` is deprecated in favor of `LLMS_Frontend_Assets::is_inline_enqueued()`.\n *              Private properties `$enqueued_inline_scripts` and `$inline_scripts` have been removed.\n *              Removed private methods `get_inline_scripts()` and `output_inline_scripts()`.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Frontend_Assets::enqueue_inline_pw_script()` method\n *              - `LLMS_Frontend_Assets::enqueue_inline_script()` method\n *              - `LLMS_Frontend_Assets::is_inline_script_enqueued()` method\n */\nclass LLMS_Frontend_Assets {\n\n\t/**\n\t * Inline script ids that have been enqueued.\n\t *\n\t * @var  array\n\t */\n\tprivate static $enqueued_inline_scripts = array();\n\n\t/**\n\t * Array of inline scripts to be output in the header / footer respectively.\n\t *\n\t * @var  array\n\t */\n\tprivate static $inline_scripts = array(\n\t\t'header' => array(),\n\t\t'footer' => array(),\n\t);\n\n\t/**\n\t * Initializer\n\t *\n\t * Replaces non-static __construct() from 3.4.0 & lower.\n\t *\n\t * @since 3.4.1\n\t * @since 3.17.5 Unknown.\n\t * @since 5.6.0 Add content protection inline script enqueue.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tadd_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_styles' ) );\n\t\tadd_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );\n\t\tadd_action( 'wp_head', array( __CLASS__, 'output_header_scripts' ) );\n\t\tadd_action( 'wp_print_footer_scripts', array( __CLASS__, 'output_footer_scripts' ), 1 );\n\t\tadd_action( 'wp', array( __CLASS__, 'enqueue_content_protection' ) );\n\t}\n\n\t/**\n\t * Enqueue inline copy prevention scripts.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic static function enqueue_content_protection() {\n\n\t\t$allow_copying = ! llms_parse_bool( get_option( 'lifterlms_content_protection', 'no' ) ) || llms_can_user_bypass_restrictions( get_current_user_id() );\n\n\t\t/**\n\t\t * Filters whether or not content prevention is enabled.\n\t\t *\n\t\t * This hook runs on the `wp` action, at this point the current user is available and\n\t\t * the global `$post` has already been set up.\n\t\t *\n\t\t * @since 5.6.0\n\t\t *\n\t\t * @param boolean $allow_copying Whether or not copying is allowed. If `true`, copying\n\t\t *                               is allowed, otherwise copy prevention scripts are\n\t\t *                               loaded.\n\t\t */\n\t\t$allow_copying = apply_filters( 'llms_skip_content_prevention', $allow_copying );\n\n\t\tif ( $allow_copying ) {\n\t\t\treturn;\n\t\t}\n\n\t\tob_start();\n\t\t?>\n\t\t( function(){\n\t\t\tfunction dispatchEvent( type ) {\n\t\t\t\tdocument.dispatchEvent( new Event( type ) );\n\t\t\t}\n\t\t\tdocument.addEventListener( 'copy', function( event ) {\n\t\t\t\t// Allow copying if the target is an input or textarea element\n\t\t\t\tif (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {\n\t\t\t\t\treturn; // Let the default copy behavior proceed\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Prevent copying outside input/textarea elements\n\t\t\t\tevent.preventDefault();\n\t\t\t\tevent.clipboardData.setData( 'text/plain', '<?php echo esc_html__( 'Copying is not allowed.', 'lifterlms' ); ?>' );\n\t\t\t\tdispatchEvent( 'llms-copy-prevented' );\n\t\t\t}, false );\n\t\t\tdocument.addEventListener( 'contextmenu', function( event ) {\n\t\t\t\t// Prevent right-click context menu on images\n\t\t\t\tif ( event.target && 'IMG' === event.target.nodeName ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tdispatchEvent( 'llms-context-prevented' );\n\t\t\t\t}\n\t\t\t}, false );\n\t\t} )();\n\t\t<?php\n\t\t$script = ob_get_clean();\n\t\tllms()->assets->enqueue_inline( 'llms-integrity', $script, 'header' );\n\t}\n\n\t/**\n\t * Enqueue Styles\n\t *\n\t * @since 1.0.0\n\t * @since 3.18.0 Unknown.\n\t * @since 3.35.0 Explicitly define asset versions.\n\t * @since 4.4.0 Enqueue & register scripts using `LLMS_Assets` methods.\n\t * @since 5.0.0 Enqueue select2 on account and checkout pages for searchable dropdowns for country & state.\n\t *\n\t * @return void\n\t */\n\tpublic static function enqueue_styles() {\n\n\t\tglobal $post_type;\n\n\t\tllms()->assets->register_style( 'llms-iziModal' );\n\n\t\tllms()->assets->enqueue_style( 'webui-popover' );\n\t\tllms()->assets->enqueue_style( 'lifterlms-styles' );\n\n\t\tif ( in_array( $post_type, array( 'llms_my_certificate', 'llms_certificate' ), true ) ) {\n\t\t\tllms()->assets->enqueue_style( 'certificates' );\n\t\t} elseif ( is_llms_account_page() ) {\n\t\t\tllms()->assets->enqueue_style( 'llms-iziModal' );\n\t\t} elseif ( is_singular( 'llms_quiz' ) ) {\n\t\t\twp_enqueue_style( 'wp-mediaelement' );\n\t\t}\n\n\t\tif ( is_llms_account_page() || is_llms_checkout() ) {\n\t\t\tllms()->assets->enqueue_style( 'llms-select2-styles' );\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue Scripts\n\t *\n\t * @since 1.0.0\n\t * @since 3.22.0 Unknown.\n\t * @since 3.35.0 Explicitly define asset versions.\n\t * @since 3.36.0 Localize tracking with client-side settings.\n\t * @since 4.0.0 Remove dependencies \"collapse\" and \"transition\".\n\t * @since 4.4.0 Enqueue & register scripts using `LLMS_Assets` methods.\n\t *              Add Add `window.llms.ajax_nonce` data to replace `wp_ajax_data.nonce`.\n\t *              Moved inline scripts to `enqueue_inline_scripts()`.\n\t * @since 5.0.0 Enqueue locale data and dependencies on account and checkout pages for searchable dropdowns for country & state.\n\t *               Remove password strength inline enqueue.\n\t * @since 7.5.0 Enqueue `llms-favorites` script on lesson and course page.\n\t *\n\t * @return void\n\t */\n\tpublic static function enqueue_scripts() {\n\n\t\t// I don't think we need these next 3 scripts.\n\t\twp_enqueue_script( 'jquery-ui-tooltip' );\n\t\twp_enqueue_script( 'jquery-ui-datepicker' );\n\t\twp_enqueue_script( 'jquery-ui-slider' );\n\n\t\tllms()->assets->enqueue_script( 'webui-popover' );\n\n\t\tllms()->assets->register_script( 'llms-jquery-matchheight' );\n\t\tif ( is_llms_account_page() || is_course() || is_membership() || is_lesson() || is_memberships() || is_courses() || is_tax( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) ) ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-jquery-matchheight' );\n\t\t}\n\n\t\tllms()->assets->enqueue_script( 'llms' );\n\n\t\tllms()->assets->register_script( 'llms-notifications' );\n\t\tif ( get_current_user_id() ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-notifications' );\n\t\t}\n\n\t\t// Doesn't seem like there's any reason to enqueue this script on the frontend.\n\t\twp_enqueue_script( 'llms-ajax', LLMS_PLUGIN_URL . 'assets/js/llms-ajax' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), llms()->version, true );\n\n\t\t// I think we only need this on account and checkout pages.\n\t\tllms()->assets->enqueue_script( 'llms-form-checkout' );\n\n\t\tif ( is_singular( 'llms_quiz' ) ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-quiz' );\n\t\t}\n\n\t\tllms()->assets->register_script( 'llms-favorites' );\n\t\tif ( ( is_lesson() || is_course() ) && true === llms_is_favorites_enabled() ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-favorites' );\n\t\t}\n\n\t\tllms()->assets->register_script( 'llms-iziModal' );\n\t\tif ( is_llms_account_page() ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-iziModal' );\n\t\t}\n\n\t\tself::enqueue_inline_scripts();\n\t\tself::enqueue_locale_scripts();\n\t}\n\n\t/**\n\t * Enqueue inline scripts.\n\t *\n\t * @since 4.4.0\n\t * @since 7.0.0 Include checkout page script data for AJAX-powered gateways.\n\t *\n\t * @return void\n\t */\n\tprotected static function enqueue_inline_scripts() {\n\n\t\t// Ensure the main llms object exists.\n\t\tllms()->assets->enqueue_inline( 'llms-obj', 'window.llms = window.llms || {};', 'footer', 5 );\n\n\t\t// Define inline scripts.\n\t\t$scripts = array(\n\t\t\t'llms-ajaxurl'           => 'window.llms.ajaxurl = \"' . admin_url( 'admin-ajax.php', is_ssl() ? 'https' : 'http' ) . '\";',\n\t\t\t'llms-ajax-nonce'        => 'window.llms.ajax_nonce = \"' . wp_create_nonce( LLMS_AJAX::NONCE ) . '\";',\n\t\t\t'llms-tracking-settings' => \"window.llms.tracking = '\" . wp_json_encode( llms()->events()->get_client_settings() ) . \"';\",\n\t\t\t'llms-LLMS-obj'          => 'window.LLMS = window.LLMS || {};',\n\t\t\t'llms-l10n'              => 'window.LLMS.l10n = window.LLMS.l10n || {}; window.LLMS.l10n.strings = ' . LLMS_L10n::get_js_strings( true ) . ';',\n\t\t);\n\n\t\t$checkout_urls = self::get_checkout_urls();\n\t\tif ( ! empty( $checkout_urls ) ) {\n\t\t\t$scripts['llms-checkout-urls'] = \"window.llms.checkoutUrls = JSON.parse( '\" . wp_json_encode( $checkout_urls ) . \"' );\";\n\t\t}\n\n\t\t// Enqueue them.\n\t\tforeach ( $scripts as $handle => $script ) {\n\t\t\tllms()->assets->enqueue_inline( $handle, $script, 'footer' );\n\t\t}\n\t}\n\n\t/**\n\t * Enqueue dependencies and inline script data for form localization\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprotected static function enqueue_locale_scripts() {\n\n\t\tif ( is_llms_account_page() || is_llms_checkout() ) {\n\t\t\tllms()->assets->enqueue_script( 'llms-select2' );\n\t\t\tllms()->assets->enqueue_inline(\n\t\t\t\t'llms-countries-locale',\n\t\t\t\t\"window.llms.address_info = '\" . wp_json_encode( llms_get_countries_address_info() ) . \"';\",\n\t\t\t\t'footer',\n\t\t\t\t20\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Retrieves AJAX checkout URLs used for checkout and switching payment source on the student dashboard.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_checkout_urls() {\n\t\t$urls       = array();\n\t\t$controller = LLMS_Controller_Checkout::instance();\n\n\t\tif ( is_llms_checkout() ) {\n\t\t\t$urls = array(\n\t\t\t\t'createPendingOrder'  => $controller->get_url( $controller::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t\t'confirmPendingOrder' => $controller->get_url( $controller::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t);\n\t\t} elseif ( is_llms_account_page() && 'orders' === LLMS_Student_Dashboard::get_current_tab( 'slug' ) && is_numeric( get_query_var( 'orders', false ) ) ) {\n\t\t\t$urls = array(\n\t\t\t\t'switchPaymentSource' => $controller->get_url( $controller::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t);\n\t\t}\n\n\t\treturn $urls;\n\t}\n\n\t/**\n\t * Output inline scripts in the footer\n\t *\n\t * @since 3.4.1\n\t * @since 4.4.0 Use `LLMS_Assets::output_inline()` to output scripts.\n\t *\n\t * @return void\n\t */\n\tpublic static function output_footer_scripts() {\n\t\tllms()->assets->output_inline( 'footer' );\n\t}\n\n\t/**\n\t * Output inline scripts in the header\n\t *\n\t * @since 3.4.1\n\t * @since 4.4.0 Use `LLMS_Assets::output_inline()` to output scripts.\n\t *\n\t * @return void\n\t */\n\tpublic static function output_header_scripts() {\n\t\tllms()->assets->output_inline( 'header' );\n\t}\n}\n\nreturn LLMS_Frontend_Assets::init();\n"
  },
  {
    "path": "includes/class.llms.gateway.manual.php",
    "content": "<?php\n/**\n * Manual Payment Gateway Class\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Manual Payment Gateway Class\n *\n * @since 3.0.0\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Payment_Gateway_Manual extends LLMS_Payment_Gateway {\n\n\t/**\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tpublic $payment_instructions;\n\n\t/**\n\t * Constructor\n\t *\n\t * @return  void\n\t * @since   3.0.0\n\t * @version 3.10.0\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id                   = 'manual';\n\t\t$this->admin_description    = __( 'Collect manual or offline payments. Also handles any free orders during checkout.', 'lifterlms' );\n\t\t$this->admin_title          = __( 'Manual', 'lifterlms' );\n\t\t$this->title                = __( 'Manual', 'lifterlms' );\n\t\t$this->description          = __( 'Pay manually via check', 'lifterlms' );\n\t\t$this->payment_instructions = ''; // Fields.\n\n\t\t$this->supports = array(\n\t\t\t'checkout_fields'    => false,\n\t\t\t'refunds'            => false, // Manual refunds are available always for all gateways and are not handled by this class.\n\t\t\t'single_payments'    => true,\n\t\t\t'recurring_payments' => true,\n\t\t\t'test_mode'          => false,\n\t\t);\n\n\t\tadd_filter( 'llms_get_gateway_settings_fields', array( $this, 'get_settings_fields' ), 10, 2 );\n\t\tadd_action( 'lifterlms_before_view_order_table', array( $this, 'before_view_order_table' ) );\n\n\t}\n\n\t/**\n\t * Output payment instructions if the order is pending.\n\t *\n\t * @since 3.0.0\n\t * @since 6.4.0 Allowed classes extended from this manual payment gateway class to display payment instructions.\n\t *\n\t * @return void\n\t */\n\tpublic function before_view_order_table() {\n\n\t\tglobal $wp;\n\n\t\tif ( ! empty( $wp->query_vars['orders'] ) ) {\n\n\t\t\t$order = new LLMS_Order( intval( $wp->query_vars['orders'] ) );\n\n\t\t\tif (\n\t\t\t\t$order->get( 'payment_gateway' ) === $this->id &&\n\t\t\t\tin_array( $order->get( 'status' ), array( 'llms-pending', 'llms-on-hold', true ) )\n\t\t\t) {\n\t\t\t\techo wp_kses_post( $this->get_payment_instructions() );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Get fields displayed on the checkout form\n\t *\n\t * @return   string\n\t * @since    3.0.0\n\t * @version  3.7.5\n\t */\n\tpublic function get_payment_instructions() {\n\t\t$opt = $this->get_option( 'payment_instructions' );\n\t\tif ( $opt ) {\n\t\t\t$fields = '<div class=\"llms-notice llms-info\"><h3>' . esc_html__( 'Payment Instructions', 'lifterlms' ) . '</h3>' . wpautop( wptexturize( wp_kses_post( $opt ) ) ) . '</div>';\n\t\t} else {\n\t\t\t$fields = '';\n\t\t}\n\t\treturn apply_filters( 'llms_get_payment_instructions', $fields, $this->id );\n\t}\n\n\t/**\n\t * Get admin setting fields\n\t *\n\t * @param    array  $fields      default fields\n\t * @param    string $gateway_id  gateway ID\n\t * @return   array\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function get_settings_fields( $fields, $gateway_id ) {\n\n\t\tif ( $this->id !== $gateway_id ) {\n\t\t\treturn $fields;\n\t\t}\n\n\t\t$fields[] = array(\n\t\t\t'id'    => $this->get_option_name( 'payment_instructions' ),\n\t\t\t'desc'  => '<br>' . __( 'Displayed to the user when this gateway is selected during checkout. Add information here instructing the student on how to send payment.', 'lifterlms' ),\n\t\t\t'title' => __( 'Payment Instructions', 'lifterlms' ),\n\t\t\t'type'  => 'textarea',\n\t\t);\n\n\t\treturn $fields;\n\n\t}\n\n\t/**\n\t * Called when the Update Payment Method form is submitted from a single order view on the student dashboard\n\t *\n\t * Gateways should do whatever the gateway needs to do to validate the new payment method and save it to the order\n\t * so that future payments on the order will use this new source\n\t *\n\t * @param    obj   $order      Instance of the LLMS_Order\n\t * @param    array $form_data  Additional data passed from the submitted form (EG $_POST)\n\t * @return   void\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tpublic function handle_payment_source_switch( $order, $form_data = array() ) {\n\n\t\t$previous_gateway = $order->get( 'payment_gateway' );\n\n\t\tif ( $this->get_id() === $previous_gateway ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$order->set( 'payment_gateway', $this->get_id() );\n\t\t$order->set( 'gateway_customer_id', '' );\n\t\t$order->set( 'gateway_source_id', '' );\n\t\t$order->set( 'gateway_subscription_id', '' );\n\n\t\t$order->add_note( sprintf( __( 'Payment method switched from \"%1$s\" to \"%2$s\"', 'lifterlms' ), $previous_gateway, $this->get_admin_title() ) );\n\n\t}\n\n\t/**\n\t * Handle a Pending Order.\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t * @since 6.4.0 Use `llms_redirect_and_exit()` in favor of `wp_redirect()` and `exit()`.\n\t *\n\t * @param LLMS_Order          $order   Order object.\n\t * @param LLMS_Access_Plan    $plan    Access plan object.\n\t * @param LLMS_Student        $student Student object.\n\t * @param LLMS_Coupon|boolean $coupon  Coupon object or `false` when no coupon is being used for the order.\n\t * @return void\n\t */\n\tpublic function handle_pending_order( $order, $plan, $student, $coupon = false ) {\n\n\t\t// Free orders (no payment is due).\n\t\tif ( floatval( 0 ) === $order->get_initial_price( array(), 'float' ) ) {\n\n\t\t\t// Free access plans do not generate receipts.\n\t\t\tif ( $plan->is_free() ) {\n\n\t\t\t\t$order->set( 'status', 'llms-completed' );\n\n\t\t\t\t// Free trial, reduced to free via coupon, etc....\n\t\t\t\t// We do want to record a transaction and then generate a receipt.\n\t\t\t} else {\n\n\t\t\t\t// Record a $0.00 transaction to ensure a receipt is sent.\n\t\t\t\t$order->record_transaction(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'amount'             => floatval( 0 ),\n\t\t\t\t\t\t'source_description' => __( 'Free', 'lifterlms' ),\n\t\t\t\t\t\t'transaction_id'     => uniqid(),\n\t\t\t\t\t\t'status'             => 'llms-txn-succeeded',\n\t\t\t\t\t\t'payment_gateway'    => 'manual',\n\t\t\t\t\t\t'payment_type'       => 'single',\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t}\n\n\t\t\treturn $this->complete_transaction( $order );\n\n\t\t}\n\n\t\t/**\n\t\t * Action triggered when a manual payment is due.\n\t\t *\n\t\t * @hooked LLMS_Notification: manual_payment_due - 10\n\t\t *\n\t\t * @since Unknown.\n\t\t *\n\t\t * @param LLMS_Order                  $order   The order object.\n\t\t * @param LLMS_Payment_Gateway_Manual $gateway Manual gateway instance.\n\t\t */\n\t\tdo_action( 'llms_manual_payment_due', $order, $this );\n\n\t\t/**\n\t\t * Action triggered when the pending order processing has been completed.\n\t\t *\n\t\t * @since Unknown.\n\t\t *\n\t\t * @param LLMS_Order $order The order object.\n\t\t */\n\t\tdo_action( 'lifterlms_handle_pending_order_complete', $order );\n\n\t\tllms_redirect_and_exit( $order->get_view_link() );\n\n\t}\n\n\t/**\n\t * Called by scheduled actions to charge an order for a scheduled recurring transaction\n\t * This function must be defined by gateways which support recurring transactions\n\t *\n\t * @param    obj $order   Instance LLMS_Order for the order being processed\n\t * @return   mixed\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tpublic function handle_recurring_transaction( $order ) {\n\n\t\t// Switch to order on hold if it's a paid order.\n\t\tif ( $order->get_price( 'total', array(), 'float' ) > 0 ) {\n\n\t\t\t// Update status.\n\t\t\t$order->set_status( 'on-hold' );\n\n\t\t\t/**\n\t\t\t * @hooked LLMS_Notification: manual_payment_due - 10\n\t\t\t */\n\t\t\tdo_action( 'llms_manual_payment_due', $order, $this );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Determine if the gateway is enabled according to admin settings checkbox\n\t *\n\t * @return   boolean\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function is_enabled() {\n\t\treturn ( 'yes' === $this->get_enabled() ) ? true : false;\n\t}\n\n\n}\n"
  },
  {
    "path": "includes/class.llms.generator.php",
    "content": "<?php\n/**\n * Generate LMS Content from export files or raw arrays of data\n *\n * @package LifterLMS/Classes\n *\n * @since 3.3.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Generator class.\n *\n * @since 3.3.0\n * @since 3.30.2 Added hooks and made numerous private functions public to expand extendability.\n * @since 3.36.3 New method: is_generator_valid()\n *               Bugfix: Fix return of `set_generator()`.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Generator::add_custom_values()` method\n *              - `LLMS_Generator::format_date()` method\n *              - `LLMS_Generator::get_author_id_from_raw()` method\n *              - `LLMS_Generator::get_default_post_status()` method\n *              - `LLMS_Generator::get_generated_posts()` method\n *              - `LLMS_Generator::increment()` method\n */\nclass LLMS_Generator {\n\n\t/**\n\t * Courses generator subclass instance\n\t *\n\t * @var LLMS_Generator_Courses\n\t */\n\tprotected $courses_generator;\n\n\t/**\n\t * Instance of WP_Error\n\t *\n\t * @var obj\n\t */\n\tpublic $error;\n\n\t/**\n\t * Array of generated objects.\n\t *\n\t * @var array\n\t */\n\tprotected $generated = array();\n\n\t/**\n\t * Name of the Generator to use for generation\n\t *\n\t * @var string\n\t */\n\tprotected $generator = '';\n\n\t/**\n\t * Raw contents passed into the generator's constructor\n\t *\n\t * @var array\n\t */\n\tprotected $raw = array();\n\n\t/**\n\t * Construct a new generator instance with data\n\t *\n\t * @since 3.3.0\n\t * @since 4.7.0 Move most logic into helper functions.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @param array|string $raw Array or a JSON string of raw content.\n\t * @return void\n\t */\n\tpublic function __construct( $raw ) {\n\n\t\t// Load generator class.\n\t\t$this->courses_generator = new LLMS_Generator_Courses();\n\n\t\t// Parse raw data.\n\t\t$this->raw = $this->parse_raw( $raw );\n\n\t\t// Instantiate an empty error object.\n\t\t$this->error = new WP_Error();\n\n\t\t// Add hooks.\n\t\t$this->add_hooks();\n\n\t}\n\n\t/**\n\t * Add actions and filters used by the class.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tprotected function add_hooks() {\n\n\t\t// Watch creation of things, used on generation completion to return results of created objects.\n\t\tforeach ( array( 'access_plan', 'course', 'section', 'lesson', 'quiz', 'question', 'term', 'user' ) as $type ) {\n\t\t\tadd_action( 'llms_generator_new_' . $type, array( $this, 'object_created' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * When called, generates raw content based on the defined generator\n\t *\n\t * @since 3.3.0\n\t * @since 3.30.2 Add before and after generation hooks.\n\t * @since 4.7.0 Return early if not generator is set.\n\t *\n\t * @return void\n\t */\n\tpublic function generate() {\n\n\t\tif ( empty( $this->generator ) ) {\n\t\t\treturn $this->error->add( 'missing-generator', __( 'No generator supplied.', 'lifterlms' ) );\n\t\t}\n\n\t\tglobal $wpdb;\n\n\t\t$wpdb->hide_errors();\n\n\t\t$wpdb->query( 'START TRANSACTION' ); // db call ok; no-cache ok.\n\n\t\t/**\n\t\t * Action run immediately prior to a LifterLMS Generator running.\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param LLMS_Generator $generator The generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_before_generate', $this );\n\n\t\ttry {\n\t\t\tcall_user_func( $this->generator, $this->raw );\n\t\t} catch ( Exception $exception ) {\n\t\t\t$this->error->add( $this->get_error_code( $exception->getCode(), $this->generator[0] ), $exception->getMessage(), $exception->getTrace() );\n\t\t}\n\n\t\t/**\n\t\t * Action run immediately after a LifterLMS Generator running.\n\t\t *\n\t\t * @since 3.30.2\n\t\t *\n\t\t * @param LLMS_Generator $generator The generator instance.\n\t\t */\n\t\tdo_action( 'llms_generator_after_generate', $this );\n\n\t\tif ( $this->is_error() ) {\n\t\t\t$wpdb->query( 'ROLLBACK' ); // db call ok; no-cache ok.\n\t\t} else {\n\t\t\t$wpdb->query( 'COMMIT' ); // db call ok; no-cache ok.\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieve a human-readable error code from a machine-readable error number\n\t *\n\t * @since 4.7.0\n\t * @since 4.9.0 Handle PHP core errors, warnings, notices, etc... with a human-readable error code.\n\t *\n\t * @param int $code  Error number.\n\t * @param obj $class Generator class instance.\n\t * @return string A human-readable error code.\n\t */\n\tprotected function get_error_code( $code, $class ) {\n\n\t\t// See if the error code is a native php exception code constant.\n\t\t$ret = llms_php_error_constant_to_code( $code );\n\n\t\t// Code is not a native PHP exception code.\n\t\tif ( is_numeric( $ret ) ) {\n\n\t\t\t$reflect   = new ReflectionClass( $class );\n\t\t\t$constants = array_flip( $reflect->getConstants() );\n\t\t\t$ret       = isset( $constants[ $code ] ) ? $constants[ $code ] : 'ERROR_UNKNOWN';\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the human-readable error retrieved from a given error code\n\t\t *\n\t\t * @since 4.9.0\n\t\t *\n\t\t * @param string $ret   The human-readable error code.\n\t\t * @param int    $code  The initial error code as an integer.\n\t\t * @param obj    $class Generator class instance.\n\t\t */\n\t\treturn apply_filters( 'llms_generator_get_error_code', $ret, $code, $class );\n\n\t}\n\n\t/**\n\t * Retrieves a multi-dimensional array of content generated by the most class\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return array Returns an associative array where the keys are the object type and the values are an array of integers representing the generated object IDs.\n\t */\n\tpublic function get_generated_content() {\n\t\treturn $this->generated;\n\t}\n\n\t/**\n\t * Retrieve the array of generated course ids\n\t *\n\t * @since 3.7.3\n\t * @since 3.14.8 Unknown.\n\t * @since 4.7.0 Access generated posts from the `$generated` property in favor of the removed `$posts` property.\n\t *\n\t * @return array\n\t */\n\tpublic function get_generated_courses() {\n\t\tif ( isset( $this->generated['course'] ) ) {\n\t\t\treturn $this->generated['course'];\n\t\t}\n\t\treturn array();\n\t}\n\n\t/**\n\t * Get an array of valid LifterLMS generators\n\t *\n\t * @since 3.3.0\n\t * @since 3.14.8 Unknown.\n\t * @since 4.7.0 Load generators from `LLMS_Generator_Courses()`.\n\t * @since 4.13.0 Use `clone_course()` method for cloning courses in favor of `genrate_course()`.\n\t *\n\t * @return array\n\t */\n\tprotected function get_generators() {\n\n\t\t/**\n\t\t * Filter the list of available generators.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array[] $generators Array of generators. Array key is the generator name and the array value is a callable function.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_generators',\n\t\t\tarray(\n\t\t\t\t'LifterLMS/BulkCourseExporter'    => array( $this->courses_generator, 'generate_courses' ),\n\t\t\t\t'LifterLMS/BulkCourseGenerator'   => array( $this->courses_generator, 'generate_courses' ),\n\t\t\t\t'LifterLMS/SingleCourseCloner'    => array( $this->courses_generator, 'clone_course' ),\n\t\t\t\t'LifterLMS/SingleCourseExporter'  => array( $this->courses_generator, 'generate_course' ),\n\t\t\t\t'LifterLMS/SingleCourseGenerator' => array( $this->courses_generator, 'generate_course' ),\n\t\t\t\t'LifterLMS/SingleLessonCloner'    => array( $this->courses_generator, 'clone_lesson' ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Get the results of the generate function\n\t *\n\t * @since 3.3.0\n\t * @since 4.7.0 Return generated stats from `$this->stats()` instead of from removed `$stats` property.\n\t *\n\t * @return int[]|WP_Error Array of stats on success and an error object on failure.\n\t */\n\tpublic function get_results() {\n\n\t\tif ( $this->is_error() ) {\n\t\t\treturn $this->error;\n\t\t}\n\n\t\treturn $this->get_stats();\n\n\t}\n\n\t/**\n\t * Get \"stats\" about the generated content.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_stats() {\n\n\t\t$stats = array();\n\t\tforeach ( $this->generated as $type => $ids ) {\n\t\t\t$stats[ $type ] = count( $ids );\n\t\t}\n\n\t\t// Add old plural keys that were guaranteed to exist.\n\t\t$backwards_compat = array(\n\t\t\t'course'      => 'courses',\n\t\t\t'section'     => 'sections',\n\t\t\t'lesson'      => 'lessons',\n\t\t\t'access_plan' => 'plans',\n\t\t\t'quiz'        => 'quizzes',\n\t\t\t'question'    => 'questions',\n\t\t\t'term'        => 'terms',\n\t\t\t'user'        => 'authors',\n\t\t);\n\t\tforeach ( $backwards_compat as $curr => $old ) {\n\t\t\t$stats[ $old ] = isset( $stats[ $curr ] ) ? $stats[ $curr ] : 0;\n\t\t}\n\n\t\treturn $stats;\n\n\t}\n\n\t/**\n\t * Determines if there was an error during the running of the generator\n\t *\n\t * @since 3.3.0\n\t * @since 3.16.11 Unknown.\n\t *\n\t * @return boolean Returns `true` when there was an error and `false` if there's no errors.\n\t */\n\tpublic function is_error() {\n\t\treturn ( 0 !== count( $this->error->get_error_messages() ) );\n\t}\n\n\t/**\n\t * Determine if a generator is a valid generator.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @param string $generator Generator name.\n\t * @return bool\n\t */\n\tprotected function is_generator_valid( $generator ) {\n\n\t\treturn in_array( $generator, array_keys( $this->get_generators() ), true );\n\n\t}\n\n\t/**\n\t * Record the generation of an object\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param LLMS_Post_Model|array|WP_User $object Created object or array.\n\t * @return void\n\t */\n\tpublic function object_created( $object ) {\n\n\t\tswitch ( current_action() ) {\n\n\t\t\tcase 'llms_generator_new_access_plan':\n\t\t\tcase 'llms_generator_new_course':\n\t\t\tcase 'llms_generator_new_section':\n\t\t\tcase 'llms_generator_new_lesson':\n\t\t\tcase 'llms_generator_new_quiz':\n\t\t\tcase 'llms_generator_new_question':\n\t\t\t\t$this->record_generation( $object->get( 'id' ), $object->get( 'type' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_generator_new_user':\n\t\t\t\t$this->record_generation( $object, 'user' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_generator_new_term':\n\t\t\t\t$this->record_generation( $object['term_id'], 'term' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Parse raw data\n\t *\n\t * @since 4.7.0\n\t *\n\t * @param string|array|obj $raw Accepts a JSON string, array, or object of raw data to pass to a generator.\n\t * @return array\n\t */\n\tprotected function parse_raw( $raw ) {\n\n\t\tif ( is_string( $raw ) ) {\n\t\t\t$raw = json_decode( $raw, true );\n\t\t}\n\n\t\treturn (array) $raw;\n\n\t}\n\n\t/**\n\t * Records a generated post id\n\t *\n\t * @since 3.14.8\n\t * @since 4.7.0 Modified method access from `private` to `protected`.\n\t *               Add IDs to the `generated` variable in favor of `posts`.\n\t *\n\t * @param int    $id  WP Post ID of the generated post.\n\t * @param string $key Key of the stat to increment.\n\t * @return void\n\t */\n\tprotected function record_generation( $id, $key ) {\n\n\t\t// Remove LifterLMS Prefix from the key (if it exists).\n\t\t$key = str_replace( 'llms_', '', $key );\n\n\t\t// Add an array if it doesn't already exist.\n\t\tif ( ! isset( $this->generated[ $key ] ) ) {\n\t\t\t$this->generated[ $key ] = array();\n\t\t}\n\n\t\t// Record the ID.\n\t\t$this->generated[ $key ][] = $id;\n\n\t}\n\n\t/**\n\t * Configure the default post status for generated posts at runtime\n\t *\n\t * @since 3.7.3\n\t * @since 4.7.0 Call `set_default_post_status()` from the configured generator.\n\t *\n\t * @param string $status Any valid WP Post Status.\n\t * @return void\n\t */\n\tpublic function set_default_post_status( $status ) {\n\t\tcall_user_func( array( $this->generator[0], 'set_default_post_status' ), $status );\n\t}\n\n\t/**\n\t * Sets the generator to use for the current instance\n\t *\n\t * @since 3.3.0\n\t * @since 3.36.3 Fix error causing `null` to be returned instead of expected `WP_Error`.\n\t *               Return the generator name on success instead of void.\n\t *\n\t * @param string $generator Generator string, eg: \"LifterLMS/SingleCourseExporter\"\n\t * @return string|WP_Error Name of the generator on success, otherwise an error object.\n\t */\n\tpublic function set_generator( $generator = null ) {\n\n\t\t// Interpret the generator from the raw data.\n\t\tif ( empty( $generator ) ) {\n\n\t\t\t// No generator can be interpreted.\n\t\t\tif ( ! isset( $this->raw['_generator'] ) ) {\n\n\t\t\t\t$this->error->add( 'missing-generator', __( 'The supplied file cannot be processed by the importer.', 'lifterlms' ) );\n\t\t\t\treturn $this->error;\n\n\t\t\t}\n\n\t\t\t// Set the generator using the interpreted data.\n\t\t\treturn $this->set_generator( $this->raw['_generator'] );\n\n\t\t}\n\n\t\t// Invalid generator.\n\t\tif ( ! $this->is_generator_valid( $generator ) ) {\n\t\t\t$this->error->add( 'invalid-generator', __( 'The supplied generator is invalid.', 'lifterlms' ) );\n\t\t\treturn $this->error;\n\t\t}\n\n\t\t// Set the generator.\n\t\t$generators      = $this->get_generators();\n\t\t$this->generator = $generators[ $generator ];\n\n\t\t// Return the generator name.\n\t\treturn $generator;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.hasher.php",
    "content": "<?php\n/**\n * LifterLMS hash ID encrypt/decrypt\n *\n * Based on PseudoCrypt by KevBurns (http://blog.kevburnsjr.com/php-unique-hash).\n *\n * Modified from original source to remove reliance on bcmath functions.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.16.7\n * @version 3.24.0\n *\n * @link http://stackoverflow.com/a/1464155/933782\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Hasher\n *\n * @since 3.16.7\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Hasher {\n\n\t/**\n\t * Key: Next prime greater than 62 ^ n / 1.618033988749894848\n\t * Value: modular multiplicative inverse\n\t *\n\t * @var array\n\t */\n\tprivate static $golden_primes = array(\n\t\t'1'                  => '1',\n\t\t'41'                 => '59',\n\t\t'2377'               => '1677',\n\t\t'147299'             => '187507',\n\t\t'9132313'            => '5952585',\n\t\t'566201239'          => '643566407',\n\t\t'35104476161'        => '22071637057',\n\t\t'2176477521929'      => '294289236153',\n\t\t'134941606358731'    => '88879354792675',\n\t\t'8366379594239857'   => '7275288500431249',\n\t\t'518715534842869223' => '280042546585394647',\n\t);\n\n\t/**\n\t * Ascii  =                     0  9,         A  Z,         a  z\n\t * $chars = array_merge(range(48,57), range(65,90), range(97,122))\n\t *\n\t * @var array\n\t */\n\tprivate static $chars62 = array(\n\t\t0  => 48,\n\t\t1  => 49,\n\t\t2  => 50,\n\t\t3  => 51,\n\t\t4  => 52,\n\t\t5  => 53,\n\t\t6  => 54,\n\t\t7  => 55,\n\t\t8  => 56,\n\t\t9  => 57,\n\t\t10 => 65,\n\t\t11 => 66,\n\t\t12 => 67,\n\t\t13 => 68,\n\t\t14 => 69,\n\t\t15 => 70,\n\t\t16 => 71,\n\t\t17 => 72,\n\t\t18 => 73,\n\t\t19 => 74,\n\t\t20 => 75,\n\t\t21 => 76,\n\t\t22 => 77,\n\t\t23 => 78,\n\t\t24 => 79,\n\t\t25 => 80,\n\t\t26 => 81,\n\t\t27 => 82,\n\t\t28 => 83,\n\t\t29 => 84,\n\t\t30 => 85,\n\t\t31 => 86,\n\t\t32 => 87,\n\t\t33 => 88,\n\t\t34 => 89,\n\t\t35 => 90,\n\t\t36 => 97,\n\t\t37 => 98,\n\t\t38 => 99,\n\t\t39 => 100,\n\t\t40 => 101,\n\t\t41 => 102,\n\t\t42 => 103,\n\t\t43 => 104,\n\t\t44 => 105,\n\t\t45 => 106,\n\t\t46 => 107,\n\t\t47 => 108,\n\t\t48 => 109,\n\t\t49 => 110,\n\t\t50 => 111,\n\t\t51 => 112,\n\t\t52 => 113,\n\t\t53 => 114,\n\t\t54 => 115,\n\t\t55 => 116,\n\t\t56 => 117,\n\t\t57 => 118,\n\t\t58 => 119,\n\t\t59 => 120,\n\t\t60 => 121,\n\t\t61 => 122,\n\t);\n\n\t/**\n\t * Modulo function\n\t *\n\t * @todo  figure this out better...\n\t *        64 bit systems can use % without issue\n\t *        32 bit systems have problems with % and needs to use fmod\n\t *                however after 100001 unhash no longer works correctly when using fmod\n\t *\n\t * @param    int $a  first int\n\t * @param    int $b  second int\n\t * @return   int\n\t * @since    3.16.10\n\t * @version  3.16.10\n\t */\n\tprivate static function mod( $a, $b ) {\n\n\t\t// 64 bit systems\n\t\tif ( 8 === PHP_INT_SIZE ) {\n\t\t\treturn $a % $b;\n\t\t}\n\t\t// 32 bit systems?\n\t\treturn fmod( $a, $b );\n\n\t}\n\n\t/**\n\t * Base 62 encode a number\n\t *\n\t * @param    int $int  number to encode\n\t * @return   string\n\t * @since    3.16.7\n\t * @version  3.16.10\n\t */\n\tpublic static function base62( $int ) {\n\t\t$key = '';\n\t\twhile ( $int > 0 ) {\n\t\t\t$mod  = self::mod( $int, 62 );\n\t\t\t$key .= chr( self::$chars62[ $mod ] );\n\t\t\t$int  = floor( $int / 62 );\n\t\t}\n\t\treturn strrev( $key );\n\t}\n\n\t/**\n\t * Hash a number\n\t *\n\t * @param    int $num   number to hash\n\t * @return   string\n\t * @since    3.16.7\n\t * @version  3.24.0\n\t */\n\tpublic static function hash( $num ) {\n\n\t\t$numlen = strlen( $num );\n\t\tif ( $numlen <= 3 ) {\n\t\t\t$len = 3;\n\t\t} elseif ( 4 === $numlen || 5 === $numlen ) {\n\t\t\t$len = 4;\n\t\t} else {\n\t\t\t$len = 5;\n\t\t}\n\n\t\t$ceil   = pow( 62, $len );\n\t\t$primes = array_keys( self::$golden_primes );\n\t\t$prime  = $primes[ $len ];\n\t\t$dec    = self::mod( ( $num * $prime ), $ceil );\n\t\t$hash   = self::base62( $dec );\n\t\treturn str_pad( $hash, $len, '0', STR_PAD_LEFT );\n\n\t}\n\n\t/**\n\t * Decode a base62 encoded string to get the associated number\n\t *\n\t * @param    string $key   encoded character\n\t * @return   int\n\t * @since    3.16.7\n\t * @version  3.16.7\n\t */\n\tpublic static function unbase62( $key ) {\n\t\t$int = 0;\n\t\tforeach ( str_split( strrev( $key ) ) as $i => $char ) {\n\t\t\t$dec = array_search( ord( $char ), self::$chars62 );\n\t\t\t$int = ( ( $dec * pow( 62, $i ) ) + $int );\n\t\t}\n\t\treturn $int;\n\t}\n\n\t/**\n\t * Decode a hashed string to get the original number\n\t *\n\t * @param    [type] $hash  encoded hash string\n\t * @return   int\n\t * @since    3.16.7\n\t * @version  3.16.10\n\t */\n\tpublic static function unhash( $hash ) {\n\n\t\t$len       = strlen( $hash );\n\t\t$ceil      = pow( 62, $len );\n\t\t$mmiprimes = array_values( self::$golden_primes );\n\t\t$mmi       = $mmiprimes[ $len ];\n\t\t$num       = self::unbase62( $hash );\n\t\t$dec       = self::mod( ( $num * $mmi ), $ceil );\n\t\treturn $dec;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.https.php",
    "content": "<?php\n/**\n * Handle HTTPS related redirects\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 3.35.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_HTTPS\n *\n * @since 3.0.0\n * @since 3.35.1 Sanitize `$_SERVER` input.\n */\nclass LLMS_HTTPS {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.0.0\n\t */\n\tpublic function __construct() {\n\n\t\tif ( 'yes' === get_option( 'lifterlms_checkout_force_ssl' ) ) {\n\n\t\t\tadd_action( 'template_redirect', array( $this, 'force_https_redirect' ) );\n\t\t\tadd_action( 'template_redirect', array( $this, 'unforce_https_redirect' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieve the http/s version of the current url.\n\t *\n\t * @since 3.35.1\n\t *\n\t * @param bool $https If true, gets the HTTPS url, otherwise gets url without HTTPS\n\t * @return string\n\t */\n\tprotected function get_force_redirect_url( $https = true ) {\n\n\t\t$uri = ! empty( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ) : '';\n\n\t\t// URI is http, switch it to https.\n\t\tif ( $uri && 0 === strpos( $uri, 'http' ) ) {\n\t\t\treturn $https ? preg_replace( '|^http://|', 'https://', $uri ) : preg_replace( '|^https://|', 'http://', $uri );\n\t\t}\n\n\t\t// URI doesn't have a protocol, build a new uri.\n\t\t$redirect = $https ? 'https://' : 'http://';\n\t\tif ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) {\n\t\t\t$redirect .= sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_HOST'] ) );\n\t\t} elseif ( ! empty( $_SERVER['HTTP_HOST'] ) ) {\n\t\t\t$redirect .= sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) );\n\t\t}\n\n\t\t$redirect .= $uri;\n\t\treturn $redirect;\n\n\t}\n\n\t/**\n\t * Redirect to https checkout page is force is enabled\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown\n\t * @since 3.35.1 Sanitize `$_SERVER` input.\n\t *\n\t * @return void\n\t */\n\tpublic function force_https_redirect() {\n\n\t\tif ( ! is_ssl() && ( is_llms_checkout() || is_llms_account_page() || apply_filters( 'llms_force_ssl_checkout', false ) ) ) {\n\t\t\tllms_redirect_and_exit(\n\t\t\t\t$this->get_force_redirect_url( true ),\n\t\t\t\tarray(\n\t\t\t\t\t'status' => 301,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Redirect back to http when not on checkout if force ssl is enabled and the site isn't fully ssl'd\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown\n\t * @since 3.35.1 Sanitize `$_SERVER` input.\n\t *\n\t * @return void\n\t */\n\tpublic function unforce_https_redirect() {\n\n\t\tif ( ! llms_is_site_https() && is_ssl() && ! is_llms_checkout() & ! is_llms_account_page() && ! llms_is_ajax() && apply_filters( 'llms_unforce_ssl_checkout', true ) ) {\n\t\t\tllms_redirect_and_exit(\n\t\t\t\t$this->get_force_redirect_url( false ),\n\t\t\t\tarray(\n\t\t\t\t\t'status' => 301,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t}\n\n\n}\n\nreturn new LLMS_HTTPS();\n"
  },
  {
    "path": "includes/class.llms.install.php",
    "content": "<?php\n/**\n * LLMS_Install class file\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Install LifterLMS\n *\n * Creates required pages, cronjobs, options, tables, and more.\n *\n * Additionally handles running database updates and migrations required with plugin updates.\n *\n * @since 1.0.0\n * @since 4.0.0 Added db update functions for session manager library cleanup.\n * @since 4.15.0 Added db update functions for orphan access plans cleanup.\n * @since 5.2.0 Removed private class property $db_updates.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Install::db_updates()` method\n *              - `LLMS_Install::update_notice()` method\n */\nclass LLMS_Install {\n\n\t/**\n\t * Instances of the bg updater.\n\t *\n\t * @var LLMS_Background_Updater\n\t */\n\tpublic static $background_updater;\n\n\t/**\n\t * Initialize the install class\n\t *\n\t * Hooks all actions.\n\t *\n\t * @since 3.0.0\n\t * @since 3.4.3 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tinclude_once ABSPATH . 'wp-admin/includes/plugin.php';\n\t\trequire_once 'admin/llms.functions.admin.php';\n\n\t\tadd_action( 'init', array( __CLASS__, 'init_background_updater' ), 4 );\n\t\tadd_action( 'init', array( __CLASS__, 'check_version' ), 5 );\n\t\tadd_action( 'admin_init', array( __CLASS__, 'update_actions' ) );\n\t\tadd_action( 'admin_init', array( __CLASS__, 'wizard_redirect' ) );\n\t}\n\n\t/**\n\t * Checks the current LLMS version and runs installer if required\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function check_version() {\n\t\tif ( ! defined( 'IFRAME_REQUEST' ) && get_option( 'lifterlms_current_version' ) !== llms()->version ) {\n\t\t\tself::install();\n\t\t\tdo_action( 'lifterlms_updated' );\n\t\t}\n\t}\n\n\t/**\n\t * Create LifterLMS cron jobs\n\t *\n\t * @since 1.0.0\n\t * @since 3.28.0 Remove unused cronjob `lifterlms_cleanup_sessions`.\n\t * @since 4.0.0 Add expired session cleanup.\n\t * @since 4.5.0 Add log backup cron.\n\t *\n\t * @return void\n\t */\n\tpublic static function create_cron_jobs() {\n\n\t\t$crons = array(\n\t\t\tarray(\n\t\t\t\t/**\n\t\t\t\t * Filter the recurrence interval at which files in the LifterLMS logs are scanned and backed up.\n\t\t\t\t *\n\t\t\t\t * @since 4.5.0\n\t\t\t\t *\n\t\t\t\t * @link https://developer.wordpress.org/reference/functions/wp_get_schedules/\n\t\t\t\t *\n\t\t\t\t * @param string $recurrence Cron job recurrence interval. Must be valid interval as retrieved from `wp_get_schedules()`. Default is \"daily\".\n\t\t\t\t */\n\t\t\t\t'hook'     => 'llms_backup_logs',\n\t\t\t\t'interval' => apply_filters( 'llms_backup_logs_interval', 'daily' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t/**\n\t\t\t\t * Filter the recurrence interval at which files in the LifterLMS tmp directory are cleaned.\n\t\t\t\t *\n\t\t\t\t * @since 4.5.0\n\t\t\t\t *\n\t\t\t\t * @link https://developer.wordpress.org/reference/functions/wp_get_schedules/\n\t\t\t\t *\n\t\t\t\t * @param string $recurrence Cron job recurrence interval. Must be valid interval as retrieved from `wp_get_schedules()`. Default is \"daily\".\n\t\t\t\t */\n\t\t\t\t'hook'     => 'llms_cleanup_tmp',\n\t\t\t\t'interval' => apply_filters( 'llms_cleanup_tmp_interval', 'daily' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'hook'     => 'llms_send_tracking_data',\n\t\t\t\t/**\n\t\t\t\t * Filter the recurrence interval at which tracking data is gathered and sent.\n\t\t\t\t *\n\t\t\t\t * @since Unknown\n\t\t\t\t *\n\t\t\t\t * @link https://developer.wordpress.org/reference/functions/wp_get_schedules/\n\t\t\t\t *\n\t\t\t\t * @param string $recurrence Cron job recurrence interval. Must be valid interval as retrieved from `wp_get_schedules()`. Default is \"daily\".\n\t\t\t\t */\n\t\t\t\t'interval' => apply_filters( 'llms_tracker_schedule_interval', 'daily' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'hook'     => 'llms_delete_expired_session_data',\n\t\t\t\t/**\n\t\t\t\t * Filter the recurrence interval at which expired session are removed from the database.\n\t\t\t\t *\n\t\t\t\t * @since 4.0.0\n\t\t\t\t *\n\t\t\t\t * @link https://developer.wordpress.org/reference/functions/wp_get_schedules/\n\t\t\t\t *\n\t\t\t\t * @param string $recurrence Cron job recurrence interval. Must be valid interval as retrieved from `wp_get_schedules()`. Default is \"hourly\".\n\t\t\t\t */\n\t\t\t\t'interval' => apply_filters( 'llms_delete_expired_session_data_recurrence', 'hourly' ),\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $crons as $data ) {\n\t\t\tif ( ! wp_next_scheduled( $data['hook'] ) ) {\n\t\t\t\twp_schedule_event( time(), $data['interval'], $data['hook'] );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Create basic course difficulties on installation\n\t *\n\t * @since 3.0.4\n\t *\n\t * @return void\n\t */\n\tpublic static function create_difficulties() {\n\n\t\tforeach ( self::get_difficulties() as $name ) {\n\n\t\t\t// Only create if it doesn't already exist.\n\t\t\tif ( ! get_term_by( 'name', $name, 'course_difficulty' ) ) {\n\n\t\t\t\twp_insert_term( $name, 'course_difficulty' );\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Create files needed by LifterLMS\n\t *\n\t * @since 3.0.0\n\t * @since 3.15.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function create_files() {\n\t\t$upload_dir = wp_upload_dir();\n\t\t$files      = array(\n\t\t\tarray(\n\t\t\t\t'base'    => LLMS_LOG_DIR,\n\t\t\t\t'file'    => '.htaccess',\n\t\t\t\t'content' => 'deny from all',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'base'    => LLMS_LOG_DIR,\n\t\t\t\t'file'    => 'index.html',\n\t\t\t\t'content' => '',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'base'    => LLMS_TMP_DIR,\n\t\t\t\t'file'    => '.htaccess',\n\t\t\t\t'content' => 'deny from all',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'base'    => LLMS_TMP_DIR,\n\t\t\t\t'file'    => 'index.html',\n\t\t\t\t'content' => '',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $files as $file ) {\n\t\t\tif ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) {\n\t\t\t\t$file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'w' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen\n\t\t\t\tif ( $file_handle ) {\n\t\t\t\t\tfwrite( $file_handle, $file['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite\n\t\t\t\t\tfclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Store all default options in the DB.\n\t *\n\t * @since 1.0.0\n\t * @since 3.8.0 Unknown.\n\t * @since 4.0.0 Include abstract table file.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic static function create_options() {\n\n\t\t$settings = LLMS_Admin_Settings::get_settings_tabs();\n\n\t\tforeach ( $settings as $section ) {\n\t\t\tforeach ( $section->get_settings( true ) as $value ) {\n\t\t\t\tif ( isset( $value['default'] ) && isset( $value['id'] ) ) {\n\t\t\t\t\t$autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true;\n\t\t\t\t\tadd_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get array of essential starter pages.\n\t *\n\t * @since 7.3.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_pages() {\n\t\t/**\n\t\t * Filters the essential starter pages.\n\t\t *\n\t\t * These are the pages that are going to be created when installing LifterLMS.\n\t\t * All these pages, as long as their `docs_url`, `description` and `wizard_title`\n\t\t * fields are defined, are going to be shown in the Setup Wizard.\n\t\t *\n\t\t * @since 7.3.0\n\t\t *\n\t\t * @param array $pages A multidimensional array defining the essential starter pages.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_install_get_pages',\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'content'      => '',\n\t\t\t\t\t'option'       => 'lifterlms_shop_page_id',\n\t\t\t\t\t'slug'         => 'courses',\n\t\t\t\t\t'title'        => __( 'Course Catalog', 'lifterlms' ),\n\t\t\t\t\t'wizard_title' => __( 'Course Catalog', 'lifterlms' ),\n\t\t\t\t\t'description'  => __( 'This page is where your visitors will find a list of all your available courses.', 'lifterlms' ),\n\t\t\t\t\t'docs_url'     => 'https://lifterlms.com/docs/course-catalog/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Course%20Catalog',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'content'      => '',\n\t\t\t\t\t'option'       => 'lifterlms_memberships_page_id',\n\t\t\t\t\t'slug'         => 'memberships',\n\t\t\t\t\t'title'        => __( 'Membership Catalog', 'lifterlms' ),\n\t\t\t\t\t'wizard_title' => __( 'Membership Catalog', 'lifterlms' ),\n\t\t\t\t\t'description'  => __( 'This page is where your visitors will find a list of all your available memberships.', 'lifterlms' ),\n\t\t\t\t\t'docs_url'     => 'https://lifterlms.com/docs/membership-catalog/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Membership%20Catalog',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'content'      => '[lifterlms_checkout]',\n\t\t\t\t\t'option'       => 'lifterlms_checkout_page_id',\n\t\t\t\t\t'slug'         => 'purchase',\n\t\t\t\t\t'title'        => __( 'Purchase', 'lifterlms' ),\n\t\t\t\t\t'wizard_title' => __( 'Checkout', 'lifterlms' ),\n\t\t\t\t\t'description'  => __( 'This is the page where visitors will be directed in order to pay for courses and memberships.', 'lifterlms' ),\n\t\t\t\t\t'docs_url'     => 'https://lifterlms.com/docs/checkout-page/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Checkout%20Page',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'content'      => '[lifterlms_my_account]',\n\t\t\t\t\t'option'       => 'lifterlms_myaccount_page_id',\n\t\t\t\t\t'slug'         => 'dashboard',\n\t\t\t\t\t'title'        => __( 'Dashboard', 'lifterlms' ),\n\t\t\t\t\t'wizard_title' => __( 'Student Dashboard', 'lifterlms' ),\n\t\t\t\t\t'description'  => __( 'Page where students can view and manage their current enrollments, earned certificates and achievements, account information, and purchase history.', 'lifterlms' ),\n\t\t\t\t\t'docs_url'     => 'https://lifterlms.com/docs/student-dashboard/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Wizard&utm_content=LifterLMS%20Student%20Dashboard',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Create essential starter pages.\n\t *\n\t * @since 1.0.0\n\t * @since 3.24.0 Unknown.\n\t * @since 7.3.0 Using `$this->get_pages()` method now.\n\t *\n\t * @return boolean False on error, true on success.\n\t */\n\tpublic static function create_pages() {\n\t\t/**\n\t\t * Filters the essential pages to be installed.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * {@see `llms_install_get_pages} filter hook.\n\t\t *\n\t\t * @param array $pages A multidimensional array defining the essential starter pages to be installed.\n\t\t */\n\t\t$pages = apply_filters( 'llms_install_create_pages', self::get_pages() );\n\t\tforeach ( $pages as $page ) {\n\t\t\tif ( ! llms_create_page( $page['slug'], $page['title'], $page['content'], $page['option'] ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Create LifterLMS DB tables\n\t *\n\t * @since 1.0.0\n\t * @since 3.3.1 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function create_tables() {\n\n\t\tglobal $wpdb;\n\n\t\t$wpdb->hide_errors();\n\n\t\trequire_once ABSPATH . 'wp-admin/includes/upgrade.php';\n\n\t\tdbDelta( self::get_schema() );\n\t}\n\n\t/**\n\t * Create default LifterLMS Product & Access Plan Visibility Options\n\t *\n\t * @since 3.6.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function create_visibilities() {\n\t\tforeach ( array_keys( llms_get_access_plan_visibility_options() ) as $term ) {\n\t\t\tif ( ! get_term_by( 'name', $term, 'llms_access_plan_visibility' ) ) {\n\t\t\t\twp_insert_term( $term, 'llms_access_plan_visibility' );\n\t\t\t}\n\t\t}\n\t\tforeach ( array_keys( llms_get_product_visibility_options() ) as $term ) {\n\t\t\tif ( ! get_term_by( 'name', $term, 'llms_product_visibility' ) ) {\n\t\t\t\twp_insert_term( $term, 'llms_product_visibility' );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Dispatches the bg updater\n\t *\n\t * @since 3.4.3\n\t *\n\t * @return void\n\t */\n\tpublic static function dispatch_db_updates() {\n\t\tself::$background_updater->save()->dispatch();\n\t}\n\n\t/**\n\t * Retrieve the default difficulty terms that should be created on a fresh install\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return array\n\t */\n\tpublic static function get_difficulties() {\n\t\treturn apply_filters(\n\t\t\t'llms_install_create_difficulties',\n\t\t\tarray(\n\t\t\t\t_x( 'Beginner', 'course difficulty name', 'lifterlms' ),\n\t\t\t\t_x( 'Intermediate', 'course difficulty name', 'lifterlms' ),\n\t\t\t\t_x( 'Advanced', 'course difficulty name', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Get a string of table data that can be passed to dbDelta() to install LLMS tables.\n\t *\n\t * @since 3.0.0\n\t * @since 3.16.9 Unknown\n\t * @since 3.16.9 Unknown\n\t * @since 3.34.0 Added `llms_install_get_schema` filter to method return.\n\t * @since 3.36.0 Added `wp_lifterlms_events` table.\n\t * @since 4.0.0 Added `wp_lifterlms_sessions` table.\n\t * @since 4.5.0 Added `wp_lifterlms_events_open_sessions` table.\n\t * @since 7.8.0 Added column `can_be_resumed` and `current_question_id` to the quiz attempt table.\n\t *\n\t * @return string\n\t */\n\tprivate static function get_schema() {\n\n\t\tglobal $wpdb;\n\n\t\t$collate = '';\n\n\t\tif ( $wpdb->has_cap( 'collation' ) ) {\n\n\t\t\tif ( ! empty( $wpdb->charset ) ) {\n\t\t\t\t$collate .= \"DEFAULT CHARACTER SET $wpdb->charset\";\n\t\t\t}\n\t\t\tif ( ! empty( $wpdb->collate ) ) {\n\t\t\t\t$collate .= \" COLLATE $wpdb->collate\";\n\t\t\t}\n\t\t}\n\n\t\t$tables = \"\nCREATE TABLE `{$wpdb->prefix}lifterlms_user_postmeta` (\n  meta_id bigint(20) NOT NULL auto_increment,\n  user_id bigint(20) NOT NULL,\n  post_id bigint(20) NOT NULL,\n  meta_key varchar(255) NULL,\n  meta_value longtext NULL,\n  updated_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00',\n  PRIMARY KEY (`meta_id`),\n  KEY user_id (`user_id`),\n  KEY post_id (`post_id`)\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_quiz_attempts` (\n  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n  `student_id` bigint(20) DEFAULT NULL,\n  `quiz_id` bigint(20) DEFAULT NULL,\n  `lesson_id` bigint(20) DEFAULT NULL,\n  `start_date` datetime DEFAULT NULL,\n  `update_date` datetime DEFAULT NULL,\n  `end_date` datetime DEFAULT NULL,\n  `status` varchar(15) DEFAULT '',\n  `attempt` bigint(20) DEFAULT NULL,\n  `grade` float DEFAULT NULL,\n  `can_be_resumed` tinyint(1) DEFAULT '0',\n  `current_question_id` bigint(20) DEFAULT NULL,\n  `questions` longtext,\n  PRIMARY KEY (`id`),\n  KEY `student_id` (`student_id`),\n  KEY `quiz_id` (`quiz_id`)\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_product_to_voucher` (\n  `product_id` bigint(20) NOT NULL,\n  `voucher_id` bigint(20) NOT NULL,\n  KEY `product_id` (`product_id`),\n  KEY `voucher_id` (`voucher_id`)\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_voucher_code_redemptions` (\n  `id` int(20) unsigned NOT NULL AUTO_INCREMENT,\n  `code_id` bigint(20) NOT NULL,\n  `user_id` bigint(20) NOT NULL,\n  `redemption_date` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `code_id` (`code_id`),\n  KEY `user_id` (`user_id`)\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_vouchers_codes` (\n  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n  `voucher_id` bigint(20) NOT NULL,\n  `code` varchar(20) NOT NULL DEFAULT '',\n  `redemption_count` bigint(20) DEFAULT NULL,\n  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',\n  `created_at` datetime DEFAULT NULL,\n  `updated_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `code` (`code`),\n  KEY `voucher_id` (`voucher_id`)\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_notifications` (\n  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n  `created` datetime DEFAULT NULL,\n  `updated` datetime DEFAULT NULL,\n  `status` varchar(11) DEFAULT '0',\n  `type` varchar(75) DEFAULT NULL,\n  `subscriber` varchar(255) DEFAULT NULL,\n  `trigger_id` varchar(75) DEFAULT NULL,\n  `user_id` bigint(20) DEFAULT NULL,\n  `post_id` bigint(20) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `status` (`status`),\n  KEY `type` (`type`),\n  KEY `subscriber` (`subscriber`(191))\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_events` (\n  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n  `date` datetime DEFAULT NULL,\n  `actor_id` bigint(20) DEFAULT NULL,\n  `object_type` varchar(55) DEFAULT NULL,\n  `object_id` bigint(20) DEFAULT NULL,\n  `event_type` varchar(55) DEFAULT NULL,\n  `event_action` varchar(55) DEFAULT NULL,\n  `meta` longtext DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY actor_id (`actor_id`),\n  KEY object_id (`object_id`)\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_events_open_sessions` (\n\t`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n\t`event_id` bigint(20) unsigned NOT NULL,\n\tPRIMARY KEY (`id`)\n) $collate;\nCREATE TABLE `{$wpdb->prefix}lifterlms_sessions` (\n  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n  `session_key` char(32) NOT NULL,\n  `data` longtext NOT NULL,\n  `expires` BIGINT unsigned NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `session_key` (`session_key`)\n) $collate;\n\";\n\n\t\t/**\n\t\t * Filter the database table schema.\n\t\t *\n\t\t * @since 3.34.0\n\t\t *\n\t\t * @param string $tables  A semi-colon (`;`) separated list of database table creating commands.\n\t\t * @param string $collate Database collation statement.\n\t\t */\n\t\treturn apply_filters( 'llms_install_get_schema', $tables, $collate );\n\t}\n\n\t/**\n\t * Initializes the bg updater class\n\t *\n\t * @since 3.4.3\n\t * @since 3.6.0 Unknown.\n\t * @since 5.2.0 Use `LLMS_PLUGIN_DIR` to include required class file.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic static function init_background_updater() {\n\n\t\tself::$background_updater = new LLMS_Background_Updater();\n\t}\n\n\t/**\n\t * Core install function\n\t *\n\t * @since 1.0.0\n\t * @since 3.13.0 Unknown.\n\t * @since 5.0.0 Install forms.\n\t * @since 5.2.0 Moved DB update logic to LLMS_Install::run_db_updates().\n\t *\n\t * @return void\n\t */\n\tpublic static function install() {\n\n\t\tif ( ! is_blog_installed() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Action run immediately prior to LLMS_Install::install() routine.\n\t\t *\n\t\t * @since Unknown\n\t\t */\n\t\tdo_action( 'lifterlms_before_install' );\n\n\t\tLLMS_Site::set_lock_url();\n\t\tself::create_tables();\n\t\tself::create_options();\n\t\tLLMS_Roles::install();\n\n\t\tself::verify_permalinks();\n\n\t\tLLMS_Post_Types::register_post_types();\n\t\tLLMS_Post_Types::register_taxonomies();\n\n\t\tllms()->query->init_query_vars();\n\t\tllms()->query->add_endpoints();\n\n\t\tself::create_cron_jobs();\n\t\tself::create_files();\n\t\tself::create_difficulties();\n\t\tself::create_visibilities();\n\n\t\tLLMS_Forms::instance()->install();\n\n\t\tLLMS_Beaver_Builder::instance()->install();\n\n\t\t$version    = get_option( 'lifterlms_current_version', null );\n\t\t$db_version = get_option( 'lifterlms_db_version', $version );\n\n\t\t// Trigger first time run redirect.\n\t\tif ( ( is_null( $version ) || is_null( $db_version ) ) || 'no' === get_option( 'lifterlms_first_time_setup', 'no' ) ) {\n\t\t\tupdate_option( '_llms_first_time_setup_redirect', 'yes', false );\n\t\t}\n\n\t\tself::run_db_updates( $db_version );\n\t\tself::update_llms_version();\n\n\t\tflush_rewrite_rules();\n\n\t\t/**\n\t\t * Action run immediately after the LLMS_Install::install() routine has completed.\n\t\t *\n\t\t * @since Unknown\n\t\t */\n\t\tdo_action( 'lifterlms_after_install' );\n\t}\n\n\t/**\n\t * Retrieve permalinks structure to verify if they are set, and any new defaults are saved\n\t *\n\t * @since 7.6.0\n\t *\n\t * @return void\n\t */\n\tpublic static function verify_permalinks() {\n\t\tif ( ! get_option( 'lifterlms_permalinks' ) ) {\n\t\t\tllms_switch_to_site_locale();\n\n\t\t\t// Retrieve the permalink structure, which will also save the default structure if it's not set.\n\t\t\tllms_get_permalink_structure();\n\n\t\t\tllms_restore_locale();\n\t\t}\n\t}\n\n\t/**\n\t * Remove the difficulties created by the `create_difficulties()` function\n\t *\n\t * Used during uninstall when \"remove_all_data\" is set.\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic static function remove_difficulties() {\n\n\t\tforeach ( self::get_difficulties() as $name ) {\n\n\t\t\t$term = get_term_by( 'name', $name, 'course_difficulty' );\n\t\t\tif ( $term ) {\n\n\t\t\t\twp_delete_term( $term->term_id, 'course_difficulty' );\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Run database updates\n\t *\n\t * If no updates are required for the current version, records the DB version as the current\n\t * plugin version.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $db_version The DB version to upgrade from.\n\t * @return void\n\t */\n\tprivate static function run_db_updates( $db_version ) {\n\n\t\tif ( ! is_null( $db_version ) ) {\n\n\t\t\t// Load the upgrader.\n\t\t\t$upgrader = new LLMS_DB_Upgrader( $db_version );\n\t\t\tif ( $upgrader->update() ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tself::update_db_version();\n\t}\n\n\t/**\n\t * Handle form submission of update related actions\n\t *\n\t * @since 3.4.3\n\t * @since 5.2.0 Use `LLMS_DB_Upgrader` and remove the \"force upgrade\" action handler.\n\t *\n\t * @return void\n\t */\n\tpublic static function update_actions() {\n\n\t\tif ( empty( $_GET['llms-db-update'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! isset( $_REQUEST['llms-db-update'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['llms-db-update'] ) ), 'do_db_updates' ) ) {\n\t\t\twp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'lifterlms' ) );\n\t\t}\n\n\t\tif ( ! current_user_can( 'manage_options' ) ) {\n\t\t\twp_die( esc_html__( 'You are not allowed to perform the requested action.', 'lifterlms' ) );\n\t\t}\n\n\t\tLLMS_Admin_Notices::delete_notice( 'bg-db-update' );\n\n\t\t$upgrader = new LLMS_DB_Upgrader( get_option( 'lifterlms_db_version' ) );\n\t\t$upgrader->enqueue_updates();\n\t\tllms_redirect_and_exit( remove_query_arg( array( 'llms-db-update' ) ) );\n\t}\n\n\t/**\n\t * Update the LifterLMS DB record to the latest version\n\t *\n\t * @since 3.0.0\n\t * @since 3.4.3 Unknown.\n\t *\n\t * @param string $version Version number.\n\t * @return void\n\t */\n\tpublic static function update_db_version( $version = null ) {\n\t\tdelete_option( 'lifterlms_db_version' );\n\t\tadd_option( 'lifterlms_db_version', is_null( $version ) ? llms()->version : $version );\n\t}\n\n\t/**\n\t * Update the LifterLMS version record to the latest version\n\t *\n\t * @since 3.0.0\n\t * @since 3.4.3 Unknown.\n\t *\n\t * @param string $version Version number.\n\t * @return void\n\t */\n\tpublic static function update_llms_version( $version = null ) {\n\t\tdelete_option( 'lifterlms_current_version' );\n\t\tadd_option( 'lifterlms_current_version', is_null( $version ) ? llms()->version : $version );\n\t}\n\n\t/**\n\t * Redirects users to the setup wizard\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Unknown.\n\t * @since 5.2.0 Use strict array comparison and `wp_safe_redirect()` in favor of `wp_redirect()`.\n\t *\n\t * @return void\n\t */\n\tpublic static function wizard_redirect() {\n\n\t\tif ( 'yes' === get_option( '_llms_first_time_setup_redirect', 'no' ) ) {\n\n\t\t\tupdate_option( '_llms_first_time_setup_redirect', 'no' );\n\n\t\t\tif ( ( ! empty( $_GET['page'] ) && in_array( $_GET['page'], array( 'llms-setup' ), true ) ) || is_network_admin() || isset( $_GET['activate-multi'] ) || apply_filters( 'llms_prevent_automatic_wizard_redirect', false ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( current_user_can( 'install_plugins' ) ) {\n\n\t\t\t\twp_safe_redirect( admin_url() . '?page=llms-setup' );\n\t\t\t\texit;\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the WP User ID of the first available user who can 'manage_options'\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return int Returns the ID of the current user if they can 'manage_options'.\n\t *             Otherwise returns the ID of the first Administrator if they can 'manage_options'.\n\t *             Returns 0 if the first Administrator cannot 'manage_options' or the current site has no Administrators.\n\t */\n\tpublic static function get_can_install_user_id() {\n\n\t\t$capability = 'manage_options';\n\n\t\tif ( current_user_can( $capability ) ) {\n\t\t\treturn get_current_user_id();\n\t\t}\n\n\t\t// Get the first user with administrator role.\n\t\t// Here, for simplicity, we're assuming the administrator's role capabilities are the original ones.\n\t\t$first_admin_user = get_users(\n\t\t\tarray(\n\t\t\t\t'role'    => 'Administrator',\n\t\t\t\t'number'  => 1,\n\t\t\t\t'orderby' => 'ID',\n\t\t\t)\n\t\t);\n\n\t\t// Return 0 if the first Administrator cannot 'manage_options' or the current site has no Administrators.\n\t\treturn ! empty( $first_admin_user ) && $first_admin_user[0]->has_cap( $capability ) ? $first_admin_user[0]->ID : 0;\n\t}\n}\n\nLLMS_Install::init();\n"
  },
  {
    "path": "includes/class.llms.integrations.php",
    "content": "<?php\n/**\n * LifterLMS Integrations\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Integrations\n *\n * @since 1.0.0\n * @since 3.18.2 Updated.\n * @since 3.33.1 Integrations are now loaded based on their defined priority.\n * @since 3.33.2 Integration priority checks are backwards compatible to handle deprecated legacy integrations.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Removed the deprecated `LLMS_Integrations::$_instance` property.\n */\nclass LLMS_Integrations {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Array of integrations, regardless of availability\n\t *\n\t * @var  LLMS_Abstract_Integration[]\n\t */\n\tprivate $integrations = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    1.0.0\n\t * @version  3.17.8\n\t */\n\tprivate function __construct() {\n\t\t$this->init();\n\t}\n\n\t/**\n\t * Get an integration instance by id\n\t *\n\t * @param    string $id  id of the integration\n\t * @return   LLMS_Abstract_Integration|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_integration( $id ) {\n\t\t$available = $this->get_available_integrations();\n\t\treturn isset( $available[ $id ] ) ? $available[ $id ] : false;\n\t}\n\n\t/**\n\t * Initialize Integration Classes\n\t *\n\t * @since 1.0.0\n\t * @since 3.18.0 Updated.\n\t * @since 3.33.1 Updated sort order to be based off the priority defined for the integration.\n\t * @since 3.33.2 Made sort order check backwards compatible with deprecated legacy integrations.\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\n\t\t$integrations = apply_filters(\n\t\t\t'lifterlms_integrations',\n\t\t\tarray(\n\t\t\t\t'LLMS_Integration_BBPress',\n\t\t\t\t'LLMS_Integration_Buddypress',\n\t\t\t)\n\t\t);\n\n\t\tif ( ! empty( $integrations ) ) {\n\n\t\t\tforeach ( $integrations as $integration ) {\n\n\t\t\t\t$load_integration = new $integration();\n\n\t\t\t\t$priority = method_exists( $load_integration, 'get_priority' ) ? $load_integration->get_priority() : 50;\n\t\t\t\twhile ( array_key_exists( (string) $priority, $this->integrations ) ) {\n\t\t\t\t\t$priority += .01;\n\t\t\t\t}\n\n\t\t\t\t$this->integrations[ (string) $priority ] = $load_integration;\n\n\t\t\t\tksort( $this->integrations );\n\n\t\t\t}\n\t\t}\n\n\t\tdo_action( 'llms_integrations_init', $this );\n\n\t}\n\n\t/**\n\t * Get available integrations\n\t *\n\t * @return   LLMS_Abstract_Integration[]\n\t * @since    1.0.0\n\t * @version  3.17.8\n\t */\n\tpublic function get_available_integrations() {\n\n\t\t$_available_integrations = array();\n\n\t\tforeach ( $this->integrations as $integration ) {\n\n\t\t\tif ( $integration->is_available() ) {\n\n\t\t\t\t$_available_integrations[ $integration->id ] = $integration;\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'lifterlms_available_integrations', $_available_integrations );\n\t}\n\n\t/**\n\t * Get all integrations regardless of availability\n\t *\n\t * @return   LLMS_Abstract_Integration[]\n\t * @since    3.18.2\n\t * @version  3.18.2\n\t */\n\tpublic function get_integrations() {\n\t\treturn $this->integrations;\n\t}\n\n\t/**\n\t * Get all integrations regardless of availability\n\t *\n\t * @return   LLMS_Abstract_Integration[]\n\t * @since    1.0.0\n\t * @version  3.17.8\n\t * @todo     deprecate\n\t */\n\tpublic function integrations() {\n\t\treturn $this->get_integrations();\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.l10n.php",
    "content": "<?php\n/**\n * Localization Functions\n *\n * Currently only used to translate strings output by Javascript functions.\n * More robust features will be added in the future.\n *\n * @package LifterLMS/Classes\n *\n * @since 2.7.3\n * @version 3.17.8\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_L10n\n *\n * @since 2.7.3\n * @since 3.17.8 Unknown.\n */\nclass LLMS_L10n {\n\n\t/**\n\t * Create an object of translatable strings\n\t *\n\t * This object is added to the LLMS.l10n JS object.\n\t *\n\t * The text used in JS *MUST* exactly match the string found in this object.\n\t *\n\t * @since 2.7.3\n\t * @since 3.17.8 Unknown.\n\t *\n\t * @param  boolean $json If `true`, convert to JSON, otherwise return the array.\n\t * @return string|array If `$json` is `true`, returns a JSON string, otherwise an array.\n\t */\n\tpublic static function get_js_strings( $json = true ) {\n\n\t\t$strings = array();\n\n\t\t// Add strings that should only be translated on the admin panel.\n\t\tif ( is_admin() ) {\n\n\t\t\t$strings = apply_filters( 'lifterlms_js_l10n_admin', $strings );\n\n\t\t}\n\n\t\t// Allow filtering so extensions don't have to implement their own l10n functions.\n\t\t$strings = apply_filters( 'lifterlms_js_l10n', $strings );\n\n\t\tif ( true === $json ) {\n\n\t\t\treturn json_encode( $strings );\n\n\t\t} else {\n\n\t\t\treturn $strings;\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.lesson.handler.php",
    "content": "<?php\n/**\n * Lesson Handler Class\n *\n * Main Handler for lesson management in LifterLMS\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 5.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Lesson_Handler\n *\n * @since 1.0.0\n * @since 5.7.0 Deprecated the `LLMS_Lesson_Handler::assign_to_course()` method with no replacement.\n */\nclass LLMS_Lesson_Handler {\n\n\tpublic function __construct( $lesson ) {}\n\n\tpublic static function get_lesson_options_for_select_list() {\n\n\t\t$lessons = LLMS_Post_Handler::get_posts( 'lesson' );\n\n\t\t$options = array();\n\n\t\tif ( ! empty( $lessons ) ) {\n\n\t\t\tforeach ( $lessons as $key => $value ) {\n\n\t\t\t\t// Get parent course if assigned.\n\t\t\t\t$parent_course = get_post_meta( $value->ID, '_llms_parent_course', true );\n\n\t\t\t\tif ( $parent_course ) {\n\t\t\t\t\t$title = $value->post_title . ' ( ' . get_the_title( $parent_course ) . ' )';\n\t\t\t\t} else {\n\t\t\t\t\t$title = $value->post_title . ' ( ' . __( 'unassigned', 'lifterlms' ) . ' )';\n\t\t\t\t}\n\n\t\t\t\t$options[ $value->ID ] = $title;\n\n\t\t\t}\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n\t/**\n\t * Assigns the lesson to a section and course, optionally by duplicating it.\n\t *\n\t * @since 1.2.4 Introduced.\n\t * @deprecated 5.7.0 There is not a replacement.\n\t *\n\t * @param int  $course_id\n\t * @param int  $section_id\n\t * @param int  $lesson_id\n\t * @param bool $duplicate\n\t * @param bool $reset_order\n\t * @return false|int|WP_Error\n\t */\n\tpublic static function assign_to_course( $course_id, $section_id, $lesson_id, $duplicate = true, $reset_order = true ) {\n\n\t\tllms_deprecated_function( __METHOD__, '5.7.0' );\n\n\t\t// Get position of next lesson.\n\t\t$section      = new LLMS_Section( $section_id );\n\t\t$lesson_order = $section->get_next_available_lesson_order();\n\n\t\t// First determine if lesson is associated with a course.\n\t\t// We need to know this because if it is already associated then we duplicate it and assign the dupe.\n\t\t$parent_course  = get_post_meta( $lesson_id, '_llms_parent_course', true );\n\t\t$parent_section = get_post_meta( $lesson_id, '_llms_parent_section', true );\n\n\t\t// Parent course exists, lets dupe this baby!.\n\t\tif ( $parent_course && true == $duplicate ) {\n\t\t\t$lesson_id = self::duplicate_lesson( $course_id, $section_id, $lesson_id );\n\t\t} else {\n\t\t\t// Add parent section and course to new lesson.\n\t\t\tupdate_post_meta( $lesson_id, '_llms_parent_section', $section_id );\n\t\t\tupdate_post_meta( $lesson_id, '_llms_parent_course', $course_id );\n\n\t\t}\n\n\t\tif ( $reset_order ) {\n\t\t\tupdate_post_meta( $lesson_id, '_llms_order', $lesson_order );\n\t\t}\n\n\t\treturn $lesson_id;\n\n\t}\n\n\tpublic static function duplicate_lesson( $course_id, $section_id, $lesson_id ) {\n\n\t\tif ( ! isset( $course_id ) || ! isset( $section_id ) || ! isset( $lesson_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Duplicate the lesson.\n\t\t$new_lesson_id = self::duplicate( $lesson_id );\n\n\t\tif ( ! $new_lesson_id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Add parent section and course to new lesson.\n\t\tupdate_post_meta( $new_lesson_id, '_llms_parent_section', $section_id );\n\t\tupdate_post_meta( $new_lesson_id, '_llms_parent_course', $course_id );\n\n\t\treturn $new_lesson_id;\n\n\t}\n\n\tpublic static function duplicate( $post_id ) {\n\n\t\t// Make sure we have a post id and it returns a post.\n\t\tif ( ! isset( $post_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$post_obj = get_post( $post_id );\n\t\t// Last check.\n\t\tif ( ! isset( $post_obj ) || null == $post_obj ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// No going back now.\n\n\t\t// Create duplicate post.\n\t\t$args = array(\n\t\t\t'comment_status' => $post_obj->comment_status,\n\t\t\t'ping_status'    => $post_obj->ping_status,\n\t\t\t'post_author'    => $post_obj->post_author,\n\t\t\t'post_content'   => $post_obj->post_content,\n\t\t\t'post_excerpt'   => $post_obj->post_excerpt,\n\t\t\t'post_name'      => $post_obj->post_name,\n\t\t\t'post_parent'    => $post_obj->post_parent,\n\t\t\t'post_status'    => 'publish',\n\t\t\t'post_title'     => $post_obj->post_title,\n\t\t\t'post_type'      => $post_obj->post_type,\n\t\t\t'to_ping'        => $post_obj->to_ping,\n\t\t\t'menu_order'     => $post_obj->menu_order,\n\t\t\t'post_password'  => $post_obj->post_password,\n\t\t);\n\n\t\t// Create the duplicate post.\n\t\t$new_post_id = wp_insert_post( $args );\n\n\t\tif ( $new_post_id ) {\n\n\t\t\t// Get all current post terms and set them to the new post.\n\t\t\t$taxonomies = get_object_taxonomies( $post_obj->post_type );\n\t\t\tforeach ( $taxonomies as $taxonomy ) {\n\t\t\t\t$post_terms = wp_get_object_terms(\n\t\t\t\t\t$post_obj->ID,\n\t\t\t\t\t$taxonomy,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'fields' => 'slugs',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\twp_set_object_terms( $new_post_id, $post_terms, $taxonomy, false );\n\t\t\t}\n\n\t\t\t// Duplicate meta.\n\t\t\t$insert_meta = self::duplicate_meta( $post_id, $new_post_id );\n\n\t\t}\n\n\t\treturn $new_post_id;\n\n\t}\n\n\tpublic static function duplicate_meta( $post_id, $new_post_id ) {\n\t\tglobal $wpdb;\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t// Duplicate all post meta.\n\t\t$post_meta_infos = $wpdb->get_results( \"SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=$post_id\" );\n\n\t\tif ( count( $post_meta_infos ) != 0 ) {\n\n\t\t\t$sql_query = \"INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) \";\n\n\t\t\tforeach ( $post_meta_infos as $meta_info ) {\n\n\t\t\t\t// Do not copy the following meta values.\n\t\t\t\tif ( '_llms_parent_section' === $meta_info->meta_key ) {\n\t\t\t\t\t$meta_info->meta_value = '';\n\t\t\t\t}\n\t\t\t\tif ( '_llms_parent_course' === $meta_info->meta_key ) {\n\t\t\t\t\t$meta_info->meta_value = '';\n\t\t\t\t}\n\t\t\t\tif ( '_prerequisite' === $meta_info->meta_key ) {\n\t\t\t\t\t$meta_info->meta_value = '';\n\t\t\t\t}\n\t\t\t\tif ( '_has_prerequisite' === $meta_info->meta_key ) {\n\t\t\t\t\t$meta_info->meta_value = '';\n\t\t\t\t}\n\n\t\t\t\t$meta_key        = $meta_info->meta_key;\n\t\t\t\t$meta_value      = addslashes( $meta_info->meta_value );\n\t\t\t\t$sql_query_sel[] = \"SELECT $new_post_id, '$meta_key', '$meta_value'\";\n\n\t\t\t}\n\n\t\t\t$sql_query       .= implode( ' UNION ALL ', $sql_query_sel );\n\t\t\t$insert_post_meta = $wpdb->query( $sql_query );\n\n\t\t\treturn $insert_post_meta;\n\t\t}\n\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.membership.data.php",
    "content": "<?php\n/**\n * Query data about a membership.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.32.0\n * @version 6.10.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Query data about a membership.\n *\n * @since 3.32.0\n * @since 3.35.0 Sanitize post ids from WP_Query before using for a new DB query.\n */\nclass LLMS_Membership_Data extends LLMS_Abstract_Post_Data {\n\n\t/**\n\t * Retrieve # of membership enrollments within the period.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $period Optional.Date period [current|previous]. Default 'current'.\n\t * @return int\n\t */\n\tpublic function get_enrollments( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT DISTINCT COUNT( user_id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_value = 'yes'\n\t\t\t  AND meta_key = '_start_date'\n\t\t\t  AND post_id = %d\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve # of engagements related to the membership awarded within the period.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $type   Engagement type [email|certificate|achievement].\n\t * @param string $period Optional. Date period [current|previous]. Default 'current'.\n\t * @return int\n\t */\n\tpublic function get_engagements( $type, $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT DISTINCT COUNT( user_id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_key = %s\n\t\t\t  AND post_id = %d\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t'_' . $type,\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve # of orders placed for the membership within the period.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $period Optional. Date period [current|previous]. Default 'current'.\n\t * @return int\n\t */\n\tpublic function get_orders( $period = 'current' ) {\n\n\t\t$query = $this->orders_query(\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'after'     => $this->get_date( $period, 'start' ),\n\t\t\t\t\t'before'    => $this->get_date( $period, 'end' ),\n\t\t\t\t\t'inclusive' => true,\n\t\t\t\t),\n\t\t\t),\n\t\t\t1\n\t\t);\n\t\treturn $query->found_posts;\n\n\t}\n\n\t/**\n\t * Retrieve total amount of transactions related to orders for the course completed within the period.\n\t *\n\t * @since 3.32.0\n\t * @since 3.35.0 Sanitize post ids from WP_Query before using for a new DB query.\n\t *\n\t * @param string $period Optional. Date period [current|previous]. Default 'current'.\n\t * @return float\n\t */\n\tpublic function get_revenue( $period ) {\n\n\t\t$query     = $this->orders_query( -1 );\n\t\t$order_ids = wp_list_pluck( $query->posts, 'ID' );\n\n\t\t$revenue = 0;\n\n\t\tif ( $order_ids ) {\n\n\t\t\t$order_ids = implode( ',', array_map( 'absint', $order_ids ) );\n\n\t\t\tglobal $wpdb;\n\n\t\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- ID list is sanitized via `absint()` earlier in this method.\n\t\t\t$revenue = $wpdb->get_var(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"SELECT SUM( m2.meta_value )\n\t\t\t\t FROM $wpdb->posts AS p\n\t\t\t\t LEFT JOIN $wpdb->postmeta AS m1 ON m1.post_id = p.ID AND m1.meta_key = '_llms_order_id' -- join for the ID\n\t\t\t\t LEFT JOIN $wpdb->postmeta AS m2 ON m2.post_id = p.ID AND m2.meta_key = '_llms_amount'-- get the actual amounts\n\t\t\t\t WHERE p.post_type = 'llms_transaction'\n\t\t\t\t   AND p.post_status = 'llms-txn-succeeded'\n\t\t\t\t   AND m1.meta_value IN ({$order_ids})\n\t\t\t\t   AND p.post_modified BETWEEN %s AND %s\n\t\t\t\t;\",\n\t\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t\t)\n\t\t\t);\n\t\t\t// phpcs:enabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t\tif ( is_null( $revenue ) ) {\n\t\t\t\t$revenue = 0;\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_membership_data_get_revenue', $revenue, $period, $this );\n\n\t}\n\n\t/**\n\t * Retrieve the number of unenrollments on a given date.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $period Optional. Date period [current|previous]. Default 'current'.\n\t * @return int\n\t */\n\tpublic function get_unenrollments( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT DISTINCT COUNT( user_id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\tWHERE meta_value != 'enrolled'\n\t\t\t  AND meta_key = '_status'\n\t\t\t  AND post_id = %d\n\t\t\t  AND updated_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Execute a WP Query to retrieve orders within the given date range.\n\t *\n\t * @since 3.32.0\n\t * @since 6.10.1 Fixed typo preventing one-time orders from adding to revenue.\n\t *\n\t * @param int   $num_orders Optional. Number of orders to retrieve. Default 1.\n\t * @param array $dates      Optiona. Date range (passed to WP_Query['date_query']). Default empty array.\n\t * @return obj\n\t */\n\tprivate function orders_query( $num_orders = 1, $dates = array() ) {\n\n\t\t$args = array(\n\t\t\t'post_type'      => 'llms_order',\n\t\t\t'post_status'    => array( 'llms-active', 'llms-completed' ),\n\t\t\t'posts_per_page' => $num_orders,\n\t\t\t'meta_key'       => '_llms_product_id',\n\t\t\t'meta_value'     => $this->post_id,\n\t\t);\n\n\t\tif ( $dates ) {\n\t\t\t$args['date_query'] = $dates;\n\t\t}\n\n\t\t$query = new WP_Query( $args );\n\n\t\treturn $query;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.nav.menus.php",
    "content": "<?php\n/**\n * LifterLMS Navigation Menus\n *\n * @package LifterLMS/Classes\n *\n * @since 3.14.7\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Navigation Menus class.\n *\n * @since 3.14.7\n * @since 3.24.0 Unknown.\n * @since 3.37.12 Fixed possible access to undefined index.\n *                Excluded endpoints with an empty url.\n *                Made sure to use strict comparisons.\n */\nclass LLMS_Nav_Menus {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.14.7\n\t * @since 3.22.0 Unknown.\n\t * @since 7.1.0 Postpone the LifterLMS menu meta box addition to `admin_head-nav-menus.php`\n\t *               rather than `load-nav-menus.php` it's not initially hidden (for new users).\n\t * @since 7.2.0 Add navigation link block and enqueue block editor assets.\n\t * @since 7.3.0 Change `render_block_llms/navigation-link` to `render_block` for compatibility with LLMS block visibility.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Filter menu items on frontend to add real URLs to menu items.\n\t\tadd_filter( 'wp_nav_menu_objects', array( $this, 'filter_nav_items' ) );\n\n\t\t// Add meta box to the Appearance -> Menus screen on admin panel.\n\t\tadd_action( 'admin_head-nav-menus.php', array( $this, 'add_metabox' ) );\n\n\t\t// Add LifterLMS menu item type section to customizer.\n\t\tadd_filter( 'customize_nav_menu_available_item_types', array( $this, 'customize_add_type' ) );\n\n\t\t// Add LifterLMS menu items links to the customizer.\n\t\tadd_filter( 'customize_nav_menu_available_items', array( $this, 'customize_add_items' ), 10, 4 );\n\n\t\t// Add active classes for nav items for catalog pages.\n\t\tadd_filter( 'wp_nav_menu_objects', array( $this, 'menu_item_classes' ) );\n\n\t\t// Register block.\n\t\tadd_action( 'init', array( $this, 'register_block' ) );\n\n\t\t// Render block.\n\t\tadd_filter( 'render_block', array( $this, 'render_block' ), 10, 2 );\n\n\t\t// Load menu items data in block editor.\n\t\tadd_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );\n\t}\n\n\t/**\n\t * Add nav menu metabox.\n\t *\n\t * @since 3.14.7\n\t *\n\t * @return void\n\t */\n\tpublic function add_metabox() {\n\n\t\tadd_meta_box( 'llms-nav-menu', __( 'LifterLMS', 'lifterlms' ), array( $this, 'output' ), 'nav-menus', 'side', 'default' );\n\t\tadd_action( 'admin_print_footer_scripts', array( $this, 'output_scripts' ) );\n\t}\n\n\t/**\n\t * Adds LifterLMS menu items to the customizer.\n\t *\n\t * @since 3.14.7\n\t *\n\t * @param array   $items  Optional. Menu items. Default empty array.\n\t * @param string  $type   Optional. Requested menu item type. Default empty string.\n\t * @param string  $object Optional. Requested menu item object. Default empty string.\n\t * @param integer $page   Optional. Requested page number. Default `0`.\n\t * @return array\n\t */\n\tpublic function customize_add_items( $items = array(), $type = '', $object = '', $page = 0 ) {\n\n\t\tif ( 'llms_nav' !== $object ) {\n\t\t\treturn $items;\n\t\t}\n\n\t\tforeach ( $this->get_nav_items() as $id => $data ) {\n\n\t\t\t$items[] = array(\n\t\t\t\t'classes'    => 'llms-nav-item-' . $id,\n\t\t\t\t'id'         => $id,\n\t\t\t\t'title'      => $data['title'],\n\t\t\t\t'type_label' => __( 'Custom Link', 'lifterlms' ),\n\t\t\t\t'url'        => esc_url_raw( $data['url'] ),\n\t\t\t);\n\n\t\t}\n\n\t\treturn array_slice( $items, 10 * $page, 10 );\n\t}\n\n\t/**\n\t * Add the LifterLMS menu item section to the customizer.\n\t *\n\t * @since 3.14.7\n\t *\n\t * @param array $types Existing menu item types.\n\t * @return array\n\t */\n\tpublic function customize_add_type( $types ) {\n\n\t\t$types['llms_nav_menu_items'] = array(\n\t\t\t'title'  => _x( 'LifterLMS', 'customizer menu section title', 'lifterlms' ),\n\t\t\t'type'   => 'llms_nav',\n\t\t\t'object' => 'llms_nav',\n\t\t);\n\n\t\treturn $types;\n\t}\n\n\t/**\n\t * Filters Nav Menu Items to convert #llms- urls into actual URLs.\n\t *\n\t * Also hides URLs that should only be available to logged-in users.\n\t *\n\t * @since 3.14.7\n\t * @since 3.37.12 Use `in_array` with strict types comparison.\n\t * @since 7.2.0 Remove passing item data by reference and improve URL checks.\n\t *\n\t * @param array $items Nav menu items.\n\t * @return array\n\t */\n\tpublic function filter_nav_items( $items ) {\n\n\t\t$urls = array(\n\t\t\t'#llms-signout',\n\t\t\t'#llms-signin',\n\t\t);\n\n\t\tforeach ( $items as $i => $data ) {\n\t\t\t$is_object = is_object( $data ) && property_exists( $data, 'url' );\n\t\t\t$url       = $is_object ? $data->url : $data['url'] ?? '';\n\n\t\t\tif ( ! in_array( $url, $urls, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$data      = (object) $data;\n\t\t\t$logged_in = is_user_logged_in();\n\n\t\t\tif ( '#llms-signin' === $url && ! $logged_in ) {\n\t\t\t\t$data->url = llms_get_page_url( 'myaccount' );\n\t\t\t} elseif ( '#llms-signout' === $url && $logged_in ) {\n\t\t\t\t$data->url = wp_logout_url( llms_get_page_url( 'myaccount' ) );\n\t\t\t} else {\n\t\t\t\tunset( $items[ $i ] );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$items[ $i ] = $is_object ? $data : (array) $data;\n\t\t}\n\n\t\treturn $items;\n\t}\n\n\t/**\n\t * Retrieve a filtered array of custom LifterLMS nav menu items.\n\t *\n\t * @since 3.14.7\n\t * @since 3.37.12 Fixed possible access to undefined index.\n\t *                Excluded endpoints with an empty url.\n\t *\n\t * @return array\n\t */\n\tprivate function get_nav_items() {\n\n\t\t$items = array();\n\n\t\tforeach ( LLMS_Student_Dashboard::get_tabs() as $id => $data ) {\n\n\t\t\tif ( ! empty( $data['nav_item'] ) ) {\n\n\t\t\t\t$url = ! empty( $data['endpoint'] ) ? llms_get_endpoint_url( $data['endpoint'], '', llms_get_page_url( 'myaccount' ) ) : '';\n\n\t\t\t\t// No URL no nav item.\n\t\t\t\tif ( empty( $url ) ) {\n\t\t\t\t\tif ( empty( $data['url'] ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$url = $data['url'];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$title = empty( $data['title'] ) ? '' : $data['title'];\n\n\t\t\t\t$items[ $id ] = array(\n\t\t\t\t\t'url'   => $url,\n\t\t\t\t\t'label' => $title,\n\t\t\t\t\t'title' => $title,\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t\t$items['signin']  = array(\n\t\t\t'url'   => '#llms-signin',\n\t\t\t'label' => __( 'Sign In', 'lifterlms' ),\n\t\t\t'title' => __( 'Sign In', 'lifterlms' ),\n\t\t);\n\t\t$items['signout'] = array(\n\t\t\t'url'   => '#llms-signout',\n\t\t\t'label' => __( 'Sign Out', 'lifterlms' ),\n\t\t\t'title' => __( 'Sign Out', 'lifterlms' ),\n\t\t);\n\n\t\t/**\n\t\t * Filters array of custom LifterLMS nav menu items\n\t\t *\n\t\t * @since 3.14.7\n\t\t *\n\t\t * @param array $items Array of custom LifterLMS nav menu items.\n\t\t */\n\t\treturn apply_filters( 'llms_nav_menu_items', $items );\n\t}\n\n\t/**\n\t * Add \"active\" classes to menu items for LLMS catalog pages.\n\t *\n\t * @since 3.22.0\n\t * @since 3.37.12 Use strict comparisons.\n\t *                Cast `page_for_posts` option to int in order to use strict comparisons.\n\t * @since 4.12.0 Make sure `is_lifterlms()` exists before calling it.\n\t *\n\t * @param array $menu_items Menu items.\n\t * @return array\n\t */\n\tpublic function menu_item_classes( $menu_items ) {\n\n\t\tif ( ! function_exists( 'is_lifterlms' ) || ! is_lifterlms() ) {\n\t\t\treturn $menu_items;\n\t\t}\n\n\t\t$courses_id     = llms_get_page_id( 'courses' );\n\t\t$memberships_id = llms_get_page_id( 'memberships' );\n\t\t$blog_id        = absint( get_option( 'page_for_posts', 0 ) );\n\n\t\tforeach ( $menu_items as $key => $item ) {\n\n\t\t\t$classes   = $item->classes;\n\t\t\t$object_id = absint( $item->object_id );\n\n\t\t\t// Remove active class from blog archive.\n\t\t\tif ( $blog_id === $object_id ) {\n\n\t\t\t\t$menu_items[ $key ]->current = false;\n\t\t\t\tforeach ( array( 'current_page_parent', 'current-menu-item' ) as $class ) {\n\t\t\t\t\tif ( in_array( $class, $classes, true ) ) {\n\t\t\t\t\t\tunset( $classes[ array_search( $class, $classes, true ) ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} elseif ( 'page' === $item->object && ( ( is_courses() && $courses_id === $object_id ) || ( is_memberships() && $memberships_id === $object_id ) ) ) {\n\n\t\t\t\t$menu_items[ $key ]->current = true;\n\t\t\t\t$classes[]                   = 'current-menu-item';\n\t\t\t\t$classes[]                   = 'current_page_item';\n\n\t\t\t\t// Set parent links for courses & memberships.\n\t\t\t} elseif ( ( $courses_id === $object_id && ( is_singular( 'course' ) || is_course_taxonomy() ) ) || ( $memberships_id === $object_id && ( is_singular( 'llms_membership' ) || is_membership_taxonomy() ) ) ) {\n\n\t\t\t\t$classes[] = 'current_page_parent';\n\n\t\t\t}\n\n\t\t\t$menu_items[ $key ]->classes = array_unique( $classes );\n\n\t\t}\n\n\t\treturn $menu_items;\n\t}\n\n\t/**\n\t * Output the metabox.\n\t *\n\t * @since 3.14.7\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function output() {\n\n\t\t?>\n\t\t<div id=\"posttype-llms-nav-items\" class=\"posttypediv\">\n\t\t\t<div id=\"tabs-panel-llms-nav-items\" class=\"tabs-panel tabs-panel-active\">\n\t\t\t\t<ul id=\"llms-nav-items-checklist\" class=\"categorychecklist form-no-clear\">\n\t\t\t\t\t<?php\n\t\t\t\t\t$i = -1;\n\t\t\t\t\tforeach ( $this->get_nav_items() as $key => $data ) :\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t<label class=\"menu-item-title\">\n\t\t\t\t\t\t\t\t<input type=\"checkbox\" class=\"menu-item-checkbox\" name=\"menu-item[<?php echo esc_attr( $i ); ?>][menu-item-object-id]\" value=\"<?php echo esc_attr( $i ); ?>\" /> <?php echo esc_html( $data['label'] ); ?>\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<input type=\"hidden\" class=\"menu-item-type\" name=\"menu-item[<?php echo esc_attr( $i ); ?>][menu-item-type]\" value=\"custom\" />\n\t\t\t\t\t\t\t<input type=\"hidden\" class=\"menu-item-title\" name=\"menu-item[<?php echo esc_attr( $i ); ?>][menu-item-title]\" value=\"<?php echo esc_html( $data['title'] ); ?>\" />\n\t\t\t\t\t\t\t<input type=\"hidden\" class=\"menu-item-url\" name=\"menu-item[<?php echo esc_attr( $i ); ?>][menu-item-url]\" value=\"<?php echo esc_url( $data['url'] ); ?>\" />\n\t\t\t\t\t\t\t<input type=\"hidden\" class=\"menu-item-classes\" name=\"menu-item[<?php echo esc_attr( $i ); ?>][menu-item-classes]\" value=\"<?php echo esc_attr( 'llms-nav-item-' . $key ); ?>\" />\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t--$i;\n\t\t\t\t\tendforeach;\n\t\t\t\t\t?>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t\t<p class=\"button-controls\">\n\t\t\t\t<span class=\"list-controls\">\n\t\t\t\t\t<a href=\"<?php echo esc_url( admin_url( 'nav-menus.php?page-tab=all&selectall=1#posttype-llms-nav-items' ) ); ?>\" class=\"select-all\"><?php esc_html_e( 'Select all', 'lifterlms' ); ?></a>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"add-to-menu\">\n\t\t\t\t\t<input type=\"submit\" class=\"button-secondary submit-add-to-menu right\" value=\"<?php esc_attr_e( 'Add to menu', 'lifterlms' ); ?>\" name=\"add-post-type-menu-item\" id=\"submit-posttype-llms-nav-items\">\n\t\t\t\t\t<span class=\"spinner\"></span>\n\t\t\t\t</span>\n\t\t\t</p>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Output JS to ensure that users don't edit the #llms-signout URL that's replaced dynamically with an actual signout link.\n\t *\n\t * @since 3.14.7\n\t *\n\t * @return void\n\t */\n\tpublic function output_scripts() {\n\t\t?>\n\t\t<script type=\"text/javascript\">\n\t\tjQuery( '#menu-to-edit' ).on( 'click', 'a.item-edit', function() {\n\t\t\tvar $settings = jQuery(this).closest( '.menu-item-bar' ).next( '.menu-item-settings' ),\n\t\t\t\t$url = $settings.find( '.edit-menu-item-url' );\n\n\t\t\tif ( 0 === $url.val().indexOf( '#llms-sign' ) ) {\n\t\t\t\t$url.closest( 'p.field-url' ).css( 'display', 'none' );\n\t\t\t}\n\t\t} );\n\t\t</script>\n\t\t<?php\n\t}\n\n\t/**\n\t * Register navigation link block.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function register_block() {\n\t\t$block_dir = LLMS_PLUGIN_DIR . 'blocks/navigation-link';\n\n\t\tif ( file_exists( \"$block_dir/block.json\" ) ) {\n\t\t\tregister_block_type( $block_dir );\n\t\t}\n\t}\n\n\t/**\n\t * Render the navigation link block.\n\t *\n\t * @since 7.2.0\n\t * @since 7.3.0 Add block name check since filter changed.\n\t *\n\t * @param string $block_content Block content.\n\t * @param array  $block Block data.\n\t * @return string\n\t */\n\tpublic function render_block( string $block_content, array $block ): string {\n\n\t\tif ( 'llms/navigation-link' !== $block['blockName'] ) {\n\t\t\treturn $block_content;\n\t\t}\n\n\t\t$items = $this->filter_nav_items( $this->get_nav_items() );\n\t\t$page  = $block['attrs']['page'] ?? 'dashboard';\n\n\t\tif ( ! $page ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$url = $items[ $page ]['url'] ?? '';\n\n\t\t// Support conditional URLs, e.g. when user logged in or not.\n\t\tif ( ! $url ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$label = $block['attrs']['label'] ?? $items[ $page ]['label'] ?? '';\n\n\t\t$html  = '<li class=\"wp-block-navigation-item\">';\n\t\t$html .= '<a href=\"' . esc_url( $url ) . '\" class=\"wp-block-navigation-item__content\">';\n\t\t$html .= '<span class=\"wp-block-navigation-item__label\">';\n\t\t$html .= esc_html( $label );\n\t\t$html .= '</span></a></li>';\n\n\t\treturn $html;\n\t}\n\n\t/**\n\t * Add LifterLMS nav menu item data to block editor.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function enqueue_block_editor_assets() {\n\t\t$links = array();\n\n\t\tforeach ( $this->get_nav_items() as $key => $data ) {\n\t\t\t$links[ $key ] = $data['label'];\n\t\t}\n\n\t\twp_localize_script(\n\t\t\t'llms-navigation-link-editor-script',\n\t\t\t'llmsNavMenuItems',\n\t\t\t$links\n\t\t);\n\t}\n}\n\nreturn new LLMS_Nav_Menus();\n"
  },
  {
    "path": "includes/class.llms.oembed.php",
    "content": "<?php\n/**\n * Handle custom oEmbed Providers\n *\n * @package LifterLMS/Classes\n *\n * @since 1.4.6\n * @version 1.4.6\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Handle custom oEmbed Providers\n *\n * @since 1.4.6\n */\nclass LLMS_OEmbed {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 1.4.6\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t/**\n\t\t * Add oEmbed Provider for Wistia\n\t\t *\n\t\t * @since 1.4.6\n\t\t */\n\t\twp_oembed_add_provider( '/https?\\:\\/\\/(.+)?(wistia\\.com|wi\\.st)\\/.*/', 'https://fast.wistia.com/oembed', true );\n\n\t}\n\n}\n\nreturn new LLMS_OEmbed();\n"
  },
  {
    "path": "includes/class.llms.payment.gateways.php",
    "content": "<?php\n/**\n * LLMS_Payment_Gateways class file.\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 6.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Manage and access LifterLMS payment gateways.\n *\n * @since 1.0.0\n * @since 3.0.0 Unknown.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Removed the deprecated `LLMS_Payment_Gateways::$_instance` property.\n */\nclass LLMS_Payment_Gateways {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Payment Gateways\n\t *\n\t * @var LLMS_Payment_Gateway[]\n\t */\n\tpublic $payment_gateways = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'lifterlms_payment_gateways', array( $this, 'add_core_gateways' ) );\n\n\t\t/**\n\t\t * Filters the list of registered LifterLMS Payment Gateway classes.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string[] $gateways Array of payment gateway class names.\n\t\t */\n\t\t$gateways = apply_filters( 'lifterlms_payment_gateways', $this->payment_gateways );\n\n\t\tforeach ( $gateways as $gateway ) {\n\n\t\t\t$load_gateway = new $gateway();\n\n\t\t\t$order = absint( $load_gateway->get_display_order() );\n\n\t\t\t// If the order already exists create a new order for it.\n\t\t\tif ( isset( $this->payment_gateways[ $order ] ) ) {\n\t\t\t\t$order = max( array_keys( $this->payment_gateways ) ) + 1;\n\t\t\t}\n\n\t\t\t$this->payment_gateways[ $order ] = $load_gateway;\n\t\t}\n\n\t\tksort( $this->payment_gateways );\n\n\t}\n\n\t/**\n\t * Register core gateways.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string[] $gateways Array of gateway class names.\n\t * @return string[]\n\t */\n\tpublic function add_core_gateways( $gateways ) {\n\t\t$gateways[] = 'LLMS_Payment_Gateway_Manual';\n\t\treturn $gateways;\n\t}\n\n\t/**\n\t * Get only enabled payment gateways.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_enabled_payment_gateways() {\n\n\t\t$gateways = array();\n\t\tforeach ( $this->get_payment_gateways() as $gateway ) {\n\t\t\tif ( $gateway->is_enabled() ) {\n\t\t\t\t$gateways[ $gateway->get_id() ] = $gateway;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters the registered LifterLMS Payment Gateways which are explicitly enabled.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param LLMS_Payment_Gateway[] $gateways List of enabled gateways.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_enabled_payment_gateways', $gateways );\n\n\t}\n\n\t/**\n\t * Retrieves the default payment gateway ID.\n\t *\n\t * The default gateway is the first gateway in the list of enabled gateways.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_default_gateway() {\n\n\t\t$gateways = $this->get_enabled_payment_gateways();\n\t\t$ids      = array_keys( $gateways );\n\t\treturn array_shift( $ids );\n\n\t}\n\n\t/**\n\t * Retrieves a payment gateway object by the gateway ID.\n\t *\n\t * @since 2.5.0\n\t *\n\t * @param string $id  id of the gateway (paypal, stripe, etc...)\n\t * @return LLMS_Payment_Gateway|boolean Returns the gateway if it's registered, otherwise `false`.\n\t */\n\tpublic function get_gateway_by_id( $id ) {\n\n\t\t$gateways = $this->get_payment_gateways();\n\n\t\tif ( array_key_exists( $id, $gateways ) ) {\n\t\t\treturn $gateways[ $id ];\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Retrieves all registered payment gateways.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return LLMS_Payment_Gateway[]\n\t */\n\tpublic function get_payment_gateways() {\n\n\t\t$gateways = array();\n\t\tforeach ( $this->payment_gateways as $gateway ) {\n\t\t\t$gateways[ $gateway->id ] = $gateway;\n\t\t}\n\t\treturn $gateways;\n\n\t}\n\n\t/**\n\t * Retrieves all enabled gateways which support the specified gateway feature.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @see LLMS_Payment_Gateway::get_supported_features()\n\t *\n\t * @param string $feature A gateway feature string.\n\t * @return array\n\t */\n\tpublic function get_supporting_gateways( $feature ) {\n\n\t\t$gateways = array();\n\t\tforeach ( $this->get_enabled_payment_gateways() as $id => $gateway ) {\n\t\t\tif ( $gateway->supports( $feature ) ) {\n\t\t\t\t$gateways[ $id ] = $gateway;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters the list of gateways supporting the specified feature.\n\t\t *\n\t\t * Hook description.\n\t\t *\n\t\t * @since 3.10.0\n\t\t *\n\t\t * @param LLMS_Payment_Gateway[] $gateways Array of supporting gateways.\n\t\t * @param string                 $feature  The requested gateway feature string.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_supporting_payment_gateways', $gateways, $feature );\n\n\t}\n\n\t/**\n\t * Determines if any payment gateways are registered.\n\t *\n\t * @since 3.0.0\n\t * @since 6.5.0 Refactor for code simplicity.\n\t *\n\t * @param boolean $enabled Whether or not to check against only enabled gateways.\n\t * @return boolean\n\t */\n\tpublic function has_gateways( $enabled = false ) {\n\t\t$method = $enabled ? 'get_enabled_payment_gateways' : 'get_payment_gateways';\n\t\treturn count( $this->{$method}() ) >= 1;\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.person.handler.php",
    "content": "<?php\n/**\n * User Handling for login and registration (mostly)\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Person_Handler class.\n *\n * @since 3.0.0\n * @since 3.35.0 Sanitize field data when filling field with user-submitted data.\n * @since 5.0.0 Private methods `LLMS_Person_Handler::fill_fields()` and `LLMS_Person_Handler::insert_data()` were removed.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Person_Handler::register()` method\n *              - `LLMS_Person_Handler::sanitize_field() method`\n *              - `LLMS_Person_Handler::update()` method\n *              - `LLMS_Person_Handler::validate_fields()` method\n *              - `LLMS_Person_Handler::voucher_toggle_script()` method\n */\nclass LLMS_Person_Handler {\n\n\t/**\n\t * Prefix for all user meta field keys\n\t *\n\t * @var string\n\t */\n\tprivate static $meta_prefix = 'llms_';\n\n\t/**\n\t * Prevents the hacky voucher script from being output multiple times\n\t *\n\t * @var boolean\n\t */\n\tprivate static $voucher_script_output = false;\n\n\t/**\n\t * Locate password fields from a given form location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location From location.\n\t * @return false|array[]\n\t */\n\tprotected static function find_password_fields( $location ) {\n\n\t\t$forms = LLMS_Forms::instance();\n\t\t$all   = $forms->get_form_fields( $location );\n\n\t\t$pwd = $forms->get_field_by( (array) $all, 'id', 'password' );\n\n\t\t// If we don't have a password in the form return early.\n\t\tif ( ! $pwd ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Setup the return array.\n\t\t$fields = array( $pwd );\n\n\t\t// Add confirmation and strength meter if they exist.\n\t\tforeach ( array( 'password_confirm', 'llms-password-strength-meter' ) as $id ) {\n\n\t\t\t$field = $forms->get_field_by( $all, 'id', $id );\n\t\t\tif ( $field ) {\n\n\t\t\t\t// If we have a confirmation field ensure that the fields sit side by side.\n\t\t\t\tif ( 'password_confirm' === $id ) {\n\n\t\t\t\t\t$fields[0]['columns']         = 6;\n\t\t\t\t\t$fields[0]['last_column']     = false;\n\t\t\t\t\t$fields[0]['wrapper_classes'] = array();\n\n\t\t\t\t\t$field['columns']         = 6;\n\t\t\t\t\t$field['last_column']     = true;\n\t\t\t\t\t$field['wrapper_classes'] = array();\n\n\t\t\t\t}\n\n\t\t\t\t$fields[] = $field;\n\t\t\t}\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Generate a unique login based on the user's email address\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.4 Unknown.\n\t *\n\t * @param string $email User's email address.\n\t * @return string\n\t */\n\tpublic static function generate_username( $email ) {\n\n\t\t/**\n\t\t * Allow custom username generation\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $custom_username The custom-generated username. If the filter returns a truthy string it will be used in favor\n\t\t *                                of the automatically generated username.\n\t\t * @param string $email           User's email address.\n\t\t */\n\t\t$custom_username = apply_filters( 'lifterlms_generate_username', null, $email );\n\t\tif ( $custom_username && is_string( $custom_username ) ) {\n\t\t\treturn $custom_username;\n\t\t}\n\n\t\t$username      = sanitize_user( current( explode( '@', $email ) ), true );\n\t\t$orig_username = $username;\n\t\t$i             = 1;\n\t\twhile ( username_exists( $username ) ) {\n\n\t\t\t$username = $orig_username . $i;\n\t\t\t++$i;\n\n\t\t}\n\n\t\t/**\n\t\t * Modify an auto-generated username before it is used\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $username The generated user name.\n\t\t * @param string $email    User's email address which was used to generate the username.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_generated_username', $username, $email );\n\t}\n\n\t/**\n\t * Get the fields for the login form\n\t *\n\t * @since 3.0.0\n\t * @since 3.0.4 Unknown.\n\t * @since 5.0.0 Remove usage of the deprecated `lifterlms_registration_generate_username`.\n\t *\n\t * @param string $layout Form layout. Accepts \"columns\" (default) or \"stacked\".\n\t * @return array[] An array of form field arrays.\n\t */\n\tpublic static function get_login_fields( $layout = 'columns' ) {\n\n\t\t$usernames = LLMS_Forms::instance()->are_usernames_enabled();\n\n\t\t/**\n\t\t * Customize the fields used to build the user login form\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @param array[] $fields An array of form field arrays.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'lifterlms_person_login_fields',\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'columns'     => ( 'columns' == $layout ) ? 6 : 12,\n\t\t\t\t\t'id'          => 'llms_login',\n\t\t\t\t\t'label'       => ! $usernames ? __( 'Email Address', 'lifterlms' ) : __( 'Username or Email Address', 'lifterlms' ),\n\t\t\t\t\t'last_column' => ( 'columns' == $layout ) ? false : true,\n\t\t\t\t\t'required'    => true,\n\t\t\t\t\t'type'        => ! $usernames ? 'email' : 'text',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'columns'           => ( 'columns' == $layout ) ? 6 : 12,\n\t\t\t\t\t'id'                => 'llms_password',\n\t\t\t\t\t'label'             => __( 'Password', 'lifterlms' ),\n\t\t\t\t\t'last_column'       => ( 'columns' == $layout ) ? true : true,\n\t\t\t\t\t'required'          => true,\n\t\t\t\t\t'type'              => 'password',\n\t\t\t\t\t'visibility_toggle' => true,\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'columns'     => ( 'columns' == $layout ) ? 3 : 12,\n\t\t\t\t\t'classes'     => 'llms-button-action',\n\t\t\t\t\t'id'          => 'llms_login_button',\n\t\t\t\t\t'value'       => __( 'Login', 'lifterlms' ),\n\t\t\t\t\t'last_column' => ( 'columns' == $layout ) ? false : true,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'columns'     => ( 'columns' == $layout ) ? 6 : 6,\n\t\t\t\t\t'id'          => 'llms_remember',\n\t\t\t\t\t'label'       => __( 'Remember me', 'lifterlms' ),\n\t\t\t\t\t'last_column' => false,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'checkbox',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'columns'         => ( 'columns' == $layout ) ? 3 : 6,\n\t\t\t\t\t'id'              => 'llms_lost_password',\n\t\t\t\t\t'last_column'     => true,\n\t\t\t\t\t'description'     => '<a href=\"' . esc_url( wp_lostpassword_url() ) . '\">' . __( 'Lost your password?', 'lifterlms' ) . '</a>',\n\t\t\t\t\t'type'            => 'html',\n\t\t\t\t\t'wrapper_classes' => 'align-right',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve fields for password recovery\n\t *\n\t * Used to generate the form where a username/email is entered to start the password reset process.\n\t *\n\t * @since 3.8.0\n\t * @since 5.0.0 Use LLMS_Forms::are_usernames_enabled() in favor of deprecated option \"lifterlms_registration_generate_username\".\n\t *               Remove field values set to the default value for a form field.\n\t *\n\t * @return array[] An array of form field arrays.\n\t */\n\tpublic static function get_lost_password_fields() {\n\n\t\t$usernames = LLMS_Forms::instance()->are_usernames_enabled();\n\n\t\tif ( ! $usernames ) {\n\t\t\t$message = __( 'Lost your password? Enter your email address and we will send you a link to reset it.', 'lifterlms' );\n\t\t} else {\n\t\t\t$message = __( 'Lost your password? Enter your username or email address and we will send you a link to reset it.', 'lifterlms' );\n\t\t}\n\n\t\t/**\n\t\t * Filter the message displayed on the lost password form.\n\t\t *\n\t\t * @since Unknown.\n\t\t *\n\t\t * @param string $message The message displayed before the form.\n\t\t */\n\t\t$message = apply_filters( 'lifterlms_lost_password_message', $message );\n\n\t\t/**\n\t\t * Filter the form fields displayed for the lost password form.\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param array[] $fields An array of form field arrays.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'lifterlms_lost_password_fields',\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'id'    => 'llms_lost_password_message',\n\t\t\t\t\t'type'  => 'html',\n\t\t\t\t\t'value' => $message,\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'id'       => 'llms_login',\n\t\t\t\t\t'label'    => ! $usernames ? __( 'Email Address', 'lifterlms' ) : __( 'Username or Email Address', 'lifterlms' ),\n\t\t\t\t\t'required' => true,\n\t\t\t\t\t'type'     => ! $usernames ? 'email' : 'text',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'classes' => 'llms-button-action auto',\n\t\t\t\t\t'id'      => 'llms_lost_password_button',\n\t\t\t\t\t'value'   => __( 'Reset Password', 'lifterlms' ),\n\t\t\t\t\t'type'    => 'submit',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an array of password fields.\n\t *\n\t * This is only used on the password rest form as a fallback\n\t * when no \"custom\" password fields can be found in either of the default\n\t * checkout or registration forms.\n\t *\n\t * @since 3.7.0\n\t * @since 5.0.0 Removed optional parameters\n\t *\n\t * @return array[]\n\t */\n\tprivate static function get_password_fields() {\n\n\t\t$fields = array();\n\n\t\t$fields[] = array(\n\t\t\t'columns'           => 6,\n\t\t\t'classes'           => 'llms-password',\n\t\t\t'id'                => 'password',\n\t\t\t'label'             => __( 'Password', 'lifterlms' ),\n\t\t\t'last_column'       => false,\n\t\t\t'match'             => 'password_confirm',\n\t\t\t'required'          => true,\n\t\t\t'type'              => 'password',\n\t\t\t'visibility_toggle' => true,\n\t\t);\n\t\t$fields[] = array(\n\t\t\t'columns'  => 6,\n\t\t\t'classes'  => 'llms-password-confirm',\n\t\t\t'id'       => 'password_confirm',\n\t\t\t'label'    => __( 'Confirm Password', 'lifterlms' ),\n\t\t\t'match'    => 'password',\n\t\t\t'required' => true,\n\t\t\t'type'     => 'password',\n\t\t);\n\n\t\t$fields[] = array(\n\t\t\t'classes'      => 'llms-password-strength-meter',\n\t\t\t'description'  => __( 'A strong password is required. The password must be at least 6 characters in length. Consider adding letters, numbers, and symbols to increase the password strength.', 'lifterlms' ),\n\t\t\t'id'           => 'llms-password-strength-meter',\n\t\t\t'type'         => 'html',\n\t\t\t'min_length'   => 6,\n\t\t\t'min_strength' => 'weak',\n\t\t);\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Retrieve form fields used on the password reset form.\n\t *\n\t * This method will attempt to the \"custom\" password fields in the checkout form\n\t * and then in the registration form. At least a password field must be found. If\n\t * it cannot be found this function falls back to a set of default fields as defined\n\t * in the LLMS_Person_Handler::get_password_fields() method.\n\t *\n\t * @since Unknown\n\t * @since 5.0.0 Get fields from the checkout or registration forms before falling back to default fields.\n\t *              Changed filter on return from \"lifterlms_lost_password_fields\" to \"llms_password_reset_fields\".\n\t *\n\t * @param string $key User password reset key, usually populated via $_GET vars.\n\t * @param string $login User login (username), usually populated via $_GET vars.\n\t * @return array[]\n\t */\n\tpublic static function get_password_reset_fields( $key = '', $login = '' ) {\n\n\t\t$fields = array();\n\t\tforeach ( array( 'checkout', 'registration' ) as $location ) {\n\t\t\t$fields = self::find_password_fields( $location );\n\t\t\tif ( $fields ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Fallback if no custom fields are found.\n\t\tif ( ! $fields ) {\n\t\t\t$location = 'fallback';\n\t\t\t$fields   = self::get_password_fields();\n\t\t}\n\n\t\t// Add button.\n\t\t$fields[] = array(\n\t\t\t'classes' => 'llms-button-action auto',\n\t\t\t'id'      => 'llms_lost_password_button',\n\t\t\t'type'    => 'submit',\n\t\t\t'value'   => __( 'Reset Password', 'lifterlms' ),\n\t\t);\n\n\t\t// Add hidden fields.\n\t\t$fields[] = array(\n\t\t\t'id'    => 'llms_reset_key',\n\t\t\t'type'  => 'hidden',\n\t\t\t'value' => $key,\n\t\t);\n\t\t$fields[] = array(\n\t\t\t'id'    => 'llms_reset_login',\n\t\t\t'type'  => 'hidden',\n\t\t\t'value' => $login,\n\t\t);\n\n\t\t/**\n\t\t * Filter password reset form fields.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array[] $fields   Array of form field arrays.\n\t\t * @param string  $key      User password reset key, usually populated via $_GET vars.\n\t\t * @param string  $login    User login (username), usually populated via $_GET vars.\n\t\t * @param string  $location Location where the fields were retrieved from. Either \"checkout\", \"registration\", or \"fallback\".\n\t\t *                          Fallback denotes that no password field was located in either of the previous forms so a default\n\t\t *                          set of fields is generated programmatically.\n\t\t */\n\t\treturn apply_filters( 'llms_password_reset_fields', $fields, $key, $login, $location );\n\t}\n\n\t/**\n\t * Login a user\n\t *\n\t * @since 3.0.0\n\t * @since 3.29.4 Unknown.\n\t * @since 5.0.0 Removed email lookup logic since `wp_authenticate()` supports email addresses as `user_login` since WP 4.5.\n\t *\n\t * @param array $data {\n\t *     User login information.\n\t *\n\t *     @type string $llms_login User email address or username.\n\t *     @type string $llms_password User password.\n\t *     @type string $llms_remember Whether to extend the cookie duration to keep the user logged in for a longer period.\n\t * }\n\t * @return WP_Error|int The WP_User ID on login success or an error object on failure.\n\t */\n\tpublic static function login( $data ) {\n\n\t\t/**\n\t\t * Run an action prior to user login.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param array $data {\n\t\t *    User login credentials.\n\t\t *\n\t\t *    @type string $user_login User's username.\n\t\t *    @type string $password User's password.\n\t\t *    @type bool $remeber Whether to extend the cookie duration to keep the user logged in for a longer period.\n\t\t * }\n\t\t */\n\t\tdo_action( 'lifterlms_before_user_login', $data );\n\n\t\t/**\n\t\t * Filter user submitted login data prior to data validation.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param array $data {\n\t\t *    User login credentials.\n\t\t *\n\t\t *    @type string $user_login User's username.\n\t\t *    @type string $password User's password.\n\t\t *    @type bool $remeber Whether to extend the cookie duration to keep the user logged in for a longer period.\n\t\t * }\n\t\t */\n\t\t$data = apply_filters( 'lifterlms_user_login_data', $data );\n\n\t\t// Validate the fields & allow custom validation to occur.\n\t\t$valid = self::validate_login_fields( $data );\n\n\t\t// If errors found, return them.\n\t\tif ( is_wp_error( $valid ) ) {\n\n\t\t\t/**\n\t\t\t * Filters the errors found during a LifterLMS user login attempt\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param WP_Error       $valid  Error object containing information about the login error.\n\t\t\t * @param array          $data   User submitted login form data.\n\t\t\t * @param WP_Error|false $signon The original WP Error object returned by `wp_signon()` or false if the error\n\t\t\t *                               is encountered prior to the signon attempt.\n\t\t\t */\n\t\t\treturn apply_filters( 'lifterlms_user_login_errors', $valid, $data, false );\n\n\t\t}\n\n\t\t$creds = array(\n\t\t\t'user_login'    => wp_unslash( $data['llms_login'] ), // Unslash ensures that an email address with an apostrophe is unescaped for lookups.\n\t\t\t'user_password' => $data['llms_password'],\n\t\t\t'remember'      => isset( $data['llms_remember'] ),\n\t\t);\n\n\t\t/**\n\t\t * Filter a user's login credentials immediately prior to signing in.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array $creds {\n\t\t *    User login credentials.\n\t\t *\n\t\t *    @type string $user_login User's username.\n\t\t *    @type string $password User's password.\n\t\t *    @type bool $remeber Whether to extend the cookie duration to keep the user logged in for a longer period.\n\t\t * }\n\t\t */\n\t\t$creds  = apply_filters( 'lifterlms_login_credentials', $creds );\n\t\t$signon = wp_signon( $creds, is_ssl() );\n\n\t\tif ( is_wp_error( $signon ) ) {\n\n\t\t\t$err = new WP_Error( 'login-error', __( 'Could not find an account with the supplied email address and password combination.', 'lifterlms' ) );\n\t\t\t// This hook is documented in includes/class.llms.person.handler.php.\n\t\t\treturn apply_filters( 'lifterlms_user_login_errors', $err, $data, $signon );\n\n\t\t}\n\n\t\treturn $signon->ID;\n\t}\n\n\t/**\n\t * Validate login form fields\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $data Array of user-submitted data, usually from `$_POST`.\n\t * @return WP_Error|true Returns an error object or `true` if the submission is valid.\n\t */\n\tprotected static function validate_login_fields( $data ) {\n\n\t\t$err = new WP_Error();\n\n\t\t$fields = self::get_login_fields();\n\n\t\tforeach ( $fields as $field ) {\n\n\t\t\t$name  = isset( $field['name'] ) ? $field['name'] : $field['id'];\n\t\t\t$label = isset( $field['label'] ) ? $field['label'] : $name;\n\n\t\t\t$field_type = isset( $field['type'] ) ? $field['type'] : '';\n\t\t\t$val        = isset( $data[ $name ] ) ? $data[ $name ] : '';\n\n\t\t\t// Ensure required fields are submitted.\n\t\t\tif ( ! empty( $field['required'] ) && empty( $val ) ) {\n\n\t\t\t\t$err->add( $field['id'], sprintf( __( '%s is a required field', 'lifterlms' ), $label ), 'required' );\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\t// Email fields must be emails.\n\t\t\tif ( 'email' === $field_type && ! is_email( $val ) ) {\n\t\t\t\t$err->add( $field['id'], sprintf( __( '%s must be a valid email address', 'lifterlms' ), $label ), 'invalid' );\n\t\t\t}\n\t\t}\n\n\t\t$valid = $err->has_errors() ? $err : true;\n\n\t\t/**\n\t\t * Filters the validation result of user-submitted login data\n\t\t *\n\t\t * @since 4.21.0\n\t\t *\n\t\t * @param WP_Error|boolean $valid An error object containing validation errors or `true` if no validation errors found.\n\t\t * @param array            $data  User submitted login data.\n\t\t */\n\t\treturn apply_filters( 'llms_after_user_login_data_validation', $valid, $data );\n\t}\n\n\t/**\n\t * Retrieve an array of fields for a specific screen\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.0 Unknown.\n\t * @deprecated 5.0.0 `LLMS_Person_Handler::get_available_fields()` is deprecated in favor of `LLMS_Forms::get_form_fields()`.\n\t *\n\t * @param string    $screen Name os the screen [account|checkout|registration].\n\t * @param array|int $data   Array of data to fill fields with or a WP User ID.\n\t * @return array\n\t */\n\tpublic static function get_available_fields( $screen = 'registration', $data = array() ) {\n\t\t_deprecated_function( 'LLMS_Person_Handler::get_available_fields()', '5.0.0', 'LLMS_Forms::get_form_fields()' );\n\t\treturn LLMS_Forms::instance()->get_form_fields( $screen );\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.playnice.php",
    "content": "<?php\n/**\n * Make LifterLMS play nicely with other plugins, themes, & webhosts\n *\n * * * * * * * * * * * * * * * * * *\n * True, there is no joy           *\n * in software conflicts (or war)  *\n * Here we are, trying             *\n * * * * * * * * * * * * * * * * * *\n *\n * @package LifterLMS/Classes\n *\n * @since 3.1.3\n * @version 6.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_PlayNice class\n *\n * @since 3.1.3\n * @since 3.31.0 Resolve dashboard endpoint 404s resulting from changes in WC 3.6.\n * @since 3.37.17 Changed the way we handle the dashboard endpoints conflict, using a different wc filter hook.\n *                Deprecated `LLMS_PlayNice::wc_is_account_page()`.\n * @since 3.37.18 Resolve Divi/WC conflict encountered using the frontend pagebuilder on courses and memberships.\n * @since 4.0.0 Removed previously deprecated method `LLMS_PlayNice::wc_is_account_page()`.\n *              Remove Divi Frontend Builder WC conflict code.\n */\nclass LLMS_PlayNice {\n\n\t/**\n\t * Hold temporary variables used by methods in this class.\n\t *\n\t * @var array\n\t */\n\tprivate $temp_vars = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.1.3\n\t * @since 3.31.0 Add `plugins_loaded` hook.\n\t * @since 6.8.0 Account for BuddyBoss compatibility issue.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t// Optimize press live editor initialization.\n\t\tadd_action( 'op_liveeditor_init', array( $this, 'wp_optimizepress_live_editor' ) );\n\n\t\t// WPEngine heartbeat fix.\n\t\tadd_filter( 'wpe_heartbeat_allowed_pages', array( $this, 'wpe_heartbeat_allowed_pages' ) );\n\n\t\t// BuddyBoss profile nav compatibility issue fix (the nav is set up at priority 6).\n\t\tadd_action( 'bp_init', array( $this, 'buddyboss_compatibility' ), 5 );\n\n\t\t// Load other playnice things based on the presence of other plugins.\n\t\tadd_action( 'init', array( $this, 'plugins_loaded' ), 11 );\n\n\t}\n\n\t/**\n\t * Compatibility for BuddyBoss.\n\t *\n\t * @since 6.8.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/2142#issuecomment-1157924080.\n\t *\n\t * @return void\n\t */\n\tpublic function buddyboss_compatibility() {\n\n\t\tif ( ! function_exists( 'is_plugin_active' ) || ! function_exists( 'bp_is_my_profile' ) || bp_is_my_profile() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tis_plugin_active( 'buddyboss-platform/bp-loader.php' ) ||\n\t\t\t( is_multisite() && is_plugin_active_for_network( 'buddyboss-platform/bp-loader.php' ) )\n\t\t) {\n\t\t\t$plugin_data    = get_plugin_data( trailingslashit( WP_PLUGIN_DIR ) . 'buddyboss-platform/bp-loader.php' );\n\t\t\t$plugin_version = ! empty( $plugin_data['Version'] ) ? $plugin_data['Version'] : 0;\n\t\t\tif ( $plugin_version && version_compare( $plugin_version, '2.0.3', '>=' ) ) {\n\t\t\t\t// Nothing to do.\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Do not add our profile nav items when not in front-end (and not in \"my profile\"), to avoid a fatal error.\n\t\t$bp_integration = llms()->integrations()->get_integration( 'buddypress' );\n\t\tremove_action( 'bp_setup_nav', array( $bp_integration, 'add_profile_nav_items' ) );\n\n\t}\n\n\t/**\n\t * Conditionally add hooks after the other plugin is loaded.\n\t *\n\t * @since 3.31.0\n\t * @since 3.37.17 Changed the way we handle endpoints conflict, using a different WC filter hook.\n\t * @since 3.37.18 Add fix for Divi Frontend-Builder WC conflict.\n\t * @since 4.0.0 Remove Divi Frontend Builder WC conflict code.\n\t *\n\t * @return void\n\t */\n\tpublic function plugins_loaded() {\n\n\t\t$wc_exists = function_exists( 'WC' );\n\n\t\tif ( $wc_exists ) {\n\t\t\tadd_filter( 'woocommerce_account_endpoint_page_not_found', array( $this, 'wc_account_endpoint_page_not_found' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Allow our dashboard endpoints sharing a query var with WC to function\n\t *\n\t * Inform WC that it should not force a 404 because we're on a valid endpoint.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/849\n\t *\n\t * @param bool $is_page_not_found True from `woocommerce_account_endpoint_page_not_found` filter.\n\t * @return bool\n\t */\n\tpublic function wc_account_endpoint_page_not_found( $is_page_not_found ) {\n\n\t\tif ( is_llms_account_page() && is_wc_endpoint_url() ) {\n\t\t\t$is_page_not_found = false;\n\t\t}\n\n\t\treturn $is_page_not_found;\n\n\t}\n\n\t/**\n\t * OptimizePress LiveEditor fix.\n\t *\n\t * The live editor for OptimizePress does not work because it is trying to load a frontend environment\n\t * in the admin area and needs access to LifterLMS frontend files.\n\t *\n\t * This function loads all frontend files when the OptimizePress live editor is initialized.\n\t *\n\t * @since 3.2.2\n\t * @since 3.19.6 Unknown.\n\t * @since 4.0.0 Removed inclusion of removed 'class.llms.person.php' file.\n\t * @since 5.0.0 Remove inclusion of removed files:\n\t *                    + forms/frontend/class.llms.frontend.forms.php\n\t *                    + forms/frontend/class.llms.frontend.password.php\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic function wp_optimizepress_live_editor() {\n\n\t\t// These files are necessary to get optimizepress ajax to play nicely in the liveeditor.\n\t\tinclude_once 'class.llms.ajax.php';\n\t\tinclude_once 'class.llms.ajax.handler.php';\n\n\t\t// These files are all necessary to get the liveeditor to open.\n\t\tinclude_once 'llms.template.functions.php';\n\t\tinclude_once 'class.llms.https.php';\n\n\t\tinclude_once 'class.llms.template.loader.php';\n\t\tinclude_once 'class.llms.frontend.assets.php';\n\t}\n\n\t/**\n\t * WPE blocks the WordPress Heartbeat script from being loaded\n\t *\n\t * Event when it's explicitly defined as a dependency.\n\t *\n\t * @since 3.16.4\n\t *\n\t * @param array $pages List of pages that the heartbeat is allowed to load on.\n\t * @return array\n\t */\n\tpublic function wpe_heartbeat_allowed_pages( $pages ) {\n\n\t\tif ( is_admin() && isset( $_GET['page'] ) && 'llms-course-builder' === $_GET['page'] ) {\n\n\t\t\t$pages[] = 'admin.php';\n\n\t\t}\n\n\t\treturn $pages;\n\n\t}\n\n}\n\nreturn new LLMS_PlayNice();\n"
  },
  {
    "path": "includes/class.llms.post-types.php",
    "content": "<?php\n/**\n * Register Post Types, Taxonomies, Statuses.\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Post_Types class\n *\n * @since 1.0.0\n * @since 3.30.3 Removed duplicate array keys when registering course_tag taxonomy.\n * @since 3.33.0 `llms_question` post type is not publicly queryable anymore.\n * @since 3.34.1 Add the custom property `show_in_llms_rest` set to true by default, to those taxonomies we want to be shown in LLMS REST api.\n * @since 3.37.12 Added 'revisions' support to course, lesson, and llms_mebership post types.\n */\nclass LLMS_Post_Types {\n\n\t/**\n\t * Reference to the block templates list.\n\t *\n\t * @var array\n\t */\n\tprivate static $templates = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.4 Unknown.\n\t * @since 4.3.2 Add filter to deregister protected post types.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tadd_action( 'init', array( __CLASS__, 'add_membership_restriction_support' ) );\n\t\tadd_action( 'init', array( __CLASS__, 'register_post_types' ), 5 );\n\t\tadd_action( 'init', array( __CLASS__, 'register_post_statuses' ), 9 );\n\t\tadd_action( 'init', array( __CLASS__, 'register_taxonomies' ), 5 );\n\n\t\tadd_action( 'admin_bar_menu', array( __CLASS__, 'add_launch_course_builder_to_course_post_type_admin_bar' ), 100 );\n\n\t\tadd_filter( 'wp_sitemaps_post_types', array( __CLASS__, 'deregister_sitemap_post_types' ) );\n\n\t\tadd_action( 'after_setup_theme', array( __CLASS__, 'add_thumbnail_support' ), 777 );\n\t}\n\n\t/**\n\t * Add Launch Course Builder top admin bar button for Course posts.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function add_launch_course_builder_to_course_post_type_admin_bar( $wp_admin_bar ) {\n\t\tif ( is_admin() || ! is_singular( 'course' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'page'      => 'llms-course-builder',\n\t\t\t\t'course_id' => get_the_ID(),\n\t\t\t),\n\t\t\tadmin_url( 'admin.php' )\n\t\t);\n\n\t\t$icon_url = 'data:image/svg+xml;base64,' . base64_encode( file_get_contents( LLMS_PLUGIN_DIR . 'assets/images/lifterlms-icon-grey.svg' ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode\n\n\t\t$wp_admin_bar->add_node(\n\t\t\tarray(\n\t\t\t\t'id'    => 'course_launch_course_builder',\n\t\t\t\t'title' => '<div style=\"float:left;width:32px;height:32px;background-repeat:no-repeat;background-position:center;background-size:20px auto;background-image:url(\\'' . $icon_url . '\\')\" aria-hidden=\"true\"></div> ' . __( 'Launch Course Builder', 'lifterlms' ),\n\t\t\t\t'href'  => $url,\n\t\t\t\t'meta'  => array(\n\t\t\t\t\t'class' => 'llms-admin-bar-launch-course-builder',\n\t\t\t\t\t'title' => __( 'Launch Course Builder', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Add post type support for membership restrictions\n\t *\n\t * This enables the \"Membership Access\" metabox to display\n\t *\n\t * @since 3.0.0\n\t * @since 3.0.4 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function add_membership_restriction_support() {\n\n\t\t/**\n\t\t * Add llms-membership-restrictions support for the following post types.\n\t\t *\n\t\t * Adding support for a post type enables the display of the Membership Restriction metabox\n\t\t * for each specified post type.\n\t\t *\n\t\t * These post types can then be \"restricted\" to enrollment in the selected memberships.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string[] $post_types Array of post type names.\n\t\t */\n\t\t$post_types = apply_filters( 'llms_membership_restricted_post_types', array( 'post', 'page' ) );\n\t\tforeach ( $post_types as $post_type ) {\n\t\t\tadd_post_type_support( $post_type, 'llms-membership-restrictions' );\n\t\t}\n\t}\n\n\t/**\n\t * Ensure LifterLMS Post Types have thumbnail support\n\t *\n\t * @since 2.4.1\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic static function add_thumbnail_support() {\n\n\t\t// Ensure theme support exists for LifterLMS post types.\n\t\tif ( ! current_theme_supports( 'post-thumbnails' ) ) {\n\t\t\tadd_theme_support( 'post-thumbnails' );\n\t\t}\n\n\t\t$thumbnail_post_types = array(\n\t\t\t'course',\n\t\t\t'lesson',\n\t\t\t'llms_membership',\n\t\t);\n\n\t\tforeach ( $thumbnail_post_types as $p ) {\n\n\t\t\tadd_post_type_support( $p, 'thumbnail' );\n\n\t\t}\n\n\t\tadd_image_size( 'llms_notification_icon', 64, 64, true );\n\t}\n\n\t/**\n\t * De-register protected post types from wp-sitemap.xml\n\t *\n\t * @since 4.3.2\n\t *\n\t * @param WP_Post_Type[] $post_types Array of post types.\n\t * @return WP_Post_Type[]\n\t */\n\tpublic static function deregister_sitemap_post_types( $post_types ) {\n\n\t\tunset(\n\t\t\t$post_types['lesson'],\n\t\t\t$post_types['llms_quiz'],\n\t\t\t$post_types['llms_certificate'],\n\t\t\t$post_types['llms_my_certificate']\n\t\t);\n\n\t\treturn $post_types;\n\t}\n\n\t/**\n\t * Retrieve all registered order statuses\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_order_statuses() {\n\n\t\t$statuses = array(\n\n\t\t\t// Single payment only.\n\t\t\t'llms-completed'      => array(\n\t\t\t\t'label'       => _x( 'Completed', 'Order status', 'lifterlms' ),\n\t\t\t\t/* translators: %s: Completed count, %s: Completed count. */\n\t\t\t\t'label_count' => _n_noop( 'Completed <span class=\"count\">(%s)</span>', 'Completed <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t// Recurring only.\n\t\t\t'llms-active'         => array(\n\t\t\t\t'label'       => _x( 'Active', 'Order status', 'lifterlms' ),\n\t\t\t\t/* translators: %s: Active count, %s: Active count. */\n\t\t\t\t'label_count' => _n_noop( 'Active <span class=\"count\">(%s)</span>', 'Active <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\t\t\t'llms-expired'        => array(\n\t\t\t\t'label'       => _x( 'Expired', 'Order status', 'lifterlms' ),\n\t\t\t\t'label_count' => _n_noop( 'Expired <span class=\"count\">(%s)</span>', 'Expired <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\t\t\t'llms-on-hold'        => array(\n\t\t\t\t'label'       => _x( 'On Hold', 'Order status', 'lifterlms' ),\n\t\t\t\t/* translators: %s: On hold count, %s: On hold count. */\n\t\t\t\t'label_count' => _n_noop( 'On Hold <span class=\"count\">(%s)</span>', 'On Hold <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\t\t\t'llms-pending-cancel' => array(\n\t\t\t\t'label'       => _x( 'Pending Cancellation', 'Order status', 'lifterlms' ),\n\t\t\t\t'label_count' => _n_noop( 'Pending Cancellation <span class=\"count\">(%s)</span>', 'Pending Cancellation <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t// Shared.\n\t\t\t'llms-pending'        => array(\n\t\t\t\t'label'       => _x( 'Pending Payment', 'Order status', 'lifterlms' ),\n\t\t\t\t/* translators: %s: Pending Payment count, %s: Pending Payment count. */\n\t\t\t\t'label_count' => _n_noop( 'Pending Payment <span class=\"count\">(%s)</span>', 'Pending Payment <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\t\t\t'llms-cancelled'      => array(\n\t\t\t\t'label'       => _x( 'Cancelled', 'Order status', 'lifterlms' ),\n\t\t\t\t/* translators: %s: Cancelled count, %s: Cancelled count. */\n\t\t\t\t'label_count' => _n_noop( 'Cancelled <span class=\"count\">(%s)</span>', 'Cancelled <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\t\t\t'llms-refunded'       => array(\n\t\t\t\t'label'       => _x( 'Refunded', 'Order status', 'lifterlms' ),\n\t\t\t\t/* translators: %s: Refunded count, %s: Refunded count. */\n\t\t\t\t'label_count' => _n_noop( 'Refunded <span class=\"count\">(%s)</span>', 'Refunded <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\t\t\t'llms-failed'         => array(\n\t\t\t\t'label'       => _x( 'Failed', 'Order status', 'lifterlms' ),\n\t\t\t\t/* translators: %s: Failed count, %s: Failed count. */\n\t\t\t\t'label_count' => _n_noop( 'Failed <span class=\"count\">(%s)</span>', 'Failed <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t),\n\n\t\t);\n\n\t\t$defaults = array(\n\t\t\t'public'                    => true,\n\t\t\t'exclude_from_search'       => false,\n\t\t\t'show_in_admin_all_list'    => true,\n\t\t\t'show_in_admin_status_list' => true,\n\t\t);\n\n\t\tforeach ( $statuses as &$status ) {\n\t\t\t$status = array_merge( $status, $defaults );\n\t\t}\n\n\t\t/**\n\t\t * Filter the list of order statuses that will be registered with WordPress.\n\t\t *\n\t\t * @since 3.19.0\n\t\t *\n\t\t * @param array[] $statuses Array of post status arrays.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_register_order_post_statuses', $statuses );\n\t}\n\n\t/**\n\t * Get an array of capabilities for a custom post type\n\t *\n\t * Due to core bug does not allow us to use capability_type in post type registration.\n\t * See https://core.trac.wordpress.org/ticket/30991.\n\t *\n\t * @since 3.13.0\n\t * @since 6.0.0 Add specific case for `llms_my_achievement`, `llms_my_certificate` post types.\n\t *\n\t * @param string $post_type Post type name.\n\t * @return array\n\t */\n\tpublic static function get_post_type_caps( $post_type ) {\n\n\t\tif ( ! is_array( $post_type ) ) {\n\t\t\t$singular = $post_type;\n\t\t\t$plural   = $post_type . 's';\n\t\t} else {\n\t\t\t$singular = $post_type[0];\n\t\t\t$plural   = $post_type[1];\n\t\t}\n\n\t\tif ( in_array( $singular, array( 'my_achievement', 'my_certificate' ), true ) ) {\n\t\t\t$caps = self::get_earned_engagements_post_type_caps();\n\t\t} else {\n\t\t\t$caps = array(\n\n\t\t\t\t'read_post'              => sprintf( 'read_%s', $singular ),\n\t\t\t\t'read_private_posts'     => sprintf( 'read_private_%s', $plural ),\n\n\t\t\t\t'edit_post'              => sprintf( 'edit_%s', $singular ),\n\t\t\t\t'edit_posts'             => sprintf( 'edit_%s', $plural ),\n\t\t\t\t'edit_others_posts'      => sprintf( 'edit_others_%s', $plural ),\n\t\t\t\t'edit_private_posts'     => sprintf( 'edit_private_%s', $plural ),\n\t\t\t\t'edit_published_posts'   => sprintf( 'edit_published_%s', $plural ),\n\n\t\t\t\t'publish_posts'          => sprintf( 'publish_%s', $plural ),\n\n\t\t\t\t'delete_post'            => sprintf( 'delete_%s', $singular ),\n\t\t\t\t'delete_posts'           => sprintf( 'delete_%s', $plural ), // This is the core bug issue here.\n\t\t\t\t'delete_private_posts'   => sprintf( 'delete_private_%s', $plural ),\n\t\t\t\t'delete_published_posts' => sprintf( 'delete_published_%s', $plural ),\n\t\t\t\t'delete_others_posts'    => sprintf( 'delete_others_%s', $plural ),\n\n\t\t\t\t'create_posts'           => sprintf( 'create_%s', $plural ),\n\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * Filter the list of post type capabilities for the given post type.\n\t\t *\n\t\t * The dynamic portion of this hook, `$singular` refers to the post type's\n\t\t * name, for example \"course\" or \"llms_membership\".\n\t\t *\n\t\t * @since 3.13.0\n\t\t *\n\t\t * @param array $caps Array of capabilities.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t\"llms_get_{$singular}_post_type_caps\",\n\t\t\t$caps\n\t\t);\n\t}\n\n\t/**\n\t * Get an array of capabilities for earned engagements post types.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_earned_engagements_post_type_caps() {\n\n\t\treturn array(\n\n\t\t\t'read_post'              => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'read_private_posts'     => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\n\t\t\t'edit_post'              => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'edit_posts'             => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'edit_others_posts'      => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'edit_private_posts'     => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'edit_published_posts'   => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\n\t\t\t'publish_posts'          => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\n\t\t\t'delete_post'            => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'delete_posts'           => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'delete_private_posts'   => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'delete_published_posts' => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t'delete_others_posts'    => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\n\t\t\t'create_posts'           => LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP,\n\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve taxonomy capabilities for custom taxonomies\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param string|array $tax Taxonomy name/names (pass array of singular, plural to customize plural spelling).\n\t * @return array\n\t */\n\tpublic static function get_tax_caps( $tax ) {\n\n\t\tif ( ! is_array( $tax ) ) {\n\t\t\t$singular = $tax;\n\t\t\t$plural   = $tax . 's';\n\t\t} else {\n\t\t\t$singular = $tax[0];\n\t\t\t$plural   = $tax[1];\n\t\t}\n\n\t\t/**\n\t\t * Customize the taxonomy capabilities for the given taxonomy.\n\t\t *\n\t\t * The dynamic portion of this hook, `$singular` refers to the taxonomy's\n\t\t * registered name.\n\t\t *\n\t\t * @since 3.13.0\n\t\t *\n\t\t * @param array $caps Array of capabilities.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t\"llms_get_{$singular}_tax_caps\",\n\t\t\tarray(\n\t\t\t\t'manage_terms' => sprintf( 'manage_%s', $plural ),\n\t\t\t\t'edit_terms'   => sprintf( 'edit_%s', $plural ),\n\t\t\t\t'delete_terms' => sprintf( 'delete_%s', $plural ),\n\t\t\t\t'assign_terms' => sprintf( 'assign_%s', $plural ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieves the block template for use in post type registration.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $post_type The post type.\n\t * @return array|null Returns the block template array or null if no template is defined for the post type.\n\t */\n\tprivate static function get_template( $post_type ) {\n\n\t\tif ( empty( self::$templates ) ) {\n\t\t\tself::$templates = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-block-templates.php';\n\t\t}\n\n\t\treturn self::$templates[ $post_type ] ?? null;\n\t}\n\n\t/**\n\t * Register a custom post type.\n\t *\n\t * Automatically checks for duplicates and filters data.\n\t *\n\t * @since 3.13.0\n\t * @since 5.5.0 Added `lifterlms_register_post_type_{$name}` filters deprecation\n\t *              where `$name` is the the post type name, if the unprefixed name (removing 'llms_')\n\t *              is different from `$name`. E.g. it'll be triggered when registering when using\n\t *              `lifterlms_register_post_type_llms_engagement` but not when using `lifterlms_register_post_type_course`,\n\t *              for the latter, both the name and the unprefixed name are the same.\n\t * @since 6.0.0 Automatically load templates from the `llms-block-templates` schema.\n\t *              Added return value.\n\t *\n\t * @param string $name Post type name.\n\t * @param array  $data Post type data.\n\t * @return WP_Post_Type|WP_Error\n\t */\n\tpublic static function register_post_type( $name, $data ) {\n\n\t\tif ( ! post_type_exists( $name ) ) {\n\n\t\t\t$unprefixed_name = str_replace( 'llms_', '', $name );\n\n\t\t\tif ( $unprefixed_name !== $name ) {\n\t\t\t\t$data = apply_filters_deprecated(\n\t\t\t\t\t\"lifterlms_register_post_type_{$name}\",\n\t\t\t\t\tarray( $data ),\n\t\t\t\t\t'5.5.0',\n\t\t\t\t\t\"lifterlms_register_post_type_{$unprefixed_name}\"\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif ( empty( $data['template'] ) ) {\n\t\t\t\t$data['template'] = self::get_template( $name );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Modify post type registration arguments of a LifterLMS custom post type.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook refers to the post type's name with the `llms_` prefix\n\t\t\t * removed (if it exist). For example, to modify the arguments for the membership post type\n\t\t\t * (`llms_membership`) the full hook would be \"lifterlms_register_post_type_membership\".\n\t\t\t *\n\t\t\t * @since 3.13.0\n\t\t\t *\n\t\t\t * @param array $data Post type registration arguments passed to `register_post_type()`.\n\t\t\t */\n\t\t\t$data = apply_filters( \"lifterlms_register_post_type_{$unprefixed_name}\", $data );\n\t\t\treturn register_post_type( $name, $data );\n\n\t\t}\n\n\t\treturn get_post_type_object( $name );\n\t}\n\n\t/**\n\t * Register Post Types.\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.4 Made 'llms_access_plan' post type hierarchical to prevent a conflict with the Redirection plugin.\n\t * @since 3.33.0 `llms_question` post type is not publicly queryable anymore.\n\t * @since 3.37.12 Added 'revisions' support to course, lesson, and llms_mebership post types.\n\t * @since 4.5.1 Removed \"excerpt\" support for the course post type.\n\t * @since 4.17.0 Add \"llms-sales-page\" feature to course and membership post types.\n\t * @since 5.5.0 Register all the post types using `self::register_post_type()`.\n\t * @since 5.8.0 Remove all post type descriptions.\n\t * @since 6.0.0 Show `llms_my_certificate` ui (edit) only to who can `manage_lifterlms`.\n\t *             Register `llms_my_achievement` post type.\n\t *             Add thumbnail support for achievement and certificates (earned and template)\n\t *             Renames `llms_certificate` slug from `certificate` to `certificate-template`.\n\t *             Rename `llms_my_certificate` slug from `my_certificate` to `certificate`.\n\t *             Replaced the use of the deprecated `get_page() function with `get_post()`.\n\t *\n\t * @return void\n\t */\n\tpublic static function register_post_types() {\n\t\t$permalinks = llms_get_permalink_structure();\n\n\t\t// Course.\n\t\t$catalog_id = llms_get_page_id( 'shop' );\n\t\tself::register_post_type(\n\t\t\t'course',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Courses', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Course', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Courses', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Course', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Course', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Course', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Course', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Course', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Course', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Courses', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Courses found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Courses found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Course', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => true,\n\t\t\t\t'show_ui'             => true,\n\t\t\t\t'menu_icon'           => 'dashicons-welcome-learn-more',\n\t\t\t\t'capabilities'        => self::get_post_type_caps( 'course' ),\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => true,\n\t\t\t\t'exclude_from_search' => false,\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'rewrite'             => array(\n\t\t\t\t\t'slug'       => $permalinks['course_base'],\n\t\t\t\t\t'with_front' => false,\n\t\t\t\t\t'feeds'      => true,\n\t\t\t\t),\n\t\t\t\t'query_var'           => true,\n\t\t\t\t'supports'            => array( 'title', 'author', 'editor', 'thumbnail', 'comments', 'custom-fields', 'page-attributes', 'revisions', 'llms-clone-post', 'llms-export-post', 'llms-sales-page' ),\n\t\t\t\t'has_archive'         => ( $catalog_id && get_post( $catalog_id ) ) ? get_page_uri( $catalog_id ) : $permalinks['courses_base'],\n\t\t\t\t'show_in_nav_menus'   => true,\n\t\t\t\t'menu_position'       => 52,\n\t\t\t)\n\t\t);\n\n\t\t// Section.\n\t\tself::register_post_type(\n\t\t\t'section',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Sections', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Section', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Section', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Section', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Section', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Section', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Section', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Section', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Sections', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Sections found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Sections found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Sections', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Sections', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t)\n\t\t);\n\n\t\t// Lesson.\n\t\tself::register_post_type(\n\t\t\t'lesson',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Lessons', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Lesson', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Lesson', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Lesson', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Lesson', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Lesson', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Lesson', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Lesson', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Lessons', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Lessons found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Lessons found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Lessons', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Lessons', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => true,\n\t\t\t\t'show_ui'             => true,\n\t\t\t\t'capabilities'        => self::get_post_type_caps( 'lesson' ),\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => true,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=course',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'rewrite'             => array(\n\t\t\t\t\t'slug'       => $permalinks['lesson_base'],\n\t\t\t\t\t'with_front' => false,\n\t\t\t\t\t'feeds'      => true,\n\t\t\t\t),\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'query_var'           => true,\n\t\t\t\t'supports'            => array( 'title', 'editor', 'excerpt', 'thumbnail', 'comments', 'custom-fields', 'page-attributes', 'revisions', 'author', 'llms-clone-post', 'llms-detach-post' ),\n\t\t\t)\n\t\t);\n\n\t\t// Quiz.\n\t\tself::register_post_type(\n\t\t\t'llms_quiz',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Quizzes', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Quiz', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Quiz', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Quiz', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Quiz', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Quiz', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Quiz', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Quiz', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Quiz', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Quizzes found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Quizzes found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Quizzes', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Quizzes', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => true,\n\t\t\t\t'show_ui'             => false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'capabilities'        => self::get_post_type_caps( array( 'quiz', 'quizzes' ) ),\n\t\t\t\t'publicly_queryable'  => true,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=course',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'rewrite'             => array(\n\t\t\t\t\t'slug'       => $permalinks['quiz_base'],\n\t\t\t\t\t'with_front' => false,\n\t\t\t\t\t'feeds'      => true,\n\t\t\t\t),\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'query_var'           => true,\n\t\t\t\t'supports'            => array( 'title', 'editor', 'author', 'custom-fields' ),\n\t\t\t)\n\t\t);\n\n\t\t// Quiz Question.\n\t\tself::register_post_type(\n\t\t\t'llms_question',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Questions', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Question', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Question', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Question', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Question', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Question', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Question', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Question', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Questions', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Questions found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Questions found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Questions', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Quiz Questions', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'capabilities'        => self::get_post_type_caps( 'question' ),\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=course',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title', 'editor' ),\n\t\t\t)\n\t\t);\n\n\t\t// Membership.\n\t\t$membership_page_id = llms_get_page_id( 'memberships' );\n\t\tself::register_post_type(\n\t\t\t'llms_membership',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Memberships', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Membership', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Memberships', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Membership', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Membership', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Membership', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Membership', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Membership', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Membership', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Memberships', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Memberships found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Memberships found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Membership', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => true,\n\t\t\t\t'show_ui'             => true,\n\t\t\t\t'capabilities'        => self::get_post_type_caps( 'membership' ),\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'menu_icon'           => 'dashicons-groups',\n\t\t\t\t'publicly_queryable'  => true,\n\t\t\t\t'exclude_from_search' => false,\n\t\t\t\t'show_in_menu'        => true,\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'rewrite'             => array(\n\t\t\t\t\t'slug'       => _x( 'membership', 'membership url slug', 'lifterlms' ),\n\t\t\t\t\t'with_front' => false,\n\t\t\t\t\t'feeds'      => true,\n\t\t\t\t),\n\t\t\t\t'query_var'           => true,\n\t\t\t\t'supports'            => array( 'title', 'editor', 'thumbnail', 'comments', 'custom-fields', 'page-attributes', 'revisions', 'llms-sales-page' ),\n\t\t\t\t'has_archive'         => ( $membership_page_id && get_post( $membership_page_id ) ) ? get_page_uri( $membership_page_id ) : $permalinks['memberships_base'],\n\t\t\t\t'show_in_nav_menus'   => true,\n\t\t\t\t'menu_position'       => 52,\n\t\t\t)\n\t\t);\n\n\t\t// Engagement.\n\t\tself::register_post_type(\n\t\t\t'llms_engagement',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Engagements', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Engagement', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Engagement', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Engagement', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Engagement', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Engagement', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Engagement', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Engagement', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Engagement', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Engagement found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Engagement found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Engagement', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Engagements', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_engagements_access', 'manage_lifterlms' ) ) ) ? true : false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'menu_position'       => 52,\n\t\t\t\t'menu_icon'           => 'dashicons-awards',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t)\n\t\t);\n\n\t\t// Order.\n\t\tself::register_post_type(\n\t\t\t'llms_order',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Orders', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Order', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Order', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Order', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Order', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Order', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Order', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Order', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Orders', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Orders found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Orders found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Orders', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Orders', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_order_access', 'manage_lifterlms' ) ) ) ? true : false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'menu_icon'           => 'dashicons-cart',\n\t\t\t\t'menu_position'       => 52,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title', 'comments', 'custom-fields' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t\t'capabilities'        => array(\n\t\t\t\t\t'create_posts' => 'do_not_allow',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\t// Transaction.\n\t\tself::register_post_type(\n\t\t\t'llms_transaction',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Transactions', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Transaction', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Transaction', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Transaction', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Transaction', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Transaction', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Transaction', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Transaction', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Transactions', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Transactions found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Transactions found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Transactions', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Orders', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => false,\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( '' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t\t'capabilities'        => array(\n\t\t\t\t\t'create_posts' => 'do_not_allow',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\t// Achievement.\n\t\tself::register_post_type(\n\t\t\t'llms_achievement',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'                  => __( 'Achievement Templates', 'lifterlms' ),\n\t\t\t\t\t'singular_name'         => __( 'Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'add_new'               => __( 'Add Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'          => __( 'Add New Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'edit'                  => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'             => __( 'Edit Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'new_item'              => __( 'New Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'view'                  => __( 'View Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'view_item'             => __( 'View Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'search_items'          => __( 'Search Achievement Templates', 'lifterlms' ),\n\t\t\t\t\t'not_found'             => __( 'No Achievement Templates found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash'    => __( 'No Achievement Templates found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'                => __( 'Parent Achievement Template', 'lifterlms' ),\n\t\t\t\t\t'menu_name'             => _x( 'Achievements', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'featured_image'        => __( 'Achievement Image', 'lifterlms' ),\n\t\t\t\t\t'set_featured_image'    => __( 'Set achievement  image', 'lifterlms' ),\n\t\t\t\t\t'remove_featured_image' => __( 'Remove achievement image', 'lifterlms' ),\n\t\t\t\t\t'use_featured_image'    => __( 'Use achievement image', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_achievements_access', 'manage_lifterlms' ) ) ) ? true : false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=llms_engagement',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title', 'thumbnail' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t)\n\t\t);\n\n\t\t// Earned achievements.\n\t\tself::register_post_type(\n\t\t\t'llms_my_achievement',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'                  => __( 'Awarded Achievements', 'lifterlms' ),\n\t\t\t\t\t'singular_name'         => __( 'Awarded Achievement', 'lifterlms' ),\n\t\t\t\t\t'add_new'               => __( 'Award Achievement', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'          => __( 'Award New Achievement', 'lifterlms' ),\n\t\t\t\t\t'edit'                  => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'             => __( 'Edit Awarded Achievement', 'lifterlms' ),\n\t\t\t\t\t'new_item'              => __( 'New Awarded Achievement', 'lifterlms' ),\n\t\t\t\t\t'view'                  => __( 'View Awarded Achievement', 'lifterlms' ),\n\t\t\t\t\t'view_item'             => __( 'View Awarded Achievement', 'lifterlms' ),\n\t\t\t\t\t'search_items'          => __( 'Search Awarded Achievements', 'lifterlms' ),\n\t\t\t\t\t'not_found'             => __( 'No Awarded Achievements found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash'    => __( 'No Awarded Achievements found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'                => __( 'Parent Awarded Achievements', 'lifterlms' ),\n\t\t\t\t\t'menu_name'             => _x( 'Awarded Achievements', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'featured_image'        => __( 'Achievement Image', 'lifterlms' ),\n\t\t\t\t\t'set_featured_image'    => __( 'Set awarded achievement image', 'lifterlms' ),\n\t\t\t\t\t'remove_featured_image' => __( 'Remove awarded achievement image', 'lifterlms' ),\n\t\t\t\t\t'use_featured_image'    => __( 'Use awarded achievement image', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'description'         => __( 'This is where you can view all of the awarded achievements.', 'lifterlms' ),\n\t\t\t\t'public'              => false,\n\t\t\t\t/**\n\t\t\t\t * Filters the needed capability to generate and allow a UI for managing `llms_my_achievement` post type in the admin.\n\t\t\t\t *\n\t\t\t\t * @since 6.0.0\n\t\t\t\t *\n\t\t\t\t * @param bool $show_ui The needed capability to generate and allow a UI for managing `llms_my_achievement` post type in the admin.\n\t\t\t\t *                      Default is `manage_earned_engagements`.\n\t\t\t\t */\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_my_achievements_access', LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP ) ) ) ? true : false,\n\t\t\t\t'capabilities'        => self::get_post_type_caps( 'my_achievement' ),\n\t\t\t\t'map_meta_cap'        => false,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t/** This filter is documented above. */\n\t\t\t\t'show_in_menu'        => ( current_user_can( apply_filters( 'lifterlms_admin_my_achievements_access', LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP ) ) ) ? 'edit.php?post_type=llms_engagement' : false,\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'has_archive'         => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title', 'thumbnail' ),\n\t\t\t)\n\t\t);\n\n\t\t// Certificate.\n\t\tself::register_certificate_post_type(\n\t\t\t'llms_certificate',\n\t\t\tarray(\n\t\t\t\t'name'               => __( 'Certificate Templates', 'lifterlms' ),\n\t\t\t\t'singular_name'      => __( 'Certificate Template', 'lifterlms' ),\n\t\t\t\t'add_new'            => __( 'Add Certificate Template', 'lifterlms' ),\n\t\t\t\t'add_new_item'       => __( 'Add New Certificate Template', 'lifterlms' ),\n\t\t\t\t'edit_item'          => __( 'Edit Certificate Template', 'lifterlms' ),\n\t\t\t\t'new_item'           => __( 'New Certificate Template', 'lifterlms' ),\n\t\t\t\t'view'               => __( 'View Certificate Template', 'lifterlms' ),\n\t\t\t\t'view_item'          => __( 'View Certificate Template', 'lifterlms' ),\n\t\t\t\t'search_items'       => __( 'Search Certificate Templates', 'lifterlms' ),\n\t\t\t\t'not_found'          => __( 'No Certificate Templates found', 'lifterlms' ),\n\t\t\t\t'not_found_in_trash' => __( 'No Certificate Templates found in trash', 'lifterlms' ),\n\t\t\t\t'parent'             => __( 'Parent Certificate Templates', 'lifterlms' ),\n\t\t\t\t'menu_name'          => _x( 'Certificates', 'Admin menu name', 'lifterlms' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'map_meta_cap' => true,\n\t\t\t),\n\t\t\t$permalinks['certificate_template_base'],\n\t\t\t/**\n\t\t\t * Filters the WordPress user capability required for a user to manage certificate templates on the admin panel.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param string $capability User capability. Default: `manage_lifterlms`.\n\t\t\t */\n\t\t\tapply_filters( 'lifterlms_admin_certificates_access', 'manage_lifterlms' )\n\t\t);\n\n\t\t// Earned certificate.\n\t\tself::register_certificate_post_type(\n\t\t\t'llms_my_certificate',\n\t\t\tarray(\n\t\t\t\t'name'               => __( 'Awarded Certificates', 'lifterlms' ),\n\t\t\t\t'singular_name'      => __( 'Awarded Certificate', 'lifterlms' ),\n\t\t\t\t'add_new'            => __( 'Award Certificate', 'lifterlms' ),\n\t\t\t\t'add_new_item'       => __( 'Award New Certificate', 'lifterlms' ),\n\t\t\t\t'edit_item'          => __( 'Edit Awarded Certificate', 'lifterlms' ),\n\t\t\t\t'new_item'           => __( 'New Awarded Certificate', 'lifterlms' ),\n\t\t\t\t'view'               => __( 'View Awarded Certificate', 'lifterlms' ),\n\t\t\t\t'view_item'          => __( 'View Awarded Certificate', 'lifterlms' ),\n\t\t\t\t'search_items'       => __( 'Search Awarded Certificates', 'lifterlms' ),\n\t\t\t\t'not_found'          => __( 'No Awarded Certificates found', 'lifterlms' ),\n\t\t\t\t'not_found_in_trash' => __( 'No Awarded Certificates found in trash', 'lifterlms' ),\n\t\t\t\t'parent'             => __( 'Parent Awarded Certificates', 'lifterlms' ),\n\t\t\t\t'menu_name'          => _x( 'Awarded Certificates', 'Admin menu name', 'lifterlms' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'capabilities' => self::get_post_type_caps( 'my_certificate' ),\n\t\t\t\t'map_meta_cap' => false,\n\t\t\t),\n\t\t\t$permalinks['certificate_base'],\n\t\t\t/**\n\t\t\t * Filters the needed capability to generate and allow a UI for managing `llms_my_certificate` post type in the admin.\n\t\t\t *\n\t\t\t * @since 6.0.0\n\t\t\t *\n\t\t\t * @param bool $show_ui The needed capability to generate and allow a UI for managing `llms_my_certificate` post type in the admin.\n\t\t\t *                      Default is `manage_earned_engagements`.\n\t\t\t */\n\t\t\tapply_filters( 'lifterlms_admin_my_certificates_access', LLMS_Roles::MANAGE_EARNED_ENGAGEMENT_CAP )\n\t\t);\n\n\t\t// Email.\n\t\tself::register_post_type(\n\t\t\t'llms_email',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Email Templates', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Email Template', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Email Template', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Email Template', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Email Template', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Email Template', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Email Template', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Email Template', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Email Templates', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Emails found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Emails found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Email Templates', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Emails', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_emails_access', 'manage_lifterlms' ) ) ) ? true : false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=llms_engagement',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title', 'editor' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t)\n\t\t);\n\n\t\t// Coupon.\n\t\tself::register_post_type(\n\t\t\t'llms_coupon',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Coupons', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Coupon', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Coupon', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Coupon', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Coupon', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Coupon', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Coupon', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Coupon', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Coupon', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Coupon found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Coupon found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Coupon', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Coupons', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_coupons_access', 'manage_lifterlms' ) ) ) ? true : false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=llms_order',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t)\n\t\t);\n\n\t\t// Voucher.\n\t\tself::register_post_type(\n\t\t\t'llms_voucher',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Vouchers', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Voucher', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Voucher', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Voucher', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Voucher', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Voucher', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Voucher', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Voucher', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Voucher', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Voucher found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Voucher found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Voucher', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Vouchers', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_vouchers_access', 'manage_lifterlms' ) ) ) ? true : false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=llms_order',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t)\n\t\t);\n\n\t\t// Review.\n\t\tself::register_post_type(\n\t\t\t'llms_review',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Reviews', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Review', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Reviews', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Review', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Review', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Review', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Review', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Review', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Review', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Reviews', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Reviews found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Reviews found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Review', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => ( current_user_can( apply_filters( 'lifterlms_admin_reviews_access', 'manage_lifterlms' ) ) ) ? true : false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t'show_in_menu'        => 'edit.php?post_type=course',\n\t\t\t\t'hierarchical'        => false,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'has_archive'         => false,\n\t\t\t\t'supports'            => array( 'title', 'editor', 'excerpt', 'thumbnail', 'comments', 'custom-fields', 'page-attributes' ),\n\t\t\t)\n\t\t);\n\n\t\t// Access Plan.\n\t\tself::register_post_type(\n\t\t\t'llms_access_plan',\n\t\t\tarray(\n\t\t\t\t'labels'              => array(\n\t\t\t\t\t'name'               => __( 'Access Plans', 'lifterlms' ),\n\t\t\t\t\t'singular_name'      => __( 'Access Plan', 'lifterlms' ),\n\t\t\t\t\t'add_new'            => __( 'Add Access Plan', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'       => __( 'Add New Access Plan', 'lifterlms' ),\n\t\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t\t'edit_item'          => __( 'Edit Access Plan', 'lifterlms' ),\n\t\t\t\t\t'new_item'           => __( 'New Access Plan', 'lifterlms' ),\n\t\t\t\t\t'view'               => __( 'View Access Plan', 'lifterlms' ),\n\t\t\t\t\t'view_item'          => __( 'View Access Plan', 'lifterlms' ),\n\t\t\t\t\t'search_items'       => __( 'Search Access Plans', 'lifterlms' ),\n\t\t\t\t\t'not_found'          => __( 'No Access Plans found', 'lifterlms' ),\n\t\t\t\t\t'not_found_in_trash' => __( 'No Access Plans found in trash', 'lifterlms' ),\n\t\t\t\t\t'parent'             => __( 'Parent Access Plans', 'lifterlms' ),\n\t\t\t\t\t'menu_name'          => _x( 'Access Plans', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'public'              => false,\n\t\t\t\t'show_ui'             => false,\n\t\t\t\t'map_meta_cap'        => true,\n\t\t\t\t'publicly_queryable'  => false,\n\t\t\t\t'exclude_from_search' => true,\n\t\t\t\t/**\n\t\t\t\t * Making this post type hierarchical prevents a conflict\n\t\t\t\t * with the Redirection plugin (https://wordpress.org/plugins/redirection/)\n\t\t\t\t * When 301 monitoring is turned on, Redirection creates access plans\n\t\t\t\t * for each access plan that redirect the course or membership\n\t\t\t\t * to the site's home page.\n\t\t\t\t */\n\t\t\t\t'hierarchical'        => true,\n\t\t\t\t'show_in_nav_menus'   => false,\n\t\t\t\t'rewrite'             => false,\n\t\t\t\t'query_var'           => false,\n\t\t\t\t'supports'            => array( 'title' ),\n\t\t\t\t'has_archive'         => false,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Registers awarded and template certificate post types.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $post_type    Post type name.\n\t * @param array  $labels       Array of post type labels.\n\t * @param array  $args         Array of post type args.\n\t * @param string $rewrite_slug Post type rewrite slug.\n\t * @param string $admin_cap    User capability required to manage the post type on the admin panel.\n\t * @return void\n\t */\n\tprivate static function register_certificate_post_type( $post_type, $labels, $args, $rewrite_slug, $admin_cap ) {\n\n\t\t$user_can = current_user_can( $admin_cap );\n\t\t$supports = array( 'title', 'editor', 'thumbnail' );\n\n\t\tif ( 'llms_my_certificate' === $post_type ) {\n\t\t\t$supports[] = 'author';\n\t\t}\n\n\t\t$base_labels = array(\n\t\t\t'edit'                  => __( 'Edit', 'lifterlms' ),\n\t\t\t'featured_image'        => __( 'Background Image', 'lifterlms' ),\n\t\t\t'set_featured_image'    => __( 'Set background image', 'lifterlms' ),\n\t\t\t'remove_featured_image' => __( 'Remove background image', 'lifterlms' ),\n\t\t\t'use_featured_image'    => __( 'Use background image', 'lifterlms' ),\n\t\t);\n\n\t\t$base_args = array(\n\t\t\t'labels'              => wp_parse_args( $labels, $base_labels ),\n\t\t\t'show_ui'             => $user_can,\n\t\t\t'publicly_queryable'  => 'llms_certificate' === $post_type ? $user_can : true,\n\t\t\t'show_in_rest'        => llms_is_block_editor_supported_for_certificates() && $user_can,\n\t\t\t'public'              => true,\n\t\t\t'hierarchical'        => false,\n\t\t\t'exclude_from_search' => true,\n\t\t\t'show_in_menu'        => 'edit.php?post_type=llms_engagement',\n\t\t\t'show_in_nav_menus'   => false,\n\t\t\t'query_var'           => true,\n\t\t\t'supports'            => $supports,\n\t\t\t'rewrite'             => array(\n\t\t\t\t'slug'       => $rewrite_slug,\n\t\t\t\t'with_front' => false,\n\t\t\t\t'feeds'      => true,\n\t\t\t),\n\t\t);\n\n\t\tself::register_post_type( $post_type, wp_parse_args( $args, $base_args ) );\n\t}\n\n\t/**\n\t * Register post statuses\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknwn.\n\t *\n\t * @return void\n\t */\n\tpublic static function register_post_statuses() {\n\n\t\t$order_statuses = self::get_order_statuses();\n\n\t\t$txn_statuses = apply_filters(\n\t\t\t'lifterlms_register_transaction_post_statuses',\n\t\t\tarray(\n\t\t\t\t'llms-txn-failed'    => array(\n\t\t\t\t\t'label'                     => _x( 'Failed', 'Transaction status', 'lifterlms' ),\n\t\t\t\t\t'public'                    => true,\n\t\t\t\t\t'exclude_from_search'       => false,\n\t\t\t\t\t'show_in_admin_all_list'    => true,\n\t\t\t\t\t'show_in_admin_status_list' => true,\n\t\t\t\t\t/* translators: %s: Failed count, %s: Failed count. */\n\t\t\t\t\t'label_count'               => _n_noop( 'Failed <span class=\"count\">(%s)</span>', 'Failed <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'llms-txn-pending'   => array(\n\t\t\t\t\t'label'                     => _x( 'Pending', 'Transaction status', 'lifterlms' ),\n\t\t\t\t\t'public'                    => true,\n\t\t\t\t\t'exclude_from_search'       => false,\n\t\t\t\t\t'show_in_admin_all_list'    => true,\n\t\t\t\t\t'show_in_admin_status_list' => true,\n\t\t\t\t\t/* translators: %s: Pending count, %s: Pending count. */\n\t\t\t\t\t'label_count'               => _n_noop( 'Pending <span class=\"count\">(%s)</span>', 'Pending <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'llms-txn-refunded'  => array(\n\t\t\t\t\t'label'                     => _x( 'Refunded', 'Transaction status', 'lifterlms' ),\n\t\t\t\t\t'public'                    => true,\n\t\t\t\t\t'exclude_from_search'       => false,\n\t\t\t\t\t'show_in_admin_all_list'    => true,\n\t\t\t\t\t'show_in_admin_status_list' => true,\n\t\t\t\t\t/* translators: %s: Refuned count, %s: Refunded count. */\n\t\t\t\t\t'label_count'               => _n_noop( 'Refunded <span class=\"count\">(%s)</span>', 'Refunded <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'llms-txn-succeeded' => array(\n\t\t\t\t\t'label'                     => _x( 'Succeeded', 'Transaction status', 'lifterlms' ),\n\t\t\t\t\t'public'                    => true,\n\t\t\t\t\t'exclude_from_search'       => false,\n\t\t\t\t\t'show_in_admin_all_list'    => true,\n\t\t\t\t\t'show_in_admin_status_list' => true,\n\t\t\t\t\t'label_count'               => _n_noop( 'Succeeded <span class=\"count\">(%s)</span>', 'Succeeded <span class=\"count\">(%s)</span>', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tforeach ( array_merge( $order_statuses, $txn_statuses ) as $status => $values ) {\n\n\t\t\tregister_post_status( $status, $values );\n\n\t\t}\n\t}\n\n\t/**\n\t * Register a custom post type taxonomy\n\t * Automatically checks for duplicates and filters data\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param string       $name   Taxonomy name.\n\t * @param string|array $object Post type object(s) to associate the taxonomy with.\n\t * @param array        $data   Taxonomy data.\n\t * @return void\n\t */\n\tpublic static function register_taxonomy( $name, $object, $data ) {\n\n\t\tif ( ! taxonomy_exists( $name ) ) {\n\t\t\t$filter_name = str_replace( 'llms_', '', $name );\n\t\t\tregister_taxonomy(\n\t\t\t\t$name,\n\t\t\t\tapply_filters( 'lifterlms_register_taxonomy_objects_' . $filter_name, $object ),\n\t\t\t\tapply_filters( 'lifterlms_register_taxonomy_args_' . $filter_name, $data )\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Register Taxonomies\n\t *\n\t * @since 1.0.0\n\t * @since 3.30.3 Removed duplicate array keys when registering course_tag taxonomy.\n\t * @since 3.34.1 Add custom property `show_in_llms_rest` set to true by default to those taxonomies we want to show in LLMS REST api.\n\t *\n\t * @return void\n\t */\n\tpublic static function register_taxonomies() {\n\n\t\t$permalinks = llms_get_permalink_structure();\n\n\t\t// Course cat.\n\t\tself::register_taxonomy(\n\t\t\t'course_cat',\n\t\t\tarray( 'course' ),\n\t\t\tarray(\n\t\t\t\t'label'             => __( 'Course Categories', 'lifterlms' ),\n\t\t\t\t'labels'            => array(\n\t\t\t\t\t'name'              => __( 'Course Categories', 'lifterlms' ),\n\t\t\t\t\t'singular_name'     => __( 'Course Category', 'lifterlms' ),\n\t\t\t\t\t'menu_name'         => _x( 'Categories', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'search_items'      => __( 'Search Course Categories', 'lifterlms' ),\n\t\t\t\t\t'all_items'         => __( 'All Course Categories', 'lifterlms' ),\n\t\t\t\t\t'parent_item'       => __( 'Parent Course Category', 'lifterlms' ),\n\t\t\t\t\t'parent_item_colon' => __( 'Parent Course Category:', 'lifterlms' ),\n\t\t\t\t\t'edit_item'         => __( 'Edit Course Category', 'lifterlms' ),\n\t\t\t\t\t'update_item'       => __( 'Update Course Category', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'      => __( 'Add New Course Category', 'lifterlms' ),\n\t\t\t\t\t'new_item_name'     => __( 'New Course Category Name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'capabilities'      => self::get_tax_caps( 'course_cat' ),\n\t\t\t\t'hierarchical'      => true,\n\t\t\t\t'query_var'         => true,\n\t\t\t\t'show_admin_column' => true,\n\t\t\t\t'show_ui'           => true,\n\t\t\t\t'rewrite'           => array(\n\t\t\t\t\t'slug'         => $permalinks['course_category_base'],\n\t\t\t\t\t'with_front'   => false,\n\t\t\t\t\t'hierarchical' => true,\n\t\t\t\t),\n\t\t\t\t'show_in_llms_rest' => true,\n\t\t\t)\n\t\t);\n\n\t\t// Course difficulty.\n\t\tself::register_taxonomy(\n\t\t\t'course_difficulty',\n\t\t\tarray( 'course' ),\n\t\t\tarray(\n\t\t\t\t'label'             => __( 'Course Difficulties', 'lifterlms' ),\n\t\t\t\t'labels'            => array(\n\t\t\t\t\t'name'              => __( 'Course Difficulties', 'lifterlms' ),\n\t\t\t\t\t'singular_name'     => __( 'Course Difficulty', 'lifterlms' ),\n\t\t\t\t\t'menu_name'         => _x( 'Difficulties', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'search_items'      => __( 'Search Course Difficulties', 'lifterlms' ),\n\t\t\t\t\t'all_items'         => __( 'All Course Difficulties', 'lifterlms' ),\n\t\t\t\t\t'parent_item'       => __( 'Parent Course Difficulty', 'lifterlms' ),\n\t\t\t\t\t'parent_item_colon' => __( 'Parent Course Difficulty:', 'lifterlms' ),\n\t\t\t\t\t'edit_item'         => __( 'Edit Course Difficulty', 'lifterlms' ),\n\t\t\t\t\t'update_item'       => __( 'Update Course Difficulty', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'      => __( 'Add New Course Difficulty', 'lifterlms' ),\n\t\t\t\t\t'new_item_name'     => __( 'New Course Difficulty Name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'capabilities'      => self::get_tax_caps( array( 'course_difficulty', 'course_difficulties' ) ),\n\t\t\t\t'hierarchical'      => false,\n\t\t\t\t'query_var'         => true,\n\t\t\t\t'show_admin_column' => true,\n\t\t\t\t'show_ui'           => true,\n\t\t\t\t'rewrite'           => array(\n\t\t\t\t\t'slug'       => $permalinks['course_difficulty_base'],\n\t\t\t\t\t'with_front' => false,\n\t\t\t\t),\n\t\t\t\t'show_in_llms_rest' => true,\n\t\t\t)\n\t\t);\n\n\t\t// Course tag.\n\t\tself::register_taxonomy(\n\t\t\t'course_tag',\n\t\t\tarray( 'course' ),\n\t\t\tarray(\n\t\t\t\t'label'             => __( 'Course Tags', 'lifterlms' ),\n\t\t\t\t'labels'            => array(\n\t\t\t\t\t'name'              => __( 'Course Tags', 'lifterlms' ),\n\t\t\t\t\t'singular_name'     => __( 'Course Tag', 'lifterlms' ),\n\t\t\t\t\t'menu_name'         => _x( 'Tags', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'search_items'      => __( 'Search Course Tags', 'lifterlms' ),\n\t\t\t\t\t'all_items'         => __( 'All Course Tags', 'lifterlms' ),\n\t\t\t\t\t'parent_item'       => __( 'Parent Course Tag', 'lifterlms' ),\n\t\t\t\t\t'parent_item_colon' => __( 'Parent Course Tag:', 'lifterlms' ),\n\t\t\t\t\t'edit_item'         => __( 'Edit Course Tag', 'lifterlms' ),\n\t\t\t\t\t'update_item'       => __( 'Update Course Tag', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'      => __( 'Add New Course Tag', 'lifterlms' ),\n\t\t\t\t\t'new_item_name'     => __( 'New Course Tag Name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'capabilities'      => self::get_tax_caps( 'course_tag' ),\n\t\t\t\t'hierarchical'      => false,\n\t\t\t\t'query_var'         => true,\n\t\t\t\t'show_admin_column' => true,\n\t\t\t\t'show_ui'           => true,\n\t\t\t\t'rewrite'           => array(\n\t\t\t\t\t'slug'       => $permalinks['course_tag_base'],\n\t\t\t\t\t'with_front' => false,\n\t\t\t\t),\n\t\t\t\t'show_in_llms_rest' => true,\n\t\t\t)\n\t\t);\n\n\t\t// Course track.\n\t\tself::register_taxonomy(\n\t\t\t'course_track',\n\t\t\tarray( 'course' ),\n\t\t\tarray(\n\t\t\t\t'label'             => __( 'Course Track', 'lifterlms' ),\n\t\t\t\t'labels'            => array(\n\t\t\t\t\t'name'              => __( 'Course Tracks', 'lifterlms' ),\n\t\t\t\t\t'singular_name'     => __( 'Course Track', 'lifterlms' ),\n\t\t\t\t\t'menu_name'         => _x( 'Tracks', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'search_items'      => __( 'Search Course Tracks', 'lifterlms' ),\n\t\t\t\t\t'all_items'         => __( 'All Course Tracks', 'lifterlms' ),\n\t\t\t\t\t'parent_item'       => __( 'Parent Course Track', 'lifterlms' ),\n\t\t\t\t\t'parent_item_colon' => __( 'Parent Course Track:', 'lifterlms' ),\n\t\t\t\t\t'edit_item'         => __( 'Edit Course Track', 'lifterlms' ),\n\t\t\t\t\t'update_item'       => __( 'Update Course Track', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'      => __( 'Add New Course Track', 'lifterlms' ),\n\t\t\t\t\t'new_item_name'     => __( 'New Course Track Name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'capabilities'      => self::get_tax_caps( 'course_track' ),\n\t\t\t\t'hierarchical'      => true,\n\t\t\t\t'query_var'         => true,\n\t\t\t\t'show_admin_column' => true,\n\t\t\t\t'show_ui'           => true,\n\t\t\t\t'rewrite'           => array(\n\t\t\t\t\t'slug'         => $permalinks['course_track_base'],\n\t\t\t\t\t'with_front'   => false,\n\t\t\t\t\t'hierarchical' => true,\n\t\t\t\t),\n\t\t\t\t'show_in_llms_rest' => true,\n\t\t\t)\n\t\t);\n\n\t\t// Membership cat.\n\t\tself::register_taxonomy(\n\t\t\t'membership_cat',\n\t\t\tarray( 'llms_membership' ),\n\t\t\tarray(\n\t\t\t\t'hierarchical'      => true,\n\t\t\t\t'label'             => __( 'Membership Categories', 'lifterlms' ),\n\t\t\t\t'labels'            => array(\n\t\t\t\t\t'name'              => __( 'Membership Categories', 'lifterlms' ),\n\t\t\t\t\t'singular_name'     => __( 'Membership Category', 'lifterlms' ),\n\t\t\t\t\t'menu_name'         => _x( 'Categories', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'search_items'      => __( 'Search Membership Categories', 'lifterlms' ),\n\t\t\t\t\t'all_items'         => __( 'All Membership Categories', 'lifterlms' ),\n\t\t\t\t\t'parent_item'       => __( 'Parent Membership Category', 'lifterlms' ),\n\t\t\t\t\t'parent_item_colon' => __( 'Parent Membership Category:', 'lifterlms' ),\n\t\t\t\t\t'edit_item'         => __( 'Edit Membership Category', 'lifterlms' ),\n\t\t\t\t\t'update_item'       => __( 'Update Membership Category', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'      => __( 'Add New Membership Category', 'lifterlms' ),\n\t\t\t\t\t'new_item_name'     => __( 'New Membership Category Name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'capabilities'      => self::get_tax_caps( 'membership_cat' ),\n\t\t\t\t'show_ui'           => true,\n\t\t\t\t'show_in_menu'      => true,\n\t\t\t\t'query_var'         => true,\n\t\t\t\t'show_admin_column' => true,\n\t\t\t\t'rewrite'           => array(\n\t\t\t\t\t'slug'         => $permalinks['membership_category_base'],\n\t\t\t\t\t'with_front'   => false,\n\t\t\t\t\t'hierarchical' => true,\n\t\t\t\t),\n\t\t\t\t'show_in_llms_rest' => true,\n\t\t\t)\n\t\t);\n\n\t\t// Membership tag.\n\t\tself::register_taxonomy(\n\t\t\t'membership_tag',\n\t\t\tarray( 'llms_membership' ),\n\t\t\tarray(\n\t\t\t\t'hierarchical'      => false,\n\t\t\t\t'label'             => __( 'Membership Tags', 'lifterlms' ),\n\t\t\t\t'labels'            => array(\n\t\t\t\t\t'name'              => __( 'Membership Tags', 'lifterlms' ),\n\t\t\t\t\t'singular_name'     => __( 'Membership Tag', 'lifterlms' ),\n\t\t\t\t\t'menu_name'         => _x( 'Tags', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t\t'search_items'      => __( 'Search Membership Tags', 'lifterlms' ),\n\t\t\t\t\t'all_items'         => __( 'All Membership Tags', 'lifterlms' ),\n\t\t\t\t\t'parent_item'       => __( 'Parent Membership Tag', 'lifterlms' ),\n\t\t\t\t\t'parent_item_colon' => __( 'Parent Membership Tag:', 'lifterlms' ),\n\t\t\t\t\t'edit_item'         => __( 'Edit Membership Tag', 'lifterlms' ),\n\t\t\t\t\t'update_item'       => __( 'Update Membership Tag', 'lifterlms' ),\n\t\t\t\t\t'add_new_item'      => __( 'Add New Membership Tag', 'lifterlms' ),\n\t\t\t\t\t'new_item_name'     => __( 'New Membership Tag Name', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'capabilities'      => self::get_tax_caps( 'membership_tag' ),\n\t\t\t\t'show_ui'           => true,\n\t\t\t\t'show_in_menu'      => 'lifterlms',\n\t\t\t\t'query_var'         => true,\n\t\t\t\t'show_admin_column' => true,\n\t\t\t\t'rewrite'           => array(\n\t\t\t\t\t'slug'       => $permalinks['membership_tag_base'],\n\t\t\t\t\t'with_front' => false,\n\t\t\t\t),\n\t\t\t\t'show_in_llms_rest' => true,\n\t\t\t)\n\t\t);\n\n\t\t// Course/membership visibility.\n\t\tself::register_taxonomy(\n\t\t\t'llms_product_visibility',\n\t\t\tarray( 'course', 'llms_membership' ),\n\t\t\tarray(\n\t\t\t\t'hierarchical'      => false,\n\t\t\t\t'show_ui'           => false,\n\t\t\t\t'show_in_nav_menus' => false,\n\t\t\t\t'query_var'         => is_admin(),\n\t\t\t\t'rewrite'           => false,\n\t\t\t\t'public'            => false,\n\t\t\t)\n\t\t);\n\n\t\t// Access plan visibility.\n\t\tself::register_taxonomy(\n\t\t\t'llms_access_plan_visibility',\n\t\t\tarray( 'llms_access_plan' ),\n\t\t\tarray(\n\t\t\t\t'hierarchical'      => false,\n\t\t\t\t'show_ui'           => false,\n\t\t\t\t'show_in_nav_menus' => false,\n\t\t\t\t'query_var'         => is_admin(),\n\t\t\t\t'rewrite'           => false,\n\t\t\t\t'public'            => false,\n\t\t\t)\n\t\t);\n\t}\n}\n\nLLMS_Post_Types::init();\n"
  },
  {
    "path": "includes/class.llms.post.handler.php",
    "content": "<?php\n/**\n * Post Handler Class\n *\n * Main Handler for post management in LifterLMS\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 5.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Post_Handler\n *\n * @since 1.0.0\n * @since 4.4.3 Deprecated the `LLMS_Post_Handler::create_lesson()` method with no replacement.\n * @since 5.7.0 Deprecated the `LLMS_Post_Handler::create_section()` method with no replacement.\n */\nclass LLMS_Post_Handler {\n\n\t/**\n\t * Create Post\n\t *\n\t * @param  string $type [optional: a post type]\n\t * @param  string $title [optional: a title for the post]\n\t * @return int [id of section]\n\t */\n\tpublic static function create( $type = 'post', $title = '', $excerpt = '' ) {\n\n\t\tif ( empty( $title ) ) {\n\t\t\t$title = 'Section 1';\n\t\t}\n\n\t\t// Create section post.\n\t\t$post_data = apply_filters(\n\t\t\t'lifterlms_new_post',\n\t\t\tarray(\n\t\t\t\t'post_type'    => $type,\n\t\t\t\t'post_title'   => $title,\n\t\t\t\t'post_status'  => 'publish',\n\t\t\t\t'post_author'  => get_current_user_id(),\n\t\t\t\t'post_excerpt' => $excerpt,\n\t\t\t)\n\t\t);\n\n\t\t$post_id = wp_insert_post( $post_data, true );\n\n\t\t// Check for error in update.\n\t\tif ( is_wp_error( $post_id ) ) {\n\t\t\t// For now just log the error and set $post_id to 0 (false).\n\t\t\tllms_log( $post_id->get_error_message() );\n\t\t\t$post_id = 0;\n\t\t}\n\n\t\treturn $post_id;\n\n\t}\n\n\tpublic static function update_title( $post_id, $title ) {\n\n\t\t$post_data = array(\n\t\t\t'ID'         => $post_id,\n\t\t\t'post_title' => $title,\n\t\t);\n\n\t\t// Update the post into the database.\n\t\t$updated_post_id = wp_update_post( $post_data );\n\n\t\tif ( $updated_post_id ) {\n\t\t\treturn array(\n\t\t\t\t'id'    => $updated_post_id,\n\t\t\t\t'title' => $title,\n\t\t\t);\n\t\t}\n\n\t}\n\n\tpublic static function update_excerpt( $post_id, $excerpt ) {\n\n\t\t$post_data = array(\n\t\t\t'ID'           => $post_id,\n\t\t\t'post_excerpt' => $excerpt,\n\t\t);\n\n\t\t// Update the post into the database.\n\t\t$updated_post_id = wp_update_post( $post_data );\n\n\t\tif ( $updated_post_id ) {\n\t\t\treturn array(\n\t\t\t\t'id'           => $updated_post_id,\n\t\t\t\t'post_excerpt' => $excerpt,\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Creates a new Section\n\t *\n\t * @since Unknown Introduced.\n\t * @deprecated 5.7.0 There is not a replacement.\n\t *\n\t * @param  int    $course_id The parent course id.\n\t * @param  string $title     An optional title for the section.\n\t * @return int Post id of the section.\n\t */\n\tpublic static function create_section( $course_id, $title = '' ) {\n\n\t\tllms_deprecated_function( __METHOD__, '5.7.0' );\n\n\t\t// No course id? no new section!.\n\t\tif ( ! isset( $course_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Set the section_order variable.\n\t\t// Get the count of sections in the course and add 1.\n\t\t$course        = new LLMS_Course( $course_id );\n\t\t$sections      = $course->get_sections( 'posts' );\n\t\t$section_order = count( $sections ) + 1;\n\n\t\t$title = isset( $title ) ? $title : 'New Section';\n\n\t\t$post_id = self::create( 'section', $title );\n\n\t\t// If post created set parent course and order to order determined above.\n\t\tif ( $post_id ) {\n\t\t\tupdate_post_meta( $post_id, '_llms_order', $section_order );\n\n\t\t\t$section               = new LLMS_Section( $post_id );\n\t\t\t$updated_parent_course = $section->set_parent_course( $course_id );\n\t\t}\n\n\t\treturn $post_id;\n\t}\n\n\t/**\n\t * Create lesson\n\t *\n\t * @since Unknown\n\t * @deprecated 4.4.3\n\t *\n\t * @param int    $course_id  WP_Post ID of the course.\n\t * @param int    $section_id WP_Post ID of the lesson's parent section.\n\t * @param string $title   Optional lesson title.\n\t * @param string $excerpt Option excerpt.\n\t * @return int WP_Post ID of the created lesson.\n\t */\n\tpublic static function create_lesson( $course_id, $section_id, $title = '', $excerpt = '' ) {\n\n\t\t// No course id or section id? no new lesson!.\n\t\tif ( ! isset( $course_id ) || ! isset( $course_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Set the lesson_order variable.\n\t\t// Get the count of lessons in the section.\n\t\t$section      = new LLMS_Section( $section_id );\n\t\t$lesson_order = $section->get_next_available_lesson_order();\n\n\t\t$title = isset( $title ) ? $title : 'New Lesson';\n\n\t\t$post_id = self::create( 'lesson', $title, $excerpt );\n\n\t\t// If post created set parent section, parent course and order determined above.\n\t\tif ( $post_id ) {\n\t\t\tupdate_post_meta( $post_id, '_llms_order', $lesson_order );\n\n\t\t\t$lesson                 = new LLMS_Lesson( $post_id );\n\t\t\t$updated_parent_section = $lesson->set_parent_section( $section_id );\n\t\t\t$updated_parent_course  = $lesson->set_parent_course( $course_id );\n\n\t\t}\n\n\t\treturn $post_id;\n\t}\n\n\tpublic static function get_posts( $type = 'post' ) {\n\n\t\t$args      = array(\n\t\t\t'posts_per_page'   => 1000,\n\t\t\t'post_type'        => $type,\n\t\t\t'nopaging'         => true,\n\t\t\t'post_status'      => 'publish',\n\t\t\t'orderby'          => 'post_title',\n\t\t\t'order'            => 'ASC',\n\t\t\t'suppress_filters' => true,\n\t\t);\n\t\t$postslist = get_posts( $args );\n\n\t\tif ( ! empty( $postslist ) ) {\n\n\t\t\tforeach ( $postslist as $key => $value ) {\n\t\t\t\t$value->edit_url = get_edit_post_link( $value->ID, false );\n\t\t\t}\n\t\t}\n\n\t\treturn $postslist;\n\n\t}\n\n\tpublic static function get_lesson_options_for_select_list() {\n\n\t\t$lessons = self::get_posts( 'lesson' );\n\n\t\t$options = array();\n\n\t\tif ( ! empty( $lessons ) ) {\n\n\t\t\tforeach ( $lessons as $key => $value ) {\n\n\t\t\t\t// Get parent course if assigned.\n\t\t\t\t$parent_course = get_post_meta( $value->ID, '_llms_parent_course', true );\n\n\t\t\t\tif ( $parent_course ) {\n\t\t\t\t\t$title = $value->post_title . ' ( ' . get_the_title( $parent_course ) . ' )';\n\t\t\t\t} else {\n\t\t\t\t\t$title = $value->post_title . ' ( ' . __( 'unassigned', 'lifterlms' ) . ' )';\n\t\t\t\t}\n\n\t\t\t\t$options[ $value->ID ] = $title;\n\n\t\t\t}\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n\tpublic static function get_prerequisite( $post_id ) {\n\t\t$post_type = get_post_type( $post_id );\n\n\t\tif ( 'course' === $post_type ) {\n\t\t\t$course = new LLMS_Course( $post_id );\n\t\t\treturn $course->get_prerequisite();\n\n\t\t} elseif ( 'lesson' === $post_type ) {\n\t\t\t$lesson = new LLMS_Lesson( $post_id );\n\t\t\treturn $lesson->get_prerequisite();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.post.relationships.php",
    "content": "<?php\n/**\n * Define post and record relationships to automate cleanup of information when posts are deleted from the DB.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.16.12\n * @version 7.6.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Hooks and actions related to post relationships.\n *\n * @since 3.16.12\n * @since 3.24.0 Unknown.\n * @since 3.37.8 Delete student quiz attempts when a quiz is deleted.\n * @since 4.15.0 Delete access plans related to courses/memberships on their deletion.\n */\nclass LLMS_Post_Relationships {\n\n\t/**\n\t * Configure relationships.\n\t *\n\t * @since Unknown.\n\t * @since 7.6.2 Added `llms_voucher` relationship.\n\t * @var array\n\t */\n\tprivate $relationships = array(\n\t\t'course'          => array(\n\t\t\tarray(\n\t\t\t\t'action'    => 'delete',\n\t\t\t\t'meta_key'  => '_llms_product_id',\n\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t),\n\t\t),\n\n\t\t'llms_membership' => array(\n\t\t\tarray(\n\t\t\t\t'action'    => 'delete',\n\t\t\t\t'meta_key'  => '_llms_product_id',\n\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t),\n\t\t),\n\n\t\t'lesson'          => array(\n\t\t\tarray(\n\t\t\t\t'action'               => 'unset',\n\t\t\t\t'meta_key'             => '_llms_prerequisite', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'meta_keys_additional' => array( '_llms_has_prerequisite' ),\n\t\t\t\t'post_type'            => 'lesson',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'action'    => 'unset',\n\t\t\t\t'meta_key'  => '_llms_lesson_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'post_type' => 'llms_quiz',\n\t\t\t),\n\t\t),\n\n\t\t'llms_order'      => array(\n\t\t\tarray(\n\t\t\t\t'action'    => 'delete',\n\t\t\t\t'meta_key'  => '_llms_order_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'post_type' => 'llms_transaction',\n\t\t\t),\n\t\t),\n\n\t\t'llms_quiz'       => array(\n\t\t\tarray(\n\t\t\t\t'action'    => 'delete', // Delete = force delete; trash = move to trash.\n\t\t\t\t'meta_key'  => '_llms_parent_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'post_type' => 'llms_question',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'action'               => 'unset',\n\t\t\t\t'meta_key'             => '_llms_quiz', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'meta_keys_additional' => array( '_llms_quiz_enabled' ),\n\t\t\t\t'post_type'            => 'lesson',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'action'     => 'delete',\n\t\t\t\t'table_name' => 'lifterlms_quiz_attempts',\n\t\t\t\t'table_key'  => 'quiz_id',\n\t\t\t),\n\t\t),\n\n\t\t'llms_voucher'    => array(\n\t\t\tarray(\n\t\t\t\t'action'     => 'delete',\n\t\t\t\t'table_name' => 'lifterlms_vouchers_codes',\n\t\t\t\t'table_key'  => 'voucher_id',\n\t\t\t),\n\t\t),\n\n\t);\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.16.12\n\t * @since 5.4.0 Prevent course/membership with active subscriptions deletion.\n\t * @since 6.0.0 Added hook to cleanup user post meta data when awarded certs and achievements are deleted.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'delete_post', array( $this, 'maybe_update_relationships' ) );\n\t\tadd_action( 'pre_delete_post', array( __CLASS__, 'maybe_prevent_product_deletion' ), 10, 2 );\n\n\t\tadd_action( 'before_delete_post', array( __CLASS__, 'maybe_clean_earned_engagments_related_user_post_meta' ) );\n\t}\n\n\t/**\n\t * Maybe delete LifterLMS user post meta related to earned engagements.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $post_id Post ID.\n\t * @return void\n\t */\n\tpublic static function maybe_clean_earned_engagments_related_user_post_meta( $post_id ) {\n\n\t\t$post_types = array(\n\t\t\t'llms_my_certificate',\n\t\t\t'llms_my_achievement',\n\t\t);\n\t\t$post_type  = get_post_type( $post_id );\n\n\t\tif ( ! in_array( $post_type, $post_types, true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$earned_engagement = 'llms_my_certificate' === $post_type ? new LLMS_User_Certificate( $post_id ) : new LLMS_User_Achievement( $post_id );\n\n\t\tdo_action_deprecated(\n\t\t\t'llms_before_delete_' . str_replace( 'llms_my_', '', $post_type ),\n\t\t\tarray(\n\t\t\t\t$earned_engagement,\n\t\t\t),\n\t\t\t'6.0.0',\n\t\t\t'',\n\t\t\t__( 'Use WordPress core  `before_delete_post` action hook', 'lifterlms' )\n\t\t);\n\n\t\tglobal $wpdb;\n\t\t$wpdb->delete(\n\t\t\t\"{$wpdb->prefix}lifterlms_user_postmeta\",\n\t\t\tarray(\n\t\t\t\t'user_id'    => $earned_engagement->get_user_id(),\n\t\t\t\t'meta_key'   => '_' . str_replace( 'llms_my_', '', $post_type ) . '_earned',\n\t\t\t\t'meta_value' => $post_id,\n\t\t\t),\n\t\t\tarray( '%d', '%s', '%d' )\n\t\t); // no-cache ok.\n\n\t\tadd_action(\n\t\t\t'after_delete_post',\n\t\t\tfunction ( $post_id ) use ( $earned_engagement, $post_type ) {\n\n\t\t\t\tif ( $earned_engagement->get( 'id' ) === $post_id ) {\n\t\t\t\t\tdo_action_deprecated(\n\t\t\t\t\t\t'llms_delete_' . str_replace( 'llms_my_', '', $post_type ),\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t$earned_engagement,\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'6.0.0',\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t__( 'Use WordPress core `deleted_post` action hook.', 'lifterlms' )\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * Determine whether a product deletion should take place.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @param bool|null $delete Whether to go forward with deletion.\n\t * @param WP_Post   $post   Post object.\n\t * @return bool|null\n\t */\n\tpublic static function maybe_prevent_product_deletion( $delete, $post ) {\n\n\t\tif ( ! in_array( get_post_type( $post ), array( 'course', 'llms_membership' ), true ) ) {\n\t\t\treturn $delete;\n\t\t}\n\n\t\t$product = llms_get_product( $post );\n\n\t\tif ( empty( $product ) || ! $product->has_active_subscriptions() ) {\n\t\t\treturn $delete;\n\t\t}\n\n\t\t// If performing the deletion via REST API change the error message to reflect the reason for the prevention.\n\t\tif ( llms_is_rest() ) {\n\t\t\t// Filter the error message.\n\t\t\tadd_filter( 'rest_request_after_callbacks', array( __CLASS__, 'rest_filter_products_with_active_subscriptions_error_message' ), 10, 3 );\n\t\t} else { // Deleting via wp-admin.\n\t\t\twp_die(\n\t\t\t\tesc_html( self::delete_product_with_active_subscriptions_error_message( $product->get( 'id' ) ) )\n\t\t\t);\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Filter the error message returned when trying to delete a product with active subscription via REST API.\n\t *\n\t * The original message is a standard permission denied message.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.\n\t *                                                                   Usually a WP_REST_Response or WP_Error.\n\t * @param array                                            $handler  Route handler used for the request.\n\t * @param WP_REST_Request                                  $request  Request used to generate the response.\n\t * @return WP_REST_Response|WP_HTTP_Response|WP_Error|mixed\n\t */\n\tpublic static function rest_filter_products_with_active_subscriptions_error_message( $response, $handler, $request ) {\n\n\t\tif ( is_wp_error( $response ) ) {\n\t\t\tforeach ( $response->errors as $code => &$data ) {\n\t\t\t\t// Error code can be produced by our rest-api or by wp core.\n\t\t\t\tif ( in_array( $code, array( 'llms_rest_cannot_delete', 'rest_cannot_delete' ), true ) ) {\n\t\t\t\t\t$data[0] = self::delete_product_with_active_subscriptions_error_message( $request['id'] );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $response;\n\t}\n\n\t/**\n\t * Returns the error message to display when deleting a product with active subscriptions.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @param int $post_id The WP_Post ID of the product.\n\t * @return string\n\t */\n\tpublic static function delete_product_with_active_subscriptions_error_message( $post_id ) {\n\n\t\t$post_type = get_post_type( $post_id );\n\n\t\tif ( ! in_array( $post_type, array( 'course', 'llms_membership' ), true ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$post_type_object = get_post_type_object( $post_type );\n\t\t$post_type_name   = $post_type_object->labels->name;\n\t\treturn sprintf(\n\t\t\t// Translators: %s = The post type plural name.\n\t\t\t__( 'Sorry, you are not allowed to delete %s with active subscriptions.', 'lifterlms' ),\n\t\t\t$post_type_name\n\t\t);\n\t}\n\n\t/**\n\t * Delete / Trash posts related to the deleted post.\n\t *\n\t * @since 3.16.12\n\t * @since 3.37.8 Allow for deletion of related items outside the WP core posts table.\n\t *\n\t * @param WP_Post $post WP Post that's been deleted.\n\t * @param array   $data Relationship data array.\n\t * @return void\n\t */\n\tprivate function delete_relationships( $post, $data ) {\n\n\t\tif ( isset( $data['post_type'] ) && isset( $data['meta_key'] ) ) {\n\n\t\t\t$this->delete_wp_posts( $post, $data );\n\n\t\t} elseif ( isset( $data['table_name'] ) && isset( $data['table_key'] ) ) {\n\n\t\t\t$this->delete_table_records( $post, $data );\n\n\t\t}\n\t}\n\n\t/**\n\t * Delete records from a table that are related to the deleted post.\n\t *\n\t * @since 3.37.8\n\t *\n\t * @param WP_Post $post WP Post that's been deleted.\n\t * @param array   $data Relationship data array.\n\t * @return void\n\t */\n\tprivate function delete_table_records( $post, $data ) {\n\n\t\tglobal $wpdb;\n\t\t$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\t\t$wpdb->prefix . $data['table_name'],\n\t\t\tarray(\n\t\t\t\t$data['table_key'] => $post->ID,\n\t\t\t),\n\t\t\t'%d'\n\t\t);\n\t}\n\n\t/**\n\t * Delete or trash WP Posts related to the deleted post.\n\t *\n\t * @since 3.37.8\n\t *\n\t * @param WP_Post $post WP Post that's been deleted.\n\t * @param array   $data Relationship data array.\n\t * @return void\n\t */\n\tprivate function delete_wp_posts( $post, $data ) {\n\n\t\t$relationships = $this->get_related_posts( $post->ID, $data['post_type'], $data['meta_key'] );\n\n\t\t$force = ( 'delete' === $data['action'] );\n\n\t\tforeach ( $relationships as $id ) {\n\t\t\twp_delete_post( $id, $force );\n\t\t}\n\t}\n\n\t/**\n\t * Get a list of post types with relationships that should be checked.\n\t *\n\t * @since 3.16.12\n\t *\n\t * @return array\n\t */\n\tprivate function get_post_types() {\n\t\treturn array_keys( $this->get_relationships() );\n\t}\n\n\t/**\n\t * Retrieve filtered LifterLMS post relationships array.\n\t *\n\t * @since 3.16.12\n\t *\n\t * @return array\n\t */\n\tprivate function get_relationships() {\n\t\treturn apply_filters( 'llms_get_post_relationships', $this->relationships );\n\t}\n\n\t/**\n\t * Retrieve an array of post ids related to the deleted post by post type and meta key.\n\t *\n\t * @since 3.16.12\n\t *\n\t * @param int    $post_id   WP Post ID of the deleted post.\n\t * @param string $post_type WP Post type of the related post(s).\n\t * @param string $meta_key  meta_key to check for relations by.\n\t * @return array\n\t */\n\tprivate function get_related_posts( $post_id, $post_type, $meta_key ) {\n\n\t\tglobal $wpdb;\n\t\treturn $wpdb->get_col(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT p.ID\n\t\t\t FROM {$wpdb->posts} AS p\n\t\t\t LEFT JOIN {$wpdb->postmeta} AS pm\n\t\t\t        ON p.ID = pm.post_id\n\t\t\t       AND pm.meta_key = %s\n\t\t\t WHERE p.post_type = %s\n\t\t\t   AND pm.meta_value = %d\",\n\t\t\t\t$meta_key,\n\t\t\t\t$post_type,\n\t\t\t\t$post_id\n\t\t\t)\n\t\t); // db-call ok; no-cache ok.\n\t}\n\n\t/**\n\t * Check relationships and delete / update related posts when a post is deleted.\n\t *\n\t * Called on `delete_post` hook (before a post is deleted).\n\t *\n\t * @since 3.16.12\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param int $post_id WP Post ID of the deleted post.\n\t * @return void\n\t */\n\tpublic function maybe_update_relationships( $post_id ) {\n\n\t\t$post = get_post( $post_id );\n\t\tif ( ! in_array( $post->post_type, $this->get_post_types(), true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tforeach ( $this->get_relationships() as $post_type => $relationships ) {\n\n\t\t\tif ( $post->post_type !== $post_type ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tforeach ( $relationships as $data ) {\n\n\t\t\t\tif ( in_array( $data['action'], array( 'delete', 'trash' ), true ) ) {\n\n\t\t\t\t\t$this->delete_relationships( $post, $data );\n\n\t\t\t\t} elseif ( 'unset' === $data['action'] ) {\n\n\t\t\t\t\t$this->unset_relationships( $post, $data );\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Unsets relationship data from post_meta when a post is deleted.\n\t *\n\t * @since 3.16.12\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param WP_Post $post WP Post that's been deleted.\n\t * @param array   $data Relationship data array.\n\t * @return void\n\t */\n\tprivate function unset_relationships( $post, $data ) {\n\n\t\t$relationships = $this->get_related_posts( $post->ID, $data['post_type'], $data['meta_key'] );\n\n\t\tforeach ( $relationships as $id ) {\n\n\t\t\tdelete_post_meta( $id, $data['meta_key'], $post->ID );\n\n\t\t\tif ( isset( $data['meta_keys_additional'] ) ) {\n\t\t\t\tforeach ( $data['meta_keys_additional'] as $key ) {\n\t\t\t\t\tdelete_post_meta( $id, $key );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nreturn new LLMS_Post_Relationships();\n"
  },
  {
    "path": "includes/class.llms.query.php",
    "content": "<?php\n/**\n * LLMS_Query class file.\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Query base class\n *\n * Handles queries and endpoints.\n *\n * @since 1.0.0\n * @since 4.0.0 Remove previously deprecated methods.\n */\nclass LLMS_Query {\n\n\t/**\n\t * Query var\n\t *\n\t * @var array\n\t */\n\tpublic $query_vars = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.0.0\n\t * @since 3.28.2 Unknown.\n\t * @since 3.36.3 Changed `pre_get_posts` callback from `10 (default) to `15`,\n\t *               so to avoid conflicts with the Divi theme whose callback runs at `10`,\n\t *               but since themes are loaded after plugins it overrode our one.\n\t * @since 4.5.0 Added action to serve 404s on unviewable certificates.\n\t * @since 6.0.0 Add callback to redirect old `llms_my_certificates` requests to the new url.\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'init', array( $this, 'add_endpoints' ) );\n\n\t\tif ( ! is_admin() ) {\n\n\t\t\tadd_filter( 'query_vars', array( $this, 'set_query_vars' ), 0 );\n\t\t\tadd_action( 'parse_request', array( $this, 'parse_request' ), 0 );\n\t\t\tadd_action( 'wp', array( $this, 'maybe_404_certificate' ), 50 );\n\t\t\tadd_action( 'wp', array( $this, 'maybe_redirect_certificate' ), 50 );\n\n\t\t}\n\n\t\t$this->init_query_vars();\n\n\t\tadd_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 15 );\n\t\tadd_filter( 'get_previous_post_where', array( $this, 'exclude_hidden_llms_products' ) );\n\t\tadd_filter( 'get_next_post_where', array( $this, 'exclude_hidden_llms_products' ) );\n\t}\n\n\t/**\n\t * Add Query Endpoints\n\t *\n\t * @since 1.0.0\n\t * @since 3.28.2 Handle dashboard tab pagination via a rewrite rule.\n\t * @since 5.0.2 Add support for slugs with non-latin characters.\n\t *\n\t * @return void\n\t */\n\tpublic function add_endpoints() {\n\n\t\tforeach ( $this->get_query_vars() as $key => $var ) {\n\t\t\tadd_rewrite_endpoint( $var, EP_PAGES, $key );\n\t\t}\n\n\t\tglobal $wp_rewrite;\n\t\tforeach ( LLMS_Student_Dashboard::get_tabs() as $id => $tab ) {\n\t\t\tif ( ! empty( $tab['paginate'] ) ) {\n\t\t\t\t$regex    = sprintf( '(.?.+?)/%1$s/%2$s/?([0-9]{1,})/?$', urldecode( $tab['endpoint'] ), $wp_rewrite->pagination_base );\n\t\t\t\t$redirect = sprintf( 'index.php?pagename=$matches[1]&%s=$matches[3]&paged=$matches[2]', $id );\n\t\t\t\tadd_rewrite_rule( $regex, $redirect, 'top' );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get query variables\n\t *\n\t * @since Unknown\n\t *\n\t * @return array\n\t */\n\tpublic function get_query_vars() {\n\t\treturn apply_filters( 'llms_get_endpoints', $this->query_vars );\n\t}\n\n\t/**\n\t * Get a taxonomy query that filters out courses & memberships based on catalog / search visibility settings\n\t *\n\t * @since 3.6.0\n\t *\n\t * @param array $query Existing taxonomy query from the global $wp_query.\n\t * @return array\n\t */\n\tprivate function get_tax_query( $query = array() ) {\n\n\t\tif ( ! is_array( $query ) ) {\n\t\t\t$query = array(\n\t\t\t\t'relation' => 'AND',\n\t\t\t);\n\t\t}\n\n\t\t$terms = wp_list_pluck(\n\t\t\tget_terms(\n\t\t\t\tarray(\n\t\t\t\t\t'taxonomy'   => 'llms_product_visibility',\n\t\t\t\t\t'hide_empty' => false,\n\t\t\t\t)\n\t\t\t),\n\t\t\t'term_taxonomy_id',\n\t\t\t'name'\n\t\t);\n\n\t\t$not_in = ( is_search() ) ? array( $terms['hidden'], $terms['catalog'] ) : array( $terms['hidden'], $terms['search'] );\n\n\t\t$query[] = array(\n\t\t\t'field'    => 'term_taxonomy_id',\n\t\t\t'operator' => 'NOT IN',\n\t\t\t'taxonomy' => 'llms_product_visibility',\n\t\t\t'terms'    => $not_in,\n\t\t);\n\n\t\treturn $query;\n\t}\n\n\t/**\n\t * Init queries\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function init_query_vars() {\n\n\t\t$this->query_vars = array(\n\t\t\t'confirm-payment' => get_option( 'lifterlms_myaccount_confirm_payment_endpoint', 'confirm-payment' ),\n\t\t\t'lost-password'   => get_option( 'lifterlms_myaccount_lost_password_endpoint', 'lost-password' ),\n\t\t);\n\t}\n\n\t/**\n\t * Parse the request for query variables\n\t *\n\t * @since unknown\n\t * @since 3.31.0 sanitize and unslash `$_GET` vars.\n\t *\n\t * @return void\n\t */\n\tpublic function parse_request() {\n\n\t\tglobal $wp;\n\n\t\tforeach ( $this->get_query_vars() as $key => $var ) {\n\t\t\tif ( isset( $_GET[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\t\t$wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\t} elseif ( isset( $wp->query_vars[ $var ] ) ) {\n\t\t\t\t$wp->query_vars[ $key ] = $wp->query_vars[ $var ];\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Sets the WP_Query variables for \"post_type\" on LifterLMS custom taxonomy archive pages for Courses and Memberships.\n\t *\n\t * @since 1.4.4 Moved from LLMS_Post_Types.\n\t * @since 3.16.8\n\t * @since 3.33.0 Added `post_title` as a secondary sort when the primary sort is `menu_order`\n\t * @since 3.36.3 Changed `pre_get_posts` callback from `10 (default) to `15`,\n\t *               so to avoid conflicts with the Divi theme whose callback runs at `10`,\n\t *               but since themes are loaded after plugins it overrode our one.\n\t * @since 3.36.4 Don't remove this callback from within the callback itself.\n\t *               Rather use a static variable to make sure the business logic of this\n\t *               method is executed only once.\n\t *\n\t * @param WP_Query $query Main WP_Query Object.\n\t * @return void\n\t */\n\tpublic function pre_get_posts( $query ) {\n\n\t\tstatic $done      = false;\n\t\t$modify_tax_query = false;\n\n\t\tif ( $done ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! is_admin() && $query->is_main_query() ) {\n\n\t\t\tif ( is_search() ) {\n\t\t\t\t$modify_tax_query = true;\n\t\t\t}\n\n\t\t\tif ( is_tax( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) ) ) {\n\n\t\t\t\t$query->set( 'post_type', array( 'course', 'llms_membership' ) );\n\t\t\t\t$modify_tax_query = true;\n\n\t\t\t}\n\n\t\t\tif ( is_post_type_archive( 'course' ) || $query->get( 'page_id' ) == llms_get_page_id( 'courses' ) || is_tax( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track' ) ) ) {\n\n\t\t\t\t$query->set( 'posts_per_page', get_option( 'lifterlms_shop_courses_per_page', 10 ) );\n\n\t\t\t\t$sorting = explode( ',', get_option( 'lifterlms_shop_ordering', 'menu_order,ASC' ) );\n\n\t\t\t\t$orderby = empty( $sorting[0] ) ? 'menu_order' : $sorting[0];\n\t\t\t\tif ( 'menu_order' === $orderby ) {\n\t\t\t\t\t$orderby .= ' post_title';\n\t\t\t\t}\n\t\t\t\t$order = empty( $sorting[1] ) ? 'ASC' : $sorting[1];\n\n\t\t\t\t$query->set( 'orderby', apply_filters( 'llms_courses_orderby', $orderby ) );\n\t\t\t\t$query->set( 'order', apply_filters( 'llms_courses_order', $order ) );\n\n\t\t\t\t$modify_tax_query = true;\n\n\t\t\t} elseif ( is_post_type_archive( 'llms_membership' ) || $query->get( 'page_id' ) == llms_get_page_id( 'memberships' ) || is_tax( array( 'membership_tag', 'membership_cat' ) ) ) {\n\n\t\t\t\t$query->set( 'posts_per_page', get_option( 'lifterlms_memberships_per_page', 10 ) );\n\n\t\t\t\t$sorting = explode( ',', get_option( 'lifterlms_memberships_ordering', 'menu_order,ASC' ) );\n\n\t\t\t\t$orderby = empty( $sorting[0] ) ? 'menu_order' : $sorting[0];\n\t\t\t\tif ( 'menu_order' === $orderby ) {\n\t\t\t\t\t$orderby .= ' post_title';\n\t\t\t\t}\n\t\t\t\t$order = empty( $sorting[1] ) ? 'ASC' : $sorting[1];\n\n\t\t\t\t$query->set( 'orderby', apply_filters( 'llms_memberships_orderby', $orderby ) );\n\t\t\t\t$query->set( 'order', apply_filters( 'llms_memberships_order', $order ) );\n\n\t\t\t\t$modify_tax_query = true;\n\n\t\t\t}\n\n\t\t\t// Do it once.\n\t\t\t$done = true;\n\n\t\t}\n\n\t\tif ( $modify_tax_query ) {\n\n\t\t\t$query->set( 'tax_query', $this->get_tax_query( $query->get( 'tax_query' ) ) );\n\n\t\t}\n\t}\n\n\t/**\n\t * Serve a 404 for certificates that are not viewable by the current user\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_404_certificate() {\n\n\t\tif ( 'llms_my_certificate' === get_post_type() ) {\n\t\t\t$cert = new LLMS_User_Certificate( get_the_ID() );\n\t\t\tif ( ! $cert->can_user_view() ) {\n\n\t\t\t\tglobal $wp_query;\n\t\t\t\t$wp_query->set_404();\n\t\t\t\tstatus_header( 404 );\n\t\t\t\tnocache_headers();\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Redirect requests to old llms_my_certificate URLs to the new url.\n\t *\n\t * Redirects `/my_certificate/slug` to `/certificate/slug` maintaining\n\t * translations.\n\t *\n\t * This will only redirect if `$wp_query` detects a 404 and a certificate\n\t * exists with the parsed slug. This check is important to prevent against\n\t * collisions which are theoretically possible, though probably unlikely.\n\t *\n\t * @since 6.0.0\n\t * @since 7.5.0 Fixed passing null to parameter #1 ($haystack) using `strpos`.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_redirect_certificate() {\n\n\t\tglobal $wp, $wp_query;\n\n\t\t$old  = sprintf( '/%s/', _x( 'my_certificate', 'slug', 'lifterlms' ) );\n\t\t$path = wp_parse_url( home_url( $wp->request ), PHP_URL_PATH );\n\t\tif ( $wp_query->is_404() && $path && 0 === strpos( $path, $old ) ) {\n\t\t\t$slug     = str_replace( $old, '', $path );\n\t\t\t$new_post = get_page_by_path( $slug, 'OBJECT', 'llms_my_certificate' );\n\t\t\tif ( $new_post ) {\n\t\t\t\tllms_redirect_and_exit( get_permalink( $new_post->ID ) );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Set query variables\n\t *\n\t * @since Unknown\n\t *\n\t * @param  array $vars WP query variables available for query.\n\t * @return array\n\t */\n\tpublic function set_query_vars( $vars ) {\n\n\t\tforeach ( $this->get_query_vars() as $key => $var ) {\n\t\t\t$vars[] = $key;\n\t\t}\n\n\t\treturn $vars;\n\t}\n\n\t/**\n\t * Avoid showing hidden products in the previous/next post queries.\n\t *\n\t * @since 9.0.0\n\t * @param $where\n\t *\n\t * @return string\n\t */\n\tpublic function exclude_hidden_llms_products( $where ) {\n\t\tglobal $wpdb;\n\n\t\t$where .= \" AND p.ID NOT IN (\n\t\tSELECT object_id\n\t\tFROM {$wpdb->term_relationships} AS tr\n\t\tINNER JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id\n\t\tINNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id\n\t\tWHERE tt.taxonomy = 'llms_product_visibility'\n\t\tAND t.slug IN ('hidden')\n\t)\";\n\n\t\treturn $where;\n\t}\n}\n\nreturn new LLMS_Query();\n"
  },
  {
    "path": "includes/class.llms.query.quiz.attempt.php",
    "content": "<?php\n/**\n * Query LifterLMS Students for a given course / membership.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.16.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Query LifterLMS Students for a given course / membership\n *\n * @since 3.16.0\n * @since 3.35.0 Unknown.\n * @since 4.2.0 Added `exclude` arg.\n *\n * @arg  $attempt    (int)       Query by attempt number\n * @arg  $quiz_id    (int|array) Query by Quiz WP post ID (locate multiple quizzes with an array of ids)\n * @arg  $student_id (int|array) Query by WP User ID (locate by multiple users with an array of ids)\n *\n * @arg  $page       (int)       Get results by page\n * @arg  $per_page   (int)       Number of results per page (default: 25)\n * @arg  $sort       (array)     Define query sorting options [id,student_id,quiz_id,start_date,update_date,end_date,attempt,grade,current,passed]\n *\n * @example\n *       $query = new LLMS_Query_Quiz_Attempt( array(\n *           'student_id' => 1234,\n *           'quiz_id' => 5678,\n *       ) );\n */\nclass LLMS_Query_Quiz_Attempt extends LLMS_Database_Query {\n\n\t/**\n\t * Identify the extending query\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'quiz_attempt';\n\n\t/**\n\t * Retrieve default arguments for a student query.\n\t *\n\t * @since 3.16.0\n\t * @since 4.2.0 Added `exclude` default arg.\n\t * @since 7.8.0 Added `can_be_resumed` default arg.\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_args() {\n\n\t\t$args = array(\n\t\t\t'student_id'     => array(),\n\t\t\t'quiz_id'        => array(),\n\t\t\t'sort'           => array(\n\t\t\t\t'start_date' => 'DESC',\n\t\t\t\t'attempt'    => 'DESC',\n\t\t\t\t'id'         => 'ASC',\n\t\t\t),\n\t\t\t'status'         => array(),\n\t\t\t'status_exclude' => array(),\n\t\t\t'attempt'        => null,\n\t\t\t'exclude'        => array(),\n\t\t\t'can_be_resumed' => null,\n\t\t\t'search'         => '',\n\t\t);\n\n\t\t$args = wp_parse_args( $args, parent::get_default_args() );\n\n\t\treturn apply_filters( $this->get_filter( 'default_args' ), $args );\n\t}\n\n\t/**\n\t * Retrieve an array of LLMS_Quiz_Attempts for the given result set returned by the query\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return LLMS_Quiz_Attempt[]\n\t */\n\tpublic function get_attempts() {\n\n\t\t$attempts = array();\n\t\t$results  = $this->get_results();\n\n\t\tif ( $results ) {\n\n\t\t\tforeach ( $results as $result ) {\n\t\t\t\t$attempts[] = new LLMS_Quiz_Attempt( $result->id );\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $attempts;\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'get_attempts' ), $attempts );\n\t}\n\n\t/**\n\t * Parses data passed to $statuses\n\t *\n\t * Convert strings to array and ensure resulting array contains only valid statuses.\n\t * If no valid statuses, returns to the default.\n\t *\n\t * @since 3.16.0\n\t * @since 4.2.0 Added `exclude` arg sanitization.\n\t *\n\t * @return void\n\t */\n\tprotected function parse_args() {\n\n\t\t// Sanitize post, user, excluded attempts ids.\n\t\tforeach ( array( 'student_id', 'quiz_id', 'exclude' ) as $key ) {\n\t\t\t$this->arguments[ $key ] = $this->sanitize_id_array( $this->arguments[ $key ] );\n\t\t}\n\n\t\t// Validate status args.\n\t\t$valid_statuses = array_keys( llms_get_quiz_attempt_statuses() );\n\t\tforeach ( array( 'status', 'status_exclude' ) as $key ) {\n\n\t\t\t// Allow single statuses to be passed in as a string.\n\t\t\tif ( is_string( $this->arguments[ $key ] ) ) {\n\t\t\t\t$this->arguments[ $key ] = array( $this->arguments[ $key ] );\n\t\t\t}\n\n\t\t\t// Ensure submitted statuses are valid.\n\t\t\tif ( $this->arguments[ $key ] ) {\n\t\t\t\t$this->arguments[ $key ] = array_intersect( $valid_statuses, $this->arguments[ $key ] );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Prepare the SQL for the query.\n\t *\n\t * @since 3.16.0\n\t * @since 6.0.0 Renamed from `preprare_query()`.\n\t * @since 10.0.0 Build count_query from shared clauses instead of using SQL_CALC_FOUND_ROWS.\n\t *\n\t * @return string\n\t */\n\tprotected function prepare_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$from  = \"FROM {$wpdb->prefix}lifterlms_quiz_attempts qa\";\n\t\t$joins = $this->sql_joins();\n\t\t$where = $this->sql_where();\n\n\t\tif ( $this->get( 'count_only' ) ) {\n\t\t\treturn \"SELECT COUNT(*) AS total {$from} {$joins} {$where};\";\n\t\t}\n\n\t\tif ( ! $this->get( 'no_found_rows' ) ) {\n\t\t\t$this->count_query = \"SELECT COUNT(*) {$from} {$joins} {$where}\";\n\t\t}\n\n\t\treturn \"SELECT qa.id {$from} {$joins} {$where} {$this->sql_orderby()} {$this->sql_limit()};\";\n\t}\n\n\t/**\n\t * SQL \"joins\" clause for the query.\n\t *\n\t * @since 9.1.0\n\t *\n\t * @return string\n\t */\n\tprotected function sql_joins() {\n\t\tglobal $wpdb;\n\n\t\t$joins = '';\n\n\t\t// Join users table for search functionality\n\t\tif ( $this->get( 'search' ) ) {\n\t\t\t$joins .= \" LEFT JOIN {$wpdb->users} u ON qa.student_id = u.ID\";\n\t\t\t$joins .= \" LEFT JOIN {$wpdb->usermeta} um_first ON u.ID = um_first.user_id AND um_first.meta_key = 'first_name'\";\n\t\t\t$joins .= \" LEFT JOIN {$wpdb->usermeta} um_last ON u.ID = um_last.user_id AND um_last.meta_key = 'last_name'\";\n\t\t\t// Add in posts of type llms_quiz for search functionality\n\t\t\t$joins .= \" LEFT JOIN {$wpdb->posts} p ON qa.quiz_id = p.ID AND p.post_type = 'llms_quiz'\";\n\t\t}\n\n\t\treturn $joins;\n\t}\n\n\t/**\n\t * SQL \"where\" clause for the query.\n\t *\n\t * @since 3.16.0\n\t * @since 3.35.0 Better SQL preparation.\n\t * @since 4.2.0 Added `exclude` arg logic.\n\t * @since 7.8.0 Added `can_be_resumed` arg logic.\n\t *\n\t * @return string\n\t */\n\tprotected function sql_where() {\n\n\t\tglobal $wpdb;\n\n\t\t$sql = 'WHERE 1';\n\n\t\tforeach ( array( 'quiz_id', 'student_id' ) as $key ) {\n\t\t\t$ids = $this->get( $key );\n\t\t\tif ( $ids ) {\n\t\t\t\t$prepared = implode( ',', $ids );\n\t\t\t\t$sql     .= \" AND qa.{$key} IN ({$prepared})\";\n\t\t\t}\n\t\t}\n\n\t\t// Add attempt lookup.\n\t\t$val = $this->get( 'attempt' );\n\t\tif ( '' !== $val ) {\n\t\t\t$sql .= $wpdb->prepare( ' AND qa.attempt = %d', $val );\n\t\t}\n\n\t\t// Add attempt exclude.\n\t\t$exclude = $this->get( 'exclude' );\n\t\tif ( $exclude ) {\n\t\t\t$prepared = implode( ',', $exclude );\n\t\t\t$sql     .= \" AND qa.id NOT IN ({$prepared})\";\n\t\t}\n\n\t\t$status = $this->get( 'status' );\n\t\tif ( $status ) {\n\t\t\t$prepared = implode( ',', array_map( array( $this, 'escape_and_quote_string' ), $status ) );\n\t\t\t$sql     .= \" AND qa.status IN ({$prepared})\";\n\t\t}\n\n\t\t$status_exclude = $this->get( 'status_exclude' );\n\t\tif ( $status_exclude ) {\n\t\t\t$prepared = implode( ',', array_map( array( $this, 'escape_and_quote_string' ), $status_exclude ) );\n\t\t\t$sql     .= \" AND qa.status NOT IN ({$prepared})\";\n\t\t}\n\n\t\t$can_be_resumed = $this->get( 'can_be_resumed' );\n\t\tif ( '' !== $can_be_resumed ) {\n\t\t\t$sql .= $wpdb->prepare( ' AND qa.can_be_resumed = %d', $can_be_resumed );\n\t\t}\n\n\t\t$search = $this->get( 'search' );\n\t\tif ( $search ) {\n\t\t\t$search = $wpdb->esc_like( $search );\n\t\t\t$sql   .= $wpdb->prepare(\n\t\t\t\t' AND (\n\t\t\t\t\tu.user_login LIKE %s\n\t\t\t\t\tOR u.user_email LIKE %s\n\t\t\t\t\tOR u.display_name LIKE %s\n\t\t\t\t\tOR um_first.meta_value LIKE %s\n\t\t\t\t\tOR um_last.meta_value LIKE %s\n\t\t\t\t\tOR p.post_title LIKE %s\n\t\t\t\t)',\n\t\t\t\t'%' . $search . '%',\n\t\t\t\t'%' . $search . '%',\n\t\t\t\t'%' . $search . '%',\n\t\t\t\t'%' . $search . '%',\n\t\t\t\t'%' . $search . '%',\n\t\t\t\t'%' . $search . '%'\n\t\t\t);\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'where' ), $sql, $this );\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.query.user.postmeta.php",
    "content": "<?php\n/**\n * LifterLMS User Postmeta Query\n *\n * @package LifterLMS/Classes\n *\n * @since 3.15.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS User Postmeta Query\n *\n * @since 3.15.0\n */\nclass LLMS_Query_User_Postmeta extends LLMS_Database_Query {\n\n\t/**\n\t * Identify the extending query\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'user_postmeta';\n\n\t/**\n\t * Retrieve default arguments for a student query\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tprotected function get_default_args() {\n\n\t\t$args = array(\n\t\t\t'include_post_children' => true,\n\t\t\t'query'                 => array(),\n\t\t\t'query_compare'         => 'OR',\n\t\t\t'post_id'               => array(),\n\t\t\t'sort'                  => array(\n\t\t\t\t'updated_date' => 'DESC',\n\t\t\t\t'meta_id'      => 'ASC',\n\t\t\t),\n\t\t\t'types'                 => array(),\n\t\t\t'user_id'               => array(),\n\t\t);\n\n\t\t$args = wp_parse_args( $args, parent::get_default_args() );\n\n\t\treturn apply_filters( $this->get_filter( 'default_args' ), $args );\n\n\t}\n\n\t/**\n\t * Retrieve an array of LLMS_User_Postmetas for the given set of results\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_metas() {\n\n\t\t$metas   = array();\n\t\t$results = $this->get_results();\n\n\t\tif ( $results ) {\n\n\t\t\tforeach ( $results as $result ) {\n\t\t\t\t$metas[] = new LLMS_User_Postmeta( $result->meta_id );\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $metas;\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'get_metas' ), $metas );\n\n\t}\n\n\t/**\n\t * Parses data passed to $statuses\n\t * Convert strings to array and ensure resulting array contains only valid statuses\n\t * If no valid statuses, returns to the default\n\t *\n\t * @return   void\n\t * @since    3.15.0\n\t * @since 7.5.0 Added 'Favorites' event.\n\t */\n\tprotected function parse_args() {\n\n\t\t// Sanitize post & user ids.\n\t\tforeach ( array( 'post_id', 'user_id' ) as $key ) {\n\n\t\t\t$this->arguments[ $key ] = $this->sanitize_id_array( $this->arguments[ $key ] );\n\n\t\t}\n\n\t\tif ( $this->arguments['include_post_children'] ) {\n\n\t\t\tforeach ( $this->arguments['post_id'] as $id ) {\n\n\t\t\t\tif ( 'course' !== get_post_type( $id ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$course                     = llms_get_post( $id );\n\t\t\t\t$this->arguments['post_id'] = array_merge(\n\t\t\t\t\t$this->arguments['post_id'],\n\t\t\t\t\t$this->sanitize_id_array( $course->get_sections( 'ids' ) ),\n\t\t\t\t\t$this->sanitize_id_array( $course->get_lessons( 'ids' ) ),\n\t\t\t\t\t$this->sanitize_id_array( $course->get_quizzes() )\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->arguments['types'] ) {\n\n\t\t\t$all_events = array(\n\t\t\t\t'completion'  => array(\n\t\t\t\t\t'key'   => '_is_complete',\n\t\t\t\t\t'value' => 'yes',\n\t\t\t\t),\n\t\t\t\t'status'      => array(\n\t\t\t\t\t'compare' => 'IS NOT NULL',\n\t\t\t\t\t'key'     => '_status',\n\t\t\t\t),\n\t\t\t\t'achievement' => array(\n\t\t\t\t\t'compare' => 'IS NOT NULL',\n\t\t\t\t\t'key'     => '_achievement_earned',\n\t\t\t\t),\n\t\t\t\t'certificate' => array(\n\t\t\t\t\t'compare' => 'IS NOT NULL',\n\t\t\t\t\t'key'     => '_certificate_earned',\n\t\t\t\t),\n\t\t\t\t'email'       => array(\n\t\t\t\t\t'compare' => 'IS NOT NULL',\n\t\t\t\t\t'key'     => '_email_sent',\n\t\t\t\t),\n\t\t\t\t'purchase'    => array(\n\t\t\t\t\t'compare' => 'LIKE',\n\t\t\t\t\t'key'     => '_enrollment_trigger',\n\t\t\t\t\t'value'   => 'order_%',\n\t\t\t\t),\n\t\t\t\t'favorites'   => array(\n\t\t\t\t\t'key'     => '_favorite',\n\t\t\t\t\t'compare' => 'IS NOT NULL',\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tif ( is_string( $this->arguments['types'] ) && 'all' === $this->arguments['types'] ) {\n\n\t\t\t\t$this->arguments['query'] = array_values( $all_events );\n\n\t\t\t} else {\n\n\t\t\t\t$this->arguments['query'] = array();\n\n\t\t\t\tif ( ! is_array( $this->arguments['types'] ) ) {\n\t\t\t\t\t$this->arguments['types'] = array( $this->arguments['types'] );\n\t\t\t\t}\n\n\t\t\t\tforeach ( $this->arguments['types'] as $type ) {\n\t\t\t\t\tif ( ! isset( $all_events[ $type ] ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t$this->arguments['query'][] = $all_events[ $type ];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->arguments['query'] ) {\n\n\t\t\tforeach ( $this->arguments['query'] as $i => &$query ) {\n\n\t\t\t\t// Ensure that each query has a compare operator.\n\t\t\t\t$query = wp_parse_args(\n\t\t\t\t\t$query,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'compare' => '=',\n\t\t\t\t\t\t'key'     => '',\n\t\t\t\t\t\t'value'   => '',\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$operators = array( '=', '!=', 'LIKE', 'IN', 'NOT IN', 'IS NOT NULL' );\n\t\t\t\tif ( ! in_array( $query['compare'], $operators ) ) {\n\t\t\t\t\tunset( $this->arguments['query'][ $i ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( ! in_array( $this->arguments['query_compare'], array( 'AND', 'OR' ) ) ) {\n\t\t\t$this->arguments['query_compare'] = 'OR';\n\t\t}\n\n\t}\n\n\t/**\n\t * Prepare the SQL for the query.\n\t *\n\t * @since 3.15.0\n\t * @since 6.0.0 Renamed from `preprare_query()`.\n\t * @since 10.0.0 Build count_query from shared clauses instead of using SQL_CALC_FOUND_ROWS.\n\t *\n\t * @return string\n\t */\n\tprotected function prepare_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$from  = \"FROM {$wpdb->prefix}lifterlms_user_postmeta\";\n\t\t$where = $this->sql_where();\n\n\t\tif ( $this->get( 'count_only' ) ) {\n\t\t\treturn \"SELECT COUNT(*) AS total {$from} {$where};\";\n\t\t}\n\n\t\tif ( ! $this->get( 'no_found_rows' ) ) {\n\t\t\t$this->count_query = \"SELECT COUNT(*) {$from} {$where}\";\n\t\t}\n\n\t\t$vars = array(\n\t\t\t$this->get_skip(),\n\t\t\t$this->get( 'per_page' ),\n\t\t);\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$sql = $wpdb->prepare(\n\t\t\t\"SELECT meta_id\n\t\t\t {$from}\n\t\t\t {$where}\n\t\t\t {$this->sql_orderby()}\n\t\t\t LIMIT %d, %d;\",\n\t\t\t$vars\n\t\t);\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\treturn $sql;\n\n\t}\n\n\t/**\n\t * SQL \"where\" clause for the query\n\t *\n\t * @return   string\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tprotected function sql_where() {\n\n\t\tglobal $wpdb;\n\n\t\t$sql = 'WHERE 1';\n\n\t\tforeach ( array( 'post_id', 'user_id' ) as $key ) {\n\n\t\t\t$ids = $this->get( $key );\n\t\t\tif ( $ids ) {\n\t\t\t\t$prepared = implode( ',', $ids );\n\t\t\t\t$sql     .= \" AND {$key} IN ({$prepared})\";\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->get( 'query' ) ) {\n\n\t\t\t$sql .= ' AND ( ';\n\n\t\t\tforeach ( $this->get( 'query' ) as $i => $query ) {\n\n\t\t\t\tif ( 0 !== $i ) {\n\t\t\t\t\t$sql .= \" {$this->get( 'query_compare' )} \";\n\t\t\t\t}\n\n\t\t\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t\t\tswitch ( $query['compare'] ) {\n\n\t\t\t\t\tcase '=':\n\t\t\t\t\tcase '!=':\n\t\t\t\t\tcase 'LIKE':\n\t\t\t\t\t\t$sql .= $wpdb->prepare( \"( meta_key = %s AND meta_value {$query['compare']} %s )\", $query['key'], $query['value'] );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'IN':\n\t\t\t\t\tcase 'NOT IN':\n\t\t\t\t\t\t$query['value'] = array_map( array( $this, 'escape_and_quote_string' ), $query['value'] );\n\t\t\t\t\t\t$vals           = implode( ',', $query['value'] );\n\t\t\t\t\t\t$sql           .= $wpdb->prepare( \"( meta_key = %s AND meta_value {$query['compare']} ( {$vals} ) )\", $query['key'] );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase 'IS NOT NULL':\n\t\t\t\t\t\t$sql .= $wpdb->prepare( \"( meta_key = %s AND meta_value {$query['compare']} )\", $query['key'] );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t\t}\n\n\t\t\t$sql .= ' )';\n\n\t\t}\n\n\t\treturn apply_filters( $this->get_filter( 'where' ), $sql, $this );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.question.manager.php",
    "content": "<?php\n/**\n * LifterLMS Quiz Question Manager\n *\n * Don't instantiate this directly, instead use the wrapper functions\n * found in the LLMS_Quiz and LLMS_Question classes\n *\n * @package LifterLMS/Classes\n *\n * @since 3.16.0\n * @version 3.27.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Question_Manager class.\n *\n * @since 3.16.0\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Question_Manager {\n\n\t/**\n\t * @var LLMS_Question|LLMS_Quiz\n\t * @since 3.16.0\n\t */\n\tpublic $parent;\n\n\t/**\n\t * Constructor\n\t *\n\t * @param    obj $parent  instance of the parent LLMS_Quiz or LLMS_Question\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function __construct( $parent ) {\n\n\t\t$this->parent = $parent;\n\n\t}\n\n\t/**\n\t * Quick access to the parent attribute\n\t *\n\t * @return   [type]\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tprivate function get_parent() {\n\t\treturn $this->parent;\n\t}\n\n\t/**\n\t * Quick access to parents type property\n\t *\n\t * @return   string    [llms_quiz|llms_question]\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tprivate function get_parent_type() {\n\t\treturn $this->parent->get( 'type' );\n\t}\n\n\t/**\n\t * Retrieve the related LLMS_Quiz\n\t *\n\t * @return   obj\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tprivate function get_quiz() {\n\n\t\tif ( 'llms_quiz' === $this->get_parent_type() ) {\n\t\t\treturn $this->parent;\n\t\t}\n\t\treturn $this->parent->get_quiz();\n\n\t}\n\n\t/**\n\t * Create a new question and add it to the quiz\n\t *\n\t * @param    array $data  array of question data\n\t * @return   false|question id\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function create_question( $data = array() ) {\n\n\t\t// Ensure the question belongs to this quiz.\n\t\t$data['parent_id'] = $this->get_parent()->get( 'id' );\n\n\t\t$question = new LLMS_Question( 'new', $data );\n\t\tif ( $question->get( 'id' ) ) {\n\t\t\treturn $question->get( 'id' );\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Delete a question associated with this quiz\n\t * skips trash and force deletes the question\n\t *\n\t * @param    int $id  WP Post ID of a question (must be associated with this quiz)\n\t * @return   boolean      true = deleted, false = error\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function delete_question( $id ) {\n\n\t\t$question = $this->get_question( $id );\n\t\tif ( ! $question ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Error.\n\t\tif ( ! wp_delete_post( $id, true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Deleted.\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Retrieve a question associated with this quiz by question ID\n\t *\n\t * @param    int $id  WP Post ID of the question\n\t * @return   boolean\n\t * @since    3.16.0\n\t * @version  3.27.0\n\t */\n\tpublic function get_question( $id ) {\n\n\t\t$question = llms_get_post( $id );\n\n\t\t// Not valid question, return false.\n\t\tif ( empty( $question ) || ! is_a( $question, 'LLMS_Question' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$parent_id = $question->get( 'parent_id' );\n\n\t\t// When parent id is set, only retrieve questions attached to this parent.\n\t\tif ( $parent_id && $parent_id !== $this->get_parent()->get( 'id' ) ) {\n\n\t\t\tif ( 'llms_question' === $this->get_parent_type() && $this->get_quiz()->get( 'id' ) === $question->get_quiz()->get( 'id' ) ) {\n\t\t\t\treturn $question;\n\t\t\t}\n\n\t\t\treturn false;\n\t\t}\n\n\t\t// Success.\n\t\treturn $question;\n\n\t}\n\n\t/**\n\t * Get questions\n\t *\n\t * @param    string $return  type of return [ids|posts|questions]\n\t * @return   array\n\t * @since    3.3.0\n\t * @version  3.24.0\n\t */\n\tpublic function get_questions( $return = 'questions' ) {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_parent_id',\n\t\t\t\t\t\t'value' => $this->get_parent()->get( 'id' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'order'          => 'ASC',\n\t\t\t\t'orderby'        => 'menu_order',\n\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t'post_type'      => 'llms_question',\n\t\t\t\t'posts_per_page' => 500,\n\t\t\t)\n\t\t);\n\n\t\tif ( 'ids' === $return ) {\n\t\t\t$ret = wp_list_pluck( $query->posts, 'ID' );\n\t\t} elseif ( 'posts' === $return ) {\n\t\t\t$ret = $query->posts;\n\t\t} else {\n\t\t\t$ret = array();\n\t\t\tforeach ( $query->posts as $post ) {\n\t\t\t\t$ret[] = new LLMS_Question( $post );\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_quiz_get_questions', $ret, $this, $return );\n\n\t}\n\n\t/**\n\t * Create or update questions\n\t * If 'id' passed in $data array will update existing question\n\t * Omit 'id' to create a new question\n\t *\n\t * @param    array $data  array of question data\n\t * @return   false|question id\n\t * @since    3.16.0\n\t * @version  3.17.2\n\t */\n\tpublic function update_question( $data = array() ) {\n\n\t\t// If there's no ID, we'll add a new question.\n\t\tif ( ! isset( $data['id'] ) ) {\n\t\t\treturn $this->create_question( $data );\n\t\t}\n\n\t\t// Get the question.\n\t\t$question = $this->get_question( $data['id'] );\n\t\tif ( ! $question ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Update all submitted data.\n\t\tforeach ( $data as $key => $val ) {\n\n\t\t\t// Merge image data into the array.\n\t\t\tif ( 'image' === $key ) {\n\t\t\t\t$val = array_merge(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'enabled' => 'no',\n\t\t\t\t\t\t'id'      => '',\n\t\t\t\t\t\t'src'     => '',\n\t\t\t\t\t),\n\t\t\t\t\t$question->get( $key ),\n\t\t\t\t\t$val\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t$question->set( $key, $val );\n\t\t}\n\n\t\t// Return question ID.\n\t\treturn $question->get( 'id' );\n\n\t}\n\n\n}\n"
  },
  {
    "path": "includes/class.llms.question.types.php",
    "content": "<?php\n/**\n * LifterLMS Question Types\n *\n * @package LifterLMS/Classes\n *\n * @since 3.16.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Question_Types class.\n *\n * @since 3.16.0\n * @since 3.30.3 Fixed typo.\n */\nclass LLMS_Question_Types {\n\n\t/**\n\t * Initializer\n\t *\n\t * @return   void\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic static function init() {\n\n\t\tadd_filter( 'llms_get_question_types', array( __CLASS__, 'load' ), 5 );\n\n\t}\n\n\t/**\n\t * Retrieve question type model defaults\n\t *\n\t * @since 3.16.0\n\t * @since 3.30.3 Fixed typo in icon name.\n\t *\n\t * @return array\n\t */\n\tpublic static function get_model() {\n\n\t\treturn apply_filters(\n\t\t\t'llms_question_type_model_defaults',\n\t\t\tarray(\n\t\t\t\t'choices'         => array(\n\t\t\t\t\t'selectable' => true,\n\t\t\t\t\t'markers'    => range( 'A', 'Z' ),\n\t\t\t\t\t'max'        => 26,\n\t\t\t\t\t'min'        => 2,\n\t\t\t\t\t'multi'      => true,\n\t\t\t\t\t'type'       => 'text',\n\t\t\t\t),\n\t\t\t\t'clarifications'  => true,\n\t\t\t\t'description'     => true,\n\t\t\t\t'default_choices' => array(),\n\t\t\t\t'grading'         => 'auto',\n\t\t\t\t'group'           => array(\n\t\t\t\t\t'order' => 20,\n\t\t\t\t\t'name'  => __( 'Other', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'            => 'question-circle',\n\t\t\t\t'id'              => 'generic',\n\t\t\t\t'image'           => true,\n\t\t\t\t'name'            => esc_html__( 'Question', 'lifterlms' ),\n\t\t\t\t'placeholder'     => esc_attr__( 'Enter your question...', 'lifterlms' ),\n\t\t\t\t'points'          => true,\n\t\t\t\t'random_lock'     => false,\n\t\t\t\t'video'           => true,\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve all the default question types loaded by the LifterLMS core\n\t *\n\t * @return   array\n\t * @since    3.16.0\n\t * @version  3.27.0\n\t */\n\tprivate static function get_types() {\n\n\t\t$upgrade_url = 'https://lifterlms.com/product/advanced-quizzes/?utm_source=LifterLMS%20Plugin&utm_medium=Quiz%20Builder%20Button&utm_campaign=Advanced%20Question%20Upsell&utm_content=3.16.0&utm_term=';\n\n\t\treturn array(\n\n\t\t\t'choice'          => array(\n\t\t\t\t'choices' => array(),\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 0,\n\t\t\t\t\t'name'  => __( 'Basic Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'check',\n\t\t\t\t'id'      => 'choice',\n\t\t\t\t'name'    => esc_html__( 'Multiple Choice', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'picture_choice'  => array(\n\t\t\t\t'choices' => array(\n\t\t\t\t\t'type' => 'image',\n\t\t\t\t),\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 0,\n\t\t\t\t\t'name'  => __( 'Basic Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'picture-o',\n\t\t\t\t'id'      => 'picture_choice',\n\t\t\t\t'name'    => esc_html__( 'Picture Choice', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'true_false'      => array(\n\t\t\t\t'choices'         => array(\n\t\t\t\t\t'max'   => 2,\n\t\t\t\t\t'min'   => 2,\n\t\t\t\t\t'multi' => false,\n\t\t\t\t),\n\t\t\t\t'default_choices' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'choice'  => esc_html__( 'True', 'lifterlms' ),\n\t\t\t\t\t\t'correct' => true,\n\t\t\t\t\t\t'marker'  => 'A',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'choice' => esc_html__( 'False', 'lifterlms' ),\n\t\t\t\t\t\t'marker' => 'B',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'group'           => array(\n\t\t\t\t\t'order' => 0,\n\t\t\t\t\t'name'  => __( 'Basic Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'            => 'toggle-on',\n\t\t\t\t'id'              => 'true_false',\n\t\t\t\t'name'            => esc_html__( 'True or False', 'lifterlms' ),\n\t\t\t),\n\n\t\t\t'content'         => array(\n\t\t\t\t'choices'        => false,\n\t\t\t\t'clarifications' => false,\n\t\t\t\t'icon'           => 'window-maximize',\n\t\t\t\t'id'             => 'content',\n\t\t\t\t'grading'        => false,\n\t\t\t\t'name'           => esc_html__( 'Content', 'lifterlms' ),\n\t\t\t\t'placeholder'    => esc_attr__( 'Enter your content title...', 'lifterlms' ),\n\t\t\t\t'points'         => false,\n\t\t\t\t'random_lock'    => true,\n\t\t\t),\n\n\t\t\t'existing'        => array(\n\t\t\t\t'choices'        => false,\n\t\t\t\t'clarifications' => false,\n\t\t\t\t'icon'           => 'file-text-o',\n\t\t\t\t'id'             => 'existing',\n\t\t\t\t'grading'        => false,\n\t\t\t\t'name'           => esc_html__( 'Add Existing Question', 'lifterlms' ),\n\t\t\t\t'placeholder'    => '',\n\t\t\t\t'points'         => false,\n\t\t\t\t'random_lock'    => true,\n\t\t\t),\n\n\t\t\t// 'group' => array(\n\t\t\t// 'choices' => false,\n\t\t\t// 'clarifications' => false,\n\t\t\t// 'group' => array(\n\t\t\t// 'order' => 0,\n\t\t\t// 'name' => __( 'Basic Questions', 'lifterlms' )\n\t\t\t// ),\n\t\t\t// 'icon' => 'sitemap',\n\t\t\t// 'id' => 'group',\n\t\t\t// 'grading' => false,\n\t\t\t// 'name' => esc_html__( 'Question Group', 'lifterlms' ),\n\t\t\t// 'placeholder' => esc_attr__( 'Enter your group title...', 'lifterlms' ),\n\t\t\t// ),\n\n\t\t\t'blank'           => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'window-minimize',\n\t\t\t\t'id'      => 'blank',\n\t\t\t\t'name'    => esc_html__( 'Fill in the Blank', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'blank',\n\t\t\t),\n\n\t\t\t'reorder'         => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'sort-numeric-asc',\n\t\t\t\t'id'      => 'reorder',\n\t\t\t\t'name'    => esc_html__( 'Reorder Items', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'reorder',\n\t\t\t),\n\n\t\t\t'picture_reorder' => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'picture-o',\n\t\t\t\t'id'      => 'picture_reorder',\n\t\t\t\t'name'    => esc_html__( 'Reorder Pictures', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'picture_reorder',\n\t\t\t),\n\n\t\t\t'short_answer'    => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'align-left',\n\t\t\t\t'id'      => 'short_answer',\n\t\t\t\t'name'    => esc_html__( 'Short Answer', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'short_answer',\n\t\t\t),\n\n\t\t\t'long_answer'     => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'paragraph',\n\t\t\t\t'id'      => 'long_answer',\n\t\t\t\t'name'    => esc_html__( 'Long Answer', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'long_answer',\n\t\t\t),\n\n\t\t\t'upload'          => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'cloud-upload',\n\t\t\t\t'id'      => 'upload',\n\t\t\t\t'name'    => esc_html__( 'File Upload', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'upload',\n\t\t\t),\n\n\t\t\t'code'            => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'code',\n\t\t\t\t'id'      => 'code',\n\t\t\t\t'name'    => esc_html__( 'Code', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'code',\n\t\t\t),\n\n\t\t\t'scale'           => array(\n\t\t\t\t'choices' => false,\n\t\t\t\t'group'   => array(\n\t\t\t\t\t'order' => 10,\n\t\t\t\t\t'name'  => __( 'Advanced Questions', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'icon'    => 'sliders',\n\t\t\t\t'id'      => 'scale',\n\t\t\t\t'name'    => esc_html__( 'Scale', 'lifterlms' ),\n\t\t\t\t'upgrade' => $upgrade_url . 'scale',\n\t\t\t),\n\n\t\t);\n\n\t}\n\n\t/**\n\t * Load core question types\n\t *\n\t * @param    array $questions  array of question types (probably empty).\n\t * @return   void\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic static function load( $questions ) {\n\n\t\t$model = self::get_model();\n\n\t\tforeach ( self::get_types() as $id => $type ) {\n\n\t\t\tif ( is_array( $type['choices'] ) ) {\n\t\t\t\t$type['choices'] = wp_parse_args( $type['choices'], $model['choices'] );\n\t\t\t}\n\t\t\t$questions[ $id ] = wp_parse_args( $type, $model );\n\n\t\t}\n\n\t\treturn $questions;\n\n\t}\n\n}\n\nLLMS_Question_Types::init();\n"
  },
  {
    "path": "includes/class.llms.quiz.data.php",
    "content": "<?php\n/**\n * Query data about a quiz\n *\n * @package LifterLMS/Classes\n *\n * @since 3.16.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Query data about a quiz.\n *\n * @since 3.16.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 3.31.0 Extends LLMS_Abstract_Post_Data.\n * @since 4.0.0 Removed deprecated properties `$quiz` and `$quiz_id`.\n */\nclass LLMS_Quiz_Data extends LLMS_Abstract_Post_Data {\n\n\t/**\n\t * Post ID of the quiz\n\t *\n\t * @var int\n\t */\n\tprotected $quiz_id;\n\n\t/**\n\t * Post object of the quiz\n\t */\n\tprotected $quiz;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.16.0\n\t *\n\t * @param    int $quiz_id  WP Post ID of the quiz\n\t */\n\tpublic function __construct( $quiz_id ) {\n\n\t\t$this->quiz_id = $quiz_id;\n\t\t$this->quiz    = llms_get_post( $this->quiz_id );\n\t\tparent::__construct( $quiz_id );\n\t}\n\n\t/**\n\t * Retrieve # of quiz attempts within the period\n\t *\n\t * @since    3.16.0\n\t *\n\t * @param    string $period  date period [current|previous]\n\t * @return   int\n\t */\n\tpublic function get_attempt_count( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT COUNT( id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_quiz_attempts\n\t\t\tWHERE quiz_id = %d\n\t\t\t  AND update_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve avg grade of quiz attempts within the period\n\t *\n\t * @since    3.16.0\n\t *\n\t * @param    string $period  date period [current|previous]\n\t * @return   int\n\t */\n\tpublic function get_average_grade( $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$grade = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT ROUND( AVG( grade ), 3 )\n\t\t\tFROM {$wpdb->prefix}lifterlms_quiz_attempts\n\t\t\tWHERE quiz_id = %d\n\t\t\t  AND update_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);\n\n\t\treturn $grade ? $grade : 0;\n\t}\n\n\t/**\n\t * Retrieve the number assignments with a given status\n\t *\n\t * @since    3.24.0\n\t *\n\t * @param    string $status  status name\n\t * @param    string $period  date period [current|previous]\n\t * @return   int\n\t */\n\tpublic function get_count_by_status( $status, $period = 'current' ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\tSELECT COUNT( id )\n\t\t\tFROM {$wpdb->prefix}lifterlms_quiz_attempts\n\t\t\tWHERE quiz_id = %d\n\t\t\t  AND status = %s\n\t\t\t  AND update_date BETWEEN %s AND %s\n\t\t\t\",\n\t\t\t\t$this->post_id,\n\t\t\t\t$status,\n\t\t\t\t$this->get_date( $period, 'start' ),\n\t\t\t\t$this->get_date( $period, 'end' )\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve # of quiz fails within the period.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $period Date period [current|previous].\n\t * @return int\n\t */\n\tpublic function get_fail_count( $period = 'current' ) {\n\t\treturn $this->get_count_by_status( 'fail', $period );\n\t}\n\n\t/**\n\t * Retrieve # of quiz passes within the period.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $period Date period [current|previous].\n\t * @return int\n\t */\n\tpublic function get_pass_count( $period = 'current' ) {\n\t\treturn $this->get_count_by_status( 'pass', $period );\n\t}\n\n\t/**\n\t * Retrieve recent LLMS_User_Postmeta for the quiz.\n\t * This overrides the LLMS_Abstract_Post_Data method.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return array\n\t */\n\tpublic function recent_events( $args = array() ) {\n\n\t\t$query_args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'types' => array(),\n\t\t\t)\n\t\t);\n\n\t\treturn parent::recent_events( $query_args );\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.review.php",
    "content": "<?php\n/**\n * LifterLMS Course reviews\n *\n * This class handles the front end of the reviews. It is responsible\n * for outputting the HTML on the course page (if reviews are activated).\n *\n * @package LifterLMS/Classes\n *\n * @since 1.2.7\n * @version 7.1.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Reviews class\n *\n * @since 1.2.7\n */\nclass LLMS_Reviews {\n\n\t/**\n\t * This is the constructor for this class.\n\t *\n\t * It takes care of attaching the functions in this file to the\n\t * appropriate actions.\n\t * These actions are:\n\t * 1) Output after course info.\n\t * 2) Output after membership info.\n\t * 3 & 4) Add function call to the proper AJAX call.\n\t *\n\t * @since 3.1.3\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'wp_ajax_LLMSSubmitReview', array( $this, 'process_review' ) );\n\t\tadd_action( 'wp_ajax_nopriv_LLMSSubmitReview', array( $this, 'process_review' ) );\n\t}\n\n\t/**\n\t * This function handles the HTML output of the reviews and review form.\n\t * If the option is enabled, the review form will be output,\n\t * if not, nothing will happen. This function also checks to\n\t * see if a user is allowed to review more than once.\n\t *\n\t * @since 1.2.7\n\t * @since 3.24.0 Unknown.\n\t * @since 7.1.3 Improve inline styles, escape output.\n\t *\n\t * @return void\n\t */\n\tpublic static function output() {\n\n\t\t/**\n\t\t * Check to see if we are supposed to output the code at all.\n\t\t */\n\t\tif ( get_post_meta( get_the_ID(), '_llms_display_reviews', true ) ) {\n\n\t\t\t/**\n\t\t\t * Filters the reviews section title.\n\t\t\t *\n\t\t\t * @since 1.2.7\n\t\t\t *\n\t\t\t * @param string $section_title The section title.\n\t\t\t */\n\t\t\t$section_title = apply_filters( 'lifterlms_reviews_section_title', __( 'What Others Have Said', 'lifterlms' ) );\n\n\t\t\t?>\n\t\t\t<div id=\"old_reviews\">\n\t\t\t\t<h3><?php echo esc_html( $section_title ); ?></h3>\n\t\t\t\t<?php\n\t\t\t\t$args = array(\n\t\t\t\t\t'posts_per_page'   => get_post_meta( get_the_ID(), '_llms_num_reviews', true ),\n\t\t\t\t\t'post_type'        => 'llms_review',\n\t\t\t\t\t'post_status'      => 'publish',\n\t\t\t\t\t'post_parent'      => get_the_ID(),\n\t\t\t\t\t'suppress_filters' => true,\n\t\t\t\t);\n\n\t\t\t\t$posts_array = get_posts( $args );\n\n\t\t\t\t/**\n\t\t\t\t * Allow review custom styles to be filtered.\n\t\t\t\t *\n\t\t\t\t * @since 1.2.7\n\t\t\t\t *\n\t\t\t\t * @param array $styles Array of custom styles.\n\t\t\t\t */\n\t\t\t\t$styles = apply_filters(\n\t\t\t\t\t'llms_review_custom_styles',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'background-color' => '#efefef',\n\t\t\t\t\t\t'title-color'      => 'inherit',\n\t\t\t\t\t\t'text-color'       => 'inherit',\n\t\t\t\t\t\t'custom-css'       => '',\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$inline_styles = '';\n\n\t\t\t\tif ( $styles['background-color'] ?? '' ) {\n\t\t\t\t\t$inline_styles .= '.llms_review{background-color:' . $styles['background-color'] . '}';\n\t\t\t\t}\n\n\t\t\t\tif ( $styles['title-color'] ?? '' ) {\n\t\t\t\t\t$inline_styles .= '.llms_review h5{color:' . $styles['title-color'] . '}';\n\t\t\t\t}\n\n\t\t\t\tif ( $styles['text-color'] ?? '' ) {\n\t\t\t\t\t$inline_styles .= '.llms_review h6,.llms_review p{color:' . $styles['text-color'] . '}';\n\t\t\t\t}\n\n\t\t\t\tif ( $styles['custom-css'] ?? '' ) {\n\n\t\t\t\t\t// Remove style tags in case they were added with the filter.\n\t\t\t\t\t$inline_styles .= str_replace( array( '<style>', '</style>' ), '', $styles['custom-css'] );\n\t\t\t\t}\n\n\t\t\t\tif ( $inline_styles ) {\n\t\t\t\t\techo '<style id=\"llms_review_custom_styles\">' . esc_html( $inline_styles ) . '</style>';\n\t\t\t\t}\n\n\t\t\t\tforeach ( $posts_array as $post ) {\n\t\t\t\t\t?>\n\t\t\t\t\t<div class=\"llms_review\">\n\t\t\t\t\t\t<div class=\"llms_review_title\"><strong><?php echo esc_html( get_the_title( $post->ID ) ); ?></strong></div>\n\t\t\t\t\t\t<div class=\"llms_review_name\">\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t// Translators: %s = The author display name.\n\t\t\t\t\t\t\techo esc_html( sprintf( __( 'By: %s', 'lifterlms' ), get_the_author_meta( 'display_name', get_post_field( 'post_author', $post->ID ) ) ) );\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<p><?php echo esc_html( get_post_field( 'post_content', $post->ID ) ); ?></p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<?php\n\t\t\t\t}\n\t\t\t\t?>\n\t\t\t\t<hr>\n\t\t\t</div>\n\t\t\t<?php\n\t\t}\n\n\t\t/**\n\t\t * Check to see if reviews are open.\n\t\t */\n\t\tif ( get_post_meta( get_the_ID(), '_llms_reviews_enabled', true ) && is_user_logged_in() ) {\n\n\t\t\t/**\n\t\t\t * Filters the thank you text.\n\t\t\t *\n\t\t\t * @since 1.2.7\n\t\t\t *\n\t\t\t * @param string $thank_you_text The thank you text.\n\t\t\t */\n\t\t\t$thank_you_text = apply_filters( 'llms_review_thank_you_text', __( 'Thank you for your review!', 'lifterlms' ) );\n\n\t\t\tif ( ! self::current_user_can_write_review( get_the_ID() ) ) {\n\t\t\t\t?>\n\t\t\t\t<div id=\"thank_you_box\">\n\t\t\t\t\t<div><?php echo esc_html( $thank_you_text ); ?></div>\n\t\t\t\t</div>\n\t\t\t\t<?php\n\t\t\t} else {\n\t\t\t\t?>\n\t\t\t\t<div class=\"review_box\" id=\"review_box\">\n\t\t\t\t\t<h3><?php esc_html_e( 'Write a Review', 'lifterlms' ); ?></h3>\n\t\t\t\t\t<!--<form method=\"post\" name=\"review_form\" id=\"review_form\">-->\n\t\t\t\t\t<label for=\"review_title\">\n\t\t\t\t\t\t<?php esc_html_e( 'Review Title', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t\t<input type=\"text\" name=\"review_title\" id=\"review_title\">\n\t\t\t\t\t<div class=\"review_error\" id=\"review_title_error\"><?php esc_html_e( 'Review Title is required.', 'lifterlms' ); ?></div>\n\t\t\t\t\t<label for=\"review_text\">\n\t\t\t\t\t\t<?php esc_html_e( 'Review Text', 'lifterlms' ); ?>\n\t\t\t\t\t</label>\n\t\t\t\t\t<textarea name=\"review_text\" id=\"review_text\"></textarea>\n\t\t\t\t\t<div class=\"review_error\" id=\"review_text_error\"><?php esc_html_e( 'Review Text is required.', 'lifterlms' ); ?></div>\n\t\t\t\t\t<input name=\"action\" value=\"submit_review\" type=\"hidden\">\n\t\t\t\t\t<input name=\"post_ID\" value=\"<?php echo esc_attr( get_the_ID() ); ?>\" type=\"hidden\" id=\"post_ID\">\n\t\t\t\t\t<?php wp_nonce_field( 'llms-review', 'llms_review_nonce' ); ?>\n\t\t\t\t\t<input type=\"submit\" class=\"button\" value=\"<?php esc_attr_e( 'Leave Review', 'lifterlms' ); ?>\" id=\"llms_review_submit_button\">\n\t\t\t\t\t<!--</form>\t-->\n\t\t\t\t</div>\n\t\t\t\t<div class=\"thank_you_box\" id=\"thank_you_box\">\n\t\t\t\t\t<h2><?php echo esc_html( $thank_you_text ); ?></h2>\n\t\t\t\t</div>\n\t\t\t\t<?php\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * This function adds the review to the database. It is\n\t * called by the AJAX handler when the submit review button\n\t * is pressed. This function gathers the data from $_POST and\n\t * then adds the review with the appropriate content.\n\t *\n\t * @since 1.2.7\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.5.2 Now checking if the user can write a review.\n\t *\n\t * @return void\n\t */\n\tpublic function process_review() {\n\t\t// Check the nonce.\n\t\tif ( ! isset( $_POST['llms_review_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['llms_review_nonce'] ), 'llms-review' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$parent_id = llms_filter_input_sanitize_string( INPUT_POST, 'pageID' );\n\n\t\t// Make sure the current user can write reviews yet.\n\t\tif ( ! self::current_user_can_write_review( $parent_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$post = array(\n\t\t\t'post_content' => llms_filter_input_sanitize_string( INPUT_POST, 'review_text' ), // The full text of the post.\n\t\t\t'post_name'    => llms_filter_input_sanitize_string( INPUT_POST, 'review_title' ), // The name (slug) for your post.\n\t\t\t'post_title'   => llms_filter_input_sanitize_string( INPUT_POST, 'review_title' ), // The title of your post.\n\t\t\t'post_status'  => 'publish',\n\t\t\t'post_type'    => 'llms_review',\n\t\t\t'post_parent'  => $parent_id, // Sets the parent of the new post, if any. Default 0.\n\t\t\t'post_excerpt' => llms_filter_input_sanitize_string( INPUT_POST, 'review_title' ),\n\t\t);\n\n\t\t$result = wp_insert_post( $post, true );\n\t}\n\n\t/**\n\t * Check to see if we are allowed to write more than one review.\n\t * If we are not, check to see if we have written a review already.\n\t *\n\t * @since 7.5.2\n\t *\n\t * @param int $parent_id The ID of the parent post.\n\t * @return bool True if the user can write a review, false if not.\n\t */\n\tpublic static function current_user_can_write_review( $parent_id ) {\n\t\t// Make sure the user is logged in.\n\t\tif ( ! is_user_logged_in() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Make sure we have a post ID to check.\n\t\tif ( empty( $parent_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Check if reviews are disabled.\n\t\tif ( ! get_post_meta( $parent_id, '_llms_reviews_enabled', true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Check if reviews are limited and the user has already written a review.\n\t\t$args        = array(\n\t\t\t'posts_per_page'   => 1,\n\t\t\t'post_type'        => 'llms_review',\n\t\t\t'post_status'      => 'publish',\n\t\t\t'post_parent'      => $parent_id,\n\t\t\t'author'           => get_current_user_id(),\n\t\t\t'suppress_filters' => true,\n\t\t);\n\t\t$posts_array = get_posts( $args );\n\t\tif ( get_post_meta( $parent_id, '_llms_multiple_reviews_disabled', true ) && $posts_array ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// If we got here, we can write a review.\n\t\treturn true;\n\t}\n}\n\nreturn new LLMS_Reviews();\n"
  },
  {
    "path": "includes/class.llms.roles.php",
    "content": "<?php\n/**\n * LLMS_Roles class.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.13.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Custom Roles and Capabilities.\n *\n * @since 3.13.0\n */\nclass LLMS_Roles {\n\n\t/**\n\t * The capability name to manage earned engagament.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tconst MANAGE_EARNED_ENGAGEMENT_CAP = 'manage_earned_engagement';\n\n\t/**\n\t * Retrieve an array of all capabilities for a role\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param string $role Name of the role.\n\t * @return array\n\t */\n\tprivate static function get_all_caps( $role ) {\n\n\t\t$caps         = array();\n\t\t$caps['core'] = self::get_core_caps( $role );\n\t\t$caps['wp']   = self::get_wp_caps( $role );\n\t\t$caps         = array_merge( $caps, self::get_post_type_caps( $role ) );\n\n\t\treturn apply_filters( 'llms_get_all_' . $role . '_caps', $caps );\n\n\t}\n\n\t/**\n\t * Get an array of registered core lifterlms caps.\n\t *\n\t * @since 3.13.0\n\t * @since 3.14.0 Add the `lifterlms_instructor` capability.\n\t * @since 3.34.0 Added capabilities for student management.\n\t * @since 4.21.2 Added the `view_grades` capability.\n\t * @since 6.0.0 Added `manage_earned_engagement` capability.\n\t *\n\t * @link https://lifterlms.com/docs/roles-and-capabilities/\n\t *\n\t * @return string[]\n\t */\n\tpublic static function get_all_core_caps() {\n\n\t\t/**\n\t\t * Filters the list of available LifterLMS core user capabilities\n\t\t *\n\t\t * @since 3.13.0\n\t\t *\n\t\t * @param string[] $capabilities List of LifterLMS user capabilities.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_get_all_core_caps',\n\t\t\tarray(\n\t\t\t\t'lifterlms_instructor',\n\t\t\t\t'manage_lifterlms',\n\t\t\t\tself::MANAGE_EARNED_ENGAGEMENT_CAP,\n\t\t\t\t'view_lifterlms_reports',\n\t\t\t\t'view_others_lifterlms_reports',\n\t\t\t\t'enroll',\n\t\t\t\t'unenroll',\n\t\t\t\t'create_students',\n\t\t\t\t'view_grades',\n\t\t\t\t'view_students',\n\t\t\t\t'view_others_students',\n\t\t\t\t'edit_students',\n\t\t\t\t'edit_others_students',\n\t\t\t\t'delete_students',\n\t\t\t\t'delete_others_students',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the LifterLMS core capabilities for a give role\n\t *\n\t * @since 3.13.0\n\t * @since 3.34.0 Added student management capabilities.\n\t * @since 4.21.2 Added 'view_grades' to the list of instructor/assistant caps which are not automatically available.\n\t * @since 6.0.0 Added `manage_earned_engagement` to the list of instructor/assistant caps which are not automatically available.\n\t *\n\t * @param string $role Name of the role.\n\t * @return string[]\n\t */\n\tprivate static function get_core_caps( $role ) {\n\n\t\t$all_caps = array_fill_keys( array_values( self::get_all_core_caps() ), true );\n\n\t\tswitch ( $role ) {\n\n\t\t\tcase 'instructor':\n\t\t\tcase 'instructors_assistant':\n\t\t\t\t$caps = $all_caps;\n\t\t\t\tunset(\n\t\t\t\t\t$caps['enroll'],\n\t\t\t\t\t$caps['unenroll'],\n\t\t\t\t\t$caps['manage_lifterlms'],\n\t\t\t\t\t$caps[ self::MANAGE_EARNED_ENGAGEMENT_CAP ],\n\t\t\t\t\t$caps['view_others_lifterlms_reports'],\n\t\t\t\t\t$caps['create_students'],\n\t\t\t\t\t$caps['view_others_students'],\n\t\t\t\t\t$caps['edit_students'],\n\t\t\t\t\t$caps['edit_others_students'],\n\t\t\t\t\t$caps['delete_students'],\n\t\t\t\t\t$caps['delete_others_students'],\n\t\t\t\t\t$caps['view_grades']\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'administrator':\n\t\t\tcase 'lms_manager':\n\t\t\t\t$caps = $all_caps;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$caps = array();\n\n\t\t}\n\n\t\t/**\n\t\t * Filters the LifterLMS capabilities added to a LifterLMS user role.\n\t\t *\n\t\t * The dynamic portion of this hook `$role` refers to the user's role name.\n\t\t *\n\t\t * @since 4.21.2\n\t\t *\n\t\t * @param string[] $caps     List of capabilities provided to the role.\n\t\t * @param string[] $all_caps Full list of all LifterLMS user capabilities.\n\t\t */\n\t\treturn apply_filters( \"llms_get_{$role}_core_caps\", $caps, $all_caps );\n\n\t}\n\n\t/**\n\t * Retrieve the post type specific capabilities for a give role.\n\t *\n\t * @since 3.13.0\n\t * @since 4.21.2 Use strict comparisons for `in_array()`.\n\t *\n\t * @param string $role Name of the role\n\t * @return array\n\t */\n\tprivate static function get_post_type_caps( $role ) {\n\n\t\t$caps = array();\n\n\t\t// Students get nothing.\n\t\tif ( 'student' !== $role ) {\n\n\t\t\t$post_types = array(\n\t\t\t\t'course'          => 'course',\n\t\t\t\t'lesson'          => 'lesson',\n\t\t\t\t'llms_quiz'       => array( 'quiz', 'quizzes' ),\n\t\t\t\t'llms_question'   => 'question',\n\t\t\t\t'llms_membership' => 'membership',\n\t\t\t);\n\t\t\tforeach ( $post_types as $post_type => $names ) {\n\n\t\t\t\t$post_caps = LLMS_Post_Types::get_post_type_caps( $names );\n\n\t\t\t\t// Filter the caps down for these roles.\n\t\t\t\tif ( in_array( $role, array( 'instructor', 'instructors_assistant' ), true ) ) {\n\n\t\t\t\t\t$allowed = array(\n\t\t\t\t\t\t'instructor'            => array(\n\t\t\t\t\t\t\t'delete_posts',\n\t\t\t\t\t\t\t'delete_published_posts',\n\t\t\t\t\t\t\t'edit_post',\n\t\t\t\t\t\t\t'edit_posts',\n\t\t\t\t\t\t\t'edit_published_posts',\n\t\t\t\t\t\t\t'publish_posts',\n\t\t\t\t\t\t\t'create_posts',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'instructors_assistant' => array(\n\t\t\t\t\t\t\t'edit_post',\n\t\t\t\t\t\t\t'edit_posts',\n\t\t\t\t\t\t\t'edit_published_posts',\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\n\t\t\t\t\tforeach ( $post_caps as $post_cap => $cpt_cap ) {\n\n\t\t\t\t\t\tif ( ! in_array( $post_cap, $allowed[ $role ], true ) ) {\n\t\t\t\t\t\t\tunset( $post_caps[ $post_cap ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$caps[ $post_type ] = array_fill_keys( array_values( $post_caps ), true );\n\n\t\t\t}\n\n\t\t\t$taxes = array(\n\t\t\t\t'course_cat'        => 'course_cat',\n\t\t\t\t'course_difficulty' => array( 'course_difficulty', 'course_difficulties' ),\n\t\t\t\t'course_tag'        => 'course_tag',\n\t\t\t\t'course_track'      => 'course_track',\n\t\t\t\t'membership_cat'    => 'membership_cat',\n\t\t\t\t'membership_tag'    => 'membership_tag',\n\t\t\t);\n\t\t\tforeach ( $taxes as $tax => $names ) {\n\n\t\t\t\t$tax_caps = LLMS_Post_Types::get_tax_caps( $names );\n\n\t\t\t\t// Filter the caps down for these roles.\n\t\t\t\tif ( in_array( $role, array( 'instructor', 'instructors_assistant' ), true ) ) {\n\n\t\t\t\t\t$allowed = array(\n\t\t\t\t\t\t'assign_terms',\n\t\t\t\t\t);\n\n\t\t\t\t\tforeach ( $tax_caps as $tax_cap => $ct_cap ) {\n\n\t\t\t\t\t\tif ( ! in_array( $tax_cap, $allowed, true ) ) {\n\t\t\t\t\t\t\tunset( $tax_caps[ $tax_cap ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$caps[ $tax ] = array_fill_keys( array_values( $tax_caps ), true );\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_' . $role . '_post_type_caps', $caps );\n\n\t}\n\n\t/**\n\t * Retrieve the core WP capabilities for a give role\n\t *\n\t * @since 3.13.0\n\t * @since 3.34.0 Add the `list_users` capability to instructors.\n\t *\n\t * @param string $role Name of the role.\n\t * @return array\n\t */\n\tprivate static function get_wp_caps( $role ) {\n\n\t\t$caps = array(\n\t\t\t'read' => true,\n\t\t);\n\n\t\tswitch ( $role ) {\n\n\t\t\tcase 'instructor':\n\t\t\t\t$add = array(\n\t\t\t\t\t'create_users'  => true,\n\t\t\t\t\t'edit_users'    => true,\n\t\t\t\t\t'promote_users' => true,\n\t\t\t\t\t'list_users'    => true,\n\n\t\t\t\t\t'read'          => true,\n\t\t\t\t\t'upload_files'  => true,\n\n\t\t\t\t\t/**\n\t\t\t\t\t * See WP Core issue(s)\n\t\t\t\t\t *\n\t\t\t\t\t * @link https://core.trac.wordpress.org/ticket/22895\n\t\t\t\t\t * @link https://core.trac.wordpress.org/ticket/16808\n\t\t\t\t\t */\n\t\t\t\t\t'edit_posts'    => true,\n\t\t\t\t);\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'instructors_assistant':\n\t\t\t\t$add = array(\n\t\t\t\t\t'read'         => true,\n\t\t\t\t\t'upload_files' => true,\n\n\t\t\t\t\t/**\n\t\t\t\t\t * See WP Core issue(s)\n\t\t\t\t\t *\n\t\t\t\t\t * @link https://core.trac.wordpress.org/ticket/22895\n\t\t\t\t\t * @link https://core.trac.wordpress.org/ticket/16808\n\t\t\t\t\t */\n\t\t\t\t\t'edit_posts'   => true,\n\t\t\t\t);\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'lms_manager':\n\t\t\t\t$add = array(\n\t\t\t\t\t'read_private_pages'     => true,\n\t\t\t\t\t'read_private_posts'     => true,\n\t\t\t\t\t'edit_posts'             => true,\n\t\t\t\t\t'edit_pages'             => true,\n\t\t\t\t\t'edit_published_posts'   => true,\n\t\t\t\t\t'edit_published_pages'   => true,\n\t\t\t\t\t'edit_private_pages'     => true,\n\t\t\t\t\t'edit_private_posts'     => true,\n\t\t\t\t\t'edit_others_posts'      => true,\n\t\t\t\t\t'edit_others_pages'      => true,\n\t\t\t\t\t'publish_posts'          => true,\n\t\t\t\t\t'publish_pages'          => true,\n\t\t\t\t\t'delete_posts'           => true,\n\t\t\t\t\t'delete_pages'           => true,\n\t\t\t\t\t'delete_private_pages'   => true,\n\t\t\t\t\t'delete_private_posts'   => true,\n\t\t\t\t\t'delete_published_pages' => true,\n\t\t\t\t\t'delete_published_posts' => true,\n\t\t\t\t\t'delete_others_posts'    => true,\n\t\t\t\t\t'delete_others_pages'    => true,\n\t\t\t\t\t'manage_categories'      => true,\n\t\t\t\t\t'manage_links'           => true,\n\t\t\t\t\t'moderate_comments'      => true,\n\t\t\t\t\t'upload_files'           => true,\n\t\t\t\t\t'export'                 => true,\n\t\t\t\t\t'import'                 => true,\n\n\t\t\t\t\t'edit_users'             => true,\n\t\t\t\t\t'create_users'           => true,\n\t\t\t\t\t'list_users'             => true,\n\t\t\t\t\t'promote_users'          => true,\n\t\t\t\t\t'delete_users'           => true,\n\t\t\t\t);\n\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$add = array();\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_' . $role . '_wp_caps', array_merge( $add, $caps ) );\n\n\t}\n\n\t/**\n\t * Retrieve LifterLMS roles and role names\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_roles() {\n\n\t\treturn apply_filters(\n\t\t\t'llms_get_roles',\n\t\t\tarray(\n\t\t\t\t'lms_manager'           => __( 'LMS Manager', 'lifterlms' ),\n\t\t\t\t'instructor'            => __( 'Instructor', 'lifterlms' ),\n\t\t\t\t'instructors_assistant' => __( 'Instructor\\'s Assistant', 'lifterlms' ),\n\t\t\t\t'student'               => __( 'Student', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Install custom roles and related capabilities\n\t *\n\t * Called from LLMS_Install during installation and upgrades.\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return void\n\t */\n\tpublic static function install() {\n\n\t\tglobal $wp_roles;\n\n\t\tif ( ! class_exists( 'WP_Roles' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$roles                  = self::get_roles();\n\t\t$roles['administrator'] = __( 'Administrator', 'lifterlms' );\n\n\t\t$wp_roles = wp_roles();\n\n\t\tforeach ( $roles as $role => $name ) {\n\n\t\t\t$role_obj = $wp_roles->get_role( $role );\n\n\t\t\tif ( ! $role_obj ) {\n\t\t\t\t$role_obj = $wp_roles->add_role( $role, $name );\n\t\t\t}\n\n\t\t\tself::update_caps( $role_obj, 'add' );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Uninstall custom roles and remove custom caps from default WP roles\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return void\n\t */\n\tpublic static function remove_roles() {\n\n\t\tif ( ! class_exists( 'WP_Roles' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$wp_roles = wp_roles();\n\n\t\t// Delete all our custom roles.\n\t\tforeach ( array_keys( self::get_roles() ) as $role ) {\n\t\t\t$wp_roles->remove_role( $role );\n\t\t}\n\n\t\t// Remove custom caps from the WP core admin role.\n\t\tself::update_caps( $wp_roles->get_role( 'administrator' ), 'remove', array( 'wp' ) );\n\n\t}\n\n\t/**\n\t * Update the capabilities for a given role\n\t *\n\t * @since 3.13.0\n\t * @since 4.5.1 Added `$exclude_group` parameter that allows excluding groups of caps from the update.\n\t *\n\t * @param WP_Role  $role           Role object.\n\t * @param string   $type           Update type [add|remove].\n\t * @param string[] $exclude_groups Array of groups to exclude.\n\t * @return void\n\t */\n\tprivate static function update_caps( $role, $type = 'add', $exclude_groups = array() ) {\n\n\t\t$role_caps = self::get_all_caps( $role->name );\n\t\t$role_caps = empty( $exclude_groups ) ? $role_caps : array_diff_key( $role_caps, array_flip( $exclude_groups ) );\n\n\t\tforeach ( $role_caps as $group => $caps ) {\n\n\t\t\tforeach ( array_keys( $caps ) as $cap ) {\n\n\t\t\t\tif ( 'add' === $type ) {\n\t\t\t\t\t$role->add_cap( $cap );\n\t\t\t\t} elseif ( 'remove' === $type ) {\n\t\t\t\t\t$role->remove_cap( $cap );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Returns an array of role names.\n\t *\n\t * LLMS roles and WP core roles are translated.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_all_role_names() {\n\n\t\t$all_roles = wp_roles()->roles;\n\n\t\treturn array_merge(\n\t\t\tarray_combine(\n\t\t\t\tarray_keys( $all_roles ),\n\t\t\t\tarray_map(\n\t\t\t\t\t'translate_user_role', // Translates WP Core roles.\n\t\t\t\t\tarray_column( $all_roles, 'name' )\n\t\t\t\t)\n\t\t\t),\n\t\t\tself::get_roles() // So our roles are translated as well.\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.session.php",
    "content": "<?php\n/**\n * LLMS_Session.\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Session class.\n *\n * @since 1.0.0\n * @since 3.7.7 Unknown.\n * @since 3.37.7 Added a second parameter to the `get()` method, that represents the default value\n *               to return if the session variable requested doesn't exist.\n * @since 4.0.0 Major refactor to remove reliance on the wp-session-manager library:\n *               + Moved getters & setter methods into LLMS_Abstract_Session_Data\n *               + Added new methods to support built-in DB session management.\n *               + Deprecated legacy methods\n *               + Removed the ability to utilize PHP sessions.\n *               + Removed unused methods.\n */\nclass LLMS_Session extends LLMS_Abstract_Session_Database_Handler {\n\n\t/**\n\t * Session cookie name\n\t *\n\t * @var string\n\t */\n\tprotected $cookie = '';\n\n\t/**\n\t * Timestamp of the session's expiration\n\t *\n\t * @var int\n\t */\n\tprotected $expires;\n\n\t/**\n\t * Timestamp of when the session is nearing expiration\n\t *\n\t * @var int\n\t */\n\tprotected $expiring;\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.0.0\n\t * @since 3.7.5 Unknown.\n\t * @since 4.0.0 Removed PHP sessions.\n\t *               Added session auto-destroy on `wp_logout`.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t/**\n\t\t * Customize the name of the LifterLMS User Session Cookie\n\t\t *\n\t\t * @since 4.0.0\n\t\t *\n\t\t * @param string $name Default session cookie name.\n\t\t */\n\t\t$this->cookie = apply_filters( 'llms_session_cookie_name', sprintf( 'wp_llms_session_%s', COOKIEHASH ) );\n\n\t\t/**\n\t\t * Trigger cleanup via action.\n\t\t *\n\t\t * This is hooked to an hourly scheduled task.\n\t\t */\n\t\tadd_action( 'llms_delete_expired_session_data', array( $this, 'clean' ) );\n\n\t\tif ( $this->should_init() ) {\n\n\t\t\t$this->init_cookie();\n\n\t\t\tadd_action( 'wp_logout', array( $this, 'destroy' ) );\n\t\t\tadd_action( 'shutdown', array( $this, 'maybe_save_data' ), 20 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Destroys the current session\n\t *\n\t * Removes session data from the database, expires the cookie,\n\t * and resets class variables.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function destroy() {\n\n\t\t// Delete from DB.\n\t\t$this->delete( $this->get_id() );\n\n\t\t// Reset class vars.\n\t\t$this->id       = '';\n\t\t$this->data     = array();\n\t\t$this->is_clean = true;\n\n\t\t// Destroy the cookie.\n\t\treturn llms_setcookie( $this->cookie, '', time() - YEAR_IN_SECONDS, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $this->use_secure_cookie(), true );\n\n\t}\n\n\t/**\n\t * Retrieve an validate the session cookie\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return false|mixed[]\n\t */\n\tprotected function get_cookie() {\n\n\t\t$value = isset( $_COOKIE[ $this->cookie ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ $this->cookie ] ) ) : false;\n\n\t\tif ( empty( $value ) || ! is_string( $value ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t/**\n\t\t * Explode the cookie into it's parts.\n\t\t *\n\t\t * @param string|int $0 User ID.\n\t\t * @param int        $1 Expiration timestamp.\n\t\t * @param int        $2 Expiration variance timestamp.\n\t\t * @param string     $3 Cookie hash.\n\t\t */\n\t\t$parts = explode( '||', $value );\n\n\t\tif ( empty( $parts[0] ) || empty( $parts[3] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$hash_str = sprintf( '%1$s|%2$s', $parts[0], $parts[1] );\n\t\t$expected = hash_hmac( 'md5', $hash_str, wp_hash( $hash_str ) );\n\n\t\tif ( ! hash_equals( $expected, $parts[3] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $parts;\n\n\t}\n\n\t/**\n\t * Initialize the session cookie\n\t *\n\t * Retrieves and validates the cookie,\n\t * when there's a valid cookie it will initialize the object\n\t * with data from the cookie. Otherwise it sets up and saves\n\t * a new session and cookie.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function init_cookie() {\n\n\t\t$cookie = $this->get_cookie();\n\n\t\t$set_cookie = false;\n\n\t\tif ( $cookie ) {\n\n\t\t\t$this->id       = $cookie[0];\n\t\t\t$this->expires  = $cookie[1];\n\t\t\t$this->expiring = $cookie[2];\n\t\t\t$this->data     = $this->read( $this->id );\n\n\t\t\t// If the user has logged in, update the session data.\n\t\t\t$update_id = $this->maybe_update_id();\n\n\t\t\t// If the session is nearing expiration, update the session.\n\t\t\t$extend_expiration = $this->maybe_extend_expiration();\n\n\t\t\t// If either of these two items are true, the cookie needs to be updated.\n\t\t\t$set_cookie = $update_id || $extend_expiration;\n\n\t\t} else {\n\n\t\t\t$this->id       = $this->generate_id();\n\t\t\t$this->data     = array();\n\t\t\t$this->is_clean = false;\n\t\t\t$set_cookie     = true;\n\t\t\t$this->set_expiration();\n\n\t\t}\n\n\t\tif ( $set_cookie ) {\n\t\t\t$this->set_cookie();\n\t\t}\n\n\t}\n\n\t/**\n\t * Extend the sessions expiration when the session is nearing expiration\n\t *\n\t * If the user is still active on the site and the cookie is older than the\n\t * \"expiring\" time but not yet expired, renew the session.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return boolean `true` if the expiration was extended, otherwise `false`.\n\t */\n\tprotected function maybe_extend_expiration() {\n\n\t\tif ( time() > $this->expiring ) {\n\t\t\t$this->set_expiration();\n\t\t\t$this->is_clean = false;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Save session data if not clean\n\t *\n\t * Callback for `shutdown` action hook.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function maybe_save_data() {\n\n\t\tif ( ! $this->is_clean ) {\n\t\t\treturn $this->save( $this->expires );\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Updates the session id when an anonymous visitor logs in.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return boolean `true` if the id was updated, otherwise `false`.\n\t */\n\tprotected function maybe_update_id() {\n\n\t\t$uid = strval( get_current_user_id() );\n\t\tif ( $uid && $uid !== $this->get_id() ) {\n\t\t\t$old_id         = $this->get_id();\n\t\t\t$this->id       = $uid;\n\t\t\t$this->is_clean = false;\n\t\t\t$this->delete( $old_id );\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Determines if the cookie and related save/destroy handler actions should be initialized\n\t *\n\t * When doing CRON or when on the admin panel we don't want to load, otherwise we do.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function should_init() {\n\n\t\t$init = ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( is_admin() && ! wp_doing_ajax() ) ? false : true;\n\n\t\t/**\n\t\t * Filter whether or not session cookies and related hooks are initialized\n\t\t *\n\t\t * @since 4.0.0\n\t\t *\n\t\t * @param boolean $init Whether or not initialization should take place.\n\t\t */\n\t\treturn apply_filters( 'llms_session_should_init', $init );\n\n\t}\n\n\t/**\n\t * Set the cookie\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function set_cookie() {\n\n\t\t$hash_str = sprintf( '%1$s|%2$s', $this->get_id(), $this->expires );\n\t\t$hash     = hash_hmac( 'md5', $hash_str, wp_hash( $hash_str ) );\n\t\t$value    = sprintf( '%1$s||%2$d||%3$d||%4$s', $this->get_id(), $this->expires, $this->expiring, $hash );\n\n\t\t// There's no cookie set or the existing cookie needs to be updated.\n\t\tif ( ! isset( $_COOKIE[ $this->cookie ] ) || $_COOKIE[ $this->cookie ] !== $value ) {\n\n\t\t\treturn llms_setcookie( $this->cookie, $value, $this->expires, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $this->use_secure_cookie(), true );\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Set cookie expiration and expiring timestamps\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function set_expiration() {\n\n\t\t/**\n\t\t * Filter the lifespan of user session data\n\t\t *\n\t\t * @since 4.0.0\n\t\t *\n\t\t * @param int $duration Lifespan of session data, in seconds.\n\t\t */\n\t\t$duration = (int) apply_filters( 'llms_session_data_expiration_duration', HOUR_IN_SECONDS * 6 );\n\n\t\t/**\n\t\t * Filter the user session lifespan variance\n\t\t *\n\t\t * This is subtracted from the session cookie expiration to determine it's \"expiring\" timestamp.\n\t\t *\n\t\t * When an active session passes it's expiring timestamp but has not yet passed it's expiration timestamp\n\t\t * the session data will be extended and the data session will not be destroyed.\n\t\t *\n\t\t * @since 4.0.0\n\t\t *\n\t\t * @param int $duration Lifespan of session data, in seconds.\n\t\t */\n\t\t$variance = (int) apply_filters( 'llms_session_data_expiration_variance', HOUR_IN_SECONDS );\n\n\t\t$this->expires  = time() + $duration;\n\t\t$this->expiring = $this->expires - $variance;\n\n\t}\n\n\t/**\n\t * Determine if a secure cookie should be used.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return boolean\n\t */\n\tprotected function use_secure_cookie() {\n\n\t\t$secure = llms_is_site_https() && is_ssl();\n\n\t\t/**\n\t\t * Determine whether or not a secure cookie should be used for user session data\n\t\t *\n\t\t * @since 4.0.0\n\t\t *\n\t\t * @param boolean $secure Whether or not a secure cookie should be used.\n\t\t */\n\t\treturn apply_filters( 'llms_session_use_secure_cookie', $secure );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.sidebars.php",
    "content": "<?php\n/**\n * LifterLMS Sidebars\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 3.0.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Sidebars\n *\n * @since 3.0.0\n */\nclass LLMS_Sidebars {\n\n\t/**\n\t * Static Constructor\n\t *\n\t * @since 3.0.0\n\t */\n\tpublic static function init() {\n\n\t\t// Replaces sidebars with course & lesson sidebars.\n\t\tadd_filter( 'sidebars_widgets', array( __CLASS__, 'replace_default_sidebars' ) );\n\n\t\t// Registers llms core sidebars.\n\t\tadd_action( 'widgets_init', array( __CLASS__, 'register_sidebars' ), 5 );\n\n\t\t// Custom actions for genesis.\n\t\tadd_action( 'genesis_init', array( __CLASS__, 'genesis_support' ) );\n\n\t}\n\n\t/**\n\t * Output course sidebar\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   void\n\t */\n\tpublic static function do_course_sidebar() {\n\t\tif ( is_active_sidebar( 'llms_course_widgets_side' ) ) {\n\t\t\tdynamic_sidebar( 'llms_course_widgets_side' );\n\t\t}\n\t}\n\n\t/**\n\t * Output lesson sidebar\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   void\n\t */\n\tpublic static function do_lesson_sidebar() {\n\t\tif ( is_active_sidebar( 'llms_lesson_widgets_side' ) ) {\n\t\t\tdynamic_sidebar( 'llms_lesson_widgets_side' );\n\t\t}\n\t}\n\n\t/**\n\t * Get the theme default sidebar that will be replaced by course and lesson sidebars\n\t *\n\t * @since 3.0.0\n\t * @since 3.0.1 Unknown.\n\t *\n\t * @return string\n\t */\n\tprivate static function get_theme_default_sidebar() {\n\n\t\t$theme = get_option( 'template' );\n\n\t\tswitch ( $theme ) {\n\n\t\t\tcase 'canvas':\n\t\t\t\t$id = 'primary';\n\t\t\t\tbreak;\n\n\t\t\tcase 'Divi':\n\t\t\tcase 'twentyeleven':\n\t\t\tcase 'twentyfifteen':\n\t\t\tcase 'twentyfourteen':\n\t\t\tcase 'twentyseventeen':\n\t\t\tcase 'twentysixteen':\n\t\t\tcase 'twentytwelve':\n\t\t\t\t$id = 'sidebar-1';\n\t\t\t\tbreak;\n\n\t\t\tcase 'twentythirteen':\n\t\t\t\t$id = 'sidebar-2';\n\t\t\t\tbreak;\n\n\t\t\tcase 'twentyten':\n\t\t\t\t$id = 'primary-widget-area';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$id = '';\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_theme_default_sidebar', $id, $theme );\n\n\t}\n\n\t/**\n\t * Custom static constructor that modifies methods for native genesis sidebar compatibility\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   void\n\t */\n\tpublic static function genesis_support() {\n\n\t\t// Remove default registration in favor of genesis registration methods.\n\t\tremove_action( 'widgets_init', array( __CLASS__, 'register_sidebars' ), 5 );\n\n\t\t// Add genesis registration method.\n\t\tadd_action( 'widgets_init', array( __CLASS__, 'genesis_register_sidebars' ), 5 );\n\n\t\t// Replace primary genesis sidebar with our course / lesson sidebar.\n\t\tadd_action( 'genesis_before_sidebar_widget_area', array( __CLASS__, 'genesis_do_sidebar' ) );\n\n\t\t// Genesis uses it's own reg method so we can send an empty array of settings.\n\t\tadd_filter( 'llms_sidebar_settings', '__return_empty_array' );\n\n\t}\n\n\t/**\n\t * Outputs llms sidebars in place of the default Genesis Primary Sidebar\n\t * Removes the default sidebar action and calls the respective output method\n\t * from this class instead\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   void\n\t */\n\tpublic static function genesis_do_sidebar() {\n\n\t\t$post_type = get_post_type();\n\n\t\tif ( in_array( $post_type, array( 'course', 'lesson' ) ) ) {\n\n\t\t\tremove_action( 'genesis_sidebar', 'genesis_do_sidebar' );\n\n\t\t\t$method = 'do_' . $post_type . '_sidebar';\n\n\t\t\tif ( method_exists( __CLASS__, $method ) ) {\n\t\t\t\tadd_action( 'genesis_sidebar', array( __CLASS__, $method ) );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Register LifterLMS Sidebars using genesis methods\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   void\n\t */\n\tpublic static function genesis_register_sidebars() {\n\n\t\t$sidebars = self::get_sidebars();\n\n\t\tforeach ( $sidebars as $sidebar ) {\n\n\t\t\tgenesis_register_sidebar( $sidebar );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Get a filtered array of sidebars to register\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   array\n\t */\n\tpublic static function get_sidebars() {\n\n\t\t$sidebars = array(\n\t\t\tapply_filters(\n\t\t\t\t'lifterlms_register_lesson_sidebar',\n\t\t\t\tarray(\n\t\t\t\t\t'id'          => 'llms_course_widgets_side',\n\t\t\t\t\t'description' => __( 'Widgets in this area will be shown on LifterLMS courses.', 'lifterlms' ),\n\t\t\t\t\t'name'        => __( 'Course Sidebar', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t),\n\t\t\tapply_filters(\n\t\t\t\t'lifterlms_register_course_sidebar',\n\t\t\t\tarray(\n\t\t\t\t\t'description' => __( 'Widgets in this area will be shown on LifterLMS lessons.', 'lifterlms' ),\n\t\t\t\t\t'id'          => 'llms_lesson_widgets_side',\n\t\t\t\t\t'name'        => __( 'Lesson Sidebar', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t),\n\t\t);\n\n\t\t$settings = apply_filters(\n\t\t\t'llms_sidebar_settings',\n\t\t\tarray(\n\t\t\t\t'before_widget' => '<li id=\"%1$s\" class=\"widget %2$s\">',\n\t\t\t\t'after_widget'  => '</li>',\n\t\t\t\t'before_title'  => '<h2 class=\"widgettitle\">',\n\t\t\t\t'after_title'   => '</h2>',\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $sidebars as &$s ) {\n\n\t\t\t$s = array_merge( $settings, $s );\n\n\t\t}\n\n\t\treturn $sidebars;\n\t}\n\n\t/**\n\t * Registers all sidebars\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   void\n\t */\n\tpublic static function register_sidebars() {\n\n\t\t$sidebars = self::get_sidebars();\n\n\t\tforeach ( $sidebars as $sidebar ) {\n\n\t\t\tregister_sidebar( $sidebar );\n\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Replaces existing sidebars with Course / Lesson sidebar widgets for supporting themes\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Unknown.\n\t *\n\t * @param    array $widgets    array of sidebars and their widgets\n\t * @return   array\n\t */\n\tpublic static function replace_default_sidebars( $widgets ) {\n\n\t\tif ( is_singular( 'course' ) || is_singular( 'lesson' ) ) {\n\n\t\t\t$sidebar_id = self::get_theme_default_sidebar();\n\n\t\t\tif ( $sidebar_id ) {\n\n\t\t\t\tif ( is_singular( 'course' ) && array_key_exists( 'llms_course_widgets_side', $widgets ) ) {\n\n\t\t\t\t\t$widgets[ $sidebar_id ] = $widgets['llms_course_widgets_side'];\n\n\t\t\t\t} elseif ( is_singular( 'lesson' ) && array_key_exists( 'llms_lesson_widgets_side', $widgets ) ) {\n\n\t\t\t\t\t$widgets[ $sidebar_id ] = $widgets['llms_lesson_widgets_side'];\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $widgets;\n\n\t}\n\n}\n\nLLMS_Sidebars::init();\n"
  },
  {
    "path": "includes/class.llms.site.php",
    "content": "<?php\n/**\n * LifterLMS Site Information.\n *\n * Handle Site switching to prevent recurring payment duplicates\n * when using stating sites\n *\n * Heavily inspired by WC Subscriptions. Thank you!\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Site class.\n *\n * @since 3.0.0\n */\nclass LLMS_Site {\n\n\t/**\n\t * String part used to encrypt and decrypt the lock url.\n\t *\n\t * @var string\n\t */\n\tpublic static $lock_string = '_[llms_site_url]_';\n\n\t/**\n\t * Clears the value of the lock URL\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function clear_lock_url() {\n\t\tupdate_option( 'llms_site_url', '' );\n\t}\n\n\t/**\n\t * Check if the site is cloned and not ignored\n\t *\n\t * @since 4.12.0\n\t * @since 4.13.0 Reverse the order of checks in the `if` statements for a minor performance improvement\n\t *               when the `LLMS_SITE_IS_CLONE` constant is being used.\n\t *\n\t * @return boolean Returns `true` when a clone is detected, otherwise `false`.\n\t */\n\tpublic static function check_status() {\n\n\t\tif ( self::is_clone() && ! self::is_clone_ignored() ) {\n\n\t\t\t/**\n\t\t\t * Action triggered when the current website is determined to be a \"cloned\" site\n\t\t\t *\n\t\t\t * @since 3.7.4\n\t\t\t * @since 4.12.0 Moved from LLMS_Admin_Notices_Core::check_staging().\n\t\t\t */\n\t\t\tdo_action( 'llms_site_clone_detected' );\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Get the lock url for the current site.\n\t *\n\t * Gets the WP site url and inserts the lock string into the (approximate) middle of the url.\n\t *\n\t * @since 3.0.0\n\t * @since 5.9.0 Pass an explicit integer to `substr_replace()`.\n\t *\n\t * @return string\n\t */\n\tpublic static function get_lock_url() {\n\t\t$site_url = get_site_url();\n\t\treturn substr_replace( $site_url, self::$lock_string, intval( strlen( $site_url ) / 2 ), 0 );\n\t}\n\n\t/**\n\t * Stores the current site's lock url into the database\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_lock_url() {\n\t\tupdate_option( 'llms_site_url', self::get_lock_url() );\n\t}\n\n\t/**\n\t * Gets the stored url and cleans it for comparisons\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic static function get_url() {\n\n\t\t$url = get_option( 'llms_site_url' );\n\n\t\t// Remove the lock string before returning it.\n\t\t$url = str_replace( self::$lock_string, '', $url );\n\n\t\t$url = set_url_scheme( $url );\n\n\t\t/**\n\t\t * Filters the stored LLMS_Site URL\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $url The cleaned LLMS_Site URL.\n\t\t */\n\t\treturn apply_filters( 'llms_site_get_url', $url );\n\n\t}\n\n\t/**\n\t * Get a single feature's status\n\t *\n\t * Checks for a feature constant first and, if none is defined,\n\t * uses the stored site setting (with a fallback to the default value), and\n\t * a final fallback to `false` if the feature cannot be found.\n\t *\n\t * @since 3.0.0\n\t * @since 4.12.0 Allow feature configuration via constants.\n\t *\n\t * @param string $feature Feature id/key.\n\t * @return bool\n\t */\n\tpublic static function get_feature( $feature ) {\n\n\t\t$status = self::get_feature_constant( $feature );\n\t\tif ( is_null( $status ) ) {\n\n\t\t\t$features = self::get_features();\n\t\t\t$status   = isset( $features[ $feature ] ) ? $features[ $feature ] : false;\n\n\t\t}\n\n\t\t/**\n\t\t * Filters the status of a LLMS_Site feature.\n\t\t *\n\t\t * @since 4.12.0\n\t\t *\n\t\t * @param boolean $status  Status of the feature.\n\t\t * @param string  $feature The feature ID/key.\n\t\t */\n\t\treturn apply_filters( 'llms_site_get_feature', $status, $feature );\n\n\t}\n\n\t/**\n\t * Retrieve a constant value for a site feature\n\t *\n\t * This allows site features to be explicitly enabled or disabled\n\t * in a wp-config.php file.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @param string $feature Feature id/key.\n\t * @return bool\n\t */\n\tprotected static function get_feature_constant( $feature ) {\n\n\t\t$constant = sprintf( 'LLMS_SITE_FEATURE_%s', strtoupper( $feature ) );\n\t\tif ( defined( $constant ) ) {\n\t\t\treturn constant( $constant );\n\t\t}\n\n\t\treturn null;\n\n\t}\n\n\t/**\n\t * Get a list of automated features\n\t *\n\t * These features are features that should be disabled\n\t * in testing or staging environments.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return array An associative array of site features.\n\t */\n\tpublic static function get_features() {\n\n\t\t/**\n\t\t * Filters the default values for LLMS_Site features\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param array $defaults An associative array of site features.\n\t\t */\n\t\t$defaults = apply_filters(\n\t\t\t'llms_site_default_features',\n\t\t\tarray(\n\t\t\t\t'recurring_payments' => true,\n\t\t\t)\n\t\t);\n\n\t\treturn get_option( 'llms_site_get_features', $defaults );\n\n\t}\n\n\t/**\n\t * Update the status of a specific feature and save it to the db\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $feature Name / key of the feature.\n\t * @param bool   $val     Status of the feature [true = enabled; false = disabled].\n\t * @return void\n\t */\n\tpublic static function update_feature( $feature, $val ) {\n\n\t\t$features             = self::get_features();\n\t\t$features[ $feature ] = $val;\n\t\tupdate_option( 'llms_site_get_features', $features );\n\n\t}\n\n\t/**\n\t * Determine if this is a cloned site\n\t *\n\t * Compares the stored (and cleaned) llms_site_url against the WP site url.\n\t *\n\t * @since 3.0.0\n\t * @since 4.13.0 Add `LLMS_SITE_IS_CLONE` constant check.\n\t *\n\t * @return boolean Returns `true` if it's a cloned site (urls do not match)\n\t *                 and `false` if it's not (urls DO match).\n\t */\n\tpublic static function is_clone() {\n\n\t\t$is_clone = defined( 'LLMS_SITE_IS_CLONE' ) ? LLMS_SITE_IS_CLONE : ( get_site_url() !== self::get_url() );\n\n\t\t/**\n\t\t * Filters whether or not the site is a \"cloned\" site\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param boolean $is_clone When `true` the site is considered a \"clone\", otherwise it is not.\n\t\t */\n\t\treturn apply_filters( 'llms_site_is_clone', $is_clone );\n\n\t}\n\n\t/**\n\t * Determines whether or not the clone warning notice has been ignored\n\t *\n\t * This prevents the warning from redisplaying when the site is a clone\n\t * and automatic payments remain disabled.\n\t *\n\t * @since 3.0.0\n\t * @since 4.12.0 Use `llms_parse_bool()` to determine check the option value.\n\t *\n\t * @return boolean\n\t */\n\tpublic static function is_clone_ignored() {\n\n\t\t/**\n\t\t * Filters whether or not the \"clone\" site has already been ignored.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param boolean $is_clone_ignored If `true`, the clone is ignored, otherwise it is not.\n\t\t */\n\t\treturn apply_filters( 'llms_site_is_clone_ignored', llms_parse_bool( get_option( 'llms_site_url_ignore', 'no' ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.student.dashboard.php",
    "content": "<?php\n/**\n * Retrieve data sets used by various other classes and functions\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Student_Dashboard class.\n *\n * @since 3.0.0\n * @since 3.28.2 Unknown.\n * @since 4.0.0 Removed deprecated methods.\n */\nclass LLMS_Student_Dashboard {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.0.0\n\t * @version  3.24.0\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'llms_get_endpoints', array( $this, 'add_endpoints' ) );\n\t\tadd_filter( 'lifterlms_student_dashboard_title', array( $this, 'modify_dashboard_title' ), 5 );\n\t\tadd_filter( 'rewrite_rules_array', array( $this, 'modify_rewrite_rules_order' ) );\n\n\t}\n\n\t/**\n\t * Add endpoints to the LLMS_Query class to be automatically registered\n\t *\n\t * @param    array $endpoints  updated array of endpoints\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function add_endpoints( $endpoints ) {\n\n\t\treturn array_merge( $endpoints, $this->get_endpoints() );\n\n\t}\n\n\t/**\n\t * Retrieve an array of all endpoint data for student dashboard endpoints\n\t *\n\t * @return   array\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function get_endpoints() {\n\n\t\t$endpoints = array();\n\t\tforeach ( self::get_tabs() as $var => $data ) {\n\n\t\t\tif ( empty( $data['endpoint'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$endpoints[ $var ] = $data['endpoint'];\n\n\t\t}\n\n\t\treturn $endpoints;\n\n\t}\n\n\t/**\n\t * Get list of student's courses used for recent courses on the dashboard\n\t * and all courses (paginated) on the \"View Courses\" endpoint\n\t *\n\t * @param    integer $limit  number of courses to return\n\t * @param    integer $skip   number of courses to skip (for pagination)\n\t * @return   array\n\t * @since    3.6.0\n\t * @version  3.6.0\n\t */\n\tprivate static function get_courses( $limit = 10, $skip = 0 ) {\n\n\t\t// Get sorting option.\n\t\t$option = get_option( 'lifterlms_myaccount_courses_in_progress_sorting', 'date,DESC' );\n\t\t// Parse to order & orderby.\n\t\t$option  = explode( ',', $option );\n\t\t$orderby = ! empty( $option[0] ) ? $option[0] : 'date';\n\t\t$order   = ! empty( $option[1] ) ? $option[1] : 'DESC';\n\n\t\t$student = new LLMS_Student();\n\t\treturn $student->get_courses(\n\t\t\tarray(\n\t\t\t\t'limit'   => $limit,\n\t\t\t\t'order'   => $order,\n\t\t\t\t'orderby' => $orderby,\n\t\t\t\t'skip'    => $skip,\n\t\t\t\t'status'  => 'enrolled',\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the current tab when on the student dashboard\n\t *\n\t * @param    string $return   type of return, either \"data\" for an array of data or 'slug' for just the slug\n\t * @return   mixed\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic static function get_current_tab( $return = 'data' ) {\n\n\t\tglobal $wp;\n\n\t\t// Set default tab.\n\t\t$current_tab = apply_filters( 'llms_student_dashboard_default_tab', 'dashboard' );\n\n\t\t$tabs = self::get_tabs();\n\n\t\tforeach ( $tabs as $var => $data ) {\n\t\t\tif ( isset( $wp->query_vars[ $var ] ) ) {\n\t\t\t\t$current_tab = $var;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif ( 'data' === $return ) {\n\t\t\treturn $tabs[ $current_tab ];\n\t\t} else {\n\t\t\treturn $current_tab;\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieve all dashboard tabs and related data\n\t *\n\t * @since 3.0.0\n\t * @since 3.28.2 Unknown.\n\t * @since 6.0.0 Add pagination to the view-achievements and view-certificates tabs.\n\t * @since 7.5.0 Add view-favorites tab.\n\t *\n\t * @return array\n\t */\n\tpublic static function get_tabs() {\n\n\t\t$tabs = array(\n\t\t\t'dashboard'         => array(\n\t\t\t\t'content'  => 'lifterlms_template_student_dashboard_home',\n\t\t\t\t'endpoint' => false,\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'Dashboard', 'lifterlms' ),\n\t\t\t\t'url'      => llms_get_page_url( 'myaccount' ),\n\t\t\t),\n\t\t\t'view-courses'      => array(\n\t\t\t\t'content'  => 'lifterlms_template_student_dashboard_my_courses',\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_courses_endpoint', 'my-courses' ),\n\t\t\t\t'paginate' => true,\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'My Courses', 'lifterlms' ),\n\t\t\t),\n\t\t\t'my-grades'         => array(\n\t\t\t\t'content'  => 'lifterlms_template_student_dashboard_my_grades',\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_grades_endpoint', 'my-grades' ),\n\t\t\t\t'paginate' => true,\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'My Grades', 'lifterlms' ),\n\t\t\t),\n\t\t\t'view-memberships'  => array(\n\t\t\t\t'content'  => 'lifterlms_template_student_dashboard_my_memberships',\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_memberships_endpoint', 'my-memberships' ),\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'My Memberships', 'lifterlms' ),\n\t\t\t),\n\t\t\t'view-achievements' => array(\n\t\t\t\t'content'  => 'lifterlms_template_student_dashboard_my_achievements',\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_achievements_endpoint', 'my-achievements' ),\n\t\t\t\t'paginate' => true,\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'My Achievements', 'lifterlms' ),\n\t\t\t),\n\t\t\t'view-certificates' => array(\n\t\t\t\t'content'  => 'lifterlms_template_student_dashboard_my_certificates',\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_certificates_endpoint', 'my-certificates' ),\n\t\t\t\t'paginate' => true,\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'My Certificates', 'lifterlms' ),\n\t\t\t),\n\t\t\t'notifications'     => array(\n\t\t\t\t'content'  => 'lifterlms_template_student_dashboard_my_notifications',\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_notifications_endpoint', 'notifications' ),\n\t\t\t\t'paginate' => true,\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'Notifications', 'lifterlms' ),\n\t\t\t),\n\t\t\t'edit-account'      => array(\n\t\t\t\t'content'  => array( __CLASS__, 'output_edit_account_content' ),\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_edit_account_endpoint', 'edit-account' ),\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'Edit Account', 'lifterlms' ),\n\t\t\t),\n\t\t\t'redeem-voucher'    => array(\n\t\t\t\t'content'  => array( __CLASS__, 'output_redeem_voucher_content' ),\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_redeem_vouchers_endpoint', 'redeem-voucher' ),\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'Redeem a Voucher', 'lifterlms' ),\n\t\t\t),\n\t\t\t'orders'            => array(\n\t\t\t\t'content'  => array( __CLASS__, 'output_orders_content' ),\n\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_orders_endpoint', 'orders' ),\n\t\t\t\t'nav_item' => true,\n\t\t\t\t'title'    => __( 'Order History', 'lifterlms' ),\n\t\t\t),\n\t\t\t'signout'           => array(\n\t\t\t\t'endpoint' => false,\n\t\t\t\t'title'    => __( 'Sign Out', 'lifterlms' ),\n\t\t\t\t'nav_item' => false,\n\t\t\t\t'url'      => wp_logout_url( llms_get_page_url( 'myaccount' ) ),\n\t\t\t),\n\t\t);\n\n\t\tif ( llms_is_favorites_enabled() ) {\n\t\t\t$tabs = llms_assoc_array_insert(\n\t\t\t\t$tabs,\n\t\t\t\t'view-certificates',\n\t\t\t\t'view-favorites',\n\t\t\t\tarray(\n\t\t\t\t\t'content'  => 'llms_template_student_dashboard_my_favorites',\n\t\t\t\t\t'endpoint' => get_option( 'lifterlms_myaccount_favorites_endpoint', 'my-favorites' ),\n\t\t\t\t\t'paginate' => true,\n\t\t\t\t\t'nav_item' => true,\n\t\t\t\t\t'title'    => __( 'My Favorites', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn apply_filters(\n\t\t\t'llms_get_student_dashboard_tabs',\n\t\t\t$tabs\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve dashboard tab data as required to display navigation links\n\t * Excludes any endpoint disabled by deleting the slug from account settings\n\t *\n\t * @return   array\n\t * @since    3.17.5\n\t * @version  3.17.5\n\t */\n\tpublic static function get_tabs_for_nav() {\n\n\t\t$tabs = array();\n\n\t\tforeach ( self::get_tabs() as $var => $data ) {\n\n\t\t\tif ( isset( $data['url'] ) ) {\n\t\t\t\t$url = $data['url'];\n\t\t\t} elseif ( ! empty( $data['endpoint'] ) ) {\n\t\t\t\t$url = llms_get_endpoint_url( $var, '', llms_get_page_url( 'myaccount' ) );\n\t\t\t} else {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$tabs[ $var ] = array(\n\t\t\t\t'url'   => $url,\n\t\t\t\t'title' => $data['title'],\n\t\t\t);\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_student_dashboard_tabs_for_nav', $tabs );\n\n\t}\n\n\t/**\n\t * Determine if an endpoint is disabled\n\t * If the custom endpoint option is an empty string (blank) the settings define the endpoint as disabled\n\t *\n\t * @param    string $endpoint  endpoint slug (eg: my-courses)\n\t * @return   bool\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tpublic static function is_endpoint_enabled( $endpoint ) {\n\n\t\t$tabs = self::get_tabs();\n\t\tif ( isset( $tabs[ $endpoint ] ) && ! empty( $tabs[ $endpoint ]['endpoint'] ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Handle modification of the default dashboard title for certain pages and sub pages\n\t *\n\t * @param    string $title  default title HTML\n\t * @return   string\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function modify_dashboard_title( $title ) {\n\n\t\tglobal $wp_query;\n\t\t$tab = self::get_current_tab( 'tab' );\n\n\t\tif ( 'my-grades' === $tab && ! empty( $wp_query->query['my-grades'] ) ) {\n\n\t\t\t$course = get_posts(\n\t\t\t\tarray(\n\t\t\t\t\t'name'      => $wp_query->query['my-grades'],\n\t\t\t\t\t'post_type' => 'course',\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$course = array_shift( $course );\n\t\t\tif ( $course ) {\n\n\t\t\t\t$data = self::get_current_tab();\n\n\t\t\t\t$new_title  = '<a href=\"' . esc_url( llms_get_endpoint_url( 'my-grades' ) ) . '\">' . $data['title'] . '</a>';\n\t\t\t\t$new_title .= sprintf( ' %1$s <a href=\"%2$s\">%3$s</a>', apply_filters( 'llms_student_dashboard_title_separator', '<small>&gt;</small>' ), get_permalink( $course->ID ), get_the_title( $course->ID ) );\n\n\t\t\t\t$title = str_replace( $data['title'], $new_title, $title );\n\n\t\t\t}\n\t\t}\n\n\t\treturn $title;\n\n\t}\n\n\tpublic function modify_rewrite_rules_order( $rules ) {\n\t\treturn $rules;\n\t}\n\n\t/**\n\t * Callback to output the edit account content\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic static function output_edit_account_content() {\n\t\tllms_get_template(\n\t\t\t'myaccount/form-edit-account.php',\n\t\t\tarray(\n\t\t\t\t'user' => get_user_by( 'id', get_current_user_id() ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Endpoint to output orders content\n\t *\n\t * @since 3.0.0\n\t * @since 3.8.0 Unknown.\n\t * @since 6.0.0 Use `llms_template_view_order()` in favor of including the template file directly.\n\t *\n\t * @return void\n\t */\n\tpublic static function output_orders_content() {\n\n\t\tglobal $wp;\n\n\t\t$args = array();\n\n\t\tif ( ! empty( $wp->query_vars['orders'] ) ) {\n\n\t\t\t$order = llms_get_post( $wp->query_vars['orders'] );\n\t\t\tllms_template_view_order( $order );\n\n\t\t} else {\n\n\t\t\t$student = new LLMS_Student();\n\t\t\tllms_get_template(\n\t\t\t\t'myaccount/my-orders.php',\n\t\t\t\tarray(\n\t\t\t\t\t'orders' => $student->get_orders(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'page' => isset( $_GET['opage'] ) ? intval( $_GET['opage'] ) : 1,\n\t\t\t\t\t\t)\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Callback to output content for the voucher endpoint\n\t *\n\t * @return   void\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic static function output_redeem_voucher_content() {\n\n\t\tllms_get_template(\n\t\t\t'myaccount/form-redeem-voucher.php',\n\t\t\tarray(\n\t\t\t\t'user' => get_user_by( 'id', get_current_user_id() ),\n\t\t\t)\n\t\t);\n\n\t}\n\n}\n\nreturn new LLMS_Student_Dashboard();\n"
  },
  {
    "path": "includes/class.llms.student.query.php",
    "content": "<?php\n/**\n * Query LifterLMS Students for a given course / membership\n *\n * @package LifterLMS/Classes\n *\n * @since 3.4.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Student_Query\n *\n * @since 3.4.0\n * @since 3.13.0 Unknown.\n */\nclass LLMS_Student_Query extends LLMS_Database_Query {\n\n\t/**\n\t * Identify the extending query\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'student';\n\n\t/**\n\t * Retrieve default arguments for a student query\n\t *\n\t * @since 3.4.0\n\t * @since 4.10.2 Drop usage of `this->get_filter( 'default_args' )` in favor of `'llms_student_query_default_args'`.\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_args() {\n\n\t\tglobal $post;\n\n\t\t$post_id = ! empty( $post->ID ) ? $post->ID : array();\n\n\t\t$args = array(\n\t\t\t'post_id'  => $post_id,\n\t\t\t'sort'     => array(\n\t\t\t\t'date'       => 'DESC',\n\t\t\t\t'status'     => 'ASC',\n\t\t\t\t'last_name'  => 'ASC',\n\t\t\t\t'first_name' => 'ASC',\n\t\t\t\t'id'         => 'ASC',\n\t\t\t),\n\t\t\t'statuses' => array_keys( llms_get_enrollment_statuses() ),\n\t\t);\n\n\t\t$args = wp_parse_args( $args, parent::get_default_args() );\n\n\t\t/**\n\t\t * Filters the student query default args\n\t\t *\n\t\t * @since 3.4.0\n\t\t *\n\t\t * @param array              $args          Array of default arguments to set up the query with.\n\t\t * @param LLMS_Student_Query $student_query Instance of LLMS_Student_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_student_query_default_args', $args, $this );\n\t}\n\n\t/**\n\t * Retrieve an array of LLMS_Students for the given set of students returned by the query\n\t *\n\t * @since 3.4.0\n\t * @since 3.8.0 Unknown.\n\t * @since 4.10.2 Drop usage of `this->get_filter( 'get_students' )` in favor of `'llms_student_query_get_students'`.\n\t *\n\t * @return array\n\t */\n\tpublic function get_students() {\n\n\t\t$students = array();\n\t\t$results  = $this->get_results();\n\n\t\tif ( $results ) {\n\n\t\t\tforeach ( $results as $result ) {\n\t\t\t\t$students[] = new LLMS_Student( $result->id );\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $students;\n\t\t}\n\n\t\t/**\n\t\t * Filters the list of students\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 4.10.2 Pass this query instance as second parameter.\n\t\t *\n\t\t * @param LLMS_Student[]     $students      Array of LLMS_Student instances.\n\t\t * @param LLMS_Student_Query $student_query Instance of LLMS_Student_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_student_query_get_students', $students, $this );\n\t}\n\n\t/**\n\t * Parses data passed to $statuses\n\t *\n\t * Convert strings to array and ensure resulting array contains only valid statuses\n\t * If no valid statuses, returns to the default.\n\t *\n\t * @since 3.4.0\n\t * @since 3.13.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tprotected function parse_args() {\n\n\t\t$statuses = $this->arguments['statuses'];\n\n\t\t// Allow strings to be submitted when only requesting one status.\n\t\tif ( is_string( $statuses ) ) {\n\t\t\t$statuses = array( $statuses );\n\t\t}\n\n\t\t// Ensure only valid statuses are used.\n\t\t$statuses = array_intersect( $statuses, array_keys( llms_get_enrollment_statuses() ) );\n\n\t\t// No statuses should return original default.\n\t\tif ( ! $statuses ) {\n\t\t\t$statuses = array_keys( llms_get_enrollment_statuses() );\n\t\t}\n\n\t\t$this->arguments['statuses'] = $statuses;\n\n\t\t// Allow numeric strings & ints to be passed instead of an array.\n\t\t$post_ids = $this->arguments['post_id'];\n\t\tif ( ! is_array( $post_ids ) && is_numeric( $post_ids ) && $post_ids > 0 ) {\n\t\t\t$post_ids = array( $post_ids );\n\t\t}\n\n\t\tforeach ( $post_ids as $key => &$id ) {\n\t\t\t$id = absint( $id ); // Verify we have ints.\n\t\t\tif ( $id <= 0 ) { // Remove anything negative or 0.\n\t\t\t\tunset( $post_ids[ $key ] );\n\t\t\t}\n\t\t}\n\t\t$this->arguments['post_id'] = $post_ids;\n\t}\n\n\t/**\n\t * Prepare the SQL for the query.\n\t *\n\t * @since 3.4.0\n\t * @since 3.13.0 Unknown.\n\t * @since 4.10.2 Demands to `$this->sql_select()` to determine whether or not `SQL_CALC_FOUND_ROWS` statement is needed.\n\t * @since 6.0.0 Renamed from `preprare_query()`.\n\t * @since 10.0.0 Build count_query from shared clauses instead of using SQL_CALC_FOUND_ROWS.\n\t *\n\t * @return string\n\t */\n\tprotected function prepare_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$search_vars = array();\n\n\t\tif ( $this->get( 'search' ) ) {\n\t\t\t$search = '%' . $wpdb->esc_like( $this->get( 'search' ) ) . '%';\n\t\t\t$search_vars[] = $search;\n\t\t\t$search_vars[] = $search;\n\t\t\t$search_vars[] = $search;\n\t\t}\n\n\t\t$base_query = \"SELECT {$this->sql_select()}\n\t\t\tFROM {$wpdb->users} AS u\n\t\t\t{$this->sql_joins()}\n\t\t\t{$this->sql_search()}\n\t\t\t{$this->sql_having()}\";\n\n\t\tif ( $this->get( 'count_only' ) ) {\n\t\t\t$sql_query = \"SELECT COUNT(*) AS total FROM ({$base_query}) as t;\";\n\n\t\t\tif ( empty( $search_vars ) ) {\n\t\t\t\treturn $sql_query;\n\t\t\t}\n\n\t\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\treturn $wpdb->prepare( $sql_query, $search_vars );\n\t\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t}\n\n\t\tif ( ! $this->get( 'no_found_rows' ) ) {\n\t\t\t$count_sql = \"SELECT COUNT(*) FROM ({$base_query}) as t\";\n\t\t\tif ( ! empty( $search_vars ) ) {\n\t\t\t\t// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber\n\t\t\t\t$this->count_query = $wpdb->prepare( $count_sql, $search_vars );\n\t\t\t} else {\n\t\t\t\t$this->count_query = $count_sql;\n\t\t\t}\n\t\t}\n\n\t\t$vars = $search_vars;\n\t\t$vars[] = $this->get_skip();\n\t\t$vars[] = $this->get( 'per_page' );\n\n\t\t$sql_query = \"{$base_query}\n\t\t\t{$this->sql_orderby()}\n\t\t\tLIMIT %d, %d;\";\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $vars is an array with the correct number of items.\n\t\t$sql = $wpdb->prepare(\n\t\t\t$sql_query,\n\t\t\t$vars\n\t\t);\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\treturn $sql;\n\t}\n\n\t/**\n\t * Determines if a field should be selected/joined based on searching and sorting arguments\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param string $field Field name/key.\n\t * @return bool\n\t */\n\tprivate function requires_field( $field ) {\n\n\t\t// Get the fields we're sorting by to see if we need to select them for the sorting.\n\t\t$sort_fields = array_keys( $this->get( 'sort' ) );\n\n\t\tif ( in_array( $field, $sort_fields ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( $this->get( 'search' ) ) {\n\n\t\t\t$search_fields = array( 'last_name', 'first_name', 'user_email' );\n\t\t\tif ( in_array( $field, $search_fields ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve prepared SQL for the HAVING clause\n\t *\n\t * @since 3.4.0\n\t * @since 3.13.0 Unknown.\n\t * @since 4.10.2 Drop usage of `this->get_filter( 'having' )` in favor of `'llms_student_query_having'`.\n\t *\n\t * @return string\n\t */\n\tprivate function sql_having() {\n\n\t\tglobal $wpdb;\n\n\t\t$sql = \"HAVING status IS NOT NULL AND {$this->sql_status_in()}\";\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query HAVING clause\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string             $sql           The HAVING clause of the query.\n\t\t * @param LLMS_Student_Query $student_query Instance of LLMS_Student_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_student_query_having', $sql, $this );\n\t}\n\n\t/**\n\t * Setup joins based on submitted sort and search args\n\t *\n\t * @since 3.13.0\n\t * @since 4.10.2 Drop usage of `this->get_filter( 'join' )` in favor of `'llms_student_query_join'`.\n\t *\n\t * @return string\n\t */\n\tprivate function sql_joins() {\n\n\t\tglobal $wpdb;\n\n\t\t$joins = array();\n\n\t\t$fields = array(\n\t\t\t'first_name'       => \"JOIN {$wpdb->usermeta} AS m_first ON u.ID = m_first.user_id AND m_first.meta_key = 'first_name'\",\n\t\t\t'last_name'        => \"JOIN {$wpdb->usermeta} AS m_last ON u.ID = m_last.user_id AND m_last.meta_key = 'last_name'\",\n\t\t\t'overall_progress' => \"JOIN {$wpdb->usermeta} AS m_o_p ON u.ID = m_o_p.user_id AND m_o_p.meta_key = 'llms_overall_progress'\",\n\t\t\t'overall_grade'    => \"JOIN {$wpdb->usermeta} AS m_o_g ON u.ID = m_o_g.user_id AND m_o_g.meta_key = 'llms_overall_grade'\",\n\t\t);\n\n\t\t// Add the fields to the array of fields to select.\n\t\tforeach ( $fields as $key => $statement ) {\n\t\t\tif ( $this->requires_field( $key ) ) {\n\t\t\t\t$joins[] = $statement;\n\t\t\t}\n\t\t}\n\n\t\t$sql = implode( ' ', $joins );\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query JOIN clause\n\t\t *\n\t\t * @since 3.13.0\n\t\t *\n\t\t * @param string             $sql           The JOIN clause of the query.\n\t\t * @param LLMS_Student_Query $student_query Instance of LLMS_Student_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_student_query_join', $sql, $this );\n\t}\n\n\t/**\n\t * Retrieve the prepared SEARCH query for the WHERE clause\n\t *\n\t * @since 3.4.0\n\t * @since 3.8.0 Unknown.\n\t * @since 4.10.2 Drop usage of `this->get_filter( 'search' )` in favor of `'llms_student_query_search'`.\n\t *\n\t * @return string\n\t */\n\tprivate function sql_search() {\n\n\t\t$sql = '';\n\n\t\tif ( $this->get( 'search' ) ) {\n\n\t\t\tglobal $wpdb;\n\t\t\t$sql .= '  AND (\n                   m_last.meta_value LIKE %s\n                OR m_first.meta_value LIKE %s\n                OR u.user_email LIKE %s\n            )';\n\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t/**\n\t\t * Filters the part of the SQL query that performs the search.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string             $sql           The SQL part that performs the search.\n\t\t * @param LLMS_Student_Query $student_query Instance of LLMS_Student_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_student_query_search', $sql, $this );\n\t}\n\n\t/**\n\t * Set up the SQL for the select statement.\n\t *\n\t * @since 3.13.0\n\t * @since 4.10.2 Drop usage of `this->get_filter( 'select' )` in favor of `'llms_student_query_select'`.\n\t *               Use `$this->sql_select_columns({columns})` to determine additional columns to select.\n\t * @since 5.10.0 Add a subquery for completed date.\n\t *\n\t * @return string\n\t */\n\tprivate function sql_select() {\n\n\t\t$selects = array();\n\n\t\t// Always select the ID.\n\t\t$selects[] = 'u.ID as id';\n\n\t\t// Always add the subqueries for enrollment status.\n\t\t$selects[] = \"( {$this->sql_subquery( 'meta_value' )} ) AS status\";\n\n\t\t// All the possible fields.\n\t\t$fields = array(\n\t\t\t'completed'        => \"( {$this->sql_subquery( 'updated_date', '_is_complete' )} ) AS completed\",\n\t\t\t'date'             => \"( {$this->sql_subquery( 'updated_date' )} ) AS `date`\",\n\t\t\t'last_name'        => 'm_last.meta_value AS last_name',\n\t\t\t'first_name'       => 'm_first.meta_value AS first_name',\n\t\t\t'email'            => 'u.user_email AS email',\n\t\t\t'registered'       => 'u.user_registered AS registered',\n\t\t\t'overall_progress' => 'CAST( m_o_p.meta_value AS decimal( 5, 2 ) ) AS overall_progress',\n\t\t\t'overall_grade'    => 'CAST( m_o_g.meta_value AS decimal( 5, 2 ) ) AS overall_grade',\n\t\t);\n\n\t\t// Add the fields to the array of fields to select.\n\t\tforeach ( $fields as $key => $statement ) {\n\t\t\tif ( $this->requires_field( $key ) ) {\n\t\t\t\t$selects[] = $statement;\n\t\t\t}\n\t\t}\n\n\t\t$sql = implode( ', ', $selects );\n\t\t$sql = $this->sql_select_columns( $sql );\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query SELECT clause\n\t\t *\n\t\t * @since 3.13.0\n\t\t *\n\t\t * @param string             $sql           The SELECT clause of the query.\n\t\t * @param LLMS_Student_Query $student_query Instance of LLMS_Student_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_student_query_select', $sql, $this );\n\t}\n\n\t/**\n\t * Generate an SQL IN clause based on submitted status arguments\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param string $column Name of the column.\n\t * @return string\n\t */\n\tprivate function sql_status_in( $column = 'status' ) {\n\t\tglobal $wpdb;\n\t\t$comma    = false;\n\t\t$statuses = array();\n\t\t$sql      = '';\n\t\tforeach ( $this->get( 'statuses' ) as $status ) {\n\t\t\t$sql       .= $comma ? ',%s' : '%s';\n\t\t\t$statuses[] = $status;\n\t\t\t$comma      = true;\n\t\t}\n\n\t\t$sql = $wpdb->prepare( $sql, $statuses ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared\n\t\treturn \"{$column} IN ( {$sql} )\";\n\t}\n\n\t/**\n\t * Generate an SQL subquery for the meta key in the main query.\n\t *\n\t * @since 3.13.0\n\t * @since 5.10.0 Add `$meta_key` argument.\n\t *\n\t * @param string $column   Column name.\n\t * @param string $meta_key Optional meta key to use in the WHERE condition. Defaults to '_status'.\n\t * @return string\n\t */\n\tprivate function sql_subquery( $column, $meta_key = '_status' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$post_ids = $this->get( 'post_id' );\n\t\tif ( $post_ids ) {\n\t\t\t$post_ids = implode( ',', $post_ids );\n\t\t\t$and      = \"AND post_id IN ( {$post_ids} )\";\n\t\t} else {\n\t\t\t$and = \"AND {$this->sql_status_in( 'meta_value' )}\";\n\t\t}\n\n\t\treturn \"SELECT {$column}\n\t\t\t\tFROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\t\tWHERE meta_key = '{$meta_key}'\n\t\t  \t\t  AND user_id = id\n\t\t  \t\t  {$and}\n\t\t\t\tORDER BY updated_date DESC\n\t\t\t\tLIMIT 1\";\n\t}\n}\n"
  },
  {
    "path": "includes/class.llms.template.loader.php",
    "content": "<?php\n/**\n * Template loader\n *\n * @package LifterLMS/Classes\n *\n * @since 1.0.0\n * @version 7.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Template loader class.\n *\n * @since 1.0.0\n * @since 3.20.0 Unknown.\n * @since 3.37.2 Notices are printed on sales pages too.\n * @since 3.37.10 Notices are printed on pages configured as a membership restriction redirect page.\n * @since 3.41.1 Fixed content membership restricted post's content not restricted in REST requests.\n * @since 4.0.0 Don't pass objects by reference because it's unnecessary.\n */\nclass LLMS_Template_Loader {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 1.0.0\n\t * @since 3.20.0 Unknown.\n\t * @since 3.41.1 Predispose posts content restriction in REST requests.\n\t * @since 5.8.0 Handle block templates loading.\n\t * @since 6.2.0 Added 'llms_template_loader_priority' filter.\n\t * @since 6.4.0 Reverted back the priority of the `$this->template_loader()` callback\n\t *              (`template_include` hook's callback) from 100 to 10.\n\t */\n\tpublic function __construct() {\n\n\t\t// Template loading for FSE themes.\n\t\tadd_action( 'template_redirect', array( $this, 'hook_block_template_loader' ) );\n\n\t\t/**\n\t\t* Filters the template loading priority.\n\t\t*\n\t\t* Callback for the WP core filter `template_include`.\n\t\t*\n\t\t* @since 6.2.0\n\t\t*\n\t\t* @param int $priority The filter callback priority.\n\t\t*/\n\t\t$template_loader_cb_priority = apply_filters( 'llms_template_loader_priority', 10 );\n\t\t/**\n\t\t * Do template loading.\n\t\t *\n\t\t * The default priority is 10, so to allow theme builders, like Divi and Elementor (Pro),\n\t\t * to override our templates (except single content restricted).\n\t\t * see https://github.com/gocodebox/lifterlms/issues/2111\n\t\t */\n\t\tadd_filter( 'template_include', array( $this, 'template_loader' ), $template_loader_cb_priority );\n\n\t\tadd_action( 'rest_api_init', array( $this, 'maybe_prepare_post_content_restriction' ) );\n\n\t\t// Restriction actions for each kind of restriction.\n\t\t$reasons = apply_filters(\n\t\t\t'llms_restriction_reasons',\n\t\t\tarray(\n\t\t\t\t'course_prerequisite',\n\t\t\t\t'course_track_prerequisite',\n\t\t\t\t'course_time_period',\n\t\t\t\t'enrollment_lesson',\n\t\t\t\t'lesson_drip',\n\t\t\t\t'lesson_prerequisite',\n\t\t\t\t'membership',\n\t\t\t\t'sitewide_membership',\n\t\t\t\t'quiz',\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $reasons as $reason ) {\n\t\t\tadd_action( 'llms_content_restricted_by_' . $reason, array( $this, 'restricted_by_' . $reason ), 10, 1 );\n\t\t}\n\n\t\tadd_action( 'wp', array( $this, 'maybe_redirect_to_sales_page' ) );\n\t}\n\n\t/**\n\t * Add a notice and/or redirect during restriction actions.\n\t *\n\t * @since 3.0.0\n\t * @since 7.4.0 Added `nocache_headers()` to prevent caching of redirects.\n\t *\n\t * @param string $msg      Notice message to display.\n\t * @param string $redirect Optional. Url to redirect to after setting a notice. Default empty string.\n\t * @param string $msg_type Optional. Type of message to display [notice|success|error|debug]. Default 'notice'.\n\t * @return void\n\t */\n\tprivate function handle_restriction( $msg = '', $redirect = '', $msg_type = 'notice' ) {\n\n\t\tif ( $msg ) {\n\t\t\tllms_add_notice( do_shortcode( $msg ), $msg_type );\n\t\t}\n\n\t\tif ( $redirect ) {\n\t\t\tnocache_headers();\n\t\t\twp_redirect( $redirect );\n\t\t\texit;\n\t\t}\n\t}\n\n\t/**\n\t * Handle sales page redirects for courses & memberships\n\t *\n\t * @since 3.20.0\n\t * @since 3.37.2 Flag to print notices, if there are, when landing on the redirected sales page.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_redirect_to_sales_page() {\n\n\t\t// Only proceed for courses and memberships.\n\t\tif ( ! in_array( get_post_type(), array( 'course', 'llms_membership' ), true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$page_restricted = llms_page_restricted( get_the_id() );\n\n\t\t// Only proceed if the page isn't restricted.\n\t\tif ( ! $page_restricted['is_restricted'] ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/** @var LLMS_Trait_Sales_Page $post */\n\t\t$post = llms_get_post( get_the_ID() );\n\n\t\tif ( ! $post->has_sales_page_redirect() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tllms_redirect_and_exit(\n\t\t\tllms_notice_count() ?\n\t\t\t\tadd_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'llms_print_notices' => 1,\n\t\t\t\t\t),\n\t\t\t\t\t$post->get_sales_page_url()\n\t\t\t\t) : $post->get_sales_page_url(),\n\t\t\tarray(\n\t\t\t\t'safe' => false,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Handle redirects and messages when a user attempts to access an item\n\t * restricted by a course track prerequisite.\n\t *\n\t * Redirect to parent course and display message.\n\t * If course do nothing.\n\t *\n\t * @since 3.7.3\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_course_track_prerequisite( $info ) {\n\n\t\tif ( 'course' === get_post_type( $info['content_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$msg      = llms_get_restriction_message( $info );\n\t\t$course   = llms_get_post_parent_course( $info['content_id'] );\n\t\t$redirect = get_permalink( $course->get( 'id' ) );\n\t\t$this->handle_restriction(\n\t\t\tapply_filters( 'llms_restricted_by_course_track_prerequisite_message', $msg, $info ),\n\t\t\tapply_filters( 'llms_restricted_by_course_track_prerequisite_redirect', $redirect, $info ),\n\t\t\t'error'\n\t\t);\n\t}\n\n\t/**\n\t * Handle redirects and messages when a user attempts to access an item\n\t * restricted by a course prerequisite.\n\t *\n\t * Redirect to parent course and display message.\n\t * If course do nothing.\n\t *\n\t * @since 3.7.3\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_course_prerequisite( $info ) {\n\n\t\tif ( 'course' === get_post_type( $info['content_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$msg      = llms_get_restriction_message( $info );\n\t\t$course   = llms_get_post_parent_course( $info['content_id'] );\n\t\t$redirect = get_permalink( $course->get( 'id' ) );\n\t\t$this->handle_restriction(\n\t\t\tapply_filters( 'llms_restricted_by_course_prerequisite_message', $msg, $info ),\n\t\t\tapply_filters( 'llms_restricted_by_course_prerequisite_redirect', $redirect, $info ),\n\t\t\t'error'\n\t\t);\n\t}\n\n\t/**\n\t * Handle redirects and messages when a course or associated quiz or lesson has time period\n\t * date restrictions placed upon it.\n\t *\n\t * Quizzes & Lessons redirect to the parent course.\n\t * Courses display a notice until the course opens and an error once the course closes.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_course_time_period( $info ) {\n\n\t\t$post_type = get_post_type( $info['content_id'] );\n\n\t\t// If this restriction occurs when attempting to view a lesson,\n\t\t// redirect the user to the course, course restriction will handle display of the\n\t\t// message once we get there.\n\t\t// This prevents duplicate messages from being displayed.\n\t\tif ( 'lesson' === $post_type || 'llms_quiz' === $post_type ) {\n\t\t\t$msg      = '';\n\t\t\t$redirect = get_permalink( $info['restriction_id'] );\n\t\t}\n\n\t\tif ( ! $msg && ! $redirect ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Handle the restriction action & allow developers to filter the results.\n\t\t$this->handle_restriction(\n\t\t\tapply_filters( 'llms_restricted_by_course_time_period_message', $msg, $info ),\n\t\t\tapply_filters( 'llms_restricted_by_course_time_period_redirect', $redirect, $info ),\n\t\t\t'notice'\n\t\t);\n\t}\n\n\t/**\n\t * Handle redirects and messages when a user attempts to access a lesson\n\t * for a course they're not enrolled in.\n\t *\n\t * Redirect to parent course and display message.\n\t *\n\t * @since 3.0.0\n\t * @since 3.2.4 Moved message generation to `llms_get_restriction_message()`\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_enrollment_lesson( $info ) {\n\n\t\t$msg      = llms_get_restriction_message( $info );\n\t\t$redirect = get_permalink( $info['restriction_id'] );\n\n\t\t$this->handle_restriction(\n\t\t\tapply_filters( 'llms_restricted_by_enrollment_lesson_message', $msg, $info ),\n\t\t\tapply_filters( 'llms_restricted_by_enrollment_lesson_redirect', $redirect, $info ),\n\t\t\t'error'\n\t\t);\n\t}\n\n\t/**\n\t * Handle redirects and messages when a user attempts to access a lesson\n\t * for that is restricted by lesson drip settings.\n\t *\n\t * Redirect to parent course and display message.\n\t *\n\t * @since 3.0.0\n\t * @since 3.2.4 Moved message generation to `llms_get_restriction_message()`\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_lesson_drip( $info ) {\n\n\t\t$lesson = new LLMS_Lesson( $info['restriction_id'] );\n\n\t\t$msg      = llms_get_restriction_message( $info );\n\t\t$redirect = get_permalink( $lesson->get( 'parent_course' ) );\n\n\t\t$this->handle_restriction(\n\t\t\tapply_filters( 'llms_restricted_by_lesson_drip_message', $msg, $info ),\n\t\t\tapply_filters( 'llms_restricted_by_lesson_drip_redirect', $redirect, $info ),\n\t\t\t'error'\n\t\t);\n\t}\n\n\t/**\n\t * Handle redirects and messages when a user attempts to access a lesson\n\t * for that is restricted by prerequisite lesson.\n\t *\n\t * Redirect to parent course and display message.\n\t *\n\t * @since 3.0.0\n\t * @since 3.2.4 Moved message generation to `llms_get_restriction_message()`\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_lesson_prerequisite( $info ) {\n\n\t\t$msg      = llms_get_restriction_message( $info );\n\t\t$redirect = get_permalink( $info['restriction_id'] );\n\t\t$this->handle_restriction(\n\t\t\tapply_filters( 'llms_restricted_by_lesson_prerequisite_message', $msg, $info ),\n\t\t\tapply_filters( 'llms_restricted_by_lesson_prerequisite_redirect', $redirect, $info ),\n\t\t\t'error'\n\t\t);\n\t}\n\n\t/**\n\t * Handle content restricted to a membership.\n\t *\n\t * Parses and obeys Membership \"Restriction Behavior\" settings.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.10 Added Flag to print notices when landing on the redirected page.\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_membership( $info ) {\n\n\t\t$membership_id = $info['restriction_id'];\n\n\t\t// Do nothing if we don't have a membership id.\n\t\tif ( ! empty( $membership_id ) && is_numeric( $membership_id ) ) {\n\n\t\t\t// Instantiate the membership.\n\t\t\t$membership = new LLMS_Membership( $membership_id );\n\n\t\t\t$msg      = '';\n\t\t\t$redirect = '';\n\n\t\t\tif ( 'yes' === $membership->get( 'restriction_add_notice' ) ) {\n\n\t\t\t\t$msg = $membership->get( 'restriction_notice' );\n\n\t\t\t}\n\n\t\t\t// Get the redirect based on the redirect type (if set).\n\t\t\tswitch ( $membership->get( 'restriction_redirect_type' ) ) {\n\n\t\t\t\tcase 'custom':\n\t\t\t\t\t$redirect = $membership->get( 'redirect_custom_url' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'membership':\n\t\t\t\t\t$redirect = get_permalink( $membership->get( 'id' ) );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'page':\n\t\t\t\t\t$redirect = get_permalink( $membership->get( 'redirect_page_id' ) );\n\t\t\t\t\t// Make sure to print notices in wp pages.\n\t\t\t\t\t$redirect = empty( $msg ) ? $redirect : add_query_arg(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'llms_print_notices' => 1,\n\t\t\t\t\t\t),\n\t\t\t\t\t\t$redirect\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\t// Handle the restriction action & allow developers to filter the results.\n\t\t\t$this->handle_restriction(\n\t\t\t\tapply_filters( 'llms_restricted_by_membership_message', $msg, $info ),\n\t\t\t\tapply_filters( 'llms_restricted_by_membership_redirect', $redirect, $info )\n\t\t\t);\n\n\t\t}\n\t}\n\n\t/**\n\t * Handle attempts to access quizzes.\n\t *\n\t * @since 3.1.6\n\t * @since 3.16.1 Unknown.\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_quiz( $info ) {\n\n\t\t$msg      = '';\n\t\t$redirect = '';\n\n\t\tif ( get_current_user_id() ) {\n\n\t\t\t$msg  = __( 'You must be enrolled in the course to access this quiz.', 'lifterlms' );\n\t\t\t$quiz = llms_get_post( $info['restriction_id'] );\n\t\t\tif ( $quiz ) {\n\t\t\t\t$course = $quiz->get_course();\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$redirect = get_permalink( $course->get( 'id' ) );\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\n\t\t\t$msg      = __( 'You must be logged in to take quizzes.', 'lifterlms' );\n\t\t\t$redirect = llms_person_my_courses_url();\n\n\t\t}\n\n\t\t$this->handle_restriction(\n\t\t\tapply_filters( 'llms_restricted_by_membership_message', $msg, $info ),\n\t\t\tapply_filters( 'llms_restricted_by_membership_redirect', $redirect, $info ),\n\t\t\t'error'\n\t\t);\n\t}\n\n\t/**\n\t * Handle content restricted to a membership\n\t *\n\t * Parses and obeys Membership \"Restriction Behavior\" settings.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $info Array of restriction info from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function restricted_by_sitewide_membership( $info ) {\n\t\t$this->restricted_by_membership( $info );\n\t}\n\n\t/**\n\t * Hooks the callback to load FSE block templates.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function hook_block_template_loader() {\n\t\tadd_filter( 'pre_get_block_templates', array( $this, 'block_template_loader' ), 99, 3 );\n\t}\n\n\t/**\n\t * Filter blocks templates.\n\t *\n\t * @since 5.8.0\n\t * @since 6.0.0 Remove LifterLMS 6.0 version check about the certificate template.\n\t *              Use `llms_is_block_theme()` in favor of `wp_is_block_theme()`.\n\t *\n\t * @param WP_Block_Template[] $result        Array of found block templates.\n\t * @param array               $query {\n\t *     Optional. Arguments to retrieve templates.\n\t *\n\t *     @type array  $slug__in List of slugs to include.\n\t *     @type int    $wp_id Post ID of customized template.\n\t * }\n\t * @param array               $template_type wp_template or wp_template_part.\n\t * @return array Templates.\n\t */\n\tpublic function block_template_loader( $result, $query, $template_type ) {\n\n\t\t// Bail if it's not a block theme, or is being retrieved a non wp_template file.\n\t\tif ( ! llms_is_block_theme() || 'wp_template' !== $template_type ) {\n\t\t\treturn $result;\n\t\t}\n\n\t\t$template_name = $this->get_maybe_forced_template();\n\n\t\t/**\n\t\t * Since LifterLMS 6.0.0 certificates have their own PHP template that do no depend on the theme.\n\t\t * This means that we can use the PHP template loaded in the method `LLMS_Template_Loader::template_loader()` below.\n\t\t */\n\t\t$template_name = is_singular( array( 'llms_certificate', 'llms_my_certificate' ) ) ? '' : $template_name;\n\n\t\t// Focus mode always uses the PHP template (not the block template) for a stable, non-editable layout.\n\t\tif ( 'single-lesson-focus' === $template_name ) {\n\t\t\treturn $result;\n\t\t}\n\n\t\t/**\n\t\t * Filters the block template to be loded forced.\n\t\t *\n\t\t * @since 5.8.0\n\t\t *\n\t\t * @param string $template_slug The template slug to be force loaded.\n\t\t * @param string $template      The name of template to be force loaded.\n\t\t */\n\t\t$template_slug = apply_filters( 'llms_forced_block_template_slug', $template_name ? LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . $template_name : '', $template_name );\n\n\t\tif ( empty( $template_slug ) ) {\n\t\t\treturn $result;\n\t\t}\n\n\t\t// Prevent template_loader to load a php template.\n\t\tadd_filter( 'llms_force_php_template_loading', '__return_false' );\n\n\t\treturn llms()->block_templates()->add_llms_block_templates(\n\t\t\tarray(),\n\t\t\tarray( 'slug__in' => array( $template_slug ) )\n\t\t);\n\t}\n\n\t/**\n\t * Check if content should be restricted and include overrides where appropriate.\n\t *\n\t * Triggers actions based on content restrictions.\n\t *\n\t * @since 1.0.0\n\t * @since 3.16.11 Unknown.\n\t * @since 3.37.2 Make sure to print notices on sales page redirect.\n\t * @since 4.10.1 Refactor to reduce code duplication and replace usage of `llms_shop` with `courses` for catalog check.\n\t * @since 5.8.0 Refactor: moved the template guessing in a specific method.\n\t * @since 6.4.0 Defer single content restricted template loading.\n\t *\n\t * @param string $template The template to load.\n\t * @return string\n\t */\n\tpublic function template_loader( $template ) {\n\n\t\t$page_restricted = llms_page_restricted( get_the_ID() );\n\n\t\t$this->maybe_print_notices_on_sales_page_redirect();\n\n\t\tif ( $page_restricted['is_restricted'] ) {\n\t\t\t/**\n\t\t\t * Generic action triggered when content is restricted.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @see llms_content_restricted_by_{$page_restricted['reason']} A specific hook triggered by a specific restriction reason.\n\t\t\t *\n\t\t\t * @param array $page_restricted Restriction information from `llms_page_restricted()`.\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_content_restricted', $page_restricted );\n\n\t\t\t/**\n\t\t\t * Action triggered when content is restricted for the specified reason.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `{$page_restricted['reason']}` refers to the restriction reason\n\t\t\t * code generated by `llms_page_restricted()`.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @see llms_content_restricted A generic hook triggered at the same time.\n\t\t\t *\n\t\t\t * @param array $page_restricted Restriction information from `llms_page_restricted()`.\n\t\t\t */\n\t\t\tdo_action( \"llms_content_restricted_by_{$page_restricted['reason']}\", $page_restricted );\n\n\t\t\tif ( is_home() && 'sitewide_membership' === $page_restricted['reason'] ) {\n\t\t\t\t// Prints notices on the blog page when there's not redirects setup.\n\t\t\t\tadd_action( 'loop_start', 'llms_print_notices', 5 );\n\t\t\t}\n\t\t}\n\n\t\t$forced_template = $this->maybe_force_php_template( $template );\n\n\t\t/**\n\t\t * When restricting single content use a lower priority so to always override\n\t\t * theme builders like Divi and Elementor (Pro).\n\t\t * see https://github.com/gocodebox/lifterlms/issues/2063.\n\t\t */\n\t\tif ( llms_template_file_path( 'single-no-access.php' ) === $forced_template ) {\n\n\t\t\t/**\n\t\t\t * Filters the template loading priority for single restricted content.\n\t\t\t *\n\t\t\t * @since 6.4.0\n\t\t\t *\n\t\t\t * @param int $priority The filter callback priority.\n\t\t\t */\n\t\t\t$template_loader_restricted_cb_priority = apply_filters( 'llms_template_loader_restricted_priority', 100 );\n\t\t\tadd_filter( 'template_include', array( $this, 'maybe_force_php_template' ), $template_loader_restricted_cb_priority );\n\n\t\t} else {\n\t\t\t$template = $forced_template;\n\t\t}\n\n\t\treturn $template;\n\t}\n\n\t/**\n\t * Force the PHP template to be loaded.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @param string $template The original template to load.\n\t * @return string\n\t */\n\tpublic function maybe_force_php_template( $template ) {\n\n\t\t/**\n\t\t * Filters whether or not forcing a LifterLMS php template to be loaded.\n\t\t *\n\t\t * @since 5.8.0\n\t\t *\n\t\t * @param bool $force Whether or not forcing a LifterLMS PHP template to be loaded.\n\t\t */\n\t\t$forced_template = apply_filters( 'llms_force_php_template_loading', true ) ? $this->get_maybe_forced_template() : false;\n\t\treturn $forced_template ? llms_template_file_path( \"{$forced_template}.php\" ) : $template;\n\t}\n\n\t/**\n\t * Retrieve the hierarchical template to be loaded.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return null|string\n\t */\n\tprivate function get_maybe_forced_template() {\n\n\t\t$page_restricted = llms_page_restricted( get_the_ID() );\n\t\t$template        = null;\n\n\t\tif ( $page_restricted['is_restricted'] ) {\n\n\t\t\t// Blog should bypass checks, except when sitewide restrictions are enabled.\n\t\t\tif ( ( is_home() && 'sitewide_membership' === $page_restricted['reason'] ) ||\n\t\t\t\t\t// Course and membership content restrictions are handled by conditional elements in the editor.\n\t\t\t\t\t( in_array( get_post_type(), array( 'course', 'llms_membership' ), true ) ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Content is restricted.\n\t\t\t$template = 'single-no-access';\n\n\t\t} elseif ( is_post_type_archive( 'course' ) || is_page( llms_get_page_id( 'courses' ) ) ) {\n\n\t\t\t$template = 'archive-course';\n\n\t\t} elseif ( is_post_type_archive( 'llms_membership' ) || is_page( llms_get_page_id( 'memberships' ) ) ) {\n\n\t\t\t$template = 'archive-llms_membership';\n\n\t\t} elseif ( is_tax( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) ) ) {\n\n\t\t\tglobal $wp_query;\n\t\t\t$obj      = $wp_query->get_queried_object();\n\t\t\t$template = 'taxonomy-' . $obj->taxonomy;\n\n\t\t} elseif ( is_singular( array( 'llms_certificate', 'llms_my_certificate' ) ) ) {\n\n\t\t\t$template = 'single-certificate';\n\n\t\t} elseif ( is_singular( apply_filters( 'llms_focus_mode_post_types', array( 'lesson', 'llms_quiz' ) ) ) && llms_is_focus_mode_enabled( get_the_ID() ) ) {\n\n\t\t\t$template = 'single-lesson-focus';\n\n\t\t}\n\n\t\t/**\n\t\t * Filters the template to be loded forced.\n\t\t *\n\t\t * @since 5.8.0\n\t\t *\n\t\t * @param string $template The template slug to be loaded forced.\n\t\t */\n\t\treturn apply_filters( 'llms_forced_template', $template );\n\t}\n\n\t/**\n\t * Maybe print notices after redirection.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tprivate function maybe_print_notices_on_sales_page_redirect() {\n\n\t\tif ( llms_filter_input( INPUT_GET, 'llms_print_notices' ) ) {\n\t\t\t// Prints notices on the page at loop start.\n\t\t\tadd_action( 'loop_start', 'llms_print_notices', 5 );\n\t\t}\n\t}\n\n\t/**\n\t * Maybe restrict the post content in REST requets\n\t *\n\t * @since 3.41.1\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_prepare_post_content_restriction() {\n\t\t// Fired on `setup_postdata()` see `WP_REST_Posts_Controller::prepare_item_for_response()`.\n\t\tadd_action( 'the_post', array( $this, 'maybe_restrict_post_content' ), 9999, 2 );\n\t}\n\n\t/**\n\t * Maybe restrict the post content in the REST loop\n\t *\n\t * @since 3.41.1\n\t * @since 4.0.0 Don't pass by reference because it's unnecessary.\n\t * @since 4.10.1 Fixed incorrect position of `true` in `in_array()`.\n\t *\n\t * @param WP_Post  $post  Post Object.\n\t * @param WP_Query $query Query object.\n\t * @return void\n\t */\n\tpublic function maybe_restrict_post_content( $post, $query ) {\n\t\t/**\n\t\t * Filters the post types that must be skipped.\n\t\t *\n\t\t * The LifterLMS post types content restriction should be handled by the LifterLMS rest-api.\n\t\t *\n\t\t * @since 3.41.1\n\t\t *\n\t\t * @param string[] $post_types The array of post types to skip.\n\t\t */\n\t\t$skip = apply_filters(\n\t\t\t'llms_in_rest_restrict_content_skip_post_types',\n\t\t\tarray(\n\t\t\t\t'course',\n\t\t\t\t'lesson',\n\t\t\t\t'llms_quiz',\n\t\t\t\t'llms_membership',\n\t\t\t\t'llms_question',\n\t\t\t\t'llms_certificate',\n\t\t\t\t'llms_my_certificate',\n\t\t\t)\n\t\t);\n\n\t\tif ( in_array( get_post_type( $post ), $skip, true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Needed by `llms_page_restricted()` to work as expected.\n\t\t$is_singular        = $query->is_singular;\n\t\t$query->is_singular = true;\n\n\t\t$page_restricted = llms_page_restricted( get_the_ID() );\n\n\t\tif ( $page_restricted['is_restricted'] ) {\n\n\t\t\t$msg    = __( 'This content is restricted', 'lifterlms' );\n\t\t\t$reason = $page_restricted['reason'];\n\n\t\t\tif ( in_array( $reason, array( 'membership', 'sitewide_membership' ), true ) ) {\n\n\t\t\t\t$membership_id = $page_restricted['restriction_id'];\n\n\t\t\t\tif ( ! empty( $membership_id ) && is_numeric( $membership_id ) ) {\n\n\t\t\t\t\t$membership = new LLMS_Membership( $membership_id );\n\n\t\t\t\t\tif ( 'yes' === $membership->get( 'restriction_add_notice' ) ) {\n\t\t\t\t\t\t$msg = $membership->get( 'restriction_notice' );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filters the restriction message.\n\t\t\t *\n\t\t\t * The dynamic portion of the hook name, `$reason`, refers to the restriction reason.\n\t\t\t *\n\t\t\t * @since 3.41.1\n\t\t\t *\n\t\t\t * @param string $message     Restriction message.\n\t\t\t * @param array  $restriction Array of restriction info from `llms_page_restricted()`.\n\t\t\t */\n\t\t\t$msg = apply_filters( \"llms_in_rest_restricted_by_{$reason}_message\", $msg, $page_restricted );\n\n\t\t\t$post->post_content = $msg;\n\t\t\t$post->post_excerpt = $msg;\n\t\t}\n\n\t\t$query->is_singular = $is_singular;\n\t}\n}\n\nnew LLMS_Template_Loader();\n"
  },
  {
    "path": "includes/class.llms.track.php",
    "content": "<?php\n/**\n * LifterLMS Course Tracks\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Course Tracks\n *\n * @since 3.0.0\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Track {\n\n\t/**\n\t * @var string\n\t * @since 3.0.0\n\t */\n\tpublic $taxonomy = 'course_track';\n\n\t/**\n\t * @var WP_Term\n\t * @since 3.0.0\n\t */\n\tpublic $term;\n\n\t/**\n\t * Constructor\n\t *\n\t * @param    int|string|obj $term   term_id, term_slug, or instance of a WP_Term\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function __construct( $term ) {\n\n\t\tif ( is_numeric( $term ) ) {\n\n\t\t\t$this->term = get_term( $term, $this->taxonomy );\n\n\t\t} elseif ( is_string( $term ) ) {\n\n\t\t\t$this->term = get_term_by( 'slug', $term, $this->taxonomy );\n\n\t\t} elseif ( $term instanceof WP_Term ) {\n\n\t\t\t$this->term = $term;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Get an array of WP Posts for the courses in the track\n\t *\n\t * @return   array\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function get_courses() {\n\n\t\t// No posts in the term, return an empty array.\n\t\tif ( 0 === $this->term->count ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t$q = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t'post_type'      => 'course',\n\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t'tax_query'      => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'field'            => 'id',\n\t\t\t\t\t\t'include_children' => false,\n\t\t\t\t\t\t'taxonomy'         => $this->taxonomy,\n\t\t\t\t\t\t'terms'            => $this->term->term_id,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tif ( $q->have_posts() ) {\n\t\t\treturn $q->posts;\n\t\t} else {\n\t\t\treturn array();\n\t\t}\n\t}\n\n\t/**\n\t * Get a permalink to the track's archive page\n\t *\n\t * @return   string\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function get_permalink() {\n\t\treturn get_term_link( $this->term->term_id, $this->taxonomy );\n\t}\n\n\t/**\n\t * Get the track's title\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn $this->term->name;\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.tracker.php",
    "content": "<?php\n/**\n * Handle sending data to LifterLMS when tracking is enabled\n *\n * @package LifterLMS/Classes\n *\n * @since 3.0.0\n * @version 7.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS telemetry tracking data.\n *\n * @since 3.0.0\n */\nclass LLMS_Tracker {\n\n\t/**\n\t * URL endpoint where we'll receive the data\n\t */\n\tconst API_URL = 'https://lifterlms.com/llms-api/tracking';\n\n\t/**\n\t * Initialize.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\t\tadd_action( 'llms_send_tracking_data', array( __CLASS__, 'send_data' ) );\n\t}\n\n\t/**\n\t * Retrieve a timestamp of the last time the tracks sent data home\n\t *\n\t * @since    3.0.0\n\t *\n\t * @return   int        a timestamp\n\t */\n\tprivate static function get_last_send_time() {\n\t\treturn apply_filters( 'llms_tracker_get_last_send_time', get_option( 'llms_tracker_last_send_time', 0 ) );\n\t}\n\n\t/**\n\t * Send data home\n\t *\n\t * @since 3.0.0\n\t * @since 7.4.0 Fix return type.\n\t *\n\t * @param bool $force Force send regardless or the last send time.\n\t * @return array|WP_Error|void\n\t */\n\tpublic static function send_data( $force = false ) {\n\n\t\t// Don't trigger during AJAX Requests.\n\t\tif ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Allow forcing of the send despite the interval.\n\t\tif ( ! $force && ! apply_filters( 'llms_tracker_force_send', false ) ) {\n\n\t\t\t// Only send data once a week.\n\t\t\t$last_send = self::get_last_send_time();\n\t\t\tif ( $last_send && $last_send > apply_filters( 'llms_tracker_send_interval', strtotime( '-1 week' ) ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Record a last send time.\n\t\tupdate_option( 'llms_tracker_last_send_time', time() );\n\n\t\t$r = wp_remote_post(\n\t\t\tself::API_URL,\n\t\t\tarray(\n\t\t\t\t'body'        => array(\n\t\t\t\t\t'data' => json_encode( LLMS_Data::get_data( 'tracker' ) ),\n\t\t\t\t),\n\t\t\t\t'cookies'     => array(),\n\t\t\t\t'headers'     => array(\n\t\t\t\t\t'user-agent' => 'LifterLMS_Tracker/' . md5( esc_url( home_url( '/' ) ) ) . ';',\n\t\t\t\t),\n\t\t\t\t'method'      => 'POST',\n\t\t\t\t'redirection' => 5,\n\t\t\t\t'timeout'     => 60,\n\t\t\t)\n\t\t);\n\n\t\tif ( ! is_wp_error( $r ) ) {\n\n\t\t\treturn json_decode( $r['body'], true );\n\n\t\t} else {\n\n\t\t\treturn $r;\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/class.llms.user.permissions.php",
    "content": "<?php\n/**\n * LLMS_User_Permissions class file\n *\n * @package LifterLMS/Classes\n *\n * @since 3.13.0\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Filters and actions related to user permissions\n *\n * @since 3.13.0\n * @since 3.34.0 Always add the `editable_roles` filter.\n * @since 3.34.0 Added methods and logic for managing user management of other users.\n *                  Add logic for `view_students`, `edit_students`, and `delete_students` capabilities.\n * @since 3.36.5 Add `llms_user_caps_edit_others_posts_post_types` filter to allow 3rd parties to utilize core methods for modifying other users posts.\n * @since 3.37.14 Use strict comparisons where needed.\n * @since 3.41.0 Improve user management of other users when the managing user has multiple roles.\n */\nclass LLMS_User_Permissions {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.13.0\n\t * @since 3.34.0 Always add the `editable_roles` filter.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'user_has_cap', array( $this, 'handle_caps' ), 10, 3 );\n\t\tadd_filter( 'editable_roles', array( $this, 'editable_roles' ) );\n\t\tadd_filter( 'rest_user_query', array( $this, 'filter_rest_user_query' ), 10, 2 );\n\t}\n\n\t/**\n\t * Determines what other user roles can be managed by a user role\n\t *\n\t * Allows LMS Managers to create instructors and other managers.\n\t * Allows instructors to create & manage assistants.\n\t *\n\t * @since 3.13.0\n\t * @since 3.34.0 Moved the `llms_editable_roles` filter to the class method get_editable_roles().\n\t * @since 3.37.14 Use strict comparison.\n\t * @since 4.10.0 Better handling of users with multiple roles.\n\t *\n\t * @link https://codex.wordpress.org/Plugin_API/Filter_Reference/editable_roles\n\t *\n\t * @param array $all_roles All roles array.\n\t * @return array\n\t */\n\tpublic function editable_roles( $all_roles ) {\n\n\t\t/**\n\t\t * Prevent issues when other plugins call get_editable_roles() before `init`.\n\t\t *\n\t\t * @link https://github.com/gocodebox/lifterlms/issues/1727\n\t\t */\n\t\tif ( ! function_exists( 'wp_get_current_user' ) ) {\n\t\t\treturn $all_roles;\n\t\t}\n\n\t\tif ( is_multisite() && is_super_admin() ) {\n\t\t\treturn $all_roles;\n\t\t}\n\n\t\t$user       = wp_get_current_user();\n\t\t$user_roles = $user->roles;\n\n\t\tif ( in_array( 'administrator', $user_roles, true ) ) {\n\t\t\treturn $all_roles;\n\t\t}\n\n\t\t$editable_roles = self::get_editable_roles();\n\n\t\tif ( empty( array_intersect( $user_roles, array_keys( $editable_roles ) ) ) ) {\n\t\t\treturn $all_roles;\n\t\t}\n\n\t\t$roles = array();\n\t\tforeach ( $user_roles as $user_role ) {\n\t\t\tif ( isset( $editable_roles[ $user_role ] ) ) {\n\t\t\t\t$roles = array_merge( $roles, $editable_roles[ $user_role ] );\n\t\t\t}\n\t\t}\n\n\t\t$roles = array_unique( $roles );\n\n\t\tforeach ( array_keys( $all_roles ) as $role ) {\n\t\t\tif ( ! in_array( $role, $roles, true ) ) {\n\t\t\t\tunset( $all_roles[ $role ] );\n\t\t\t}\n\t\t}\n\n\t\treturn $all_roles;\n\t}\n\n\t/**\n\t * Filter the WP_User_Query args to ensure that instructors can only see their students\n\t *\n\t * @since 8.0.2\n\t *\n\t * @param array           $args WP_User_Query args.\n\t * @param WP_REST_Request $request Request object.\n\t * @return array\n\t */\n\tpublic function filter_rest_user_query( $args, $request ) {\n\n\t\t$user = wp_get_current_user();\n\n\t\tif ( ! $user ) {\n\t\t\treturn $args;\n\t\t}\n\n\t\tif ( ! in_array( 'instructor', $user->roles, true ) ) {\n\t\t\treturn $args;\n\t\t}\n\n\t\t$instructor = llms_get_instructor( $user );\n\n\t\tif ( ! $instructor ) {\n\t\t\treturn $args;\n\t\t}\n\n\t\t$student_query = $instructor->get_students( array( 'statuses' => array( 'enrolled' ) ) );\n\t\t$students      = $student_query->get_results();\n\n\t\tif ( empty( $students ) ) {\n\t\t\t$args['include'] = array( 0 );\n\t\t} else {\n\t\t\t$args['include'] = wp_list_pluck( $students, 'id' );\n\t\t}\n\n\t\treturn $args;\n\t}\n\n\t/**\n\t * Handle capabilities checks for lms content to allow *editing* content based on course instructor\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param bool[]   $allcaps Array of key/value pairs where keys represent a capability name and boolean values\n\t *                          represent whether the user has that capability.\n\t * @param string[] $cap     Required primitive capabilities for the requested capability.\n\t * @param array    $args {\n\t *     Arguments that accompany the requested capability check.\n\t *\n\t *     @type string    $0 Requested capability.\n\t *     @type int       $1 Concerned user ID.\n\t *     @type mixed  ...$2 Optional second and further parameters, typically object ID.\n\t * }\n\t * @return array\n\t */\n\tpublic function edit_others_lms_content( $allcaps, $cap, $args ) {\n\n\t\t/**\n\t\t * this might be a problem\n\t\t * this happens when in wp-admin/includes/post.php\n\t\t * when actually creating/updating a course\n\t\t * and no post_id is passed in $args[2].\n\t\t */\n\t\tif ( empty( $args[2] ) ) {\n\t\t\t$allcaps[ $cap[0] ] = true;\n\t\t\treturn $allcaps;\n\t\t}\n\n\t\t$instructor = llms_get_instructor( $args[1] );\n\t\tif ( $instructor && $instructor->is_instructor( $args[2] ) ) {\n\t\t\t$allcaps[ $cap[0] ] = true;\n\t\t}\n\n\t\treturn $allcaps;\n\t}\n\n\t/**\n\t * Get a map of roles that can be managed by LifterLMS User Roles\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return array\n\t */\n\tpublic static function get_editable_roles() {\n\n\t\t/**\n\t\t * Get a map of roles that can be managed by LifterLMS User Roles\n\t\t *\n\t\t * @since 3.13.0\n\t\t *\n\t\t * @param array $roles Array of user roles. The array key is the role and the value is an array of roles that can be managed by that role.\n\t\t */\n\t\t$roles = apply_filters(\n\t\t\t'llms_editable_roles',\n\t\t\tarray(\n\t\t\t\t'lms_manager' => array( 'instructor', 'instructors_assistant', 'lms_manager', 'student' ),\n\t\t\t\t'instructor'  => array( 'instructors_assistant' ),\n\t\t\t)\n\t\t);\n\n\t\treturn $roles;\n\t}\n\n\t/**\n\t * Modify a users ability to `view_grades`\n\t *\n\t * Users can view the grades (quiz results) if one of the following conditions is met:\n\t *   + Users can view their own grades.\n\t *   + Admins and LMS Managers can view anyone's grade.\n\t *   + Any user who has been explicitly granted the `view_grades` cap can view anyone's grade (via custom code).\n\t *   + Any instructor/assistant who can `edit_post` for the course the quiz belongs to can view grades of the students within that course.\n\t *\n\t * @since 4.21.2\n\t *\n\t * @param bool[] $allcaps Array of key/value pairs where keys represent a capability name and boolean values\n\t *                        represent whether the user has that capability.\n\t * @param array  $args {\n\t *   Arguments that accompany the requested capability check.\n\t *\n\t *     @type string $0 Requested capability: 'view_grades'.\n\t *     @type int    $1 Current User ID.\n\t *     @type int    $2 Requested User ID.\n\t *     @type int    $3 WP_Post ID of the quiz (optional)\n\t * }\n\t * @return array\n\t */\n\tprivate function handle_cap_view_grades( $allcaps, $args ) {\n\n\t\t// Logged out user or missing required args.\n\t\tif ( empty( $args[1] ) || empty( $args[2] ) ) {\n\t\t\treturn $allcaps;\n\t\t}\n\n\t\t$requested_cap     = $args[0];\n\t\t$current_user_id   = intval( $args[1] );\n\t\t$requested_user_id = intval( $args[2] );\n\t\t$post_id           = isset( $args[3] ) ? intval( $args[3] ) : false;\n\n\t\t// Administrators and LMS managers explicitly have the cap so we don't need to perform any further checks.\n\t\tif ( ! empty( $allcaps[ $requested_cap ] ) ) {\n\t\t\treturn $allcaps;\n\t\t}\n\n\t\t// Users can view their own grades.\n\t\tif ( $current_user_id === $requested_user_id ) {\n\t\t\t$allcaps[ $requested_cap ] = true;\n\t\t} elseif ( $post_id && current_user_can( 'edit_post', $post_id ) ) {\n\t\t\tif ( $this->instructor_has_student( $current_user_id, $requested_user_id ) ) {\n\t\t\t\t$allcaps[ $requested_cap ] = true;\n\t\t\t}\n\t\t} elseif ( ! $post_id && current_user_can( 'view_students', $requested_user_id ) ) {\n\t\t\tif ( $this->instructor_has_student( $current_user_id, $requested_user_id ) ) {\n\t\t\t\t$allcaps[ $requested_cap ] = true;\n\t\t\t}\n\t\t}\n\n\t\treturn $allcaps;\n\t}\n\n\t/**\n\t * Custom capability checks for LifterLMS things\n\t *\n\t * @since 3.13.0\n\t * @since 3.34.0 Add logic for `edit_users` and `delete_users` capabilities with regards to LifterLMS user roles.\n\t *               Add logic for `view_students`, `edit_students`, and `delete_students` capabilities.\n\t * @since 3.36.5 Add `llms_user_caps_edit_others_posts_post_types` filter.\n\t * @since 3.37.14 Use strict comparison.\n\t * @since 4.21.2 Add logic to handle the `view_grades` capability.\n\t *\n\t * @param bool[]   $allcaps Array of key/value pairs where keys represent a capability name and boolean values\n\t *                          represent whether the user has that capability.\n\t * @param string[] $cap     Required primitive capabilities for the requested capability.\n\t * @param array    $args {\n\t *     Arguments that accompany the requested capability check.\n\t *\n\t *     @type string    $0 Requested capability.\n\t *     @type int       $1 Concerned user ID.\n\t *     @type mixed  ...$2 Optional second and further parameters, typically object ID.\n\t * }\n\t * @return array\n\t */\n\tpublic function handle_caps( $allcaps, $cap, $args ) {\n\n\t\t/**\n\t\t * Modify the list of post types that users may not own but can still edit based on instructor permissions on the course\n\t\t *\n\t\t * @since 3.36.5\n\t\t *\n\t\t * @param string[] $post_types Array of unprefixed post type names.\n\t\t */\n\t\t$post_types = apply_filters( 'llms_user_caps_edit_others_posts_post_types', array( 'courses', 'lessons', 'sections', 'quizzes', 'questions', 'memberships' ) );\n\t\tforeach ( $post_types as $cpt ) {\n\t\t\t// Allow any instructor to edit courses they're attached to.\n\t\t\tif ( in_array( sprintf( 'edit_others_%s', $cpt ), $cap, true ) ) {\n\t\t\t\t$allcaps = $this->edit_others_lms_content( $allcaps, $cap, $args );\n\t\t\t}\n\t\t}\n\n\t\t$required_cap = ! empty( $cap[0] ) ? $cap[0] : false;\n\n\t\tif ( 'view_grades' === $required_cap ) {\n\t\t\treturn $this->handle_cap_view_grades( $allcaps, $args );\n\t\t}\n\n\t\t// We don't have a cap or the user doesn't have the requested cap.\n\t\tif ( ! $required_cap || empty( $allcaps[ $required_cap ] ) ) {\n\t\t\treturn $allcaps;\n\t\t}\n\n\t\t$user_id   = ! empty( $args[1] ) ? $args[1] : false;\n\t\t$object_id = ! empty( $args[2] ) ? $args[2] : false;\n\n\t\tif ( in_array( $required_cap, array( 'edit_users', 'delete_users' ), true ) ) {\n\t\t\tif ( $user_id && $object_id && false === $this->user_can_manage_user( $user_id, $object_id ) ) {\n\t\t\t\tunset( $allcaps[ $required_cap ] );\n\t\t\t}\n\t\t}\n\n\t\tif ( in_array( $required_cap, array( 'view_students', 'edit_students', 'delete_students' ), true ) ) {\n\t\t\t$others_cap = str_replace( '_', '_others_', $required_cap );\n\t\t\tif ( $user_id && $object_id && ! user_can( $user_id, $others_cap ) ) {\n\t\t\t\t$instructor = llms_get_instructor( $user_id );\n\t\t\t\tif ( ! $instructor || ! $instructor->has_student( $object_id ) ) {\n\t\t\t\t\tunset( $allcaps[ $required_cap ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $allcaps;\n\t}\n\n\t/**\n\t * Determines if the current user is an instructor.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return bool\n\t */\n\tpublic static function is_current_user_instructor() {\n\n\t\treturn ( current_user_can( 'lifterlms_instructor' ) && current_user_can( 'list_users' ) && ! current_user_can( 'manage_lifterlms' ) );\n\t}\n\n\t/**\n\t * Determine if a user can manage another user.\n\t *\n\t * Run on `user_has_cap` filters for the `edit_users` and `delete_users` capabilities.\n\t *\n\t * @since 3.34.0\n\t * @since 3.41.0 Better handling of users with multiple roles.\n\t *\n\t * @param int $user_id WP User ID of the user requesting to perform the action.\n\t * @param int $edit_id WP User ID of the user the action will be performed on.\n\t * @return bool|null Returns true if the user performs the action, false if it can't, and null for core user roles which are skipped.\n\t */\n\tprotected function user_can_manage_user( $user_id, $edit_id ) {\n\n\t\t$user = get_user_by( 'id', $user_id );\n\n\t\t/**\n\t\t * Filter the list of \"ignored\" user roles\n\t\t *\n\t\t * If a user has one of the roles specified in this list, LifterLMS\n\t\t * will not attempt to determine if the user can manage other users\n\t\t * and will instead allow the WordPress core (or another plugin)\n\t\t * to determine if they have the required permissions.\n\t\t *\n\t\t * @since 3.41.0\n\t\t *\n\t\t * @param string[] $ignored Array of user roles.\n\t\t */\n\t\t$ignored   = apply_filters( 'llms_user_can_manage_user_ignored_roles', array( 'administrator' ) );\n\t\t$lms_roles = array_keys( LLMS_Roles::get_roles() );\n\n\t\t$user_roles         = array_intersect( $user->roles, $lms_roles );\n\t\t$user_ignored_roles = array_intersect( $user->roles, $ignored );\n\n\t\t/**\n\t\t * Skip the user because:\n\t\t *\n\t\t * + User has no LMS roles, eg: Administrator, Editor, or Subscriber.\n\t\t * + User has an LMS role and a \"protected\" role, eg: Administrator and student.\n\t\t *\n\t\t * In both scenarios we will return `null` which signals that the WordPress core (or another plugin)\n\t\t * should take care of determining if the user can manage the user.\n\t\t */\n\t\tif ( ! $user_roles || ! empty( $user_ignored_roles ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$edit_id = absint( $edit_id );\n\t\t$user_id = absint( $user_id );\n\n\t\t// Users can edit themselves.\n\t\tif ( $user_id === $edit_id ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t$edit_user = get_user_by( 'id', $edit_id );\n\n\t\tif ( ! $edit_user ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$edit_roles = array_intersect( $edit_user->roles, $lms_roles );\n\n\t\t$editable_roles = self::get_editable_roles();\n\n\t\tforeach ( $user_roles as $role ) {\n\n\t\t\tif ( 'instructor' === $role && in_array( 'instructors_assistant', $edit_roles, true ) ) {\n\t\t\t\t$instructor = llms_get_instructor( $user );\n\t\t\t\tif ( in_array( $edit_id, array_map( 'absint', $instructor->get_assistants() ), true ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} elseif ( ! empty( $editable_roles[ $role ] ) && array_intersect( $edit_roles, $editable_roles[ $role ] ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determine if an instructor has a student.\n\t *\n\t * @since 7.6.0\n\t *\n\t * @param int $current_user_id WP User ID of the user requesting to perform the action.\n\t * @param int $requested_user_id WP User ID of the user the action will be performed on.\n\t * @return bool Returns true if the user has the student, false if it doesn't\n\t */\n\tprotected function instructor_has_student( $current_user_id, $requested_user_id ) {\n\n\t\t$instructor = llms_get_instructor( $current_user_id );\n\t\treturn $instructor && $instructor->has_student( $requested_user_id );\n\t}\n}\n\nreturn new LLMS_User_Permissions();\n"
  },
  {
    "path": "includes/class.llms.view.manager.php",
    "content": "<?php\n/**\n * View Manager\n *\n * Allows qualifying user roles to view as various user types to make easier testing and editing of LLMS Content.\n *\n * @package LifterLMS/Classes\n *\n * @since 3.7.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_View_Manager class.\n *\n * @since 3.7.0\n */\nclass LLMS_View_Manager {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.7.0\n\t * @since 4.2.0 Added early return when creating a pending order.\n\t */\n\tpublic function __construct() {\n\n\t\t// Do nothing if we're creating a pending order.\n\t\tif ( ! empty( $_POST['action'] ) && 'create_pending_order' === $_POST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing\n\t\t\treturn;\n\t\t}\n\n\t\tadd_action( 'init', array( $this, 'add_actions' ) );\n\t}\n\n\t/**\n\t * Add actions & filters.\n\t *\n\t * @since 3.7.0\n\t * @since 4.2.0 Added filter to handle the displaying of the free enroll.\n\t * @since 4.16.0 Added filters to handle modification of the student dashboard.\n\t * @since 5.9.0 Pass second parameter to `modify_course_open()` methods.\n\t *\n\t * @return void\n\t */\n\tpublic function add_actions() {\n\n\t\t// Output view links on the admin menu.\n\t\tadd_action( 'admin_bar_menu', array( $this, 'add_menu_items' ), 777 );\n\n\t\t// Filter page restrictions.\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'modify_restrictions' ), 10, 1 );\n\t\tadd_filter( 'llms_is_course_open', array( $this, 'modify_course_open' ), 10, 2 );\n\t\tadd_filter( 'llms_is_course_enrollment_open', array( $this, 'modify_course_open' ), 10, 2 );\n\n\t\t// Filters we'll only run when view as links are called.\n\t\tif ( isset( $_GET['llms-view-as'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended\n\n\t\t\tadd_filter( 'llms_is_course_complete', array( $this, 'modify_completion' ), 10, 1 );\n\t\t\tadd_filter( 'llms_is_lesson_complete', array( $this, 'modify_completion' ), 10, 1 );\n\t\t\tadd_filter( 'llms_is_track_complete', array( $this, 'modify_completion' ), 10, 1 );\n\n\t\t\tadd_filter( 'llms_get_enrollment_status', array( $this, 'modify_enrollment_status' ), 10, 1 );\n\n\t\t\tadd_filter( 'llms_display_free_enroll_form', array( $this, 'modify_display_free_enroll_form' ), 10, 1 );\n\n\t\t\tadd_filter( 'llms_display_student_dashboard', array( $this, 'modify_dashboard' ), 10, 1 );\n\t\t\tadd_filter( 'llms_hide_registration_form', array( $this, 'modify_dashboard' ), 10, 1 );\n\t\t\tadd_filter( 'llms_enable_open_registration', array( $this, 'enable_open_reg' ), 10, 1 );\n\t\t\tadd_filter( 'llms_hide_login_form', array( $this, 'modify_dashboard' ), 10, 1 );\n\n\t\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'scripts' ) );\n\n\t\t}\n\t}\n\n\t/**\n\t * Add view links to the admin menu bar for qualifying users.\n\t *\n\t * @since 3.7.0\n\t * @since 3.16.0 Unknown.\n\t * @since 4.2.0 Updated icon.\n\t * @since 4.5.1 Use `should_display()` method to determine if the view manager should be added to the admin bar.\n\t * @since 4.16.0 Retrieve nodes to add from `get_menu_items_to_add()`.\n\t *\n\t * @param WP_Admin_Bar $wp_admin_bar Admin bar class instance.\n\t * @return void\n\t */\n\tpublic function add_menu_items( $wp_admin_bar ) {\n\n\t\tif ( ! $this->should_display() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tforeach ( $this->get_menu_items_to_add() as $node ) {\n\t\t\t$wp_admin_bar->add_node( $node );\n\t\t}\n\t}\n\n\t/**\n\t * Forces open registration on when previewing the registration form\n\t *\n\t * If open registration is disabled, adds an action to output an info notice at the start\n\t * of the form alerting users that they're viewing a preview.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $status Current open registration status.\n\t * @return string\n\t */\n\tpublic function enable_open_reg( $status ) {\n\n\t\tif ( ! llms_parse_bool( $status ) ) {\n\t\t\tadd_action( 'lifterlms_register_form_start', array( $this, 'open_reg_notice' ) );\n\t\t}\n\n\t\treturn 'yes';\n\t}\n\n\t/**\n\t * Inline JS.\n\t *\n\t * Updates links so admins can navigate around quickly when \"viewing as\".\n\t *\n\t * @since 3.7.0\n\t * @since 3.35.0 Sanitize `$_GET` data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string\n\t */\n\tprivate function get_inline_script() {\n\t\tob_start();\n\t\t?>\n\t\twindow.llms.ViewManager.set_nonce( '<?php echo esc_js( llms_filter_input_sanitize_string( INPUT_GET, 'view_nonce' ) ); ?>' ).set_view( '<?php echo esc_js( $this->get_view() ); ?>' ).update_links();\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Retrieve an array of nodes to be added to the admin bar\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return array[] An array of arrays formatted to be passed to `WP_Admin_Bar::add_node()`.\n\t */\n\tprivate function get_menu_items_to_add() {\n\n\t\t$nodes  = array();\n\t\t$view   = $this->get_view();\n\t\t$views  = $this->get_views();\n\t\t$top_id = 'llms-view-as-menu';\n\n\t\t// Translators: %s = View manager role name.\n\t\t$title = sprintf( __( 'Viewing as %s', 'lifterlms' ), $views[ $view ] );\n\n\t\t// Add the top-level node.\n\t\t$nodes[] = array(\n\t\t\t'id'     => $top_id,\n\t\t\t'parent' => 'top-secondary',\n\t\t\t'title'  => '<span class=\"ab-icon\"><img src=\"' . llms()->plugin_url() . '/assets/images/lifterlms-icon.png\" style=\"height:17px;margin-top:3px;opacity:0.65;\" alt=\"LifterLMS\"></span>' . $title,\n\t\t);\n\n\t\t// Add view as links.\n\t\tforeach ( $views as $role => $name ) {\n\n\t\t\t// Exclude the current view.\n\t\t\tif ( $role === $view ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$nodes[] = array(\n\t\t\t\t'href'   => self::get_url( $role ),\n\t\t\t\t'id'     => 'llms-view-as--' . $role,\n\t\t\t\t'parent' => $top_id,\n\t\t\t\t// Translators: %s = View manager role name.\n\t\t\t\t'title'  => sprintf( __( 'View as %s', 'lifterlms' ), $name ),\n\t\t\t);\n\n\t\t}\n\n\t\treturn $nodes;\n\t}\n\n\t/**\n\t * Get a view url for the requested view.\n\t *\n\t * @since 3.7.0\n\t * @since 4.2.0 Take into account already present query args. e.g. ?plan=X.\n\t * @since 4.16.0 Changed method signature to add the `$href` parameter and changed access from private to public static.\n\t *\n\t * @param string       $role Role to view the screen as. Accepts \"self\", \"visitor\", or \"student\".\n\t * @param string|false $href Optional. The URL to create a URL for. If `false`, uses `$_SERVER['REQUEST_URI']`.\n\t * @param array        $args Optional. Additional query args to add to the url. Default empty array.\n\t * @return string\n\t */\n\tpublic static function get_url( $role, $href = false, $args = array() ) {\n\n\t\t// If we want to view as \"self\" we should remove the query vars (if they're set).\n\t\tif ( 'self' === $role ) {\n\t\t\treturn remove_query_arg( array( 'llms-view-as', 'view_nonce' ), $href );\n\t\t}\n\n\t\t// Create a new URL.\n\t\t$args['llms-view-as'] = $role;\n\t\t$href                 = add_query_arg( $args, $href );\n\t\treturn html_entity_decode( esc_url( wp_nonce_url( $href, 'llms-view-as', 'view_nonce' ) ) );\n\t}\n\n\t/**\n\t * Get the current view role/type.\n\t *\n\t * @since 3.7.0\n\t * @since 3.35.0 Sanitize `$_GET` data.\n\t * @since 4.16.0 Don't access `$_GET` directly, use `llms_filter_input()`.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return string\n\t */\n\tprivate function get_view() {\n\n\t\tif ( ! isset( $_REQUEST['view_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['view_nonce'] ) ), 'llms-view-as' ) ) {\n\t\t\treturn 'self';\n\t\t}\n\n\t\t// Ensure it's a valid view.\n\t\t$views = $this->get_views();\n\t\t$view  = llms_filter_input( INPUT_GET, 'llms-view-as' );\n\t\tif ( ! $view || ! isset( $views[ $view ] ) ) {\n\t\t\treturn 'self';\n\t\t}\n\n\t\treturn $view;\n\t}\n\n\t/**\n\t * Test get_views() method\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_views() {\n\t\t$this->assertEquals( array( 'self', 'visitor', 'student' ), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_views' ) ) );\n\t}\n\n\t/**\n\t * Get a list of available views.\n\t *\n\t * @since 3.7.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_views() {\n\t\treturn array(\n\t\t\t'self'    => __( 'Myself', 'lifterlms' ),\n\t\t\t'visitor' => __( 'Visitor', 'lifterlms' ),\n\t\t\t'student' => __( 'Student', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Modify the completion status of course, lessons, tracks based on current view.\n\t *\n\t * Visitors and students will always show content as not completed.\n\t *\n\t * @since 3.7.0\n\t *\n\t * @param boolean $completed The actual status for the current user.\n\t * @return boolean\n\t */\n\tpublic function modify_completion( $completed ) {\n\t\tswitch ( $this->get_view() ) {\n\t\t\tcase 'visitor':\n\t\t\t\t$status = false;\n\t\t\t\tbreak;\n\t\t\tcase 'student':\n\t\t\t\t$status = true;\n\t\t\t\tbreak;\n\t\t}\n\t\treturn $completed;\n\t}\n\n\t/**\n\t * Modify the status of a course access period based on the current view.\n\t *\n\t * Students and Visitors will see the actual access period.\n\t *\n\t * If viewing as self and self can bypass restrictions will appear as if course is open.\n\t *\n\t * @since 3.7.0\n\t * @since 5.9.0 Pass the course ID to `llms_can_user_bypass_restrictions()`.\n\t *\n\t * @param boolean $status The default status.\n\t * @return boolean\n\t */\n\tpublic function modify_course_open( $status, $course ) {\n\n\t\tif (\n\t\t\t'self' === $this->get_view() &&\n\t\t\tllms_can_user_bypass_restrictions( get_current_user_id(), $course->get( 'id' ) )\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn $status;\n\t}\n\n\t/**\n\t * Modify the student dashboard\n\t *\n\t * @since 4.16.0\n\t *\n\t * @param boolean $value Default value from the filter.\n\t * @return boolean\n\t */\n\tpublic function modify_dashboard( $value ) {\n\n\t\tswitch ( $this->get_view() ) {\n\n\t\t\tcase 'visitor':\n\t\t\t\t$value = false;\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$value = true;\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn $value;\n\t}\n\n\t/**\n\t * Modify the enrollment status of current user based on the view.\n\t *\n\t * Students will always show as enrolled.\n\t *\n\t * Visitors will always show as not-enrolled.\n\t *\n\t * @since 3.7.0\n\t *\n\t * @param string $status The actual status for the current user.\n\t * @return string\n\t */\n\tpublic function modify_enrollment_status( $status ) {\n\n\t\tswitch ( $this->get_view() ) {\n\n\t\t\tcase 'visitor':\n\t\t\t\t$status = false;\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$status = 'enrolled';\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $status;\n\t}\n\n\t/**\n\t * Modify the displaying of the free enroll form (free access plans).\n\t *\n\t * Visitors will never be shown the free enroll form.\n\t *\n\t * @since 4.2.0\n\t *\n\t * @param bool $display Whether or not the form is being displayed.\n\t * @return bool\n\t */\n\tpublic function modify_display_free_enroll_form( $display ) {\n\n\t\tif ( ! $display || 'visitor' === $this->get_view() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $display;\n\t}\n\n\t/**\n\t * Modify llms_page_restricted for qualifying users to allow them to bypass restrictions.\n\t *\n\t * @since 3.7.0\n\t * @since 5.9.0 Pass the course ID to `llms_can_user_bypass_restrictions()`.\n\t *\n\t * @param array $restrictions Restriction data.\n\t * @return array\n\t */\n\tpublic function modify_restrictions( $restrictions ) {\n\n\t\tif (\n\t\t\t'self' === $this->get_view() &&\n\t\t\tllms_can_user_bypass_restrictions( get_current_user_id(), $restrictions['restriction_id'] )\n\t\t) {\n\n\t\t\t$restrictions['is_restricted'] = false;\n\t\t\t$restrictions['reason']        = 'role-access';\n\n\t\t}\n\n\t\treturn $restrictions;\n\t}\n\n\t/**\n\t * Output a notice alerting users that open registration is currently disabled\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Added missing textdomain.\n\t *\n\t * @return void\n\t */\n\tpublic function open_reg_notice() {\n\t\tllms_print_notice( __( 'This is a preview of the Open Registration form but Open Registration is currently disabled. Enable Open Registration to allow users to create accounts on this page.', 'lifterlms' ), 'debug' );\n\t}\n\n\t/**\n\t * Enqueue Scripts.\n\t *\n\t * @since 3.7.0\n\t * @since 3.17.8 Unknown.\n\t * @since 3.35.0 Declare asset version.\n\t *\n\t * @return void\n\t */\n\tpublic function scripts() {\n\n\t\t// If it's self we don't need anything fancy going on here.\n\t\tif ( 'self' === $this->get_view() ) {\n\t\t\treturn;\n\t\t}\n\n\t\twp_enqueue_script( 'llms-view-manager', LLMS_PLUGIN_URL . '/assets/js/llms-view-manager' . LLMS_ASSETS_SUFFIX . '.js', array( 'jquery' ), llms()->version, true );\n\t\twp_add_inline_script( 'llms-view-manager', $this->get_inline_script(), 'after' );\n\t}\n\n\n\t/**\n\t * Determine whether or not the view manager should be added to the WP Admin Bar\n\t *\n\t * The view manager is only displayed when the following criteria is met:\n\t * + The current user must have a role that is allowed to bypass LifterLMS restrictions\n\t * + Must be viewing one of the following:\n\t *   + a single course, lesson, membership, or quiz\n\t *   + LifterLMS checkout page\n\t *   + LifterLMS student dashboard page\n\t *\n\t * @since 4.5.1\n\t * @since 4.16.0 Display on the student dashboard.\n\t * @since 5.9.0 When possible, pass the post ID to `llms_can_user_bypass_restrictions()`.\n\t *\n\t * @return boolean\n\t */\n\tprotected function should_display() {\n\n\t\t$display = false;\n\n\t\tglobal $post;\n\t\t$is_restricted_post = $post && ( is_llms_checkout() || is_llms_account_page() || in_array( $post->post_type, array( 'course', 'lesson', 'llms_membership', 'llms_quiz' ), true ) );\n\t\t$post_id            = $is_restricted_post ? $post->ID : null;\n\t\tif ( llms_can_user_bypass_restrictions( get_current_user_id(), $post_id ) ) {\n\t\t\t$display = is_admin() || is_post_type_archive() || ! $post || ! $is_restricted_post ? false : true;\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not the \"View As...\" menu item should be displayed in the WP Admin Bar\n\t\t *\n\t\t * @since 4.5.1\n\t\t *\n\t\t * @param boolean $display Whether or not the menu item should be displayed.\n\t\t */\n\t\treturn apply_filters( 'llms_view_manager_should_display', $display );\n\t}\n}\n\nreturn new LLMS_View_Manager();\n"
  },
  {
    "path": "includes/class.llms.voucher.php",
    "content": "<?php\n/**\n * Voucher Class\n *\n * @package LifterLMS/Classes\n *\n * @since 2.0.0\n * @version 3.37.17\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Voucher class\n *\n * @since 2.0.0\n * @since 3.27.0 Unknown.\n * @since 3.37.17 Only allow vouchers to be used if the voucher post is \"published\".\n */\nclass LLMS_Voucher {\n\n\t// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared\n\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t/**\n\t * ID of the voucher\n\t * This will be a LifterLMS Voucher custom post type Post ID\n\t *\n\t * @var int\n\t */\n\tprotected $id;\n\n\n\t/**\n\t * Unprefixed name of the vouchers codes table\n\t *\n\t * @var string\n\t */\n\tprotected $codes_table_name = 'lifterlms_vouchers_codes';\n\n\t/**\n\t * Unprefixed name of the product to voucher xref table\n\t *\n\t * @var string\n\t */\n\tprotected $product_to_voucher_table = 'lifterlms_product_to_voucher';\n\n\t/**\n\t * Unprefixed name of the voucher redemptions table\n\t *\n\t * @var string\n\t */\n\tprotected $redemptions_table = 'lifterlms_voucher_code_redemptions';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param int $id WP_Post ID of the voucher.\n\t * @return void\n\t */\n\tpublic function __construct( $id = null ) {\n\t\t$this->id = $id;\n\t}\n\n\t/**\n\t * Retrieve the prefixed database table name for the table where voucher codes are stored\n\t *\n\t * @since 2.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_codes_table_name() {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->prefix . $this->codes_table_name;\n\t}\n\n\t/**\n\t * Retrieve the prefixed database table name where voucher to product relationships are stored\n\t *\n\t * @since 2.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_product_to_voucher_table_name() {\n\n\t\tglobal $wpdb;\n\t\treturn $wpdb->prefix . $this->product_to_voucher_table;\n\t}\n\n\t/**\n\t * Retrieve the prefixed database table name where voucher redemptions are stored\n\t *\n\t * @since 2.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_redemptions_table_name() {\n\n\t\tglobal $wpdb;\n\t\treturn $wpdb->prefix . $this->redemptions_table;\n\t}\n\n\t/**\n\t * Get voucher title\n\t *\n\t * @since 2.0.0\n\t * @since 3.6.2 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_voucher_title() {\n\t\treturn get_the_title( $this->id );\n\t}\n\n\t/**\n\t * Get a single voucher code by id\n\t *\n\t * @since 2.0.0\n\t *\n\t * @return obj\n\t */\n\tpublic function get_voucher_by_voucher_id() {\n\n\t\tglobal $wpdb;\n\n\t\t$table = $this->get_codes_table_name();\n\n\t\t$query = \"SELECT * FROM $table WHERE `voucher_id` = $this->id AND `is_deleted` = 0 LIMIT 1\";\n\t\treturn $wpdb->get_row( $query );\n\t}\n\n\t/**\n\t * Get a single voucher code by string\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param string $code Voucher code string.\n\t * @return obj\n\t */\n\tpublic function get_voucher_by_code( $code ) {\n\n\t\tglobal $wpdb;\n\n\t\t$table          = $this->get_codes_table_name();\n\t\t$redeemed_table = $this->get_redemptions_table_name();\n\n\t\t$sql = $wpdb->prepare(\n\t\t\t\"SELECT c.*, count(r.id) as used\n                  FROM $table as c\n                  LEFT JOIN $redeemed_table as r\n                  ON c.`id` = r.`code_id`\n                  WHERE c.`code` = %s AND c.`is_deleted` = 0\n                  GROUP BY c.`id`\n                  LIMIT 1\",\n\t\t\t$code\n\t\t);\n\t\treturn $wpdb->get_row( $sql );\n\t}\n\n\t/**\n\t * Get a list of voucher codes\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param string $format Return format.\n\t * @return array\n\t */\n\tpublic function get_voucher_codes( $format = 'OBJECT' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$table          = $this->get_codes_table_name();\n\t\t$redeemed_table = $this->get_redemptions_table_name();\n\n\t\t$query = \"SELECT c.*, count(r.id) as used\n                  FROM $table as c\n                  LEFT JOIN $redeemed_table as r\n                  ON c.`id` = r.`code_id`\n                  WHERE `voucher_id` = $this->id AND `is_deleted` = 0\n                  GROUP BY c.id\";\n\t\treturn $wpdb->get_results( $query, $format );\n\t}\n\n\t/**\n\t * Retrieve a voucher by ID.\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param int $code_id Voucher code ID.\n\t * @return object\n\t */\n\tpublic function get_voucher_code_by_code_id( $code_id ) {\n\n\t\tglobal $wpdb;\n\n\t\t$table = $this->get_codes_table_name();\n\n\t\t$query = $wpdb->prepare( 'SELECT * FROM $table WHERE `id` = %d AND `is_deleted` = 0 LIMIT 1', $code_id );\n\t\treturn $wpdb->get_row( $query );\n\t}\n\n\t/**\n\t * Save a voucher code\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param array $data Voucher data.\n\t * @return int|false The number of rows inserted, or false on error.\n\t */\n\tpublic function save_voucher_code( $data ) {\n\n\t\tglobal $wpdb;\n\n\t\t$data['voucher_id'] = $this->id;\n\t\t$data['created_at'] = date( 'Y-m-d H:i:s' );\n\t\t$data['updated_at'] = date( 'Y-m-d H:i:s' );\n\n\t\treturn $wpdb->insert( $this->get_codes_table_name(), $data );\n\t}\n\n\t/**\n\t * Update a voucher code.\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param array $data Array of voucher data.\n\t * @return int|bool The number of rows updated, or false on error.\n\t */\n\tpublic function update_voucher_code( $data ) {\n\n\t\tglobal $wpdb;\n\n\t\t$data['updated_at'] = date( 'Y-m-d H:i:s' );\n\n\t\t$where = array(\n\t\t\t'id' => $data['id'],\n\t\t);\n\t\tunset( $data['id'] );\n\t\treturn $wpdb->update( $this->get_codes_table_name(), $data, $where );\n\t}\n\n\t/**\n\t * Delete a voucher code.\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param int $id Voucher code id.\n\t * @return int}bool The number of rows updated, or false on error.\n\t */\n\tpublic function delete_voucher_code( $id ) {\n\n\t\tglobal $wpdb;\n\n\t\t$data['updated_at'] = date( 'Y-m-d H:i:s' );\n\t\t$data['is_deleted'] = 1;\n\n\t\t$where = array(\n\t\t\t'id' => $id,\n\t\t);\n\t\tunset( $data['id'] );\n\t\treturn $wpdb->update( $this->get_codes_table_name(), $data, $where );\n\t}\n\n\t/**\n\t * Determine if a voucher is valid\n\t *\n\t * @since 2.0.0\n\t * @since 3.0.0 Unknown.\n\t * @since 3.37.17 Ensure the code's parent post is published.\n\t *\n\t * @param string $code Voucher code.\n\t * @return WP_Error|object WP_Error if invalid or not redeemable OR a voucher data object.\n\t */\n\tpublic function check_voucher( $code ) {\n\n\t\t$voucher = $this->get_voucher_by_code( $code );\n\n\t\tif ( empty( $voucher ) ) {\n\n\t\t\treturn new WP_Error( 'not-found', sprintf( __( 'Voucher code \"%s\" could not be found.', 'lifterlms' ), $code ) );\n\n\t\t} elseif ( $voucher->redemption_count <= $voucher->used ) {\n\n\t\t\treturn new WP_Error( 'max', sprintf( __( 'Voucher code \"%s\" has already been redeemed the maximum number of times.', 'lifterlms' ), $code ) );\n\n\t\t} elseif ( '1' === $voucher->is_deleted || 'publish' !== get_post_status( $voucher->voucher_id ) ) { // @todo because get_voucher_code() adds `is_deleted=0` we should never get here, I think.\n\n\t\t\treturn new WP_Error( 'deleted', sprintf( __( 'Voucher code \"%s\" is no longer valid.', 'lifterlms' ), $code ) );\n\n\t\t}\n\n\t\treturn $voucher;\n\t}\n\n\t/**\n\t * Attempt to redeem a voucher for a user with a code\n\t *\n\t * @since 2.0.0\n\t * @since 3.27.0 Unknown.\n\t *\n\t * @param string $code    Voucher code of the voucher being redeemed.\n\t * @param int    $user_id WP_User ID of the user redeeming the voucher.\n\t * @return bool|WP_Error Error object on failure, `true` when successful.\n\t */\n\tpublic function use_voucher( $code, $user_id ) {\n\n\t\t$code = sanitize_text_field( $code );\n\n\t\t$voucher = $this->check_voucher( $code );\n\n\t\tif ( ! is_wp_error( $voucher ) ) {\n\n\t\t\t$this->id = $voucher->voucher_id;\n\n\t\t\t// Ensure the user hasn't already redeemed this voucher.\n\t\t\tif ( $this->get_redemptions_for_code_by_user( $voucher->id, $user_id ) ) {\n\n\t\t\t\treturn new WP_Error( 'error', __( 'You have already redeemed this voucher.', 'lifterlms' ) );\n\n\t\t\t}\n\n\t\t\t// Get products linked to the voucher.\n\t\t\t$products = $this->get_products();\n\n\t\t\tif ( ! empty( $products ) ) {\n\n\t\t\t\t// Loop through all of them and attempt enrollment.\n\t\t\t\tforeach ( $products as $product ) {\n\n\t\t\t\t\tllms_enroll_student( $user_id, $product, 'voucher' );\n\n\t\t\t\t}\n\n\t\t\t\t/**\n\t\t\t\t * Perform action before voucher redeemed.\n\t\t\t\t *\n\t\t\t\t * Action to perform before the voucher redeemed.\n\t\t\t\t *\n\t\t\t\t * @since 2.2.1\n\t\t\t\t * @since 3.24.1 Added $voucher_title parameter.\n\t\t\t\t * @since 3.27.0 Changed $voucher_title to $voucher_code to fix undefined property notice.\n\t\t\t\t *\n\t\t\t\t * @param int    $voucher_id   Voucher id of the voucher being redeemed.\n\t\t\t\t * @param int    $user_id      WP_User ID of the user redeeming the voucher.\n\t\t\t\t * @param string $voucher_code Voucher code of the voucher being redeemed.\n\t\t\t\t */\n\t\t\t\tdo_action( 'llms_voucher_used', $voucher->id, $user_id, $voucher->code );\n\n\t\t\t\t// Use voucher code.\n\t\t\t\t$data = array(\n\t\t\t\t\t'user_id' => $user_id,\n\t\t\t\t\t'code_id' => $voucher->id,\n\t\t\t\t);\n\t\t\t\t$this->save_redeemed_code( $data );\n\n\t\t\t\treturn true;\n\n\t\t\t}\n\t\t} else {\n\n\t\t\treturn $voucher;\n\n\t\t}\n\t}\n\n\t/**\n\t * Redeemed Codes\n\t *\n\t * @since 2.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_redeemed_codes( $format = 'OBJECT' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$table          = $this->get_codes_table_name();\n\t\t$redeemed_table = $this->get_redemptions_table_name();\n\t\t$users_table    = $wpdb->prefix . 'users';\n\n\t\t$query = \"SELECT r.`id`, c.`id` as code_id, c.`voucher_id`, c.`code`, c.`redemption_count`, r.`user_id`, u.`user_email`, r.`redemption_date`\n                  FROM $table as c\n                  JOIN $redeemed_table as r\n                  ON c.`id` = r.`code_id`\n                  JOIN $users_table as u\n                  ON r.`user_id` = u.`ID`\n                  WHERE c.`is_deleted` = 0 AND c.`voucher_id` = $this->id\";\n\n\t\treturn $wpdb->get_results( $query, $format );\n\t}\n\n\t/**\n\t * Retrieve the number of times a voucher was redeemed by a specific user\n\t *\n\t * Hint, it should always be 1 or 0\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param int $code_id Voucher Code ID from wp_lifterlms_vouchers_codes table.\n\t * @param int $user_id User ID from wp_users tables.\n\t * @return int\n\t */\n\tpublic function get_redemptions_for_code_by_user( $code_id, $user_id ) {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT count(id) FROM {$this->get_redemptions_table_name()} WHERE user_id = %d and code_id = %d\", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\t\tarray( $user_id, $code_id )\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Save redeemed code\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param array $data Voucher data.\n\t * @return int|false The number of rows updated, or false on error.\n\t */\n\tpublic function save_redeemed_code( $data ) {\n\n\t\tglobal $wpdb;\n\n\t\t$data['redemption_date'] = date( 'Y-m-d H:i:s' );\n\n\t\treturn $wpdb->insert( $this->get_redemptions_table_name(), $data );\n\t}\n\n\t/**\n\t * Get an  array of IDs for products associated with this voucher\n\t *\n\t * @since 2.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $post_type Allows filtering of products by post type.\n\t * @return array\n\t */\n\tpublic function get_products( $post_type = 'any' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$table = $this->get_product_to_voucher_table_name();\n\n\t\t$products = $wpdb->get_col( $wpdb->prepare( \"SELECT product_id FROM {$table} WHERE `voucher_id` = %d;\", $this->id ) ); //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\tif ( ! empty( $products ) ) {\n\n\t\t\t// Filter any products that don't match the supplied post type.\n\t\t\tif ( 'any' !== $post_type ) {\n\t\t\t\tforeach ( $products as $i => $id ) {\n\t\t\t\t\tif ( get_post_type( $id ) !== $post_type ) {\n\t\t\t\t\t\tunset( $products[ $i ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Convert all elements to ints.\n\t\t\t$products = array_map( 'intval', $products );\n\n\t\t}\n\n\t\treturn $products;\n\t}\n\n\t/**\n\t * Determine if the product is linked to a voucher by code\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param string $code       Voucher code string.\n\t * @param int    $product_id WP_Post ID of the product (course or membership).\n\t * @return boolean\n\t */\n\tpublic function is_product_to_voucher_link_valid( $code, $product_id ) {\n\n\t\t$voucher = $this->check_voucher( $code );\n\n\t\tif ( $voucher ) {\n\t\t\t$this->id = $voucher->voucher_id;\n\n\t\t\t$products = $this->get_products();\n\n\t\t\tif ( ! empty( $products ) && in_array( $product_id, $products ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Dupcheck generated voucher codes.\n\t *\n\t * @since 2.0.0\n\t * @since 3.35.0 Prepare SQL.\n\t *\n\t * @param string[] $codes Array of voucher code strings.\n\t * @return boolean\n\t */\n\tpublic function is_code_duplicate( $codes ) {\n\n\t\tglobal $wpdb;\n\t\t$codes_as_string = join( '\",\"', $codes );\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$codes = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT code\n             FROM {$this->get_codes_table_name()}\n             WHERE code IN ( {$codes_as_string} )\n               AND voucher_id != %d\",\n\t\t\t\tarray( $this->id )\n\t\t\t),\n\t\t\tARRAY_A\n\t\t);\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\tif ( count( $codes ) ) {\n\t\t\treturn $codes;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Save products to a voucher\n\t *\n\t * @since 2.0.0\n\t *\n\t * @param int $product_id WP_Post ID of the product (course or membership).\n\t * @return int|false The number of rows updated, or false on error.\n\t */\n\tpublic function save_product( $product_id ) {\n\n\t\tglobal $wpdb;\n\n\t\t$data['voucher_id'] = $this->id;\n\t\t$data['product_id'] = $product_id;\n\n\t\treturn $wpdb->insert( $this->get_product_to_voucher_table_name(), $data );\n\t}\n\n\t/**\n\t * Delete products from a voucher\n\t *\n\t * @since 2.0.0\n\t *\n\t * @return int\n\t */\n\tpublic function delete_products() {\n\n\t\tglobal $wpdb;\n\n\t\treturn $wpdb->delete(\n\t\t\t$this->get_product_to_voucher_table_name(),\n\t\t\tarray(\n\t\t\t\t'voucher_id' => $this->id,\n\t\t\t)\n\t\t);\n\t}\n}\n\n// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared\n// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n"
  },
  {
    "path": "includes/controllers/class-llms-controller-awards.php",
    "content": "<?php\n/**\n * LLMS_Controller_Awards class\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 6.0.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Callback controller for award posts.\n *\n * Handles actions for `llms_my_achievement` and `llms_my_certificate` post types.\n *\n * @since 6.0.0\n */\nclass LLMS_Controller_Awards {\n\n\t/**\n\t * List of supported post types.\n\t *\n\t * @var string[]\n\t */\n\tprivate static $post_types = array(\n\t\t'llms_my_achievement',\n\t\t'llms_my_certificate',\n\t);\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 6.0.0\n\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tforeach ( self::$post_types as $post_type ) {\n\n\t\t\t$unprefixed = self::strip_prefix( $post_type );\n\n\t\t\tadd_action( \"llms_user_earned_{$unprefixed}\", array( __CLASS__, 'on_earn' ), 20, 2 );\n\t\t\tadd_action( \"save_post_{$post_type}\", array( __CLASS__, 'on_save' ), 20 );\n\n\t\t}\n\n\t\tadd_action( 'rest_after_insert_llms_my_certificate', array( __CLASS__, 'on_rest_insert' ), 20, 3 );\n\n\t}\n\n\t/**\n\t * Convert post type to award type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $post_type A post type string.\n\t * @return string\n\t */\n\tprivate static function strip_prefix( $post_type ) {\n\t\treturn llms_strip_prefixes( $post_type, array( 'llms_my_' ) );\n\t}\n\n\t/**\n\t * Retrieves the award post model for the given WP_Post.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param integer $post_id WP_Post ID.\n\t * @return boolean|LLMS_User_Achievement|LLMS_User_Certificate Returns `false` for invalid post types.\n\t *                                                             Otherwise returns the post model object.\n\t */\n\tprivate static function get_object( $post_id ) {\n\n\t\t$post_type = get_post_type( $post_id );\n\t\tif ( ! in_array( $post_type, self::$post_types, true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$class_name = sprintf( 'LLMS_User_%s', ucwords( self::strip_prefix( $post_type ) ) );\n\t\treturn new $class_name( $post_id );\n\n\t}\n\n\t/**\n\t * Records a timestamp when the award is earned.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $user_id WP_User ID of the user who earned the certificate.\n\t * @param int $post_id WP_Post ID of the certificate post.\n\t * @return boolean|string Returns `false` if the certificate could not be loaded, otherwise returns the current\n\t *                        timestamp in MySQL format.\n\t */\n\tpublic static function on_earn( $user_id, $post_id ) {\n\n\t\t$obj = self::get_object( $post_id );\n\t\tif ( ! $obj ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$ts = llms_current_time( 'mysql' );\n\t\t$obj->set( 'awarded', $ts );\n\n\t\treturn $ts;\n\n\t}\n\n\t/**\n\t * Awarded certificate REST API insertion callback.\n\t *\n\t * Automatically syncs an awarded certificate with its parent when inserted via the REST API and\n\t * sets a unique post name (slug).\n\t *\n\t * This method relies on the fact that there is (currently) no native way to insert an awarded\n\t * certificate into the database via the REST API with a linked parent template without using the\n\t * `AwardCertificateButton` Javascript component. The component sets the parent and student and allows\n\t * this callback function to perform the remaining (necessary) sync operations.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param stdClass        $post     The post object.\n\t * @param WP_Rest_Request $request  Rest request object.\n\t * @param boolean         $creating Whether or not the post is being created.\n\t * @return integer Returns an integer, primarily for unit tests: `0` if the insertion is an update,\n\t *                 `1` if the post has not parent, and `2` when the certificate is synced and updated.\n\t */\n\tpublic static function on_rest_insert( $post, $request, $creating ) {\n\n\t\tif ( ! $creating ) {\n\t\t\treturn 0;\n\t\t}\n\n\t\t$cert = self::get_object( $post->ID );\n\t\tif ( ! $cert->get( 'parent' ) ) {\n\t\t\treturn 1;\n\t\t}\n\n\t\tadd_filter( 'llms_certificate_merge_data', array( __CLASS__, 'on_rest_insert_merge_data' ) );\n\n\t\t$cert->sync( 'create' );\n\t\t$cert->set( 'name', llms()->certificates()->get_unique_slug( $cert->get( 'title' ) ) );\n\n\t\tremove_filter( 'llms_certificate_merge_data', array( __CLASS__, 'on_rest_insert_merge_data' ) );\n\n\t\treturn 2;\n\n\t}\n\n\t/**\n\t * Modifies the merge data used when awarded a certificate using the REST API.\n\t *\n\t * This removes the `{sequential_id}` merge data. When creating the draft we don't want to use\n\t * the default `1` or whatever the parent's ID is. If we use `1` that's just generally incorrect and\n\t * if we use the parent's ID it might become the wrong ID by the time the certificate is published / awarded.\n\t *\n\t * Removing this will not merge the ID but on awarding the ID will be automatically merged with other merge codes.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $merge_data Merge data.\n\t * @return array\n\t */\n\tpublic static function on_rest_insert_merge_data( $merge_data ) {\n\t\tunset( $merge_data['{sequential_id}'] );\n\t\treturn $merge_data;\n\t}\n\n\t/**\n\t * Callback function when a post is saved or updated.\n\t *\n\t * This method automatically merges the certificates `post_content` and additionally triggers\n\t * the creation actions to be fired if the certificate is newly created.\n\t *\n\t * @since 6.0.0\n\t * @since 6.4.0 Added replacement of references to reusable blocks with their actual blocks.\n\t *\n\t * @param int $post_id WP_Post ID of the certificate.\n\t * @return boolean Returns `true` if the certificate can't be loaded, otherwise returns `true`.\n\t */\n\tpublic static function on_save( $post_id ) {\n\n\t\t$obj = self::get_object( $post_id );\n\t\tif ( ! $obj || 'publish' !== $obj->get( 'status' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$post_type = get_post_type( $post_id );\n\n\t\tremove_action( \"save_post_{$post_type}\", array( __CLASS__, 'on_save' ), 20 );\n\n\t\t$is_awarded = $obj->is_awarded();\n\n\t\tif ( 'llms_my_certificate' === $post_type ) {\n\n\t\t\tif ( ! $is_awarded ) {\n\t\t\t\t$obj->update_sequential_id();\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Whenever an awarded certificate is updated, we want to re-merge the content\n\t\t\t * in the event that any shortcodes or merge codes were added.\n\t\t\t */\n\t\t\t$content = $obj->get( 'content', true );\n\t\t\t$obj->set( 'content', $obj->merge_content( $content, true ) );\n\t\t}\n\n\t\t/**\n\t\t * If the award is being published for the first time, trigger the creation actions.\n\t\t */\n\t\tif ( ! $obj->is_awarded() ) {\n\n\t\t\tLLMS_Engagement_Handler::create_actions(\n\t\t\t\tself::strip_prefix( $post_type ),\n\t\t\t\t$obj->get_user_id(),\n\t\t\t\t$post_id,\n\t\t\t\t$obj->get( 'related' ),\n\t\t\t\t$obj->get( 'engagement' )\n\t\t\t);\n\n\t\t}\n\n\t\tadd_action( \"save_post_{$post_type}\", array( __CLASS__, 'on_save' ), 20 );\n\n\t\treturn true;\n\n\t}\n\n}\n\nreturn LLMS_Controller_Awards::init();\n"
  },
  {
    "path": "includes/controllers/class-llms-controller-checkout.php",
    "content": "<?php\n/**\n * LLMS_Controller_Checkout\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 7.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Checkout form controller.\n *\n * Processes orders and interacts with payment gateway classes during checkout.\n *\n * @since 7.0.0\n */\nclass LLMS_Controller_Checkout {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Action for creating a pending order.\n\t *\n\t * Used as both the nonce and the posted `action` field.\n\t */\n\tpublic const ACTION_CREATE_PENDING_ORDER = 'create_pending_order';\n\n\t/**\n\t * Action for confirming a pending order.\n\t *\n\t * Used as both the nonce and the posted `action` field.\n\t */\n\tpublic const ACTION_CONFIRM_PENDING_ORDER = 'confirm_pending_order';\n\n\t/**\n\t * Action for switching the payment source for an order.\n\t *\n\t * Used as the nonce action.\n\t */\n\tpublic const ACTION_SWITCH_PAYMENT_SOURCE = 'llms_switch_order_source';\n\n\t/**\n\t * Query string variable used to identify AJAX order requests.\n\t */\n\tpublic const AJAX_QS_VAR = 'llms-checkout';\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t$actions = array(\n\t\t\t'create_pending_order',\n\t\t\t'confirm_pending_order',\n\t\t\t'switch_payment_source',\n\t\t);\n\t\tforeach ( $actions as $action ) {\n\t\t\tadd_action( 'init', array( $this, \"{$action}_ajax\" ), 5 );\n\t\t\tadd_action( 'init', array( $this, $action ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Checkout confirm order controller.\n\t *\n\t * Called via the confirm order form (via user form submission) or programmatically by payment gateways which\n\t * require an order confirmation step. PayPal is a two-step checkout that requires confirmation\n\t * whereas Stripe is a one-step checkout without a confirmation step.\n\t *\n\t * Validates all submitted data and passes the validated `LLMS_Order` object to the payment gateway's\n\t * `confirm_pending_order()` method for further processing.\n\t *\n\t * If an error is encountered the method short circuits and adds an error notice via {@see llms_add_notice()},\n\t * during gateway processing the same pattern should be observed.\n\t *\n\t * Upon success, gateways should perform a redirect to the appropriate URL (course, membership, etc...).\n\t *\n\t * Note that this method is widely used but the AJAX equivalent, {@see LLMS_Controller_Checkout::confirm_pending_order_ajax()},\n\t * is preferred when implementing a new gateway.\n\t *\n\t * @since 7.0.0 Relocated from `LLMS_Controller_Orders`.\n\t *\n\t * @return null|boolean|void Returns `null` when the form isn't submitted or there's a nonce verification issue.\n\t *                           Returns `false` when the the request is missing the action parameter or the action doesn't match\n\t *                           the expected action. Otherwise there is no/void return.\n\t */\n\tpublic function confirm_pending_order() {\n\n\t\t// Verify form submission.\n\t\t$verify = $this->verify_request( '_wpnonce', self::ACTION_CONFIRM_PENDING_ORDER );\n\t\tif ( ! $verify ) {\n\t\t\treturn $verify;\n\t\t}\n\n\t\t// Ensure we have an order key we can locate the order with.\n\t\t$key = llms_filter_input_sanitize_string( INPUT_POST, 'llms_order_key' );\n\t\tif ( ! $key ) {\n\t\t\treturn llms_add_notice( __( 'Could not locate an order to confirm.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t// Lookup the order & return error if not found.\n\t\t$order = llms_get_order_by_key( $key );\n\t\tif ( ! $order || ! $order instanceof LLMS_Order ) {\n\t\t\treturn llms_add_notice( __( 'Could not locate an order to confirm.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t// Can the order be confirmed?\n\t\tif ( ! $order->can_be_confirmed() ) {\n\t\t\treturn llms_add_notice( __( 'Only pending orders can be confirmed.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t// Get the gateway.\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( $order->get( 'payment_gateway' ) );\n\n\t\t// Pass the order to the gateway.\n\t\t$gateway->confirm_pending_order( $order );\n\n\t}\n\n\t/**\n\t * AJAX checkout confirm order controller.\n\t *\n\t * Verifies the AJAX request, passes `$_POST` data to the `LLMS_Order_Generator`, and outputs a JSON response.\n\t *\n\t * Initiated via the confirm order form (via user form submission) or programmatically by payment gateways which\n\t * require an order confirmation step. PayPal is a two-step checkout that requires confirmation\n\t * whereas Stripe is a one-step checkout without a confirmation step.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return null|boolean|void Returns `null` when the form isn't submitted or there's a nonce verification issue.\n\t *                           Returns `false` when the the request is missing the action parameter or the action doesn't match\n\t *                           the expected action. Otherwise there is no return and a JSON response is output.\n\t */\n\tpublic function confirm_pending_order_ajax() {\n\n\t\t$verify = $this->verify_request( self::AJAX_QS_VAR, self::ACTION_CONFIRM_PENDING_ORDER );\n\t\tif ( ! $verify ) {\n\t\t\treturn $verify;\n\t\t}\n\n\t\t$this->start_ajax( 'confirm_pending_order' );\n\n\t\t// Confirm the order.\n\t\t$generator = new LLMS_Order_Generator( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified via `verify_request()`.\n\t\t$this->send_json( $generator->confirm() );\n\n\t}\n\n\t/**\n\t * Checkout new order controller.\n\t *\n\t * Handles form submission of the checkout form for new (or pending) orders.\n\t *\n\t * Verifies the request, validates request data, creates/updates the user, and creates/updates\n\t * the order post.\n\t *\n\t * If the order is created successfully the order, access plan, student, and coupon data\n\t * is passed to the payment gateway's `handle_pending_order()` method for further processing.\n\t *\n\t * If errors are encountered they are displayed to the user via {@see llms_add_notice()} and execution\n\t * of the method is halted early. Gateways should do the same if they encounter errors during processing.\n\t *\n\t * This method also handles free enrollment form submission from the access plan button (on pricing tables, etc...).\n\t * In the event of validation issues during free enrollment form submission the user is automatically redirect to checkout\n\t * where the validation issues will be displayed.\n\t *\n\t * Upon success the gateway should redirect the user to the relevant next step. For multi-step checkout that\n\t * requires payment confirmation, the user should be redirected to the order confirmation page, for one-step\n\t * gateways assuming the order is moved to active or completed status and enrollment takes place, the user\n\t * should be redirected to the relevant course or membership URL.\n\t *\n\t * @since 7.0.0 Moved from `LLMS_Controller_Orders.\n\t *\n\t * @return null|boolean|void Returns `null` when the form isn't submitted or there's a nonce verification issue.\n\t *                           Returns `false` when the the request is missing the action parameter or the action doesn't match\n\t *                           the expected action. Otherwise there is no/void return.\n\t */\n\tpublic function create_pending_order() {\n\n\t\t$verify = $this->verify_request( '_llms_checkout_nonce', self::ACTION_CREATE_PENDING_ORDER );\n\t\tif ( ! $verify ) {\n\t\t\treturn $verify;\n\t\t}\n\n\t\t@set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged\n\n\t\t/**\n\t\t * Allow 3rd parties to perform their own validation prior to standard validation.\n\t\t *\n\t\t * If this returns a truthy, we'll stop processing.\n\t\t *\n\t\t * The extension should add a notice in addition to returning the truthy.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param boolean $valid Validation status. If `true` ceases checkout execution. If `false` checkout proceeds.\n\t\t */\n\t\tif ( apply_filters( 'llms_before_checkout_validation', false ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$setup_data = $this->extract_setup_data( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified via `verify_request()`.\n\n\t\t// Setup the pending order.\n\t\t$setup = llms_setup_pending_order( $setup_data );\n\t\tif ( is_wp_error( $setup ) ) {\n\n\t\t\tllms_add_notice( $setup->get_error_message(), 'error' );\n\n\t\t\t/*\n\t\t\t * If the free enroll form is being submitted and there were validation issues this will redirect\n\t\t\t * to the checkout page in favor of returning an error.\n\t\t\t */\n\t\t\t$this->maybe_redirect_from_free_enroll_form( $setup_data['plan_id'], llms_filter_input( INPUT_POST, 'form' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified via `verify_request()`.\n\n\t\t\treturn $setup;\n\n\t\t}\n\n\t\t/**\n\t\t * Allow gateways, extensions, etc to do their own validation.\n\t\t *\n\t\t * After all standard validations are successfully.\n\t\t *\n\t\t * If this returns a truthy, we'll stop processing.\n\t\t * The extension should add a notice in addition to returning the truthy.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param boolean $stop_processing When a `true`, we'll stop processing. Default is `false`.\n\t\t */\n\t\tif ( apply_filters( 'llms_after_checkout_validation', false ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$order_id = 'new';\n\n\t\t// Get order ID by Key if it exists.\n\t\tif ( ! empty( $_POST['llms_order_key'] ) ) {  // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified via `verify_request()`.\n\t\t\t$locate = llms_get_order_by_key( llms_filter_input_sanitize_string( INPUT_POST, 'llms_order_key' ), 'id' );\n\t\t\tif ( $locate ) {\n\t\t\t\t$order_id = $locate;\n\t\t\t}\n\t\t}\n\n\t\t// Instantiate the order.\n\t\t$order = new LLMS_Order( $order_id );\n\n\t\t// If there's no id we can't proceed, return an error.\n\t\tif ( ! $order->get( 'id' ) ) {\n\t\t\treturn llms_add_notice( __( 'There was an error creating your order, please try again.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t// Add order key to globals so the order can be retried if processing errors occur.\n\t\t$_POST['llms_order_key'] = $order->get( 'order_key' );\n\n\t\t$order->init( $setup['person'], $setup['plan'], $setup['gateway'], $setup['coupon'] );\n\n\t\t// Pass to the gateway to start processing.\n\t\t$setup['gateway']->handle_pending_order( $order, $setup['plan'], $setup['person'], $setup['coupon'] );\n\n\t}\n\n\t/**\n\t * AJAX checkout new order controller.\n\t *\n\t * Handles AJAX form submission of the checkout form for new (or pending) orders.\n\t *\n\t * Verifies the AJAX request, passes the `$_POST` data to {@see LLMS_Order_Generator::generate},\n\t * hands the resulting order and data to the gateway's `handle_pending_order()` method and then\n\t * outputs a JSON response object.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return null|boolean|void Returns `null` when the form isn't submitted or there's a nonce verification issue.\n\t *                           Returns `false` when the the request is missing the action parameter or the action doesn't match\n\t *                           the expected action. Otherwise there is no return and a JSON response is output.\n\t */\n\tpublic function create_pending_order_ajax() {\n\n\t\t$verify = $this->verify_request( self::AJAX_QS_VAR, self::ACTION_CREATE_PENDING_ORDER );\n\t\tif ( ! $verify ) {\n\t\t\treturn $verify;\n\t\t}\n\n\t\t$this->start_ajax( 'create_pending_order' );\n\n\t\t// Generate the order.\n\t\t$generator = new LLMS_Order_Generator( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified via `verify_request()`.\n\t\t$order     = $generator->generate( $generator::UA_VALIDATE );\n\t\tif ( is_wp_error( $order ) ) {\n\t\t\t$this->send_json( $order );\n\t\t}\n\n\t\t// Pending order creation success, pass it over to the gateway.\n\t\t$handle = $generator->get_gateway()->handle_pending_order(\n\t\t\t$order,\n\t\t\t$generator->get_plan(),\n\t\t\t$generator->get_user_data(),\n\t\t\t$generator->get_coupon()\n\t\t);\n\n\t\t// Automatically add the order key to non-error return arrays.\n\t\tif ( ! is_wp_error( $handle ) ) {\n\t\t\t$handle['order_key'] = $order->get( 'order_key' );\n\t\t}\n\n\t\t$this->send_json( $handle );\n\n\t}\n\n\t/**\n\t * Extracts data from `$_POST` into an array that can be passed into `llms_setup_pending_order()`.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array $posted_data Data array, from `$_POST`.\n\t * @return array\n\t */\n\tprivate function extract_setup_data( $posted_data ) {\n\n\t\t$plan_id = absint( $posted_data['llms_plan_id'] ?? 0 );\n\n\t\t$data = array(\n\t\t\t'plan_id'         => $plan_id,\n\t\t\t'agree_to_terms'  => llms_parse_bool( $posted_data['llms_agree_to_terms'] ?? '' ),\n\t\t\t'coupon_code'     => sanitize_text_field( $posted_data['llms_coupon_code'] ?? '' ),\n\t\t\t'customer'        => $this->extract_user_data( $posted_data, $plan_id ),\n\t\t\t'payment_gateway' => sanitize_text_field( $posted_data['llms_payment_gateway'] ?? '' ),\n\t\t);\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Extracts user registration / update information from a posted data array.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array $posted_data Raw $_POST (or similar) data.\n\t * @return array\n\t */\n\tprivate function extract_user_data( $posted_data, $plan_id ) {\n\n\t\t$user_data = array();\n\t\t$plan      = $plan_id ? llms_get_post( $plan_id ) : false;\n\n\t\t$uid = get_current_user_id();\n\t\tif ( $uid ) {\n\t\t\t$user_data['user_id'] = $uid;\n\t\t}\n\n\t\tforeach ( LLMS_Forms::instance()->get_form_fields( 'checkout', compact( 'plan' ) ) as $field ) {\n\t\t\tif ( isset( $posted_data[ $field['name'] ] ) ) {\n\t\t\t\t$user_data[ $field['name'] ] = $posted_data[ $field['name'] ];\n\t\t\t}\n\t\t}\n\n\t\treturn $user_data;\n\n\t}\n\n\t/**\n\t * Retrieves the AJAX URL for the requested action.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $action A checkout action. Expects a class action constant: `LLMS_Controller_Checkout::ACTION_*`.\n\t * @return string\n\t */\n\tpublic function get_url( $action ) {\n\t\treturn add_query_arg(\n\t\t\tself::AJAX_QS_VAR,\n\t\t\twp_create_nonce( $action ),\n\t\t\tget_site_url()\n\t\t);\n\t}\n\n\t/**\n\t * Handles redirection during {@see LLMS_Controller_Checkout::create_pending_order()} if validation errors are encountered\n\t * via the free checkout/enrollment form.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param int    $plan_id WP_Post ID of the access plan.\n\t * @param string $form    Value of the posted `form`, should be `free_enroll`.\n\t * @return null|bool|void Returns `null` when called in an invalid context, `false` if the supplied access plan ID is invalid,\n\t *                        and `void` when a redirect is performed to the checkout page.\n\t */\n\tprivate function maybe_redirect_from_free_enroll_form( $plan_id, $form ) {\n\n\t\t// Not the free enroll form.\n\t\tif ( ! get_current_user_id() || 'free_enroll' !== $form || ! $plan_id ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Invalid plan submitted.\n\t\t$plan = llms_get_post( $plan_id );\n\t\tif ( ! is_a( $plan, 'LLMS_Access_Plan' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Redirect to the checkout screen.\n\t\tllms_redirect_and_exit( $plan->get_checkout_url() );\n\n\t}\n\n\t/**\n\t * Sends a JSON response.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array|WP_Error $data Response data.\n\t * @return void\n\t */\n\tprivate function send_json( $data ) {\n\t\twp_send_json( $data, is_wp_error( $data ) ? 400 : 200 );\n\t}\n\n\t/**\n\t * Denotes an AJAX request in this method has started.\n\t *\n\t * This method \"alerts\" WordPress that an AJAX request is being processed. This is important, primarily, for\n\t * testing purposes as `wp_send_json()` will call `wp_die()` when doing ajax as opposed to `die()` when not\n\t * doing ajax. This helps us unit test better.\n\t *\n\t * Secondly, this will remove the non-ajax action callback of the method's name ensuring that the non-ajax version doesn't\n\t * run immediately behind the ajax version.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $method Name of the non-ajax method to remove.\n\t * @return void\n\t */\n\tprivate function start_ajax( $method ) {\n\n\t\t// Tell WP we're doing AJAX.\n\t\tadd_filter( 'wp_doing_ajax', '__return_true' );\n\n\t\t// Don't process the non-ajax method.\n\t\tremove_action( 'init', array( $this, $method ) );\n\n\t}\n\n\t/**\n\t * Handle form submission of the \"Update Payment Method\" form on the student dashboard when viewing a single order.\n\t *\n\t * @since 7.0.0 Relocated from `LLMS_Controller_Orders`.\n\t *\n\t * @return void\n\t */\n\tpublic function switch_payment_source() {\n\n\t\t// Invalid nonce or the form wasn't submitted.\n\t\tif ( ! isset( $_REQUEST['_switch_source_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_switch_source_nonce'] ) ), self::ACTION_SWITCH_PAYMENT_SOURCE ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$data = $this->switch_payment_source_setup();\n\t\tif ( is_wp_error( $data ) ) {\n\t\t\treturn llms_add_notice( $data->get_error_message(), 'error' );\n\t\t}\n\n\t\t// Handoff to the gateway.\n\t\tllms()->payment_gateways()->get_gateway_by_id( $data['new_gateway'] )->handle_payment_source_switch( $data['order'], $_POST );\n\n\t\tif ( ! llms_notice_count( 'error' ) ) {\n\t\t\t$this->switch_payment_source_success( $data );\n\t\t}\n\n\t}\n\n\t/**\n\t * Handle ajax payment method switching from the student dashboard.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function switch_payment_source_ajax() {\n\n\t\t// Invalid nonce or the form wasn't submitted.\n\t\tif ( ! isset( $_REQUEST[ self::AJAX_QS_VAR ] ) ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ self::AJAX_QS_VAR ] ) ), self::ACTION_SWITCH_PAYMENT_SOURCE ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$this->start_ajax( 'switch_payment_source' );\n\n\t\t$data = $this->switch_payment_source_setup();\n\t\tif ( is_wp_error( $data ) ) {\n\t\t\t$this->send_json( $data );\n\t\t}\n\n\t\t// Handoff to the gateway.\n\t\t$gateway_res = llms()->payment_gateways()->get_gateway_by_id( $data['new_gateway'] )->handle_payment_source_switch( $data['order'], $_POST );\n\n\t\t$next_action = is_wp_error( $gateway_res ) ?\n\t\t\tfalse :\n\t\t\t/**\n\t\t\t * Filters the next action when switching payment sources.\n\t\t\t *\n\t\t\t * Defaults to `COMPLETE` when gateways don't return a value via `next_action`\n\t\t\t * in the response array.\n\t\t\t *\n\t\t\t * The `COMPLETE` action records the switch, updates the payment method, and changes\n\t\t\t * `pending-cancel` to `active` status.\n\t\t\t *\n\t\t\t * Any other status will do nothing and the gateway should provide it's necessary logic in the\n\t\t\t * {@see LLMS_Payment_Gateway::handle_payment_source_switch()} method.\n\t\t\t *\n\t\t\t * This is used by gateways such as PayPal that require a creation and approval step on PayPal as opposed\n\t\t\t * to a gateway like Stripe that doesn't require end-user approval on the Stripe platform.\n\t\t\t *\n\t\t\t * @since 7.0.0\n\t\t\t *\n\t\t\t * @param type $arg Description.\n\t\t\t */\n\t\t\tapply_filters(\n\t\t\t\t'llms_switch_payment_source_next_action',\n\t\t\t\t$gateway_res['next_action'] ?? 'COMPLETE',\n\t\t\t\t$gateway_res,\n\t\t\t\t$data\n\t\t\t);\n\n\t\tif ( 'COMPLETE' === $next_action ) {\n\t\t\t$this->switch_payment_source_success( $data, true );\n\t\t}\n\n\t\t$this->send_json( $gateway_res );\n\n\t}\n\n\t/**\n\t * Validates and parses user-submitted `$_POST` data during payment source switching.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return WP_Error|array {\n\t *     An error object or an associative array on success.\n\t *\n\t *     @type string     $old_gateway The ID of the order's previous payment gateway.\n\t *     @type string     $new_gateway The ID of the order's new payment gateway.\n\t *     @type LLMS_Order $order       The order object.\n\t * }\n\t */\n\tprivate function switch_payment_source_setup() {\n\n\t\t$order_id = llms_filter_input( INPUT_POST, 'order_id', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( ! $order_id ) {\n\t\t\treturn new WP_Error( 'switch-source-order-missing', __( 'Missing order information.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t$order = llms_get_post( $order_id );\n\t\tif ( ! is_a( $order, 'LLMS_Order' ) || get_current_user_id() !== $order->get( 'user_id' ) ) {\n\t\t\treturn new WP_Error( 'switch-source-order-invalid', __( 'Invalid order.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t$new_gateway = llms_filter_input_sanitize_string( INPUT_POST, 'llms_payment_gateway' );\n\t\tif ( empty( $new_gateway ) ) {\n\t\t\treturn new WP_Error( 'switch-source-gateway-missing', __( 'Missing gateway information.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t$old_gateway = $order->get( 'payment_gateway' );\n\t\t$can_process = llms_can_gateway_be_used_for_plan_or_order( $new_gateway, $order, true );\n\t\tif ( is_wp_error( $can_process ) ) {\n\t\t\treturn $can_process;\n\t\t}\n\n\t\t// Prevent tampering with the form action and ensure the submitted action matches the expected action for the order.\n\t\t$action = llms_filter_input( INPUT_POST, 'llms_switch_action' );\n\t\tif ( empty( $action ) || $order->get_switch_source_action() !== $action ) {\n\t\t\treturn new WP_Error( 'switch-source-action-invalid', __( 'Invalid action.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t// Temporarily store the gateway IDs so the previous values are accessible to the old gateway after the source switch.\n\t\t$order->set(\n\t\t\t'temp_gateway_ids',\n\t\t\t/**\n\t\t\t * Filters the gateway IDs that are temporarily stored during a payment source switch.\n\t\t\t *\n\t\t\t * @since 7.0.0\n\t\t\t *\n\t\t\t * @param array      $temp_ids {\n\t\t\t *     An array of gateway-related IDs to be temporarily cached.\n\t\t\t *\n\t\t\t *     @type string customer     The value of the `gateway_customer_id` property.\n\t\t\t *     @type string source       The value of the `gateway_source_id` property.\n\t\t\t *     @type string subscription The value of the `gateway_subscription_id` property.\n\t\t\t * }\n\t\t\t * @param LLMS_Order $order     The order object.\n\t\t\t */\n\t\t\tapply_filters(\n\t\t\t\t'llms_order_set_temp_gateway_ids',\n\t\t\t\tarray(\n\t\t\t\t\t'customer'     => $order->get( 'gateway_customer_id' ),\n\t\t\t\t\t'source'       => $order->get( 'gateway_source_id' ),\n\t\t\t\t\t'subscription' => $order->get( 'gateway_subscription_id' ),\n\t\t\t\t),\n\t\t\t\t$order\n\t\t\t)\n\t\t);\n\n\t\treturn compact( 'old_gateway', 'new_gateway', 'order' );\n\n\t}\n\n\t/**\n\t * Action run following a successful payment source switch.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array $args Payment switch arguments from {@see LLMS_Controller_Checkout::switch_payment_source_setup()}.\n\t * @param bool  $note If `true`, automatically records an order note for the source the switch.\n\t * @return void\n\t */\n\tprivate function switch_payment_source_success( $args, $note = false ) {\n\n\t\t$order       = $args['order'];\n\t\t$old_gateway = $args['old_gateway'];\n\t\t$new_gateway = $args['new_gateway'];\n\n\t\t$order->set( 'payment_gateway', $new_gateway );\n\n\t\tif ( $note ) {\n\t\t\t$order->add_note(\n\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %1$s = old payment gateway ID; %2$s = new payment gateway ID.\n\t\t\t\t\t__( 'Payment source updated by customer. Payment gateway changed from \"%1$s\" to \"%2$s\".', 'lifterlms' ),\n\t\t\t\t\t$old_gateway,\n\t\t\t\t\t$new_gateway\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// If the order is pending-cancel, reactivate it.\n\t\tif ( 'llms-pending-cancel' === $order->get( 'status' ) ) {\n\t\t\t$order->set_status( 'active' );\n\t\t}\n\n\t\t/**\n\t\t * Action run after an order's payment source is switched.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param LLMS_Order $order       Order object.\n\t\t * @param string     $new_gateway The payment gateway ID of the new gateway.\n\t\t * @param string     $old_gateway The payment gateway ID of the previous gateway.\n\t\t */\n\t\tdo_action( 'llms_order_payment_source_switched', $order, $new_gateway, $old_gateway );\n\n\t\t// Cleanup temp data.\n\t\tdelete_post_meta( $order->get( 'id' ), '_llms_temp_gateway_ids' );\n\n\t}\n\n\t/**\n\t * Verifies an incoming request nonce and posted action field.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $field The nonce field.\n\t * @param string $nonce The nonce & action value.\n\t * @return null|bool Returns `null` if the nonce isn't submitted or can't be verified, `false` if the\n\t *                   action isn't submitted or doesn't match the intended action, and `true` if\n\t *                   the request is verified successfully.\n\t */\n\tprivate function verify_request( $field, $nonce ) {\n\n\t\tif ( ! isset( $_REQUEST[ $field ] ) ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ $field ] ) ), $nonce ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif ( llms_filter_input( INPUT_POST, 'action' ) !== $nonce ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n}\n\nreturn LLMS_Controller_Checkout::instance();\n"
  },
  {
    "path": "includes/controllers/class.llms.controller.achievements.php",
    "content": "<?php\n/**\n * LLMS_Controller_Achievements class\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 3.18.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Handles awarded user achievements.\n *\n * @since 3.18.0\n * @since 3.35.0 Sanitize `$_POST` data.\n * @since 6.0.0 Extended from the LLMS_Abstract_Controller_User_Engagements class.\n */\nclass LLMS_Controller_Achievements extends LLMS_Abstract_Controller_User_Engagements {\n\n\t/**\n\t * Type of user engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $engagement_type = 'achievement';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.18.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tparent::__construct();\n\t\tadd_action( 'init', array( $this, 'maybe_handle_reporting_actions' ) );\n\t}\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Controller_User_Engagements::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\tswitch ( $text_type ) {\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENT_INSUFFICIENT_PERMISSIONS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: awarded achievement ID */\n\t\t\t\t\t__( 'Sorry, you are not allowed to edit the awarded achievement #%1$d.', 'lifterlms' ),\n\t\t\t\t\t( $variables['engagement_id'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENT_INVALID_TEMPLATE:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: awarded achievement ID */\n\t\t\t\t\t__( 'Sorry, the awarded achievement #%1$d does not have a valid achievement template.', 'lifterlms' ),\n\t\t\t\t\t( $variables['engagement_id'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INSUFFICIENT_PERMISSIONS:\n\t\t\t\treturn __( 'Sorry, you are not allowed to edit awarded achievements.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INVALID_NONCE:\n\t\t\t\treturn __( 'Sorry, you are not allowed to sync awarded achievements.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_MISSING_AWARDED_ENGAGEMENT_ID:\n\t\t\t\treturn __( 'Sorry, you need to provide a valid awarded achievement ID.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_MISSING_ENGAGEMENT_TEMPLATE_ID:\n\t\t\t\treturn __( 'Sorry, you need to provide a valid achievement template ID.', 'lifterlms' );\n\t\t\tdefault:\n\t\t\t\treturn parent::get_text( $text_type );\n\t\t}\n\t}\n\n\t/**\n\t * Handle achievement form actions to download (for students and admins) and to delete (admins only)\n\t *\n\t * @since 3.18.0\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_handle_reporting_actions() {\n\n\t\tif ( ! isset( $_REQUEST['_llms_achievement_actions_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_achievement_actions_nonce'] ) ), 'llms-achievement-actions' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isset( $_POST['llms_delete_achievement'] ) ) {\n\t\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$this->delete( llms_filter_input( INPUT_POST, 'achievement_id', FILTER_SANITIZE_NUMBER_INT ) );\n\t\t}\n\t}\n}\n\nreturn new LLMS_Controller_Achievements();\n"
  },
  {
    "path": "includes/controllers/class.llms.controller.admin.quiz.attempts.php",
    "content": "<?php\n/**\n * Quiz Attempt Forms on the admin panel\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 3.16.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Quiz Attempt Forms on the admin panel\n *\n * Allows admins to grade, leave remarks, and delete quiz attempts.\n *\n * @since 3.16.0\n * @since 3.30.3 Fixed an issue causing backlashes to be saved around escaped characters when leaving remarks.\n * @since 3.35.0 Sanitize `$_POST` data.\n */\nclass LLMS_Controller_Admin_Quiz_Attempts {\n\n\tpublic function __construct() {\n\n\t\tadd_action( 'admin_init', array( $this, 'maybe_run_actions' ) );\n\t}\n\n\t/**\n\t * Run actions on form submission.\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.9 Unknown.\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 4.4.4 Made sure to exit after redirecting on attempt deletion.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.8.0 Added `llms_quiz_resumable_attempt_action` action and single resumable attempt delete.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_run_actions() {\n\n\t\tif ( ! isset( $_REQUEST['_llms_quiz_attempt_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_quiz_attempt_nonce'] ) ), 'llms_quiz_attempt_actions' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isset( $_POST['llms_quiz_attempt_action'] ) && isset( $_POST['llms_attempt_id'] ) ) {\n\n\t\t\t$action  = llms_filter_input( INPUT_POST, 'llms_quiz_attempt_action' );\n\t\t\t$attempt = new LLMS_Quiz_Attempt( absint( $_POST['llms_attempt_id'] ) );\n\n\t\t\tif ( ! current_user_can( 'edit_post', $attempt->get( 'quiz_id' ) ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( 'llms_attempt_delete' === $action ) {\n\t\t\t\t$url = add_query_arg(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'page'    => 'llms-reporting',\n\t\t\t\t\t\t'tab'     => 'quizzes',\n\t\t\t\t\t\t'quiz_id' => $attempt->get( 'quiz_id' ),\n\t\t\t\t\t\t'stab'    => 'attempts',\n\t\t\t\t\t),\n\t\t\t\t\tadmin_url( 'admin.php' )\n\t\t\t\t);\n\t\t\t\t$attempt->delete();\n\t\t\t\twp_safe_redirect( $url );\n\t\t\t\texit();\n\t\t\t} elseif ( 'llms_attempt_grade' === $action && ( isset( $_POST['remarks'] ) || isset( $_POST['points'] ) ) ) {\n\t\t\t\t$this->save_grade( $attempt );\n\t\t\t} elseif ( 'llms_disable_resume_attempt' === $action ) {\n\t\t\t\t$attempt->set( 'can_be_resumed', false );\n\t\t\t\t$attempt->save();\n\t\t\t\t$attempt->end();\n\t\t\t}\n\t\t}\n\n\t\tif ( isset( $_POST['llms_quiz_resumable_attempt_action'] ) ) {\n\n\t\t\t$action = llms_filter_input( INPUT_POST, 'llms_quiz_resumable_attempt_action' );\n\n\t\t\tif ( 'llms_clear_resumable_attempts' === $action ) {\n\n\t\t\t\t$quiz_id = llms_filter_input( INPUT_POST, 'llms_quiz_id' );\n\n\t\t\t\tif ( ! current_user_can( 'edit_post', $quiz_id ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t$resumable_attempts = $this->get_resumable_attempts( $quiz_id );\n\n\t\t\t\tforeach ( $resumable_attempts as $attempt_id ) {\n\t\t\t\t\t$attempt = new LLMS_Quiz_Attempt( $attempt_id );\n\t\t\t\t\t$attempt->set( 'can_be_resumed', false );\n\t\t\t\t\t$attempt->save();\n\n\t\t\t\t\t$attempt->end();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get resumable attempts for a quiz.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param int $quiz_id Quiz ID.\n\t */\n\tpublic function get_resumable_attempts( $quiz_id ) {\n\n\t\t// Query to get all resumable attempts.\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'quiz_id'        => $quiz_id,\n\t\t\t\t'can_be_resumed' => true,\n\t\t\t\t'status'         => 'incomplete',\n\t\t\t)\n\t\t);\n\n\t\treturn wp_list_pluck( $query->get_results(), 'id' );\n\t}\n\n\t/**\n\t * Saves changes to a quiz\n\t *\n\t * @since 3.16.0\n\t * @since 3.30.3 Strip slashes on remarks.\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 4.4.4 Use strict type comparisons where needed.\n\t *\n\t * @param LLMS_Quiz_Attempt $attempt Quiz attempt instance.\n\t * @return void\n\t */\n\tprivate function save_grade( $attempt ) {\n\n\t\t// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in `maybe_run_actions()` method.\n\n\t\t$remarks = isset( $_POST['remarks'] ) ? llms_filter_input_sanitize_string( INPUT_POST, 'remarks', array( FILTER_REQUIRE_ARRAY ) ) : array();\n\t\t$points  = isset( $_POST['points'] ) ? llms_filter_input_sanitize_string( INPUT_POST, 'points', array( FILTER_REQUIRE_ARRAY ) ) : array();\n\n\t\t$questions = $attempt->get_questions();\n\t\tforeach ( $questions as &$question ) {\n\n\t\t\tif ( isset( $remarks[ $question['id'] ] ) ) {\n\t\t\t\t$question['remarks'] = wp_kses_post( nl2br( stripslashes( $remarks[ $question['id'] ] ) ) );\n\t\t\t}\n\n\t\t\tif ( isset( $points[ $question['id'] ] ) ) {\n\t\t\t\t$earned             = absint( $points[ $question['id'] ] );\n\t\t\t\t$question['earned'] = $earned;\n\t\t\t\tif ( ( $earned / $question['points'] ) >= 0.5 ) {\n\t\t\t\t\t$question['correct'] = 'yes';\n\t\t\t\t} else {\n\t\t\t\t\t$question['correct'] = 'no';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Update the attempt with new questions.\n\t\t$attempt->set_questions( $questions, true );\n\n\t\t// Attempt to calculate the grade.\n\t\t$attempt->calculate_grade()->save();\n\n\t\t// If all questions were graded the grade will have been calculated and we can trigger completion actions.\n\t\tif ( in_array( $attempt->get( 'status' ), array( 'fail', 'pass' ), true ) ) {\n\t\t\t$attempt->do_completion_actions();\n\t\t}\n\n\t\tdo_action( 'llms_quiz_graded', $attempt->get_student()->get_id(), $attempt->get( 'quiz_id' ), $attempt );\n\n\t\t// phpcs:enable\n\t}\n}\n\nreturn new LLMS_Controller_Admin_Quiz_Attempts();\n"
  },
  {
    "path": "includes/controllers/class.llms.controller.certificates.php",
    "content": "<?php\n/**\n * LLMS_Controller_Certificates class\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 3.18.0\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Handles awarded user certificates.\n *\n * @since 3.18.0\n * @since 3.35.0 Sanitize `$_POST` data.\n * @since 3.37.4 Modify `llms_certificate` post type registration to allow certificate templates to be exported.\n *               When exporting a certificate template, use the `post_author` for the certificate's WP User ID.\n * @since 4.3.1 Properly use an `error` notice to display a WP_Error when trying to download a certificate.\n * @since 6.0.0 Extended from the LLMS_Abstract_Controller_User_Engagements class.\n */\nclass LLMS_Controller_Certificates extends LLMS_Abstract_Controller_User_Engagements {\n\n\t/**\n\t * Type of user engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $engagement_type = 'certificate';\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.4 Add filter hook for `lifterlms_register_post_type_llms_certificate`.\n\t * @since 5.5.0 Drop usage of deprecated `lifterlms_register_post_type_llms_certificate` in favor of `lifterlms_register_post_type_certificate`.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tparent::__construct();\n\n\t\tadd_filter( 'lifterlms_register_post_type_certificate', array( $this, 'maybe_allow_public_query' ) );\n\n\t\tadd_action( 'init', array( $this, 'maybe_handle_reporting_actions' ) );\n\t\tadd_action( 'wp', array( $this, 'maybe_authenticate_export_generation' ) );\n\t}\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Controller_User_Engagements::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\tswitch ( $text_type ) {\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENT_INSUFFICIENT_PERMISSIONS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: awarded certificate ID */\n\t\t\t\t\t__( 'Sorry, you are not allowed to edit the awarded certificate #%1$d.', 'lifterlms' ),\n\t\t\t\t\t( $variables['engagement_id'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENT_INVALID_TEMPLATE:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$d: awarded certificate ID */\n\t\t\t\t\t__( 'Sorry, the awarded certificate #%1$d does not have a valid certificate template.', 'lifterlms' ),\n\t\t\t\t\t( $variables['engagement_id'] ?? 0 )\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INSUFFICIENT_PERMISSIONS:\n\t\t\t\treturn __( 'Sorry, you are not allowed to edit awarded certificates.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INVALID_NONCE:\n\t\t\t\treturn __( 'Sorry, you are not allowed to sync awarded certificates.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_MISSING_AWARDED_ENGAGEMENT_ID:\n\t\t\t\treturn __( 'Sorry, you need to provide a valid awarded certificate ID.', 'lifterlms' );\n\t\t\tcase self::TEXT_SYNC_MISSING_ENGAGEMENT_TEMPLATE_ID:\n\t\t\t\treturn __( 'Sorry, you need to provide a valid certificate template ID.', 'lifterlms' );\n\t\t\tdefault:\n\t\t\t\treturn parent::get_text( $text_type );\n\t\t}\n\t}\n\n\t/**\n\t * Modify certificate post type registration data during a certificate template export.\n\t *\n\t * @since 3.37.4\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/776\n\t *\n\t * @param array $post_type_args Array of `llms_certificate` post type registration arguments.\n\t * @return array\n\t */\n\tpublic function maybe_allow_public_query( $post_type_args ) {\n\n\t\tif ( ! empty( $_REQUEST['_llms_cert_auth'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\n\t\t\t$auth = llms_filter_input( INPUT_GET, '_llms_cert_auth', FILTER_SANITIZE_FULL_SPECIAL_CHARS );\n\n\t\t\tglobal $wpdb;\n\t\t\t$post_id = $wpdb->get_var( $wpdb->prepare( \"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_llms_auth_nonce' AND meta_value = %s\", $auth ) ); // db call ok; no-cache ok.\n\t\t\tif ( $post_id && 'llms_certificate' === get_post_type( $post_id ) ) {\n\t\t\t\t$post_type_args['publicly_queryable'] = true;\n\t\t\t}\n\t\t}\n\n\t\treturn $post_type_args;\n\t}\n\n\t/**\n\t * Allow cURL requests to view a certificate to be authenticated via a nonce.\n\t *\n\t * A cURL request is used to scrape the HTML and this will authenticate the scrape.\n\t *\n\t * @since 3.18.0\n\t * @since 3.24.0 Unknown.\n\t * @since 3.37.4 Use the `post_author` as the WP_User ID when exporting a certificate template.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_authenticate_export_generation() {\n\n\t\tif ( empty( $_REQUEST['_llms_cert_auth'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\treturn;\n\t\t}\n\n\t\t$post_id   = get_the_ID();\n\t\t$post_type = get_post_type( $post_id );\n\t\tif ( ! in_array( $post_type, array( 'llms_my_certificate', 'llms_certificate' ), true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( get_post_meta( $post_id, '_llms_auth_nonce', true ) !== $_REQUEST['_llms_cert_auth'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t\t\treturn;\n\t\t}\n\n\t\t$cert = new LLMS_User_Certificate( $post_id );\n\t\t$uid  = ( 'llms_certificate' === $post_type ) ? get_post_field( 'post_author', $post_id ) : $cert->get_user_id();\n\t\twp_set_current_user( $uid );\n\t}\n\n\t/**\n\t * Handle certificate form actions\n\t *\n\t * Manages frontend actions to download and manage certificate sharing settings and reporting (admin)\n\t * actions to download and delete.\n\t *\n\t * The method name is a misnomer as this method handles actions on reporting screens as well as\n\t * on the site's frontend when actually viewing a certificate\n\t *\n\t * @since 3.18.0\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 4.5.0 Add handler for changing certificate sharing settings.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_handle_reporting_actions() {\n\t\tif ( ! isset( $_REQUEST['_llms_cert_actions_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_cert_actions_nonce'] ) ), 'llms-cert-actions' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$cert_id = absint( llms_filter_input( INPUT_POST, 'certificate_id', FILTER_SANITIZE_NUMBER_INT ) );\n\t\tif ( isset( $_POST['llms_generate_cert'] ) ) {\n\t\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\t\t\tif ( ! $cert->can_user_manage() ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t$this->download( $cert_id );\n\t\t} elseif ( isset( $_POST['llms_delete_cert'] ) ) {\n\t\t\tif ( ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$this->delete( $cert_id );\n\t\t} elseif ( isset( $_POST['llms_enable_cert_sharing'] ) ) {\n\t\t\t$this->change_sharing_settings( $cert_id, (bool) $_POST['llms_enable_cert_sharing'] );\n\t\t}\n\t}\n\n\t/**\n\t * Change shareable settings of a certificate.\n\t *\n\t * @since 4.5.0\n\t *\n\t * @param int  $cert_id    WP Post ID of the llms_my_certificate.\n\t * @param bool $is_allowed Allow share the certificate or not.\n\t * @return WP_Error|boolean Returns `true` on success and `false` on failure or an error object when the user does not have sufficient privileges.\n\t */\n\tprivate function change_sharing_settings( $cert_id, $is_allowed ) {\n\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\tif ( ! $cert->can_user_manage() ) {\n\t\t\treturn new WP_Error( 'insufficient-permissions', __( 'You are not allowed to manage this certificate.', 'lifterlms' ) );\n\t\t}\n\n\t\treturn $cert->set( 'allow_sharing', $is_allowed ? 'yes' : 'no' );\n\t}\n\n\t/**\n\t * Download a Certificate.\n\t *\n\t * Generates an HTML export of the certificate from the \"Download\" button\n\t * on the View Certificate front end & on reporting backend for admins.\n\t *\n\t * @since 3.18.0\n\t * @since 4.3.1 Properly use an `error` notice to display a WP_Error.\n\t *\n\t * @return void\n\t */\n\tprivate function download( $cert_id ) {\n\n\t\t$filepath = llms()->certificates()->get_export( $cert_id );\n\t\tif ( is_wp_error( $filepath ) ) {\n\t\t\t// @todo Need to handle errors differently on admin panel.\n\t\t\treturn llms_add_notice( $filepath->get_error_message(), 'error' );\n\t\t}\n\n\t\theader( 'Content-Description: File Transfer' );\n\t\theader( 'Content-Type: application/octet-stream' );\n\t\theader( 'Content-Disposition: attachment; filename=\"' . basename( $filepath ) . '\"' );\n\n\t\treadfile( $filepath ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile\n\n\t\t// Delete file after download.\n\t\tignore_user_abort( true );\n\t\twp_delete_file( $filepath );\n\t\texit;\n\t}\n}\n\nreturn new LLMS_Controller_Certificates();\n"
  },
  {
    "path": "includes/controllers/class.llms.controller.lesson.progression.php",
    "content": "<?php\n/**\n * Lesson Progression Actions\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 3.17.1\n * @version 6.10.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Controller_Lesson_Progression class\n *\n * @since 3.17.1\n */\nclass LLMS_Controller_Lesson_Progression {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.17.1\n\t * @since 3.29.0 Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'admin_init', array( $this, 'handle_admin_managment_forms' ) );\n\n\t\tadd_action( 'init', array( $this, 'handle_complete_form' ) );\n\t\tadd_action( 'init', array( $this, 'handle_incomplete_form' ) );\n\n\t\tadd_action( 'lifterlms_quiz_completed', array( $this, 'quiz_complete' ), 10, 3 );\n\t\tadd_filter( 'llms_allow_lesson_completion', array( $this, 'quiz_maybe_prevent_lesson_completion' ), 10, 5 );\n\n\t\tadd_action( 'llms_trigger_lesson_completion', array( $this, 'mark_complete' ), 10, 4 );\n\n\t}\n\n\t/**\n\t * Retrieve a lesson ID from form data for the mark complete / incomplete forms\n\t *\n\t * @since 3.29.0\n\t *\n\t * @param string $action Form action, either \"complete\" or \"incomplete\".\n\t * @return int|null Returns `null` when either required post fields are missing or if the lesson_id is non-numeric, int (lesson id) on success.\n\t */\n\tprivate function get_lesson_id_from_form_data( $action ) {\n\n\t\tif ( ! isset( $_REQUEST['_wpnonce'] ) ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'mark_' . $action ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$submitted = llms_filter_input( INPUT_POST, 'mark_' . $action );\n\t\t$lesson_id = llms_filter_input( INPUT_POST, 'mark-' . $action );\n\n\t\t// Required fields.\n\t\tif ( is_null( $submitted ) || is_null( $lesson_id ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$lesson_id = absint( $lesson_id );\n\n\t\t// Invalid lesson ID.\n\t\tif ( ! $lesson_id || ! is_numeric( $lesson_id ) ) {\n\n\t\t\tllms_add_notice( __( 'An error occurred, please try again.', 'lifterlms' ), 'error' );\n\t\t\treturn null;\n\n\t\t}\n\n\t\treturn $lesson_id;\n\n\t}\n\n\t/**\n\t * Handle form submission from the Student -> Courses -> Course table where admins can toggle completion of lessons for a student.\n\t *\n\t * @since 3.29.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 6.10.0 Check the current user can edit the lesson they're going to mark complete/incomplete.\n\t *\n\t * @return void\n\t */\n\tpublic function handle_admin_managment_forms() {\n\n\t\tif ( ! isset( $_REQUEST['llms-admin-progression-nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['llms-admin-progression-nonce'] ) ), 'llms-admin-lesson-progression' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$action     = llms_filter_input( INPUT_POST, 'llms-lesson-action' );\n\t\t$lesson_id  = absint( llms_filter_input( INPUT_POST, 'lesson_id' ) );\n\t\t$student_id = absint( llms_filter_input( INPUT_POST, 'student_id' ) );\n\n\t\t// Missing required data.\n\t\tif ( empty( $action ) || empty( $lesson_id ) || empty( $student_id ) || ! current_user_can( 'edit_post', $lesson_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$trigger = 'admin_' . get_current_user_id();\n\n\t\tif ( 'complete' === $action ) {\n\t\t\t$this->mark_complete( $student_id, $lesson_id, $trigger );\n\t\t} elseif ( 'incomplete' === $action ) {\n\t\t\tllms_mark_incomplete( $student_id, $lesson_id, 'lesson', $trigger );\n\t\t}\n\n\t}\n\n\t/**\n\t * Mark Lesson as complete\n\t *\n\t * + Complete Lesson form post.\n\t * + Marks lesson as complete and returns completion message to user.\n\t * + Autoadvances to next lesson if completion is successful.\n\t *\n\t * @since 3.17.1\n\t * @since 3.29.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function handle_complete_form() {\n\n\t\t$lesson_id = $this->get_lesson_id_from_form_data( 'complete' );\n\n\t\tif ( is_null( $lesson_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Filter to modify the user id instead of current logged in user id.\n\t\t *\n\t\t * @param int $user_id User id to mark lesson as complete.\n\t\t *\n\t\t * @since 5.4.0\n\t\t */\n\t\t$user_id = apply_filters( 'llms_lesson_completion_user_id', get_current_user_id() );\n\n\t\tdo_action( 'llms_trigger_lesson_completion', $user_id, $lesson_id, 'lesson_' . $lesson_id );\n\n\t\tif ( apply_filters( 'lifterlms_autoadvance', true ) ) {\n\n\t\t\t$lesson         = new LLMS_Lesson( $lesson_id );\n\t\t\t$next_lesson_id = $lesson->get_next_lesson();\n\t\t\tif ( $next_lesson_id ) {\n\n\t\t\t\twp_redirect( apply_filters( 'llms_lesson_complete_redirect', get_permalink( $next_lesson_id ) ) );\n\t\t\t\texit;\n\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Mark Lesson as incomplete\n\t *\n\t * + Incomplete Lesson form post.\n\t * + Marks lesson as incomplete and returns incompletion message to user.\n\t *\n\t * @since 3.17.1\n\t * @since 3.29.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function handle_incomplete_form() {\n\n\t\t$lesson_id = $this->get_lesson_id_from_form_data( 'incomplete' );\n\n\t\tif ( is_null( $lesson_id ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Filter to modify the user id instead of current logged in user id.\n\t\t *\n\t\t * @param int $user_id User id to mark lesson as incomplete.\n\t\t *\n\t\t * @since 5.4.0\n\t\t */\n\t\t$user_id = apply_filters( 'llms_lesson_incomplete_user_id', get_current_user_id() );\n\n\t\t// Mark incomplete and add a notice on success.\n\t\tif ( llms_mark_incomplete( $user_id, $lesson_id, 'lesson', 'lesson_' . $lesson_id ) ) {\n\t\t\t// Translators: %s is the title of the lesson.\n\t\t\tllms_add_notice( sprintf( __( 'The lesson %s is now marked as incomplete.', 'lifterlms' ), get_the_title( $lesson_id ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Handle completion of lesson via `llms_trigger_lesson_completion` action\n\t *\n\t * @since 3.17.1\n\t * @since 3.29.0 Unknown.\n\t *\n\t * @param int    $user_id   User ID.\n\t * @param int    $lesson_id Lesson ID.\n\t * @param string $trigger   Optional trigger description string.\n\t * @param array  $args      Optional arguments.\n\t * @return void\n\t */\n\tpublic function mark_complete( $user_id, $lesson_id, $trigger = '', $args = array() ) {\n\n\t\tif ( llms_allow_lesson_completion( $user_id, $lesson_id, $trigger, $args ) ) {\n\n\t\t\tllms_mark_complete( $user_id, $lesson_id, 'lesson', $trigger );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Trigger lesson completion when a quiz is completed\n\t *\n\t * @since 3.17.1\n\t *\n\t * @param int $student_id WP User ID.\n\t * @param int $quiz_id    WP Post ID of the quiz.\n\t * @param obj $attempt    Instance of the LLMS_Quiz_Attempt.\n\t * @return void\n\t */\n\tpublic function quiz_complete( $student_id, $quiz_id, $attempt ) {\n\n\t\tdo_action(\n\t\t\t'llms_trigger_lesson_completion',\n\t\t\t$student_id,\n\t\t\t$attempt->get( 'lesson_id' ),\n\t\t\t'quiz_' . $quiz_id,\n\t\t\tarray(\n\t\t\t\t'attempt' => $attempt,\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Before a lesson is marked as complete, check if all the lesson's quiz requirements are met\n\t *\n\t * @since 3.17.1\n\t *\n\t * @param bool   $allow_completion Whether or not to allow completion (true by default, false if something else has already prevented).\n\t * @param int    $user_id          WP User ID of the student completing the lesson.\n\t * @param int    $lesson_id        WP Post ID of the lesson to be completed.\n\t * @param string $trigger          Text string to record the reason why the lesson is being completed.\n\t * @param array  $args             Optional additional arguments from the triggering function.\n\t * @return bool\n\t */\n\tpublic function quiz_maybe_prevent_lesson_completion( $allow_completion, $user_id, $lesson_id, $trigger, $args ) {\n\n\t\t// If allow completion is already false, we don't need to run any quiz checks.\n\t\tif ( ! $allow_completion ) {\n\t\t\treturn $allow_completion;\n\t\t}\n\n\t\t$lesson           = llms_get_post( $lesson_id );\n\t\t$passing_required = llms_parse_bool( $lesson->get( 'require_passing_grade' ) );\n\n\t\t// If the lesson is being completed by a quiz.\n\t\tif ( 0 === strpos( $trigger, 'quiz_' ) ) {\n\n\t\t\t// Passing is required AND the attempt was a failure.\n\t\t\tif ( $passing_required && ! $args['attempt']->is_passing() ) {\n\t\t\t\t$allow_completion = false;\n\t\t\t}\n\t\t} elseif ( $lesson->is_quiz_enabled() ) {\n\n\t\t\t$quiz_id = $lesson->get( 'quiz' );\n\t\t\t$student = llms_get_student( $user_id );\n\t\t\t$attempt = $student->quizzes()->get_best_attempt( $quiz_id );\n\n\t\t\t// Passing is not required but there's not attempts yet.\n\t\t\t// At least one attempt (passing or otherwise) is required!.\n\t\t\tif ( ! $passing_required && ! $attempt ) {\n\t\t\t\t$allow_completion = false;\n\n\t\t\t\t// Passing is required and there's no attempts or the best attempt is not passing.\n\t\t\t} elseif ( $passing_required && ( ! $attempt || ! $attempt->is_passing() ) ) {\n\t\t\t\t$allow_completion = false;\n\t\t\t}\n\t\t}\n\n\t\treturn $allow_completion;\n\n\t}\n\n}\n\nreturn new LLMS_Controller_Lesson_Progression();\n"
  },
  {
    "path": "includes/controllers/class.llms.controller.orders.php",
    "content": "<?php\n/**\n * Order processing and related actions controller.\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 3.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Controller_Orders class.\n *\n * @since 3.0.0\n * @since 3.33.0 Added logic to delete any enrollment records linked to an LLMS_Order on its permanent deletion.\n * @since 3.34.4 Added filter `llms_order_can_be_confirmed`.\n * @since 3.34.5 Fixed logic error in `llms_order_can_be_confirmed` conditional.\n * @since 3.36.1 In `recurring_charge()`, made sure to process only proper LLMS_Orders of existing users.\n * @since 4.2.0 Added logic to set the order status to 'cancelled' when an enrollment linked to an order is deleted.\n *              Also `llms_unenroll_on_error_order` fiter hook added.\n * @since 5.0.0 Build customer data using LLMS_Forms fields information.\n */\nclass LLMS_Controller_Orders {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Updated.\n\t * @since 3.33.0 Added `before_delete_post` action to handle order deletion.\n\t * @since 4.2.0 Added `llms_user_enrollment_deleted` action to handle order status change on enrollment deletion.\n\t * @since 5.4.0 Perform `error_order()` when Detect a product deletion while processing a recurring charge.\n\t * @since 7.0.0 Added callback for `wp_untrash_post_status` filter.\n\t *              Remove action callbacks for order confirm, create, and payment source switch in favor of hooks in `LLMS_Controller_Checkout`.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'wp_untrash_post_status', array( $this, 'set_untrash_status' ), 10, 3 );\n\n\t\t// This action adds our lifterlms specific actions when order & transaction statuses change.\n\t\tadd_action( 'transition_post_status', array( $this, 'transition_status' ), 10, 3 );\n\n\t\t// This action adds lifterlms specific action when an order is deleted, just before the WP post postmetas are removed.\n\t\tadd_action( 'before_delete_post', array( $this, 'on_delete_order' ) );\n\n\t\t// This action is meant to do specific actions on orders when an enrollment, with an order as trigger, is deleted.\n\t\tadd_action( 'llms_user_enrollment_deleted', array( $this, 'on_user_enrollment_deleted' ), 10, 3 );\n\n\t\t// Transaction status changes cascade up to the order to change the order status.\n\t\tadd_action( 'lifterlms_transaction_status_failed', array( $this, 'transaction_failed' ), 10, 1 );\n\t\tadd_action( 'lifterlms_transaction_status_refunded', array( $this, 'transaction_refunded' ), 10, 1 );\n\t\tadd_action( 'lifterlms_transaction_status_succeeded', array( $this, 'transaction_succeeded' ), 10, 1 );\n\n\t\t// Status changes for orders to enroll students and trigger completion actions.\n\t\tadd_action( 'lifterlms_order_status_completed', array( $this, 'complete_order' ), 10, 2 );\n\t\tadd_action( 'lifterlms_order_status_active', array( $this, 'complete_order' ), 10, 2 );\n\n\t\t// Status changes to pending cancel.\n\t\tadd_action( 'lifterlms_order_status_pending-cancel', array( $this, 'pending_cancel_order' ), 10, 1 );\n\n\t\t// Status changes for orders to unenroll students upon purchase.\n\t\tadd_action( 'lifterlms_order_status_refunded', array( $this, 'error_order' ), 10, 1 );\n\t\tadd_action( 'lifterlms_order_status_cancelled', array( $this, 'error_order' ), 10, 1 );\n\t\tadd_action( 'lifterlms_order_status_expired', array( $this, 'error_order' ), 10, 1 );\n\t\tadd_action( 'lifterlms_order_status_failed', array( $this, 'error_order' ), 10, 1 );\n\t\tadd_action( 'lifterlms_order_status_on-hold', array( $this, 'error_order' ), 10, 1 );\n\t\tadd_action( 'lifterlms_order_status_trash', array( $this, 'error_order' ), 10, 1 );\n\n\t\t// Detect a product deletion while processing a recurring charge.\n\t\tadd_action( 'llms_order_recurring_charge_aborted_product_deleted', array( $this, 'error_order' ), 10, 1 );\n\n\t\t/**\n\t\t * Scheduler Actions\n\t\t */\n\n\t\t// Charge recurring payments.\n\t\tadd_action( 'llms_charge_recurring_payment', array( $this, 'recurring_charge' ), 10, 1 );\n\n\t\t// Expire access plans.\n\t\tadd_action( 'llms_access_plan_expiration', array( $this, 'expire_access' ), 10, 1 );\n\t}\n\n\t/**\n\t * Perform actions on a successful order completion.\n\t *\n\t * @since 1.0.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @param LLMS_Order $order      Instance of an LLMS_Order.\n\t * @param string     $old_status Previous order status (eg: 'pending').\n\t * @return void\n\t */\n\tpublic function complete_order( $order, $old_status ) {\n\n\t\t// Clear expiration date when moving from a pending-cancel order.\n\t\tif ( 'pending-cancel' === $old_status ) {\n\t\t\t$order->set( 'date_access_expires', '' );\n\t\t}\n\n\t\t// Record access start time & maybe schedule expiration.\n\t\t$order->start_access();\n\n\t\t$order_id   = $order->get( 'id' );\n\t\t$product_id = $order->get( 'product_id' );\n\t\t$user_id    = $order->get( 'user_id' );\n\n\t\tunset( llms()->session->llms_coupon );\n\n\t\t/**\n\t\t * Action fired on order complete.\n\t\t *\n\t\t * Prior to the students being enrolled.\n\t\t *\n\t\t * @since 1.0.0\n\t\t *\n\t\t * @param integer $order_id The WP_Post ID of the order.\n\t\t */\n\t\tdo_action( 'lifterlms_order_complete', $order_id ); // @todo used by AffiliateWP only, can remove after updating AffiliateWP.\n\n\t\t/**\n\t\t * Filter whether the user should be automatically enrolled when the order completes.\n\t\t *\n\t\t * @since 9.1.2\n\t\t *\n\t\t * @param bool Whether to enroll the student. Defaults to true.\n\t\t * @param LLMS_Order $order The order.\n\t\t */\n\t\tif ( apply_filters( 'lifterlms_enroll_student_on_order_complete', true, $order ) ) {\n\t\t\tllms_enroll_student( $user_id, $product_id, 'order_' . $order_id );\n\t\t}\n\n\t\t// Trigger purchase action, used by engagements.\n\n\t\t/**\n\t\t * Action fired on product purchased.\n\t\t *\n\t\t * After the student has been enrolled.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param integer $user_id    The WP_User ID of the buyer.\n\t\t * @param integer $product_id The WP_Post ID of the purchased product (course/membership).\n\t\t */\n\t\tdo_action( 'lifterlms_product_purchased', $user_id, $product_id );\n\n\t\t/**\n\t\t * Action fired on access plan purchased.\n\t\t *\n\t\t * After the student has been enrolled.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param integer $user_id    The WP_User ID of the buyer.\n\t\t * @param integer $product_id The WP_Post ID of the purchased access plan.\n\t\t */\n\t\tdo_action( 'lifterlms_access_plan_purchased', $user_id, $order->get( 'plan_id' ) );\n\n\t\t// Maybe schedule a payment.\n\t\t$order->maybe_schedule_payment();\n\t}\n\n\t/**\n\t * Called when an order's status changes to refunded, cancelled, expired, or failed.\n\t *\n\t * Also called on product deletion detected while processing a recurring charge.\n\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t * @since 4.2.0 Added `llms_unenroll_on_error_order` filter hook.\n\t * @since 5.4.0 Unenroll with 'cancelled' status on 'llms_order_recurring_charge_aborted_product_deleted'.\n\t *              The `$order` param can be also a WP_Post or its `ID`.\n\t *\n\t * @param int|WP_Post|LLMS_Order $order Instance of an LLMS_Order, WP_Post or WP_Post ID of the order.\n\t * @return void\n\t */\n\tpublic function error_order( $order ) {\n\n\t\t$order = is_a( $order, 'LLMS_Order' ) ? $order : llms_get_post( $order );\n\t\tif ( ! ( $order && is_a( $order, 'LLMS_Order' ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$order->unschedule_recurring_payment();\n\n\t\t/**\n\t\t * Determine if student should be unenrolled on order error.\n\t\t *\n\t\t * @since 4.2.0\n\t\t *\n\t\t * @param bool       $unenroll_on_error_order True if the student should be unenrolled, false otherwise. Default true.\n\t\t * @param LLMS_Order $order                   Order object.\n\t\t */\n\t\tif ( ! apply_filters( 'llms_unenroll_on_error_order', true, $order ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tswitch ( current_filter() ) {\n\n\t\t\tcase 'lifterlms_order_status_trash':\n\t\t\tcase 'lifterlms_order_status_cancelled':\n\t\t\tcase 'lifterlms_order_status_on-hold':\n\t\t\tcase 'lifterlms_order_status_refunded':\n\t\t\tcase 'llms_order_recurring_charge_aborted_product_deleted':\n\t\t\t\t$status = 'cancelled';\n\t\t\t\tbreak;\n\n\t\t\tcase 'lifterlms_order_status_expired':\n\t\t\tcase 'lifterlms_order_status_failed':\n\t\t\tdefault:\n\t\t\t\t$status = 'expired';\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\tllms_unenroll_student( $order->get( 'user_id' ), $order->get( 'product_id' ), $status, 'order_' . $order->get( 'id' ) );\n\t}\n\n\t/**\n\t * Called when a post is permanently deleted.\n\t *\n\t * Will delete any enrollment records linked to the LLMS_Order with the ID of the deleted post.\n\t *\n\t * @since 3.33.0\n\t *\n\t * @param int $post_id WP_Post ID.\n\t * @return void\n\t */\n\tpublic function on_delete_order( $post_id ) {\n\n\t\t$order = llms_get_post( $post_id );\n\t\tif ( $order && is_a( $order, 'LLMS_Order' ) ) {\n\t\t\tllms_delete_student_enrollment( $order->get( 'user_id' ), $order->get( 'product_id' ), 'order_' . $order->get( 'id' ) );\n\t\t}\n\t}\n\n\t/**\n\t * Called when an user enrollment is deleted.\n\t *\n\t * Will set the related order status to 'cancelled'.\n\t *\n\t * @since 4.2.0\n\t *\n\t * @param int    $user_id    WP User ID.\n\t * @param int    $product_id WP Post ID of the course or membership.\n\t * @param string $trigger    The deleted enrollment trigger, or 'any' if no specific trigger.\n\t * @return void\n\t */\n\tpublic function on_user_enrollment_deleted( $user_id, $product_id, $trigger ) {\n\n\t\t$order_id = 'order_' === substr( $trigger, 0, 6 ) ? absint( substr( $trigger, 6 ) ) : false;\n\t\t$order    = $order_id ? llms_get_post( $order_id ) : false;\n\n\t\tif ( $order && is_a( $order, 'LLMS_Order' ) ) {\n\n\t\t\t// No need to run an unenrollment as we're reacting to an enrollment deletion, user enrollments data already removed.\n\t\t\tadd_filter( 'llms_unenroll_on_error_order', '__return_false', 100 );\n\t\t\t$order->set_status( 'cancelled' );\n\t\t\t// Reset unenrollment's suspension..\n\t\t\tremove_filter( 'llms_unenroll_on_error_order', '__return_false', 100 );\n\n\t\t}\n\t}\n\n\t/**\n\t * Handle expiration & cancellation from a course / membership.\n\t *\n\t * Called via scheduled action set during order completion for plans with a limited access plan.\n\t * Additionally called when an order is marked as \"pending-cancel\" to revoke access at the end of a pre-paid period.\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t * @since 7.5.0 Potentially allow recurring payment to go ahead even if access plans expired.\n\t *\n\t * @param int $order_id WP_Post ID of the LLMS Order.\n\t * @return void\n\t */\n\tpublic function expire_access( $order_id ) {\n\n\t\t$order            = new LLMS_Order( $order_id );\n\t\t$new_order_status = false;\n\n\t\t// Pending cancel order moves to cancelled.\n\t\tif ( 'llms-pending-cancel' === $order->get( 'status' ) ) {\n\n\t\t\t$status           = 'cancelled'; // Enrollment status.\n\t\t\t$note             = __( 'Student unenrolled at the end of access period due to subscription cancellation.', 'lifterlms' );\n\t\t\t$new_order_status = 'cancelled';\n\n\t\t\t// All others move to expired.\n\t\t} else {\n\n\t\t\t$status = 'expired'; // Enrollment status.\n\t\t\t$note   = __( 'Student unenrolled due to automatic access plan expiration', 'lifterlms' );\n\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not recurring payments should be stopped on access plan expiration.\n\t\t *\n\t\t * By default when an access plan expires, recurring payments are stopped.\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @param bool\n\t\t * @param LLMS_Order $order             Instance of the order.\n\t\t * @param mixed      $new_order_status  New order status. If `false` it means that the new order status is not\n\t\t *                                      going to change. At this stage the orders status is not changed yet.\n\t\t * @param string     $enrollment_status The new enrollment status. At this stage the enrollment status is not changed yet.\n\t\t */\n\t\t$unschedule_recurring_payment = apply_filters(\n\t\t\t'llms_unschedule_recurring_payment_on_access_plan_expiration',\n\t\t\ttrue,\n\t\t\t$order,\n\t\t\t$new_order_status,\n\t\t\t$status\n\t\t);\n\n\t\tllms_unenroll_student( $order->get( 'user_id' ), $order->get( 'product_id' ), $status, 'order_' . $order->get( 'id' ) );\n\t\t$order->add_note( $note );\n\n\t\tif ( $unschedule_recurring_payment ) {\n\t\t\t$order->unschedule_recurring_payment();\n\t\t}\n\n\t\tif ( $new_order_status ) {\n\t\t\t$order->set_status( $new_order_status );\n\t\t}\n\t}\n\n\t/**\n\t * Unschedule recurring payments and schedule access expiration.\n\t *\n\t * @since 3.19.0\n\t *\n\t * @param LLMS_Order $order LLMS_Order object.\n\t * @return void\n\t */\n\tpublic function pending_cancel_order( $order ) {\n\n\t\t$date = $order->get_next_payment_due_date( 'Y-m-d H:i:s' );\n\t\t$order->set( 'date_access_expires', $date );\n\n\t\t$order->unschedule_recurring_payment();\n\t\t$order->maybe_schedule_expiration();\n\t}\n\n\t/**\n\t * Trigger a recurring payment.\n\t *\n\t * Called by action scheduler.\n\t *\n\t * @since 3.0.0\n\t * @since 3.32.0 Record order notes and trigger actions during errors.\n\t * @since 3.36.1 Made sure to process only proper LLMS_Orders of existing users.\n\t * @since 5.2.0 Fixed buggy logging on gateway error because it doesn't support recurring payments.\n\t * @since 5.4.0 Handle case when the order's related product has been removed.\n\t *\n\t * @param int $order_id WP Post ID of the order.\n\t * @return bool `false` if the recurring charge cannot be processed, `true` when the charge is successfully handed off to the gateway.\n\t */\n\tpublic function recurring_charge( $order_id ) {\n\n\t\t// Make sure the order still exists.\n\t\t$order = llms_get_post( $order_id );\n\t\tif ( ! $order || ! is_a( $order, 'LLMS_Order' ) ) {\n\n\t\t\t/**\n\t\t\t * Fired when a LifterLMS order's recurring charge errors because the order doesn't exist anymore\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int                    $order_id   WP Post ID of the order.\n\t\t\t * @param LLMS_Controller_Orders $controller This controller's instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_order_recurring_charge_order_error', $order_id, $this );\n\t\t\tllms_log( sprintf( 'Recurring charge for Order #%d could not be processed because the order no longer exists.', $order_id ), 'recurring-payments' );\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// Check the user still exists.\n\t\t$user_id = $order->get( 'user_id' );\n\t\tif ( ! get_user_by( 'id', $user_id ) ) {\n\n\t\t\t/**\n\t\t\t * Fired when a LifterLMS order's recurring charge errors because the user who placed the order doesn't exist anymore\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int                    $order_id   WP Post ID of the order.\n\t\t\t * @param int                    $user_id    WP User ID of the user who placed the order.\n\t\t\t * @param LLMS_Controller_Orders $controller This controller's instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_order_recurring_charge_user_error', $order_id, $user_id, $this );\n\t\t\tllms_log( sprintf( 'Recurring charge for Order #%1$d could not be processed because the user (#%2$d) no longer exists.', $order_id, $user_id ), 'recurring-payments' );\n\n\t\t\t// Translators: %d = The deleted user's ID.\n\t\t\t$order->add_note( sprintf( __( 'Recurring charge skipped. The user (#%d) no longer exists.', 'lifterlms' ), $user_id ) );\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// Ensure Gateway is still available.\n\t\t$gateway = $order->get_gateway();\n\n\t\tif ( is_wp_error( $gateway ) ) {\n\t\t\t/**\n\t\t\t * Fired when a LifterLMS order's recurring charge errors because of a gateway error. E.g. it's not available anymore.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int                    $order_id   WP Post ID of the order.\n\t\t\t * @param WP_Error               $error      WP_Error instance.\n\t\t\t * @param LLMS_Controller_Orders $controller This controller's instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_order_recurring_charge_gateway_error', $order_id, $gateway, $this );\n\n\t\t\tllms_log(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Recurring charge for Order #%1$d could not be processed because the \"%2$s\" gateway is no longer available. Gateway Error: %3$s',\n\t\t\t\t\t$order_id,\n\t\t\t\t\t$order->get( 'payment_gateway' ),\n\t\t\t\t\t$gateway->get_error_message()\n\t\t\t\t),\n\t\t\t\t'recurring-payments'\n\t\t\t);\n\n\t\t\t$order->add_note(\n\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %s = error message encountered while loading the gateway.\n\t\t\t\t\t__( 'Recurring charge was not processed due to an error encountered while loading the payment gateway: %s.', 'lifterlms' ),\n\t\t\t\t\t$gateway->get_error_message()\n\t\t\t\t)\n\t\t\t);\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// Gateway doesn't support recurring payments.\n\t\tif ( ! $gateway->supports( 'recurring_payments' ) ) {\n\n\t\t\t/**\n\t\t\t * Fired when a LifterLMS order's recurring charge errors because the selected gateway doesn't support recurring payments.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int                    $order_id   WP Post ID of the order.\n\t\t\t * @param LLMS_Payment_Gateway   $gateway    LLMS_Payment_Gateway extending class instance.\n\t\t\t * @param LLMS_Controller_Orders $controller This controller's instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_order_recurring_charge_gateway_payments_disabled', $order_id, $gateway, $this );\n\t\t\tllms_log(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Recurring charge for order #%d could not be processed because the gateway no longer supports recurring payments.',\n\t\t\t\t\t$order_id\n\t\t\t\t),\n\t\t\t\t'recurring-payments'\n\t\t\t);\n\n\t\t\t$order->add_note( __( 'Recurring charge skipped because recurring payments are disabled for the payment gateway.', 'lifterlms' ) );\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// Recurring payments disabled as a site feature when in staging mode.\n\t\tif ( ! LLMS_Site::get_feature( 'recurring_payments' ) ) {\n\n\t\t\t/**\n\t\t\t * Fired when a LifterLMS order's recurring charge errors because the recurring payments site feature is disabled.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int                    $order_id   WP Post ID of the order.\n\t\t\t * @param LLMS_Payment_Gateway   $gateway    LLMS_Payment_Gateway extending class instance.\n\t\t\t * @param LLMS_Controller_Orders $controller This controller's instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_order_recurring_charge_skipped', $order_id, $gateway, $this );\n\t\t\t$order->add_note( __( 'Recurring charge skipped because recurring payments are disabled in staging mode.', 'lifterlms' ) );\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// Related product removed.\n\t\tif ( empty( $order->get_product() ) ) {\n\n\t\t\t/**\n\t\t\t * Fired when a LifterLMS order's recurring charge errors because the purchased product (Course/Membership) doesn't exist anymore.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int                    $order_id   WP Post ID of the order.\n\t\t\t * @param LLMS_Controller_Orders $controller This controller's instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_order_recurring_charge_aborted_product_deleted', $order_id, $this );\n\t\t\tllms_log(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Recurring charge for order #%d could not be processed because the product #%d does not exist anymore.',\n\t\t\t\t\t$order_id,\n\t\t\t\t\t$order->get( 'product_id' )\n\t\t\t\t),\n\t\t\t\t'recurring-payments'\n\t\t\t);\n\n\t\t\t$order->add_note( __( 'Recurring charge aborted because the purchased product does not exist anymore.', 'lifterlms' ) );\n\t\t\treturn false;\n\n\t\t}\n\n\t\t// Passed validation, hand off to the gateway.\n\t\t$gateway->handle_recurring_transaction( $order );\n\t\treturn true;\n\t}\n\n\t/**\n\t * Sets an order's post status to `llms-pending` when untrashing an order.\n\t *\n\t * This is a filter hook callback for the WP core filter `wp_untrash_post_status`.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $new_status      The new status of the post after untrashing.\n\t * @param int    $post_id         The WP_Post ID of the order.\n\t * @param string $previous_status The status of the post at the point where it was trashed.\n\t * @return string\n\t */\n\tpublic function set_untrash_status( $new_status, $post_id, $previous_status ) {\n\n\t\tif ( 'llms_order' === get_post_type( $post_id ) ) {\n\t\t\t/**\n\t\t\t * Filters the status that an order post gets assigned when it is restored from the trash.\n\t\t\t *\n\t\t\t * This is a filter nearly identical to `wp_untrash_post_status` applied specifically to `llms_order` posts.\n\t\t\t *\n\t\t\t * @since 7.0.0\n\t\t\t *\n\t\t\t * @link https://developer.wordpress.org/reference/hooks/wp_untrash_post_status/\n\t\t\t *\n\t\t\t * @param string $new_status      The new status of the post being restored.\n\t\t\t * @param int    $post_id         The ID of the post being restored.\n\t\t\t * @param string $previous_status The status of the post at the point where it was trashed.\n\t\t\t */\n\t\t\t$new_status = apply_filters( 'llms_untrash_order_status', 'llms-pending', $post_id, $previous_status );\n\t\t}\n\n\t\treturn $new_status;\n\t}\n\n\t/**\n\t * When a transaction fails, update the parent order's status.\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t *\n\t * @param LLMS_Transaction $txn Instance of the LLMS_Transaction.\n\t * @return void\n\t */\n\tpublic function transaction_failed( $txn ) {\n\n\t\t$order = $txn->get_order();\n\n\t\t// Halt if legacy.\n\t\tif ( $order->is_legacy() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( $order->can_be_retried() ) {\n\n\t\t\t$order->maybe_schedule_retry();\n\n\t\t} else {\n\n\t\t\t$order->set( 'status', 'llms-failed' );\n\n\t\t}\n\t}\n\n\t/**\n\t * When a transaction is refunded, update the parent order's status.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param LLMS_Transaction $txn Instance of the LLMS_Transaction.\n\t * @return void\n\t */\n\tpublic function transaction_refunded( $txn ) {\n\n\t\t$order = $txn->get_order();\n\n\t\t// Halt if legacy.\n\t\tif ( $order->is_legacy() ) {\n\t\t\treturn; }\n\n\t\t$order->set( 'status', 'llms-refunded' );\n\t}\n\n\t/**\n\t * When a transaction succeeds, update the parent order's status.\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t *\n\t * @param LLMS_Transaction $txn Instance of the LLMS_Transaction.\n\t * @return void\n\t */\n\tpublic function transaction_succeeded( $txn ) {\n\n\t\t// Get the order.\n\t\t$order = $txn->get_order();\n\n\t\t// Halt if legacy.\n\t\tif ( $order->is_legacy() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Update the status based on the order type.\n\t\t$status = $order->is_recurring() ? 'llms-active' : 'llms-completed';\n\t\t$order->set( 'status', $status );\n\t\t$order->set( 'last_retry_rule', '' ); // Retries should always start with tne first rule for new transactions.\n\n\t\t// Maybe schedule a payment.\n\t\t$order->maybe_schedule_payment();\n\t}\n\n\t/**\n\t * Trigger actions when the status of LifterLMS Orders and LifterLMS Transactions change status.\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @param string  $new_status New status.\n\t * @param string  $old_status Old status.\n\t * @param WP_Post $post       WP_Post instance of the transaction.\n\t * @return void\n\t */\n\tpublic function transition_status( $new_status, $old_status, $post ) {\n\n\t\t// Don't do anything if the status hasn't changed.\n\t\tif ( $new_status === $old_status ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// We're only concerned with order post statuses here.\n\t\tif ( 'llms_order' !== $post->post_type && 'llms_transaction' !== $post->post_type ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$post_type = str_replace( 'llms_', '', $post->post_type );\n\t\t$obj       = 'order' === $post_type ? new LLMS_Order( $post ) : new LLMS_Transaction( $post );\n\n\t\t// Record order status changes as notes.\n\t\tif ( 'order' === $post_type ) {\n\t\t\t/* translators: %1$s: Old Status, %2$s: New status. */\n\t\t\t$obj->add_note( sprintf( __( 'Order status changed from %1$s to %2$s', 'lifterlms' ), llms_get_order_status_name( $old_status ), llms_get_order_status_name( $new_status ) ) );\n\t\t}\n\n\t\t// Remove prefixes from all the things.\n\t\t$new_status = str_replace( array( 'llms-', 'txn-' ), '', $new_status );\n\t\t$old_status = str_replace( array( 'llms-', 'txn-' ), '', $old_status );\n\n\t\t/**\n\t\t * Fired when a LifterLMS order or transaction changes status.\n\t\t *\n\t\t * The first dynamic portion of this hook, `$post_type`, refers to the unprefixed object post type ('order|transaction').\n\t\t * The second dynamic portion of this hook, `$old_status`, refers to the previous object status.\n\t\t * The third dynamic portion of this hook, `$new_status`, refers to the new object status.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param LLMS_Order|LLMS_Transaction $object     The LifterLMS order or transaction instance.\n\t\t * @param string                      $old_status The previous order or transaction status.\n\t\t * @param string                      $new_status The new order or transaction status.\n\t\t */\n\t\tdo_action( \"lifterlms_{$post_type}_status_{$old_status}_to_{$new_status}\", $obj, $old_status, $new_status );\n\n\t\t/**\n\t\t * Fired when a LifterLMS order or transaction changes status.\n\t\t *\n\t\t * The first dynamic portion of this hook, `$post_type`, refers to the unprefixed object post type ('order|transaction').\n\t\t * The second dynamic portion of this hook, `$new_status`, refers to the new object status.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param LLMS_Order|LLMS_Transaction $object     The LifterLMS order or transaction instance.\n\t\t * @param string                      $old_status The previous order or transaction status.\n\t\t * @param string                      $new_status The new order or transaction status.\n\t\t */\n\t\tdo_action( \"lifterlms_{$post_type}_status_{$new_status}\", $obj, $old_status, $new_status );\n\t}\n\n\t/**\n\t * Validate a gateway can be used to process the current action / transaction.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param string           $gateway_id Gateway's id.\n\t * @param LLMS_Access_Plan $plan       Instance of the LLMS_Access_Plan related to the action/transaction.\n\t * @return WP_Error|LLMS_Payment_Gateway WP_Error or LLMS_Payment_Gateway subclass.\n\t */\n\tprivate function validate_selected_gateway( $gateway_id, $plan ) {\n\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( $gateway_id );\n\t\t$err     = new WP_Error();\n\n\t\t// Valid gateway.\n\t\tif ( is_subclass_of( $gateway, 'LLMS_Payment_Gateway' ) ) {\n\n\t\t\t// Gateway not enabled.\n\t\t\tif ( 'manual' !== $gateway->get_id() && ! $gateway->is_enabled() ) {\n\n\t\t\t\treturn $err->add( 'gateway-error', __( 'The selected payment gateway is not currently enabled.', 'lifterlms' ) );\n\n\t\t\t\t// It's a recurring plan and the gateway doesn't support recurring.\n\t\t\t} elseif ( $plan->is_recurring() && ! $gateway->supports( 'recurring_payments' ) ) {\n\t\t\t\t// Translators: %s = The gateway display name.\n\t\t\t\treturn $err->add( 'gateway-error', sprintf( __( '%s does not support recurring payments and cannot process this transaction.', 'lifterlms' ), $gateway->get_title() ) );\n\n\t\t\t\t// Not recurring and the gateway doesn't support single payments.\n\t\t\t} elseif ( ! $plan->is_recurring() && ! $gateway->supports( 'single_payments' ) ) {\n\t\t\t\t// Translators: %s = The gateway display name.\n\t\t\t\treturn $err->add( 'gateway-error', sprintf( __( '%s does not support single payments and cannot process this transaction.', 'lifterlms' ), $gateway->get_title() ) );\n\n\t\t\t}\n\t\t} else {\n\n\t\t\treturn $err->add( 'invalid-gateway', __( 'An invalid payment method was selected.', 'lifterlms' ) );\n\n\t\t}\n\n\t\treturn $gateway;\n\t}\n\n\t/**\n\t * Confirm order form post.\n\t *\n\t * User clicks confirm order or gateway determines the order is confirmed.\n\t *\n\t * Executes payment gateway confirm order method and completes order.\n\t * Redirects user to appropriate page / post\n\t *\n\t * @since 3.0.0\n\t * @since 3.4.0 Unknown.\n\t * @since 3.34.4 Added filter `llms_order_can_be_confirmed`.\n\t * @since 3.34.5 Fixed logic error in `llms_order_can_be_confirmed` conditional.\n\t * @since 3.35.0 Return early if nonce doesn't pass verification and sanitize `$_POST` data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @deprecated 7.0.0 Deprecated in favor of {@see LLMS_Controller_Checkout::confirm_pending_order()}.\n\t *\n\t * @return void\n\t */\n\tpublic function confirm_pending_order() {\n\t\t_deprecated_function( __METHOD__, '7.0.0', 'LLMS_Controller_Checkout::confirm_pending_order' );\n\t\tLLMS_Controller_Checkout::instance()->confirm_pending_order();\n\t}\n\n\t/**\n\t * Handle form submission of the checkout / payment form.\n\t *\n\t *      1. Logs in or Registers a user\n\t *      2. Validates all fields\n\t *      3. Handles coupon pricing adjustments\n\t *      4. Creates a PENDING llms_order\n\t *\n\t *      If errors, returns error on screen to user\n\t *      If success, passes to the selected gateways \"process_payment\" method\n\t *          the process_payment method should complete by returning an error or\n\t *          triggering the \"lifterlms_process_payment_redirect\" // Todo check this last statement.\n\t *\n\t * @since 3.0.0\n\t * @since 3.27.0 Unknown.\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 5.0.0 Build customer data using LLMS_Forms fields information.\n\t * @since 5.0.1 Delegate sanitization of user information fields of the `$_POST` to LLMS_Form_Handler::submit().\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @deprecated 7.0.0 Deprecated in favor of {@see LLMS_Controller_Checkout::create_pending_order()}.\n\t *\n\t * @return void\n\t */\n\tpublic function create_pending_order() {\n\t\t_deprecated_function( __METHOD__, '7.0.0', 'LLMS_Controller_Checkout::create_pending_order' );\n\t\tLLMS_Controller_Checkout::instance()->create_pending_order();\n\t}\n\n\n\t/**\n\t * Handle form submission of the \"Update Payment Method\" form on the student dashboard when viewing a single order.\n\t *\n\t * @since 3.10.0\n\t * @since 3.19.0 Unknown.\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @deprecated 7.0.0 Deprecated in favor of {@see LLMS_Controller_Checkout::switch_payment_source()}.\n\t *\n\t * @return void\n\t */\n\tpublic function switch_payment_source() {\n\t\t_deprecated_function( __METHOD__, '7.0.0', 'LLMS_Controller_Checkout::switch_payment_source' );\n\t\tLLMS_Controller_Checkout::instance()->switch_payment_source();\n\t}\n}\n\nreturn new LLMS_Controller_Orders();\n"
  },
  {
    "path": "includes/controllers/class.llms.controller.quizzes.php",
    "content": "<?php\n/**\n * LLMS_Controller_Quizzes class file\n *\n * @package LifterLMS/Controllers/Classes\n *\n * @since 3.9.0\n * @version 5.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Quiz related form controller\n *\n * @since 3.9.0\n * @since 5.0.0 Removed previously deprecated method `LLMS_Controller_Quizzes::take_quiz()`.\n */\nclass LLMS_Controller_Quizzes {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.9.0\n\t * @since 3.37.8 Add reporting actions handler action.\n\t * @since 4.14.0 Remove `add_action()` for deprecated `take_quiz()` method.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'admin_init', array( $this, 'maybe_handle_reporting_actions' ) );\n\n\t}\n\n\t/**\n\t * Handle quiz reporting screen actions buttons\n\t *\n\t * On the quiz reporting screen this allows orphaned quizzes to be deleted.\n\t *\n\t * @since 3.37.8\n\t * @since 5.1.0 Use a deep orphan check to determine if the quiz can be deleted.\n\t * @since 9.2.3 Add capability check before deleting quiz.\n\t *\n\t * @return null|false|WP_Post `null` if the form wasn't submitted or the nonce couldn't be verified.\n\t *                            `false` if an error was encountered.\n\t *                            `WP_Post` of the deleted quiz on success.\n\t */\n\tpublic function maybe_handle_reporting_actions() {\n\n\t\tif ( ! isset( $_REQUEST['_llms_quiz_actions_nonce'] ) ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_quiz_actions_nonce'] ) ), 'llms-quiz-actions' ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$id = llms_filter_input( INPUT_POST, 'llms_del_quiz', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( $id && 'llms_quiz' === get_post_type( $id ) ) {\n\t\t\tif ( ! current_user_can( 'delete_post', $id ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t$quiz = llms_get_post( $id );\n\t\t\tif ( $quiz && ( $quiz->is_orphan( true ) || ! $quiz->get_course() ) ) {\n\t\t\t\treturn wp_delete_post( $id, true );\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n}\n\nreturn new LLMS_Controller_Quizzes();\n"
  },
  {
    "path": "includes/controllers/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widget-base.php",
    "content": "<?php\n\nabstract class LLMS_Elementor_Widget_Base extends \\Elementor\\Widget_Base {\n\n\tpublic function __construct( $data = array(), $args = null ) {\n\t\tparent::__construct( $data, $args );\n\t}\n\n\tpublic function get_icon() {\n\t\treturn 'dashicons-before dashicons-welcome-learn-more';\n\t}\n\n\tpublic function get_categories() {\n\t\treturn array( 'lifterlms' );\n\t}\n\n\tprotected function add_footer_promo_control() {\n\n\t\t$this->add_control(\n\t\t\t'llms_footer_promo',\n\t\t\tarray(\n\t\t\t\t'label'           => '',\n\t\t\t\t'type'            => \\Elementor\\Controls_Manager::RAW_HTML,\n\t\t\t\t'raw'             => '<hr><p style=\"margin-top: 20px;\">' .\n\t\t\t\t\t\t\t\t\t/* translators: %1$s: Opening learn more link tag, %2$s: Closing link tag. */\n\t\t\t\t\t\t\t\t\tsprintf( esc_html__( 'Learn more about %1$sediting LifterLMS courses with Elementor%2$s', 'lifterlms' ), '<a target=\"_blank\" href=\"https://lifterlms.com/docs/how-to-edit-courses-with-elementor/?utm_source=LifterLMS%20Plugin&utm_medium=Elementor%20Edit%20Panel%20&utm_campaign=Plugin%20to%20Sale\">', '</a>' ) .\n\t\t\t\t\t\t\t\t\t'</p>',\n\t\t\t\t'content_classes' => 'lifterlms-notice',\n\t\t\t)\n\t\t);\n\t}\n\n\tprotected function render() {\n\t\t$settings = $this->get_settings_for_display();\n\n\t\techo do_shortcode( '[lifterlms_course_continue_button]' );\n\t}\n\n\tprotected function _content_template() {\n\t\t// Define your template variables here\n\t}\n}\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widget-course-continue-button.php",
    "content": "<?php\n\nclass LLMS_Elementor_Widget_Course_Continue_Button extends LLMS_Elementor_Widget_Base {\n\n\tpublic function get_name() {\n\t\treturn 'llms_course_continue_button_widget';\n\t}\n\n\tpublic function get_title() {\n\t\treturn __( 'Course Continue Button', 'lifterlms' );\n\t}\n\n\tprotected function _register_controls() {\n\t\t$this->start_controls_section(\n\t\t\t'content_section',\n\t\t\tarray(\n\t\t\t\t'label' => __( 'Course Continue Button', 'lifterlms' ),\n\t\t\t\t'tab'   => \\Elementor\\Controls_Manager::TAB_CONTENT,\n\t\t\t)\n\t\t);\n\n\t\t$this->add_control(\n\t\t\t'description',\n\t\t\tarray(\n\t\t\t\t'label'     => esc_html__( 'Show course continue button to students for the current course.', 'lifterlms' ),\n\t\t\t\t'type'      => \\Elementor\\Controls_Manager::HEADING,\n\t\t\t\t'separator' => 'before',\n\t\t\t)\n\t\t);\n\n\t\t$this->add_footer_promo_control();\n\n\t\t$this->end_controls_section();\n\t}\n\n\tprotected function render() {\n\t\t$settings = $this->get_settings_for_display();\n\n\t\techo do_shortcode( '[lifterlms_course_continue_button]' );\n\t}\n}\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widget-course-instructors.php",
    "content": "<?php\n\nclass LLMS_Elementor_Widget_Course_Instructors extends LLMS_Elementor_Widget_Base {\n\n\tpublic function get_name() {\n\t\treturn 'llms_course_instructors_widget';\n\t}\n\n\tpublic function get_title() {\n\t\treturn __( 'Course Instructors', 'lifterlms' );\n\t}\n\n\tprotected function _register_controls() {\n\t\t$this->start_controls_section(\n\t\t\t'content_section',\n\t\t\tarray(\n\t\t\t\t'label' => __( 'Course Instructors', 'lifterlms' ),\n\t\t\t\t'tab'   => \\Elementor\\Controls_Manager::TAB_CONTENT,\n\t\t\t)\n\t\t);\n\n\t\t$this->add_control(\n\t\t\t'description',\n\t\t\tarray(\n\t\t\t\t'label'     => esc_html__( 'Show current course instructors.', 'lifterlms' ),\n\t\t\t\t'type'      => \\Elementor\\Controls_Manager::HEADING,\n\t\t\t\t'separator' => 'before',\n\t\t\t)\n\t\t);\n\n\t\t$this->add_footer_promo_control();\n\n\t\t$this->end_controls_section();\n\t}\n\n\tprotected function render() {\n\t\t$settings = $this->get_settings_for_display();\n\n\t\techo do_shortcode( '[lifterlms_course_instructors]' );\n\t}\n}\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widget-course-meta-info.php",
    "content": "<?php\n\nclass LLMS_Elementor_Widget_Course_Meta_Info extends LLMS_Elementor_Widget_Base {\n\n\tpublic function get_name() {\n\t\treturn 'llms_course_meta_information_widget';\n\t}\n\n\tpublic function get_title() {\n\t\treturn __( 'Course Meta Information', 'lifterlms' );\n\t}\n\n\tprotected function _register_controls() {\n\t\t$this->start_controls_section(\n\t\t\t'content_section',\n\t\t\tarray(\n\t\t\t\t'label' => __( 'Course Meta Information', 'lifterlms' ),\n\t\t\t\t'tab'   => \\Elementor\\Controls_Manager::TAB_CONTENT,\n\t\t\t)\n\t\t);\n\n\t\t$this->add_control(\n\t\t\t'description',\n\t\t\tarray(\n\t\t\t\t'label'     => esc_html__( 'Show current course meta information.', 'lifterlms' ),\n\t\t\t\t'type'      => \\Elementor\\Controls_Manager::HEADING,\n\t\t\t\t'separator' => 'before',\n\t\t\t)\n\t\t);\n\n\t\t$this->add_footer_promo_control();\n\n\t\t$this->end_controls_section();\n\t}\n\n\tprotected function render() {\n\t\t$settings = $this->get_settings_for_display();\n\n\t\techo do_shortcode( '[lifterlms_course_meta_info]' );\n\t}\n}\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widget-course-progress.php",
    "content": "<?php\n\nclass LLMS_Elementor_Widget_Course_Progress extends LLMS_Elementor_Widget_Base {\n\n\tpublic function get_name() {\n\t\treturn 'llms_course_progress_widget';\n\t}\n\n\tpublic function get_title() {\n\t\treturn __( 'Course Progress', 'lifterlms' );\n\t}\n\n\tprotected function _register_controls() {\n\t\t$this->start_controls_section(\n\t\t\t'content_section',\n\t\t\tarray(\n\t\t\t\t'label' => __( 'Course Progress', 'lifterlms' ),\n\t\t\t\t'tab'   => \\Elementor\\Controls_Manager::TAB_CONTENT,\n\t\t\t)\n\t\t);\n\n\t\t$this->add_control(\n\t\t\t'description',\n\t\t\tarray(\n\t\t\t\t'label'     => esc_html__( 'Show course progress to students for the current course.', 'lifterlms' ),\n\t\t\t\t'type'      => \\Elementor\\Controls_Manager::HEADING,\n\t\t\t\t'separator' => 'before',\n\t\t\t)\n\t\t);\n\n\t\t$this->add_footer_promo_control();\n\n\t\t$this->end_controls_section();\n\t}\n\n\tprotected function render() {\n\t\t$settings = $this->get_settings_for_display();\n\n\t\techo do_shortcode( '[lifterlms_course_progress check_enrollment=\"1\"]' );\n\t}\n}\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widget-course-syllabus.php",
    "content": "<?php\n\nclass LLMS_Elementor_Widget_Course_Syllabus extends LLMS_Elementor_Widget_Base {\n\n\tpublic function get_name() {\n\t\treturn 'llms_course_syllabus_widget';\n\t}\n\n\tpublic function get_title() {\n\t\treturn __( 'Course Syllabus', 'lifterlms' );\n\t}\n\n\tprotected function _register_controls() {\n\t\t$this->start_controls_section(\n\t\t\t'content_section',\n\t\t\tarray(\n\t\t\t\t'label' => __( 'Course Syllabus', 'lifterlms' ),\n\t\t\t\t'tab'   => \\Elementor\\Controls_Manager::TAB_CONTENT,\n\t\t\t)\n\t\t);\n\n\t\t$this->add_control(\n\t\t\t'description',\n\t\t\tarray(\n\t\t\t\t'label'     => esc_html__( 'Show course syllabus for the current course.', 'lifterlms' ),\n\t\t\t\t'type'      => \\Elementor\\Controls_Manager::HEADING,\n\t\t\t\t'separator' => 'before',\n\t\t\t)\n\t\t);\n\n\t\t$this->add_footer_promo_control();\n\n\t\t$this->end_controls_section();\n\t}\n\n\tprotected function render() {\n\t\t$settings = $this->get_settings_for_display();\n\n\t\techo do_shortcode( '[lifterlms_course_syllabus]' );\n\t}\n}\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widget-pricing-table.php",
    "content": "<?php\n\nclass LLMS_Elementor_Widget_Pricing_Table extends LLMS_Elementor_Widget_Base {\n\n\tpublic function get_name() {\n\t\treturn 'llms_pricing_table_widget';\n\t}\n\n\tpublic function get_title() {\n\t\treturn __( 'Pricing Table', 'lifterlms' );\n\t}\n\n\tprotected function _register_controls() {\n\t\t$this->start_controls_section(\n\t\t\t'content_section',\n\t\t\tarray(\n\t\t\t\t'label' => __( 'Pricing Table', 'lifterlms' ),\n\t\t\t\t'tab'   => \\Elementor\\Controls_Manager::TAB_CONTENT,\n\t\t\t)\n\t\t);\n\n\t\t$this->add_control(\n\t\t\t'description',\n\t\t\tarray(\n\t\t\t\t'label'     => esc_html__( 'Show pricing table for the current course.', 'lifterlms' ),\n\t\t\t\t'type'      => \\Elementor\\Controls_Manager::HEADING,\n\t\t\t\t'separator' => 'before',\n\t\t\t)\n\t\t);\n\n\t\t$this->add_footer_promo_control();\n\n\t\t$this->end_controls_section();\n\t}\n\n\tprotected function render() {\n\t\t$settings = $this->get_settings_for_display();\n\n\t\techo do_shortcode( '[lifterlms_pricing_table]' );\n\t}\n}\n"
  },
  {
    "path": "includes/elementor/class-llms-elementor-widgets.php",
    "content": "<?php\n/**\n * LifterLMS Elementor Widgets\n *\n * @package LifterLMS/Classes\n *\n * @since 7.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Elementor_Widgets\n *\n * @since 7.7.0\n */\nclass LLMS_Elementor_Widgets {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'elementor/widgets/widgets_registered', array( $this, 'init' ) );\n\t\tadd_action( 'elementor/elements/categories_registered', array( $this, 'add_widget_categories' ) );\n\t\tadd_filter( 'llms_render_block', array( $this, 'maybe_stop_rendering_block' ), 10, 2 );\n\t}\n\n\t/**\n\t * Avoid rendering blocks on the front-end that are in an Elementor page (ie. a Text Editor widget when page/post first edited).\n\t *\n\t * @param $should_render bool Whether to render the block or not.\n\t * @param $block WP_Block The block instance.\n\t *\n\t * @return false|mixed\n\t */\n\tfunction maybe_stop_rendering_block( $should_render, $block ) {\n\t\tif ( ! class_exists( 'Elementor\\Plugin' ) || ! method_exists( 'Elementor\\Plugin', 'instance' ) ) {\n\t\t\treturn $should_render;\n\t\t}\n\n\t\t$instance = Elementor\\Plugin::instance();\n\n\t\tif ( ! $instance ) {\n\t\t\treturn $should_render;\n\t\t}\n\n\t\t$documents = $instance->documents;\n\n\t\tif ( ! $documents || ! method_exists( $documents, 'get' ) ) {\n\t\t\treturn $should_render;\n\t\t}\n\n\t\t$document = $documents->get( get_the_ID() );\n\n\t\tif ( ! $document || ! method_exists( $document, 'is_built_with_elementor' ) ) {\n\t\t\treturn $should_render;\n\t\t}\n\n\t\tif ( $document->is_built_with_elementor() ) {\n\t\t\t$should_render = false;\n\t\t}\n\n\t\treturn $should_render;\n\t}\n\n\tpublic function init() {\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widget-base.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widget-course-meta-info.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widget-course-instructors.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widget-pricing-table.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widget-course-progress.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widget-course-continue-button.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/elementor/class-llms-elementor-widget-course-syllabus.php';\n\n\t\t\\Elementor\\Plugin::instance()->widgets_manager->register( new LLMS_Elementor_Widget_Course_Meta_Info() );\n\t\t\\Elementor\\Plugin::instance()->widgets_manager->register( new LLMS_Elementor_Widget_Course_Instructors() );\n\t\t\\Elementor\\Plugin::instance()->widgets_manager->register( new LLMS_Elementor_Widget_Pricing_Table() );\n\t\t\\Elementor\\Plugin::instance()->widgets_manager->register( new LLMS_Elementor_Widget_Course_Progress() );\n\t\t\\Elementor\\Plugin::instance()->widgets_manager->register( new LLMS_Elementor_Widget_Course_Continue_Button() );\n\t\t\\Elementor\\Plugin::instance()->widgets_manager->register( new LLMS_Elementor_Widget_Course_Syllabus() );\n\t}\n\n\tpublic function add_widget_categories( $elements_manager ) {\n\n\t\t$elements_manager->add_category(\n\t\t\t'lifterlms',\n\t\t\tarray(\n\t\t\t\t'title' => 'LifterLMS',\n\t\t\t\t'icon'  => 'dashicons-before dashicons-welcome-learn-more',\n\t\t\t)\n\t\t);\n\t}\n}\n\nreturn new LLMS_Elementor_Widgets();\n"
  },
  {
    "path": "includes/emails/class.llms.email.engagement.php",
    "content": "<?php\n/**\n * Engagement Email\n *\n * @package LifterLMS/Emails/Classes\n *\n * @since 1.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Engagement Email Class\n *\n * Generates emails and sends to user. Triggered from an engagement.\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Email_Engagement extends LLMS_Email {\n\n\t/**\n\t * Email identifier\n\t *\n\t * @since 1.0.0\n\t * @var string\n\t */\n\tprotected $id = 'engagement';\n\n\t/**\n\t * @since 3.8.0\n\t * @var WP_User\n\t */\n\tpublic $student;\n\n\t/**\n\t * Initialize all variables\n\t *\n\t * @since 1.0.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @param array $args Associative array of engagement args.\n\t * @return void\n\t */\n\tpublic function init( $args ) {\n\n\t\t$this->student    = new WP_User( $args['person_id'] );\n\t\t$this->email_post = get_post( $args['email_id'] );\n\n\t\t$this->add_merge_data(\n\t\t\tarray(\n\t\t\t\t'{user_login}'    => stripslashes( $this->student->user_login ),\n\t\t\t\t'{first_name}'    => stripslashes( $this->student->first_name ),\n\t\t\t\t'{last_name}'     => stripslashes( $this->student->last_name ),\n\t\t\t\t'{email_address}' => stripslashes( $this->student->user_email ),\n\t\t\t\t'{site_url}'      => get_permalink( llms_get_page_id( 'myaccount' ) ),\n\t\t\t\t'{current_date}'  => date_i18n( get_option( 'date_format' ), current_time( 'timestamp' ) ),\n\t\t\t)\n\t\t);\n\n\t\t// Setup subject, headline, & body.\n\t\t$this->body    = $this->email_post->post_content;\n\t\t$this->subject = get_post_meta( $this->email_post->ID, '_llms_email_subject', true );\n\t\t$this->heading = get_post_meta( $this->email_post->ID, '_llms_email_heading', true );\n\n\t\t// Setup all the recipients.\n\t\tforeach ( array( 'to', 'cc', 'bcc' ) as $type ) {\n\n\t\t\t$list = get_post_meta( $this->email_post->ID, '_llms_email_' . $type, true );\n\n\t\t\t// Fall back to student email for existing emails with no definition.\n\t\t\tif ( ! $list && 'to' === $type ) {\n\t\t\t\t$list = '{student_email}';\n\t\t\t}\n\n\t\t\tif ( ! $list ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tforeach ( $this->merge_emails( $list ) as $email ) {\n\t\t\t\t$this->add_recipient( $email, $type );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Handles email merge codes that can be used in the to, cc, and bcc fields\n\t *\n\t * @since 3.1.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @param string $list Unmerged, comma-separated list of emails\n\t * @return array\n\t */\n\tprivate function merge_emails( $list ) {\n\n\t\t$codes = array(\n\t\t\t'{student_email}',\n\t\t\t'{admin_email}',\n\t\t);\n\n\t\t$addresses = array(\n\t\t\t$this->student->ID,\n\t\t\tget_option( 'admin_email' ),\n\t\t);\n\n\t\t$merged = str_replace( $codes, $addresses, $list );\n\t\t$array  = explode( ',', $merged );\n\t\treturn array_map( 'trim', $array );\n\n\t}\n\n\t/**\n\t * Send email\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function send() {\n\n\t\tadd_filter( 'llms_user_info_shortcode_user_id', array( $this, 'set_shortcode_user' ) );\n\n\t\t$ret = parent::send();\n\n\t\tremove_filter( 'llms_user_info_shortcode_user_id', array( $this, 'set_shortcode_user' ) );\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Set the user ID used by [llms-user] to the user receiving the email.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param int $uid WP_User ID of the current user.\n\t * @return int\n\t */\n\tpublic function set_shortcode_user( $uid ) {\n\t\treturn $this->student->ID;\n\t}\n\n}\n"
  },
  {
    "path": "includes/emails/class.llms.email.php",
    "content": "<?php\n/**\n * Email Base\n *\n * @package LifterLMS/Emails/Classes\n *\n * @since 1.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Email Base Class\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 4.0.0 Always supply a from address even if the option is empty.\n */\nclass LLMS_Email {\n\n\t/**\n\t * @var array\n\t * @since 3.15.0\n\t */\n\tprivate $attachments = array();\n\n\t/**\n\t * @var string\n\t * @since 3.8.0\n\t */\n\tprotected $body = '';\n\n\t/**\n\t * @var string\n\t * @since 3.8.0\n\t */\n\tprotected $content_type = 'text/html';\n\n\t/**\n\t * @var WP_Post\n\t * @since 3.26.1\n\t */\n\tpublic $email_post;\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tprivate $find = array();\n\n\t/**\n\t * @var array\n\t * @since 3.8.0\n\t */\n\tprivate $headers = array();\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tprotected $heading = '';\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tprotected $id = 'generic';\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tprivate $recipient = array();\n\n\t/**\n\t * @var array\n\t * @since 1.0.0\n\t */\n\tprivate $replace = array();\n\n\t/**\n\t * @var string\n\t * @since 1.0.0\n\t */\n\tprotected $subject = '';\n\n\t/**\n\t * @var string\n\t * @since 3.8.0\n\t */\n\tprotected $template_html = 'emails/template.php';\n\n\t/**\n\t * Initializer\n\t * Children can configure the email in this function called by the __construct() function\n\t *\n\t * @param    array $args  optional arguments passed in from the constructor\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function init( $args ) {}\n\n\t/**\n\t * Constructor\n\t * Sets up data needed to generate email content\n\t *\n\t * @since    1.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function __construct( $args = array() ) {\n\n\t\t$this->add_header( 'Content-Type', $this->get_content_type() );\n\n\t\t$this->add_merge_data(\n\t\t\tarray(\n\t\t\t\t'{blogname}'     => get_bloginfo( 'name', 'display' ),\n\t\t\t\t'{site_title}'   => get_bloginfo( 'name', 'display' ),\n\t\t\t\t'{divider}'      => llms()->mailer()->get_divider_html(),\n\t\t\t\t'{button_style}' => llms()->mailer()->get_button_style(),\n\t\t\t)\n\t\t);\n\n\t\t$this->init( $args );\n\n\t}\n\n\t/**\n\t * Add an attachment to the email\n\t *\n\t * @param    string $attachment  full system path to a file to attach\n\t * @return   void\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function add_attachment( $attachment ) {\n\n\t\tarray_push( $this->attachments, $attachment );\n\n\t}\n\n\t/**\n\t * Add a single header to the email headers array\n\t *\n\t * @param    string $key   header key eg: 'Cc'\n\t * @param    string $val   header value eg: 'noreply@website.tld'\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function add_header( $key, $val ) {\n\n\t\tarray_push( $this->headers, sprintf( '%1$s: %2$s', $key, $val ) );\n\n\t}\n\n\t/**\n\t * Add merge data that will be used in the email\n\t *\n\t * @param    array $data    associative array where\n\t *                             $key = merge field\n\t *                             $val = merge value\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function add_merge_data( $data = array() ) {\n\n\t\tforeach ( $data as $find => $replace ) {\n\n\t\t\tarray_push( $this->find, $find );\n\t\t\tarray_push( $this->replace, $replace );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Add a single recipient for sending to, cc, or bcc\n\t *\n\t * @param    int|string $address  if string, must be a valid email address\n\t *                                if int, must be the WP User ID of a user\n\t * @param    string     $type     recipient type [to,cc,bcc]\n\t * @param    string     $name     recipient name (optional)\n\t * @return   boolean\n\t * @since    3.8.0\n\t * @version  3.10.1\n\t */\n\tpublic function add_recipient( $address, $type = 'to', $name = '' ) {\n\n\t\t// If an ID was supplied, get the information from the student object.\n\t\tif ( is_numeric( $address ) ) {\n\t\t\t$student = llms_get_student( $address );\n\t\t\tif ( ! $student ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t$address = $student->get( 'user_email' );\n\t\t\t$name    = $student->get_name();\n\t\t}\n\n\t\t// Ensure address is a valid email.\n\t\tif ( ! filter_var( $address, FILTER_VALIDATE_EMAIL ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// If a name is supplied format the name & address.\n\t\tif ( $name ) {\n\t\t\t$address = sprintf( '%1$s <%2$s>', $name, $address );\n\t\t}\n\n\t\tif ( 'to' === $type ) {\n\n\t\t\tarray_push( $this->recipient, $address );\n\t\t\treturn true;\n\n\t\t} elseif ( 'cc' === $type || 'bcc' === $type ) {\n\n\t\t\t$this->add_header( ucfirst( $type ), $address );\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Add multiple recipients\n\t *\n\t * @param    array $recipients  array of recipient information\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function add_recipients( $recipients = array() ) {\n\n\t\tforeach ( $recipients as $data ) {\n\n\t\t\t$data = wp_parse_args(\n\t\t\t\t$data,\n\t\t\t\tarray(\n\t\t\t\t\t'address' => '',\n\t\t\t\t\t'type'    => 'to',\n\t\t\t\t\t'name'    => '',\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tif ( $data['address'] ) {\n\t\t\t\t$this->add_recipient( $data['address'], $data['type'], $data['name'] );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t *  Format string method\n\t *\n\t *  Finds and replaces merge fields with appropriate data.\n\t *\n\t * @since 1.0.0\n\t * @since 5.0.0 Process shortocdes when formatting a string.\n\t *\n\t * @param string $string String to be formatted.\n\t * @return string\n\t */\n\tpublic function format_string( $string ) {\n\t\treturn do_shortcode( str_replace( $this->find, $this->replace, $string ) );\n\t}\n\n\t/**\n\t * Get attachments\n\t *\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_attachments() {\n\t\treturn apply_filters( 'llms_email_get_attachments', $this->attachments, $this );\n\t}\n\n\t/**\n\t * Get the body content of the email\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_body() {\n\t\treturn apply_filters( 'llms_email_body', $this->format_string( $this->body ), $this );\n\t}\n\n\t/**\n\t * Get email content\n\t *\n\t * @return string\n\t * @since    1.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_content() {\n\n\t\t$content = apply_filters( 'llms_email_content_get_content', $this->get_content_html(), $this );\n\t\treturn wordwrap( $content, 70 );\n\n\t}\n\n\t/**\n\t * Get the HTML email content\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.26.1\n\t */\n\tpublic function get_content_html() {\n\n\t\tglobal $post;\n\t\t$temp = null;\n\n\t\t// Override the $post global with the email post content (if it exists).\n\t\t// This fixes Elementor / WC conflict outlined at https://github.com/gocodebox/lifterlms/issues/730.\n\t\tif ( isset( $this->email_post ) ) {\n\t\t\t$temp = $post;\n\t\t\t$post = $this->email_post;\n\t\t}\n\n\t\tob_start();\n\t\tllms_get_template(\n\t\t\t$this->template_html,\n\t\t\tarray(\n\t\t\t\t'email_heading' => $this->get_heading(),\n\t\t\t\t'email_message' => $this->get_body(),\n\t\t\t)\n\t\t);\n\n\t\t$html = apply_filters( 'llms_email_content_get_content_html', ob_get_clean(), $this );\n\n\t\t// Restore the default $post global.\n\t\tif ( $temp ) {\n\t\t\t$post = $temp;\n\t\t}\n\n\t\treturn $html;\n\n\t}\n\n\t/**\n\t * Get the content type\n\t *\n\t * @return string\n\t * @since    1.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_content_type() {\n\t\treturn apply_filters( 'llms_email_content_type', $this->content_type, $this );\n\t}\n\n\t/**\n\t * Get from email option data\n\t *\n\t * @since 1.0.0\n\t * @since 4.0.0 Use the address provided by `wp_mail_from` as the default if no option is stored.\n\t *\n\t * @return string\n\t */\n\tpublic function get_from_address( $from_address ) {\n\t\treturn sanitize_email( get_option( 'lifterlms_email_from_address', $from_address ) );\n\t}\n\n\t/**\n\t * Get from name option data\n\t *\n\t * @return   string\n\t * @since    1.0.0\n\t * @version  1.0.0\n\t */\n\tpublic function get_from_name() {\n\t\treturn wp_specialchars_decode( esc_html( get_option( 'lifterlms_email_from_name' ) ), ENT_QUOTES );\n\t}\n\n\t/**\n\t * Get email headers\n\t *\n\t * @return   string|array\n\t * @since    1.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_headers() {\n\t\treturn apply_filters( 'lifterlms_email_headers', $this->headers, $this->id );\n\t}\n\n\t/**\n\t * Get the text of the email \"heading\"\n\t *\n\t * @return string\n\t * @since    1.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_heading() {\n\t\treturn apply_filters( 'lifterlms_email_heading', $this->format_string( $this->heading ), $this );\n\t}\n\n\t/**\n\t * Get recipient email address\n\t *\n\t * @return   string|array\n\t * @since    1.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_recipient() {\n\t\treturn apply_filters( 'lifterlms_email_recipient', $this->recipient, $this );\n\t}\n\n\t/**\n\t * Get email subject\n\t *\n\t * @return   string\n\t * @since    1.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_subject() {\n\t\treturn apply_filters( 'lifterlms_email_subject', $this->format_string( $this->subject ), $this );\n\t}\n\n\t/**\n\t * Set the body for the email\n\t *\n\t * @param    string $body   text or html body content for the email\n\t * @return   $this\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function set_body( $body = '' ) {\n\t\t$this->body = $body;\n\t\treturn $this;\n\t}\n\n\t/**\n\t * set the content_type for the email\n\t *\n\t * @param    string $content_type   content type (for the header)\n\t * @return   $this\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function set_content_type( $content_type = 'text/html' ) {\n\t\t$this->content_type = $content_type;\n\t\treturn $this;\n\t}\n\n\t/**\n\t * set the heading for the email\n\t *\n\t * @param    string $heading    text string to use for the email heading\n\t * @return   $this\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function set_heading( $heading = '' ) {\n\t\t$this->heading = $heading;\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Set the ID of the email\n\t *\n\t * @param    string $id   id string\n\t * @return   $this\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function set_id( $id = '' ) {\n\t\t$this->id = $id;\n\t\treturn $this;\n\t}\n\n\t/**\n\t * set the subject for the email\n\t *\n\t * @param    string $subject Text string to use for the email subject.\n\t * @return   $this\n\t * @since    3.8.0\n\t * @version  3.24.0\n\t */\n\tpublic function set_subject( $subject = '' ) {\n\t\t$this->subject = html_entity_decode( $subject, ENT_QUOTES );\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Send email\n\t *\n\t * @return bool\n\t * @since    1.0.0\n\t * @version  3.15.0\n\t */\n\tpublic function send() {\n\n\t\tdo_action( 'lifterlms_email_' . $this->id . '_before_send', $this );\n\n\t\tadd_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );\n\t\tadd_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );\n\t\tadd_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );\n\n\t\t$return = wp_mail( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );\n\n\t\tremove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );\n\t\tremove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );\n\t\tremove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );\n\n\t\tdo_action( 'lifterlms_email_' . $this->id . '_after_send', $this, $return );\n\n\t\treturn $return;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/emails/class.llms.email.reset.password.php",
    "content": "<?php\n/**\n * LifterLMS Password Reset Email\n *\n * @package LifterLMS/Emails/Classes\n *\n * @since 1.0.0\n * @version 3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Password Reset Email class\n *\n * @since 1.0.0\n * @version 3.8.0\n */\nclass LLMS_Email_Reset_Password extends LLMS_Email {\n\n\tprotected $id = 'reset_password';\n\n\t/**\n\t * Initializer\n\t *\n\t * @param    array $args  associative array of user related data for the email to be sent\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function init( $args = array() ) {\n\n\t\t$this->add_recipient( $args['user']->ID );\n\n\t\t$original_locale = get_locale();\n\t\t$locale          = get_user_locale( $args['user']->ID );\n\t\tif ( $locale && $locale !== $original_locale ) {\n\t\t\tswitch_to_locale( $locale );\n\t\t}\n\n\t\t$this->body    = $this->get_body_content( $args );\n\t\t$this->subject = __( 'Password Reset for {site_title}', 'lifterlms' );\n\t\t$this->heading = __( 'Reset Your Password', 'lifterlms' );\n\n\t\tif ( $locale && $locale !== $original_locale ) {\n\t\t\trestore_previous_locale();\n\t\t}\n\n\t\t$this->add_merge_data(\n\t\t\tarray(\n\t\t\t\t'{user_login}' => $args['login_display'],\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Custom content for the password reset email\n\t *\n\t * @param    array $data  associative array of user related data for the email to be sent\n\t * @since    3.8.0\n\t */\n\tpublic function get_body_content( $data ) {\n\n\t\t$url = esc_url(\n\t\t\tadd_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\t'key'   => $data['key'],\n\t\t\t\t\t'login' => rawurlencode( $data['user']->user_login ),\n\t\t\t\t),\n\t\t\t\twp_lostpassword_url()\n\t\t\t)\n\t\t);\n\n\t\tob_start();\n\t\tllms_get_template(\n\t\t\t'emails/reset-password.php',\n\t\t\tarray(\n\t\t\t\t'url' => $url,\n\t\t\t)\n\t\t);\n\t\treturn ob_get_clean();\n\t}\n}\n"
  },
  {
    "path": "includes/emails/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/forms/class-llms-form-field.php",
    "content": "<?php\n/**\n * Setup and render form fields.\n *\n * @package LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 6.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Form_Field class\n *\n * @since 5.0.0\n */\nclass LLMS_Form_Field {\n\n\t/**\n\t * Form Field Settings\n\t *\n\t * @var array {\n\t *     Array of field settings.\n\t *\n\t *     @type array           $attributes        Associative array of HTML attributes to add to the field element.\n\t *     @type bool            $checked           Determines if radio and checkbox fields are checked.\n\t *     @type int             $columns           Number of columns the field wrapper should occupy when rendered. Accepts integers >= 1 and <= 12.\n\t *     @type string[]|string $classes           Additional CSS classes to add to the field element. Accepts a string or an array of strings.\n\t *     @type string          $data_store        Determines where to store field values. Accepts \"users\" or \"usermeta\" to store on the respective WP core tables.\n\t *     @type string|false    $data_store_key    Determines the key name to use when storing the field value. Pass `false` to disable automatic storage. Defaults to the value of the `$name` property.\n\t *     @type string          $description       A string to use as the field's description or helper text.\n\t *     @type string          $default           The default value to use for the field.\n\t *     @type bool            $disabled          Whether or not the field is enabled.\n\t *     @type string          $id                The field's HTML \"id\" attribute. Must be unique. If not supplied, an ID is automatically generated.\n\t *     @type string          $label             Text to use in the label element associated with the field.\n\t *     @type bool            $label_show_empty  When true and no `$label` is supplied, will show an empty label element.\n\t *     @type bool            $last_column       When true, outputs a clearfix element following the element's wrapper. Allows ending a \"row\" of fields.\n\t *     @type bool            $match             Match this field to another field for validation purposes. Must be the `$id` of another field in the form.\n\t *     @type string          $name              The field's HTML \"name\" attribute. Default's to the value of `$id` when not supplied.\n\t *     @type array           $options           An associative array of options used for select, checkbox groups, and radio fields.\n\t *     @type string          $options_preset    A string representing a pre-defined set of `$options`. Accepts \"countries\" or \"states\". Custom presets can be defined using the filter \"llms_form_field_options_preset_{$preset_id}\".\n\t *     @type string          $placeholder       The field's HTML placeholder attribute.\n\t *     @type bool            $required          Determines if the field is marked as required.\n\t *     @type string          $selected          Alias of `$default`.\n\t *     @type string          $type              Field type. Accepts any HTML5 input type (text, email, tel, etc...), radio, checkbox, select, textarea, button, reset, submit, and html.\n\t *     @type string          $value             Value of the field.\n\t *     @type string          $visibility_toggle Determines if the field should show a button to toggle field masking (for password fields).\n\t *     @type string[]|string $wrapper_classes   Additional CSS classes to add to the field's wrapper element. Accepts a string or an array of strings.\n\t * }\n\t */\n\tprotected $settings = array();\n\n\t/**\n\t * Cached field HTML.\n\t *\n\t * @var string\n\t */\n\tprotected $html = '';\n\n\t/**\n\t * Data source where to get field value from.\n\t *\n\t * @var null|WP_Post|WP_User\n\t */\n\tprivate $data_source;\n\n\t/**\n\t * Data source type where to get field value from.\n\t *\n\t * @var null|string\n\t */\n\tprivate $data_source_type;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array      $settings    Field settings.\n\t * @param int|object $data_source Optional. Data source where to get field value from. Default is `null`.\n\t *                                Can be a WP_User or a WP_Post, or their id.\n\t *                                The actual object will be retrieved basing on the data_store.\n\t * @return void\n\t */\n\tpublic function __construct( $settings = array(), $data_source = null ) {\n\n\t\t/**\n\t\t * Filters the settings of a LifterLMS Form Field\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array           $settings Field settings.\n\t\t * @param LLMS_Form_Field $field    Form field class instance.\n\t\t */\n\t\t$this->settings = apply_filters( 'llms_field_settings', wp_parse_args( $settings, $this->get_defaults() ), $this );\n\n\t\t$this->define_data_source( $data_source );\n\n\t\t$this->prepare();\n\t}\n\n\t/**\n\t * Define the source of the data\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param int|object $data_source Data source where to get field value from.\n\t * @return void\n\t */\n\tprivate function define_data_source( $data_source ) {\n\n\t\tif ( empty( $this->settings['data_store'] ) || ! in_array( $this->settings['data_store'], array( 'users', 'usermeta' ), true ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! is_null( $data_source ) ) {\n\t\t\t$data_source = $data_source instanceof WP_User ? $data_source : get_user_by( 'ID', $data_source );\n\t\t} elseif ( is_user_logged_in() ) {\n\t\t\t$data_source = wp_get_current_user();\n\t\t}\n\n\t\tif ( $data_source instanceof WP_User ) {\n\t\t\t$this->data_source      = $data_source;\n\t\t\t$this->data_source_type = 'wp_user';\n\t\t}\n\t}\n\n\t/**\n\t * Merge an array of classes into a string or array of classes\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string[]|string $classes  Classes.\n\t * @param string[]        $defaults Default classes.\n\t * @return string[]\n\t */\n\tprotected function classes_ensure_array( $classes, $defaults = array() ) {\n\n\t\tif ( is_string( $classes ) ) {\n\t\t\t$classes = array_map( 'esc_attr', array_map( 'trim', explode( ' ', $classes ) ) );\n\t\t}\n\n\t\t$classes = array_merge( $defaults, $classes );\n\n\t\treturn array_filter( $classes );\n\t}\n\n\t/**\n\t * Returns an array of form field objects from this checkbox or radio field's options array.\n\t *\n\t * @since 6.2.0 Moved from `LLMS_Form_Field::get_field_html()` and added the hidden logic.\n\t *\n\t * @param string $is_hidden If true, returns only the checked fields and sets their type to 'hidden',\n\t *                          else returns all options as `$this->settings['type']` form fields.\n\t * @return LLMS_Form_Field[]\n\t */\n\tpublic function explode_options_to_fields( $is_hidden = false ) {\n\n\t\t$fields = array();\n\t\t$value  = ! empty( $this->settings['value'] ) || is_array( $this->settings['value'] )\n\t\t\t? $this->settings['value']\n\t\t\t: $this->settings['default'];\n\n\t\tforeach ( $this->settings['options'] as $key => $val ) {\n\n\t\t\t$name    = $this->settings['name'];\n\t\t\t$checked = $value === $key;\n\n\t\t\tif ( 'checkbox' === $this->settings['type'] ) {\n\t\t\t\t$name   .= '[]';\n\t\t\t\t$value   = is_array( $value ) ? $value : array( $value );\n\t\t\t\t$checked = in_array( $key, $value, true );\n\t\t\t}\n\n\t\t\tif ( $is_hidden && ! $checked ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$fields[] = new self(\n\t\t\t\tarray(\n\t\t\t\t\t'data_store' => false,\n\t\t\t\t\t'id'         => sprintf( '%1$s--%2$s', $this->settings['id'], $key ),\n\t\t\t\t\t'name'       => $name,\n\t\t\t\t\t'value'      => $key,\n\t\t\t\t\t'label'      => $val,\n\t\t\t\t\t'checked'    => $checked,\n\t\t\t\t\t'type'       => $is_hidden ? 'hidden' : $this->settings['type'],\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Get default field settings.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_defaults() {\n\n\t\treturn array(\n\t\t\t'attributes'        => array(),\n\t\t\t'checked'           => false,\n\t\t\t'columns'           => 12,\n\t\t\t'classes'           => array(), // Or string of space-separated classes.\n\t\t\t'data_store'        => 'usermeta', // Users or usermeta.\n\t\t\t'data_store_key'    => '', // Defaults to value passed for \"name\".\n\t\t\t'description'       => '',\n\t\t\t'default'           => '',\n\t\t\t'disabled'          => false,\n\t\t\t'id'                => '',\n\t\t\t'label'             => '',\n\t\t\t'label_show_empty'  => false,\n\t\t\t'last_column'       => true,\n\t\t\t'match'             => '', // Test.\n\t\t\t'name'              => '', // Defaults to value passed for \"id\".\n\t\t\t'options'           => array(),\n\t\t\t'options_preset'    => '',\n\t\t\t'placeholder'       => '',\n\t\t\t'required'          => false,\n\t\t\t'selected'          => '', // Alias of \"default\".\n\t\t\t'type'              => 'text',\n\t\t\t'value'             => '',\n\t\t\t'visibility_toggle' => false,\n\t\t\t'wrapper_classes'   => array(), // Or string of space-separated classes.\n\t\t);\n\t}\n\n\t/**\n\t * Ensure deprecated settings still function.\n\t *\n\t * The legacy \"min_length\", \"max_length\", and \"style\" settings should now\n\t * be passed via the \"attributes\" setting.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_deprecated_html_attributes() {\n\n\t\t$attrs = array();\n\t\tforeach ( array( 'min_length', 'max_length', 'style' ) as $attr ) {\n\t\t\tif ( isset( $this->settings[ $attr ] ) ) {\n\t\t\t\t$attrs[ str_replace( '_', '', $attr ) ] = esc_attr( $this->settings[ $attr ] );\n\t\t\t}\n\t\t}\n\n\t\treturn $attrs;\n\t}\n\n\t/**\n\t * Retrieve HTML for the fields description\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_description_html() {\n\n\t\treturn $this->settings['description'] ? sprintf( '<span class=\"llms-description\">%s</span>', $this->settings['description'] ) : '';\n\t}\n\n\t/**\n\t * Retrieve HTML for the visibility toggle button\n\t *\n\t * @since TBD\n\t *\n\t * @return string\n\t */\n\tprotected function get_visibility_toggle_html() {\n\n\t\treturn $this->settings['visibility_toggle'] ? '<div class=\"llms-visibility-toggle\"><button type=\"button\" class=\"llms-button-plain hide-if-no-js\" data-toggle=\"1\"><i class=\"fa fa-eye\"></i> <span class=\"llms-visibility-toggle-state\">' . esc_html__( 'Show Password', 'lifterlms' ) . '</span></button></div>' : '';\n\t}\n\n\t/**\n\t * Retrieve the full HTML for the field.\n\t *\n\t * @since 5.0.0\n\t * @since 6.2.0 Moved exploding of checkbox and radio options to `explode_options_to_fields()`.\n\t *\n\t * @return string\n\t */\n\tprotected function get_field_html() {\n\n\t\t/**\n\t\t * Allow 3rd parties to create custom field types or their own field HTML methods.\n\t\t *\n\t\t * Returning a non-empty string will override default HTML generation and use the returned HTML instead.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string          $html     Override html.\n\t\t * @param array           $settings Array of field settings initially passed to the class constructor.\n\t\t * @param LLMS_Form_Field $field    Form field object.\n\t\t */\n\t\t$override = apply_filters( 'llms_form_field_get_' . $this->settings['type'] . '_html', '', $this->settings, $this );\n\t\tif ( ! empty( $override ) ) {\n\t\t\treturn $override;\n\t\t}\n\n\t\t$extra_attrs  = array();\n\t\t$inner_html   = '';\n\t\t$self_closing = false;\n\n\t\tswitch ( $this->settings['type'] ) {\n\n\t\t\tcase 'button':\n\t\t\tcase 'reset':\n\t\t\tcase 'submit':\n\t\t\t\t$tag                 = 'button';\n\t\t\t\t$classes             = array( 'llms-field-button' );\n\t\t\t\t$inner_html          = $this->settings['value'];\n\t\t\t\t$extra_attrs['type'] = $this->settings['type'];\n\t\t\t\tbreak;\n\n\t\t\tcase 'checkbox':\n\t\t\tcase 'radio':\n\t\t\t\t$is_group     = ! empty( $this->settings['options'] );\n\t\t\t\t$tag          = $is_group ? 'div' : 'input';\n\t\t\t\t$self_closing = ! $is_group;\n\t\t\t\t$classes      = array( sprintf( 'llms-field-%s', $this->settings['type'] ) );\n\n\t\t\t\tif ( ! $is_group ) {\n\n\t\t\t\t\t$extra_attrs['type'] = $this->settings['type'];\n\t\t\t\t\tif ( true === $this->settings['checked'] ) {\n\t\t\t\t\t\t$extra_attrs['checked'] = 'checked';\n\t\t\t\t\t}\n\t\t\t\t} else {\n\n\t\t\t\t\t$classes[] = 'llms-input-group';\n\t\t\t\t\t$fields    = $this->explode_options_to_fields( false );\n\t\t\t\t\tforeach ( $fields as $field ) {\n\t\t\t\t\t\t$inner_html .= $field->get_html();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase 'html':\n\t\t\t\t$tag        = 'div';\n\t\t\t\t$classes    = array( 'llms-field-html' );\n\t\t\t\t$inner_html = $this->settings['value'];\n\t\t\t\tbreak;\n\n\t\t\tcase 'select':\n\t\t\t\t$tag        = 'select';\n\t\t\t\t$classes    = array( 'llms-field-select' );\n\t\t\t\t$inner_html = $this->get_options_html();\n\t\t\t\tbreak;\n\n\t\t\tcase 'textarea':\n\t\t\t\t$tag        = 'textarea';\n\t\t\t\t$classes    = array( 'llms-field-textarea' );\n\t\t\t\t$inner_html = $this->settings['value'];\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$tag                 = 'input';\n\t\t\t\t$self_closing        = true;\n\t\t\t\t$classes             = array( 'llms-field-input' );\n\t\t\t\t$extra_attrs['type'] = $this->settings['type'];\n\n\t\t}\n\n\t\t$extra_attrs['class'] = implode( ' ', $this->classes_ensure_array( $this->settings['classes'], $classes ) );\n\n\t\t$attributes = array_merge( $this->get_html_attributes( $this->settings ), $extra_attrs );\n\t\tksort( $attributes );\n\n\t\t$attrs = '';\n\t\tforeach ( $attributes as $attr => $val ) {\n\t\t\t$attrs .= sprintf( ' %1$s=\"%2$s\"', $attr, $val );\n\t\t}\n\n\t\t$open  = $self_closing ? sprintf( '<%1$s%2$s', $tag, $attrs ) : sprintf( '<%1$s%2$s>', $tag, $attrs );\n\t\t$close = $self_closing ? ' />' : sprintf( '</%s>', $tag );\n\n\t\treturn sprintf( '%1$s%2$s%3$s', $open, $inner_html, $close );\n\t}\n\n\t/**\n\t * Retrieve an array of HTML attributes which should be added to the main field element.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_html_attributes() {\n\n\t\t$check = array(\n\t\t\t'id',\n\t\t\t'disabled',\n\t\t\t'name',\n\t\t\t'placeholder',\n\t\t\t'required',\n\t\t\t'value',\n\t\t);\n\n\t\t// Input groups and html only have an id.\n\t\tif ( $this->is_input_group() || 'html' === $this->settings['type'] ) {\n\t\t\t$check = array( 'id' );\n\t\t}\n\n\t\t$attrs = array();\n\n\t\t// Settings attributes.\n\t\tforeach ( $check as $attr ) {\n\t\t\tif ( ! empty( $this->settings[ $attr ] ) ) {\n\t\t\t\t$attrs[ $attr ] = esc_attr( wp_strip_all_tags( $this->settings[ $attr ] ) );\n\t\t\t}\n\t\t}\n\n\t\t// Any custom attributes.\n\t\tforeach ( $this->settings['attributes'] as $attr => $val ) {\n\t\t\t$attrs[ $attr ] = esc_attr( wp_strip_all_tags( $val ) );\n\t\t}\n\n\t\tif ( $this->settings['match'] ) {\n\t\t\t$attrs['data-match'] = $this->settings['match'];\n\t\t}\n\n\t\treturn array_merge( $attrs, $this->get_deprecated_html_attributes() );\n\t}\n\n\t/**\n\t * Retrieve the field's HTML.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_html() {\n\n\t\t/**\n\t\t * Short-circuit field HTML generation.\n\t\t *\n\t\t * Allows a 3rd party to replace the HTML generation method with entirely custom HTML\n\t\t * by returning a non-null value.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string          $pre       The pre-rendered HTML content. Default `null`.\n\t\t * @param array           $settings  The prepared field settings array.\n\t\t * @param LLMS_Form_Field $field_obj Form field instance.\n\t\t */\n\t\t$pre = apply_filters( 'llms_form_field_pre_render', null, $this->settings, $this );\n\t\tif ( ! is_null( $pre ) ) {\n\t\t\treturn $pre;\n\t\t}\n\n\t\t$before = '';\n\t\t$after  = '';\n\n\t\tif ( 'hidden' !== $this->settings['type'] ) {\n\n\t\t\t$before .= sprintf( '<div class=\"%s\">', implode( ' ', $this->settings['wrapper_classes'] ) );\n\n\t\t\t$label_pos   = $this->get_label_position();\n\t\t\t$$label_pos .= $this->get_label_html();\n\n\t\t\t$desc = $this->get_description_html();\n\n\t\t\tif ( $this->is_input_group() ) {\n\t\t\t\t$before .= $desc;\n\t\t\t} else {\n\t\t\t\t$after .= $this->get_description_html();\n\t\t\t}\n\n\t\t\t$after .= $this->get_visibility_toggle_html();\n\n\t\t\t$after .= '</div>';\n\n\t\t\tif ( $this->settings['last_column'] ) {\n\t\t\t\t$after .= '<div class=\"clear\"></div>';\n\t\t\t}\n\t\t}\n\n\t\t$this->html = $before . $this->get_field_html() . $after;\n\n\t\treturn apply_filters( 'llms_form_field', $this->html, $this->settings );\n\t}\n\n\t/**\n\t * Retrieve the HTML for the fields label.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label_html() {\n\n\t\tif ( empty( $this->settings['label'] ) && ! $this->settings['label_show_empty'] ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$required = '';\n\t\tif ( $this->settings['required'] ) {\n\n\t\t\t/**\n\t\t\t * Customize the character used to denote a required field\n\t\t\t *\n\t\t\t * @since Unknown.\n\t\t\t *\n\t\t\t * @param string $character The character used to denote a required field. Defaults to \"*\" (an asterisk).\n\t\t\t * @param array  $settings  Associative array of field settings.\n\t\t\t */\n\t\t\t$char     = apply_filters( 'lifterlms_form_field_required_character', '*', $this->settings );\n\t\t\t$required = sprintf( '<span class=\"llms-required\">%s</span>', $char );\n\n\t\t}\n\n\t\treturn sprintf( '<label for=\"%1$s\">%2$s%3$s</label>', esc_attr( $this->settings['id'] ), $this->settings['label'], $required );\n\t}\n\n\t/**\n\t * Determines if the label element should be rendered before the field or after it.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_label_position() {\n\n\t\t$pos = 'before';\n\n\t\tif ( in_array( $this->settings['type'], array( 'checkbox', 'radio' ), true ) && empty( $this->settings['options'] ) ) {\n\t\t\t$pos = 'after';\n\t\t}\n\n\t\treturn $pos;\n\t}\n\n\t/**\n\t * Retrieve the HTML for an options list in a select field.\n\t *\n\t * This function works recursively to build optgroups.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $options      Prepared options array.\n\t * @param mixed $selected_val The value of the option that should be marked as \"selected\".\n\t * @return string\n\t */\n\tprotected function get_option_list_html( $options, $selected_val ) {\n\n\t\t$html = '';\n\t\tforeach ( $options as $key => $val ) {\n\n\t\t\tif ( is_array( $val ) ) {\n\n\t\t\t\t$label         = isset( $val['label'] ) ? $val['label'] : $key;\n\t\t\t\t$group_options = isset( $val['options'] ) ? $val['options'] : $val;\n\t\t\t\t$html         .= sprintf( '<optgroup label=\"%1$s\" data-key=\"%2$s\">%3$s</optgroup>', esc_attr( $label ), esc_attr( $key ), $this->get_option_list_html( $group_options, $selected_val ) );\n\n\t\t\t} else {\n\n\t\t\t\t$selected = ( (string) $key === (string) $selected_val ) ? ' selected=\"selected\"' : '';\n\t\t\t\t$disabled = ( $this->settings['placeholder'] && '' === $key ) ? ' disabled=\"disabled\"' : '';\n\t\t\t\t$html    .= sprintf( '<option value=\"%1$s\"%3$s%4$s>%2$s</option>', esc_attr( $key ), esc_attr( $val ), $selected, $disabled );\n\n\t\t\t}\n\t\t}\n\n\t\treturn $html;\n\t}\n\n\t/**\n\t * Retrieve the html for all options in a select field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_options_html() {\n\n\t\t$html = '';\n\n\t\tif ( ! $this->settings['options'] ) {\n\t\t\treturn $html;\n\t\t}\n\n\t\t$selected_val = ! empty( $this->settings['value'] ) ? $this->settings['value'] : $this->settings['default'];\n\t\t$html        .= $this->get_option_list_html( $this->settings['options'], $selected_val );\n\n\t\treturn $html;\n\t}\n\n\t/**\n\t * Retrieve the field settings array.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_settings() {\n\t\treturn $this->settings;\n\t}\n\n\t/**\n\t * Determines if the field is a group of checkboxes or radios.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return bool\n\t */\n\tprotected function is_input_group() {\n\n\t\treturn in_array( $this->settings['type'], array( 'checkbox', 'radio' ), true ) && ! empty( $this->settings['options'] );\n\t}\n\n\t/**\n\t * Prepares the field for rendering by configuring all of it's settings.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function prepare() {\n\n\t\tif ( empty( $this->settings['id'] ) ) {\n\t\t\t$this->settings['id'] = uniqid( 'llms-field-' );\n\t\t}\n\n\t\t$this->prepare_wrapper_classes();\n\n\t\t$this->settings['classes'] = $this->classes_ensure_array( $this->settings['classes'] );\n\n\t\t// Allow setting `disabled` to `true` to disable the field.\n\t\tif ( true === $this->settings['disabled'] ) {\n\t\t\t$this->settings['disabled'] = 'disabled';\n\t\t}\n\n\t\t// Allow setting `required` to `true` to make the field required the field.\n\t\tif ( true === $this->settings['required'] ) {\n\t\t\t$this->settings['required'] = 'required';\n\t\t}\n\n\t\t// When name is `false` we don't want to output a name on the field.\n\t\tif ( false !== $this->settings['name'] ) {\n\t\t\t// Use the field id as the name if name isn't specified.\n\t\t\t$this->settings['name'] = empty( $this->settings['name'] ) ? $this->settings['id'] : $this->settings['name'];\n\t\t}\n\n\t\t// When `data_store_key` is false we won't automatically store or populate the field.\n\t\tif ( false !== $this->settings['data_store_key'] && empty( $this->settings['data_store_key'] ) ) {\n\t\t\t$this->prepare_storage();\n\t\t}\n\n\t\t// Add preset options.\n\t\tif ( $this->settings['options_preset'] ) {\n\t\t\t$this->prepare_options_from_preset();\n\t\t} elseif ( ! empty( $this->settings['options'] ) ) {\n\t\t\t$this->settings['options'] = $this->prepare_options( $this->settings['options'] );\n\t\t}\n\n\t\t$this->prepare_value();\n\n\t\tif ( 'llms-password-strength-meter' === $this->settings['id'] ) {\n\t\t\t$this->prepare_password_strength_meter();\n\t\t} elseif ( 'llms_voucher' === $this->settings['id'] ) {\n\t\t\t$this->prepare_voucher();\n\t\t}\n\t}\n\n\t/**\n\t * Prepare the fields options.\n\t *\n\t * Allows options to be setup as an associative array of key/value pairs or\n\t * an array of associative arrays each with a \"label\" and \"key\" property.\n\t * The \"key\" property may be omitted, in which case the \"label\" will be\n\t * duplicated as the option's \"value\".\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $raw Raw field data.\n\t * @return array\n\t */\n\tprotected function prepare_options( $raw ) {\n\n\t\t$prepared = array();\n\n\t\tforeach ( $raw as $key => $val ) {\n\n\t\t\tif ( is_array( $val ) ) {\n\n\t\t\t\t// Option group.\n\t\t\t\tif ( isset( $val['options'] ) ) {\n\n\t\t\t\t\t$prepared[ $key ] = array(\n\t\t\t\t\t\t'label'   => isset( $val['label'] ) ? $val['label'] : $key,\n\t\t\t\t\t\t'options' => $this->prepare_options( $val['options'] ),\n\t\t\t\t\t);\n\n\t\t\t\t\t// From block editor options array.\n\t\t\t\t} elseif ( isset( $val['text'] ) ) {\n\n\t\t\t\t\t$item_key              = isset( $val['key'] ) ? $val['key'] : $val['text'];\n\t\t\t\t\t$prepared[ $item_key ] = $val['text'];\n\n\t\t\t\t\tif ( isset( $val['default'] ) && llms_parse_bool( $val['default'] ) ) {\n\t\t\t\t\t\tif ( 'checkbox' === $this->settings['type'] ) { // Account for multiple defaults.\n\t\t\t\t\t\t\t$this->settings['default']   = is_array( $this->settings['default'] ) ? $this->settings['default'] : array();\n\t\t\t\t\t\t\t$this->settings['default'][] = $item_key;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t$this->settings['default'] = $item_key;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Flat array of $key=>$val.\n\t\t\t} else {\n\n\t\t\t\t$prepared[ $key ] = $val;\n\n\t\t\t}\n\t\t}\n\n\t\t// Add a placeholder.\n\t\tif ( $this->settings['placeholder'] ) {\n\t\t\t$this->settings['default'] = '';\n\t\t\t$prepared                  = array_merge( array( '' => $this->settings['placeholder'] ), $prepared );\n\t\t}\n\n\t\treturn $prepared;\n\t}\n\n\t/**\n\t * Retrieve options list data based on the options_preset settings.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function prepare_options_from_preset() {\n\n\t\t$preset_id = $this->settings['options_preset'];\n\t\tswitch ( $preset_id ) {\n\t\t\tcase 'countries':\n\t\t\t\t$options                             = get_lifterlms_countries();\n\t\t\t\t$default                             = get_lifterlms_country();\n\t\t\t\t$this->settings['wrapper_classes'][] = 'llms-l10n-country-select';\n\t\t\t\tbreak;\n\n\t\t\tcase 'states':\n\t\t\t\t$options                             = llms_get_states();\n\t\t\t\t$this->settings['wrapper_classes'][] = 'llms-l10n-state-select';\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t/**\n\t\t\t\t * Define custom / 3rd party presets\n\t\t\t\t *\n\t\t\t\t * @since 5.0.0\n\t\t\t\t *\n\t\t\t\t * @param array $options              Array of options.\n\t\t\t\t * @param array $settings             Prepared field settings.\n\t\t\t\t * @param LLMS_Form_Field $fomr_field Form field object instance.\n\t\t\t\t */\n\t\t\t\t$options = apply_filters( \"llms_form_field_options_preset_{$preset_id}\", array(), $this->settings, $this );\n\t\t}\n\n\t\tif ( isset( $options ) ) {\n\t\t\t$this->settings['options'] = $options;\n\t\t}\n\n\t\tif ( isset( $default ) && ! $this->settings['default'] ) {\n\t\t\t$this->settings['default'] = $default;\n\t\t}\n\t}\n\n\t/**\n\t * Additional preparation for the password strength meter.\n\t *\n\t * @since 5.0.0\n\t * @since 5.10.0 Make sure to enqueue the strength meter js, whether or not `wp_enqueue_scripts` hook has been fired yet.\n\t *\n\t * @return void\n\t */\n\tprotected function prepare_password_strength_meter() {\n\n\t\t$meter_settings = array(\n\t\t\t'blocklist'    => array(),\n\t\t\t'min_strength' => ! empty( $this->settings['min_strength'] ) ? $this->settings['min_strength'] : 'weak',\n\t\t\t'min_length'   => ! empty( $this->settings['min_length'] ) ? max( 6, $this->settings['min_length'] ) : 6,\n\t\t);\n\n\t\t// Backwards compat functionality ends up outputting a minlength attribute on the <div> and we don't want that.\n\t\tunset( $this->settings['min_length'] );\n\n\t\t/**\n\t\t * Modify password strength meter settings.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array $meter_settings {\n\t\t *     Hash of meter configuration options.\n\t\t *\n\t\t *     @type string[] $blocklist    A list of strings that are penalized when used in the password. See \"user_inputs\" at https://github.com/dropbox/zxcvbn#usage.\n\t\t *     @type string   $min_strength The minimum acceptable password strength. Accepts \"strong\", \"medium\", or \"weak\". Default: \"weak\".\n\t\t *     @type int      $min_length   The minimum acceptable password length. Must be >= 6. Default: 6.\n\t\t * }\n\t\t */\n\t\t$meter_settings = apply_filters( 'llms_password_strength_meter_settings', $meter_settings, $this->settings, $this );\n\n\t\t// If scripts have been enqueued, add password strength meter script.\n\t\tif ( did_action( 'wp_enqueue_scripts' ) ) {\n\t\t\treturn $this->enqueue_strength_meter( $meter_settings );\n\t\t}\n\t\t// Otherwise add it whe `wp_enqueue_scripts` is fired.\n\t\tadd_action(\n\t\t\t'wp_enqueue_scripts',\n\t\t\tfunction () use ( $meter_settings ) {\n\t\t\t\t$this->enqueue_strength_meter( $meter_settings );\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * Enqueue password strength meter script.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @param array $meter_settings {\n\t *     Hash of meter configuration options.\n\t *\n\t *     @type string[] $blocklist    A list of strings that are penalized when used in the password. See \"user_inputs\" at https://github.com/dropbox/zxcvbn#usage.\n\t *     @type string   $min_strength The minimum acceptable password strength. Accepts \"strong\", \"medium\", or \"weak\". Default: \"weak\".\n\t *     @type int      $min_length   The minimum acceptable password length. Must be >= 6. Default: 6.\n\t * }\n\t * @return void\n\t */\n\tprivate function enqueue_strength_meter( $meter_settings ) {\n\n\t\twp_enqueue_script( 'password-strength-meter' );\n\t\t// Localize the script with meter data.\n\t\tllms()->assets->enqueue_inline(\n\t\t\t'llms-pw-strength-settings',\n\t\t\t'window.LLMS.PasswordStrength = window.LLMS.PasswordStrength || {};window.LLMS.PasswordStrength.get_settings = function() { return JSON.parse( \\'' . wp_json_encode( $meter_settings ) . '\\' ); };',\n\t\t\t'footer',\n\t\t\t15\n\t\t);\n\t}\n\n\t/**\n\t * Setup default storage information.\n\t *\n\t * Ensures fields stored on the wp_users table have the proper default `data_store`.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function prepare_storage() {\n\n\t\t$name = $this->settings['name'];\n\n\t\t// Field Name => Storage Key.\n\t\t$users_fields = array(\n\n\t\t\t// We prefer these aliases for legacy reasons.\n\t\t\t'email_address' => 'user_email',\n\t\t\t'password'      => 'user_pass',\n\n\t\t\t// Default wp_users column names.\n\t\t\t'user_login'    => 'user_login',\n\t\t\t'user_pass'     => 'user_pass',\n\t\t\t'user_nicename' => 'user_nicename',\n\t\t\t'user_email'    => 'user_email',\n\t\t\t'user_url'      => 'user_url',\n\t\t\t'display_name'  => 'display_name',\n\n\t\t);\n\n\t\t// Set data storage for items on the wp_users table.\n\t\tif ( in_array( $name, array_keys( $users_fields ), true ) ) {\n\t\t\t$this->settings['data_store'] = 'users';\n\t\t\t$name                         = $users_fields[ $name ];\n\n\t\t\t// Don't save default core confirmation fields.\n\t\t} elseif ( in_array( $name, LLMS_CONFIRMATION_FIELDS, true ) ) {\n\t\t\t$this->settings['data_store'] = false;\n\t\t}\n\n\t\t$this->settings['data_store_key'] = $name;\n\t}\n\n\t/**\n\t * Prepare the field's value.\n\t *\n\t * @since 5.0.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tprotected function prepare_value() {\n\n\t\t// Never autoload passwords and or fields with an explicit value (except radio and checkbox).\n\t\tif ( 'password' === $this->settings['type'] || ! empty( $this->settings['value'] && ! in_array( $this->settings['type'], array( 'checkbox', 'radio' ), true ) ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$user_val = null;\n\n\t\t// Attempt to populate field data from the most recent $_POST action.\n\t\tif ( 'POST' === strtoupper( getenv( 'REQUEST_METHOD' ) ) ) {\n\t\t\t$posted = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce is verified prior to reaching this method.\n\t\t\tif ( isset( $posted[ $this->settings['name'] ] ) ) {\n\t\t\t\t$filter_options = is_array( $posted[ $this->settings['name'] ] ) ? array( FILTER_REQUIRE_ARRAY ) : array();\n\t\t\t\t$user_val       = llms_filter_input_sanitize_string( INPUT_POST, $this->settings['name'], $filter_options );\n\t\t\t}\n\t\t}\n\n\t\t// Auto-populate field from the datastore if we have a user and datastore information.\n\t\tif ( is_null( $user_val ) && ( isset( $this->data_source ) && 'wp_user' === $this->data_source_type ) && $this->settings['data_store_key'] ) {\n\t\t\t$user_val = $this->data_source->get( $this->settings['data_store_key'] );\n\t\t}\n\n\t\t// Set the value to the user's submitted or stored value.\n\t\tif ( ! is_null( $user_val ) ) {\n\t\t\tif ( in_array( $this->settings['type'], array( 'checkbox', 'radio' ), true ) && ! $this->is_input_group() ) {\n\t\t\t\t$this->settings['checked'] = ( $this->settings['value'] === $user_val );\n\t\t\t} else {\n\t\t\t\t$this->settings['value'] = $user_val;\n\t\t\t}\n\t\t}\n\n\t\t// Handle \"default\" alias \"selected\".\n\t\tif ( isset( $this->settings['selected'] ) && '' !== $this->settings['selected'] ) {\n\t\t\t$this->settings['default'] = $this->settings['selected'];\n\t\t}\n\n\t\t// Add default value if there's no explicit value and a default value is set.\n\t\tif ( ! $this->settings['value'] && ! is_array( $this->settings['value'] ) && '' !== $this->settings['default'] ) {\n\t\t\t$this->settings['value'] = $this->settings['default'];\n\t\t}\n\t}\n\n\t/**\n\t * Additional preparation for the special voucher field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function prepare_voucher() {\n\n\t\tif ( ! $this->settings['required'] && $this->settings['toggleable'] ) {\n\n\t\t\t$this->settings['label'] = sprintf( '<a class=\"llms-voucher-toggle\" id=\"llms-voucher-toggle\" href=\"#\">%s</a>', $this->settings['label'] );\n\n\t\t\t$this->settings['attributes']['style'] = 'display:none;';\n\n\t\t\t$this->settings['data_store_key'] = false;\n\n\t\t}\n\t}\n\n\t/**\n\t * Prepare CSS wrapper classes for the field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function prepare_wrapper_classes() {\n\n\t\t$defaults = array();\n\n\t\t// Base field class.\n\t\t$defaults[] = 'llms-form-field';\n\n\t\t// Add class for the field type.\n\t\t$defaults[] = sprintf( 'type-%s', $this->settings['type'] );\n\n\t\tif ( $this->is_input_group() ) {\n\t\t\t$defaults[] = 'is-group';\n\t\t}\n\n\t\t// Add columns classes.\n\t\t$defaults[] = sprintf( 'llms-cols-%d', $this->settings['columns'] );\n\t\tif ( $this->settings['last_column'] ) {\n\t\t\t$defaults[] = 'llms-cols-last';\n\t\t}\n\n\t\t// If required, add a class.\n\t\tif ( $this->settings['required'] ) {\n\t\t\t$defaults[] = 'llms-is-required';\n\t\t}\n\n\t\t$this->settings['wrapper_classes'] = $this->classes_ensure_array(\n\t\t\t$this->settings['wrapper_classes'],\n\t\t\t$defaults\n\t\t);\n\t}\n\n\t/**\n\t * Render/output the field's html.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function render() {\n\n\t\tif ( ! $this->html ) {\n\t\t\t$this->get_html();\n\t\t}\n\n\t\techo wp_kses( $this->html, LLMS_ALLOWED_HTML_FORM_FIELDS );\n\t}\n}\n"
  },
  {
    "path": "includes/forms/class-llms-form-handler.php",
    "content": "<?php\n/**\n * Handle LifterLMS Form submissions.\n *\n * @package  LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Form_Handler class.\n *\n * @since 5.0.0\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n */\nclass LLMS_Form_Handler {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Validation class instance.\n\t *\n\t * @var LLMS_Form_Validator\n\t */\n\tprotected $validator = null;\n\n\t/**\n\t * Private Constructor.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t$this->validator = new LLMS_Form_Validator();\n\n\t\tadd_action( 'lifterlms_before_user_update', array( $this, 'maybe_modify_edit_account_field_settings' ), 10, 3 );\n\t\tadd_action( 'lifterlms_before_user_update', array( $this, 'maybe_modify_required_address_fields' ), 10, 3 );\n\t\tadd_action( 'lifterlms_before_user_registration', array( $this, 'maybe_modify_required_address_fields' ), 10, 3 );\n\t}\n\n\t/**\n\t * Retrieve fields for a given form.\n\t *\n\t * Ensures the form exists and that the current user can access the form.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $action   User action to be performed. Either \"update\" (for an existing user) or \"registration\" for a new user.\n\t * @param string $location Form location ID.\n\t * @param array  $args     Additional arguments passed to the short-circuit filter.\n\t * @return WP_Error|array[] Array of LLMS_Form_Field arrays on success or an error object on failure.\n\t */\n\tprotected function get_fields( $action, $location, $args = array() ) {\n\n\t\t$fields = LLMS_Forms::instance()->get_form_fields( $location, $args );\n\n\t\t// Form couldn't be located.\n\t\tif ( false === $fields ) {\n\t\t\t// Translators: %s = form location ID.\n\t\t\treturn new WP_Error( 'llms-form-invalid-location', sprintf( __( 'The form location \"%s\" is invalid.', 'lifterlms' ), $location ), $args );\n\n\t\t} elseif ( 'account' === $location && 'update' !== $action ) {\n\t\t\t// No logged in user, can't update.\n\t\t\treturn new WP_Error( 'llms-form-no-user', __( 'You must be logged in to perform this action.', 'lifterlms' ), $args );\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Insert user data into the database.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string  $action      Type of insert action. Either \"registration\" for a new user or \"update\" for an existing one.\n\t * @param array   $posted_data User-submitted form data.\n\t * @param array[] $fields      List of LifterLMS Form fields for the form.\n\t * @return WP_Error|int Error on failure or WP_User ID on success.\n\t */\n\tprotected function insert( $action, $posted_data, $fields ) {\n\n\t\t$func     = 'registration' === $action ? 'wp_insert_user' : 'wp_update_user';\n\t\t$prepared = $this->prepare_data_for_insert( $posted_data, $fields, $action );\n\n\t\t$user_id = $func( $prepared['users'] );\n\t\tif ( is_wp_error( $user_id ) ) {\n\t\t\treturn $user_id;\n\t\t}\n\n\t\tforeach ( $prepared['usermeta'] as $key => $val ) {\n\t\t\t// Double check that fields like password_confirm aren't saved to user meta.\n\t\t\tif ( in_array( $key, LLMS_CONFIRMATION_FIELDS, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tupdate_user_meta( $user_id, $key, $val );\n\t\t}\n\n\t\treturn $user_id;\n\t}\n\n\t/**\n\t * Modify LifterLMS Fields prior to performing submit handler validations.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Do not allow submitting a password change without providing a `password_current`\n\t *\n\t * @param array   $posted_data User submitted form data (passed by reference).\n\t * @param string  $location    Form location ID.\n\t * @param array[] $fields      Array of LifterLMS Form Fields (passed by reference).\n\t * @return void\n\t */\n\tpublic function maybe_modify_edit_account_field_settings( &$posted_data, $location, &$fields ) {\n\n\t\tif ( 'account' !== $location ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * If email address and passwords aren't submitted we can mark them as \"optional\" fields.\n\t\t *\n\t\t * These fields are dynamically toggled and disabled if they're not modified.\n\t\t * Process `password_current` as last as it depends on `password` field submission.\n\t\t */\n\t\tforeach ( array( 'email_address', 'password', 'password_current' ) as $field_id ) {\n\n\t\t\t// If the field exists and it's not included (or empty) in the posted data.\n\t\t\t$index = LLMS_Forms::instance()->get_field_by( $fields, 'id', $field_id, 'index' );\n\t\t\tif ( false !== $index && empty( $posted_data[ $fields[ $index ]['name'] ] ) ) {\n\n\t\t\t\t// When updating a password, the `password_current` is mandatory.\n\t\t\t\tif ( 'account' === $location && 'password_current' === $field_id ) {\n\t\t\t\t\t// Get `password` field.\n\t\t\t\t\t$password_index = LLMS_Forms::instance()->get_field_by( $fields, 'id', 'password', 'index' );\n\t\t\t\t\t// If a `passowrd` feld has been submitted then the `password_current` cannot be skipped.\n\t\t\t\t\tif ( false !== $password_index &&\n\t\t\t\t\t\t\t! empty( $posted_data[ $fields[ $password_index ]['name'] ] ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Remove the field so we don't accidentally save an empty value later.\n\t\t\t\tunset( $posted_data[ $fields[ $index ]['name'] ] );\n\n\t\t\t\t// Mark the field as optional (for validation purposes).\n\t\t\t\t$fields[ $index ]['required'] = false;\n\n\t\t\t\t// Check if there's a confirm field and do the same.\n\t\t\t\t$con_index = LLMS_Forms::instance()->get_field_by( $fields, 'id', \"{$field_id}_confirm\", 'index' );\n\t\t\t\tif ( false !== $con_index && empty( $posted_data[ $fields[ $con_index ]['name'] ] ) ) {\n\t\t\t\t\tunset( $posted_data[ $fields[ $con_index ]['name'] ] );\n\t\t\t\t\t$fields[ $con_index ]['required'] = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Modify LifterLMS Fields to allow some address fields to be conditionally required.\n\t *\n\t * Uses available country locale information to remove the \"required\" attribute for state\n\t * and zip code fields when a user has chosen a country that doesn't use states and/or\n\t * zip codes.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $posted_data User submitted form data (passed by reference).\n\t * @param string  $location    Form location ID.\n\t * @param array[] $fields      Array of LifterLMS Form Fields (passed by reference).\n\t * @return void\n\t */\n\tpublic function maybe_modify_required_address_fields( &$posted_data, $location, &$fields ) {\n\n\t\t// Only proceed if we have a country to review.\n\t\tif ( empty( $posted_data['llms_billing_country'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$country = $posted_data['llms_billing_country'];\n\t\t$info    = llms_get_country_address_info( $country );\n\n\t\t// Fields to chek.\n\t\t$check = array(\n\t\t\t'llms_billing_city'  => 'city',\n\t\t\t'llms_billing_state' => 'state',\n\t\t\t'llms_billing_zip'   => 'postcode',\n\t\t);\n\n\t\tforeach ( $check as $post_key => $info_key ) {\n\n\t\t\t$index = LLMS_Forms::instance()->get_field_by( $fields, 'name', $post_key, 'index' );\n\n\t\t\t// Field exists, no data was posted, and the field is disabled (is `false`) in the address info array.\n\t\t\tif ( false !== $index && empty( $posted_data[ $post_key ] ) && ! $info[ $info_key ] ) {\n\t\t\t\t$fields[ $index ]['required'] = false;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Prepares user-submitted data for insertion into the database.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $posted_data Sanitized & validated user-submitted form data.\n\t * @param array[] $fields      LifterLMS form fields list.\n\t * @param string  $action      Insert action, either \"registration\" for new users or \"update\" for existing, users.\n\t * @return array\n\t */\n\tprotected function prepare_data_for_insert( $posted_data, $fields, $action ) {\n\n\t\t$prepared = array();\n\n\t\tforeach ( $fields as $field ) {\n\n\t\t\tif ( empty( $field['data_store_key'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// We need to account for fields that are part of the form but are not present in the `$posted_data`\n\t\t\t// e.g. unchecked check boxes.\n\t\t\tif ( isset( $posted_data[ $field['name'] ] ) || 'checkbox' === $field['type'] ) {\n\n\t\t\t\tif ( ! isset( $prepared[ $field['data_store'] ] ) ) {\n\t\t\t\t\t$prepared[ $field['data_store'] ] = array();\n\t\t\t\t}\n\n\t\t\t\t$prepared[ $field['data_store'] ][ $field['data_store_key'] ] = isset( $posted_data[ $field['name'] ] ) ? $posted_data[ $field['name'] ] : array();\n\t\t\t}\n\t\t}\n\n\t\tif ( 'registration' === $action ) {\n\n\t\t\t$defaults = array(\n\t\t\t\t'role'                 => 'student',\n\t\t\t\t'show_admin_bar_front' => false,\n\t\t\t);\n\n\t\t\t// Add a username if we don't have a user_login field.\n\t\t\tif ( empty( $prepared['users']['user_login'] ) ) {\n\t\t\t\t$defaults['user_login'] = LLMS_Person_Handler::generate_username( $posted_data['email_address'] );\n\t\t\t}\n\n\t\t\t// Add a password if we don't have a password field.\n\t\t\tif ( empty( $prepared['users']['user_pass'] ) ) {\n\t\t\t\t$defaults['user_pass'] = wp_generate_password( 32, true, true );\n\t\t\t}\n\n\t\t\t$prepared['users'] = wp_parse_args( $prepared['users'], $defaults );\n\n\t\t} elseif ( 'update' === $action ) {\n\n\t\t\t$prepared['users']['ID'] = empty( $posted_data['user_id'] ) ? get_current_user_id() : absint( $posted_data['user_id'] );\n\n\t\t}\n\n\t\t// Record an IP Address.\n\t\t$prepared['usermeta']['llms_ip_address'] = llms_get_ip_address();\n\n\t\t// If terms have been agreed to, record a time stamp for the agreement.\n\t\tif ( isset( $posted_data['llms_agress_to_terms'] ) ) {\n\t\t\t$prepared['usermeta']['llms_agress_to_terms'] = current_time( 'mysql' );\n\t\t}\n\n\t\t/**\n\t\t * Filter data added to the wp_users data via `wp_insert_user()` or `wp_update_user()`.\n\t\t *\n\t\t * The dynamic portion of this hook, `$action`, can be either \"registration\" or \"update\".\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.0.0 Moved from `LLMS_Person_Handler::insert_data()`.\n\t\t *\n\t\t * @param array  $user_data   Array of user data.\n\t\t * @param array  $posted_data Array of user-submitted data.\n\t\t * @param string $action      Submission action, either \"registration\" or \"update\".\n\t\t */\n\t\t$prepared['users'] = apply_filters( \"lifterlms_user_{$action}_insert_user\", $prepared['users'], $posted_data, $action );\n\n\t\t/**\n\t\t * Filter meta data to be added for the user.\n\t\t *\n\t\t * The dynamic portion of this hook, `$action`, can be either \"registration\" or \"update\".\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.0.0 Moved from `LLMS_Person_Handler::insert_data()`.\n\t\t *\n\t\t * @param array  $user_meta   Array of user meta data.\n\t\t * @param array  $posted_data Array of user-submitted data.\n\t\t * @param string $action      Submission action, either \"registration\" or \"update\".\n\t\t */\n\t\t$prepared['usermeta'] = apply_filters( \"lifterlms_user_{$action}_insert_user_meta\", $prepared['usermeta'], $posted_data, $action );\n\n\t\treturn $prepared;\n\t}\n\n\t/**\n\t * Form submission handler.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Remove invisible fields from when loading the checkout form.\n\t * @since 7.0.0 Allow submission validation only (without actually submitting the fields) using the\n\t *              `validate_only` flag in the `$args` array.\n\t *\n\t * @param array  $posted_data User-submitted form data.\n\t * @param string $location    Form location ID.\n\t * @param array  $args        Additional arguments passed to the short-circuit filter.\n\t * @return integer|boolean|WP_Error On success returns the `WP_User` ID.\n\t *                                  If the `validate_only` argument is passed returns `true` on success.\n\t *                                  Returns an error object if any validation or processing errors are encountered.\n\t */\n\tpublic function submit( $posted_data, $location, $args = array() ) {\n\n\t\t// Determine the user action to perform.\n\t\t$action = get_current_user_id() ? 'update' : 'registration';\n\n\t\t// Load the form, filtering out invisible fields, only for checkout form.\n\t\tif ( 'checkout' === $location ) {\n\t\t\tadd_filter( 'llms_forms_remove_invisible_field', '__return_true', 999 );\n\t\t}\n\t\t$fields = $this->get_fields( $action, $location, $args );\n\t\tif ( 'checkout' === $location ) {\n\t\t\tremove_filter( 'llms_forms_remove_invisible_field', '__return_true', 999 );\n\t\t}\n\n\t\tif ( is_wp_error( $fields ) ) {\n\t\t\treturn $this->submit_error( $fields, $posted_data, $action );\n\t\t}\n\n\t\t// Make sure the user id cannot be forced by user submission.\n\t\tunset( $posted_data['user_id'] );\n\n\t\tif ( ! empty( $args['validate_only'] ) ) {\n\t\t\treturn $this->validate_fields( $posted_data, $location, $fields, $action );\n\t\t}\n\n\t\treturn $this->submit_fields( $posted_data, $location, $fields, $action );\n\t}\n\n\t/**\n\t * Form fields submission.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Added \"lifterlms_user_{$action}_required_data\" filter, to filter the required fields validity of the form submission.\n\t * @since 5.4.1 Sanitize filed only after validation. See https://github.com/gocodebox/lifterlms/issues/1829.\n\t * @since 6.0.0 Notify developers of the deprecated `lifterlms_created_person` action hook.\n\t * @since 7.0.0 Moved validation logic to the `validate_fields()` method.\n\t *\n\t * @param array   $posted_data User-submitted form data.\n\t * @param string  $location    Form location ID.\n\t * @param array[] $fields      Array of LifterLMS Form Fields.\n\t * @param string  $action      User action to perform.\n\t * @return int|WP_Error WP_User ID on success, error object on failure.\n\t */\n\tpublic function submit_fields( $posted_data, $location, $fields, $action ) {\n\n\t\t$validate = $this->validate_fields( $posted_data, $location, $fields, $action );\n\t\tif ( is_wp_error( $validate ) ) {\n\t\t\treturn $validate;\n\t\t}\n\n\t\t// Sanitize.\n\t\t$posted_data = $this->validator->sanitize_fields( $posted_data, $fields );\n\n\t\t$user_id = $this->insert( $action, $posted_data, $fields );\n\t\tif ( is_wp_error( $user_id ) ) {\n\t\t\treturn $this->submit_error( $user_id, $posted_data, $action );\n\t\t}\n\n\t\tif ( 'registration' === $action ) {\n\n\t\t\t/**\n\t\t\t * Deprecated user creation hook.\n\t\t\t *\n\t\t\t * @since Unknown.\n\t\t\t * @deprecated 5.0.0\n\t\t\t *\n\t\t\t * @param int    $user_id     WP_User ID of the newly created user.\n\t\t\t * @param array  $posted_data Array of user-submitted data.\n\t\t\t * @param string $location    Form location.\n\t\t\t */\n\t\t\tdo_action_deprecated(\n\t\t\t\t'lifterlms_created_person',\n\t\t\t\tarray( $user_id, $posted_data, $location ),\n\t\t\t\t'5.0.0',\n\t\t\t\t'lifterlms_user_registered'\n\t\t\t);\n\n\t\t\t/**\n\t\t\t * Fire an action after a user has been registered.\n\t\t\t *\n\t\t\t * @since 3.0.0\n\t\t\t * @since 5.0.0 Moved from `LLMS_Person_Handler::register()`.\n\t\t\t *\n\t\t\t * @param int    $user_id     WP_User ID of the user.\n\t\t\t * @param array  $posted_data Array of user submitted data.\n\t\t\t * @param string $location    Form location.\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_user_registered', $user_id, $posted_data, $location );\n\n\t\t} elseif ( 'update' === $action ) {\n\n\t\t\t/**\n\t\t\t * Fire an action after a user has been updated.\n\t\t\t *\n\t\t\t * @since 3.0.0\n\t\t\t * @since 5.0.0 Moved from `LLMS_Person_Handler::update()`.\n\t\t\t *\n\t\t\t * @param int    $user_id     WP_User ID of the user.\n\t\t\t * @param array  $posted_data Array of user submitted data.\n\t\t\t * @param string $location    Form location.\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_user_updated', $user_id, $posted_data, $location );\n\n\t\t}\n\n\t\treturn $user_id;\n\t}\n\n\t/**\n\t * Ensure all errors objects encountered during form submission are filterable.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param WP_Error $error       Error object.\n\t * @param array    $posted_data User-submitted form data.\n\t * @param string   $action      Form action, either \"registration\" or \"update\".\n\t * @return WP_Error\n\t */\n\tprotected function submit_error( $error, $posted_data, $action ) {\n\n\t\t/**\n\t\t * Filter the error return when the insert/update fails.\n\t\t *\n\t\t * The dynamic portion of this hook, `$action`, can be either \"registration\" or \"update\".\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.0.0 Moved from `LLMS_Person_Handler::insert_data()`.\n\t\t *\n\t\t * @param WP_Error $error       Error object.\n\t\t * @param array    $posted_data Array of user-submitted data.\n\t\t * @param string   $action      Submission action, either \"registration\" or \"update\"!\n\t\t */\n\t\treturn apply_filters( \"lifterlms_user_{$action}_failure\", $error, $posted_data, $action );\n\t}\n\n\t/**\n\t * Form fields submission validation.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array   $posted_data User-submitted form data.\n\t * @param string  $location    Form location ID.\n\t * @param array[] $fields      Array of LifterLMS Form Fields.\n\t * @param string  $action      User action to perform.\n\t * @return boolean|WP_Error Returns `true` on success and an error object on failure.\n\t */\n\tprotected function validate_fields( $posted_data, $location, $fields, $action ) {\n\n\t\t/**\n\t\t * Run an action immediately prior to user registration or update.\n\t\t *\n\t\t * The dynamic portion of this hook, `$action`, can be either \"registration\" or \"update\".\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.0.0 Moved from `LLMS_Person_Handler::update()` & LLMS_Person_Handler::register().\n\t\t *              Added parameters `$fields` and `$args`.\n\t\t *              Triggered by `do_action_ref_array()` instead of `do_action()` allowing modification\n\t\t *              of `$posted_data` and `$fields` via hooks.\n\t\t *\n\t\t * @param array   $posted_data Array of user-submitted data (passed by reference).\n\t\t * @param string  $location    Form location.\n\t\t * @param array[] $fields      Array of LifterLMS Form Fields (passed by reference).\n\t\t */\n\t\tdo_action_ref_array( \"lifterlms_before_user_{$action}\", array( &$posted_data, $location, &$fields ) );\n\n\t\t// Check for all required fields.\n\t\t$required = $this->validator->validate_required_fields( $posted_data, $fields );\n\n\t\t/**\n\t\t * Filter the required fields validity of the form submission.\n\t\t *\n\t\t * The dynamic portion of this hook, `$action`, can be either \"registration\" or \"update\".\n\t\t *\n\t\t * @since 5.0.1\n\t\t *\n\t\t * @param WP_Error|true $valid       Error object containing required validation errors or true when the data is valid.\n\t\t * @param array         $posted_data Array of user-submitted data.\n\t\t * @param string        $location    Form location.\n\t\t */\n\t\t$required = apply_filters( \"lifterlms_user_{$action}_required_data\", $required, $posted_data, $location );\n\n\t\tif ( is_wp_error( $required ) ) {\n\t\t\treturn $this->submit_error( $required, $posted_data, $action );\n\t\t}\n\n\t\t$posted_data = wp_unslash( $posted_data );\n\n\t\t$valid = $this->validator->validate_fields( $posted_data, $fields );\n\t\tif ( is_wp_error( $valid ) ) {\n\t\t\treturn $this->submit_error( $valid, $posted_data, $action );\n\t\t}\n\n\t\t// Validate matching fields.\n\t\t$matches = $this->validator->validate_matching_fields( $posted_data, $fields );\n\t\tif ( is_wp_error( $matches ) ) {\n\t\t\treturn $this->submit_error( $matches, $posted_data, $action );\n\t\t}\n\n\t\t/**\n\t\t * Filter the validity of the form submission.\n\t\t *\n\t\t * The dynamic portion of this hook, `$action`, can be either \"registration\" or \"update\".\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.0.0 Unknown.\n\t\t *\n\t\t * @param WP_Error|true $valid       Error object containing validation errors or true when the data is valid.\n\t\t * @param array         $posted_data Array of user-submitted data.\n\t\t * @param string        $location    Form location.\n\t\t */\n\t\t$valid = apply_filters( \"lifterlms_user_{$action}_data\", true, $posted_data, $location );\n\t\tif ( is_wp_error( $valid ) ) {\n\t\t\treturn $this->submit_error( $valid, $posted_data, $action );\n\t\t}\n\n\t\t/**\n\t\t * Run an action immediately after user registration/update fields have been validated.\n\t\t *\n\t\t * The dynamic portion of this hook, `$action`, can be either \"registration\" or \"update\".\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.0.0 Moved from `LLMS_Person_Handler::update()` & LLMS_Person_Handler::register().\n\t\t *              Added parameters `$fields` and `$args`.\n\t\t *\n\t\t * @param array   $posted_data Array of user-submitted data.\n\t\t * @param string  $location    Form location.\n\t\t * @param array[] $fields      Array of LifterLMS Form Fields.\n\t\t */\n\t\tdo_action( \"lifterlms_user_{$action}_after_validation\", $posted_data, $location, $fields );\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "includes/forms/class-llms-form-post-type.php",
    "content": "<?php\n/**\n * LLMS_Form_Post_Type class\n *\n * @package LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Forms Post Type\n *\n * Handle post type registration and interactions\n *\n * @since 5.0.0\n */\nclass LLMS_Form_Post_Type {\n\n\tprivate $forms = null;\n\n\t/**\n\t * User Capability required to manage forms\n\t *\n\t * @var string\n\t */\n\tpublic $capability = 'manage_lifterlms';\n\n\t/**\n\t * Forms post type name.\n\t *\n\t * @var string\n\t */\n\tpublic $post_type = 'llms_form';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct( $forms ) {\n\n\t\t$this->forms = $forms;\n\n\t\tadd_action( 'init', array( $this, 'register_post_type' ) );\n\t\tadd_action( 'init', array( $this, 'register_meta' ) );\n\n\t\t// Modify permalink.\n\t\tadd_filter( 'post_type_link', array( $this, 'modify_permalink' ), 10, 2 );\n\n\t\t// Prevent deletion of core forms.\n\t\tadd_filter( 'pre_delete_post', array( $this, 'maybe_prevent_deletion' ), 20, 2 );\n\t\tadd_filter( 'pre_trash_post', array( $this, 'maybe_prevent_deletion' ), 20, 2 );\n\n\t\tadd_filter( 'rest_prepare_post_type', array( $this, 'enable_post_type_visibility' ), 10, 2 );\n\n\t\t/**\n\t\t * Filters the capability required to manage LifterLMS Forms\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string $capability The user capability. Default: \"manage_lifterlms\".\n\t\t */\n\t\t$this->capability = apply_filters( 'llms_forms_managment_capability', $this->capability );\n\n\t}\n\n\t/**\n\t * Forces the non-visible form post type to be visible when REST requests for the post type info are made via the admin panel\n\t *\n\t * This enabled the \"Preview in new tab\" functionality of the block editor to be used to preview LifterLMS form posts.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param WP_REST_Response $response Response object.\n\t * @param WP_Post_Type     $post_type Post Type object.\n\t * @return WP_REST_Response\n\t */\n\tpublic function enable_post_type_visibility( $response, $post_type ) {\n\t\tif ( is_admin() && $this->post_type === $post_type->name ) {\n\t\t\t$response->data['viewable'] = true;\n\t\t}\n\t\treturn $response;\n\n\t}\n\n\t/**\n\t * Retrieve a permalink for a given form post.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param WP_Post $post Form post object.\n\t * @return string|false Permalink to the form or `false` if no permalink exists for the given location.\n\t */\n\tprivate function get_permalink( $post ) {\n\n\t\t$url      = false;\n\t\t$location = get_post_meta( $post->ID, '_llms_form_location', true );\n\n\t\t$method = \"get_permalink_for_{$location}\";\n\t\tif ( $this->forms->is_location_valid( $location ) && method_exists( $this, $method ) ) {\n\t\t\t$url = $this->$method();\n\t\t}\n\n\t\t/**\n\t\t * Filters the permalink for a LifterLMS form\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string|false $url      The form's URL.\n\t\t * @param string       $location The location ID for the form.\n\t\t * @param WP_Post      $post     The form post object.\n\t\t */\n\t\treturn apply_filters( 'llms_form_permalink', $url, $location, $post );\n\n\t}\n\n\t/**\n\t * Retrieve permalink for the account edit form\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprivate function get_permalink_for_account() {\n\t\treturn llms_get_endpoint_url( 'edit-account', '', llms_get_page_url( 'myaccount' ) );\n\t}\n\n\t/**\n\t * Retrieve permalink for the checkout form\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprivate function get_permalink_for_checkout() {\n\n\t\t$url  = llms_get_page_url( 'checkout' );\n\t\t$args = array();\n\n\t\t// Add an access plan to the URL.\n\t\t$plans = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'llms_access_plan',\n\t\t\t\t'posts_per_page' => 1,\n\t\t\t\t'orderby'        => 'ID',\n\t\t\t\t'order'          => 'ASC',\n\t\t\t)\n\t\t);\n\t\tif ( $plans->have_posts() ) {\n\t\t\t$args = array(\n\t\t\t\t'plan' => $plans->posts[0]->ID,\n\t\t\t);\n\t\t}\n\n\t\treturn LLMS_View_Manager::get_url( 'visitor', $url, $args );\n\n\t}\n\n\t/**\n\t * Retrieve permalink for the registration form\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string|false Permalink or `false` when open registration is disabled.\n\t */\n\tprivate function get_permalink_for_registration() {\n\n\t\treturn LLMS_View_Manager::get_url( 'visitor', llms_get_page_url( 'myaccount' ) );\n\n\t}\n\n\t/**\n\t * Maybe prevent a post from being deleted/trashed.\n\t *\n\t * We do not allow the \"core\" forms to be deleted. This action prevents both\n\t * deletion and trash actions when run against one of the core form.\n\t *\n\t * @since 5.0.0\n\t * @since 6.4.0 Use `LLMS_Forms::is_a_core_form()` to determine whether a form is a core form and cannot be deleted.\n\t *\n\t * @param null|bool $prevent Whether or not the action has been prevented.\n\t * @param WP_Post   $post    The form post object.\n\t * @return null|false Returns `null` when we don't prevent the action and `false` if we should.\n\t */\n\tpublic function maybe_prevent_deletion( $prevent, $post ) {\n\n\t\tif ( $post->post_type === $this->post_type && LLMS_Forms::instance()->is_a_core_form( $post ) ) {\n\t\t\t$prevent = false;\n\t\t}\n\n\t\treturn $prevent;\n\t}\n\n\t/**\n\t * Modify the permalink of a given form.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string  $permalink Default permalink.\n\t * @param WP_Post $post      Post object.\n\t * @return string|false\n\t */\n\tpublic function modify_permalink( $permalink, $post ) {\n\n\t\tif ( $this->post_type !== $post->post_type ) {\n\t\t\treturn $permalink;\n\t\t}\n\n\t\treturn $this->get_permalink( $post );\n\n\t}\n\n\t/**\n\t * Register the forms post type.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function register_post_type() {\n\n\t\t$args = array(\n\t\t\t'label'               => __( 'LifterLMS Forms', 'lifterlms' ),\n\t\t\t'labels'              => array(\n\t\t\t\t'name'               => __( 'Forms', 'lifterlms' ),\n\t\t\t\t'singular_name'      => __( 'Form', 'lifterlms' ),\n\t\t\t\t'menu_name'          => _x( 'Forms', 'Admin menu name', 'lifterlms' ),\n\t\t\t\t'add_new'            => __( 'Add New Form', 'lifterlms' ),\n\t\t\t\t'add_new_item'       => __( 'Add New Form', 'lifterlms' ),\n\t\t\t\t'edit'               => __( 'Edit', 'lifterlms' ),\n\t\t\t\t'edit_item'          => __( 'Edit Form', 'lifterlms' ),\n\t\t\t\t'view'               => __( 'View Form', 'lifterlms' ),\n\t\t\t\t'view_item'          => __( 'View Form', 'lifterlms' ),\n\t\t\t\t'search_items'       => __( 'Search Forms', 'lifterlms' ),\n\t\t\t\t'not_found'          => __( 'No Forms found', 'lifterlms' ),\n\t\t\t\t'not_found_in_trash' => __( 'No Forms found in trash', 'lifterlms' ),\n\t\t\t),\n\t\t\t'public'              => false,\n\t\t\t'exclude_from_search' => true,\n\t\t\t'publicly_queryable'  => false,\n\t\t\t'show_ui'             => true,\n\t\t\t'show_in_nav_menus'   => false,\n\t\t\t'show_in_menu'        => 'lifterlms',\n\t\t\t'show_in_admin_bar'   => false,\n\t\t\t'supports'            => array( 'title', 'editor', 'custom-fields' ),\n\t\t\t'show_in_rest'        => true,\n\t\t\t'rewrite'             => false,\n\t\t\t'capabilities'        => array(\n\t\t\t\t'edit_post'              => $this->capability,\n\t\t\t\t'read_post'              => $this->capability,\n\t\t\t\t'delete_post'            => $this->capability,\n\t\t\t\t'edit_posts'             => $this->capability,\n\t\t\t\t'edit_others_posts'      => $this->capability,\n\t\t\t\t'publish_posts'          => $this->capability,\n\t\t\t\t'read_private_posts'     => $this->capability,\n\t\t\t\t'read'                   => 'read',\n\t\t\t\t'delete_posts'           => $this->capability,\n\t\t\t\t'delete_private_posts'   => $this->capability,\n\t\t\t\t'delete_published_posts' => $this->capability,\n\t\t\t\t'delete_others_posts'    => $this->capability,\n\t\t\t\t'edit_private_posts'     => $this->capability,\n\t\t\t\t'edit_published_posts'   => $this->capability,\n\t\t\t\t'create_posts'           => false,\n\t\t\t),\n\t\t);\n\n\t\tLLMS_Post_Types::register_post_type( $this->post_type, $args );\n\n\t}\n\n\t/**\n\t * Register custom postmeta properties for the forms post type.\n\t *\n\t * @since 5.0.0\n\t * @since 5.10.0 Added new meta for checkout forms and free access plans.\n\t *\n\t * @return void\n\t */\n\tpublic function register_meta() {\n\n\t\t$props = array(\n\t\t\t'_llms_form_location'                => array(\n\t\t\t\t'description' => __( 'Determines the front-end location where the form is displayed.', 'lifterlms' ),\n\t\t\t),\n\t\t\t'_llms_form_show_title'              => array(\n\t\t\t\t'description' => __( 'Determines whether or not to display the form\\'s title on the front-end.', 'lifterlms' ),\n\t\t\t),\n\t\t\t// This is only actually used for 'checkout' forms.\n\t\t\t'_llms_form_title_free_access_plans' => array(\n\t\t\t\t'description' => __( 'The alternative form title to be shown on checkout for free access plans.', 'lifterlms' ),\n\t\t\t\t'default'     => __( 'Student Information', 'lifterlms' ),\n\t\t\t),\n\t\t\t'_llms_form_is_core'                 => array(\n\t\t\t\t'description' => __( 'Determines if the form is a core form required for basic site functionality.', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $props as $prop => $settings ) {\n\n\t\t\tregister_meta(\n\t\t\t\t'post',\n\t\t\t\t$prop,\n\t\t\t\twp_parse_args(\n\t\t\t\t\t$settings,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'object_subtype'    => $this->post_type,\n\t\t\t\t\t\t'sanitize_callback' => 'sanitize_text_field',\n\t\t\t\t\t\t'auth_callback'     => array( $this, 'meta_auth_callback' ),\n\t\t\t\t\t\t'type'              => 'string',\n\t\t\t\t\t\t'single'            => true,\n\t\t\t\t\t\t'show_in_rest'      => true,\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Meta field update authorization callback.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param bool   $allowed   Is the update allowed.\n\t * @param string $meta_key  Meta keyname.\n\t * @param int    $object_id WP Object ID (post,comment,etc)...\n\t * @param int    $user_id   WP User ID.\n\t * @param string $cap       Requested capability.\n\t * @param array  $caps      User capabilities.\n\t * @return bool\n\t */\n\tpublic function meta_auth_callback( $allowed, $meta_key, $object_id, $user_id, $cap, $caps ) {\n\t\treturn user_can( $user_id, $this->capability, $object_id );\n\t}\n\n}\n"
  },
  {
    "path": "includes/forms/class-llms-form-templates.php",
    "content": "<?php\n/**\n * LLMS_Form_Templates class.\n *\n * @package LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 5.1.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Manage llms_form post type templates\n *\n * Handles creation of reusable blocks for the core/default fields used\n * by the default checkout, registration, and account edit forms.\n *\n * @since 5.0.0\n */\nclass LLMS_Form_Templates {\n\n\t/**\n\t * Transform a block definition into a confirm group\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $block A WP_Block definition array.\n\t * @return array\n\t */\n\tprivate static function add_confirm_group( $block ) {\n\n\t\t$inner = array(\n\t\t\tself::get_confirm_group_controller( $block ),\n\t\t\tself::get_confirm_group_controlled( $block ),\n\t\t);\n\n\t\tif ( is_rtl() ) {\n\t\t\t$inner = array_reverse( $inner );\n\t\t}\n\n\t\t$attrs = array(\n\t\t\t'fieldLayout' => 'columns',\n\t\t);\n\n\t\tif ( ! empty( $block['attrs']['llms_visibility'] ) ) {\n\t\t\t$attrs['llms_visibility'] = $block['attrs']['llms_visibility'];\n\t\t}\n\n\t\treturn array(\n\t\t\t'blockName'   => 'llms/form-field-confirm-group',\n\t\t\t'innerBlocks' => $inner,\n\t\t\t'attrs'       => $attrs,\n\t\t);\n\t}\n\n\t/**\n\t * Create a wp_block (resuable block) post for a given field id\n\t *\n\t * This method will attempt to use an existing reusable block field\n\t * if it already exists and will only create it if one isn't found.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.1 Run serialized block content through `wp_slash()` to preserve special characters converted to character codes.\n\t *\n\t * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t * @return int Returns the WP_Post ID of the the wp_block post type or `0` on failure.\n\t */\n\tprivate static function create_reusable_block( $field_id ) {\n\n\t\t$existing = self::find_reusable_block( $field_id );\n\t\tif ( $existing ) {\n\t\t\treturn $existing->ID;\n\t\t}\n\n\t\t$block_data = self::get_block_data( $field_id );\n\n\t\t$args = array(\n\t\t\t'post_title'   => $block_data['title'],\n\t\t\t'post_content' => wp_slash( serialize_blocks( $block_data['block'] ) ),\n\t\t\t'post_status'  => 'publish',\n\t\t\t'post_type'    => 'wp_block',\n\t\t\t'meta_input'   => array(\n\t\t\t\t'_is_llms_field' => 'yes',\n\t\t\t\t'_llms_field_id' => $field_id,\n\t\t\t),\n\t\t);\n\n\t\treturn wp_insert_post( $args );\n\t}\n\n\t/**\n\t * Locates an existing wp_block post by field id\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Method access changed from private to public.\n\t *\n\t * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t * @return WP_Post|boolean Returns the post object or false if not found.\n\t */\n\tpublic static function find_reusable_block( $field_id ) {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'posts_per_page' => 1,\n\t\t\t\t'no_found_rows'  => true,\n\t\t\t\t'post_type'      => 'wp_block',\n\t\t\t\t'meta_key'       => '_llms_field_id',\n\t\t\t\t'meta_value'     => $field_id,\n\t\t\t)\n\t\t);\n\n\t\treturn $query->posts ? $query->posts[0] : false;\n\t}\n\n\t/**\n\t * Retrieve a block array for use in a template\n\t *\n\t * Returns a reusable block when `$reusable` is `true` or returns a regular\n\t * block modified by legacy options for the given location when `$reusable` is `false`.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Method access set to public.\n\t *\n\t * @param string  $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t * @param string  $location Form location. Accepts \"checkout\", \"registration\", or \"account\".\n\t * @param boolean $reusable Whether or not a reusable block should be retrieved.\n\t * @return array\n\t */\n\tpublic static function get_block( $field_id, $location, $reusable ) {\n\n\t\tif ( $reusable ) {\n\t\t\treturn self::get_reusable_block( $field_id );\n\t\t}\n\n\t\t$legacy_opt = self::get_legacy_option( $field_id, $location );\n\t\t// Add a confirm group for email when confirmation is set or for password fields.\n\t\t$confirm    = ( ( 'email' === $field_id && 'yes' === $legacy_opt ) || 'password' === $field_id );\n\t\t$block_data = self::get_block_data( $field_id, $confirm );\n\t\tif ( 'hidden' === $legacy_opt ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t$block = $block_data['block'][0];\n\n\t\tif ( in_array( $legacy_opt, array( 'required', 'optional' ), true ) ) {\n\t\t\t$block = self::set_required_atts( $block, ( 'required' === $legacy_opt ) );\n\t\t}\n\n\t\treturn $block;\n\t}\n\n\t/**\n\t * Retrieve data for a given field by id\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string  $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t * @param boolean $confirm  If `true` and the schema includes a confirmation field, will convert the field to a confirm group.\n\t * @return array Returns an array containing the block data and title.\n\t */\n\tprivate static function get_block_data( $field_id, $confirm = true ) {\n\n\t\t$block = self::get_reusable_block_schema( $field_id );\n\t\t$title = $block['title'];\n\t\tunset( $block['title'] );\n\n\t\tif ( $confirm && ! empty( $block['confirm'] ) ) {\n\t\t\t$block = self::add_confirm_group( $block );\n\t\t}\n\n\t\t$block = self::prepare_blocks( array( $block ) );\n\t\treturn compact( 'title', 'block' );\n\t}\n\n\t/**\n\t * Creates a WP_Block array definition for the confirmation (controlled) block in a confirm group\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $block A WP_Block definition array for the primary/default block in the group.\n\t * @return array A new WP_Block definition array for the controlled block.\n\t */\n\tprivate static function get_confirm_group_controlled( $block ) {\n\n\t\t$block['blockName'] = 'llms/form-field-text';\n\n\t\t$block['attrs'] = wp_parse_args(\n\t\t\tarray(\n\t\t\t\t'field'               => $block['confirm'],\n\t\t\t\t'id'                  => $block['attrs']['id'] . '_confirm',\n\t\t\t\t'name'                => $block['attrs']['id'] . '_confirm',\n\t\t\t\t'label'               => sprintf( __( 'Confirm %s', 'lifterlms' ), $block['attrs']['label'] ),\n\t\t\t\t'columns'             => 6,\n\t\t\t\t'last_column'         => is_rtl() ? false : true,\n\t\t\t\t'isConfirmationField' => true,\n\t\t\t\t'llms_visibility'     => 'off',\n\t\t\t\t'match'               => $block['attrs']['id'],\n\t\t\t\t'data_store'          => false,\n\t\t\t\t'data_store_key'      => false,\n\t\t\t),\n\t\t\t$block['attrs']\n\t\t);\n\n\t\treturn $block;\n\t}\n\n\t/**\n\t * Creates a WP_Block array definition for the primary (controller) block in a confirm group\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $block A WP_Block definition array for the primary/default block in the group.\n\t * @return array A new WP_Block definition array for the controller block.\n\t */\n\tprivate static function get_confirm_group_controller( $block ) {\n\n\t\t$block['attrs'] = wp_parse_args(\n\t\t\tarray(\n\t\t\t\t'columns'                    => 6,\n\t\t\t\t'last_column'                => is_rtl() ? true : false,\n\t\t\t\t'isConfirmationControlField' => true,\n\t\t\t\t'llms_visibility'            => 'off',\n\t\t\t\t'match'                      => $block['attrs']['id'] . '_confirm',\n\t\t\t),\n\t\t\t$block['attrs']\n\t\t);\n\n\t\treturn $block;\n\t}\n\n\t/**\n\t * Retrieves legacy option's value for a given field and location\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t * @param string $location Form location. Accepts \"checkout\", \"registration\", or \"account\".\n\t * @return string\n\t */\n\tprivate static function get_legacy_option( $field_id, $location ) {\n\n\t\t$name_map = array(\n\t\t\t'address' => 'address',\n\t\t\t'email'   => 'email_confirmation',\n\t\t\t'name'    => 'names',\n\t\t\t'phone'   => 'phone',\n\t\t);\n\n\t\t$val = '';\n\n\t\tif ( array_key_exists( $field_id, $name_map ) ) {\n\n\t\t\t$key = sprintf( 'lifterlms_user_info_field_%1$s_%2$s_visibility', $name_map[ $field_id ], $location );\n\t\t\t$val = get_option( $key );\n\n\t\t}\n\n\t\treturn $val;\n\t}\n\n\t/**\n\t * Retrieves a core/block WP_Block array for a given default/core field\n\t *\n\t * This method will attempt to use an existing wp_block for the given field id\n\t * if it exists, and when not found creates a new one.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t * @return array A WP_Block definition array.\n\t */\n\tprivate static function get_reusable_block( $field_id ) {\n\n\t\t$ref = self::create_reusable_block( $field_id );\n\n\t\treturn array(\n\t\t\t'blockName'    => 'core/block',\n\t\t\t'attrs'        => compact( 'ref' ),\n\t\t\t'innerContent' => array(),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieves the schema definition for a default/core reusable block\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $field_id The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t * @return array The block definition schema. This is a WP_Block array definition but missing some data that is automatically populated before serialization.\n\t */\n\tprivate static function get_reusable_block_schema( $field_id ) {\n\n\t\t$list = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-reusable-blocks.php';\n\n\t\t$definition = empty( $list[ $field_id ] ) ? array() : self::prepare_block_attrs( $list[ $field_id ] );\n\n\t\t/**\n\t\t * Filters the result of a schema definition.\n\t\t *\n\t\t * This hook can be used to add definitions for custom (non-core) fields or to modify a core definition.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array  $definition The schema definition.\n\t\t * @param string $field_id   The field's identifier as found in the block schema list returned by LLMS_Form_Templates::get_reusable_block_schema().\n\t\t */\n\t\treturn apply_filters( 'llms_get_reusable_block_schema', $definition, $field_id );\n\t}\n\n\t/**\n\t * Retrieve the block template HTML for a given location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location Form location. Accepts \"checkout\", \"registration\", or \"account\".\n\t * @return string\n\t */\n\tpublic static function get_template( $location ) {\n\n\t\t/**\n\t\t * Filters whether or not reusable blocks should be used when generating a form template\n\t\t *\n\t\t * By default when migrating from 4.x, non-reusable blocks will be used in order to ensure legacy settings\n\t\t * are transferred during an upgrade to 5.x. However, on a \"clean\" install of 5.x, reusable blocks will be\n\t\t * used in favor of regular blocks.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param boolean $use_reusable Whether or not to use reusable blocks.\n\t\t */\n\t\t$use_reusable = apply_filters( 'llms_blocks_template_use_reusable_blocks', ( 'not-set' === get_option( 'lifterlms_registration_generate_username', 'not-set' ) ) );\n\n\t\t$blocks = self::get_template_blocks( $location, $use_reusable );\n\n\t\treturn serialize_blocks( $blocks );\n\t}\n\n\t/**\n\t * Retrieve a list of blocks for the given template\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string  $location Form location id.\n\t * @param boolean $reusable Whether or not reusable blocks should be used.\n\t * @return array[]\n\t */\n\tprivate static function get_template_blocks( $location, $reusable ) {\n\n\t\t$blocks = array();\n\n\t\t// Email and password are added in different locations depending on the form.\n\t\t$base = array(\n\t\t\tself::get_block( 'email', $location, $reusable ),\n\t\t\tself::get_block( 'password', $location, $reusable ),\n\t\t);\n\n\t\t// Username only added when option is off on legacy sites.\n\t\tif ( 'account' !== $location && ! llms_parse_bool( get_option( 'lifterlms_registration_generate_username', 'yes' ) ) ) {\n\t\t\tarray_unshift( $base, self::get_reusable_block( 'username' ) );\n\t\t}\n\n\t\t// Email and password go first on checkout/reg forms.\n\t\tif ( 'account' !== $location ) {\n\t\t\t$blocks = array_merge( $base, $blocks );\n\t\t}\n\n\t\t$blocks[] = self::get_block( 'name', $location, $reusable );\n\n\t\t// Display name on account only, users can add to other forms if desired.\n\t\tif ( 'account' === $location ) {\n\t\t\t$blocks[] = self::get_block( 'display_name', $location, $reusable );\n\t\t}\n\n\t\t$blocks[] = self::get_block( 'address', $location, $reusable );\n\t\t$blocks[] = self::get_block( 'phone', $location, $reusable );\n\n\t\tif ( 'registration' === $location ) {\n\t\t\t$blocks[] = self::get_voucher_block();\n\t\t}\n\n\t\t// Email and password go at the end on the account form.\n\t\tif ( 'account' === $location ) {\n\t\t\t$blocks = array_merge( $blocks, $base );\n\t\t}\n\n\t\treturn array_filter( $blocks );\n\t}\n\n\t/**\n\t * Retrieve block for the voucher row.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprivate static function get_voucher_block() {\n\n\t\t// Don't include voucher if legacy option has vouchers hidden.\n\t\t$option = get_option( 'lifterlms_voucher_field_registration_visibility', 'optional' );\n\t\tif ( 'hidden' === $option ) {\n\t\t\treturn array();\n\t\t}\n\n\t\treturn array(\n\t\t\t'blockName'    => 'llms/form-field-redeem-voucher',\n\t\t\t'attrs'        => array(\n\t\t\t\t'id'             => 'llms_voucher',\n\t\t\t\t'label'          => __( 'Have a voucher?', 'lifterlms' ),\n\t\t\t\t'placeholder'    => __( 'Voucher Code', 'lifterlms' ),\n\t\t\t\t'required'       => ( 'required' === $option ),\n\t\t\t\t'toggleable'     => true,\n\t\t\t\t'data_store'     => false,\n\t\t\t\t'data_store_key' => false,\n\t\t\t),\n\t\t\t'innerContent' => array(),\n\t\t);\n\t}\n\n\t/**\n\t * Prepares block attributes for a given reusable block\n\t *\n\t * This method loads a reusable block from the blocks schema and attempts to locate a user information field\n\t * for the given field block from the user information fields schema.\n\t *\n\t * The field is matched by the block's \"id\" attribute which should match a user information field's \"name\" attribute.\n\t *\n\t * When a match is found, the information field data is merged into the block data and the settings are converted from field settings\n\t * to block attributes.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $block A partial WP_Block array used to create a reusable block.\n\t * @return array\n\t */\n\tprivate static function prepare_block_attrs( $block ) {\n\n\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\t\t\tforeach ( $block['innerBlocks'] as &$inner_block ) {\n\t\t\t\t$inner_block = self::prepare_block_attrs( $inner_block );\n\t\t\t}\n\t\t} elseif ( ! empty( $block['attrs']['id'] ) ) {\n\n\t\t\t// If we find a field, merge the block into the field and convert it to block attributes.\n\t\t\t$field          = llms_get_user_information_field( $block['attrs']['id'] );\n\t\t\t$block['attrs'] = $field ? LLMS_Forms::instance()->convert_settings_to_block_attrs( wp_parse_args( $field, $block['attrs'] ) ) : $block['attrs'];\n\n\t\t}\n\n\t\treturn $block;\n\t}\n\n\t/**\n\t * Recursively prepare a list of blocks to ensure it can be passed into serialize_blocks() without error\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $blocks Array of WP_Block definition arrays.\n\t * @return array[]\n\t */\n\tprivate static function prepare_blocks( $blocks ) {\n\n\t\tforeach ( $blocks as &$block ) {\n\n\t\t\t$block = wp_parse_args(\n\t\t\t\t$block,\n\t\t\t\tarray(\n\t\t\t\t\t'attrs'       => array(),\n\t\t\t\t\t'innerBlocks' => array(),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\t\t\t\t$block['innerBlocks'] = self::prepare_blocks( $block['innerBlocks'] );\n\t\t\t}\n\n\t\t\t// WP core serialize_block() doesn't work unless this...\n\t\t\t$block['innerContent'] = array_fill( 0, count( $block['innerBlocks'] ), null );\n\n\t\t}\n\n\t\treturn $blocks;\n\t}\n\n\t/**\n\t * Modifies the `required` block attribute\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $block    A WP_Block definition array.\n\t * @param boolean $required Desired value of the required attribute.\n\t */\n\tprivate static function set_required_atts( $block, $required ) {\n\n\t\tif ( isset( $block['attrs']['required'] ) && 'llms/form-field-user-address-street-secondary' !== $block['blockName'] ) {\n\t\t\t$block['attrs']['required'] = $required;\n\t\t}\n\n\t\tforeach ( $block['innerBlocks'] as &$inner_block ) {\n\t\t\t$inner_block = self::set_required_atts( $inner_block, $required );\n\t\t}\n\n\t\treturn $block;\n\t}\n}\n"
  },
  {
    "path": "includes/forms/class-llms-form-validator.php",
    "content": "<?php\n/**\n * Handles data sanitization & validation for the LLMS_Form_Handler class\n *\n * @package LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 5.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Form_Handler class.\n *\n * @since 5.0.0\n */\nclass LLMS_Form_Validator {\n\n\t/**\n\t * Filters a list of fields down to only the required fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $fields Array of LifterLMS Form Field settings arrays.\n\t * @return array[]\n\t */\n\tpublic function get_required_fields( $fields ) {\n\t\treturn array_values(\n\t\t\tarray_filter(\n\t\t\t\t$fields,\n\t\t\t\tfunction( $field ) {\n\t\t\t\t\treturn ! empty( $field['required'] );\n\t\t\t\t}\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Sanitize a single field according to its type\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param mixed $posted_value User-submitted (dirty) value.\n\t * @param array $field        LifterLMS field settings.\n\t * @return mixed\n\t */\n\tpublic function sanitize_field( $posted_value, $field ) {\n\n\t\t$map = array(\n\t\t\t'email'    => 'sanitize_email',\n\t\t\t'number'   => array( $this, 'sanitize_field_number' ),\n\t\t\t'tel'      => array( $this, 'sanitize_field_tel' ),\n\t\t\t'textarea' => 'sanitize_textarea_field',\n\t\t\t'url'      => 'esc_url_raw',\n\t\t);\n\n\t\t$func = isset( $map[ $field['type'] ] ) ? $map[ $field['type'] ] : 'sanitize_text_field';\n\n\t\t// Turn the submitted value into array, so to unify sanitization of scalar and array posted values.\n\t\t$to_sanitize = is_array( $posted_value ) ? $posted_value : array( $posted_value );\n\t\t$sanitized   = array();\n\n\t\tforeach ( $to_sanitize as $value ) {\n\t\t\t$sanitized[] = trim( call_user_func( $func, $value ) );\n\t\t}\n\n\t\treturn is_array( $posted_value ) ? $sanitized : $sanitized[0];\n\n\t}\n\n\t/**\n\t * Sanitize a number field\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return string\n\t */\n\tprotected function sanitize_field_number( $posted_value ) {\n\t\treturn preg_replace( '/[^0-9.,]/', '', $posted_value );\n\t}\n\n\t/**\n\t * Sanitize a telephone field\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return string\n\t */\n\tprotected function sanitize_field_tel( $posted_value ) {\n\t\treturn preg_replace( '/[^\\s\\#0-9\\-\\+\\(\\)\\.]/', '', $posted_value );\n\n\t}\n\n\t/**\n\t * Sanitize all user-submitted data according to field settings\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $posted_data User-submitted form data.\n\t * @param array[] $fields      LifterLMS form fields settings.\n\t * @return array\n\t */\n\tpublic function sanitize_fields( $posted_data, $fields ) {\n\n\t\tforeach ( $fields as $field ) {\n\n\t\t\tif ( empty( $field['name'] ) || ! isset( $posted_data[ $field['name'] ] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$posted_data[ $field['name'] ] = $this->sanitize_field( $posted_data[ $field['name'] ], $field );\n\n\t\t}\n\n\t\treturn $posted_data;\n\n\t}\n\n\t/**\n\t * Validate a posted value\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param mixed $posted_value Posted data.\n\t * @param array $field        LifterLMS Form Field settings array.\n\t * @return WP_Error|true\n\t */\n\tpublic function validate_field( $posted_value, $field ) {\n\n\t\t// Validate field by type.\n\t\t$type_map = array(\n\t\t\t'email'  => array( $this, 'validate_field_email' ),\n\t\t\t'number' => array( $this, 'validate_field_number' ),\n\t\t\t'tel'    => array( $this, 'validate_field_tel' ),\n\t\t\t'url'    => array( $this, 'validate_field_url' ),\n\t\t);\n\n\t\t// Turn the submitted value into array, so to unify validation of scalar and array posted values.\n\t\t$to_validate = is_array( $posted_value ) ? $posted_value : array( $posted_value );\n\n\t\tforeach ( $to_validate as $value ) {\n\n\t\t\t$valid = isset( $type_map[ $field['type'] ] ) ? call_user_func( $type_map[ $field['type'] ], $value, $field ) : true;\n\t\t\tif ( is_wp_error( $valid ) ) { // Return as soon as a field is not valid.\n\t\t\t\treturn $valid;\n\t\t\t}\n\n\t\t\t// HTML Attribute Validations.\n\t\t\tif ( ! empty( $field['attributes']['minlength'] ) ) {\n\t\t\t\t$valid = $this->validate_field_attribute_minlength( $value, $field['attributes']['minlength'], $field );\n\t\t\t\tif ( is_wp_error( $valid ) ) {\n\t\t\t\t\treturn $valid;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Perform special validations for special field types (scalar by their nature).\n\t\t$extra_map = array(\n\t\t\t'llms_voucher'     => array( $this, 'validate_field_voucher' ),\n\t\t\t'password_current' => array( $this, 'validate_field_current_password' ),\n\t\t\t'user_email'       => array( $this, 'validate_field_user_email' ),\n\t\t\t'user_login'       => array( $this, 'validate_field_user_login' ),\n\t\t);\n\t\t$valid     = isset( $extra_map[ $field['id'] ] ) ? call_user_func( $extra_map[ $field['id'] ], $posted_value ) : true;\n\t\tif ( is_wp_error( $valid ) ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validates the html input minlength attribute\n\t *\n\t * Used by the User Password field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted value.\n\t * @param int    $minlength    The minimum string length as parsed from the field block.\n\t * @param array  $field        LifterLMS Form Field settings array.\n\t * @return WP_Error|boolean Returns `true` for a valid value, otherwise an error.\n\t */\n\tprotected function validate_field_attribute_minlength( $posted_value, $minlength, $field ) {\n\n\t\tif ( strlen( $posted_value ) < $minlength ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t'llms-form-field-invalid',\n\t\t\t\tsprintf(\n\t\t\t\t\t__( 'The %1$s must be at least %2$d characters in length.', 'lifterlms' ),\n\t\t\t\t\tisset( $field['label'] ) ? $field['label'] : $field['name'],\n\t\t\t\t\t$minlength\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validate an email field\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_email( $posted_value ) {\n\n\t\tif ( ! is_email( $posted_value ) ) {\n\t\t\t// Translators: %s user submitted value.\n\t\t\treturn new WP_Error( 'llms-form-field-invalid', sprintf( __( 'The email address \"%s\" is not valid.', 'lifterlms' ), $posted_value ) );\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validate a number field\n\t *\n\t * Ensures the posted valued is numeric and, where applicable, ensures that the number falls\n\t * within minimum and maximum value requirements.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @param array  $field        The LLMS_Form_Field settings array.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_number( $posted_value, $field ) {\n\n\t\t$temp_value = str_replace( ',', '', $posted_value );\n\t\tif ( ! is_numeric( $temp_value ) ) {\n\t\t\t// Translators: %1$s field label or name; %2$s = user submitted value.\n\t\t\treturn new WP_Error( 'llms-form-field-invalid', sprintf( __( 'The %1$s \"%2$s\" is not valid number.', 'lifterlms' ), isset( $field['label'] ) ? $field['label'] : $field['name'], $posted_value ) );\n\t\t} elseif ( isset( $field['attributes'] ) ) {\n\t\t\tif ( ( ! empty( $field['attributes']['min'] ) || ( isset( $field['attributes']['min'] ) && '0' === $field['attributes']['min'] ) ) && $temp_value < $field['attributes']['min'] ) {\n\t\t\t\t// Translators: %1$s = field label or name; %2$s = user submitted value; %3$d = minimum allowed number.\n\t\t\t\treturn new WP_Error( 'llms-form-field-invalid', sprintf( __( 'The %1$s \"%2$s\" must be greater than or equal to %3$d.', 'lifterlms' ), isset( $field['label'] ) ? $field['label'] : $field['name'], $posted_value, $field['attributes']['min'] ) );\n\t\t\t} elseif ( ( ! empty( $field['attributes']['max'] ) || ( isset( $field['attributes']['max'] ) && '0' === $field['attributes']['max'] ) ) && $temp_value > $field['attributes']['max'] ) {\n\t\t\t\t// Translators: %1$s = field label or name; %2$s = user submitted value; %3$d = maximum allowed number.\n\t\t\t\treturn new WP_Error( 'llms-form-field-invalid', sprintf( __( 'The %1$s \"%2$s\" must be less than or equal to %3$d.', 'lifterlms' ), isset( $field['label'] ) ? $field['label'] : $field['name'], $posted_value, $field['attributes']['max'] ) );\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validate a logged-in users current password\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_current_password( $posted_value ) {\n\n\t\tif ( ! is_user_logged_in() ) {\n\t\t\treturn new WP_Error( 'llms-form-field-invalid-no-user', __( 'You must be logged in to update your password.', 'lifterlms' ), $posted_value );\n\t\t}\n\n\t\t$user = wp_get_current_user();\n\t\tif ( ! wp_check_password( $posted_value, $user->user_pass ) ) {\n\t\t\treturn new WP_Error( 'llms-form-field-invalid', __( 'The submitted password was not correct.', 'lifterlms' ), $posted_value );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Validate a telephone field\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_tel( $posted_value ) {\n\n\t\tif ( 0 < strlen( trim( preg_replace( '/[\\s\\#0-9\\-\\+\\(\\)\\.]/', '', $posted_value ) ) ) ) {\n\t\t\t// Translators: %s = user submitted value.\n\t\t\treturn new WP_Error( 'llms-form-field-invalid', sprintf( __( 'The phone number \"%s\" is not valid.', 'lifterlms' ), $posted_value ) );\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validate a url field\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_url( $posted_value ) {\n\n\t\tif ( ! filter_var( $posted_value, FILTER_VALIDATE_URL ) ) {\n\t\t\t// Translators: %s = user submitted value.\n\t\t\treturn new WP_Error( 'llms-form-field-invalid', sprintf( __( 'The URL \"%s\" is not valid.', 'lifterlms' ), $posted_value ) );\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validate a user-email field\n\t *\n\t * User emails must be unique.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_user_email( $posted_value ) {\n\t\tif ( email_exists( $posted_value ) ) {\n\t\t\treturn new WP_Error( 'llms-form-field-not-unique', sprintf( __( 'An account with the email address \"%s\" already exists.', 'lifterlms' ), $posted_value ) );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Validate a user-login field\n\t *\n\t * Ensures that a username isn't found in the LifterLMS username blocklist, that it meets the default\n\t * WP core username criteria and that the username doesn't already exist.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_user_login( $posted_value ) {\n\t\tif ( in_array( $posted_value, llms_get_usernames_blocklist(), true ) || ! validate_username( $posted_value ) ) {\n\t\t\treturn new WP_Error( 'llms-form-field-invalid', sprintf( __( 'The username \"%s\" is invalid, please try a different username.', 'lifterlms' ), $posted_value ), $posted_value );\n\t\t} elseif ( username_exists( $posted_value ) ) {\n\t\t\treturn new WP_Error( 'llms-form-field-not-unique', sprintf( __( 'An account with the username \"%s\" already exists.', 'lifterlms' ), $posted_value ), $posted_value );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Validate a voucher field ensuring it's a valid and usable voucher code\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $posted_value User-submitted (dirty) value.\n\t * @return WP_Error|boolean Returns `true` for a valid submission, otherwise an error.\n\t */\n\tprotected function validate_field_voucher( $posted_value ) {\n\n\t\t$voucher = new LLMS_Voucher();\n\t\t$check   = $voucher->check_voucher( $posted_value );\n\t\tif ( is_wp_error( $check ) ) {\n\t\t\treturn new WP_Error( 'llms-form-field-invalid', $check->get_error_message(), array( $posted_value, $check ) );\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Validate submitted field values.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Don't validate form with no user input only if the form is not empty itself (e.g. contains only invisible fields).\n\t *\n\t * @param array   $posted_data Array of posted data.\n\t * @param array[] $fields      Array of LifterLMS Form Fields.\n\t * @return WP_Error|true\n\t */\n\tpublic function validate_fields( $posted_data, $fields ) {\n\n\t\tif ( empty( $posted_data ) && ! empty( $fields ) ) {\n\t\t\treturn new WP_Error( 'llms-form-no-input', __( 'Cannot validate a form with no user input.', 'lifterlms' ) );\n\t\t}\n\n\t\t$err      = new WP_Error();\n\t\t$err_data = array();\n\t\tforeach ( $fields as $field ) {\n\n\t\t\tif ( empty( $field['name'] ) || empty( $posted_data[ $field['name'] ] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$valid = $this->validate_field( $posted_data[ $field['name'] ], $field );\n\t\t\tif ( is_wp_error( $valid ) ) {\n\t\t\t\t$err->add( $valid->get_error_code(), $valid->get_error_message() );\n\t\t\t\t$err_data[ $field['name'] ] = $field;\n\t\t\t}\n\t\t}\n\n\t\tif ( $err->errors ) {\n\t\t\t$err->add_data( $err_data );\n\t\t\treturn $err;\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Ensure matching fields match one another.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $posted_data Array of posted data.\n\t * @param array[] $fields      Array of LifterLMS form fields.\n\t * @return WP_Error|true\n\t */\n\tpublic function validate_matching_fields( $posted_data, $fields ) {\n\n\t\t$err      = new WP_Error();\n\t\t$err_data = array();\n\n\t\t$matches = array();\n\t\tforeach ( $fields as $field ) {\n\n\t\t\t// Field doesn't have a match to check or it was already checked by it's match.\n\t\t\tif ( empty( $field['match'] ) || in_array( $field['id'], $matches, true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$field_name = isset( $field['label'] ) ? $field['label'] : $field['name'];\n\n\t\t\t$name        = $field['name'];\n\t\t\t$match_field = LLMS_Forms::instance()->get_field_by( $fields, 'id', $field['match'] );\n\t\t\tif ( ! $match_field ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$match = $match_field['name'];\n\n\t\t\t$val   = isset( $posted_data[ $name ] ) ? $posted_data[ $name ] : '';\n\t\t\t$match = isset( $posted_data[ $match ] ) ? $posted_data[ $match ] : '';\n\n\t\t\tif ( $val !== $match ) {\n\n\t\t\t\t$match_name = isset( $match_field['label'] ) ? $match_field['label'] : $match_field['name'];\n\t\t\t\t$err->add( 'llms-form-field-not-matched', sprintf( __( '%1$s must match %2$s.', 'lifterlms' ), $field_name, $match_name ) );\n\t\t\t\t$err_data[] = array( $field, $match_field );\n\n\t\t\t}\n\n\t\t\t// Fields reference each other so we only need to check the pair one time.\n\t\t\t$matches[] = $match_field['id'];\n\n\t\t}\n\n\t\tif ( $err->errors ) {\n\t\t\t$err->add_data( $err_data, 'llms-form-field-not-matched' );\n\t\t\treturn $err;\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Ensure that all of the forms required fields are present in the submitted data.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $posted_data User data (likely from $_POST).\n\t * @param array[] $fields      Array of LifterLMS form fields.\n\t * @return WP_Error|true\n\t */\n\tpublic function validate_required_fields( $posted_data, $fields ) {\n\n\t\t// Ensure all required fields have been submitted.\n\t\t$err      = new WP_Error();\n\t\t$err_data = array();\n\t\tforeach ( $this->get_required_fields( $fields ) as $field ) {\n\n\t\t\tif ( empty( $posted_data[ $field['name'] ] ) ) {\n\t\t\t\t// Translators: %s = field label or name.\n\t\t\t\t$err->add( 'llms-form-missing-required', sprintf( __( '%s is a required field.', 'lifterlms' ), isset( $field['label'] ) ? $field['label'] : $field['name'] ) );\n\t\t\t\t$err_data[ $field['name'] ] = $field;\n\t\t\t}\n\t\t}\n\n\t\tif ( $err->errors ) {\n\t\t\t$err->add_data( $err_data, 'llms-form-missing-required' );\n\t\t\treturn $err;\n\t\t}\n\n\t\treturn true;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/forms/class-llms-forms-admin-bar.php",
    "content": "<?php\n/**\n * LLMS_Forms_Admin_Bar calss\n *\n * @package  LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Add WP Admin Bar Nodes to enable editing of the currently-viewed form by a qualifying user\n *\n * @since 5.0.0\n */\nclass LLMS_Forms_Admin_Bar {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'admin_bar_menu', array( $this, 'add_menu_items' ), 999 );\n\n\t}\n\n\t/**\n\t * Add view links to the admin menu bar for qualifying users.\n\t *\n\t * @since 3.7.0\n\t * @since 3.16.0 Unknown.\n\t * @since 4.2.0 Updated icon.\n\t * @since 4.5.1 Use `should_display()` method to determine if the view manager should be added to the admin bar.\n\t * @since 4.16.0 Retrieve nodes to add from `get_menu_items_to_add()`.\n\t *\n\t * @param WP_Admin_Bar $wp_admin_bar Admin bar class instance.\n\t * @return void\n\t */\n\tpublic function add_menu_items( $wp_admin_bar ) {\n\n\t\tif ( ! $this->should_display() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$args = array( $this->get_current_location() );\n\t\t$plan = llms_filter_input( INPUT_GET, 'plan', FILTER_SANITIZE_NUMBER_INT );\n\t\tif ( $plan ) {\n\t\t\t$args[] = array( 'plan' => llms_get_post( $plan ) );\n\t\t}\n\t\t$form = llms_get_form( ...$args );\n\n\t\t$wp_admin_bar->add_node(\n\t\t\tarray(\n\t\t\t\t'id'     => 'llms-edit-form',\n\t\t\t\t'parent' => 'edit',\n\t\t\t\t'title'  => __( 'Edit Form', 'lifterlms' ),\n\t\t\t\t'href'   => get_edit_post_link( $form->ID ),\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the form location for the current screen\n\t *\n\t * Must be on a checkout screen, the \"edit account\" tab of the dashboard,\n\t * or be viewing as a visitor on the main dashboard page with open registration enabled.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string|boolean Returns the location id as a string or `false` if not on a form location screen.\n\t */\n\tprivate function get_current_location() {\n\n\t\tif ( is_llms_checkout() ) {\n\n\t\t\treturn 'checkout';\n\n\t\t} elseif ( is_llms_account_page() ) {\n\n\t\t\t$tab = LLMS_Student_Dashboard::get_current_tab( 'tab' );\n\n\t\t\tif ( 'edit-account' === $tab ) {\n\t\t\t\treturn 'account';\n\t\t\t}\n\n\t\t\tif ( 'dashboard' === $tab && 'visitor' === llms_filter_input( INPUT_GET, 'llms-view-as' ) && llms_parse_bool( llms_get_open_registration_status() ) ) {\n\t\t\t\treturn 'registration';\n\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Determine whether or an Edit Form node should be added to the admin bar.\n\t *\n\t * The user must be able to edit forms and be on a screen with a displayed form.\n\t *\n\t * @return boolean\n\t */\n\tprivate function should_display() {\n\n\t\t$display = ( current_user_can( LLMS_Forms::instance()->get_capability() ) && $this->get_current_location() );\n\n\t\t/**\n\t\t * Filters whether or not the \"Edit Form\" WP_Admin_Bar node is displayed\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param boolean $display Whether or not to display the node.\n\t\t */\n\t\treturn apply_filters( 'llms_should_display_wp_admin_bar_nodes_for_forms', $display );\n\n\t}\n\n}\n\nreturn new LLMS_Forms_Admin_Bar();\n"
  },
  {
    "path": "includes/forms/class-llms-forms-classic-editor.php",
    "content": "<?php\n/**\n * Disables Classic Editor plugin functionality for forms post types\n *\n * We do not support the classic editor for form building.\n *\n * @package LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Forms_Classic_Editor\n *\n * @since 5.0.0\n */\nclass LLMS_Forms_Classic_Editor {\n\n\t/**\n\t * Static \"constructor\"\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\tadd_filter( 'use_block_editor_for_post_type', array( __CLASS__, 'force_block_editor' ), 200, 2 );\n\t\tadd_filter( 'classic_editor_enabled_editors_for_post_type', array( __CLASS__, 'disable_classic_editor' ), 20, 2 );\n\n\t}\n\n\t/**\n\t * Force the block editor to be used for forms post type editing\n\t *\n\t * The classic editor uses this filter (at priority 100) to disable the block editor\n\t * when the default editor for all users is the classic editor and users are not\n\t * allowed to switch editors.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @link https://developer.wordpress.org/reference/functions/use_block_editor_for_post_type\n\t *\n\t * @param boolean $use_block_editor Whether or not to use the block editor for the post type.\n\t * @param string  $post_type        The post type being checked.\n\t * @return boolean\n\t */\n\tpublic static function force_block_editor( $use_block_editor, $post_type ) {\n\t\treturn LLMS_Forms::instance()->get_post_type() === $post_type ? true : $use_block_editor;\n\t}\n\n\t/**\n\t * Prevent users from being allowed to choose the classic editor for forms post types\n\t *\n\t * The classic editor uses this filter to determine which editors are available for the given custom\n\t * post type when users are allowed to choose which editor to use.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array  $editors   Associative array. The array key identifies the editor and the array value is a boolean\n\t *                          specifying whether or not the editor is enabled for the given post type.\n\t * @param string $post_type The post type being checked.\n\t * @return array\n\t */\n\tpublic static function disable_classic_editor( $editors, $post_type ) {\n\t\tif ( LLMS_Forms::instance()->get_post_type() === $post_type ) {\n\t\t\t$editors['classic_editor'] = false;\n\t\t}\n\t\treturn $editors;\n\t}\n\n}\n\nreturn LLMS_Forms_Classic_Editor::init();\n"
  },
  {
    "path": "includes/forms/class-llms-forms-data.php",
    "content": "<?php\n/**\n * LLMS_Forms Data class file\n *\n * @package LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Manage data associated with llms_form posts\n *\n * @since 5.0.0\n */\nclass LLMS_Forms_Data {\n\n\t/**\n\t * Reference to the LLMS_Forms instance\n\t *\n\t * @var LLMS_Forms\n\t */\n\tprivate $forms = null;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->forms = LLMS_Forms::instance();\n\n\t\tadd_action( \"save_post_{$this->forms->get_post_type()}\", array( $this, 'save_username_locations' ), 10, 2 );\n\n\t}\n\n\t/**\n\t * Locate a username/user login block within a list of blocks\n\t *\n\t * Checks into innerBlocks recursively.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $blocks Array of WP_Block definition arrays.\n\t * @return boolean Returns `true` when a username block is found, otherwise returns `false`.\n\t */\n\tprivate function has_username_block( $blocks ) {\n\n\t\tforeach ( $blocks as $block ) {\n\n\t\t\tif ( 'llms/form-field-user-login' === $block['blockName'] ) {\n\t\t\t\treturn true;\n\t\t\t} elseif ( $block['innerBlocks'] ) {\n\t\t\t\tif ( $this->has_username_block( $block['innerBlocks'] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * When saving a form store a form reference in the options table\n\t *\n\t * This will be used to LLMS_Forms::are_usernames_enabled() to determine\n\t * if the site allows login via usernames.\n\t *\n\t * Callback function for save_post_llms_forms and delete_post hooks.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param int     $post_id ID of the form being saved.\n\t * @param WP_Post $post    Form post object.\n\t * @return int[] Returns an array of WP_Post IDs representing all the forms where the username block existss.\n\t */\n\tpublic function save_username_locations( $post_id, $post ) {\n\n\t\t// Load existing locations.\n\t\t$locations = get_option( 'llms_forms_username_locations', array() );\n\n\t\t$post_id            = absint( $post_id );\n\t\t$blocks             = $this->forms->parse_blocks( $post->post_content );\n\t\t$has_username_block = $this->has_username_block( $blocks );\n\n\t\t// Add or remove the location depending on the presence of the block.\n\t\tif ( $has_username_block ) {\n\t\t\t$locations[] = $post_id;\n\t\t} else {\n\t\t\t$locations = array_diff( $locations, array( $post_id ) );\n\t\t}\n\n\t\t$locations = array_unique( $locations );\n\n\t\t// Store it.\n\t\tupdate_option( 'llms_forms_username_locations', $locations );\n\n\t\treturn $locations;\n\n\t}\n\n}\n\nreturn new LLMS_Forms_Data();\n"
  },
  {
    "path": "includes/forms/class-llms-forms-dynamic-fields.php",
    "content": "<?php\n/**\n * LLMS_Forms_Dynamic_Fields file\n *\n * @package LifterLMS/Classes/Forms\n *\n * @since 5.0.0\n * @version 5.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Manage dynamically generated fields added to the form outside of the block editor\n *\n * @since 5.0.0\n */\nclass LLMS_Forms_Dynamic_Fields {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Added logic to make sure forms have all the required fields.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_filter( 'llms_get_form_blocks', array( $this, 'add_password_strength_meter' ), 10, 2 );\n\t\tadd_filter( 'llms_get_form_blocks', array( $this, 'maybe_add_required_block_fields' ), 10, 3 );\n\t\tadd_filter( 'llms_get_form_blocks', array( $this, 'modify_account_form' ), 15, 2 );\n\n\t}\n\n\t/**\n\t * Creates a new HTML block with the given settings and inserts it into an existing blocks array at the specified location\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $blocks         Array of WP_Block arrays.\n\t * @param array   $block_settings Block attributes used to generate a new custom HTML field block.\n\t * @param integer $index          Desired index of the new block.\n\t *\n\t * @return array[]\n\t */\n\tprivate function add_block( $blocks, $block_settings, $index ) {\n\n\t\t// Make the new block.\n\t\t$add_block = parse_blocks(\n\t\t\tLLMS_Forms::instance()->get_custom_field_block_markup( $block_settings )\n\t\t);\n\n\t\t// Add it into the form after the specified index.\n\t\tarray_splice( $blocks, $index + 1, 0, $add_block );\n\n\t\treturn $blocks;\n\n\t}\n\n\t/**\n\t * Adds a password strength meter to a block list\n\t *\n\t * This function will programmatically add an html block containing the necessary\n\t * markup for the password strength meter to function.\n\t *\n\t * This will locate the user password block and output the meter immediately after\n\t * the block. If the password block is within a group it'll output it after the\n\t * group block.\n\t *\n\t * @since 5.0.0\n\t * @since 5.0.1 Add `aria-live=polite` to ensure password strength is announced for screen readers.\n\t *\n\t * @param array[] $blocks WP_Block list.\n\t * @return array[]\n\t */\n\tpublic function add_password_strength_meter( $blocks, $location ) {\n\n\t\t$password = $this->find_block( 'password', $blocks );\n\n\t\t// No password field in the form.\n\t\tif ( ! $password ) {\n\t\t\treturn $blocks;\n\t\t}\n\n\t\tlist( $index, $block ) = $password;\n\n\t\t// Meter not enabled.\n\t\tif ( empty( $block['attrs']['meter'] ) || ! llms_parse_bool( $block['attrs']['meter'] ) ) {\n\t\t\treturn $blocks;\n\t\t}\n\n\t\t$meter_settings = array(\n\t\t\t'type'            => 'html',\n\t\t\t'id'              => 'llms-password-strength-meter',\n\t\t\t'classes'         => 'llms-password-strength-meter',\n\t\t\t'description'     => ! empty( $block['attrs']['meter_description'] ) ? $block['attrs']['meter_description'] : '',\n\t\t\t'min_length'      => ! empty( $block['attrs']['html_attrs']['minlength'] ) ? $block['attrs']['html_attrs']['minlength'] : '',\n\t\t\t'min_strength'    => ! empty( $block['attrs']['min_strength'] ) ? $block['attrs']['min_strength'] : '',\n\t\t\t'llms_visibility' => ! empty( $block['attrs']['llms_visibility'] ) ? $block['attrs']['llms_visibility'] : '',\n\t\t\t'attributes'      => array(\n\t\t\t\t'aria-live' => 'polite',\n\t\t\t),\n\t\t);\n\n\t\tif ( 'account' === $location ) {\n\t\t\t$meter_settings['wrapper_classes'] = 'llms-visually-hidden-field';\n\t\t}\n\n\t\t/**\n\t\t * Filters the settings used to create the dynamic password strength meter block\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array $meter_settings Array or block attributes/settings.\n\t\t */\n\t\t$meter_settings = apply_filters( 'llms_password_strength_meter_field_settings', $meter_settings );\n\n\t\treturn $this->add_block( $blocks, $meter_settings, $index );\n\n\t}\n\n\t/**\n\t * Finds a block with the specified ID within a list of blocks\n\t *\n\t * There's a gotcha with this function... if a user password field is placed within a wp core columns block\n\t * the password strength meter will be added outside the column the password is contained within.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string  $id           The ID of the field to find.\n\t * @param array[] $blocks       WP_Block list.\n\t * @param integer $parent_index Top level index of the parent block. Used to hold a reference to the current index within the toplevel\n\t *                              blocks of the form when looking into the innerBlocks of a block.\n\t * @return boolean|array Returns `false` when the block cannot be found in the given list, otherwise returns a numeric array\n\t *                       where item `0` is the index of the block within the list (the index of the items parent if it's in a\n\t *                       group) and item `1` is the block array.\n\t */\n\tprivate function find_block( $id, $blocks, $parent_index = null ) {\n\n\t\tforeach ( $blocks as $index => $block ) {\n\n\t\t\tif ( ! empty( $block['attrs']['id'] ) && $id === $block['attrs']['id'] ) {\n\t\t\t\treturn array( is_null( $parent_index ) ? $index : $parent_index, $block );\n\t\t\t}\n\n\t\t\tif ( $block['innerBlocks'] ) {\n\t\t\t\t$inner = $this->find_block( $id, $block['innerBlocks'], is_null( $parent_index ) ? $index : $parent_index );\n\t\t\t\tif ( false !== $inner ) {\n\t\t\t\t\treturn $inner;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Retrieve the fields required for a given location based on user state\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param string $location The request form location ID.\n\t * @param array  $args     Additional arguments passed to the short-circuit filter.\n\t * @return array[] Array of field_id => block_name required or an empty array if no fields required.\n\t */\n\tprivate function get_required_fields_for_location( $location, $args ) {\n\n\t\t$fields = array();\n\n\t\tif (\n\t\t\t( ! is_user_logged_in() && in_array( $location, array( 'checkout', 'registration' ), true ) ) ||\n\t\t\t\t( is_user_logged_in() && 'account' === $location ) ) {\n\t\t\t$fields = array(\n\t\t\t\t// Field ID => block name.\n\t\t\t\t'email_address' => 'email',\n\t\t\t\t'password'      => 'password',\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * Filters the required block fields to add to the form\n\t\t *\n\t\t * @since 5.1.0\n\t\t *\n\t\t * @param array[] $fields   Array of field_id => block_name required.\n\t\t * @param string  $location The request form location ID.\n\t\t * @param array   $args     Additional arguments passed to the short-circuit filter.\n\t\t */\n\t\treturn apply_filters( 'llms_forms_required_block_fields', $fields, $location, $args );\n\n\t}\n\n\t/**\n\t * Retrieve the HTML for a field toggle button link\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $fields      A comma-separated list of selectors for the controlled fields.\n\t * @param string $field_label Label for the original field.\n\t * @return string\n\t */\n\tprivate function get_toggle_button_html( $fields, $field_label ) {\n\n\t\t// Translator: %s = user-selected label for the given field being toggled.\n\t\t$change_text = sprintf( esc_attr_x( 'Change %s', 'Toggle button for changing email or password', 'lifterlms' ), $field_label );\n\t\t$cancel_text = esc_attr_x( 'Cancel', 'Cancel password or email address change button text', 'lifterlms' );\n\n\t\treturn '<a class=\"llms-toggle-fields\" data-fields=\"' . $fields . '\" data-change-text=\"' . $change_text . '\" data-cancel-text=\"' . $cancel_text . '\" href=\"#\">' . $change_text . '</a>';\n\n\t}\n\n\t/**\n\t * Modifies account form to improve the UX of editing the email address and password fields\n\t *\n\t * Adds a \"Current Password\" field used to verify the existing user password when changing passwords.\n\t *\n\t * Forces email & password fields to be required and makes them disabled and visually hidden on page load.\n\t *\n\t * Adds a toggle button for each set of fields, when the toggle is clicked the fields are revealed and enabled\n\t * so they can be used. Ensuring that the fields are only required when they're being explicitly changed.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $blocks   Array of parsed WP_Block arrays.\n\t * @param string  $location The form location ID.\n\t *\n\t * @return array[]\n\t */\n\tpublic function modify_account_form( $blocks, $location ) {\n\n\t\t// Only add toggles on the account edit form.\n\t\tif ( 'account' !== $location ) {\n\t\t\treturn $blocks;\n\t\t}\n\n\t\t$blocks = $this->modify_toggle_blocks( $blocks );\n\n\t\tforeach ( array( 'email_address', 'password' ) as $id ) {\n\t\t\t$field  = $this->find_block( $id, $blocks );\n\t\t\t$blocks = $field ? $this->{\"toggle_for_$id\"}( $field, $blocks ) : $blocks;\n\t\t}\n\n\t\treturn $blocks;\n\n\t}\n\n\t/**\n\t * Maybe add the required email and password block to a form.\n\t *\n\t * @since 5.1.0\n\t * @since 5.4.1 Make sure added reusable blocks contain the actual required field,\n\t *              otherwise fall back on the dynamically generated ones.\n\t *\n\t * @param array[] $blocks   Array of parsed WP_Block arrays.\n\t * @param string  $location The request form location ID.\n\t * @param array   $args     Additional arguments passed to the short-circuit filter.\n\t * @return array[]\n\t */\n\tpublic function maybe_add_required_block_fields( $blocks, $location, $args ) {\n\n\t\t$fields_to_require = $this->get_required_fields_for_location( $location, $args );\n\t\tif ( empty( $fields_to_require ) ) {\n\t\t\treturn $blocks;\n\t\t}\n\n\t\tforeach ( $fields_to_require as $field_id => $field_block_name ) {\n\n\t\t\t$block = $this->find_block( $field_id, $blocks );\n\n\t\t\tif ( ! empty( $block ) ) {\n\t\t\t\t// Fields in non checkout forms are always visible - see LLMS_Forms::get_form_html().\n\t\t\t\t$blocks = 'checkout' === $location ? $this->make_block_visible( $block[1], $blocks, $block[0] ) : $blocks;\n\t\t\t\tunset( $fields_to_require[ $field_id ] );\n\t\t\t\tif ( empty( $fields_to_require ) ) { // All the required blocks are present.\n\t\t\t\t\treturn $blocks;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $this->add_required_block_fields( $fields_to_require, $blocks, $location );\n\n\t}\n\n\t/**\n\t * Add required block fields.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @param string[] $fields_to_require Array of field ids to require.\n\t * @param array[]  $blocks            Array of parsed WP_Block arrays to add required fields to.\n\t * @param string   $location          The request form location ID.\n\t * @return array[]\n\t */\n\tprivate function add_required_block_fields( $fields_to_require, $blocks, $location ) {\n\n\t\t$blocks_to_add = array();\n\t\tforeach ( $fields_to_require as $field_id => $block_to_add ) {\n\n\t\t\t// If a reusable block exists for the field, use it. Otherwise use a dynamically generated block from the template schema.\n\t\t\t$use_reusable = LLMS_Form_Templates::find_reusable_block( $block_to_add );\n\t\t\t$block        = LLMS_Form_Templates::get_block( $block_to_add, $location, $use_reusable );\n\n\t\t\tif ( $use_reusable ) {\n\t\t\t\t// Load reusable block.\n\t\t\t\t$_blocks = LLMS_Forms::instance()->load_reusable_blocks( array( $block ) );\n\t\t\t\t// The reusable block doesn't contain the needed block, use a dynamically generated block from the template schema.\n\t\t\t\tif ( empty( $_blocks ) || ! $this->find_block( $field_id, $_blocks ) ) {\n\t\t\t\t\t$_blocks = array( LLMS_Form_Templates::get_block( $block_to_add, $location, false ) );\n\t\t\t\t}\n\t\t\t\t$block = $_blocks[0];\n\t\t\t}\n\n\t\t\t$blocks_to_add[] = $block;\n\t\t}\n\n\t\t// Make blocks to add visible.\n\t\t$blocks_to_add = 'checkout' === $location ? array_map( array( $this, 'make_all_visible' ), $blocks_to_add ) : $blocks_to_add;\n\n\t\treturn array_merge(\n\t\t\t$blocks,\n\t\t\t$blocks_to_add\n\t\t);\n\n\t}\n\n\t/**\n\t * Make a block visible within its list of blocks\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param array   $block       Parsed WP_Block array.\n\t * @param array[] $blocks      Array of parsed WP_Block arrays.\n\t * @param int     $block_index Index of the block within the `$blocks` list.\n\t *                             If the block is in a group, this is the the index of the item's parent.\n\t * @return array[]\n\t */\n\tprivate function make_block_visible( $block, $blocks, $block_index ) {\n\n\t\tif ( LLMS_Forms::instance()->is_block_visible_in_list( $block, array( $blocks[ $block_index ] ) ) ) {\n\t\t\treturn $blocks;\n\t\t}\n\n\t\t// If the block has a confirm group, use that.\n\t\t$confirm = $this->get_confirm_group( $block['attrs']['id'], array( $blocks[ $block_index ] ) );\n\n\t\t$block_to_add = empty( $confirm ) ? $block : $confirm;\n\n\t\t$replace = true;\n\t\t// Insert the visible block before the invisible one if the block is in a group,\n\t\t// so to avoid the replacement of the whole group which might contain other required fields.\n\t\t// But replace the invisible with the visible if otherwise.\n\t\tif ( $block_to_add !== $blocks[ $block_index ] ) {\n\t\t\t$replace = false;\n\t\t\t$this->remove_block( $block_to_add, $blocks );\n\t\t}\n\n\t\t// Make the block to add and its children visible.\n\t\t$block_to_add = $this->make_all_visible( $block_to_add );\n\n\t\tarray_splice( $blocks, $block_index, (int) ( ! empty( $replace ) ), array( $block_to_add ) );\n\n\t\treturn $blocks;\n\n\t}\n\n\t/**\n\t * Remove block from the list which contains it.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param array   $block  Parsed WP_Block array.\n\t * @param array[] $blocks Array of parsed WP_Block arrays (passed by reference).\n\t * @param array   $parent Optional. Parsed WP_Block array representing the parent block of the `$blocks`, in case this is a list of inner blocks. Default null.\n\t *                        Passed by reference.\n\t * @return bool\n\t */\n\tprivate function remove_block( $block, &$blocks, &$parent = null ) {\n\n\t\tforeach ( $blocks as $index => &$_block ) {\n\n\t\t\tif ( $_block === $block ) {\n\t\t\t\tarray_splice( $blocks, $index, 1 ); // Remove and re-index.\n\t\t\t\t// If we're removing an innerBlock we need to update the innerContent too, to avoid wp calling the render method on nulls.\n\t\t\t\tif ( ! is_null( $parent ) ) {\n\t\t\t\t\t$this->remove_inner_block_from_inner_content( $index, $parent );\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tif ( ! empty( $_block['innerBlocks'] ) ) {\n\t\t\t\t$removed = $this->remove_block( $block, $_block['innerBlocks'], $_block );\n\t\t\t}\n\t\t\tif ( ! empty( $removed ) ) { // Break as soon as the desired block is removed from one of the innerBlocks.\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Remove inner block reference from inner content\n\t *\n\t * See WP_Block::inner_content documentation.\n\t *\n\t * The inner_content block's property is an array of string fragments and null markers where inner blocks were found.\n\t * So here we cycle over the block's parent innerContent field looking for references to innerBlocks (null).\n\t * When we found a positional correspondance between the removed innerBlock and its refernce in innerContent we remove the latter too.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param int   $inner_block_index The index of the inner block in the block's innerBlocks list.\n\t * @param array $parent            Parsed WP_Block array representing the inner blocks parent. Passed by reference.\n\t */\n\tprivate function remove_inner_block_from_inner_content( $inner_block_index, &$parent ) {\n\n\t\t$inner_block_in_content_index = 0;\n\t\tforeach ( $parent['innerContent'] as $chunk_index => $chunk ) {\n\t\t\tif ( ! is_string( $chunk ) && $inner_block_index === $inner_block_in_content_index++ ) {\n\t\t\t\tarray_splice( $parent['innerContent'], $chunk_index, 1 ); // Remove and re-index.\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Make the block and its children visible\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param array $block A parsed WP_Block.\n\t * @return array\n\t */\n\tprivate function make_all_visible( $block ) {\n\n\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\t\t\tforeach ( $block['innerBlocks'] as $index => $inner_block ) {\n\t\t\t\t$block['innerBlocks'][ $index ] = $this->make_all_visible( $inner_block );\n\t\t\t}\n\t\t}\n\t\t$block['attrs']['llms_visibility'] = '';\n\n\t\treturn $block;\n\n\t}\n\n\t/**\n\t * Get confirm group in a list of blocks for a given block id\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param string  $id     The ID of the field to find the confirm group for.\n\t * @param array[] $blocks WP_Block list.\n\t * @return array\n\t */\n\tprivate function get_confirm_group( $id, $blocks ) {\n\n\t\tforeach ( $blocks as $index => $block ) {\n\n\t\t\tif ( $block['innerBlocks'] ) {\n\t\t\t\tif ( ( 'llms/form-field-confirm-group' === $block['blockName'] ) &&\n\t\t\t\t\t\t$this->find_block( $id, $block['innerBlocks'] ) ) {\n\t\t\t\t\treturn $block;\n\t\t\t\t}\n\t\t\t\t$inner = $this->get_confirm_group( $id, $block['innerBlocks'] );\n\t\t\t\tif ( false !== $inner ) {\n\t\t\t\t\treturn $inner;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Modifies block settings for toggle-controlled fields\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $blocks Array of WP_Block arrays.\n\t * @return array[]\n\t */\n\tprivate function modify_toggle_blocks( $blocks ) {\n\n\t\t// List of toggle fields to modify.\n\t\t$fields = array(\n\t\t\t'email_address',\n\t\t\t'email_address_confirm',\n\t\t\t'password',\n\t\t\t'password_confirm',\n\t\t);\n\n\t\tforeach ( $blocks as &$block ) {\n\n\t\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\t\t\t\t$block['innerBlocks'] = $this->modify_toggle_blocks( $block['innerBlocks'] );\n\t\t\t} elseif ( ! empty( $block['attrs']['id'] ) && in_array( $block['attrs']['id'], $fields, true ) ) {\n\t\t\t\t$block['attrs']['wrapper_classes'] = 'llms-visually-hidden-field';\n\t\t\t\t$block['attrs']['disabled']        = true;\n\t\t\t\t$block['attrs']['required']        = true;\n\t\t\t}\n\t\t}\n\n\t\treturn $blocks;\n\t}\n\n\t/**\n\t * Adds a toggle link button allowing the user to change their email address\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $email  Email field data as located by LLMS_Forms_Dynamic_Fields::find_block().\n\t * @param array[] $blocks Array of WP_Block arrays.\n\t * @return array[]\n\t */\n\tprivate function toggle_for_email_address( $email, $blocks ) {\n\n\t\treturn $this->add_block(\n\t\t\t$blocks,\n\t\t\tarray(\n\t\t\t\t'type'  => 'html',\n\t\t\t\t'id'    => 'llms-field-toggle--email',\n\t\t\t\t'value' => $this->get_toggle_button_html( '#email_address,#email_address_confirm', $email[1]['attrs']['label'] ),\n\t\t\t),\n\t\t\t$email[0]\n\t\t);\n\n\t}\n\n\n\t/**\n\t * Adds a current password field and a toggle link button allowing the user to change their password\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array   $password Password field data as located by LLMS_Forms_Dynamic_Fields::find_block().\n\t * @param array[] $blocks   Array of WP_Block arrays.\n\t * @return array[]\n\t */\n\tprivate function toggle_for_password( $password, $blocks ) {\n\n\t\t// Add the toggle button.\n\t\t$blocks = $this->add_block(\n\t\t\t$blocks,\n\t\t\tarray(\n\t\t\t\t'type'  => 'html',\n\t\t\t\t'id'    => 'llms-field-toggle--password',\n\t\t\t\t'value' => $this->get_toggle_button_html( '#password,#password_confirm,#llms-password-strength-meter,#password_current', $password[1]['attrs']['label'] ),\n\t\t\t),\n\t\t\t$password[1]['attrs']['meter'] ? $password[0] + 1 : $password[0]\n\t\t);\n\n\t\t/**\n\t\t * Filters the settings used to create the dynamic password strength meter block\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array $settings Array or block attributes/settings.\n\t\t */\n\t\t$current_password = apply_filters(\n\t\t\t'llms_current_password_field_settings',\n\t\t\tarray(\n\t\t\t\t'type'            => 'password',\n\t\t\t\t'id'              => 'password_current',\n\t\t\t\t'name'            => 'password_current',\n\t\t\t\t'label'           => sprintf( __( 'Current %s', 'lifterlms' ), $password[1]['attrs']['label'] ),\n\t\t\t\t'required'        => true,\n\t\t\t\t'disabled'        => true,\n\t\t\t\t'data_store_key'  => false,\n\t\t\t\t'wrapper_classes' => 'llms-visually-hidden-field',\n\t\t\t)\n\t\t);\n\t\treturn $this->add_block( $blocks, $current_password, $password[0] - 1 );\n\n\t}\n\n}\n\nreturn new LLMS_Forms_Dynamic_Fields();\n"
  },
  {
    "path": "includes/forms/class-llms-forms-unsupported-versions.php",
    "content": "<?php\n/**\n * LLMS_Forms_Unsupported_Versions file\n *\n * @package LifterLMS/Classes/Forms\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Handles admin interface changes when forms cannot be managed with the block editor\n *\n * The file, class, and all class methods will be removed without warning when the overall supported\n * WordPress version is 5.7. Class methods are public in order to function within the WordPress API\n * but should be considered private for this reason.\n *\n * @since 5.0.0\n *\n * @access private\n */\nclass LLMS_Forms_Unsupported_Versions {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @access private\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tif ( LLMS_Forms::instance()->are_requirements_met() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tadd_action( 'current_screen', array( $this, 'init' ) );\n\t}\n\n\t/**\n\t * Add actions depending on the current screen\n\t *\n\t * @since 5.0.0\n\t *\n\t * @access private\n\t *\n\t * @return void\n\t */\n\tpublic function init() {\n\n\t\t$screen = get_current_screen();\n\n\t\tif ( 'edit-llms_form' === $screen->id ) {\n\n\t\t\tadd_action( 'admin_print_styles', array( $this, 'print_styles' ) );\n\t\t\tadd_action( 'admin_notices', array( $this, 'output_notice' ) );\n\n\t\t} elseif ( 'llms_form' === $screen->id ) {\n\n\t\t\tllms_redirect_and_exit( admin_url( 'edit.php?post_type=llms_form' ) );\n\n\t\t}\n\t}\n\n\t/**\n\t * Output an admin error notice alerting users when requirements are not met.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @access private\n\t *\n\t * @return void\n\t */\n\tpublic function output_notice() {\n\t\t?>\n\t\t<div class=\"notice notice-error\">\n\t\t\t<p><b><?php esc_html_e( 'Minimum Version Requirements Error', 'lifterlms' ); ?></b></p>\n\t\t\t<p><?php printf( esc_html__( 'In order to manage LifterLMS Forms you must upgrade to at least WordPress version %s or later or install the latest version of the Gutenberg plugin.', 'lifterlms' ), esc_html( LLMS_Forms::instance()::MIN_WP_VERSION ) ); ?></p>\n\t\t\t<p><?php esc_html_e( 'If you do not upgrade, your forms will display properly on the frontend and users will be able to create accounts, enroll, and checkout but you will be unable to customize them.', 'lifterlms' ); ?></p>\n\t\t</div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Output dirty inline CSS to prevent interaction with the posts table list\n\t *\n\t * @since 5.0.0\n\t *\n\t * @access private\n\t *\n\t * @return void\n\t */\n\tpublic function print_styles() {\n\t\techo '<style type=\"text/css\" id=\"llms-forms-unsupported-styles\">#the-list { pointer-events: none; filter: blur( 1px ); }</style>';\n\t}\n}\n\nreturn new LLMS_Forms_Unsupported_Versions();\n"
  },
  {
    "path": "includes/forms/class-llms-forms.php",
    "content": "<?php\n/**\n * Register and manage LifterLMS user forms.\n *\n * @package LifterLMS/Classes\n *\n * @since 5.0.0\n * @version 7.1.4\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Forms class\n *\n * @since 5.0.0\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n */\nclass LLMS_Forms {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Minimum Supported WP Version required to manage forms with the block editor UI.\n\t */\n\tconst MIN_WP_VERSION = '5.7.0';\n\n\t/**\n\t * Provide access to the post type manager class\n\t *\n\t * @var LLMS_Forms_Post_Type\n\t */\n\tpublic $post_type_manager = null;\n\n\t/**\n\t * Private Constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t$this->post_type_manager = new LLMS_Form_Post_Type( $this );\n\n\t\tadd_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 );\n\t\tadd_filter( 'llms_get_form_post', array( $this, 'maybe_load_preview' ) );\n\t}\n\n\t/**\n\t * Determines if the WP core requirements are met\n\t *\n\t * This is used to determine if the block editor can be used to manage forms and fields,\n\t * all frontend and server-side handling works on all core supported WP versions.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function are_requirements_met() {\n\t\tglobal $wp_version;\n\t\treturn version_compare( $wp_version, self::MIN_WP_VERSION, '>=' ) || is_plugin_active( 'gutenberg/gutenberg.php' );\n\t}\n\n\t/**\n\t * Determine if usernames are enabled on the site.\n\t *\n\t * This method is used to determine if a username can be used to login / reset a user's password.\n\t *\n\t * A reference to every form with a username block is stored in an option. The option is an array\n\t * of integers, the WP_Post IDs of all the form posts containing a username block.\n\t *\n\t * If the array is empty, there are no forms with username blocks and, therefore, usernames are disabled.\n\t * If the array contains at least one item that means there is a form with a username block in it and,\n\t * we therefore consider usernames to be enabled for the site.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return bool\n\t */\n\tpublic function are_usernames_enabled() {\n\n\t\t$locations = get_option( 'llms_forms_username_locations', array() );\n\n\t\t/**\n\t\t * Use this to explicitly enable of disable username fields.\n\t\t *\n\t\t * Note that usage of this filter will not actually disable the llms/form-field-username block.\n\t\t * It's possible to create a confusing user experience by explicitly disabling usernames and\n\t\t * leaving username field blocks on one or more forms. If you decide to explicitly disable via\n\t\t * this filter you should also remove all the username blocks from all of your forms.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param boolean $enabled Whether or not usernames are enabled.\n\t\t */\n\t\treturn apply_filters( 'llms_are_usernames_enabled', ! empty( $locations ) );\n\t}\n\n\t/**\n\t * Converts a block to settings understandable by `llms_form_field()`\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Added logic to remove invisible fields.\n\t *              Added `$block_list` param.\n\t *\n\t * @param array   $block      A WP Block array.\n\t * @param array[] $block_list Optional. The list of WP Block array `$block` comes from. Default is empty array.\n\t * @return array\n\t */\n\tprivate function block_to_field_settings( $block, $block_list = array() ) {\n\n\t\t$is_visible = $this->is_block_visible_in_list( $block, $block_list );\n\n\t\t/**\n\t\t * Filters whether or not invisible fields should be included\n\t\t *\n\t\t * If the block is not visible (according to LLMS block-level visibility settings)\n\t\t * it will return an empty array (signaling the field to be removed).\n\t\t *\n\t\t * @since 5.1.0\n\t\t *\n\t\t * @param boolean $filter     Whether or not invisible fields should be included. Default is `false`.\n\t\t * @param array   $block      A WP Block array.\n\t\t * @param array[] $block_list The list of WP Block array `$block` comes from.\n\t\t */\n\t\tif ( ! $is_visible && apply_filters( 'llms_forms_remove_invisible_field', false, $block, $block_list ) ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t$attrs = $this->convert_settings_format( $block['attrs'], 'block' );\n\n\t\t// If the field is required and hidden it's impossible for the user to fill it out so it gets marked as optional at runtime.\n\t\tif ( ! empty( $attrs['required'] ) && ! $is_visible ) {\n\t\t\t$attrs['required'] = false;\n\t\t}\n\n\t\t/**\n\t\t * Filter an LLMS_Form_Field settings array after conversion from a field block\n\t\t *\n\t\t * @since 5.0.0\n\t\t * @since 5.1.0 Added `$block_list` param.\n\t\t *\n\t\t * @param array   $attrs      An array of LLMS_Form_Field settings.\n\t\t * @param array   $block      A WP Block array.\n\t\t * @param array[] $block_list The list of WP Block array `$block` comes from.\n\t\t */\n\t\treturn apply_filters( 'llms_forms_block_to_field_settings', $attrs, $block, $block_list );\n\t}\n\n\t/**\n\t * Cascade all llms_visibility attributes down into inner blocks.\n\t *\n\t * If a parent block has a visibility setting this will apply that visibility to a chlid block *if*\n\t * the child block does not have a visibility setting of its own.\n\t *\n\t * Ultimately this ensures that a field block that's not visible can be marked as \"optional\" so that\n\t * form validation can take place.\n\t *\n\t * For example, if a columns block is displayed only to logged out users and it's child fields are marked\n\t * as required that means that it's required only to logged out users and the field becomes \"optional\"\n\t * (for validation purposes) to logged in users.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[]     $blocks     Array of parsed block arrays.\n\t * @param string|null $visibility The llms_visibility attribute of the parent block which is applied to all innerBlocks\n\t *                                if the innerBlock does not already have it's own visibility attribute.\n\t * @return array[]\n\t */\n\tprivate function cascade_visibility_attrs( $blocks, $visibility = null ) {\n\n\t\tforeach ( $blocks as &$block ) {\n\n\t\t\t// If a visibility setting has been passed from the parent and the block does not have visibility setting of it's own.\n\t\t\tif ( $visibility && ( empty( $block['attrs']['llms_visibility'] ) || 'off' === $block['attrs']['llms_visibility'] ) ) {\n\t\t\t\t$block['attrs']['llms_visibility'] = $visibility;\n\t\t\t}\n\n\t\t\t// This block has a visibility attribute and it should be applied it to all the innerBlocks.\n\t\t\tif ( ! empty( $block['attrs']['llms_visibility'] ) && ! empty( $block['innerBlocks'] ) ) {\n\t\t\t\t$block['innerBlocks'] = $this->cascade_visibility_attrs( $block['innerBlocks'], $block['attrs']['llms_visibility'] );\n\t\t\t}\n\t\t}\n\n\t\treturn $blocks;\n\t}\n\n\t/**\n\t * Converts field settings formats\n\t *\n\t * There are small differences between the LLMS_Form_Fields settings array\n\t * and the WP_Block settings array.\n\t *\n\t * This method accepts an associative array\n\t * in one format or the other and converts it from the original format to the opposite format.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array  $map            Associative array of settings.\n\t * @param string $orignal_format The original format of the submitted `$map`. Either \"field\" for\n\t *                               an array of LLMS_Form_Field settings or `block` for an array\n\t *                               of WP_Block attributes.\n\t * @return [type] [description]\n\t */\n\tprivate function convert_settings_format( $map, $orignal_format ) {\n\n\t\t// Block attributes to LLMS_Form_Field settings.\n\t\t$keys = array(\n\t\t\t'field'      => 'type',\n\t\t\t'className'  => 'classes',\n\t\t\t'html_attrs' => 'attributes',\n\t\t);\n\n\t\t// LLMS_Form_Field settings to block attributes.\n\t\tif ( 'field' === $orignal_format ) {\n\t\t\t$keys = array_flip( $keys );\n\t\t}\n\n\t\t// Loop through the original map and rename the necessary keys.\n\t\tforeach ( $keys as $orig_key => $new_key ) {\n\t\t\tif ( isset( $map[ $orig_key ] ) ) {\n\t\t\t\t$map[ $new_key ] = $map[ $orig_key ];\n\t\t\t\tunset( $map[ $orig_key ] );\n\t\t\t}\n\t\t}\n\n\t\treturn $map;\n\t}\n\n\t/**\n\t * Converts an array of LLMS_Form_Field settings to a block attributes array\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $settings An array of LLMS_Form_Field settings.\n\t * @return array An array of WP_Block attributes.\n\t */\n\tpublic function convert_settings_to_block_attrs( $settings ) {\n\t\treturn $this->convert_settings_format( $settings, 'field' );\n\t}\n\n\t/**\n\t * Create a form for a given location with the provided data.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location_id Location id.\n\t * @param bool   $recreate    If `true` and the form already exists, will recreate the existing form using the existing form's id.\n\t * @return int|false Returns the created/update form post ID on success.\n\t *                   If the location doesn't exist, returns `false`.\n\t *                   If the form already exists and `$recreate` is `false` will return `false`.\n\t */\n\tpublic function create( $location_id, $recreate = false ) {\n\n\t\tif ( ! $this->is_location_valid( $location_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$locs = $this->get_locations();\n\t\t$data = $locs[ $location_id ];\n\n\t\t$existing = $this->get_form_post( $location_id );\n\n\t\t// Form already exists and we haven't requested an update.\n\t\tif ( false !== $existing && ! $recreate ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$args = array(\n\t\t\t'ID'           => $existing ? $existing->ID : 0,\n\t\t\t'post_content' => LLMS_Form_Templates::get_template( $location_id ),\n\t\t\t'post_status'  => 'publish',\n\t\t\t'post_title'   => $data['title'],\n\t\t\t'post_type'    => $this->get_post_type(),\n\t\t\t'meta_input'   => $data['meta'],\n\t\t\t'post_author'  => $existing ? $existing->post_author : LLMS_Install::get_can_install_user_id(),\n\t\t);\n\n\t\t/**\n\t\t * Filter arguments used to install a new form.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array  $args        Array of arguments to be passed to wp_insert_post\n\t\t * @param string $location_id Location ID/name.\n\t\t * @param array  $data        Array of location information from LLMS_Forms::get_locations().\n\t\t */\n\t\t$args = apply_filters( 'llms_forms_install_post_args', $args, $location_id, $data );\n\n\t\treturn wp_insert_post( $args );\n\t}\n\n\t/**\n\t * Retrieve the form management user capability.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_capability() {\n\t\treturn $this->post_type_manager->capability;\n\t}\n\n\t/**\n\t * Pull LifterLMS Form Field blocks from an array of parsed WP Blocks.\n\t *\n\t * Searches innerBlocks arrays recursively.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 First check block's innerBlock attribute exists when checking for inner blocks.\n\t *              Also made the access visibility public.\n\t * @since 5.9.0 Pass an empty string to `strpos()` instead of `null`.\n\t *\n\t * @param array $blocks Array of WP Block arrays from `parse_blocks()`.\n\t * @return array\n\t */\n\tpublic function get_field_blocks( $blocks ) {\n\n\t\t$fields = array();\n\n\t\tforeach ( $blocks as $block ) {\n\n\t\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\t\t\t\t$fields = array_merge( $fields, $this->get_field_blocks( $block['innerBlocks'] ) );\n\t\t\t} elseif ( false !== strpos( $block['blockName'] ?? '', 'llms/form-field-' ) ) {\n\t\t\t\t$fields[] = $block;\n\t\t\t} elseif ( 'core/html' === $block['blockName'] && ! empty( $block['attrs']['type'] ) ) {\n\t\t\t\t$fields[] = $block;\n\t\t\t}\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Returns a list of field names used by LifterLMS forms\n\t *\n\t * Used to validate uniqueness of custom field data.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string[]\n\t */\n\tpublic function get_field_names() {\n\n\t\t$names = array(\n\t\t\t'user_login',\n\t\t\t'user_login_confirm',\n\t\t\t'email_address',\n\t\t\t'email_address_confirm',\n\t\t\t'password',\n\t\t\t'password_confirm',\n\t\t\t'first_name',\n\t\t\t'last_name',\n\t\t\t'display_name',\n\t\t\t'llms_billing_address_1',\n\t\t\t'llms_billing_address_2',\n\t\t\t'llms_billing_city',\n\t\t\t'llms_billing_country',\n\t\t\t'llms_billing_state',\n\t\t\t'llms_billing_zip',\n\t\t\t'llms_phone',\n\t\t);\n\n\t\t/**\n\t\t * Filters the list of field names used by LifterLMS forms\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string[] $names List of registered field names.\n\t\t */\n\t\treturn apply_filters( 'llms_forms_field_names', $names );\n\t}\n\n\t/**\n\t * Retrieve an array of parsed blocks for the form at a given location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t * @param array  $args     Additional arguments passed to the short-circuit filter.\n\t * @return array|false\n\t */\n\tpublic function get_form_blocks( $location, $args = array() ) {\n\n\t\t$post = $this->get_form_post( $location, $args );\n\t\tif ( ! $post ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$content  = $post->post_content;\n\t\t$content .= $this->get_additional_fields_html( $location, $args );\n\n\t\t$blocks = $this->parse_blocks( $content );\n\n\t\t/**\n\t\t * Filters the parsed block list for a given LifterLMS form\n\t\t *\n\t\t * This hook can be used to programmatically modify, insert, or remove\n\t\t * blocks (fields) from a form.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array[] $blocks   Array of parsed WP_Block arrays.\n\t\t * @param string  $location The request form location ID.\n\t\t * @param array   $args     Additional arguments passed to the short-circuit filter.\n\t\t */\n\t\treturn apply_filters( 'llms_get_form_blocks', $blocks, $location, $args );\n\t}\n\n\t/**\n\t * Retrieve an array of LLMS_Form_Fields settings arrays for the form at a given location.\n\t *\n\t * This method is used by the LLMS_Form_Handler to perform validations on user-submitted data.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t * @param array  $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.\n\t * @return false|array\n\t */\n\tpublic function get_form_fields( $location, $args = array() ) {\n\n\t\t$blocks = $this->get_form_blocks( $location, $args );\n\n\t\tif ( false === $blocks ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$fields = $this->get_fields_settings_from_blocks( $blocks );\n\n\t\t/**\n\t\t * Modify the parsed array of LifterLMS Form Fields\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array[] $fields   Array of LifterLMS Form Field settings data.\n\t\t * @param string  $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t\t * @param array   $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.\n\t\t */\n\t\treturn apply_filters( 'llms_get_form_fields', $fields, $location, $args );\n\t}\n\n\t/**\n\t * Retrieve an array of LLMS_Form_Field settings from an array of blocks.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Pass the whole list of blocks to the `$this->block_to_field_settings()` method\n\t *              to better check whether a block is visible.\n\t * @since 6.2.0 Exploded hidden checkbox fields.\n\t *\n\t * @param array $blocks Array of WP Block arrays from `parse_blocks()`.\n\t * @return array\n\t */\n\tpublic function get_fields_settings_from_blocks( $blocks ) {\n\n\t\t$fields = array();\n\t\t$blocks = $this->get_field_blocks( $blocks );\n\n\t\tforeach ( $blocks as $block ) {\n\t\t\t$settings = $this->block_to_field_settings( $block, $blocks );\n\n\t\t\tif ( empty( $settings ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (\n\t\t\t\t'hidden' === ( $settings['type'] ?? null ) &&\n\t\t\t\tisset( $block['attrs']['field'] ) && 'checkbox' === $block['attrs']['field']\n\t\t\t) {\n\t\t\t\t// Convert hidden checkbox settings into multiple \"checked\" hidden fields.\n\t\t\t\t$settings['type'] = $block['attrs']['field'];\n\t\t\t\t$field            = new LLMS_Form_Field( $settings );\n\t\t\t\t$form_fields      = $field->explode_options_to_fields( true );\n\t\t\t\tforeach ( $form_fields as $form_field ) {\n\t\t\t\t\t$fields[] = $form_field->get_settings();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t$field    = new LLMS_Form_Field( $settings );\n\t\t\t\t$fields[] = $field->get_settings();\n\t\t\t}\n\t\t}\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Retrieve a field item from a list of fields by a key/value pair.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $fields List of LifterLMS Form Fields.\n\t * @param string  $key    Setting key to search for.\n\t * @param mixed   $val    Setting valued to search for.\n\t * @param string  $return Determine the return value. Use \"field\" to return the field settings\n\t *                        array. Use \"index\" to return the index of the field in the $fields array.\n\t * @return array|int|false `false` when the field isn't found in $fields, otherwise returns the field settings\n\t *                          as an array when `$return` is \"field\". Otherwise returns the field's index as an int.\n\t */\n\tpublic function get_field_by( $fields, $key, $val, $return = 'field' ) {\n\n\t\tforeach ( $fields as $index => $field ) {\n\t\t\tif ( isset( $field[ $key ] ) && $val === $field[ $key ] ) {\n\t\t\t\treturn 'field' === $return ? $field : $index;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve the rendered HTML for the form at a given location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t * @param array  $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.\n\t * @return string\n\t */\n\tpublic function get_form_html( $location, $args = array() ) {\n\n\t\t$blocks = $this->get_form_blocks( $location, $args );\n\t\tif ( ! $blocks ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$disable_visibility = ( 'checkout' !== $location );\n\n\t\t// Force fields to display regardless of visibility settings when viewing account/registration forms.\n\t\tif ( $disable_visibility ) {\n\t\t\tadd_filter( 'llms_blocks_visibility_should_filter_block', '__return_false', 999 );\n\t\t}\n\n\t\t$html = '';\n\t\tforeach ( $blocks as $block ) {\n\t\t\t$html .= render_block( $block );\n\t\t}\n\n\t\tif ( $disable_visibility ) {\n\t\t\tremove_filter( 'llms_blocks_visibility_should_filter_block', '__return_false', 999 );\n\t\t}\n\n\t\t/**\n\t\t * Modify the parsed array of LifterLMS Form Fields.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string $html     Form fields HTML.\n\t\t * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t\t * @param array  $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.\n\t\t */\n\t\treturn apply_filters( 'llms_get_form_html', $html, $location, $args );\n\t}\n\n\t/**\n\t * Retrieve the WP Post for the form at a given location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t * @param array  $args     Additional arguments passed to the short-circuit filter.\n\t * @return WP_Post|false\n\t */\n\tpublic function get_form_post( $location, $args = array() ) {\n\n\t\t// @todo Add caching. This runs twice on some page loads.\n\n\t\t/**\n\t\t * Skip core lookup of the form for the request location and return a custom form post.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param null|WP_Post $post     Return a WP_Post object to short-circuit default lookup query.\n\t\t * @param string       $location Form location. Either \"checkout\", \"registration\", or \"account\".\n\t\t * @param array        $args     Additional custom arguments.\n\t\t */\n\t\t$post = apply_filters( 'llms_get_form_post_pre_query', null, $location, $args );\n\t\tif ( is_a( $post, 'WP_Post' ) ) {\n\t\t\treturn $post;\n\t\t}\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_type'      => $this->get_post_type(),\n\t\t\t\t'posts_per_page' => 1,\n\t\t\t\t'no_found_rows'  => true,\n\t\t\t\t// Only show published forms to end users but allow admins to \"preview\" drafts.\n\t\t\t\t'post_status'    => current_user_can( $this->get_capability() ) ? array( 'publish', 'draft' ) : 'publish',\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\t'relation' => 'AND',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_form_location',\n\t\t\t\t\t\t'value' => $location,\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_form_is_core',\n\t\t\t\t\t\t'value' => 'yes',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\t$post = $query->have_posts() ? $query->posts[0] : false;\n\n\t\t/**\n\t\t * Filters the returned `llms_form` post object\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param WP_Post|boolean $post     The post object of the form or `false` if no form could be located.\n\t\t * @param string       $location Form location. Either \"checkout\", \"registration\", or \"account\".\n\t\t * @param array        $args     Additional custom arguments.\n\t\t */\n\t\treturn apply_filters( 'llms_get_form_post', $post, $location, $args );\n\t}\n\n\t/**\n\t * Check whether a given form is a core form.\n\t *\n\t * When there are multiple forms for a location, the core form is identified as the one with the lowest ID.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @param WP_Post|int $form Form's WP_Post instance, or its ID.\n\t * @return boolean\n\t */\n\tpublic function is_a_core_form( $form ) {\n\n\t\t$form_id = $form instanceof WP_Post ? $form->ID : $form;\n\n\t\tif ( ! $form_id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn in_array( $form_id, $this->get_core_forms( 'ids' ), true );\n\t}\n\n\t/**\n\t * Retrieves only core forms.\n\t *\n\t * When there are multiple forms for a location, the core form is identified as the one with the lowest ID.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @param string $return What to return: 'posts', for an array of WP_Post; 'ids' for an array of WP_Post ids.\n\t * @return WP_Post[]|int[]\n\t */\n\tprivate function get_core_forms( $return = 'posts', $use_cache = true ) {\n\n\t\tglobal $wpdb;\n\n\t\t$forms_cache_key = 'posts' === $return ? 'llms_core_forms' : 'llms_core_form_ids';\n\t\t$forms           = $use_cache ? wp_cache_get( $forms_cache_key ) : false;\n\n\t\tif ( false !== $forms ) {\n\t\t\treturn $forms;\n\t\t}\n\n\t\t$locations              = array_keys( $this->get_locations() );\n\t\t$locations_placeholders = implode( ',', array_fill( 0, count( $locations ), '%s' ) );\n\t\t$prepare_values         = array_merge( array( $this->get_post_type() ), $locations );\n\n\t\t$query = \"\nSELECT MIN({$wpdb->posts}.ID) AS ID\nFROM $wpdb->posts\nINNER JOIN {$wpdb->postmeta} AS locations ON {$wpdb->posts}.ID = locations.post_id AND locations.meta_key='_llms_form_location'\nINNER JOIN {$wpdb->postmeta} AS is_cores ON {$wpdb->posts}.ID = is_cores.post_id AND is_cores.meta_key='_llms_form_is_core'\nWHERE {$wpdb->posts}.post_type = %s\nAND locations.meta_value IN ({$locations_placeholders})\nAND is_cores.meta_value = 'yes'\nGROUP BY locations.meta_value\";\n\n\t\t$form_ids = $wpdb->get_col(\n\t\t\t$wpdb->prepare(\n\t\t\t\t$query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is prepared.\n\t\t\t\t$prepare_values\n\t\t\t)\n\t\t);\n\n\t\t$form_ids = array_map( 'absint', $form_ids );\n\t\t$forms    = 'post' === $return ? array_map( 'get_post', $form_ids ) : $form_ids;\n\n\t\twp_cache_set( $forms_cache_key, $forms );\n\n\t\treturn $forms;\n\t}\n\n\n\t/**\n\t * Retrieve additional fields added to the form programmatically.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t * @param array  $args     Additional arguments passed to the short-circuit filter.\n\t * @return array[]\n\t */\n\tprivate function get_additional_fields( $location, $args = array() ) {\n\n\t\t/**\n\t\t * Filter to add custom fields to a form programmatically.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.0.0 Moved from deprecated function `LLMS_Person_Handler::get_available_fields()`.\n\t\t *\n\t\t * @param array[] $fields   Array of field array suitable to pass to `llms_form_field()`.\n\t\t * @param string  $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t\t * @param array   $args     Additional arguments passed to the short-circuit filter.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_get_person_fields', array(), $location, $args );\n\t}\n\n\t/**\n\t * Retrieve HTML for the form's additional programmatically-added fields.\n\t *\n\t * Gets the HTML for each field from `llms_form_field()` and wraps it as a `wp/html` block.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n\t * @param array  $args     Additional arguments passed to the short-circuit filter.\n\t * @return string\n\t */\n\tprivate function get_additional_fields_html( $location, $args = array() ) {\n\n\t\t$html   = '';\n\t\t$fields = $this->get_additional_fields( $location, $args );\n\n\t\tforeach ( $fields as $field ) {\n\t\t\t$html .= \"\\r\" . $this->get_custom_field_block_markup( $field );\n\t\t}\n\n\t\treturn $html;\n\t}\n\n\t/**\n\t * Retrieve the HTML markup for a custom form field block\n\t *\n\t * Retrieves an array of `LLMS_Form_Field` settings, generates the HTML\n\t * for the field, and wraps it in a `wp:html` block.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $settings Form field settings (passed to `llms_form_field()`).\n\t * @return string\n\t */\n\tpublic function get_custom_field_block_markup( $settings ) {\n\t\treturn sprintf( '<!-- wp:html %1$s -->%2$s%3$s%2$s<!-- /wp:html -->', wp_json_encode( $settings ), \"\\r\", llms_form_field( $settings, false ) );\n\t}\n\n\t/**\n\t * Retrieve an array of form fields used for the \"free enrollment\" form\n\t *\n\t * This is the \"one-click\" enrollment form used when a logged-in user clicks the \"checkout\" button\n\t * from an access plan.\n\t *\n\t * This function converts the checkout form to hidden fields, the result is that users with all required fields\n\t * will be enrolled into the course with a single click (no need to head to the checkout page) and users\n\t * who are missing required information will be directed to the checkout page.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Specifiy to pass the new 3rd param to the `llms_forms_block_to_field_settings` filter callback.\n\t * @since 5.9.0 Fix php 8.1 deprecation warnings when `get_form_fields()` returns `false`.\n\t * @since 7.0.0 Retrieve and use the free checkout redirect URL as not encoded.\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan being used for enrollment.\n\t * @return array[] List of LLMS_Form_Field settings arrays.\n\t */\n\tpublic function get_free_enroll_form_fields( $plan ) {\n\n\t\t// Convert all fields to hidden fields and remove any fields hidden by LLMS block-level visibility settings.\n\t\tadd_filter( 'llms_forms_block_to_field_settings', array( $this, 'prepare_field_for_free_enroll_form' ), 999, 3 );\n\t\t$fields = $this->get_form_fields( 'checkout', compact( 'plan' ) );\n\t\tremove_filter( 'llms_forms_block_to_field_settings', array( $this, 'prepare_field_for_free_enroll_form' ), 999, 3 );\n\n\t\t// If no fields are found, ensure we add to an array instead of casting false to an array (causing a PHP 8.1 deprecation warning).\n\t\t$fields = ! is_array( $fields ) ? array() : $fields;\n\n\t\t// Add additional fields required for form processing.\n\t\t$fields[] = array(\n\t\t\t'name'           => 'free_checkout_redirect',\n\t\t\t'type'           => 'hidden',\n\t\t\t'value'          => $plan->get_redirection_url( false ),\n\t\t\t'data_store_key' => false,\n\t\t);\n\n\t\t$fields[] = array(\n\t\t\t'id'             => 'llms-plan-id',\n\t\t\t'name'           => 'llms_plan_id',\n\t\t\t'type'           => 'hidden',\n\t\t\t'value'          => $plan->get( 'id' ),\n\t\t\t'data_store_key' => false,\n\t\t);\n\n\t\t/**\n\t\t * Filter the list of LLMS_Form_Fields used to generate the \"free enrollment\" form\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param array[]          $fields List of LLMS_Form_Field settings arrays.\n\t\t * @param LLMS_Access_Plan $plan   Access plan being used for enrollment.\n\t\t */\n\t\treturn apply_filters( 'llms_forms_get_free_enroll_form_fields', $fields, $plan );\n\t}\n\n\t/**\n\t * Retrieve the HTML of form fields used for the \"free enrollment\" form\n\t *\n\t * @since 5.0.0\n\t *\n\t * @see LLMS_Forms::get_free_enroll_form_fields()\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan being used for enrollment.\n\t * @return string\n\t */\n\tpublic function get_free_enroll_form_html( $plan ) {\n\n\t\t$html = '';\n\t\tforeach ( $this->get_free_enroll_form_fields( $plan ) as $field ) {\n\t\t\t$html .= llms_form_field( $field, false );\n\t\t}\n\n\t\treturn $html;\n\t}\n\n\t/**\n\t * Retrieve information on all the available form locations.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array[] {\n\t *     An associative array. The array key is the location ID and each array is a location definition array.\n\t *\n\t *     @type string  $name        The human-readable location name (as displayed on the admin panel).\n\t *     @type string  $description A description of the form (as displayed on the admin panel).\n\t *     @type string  $title       The form's post title. This is displayed to the end user when the \"Show Form Title\" option is enabled.\n\t *     @type array   $meta        An associative array of postmeta information for the form. The array key is the meta key and the value is the meta value.\n\t *     @type string  $template    A string used to generate the post content of the form post, usually retrieve from `LLMS_Form_Templates`.\n\t *     @type array   $meta        Array of meta data used when generating the form. The array key is the meta key and array value is the meta value.\n\t *     @type array[] $required    Array of arrays defining required fields for each form.\n\t * }\n\t */\n\tpublic function get_locations() {\n\n\t\t$locations = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-form-locations.php';\n\n\t\t/**\n\t\t * Filter the available form locations.\n\t\t *\n\t\t * NOTE: Removing core forms (as well as modifying the ids / keys) may cause areas of LifterLMS to stop working.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param  array[] $locations Associative array of form location information.\n\t\t */\n\t\treturn apply_filters( 'llms_forms_get_locations', $locations );\n\t}\n\n\t/**\n\t * Retrieve the forms post type name.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_post_type() {\n\t\treturn $this->post_type_manager->post_type;\n\t}\n\n\t/**\n\t * Determine if a block is visible based on LifterLMS Visibility Settings.\n\t *\n\t * @since 5.0.0\n\t * @since 7.1.4 Fixed an issue running unit tests on PHP 7.4 and WordPress 6.2\n\t *              expecting `render_block()` returning a string while we were applying a filter\n\t *              that returned the boolean `true`.\n\t *\n\t * @param array $block Parsed block array.\n\t * @return bool\n\t */\n\tprivate function is_block_visible( $block ) {\n\n\t\t// Make the block return a non empty string if it's visible, it will already automatically return an empty string if it's invisible.\n\t\tadd_filter( 'render_block', array( __CLASS__, '__return_string' ), 5 );\n\n\t\t// Don't run this class render function on the block during this test.\n\t\tremove_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 );\n\n\t\t// Render the block.\n\t\t$render = render_block( $block );\n\n\t\t// Cleanup / reapply filters.\n\t\tadd_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 );\n\t\tremove_filter( 'render_block', array( __CLASS__, '__return_string' ), 5 );\n\n\t\t/**\n\t\t * Filter whether or not the block is visible.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param bool  $visible Whether or not the block is visible.\n\t\t * @param array $block   Parsed block array.\n\t\t */\n\t\treturn apply_filters( 'llms_forms_is_block_visible', llms_parse_bool( $render ), $block );\n\t}\n\n\t/**\n\t * Determine if a block is visible in the list it's contained based on LifterLMS Visibility Settings\n\t *\n\t * Fall back on `$this->is_block_visible()` if empty `$block_list` is provided.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param array   $block      Parsed block array.\n\t * @param array[] $block_list The list of WP Block array `$block` comes from.\n\t * @return bool Returns `true` if `$block` (and all its parents) are visible. Returns `false` when `$block`\n\t *              or any of its parents are hidden or when `$block` is not found within `$block_list`.\n\t */\n\tpublic function is_block_visible_in_list( $block, $block_list ) {\n\n\t\tif ( empty( $block_list ) ) {\n\t\t\treturn $this->is_block_visible( $block );\n\t\t}\n\n\t\t$path       = $this->get_block_path( $block, $block_list );\n\t\t$is_visible = ! empty( $path ); // Assume the block is visible until proven hidden, except when path is empty.\n\t\tforeach ( $path as $block ) {\n\t\t\tif ( ! $this->is_block_visible( $block ) ) {\n\t\t\t\t$is_visible = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter whether or not the block is visible in the list of blocks it's contained.\n\t\t *\n\t\t * @since 5.1.0\n\t\t *\n\t\t * @param bool    $is_visible Whether or not the block is visible.\n\t\t * @param array   $block      Parsed block array.\n\t\t * @param array[] $block_list The list of WP Block array `$block` comes from.\n\t\t */\n\t\treturn apply_filters( 'llms_forms_is_block_visible', $is_visible, $block, $block_list );\n\t}\n\n\t/**\n\t * Returns a list of block parents plus the block itself in reverse order\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param array   $block      Parsed block array.\n\t * @param array[] $block_list The list of WP Block array `$block` comes from.\n\t * @param int     $iterations Stores the number of iterations.\n\t * @return array[] List of WP_Block arrays or an empty array if `$block` cannot be found within `$block_list`.\n\t */\n\tprivate function get_block_path( $block, $block_list, $iterations = 0 ) {\n\n\t\tforeach ( $block_list as $_block ) {\n\n\t\t\t// Found the block.\n\t\t\tif ( $block === $_block ) {\n\t\t\t\treturn array( $block );\n\t\t\t}\n\n\t\t\t// No innerblocks, proceed to the next block.\n\t\t\tif ( empty( $_block['innerBlocks'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Look in innerblocks for the block.\n\t\t\tforeach ( $_block['innerBlocks'] as $inner_block ) {\n\n\t\t\t\t// The inner block needs to be merged to the path.\n\t\t\t\t$to_merge = array( $inner_block );\n\n\t\t\t\tif ( $block === $inner_block ) { // Inner block is the one we're looking for.\n\t\t\t\t\t$path     = array( $block );\n\t\t\t\t\t$to_merge = array(); // Inner block equals the path, no need to merge it.\n\t\t\t\t} else {\n\t\t\t\t\t$path = $this->get_block_path( $block, array( $inner_block ), $iterations + 1 );\n\t\t\t\t}\n\n\t\t\t\tif ( $path ) {\n\n\t\t\t\t\t// First iteration, append first block too.\n\t\t\t\t\tif ( ! $iterations ) {\n\t\t\t\t\t\t$to_merge[] = $_block;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Merge.\n\t\t\t\t\treturn array_merge( $path, $to_merge );\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Block not found in the list.\n\t\treturn array();\n\t}\n\n\t/**\n\t * Returns a filtered version of `$block_list` containing only the passed `$block` and its parents.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param array   $block      Parsed block array.\n\t * @param array[] $block_list The list of WP Block array `$block` comes from.\n\t * @return array[] Filtered version of `$block_list` containing only the passed `$block` and its parents.\n\t *                 Or an empty array if `$block` cannot be found within `$block_list`.\n\t */\n\tprivate function get_block_tree( $block, $block_list ) {\n\n\t\tforeach ( $block_list as &$_block ) {\n\n\t\t\t// Found the block.\n\t\t\tif ( $block === $_block ) {\n\t\t\t\treturn array( $block );\n\t\t\t}\n\n\t\t\tif ( ! empty( $_block['innerBlocks'] ) ) {\n\t\t\t\t$tree = $this->get_block_tree( $block, $_block['innerBlocks'] );\n\t\t\t}\n\n\t\t\tif ( ! empty( $tree ) ) { // Break as soon as the desired block is removed from one of the innerBlocks.\n\t\t\t\tif ( $_block['innerBlocks'] !== $tree ) { // Update innerBlocks/innerContent structure if needed.\n\t\t\t\t\t$_block['innerBlocks'] = $tree;\n\t\t\t\t\t// Update innerContent to reflect the innerBlocks changes = only 1 innerBlock.\n\t\t\t\t\t$inner_block_in_content_index = 0;\n\t\t\t\t\tforeach ( $_block['innerContent'] as $index => $chunk ) {\n\t\t\t\t\t\tif ( ! is_string( $chunk ) && $inner_block_in_content_index++ ) {\n\t\t\t\t\t\t\tunset( $_block['innerContent'][ $index ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Re-index.\n\t\t\t\t\t$_block['innerContent'] = array_values( $_block['innerContent'] );\n\t\t\t\t}\n\n\t\t\t\treturn array( $_block );\n\t\t\t}\n\t\t}\n\n\t\treturn array();\n\t}\n\n\t/**\n\t * Installation function to install core forms.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param bool $recreate Whether or not to recreate an existing form. This is passed to `LLMS_Forms::create()`.\n\t * @return WP_Post[] Array of created posts. Array key is the location id and array value is the WP_Post object.\n\t */\n\tpublic function install( $recreate = false ) {\n\n\t\t$installed = array();\n\n\t\tforeach ( array_keys( $this->get_locations() ) as $location ) {\n\t\t\t$installed[ $location ] = $this->create( $location, $recreate );\n\t\t}\n\n\t\treturn $installed;\n\t}\n\n\t/**\n\t * Determines if a location is a valid & registered form location\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location The location id.\n\t * @return boolean\n\t */\n\tpublic function is_location_valid( $location ) {\n\t\treturn in_array( $location, array_keys( $this->get_locations() ), true );\n\t}\n\n\t/**\n\t * Loads reusable blocks into a block list.\n\t *\n\t * A reusable block contains a reference to the block post, e.g. `<!-- wp:block {\"ref\":2198} /-->`,\n\t * which will be loaded during rendering.\n\t *\n\t * Dereferencing the reusable blocks allows the entire block list to be reviewed and to validate all form fields.\n\t * This function will replace each reusable block with the parsed blocks from its reference post.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Access turned to public.\n\t *\n\t * @param array[] $blocks An array of blocks from `parse_blocks()`,\n\t *                        where each block is usually an array cast from `WP_Block_Parser_Block`.\n\t *\n\t * @return array[]\n\t */\n\tpublic function load_reusable_blocks( $blocks ) {\n\n\t\t$loaded = array();\n\n\t\tforeach ( $blocks as $block ) {\n\n\t\t\t// Skip blocks that are not reusable blocks.\n\t\t\tif ( 'core/block' === $block['blockName'] ) {\n\n\t\t\t\t// Skip reusable blocks that do not exist or are not published.\n\t\t\t\t$post = get_post( $block['attrs']['ref'] );\n\t\t\t\tif ( ! $post || 'publish' !== get_post_status( $post ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$loaded = array_merge( $loaded, $this->parse_blocks( $post->post_content ) );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Does this block's inner blocks have references to reusable blocks?\n\t\t\tif ( $block['innerBlocks'] ) {\n\t\t\t\t$block['innerBlocks'] = $this->load_reusable_blocks( $block['innerBlocks'] );\n\t\t\t}\n\n\t\t\t$loaded[] = $block;\n\t\t}\n\n\t\treturn $loaded;\n\t}\n\n\t/**\n\t * Load form autosaves when previewing a form\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param WP_Post|boolean $post WP_Post object for the llms_form post or `false` if no form found.\n\t * @return WP_Post|boolean\n\t */\n\tpublic function maybe_load_preview( $post ) {\n\n\t\t// No form post found.\n\t\tif ( ! is_object( $post ) ) {\n\t\t\treturn $post;\n\t\t}\n\n\t\t// The `_set_preview()` method is marked as private but has existed since 2.7 and my guess is that we can use this safely.\n\t\tif ( ! function_exists( '_set_preview' ) ) {\n\t\t\treturn $post;\n\t\t}\n\n\t\t$is_preview = ( is_preview() && current_user_can( $this->get_capability(), $post->ID ) );\n\n\t\treturn $is_preview ? _set_preview( $post ) : $post;\n\t}\n\n\t/**\n\t * Parse the post_content of a form into a list of WP_Block arrays.\n\t *\n\t * This method parses the blocks, loads block data from any reusable blocks,\n\t * and cascades visibility attributes onto a block's innerBlocks.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $content Post content HTML.\n\t * @return array[] Array of parsed block arrays.\n\t */\n\tpublic function parse_blocks( $content ) {\n\n\t\t$blocks = parse_blocks( $content );\n\n\t\t$blocks = $this->load_reusable_blocks( $blocks );\n\n\t\t$blocks = $this->cascade_visibility_attrs( $blocks );\n\n\t\treturn $blocks;\n\t}\n\n\t/**\n\t * Modifies a field for usage in the \"free enrollment\" checkout form\n\t *\n\t * If the block is not visible (according to LLMS block-level visibility settings)\n\t * it will return an empty array (signaling the field to be removed).\n\t *\n\t * Otherwise the block will be converted to a hidden field.\n\t *\n\t * This method is a filter callback and is intended for internal use only.\n\t *\n\t * Backwards incompatible changes and/or method removal may occur without notice.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Added `$block_list` param.\n\t * @access private\n\t *\n\t * @param array   $attrs      LLMS_Form_Field settings array for the field.\n\t * @param array   $block      WP_Block settings array.\n\t * @param array[] $block_list The list of WP Block array `$block` comes from.\n\t * @return array\n\t */\n\tpublic function prepare_field_for_free_enroll_form( $attrs, $block, $block_list ) {\n\n\t\tif ( ! $this->is_block_visible_in_list( $block, $block_list ) ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t$attrs['type'] = 'hidden';\n\t\tif ( isset( $attrs['classes'] ) && strpos( $attrs['classes'], 'llms-select2' ) !== false ) {\n\t\t\t// Avoids trying to register the select2 script when the field is a hidden field vs. select field.\n\t\t\t$attrs['classes'] = str_replace( 'llms-select2', '', $attrs['classes'] );\n\t\t}\n\t\treturn $attrs;\n\t}\n\n\t/**\n\t * Render form field blocks.\n\t *\n\t * @since 5.0.0\n\t * @since 5.9.0 Pass an empty string to `strpos()` instead of `null`.\n\t *\n\t * @param string $html  Block HTML.\n\t * @param array  $block Array of block information.\n\t * @return string\n\t */\n\tpublic function render_field_block( $html, $block ) {\n\n\t\t// Return HTML for any non llms/form-field blocks.\n\t\tif ( false === strpos( $block['blockName'] ?? '', 'llms/form-field-' ) ) {\n\t\t\treturn $html;\n\t\t}\n\n\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\n\t\t\t$inner_blocks = array_map( 'render_block', $block['innerBlocks'] );\n\t\t\treturn implode( \"\\n\", $inner_blocks );\n\n\t\t}\n\n\t\t$attrs = $this->block_to_field_settings( $block );\n\n\t\treturn llms_form_field( $attrs, false );\n\t}\n\n\t/**\n\t * Returns a non-empty string.\n\t *\n\t * Useful for returning a non empty string to filters easily.\n\t *\n\t * @since 7.1.4\n\t *\n\t * @access private\n\t *\n\t * @return string\n\t */\n\tpublic static function __return_string(): string {// phpcs:ignore -- PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore.\n\t\treturn '1';\n\t}\n}\n\nreturn LLMS_Forms::instance();\n"
  },
  {
    "path": "includes/forms/controllers/class.llms.controller.account.php",
    "content": "<?php\n/**\n * Form submission handler for forms on the Student Dashboard.\n *\n * @package LifterLMS/Forms/Controllers/Classes\n *\n * @since 3.7.0\n * @version 6.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Controller_Account class.\n *\n * @since 3.7.0\n * @since 3.35.0 Sanitize `$_POST` data.\n * @since 3.37.17 Refactored `lost_password()` and `reset_password()` methods.\n */\nclass LLMS_Controller_Account {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.7.0\n\t * @since 3.10.0 Add student subscription cancellation handler.\n\t * @since 5.0.0 Add reset password link redirection handler.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'wp', array( $this, 'reset_password_link_redirect' ), 1 );\n\n\t\tadd_action( 'init', array( $this, 'update' ) );\n\t\tadd_action( 'init', array( $this, 'lost_password' ) );\n\t\tadd_action( 'init', array( $this, 'reset_password' ) );\n\t\tadd_action( 'init', array( $this, 'cancel_subscription' ) );\n\t\tadd_action( 'init', array( $this, 'redeem_voucher' ) );\n\t}\n\n\t/**\n\t * Lets student cancel recurring access plan subscriptions from the student dashboard view order screen\n\t *\n\t * @since 3.10.0\n\t * @since 3.19.0 Unknown.\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t *\n\t * @return void\n\t */\n\tpublic function cancel_subscription() {\n\n\t\t// Invalid nonce or the form wasn't submitted.\n\t\tif ( ! isset( $_REQUEST['_cancel_sub_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_cancel_sub_nonce'] ) ), 'llms_cancel_subscription' ) ) {\n\t\t\treturn;\n\t\t} elseif ( empty( $_POST['order_id'] ) ) {\n\t\t\treturn llms_add_notice( __( 'Something went wrong. Please try again.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t$order = llms_get_post( llms_filter_input( INPUT_POST, 'order_id', FILTER_SANITIZE_NUMBER_INT ) );\n\t\t$uid   = get_current_user_id();\n\n\t\tif ( ! $order || $uid != $order->get( 'user_id' ) ) {\n\t\t\treturn llms_add_notice( __( 'Something went wrong. Please try again.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t$note = __( 'Subscription cancelled by student from account page.', 'lifterlms' );\n\n\t\t// Active subscriptions move to pending-cancel.\n\t\t// All other statuses are cancelled immediately.\n\t\tif ( 'llms-active' === $order->get( 'status' ) ) {\n\t\t\t$new_status = 'pending-cancel';\n\t\t\t$note      .= ' ' . __( 'Enrollment will be cancelled at the end of the prepaid period.', 'lifterlms' );\n\t\t} else {\n\t\t\t$new_status = 'cancelled';\n\t\t}\n\n\t\t$order->set_status( $new_status );\n\t\t$order->add_note( $note );\n\n\t\t/**\n\t\t * Action triggered after a recurring subscription is cancelled from the student dashboard by the student.\n\t\t *\n\t\t * @since 3.17.8\n\t\t *\n\t\t * @param LLMS_Order $order The order object.\n\t\t * @param integer    $uid   The WP_User ID the student who cancelled the subscription.\n\t\t */\n\t\tdo_action( 'llms_subscription_cancelled_by_student', $order, $uid );\n\t}\n\n\t/**\n\t * Handle submission of user account edit form\n\t *\n\t * @since 3.7.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function update() {\n\n\t\tif ( ! isset( $_REQUEST['_llms_update_person_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_update_person_nonce'] ) ), 'llms_update_person' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tdo_action( 'llms_before_user_account_update_submit' );\n\n\t\t// No user logged in, can't update!\n\t\t// This shouldn't happen but let's check anyway.\n\t\tif ( ! get_current_user_id() ) {\n\t\t\treturn llms_add_notice( __( 'Please log in and try again.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t$person_id = llms_update_user( $_POST, 'account' );\n\n\t\t// Validation or update issues.\n\t\tif ( is_wp_error( $person_id ) ) {\n\n\t\t\tforeach ( $person_id->get_error_messages() as $msg ) {\n\t\t\t\tllms_add_notice( $msg, 'error' );\n\t\t\t}\n\t\t\treturn;\n\n\t\t} elseif ( ! is_numeric( $person_id ) ) {\n\n\t\t\treturn llms_add_notice( __( 'An unknown error occurred when attempting to create an account, please try again.', 'lifterlms' ), 'error' );\n\n\t\t} else {\n\n\t\t\tllms_add_notice( __( 'Your account information has been saved.', 'lifterlms' ), 'success' );\n\n\t\t\t// Handle redirect.\n\t\t\tllms_redirect_and_exit( apply_filters( 'lifterlms_update_account_redirect', llms_get_endpoint_url( 'edit-account', '', llms_get_page_url( 'myaccount' ) ) ) );\n\n\t\t}\n\t}\n\n\t/**\n\t * Handle form submission of the Lost Password form\n\t *\n\t * This is the form that sends a password recovery email with a link to reset the password.\n\t *\n\t * @since 3.8.0\n\t * @since 3.9.5 Unknown.\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 3.37.17 Refactored for readability and added new hooks.\n\t * @since 4.21.3 Increase 3rd party support for WP core hooks.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return null|WP_Error|true `null` when nonce cannot be verified.\n\t *                            `WP_Error` when an error is encountered.\n\t *                            `true` on success.\n\t */\n\tpublic function lost_password() {\n\n\t\t// Invalid nonce or the form wasn't submitted.\n\t\tif ( ! isset( $_REQUEST['_lost_password_nonce'] ) ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_lost_password_nonce'] ) ), 'llms_lost_password' ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t/**\n\t\t * Fire an action immediately prior to the lost password form submission processing.\n\t\t *\n\t\t * @since 3.37.17\n\t\t */\n\t\tdo_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$err   = new WP_Error();\n\t\t$user  = false;\n\t\t$login = llms_filter_input_sanitize_string( INPUT_POST, 'llms_login' );\n\n\t\t// Login is required.\n\t\tif ( empty( $login ) ) {\n\t\t\t$err->add( 'llms_pass_reset_missing_login', __( 'Enter a username or e-mail address.', 'lifterlms' ) );\n\t\t} else {\n\n\t\t\t// Locate the user.\n\t\t\t$field = strpos( $login, '@' ) ? 'email' : 'login';\n\t\t\t$user  = get_user_by( $field, $login );\n\n\t\t\t// No user found.\n\t\t\tif ( ! $user ) {\n\t\t\t\t$err->add( 'llms_pass_reset_invalid_login', __( 'Invalid username or e-mail address.', 'lifterlms' ) );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Ensure 3rd parties that don't use the 2nd param of `lostpassword_post` still work with our reset functionality.\n\t\t *\n\t\t * This specifically adds support for WordFence's \"max allowed password resets\" under brute force protection, but\n\t\t * might be useful in other scenarios.\n\t\t */\n\t\t$_POST['user_login'] = $login;\n\n\t\t/**\n\t\t * Fires before errors are returned from a password reset request.\n\t\t *\n\t\t * Mimics WordPress core behavior so 3rd parties don't need to add special handlers for LifterLMS\n\t\t * password reset flows.\n\t\t *\n\t\t * @since 3.37.17\n\t\t *\n\t\t * @link https://developer.wordpress.org/reference/hooks/lostpassword_post/\n\t\t *\n\t\t * @param WP_Error      $err  A WP_Error object containing any errors generated by using invalid credentials.\n\t\t * @param WP_User|false $user WP_User object if found, false if the user does not exist.\n\t\t */\n\t\tdo_action( 'lostpassword_post', $err, $user );\n\n\t\t// If we have errors, output them and return.\n\t\tif ( ! empty( $err->errors ) ) { // @todo: When we can drop support for WP 5.0 and earlier we can switch to $err->has_errors().\n\t\t\tforeach ( $err->get_error_messages() as $message ) {\n\t\t\t\tllms_add_notice( $message, 'error' );\n\t\t\t}\n\t\t\treturn $err;\n\t\t}\n\n\t\t// Set the user's password reset key.\n\t\t$key = get_password_reset_key( $user );\n\t\tif ( is_wp_error( $key ) ) {\n\t\t\tllms_add_notice( $key->get_error_message(), 'error' );\n\t\t\treturn $key;\n\t\t}\n\n\t\t// Setup the email.\n\t\t$email = llms()->mailer()->get_email(\n\t\t\t'reset_password',\n\t\t\tarray(\n\t\t\t\t'key'           => $key,\n\t\t\t\t'user'          => $user,\n\t\t\t\t'login_display' => 'email' === $field ? $user->user_email : $user->user_login,\n\t\t\t)\n\t\t);\n\n\t\t// Error generating or sending the email.\n\t\tif ( ! $email || ! $email->send() ) {\n\n\t\t\t$err->add( 'llms_pass_reset_email_failure', __( 'Unable to reset password due to an unknown error. Please try again.', 'lifterlms' ) );\n\t\t\tllms_add_notice( $err->get_error_message(), 'error' );\n\t\t\treturn $err;\n\n\t\t}\n\n\t\t// Success.\n\t\tllms_add_notice( __( 'Check your e-mail for the confirmation link.', 'lifterlms' ) );\n\t\treturn true;\n\t}\n\n\t/**\n\t * Redeem a voucher from the \"Redeem Voucher\" endpoint of the student dashboard\n\t *\n\t * @since 4.12.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return null|true|WP_Error Returns `null` when the form hasn't been submitted, there's a nonce error, or there's no logged in user.\n\t *                            Returns `true` on success and an error object when an error is encountered redeeming the voucher.\n\t */\n\tpublic function redeem_voucher() {\n\n\t\tif ( ! isset( $_REQUEST['lifterlms_voucher_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['lifterlms_voucher_nonce'] ) ), 'lifterlms_voucher_check' ) || ! get_current_user_id() ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$voucher  = new LLMS_Voucher();\n\t\t$redeemed = $voucher->use_voucher( llms_filter_input_sanitize_string( INPUT_POST, 'llms_voucher_code' ), get_current_user_id() );\n\n\t\tif ( is_wp_error( $redeemed ) ) {\n\t\t\tllms_add_notice( $redeemed->get_error_message(), 'error' );\n\t\t\treturn $redeemed;\n\t\t}\n\n\t\tllms_add_notice( __( 'Voucher redeemed successfully!', 'lifterlms' ), 'success' );\n\t\treturn true;\n\t}\n\n\t/**\n\t * Handle form submission of the Reset Password form\n\t *\n\t * This is the form that actually updates a users password.\n\t *\n\t * @since 3.8.0\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t * @since 3.37.17 Use WP core functions in favor of their (deprecated) LifterLMS clones.\n\t * @since 4.21.0 Use `addslashes()` and `FILTER_UNSAFE_RAW` to mimic magic quotes behavior of the WP core reset flow.\n\t * @since 5.0.0 Refactored to move reset logic into it's own method.\n\t *\n\t * @return null|WP_Error|true Returns `null` for nonce errors or when the form hasn't been submitted, an error object when\n\t *                            errors are encountered, and `true` on success.\n\t */\n\tpublic function reset_password() {\n\n\t\t$result = $this->reset_password_handler();\n\n\t\tif ( ! $result ) {\n\t\t\treturn null;\n\t\t} elseif ( is_wp_error( $result ) ) {\n\t\t\tllms_add_notice( implode( '<br>', $result->get_error_messages() ), 'error' );\n\t\t\treturn $result;\n\t\t}\n\n\t\t// Success.\n\t\tllms_add_notice( __( 'Your password has been updated.', 'lifterlms' ) );\n\t\tllms_redirect_and_exit( add_query_arg( 'password-reset', 1, llms_get_page_url( 'myaccount' ) ) );\n\t}\n\n\t/**\n\t * Handle the submission of the password reset form.\n\t *\n\t * @since 5.0.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return null|WP_Error|true Returns `null` when the nonce can't be verified, on failure a `WP_Error` object, and `true` on success.\n\t */\n\tprivate function reset_password_handler() {\n\n\t\t// Invalid nonce or the form wasn't submitted.\n\t\tif ( ! isset( $_REQUEST['_reset_password_nonce'] ) ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_reset_password_nonce'] ) ), 'llms_reset_password' ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t/**\n\t\t * Fire an action before the user password reset form is handled.\n\t\t *\n\t\t * @since 5.0.0\n\t\t */\n\t\tdo_action( 'llms_before_user_reset_password_submit' );\n\n\t\t/**\n\t\t * Add custom validations to the password reset form.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param WP_Error|true $valid Whether or not the submitted data is valid. Return `true` for valid data or a `WP_Error` when invalid.\n\t\t */\n\t\t$valid = apply_filters( 'llms_validate_password_reset_form', $this->validate_password_reset( wp_unslash( $_POST ) ) );\n\t\tif ( is_wp_error( $valid ) ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\t$login = llms_filter_input_sanitize_string( INPUT_POST, 'llms_reset_login' );\n\t\t$key   = llms_filter_input_sanitize_string( INPUT_POST, 'llms_reset_key' );\n\t\t$user  = check_password_reset_key( $key, $login );\n\n\t\tif ( is_wp_error( $user ) ) {\n\t\t\t// Error code is either \"llms_password_reset_invalid_key\" or \"llms_password_reset_expired_key\".\n\t\t\treturn new WP_Error( sprintf( 'llms_password_reset_%s', $user->get_error_code() ), __( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', 'lifterlms' ) );\n\t\t}\n\n\t\treset_password( $user, addslashes( llms_filter_input( INPUT_POST, 'password' ) ) );\n\n\t\t/**\n\t\t * Send the WP Core admin notification when a user's password is changed via the password reset form.\n\t\t *\n\t\t * @since 3.37.17\n\t\t *\n\t\t * @param bool    $notify_admin If `true`, the admin will be notified.\n\t\t * @param WP_User $user         User object.\n\t\t */\n\t\t$notify_admin = apply_filters( 'llms_password_reset_send_admin_notification', true, $user );\n\t\tif ( $notify_admin ) {\n\t\t\twp_password_change_notification( $user );\n\t\t}\n\n\t\t/**\n\t\t * Fire an action the the user's password is reset.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param WP_User $user User object.\n\t\t */\n\t\tdo_action( 'llms_user_password_reset', $user );\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Automatically redirect password reset links to the password reset form page.\n\t *\n\t * Strips the `key` and `login` query string parameters and sets them in a cookie\n\t * (which is accessed later to populate the hidden fields on the reset form) and then\n\t * redirect to the password reset form.\n\t *\n\t * @since 5.0.0\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 6.6.0 Prevented client and server caching of the password reset form page.\n\t *\n\t * @return void\n\t */\n\tpublic function reset_password_link_redirect() {\n\n\t\tif ( is_llms_account_page() && isset( $_GET['key'] ) && isset( $_GET['login'] ) ) {\n\n\t\t\t$user = get_user_by( 'login', wp_unslash( llms_filter_input_sanitize_string( INPUT_GET, 'login' ) ) );\n\t\t\t$uid  = $user ? $user->ID : 0;\n\t\t\t$val  = sprintf( '%1$d:%2$s', $uid, wp_unslash( llms_filter_input_sanitize_string( INPUT_GET, 'key' ) ) );\n\n\t\t\t( new LLMS_Cache_Helper() )->maybe_no_cache();\n\t\t\tllms_set_password_reset_cookie( $val );\n\t\t\tllms_redirect_and_exit( add_query_arg( 'reset-pass', 1, wp_lostpassword_url() ) );\n\t\t}\n\t}\n\n\t/**\n\t * Validates the password reset form.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $posted_data User submitted data.\n\t * @return WP_Error|true\n\t */\n\tprotected function validate_password_reset( $posted_data ) {\n\n\t\t$err = new WP_Error();\n\n\t\t$fields = LLMS_Person_Handler::get_password_reset_fields();\n\n\t\t// Validate required fields.\n\t\tforeach ( $fields as &$field ) {\n\n\t\t\t$obj   = new LLMS_Form_Field( $field );\n\t\t\t$field = $obj->get_settings();\n\n\t\t\t// Field is required, submittable, and wasn't posted.\n\t\t\tif ( ! empty( $field['required'] ) && ! empty( $field['name'] ) && empty( $posted_data[ $field['name'] ] ) ) {\n\n\t\t\t\t// Translators: %s = field label or id.\n\t\t\t\t$msg = sprintf( __( '%s is a required field.', 'lifterlms' ), isset( $field['label'] ) ? $field['label'] : $field['name'] );\n\t\t\t\t$err->add( 'llms-password-reset-missing-field', $msg );\n\n\t\t\t}\n\t\t}\n\n\t\tif ( count( $err->errors ) ) {\n\t\t\treturn $err;\n\t\t}\n\n\t\t// If we have a password and password confirm and they don't match.\n\t\tif ( isset( $posted_data['password'] ) && isset( $posted_data['password_confirm'] ) && $posted_data['password'] !== $posted_data['password_confirm'] ) {\n\n\t\t\t$msg = __( 'The submitted passwords do must match.', 'lifterlms' );\n\t\t\t$err->add( 'llms-passwords-must-match', $msg );\n\t\t\treturn $err;\n\n\t\t}\n\n\t\treturn true;\n\t}\n}\n\nreturn new LLMS_Controller_Account();\n"
  },
  {
    "path": "includes/forms/controllers/class.llms.controller.login.php",
    "content": "<?php\n/**\n * User Login Form Controller\n *\n * @package LifterLMS/Forms/Controllers/Classes\n *\n * @since 3.19.4\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Controller_Login\n *\n * @since 3.19.4\n * @since 3.35.0 Sanitize `$_POST` data.\n */\nclass LLMS_Controller_Login {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.19.4\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'init', array( $this, 'login' ) );\n\n\t}\n\n\t/**\n\t * Handle Login Form Submission\n\t *\n\t * @since 3.19.4\n\t * @since 3.35.0 Sanitize `$_POST` data.\n\t *\n\t * @return void\n\t */\n\tpublic function login() {\n\n\t\tif ( ! isset( $_REQUEST['_llms_login_user_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_login_user_nonce'] ) ), 'llms_login_user' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$login = LLMS_Person_Handler::login( $_POST );\n\n\t\t// Validation or login issues.\n\t\tif ( is_wp_error( $login ) ) {\n\t\t\tforeach ( $login->get_error_messages() as $msg ) {\n\t\t\t\tllms_add_notice( $msg, 'error' );\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t$redirect = isset( $_POST['redirect'] ) ? llms_filter_input( INPUT_POST, 'redirect', FILTER_SANITIZE_URL ) : get_permalink( llms_get_page_id( 'myaccount' ) );\n\n\t\tllms_redirect_and_exit( apply_filters( 'lifterlms_login_redirect', $redirect, $login ) );\n\n\t}\n\n}\n\nreturn new LLMS_Controller_Login();\n"
  },
  {
    "path": "includes/forms/controllers/class.llms.controller.registration.php",
    "content": "<?php\n/**\n * User Registration Forms\n *\n * @package LifterLMS/Forms/Controllers/Classes\n *\n * @since 3.0.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * User Registration Forms (excludes checkout registration)\n *\n * @since 3.0.0\n */\nclass LLMS_Controller_Registration {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'init', array( $this, 'register' ) );\n\t\tadd_action( 'lifterlms_user_registered', array( $this, 'voucher' ), 10, 3 );\n\t}\n\n\t/**\n\t * Attempt to redeem a voucher on user registration if a voucher was submitted during registration\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.4 Unknown.\n\t *\n\t * @param int    $person_id WP_User ID of the newly registered user.\n\t * @param array  $data      $_POST data.\n\t * @param string $screen    Screen user registered from [checkout|registration].\n\t * @return void\n\t */\n\tpublic function voucher( $person_id, $data, $screen ) {\n\n\t\tif ( 'registration' === $screen && ! empty( $data['llms_voucher'] ) ) {\n\n\t\t\t$voucher  = new LLMS_Voucher();\n\t\t\t$redeemed = $voucher->use_voucher( $data['llms_voucher'], $person_id );\n\n\t\t\tif ( is_wp_error( $redeemed ) ) {\n\n\t\t\t\tllms_add_notice( $redeemed->get_error_message(), 'error' );\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Handle submission of user registration forms\n\t *\n\t * @since 3.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function register() {\n\n\t\tif ( ! isset( $_REQUEST['_llms_register_person_nonce'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\tif ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_llms_register_person_nonce'] ) ), 'llms_register_person' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Allow 3rd parties to perform their own validation prior to standard validation.\n\t\t *\n\t\t * If this returns a truthy, we'll stop processing.\n\t\t *\n\t\t * The extension should add a notice in addition to returning the truthy.\n\t\t *\n\t\t * @since 7.8.0\n\t\t *\n\t\t * @param boolean $valid Validation status. If `true` ceases registration execution. If `false` registration proceeds.\n\t\t */\n\t\tif ( apply_filters( 'llms_before_registration_validation', false ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tdo_action( 'lifterlms_before_new_user_registration' );\n\n\t\t// Already logged in can't register!\n\t\t// This shouldn't happen but let's check anyway.\n\t\tif ( get_current_user_id() ) {\n\t\t\treturn llms_add_notice( __( 'Already logged in! Please log out and try again.', 'lifterlms' ), 'error' );\n\t\t}\n\n\t\t$person_id = llms_register_user( $_POST, 'registration', true );\n\n\t\t// Validation or registration issues.\n\t\tif ( is_wp_error( $person_id ) ) {\n\n\t\t\tforeach ( $person_id->get_error_messages() as $msg ) {\n\t\t\t\tllms_add_notice( $msg, 'error' );\n\t\t\t}\n\t\t\treturn;\n\n\t\t} elseif ( ! is_numeric( $person_id ) ) {\n\n\t\t\t// Catch unexpected returns from llms_register_user().\n\t\t\treturn llms_add_notice( __( 'An unknown error occurred when attempting to create an account, please try again.', 'lifterlms' ), 'error' );\n\n\t\t} else {\n\n\t\t\t// Handle redirect.\n\t\t\tllms_redirect_and_exit( apply_filters( 'lifterlms_registration_redirect', llms_get_page_url( 'myaccount' ) ) );\n\n\t\t}\n\t}\n}\n\nreturn new LLMS_Controller_Registration();\n"
  },
  {
    "path": "includes/forms/controllers/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/forms/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/functions/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/functions/llms-functions-access-plans.php",
    "content": "<?php\n/**\n * Functions for LifterLMS Access Plans\n *\n * @package LifterLMS/Functions\n *\n * @since 3.29.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Create or update an access plan.\n *\n * If $props has an \"ID\" parameter, that plan will be updated, otherwise a new plan will be created.\n *\n * @see LLMS_Access_Plan\n *\n * @since 3.29.0\n * @since 3.30.0 Added checkout redirect options.\n * @since 3.30.3 Fixed spelling errors.\n * @since 7.0.0 Correctly handle `$checkout_redirect_forced` property when updating.\n *\n * @param array $props {\n *     An array of of properties that make up the plan to create or update.\n *\n *     @type int $product_id Required) WP Post ID of the related LifterLMS Product (course or membership).\n *     @type int $id WP Post ID of the Access Plan, if omitted a new plan is created, if supplied, that plan is updated.\n *     @type string $access_expiration Expiration type [lifetime|limited-period|limited-date].\n *     @type string $access_expires Date access expires in m/d/Y format. Only applicable when $access_expiration is \"limited-date\".\n *     @type int $access_length Length of access from time of purchase, combine with $access_period. Only applicable when $access_expiration is \"limited-period\".\n *     @type string $access_period Time period of access from time of purchase, combine with $access_length. Only applicable when $access_expiration is \"limited-period\" [year|month|week|day].\n *     @type string $availability Determine if this access plan is available to anyone or to members only. Use with $availability_restrictions to determine if the member can use the access plan. [open|members].\n *     @type array $availability_restrictions Indexed array of LifterLMS Membership IDs a user must belong to to use the access plan. Only applicable if $availability is \"members\".\n *     @type string $checkout_redirect_forced On a members' only access plan, whether to force redirect users back to the redirect settings specified in this access plan.\n *     @type string $checkout_redirect_type Type of checkout redirection [self|page|url].\n *     @type string $content Plan description (post_content).\n *     @type string $enroll_text Text to display on buy buttons.\n *     @type int $frequency Frequency of billing. 0 = a one-time payment [0-6].\n *     @type string $is_free Whether or not the plan requires payment [yes|no].\n *     @type int $length Number of intervals to run payment for, combine with $period & $frequency. 0 = forever / until cancelled. Only applicable if $frequency is not 0.\n *     @type int $menu_order Order to display access plans in when listing them. Displayed in ascending order.\n *     @type string $on_sale Enable or disable plan sale pricing [yes|no].\n *     @type string $period Interval period, combine with $length. Only applicable if $frequency is not 0.  [year|month|week|day].\n *     @type float $price Price per charge/\n *     @type string $sale_end Date when the sale pricing ends.\n *     @type string $sale_start Date when the sale pricing begins.\n *     @type float $sale_price Sale price.\n *     @type string $sku Short user-created plan identifier.\n *     @type string $title Plan title.\n *     @type int $trial_length length of the trial period. Only applicable if $trial_offer is \"yes\".\n *     @type string $trial_offer Enable or disable a plan trial period. [yes|no].\n *     @type string $trial_period Period for the trial period. Only applicable if $trial_offer is \"yes\". [year|month|week|day].\n *     @type float $trial_price Price for the trial period. Can be 0 for a free trial period.\n * }\n * @return LLMS_Access_Plan|WP_Error `LLMS_Access_Plan` on success, `WP_Error` on failure.\n */\nfunction llms_insert_access_plan( $props = array() ) {\n\n\t$action = 'create';\n\n\tif ( ! empty( $props['id'] ) ) {\n\n\t\t$action = 'update';\n\t\t$plan   = llms_get_post( $props['id'] );\n\t\tif ( ! $plan || ! is_a( $plan, 'LLMS_Access_Plan' ) ) {\n\t\t\t// Translators: %s = The invalid access plan ID.\n\t\t\treturn new WP_Error( 'invalid-plan', sprintf( __( 'Access Plan ID \"%s\" is not valid.', 'lifterlms' ), $props['id'] ) );\n\t\t}\n\t\tunset( $props['id'] );\n\t\t$plan_array = $plan->toArray();\n\t\t/**\n\t\t * The property 'checkout_redirect_forced' is not sent when the related checkbox is unchecked,\n\t\t * so we have to avoid to override it with the saved value.\n\t\t *\n\t\t * {@see https://github.com/gocodebox/lifterlms/issues/2234}\n\t\t */\n\t\tif ( ! isset( $props['checkout_redirect_forced'] ) ) {\n\t\t\tunset( $plan_array['checkout_redirect_forced'] );\n\t\t}\n\t\t$props = wp_parse_args( $props, $plan );\n\n\t}\n\n\t// Merge in default properties.\n\t$props = wp_parse_args(\n\t\t$props,\n\t\tapply_filters(\n\t\t\t'llms_access_plan_default_properties',\n\t\t\tarray(\n\t\t\t\t'access_expiration'        => 'lifetime',\n\t\t\t\t'access_length'            => 1,\n\t\t\t\t'access_period'            => 'year',\n\t\t\t\t'availability'             => 'open',\n\t\t\t\t'checkout_redirect_forced' => 'no',\n\t\t\t\t'checkout_redirect_type'   => 'self',\n\t\t\t\t'frequency'                => 0,\n\t\t\t\t'is_free'                  => 'yes',\n\t\t\t\t'length'                   => 0,\n\t\t\t\t'on_sale'                  => 'no',\n\t\t\t\t'period'                   => 'year',\n\t\t\t\t'price'                    => 0,\n\t\t\t\t'sale_price'               => 0,\n\t\t\t\t'title'                    => __( 'Access Plan', 'lifterlms' ),\n\t\t\t\t'trial_length'             => 1,\n\t\t\t\t'trial_offer'              => 'no',\n\t\t\t\t'trial_period'             => 'year',\n\t\t\t\t'trial_price'              => 0,\n\t\t\t\t'visibility'               => 'visible',\n\t\t\t)\n\t\t)\n\t);\n\n\t/**\n\t * Modify the properties passed into `llms_insert_access_plan()`.\n\t *\n\t * Either `llms_access_plan_before_create` for new plans or `llms_access_plan_before_update` for updates.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @param array $props Properties used to create/update the access plan.\n\t */\n\t$props = apply_filters( 'llms_access_plan_before_' . $action, $props );\n\n\t// Cannot create an access plan without a product.\n\tif ( empty( $props['product_id'] ) || ! is_numeric( $props['product_id'] ) ) {\n\t\t// Translators: %s = property key ('product_id').\n\t\treturn new WP_Error( 'missing-product-id', sprintf( __( 'Missing required property: \"%s\".', 'lifterlms' ), 'product_id' ) );\n\t}\n\n\t// Paid plan.\n\tif ( $props['price'] > 0 ) {\n\n\t\t$props['is_free'] = 'no';\n\n\t\t// One-time (no trial).\n\t\tif ( 0 === $props['frequency'] ) {\n\t\t\t$props['trial_offer'] = 'no';\n\t\t}\n\t} else {\n\n\t\t$props['is_free']     = 'yes';\n\t\t$props['price']       = 0;\n\t\t$props['frequency']   = 0;\n\t\t$props['on_sale']     = 'no';\n\t\t$props['trial_offer'] = 'no';\n\n\t}\n\n\t// Unset recurring props when it's a 1-time payment.\n\tif ( 0 === $props['frequency'] ) {\n\t\tunset( $props['length'], $props['period'] );\n\t}\n\n\t// Unset trial props when no trial enabled.\n\tif ( ! llms_parse_bool( $props['trial_offer'] ) ) {\n\t\tunset( $props['trial_price'], $props['trial_length'], $props['trial_period'] );\n\t}\n\n\t// Unset sale props when no sale enabled.\n\tif ( ! llms_parse_bool( $props['on_sale'] ) ) {\n\t\tunset( $props['sale_price'], $props['sale_end'], $props['sale_start'] );\n\t}\n\n\t// Unset expiration props based on expiration settings.\n\tif ( 'lifetime' === $props['access_expiration'] ) {\n\t\tunset( $props['access_expires'], $props['access_length'], $props['access_period'] );\n\t} elseif ( 'limited-date' === $props['access_expiration'] ) {\n\t\tunset( $props['access_length'], $props['access_period'] );\n\t} elseif ( 'limited-period' === $props['access_expiration'] ) {\n\t\tunset( $props['access_expires'] );\n\t}\n\n\t// Ensure visibility setting is valid.\n\tif ( ! in_array( $props['visibility'], array_keys( llms_get_access_plan_visibility_options() ), true ) ) {\n\t\t// Translators: %s = supplied visibility setting.\n\t\treturn new WP_Error( 'invalid-visibility', sprintf( __( 'Invalid access plan visibility: \"%s\"', 'lifterlms' ), $props['visibility'] ) );\n\t}\n\n\t// Ensure all periods are valid.\n\t$valid_periods = array_keys( llms_get_access_plan_period_options() );\n\tforeach ( array( 'period', 'access_period', 'trial_period' ) as $key ) {\n\t\tif ( ! empty( $props[ $key ] ) && ! in_array( $props[ $key ], $valid_periods, true ) ) {\n\t\t\t// Translators: %1$s = plan period key name; %2$s = the invalid period.\n\t\t\treturn new WP_Error( 'invalid-' . $key, sprintf( __( 'Invalid access plan %1$s: \"%2$s\"', 'lifterlms' ), $key, $props[ $key ] ) );\n\t\t}\n\t}\n\n\t$checkout_redirect_type = $props['checkout_redirect_type'];\n\n\t// Ensure that the checkout redirection type is valid.\n\tif ( ! in_array( $checkout_redirect_type, array_keys( llms_get_checkout_redirection_types() ), true ) ) {\n\t\t// Translators: %s = supplied checkout redirect type.\n\t\treturn new WP_Error( 'invalid-checkout-redirect-type', sprintf( __( 'Invalid checkout redirect type: \"%s\"', 'lifterlms' ), $checkout_redirect_type ) );\n\t\t// Ensure that the correct checkout redirection value is set if the type is page.\n\t} elseif ( 'page' === $checkout_redirect_type && empty( get_post( $props['checkout_redirect_page'] ) ) ) {\n\t\t// Translators: %d = supplied checkout redirect page ID.\n\t\treturn new WP_Error( 'invalid-checkout-redirect-page', sprintf( __( 'Invalid checkout redirect page ID: \"%d\"', 'lifterlms' ), $props['checkout_redirect_page'] ) );\n\t\t// Ensure that the correct checkout redirection value is set if the type is url.\n\t} elseif ( 'url' === $checkout_redirect_type && ! filter_var( $props['checkout_redirect_url'], FILTER_VALIDATE_URL ) ) {\n\t\t// Translators: %s = supplied checkout redirect page URL.\n\t\treturn new WP_Error( 'invalid-checkout-redirect-url', sprintf( __( 'Invalid checkout redirect URL: \"%s\"', 'lifterlms' ), $props['checkout_redirect_url'] ) );\n\n\t}\n\n\tif ( 'create' === $action ) {\n\t\t$plan = new LLMS_Access_Plan( 'new' );\n\t\tif ( ! $plan ) {\n\t\t\treturn new WP_Error( 'plan-creation', __( 'An error was encountered while creating the access plan', 'lifterlms' ) );\n\t\t}\n\t}\n\n\t// Set visibility.\n\t$plan->set_visibility( $props['visibility'] );\n\n\t// Set all valid properties.\n\t$valid_props = array_keys( $plan->get_properties() );\n\tforeach ( $props as $prop_key => $prop_val ) {\n\t\tif ( in_array( $prop_key, $valid_props, true ) ) {\n\t\t\t$plan->set( $prop_key, $prop_val );\n\t\t}\n\t}\n\n\t/**\n\t * Do something with an access plan immediately after the access plan is created/updated.\n\t *\n\t * Either `llms_access_plan_after_create` during creation or  `llms_access_plan_after_update` during an update.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @param LLMS_Access_Plan $plan  Access plan instance.\n\t * @param array            $props Properties used to create/update the access plan.\n\t */\n\tdo_action( 'llms_access_plan_after_' . $action, $plan, $props );\n\n\treturn $plan;\n\n}\n\n/**\n * Retrieve available options for access plan periods\n *\n * @since 3.29.0\n *\n * @return array\n */\nfunction llms_get_access_plan_period_options() {\n\treturn apply_filters(\n\t\t'llms_get_access_plan_period_options',\n\t\tarray(\n\t\t\t'year'  => __( 'Year', 'lifterlms' ),\n\t\t\t'month' => __( 'Month', 'lifterlms' ),\n\t\t\t'week'  => __( 'Week', 'lifterlms' ),\n\t\t\t'day'   => __( 'Day', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Get a list of available access plan visibility options\n *\n * @since 3.8.0\n *\n * @return array\n */\nfunction llms_get_access_plan_visibility_options() {\n\treturn apply_filters(\n\t\t'lifterlms_access_plan_visibility_options',\n\t\tarray(\n\t\t\t'visible'  => __( 'Visible', 'lifterlms' ),\n\t\t\t'hidden'   => __( 'Hidden', 'lifterlms' ),\n\t\t\t'featured' => __( 'Featured', 'lifterlms' ),\n\t\t)\n\t);\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-conditional-tags.php",
    "content": "<?php\n/**\n * LifterLMS Conditional Tag Functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.37.0\n * @version 3.37.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! function_exists( 'is_course' ) ) {\n\n\t/**\n\t * Determine if a single course is being displayed.\n\t *\n\t * @since Unknown\n\t *\n\t * @return boolean\n\t */\n\tfunction is_course() {\n\t\treturn is_singular( array( 'course' ) );\n\t}\n}\n\nif ( ! function_exists( 'is_course_category' ) ) {\n\n\t/**\n\t * Determine if a course category archive page is being displayed.\n\t *\n\t * @since Unknown\n\t *\n\t * @param  mixed $term Single or array of course category ID(s), name(s), or slug(s).\n\t * @return boolean\n\t */\n\tfunction is_course_category( $term = '' ) {\n\t\treturn is_tax( 'course_cat', $term );\n\t}\n}\n\nif ( ! function_exists( 'is_course_tag' ) ) {\n\n\t/**\n\t * Determine if a course tag archive page is being displayed.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @param  mixed $term Single or array of course tag ID(s), name(s), or slug(s).\n\t * @return boolean\n\t */\n\tfunction is_course_tag( $term = '' ) {\n\t\treturn is_tax( 'course_tag', $term );\n\t}\n}\n\nif ( ! function_exists( 'is_course_taxonomy' ) ) {\n\n\t/**\n\t * Determine if any course taxonomy archive page is being displayed.\n\t *\n\t * @since Unknown\n\t *\n\t * @return boolean\n\t */\n\tfunction is_course_taxonomy() {\n\t\treturn is_tax( get_object_taxonomies( 'course' ) );\n\t}\n}\n\nif ( ! function_exists( 'is_courses' ) ) {\n\n\t/**\n\t * Determine if the course catalog (post type archive) is being displayed.\n\t *\n\t * @since 1.4.4\n\t * @since 3.0.0 Unknown.\n\t * @since 3.37.0 Remove ternary.\n\t *\n\t * @return boolean\n\t */\n\tfunction is_courses() {\n\t\treturn ( ( is_post_type_archive( 'course' ) ) || ( is_singular() && is_page( llms_get_page_id( 'courses' ) ) ) );\n\t}\n}\n\nif ( ! function_exists( 'is_lesson' ) ) {\n\n\t/**\n\t * Determine if current post is a lifterLMS Lesson\n\t *\n\t * @since Unknown\n\t * @since 3.37.0 Use `is_singular()` instead of comparing against global post's post type.\n\t *\n\t * @return boolean\n\t */\n\tfunction is_lesson() {\n\t\treturn is_singular( array( 'lesson' ) );\n\t}\n}\nif ( ! function_exists( 'is_lifterlms' ) ) {\n\n\t/**\n\t * Determine if a LifterLMS post type or post type archive is being displayed.\n\t *\n\t * @since Unknown.\n\t *\n\t * @return boolean\n\t */\n\tfunction is_lifterlms() {\n\n\t\t/**\n\t\t * Modify the return of the is_lifterlms() conditional function.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 3.37.0 Add check for `is_membership_taxonomy()`.\n\t\t *\n\t\t * @param boolean $is_lifterlms Default value.\n\t\t */\n\t\treturn apply_filters( 'is_lifterlms', ( is_course() || is_courses() || is_course_taxonomy() || is_lesson() || is_quiz() || is_membership() || is_memberships() || is_membership_taxonomy() ) );\n\n\t}\n}\n\nif ( ! function_exists( 'is_llms_account_page' ) ) {\n\n\t/**\n\t * Determine if the LifterLMS Student Dashboard (account page) is being displayed.\n\t *\n\t * @since 1.4.6\n\t * @since 3.37.0 Remove ternary condition.\n\t *\n\t * @return boolean\n\t */\n\tfunction is_llms_account_page() {\n\n\t\t/**\n\t\t * Override the default return of `is_llms_account_page()`\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param bool $override Default override value (false).\n\t\t */\n\t\treturn ( is_page( llms_get_page_id( 'myaccount' ) ) || apply_filters( 'lifterlms_is_account_page', false ) );\n\n\t}\n}\n\nif ( ! function_exists( 'is_llms_checkout' ) ) {\n\n\t/**\n\t * Determine if the LifterLMS Checkout page is being displayed.\n\t *\n\t * @since 1.4.6\n\t * @since 3.37.0 Remove ternary condition.\n\t *\n\t * @return boolean\n\t */\n\tfunction is_llms_checkout() {\n\t\treturn is_page( llms_get_page_id( 'checkout' ) );\n\t}\n}\n\nif ( ! function_exists( 'is_membership' ) ) {\n\n\t/**\n\t * Determine if a Membership is being displayed.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return bool\n\t */\n\tfunction is_membership() {\n\t\treturn is_singular( array( 'llms_membership' ) );\n\t}\n}\n\nif ( ! function_exists( 'is_membership_category' ) ) {\n\n\t/**\n\t * Determine if a membership category archive page is being displayed.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @param  mixed $term Single or array of membership category ID(s), name(s), or slug(s).\n\t * @return boolean\n\t */\n\tfunction is_membership_category( $term = '' ) {\n\t\treturn is_tax( 'membership_cat', $term );\n\t}\n}\n\nif ( ! function_exists( 'is_membership_tag' ) ) {\n\n\t/**\n\t * Determine if a membership tag archive page is being displayed.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @param  mixed $term Single or array of membership tag ID(s), name(s), or slug(s).\n\t * @return boolean\n\t */\n\tfunction is_membership_tag( $term = '' ) {\n\t\treturn is_tax( 'membership_tag', $term );\n\t}\n}\n\nif ( ! function_exists( 'is_membership_taxonomy' ) ) {\n\n\t/**\n\t * Determine if any course taxonomy archive page is being displayed.\n\t *\n\t * @since 3.22.0\n\t *\n\t * @return bool\n\t */\n\tfunction is_membership_taxonomy() {\n\t\treturn is_tax( get_object_taxonomies( 'llms_membership' ) );\n\t}\n}\n\nif ( ! function_exists( 'is_memberships' ) ) {\n\n\t/**\n\t * Determine if the membership catalog (post type archive) is being displayed.\n\t *\n\t * @since Unknown\n\t * @since 3.37.0 Removed ternary condition.\n\t *\n\t * @return boolean\n\t */\n\tfunction is_memberships() {\n\t\treturn ( is_post_type_archive( 'llms_membership' ) || ( is_singular() && is_page( llms_get_page_id( 'memberships' ) ) ) );\n\t}\n}\n\nif ( ! function_exists( 'is_quiz' ) ) {\n\n\t/**\n\t * Determine if a single Quiz is being displayed.\n\t *\n\t * @since Unknown.\n\t * @since 3.37.0 Use `is_singular()` instead of comparing against global post's post type.\n\t *\n\t * @return boolean\n\t */\n\tfunction is_quiz() {\n\t\treturn is_singular( array( 'llms_quiz' ) );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-content.php",
    "content": "<?php\n/**\n * (Post) Content functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.25.1\n * @version 4.17.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! function_exists( 'llms_get_post_content' ) ) {\n\n\t/**\n\t * Post Template Include\n\t *\n\t * Adds LifterLMS template content before and after the post's default content.\n\t *\n\t * @since 1.0.0\n\t * @since 3.25.2 Unknown.\n\t * @since 4.17.0 Refactored.\n\t *\n\t * @param string $content WP_Post post_content.\n\t * @return string\n\t */\n\tfunction llms_get_post_content( $content ) {\n\n\t\tglobal $post;\n\t\tif ( ! $post instanceof WP_Post ) {\n\t\t\treturn $content;\n\t\t}\n\n\t\t$restrictions = llms_page_restricted( $post->ID );\n\n\t\tif ( in_array( $post->post_type, array( 'course', 'llms_membership', 'lesson', 'llms_quiz' ), true ) ) {\n\n\t\t\t$post_type       = str_replace( 'llms_', '', $post->post_type );\n\t\t\t$template_before = 'single-' . $post_type . '-before';\n\t\t\t$template_after  = 'single-' . $post_type . '-after';\n\n\t\t\tif ( $restrictions['is_restricted'] ) {\n\t\t\t\t$content = llms_get_post_sales_page_content( $post, $content );\n\t\t\t\tif ( in_array( $post->post_type, array( 'lesson', 'llms_quiz' ), true ) ) {\n\t\t\t\t\t$content         = '';\n\t\t\t\t\t$template_before = 'no-access-before';\n\t\t\t\t\t$template_after  = 'no-access-after';\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tob_start();\n\t\t\tload_template( llms_get_template_part_contents( 'content', $template_before ), false );\n\t\t\t$before = ob_get_clean();\n\n\t\t\tob_start();\n\t\t\tload_template( llms_get_template_part_contents( 'content', $template_after ), false );\n\t\t\t$after = ob_get_clean();\n\n\t\t\t$content = do_shortcode( $before . $content . $after );\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the post_content of a LifterLMS post type.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string  $content      Post content.\n\t\t * @param WP_Post $post         Post object.\n\t\t * @param array   $restrictions Result from `llms_page_restricted()` for the current post.\n\t\t */\n\t\treturn apply_filters( 'llms_get_post_content', $content, $post, $restrictions );\n\n\t}\n}\n\n/**\n * Retrieve the sales page content for a course or membership\n *\n * By default only courses and memberships support sales pages, the meta property\n * must be set to `content` or an empty string, and the post must have a `post_excerpt`\n * property value.\n *\n * @since 4.17.0\n *\n * @param WP_Post $post    The post object.\n * @param string  $default Optional. Default content to use when no override content can be found.\n * @return string\n */\nfunction llms_get_post_sales_page_content( $post, $default = '' ) {\n\n\t$content = $default;\n\n\tif ( post_type_supports( $post->post_type, 'llms-sales-page' ) ) {\n\t\t$sales_page = get_post_meta( $post->ID, '_llms_sales_page_content_type', true );\n\t\tif ( $post->post_excerpt && ( '' === $sales_page || 'content' === $sales_page ) ) {\n\t\t\tadd_filter( 'the_excerpt', array( $GLOBALS['wp_embed'], 'autoembed' ), 9 );\n\t\t\t$content = llms_get_excerpt( $post->ID );\n\t\t}\n\t}\n\n\t/**\n\t * Filters the HTML content of a LifterLMS post type's sales page content\n\t *\n\t * @since 4.17.0\n\t *\n\t * @param string  $content HTML content of the sales page.\n\t * @param WP_Post $content Post object.\n\t * @param string  $default Default content used when no override content can be found.\n\t */\n\treturn apply_filters( 'llms_post_sales_page_content', $content, $post, $default );\n\n}\n\n/**\n * Initialize LifterLMS post type content filters\n *\n * This method is used to determine whether or `llms_get_post_content()` should automatically\n * be added as a filter callback for the WP core `the_content` filter.\n *\n * When working with posts on the admin panel (during course building, importing) we don't want\n * other plugins that may desire running `apply_filters( 'the_content', $content )` to apply our\n * plugin's filters.\n *\n * @since 4.17.0\n *\n * @param callable $callback Optional. Callback function to be added as a callback for the filter `the_content`. Default 'llms_get_post_content'.\n * @param integer  $priority Optional. Priority used when adding the filter. Default: 10.\n * @return boolean Returns `true` if content filters are added and `false` if not.\n */\nfunction llms_post_content_init( $callback = 'llms_get_post_content', $priority = 10 ) {\n\n\t// Don't filter post content on the admin panel.\n\t$should_filter = ( false === is_admin() );\n\n\t/**\n\t * Filters whether or not LifterLMS content filters should be applied.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @param boolean  $should_filter Whether or not to filter the content.\n\t * @param callable $callback      Callback function to be added as a callback for the filter `the_content`.\n\t */\n\tif ( apply_filters( 'llms_should_filter_post_content', $should_filter, $callback ) ) {\n\t\treturn add_filter( 'the_content', $callback, $priority );\n\t}\n\n\treturn false;\n\n}\n\nllms_post_content_init();\n"
  },
  {
    "path": "includes/functions/llms-functions-deprecated.php",
    "content": "<?php\n/**\n * Deprecated Functions\n *\n * * * * * * * * * * * * * * * *\n * sometimes a thing must      *\n * be set aflame for from that *\n * black all new things come   *\n * * * * * * * * * * * * * * * *\n *\n * @package LifterLMS/Functions\n *\n * @since 3.29.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Add product-id to WP query variables\n *\n * @param array $vars [WP query variables]\n * @return array $vars [WP query variables]\n *\n * @todo  deprecate?\n */\nfunction llms_add_query_var_product_id( $vars ) {\n\t$vars[] = 'product-id';\n\treturn $vars;\n}\nadd_filter( 'query_vars', 'llms_add_query_var_product_id' );\n\n\n/**\n * Sanitize text field\n *\n * @param  string $var [raw text field input]\n * @return string [clean string]\n *\n * @todo  deprecate b/c sanitize_text_field() already exists....\n */\nfunction llms_clean( $var ) {\n\treturn sanitize_text_field( $var );\n}\n\n/**\n * Schedule expired membership cron\n *\n * @return void\n */\nfunction llms_expire_membership_schedule() {\n\tif ( ! wp_next_scheduled( 'llms_check_for_expired_memberships' ) ) {\n\t\t  wp_schedule_event( time(), 'daily', 'llms_check_for_expired_memberships' );\n\t}\n}\nadd_action( 'wp', 'llms_expire_membership_schedule' );\n\n/**\n * Expire Membership\n *\n * @return void\n */\nfunction llms_expire_membership() {\n\tglobal $wpdb;\n\n\t// find all memberships wth an expiration date\n\t$args = array(\n\t\t'post_type'      => 'llms_membership',\n\t\t'posts_per_page' => 500,\n\t\t'meta_query'     => array(\n\t\t\t'key' => '_llms_expiration_interval',\n\t\t),\n\t);\n\n\t$posts = get_posts( $args );\n\n\tif ( empty( $posts ) ) {\n\t\treturn;\n\t}\n\n\tforeach ( $posts as $post ) {\n\n\t\t// make sure interval and period exist before continuing.\n\t\t$interval = get_post_meta( $post->ID, '_llms_expiration_interval', true );\n\t\t$period   = get_post_meta( $post->ID, '_llms_expiration_period', true );\n\n\t\tif ( empty( $interval ) || empty( $period ) ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// query postmeta table and find all users enrolled\n\t\t$meta_key_status   = '_status';\n\t\t$meta_value_status = 'Enrolled';\n\n\t\t$results = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT * FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE post_id = %d AND meta_key = %s AND meta_value = %s ORDER BY updated_date DESC\",\n\t\t\t\t$post->ID,\n\t\t\t\t$meta_key_status,\n\t\t\t\t$meta_value_status\n\t\t\t)\n\t\t);\n\n\t\tfor ( $i = 0; $i < count( $results ); $i++ ) {\n\t\t\t$results[ $results[ $i ]->post_id ] = $results[ $i ];\n\t\t\tunset( $results[ $i ] );\n\t\t}\n\n\t\t$enrolled_users = $results;\n\n\t\tforeach ( $enrolled_users as $user ) {\n\n\t\t\t$user_id               = $user->user_id;\n\t\t\t$meta_key_start_date   = '_start_date';\n\t\t\t$meta_value_start_date = 'yes';\n\n\t\t\t$start_date = $wpdb->get_results(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"SELECT updated_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d AND post_id = %d AND meta_key = %s AND meta_value = %s ORDER BY updated_date DESC\",\n\t\t\t\t\t$user_id,\n\t\t\t\t\t$post->ID,\n\t\t\t\t\t$meta_key_start_date,\n\t\t\t\t\t$meta_value_start_date\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// add expiration terms to start date\n\t\t\t$exp_date = date( 'Y-m-d', strtotime( date( 'Y-m-d', strtotime( $start_date[0]->updated_date ) ) . ' +' . $interval . ' ' . $period ) );\n\n\t\t\t// get current datetime\n\t\t\t$today = current_time( 'mysql' );\n\t\t\t$today = date( 'Y-m-d', strtotime( $today ) );\n\n\t\t\t// if a date parse causes exp date to be unmodified then return.\n\t\t\tif ( $exp_date == $start_date[0]->updated_date ) {\n\t\t\t\tLLMS_log( 'An error occurred modifying the date value. Function: llms_expire_membership, interval: ' . $interval . ' period: ' . $period );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// compare expiration date to current date.\n\t\t\tif ( $exp_date < $today ) {\n\t\t\t\t$set_user_expired = array(\n\t\t\t\t\t'post_id'  => $post->ID,\n\t\t\t\t\t'user_id'  => $user_id,\n\t\t\t\t\t'meta_key' => '_status',\n\t\t\t\t);\n\n\t\t\t\t$status_update = array(\n\t\t\t\t\t'meta_value'   => 'Expired',\n\t\t\t\t\t'updated_date' => current_time( 'mysql' ),\n\t\t\t\t);\n\n\t\t\t\t// change enrolled to expired in user_postmeta\n\t\t\t\t$wpdb->update( $wpdb->prefix . 'lifterlms_user_postmeta', $status_update, $set_user_expired );\n\n\t\t\t\t// remove membership id from usermeta array\n\t\t\t\t$users_levels = get_user_meta( $user_id, '_llms_restricted_levels', true );\n\t\t\t\tif ( in_array( $post->ID, $users_levels ) ) {\n\t\t\t\t\t$key = array_search( $post->ID, $users_levels );\n\t\t\t\t\tunset( $users_levels[ $key ] );\n\n\t\t\t\t\tupdate_user_meta( $user_id, '_llms_restricted_levels', $users_levels );\n\t\t\t\t}\n\t\t\t}\n\t\t}// End foreach().\n\t}// End foreach().\n\n}\nadd_action( 'llms_check_for_expired_memberships', 'llms_expire_membership' );\n\n/**\n * Retrieve the minimum accepted password strength for student passwords\n *\n * @since 3.0.0\n * @deprecated 5.0.0 `llms_get_minimum_password_strength` is deprecated with no replacement.\n *\n * @return string\n */\nfunction llms_get_minimum_password_strength() {\n\tllms_deprecated_function( 'llms_get_minimum_password_strength', '5.0.0' );\n\treturn apply_filters( 'llms_get_minimum_password_strength', 'strong' );\n}\n\n/**\n * Backwards compatibility for the deprecated earned engagement content meta keys.\n *\n * This public function is intentionally marked as private to denote it's temporary lifespan. This function\n * will be removed in the next major release when the associated meta key is also fully removed.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @param string                                      $val Default value (an empty string).\n * @param LLMS_User_Certificate|LLMS_User_Achievement $obj User engagement object.\n * @return string\n */\nfunction llms_earned_engagement_deprecated_content( $val, $obj ) {\n\t_llms_earned_engagement_deprecated_function( $obj, 'content', 'the WP_Post object property \"post_content\"' );\n\treturn $obj->get( 'content' );\n}\n\n/**\n * Backwards compatibility for the deprecated earned engagement image meta keys.\n *\n * This public function is intentionally marked as private to denote it's temporary lifespan. This function\n * will be removed in the next major release when the associated meta key is also fully removed.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @param string                                      $val Default value (an empty string).\n * @param LLMS_User_Certificate|LLMS_User_Achievement $obj User engagement object.\n * @return int\n */\nfunction llms_earned_engagement_deprecated_image( $val, $obj ) {\n\t_llms_earned_engagement_deprecated_function( $obj, 'image', 'the WP_Post meta key \"_thumbnail_id\"' );\n\treturn get_post_thumbnail_id( $obj->get( 'id' ) );\n}\n\n/**\n * Backwards compatibility for the deprecated earned engagement template meta keys.\n *\n * This public function is intentionally marked as private to denote it's temporary lifespan. This function\n * will be removed in the next major release when the associated meta key is also fully removed.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @param string                                      $val Default value (an empty string).\n * @param LLMS_User_Certificate|LLMS_User_Achievement $obj User engagement object.\n * @return string\n */\nfunction llms_earned_engagement_deprecated_template( $val, $obj ) {\n\t_llms_earned_engagement_deprecated_function( $obj, 'template', 'the WP_Post object property \"post_parent\"' );\n\treturn $obj->get( 'parent' );\n}\n\n/**\n * Backwards compatibility for the deprecated earned engagement title meta keys.\n *\n * This public function is intentionally marked as private to denote it's temporary lifespan. This function\n * will be removed in the next major release when the associated meta key is also fully removed.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @param string                                      $val Default value (an empty string).\n * @param LLMS_User_Certificate|LLMS_User_Achievement $obj User engagement object.\n * @return string\n */\nfunction llms_earned_engagement_deprecated_title( $val, $obj ) {\n\t_llms_earned_engagement_deprecated_function( $obj, 'title', 'the WP_Post object property \"post_title\"' );\n\treturn $obj->get( 'title' );\n}\n\n/**\n * Handle earned engagement deprecated meta keys.\n *\n * Throws a deprecation warning and replaces the default value with the new value.\n *\n * This public function is intentionally marked as private to denote it's temporary lifespan. This function\n * will be removed in the next major release when the associated meta key is also fully removed.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @param string  $val    Meta value.\n * @param int     $obj_id Object ID.\n * @param string  $key    Meta key.\n * @return string\n */\nfunction llms_engagement_handle_deprecated_meta_keys( $val, $obj_id, $key ) {\n\n\t$deprecated = array(\n\t\t'_llms_certificate_content' => 'llms_earned_engagement_deprecated_content',\n\t\t'_llms_achievement_content' => 'llms_earned_engagement_deprecated_content',\n\n\t\t'_llms_certificate_title' => 'llms_earned_engagement_deprecated_title',\n\t\t'_llms_achievement_title' => 'llms_earned_engagement_deprecated_title',\n\n\t\t'_llms_certificate_image' => 'llms_earned_engagement_deprecated_image',\n\t\t'_llms_achievement_image' => 'llms_earned_engagement_deprecated_image',\n\n\t\t'_llms_certificate_template' => 'llms_earned_engagement_deprecated_template',\n\t\t'_llms_achievement_template' => 'llms_earned_engagement_deprecated_template',\n\t);\n\n\tif ( array_key_exists( $key, $deprecated ) ) {\n\n\t\t$post_type = get_post_type( $obj_id );\n\t\tif ( in_array( $post_type, array( 'llms_my_achievement', 'llms_my_certificate' ), true ) ) {\n\n\t\t\t$class = 'LLMS_User_' . strtoupper( str_replace( 'llms_my_', '', $post_type ) );\n\t\t\treturn $deprecated[ $key ]( $val, new $class( $obj_id ) );\n\n\t\t}\n\t}\n\n\treturn $val;\n}\nadd_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\n/**\n * Throw a deprecated function warning for earned engagement meta deprecations.\n *\n * This public function is intentionally marked as private to denote it's temporary lifespan. This function\n * will be removed in the next major release when the associated meta key is also fully removed.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @param LLMS_User_Certificate|LLMS_User_Achievement $obj             User engagement object.\n * @param string                                      $meta_key        Deprecated meta key part (excluding the prefix and post type).\n * @param string                                      $replacement_msg The replacement message.\n * @return void\n */\nfunction _llms_earned_engagement_deprecated_function( $obj, $meta_key, $replacement_msg ) {\n\t$classname = get_class( $obj );\n\t$keyname   = strtolower( str_replace( 'LLMS_User_', '', $classname ) ) . '_' . $meta_key;\n\t_deprecated_function( esc_html( \"{$classname} meta key '{$keyname}'\" ), '6.0.0', wp_kses_post( $replacement_msg ) );\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-forms.php",
    "content": "<?php\n/**\n * Functions for LifterLMS Forms\n *\n * @package LifterLMS/Functions/Forms\n *\n * @since 5.0.0\n * @version 7.1.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Generate the HTML for a form field\n *\n * This function is used during AJAX calls so needs to be in a core file\n * loaded during AJAX calls!\n *\n * @since 3.0.0\n * @since 3.19.4 Unknown.\n * @since 5.0.0 Move from file: llms.functions.core.php.\n *              Utilize `LLMS_Form_Field` class for field generation and output.\n *\n * @param array      $field       Field settings.\n * @param boolean    $echo        Optional. Whether or not to output (echo) the field HTML. Default is `true`.\n * @param int|object $data_source Optional. Data source where to get field value from. Default is `null`.\n * @return string\n */\nfunction llms_form_field( $field = array(), $echo = true, $data_source = null ) {\n\n\t$args = array( $field );\n\tif ( ! is_null( $data_source ) ) {\n\t\t$args[] = $data_source;\n\t}\n\n\t$field = new LLMS_Form_Field( ...$args );\n\n\tif ( $echo ) {\n\t\t$field->render();\n\t}\n\n\treturn $field->get_html();\n\n}\n\n/**\n * Retrieve the form post for a form at a given location.\n *\n * @since 5.0.0\n *\n * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n * @param array  $args Additional arguments passed to the short-circuit filter in `LLMS_Forms->get_form_post()`.\n * @return WP_Post|false\n */\nfunction llms_get_form( $location, $args = array() ) {\n\treturn LLMS_Forms::instance()->get_form_post( $location, $args );\n}\n\n/**\n * Retrieve the HTML for a form at the given location.\n *\n * @since 5.0.0\n *\n * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n * @param array  $args Additional arguments passed to the short-circuit filter in `LLMS_Forms->get_form_post()`.\n * @return string\n */\nfunction llms_get_form_html( $location, $args = array() ) {\n\treturn LLMS_Forms::instance()->get_form_html( $location, $args );\n}\n\n/**\n * Retrieve the title of a form at a given location.\n *\n * Returns an empty string if the form is disabled via form settings.\n *\n * @since 5.0.0\n * @since 5.10.0 Return specific form title for checkout forms and free access plans.\n * @since 7.1.3 Added 3rd missing `$post_id` parameter for the Post Title Filter.\n *\n * @param string $location Form location, one of: \"checkout\", \"registration\", or \"account\".\n * @param array  $args Additional arguments passed to the short-circuit filter in `LLMS_Forms->get_form_post()`.\n * @return string\n */\nfunction llms_get_form_title( $location, $args = array() ) {\n\n\t$post = llms_get_form( $location, $args );\n\tif ( ! $post || ! llms_parse_bool( get_post_meta( $post->ID, '_llms_form_show_title', true ) ) ) {\n\t\treturn '';\n\t}\n\n\treturn 'checkout' === $location && isset( $args['plan'] ) && $args['plan']->is_free()\n\t\t?\n\t\tapply_filters( 'the_title', get_post_meta( $post->ID, '_llms_form_title_free_access_plans', true ), $post->ID )\n\t\t:\n\t\tget_the_title( $post->ID );\n\n}\n\n/**\n * Displays a login form.\n *\n * Only displays the form for logged out users (because logged in users cannot login).\n *\n * @since 1.0.0\n * @since 3.19.4 Unknown\n * @since 5.0.0 Moved logic and filters for the $message, $redirect, and $layout parameters from the template into the function.\n *\n * @param string $message  Optional. Messages to display before login form via llms_add_notice().\n * @param string $redirect Optional. URL to redirect to after login. Defaults to current page url.\n * @param string $layout   Optional. Form layout. Accepts either 'columns' (default) or 'stacked'. Default is 'columns'.\n * @return void\n */\nif ( ! function_exists( 'llms_get_login_form' ) ) {\n\tfunction llms_get_login_form( $message = null, $redirect = null, $layout = 'columns' ) {\n\n\t\t/**\n\t\t * Filters whether or not the login form should be displayed\n\t\t *\n\t\t * By default, the registration form is hidden from logged-in users and\n\t\t * displayed to logged out users.\n\t\t *\n\t\t * @since 4.16.0\n\t\t * @since 5.0.0 Moved from template `global/form-login.php`/.\n\t\t *\n\t\t * @param boolean $hide_form Whether or not to hide the form. If `true`, the form is hidden, otherwise it is displayed.\n\t\t */\n\t\tif ( apply_filters( 'llms_hide_login_form', is_user_logged_in() ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Customize the layout of the login form.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string $layout Form layout. Accepts \"columns\" (default) for a side-by-side layout\n\t\t *                       for form fields or \"stacked\" so fields sit on top of each other. Default is 'columns'.\n\t\t */\n\t\t$layout = apply_filters( 'llms_login_form_layout', $layout );\n\n\t\tif ( ! empty( $message ) ) {\n\t\t\tllms_add_notice( $message, 'notice' );\n\t\t}\n\n\t\t$redirect = empty( $redirect ) ? get_permalink() : $redirect;\n\n\t\tllms_get_template( 'global/form-login.php', compact( 'message', 'redirect', 'layout' ) );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-l10n.php",
    "content": "<?php\n/**\n * Localization functions\n *\n * @package LifterLMS/Functions\n *\n * @since 4.9.0\n * @version 4.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieve the current plugin locale.\n *\n * @since 4.9.0\n *\n * @param string $domain Text domain.\n * @return string\n */\nfunction llms_get_locale( $domain = 'lifterlms' ) {\n\n\t$locale = determine_locale();\n\n\t/**\n\t * Filter the plugin's locale\n\t *\n\t * @since Unknown\n\t *\n\t * @link https://developer.wordpress.org/reference/hooks/plugin_locale/\n\t *\n\t * @param string $locale The plugin's current locale.\n\t * @param string $domain The textdomain.\n\t */\n\treturn apply_filters( 'plugin_locale', $locale, $domain );\n}\n\nfunction llms_l10n_get_safe_directory() {\n\n\t/**\n\t * Filter the LifterLMS language file \"safe\" directory.\n\t *\n\t * This safe directory exists to provide a place where custom translations can be placed\n\t * which will not be automatically overridden by l10n files automatically pulled into\n\t * the default language directory from the WP GlotPress server during plugin updates.\n\t *\n\t * By default the safe directory is `wp-content/languages/lifterlms`.\n\t *\n\t * @since 4.9.0\n\t *\n\t * @param string $path Full server path to the safe directory.\n\t */\n\treturn apply_filters( 'llms_l10n_safe_directory', WP_LANG_DIR . '/lifterlms' );\n}\n\n/**\n * Load MO format localization files for the given text domain\n *\n * This function localizes using the WP Core's default language file directories as a after\n * checking in the LifterLMS-defined \"safe\" directory.\n *\n * Language files files can be found in the following locations (The first loaded file takes priority):\n *\n *   1. wp-content/languages/{$domain}/{$domain}-{$locale}.mo\n *\n *      This is recommended \"safe\" location where custom language files can be stored. A file\n *      stored in this directory will never be automatically overwritten.\n *\n *   2. wp-content/languages/plugins/{$domain}-{$locale}.mo\n *\n *      This is the default directory where WordPress will download language files from the\n *      WordPress GlotPress server during updates. If you store a custom language file in this\n *      directory it will be overwritten during updates.\n *\n *   3. wp-content/plugins/{$domain}/languages/{$domain}-{$locale}.mo\n *\n *      This is the the LifterLMS plugin directory. A language file stored in this directory will\n *      be removed from the server during a LifterLMS plugin update.\n *\n * @since 4.9.0\n *\n * @param string      $domain       Textdomain being loaded.\n * @param string|null $plugin_dir   Full path to the plugin directory, if none supplied `LLMS_PLUGIN_DIR` is used.\n * @param string|null $language_dir Relative path to the language directory within the plugin. If none supplied, `languages` is used.\n * @return void\n */\nfunction llms_load_textdomain( $domain, $plugin_dir = null, $language_dir = null ) {\n\n\t$plugin_dir   = $plugin_dir ? $plugin_dir : LLMS_PLUGIN_DIR;\n\t$language_dir = $language_dir ? $language_dir : 'languages';\n\n\t/**\n\t * Load from the custom LifterLMS \"safe\" directory (if it exists).\n\t *\n\t * Example path: wp-content/languages/lifterlms/lifterlms-en_US.mo\n\t */\n\tload_textdomain( $domain, sprintf( '%1$s/%2$s-%3$s.mo', llms_l10n_get_safe_directory(), $domain, llms_get_locale( $domain ) ) );\n\n\t/**\n\t * Load from default plugin locations specified by the WP core.\n\t *\n\t * 1. wp-content/languages/plugins/lifterlms-en_US.mo\n\t * 2. wp-content/plugins/lifterlms/languages/lifterlms-en_US.mo\n\t */\n\tload_plugin_textdomain( $domain, false, sprintf( '%1$s/%2$s', basename( $plugin_dir ), $language_dir ) );\n}\n\n/**\n * Retrieve the current permalink structure. If no structure is set, the default structure is returned.\n *\n * Note: this should be called on install or update of LifterLMS at a time when the site language is known and set.\n *\n * @since 7.6.0\n *\n * @return array\n */\nfunction llms_get_permalink_structure() {\n\t$saved_permalinks = (array) get_option( 'llms_permalinks', array() );\n\n\t$permalinks = wp_parse_args(\n\t\t// Remove false or empty entries so we can use the default values.\n\t\tarray_filter( $saved_permalinks ),\n\t\tarray(\n\t\t\t'course_base'               => _x( 'course', 'course url slug', 'lifterlms' ),\n\t\t\t'courses_base'              => _x( 'courses', 'course archive url slug', 'lifterlms' ),\n\t\t\t'memberships_base'          => _x( 'memberships', 'membership archive url slug', 'lifterlms' ),\n\t\t\t'lesson_base'               => _x( 'lesson', 'lesson url slug', 'lifterlms' ),\n\t\t\t'quiz_base'                 => _x( 'quiz', 'quiz url slug', 'lifterlms' ),\n\t\t\t'certificate_template_base' => _x( 'certificate-template', 'slug', 'lifterlms' ),\n\t\t\t'certificate_base'          => _x( 'certificate', 'slug', 'lifterlms' ),\n\t\t\t'course_category_base'      => _x( 'course-category', 'slug', 'lifterlms' ),\n\t\t\t'course_tag_base'           => _x( 'course-tag', 'slug', 'lifterlms' ),\n\t\t\t'course_track_base'         => _x( 'course-track', 'slug', 'lifterlms' ),\n\t\t\t'course_difficulty_base'    => _x( 'course-difficulty', 'slug', 'lifterlms' ),\n\t\t\t'membership_category_base'  => _x( 'membership-category', 'slug', 'lifterlms' ),\n\t\t\t'membership_tag_base'       => _x( 'membership-tag', 'slug', 'lifterlms' ),\n\t\t)\n\t);\n\n\tarray_filter( $permalinks, 'untrailingslashit' );\n\n\tif ( $saved_permalinks !== $permalinks ) {\n\t\tupdate_option( 'llms_permalinks', $permalinks );\n\t}\n\n\treturn $permalinks;\n}\n/**\n * Set the permalink structure and only allow keys we know about.\n *\n * @since 7.6.0\n *\n * @param array $permalinks\n *\n * @return void\n */\nfunction llms_set_permalink_structure( $permalinks ) {\n\t$defaults = llms_get_permalink_structure();\n\n\t$permalinks = wp_parse_args(\n\t\t// Only allow values whose keys are in the defaults array.\n\t\tarray_intersect_key( $permalinks, $defaults ),\n\t\t$defaults\n\t);\n\n\tarray_filter( $permalinks, 'untrailingslashit' );\n\n\tupdate_option( 'llms_permalinks', $permalinks );\n}\n\n/**\n * Switch LifterLMS language to site language.\n *\n * @param string $textdomain Text domain. Defaults to lifterlms.\n * @param string $plugin_dir Plugin directory. Defaults to null.\n * @param string $language_dir Language directory. Defaults to null.\n *\n * @since 7.6.0\n */\nfunction llms_switch_to_site_locale( $textdomain = 'lifterlms', $plugin_dir = null, $language_dir = null ) {\n\tglobal $wp_locale_switcher;\n\n\tif ( function_exists( 'switch_to_locale' ) && isset( $wp_locale_switcher ) ) {\n\t\tswitch_to_locale( get_locale() );\n\n\t\t// Filter on plugin_locale so load_plugin_textdomain loads the correct locale.\n\t\tadd_filter( 'plugin_locale', 'get_locale' );\n\n\t\tllms_load_textdomain( $textdomain, $plugin_dir, $language_dir );\n\t}\n}\n\n/**\n * Switch LifterLMS language to original.\n *\n * @param string $textdomain Text domain. Defaults to lifterlms.\n * @param string $plugin_dir Plugin directory. Defaults to null.\n * @param string $language_dir Language directory. Defaults to null.\n *\n * @since 7.6.0\n */\nfunction llms_restore_locale( $textdomain = 'lifterlms', $plugin_dir = null, $language_dir = null ) {\n\tglobal $wp_locale_switcher;\n\n\tif ( function_exists( 'restore_previous_locale' ) && isset( $wp_locale_switcher ) ) {\n\t\trestore_previous_locale();\n\n\t\tremove_filter( 'plugin_locale', 'get_locale' );\n\n\t\tllms_load_textdomain( $textdomain, $plugin_dir, $language_dir );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-locale.php",
    "content": "<?php\n/**\n * Localization functions.\n *\n * @package LifterLMS/Functions/Locales\n *\n * @since 5.0.0\n * @version 5.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get countries address formatting and l10n information.\n *\n * Provides a list of language and address information for supported countries.\n *\n * @since 5.0.0\n *\n * @see languages/countries-address-info.php\n *\n * @return array\n */\nfunction llms_get_countries_address_info() {\n\n\t$info = require LLMS_PLUGIN_DIR . 'languages/countries-address-info.php';\n\n\t/**\n\t * Modify the default states list.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $info Multi-dimensional array. See \"languages/address-countries-address-info.php\" for details.\n\t */\n\treturn apply_filters( 'llms_countries_address_info', $info );\n\n}\n\n/**\n * Retrieve locale information for a specific country\n *\n * @since 5.0.0\n *\n * @param string $code Country code.\n * @return array\n */\nfunction llms_get_country_address_info( $code ) {\n\t$all = llms_get_countries_address_info();\n\treturn isset( $all[ $code ] ) ? $all[ $code ] : array();\n}\n\n/**\n * Retrieve the country name by country code\n *\n * @since 3.8.0\n *\n * @param string $code Country code.\n * @return string\n */\nfunction llms_get_country_name( $code ) {\n\t$countries = get_lifterlms_countries();\n\treturn isset( $countries[ $code ] ) ? $countries[ $code ] : $code;\n}\n\n/**\n * Retrieve a list of states for a given country.\n *\n * @since 5.0.0\n *\n * @param string $code Country code.\n * @return array\n */\nfunction llms_get_country_states( $code ) {\n\t$all = llms_get_states();\n\treturn isset( $all[ $code ] ) ? $all[ $code ] : array();\n}\n\n/**\n * Retrieve a list of states organized by country.\n *\n * @since 5.0.0\n *\n * @see languages/states.php\n *\n * @return array\n */\nfunction llms_get_states() {\n\n\t$states = require LLMS_PLUGIN_DIR . 'languages/states.php';\n\n\t/**\n\t * Modify the default states list.\n\t *\n\t * @since 1.0.0\n\t *\n\t * @param array $states Multi-dimensional array. See \"languages/states.php\" for details.\n\t */\n\treturn apply_filters( 'lifterlms_states', $states );\n\n}\n\n/**\n * Retrieve the translated (and optionally pluralized) name for a given time period string\n *\n * This is used primarily to display time period data which is stored directly in the database. When displaying\n * to a user, we wish to ensure that the translated version is displayed instead of the raw and untranslated value\n * stored in the database.\n *\n * @since 5.3.0\n *\n * @param string  $period A time period string, accepts \"day\", \"week\", \"month\", or \"year\".\n * @param integer $length The length of the period, passed to `_n()` and used for pluralization. Defaults to `1`.\n * @return string The translated and pluralized time period string. Returns the submitted string for unsupported strings.\n */\nfunction llms_get_time_period_l10n( $period, $length = 1 ) {\n\n\tswitch ( strtolower( $period ) ) {\n\n\t\tcase 'day':\n\t\t\t$period = _n( 'day', 'days', $length, 'lifterlms' );\n\t\t\tbreak;\n\n\t\tcase 'week':\n\t\t\t$period = _n( 'week', 'weeks', $length, 'lifterlms' );\n\t\t\tbreak;\n\n\t\tcase 'month':\n\t\t\t$period = _n( 'month', 'months', $length, 'lifterlms' );\n\t\t\tbreak;\n\n\t\tcase 'year':\n\t\t\t$period = _n( 'year', 'years', $length, 'lifterlms' );\n\t\t\tbreak;\n\n\t}\n\n\t/**\n\t * Filter the translated name for a given time period string.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param string $period Translated period name.\n\t * @param int    $length Period length, used for pluralization.\n\t */\n\treturn apply_filters( 'llms_time_period_l10n', $period, $length );\n\n}\n\n/**\n * Get Countries array for Select list\n *\n * @since 1.0.0\n * @since 3.28.2 Updated country list.\n * @since 5.0.0 Moved from llms.functions.currency.php.\n *               Use country list stored in file at languages/countries.php.\n *\n * @return array\n */\nfunction get_lifterlms_countries() {\n\n\t$countries = require LLMS_PLUGIN_DIR . 'languages/countries.php';\n\n\t/**\n\t * Modify the default countries list.\n\t *\n\t * @since 1.0.0\n\t *\n\t * @param array $countries Associative array of Country Code => Country Name.\n\t */\n\t$countries = apply_filters( 'lifterlms_countries', $countries );\n\n\treturn array_unique( $countries );\n\n}\n\n/**\n * Get the default LifterLMS country as configured in site settings.\n *\n * @since 3.0.0\n * @since 5.0.0 Moved from llms.functions.currency.php.\n *\n * @return string Country code.\n */\nfunction get_lifterlms_country() {\n\treturn apply_filters( 'lifterlms_country', get_option( 'lifterlms_country', 'US' ) );\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-options.php",
    "content": "<?php\n/**\n * Option/Settings related functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.29.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieve a \"secure\" option.\n *\n * Checks environment variables and then constant definitions\n *\n * @since 3.29.0\n *\n * @param string $secure_name Name of the option variable / constant.\n * @param mixed  $default     Optional default value used as a fallback.\n * @param string $db_name     Optional option name to fallback on if no constant or environment var is found.\n * @return mixed\n */\nfunction llms_get_secure_option( $secure_name, $default = false, $db_name = '' ) {\n\n\t// Try an environment variable first.\n\t$val = getenv( $secure_name );\n\n\tif ( false !== $val ) {\n\t\treturn $val;\n\t}\n\n\t// Try a constant.\n\tif ( defined( $secure_name ) ) {\n\t\treturn constant( $secure_name );\n\t}\n\n\tif ( $db_name ) {\n\t\treturn get_option( $db_name, $default );\n\t}\n\n\t// Return the default value.\n\treturn $default;\n\n}\n\n/**\n * Determines if the given option name is stored in a \"secure\" manner, i.e. an environment variable or a constant.\n *\n * @since 7.0.0\n *\n * @param string $secure_name The name of the possibly secure option.\n * @return bool Returns `true` if the option is defined in an environment variable or a constant, else `false`.\n */\nfunction llms_is_option_secure( $secure_name ) {\n\n\t// Sanity check for empty strings to prevent `getenv()` from returning ALL variables.\n\tif ( '' === $secure_name ) {\n\t\treturn false;\n\t}\n\n\t/*\n\t * Note: Do not store `false` values in an environment variable\n\t * because `getenv()` returns `false` if the variable is not set.\n\t */\n\tif ( false !== getenv( $secure_name ) ) {\n\t\treturn true;\n\t}\n\n\treturn defined( $secure_name );\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-progression.php",
    "content": "<?php\n/**\n * Course / Lesson progression functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.29.0\n * @version 3.29.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Determine if lesson completion is allowed for a given user & lesson\n *\n * @param   int    $user_id    WP User ID.\n * @param   int    $lesson_id  WP Post ID of a lesson.\n * @param   string $trigger    Optional trigger description string.\n * @param   array  $args       Optional arguments.\n * @return  boolean\n * @since   3.29.0\n * @version 3.29.0\n */\nfunction llms_allow_lesson_completion( $user_id, $lesson_id, $trigger = '', $args = array() ) {\n\t/**\n\t * @filter llms_allow_lesson_completion\n\t * @since 3.17.1\n\t * @version 3.17.1\n\t */\n\treturn apply_filters( 'llms_allow_lesson_completion', true, $user_id, $lesson_id, $trigger, $args );\n}\n\n/**\n * Determines whether or not a \"Mark Complete\" button should be displayed for a given lesson\n *\n * If the lesson has a quiz, the button will only be shown if the current user has\n * already met the quiz requirements (passed the quiz, or completed at least one attempt\n * if passing is not required).\n *\n * @since 3.29.0\n * @since 10.0.0 Show button when quiz requirements are already met. Fixes issue #3058.\n *\n * @param LLMS_Lesson $lesson LLMS_Lesson instance.\n * @return boolean\n */\nfunction llms_show_mark_complete_button( $lesson ) {\n\n\t$show = true;\n\n\t// If a quiz button should be shown, check if user already met quiz requirements.\n\tif ( llms_show_take_quiz_button( $lesson ) ) {\n\t\t$show = false;\n\n\t\t// Check if current user has already met quiz requirements.\n\t\t$user_id = get_current_user_id();\n\t\tif ( $user_id && $lesson->is_quiz_enabled() ) {\n\t\t\t$student = llms_get_student( $user_id );\n\t\t\tif ( $student ) {\n\t\t\t\t$quiz_id = $lesson->get( 'quiz' );\n\t\t\t\t$attempt = $student->quizzes()->get_best_attempt( $quiz_id );\n\n\t\t\t\tif ( $attempt ) {\n\t\t\t\t\t$passing_required = llms_parse_bool( $lesson->get( 'require_passing_grade' ) );\n\t\t\t\t\t// Show button if: passing not required, OR attempt is passing.\n\t\t\t\t\tif ( ! $passing_required || $attempt->is_passing() ) {\n\t\t\t\t\t\t$show = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn apply_filters( 'llms_show_mark_complete_button', $show, $lesson );\n\n}\n\n\n/**\n * Determines whether or not a \"Take Quiz\" button should be displayed for a given lesson.\n *\n * @param   obj $lesson LLMS_Lesson.\n * @return  boolean\n * @since   3.29.0\n * @version 3.29.0\n */\nfunction llms_show_take_quiz_button( $lesson ) {\n\n\t// If a lesson has a quiz, show the button, otherwise don't.\n\t$show = $lesson->has_quiz();\n\n\t// if the lesson has a quiz make sure we can show the button to the current user.\n\tif ( $show ) {\n\n\t\t$quiz_id = $lesson->get( 'quiz' );\n\n\t\t// if the quiz isn't published and the current user can't edit the quiz don't show the button.\n\t\tif ( 'publish' !== get_post_status( $quiz_id ) && ! current_user_can( 'edit_post', $quiz_id ) ) {\n\t\t\t$show = false;\n\t\t}\n\t}\n\n\t// allow 3rd parties to modify default behavior.\n\treturn apply_filters( 'llms_show_take_quiz_button', $show, $lesson );\n\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-template-view-order.php",
    "content": "<?php\n/**\n * Order view template functions.\n *\n * @package LifterLMS/Functions\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\nif ( ! function_exists( 'llms_template_view_order' ) ) {\n\n\t/**\n\t * Loads the template for a single order view on the student dashboard.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_Order $order The order to display.\n\t * @return void\n\t */\n\tfunction llms_template_view_order( $order ) {\n\n\t\t// Validate order object and only allow the order's user to view the order.\n\t\tif ( ! $order instanceof LLMS_Order || get_current_user_id() !== $order->get( 'user_id' ) ) {\n\t\t\tesc_html_e( 'Invalid Order.', 'lifterlms' );\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Allows customization of the view order layout on the student dashboard.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param boolean    $use_stacked_layout If `true`, forces usage of the stacked layout instead of the default side-by-side layout.\n\t\t * @param LLMS_Order $order              The order to display.\n\t\t */\n\t\t$layout_class = apply_filters( 'llms_sd_stacked_order_layout', false, $order ) ? 'llms-stack-cols' : '';\n\n\t\t$transactions = _llms_template_view_order_get_transactions( $order );\n\n\t\tllms_get_template( 'myaccount/view-order.php', compact( 'order', 'transactions', 'layout_class' ) );\n\t}\n}\n\nif ( ! function_exists( 'llms_template_view_order_actions' ) ) {\n\n\t/**\n\t * Loads the single order view actions sidebar on the student dashboard.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_Order $order The order to display.\n\t * @return void\n\t */\n\tfunction llms_template_view_order_actions( $order ) {\n\t\tllms_get_template( 'myaccount/view-order-actions.php', compact( 'order' ) );\n\t}\n}\n\nif ( ! function_exists( 'llms_template_view_order_information' ) ) {\n\n\t/**\n\t * Loads the single order view information main area on the student dashboard.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_Order $order The order to display.\n\t * @return void\n\t */\n\tfunction llms_template_view_order_information( $order ) {\n\t\t$gateway = $order->get_gateway();\n\t\tllms_get_template( 'myaccount/view-order-information.php', compact( 'order', 'gateway' ) );\n\t}\n}\n\nif ( ! function_exists( 'llms_template_view_order_transactions' ) ) {\n\n\t/**\n\t * Loads the single order view transactions table on the student dashboard.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_Order $order        The order to display.\n\t * @param array      $transactions Result array from LLMS_Order::get_transactions(). If null, will load transactions from the order.\n\t * @param integer    $per_page     Number of results to display per page. Only used if `$transactions` is `null`.\n\t * @param integer    $page         Current results page to display. Only used if `$transactions` is `null`.\n\t * @return void\n\t */\n\tfunction llms_template_view_order_transactions( $order, $transactions = null, $per_page = 20, $page = null ) {\n\n\t\tif ( is_null( $transactions ) ) {\n\t\t\t$transactions = _llms_template_view_order_get_transactions( $order, $per_page, $page );\n\t\t}\n\n\t\tif ( empty( $transactions['transactions'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tllms_get_template( 'myaccount/view-order-transactions.php', compact( 'transactions' ) );\n\t}\n}\n\n/**\n * Loads transactions for the given order.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @param LLMS_order   $order    Order object.\n * @param integer      $per_page Transactions to display per page.\n * @param null|integer $page     Results page.\n * @return array Results from LLMS_Order::get_transactions().\n */\nfunction _llms_template_view_order_get_transactions( $order, $per_page = 20, $page = null ) {\n\n\t$page = is_null( $page ) ? absint( llms_filter_input( INPUT_GET, 'txnpage', FILTER_SANITIZE_NUMBER_INT ) ) : $page;\n\n\treturn $order->get_transactions(\n\t\tarray(\n\t\t\t/**\n\t\t\t * Filters the number of transactions displayed on each page when viewing order details.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param integer $per_page Number of orders per page. Default: `20`.\n\t\t\t */\n\t\t\t'per_page' => apply_filters( 'llms_student_dashboard_transactions_per_page', $per_page ),\n\t\t\t'paged'    => $page ? $page : 1,\n\t\t)\n\t);\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-templates-courses.php",
    "content": "<?php\n/**\n * Course template functions\n *\n * @package LifterLMS/Functions\n *\n * @since 4.11.0\n * @version 4.11.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! function_exists( 'lifterlms_template_course_author' ) ) {\n\t/**\n\t * Get single post author template\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_course_author() {\n\t\tllms_get_template( 'course/author.php' );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-templates-memberships.php",
    "content": "<?php\n/**\n * Membership template functions\n *\n * @package LifterLMS/Functions\n *\n * @since 4.11.0\n * @version 4.11.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n\nif ( ! function_exists( 'llms_template_membership_instructors' ) ) {\n\t/**\n\t * Get single membership instructors template\n\t *\n\t * @since 4.11.0\n\t *\n\t * @return void\n\t */\n\tfunction llms_template_membership_instructors() {\n\t\tllms_get_template( 'membership/instructors.php' );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-templates-shared.php",
    "content": "<?php\n/**\n * Shared template functions\n *\n * A \"shared\" function is any function used by more than one post type.\n *\n * @package LifterLMS/Functions\n *\n * @since 4.11.0\n * @version 4.11.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! function_exists( 'llms_template_instructors' ) ) {\n\n\t/**\n\t * Get single post instructors template\n\t *\n\t * Used by courses and membership.\n\t *\n\t * @since 4.11.0\n\t *\n\t * @return void\n\t */\n\tfunction llms_template_instructors() {\n\n\t\t$llms_post = llms_get_post( get_the_ID() );\n\t\tif ( ! $llms_post || ! $llms_post instanceof LLMS_Post_Model || ! $llms_post instanceof LLMS_Interface_Post_Instructors ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$instructors = $llms_post->get_instructors( true );\n\t\tif ( ! $instructors ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$count = count( $instructors );\n\n\t\tllms_get_template( 'shared/instructors.php', compact( 'llms_post', 'instructors', 'count' ) );\n\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms-functions-user-information-fields.php",
    "content": "<?php\n/**\n * Functions for LifterLMS user information fields\n *\n * @package LifterLMS/Functions\n *\n * @since 5.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieve a single user information field by its ID attribute.\n *\n * @since 5.0.0\n *\n * @param string $name The field's name.\n * @return array|boolean Returns the field settings array or `false` when the field cannot be found.\n */\nfunction llms_get_user_information_field( $name ) {\n\n\t$fields = llms_get_user_information_fields();\n\n\t$field_index = array_search( $name, array_column( $fields, 'name' ), true );\n\treturn false === $field_index ? false : $fields[ $field_index ];\n}\n\n/**\n * Retrieve the filtered user information field schema\n *\n * @since 5.0.0\n *\n * @return array[] A list of LLMS_Form_Field settings arrays.\n */\nfunction llms_get_user_information_fields() {\n\n\t$fields = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-user-information-fields.php';\n\n\t/**\n\t * Filters the user information fields schema\n\t *\n\t * Custom fields can be added, removed, and modified using this filter. Please note that\n\t * LifterLMS relies on these fields so removal or modification of attributes (like `name`,\n\t * `id`, and `data_store*`) may cause LifterLMS to break in unexpected ways.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array[] $fields List of field definitions.\n\t */\n\treturn apply_filters( 'llms_user_information_fields', $fields );\n}\n\n/**\n * Retrieve user information fields used by the block editor\n *\n * This is used for JS localization purposes and returns a reduced set of data as used by\n * the editor for validation purposes.\n *\n * @since 5.0.0\n *\n * @return array[]\n */\nfunction llms_get_user_information_fields_for_editor() {\n\n\t$fields = llms_get_user_information_fields();\n\n\t/**\n\t * Filters the list of keys included for user information fields when localized into the block editor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string[] $keys Array of key names.\n\t */\n\t$keys = apply_filters(\n\t\t'llms_get_user_information_fields_for_editor_keys',\n\t\tarray(\n\t\t\t'id',\n\t\t\t'name',\n\t\t\t'label',\n\t\t\t'data_store',\n\t\t\t'data_store_key',\n\t\t)\n\t);\n\n\t// Add a value so we can use array_interect_key() later.\n\t$keys = array_fill_keys( $keys, 1 );\n\n\t// Return a reduced list.\n\treturn array_map(\n\t\tfunction ( $field ) use ( $keys ) {\n\t\t\treturn array_intersect_key( $field, $keys );\n\t\t},\n\t\t$fields\n\t);\n}\n\n/**\n * Add [llms-user] shortcodes to email and certificate template editor instances.\n *\n * This is a callback function for the `llms_merge_codes_for_button` filter.\n *\n * @since 6.0.0\n *\n * @access private\n *\n * @see llms_merge_codes_for_button\n *\n * @param array[]        $codes  Associative array of merge codes where the array key is the merge code and the array value is a name / description of the merge code.\n * @param WP_Screen|null $screen The screen object from `get_current_screen().\n * @return array[]\n */\nfunction _llms_add_user_info_to_merge_buttons( $codes, $screen ) {\n\n\tif ( $screen && ! empty( $screen->post_type ) && in_array( $screen->post_type, array( 'llms_certificate', 'llms_email' ), true ) ) {\n\n\t\tforeach ( llms_get_user_information_fields_for_editor() as $field ) {\n\n\t\t\tif ( 'password' === $field['id'] ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( 'llms_billing_address_2' === $field['id'] ) {\n\t\t\t\t$field['label'] = __( 'Address Line 2', 'lifterlms' );\n\t\t\t}\n\n\t\t\t$shortcode           = \"[llms-user {$field['data_store_key']}]\";\n\t\t\t$codes[ $shortcode ] = $field['label'];\n\n\t\t}\n\t}\n\n\treturn $codes;\n}\nadd_filter( 'llms_merge_codes_for_button', '_llms_add_user_info_to_merge_buttons', 10, 2 );\n"
  },
  {
    "path": "includes/functions/llms-functions-wrappers.php",
    "content": "<?php\n/**\n * Functions that wrap native PHP or WordPress core functions\n *\n * Most of these are pluggable primarily to allow easier testing when\n * running phpunit.\n *\n * @package LifterLMS/Functions\n *\n * @since 5.3.0\n * @version 7.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! function_exists( 'llms_current_time' ) ) {\n\t/**\n\t * Retrieve the current time based on specified type.\n\t *\n\t * This is a wrapper for the WP Core current_time which can be plugged\n\t * We plug this during unit testing to allow mocking the current time.\n\t *\n\t * The 'mysql' type will return the time in the format for MySQL DATETIME field.\n\t * The 'timestamp' type will return the current timestamp.\n\t * Other strings will be interpreted as PHP date formats (e.g. 'Y-m-d').\n\t *\n\t * If $gmt is set to either '1' or 'true', then both types will use GMT time.\n\t * if $gmt is false, the output is adjusted with the GMT offset in the WordPress option.\n\t *\n\t * @since 3.4.0\n\t * @since 5.3.0 Moved location from `includes/llms.functions.core.php`.\n\t *\n\t * @link https://developer.wordpress.org/reference/functions/current_time/\n\t * @link https://github.com/gocodebox/lifterlms-tests/blob/472c5a286e9f65e2be0c1d6b7edd8d5340d052ed/framework/functions-llms-tests.php#L2-L26\n\t *\n\t * @param string   $type Type of time to retrieve. Accepts 'mysql', 'timestamp', or PHP date format string (e.g. 'Y-m-d').\n\t * @param int|bool $gmt  Optional. Whether to use GMT timezone. Default false.\n\t * @return int|string Integer if $type is 'timestamp', string otherwise.\n\t */\n\tfunction llms_current_time( $type, $gmt = 0 ) {\n\t\treturn current_time( $type, $gmt );\n\t}\n}\n\nif ( ! function_exists( 'llms_exit' ) ) {\n\t/**\n\t * Native php exit() wrapper\n\t *\n\t * This wrapper exists primarily to allow easy testing of code that calls exit().\n\t *\n\t * @since 5.3.0\n\t *\n\t * @link https://www.php.net/manual/en/function.exit.php\n\t * @link https://github.com/gocodebox/lifterlms-tests/blob/472c5a286e9f65e2be0c1d6b7edd8d5340d052ed/framework/functions-llms-tests.php#L164-L176\n\t *\n\t * @param int|string $status Exit status passed to `exit()`.\n\t * @return void\n\t */\n\tfunction llms_exit( $status = null ) {\n\t\tif ( is_null( $status ) ) {\n\t\t\texit();\n\t\t}\n\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\texit( $status );\n\t}\n}\n\nif ( ! function_exists( 'llms_filter_input' ) ) {\n\t/**\n\t * Gets a specific external variable by name and optionally filters it\n\t *\n\t * This is a pluggable wrapper around native `filter_input` which is plugged in the testing framework\n\t * to allow easy mocking of form variables when testing form controller functions and methods.\n\t *\n\t * @since 3.29.0\n\t * @since 5.3.0 Moved location from `includes/llms.functions.core.php`.\n\t *\n\t * @link https://www.php.net/manual/en/function.filter-input.php\n\t * @link https://github.com/gocodebox/lifterlms-tests/blob/472c5a286e9f65e2be0c1d6b7edd8d5340d052ed/framework/functions-llms-tests.php#L113-L162\n\t *\n\t * @param int    $type          One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.\n\t * @param string $variable_name Name of a variable to get.\n\t * @param int    $filter        Optional. The ID of the filter to apply. Default is `FILTER_DEFAULT`.\n\t * @param mixed  $options       Optional. Associative array of options or bitwise disjunction of flags. Default is empty array.\n\t *                              If filter accepts options, flags can be provided in \"flags\" field of array.\n\t * @return mixed  Value of the requested variable on success, FALSE if the filter fails, or NULL if\n\t *                the variable_name variable is not set. If the flag FILTER_NULL_ON_FAILURE is used,\n\t *                it returns FALSE if the variable is not set and NULL if the filter fails.\n\t */\n\tfunction llms_filter_input( $type, $variable_name, $filter = FILTER_DEFAULT, $options = array() ) {\n\t\treturn filter_input( $type, $variable_name, $filter, $options );\n\t}\n}\n\nif ( ! function_exists( 'llms_redirect_and_exit' ) ) {\n\t/**\n\t * Redirect and exit\n\t *\n\t * Wrapper for WP core redirects which automatically calls `exit();`.\n\t *\n\t * This function is redefined when running phpunit tests to make testing code that redirects (and exits).\n\t *\n\t * @since 3.19.4\n\t * @since 5.3.0 Moved location from `includes/llms.functions.core.php`.\n\t * @since 7.4.0 Added `nocache_headers()` to prevent caching of temporary redirects.\n\t *\n\t * @link https://github.com/gocodebox/lifterlms-tests/blob/472c5a286e9f65e2be0c1d6b7edd8d5340d052ed/framework/functions-llms-tests.php#L178-L199\n\t *\n\t * @param string $location Full URL to redirect to.\n\t * @param array  $options  {\n\t *     Optional. Array of options. Default is empty array.\n\t *\n\t *     @type int  $status HTTP status code of the redirect. Default: `302`.\n\t *     @type bool $safe   If true, use `wp_safe_redirect()` otherwise use `wp_redirect()`. Default: `true`.\n\t * }\n\t * @return void\n\t */\n\tfunction llms_redirect_and_exit( $location, $options = array() ) {\n\n\t\t$options = wp_parse_args(\n\t\t\t$options,\n\t\t\tarray(\n\t\t\t\t'status' => 302,\n\t\t\t\t'safe'   => true,\n\t\t\t)\n\t\t);\n\n\t\tif ( 302 === $options['status'] ) {\n\t\t\tnocache_headers(); // Prevent caching of redirects.\n\t\t}\n\n\t\t$func = $options['safe'] ? 'wp_safe_redirect' : 'wp_redirect';\n\t\t$func( $location, $options['status'] );\n\t\texit();\n\t}\n}\n\nif ( ! function_exists( 'llms_setcookie' ) ) {\n\t/**\n\t * Set a cookie.\n\t *\n\t * A pluggable wrapper for the native PHP function `set_cookie()`.\n\t *\n\t * The lifterlms-tests library plugs this function during unit testing so we can mock\n\t * the returns of methods that set cookies and write tests for those functions.\n\t *\n\t * @since 4.0.0\n\t * @since 5.3.0 Moved location from `includes/llms.functions.core.php`.\n\t *\n\t * @link https://www.php.net/manual/en/function.setcookie.php\n\t * @link https://github.com/gocodebox/lifterlms-tests/blob/trunk/framework/functions-llms-tests.php#L81-L111\n\t *\n\t * @param string $name     The name of the cookie.\n\t * @param string $value    The value of the cookie.\n\t * @param int    $expires  The time wehn the cookie expires as a Unix timestamp.\n\t * @param string $path     The path on the server where the cookie will be available.\n\t * @param string $domain   The (sub)domain that the cookie is available to.\n\t * @param bool   $secure   Indicates the cookie should only be transmitted over a secure HTTPS connection.\n\t * @param bool   $httponly When `true` the cookie will only be made accessible through the HTTP protocol,\n\t *                         preventing it from being accessed by scripting languages (such as Javascript).\n\t *\n\t * @return boolean\n\t */\n\tfunction llms_setcookie( $name, $value = '', $expires = 0, $path = '', $domain = '', $secure = false, $httponly = false ) {\n\t\treturn setcookie( $name, $value, $expires, $path, $domain, $secure, $httponly );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.access.php",
    "content": "<?php\n/**\n * Functions used for managing page / post access\n *\n * @package LifterLMS/Functions\n *\n * @since 1.0.0\n * @version 7.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Determine if content should be restricted.\n *\n * Called during \"template_include\" to determine if redirects\n * or template overrides are in order.\n *\n * @since 1.0.0\n * @since 3.16.11 Unknown.\n * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n *\n * @param int      $post_id WordPress Post ID of the content.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return array Restriction check result data.\n */\nfunction llms_page_restricted( $post_id, $user_id = null ) {\n\n\t$results = array(\n\t\t'content_id'     => $post_id,\n\t\t'is_restricted'  => false,\n\t\t'reason'         => 'accessible',\n\t\t'restriction_id' => 0,\n\t);\n\n\tif ( ! $user_id ) {\n\t\t$user_id = get_current_user_id();\n\t}\n\n\t$student = false;\n\tif ( $user_id ) {\n\t\t$student = new LLMS_Student( $user_id );\n\t}\n\n\t$post_type = get_post_type( $post_id );\n\n\t/**\n\t * Do checks to determine if the content should be restricted.\n\t */\n\t$sitewide_membership_id = llms_is_post_restricted_by_sitewide_membership( $post_id, $user_id );\n\t$membership_id          = llms_is_post_restricted_by_membership( $post_id, $user_id );\n\n\tif ( is_home() && $sitewide_membership_id ) {\n\t\t$restriction_id = $sitewide_membership_id;\n\t\t$reason         = 'sitewide_membership';\n\t\t// if it's a search page and the site isn't restricted to a membership bypass restrictions.\n\t} elseif ( ( is_search() ) && ! get_option( 'lifterlms_membership_required', '' ) ) {\n\t\treturn apply_filters( 'llms_page_restricted', $results, $post_id );\n\t} elseif ( is_singular() && $sitewide_membership_id ) {\n\t\t$restriction_id = $sitewide_membership_id;\n\t\t$reason         = 'sitewide_membership';\n\t} elseif ( is_singular() && $membership_id ) {\n\t\t$restriction_id = $membership_id;\n\t\t$reason         = 'membership';\n\t} elseif ( is_singular() && 'lesson' === $post_type ) {\n\t\t$lesson = new LLMS_Lesson( $post_id );\n\t\t// if lesson is free, return accessible results and skip the rest of this function.\n\t\tif ( $lesson->is_free() ) {\n\t\t\treturn $results;\n\t\t} else {\n\t\t\t$restriction_id = $lesson->get( 'parent_course' );\n\t\t\t$reason         = 'enrollment_lesson';\n\t\t}\n\t} elseif ( is_singular() && 'course' === $post_type ) {\n\t\t$restriction_id = $post_id;\n\t\t$reason         = 'enrollment_course';\n\t} elseif ( is_singular() && 'llms_membership' === $post_type ) {\n\t\t$restriction_id = $post_id;\n\t\t$reason         = 'enrollment_membership';\n\t} else {\n\n\t\t/**\n\t\t * Allow filtering of results before checking if the student has access.\n\t\t *\n\t\t * @since Unknown.\n\t\t *\n\t\t * @param array $results Restriction check result data.\n\t\t * @param int   $post_id WordPress Post ID of the content.\n\t\t */\n\t\t$results = apply_filters( 'llms_page_restricted_before_check_access', $results, $post_id );\n\t\textract( $results ); // phpcs:ignore\n\n\t}\n\n\t/**\n\t * Content should be restricted, so we'll do the restriction checks\n\t * and return restricted results.\n\t *\n\t * This is run if we have a restriction and a reason for restriction\n\t * and we either don't have a logged in student or the logged in student doesn't have access.\n\t */\n\tif ( ! empty( $restriction_id ) && ! empty( $reason ) && ( ! $student || ! $student->is_enrolled( $restriction_id ) ) ) {\n\n\t\t$results['is_restricted']  = true;\n\t\t$results['reason']         = $reason;\n\t\t$results['restriction_id'] = $restriction_id;\n\n\t\t/**\n\t\t * Allow filtering of the restricted results.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array $results Restriction check result data.\n\t\t * @param int   $post_id WordPress Post ID of the content.\n\t\t */\n\t\treturn apply_filters( 'llms_page_restricted', $results, $post_id );\n\n\t}\n\n\t/**\n\t * At this point student has access or the content isn't supposed to be restricted\n\t * we need to do some additional checks for specific post types.\n\t */\n\tif ( is_singular() ) {\n\n\t\tif ( 'llms_quiz' === $post_type ) {\n\n\t\t\t$quiz_id = llms_is_quiz_accessible( $post_id, $user_id );\n\t\t\tif ( $quiz_id ) {\n\n\t\t\t\t$results['is_restricted']  = true;\n\t\t\t\t$results['reason']         = 'quiz';\n\t\t\t\t$results['restriction_id'] = $post_id;\n\t\t\t\t/* This filter is documented above. */\n\t\t\t\treturn apply_filters( 'llms_page_restricted', $results, $post_id );\n\n\t\t\t}\n\t\t}\n\n\t\tif ( 'lesson' === $post_type || 'llms_quiz' === $post_type ) {\n\t\t\t$course_id = llms_is_post_restricted_by_time_period( $post_id, $user_id );\n\t\t\tif ( $course_id ) {\n\t\t\t\tif ( 'lesson' === $post_type ) {\n\t\t\t\t\t$lesson = new LLMS_Lesson( $post_id );\n\t\t\t\t}\n\n\t\t\t\t// If the lesson is dripped based on enrollment, we don't want to restrict it based on course time period.\n\t\t\t\tif ( 'lesson' !== $post_type || ( $lesson && 'enrollment' !== $lesson->get( 'drip_method' ) ) ) {\n\t\t\t\t\t$results['is_restricted']  = true;\n\t\t\t\t\t$results['reason']         = 'course_time_period';\n\t\t\t\t\t$results['restriction_id'] = $course_id;\n\t\t\t\t\t/* This filter is documented above. */\n\t\t\t\t\treturn apply_filters( 'llms_page_restricted', $results, $post_id );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$lesson_id = llms_is_post_restricted_by_drip_settings( $post_id, $user_id );\n\t\t\tif ( $lesson_id ) {\n\n\t\t\t\t$results['is_restricted']  = true;\n\t\t\t\t$results['reason']         = 'lesson_drip';\n\t\t\t\t$results['restriction_id'] = $lesson_id;\n\t\t\t\t/* This filter is documented above. */\n\t\t\t\treturn apply_filters( 'llms_page_restricted', $results, $post_id );\n\t\t\t}\n\n\t\t\t$prereq_data = llms_is_post_restricted_by_prerequisite( $post_id, $user_id );\n\t\t\tif ( $prereq_data ) {\n\n\t\t\t\t$results['is_restricted']  = true;\n\t\t\t\t$results['reason']         = sprintf( '%s_prerequisite', $prereq_data['type'] );\n\t\t\t\t$results['restriction_id'] = $prereq_data['id'];\n\t\t\t\t/* This filter is documented above. */\n\t\t\t\treturn apply_filters( 'llms_page_restricted', $results, $post_id );\n\t\t\t}\n\t\t}\n\t}\n\n\t/* This filter is documented above. */\n\treturn apply_filters( 'llms_page_restricted', $results, $post_id );\n}\n\n/**\n * Retrieve a message describing the reason why content is restricted.\n * Accepts an associative array of restriction data that can be retrieved from llms_page_restricted().\n *\n * This function doesn't handle all restriction types but it should in the future.\n * Currently it's being utilized for tooltips on lesson previews and some messages\n * output during LLMS_Template_Loader handling redirects.\n *\n * @since 3.2.4\n * @since 3.16.12 Unknown.\n *\n * @param array $restriction Array of data from `llms_page_restricted()`.\n * @return string\n */\nfunction llms_get_restriction_message( $restriction ) {\n\n\t$msg = __( 'You do not have permission to access this content', 'lifterlms' );\n\n\tswitch ( $restriction['reason'] ) {\n\n\t\tcase 'course_prerequisite':\n\t\t\t$lesson      = new LLMS_Lesson( $restriction['content_id'] );\n\t\t\t$course_id   = $restriction['restriction_id'];\n\t\t\t$prereq_link = '<a href=\"' . get_permalink( $course_id ) . '\">' . get_the_title( $course_id ) . '</a>';\n\t\t\t$msg         = sprintf(\n\t\t\t\t/* Translators: %$1s = lesson title; %2$s link of the course prerequisite */\n\t\t\t\t_x(\n\t\t\t\t\t'The lesson \"%1$s\" cannot be accessed until the required prerequisite course \"%2$s\" is completed.',\n\t\t\t\t\t'restricted by course prerequisite message',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t),\n\t\t\t\t$lesson->get( 'title' ),\n\t\t\t\t$prereq_link\n\t\t\t);\n\t\t\tbreak;\n\n\t\tcase 'course_track_prerequisite':\n\t\t\t$lesson      = new LLMS_Lesson( $restriction['content_id'] );\n\t\t\t$track       = new LLMS_Track( $restriction['restriction_id'] );\n\t\t\t$prereq_link = '<a href=\"' . $track->get_permalink() . '\">' . $track->term->name . '</a>';\n\t\t\t$msg         = sprintf(\n\t\t\t\t/* Translators: %$1s = lesson title; %2$s link of the track prerequisite */\n\t\t\t\t_x(\n\t\t\t\t\t'The lesson \"%1$s\" cannot be accessed until the required prerequisite track \"%2$s\" is completed.',\n\t\t\t\t\t'restricted by course track prerequisite message',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t),\n\t\t\t\t$lesson->get( 'title' ),\n\t\t\t\t$prereq_link\n\t\t\t);\n\t\t\tbreak;\n\n\t\t// this particular case is only utilized by lessons, courses do the check differently in the template.\n\t\tcase 'course_time_period':\n\t\t\t$course = new LLMS_Course( $restriction['restriction_id'] );\n\t\t\t// if the start date hasn't passed yet.\n\t\t\tif ( ! $course->has_date_passed( 'start_date' ) ) {\n\t\t\t\t$msg = $course->get( 'course_opens_message' );\n\t\t\t} elseif ( $course->has_date_passed( 'end_date' ) ) {\n\t\t\t\t$msg = $course->get( 'course_closed_message' );\n\t\t\t}\n\t\t\tbreak;\n\n\t\tcase 'enrollment_lesson':\n\t\t\t$course = new LLMS_Course( $restriction['restriction_id'] );\n\t\t\t$msg    = $course->get( 'content_restricted_message' );\n\t\t\tbreak;\n\n\t\tcase 'lesson_drip':\n\t\t\t$lesson = new LLMS_Lesson( $restriction['restriction_id'] );\n\t\t\t$msg    = sprintf(\n\t\t\t\t/* Translators: %$1s = lesson title; %2$s available date */\n\t\t\t\t_x(\n\t\t\t\t\t'The lesson \"%1$s\" will be available on %2$s',\n\t\t\t\t\t'lesson restricted by drip settings message',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t),\n\t\t\t\t$lesson->get( 'title' ),\n\t\t\t\t$lesson->get_available_date()\n\t\t\t);\n\t\t\tbreak;\n\n\t\tcase 'lesson_prerequisite':\n\t\t\t$lesson        = new LLMS_Lesson( $restriction['content_id'] );\n\t\t\t$prereq_lesson = new LLMS_Lesson( $restriction['restriction_id'] );\n\t\t\t$prereq_link   = '<a href=\"' . get_permalink( $prereq_lesson->get( 'id' ) ) . '\">' . $prereq_lesson->get( 'title' ) . '</a>';\n\t\t\t$msg           = sprintf(\n\t\t\t\t/* Translators: %$1s = lesson title; %2$s link of the lesson prerequisite */\n\t\t\t\t_x(\n\t\t\t\t\t'The lesson \"%1$s\" cannot be accessed until the required prerequisite \"%2$s\" is completed.',\n\t\t\t\t\t'lesson restricted by prerequisite message',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t),\n\t\t\t\t$lesson->get( 'title' ),\n\t\t\t\t$prereq_link\n\t\t\t);\n\t\t\tbreak;\n\n\t\tdefault:\n\t}\n\n\t/**\n\t * Allow filtering the restriction message.\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $msg         Restriction message.\n\t * @param array  $restriction Array of data from `llms_page_restricted()`.\n\t */\n\treturn apply_filters( 'llms_get_restriction_message', do_shortcode( $msg ), $restriction );\n}\n\n/**\n * Get a boolean out of llms_page_restricted for easy if checks.\n *\n * @since 3.0.0\n * @since 3.37.10 Made `$user_id` parameter optional. Default is `null`.\n *\n * @param int      $post_id WordPress Post ID of the content.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return bool\n */\nfunction llms_is_page_restricted( $post_id, $user_id = null ) {\n\t$restrictions = llms_page_restricted( $post_id, $user_id );\n\treturn $restrictions['is_restricted'];\n}\n\n/**\n * Determine if a lesson/quiz is restricted by drip settings.\n *\n * @since 3.0.0\n * @since 3.16.11 Unknown.\n * @since 3.37.10 Use strict comparison '===' in place of '=='.\n * @since 6.5.0 Improve code readability turning if-elseif into a switch-case.\n *                Bypass drip content restriction on already completed lessons.\n *\n * @param int      $post_id WP Post ID of a lesson or quiz.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return int|false False if the lesson is available.\n *                   WP Post ID of the lesson if it is not.\n */\nfunction llms_is_post_restricted_by_drip_settings( $post_id, $user_id = null ) {\n\n\t$post_type = get_post_type( $post_id );\n\n\tswitch ( $post_type ) {\n\t\t// If we're on a lesson, lesson id is the post id.\n\t\tcase 'lesson':\n\t\t\t$lesson_id = $post_id;\n\t\t\tbreak;\n\t\tcase 'llms_quiz':\n\t\t\t$quiz      = llms_get_post( $post_id );\n\t\t\t$lesson_id = $quiz->get( 'lesson_id' );\n\t\t\tif ( ! $lesson_id ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault: // Don't pass other post types.\n\t\t\treturn false;\n\t}\n\n\t$lesson  = new LLMS_Lesson( $lesson_id );\n\t$user_id = $user_id ?? get_current_user_id();\n\t/**\n\t * Filters whether or not to bypass drip restrictions on completed lessons.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @param boolean $drip_bypass Whether or not to bypass drip restrictions on completed lessons.\n\t * @param int     $post_id     WP Post ID of a lesson or quiz potentially restricted by drip settings.\n\t * @param int     $user_id     WP User ID.\n\t */\n\t$drip_bypass  = apply_filters( 'llms_lesson_drip_bypass_if_completed', true, $post_id, $user_id );\n\t$is_available = ( $drip_bypass && $user_id && llms_is_complete( $user_id, $lesson_id, 'lesson' ) ) || $lesson->is_available();\n\n\treturn $is_available ? false : $lesson_id;\n}\n\n/**\n * Determine if a lesson/quiz is restricted by a prerequisite lesson.\n *\n * @since 3.0.0\n * @since 3.16.11 Unknown.\n * @since 6.5.0 Improve code readability turning if-elseif into a switch-case.\n *\n * @param int      $post_id WP Post ID of a lesson or quiz.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return array|false False if the post is not restricted or the user has completed the prereq\n *                     associative array with prereq type and prereq id\n *                     array(\n *                         type => [course|course_track|lesson]\n *                         id => int (object id)\n *                     ).\n */\nfunction llms_is_post_restricted_by_prerequisite( $post_id, $user_id = null ) {\n\n\t$post_type = get_post_type( $post_id );\n\n\tswitch ( $post_type ) {\n\t\t// If we're on a lesson, lesson id is the post id.\n\t\tcase 'lesson':\n\t\t\t$lesson_id = $post_id;\n\t\t\tbreak;\n\t\tcase 'llms_quiz':\n\t\t\t$quiz      = llms_get_post( $post_id );\n\t\t\t$lesson_id = $quiz->get( 'lesson_id' );\n\t\t\tif ( ! $lesson_id ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault: // Don't pass other post types.\n\t\t\treturn false;\n\t}\n\n\t$lesson = llms_get_post( $lesson_id );\n\t$course = $lesson->get_course();\n\n\tif ( ! $course ) {\n\t\treturn false;\n\t}\n\n\t// Get an array of all possible prerequisites.\n\t$prerequisites = array();\n\n\tif ( $course->has_prerequisite( 'course' ) ) {\n\t\t$prerequisites[] = array(\n\t\t\t'id'   => $course->get_prerequisite_id( 'course' ),\n\t\t\t'type' => 'course',\n\t\t);\n\t}\n\n\tif ( $course->has_prerequisite( 'course_track' ) ) {\n\t\t$prerequisites[] = array(\n\t\t\t'id'   => $course->get_prerequisite_id( 'course_track' ),\n\t\t\t'type' => 'course_track',\n\t\t);\n\t}\n\n\tif ( $lesson->has_prerequisite() ) {\n\t\t$prerequisites[] = array(\n\t\t\t'id'   => $lesson->get_prerequisite(),\n\t\t\t'type' => 'lesson',\n\t\t);\n\t}\n\n\t// Prerequisites exist and user is not logged in, return the first prereq id.\n\tif ( $prerequisites && ! $user_id ) {\n\n\t\treturn array_shift( $prerequisites );\n\n\t\t// If incomplete, send the prereq id.\n\t} else {\n\n\t\t$student = new LLMS_Student( $user_id );\n\t\tforeach ( $prerequisites as $prereq ) {\n\t\t\tif ( ! $student->is_complete( $prereq['id'], $prereq['type'] ) ) {\n\t\t\t\treturn $prereq;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Otherwise return false: no prerequisite.\n\treturn false;\n}\n\n/**\n * Determine if a course (or lesson/quiz) is \"open\" according to course time period settings.\n *\n * @since 3.0.0\n * @since 3.16.11 Unknown.\n * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n * @since 6.5.0 Improve code readability turning if-elseif into a switch-case.\n *\n * @param int      $post_id WP Post ID of a course, lesson, or quiz.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return int|false False if the post is not restricted by course time period,\n *                   WP Post ID of the course if it is.\n */\nfunction llms_is_post_restricted_by_time_period( $post_id, $user_id = null ) {\n\n\t$post_type = get_post_type( $post_id );\n\n\tswitch ( $post_type ) {\n\t\t// If we're on a lesson, get course information.\n\t\tcase 'lesson':\n\t\t\t$lesson    = new LLMS_Lesson( $post_id );\n\t\t\t$course_id = $lesson->get( 'parent_course' );\n\t\t\tbreak;\n\t\tcase 'llms_quiz':\n\t\t\t$quiz      = llms_get_post( $post_id );\n\t\t\t$lesson_id = $quiz->get( 'lesson_id' );\n\t\t\tif ( ! $lesson_id ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t$lesson = llms_get_post( $lesson_id );\n\t\t\tif ( ! $lesson_id ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t$course_id = $lesson->get( 'parent_course' );\n\t\t\tbreak;\n\t\tcase 'course':\n\t\t\t$course_id = $post_id;\n\t\t\tbreak;\n\t\tdefault: // Don't pass other post types.\n\t\t\treturn false;\n\t}\n\n\t$course = new LLMS_Course( $course_id );\n\n\treturn $course->is_open() ? false : $course_id;\n}\n\n/**\n * Determine if a WordPress post (of any type) is restricted to at least one LifterLMS Membership level.\n *\n * This function replaces the now deprecated page_restricted_by_membership() (and has slightly different functionality).\n *\n * @since 3.0.0\n * @since 3.16.14 Unknown.\n * @since 3.37.10 Call `in_array()` with strict comparison.\n *\n * @param int      $post_id WP_Post ID.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return bool|int WP_Post ID of the membership if a restriction is found.\n *                  False if no restrictions found.\n */\nfunction llms_is_post_restricted_by_membership( $post_id, $user_id = null ) {\n\n\t// don't check these posts types.\n\t$skip = apply_filters(\n\t\t'llms_is_post_restricted_by_membership_skip_post_types',\n\t\tarray(\n\t\t\t'course',\n\t\t\t'lesson',\n\t\t\t'llms_quiz',\n\t\t\t'llms_membership',\n\t\t\t'llms_question',\n\t\t\t'llms_certificate',\n\t\t\t'llms_my_certificate',\n\t\t)\n\t);\n\n\tif ( in_array( get_post_type( $post_id ), $skip, true ) ) {\n\t\treturn false;\n\t}\n\n\t$memberships = get_post_meta( $post_id, '_llms_restricted_levels', true );\n\t$restricted  = get_post_meta( $post_id, '_llms_is_restricted', true );\n\n\tif ( 'yes' === $restricted && $memberships && is_array( $memberships ) ) {\n\n\t\t// if no user, return the first membership from the array as the restriction id.\n\t\tif ( ! $user_id ) {\n\n\t\t\t$restriction_id = array_shift( $memberships );\n\n\t\t} else {\n\n\t\t\t$student = llms_get_student( $user_id );\n\t\t\tif ( ! $student ) {\n\n\t\t\t\t$restriction_id = array_shift( $memberships );\n\n\t\t\t} else {\n\n\t\t\t\t// reverse so to ensure that if user is in none of the memberships,\n\t\t\t\t// they'd encounter the same restriction settings as a visitor.\n\t\t\t\t$memberships = array_reverse( $memberships );\n\n\t\t\t\t// loop through the memberships.\n\t\t\t\tforeach ( $memberships as $mid ) {\n\n\t\t\t\t\t// set this as the restriction id.\n\t\t\t\t\t$restriction_id = $mid;\n\n\t\t\t\t\t// once we find the student has access break the loop,\n\t\t\t\t\t// this will be the restriction that the template loader will check against later.\n\t\t\t\t\tif ( $student->is_enrolled( $mid ) ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn absint( $restriction_id );\n\n\t}\n\n\treturn false;\n}\n\n/**\n * Determine if a post should bypass sitewide membership restrictions.\n *\n * If sitewide membership restriction is disabled, this will always return false.\n * This function replaces the now deprecated site_restricted_by_membership() (and has slightly different functionality).\n *\n * @since 3.0.0\n * @since 3.37.10 Do not apply membership restrictions on the page set as membership's restriction redirect page.\n *                  Exclude the privacy policy from the sitewide restriction.\n *                  Call `in_array()` with strict comparison.\n *\n * @param int      $post_id WP Post ID.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return bool|int If the post is not restricted (or there are not sitewide membership restrictions) returns false.\n *                  If the post is restricted, returns the membership id required.\n */\nfunction llms_is_post_restricted_by_sitewide_membership( $post_id, $user_id = null ) {\n\n\t$membership_id = absint( get_option( 'lifterlms_membership_required', '' ) );\n\n\t// site is restricted to a membership.\n\tif ( ! empty( $membership_id ) ) {\n\n\t\t$membership = new LLMS_Membership( $membership_id );\n\n\t\tif ( ! $membership || ! is_a( $membership, 'LLMS_Membership' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Restricted contents redirection page id, if any.\n\t\t$redirect_page_id = 'page' === $membership->get( 'restriction_redirect_type' ) ? absint( $membership->get( 'redirect_page_id' ) ) : 0;\n\n\t\t/**\n\t\t * Pages that can be bypassed when sitewide restrictions are enabled.\n\t\t */\n\t\t$allowed = apply_filters(\n\t\t\t'lifterlms_sitewide_restriction_bypass_ids',\n\t\t\tarray_filter(\n\t\t\t\tarray(\n\t\t\t\t\tabsint( $membership_id ), // the membership page the site is restricted to.\n\t\t\t\t\tabsint( get_option( 'lifterlms_terms_page_id' ) ), // terms and conditions.\n\t\t\t\t\tllms_get_page_id( 'memberships' ), // membership archives.\n\t\t\t\t\tllms_get_page_id( 'myaccount' ), // lifterlms account page.\n\t\t\t\t\tllms_get_page_id( 'checkout' ), // lifterlms checkout page.\n\t\t\t\t\tabsint( get_option( 'wp_page_for_privacy_policy' ) ), // wp privacy policy page.\n\t\t\t\t\t$redirect_page_id, // Restricted contents redirection page id.\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\tif ( in_array( $post_id, $allowed, true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $membership_id;\n\n\t} else {\n\n\t\treturn false;\n\n\t}\n}\n\n/**\n * Determine if a quiz should be accessible by a user.\n *\n * @since 3.1.6\n * @since 3.16.1 Unknown.\n *\n * @param int      $post_id WP Post ID.\n * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n * @return bool|int If the post is not restricted returns false.\n *                  If the post is restricted, returns the quiz id.\n */\nfunction llms_is_quiz_accessible( $post_id, $user_id = null ) {\n\n\t$quiz      = llms_get_post( $post_id );\n\t$lesson_id = $quiz->get( 'lesson_id' );\n\n\t// No lesson or the user is not enrolled.\n\tif ( ! $lesson_id || ! llms_is_user_enrolled( $user_id, $lesson_id ) ) {\n\t\treturn $post_id;\n\t}\n\n\treturn false;\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.certificate.php",
    "content": "<?php\n/**\n * LifterLMS Certificate Functions\n *\n * @package LifterLMS/Functions\n *\n * @since 2.2.0\n * @version 6.11.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieve the LLMS_User_Certificate instance for a given post.\n *\n * Expects the input post to be either an `llms_my_certificate` post. An `llms_certificate` post can be used\n * when `$preview_template` is `true`.\n *\n * @since 6.0.0\n *\n * @param WP_Post|int|null $post             A WP_Post object or a WP_Post ID. A falsy value will use the current global `$post` object (if one exists).\n * @param boolean          $preview_template If `true`, allows loading an `llms_certificate` post type for previewing the template.\n * @return LLMS_User_Certificate|boolean Returns the LLMS_User_Certificate object for the given post. Returns `false` if the post doesn't exist or is\n *                                       not of the expected post type.\n */\nfunction llms_get_certificate( $post = null, $preview_template = false ) {\n\n\t$post = get_post( $post );\n\tif ( ! $post ) {\n\t\treturn false;\n\t}\n\n\tif ( 'llms_my_certificate' === $post->post_type || ( 'llms_certificate' === $post->post_type && $preview_template ) ) {\n\t\treturn new LLMS_User_Certificate( $post );\n\t}\n\n\treturn false;\n}\n\n/**\n * Retrieve the content of a certificate.\n *\n * This allows utilizing the `LLMS_User_Certificate` class with an `llms_certificate` post type to render a preview\n * of the certificate template. The saved `post_content` will be merged (using the current user's information).\n *\n * This function is intended for use on the certificate's front-end display template. In order to retrieve the\n * raw content use `LLMS_User_Certificate->get( 'content' )` or `WP_Post->post_content`.\n *\n * @since 2.2.0\n * @since 3.18.0 Unknown.\n * @since 6.0.0 Use `llms_get_certificate()` and `LLMS_User_Certificate` methods.\n *                If this function is used out of the intended certificate context this will now\n *                return an empty string, whereas previously it returned the content of the post.\n * @since 6.4.0 Fixed issue with merge codes in reusable blocks by merging *after* filtering the post content.\n *\n * @param integer $id WP Post ID of the cert (optional if used within a loop).\n * @return string\n */\nfunction llms_get_certificate_content( $id = 0 ) {\n\n\t$content = '';\n\n\t$certificate = llms_get_certificate( $id, true );\n\tif ( $certificate ) {\n\n\t\t// If `$id` was empty to use the global, ensure an id is available in filter on the return.\n\t\t$id = $certificate->get( 'id' );\n\n\t\t// Get raw content because we filter it again below.\n\t\t$content = $certificate->get( 'content', true );\n\t}\n\n\t/** WordPress core filter documented at {@link https://developer.wordpress.org/reference/hooks/the_content/}. */\n\t$content = apply_filters( 'the_content', $content );\n\n\t// Get merged content for templates.\n\tif ( 'llms_certificate' === get_post_type( $id ) ) {\n\t\t$content = $certificate->merge_content( $content );\n\t}\n\n\t/**\n\t * Filter the `post_content` of a certificate or certificate template.\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Added the `$certificate` parameter.\n\t *\n\t * @param string                     $content     The certificate content.\n\t * @param int                        $id          The ID of the certificate.\n\t * @param bool|LLMS_User_Certificate $certificate Certificate object or `false` if the post couldn't be found.\n\t */\n\treturn apply_filters( 'lifterlms_certificate_content', $content, $id, $certificate );\n}\n\n/**\n * Retrieves a list of fonts available for use in certificates.\n *\n * @since 6.0.0\n * @since 6.11.0 Added internal call for certificate fonts, with external option enabled.\n *\n * @return array[] {\n *     Array of font definition arrays. The array key is the font's unique id.\n *\n *     @type string      $name The human-readable name of the font.\n *     @type string|null $href The href used to load the font or `null` for system or default fonts.\n *     @type string|null $css  The CSS `font-family` rule value.\n * }\n */\nfunction llms_get_certificate_fonts() {\n\t/**\n\t * Determines whether or not webfonts are loaded from Google CDNs.\n\t *\n\t * @since 6.11.0\n\t *\n\t * @param bool $use_g_fonts If `true`, fonts are loaded from Google, otherwise they are loaded from the local site.\n\t */\n\t$use_g_fonts = apply_filters( 'llms_use_google_webfonts', false );\n\t$serif       = '\"Iowan Old Style\", \"Apple Garamond\", Baskerville, \"Times New Roman\", \"Droid Serif\", Times, \"Source Serif Pro\", serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"';\n\n\t$fonts = array(\n\n\t\t// Default fonts.\n\t\t'sans'                => array(\n\t\t\t'name'       => __( 'Sans-serif', 'lifterlms' ),\n\t\t\t'href'       => null,\n\t\t\t// From https://systemfontstack.com.\n\t\t\t'fontFamily' => '-apple-system, BlinkMacSystemFont, \"avenir next\", avenir, \"segoe ui\", \"helvetica neue\", helvetica, Ubuntu, roboto, noto, arial, sans-serif',\n\t\t),\n\t\t'serif'               => array(\n\t\t\t'name'       => __( 'Serif', 'lifterlms' ),\n\t\t\t'href'       => null,\n\t\t\t// From https://systemfontstack.com.\n\t\t\t'fontFamily' => $serif,\n\t\t),\n\n\t\t// Newspaper-style display fonts.\n\t\t'pirata-one'          => array(\n\t\t\t'name'       => 'Pirata One',\n\t\t\t'href'       => $use_g_fonts ? 'https://fonts.googleapis.com/css2?family=Pirata+One&display=swap' : LLMS_PLUGIN_URL . 'assets/css/pirata-one.css?ver=v22',\n\t\t\t'fontFamily' => '\"Pirata One\", ' . $serif,\n\t\t),\n\t\t'unifraktur-maguntia' => array(\n\t\t\t'name'       => 'UnifrakturMaguntia',\n\t\t\t'href'       => $use_g_fonts ? 'https://fonts.googleapis.com/css2?family=UnifrakturMaguntia&display=swap' : LLMS_PLUGIN_URL . 'assets/css/unifraktur-maguntia.css?ver=v16',\n\t\t\t'fontFamily' => '\"UnifrakturMaguntia\", ' . $serif,\n\t\t),\n\n\t\t// Cursive-style handwriting fonts.\n\t\t'dancing-script'      => array(\n\t\t\t'name'       => 'Dancing Script',\n\t\t\t'href'       => $use_g_fonts ? 'https://fonts.googleapis.com/css2?family=Dancing+Script&display=swap' : LLMS_PLUGIN_URL . 'assets/css/dancing-script.css?ver=v24',\n\t\t\t'fontFamily' => '\"Dancing Script\", ' . $serif,\n\t\t),\n\t\t'imperial-script'     => array(\n\t\t\t'name'       => 'Imperial Script',\n\t\t\t'href'       => $use_g_fonts ? 'https://fonts.googleapis.com/css2?family=Imperial+Script&display=swap' : LLMS_PLUGIN_URL . 'assets/css/imperial-script.css?ver=v24',\n\t\t\t'fontFamily' => '\"Imperial Script\", ' . $serif,\n\t\t),\n\n\t);\n\n\t/**\n\t * Filters the list of fonts available to certificates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array[] $fonts Array of font definitions, {@see llms_get_certificate_fonts()}.\n\t */\n\n\treturn apply_filters( 'llms_certificate_fonts', $fonts );\n}\n\n/**\n * Retrieve an array of image data for a certificate background image\n *\n * If no image found, will default to the LifterLMS placeholder (which can be filtered for a custom placeholder).\n *\n * @since 2.2.0\n * @since 6.0.0 Use `LLMS_User_Certificate::get_background_image()`.\n *\n * @param int $id Optional. WP Certificate Post ID. Default is 0.\n *                When not provide the current post id will be used.\n * @return array Associative array of certificate image details\n */\nfunction llms_get_certificate_image( $id = 0 ) {\n\n\t$id   = ( $id ) ? $id : get_the_ID();\n\t$cert = new LLMS_User_Certificate( $id );\n\treturn $cert->get_background_image();\n}\n\n/**\n * Retrieve a list of merge codes that can be used in certificate templates.\n *\n * @since 6.0.0\n * @since 6.1.0 Changed `{current_date}` label from 'Earned Date' to 'Current Date' and added `{earned_date}` merge code.\n *\n * @return string[] Associative array of merge codes where the array key is the merge code and the array value is a name / description of the merge code.\n */\nfunction llms_get_certificate_merge_codes() {\n\n\t/**\n\t * Filters the list of available merge codes for certificates.\n\t *\n\t * @since 9.1.0\n\t *\n\t * @param array[]        $codes  Associative array of merge codes where the array key is the merge code and the array value is a name / description of the merge code.\n\t */\n\treturn apply_filters(\n\t\t'llms_certificate_available_merge_codes',\n\t\tarray(\n\t\t\t'{site_title}'     => __( 'Site Title', 'lifterlms' ),\n\t\t\t'{site_url}'       => __( 'Site URL', 'lifterlms' ),\n\t\t\t'{current_date}'   => __( 'Current Date', 'lifterlms' ),\n\t\t\t'{earned_date}'    => __( 'Earned Date', 'lifterlms' ),\n\t\t\t'{first_name}'     => __( 'Student First Name', 'lifterlms' ),\n\t\t\t'{last_name}'      => __( 'Student Last Name', 'lifterlms' ),\n\t\t\t'{email_address}'  => __( 'Student Email', 'lifterlms' ),\n\t\t\t'{student_id}'     => __( 'Student User ID', 'lifterlms' ),\n\t\t\t'{user_login}'     => __( 'Student Username', 'lifterlms' ),\n\t\t\t'{certificate_id}' => __( 'Certificate ID', 'lifterlms' ),\n\t\t\t'{sequential_id}'  => __( 'Sequential Certificate ID', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Retrieves registered certificate orientations.\n *\n * @since 6.0.0\n *\n * @return array Key value array where the array key is the orientation ID and the value is the\n *               translated name of the orientation.\n */\nfunction llms_get_certificate_orientations() {\n\n\t$orientations = array(\n\t\t'portrait'  => __( 'Portrait', 'lifterlms' ),\n\t\t'landscape' => __( 'Landscape', 'lifterlms' ),\n\t);\n\n\t/**\n\t * Filters the list of available certificate orientations.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $orientations Array of orientations.\n\t */\n\treturn apply_filters( 'llms_certificate_orientations', $orientations );\n}\n\n/**\n * Retrieve the next sequential ID for a given certificate template and optionally increment it.\n *\n * If there's no existing ID, a default ID of 1 will be used. This can be customized using the filter `llms_certificate_sequential_id_starting_number`.\n *\n * When an increment is requested, the new incremented ID will be automatically persisted to the database.\n *\n * @since 6.0.0\n *\n * @param integer $template_id WP_Post ID of the certificate template (`llms_certificate`) post.\n * @param boolean $increment   Whether or not to increment the current ID.\n * @return int\n */\nfunction llms_get_certificate_sequential_id( $template_id, $increment = false ) {\n\n\t$key    = '_llms_sequential_id';\n\t$update = $increment;\n\t$id     = absint( get_post_meta( $template_id, $key, true ) );\n\n\t// No id, get the initial ID.\n\tif ( ! $id ) {\n\n\t\t/**\n\t\t * Determines the default starting number for the a certificate's sequential ID.\n\t\t *\n\t\t * The returned number *must* be an absolute integer (zero included). The returned value will be\n\t\t * passed through `absint()` to sanitize the filtered value.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param int $starting_id The starting number.\n\t\t * @param int $template_id WP_Post ID of the certificate template.\n\t\t */\n\t\t$starting_id = apply_filters( 'llms_certificate_sequential_id_starting_number', 1, $template_id );\n\t\t$id          = absint( $starting_id );\n\t\t$update      = true;\n\n\t}\n\n\tif ( $update ) {\n\t\tupdate_post_meta( $template_id, $key, $increment ? $id + 1 : $id );\n\t}\n\n\t/**\n\t * Filters the sequential ID number for a given certificate template.\n\t *\n\t * The returned number *must* be an absolute integer. The returned value will be\n\t * passed through `absint()` to sanitize the filtered value.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $id          The sequential ID.\n\t * @param int $template_id WP_Post ID of the certificate template.\n\t */\n\treturn absint( apply_filters( 'llms_certificate_sequential_id', $id, $template_id ) );\n}\n\n/**\n * Retrieves a list of registered certificate sizes.\n *\n * @since 6.0.0\n *\n * @return {\n *     Array of sizes. The array key is the size's unique ID.\n *\n *     @type string $name   The translated name for the size.\n *     @type float  $width  The portrait width dimension of the size.\n *     @type float  $height The portrait height dimension of the size.\n *     @type string $unit   The unit used for the dimensions of the size. Must be the ID of a unit registered via {@see llms_get_certificate_units()}.\n * }\n */\nfunction llms_get_certificate_sizes() {\n\n\t$sizes = array(\n\t\t// ISO 216 sizes.\n\t\t'A3'           => array(\n\t\t\t'name'   => _x( 'A3', 'Paper size name', 'lifterlms' ),\n\t\t\t'width'  => 297,\n\t\t\t'height' => 420,\n\t\t\t'unit'   => 'mm',\n\t\t),\n\t\t'A4'           => array(\n\t\t\t'name'   => _x( 'A4', 'Paper size name', 'lifterlms' ),\n\t\t\t'width'  => 210,\n\t\t\t'height' => 297,\n\t\t\t'unit'   => 'mm',\n\t\t),\n\t\t'A5'           => array(\n\t\t\t'name'   => _x( 'A5', 'Paper size name', 'lifterlms' ),\n\t\t\t'width'  => 148,\n\t\t\t'height' => 210,\n\t\t\t'unit'   => 'mm',\n\t\t),\n\t\t// North American sizes.\n\t\t'LETTER'       => array(\n\t\t\t'name'   => _x( 'Letter', 'Paper size name', 'lifterlms' ),\n\t\t\t'width'  => 8.5,\n\t\t\t'height' => 11,\n\t\t\t'unit'   => 'in',\n\t\t),\n\t\t'LEGAL'        => array(\n\t\t\t'name'   => _x( 'Legal', 'Paper size name', 'lifterlms' ),\n\t\t\t'width'  => 8.5,\n\t\t\t'height' => 14,\n\t\t\t'unit'   => 'in',\n\t\t),\n\t\t'LEDGER'       => array(\n\t\t\t'name'   => _x( 'Ledger', 'Paper size name', 'lifterlms' ),\n\t\t\t'width'  => 11,\n\t\t\t'height' => 17,\n\t\t\t'unit'   => 'in',\n\t\t),\n\t\t'USER_DEFINED' => array(\n\t\t\t'name'   => __( 'User defined', 'lifterlms' ),\n\t\t\t'width'  => get_option( 'lifterlms_certificate_default_user_defined_width', 400 ),\n\t\t\t'height' => get_option( 'lifterlms_certificate_default_user_defined_height', 400 ),\n\t\t\t'unit'   => get_option( 'lifterlms_certificate_default_user_defined_unit', 'mm' ),\n\t\t),\n\t);\n\n\t/**\n\t * Filters registered certificate size options.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $sizes Array of registered sizes.\n\t */\n\treturn apply_filters( 'llms_certificate_sizes', $sizes );\n}\n\n/**\n * Retrieves units available for certificate dimensions.\n *\n * @since 6.0.0\n *\n * @link https://developer.mozilla.org/en-US/docs/Web/CSS/length\n *\n * @return {\n *     Array of unit information. The array key is the unit ID, which should be a valid absolute length CSS unit.\n *\n *     @type string $name   Translated name of the unit.\n *     @type string $symbol Translated symbol used when displaying dimensions with the unit.\n.* }\n */\nfunction llms_get_certificate_units() {\n\n\t$units = array(\n\t\t'in' => array(\n\t\t\t'name'   => __( 'Inches', 'lifterlms' ),\n\t\t\t'symbol' => _x( '\"', 'Symbol for inches', 'lifterlms' ),\n\t\t),\n\t\t'mm' => array(\n\t\t\t'name'   => __( 'Millimeters', 'lifterlms' ),\n\t\t\t'symbol' => _x( 'mm', 'Symbol for millimeters', 'lifterlms' ),\n\t\t),\n\t);\n\n\t/**\n\t * Filters the list of certificate dimension units.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $units Array of available units.\n\t */\n\treturn apply_filters( 'llms_certificate_units', $units );\n}\n\n/**\n * Retrieve the title of a certificate\n *\n * This function is intended for use on the certificate's front-end display template.\n *\n * @since 2.2.0\n * @since 6.0.0 Use `LLMS_User_Certificate()` to retrieve the title for earned certificates.\n *\n * @param int $id WP Certificate Post ID. When not provide the current post id will be used.\n * @return string The title of the certificate.\n */\nfunction llms_get_certificate_title( $id = 0 ) {\n\n\t$id          = $id ? $id : get_the_ID();\n\t$title       = '';\n\t$certificate = llms_get_certificate( $id, false );\n\tif ( $certificate ) {\n\t\t$title = $certificate->get( 'title' );\n\t} elseif ( 'llms_certificate' === get_post_type( $id ) ) {\n\t\t$title = get_post_meta( $id, '_llms_certificate_title', true );\n\t}\n\n\t/**\n\t * Filter the title of a certificate or certificate template.\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Added the `$certificate` parameter.\n\t *\n\t * @param string $title The certificate title.\n\t * @param int    $id    The ID of the certificate.\n\t */\n\treturn apply_filters( 'lifterlms_certificate_title', $title, $id );\n}\n\n/**\n * Determines whether or not the block editor can be used to build certificates.\n *\n * The JS used for certificates in the block editor relies on WP functions and APIs available\n * since WordPress 5.8. Earlier versions of WordPress won't work.\n *\n * @since 6.0.0\n *\n * @return boolean\n */\nfunction llms_is_block_editor_supported_for_certificates() {\n\n\tglobal $wp_version;\n\t$is_supported = version_compare( $wp_version, '5.8-src', '>=' );\n\n\t/**\n\t * Filters whether or not the block editor can be used for building certificates.\n\t *\n\t * By default, `$is_supported` will be `true` for WordPress 5.8 or later and false for versions less than\n\t * 5.8.\n\t *\n\t * This filter may be used to disable the block editor on later versions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param boolean $is_supported Whether or not the block editor is supported.\n\t */\n\treturn apply_filters( 'llms_block_editor_supported_for_certificates', $is_supported );\n}\n\n/**\n * Register the custom \"print_certificate\" image size\n *\n * @since 2.2.0\n *\n * @return void\n */\nfunction llms_register_certificate_image_size() {\n\n\t$width  = get_option( 'lifterlms_certificate_bg_img_width', '800' );\n\t$height = get_option( 'lifterlms_certificate_bg_img_height', '616' );\n\n\tadd_image_size( 'lifterlms_certificate_background', $width, $height, true );\n}\nadd_action( 'after_setup_theme', 'llms_register_certificate_image_size' );\n"
  },
  {
    "path": "includes/functions/llms.functions.course.php",
    "content": "<?php\n/**\n * LifterLMS Course Functions\n *\n * @package LifterLMS/Functions\n *\n * @since Unknown\n * @version 3.37.13\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get course object\n *\n * @since Unknown\n * @since 3.37.13 Use `LLMS_Course` in favor of the deprecated `LLMS_Course_Factory::get_course()` method.\n *\n * @param WP_Post|int|false $the_course Course post object or id. If `false` uses the global `$post` object.\n * @param array             $args       Arguments to pass to the LLMS_Course Constructor.\n * @return LLMS_Course\n */\nfunction get_course( $the_course = false, $args = array() ) {\n\n\tif ( ! $the_course ) {\n\t\tglobal $post;\n\t\t$the_course = $post;\n\t}\n\n\treturn new LLMS_Course( $the_course, $args );\n\n}\n\n/**\n * Get lesson object\n *\n * @since Unknown\n * @since 3.37.13 Use `LLMS_Lesson` in favor of the deprecated `LLMS_Course_Factory::get_lesson()` method.\n *\n * @param WP_Post|int|false $the_lesson Lesson post object or id. If `false` uses the global `$post` object.\n * @param array             $args        Arguments to pass to the LLMS_Lesson Constructor.\n * @return LLMS_Lesson\n */\nfunction get_lesson( $the_lesson = false, $args = array() ) {\n\n\tif ( ! $the_lesson ) {\n\t\tglobal $post;\n\t\t$the_lesson = $post;\n\t}\n\n\treturn new LLMS_Lesson( $the_lesson, $args );\n\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.currency.php",
    "content": "<?php\n/**\n * Currency and Price related functions for LifterLMS Products\n *\n * @package LifterLMS/Functions\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get the currency selected\n *\n * @since 1.0.0\n * @since 3.0.0 Added USD as default when no option is set.\n *\n * @return string Currency code.\n */\nfunction get_lifterlms_currency() {\n\n\t/**\n\t * Hook Summary\n\t *\n\t * Hook description.\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $currency Currency code.\n\t */\n\treturn apply_filters( 'lifterlms_currency', get_option( 'lifterlms_currency', 'USD' ) );\n}\n\n/**\n * Get the name of a currency\n *\n * @since  3.0.0\n *\n * @param string $currency A currency code.\n * @return string\n */\nfunction get_lifterlms_currency_name( $currency = '' ) {\n\n\tif ( ! $currency ) {\n\t\t$currency = get_lifterlms_currency();\n\t}\n\t$name = '';\n\n\t$currencies = get_lifterlms_currencies();\n\tif ( isset( $currencies[ $currency ] ) ) {\n\t\t$name = $currencies[ $currency ];\n\t}\n\n\t/**\n\t * Filters the name of the given currency.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $name     Currency name.\n\t * @param string $currency Currency code.\n\t */\n\treturn apply_filters( 'lifterlms_currency_name', $name, $currency );\n}\n\n/**\n * Get array of supported currencies\n *\n * @since Unknown\n * @since 3.0.0 Unknown.\n * @since 5.0.0 Use currency list provided in `languages/currencies.php`.\n *\n * @return array\n */\nfunction get_lifterlms_currencies() {\n\n\t$currencies = require LLMS_PLUGIN_DIR . 'languages/currencies.php';\n\n\t/**\n\t * Filters the list of available currencies\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $currencies A list of currency codes to currency names. See \"languages/currencies.php\" for details.\n\t */\n\treturn apply_filters( 'lifterlms_currencies', $currencies );\n}\n\n/**\n * Get Currency Symbol text code\n *\n * @since Unknown\n * @since 3.30.3 Removed duplicate key \"MAD\".\n * @since 5.0.0 Retrieve symbols list from `llms_get_currency_symbols()`.\n *              If a symbol cannot be found for the supplied currency code, return the code instead of an empty string.\n *\n * @param  string $currency Currency Code.\n * @return string\n */\nfunction get_lifterlms_currency_symbol( $currency = '' ) {\n\n\tif ( ! $currency ) {\n\t\t$currency = get_lifterlms_currency();\n\t}\n\n\t$symbols         = llms_get_currency_symbols();\n\t$currency_symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : $currency;\n\n\t/**\n\t * Filters the symbol for the specified currency\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $currency_symbol Currency symbol. If the symbol contains non-Latin characters, the HTML entity code for those characters will be used.\n\t * @param string $currency        Currency code.\n\t */\n\treturn apply_filters( 'lifterlms_currency_symbol', $currency_symbol, $currency );\n}\n\n/**\n * Get the number of decimals places used for prices as defined by the setting.\n *\n * @since 3.0.0\n *\n * @return int\n */\nfunction get_lifterlms_decimals() {\n\treturn absint( apply_filters( 'lifterlms_decimals', get_option( 'lifterlms_decimals', 2 ) ) );\n}\n\n/**\n * Retrieve the character used as a decimal separator\n *\n * @since 3.0.0\n *\n * @return string\n */\nfunction get_lifterlms_decimal_separator() {\n\treturn apply_filters( 'lifterlms_decimal_separator', get_option( 'lifterlms_decimal_separator', '.' ) );\n}\n\n/**\n * Retrieve the setting for trimming zero value decimals from the end of prices\n *\n * @since  3.0.0\n *\n * @return string Either 'yes' or 'no'.\n */\nfunction get_lifterlms_trim_zero_decimals() {\n\treturn apply_filters( 'lifterlms_trim_zero_decimals', get_option( 'lifterlms_trim_zero_decimals', 'no' ) );\n}\n\n/**\n * Get a format string that can be passed to printf or sprintf to format prices\n *\n * The format string is created using user-defined price formatting settings.\n *\n * @since  3.0.0\n *\n * @return string\n */\nfunction get_lifterlms_price_format() {\n\t$pos    = get_option( 'lifterlms_currency_position', 'left' );\n\t$format = '%1$s%2$s';\n\tswitch ( $pos ) {\n\t\tcase 'left':\n\t\t\t$format = '%1$s%2$s';\n\t\t\tbreak;\n\t\tcase 'right':\n\t\t\t$format = '%2$s%1$s';\n\t\t\tbreak;\n\t\tcase 'left_space':\n\t\t\t$format = '%1$s&nbsp;%2$s';\n\t\t\tbreak;\n\t\tcase 'right_space':\n\t\t\t$format = '%2$s&nbsp;%1$s';\n\t\t\tbreak;\n\t}\n\treturn apply_filters( 'lifterlms_price_format', $format, $pos );\n}\n\n/**\n * Retrieve the character used as the thousands separator\n *\n * @since 3.0.0\n *\n * @return string\n */\nfunction get_lifterlms_thousand_separator() {\n\treturn apply_filters( 'lifterlms_thousand_separator', get_option( 'lifterlms_thousand_separator', '.' ) );\n}\n\n/**\n * Retrieve a list of available currency symbols\n *\n * Retrieves the symbols list from `languages/currency-symbols.php`.\n *\n * @since 5.0.0\n *\n * @return array Array of currency codes to their symbols. Any non-Latin characters found in a symbol are returned as an HTML character entity code.\n */\nfunction llms_get_currency_symbols() {\n\n\t$symbols = require LLMS_PLUGIN_DIR . 'languages/currency-symbols.php';\n\n\t/**\n\t * Filters the list of currency symbols\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $symbols List of currency codes to their symbol. See \"languages/currency-symbols.php\" for details.\n\t */\n\treturn apply_filters( 'lifterlms_currency_symbols', $symbols );\n}\n\n/**\n * Get a formatted price price\n *\n * @since Unknown\n * @since 3.0.0 Unknown.\n *\n * @param int   $price Price to display.\n * @param array $args  Array of arguments.\n * @return string\n */\nfunction llms_price( $price, $args = array() ) {\n\n\textract(\n\t\tapply_filters(\n\t\t\t'llms_price_args',\n\t\t\tarray_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'currency'           => '',\n\t\t\t\t\t'decimal_separator'  => get_lifterlms_decimal_separator(),\n\t\t\t\t\t'decimals'           => get_lifterlms_decimals(),\n\t\t\t\t\t'format'             => get_lifterlms_price_format(),\n\t\t\t\t\t'thousand_separator' => get_lifterlms_thousand_separator(),\n\t\t\t\t\t'trim_zeros'         => get_lifterlms_trim_zero_decimals(),\n\t\t\t\t),\n\t\t\t\t$args\n\t\t\t)\n\t\t)\n\t);\n\n\t$negative = $price < 0;\n\t$price    = apply_filters( 'raw_lifterlms_price', floatval( $negative ? $price * -1 : $price ) );\n\t$price    = apply_filters( 'formatted_lifterlms_price', number_format( $price, $decimals, $decimal_separator, $thousand_separator ), $price, $decimals, $decimal_separator, $thousand_separator );\n\n\tif ( 'yes' === $trim_zeros && $decimals > 0 ) {\n\t\t$price = llms_trim_zeros( $price );\n\t}\n\n\t$formatted_price = ( $negative ? '-' : '' ) . sprintf( $format, '<span class=\"llms-price-currency-symbol\">' . get_lifterlms_currency_symbol( $currency ) . '</span>', $price );\n\t$r               = '<span class=\"lifterlms-price\">' . $formatted_price . '</span>';\n\n\treturn apply_filters( 'llms_price', $r, $price, $args );\n}\n\n/**\n * Get a simple string (no html) based on the output of llms_price\n *\n * @since Unknown\n * @since 3.0.0 Unknown.\n *\n * @param int   $price Price to display.\n * @param array $args  Array of arguments.\n * @return string\n */\nfunction llms_price_raw( $price, $args = array() ) {\n\treturn html_entity_decode( wp_strip_all_tags( llms_price( $price, $args ) ) );\n}\n\n/**\n * Trim trailing zeros off a price\n *\n * @since 3.0.0\n *\n * @param mixed $price Price string.\n * @return string\n */\nfunction llms_trim_zeros( $price ) {\n\treturn preg_replace( '/' . preg_quote( get_lifterlms_decimal_separator(), '/' ) . '0++$/', '', $price );\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.favorite.php",
    "content": "<?php\n/**\n * LifterLMS Favorite Functions\n *\n * @package LifterLMS/Functions\n *\n * @since 7.5.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get Favorites Count.\n *\n * @since 7.5.0\n *\n * @param bool|int $object_id WP Post ID of the Lesson. If not supplied it will default to the current post ID.\n * @return int\n */\nfunction llms_get_object_total_favorites( $object_id = false ) {\n\n\tglobal $wpdb;\n\n\t// Getting ID from Global Post object.\n\tif ( ! $object_id ) {\n\t\t$object_id = get_the_ID();\n\t}\n\n\t$res = $wpdb->get_var(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT COUNT(DISTINCT meta_id) FROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\t\tWHERE post_id = %d AND meta_key = %s ORDER BY updated_date DESC\",\n\t\t\t$object_id,\n\t\t\t'_favorite'\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\treturn $res;\n\n}\n\n/**\n * Filter Hook to enable the Favorite feature.\n *\n * @since 7.5.0\n *\n * @return bool True if favorites are enabled, false otherwise.\n */\nfunction llms_is_favorites_enabled() {\n\n\t$favorite_enabled = llms_parse_bool( get_option( 'lifterlms_favorites', 'no' ) );\n\n\t/**\n\t * Filter to enable/disable the Favorite feature.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param bool $favorite_enabled True if favorites are enabled, false otherwise.\n\t */\n\treturn apply_filters( 'llms_favorites_enabled', $favorite_enabled );\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.log.php",
    "content": "<?php\n/**\n * Logging & Related Functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.0.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Copy a log file that is greater than or equal to the max allowed log file size\n *\n * If the log file's size is larger than the maximum allowed log file size (5MB) it will rename the log, adding\n * the current timestamp as a suffix and `.bk` to the extension.\n *\n * Future logs to the same file will result in a new logfile being created, ensuring that log files never grow\n * too large which could cause performance issues during reads and writes.\n *\n * @since 4.5.0\n *\n * @see llms_backup_logs()\n *\n * @param string $handle Log file handle.\n * @return null|boolean|string Returns `null` if the log file is not larger than the max size, `false` if an error is encountered,\n *                             and the new log file path on success.\n */\nfunction llms_backup_log( $handle ) {\n\n\t$file = llms_get_log_path( $handle );\n\t$size = file_exists( $file ) ? filesize( $file ) : 0;\n\n\t/**\n\t * Filter the max filesize of a log file before the log is backed up\n\t *\n\t * The value of this filter, `$maxsize` is an integer representing the maximum number of megabytes\n\t * a file can be before it is split.\n\t *\n\t * @since 4.5.0\n\t *\n\t * @param int $maxsize Maximum file size (in MB). The default value is `5` (5MB).\n\t */\n\t$maxsize = absint( apply_filters( 'llms_log_max_filesize', 5 ) ) * 1000 * 1000;\n\n\tif ( $size >= $maxsize ) {\n\n\t\t$copy = str_replace( '.log', sprintf( '-%d.log.bk', time() ), $file );\n\n\t\t/**\n\t\t * Filter the name of a log file copy that's being backed up because it's reached the maximum allowed size\n\t\t *\n\t\t * While it is possible to change the extension of the log file (`.log.bk`), it is not recommended. The cron\n\t\t * which creates copies filters out `.log.bk` so that it doesn't scan backups and attempt to split them\n\t\t * again (infinitely).\n\t\t *\n\t\t * @since 4.5.0\n\t\t *\n\t\t * @param string $copy   Full path for the copy log file (the backup).\n\t\t * @param string $file   Full path for the original log file.\n\t\t * @param string $handle Log file handle.\n\t\t */\n\t\t$copy = apply_filters( 'llms_log_split_file_name', $copy, $file, $handle );\n\t\tif ( rename( $file, $copy ) ) {\n\n\t\t\t/**\n\t\t\t * Action triggered immediately following the creation of a logfile backup.\n\t\t\t *\n\t\t\t * @since 4.5.0\n\t\t\t *\n\t\t\t * @param string $copy   Full path for the copy log file (the backup).\n\t\t\t * @param string $file   Full path for the original log file.\n\t\t\t * @param string $handle Log file handle.\n\t\t\t */\n\t\t\tdo_action( 'llms_log_file_backup_created', $copy, $file, $handle );\n\n\t\t\treturn $copy;\n\t\t}\n\t}\n\n\treturn null;\n\n}\n\n/**\n * Backup all log files in the LifterLMS log directory\n *\n * This function scans the `LLMS_LOG_DIR` and passes each log file to `llms_backup_log()` to\n * create backups of each log file.\n *\n * It does not include logs with the `.log.bk` extension as those logs are logs created by this process\n * and don't need to be scanned again.\n *\n * @since 4.5.0\n *\n * @see llms_backup_log()\n *\n * @return void\n */\nfunction llms_backup_logs() {\n\n\tforeach ( glob( LLMS_LOG_DIR . '*.log' ) as $file ) {\n\n\t\t// Get the handle from the file path.\n\t\t$parts = explode( '-', basename( $file, '.log' ) );\n\t\tif ( $parts ) {\n\t\t\tllms_backup_log( implode( '-', array_slice( $parts, 0, -1 ) ) );\n\t\t}\n\t}\n\n}\nadd_action( 'llms_backup_logs', 'llms_backup_logs' );\n\n/**\n * Retrieve a string representing a PHP callable\n *\n * This can be used to log callables regardless of the callable format.\n *\n * @since 5.2.0\n *\n * @param mixed $callable PHP callable.\n * @return string\n */\nfunction llms_get_callable_name( $callable ) {\n\n\t// Function name or static class -> method: 'function' or 'class::method'.\n\tif ( is_string( $callable ) ) {\n\t\treturn $callable;\n\t}\n\n\tif ( is_array( $callable ) && ! empty( $callable ) ) {\n\n\t\t// Class and class method: [ $class, 'method' ]. (phpcs:ignore Squiz.PHP.CommentedOutCode.Found).\n\t\tif ( is_object( $callable[0] ) ) {\n\t\t\treturn get_class( $callable[0] ) . '->' . $callable[1];\n\t\t}\n\n\t\t// Static class + method: [ 'class', 'method' ]. (phpcs:ignore Squiz.PHP.CommentedOutCode.Found).\n\t\treturn implode( '::', $callable );\n\n\t}\n\n\t// Invokable class: $class. (phpcs:ignore Squiz.PHP.CommentedOutCode.Found).\n\tif ( is_object( $callable ) ) {\n\t\treturn get_class( $callable );\n\t}\n\n\treturn 'Unknown';\n\n}\n\n/**\n * Retrieve the full path to the log file for a given log handle\n *\n * @since 3.0.0\n *\n * @param string $handle Log handle.\n * @return string\n */\nfunction llms_get_log_path( $handle ) {\n\n\treturn trailingslashit( LLMS_LOG_DIR ) . $handle . '-' . sanitize_file_name( wp_hash( $handle ) ) . '.log';\n\n}\n\n/**\n * Log arbitrary messages to a log file\n *\n * @since 1.0.0\n * @since 3.7.5 Unknown.\n *\n * @param mixed  $message Data to log.\n * @param string $handle  Allow creation of multiple log files by handle.\n * @return boolean\n */\nfunction llms_log( $message, $handle = 'llms' ) {\n\n\t/**\n\t * Filter a log data before it's written to the logger.\n\t *\n\t * This hook filters the log message in its raw format which may be a string, object, or array. To\n\t * filter the final log message after string conversion, use `llms_log_message_string`.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @see llms_log_message_string\n\t *\n\t * @param mixed  $message Data to log.\n\t * @param string $handle  Allow creation of multiple log files by handle.\n\t */\n\t$message = apply_filters( 'llms_log_message', $message, $handle );\n\n\t$ret = false;\n\t$fh  = fopen( llms_get_log_path( $handle ), 'a' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen\n\n\t// Open the file (creates it if it doesn't already exist).\n\tif ( $fh ) {\n\n\t\t$message = is_array( $message ) || is_object( $message ) ? print_r( $message, true ) : $message; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- This is intentional.\n\n\t\t/**\n\t\t * Filter a log message before it's written to the logger.\n\t\t *\n\t\t * This hook filters the log message in its final string format To filter the log message\n\t\t * before string conversion, use `llms_log_message`.\n\t\t *\n\t\t * @since 6.4.0\n\t\t *\n\t\t * @see llms_log_message\n\t\t *\n\t\t * @param string $message Log message string.\n\t\t * @param string $handle  Allow creation of multiple log files by handle.\n\t\t */\n\t\t$message = apply_filters( 'llms_log_message_string', $message, $handle );\n\n\t\t$ret = fwrite( $fh, gmdate( 'Y-m-d H:i:s' ) . ' - ' . $message . \"\\n\" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite\n\n\t\tfclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose\n\n\t}\n\n\treturn $ret ? true : false;\n\n}\n\n/**\n * Automatically anonymize a list of registered \"secure\" strings before writing logs.\n *\n * This function is a callback for the `llms_log_message_string` filter. It loads secure strings\n * defined in the `llms_secure_strings` filter and automatically anonymizes them when\n * they are found within the supplied log message.\n *\n * @since 6.4.0\n *\n * @access private\n *\n * @param string $message The string to log.\n * @param string $handle  Log file handle.\n * @return string\n */\nfunction _llms_secure_log_messages( $message, $handle ) {\n\n\t/**\n\t * Filters a list of \"secure\" strings which should be anonymized prior to logging.\n\t *\n\t * A plugin or theme that might log potentially sensitive data (such as API keys), the\n\t * API key strings can be registered with this filter to automatically be anonymized\n\t * if they are found within logs.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @param string[] $secure_strings An array of secure strings that should be anonymized.\n\t * @param string   $handle         The log handle. This can be used to only register strings for a specific log file.\n\t */\n\t$secure_strings = apply_filters( 'llms_secure_strings', array(), $handle );\n\n\t// Nothing to do.\n\tif ( empty( $secure_strings ) ) {\n\t\treturn $message;\n\t}\n\n\t$find    = array();\n\t$replace = array();\n\n\tforeach ( $secure_strings as $string ) {\n\t\tif ( false !== strpos( $message, $string ) ) {\n\t\t\t$find[]    = $string;\n\t\t\t$replace[] = llms_anonymize_string( $string );\n\t\t}\n\t}\n\n\t$message = str_replace( $find, $replace, $message );\n\n\treturn $message;\n\n}\nadd_filter( 'llms_log_message_string', '_llms_secure_log_messages', 999, 2 );\n"
  },
  {
    "path": "includes/functions/llms.functions.notice.php",
    "content": "<?php\n/**\n * Notice Functions\n *\n * Functions for managing front end notices (alert messages).\n *\n * @package LifterLMS/Functions\n *\n * @since unknown\n * @version 3.14.7\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Stores notice in llms_notices session\n *\n * @param  string $message     [The notice message]\n * @param  string $notice_type [notice type]\n * @return void\n * @since   1.0.0\n * @version 1.0.0\n */\nfunction llms_add_notice( $message, $notice_type = 'success' ) {\n\n\t$notices = llms()->session->get( 'llms_notices', array() );\n\n\tif ( 'success' === $notice_type ) {\n\t\t$message = apply_filters( 'lifterlms_add_message', $message );\n\t}\n\n\t$notices[ $notice_type ][] = apply_filters( 'lifterlms_add_' . $notice_type, $message );\n\n\tllms()->session->set( 'llms_notices', $notices );\n}\n\n/**\n * Clears all notices from session\n *\n * @return void\n * @since   1.0.0\n * @version 3.12.0\n */\nfunction llms_clear_notices() {\n\tllms()->session->set( 'llms_notices', array() );\n}\n\n/**\n * Retrieve an array of notice types\n *\n * @return   array\n * @since    1.0.0\n * @version  1.0.0\n */\nfunction llms_get_notice_types() {\n\treturn apply_filters( 'lifterlms_notice_types', array( 'debug', 'error', 'notice', 'success' ) );\n}\n\n/**\n * Gets messages and errors which are stored in the session, then clears them.\n *\n * @package LifterLMS/Functions\n *\n * @return   string\n * @since    3.0.0\n * @version  3.12.0\n */\nfunction llms_get_notices() {\n\n\t$all_notices  = apply_filters( 'lifterlms_print_notices', llms()->session->get( 'llms_notices', array() ) );\n\t$notice_types = llms_get_notice_types();\n\n\tob_start();\n\n\tforeach ( $notice_types as $notice_type ) {\n\t\tif ( llms_notice_count( $notice_type ) > 0 ) {\n\t\t\tllms_get_template(\n\t\t\t\t\"notices/{$notice_type}.php\",\n\t\t\t\tarray(\n\t\t\t\t\t'messages' => $all_notices[ $notice_type ],\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\tadd_action( 'shutdown', 'llms_clear_notices', 1 ); // Prior to shutdown functions executed by session manager.\n\n\treturn ob_get_clean();\n}\n\n\n/**\n * Returns a count of all current notices by type.\n *\n * @param  string $notice_type  Type of notice passed. IE: error, success, warning\n * @return int\n * @since   1.0.0\n * @version 1.0.0\n */\nfunction llms_notice_count( $notice_type = '' ) {\n\n\t$notice_count = 0;\n\n\t$all_notices = llms()->session->get( 'llms_notices', array() );\n\n\tif ( isset( $all_notices[ $notice_type ] ) ) {\n\n\t\t$notice_count = absint( count( $all_notices[ $notice_type ] ) );\n\n\t} elseif ( empty( $notice_type ) ) {\n\n\t\tforeach ( $all_notices as $notices ) {\n\t\t\t$notice_count += absint( count( $all_notices ) );\n\t\t}\n\t}\n\n\treturn $notice_count;\n}\n\n/**\n * Prints a single notice\n *\n * @param   string $message     [The notice message]\n * @param   string $notice_type [notice type]\n * @return  void\n * @since   1.0.0\n * @version 1.0.0\n */\nfunction llms_print_notice( $message, $notice_type = 'success' ) {\n\n\tif ( 'success' === $notice_type ) {\n\t\t$message = apply_filters( 'lifterlms_add_message', $message );\n\t}\n\n\tllms_get_template(\n\t\t\"notices/{$notice_type}.php\",\n\t\tarray(\n\t\t\t'messages' => array( apply_filters( 'lifterlms_add_' . $notice_type, $message ) ),\n\t\t)\n\t);\n}\n\n/**\n * Prints all notices\n *\n * @return  void\n * @since   1.0.0\n * @version 3.14.7\n */\nfunction llms_print_notices() {\n\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in templates.\n\techo llms_get_notices();\n\tllms_clear_notices();\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.order.php",
    "content": "<?php\n/**\n * Functions for LifterLMS Orders.\n *\n * @package LifterLMS/Functions\n *\n * @since 3.29.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Determine if a gateway can be used for a given LLMS_Access_Plan.\n *\n * @since 3.29.0\n * @since 7.0.0 Updated to utilize llms_can_gateway_be_used_for_plan_or_order().\n *\n * @param string           $gateway_id LLMS_Payment_Gateway ID.\n * @param LLMS_Access_Plan $plan       The access plan.\n * @return WP_Error|bool WP_Error on error, true on success.\n */\nfunction llms_can_gateway_be_used_for_plan( $gateway_id, $plan ) {\n\n\t$can_be_used = llms_can_gateway_be_used_for_plan_or_order( $gateway_id, $plan, true, ( 'manual' !== $gateway_id ) );\n\n\t/**\n\t * Filters whether or not a gateway can be used for a given access plan.\n\t *\n\t * @since 3.29.0\n\t * @since 7.0.0 The filter now runs on all possible return values instead of running only when the gateway can be used.\n\t *\n\t * @param boolean|WP_Error $can_be_used Whether or not the gateway can be used for the plan. This value will be `true`\n\t *                                      when the gateway can be used an an error object when it cannot.\n\t * @param string           $gateway_id  The LLMS_Payment_Gateway ID.\n\t * @param LLMS_Access_plan $plan        The access plan object.\n\t */\n\treturn apply_filters( 'llms_can_gateway_be_used_for_plan', $can_be_used, $gateway_id, $plan );\n}\n\n/**\n * Determines if a payment gateway can be used to process transactions for an LLMS_Order or an LLMS_Access_Plan.\n *\n *   + The plan/order must exist\n *   + The gateway must exist.\n *   + The gateway must be enabled unless `$enabled_only` is `false`.\n *   + The gateway must support the order/plan's type (recurring or single).\n *\n * @since 7.0.0\n * @since 7.5.0 Added check on whether a gateway can process a plan.\n *\n * @param string                          $gateway_id    Payment gateway ID.\n * @param LLMS_Order|LLMS_Access_Plan|int $plan_or_order The `WP_Post` id of a plan or order, a plan object, or an order object.\n * @param boolean                         $wp_err        Determines the return type when the gateway cannot be used.\n * @param boolean                         $enabled_only  If `true` requires the specified gateway to be enabled for use. This property\n *                                                       exists to ensure the manual payment gateway can be used to record free transactions\n *                                                       regardless of the gateway's status.\n * @return boolean|WP_Error Returns `true` if the gateway can be used. If the gateway cannot be used, returns `false` if `$wp_error` is\n *                          `false` and a `WP_Error` if `$wp_err` is `true`.\n */\nfunction llms_can_gateway_be_used_for_plan_or_order( $gateway_id, $plan_or_order, $wp_err = false, $enabled_only = true ) {\n\n\t$can_use = true;\n\n\t$plan_or_order = is_numeric( $plan_or_order ) ? llms_get_post( $plan_or_order ) : $plan_or_order;\n\t$err_data      = compact( 'gateway_id', 'plan_or_order' );\n\t$order         = is_a( $plan_or_order, 'LLMS_Order' ) ? $plan_or_order : null;\n\t$plan          = ! $order && is_a( $plan_or_order, 'LLMS_Access_Plan' ) ? $plan_or_order : null;\n\n\tif ( is_null( $order ) && is_null( $plan ) ) {\n\t\t$can_use = new WP_Error( 'post-invalid', __( 'A valid order or access plan must be supplied.', 'lifterlms' ), $err_data );\n\t} else {\n\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( $gateway_id );\n\t\tif ( ! $gateway ) {\n\t\t\t$can_use = new WP_Error( 'gateway-invalid', __( 'The selected payment gateway is not valid.', 'lifterlms' ), $err_data );\n\t\t} elseif ( $enabled_only && ! $gateway->is_enabled() ) {\n\t\t\t$can_use = new WP_Error( 'gateway-disabled', __( 'The selected payment gateway is not available.', 'lifterlms' ), $err_data );\n\t\t} elseif ( ! $gateway->can_process_access_plan( $plan, $order ) ) {\n\t\t\t// Check whether the gateway can process the plan or the order's plan (which is the plan at the moment of the order's creation).\n\t\t\t$can_use = new WP_Error( 'gateway-support-plan', __( 'The selected payment gateway is not available for the given plan.', 'lifterlms' ), $err_data );\n\t\t} elseif ( $plan_or_order->is_recurring() && ! $gateway->supports( 'recurring_payments' ) ) {\n\t\t\t$can_use = new WP_Error( 'gateway-support-recurring', __( 'The selected payment gateway does not support recurring payments.', 'lifterlms' ), $err_data );\n\t\t} elseif ( ! $plan_or_order->is_recurring() && ! $gateway->supports( 'single_payments' ) ) {\n\t\t\t$can_use = new WP_Error( 'gateway-support-single', __( 'The selected payment gateway does not support one-time payments.', 'lifterlms' ), $err_data );\n\t\t}\n\t}\n\n\t/**\n\t * Filters whether or not a gateway can be used for a given plan or order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param boolean|WP_Error $can_be_used Whether or not the gateway can be used for the plan. This value will be `true`\n\t *                                      when the gateway can be used an an error object when it cannot.\n\t * @param string           $gateway_id  The LLMS_Payment_Gateway ID.\n\t * @param LLMS_Access_plan $plan        The access plan object.\n\t */\n\t$can_use = apply_filters( 'llms_can_gateway_be_used_for_plan_or_order', $can_use, $gateway_id, $plan_or_order );\n\n\treturn is_wp_error( $can_use ) && ! $wp_err ? false : $can_use;\n}\n\n/**\n * Retrieve an LLMS Order ID by the associated order_key.\n *\n * @since 3.0.0\n * @since 3.30.1 Return `null` instead of `false` when requesting an `LLMS_Order` return and no order could be found.\n * @since 3.30.1 Return a real `int` (instead of a numeric string).\n *\n * @param string $key    The order key.\n * @param string $return Type of return, \"order\" for an instance of the LLMS_Order or \"id\" to return only the order ID.\n * @return mixed `null` when no order found, LLMS_Order when `$return` = 'order', or the WP_Post ID as an `int`.\n */\nfunction llms_get_order_by_key( $key, $return = 'order' ) {\n\n\tglobal $wpdb;\n\n\t$id = $wpdb->get_var( $wpdb->prepare( \"SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_llms_order_key' AND meta_value = %s\", $key ) ); // no-cache ok.\n\n\tif ( $id && 'order' === $return ) {\n\t\treturn new LLMS_Order( $id );\n\t}\n\n\t// Return an int not a numeric string.\n\treturn $id ? absint( $id ) : $id;\n}\n\n/**\n * Get the human readable status for a LifterLMS status.\n *\n * @since 3.0.0\n * @since 3.6.0 Unknown.\n *\n * @param string $status LifterLMS Order Status.\n * @return string\n */\nfunction llms_get_order_status_name( $status ) {\n\t$statuses = llms_get_order_statuses();\n\tif ( is_array( $statuses ) && isset( $statuses[ $status ] ) ) {\n\t\t$status = $statuses[ $status ];\n\t}\n\treturn apply_filters( 'lifterlms_get_order_status_name', $status );\n}\n\n/**\n * Retrieve an array of registered and available LifterLMS Order Post Statuses.\n *\n * @since 3.0.0\n * @since 3.19.0 Unknown.\n *\n * @param string $order_type Filter statuses which are specific to the supplied order type, defaults to any statuses.\n * @return array[]\n */\nfunction llms_get_order_statuses( $order_type = 'any' ) {\n\n\t$statuses = wp_list_pluck( LLMS_Post_Types::get_order_statuses(), 'label' );\n\n\t// Remove types depending on order type.\n\tswitch ( $order_type ) {\n\t\tcase 'recurring':\n\t\t\tunset( $statuses['llms-completed'] );\n\t\t\tbreak;\n\n\t\tcase 'single':\n\t\t\tunset( $statuses['llms-active'] );\n\t\t\tunset( $statuses['llms-expired'] );\n\t\t\tunset( $statuses['llms-on-hold'] );\n\t\t\tunset( $statuses['llms-pending-cancel'] );\n\t\t\tbreak;\n\t}\n\n\t/**\n\t * Filters the order statuses.\n\t *\n\t * @since Unknown.\n\t *\n\t * @param array[] $statuses   Array of order post status arrays.\n\t * @param string  $order_type The type of the order.\n\t */\n\treturn apply_filters( 'llms_get_order_statuses', $statuses, $order_type );\n}\n\n/**\n * Get the possible statuses of a given order.\n *\n * @since 5.4.0\n *\n * @param LLMS_Order $order The LLMS_Order instance.\n * @return array[]\n */\nfunction llms_get_possible_order_statuses( $order ) {\n\n\t$is_recurring = $order->is_recurring();\n\t$statuses     = llms_get_order_statuses( $is_recurring ? 'recurring' : 'single' );\n\n\t// Limit the possible status for recurring orders whose product ID doesn't exist anymore.\n\tif ( $is_recurring && ! llms_get_post( $order->get( 'product_id' ) ) ) {\n\t\tunset( $statuses['llms-active'] );\n\t\tunset( $statuses['llms-on-hold'] );\n\t\tunset( $statuses['llms-pending'] );\n\t\tunset( $statuses['llms-pending-cancel'] );\n\t}\n\n\treturn $statuses;\n}\n\n/**\n * Locates an order by email address and access plan ID.\n *\n * Used during AJAX checkout order creation when users are not created until the gateway confirms success.\n *\n * Ensures that only a single pending order for a given plan and email address will exist at any given time.\n *\n * @since 7.0.0\n *\n * @param string $email   An email address.\n * @param int    $plan_id Access plan WP_Post ID.\n * @return null|int Returns the post id if found, otherwise returns `null`.\n */\nfunction llms_locate_order_for_email_and_plan( $email, $plan_id ) {\n\n\t$query = new WP_Query(\n\t\tarray(\n\t\t\t'post_type'      => 'llms_order',\n\t\t\t'post_status'    => 'llms-pending',\n\t\t\t'fields'         => 'ids',\n\t\t\t'posts_per_page' => 1,\n\t\t\t'no_found_rows'  => true,\n\t\t\t'meta_query'     => array(\n\t\t\t\t'relation' => 'AND',\n\t\t\t\tarray(\n\t\t\t\t\t'key'   => '_llms_billing_email',\n\t\t\t\t\t'value' => $email,\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'key'   => '_llms_plan_id',\n\t\t\t\t\t'value' => $plan_id,\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t);\n\n\treturn $query->posts[0] ?? null;\n}\n\n/**\n * Find an existing order for a given plan by a given user.\n *\n * @since 3.30.1\n *\n * @param int $user_id The WP_User ID.\n * @param int $plan_id The Access Plan post ID.\n * @return mixed null if no order found, WP_Post ID as an int if found\n */\nfunction llms_locate_order_for_user_and_plan( $user_id, $plan_id ) {\n\n\tglobal $wpdb;\n\n\t// Query.\n\t$id = $wpdb->get_var(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT ID FROM {$wpdb->prefix}posts AS p\n\t\t\t JOIN {$wpdb->prefix}postmeta AS pm_user ON pm_user.post_id = p.ID AND pm_user.meta_key = '_llms_user_id'\n\t\t\t JOIN {$wpdb->prefix}postmeta AS pm_plan ON pm_plan.post_id = p.ID AND pm_plan.meta_key = '_llms_plan_id'\n\t\t\t WHERE p.post_type = 'llms_order'\n\t\t\t   AND pm_user.meta_value = %d\n\t\t\t   AND pm_plan.meta_value = %d\n\t\t\t;\",\n\t\t\t$user_id,\n\t\t\t$plan_id\n\t\t)\n\t); // db-cache ok.\n\n\t// Return an int not a numeric string.\n\treturn $id ? absint( $id ) : $id;\n}\n\n/**\n * Setup a pending order which can be passed to an LLMS_Payment_Gateway for processing.\n *\n * @since 3.29.0\n * @since 4.2.0 Prevent double displaying a notice to already enrolled students in the product being purchased.\n * @since 4.21.1 Sanitize coupon code prior to outputting it in error messages.\n * @since 5.0.0 Use `llms_update_user()` instead of deprecated `LLMS_Person_Handler::update()`.\n *\n * @param array $data {\n *     Data used to create a pending order.\n *\n *     @type int    plan_id         (Required) LLMS_Access_Plan ID.\n *     @type array  customer        (Required). Array of customer information formatted to be passed to `LLMS_Person_Handler::update()` or `llms_register_user()`\n *     @type string agree_to_terms  (Required if `llms_are_terms_and_conditions_required()` are required) If terms & conditions are required this should be \"yes\" for agreement.\n *     @type string payment_gateway (Optional) ID of a registered LLMS_Payment_Gateway which will be used to process the order.\n *     @type string coupon_code     (Optional) Coupon code to be applied to the order.\n * }\n * @return array\n */\nfunction llms_setup_pending_order( $data = array() ) {\n\n\t/**\n\t * Filters the order data before setting up the pending order.\n\t *\n\t * @since Unknown.\n\t *\n\t * @param array $data Array of input data from a checkout form.\n\t */\n\t$data = apply_filters( 'llms_before_setup_pending_order', $data );\n\n\t// Request keys that can be submitted with or without the `llms_` prefix.\n\t$keys = array(\n\t\t'llms_agree_to_terms',\n\t\t'llms_coupon_code',\n\t\t'llms_plan_id',\n\t);\n\tforeach ( $keys as $key ) {\n\t\tif ( isset( $data[ $key ] ) ) {\n\t\t\t$data[ str_replace( 'llms_', '', $key ) ] = $data[ $key ];\n\t\t}\n\t}\n\n\t$err = new WP_Error();\n\n\t// Check t & c if configured.\n\tif ( llms_are_terms_and_conditions_required() ) {\n\t\tif ( ! isset( $data['agree_to_terms'] ) || ! llms_parse_bool( $data['agree_to_terms'] ) ) {\n\t\t\t$err->add( 'terms-violation', sprintf( __( 'You must agree to the %s to continue.', 'lifterlms' ), get_the_title( get_option( 'lifterlms_terms_page_id' ) ) ) );\n\t\t\treturn $err;\n\t\t}\n\t}\n\n\t// We must have a plan_id to proceed.\n\tif ( empty( $data['plan_id'] ) ) {\n\t\t$err->add( 'missing-plan-id', __( 'Missing an Access Plan ID.', 'lifterlms' ) );\n\t\treturn $err;\n\t}\n\n\t// Validate the plan is a real plan.\n\t$plan = llms_get_post( absint( $data['plan_id'] ) );\n\tif ( ! $plan || 'llms_access_plan' !== $plan->get( 'type' ) ) {\n\t\t$err->add( 'invalid-plan-id', __( 'Invalid Access Plan ID.', 'lifterlms' ) );\n\t\treturn $err;\n\t}\n\n\t// Used later.\n\t$coupon_id = null;\n\t$coupon    = false;\n\n\t// If a coupon is being used, validate it.\n\tif ( ! empty( $data['coupon_code'] ) ) {\n\n\t\t$data['coupon_code'] = sanitize_text_field( $data['coupon_code'] );\n\n\t\t$coupon_id = llms_find_coupon( $data['coupon_code'] );\n\n\t\t// Coupon couldn't be found.\n\t\tif ( ! $coupon_id ) {\n\t\t\t$err->add( 'coupon-not-found', sprintf( __( 'Coupon code \"%s\" not found.', 'lifterlms' ), $data['coupon_code'] ) );\n\t\t\treturn $err;\n\t\t}\n\n\t\t// Coupon is real, make sure it's valid for the current plan.\n\t\t$coupon = llms_get_post( $coupon_id );\n\t\t$valid  = $coupon->is_valid( $data['plan_id'] );\n\n\t\t// If the coupon has a validation error, return an error message.\n\t\tif ( is_wp_error( $valid ) ) {\n\t\t\t$err->add( 'invalid-coupon', $valid->get_error_message() );\n\t\t\treturn $err;\n\t\t}\n\t}\n\n\t// If payment is required, verify we have a gateway.\n\tif ( $plan->requires_payment( $coupon_id ) && empty( $data['payment_gateway'] ) ) {\n\t\t$err->add( 'missing-gateway-id', __( 'No payment method selected.', 'lifterlms' ) );\n\t\treturn $err;\n\t}\n\n\t$gateway_id    = empty( $data['payment_gateway'] ) ? 'manual' : $data['payment_gateway'];\n\t$gateway_error = llms_can_gateway_be_used_for_plan( $gateway_id, $plan );\n\tif ( is_wp_error( $gateway_error ) ) {\n\t\treturn $gateway_error;\n\t}\n\n\tif ( empty( $data['customer'] ) ) {\n\t\t$err->add( 'missing-customer', __( 'Missing customer information.', 'lifterlms' ) );\n\t\treturn $err;\n\t}\n\n\t// Update the customer.\n\tif ( ! empty( $data['customer']['user_id'] ) ) {\n\t\t$person_id = llms_update_user( $data['customer'], 'checkout', compact( 'plan' ) );\n\t} else {\n\t\t$person_id = llms_register_user( $data['customer'], 'checkout', true, compact( 'plan' ) );\n\t}\n\n\t// Validation or registration issues.\n\tif ( is_wp_error( $person_id ) ) {\n\t\treturn $person_id;\n\t}\n\n\t// This will likely never actually happen unless there's something very strange afoot.\n\tif ( ! is_numeric( $person_id ) ) {\n\n\t\t$err->add( 'account-creation', __( 'An unknown error occurred when attempting to create an account, please try again.', 'lifterlms' ) );\n\t\treturn $err;\n\n\t}\n\n\t// Ensure the new user isn't enrolled in the product being purchased.\n\t/**\n\t * Filter to allow checkout if already enrolled.\n\t *\n\t * @param bool $block_checkout Whether to block checkout if already enrolled.\n\t * @param LLMS_Access_Plan $plan The access plan.\n\t *\n\t * @since 9.1.2\n\t */\n\tif ( llms_is_user_enrolled( $person_id, $plan->get( 'product_id' ) ) && apply_filters( 'llms_checkout_block_enrolled_checkout', true, $plan ) ) {\n\n\t\t$product = $plan->get_product();\n\t\t$err->add(\n\t\t\t'already-enrolled',\n\t\t\tsprintf(\n\t\t\t\t// Translators: %2$s = The product type (course/membership); %1$s = product permalink.\n\t\t\t\t__( 'You already have access to this %2$s! Visit your dashboard <a href=\"%1$s\">here.</a>', 'lifterlms' ),\n\t\t\t\tllms_get_page_url( 'myaccount' ),\n\t\t\t\t$product->get_post_type_label()\n\t\t\t)\n\t\t);\n\t\t// Prevent double displaying a notice to already enrolled students in the product being purchased.\n\t\tadd_filter( 'llms_display_checkout_form_enrolled_students_notice', '__return_false' );\n\n\t\treturn $err;\n\t}\n\n\t$person  = llms_get_student( $person_id );\n\t$gateway = llms()->payment_gateways()->get_gateway_by_id( $gateway_id );\n\n\t/**\n\t * Filter the return of pending order setup data.\n\t *\n\t * @since 3.30.1\n\t *\n\t * @param array $setup {\n\t *     Data used to create the pending order.\n\t *\n\t *     @type LLMS_Student $person Student object.\n\t *     @type LLMS_Access_Plan $plan Access plan object.\n\t *     @type LLMS_Payment_Gateway $gateway Instance of the selected gateway.\n\t *     @type LLMS_Coupon|false $coupon Coupon object or false if none used.\n\t * }\n\t * @param array $data Array of input data from a checkout form.\n\t */\n\treturn apply_filters( 'llms_after_setup_pending_order', compact( 'person', 'plan', 'gateway', 'coupon' ), $data );\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.page.php",
    "content": "<?php\n/**\n * Page functions.\n *\n * @package LifterLMS/Functions\n *\n * @since 1.0.0\n * @version 6.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get url for when user cancels payment.\n *\n * @since 1.0.0\n *\n * @return string\n */\nfunction llms_cancel_payment_url() {\n\n\t$cancel_payment_url = esc_url( get_permalink( llms_get_page_id( 'checkout' ) ) );\n\treturn apply_filters( 'lifterlms_checkout_confirm_payment_url', $cancel_payment_url );\n}\n\n/**\n * Get url for redirect when user confirms payment.\n *\n * @since 1.0.0\n * @since 3.38.0 Added redirect query string parameter.\n * @since 5.9.0 Avoid passing `null` to `urldecode()` when no redirect is set in the `$_GET` array.\n *\n * @return string\n */\nfunction llms_confirm_payment_url( $order_key = null ) {\n\n\t$args = array();\n\n\tif ( $order_key ) {\n\t\t$args['order'] = $order_key;\n\t}\n\n\t$redirect = llms_filter_input( INPUT_GET, 'redirect', FILTER_VALIDATE_URL );\n\tif ( $redirect ) {\n\t\t$args['redirect'] = rawurlencode( urldecode( $redirect ) );\n\t}\n\n\t$url = llms_get_endpoint_url( 'confirm-payment', '', get_permalink( llms_get_page_id( 'checkout' ) ) );\n\tif ( $args ) {\n\t\t$url = add_query_arg( $args, $url );\n\t}\n\n\t/**\n\t * Filter the checkout confirmation URL.\n\t *\n\t * @since 1.0.0\n\t *\n\t * @param string $url URL to the payment confirmation screen.\n\t */\n\treturn apply_filters( 'lifterlms_checkout_confirm_payment_url', $url );\n}\n\n/**\n * Retrieve the full URL to a LifterLMS endpoint.\n *\n * @since 1.0.0\n * @since 3.26.3 Unknown.\n * @since 5.9.0 Update to ensure the generated URL has (or doesn't have) a trailing slash based on the site's permalink settings.\n * @since 6.3.0 Try to build the correct URL even when `get_permalink()` returns an empty string (e.g. in BuddyPress profile endpoints).\n *              Prefer faster `strpos()` over `strstr()` since we only need to know if a substring is contained in a string.\n *\n * @param string $endpoint  ID of the endpoint, eg \"view-courses\".\n * @param string $value     Endpoint query parameter value.\n * @param string $permalink Base URL to append the endpoint to. Optional, uses the current page when not supplied.\n * @return string\n */\nfunction llms_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) {\n\n\t// Map endpoint to options.\n\t$vars     = llms()->query->get_query_vars();\n\t$endpoint = $vars[ $endpoint ] ?? $endpoint;\n\n\t/**\n\t * In our dashboard endpoints, get_permalink() always returns the dashboard page permalink:\n\t * something like https://example.com/dashboard/\n\t * which is the base URL to append the endpoint to.\n\t */\n\t$permalink         = $permalink ? $permalink : get_permalink();\n\t$is_base_permalink = true;\n\n\t/**\n\t * No permalink available, e.g. in BuddyPress profile endpoint.\n\t *\n\t * We need to get the base URL to append the endpoint to, starting from\n\t * the current requested URL.\n\t */\n\tif ( ! $permalink && ! empty( $_SERVER['REQUEST_URI'] ) ) {\n\t\t$permalink         = home_url( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL ) );\n\t\t$is_base_permalink = false;\n\t}\n\n\tif ( get_option( 'permalink_structure' ) ) {\n\n\t\t$query_string = '';\n\n\t\tif ( false !== strpos( $permalink, '?' ) ) {\n\t\t\t$query_string = '?' . wp_parse_url( $permalink, PHP_URL_QUERY );\n\t\t\t$permalink    = current( explode( '?', $permalink ) );\n\t\t}\n\n\t\t/**\n\t\t * Normalize the permalink when not referring to the base URL.\n\t\t */\n\t\tif ( ! $is_base_permalink ) {\n\t\t\t$permalink = _llms_normalize_endpoint_base_url( $permalink, $endpoint );\n\t\t}\n\n\t\t$url = trailingslashit( $permalink );\n\n\t\tif ( $value ) {\n\t\t\t$url .= trailingslashit( $endpoint ) . user_trailingslashit( $value );\n\t\t} else {\n\t\t\t$url .= user_trailingslashit( $endpoint );\n\t\t}\n\n\t\t$url .= $query_string;\n\n\t} else {\n\t\t$url = add_query_arg( $endpoint, $value, $permalink );\n\t}\n\n\t/**\n\t * Filter the final endpoint URL.\n\t *\n\t * @since 1.0.0\n\t * @since 5.9.0 Added `$value` and `$permalink` parameters.\n\t *\n\t * @param string $url       The endpoint URL.\n\t * @param string $endpoint  ID of the endpoint.\n\t * @param string $value     Endpoint query parameter value.\n\t * @param string $permalink Base URL to append the endpoint to. Optional, uses the current page when not supplied.\n\t */\n\treturn apply_filters( 'lifterlms_get_endpoint_url', $url, $endpoint, $value, $permalink );\n}\n\n/**\n * Normalize the endpoint base URL.\n *\n * E.g., in the BuddyPress profile's tab, on my grades, page 2, it'll look like\n * //example.com/members/admin/courses/my-courses/page/2/\n *\n * We then need to normalize the endpoint base URL, which means\n * removing /my-courses/ (the endpoint) and the pagination information /page/2/.\n *\n * @since 6.3.0\n * @access private\n *\n * @param string $url      URL to extract the Base URL, to append the endpoint to, from.\n * @param string $endpoint Slug of the endpoint, eg \"my-courses\".\n * @return string\n */\nfunction _llms_normalize_endpoint_base_url( $url, $endpoint ) {\n\n\t$_url = untrailingslashit( $url );\n\n\t// Remove pagination.\n\tglobal $wp_rewrite;\n\t$page       = llms_get_paged_query_var();\n\t$pagination = '/' . $wp_rewrite->pagination_base . '/' . $page;\n\n\tif ( $page > 1 && substr( $_url, -1 * strlen( $pagination ) ) === $pagination ) { // PHP8: str_ends_with(string $haystack, string $needle).\n\t\t$_url = substr( $_url, 0, -1 * strlen( $pagination ) );\n\t}\n\n\t// Remove the endpoint slug from the URL if it's its last part.\n\tif ( substr( $_url, -1 * strlen( $endpoint ) ) === $endpoint ) { // PHP8: str_ends_with(string $haystack, string $needle).\n\t\t$url = substr( $_url, 0, -1 * strlen( $endpoint ) );\n\t}\n\n\treturn $url;\n}\n\n/**\n * Retrieve the WordPress Page ID of a LifterLMS Core Page.\n *\n * Available core pages are:\n * + checkout (formerly \"shop\")\n * + courses (Course catalog)\n * + myaccount (Student Dashboard)\n * + memberships (Membership catalog)\n *\n * @since 1.0.0\n *\n * @param string $page The page slug/name.\n * @return int The WP_Post ID of the page or -1 if the page is not found.\n */\nfunction llms_get_page_id( $page ) {\n\n\t// Normalize some pages to make more sense without having to migrate options.\n\tif ( 'courses' === $page ) {\n\t\t$page = 'shop';\n\t}\n\n\t$id = get_option( 'lifterlms_' . $page . '_page_id' );\n\n\t/**\n\t * Filter the ID of the requested LifterLMS Page.\n\t *\n\t * The dynamic portion of this filter, {$page}, refers to the LifterLMS page slug/name.\n\t *\n\t * Note that, historically, the course catalog was called the \"shop\" and therefore when requesting\n\t * the filter will be \"lifterlms_get_shop_page_id\" instead of \"lifterlms_get_courses_page_id\".\n\t *\n\t * @since 1.0.0\n\t *\n\t * @param int|string $id The WP_Post ID of the requested page or an empty string if the page doesn't exist.\n\t */\n\t$page = apply_filters( \"lifterlms_get_{$page}_page_id\", $id );\n\n\treturn $page ? absint( $page ) : -1;\n}\n\n\n/**\n * Retrieve the URL for a LifterLMS Page.\n *\n * EG: 'checkout', 'memberships', 'myaccount', 'courses' etc...\n *\n * @since  3.0.0\n *\n * @param string $page Name of the page.\n * @param array  $args Optional array of query arguments that can be passed to add_query_arg().\n * @return string\n */\nfunction llms_get_page_url( $page, $args = array() ) {\n\t$url = add_query_arg( $args, get_permalink( llms_get_page_id( $page ) ) );\n\treturn $url ? $url : '';\n}\n\n\n/**\n * Returns the url to the lost password endpoint url.\n *\n * @since Unknown\n *\n * @return string\n */\nfunction llms_lostpassword_url( $lostpassword_url ) {\n\tif ( llms_get_page_id( 'myaccount' ) <= 0 || ! get_permalink( llms_get_page_id( 'myaccount' ) ) ) {\n\t\treturn $lostpassword_url;\n\t}\n\n\treturn llms_get_endpoint_url( 'lost-password', '', get_permalink( llms_get_page_id( 'myaccount' ) ) );\n}\nadd_filter( 'lostpassword_url', 'llms_lostpassword_url', 10, 1 );\n\n/**\n * Returns the page number query var for the current request.\n *\n * `paged`:\n * Used on the homepage, blogpage, archive pages and pages to calculate pagination.\n * 1st page is 0 and from there the number correspond to the page number\n * `page`:\n * Used on a static front page and single pages for pagination (`<!--nextpage-->`).\n * Pagination on these pages works the same, a static front page is treated as single page on pagination.\n *\n * @since 6.3.0\n *\n * @return int\n */\nfunction llms_get_paged_query_var() {\n\n\tif ( get_query_var( 'paged' ) ) {\n\t\t$paged = get_query_var( 'paged' );\n\t} elseif ( get_query_var( 'page' ) ) {\n\t\t$paged = get_query_var( 'page' );\n\t} else {\n\t\t$paged = 1;\n\t}\n\treturn (int) $paged;\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.person.php",
    "content": "<?php\n/**\n * Person Functions\n *\n * Functions for managing users in the LifterLMS system.\n *\n * @package LifterLMS/Functions\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Determines whether or not a user can bypass enrollment, drip, and prerequisite restrictions.\n *\n * @since 3.7.0\n * @since 3.9.0 Unknown.\n * @since 5.9.0 Added optional second parameter `$post_id`.\n *\n * @param LLMS_Student|WP_User|int $user    LLMS_Student, WP_User, or WP User ID, if none supplied get_current_user() will be used.\n * @param integer                  $post_id A WP_Post ID to check permissions against. If supplied, in addition to the user's role\n *                                          being allowed to bypass the restrictions, the user must also have `edit_post` capabilities\n *                                          for the requested post.\n * @return bool\n */\nfunction llms_can_user_bypass_restrictions( $user = null, $post_id = null ) {\n\n\t$user = llms_get_student( $user );\n\n\tif ( ! $user ) {\n\t\treturn false;\n\t}\n\n\t$roles = get_option( 'llms_grant_site_access', '' );\n\tif ( ! $roles ) {\n\t\t$roles = array();\n\t}\n\n\tif ( ! array_intersect( $user->get_user()->roles, $roles ) ) {\n\t\treturn false;\n\t}\n\n\tif ( $post_id && ! user_can( $user->get( 'id' ), 'edit_post', $post_id ) ) {\n\t\treturn false;\n\t}\n\n\treturn true;\n\n}\n\n/**\n * Checks LifterLMS user capabilities against an object\n *\n * @since 3.13.0\n * @since 4.5.0 Use strict array comparison.\n *\n * @param string $cap    Capability name.\n * @param int    $obj_id WP_Post or WP_User ID.\n * @return bool\n */\nfunction llms_current_user_can( $cap, $obj_id = null ) {\n\n\t$caps  = LLMS_Roles::get_all_core_caps();\n\t$grant = false;\n\n\tif ( in_array( $cap, $caps, true ) ) {\n\n\t\t// If the user has the cap, maybe do some additional checks.\n\t\tif ( current_user_can( $cap ) ) {\n\n\t\t\tswitch ( $cap ) {\n\n\t\t\t\tcase 'view_lifterlms_reports':\n\t\t\t\t\t// Can view others reports so its okay.\n\t\t\t\t\tif ( current_user_can( 'view_others_lifterlms_reports' ) ) {\n\t\t\t\t\t\t$grant = true;\n\n\t\t\t\t\t\t// Can only view their own reports check if the student is their instructor.\n\t\t\t\t\t} elseif ( $obj_id ) {\n\n\t\t\t\t\t\t$instructor = llms_get_instructor();\n\t\t\t\t\t\t$student    = llms_get_student( $obj_id );\n\t\t\t\t\t\tif ( $instructor && $student ) {\n\t\t\t\t\t\t\tforeach ( $instructor->get_posts(\n\t\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t'ids'\n\t\t\t\t\t\t\t) as $id ) {\n\t\t\t\t\t\t\t\tif ( $student->get_enrollment_status( $id ) ) {\n\t\t\t\t\t\t\t\t\t$grant = true;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\n\t\t\t\t// No other checks needed.\n\t\t\t\tdefault:\n\t\t\t\t\t$grant = true;\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Filters whether or not the current user can perform the requested action\n\t *\n\t * The dynamic portion of this hook, `$cap`, refers to the requested user capability.\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param bool $grant  Whether or not the requested capability is granted to the user.\n\t * @param int  $obj_id WP_Post or WP_User ID.\n\t */\n\treturn apply_filters( \"llms_current_user_can_{$cap}\", $grant, $obj_id );\n\n}\n\n/**\n * Delete LifterLMS Student's Enrollment record related to a given product.\n *\n * @since 3.33.0\n *\n * @see `LLMS_Student->delete_enrollment()` the class method wrapped by this function.\n *\n * @param int    $user_id    WP User ID.\n * @param int    $product_id WP Post ID of the Course or Membership.\n * @param string $trigger    Optional. Only delete the student enrollment if the original enrollment trigger matches the submitted value.\n *                           Passing \"any\" will remove regardless of enrollment trigger.\n * @return bool Whether or not the enrollment records have been successfully removed.\n */\nfunction llms_delete_student_enrollment( $user_id, $product_id, $trigger = 'any' ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->delete_enrollment( $product_id, $trigger );\n}\n\n/**\n * Disables admin bar on front end\n *\n * @since 1.0.0\n * @since 3.27.0 Unknown\n *\n * @param bool $show_admin_bar default value (true).\n * @return bool\n */\nfunction llms_disable_admin_bar( $show_admin_bar ) {\n\t/**\n\t * Filter whether or not the WP Admin Bar is disabled for users\n\t *\n\t * By default, the admin bar is disabled for all users except those with the `edit_posts` or `manage_lifterlms` capabilities.\n\t *\n\t * @since Unknown\n\t *\n\t * @param bool $disabled Whether or not the admin bar should be disabled.\n\t */\n\tif ( apply_filters( 'lifterlms_disable_admin_bar', true ) && ! ( current_user_can( 'edit_posts' ) || current_user_can( 'manage_lifterlms' ) ) ) {\n\t\t$show_admin_bar = false;\n\t}\n\treturn $show_admin_bar;\n}\nadd_filter( 'show_admin_bar', 'llms_disable_admin_bar', 10 );\n\n\n/**\n * Enroll a WordPress user in a course or membership\n *\n * @since 2.2.3\n * @since 3.0.0 Added `$trigger` parameter.\n *\n * @see LLMS_Student->enroll() the class method wrapped by this function\n *\n * @param int    $user_id    WP User ID.\n * @param int    $product_id WP Post ID of the Course or Membership.\n * @param string $trigger    String describing the event that triggered the enrollment.\n * @return bool\n */\nfunction llms_enroll_student( $user_id, $product_id, $trigger = 'unspecified' ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->enroll( $product_id, $trigger );\n}\n\n/**\n * Get an LLMS_Instructor\n *\n * @since 3.13.0\n *\n * @param mixed $user WP_User ID, instance of WP_User, or instance of any instructor class extending this class.\n * @return LLMS_Instructor|false LLMS_Instructor instance on success, false if user not found\n */\nfunction llms_get_instructor( $user = null ) {\n\t$student = new LLMS_Instructor( $user );\n\treturn $student->exists() ? $student : false;\n}\n\n/**\n * Retrieve the translated name of minimum accepted password strength for student passwords\n *\n * @since 3.0.0\n * @since 5.0.0 Remove database call to deprecated option and add the $strength parameter.\n *\n * @param string $strength Optional. Password strength value to translate. Default is 'strong'.\n * @return string\n */\nfunction llms_get_minimum_password_strength_name( $strength = 'strong' ) {\n\n\t$opts = array(\n\t\t'strong'    => __( 'strong', 'lifterlms' ),\n\t\t'medium'    => __( 'medium', 'lifterlms' ),\n\t\t'weak'      => __( 'weak', 'lifterlms' ),\n\t\t'very-weak' => __( 'very weak', 'lifterlms' ),\n\t);\n\n\t$name = isset( $opts[ $strength ] ) ? $opts[ $strength ] : $strength;\n\n\t/**\n\t * Filter the name of the password strength\n\t *\n\t * The dynamic portion of this hook, `$strength`, can be either \"strong\", \"medium\", \"weak\" or \"very-weak\".\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param $string $name Translated name of the password strength value.\n\t */\n\treturn apply_filters( 'llms_get_minimum_password_strength_name_' . $strength, $name );\n\n}\n\n/**\n * Get an LLMS_Student.\n *\n * @since 3.8.0\n * @since 3.9.0 Unknown\n * @since 7.1.0 Added the `$autoload` parameter.\n *\n * @param mixed $user     WP_User ID, instance of WP_User, or instance of any student class extending this class.\n * @param bool  $autoload If `true` and `$user` input is empty, the user will be loaded from `get_current_user_id()`.\n *                        If `$user` is not empty then this parameter has no impact.\n * @return LLMS_Student|false LLMS_Student instance on success, false if user not found.\n */\nfunction llms_get_student( $user = null, $autoload = true ) {\n\t$student = new LLMS_Student( $user, $autoload );\n\treturn $student->exists() ? $student : false;\n}\n\n/**\n * Retrieve a list of disallowed usernames.\n *\n * @since 5.0.0\n * @since 6.0.0 Removed the deprecated `llms_usernames_blacklist` filter hook.\n *\n * @return string[]\n */\nfunction llms_get_usernames_blocklist() {\n\n\t$list = array( 'admin', 'test', 'administrator', 'password', 'testing' );\n\n\t/**\n\t * Modify the list of disallowed usernames\n\t *\n\t * If a user attempts to create a new account with any username found in this list they will receive an error and will not\n\t * be able to register the account.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string[] $list List of banned usernames.\n\t */\n\treturn apply_filters( 'llms_usernames_blocklist', $list );\n\n}\n\n/**\n * Checks if user is currently enrolled in cours\n *\n * @since Unknown\n * @since 3.3.1 Updated to use `LLMS_Student->is_enrolled()`.\n *\n * @see LLMS_Student->is_complete()\n *\n * @param int $user_id      WP User ID of the user.\n * @param int $object_id    WP Post ID of a Course, Section, or Lesson.\n * @param int $object_type  Type, either Course, Section, or Lesson.\n * @return bool Returns `true` if complete, otherwise `false`.\n */\nfunction llms_is_complete( $user_id, $object_id, $object_type = 'course' ) {\n\t$s = new LLMS_Student( $user_id );\n\treturn $s->is_complete( $object_id, $object_type );\n}\n\n/**\n * Checks if user is currently enrolled courses, sections, lessons, or memberships.\n *\n * @since Unknown\n * @since 3.25.0 Unknown.\n * @since 7.1.0 From now on this function will always return false for non existing users,\n *               e.g. deleted users.\n *\n * @see LLMS_Student->is_enrolled()\n *\n * @param int       $user_id    WP_User ID.\n * @param int|int[] $product_id WP Post ID of a Course, Lesson, or Membership or array of multiple IDs.\n * @param string    $relation   Comparator for enrollment check.\n *                              All = user must be enrolled in all $product_ids.\n *                              Any = user must be enrolled in at least one of the $product_ids.\n * @param bool      $use_cache  If true, returns cached data if available, if false will run a db query.\n * @return bool\n */\nfunction llms_is_user_enrolled( $user_id, $product_id, $relation = 'all', $use_cache = true ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->exists() ?\n\t\t$student->is_enrolled( $product_id, $relation, $use_cache ) :\n\t\tfalse;\n}\n\n/**\n * Mark a lesson, section, course, or track as complete\n *\n * @since 3.3.1\n *\n * @see LLMS_Student->mark_complete()\n *\n * @param int    $user_id     WP User ID.\n * @param int    $object_id   WP Post ID of the Lesson, Section, Track, or Course.\n * @param string $object_type Object type [lesson|section|course|track].\n * @param string $trigger     String describing the event that triggered marking the object as complete.\n * @return bool\n */\nfunction llms_mark_complete( $user_id, $object_id, $object_type, $trigger = 'unspecified' ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->mark_complete( $object_id, $object_type, $trigger );\n}\n\n/**\n * Mark a lesson, section, course, or track as incomplete\n *\n * @since 3.5.0\n *\n * @see LLMS_Student->mark_incomplete()\n *\n * @param int    $user_id     WP User ID.\n * @param int    $object_id   WP Post ID of the Lesson, Section, Track, or Course.\n * @param string $object_type Object type [lesson|section|course|track].\n * @param string $trigger     String describing the event that triggered marking the object as incomplete.\n * @return bool\n */\nfunction llms_mark_incomplete( $user_id, $object_id, $object_type, $trigger = 'unspecified' ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->mark_incomplete( $object_id, $object_type, $trigger );\n}\n\n/**\n * Mark an object as favorite.\n *\n * @since 7.5.0\n *\n * @see LLMS_Student->mark_favorite()\n *\n * @param int    $user_id     WP User ID.\n * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n * @param string $object_type The object type, currently only 'lesson'.\n * @return bool\n */\nfunction llms_mark_favorite( $user_id, $object_id, $object_type ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->mark_favorite( $object_id, $object_type );\n}\n\n/**\n * Mark a lesson as unfavorite.\n *\n * @since 7.5.0\n *\n * @see LLMS_Student->mark_unfavorite()\n *\n * @param int    $user_id     WP User ID.\n * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n * @param string $object_type The object type, currently only 'lesson'.\n * @return bool\n */\nfunction llms_mark_unfavorite( $user_id, $object_id, $object_type ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->mark_unfavorite( $object_id, $object_type );\n}\n\n/**\n * Parses the password reset cookie.\n *\n * This is the cookie set when a user uses the password reset link found in a reset password email. The query string\n * vars in the link (user login and reset key) are parsed and stored in this cookie.\n *\n * @since 5.0.0\n * @since 5.1.2 Fixed typos in error messages.\n *\n * @return array|WP_Error On success, returns an associative array containing the keys \"key\" and \"login\", on error\n *                        returns a WP_Error.\n */\nfunction llms_parse_password_reset_cookie() {\n\n\tif ( ! isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) ) {\n\t\treturn new WP_Error( 'llms_password_reset_no_cookie', __( 'The password reset key could not be found. Please reset your password again if needed.', 'lifterlms' ) );\n\t}\n\n\t$parsed = array_map( 'sanitize_text_field', explode( ':', wp_unslash( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ), 2 ) );  // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized\n\tif ( 2 !== count( $parsed ) ) {\n\t\treturn new WP_Error( 'llms_password_reset_invalid_cookie', __( 'The password reset key is in an invalid format. Please reset your password again if needed.', 'lifterlms' ) );\n\t}\n\n\t$uid = $parsed[0];\n\t$key = $parsed[1];\n\n\t$user  = get_user_by( 'ID', $uid );\n\t$login = $user ? $user->user_login : '';\n\t$user  = check_password_reset_key( $key, $login );\n\n\tif ( is_wp_error( $user ) ) {\n\t\t// Error code is either \"llms_password_reset_invalid_key\" or \"llms_password_reset_expired_key\".\n\t\treturn new WP_Error( sprintf( 'llms_password_reset_%s', $user->get_error_code() ), __( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', 'lifterlms' ) );\n\t}\n\n\t// Success.\n\treturn compact( 'key', 'login' );\n\n}\n\n/**\n * Register a new user\n *\n * @since 3.0.0\n * @since 5.0.0 Use `LLMS_Form_Handler()` for registration.\n *\n * @param array  $data   Array of registration data.\n * @param string $screen The screen to be used for the validation template, accepts \"registration\" or \"checkout\".\n * @param bool   $signon If true, signon the newly created user.\n * @param array  $args   Additional arguments passed to the short-circuit filter.\n * @return int|WP_Error\n */\nfunction llms_register_user( $data = array(), $screen = 'registration', $signon = true, $args = array() ) {\n\n\t$user_id = LLMS_Form_Handler::instance()->submit( $data, $screen, $args );\n\n\tif ( is_wp_error( $user_id ) ) {\n\t\treturn $user_id;\n\t}\n\n\t// Signon.\n\tif ( $signon && ! empty( $data['password'] ) ) {\n\n\t\t$user = get_user_by( 'ID', $user_id );\n\n\t\t/**\n\t\t * Filters whether or not a new user should be \"remembered\" when signing on during account creation\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param bool    $remember If `true` (default), the user signon will be set to \"remember\".\n\t\t * @param string  $screen   Current validation template, either \"registration\" or \"checkout\".\n\t\t * @param WP_User $user     User object for the newly registered user.\n\t\t */\n\t\t$remember = apply_filters( 'llms_user_registration_remember', true, $screen, $user );\n\n\t\twp_signon(\n\t\t\tarray(\n\t\t\t\t'user_login'    => $user->user_login,\n\t\t\t\t'user_password' => $data['password'],\n\t\t\t\t'remember'      => $remember,\n\t\t\t),\n\t\t\tis_ssl()\n\t\t);\n\n\t}\n\n\treturn $user_id;\n\n}\n\n/**\n * Set or unset a user's password reset cookie.\n *\n * @since 5.0.0\n *\n * @param string $val Cookie value.\n * @return bool\n */\nfunction llms_set_password_reset_cookie( $val = '' ) {\n\n\t$cookie  = sprintf( 'wp-resetpass-%s', COOKIEHASH );\n\t$expires = $val ? 0 : time() - YEAR_IN_SECONDS;\n\t$path    = isset( $_SERVER['REQUEST_URI'] ) ? current( explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized\n\n\treturn llms_setcookie( $cookie, $val, $expires, $path, COOKIE_DOMAIN, is_ssl(), true );\n\n}\n\n/**\n * Set/Update user login time\n *\n * @since 4.5.0\n *\n * @param string  $user_login Username.\n * @param WP_User $user       WP_User object of the logged-in user.\n * @return void\n */\nfunction llms_set_user_login_time( $user_login, $user ) {\n\tupdate_user_meta( $user->ID, 'llms_last_login', llms_current_time( 'mysql' ) );\n}\nadd_action( 'wp_login', 'llms_set_user_login_time', 10, 2 );\n\n/**\n * Remove a LifterLMS Student from a course or membership\n *\n * @since 3.0.0\n *\n * @see LLMS_Student->unenroll() the class method wrapped by this function\n *\n * @param int    $user_id     WP User ID.\n * @param int    $product_id  WP Post ID of the Course or Membership.\n * @param string $new_status  The value to update the new status with after removal is complete.\n * @param string $trigger     Only remove the student if the original enrollment trigger matches the submitted value.\n *                            Passing \"any\" will remove regardless of enrollment trigger.\n * @return bool\n */\nfunction llms_unenroll_student( $user_id, $product_id, $new_status = 'expired', $trigger = 'any' ) {\n\t$student = new LLMS_Student( $user_id );\n\treturn $student->unenroll( $product_id, $trigger, $new_status );\n}\n\n/**\n * Update a user.\n *\n * @since 3.0.0\n * @since 3.7.0 Unknown.\n * @since 5.0.0 Updated to utilize LLMS_Form_Handler class.\n *\n * @param array  $data Array of user data.\n * @param string $location (Optional) screen to perform validations for, accepts \"account\" or \"checkout\". Default value: 'account'\n * @param array  $args   Additional arguments passed to the short-circuit filter.\n * @return int|WP_Error WP_User ID on success or error object on failure.\n */\nfunction llms_update_user( $data = array(), $location = 'account', $args = array() ) {\n\treturn LLMS_Form_Handler::instance()->submit( $data, $location, $args );\n}\n\n/**\n * Performs validations for submitted user data.\n *\n * The related functions `llms_update_user()` and `llms_register_user()` automatically perform validations so this method\n * should only be used if you wish to test updates / registration without actually performing the registration or update action.\n *\n * @since 7.0.0\n *\n * @param array  $data     Array of user data.\n * @param string $location (Optional) screen to perform validations for, accepts \"account\" or \"checkout\". Default value: 'checkout'\n * @param array  $args     Additional arguments passed to the short-circuit filter.\n * @return bool|WP_Error Returns `true` if the user data passes validation, otherwise returns an error object describing\n *                       the validation issues.\n */\nfunction llms_validate_user( $data = array(), $location = 'checkout', $args = array() ) {\n\t$args['validate_only'] = true;\n\treturn LLMS_Form_Handler::instance()->submit( $data, $location, $args );\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.privacy.php",
    "content": "<?php\n/**\n * Functions related to privacy policy and terms & conditions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.18.0\n * @version 3.18.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Determine if Terms & Conditions agreement is required during registration according to global settings\n *\n * @since 3.0.0\n * @since 3.3.1 Unknown.\n *\n * @return boolean\n */\nfunction llms_are_terms_and_conditions_required() {\n\n\t$enabled = get_option( 'lifterlms_registration_require_agree_to_terms' );\n\t$page_id = absint( get_option( 'lifterlms_terms_page_id', false ) );\n\n\treturn ( 'yes' === $enabled && $page_id );\n\n}\n\n/**\n * Retrieve the text/html for the custom privacy policy notice\n *\n * @since 3.18.0\n *\n * @param bool $merge If true, will merge {{policy}} to an HTML anchor.\n *                    Uses `wp_page_for_privacy_policy` for page ID & title.\n * @return string\n */\nfunction llms_get_privacy_notice( $merge = false ) {\n\n\t$text = get_option( 'llms_privacy_notice', esc_html__( 'Your personal data will be used to process your enrollment, support your experience on this website, and for other purposes described in our {{policy}}.', 'lifterlms' ) );\n\n\t$ret = $text;\n\n\t// Merge the {{policy}} code.\n\tif ( $merge ) {\n\n\t\t// Only merge if we some text saved & a page set.\n\t\tif ( $text && get_option( 'wp_page_for_privacy_policy', false ) ) {\n\t\t\t$ret = str_replace( '{{policy}}', llms_get_option_page_anchor( 'wp_page_for_privacy_policy' ), $ret );\n\t\t\t// Otherwise return empty string.\n\t\t} else {\n\t\t\t$ret = '';\n\t\t}\n\n\t\t$ret = wp_kses(\n\t\t\t$ret,\n\t\t\tarray(\n\t\t\t\t'a'      => array(\n\t\t\t\t\t'href'   => array(),\n\t\t\t\t\t'target' => array(),\n\t\t\t\t),\n\t\t\t\t'b'      => array(),\n\t\t\t\t'em'     => array(),\n\t\t\t\t'i'      => array(),\n\t\t\t\t'strong' => array(),\n\t\t\t)\n\t\t);\n\t}\n\n\treturn apply_filters( 'llms_get_privacy_notice', $ret, $text );\n\n}\n\n/**\n * Retrieve the text/html for the custom t&c notice\n *\n * @since 3.18.0\n *\n * @param bool $merge If true, will merge {{terms}} to an HTML anchor.\n *                    Uses `lifterlms_terms_page_id` for page ID & title\n * @return string\n */\nfunction llms_get_terms_notice( $merge = false ) {\n\n\t// Get the option.\n\t$text = get_option( 'llms_terms_notice' );\n\n\t// Fallback to default if no option set.\n\tif ( ! $text ) {\n\t\t$text = esc_html__( 'I have read and agree to the {{terms}}.', 'lifterlms' );\n\t}\n\n\t$ret = $text;\n\n\t// Merge the {{terms}} code.\n\tif ( $merge ) {\n\n\t\t// Only merge if we have a page set.\n\t\tif ( get_option( 'lifterlms_terms_page_id', false ) ) {\n\t\t\t$ret = str_replace( '{{terms}}', llms_get_option_page_anchor( 'lifterlms_terms_page_id' ), $ret );\n\t\t\t// Otherwise return empty string.\n\t\t} else {\n\t\t\t$ret = '';\n\t\t}\n\n\t\t$ret = wp_kses(\n\t\t\t$ret,\n\t\t\tarray(\n\t\t\t\t'a'      => array(\n\t\t\t\t\t'href'   => array(),\n\t\t\t\t\t'target' => array(),\n\t\t\t\t),\n\t\t\t\t'b'      => array(),\n\t\t\t\t'em'     => array(),\n\t\t\t\t'i'      => array(),\n\t\t\t\t'strong' => array(),\n\t\t\t)\n\t\t);\n\t}\n\n\treturn apply_filters( 'llms_get_terms_notice', $ret, $text );\n\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.quiz.php",
    "content": "<?php\n/**\n * LifterLMS Quiz Functions.\n *\n * @package LifterLMS/Functions\n *\n * @since 3.16.0\n * @version 5.3.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieve the number of columns needed for a picture choice question.\n *\n * @since 3.16.0\n *\n * @param int $num_choices Number of choices.\n * @return int\n */\nfunction llms_get_picture_choice_question_cols( $num_choices ) {\n\n\t/**\n\t * Allow 3rd parties to override this function with a custom number of columns.\n\t *\n\t * If this responds with a non null will bypass column counter function return it immediately\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param null|int $cols        Number of columns needed for a picture choice question.\n\t * @param int      $num_choices Number of choices.\n\t */\n\t$cols = apply_filters( 'llms_get_picture_choice_question_cols', null, $num_choices );\n\n\tif ( 1 === $num_choices ) {\n\t\t$cols = 1;\n\t} elseif ( $num_choices >= 25 ) {\n\t\t$cols = 5;\n\t} elseif ( $num_choices >= 10 ) {\n\t\t$max_cols = 5;\n\t\t$min_cols = 3;\n\t} else {\n\t\t$max_cols = 4;\n\t\t$min_cols = 2;\n\t}\n\n\tif ( is_null( $cols ) ) {\n\n\t\t$i = $max_cols;\n\t\twhile ( $i >= $min_cols ) {\n\t\t\tif ( 0 === $num_choices % $i ) {\n\t\t\t\t$cols = $i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t$i--;\n\t\t}\n\n\t\tif ( ! $cols ) {\n\t\t\t$cols = llms_get_picture_choice_question_cols( $num_choices + 1 );\n\t\t}\n\t}\n\n\t/** This filter is documented above */\n\treturn apply_filters( 'llms_get_picture_choice_question_cols', $cols, $num_choices );\n\n}\n\n/**\n * Retrieve data for a single question type.\n *\n * @since 3.16.0\n *\n * @param string $type Id of the question type.\n * @return array|false\n */\nfunction llms_get_question_type( $type ) {\n\n\t$types = llms_get_question_types();\n\t$ret   = isset( $types[ $type ] ) ? $types[ $type ] : false;\n\n\t/**\n\t * Filters the data for a single question type.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array|false $data Data for a single question type. False it there's no data for a given quesiton type.\n\t * @param string      $type Id of the question type.\n\t */\n\treturn apply_filters( 'llms_get_question_type', $ret, $type );\n\n}\n\n/**\n * Retrieve question types.\n *\n * See `LLMS_Question_Types` class for actual loading of core question types.\n *\n * @since 3.16.0\n * @return array\n */\nfunction llms_get_question_types() {\n\t/**\n\t * Filters the question types.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array $question_types Question types.\n\t */\n\treturn apply_filters( 'llms_get_question_types', array() );\n}\n\n/**\n * Retrieve statuses for quiz attempts.\n *\n * @since 3.16.0\n *\n * @return array\n */\nfunction llms_get_quiz_attempt_statuses() {\n\t/**\n\t * Filters the quiz attempt statuses\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array $quiz_attempt_statuses Statuses for quiz attempts.\n\t */\n\treturn apply_filters(\n\t\t'llms_get_quiz_attempt_statuses',\n\t\tarray(\n\t\t\t'incomplete' => __( 'Incomplete', 'lifterlms' ),\n\t\t\t'pending'    => __( 'Pending Review', 'lifterlms' ),\n\t\t\t'fail'       => __( 'Fail', 'lifterlms' ),\n\t\t\t'pass'       => __( 'Pass', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Get quiz settings defined by supporting themes\n *\n * @since 3.16.8\n * @since 3.38.0 Moved deprecation notice from `LLMS_Admin_Builder::get_custom_schemas()`.\n * @since 4.6.0 Removed logging and use `apply_filters_deprecated()` in favor of `apply_filters()`.\n * @since 5.3.3 Correctly pass an array of settings as parameter for `apply_filters_deprecated()`.\n * @deprecated 3.38.0 See https://lifterlms.com/docs/course-builder-custom-fields-for-developers for more information.\n *\n * @param string $setting Name of setting, if omitted returns all settings.\n * @param string $default Default fallback if setting not set.\n * @return array\n */\nfunction llms_get_quiz_theme_setting( $setting = '', $default = '' ) {\n\n\t/**\n\t * Deprecated.\n\t *\n\t * @since 3.17.0\n\t * @deprecated 3.17.6 Deprecated. See https://lifterlms.com/docs/course-builder-custom-fields-for-developers for more information.\n\t *\n\t * @param array[] $settings Array of quiz theme settings.\n\t */\n\t$settings = apply_filters_deprecated(\n\t\t'llms_get_quiz_theme_settings',\n\t\tarray(\n\t\t\tarray(\n\t\t\t\t'layout' => array(\n\t\t\t\t\t'id'      => '',\n\t\t\t\t\t'name'    => __( 'Layout', 'lifterlms' ),\n\t\t\t\t\t'options' => array(),\n\t\t\t\t\t'type'    => 'select', // Either: select or image_select.\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t\t'3.17.6'\n\t);\n\n\tif ( $setting ) {\n\t\treturn isset( $settings[ $setting ] ) ? $settings[ $setting ] : $default;\n\t}\n\n\treturn $settings;\n\n}\n\n/**\n * Shuffles choices until the choice order has changed from the original.\n *\n * The smaller the list of choices the greater the chance of shuffling not changing the array.\n *\n * @since 3.16.12\n *\n * @param array $choices Choices from an LLMS_Question\n * @return array\n */\nfunction llms_shuffle_choices( $choices ) {\n\n\t$count = count( $choices );\n\n\t// If we only have one choice there's not much to shuffle with.\n\tif ( $count <= 1 ) {\n\t\treturn $choices;\n\n\t\t// Reverse the array when we only have two.\n\t} elseif ( 2 === $count ) {\n\t\t$shuffled = array_reverse( $choices );\n\n\t\t// Shuffle until the order has changed.\n\t} else {\n\n\t\t$shuffled = $choices;\n\n\t\twhile ( $shuffled === $choices ) {\n\t\t\tshuffle( $shuffled );\n\t\t}\n\t}\n\n\treturn $shuffled;\n\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.template.php",
    "content": "<?php\n/**\n * LifterLMS Template functions\n *\n * @package LifterLMS/Functions\n *\n * @since Unknown\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get template part\n *\n * @since Unknown\n *\n * @param string $slug The slug name for the generic template.\n * @param string $name Optional. The name of the specialised template. Default is empty string.\n * @return void\n */\nfunction llms_get_template_part( $slug, $name = '' ) {\n\t$template = '';\n\n\tif ( $name ) {\n\t\t$template = llms_locate_template( \"{$slug}-{$name}.php\", llms()->template_path() . \"{$slug}-{$name}.php\" );\n\t}\n\n\t// Get default slug-name.php.\n\tif ( ! $template && $name && file_exists( llms()->plugin_path() . \"/templates/{$slug}-{$name}.php\" ) ) {\n\t\t$template = llms()->plugin_path() . \"/templates/{$slug}-{$name}.php\";\n\t}\n\n\tif ( ! $template ) {\n\t\t$template = llms_locate_template( \"{$slug}.php\", llms()->template_path() . \"{$slug}.php\" );\n\t}\n\n\t/**\n\t * Filters the template file path\n\t *\n\t * Allow 3rd party plugin filter template file from their plugin.\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $template The path to the template file.\n\t * @param string $slug     The slug name for the generic template.\n\t * @param stirng $name     The name of the specialised template.\n\t */\n\t$template = apply_filters( 'llms_get_template_part', $template, $slug, $name );\n\n\tif ( $template ) {\n\t\tload_template( $template, false );\n\t}\n}\n\n/**\n * Get Template part contents\n *\n * @since Unknown\n *\n * @param string $slug The slug name for the generic template.\n * @param string $name Optional. The name of the specialised template. Default is empty string.\n * @return string\n */\nfunction llms_get_template_part_contents( $slug, $name = '' ) {\n\t$template = '';\n\n\tif ( $name ) {\n\t\t$template = llms_locate_template( \"{$slug}-{$name}.php\", llms()->template_path() . \"{$slug}-{$name}.php\" );\n\t}\n\n\t// Get default slug-name.php.\n\tif ( ! $template && $name && file_exists( llms()->plugin_path() . \"/templates/{$slug}-{$name}.php\" ) ) {\n\t\t$template = llms()->plugin_path() . \"/templates/{$slug}-{$name}.php\";\n\t}\n\n\tif ( ! $template ) {\n\t\t$template = llms_locate_template( \"{$slug}.php\", llms()->template_path() . \"{$slug}.php\" );\n\t}\n\n\tif ( $template ) {\n\t\treturn $template;\n\t}\n}\n\n/**\n * Get Template Part\n *\n * @since 1.0.0\n * @since 3.16.0 Unknown\n *\n * @param string $template_name Name of template.\n * @param array  $args          Array of arguments accessible from the template.\n * @param string $template_path Optional. Dir path to template. Default is empty string.\n *                              If not supplied the one retrived from `llms()->template_path()` will be used.\n * @param string $default_path  Optional. Default path is empty string.\n *                              If not supplied the template path is `llms()->plugin_path() . '/templates/'`.\n * @return void\n */\nfunction llms_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {\n\tif ( $args && is_array( $args ) ) {\n\t\textract( $args );\n\t}\n\n\t$located = llms_locate_template( $template_name, $template_path, $default_path );\n\n\t/**\n\t * Fired before a template part is included\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $template_name Name of template.\n\t * @param string $template_path Dir path to template as passed to the `llms_get_template()` function.\n\t * @param string $located       The full path of the template file to load.\n\t * @param array  $args          Array of arguments accessible from the template.\n\t */\n\tdo_action( 'lifterlms_before_template_part', $template_name, $template_path, $located, $args );\n\n\tif ( file_exists( $located ) ) {\n\t\tinclude $located;\n\t}\n\n\t/**\n\t * Fired after a template part is included\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $template_name Name of template.\n\t * @param string $template_path Dir path to template as passed to the `llms_get_template()` function.\n\t * @param string $located       The full path of the (maybe) loaded template file.\n\t * @param array  $args          Array of arguments accessible from the template.\n\t */\n\tdo_action( 'lifterlms_after_template_part', $template_name, $template_path, $located, $args );\n}\n\n\nfunction llms_get_template_ajax( $template_name, $args = array(), $template_path = '', $default_path = '' ) {\n\n\tob_start();\n\tllms_get_template( $template_name, $args, $template_path, $default_path );\n\treturn ob_get_clean();\n}\n\n/**\n * Locate Template\n *\n * @param string $template_name Name of template.\n * @param string $template_path Optional. Dir path to template. Default is empty string.\n *                              If not supplied the one retrived from `llms()->template_path()` will be used.\n * @param string $default_path  Optional. Default path is empty string.\n *                              If not supplied the template path is `llms()->plugin_path() . '/templates/'`.\n * @return string\n *\n * @since 1.0.0\n * @since 3.0.0 Only returns path if template exists.\n */\nfunction llms_locate_template( $template_name, $template_path = '', $default_path = '' ) {\n\tif ( ! $template_path ) {\n\t\t$template_path = llms()->template_path();\n\t}\n\n\tif ( ! $default_path ) {\n\t\t$default_path = llms()->plugin_path() . '/templates/';\n\t}\n\n\t// Check theme and template directories for the template.\n\t$override_path = llms_get_template_override( $template_name );\n\n\t// Get default template.\n\t$path = ( $override_path ) ? $override_path : $default_path;\n\n\t$template = $path . $template_name;\n\n\tif ( ! file_exists( $template ) ) {\n\n\t\t$template = '';\n\n\t}\n\n\t/**\n\t * Filters the maybe located template file path\n\t *\n\t * Allow 3rd party plugin filter template file from their plugin.\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $template      The path to the template file. Empty string if no template found.\n\t * @param string $template_name Name of template.\n\t * @param string $template_path Dir path to template.\n\t */\n\treturn apply_filters( 'lifterlms_locate_template', $template, $template_name, $template_path );\n}\n\n/**\n * Get template override.\n *\n * @since Unknown\n * @since 4.8.0 Move template override directories logic into llms_get_template_override_directories.\n *\n * @param string $template Template file.\n * @return mixed Template file directory or false if none exists.\n */\nfunction llms_get_template_override( $template = '' ) {\n\n\t$dirs = llms_get_template_override_directories();\n\n\tforeach ( $dirs as $dir ) {\n\n\t\t$path = $dir . '/';\n\t\tif ( file_exists( \"{$path}{$template}\" ) ) {\n\t\t\treturn $path;\n\t\t}\n\t}\n\n\treturn false;\n}\n\n/**\n * Get template override directories.\n *\n * Moved from `llms_get_template_override()`.\n *\n * @since 4.8.0\n *\n * @return string[]\n */\nfunction llms_get_template_override_directories() {\n\n\t$dirs = wp_cache_get( 'theme-override-directories', 'llms_template_functions' );\n\tif ( false === $dirs ) {\n\t\t$dirs = array_filter(\n\t\t\tarray_unique(\n\t\t\t\tarray(\n\t\t\t\t\tget_stylesheet_directory() . '/lifterlms',\n\t\t\t\t\tget_template_directory() . '/lifterlms',\n\t\t\t\t)\n\t\t\t),\n\t\t\t'is_dir'\n\t\t);\n\t\twp_cache_set( 'theme-override-directories', $dirs, 'llms_template_functions' );\n\t}\n\n\t/**\n\t * Filters the theme override directories.\n\t *\n\t * Allow themes and plugins to determine which folders to look in for theme overrides.\n\t *\n\t * @since Unknown\n\t *\n\t * @param string[] $theme_override_directories List of theme override directory paths.\n\t */\n\treturn apply_filters( 'lifterlms_theme_override_directories', $dirs );\n}\n\n/**\n * Determine if Focus Mode is enabled for a specific post (lesson, quiz, or other post types via filter).\n *\n * For lessons and quizzes, uses the parent course focus mode setting (or global setting).\n * Add-ons (e.g. LifterLMS Assignments) can use the filter to enable focus mode for their post types.\n *\n * @since 10.0.0\n *\n * @param int $post_id The ID of the post (lesson, quiz, etc.).\n * @return bool\n */\nfunction llms_is_focus_mode_enabled( $post_id ) {\n\t$post = llms_get_post( $post_id );\n\tif ( ! $post ) {\n\t\treturn false;\n\t}\n\n\t$result = false;\n\t$type   = $post->get( 'type' );\n\n\tif ( 'lesson' === $type || 'llms_quiz' === $type ) {\n\t\t$course = llms_get_post_parent_course( $post_id );\n\t\tif ( ! $course ) {\n\t\t\treturn apply_filters( 'llms_is_focus_mode_enabled', false, $post_id );\n\t\t}\n\n\t\t$course_focus_mode = $course->get( 'focus_mode' );\n\t\tif ( 'enable' === $course_focus_mode ) {\n\t\t\t$result = true;\n\t\t} elseif ( 'disable' === $course_focus_mode ) {\n\t\t\t$result = false;\n\t\t} else {\n\t\t\t$result = 'yes' === get_option( 'lifterlms_enable_focus_mode', 'no' );\n\t\t}\n\n\t\tif ( $result && ! current_user_can( 'manage_lifterlms' ) ) {\n\t\t\t$student = llms_get_student();\n\t\t\tif ( ! $student || ! $student->is_enrolled( $course->get( 'id' ) ) ) {\n\t\t\t\t$result = false;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Filters whether focus mode is enabled for a given post.\n\t *\n\t * Used by add-ons (e.g. LifterLMS Assignments) to enable focus mode for their post types\n\t * when focus mode is enabled globally and/or for the course.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @param bool $result  Whether focus mode is enabled.\n\t * @param int  $post_id The post ID.\n\t */\n\treturn apply_filters( 'llms_is_focus_mode_enabled', $result, $post_id );\n}\n\n/**\n * Retrieve the effective focus mode content width for a post.\n *\n * Checks the parent course setting first (for lesson/quiz), then falls back to global.\n *\n * @since 10.0.0\n *\n * @param int $post_id The ID of the post (lesson, quiz, or other focus-mode post type).\n * @return string Width value: 'full', '1600', '1180', '960', or '768'.\n */\nfunction llms_get_focus_mode_content_width( $post_id ) {\n\t$course = llms_get_post_parent_course( $post_id );\n\tif ( $course ) {\n\t\t$value = $course->get( 'focus_mode_content_width' );\n\t\tif ( $value && 'inherit' !== $value ) {\n\t\t\treturn $value;\n\t\t}\n\t}\n\treturn get_option( 'lifterlms_focus_mode_content_width', '960' );\n}\n\n/**\n * Retrieve the effective focus mode sidebar position for a post.\n *\n * Checks the parent course setting first (for lesson/quiz), then falls back to global.\n *\n * @since 10.0.0\n *\n * @param int $post_id The ID of the post (lesson, quiz, or other focus-mode post type).\n * @return string 'left' or 'right'.\n */\nfunction llms_get_focus_mode_sidebar_position( $post_id ) {\n\t$course = llms_get_post_parent_course( $post_id );\n\tif ( $course ) {\n\t\t$value = $course->get( 'focus_mode_sidebar_position' );\n\t\tif ( $value && 'inherit' !== $value ) {\n\t\t\treturn $value;\n\t\t}\n\t}\n\treturn get_option( 'lifterlms_focus_mode_sidebar_position', 'left' );\n}\n\n/**\n * Get focus mode content width select options for course-level settings.\n *\n * @since 10.0.0\n *\n * @param bool $include_inherit Whether to include the \"Inherit\" option with the current global value.\n * @return array Array of key/title option arrays.\n */\nfunction llms_get_focus_mode_content_width_options( $include_inherit = false ) {\n\t$widths = array(\n\t\t'full' => __( 'Full Width', 'lifterlms' ),\n\t\t'1600' => __( 'Extra Wide (1600px)', 'lifterlms' ),\n\t\t'1180' => __( 'Wide (1180px)', 'lifterlms' ),\n\t\t'960'  => __( 'Default (960px)', 'lifterlms' ),\n\t\t'768'  => __( 'Narrow (768px)', 'lifterlms' ),\n\t);\n\n\t$options = array();\n\n\tif ( $include_inherit ) {\n\t\t$global_value = get_option( 'lifterlms_focus_mode_content_width', '960' );\n\t\t$global_label = isset( $widths[ $global_value ] ) ? $widths[ $global_value ] : $global_value;\n\t\t$options[]    = array(\n\t\t\t'key'   => 'inherit',\n\t\t\t'title' => sprintf(\n\t\t\t\t/* translators: %s: current global setting label */\n\t\t\t\t__( 'Inherit Global Setting (%s)', 'lifterlms' ),\n\t\t\t\t$global_label\n\t\t\t),\n\t\t);\n\t}\n\n\tforeach ( $widths as $key => $title ) {\n\t\t$options[] = array(\n\t\t\t'key'   => $key,\n\t\t\t'title' => $title,\n\t\t);\n\t}\n\n\treturn $options;\n}\n\n/**\n * Get focus mode sidebar position select options for course-level settings.\n *\n * @since 10.0.0\n *\n * @param bool $include_inherit Whether to include the \"Inherit\" option with the current global value.\n * @return array Array of key/title option arrays.\n */\nfunction llms_get_focus_mode_sidebar_position_options( $include_inherit = false ) {\n\t$positions = array(\n\t\t'left'  => __( 'Left', 'lifterlms' ),\n\t\t'right' => __( 'Right', 'lifterlms' ),\n\t);\n\n\t$options = array();\n\n\tif ( $include_inherit ) {\n\t\t$global_value = get_option( 'lifterlms_focus_mode_sidebar_position', 'left' );\n\t\t$global_label = isset( $positions[ $global_value ] ) ? $positions[ $global_value ] : $global_value;\n\t\t$options[]    = array(\n\t\t\t'key'   => 'inherit',\n\t\t\t'title' => sprintf(\n\t\t\t\t/* translators: %s: current global setting label */\n\t\t\t\t__( 'Inherit Global Setting (%s)', 'lifterlms' ),\n\t\t\t\t$global_label\n\t\t\t),\n\t\t);\n\t}\n\n\tforeach ( $positions as $key => $title ) {\n\t\t$options[] = array(\n\t\t\t'key'   => $key,\n\t\t\t'title' => $title,\n\t\t);\n\t}\n\n\treturn $options;\n}\n\n/**\n * Add body classes for focus mode.\n *\n * @since 10.0.0\n *\n * @param array $classes Body classes.\n * @return array\n */\nfunction llms_focus_mode_body_class( $classes ) {\n\t/**\n\t * Post types that can use the focus mode template when focus mode is enabled.\n\t *\n\t * Add-ons (e.g. LifterLMS Assignments) can add their post types here and use\n\t * the `llms_is_focus_mode_enabled` filter to return true when appropriate.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @param string[] $post_types Post type names. Default: [ 'lesson', 'llms_quiz' ].\n\t */\n\t$focus_mode_post_types = apply_filters( 'llms_focus_mode_post_types', array( 'lesson', 'llms_quiz' ) );\n\tif ( is_singular( $focus_mode_post_types ) && llms_is_focus_mode_enabled( get_the_ID() ) ) {\n\t\t$classes[] = 'llms-focus-mode';\n\n\t\t$width = llms_get_focus_mode_content_width( get_the_ID() );\n\t\tif ( 'full' !== $width ) {\n\t\t\t$classes[] = 'llms-focus-mode-width-' . $width;\n\t\t}\n\n\t\t$position  = llms_get_focus_mode_sidebar_position( get_the_ID() );\n\t\t$classes[] = 'llms-focus-mode-sidebar-' . $position;\n\t}\n\treturn $classes;\n}\nadd_filter( 'body_class', 'llms_focus_mode_body_class' );\n\n/**\n * Enqueue focus mode frontend scripts.\n *\n * @since 10.0.0\n *\n * @return void\n */\nfunction llms_focus_mode_enqueue_scripts() {\n\t$focus_mode_post_types = apply_filters( 'llms_focus_mode_post_types', array( 'lesson', 'llms_quiz' ) );\n\tif ( is_singular( $focus_mode_post_types ) && llms_is_focus_mode_enabled( get_the_ID() ) ) {\n\t\t$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';\n\n\t\twp_enqueue_style(\n\t\t\t'llms-focus-mode',\n\t\t\tllms()->plugin_url() . '/assets/css/llms-focus-mode' . $suffix . '.css',\n\t\t\tarray(),\n\t\t\tllms()->version\n\t\t);\n\n\t\twp_enqueue_script(\n\t\t\t'llms-focus-mode',\n\t\t\tllms()->plugin_url() . '/assets/js/llms-focus-mode.js',\n\t\t\tarray(),\n\t\t\tllms()->version,\n\t\t\ttrue\n\t\t);\n\t}\n}\nadd_action( 'wp_enqueue_scripts', 'llms_focus_mode_enqueue_scripts' );\n\n/**\n * Render focus mode post content.\n *\n * Themes can override by removing this action and adding their own:\n *   remove_action( 'llms_focus_mode_the_content', 'llms_focus_mode_render_content' );\n *   add_action( 'llms_focus_mode_the_content', 'my_theme_render_content' );\n *\n * @since 10.0.0\n *\n * @return void\n */\nfunction llms_focus_mode_render_content() {\n\tthe_content();\n}\nadd_action( 'llms_focus_mode_the_content', 'llms_focus_mode_render_content' );\n\n/**\n * Build the plugin's template file path.\n *\n * @since 5.8.0\n * @since 7.2.0 Do not add leading slash to absolute template directory.\n *\n * @param string $template                    Template file name.\n * @param string $template_directory          Template directory relative to the plugin base directory.\n * @param bool   $template_directory_absolute Whether the template directory is absolute or not.\n * @return string\n */\nfunction llms_template_file_path( $template, $template_directory = 'templates', $template_directory_absolute = false ) {\n\n\t// We have reason to use a LifterLMS template, check if there's an override we should use from a theme / etc...\n\t$override           = llms_get_template_override( $template );\n\t$template_directory = $template_directory_absolute ? $template_directory : llms()->plugin_path() . \"/{$template_directory}/\";\n\t$template_path      = $override ? $override : $template_directory;\n\n\treturn trailingslashit( $template_path ) . \"{$template}\";\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.achievements.php",
    "content": "<?php\n/**\n * Achievements & Related template functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.14.0\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get the content of a single achievement\n *\n * @since 3.14.0\n *\n * @param LLMS_User_Achievement $achievement Instance of an LLMS_User_Achievement.\n * @return void\n */\nfunction llms_get_achievement( $achievement ) {\n\n\tob_start();\n\n\tllms_get_template(\n\t\t'achievements/template.php',\n\t\tarray(\n\t\t\t'achievement' => $achievement,\n\t\t)\n\t);\n\n\treturn ob_get_clean();\n}\n\n/**\n * Output the content of a single achievement\n *\n * @since 3.14.0\n *\n * @param LLMS_Achievement $achievement Instance of an LLMS_User_Achievement.\n * @return void\n */\nfunction llms_the_achievement( $achievement ) {\n\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\techo llms_get_achievement( $achievement );\n}\n\n/**\n * Retrieve the number of columns used in achievement loops\n *\n * @since 3.14.0\n *\n * @return int\n */\nfunction llms_get_achievement_loop_columns() {\n\treturn apply_filters( 'llms_achievement_loop_columns', 4 );\n}\n\n\n/**\n * Get template for achievements loop.\n *\n * @since 3.14.0\n * @since 3.14.1 Unknown.\n * @since 6.0.0 Updated to use the new signature of the {@see LLMS_Student::get_achievements()}.\n * @since 7.2.0 Made sure to always enqueue needed assets.\n *\n * @param LLMS_Student $student Optional. LLMS_Student (uses current if none supplied). Default is `null`.\n *                              The current student will be used if none supplied.\n * @param bool|int     $limit   Optional. Number of achievements to show or `false` to display all.\n * @param int          $columns Optional. Number of achievements columns. Default is `null`.\n *                              The default achievement loop columns will be used if none supplied. See `llms_get_achievement_loop_columns()`.\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_achievements_loop' ) ) {\n\tfunction lifterlms_template_achievements_loop( $student = null, $limit = false, $columns = null ) {\n\n\t\t// Get the current student if none supplied.\n\t\tif ( ! $student ) {\n\t\t\t$student = llms_get_student();\n\t\t}\n\n\t\t// Don't proceed without a student.\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\tllms()->assets->enqueue_style( 'llms-iziModal' );\n\t\tllms()->assets->enqueue_script( 'llms-iziModal' );\n\n\t\t$cols     = $columns ? $columns : llms_get_achievement_loop_columns();\n\t\t$per_page = $cols * 5;\n\n\t\t// Get achievements.\n\t\t$query        = $student->get_achievements(\n\t\t\tarray(\n\t\t\t\t'page'     => max( 1, get_query_var( 'paged' ) ),\n\t\t\t\t'per_page' => $limit ? min( $limit, $per_page ) : $per_page,\n\t\t\t)\n\t\t);\n\t\t$achievements = $query->get_awards();\n\n\t\t/**\n\t\t * If no columns are specified and we have a specified limit\n\t\t * and results and the limit is less than the number of columns\n\t\t * force the columns to equal the limit.\n\t\t */\n\t\tif ( ! $columns && $limit && $limit < $cols && $query->get_number_results() ) {\n\t\t\t$cols = $limit;\n\t\t}\n\n\t\t$pagination = 'dashboard' === LLMS_Student_Dashboard::get_current_tab( 'slug' ) ? false : array(\n\t\t\t'total'   => $query->get_max_pages(),\n\t\t\t'context' => 'student_dashboard',\n\t\t);\n\n\t\tllms_get_template(\n\t\t\t'achievements/loop.php',\n\t\t\tcompact( 'cols', 'achievements', 'pagination' )\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.certificates.php",
    "content": "<?php\n/**\n * Certificates & Related template functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.14.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Loads the certificate content template.\n *\n * @since 6.0.0\n *\n * @param LLMS_User_Certificate $certificate Certificate object.\n * @return void\n */\nfunction llms_certificate_content( $certificate ) {\n\t$template = 1 === $certificate->get_template_version() ? 'content-legacy' : 'content';\n\tllms_get_template(\n\t\t\"certificates/{$template}.php\",\n\t\tcompact( 'certificate' )\n\t);\n}\n\n/**\n * Outputs dynamic CSS for a single certificate template.\n *\n * Hooked to action `wp_head` at priority 10.\n *\n * @since 6.0.0\n *\n * @return void\n */\nfunction llms_certificate_styles() {\n\n\t$certificate = llms_get_certificate( get_the_ID(), true );\n\tif ( ! $certificate || 1 === $certificate->get_template_version() ) {\n\t\treturn;\n\t}\n\n\t$image          = $certificate->get_background_image();\n\t$background_img = $image['src'];\n\n\t$background_color = $certificate->get( 'background' );\n\n\t$padding = implode( ' ', $certificate->get_margins( true ) );\n\n\t$dimensions = $certificate->get_dimensions_for_display();\n\t$width      = $dimensions['width'];\n\t$height     = $dimensions['height'];\n\n\t$fonts = $certificate->get_custom_fonts();\n\n\tllms_get_template(\n\t\t'certificates/dynamic-styles.php',\n\t\tcompact( 'certificate', 'width', 'height', 'background_color', 'background_img', 'padding', 'fonts' )\n\t);\n}\n\n/**\n * Loads the certificate actions template.\n *\n * @since 6.0.0\n *\n * @param LLMS_User_Certificate $certificate Certificate object.\n * @return void\n */\nfunction llms_certificate_actions( $certificate ) {\n\n\tif ( ! $certificate->can_user_manage() ) {\n\t\treturn;\n\t}\n\n\t$dashboard_url   = get_permalink( llms_get_page_id( 'myaccount' ) );\n\t$cert_ep_enabled = LLMS_Student_Dashboard::is_endpoint_enabled( 'view-certificates' );\n\n\t$back_link = $cert_ep_enabled ? llms_get_endpoint_url( 'view-certificates', '', $dashboard_url ) : $dashboard_url;\n\t$back_text = $cert_ep_enabled ? __( 'All certificates', 'lifterlms' ) : __( 'Dashboard', 'lifterlms' );\n\n\t$is_template        = 'llms_certificate' === $certificate->get( 'type' );\n\t$is_sharing_enabled = $certificate->is_sharing_enabled();\n\tllms_get_template(\n\t\t'certificates/actions.php',\n\t\tcompact( 'certificate', 'back_link', 'back_text', 'is_sharing_enabled', 'is_template' )\n\t);\n}\n\n/**\n * Get the content of a single certificates\n *\n * @since 3.14.0\n *\n * @param LLMS_User_Certificate $certificate Instance of an LLMS_User_Certificate.\n * @return void\n */\nfunction llms_get_certificate_preview( $certificate ) {\n\n\tob_start();\n\n\tllms_get_template(\n\t\t'certificates/preview.php',\n\t\tarray(\n\t\t\t'certificate' => $certificate,\n\t\t)\n\t);\n\n\treturn ob_get_clean();\n}\n/**\n * Output the content of a single certificate\n *\n * @since 3.14.0\n *\n * @param LLMS_User_Certificate $certificate Instance of an LLMS_User_Certificate.\n * @return void\n */\nfunction llms_the_certificate_preview( $certificate ) {\n\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in template file.\n\techo llms_get_certificate_preview( $certificate );\n}\n\n/**\n * Retrieve the number of columns used in certificates loops\n *\n * @since 3.14.0\n * @since 6.0.0 Reduced default columns from 5 to 3.\n *\n * @return int\n */\nfunction llms_get_certificates_loop_columns() {\n\t/**\n\t * Filters the number of columns used to display a list of certificate previews.\n\t *\n\t * @since 3.14.0\n\t *\n\t * @param integer $cols Number of columns.\n\t */\n\treturn apply_filters( 'llms_certificates_loop_columns', 3 );\n}\n\n\n/**\n * Get template for certificates loop\n *\n * @since 3.14.0\n * @since 6.0.0 Updated to use the new signature of the {@see LLMS_Student::get_certificates()}.\n *              Add pagination.\n *\n * @param LLMS_Student $student Optional. LLMS_Student (uses current if none supplied). Default is `null`.\n *                              The current student will be used if none supplied.\n * @param bool|int     $limit   Optional. Number of certificates to show (defaults to all). Default is `false`.\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_certificates_loop' ) ) {\n\tfunction lifterlms_template_certificates_loop( $student = null, $limit = false ) {\n\n\t\t// Get the current student if none supplied.\n\t\tif ( ! $student ) {\n\t\t\t$student = llms_get_student();\n\t\t}\n\n\t\t// Don't proceed without a student.\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$cols     = llms_get_certificates_loop_columns();\n\t\t$per_page = $cols * 5;\n\n\t\t// Get certificates.\n\t\t$query        = $student->get_certificates(\n\t\t\tarray(\n\t\t\t\t'page'     => max( 1, get_query_var( 'paged' ) ),\n\t\t\t\t'per_page' => $limit ? min( $limit, $per_page ) : $per_page,\n\t\t\t)\n\t\t);\n\t\t$certificates = $query->get_awards();\n\n\t\t/**\n\t\t * If no columns are specified and we have a specified limit\n\t\t * and results and the limit is less than the number of columns\n\t\t * force the columns to equal the limit.\n\t\t */\n\t\tif ( $limit && $limit < $cols && $query->get_number_results() ) {\n\t\t\t$cols = $limit;\n\t\t}\n\n\t\t$pagination = 'dashboard' === LLMS_Student_Dashboard::get_current_tab( 'slug' ) ? false : array(\n\t\t\t'total'   => $query->get_max_pages(),\n\t\t\t'context' => 'student_dashboard',\n\t\t);\n\n\t\tllms_get_template(\n\t\t\t'certificates/loop.php',\n\t\t\tcompact( 'cols', 'certificates', 'pagination' )\n\t\t);\n\t}\n}\n\n/**\n * Automatically remove all non-safelisted print stylesheets from certificate and certificate templates.\n *\n * @since 6.0.0\n *\n * @return boolean Returns `false` when run on non-certificate post types, otherwise returns `true`.\n */\nfunction llms_certificates_remove_print_styles() {\n\n\tif ( ! in_array( get_post_type(), array( 'llms_certificate', 'llms_my_certificate' ), true ) ) {\n\t\treturn false;\n\t}\n\n\t/**\n\t * A list of registered print stylesheet handles which should be allowed for certificate and certificate templates.\n\t *\n\t * By default, any enqueued print stylesheets are automatically dequeued to prevent visual issues encountered when\n\t * printing certificates.\n\t *\n\t * Any stylesheets added to this safelist will not be removed from certificates.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string[] $safelist Array of print stylesheet handles.\n\t */\n\t$safelist = apply_filters( 'llms_certificate_print_styles_safelist', array() );\n\n\t$styles = wp_styles();\n\tforeach ( $styles->queue as $handle ) {\n\t\t$style = $styles->registered[ $handle ] ?? false;\n\t\tif ( ! empty( $style->args ) && 'print' === $style->args && ! in_array( $handle, $safelist, true ) ) {\n\t\t\twp_dequeue_style( $handle );\n\t\t}\n\t}\n\n\treturn true;\n}\nadd_action( 'wp_enqueue_scripts', 'llms_certificates_remove_print_styles', 999 );\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.dashboard.php",
    "content": "<?php\n/**\n * Template functions for the student dashboard\n *\n * @package LifterLMS/Functions\n *\n * @since 3.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! function_exists( 'lifterlms_student_dashboard' ) ) {\n\n\t/**\n\t * Output the LifterLMS Student Dashboard\n\t *\n\t * @since 3.25.1\n\t * @since 3.35.0 unslash `$_GET` data.\n\t * @since 3.37.10 Add filter `llms_enable_open_registration`.\n\t * @since 5.0.0 During password reset, retrieve reset key and login from cookie instead of query string.\n\t *              Use `llms_get_open_registration_status()`.\n\t *\n\t * @param array $options Optional. Array of options. Default empty array.\n\t * @return void\n\t */\n\tfunction lifterlms_student_dashboard( $options = array() ) {\n\n\t\t$options = wp_parse_args(\n\t\t\t$options,\n\t\t\tarray(\n\t\t\t\t'layout'         => 'columns',\n\t\t\t\t'login_redirect' => get_permalink( llms_get_page_id( 'myaccount' ) ),\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Fires before the student dashboard output.\n\t\t *\n\t\t * @since 7.8.0\n\t\t *\n\t\t * @param string $layout The layout of the dashboard.\n\t\t *\n\t\t * @hooked lifterlms_template_student_dashboard_wrapper_open - 10\n\t\t */\n\t\tdo_action( 'lifterlms_before_student_dashboard', $options['layout'] );\n\n\t\t/**\n\t\t * Filters whether or not to display the student dashboard\n\t\t *\n\t\t * By default, this condition will show the dashboard to a logged in user\n\t\t * and the login/registration forms (as well as the password recovery flow)\n\t\t * to logged out users.\n\t\t *\n\t\t * The `LLMS_View_Manager` class uses this filter to modify the dashboard view\n\t\t * conditionally based on the requested view role.\n\t\t *\n\t\t * @since 4.16.0\n\t\t *\n\t\t * @param bool $is_user_logged-in Whether or not the user is logged in.\n\t\t */\n\t\t$display_dashboard = apply_filters( 'llms_display_student_dashboard', is_user_logged_in() );\n\n\t\t// Not displaying the dashboard (the user is not logged in), we'll show login/registration forms.\n\t\tif ( ! $display_dashboard ) {\n\n\t\t\t/**\n\t\t\t * Allow adding a notice message to be displayed in the student dashboard where `llms_print_notices()` will be invoked.\n\t\t\t *\n\t\t\t * @since unknown\n\t\t\t *\n\t\t\t * @param string $message The notice message to be displayed in the student dashboard. Default empty string.\n\t\t\t */\n\t\t\t$message = apply_filters( 'lifterlms_my_account_message', '' );\n\t\t\tif ( ! empty( $message ) ) {\n\t\t\t\tllms_add_notice( $message );\n\t\t\t}\n\n\t\t\tglobal $wp;\n\t\t\tif ( isset( $wp->query_vars['lost-password'] ) ) {\n\n\t\t\t\t$args = array();\n\t\t\t\tif ( llms_filter_input( INPUT_GET, 'reset-pass', FILTER_SANITIZE_NUMBER_INT ) ) {\n\t\t\t\t\t$args['form'] = 'reset_password';\n\t\t\t\t\t$cookie       = llms_parse_password_reset_cookie();\n\t\t\t\t\t$key          = '';\n\t\t\t\t\t$login        = '';\n\t\t\t\t\t$fields       = array();\n\t\t\t\t\tif ( is_wp_error( $cookie ) ) {\n\t\t\t\t\t\tllms_add_notice( $cookie->get_error_message(), 'error' );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$fields = LLMS_Person_Handler::get_password_reset_fields( $cookie['key'], $cookie['login'] );\n\t\t\t\t\t}\n\t\t\t\t\t$args['fields'] = $fields;\n\t\t\t\t} else {\n\t\t\t\t\t$args['form']   = 'lost_password';\n\t\t\t\t\t$args['fields'] = LLMS_Person_Handler::get_lost_password_fields();\n\t\t\t\t}\n\n\t\t\t\tllms_get_template( 'myaccount/form-lost-password.php', $args );\n\n\t\t\t} else {\n\n\t\t\t\tllms_print_notices();\n\n\t\t\t\tllms_get_login_form(\n\t\t\t\t\tnull,\n\t\t\t\t\t/**\n\t\t\t\t\t * Filter login form redirect URL\n\t\t\t\t\t *\n\t\t\t\t\t * @since unknown\n\t\t\t\t\t *\n\t\t\t\t\t * @param string $login_redirect The login redirect URL.\n\t\t\t\t\t */\n\t\t\t\t\tapply_filters( 'llms_student_dashboard_login_redirect', $options['login_redirect'] )\n\t\t\t\t);\n\n\t\t\t\tif ( llms_parse_bool( llms_get_open_registration_status() ) ) {\n\n\t\t\t\t\tllms_get_template( 'global/form-registration.php' );\n\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\n\t\t\t$tabs = LLMS_Student_Dashboard::get_tabs();\n\n\t\t\t$current_tab = LLMS_Student_Dashboard::get_current_tab( 'slug' );\n\n\t\t\t/**\n\t\t\t * Fires before the student dashboard content output.\n\t\t\t *\n\t\t\t * @since unknown\n\t\t\t *\n\t\t\t * @hooked lifterlms_template_student_dashboard_navigation - 10\n\t\t\t * @hooked lifterlms_template_student_dashboard_header - 20\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_before_student_dashboard_content' );\n\n\t\t\tif ( isset( $tabs[ $current_tab ] ) && isset( $tabs[ $current_tab ]['content'] ) && is_callable( $tabs[ $current_tab ]['content'] ) ) {\n\n\t\t\t\tcall_user_func( $tabs[ $current_tab ]['content'] );\n\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Fires after the student dashboard output.\n\t\t *\n\t\t * @since unknown\n\t\t *\n\t\t * @hooked lifterlms_template_student_dashboard_wrapper_close - 10\n\t\t */\n\t\tdo_action( 'lifterlms_after_student_dashboard' );\n\t}\n}\n\n\nif ( ! function_exists( 'lifterlms_template_my_courses_loop' ) ) {\n\n\t/**\n\t * Get course tiles for a student's courses\n\t *\n\t * @since 3.14.0\n\t * @since 3.26.3 Unknown.\n\t * @since 3.37.15 Added secondary sorting by `post_title` when the primary sort is `menu_order`.\n\t * @since 6.3.0 Fix paged query not working when using plain permalinks.\n\t * @since 7.1.3 Added filter for filtering 'Not enrolled text'.\n\t *\n\t * @param LLMS_Student $student Optional. LLMS_Student (current student if none supplied). Default `null`.\n\t * @param bool         $preview Optional. If true, outputs a short list of courses (based on dashboard_recent_courses filter). Default `false`.\n\t * @return void\n\t */\n\tfunction lifterlms_template_my_courses_loop( $student = null, $preview = false ) {\n\n\t\t$student = llms_get_student( $student );\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$courses = $student->get_courses(\n\t\t\t/**\n\t\t\t * Filter the query args to retrieve the courses ids to be used for the \"my_courses\" loop.\n\t\t\t *\n\t\t\t * @since unknown\n\t\t\t *\n\t\t\t * @param array $args The query args.\n\t\t\t */\n\t\t\tapply_filters(\n\t\t\t\t'llms_my_courses_loop_courses_query_args',\n\t\t\t\tarray(\n\t\t\t\t\t'limit' => 500,\n\t\t\t\t),\n\t\t\t\t$student\n\t\t\t)\n\t\t);\n\n\t\tif ( ! $courses['results'] ) {\n\n\t\t\tprintf(\n\t\t\t\t'<p>%s</p>',\n\t\t\t\t/**\n\t\t\t\t * Not enrolled text.\n\t\t\t\t *\n\t\t\t\t * Allows developers to filter the text to be displayed when the student is not enrolled in any courses.\n\t\t\t\t *\n\t\t\t\t * @since 7.1.3\n\t\t\t\t *\n\t\t\t\t * @param string $not_enrolled_text The text to be displayed when the student is not enrolled in any course.\n\t\t\t\t */\n\t\t\t\tesc_html( apply_filters( 'lifterlms_dashboard_courses_not_enrolled_text', __( 'You are not enrolled in any courses.', 'lifterlms' ) ) )\n\t\t\t);\n\n\t\t} else {\n\n\t\t\tadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_status', 25 );\n\t\t\tadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_date', 30 );\n\n\t\t\t// get sorting option.\n\t\t\t$option = get_option( 'lifterlms_myaccount_courses_in_progress_sorting', 'date,DESC' );\n\t\t\t// parse to order & orderby.\n\t\t\t$option  = explode( ',', $option );\n\t\t\t$orderby = ! empty( $option[0] ) ? $option[0] : 'date';\n\t\t\t$order   = ! empty( $option[1] ) ? $option[1] : 'DESC';\n\n\t\t\t// Enrollment date will obey the results order.\n\t\t\tif ( 'date' === $orderby ) {\n\t\t\t\t$orderby = 'post__in';\n\t\t\t} elseif ( 'order' === $orderby ) {\n\t\t\t\t// Add secondary sorting by `post_title` when the primary sort is `menu_order`.\n\t\t\t\t$orderby = 'menu_order post_title';\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filter the number of courses per page to be displayed in the dashboard.\n\t\t\t *\n\t\t\t * @since unknown\n\t\t\t *\n\t\t\t * @param int $per_page The number or courses per page to be displayed. Defaults to the 'Courses per page' course catalog's setting.\n\t\t\t */\n\t\t\t$per_page = apply_filters( 'llms_dashboard_courses_per_page', get_option( 'lifterlms_shop_courses_per_page', 9 ) );\n\t\t\tif ( $preview ) {\n\t\t\t\t/**\n\t\t\t\t * Filter the number of courses per page to be displayed in the dashboard, when outputting a short list of courses.\n\t\t\t\t *\n\t\t\t\t * @since unknown\n\t\t\t\t *\n\t\t\t\t * @param int $per_page The number or courses per page to be displayed. Default is `3`.\n\t\t\t\t */\n\t\t\t\t$per_page = apply_filters( 'llms_dashboard_recent_courses_count', llms_get_loop_columns() );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filter the wp query args to retrieve the courses for the \"my_courses\" loop.\n\t\t\t *\n\t\t\t * @since unknown\n\t\t\t *\n\t\t\t * @param array $args The query args.\n\t\t\t */\n\t\t\t$query_args = apply_filters(\n\t\t\t\t'llms_dashboard_courses_wp_query_args',\n\t\t\t\tarray(\n\t\t\t\t\t'paged'          => llms_get_paged_query_var(),\n\t\t\t\t\t'orderby'        => $orderby,\n\t\t\t\t\t'order'          => $order,\n\t\t\t\t\t'post__in'       => $courses['results'],\n\t\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t\t'post_type'      => 'course',\n\t\t\t\t\t'posts_per_page' => $per_page,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$query = new WP_Query( $query_args );\n\n\t\t\t// Prevent pagination on the preview.\n\t\t\tif ( $preview ) {\n\t\t\t\t$query->max_num_pages = 1;\n\t\t\t}\n\n\t\t\tadd_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\n\t\t\tlifterlms_loop( $query );\n\n\t\t\tremove_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\n\t\t\tremove_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_status', 25 );\n\t\t\tremove_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_date', 30 );\n\n\t\t}\n\t}\n}\n\nif ( ! function_exists( 'llms_template_my_favorites_loop' ) ) {\n\n\t/**\n\t * Get student's favorites.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param LLMS_Student $student   Optional. LLMS_Student (current student if none supplied). Default `null`.\n\t * @param array        $favorites Optional. Array of favorites (current student's favorites if none supplied). Default `null`.\n\t * @return void\n\t */\n\tfunction llms_template_my_favorites_loop( $student = null, $favorites = null ) {\n\n\t\t$student = llms_get_student( $student );\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$favorites = $favorites ?? $student->get_favorites();\n\n\t\tif ( ! $favorites ) {\n\n\t\t\tprintf( '<p>%s</p>', esc_html__( 'No favorites found.', 'lifterlms' ) );\n\n\t\t} else {\n\n\t\t\t// Adding Parent Course IDs in Favorites for each lesson.\n\t\t\tforeach ( $favorites as $key => $favorite ) {\n\t\t\t\t$lesson                  = new LLMS_Lesson( $favorite->post_id );\n\t\t\t\t$favorite->parent_course = $lesson->get( 'parent_course' );\n\t\t\t}\n\n\t\t\t// Grouping Favorites by Parent Course ID.\n\t\t\t$favorites = array_reduce(\n\t\t\t\t$favorites,\n\t\t\t\tfunction ( $carry, $item ) {\n\t\t\t\t\t$carry[ $item->parent_course ][] = $item;\n\t\t\t\t\treturn $carry;\n\t\t\t\t},\n\t\t\t\tarray()\n\t\t\t);\n\n\t\t\techo '<div class=\"llms-syllabus-wrapper\">';\n\n\t\t\t// Printing Favorite Lessons under each Parent Course.\n\t\t\tforeach ( $favorites as $course => $lessons ) {\n\n\t\t\t\t// Get Course Name.\n\t\t\t\t$course = new LLMS_Course( $course );\n\n\t\t\t\techo '<h3 class=\"llms-h3 llms-section-title\">';\n\t\t\t\t\techo esc_html( $course->get( 'title' ) );\n\t\t\t\techo '</h3>';\n\n\t\t\t\tforeach ( $lessons as $lesson ) {\n\n\t\t\t\t\t$lesson = new LLMS_Lesson( $lesson->post_id );\n\n\t\t\t\t\tllms_get_template(\n\t\t\t\t\t\t'course/lesson-preview.php',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'lesson' => $lesson,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\techo '</div>';\n\t\t}\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_my_memberships_loop' ) ) {\n\n\t/**\n\t * Get course tiles for a student's memberships\n\t *\n\t * @since 3.14.0\n\t * @since 3.14.8 Unknown.\n\t * @since 7.1.3 Added filter for filtering 'Not enrolled text'.\n\t *\n\t * @param LLMS_Student $student Optional. LLMS_Student (current student if none supplied). Default `null`.\n\t * @return void\n\t */\n\tfunction lifterlms_template_my_memberships_loop( $student = null ) {\n\n\t\t$student = llms_get_student( $student );\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$memberships = $student->get_membership_levels();\n\n\t\tif ( ! $memberships ) {\n\n\t\t\tprintf(\n\t\t\t\t'<p>%s</p>',\n\t\t\t\t/**\n\t\t\t\t * Not enrolled text.\n\t\t\t\t *\n\t\t\t\t * Allows developers to filter the text to be displayed when the student is not enrolled in any memberships.\n\t\t\t\t *\n\t\t\t\t * @since 7.1.3\n\t\t\t\t *\n\t\t\t\t * @param string $not_enrolled_text The text to be displayed when the student is not enrolled in any memberships.\n\t\t\t\t */\n\t\t\t\tesc_html( apply_filters( 'lifterlms_dashboard_memberships_not_enrolled_text', __( 'You are not enrolled in any memberships.', 'lifterlms' ) ) )\n\t\t\t);\n\n\t\t} else {\n\n\t\t\tadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_status', 25 );\n\t\t\tadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_date', 30 );\n\n\t\t\t$query = new WP_Query(\n\t\t\t\tarray(\n\t\t\t\t\t'orderby'        => 'title',\n\t\t\t\t\t'order'          => 'ASC',\n\t\t\t\t\t'post__in'       => $memberships,\n\t\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t\t'post_type'      => 'llms_membership',\n\t\t\t\t\t'posts_per_page' => -1,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$query->max_num_pages = 1; // Prevent pagination here.\n\n\t\t\tlifterlms_loop( $query );\n\n\t\t\tremove_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_status', 25 );\n\t\t\tremove_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_enroll_date', 30 );\n\n\t\t}\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_home' ) ) {\n\n\t/**\n\t * Main dashboard homepage template\n\t *\n\t * @since 3.14.0\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_home() {\n\t\tllms_get_template( 'myaccount/dashboard.php' );\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_header' ) ) {\n\n\t/**\n\t * Dashboard header template\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_header() {\n\t\tllms_get_template( 'myaccount/header.php' );\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_my_achievements' ) ) {\n\n\t/**\n\t * Template for My Achievements on dashboard\n\t *\n\t * @since 3.14.0\n\t * @since 3.19.0 Unknown.\n\t * @since 6.0.0 Don't output HTML when the endpoint is disabled.\n\t *\n\t * @param bool $preview If `true`, outputs a short list of achievements to display on the dashboard\n\t *                      landing page. Otherwise displays all of the earned achievements for display\n\t *                      on the view-achievements endpoint.\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_my_achievements( $preview = false ) {\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$enabled = LLMS_Student_Dashboard::is_endpoint_enabled( 'view-achievements' );\n\t\tif ( ! $enabled ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$more = false;\n\t\tif ( $preview ) {\n\t\t\t$more = array(\n\t\t\t\t'url'  => llms_get_endpoint_url( 'view-achievements', '', llms_get_page_url( 'myaccount' ) ),\n\t\t\t\t'text' => __( 'View All My Achievements', 'lifterlms' ),\n\t\t\t);\n\t\t}\n\n\t\tob_start();\n\n\t\tlifterlms_template_achievements_loop( $student, $preview ? llms_get_achievement_loop_columns() : false );\n\n\t\tllms_get_template(\n\t\t\t'myaccount/dashboard-section.php',\n\t\t\tarray(\n\t\t\t\t'action'  => 'my_achievements',\n\t\t\t\t'slug'    => 'llms-my-achievements',\n\t\t\t\t'title'   => $preview ? __( 'My Achievements', 'lifterlms' ) : '',\n\t\t\t\t'content' => ob_get_clean(),\n\t\t\t\t'more'    => $more,\n\t\t\t)\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_my_certificates' ) ) {\n\n\t/**\n\t * Template for My Certificates on dashboard\n\t *\n\t * @since 3.14.0\n\t * @since 3.19.0 Unknown\n\t * @since 6.0.0 Output short list when `$preview` is `true`.\n\t *               Don't output any HTML when the endpoint is disabled.\n\t *\n\t * @param bool $preview If `true`, outputs a short list of certificates to display on the dashboard\n\t *                      landing page. Otherwise displays all of the earned certificates for display\n\t *                      on the view-certificates endpoint.\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_my_certificates( $preview = false ) {\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$enabled = LLMS_Student_Dashboard::is_endpoint_enabled( 'view-certificates' );\n\t\tif ( ! $enabled ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$more = false;\n\t\tif ( $preview ) {\n\t\t\t$more = array(\n\t\t\t\t'url'  => llms_get_endpoint_url( 'view-certificates', '', llms_get_page_url( 'myaccount' ) ),\n\t\t\t\t'text' => __( 'View All My Certificates', 'lifterlms' ),\n\t\t\t);\n\t\t}\n\n\t\tob_start();\n\t\tlifterlms_template_certificates_loop( $student, $preview ? llms_get_certificates_loop_columns() : false );\n\n\t\tllms_get_template(\n\t\t\t'myaccount/dashboard-section.php',\n\t\t\tarray(\n\t\t\t\t'action'  => 'my_certificates',\n\t\t\t\t'slug'    => 'llms-my-certificates',\n\t\t\t\t'title'   => $preview ? __( 'My Certificates', 'lifterlms' ) : '',\n\t\t\t\t'content' => ob_get_clean(),\n\t\t\t\t'more'    => $more,\n\t\t\t)\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_my_courses' ) ) {\n\n\t/**\n\t * Template for My Courses section on dashboard index\n\t *\n\t * @since 3.14.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @param bool $preview Optional. If true, outputs a short list of courses (based on dashboard_recent_courses filter). Default `false`.\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_my_courses( $preview = false ) {\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$more = false;\n\t\tif ( $preview && LLMS_Student_Dashboard::is_endpoint_enabled( 'view-courses' ) ) {\n\t\t\t$more = array(\n\t\t\t\t'url'  => llms_get_endpoint_url( 'view-courses', '', llms_get_page_url( 'myaccount' ) ),\n\t\t\t\t'text' => __( 'View All My Courses', 'lifterlms' ),\n\t\t\t);\n\t\t}\n\n\t\tob_start();\n\t\tlifterlms_template_my_courses_loop( $student, $preview );\n\n\t\tllms_get_template(\n\t\t\t'myaccount/dashboard-section.php',\n\t\t\tarray(\n\t\t\t\t'action'  => 'my_courses',\n\t\t\t\t'slug'    => 'llms-my-courses',\n\t\t\t\t'title'   => $preview ? __( 'My Courses', 'lifterlms' ) : '',\n\t\t\t\t'content' => ob_get_clean(),\n\t\t\t\t'more'    => $more,\n\t\t\t)\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_student_dashboard_my_favorites' ) ) {\n\n\t/**\n\t * Template for My Favorites section on dashboard index.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @return void\n\t */\n\tfunction llms_template_student_dashboard_my_favorites() {\n\n\t\t$student = llms_get_student();\n\n\t\tif ( ! $student || ! llms_is_favorites_enabled() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tob_start();\n\t\tllms_template_my_favorites_loop( $student );\n\n\t\tllms_get_template(\n\t\t\t'myaccount/my-favorites.php',\n\t\t\tarray(\n\t\t\t\t'content' => ob_get_clean(),\n\t\t\t)\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_my_grades' ) ) {\n\n\t/**\n\t * Output the \"My Grades\" template screen on the student dashboard.\n\t *\n\t * @since 3.24.0\n\t * @since 3.26.3 Unknown.\n\t * @since 5.3.2 Cast achievement_template ID to string when comparing to the list of achievement IDs related the course/membership (list of strings).\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 6.0.0 Use updated method signature for `LLMS_Student::get_achievements()`.\n\t * @since 6.3.0 Prevent trying to access to a non existing index when retrieving the slug from the `$wp_query`.\n\t *              Fixed pagination not working when using plain permalinks.\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_my_grades() {\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\tglobal $wp_query, $wp_rewrite;\n\t\t$slug = $wp_query->query['my-grades'] ?? '';\n\n\t\t// List courses.\n\t\tif ( empty( $slug ) || false !== strpos( $slug, $wp_rewrite->pagination_base . '/' ) ) {\n\n\t\t\t/**\n\t\t\t * Filter the number of courses per pages to be displayed in my grades\n\t\t\t *\n\t\t\t * @since unknown\n\t\t\t *\n\t\t\t * @param int $per_page The number of courses per pages to be displayed. Default is `10`.\n\t\t\t */\n\t\t\t$per_page = apply_filters( 'llms_sd_grades_courses_per_page', 10 );\n\t\t\t$page     = llms_get_paged_query_var();\n\n\t\t\t$sort = llms_filter_input_sanitize_string( INPUT_GET, 'sort' );\n\t\t\tif ( ! $sort ) {\n\t\t\t\t$sort = 'date_desc';\n\t\t\t}\n\t\t\t$parts = explode( '_', $sort );\n\n\t\t\t// Validate sort.\n\t\t\t$parts[0] = llms_sanitize_with_safelist( $parts[0], array( 'date', 'title' ) );\n\t\t\t$parts[1] = llms_sanitize_with_safelist( $parts[1], array( 'desc', 'asc' ) );\n\n\t\t\t$courses = $student->get_courses(\n\t\t\t\tarray(\n\t\t\t\t\t'limit'   => $per_page,\n\t\t\t\t\t'skip'    => $per_page * ( $page - 1 ),\n\t\t\t\t\t'orderby' => $parts[0],\n\t\t\t\t\t'order'   => strtoupper( $parts[1] ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tadd_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\t\t\tllms_get_template(\n\t\t\t\t'myaccount/my-grades.php',\n\t\t\t\tarray(\n\t\t\t\t\t'courses'    => array_map( 'llms_get_post', $courses['results'] ),\n\t\t\t\t\t'student'    => $student,\n\t\t\t\t\t'sort'       => $sort,\n\t\t\t\t\t'pagination' => array(\n\t\t\t\t\t\t'current' => absint( $page ),\n\t\t\t\t\t\t'max'     => absint( ceil( $courses['found'] / $per_page ) ),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t\tremove_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\n\t\t\t// Show single.\n\t\t} else {\n\n\t\t\t$course = get_posts(\n\t\t\t\tarray(\n\t\t\t\t\t'name'      => $slug,\n\t\t\t\t\t'post_type' => 'course',\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$course = array_shift( $course );\n\t\t\tif ( $course ) {\n\t\t\t\t$course = llms_get_post( $course );\n\t\t\t}\n\n\t\t\t// It's not stupid if it works unless it is stupid.\n\t\t\t$post_ids = array_merge(\n\t\t\t\tarray( $course->get( 'id' ) ),\n\t\t\t\t$course->get_sections( 'ids' ),\n\t\t\t\t$course->get_lessons( 'ids' ),\n\t\t\t\t$course->get_quizzes()\n\t\t\t);\n\n\t\t\t$achievements = $student->get_achievements(\n\t\t\t\tarray(\n\t\t\t\t\t'related_posts' => $post_ids,\n\t\t\t\t\t'per_page'      => 1,\n\t\t\t\t\t'no_found_rows' => true,\n\t\t\t\t)\n\t\t\t)->get_awards();\n\n\t\t\t$latest_achievement = $achievements ? $achievements[0] : false;\n\n\t\t\t$last_activity = $student->get_events(\n\t\t\t\tarray(\n\t\t\t\t\t'per_page' => 1,\n\t\t\t\t\t'post_id'  => $course->get( 'id' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\tllms_get_template(\n\t\t\t\t'myaccount/my-grades-single.php',\n\t\t\t\tarray(\n\t\t\t\t\t'course'             => $course,\n\t\t\t\t\t'student'            => $student,\n\t\t\t\t\t'latest_achievement' => $latest_achievement,\n\t\t\t\t\t'last_activity'      => $last_activity ? strtotime( $last_activity[0]->get( 'updated_date' ) ) : false,\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_my_grades_table' ) ) {\n\n\t/**\n\t * Output the template for a single grades table on the student dashboard\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param LLMS_Course  $course  LLMS_Course.\n\t * @param LLMS_Student $student LLMS_Student.\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_my_grades_table( $course, $student ) {\n\t\t/**\n\t\t * Filter the student dashboard \"my grades\" table headings\n\t\t *\n\t\t * @since unknown\n\t\t *\n\t\t * @param array $section_headings \"My Grades\" table headings.\n\t\t */\n\t\t$section_headings = apply_filters(\n\t\t\t'llms_student_dashboard_my_grades_table_headings',\n\t\t\tarray(\n\t\t\t\t'completion_date' => __( 'Completion Date', 'lifterlms' ),\n\t\t\t\t'associated_quiz' => __( 'Quiz', 'lifterlms' ),\n\t\t\t\t'overall_grade'   => __( 'Grade', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tllms_get_template(\n\t\t\t'myaccount/my-grades-single-table.php',\n\t\t\tarray(\n\t\t\t\t'course'           => $course,\n\t\t\t\t'student'          => $student,\n\t\t\t\t'section_headings' => $section_headings,\n\t\t\t)\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_my_memberships' ) ) {\n\n\t/**\n\t * Template for My Memberships section on dashboard index\n\t *\n\t * @since 3.14.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @param bool $preview Optional. If true, outputs a short list of courses (based on dashboard_recent_courses filter). Default `false`.\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_my_memberships( $preview = false ) {\n\n\t\t$student = llms_get_student();\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$more = false;\n\t\tif ( $preview && LLMS_Student_Dashboard::is_endpoint_enabled( 'view-memberships' ) ) {\n\t\t\t$more = array(\n\t\t\t\t'url'  => llms_get_endpoint_url( 'view-memberships', '', llms_get_page_url( 'myaccount' ) ),\n\t\t\t\t'text' => __( 'View All My Memberships', 'lifterlms' ),\n\t\t\t);\n\t\t}\n\n\t\tob_start();\n\t\tlifterlms_template_my_memberships_loop( $student );\n\n\t\tllms_get_template(\n\t\t\t'myaccount/dashboard-section.php',\n\t\t\tarray(\n\t\t\t\t'action'  => 'my_memberships',\n\t\t\t\t'slug'    => 'llms-my-memberships',\n\t\t\t\t'title'   => $preview ? __( 'My Memberships', 'lifterlms' ) : '',\n\t\t\t\t'content' => ob_get_clean(),\n\t\t\t\t'more'    => $more,\n\t\t\t)\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_my_notifications' ) ) {\n\n\t/**\n\t * Template for My Notifications student dashboard endpoint\n\t *\n\t * @since 3.26.3\n\t * @since 3.35.0 Sanitize `$_GET` data.\n\t * @since 3.37.15 Use `in_array()`'s strict comparison.\n\t * @since 3.37.16 Fixed typo when comparing the current view.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *              Fix how the protected {@see LLMS_Notifications_Query::$max_pages} property is accessed.\n\t * @since 6.3.0 Fix paged query not working when using plain permalinks.\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_my_notifications() {\n\n\t\t$url = llms_get_endpoint_url( 'notifications', '', llms_get_page_url( 'myaccount' ) );\n\n\t\t$sections = array(\n\t\t\tarray(\n\t\t\t\t'url'  => $url,\n\t\t\t\t'name' => __( 'View Notifications', 'lifterlms' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'url'  => add_query_arg( 'sdview', 'prefs', $url ),\n\t\t\t\t'name' => __( 'Manage Preferences', 'lifterlms' ),\n\t\t\t),\n\t\t);\n\n\t\t$view = isset( $_GET['sdview'] ) ? llms_filter_input( INPUT_GET, 'sdview' ) : 'view';\n\n\t\tif ( 'view' === $view ) {\n\n\t\t\t$page = llms_get_paged_query_var();\n\n\t\t\t$notifications = new LLMS_Notifications_Query(\n\t\t\t\tarray(\n\t\t\t\t\t'page'       => $page,\n\t\t\t\t\t/**\n\t\t\t\t\t * Filter the number of notifications per page to be displayed in the dashboard's \"my_notifications\" tab.\n\t\t\t\t\t *\n\t\t\t\t\t * @since unknown\n\t\t\t\t\t *\n\t\t\t\t\t * @param int $per_page The number of notifications per page to be displayed. Default `25`.\n\t\t\t\t\t */\n\t\t\t\t\t'per_page'   => apply_filters( 'llms_sd_my_notifications_per_page', 25 ),\n\t\t\t\t\t'subscriber' => get_current_user_id(),\n\t\t\t\t\t'sort'       => array(\n\t\t\t\t\t\t'created' => 'DESC',\n\t\t\t\t\t\t'id'      => 'DESC',\n\t\t\t\t\t),\n\t\t\t\t\t'types'      => 'basic',\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$pagination = array(\n\t\t\t\t'max'     => $notifications->get_max_pages(),\n\t\t\t\t'current' => $page,\n\t\t\t);\n\n\t\t\t$args = array(\n\t\t\t\t'notifications' => $notifications->get_notifications(),\n\t\t\t\t'pagination'    => $pagination,\n\t\t\t\t'sections'      => $sections,\n\t\t\t);\n\n\t\t} else {\n\n\t\t\t/**\n\t\t\t * Filter the types of subscriber notification which can be managed\n\t\t\t *\n\t\t\t * @since unknown\n\t\t\t *\n\t\t\t * @param array $types The array of manageable types. Default is `array( 'email' )`.\n\t\t\t */\n\t\t\t$types = apply_filters( 'llms_notification_subscriber_manageable_types', array( 'email' ) );\n\n\t\t\t$settings = array();\n\t\t\t$student  = new LLMS_Student( get_current_user_id() );\n\n\t\t\tforeach ( llms()->notifications()->get_controllers() as $controller ) {\n\n\t\t\t\tforeach ( $types as $type ) {\n\n\t\t\t\t\t$configs = $controller->get_subscribers_settings( $type );\n\n\t\t\t\t\tif ( in_array( 'student', array_keys( $configs ), true ) && 'yes' === $configs['student'] ) {\n\n\t\t\t\t\t\tif ( ! isset( $settings[ $type ] ) ) {\n\t\t\t\t\t\t\t$settings[ $type ] = array();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$settings[ $type ][ $controller->id ] = array(\n\t\t\t\t\t\t\t'name'  => $controller->get_title(),\n\t\t\t\t\t\t\t'value' => $student->get_notification_subscription( $type, $controller->id, 'yes' ),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$args = array(\n\t\t\t\t'sections' => $sections,\n\t\t\t\t'settings' => $settings,\n\t\t\t);\n\n\t\t}\n\n\t\tadd_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\n\t\tllms_get_template( 'myaccount/my-notifications.php', $args );\n\n\t\tremove_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_navigation' ) ) {\n\n\t/**\n\t * Dashboard Navigation template\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_navigation() {\n\t\tllms_get_template( 'myaccount/navigation.php' );\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_title' ) ) {\n\t/**\n\t * Dashboard title template\n\t *\n\t * @since 3.0.0\n\t * @since 3.14.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_title() {\n\t\t$data  = LLMS_Student_Dashboard::get_current_tab();\n\t\t$title = isset( $data['title'] ) ? $data['title'] : '';\n\n\t\t/**\n\t\t * Filter the student dasbhoard title for the current tab\n\t\t *\n\t\t * @since unknown\n\t\t *\n\t\t * @param string $title The student dashboard title.\n\t\t */\n\t\techo wp_kses_post( apply_filters( 'lifterlms_student_dashboard_title', '<h2 class=\"llms-sd-title\">' . $title . '</h2>', $data ) );\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_wrapper_close' ) ) :\n\t/**\n\t * Output the student dashboard wrapper closing tags\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_wrapper_close() {\n\t\techo '</div><!-- .llms-student-dashboard -->';\n\t}\nendif;\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_wrapper_open' ) ) :\n\t/**\n\t * Output the student dashboard wrapper opening tags\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t * @since 7.8.0\n\t *\n\t * @param string $layout Dashboard layout. Accepts \"stacked\" (default) or \"columns\".\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_wrapper_open( $layout ) {\n\t\t$current = LLMS_Student_Dashboard::get_current_tab( 'slug' );\n\t\techo '<div class=\"llms-student-dashboard ' . esc_attr( $current ) . ' llms-sd-layout-' . esc_attr( $layout ) . '\" data-current=\"' . esc_attr( $current ) . '\">';\n\t}\nendif;\n\nif ( ! function_exists( 'lifterlms_template_student_dashboard_select_mobile_navigation' ) ) :\n\n\t/**\n\t * Output the student dashboard mobile navigation\n\t *\n\t * @since 9.0.0\n\t *\n\t * @param string $current The current tab slug.\n\t * @return void\n\t */\n\tfunction lifterlms_template_student_dashboard_select_mobile_navigation( $current ) {\n\t\t?>\n\t\t<select onChange=\"window.location.replace(this.options[this.selectedIndex].value)\">\n\t\t\t<?php foreach ( LLMS_Student_Dashboard::get_tabs_for_nav() as $var => $data ) : ?>\n\t\t\t\t<option value=\"<?php echo esc_attr( esc_url( $data['url'] ) ); ?>\" <?php selected( $var, $current ); ?>>\n\t\t\t\t\t<?php echo esc_html( $data['title'] ); ?>\n\t\t\t\t</option>\n\t\t\t<?php endforeach; ?>\n\t\t</select>\n\t\t<?php\n\t}\nendif;\n\n/**\n * Modify the pagination links displayed on endpoints using the default LLMS loop.\n *\n * @since 3.24.0\n * @since 3.26.3 Unknown.\n * @since 6.3.0 Fixed pagination when using plain permalinks.\n * @since 7.2.0 Made sure the pagination links is not altered when not in the LifterLMS dashboard context.\n *\n * @param string $link Default link.\n * @return string\n */\nfunction llms_modify_dashboard_pagination_links( $link ) {\n\n\t/**\n\t * Allow 3rd parties to disable dashboard pagination link rewriting\n\t *\n\t * Resolves compatibility issues with LifterLMS WooCommerce.\n\t *\n\t * @since unknown\n\t * @since 7.2.0 Defaults to `false` only on the LifterLMS dashboard context, while `true` elsewhere.\n\t *\n\t * @param bool   $disable Whether or not the dashboard pagination links should be disabled.\n\t *                        Default `false` in the LifterLMS dashboard context, `true` elsewhere.\n\t * @param string $link    The default link.\n\t */\n\tif ( apply_filters( 'llms_modify_dashboard_pagination_links_disable', ! is_page( llms_get_page_id( 'myaccount' ) ), $link ) ) {\n\t\treturn $link;\n\t}\n\n\tglobal $wp_rewrite;\n\n\t$query = wp_parse_url( $link, PHP_URL_QUERY );\n\n\tif ( $query ) {\n\t\t$link = str_replace( '?' . $query, '', $link );\n\t}\n\t// No plain permalinks.\n\tif ( get_option( 'permalink_structure' ) ) {\n\t\t$parts = explode( '/', untrailingslashit( $link ) );\n\t\t$page  = end( $parts );\n\t\t$link  = llms_get_endpoint_url( LLMS_Student_Dashboard::get_current_tab( 'slug' ), $wp_rewrite->pagination_base . '/' . $page . '/', llms_get_page_url( 'myaccount' ) );\n\t} else { // With plain permalinks.\n\t\tpreg_match( '/paged?=([0-9]+)/', $link, $pages ); // Extract the 'page(d)' var.\n\t\t$paged  = empty( $pages ) || count( $pages ) < 2 || $pages[1] < 2 ? '' : $pages[0]; // No pagination or page 1 nothing to add.\n\t\t$query .= $paged ? '&' . $paged : '';\n\t\t$link   = home_url();\n\t}\n\n\tif ( $query ) {\n\t\t$link .= '?' . $query;\n\t}\n\n\treturn $link;\n}\n\n/**\n * Output content for a single cell on the student single course grades table\n *\n * @since 3.24.0\n *\n * @param string       $id           Key of the table cell.\n * @param LLMS_Lesson  $lesson       LLMS_Lesson.\n * @param LLMS_Student $student      LLMS_Student.\n * @param array        $restrictions Restriction data from `llms_page_restricted()`.\n * @return void\n */\nfunction llms_sd_my_grades_table_content( $id, $lesson, $student, $restrictions ) {\n\n\tob_start();\n\n\t/**\n\t * Fires before the student dashboard my grades table cell content output\n\t *\n\t * The dynamic portion of the hook name, `$id`, refers to the key of the table cell.\n\t *\n\t * @since unknown\n\t *\n\t * @param LLMS_Lesson  $lesson       LLMS_Lesson instance.\n\t * @param LLMS_Student $student      LLMS_Student instance.\n\t * @param array        $restrictions Restriction data from `llms_page_restricted()`.\n\t */\n\tdo_action( 'llms_sd_my_grades_table_content_' . $id . '_before', $lesson, $student, $restrictions );\n\n\tswitch ( $id ) {\n\n\t\tcase 'completion_date':\n\t\t\tif ( $student->is_complete( $lesson->get( 'id' ) ) ) {\n\t\t\t\techo esc_html( $student->get_completion_date( $lesson->get( 'id' ), get_option( 'date_format' ) ) );\n\t\t\t} else {\n\t\t\t\techo '&ndash;';\n\t\t\t}\n\t\t\tbreak;\n\n\t\tcase 'associated_quiz':\n\t\t\tif ( $lesson->has_quiz() && $restrictions['is_restricted'] ) {\n\t\t\t\techo '<i class=\"fa fa-lock\" aria-hidden=\"true\"></i>';\n\t\t\t} elseif ( $lesson->has_quiz() ) {\n\t\t\t\t$attempt = $student->quizzes()->get_last_attempt( $lesson->get( 'quiz' ) );\n\t\t\t\t$url     = $attempt ? $attempt->get_permalink() : get_permalink( $lesson->get( 'quiz' ) );\n\t\t\t\t$text    = $attempt ? __( 'Review', 'lifterlms' ) : __( 'Start', 'lifterlms' );\n\t\t\t\tif ( $attempt ) {\n\t\t\t\t\techo '<span class=\"llms-status llms-' . esc_attr( $attempt->get( 'status' ) ) . '\">' . esc_html( $attempt->l10n( 'status' ) ) . '</span>';\n\t\t\t\t}\n\t\t\t\techo '<a href=\"' . esc_url( $url ) . '\">' . esc_html( $text ) . '</a>';\n\t\t\t} else {\n\t\t\t\techo '&ndash;';\n\t\t\t}\n\t\t\tbreak;\n\n\t\tcase 'overall_grade':\n\t\t\t$grade = $student->get_grade( $lesson->get( 'id' ) );\n\t\t\techo is_numeric( $grade ) ? wp_kses_post( llms_get_donut( $grade, '', 'mini' ) ) : '&ndash;';\n\t\t\tbreak;\n\n\t}\n\n\t/**\n\t * Fires after the student dashboard my grades default table cell content output\n\t *\n\t * If id id one oare `completion_date`, `associated_quiz`, `overall_grade`.\n\t * Can be used to display custom table cells.\n\t *\n\t * The dynamic portion of the hook name, `$id`, refers to the key of the table cell.\n\t *\n\t * @since unknown\n\t *\n\t * @param LLMS_Lesson  $lesson       LLMS_Lesson instance.\n\t * @param LLMS_Student $student      LLMS_Student instance.\n\t * @param array        $restrictions Restriction data from `llms_page_restricted()`.\n\t */\n\tdo_action( 'llms_sd_my_grades_table_content_' . $id, $lesson, $student, $restrictions );\n\n\t$html = ob_get_clean();\n\n\t/**\n\t * Filters the HTML returned by llms_sd_my_grades_table_content().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string       $html         The cell HTML.\n\t * @param string       $id           Key of the table cell.\n\t * @param LLMS_Lesson  $lesson       LLMS_Lesson.\n\t * @param LLMS_Student $student      LLMS_Student.\n\t * @param array        $restrictions Restriction data from `llms_page_restricted()`.\n\t */\n\treturn apply_filters( 'llms_sd_my_grades_table_content', $html, $id, $lesson, $student, $restrictions );\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.dashboard.widgets.php",
    "content": "<?php\n/**\n * Template functions for displaying stat widgets on the student dashboard\n *\n * @package LifterLMS/Functions\n *\n * @since 3.24.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Main function used to display a dashboard widget\n *\n * @param    string $title       Title of the widget.\n * @param    string $content     Content (HTML) of the widget body.\n * @param    string $empty_text  Content (text) to display if $content is empty.\n * @return   void\n * @since    3.24.0\n * @version  3.24.0\n */\nfunction llms_sd_dashboard_widget( $title, $content, $empty_text = '' ) {\n\t?>\n\t<div class=\"llms-sd-widget\">\n\t\t<h4 class=\"llms-sd-widget-title\"><?php echo esc_html( $title ); ?></h4>\n\t\t<?php if ( $content ) : ?>\n\t\t\t<?php\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped before being passed to dashboard widget.\n\t\t\techo $content;\n\t\t\t?>\n\t\t<?php elseif ( ! $content && $empty_text ) : ?>\n\t\t\t<p class=\"llms-sd-widget-empty\"><?php echo esc_html( $empty_text ); ?></p>\n\t\t<?php endif; ?>\n\t</div>\n\t<?php\n}\n\n/**\n * Displays a date widget\n *\n * @param   string $title      Title of the widget.\n * @param   int    $timestamp  Timestamp used to display the date.\n * @param   string $empty_text Content (text) to display if $content is empty.\n * @return  void\n * @since   3.24.0\n * @version 3.24.0\n */\nfunction llms_sd_dashboard_date_widget( $title, $timestamp, $empty_text = '' ) {\n\n\t$html = '';\n\tif ( $timestamp ) {\n\t\tob_start();\n\t\t?>\n\t\t<div class=\"llms-sd-date\">\n\t\t\t<span class=\"month\"><?php echo esc_html( date_i18n( 'F', $timestamp ) ); ?></span>\n\t\t\t<span class=\"day\"><?php echo esc_html( date_i18n( 'j', $timestamp ) ); ?></span>\n\t\t\t<span class=\"year\"><?php echo esc_html( date_i18n( 'Y', $timestamp ) ); ?></span>\n\t\t\t<span class=\"diff\"><?php printf( esc_html__( '%s ago', 'lifterlms' ), esc_html( llms_get_date_diff( $timestamp, current_time( 'timestamp' ) ) ) ); ?>\n\t\t</div>\n\t\t<?php\n\t\t$html = ob_get_clean();\n\t}\n\n\tllms_sd_dashboard_widget( $title, $html, $empty_text );\n}\n\n/**\n * Displays a donut chart widget\n *\n * @param    string $title  Title of the widget.\n * @param    float  $perc   donut chart percentage.\n * @param    string $text   Text to display within the donut.\n * @param    string $size   Size of the chart.\n * @return   void\n * @since    3.24.0\n * @version  3.24.0\n */\nfunction llms_sd_dashboard_donut_widget( $title, $perc, $text = '', $size = 'medium' ) {\n\n\tllms_sd_dashboard_widget( $title, llms_get_donut( $perc, $text, $size ) );\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.loop.php",
    "content": "<?php\n/**\n * Template functions for loops (catalogs)\n *\n * @package LifterLMS/Functions\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! function_exists( 'lifterlms_archive_description' ) ) {\n\t/**\n\t * Output the archive description for LifterLMS catalogs pages and post type / tax archives.\n\t *\n\t * @since 3.16.10\n\t * @since 3.19.0 Unknown.\n\t * @since 4.10.0 Moved logic to `lifterlms_get_archive_description()` so the function can be called without outputting the content.\n\t *\n\t * @see lifterlms_get_archive_description()\n\t *\n\t * @return void\n\t */\n\tfunction lifterlms_archive_description() {\n\t\techo wp_kses_post( lifterlms_get_archive_description() );\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_get_archive_description' ) ) {\n\t/**\n\t * Retrieve the archive description for LifterLMS catalogs pages and post type / tax archives.\n\t *\n\t * If content is added to the course/membership catalog page via the WP editor, output it as the archive description before the loop.\n\t *\n\t * @since 4.10.0 Moved from `lifterlms_archive_description()`.\n\t *               Adjusted filter `llms_archive_description` to always run instead of only running if content exists to display,\n\t *               this allows developers to filter the content even when an empty string is returned.\n\t * @since 7.3.0 Fixed PHP Warning when no course/membership catalog page was set or if the\n\t *              selected page doesn't exist anymore.\n\t *\n\t * @return string\n\t */\n\tfunction lifterlms_get_archive_description() {\n\n\t\t$content = '';\n\t\t$page_id = false;\n\n\t\t// Get the page id for the catalog page setup in LLMS settings.\n\t\tif ( is_post_type_archive( 'course' ) || is_tax( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track' ) ) ) {\n\t\t\t$page_id = llms_get_page_id( 'courses' );\n\t\t} elseif ( is_post_type_archive( 'llms_membership' ) || is_tax( array( 'membership_tag', 'membership_cat' ) ) ) {\n\t\t\t$page_id = llms_get_page_id( 'memberships' );\n\t\t}\n\n\t\t// If a description is setup for the taxonomy term, use that description.\n\t\tif ( is_tax( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) ) ) {\n\t\t\t$content = get_the_archive_description();\n\t\t}\n\n\t\t// If we don't have a description, try to pull it from the page's content area.\n\t\tif ( empty( $content ) && (int) $page_id > 0 ) {\n\t\t\t$page    = get_post( $page_id );\n\t\t\t$content = $page ? $page->post_content : $content;\n\t\t}\n\n\t\t/**\n\t\t * Filter the archive description\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 4.10.0 Added `$page_id` parameter.\n\t\t *\n\t\t * @param string    $content HTML description string.\n\t\t * @param int|false $page_id WP_Post ID of the archive page being displayed.\n\t\t */\n\t\treturn apply_filters( 'llms_archive_description', llms_content( $content ), $page_id );\n\t}\n}\n\n/**\n * Output a LifterLMS Loop\n *\n * @param    obj $query  WP_Query, uses global $wp_query if not supplied\n * @return   void\n * @since    3.14.0\n * @version  3.14.0\n */\nfunction lifterlms_loop( $query = null ) {\n\n\tglobal $wp_query;\n\t$temp = null;\n\n\tif ( $query ) {\n\t\t$temp     = $wp_query;\n\t\t$wp_query = $query;\n\t}\n\n\tif ( have_posts() ) {\n\n\t\t/**\n\t\t * lifterlms_before_loop hook\n\t\t *\n\t\t * @hooked lifterlms_loop_start - 10\n\t\t */\n\t\tdo_action( 'lifterlms_before_loop' );\n\n\t\twhile ( have_posts() ) {\n\t\t\tthe_post();\n\t\t\tllms_get_template_part( 'loop/content', get_post_type() );\n\t\t}\n\n\t\t/**\n\t\t * lifterlms_before_loop hook\n\t\t *\n\t\t * @hooked lifterlms_loop_end - 10\n\t\t */\n\t\tdo_action( 'lifterlms_after_loop' );\n\n\t\tllms_get_template_part( 'loop/pagination' );\n\n\t} else {\n\n\t\tllms_get_template( 'loop/none-found.php' );\n\t}\n\n\tif ( $query ) {\n\t\t$wp_query = $temp;\n\t\twp_reset_postdata();\n\t}\n}\n\n/**\n * Link pagination helper.\n *\n * This is a wrapper around WP's `paginate_links()` method with common styling\n * and helpers for use within LifterLMS.\n *\n * @since 6.0.0\n *\n * @param array $args {\n *     Pagination arguments.\n *\n *     @type integer $current Current page number. Defaults to `1` or the value of `get_query_var( 'paged' )`.\n *     @type integer $total   Total number of pages to display. Defaults to `1` or `$wp_query->max_num_pages`.\n *     @type string  $context Display context. Adds additional customization depending on the context. Supported\n *                            contexts are \"student_dashboard\" which automatically filters links for use on the\n *                            dashboard.\n * }\n * @return string\n */\nfunction llms_paginate_links( $args ) {\n\n\tglobal $wp_query;\n\n\t$args = wp_parse_args(\n\t\t$args,\n\t\tarray(\n\t\t\t'current' => max( 1, get_query_var( 'paged' ) ),\n\t\t\t'total'   => max( 1, $wp_query->max_num_pages ),\n\t\t\t'context' => '',\n\t\t)\n\t);\n\n\t// Don't display pagination if there's only one page of results and `show_for_single` isn't explicitly enabled.\n\tif ( $args['total'] <= 1 ) {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Filter the list of CSS classes on the pagination wrapper element.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @param string[] $classes Array of CSS classes.\n\t */\n\t$classes = apply_filters( 'llms_get_pagination_wrapper_classes', array( 'llms-pagination' ) );\n\n\tif ( 'student_dashboard' === $args['context'] ) {\n\t\tadd_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\t}\n\n\t$links = paginate_links(\n\t\tarray(\n\t\t\t'base'      => str_replace( 999999, '%#%', get_pagenum_link( 999999, false ) ),\n\t\t\t'format'    => '?page=%#%',\n\t\t\t'total'     => $args['total'],\n\t\t\t'current'   => $args['current'],\n\t\t\t'prev_next' => true,\n\t\t\t// Translators: %s = Left double arrow character.\n\t\t\t'prev_text' => sprintf( _x( '%s Previous', 'pagination link text', 'lifterlms' ), '«' ),\n\t\t\t// Translators: %s = Right double arrow character.\n\t\t\t'next_text' => sprintf( _x( 'Next %s', 'pagination link text', 'lifterlms' ), '»' ),\n\t\t\t'type'      => 'list',\n\t\t)\n\t);\n\n\tif ( 'student_dashboard' === $args['context'] ) {\n\t\tremove_filter( 'paginate_links', 'llms_modify_dashboard_pagination_links' );\n\t}\n\n\treturn sprintf(\n\t\t'<nav class=\"%1$s\">%2$s</nav>',\n\t\tesc_attr( implode( ' ', $classes ) ),\n\t\t$links\n\t);\n}\n\n/**\n * Retrieve the number of columns for llms loops\n *\n * @return   int\n * @since    3.14.0\n * @version  3.14.0\n */\nfunction llms_get_loop_columns() {\n\treturn absint( apply_filters( 'lifterlms_loop_columns', 3 ) );\n}\n\n/**\n * Get classes to add to the loop wrapper based on the queried object\n * Used in templates/loop/loop-start.php\n *\n * @return   string\n * @since    3.0.0\n * @version  3.14.0\n */\nfunction llms_get_loop_list_classes() {\n\n\t$classes = array();\n\n\t$obj = get_queried_object();\n\n\tif ( $obj && $obj->name ) {\n\t\t$classes[] = 'llms-' . str_replace( 'llms_', '', $obj->name ) . '-list';\n\t}\n\n\t$classes[] = sprintf( 'cols-%d', llms_get_loop_columns() );\n\n\treturn ' ' . implode( ' ', apply_filters( 'llms_get_loop_list_classes', $classes ) );\n}\n\n\n/**\n * Get archive loop end\n *\n * @return  void\n * @since   1.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_loop_end' ) ) {\n\tfunction lifterlms_loop_end() {\n\t\tllms_get_template( 'loop/loop-end.php' );\n\t}\n}\n\n\n/**\n * Output a featured video on the course tile in a LifterLMS Loop.\n *\n * @since 3.3.0\n * @since 7.1.3 Add div tag to wrap featured video output in loop.\n *\n * @return void\n */\nfunction lifterlms_loop_featured_video() {\n\tglobal $post;\n\tif ( 'course' === $post->post_type || 'llms_membership' === $post->post_type ) {\n\t\t$product = llms_get_post( $post );\n\t\tif ( 'yes' === $product->get( 'tile_featured_video' ) ) {\n\t\t\t$video = $product->get_video();\n\t\t\tif ( $video ) {\n\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\techo '<div class=\"llms-video-wrapper\">' . $video . '</div>';\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Archive loop link end\n *\n * @return  void\n * @since   1.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_loop_link_end' ) ) {\n\tfunction lifterlms_loop_link_end() {\n\t\techo '</a><!-- .llms-loop-link -->';\n\t}\n}\n\n/**\n * Archive loop link start\n *\n * @return  void\n * @since   1.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_loop_link_start' ) ) {\n\tfunction lifterlms_loop_link_start() {\n\t\techo '<a class=\"llms-loop-link\" href=\"' . esc_url( get_the_permalink() ) . '\">';\n\t}\n}\n\n/**\n * Get Archive loop start\n *\n * @return  void\n * @since   1.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_loop_start' ) ) {\n\tfunction lifterlms_loop_start() {\n\t\tllms_get_template( 'loop/loop-start.php' );\n\t}\n}\n\n/**\n * Get loop item author template\n *\n * @return void\n * @since   1.0.0\n * @version 1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_loop_author' ) ) {\n\n\tfunction lifterlms_template_loop_author() {\n\t\tllms_get_template( 'loop/author.php' );\n\t}\n}\n\n/**\n * Course Difficulty Template Include\n *\n * @return void\n * @since   1.0.0\n * @version 1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_loop_difficulty' ) ) {\n\n\tfunction lifterlms_template_loop_difficulty() {\n\t\tif ( 'course' === get_post_type( get_the_ID() ) ) {\n\t\t\tllms_get_template( 'course/difficulty.php' );\n\t\t}\n\t}\n}\n\n/**\n * Count of total lessons in a course.\n *\n * @since 7.5.0\n *\n * @return void.\n */\nif ( ! function_exists( 'lifterlms_template_loop_lesson_count' ) ) {\n\n\tfunction lifterlms_template_loop_lesson_count() {\n\t\tif ( 'course' === get_post_type( get_the_ID() ) ) {\n\t\t\tllms_get_template( 'course/lesson-count.php' );\n\t\t}\n\t}\n}\n\nif ( ! function_exists( 'lifterlms_template_loop_featured_pricing_information' ) ) {\n\tfunction lifterlms_template_loop_featured_pricing_information() {\n\t\tif ( in_array( get_post_type( get_the_ID() ), array( 'course', 'llms_membership' ) ) ) {\n\t\t\tllms_get_template( 'loop/featured-pricing.php' );\n\t\t}\n\t}\n}\n\n/**\n * Show enrollment date meta\n * used on Dashboard only\n *\n * @return void\n * @since   1.0.0\n * @version 1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_loop_enroll_date' ) ) {\n\n\tfunction lifterlms_template_loop_enroll_date() {\n\t\tllms_get_template( 'loop/enroll-date.php' );\n\t}\n}\n\n/**\n * Show enrollment status meta\n * used on dashboard only\n *\n * @return void\n * @since   1.0.0\n * @version 1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_loop_enroll_status' ) ) {\n\n\tfunction lifterlms_template_loop_enroll_status() {\n\t\tllms_get_template( 'loop/enroll-status.php' );\n\t}\n}\n\n/**\n * Lesson Length Template Include\n *\n * @return void\n * @since   1.0.0\n * @version 1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_loop_length' ) ) {\n\n\tfunction lifterlms_template_loop_length() {\n\t\tif ( 'course' === get_post_type( get_the_ID() ) ) {\n\t\t\tllms_get_template( 'course/length.php' );\n\t\t}\n\t}\n}\n\n/**\n * Archive loop progress bar for courses\n *\n * @return  void\n * @since   1.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_template_loop_progress' ) ) {\n\tfunction lifterlms_template_loop_progress() {\n\t\t$uid = get_current_user_id();\n\t\t$cid = get_the_ID();\n\t\tif ( 'course' === get_post_type() && $uid ) {\n\n\t\t\t$student = new LLMS_Student( $uid );\n\t\t\tlifterlms_course_progress_bar( $student->get_progress( $cid, 'course' ), false, false );\n\n\t\t}\n\t}\n}\n\n/**\n * Product Thumbnail Template Include\n *\n * @return void\n * @since   1.0.0\n * @version 1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_loop_thumbnail' ) ) {\n\n\tfunction lifterlms_template_loop_thumbnail() {\n\t\tllms_get_template( 'loop/featured-image.php' );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.pricing.table.php",
    "content": "<?php\n/**\n * Template functions for pricing tables\n *\n * @package LifterLMS/Functions\n *\n * @since 3.23.0\n * @version 3.38.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieve a list of CSS classes for a single access plan element\n *\n * @since 3.23.0\n *\n * @param LLMS_Access_Plan $plan Access plan object.\n * @return string\n */\nfunction llms_get_access_plan_classes( $plan ) {\n\t$classes = array(\n\t\t'llms-access-plan',\n\t\tsprintf( 'llms-access-plan-%d', $plan->get( 'id' ) ),\n\t);\n\tif ( $plan->is_featured() ) {\n\t\t$classes[] = 'featured';\n\t}\n\tif ( $plan->is_on_sale() ) {\n\t\t$classes[] = 'on-sale';\n\t}\n\treturn implode( ' ', apply_filters( 'llms_access_plan_classes', $classes, $plan ) );\n}\n\nif ( ! function_exists( 'llms_template_access_plan' ) ) {\n\t/**\n\t * Include single access plan template within the pricing table\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_access_plan_button' ) ) {\n\t/**\n\t * Include Single Access Plan Button Template\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan_button( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan-button.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_access_plan_description' ) ) {\n\t/**\n\t * Include Single Access Plan Description Template\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan_description( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan-description.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_access_plan_feature' ) ) {\n\t/**\n\t * Include Single Access Plan Featured Template\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan_feature( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan-feature.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_access_plan_pricing' ) ) {\n\t/**\n\t * Include Single Access Plan pricing Template\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan_pricing( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan-pricing.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_access_plan_restrictions' ) ) {\n\t/**\n\t * Include Single Access Plan restrictions Template\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan_restrictions( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan-restrictions.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_access_plan_title' ) ) {\n\t/**\n\t * Include Single Access Plan title Template\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan_title( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan-title.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_access_plan_trial' ) ) {\n\t/**\n\t * Include Single Access Plan trial Template\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param LLMS_Access_Plan $plan Access plan object.\n\t * @return void\n\t */\n\tfunction llms_template_access_plan_trial( $plan ) {\n\t\tllms_get_template(\n\t\t\t'product/access-plan-trial.php',\n\t\t\tcompact( 'plan' )\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_product_not_purchasable' ) ) {\n\t/**\n\t * Include template for products that aren't purchasable\n\t *\n\t * @since 3.38.0\n\t *\n\t * @param int $post_id Optional. WP Post ID of the product. Default is ID of the global $post.\n\t * @return void\n\t */\n\tfunction llms_template_product_not_purchasable( $post_id = null ) {\n\n\t\t$post_id = $post_id ? $post_id : get_the_ID();\n\t\t$product = new LLMS_Product( $post_id );\n\n\t\tllms_get_template(\n\t\t\t'product/not-purchasable.php',\n\t\t\tcompact( 'product' )\n\t\t);\n\t}\n}\n\n\nif ( ! function_exists( 'lifterlms_template_pricing_table' ) ) {\n\t/**\n\t * Include pricing table for a LifterLMS Product (course or membership)\n\t *\n\t * @since 3.0.0\n\t * @since 3.38.0 Fixed spelling error in variable passed to template.\n\t * @since 6.0.0 Removed the deprecated and misspelled `$purchaseable` global variable.\n\t *\n\t * @param int $post_id Optional. WP Post ID of the product. Default is ID of the global $post.\n\t * @return void\n\t */\n\tfunction lifterlms_template_pricing_table( $post_id = null ) {\n\n\t\t$post_id = $post_id ? $post_id : get_the_ID();\n\t\t$product = new LLMS_Product( $post_id );\n\n\t\t/**\n\t\t * Filter current user's enrollment status\n\t\t *\n\t\t * This filter is used to customize the output behavior of the pricing table.\n\t\t * It does not modify the user's enrollment status.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 9.1.2 Added product param.\n\t\t *\n\t\t * @param boolean $is_enrolled User's current enrollment status.\n\t\t * @param LLMS_Product $product Product for the pricing table.\n\t\t */\n\t\t$is_enrolled = apply_filters(\n\t\t\t'llms_product_pricing_table_enrollment_status',\n\t\t\tllms_is_user_enrolled( get_current_user_id(), $product->get( 'id' ) ),\n\t\t\t$product\n\t\t);\n\n\t\t$purchasable      = $product->is_purchasable();\n\t\t$has_free         = $product->has_free_access_plan();\n\t\t$has_restrictions = $product->has_restrictions();\n\n\t\tllms_get_template(\n\t\t\t'product/pricing-table.php',\n\t\t\tcompact( 'product', 'is_enrolled', 'purchasable', 'has_free', 'has_restrictions' )\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.privacy.php",
    "content": "<?php\n/**\n * Privacy related template functions\n *\n * @package LifterLMS/Functions\n *\n * @since 3.18.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Get the HTML for the Terms field displayed on reg forms\n *\n * @since 3.0.0\n * @since 3.18.1 Unknown.\n *\n * @param boolean $echo Echo the data if true, return otherwise.\n * @return string\n */\nif ( ! function_exists( 'llms_agree_to_terms_form_field' ) ) {\n\n\tfunction llms_agree_to_terms_form_field( $echo = true ) {\n\n\t\t// Because `do_action()` passes empty string.\n\t\tif ( '' === $echo ) {\n\t\t\t$echo = true;\n\t\t}\n\n\t\t$ret = '';\n\n\t\tif ( llms_are_terms_and_conditions_required() ) {\n\n\t\t\t$ret = llms_form_field(\n\t\t\t\tarray(\n\t\t\t\t\t'columns'         => 12,\n\t\t\t\t\t'description'     => '',\n\t\t\t\t\t'default'         => 'no',\n\t\t\t\t\t'id'              => 'llms_agree_to_terms',\n\t\t\t\t\t'label'           => llms_get_terms_notice( true ),\n\t\t\t\t\t'last_column'     => true,\n\t\t\t\t\t'required'        => true,\n\t\t\t\t\t'type'            => 'checkbox',\n\t\t\t\t\t'value'           => 'yes',\n\t\t\t\t\t'wrapper_classes' => 'llms-agree-to-terms-wrapper',\n\t\t\t\t),\n\t\t\t\tfalse\n\t\t\t);\n\n\t\t}\n\n\t\t$ret = apply_filters( 'llms_agree_to_terms_form_field', $ret, $echo );\n\n\t\tif ( $echo ) {\n\n\t\t\techo wp_kses( $ret, LLMS_ALLOWED_HTML_FORM_FIELDS );\n\t\t\treturn;\n\n\t\t}\n\n\t\treturn $ret;\n\t}\n}\n\n\n\n/**\n * Get the HTML for the Privacy Policy section on checkout / registration forms\n *\n * @since 3.0.0\n * @since 3.18.1 Unknown.\n * @since 5.0.0 Update to support changes to `llms_form_field()`.\n *\n * @param boolean $echo Echo the data if true, return otherwise.\n * @return string\n */\nif ( ! function_exists( 'llms_privacy_policy_form_field' ) ) {\n\n\tfunction llms_privacy_policy_form_field( $echo = true ) {\n\n\t\t// Because `do_action()` passes empty string.\n\t\tif ( '' === $echo ) {\n\t\t\t$echo = true;\n\t\t}\n\n\t\t$ret = '';\n\n\t\t$notice = llms_get_privacy_notice( true );\n\t\tif ( $notice ) {\n\t\t\t$ret = llms_form_field(\n\t\t\t\tarray(\n\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t'value'       => '<p>' . $notice . '</p>',\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t'type'        => 'html',\n\t\t\t\t\t'id'          => 'llms-privacy-policy',\n\t\t\t\t),\n\t\t\t\tfalse\n\t\t\t);\n\t\t}\n\n\t\t$ret = apply_filters( 'llms_privacy_policy_form_field', $ret, $echo );\n\n\t\tif ( $echo ) {\n\n\t\t\techo wp_kses( $ret, LLMS_ALLOWED_HTML_FORM_FIELDS );\n\t\t\treturn;\n\n\t\t}\n\n\t\treturn $ret;\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.templates.quizzes.php",
    "content": "<?php\n/**\n * Template functions for quizzes & questions\n *\n * @package LifterLMS/Functions\n *\n * @since 1.0.0\n * @version 3.16.0\n */\n\nif ( ! defined( 'ABSPATH' ) ) {\n\texit; }\n\n/**\n * Single question main content template\n *\n * @return void\n * @since    3.16.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_question_content' ) ) {\n\tfunction lifterlms_template_question_content( $args ) {\n\n\t\t$type = $args['question']->get( 'question_type' );\n\n\t\t$template = apply_filters( 'llms_get_' . $type . '_question_template', 'quiz/questions/content-' . $type, $args['question'] );\n\t\tllms_get_template(\n\t\t\t$template . '.php',\n\t\t\tarray(\n\t\t\t\t'question' => $args['question'],\n\t\t\t\t'attempt'  => $args['attempt'],\n\t\t\t)\n\t\t);\n\n\t}\n}\n\n/**\n * Single question description template\n *\n * @return void\n * @since    3.16.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_question_description' ) ) {\n\tfunction lifterlms_template_question_description( $args ) {\n\t\tllms_get_template( 'quiz/questions/description.php', $args );\n\t}\n}\n\n/**\n * Single question featured image template\n *\n * @return void\n * @since    3.16.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_question_image' ) ) {\n\tfunction lifterlms_template_question_image( $args ) {\n\t\tllms_get_template( 'quiz/questions/image.php', $args );\n\t}\n}\n\n/**\n * Single question featured video template\n *\n * @return void\n * @since    3.16.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_question_video' ) ) {\n\tfunction lifterlms_template_question_video( $args ) {\n\t\tllms_get_template( 'quiz/questions/video.php', $args );\n\t}\n}\n\n/**\n * Question Wrapper End Template Include\n *\n * @return void\n * @since    1.0.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_question_wrapper_end' ) ) {\n\tfunction lifterlms_template_question_wrapper_end( $args ) {\n\t\tllms_get_template( 'quiz/questions/wrapper-end.php', $args );\n\t}\n}\n\n/**\n * Question Wrapper Start Template Include\n *\n * @return void\n * @since    1.0.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_question_wrapper_start' ) ) {\n\tfunction lifterlms_template_question_wrapper_start( $args ) {\n\t\tllms_get_template( 'quiz/questions/wrapper-start.php', $args );\n\t}\n}\n\n/**\n * Passing Percent Template Include\n *\n * @return void\n * @since    1.0.0\n * @version  1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_quiz_meta_info' ) ) {\n\tfunction lifterlms_template_quiz_meta_info() {\n\t\tllms_get_template( 'quiz/meta-information.php' );\n\t}\n}\n\n/**\n * Quiz Single Attempt Results\n *\n * @return   void\n * @since    3.16.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_quiz_attempt_results' ) ) {\n\tfunction lifterlms_template_quiz_attempt_results( $attempt = null ) {\n\t\tllms_get_template(\n\t\t\t'quiz/results-attempt.php',\n\t\t\tarray(\n\t\t\t\t'attempt' => $attempt,\n\t\t\t)\n\t\t);\n\t}\n}\n\n/**\n * Quiz Single Attempt Results Question List\n *\n * @return   void\n * @since    3.16.0\n * @version  3.16.0\n */\nif ( ! function_exists( 'lifterlms_template_quiz_attempt_results_questions_list' ) ) {\n\tfunction lifterlms_template_quiz_attempt_results_questions_list( $attempt = null ) {\n\t\tllms_get_template(\n\t\t\t'quiz/results-attempt-questions-list.php',\n\t\t\tarray(\n\t\t\t\t'attempt' => $attempt,\n\t\t\t)\n\t\t);\n\t}\n}\n\n\n/**\n * Quiz Results Template Include\n *\n * @return void\n * @since    1.0.0\n * @version  1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_quiz_results' ) ) {\n\tfunction lifterlms_template_quiz_results() {\n\t\tllms_get_template( 'quiz/results.php' );\n\t}\n}\n\n\n\n/**\n * Lesson Return link Template Include\n *\n * @return void\n * @since    1.0.0\n * @version  1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_quiz_return_link' ) ) {\n\tfunction lifterlms_template_quiz_return_link() {\n\t\tllms_get_template( 'quiz/return-to-lesson.php' );\n\t}\n}\n\n/**\n * Quiz: wrapper end ( quiz container )\n *\n * @return   void\n * @since    1.0.0\n * @version  1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_quiz_wrapper_end' ) ) {\n\tfunction lifterlms_template_quiz_wrapper_end() {\n\t\tllms_get_template( 'quiz/quiz-wrapper-end.php' );\n\t}\n}\n\n/**\n * Quiz: wrapper start ( quiz container )\n *\n * @return   void\n * @since    1.0.0\n * @version  1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_quiz_wrapper_start' ) ) {\n\tfunction lifterlms_template_quiz_wrapper_start() {\n\t\tllms_get_template( 'quiz/quiz-wrapper-start.php' );\n\t}\n}\n\n/**\n * Start Button Template Include\n *\n * @todo  this should be renamed to lifterlms_template_quiz_start_button\n * @return void\n * @since    1.0.0\n * @version  1.0.0\n */\nif ( ! function_exists( 'lifterlms_template_start_button' ) ) {\n\tfunction lifterlms_template_start_button() {\n\t\tllms_get_template( 'quiz/start-button.php' );\n\t}\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.updates.php",
    "content": "<?php\n/**\n * LifterLMS Update Functions\n *\n * Functions here are used by the background updater during db updates.\n *\n * @package LifterLMS/Functions\n *\n * @since 3.4.3\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// Include all update function files.\nforeach ( glob( LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-*.php' ) as $filename ) {\n\trequire_once $filename;\n}\n\n/**\n * Get the number of items per page used in paginated migration queries.\n *\n * @since 6.0.0\n *\n * @return int\n */\nfunction llms_update_util_get_items_per_page() {\n\t/**\n\t * Filters the number of items per page in migration queries.\n\t *\n\t * This filter exists primarily to allow phpunit tests for migration\n\t * functions and queries to reduce the number of items per query. In this\n\t * way pagination functionality can be tested without having to tests\n\t * a large number of items.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $per_page Number of items per page.\n\t */\n\treturn apply_filters( 'llms_update_items_per_page', 50 );\n}\n\n/**\n * Duplicate a WP Post & all relate metadata.\n *\n * @since 3.16.0\n *\n * @param int $id WP Post ID.\n * @return int WP Post ID of the new duplicate.\n */\nfunction llms_update_util_post_duplicator( $id ) {\n\n\t$copy = (array) get_post( $id );\n\tunset( $copy['ID'] );\n\t$new_id = wp_insert_post( $copy );\n\tforeach ( get_post_custom( $id ) as $key => $values ) {\n\t\tforeach ( $values as $value ) {\n\t\t\tadd_post_meta( $new_id, $key, maybe_unserialize( $value ) );\n\t\t}\n\t}\n\n\treturn $new_id;\n\n}\n\n/**\n * Update the key of a postmeta item\n *\n * @since 3.4.3\n *\n * @param string $post_type Post type.\n * @param string $new_key   New postmeta key.\n * @param string $old_key   Old postmeta key.\n * @return void\n */\nfunction llms_update_util_rekey_meta( $post_type, $new_key, $old_key ) {\n\n\tglobal $wpdb;\n\n\t$wpdb->query(\n\t\t$wpdb->prepare(\n\t\t\t\"UPDATE {$wpdb->prefix}postmeta AS m\n\t\t INNER JOIN {$wpdb->prefix}posts AS p ON p.ID = m.post_ID\n\t\t SET m.meta_key = %s\n\t \t WHERE p.post_type = %s AND m.meta_key = %s;\",\n\t\t\tarray( $new_key, $post_type, $old_key )\n\t\t)\n\t); // no-cache ok.\n\n}\n"
  },
  {
    "path": "includes/functions/llms.functions.user.postmeta.php",
    "content": "<?php\n/**\n * CRUD LifterLMS User Postmeta Data\n *\n * All functions are pluggable.\n *\n * @package LifterLMS/Functions\n *\n * @since 3.21.0\n * @version 3.36.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * CRUD LifterLMS User Postmeta Data.\n *\n * @since 3.21.0\n * @since 3.33.0 Added `llms_bulk_delete_user_postmeta`.\n *               Also now `llms_delete_user_postmeta` returns true only if at least one existing user postmeta has been successfully deleted.\n * @since 3.36.3 Fix doc and indentation.\n */\nif ( ! function_exists( 'llms_delete_user_postmeta' ) ) :\n\t/**\n\t * Delete user postmeta data.\n\t *\n\t * @since 3.21.0\n\t * @since 3.33.0 Returns true only if at least one existing user postmeta has been successfully deleted.\n\t *\n\t * @param int    $user_id    WP User ID.\n\t * @param int    $post_id    WP Post ID.\n\t * @param string $meta_key   Optional. Meta key for lookup, if not supplied, all matching items will be removed. Default null.\n\t * @param mixed  $meta_value Optional. Meta value for lookup, if not supplied, all matching items will be removed. Default null.\n\t *\n\t * @return bool False if no postmetas has been deleted either because they do not exist or because of an error during the\n\t *              actual row deletion from the db. True if at least one existing user postmeta has been successfully deleted.\n\t */\n\tfunction llms_delete_user_postmeta( $user_id, $post_id, $meta_key = null, $meta_value = null ) {\n\n\t\t$ret = false;\n\n\t\t$existing = _llms_query_user_postmeta( $user_id, $post_id, $meta_key, maybe_unserialize( $meta_value ) );\n\t\tif ( $existing ) {\n\t\t\tforeach ( $existing as $obj ) {\n\t\t\t\t$item = new LLMS_User_Postmeta( $obj->meta_id, false );\n\t\t\t\tif ( ! $item->delete() ) {\n\t\t\t\t\t$ret = $ret || false;\n\t\t\t\t} else {\n\t\t\t\t\t$ret = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $ret;\n\n\t}\nendif;\n\nif ( ! function_exists( 'llms_bulk_delete_user_postmeta' ) ) :\n\t/**\n\t * Bulk remove user postmeta data.\n\t *\n\t * @since 3.33.0\n\t *\n\t * @param int   $user_id WP User ID.\n\t * @param int   $post_id WP Post ID.\n\t * @param array $data    Optional. Associative array of meta keys => meta values to delete.\n\t *                       If not meta values supplied, all matching items will be removed. Default empty array.\n\t * @return array|boolean On error returns an associative array of the submitted keys, each item will be true for success or false for error.\n\t *                       On success returns true.\n\t */\n\tfunction llms_bulk_delete_user_postmeta( $user_id, $post_id, $data = array() ) {\n\n\t\t$res = array_fill_keys( array_keys( $data ), null );\n\t\t$err = false;\n\n\t\tif ( ! empty( $data ) ) {\n\t\t\tforeach ( $data as $key => $value ) {\n\t\t\t\t$delete      = llms_delete_user_postmeta( $user_id, $post_id, $key, $value );\n\t\t\t\t$res[ $key ] = $delete;\n\t\t\t\tif ( ! $delete ) {\n\t\t\t\t\t$err = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t$res = llms_delete_user_postmeta( $user_id, $post_id );\n\t\t\t$err = ! $res;\n\t\t}\n\n\t\treturn $err ? $res : true;\n\n\t}\nendif;\n\nif ( ! function_exists( 'llms_get_user_postmeta' ) ) :\n\t/**\n\t * Get user postmeta data or dates by user, post, and key.\n\t *\n\t * @since 3.21.0\n\t *\n\t * @param int    $user_id  WP User ID.\n\t * @param int    $post_id  WP Post ID.\n\t * @param string $meta_key Optional. Meta key, if not supplied returns associative array of all metadata found for the given user / post. Default null.\n\t * @param bool   $single   Optional. If true, returns only the data. Default true.\n\t * @param string $return   Optional. Determine if the meta value or updated date should be returned [meta_value,updated_date]. Default 'meta_value'.\n\t * @return mixed\n\t */\n\tfunction llms_get_user_postmeta( $user_id, $post_id, $meta_key = null, $single = true, $return = 'meta_value' ) {\n\n\t\t$single = is_null( $meta_key ) ? false : $single;\n\n\t\t$res = array();\n\n\t\t$metas = _llms_query_user_postmeta( $user_id, $post_id, $meta_key );\n\t\tif ( count( $metas ) ) {\n\t\t\tforeach ( $metas as $meta ) {\n\t\t\t\tif ( $meta_key ) {\n\t\t\t\t\t$res[ $meta_key ][] = maybe_unserialize( $meta->$return );\n\t\t\t\t} else {\n\t\t\t\t\t$res[ $meta->meta_key ][] = maybe_unserialize( $meta->$return );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( $single ) {\n\t\t\treturn count( $res ) ? $res[ $meta_key ][0] : '';\n\t\t} elseif ( $meta_key ) {\n\t\t\treturn count( $res ) ? $res[ $meta_key ] : array();\n\t\t}\n\n\t\treturn $res;\n\n\t}\nendif;\n\nif ( ! function_exists( 'llms_update_user_postmeta' ) ) :\n\t/**\n\t * Update user postmeta data.\n\t *\n\t * @since 3.21.0\n\t *\n\t * @param int    $user_id    WP User ID.\n\t * @param int    $post_id    WP Post ID.\n\t * @param string $meta_key   Meta key.\n\t * @param mixed  $meta_value Meta value (don't serialize serializable values).\n\t * @param bool   $unique     Optional. If true, updates existing value (if it exists).\n\t *                           If false, will add a new record (allowing multiple records with the same key to exist).\n\t *                           Deafult true.\n\t * @return bool\n\t */\n\tfunction llms_update_user_postmeta( $user_id, $post_id, $meta_key, $meta_value, $unique = true ) {\n\n\t\t$item = false;\n\n\t\t// if unique is true, make an update to the existing item (if it exists).\n\t\tif ( $unique ) {\n\n\t\t\t// locate the item.\n\t\t\t$existing = _llms_query_user_postmeta( $user_id, $post_id, $meta_key );\n\t\t\tif ( $existing ) {\n\n\t\t\t\t// load it and make sure it exists.\n\t\t\t\t$item = new LLMS_User_Postmeta( $existing[0]->meta_id, false );\n\t\t\t\tif ( ! $item->exists() ) {\n\t\t\t\t\t$item = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( ! $item ) {\n\t\t\t$item = new LLMS_User_Postmeta();\n\t\t}\n\n\t\t// setup the data we want to store.\n\t\t$updated_date = llms_current_time( 'mysql' );\n\t\t$meta_value   = maybe_serialize( $meta_value );\n\t\t$item->setup( compact( 'user_id', 'post_id', 'meta_key', 'meta_value', 'updated_date' ) );\n\t\treturn $item->save();\n\n\t}\nendif;\n\nif ( ! function_exists( 'llms_bulk_update_user_postmeta' ) ) :\n\t/**\n\t * Update bulk update user postmeta data.\n\t *\n\t * @since 3.21.0\n\t *\n\t * @param int   $user_id WP User ID.\n\t * @param int   $post_id WP Post ID.\n\t * @param array $data    Optional. Associative array of meta keys => meta values to update.\n\t *                       Default empty array.\n\t * @param bool  $unique  Optional. If true, updates existing value (if it exists).\n\t *                       If false, will add a new record (allowing multiple records with the same key to exist).\n\t *                       Deafult true.\n\t * @return array|true On error returns an associative array of the submitted keys, each item will be true for success or false for error.\n\t *                    On success returns true.\n\t */\n\tfunction llms_bulk_update_user_postmeta( $user_id, $post_id, $data = array(), $unique = true ) {\n\n\t\t$res = array_fill_keys( array_keys( $data ), null );\n\t\t$err = false;\n\t\tforeach ( $data as $key => $val ) {\n\t\t\t$update      = llms_update_user_postmeta( $user_id, $post_id, $key, $val, $unique );\n\t\t\t$res[ $key ] = $update;\n\t\t\tif ( ! $update ) {\n\t\t\t\t$err = true;\n\t\t\t}\n\t\t}\n\n\t\treturn $err ? $res : true;\n\n\t}\nendif;\n\nif ( ! function_exists( '_llms_query_user_postmeta' ) ) :\n\t/**\n\t * Query user postmeta data.\n\t * This function is marked for internal use only.\n\t *\n\t * @since 3.21.0\n\t *\n\t * @access private\n\t *\n\t * @param int    $user_id WP User ID.\n\t * @param int    $post_id WP Post ID.\n\t * @param string $meta_key   Optional. Meta key. Default null.\n\t * @param string $meta_value Optional. Meta value. Default null.\n\t * @return array\n\t */\n\tfunction _llms_query_user_postmeta( $user_id, $post_id, $meta_key = null, $meta_value = null ) {\n\n\t\tglobal $wpdb;\n\n\t\t$key = $meta_key ? $wpdb->prepare( 'AND meta_key = %s', $meta_key ) : '';\n\t\t$val = $meta_value ? $wpdb->prepare( 'AND meta_value = %s', $meta_value ) : '';\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$res = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT * FROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\t\t WHERE user_id = %d AND post_id = %d {$key} {$val} ORDER BY updated_date DESC\",\n\t\t\t\t$user_id,\n\t\t\t\t$post_id\n\t\t\t)\n\t\t);\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\treturn $res;\n\n\t}\nendif;\n"
  },
  {
    "path": "includes/functions/updates/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-300.php",
    "content": "<?php\n/**\n * Update functions for version 3.0.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n\n/**\n * Creates access plans for each course & membership\n *\n * Creates up to 3 plans per course and up to two plans per membership.\n *\n * Migrates price & subscription data to a single & recurring plan where applicable.\n *\n * If course is restricted to a membership a free members only plan will be created\n * in addition to paid open recurring & single plans.\n *\n * If course is restricted to a membership and no price is found\n * only one free members only plan will be created.\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_create_access_plans() {\n\n\t$courses = new WP_Query(\n\t\tarray(\n\t\t\t'post_type'      => array( 'course', 'llms_membership' ),\n\t\t\t'posts_per_page' => -1,\n\t\t\t'status'         => 'any',\n\t\t)\n\t);\n\n\tif ( $courses->have_posts() ) {\n\t\tforeach ( $courses->posts as $post ) {\n\n\t\t\t$meta = get_post_meta( $post->ID );\n\n\t\t\t$is_free       = ( ! $meta['_price'][0] || floatval( 0 ) === floatval( $meta['_price'][0] ) );\n\t\t\t$has_recurring = ( 1 == $meta['_llms_recurring_enabled'][0] );\n\t\t\tif ( 'course' === $post->post_type ) {\n\t\t\t\t$members_only = ( 'on' === $meta['_llms_is_restricted'][0] && $meta['_llms_restricted_levels'][0] );\n\t\t\t} else {\n\t\t\t\t$members_only = false;\n\t\t\t}\n\n\t\t\t// Base plan for single & recurring.\n\t\t\t$base_plan = array(\n\n\t\t\t\t'access_expiration'         => 'lifetime',\n\t\t\t\t'availability'              => 'open',\n\t\t\t\t'availability_restrictions' => array(),\n\t\t\t\t'content'                   => '',\n\t\t\t\t'enroll_text'               => ( 'course' === $post->post_type ) ? __( 'Enroll', 'lifterlms' ) : __( 'Join', 'lifterlms' ),\n\t\t\t\t'featured'                  => 'no',\n\t\t\t\t'frequency'                 => 0,\n\t\t\t\t'is_free'                   => 'no',\n\t\t\t\t'product_id'                => $post->ID,\n\t\t\t\t'sku'                       => $meta['_sku'][0],\n\t\t\t\t'trial_offer'               => 'no',\n\n\t\t\t);\n\n\t\t\t$single = array_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'price' => $meta['_price'][0],\n\t\t\t\t),\n\t\t\t\t$base_plan\n\t\t\t);\n\n\t\t\t$recurring = array_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'price' => $meta['_llms_subscription_price'][0],\n\t\t\t\t),\n\t\t\t\t$base_plan\n\t\t\t);\n\n\t\t\t/**\n\t\t\t * Determine what kinds of plans to create\n\t\t\t */\n\n\t\t\t// Free and members only, only available to members.\n\t\t\tif ( $is_free && $members_only ) {\n\n\t\t\t\t$free_members_only = true;\n\t\t\t\t$single_paid_open  = false;\n\t\t\t\t$single_free_open  = false;\n\t\t\t\t$recurring_paid    = false;\n\n\t\t\t} elseif ( ! $is_free && $members_only ) {\n\n\t\t\t\t$free_members_only = true;\n\t\t\t\t$single_paid_open  = true;\n\t\t\t\t$single_free_open  = false;\n\t\t\t\t$recurring_paid    = $has_recurring;\n\n\t\t\t} else {\n\t\t\t\t// No restrictions, normal settings apply.\n\n\t\t\t\t$free_members_only = false;\n\t\t\t\t$single_paid_open  = ! $is_free ? true : false;\n\t\t\t\t$single_free_open  = $is_free ? true : false;\n\t\t\t\t$recurring_paid    = $has_recurring;\n\n\t\t\t}\n\n\t\t\t$order = 1;\n\n\t\t\t/**\n\t\t\t * CREATE THE PLANS\n\t\t\t */\n\t\t\tif ( $free_members_only ) {\n\n\t\t\t\t$plan                              = $single;\n\t\t\t\t$plan['menu_order']                = $order;\n\t\t\t\t$plan['is_free']                   = 'yes';\n\t\t\t\t$plan['sku']                       = ! empty( $plan['sku'] ) ? $plan['sku'] . '-membersonly' : '';\n\t\t\t\t$plan['availability']              = 'members';\n\t\t\t\t$plan['availability_restrictions'] = unserialize( $meta['_llms_restricted_levels'][0] );\n\n\t\t\t\t$obj = new LLMS_Access_Plan( 'new', __( 'Members Only', 'lifterlms' ) );\n\t\t\t\tforeach ( $plan as $key => $val ) {\n\t\t\t\t\t$obj->set( $key, $val );\n\t\t\t\t}\n\n\t\t\t\tunset( $plan );\n\t\t\t\t$order++;\n\n\t\t\t}\n\n\t\t\tif ( $single_paid_open ) {\n\n\t\t\t\t$plan               = $single;\n\t\t\t\t$plan['menu_order'] = $order;\n\t\t\t\t$plan['sku']        = ! empty( $plan['sku'] ) ? $plan['sku'] . '-onetime' : '';\n\t\t\t\t$plan['on_sale']    = ! empty( $meta['_sale_price'][0] ) ? 'yes' : 'no';\n\n\t\t\t\tif ( 'yes' === $plan['on_sale'] ) {\n\n\t\t\t\t\t$plan['sale_end']   = ! empty( $meta['_sale_price_dates_to'][0] ) ? date( 'm/d/Y', strtotime( $meta['_sale_price_dates_to'][0] ) ) : '';\n\t\t\t\t\t$plan['sale_start'] = ! empty( $meta['_sale_price_dates_from'][0] ) ? date( 'm/d/Y', strtotime( $meta['_sale_price_dates_from'][0] ) ) : '';\n\t\t\t\t\t$plan['sale_price'] = $meta['_sale_price'][0];\n\n\t\t\t\t}\n\n\t\t\t\t$obj = new LLMS_Access_Plan( 'new', __( 'One-Time Payment', 'lifterlms' ) );\n\t\t\t\tforeach ( $plan as $key => $val ) {\n\t\t\t\t\t$obj->set( $key, $val );\n\t\t\t\t}\n\n\t\t\t\tunset( $plan );\n\t\t\t\t$order++;\n\n\t\t\t}\n\n\t\t\tif ( $single_free_open ) {\n\n\t\t\t\t$plan               = $single;\n\t\t\t\t$plan['menu_order'] = $order;\n\t\t\t\t$plan['is_free']    = 'yes';\n\t\t\t\t$plan['sku']        = ! empty( $plan['sku'] ) ? $plan['sku'] . '-free' : '';\n\n\t\t\t\t$obj = new LLMS_Access_Plan( 'new', __( 'Free', 'lifterlms' ) );\n\t\t\t\tforeach ( $plan as $key => $val ) {\n\t\t\t\t\t$obj->set( $key, $val );\n\t\t\t\t}\n\n\t\t\t\tunset( $plan );\n\t\t\t\t$order++;\n\n\t\t\t}\n\n\t\t\tif ( $recurring_paid ) {\n\n\t\t\t\t$plan               = $recurring;\n\t\t\t\t$plan['menu_order'] = $order;\n\t\t\t\t$plan['sku']        = ! empty( $plan['sku'] ) ? $plan['sku'] . '-subscription' : '';\n\n\t\t\t\tif ( isset( $meta['_llms_subscription_first_payment'][0] ) && $meta['_llms_subscription_first_payment'][0] != $meta['_llms_subscription_price'][0] ) {\n\t\t\t\t\t$plan['trial_offer']  = 'yes';\n\t\t\t\t\t$plan['trial_length'] = $meta['_llms_billing_freq'][0];\n\t\t\t\t\t$plan['trial_period'] = $meta['_llms_billing_period'][0];\n\t\t\t\t\t$plan['trial_price']  = $meta['_llms_subscription_first_payment'][0];\n\t\t\t\t}\n\n\t\t\t\t$plan['frequency'] = $meta['_llms_billing_freq'][0];\n\t\t\t\t$plan['length']    = $meta['_llms_billing_cycle'][0];\n\t\t\t\t$plan['period']    = $meta['_llms_billing_period'][0];\n\n\t\t\t\t$obj = new LLMS_Access_Plan( 'new', __( 'Subscription', 'lifterlms' ) );\n\t\t\t\tforeach ( $plan as $key => $val ) {\n\t\t\t\t\t$obj->set( $key, $val );\n\t\t\t\t}\n\n\t\t\t\tunset( $plan );\n\t\t\t\t$order++;\n\n\t\t\t}\n\n\t\t\t$keys = array(\n\t\t\t\t'_regular_price',\n\t\t\t\t'_price',\n\t\t\t\t'_sale_price',\n\t\t\t\t'_sale_price_dates_from',\n\t\t\t\t'_sale_price_dates_to',\n\t\t\t\t'_on_sale',\n\t\t\t\t'_llms_recurring_enabled',\n\t\t\t\t'_llms_subscription_price',\n\t\t\t\t'_llms_subscription_first_payment',\n\t\t\t\t'_llms_billing_period',\n\t\t\t\t'_llms_billing_freq',\n\t\t\t\t'_llms_billing_cycle',\n\t\t\t\t'_llms_subscriptions',\n\t\t\t\t'_sku',\n\t\t\t\t'_is_custom_single_price',\n\t\t\t\t'_custom_single_price_html',\n\t\t\t\t'_llms_is_restricted',\n\t\t\t\t'_llms_restricted_levels',\n\n\t\t\t\t'_llms_expiration_interval',\n\t\t\t\t'_llms_expiration_period',\n\t\t\t);\n\n\t\t\tforeach ( $keys as $key ) {\n\t\t\t\tdelete_post_meta( $post->ID, $key );\n\t\t\t}\n\t\t}\n\t}\n\n}\n\n/**\n * Delete deprecated options that are no longer used by LifterLMS after 3.0.0\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_del_deprecated_options() {\n\n\t/**\n\t * Delete legacy options related to LifterLMS updating\n\t * prior to 2.0 release. this is long overdue\n\t */\n\tdelete_option( 'lifterlms_is_activated' );\n\tdelete_option( 'lifterlms_update_key' );\n\tdelete_option( 'lifterlms_authkey' );\n\tdelete_option( 'lifterlms_activation_key' );\n\n\t/**\n\t * Legacy option no longer needed\n\t */\n\tdelete_option( 'lifterlms_student_role_created' );\n\n\t/**\n\t * Delete course and membership display & related options\n\t * these are now filters or can be handled with action hooks\n\t * moving forward\n\t */\n\tdelete_option( 'lifterlms_button_purchase_membership_custom_text' );\n\tdelete_option( 'lifterlms_course_display_outline_lesson_thumbnails' );\n\tdelete_option( 'lifterlms_course_display_author' );\n\tdelete_option( 'lifterlms_course_display_banner' );\n\tdelete_option( 'lifterlms_course_display_difficulty' );\n\tdelete_option( 'lifterlms_course_display_length' );\n\tdelete_option( 'lifterlms_course_display_categories' );\n\tdelete_option( 'lifterlms_course_display_tags' );\n\tdelete_option( 'lifterlms_course_display_tracks' );\n\tdelete_option( 'lifterlms_lesson_nav_display_excerpt' );\n\tdelete_option( 'lifterlms_course_display_outline' );\n\tdelete_option( 'lifterlms_course_display_outline_titles' );\n\tdelete_option( 'lifterlms_course_display_outline_lesson_thumbnails' );\n\tdelete_option( 'lifterlms_display_lesson_complete_placeholders' );\n\tdelete_option( 'redirect_to_checkout' );\n\n}\n\n/**\n * Migrate deprecated account field related options to new ones\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_migrate_account_field_options() {\n\n\t$email_confirm = get_option( 'lifterlms_registration_confirm_email' );\n\tif ( 'yes' === $email_confirm ) {\n\t\t$email_confirm = 'yes';\n\t} elseif ( 'no' === $email_confirm ) {\n\t\t$email_confirm = 'no';\n\t} else {\n\t\t$email_confirm = false;\n\t}\n\n\t$names = get_option( 'lifterlms_registration_require_name' );\n\tif ( 'yes' === $names ) {\n\t\t$names = 'required';\n\t} elseif ( 'no' === $names ) {\n\t\t$names = 'hidden';\n\t} else {\n\t\t$names = false;\n\t}\n\n\t$addresses = get_option( 'lifterlms_registration_require_address' );\n\tif ( 'yes' === $addresses ) {\n\t\t$addresses = 'required';\n\t} elseif ( 'no' === $addresses ) {\n\t\t$addresses = 'hidden';\n\t} else {\n\t\t$addresses = false;\n\t}\n\n\t$phone = get_option( 'lifterlms_registration_add_phone' );\n\tif ( 'yes' === $phone ) {\n\t\t$phone = 'optional';\n\t} elseif ( 'no' === $phone ) {\n\t\t$phone = 'hidden';\n\t} else {\n\t\t$phone = false;\n\t}\n\n\tforeach ( array( 'checkout', 'registration', 'account' ) as $screen ) {\n\n\t\tif ( $email_confirm ) {\n\t\t\tupdate_option( 'lifterlms_user_info_field_email_confirmation_' . $screen . '_visibility', $email_confirm );\n\t\t}\n\t\tif ( $names ) {\n\t\t\tupdate_option( 'lifterlms_user_info_field_names_' . $screen . '_visibility', $names );\n\t\t}\n\t\tif ( $addresses ) {\n\t\t\tupdate_option( 'lifterlms_user_info_field_address_' . $screen . '_visibility', $addresses );\n\t\t}\n\t\tif ( $phone ) {\n\t\t\tupdate_option( 'lifterlms_user_info_field_phone_' . $screen . '_visibility', $phone );\n\t\t}\n\t}\n\n\tdelete_option( 'lifterlms_registration_confirm_email' );\n\tdelete_option( 'lifterlms_registration_require_name' );\n\tdelete_option( 'lifterlms_registration_require_address' );\n\tdelete_option( 'lifterlms_registration_add_phone' );\n\n}\n\n/**\n * Move coupon title (previously used for description) to the postmeta table in the new description field\n * Move old coupon code from meta table to the coupon post title *\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_migrate_coupon_data() {\n\n\tglobal $wpdb;\n\n\t$coupon_title_metas = $wpdb->get_results(\n\t\t\"SELECT * FROM {$wpdb->postmeta}\n\t\t WHERE meta_key = '_llms_coupon_title';\"\n\t);\n\n\tforeach ( $coupon_title_metas as $obj ) {\n\n\t\t// Update new description field with the title b/c the title previously acted as a description.\n\t\tupdate_post_meta( $obj->post_id, '_llms_description', get_the_title( $obj->post_id ) );\n\n\t\t// Update the post title to be the value of the old meta field.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'         => $obj->post_id,\n\t\t\t\t'post_title' => $obj->meta_value,\n\t\t\t)\n\t\t);\n\n\t\t// Clean up.\n\t\tdelete_post_meta( $obj->post_id, '_llms_coupon_title' );\n\n\t}\n\n}\n\n/**\n * Update keys of course meta fields for consistency\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_migrate_course_postmeta() {\n\n\tglobal $wpdb;\n\n\t// Rekey meta fields.\n\tllms_update_util_rekey_meta( 'course', '_llms_audio_embed', '_audio_embed' );\n\tllms_update_util_rekey_meta( 'course', '_llms_video_embed', '_video_embed' );\n\tllms_update_util_rekey_meta( 'course', '_llms_has_prerequisite', '_has_prerequisite' );\n\tllms_update_util_rekey_meta( 'course', '_llms_length', '_lesson_length' );\n\tllms_update_util_rekey_meta( 'course', '_llms_capacity', '_lesson_max_user' );\n\tllms_update_util_rekey_meta( 'course', '_llms_prerequisite', '_prerequisite' );\n\tllms_update_util_rekey_meta( 'course', '_llms_prerequisite_track', '_prerequisite_track' );\n\n\tllms_update_util_rekey_meta( 'course', '_llms_start_date', '_course_dates_from' );\n\tllms_update_util_rekey_meta( 'course', '_llms_end_date', '_course_dates_to' );\n\n\t// Updates course enrollment settings and reformats existing dates.\n\t$dates = $wpdb->get_results(\n\t\t\"SELECT m.meta_id, m.post_id, m.meta_value\n\t\t FROM {$wpdb->postmeta} AS m\n\t\t INNER JOIN {$wpdb->posts} AS p ON p.ID = m.post_ID\n\t \t WHERE p.post_type = 'course' AND ( m.meta_key = '_llms_start_date' OR m.meta_key = '_llms_end_date' );\"\n\t); // db call ok; no-cache ok.\n\tforeach ( $dates as $r ) {\n\t\t// If no value in the field skip it otherwise we end up with start of the epoch.\n\t\tif ( ! $r->meta_value ) {\n\t\t\tcontinue; }\n\t\t$wpdb->update(\n\t\t\t$wpdb->postmeta,\n\t\t\tarray(\n\t\t\t\t'meta_value' => date( 'm/d/Y', strtotime( $r->meta_value ) ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'meta_id' => $r->meta_id,\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\t\tadd_post_meta( $r->post_id, '_llms_time_period', 'yes' );\n\t\tadd_post_meta( $r->post_id, '_llms_course_opens_message', sprintf( __( 'This course opens on [lifterlms_course_info id=\"%d\" key=\"start_date\"].', 'lifterlms' ), $r->post_id ) );\n\t\tadd_post_meta( $r->post_id, '_llms_course_closed_message', sprintf( __( 'This course closed on [lifterlms_course_info id=\"%d\" key=\"end_date\"].', 'lifterlms' ), $r->post_id ) );\n\t}\n\n\t// Update course capacity bool and related settings.\n\t$capacity = $wpdb->get_results(\n\t\t\"SELECT m.post_id, m.meta_value\n\t\t FROM {$wpdb->postmeta} AS m\n\t\t INNER JOIN {$wpdb->posts} AS p ON p.ID = m.post_ID\n\t \t WHERE p.post_type = 'course' AND m.meta_key = '_llms_capacity';\"\n\t); // db call ok; no-cache ok.\n\tforeach ( $capacity as $r ) {\n\t\tif ( $r->meta_value ) {\n\t\t\tadd_post_meta( $r->post_id, '_llms_enable_capacity', 'yes' );\n\t\t\tadd_post_meta( $r->post_id, '_llms_capacity_message', __( 'Enrollment has closed because the maximum number of allowed students has been reached.', 'lifterlms' ) );\n\t\t}\n\t}\n\n\t// Convert numeric has_preqeq to \"yes\".\n\t$prereq = $wpdb->query(\n\t\t\"UPDATE {$wpdb->prefix}postmeta AS m\n\t\t INNER JOIN {$wpdb->prefix}posts AS p ON p.ID = m.post_ID\n\t\t SET m.meta_value = 'yes'\n\t \t WHERE p.post_type = 'course' AND m.meta_key = '_llms_has_prerequisite' AND m.meta_value = 1;\"\n\t); // db call ok; no-cache ok.\n\n\t// Convert empty has_prereq to \"no\".\n\t$prereq = $wpdb->query(\n\t\t\"UPDATE {$wpdb->prefix}postmeta AS m\n\t\t INNER JOIN {$wpdb->prefix}posts AS p ON p.ID = m.post_ID\n\t\t SET m.meta_value = 'no'\n\t \t WHERE p.post_type = 'course' AND m.meta_key = '_llms_has_prerequisite' AND m.meta_value = '';\"\n\t); // db call ok; no-cache ok.\n\n}\n\n/**\n * Update keys of email meta fields for consistency\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_migrate_email_postmeta() {\n\n\tllms_update_util_rekey_meta( 'llms_email', '_llms_email_subject', '_email_subject' );\n\tllms_update_util_rekey_meta( 'llms_email', '_llms_email_heading', '_email_heading' );\n\n}\n\n/**\n * Update keys of lesson meta fields for consistency\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_migrate_lesson_postmeta() {\n\n\tglobal $wpdb;\n\n\tllms_update_util_rekey_meta( 'lesson', '_llms_audio_embed', '_audio_embed' );\n\tllms_update_util_rekey_meta( 'lesson', '_llms_video_embed', '_video_embed' );\n\tllms_update_util_rekey_meta( 'lesson', '_llms_has_prerequisite', '_has_prerequisite' );\n\tllms_update_util_rekey_meta( 'lesson', '_llms_prerequisite', '_prerequisite' );\n\tllms_update_util_rekey_meta( 'lesson', '_llms_days_before_available', '_days_before_avalailable' );\n\n\t// Convert numeric has_preqeq to \"yes\".\n\t// Convert numeric free_lesson to \"yes\".\n\t// Convert numeric require_passing_grade to \"yes\".\n\t$wpdb->query(\n\t\t\"UPDATE {$wpdb->prefix}postmeta AS m\n\t\t INNER JOIN {$wpdb->prefix}posts AS p ON p.ID = m.post_ID\n\t\t SET m.meta_value = 'yes'\n\t \t WHERE p.post_type = 'lesson' AND (\n\t \t \t   ( m.meta_key = '_llms_has_prerequisite' AND m.meta_value = 1 )\n\t \t \tOR ( m.meta_key = '_llms_free_lesson' AND m.meta_value = 1 )\n\t \t \tOR ( m.meta_key = '_llms_require_passing_grade' AND m.meta_value = 1 )\n\t \t );\"\n\t); // db call ok; no-cache ok.\n\n\t// Convert empty has_prereq to \"no\".\n\t// Convert empty free_lesson to \"no\".\n\t// Convert empty require_passing_grade to \"no\".\n\t$wpdb->query(\n\t\t\"UPDATE {$wpdb->prefix}postmeta AS m\n\t\t INNER JOIN {$wpdb->prefix}posts AS p ON p.ID = m.post_ID\n\t\t SET m.meta_value = 'no'\n\t \t WHERE p.post_type = 'lesson' AND (\n\t \t \t   ( m.meta_key = '_llms_has_prerequisite' AND m.meta_value = '' )\n\t \t \tOR ( m.meta_key = '_llms_free_lesson' AND m.meta_value = '' )\n\t \t \tOR ( m.meta_key = '_llms_require_passing_grade' AND m.meta_value = '' )\n\t \t );\"\n\t); // db call ok; no-cache ok.\n\n\t// Updates course enrollment settings and reformats existing dates.\n\t$drips = $wpdb->get_results(\n\t\t\"SELECT m.post_id\n\t\t FROM {$wpdb->postmeta} AS m\n\t\t INNER JOIN {$wpdb->posts} AS p ON p.ID = m.post_ID\n\t \t WHERE p.post_type = 'lesson' AND m.meta_key = '_llms_days_before_available';\"\n\t); // db call ok; no-cache ok.\n\tforeach ( $drips as $r ) {\n\t\tadd_post_meta( $r->post_id, '_llms_drip_method', 'enrollment' );\n\t}\n\n}\n\n/**\n * Change the post type of orders and rekey meta fields\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_migrate_order_data() {\n\n\tglobal $wpdb;\n\n\t// Prefix the old unprefixed order post type.\n\t$wpdb->query(\n\t\t\"UPDATE {$wpdb->posts}\n\t\t SET post_type = 'llms_order'\n\t\t WHERE post_type = 'order';\"\n\t);\n\n\t// Rekey postmetas.\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_payment_gateway', '_llms_payment_method' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_product_id', '_llms_order_product_id' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_currency', '_llms_order_currency' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_coupon_id', '_llms_order_coupon_id' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_coupon_code', '_llms_order_coupon_code' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_coupon_type', '_llms_order_coupon_type' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_coupon_amount', '_llms_order_coupon_amount' );\n\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_billing_frequency', '_llms_order_billing_freq' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_billing_length', '_llms_order_billing_cycle' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_billing_period', '_llms_order_billing_period' );\n\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_gateway_api_mode', '_llms_stripe_api_mode' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_gateway_subscription_id', '_llms_stripe_subscription_id' );\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_gateway_customer_id', '_llms_stripe_customer_id' );\n\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_trial_total', '_llms_order_first_payment' );\n\n\tllms_update_util_rekey_meta( 'llms_order', '_llms_start_date', '_llms_order_date' );\n\n}\n\n/**\n * Migrate all orders from the 2.x to 3.x data structure\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_update_orders() {\n\n\t$args = array(\n\t\t'post_type'      => array( 'llms_order' ),\n\t\t'posts_per_page' => -1,\n\t\t'status'         => 'publish',\n\t);\n\n\t$orders = new WP_Query( $args );\n\n\tif ( $orders->have_posts() ) {\n\t\tforeach ( $orders->posts as $post ) {\n\n\t\t\t$order = new LLMS_Order( $post );\n\n\t\t\t// Add an order key.\n\t\t\t$order->set( 'order_key', $order->generate_order_key() );\n\n\t\t\t$order->set( 'access_expiration', 'lifetime' );\n\n\t\t\t// Add coupon used info.\n\t\t\t$coupon_used = $order->get( 'coupon_id' ) ? 'yes' : 'no';\n\t\t\t$order->set( 'coupon_used', $coupon_used );\n\n\t\t\t// Add data about the user to the order if we can find it.\n\t\t\tif ( isset( $order->user_id ) ) {\n\n\t\t\t\t$id = $order->get( 'user_id' );\n\n\t\t\t\tif ( $id && get_user_by( 'ID', $id ) ) {\n\n\t\t\t\t\t$student = new LLMS_Student( $id );\n\n\t\t\t\t\t$metas = array(\n\t\t\t\t\t\t'billing_address_1'  => 'billing_address_1',\n\t\t\t\t\t\t'billing_address_2'  => 'billing_address_2',\n\t\t\t\t\t\t'billing_city'       => 'billing_city',\n\t\t\t\t\t\t'billing_country'    => 'billing_country',\n\t\t\t\t\t\t'billing_email'      => 'user_email',\n\t\t\t\t\t\t'billing_first_name' => 'first_name',\n\t\t\t\t\t\t'billing_last_name'  => 'last_name',\n\t\t\t\t\t\t'billing_state'      => 'billing_state',\n\t\t\t\t\t\t'billing_zip'        => 'billing_zip',\n\t\t\t\t\t);\n\n\t\t\t\t\tforeach ( $metas as $ordermeta => $usermeta ) {\n\n\t\t\t\t\t\t$v = $student->$usermeta;\n\t\t\t\t\t\tif ( $v ) {\n\n\t\t\t\t\t\t\t$order->set( $ordermeta, $v );\n\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Setup trial info if there was a first payment recorded.\n\t\t\tif ( $order->get( 'trial_total' ) ) {\n\n\t\t\t\t$order->set( 'trial_offer', 'yes' );\n\t\t\t\t$order->set( 'trial_length', $order->get( 'billing_length' ) );\n\t\t\t\t$order->set( 'trial_period', $order->get( 'billing_period' ) );\n\t\t\t\t$order->set( 'trial_original_total', $order->get( 'trial_total' ) );\n\n\t\t\t} else {\n\n\t\t\t\t$order->set( 'trial_offer', 'no' );\n\n\t\t\t}\n\n\t\t\t$total = $order->is_recurring() ? get_post_meta( $post->ID, '_llms_order_recurring_price', true ) : get_post_meta( $post->ID, '_llms_order_total', true );\n\t\t\t$order->set( 'original_total', $total );\n\t\t\t$order->set( 'total', $total );\n\n\t\t\t$order->add_note( sprintf( __( 'This order was migrated to the LifterLMS 3.0 data structure. %1$sLearn more%2$s.', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/lifterlms-orders#migration\" target=\"_blank\">', '</a>' ) );\n\n\t\t\t// Remove deprecated.\n\t\t\tdelete_post_meta( $post->ID, '_llms_order_recurring_price' );\n\t\t\tdelete_post_meta( $post->ID, '_llms_order_total' );\n\t\t\tdelete_post_meta( $post->ID, '_llms_order_coupon_limit' );\n\t\t\tdelete_post_meta( $post->ID, '_llms_order_product_price' );\n\t\t\tdelete_post_meta( $post->ID, '_llms_order_billing_start_date' );\n\t\t\tdelete_post_meta( $post->ID, '_llms_order_coupon_value' );\n\t\t\tdelete_post_meta( $post->ID, '_llms_order_original_total' );\n\n\t\t}\n\t}\n}\n\n/**\n * Update db version at conclusion of 3.0.0 updates\n *\n * @since 3.0.0\n *\n * @return void\n */\nfunction llms_update_300_update_db_version() {\n\n\tLLMS_Install::update_db_version( '3.0.0' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-303.php",
    "content": "<?php\n/**\n * Update functions for version 3.0.3\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Fix students with the bugged role \"students\"\n *\n * @since 3.0.3\n *\n * @return void\n */\nfunction llms_update_303_update_students_role() {\n\n\t// Add the bugged role so we can remove it.\n\t// We delete it at the conclusion of the function.\n\tif ( ! get_role( 'studnet' ) ) {\n\n\t\tadd_role(\n\t\t\t'studnet',\n\t\t\t__( 'Student', 'lifterlms' ),\n\t\t\tarray(\n\t\t\t\t'read' => true,\n\t\t\t)\n\t\t);\n\n\t}\n\n\t$users = new WP_User_Query(\n\t\tarray(\n\t\t\t'number'   => -1,\n\t\t\t'role__in' => array( 'studnet' ),\n\t\t)\n\t);\n\n\tif ( $users->get_results() ) {\n\t\tforeach ( $users->get_results() as $user ) {\n\t\t\t$user->remove_role( 'studnet' );\n\t\t\t$user->add_role( 'student' );\n\t\t}\n\t}\n\n\t// Remove the bugged role when finished.\n\tremove_role( 'studnet' );\n\n}\n\n/**\n * Update db version at conclusion of 3.0.3 updates\n *\n * @since 3.0.3\n *\n * @return void\n */\nfunction llms_update_303_update_db_version() {\n\n\tLLMS_Install::update_db_version( '3.0.3' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-3120.php",
    "content": "<?php\n/**\n * Update functions for version 3.12.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Add end dates to LifterLMS Orders which have a length but no saved end date\n *\n * @since 3.12.0\n *\n * @return void\n */\nfunction llms_update_3120_update_order_end_dates() {\n\n\tglobal $wpdb;\n\n\t$ids = $wpdb->get_col(\n\t\t\"SELECT posts.ID\n\t\t FROM {$wpdb->posts} AS posts\n\t\t JOIN {$wpdb->postmeta} AS meta1 ON meta1.post_id = posts.ID AND meta1.meta_key = '_llms_billing_length'\n\t\t LEFT JOIN {$wpdb->postmeta} AS meta2 ON meta2.post_id = posts.ID AND meta2.meta_key = '_llms_date_billing_end'\n\t\t WHERE posts.post_type = 'llms_order'\n\t\t   AND meta2.meta_value IS NULL\n\t\t   AND meta1.meta_value > 0;\"\n\t); // db call ok; no-cache ok.\n\n\tforeach ( $ids as $id ) {\n\n\t\t$order = llms_get_post( $id );\n\t\tif ( ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t$order->maybe_schedule_payment( true );\n\n\t}\n\n}\n\n/**\n * Rename options for bbPress and BuddyPress to follow the abstract integration options structure\n *\n * @since 3.12.0\n *\n * @return void\n */\nfunction llms_update_3120_update_integration_options() {\n\n\tglobal $wpdb;\n\t$wpdb->update(\n\t\t$wpdb->options,\n\t\tarray(\n\t\t\t'option_name' => 'llms_integration_bbpress_enabled',\n\t\t),\n\t\tarray(\n\t\t\t'option_name' => 'lifterlms_bbpress_enabled',\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\t$wpdb->update(\n\t\t$wpdb->options,\n\t\tarray(\n\t\t\t'option_name' => 'llms_integration_buddypress_enabled',\n\t\t),\n\t\tarray(\n\t\t\t'option_name' => 'lifterlms_buddypress_enabled',\n\t\t)\n\t); // db call ok; no-cache ok.\n\n}\n\n/**\n * Update db version at conclusion of 3.12.0 updates\n *\n * @since 3.12.0\n *\n * @return void\n */\nfunction llms_update_3120_update_db_version() {\n\n\tLLMS_Install::update_db_version( '3.12.0' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-3130.php",
    "content": "<?php\n/**\n * Update functions for version 3.13.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n\n/**\n * Setup default instructor data for courses and memberships\n *\n * @since 3.13.0\n *\n * @return void\n */\nfunction llms_update_3130_create_default_instructors() {\n\n\t$query = new WP_Query(\n\t\tarray(\n\t\t\t'post_type'      => array( 'course', 'llms_membership' ),\n\t\t\t'posts_per_page' => -1,\n\t\t)\n\t);\n\n\tforeach ( $query->posts as $post ) {\n\t\t$course = llms_get_post( $post );\n\t\t$course->set_instructors();\n\t}\n\n}\n\n/**\n * Add an admin notice about the new builder\n *\n * @since 3.13.0\n *\n * @return void\n */\nfunction llms_update_3130_builder_notice() {\n\n\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\tLLMS_Admin_Notices::add_notice(\n\t\t'update-3130',\n\t\tarray(\n\t\t\t'html'        => sprintf(\n\t\t\t\t__( 'Welcome to LifterLMS 3.13.0! We\\'ve packed a ton of features into this release: Take a moment to get familiar with the all new %1$scourse builder%3$s and our new %2$suser roles%3$s.', 'lifterlms' ),\n\t\t\t\t'<a href=\"https://lifterlms.com/docs/using-course-builder/\" target=\"_blank\">',\n\t\t\t\t'<a href=\"https://lifterlms.com/docs/roles-and-capabilities/\" target=\"_blank\">',\n\t\t\t\t'</a>'\n\t\t\t),\n\t\t\t'type'        => 'info',\n\t\t\t'dismissible' => true,\n\t\t\t'remindable'  => false,\n\t\t)\n\t);\n\n}\n\n/**\n * Update db version at conclusion of 3.13.0 updates\n *\n * @since 3.13.0\n *\n * @return void\n */\nfunction llms_update_3130_update_db_version() {\n\n\tLLMS_Install::update_db_version( '3.13.0' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-3160.php",
    "content": "<?php\n/**\n * Update functions for version 3.16.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Add yes/no vals for quiz new quiz settings\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_update_quiz_settings() {\n\n\tglobal $wpdb;\n\t$ids = $wpdb->get_col( \"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'llms_quiz'\" );\n\n\tforeach ( $ids as $id ) {\n\n\t\t$quiz = llms_get_post( $id );\n\n\t\tif ( $quiz->get( 'time_limit' ) > 0 ) {\n\t\t\t$quiz->set( 'limit_time', 'yes' );\n\t\t}\n\n\t\tif ( $quiz->get( 'allowed_attempts' ) > 0 ) {\n\t\t\t$quiz->set( 'limit_attempts', 'yes' );\n\t\t}\n\t}\n\n}\n\n/**\n * Rename meta keys for lesson -> quiz relationship\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_lesson_to_quiz_relationships_migration() {\n\n\tglobal $wpdb;\n\t$wpdb->update(\n\t\t$wpdb->postmeta,\n\t\tarray(\n\t\t\t'meta_key' => '_llms_quiz',\n\t\t),\n\t\tarray(\n\t\t\t'meta_key' => '_llms_assigned_quiz',\n\t\t)\n\t); // db call ok; no-cache ok.\n\n}\n\n/**\n * Migrate attempt data from the former location on the wp_usermeta table\n *\n * @since 3.16.0\n * @since 3.24.1 Unknown.\n *\n * @return void\n */\nfunction llms_update_3160_attempt_migration() {\n\n\tglobal $wpdb;\n\t$query = $wpdb->get_results( \"SELECT user_id, meta_value FROM {$wpdb->usermeta} WHERE meta_key = 'llms_quiz_data' LIMIT 100;\" ); // db call ok; no-cache ok.\n\n\t// Finished.\n\tif ( ! $query ) {\n\t\tset_transient( 'llms_update_3160_attempt_migration', 'complete', DAY_IN_SECONDS );\n\t\treturn false;\n\t}\n\n\tforeach ( $query as $record ) {\n\n\t\tif ( ! empty( $record->meta_value ) ) {\n\n\t\t\tforeach ( unserialize( $record->meta_value ) as $attempt ) {\n\n\t\t\t\tif ( ! is_array( $attempt ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$to_insert = array();\n\t\t\t\t$format    = array();\n\n\t\t\t\t$start = $attempt['start_date'];\n\t\t\t\t$end   = $attempt['end_date'];\n\n\t\t\t\tif ( $end ) {\n\t\t\t\t\t$to_insert['update_date'] = $end;\n\t\t\t\t\t$format[]                 = '%s';\n\t\t\t\t} elseif ( $start ) {\n\t\t\t\t\t$to_insert['update_date'] = $start;\n\t\t\t\t\t$format[]                 = '%s';\n\t\t\t\t} else {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tforeach ( $attempt as $key => $val ) {\n\n\t\t\t\t\t$insert_key = $key;\n\t\t\t\t\t$insert_val = $val;\n\n\t\t\t\t\tif ( 'assoc_lesson' === $key ) {\n\t\t\t\t\t\t$insert_key = 'lesson_id';\n\t\t\t\t\t} elseif ( 'id' === $key ) {\n\t\t\t\t\t\t$insert_key = 'quiz_id';\n\t\t\t\t\t} elseif ( 'user_id' === $key ) {\n\t\t\t\t\t\t$insert_key = 'student_id';\n\t\t\t\t\t} elseif ( 'wpnonce' === $key ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} elseif ( 'current' === $key ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} elseif ( 'questions' === $key ) {\n\t\t\t\t\t\t$insert_val = serialize( $val );\n\t\t\t\t\t} elseif ( 'passed' === $key ) {\n\t\t\t\t\t\t$insert_key = 'status';\n\t\t\t\t\t\tif ( $val ) {\n\t\t\t\t\t\t\t$insert_val = 'pass';\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Quiz has been initialized but hasn't been started yet,\n\t\t\t\t\t\t\t// we don't need to migrate these.\n\t\t\t\t\t\t\tif ( ! $start && ! $end ) {\n\t\t\t\t\t\t\t\t// $insert_val = 'new';\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t} elseif ( $start && ! $end ) {\n\t\t\t\t\t\t\t\t// sSill taking the quiz.\n\t\t\t\t\t\t\t\tif ( isset( $attempt['current'] ) && $attempt['current'] ) {\n\t\t\t\t\t\t\t\t\t$insert_val = 'current';\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Quiz was abandoned.\n\t\t\t\t\t\t\t\t$insert_val = 'incomplete';\n\t\t\t\t\t\t\t\t// Actual failure.\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t$insert_val = 'fail';\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch ( $insert_key ) {\n\n\t\t\t\t\t\tcase 'lesson_id':\n\t\t\t\t\t\tcase 'quiz_id':\n\t\t\t\t\t\tcase 'student_id':\n\t\t\t\t\t\tcase 'attempt':\n\t\t\t\t\t\t\t$insert_format = '%d';\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tcase 'grade':\n\t\t\t\t\t\t\t$insert_format = '%f';\n\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t$insert_format = '%s';\n\n\t\t\t\t\t}\n\n\t\t\t\t\t$to_insert[ $insert_key ] = $insert_val;\n\t\t\t\t\t$format[]                 = $insert_format;\n\n\t\t\t\t}\n\n\t\t\t\t$wpdb->insert( $wpdb->prefix . 'lifterlms_quiz_attempts', $to_insert, $format ); // db call ok; no-cache ok.\n\n\t\t\t}\n\t\t}\n\n\t\t// Backup original.\n\t\tupdate_user_meta( $record->user_id, 'llms_legacy_quiz_data', $record->meta_value );\n\n\t\t// Selete the original so it's not there on the next run.\n\t\tdelete_user_meta( $record->user_id, 'llms_quiz_data' );\n\n\t}\n\n\t// Needs to run again.\n\treturn true;\n\n}\n\n/**\n * Create duplicate questions for each question attached to multiple quizzes\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_ensure_no_dupe_question_rels() {\n\n\tif ( 'complete' !== get_transient( 'llms_update_3160_attempt_migration' ) ) {\n\t\treturn true;\n\t}\n\n\t$skip = get_transient( 'llms_3160_skipper_dupe_q' );\n\tif ( ! $skip ) {\n\t\t$skip = 0;\n\t}\n\tset_transient( 'llms_3160_skipper_dupe_q', $skip + 20, DAY_IN_SECONDS );\n\n\tglobal $wpdb;\n\t$question_ids = $wpdb->get_col(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT ID\n\t\t FROM {$wpdb->posts}\n\t\t WHERE post_type = 'llms_question'\n\t\t ORDER BY ID ASC\n\t\t LIMIT %d, 20;\",\n\t\t\t$skip\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\tif ( ! $question_ids ) {\n\t\tset_transient( 'llms_update_3160_ensure_no_dupe_question_rels_status', 'complete', DAY_IN_SECONDS );\n\t\treturn false;\n\t}\n\n\tforeach ( $question_ids as $qid ) {\n\n\t\t$parts = array(\n\t\t\tserialize(\n\t\t\t\tarray(\n\t\t\t\t\t'id' => $qid,\n\t\t\t\t)\n\t\t\t),\n\t\t\tserialize(\n\t\t\t\tarray(\n\t\t\t\t\t'id' => absint( $qid ),\n\t\t\t\t)\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $parts as &$part ) {\n\t\t\t$part = substr( $part, 5, -1 );\n\t\t}\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$quiz_ids = $wpdb->get_col(\n\t\t\t\"\n\t\t\tSELECT post_id\n\t\t\tFROM {$wpdb->postmeta}\n\t\t\tWHERE meta_key = '_llms_questions'\n\t\t\t  AND ( meta_value LIKE '%{$parts[0]}%' OR meta_value LIKE '%{$parts[1]}%' );\"\n\t\t); // db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t// Question is attached to 2 or more quizzes.\n\t\tif ( count( $quiz_ids ) >= 2 ) {\n\n\t\t\t// Remove the first quiz and duplicate questions for the remaining quizzes.\n\t\t\tarray_shift( $quiz_ids );\n\n\t\t\tforeach ( $quiz_ids as $quiz_id ) {\n\n\t\t\t\t// Copy the question and add update the reference on the quiz.\n\t\t\t\t$question_copy_id = llms_update_util_post_duplicator( $qid );\n\t\t\t\t$questions        = get_post_meta( $quiz_id, '_llms_questions', true );\n\t\t\t\tforeach ( $questions as &$qdata ) {\n\t\t\t\t\tif ( $qdata['id'] == $qid ) {\n\t\t\t\t\t\t$qdata['id'] = $question_copy_id;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tupdate_post_meta( $quiz_id, '_llms_questions', $questions );\n\n\t\t\t\t// Update references to the quiz in quiz attempts.\n\t\t\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\t\t$attempt_ids = $wpdb->get_col(\n\t\t\t\t\t\"\n\t\t\t\t\tSELECT id\n\t\t\t\t\tFROM {$wpdb->prefix}lifterlms_quiz_attempts\n\t\t\t\t\tWHERE quiz_id = {$quiz_id}\n\t\t\t\t\t  AND ( questions LIKE '%{$parts[0]}%' OR questions LIKE '%{$parts[1]}%' );\"\n\t\t\t\t); // db call ok; no-cache ok.\n\t\t\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t\t\tforeach ( $attempt_ids as $aid ) {\n\n\t\t\t\t\t$attempt    = new LLMS_Quiz_Attempt( $aid );\n\t\t\t\t\t$attempt_qs = $attempt->get_questions();\n\t\t\t\t\tforeach ( $attempt_qs as &$answer ) {\n\t\t\t\t\t\tif ( $answer['id'] == $qid ) {\n\t\t\t\t\t\t\t$answer['id'] = $question_copy_id;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t$attempt->set_questions( $attempt_qs, true );\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Need to run again.\n\treturn true;\n\n}\n\n/**\n * Create duplicates for any quiz attached to multiple lessons\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_ensure_no_lesson_dupe_rels() {\n\n\tif ( 'complete' !== get_transient( 'llms_update_3160_ensure_no_dupe_question_rels_status' ) ) {\n\t\treturn true;\n\t}\n\n\t$skip = get_transient( 'llms_3160_skipper_dupe_l' );\n\tif ( ! $skip ) {\n\t\t$skip = 0;\n\t}\n\tset_transient( 'llms_3160_skipper_dupe_l', $skip + 100, DAY_IN_SECONDS );\n\n\tglobal $wpdb;\n\t$res = $wpdb->get_results(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT post_id AS lesson_id, meta_value AS quiz_id\n\t\t FROM {$wpdb->postmeta}\n\t\t WHERE meta_key = '_llms_quiz'\n\t\t   AND meta_value != 0\n\t\t ORDER BY lesson_id ASC\n\t\t LIMIT %d, 100\n\t\t;\",\n\t\t\t$skip\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\tif ( ! $res ) {\n\t\tset_transient( 'llms_update_3160_ensure_no_lesson_dupe_rels', 'complete', DAY_IN_SECONDS );\n\t\treturn false;\n\t}\n\n\t$quizzes_set = array();\n\n\tforeach ( $res as $data ) {\n\n\t\t$lesson = llms_get_post( $data->lesson_id );\n\t\tif ( ! $lesson ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Quiz no longer exists, unset the data from the lesson.\n\t\t$quiz = llms_get_post( $data->quiz_id );\n\t\tif ( ! $quiz ) {\n\t\t\t$lesson->set( 'quiz', 0 );\n\t\t\t$lesson->set( 'quiz_enabled', 'no' );\n\t\t\tcontinue;\n\t\t}\n\n\t\t/**\n\t\t * Quiz already attached to a lesson\n\t\t * + duplicate it\n\t\t * + assign lesson/quiz relationships off new quiz\n\t\t * + find quiz attempts by old quiz / lesson\n\t\t * + update attempt quiz id\n\t\t * + update attempt question ids\n\t\t */\n\t\tif ( in_array( $data->quiz_id, $quizzes_set ) ) {\n\n\t\t\t$orig_questions = get_post_meta( $data->quiz_id, '_llms_questions', true );\n\t\t\t$qid_map        = array();\n\t\t\t$dupe_quiz_id   = llms_update_util_post_duplicator( $data->quiz_id );\n\t\t\tforeach ( $orig_questions as &$oqdata ) {\n\t\t\t\t$dupe_q                   = llms_update_util_post_duplicator( $oqdata['id'] );\n\t\t\t\t$qid_map[ $oqdata['id'] ] = $dupe_q;\n\t\t\t\t$oqdata['id']             = $dupe_q;\n\t\t\t}\n\t\t\tupdate_post_meta( $dupe_quiz_id, '_llms_questions', $orig_questions );\n\t\t\tupdate_post_meta( $dupe_quiz_id, '_llms_lesson_id', $data->lesson_id );\n\n\t\t\t$lesson->set( 'quiz', $dupe_quiz_id );\n\n\t\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\t$attempt_ids = $wpdb->get_col(\n\t\t\t\t\"\n\t\t\t\tSELECT id\n\t\t\t\tFROM {$wpdb->prefix}lifterlms_quiz_attempts\n\t\t\t\tWHERE quiz_id = {$data->quiz_id} AND lesson_id = {$data->lesson_id}\"\n\t\t\t); // db call ok; no-cache ok.\n\t\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t\tforeach ( $attempt_ids as $aid ) {\n\t\t\t\t$attempt   = new LLMS_Quiz_Attempt( $aid );\n\t\t\t\t$questions = $attempt->get_questions();\n\t\t\t\tforeach ( $questions as &$aqd ) {\n\n\t\t\t\t\tif ( isset( $qid_map[ $aqd['id'] ] ) ) {\n\t\t\t\t\t\t$aqd['id'] = $qid_map[ $aqd['id'] ];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$attempt->set_questions( $questions, true );\n\t\t\t\t$attempt->set( 'quiz_id', $dupe_quiz_id );\n\t\t\t\t$attempt->save();\n\n\t\t\t}\n\t\t}\n\n\t\t$quizzes_set[] = $data->quiz_id;\n\t\t$lesson->set( 'quiz_enabled', 'yes' ); // Ensure the new quiz enabled key is set.\n\n\t}\n\n\t// Run it again.\n\treturn true;\n\n}\n\n/**\n * Update question & choice data to new structure\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_update_question_data() {\n\n\tif ( 'complete' !== get_transient( 'llms_update_3160_ensure_no_lesson_dupe_rels' ) ) {\n\t\treturn true;\n\t}\n\n\t$skip = get_transient( 'llms_3160_skipper_qdata' );\n\tif ( ! $skip ) {\n\t\t$skip = 0;\n\t}\n\tset_transient( 'llms_3160_skipper_qdata', $skip + 100, DAY_IN_SECONDS );\n\n\tglobal $wpdb;\n\t$res = $wpdb->get_results(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT post_id AS quiz_id, meta_value AS questions\n\t\t FROM {$wpdb->postmeta}\n\t\t WHERE meta_key = '_llms_questions'\n\t\t ORDER BY post_id ASC\n\t\t LIMIT %d, 100;\",\n\t\t\t$skip\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\t// Finished.\n\tif ( ! $res ) {\n\t\tset_transient( 'llms_update_3160_update_question_data', 'complete', DAY_IN_SECONDS );\n\t\treturn false;\n\t}\n\n\tforeach ( $res as $data ) {\n\t\t$questions = maybe_unserialize( $data->questions );\n\t\tif ( is_array( $questions ) ) {\n\t\t\tforeach ( $questions as $raw_question ) {\n\n\t\t\t\t$points = isset( $raw_question['points'] ) ? $raw_question['points'] : 1;\n\n\t\t\t\t$question = llms_get_post( $raw_question['id'] );\n\n\t\t\t\tif ( ! $question ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$question->set( 'parent_id', $data->quiz_id );\n\t\t\t\t$question->set( 'question_type', 'choice' );\n\t\t\t\t$question->set( 'points', $points );\n\t\t\t\tupdate_post_meta( $question->get( 'id' ), '_llms_legacy_question_title', $question->get( 'title' ) );\n\t\t\t\t$question->set( 'title', strip_tags( str_replace( array( '<p>', '</p>' ), '', $question->get( 'content' ) ), '<b><em><u><strong><i>' ) );\n\n\t\t\t\t$options = get_post_meta( $question->get( 'id' ), '_llms_question_options', true );\n\n\t\t\t\tupdate_post_meta( $question->get( 'id' ), '_llms_legacy_question_options', $options );\n\t\t\t\tdelete_post_meta( $question->get( 'id' ), '_llms_question_options' );\n\n\t\t\t\tif ( ! $options ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t$clarify = '';\n\n\t\t\t\t$markers = range( 'A', 'Z' );\n\n\t\t\t\tforeach ( (array) $options as $index => $option ) {\n\n\t\t\t\t\tif ( ! isset( $option['option_text'] ) ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t$correct = false;\n\t\t\t\t\t// No correct_option set for the choice, set it to false.\n\t\t\t\t\tif ( ! isset( $option['correct_option'] ) ) {\n\t\t\t\t\t\t$correct = false;\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Handle bool strings like \"on\" \"off\" \"yes\" \"no\"\n\t\t\t\t\t\t * and questions imported from a 3rd party Excel to LifterLMS plugin\n\t\t\t\t\t\t * that doesn't save options in the expected format...\n\t\t\t\t\t\t *  dev if you're reading this I love you but you caused me a pretty large headache\n\t\t\t\t\t\t * trying to figure out where in our codebase we went wrong...\n\t\t\t\t\t\t */\n\t\t\t\t\t} elseif ( is_string( $option['correct_option'] ) && '' !== $option['correct_option'] ) {\n\t\t\t\t\t\t$correct = true;\n\t\t\t\t\t\t// Catch everything else and filter var it.\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t$correct = filter_var( $option['correct_option'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );\n\n\t\t\t\t\t\t// Nothing should get here but I'm tired...\n\t\t\t\t\t\tif ( is_null( $correct ) ) {\n\t\t\t\t\t\t\t$correct = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t$question->create_choice(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'choice'  => $option['option_text'],\n\t\t\t\t\t\t\t'correct' => $correct,\n\t\t\t\t\t\t\t'marker'  => $markers[ $index ],\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\n\t\t\t\t\t// If an option desc is set.\n\t\t\t\t\tif ( ! empty( $option['option_description'] ) ) {\n\t\t\t\t\t\t// If the description hasn't already been added to the new clarification.\n\t\t\t\t\t\tif ( false === strpos( $clarify, $option['option_description'] ) ) {\n\t\t\t\t\t\t\t$clarify .= $option['option_description'] . '<br><br>';\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( $clarify ) {\n\t\t\t\t\t$question->set( 'clarifications', trim( rtrim( $clarify, '<br><br>' ) ) );\n\t\t\t\t\t$question->set( 'clarifications_enabled', 'yes' );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Run it again.\n\treturn true;\n\n}\n\n/**\n * Update question data to new formats & match question choice indexes to new choice IDs\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_update_attempt_question_data() {\n\n\tif ( 'complete' !== get_transient( 'llms_update_3160_update_question_data' ) ) {\n\t\treturn true;\n\t}\n\n\t$skip = get_transient( 'llms_update_3160_skipper' );\n\tif ( ! $skip ) {\n\t\t$skip = 0;\n\t}\n\tset_transient( 'llms_update_3160_skipper', $skip + 500, DAY_IN_SECONDS );\n\n\tglobal $wpdb;\n\t$res = $wpdb->get_col( $wpdb->prepare( \"SELECT id FROM {$wpdb->prefix}lifterlms_quiz_attempts ORDER BY id ASC LIMIT %d, 500\", $skip ) ); // db call ok; no-cache ok.\n\n\t// Finished.\n\tif ( ! $res ) {\n\t\tset_transient( 'llms_update_3160_update_attempt_question_data', 'complete', DAY_IN_SECONDS );\n\t\treturn false;\n\t}\n\n\tforeach ( $res as $att_id ) {\n\n\t\t$attempt   = new LLMS_Quiz_Attempt( $att_id );\n\t\t$questions = $attempt->get_questions();\n\t\tforeach ( $questions as &$question ) {\n\n\t\t\t$question['earned'] = empty( $question['correct'] ) ? 0 : $question['points'];\n\t\t\tif ( ! isset( $question['answer'] ) ) {\n\t\t\t\t$question['answer'] = array();\n\t\t\t} elseif ( ! is_array( $question['answer'] ) && is_numeric( $question['answer'] ) ) {\n\t\t\t\t$obj = llms_get_post( $question['id'] );\n\t\t\t\tif ( $obj ) {\n\t\t\t\t\t$choices = $obj->get_choices();\n\t\t\t\t\tif ( isset( $choices[ $question['answer'] ] ) ) {\n\t\t\t\t\t\t$question['answer'] = array( $choices[ $question['answer'] ]->get( 'id' ) );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t$attempt->set_questions( $questions, true );\n\n\t}\n\n\treturn true;\n\n}\n\n/**\n * Ensure quizzes backreference their parent lessons\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_update_quiz_to_lesson_rels() {\n\n\tif ( 'complete' !== get_transient( 'llms_update_3160_update_attempt_question_data' ) ) {\n\t\treturn true;\n\t}\n\n\tglobal $wpdb;\n\t$ids = $wpdb->get_col( \"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_llms_quiz_enabled' AND meta_value = 'yes'\" );\n\n\tforeach ( $ids as $id ) {\n\n\t\t$lesson = llms_get_post( $id );\n\t\tif ( $lesson ) {\n\t\t\t$quiz_id = $lesson->get( 'quiz' );\n\t\t\tif ( $quiz_id ) {\n\t\t\t\t$quiz = llms_get_post( $quiz_id );\n\t\t\t\t$quiz->set( 'lesson_id', $id );\n\t\t\t}\n\t\t}\n\t}\n\n}\n\n/**\n * Add an admin notice about new quiz things\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_builder_notice() {\n\n\tif ( 'complete' !== get_transient( 'llms_update_3160_update_attempt_question_data' ) ) {\n\t\treturn true;\n\t}\n\n\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\tLLMS_Admin_Notices::add_notice(\n\t\t'update-3160',\n\t\tarray(\n\t\t\t'html'        => sprintf(\n\t\t\t\t__( 'Welcome to LifterLMS 3.16.0! This update adds significant improvements to the quiz-building experience. Notice quizzes and questions are no longer found under \"Courses\" on the sidebar? Your quizzes have not been deleted but they have been moved! Read more about the all new %1$squiz builder%2$s.', 'lifterlms' ),\n\t\t\t\t'<a href=\"http://blog.lifterlms.com/hello-quizzes/\" target=\"_blank\">',\n\t\t\t\t'</a>'\n\t\t\t),\n\t\t\t'type'        => 'info',\n\t\t\t'dismissible' => true,\n\t\t\t'remindable'  => false,\n\t\t)\n\t);\n\n}\n\n/**\n * Update db version at conclusion of 3.16.0 updates\n *\n * @since 3.16.0\n *\n * @return void\n */\nfunction llms_update_3160_update_db_version() {\n\n\tif ( 'complete' !== get_transient( 'llms_update_3160_update_attempt_question_data' ) ) {\n\t\treturn true;\n\t}\n\n\tLLMS_Install::update_db_version( '3.16.0' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-3280.php",
    "content": "<?php\n/**\n * Update functions for version 3.28.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Clear the unused cron `lifterlms_cleanup_sessions`\n *\n * @since 3.28.0\n *\n * @return void\n */\nfunction llms_update_3280_clear_session_cleanup_cron() {\n\twp_clear_scheduled_hook( 'lifterlms_cleanup_sessions' );\n}\n\n/**\n * Update db version at conclusion of 3.28.0 updates\n *\n * @return void\n *\n * @since 3.28.0\n */\nfunction llms_update_3280_update_db_version() {\n\tLLMS_Install::update_db_version( '3.28.0' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-343.php",
    "content": "<?php\n/**\n * Update functions for version 3.4.3\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Rename meta keys for parent section and parent course relationships for all LifterLMS Lessons and Sections\n *\n * @since 3.4.3\n *\n * @return void\n */\nfunction llms_update_343_update_relationships() {\n\n\tglobal $wpdb;\n\n\t// Update parent course key for courses and lessons.\n\t$wpdb->query(\n\t\t\"UPDATE {$wpdb->postmeta} AS m\n\t\t JOIN {$wpdb->posts} AS p ON p.ID = m.post_id\n\t\t SET m.meta_key = '_llms_parent_course'\n\t\t WHERE m.meta_key = '_parent_course'\n\t\t   AND ( p.post_type = 'lesson' OR p.post_type = 'section' );\"\n\t);\n\n\t// Update parent section key for lessons.\n\t$wpdb->query(\n\t\t\"UPDATE {$wpdb->postmeta} AS m\n\t\t JOIN {$wpdb->posts} AS p ON p.ID = m.post_id\n\t\t SET m.meta_key = '_llms_parent_section'\n\t\t WHERE m.meta_key = '_parent_section'\n\t\t   AND p.post_type = 'lesson';\"\n\t);\n\n}\n\n/**\n * Update db version at conclusion of 3.4.3 updates\n *\n * @since 3.4.3\n *\n * @return void\n */\nfunction llms_update_343_update_db_version() {\n\n\tLLMS_Install::update_db_version( '3.4.3' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-360.php",
    "content": "<?php\n/**\n * Update functions for version 3.6.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Add course and membership visibility settings\n *\n * Default course is catalog only and default membership is catalog & search.\n * Courses were NOT SEARCHABLE in earlier versions.\n *\n * @since 3.6.0\n *\n * @return void\n */\nfunction llms_update_360_set_product_visibility() {\n\t$query = new WP_Query(\n\t\tarray(\n\t\t\t'post_status'    => 'any',\n\t\t\t'post_type'      => array( 'course', 'llms_membership' ),\n\t\t\t'posts_per_page' => -1,\n\t\t)\n\t);\n\tif ( $query->have_posts() ) {\n\t\tforeach ( $query->posts as $post ) {\n\t\t\t$visibility = ( 'course' === $post->post_type ) ? 'catalog' : 'catalog_search';\n\t\t\twp_set_object_terms( $post->ID, $visibility, 'llms_product_visibility', false );\n\t\t}\n\t}\n}\n\n/**\n * Update db version at conclusion of 3.6.0 updates\n *\n * @since 3.6.0\n *\n * @return void\n */\nfunction llms_update_360_update_db_version() {\n\n\tLLMS_Install::update_db_version( '3.6.0' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-380.php",
    "content": "<?php\n/**\n * Update functions for version 3.8.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 3.39.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Add visibility settings to all access plans and delete the \"featured\" meta values for all access plans\n *\n * @since 3.8.0\n *\n * @return void\n */\nfunction llms_update_380_set_access_plan_visibility() {\n\t$query = new WP_Query(\n\t\tarray(\n\t\t\t'post_status'    => 'any',\n\t\t\t'post_type'      => array( 'llms_access_plan' ),\n\t\t\t'posts_per_page' => -1,\n\t\t)\n\t);\n\tif ( $query->have_posts() ) {\n\t\tforeach ( $query->posts as $post ) {\n\t\t\t$plan       = llms_get_post( $post );\n\t\t\t$visibility = $plan->is_featured() ? 'featured' : 'visible';\n\t\t\twp_set_object_terms( $post->ID, $visibility, 'llms_access_plan_visibility', false );\n\t\t\tdelete_post_meta( $post->ID, '_llms_featured' );\n\t\t}\n\t}\n}\n\n/**\n * Update db version at conclusion of 3.8.0 updates\n *\n * @since 3.8.0\n *\n * @return void\n */\nfunction llms_update_380_update_db_version() {\n\n\tLLMS_Install::update_db_version( '3.8.0' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-400.php",
    "content": "<?php\n/**\n * Update functions for version 4.0.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 4.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Remove session data stored on the options table by removed the WP Session Manager library\n *\n * @since 4.0.0\n *\n * @return void\n */\nfunction llms_update_400_remove_session_options() {\n\tglobal $wpdb;\n\t$wpdb->query( \"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_wp_session_%';\" ); // db call ok; no cache ok.\n}\n\n/**\n * Clear cron hook used by the WP Session Manager library to cleanup expired sessions\n *\n * @since 4.0.0\n *\n * @return void\n */\nfunction llms_update_400_clear_session_cron() {\n\twp_clear_scheduled_hook( 'wp_session_garbage_collection' );\n}\n\n/**\n * Update db version to 4.0.0\n *\n * @since 4.0.0\n *\n * @return void\n */\nfunction llms_update_400_update_db_version() {\n\tLLMS_Install::update_db_version( '4.0.0' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-4150.php",
    "content": "<?php\n/**\n * Update functions for version 4.15.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 4.15.0\n * @version 4.15.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Remove orphan access plans\n *\n * @since 4.15.0\n *\n * @return bool True if it needs to run again, false otherwise.\n */\nfunction llms_update_4150_remove_orphan_access_plans() {\n\n\t$limit = 50;\n\n\tglobal $wpdb;\n\n\t$orphan_access_plans = $wpdb->get_col(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT pm.post_id AS apid\n\t\t\tFROM {$wpdb->postmeta} AS pm\n\t\t\tLEFT JOIN {$wpdb->posts} AS p\n\t\t\tON pm.meta_value = p.ID\n\t\t\tWHERE pm.meta_key = '_llms_product_id'\n\t\t\tAND p.ID IS NULL\n\t\t\tORDER BY apid ASC\n\t\t\tLIMIT %d\n\t\t\",\n\t\t\t$limit\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\t// Finished.\n\tif ( empty( $orphan_access_plans ) ) {\n\t\tset_transient( 'llms_update_4150_remove_orphan_access_plans', 'complete', DAY_IN_SECONDS );\n\t\treturn false;\n\t}\n\n\tforeach ( $orphan_access_plans as $orphan_access_plan_id ) {\n\t\twp_delete_post( $orphan_access_plan_id );\n\t}\n\n\t// Needs to run again.\n\treturn true;\n}\n\n/**\n * Update db version to 4.15.0\n *\n * @since 4.15.0\n *\n * @return void|true True if it needs to run again, nothing if otherwise.\n */\nfunction llms_update_4150_update_db_version() {\n\tif ( 'complete' !== get_transient( 'llms_update_4150_remove_orphan_access_plans' ) ) {\n\t\t// Needs to run again.\n\t\treturn true;\n\t}\n\tLLMS_Install::update_db_version( '4.15.0' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-450.php",
    "content": "<?php\n/**\n * Update functions for version 4.5.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 4.5.0\n * @version 4.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Record open sessions in wp_lifterlms_events_open_sessions\n *\n * @since 4.5.0\n *\n * @return bool True if it needs to run again, false otherwise.\n */\nfunction llms_update_450_migrate_events_open_sessions() {\n\n\t$limit = 200;\n\t$skip  = get_transient( 'llms_450_skipper_events_open_sessions' );\n\tif ( ! $skip ) {\n\t\t$skip = 0;\n\t}\n\tset_transient( 'llms_450_skipper_events_open_sessions', $skip + $limit, DAY_IN_SECONDS );\n\n\tglobal $wpdb;\n\t$maybe_open_sessions = $wpdb->get_results(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT id, actor_id, object_id\n\t\t\tFROM {$wpdb->prefix}lifterlms_events\n\t\t\tWHERE event_type='session'\n\t\t\tAND event_action='start'\n\t\t\tORDER BY id ASC\n\t\t\tLIMIT %d, %d\n\t\t\",\n\t\t\t$skip,\n\t\t\t$limit\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\t// Finished.\n\tif ( empty( $maybe_open_sessions ) ) {\n\t\tset_transient( 'llms_update_450_migrate_events_open_sessions', 'complete', DAY_IN_SECONDS );\n\t\treturn false;\n\t}\n\n\t$insert = '';\n\tforeach ( $maybe_open_sessions as $maybe_open_session ) {\n\t\t// Create an event instance so to pass it to the `LLMS_Sessions::instance()->is_session_open()` util.\n\t\t$start = new LLMS_Event( $maybe_open_session->id );\n\n\t\t// Set the only useful properties, without the need to save them from the db.\n\t\t$start->set( 'actor_id', $maybe_open_session->actor_id, false );\n\t\t$start->set( 'object_id', $maybe_open_session->object_id, false );\n\n\t\tif ( LLMS_Sessions::instance()->is_session_open( $start ) ) {\n\t\t\tif ( ! empty( $insert ) ) {\n\t\t\t\t$insert .= ', ';\n\t\t\t}\n\t\t\t$insert .= $wpdb->prepare( '(%s)', $maybe_open_session->id );\n\t\t}\n\t}\n\n\t// Add the open sessions to the new table.\n\tif ( ! empty( $insert ) ) {\n\t\t$wpdb->query(\n\t\t\t\"INSERT INTO {$wpdb->prefix}lifterlms_events_open_sessions ( `event_id` ) VALUES \" . $insert . ';' // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Values are prepared above.\n\t\t); // db call ok; no-cache ok.\n\t}\n\n\t// Needs to run again.\n\treturn true;\n}\n\n/**\n * Update db version to 4.5.0\n *\n * @since 4.5.0\n *\n * @return void|true True if it needs to run again, nothing if otherwise.\n */\nfunction llms_update_450_update_db_version() {\n\tif ( 'complete' !== get_transient( 'llms_update_450_migrate_events_open_sessions' ) ) {\n\t\t// Needs to run again.\n\t\treturn true;\n\t}\n\tLLMS_Install::update_db_version( '4.5.0' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-500.php",
    "content": "<?php\n/**\n * Update functions for version 5.0.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Turn off autoload for accounting legacy options\n *\n * @since 5.0.0\n *\n * @return bool True if it needs to run again, false otherwise.\n */\nfunction llms_update_500_legacy_options_autoload_off() {\n\n\tglobal $wpdb;\n\n\t$legacy_options_to_stop_autoloading = array(\n\t\t'lifterlms_registration_generate_username',\n\t\t'lifterlms_registration_password_strength',\n\t\t'lifterlms_registration_password_min_strength',\n\t);\n\n\t$sql = \"\n\t\tUPDATE {$wpdb->options} SET autoload='no'\n\t\tWHERE option_name IN (\" . implode( ', ', array_fill( 0, count( $legacy_options_to_stop_autoloading ), '%s' ) ) . ')';\n\n\t$wpdb->query(\n\t\t$wpdb->prepare(\n\t\t\t$sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- No user input, it's safe.\n\t\t\t$legacy_options_to_stop_autoloading\n\t\t)\n\t); // db call ok; no-cache ok.\n\n\treturn false;\n\n}\n\n/**\n * Admin welcome notice\n *\n * @since 5.0.0\n *\n * @return void\n */\nfunction llms_update_500_add_admin_notice() {\n\n\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\t$notice_id        = 'v500-welcome-msg';\n\t$get_started_link = add_query_arg(\n\t\tarray(\n\t\t\t'post_type' => 'llms_form',\n\t\t),\n\t\tadmin_url( 'edit.php' )\n\t);\n\n\t$html = sprintf(\n\t\t'<strong>%1$s</strong><br><br>%2$s<br><br>%3$s',\n\t\t__( 'Welcome to LifterLMS 5.0!', 'lifterlms' ),\n\t\t__( 'This new version of LifterLMS brings you the power to build and customize your student information forms using a simple point and click interface constructed on top of the WordPress block editor. Customization like the removal of default fields, changing the text of field labels, or reordering fields within a form is all possible without any code or professional help.', 'lifterlms' ),\n\t\tsprintf(\n\t\t\t// Translators: %1$s = Opening anchor tag to Forms admin page; %2$s = Closing anchor tag.\n\t\t\t__( '%1$sGet Started%2$s', 'lifterlms' ),\n\t\t\t'<a class=\"button-primary\" href=\"' . esc_url( $get_started_link ) . '\" >',\n\t\t\t'</a>'\n\t\t) . ' ' .\n\t\tsprintf(\n\t\t\t// Translators: %1$s = Opening anchor tag to the welcome blog post on lifterlms.com; %2$s = Closing anchor tag.\n\t\t\t__( '%1$sRead More%2$s', 'lifterlms' ),\n\t\t\t'<a class=\"button\" href=\"https://blog.lifterlms.com/5-0/\" target=\"_blank\" rel=\"noopener\">',\n\t\t\t'</a>'\n\t\t)\n\t);\n\n\tLLMS_Admin_Notices::add_notice(\n\t\t$notice_id,\n\t\t$html,\n\t\tarray(\n\t\t\t'type'             => 'success',\n\t\t\t'dismiss_for_days' => 0,\n\t\t\t'remindable'       => false,\n\t\t)\n\t);\n\treturn false;\n}\n\n/**\n * Update db version to 5.0.0\n *\n * @since 5.0.0]\n *\n * @return void|true True if it needs to run again, nothing if otherwise.\n */\nfunction llms_update_500_update_db_version() {\n\n\tLLMS_Install::update_db_version( '5.0.0' );\n\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-520.php",
    "content": "<?php\n/**\n * Update functions for version 5.2.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 5.2.0\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Explicitly set no subscribers for the new upcoming payment reminder notification\n *\n * @since 5.2.0\n *\n * @return bool True if it needs to run again, false otherwise.\n */\nfunction llms_update_520_upcoming_reminder_notification_backward_compat() {\n\n\t$subscribers_for_type = array(\n\t\t'email' => array(\n\t\t\t'student',\n\t\t),\n\t\t'basic' => array(\n\t\t\t'student',\n\t\t\t'author',\n\t\t\t'custom',\n\t\t),\n\t);\n\n\tforeach ( $subscribers_for_type as $type => $subscribers ) {\n\t\tadd_option( \"llms_notification_upcoming_payment_reminder_{$type}_subscribers\", array_fill_keys( $subscribers, 'no' ) );\n\t}\n\n\treturn false;\n\n}\n\n/**\n * Update db version to 5.2.0\n *\n * @since 5.2.0\n *\n * @return void|true True if it needs to run again, nothing if otherwise.\n */\nfunction llms_update_520_update_db_version() {\n\tLLMS_Install::update_db_version( '5.2.0' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-600.php",
    "content": "<?php\n/**\n * Update functions for version 6.0.0.\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\nnamespace LLMS\\Updates\\Version_6_0_0;\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieves the DB version of the migration.\n *\n * @since 6.0.0\n *\n * @return string\n */\nfunction _get_db_version() {\n\treturn '6.0.0';\n}\n\n/**\n * Migrate deprecated meta values for earned achievements.\n *\n * @since 6.0.0\n *\n * @return bool Returns `true` if more records need to be updated and `false` upon completion.\n */\nfunction migrate_achievements() {\n\treturn _migrate_awards( 'achievement' );\n}\n\n/**\n * Migrate deprecated meta values for earned certificates.\n *\n * @since 6.0.0\n *\n * @return bool Returns `true` if more records need to be updated and `false` upon completion.\n */\nfunction migrate_certificates() {\n\treturn _migrate_awards( 'certificate' );\n}\n\n/**\n * Migrates meta data for achievement and certificate template posts.\n *\n * @since 6.0.0\n *\n * @return bool Returns `true` if more records need to be updated and `false` upon completion.\n */\nfunction migrate_award_templates() {\n\n\t$per_page = llms_update_util_get_items_per_page();\n\n\t$query = new \\WP_Query(\n\t\tarray(\n\t\t\t'orderby'        => array( 'ID' => 'ASC' ),\n\t\t\t'post_status'    => 'any',\n\t\t\t'post_type'      => array( 'llms_achievement', 'llms_certificate' ),\n\t\t\t'posts_per_page' => $per_page,\n\t\t\t'no_found_rows'  => true, // We don't care about found rows since we'll run the query as many times as needed anyway.\n\t\t\t'meta_query'     => array(\n\t\t\t\t'relation' => 'OR',\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_achievement_image',\n\t\t\t\t\t'compare' => 'EXISTS',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_certificate_image',\n\t\t\t\t\t'compare' => 'EXISTS',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_achievement_content',\n\t\t\t\t\t'compare' => 'EXISTS',\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t);\n\n\t$legacy_option_added['achievement'] = false;\n\t$legacy_option_added['certificate'] = false;\n\n\tforeach ( $query->posts as $post ) {\n\n\t\t$type           = llms_strip_prefixes( $post->post_type );\n\t\t$image_migrated = _migrate_image( $post->ID, $type );\n\n\t\tif ( 'achievement' === $type ) {\n\t\t\t_migrate_achievement_content( $post->ID );\n\t\t}\n\n\t\tif ( ! $legacy_option_added[ $type ] && ! $image_migrated ) {\n\t\t\t_add_legacy_opt( $type );\n\t\t\t$legacy_option_added[ $type ] = true;\n\t\t}\n\t}\n\n\t// If there was 50 results assume there's another page and run again, otherwise we're done.\n\treturn ( count( $query->posts ) === $per_page );\n\n}\n\n/**\n * Shows an admin welcome notice.\n *\n * @since 6.0.0\n *\n * @return boolean\n */\nfunction show_notice() {\n\n\t$notice_id = sprintf( 'v%s-welcome-msg', str_replace( array( '.', '-' ), '', _get_db_version() ) );\n\n\t$get_started_link = admin_url( 'post-new.php?post_type=llms_certificate' );\n\n\t$html = sprintf(\n\t\t'<strong>%1$s</strong><br><br>%2$s<br><br>%3$s',\n\t\t__( 'Welcome to LifterLMS 6.0.0!', 'lifterlms' ),\n\t\t__( \"This new version brings you the power to build and customize certificates using the WordPress block editor! Start building beautiful certificates using all the blocks you're already familiar, the new Certificate Title block, and new certificate settings like page size (Letter or A4, for example), orientation, background color, and more. In addition to design features, this version also adds the ability to modify already-awarded certificates and achievements and to sync template updates with the click of a button.\", 'lifterlms' ),\n\t\tsprintf(\n\t\t\t// Translators: %1$s = Opening anchor tag to Forms admin page; %2$s = Closing anchor tag.\n\t\t\t__( '%1$sGet Started%2$s', 'lifterlms' ),\n\t\t\t'<a class=\"button-primary\" href=\"' . esc_url( $get_started_link ) . '\" >',\n\t\t\t'</a>'\n\t\t) . ' ' .\n\t\tsprintf(\n\t\t\t// Translators: %1$s = Opening anchor tag to the welcome blog post on lifterlms.com; %2$s = Closing anchor tag.\n\t\t\t__( '%1$sRead More%2$s', 'lifterlms' ),\n\t\t\t'<a class=\"button\" href=\"https://blog.lifterlms.com/6-beta?utm_source=notice&utm_medium=product&utm_campaign=lifterlmsplugin&utm_content=600-welcome\" target=\"_blank\" rel=\"noopener\">',\n\t\t\t'</a>'\n\t\t)\n\t);\n\n\t\\LLMS_Admin_Notices::add_notice(\n\t\t$notice_id,\n\t\t$html,\n\t\tarray(\n\t\t\t'type'             => 'success',\n\t\t\t'dismiss_for_days' => 0,\n\t\t\t'remindable'       => false,\n\t\t)\n\t);\n\treturn false;\n\n}\n\n/**\n * Update db version to 6.0.0.\n *\n * @since 6.0.0\n *\n * @return boolean\n */\nfunction update_db_version() {\n\t\\LLMS_Install::update_db_version( _get_db_version() );\n\treturn false;\n}\n\n/**\n * Migrate deprecated meta values for user awards by type.\n *\n * Queries 50 earned awards at a time and migrates their data by moving meta data\n * to the new location and then deleting the deprecated meta values.\n *\n * @since 6.0.0\n *\n * @param string $type Award type, either \"achievement\" or \"certificate\".\n * @return boolean Returns `true` if there are more results and `false` if there are no further results.\n */\nfunction _migrate_awards( $type ) {\n\n\t$per_page = llms_update_util_get_items_per_page();\n\n\t$query_args = array(\n\t\t'orderby'        => array( 'ID' => 'ASC' ),\n\t\t'post_type'      => \"llms_my_{$type}\",\n\t\t'post_status'    => 'any',\n\t\t'posts_per_page' => $per_page,\n\t\t'no_found_rows'  => true, // We don't care about found rows since we'll run the query as many times as needed anyway.\n\t\t'fields'         => 'ids', // We just need the ID for the updates we'll perform.\n\t\t'meta_query'     => array(\n\t\t\t'relation' => 'OR',\n\t\t\tarray(\n\t\t\t\t'key'     => \"_llms_{$type}_title\",\n\t\t\t\t'compare' => 'EXISTS',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'key'     => \"_llms_{$type}_template\",\n\t\t\t\t'compare' => 'EXISTS',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'key'     => \"_llms_{$type}_image\",\n\t\t\t\t'compare' => 'EXISTS',\n\t\t\t),\n\t\t),\n\t);\n\n\tif ( 'achievement' === $type ) {\n\t\t$query_args['meta_query'][] = array(\n\t\t\t'key'     => '_llms_achievement_content',\n\t\t\t'compare' => 'EXISTS',\n\t\t);\n\t}\n\n\t$query = new \\WP_Query( $query_args );\n\n\t// Don't trigger deprecations.\n\tremove_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\n\t// Don't trigger save hooks.\n\tremove_action( \"save_post_llms_my_{$type}\", array( 'LLMS_Controller_Awards', 'on_save' ), 20 );\n\n\t$legacy_option_added = false;\n\tforeach ( $query->posts as $post_id ) {\n\n\t\t$image_migrated = _migrate_award( $post_id, $type );\n\n\t\tif ( ! $legacy_option_added && ! $image_migrated ) {\n\t\t\t_add_legacy_opt( $type );\n\t\t\t$legacy_option_added = true;\n\t\t}\n\t}\n\t// Re-enable deprecations.\n\tadd_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\n\t// Re-enabled save hooks.\n\tadd_action( \"save_post_llms_my_{$type}\", array( 'LLMS_Controller_Awards', 'on_save' ), 20 );\n\n\t// If there was 50 results assume there's another page and run again, otherwise we're done.\n\treturn ( count( $query->posts ) === $per_page );\n\n}\n\n/**\n * Migrate meta values for a single award.\n *\n * Performs the following updates:\n *   + Copies lifterlms_user_postmeta user data to the post_author property.\n *   + Moves the title from postmeta to the post_title property.\n *   + Moves the template relationship from meta to the post_parent property.\n *   + Moves the award image from custom meta to the post's featured image.\n *\n * And then deletes the previous metadata after performing the necessary updates.\n *\n * @since 6.0.0\n *\n * @param int    $post_id WP_Post ID.\n * @param string $type    Award type, either \"achievement\" or \"certificate\".\n * @return bool `true` if there was an image to migrate, else `false`.\n */\nfunction _migrate_award( $post_id, $type ) {\n\n\t$obj = 'achievement' === $type ? new \\LLMS_User_Achievement( $post_id ) : new \\LLMS_User_Certificate( $post_id );\n\n\t$updates = array(\n\t\t'awarded' => $obj->get_earned_date( 'Y-m-d H:i:s' ),\n\t\t'author'  => $obj->get_user_id(),\n\t);\n\n\t$title = get_post_meta( $post_id, \"_llms_{$type}_title\", true );\n\tif ( $title ) {\n\t\t$updates['title'] = $title;\n\t}\n\n\t$template = get_post_meta( $post_id, \"_llms_{$type}_template\", true );\n\tif ( $template ) {\n\t\t$updates['parent'] = $template;\n\t}\n\t$obj->set_bulk( $updates );\n\n\t$image_migrated = _migrate_image( $post_id, $type );\n\n\tif ( 'achievement' === $type ) {\n\t\t_migrate_achievement_content( $post_id );\n\t}\n\n\tdelete_post_meta( $post_id, \"_llms_{$type}_title\" );\n\tdelete_post_meta( $post_id, \"_llms_{$type}_template\" );\n\n\treturn $image_migrated;\n}\n\n/**\n * Migrate the achievement content legacy post meta to post_content.\n *\n * @since 6.0.0\n *\n * @param int $post_id WP_Post ID.\n * @return void\n */\nfunction _migrate_achievement_content( $post_id ) {\n\t$meta_key = '_llms_achievement_content';\n\t$content  = get_post_meta( $post_id, $meta_key, true );\n\tif ( $content ) {\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'           => $post_id,\n\t\t\t\t'post_content' => $content,\n\t\t\t)\n\t\t);\n\t}\n\n\tdelete_post_meta( $post_id, $meta_key );\n\n}\n\n/**\n * Migrate the attachment image id from the legacy post meta location\n * to the WP core's featured image.\n *\n * @since 6.0.0\n *\n * @param int    $post_id WP_Post ID.\n * @param string $type    Award type, either \"achievement\" or \"certificate\".\n * @return bool `true` if there was an image to migrate, else `false`.\n */\nfunction _migrate_image( $post_id, $type ) {\n\n\t$image_migrated = false;\n\t$image          = get_post_meta( $post_id, \"_llms_{$type}_image\", true );\n\tif ( $image ) {\n\t\tset_post_thumbnail( $post_id, $image );\n\t\t$image_migrated = true;\n\t}\n\n\tdelete_post_meta( $post_id, \"_llms_{$type}_image\" );\n\n\treturn $image_migrated;\n}\n\n/**\n * Adds an option used to determine if the site has at least one legacy achievement or certificate template or award\n * that uses the default image.\n *\n * @since 6.0.0\n *\n * @param string $engagement_type Either 'achievement' or 'certificate'.\n * @return void\n */\nfunction _add_legacy_opt( $engagement_type ) {\n\tupdate_option( \"llms_has_{$engagement_type}s_with_legacy_default_image\", 'yes', 'no' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-6100.php",
    "content": "<?php\n/**\n * Update functions for version 6.10.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 6.10.0\n * @version 6.10.0\n */\n\nnamespace LLMS\\Updates\\Version_6_10_0;\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieves the DB version of the migration.\n *\n * @since 6.10.0\n *\n * @access private\n *\n * @return string\n */\nfunction _get_db_version() {\n\treturn '6.10.0';\n}\n\n/**\n * Migrates spanish user's provinces to the correct ones.\n *\n * @since 6.10.0\n *\n * @return bool Returns `true` if more records need to be updated and `false` upon completion.\n */\nfunction migrate_spanish_users() {\n\n\t$per_page = \\llms_update_util_get_items_per_page();\n\n\t$states_migration_map = array(\n\t\t'AS' => 'O', // Asturias.\n\t\t'CB' => 'S', // Cantabria.\n\t\t'RI' => 'LO', // 'La Rioja'.\n\t\t'MD' => 'M', // Madrid.\n\t\t'MC' => 'MU', // 'Murcia'.\n\t\t'NC' => 'NA', // 'Navarra'.\n\t\t'VC' => 'V', // 'Valencia'.\n\t\t// No map.\n\t\t'AN' => '',\n\t\t'AR' => '',\n\t\t'PV' => '',\n\t\t'CN' => '',\n\t\t'CL' => '',\n\t\t'CM' => '',\n\t\t'CT' => '',\n\t\t'EX' => '',\n\t\t'GA' => '',\n\t\t'SA' => '',\n\t);\n\n\t$query = new \\WP_User_Query(\n\t\tarray(\n\t\t\t'orderby'        => array(\n\t\t\t\t'ID' => 'ASC',\n\t\t\t),\n\t\t\t'meta_query'     => array(\n\t\t\t\t'relation' => 'AND',\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => 'llms_billing_country',\n\t\t\t\t\t'value'   => 'ES',\n\t\t\t\t\t'compare' => '=',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => 'llms_billing_state',\n\t\t\t\t\t'value'   => array_keys( $states_migration_map ),\n\t\t\t\t\t'compare' => 'IN',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'posts_per_page' => $per_page,\n\t\t\t'no_found_rows'  => true, // We don't care about found rows since we'll run the query as many times as needed anyway.\n\t\t)\n\t);\n\n\t$users = $query->get_results();\n\tif ( $users ) {\n\t\tforeach ( $users as $user ) {\n\t\t\t$new_state = $states_migration_map[ \\get_user_meta( $user->ID, 'llms_billing_state', true ) ] ?? '';\n\t\t\t\\update_user_meta( $user->ID, 'llms_billing_state', $new_state );\n\t\t}\n\t}\n\n\t// If there was `$per_page` results assume there's another page and run again, otherwise we're done.\n\treturn ( count( $users ) === $per_page );\n\n}\n\n/**\n * Update db version to 6.10.0.\n *\n * @since 6.10.0\n *\n * @return false.\n */\nfunction update_db_version() {\n\t\\LLMS_Install::update_db_version( _get_db_version() );\n\treturn false;\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-630.php",
    "content": "<?php\n/**\n * Update functions for version 6.3.0.\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 6.3.0\n * @version 6.3.0\n */\n\nnamespace LLMS\\Updates\\Version_6_3_0;\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * For old users: explicitly limit by default the buddypress profile endpoints to those existing prior to 6.3.0.\n *\n * @since 6.3.0\n *\n * @return bool True if it needs to run again, false otherwise.\n */\nfunction buddypress_profile_endpoints_bc() {\n\n\tif ( ! llms_parse_bool( get_option( 'llms_integration_buddypress_enabled', 'no' ) ) ) {\n\t\treturn;\n\t}\n\n\tupdate_option(\n\t\t'llms_integration_buddypress_profile_endpoints',\n\t\tarray(\n\t\t\t'view-courses',\n\t\t\t'view-memberships',\n\t\t\t'view-achievements',\n\t\t\t'view-certificates',\n\t\t)\n\t);\n\n\treturn false;\n\n}\n\n/**\n * Update db version to 6.3.0.\n *\n * @since 6.3.0\n *\n * @return void|true True if it needs to run again, nothing if otherwise.\n */\nfunction update_db_version() {\n\t\\LLMS_Install::update_db_version( '6.3.0' );\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-750.php",
    "content": "<?php\n/**\n * Update functions for version 7.5.0\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 7.5.0\n * @version [versoin]]\n */\n\nnamespace LLMS\\Updates\\Version_7_5_0;\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieves the DB version of the migration.\n *\n * @since 7.5.0\n *\n * @access private\n *\n * @return string\n */\nfunction _get_db_version() {\n\treturn '7.5.0';\n}\n\n/**\n * Disable favorites feature for old users.\n *\n * @since 7.5.0\n *\n * @return void\n */\nfunction favorites_feature_bc() {\n\tupdate_option( 'lifterlms_favorites', 'no' );\n}\n\n/**\n * Update db version to 7.5.0\n *\n * @since 7.5.0\n *\n * @return false.\n */\nfunction update_db_version() {\n\t\\LLMS_Install::update_db_version( _get_db_version() );\n\treturn false;\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-780.php",
    "content": "<?php\n/**\n * Update functions for version [version]\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 7.8.0\n * @version 7.8.0\n */\n\nnamespace LLMS\\Updates\\Version_7_8_0;\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieves the DB version of the migration.\n *\n * @since 7.8.0\n *\n * @access private\n *\n * @return string\n */\nfunction _get_db_version() {\n\treturn '7.8.0';\n}\n\n/**\n * Create a new option to enable Access Plan SKUs if any existing plans have a SKU set.\n *\n * @since 7.8.0\n *\n * @return false\n */\nfunction maybe_set_option_llms_access_plans_allow_skus() {\n\t// Find postmeta values for `_llms_plan_sku` that are not empty.\n\tglobal $wpdb;\n\t$found_plan_skus = $wpdb->get_results(\n\t\t\"SELECT *\n\t\t FROM {$wpdb->postmeta}\n\t\t WHERE meta_key = '_llms_sku'\n\t\t AND meta_value != ''\"\n\t);\n\n\t// If we found a plan with a SKU, update the option and return.\n\tif ( $found_plan_skus ) {\n\t\tupdate_option( 'llms_access_plans_allow_skus', 'yes' );\n\t}\n\n\treturn false;\n}\n\n/**\n * Update db version to [version].\n *\n * @since 7.8.0\n *\n * @return false.\n */\nfunction update_db_version() {\n\t\\LLMS_Install::update_db_version( _get_db_version() );\n\treturn false;\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-785.php",
    "content": "<?php\n/**\n * Update functions for version [version]\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 7.8.5\n * @version 7.8.5\n */\n\nnamespace LLMS\\Updates\\Version_7_8_5;\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieves the DB version of the migration.\n *\n * @since 7.8.5\n *\n * @access private\n *\n * @return string\n */\nfunction _get_db_version() {\n\treturn '7.8.5';\n}\n\n/**\n * Verify and delete the password_confirm usermeta.\n *\n * @since 7.8.5\n *\n * @return false\n */\nfunction maybe_remove_pwc() {\n\tglobal $wpdb;\n\t$found_pwc_meta = $wpdb->get_results(\n\t\t\"SELECT *\n\t\t FROM {$wpdb->usermeta}\n\t\t WHERE meta_key = 'password_confirm'\"\n\t);\n\n\tif ( $found_pwc_meta ) {\n\t\tupdate_option( 'llms_pwc_notice', 'yes' );\n\n\t\t$wpdb->query(\n\t\t\t\"DELETE\n\t\t FROM {$wpdb->usermeta}\n\t\t WHERE meta_key = 'password_confirm'\"\n\t\t);\n\n\t\tshow_notice();\n\t}\n\n\treturn false;\n}\n\n/**\n * Shows an admin notice.\n *\n * @since 7.8.5\n *\n * @return boolean\n */\nfunction show_notice() {\n\n\t$notice_id = sprintf( 'v%s-msg', str_replace( array( '.', '-' ), '', _get_db_version() ) );\n\n\t$html = sprintf(\n\t\t'<strong>%1$s</strong><br><br>%2$s<br><br>%3$s',\n\t\t__( 'Security Notice', 'lifterlms' ),\n\t\tsprintf(\n\t\t\t// Translators: %1$s = Opening anchor tag to the welcome blog post on lifterlms.com; %2$s = Closing anchor tag.\n\t\t\t__( 'We\\'ve detected that your site has been affected by a security issue fixed in the v7.8.5 update to LifterLMS. Further action is required. Your site may have been saving user passwords to the user meta table in plaintext. %1$sClick here to learn more%2$s.', 'lifterlms' ),\n\t\t\t'<a href=\"https://lifterlms.com/blog/security-release-password-block/?utm_source=notice&utm_medium=product&utm_campaign=lifterlmsplugin&utm_content=785-notice\" target=\"_blank\" rel=\"noopener\">',\n\t\t\t'</a>'\n\t\t),\n\t\tsprintf(\n\t\t\t// Translators: %1$s = Opening anchor tag to the welcome blog post on lifterlms.com; %2$s = Closing anchor tag.\n\t\t\t__( '%1$sRead More%2$s', 'lifterlms' ),\n\t\t\t'<a class=\"button\" href=\"https://lifterlms.com/blog/security-release-password-block/?utm_source=notice&utm_medium=product&utm_campaign=lifterlmsplugin&utm_content=785-notice\" target=\"_blank\" rel=\"noopener\">',\n\t\t\t'</a>'\n\t\t)\n\t);\n\n\t\\LLMS_Admin_Notices::add_notice(\n\t\t$notice_id,\n\t\t$html,\n\t\tarray(\n\t\t\t'type'             => 'error',\n\t\t\t'dismiss_for_days' => 0,\n\t\t\t'remindable'       => false,\n\t\t)\n\t);\n\treturn false;\n}\n\n/**\n * Update db version to [version].\n *\n * @since 7.8.5\n *\n * @return false.\n */\nfunction update_db_version() {\n\tglobal $wpdb;\n\n\t$found_pwc_meta = $wpdb->get_results(\n\t\t\"SELECT *\n\t\t FROM {$wpdb->usermeta}\n\t\t WHERE meta_key = 'password_confirm'\"\n\t);\n\n\tif ( $found_pwc_meta ) {\n\t\t// Don't update the db version yet.\n\t\treturn;\n\t}\n\n\t\\LLMS_Install::update_db_version( _get_db_version() );\n\treturn false;\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-900.php",
    "content": "<?php\n/**\n * Update functions for version [version]\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 9.0.0\n */\n\nnamespace LLMS\\Updates\\Version_9_0_0;\n\ndefined( 'ABSPATH' ) || exit;\n\nfunction _get_db_version() {\n\treturn '9.0.0';\n}\n\n/**\n * Shows an admin notice.\n *\n * @since 9.0.0\n *\n * @return boolean\n */\nfunction show_notice() {\n\n\t$notice_id = sprintf( 'v%s-msg', str_replace( array( '.', '-' ), '', _get_db_version() ) );\n\n\t$html = sprintf(\n\t\t'<strong>%1$s</strong><br><br>%2$s<br><br>%3$s',\n\t\t__( 'New Features Available', 'lifterlms' ),\n\t\t// Translators: %1$s = Opening anchor tag to the security settings tab; %2$s = Closing anchor tag.\n\t\tsprintf(\n\t\t\t__( 'We\\'ve added spam and security features to protect your website inside the core plugin. You can review the available features on the new %1$sSecurity settings tab%2$s.', 'lifterlms' ),\n\t\t\t'<a href=\"' . admin_url( 'admin.php?page=llms-settings&tab=security' ) . '\">',\n\t\t\t'</a>'\n\t\t),\n\t\tsprintf(\n\t\t\t// Translators: %1$s = Opening anchor tag to the blog post on lifterlms.com; %2$s = Closing anchor tag.\n\t\t\t__( '%1$sRead More%2$s', 'lifterlms' ),\n\t\t\t'<a class=\"button\" href=\"https://lifterlms.com/blog/new-website-spam-and-security-features/?utm_source=notice&utm_medium=product&utm_campaign=lifterlmsplugin&utm_content=900-notice\" target=\"_blank\" rel=\"noopener\">',\n\t\t\t'</a>'\n\t\t)\n\t);\n\n\t\\LLMS_Admin_Notices::add_notice(\n\t\t$notice_id,\n\t\t$html,\n\t\tarray(\n\t\t\t'type'             => 'info',\n\t\t\t'dismiss_for_days' => 0,\n\t\t\t'remindable'       => false,\n\t\t)\n\t);\n\treturn false;\n}\n\n/**\n * Update db version.\n *\n * @since 9.0.0\n *\n * @return false.\n */\nfunction update_db_version() {\n\t\\LLMS_Install::update_db_version( _get_db_version() );\n\treturn false;\n}\n"
  },
  {
    "path": "includes/functions/updates/llms-functions-updates-921.php",
    "content": "<?php\n/**\n * Update functions for version 9.2.1\n *\n * @package LifterLMS/Functions/Updates\n *\n * @since 9.2.1\n */\n\nnamespace LLMS\\Updates\\Version_9_2_1;\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieves the DB version of the migration.\n *\n * @since 9.2.1\n *\n * @return string\n */\nfunction _get_db_version() {\n\treturn '9.2.1';\n}\n\n/**\n * Clear stale course data locks and re-schedule course data processing.\n *\n * Finds all `_llms_temp_calc_data_lock` postmeta entries, deletes them, and\n * triggers `llms_course_calculate_data` for each affected course.\n *\n * Returns `true` if there may be more records to process so the background\n * updater can call this again, otherwise `false` when done.\n *\n * @since 9.2.1\n *\n * @return bool\n */\nfunction reset_course_calc_data_locks() {\n\n\tglobal $wpdb;\n\n\t$per_page = \\llms_update_util_get_items_per_page();\n\n\t// Find a page of locked courses.\n\t$course_ids = $wpdb->get_col(\n\t\t$wpdb->prepare(\n\t\t\t\"\n\t\t\tSELECT DISTINCT pm.post_id\n\t\t\tFROM {$wpdb->postmeta} AS pm\n\t\t\tINNER JOIN {$wpdb->posts} AS p\n\t\t\t\tON p.ID = pm.post_id\n\t\t\tWHERE pm.meta_key = %s\n\t\t\t  AND p.post_type = %s\n\t\t\tLIMIT %d\n\t\t\t\",\n\t\t\t'_llms_temp_calc_data_lock',\n\t\t\t'course',\n\t\t\t$per_page\n\t\t)\n\t);// db call ok; no-cache ok.\n\n\tif ( empty( $course_ids ) ) {\n\t\treturn false;\n\t}\n\n\tforeach ( $course_ids as $course_id ) {\n\n\t\t$course_id = (int) $course_id;\n\n\t\t// Drop the temp lock meta.\n\t\t\\delete_post_meta( $course_id, '_llms_temp_calc_data_lock' );\n\n\t\t// Kick off a fresh course data calculation round.\n\t\t// LLMS_Processor_Course_Data::schedule_calculation() is already\n\t\t// wired to this action and internally avoids duplicate scheduling\n\t\t// via wp_next_scheduled().\n\t\t\\do_action( 'llms_course_calculate_data', $course_id );\n\t}\n\n\t// If there were $per_page results, assume there might be more.\n\treturn ( count( $course_ids ) === $per_page );\n}\n\n/**\n * Update db version to 9.2.1.\n *\n * @since 9.2.1\n *\n * @return false\n */\nfunction update_db_version() {\n\t\\LLMS_Install::update_db_version( _get_db_version() );\n\treturn false;\n}\n"
  },
  {
    "path": "includes/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/integrations/class.llms.integration.bbpress.php",
    "content": "<?php\n/**\n * bbPress Integration\n *\n * @package LifterLMS/Integrations/Classes\n *\n * @since 3.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * bbPress Integration\n *\n * @since 3.0.0\n * @since 3.30.3 Fixed spelling errors.\n * @since 3.35.0 Sanitize input data.\n * @since 3.37.11 Don't update saved forum values during course quick edits.\n * @since 3.38.1 When looking for forum course restrictions make sure to run a more generic query\n *               so that it matches forum ids whether they've been save as integers or strings.\n * @since 4.0.0 Added MySQL 8.0 compatibility.\n */\nclass LLMS_Integration_BBPress extends LLMS_Abstract_Integration {\n\n\t/**\n\t * Integration ID\n\t *\n\t * @var string\n\t */\n\tpublic $id = 'bbpress';\n\n\t/**\n\t * Display order on Integrations tab\n\t *\n\t * @var integer\n\t */\n\tprotected $priority = 5;\n\n\t/**\n\t * Configure the integration\n\t *\n\t * @since 3.8.0\n\t * @since 3.30.3 Fixed spelling errors.\n\t *\n\t * @return void\n\t */\n\tprotected function configure() {\n\n\t\tadd_action( 'init', array( $this, 'set_title_and_description' ) );\n\n\t\tif ( $this->is_available() ) {\n\n\t\t\t// Custom engagements.\n\t\t\tadd_filter( 'lifterlms_engagement_triggers', array( $this, 'register_engagement_triggers' ) );\n\n\t\t\tadd_action( 'bbp_new_topic', array( llms()->engagements(), 'maybe_trigger_engagement' ), 10, 4 );\n\t\t\tadd_action( 'bbp_new_reply', array( llms()->engagements(), 'maybe_trigger_engagement' ), 10, 5 );\n\n\t\t\tadd_filter( 'lifterlms_external_engagement_query_arguments', array( $this, 'engagement_query_args' ), 10, 3 );\n\n\t\t\t// Register shortcode.\n\t\t\tadd_filter( 'llms_load_shortcodes', array( $this, 'register_shortcodes' ) );\n\n\t\t\t// Add memberships restriction metabox.\n\t\t\tadd_filter( 'llms_membership_restricted_post_types', array( $this, 'add_membership_restrictions' ) );\n\n\t\t\t// Check forum/bbp template restrictions.\n\t\t\tadd_filter( 'llms_page_restricted_before_check_access', array( $this, 'restriction_checks_memberships' ), 40, 1 );\n\t\t\tadd_filter( 'llms_page_restricted_before_check_access', array( $this, 'restriction_checks_courses' ), 50, 1 );\n\n\t\t\t// Add and save custom fields.\n\t\t\tadd_filter( 'llms_metabox_fields_lifterlms_course_options', array( $this, 'course_settings_fields' ) );\n\t\t\tadd_action( 'llms_metabox_after_save_lifterlms-course-options', array( $this, 'save_course_settings' ) );\n\t\t\tadd_filter( 'llms_get_course_properties', array( $this, 'add_course_props' ), 10, 2 );\n\n\t\t\tadd_action( 'llms_content_restricted_by_bbp_course_forum', array( $this, 'handle_course_forum_restriction' ), 10, 1 );\n\n\t\t}\n\t}\n\n\tpublic function set_title_and_description() {\n\t\t$this->title = __( 'bbPress', 'lifterlms' );\n\t\t/* translators: %1$s: Open learn more link tag, %2$s: Closing tag. */\n\t\t$this->description = sprintf( __( 'Restrict forums and topics to memberships, add forums to courses, and %1$smore%2$s.', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/lifterlms-and-bbpress/\" target=\"_blank\">', '</a>' );\n\t}\n\n\t/**\n\t * Register the custom course property with the LLMS_Course Model\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param array       $props  Default properties.\n\t * @param LLMS_Course $course Course object.\n\t * @return array\n\t */\n\tpublic function add_course_props( $props, $course ) {\n\t\t$props['bbp_forum_ids'] = 'array';\n\t\treturn $props;\n\t}\n\n\t/**\n\t * Add the membership restrictions metabox to bbPress forums on admin panel\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string[] $post_types Array of existing post types.\n\t * @return string[]\n\t */\n\tpublic function add_membership_restrictions( $post_types ) {\n\t\t$post_types[] = bbp_get_forum_post_type();\n\t\treturn $post_types;\n\t}\n\n\t/**\n\t * Register custom bbPress tab with the LLMS Course metabox\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param array $fields Existing fields.\n\t * @return array\n\t */\n\tpublic function course_settings_fields( $fields ) {\n\n\t\tglobal $post;\n\n\t\t$selected = $this->get_course_forum_ids( $post );\n\n\t\t$fields[] = array(\n\t\t\t'title'  => __( 'bbPress', 'lifterlms' ),\n\t\t\t'fields' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'allow_null'      => false,\n\t\t\t\t\t'data_attributes' => array(\n\t\t\t\t\t\t'post-type'   => 'forum',\n\t\t\t\t\t\t'allow-clear' => true,\n\t\t\t\t\t\t'placeholder' => __( 'Select forums', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t\t'desc'            => __( 'Add forums which will only be available to students currently enrolled in this course.', 'lifterlms' ),\n\t\t\t\t\t'class'           => 'llms-select2-post',\n\t\t\t\t\t'id'              => '_llms_bbp_forum_ids',\n\t\t\t\t\t'type'            => 'select',\n\t\t\t\t\t'label'           => __( 'Private Course Forums', 'lifterlms' ),\n\t\t\t\t\t'multi'           => true,\n\t\t\t\t\t'value'           => llms_make_select2_post_array( $selected ),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\treturn $fields;\n\t}\n\n\t/**\n\t * Parse action arguments for bbPress engagements and pass them back to the LLMS Engagements handler\n\t *\n\t * @since 3.12.0\n\t * @since 3.37.11 Use strict comparison for `in_array()`.\n\t *\n\t * @param array  $query_args Query args for handler.\n\t * @param string $action     Triggering action name.\n\t * @param array  $orig_args  Original arguments from the action (indexed array).\n\t * @return array\n\t */\n\tpublic function engagement_query_args( $query_args, $action, $orig_args ) {\n\n\t\tif ( in_array( $action, array( 'bbp_new_reply', 'bbp_new_topic' ), true ) ) {\n\n\t\t\t$query_args['trigger_type']    = $action;\n\t\t\t$query_args['related_post_id'] = '';\n\n\t\t\tif ( 'bbp_new_reply' === $action ) {\n\n\t\t\t\t$query_args['user_id'] = $orig_args[4]; // Reply Author.\n\n\t\t\t} elseif ( 'bbp_new_topic' === $action ) {\n\n\t\t\t\t$query_args['user_id'] = $orig_args[3]; // Topic Author.\n\n\t\t\t}\n\t\t}\n\n\t\treturn $query_args;\n\t}\n\n\t/**\n\t * Handle course forum restrictions\n\t *\n\t * Add a notice and redirect to the course\n\t *\n\t * @since 3.12.0\n\t * @since 3.13.0 Unknown.\n\t * @since 3.37.11 Use `llms_redirect_and_exit()` in favor of `wp_redirect()`.\n\t *\n\t * @param array $restriction Restriction Results from `llms_page_restricted()`.\n\t * @return void\n\t */\n\tpublic function handle_course_forum_restriction( $restriction ) {\n\n\t\t/**\n\t\t * Customize the restriction notice message displayed when a forum is restricted to a course.\n\t\t *\n\t\t * @since 3.37.11\n\t\t *\n\t\t * @param string $msg         Default message.\n\t\t * @param array  $restriction Results from `llms_page_restricted()`.\n\t\t */\n\t\t$msg = apply_filters( 'llms_bbp_course_forum_restriction_msg', __( 'You must be enrolled in this course to access the course forum.', 'lifterlms' ), $restriction );\n\n\t\tllms_add_notice( $msg, 'error' );\n\t\tllms_redirect_and_exit( get_permalink( $restriction['restriction_id'] ) );\n\t}\n\n\t/**\n\t * Retrieve course ids restricted to a LifterLMS course\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param mixed $course WP_Post, LLMS_Course, or WP_Post ID.\n\t * @return array\n\t */\n\tpublic function get_course_forum_ids( $course ) {\n\n\t\t$course = llms_get_post( $course );\n\t\tif ( ! $course ) {\n\t\t\t$ids = array();\n\t\t} else {\n\t\t\t$ids = $course->get( 'bbp_forum_ids' );\n\t\t\tif ( '' === $ids ) {\n\t\t\t\t$ids = array();\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Customize the bbPress forum IDs associated with a course.\n\t\t *\n\t\t * @since 3.37.11\n\t\t *\n\t\t * @param int[]       $ids    Array of WP_Post IDs of the bbPress forums restricted to the course.\n\t\t * @param LLMS_Course $course LifterLMS course object.\n\t\t */\n\t\treturn apply_filters( 'llms_bbp_get_course_forum_ids', $ids, $course );\n\t}\n\n\t/**\n\t * Check if a forum is restricted to a course(s)\n\t *\n\t * @since 3.12.0\n\t * @since 3.38.1 Make the query more generic so that it matches forum ids whether they've been saved as integers or strings.\n\t * @since 4.0.0 Escape `{` character in SQL query to add MySQL 8.0 support.\n\t *\n\t * @param int $forum_id WP_Post ID of the forum.\n\t * @return int[]\n\t */\n\tpublic function get_forum_course_restrictions( $forum_id ) {\n\n\t\tglobal $wpdb;\n\t\t$query = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT metas.post_id\n\t\t\t FROM {$wpdb->postmeta} AS metas\n\t\t\t JOIN {$wpdb->posts} AS posts on posts.ID = metas.post_id\n\t\t\t WHERE metas.meta_key = '_llms_bbp_forum_ids'\n\t\t\t   AND metas.meta_value REGEXP %s\n\t\t\t   AND posts.post_status = 'publish';\",\n\t\t\t\t'a:[0-9][0-9]*:\\{(i:[0-9][0-9]*;(i|s:[0-9][0-9]*):\"?[0-9][0-9]*\"?;)*(i:[0-9][0-9]*;(i|s:[0-9][0-9]*):\"?' . sprintf( '%d', absint( $forum_id ) ) . '\"?;)'\n\t\t\t)\n\t\t);\n\n\t\t$query = array_map( 'absint', $query );\n\n\t\treturn $query;\n\t}\n\n\t/**\n\t * Determine if bbPress is installed and activated\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_installed() {\n\t\treturn class_exists( 'bbPress' );\n\t}\n\n\t/**\n\t * Register shortcodes via LifterLMS core registration methods\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param string[] $classes Existing shortcode classes.\n\t * @return array\n\t */\n\tpublic function register_shortcodes( $classes ) {\n\t\t$classes[] = 'LLMS_BBP_Shortcode_Course_Forums_List';\n\t\treturn $classes;\n\t}\n\n\t/**\n\t * Check forum restrictions for course restrictions\n\t *\n\t * @since 3.12.0\n\t * @since 3.12.2 Unknown.\n\t *\n\t * @param array $results Array of restriction results.\n\t * @return array\n\t */\n\tpublic function restriction_checks_courses( $results ) {\n\n\t\t$post_id = null;\n\n\t\tif ( bbp_is_forum( $results['content_id'] ) ) {\n\n\t\t\t$user_id = get_current_user_id();\n\t\t\t$courses = $this->get_forum_course_restrictions( $results['content_id'] );\n\n\t\t\t// No user and at least one course restriction, return the first.\n\t\t\tif ( $courses && ! $user_id ) {\n\n\t\t\t\t$post_id = $courses[0];\n\n\t\t\t\t// Courses and a user, find at least one enrollment.\n\t\t\t} elseif ( $courses && $user_id ) {\n\n\t\t\t\tforeach ( $courses as $course_id ) {\n\t\t\t\t\t// Not enrolled, use this for the restriction but dont break because we may find an enrollment later.\n\t\t\t\t\tif ( ! llms_is_user_enrolled( $user_id, $course_id ) ) {\n\t\t\t\t\t\t$post_id = $course_id;\n\t\t\t\t\t\t// Enrolled in one, reset the post id and break.\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$post_id = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} elseif ( bbp_is_topic( $results['content_id'] ) ) {\n\n\t\t\t$results['content_id'] = bbp_get_topic_forum_id( $results['content_id'] );\n\t\t\treturn $this->restriction_checks_courses( $results );\n\n\t\t}\n\n\t\tif ( $post_id ) {\n\n\t\t\t$results['restriction_id'] = $post_id;\n\t\t\t$results['reason']         = 'bbp_course_forum';\n\n\t\t}\n\n\t\treturn $results;\n\t}\n\n\t/**\n\t * Check membership restrictions for Topics and Forum Archive pages\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param array $results Array of restriction results.\n\t * @return array\n\t */\n\tpublic function restriction_checks_memberships( $results ) {\n\n\t\t$post_id = null;\n\n\t\t// Forum archive, grab the page (if set).\n\t\tif ( bbp_is_forum_archive() ) {\n\n\t\t\t$page    = bbp_get_page_by_path( bbp_get_root_slug() );\n\t\t\t$post_id = ( $page && $page->ID ) ? $page->ID : null;\n\t\t\t$reason  = 'membership';\n\n\t\t} elseif ( bbp_is_topic( $results['content_id'] ) ) {\n\n\t\t\t$post_id = bbp_get_topic_forum_id( $results['content_id'] );\n\t\t\t$reason  = 'membership';\n\n\t\t}\n\n\t\tif ( $post_id ) {\n\n\t\t\t$restriction_id = llms_is_post_restricted_by_membership( $post_id, get_current_user_id() );\n\n\t\t\tif ( $restriction_id ) {\n\n\t\t\t\t$results['restriction_id'] = $restriction_id;\n\t\t\t\t$results['reason']         = 'membership';\n\n\t\t\t}\n\t\t}\n\n\t\treturn $results;\n\t}\n\n\t/**\n\t * Register engagement triggers\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param string[] $triggers Existing triggers.\n\t * @return array\n\t */\n\tpublic function register_engagement_triggers( $triggers ) {\n\t\t$triggers['bbp_new_topic'] = __( 'Student creates a new forum topic', 'lifterlms' );\n\t\t$triggers['bbp_new_reply'] = __( 'Student creates a new forum reply', 'lifterlms' );\n\t\treturn $triggers;\n\t}\n\n\t/**\n\t * Save course metabox custom fields\n\t *\n\t * @since 3.12.0\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 3.37.11 Don't update saved forum values during course quick edits & remove redundant sanitization.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @param int $post_id WP_Post ID of the course.\n\t * @return null|int[]\n\t */\n\tpublic function save_course_settings( $post_id ) {\n\n\t\t// Return early on quick edits.\n\t\t$action = llms_filter_input( INPUT_POST, 'action' );\n\t\tif ( 'inline-save' === $action ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t$ids = array();\n\n\t\tif ( isset( $_POST['_llms_bbp_forum_ids'] ) ) {  // phpcs:ignore WordPress.Security.NonceVerification.Missing\n\n\t\t\t$ids = llms_filter_input( INPUT_POST, '_llms_bbp_forum_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );\n\t\t}\n\n\t\tupdate_post_meta( $post_id, '_llms_bbp_forum_ids', $ids );\n\n\t\treturn $ids;\n\t}\n}\n"
  },
  {
    "path": "includes/integrations/class.llms.integration.buddypress.php",
    "content": "<?php\n/**\n * BuddyPress Integration\n *\n * @package LifterLMS/Integrations/Classes\n *\n * @since 1.0.0\n * @version 6.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * BuddyPress Integration.\n *\n * @since 1.0.0\n * @since 3.37.17 Fixed `courses` pagination.\n */\nclass LLMS_Integration_Buddypress extends LLMS_Abstract_Integration {\n\n\tpublic $id = 'buddypress';\n\n\t/**\n\t * Display order on Integrations tab.\n\t *\n\t * @var integer\n\t */\n\tprotected $priority = 5;\n\n\t/**\n\t * Current endpoint's key being processed.\n\t *\n\t * @var string\n\t */\n\tprivate $current_endpoint_key;\n\n\t/**\n\t * Profile endpoints.\n\t *\n\t * @var array[]\n\t */\n\tprivate $endpoints;\n\n\t/**\n\t * Options data abstract version.\n\t *\n\t * This is used to determine the behavior of the `get_option()` method.\n\t *\n\t * Concrete classes should use version 2 in order to use the new (future default)\n\t * behavior of the method.\n\t *\n\t * @var int\n\t */\n\tprotected $version = 2;\n\n\t/**\n\t * Configure the integration.\n\t *\n\t * Do things like configure ID and title here.\n\t *\n\t * @since 3.12.0\n\t *\n\t * @return void\n\t */\n\tprotected function configure() {\n\n\t\tadd_action( 'init', array( $this, 'set_title_and_description' ) );\n\n\t\tif ( $this->is_available() ) {\n\n\t\t\tadd_action( 'bp_setup_nav', array( $this, 'add_profile_nav_items' ) );\n\t\t\tadd_filter( 'llms_page_restricted_before_check_access', array( $this, 'restriction_checks' ), 40, 1 );\n\t\t\tadd_filter( 'lifterlms_update_account_redirect', array( $this, 'maybe_alter_update_account_redirect' ) );\n\n\t\t\t// Groups Add-on integration.\n\t\t\tadd_filter( 'llms_groups_enqueue_dashboard_style', array( $this, 'return_true_on_bp_my_profile' ) );\n\t\t\tadd_filter( 'llms_groups_maybe_hide_dashboard_tab', array( $this, 'return_true_on_bp_my_profile' ) );\n\n\t\t}\n\t}\n\n\tpublic function set_title_and_description() {\n\t\t$this->title = __( 'BuddyPress', 'lifterlms' );\n\t\t/* translators: %1$s: Open learn more link tag, %2$s: Closing tag. */\n\t\t$this->description = sprintf( __( 'Add LifterLMS information to user profiles and enable membership restrictions for activity, group, and member directories. %1$sLearn More%2$s.', 'lifterlms' ), '<a href=\"https://lifterlms.com/docs/lifterlms-and-buddypress/\" target=\"_blank\">', '</a>' );\n\t}\n\n\t/**\n\t * Retrieve integration settings.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_integration_settings() {\n\n\t\t$settings = array();\n\n\t\tif ( $this->is_available() ) {\n\n\t\t\t$display_eps = $this->get_profile_endpoints_options();\n\n\t\t\t$settings[] = array(\n\t\t\t\t'class'   => 'llms-select2',\n\t\t\t\t'desc'    => '<br>' . __( 'The following LifterLMS Student Dashboard areas will be added to the BuddyPress user profiles', 'lifterlms' ),\n\t\t\t\t'default' => array_keys( $display_eps ),\n\t\t\t\t'id'      => $this->get_option_name( 'profile_endpoints' ),\n\t\t\t\t'options' => $display_eps,\n\t\t\t\t'type'    => 'multiselect',\n\t\t\t\t'title'   => __( 'User Profile Endpoints', 'lifterlms' ),\n\t\t\t);\n\n\t\t}\n\n\t\treturn $settings;\n\t}\n\n\t/**\n\t * Add LLMS navigation items to the BuddyPress User Profile.\n\t *\n\t * @since 1.0.0\n\t * @since 6.3.0 Display all registered dashboard tabs (enabled in the settings) automatically.\n\t *              Use `bp_loggedin_user_domain()` to determine the current user domain\n\t *              to be used in the profile nav item's links, in favor of relying on the global `$bp`.\n\t * @since 6.8.0 Revert adding nav items only on bp my profile. @link https://github.com/gocodebox/lifterlms/issues/2142.\n\t *\n\t * @return void\n\t */\n\tpublic function add_profile_nav_items() {\n\n\t\t$profile_endpoints = $this->get_profile_endpoints();\n\n\t\tif ( empty( $profile_endpoints ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$bp_is_my_profile = bp_is_my_profile();\n\t\t$user_domain      = bp_loggedin_user_domain();\n\t\t$first_endpoint   = reset( $profile_endpoints );\n\t\t/**\n\t\t * Filters the LifterLMS main nav item slug in the BuddyPress  profile menu.\n\t\t *\n\t\t * @since 6.3.0\n\t\t *\n\t\t * @param string $slug The LifterLMS main nav item slug in the BuddyPress profile menu.\n\t\t */\n\t\t$main_nav_slug = apply_filters( 'llms_buddypress_main_nav_item_slug', _x( 'courses', 'BuddyPress profile main nav item slug', 'lifterlms' ) );\n\t\t$parent_url    = $user_domain . $main_nav_slug . '/';\n\n\t\t// Add the main nav menu.\n\t\tbp_core_new_nav_item(\n\t\t\tarray(\n\t\t\t\t/**\n\t\t\t\t * Filters the LifterLMS main nav item label in the BuddyPress profile menu.\n\t\t\t\t *\n\t\t\t\t * @since 6.3.0\n\t\t\t\t *\n\t\t\t\t * @param string $label The LifterLMS main nav item label in the BuddyPress profile menu.\n\t\t\t\t */\n\t\t\t\t'name'                    => apply_filters( 'llms_buddypress_main_nav_item_label', _x( 'Courses', 'BuddyPress profile main nav item label', 'lifterlms' ) ),\n\t\t\t\t'slug'                    => $main_nav_slug,\n\t\t\t\t/**\n\t\t\t\t * Filters the LifterLMS main nav item position in the BuddyPress profile menu.\n\t\t\t\t *\n\t\t\t\t * @since 6.3.0\n\t\t\t\t *\n\t\t\t\t * @param string $position The LifterLMS main nav item position in the BuddyPress profile menu.\n\t\t\t\t */\n\t\t\t\t'position'                => apply_filters( 'llms_buddypress_main_nav_item_position', 20 ),\n\t\t\t\t'default_subnav_slug'     => $first_endpoint['endpoint'],\n\t\t\t\t'show_for_displayed_user' => false,\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $profile_endpoints as $ep_key => $profile_endpoint ) {\n\t\t\t// Add sub nav item.\n\t\t\tbp_core_new_subnav_item(\n\t\t\t\tarray(\n\t\t\t\t\t'name'            => $profile_endpoint['title'],\n\t\t\t\t\t'slug'            => $profile_endpoint['endpoint'],\n\t\t\t\t\t'parent_slug'     => $main_nav_slug,\n\t\t\t\t\t'parent_url'      => $parent_url,\n\t\t\t\t\t'screen_function' => function () use ( $ep_key, $profile_endpoint ) {\n\t\t\t\t\t\t$this->endpoint_content( $ep_key, $profile_endpoint['content'] );\n\t\t\t\t\t},\n\t\t\t\t\t'user_has_access' => $bp_is_my_profile,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Redirect on the same bb profile page when successfully update the account.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @param string $account_update_redirect_url Account update redirect url.\n\t * @return string\n\t */\n\tpublic function maybe_alter_update_account_redirect( $account_update_redirect_url ) {\n\t\treturn bp_is_my_profile() ? bp_get_requested_url() : $account_update_redirect_url;\n\t}\n\n\t/**\n\t * Checks if the BuddyPress plugin is installed & activated\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_installed() {\n\t\treturn ( class_exists( 'BuddyPress' ) );\n\t}\n\n\t/**\n\t * Callback for \"Achievements\" profile screen.\n\t *\n\t * @since 1.0.0\n\t * @since 3.14.4 Unknown.\n\t * @deprecated 6.3.0 Deprecated with no replacement. {@see LLMS_Integration_Buddypress::endpoint_content()}.\n\t *\n\t * @return void\n\t */\n\tpublic function achievements_screen() {\n\n\t\tllms_deprecated_function( 'LLMS_Integration_Buddypress::achievements_screen()', '6.3.0' );\n\n\t\tadd_action( 'bp_template_content', 'lifterlms_template_student_dashboard_my_achievements' );\n\t\tbp_core_load_template( apply_filters( 'bp_core_template_plugin', 'members/single/plugins' ) );\n\t}\n\n\t/**\n\t * Callback for \"Certificates\" profile screen.\n\t *\n\t * @since 1.0.0\n\t * @since 3.14.4 Unknown.\n\t * @deprecated 6.3.0 Deprecated with no replacement. {@see LLMS_Integration_Buddypress::endpoint_content()}.\n\t *\n\t * @return void\n\t */\n\tpublic function certificates_screen() {\n\n\t\tllms_deprecated_function( 'LLMS_Integration_Buddypress::certificates_screen()', '6.3.0' );\n\n\t\tadd_action( 'bp_template_content', 'lifterlms_template_student_dashboard_my_certificates' );\n\t\tbp_core_load_template( apply_filters( 'bp_core_template_plugin', 'members/single/plugins' ) );\n\t}\n\n\t/**\n\t * Callback for \"Courses\" profile screen.\n\t *\n\t * @since 1.0.0\n\t * @since 3.14.4 Unknown.\n\t * @since 3.37.17 Added action and filters to fix handling pagination links mofication.\n\t * @deprecated 6.3.0 Deprecated with no replacement. {@see LLMS_Integration_Buddypress::endpoint_content()}.\n\t *\n\t * @return void\n\t */\n\tpublic function courses_screen() {\n\n\t\tllms_deprecated_function( 'LLMS_Integration_Buddypress::courses_screen()', '6.3.0' );\n\n\t\t// Prevent paginate links alteration performed in includes/functions/llms.functions.templates.dashboard.php.\n\t\tadd_filter( 'llms_modify_dashboard_pagination_links_disable', '__return_true', 999 );\n\n\t\t// Add specific paginate links filter.\n\t\tadd_filter( 'paginate_links', array( $this, 'modify_courses_paginate_links' ) );\n\n\t\tadd_action( 'bp_template_content', 'lifterlms_template_student_dashboard_my_courses' );\n\n\t\t// Remove specific paginate links filter after the template has been rendered.\n\t\tadd_action( 'bp_template_content', array( $this, 'remove_courses_paginate_links_filter' ), 15 );\n\n\t\tbp_core_load_template( apply_filters( 'bp_core_template_plugin', 'members/single/plugins' ) );\n\t}\n\n\t/**\n\t * Callback for endpoint profile content.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @param string   $ep_key         The endpoint's key being processed.\n\t * @param Callable $ep_template_cb The endpoint's template callback.\n\t * @return void\n\t */\n\tpublic function endpoint_content( $ep_key, $ep_template_cb ) {\n\n\t\t// Store what endpoint key we're processing.\n\t\t$this->current_endpoint_key = $ep_key;\n\n\t\t// Enqueue scripts.\n\t\tadd_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ), 20 );\n\n\t\t// Prevent paginate links alteration performed in includes/functions/llms.functions.templates.dashboard.php.\n\t\tadd_filter( 'llms_modify_dashboard_pagination_links_disable', '__return_true', 999 );\n\n\t\t// Add specific paginate links filter.\n\t\tadd_filter( 'paginate_links', array( $this, 'modify_paginate_links' ) );\n\n\t\tadd_action( 'bp_template_content', $ep_template_cb );\n\n\t\t// Remove specific paginate links filter after the template has been rendered.\n\t\tadd_action( 'bp_template_content', array( $this, 'remove_paginate_links_filter' ), 15 );\n\n\t\t// This triggers 'bp_template_content' action hook.\n\t\tbp_core_load_template( apply_filters( 'bp_core_template_plugin', 'members/single/plugins' ) );\n\t}\n\n\t/**\n\t * Enqueue assets specific for the profile endpoints.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function enqueue_assets() {\n\n\t\tif ( empty( $this->current_endpoint_key ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( 'view-achievements' === $this->current_endpoint_key ) {\n\t\t\t// The iziModal is needed by the achievements endpoint.\n\t\t\tllms()->assets->enqueue_style( 'llms-iziModal' );\n\t\t\tllms()->assets->enqueue_script( 'llms-iziModal' );\n\t\t}\n\n\t\tif ( 'edit-account' === $this->current_endpoint_key ) {\n\t\t\t// Needed in the account edit endpoint.\n\t\t\tllms()->assets->enqueue_style( 'llms-select2-styles' );\n\t\t\tllms()->assets->enqueue_script( 'llms-select2' );\n\t\t\twp_add_inline_script(\n\t\t\t\t'llms',\n\t\t\t\t\"window.llms.address_info = '\" . wp_json_encode( llms_get_countries_address_info() ) . \"';\"\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Remove specific paginate links filter after the template has been rendered.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function remove_paginate_links_filter() {\n\t\tremove_filter( 'paginate_links', array( $this, 'modify_paginate_links' ) );\n\t}\n\n\t/**\n\t * Remove specific paginate links filter after the template has been rendered.\n\t *\n\t * @since 3.37.17\n\t * @deprecated 6.3.0 Deprecated with no replacement. {@see LLMS_Integration_Buddypress::remove_paginate_links_filter()}.\n\t *\n\t * @return void\n\t */\n\tpublic function remove_courses_paginate_links_filter() {\n\n\t\tllms_deprecated_function( 'LLMS_Integration_Buddypress::remove_courses_paginate_links_filter()', '6.3.0' );\n\n\t\tremove_filter( 'paginate_links', array( $this, 'modify_courses_paginate_links' ) );\n\t}\n\n\t/**\n\t * Modify the pagination links displayed on the courses endpoint in the bp member profile.\n\t *\n\t * @since 3.37.17\n\t * @deprecated 6.3.0 Deprecated with no replacement. {@see LLMS_Integration_Buddypress::modify_paginate_links()}.\n\t *\n\t * @param string $link Default link.\n\t * @return string\n\t */\n\tpublic function modify_courses_paginate_links( $link ) {\n\n\t\tllms_deprecated_function( 'LLMS_Integration_Buddypress::modify_courses_paginate_links()', '6.3.0' );\n\n\t\t$this->current_endpoint_key;\n\t\treturn $this->modify_paginate_links( $link );\n\t}\n\n\t/**\n\t * Modify the pagination links for the endpoints in the bp member profile.\n\t *\n\t * This fixes the pagination not correctly working on the fist subnav.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @param string $link Default link.\n\t * @return string\n\t */\n\tpublic function modify_paginate_links( $link ) {\n\n\t\tglobal $wp_rewrite;\n\n\t\t// With ugly permalinks actually the whole BuddyPress member profile page doesn't work.\n\t\tif ( ! get_option( 'permalink_structure' ) ) {\n\t\t\treturn $link;\n\t\t}\n\n\t\t// Remove query vars if any, we'll add them back later.\n\t\t$query = wp_parse_url( $link, PHP_URL_QUERY );\n\t\tif ( $query ) {\n\t\t\t$link = str_replace( '?' . $query, '', $link );\n\t\t}\n\n\t\t// Retrieve link's page number.\n\t\t$parts = explode( '/', untrailingslashit( $link ) );\n\t\t$page  = end( $parts );\n\n\t\t// For links to page 1 let's remove it to avoid ugly URLs.\n\t\tif ( 1 === (int) $page ) {\n\t\t\t$link = str_replace(\n\t\t\t\tuser_trailingslashit( $wp_rewrite->pagination_base . '/' . $page ),\n\t\t\t\t'',\n\t\t\t\t$link\n\t\t\t);\n\t\t}\n\n\t\t$endpoints = $this->get_profile_endpoints();\n\n\t\t// If we're not the first subnav, our job is done, add back the query var and return.\n\t\tif ( key( $endpoints ) !== $this->current_endpoint_key ) {\n\t\t\treturn $query ? $link . '?' . $query : $link;\n\t\t}\n\n\t\t// Retrieve our first subnav menu item.\n\t\t$first_subnav_item = buddypress()->members->nav->get_secondary(\n\t\t\tarray(\n\t\t\t\t/** This filter is documented above */\n\t\t\t\t'parent_slug' => apply_filters( 'llms_buddypress_main_nav_item_slug', _x( 'courses', 'BuddyPress profile main nav item slug', 'lifterlms' ) ),\n\t\t\t\t'slug'        => $endpoints[ $this->current_endpoint_key ]['endpoint'],\n\t\t\t)\n\t\t);\n\n\t\tif ( is_array( $first_subnav_item ) ) {\n\t\t\t$first_subnav_item = reset( $first_subnav_item );\n\t\t} else { // Bail.\n\t\t\treturn $query ? $link . '?' . $query : $link;\n\t\t}\n\n\t\t$current_page = llms_get_paged_query_var();\n\n\t\t/**\n\t\t * Here's the core of this filter.\n\t\t *\n\t\t * What happens is that the pagination links on the first page of the fist subnav,\n\t\t * e.g. 'my-courses' endpoint (as example for the fist subnav) are of this type:\n\t\t * `example.local/members/admin/courses/page/N`,\n\t\t * where 'courses' is the slug of the main nav item.\n\t\t * While the \"working\" paginate links must be of the type:\n\t\t * `example.local/members/admin/courses/my-courses/page/N`\n\t\t * where 'courses' is the slug of the main nav item, and 'my-courses' is the slug of\n\t\t * the subnav item which is the default subnav for the main nav item.\n\t\t *\n\t\t * So what we do here is replacing the link that looks like:\n\t\t * `example.local/members/admin/courses/page/N` to something like:\n\t\t * `example.local/members/admin/courses/my-courses/page/N`\n\t\t *\n\t\t * Despite one might expect, `$first_subnav_item->link` doesn't point to `example.local/members/admin/courses/my-courses/`\n\t\t * but to `example.local/members/admin/courses/`, which is the link of the parent nav, the main nav item,\n\t\t * this because the 'my-courses' subnav item is the default of the 'courses' nav item (default_subnav_slug).\n\t\t */\n\t\tif ( 1 === $current_page ) {\n\t\t\t$link = user_trailingslashit( $first_subnav_item->link . $first_subnav_item->slug . '/' . $wp_rewrite->pagination_base . '/' . $page );\n\t\t} elseif ( 1 === (int) $page ) {\n\t\t\t/**\n\t\t\t * For links to page 1, when not on page 1, let's back on the main nav item URL, so we replace something like\n\t\t\t * `example.local/members/admin/courses/my-courses/`\n\t\t\t * to something like\n\t\t\t * `example.local/members/admin/courses/`\n\t\t\t */\n\t\t\t$link = $first_subnav_item->link;\n\t\t}\n\n\t\treturn $query ? $link . '?' . $query : $link;\n\t}\n\n\t/**\n\t * Helper that returns true when on BuddyPress My Profile.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @param mixed $arg Argument to return when not on BuddyPress My Profile.\n\t * @return mixed\n\t */\n\tpublic function return_true_on_bp_my_profile( $arg = null ) {\n\t\treturn bp_is_my_profile() ? true : $arg;\n\t}\n\n\t/**\n\t * Callback for \"memberships\" profile screen.\n\t *\n\t * @since 1.0.0\n\t * @since 3.14.4 Unknown.\n\t * @deprecated 6.3.0 Deprecated with no replacement. {@see LLMS_Integration_Buddypress::endpoint_content()}.\n\t *\n\t * @return void\n\t */\n\tpublic function memberships_screen() {\n\n\t\tllms_deprecated_function( 'LLMS_Integration_Buddypress::memberships_screen()', '6.3.0' );\n\n\t\tadd_action( 'bp_template_content', 'lifterlms_template_student_dashboard_my_memberships' );\n\t\tbp_core_load_template( apply_filters( 'bp_core_template_plugin', 'members/single/plugins' ) );\n\t}\n\n\t/**\n\t * Allows restricting of BP Directory Pages for Activity and Members via LifterLMS membership restrictions.\n\t *\n\t * @since 3.12.0\n\t *\n\t * @param array $results Array of restriction results.\n\t * @return array\n\t */\n\tpublic function restriction_checks( $results ) {\n\n\t\t// Only check directories.\n\t\tif ( ! bp_is_directory() ) {\n\t\t\treturn $results;\n\t\t}\n\n\t\t$post_id = null;\n\n\t\t// Activity.\n\t\tif ( bp_is_activity_component() ) {\n\n\t\t\t$post_id = bp_core_get_directory_page_id( 'activity' );\n\n\t\t} elseif ( bp_is_members_component() ) {\n\n\t\t\t$post_id = bp_core_get_directory_page_id( 'members' );\n\n\t\t} elseif ( bp_is_groups_component() ) {\n\n\t\t\t$post_id = bp_core_get_directory_page_id( 'groups' );\n\n\t\t}\n\n\t\tif ( $post_id ) {\n\n\t\t\t$restriction_id = llms_is_post_restricted_by_membership( $post_id, get_current_user_id() );\n\n\t\t\tif ( $restriction_id ) {\n\n\t\t\t\t$results['content_id']     = $post_id;\n\t\t\t\t$results['restriction_id'] = $restriction_id;\n\t\t\t\t$results['reason']         = 'membership';\n\n\t\t\t}\n\t\t}\n\n\t\treturn $results;\n\t}\n\n\t/**\n\t * Get profile endpoints options.\n\t *\n\t * Used to populate the settings' select.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function get_profile_endpoints_options() {\n\n\t\t$endpoints = $this->get_profile_endpoints( false );\n\n\t\treturn array_combine(\n\t\t\tarray_keys( $endpoints ),\n\t\t\tarray_column( $endpoints, 'title' )\n\t\t);\n\t}\n\n\t/**\n\t * Populate list of endpoints from LifterLMS Dashboard Settings.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function populate_profile_endpoints() {\n\n\t\t$exclude_llms_eps = array( 'dashboard', 'signout' );\n\t\t$exclude_fields   = array( 'nav_item', 'url', 'paginate' );\n\t\t$endpoints        = array();\n\n\t\tforeach ( LLMS_Student_Dashboard::get_tabs() as $ep_key => $endpoint ) {\n\t\t\tif ( ! in_array( $ep_key, $exclude_llms_eps, true ) ) {\n\t\t\t\t$endpoints[ $ep_key ] = array_diff_key( $endpoint, array_flip( $exclude_fields ) );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter profile endpoints.\n\t\t *\n\t\t * Modify the LifterLMS dashboard endpoints which can be added to the BuddyPress profile page as custom tabs.\n\t\t *\n\t\t * @since 6.3.0\n\t\t *\n\t\t * @param array $endpoints Array of endpoint data.\n\t\t */\n\t\t$this->endpoints = apply_filters( 'llms_buddypress_profile_endpoints', $endpoints );\n\t}\n\n\t/**\n\t * Get a list of custom endpoints to add to BuddyPress profile page.\n\t *\n\t * @since 6.3.0\n\t * @since 6.8.0 Remove redundant check on `is_null()`: `isset()` already implies it.\n\t *\n\t * @param bool $active_only If true, returns only active endpoints.\n\t * @return array\n\t */\n\tpublic function get_profile_endpoints( $active_only = true ) {\n\n\t\tif ( ! isset( $this->endpoints ) ) {\n\t\t\t$this->populate_profile_endpoints();\n\t\t}\n\n\t\t// Remove endpoints that don't have an 'endpoint' value.\n\t\t$endpoints = array_filter(\n\t\t\t$this->endpoints,\n\t\t\tfunction ( $endpoint ) {\n\t\t\t\treturn ! empty( $endpoint['endpoint'] );\n\t\t\t}\n\t\t);\n\n\t\tif ( $active_only ) {\n\n\t\t\t// If no endpoints are saved an empty string is returned.\n\t\t\t$active = $this->get_option( 'profile_endpoints', array_keys( $endpoints ) );\n\n\t\t\t// Filter active endpoints only.\n\t\t\t$endpoints = '' === $active\n\t\t\t\t?\n\t\t\t\tarray()\n\t\t\t\t:\n\t\t\t\tarray_intersect_key(\n\t\t\t\t\t$endpoints,\n\t\t\t\t\tarray_flip( $active )\n\t\t\t\t);\n\n\t\t}\n\n\t\treturn $endpoints;\n\t}\n}\n"
  },
  {
    "path": "includes/integrations/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/interfaces/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/interfaces/interface.llms.notification.manager.php",
    "content": "<?php\n/**\n * LifterLMS Notification Interface\n *\n * @package LifterLMS/Interfaces\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Interface_Notification_Manager\n *\n * @since Unknown\n */\ninterface LLMS_Interface_Notification_Manager {\n\n\t/**\n\t * Characters added before merge codes\n\t *\n\t * @var string\n\t */\n\tconst MERGE_CODE_PREFIX = '{{';\n\n\t/**\n\t * Characters added after merge codes\n\t *\n\t * @var string\n\t */\n\tconst MERGE_CODE_SUFFIX = '}}';\n\n\t/**\n\t * Callback function for notifications\n\t *\n\t * Depending on the action that triggers this callback there will be a variable number of parameters\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function callback();\n\n}\n"
  },
  {
    "path": "includes/interfaces/llms.interface.notification.controller.php",
    "content": "<?php\n/**\n * LifterLMS Notification Controller Interface\n *\n * @package LifterLMS/Interfaces\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Interface_Notification_Controller\n *\n * @since Unknown\n */\ninterface LLMS_Interface_Notification_Controller {\n\n\t/**\n\t * Callback function for sending notifications\n\t *\n\t * Depending on the action that triggers this callback there will be a variable number of parameters\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function action_callback();\n\n}\n"
  },
  {
    "path": "includes/interfaces/llms.interface.post.instructors.php",
    "content": "<?php\n/**\n * LifterLMS Post Instructors Interface\n *\n * @package LifterLMS/Interfaces\n *\n * @since 3.13.0\n * @version 3.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Interface_Post_Instructors interface\n *\n * @since 3.13.0\n */\ninterface LLMS_Interface_Post_Instructors {\n\n\t/**\n\t * Retrieve an instance of the Post Instructors model\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return obj\n\t */\n\tpublic function instructors();\n\n\t/**\n\t * Retrieve course instructor information\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param boolean $exclude_hidden If true, excludes hidden instructors from the return array.\n\t * @return array\n\t */\n\tpublic function get_instructors( $exclude_hidden = false );\n\n\t/**\n\t * Save instructor information\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param array $instructors Array of course instructor information.\n\t * @return array\n\t */\n\tpublic function set_instructors( $instructors = array() );\n\n}\n"
  },
  {
    "path": "includes/llms-notifications.php",
    "content": "<?php\ndefined( 'ABSPATH' ) || exit;\n\nrequire_once LLMS_PLUGIN_DIR . '/libraries/banner-notifications/banner-notifications.php';\n\n$GLOBALS['lifterlms_banner_notifications'] = new Gocodebox_Banner_Notifier(\n\tarray(\n\t\t'prefix'            => 'lifterlms',\n\t\t'version'           => llms()->version,\n\t\t'notifications_url' => 'https://notifications.lifterlms.com/v1/notifications.json',\n\t)\n);\n\nfunction llms_maybe_hide_notifications( $priority ) {\n\tif ( ! is_admin() ) {\n\t\treturn 0;\n\t}\n\n\t$current_screen = get_current_screen();\n\n\tif ( ! isset( $current_screen->post_type ) ) {\n\t\treturn $priority;\n\t}\n\n\t// Check if we're on the main WP admin dashboard.\n\tif ( 'dashboard' === $current_screen->id ) {\n\t\treturn $priority;\n\t}\n\n\tif ( llms_is_block_editor() ) {\n\t\treturn 0;\n\t}\n\n\tif (\n\t\tstrpos( $current_screen->post_type, 'llms_' ) !== 0 &&\n\t\tstrpos( $current_screen->base, 'lifterlms' ) !== 0 &&\n\t\t! in_array( $current_screen->post_type, array( 'course', 'lesson' ), true )\n\t) {\n\t\treturn 0;\n\t}\n\n\treturn $priority;\n}\n\nadd_filter( 'lifterlms_max_notification_priority', 'llms_maybe_hide_notifications', 10, 1 );\n"
  },
  {
    "path": "includes/llms.functions.core.php",
    "content": "<?php\n/**\n * Core LifterLMS functions file\n *\n * @package LifterLMS/Functions\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nrequire_once 'functions/llms-functions-l10n.php';\n\nrequire_once 'functions/llms-functions-access-plans.php';\nrequire_once 'functions/llms-functions-deprecated.php';\nrequire_once 'functions/llms-functions-forms.php';\nrequire_once 'functions/llms-functions-locale.php';\nrequire_once 'functions/llms-functions-options.php';\nrequire_once 'functions/llms-functions-progression.php';\nrequire_once 'functions/llms-functions-user-information-fields.php';\nrequire_once 'functions/llms-functions-wrappers.php';\n\nrequire_once 'functions/llms.functions.access.php';\nrequire_once 'functions/llms.functions.certificate.php';\nrequire_once 'functions/llms.functions.course.php';\nrequire_once 'functions/llms.functions.currency.php';\nrequire_once 'functions/llms.functions.log.php';\nrequire_once 'functions/llms.functions.notice.php';\nrequire_once 'functions/llms.functions.order.php';\nrequire_once 'functions/llms.functions.page.php';\nrequire_once 'functions/llms.functions.person.php';\nrequire_once 'functions/llms.functions.privacy.php';\nrequire_once 'functions/llms.functions.quiz.php';\nrequire_once 'functions/llms.functions.template.php';\nrequire_once 'functions/llms.functions.user.postmeta.php';\nrequire_once 'functions/llms.functions.favorite.php';\n\nif ( ! function_exists( 'llms_anonymize_string' ) ) {\n\t/**\n\t * Anonymize a string.\n\t *\n\t * Masks the characters in a string with the specified character leaving a small number\n\t * of characters visible. For example `llms_anonymize_string( 'MY_SECRET_STRING' ) will return\n\t * 'MY************NG'.\n\t *\n\t * The number of retained original characters is dependent on the string's length:\n\t *\n\t * | Length        | At start | At end | Example      |\n\t * | ------------- | -------- | ------ | ------------ |\n\t * | 1             | 0        | 0      | *            |\n\t * | >= 2 && <= 6  | 0        | 1      | *****A       |\n\t * | >= 7 && <= 10 | 0        | 2      | ********AA   |\n\t * | >= 11         | 2        | 2      | AA*******AA  |\n\t *\n\t * Any string that validates as an email address using `is_email()` will be split at the `@` symbol\n\t * and each part of the email address will be anonymized separately, for example:\n\t * `llms_anonymize_string( 'help@lifterlms.com' )` will return '***p@li*********om'.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @param string $string The input string to be anonymized.\n\t * @param string $char   The character used to mask the string.\n\t * @return string\n\t */\n\tfunction llms_anonymize_string( $string, $char = '*' ) {\n\n\t\tif ( is_email( $string ) ) {\n\t\t\t$parts = explode( '@', $string );\n\t\t\treturn llms_anonymize_string( $parts[0] ) . '@' . llms_anonymize_string( $parts[1] );\n\t\t}\n\n\t\t$len = strlen( $string );\n\n\t\t$at_front = 2;\n\t\t$at_back  = 2;\n\t\tif ( 1 === $len ) {\n\t\t\treturn $char;\n\t\t} elseif ( $len <= 6 ) {\n\t\t\t$at_front = 0;\n\t\t\t$at_back  = 1;\n\t\t} elseif ( $len <= 10 ) {\n\t\t\t$at_front = 0;\n\t\t}\n\n\t\t$start = substr( $string, 0, $at_front );\n\t\t$body  = str_repeat( $char, strlen( $string ) - ( $at_front + $at_back ) );\n\t\t$end   = substr( $string, - $at_back );\n\n\t\treturn \"{$start}{$body}{$end}\";\n\t}\n}\n\n\n/**\n * Insert elements into an associative array after a specific array key\n *\n * If the requested key doesn't exit, the new item will be added to the end of the array.\n * If you need to insert at the beginning of an array use array_merge( $new_item, $orig_item ).\n *\n * @since 3.21.0\n *\n * @param array  $array       Original associative array.\n * @param string $after_key   Key name in original array to insert new item after.\n * @param string $insert_key  Key name of the item to be inserted.\n * @param mixed  $insert_item Value to be inserted.\n * @return array\n */\nfunction llms_assoc_array_insert( $array, $after_key, $insert_key, $insert_item ) {\n\n\t$res = array();\n\n\t$new_item = array(\n\t\t$insert_key => $insert_item,\n\t);\n\n\t$index = array_search( $after_key, array_keys( $array ) );\n\tif ( false !== $index ) {\n\t\t++$index;\n\n\t\t$res = array_merge(\n\t\t\tarray_slice( $array, 0, $index, true ),\n\t\t\t$new_item,\n\t\t\tarray_slice( $array, $index, count( $array ) - 1, true )\n\t\t);\n\t} else {\n\t\t$res = array_merge( $array, $new_item );\n\t}\n\n\treturn $res;\n}\n\n/**\n * Do apply_filters( 'the_content', $content ) without actions adding their own content onto us...\n *\n * @param string $content Optional. The content. Default is empty string.\n * @return string\n * @since 3.16.10\n * @version 3.19.2\n */\nif ( ! function_exists( 'llms_content' ) ) {\n\tfunction llms_content( $content = '' ) {\n\t\t$content = do_shortcode( shortcode_unautop( wpautop( convert_chars( wptexturize( $content ) ) ) ) );\n\t\tglobal $wp_embed;\n\t\tif ( $wp_embed && method_exists( $wp_embed, 'autoembed' ) ) {\n\t\t\t$content = $wp_embed->autoembed( $content );\n\t\t}\n\t\treturn $content;\n\t}\n}\n\n/**\n * Mark a function as deprecated and inform when it is used.\n *\n * This function uses WP core's `_deprecated_function()`, logging to the LifterLMS log file\n * located at `wp-content/updloads/llms-logs/llms-{$hash}.log` instead of `wp-content/debug.log`.\n *\n * @since 2.6.0\n * @since 3.6.0 Unknown.\n * @since 4.4.0 Uses WP `_deprecated_function()` instead of duplicating its logic.\n *\n * @param string $function    Name of the deprecated function.\n * @param string $version     LifterLMS version that deprecated the function.\n * @param string $replacement Optional. Replacement function. Default is `null`.\n * @return void\n */\nfunction llms_deprecated_function( $function, $version, $replacement = null ) {\n\n\t_deprecated_function( esc_html( $function ), esc_html( $version ), esc_html( $replacement ) );\n}\n\n/**\n * Cron function to cleanup files in the LLMS_TMP_DIR\n *\n * Removes any files that are more than a day old.\n *\n * @since 3.18.0\n * @since 4.10.1 Use strict type comparisons.\n *\n * @return void\n */\nfunction llms_cleanup_tmp() {\n\n\t$max_age = llms_current_time( 'timestamp' ) - apply_filters( 'llms_tmpfile_max_age', DAY_IN_SECONDS );\n\n\t$exclude = array( '.htaccess', 'index.html' );\n\n\tforeach ( glob( LLMS_TMP_DIR . '*' ) as $file ) {\n\n\t\t// Don't cleanup index and .htaccess.\n\t\tif ( in_array( basename( $file ), $exclude, true ) ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ( filemtime( $file ) < $max_age ) {\n\t\t\twp_delete_file( $file );\n\t\t}\n\t}\n}\nadd_action( 'llms_cleanup_tmp', 'llms_cleanup_tmp' );\n\n/**\n * Escape and add quotes to a string, useful for array mapping when building queries.\n *\n * @since 6.0.0\n *\n * @param string $str Input string.\n * @return string Escaped string wrapped in quotation marks.\n */\nfunction llms_esc_and_quote_str( $str ) {\n\treturn \"'\" . esc_sql( $str ) . \"'\";\n}\n\n/**\n * Retrieve an array of post types which can be completed by students\n *\n * @since 4.2.0\n *\n * @return string[]\n */\nfunction llms_get_completable_post_types() {\n\n\t/**\n\t * Filter the list of post types which can be completed by students.\n\t *\n\t * @since Unknown\n\t *\n\t * @param string[] $post_types WP_Post post type names.\n\t */\n\treturn apply_filters( 'llms_completable_post_types', array( 'course', 'section', 'lesson' ) );\n}\n\n/**\n * Retrieve an array of taxonomies which can be completed by students\n *\n * @since 4.2.0\n *\n * @return string[]\n */\nfunction llms_get_completable_taxonomies() {\n\n\t/**\n\t * Filter the list of taxonomies which can be completed by students.\n\t *\n\t * @since 4.2.0\n\t *\n\t * @param string[] $taxonomies Taxonomy names.\n\t */\n\treturn apply_filters( 'llms_completable_taxonomies', array( 'course_track' ) );\n}\n\n/**\n * Retrieve an array of post types whose name doesn't start with the prefix 'llms_'.\n *\n * @since 4.10.1\n *\n * @return string[]\n */\nfunction llms_get_unprefixed_post_types() {\n\n\t/**\n\t * Filter the list of post types whose name doesn't start with the prefix 'llms_'.\n\t *\n\t * @since 4.10.1\n\t *\n\t * @param string[] $post_types WP_Post post type names.\n\t */\n\treturn apply_filters( 'llms_unprefixed_post_types', array( 'course', 'section', 'lesson' ) );\n}\n\n/**\n * Get themes natively supported by LifterLMS\n *\n * @since 3.0.0\n *\n * @return array\n */\nfunction llms_get_core_supported_themes() {\n\treturn array(\n\t\t'canvas',\n\t\t'Divi',\n\t\t'genesis',\n\t\t'twentyseventeen',\n\t\t'twentysixteen',\n\t\t'twentyfifteen',\n\t\t'twentyfourteen',\n\t\t'twentythirteen',\n\t\t'twentyeleven',\n\t\t'twentytwelve',\n\t\t'twentyten',\n\t);\n}\n\n/**\n * Get human readable time difference between 2 dates\n *\n * Return difference between 2 dates in year, month, hour, minute or second\n * The $precision caps the number of time units used: for instance if\n * $time1 - $time2 = 3 days, 4 hours, 12 minutes, 5 seconds\n * - with precision = 1 : 3 days\n * - with precision = 2 : 3 days, 4 hours\n * - with precision = 3 : 3 days, 4 hours, 12 minutes.\n *\n * @since Unknown\n * @since 3.24.0 Unknown.\n *\n * @source http://www.if-not-true-then-false.com/2010/php-calculate-real-differences-between-two-dates-or-timestamps/\n *\n * @param mixed   $time1     A time (string or timestamp).\n * @param mixed   $time2     A time (string or timestamp).\n * @param integer $precision Optional precision. Default is 2.\n * @return string time difference\n */\nfunction llms_get_date_diff( $time1, $time2, $precision = 2 ) {\n\t// If not numeric then convert timestamps.\n\tif ( ! is_numeric( $time1 ) ) {\n\t\t$time1 = strtotime( $time1 );\n\t}\n\tif ( ! is_numeric( $time2 ) ) {\n\t\t$time2 = strtotime( $time2 );\n\t}\n\t// If time1 > time2 then swap the 2 values.\n\tif ( $time1 > $time2 ) {\n\t\tlist( $time1, $time2 ) = array( $time2, $time1 );\n\t}\n\t// Set up intervals and diffs arrays.\n\t$intervals     = array( 'year', 'month', 'day', 'hour', 'minute', 'second' );\n\t$l18n_singular = array(\n\t\t'year'   => __( 'year', 'lifterlms' ),\n\t\t'month'  => __( 'month', 'lifterlms' ),\n\t\t'day'    => __( 'day', 'lifterlms' ),\n\t\t'hour'   => __( 'hour', 'lifterlms' ),\n\t\t'minute' => __( 'minute', 'lifterlms' ),\n\t\t'second' => __( 'second', 'lifterlms' ),\n\t);\n\t$l18n_plural   = array(\n\t\t'year'   => __( 'years', 'lifterlms' ),\n\t\t'month'  => __( 'months', 'lifterlms' ),\n\t\t'day'    => __( 'days', 'lifterlms' ),\n\t\t'hour'   => __( 'hours', 'lifterlms' ),\n\t\t'minute' => __( 'minutes', 'lifterlms' ),\n\t\t'second' => __( 'seconds', 'lifterlms' ),\n\t);\n\t$diffs         = array();\n\tforeach ( $intervals as $interval ) {\n\t\t// Create temp time from time1 and interval.\n\t\t$ttime = strtotime( '+1 ' . $interval, $time1 );\n\t\t// Set initial values.\n\t\t$add    = 1;\n\t\t$looped = 0;\n\t\t// Loop until temp time is smaller than time2.\n\t\twhile ( $time2 >= $ttime ) {\n\t\t\t// Create new temp time from time1 and interval.\n\t\t\t++$add;\n\t\t\t$ttime = strtotime( '+' . $add . ' ' . $interval, $time1 );\n\t\t\t++$looped;\n\t\t}\n\t\t$time1              = strtotime( '+' . $looped . ' ' . $interval, $time1 );\n\t\t$diffs[ $interval ] = $looped;\n\t}\n\t$count = 0;\n\t$times = array();\n\tforeach ( $diffs as $interval => $value ) {\n\t\t// Break if we have needed precision.\n\t\tif ( $count >= $precision ) {\n\t\t\tbreak;\n\t\t}\n\t\t// Add value and interval if value is bigger than 0.\n\t\tif ( $value > 0 ) {\n\t\t\tif ( 1 != $value ) {\n\t\t\t\t$text = $l18n_plural[ $interval ];\n\t\t\t} else {\n\t\t\t\t$text = $l18n_singular[ $interval ];\n\t\t\t}\n\t\t\t// Add value and interval to times array.\n\t\t\t$times[] = $value . ' ' . $text;\n\t\t\t++$count;\n\t\t}\n\t}\n\t// Return string with times.\n\treturn implode( ', ', $times );\n}\n\n/**\n * Instantiate an instance of DOMDocument with an HTML string\n *\n * This function suppresses PHP warnings that would be thrown by DOMDocument when\n * loading a partial string or an HTML string with errors.\n *\n * @see LLMS_DOM_Document->load().\n *\n * @since 4.7.0\n * @since 4.8.0 Remove reliance on `mb_convert_encoding()`.\n * @since 4.13.0 Add back partial reliance on `mb_convert_encoding()` but keep the previous implementation as a fall-back.\n *               Also fix a potential fatal in the fall-back which tried to manipulate a non existent node.\n *               Wrapper for `LLMS_Dom_Document:load()`.\n *\n * @param string $string An HTML string, either a full HTML document or a partial string.\n * @return DOMDocument|WP_Error Returns an instance of DOMDocument with `$string` loaded into it\n *                              or an error object when DOMDocument isn't available or an error is encountered during loading.\n */\nfunction llms_get_dom_document( $string ) {\n\n\t$llms_dom = new LLMS_DOM_Document( $string );\n\t$load     = $llms_dom->load();\n\n\treturn is_wp_error( $load ) ? $load : $llms_dom->dom();\n}\n\n/**\n * Retrieve the HTML for a donut chart\n *\n * Note that this must be used in conjunction with some JS to initialize the chart!\n *\n * @since 3.9.0\n * @since 3.24.0 Unknown.\n *\n * @param mixed  $percentage Percentage to display\n * @param string $text       Optional. Text/caption to display (short). Default is empty string.\n * @param string $size       Optional. Size of the chart (mini, small, default, large). Default is 'default'.\n * @param array  $classes    Optional. Additional custom css classes to add to the chart element. Default is empty array.\n * @return string\n */\nfunction llms_get_donut( $percentage, $text = '', $size = 'default', $classes = array() ) {\n\t$percentage = is_numeric( $percentage ) ? $percentage : 0;\n\t$classes    = array_merge( array( 'llms-donut', $size ), $classes );\n\t$classes    = implode( ' ', $classes );\n\t$percentage = 'mini' === $size ? round( $percentage, 0 ) : llms()->grades()->round( $percentage );\n\treturn '\n\t\t<div class=\"' . esc_attr( $classes ) . '\" data-perc=\"' . esc_attr( $percentage ) . '\">\n\t\t\t<div class=\"inside\">\n\t\t\t\t<div class=\"percentage\">\n\t\t\t\t\t' . esc_html( $percentage ) . '<small>%</small>\n\t\t\t\t\t<div class=\"caption\">' . esc_html( $text ) . '</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>';\n}\n\n/**\n * Get a list of registered engagement triggers\n *\n * @return array\n * @since 3.1.0\n * @since 3.24.1\n */\nfunction llms_get_engagement_triggers() {\n\t/**\n\t * Filter the engagement triggers\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $engagement_triggers An associative array of engagement triggers. Keys are the engagement trigger slugs, values are their description.\n\t */\n\treturn apply_filters(\n\t\t'lifterlms_engagement_triggers',\n\t\tarray(\n\t\t\t'user_registration'      => __( 'Student creates a new account', 'lifterlms' ),\n\t\t\t'access_plan_purchased'  => __( 'Student Purchases an Access Plan', 'lifterlms' ),\n\t\t\t'course_enrollment'      => __( 'Student enrolls in a course', 'lifterlms' ),\n\t\t\t'course_purchased'       => __( 'Student purchases a course', 'lifterlms' ),\n\t\t\t'course_completed'       => __( 'Student completes a course', 'lifterlms' ),\n\t\t\t// 'days_since_login' => __( 'Days since user last logged in', 'lifterlms' ), // @todo.\n\t\t\t'lesson_completed'       => __( 'Student completes a lesson', 'lifterlms' ),\n\t\t\t'quiz_completed'         => __( 'Student completes a quiz', 'lifterlms' ),\n\t\t\t'quiz_passed'            => __( 'Student passes a quiz', 'lifterlms' ),\n\t\t\t'quiz_failed'            => __( 'Student fails a quiz', 'lifterlms' ),\n\t\t\t'section_completed'      => __( 'Student completes a section', 'lifterlms' ),\n\t\t\t'course_track_completed' => __( 'Student completes a course track', 'lifterlms' ),\n\t\t\t'membership_enrollment'  => __( 'Student enrolls in a membership', 'lifterlms' ),\n\t\t\t'membership_purchased'   => __( 'Student purchases a membership', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Verify if the current user can edit the given product id (course or membership).\n *\n * @param $product_id\n * @since 8.0.2\n *\n * @return bool\n */\nfunction llms_current_user_can_edit_product( $product_id ) {\n\treturn current_user_can( 'edit_course', $product_id ) || current_user_can( 'edit_membership', $product_id );\n}\n\n/**\n * Get a list of registered engagement types\n *\n * @return array\n * @since 3.1.0\n * @version 3.24.0\n */\nfunction llms_get_engagement_types() {\n\t/**\n\t * Filter the engagement types\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $engagement_types An associative array of engagement types. Keys are the engagement type slugs, values are their description.\n\t */\n\treturn apply_filters(\n\t\t'lifterlms_engagement_types',\n\t\tarray(\n\t\t\t'achievement' => __( 'Award an Achievement', 'lifterlms' ),\n\t\t\t'certificate' => __( 'Award a Certificate', 'lifterlms' ),\n\t\t\t'email'       => __( 'Send an Email', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Retrieve a list of post types which users can be enrolled into.\n *\n * @since 4.4.1\n *\n * @return string[] A list of post type names.\n */\nfunction llms_get_enrollable_post_types() {\n\n\t/**\n\t * Customize the post types which users can be enrolled into.\n\t *\n\t * This filter differs slightly from `llms_user_enrollment_status_allowed_post_types`. This filter\n\t * determines which post types a user can be physically associated with through enrollment while\n\t * `llms_user_enrollment_status_allowed_post_types` allows checking of user enrollment based on\n\t * posts which are associated with a post type.\n\t *\n\t * @since 3.37.9\n\t *\n\t * @see llms_user_enrollment_status_allowed_post_types\n\t *\n\t * @param string[] $post_types Array of post type names.\n\t */\n\treturn apply_filters( 'llms_user_enrollment_allowed_post_types', array( 'course', 'llms_membership' ) );\n}\n\n/**\n * Retrieve a list of post types that can be used to check a users enrollment status in an enroll-able post type.\n *\n * @since 4.4.1\n *\n * @return string[] A list of post type names.\n */\nfunction llms_get_enrollable_status_check_post_types() {\n\n\t/**\n\t * Customize the post types that can be used to check a user's enrollment status.\n\t *\n\t * This filter differs slightly from `llms_user_enrollment_allowed_post_types`. The difference is that\n\t * a user can be enrolled into a course but we can check their course enrollment status using the ID of a child (section or lesson).\n\t *\n\t * When adding a new post type for custom enrollment functionality the post type should be registered with\n\t * both of these filters.\n\t *\n\t * @since 3.37.9\n\t *\n\t * @see llms_user_enrollment_allowed_post_types\n\t *\n\t * @param string[] $post_types List of allowed post types names.\n\t */\n\treturn apply_filters( 'llms_user_enrollment_status_allowed_post_types', array( 'course', 'section', 'lesson', 'llms_membership' ) );\n}\n\n/**\n * Retrieve an HTML anchor for an option page\n *\n * @since 3.18.0\n *\n * @param string $option_name Option name.\n * @param string $target      Optional. HTML target attribute. Defaults to _blank.\n * @return string\n */\nfunction llms_get_option_page_anchor( $option_name, $target = '_blank' ) {\n\n\t$page_id = get_option( $option_name );\n\n\tif ( ! $page_id ) {\n\t\treturn '';\n\t}\n\n\t$target = $target ? ' target=\"' . esc_attr( $target ) . '\"' : '';\n\n\treturn sprintf(\n\t\t'<a href=\"%1$s\"%2$s>%3$s</a>',\n\t\tget_the_permalink( $page_id ),\n\t\t$target,\n\t\tget_the_title( $page_id )\n\t);\n}\n\n/**\n * Get a list of available product (course & membership) catalog visibility options\n *\n * @since 3.6.0\n *\n * @return array\n */\nfunction llms_get_product_visibility_options() {\n\t/**\n\t * Filter the product visibility options\n\t *\n\t * @since 3.6.0\n\t *\n\t * @param array $product_visibility_options. An associative array representing of visibility options. Keys are the engagement type slugs, values are their description.\n\t */\n\treturn apply_filters(\n\t\t'lifterlms_product_visibility_options',\n\t\tarray(\n\t\t\t'catalog_search' => __( 'Catalog &amp; Search', 'lifterlms' ),\n\t\t\t'catalog'        => __( 'Catalog only', 'lifterlms' ),\n\t\t\t'search'         => __( 'Search only', 'lifterlms' ),\n\t\t\t'hidden'         => __( 'Hidden', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Get an array of student IDs based on enrollment status a course or membership\n *\n * @since 3.0.0\n * @since 3.8.0 Unknown.\n * @since 4.10.2 Instantiate the student query passing `no_found_rows` arg as `true`,\n *               as we don't need (and do not return) pagination info, e.g. max_pages.\n * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n *\n * @param int          $post_id  WP_Post id of a course or membership.\n * @param string|array $statuses List of enrollment statuses to query by status query is an OR relationship. Default is 'enrolled'.\n * @param integer      $limit    Number of results.\n * @param integer      $skip     Number of results to skip (for pagination).\n * @return array\n */\nfunction llms_get_enrolled_students( $post_id, $statuses = 'enrolled', $limit = 50, $skip = 0 ) {\n\n\t$query = new LLMS_Student_Query(\n\t\tarray(\n\t\t\t'post_id'       => $post_id,\n\t\t\t'statuses'      => $statuses,\n\t\t\t'page'          => ( 0 === $skip ) ? 1 : ( $skip / $limit ) + 1,\n\t\t\t'per_page'      => $limit,\n\t\t\t'sort'          => array(\n\t\t\t\t'id' => 'ASC',\n\t\t\t),\n\t\t\t'no_found_rows' => true,\n\t\t)\n\t);\n\n\tif ( $query->has_results() ) {\n\t\treturn wp_list_pluck( $query->get_results(), 'id' );\n\t}\n\n\treturn array();\n}\n\n/**\n * Retrieve default instructor data structure.\n *\n * @since 3.25.0\n *\n * @return array\n */\nfunction llms_get_instructors_defaults() {\n\t/**\n\t * Filter the instructor's default data structure.\n\t *\n\t * @since 3.25.0\n\t *\n\t * @param array $product_visibility_options. An associative array representing the instructor's default data structure.\n\t */\n\treturn apply_filters(\n\t\t'llms_post_instructors_get_defaults',\n\t\tarray(\n\t\t\t'label'      => __( 'Author', 'lifterlms' ),\n\t\t\t'visibility' => 'visible',\n\t\t\t'id'         => '',\n\t\t)\n\t);\n}\n\n/**\n * Function used to sanitize user input in a manner similar to the (deprecated) FILTER_SANITIZE_STRING.\n *\n * This function retrieves the raw user input via `llms_filter_input()` using the FILTER_UNSAFE_RAW filter, strips\n * all tags, and then encodes single and double quotes with the relevant HTML entity codes.\n *\n * In many cases, the usage of `FILTER_SANITIZE_STRING` can be easily replaced with `FILTER_SANITIZE_FULL_SPECIAL_CHARS` but\n * in some cases, especially when storing the user input, encoding all special characters can result in an stored XSS injection\n * so this function can be used to preserve the pre PHP 8.1 behavior where sanitization is expected during the retrieval\n * of user input.\n *\n * @since 5.9.0\n *\n * @param int    $type          One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.\n * @param string $variable_name Name of a variable to retrieve.\n * @param int[]  $flags         Array of supported filter options and flags.\n *                              Accepts `FILTER_REQUIRE_ARRAY` in order to require the input to be an array.\n *                              Accepts `FILTER_FLAG_NO_ENCODE_QUOTES` to prevent encoding of quotes.\n * @return string|string[]|null|boolean Value of the requested variable on success, `false` if the filter fails, or `null` if the `$variable_name` variable is not set.\n */\nfunction llms_filter_input_sanitize_string( $type, $variable_name, $flags = array() ) {\n\n\t$require_array = in_array( FILTER_REQUIRE_ARRAY, $flags, true );\n\n\t$string = llms_filter_input( $type, $variable_name, FILTER_UNSAFE_RAW, $require_array ? FILTER_REQUIRE_ARRAY : array() );\n\n\t// If we have an empty string or the input var isn't found we can return early.\n\tif ( empty( $string ) ) {\n\t\treturn $string;\n\t}\n\n\t$string = $require_array ? array_map( 'wp_strip_all_tags', $string ) : wp_strip_all_tags( $string );\n\n\tif ( ! in_array( FILTER_FLAG_NO_ENCODE_QUOTES, $flags, true ) ) {\n\t\t$string = str_replace(\n\t\t\tarray( \"'\", '\"' ),\n\t\t\tarray( '&#39;', '&#34;' ),\n\t\t\t$string\n\t\t);\n\t}\n\n\treturn $string;\n}\n\n\n/**\n * Get the most recently created coupon ID for a given code\n *\n * @param string $code        Optional. The coupon's code (title). Default is empty string.\n * @param int    $dupcheck_id Optional. Coupon id that can be passed which will be excluded during the query\n *                            this is used to dupcheck the coupon code during coupon creation. Default is 0.\n * @return int\n * @since   3.0.0\n * @version 3.0.0\n */\nfunction llms_find_coupon( $code = '', $dupcheck_id = 0 ) {\n\n\tglobal $wpdb;\n\treturn $wpdb->get_var(\n\t\t$wpdb->prepare(\n\t\t\t\"SELECT ID\n\t\t FROM {$wpdb->posts}\n\t\t WHERE post_title = %s\n\t\t AND post_type = 'llms_coupon'\n\t\t AND post_status = 'publish'\n\t\t AND ID != %d\n\t\t ORDER BY ID desc;\n\t\t\",\n\t\t\tarray( $code, $dupcheck_id )\n\t\t)\n\t); // no-cache ok.\n}\n\n/**\n * Get a list of available course / membership enrollment statuses\n *\n * @since 3.0.0\n *\n * @return array\n */\nfunction llms_get_enrollment_statuses() {\n\t/**\n\t * Filter the enrollment statuses\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $enrollment_statuses An associative array representing the enrollment statuses. Keys are the statuses, values are their human readable labels (names).\n\t */\n\treturn apply_filters(\n\t\t'llms_get_enrollment_statuses',\n\t\tarray(\n\t\t\t'cancelled' => __( 'Cancelled', 'lifterlms' ),\n\t\t\t'enrolled'  => __( 'Enrolled', 'lifterlms' ),\n\t\t\t'expired'   => __( 'Expired', 'lifterlms' ),\n\t\t)\n\t);\n}\n\n/**\n * Get the human readable (and translated) name of an enrollment status\n *\n * @since 3.0.0\n * @since 3.6.0 Unknown.\n *\n * @param string $status Enrollment status key.\n * @return string\n */\nfunction llms_get_enrollment_status_name( $status ) {\n\n\t$status   = strtolower( $status ); // Backwards compatibility.\n\t$statuses = llms_get_enrollment_statuses();\n\tif ( is_array( $statuses ) && isset( $statuses[ $status ] ) ) {\n\t\t$status = $statuses[ $status ];\n\t}\n\t/**\n\t * Filter the enrollment status name\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $enrollment_status The enrollment status name.\n\t */\n\treturn apply_filters( 'lifterlms_get_enrollment_status_name', $status );\n}\n\n/**\n * Retrieve an IP Address for the current user\n *\n * @since 3.0.0\n * @since 3.35.0 Sanitize superglobal input.\n *\n * @return string\n */\nfunction llms_get_ip_address() {\n\n\t$ip = '';\n\n\t// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Look below you.\n\t// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Look below you.\n\tif ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) {\n\t\t$ip = $_SERVER['HTTP_X_REAL_IP'];\n\t} elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {\n\t\t// Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2.\n\t\t// Make sure we always only send through the first IP in the list which should always be the client IP.\n\t\t$ip = trim( current( explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );\n\t} elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {\n\t\t$ip = $_SERVER['REMOTE_ADDR'];\n\t}\n\t// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized\n\t// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash\n\n\t$ip = sanitize_text_field( wp_unslash( $ip ) );\n\n\tif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {\n\t\treturn '';\n\t}\n\n\treturn $ip;\n}\n\n/**\n * Retrieves and filters the value open registration option\n *\n * @since 5.0.0\n *\n * @return string The value of the open registration status. Either \"yes\" for enabled or \"no\" for disabled.\n */\nfunction llms_get_open_registration_status() {\n\n\t$status = get_option( 'lifterlms_enable_myaccount_registration', 'no' );\n\n\t/**\n\t * Filter the value of the open registration setting\n\t *\n\t * @since 3.37.10\n\t *\n\t * @param string $status The current value of the open registration option. Either \"yes\" for enabled or \"no\" for disabled.\n\t */\n\treturn apply_filters( 'llms_enable_open_registration', $status );\n}\n\n/**\n * Retrieve the LLMS Post Model for a give post by ID or WP_Post Object\n *\n * @since 3.3.0\n * @since 3.16.11 Unknown.\n * @since 4.10.1 Made sure to only instantiate LifterLMS classes.\n *\n * @param WP_Post|int $post  Instance of WP_Post or a WP Post ID.\n * @param mixed       $error Determine what to return if the LLMS class isn't found.\n *                           post  = WP_Post\n *                           falsy = false.\n * @return LLMS_Post_Model|WP_Post|null|false LLMS_Post_Model extended object,\n *                                            null if WP get_post() fails,\n *                                            WP_Post if LLMS_Post_Model extended class isn't found and $error = 'post'\n *                                            false if LLMS_Post_Model extended class isn't found and $error != 'post'.\n */\nfunction llms_get_post( $post, $error = false ) {\n\n\t$post = get_post( $post );\n\tif ( ! $post ) {\n\t\treturn $post;\n\t}\n\n\t$class = '';\n\n\t// Check whether it's an llms post candidate: `post_type` starts with the 'llms_' prefix, or is one of the unprefixed ones.\n\tif ( 0 === strpos( $post->post_type, 'llms_' ) || in_array( $post->post_type, llms_get_unprefixed_post_types(), true ) ) {\n\t\t$post_type = explode( '_', str_replace( 'llms_', '', $post->post_type ) );\n\t\t$class     = 'LLMS';\n\t\tforeach ( $post_type as $part ) {\n\t\t\t$class .= '_' . ucfirst( $part );\n\t\t}\n\t}\n\n\tif ( $class && class_exists( $class ) ) {\n\t\treturn new $class( $post );\n\t} elseif ( 'post' === $error ) {\n\t\treturn $post;\n\t}\n\n\treturn false;\n}\n\n/**\n * Retrieve the parent course for a section, lesson, or quiz\n *\n * @since 3.6.0\n * @since 3.17.7 Unknown.\n * @since 3.37.14 Bail if `$post` is not an istance of `LLMS_Post_Model`.\n *                Use strict comparison.\n *\n * @param WP_Post|int $post WP Post ID or instance of WP_Post.\n * @return LLMS_Course|null Instance of the LLMS_Course or null.\n */\nfunction llms_get_post_parent_course( $post ) {\n\n\t$post = llms_get_post( $post );\n\n\tif ( ! $post || ! is_a( $post, 'LLMS_Post_Model' ) ) {\n\t\treturn null;\n\t}\n\n\t/**\n\t * Filter the course children post types\n\t *\n\t * @since Unknown\n\t *\n\t * @param $post_type string[] Names of the post types that can be children of a course.\n\t */\n\t$post_types = apply_filters( 'llms_course_children_post_types', array( 'section', 'lesson', 'llms_quiz' ) );\n\tif ( ! in_array( $post->get( 'type' ), $post_types, true ) ) {\n\t\treturn null;\n\t}\n\n\t/** @var LLMS_Section|LLMS_Lesson|LLMS_Quiz $post */\n\treturn $post->get_course();\n}\n\n\n/**\n * Retrieve an array of existing transaction statuses\n *\n * @since 3.0.0\n *\n * @return array\n */\nfunction llms_get_transaction_statuses() {\n\t/**\n\t * Filter the transaction statuses\n\t *\n\t * @since Unknown\n\t *\n\t * @param $statuses string[] Names of the possible transaction statuses.\n\t */\n\treturn apply_filters(\n\t\t'llms_get_transaction_statuses',\n\t\tarray(\n\t\t\t'llms-txn-failed',\n\t\t\t'llms-txn-pending',\n\t\t\t'llms-txn-refunded',\n\t\t\t'llms-txn-succeeded',\n\t\t)\n\t);\n}\n\n/**\n * Determine is request is an ajax request\n *\n * @since 3.0.1\n * @since 4.0.0 Use WP core `wp_doing_ajax()`.\n *\n * @return bool\n */\nfunction llms_is_ajax() {\n\treturn wp_doing_ajax();\n}\n\n\n\n/**\n * Determine if request is a REST request\n *\n * @since 3.27.0\n *\n * @return bool\n */\nfunction llms_is_rest() {\n\t/**\n\t * Filters whether the current request is a REST request.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @param $is_rest Whether the current request is a REST request.\n\t */\n\treturn apply_filters( 'llms_is_rest', ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );\n}\n\n/**\n * Determine whether the current theme is a block theme.\n *\n * Just a wrapper for WordPress core `wp_is_block_theme()` so to filter for testing purposes.\n *\n * @since 6.0.0\n *\n * @return string\n */\nfunction llms_is_block_theme() {\n\t/**\n\t * Filters whether the current theme is a block theme.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param $is_block_theme Whether the current theme is a block theme.\n\t */\n\treturn apply_filters( 'llms_is_block_theme', function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() );\n}\n\n/**\n * Checks if the current admin page is the block editor.\n *\n * @since 7.2.0\n *\n * @return bool\n */\nfunction llms_is_block_editor(): bool {\n\tif ( function_exists( 'is_gutenberg_page' ) && is_gutenberg_page() ) {\n\t\treturn true;\n\t}\n\n\t$current_screen = get_current_screen();\n\n\tif ( method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n/**\n * Determine if the current request is a block rendering request in the editor.\n *\n * @since 7.2.0\n *\n * @return bool\n */\nfunction llms_is_editor_block_rendering() {\n\tif ( ! defined( 'REST_REQUEST' ) || ! is_user_logged_in() ) {\n\t\treturn false;\n\t}\n\n\tglobal $wp;\n\n\tif ( ! $wp instanceof WP || empty( $wp->query_vars['rest_route'] ) ) {\n\t\treturn false;\n\t}\n\n\t$route = $wp->query_vars['rest_route'];\n\n\treturn false !== strpos( $route, '/block-renderer/' );\n}\n\n/**\n * Check if the home URL is https. If it is, we don't need to do things such as 'force ssl'.\n *\n * @thanks woocommerce <3.\n *\n * @since 3.0.0\n *\n * @return bool\n */\nfunction llms_is_site_https() {\n\treturn false !== strstr( get_option( 'home' ), 'https:' );\n}\n\n/**\n * Create an array that can be passed to metabox select elements configured as an llms-select2-post query-ier\n *\n * @since 3.0.0\n * @since 3.6.0 Unknown\n *\n * @param array  $post_ids  Optional. Indexed array of WordPress Post IDs. Defayult is empty array.\n * @param string $template  Optional. A template to customize the way the results look. Default is empty string.\n *                          {title} and {id} can be passed into the template\n *                          and will be replaced with the post title and post id respectively.\n * @return array\n */\nfunction llms_make_select2_post_array( $post_ids = array(), $template = '' ) {\n\n\tif ( ! $template ) {\n\t\t$template = '{title} (' . __( 'ID#', 'lifterlms' ) . ' {id})';\n\t}\n\n\tif ( ! is_array( $post_ids ) ) {\n\t\t$post_ids = array( $post_ids );\n\t}\n\n\t$ret = array();\n\tforeach ( $post_ids as $id ) {\n\n\t\t$title = str_replace( array( '{title}', '{id}' ), array( get_the_title( $id ), $id ), $template );\n\n\t\t$ret[] = array(\n\t\t\t'key'   => $id,\n\t\t\t'title' => $title,\n\t\t);\n\t}\n\t/**\n\t * Filter the select2 post array\n\t *\n\t * @since Unknown\n\t *\n\t * @param array Associative array of representing select2 post elements.\n\t * @param array $post_ids  Optional. Indexed array of WordPress Post IDs.\n\t */\n\treturn apply_filters( 'llms_make_select2_post_array', $ret, $post_ids );\n}\n\n/**\n * Create an array that can be passed to metabox select elements configured as an llms-select2-student query-ier.\n *\n * @since 3.10.1\n * @version 3.23.0\n *\n * @param array  $user_ids Optional. Indexed array of WordPress User IDs. Default is empty array.\n * @param string $template Optional. A template to customize the way the results look. Default is empty string.\n *                         %1$s = student name\n *                         %2$s = student email.\n * @return array\n */\nfunction llms_make_select2_student_array( $user_ids = array(), $template = '' ) {\n\tif ( ! $template ) {\n\t\t$template = '%1$s &lt;%2$s&gt;';\n\t}\n\tif ( ! is_array( $user_ids ) ) {\n\t\t$user_ids = array( $user_ids );\n\t}\n\t$ret = array();\n\tforeach ( $user_ids as $id ) {\n\t\t$student = llms_get_student( $id );\n\t\tif ( ! $student ) {\n\t\t\tcontinue;\n\t\t}\n\t\t$ret[] = array(\n\t\t\t'key'   => $id,\n\t\t\t'title' => sprintf( $template, $student->get_name(), $student->get( 'user_email' ) ),\n\t\t);\n\t}\n\n\t/**\n\t * Filter the select2 student array\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $elements  Associative array representing select2 student elements.\n\t * @param array $post_ids  Optional. Indexed array of WordPress Post IDs.\n\t */\n\treturn apply_filters( 'llms_make_select2_student_array', $ret, $user_ids );\n}\n\n/**\n * Define a constant if it's not already defined\n *\n * @since 3.15.0\n *\n * @param string $name  Constant name.\n * @param mixed  $value Constant values.\n * @return void\n */\nfunction llms_maybe_define_constant( $name, $value ) {\n\tif ( ! defined( $name ) ) {\n\t\tdefine( $name, $value );\n\t}\n}\n\n/**\n * Parse booleans\n *\n * Mostly used to parse yes/no bools stored in various meta data fields\n *\n * @since 3.16.0\n *\n * @param mixed $val Value to parse.\n * @return bool\n */\nfunction llms_parse_bool( $val ) {\n\treturn filter_var( $val, FILTER_VALIDATE_BOOLEAN );\n}\n\n/**\n * Convert a PHP error constant to a human readable error code\n *\n * @since 4.9.0\n *\n * @link https://www.php.net/manual/en/errorfunc.constants.php\n *\n * @param int $code A predefined php error constant.\n * @return string A human readable string version of the constant.\n */\nfunction llms_php_error_constant_to_code( $code ) {\n\n\t$codes = array(\n\t\tE_ERROR             => 'E_ERROR', // 1.\n\t\tE_WARNING           => 'E_WARNING', // 2.\n\t\tE_PARSE             => 'E_PARSE', // 4.\n\t\tE_NOTICE            => 'E_NOTICE', // 8.\n\t\tE_CORE_ERROR        => 'E_CORE_ERROR', // 16.\n\t\tE_CORE_WARNING      => 'E_CORE_WARNING', // 32.\n\t\tE_COMPILE_ERROR     => 'E_COMPILE_ERROR', // 64.\n\t\tE_COMPILE_WARNING   => 'E_COMPILE_WARNING', // 128.\n\t\tE_USER_ERROR        => 'E_USER_ERROR', // 256.\n\t\tE_USER_WARNING      => 'E_USER_WARNING', // 512.\n\t\tE_USER_NOTICE       => 'E_USER_NOTICE', // 1024.\n\t\tE_STRICT            => 'E_STRICT', // 2048.\n\t\tE_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', // 4096.\n\t\tE_DEPRECATED        => 'E_DEPRECATED', // 8192.\n\t\tE_USER_DEPRECATED   => 'E_USER_DEPRECATED', // 16384.\n\t);\n\n\treturn isset( $codes[ $code ] ) ? $codes[ $code ] : $code;\n}\n\n/**\n * Wrapper for set_time_limit to ensure it's enabled before calling\n *\n * @since 3.16.5\n *\n * @source thanks WooCommerce <3\n *\n * @param int $limit  Optional. Script time limit. Default is 0 = no time limit.\n * @return void\n */\nfunction llms_set_time_limit( $limit = 0 ) {\n\n\tif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {\n\n\t\t@set_time_limit( $limit ); // @phpcs:ignore\n\n\t}\n}\n\n/**\n * Strips a list of prefixes from the start of a string.\n *\n * By default, strips `llms_`, `lifterlms_`, 'llms-', or 'lifterlms-'. Other prefixes may be provided.\n *\n * Will strip only the first prefix found from the list of supplied prefixes.\n *\n * @since 6.0.0\n * @since 7.0.0 Added `llms-` and `lifterlms-` as additional default prefixes to strip.\n *\n * @param string   $string   String to modify.\n * @param string[] $prefixes List of prefixs.\n * @return string The modified string. If no prefixes were found, the original string is returned without modification.\n */\nfunction llms_strip_prefixes( $string, $prefixes = array() ) {\n\n\t$prefixes = empty( $prefixes ) ? array( 'llms_', 'lifterlms_', 'llms-', 'lifterlms-' ) : $prefixes;\n\n\tforeach ( $prefixes as $prefix ) {\n\t\tif ( 0 === strpos( $string, $prefix ) ) {\n\t\t\t$string = substr( $string, strlen( $prefix ) );\n\n\t\t\t/**\n\t\t\t * Most of the time we'll be using this to replace `llms_` as we don't often use `lifterlms_` for\n\t\t\t * prefixing (anymore).\n\t\t\t *\n\t\t\t * Also, while it's probably not ever in use, this will prevent double-stripping if, for example,\n\t\t\t * the string was `llms_lifterlms_something`. If we did want to strip that, the `$prefixes` should\n\t\t\t * be overwritten to have both these items stripped.\n\t\t\t *\n\t\t\t * So once we find a prefix, we'll break the loop and return the string with the stripped prefix.\n\t\t\t */\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn $string;\n}\n\n/**\n * Trim a string and append a suffix\n *\n * @since 3.0.0\n *\n * @source thank you WooCommerce <3\n *\n * @param string $string Input string.\n * @param int    $chars  Optional. Max number of characters. Default is 200.\n * @param string $suffix Optional. A suffix to append. Default is '...'.\n * @return string\n */\nfunction llms_trim_string( $string, $chars = 200, $suffix = '...' ) {\n\tif ( strlen( $string ) > $chars ) {\n\t\tif ( function_exists( 'mb_substr' ) ) {\n\t\t\t$string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix;\n\t\t} else {\n\t\t\t$string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix;\n\t\t}\n\t}\n\treturn $string;\n}\n\n/**\n * Verify nonce with additional checks to confirm request method\n *\n * Skips verification if the nonce is not set\n * Useful for checking nonce for various LifterLMS forms which check for the form submission on init actions.\n *\n * @since 3.8.0\n * @since 3.35.0 Sanitize nonce field before verification.\n * @deprecated 10.0.0 Use `isset()` and `wp_verify_nonce()` directly.\n *\n * @param string $nonce          Name of the nonce field.\n * @param string $action         Name of the action.\n * @param string $request_method Optional. Name of the intended request method. Default is 'POST'.\n * @return null|false|int\n */\nfunction llms_verify_nonce( $nonce, $action, $request_method = 'POST' ) {\n\n\t_deprecated_function( __FUNCTION__, '10.0.0', 'isset() and wp_verify_nonce()' );\n\n\t/**\n\t * Filter whether to use $_SERVER instead of getenv when fetching an environment variable.\n\t *\n\t * @since 9.0.0\n\t */\n\t$server_request_method = apply_filters( 'llms_use_server_for_environment_fetch', false, $nonce, $action, $request_method ) ?\n\t\t$_SERVER['REQUEST_METHOD'] :\n\t\tgetenv( 'REQUEST_METHOD' );\n\n\tif ( strtoupper( $server_request_method ) !== $request_method ) {\n\t\treturn;\n\t}\n\n\tif ( empty( $_REQUEST[ $nonce ] ) ) {\n\t\treturn;\n\t}\n\n\treturn wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ $nonce ] ) ), $action );\n}\n\n/**\n * Check that the test value is a member of a specific array for sanitization purposes.\n *\n * @param mixed $needle Value to be tested.\n * @param array $safelist Array of safelist values.\n * @param mixed $default Default value to return if the needle is not in the safelist. Defaults to the first value in the safelist array if not provided.\n * @since 7.6.0\n */\nfunction llms_sanitize_with_safelist( $needle, $safelist, $default = null ) {\n\tif ( ! in_array( $needle, $safelist ) ) {\n\t\tif ( isset( $default ) ) {\n\t\t\treturn $default;\n\t\t} else {\n\t\t\treturn $safelist[0];\n\t\t}\n\t} else {\n\t\treturn $needle;\n\t}\n}\n"
  },
  {
    "path": "includes/llms.spam.functions.php",
    "content": "<?php\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Code related to spam detection and prevention.\n */\n\n// Constants. Define these in wp-config.php to override.\nif ( ! defined( 'LLMS_SPAM_ACTION_NUM_LIMIT' ) ) {\n\tdefine( 'LLMS_SPAM_ACTION_NUM_LIMIT', 10 );\n}\nif ( ! defined( 'LLMS_SPAM_ACTION_TIME_LIMIT' ) ) {\n\tdefine( 'LLMS_SPAM_ACTION_TIME_LIMIT', 900 );  // in seconds\n}\n\n/**\n * Determine whether the current visitor a spammer.\n *\n * @since 9.0.0\n *\n * @return bool Whether the current visitor a spammer.\n */\nfunction llms_is_spammer() {\n\t$is_spammer = false;\n\n\t$activity = llms_get_spam_activity();\n\tif ( false !== $activity && count( $activity ) >= LLMS_SPAM_ACTION_NUM_LIMIT ) {\n\t\t$is_spammer = true;\n\t}\n\n\t/**\n\t * Allow filtering whether the current visitor is a spammer.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @param bool  $is_spammer Whether the current visitor is a spammer.\n\t * @param array $activity   The list of potential spam activity.\n\t */\n\treturn apply_filters( 'llms_is_spammer', $is_spammer, $activity );\n}\n\n/**\n * Get the list of potential spam activity.\n *\n * @since 9.0.0\n *\n * @param string|null $ip The IP address to get activity for, or leave as null to attempt to determine current IP address.\n *\n * @return array|false The list of potential spam activity if successful, or false if IP could not be determined.\n */\nfunction llms_get_spam_activity( $ip = null ) {\n\tif ( empty( $ip ) ) {\n\t\t$ip = llms_get_ip_address();\n\t}\n\n\t// If we can't determine the IP, let's bail.\n\tif ( empty( $ip ) ) {\n\t\treturn false;\n\t}\n\n\t$ip            = preg_replace( '/[^0-9a-fA-F:., ]/', '', $ip );\n\t$transient_key = 'llms_spam_activity_' . $ip;\n\t$activity      = get_transient( $transient_key );\n\tif ( empty( $activity ) || ! is_array( $activity ) ) {\n\t\t$activity = array();\n\t}\n\n\t// Remove old items.\n\t$new_activity = array();\n\t$now          = time(); // UTC\n\tforeach ( $activity as $item ) {\n\t\t// Determine whether this item is recent enough to include.\n\t\tif ( $item > $now - ( absint( LLMS_SPAM_ACTION_TIME_LIMIT ) ) ) {\n\t\t\t$new_activity[] = $item;\n\t\t}\n\t}\n\n\treturn $new_activity;\n}\n\n/**\n * Track spam activity.\n * When we hit a certain number, the spam flag will trigger.\n * For now we are only tracking credit card declines their timestamps.\n * IP address isn't a perfect way to track this, but it's the best we have.\n *\n * @since 9.0.0\n *\n * @param string|null $ip The IP address to track activity for, or leave as null to attempt to determine current IP address.\n *\n * @return bool True if the tracking of activity was successful, or false if IP could not be determined.\n */\nfunction llms_track_spam_activity( $ip = null ) {\n\tif ( empty( $ip ) ) {\n\t\t$ip = llms_get_ip_address();\n\t}\n\n\t// If we can't determine the IP, let's bail.\n\tif ( empty( $ip ) ) {\n\t\treturn false;\n\t}\n\n\t$activity = llms_get_spam_activity( $ip );\n\t$now      = time(); // UTC\n\tarray_unshift( $activity, $now );\n\n\t// If we have more than the limit, don't bother storing them.\n\tif ( count( $activity ) > absint( LLMS_SPAM_ACTION_NUM_LIMIT ) ) {\n\t\trsort( $activity );\n\t\t$activity = array_slice( $activity, 0, absint( LLMS_SPAM_ACTION_NUM_LIMIT ) );\n\t}\n\n\t// Save to transient.\n\t$ip            = preg_replace( '/[^0-9a-fA-F:., ]/', '', $ip );\n\t$transient_key = 'llms_spam_activity_' . $ip;\n\tset_transient( $transient_key, $activity, (int) absint( LLMS_SPAM_ACTION_TIME_LIMIT ) );\n\n\treturn true;\n}\n\n/**\n * Clears all stored spam activity for an IP address.\n * Note that the llms_get_spam_activity function clears out old values\n * automatically, and this should only be used to completely clear the activity.\n *\n * @since 9.0.0\n *\n * @param string|null $ip The IP address to clear activity for, or leave as null to attempt to determine current IP address.\n *\n * @return bool True if the clearing of activity was successful, or false if IP could not be determined.\n */\nfunction llms_clear_spam_activity( $ip = null ) {\n\tif ( empty( $ip ) ) {\n\t\t$ip = llms_get_ip_address();\n\t}\n\n\t// If we can't determine the IP, let's bail.\n\tif ( empty( $ip ) ) {\n\t\treturn false;\n\t}\n\n\t$transient_key = 'llms_spam_activity_' . $ip;\n\n\tdelete_transient( $transient_key );\n\n\treturn true;\n}\n\n/**\n * Track spam activity when checkouts or billing updates fail.\n * Hooked on wp so the $post global is set up.\n *\n * @since 9.0.0\n * @param MemberOrder $morder The order object used at checkout. We ignore it.\n */\nfunction llms_track_failed_checkouts_for_spam() {\n\t// Bail if Spam Protection is disabled.\n\tif ( ! llms_is_spam_protection_enabled() ) {\n\t\treturn;\n\t}\n\n\t// Bail if we're not on the LifterLMS checkout page.\n\tif ( is_admin() || ! is_llms_checkout() ) {\n\t\treturn;\n\t}\n\n\t// Bail if there are no notices with type error.\n\t$notices = llms()->session->get( 'llms_notices', array() );\n\t$types   = array_keys( $notices );\n\tif ( ! in_array( 'error', $types ) ) {\n\t\treturn;\n\t}\n\n\tllms_track_spam_activity();\n}\n\n/**\n * Determine whether spam protection is enabled.\n *\n * @since 9.0.0\n *\n * @return bool Whether spam protection is enabled.\n */\nfunction llms_is_spam_protection_enabled() {\n\treturn llms_parse_bool( get_option( 'lifterlms_spam_protection', 'yes' ) );\n}\n\nadd_action( 'wp', 'llms_track_failed_checkouts_for_spam' );\n\n/**\n * Disable checkout and billing update forms for spammers.\n *\n * @since 9.0.0\n *\n * @return mixed Truthy means stop checkout.\n */\nfunction llms_disable_checkout_for_spammers() {\n\t// Bail if Spam Protection is disabled.\n\tif ( ! llms_is_spam_protection_enabled() ) {\n\t\treturn false;\n\t}\n\n\t// Bail if the current visitor is not a spammer.\n\tif ( ! llms_is_spammer() ) {\n\t\treturn false;\n\t}\n\n\t// Show a notice at LifterLMS checkout RE spam.\n\t$notice = __( 'Suspicious activity detected. Try again in a few minutes.', 'lifterlms' );\n\tllms_add_notice( $notice, 'error' );\n\n\treturn true;\n}\nadd_filter( 'llms_before_checkout_validation', 'llms_disable_checkout_for_spammers' );\n"
  },
  {
    "path": "includes/llms.template.functions.php",
    "content": "<?php\n/**\n * Front end template functions\n *\n * @package LifterLMS/Functions/Templates\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nrequire 'functions/llms-functions-content.php';\nrequire 'functions/llms-functions-conditional-tags.php';\nrequire 'functions/llms-functions-templates-courses.php';\nrequire 'functions/llms-functions-templates-memberships.php';\nrequire 'functions/llms-functions-templates-shared.php';\nrequire 'functions/llms-functions-template-view-order.php';\n\nrequire 'functions/llms.functions.templates.achievements.php';\nrequire 'functions/llms.functions.templates.certificates.php';\nrequire 'functions/llms.functions.templates.dashboard.php';\nrequire 'functions/llms.functions.templates.dashboard.widgets.php';\nrequire 'functions/llms.functions.templates.loop.php';\nrequire 'functions/llms.functions.templates.pricing.table.php';\nrequire 'functions/llms.functions.templates.privacy.php';\nrequire 'functions/llms.functions.templates.quizzes.php';\n\n/**\n * Output email body content\n *\n * @return   void\n * @since    3.8.0\n * @version  3.8.0\n */\nif ( ! function_exists( 'llms_email_body' ) ) {\n\n\tfunction llms_email_body( $content = '' ) {\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo apply_filters( 'the_content', $content );\n\t}\n}\n\n\n/**\n * Output email footer template\n *\n * @return   void\n * @since    3.8.0\n * @version  3.8.0\n */\nif ( ! function_exists( 'llms_email_footer' ) ) {\n\n\tfunction llms_email_footer() {\n\t\tllms_get_template( 'emails/footer.php' );\n\t}\n}\n\n/**\n * Output email header template with optional heading\n *\n * @param    string  $heading   optional heading text to output above the main content\n * @return   void\n * @since    3.8.0\n * @version  3.8.0\n */\nif ( ! function_exists( 'llms_email_header' ) ) {\n\n\tfunction llms_email_header( $heading = '' ) {\n\t\tllms_get_template(\n\t\t\t'emails/header.php',\n\t\t\tarray(\n\t\t\t\t'email_heading' => $heading,\n\t\t\t)\n\t\t);\n\t}\n}\n\n/**\n * Template Redirect\n *\n * @return void\n */\nfunction llms_template_redirect() {\n\tglobal $wp_query, $wp;\n\n\t// When default permalinks are enabled, redirect shop page to post type archive url.\n\tif ( ! empty( $_GET['page_id'] ) && get_option( 'permalink_structure' ) == '' && llms_get_page_id( 'shop' ) == $_GET['page_id'] ) {\n\t\twp_safe_redirect( get_post_type_archive_link( 'course' ) );\n\t\texit;\n\t}\n\t// When default permalinks are enabled, redirect memberships page to post type archive url.\n\tif ( ! empty( $_GET['page_id'] ) && get_option( 'permalink_structure' ) == '' && llms_get_page_id( 'memberships' ) == $_GET['page_id'] ) {\n\t\twp_safe_redirect( get_post_type_archive_link( 'llms_membership' ) );\n\t\texit;\n\t}\n}\nadd_action( 'template_redirect', 'llms_template_redirect' );\n\n/**\n * Title Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_title' ) ) {\n\n\tfunction lifterlms_template_single_title() {\n\n\t\tllms_get_template( 'course/title.php' );\n\t}\n}\n\n/**\n * Short Description Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_short_description' ) ) {\n\n\tfunction lifterlms_template_single_short_description() {\n\n\t\tllms_get_template( 'course/short-description.php' );\n\t}\n}\n\n/**\n * Course Content Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_course_content' ) ) {\n\n\tfunction lifterlms_template_single_course_content() {\n\t\tglobal $post;\n\t\t$page_restricted = llms_page_restricted( $post->ID );\n\n\t\tif ( $page_restricted['is_restricted'] ) {\n\t\t\tllms_get_template( 'course/short-description.php' );\n\t\t} else {\n\t\t\tllms_get_template( 'course/full-description.php' );\n\t\t}\n\t}\n}\n\n/**\n * Course Full Description Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_full_description' ) ) {\n\n\tfunction lifterlms_template_single_full_description() {\n\n\t\tllms_get_template( 'lesson/full-description.php' );\n\t}\n}\n\n/**\n * Membership Featured Image Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_membership_full_description' ) ) {\n\n\tfunction lifterlms_template_single_membership_full_description() {\n\n\t\tllms_get_template( 'membership/full-description.php' );\n\t}\n}\n\n/**\n * Add a course progress bar with a continue button\n *\n * @return   void\n * @since    3.0.1\n * @version  3.0.1\n */\nif ( ! function_exists( 'lifterlms_template_single_course_progress' ) ) {\n\tfunction lifterlms_template_single_course_progress() {\n\t\tllms_get_template( 'course/progress.php' );\n\t}\n}\n\n\n\n/**\n * Open the course meta information wrapper\n *\n * @since   3.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_template_single_meta_wrapper_start' ) ) {\n\tfunction lifterlms_template_single_meta_wrapper_start() {\n\t\tllms_get_template( 'course/meta-wrapper-start.php' );\n\t}\n}\n/**\n * Close the course meta information wrapper\n *\n * @since   3.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_template_single_meta_wrapper_end' ) ) {\n\tfunction lifterlms_template_single_meta_wrapper_end() {\n\t\tllms_get_template( 'course/meta-wrapper-end.php' );\n\t}\n}\n\n/**\n * Course Estimated Length Template\n * replaced 'lifterlms_template_single_lesson_length()' which was misnamed as being related to a lesson\n * when it was actually related to a course\n *\n * @return  void\n * @since   3.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_template_single_length' ) ) {\n\tfunction lifterlms_template_single_length() {\n\n\t\tllms_get_template( 'course/length.php' );\n\t}\n}\n\n/**\n * Display a list of course categories\n *\n * @return  void\n * @since   3.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_template_single_course_categories' ) ) {\n\tfunction lifterlms_template_single_course_categories() {\n\t\tllms_get_template( 'course/categories.php' );\n\t}\n}\n\n/**\n * Display a list of course tags\n *\n * @return  void\n * @since   3.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_template_single_course_tags' ) ) {\n\tfunction lifterlms_template_single_course_tags() {\n\t\tllms_get_template( 'course/tags.php' );\n\t}\n}\n\n/**\n * Display a list of course tracks\n *\n * @return  void\n * @since   3.0.0\n * @version 3.0.0\n */\nif ( ! function_exists( 'lifterlms_template_single_course_tracks' ) ) {\n\tfunction lifterlms_template_single_course_tracks() {\n\t\tllms_get_template( 'course/tracks.php' );\n\t}\n}\n\n/**\n * Course Video Embed Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_video' ) ) {\n\n\tfunction lifterlms_template_single_video() {\n\n\t\tllms_get_template( 'course/video.php' );\n\t}\n}\n\n/**\n * Membership Video Embed Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_membership_video' ) ) {\n\n\tfunction lifterlms_template_single_membership_video() {\n\n\t\tllms_get_template( 'membership/video.php' );\n\t}\n}\n\n\n/**\n * Lesson Video Embed Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_lesson_video' ) ) {\n\n\tfunction lifterlms_template_single_lesson_video() {\n\n\t\tllms_get_template( 'lesson/video.php' );\n\t}\n}\n\n/**\n * Course Audio Embed Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_audio' ) ) {\n\n\tfunction lifterlms_template_single_audio() {\n\n\t\tllms_get_template( 'course/audio.php' );\n\t}\n}\n\n/**\n * Membership Audio Embed Template Include.\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_membership_audio' ) ) {\n\n\tfunction lifterlms_template_single_membership_audio() {\n\n\t\tllms_get_template( 'membership/audio.php' );\n\t}\n}\n\n/**\n * Lesson Audio Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_lesson_audio' ) ) {\n\n\tfunction lifterlms_template_single_lesson_audio() {\n\n\t\tllms_get_template( 'lesson/audio.php' );\n\t}\n}\n\n/**\n * Course Difficulty Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_difficulty' ) ) {\n\n\tfunction lifterlms_template_single_difficulty() {\n\n\t\tllms_get_template( 'course/difficulty.php' );\n\t}\n}\n\n/**\n * Course Prerequisites Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_prerequisites' ) ) {\n\n\tfunction lifterlms_template_single_prerequisites() {\n\n\t\tglobal $post;\n\t\tllms_get_template(\n\t\t\t'course/prerequisites.php',\n\t\t\tarray(\n\t\t\t\t'course' => new LLMS_Course( $post ),\n\t\t\t)\n\t\t);\n\t}\n}\n\n/**\n * Course Syllabus Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_syllabus' ) ) {\n\n\tfunction lifterlms_template_single_syllabus() {\n\n\t\tllms_get_template( 'course/syllabus.php' );\n\t}\n}\n\n/**\n * Parent Course Link Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_parent_course' ) ) {\n\n\tfunction lifterlms_template_single_parent_course() {\n\n\t\tllms_get_template( 'course/parent-course.php' );\n\t}\n}\n\nif ( ! function_exists( 'llms_template_favorite' ) ) {\n\n\t/**\n\t * Favorite Lesson Template Include.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t * @param string $object_type The object type, currently only 'lesson'.\n\t * @return void\n\t */\n\tfunction llms_template_favorite( $object_id = null, $object_type = 'lesson' ) {\n\n\t\tllms()->assets->enqueue_script( 'llms-favorites' );\n\t\tllms_get_template(\n\t\t\t'course/favorite.php',\n\t\t\tarray(\n\t\t\t\t'object_id'   => $object_id,\n\t\t\t\t'object_type' => $object_type,\n\t\t\t)\n\t\t);\n\t}\n}\n\nif ( ! function_exists( 'llms_template_syllabus_favorite_lesson_preview' ) ) {\n\n\t/**\n\t * Favorite Lesson Template Include when displayed in the syllabus lesson preview.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @return void\n\t */\n\tfunction llms_template_syllabus_favorite_lesson_preview( $lesson ) {\n\t\tif ( 'course' === get_post_type( get_the_ID() ) ) {\n\t\t\tllms_template_favorite( $lesson->get( 'id' ) );\n\t\t}\n\t}\n}\n\n/**\n * Complete Lesson Link Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_complete_lesson_link' ) ) {\n\n\tfunction lifterlms_template_complete_lesson_link() {\n\t\tllms_get_template( 'course/complete-lesson-link.php' );\n\t}\n}\n\n/**\n * Lesson Navigation Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_lesson_navigation' ) ) {\n\n\tfunction lifterlms_template_lesson_navigation() {\n\n\t\tllms_get_template( 'course/lesson-navigation.php' );\n\t}\n}\n\n/**\n * Membership Title Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_template_single_membership_title' ) ) {\n\n\tfunction lifterlms_template_single_membership_title() {\n\n\t\tllms_get_template( 'membership/title.php' );\n\t}\n}\n\n\n\nif ( ! function_exists( 'lifterlms_get_content' ) ) {\n\n\tfunction lifterlms_get_content( $args ) {\n\n\t\tllms_get_template( 'content-single-question.php', $args );\n\t}\n}\n\n/**\n * When the_post is called, put course data into a global.\n *\n * @param mixed $post\n * @return LLMS_Course\n */\nfunction llms_setup_course_data( $post ) {\n\tif ( ! is_admin() ) {\n\n\t\tif ( $post && 'course' === $post->post_type ) {\n\n\t\t\tunset( $GLOBALS['course'] );\n\n\t\t\tif ( is_int( $post ) ) {\n\t\t\t\t$post = get_post( $post );\n\t\t\t}\n\n\t\t\tif ( empty( $post->post_type ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$GLOBALS['course'] = new LLMS_Course( $post );\n\n\t\t\treturn $GLOBALS['course'];\n\n\t\t}\n\t}\n}\nadd_action( 'the_post', 'llms_setup_course_data' );\n\n/**\n * When the_post is called, put lesson data into a global.\n *\n * @param mixed $post\n * @return LLMS_Course\n */\nfunction llms_setup_lesson_data( $post ) {\n\tif ( ! is_admin() ) {\n\n\t\tif ( 'lesson' == $post->post_type ) {\n\t\t\tunset( $GLOBALS['lesson'] );\n\n\t\t\tif ( is_int( $post ) ) {\n\t\t\t\t$post = get_post( $post ); }\n\n\t\t\tif ( empty( $post->post_type ) ) {\n\t\t\t\treturn; }\n\n\t\t\t$courseid = get_post_meta( $post->ID, '_llms_parent_course' );\n\n\t\t\tif ( isset( $courseid ) ) {\n\t\t\t\t$parent_course = get_post( $courseid );\n\t\t\t}\n\n\t\t\t$GLOBALS['lesson'] = get_lesson( $post );\n\n\t\t\tllms_setup_course_data( $parent_course );\n\n\t\t\treturn $GLOBALS['lesson'];\n\t\t}\n\t}\n}\nadd_action( 'the_post', 'llms_setup_lesson_data' );\n\n/**\n * Returns post array of data for sections associated with a course\n *\n * @param array\n * @return array\n */\nfunction get_section_data( $sections ) {\n\tglobal $post;\n\t$html = '';\n\t$args = array(\n\t\t'post_type'   => 'section',\n\t\t'post_status' => 'publish',\n\t\t'nopaging'    => true,\n\t);\n\n\t$sections_query = get_posts( $args );\n\n\t$array = array();\n\n\tforeach ( $sections as $key => $value ) :\n\n\t\tforeach ( $sections_query as $section ) :\n\n\t\t\tif ( $value == $section->ID ) {\n\t\t\t\t$array[ $section->ID ] = $section;\n\t\t\t}\n\n\t\tendforeach;\n\n\tendforeach;\n\n\treturn $array;\n}\n\n/**\n * Returns post array of data for lessons associated with a course\n *\n * @param array\n * @return array\n */\nfunction get_lesson_data( $lessons ) {\n\tglobal $post;\n\t$html = '';\n\t$args = array(\n\t\t'post_type'   => 'lesson',\n\t\t'post_status' => 'publish',\n\t\t'nopaging'    => true,\n\t);\n\n\t$lessons_query = get_posts( $args );\n\n\t$array = array();\n\n\tforeach ( $lessons as $key => $value ) :\n\n\t\tforeach ( $lessons_query as $lesson ) :\n\n\t\t\tif ( $value == $lesson->ID ) {\n\t\t\t\t$array[ $value ] = $lesson;\n\t\t\t}\n\n\t\tendforeach;\n\n\tendforeach;\n\n\treturn $array;\n}\n\n/**\n * Get Page Title\n *\n * @param  boolean $echo [echo string?]\n * @return string $page_title [page title]\n */\nif ( ! function_exists( 'lifterlms_page_title' ) ) {\n\n\tfunction lifterlms_page_title( $echo = true ) {\n\n\t\t$page_title = '';\n\n\t\tif ( is_search() ) {\n\t\t\t$page_title = sprintf( __( 'Search Results: &ldquo;%s&rdquo;', 'lifterlms' ), get_search_query() );\n\n\t\t\tif ( get_query_var( 'paged' ) ) {\n\t\t\t\t$page_title .= sprintf( __( '&nbsp;&ndash; Page %s', 'lifterlms' ), get_query_var( 'paged' ) );\n\t\t\t}\n\t\t} elseif ( is_tax() ) {\n\n\t\t\t$page_title = single_term_title( '', false );\n\n\t\t} elseif ( is_post_type_archive( 'course' ) ) {\n\n\t\t\t$page_title = get_the_title( llms_get_page_id( 'courses' ) );\n\n\t\t} elseif ( is_post_type_archive( 'llms_membership' ) ) {\n\n\t\t\t$page_title = get_the_title( llms_get_page_id( 'memberships' ) );\n\n\t\t}\n\n\t\t$page_title = apply_filters( 'lifterlms_page_title', $page_title );\n\n\t\tif ( $echo ) {\n\n\t\t\techo wp_kses_post( $page_title );\n\n\t\t} else {\n\n\t\t\treturn $page_title;\n\n\t\t}\n\t}\n}\n\n/**\n * Outputs the html for a progress bar\n *\n * @param    int     $progress  percent completion\n * @param    string   $link     permalink to link the button to, if false will output a span with no href\n * @param    bool     $button   output a button with the link\n * @param    bool     $echo     true will echo content, false will return it\n * @return   void|string\n * @since    1.0.0\n * @version  3.24.0\n */\nif ( ! function_exists( 'lifterlms_course_progress_bar' ) ) {\n\n\tfunction lifterlms_course_progress_bar( $progress, $link = false, $button = true, $echo = true ) {\n\n\t\t$progress = round( $progress, 2 );\n\n\t\t$tag  = ( $link ) ? 'a' : 'span';\n\t\t$href = ( $link ) ? ' href=\" ' . $link . ' \"' : '';\n\n\t\t$html = llms_get_progress_bar_html( $progress );\n\n\t\tif ( $button ) {\n\t\t\t$html .= '<' . $tag . ' class=\"llms-button-primary llms-purchase-button\"' . $href . '>' . __( 'Continue', 'lifterlms' ) . '(' . $progress . '%)</' . $tag . '>';\n\t\t}\n\n\t\tif ( $echo ) {\n\t\t\techo wp_kses_post( $html );\n\t\t} else {\n\t\t\treturn $html;\n\t\t}\n\t}\n}\n\nfunction llms_get_progress_bar_html( $percentage ) {\n\n\t$percentage = sprintf( '%s%%', $percentage );\n\n\t$html = '<div class=\"llms-progress\">\n\t\t<div class=\"progress__indicator\">' . $percentage . '</div>\n\t\t<div class=\"llms-progress-bar\">\n\t\t\t<div class=\"progress-bar-complete\" data-progress=\"' . $percentage . '\"  style=\"width:' . $percentage . '\"></div>\n\t\t</div></div>';\n\n\treturn $html;\n}\n\n\n/**\n * Output a course continue button linking to the incomplete lesson for a given student.\n *\n * If the course is complete \"Course Complete\" is displayed.\n *\n * @since 3.11.1\n * @since 3.15.0 Unknown.\n * @since 7.1.0 Remove check on student existence, now included in the enrollment check.\n *\n * @param int          $post_id  WP Post ID for a course, lesson, or quiz.\n * @param LLMS_Student $student  Instance of an LLMS_Student, defaults to current student.\n * @param int          $progress Current progress of the student through the course.\n * @return void\n */\nif ( ! function_exists( 'lifterlms_course_continue_button' ) ) {\n\n\tfunction lifterlms_course_continue_button( $post_id = null, $student = null, $progress = null ) {\n\n\t\tif ( ! $post_id ) {\n\t\t\t$post_id = get_the_ID();\n\t\t\tif ( ! $post_id ) {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t}\n\n\t\t$course = llms_get_post( $post_id );\n\t\tif ( ! $course || ! is_a( $course, 'LLMS_Post_Model' ) ) {\n\t\t\treturn '';\n\t\t}\n\t\tif ( in_array( $course->get( 'type' ), array( 'lesson', 'quiz' ) ) ) {\n\t\t\t$course = llms_get_post_parent_course( $course->get( 'id' ) );\n\t\t\tif ( ! $course ) {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t}\n\n\t\tif ( ! $student ) {\n\t\t\t$student = llms_get_student();\n\t\t}\n\t\tif ( ! $student || ! llms_is_user_enrolled( $student->get_id(), $course->get( 'id' ) ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tif ( is_null( $progress ) ) {\n\t\t\t$progress = $student->get_progress( $course->get( 'id' ), 'course' );\n\t\t}\n\n\t\tif ( 100 == $progress ) {\n\n\t\t\techo '<p class=\"llms-course-complete-text\">' . wp_kses_post( apply_filters( 'llms_course_continue_button_complete_text', __( 'Course Complete', 'lifterlms' ), $course ) ) . '</p>';\n\n\t\t} else {\n\n\t\t\t$lesson = apply_filters( 'llms_course_continue_button_next_lesson', $student->get_next_lesson( $course->get( 'id' ) ), $course, $student );\n\t\t\tif ( $lesson ) { ?>\n\n\t\t\t\t<a class=\"llms-button-primary llms-course-continue-button\" href=\"<?php echo esc_url( get_permalink( $lesson ) ); ?>\">\n\n\t\t\t\t\t<?php if ( 0 == $progress ) : ?>\n\n\t\t\t\t\t\t<?php esc_html_e( 'Get Started', 'lifterlms' ); ?>\n\n\t\t\t\t\t<?php else : ?>\n\n\t\t\t\t\t\t<?php esc_html_e( 'Continue', 'lifterlms' ); ?>\n\n\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t</a>\n\n\t\t\t\t<?php\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Course Thumbnail Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_get_course_thumbnail' ) ) {\n\n\tfunction lifterlms_get_course_thumbnail() {\n\t\tglobal $post;\n\n\t\tif ( has_post_thumbnail() ) {\n\n\t\t\treturn lifterlms_get_featured_image( $post->ID );\n\t\t} elseif ( llms_placeholder_img_src() ) {\n\t\t\treturn llms_placeholder_img( 'full' );\n\t\t}\n\t}\n}\n\n/**\n * Featured Image Template Include\n *\n * @return void\n */\nif ( ! function_exists( 'lifterlms_get_featured_image' ) ) {\n\n\tfunction lifterlms_get_featured_image( $post_id ) {\n\n\t\tif ( has_post_thumbnail( $post_id ) ) {\n\n\t\t\treturn llms_featured_img( $post_id, 'full' );\n\t\t} elseif ( llms_placeholder_img_src() ) {\n\n\t\t\treturn llms_placeholder_img();\n\t\t}\n\t}\n}\n\n/**\n * Get the placeholder image URL for courses\n *\n * @access public\n * @return string\n */\nfunction llms_placeholder_img_src() {\n\treturn apply_filters( 'lifterlms_placeholder_img_src', llms()->plugin_url() . '/assets/images/placeholder.png' );\n}\n\n/**\n * Get the placeholder image\n *\n * @access public\n * @return string\n */\nfunction llms_placeholder_img( $size = 'full' ) {\n\treturn apply_filters( 'lifterlms_placeholder_img', '<img src=\"' . esc_url( llms_placeholder_img_src() ) . '\" alt=\"placeholder\" class=\"llms-placeholder llms-featured-image wp-post-image\" />' );\n}\n\n/**\n * Get the featured image.\n *\n * @since unknown\n * @since 7.1.2 Fix bug when the featured image file is not available.\n *\n * @access public\n *\n * @param int|WP_Post  $post_id Post ID or WP_Post object.\n * @param string|int[] $size    Accepts any registered image size name, or an array of width and height values in pixels (in that order).\n * @return string\n */\nfunction llms_featured_img( $post_id, $size ) {\n\t$img  = wp_get_attachment_image_src( get_post_thumbnail_id( $post_id ), $size );\n\t$html = '';\n\n\tif ( isset( $img[0] ) ) {\n\t\t$html = '<img src=\"' . esc_url( $img[0] ) . '\" alt=\"' . esc_attr( get_the_title( $post_id ) ) . '\" class=\"llms-featured-image wp-post-image\">';\n\t}\n\n\t/**\n\t * Filters the featured image of a given LifterLMS post.\n\t *\n\t * @since unknown\n\t * @since 7.1.2 Added `$post_id` parameter.\n\t *\n\t * @param string      $html    HTML img element or empty string if the post has no thumbnail.\n\t * @param int|WP_Post $post_id Post ID or WP_Post object.\n\t */\n\treturn apply_filters( 'lifterlms_featured_img', $html, $post_id );\n}\n\n/**\n * Retrieve author name, avatar, and bio\n *\n * @param    array $args  arguments\n * @return   string\n * @since    3.0.0\n * @version  3.13.0\n */\nfunction llms_get_author( $args = array() ) {\n\n\t$args = wp_parse_args(\n\t\t$args,\n\t\tarray(\n\t\t\t'avatar'      => true,\n\t\t\t'avatar_size' => 96,\n\t\t\t'bio'         => false,\n\t\t\t'label'       => '',\n\t\t\t'user_id'     => get_the_author_meta( 'ID' ),\n\t\t)\n\t);\n\n\t$name = get_the_author_meta( 'display_name', $args['user_id'] );\n\n\tif ( $args['avatar'] ) {\n\t\t$img = get_avatar( $args['user_id'], $args['avatar_size'], apply_filters( 'lifterlms_author_avatar_placeholder', '' ), $name );\n\t} else {\n\t\t$img = '';\n\t}\n\n\t$img = apply_filters( 'llms_get_author_image', $img );\n\n\t$desc = '';\n\tif ( $args['bio'] ) {\n\t\t$desc = get_the_author_meta( 'description', $args['user_id'] );\n\t}\n\n\tob_start();\n\t?>\n\t<div class=\"llms-author\">\n\t\t<?php\n\t\t\t// Escaping, but allowing flexibility for the filter above.\n\t\t\techo wp_kses_post( $img );\n\t\t?>\n\t\t<span class=\"llms-author-info name\"><?php echo esc_html( $name ); ?></span>\n\t\t<?php if ( $args['label'] ) : ?>\n\t\t\t<span class=\"llms-author-info label\"><?php echo esc_html( $args['label'] ); ?></span>\n\t\t<?php endif; ?>\n\t\t<?php if ( $desc ) : ?>\n\t\t\t<p class=\"llms-author-info bio\"><?php echo wp_kses( $desc, wp_kses_allowed_html( 'user_description' ) ); ?></p>\n\t\t<?php endif; ?>\n\t</div>\n\t<?php\n\t$html = ob_get_clean();\n\n\treturn apply_filters( 'llms_get_author', $html );\n}\n\n/**\n * Global Content Wrapper Start Template\n *\n * @return [type] [description]\n */\nif ( ! function_exists( 'lifterlms_output_content_wrapper' ) ) {\n\n\tfunction lifterlms_output_content_wrapper() {\n\t\tllms_get_template( 'global/wrapper-start.php' );\n\t}\n}\n\n/**\n * Global Content Wrapper End Template\n *\n * @return [type] [description]\n */\nif ( ! function_exists( 'lifterlms_output_content_wrapper_end' ) ) {\n\n\tfunction lifterlms_output_content_wrapper_end() {\n\t\tllms_get_template( 'global/wrapper-end.php' );\n\t}\n}\n\n/**\n * Sidebar Template\n *\n * @return [type] [description]\n */\nif ( ! function_exists( 'lifterlms_get_sidebar' ) ) {\n\n\tfunction lifterlms_get_sidebar() {\n\t\tllms_get_template( 'global/sidebar.php' );\n\t}\n}\n\n\n\n\n/**\n * Get the link to the edit account details page\n *\n * @return string\n */\nfunction llms_person_edit_account_url() {\n\t$edit_account_url = llms_get_endpoint_url( 'edit-account', '', get_permalink( llms_get_page_id( 'myaccount' ) ) );\n\n\treturn apply_filters( 'lifterlms_person_edit_account_url', $edit_account_url );\n}\n\n/**\n * Get the link to the redeem voucher page\n *\n * @return string\n */\nfunction llms_person_redeem_voucher_url() {\n\n\t$url = llms_get_endpoint_url( 'redeem-voucher', '', get_permalink( llms_get_page_id( 'myaccount' ) ) );\n\n\treturn apply_filters( 'lifterlms_person_redeem_voucher_url', $url );\n}\n\n/**\n * Get the link to the My Courses endpoint\n *\n * @return string\n *\n * @since  3.0.0\n */\nfunction llms_person_my_courses_url() {\n\n\t$url = llms_get_endpoint_url( 'my-courses', '', get_permalink( llms_get_page_id( 'myaccount' ) ) );\n\n\treturn apply_filters( 'lifterlms_person_my_courses_url', $url );\n}\n\n\n/**\n * Get Product Query Var\n * REFACTOR: Move to query class\n *\n * @param  array $vars [array of query variables]\n * @return array $vars [array of query variables]\n */\nfunction get_product_query_var( $vars ) {\n\t$vars[] = 'product';\n\treturn $vars;\n}\nadd_filter( 'query_vars', 'get_product_query_var' );\n\n/**\n * Get available payment gateway options\n * Get's available payment gateways options IE: single, recurring\n *\n * @return void\n */\nfunction get_available_payment_options() {\n\n\t$_available_options = array();\n\t$option_prefix      = 'lifterlms_gateway_enable_';\n\t$options            = array(\n\t\t'paypal',\n\t);\n\n\tforeach ( $options as $option ) {\n\t\t$single_option = '';\n\n\t\t$single_option = get_option( $option_prefix . $option, 'no' );\n\n\t\tif ( 'yes' === $single_option ) {\n\n\t\t\t\tarray_push( $_available_options, $option );\n\t\t}\n\n\t\tllms_get_template( 'checkout/' . $option . '.php' );\n\n\t}\n}\n\n/**\n * Get Product Object\n *\n * @since Unknown\n * @since 3.37.13 Use `LLMS_Product` in favor of the deprecated `LLMS_Course_Factory::get_product()` method.\n *\n * @param WP_Post|int|false $the_product Course or membership post object or id. If `false` uses the global `$post` object.\n * @param array             $args        Arguments to pass to the LLMS_Product Constructor.\n * @return LLMS_Proudct\n */\nfunction llms_get_product( $the_product = false, $args = array() ) {\n\tif ( ! $the_product ) {\n\t\tglobal $post;\n\t\t$the_product = $post;\n\t}\n\treturn new LLMS_Product( $the_product, $args );\n}\n\n/**\n * Retrieve an excerpt\n *\n * @todo  deprecate this, I have no idea why this is being done this way...\n *\n * @param  int $post_id WordPress post id\n * @return string\n * @version  2.7.5\n */\nfunction llms_get_excerpt( $post_id ) {\n\tglobal $post;\n\n\t$temp = $post;\n\t$post = get_post( $post_id );\n\tsetup_postdata( $post );\n\n\t$excerpt = apply_filters( 'the_excerpt', $post->post_excerpt );\n\n\twp_reset_postdata();\n\t$post = $temp;\n\n\treturn $excerpt;\n}\n\n/**\n * Shuffles an array while keeping the array indices\n *\n * @param array $array\n *\n * @return bool\n */\nfunction llms_shuffle_assoc( &$array ) {\n\t$keys = array_keys( $array );\n\n\tshuffle( $keys );\n\n\tforeach ( $keys as $key ) {\n\t\t$new[ $key ] = $array[ $key ];\n\t}\n\n\t$array = $new;\n\n\treturn true;\n}\n\n/**\n * Get Image size for custom image sizes\n *\n * @param  string $name\n * @param  arrray $default\n * @return array\n */\nif ( ! function_exists( 'llms_get_image_size' ) ) {\n\tfunction llms_get_image_size( $name, $default = array() ) {\n\n\t\tglobal $_wp_additional_image_sizes;\n\n\t\tif ( isset( $_wp_additional_image_sizes[ $name ] ) ) {\n\t\t\treturn $_wp_additional_image_sizes[ $name ];\n\t\t}\n\n\t\treturn $default;\n\t}\n}\n\n/**\n * Add various css classes to LifterLMS post types when `post_class()` is called\n *\n * Succeeds now deprecated `llms_lesson_complete_classes()`.\n *\n * @param    array $classes  array of classes to be applied to the post element\n * @param    array $class    array of additional classes\n * @param    int   $post_id  WP Post ID\n * @return   array\n * @since    2.7.11\n * @version  3.0.0\n *\n * @todo  add additional classes based on course/lesson availability and whatnot\n */\nfunction llms_post_classes( $classes, $class = array(), $post_id = '' ) {\n\n\tif ( ! $post_id ) {\n\t\treturn $classes;\n\t}\n\n\t$post_type = get_post_type( $post_id );\n\n\t// Add enrolled classes.\n\tif ( 'lesson' === $post_type || 'course' === $post_type || 'llms_membership' === $post_type ) {\n\n\t\t$classes[] = llms_is_user_enrolled( get_current_user_id(), $post_id ) ? 'is-enrolled' : 'not-enrolled';\n\n\t}\n\n\t// Add completion classes.\n\tif ( 'lesson' === $post_type || 'course' === $post_type ) {\n\n\t\tif ( get_current_user_id() ) {\n\n\t\t\t$student   = new LLMS_Student();\n\t\t\t$classes[] = $student->is_complete( $post_id, $post_type ) ? 'is-complete' : 'is-incomplete';\n\n\t\t} else {\n\n\t\t\t$classes[] = 'is-complete';\n\n\t\t}\n\t}\n\n\treturn $classes;\n}\n\n/**\n * Output course reviews\n *\n * @return   void\n * @since    3.1.3\n * @version  3.1.3\n */\nif ( ! function_exists( 'lifterlms_template_single_reviews' ) ) {\n\tfunction lifterlms_template_single_reviews() {\n\t\tLLMS_Reviews::output();\n\t}\n}\n\n/**\n * Function to check if a post is built with Elementor\n *\n * @since 7.7.0\n */\nif ( ! function_exists( 'llms_is_elementor_post' ) ) {\n\tfunction llms_is_elementor_post( $post_id = false ) {\n\t\tif ( ! $post_id ) {\n\t\t\t$post_id = get_the_ID();\n\t\t}\n\t\tif ( ! $post_id ) {\n\t\t\treturn false;\n\t\t}\n\t\tif ( ! class_exists( 'Elementor\\Plugin' ) ) {\n\t\t\treturn false;\n\t\t}\n\t\t$elementor_plugin = Elementor\\Plugin::instance();\n\t\tif ( ! $elementor_plugin->documents || ! method_exists( $elementor_plugin->documents, 'get' ) ) {\n\t\t\treturn false;\n\t\t}\n\t\t$elementor_post = $elementor_plugin->documents->get( $post_id );\n\t\tif ( ! $elementor_post || ! method_exists( $elementor_post, 'is_built_with_elementor' ) ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn $elementor_post->is_built_with_elementor();\n\t}\n}\n\n\n/**\n * Function to check if a post is built with Beaver Builder\n *\n * @since 8.0.0\n */\nif ( ! function_exists( 'llms_is_beaver_builder_post' ) ) {\n\tfunction llms_is_beaver_builder_post( $post_id = false ) {\n\t\tif ( ! $post_id ) {\n\t\t\t$post_id = get_the_ID();\n\t\t}\n\t\treturn $post_id && class_exists( 'FLBuilderModel' ) && FLBuilderModel::is_builder_enabled( $post_id );\n\t}\n}\n"
  },
  {
    "path": "includes/llms.template.hooks.php",
    "content": "<?php\n/**\n * LifterLMS template hooks\n *\n * Defines all action hooks used by various templates.\n *\n * @package LifterLMS/Hooks\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Main content wrappers\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_before_main_content', 'lifterlms_output_content_wrapper', 10 );\nadd_action( 'lifterlms_after_main_content', 'lifterlms_output_content_wrapper_end', 10 );\n\n/**\n * Single Course\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_single_course_before_summary', 'lifterlms_template_single_video', 20 );\nadd_action( 'lifterlms_single_course_before_summary', 'lifterlms_template_single_audio', 30 );\n\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_start', 5 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_length', 10 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_difficulty', 20 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tracks', 25 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_categories', 30 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_tags', 35 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_course_author', 40 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_meta_wrapper_end', 50 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_prerequisites', 55 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_pricing_table', 60 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_course_progress', 60 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_syllabus', 90 );\nadd_action( 'lifterlms_single_course_after_summary', 'lifterlms_template_single_reviews', 100 );\n\n/**\n * Single Lesson\n *\n * @since Unknown\n * @since 7.5.0 Maybe add favorite template.\n */\nadd_action( 'lifterlms_single_lesson_before_summary', 'lifterlms_template_single_parent_course', 10 );\nif ( llms_is_favorites_enabled() ) {\n\tadd_action( 'lifterlms_single_lesson_before_summary', 'llms_template_favorite', 10 );\n}\nadd_action( 'lifterlms_single_lesson_before_summary', 'lifterlms_template_single_lesson_video', 20 );\nadd_action( 'lifterlms_single_lesson_before_summary', 'lifterlms_template_single_lesson_audio', 20 );\n\nadd_action( 'lifterlms_single_lesson_after_summary', 'lifterlms_template_complete_lesson_link', 10 );\nadd_action( 'lifterlms_single_lesson_after_summary', 'lifterlms_template_lesson_navigation', 20 );\n\n/**\n * Course & Membership Loops\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_archive_description', 'lifterlms_archive_description', 10 );\nadd_action( 'lifterlms_before_loop', 'lifterlms_loop_start', 10 );\nadd_action( 'lifterlms_loop', 'lifterlms_loop', 10 );\nadd_action( 'lifterlms_after_loop', 'lifterlms_loop_end', 10 );\n\n/**\n * Course & Membership Loop Items\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_before_loop_item', 'lifterlms_loop_featured_video', 8 );\n\nadd_action( 'lifterlms_before_loop_item', 'lifterlms_loop_link_start', 10 );\n\nadd_action( 'lifterlms_before_loop_item_title', 'lifterlms_template_loop_thumbnail', 10 );\nadd_action( 'lifterlms_before_loop_item_title', 'lifterlms_template_loop_progress', 15 );\n\nadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_author', 10 );\nadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_length', 15 );\nadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_difficulty', 20 );\nadd_action( 'lifterlms_after_loop_item_title', 'lifterlms_template_loop_lesson_count', 22 );\n\nadd_action( 'lifterlms_after_loop_item', 'lifterlms_template_loop_featured_pricing_information', 3 );\nadd_action( 'lifterlms_after_loop_item', 'lifterlms_loop_link_end', 5 );\n\n/**\n * Course Syllabus\n *\n * @since 7.5.0\n */\nif ( llms_is_favorites_enabled() ) {\n\tadd_action( 'llms_lesson_preview_after_title', 'llms_template_syllabus_favorite_lesson_preview', 10 );\n}\n\n/**\n * Emails\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_email_header', 'llms_email_header', 10, 1 );\nadd_action( 'lifterlms_email_body', 'llms_email_body', 10, 1 );\nadd_action( 'lifterlms_email_footer', 'llms_email_footer', 10 );\n\n/**\n * Pricing Tables\n *\n * @since Unknown\n * @since 3.38.0 Added `lifterlms_product_not_purchasable`.\n */\nadd_action( 'llms_access_plan', 'llms_template_access_plan', 10 );\n\nadd_action( 'llms_before_access_plan', 'llms_template_access_plan_feature', 10 );\n\nadd_action( 'llms_acces_plan_content', 'llms_template_access_plan_title', 10 );\nadd_action( 'llms_acces_plan_content', 'llms_template_access_plan_pricing', 20 );\nadd_action( 'llms_acces_plan_content', 'llms_template_access_plan_restrictions', 30 );\nadd_action( 'llms_acces_plan_content', 'llms_template_access_plan_description', 40 );\n\nadd_action( 'llms_acces_plan_footer', 'llms_template_access_plan_trial', 10 );\nadd_action( 'llms_acces_plan_footer', 'llms_template_access_plan_button', 20 );\n\nadd_action( 'lifterlms_product_not_purchasable', 'llms_template_product_not_purchasable', 10 );\n\n/**\n * Privacy\n *\n * @since Unknown\n */\nadd_action( 'llms_registration_privacy', 'llms_privacy_policy_form_field', 10 );\nadd_action( 'llms_registration_privacy', 'llms_agree_to_terms_form_field', 20 );\n\n/**\n * Quizzes\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_single_quiz_before_summary', 'lifterlms_template_quiz_wrapper_start', 5 );\nadd_action( 'lifterlms_single_quiz_before_summary', 'lifterlms_template_quiz_return_link', 10 );\nadd_action( 'lifterlms_single_quiz_before_summary', 'lifterlms_template_quiz_results', 15 );\n\nadd_action( 'llms_single_quiz_attempt_results', 'lifterlms_template_quiz_attempt_results', 10 );\nadd_action( 'llms_single_quiz_attempt_results_main', 'lifterlms_template_quiz_attempt_results_questions_list', 10 );\n\nadd_action( 'lifterlms_single_quiz_before_summary', 'lifterlms_template_quiz_meta_info', 25 );\n\nadd_action( 'lifterlms_single_quiz_after_summary', 'lifterlms_template_quiz_wrapper_end', 5 );\nadd_action( 'lifterlms_single_quiz_after_summary', 'lifterlms_template_start_button', 10 );\n\n/**\n * Questions\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_single_question_before_summary', 'lifterlms_template_question_wrapper_start', 10 );\n\nadd_action( 'lifterlms_single_question_content', 'lifterlms_template_question_description', 10 );\nadd_action( 'lifterlms_single_question_content', 'lifterlms_template_question_image', 20 );\nadd_action( 'lifterlms_single_question_content', 'lifterlms_template_question_video', 30 );\nadd_action( 'lifterlms_single_question_content', 'lifterlms_template_question_content', 40 );\n\nadd_action( 'lifterlms_single_question_after_summary', 'lifterlms_template_question_wrapper_end', 10 );\n\n/**\n * Student Dashboard\n *\n * @since 7.8.0\n */\nadd_action( 'lifterlms_before_student_dashboard', 'lifterlms_template_student_dashboard_wrapper_open', 10 );\n\nadd_action( 'lifterlms_before_student_dashboard_content', 'lifterlms_template_student_dashboard_navigation', 10 );\nadd_action( 'lifterlms_before_student_dashboard_content', 'lifterlms_template_student_dashboard_header', 20 );\n\nadd_action( 'lifterlms_student_dashboard_header', 'lifterlms_template_student_dashboard_title', 10 );\n\nadd_action( 'lifterlms_student_dashboard_index', 'lifterlms_template_student_dashboard_my_courses', 10 );\nadd_action( 'lifterlms_student_dashboard_index', 'lifterlms_template_student_dashboard_my_achievements', 20 );\nadd_action( 'lifterlms_student_dashboard_index', 'lifterlms_template_student_dashboard_my_certificates', 30 );\nadd_action( 'llms_achievement_content', 'llms_the_achievement', 10 );\nadd_action( 'llms_certificate_preview', 'llms_the_certificate_preview', 10 );\nadd_action( 'lifterlms_student_dashboard_index', 'lifterlms_template_student_dashboard_my_memberships', 40 );\n\nadd_action( 'llms_my_grades_course_table', 'lifterlms_template_student_dashboard_my_grades_table', 10, 2 );\n\nadd_action( 'llms_view_order_information', 'llms_template_view_order_information', 10 );\nadd_action( 'llms_view_order_actions', 'llms_template_view_order_actions', 10 );\nadd_action( 'llms_view_order_transactions', 'llms_template_view_order_transactions', 10, 2 );\n\nadd_action( 'lifterlms_after_student_dashboard', 'lifterlms_template_student_dashboard_wrapper_close', 10 );\n\n/**\n * Single Membership\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_single_membership_before_summary', 'lifterlms_template_single_membership_video', 20 );\nadd_action( 'lifterlms_single_membership_before_summary', 'lifterlms_template_single_membership_audio', 30 );\n\nadd_action( 'lifterlms_single_membership_after_summary', 'lifterlms_template_pricing_table', 10 );\n\n/**\n * Sidebar\n *\n * LifterLMS *does not* automatically output sidebars on course\n * and membership catalogs.\n *\n * But there is a \"stub\" that themes (or other plugins) can utilize\n * in order to add a sidebar to the catalogs.\n *\n * @since Unknown\n */\nadd_action( 'lifterlms_sidebar', 'lifterlms_get_sidebar', 10 );\n\n/**\n * Single Certificate\n *\n * @since 6.0.0\n */\nadd_action( 'wp_head', 'llms_certificate_styles' );\nadd_action( 'llms_display_certificate', 'llms_certificate_content', 10 );\nadd_action( 'llms_display_certificate', 'llms_certificate_actions', 20 );\n\nif ( ! is_admin() ) {\n\tadd_filter( 'post_class', 'llms_post_classes', 20, 3 );\n}\n"
  },
  {
    "path": "includes/models/class-llms-event.php",
    "content": "<?php\n/**\n * LifterLMS Event Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.36.0\n * @version 4.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Event Model\n *\n * @since 3.36.0\n * @since 4.3.0 Added record `$type` property definition.\n */\nclass LLMS_Event extends LLMS_Abstract_Database_Store {\n\n\t/**\n\t * Array of table column name => format\n\t *\n\t * @var  array\n\t */\n\tprotected $columns = array(\n\t\t'date'         => '%s',\n\t\t'actor_id'     => '%d',\n\t\t'object_type'  => '%s',\n\t\t'object_id'    => '%d',\n\t\t'event_type'   => '%s',\n\t\t'event_action' => '%s',\n\t\t'meta'         => '%s',\n\t);\n\n\t/**\n\t * Created date key name.\n\t *\n\t * @var string\n\t */\n\tprotected $date_created = 'date';\n\n\t/**\n\t * Updated date not supported.\n\t *\n\t * @var null\n\t */\n\tprotected $date_updated = null;\n\n\t/**\n\t * Database Table Name\n\t *\n\t * @var  string\n\t */\n\tprotected $table = 'events';\n\n\t/**\n\t * The record type\n\t *\n\t * @var string\n\t */\n\tprotected $type = 'event';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param int  $id Event ID.\n\t * @param bool $hydrate If true, hydrates the object on instantiation if an ID is supplied.\n\t */\n\tpublic function __construct( $id = null, $hydrate = false ) {\n\n\t\t$this->id = $id;\n\t\tif ( $this->id && $hydrate ) {\n\t\t\t$this->hydrate();\n\t\t}\n\n\t\t// Adds created and updated dates on instantiation.\n\t\tparent::__construct();\n\n\t}\n\n\t/**\n\t * Delete meta data\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param string $key Meta key, if omitted deletes *all* metadata.\n\t * @param bool   $save If true, saves updated metadata to the database.\n\t * @return LLMS_Event\n\t */\n\tpublic function delete_meta( $key = null, $save = false ) {\n\n\t\tif ( ! $key ) {\n\t\t\treturn $this->set_unencoded_metas( array(), $save );\n\t\t}\n\n\t\t$all = $this->get_meta( null, false );\n\t\tunset( $all[ $key ] );\n\t\treturn $this->set_unencoded_metas( $all, $save );\n\n\t}\n\n\t/**\n\t * Retrieve metadata.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param string $key Metadata key, if omitted returns an associative array of all metadata as key=>val pairs.\n\t * @param bool   $cache If true, uses cached data when available.\n\t * @return mixed\n\t */\n\tpublic function get_meta( $key = null, $cache = true ) {\n\n\t\t$all = $this->get( 'meta', $cache );\n\t\t$all = empty( $all ) ? array() : json_decode( $all, true );\n\n\t\tif ( ! $key ) {\n\t\t\treturn $all;\n\t\t}\n\n\t\treturn isset( $all[ $key ] ) ? $all[ $key ] : null;\n\n\t}\n\n\t/**\n\t * Update/Add a single meta item.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param string $key Meta key.\n\t * @param mixed  $val Meta value.\n\t * @param bool   $save If true, saves the updated metadata to the database.\n\t * @return LLMS_Event\n\t */\n\tpublic function set_meta( $key, $val, $save = false ) {\n\n\t\t$all         = $this->get_meta();\n\t\t$all[ $key ] = $val;\n\t\treturn $this->set_unencoded_metas( $all, $save );\n\n\t}\n\n\t/**\n\t * Update/Add multiple metas.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param array $metas Associative array of metadata to update/add as key=>val pairs.\n\t * @param bool  $save If true, saves the updated metadata to the database.\n\t * @return LLMS_Event\n\t */\n\tpublic function set_metas( $metas, $save = false ) {\n\n\t\tforeach ( $metas as $key => $val ) {\n\t\t\t$this->set_meta( $key, $val );\n\t\t}\n\n\t\tif ( $save ) {\n\t\t\t$this->save();\n\t\t}\n\n\t\treturn $this;\n\n\t}\n\n\t/**\n\t * Encode the array of metadata before setting it to the object.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @param array $metas Associative array of metadata to update/add as key=>val pairs.\n\t * @param bool  $save If true, saves the updated metadata to the database.\n\t * @return LLMS_Event\n\t */\n\tprotected function set_unencoded_metas( $metas, $save = false ) {\n\t\treturn $this->set( 'meta', wp_json_encode( $metas ), $save );\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "includes/models/model.llms.access.plan.php",
    "content": "<?php\n/**\n * LifterLMS Access Plan Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.0.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Access_Plan Model.\n *\n * @property  $access_expiration  (string)  Expiration type [lifetime|limited-period|limited-date]\n * @property  $access_expires  (string)  Date access expires in m/d/Y format. Only applicable when $access_expiration is \"limited-date\"\n * @property  $access_length  (int)  Length of access from time of purchase, combine with $access_period. Only applicable when $access_expiration is \"limited-period\"\n * @property  $access_period  (string)  Time period of access from time of purchase, combine with $access_length. Only applicable when $access_expiration is \"limited-period\" [year|month|week|day]\n * @property  $availability  (string)  Determine if this access plan is available to anyone or to members only. Use with $availability_restrictions to determine if the member can use the access plan. [open|members]\n * @property  $availability_restrictions (array)  Indexed array of LifterLMS Membership IDs a user must belong to to use the access plan. Only applicable if $availability is \"members\".\n * @property  $content  (string)  Plan description (post_content)\n * @property  $checkout_redirect_forced (string) On a members' only access plan, whether to force redirect users back to course after checking out the membership.\n * @property  $checkout_redirect_type (string) Type of checkout redirection [self|page|url]\n * @property  $checkout_redirect_page (int) Page to redirect to after checkout\n * @property  $checkout_redirect_url (string) URL to redirect to after checkout\n * @property  $enroll_text  (string)  Text to display on buy buttons\n * @property  $frequency  (int)  Frequency of billing. 0 = a one-time payment [0-6]\n * @property  $id  (int)  Post ID\n * @property  $is_free  (string)  Whether or not the plan requires payment [yes|no]\n * @property  $length  (int)  Number of intervals to run payment for, combine with $period & $frequency. 0 = forever / until cancelled. Only applicable if $frequency is not 0.\n * @property  $menu_order  (int)  Order to display access plans in when listing them. Displayed in ascending order.\n * @property  $on_sale  (string)  Enable or disable plan sale pricing [yes|no]\n * @property  $period  (string)  Interval period, combine with $length. Only applicable if $frequency is not 0.  [year|month|week|day]\n * @property  $price  (float)  Price per charge\n * @property  $product_id  (int)  WP Post ID of the related LifterLMS Product (course or membership)\n * @property  $sale_end  (string)  Date when the sale pricing ends\n * @property  $sale_start (string)  Date when the sale pricing begins\n * @property  $sale_price (float)  Sale price\n * @property  $sku  (string)  Short user-created plan identifier\n * @property  $title  (string)  Plan title\n * @property  $trial_length  (int)  length of the trial period. Only applicable if $trial_offer is \"yes\"\n * @property  $trial_offer  (string)  Enable or disable a plan trial period. [yes|no]\n * @property  $trial_period  (string)  Period for the trial period. Only applicable if $trial_offer is \"yes\". [year|month|week|day]\n * @property  $trial_price  (float)  Price for the trial period. Can be 0 for a free trial period\n *\n * @since 3.0.0\n * @since 3.30.0 Added checkout redirect properties and methods\n * @since 3.30.1 Added method to get the initial price due on checkout.\n * @since 3.31.0 The `$check_availability` parameter was added to the `llms_plan_get_checkout_url` filter.\n */\nclass LLMS_Access_Plan extends LLMS_Post_Model {\n\n\t/**\n\t * Map of meta properties => type.\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\t\t'access_expiration'         => 'string',\n\t\t'access_expires'            => 'string',\n\t\t'access_length'             => 'absint',\n\t\t'access_period'             => 'string',\n\t\t'availability'              => 'string',\n\t\t'availability_restrictions' => 'array',\n\t\t'content'                   => 'html',\n\t\t'checkout_redirect_forced'  => 'yesno',\n\t\t'checkout_redirect_type'    => 'string',\n\t\t'checkout_redirect_page'    => 'absint',\n\t\t'checkout_redirect_url'     => 'string',\n\t\t'enroll_text'               => 'string',\n\t\t'frequency'                 => 'absint',\n\t\t'is_free'                   => 'yesno',\n\t\t'length'                    => 'absint',\n\t\t'menu_order'                => 'absint',\n\t\t'on_sale'                   => 'yesno',\n\t\t'period'                    => 'string',\n\t\t'price'                     => 'float',\n\t\t'product_id'                => 'absint',\n\t\t'sale_end'                  => 'string',\n\t\t'sale_start'                => 'string',\n\t\t'sale_price'                => 'float',\n\t\t'sku'                       => 'string',\n\t\t'title'                     => 'string',\n\t\t'trial_length'              => 'absint',\n\t\t'trial_offer'               => 'yesno',\n\t\t'trial_period'              => 'string',\n\t\t'trial_price'               => 'float',\n\t);\n\n\t/**\n\t * Post Type name\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_access_plan';\n\n\t/**\n\t * Name of the model.\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'access_plan';\n\n\t/**\n\t * Determine if the access plan has expiration settings\n\t *\n\t * @since   3.0.0\n\t * @version 3.0.0\n\t * @return  boolean     true if it can expire, false if it's for lifetime access\n\t */\n\tpublic function can_expire() {\n\t\treturn ( 'lifetime' !== $this->get( 'access_expiration' ) );\n\t}\n\n\t/**\n\t * Calculate redirection url from settings\n\t *\n\t * @param    string The redirection type: self, page or url.\n\t * @return   string\n\t * @since    3.30.0\n\t * @version  3.30.0\n\t */\n\tprivate function calculate_redirection_url( $redirect_type ) {\n\n\t\t$available = $this->is_available_to_user( get_current_user_id() );\n\n\t\tif ( ! $available && 'no' === $this->get( 'checkout_redirect_forced' ) ) {\n\t\t\t$redirect_type = 'membership';\n\t\t}\n\n\t\t// by default, no special redirection is needed.\n\t\t$redirection = '';\n\n\t\tswitch ( $redirect_type ) {\n\n\t\t\t// redirect to itself.\n\t\t\tcase 'self':\n\t\t\t\t/**\n\t\t\t\t * Only set up when it is a member's only access plan with forced redirection to course.\n\t\t\t\t * This will ensure that on a regular access plan, no special parameter is added to querystring.\n\t\t\t\t * At the same time, if it is a members' only access plan,\n\t\t\t\t * after membership checkout we'd like to force redirect to course\n\t\t\t\t */\n\t\t\t\tif ( ! $available && llms_parse_bool( $this->get( 'checkout_redirect_forced' ) ) ) {\n\t\t\t\t\t$redirection = get_permalink( $this->get( 'product_id' ) );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'page':\n\t\t\t\t$redirection = get_permalink( $this->get( 'checkout_redirect_page' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase 'url':\n\t\t\t\t$redirection = $this->get( 'checkout_redirect_url' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $redirection;\n\t}\n\n\t/**\n\t * Get the translated and pluralized name of the plan's access period\n\t *\n\t * @since 3.4.6\n\t * @since 3.23.0 Unknown.\n\t * @since 5.3.0 Use llms_get_time_period_l10n().\n\t *\n\t * @param string $period Untranslated access period, if not supplied uses stored value for the plan.\n\t * @param int    $length Access length (for pluralization), if not supplied uses stored value for the plan.\n\t * @return string\n\t */\n\tpublic function get_access_period_name( $period = null, $length = null ) {\n\n\t\t$period = $period ? $period : $this->get( 'access_period' );\n\t\t$length = $length ? $length : $this->get( 'access_length' );\n\n\t\t$period = llms_get_time_period_l10n( $period, $length );\n\n\t\t/**\n\t\t * Filter the translated name of an access plan's billing period.\n\t\t *\n\t\t * @since 3.4.6\n\t\t * @version 3.4.6\n\t\t *\n\t\t * @param string $period Translated period name.\n\t\t * @param int $length Access length, used for pluralization.\n\t\t * @param LLMS_Access_Plan $this Access plan instance.\n\t\t */\n\t\treturn apply_filters( 'llms_plan_get_access_period_name', $period, $length, $this );\n\t}\n\n\n\t/**\n\t * Default arguments for creating a new post\n\t *\n\t * @since  3.0.0\n\t * @version  3.0.0\n\t *\n\t * @param  string $title   Title to create the post with\n\t * @return array\n\t */\n\tprotected function get_creation_args( $title = '' ) {\n\n\t\treturn array_merge(\n\t\t\tparent::get_creation_args( $title ),\n\t\t\tarray(\n\t\t\t\t'post_status' => 'publish',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the full URL to redirect to after successful checkout.\n\t *\n\t * @since 3.30.0\n\t * @since 7.0.0 Addeded `$encode` and `$querystring_only` parameters.\n\t *\n\t * @param bool $encode           Whether or not encoding the URL.\n\t * @param bool $querystring_only Only return the redirect URL bassed by the querystring.\n\t * @return string\n\t */\n\tpublic function get_redirection_url( $encode = true, $querystring_only = false ) {\n\n\t\t// What type of redirection is set up by user?\n\t\t$redirect_type = $this->get( 'checkout_redirect_type' );\n\n\t\t// Force redirect querystring parameter over all else.\n\t\t$redirection = llms_filter_input( INPUT_GET, 'redirect', FILTER_VALIDATE_URL ) ?? '';\n\n\t\tif ( ! $redirection && ! $querystring_only ) {\n\t\t\t$redirection = $this->calculate_redirection_url( $redirect_type );\n\t\t}\n\n\t\t/**\n\t\t * Filter the checkout redirection parameter.\n\t\t *\n\t\t * @since 3.30.0\n\t\t * @since 7.0.0 Added `$querystring_only` parameter.\n\t\t *\n\t\t * @param string            $redirection      The calculated url to redirect to.\n\t\t * @param string            $redirection_type Available redirection types 'self', 'membership', 'page', 'url' or a custom type.\n\t\t * @param LLMS_Acccess_Plan $access_plan      Current Access Plan object.\n\t\t * @param bool              $querystring_only Whether or not it was requested to only return the redirect URL passed by querystring.\n\t\t */\n\t\t$redirection = apply_filters( 'llms_plan_get_checkout_redirection', $redirection, $redirect_type, $this, $querystring_only );\n\n\t\treturn $encode ? urlencode( $redirection ) : $redirection;\n\t}\n\n\t/**\n\t * Retrieve the full URL to the checkout screen for the plan.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.0 Added access plan redirection settings.\n\t * @since 3.31.0 The `$check_availability` parameter was added to the filter `llms_plan_get_checkout_url`\n\t * @since 7.0.0 No need to add the redirect querystring parameter if not already set, except for unavailable members only plans.\n\t *\n\t * @param bool $check_availability Determine if availability checks should be made (allows retrieving plans on admin panel).\n\t * @return string\n\t */\n\tpublic function get_checkout_url( $check_availability = true ) {\n\n\t\t$ret       = '#llms-plan-locked';\n\t\t$available = $this->is_available_to_user( get_current_user_id() );\n\n\t\t// if bypassing availability checks OR plan is available to user.\n\t\tif ( ! $check_availability || $available ) {\n\n\t\t\t$ret = '';\n\n\t\t\tif ( llms_get_page_id( 'checkout' ) > 0 ) {\n\t\t\t\t$ret_params  = array(\n\t\t\t\t\t'plan' => $this->get( 'id' ),\n\t\t\t\t);\n\t\t\t\t$redirection = $this->get_redirection_url( true, true );\n\t\t\t\tif ( $redirection ) {\n\t\t\t\t\t$ret_params['redirect'] = $redirection;\n\t\t\t\t}\n\n\t\t\t\t$ret = llms_get_page_url( 'checkout', $ret_params );\n\t\t\t}\n\n\t\t\t// not available to user -- this is a member's only plan.\n\t\t} elseif ( ! $available ) {\n\n\t\t\t$memberships = $this->get_array( 'availability_restrictions' );\n\n\t\t\t// if there's only 1 plan associated with the membership return that url.\n\t\t\tif ( 1 === count( $memberships ) ) {\n\t\t\t\t$ret         = get_permalink( $memberships[0] );\n\t\t\t\t$redirection = $this->get_redirection_url();\n\n\t\t\t\tif ( $redirection ) {\n\t\t\t\t\t$ret = add_query_arg(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'redirect' => $redirection,\n\t\t\t\t\t\t),\n\t\t\t\t\t\t$ret\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter the checkout URL for an access plan.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 3.31.0 The `$check_availability` parameter was added.\n\t\t *\n\t\t * @param string $ret      The checkout URL.\n\t\t * @param LLMS_Access_Plan $this Access plan object.\n\t\t * @param bool             $check_availability Determine if availability checks should be made.\n\t\t *                                             (allows retrieving plans on admin panel)\n\t\t */\n\t\treturn apply_filters( 'llms_plan_get_checkout_url', $ret, $this, $check_availability );\n\t}\n\n\t/**\n\t * Get the initial price due on checkout.\n\t *\n\t * Automatically accounts for Trials, sales, and coupon discounts.\n\t *\n\t * @since 3.30.1\n\t * @since 3.40.0 Simplify logic by using new 4th argument ($coupon) of the `get_price()` method.\n\t *\n\t * @param array                $price_args Arguments passed to the price getter function to generate the price.\n\t * @param LLMS_Coupon|int|null $coupon     Coupon ID, object, or `null` if no coupon is being used.\n\t * @param string               $format     Format the price to be returned. Options: html, raw, float (default).\n\t * @return mixed\n\t */\n\tpublic function get_initial_price( $price_args = array(), $coupon = null, $format = 'float' ) {\n\n\t\t// If it's free it's a bit simpler.\n\t\tif ( $this->is_free() ) {\n\n\t\t\t$ret = $this->get_free_pricing_text( $format );\n\n\t\t} else {\n\n\t\t\t$price_key = 'price';\n\n\t\t\t// Setup the price key name based on the presence of a trial or sale.\n\t\t\tif ( $this->has_trial() ) {\n\t\t\t\t$price_key = 'trial_price';\n\t\t\t} elseif ( $this->is_on_sale() ) {\n\t\t\t\t$price_key = 'sale_price';\n\t\t\t}\n\n\t\t\t$ret = $this->get_price( $price_key, $price_args, $format, $coupon );\n\n\t\t}\n\n\t\t/**\n\t\t * Filter an access plan's initial price due on checkout.\n\t\t *\n\t\t * @since 3.30.1\n\t\t *\n\t\t * @param mixed                $ret        Price due on checkout.\n\t\t * @param array                $price_args Arguments passed to the price getter function to generate the price.\n\t\t * @param LLMS_Coupon|int|null $coupon     Coupon ID, object, or `null` if no coupon is being used.\n\t\t * @param string               $format     Format the price to be returned. Options: html, raw, float (default).\n\t\t * @param LLMS_Access_Plan     $this       Access Plan object.\n\t\t */\n\t\treturn apply_filters( 'llms_access_plan_get_initial_price', $ret, $price_args, $coupon, $format, $this );\n\t}\n\n\t/**\n\t * Get a string to use for 0 dollar amount prices rather than 0\n\t *\n\t * @param   string $format format to display the price in\n\t * @return  string\n\t * @since   3.0.0\n\t * @version 3.0.0\n\t */\n\tpublic function get_free_pricing_text( $format = 'html' ) {\n\t\t$text = __( 'FREE', 'lifterlms' );\n\n\t\tif ( 'html' === $format ) {\n\t\t\t$text = '<span class=\"lifterlms-price\">' . $text . '</span>';\n\t\t} elseif ( 'float' === $format ) {\n\t\t\t$text = 0.00;\n\t\t}\n\n\t\t/**\n\t\t * Filter the text displayed when a plan has no price.\n\t\t *\n\t\t * @since   3.0.0\n\t\t * @version 3.0.0\n\t\t *\n\t\t * @param string $text Displayed text.\n\t\t * @param LLMS_Access_Plan $this The access plan instance.\n\t\t */\n\t\treturn apply_filters( \"llms_get_free_{$this->model_post_type}_pricing_text\", $text, $this );\n\t}\n\n\t/**\n\t * Getter for price strings with optional formatting options\n\t *\n\t * @since 3.0.0\n\t * @since 3.23.0 Unknown.\n\t * @since 3.40.0 Added `$coupon` parameter.\n\t *\n\t * @param string               $key        Property key.\n\t * @param array                $price_args Optional array of arguments that can be passed to `llms_price()`.\n\t * @param string               $format     Optional format conversion method [html|raw|float].\n\t * @param LLMS_Coupon|int|null $coupon     Coupon ID, object, or `null` if no coupon is being used.\n\t * @return mixed\n\t */\n\tpublic function get_price( $key, $price_args = array(), $format = 'html', $coupon = null ) {\n\n\t\tif ( $coupon ) {\n\t\t\treturn $this->get_price_with_coupon( $key, $coupon, $price_args, $format );\n\t\t}\n\n\t\t$price = $this->get( $key );\n\n\t\tif ( $price <= 0 ) {\n\n\t\t\t$ret = $this->get_free_pricing_text( $format );\n\n\t\t} else {\n\n\t\t\t$ret = parent::get_price( $key, $price_args, $format );\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the access plan's price.\n\t\t *\n\t\t * @since 3.40.0\n\t\t *\n\t\t * @param mixed            $ret        Returned price.\n\t\t * @param string           $key        The key of the price property.\n\t\t * @param array            $price_args Price arguments.\n\t\t * @param string           $format     Price format string.\n\t\t * @param LLMS_Access_Plan $this       Instance of the access plan.\n\t\t */\n\t\treturn apply_filters( 'llms_plan_get_price', $ret, $key, $price_args, $format, $this );\n\t}\n\n\t/**\n\t * Apply a coupon to a price\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.0 Unknown.\n\t * @since 3.40.0 Use `wp_strip_all_tags()` in favor of `strip_tags()`.\n\t *\n\t * @param string          $key        Price to retrieve, \"price\", \"sale_price\", or \"trial_price\".\n\t * @param LLMS_Coupon|int $coupon_id  Coupon object or post id.\n\t * @param array           $price_args Optional arguments to be passed to `llms_price()`.\n\t * @param string          $format     Optional return format as passed to `llms_price()`.\n\t * @return mixed\n\t */\n\tpublic function get_price_with_coupon( $key, $coupon_id, $price_args = array(), $format = 'html' ) {\n\n\t\t// Allow id or instance to be passed for $coupon_id.\n\t\tif ( $coupon_id instanceof LLMS_Coupon ) {\n\t\t\t$coupon = $coupon_id;\n\t\t} else {\n\t\t\t$coupon = new LLMS_Coupon( $coupon_id );\n\t\t}\n\n\t\t$price = $this->get( $key );\n\n\t\t// Ensure the coupon *can* be applied to this plan.\n\t\tif ( ! $coupon->is_valid( $this ) ) {\n\t\t\treturn $price;\n\t\t}\n\n\t\t$discount_type = $coupon->get( 'discount_type' );\n\n\t\t// Price and sale price are calculated of coupon amount.\n\t\tif ( 'price' === $key || 'sale_price' === $key ) {\n\n\t\t\t$coupon_amount = $coupon->get( 'coupon_amount' );\n\n\t\t} elseif ( 'trial_price' === $key && $coupon->has_trial_discount() && $this->has_trial() ) {\n\n\t\t\t$coupon_amount = $coupon->get( 'trial_amount' );\n\n\t\t} else {\n\n\t\t\t$coupon_amount = 0;\n\n\t\t}\n\n\t\tif ( $coupon_amount ) {\n\n\t\t\t// Simple subtraction.\n\t\t\tif ( 'dollar' === $discount_type ) {\n\t\t\t\t$price = $price - $coupon_amount;\n\t\t\t} elseif ( 'percent' === $discount_type ) {\n\t\t\t\t$price = $price - ( $price * ( $coupon_amount / 100 ) );\n\t\t\t}\n\t\t}\n\t\t/**\n\t\t * Filter the price of a plan with a coupon applied before formatting the price for display.\n\t\t *\n\t\t * @since 7.8.0\n\t\t */\n\t\t$price = apply_filters( \"llms_get_{$this->model_post_type}_{$key}_price_with_coupon_before_formatting\", $price, $key, $price_args, $this );\n\n\t\t// If price is less than 0 return the pricing text.\n\t\tif ( $price <= 0 ) {\n\n\t\t\t$price = $this->get_free_pricing_text( $format );\n\n\t\t} elseif ( 'html' === $format || 'raw' === $format ) {\n\t\t\t$price = llms_price( $price, $price_args );\n\t\t\tif ( 'raw' === $format ) {\n\t\t\t\t$price = wp_strip_all_tags( $price );\n\t\t\t}\n\t\t} elseif ( 'float' === $format ) {\n\t\t\t$price = floatval( number_format( $price, get_lifterlms_decimals(), '.', '' ) );\n\t\t} else {\n\t\t\t$price = apply_filters( \"llms_get_{$this->model_post_type}_{$key}_{$format}_with_coupon\", $price, $key, $price_args, $format, $this );\n\t\t}\n\n\t\treturn apply_filters( \"llms_get_{$this->model_post_type}_{$key}_price_with_coupon\", $price, $key, $price_args, $format, $this );\n\t}\n\n\t/**\n\t * Retrieve an instance of the associated LLMS_Product\n\t *\n\t * @return   obj\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function get_product() {\n\t\treturn new LLMS_Product( $this->get( 'product_id' ) );\n\t}\n\n\t/**\n\t * Retrieve the product type (course or membership) for the associated product\n\t *\n\t * @return   string\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function get_product_type() {\n\t\t$product = $this->get_product();\n\t\treturn str_replace( 'llms_', '', $product->get( 'type' ) );\n\t}\n\n\t/**\n\t * Retrieve the text displayed on \"Buy\" buttons\n\t * Uses optional user submitted text and falls back to LifterLMS defaults if none is supplied\n\t *\n\t * @param    boolean $verbose  If true, the text will be verbose and include the plan name for accessibility.\n\t * @return   string\n\t * @since    3.0.0\n\t * @since    8.0.0 Added $verbose parameter.\n\t * @version  8.0.0\n\t */\n\tpublic function get_enroll_text( $verbose = false ) {\n\n\t\t// User custom text option.\n\t\t$text = $this->get( 'enroll_text' );\n\n\t\tif ( ! $text ) {\n\n\t\t\tswitch ( $this->get_product_type() ) {\n\n\t\t\t\tcase 'course':\n\t\t\t\t\t$text = apply_filters( 'llms_course_enroll_button_text', __( 'Enroll', 'lifterlms' ), $this );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'membership':\n\t\t\t\t\t$text = apply_filters( 'llms_membership_enroll_button_text', __( 'Join', 'lifterlms' ), $this );\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\t\t}\n\n\t\t// Build the verbose enroll text, if requested.\n\t\tif ( $verbose ) {\n\t\t\t$plan_name = $this->get( 'title' );\n\t\t\t$text      = sprintf( _x( '%1$s: Select the %2$s plan.', 'Verbose enrollment text', 'lifterlms' ), $text, $plan_name );\n\t\t}\n\n\t\treturn apply_filters( 'llms_plan_get_enroll_text', $text, $this, $verbose );\n\t}\n\n\t/**\n\t * Get a sentence explaining plan expiration details\n\t *\n\t * @return   string\n\t * @since    3.0.0\n\t * @version  3.28.2\n\t */\n\tpublic function get_expiration_details() {\n\n\t\t$ret = '';\n\n\t\t$expiration = $this->get( 'access_expiration' );\n\t\tif ( 'limited-date' === $expiration ) {\n\t\t\t$ret = sprintf( _x( 'access until %s', 'Access expiration date', 'lifterlms' ), $this->get_date( 'access_expires' ) );\n\t\t} elseif ( 'limited-period' === $expiration ) {\n\t\t\t$ret = sprintf( _x( '%1$d %2$s of access', 'Access period description', 'lifterlms' ), $this->get( 'access_length' ), $this->get_access_period_name() );\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_product_expiration_details', $ret, $this );\n\t}\n\n\t/**\n\t * Get a sentence explaining the plan's payment schedule\n\t *\n\t * @return   string\n\t * @since    3.0.0\n\t * @version  3.23.0\n\t */\n\tpublic function get_schedule_details() {\n\n\t\t$ret = '';\n\n\t\t$period    = $this->get( 'period' );\n\t\t$frequency = $this->get( 'frequency' );\n\t\t$length    = $this->get( 'length' );\n\n\t\t// One-time payments don't display anything here unless filtered.\n\t\tif ( $frequency > 0 ) {\n\n\t\t\tif ( 1 === $frequency ) {\n\t\t\t\t$ret = sprintf( _x( 'per %s', 'subscription schedule', 'lifterlms' ), $this->get_access_period_name( $period, $frequency ) );\n\t\t\t} else {\n\t\t\t\t$ret = sprintf( _x( 'every %1$d %2$s', 'subscription schedule', 'lifterlms' ), $frequency, $this->get_access_period_name( $period, $frequency ) );\n\t\t\t}\n\n\t\t\t// Add length sentence if applicable.\n\t\t\tif ( $length > 0 ) {\n\n\t\t\t\t$ret .= ' ' . sprintf( _x( 'for %1$d total payments', 'subscription # of payments', 'lifterlms' ), $length );\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_product_schedule_details', sprintf( $ret, $this->get( 'period' ), $frequency, $length ), $this );\n\t}\n\n\t/**\n\t * Get a sentence explaining the plan's trial offer\n\t *\n\t * @return   string\n\t * @since    3.0.0\n\t * @version  3.4.8\n\t */\n\tpublic function get_trial_details() {\n\n\t\t$details = '';\n\n\t\tif ( $this->has_trial() ) {\n\n\t\t\t$length  = $this->get( 'trial_length' );\n\t\t\t$period  = $this->get( 'trial_period' );\n\t\t\t$details = sprintf( _x( 'for %1$d %2$s', 'trial offer description', 'lifterlms' ), $length, $this->get_access_period_name( $period, $length ) );\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_product_trial_details', $details, $this );\n\t}\n\n\t/**\n\t * Get the access plans visibility setting\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_visibility() {\n\t\t$term = $this->get_terms( 'llms_access_plan_visibility', true );\n\t\t$ret  = ( $term && $term->name ) ? $term->name : 'visible';\n\t\treturn apply_filters( 'llms_get_access_plan_visibility', $ret, $this );\n\t}\n\n\t/**\n\t * Determine if the plan has availability restrictions\n\t *\n\t * Related product must be a COURSE.\n\t * Availability must be set to \"members\" and at least one membership must be selected.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_availability_restrictions() {\n\t\treturn ( 'course' === $this->get_product_type() && 'members' === $this->get( 'availability' ) && $this->get_array( 'availability_restrictions' ) );\n\t}\n\n\t/**\n\t * Determine if the free checkout process & interface should be used for this access plan\n\t *\n\t * @return   boolean\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function has_free_checkout() {\n\t\treturn ( $this->is_free() && apply_filters( 'llms_has_free_checkout', true, $this ) );\n\t}\n\n\t/**\n\t * Determine if the plan has a trial offer\n\t * One-time payments can't have a trial, so the plan must have a frequency greater than 0\n\t *\n\t * @return   boolean\n\t * @since    3.0.0\n\t * @version  3.23.0\n\t */\n\tpublic function has_trial() {\n\t\t$ret = false;\n\t\tif ( $this->get( 'frequency' ) > 0 ) {\n\t\t\t$ret = llms_parse_bool( $this->get( 'trial_offer' ) );\n\t\t}\n\t\treturn apply_filters( 'llms_plan_has_trial', $ret, $this );\n\t}\n\n\t/**\n\t * Determine if the plan is available to a user based on configured availability restrictions\n\t *\n\t * @param    int $user_id  (optional) WP User ID, if not supplied get_current_user_id() will be used\n\t * @return   boolean\n\t * @since    3.4.4\n\t * @version  3.23.0\n\t */\n\tpublic function is_available_to_user( $user_id = null ) {\n\n\t\t$user_id = empty( $user_id ) ? get_current_user_id() : $user_id;\n\n\t\t$access = true;\n\n\t\t// If there are membership restrictions, check the user is in at least one membership.\n\t\tif ( $this->has_availability_restrictions() ) {\n\t\t\t$access = false;\n\t\t\tforeach ( $this->get_array( 'availability_restrictions' ) as $mid ) {\n\n\t\t\t\t// Once we find a membership, exit.\n\t\t\t\tif ( llms_is_user_enrolled( $user_id, $mid ) ) {\n\t\t\t\t\t$access = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_plan_is_available_to_user', $access, $user_id, $this );\n\t}\n\n\t/**\n\t * Determine if the plan is marked as \"featured\"\n\t *\n\t * @return   boolean\n\t * @since    3.0.0\n\t * @version  3.8.0\n\t */\n\tpublic function is_featured() {\n\t\treturn ( 'featured' === $this->get_visibility() );\n\t}\n\n\t/**\n\t * Determines if a plan is marked ar free\n\t * This only returns the value of the setting and should not\n\t * be used to check if payment is required (when using a coupon for example)\n\t *\n\t * @return   boolean\n\t * @since    3.0.0\n\t * @version  3.23.0\n\t */\n\tpublic function is_free() {\n\t\treturn llms_parse_bool( $this->get( 'is_free' ) );\n\t}\n\n\t/**\n\t * Determine if a plan is *currently* on sale\n\t *\n\t * @return   boolean\n\t * @since    3.0.0\n\t * @version  3.24.3\n\t */\n\tpublic function is_on_sale() {\n\n\t\t$ret = false;\n\n\t\tif ( llms_parse_bool( $this->get( 'on_sale' ) ) ) {\n\n\t\t\t$now = llms_current_time( 'timestamp' );\n\n\t\t\t$start = $this->get( 'sale_start' );\n\t\t\t$end   = $this->get( 'sale_end' );\n\n\t\t\t// Add times if the values exist (start of day & end of day).\n\t\t\t$start = ( $start ) ? strtotime( $start . ' 00:00:00' ) : $start;\n\t\t\t$end   = ( $end ) ? strtotime( '+1 day', strtotime( $end . ' 00:00:00' ) ) : $end;\n\n\t\t\t// No dates, the product is indefinitely on sale.\n\t\t\tif ( ! $start && ! $end ) {\n\n\t\t\t\t$ret = true;\n\n\t\t\t\t// Start and end.\n\t\t\t} elseif ( $start && $end ) {\n\n\t\t\t\t$ret = ( $now < $end && $now > $start );\n\n\t\t\t\t// Only start.\n\t\t\t} elseif ( $start && ! $end ) {\n\n\t\t\t\t$ret = ( $now > $start );\n\n\t\t\t\t// Only end.\n\t\t\t} elseif ( ! $start && $end ) {\n\n\t\t\t\t$ret = ( $now < $end );\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_plan_is_on_sale', $ret, $this );\n\t}\n\n\t/**\n\t * Determine if the plan is visible\n\t * Both featured and visible access plans are considered visible\n\t *\n\t * @return   boolean\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function is_visible() {\n\t\treturn ( 'hidden' !== $this->get_visibility() );\n\t}\n\n\t/**\n\t * Determine if the Access Plan has recurring payments\n\t *\n\t * @return  boolean   true if it is recurring, false otherwise\n\t * @since   3.0.0\n\t * @version 3.0.0\n\t */\n\tpublic function is_recurring() {\n\t\treturn ( 0 !== $this->get( 'frequency' ) );\n\t}\n\n\t/**\n\t * Determine if the access plan requires payment.\n\t *\n\t * Automatically accounts for coupons, sales, trials, and whether the plan is marked as free.\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.1 Uses self::get_initial_price().\n\t *\n\t * @param int $coupon_id LLMS_Coupon ID.\n\t * @return bool true if payment required, false otherwise\n\t */\n\tpublic function requires_payment( $coupon_id = null ) {\n\n\t\t$ret = false;\n\n\t\tif ( ! $this->is_free() ) {\n\n\t\t\t$ret = ( $this->get_initial_price( array(), $coupon_id, 'float' ) > 0 );\n\n\t\t\t// Ensure that we still collect payment details if a free trial is used.\n\t\t\tif ( false === $ret ) {\n\t\t\t\t$price_key = $this->is_on_sale() ? 'sale_price' : 'price';\n\t\t\t\t$ret       = ( $this->get_price( $price_key, array(), 'float', $coupon_id ) > 0 );\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_plan_requires_payment', $ret, $coupon_id, $this );\n\t}\n\n\t/**\n\t * Update the visibility term for the access plan\n\t *\n\t * @param    string $visibility  access plan name\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function set_visibility( $visibility ) {\n\t\treturn $this->set_terms( array( $visibility ), 'llms_access_plan_visibility', false );\n\t}\n\n\t/**\n\t * Cleanup data to remove unnecessary defaults\n\t *\n\t * @param    array $arr   array of data to be serialized\n\t * @return   array\n\t * @since    3.16.11\n\t * @version  3.16.11\n\t */\n\tprotected function toArrayAfter( $arr ) {\n\t\tunset( $arr['author'] );\n\t\treturn $arr;\n\t}\n\n\t/**\n\t * Don't add custom fields during toArray()\n\t *\n\t * @param    array $arr  post model array\n\t * @return   array\n\t * @since    3.16.11\n\t * @version  3.16.11\n\t */\n\tprotected function toArrayCustom( $arr ) {\n\t\treturn $arr;\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.add-on.php",
    "content": "<?php\n/**\n * LLMS_Add_On model class file.\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.22.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Add-On Model\n *\n * @since 3.22.0\n */\nclass LLMS_Add_On {\n\n\t/**\n\t * Add On ID\n\t *\n\t * @var string\n\t */\n\tprivate $id = '';\n\n\t/**\n\t * Add On Data\n\t *\n\t * @var array\n\t */\n\tprivate $data = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.22.0\n\t * @since 4.21.3 Move lookup logic to its own private method: `lookup_add_on()`.\n\t *\n\t * @param string|array $addon      Add-on data array or a string (such as an ID or update file path) used to lookup the addon.\n\t * @param string       $lookup_key If $addon is a string, this determines how to lookup the addon from the available list of addons.\n\t * @return void\n\t */\n\tpublic function __construct( $addon = array(), $lookup_key = 'id' ) {\n\n\t\tif ( is_string( $addon ) ) {\n\t\t\t$addon = $this->lookup_add_on( $lookup_key, $addon );\n\t\t}\n\n\t\t$this->id   = ! empty( $addon['id'] ) ? $addon['id'] : '';\n\t\t$this->data = $addon ? $addon : array();\n\n\t}\n\n\t/**\n\t * Magic getter to retrieve add-on props from private $data array\n\t *\n\t * @since 3.22.0\n\t *\n\t * @param string $key Property key.\n\t * @return mixed\n\t */\n\tpublic function __get( $key ) {\n\t\treturn isset( $this->data[ $key ] ) ? $this->data[ $key ] : '';\n\t}\n\n\t/**\n\t * Activate an add-on\n\t *\n\t * @since 3.22.0\n\t * @since 3.25.0 Unknown.\n\t *\n\t * @return string|WP_Error\n\t */\n\tpublic function activate() {\n\n\t\t$ret = false;\n\t\tif ( 'plugin' === $this->get( 'type' ) ) {\n\n\t\t\t$ret = activate_plugins( $this->get( 'update_file' ) );\n\n\t\t} elseif ( 'theme' === $this->get( 'type' ) ) {\n\n\t\t\t$ret = true;\n\t\t\tswitch_theme( $this->get( 'update_file' ) );\n\n\t\t}\n\n\t\tif ( true === $ret ) {\n\t\t\t// Translators: %s = Add-on name.\n\t\t\treturn sprintf( __( '%s was successfully activated.', 'lifterlms' ), $this->get( 'title' ) );\n\t\t}\n\n\t\t// Translators: %s = Add-on name.\n\t\treturn new WP_Error( 'activation', sprintf( __( 'Could not activate %s.', 'lifterlms' ), $this->get( 'title' ) ) );\n\n\t}\n\n\t/**\n\t * Deactivate the addon\n\t *\n\t * @since 3.22.0\n\t * @since 4.21.3 Updated the failure error code from 'activation' to 'deactivation'.\n\t *\n\t * @return string|WP_Error\n\t */\n\tpublic function deactivate() {\n\n\t\t$ret = false;\n\n\t\tif ( 'plugin' === $this->get( 'type' ) ) {\n\n\t\t\tdeactivate_plugins( $this->get( 'update_file' ) );\n\t\t\t// Translators: %s = Add-on name.\n\t\t\treturn sprintf( __( '%s was successfully deactivated.', 'lifterlms' ), $this->get( 'title' ) );\n\n\t\t}\n\n\t\t// Translators: %s = Add-on name.\n\t\treturn new WP_Error( 'deactivation', sprintf( __( 'Could not deactivate %s.', 'lifterlms' ), $this->get( 'title' ) ) );\n\n\t}\n\n\t/**\n\t * Get add-on properties\n\t *\n\t * @since 3.22.0\n\t *\n\t * @param string $key Property key.\n\t * @return mixed\n\t */\n\tpublic function get( $key ) {\n\t\treturn $this->$key;\n\t}\n\n\t/**\n\t * Retrieve the update channel for the addon\n\t *\n\t * @since 3.22.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_channel_subscription() {\n\t\treturn 'stable';\n\t}\n\n\t/**\n\t * Determine the status of an addon's license\n\t *\n\t * @since 3.22.0\n\t *\n\t * @param bool $translate If `true`, returns the translated string for on-screen display.\n\t * @return string\n\t */\n\tpublic function get_install_status( $translate = false ) {\n\n\t\tif ( ! $this->is_installable() ) {\n\t\t\t$ret = 'none';\n\t\t} else {\n\t\t\t$ret = $this->is_installed() ? 'installed' : 'uninstalled';\n\t\t}\n\n\t\treturn $translate ? $this->get_l10n( $ret ) : $ret;\n\n\t}\n\n\t/**\n\t * Get the currently installed version of an addon\n\t *\n\t * @since 3.22.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_installed_version() {\n\t\tif ( $this->is_installable() && $this->is_installed() ) {\n\t\t\t$type = $this->get( 'type' );\n\t\t\tif ( 'plugin' === $type ) {\n\t\t\t\t$data = get_plugin_data( trailingslashit( WP_PLUGIN_DIR ) . $this->get( 'update_file' ) );\n\t\t\t\treturn $data['Version'];\n\t\t\t} elseif ( 'theme' === $type ) {\n\t\t\t\t$data = wp_get_theme( $this->get( 'update_file' ) );\n\t\t\t\treturn $data->get( 'Version' );\n\t\t\t}\n\t\t}\n\t\treturn '';\n\t}\n\n\t/**\n\t * Retrieve the latest available version for the current channel\n\t *\n\t * @since 3.22.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_latest_version() {\n\t\tif ( 'beta' === $this->get_channel_subscription() && $this->get( 'version_beta' ) ) {\n\t\t\treturn $this->get( 'version_beta' );\n\t\t}\n\t\treturn $this->get( 'version' );\n\t}\n\n\t/**\n\t * Translate strings\n\t *\n\t * @since 3.22.0\n\t *\n\t * @param string $string Untranslated string / key.\n\t * @return string\n\t */\n\tpublic function get_l10n( $string ) {\n\n\t\t$strings = array(\n\n\t\t\t'active'           => __( 'Active', 'lifterlms' ),\n\t\t\t'inactive'         => __( 'Inactive', 'lifterlms' ),\n\n\t\t\t'installed'        => __( 'Installed', 'lifterlms' ),\n\t\t\t'uninstalled'      => __( 'Not Installed', 'lifterlms' ),\n\n\t\t\t'activate'         => __( 'Activate', 'lifterlms' ),\n\t\t\t'deactivate'       => __( 'Deactivate', 'lifterlms' ),\n\t\t\t'install'          => __( 'Install', 'lifterlms' ),\n\n\t\t\t'none'             => __( 'N/A', 'lifterlms' ),\n\n\t\t\t'license_active'   => __( 'Licensed', 'lifterlms' ),\n\t\t\t'license_inactive' => __( 'Unlicensed', 'lifterlms' ),\n\n\t\t);\n\n\t\treturn $strings[ $string ];\n\n\t}\n\n\t/**\n\t * Determine the status of an addon's license\n\t *\n\t * @since 3.22.0\n\t *\n\t * @param bool $translate If `true`, returns the translated string for on-screen display.\n\t * @return string\n\t */\n\tpublic function get_license_status( $translate = false ) {\n\n\t\tif ( ! llms_parse_bool( $this->get( 'has_license' ) ) ) {\n\t\t\t$ret = 'none';\n\t\t} else {\n\t\t\t$ret = $this->is_licensed() ? 'license_active' : 'license_inactive';\n\t\t}\n\n\t\treturn $translate ? $this->get_l10n( $ret ) : $ret;\n\n\t}\n\n\t/**\n\t * Retrieve a utm'd link to the add-on\n\t *\n\t * @since 3.22.0\n\t * @since 4.21.3 Use `rawurlencode()` in favor of `urlencode()`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_permalink() {\n\n\t\t$url = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'utm_source'   => rawurlencode( 'LifterLMS Plugin' ),\n\t\t\t\t'utm_campaign' => rawurlencode( 'Plugin to Sale' ),\n\t\t\t\t'utm_medium'   => rawurlencode( 'Add-Ons Screen' ),\n\t\t\t\t'utm_content'  => rawurlencode( sprintf( '%1$s Ad %2$s', $this->get( 'title' ), LLMS_VERSION ) ),\n\t\t\t),\n\t\t\t$this->get( 'permalink' )\n\t\t);\n\n\t\treturn $url;\n\n\t}\n\n\t/**\n\t * Get the type of addon\n\t *\n\t * @since 3.22.0\n\t * @since 4.21.3 Use strict comparison for `in_array()`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_type() {\n\n\t\t$type = $this->get( 'type' );\n\n\t\tif ( $type ) {\n\t\t\treturn $type;\n\t\t}\n\n\t\t$cats = array_keys( $this->get( 'categories' ) );\n\n\t\tif ( in_array( 'bundles', $cats, true ) ) {\n\t\t\t$type = 'bundle';\n\t\t} elseif ( in_array( 'third-party', $cats, true ) ) {\n\t\t\t$type = 'external';\n\t\t} else {\n\t\t\t$type = 'support';\n\t\t}\n\n\t\treturn $type;\n\n\t}\n\n\t/**\n\t * Get the addon's status\n\t *\n\t * @since 3.22.0\n\t * @param bool $translate If `true`, translates the status for on-screen display.\n\t * @return string\n\t */\n\tpublic function get_status( $translate = false ) {\n\n\t\tif ( ! $this->is_installable() ) {\n\t\t\t$ret = 'none';\n\t\t} elseif ( $this->is_installed() ) {\n\t\t\t$ret = $this->is_active() ? 'active' : 'inactive';\n\t\t} else {\n\t\t\t$ret = 'uninstalled';\n\t\t}\n\n\t\tif ( $translate ) {\n\t\t\t$ret = $this->get_l10n( $ret );\n\t\t}\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Get the addon or author image URL for the add-on.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param string $type Type of image to retrieve. Defaults to 'addon'. Accepts 'addon' or 'author'.\n\t * @return string\n\t */\n\tpublic function get_image( $type = 'addon' ) {\n\n\t\t$img = 'author' === $type ? $this->get( 'author' )['image'] : $this->get( 'image' );\n\n\t\tif ( ! $img ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tif ( is_readable( llms()->plugin_path() . '/assets/images/addons/' . basename( $img ) ) ) {\n\t\t\treturn llms()->plugin_url() . '/assets/images/addons/' . basename( $img );\n\t\t}\n\n\t\treturn $img;\n\t}\n\n\t/**\n\t * Determine if there is an available update for the add-on\n\t *\n\t * @since 3.22.0\n\t *\n\t * @return bool\n\t */\n\tpublic function has_available_update() {\n\t\treturn version_compare( $this->get_installed_version(), $this->get_latest_version(), '<' );\n\t}\n\n\t/**\n\t * Determine if an installable addon is active\n\t *\n\t * @since 3.22.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_active() {\n\n\t\tif ( $this->is_installable() && $this->is_installed() ) {\n\n\t\t\t$file = $this->get( 'update_file' );\n\t\t\t$type = $this->get_type();\n\t\t\tif ( 'plugin' === $type ) {\n\t\t\t\treturn is_plugin_active( $file );\n\t\t\t} elseif ( 'theme' === $type ) {\n\t\t\t\t$theme = wp_get_theme();\n\t\t\t\treturn ( $file === $theme->get_stylesheet() );\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Determines if the add-on is installable\n\t *\n\t * @since 3.22.0\n\t * @since 3.22.1 Unknown.\n\t * @since 4.21.3 Use strict comparison for `in_array()`.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_installable() {\n\t\treturn ( $this->get( 'update_file' ) && in_array( $this->get_type(), array( 'plugin', 'theme' ), true ) );\n\t}\n\n\t/**\n\t * Determine if the add-on is currently installed\n\t *\n\t * @since 3.22.0\n\t * @since 4.21.3 Use strict comparison for `in_array()`.\n\t *\n\t * @return bool\n\t */\n\tpublic function is_installed() {\n\n\t\tif ( ! $this->is_installable() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$type = $this->get_type();\n\n\t\tif ( 'plugin' === $type ) {\n\t\t\treturn in_array( $this->get( 'update_file' ), array_keys( get_plugins() ), true );\n\t\t} elseif ( 'theme' === $type ) {\n\t\t\treturn wp_get_theme( $this->get( 'update_file' ) )->exists();\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Determines if the add-on is licensed\n\t *\n\t * @since 3.22.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_licensed() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * Locate an add-on by a key/val pair\n\t *\n\t * Loads add-ons via `llms_get_add_ons()` and loops through the items list\n\t * to find an addon specified by the key/val pair.\n\t *\n\t * @since 4.21.3\n\t *\n\t * @param string $lookup_key Key found within the add-on item. EG: \"id\" or \"update_file\".\n\t * @param string $lookup_val Value of the key to match.\n\t * @return array|false Returns the add-on data array of `false` if no found.\n\t */\n\tprivate function lookup_add_on( $lookup_key, $lookup_val ) {\n\n\t\t$addons = llms_get_add_ons();\n\n\t\t// Error communicating with the API or no items found.\n\t\tif ( is_wp_error( $addons ) || empty( $addons['items'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Loop through the list.\n\t\tforeach ( $addons['items'] as $addon ) {\n\n\t\t\t// We've found a match.\n\t\t\tif ( isset( $addon[ $lookup_key ] ) && $addon[ $lookup_key ] === $lookup_val ) {\n\t\t\t\treturn $addon;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Verifies the add-on can be uninstalled, and performs the uninstall (permanently deleting its files)\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return string|WP_Error Success message or an error object.\n\t */\n\tpublic function uninstall() {\n\n\t\t$title = $this->get( 'title' );\n\n\t\tif ( ! $this->is_installed() ) {\n\t\t\t// Translators: %s = Add-on title.\n\t\t\treturn new WP_Error( 'not-installed', sprintf( __( '%s is not installed.', 'lifterlms' ), $title ) );\n\t\t}\n\n\t\tif ( $this->is_active() ) {\n\t\t\t// Translators: %s = Add-on title.\n\t\t\treturn new WP_Error( 'uninstall-active', sprintf( __( '%s is active and cannot be uninstalled.', 'lifterlms' ), $title ) );\n\t\t}\n\n\t\treturn $this->uninstall_real();\n\n\t}\n\n\t/**\n\t * Actually performs the uninstall\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return string|WP_Error Success message or an error object.\n\t */\n\tprivate function uninstall_real() {\n\n\t\t$type = $this->get_type();\n\n\t\tif ( ! in_array( $type, array( 'plugin', 'theme' ), true ) ) {\n\t\t\t// Translators: %s = add-on type.\n\t\t\treturn new WP_Error( 'uninstall-invalid-type', sprintf( __( 'Cannot uninstall \"%s\" type add-ons.', 'lifterlms' ), $type ) );\n\t\t}\n\n\t\t$file = $this->get( 'update_file' );\n\n\t\tif ( 'plugin' === $type ) {\n\t\t\tuninstall_plugin( $file );\n\t\t\t$del = delete_plugins( array( $file ) );\n\t\t} else {\n\t\t\t$del = delete_theme( $file );\n\t\t}\n\n\t\tif ( is_wp_error( $del ) ) {\n\t\t\treturn $del;\n\t\t}\n\n\t\t// Translators: %s = Add-on title.\n\t\treturn sprintf( __( '%s was successfully uninstalled.', 'lifterlms' ), $this->get( 'title' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.coupon.php",
    "content": "<?php\n/**\n * LifterLMS Coupon Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.0.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Coupon model.\n *\n * @property  $coupon_amount  (float)  Amount to subtract from the price when using the coupon. Used with $discount_type to determine the type of discount\n * @property  $coupon_courses  (array)  Array of Course IDs the coupon can be used against\n * @property  $coupon_membership  (array) Array of Membership IDs the coupon can be used against\n * @property  $description  (string)  A string of text. Used as an internal note field  .\n * @property  $discount_type  (string)  Determines the discount type [dollar|percent]\n * @property  $enable_trial_discount  (yes/no)  Enables an optional additional amount field to apply to the Trial Price of access plans with a trial [yes|no]\n * @property  $expiration_date  (string)  Date String describing a date after which the coupon can no longer be used. Format: m/d/Y\n * @property  $plan_type  (string)  Determine the type of plans the coupon can be used with . [any|one-time|recurring]\n * @property  $title  (string)  Coupon Code / Post Title\n * @property  $trial_amount  (float)  Amount to subtract from the trial price when using the coupon. Used with $discount_type to determine the type of discount\n * @property  $usage_limit  (int)  Amount of times the coupon can be used.\n *\n * @since 3.0.0\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Coupon extends LLMS_Post_Model {\n\n\tprotected $properties = array(\n\t\t'coupon_amount'         => 'float',\n\t\t'coupon_courses'        => 'array',\n\t\t'coupon_membership'     => 'array',\n\t\t'description'           => 'string',\n\t\t'discount_type'         => 'string',\n\t\t'enable_trial_discount' => 'yesno',\n\t\t'expiration_date'       => 'string',\n\t\t'plan_type'             => 'string',\n\t\t'trial_amount'          => 'float',\n\t\t'usage_limit'           => 'absint',\n\t);\n\n\tprotected $db_post_type    = 'llms_coupon';\n\tprotected $model_post_type = 'coupon';\n\n\t/**\n\t * Determine if the coupon can be applied to an access plan\n\t *\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t * @param    int|obj $plan_id  WP Post ID of the LLMS Access Plan or an instance of LLMS_Access_Plan\n\t * @return  bool\n\t */\n\tpublic function applies_to_plan( $plan_id ) {\n\n\t\tif ( $plan_id instanceof LLMS_Access_Plan ) {\n\t\t\t$plan = $plan_id;\n\t\t} else {\n\t\t\t$plan = new LLMS_Access_Plan( $plan_id );\n\t\t}\n\n\t\t// Check if it can be applied to the plan's product first.\n\t\tif ( ! $this->applies_to_product( $plan->get( 'product_id' ) ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// If the coupon can only be used with one-time plans and the plan is recurring.\n\t\tif ( 'one-time' === $this->get( 'plan_type' ) && $plan->is_recurring() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( 'recurring' === $this->get( 'plan_type' ) && ! $plan->is_recurring() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Determine if a coupon can be applied to a specific product\n\t *\n\t * @since  3.0.0\n\t * @version  3.0.0\n\t * @param  int $product_id  WP Post ID of a LLMS Course or Membership\n\t * @return boolean     true if it can be applied, false otherwise\n\t */\n\tpublic function applies_to_product( $product_id ) {\n\t\t$products = $this->get_products();\n\t\t// No product restrictions.\n\t\tif ( empty( $products ) ) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn in_array( $product_id, $products );\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve the timestamp of a coupon expiration date\n\t * Transforms the expiration date to a timestamp and adds 23 hours 59 minutes and 59 seconds to the date\n\t * Coupons expire end of day on the expiration date (EG: 2015-12-01 @ 23:59:59)\n\t *\n\t * @return   false|int\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tpublic function get_expiration_time() {\n\t\t$expires = $this->get_date( 'expiration_date', 'U' );\n\t\tif ( ! $expires ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn ( (int) $expires + DAY_IN_SECONDS - 1 );\n\t}\n\n\t/**\n\t * Get the discount type for human reading and allow translation\n\t *\n\t * @since   3.0.0\n\t * @version 3.24.0\n\t * @return  string\n\t */\n\tpublic function get_formatted_discount_type() {\n\t\tswitch ( $this->get_discount_type() ) {\n\t\t\tcase 'percent':\n\t\t\t\treturn __( 'Percentage Discount', 'lifterlms' );\n\t\t\tbreak;\n\t\t\tcase 'dollar':\n\t\t\t\treturn sprintf( _x( '%s Discount', 'flat rate coupon discount', 'lifterlms' ), get_lifterlms_currency_symbol() );\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * Get the formatted coupon amount with currency symbol and/or percentage symbol\n\t *\n\t * @since 3.0.0\n\t * @version 3.0.0\n\t * @param   string $amount key for the amount to format\n\t * @return  string\n\t */\n\tpublic function get_formatted_amount( $amount = 'coupon_amount' ) {\n\t\t$amount = $this->get( $amount );\n\t\tswitch ( $this->get( 'discount_type' ) ) {\n\t\t\tcase 'percent':\n\t\t\t\t$amount .= '%';\n\t\t\t\tbreak;\n\t\t\tcase 'dollar':\n\t\t\t\t$amount = llms_price( $amount );\n\t\t\t\tbreak;\n\t\t}\n\t\treturn $amount;\n\t}\n\n\t/**\n\t * Get an array of all products the coupon can be used with\n\t * Combines $this->coupon_courses & $this->coupon_membership\n\t *\n\t * @since 3.0.0\n\t * @version 3.0.0\n\t * @return  array\n\t */\n\tpublic function get_products() {\n\t\treturn array_merge( $this->get_array( 'coupon_courses' ), $this->get_array( 'coupon_membership' ) );\n\t}\n\n\t/**\n\t * Get the number of remaining uses\n\t * calculated by subtracting # of uses from the usage limit\n\t *\n\t * @since  3.0.0\n\t * @version 3.0.0\n\t * @return string|int\n\t */\n\tpublic function get_remaining_uses() {\n\n\t\t$limit = $this->get( 'usage_limit' );\n\n\t\t// If usage is unlimited.\n\t\tif ( ! $limit ) {\n\n\t\t\treturn _x( 'Unlimited', 'Remaining coupon uses', 'lifterlms' );\n\n\t\t} else {\n\n\t\t\treturn $limit - $this->get_uses();\n\n\t\t}\n\t}\n\n\t/**\n\t * Get the number of times the coupon has been used\n\t *\n\t * @return   int\n\t * @since    3.0.0\n\t * @version  3.19.0\n\t */\n\tpublic function get_uses() {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => $this->meta_prefix . 'coupon_code',\n\t\t\t\t\t\t'value' => $this->get( 'title' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'post_status'    => 'any',\n\t\t\t\t'post_type'      => 'llms_order',\n\t\t\t\t'posts_per_page' => -1,\n\t\t\t)\n\t\t);\n\n\t\treturn $query->post_count;\n\t}\n\n\t/**\n\t * Determine if the main (non-trial) price is discounted by this coupon\n\t *\n\t * @return   bool\n\t * @since    3.21.1\n\t * @version  3.21.1\n\t */\n\tpublic function has_main_discount() {\n\t\treturn ( $this->get( 'coupon_amount' ) > 0 );\n\t}\n\n\t/**\n\t * Determine if a coupon has uses remaining\n\t *\n\t * @return boolean   true if uses are remaining, false otherwise\n\t */\n\tpublic function has_remaining_uses() {\n\t\t$uses = $this->get_remaining_uses();\n\t\tif ( is_numeric( $uses ) ) {\n\t\t\treturn ( $uses >= 1 ) ? true : false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Determine if trial amount discount is enabled for the coupon\n\t *\n\t * @return  boolean\n\t * @since   3.0.0\n\t * @version 3.21.1\n\t */\n\tpublic function has_trial_discount() {\n\t\treturn llms_parse_bool( $this->get( 'enable_trial_discount' ) );\n\t}\n\n\t/**\n\t * Determine if a coupon is expired\n\t *\n\t * @return  boolean   true if expired, false otherwise\n\t * @since   3.0.0\n\t * @version 3.19.0\n\t */\n\tpublic function is_expired() {\n\t\t$expires = $this->get_expiration_time();\n\t\t// No expiration date, can't expire.\n\t\tif ( ! $expires ) {\n\t\t\treturn false;\n\t\t} else {\n\t\t\treturn $expires < llms_current_time( 'timestamp' );\n\t\t}\n\t}\n\n\t/**\n\t * Perform all available validations and return a success or error message\n\t *\n\t * @param    int $plan_id  WP Post ID of an LLMS Access Plan\n\t * @return   WP_Error|true            If true, the coupon is valid, if WP_Error, there was an error\n\t * @since    3.0.0\n\t * @version  3.19.0\n\t */\n\tpublic function is_valid( $plan_id ) {\n\n\t\t$msg = false;\n\n\t\t$plan = new LLMS_Access_Plan( $plan_id );\n\n\t\tif ( ! $this->has_remaining_uses() ) {\n\n\t\t\t$msg = __( 'This coupon has reached its usage limit and can no longer be used.', 'lifterlms' );\n\n\t\t} elseif ( $this->is_expired() ) {\n\n\t\t\t$msg = sprintf( __( 'This coupon expired on %s and can no longer be used.', 'lifterlms' ), $this->get_date( 'expiration_date', 'F d, Y' ) );\n\n\t\t} elseif ( ! $this->applies_to_product( $plan->get( 'product_id' ) ) ) {\n\n\t\t\t$msg = sprintf( __( 'This coupon cannot be used to purchase \"%s\".', 'lifterlms' ), get_the_title( $plan->get( 'product_id' ) ) );\n\n\t\t} elseif ( ! $this->applies_to_plan( $plan ) ) {\n\n\t\t\t$msg = sprintf( __( 'This coupon cannot be used to purchase \"%s\".', 'lifterlms' ), $plan->get( 'title' ) );\n\n\t\t}\n\n\t\t// Error encountered.\n\t\tif ( $msg ) {\n\n\t\t\t$ret = new WP_Error();\n\t\t\t$ret->add( 'error', apply_filters( 'lifterlms_coupon_validation_error_message', $msg, $this ) );\n\n\t\t} else {\n\n\t\t\t$ret = true;\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_coupon_is_valid', $ret, $plan, $this );\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.course.php",
    "content": "<?php\n/**\n * LifterLMS Course Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 1.0.0\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Course model class.\n *\n * @since 1.0.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 4.0.0 Remove previously deprecated class methods.\n * @since 5.2.1 Check for an empty sales page URL or ID.\n * @since 5.3.0 Move audio and video embed methods to `LLMS_Trait_Audio_Video_Embed`.\n *              Move sales page methods to `LLMS_Trait_Sales_Page`.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Course::sections` property\n *              - `LLMS_Course::sku` property\n *\n * @property string $audio_embed                URL to an oEmbed enable audio URL.\n * @property float  $average_grade              Calculated value of the overall average grade of all *enrolled* students in the course..\n * @property float  $average_progress           Calculated value of the overall average progress of all *enrolled* students in the course..\n * @property int    $capacity                   Number of students who can be enrolled in the course before enrollment closes.\n * @property string $capacity_message           Message displayed when capacity has been reached.\n * @property string $content_restricted_message Message displayed when non-enrolled visitors try to access lessons/quizzes directly.\n * @property string $course_closed_message      Message displayed to visitors when the course is accessed after the Course End Date has passed. Only applicable when $time_period is 'yes'.\n * @property string $course_opens_message       Message displayed to visitors when the course is accessed before the Course Start Date has passed. Only applicable when $time_period is 'yes'.\n * @property string $enable_capacity            Whether capacity restrictions are enabled [yes|no].\n * @property string $enrollment_closed_message  Message displayed to non-enrolled visitors when the course is accessed after the Enrollment End Date has passed. Only applicable when $enrollment_period is 'yes'.\n * @property string $enrollment_end_date        After this date, registration closes.\n * @property string $enrollment_opens_message   Message displayed to non-enrolled visitors when the course is accessed before the Enrollment Start Date has passed. Only applicable when $enrollment_period is 'yes'.\n * @property string $enrollment_period          Whether or not a course time period restriction is enabled [yes|no] (all checks should check for 'yes' as an empty string might be returned).\n * @property string $enrollment_start_date      Before this date, registration is closed.\n * @property string $end_date                   Date when a course closes. Students may no longer view content or complete lessons / quizzes after this date..\n * @property string $has_prerequisite           Determine if prerequisites are enabled [yes|no].\n * @property array  $instructors                Course instructor user information.\n * @property int    $prerequisite               WP Post ID of a the prerequisite course.\n * @property int    $prerequisite_track         WP Tax ID of a the prerequisite track.\n * @property string $start_date                 Date when a course is opens. Students may register before this date but can only view content and complete lessons or quizzes after this date..\n * @property string $length                     User defined course length.\n * @property string $featured_pricing           User defined additional pricing information.\n * @property int    $sales_page_content_page_id WP Post ID of the WP page to redirect to when $sales_page_content_type is 'page'.\n * @property string $sales_page_content_type    Sales page behavior [none,content,page,url].\n * @property string $sales_page_content_url     Redirect URL for a sales page, when $sales_page_content_type is 'url'.\n * @property string $tile_featured_video        Displays the featured video instead of the featured image on course tiles [yes|no].\n * @property string $time_period                Whether or not a course time period restriction is enabled [yes|no] (all checks should check for 'yes' as an empty string might be returned).\n * @property string $video_embed                URL to an oEmbed enable video URL.\n */\nclass LLMS_Course extends LLMS_Post_Model implements LLMS_Interface_Post_Instructors {\n\n\tuse LLMS_Trait_Audio_Video_Embed;\n\tuse LLMS_Trait_Sales_Page;\n\n\t/**\n\t * Meta properties.\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\n\t\t// Public.\n\t\t'average_grade'              => 'float',\n\t\t'average_progress'           => 'float',\n\t\t'capacity'                   => 'absint',\n\t\t'capacity_message'           => 'text',\n\t\t'course_closed_message'      => 'text',\n\t\t'course_opens_message'       => 'text',\n\t\t'content_restricted_message' => 'text',\n\t\t'enable_capacity'            => 'yesno',\n\t\t'end_date'                   => 'text',\n\t\t'enrolled_students'          => 'absint',\n\t\t'enrollment_closed_message'  => 'text',\n\t\t'enrollment_end_date'        => 'text',\n\t\t'enrollment_opens_message'   => 'text',\n\t\t'enrollment_period'          => 'yesno',\n\t\t'enrollment_start_date'      => 'text',\n\t\t'has_prerequisite'           => 'yesno',\n\t\t'instructors'                => 'array',\n\t\t'length'                     => 'text',\n\t\t'prerequisite'               => 'absint',\n\t\t'prerequisite_track'         => 'absint',\n\t\t'tile_featured_video'        => 'yesno',\n\t\t'time_period'                => 'yesno',\n\t\t'start_date'                 => 'text',\n\t\t'lesson_drip'                => 'yesno',\n\t\t'drip_method'                => 'text',\n\t\t'ignore_lessons'             => 'absint',\n\t\t'days_before_available'      => 'absint',\n\t\t'featured_pricing'           => 'html',\n\t\t'completion_page_id'         => 'absint',\n\n\t\t// Private.\n\t\t'temp_calc_data'             => 'array',\n\t\t'last_data_calc_run'         => 'absint',\n\n\t);\n\n\t/**\n\t * Default property values\n\t *\n\t * @var array\n\t */\n\tprotected $property_defaults = array(\n\t\t'enrolled_students' => 0,\n\t);\n\n\t/**\n\t * DB post type name.\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'course';\n\n\t/**\n\t * Model post type name.\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'course';\n\n\t/**\n\t * Constructor for this class and the traits it uses.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param string|int|LLMS_Post_Model|WP_Post $model 'new', WP post id, instance of an extending class, instance of WP_Post.\n\t * @param array                              $args  Args to create the post, only applies when $model is 'new'.\n\t */\n\tpublic function __construct( $model, $args = array() ) {\n\n\t\t$this->construct_audio_video_embed();\n\t\t$this->construct_sales_page();\n\t\tparent::__construct( $model, $args );\n\t}\n\n\t/**\n\t * Retrieve an instance of the Post Instructors model\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return LLMS_Post_Instructors\n\t */\n\tpublic function instructors() {\n\t\treturn new LLMS_Post_Instructors( $this );\n\t}\n\n\t/**\n\t * Retrieve the total points available for the course\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_available_points() {\n\t\t$points = 0;\n\t\tforeach ( $this->get_lessons() as $lesson ) {\n\t\t\t$points += $lesson->get( 'points' );\n\t\t}\n\n\t\t/**\n\t\t * Filters the total available points for the course.\n\t\t *\n\t\t * @since 3.24.0\n\t\t *\n\t\t * @param int         $points Number of available points.\n\t\t * @param LLMS_Course $course Course object.\n\t\t */\n\t\treturn apply_filters( 'llms_course_get_available_points', $points, $this );\n\t}\n\n\t/**\n\t * Get course's prerequisite id based on the type of prerequisite\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.3 Unknown.\n\t *\n\t * @param string $type Optional. Type of prereq to retrieve id for [course|track]. Default is 'course'.\n\t * @return int|false Post ID of a course, taxonomy ID of a track, or false if none found.\n.    */\n\tpublic function get_prerequisite_id( $type = 'course' ) {\n\n\t\tif ( $this->has_prerequisite( $type ) ) {\n\n\t\t\tswitch ( $type ) {\n\n\t\t\t\tcase 'course':\n\t\t\t\t\t$key = 'prerequisite';\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'course_track':\n\t\t\t\t\t$key = 'prerequisite_track';\n\t\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t\tif ( isset( $key ) ) {\n\t\t\t\treturn $this->get( $key );\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve course categories\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param array $args Array of args passed to wp_get_post_terms.\n\t * @return array\n\t */\n\tpublic function get_categories( $args = array() ) {\n\t\treturn wp_get_post_terms( $this->get( 'id' ), 'course_cat', $args );\n\t}\n\n\t/**\n\t * Get Difficulty\n\t *\n\t * @since 1.0.0\n\t * @since 3.24.0 Unknown.\n\t * @since 7.2.0 Added support for showing multiple difficulties.\n\t *\n\t * @param string $field Optional. Which field to return from the available term fields.\n\t *                      Any public variables from a WP_Term object are acceptable: term_id, name, slug, and more.\n\t *                      Default is 'name'.\n\t * @return string\n\t */\n\tpublic function get_difficulty( $field = 'name' ) {\n\n\t\t$terms = get_the_terms( $this->get( 'id' ), 'course_difficulty' );\n\n\t\tif ( false === $terms ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$difficulties = wp_list_pluck( $terms, $field );\n\t\treturn implode( ', ', $difficulties );\n\t}\n\n\t/**\n\t * Retrieve course instructor information\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param boolean $exclude_hidden Optional. If true, excludes hidden instructors from the return array. Default is `false`.\n\t * @return array\n\t */\n\tpublic function get_instructors( $exclude_hidden = false ) {\n\n\t\t/**\n\t\t * Filters the course's instructors list\n\t\t *\n\t\t * @since 3.13.0\n\t\t *\n\t\t * @param array       $instructors    Instructor data array.\n\t\t * @param LLMS_Course $course         Course object.\n\t\t * @param boolearn    $exclude_hidden If true, excludes hidden instructors from the return array.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_course_get_instructors',\n\t\t\t$this->instructors()->get_instructors( $exclude_hidden ),\n\t\t\t$this,\n\t\t\t$exclude_hidden\n\t\t);\n\t}\n\n\t/**\n\t * Get course lessons\n\t *\n\t * @since 3.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $return Optional. Type of return [ids|posts|lessons]. Default is 'lessons'.\n\t * @return int[]|WP_Post[]|LLMS_Lesson[] The type depends on value of `$return`.\n\t */\n\tpublic function get_lessons( $return = 'lessons' ) {\n\n\t\t$lessons = array();\n\t\tforeach ( $this->get_sections( 'sections' ) as $section ) {\n\t\t\t$lessons = array_merge( $lessons, $section->get_lessons( 'posts' ) );\n\t\t}\n\n\t\tif ( 'ids' === $return ) {\n\t\t\t$ret = wp_list_pluck( $lessons, 'ID' );\n\t\t} elseif ( 'posts' === $return ) {\n\t\t\t$ret = $lessons;\n\t\t} else {\n\t\t\t$ret = array_map( 'llms_get_post', $lessons );\n\t\t}\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Retrieve the number of course's lessons.\n\t *\n\t * This is less expensive than counting the result of {@see LLMS_Course::get_lessons()},\n\t * and should be preferred when you only need to count the number of lessons of a course.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_lessons_count() {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'meta_key'               => '_llms_parent_course',\n\t\t\t\t'meta_value'             => $this->get( 'id' ),\n\t\t\t\t'post_type'              => 'lesson',\n\t\t\t\t'posts_per_page'         => -1,\n\t\t\t\t'no_found_rows'          => true,\n\t\t\t\t'update_post_meta_cache' => false,\n\t\t\t\t'update_post_term_cache' => false,\n\t\t\t\t'fields'                 => 'ids',\n\t\t\t\t'orderby'                => 'ID',\n\t\t\t\t'order'                  => 'ASC',\n\t\t\t)\n\t\t);\n\n\t\treturn $query->post_count;\n\t}\n\n\t/**\n\t * Retrieve an array of quizzes within a course\n\t *\n\t * @since 3.12.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @return int[] Array of WP_Post IDs of the quizzes.\n\t */\n\tpublic function get_quizzes() {\n\n\t\t$quizzes = array();\n\t\tforeach ( $this->get_lessons( 'lessons' ) as $lesson ) {\n\t\t\tif ( $lesson->has_quiz() ) {\n\t\t\t\t$quizzes[] = $lesson->get( 'quiz' );\n\t\t\t}\n\t\t}\n\t\treturn $quizzes;\n\t}\n\n\t/**\n\t * Get course sections\n\t *\n\t * @since 3.0.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $return Optional. Type of return [ids|posts|sections]. Default is 'sections'.\n\t * @return int[]|WP_Post[]|LLMS_Section[] The type depends on value of `$return`.\n\t */\n\tpublic function get_sections( $return = 'sections' ) {\n\n\t\t$q = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'meta_key'       => '_llms_order',\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_parent_course',\n\t\t\t\t\t\t'value' => $this->id,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'order'          => 'ASC',\n\t\t\t\t'orderby'        => 'meta_value_num',\n\t\t\t\t'post_type'      => 'section',\n\t\t\t\t'posts_per_page' => 500,\n\t\t\t)\n\t\t);\n\n\t\tif ( 'ids' === $return ) {\n\t\t\t$r = wp_list_pluck( $q->posts, 'ID' );\n\t\t} elseif ( 'posts' === $return ) {\n\t\t\t$r = $q->posts;\n\t\t} else {\n\t\t\t$r = array();\n\t\t\tforeach ( $q->posts as $p ) {\n\t\t\t\t$r[] = new LLMS_Section( $p );\n\t\t\t}\n\t\t}\n\n\t\treturn $r;\n\t}\n\n\t/**\n\t * Retrieve the number of enrolled students in the course\n\t *\n\t * The cached value is calculated in the `LLMS_Processor_Course_Data` background processor.\n\t *\n\t * If, for whatever reason, it's not found, it will be calculated on demand and saved for later use.\n\t *\n\t * @since 3.15.0\n\t * @since 4.12.0 Use cached value where possible.\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @param boolean $skip_cache Default: `false`. Whether or not to bypass the cache. If `true`, bypasses the cache.\n\t * @return int\n\t */\n\tpublic function get_student_count( $skip_cache = false ) {\n\n\t\t$count = ! $skip_cache ? $this->get( 'enrolled_students' ) : false;\n\n\t\t/**\n\t\t * Query enrolled students when `$skip_cache=true` or when there's no stored meta data.\n\t\t *\n\t\t * The second condition is necessary to disambiguate between a cached `0` and a `0` that's\n\t\t * returned as the default value when the metadata doesn't exist.\n\t\t */\n\t\tif ( false === $count || ! isset( $this->enrolled_students ) ) {\n\n\t\t\t$query = new LLMS_Student_Query(\n\t\t\t\tarray(\n\t\t\t\t\t'post_id'    => $this->get( 'id' ),\n\t\t\t\t\t'statuses'   => array( 'enrolled' ),\n\t\t\t\t\t'per_page'   => 1,\n\t\t\t\t\t'sort'       => array(\n\t\t\t\t\t\t'id' => 'ASC',\n\t\t\t\t\t),\n\t\t\t\t\t'count_only' => true,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$count = $query->get_count_only_result();\n\n\t\t\t// Cache result for later use.\n\t\t\t$this->set( 'enrolled_students', $count );\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the number of actively enrolled students in the course\n\t\t *\n\t\t * @since 4.12.0\n\t\t *\n\t\t * @param int         $count  Number of students enrolled in the course.\n\t\t * @param LLMS_Course $course Instance of the course object.\n\t\t */\n\t\t$count = apply_filters( 'llms_course_get_student_count', $count, $this );\n\n\t\treturn absint( $count );\n\t}\n\n\t/**\n\t * Get an array of student IDs based on enrollment status in the course\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string|string[] $statuses Optional. List of enrollment statuses to query by. Students matching at least one of the provided statuses will be returned. Default is 'enrolled'.\n\t * @param integer         $limit    Optional. Number of results. Default is `50`.\n\t * @param integer         $skip     Optional. Number of results to skip (for pagination). Default is `0`.\n\t * @return array\n\t */\n\tpublic function get_students( $statuses = 'enrolled', $limit = 50, $skip = 0 ) {\n\t\treturn llms_get_enrolled_students( $this->get( 'id' ), $statuses, $limit, $skip );\n\t}\n\n\t/**\n\t * Retrieve course tags\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param array $args Array of args passed to wp_get_post_terms.\n\t * @return array\n\t */\n\tpublic function get_tags( $args = array() ) {\n\t\treturn wp_get_post_terms( $this->get( 'id' ), 'course_tag', $args );\n\t}\n\n\t/**\n\t * Get the properties that will be explicitly excluded from the array representation of the model.\n\t *\n\t * This stub can be overloaded by an extending class and the property list is filterable via the\n\t * {@see llms_get_{$this->model_post_type}_excluded_to_array_properties} filter.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return string[]\n\t */\n\tprotected function get_to_array_excluded_properties() {\n\n\t\t/**\n\t\t * Disable course property exclusion while running `toArray()`.\n\t\t *\n\t\t * This hook is intended to allow developers to retain the functionality implemented\n\t\t * prior to the introduction of this hook.\n\t\t *\n\t\t * The LifterLMS developers consider the presence of these properties to be a bug but\n\t\t * acknowledge that the removal of these properties could be seen as a backwards incompatible\n\t\t * \"feature\" removal.\n\t\t *\n\t\t * This hook disables the exclusion of the following properties: 'average_grade', 'average_progress',\n\t\t * 'enrolled_students', 'last_data_calc_run', and 'temp_calc_data'. Any excluded properties added in the\n\t\t * future will not be excluded when using this hook.\n\t\t *\n\t\t * @example `add_filter( 'llms_course_to_array_disable_prop_exclusion', '__return_true' );`\n\t\t *\n\t\t * @since 5.4.1\n\t\t *\n\t\t * @param boolean $disable Whether or not to disable property exclusions.\n\t\t */\n\t\t$disable = apply_filters( 'llms_course_to_array_disable_prop_exclusion', false );\n\t\tif ( $disable ) {\n\t\t\treturn array();\n\t\t}\n\n\t\treturn array(\n\t\t\t'average_grade',\n\t\t\t'average_progress',\n\t\t\t'enrolled_students',\n\t\t\t'last_data_calc_run',\n\t\t\t'temp_calc_data',\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve course tracks\n\t *\n\t * @since 3.3.0\n\t *\n\t * @param array $args Array of args passed to wp_get_post_terms.\n\t * @return array\n\t */\n\tpublic function get_tracks( $args = array() ) {\n\t\treturn wp_get_post_terms( $this->get( 'id' ), 'course_track', $args );\n\t}\n\n\t/**\n\t * Retrieve an array of students currently enrolled in the course\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Use `LLMS_Course::get_students()`.\n\t *\n\t * @param integer $limit Number of results.\n\t * @param integer $skip Number of results to skip (for pagination).\n\t * @return array\n\t */\n\tpublic function get_enrolled_students( $limit, $skip ) {\n\t\treturn $this->get_students( 'enrolled', $limit, $skip );\n\t}\n\n\t/**\n\t * Get a user's percentage completion through the course\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.2 Unknown.\n\t *\n\t * @return float\n\t */\n\tpublic function get_percent_complete( $user_id = '' ) {\n\n\t\t$student = llms_get_student( $user_id );\n\t\tif ( ! $student ) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn $student->get_progress( $this->get( 'id' ), 'course' );\n\t}\n\n\t/**\n\t * Retrieve an instance of the LLMS_Product for this course\n\t *\n\t * @since 3.3.0\n\t *\n\t * @return LLMS_Product\n\t */\n\tpublic function get_product() {\n\t\treturn new LLMS_Product( $this->get( 'id' ) );\n\t}\n\n\t/**\n\t * Compare a course meta info date to the current date and get a bool\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $date_key Property key, eg \"start_date\" or \"enrollment_end_date\".\n\t * @return boolean Returns `true` when the date is in the past and `false` when the date is in the future.\n\t */\n\tpublic function has_date_passed( $date_key ) {\n\n\t\t$now  = current_time( 'timestamp' );\n\t\t$date = $this->get_date( $date_key, 'U' );\n\n\t\t/**\n\t\t * If there's no date, we can't make a comparison\n\t\t * so assume it's unset and unnecessary\n\t\t * so return 'false'.\n\t\t */\n\t\tif ( ! $date ) {\n\t\t\treturn false;\n\n\t\t}\n\n\t\treturn $now > $date;\n\t}\n\n\t/**\n\t * Determine if the course is at capacity based on course capacity settings\n\t *\n\t * @since 3.0.0\n\t * @since 3.15.0 Unknown.\n\t *\n\t * @return boolean Returns `true` if not at capacity & `false` if at or over capacity.\n\t */\n\tpublic function has_capacity() {\n\n\t\t// Capacity disabled, so there is capacity.\n\t\tif ( 'yes' !== $this->get( 'enable_capacity' ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t$capacity = $this->get( 'capacity' );\n\t\t// No capacity restriction set, so it has capacity.\n\t\tif ( ! $capacity ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Compare results.\n\t\treturn ( $this->get_student_count() < $capacity );\n\t}\n\n\t/**\n\t * Determine if prerequisites are enabled and there are prereqs configured\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.5 Unknown.\n\t *\n\t * @param string $type Determine if a specific type of prereq exists [any|course|track].\n\t * @return boolean Returns true if prereq is enabled and there is a prerequisite course or track.\n\t */\n\tpublic function has_prerequisite( $type = 'any' ) {\n\n\t\tif ( 'yes' === $this->get( 'has_prerequisite' ) ) {\n\n\t\t\tif ( 'any' === $type ) {\n\n\t\t\t\treturn ( $this->get( 'prerequisite' ) || $this->get( 'prerequisite_track' ) );\n\n\t\t\t} elseif ( 'course' === $type ) {\n\n\t\t\t\treturn ( $this->get( 'prerequisite' ) ) ? true : false;\n\n\t\t\t} elseif ( 'course_track' === $type ) {\n\n\t\t\t\treturn ( $this->get( 'prerequisite_track' ) ) ? true : false;\n\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determine if students can access course content based on the current date\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_enrollment_open() {\n\n\t\t// If no period is set, enrollment is automatically open.\n\t\tif ( 'yes' !== $this->get( 'enrollment_period' ) ) {\n\n\t\t\t$is_open = true;\n\n\t\t} else {\n\n\t\t\t$is_open = ( $this->has_date_passed( 'enrollment_start_date' ) && ! $this->has_date_passed( 'enrollment_end_date' ) );\n\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not course enrollment is open.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param boolean     $is_open Whether or not enrollment is open.\n\t\t * @param LLMS_Course $course  Course object.\n\t\t */\n\t\treturn apply_filters( 'llms_is_course_enrollment_open', $is_open, $this );\n\t}\n\n\t/**\n\t * Determine if students can access course content based on the current date\n\t *\n\t * Note that enrollment does not affect the outcome of this check as regardless\n\t * of enrollment, once a course closes content is locked.\n\t *\n\t * @since 3.0.0\n\t * @since 3.7.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_open() {\n\n\t\t// If a course time period is not enabled, just return true (content is accessible).\n\t\tif ( 'yes' !== $this->get( 'time_period' ) ) {\n\n\t\t\t$is_open = true;\n\n\t\t} else {\n\n\t\t\t$is_open = ( $this->has_date_passed( 'start_date' ) && ! $this->has_date_passed( 'end_date' ) );\n\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not the course is considered open based on the current date.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param boolean     $is_open Whether or not enrollment is open.\n\t\t * @param LLMS_Course $course  Course object.\n\t\t */\n\t\treturn apply_filters( 'llms_is_course_open', $is_open, $this );\n\t}\n\n\t/**\n\t * Determine if a prerequisite is completed for a student\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $type Type of prereq [course|track].\n\t * @return boolean\n\t */\n\tpublic function is_prerequisite_complete( $type = 'course', $student_id = null ) {\n\n\t\tif ( ! $student_id ) {\n\t\t\t$student_id = get_current_user_id();\n\t\t}\n\n\t\t// No user or no prereqs so no reason to proceed.\n\t\tif ( ! $student_id || ! $this->has_prerequisite( $type ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$prereq_id = $this->get_prerequisite_id( $type );\n\n\t\t// No prereq id of this type, no need to proceed.\n\t\tif ( ! $prereq_id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Setup student.\n\t\t$student = new LLMS_Student( $student_id );\n\n\t\treturn $student->is_complete( $prereq_id, $type );\n\t}\n\n\t/**\n\t * Save instructor information\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param array $instructors Array of course instructor information.\n\t */\n\tpublic function set_instructors( $instructors = array() ) {\n\n\t\treturn $this->instructors()->set_instructors( $instructors );\n\t}\n\n\t/**\n\t * Add data to the course model when converted to array\n\t *\n\t * Called before data is sorted and returned by $this->jsonSerialize().\n\t *\n\t * @since 3.3.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @param array $arr Data to be serialized.\n\t * @return array\n\t */\n\tpublic function toArrayAfter( $arr ) {\n\n\t\t$product             = $this->get_product();\n\t\t$arr['access_plans'] = array();\n\t\tforeach ( $product->get_access_plans( false, false ) as $p ) {\n\t\t\t$arr['access_plans'][] = $p->toArray();\n\t\t}\n\n\t\t$arr['sections'] = array();\n\t\tforeach ( $this->get_sections() as $s ) {\n\t\t\t$arr['sections'][] = $s->toArray();\n\t\t}\n\n\t\t$arr['categories'] = $this->get_categories(\n\t\t\tarray(\n\t\t\t\t'fields' => 'names',\n\t\t\t)\n\t\t);\n\t\t$arr['tags']       = $this->get_tags(\n\t\t\tarray(\n\t\t\t\t'fields' => 'names',\n\t\t\t)\n\t\t);\n\t\t$arr['tracks']     = $this->get_tracks(\n\t\t\tarray(\n\t\t\t\t'fields' => 'names',\n\t\t\t)\n\t\t);\n\n\t\t$arr['difficulty'] = $this->get_difficulty();\n\n\t\treturn $arr;\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.instructor.php",
    "content": "<?php\n/**\n * LifterLMS Instructor\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.13.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Instructor model class\n *\n * Manages data and interactions with a LifterLMS Instructor or Instructor's Assistant.\n *\n * @since 3.13.0\n * @since 3.30.3 Fixed typo in \"description\" key of the the toArray() method.\n * @since 3.32.0 Add validation to data passed into the `get_students()` method.\n * @since 3.34.0 Fix issue causing `get_assistants()` to return assistants to the currently logged in user instead of using the user id of the current object.\n *               Add `has_student()` method.\n */\nclass LLMS_Instructor extends LLMS_Abstract_User_Data {\n\n\t/**\n\t * Add a parent instructor to an assistant instructor\n\t *\n\t * @param    mixed $parent_ids WP User ID of the parent instructor or array of User IDs to add multiple\n\t * @return   boolean\n\t * @since    3.13.0\n\t * @version  3.14.4\n\t */\n\tpublic function add_parent( $parent_ids ) {\n\n\t\t// Get existing parents.\n\t\t$parents = $this->get( 'parent_instructors' );\n\n\t\t// No existing, use an empty array as the default.\n\t\tif ( ! $parents ) {\n\t\t\t$parents = array();\n\t\t}\n\n\t\tif ( ! is_array( $parent_ids ) ) {\n\t\t\t$parent_ids = array( $parent_ids );\n\t\t}\n\n\t\t// Make ints.\n\t\t$parent_ids = array_map( 'absint', $parent_ids );\n\n\t\t// Add the new parents.\n\t\t$parents = array_unique( array_merge( $parents, $parent_ids ) );\n\n\t\t// Remove duplicates and save.\n\t\treturn $this->set( 'parent_instructors', array_unique( $parents ) );\n\n\t}\n\n\t/**\n\t * Retrieve an array of user ids for assistant instructors attached to the instructor\n\t *\n\t * @since 3.14.4\n\t * @since 3.34.0 Uses object ID instead of current user id.\n\t *\n\t * @return int[]\n\t */\n\tpublic function get_assistants() {\n\n\t\tglobal $wpdb;\n\t\t$results = $wpdb->get_col(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT user_id FROM {$wpdb->usermeta}\n\t\t\t WHERE meta_key = 'llms_parent_instructors'\n\t\t\t   AND meta_value LIKE %s;\",\n\t\t\t\t'%i:' . $this->get_id() . ';%'\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t\treturn $results;\n\n\t}\n\n\t/**\n\t * Retrieve instructor's courses\n\t *\n\t * @uses     $this->get_posts()\n\t * @param    array  $args    query argument, see $this->get_posts()\n\t * @param    string $return  return format, see $this->get_posts()\n\t * @return   mixed\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function get_courses( $args = array(), $return = 'llms_posts' ) {\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'post_type' => 'course',\n\t\t\t)\n\t\t);\n\t\treturn $this->get_posts( $args, $return );\n\n\t}\n\n\t/**\n\t * Retrieve instructor's memberships\n\t *\n\t * @uses     $this->get_posts()\n\t * @param    array  $args    query argument, see $this->get_posts()\n\t * @param    string $return  return format, see $this->get_posts()\n\t * @return   mixed\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function get_memberships( $args = array(), $return = 'llms_posts' ) {\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_membership',\n\t\t\t)\n\t\t);\n\t\treturn $this->get_posts( $args, $return );\n\n\t}\n\n\t/**\n\t * Retrieve instructor's posts (courses and memberships, mixed)\n\t *\n\t * @param    array  $args    query arguments passed to WP_Query\n\t * @param    string $return  return format [llms_posts|ids|posts|query]\n\t * @return   mixed\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function get_posts( $args = array(), $return = 'llms_posts' ) {\n\n\t\t$serialized_id = serialize( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize\n\t\t\tarray(\n\t\t\t\t'id' => $this->get_id(),\n\t\t\t)\n\t\t);\n\t\t$serialized_id = str_replace( array( 'a:1:{', '}' ), '', $serialized_id );\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'post_type'   => array( 'course', 'llms_membership' ),\n\t\t\t\t'post_status' => 'publish',\n\t\t\t\t'meta_query'  => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'compare' => 'LIKE',\n\t\t\t\t\t\t'key'     => '_llms_instructors',\n\t\t\t\t\t\t'value'   => $serialized_id,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\t$query = new WP_Query( $args );\n\n\t\tif ( 'llms_posts' === $return ) {\n\t\t\t$ret = array();\n\t\t\tforeach ( $query->posts as $post ) {\n\t\t\t\t$ret[] = llms_get_post( $post );\n\t\t\t}\n\t\t\treturn $ret;\n\t\t} elseif ( 'ids' === $return ) {\n\t\t\treturn wp_list_pluck( $query->posts, 'ID' );\n\t\t} elseif ( 'posts' === $return ) {\n\t\t\treturn $query->posts;\n\t\t}\n\n\t\t// If 'query' === $return.\n\t\treturn $query;\n\n\t}\n\n\t/**\n\t * Retrieve instructor's students\n\t *\n\t * @since 3.13.0\n\t * @since 3.32.0 Validate `post_id` data passed into this function to ensure only students\n\t *               in courses/memberships for this instructor are returned.\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @see LLMS_Student_Query\n\t *\n\t * @param array $args Array of args passed to LLMS_Student_Query.\n\t * @return LLMS_Student_Query\n\t */\n\tpublic function get_students( $args = array() ) {\n\n\t\t$ids = $this->get_posts(\n\t\t\tarray(\n\t\t\t\t'posts_per_page' => -1,\n\t\t\t),\n\t\t\t'ids'\n\t\t);\n\n\t\t// If post IDs were passed we need to verify they're IDs that the instructor has access to.\n\t\tif ( ! empty( $args['post_id'] ) ) {\n\t\t\t$args['post_id'] = ! is_array( $args['post_id'] ) ? array( $args['post_id'] ) : $args['post_id'];\n\t\t\t$args['post_id'] = array_intersect( $args['post_id'], $ids );\n\t\t} else {\n\t\t\t// No post IDs passed in, query all of the instructor's posts.\n\t\t\t$args['post_id'] = $ids;\n\t\t}\n\t\t// The instructor has no posts, so we want to force no results.\n\t\t// @todo add an instructor query parameter to the student query.\n\t\tif ( empty( $args['post_id'] ) ) {\n\t\t\t$args['per_page']      = 0;\n\t\t\t$args['no_found_rows'] = true;\n\t\t}\n\n\t\treturn new LLMS_Student_Query( $args );\n\n\t}\n\n\t/**\n\t * Determines if the instructor is an instructor to a specific student.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @param LLMS_Student|WP_User|int $student Student or user object or WP User ID.\n\t * @return bool\n\t */\n\tpublic function has_student( $student ) {\n\n\t\t$student = llms_get_student( $student );\n\t\tif ( ! $student ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$ids = $this->get_posts(\n\t\t\tarray(\n\t\t\t\t'posts_per_page' => -1,\n\t\t\t),\n\t\t\t'ids'\n\t\t);\n\n\t\tif ( ! $ids ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $student->is_enrolled( $ids, 'any' );\n\n\t}\n\n\t/**\n\t * Determine if the user is an instructor on a post\n\t *\n\t * @param    int $post_id  WP Post ID of a course or membership\n\t * @return   boolean\n\t * @since    3.13.0\n\t * @version  3.13.0\n\t */\n\tpublic function is_instructor( $post_id = null ) {\n\n\t\t$ret = false;\n\n\t\t// Use current post if no post is set.\n\t\tif ( ! $post_id ) {\n\t\t\tglobal $post;\n\t\t\tif ( ! $post ) {\n\t\t\t\treturn apply_filters( 'llms_instructor_is_instructor', $ret, $post_id, $this );\n\t\t\t}\n\t\t\t$post_id = $post->ID;\n\t\t}\n\n\t\t$check_id = false;\n\n\t\tswitch ( get_post_type( $post_id ) ) {\n\n\t\t\tcase 'course':\n\t\t\t\t$check_id = $post_id;\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_membership':\n\t\t\t\t$check_id = $post_id;\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_question':\n\t\t\t\t$question = llms_get_post( $post_id );\n\t\t\t\t$check_id = array();\n\t\t\t\tforeach ( $question->get_quizzes() as $qid ) {\n\t\t\t\t\t$course = llms_get_post_parent_course( $qid );\n\t\t\t\t\tif ( $course ) {\n\t\t\t\t\t\t$check_id[] = $course->get( 'id' );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$course = llms_get_post_parent_course( $post_id );\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$check_id = $course->get( 'id' );\n\t\t\t\t}\n\t\t}\n\n\t\tif ( $check_id ) {\n\n\t\t\t$check_ids = ! is_array( $check_id ) ? array( $check_id ) : $check_id;\n\n\t\t\t$query = $this->get_posts(\n\t\t\t\tarray(\n\t\t\t\t\t'post__in'       => $check_ids,\n\t\t\t\t\t'posts_per_page' => 1,\n\t\t\t\t),\n\t\t\t\t'query'\n\t\t\t);\n\n\t\t\t$ret = $query->have_posts();\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_instructor_is_instructor', $ret, $post_id, $check_id, $this );\n\n\t}\n\n\t/**\n\t * Used by exporter / cloner to get instructor data\n\t *\n\t * @since 3.16.11\n\t * @since 3.30.3 Renamed \"descrpition\" key to \"description\".\n\t *\n\t * @return array\n\t */\n\tpublic function toArray() {\n\t\treturn array(\n\t\t\t'description' => $this->get( 'description' ),\n\t\t\t'email'       => $this->get( 'user_email' ),\n\t\t\t'first_name'  => $this->get( 'first_name' ),\n\t\t\t'id'          => $this->get_id(),\n\t\t\t'last_name'   => $this->get( 'last_name' ),\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.lesson.php",
    "content": "<?php\n/**\n * LifterLMS Lesson Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 1.0.0\n * @version 6.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Lesson model class\n *\n * @since 1.0.0\n * @since 3.29.0 Unknown.\n * @since 3.36.2 When getting the lesson's available date: add available number of days to the course start date only if there's a course start date.\n * @since 4.0.0 Remove deprecated methods.\n * @since 4.4.0 Improve the query used to retrieve the previous/next so that we don't miss sibling lessons within the same section\n *              if the previous/next one(s) status is (are) not published. Make sure to always return `false` if no previous lesson is found.\n *              Use strict comparisons where needed.\n * @since 5.3.0 Move audio and video embed methods to `LLMS_Trait_Audio_Video_Embed`.\n * @since 5.7.0 Deprecated the `LLMS_Lesson::get_order()` method in favor of the `LLMS_Lesson::get( 'order' )` method.\n *              Deprecated the `LLMS_Lesson::get_parent_course()` method in favor of the `LLMS_Lesson::get( 'parent_course' )` method.\n *              Deprecated the `LLMS_Lesson::set_parent_course()` method in favor of the `LLMS_Lesson::set( 'parent_course', $course_id )` method.\n *\n * @property string $audio_embed                      URL to an oEmbed enable audio URL.\n * @property string $date_available                   Date when lesson becomes available, applies when $drip_method is \"date\".\n * @property int    $days_before_available            The number of days before the lesson is available, applies when $drip_method is \"enrollment\" or \"start\".\n * @property string $drip_method                      What sort of drip method to utilize [''(none)|date|enrollment|start|prerequisite].\n * @property string $free_lesson                      Yes if the lesson is free [yes|no].\n * @property string $has_prerequisite                 Yes if the lesson has a prereq lesson [yes|no].\n * @property int    $order                            Lesson's order within its parent section.\n * @property int    $points                           Number of points assigned to the lesson, used to calculate the weight of the lesson when grading courses.\n * @property int    $prerequisite                     WP Post ID of the prerequisite lesson, only if $has_prerequisite is 'yes'.\n * @property int    $parent_course                    WP Post ID of the course the lesson belongs to.\n * @property int    $parent_section                   WP Post ID of the section the lesson belongs to.\n * @property int    $quiz                             WP Post ID of the llms_quiz.\n * @property string $quiz_enabled                     Whether or not the attached quiz is enabled for students [yes|no].\n * @property string $require_passing_grade            Whether of not students have to pass the quiz to advance to the next lesson [yes|no].\n * @property string $require_assignment_passing_grade Whether of not students have to pass the assignment to advance to the next lesson [yes|no].\n * @property string $time_available                   Optional time to make lesson available on $date_available when $drip_method is \"date\".\n * @property string $video_embed                      URL to an oEmbed enable video URL.\n * @property string $content_added_in_builder         Whether content was (at least initially) added within the page builder.\n */\nclass LLMS_Lesson extends LLMS_Post_Model {\n\n\tuse LLMS_Trait_Audio_Video_Embed;\n\n\tprotected $properties = array(\n\n\t\t'order'                            => 'absint',\n\n\t\t// Drippable.\n\t\t'days_before_available'            => 'absint',\n\t\t'date_available'                   => 'text',\n\t\t'drip_method'                      => 'text',\n\t\t'time_available'                   => 'text',\n\n\t\t// Parent element.\n\t\t'parent_course'                    => 'absint',\n\t\t'parent_section'                   => 'absint',\n\n\t\t'free_lesson'                      => 'yesno',\n\t\t'has_prerequisite'                 => 'yesno',\n\t\t'prerequisite'                     => 'absint',\n\t\t'require_passing_grade'            => 'yesno',\n\t\t'require_assignment_passing_grade' => 'yesno',\n\t\t'points'                           => 'absint',\n\n\t\t'content_added_in_builder'         => 'yesno',\n\n\t\t// Quizzes.\n\t\t'quiz'                             => 'absint',\n\t\t'quiz_enabled'                     => 'yesno',\n\n\t);\n\n\t/**\n\t * Associative array of default property values\n\t *\n\t * @since 3.24.0\n\t * @var array\n\t */\n\tprotected $property_defaults = array(\n\t\t'points' => 1,\n\t);\n\n\t/**\n\t * Name of the post type as stored in the database\n\t *\n\t * @since unknown\n\t * @var string\n\t */\n\tprotected $db_post_type = 'lesson';\n\n\t/**\n\t * Post type name\n\t *\n\t * To use unprefixed post type names for filters and more.\n\t *\n\t * @since unknown\n\t * @var string\n\t */\n\tprotected $model_post_type = 'lesson';\n\n\t/**\n\t * Constructor for this class and the traits it uses.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param string|int|LLMS_Post_Model|WP_Post $model 'new', WP post id, instance of an extending class, instance of WP_Post.\n\t * @param array                              $args  Args to create the post, only applies when $model is 'new'.\n\t */\n\tpublic function __construct( $model, $args = array() ) {\n\n\t\t$this->construct_audio_video_embed();\n\t\tparent::__construct( $model, $args );\n\t}\n\n\t/**\n\t * Get the date a lesson became or will become available according to element drip settings\n\t *\n\t * If there are no drip settings, the published date of the element will be returned.\n\t *\n\t * @since 3.16.0\n\t * @since 3.36.2 Add available number of days to the course start date only if there's a course start date.\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n\t *\n\t * @param string $format Optional. Date format (passed to date_i18n()). Default is empty string.\n\t *                       When not specified the WP Core date + time formats will be used.\n\t * @return string\n\t */\n\tpublic function get_available_date( $format = '' ) {\n\n\t\tif ( ! $format ) {\n\t\t\t$format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );\n\t\t}\n\n\t\t$drip_method = $this->get( 'drip_method' );\n\n\t\t$days = $this->get( 'days_before_available' ) * DAY_IN_SECONDS;\n\n\t\t// Default availability is the element's post date.\n\t\t$available = $this->get_date( 'date', 'U' );\n\n\t\t// get the course setting first, if any.\n\t\t$course = $this->get_course();\n\t\tif ( $course && 'yes' === $course->get( 'lesson_drip' ) ) {\n\t\t\t$course_drip_method = $course->get( 'drip_method' );\n\n\t\t\tswitch ( $course_drip_method ) {\n\t\t\t\tcase 'start':\n\t\t\t\t\t$ignore_lessons = intval( $course->get( 'ignore_lessons' ) );\n\t\t\t\t\t$course_lessons = $course->get_lessons( 'ids' );\n\t\t\t\t\t$lesson_number  = array_search( $this->get( 'id' ), $course_lessons ) + 1;\n\n\t\t\t\t\t$course_days            = $course->get( 'days_before_available' ) * DAY_IN_SECONDS;\n\t\t\t\t\t$course_start_date      = $course->get_date( 'start_date', 'U' );\n\t\t\t\t\t$course_enrollment_date = llms_get_student() ? llms_get_student()->get_enrollment_date( $course->get( 'id' ), 'enrolled', 'U' ) : false;\n\n\t\t\t\t\t// If it's one of the first X lessons in a course, return availability based on published date.\n\t\t\t\t\tif ( $lesson_number <= $ignore_lessons ) {\n\t\t\t\t\t\treturn date_i18n( $format, $available );\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( $course_start_date || $course_enrollment_date ) {\n\t\t\t\t\t\t$available = ( ( $lesson_number - $ignore_lessons ) * $course_days ) + ( $course_start_date ? $course_start_date : $course_enrollment_date );\n\n\t\t\t\t\t\treturn date_i18n( $format, $available );\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tswitch ( $drip_method ) {\n\n\t\t\t// Available on a specific date / time.\n\t\t\tcase 'date':\n\t\t\t\t$date = $this->get( 'date_available' );\n\t\t\t\t$time = $this->get( 'time_available' );\n\n\t\t\t\tif ( ! $time ) {\n\t\t\t\t\t$time = '12:00 AM';\n\t\t\t\t}\n\n\t\t\t\t$available = strtotime( $date . ' ' . $time );\n\n\t\t\t\tbreak;\n\n\t\t\t// Available # of days after enrollment in course.\n\t\t\tcase 'enrollment':\n\t\t\t\t$student = llms_get_student();\n\t\t\t\tif ( $student ) {\n\t\t\t\t\t$available = $days + $student->get_enrollment_date( $this->get( 'parent_course' ), 'enrolled', 'U' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'prerequisite':\n\t\t\t\tif ( $this->has_prerequisite() ) {\n\t\t\t\t\t$student = llms_get_student();\n\t\t\t\t\tif ( $student ) {\n\t\t\t\t\t\t$date = $student->get_completion_date( $this->get( 'prerequisite' ), 'U' );\n\t\t\t\t\t\tif ( $date ) {\n\t\t\t\t\t\t\t$available = $days + $date;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\t// Available # of days after course start date.\n\t\t\tcase 'start':\n\t\t\t\t$course            = $this->get_course();\n\t\t\t\t$course_start_date = $course ? $course->get_date( 'start_date', 'U' ) : '';\n\n\t\t\t\tif ( $course_start_date ) {\n\t\t\t\t\t$available = $days + $course_start_date;\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn date_i18n( $format, $available );\n\t}\n\n\t/**\n\t * Retrieve an instance of LLMS_Course for the element's parent course\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return LLMS_Course|null Returns `null` if the lesson is not attached to any courses.\n\t */\n\tpublic function get_course() {\n\n\t\t$course_id = $this->get( 'parent_course' );\n\t\tif ( ! $course_id ) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn llms_get_post( $course_id );\n\t}\n\n\t/**\n\t * An array of default arguments to pass to $this->create() when creating a new post.\n\t *\n\t * @since 3.13.0\n\t * @since 6.3.0 Retrieve `comment_status` parameter value from the global discussion settings.\n\t *\n\t * @param array $args Optional. Args of data to be passed to `wp_insert_post()`. Default `null`.\n\t * @return array\n\t */\n\tprotected function get_creation_args( $args = null ) {\n\n\t\t// Allow nothing to be passed in.\n\t\tif ( empty( $args ) ) {\n\t\t\t$args = array();\n\t\t}\n\n\t\t// Backwards compat to original 3.0.0 format when just a title was passed in.\n\t\tif ( is_string( $args ) ) {\n\t\t\t$args = array(\n\t\t\t\t'post_title' => $args,\n\t\t\t);\n\t\t}\n\n\t\t$post_type = $this->get( 'db_post_type' );\n\t\t$args      = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'comment_status' => get_default_comment_status( $post_type ),\n\t\t\t\t'ping_status'    => 'closed',\n\t\t\t\t'post_author'    => get_current_user_id(),\n\t\t\t\t'post_content'   => '',\n\t\t\t\t'post_excerpt'   => '',\n\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t'post_title'     => '',\n\t\t\t\t'post_type'      => $post_type,\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Filter the model creation args\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->model_post_type`, refers to model post type.\n\t\t *\n\t\t * @since unknown\n\t\t *\n\t\t * @param array       $args   Args of data to be passed to `wp_insert_post()`.\n\t\t * @param LLMS_Lesson $lesson Instance of the LLMS_Lesson.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->model_post_type}_get_creation_args\", $args, $this );\n\t}\n\n\t/**\n\t * Retrieves the lesson's order within its parent section\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Unknown.\n\t * @deprecated 5.7.0 Use `LLMS_Lesson::get( 'order' )`, via {@see LLMS_Post_Model::get()}, instead.\n\t *\n\t * @return int\n\t */\n\tpublic function get_order() {\n\n\t\tllms_deprecated_function( __METHOD__, '5.7.0', __CLASS__ . '::get( \\'order\\' )' );\n\n\t\treturn $this->get( 'order' );\n\t}\n\n\t/**\n\t * Get parent course id\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Unknown.\n\t * @deprecated 5.7.0 Use `LLMS_Lesson::get( 'parent_course' )`, via {@see LLMS_Post_Model::get()}, instead.\n\t *\n\t * @return int\n\t */\n\tpublic function get_parent_course() {\n\n\t\tllms_deprecated_function( __METHOD__, '5.7.0', __CLASS__ . '::get( \\'parent_course\\' )' );\n\n\t\treturn absint( get_post_meta( $this->get( 'id' ), '_llms_parent_course', true ) );\n\t}\n\n\t/**\n\t * Get parent section id\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Unknown.\n\t *\n\t * @return int\n\t */\n\tpublic function get_parent_section() {\n\t\treturn absint( get_post_meta( $this->get( 'id' ), '_llms_parent_section', true ) );\n\t}\n\n\t/**\n\t * Get CSS classes to display on the course syllabus .llms-lesson-preview element\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_preview_classes() {\n\n\t\t$classes = '';\n\n\t\tif ( $this->is_complete() ) {\n\t\t\t$classes = ' is-complete has-icon';\n\t\t} elseif ( apply_filters( 'lifterlms_display_lesson_complete_placeholders', true ) && llms_is_user_enrolled( get_current_user_id(), $this->get( 'id' ) ) ) {\n\t\t\t$classes = ' is-incomplete has-icon';\n\t\t} elseif ( $this->is_free() ) {\n\t\t\t$classes = ' is-free has-icon';\n\t\t} else {\n\t\t\t$classes = ' is-incomplete';\n\t\t}\n\n\t\tif ( get_queried_object_id() === intval( $this->get( 'id' ) ) ) {\n\t\t\t$classes .= ' current-lesson';\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_preview_classes', $classes );\n\t}\n\n\t/**\n\t * Get HTML of the icon to display in the .llms-lesson-preview element on the syllabus\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_preview_icon_html() {\n\n\t\t$html = '';\n\n\t\tif ( llms_is_user_enrolled( get_current_user_id(), $this->get( 'id' ) ) ) {\n\n\t\t\tif ( $this->is_complete() || apply_filters( 'lifterlms_display_lesson_complete_placeholders', true ) ) {\n\n\t\t\t\t$html = '<span class=\"llms-lesson-complete\"><i class=\"fa fa-' . apply_filters( 'lifterlms_lesson_complete_icon', 'check-circle' ) . '\"></i></span>';\n\n\t\t\t}\n\t\t} elseif ( $this->is_free() ) {\n\n\t\t\t$html = '<span class=\"llms-icon-free\">' . __( 'FREE', 'lifterlms' ) . '</span>';\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_get_preview_icon_html', $html );\n\t}\n\n\t/**\n\t * Retrieve an instance of LLMS_Course for the elements's parent section\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return LLMS_Section|null Returns `null` it the lesson is not attached to any sections.\n\t */\n\tpublic function get_section() {\n\n\t\t$section_id = $this->get( 'parent_section' );\n\t\tif ( ! $section_id ) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn llms_get_post( $section_id );\n\t}\n\n\t/**\n\t * Retrieve an object for the assigned quiz (if a quiz is assigned)\n\t *\n\t * @since 3.3.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @return LLMS_Quiz|false Returns `false` if the lesson has no existing quiz assigned.\n\t */\n\tpublic function get_quiz() {\n\t\tif ( $this->has_quiz() ) {\n\t\t\t$quiz = llms_get_post( $this->get( 'quiz' ) );\n\t\t\tif ( $quiz ) {\n\t\t\t\treturn $quiz;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determine if lesson prereq is enabled and a prereq lesson is selected\n\t *\n\t * @since 3.0.0\n\t * @since 4.4.0 Use strict comparison.\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_prerequisite() {\n\n\t\treturn ( 'yes' === $this->get( 'has_prerequisite' ) && $this->get( 'prerequisite' ) );\n\t}\n\n\t/**\n\t * Determine if the slug (post name) of a lesson has been modified\n\t *\n\t * Ensures that lessons created via the builder with \"New Lesson\" as the title (default slug \"new-lesson-{$num}\")\n\t * have their slug renamed when the title is renamed for the first time.\n\t *\n\t * @since 3.14.8\n\t *\n\t * @return bool\n\t */\n\tpublic function has_modified_slug() {\n\n\t\t$default = sanitize_title( __( 'New Lesson', 'lifterlms' ) );\n\t\treturn ( false === strpos( $this->get( 'name' ), $default ) );\n\t}\n\n\t/**\n\t * Determine if a quiz is assigned to this lesson\n\t *\n\t * @since 3.3.0\n\t * @since 3.29.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_quiz() {\n\t\treturn $this->get( 'quiz' ) ? true : false;\n\t}\n\n\t/**\n\t * Determine if an element is available based on drip settings\n\t *\n\t * If no settings, this will return true if the posts's published\n\t * date is in the past.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_available() {\n\n\t\t$drip_method        = $this->get( 'drip_method' );\n\t\t$course_drip_method = $this->get_course() ? 'yes' === $this->get_course()->get( 'lesson_drip' ) && $this->get_course()->get( 'drip_method' ) : '';\n\n\t\t// Drip is not enabled, so the element is available.\n\t\tif ( ! $drip_method && ! $course_drip_method ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t$available = $this->get_available_date( 'U' );\n\t\t$now       = llms_current_time( 'timestamp' );\n\n\t\treturn ( $now >= $available );\n\t}\n\n\t/**\n\t * Determine if the lesson has been completed by a specific user\n\t *\n\t * @since 1.0.0\n\t * @since 3.0.0 Refactored to utilize LLMS_Student->is_complete().\n\t *              Added $user_id param.\n\t *\n\t * @param int $user_id Optional. WP_User ID of a student. Default `null`.\n\t *                     If not provided, or a falsy is provided, will fall back on the current user id.\n\t * @return bool\n\t */\n\tpublic function is_complete( $user_id = null ) {\n\n\t\t$user_id = $user_id ? $user_id : get_current_user_id();\n\n\t\t// Incomplete b/c no user.\n\t\tif ( ! $user_id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$student = new LLMS_Student( $user_id );\n\n\t\treturn $student->is_complete( $this->get( 'id' ), 'lesson' );\n\t}\n\n\n\t/**\n\t * Determine if a the lesson is marked as \"free\"\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_free() {\n\t\treturn ( 'yes' === $this->get( 'free_lesson' ) );\n\t}\n\n\t/**\n\t * Determine if the lesson is an orphan\n\t *\n\t * @since 3.14.8\n\t * @since 4.4.0 Use `in_array()` with strict comparison to decide whether the parent course/section post status\n\t *                  is in a set of allowed statuses.\n\t * @return bool\n\t */\n\tpublic function is_orphan() {\n\n\t\t$statuses = array( 'publish', 'future', 'draft', 'pending', 'private', 'auto-draft' );\n\n\t\tforeach ( array( 'course', 'section' ) as $parent ) {\n\n\t\t\t$parent_id = $this->get( sprintf( 'parent_%s', $parent ) );\n\n\t\t\tif ( ! $parent_id ) {\n\t\t\t\treturn true;\n\t\t\t} elseif ( ! in_array( get_post_status( $parent_id ), $statuses, true ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determines if a quiz is enabled for the lesson\n\t *\n\t * Lesson must have a quiz and the quiz must be enabled.\n\t *\n\t * @since 3.16.0\n\t * @since 3.18.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_quiz_enabled() {\n\t\treturn ( $this->has_quiz() && llms_parse_bool( $this->get( 'quiz_enabled' ) ) && 'publish' === get_post_status( $this->get( 'quiz' ) ) );\n\t}\n\n\t/**\n\t * Add data to the course model when converted to array\n\t *\n\t * Called before data is sorted and returned by $this->jsonSerialize().\n\t *\n\t * @since 3.3.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param array $arr Data to be serialized.\n\t * @return array\n\t */\n\tpublic function toArrayAfter( $arr ) {\n\n\t\tif ( $this->has_quiz() ) {\n\n\t\t\t$quiz = $this->get_quiz();\n\t\t\tif ( $quiz ) {\n\t\t\t\t$arr['quiz'] = $quiz->toArray();\n\t\t\t}\n\t\t}\n\n\t\treturn $arr;\n\t}\n\n\t/**\n\t * Update object data\n\t *\n\t * @since unknown.\n\t *\n\t * @param array $data Data to update as key=>val.\n\t * @return array\n\t */\n\tpublic function update( $data ) {\n\n\t\t$updated_values = array();\n\n\t\tforeach ( $data as $key => $value ) {\n\t\t\t$method = 'set_' . $key;\n\n\t\t\tif ( method_exists( $this, $method ) ) {\n\t\t\t\t$updated_value = $this->$method( $value );\n\n\t\t\t\t$updated_values[ $key ] = $updated_value;\n\n\t\t\t}\n\t\t}\n\n\t\treturn $updated_values;\n\t}\n\n\t/**\n\t * Set lesson title\n\t *\n\t * @since unknown\n\t *\n\t * @param string $title The lesson title.\n\t * @return false|array False if the title couldn't be updated. An array of the type\n\t *                     array(\n\t *                         'id'    => lesson id,\n\t *                         'title' => the new title,\n\t *                     )\n\t *                     otherwise.\n\t */\n\tpublic function set_title( $title ) {\n\n\t\treturn LLMS_Post_Handler::update_title( $this->id, $title );\n\t}\n\n\t/**\n\t * Set lesson's excerpt\n\t *\n\t * @since unknown\n\t *\n\t * @param string $excerpt The lesson excerpt.\n\t * @return false|array False if the title couldn't be updated. An array of the type\n\t *                     array(\n\t *                         'id'           => lesson id,\n\t *                         'post_excerpt' => the new excerpt,\n\t *                     )\n\t *                     otherwise.\n\t */\n\tpublic function set_excerpt( $excerpt ) {\n\n\t\treturn LLMS_Post_Handler::update_excerpt( $this->id, $excerpt );\n\t}\n\n\t/**\n\t * Set parent section\n\t *\n\t * Sets parent section in database.\n\t *\n\t * @since unknown\n\t *\n\t * @param int $section_id The WP Post ID of the section to be set as parent.\n\t * @return mixed $meta If meta didn't exist returns the meta_id else t/f if update success.\n\t *                     Returns `false` if the provided section id value was already set.\n\t */\n\tpublic function set_parent_section( $section_id ) {\n\n\t\treturn update_post_meta( $this->id, '_llms_parent_section', $section_id );\n\t}\n\n\t/**\n\t * Set order\n\t *\n\t * Sets lesson order within the parent sectionin database\n\t *\n\t * @since unknown\n\t *\n\t * @param int $order The new order\n\t * @return mixed $meta If meta didn't exist returns the meta_id else t/f if update success.\n\t *                     Returns `false` if the provided order value was already set.\n\t */\n\tpublic function set_order( $order ) {\n\n\t\treturn update_post_meta( $this->id, '_llms_order', $order );\n\t}\n\n\t/**\n\t * Set parent course\n\t *\n\t * Sets parent course in database\n\t *\n\t * @since Unknown Introduced.\n\t * @deprecated 5.7.0 Use `LLMS_Lesson::set( 'parent_course', $course_id )`, via {@see LLMS_Post_Model::set()}, instead.\n\t *\n\t * @param int $course_id The WP Post ID of the course to be set as parent.\n\t * @return int|bool If meta didn't exist returns the meta_id else t/f if update success.\n\t *                  Returns `false` if the course id value was already set.\n\t */\n\tpublic function set_parent_course( $course_id ) {\n\n\t\tllms_deprecated_function( __METHOD__, '5.7.0', __CLASS__ . '::set( \\'parent_course\\', $course_id )' );\n\n\t\treturn update_post_meta( $this->id, '_llms_parent_course', $course_id );\n\t}\n\n\t/**\n\t * Get the lesson prerequisite\n\t *\n\t * @since unknown\n\t *\n\t * @return int ID of the prerequisite post.\n\t */\n\tpublic function get_prerequisite() {\n\n\t\tif ( $this->has_prerequisite ) {\n\n\t\t\treturn $this->prerequisite;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Get whether the lesson has a content set\n\t *\n\t * @since unknown\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_content() {\n\t\tif ( ! empty( $this->post->post_content ) ) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Get next lesson ID\n\t *\n\t * @since 1.0.0\n\t * @since 3.24.0\n\t * @since 4.4.0 Improve query so that unpublished siblings do not break expected results.\n\t * @since 4.4.2 Use a numeric comparison for the previous position meta query.\n\t * @since 4.10.2 Refactor to use helper method `get_sibling()`.\n\t *\n\t * @return false|int ID of the next lesson, if any, `false` otherwise.\n\t */\n\tpublic function get_next_lesson() {\n\n\t\treturn $this->get_sibling( 'next' );\n\t}\n\n\t/**\n\t * Get previous lesson ID\n\t *\n\t * @since 1.0.0\n\t * @since 3.24.0 Unknown.\n\t * @since 4.4.0 Improve query so that unpublished siblings do not break expected results.\n\t *              Use strict comparisons where needed.\n\t *              Make sure to always return `false` if no previous lesson is found.\n\t * @since 4.4.2 Use a numeric comparison for the previous position meta query.\n\t * @since 4.10.2 Refactor to use helper method `get_sibling()`.\n\t *\n\t * @return false|int WP_Post ID of the previous lesson or `false` if one doesn't exist.\n\t */\n\tpublic function get_previous_lesson() {\n\n\t\treturn $this->get_sibling( 'prev' );\n\t}\n\n\t/**\n\t * Retrieve the sibling lesson in a specified direction\n\t *\n\t * @since 4.10.2\n\t *\n\t * @param string $direction Direction of navigation. Accepts either \"prev\" or \"next\".\n\t * @return false|int WP_Post ID of the sibling lesson or `false` if one doesn't exist.\n\t */\n\tprotected function get_sibling( $direction ) {\n\n\t\t$lesson = $this->get_sibling_lesson_query( $direction );\n\n\t\t// No lesson found within the section, look within the sibling section.\n\t\tif ( ! $lesson ) {\n\t\t\t$lesson = $this->get_sibling_section_query( $direction );\n\t\t}\n\n\t\treturn $lesson;\n\t}\n\n\t/**\n\t * Performs a query to retrieve a sibling lesson in the specified direction\n\t *\n\t * This method tries to locate a sibling lesson in the next or previous position.\n\t *\n\t * It *does not* account for lessons in a sibling section. For example, if the lesson\n\t * is the last lesson in a section this function will *not* locate the first lesson\n\t * in the course's next section. For this reason this function should not be relied upon\n\t * alone.\n\t *\n\t * @since 4.10.2\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_order()` method with `LLMS_Lesson::get( 'order' )`.\n\t *\n\t * @param string $direction Direction of navigation. Accepts either \"prev\" or \"next\".\n\t * @return false|int WP_Post ID of the sibling lesson or `false` if one doesn't exist.\n\t */\n\tprotected function get_sibling_lesson_query( $direction ) {\n\n\t\t$curr_position = $this->get( 'order' );\n\n\t\t// First cannot have a previous.\n\t\tif ( 1 === $curr_position && 'prev' === $direction ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( 'next' === $direction ) {\n\t\t\t$sibling_position = $curr_position + 1;\n\t\t\t$order            = 'ASC';\n\t\t\t$comparator       = '>=';\n\t\t} elseif ( 'prev' === $direction ) {\n\t\t\t$sibling_position = $curr_position - 1;\n\t\t\t$order            = 'DESC';\n\t\t\t$comparator       = '<=';\n\t\t}\n\n\t\t$args = array(\n\t\t\t'posts_per_page' => 1,\n\t\t\t'post_type'      => 'lesson',\n\t\t\t'nopaging'       => true,\n\t\t\t'post_status'    => 'publish',\n\t\t\t'meta_key'       => '_llms_order', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t'orderby'        => 'meta_value_num',\n\t\t\t'order'          => $order,\n\t\t\t'meta_query'     => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query\n\t\t\t\t'relation' => 'AND',\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_parent_section',\n\t\t\t\t\t'value'   => $this->get_parent_section(),\n\t\t\t\t\t'compare' => '=',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'key'     => '_llms_order',\n\t\t\t\t\t'value'   => $sibling_position,\n\t\t\t\t\t'compare' => $comparator,\n\t\t\t\t\t'type'    => 'numeric',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t/**\n\t\t * Filter the WP_Query arguments used to locate a sibling lesson for the specified lesson.\n\t\t *\n\t\t * @since 4.10.2\n\t\t *\n\t\t * @param array       $args      WP_Query arguments array.\n\t\t * @param string      $direction Navigation direction. Either \"prev\" or \"next\".\n\t\t * @param LLMS_Lesson $lesson    Current lesson object.\n\t\t */\n\t\t$args = apply_filters( 'llms_lesson_get_sibling_lesson_query_args', $args, $direction );\n\n\t\t$lessons = get_posts( $args );\n\n\t\treturn empty( $lessons ) ? false : $lessons[0]->ID;\n\t}\n\n\t/**\n\t * Performs a query to retrieve sibling lessons from the lesson's adjacent section\n\t *\n\t * This will retrieve either the first lesson from the course's next section or the last\n\t * lesson from the course's previous section.\n\t *\n\t * @since 4.10.2\n\t * @since 4.11.0 Fix PHP Notice when trying to retrieve next lesson from an empty section.\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Section::get_order()` method with `LLMS_Section::get( 'order' )`.\n\t *\n\t * @param string $direction Direction of navigation. Accepts either \"prev\" or \"next\".\n\t * @return false|int WP_Post ID of the sibling lesson or `false` if one doesn't exist.\n\t */\n\tprotected function get_sibling_section_query( $direction ) {\n\n\t\t$sibling_lesson = false;\n\t\t$curr_section   = $this->get_section();\n\n\t\t// Ensure we're not working with an orphan.\n\t\tif ( $curr_section ) {\n\n\t\t\t$curr_position = $curr_section->get( 'order' );\n\n\t\t\t// First cannot have a previous.\n\t\t\tif ( 1 === $curr_position && 'prev' === $direction ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif ( 'next' === $direction ) {\n\t\t\t\t$sibling_position = $curr_position + 1;\n\t\t\t\t$order            = 'ASC';\n\t\t\t} elseif ( 'prev' === $direction ) {\n\t\t\t\t$sibling_position = $curr_position - 1;\n\t\t\t\t$order            = 'DESC';\n\t\t\t}\n\n\t\t\t$args = array(\n\t\t\t\t'post_type'      => 'section',\n\t\t\t\t'posts_per_page' => 1,\n\t\t\t\t'nopaging'       => true,\n\t\t\t\t'meta_key'       => '_llms_order', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t\t'orderby'        => 'meta_value_num',\n\t\t\t\t'order'          => $order,\n\t\t\t\t'meta_query'     => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query\n\t\t\t\t\t'relation' => 'AND',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => '_llms_parent_course',\n\t\t\t\t\t\t'value'   => $this->get( 'parent_course' ),\n\t\t\t\t\t\t'compare' => '=',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => '_llms_order',\n\t\t\t\t\t\t'value'   => $sibling_position,\n\t\t\t\t\t\t'compare' => '=',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\t/**\n\t\t\t * Filter the WP_Query arguments used to locate a sibling lesson from a sibling section for the specified lesson.\n\t\t\t *\n\t\t\t * @since 4.10.2\n\t\t\t *\n\t\t\t * @param array       $args      WP_Query arguments array.\n\t\t\t * @param string      $direction Navigation direction. Either \"prev\" or \"next\".\n\t\t\t * @param LLMS_Lesson $lesson    Current lesson object.\n\t\t\t */\n\t\t\t$args = apply_filters( 'llms_lesson_get_sibling_section_query_args', $args, $direction, $this );\n\n\t\t\t$sections = get_posts( $args );\n\n\t\t\tif ( ! empty( $sections ) ) {\n\t\t\t\t$sibling_section = llms_get_post( $sections[0]->ID );\n\t\t\t\t$lessons         = $sibling_section ? $sibling_section->get_lessons( 'posts' ) : array( false );\n\t\t\t\t$sibling_lesson  = 'next' === $direction ? reset( $lessons ) : end( $lessons );\n\t\t\t}\n\t\t}\n\n\t\treturn $sibling_lesson instanceof WP_Post ? $sibling_lesson->ID : $sibling_lesson;\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.membership.php",
    "content": "<?php\n/**\n * LifterLMS Membership Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Membership model class\n *\n * @since 3.0.0\n * @since 3.30.0 Added optional argument to `add_auto_enroll_courses()` method.\n * @since 3.32.0 Added `get_student_count()` method.\n * @since 3.36.3 Added `get_categories()`, `get_tags()` and `toArrayAfter()` methods.\n * @since 3.38.1 Added methods for retrieving posts associated with the membership.\n * @since 4.0.0 Added MySQL 8.0 compatibility.\n * @since 5.2.1 Check for an empty sales page URL or ID.\n * @since 5.3.0 Move sales page methods to `LLMS_Trait_Sales_Page`.\n *\n * @property string $audio_embed                URL to an oEmbed enable audio URL.\n * @property int[]  $auto_enroll                Array of course IDs that users will be autoenrolled in upon successful enrollment in this membership.\n * @property array  $instructors                Course instructor user information.\n * @property string $restriction_redirect_type  What type of redirect action to take when content is restricted by this membership [none|membership|page|custom].\n * @property int    $redirect_page_id           WP Post ID of a page to redirect users to when $restriction_redirect_type is 'page'.\n * @property string $redirect_custom_url        Arbitrary URL to redirect users to when $restriction_redirect_type is 'custom'.\n * @property string $restriction_add_notice     Whether or not to add an on screen message when content is restricted by this membership [yes|no].\n * @property string $restriction_notice         Notice to display when $restriction_add_notice is 'yes'.\n * @property string $featured_pricing           User defined additional pricing information.\n * @property int    $sales_page_content_page_id WP Post ID of the WP page to redirect to when $sales_page_content_type is 'page'.\n * @property string $sales_page_content_type    Sales page behavior [none,content,page,url].\n * @property string $sales_page_content_url     Redirect URL for a sales page, when $sales_page_content_type is 'url'.\n * @property string $video_embed                URL to an oEmbed enable video URL.\n */\nclass LLMS_Membership extends LLMS_Post_Model implements LLMS_Interface_Post_Instructors {\n\n\tuse LLMS_Trait_Audio_Video_Embed;\n\tuse LLMS_Trait_Sales_Page;\n\n\t/**\n\t * Membership post meta.\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\t\t'auto_enroll'               => 'array',\n\t\t'instructors'               => 'array',\n\t\t'redirect_page_id'          => 'absint',\n\t\t'restriction_add_notice'    => 'yesno',\n\t\t'restriction_notice'        => 'html',\n\t\t'restriction_redirect_type' => 'text',\n\t\t'redirect_custom_url'       => 'text',\n\t\t'featured_pricing'          => 'html',\n\t\t'tile_featured_video'       => 'yesno',\n\t);\n\n\t/**\n\t * Database post type.\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_membership';\n\n\t/**\n\t * Model name.\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'membership';\n\n\t/**\n\t * Constructor for this class and the traits it uses.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param string|int|LLMS_Post_Model|WP_Post $model 'new', WP post id, instance of an extending class, instance of WP_Post.\n\t * @param array                              $args  Args to create the post, only applies when $model is 'new'.\n\t */\n\tpublic function __construct( $model, $args = array() ) {\n\n\t\t$this->construct_audio_video_embed();\n\t\t$this->construct_sales_page();\n\t\tparent::__construct( $model, $args );\n\t}\n\n\t/**\n\t * Add courses to autoenrollment by id\n\t *\n\t * @since 3.0.0\n\t * @since 3.30.0 Added optional `$replace` argument.\n\t *\n\t * @param array|int $course_ids Array of course id or course id as int.\n\t * @param bool      $replace    Optional. When `true`, replaces all existing courses with `$course_ids`, when false merges `$course_ids` with existing courses. Default `false`.\n\t * @return boolean Returns `true` on success, and `false` on error or if the value in the db is unchanged.\n\t */\n\tpublic function add_auto_enroll_courses( $course_ids, $replace = false ) {\n\n\t\t// allow a single course_id to be passed in.\n\t\tif ( ! is_array( $course_ids ) ) {\n\t\t\t$course_ids = array( $course_ids );\n\t\t}\n\n\t\t// add existing courses to the array if replace is false.\n\t\tif ( ! $replace ) {\n\t\t\t$course_ids = array_merge( $course_ids, $this->get_auto_enroll_courses() );\n\t\t}\n\n\t\treturn $this->set( 'auto_enroll', array_unique( $course_ids ) );\n\t}\n\n\t/**\n\t * Retrieve a list of posts associated with the membership\n\t *\n\t * An associated post is:\n\t * + A post, page, or custom post type which supports `llms-membership-restrictions` and has restrictions enabled to this membership\n\t * + A course that exists in the memberships list of auto-enroll courses\n\t * + A course that has at least one access plan with members-only availability linked to this membership\n\t *\n\t * @since 3.38.1\n\t * @since 4.15.0 Minor restructuring to only query post type data when it's needed.\n\t *\n\t * @param string $post_type If supplied, returns only associations of this post type, otherwise returns an associative array of all associations.\n\t * @return array[]|int[] An array of arrays of post IDs. The array keys are the post type and the array values are arrays of integers.\n\t *                       If `$post_type` is supplied returns an array of associated post ids as integers.\n\t */\n\tpublic function get_associated_posts( $post_type = null ) {\n\n\t\t// If we're querying only posts, we can skip these associations entirely because courses don't support them.\n\t\t$post_types = 'course' !== $post_type ? get_post_types_by_support( 'llms-membership-restrictions' ) : array();\n\n\t\t// If we're looking at a single post type we only have to query associations for that post type.\n\t\t$post_types = $post_type ? array_intersect( $post_types, array( $post_type ) ) : $post_types;\n\n\t\t// Our return array.\n\t\t$posts = array();\n\n\t\t// Retrieve all posts that are restricted to a membership via a LifterLMS Membership Restriction setting.\n\t\tforeach ( $post_types as $type ) {\n\t\t\t$posts[ $type ] = $this->query_associated_posts( $type, '_llms_is_restricted', 'yes', '_llms_restricted_levels' );\n\t\t}\n\n\t\t// Include courses if courses were requested or if no specific post type was requested.\n\t\tif ( ! $post_type || 'course' === $post_type ) {\n\t\t\t$posts['course'] = $this->query_associated_courses();\n\t\t}\n\n\t\t/**\n\t\t * Filter the list of posts associated with the membership.\n\t\t *\n\t\t * @since 3.38.1\n\t\t *\n\t\t * @param array[]         $posts     An array of arrays of post IDs. The array keys are the post type and the array values are arrays of integers.\n\t\t * @param string|null     $post_type The requested post type if only a specific post type was requested, otherwise `null` to indicate all associated post types.\n\t\t * @param LLMS_Membership $this      Membership object.\n\t\t */\n\t\t$posts = apply_filters( 'llms_membership_get_associated_posts', $posts, $post_type, $this );\n\n\t\t// If a single post type was requested, return only that.\n\t\tif ( $post_type ) {\n\t\t\t// Return the request post type array and fallback to an empty array if that post type doesn't exist.\n\t\t\treturn isset( $posts[ $post_type ] ) ? $posts[ $post_type ] : array();\n\t\t}\n\n\t\t// Remove empty arrays and return the rest.\n\t\treturn array_filter( $posts );\n\t}\n\n\t/**\n\t * Get an array of the auto enrollment course ids\n\t *\n\t * Uses a custom function due to the default \"get_array\" returning an array with an empty string\n\t *\n\t * @since 3.0.0\n\t * @since 4.15.0 Exclude unpublished courses from the return array.\n\t *\n\t * @return array\n\t */\n\tpublic function get_auto_enroll_courses() {\n\n\t\t// Ensure an array when metadata is not set.\n\t\t$courses = isset( $this->auto_enroll ) ? $this->get( 'auto_enroll' ) : array();\n\n\t\t// Exclude unpublished courses.\n\t\t$courses = array_values(\n\t\t\tarray_filter(\n\t\t\t\t$courses,\n\t\t\t\tfunction ( $id ) {\n\t\t\t\t\treturn 'publish' === get_post_status( $id );\n\t\t\t\t}\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Filters the list of the membership's auto enroll courses\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param int[]           $courses    List of LLMS_Course IDs.\n\t\t * @param LLMS_Membership $membership Membership post object.\n\t\t */\n\t\treturn apply_filters( 'llms_membership_get_auto_enroll_courses', $courses, $this );\n\t}\n\n\t/**\n\t * Retrieve membership categories.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @param array $args Array of args passed to `wp_get_post_terms()`.\n\t * @return array\n\t */\n\tpublic function get_categories( $args = array() ) {\n\t\treturn wp_get_post_terms( $this->get( 'id' ), 'membership_cat', $args );\n\t}\n\n\t/**\n\t * Retrieve course instructor information\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param boolean $exclude_hidden If true, excludes hidden instructors from the return array.\n\t * @return array\n\t */\n\tpublic function get_instructors( $exclude_hidden = false ) {\n\n\t\treturn apply_filters(\n\t\t\t'llms_membership_get_instructors',\n\t\t\t$this->instructors()->get_instructors( $exclude_hidden ),\n\t\t\t$this,\n\t\t\t$exclude_hidden\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an instance of the LLMS_Product for this course\n\t *\n\t * @since 3.3.0\n\t * @return LLMS_Product\n\t */\n\tpublic function get_product() {\n\t\treturn new LLMS_Product( $this->get( 'id' ) );\n\t}\n\n\t/**\n\t * Retrieve the number of enrolled students in the membership.\n\t *\n\t * @since 3.32.0\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @return int\n\t */\n\tpublic function get_student_count() {\n\n\t\t$query = new LLMS_Student_Query(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $this->get( 'id' ),\n\t\t\t\t'statuses'   => array( 'enrolled' ),\n\t\t\t\t'count_only' => true,\n\t\t\t\t'sort'       => array( 'id' => 'ASC' ),\n\t\t\t)\n\t\t);\n\n\t\treturn $query->get_count_only_result();\n\t}\n\n\t/**\n\t * Get an array of student IDs based on enrollment status in the membership\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string|string[] $statuses Optional. List of enrollment statuses to query by status query is an OR relationship. Default is 'enrolled'.\n\t * @param int             $limit    Optional. Number of results. Default is `50`.\n\t * @param int             $skip     Optional. Number of results to skip (for pagination). Default is `0`.\n\t * @return array\n\t */\n\tpublic function get_students( $statuses = 'enrolled', $limit = 50, $skip = 0 ) {\n\t\treturn llms_get_enrolled_students( $this->get( 'id' ), $statuses, $limit, $skip );\n\t}\n\n\t/**\n\t * Retrieve membership tags.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @param array $args Array of args passed to `wp_get_post_terms()`.\n\t * @return array\n\t */\n\tpublic function get_tags( $args = array() ) {\n\t\treturn wp_get_post_terms( $this->get( 'id' ), 'membership_tag', $args );\n\t}\n\n\t/**\n\t * Retrieve an instance of the Post Instructors model\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return LLMS_Post_Instructors\n\t */\n\tpublic function instructors() {\n\t\treturn new LLMS_Post_Instructors( $this );\n\t}\n\n\t/**\n\t * Retrieve courses associated with the membership\n\t *\n\t * @since 3.38.1\n\t * @since 4.15.0 Exclude unpublished courses.\n\t *\n\t * @see LLMS_Membership::get_associated_posts()\n\t *\n\t * @return int[]\n\t */\n\tprotected function query_associated_courses() {\n\n\t\t// Start with autoenroll courses.\n\t\t$courses = $this->get_auto_enroll_courses();\n\n\t\t// Retrieve all access plans with a members-only availability restriction for this membership.\n\t\tforeach ( $this->query_associated_posts( 'llms_access_plan', '_llms_availability', 'members', '_llms_availability_restrictions' ) as $plan_id ) {\n\t\t\t$plan = llms_get_post( $plan_id );\n\t\t\tif ( $plan ) {\n\t\t\t\t$id = $plan->get( 'product_id' );\n\t\t\t\tif ( 'publish' === get_post_status( $id ) ) {\n\t\t\t\t\t$courses[] = $id;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn array_unique( $courses );\n\t}\n\n\t/**\n\t * Performs a WPDB query to retrieve posts associated with the membership\n\t *\n\t * @since 3.38.1\n\t * @since 4.0.0 Escape `{` character in SQL query to add MySQL 8.0 support.\n\t *\n\t * @see LLMS_Membesrhip::get_associated_posts()\n\t *\n\t * @param string $post_type     Post type to query for an association with.\n\t * @param string $enabled_key   A meta key name, used to check if the association is enabled for the associated post. For example: \"_llms_is_restricted\"\n\t * @param string $enabled_value The meta value of the `$enabled_key` when the association is enabled. For example \"yes\" when checking \"_llms_is_restricted\"..\n\t * @param string $list_key      The meta key name where associations are stored as a serialized array of WP_Post IDs. For example \"_llms_restricted_levels\".\n\t * @return int[]\n\t */\n\tprotected function query_associated_posts( $post_type, $enabled_key, $enabled_value, $list_key ) {\n\n\t\tglobal $wpdb;\n\n\t\t// See if we have a cached result first.\n\t\t$cache = sprintf( 'membership_%1$d_associated_%2$s', $this->get( 'id' ), $post_type );\n\t\t$found = null;\n\t\t$ids   = wp_cache_get( $cache, '', false, $found );\n\n\t\t// We don't, perform a query.\n\t\tif ( ! $found ) {\n\n\t\t\t$ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"SELECT metas.post_id\n\t\t\t\t FROM {$wpdb->postmeta} AS metas\n\t\t\t\t JOIN {$wpdb->postmeta} AS metas2 ON metas2.post_id = metas.post_id\n\t\t\t\t JOIN {$wpdb->posts} AS posts ON posts.ID = metas.post_id\n\t\t\t\t WHERE 1\n\t\t\t\t   AND posts.post_status = 'publish'\n\t\t\t\t   AND posts.post_type = %s\n\t\t\t\t   AND metas2.meta_key = %s\n\t\t\t\t   AND metas2.meta_value = %s\n\t\t\t\t   AND metas.meta_key = %s\n\t\t\t\t   AND metas.meta_value REGEXP %s;\",\n\t\t\t\t\t$post_type,\n\t\t\t\t\t$enabled_key,\n\t\t\t\t\t$enabled_value,\n\t\t\t\t\t$list_key,\n\t\t\t\t\t'a:[0-9][0-9]*:\\{(i:[0-9][0-9]*;(i|s:[0-9][0-9]*):\"?[0-9][0-9]*\"?;)*(i:[0-9][0-9]*;(i|s:[0-9][0-9]*):\"?' . $this->get( 'id' ) . '\"?;)'\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Only return ints.\n\t\t\t$ids = array_map( 'absint', $ids );\n\n\t\t\t// Cache the result.\n\t\t\twp_cache_set( $cache, $ids );\n\n\t\t}\n\n\t\treturn $ids;\n\t}\n\n\t/**\n\t * Remove a course from auto enrollment\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param int $course_id WP_Post ID of the course.\n\t * @return bool\n\t */\n\tpublic function remove_auto_enroll_course( $course_id ) {\n\t\treturn $this->set( 'auto_enroll', array_diff( $this->get_auto_enroll_courses(), array( $course_id ) ) );\n\t}\n\n\t/**\n\t * Save instructor information\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param array $instructors Array of course instructor information.\n\t * @return array\n\t */\n\tpublic function set_instructors( $instructors = array() ) {\n\n\t\treturn $this->instructors()->set_instructors( $instructors );\n\t}\n\n\t/**\n\t * Add data to the membership model when converted to array.\n\t *\n\t * Called before data is sorted and returned by `$this->jsonSerialize()`.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @param array $arr Data to be serialized.\n\t * @return array\n\t */\n\tpublic function toArrayAfter( $arr ) {\n\t\t$arr['categories'] = $this->get_categories(\n\t\t\tarray(\n\t\t\t\t'fields' => 'names',\n\t\t\t)\n\t\t);\n\n\t\t$arr['tags'] = $this->get_tags(\n\t\t\tarray(\n\t\t\t\t'fields' => 'names',\n\t\t\t)\n\t\t);\n\n\t\treturn $arr;\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.notification.php",
    "content": "<?php\n/**\n * LLMS_Notification class file\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.8.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Notification model class.\n *\n * Used for notification CRUD and Display.\n *\n * @since 3.8.0\n */\nclass LLMS_Notification implements JsonSerializable {\n\n\t/**\n\t * Notification ID\n\t *\n\t * @var  int\n\t */\n\tpublic $id;\n\n\t/**********************************************************\n\t *\n\t * Default Properties\n\t **********************************************************/\n\n\t/**\n\t * Created Date\n\t *\n\t * @var  string (DATETIME)\n\t */\n\tprivate $created;\n\n\t/**\n\t * Updated Date\n\t *\n\t * @var  string (DATETIME)\n\t */\n\tprivate $updated;\n\n\t/**\n\t * Current Status\n\t * Options vary based on notification type\n\t *\n\t * @var  string\n\t */\n\tprivate $status;\n\n\t/**\n\t * Type of Notification\n\t * basic, email, sms, etc...\n\t *\n\t * @var  string\n\t */\n\tprivate $type;\n\n\t/**\n\t * Subscriber Identifier\n\t * WP User ID, email address (for cc,bcc), phone number, etc...\n\t *\n\t * @var  mixed\n\t */\n\tprivate $subscriber;\n\n\t/**\n\t * Trigger ID for the notification\n\t * lesson_complete, course_complete, etc...\n\t *\n\t * @var  string\n\t */\n\tprivate $trigger_id;\n\n\t/**\n\t * WP User ID of the user who triggered the notification to be generated\n\t * NOT to be confused with $subscriber and can be different than the subscriber\n\t *\n\t * @var  int\n\t */\n\tprivate $user_id;\n\n\t/**\n\t * WP Post ID of the post which triggered the notification to be generated\n\t *\n\t * @var  int\n\t */\n\tprivate $post_id;\n\n\t/**********************************************************\n\t *\n\t * View Related Properties\n\t **********************************************************/\n\t/**\n\t * Merged HTML for the notification\n\t * used for displaying a notification view\n\t *\n\t * @var string\n\t */\n\tprivate $html;\n\n\t/**\n\t * Constructor\n\t *\n\t * @param    int $notification  Notification ID\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function __construct( $notification = null ) {\n\n\t\tif ( is_numeric( $notification ) ) {\n\t\t\t$this->id = $notification;\n\t\t}\n\n\t}\n\n\t/**\n\t * Get notification properties\n\t *\n\t * @param    string $key  key to retrieve\n\t * @return   mixed\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function __get( $key ) {\n\t\treturn $this->get( $key, false );\n\t}\n\n\t/**\n\t * Create a new notification in the database\n\t *\n\t * @param    array $data  notification data\n\t * @return   int|false         new notification id on success, false otherwise\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function create( $data = array() ) {\n\n\t\t$time = current_time( 'mysql' );\n\n\t\t$data = wp_parse_args(\n\t\t\t$data,\n\t\t\tarray(\n\n\t\t\t\t'created'    => $time,\n\t\t\t\t'post_id'    => null,\n\t\t\t\t'status'     => 'new',\n\t\t\t\t'subscriber' => null,\n\t\t\t\t'trigger_id' => null,\n\t\t\t\t'type'       => '',\n\t\t\t\t'updated'    => $time,\n\t\t\t\t'user_id'    => null,\n\n\t\t\t)\n\t\t);\n\n\t\tksort( $data ); // Maintain alpha sort you savages.\n\n\t\t$format = array(\n\t\t\t'%s', // For created.\n\t\t\t'%d', // For post_id.\n\t\t\t'%s', // For status.\n\t\t\t'%s', // For subscriber.\n\t\t\t'%s', // For trigger_id.\n\t\t\t'%s', // For type.\n\t\t\t'%s', // For updated.\n\t\t\t'%d', // For user_id.\n\t\t);\n\n\t\tglobal $wpdb;\n\n\t\tif ( 1 !== $wpdb->insert( $this->get_table(), $data, $format ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$this->id = $wpdb->insert_id;\n\n\t\treturn $this->id;\n\n\t}\n\n\t/**\n\t * Determine if the triggering user is the subscriber\n\t *\n\t * @return   boolean\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function is_subscriber_self() {\n\t\treturn ( $this->get( 'subscriber' ) == $this->get( 'user_id' ) );\n\t}\n\n\t/**\n\t * Get notification properties\n\t *\n\t * @param    string $key  key to retrieve\n\t * @return   mixed\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get( $key, $skip_cache = false ) {\n\n\t\t// Id will always be accessed from the object.\n\t\tif ( 'id' === $key ) {\n\t\t\treturn $this->id;\n\t\t}\n\n\t\t// Return cached values if they exist.\n\t\tif ( ! is_null( $this->$key ) && ! $skip_cache ) {\n\t\t\treturn $this->$key;\n\t\t}\n\n\t\t// get the value from the database.\n\t\tglobal $wpdb;\n\t\t// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\treturn $wpdb->get_var( $wpdb->prepare( \"SELECT {$key} FROM {$this->get_table()} WHERE id = %d\", $this->id ) );  // db call ok; no-cache ok.\n\n\t}\n\n\t/**\n\t * Retrieve the HTML for the current notification\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_html() {\n\t\t$view = $this->get_view();\n\t\tif ( $view ) {\n\t\t\treturn $view->get_html();\n\t\t}\n\t\treturn '';\n\t}\n\n\t/**\n\t * Get the table name for notification data\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprivate function get_table() {\n\t\tglobal $wpdb;\n\t\treturn $wpdb->prefix . 'lifterlms_notifications';\n\t}\n\n\t/**\n\t * Retrieve an instance of the notification view class for the notification\n\t *\n\t * @return   LLMS_Abstract_Notification_View|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_view() {\n\t\treturn llms()->notifications()->get_view( $this );\n\t}\n\n\t/**\n\t * Called when converting a notification to JSON\n\t *\n\t * @since 3.8.0\n\t *\n\t * @todo The `mixed` return type declared by the parent method, which should be defined here as well,\n\t *       is not available until PHP 8.0. Once support is dropped for 7.4 we can add the return type declaration\n\t *       and remove the `#[ReturnTypeWillChange]` attribute. This *must* happen before the release of PHP 9.0.\n\t *\n\t * @return array\n\t */\n\t#[ReturnTypeWillChange]\n\tpublic function jsonSerialize() {\n\t\treturn $this->toArray();\n\t}\n\n\t/**\n\t * Load all notification data into the instance.\n\t *\n\t * @since 3.8.0\n\t * @since 7.1.0 Catch possible fatals while generating the notification HTML and log them.\n\t *\n\t * @return LLMS_Notification\n\t */\n\tpublic function load() {\n\n\t\tglobal $wpdb;\n\n\t\t$notification = $wpdb->get_row(\n\t\t\t$wpdb->prepare( \"SELECT created, updated, status, type, subscriber, trigger_id, user_id, post_id FROM {$this->get_table()} WHERE id = %d\", $this->id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\tARRAY_A\n\t\t); // db call ok; no-cache ok.\n\n\t\tif ( $notification ) {\n\n\t\t\tforeach ( $notification as $key => $val ) {\n\t\t\t\t$this->$key = $val;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t$this->html = $this->get_html();\n\t\t\t} catch ( Error $e ) {\n\t\t\t\tllms_log( sprintf( 'Error generating the HTML for the notification ID #%d', $this->id ) );\n\t\t\t\tllms_log( sprintf( 'Error caught %1$s in %2$s on line %3$s', $e->getMessage(), $e->getFile(), $e->getLine() ) );\n\t\t\t\t$this->set( 'status', 'error' );\n\t\t\t}\n\t\t}\n\n\t\treturn $this;\n\n\t}\n\n\t/**\n\t * Set object variables\n\t *\n\t * @since    3.8.0\n\t *\n\t * @param    string $key  variable name\n\t * @param    mixed  $val  data\n\t */\n\tpublic function set( $key, $val ) {\n\n\t\tglobal $wpdb;\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'created':\n\t\t\tcase 'id':\n\t\t\tcase 'updated':\n\t\t\t\treturn false;\n\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$this->$key = $val;\n\t\t\t\tif ( $this->id ) {\n\t\t\t\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\t\t\treturn $wpdb->query(\n\t\t\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\t\t\"UPDATE {$this->get_table()} SET {$key} = %s, updated = %s WHERE id = %d\",\n\t\t\t\t\t\t\t$val,\n\t\t\t\t\t\t\tcurrent_time( 'mysql' ),\n\t\t\t\t\t\t\t$this->id\n\t\t\t\t\t\t)\n\t\t\t\t\t); // db call ok; no-cache ok.\n\t\t\t\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Convert the notification to an array\n\t * access to all properties and meta items will be made accessible\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function toArray() {\n\t\treturn get_object_vars( $this->load() );\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.order.php",
    "content": "<?php\n/**\n * LLMS_Order class/model file\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.0.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS order model.\n *\n * Provides CRUD operations for the `llms_order` post type.\n *\n * @property string $access_expiration       Access expiration type, accepts: lifetime (default), limited-period, or limited-date.\n * @property string $access_expires          Date on which access expires in `m/d/Y` format. Only applicable when the `$access_expiration` property is set to \"limited-date\".\n * @property int    $access_length           Length of access from time of purchase, combine with the `$access_period`. Only applicable when the `$access_expiration` property is set to \"limited-period\".\n * @property string $access_period           Time period of access from time of purchase, combine with `$access_length`. Only applicable when the `$access_expiration` property is set to \"limited-period\". Accepts: year, month, week, or day.\n * @property string $anonymized              Determines if the order has been anonymized due to a personal information erasure request. Accepts \"yes\" or \"no\".\n * @property string $billing_address_1       Customer billing address line 1.\n * @property string $billing_address_2       Customer billing address line 2.\n * @property string $billing_city            Customer billing city.\n * @property string $billing_country         Customer billing country, two character ISO code.\n * @property string $billing_email           Customer email address.\n * @property string $billing_first_name      Customer first name.\n * @property string $billing_last_name       Customer last name.\n * @property string $billing_phone           Customer phone number.\n * @property string $billing_state           Customer billing state.\n * @property string $billing_zip             Customer billing zip/postal code.\n * @property int    $billing_frequency       The billing frequency interval. A value of `0` indicates a one-time payment. Accepts integers <= 6.\n * @property int    $billing_length          Number of intervals to run payment for, combine with `$billing_period` & `$billing_frequency`. A value of `0` indicates that recurring payments run indefinitely (until cancelled). Only applicable if `$billing_frequency` is not 0.\n * @property string $billing_period          The billing period. Combine with `$length`. Only applicable if `$billing_frequency` is not 0. Accepts: year, month, week, or day.\n * @property float  $coupon_amount           Amount of the coupon (flat/percentage) in relation to the plan amount.\n * @property float  $coupon_amout_trial      Amount of the coupon (flat/percentage) in relation to the plan trial amount where applicable.\n * @property string $coupon_code             Coupon code applied to the order.\n * @property int    $coupon_id               The WP_Post ID of the used coupon.\n * @property string $coupon_type             Type of coupon used, either percent or dollar.\n * @property string $coupon_used             Whether or not a coupon was used for the order. Accepts yes or no.\n * @property float  $coupon_value            Value of the coupon. When on sale, `$sale_price` minus `$total`; when not on sale `$original_total` minus `$total`.\n * @property float  $coupon_value_trial      Value of the coupon applied to the trial. The `$trial_original_total` minus `$trial_total`.\n * @property string $currency                Transaction's currency code.\n * @property string $date_access_expires     Date when access should expire as a datetime string: `Y-m-d H:i:s`.\n * @property string $date_next_payment       Date when the next recurring payment is due as a datemtime string: `Y-m-d H:i:s`. Use function LLMS_Order::get_next_payment_due_date() instead of accessing directly!\n * @property string $date_trial_end          Date when the trial ends for orders with a trial as a datemtime string: `Y-m-d H:i:s`. Use function LLMS_Order::get_trial_end_date() instead of accessing directly!\n * @property string $gateway_api_mode        API Mode of the gateway when the transaction was made, either \"test\" or \"live\".\n * @property string $gateway_customer_id     Gateway's unique ID for the customer who placed the order (if supported by the gateway).\n * @property string $gateway_source_id       Gateway's unique ID for the card or source to be used for recurring subscriptions (if supported by gateway).\n * @property string $gateway_subscription_id Gateway's unique ID for the recurring subscription (if supported by the gateway).\n * @property int    $id                      The WP_Post ID of the order.\n * @property int    $last_retry_rule         Rule number for current retry step for the order.\n * @property string $on_sale                 Whether or not sale pricing was used for the plan, either \"yes\" or \"no\".\n * @property string $order_key               A unique identifier for the order that can be passed safely in URLs.\n * @property string $order_type              Single or recurring order, either \"single\" or \"recurring\".\n * @property float  $original_total          Price of the order before applicable sale and coupon adjustments.\n * @property string $payment_gateway         LifterLMS Payment Gateway ID (eg \"paypal\" or \"stripe\").\n * @property int    $plan_id                 WP_Post ID of the purchased access plan.\n * @property string $plan_sku                SKU of the purchased access plan.\n * @property string $plan_title              Title / Name of the purchased access plan.\n * @property string $plan_ended              Whether or not the payment plan has ended. Only applicable when the plan is not \"unlimited\". Accepts \"yes\" or \"no\".\n * @property int    $product_id              WP_Post ID of the purchased course or membership product.\n * @property string $product_sku             SKU of the purchased product.\n * @property string $product_title           Title / Name of the purchased product.\n * @property string $product_type            Type of product purchased (course or membership).\n * @property float  $sale_price              Sale price before coupon adjustments.\n * @property float  $sale_value              The value of the sale, `$original_total` - `$sale_price`.\n * @property string $start_date              Date when access was initially granted; this is used to determine when access expires.\n * @property array  $temp_gateway_ids        {\n *     An associative array containing gateway ids. The gateway IDs are cached in this meta property while the source is being\n *     switched. Any gateway running actions when a source is switched may need to know the previous source IDs which might be\n *     cleared or overwritten by other gateways during the switch.\n *\n *     @type string customer     The value of the `gateway_customer_id` property when the source switch starts.\n *     @type string source       The value of the `gateway_source_id` property when the source switch starts.\n *     @type string subscription The value of the `gateway_subscription_id` property when the source switch starts.\n * }\n * @property float  $total                   Actual price of the order, after applicable sale & coupon adjustments.\n * @property int    $trial_length            Length of the trial. Combined with $trial_period to determine the actual length of the trial.\n * @property string $trial_offer             Whether or not there was a trial offer applied to the order, either yes or no.\n * @property float  $trial_original_total    Total price of the trial before applicable coupon adjustments.\n * @property string $trial_period            Period for the trial period. Accepts: year, month, week, or day.\n * @property float  $trial_total             Total price of the trial after applicable coupon adjustments/\n * @property int    $user_id                 Customer WP_User ID.\n * @property string $user_ip_address         Customer's IP address at time of purchase.\n *\n * @since 3.0.0\n * @since 3.32.0 Update to use latest action-scheduler functions.\n * @since 3.35.0 Prepare transaction revenue SQL query properly; Sanitize $_SERVER data.\n * @since 4.7.0 Added `plan_ended` meta property.\n * @since 5.3.0 Removed usage of the meta property `date_billing_end` and removed private method `calculate_billing_end_date()`.\n */\nclass LLMS_Order extends LLMS_Post_Model {\n\n\t/**\n\t * Database post type.\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_order';\n\n\t/**\n\t * Model post type.\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'order';\n\n\t/**\n\t * Meta properties.\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\n\t\t'anonymized'           => 'yesno',\n\t\t'coupon_amount'        => 'float',\n\t\t'coupon_amout_trial'   => 'float',\n\t\t'coupon_value'         => 'float',\n\t\t'coupon_value_trial'   => 'float',\n\t\t'original_total'       => 'float',\n\t\t'sale_price'           => 'float',\n\t\t'sale_value'           => 'float',\n\t\t'total'                => 'float',\n\t\t'trial_original_total' => 'float',\n\t\t'trial_total'          => 'float',\n\n\t\t'access_length'        => 'absint',\n\t\t'billing_frequency'    => 'absint',\n\t\t'billing_length'       => 'absint',\n\t\t'coupon_id'            => 'absint',\n\t\t'plan_id'              => 'absint',\n\t\t'product_id'           => 'absint',\n\t\t'trial_length'         => 'absint',\n\t\t'user_id'              => 'absint',\n\n\t\t'access_expiration'    => 'text',\n\t\t'access_expires'       => 'text',\n\t\t'access_period'        => 'text',\n\t\t'billing_address_1'    => 'text',\n\t\t'billing_address_2'    => 'text',\n\t\t'billing_city'         => 'text',\n\t\t'billing_country'      => 'text',\n\t\t'billing_email'        => 'text',\n\t\t'billing_first_name'   => 'text',\n\t\t'billing_last_name'    => 'text',\n\t\t'billing_state'        => 'text',\n\t\t'billing_zip'          => 'text',\n\t\t'billing_period'       => 'text',\n\t\t'coupon_code'          => 'text',\n\t\t'coupon_type'          => 'text',\n\t\t'coupon_used'          => 'text',\n\t\t'currency'             => 'text',\n\t\t'on_sale'              => 'text',\n\t\t'order_key'            => 'text',\n\t\t'order_type'           => 'text',\n\t\t'payment_gateway'      => 'text',\n\t\t'plan_ended'           => 'yesno',\n\t\t'plan_sku'             => 'text',\n\t\t'plan_title'           => 'text',\n\t\t'product_sku'          => 'text',\n\t\t'product_type'         => 'text',\n\t\t'title'                => 'text',\n\t\t'gateway_api_mode'     => 'text',\n\t\t'gateway_customer_id'  => 'text',\n\t\t'trial_offer'          => 'text',\n\t\t'trial_period'         => 'text',\n\t\t'user_ip_address'      => 'text',\n\n\t\t'date_access_expires'  => 'text',\n\t\t'date_next_payment'    => 'text',\n\t\t'date_trial_end'       => 'text',\n\n\t\t'temp_gateway_ids'     => 'array',\n\n\t);\n\n\t/**\n\t * Add an admin-only note to the order visible on the admin panel\n\t * notes are recorded using the wp comments API & DB\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Sanitize $_SERVER data.\n\t *\n\t * @param string  $note          Note content.\n\t * @param boolean $added_by_user Optional. If this is an admin-submitted note adds user info to note meta. Default is false.\n\t * @return null|int Null on error or WP_Comment ID of the note.\n\t */\n\tpublic function add_note( $note, $added_by_user = false ) {\n\n\t\tif ( ! $note ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Added by a user from the admin panel.\n\t\tif ( $added_by_user && is_user_logged_in() && current_user_can( apply_filters( 'lifterlms_admin_order_access', 'manage_options' ) ) ) {\n\n\t\t\t$user_id      = get_current_user_id();\n\t\t\t$user         = get_user_by( 'id', $user_id );\n\t\t\t$author       = $user->display_name;\n\t\t\t$author_email = $user->user_email;\n\n\t\t} else {\n\n\t\t\t$user_id       = 0;\n\t\t\t$author        = _x( 'LifterLMS', 'default order note author', 'lifterlms' );\n\t\t\t$author_email  = strtolower( _x( 'LifterLms', 'default order note author', 'lifterlms' ) ) . '@';\n\t\t\t$author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : 'noreply.com';\n\t\t\t$author_email  = sanitize_email( $author_email );\n\n\t\t}\n\n\t\t$note_id = wp_insert_comment(\n\t\t\tapply_filters(\n\t\t\t\t'llms_add_order_note_content',\n\t\t\t\tarray(\n\t\t\t\t\t'comment_post_ID'      => $this->get( 'id' ),\n\t\t\t\t\t'comment_author'       => $author,\n\t\t\t\t\t'comment_author_email' => $author_email,\n\t\t\t\t\t'comment_author_url'   => '',\n\t\t\t\t\t'comment_content'      => $note,\n\t\t\t\t\t'comment_type'         => 'llms_order_note',\n\t\t\t\t\t'comment_parent'       => 0,\n\t\t\t\t\t'user_id'              => $user_id,\n\t\t\t\t\t'comment_approved'     => 1,\n\t\t\t\t\t'comment_agent'        => 'LifterLMS',\n\t\t\t\t\t'comment_date'         => current_time( 'mysql' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\tdo_action( 'llms_new_order_note_added', $note_id, $this );\n\n\t\treturn $note_id;\n\t}\n\n\t/**\n\t * Called after inserting a new order into the database\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return void\n\t */\n\tprotected function after_create() {\n\t\t// Add a random key that can be passed in the URL and whatever.\n\t\t$this->set( 'order_key', $this->generate_order_key() );\n\t}\n\n\t/**\n\t * Calculate the next payment due date\n\t *\n\t * @since 3.10.0\n\t * @since 3.12.0 Unknown.\n\t * @since 3.37.6 Now uses the last successful transaction time to calculate from when the previously\n\t *               stored next payment date is in the future.\n\t * @since 4.9.0 Fix comparison for PHP8 compat.\n\t * @since 5.3.0 Determine if a limited order has ended based on number of remaining payments in favor of current date/time.\n\t *\n\t * @param string $format PHP date format used to format the returned date string.\n\t * @return string The formatted next payment due date or an empty string when there is no next payment.\n\t */\n\tprivate function calculate_next_payment_date( $format = 'Y-m-d H:i:s' ) {\n\n\t\t// If the limited plan has already ended return early.\n\t\t$remaining = $this->get_remaining_payments();\n\t\tif ( 0 === $remaining ) {\n\t\t\t// This filter is documented below.\n\t\t\treturn apply_filters( 'llms_order_calculate_next_payment_date', '', $format, $this );\n\t\t}\n\n\t\t$start_time        = $this->get_date( 'date', 'U' );\n\t\t$next_payment_time = $this->get_date( 'date_next_payment', 'U' );\n\t\t$last_txn_time     = $this->get_last_transaction_date( 'llms-txn-succeeded', 'recurring', 'U' );\n\n\t\t// If were on a trial and the trial hasn't ended yet next payment date is the date the trial ends.\n\t\tif ( $this->has_trial() && ! $this->has_trial_ended() ) {\n\n\t\t\t$next_payment_time = $this->get_trial_end_date( 'U' );\n\n\t\t} else {\n\n\t\t\t/**\n\t\t\t * Calculate next payment date from the saved `date_next_payment` calculated during\n\t\t\t * the previous recurring transaction or during order initialization.\n\t\t\t *\n\t\t\t * This condition will be encountered during the 2nd, 3rd, 4th, etc... recurring payments.\n\t\t\t */\n\t\t\tif ( $next_payment_time && $next_payment_time < llms_current_time( 'timestamp' ) ) {\n\n\t\t\t\t$from_time = $next_payment_time;\n\n\t\t\t\t/**\n\t\t\t\t * Use the order's last successful transaction date.\n\t\t\t\t *\n\t\t\t\t * This will be encountered when any amount of \"chaos\" is\n\t\t\t\t * introduced causing the previously stored `date_next_payment`\n\t\t\t\t * to be GREATER than the current time.\n\t\t\t\t *\n\t\t\t\t * Orders created\n\t\t\t\t */\n\t\t\t} elseif ( $last_txn_time && $last_txn_time > $start_time ) {\n\n\t\t\t\t$from_time = $last_txn_time;\n\n\t\t\t\t/**\n\t\t\t\t * Use the order's creation time.\n\t\t\t\t *\n\t\t\t\t * This condition will be encountered for the 1st recurring payment only.\n\t\t\t\t */\n\t\t\t} else {\n\n\t\t\t\t$from_time = $start_time;\n\n\t\t\t}\n\n\t\t\t$period            = $this->get( 'billing_period' );\n\t\t\t$frequency         = $this->get( 'billing_frequency' );\n\t\t\t$next_payment_time = strtotime( '+' . $frequency . ' ' . $period, $from_time );\n\n\t\t\t/**\n\t\t\t * Make sure the next payment is more than 2 hours in the future\n\t\t\t *\n\t\t\t * This ensures changes to the site's timezone because of daylight savings\n\t\t\t * will never cause a 2nd renewal payment to be processed on the same day.\n\t\t\t */\n\t\t\t$i = 1;\n\t\t\twhile ( $next_payment_time < ( llms_current_time( 'timestamp', true ) + 2 * HOUR_IN_SECONDS ) && $i < 3000 ) {\n\t\t\t\t$next_payment_time = strtotime( '+' . $frequency . ' ' . $period, $next_payment_time );\n\t\t\t\t++$i;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter the calculated next payment date\n\t\t *\n\t\t * @since 3.10.0\n\t\t *\n\t\t * @param string     $ret    The formatted next payment due date or an empty string when there is no next payment.\n\t\t * @param string     $format The requested date format.\n\t\t * @param LLMS_Order $order  The order object.\n\t\t */\n\t\treturn apply_filters( 'llms_order_calculate_next_payment_date', date( $format, $next_payment_time ), $format, $this );\n\t}\n\n\t/**\n\t * Calculate the end date of the trial\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param string $format Optional. Desired return format of the date. Defalt is 'Y-m-d H:i:s'.\n\t * @return string\n\t */\n\tprivate function calculate_trial_end_date( $format = 'Y-m-d H:i:s' ) {\n\n\t\t$start = $this->get_date( 'date', 'U' ); // Start with the date the order was initially created.\n\n\t\t$length = $this->get( 'trial_length' );\n\t\t$period = $this->get( 'trial_period' );\n\n\t\t$end = strtotime( '+' . $length . ' ' . $period, $start );\n\n\t\t$ret = date_i18n( $format, $end );\n\n\t\treturn apply_filters( 'llms_order_calculate_trial_end_date', $ret, $format, $this );\n\t}\n\n\t/**\n\t * Determines if an order can be confirmed.\n\t *\n\t * An order can be confirmed only when the order's status is pending.\n\t *\n\t * Additional requirements can be introduced via the filter `llms_order_can_be_confirmed`.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function can_be_confirmed() {\n\n\t\t/**\n\t\t * Determine if the order can be confirmed.\n\t\t *\n\t\t * @since 3.34.4\n\t\t *\n\t\t * @param boolean    $can_be_confirmed Whether or not the order can be confirmed.\n\t\t * @param LLMS_Order $order            Order object.\n\t\t * @param string     $gateway_id       Payment gateway ID.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_order_can_be_confirmed',\n\t\t\t( 'llms-pending' === $this->get( 'status' ) ),\n\t\t\t$this,\n\t\t\t$this->get( 'payment_gateway' )\n\t\t);\n\t}\n\n\t/**\n\t * Determine if the order can be retried for recurring payments\n\t *\n\t * @since 3.10.0\n\t * @since 5.2.0 Use strict type comparison.\n\t * @since 5.2.1 Combine conditions that return `false`.\n\t *\n\t * @return boolean\n\t */\n\tpublic function can_be_retried() {\n\n\t\t$can_retry = true;\n\n\t\tif (\n\t\t\t// Only recurring orders can be retried.\n\t\t\t! $this->is_recurring() ||\n\t\t\t// Recurring rety feature is disabled.\n\t\t\t! llms_parse_bool( get_option( 'lifterlms_recurring_payment_retry', 'yes' ) ) ||\n\t\t\t// Only active & on-hold orders qualify for a retry.\n\t\t\t! in_array( $this->get( 'status' ), array( 'llms-active', 'llms-on-hold' ), true )\n\t\t) {\n\t\t\t$can_retry = false;\n\t\t} else {\n\n\t\t\t// If the gateway isn't active or the gateway doesn't support recurring retries.\n\t\t\t$gateway = $this->get_gateway();\n\t\t\tif ( is_wp_error( $gateway ) || ! $gateway->supports( 'recurring_retry' ) ) {\n\t\t\t\t$can_retry = false;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not a recurring order can be retried\n\t\t *\n\t\t * @since 5.2.1\n\t\t *\n\t\t * @param boolean    $can_retry Whether or not the order can be retried.\n\t\t * @param LLMS_Order $order     Order object.\n\t\t */\n\t\treturn apply_filters( 'llms_order_can_be_retried', $can_retry, $this );\n\t}\n\n\t/**\n\t * Determines if the order can be resubscribed to.\n\t *\n\t * @since 3.19.0\n\t * @since 5.2.0 Use strict type comparison.\n\t *\n\t * @return bool\n\t */\n\tpublic function can_resubscribe() {\n\n\t\t$can_resubscribe = false;\n\n\t\tif ( $this->is_recurring() ) {\n\n\t\t\t/**\n\t\t\t * Filters the order statuses from which an order can be reactivated.\n\t\t\t *\n\t\t\t * @since 7.0.0\n\t\t\t *\n\t\t\t * @param string[] $allowed_statuses The list of allowed order statuses.\n\t\t\t */\n\t\t\t$allowed_statuses = apply_filters(\n\t\t\t\t'llms_order_status_can_resubscribe_from',\n\t\t\t\tarray(\n\t\t\t\t\t'llms-on-hold',\n\t\t\t\t\t'llms-pending',\n\t\t\t\t\t'llms-pending-cancel',\n\t\t\t\t)\n\t\t\t);\n\t\t\t$can_resubscribe  = in_array( $this->get( 'status' ), $allowed_statuses, true );\n\n\t\t}\n\n\t\t/**\n\t\t * Determines whether or not a user can resubscribe to an inactive recurring payment order.\n\t\t *\n\t\t * @since 3.19.0\n\t\t *\n\t\t * @param boolean    $can_resubscribe Whether or not a user can resubscribe.\n\t\t * @param LLMS_Order $order           The order object.\n\t\t */\n\t\treturn apply_filters( 'llms_order_can_resubscribe', $can_resubscribe, $this );\n\t}\n\n\t/**\n\t * Determines if the order's payment source can be changed.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function can_switch_source() {\n\n\t\t$can_switch = 'llms-active' === $this->get( 'status' ) || $this->can_resubscribe();\n\n\t\t/**\n\t\t * Filters whether or not the order's payment source can be changed.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param boolean    $can_switch Whether or not the order's source can be switched.\n\t\t * @param LLMS_Order $order      The order object.\n\t\t */\n\t\treturn apply_filters( 'llms_order_can_switch_source', $can_switch, $this );\n\t}\n\n\t/**\n\t * Generate an order key for the order\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function generate_order_key() {\n\t\t/**\n\t\t * Modify the generated order key for the order.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 5.2.1 Added the `$order` parameter.\n\t\t *\n\t\t * @param string     $order_key The generated order key.\n\t\t * @param LLMS_Order $order_key Order object.\n\t\t */\n\t\treturn apply_filters( 'lifterlms_generate_order_key', uniqid( 'order-' ), $this );\n\t}\n\n\t/**\n\t * Determine the date when access will expire\n\t *\n\t * Based on the access settings of the access plan\n\t * at the `$start_date` of access.\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @param string $format Optional. Date format. Default is 'Y-m-d'.\n\t * @return string Date string.\n\t *                \"Lifetime Access\" for plans with lifetime access.\n\t *                \"To be Determined\" for limited date when access hasn't started yet.\n\t */\n\tpublic function get_access_expiration_date( $format = 'Y-m-d' ) {\n\n\t\t$type = $this->get( 'access_expiration' );\n\n\t\t$ret = $this->get_date( 'date_access_expires', $format );\n\t\tif ( ! $ret ) {\n\t\t\tswitch ( $type ) {\n\t\t\t\tcase 'lifetime':\n\t\t\t\t\t$ret = __( 'Lifetime Access', 'lifterlms' );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'limited-date':\n\t\t\t\t\t$ret = date_i18n( $format, ( $this->get_date( 'access_expires', 'U' ) + ( DAY_IN_SECONDS - 1 ) ) );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'limited-period':\n\t\t\t\t\tif ( $this->get( 'start_date' ) ) {\n\t\t\t\t\t\t$time = strtotime( '+' . $this->get( 'access_length' ) . ' ' . $this->get( 'access_period' ), $this->get_date( 'start_date', 'U' ) ) + ( DAY_IN_SECONDS - 1 );\n\t\t\t\t\t\t$ret  = date_i18n( $format, $time );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t$ret = __( 'To be Determined', 'lifterlms' );\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t$ret = apply_filters( 'llms_order_' . $type . '_access_expiration_date', $type, $this, $format );\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_order_get_access_expiration_date', $ret, $this, $format );\n\t}\n\n\t/**\n\t * Get the current status of a student's access\n\t *\n\t * Based on the access plan data stored on the order at the time of purchase.\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t * @since 5.2.0 Use stric type comparison.\n\t *\n\t * @return string 'inactive' If the order is refunded, failed, pending, etc...\n\t *                'expired'  If access has expired according to $this->get_access_expiration_date()\n\t *                'active'   Otherwise.\n\t */\n\tpublic function get_access_status() {\n\n\t\t$statuses = apply_filters(\n\t\t\t'llms_order_allow_access_stasuses',\n\t\t\tarray(\n\t\t\t\t'llms-active',\n\t\t\t\t'llms-completed',\n\t\t\t\t'llms-pending-cancel',\n\t\t\t\t/**\n\t\t\t\t * Recurring orders can expire but still grant access\n\t\t\t\t * eg: 3monthly payments grants 1 year of access\n\t\t\t\t * on the 4th month the order will be marked as expired\n\t\t\t\t * but the access has not yet expired based on the data below.\n\t\t\t\t */\n\t\t\t\t'llms-expired',\n\t\t\t)\n\t\t);\n\n\t\t// If the order doesn't have one of the allowed statuses.\n\t\t// Return 'inactive' and don't bother checking expiration data.\n\t\tif ( ! in_array( $this->get( 'status' ), $statuses, true ) ) {\n\n\t\t\treturn 'inactive';\n\n\t\t}\n\n\t\t// Get the expiration date as a timestamp.\n\t\t$expires = $this->get_access_expiration_date( 'U' );\n\n\t\t/**\n\t\t * A translated non-numeric string will be returned for lifetime access\n\t\t * so if we have a timestamp we should compare it against the current time\n\t\t * to determine if access has expired.\n\t\t */\n\t\tif ( is_numeric( $expires ) ) {\n\n\t\t\t$now = llms_current_time( 'timestamp' );\n\n\t\t\t// Expiration date is in the past\n\t\t\t// eg: the access has already expired.\n\t\t\tif ( $expires < $now ) {\n\n\t\t\t\treturn 'expired';\n\n\t\t\t}\n\t\t}\n\n\t\t// We're active.\n\t\treturn 'active';\n\t}\n\n\t/**\n\t * Retrieve arguments passed to order-related events processed by the action scheduler\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_action_args() {\n\t\treturn array(\n\t\t\t'order_id' => $this->get( 'id' ),\n\t\t);\n\t}\n\n\t/**\n\t * Get the formatted coupon amount with a currency symbol or percentage\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $payment Coupon discount type, either 'regular' or 'trial'.\n\t * @return string\n\t */\n\tpublic function get_coupon_amount( $payment = 'regular' ) {\n\n\t\tif ( 'regular' === $payment ) {\n\t\t\t$amount = $this->get( 'coupon_amount' );\n\t\t} elseif ( 'trial' === $payment ) {\n\t\t\t$amount = $this->get( 'coupon_amount_trial' );\n\t\t}\n\n\t\t$type = $this->get( 'coupon_type' );\n\t\tif ( 'percent' === $type ) {\n\t\t\t$amount = $amount . '%';\n\t\t} elseif ( 'dollar' === $type ) {\n\t\t\t$amount = llms_price( $amount );\n\t\t}\n\t\treturn $amount;\n\t}\n\n\t/**\n\t * Retrieve the customer's full name\n\t *\n\t * @since 3.0.0\n\t * @since 3.18.0 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_customer_name() {\n\t\tif ( 'yes' === $this->get( 'anonymized' ) ) {\n\t\t\treturn __( 'Anonymous', 'lifterlms' );\n\t\t}\n\t\treturn trim( $this->get( 'billing_first_name' ) . ' ' . $this->get( 'billing_last_name' ) );\n\t}\n\n\t/**\n\t * Retrieve the customer's full billing address\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_customer_full_address() {\n\n\t\t$billing_address_1 = $this->get( 'billing_address_1' );\n\t\tif ( empty( $billing_address_1 ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$address   = array(\n\t\t\ttrim( $billing_address_1 . ' ' . $this->get( 'billing_address_2' ) ),\n\t\t);\n\t\t$address[] = trim( $this->get( 'billing_city' ) . ' ' . $this->get( 'billing_state' ) );\n\t\t$address[] = $this->get( 'billing_zip' );\n\t\t$address[] = llms_get_country_name( $this->get( 'billing_country' ) );\n\n\t\treturn implode( ', ', array_filter( $address ) );\n\t}\n\n\t/**\n\t * An array of default arguments to pass to $this->create() when creating a new post\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t * @since 5.3.1 Set the `post_date` property using `llms_current_time()`.\n\t * @since 5.9.0 Remove usage of deprecated `strftime()`.\n\t *\n\t * @param string $title Title to create the post with.\n\t * @return array\n\t */\n\tprotected function get_creation_args( $title = '' ) {\n\n\t\t$date = llms_current_time( 'mysql' );\n\n\t\tif ( empty( $title ) ) {\n\n\t\t\t$title = sprintf(\n\t\t\t\t// Translators: %1$s = Transaction creation date.\n\t\t\t\t__( 'Order &ndash; %1$s', 'lifterlms' ),\n\t\t\t\tdate_format( date_create( $date ), 'M d, Y @ h:i A' )\n\t\t\t);\n\n\t\t}\n\n\t\treturn apply_filters(\n\t\t\t\"llms_{$this->model_post_type}_get_creation_args\",\n\t\t\tarray(\n\t\t\t\t'comment_status' => 'closed',\n\t\t\t\t'ping_status'    => 'closed',\n\t\t\t\t'post_author'    => 1,\n\t\t\t\t'post_content'   => '',\n\t\t\t\t'post_date'      => $date,\n\t\t\t\t'post_excerpt'   => '',\n\t\t\t\t'post_password'  => uniqid( 'order_' ),\n\t\t\t\t'post_status'    => 'llms-' . apply_filters( 'llms_default_order_status', 'pending' ),\n\t\t\t\t'post_title'     => $title,\n\t\t\t\t'post_type'      => $this->get( 'db_post_type' ),\n\t\t\t),\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the payment gateway instance for the order's selected payment gateway\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return LLMS_Payment_Gateway|WP_Error Instance of the LLMS_Payment_Gateway extending class used for the payment.\n\t *                                       WP_Error if the gateway cannot be located, e.g. because it's no longer enabled.\n\t */\n\tpublic function get_gateway() {\n\t\t$gateways = llms()->payment_gateways();\n\t\t$gateway  = $gateways->get_gateway_by_id( $this->get( 'payment_gateway' ) );\n\t\tif ( $gateway && ( $gateway->is_enabled() || is_admin() ) ) {\n\t\t\treturn $gateway;\n\t\t} else {\n\t\t\treturn new WP_Error( 'error', sprintf( __( 'Payment gateway %s could not be located or is no longer enabled', 'lifterlms' ), $this->get( 'payment_gateway' ) ) );\n\t\t}\n\t}\n\n\t/**\n\t * Get the initial payment amount due on checkout\n\t *\n\t * This will always be the value of \"total\" except when the product has a trial.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return mixed\n\t */\n\tpublic function get_initial_price( $price_args = array(), $format = 'html' ) {\n\n\t\tif ( $this->has_trial() ) {\n\t\t\t$price = 'trial_total';\n\t\t} else {\n\t\t\t$price = 'total';\n\t\t}\n\n\t\treturn $this->get_price( $price, $price_args, $format );\n\t}\n\n\n\t/**\n\t * Get an array of the order notes\n\t *\n\t * Each note is actually a WordPress comment.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param integer $number Number of comments to return.\n\t * @param integer $page   Page number for pagination.\n\t * @return array\n\t */\n\tpublic function get_notes( $number = 10, $page = 1 ) {\n\n\t\t$comments = get_comments(\n\t\t\tarray(\n\t\t\t\t'status'  => 'approve',\n\t\t\t\t'number'  => $number,\n\t\t\t\t'offset'  => ( $page - 1 ) * $number,\n\t\t\t\t'post_id' => $this->get( 'id' ),\n\t\t\t)\n\t\t);\n\n\t\treturn $comments;\n\t}\n\n\t/**\n\t * Retrieve an LLMS_Post_Model object for the associated product\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return LLMS_Post_Model|WP_Post|null|false LLMS_Post_Model extended object (LLMS_Course|LLMS_Membership),\n\t *                                            null if WP get_post() fails,\n\t *                                            false if LLMS_Post_Model extended class isn't found.\n\t */\n\tpublic function get_product() {\n\t\treturn llms_get_post( $this->get( 'product_id' ) );\n\t}\n\n\t/**\n\t * Retrieve the last (most recent) transaction processed for the order.\n\t *\n\t * @since 3.0.0\n\t * @since 7.1.0 Skip counting the total rows found when retrieving the last transaction.\n\t *\n\t * @param array|string $status Filter by status (see transaction statuses). By default looks for any status.\n\t * @param array|string $type   Filter by type [recurring|single|trial]. By default looks for any type.\n\t * @return LLMS_Transaction|false instance of the LLMS_Transaction or false if none found\n\t */\n\tpublic function get_last_transaction( $status = 'any', $type = 'any' ) {\n\t\t$txns = $this->get_transactions(\n\t\t\tarray(\n\t\t\t\t'per_page'      => 1,\n\t\t\t\t'status'        => $status,\n\t\t\t\t'type'          => $type,\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\t\tif ( $txns['count'] ) {\n\t\t\treturn array_pop( $txns['transactions'] );\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve the date of the last (most recent) transaction\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array|string $status Optional. Filter by status (see transaction statuses). Default is 'llms-txn-succeeded'.\n\t * @param array|string $type   Optional. Filter by type [recurring|single|trial]. By default looks for any type.\n\t * @param string       $format Optional. Date format of the return. Default is 'Y-m-d H:i:s'.\n\t * @return string|false Date or false if none found.\n\t */\n\tpublic function get_last_transaction_date( $status = 'llms-txn-succeeded', $type = 'any', $format = 'Y-m-d H:i:s' ) {\n\t\t$txn = $this->get_last_transaction( $status, $type );\n\t\tif ( $txn ) {\n\t\t\treturn $txn->get_date( 'date', $format );\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve the due date of the next payment according to access plan terms\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t * @since 5.2.0 Use stric type comparisons.\n\t *\n\t * @param string $format Optional. Date return format. Default is 'Y-m-d H:i:s'.\n\t * @return string\n\t */\n\tpublic function get_next_payment_due_date( $format = 'Y-m-d H:i:s' ) {\n\n\t\t// Single payments will never have a next payment date.\n\t\tif ( ! $this->is_recurring() ) {\n\t\t\treturn new WP_Error( 'not-recurring', __( 'Order is not recurring', 'lifterlms' ) );\n\t\t} elseif ( ! in_array( $this->get( 'status' ), array( 'llms-active', 'llms-failed', 'llms-on-hold', 'llms-pending', 'llms-pending-cancel' ), true ) ) {\n\t\t\treturn new WP_Error( 'invalid-status', __( 'Invalid order status', 'lifterlms' ), $this->get( 'status' ) );\n\t\t}\n\n\t\t// Retrieve the saved due date.\n\t\t$next_payment_time = $this->get_date( 'date_next_payment', 'U' );\n\t\t// Calculate it if not saved.\n\t\tif ( ! $next_payment_time ) {\n\t\t\t$next_payment_time = $this->calculate_next_payment_date( 'U' );\n\t\t\tif ( ! $next_payment_time ) {\n\t\t\t\treturn new WP_Error( 'plan-ended', __( 'No more payments due', 'lifterlms' ) );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter the next payment due date.\n\t\t *\n\t\t * A timestamp should always be returned as the conversion to the requested format\n\t\t * will be performed on the returned value.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param int        $next_payment_time Unix timestamp for the next payment due date.\n\t\t * @param LLMS_Order $order             Order object.\n\t\t * @param string     $format            Requested date format.\n\t\t */\n\t\t$next_payment_time = apply_filters( 'llms_order_get_next_payment_due_date', $next_payment_time, $this, $format );\n\n\t\treturn date_i18n( $format, $next_payment_time );\n\t}\n\n\t/**\n\t * Retrieve the timestamp of the next scheduled event for a given action\n\t *\n\t * @since 4.6.0\n\t *\n\t * @param string $action Action hook ID. Core actions are \"llms_charge_recurring_payment\", \"llms_access_plan_expiration\".\n\t * @return int|false Returns the timestamp of the next action as an integer or `false` when no action exist.\n\t */\n\tpublic function get_next_scheduled_action_time( $action ) {\n\t\treturn as_next_scheduled_action( $action, $this->get_action_args() );\n\t}\n\n\t/**\n\t * Retrieves the number of payments remaining for a recurring plan with a limited number of payments\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return bool|int Returns `false` for invalid order types (single-payment orders or recurring orders\n\t *                  without a billing length). Otherwise returns the number of remaining payments as an integer.\n\t */\n\tpublic function get_remaining_payments() {\n\n\t\t$remaining = false;\n\n\t\tif ( $this->has_plan_expiration() ) {\n\t\t\t$len  = $this->get( 'billing_length' );\n\t\t\t$txns = $this->get_transactions(\n\t\t\t\tarray(\n\t\t\t\t\t'status'   => array( 'llms-txn-succeeded', 'llms-txn-refunded' ),\n\t\t\t\t\t'per_page' => 1,\n\t\t\t\t\t'type'     => array( 'recurring', 'single' ), // If a manual payment is recorded it's counted a single payment and that should count.\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$remaining = $len - $txns['total'];\n\t\t}\n\n\t\t/**\n\t\t * Filters the number of payments remaining for a recurring plan with a limited number of payments.\n\t\t *\n\t\t * @since 5.3.0\n\t\t *\n\t\t * @param bool|int   $remaining Number of remaining payments or `false` when called against invalid order types.\n\t\t * @param LLMS_Order $order     Order object.\n\t\t */\n\t\treturn apply_filters( 'llms_order_remaining_payments', $remaining, $this );\n\t}\n\n\t/**\n\t * Get configured payment retry rules\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return array[] {\n\t *     An array of retry rule arrays.\n\t *\n\t *     @type int    $delay         The number of seconds to delay to use when scheduling the retry attempt.\n\t *     @type string $status        The status of the order while awaiting the next retry.\n\t *     @type bool   $notifications Whether or not to trigger notifications to the student/user.\n\t * }\n\t */\n\tprivate function get_retry_rules() {\n\n\t\t$rules = array(\n\t\t\tarray(\n\t\t\t\t'delay'         => HOUR_IN_SECONDS * 12,\n\t\t\t\t'status'        => 'on-hold',\n\t\t\t\t'notifications' => false,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'delay'         => DAY_IN_SECONDS,\n\t\t\t\t'status'        => 'on-hold',\n\t\t\t\t'notifications' => true,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'delay'         => DAY_IN_SECONDS * 2,\n\t\t\t\t'status'        => 'on-hold',\n\t\t\t\t'notifications' => true,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'delay'         => DAY_IN_SECONDS * 3,\n\t\t\t\t'status'        => 'on-hold',\n\t\t\t\t'notifications' => true,\n\t\t\t),\n\t\t);\n\n\t\t/**\n\t\t * Filters the automatic payment recurring retry rules.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param array      $rules Array of retry rule arrays {@see LLMS_Order::get_retry_rules()}.\n\t\t * @param LLMS_Order $rules The order object.\n\t\t */\n\t\treturn apply_filters( 'llms_order_automatic_retry_rules', $rules, $this );\n\t}\n\n\t/**\n\t * SQL query to retrieve total amounts for transactions by type\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Prepare SQL query properly.\n\t * @since 7.7.0 Caching results to avoid duplicate queries.\n\t *\n\t * @param string $type Optional. Type can be 'amount' or 'refund_amount'. Default is 'amount'.\n\t * @return float\n\t */\n\tpublic function get_transaction_total( $type = 'amount' ) {\n\n\t\t// Check the cache.\n\t\tstatic $cache = array();\n\t\tif ( isset( $cache[ $this->get( 'id' ) ] )\n\t\t\t&& isset( $cache[ $this->get( 'id' ) ][ $type ] ) ) {\n\t\t\treturn $cache[ $this->get( 'id' ) ][ $type ];\n\t\t}\n\n\t\t$statuses = array( 'llms-txn-refunded' );\n\n\t\tif ( 'amount' === $type ) {\n\t\t\t$statuses[] = 'llms-txn-succeeded';\n\t\t}\n\n\t\t$post_statuses = '';\n\t\tforeach ( $statuses as $i => $status ) {\n\t\t\t$post_statuses .= \" p.post_status = '$status'\";\n\t\t\tif ( $i + 1 < count( $statuses ) ) {\n\t\t\t\t$post_statuses .= 'OR';\n\t\t\t}\n\t\t}\n\n\t\tglobal $wpdb;\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $post_statuses is prepared above.\n\t\t$grosse = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT SUM( m2.meta_value )\n\t\t\t FROM $wpdb->posts AS p\n\t\t\t LEFT JOIN $wpdb->postmeta AS m1 ON m1.post_id = p.ID -- Join for the ID.\n\t\t\t LEFT JOIN $wpdb->postmeta AS m2 ON m2.post_id = p.ID -- Get the actual amounts.\n\t\t\t WHERE p.post_type = 'llms_transaction'\n\t\t\t   AND ( $post_statuses )\n\t\t\t   AND m1.meta_key = %s\n\t\t\t   AND m1.meta_value = %d\n\t\t\t   AND m2.meta_key = %s\n\t\t\t;\",\n\t\t\t\tarray(\n\t\t\t\t\t\"{$this->meta_prefix}order_id\",\n\t\t\t\t\t$this->get( 'id' ),\n\t\t\t\t\t\"{$this->meta_prefix}{$type}\",\n\t\t\t\t)\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t// Save to cache.\n\t\tif ( ! isset( $cache[ $this->get( 'id' ) ] ) ) {\n\t\t\t$cache[ $this->get( 'id' ) ] = array();\n\t\t}\n\t\t$cache[ $this->get( 'id' ) ][ $type ] = floatval( $grosse );\n\n\t\treturn $cache[ $this->get( 'id' ) ][ $type ];\n\t}\n\n\t/**\n\t * Get the start date for the order.\n\t *\n\t * Gets the date of the first initially successful transaction\n\t * if none found, uses the created date of the order.\n\t *\n\t * @since 3.0.0\n\t * @since 7.1.0 Skip counting the total rows found when retrieving the first transaction.\n\t *\n\t * @param string $format Desired return format of the date.\n\t * @return string\n\t */\n\tpublic function get_start_date( $format = 'Y-m-d H:i:s' ) {\n\t\t/**\n\t\t * Get the first recorded transaction.\n\t\t * Refunds are okay b/c that would have initially given the user access.\n\t\t */\n\t\t$txns = $this->get_transactions(\n\t\t\tarray(\n\t\t\t\t'order'         => 'ASC',\n\t\t\t\t'orderby'       => 'date',\n\t\t\t\t'per_page'      => 1,\n\t\t\t\t'status'        => array( 'llms-txn-succeeded', 'llms-txn-refunded' ),\n\t\t\t\t'type'          => 'any',\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\t\tif ( $txns['count'] ) {\n\t\t\t$txn  = array_pop( $txns['transactions'] );\n\t\t\t$date = $txn->get_date( 'date', $format );\n\t\t} else {\n\t\t\t$date = $this->get_date( 'date', $format );\n\t\t}\n\n\t\t/**\n\t\t * Filter the order start date.\n\t\t *\n\t\t * @since 3.0.0\n\t\t * @since 7.1.0 Added the `$format` parameter.\n\t\t *\n\t\t * @param string     $date   The formatted start date for the order.\n\t\t * @param LLMS_Order $order  The order object.\n\t\t * @param string     $format The requested format of the date.\n\t\t */\n\t\treturn apply_filters( 'llms_order_get_start_date', $date, $this, $format );\n\t}\n\n\t/**\n\t * Retrieves the user action required when changing the order's payment source.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return null|string Returns `switch` when the payment source can be switched and `pay` when payment on the new source\n\t *                     is required before switching. A `null` return indicates that the order's payment source cannot be switched.\n\t */\n\tpublic function get_switch_source_action() {\n\n\t\t$action = null;\n\t\tif ( $this->can_switch_source() ) {\n\t\t\t$action = in_array( $this->get( 'status' ), array( 'llms-active', 'llms-pending-cancel' ), true ) ? 'switch' : 'pay';\n\t\t}\n\n\t\t/**\n\t\t * Filters the required user action for the order when switching the order's payment source.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param null|string $action The switch action ID or `null` when the payment source cannot be switched.\n\t\t * @param LLMS_Order  $order  The order object.\n\t\t */\n\t\treturn apply_filters( 'llms_order_switch_source_action', $action, $this );\n\t}\n\n\t/**\n\t * Retrieve an array of transactions associated with the order according to supplied arguments.\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t * @since 3.37.6 Add additional return property, `total`, which returns the total number of found transactions.\n\t * @since 5.2.0 Use stric type comparisons.\n\t * @since 7.1.0 Added `no_found_rows` parameter.\n\t *\n\t * @param array $args {\n\t *     Hash of query argument data, ultimately passed to a WP_Query.\n\t *\n\t *     @type string|string[] $status        Transaction post status or array of transaction post status. Defaults to \"any\".\n\t *     @type string|string[] $type          Transaction types or array of transaction types. Defaults to \"any\".\n\t *                                          Accepts \"recurring\", \"single\", or \"trial\".\n\t *     @type int             $per_page      Number of transactions to include in the return. Default `50`.\n\t *     @type int             $paged         Result set page number.\n\t *     @type string          $order         Result set order. Default \"DESC\". Accepts \"DESC\" or \"ASC\".\n\t *     @type string          $orderby       Result set ordering field. Default \"date\".\n\t *     @type bool            $no_found_rows Whether to skip counting the total rows found. Enabling can improve\n\t *                                          performance. Default `false`.\n\t * }\n\t * @return array\n\t */\n\tpublic function get_transactions( $args = array() ) {\n\n\t\textract(\n\t\t\twp_parse_args(\n\t\t\t\t$args,\n\t\t\t\tarray(\n\t\t\t\t\t'status'        => 'any', // String or array or post statuses.\n\t\t\t\t\t'type'          => 'any', // String or array of transaction types [recurring|single|trial].\n\t\t\t\t\t'per_page'      => 50, // Int, number of transactions to return.\n\t\t\t\t\t'paged'         => 1, // Int, page number of transactions to return.\n\t\t\t\t\t'order'         => 'DESC',\n\t\t\t\t\t'orderby'       => 'date', // Field to order results by.\n\t\t\t\t\t'no_found_rows' => false,\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Assume any and use this to check for valid statuses.\n\t\t$statuses = llms_get_transaction_statuses();\n\n\t\t// Check statuses.\n\t\tif ( 'any' !== $statuses ) {\n\n\t\t\t// If status is a string, ensure it's a valid status.\n\t\t\tif ( is_string( $status ) && in_array( $status, $statuses, true ) ) {\n\t\t\t\t$statuses = array( $status );\n\t\t\t} elseif ( is_array( $status ) ) {\n\t\t\t\t$temp = array();\n\t\t\t\tforeach ( $status as $stat ) {\n\t\t\t\t\tif ( in_array( (string) $stat, $statuses, true ) ) {\n\t\t\t\t\t\t$temp[] = $stat;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$statuses = $temp;\n\t\t\t}\n\t\t}\n\n\t\t// Setup type meta query.\n\t\t$types = array(\n\t\t\t'relation' => 'OR',\n\t\t);\n\n\t\tif ( 'any' === $type ) {\n\t\t\t$types[] = array(\n\t\t\t\t'key'   => $this->meta_prefix . 'payment_type',\n\t\t\t\t'value' => 'recurring',\n\t\t\t);\n\t\t\t$types[] = array(\n\t\t\t\t'key'   => $this->meta_prefix . 'payment_type',\n\t\t\t\t'value' => 'single',\n\t\t\t);\n\t\t\t$types[] = array(\n\t\t\t\t'key'   => $this->meta_prefix . 'payment_type',\n\t\t\t\t'value' => 'trial',\n\t\t\t);\n\t\t} elseif ( is_string( $type ) ) {\n\t\t\t$types[] = array(\n\t\t\t\t'key'   => $this->meta_prefix . 'payment_type',\n\t\t\t\t'value' => $type,\n\t\t\t);\n\t\t} elseif ( is_array( $type ) ) {\n\t\t\tforeach ( $type as $t ) {\n\t\t\t\t$types[] = array(\n\t\t\t\t\t'key'   => $this->meta_prefix . 'payment_type',\n\t\t\t\t\t'value' => $t,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Execute the query.\n\t\t$query = new WP_Query(\n\t\t\t/**\n\t\t\t * Filters the order's transactions query aguments.\n\t\t\t *\n\t\t\t * @since 3.0.0\n\t\t\t * @since 7.1.0 Added `$no_found_rows` arg.\n\t\t\t *\n\t\t\t * @param array $query_args {\n\t\t\t *     Hash of query argument data passed to a WP_Query.\n\t\t\t *\n\t\t\t *     @type string|string[] $status        Transaction post status or array of transaction post status.\n\t\t\t *                                          Defaults to \"any\".\n\t\t\t *     @type string|string[] $type          Transaction types or array of transaction types.\n\t\t\t *                                          Defaults to \"any\".\n\t\t\t *                                          Accepts \"recurring\", \"single\", or \"trial\".\n\t\t\t *     @type int             $per_page      Number of transactions to include in the return. Default `50`.\n\t\t\t *     @type int             $paged         Result set page number.\n\t\t\t *     @type string          $order         Result set order. Default \"DESC\". Accepts \"DESC\" or \"ASC\".\n\t\t\t *     @type string          $orderby       Result set ordering field. Default \"date\".\n\t\t\t *     @type bool            $no_found_rows Whether to skip counting the total rows found.\n\t\t\t *                                          Enabling can improve performance. Default false.\n\t\t\t * }\n\t\t\t */\n\t\t\tapply_filters(\n\t\t\t\t'llms_order_get_transactions_query',\n\t\t\t\tarray(\n\t\t\t\t\t'meta_query'     => array(\n\t\t\t\t\t\t'relation' => 'AND',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'key'   => $this->meta_prefix . 'order_id',\n\t\t\t\t\t\t\t'value' => $this->get( 'id' ),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t$types,\n\t\t\t\t\t),\n\t\t\t\t\t'order'          => $order,\n\t\t\t\t\t'orderby'        => $orderby,\n\t\t\t\t\t'post_status'    => $statuses,\n\t\t\t\t\t'post_type'      => 'llms_transaction',\n\t\t\t\t\t'posts_per_page' => $per_page,\n\t\t\t\t\t'paged'          => $paged,\n\t\t\t\t\t'no_found_rows'  => $no_found_rows,\n\t\t\t\t)\n\t\t\t),\n\t\t\t$this,\n\t\t\t$status\n\t\t);\n\n\t\t$transactions = array();\n\n\t\tforeach ( $query->posts as $post ) {\n\t\t\t$transactions[ $post->ID ] = llms_get_post( $post );\n\t\t}\n\n\t\treturn array(\n\t\t\t'total'        => $query->found_posts,\n\t\t\t'count'        => $query->post_count,\n\t\t\t'page'         => $paged,\n\t\t\t'pages'        => $query->max_num_pages,\n\t\t\t'transactions' => $transactions,\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the date when a trial will end\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $format Optional. Date return format. Default is 'Y-m-d H:i:s'.\n\t * @return string\n\t */\n\tpublic function get_trial_end_date( $format = 'Y-m-d H:i:s' ) {\n\n\t\tif ( ! $this->has_trial() ) {\n\n\t\t\t$trial_end_date = '';\n\n\t\t} else {\n\n\t\t\t// Retrieve the saved end date.\n\t\t\t$trial_end_date = $this->get_date( 'date_trial_end', $format );\n\n\t\t\t// If not saved, calculate it.\n\t\t\tif ( ! $trial_end_date ) {\n\n\t\t\t\t$trial_end_date = $this->calculate_trial_end_date( $format );\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_order_get_trial_end_date', $trial_end_date, $this );\n\t}\n\n\t/**\n\t * Gets the total revenue of an order\n\t *\n\t * @since 3.0.0\n\t * @since 3.1.3 Handle legacy orders.\n\t *\n\t * @param string $type Optional. Revenue type [grosse|net]. Default is 'net'.\n\t * @return float\n\t */\n\tpublic function get_revenue( $type = 'net' ) {\n\n\t\tif ( $this->is_legacy() ) {\n\n\t\t\t$amount = $this->get( 'total' );\n\n\t\t} else {\n\n\t\t\t$amount = $this->get_transaction_total( 'amount' );\n\n\t\t\tif ( 'net' === $type ) {\n\n\t\t\t\t$refunds = $this->get_transaction_total( 'refund_amount' );\n\n\t\t\t\t$amount = $amount - $refunds;\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_order_get_revenue', $amount, $type, $this );\n\t}\n\n\t/**\n\t * Get a link to view the order on the student dashboard\n\t *\n\t * @since 3.0.0\n\t * @since 3.8.0 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_view_link() {\n\n\t\t$link = llms_get_endpoint_url( 'orders', $this->get( 'id' ), llms_get_page_url( 'myaccount' ) );\n\t\treturn apply_filters( 'llms_order_get_view_link', $link, $this );\n\t}\n\n\t/**\n\t * Determine if the student associated with this order has access\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_access() {\n\t\treturn ( 'active' === $this->get_access_status() ) ? true : false;\n\t}\n\n\t/**\n\t * Determine if a coupon was used\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_coupon() {\n\t\treturn ( 'yes' === $this->get( 'coupon_used' ) );\n\t}\n\n\t/**\n\t * Determine if there was a discount applied to this order via either a sale or a coupon\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_discount() {\n\t\treturn ( $this->has_coupon() || $this->has_sale() );\n\t}\n\n\t/**\n\t * Determine if a recurring order has a limited number of payments\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return boolean Returns `true` for recurring orders with a billing length and `false` otherwise.\n\t */\n\tpublic function has_plan_expiration() {\n\t\treturn ( $this->is_recurring() && ( $this->get( 'billing_length' ) > 0 ) );\n\t}\n\n\t/**\n\t * Determine if the access plan was on sale during the purchase\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_sale() {\n\t\treturn ( 'yes' === $this->get( 'on_sale' ) );\n\t}\n\n\t/**\n\t * Determine if there's a payment scheduled for the order\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_scheduled_payment() {\n\t\t$date = $this->get_next_payment_due_date();\n\t\treturn is_wp_error( $date ) ? false : true;\n\t}\n\n\t/**\n\t * Determine if the order has a trial\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean True if has a trial, false if it doesn't.\n\t */\n\tpublic function has_trial() {\n\t\treturn ( $this->is_recurring() && 'yes' === $this->get( 'trial_offer' ) );\n\t}\n\n\t/**\n\t * Determine if the trial period has ended for the order\n\t *\n\t * @since 3.0.0\n\t * @since 3.10.0 Unknown.\n\t *\n\t * @return boolean True if ended, false if not ended.\n\t */\n\tpublic function has_trial_ended() {\n\t\treturn ( llms_current_time( 'timestamp' ) >= $this->get_trial_end_date( 'U' ) );\n\t}\n\n\t/**\n\t * Initializes a new order with user, plan, gateway, and coupon metadata.\n\t *\n\t * Assumes all data passed in has already been validated.\n\t *\n\t * @since 3.8.0\n\t * @since 3.10.0 Unknown.\n\t * @since 5.3.0 Don't set unused legacy property `date_billing_end`.\n\t * @since 7.0.0 Use `LLMS_Order::set_user_data()` to update user data.\n\t *\n\t * @param array|LLMS_Student|WP_User|integer $user_data User info for the person placing the order. See\n\t *                                                      {@see LLMS_Order::set_user_data()} for more info.\n\t * @param LLMS_Access_Plan                   $plan      The purchase access plan.\n\t * @param LLMS_Payment_Gateway               $gateway   Gateway being used.\n\t * @param LLMS_Coupon                        $coupon    Coupon object or `false` if no coupon used.\n\t * @return LLMS_Order\n\t */\n\tpublic function init( $user_data, $plan, $gateway, $coupon = false ) {\n\n\t\t$this->set_user_data( $user_data );\n\n\t\t// Access plan data.\n\t\t$this->set( 'plan_id', $plan->get( 'id' ) );\n\t\t$this->set( 'plan_title', $plan->get( 'title' ) );\n\t\t$this->set( 'plan_sku', $plan->get( 'sku' ) );\n\n\t\t// Product data.\n\t\t$product = $plan->get_product();\n\t\t$this->set( 'product_id', $product->get( 'id' ) );\n\t\t$this->set( 'product_title', $product->get( 'title' ) );\n\t\t$this->set( 'product_sku', $product->get( 'sku' ) );\n\t\t$this->set( 'product_type', $plan->get_product_type() );\n\n\t\t$this->set( 'payment_gateway', $gateway->get_id() );\n\t\t$this->set( 'gateway_api_mode', $gateway->get_api_mode() );\n\n\t\t// Trial data.\n\t\tif ( $plan->has_trial() ) {\n\t\t\t$this->set( 'trial_offer', 'yes' );\n\t\t\t$this->set( 'trial_length', $plan->get( 'trial_length' ) );\n\t\t\t$this->set( 'trial_period', $plan->get( 'trial_period' ) );\n\t\t\t$trial_price = $plan->get_price( 'trial_price', array(), 'float' );\n\t\t\t$this->set( 'trial_original_total', $trial_price );\n\t\t\t$trial_total = $coupon ? $plan->get_price_with_coupon( 'trial_price', $coupon, array(), 'float' ) : $trial_price;\n\t\t\t$this->set( 'trial_total', $trial_total );\n\t\t\t$this->set( 'date_trial_end', $this->calculate_trial_end_date() );\n\t\t} else {\n\t\t\t$this->set( 'trial_offer', 'no' );\n\t\t}\n\n\t\t$price = $plan->get_price( 'price', array(), 'float' );\n\t\t$this->set( 'currency', get_lifterlms_currency() );\n\n\t\t// Price data.\n\t\tif ( $plan->is_on_sale() ) {\n\t\t\t$price_key = 'sale_price';\n\t\t\t$this->set( 'on_sale', 'yes' );\n\t\t\t$sale_price = $plan->get( 'sale_price', array(), 'float' );\n\t\t\t$this->set( 'sale_price', $sale_price );\n\t\t\t$this->set( 'sale_value', $price - $sale_price );\n\t\t} else {\n\t\t\t$price_key = 'price';\n\t\t\t$this->set( 'on_sale', 'no' );\n\t\t}\n\n\t\t// Store original total before any discounts.\n\t\t$this->set( 'original_total', $price );\n\n\t\t// Get the actual total due after discounts if any are applicable.\n\t\t$total = $coupon ? $plan->get_price_with_coupon( $price_key, $coupon, array(), 'float' ) : $$price_key;\n\t\t$this->set( 'total', $total );\n\n\t\t// Coupon data.\n\t\tif ( $coupon ) {\n\t\t\t$this->set( 'coupon_id', $coupon->get( 'id' ) );\n\t\t\t$this->set( 'coupon_amount', $coupon->get( 'coupon_amount' ) );\n\t\t\t$this->set( 'coupon_code', $coupon->get( 'title' ) );\n\t\t\t$this->set( 'coupon_type', $coupon->get( 'discount_type' ) );\n\t\t\t$this->set( 'coupon_used', 'yes' );\n\t\t\t$this->set( 'coupon_value', $$price_key - $total );\n\t\t\tif ( $plan->has_trial() && $coupon->has_trial_discount() ) {\n\t\t\t\t$this->set( 'coupon_amount_trial', $coupon->get( 'trial_amount' ) );\n\t\t\t\t$this->set( 'coupon_value_trial', $trial_price - $trial_total );\n\t\t\t}\n\t\t} else {\n\t\t\t$this->set( 'coupon_used', 'no' );\n\t\t}\n\n\t\t// Get all billing schedule related information.\n\t\t$this->set( 'billing_frequency', $plan->get( 'frequency' ) );\n\t\tif ( $plan->is_recurring() ) {\n\t\t\t$this->set( 'billing_length', $plan->get( 'length' ) );\n\t\t\t$this->set( 'billing_period', $plan->get( 'period' ) );\n\t\t\t$this->set( 'order_type', 'recurring' );\n\t\t\t$this->set( 'date_next_payment', $this->calculate_next_payment_date() );\n\t\t} else {\n\t\t\t$this->set( 'order_type', 'single' );\n\t\t}\n\n\t\t$this->set( 'access_expiration', $plan->get( 'access_expiration' ) );\n\n\t\t// Get access related data so when payment is complete we can calculate the actual expiration date.\n\t\tif ( $plan->can_expire() ) {\n\t\t\t$this->set( 'access_expires', $plan->get( 'access_expires' ) );\n\t\t\t$this->set( 'access_length', $plan->get( 'access_length' ) );\n\t\t\t$this->set( 'access_period', $plan->get( 'access_period' ) );\n\t\t}\n\n\t\t/**\n\t\t * Action triggered after the order is initialized.\n\t\t *\n\t\t * @since Unknown.\n\t\t * @since 7.0.0 Added `$user_data` parameter.\n\t\t *                 The `$student` parameter returns an \"empty\" student object\n\t\t *                 if the method's input data is an array instead of an existing\n\t\t *                 user object.\n\t\t *\n\t\t * @param LLMS_Order                         $order     The order object.\n\t\t * @param LLMS_Student                       $student   The student object. If an array of data is passed\n\t\t *                                                      to `LLMS_Order::init()` then an empty student object\n\t\t *                                                      will be passed.\n\t\t * @param array|LLMS_Student|WP_User|integer $user_data User data.\n\t\t */\n\t\tdo_action(\n\t\t\t'lifterlms_new_pending_order',\n\t\t\t$this,\n\t\t\tis_array( $user_data ) ? new LLMS_Student( null, false ) : llms_get_student( $user_data ),\n\t\t\t$user_data\n\t\t);\n\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Determine if the order is a legacy order migrated from 2.x\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_legacy() {\n\t\treturn ( 'publish' === $this->get( 'status' ) );\n\t}\n\n\t/**\n\t * Determine if the order is recurring or singular\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return boolean True if recurring, false if not.\n\t */\n\tpublic function is_recurring() {\n\t\treturn $this->get( 'order_type' ) === 'recurring';\n\t}\n\n\t/**\n\t * Schedule access expiration\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_schedule_expiration() {\n\n\t\t// Get expiration date based on setting.\n\t\t$expires = $this->get_access_expiration_date( 'U' );\n\n\t\t// Will return a timestamp or \"Lifetime Access as a string\".\n\t\tif ( is_numeric( $expires ) ) {\n\t\t\t$this->unschedule_expiration();\n\t\t\tas_schedule_single_action( $expires, 'llms_access_plan_expiration', $this->get_action_args() );\n\t\t}\n\t}\n\n\t/**\n\t * Schedules the next payment due on a recurring order\n\t *\n\t * Can be called without consequence on a single payment order.\n\t * Will always unschedule the scheduled action (if one exists) before scheduling another.\n\t *\n\t * @since 3.0.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 4.7.0 Add `plan_ended` metadata when a plan ends.\n\t * @since 5.2.0 Move scheduling recurring payment into a proper method.\n\t *\n\t * @return void\n\t */\n\tpublic function maybe_schedule_payment( $recalc = true ) {\n\n\t\tif ( ! $this->is_recurring() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( $recalc ) {\n\t\t\t$this->set( 'date_next_payment', $this->calculate_next_payment_date() );\n\t\t}\n\n\t\t$date = $this->get_next_payment_due_date();\n\n\t\t// Unschedule and reschedule.\n\t\tif ( $date && ! is_wp_error( $date ) ) {\n\n\t\t\t$this->schedule_recurring_payment( $date );\n\n\t\t} elseif ( is_wp_error( $date ) ) {\n\n\t\t\tif ( 'plan-ended' === $date->get_error_code() ) {\n\n\t\t\t\t// Unschedule the next action (does nothing if no action scheduled).\n\t\t\t\t$this->unschedule_recurring_payment();\n\n\t\t\t\t// Add a note that the plan has completed.\n\t\t\t\t$this->add_note( __( 'Order payment plan completed.', 'lifterlms' ) );\n\t\t\t\t$this->set( 'plan_ended', 'yes' );\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Handles scheduling recurring payment retries when the gateway supports them\n\t *\n\t * @since 3.10.0\n\t * @since 7.0.0 Added return value.\n\t *\n\t * @return null|boolean Returns `null` if the order cannot be retried, `false` when all retry rules have been tried (or none exist), and `true`\n\t *                      when a retry is scheduled.\n\t */\n\tpublic function maybe_schedule_retry() {\n\n\t\tif ( ! $this->can_be_retried() ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Get the index of the rule to use for this retry.\n\t\t$current_rule_index = $this->get( 'last_retry_rule' );\n\t\tif ( '' === $current_rule_index ) {\n\t\t\t$current_rule_index = 0;\n\t\t} else {\n\t\t\t++$current_rule_index;\n\t\t}\n\n\t\t$rules        = $this->get_retry_rules();\n\t\t$current_rule = $rules[ $current_rule_index ] ?? false;\n\n\t\t// No rule to run.\n\t\tif ( ! $current_rule ) {\n\n\t\t\t$this->set_status( 'failed' );\n\t\t\t$this->set( 'last_retry_rule', '' );\n\n\t\t\t$this->add_note( esc_html__( 'Maximum retry attempts reached.', 'lifterlms' ) );\n\n\t\t\t/**\n\t\t\t * Action triggered when there are not more recurring payment retry rules.\n\t\t\t *\n\t\t\t * @since 3.10.0\n\t\t\t *\n\t\t\t * @param LLMS_Order $order The order object.\n\t\t\t */\n\t\t\tdo_action( 'llms_automatic_payment_maximum_retries_reached', $this );\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\t$timestamp = current_time( 'timestamp' ) + $current_rule['delay'];\n\n\t\t$this->set_date( 'next_payment', date_i18n( 'Y-m-d H:i:s', $timestamp ) );\n\t\t$this->set_status( $current_rule['status'] );\n\t\t$this->set( 'last_retry_rule', $current_rule_index );\n\n\t\t$this->add_note(\n\t\t\tsprintf(\n\t\t\t\t// Translators: %s = next attempt date.\n\t\t\t\tesc_html__( 'Automatic retry attempt scheduled for %s', 'lifterlms' ),\n\t\t\t\tdate( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp )\n\t\t\t)\n\t\t);\n\n\t\t// If notifications should be sent, trigger them.\n\t\tif ( $current_rule['notifications'] ) {\n\t\t\t/**\n\t\t\t * Triggers the \"Payment Retry Scheduled\" notification.\n\t\t\t *\n\t\t\t * @since 3.10.0\n\t\t\t *\n\t\t\t * @param LLMS_Order $order The order object.\n\t\t\t */\n\t\t\tdo_action( 'llms_send_automatic_payment_retry_notification', $this );\n\t\t}\n\n\t\t/**\n\t\t * Action triggered after a recurring payment retry is successfully scheduled.\n\t\t *\n\t\t * @since 3.10.0\n\t\t *\n\t\t * @param LLMS_Order $order The order object.\n\t\t */\n\t\tdo_action( 'llms_automatic_payment_retry_scheduled', $this );\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Record a transaction for the order\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param array $data Optional array of additional data to store for the transaction.\n\t * @return LLMS_Transaction Instance of LLMS_Transaction for the created transaction.\n\t */\n\tpublic function record_transaction( $data = array() ) {\n\n\t\textract(\n\t\t\tarray_merge(\n\t\t\t\tarray(\n\t\t\t\t\t'amount'             => 0,\n\t\t\t\t\t'completed_date'     => current_time( 'mysql' ),\n\t\t\t\t\t'customer_id'        => '',\n\t\t\t\t\t'fee_amount'         => 0,\n\t\t\t\t\t'source_id'          => '',\n\t\t\t\t\t'source_description' => '',\n\t\t\t\t\t'transaction_id'     => '',\n\t\t\t\t\t'status'             => 'llms-txn-succeeded',\n\t\t\t\t\t'payment_gateway'    => $this->get( 'payment_gateway' ),\n\t\t\t\t\t'payment_type'       => 'single',\n\t\t\t\t),\n\t\t\t\t$data\n\t\t\t)\n\t\t);\n\n\t\t$txn = new LLMS_Transaction( 'new', $this->get( 'id' ) );\n\n\t\t$txn->set( 'api_mode', $this->get( 'gateway_api_mode' ) );\n\t\t$txn->set( 'amount', $amount );\n\t\t$txn->set( 'currency', $this->get( 'currency' ) );\n\t\t$txn->set( 'gateway_completed_date', date_i18n( 'Y-m-d h:i:s', strtotime( $completed_date ) ) );\n\t\t$txn->set( 'gateway_customer_id', $customer_id );\n\t\t$txn->set( 'gateway_fee_amount', $fee_amount );\n\t\t$txn->set( 'gateway_source_id', $source_id );\n\t\t$txn->set( 'gateway_source_description', $source_description );\n\t\t$txn->set( 'gateway_transaction_id', $transaction_id );\n\t\t$txn->set( 'order_id', $this->get( 'id' ) );\n\t\t$txn->set( 'payment_gateway', $payment_gateway );\n\t\t$txn->set( 'payment_type', $payment_type );\n\t\t$txn->set( 'status', $status );\n\n\t\treturn $txn;\n\t}\n\n\t/**\n\t * Date field setter for date fields that require things to be updated when their value changes\n\t *\n\t * This is mainly used to allow updating dates which are editable from the admin panel which\n\t * should trigger additional actions when updated.\n\t *\n\t * Settable dates: date_next_payment, date_trial_end, date_access_expires.\n\t *\n\t * @since 3.10.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @param string $date_key Date field to set.\n\t * @param string $date_val Date string or a unix time stamp.\n\t */\n\tpublic function set_date( $date_key, $date_val ) {\n\n\t\t// Convert to timestamp if not already a timestamp.\n\t\tif ( ! is_numeric( $date_val ) ) {\n\t\t\t$date_val = strtotime( $date_val );\n\t\t}\n\n\t\t$this->set( 'date_' . $date_key, date( 'Y-m-d H:i:s', $date_val ) );\n\n\t\tswitch ( $date_key ) {\n\n\t\t\t// Reschedule access expiration.\n\t\t\tcase 'access_expires':\n\t\t\t\t$this->maybe_schedule_expiration();\n\t\t\t\tbreak;\n\n\t\t\t// Additionally update the next payment date & don't break because we want to reschedule payments too.\n\t\t\tcase 'trial_end':\n\t\t\t\t$this->set_date( 'next_payment', $this->calculate_next_payment_date( 'U' ) );\n\n\t\t\t\t// Everything else reschedule's payments.\n\t\t\tdefault:\n\t\t\t\t$this->maybe_schedule_payment( false );\n\n\t\t}\n\t}\n\n\t/**\n\t * Update the status of an order\n\t *\n\t * @since 3.8.0\n\t * @since 3.10.0 Unknown.\n\t * @since 5.2.0 Prefer `array_key_exists( $key, $keys )` over `in_array( $key, array_keys( $assoc_array ) )`.\n\t *\n\t * @param string $status Status name, accepts unprefixed statuses.\n\t * @return void\n\t */\n\tpublic function set_status( $status ) {\n\n\t\tif ( false === strpos( $status, 'llms-' ) ) {\n\t\t\t$status = 'llms-' . $status;\n\t\t}\n\n\t\tif ( array_key_exists( $status, llms_get_order_statuses( $this->get( 'order_type' ) ) ) ) {\n\t\t\t$this->set( 'status', $status );\n\t\t}\n\t}\n\n\t/**\n\t * Sets user-related metadata for the order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array|LLMS_Student|WP_User|integer $user_or_data Accepts a raw array user meta-data or\n\t *                                                         an input string accepted by `llms_get_student()`.\n\t *                                                         When passing an existing user the data will be pulled\n\t *                                                         from the user metadata and saved to the order.\n\t * @return array {\n\t *     Returns an associative array representing the user metadata that was stored on the order.\n\t *\n\t *     @type integer $user_id            User's WP_User id.\n\t *     @type string  $user_ip_address    User's ip address.\n\t *     @type string  $billing_email      User's email.\n\t *     @type string  $billing_first_name User's first name.\n\t *     @type string  $billing_last_name  User's last name.\n\t *     @type string  $billing_address_1  User's address line 1.\n\t *     @type string  $billing_address_2  User's address line 2.\n\t *     @type string  $billing_city       User's city.\n\t *     @type string  $billing_state      User's state.\n\t *     @type string  $billing_zip        User's zip.\n\t *     @type string  $billing_country    User's country.\n\t *     @type string  $billing_phone      User's phone.\n\t * }\n\t */\n\tpublic function set_user_data( $user_or_data ) {\n\n\t\t$to_set = array(\n\t\t\t'user_id'            => '',\n\t\t\t'billing_email'      => '',\n\t\t\t'billing_first_name' => '',\n\t\t\t'billing_last_name'  => '',\n\t\t\t'billing_address_1'  => '',\n\t\t\t'billing_address_2'  => '',\n\t\t\t'billing_city'       => '',\n\t\t\t'billing_state'      => '',\n\t\t\t'billing_zip'        => '',\n\t\t\t'billing_country'    => '',\n\t\t\t'billing_phone'      => '',\n\t\t);\n\n\t\t$user = ! is_array( $user_or_data ) ? llms_get_student( $user_or_data ) : false;\n\t\tif ( $user ) {\n\n\t\t\t$user_or_data = array();\n\n\t\t\t$map = array(\n\t\t\t\t'user_id'            => 'id',\n\t\t\t\t'billing_email'      => 'user_email',\n\t\t\t\t'billing_phone'      => 'phone',\n\t\t\t\t'billing_first_name' => 'first_name',\n\t\t\t\t'billing_last_name'  => 'last_name',\n\t\t\t);\n\n\t\t\tforeach ( array_keys( $to_set ) as $order_key ) {\n\t\t\t\t$to_set[ $order_key ] = $user->get( $map[ $order_key ] ?? $order_key );\n\t\t\t}\n\t\t}\n\n\t\t// Only use the default IP address if it wasn't specified in the input array.\n\t\t$to_set['user_ip_address'] = $user_or_data['user_ip_address'] ?? llms_get_ip_address();\n\n\t\t// Merge the data and remove excess keys.\n\t\t$to_set = array_intersect_key(\n\t\t\tarray_merge( $to_set, $user_or_data ),\n\t\t\t$to_set\n\t\t);\n\n\t\t$this->set_bulk( $to_set );\n\t\treturn $to_set;\n\t}\n\n\t/**\n\t * Record the start date of the access plan and schedule expiration if expiration is required in the future\n\t *\n\t * @since 3.0.0\n\t * @since 3.19.0 Unknown.\n\t * @since 5.2.0 Use strict type comparision.\n\t *\n\t * @return void\n\t */\n\tpublic function start_access() {\n\n\t\t// Only start access if access isn't already started.\n\t\t$date = $this->get( 'start_date' );\n\t\tif ( ! $date ) {\n\n\t\t\t// Set the start date to now.\n\t\t\t$date = llms_current_time( 'mysql' );\n\t\t\t$this->set( 'start_date', $date );\n\n\t\t}\n\n\t\t$this->unschedule_expiration();\n\n\t\t// Setup expiration.\n\t\tif ( in_array( $this->get( 'access_expiration' ), array( 'limited-date', 'limited-period' ), true ) ) {\n\n\t\t\t$expires_date = $this->get_access_expiration_date( 'Y-m-d H:i:s' );\n\t\t\t$this->set( 'date_access_expires', $expires_date );\n\t\t\t$this->maybe_schedule_expiration();\n\n\t\t}\n\t}\n\n\t/**\n\t * Cancels a scheduled expiration action\n\t *\n\t * Does nothing if no expiration is scheduled\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 4.6.0 Use `$this->get_next_scheduled_action_time()` to determine if the action is currently scheduled.\n\t *\n\t * @return void\n\t */\n\tpublic function unschedule_expiration() {\n\n\t\tif ( $this->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) ) {\n\t\t\tas_unschedule_action( 'llms_access_plan_expiration', $this->get_action_args() );\n\t\t}\n\t}\n\n\t/**\n\t * Cancels a scheduled recurring payment action\n\t *\n\t * Does nothing if no payments are scheduled\n\t *\n\t * @since 3.0.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 4.6.0 Use `$this->get_next_scheduled_action_time()` to determine if the action is currently scheduled.\n\t *\n\t * @return void\n\t */\n\tpublic function unschedule_recurring_payment() {\n\n\t\tif ( $this->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) ) {\n\n\t\t\t$action_args = $this->get_action_args();\n\n\t\t\tas_unschedule_action( 'llms_charge_recurring_payment', $action_args );\n\n\t\t\t/**\n\t\t\t * Fired after a recurring payment is unscheduled\n\t\t\t *\n\t\t\t * @since 5.2.0\n\t\t\t *\n\t\t\t * @param LLMS_Order $order       LLMS_Order instance.\n\t\t\t * @param int        $date        Timestamp of the recurring payment date UTC.\n\t\t\t * @param array      $action_args Arguments passed to the scheduler.\n\t\t\t */\n\t\t\tdo_action( 'llms_charge_recurring_payment_unscheduled', $this, $action_args );\n\n\t\t}\n\t}\n\n\t/**\n\t * Schedule recurring payment\n\t *\n\t * It will unschedule the next recurring payment action, if any, before scheduling.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string  $next_payment_date Optional. Next payment date. If not provided it'll be retrieved using `$this->get_next_payment_due_date()`.\n\t * @param boolean $gmt               Optional. Whether the provided `$next_payment_date` date is gmt. Default is `false`.\n\t *                                   Only applies when the `$next_payment_date` is provided.\n\t * @return WP_Error|integer WP_Error if the plan ended. Otherwise returns the return value of `as_schedule_single_action`: the action's ID.\n\t */\n\tpublic function schedule_recurring_payment( $next_payment_date = false, $gmt = false ) {\n\n\t\t// Unschedule the next action (does nothing if no action scheduled).\n\t\t$this->unschedule_recurring_payment();\n\n\t\t$date = $this->get_recurring_payment_due_date_for_scheduler( $next_payment_date, $gmt );\n\n\t\tif ( is_wp_error( $date ) ) {\n\t\t\treturn $date;\n\t\t}\n\n\t\t$action_args = $this->get_action_args();\n\n\t\t// Schedule the payment.\n\t\t$action_id = as_schedule_single_action(\n\t\t\t$date,\n\t\t\t'llms_charge_recurring_payment',\n\t\t\t$action_args\n\t\t);\n\n\t\t/**\n\t\t * Fired after a recurring payment is scheduled\n\t\t *\n\t\t * @since 5.2.0\n\t\t *\n\t\t * @param LLMS_Order $order       LLMS_Order instance.\n\t\t * @param integer    $date        Timestamp of the recurring payment date UTC.\n\t\t * @param array      $action_args Arguments passed to the scheduler.\n\t\t * @param integer    $action_id   Scheduled action ID.\n\t\t */\n\t\tdo_action( 'llms_charge_recurring_payment_scheduled', $this, $date, $action_args, $action_id );\n\n\t\treturn $action_id;\n\t}\n\n\t/**\n\t * Returns the recurring payment due date in a suitable format for the scheduler.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string  $next_payment_date Optional. Next payment date. If not provided it'll be retrieved using `$this->get_next_payment_due_date()`.\n\t * @param boolean $gmt               Optional. Whether the provided `$next_payment_date` date is gmt. Default is `false`.\n\t *                                   Only applies when the `$next_payment_date` is provided.\n\t * @return WP_Error|integer\n\t */\n\tpublic function get_recurring_payment_due_date_for_scheduler( $next_payment_date = false, $gmt = false ) {\n\n\t\t$date = false === $next_payment_date ? $this->get_next_payment_due_date() : $next_payment_date;\n\n\t\tif ( ! $date ) {\n\t\t\treturn new WP_Error( 'invalid-recurring-payment-date', __( 'Next recurring payment due date is not valid', 'lifterlms' ) );\n\t\t}\n\t\tif ( is_wp_error( $date ) ) {\n\t\t\treturn $date;\n\t\t}\n\n\t\t// Convert our date to Unix time and UTC before passing to the scheduler.\n\t\t// No date parameter passed, or passed date parameter was not in gmt.\n\t\tif ( ! $next_payment_date || ( $next_payment_date && ! $gmt ) ) {\n\t\t\t$date = get_gmt_from_date( $date, 'U' );\n\t\t} else {\n\t\t\t// Get timestamp.\n\t\t\t$date = date_format( date_create( $date ), 'U' );\n\t\t}\n\n\t\treturn (int) $date;\n\t}\n\n\t/**\n\t * Determine whether the recurring payment for this order can be modified.\n\t *\n\t * Depends on whether the order's gateway supports.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return bool\n\t */\n\tpublic function supports_modify_recurring_payments() {\n\t\t$gateway = $this->get_gateway();\n\t\treturn is_wp_error( $gateway ) ? false : $gateway->supports( 'modify_recurring_payments', $this );\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.post.instructors.php",
    "content": "<?php\n/**\n * LLMS Post Instructors\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.13.0\n * @version 4.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Post_Instructors class\n *\n * Allow interactions with the custom multi-author functionality\n * currently enabled for Courses and Memberships only.\n *\n * Rather than instantiating this class directly\n * you should use LLMS_Course->instructors() or LLMS_Membership()->instructors()\n *\n * @since 3.13.0\n * @since 3.30.3 Explicitly define class properties.\n * @since 4.0.0  Remove deprecated method `get_defaults()`.\n * @since 4.2.0 Normalized return structure in `get_instructors()` when no instructor set.\n */\nclass LLMS_Post_Instructors {\n\n\t/**\n\t * WP Post ID\n\t *\n\t * @var int\n\t */\n\tpublic $id;\n\n\t/**\n\t * Instance of the post (course or membership)\n\t *\n\t * @var LLMS_Post_Model\n\t */\n\tpublic $post;\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param LLMS_Post_Model|WP_Post|int $post Post object or ID.\n\t */\n\tpublic function __construct( $post ) {\n\n\t\t// Setup a post if post id of WP_Post is passed in.\n\t\tif ( is_numeric( $post ) || is_a( $post, 'WP_Post' ) ) {\n\t\t\t$post = llms_get_post( $post );\n\t\t}\n\n\t\t// Double check we have an LLMS_Post.\n\t\tif ( is_subclass_of( $post, 'LLMS_Post_Model' ) ) {\n\t\t\t$this->post = $post;\n\t\t\t$this->id   = $post->get( 'id' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieve course instructor information\n\t *\n\t * @since 3.13.0\n\t * @since 3.23.0 Unknown.\n\t * @since 4.2.0 Normalize return data when no instructor data is saved.\n\t *\n\t * @param boolean $exclude_hidden If true, excludes hidden instructors from the return array.\n\t * @return array[] {\n\t *     Array or instructor data arrays.\n\t *\n\t *     @type int    $id         WP_User ID of the instructor user.\n\t *     @type string $visibility Display visibility option for the instructor.\n\t *     @type string $label      User input display noun for the instructor. EG: \"Author\" or \"Coach\" or \"Instructor\".\n\t *     @type string $name       WP_User Display Name.\n\t * }\n\t */\n\tpublic function get_instructors( $exclude_hidden = false ) {\n\n\t\t$instructors = $this->post->get( 'instructors' );\n\n\t\t// If empty, respond with the course author in an array.\n\t\tif ( ! $instructors ) {\n\t\t\t$author_id   = $this->post->get( 'author' );\n\t\t\t$author      = get_userdata( $author_id );\n\t\t\t$instructors = array(\n\t\t\t\twp_parse_args(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'id'   => $author_id,\n\t\t\t\t\t\t'name' => $author ? $author->display_name : '',\n\t\t\t\t\t),\n\t\t\t\t\tllms_get_instructors_defaults()\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tif ( $exclude_hidden ) {\n\t\t\tforeach ( $instructors as $key => $instructor ) {\n\t\t\t\tif ( 'hidden' === $instructor['visibility'] ) {\n\t\t\t\t\tunset( $instructors[ $key ] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $instructors;\n\n\t}\n\n\t/**\n\t * Format an instructors array for saving to the db.\n\t *\n\t * @since 3.25.0\n\t *\n\t * @param array $instructors Array of full (or partial) instructor data.\n\t * @return array\n\t */\n\tpublic function pre_set_instructors( $instructors = array() ) {\n\n\t\t/**\n\t\t * We cannot allow no instructors to exist\n\t\t * so we'll revert to the default `post_author`.\n\t\t */\n\t\tif ( ! $instructors ) {\n\t\t\t// Clear so the getter will retrieve the default author.\n\t\t\t$this->post->set( 'instructors', array() );\n\t\t\t$instructors = $this->get_instructors();\n\t\t}\n\n\t\t// Allow partial arrays to be passed & we'll fill em up with defaults.\n\t\tforeach ( $instructors as $i => &$instructor ) {\n\n\t\t\t$instructor       = wp_parse_args( $instructor, llms_get_instructors_defaults() );\n\t\t\t$instructor['id'] = absint( $instructor['id'] );\n\n\t\t\t// Remove instructors without an ID.\n\t\t\tif ( empty( $instructor['id'] ) ) {\n\t\t\t\tunset( $instructors[ $i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn array_values( $instructors );\n\n\t}\n\n\t/**\n\t * Save instructor information\n\t *\n\t * @since 3.13.0\n\t * @since 3.25.0 Unknown.\n\t *\n\t * @param array $instructors Array of course instructor information.\n\t */\n\tpublic function set_instructors( $instructors = array() ) {\n\n\t\t$instructors = $this->pre_set_instructors( $instructors );\n\n\t\t// Set the post_author to be the first author in the array.\n\t\t$this->post->set( 'author', $instructors[0]['id'] );\n\n\t\t// Save the instructors array.\n\t\t$this->post->set( 'instructors', $instructors );\n\n\t\treturn $instructors;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.product.php",
    "content": "<?php\n/**\n * LifterLMS Product Model.\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 1.0.0\n * @version 6.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Product model class.\n *\n * Both Courses and Memberships are sellable and can be instantiated as a product.\n *\n * @since 1.0.0\n * @since 3.25.2 Unknown.\n * @since 3.37.17 Fixed a typo in the `post_status` query arg when retrieving access plans for this product.\n *                Use `in_array` with strict comparison where possible.\n * @since 3.38.0 Add `get_restrictions()` and `has_restrictions()` methods.\n */\nclass LLMS_Product extends LLMS_Post_Model {\n\n\t/**\n\t * Model properties.\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array();\n\n\t/**\n\t * Model DB Post Type.\n\t *\n\t * @todo The post type depends conditionally on whether it's a course or a membership so this is semantically incorrect.\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'product';\n\n\t/**\n\t * Model type.\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'product';\n\n\t/**\n\t * Retrieve the max number of access plans that can be created for this product.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return int\n\t */\n\tpublic function get_access_plan_limit() {\n\n\t\t/**\n\t\t * Determine the number of access plans allowed on the product.\n\t\t *\n\t\t * This is a (somewhat) arbitrary limit chosen mostly based on the following 2 factors:\n\t\t *\n\t\t * 1) It looks visually unappealing to have a pricing table with 7+ items on it.\n\t\t * 2) Having 7+ pricing plans creates a lot of decision fatigue for users.\n\t\t *\n\t\t * If you disagree with either of these two factors you can quite easily change the\n\t\t * limit using this filter.\n\t\t *\n\t\t * Keep in mind that increasing the limit will likely require you to add CSS to accommodate\n\t\t * 7+ plans on the automatically generated pricing tables.\n\t\t *\n\t\t * Also, since plans are limited by the core to 6, we have no pagination built in for any queries that\n\t\t * lookup or list access plans. This means that if you greatly increase the limit (say 200) you\n\t\t * could very quickly run into issues where the default queries \"do not scale\" well. In which\n\t\t * case you should first consider if you really need 200 plans and then start investigating other\n\t\t * filters to add pagination (and probably caching) to these (now slow) queries.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param int          $limit Number of plans.\n\t\t * @param LLMS_Proudct $this  Product object.\n\t\t */\n\t\treturn apply_filters( 'llms_get_product_access_plan_limit', 6, $this );\n\t}\n\n\t/**\n\t * Get all access plans for the product.\n\t *\n\t * @since 3.0.0\n\t * @since 3.25.2 Unknown.\n\t * @since 3.37.17 Fixed a typo in the `post_status` query arg when retrieving access plans for this product.\n\t *\n\t * @param bool $free_only    Optional. Only include free access plans if `true`. Defalt `false`\n\t * @param bool $visible_only Optional. Excludes hidden access plans from results. Default `true`.\n\t * @return array\n\t */\n\tpublic function get_access_plans( $free_only = false, $visible_only = true ) {\n\n\t\t$args = array(\n\t\t\t'meta_key'       => '_llms_product_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key\n\t\t\t'meta_value'     => $this->get( 'id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value\n\t\t\t'order'          => 'ASC',\n\t\t\t'orderby'        => 'menu_order',\n\t\t\t'posts_per_page' => $this->get_access_plan_limit(),\n\t\t\t'post_type'      => 'llms_access_plan',\n\t\t\t'post_status'    => 'publish',\n\t\t);\n\n\t\t// Filter results to only free access plans.\n\t\tif ( $free_only ) {\n\t\t\t$args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query\n\t\t\t\tarray(\n\t\t\t\t\t'key'   => '_llms_is_free',\n\t\t\t\t\t'value' => 'yes',\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Exclude hidden access plans from the results.\n\t\tif ( $visible_only ) {\n\t\t\t$args['tax_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query\n\t\t\t\tarray(\n\t\t\t\t\t'field'    => 'name',\n\t\t\t\t\t'operator' => 'NOT IN',\n\t\t\t\t\t'terms'    => array( 'hidden' ),\n\t\t\t\t\t'taxonomy' => 'llms_access_plan_visibility',\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t/**\n\t\t * Filter the product's access plan query args.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array        $args         Query args.\n\t\t * @param LLMS_Product $product      The LLMS_Product instance.\n\t\t * @param bool         $free_only    Whether or not to include the free access plans only.\n\t\t * @param bool         $visbile_only Whether or not to exclude the hidden access plans.\n\t\t */\n\t\t$query = new WP_Query( apply_filters( 'llms_get_product_access_plans_args', $args, $this, $free_only, $visible_only ) );\n\n\t\t$plans = array();\n\n\t\t// If we have plans, setup access plan instances.\n\t\tif ( $query->have_posts() ) {\n\t\t\tforeach ( $query->posts as $post ) {\n\t\t\t\t$plans[] = new LLMS_Access_Plan( $post );\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter the product's access plans.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array        $plans        An array of LLMS_Access_Plan instances related to the product `$product`.\n\t\t * @param LLMS_Product $product      The LLMS_Product instance.\n\t\t * @param bool         $free_only    Whether or not to include the free access plans only.\n\t\t * @param bool         $visbile_only Whether or not to exclude the hidden access plans.\n\t\t */\n\t\treturn apply_filters( 'llms_get_product_access_plans', $plans, $this, $free_only, $visible_only );\n\n\t}\n\n\t/**\n\t * Retrieve the product's catalog visibility term.\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_catalog_visibility() {\n\n\t\t$terms = wp_get_post_terms( $this->get( 'id' ), 'llms_product_visibility' );\n\n\t\tif ( $terms && is_array( $terms ) ) {\n\t\t\t$obj = $terms[0];\n\t\t\tif ( isset( $obj->name ) ) {\n\t\t\t\treturn $obj->name;\n\t\t\t}\n\t\t}\n\n\t\treturn 'catalog_search';\n\t}\n\n\t/**\n\t * Retrieve the product's catalog visibility name for display.\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_catalog_visibility_name() {\n\n\t\t$visibility = $this->get_catalog_visibility();\n\t\t$options    = llms_get_product_visibility_options();\n\t\tif ( isset( $options[ $visibility ] ) ) {\n\t\t\treturn $options[ $visibility ];\n\t\t}\n\t\treturn $visibility;\n\n\t}\n\n\n\t/**\n\t * Get the number of columns for the pricing table.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param bool $free_only Optional. Only include free access plans if true. Default `false`.\n\t * @return int\n\t */\n\tpublic function get_pricing_table_columns_count( $free_only = false ) {\n\n\t\t$count = count( $this->get_access_plans( $free_only ) );\n\n\t\tswitch ( $count ) {\n\n\t\t\tcase 0:\n\t\t\t\t$cols = 1;\n\t\t\t\tbreak;\n\n\t\t\tcase 6:\n\t\t\t\t$cols = 3;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$cols = $count;\n\t\t}\n\n\t\t/**\n\t\t * Filter the number of columns of the product's pricing table.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param int          $cols      The number of columns of the pricing table for the `$product`.\n\t\t * @param LLMS_Product $product   The LLMS_Product instance.\n\t\t * @param int          $count     The number of access plans related to the product `$product`.\n\t\t * @param bool         $free_only Whether or not to include the free access plans only.\n\t\t */\n\t\treturn apply_filters( 'llms_get_product_pricing_table_columns_count', $cols, $this, $count, $free_only );\n\t}\n\n\t/**\n\t * Retrieve a list of restrictions on the product.\n\t *\n\t * Restrictions are used to in conjunction with \"is_purchasable()\" to\n\t * determine if purchase/enrollment should be allowed for a given product.\n\t *\n\t * Restrictions in the core currently only exist on courses:\n\t * + Enrollment time period.\n\t * + Student capacity.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return string[] An array of strings describing the restrictions placed on the product.\n\t */\n\tpublic function get_restrictions() {\n\n\t\t$restrictions = array();\n\n\t\tif ( 'course' === $this->get( 'type' ) ) {\n\n\t\t\t$course = new LLMS_Course( $this->get( 'id' ) );\n\n\t\t\t// Is the course enrollment period open?\n\t\t\tif ( ! $course->is_enrollment_open() ) {\n\t\t\t\t$restrictions[] = 'enrollment_period';\n\t\t\t}\n\n\t\t\t// Does the course have capacity?\n\t\t\tif ( ! $course->has_capacity() ) {\n\t\t\t\t$restrictions[] = 'student_capacity';\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter the product's restrictions.\n\t\t *\n\t\t * @since 6.8.0\n\t\t *\n\t\t * @param string[]     $restrictions An array of strings describing the restrictions placed on the product.\n\t\t * @param LLMS_Product $product      The LLMS_Product object.\n\t\t */\n\t\treturn apply_filters( 'llms_product_get_restrictions', $restrictions, $this );\n\n\t}\n\n\n\t/**\n\t * Determine if the product has at least one free access plan.\n\t *\n\t * @since 3.0.0\n\t * @since 3.25.2 Unknown.\n\t *\n\t * @return bool\n\t */\n\tpublic function has_free_access_plan() {\n\n\t\t/**\n\t\t * Filter whether the product has free access plans.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 3.37.17 Added the `$product` param.\n\t\t *\n\t\t * @param bool         $has_free_access_plan Whether the product `$product` has free access plans.\n\t\t * @param LLMS_Product $product              The LLMS_Product instance.\n\t\t */\n\t\treturn apply_filters( 'llms_product_has_free_access_plan', ( 0 !== count( $this->get_access_plans( true ) ) ), $this );\n\n\t}\n\n\t/**\n\t * Determine if any restrictions exist on the product.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @see LLMS_Proudct::get_restrictions()\n\t *\n\t * @return boolean `true` if there is at least one restriction on the product, `false` otherwise.\n\t */\n\tpublic function has_restrictions() {\n\n\t\t$restrictions     = $this->get_restrictions();\n\t\t$has_restrictions = count( $restrictions ) > 0;\n\n\t\t/**\n\t\t * Filter whether the product has any purchase restrictions.\n\t\t *\n\t\t * @since 3.38.0\n\t\t *\n\t\t * @param bool         $has_restrictions Whether the product `$product` has restrictions.\n\t\t * @param string[]     $restrictions     Array of restrictions placed on the product.\n\t\t * @param LLMS_Product $product          The LLMS_Product object.\n\t\t */\n\t\treturn apply_filters( 'llms_product_has_restrictions', $has_restrictions, $restrictions, $this );\n\n\t}\n\n\t/**\n\t * Determine if the product is purchasable.\n\t *\n\t * At least one gateway must be enabled and at least one access plan must exist.\n\t * If the product is a course, additionally checks to ensure course enrollment is open and has capacity.\n\t *\n\t * @since 3.0.0\n\t * @since 3.25.2 Unknown.\n\t * @since 3.38.0 Use `has_restrictions()` to determine if the product has additional restrictions.\n\t *\n\t * @return bool\n\t */\n\tpublic function is_purchasable() {\n\n\t\t// Default to false.\n\t\t$purchasable = false;\n\n\t\t// If the product doesn't have any purchase restrictions, make sure we have a purchasable plan & active gateways.\n\t\tif ( ! $this->has_restrictions() ) {\n\t\t\t$gateways    = llms()->payment_gateways();\n\t\t\t$purchasable = ( $this->get_access_plans( false, false ) && $gateways->has_gateways( true ) );\n\t\t}\n\n\t\t/**\n\t\t * Filter whether the product is purchasable.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param bool         $purchasable Whether the product `$product` is purchasable.\n\t\t * @param LLMS_Product $product     The LLMS_Product instance.\n\t\t */\n\t\treturn apply_filters( 'llms_product_is_purchasable', $purchasable, $this );\n\n\t}\n\n\t/**\n\t * Update the product's catalog visibility setting.\n\t *\n\t * @since 3.6.0\n\t * @since 3.37.17 Use `in_array` with strict comparison.\n\t *\n\t * @param string $visibility Visibility term name.\n\t * @return void\n\t */\n\tpublic function set_catalog_visibility( $visibility ) {\n\t\tif ( ! in_array( $visibility, array_keys( llms_get_product_visibility_options() ), true ) ) {\n\t\t\treturn;\n\t\t}\n\t\twp_set_object_terms( $this->get( 'id' ), $visibility, 'llms_product_visibility', false );\n\t}\n\n\t/**\n\t * Check if there are active subscriptions for this product.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @param boolean $use_cache Whether or not leveraging the cache.\n\t * @return boolean\n\t */\n\tpublic function has_active_subscriptions( $use_cache = true ) {\n\n\t\t$found = false;\n\t\tif ( $use_cache ) {\n\t\t\t$subscriptions_count = wp_cache_get( $this->get( 'id' ), 'llms_product_subscriptions_count', true, $found );\n\t\t}\n\n\t\tif ( false === $found ) {\n\n\t\t\tglobal $wpdb;\n\n\t\t\t$subscriptions_count = $wpdb->get_var(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"\n\t\t\t\t\tSELECT COUNT(*) FROM {$wpdb->posts} as p\n\t\t\t\t\tJOIN {$wpdb->postmeta} as pm1\n\t\t\t\t\tJOIN {$wpdb->postmeta} as pm2\n\t\t\t\t\tWHERE p.ID=pm1.post_id\n\t\t\t\t\tAND p.post_type='llms_order'\n\t\t\t\t\tAND pm1.post_id=pm2.post_id\n\t\t\t\t\tAND pm1.meta_key='_llms_product_id' AND pm1.meta_value=%d\n\t\t\t\t\tAND pm2.meta_key='_llms_order_type' AND pm2.meta_value='recurring'\n\t\t\t\t\tAND p.post_status IN ( 'llms-active', 'llms-pending-cancel', 'llms-on-hold' )\n\t\t\t\t\t\",\n\t\t\t\t\t$this->get( 'id' )\n\t\t\t\t)\n\t\t\t);\n\n\t\t\twp_cache_set(\n\t\t\t\t$this->get( 'id' ),\n\t\t\t\t$subscriptions_count,\n\t\t\t\t'llms_product_subscriptions_count'\n\t\t\t);\n\n\t\t}\n\n\t\treturn (bool) $subscriptions_count;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.question.choice.php",
    "content": "<?php\n/**\n * LifterLMS Quiz Question Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.16.0\n * @version 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Question_Choice model class\n *\n * @since 3.16.0\n */\nclass LLMS_Question_Choice {\n\n\tprotected $prefix = '_llms_choice_';\n\n\tprivate $id = null;\n\n\tprivate $data = array();\n\n\tprivate $question    = null;\n\tprivate $question_id = null;\n\n\t/**\n\t * Constructor\n\t *\n\t * @param    int          $question_id  WP Post ID of the choice's parent LLMS_Question\n\t * @param    array|string $data_or_id   array of choice data or the choice ID string\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function __construct( $question_id, $data_or_id = array() ) {\n\n\t\t// Ensure the question is valid.\n\t\tif ( $this->set_question( $question_id ) ) {\n\n\t\t\t// If an ID is passed in, load the question data from post meta.\n\t\t\tif ( ! is_array( $data_or_id ) ) {\n\t\t\t\t$data_or_id = str_replace( $this->prefix, '', $data_or_id );\n\t\t\t\t$data_or_id = get_post_meta( $this->question_id, $this->prefix . $data_or_id, true );\n\t\t\t}\n\n\t\t\t// Hydrate with postmeta data or array of data passed in.\n\t\t\tif ( is_array( $data_or_id ) && isset( $data_or_id['id'] ) ) {\n\t\t\t\t$this->hydrate( $data_or_id );\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Creates a new question\n\t *\n\t * @param    array $data  question data array\n\t * @return   self\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function create( $data ) {\n\n\t\t$this->id = uniqid();\n\t\treturn $this->update( $data )->save();\n\t}\n\n\t/**\n\t * Delete a choice\n\t *\n\t * @return   boolean\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function delete() {\n\t\treturn delete_post_meta( $this->question_id, $this->prefix . $this->id );\n\t}\n\n\t/**\n\t * Determine if the choice that's been requested actually exists\n\t *\n\t * @return   boolean\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function exists() {\n\t\treturn ( $this->id );\n\t}\n\n\t/**\n\t * Retrieve a piece of choice data by key\n\t *\n\t * @param    string $key      name of the data to be retrieved\n\t * @param    mixed  $default  default value if key isn't set\n\t * @return   mixed\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function get( $key, $default = '' ) {\n\n\t\tif ( isset( $this->data[ $key ] ) ) {\n\t\t\treturn $this->data[ $key ];\n\t\t}\n\n\t\treturn $default;\n\t}\n\n\t/**\n\t * Generic choice getter which automatically uses correct functions based on choice type\n\t *\n\t * @return   string\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function get_choice() {\n\t\tif ( 'image' === $this->get( 'choice_type' ) ) {\n\t\t\treturn $this->get_image();\n\t\t}\n\t\treturn $this->get( 'choice' );\n\t}\n\n\t/**\n\t * Retrieve an image for picture choices\n\t *\n\t * @return   [type]\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function get_image() {\n\t\tif ( 'image' !== $this->get( 'choice_type' ) ) {\n\t\t\treturn '';\n\t\t}\n\t\t$img = $this->get( 'choice' );\n\t\tif ( is_array( $img ) && isset( $img['id'] ) ) {\n\t\t\treturn wp_get_attachment_image( $img['id'], 'full' );\n\t\t}\n\t\treturn '';\n\t}\n\n\t/**\n\t * Retrieve all of the choice data as an array\n\t *\n\t * @return   array\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function get_data() {\n\t\treturn $this->data;\n\t}\n\n\t/**\n\t * Retrieve an instance of an LLMS_Question for questions parent\n\t *\n\t * @return   obj\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function get_question() {\n\t\treturn $this->question;\n\t}\n\n\t/**\n\t * Retrieve the question ID for the given choice\n\t *\n\t * @return   int\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function get_question_id() {\n\t\treturn $this->question_id;\n\t}\n\n\t/**\n\t * Setup the id and data variables\n\t *\n\t * @param    array $data  array of question data\n\t * @return   void\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tprivate function hydrate( $data ) {\n\t\t$this->id         = $data['id'];\n\t\t$this->data['id'] = $this->id;\n\t\t$this->update( $data );\n\t}\n\n\t/**\n\t * Determine if the choice is correct\n\t *\n\t * @return   bool\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function is_correct() {\n\t\t/**\n\t\t * Filter to modify if this question choice is correct, ie. reorder questions in Advanced Quizzes.\n\t\t *\n\t\t * @since 9.1.0\n\t\t *\n\t\t * @param bool Whether this question choice is correct or not.\n\t\t * @param LLMS_Question_Choice\n\t\t */\n\t\treturn apply_filters( 'llms_question_choice_is_correct', filter_var( $this->get( 'correct' ), FILTER_VALIDATE_BOOLEAN ), $this );\n\t}\n\n\t/**\n\t * Save $this->data to the postmeta table\n\t *\n\t * @return   void\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function save() {\n\n\t\t$this->data['id'] = $this->id; // Always ensure the ID is set when saving data.\n\t\t$update           = update_post_meta( $this->question_id, $this->prefix . $this->id, $this->data );\n\n\t\treturn ( $update );\n\t}\n\n\t/**\n\t * Set a piece of data by key.\n\t *\n\t * @since 3.16.0\n\t * @since 7.4.1 Check `$type['choices']` is an array before trying to access it as such.\n\t *\n\t * @param string $key Name of the key to set.\n\t * @param mixed  $val Value to set.\n\t * @return self\n\t */\n\tpublic function set( $key, $val ) {\n\n\t\t// Don't set the ID.\n\t\tif ( 'id' === $key ) {\n\t\t\treturn $this;\n\t\t}\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'choice_type':\n\t\t\t\tif ( ! in_array( $val, array( 'text', 'image' ) ) ) {\n\t\t\t\t\t$val = 'text';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'correct':\n\t\t\t\t$val = filter_var( $val, FILTER_VALIDATE_BOOLEAN );\n\t\t\t\tbreak;\n\n\t\t\tcase 'marker':\n\t\t\t\t$type = $this->get_question()->get_question_type();\n\t\t\t\tif ( is_array( $type['choices'] ?? false ) ) {\n\t\t\t\t\t$markers = $type['choices']['markers'];\n\t\t\t\t\tif ( ! in_array( $val, $markers ) ) {\n\t\t\t\t\t\t$val = $markers[0];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'choice':\n\t\t\tdefault:\n\t\t\t\tif ( is_array( $val ) ) {\n\t\t\t\t\t$val = array_map( 'sanitize_text_field', $val );\n\t\t\t\t} else {\n\t\t\t\t\t$val = wp_kses_post( $val );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t$this->data[ $key ] = $val;\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Sets question-related data from constructor\n\t *\n\t * @param    int $id  WP Post ID of the question's parent question\n\t * @return   boolean\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function set_question( $id ) {\n\t\t$question = llms_get_post( $id );\n\t\tif ( $question && is_a( $question, 'LLMS_Question' ) ) {\n\t\t\t$this->question    = $question;\n\t\t\t$this->question_id = $id;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Update multiple data by key=>val pairs\n\t *\n\t * @param    array $data  array of data to set\n\t * @return   self\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function update( $data = array() ) {\n\n\t\tforeach ( $data as $key => $val ) {\n\t\t\t$this->set( $key, $val );\n\t\t}\n\t\treturn $this;\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.question.php",
    "content": "<?php\n/**\n * LifterLMS Quiz Question Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 1.0.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS Quiz Question Model class\n *\n * @property string $question_type Type of question.\n *\n * @since 1.0.0\n * @since 3.30.1 Fixed choice sorting issues.\n * @since 3.35.0 Escape `LIKE` clause when retrieving choices.\n * @since 3.38.2 When getting the 'not raw' question_type, made sure to always return a valid value.\n * @since 4.0.0 Remove deprecated class methods.\n */\nclass LLMS_Question extends LLMS_Post_Model {\n\n\t/**\n\t * Database post type name\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_question';\n\n\t/**\n\t * Modefl post type name\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'question';\n\n\t/**\n\t * Map of Model properties to property type\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\t\t'content'                => 'html',\n\t\t'clarifications'         => 'html',\n\t\t'clarifications_enabled' => 'yesno',\n\t\t'description_enabled'    => 'yesno',\n\t\t'image'                  => 'array',\n\t\t'multi_choices'          => 'yesno',\n\t\t'parent_id'              => 'absint',\n\t\t'points'                 => 'absint',\n\t\t'question_type'          => 'string',\n\t\t'question'               => 'html',\n\t\t'title'                  => 'html',\n\t\t'video_enabled'          => 'yesno',\n\t\t'video_src'              => 'string',\n\t);\n\n\t/**\n\t * Create a new question choice\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array $data Array of question choice data.\n\t * @return string|boolean\n\t */\n\tpublic function create_choice( $data ) {\n\n\t\t$data = wp_parse_args(\n\t\t\t$data,\n\t\t\tarray(\n\t\t\t\t'choice'      => '',\n\t\t\t\t'choice_type' => 'text',\n\t\t\t\t'correct'     => false,\n\t\t\t\t'marker'      => $this->get_next_choice_marker(),\n\t\t\t\t'question_id' => $this->get( 'id' ),\n\t\t\t)\n\t\t);\n\n\t\t$choice = new LLMS_Question_Choice( $this->get( 'id' ) );\n\t\tif ( $choice->create( $data ) ) {\n\t\t\treturn $choice->get( 'id' );\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Delete a choice by ID\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $id Choice ID.\n\t * @return boolean\n\t */\n\tpublic function delete_choice( $id ) {\n\n\t\t$choice = $this->get_choice( $id );\n\t\tif ( ! $choice ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn $choice->delete();\n\t}\n\n\t/**\n\t * Retrieve the type of automatic grading that can be performed on the question\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return string|false\n\t */\n\tpublic function get_auto_grade_type() {\n\n\t\tif ( $this->supports( 'choices' ) && $this->supports( 'grading', 'auto' ) ) {\n\t\t\treturn 'choices';\n\t\t} elseif ( $this->supports( 'grading', 'conditional' ) && llms_parse_bool( $this->get( 'auto_grade' ) ) ) {\n\t\t\treturn 'conditional';\n\t\t}\n\n\t\treturn false;\n\t}\n\n\n\t/**\n\t * An array of default arguments to pass to $this->create() when creating a new post\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.12 Unknown.\n\t *\n\t * @param array $args Args of data to be passed to wp_insert_post.\n\t * @return array\n\t */\n\tprotected function get_creation_args( $args = null ) {\n\n\t\t// Allow nothing to be passed in.\n\t\tif ( empty( $args ) ) {\n\t\t\t$args = array();\n\t\t}\n\n\t\t// Backwards compat to original 3.0.0 format when just a title was passed in.\n\t\tif ( is_string( $args ) ) {\n\t\t\t$args = array(\n\t\t\t\t'post_title' => $args,\n\t\t\t);\n\t\t}\n\n\t\tif ( isset( $args['title'] ) ) {\n\t\t\t$args['post_title'] = $args['title'];\n\t\t\tunset( $args['title'] );\n\t\t}\n\t\tif ( isset( $args['content'] ) ) {\n\t\t\t$args['post_content'] = $args['content'];\n\t\t\tunset( $args['content'] );\n\t\t}\n\n\t\t$meta = isset( $args['meta_input'] ) ? $args['meta_input'] : array();\n\n\t\t$props = array_diff( array_keys( $this->get_properties() ), array_keys( $this->get_post_properties() ) );\n\n\t\tforeach ( $props as $prop ) {\n\n\t\t\tif ( isset( $args[ $prop ] ) ) {\n\n\t\t\t\t$meta[ $this->meta_prefix . $prop ] = $args[ $prop ];\n\t\t\t\tunset( $args[ $prop ] );\n\n\t\t\t}\n\t\t}\n\n\t\t$args['meta_input'] = wp_parse_args( $meta, $meta );\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'comment_status' => 'closed',\n\t\t\t\t'meta_input'     => array(),\n\t\t\t\t'menu_order'     => 1,\n\t\t\t\t'ping_status'    => 'closed',\n\t\t\t\t'post_author'    => get_current_user_id(),\n\t\t\t\t'post_content'   => '',\n\t\t\t\t'post_excerpt'   => '',\n\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t'post_title'     => '',\n\t\t\t\t'post_type'      => $this->get( 'db_post_type' ),\n\t\t\t)\n\t\t);\n\n\t\treturn apply_filters( \"llms_{$this->model_post_type}_get_creation_args\", $args, $this );\n\t}\n\n\n\t/**\n\t * Getter\n\t *\n\t * @since 3.38.2\n\t *\n\t * @param string  $key The property key.\n\t * @param boolean $raw Optional. Whether or not we need to get the raw value. Default false.\n\t * @return mixed\n\t */\n\tpublic function get( $key, $raw = false ) {\n\n\t\t$value = parent::get( $key, $raw );\n\n\t\t// When getting the 'not raw' value, make sure we always return a valid question type.\n\t\tif ( ! $raw && ! $value && 'question_type' === $key ) {\n\t\t\t$value = 'choice';\n\t\t}\n\n\t\treturn $value;\n\t}\n\n\t/**\n\t * Retrieve a choice by id\n\t *\n\t * @since 3.16.0\n\t * @since 4.4.0 Use strict comparison.\n\t *\n\t * @param string $id Choice ID.\n\t * @return obj|false\n\t */\n\tpublic function get_choice( $id ) {\n\t\t$choice = new LLMS_Question_Choice( $this->get( 'id' ), $id );\n\t\tif ( $choice->exists() && absint( $this->get( 'id' ) ) === absint( $choice->get_question_id() ) ) {\n\t\t\treturn $choice;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve the question's choices\n\t *\n\t * @since 3.16.0\n\t * @since 3.30.1 Improve choice sorting to accommodate numeric markers.\n\t * @since 3.35.0 Escape `LIKE` clause.\n\t * @since 4.4.0 Don't allow objects when using `unserialize()`.\n\t *\n\t * @param string $return Optional. Determine how to return the choice data.\n\t *                       'choices' (default) returns an array of LLMS_Question_Choice objects.\n\t *                       'ids' returns an array of LLMS_Question_Choice ids.\n\t * @return array\n\t */\n\tpublic function get_choices( $return = 'choices' ) {\n\n\t\tglobal $wpdb;\n\t\t$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT meta_key AS id\n\t\t\t\t  , meta_value AS data\n\t\t\t FROM {$wpdb->postmeta}\n\t\t\t WHERE post_id = %d\n\t\t\t   AND meta_key LIKE %s\n\t\t\t;\",\n\t\t\t\t$this->get( 'id' ),\n\t\t\t\t'_llms_choice_%'\n\t\t\t)\n\t\t);\n\n\t\tusort( $results, array( $this, 'sort_choices' ) );\n\n\t\tif ( 'ids' === $return ) {\n\t\t\treturn wp_list_pluck( $results, 'id' );\n\t\t}\n\n\t\t$ret = array();\n\t\tforeach ( $results as $result ) {\n\t\t\t$ret[] = new LLMS_Question_Choice( $this->get( 'id' ), unserialize( $result->data, array( 'allowed_classes' => false ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize\n\t\t}\n\n\t\treturn $ret;\n\t}\n\n\t/**\n\t * Retrieve the question description (post_content)\n\t *\n\t * Add's extra allowed tags to wp_kses_post allowed tags so that async audio shortcodes will work properly\n\t *\n\t * @since 3.16.6\n\t *\n\t * @return string\n\t */\n\tpublic function get_description() {\n\n\t\tglobal $allowedposttags;\n\t\t$allowedposttags['source'] = array(\n\t\t\t'src'  => true,\n\t\t\t'type' => true,\n\t\t);\n\t\t$desc                      = $this->get( 'content' );\n\t\tunset( $allowedposttags['source'] );\n\n\t\treturn apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_get_description', $desc, $this );\n\t}\n\n\t/**\n\t * Retrieve the correct values for a conditionally graded question\n\t *\n\t * @since 3.16.15\n\t *\n\t * @return array\n\t */\n\tpublic function get_conditional_correct_value() {\n\n\t\t$correct = explode( '|', $this->get( 'correct_value' ) );\n\t\t$correct = array_map( 'trim', $correct );\n\n\t\treturn $correct;\n\t}\n\n\t/**\n\t * Retrieve correct choices for a given question\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_correct_choice() {\n\n\t\t$correct = false;\n\n\t\tif ( $this->supports( 'choices' ) && $this->supports( 'grading', 'auto' ) ) {\n\n\t\t\t$multi   = ( 'yes' === $this->get( 'multi_choices' ) );\n\t\t\t$correct = array();\n\n\t\t\tforeach ( $this->get_choices() as $choice ) {\n\n\t\t\t\tif ( $choice->is_correct() ) {\n\t\t\t\t\t$correct[] = $choice->get( 'id' );\n\t\t\t\t\tif ( ! $multi ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Always sort multi choices for easy auto comparison.\n\t\t\tif ( $multi && $this->supports( 'selectable' ) ) {\n\t\t\t\tsort( $correct );\n\t\t\t}\n\t\t}\n\n\t\treturn $correct;\n\t}\n\n\t/**\n\t * Get the question text (title).\n\t *\n\t * @since 3.16.0\n\t * @since 7.8.0 Added $attempt param.\n\t *\n\t * @param string            $format  Optional. Format of the question text. Accepts 'html' or 'plain'.\n\t * @param LLMS_Quiz_Attempt $attempt Optional. Quiz Attempt object.\n\t * @return string\n\t */\n\tpublic function get_question( $format = 'html', $attempt = null ) {\n\t\t/**\n\t\t * Filter the question text.\n\t\t *\n\t\t * The dynamic portion of this filter, `{$type}`, refers to the question type.\n\t\t *\n\t\t * @since 7.8.0\n\t\t *\n\t\t * @param string            $title    Question title.\n\t\t * @param string            $format   Format of the question text. Accepts 'html' or 'plain'.\n\t\t * @param LLMS_Question     $question Question object.\n\t\t * @param LLMS_Quiz_Attempt $attempt  Attempt object.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->get( 'question_type' )}_question_get_question\", $this->get( 'title' ), $format, $this, $attempt );\n\t}\n\n\t/**\n\t * Retrieve child questions (for question group)\n\t *\n\t * @since 3.16.0\n\t *\n\t * @todo Need to prevent access for non-group questions.\n\t *\n\t * @return array\n\t */\n\tpublic function get_questions() {\n\t\treturn $this->questions()->get_questions();\n\t}\n\n\t/**\n\t * Retrieve URL for an image associated with the question if it's enabled\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string|array $size   Registered image size or a numeric array with width/height.\n\t * @param null         $unused Unused parameter.\n\t * @return string Source URL or an Eepty string if no image or not supported.\n\t */\n\tpublic function get_image( $size = 'full', $unused = null ) {\n\n\t\t$url = '';\n\n\t\tif ( $this->has_image() ) {\n\t\t\t$img = $this->get( 'image' );\n\t\t\tif ( isset( $img['id'] ) && is_numeric( $img['id'] ) ) {\n\t\t\t\t$src = wp_get_attachment_image_src( $img['id'], $size );\n\t\t\t\tif ( $src ) {\n\t\t\t\t\t$url = $src[0];\n\t\t\t\t} elseif ( isset( $img['src'] ) ) {\n\t\t\t\t\t$url = $img['src'];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_get_image', $url, $this );\n\t}\n\n\t/**\n\t * Retrieve the next marker for question choices.\n\t *\n\t * @since 3.16.0\n\t * @since 3.30.1 Fixed bug which caused the next marker to be 1 index too high.\n\t * @since 7.4.1 Check `$type['choices']` is an array before trying to access it as such.\n\t *\n\t * @return string\n\t */\n\tprotected function get_next_choice_marker() {\n\t\t$next_index = count( $this->get_choices( 'ids', false ) );\n\t\t$type       = $this->get_question_type();\n\t\tif ( ! is_array( $type['choices'] ?? false ) ) {\n\t\t\treturn false;\n\t\t}\n\t\t$markers = $type['choices']['markers'];\n\t\treturn $next_index > count( $markers ) ? false : $markers[ $next_index ];\n\t}\n\n\t/**\n\t * Retrieve question type data for the given question\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return array\n\t */\n\tpublic function get_question_type() {\n\t\treturn llms_get_question_type( $this->get( 'question_type' ) );\n\t}\n\n\t/**\n\t * Retrieve an instance of the questions parent LLMS_Quiz\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return obj\n\t */\n\tpublic function get_quiz() {\n\t\treturn new LLMS_Quiz( $this->get( 'parent_id' ) );\n\t}\n\n\t/**\n\t * Retrieve video embed for question featured video\n\t *\n\t * @since 3.16.0\n\t * @since 3.17.0 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_video() {\n\n\t\t$html  = '';\n\t\t$embed = $this->get( 'video_src' );\n\n\t\tif ( $embed ) {\n\n\t\t\t// Get oembed.\n\t\t\t$html = wp_oembed_get( $embed );\n\n\t\t\t// Fallback to video shortcode.\n\t\t\tif ( ! $html ) {\n\t\t\t\t$html = do_shortcode( '[video src=\"' . $embed . '\"]' );\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_' . $this->get( 'question_type' ) . '_question_get_video', $html, $embed, $this );\n\t}\n\n\t/**\n\t * Attempt to grade a question\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.15 Unknown.\n\t * @since 4.4.0 Combined nested if statements into a single condition.\n\t *\n\t * @param array[] $answer Selected answer(s).\n\t * @return string|null Returns `null` if the question cannot be automatically graded.\n\t *                     Returns `yes` for correct answers and `no` for incorrect answers.\n\t */\n\tpublic function grade( $answer ) {\n\n\t\t$question_type = $this->get( 'question_type' );\n\n\t\t/**\n\t\t * Use this filter to bypass core grading for a given question type.\n\t\t *\n\t\t * If the filter returns a non-null value core grading is bypassed.\n\t\t *\n\t\t * The dynamic portion of this hook, `$question_type`, refers to the type of question being graded.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param null|string   $grade    Defaults to `null` which signifies that LifterLMS should attempt to grade the answer.\n\t\t *                                Return `yes` (correct) or `no` (incorrect) to bypass core grading methods.\n\t\t * @param string[]      $answer   User-submitted answers.\n\t\t * @param LLMS_Question $question Question object.\n\t\t */\n\t\t$grade = apply_filters( \"llms_{$question_type}_question_pre_grade\", null, $answer, $this );\n\n\t\tif ( is_null( $grade ) && $this->get( 'points' ) >= 1 ) {\n\n\t\t\t$grading_type = $this->get_auto_grade_type();\n\n\t\t\tif ( 'choices' === $grading_type ) {\n\n\t\t\t\tsort( $answer );\n\t\t\t\t$grade = ( $answer === $this->get_correct_choice() ) ? 'yes' : 'no';\n\n\t\t\t} elseif ( 'conditional' === $grading_type ) {\n\n\t\t\t\t$correct = $this->get_conditional_correct_value();\n\n\t\t\t\t/**\n\t\t\t\t * Filter whether or not conditionally graded question answers are treated as a case-sensitive\n\t\t\t\t *\n\t\t\t\t * By default, case sensitivity is disabled.\n\t\t\t\t *\n\t\t\t\t * @since 3.16.15\n\t\t\t\t *\n\t\t\t\t * @param boolean       $case_sensitive Whether or not answers are treated as case-sensitive.\n\t\t\t\t * @param string[]      $answer         User-submitted answers.\n\t\t\t\t * @param string[]      $correct        Correct answers.\n\t\t\t\t * @param LLMS_Question $question       Question object.\n\t\t\t\t */\n\t\t\t\tif ( false === apply_filters( 'llms_quiz_grading_case_sensitive', false, $answer, $correct, $this ) ) {\n\n\t\t\t\t\t$answer  = array_map( 'strtolower', $answer );\n\t\t\t\t\t$correct = array_map( 'strtolower', $correct );\n\n\t\t\t\t}\n\n\t\t\t\t$answer  = array_map( 'trim', $answer );\n\t\t\t\t$correct = array_map( 'trim', $correct );\n\n\t\t\t\t$grade = ( $answer === $correct ) ? 'yes' : 'no';\n\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter the grading result of an answer for a given question type.\n\t\t *\n\t\t * The dynamic portion of this hook, `$question_type`, refers to the type of question being graded.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param null|string   $grade    Defaults to `null` which signifies that LifterLMS should attempt to grade the answer.\n\t\t *                                Return `yes` (correct) or `no` (incorrect) to bypass core grading methods.\n\t\t * @param string[]      $answer   User-submitted answers.\n\t\t * @param LLMS_Question $question Question object.\n\t\t */\n\t\treturn apply_filters( \"llms_{$question_type}_question_grade\", $grade, $answer, $this );\n\t}\n\n\t/**\n\t * Determine if a description is enabled and not empty\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.12 Unknown.\n\t *\n\t * @return bool\n\t */\n\tpublic function has_description() {\n\t\t$enabled = $this->get( 'description_enabled' );\n\t\t$content = $this->get( 'content' );\n\t\treturn ( 'yes' === $enabled && $content );\n\t}\n\n\t/**\n\t * Determine if a featured image is enabled and not empty\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return bool\n\t */\n\tpublic function has_image() {\n\t\t$img = $this->get( 'image' );\n\t\tif ( is_array( $img ) ) {\n\t\t\tif ( ! empty( $img['enabled'] ) && ( ! empty( $img['id'] ) || ! empty( $img['src'] ) ) ) {\n\t\t\t\treturn ( 'yes' === $img['enabled'] );\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determine if a featured video is enabled & not empty\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.12 Unknown.\n\t *\n\t * @return bool\n\t */\n\tpublic function has_video() {\n\t\t$enabled = $this->get( 'video_enabled' );\n\t\t$src     = $this->get( 'video_src' );\n\t\treturn ( 'yes' === $enabled && $src );\n\t}\n\n\t/**\n\t * Determine if the question is an orphan\n\t *\n\t * @since 3.27.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_orphan() {\n\n\t\t$statuses  = array( 'publish', 'draft' );\n\t\t$parent_id = $this->get( 'parent_id' );\n\n\t\tif ( ! $parent_id ) {\n\t\t\treturn true;\n\t\t} elseif ( ! in_array( get_post_status( $parent_id ), $statuses, true ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Access question manager (used for question groups)\n\t *\n\t * @since 3.16.0\n\t *\n\t * @todo Need to prevent access for non-group questions.\n\t *\n\t * @return obj\n\t */\n\tpublic function questions() {\n\t\treturn new LLMS_Question_Manager( $this );\n\t}\n\n\t/**\n\t * Sort choices by marker.\n\t *\n\t * @since 3.30.1\n\t * @since 4.4.0 Don't allow objects when using `unserialize()`.\n\t *\n\t * @param string $choice_a Serialized choice data.\n\t * @param string $choice_b Serialized choice data.\n\t * @return int\n\t */\n\tprivate function sort_choices( $choice_a, $choice_b ) {\n\t\t$a_data = unserialize( $choice_a->data, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize\n\t\t$b_data = unserialize( $choice_b->data, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize\n\t\treturn strnatcmp( $a_data['marker'], $b_data['marker'] );\n\t}\n\n\t/**\n\t * Determine if the question supports a question feature\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.15 Unknown.\n\t *\n\t * @param string $feature Name of the feature (eg \"choices\").\n\t * @param mixed  $option  Allow matching feature options.\n\t * @return boolean\n\t */\n\tpublic function supports( $feature, $option = null ) {\n\n\t\t$ret = false;\n\n\t\t$type = $this->get_question_type();\n\t\tif ( $type ) {\n\t\t\tif ( 'choices' === $feature ) {\n\t\t\t\t$ret = ( ! empty( $type['choices'] ) );\n\t\t\t} elseif ( 'grading' === $feature ) {\n\t\t\t\t$ret = ( $type['grading'] && $option === $type['grading'] );\n\t\t\t} elseif ( 'points' === $feature ) {\n\t\t\t\t$ret = $type['points'];\n\t\t\t} elseif ( 'random_lock' === $feature ) {\n\t\t\t\t$ret = $type['random_lock'];\n\t\t\t} elseif ( 'selectable' === $feature ) {\n\t\t\t\t$ret = empty( $type['choices'] ) ? false : $type['choices']['selectable'];\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filter supported features of a given question type.\n\t\t *\n\t\t * The dynamic portion of this hook, `$this->get( 'question_type' )`, refers to the type of question\n\t\t * being filtered.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param boolean       $ret      Return value.\n\t\t * @param string        $string   Name of the feature being checked.\n\t\t * @param string        $option   Name of the option being checked.\n\t\t * @param LLMS_Question $question Instance of the LLMS_Question.\n\t\t */\n\t\treturn apply_filters( \"llms_{$this->get( 'question_type' )}_question_supports\", $ret, $feature, $option, $this );\n\t}\n\n\t/**\n\t * Called before data is sorted and returned by $this->toArray()\n\t *\n\t * Extending classes should override this data if custom data should\n\t * be added when object is converted to an array or json.\n\t *\n\t * @since 3.3.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param array $arr Array of data to be serialized.\n\t * @return array\n\t */\n\tprotected function toArrayAfter( $arr ) {\n\n\t\tunset( $arr['author'] );\n\t\tunset( $arr['date'] );\n\t\tunset( $arr['excerpt'] );\n\t\tunset( $arr['modified'] );\n\t\tunset( $arr['status'] );\n\n\t\t$choices = array();\n\t\tforeach ( $this->get_choices() as $choice ) {\n\t\t\t$choices[] = $choice->get_data();\n\t\t}\n\t\t$arr['choices'] = $choices;\n\n\t\tif ( 'group' === $this->get( 'question_type' ) ) {\n\t\t\t$arr['questions'] = array();\n\t\t\tforeach ( $this->get_questions() as $question ) {\n\t\t\t\t$arr['questions'][] = $question->toArray();\n\t\t\t}\n\t\t}\n\n\t\treturn $arr;\n\t}\n\n\t/**\n\t * Update a question choice\n\t *\n\t * If no id is supplied will create a new choice.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array $data Array of choice data.\n\t * @return string|boolean\n\t */\n\tpublic function update_choice( $data ) {\n\n\t\t// If there's no ID, we'll add a new choice.\n\t\tif ( ! isset( $data['id'] ) ) {\n\t\t\treturn $this->create_choice( $data );\n\t\t}\n\n\t\t// Get the question.\n\t\t$choice = $this->get_choice( $data['id'] );\n\t\tif ( ! $choice ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$choice->update( $data )->save();\n\n\t\t// Return choice ID.\n\t\treturn $choice->get( 'id' );\n\t}\n\n\t/**\n\t * Retrieve quizzes this quiz is assigned to\n\t *\n\t * @since 3.12.0\n\t *\n\t * @return array Array of WP_Post IDs (quiz post types).\n\t */\n\tpublic function get_quizzes() {\n\n\t\t$id  = absint( $this->get( 'id' ) );\n\t\t$len = strlen( strval( $id ) );\n\n\t\t$str_like = '%' . sprintf( 's:2:\"id\";s:%1$d:\"%2$s\";', $len, $id ) . '%';\n\t\t$int_like = '%' . sprintf( 's:2:\"id\";i:%1$s;', $id ) . '%';\n\n\t\tglobal $wpdb;\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$query = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery\n\t\t\t\"SELECT post_id\n\t\t\t FROM {$wpdb->postmeta}\n\t\t\t WHERE meta_key = '_llms_questions'\n\t\t\t   AND (\n\t\t\t   \t      meta_value LIKE '{$str_like}'\n\t\t\t   \t   OR meta_value LIKE '{$int_like}'\n\t\t\t   );\"\n\t\t);\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\treturn $query;\n\t}\n\n\t/**\n\t * Don't add custom fields during toArray()\n\t *\n\t * @since 3.16.11\n\t *\n\t * @param array $arr Post model array.\n\t * @return array\n\t */\n\tprotected function toArrayCustom( $arr ) {\n\t\treturn $arr;\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.quiz.attempt.php",
    "content": "<?php\n/**\n * Quiz Attempt Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.9.0\n * @version 7.4.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Quiz_Attempt model class.\n *\n * @since 3.9.0\n * @since 3.9.2 Added `calculate_point_weight()`, `get_question_order()`, `is_passing()` methods.\n * @since 3.16.0 Unknown.\n * @since 3.16.7 Unknown.\n * @since 3.17.1 Unknown.\n * @since 3.19.2 Unknown.\n * @since 3.24.0 Unknown.\n * @since 3.26.3 Unknown.\n * @since 3.29.0 Unknown.\n * @since 4.0.0 Remove reliance on deprecated method `LLMS_Quiz::get_passing_percent()` & remove deprecated class method `get_status()`.\n *              Fix issue encountered when answering a question incorrectly after initially answering it correctly.\n * @since 4.2.0 Use strict type comparisons where possible.\n *              In the `l10n()` method, made sure the status key exists to avoid trying to access to array's undefined index.\n *              Added the public method `get_siblings()`.\n * @since 4.3.0 Added `$type` property declaration.\n */\nclass LLMS_Quiz_Attempt extends LLMS_Abstract_Database_Store {\n\n\t/**\n\t * Array of table column name => format\n\t *\n\t * @since unknown\n\t * @since 7.8.0 Added `can_be_resumed` column.\n\t * @var array\n\t */\n\tprotected $columns = array(\n\t\t'student_id'          => '%d',\n\t\t'quiz_id'             => '%d',\n\t\t'lesson_id'           => '%d',\n\t\t'start_date'          => '%s',\n\t\t'update_date'         => '%s',\n\t\t'end_date'            => '%s',\n\t\t'status'              => '%s',\n\t\t'attempt'             => '%d',\n\t\t'grade'               => '%f',\n\t\t'questions'           => '%s',\n\t\t'can_be_resumed'      => '%d',\n\t\t'current_question_id' => '%d',\n\t);\n\n\tprotected $date_created = 'start_date';\n\tprotected $date_updated = 'update_date';\n\n\t/**\n\t * Database Table Name\n\t *\n\t * @var string\n\t */\n\tprotected $table = 'quiz_attempts';\n\n\t/**\n\t * The record type\n\t *\n\t * @var string\n\t */\n\tprotected $type = 'quiz_attempt';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param mixed $item Optional. Array/obj of attempt data or int. Default `null`.\n\t * @return void\n\t */\n\tpublic function __construct( $item = null ) {\n\n\t\tif ( is_numeric( $item ) ) {\n\n\t\t\t$this->id = $item;\n\n\t\t} elseif ( is_object( $item ) && isset( $item->id ) ) {\n\n\t\t\t$this->id = $item->id;\n\n\t\t} elseif ( is_array( $item ) && isset( $item['id'] ) ) {\n\n\t\t\t$this->id = $item['id'];\n\n\t\t}\n\n\t\tif ( ! $this->id ) {\n\n\t\t\tif ( is_array( $item ) || is_object( $item ) ) {\n\t\t\t\t$this->setup( $item );\n\t\t\t}\n\n\t\t\tparent::__construct();\n\n\t\t}\n\t}\n\n\t/**\n\t * Answer a question\n\t *\n\t * Records the selected option and whether or not the selected option was the correct option.\n\t *\n\t * Automatically updates & saves the attempt to the database\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Updated to accommodate quiz builder improvements.\n\t * @since 4.0.0 Explicitly set earned points to `0` when answering incorrectly.\n\t *              Exit the loop as soon as we find our question.\n\t *              Use strict comparison for IDs.\n\t *\n\t * @param int      $question_id WP_Post ID of the LLMS_Question.\n\t * @param string[] $answer      Array of selected choice IDs (for core question types) or an array containing the user-submitted answer(s).\n\t * @return LLMS_Quiz_Attempt Instance of the current attempt.\n\t */\n\tpublic function answer_question( $question_id, $answer ) {\n\n\t\t$questions = $this->get_questions();\n\n\t\tforeach ( $questions as $key => $data ) {\n\n\t\t\tif ( absint( $question_id ) !== absint( $data['id'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$question                     = llms_get_post( $question_id );\n\t\t\t$graded                       = $question->grade( $answer );\n\t\t\t$questions[ $key ]['answer']  = $answer;\n\t\t\t$questions[ $key ]['correct'] = $graded;\n\t\t\t$questions[ $key ]['earned']  = llms_parse_bool( $graded ) ? $questions[ $key ]['points'] : 0;\n\n\t\t\tbreak;\n\t\t}\n\n\t\t$this->set_questions( $questions )->save();\n\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Calculate and the grade for a completed quiz\n\t *\n\t * @since 3.9.0\n\t * @since 3.24.0 Unknown.\n\t * @since 4.0.0 Remove reliance on deprecated method `LLMS_Quiz::get_passing_percent()`.\n\t *\n\t * @return LLMS_Quiz_Attempt Instance of the current quiz attempt.\n\t */\n\tpublic function calculate_grade() {\n\n\t\t$status = 'pending';\n\n\t\tif ( $this->is_auto_gradeable() ) {\n\n\t\t\t$grade = llms()->grades()->round( $this->get_count( 'earned' ) * $this->calculate_point_weight() );\n\n\t\t\t$quiz      = $this->get_quiz();\n\t\t\t$min_grade = $quiz ? $quiz->get( 'passing_percent' ) : 100;\n\n\t\t\t$this->set( 'grade', $grade );\n\t\t\t$status = ( $min_grade <= $grade ) ? 'pass' : 'fail';\n\n\t\t}\n\n\t\t$this->set_status( $status );\n\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Calculate the weight of each point\n\t *\n\t * @since 3.9.2\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @return float\n\t */\n\tprivate function calculate_point_weight() {\n\t\t$available = $this->get_count( 'available_points' );\n\t\treturn ( $available > 0 ) ? ( 100 / $available ) : 0;\n\t}\n\n\t/**\n\t * Run actions designating quiz completion\n\t *\n\t * @since 3.16.0\n\t * @since 3.17.1 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function do_completion_actions() {\n\n\t\t// Do quiz completion actions.\n\t\tdo_action( 'lifterlms_quiz_completed', $this->get_student()->get_id(), $this->get( 'quiz_id' ), $this );\n\n\t\t$passed = false;\n\n\t\tswitch ( $this->get( 'status' ) ) {\n\n\t\t\tcase 'pass':\n\t\t\t\t$passed = true;\n\t\t\t\tdo_action( 'lifterlms_quiz_passed', $this->get_student()->get_id(), $this->get( 'quiz_id' ), $this );\n\t\t\t\tbreak;\n\n\t\t\tcase 'fail':\n\t\t\t\tdo_action( 'lifterlms_quiz_failed', $this->get_student()->get_id(), $this->get( 'quiz_id' ), $this );\n\t\t\t\tbreak;\n\n\t\t\tcase 'pending':\n\t\t\t\tdo_action( 'lifterlms_quiz_pending', $this->get_student()->get_id(), $this->get( 'quiz_id' ), $this );\n\t\t\t\tbreak;\n\n\t\t}\n\t}\n\n\t/**\n\t * End a quiz attempt\n\t *\n\t * Sets end date, unsets the quiz as the current quiz, and records a grade.\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param boolean $silent Optional. If `true`, will not trigger actions or mark related lesson as complete. Default `false`.\n\t * @return LLMS_Quiz_Attempt This quiz attempt instance (for chaining).\n\t */\n\tpublic function end( $silent = false ) {\n\n\t\t$this->set( 'end_date', current_time( 'mysql' ) );\n\t\t$this->calculate_grade()->save();\n\n\t\tif ( ! $silent ) {\n\n\t\t\t$this->do_completion_actions();\n\n\t\t}\n\n\t\t// Clear \"cached\" grade so it's recalculated next time it's requested.\n\t\t$this->get_student()->set( 'overall_grade', '' );\n\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Get sibling attempts\n\t *\n\t * @since 4.2.0\n\t *\n\t * @param array  $args Optional. List of args to be passed as params of the quiz attempts query. Default empty array.\n\t *                     See `LLMS_Query_Quiz_Attempt` and `LLMS_Database_Query` for the list of args.\n\t *                     By default the `per_page` param is set to 1000.\n\t * @param string $return Optional. Type of return [ids|attempts]. Default 'attempts'.\n\t * @return int[]|LLMS_Quiz_Attempt[] Type depends on value of `$return`.\n\t */\n\tpublic function get_siblings( $args = array(), $return = 'attempts' ) {\n\n\t\t$defaults = array(\n\t\t\t'per_page' => 1000,\n\t\t);\n\n\t\t$args  = wp_parse_args( $args, $defaults );\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray_merge(\n\t\t\t\t$args,\n\t\t\t\tarray(\n\t\t\t\t\t'student_id' => $this->get( 'student_id' ),\n\t\t\t\t\t'quiz_id'    => $this->get( 'quiz_id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\treturn 'ids' === $return ? wp_list_pluck( $query->get_results(), 'id' ) : $query->get_attempts();\n\t}\n\n\t/**\n\t * Retrieve a count for various pieces of information related to the attempt\n\t *\n\t * @since 3.9.0\n\t * @since 3.19.2 Unknown.\n\t * @since 4.2.0 Ensure only one return point.\n\t *\n\t * @param string $key The key of the data to count.\n\t * @return int\n\t */\n\tpublic function get_count( $key ) {\n\n\t\t$count     = 0;\n\t\t$questions = $this->get_questions();\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'available_points':\n\t\t\tcase 'correct_answers':\n\t\t\tcase 'earned':\n\t\t\tcase 'gradeable_questions': // Like \"questions\" but excludes content questions.\n\t\t\tcase 'points': // Legacy version of earned.\n\t\t\t\tforeach ( $questions as $data ) {\n\t\t\t\t\t// Get the total number of correct answers.\n\t\t\t\t\tif ( 'correct_answers' === $key ) {\n\t\t\t\t\t\tif ( 'yes' === $data['correct'] ) {\n\t\t\t\t\t\t\t++$count;\n\t\t\t\t\t\t}\n\t\t\t\t\t} elseif ( 'earned' === $key || 'points' === $key ) {\n\t\t\t\t\t\t$count += $data['earned'];\n\t\t\t\t\t\t// Get the total number of possible points.\n\t\t\t\t\t} elseif ( 'available_points' === $key ) {\n\t\t\t\t\t\t$count += $data['points'];\n\t\t\t\t\t} elseif ( 'gradeable_questions' === $key ) {\n\t\t\t\t\t\tif ( $data['points'] ) {\n\t\t\t\t\t\t\t++$count;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'questions':\n\t\t\t\t$count = count( $questions );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $count;\n\t}\n\n\t/**\n\t * Retrieve a formatted date\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param string $key    'start' or 'end'.\n\t * @param string $format Optional. Output date format (PHP), uses WordPress format options if none provided.\n\t *                       If not provided defaults to WP date format options.\n\t * @return string\n\t */\n\tpublic function get_date( $key, $format = null ) {\n\t\tif ( ! $this->get( $key . '_date' ) ) {\n\t\t\treturn '';\n\t\t}\n\t\t$date   = strtotime( $this->get( $key . '_date' ) );\n\t\t$format = ! $format ? get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) : $format;\n\t\treturn date_i18n( $format, $date );\n\t}\n\n\t/**\n\t * Retrieve the first question for the attempt\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @return int|false\n\t */\n\tpublic function get_first_question() {\n\n\t\t$questions = $this->get_questions();\n\t\tif ( $questions ) {\n\t\t\t$first = array_shift( $questions );\n\t\t\treturn $first['id'];\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get the numeric order of a question in a given quiz\n\t *\n\t * @since 3.9.2\n\t * @since 3.16.0 Unknown.\n\t * @since 4.2.0 Use strict type comparison.\n\t *\n\t * @param int $question_id WP Post ID of the LLMS_Question.\n\t * @return int\n\t */\n\tpublic function get_question_order( $question_id ) {\n\n\t\tforeach ( $this->get_questions() as $order => $data ) {\n\n\t\t\tif ( absint( $data['id'] ) === $question_id ) {\n\t\t\t\treturn $order + 1;\n\t\t\t}\n\t\t}\n\n\t\treturn 0;\n\t}\n\n\t/**\n\t * Get an encoded attempt key that can be passed in URLs and the like\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.7 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_key() {\n\t\treturn LLMS_Hasher::hash( $this->get( 'id' ) );\n\t}\n\n\t/**\n\t * Retrieve an array of blank questions for insertion into a new attempt during initialization.\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t * @since 7.4.1 Moved randomization into `LLMS_Quiz_Attempt::randomize_attempt_questions()`.\n\t *\n\t * @return array\n\t */\n\tprivate function get_new_questions() {\n\n\t\t$quiz = llms_get_post( $this->get( 'quiz_id' ) );\n\n\t\t$questions = array();\n\n\t\tif ( $quiz ) {\n\n\t\t\t/**\n\t\t\t * Filter randomize value for quiz questions.\n\t\t\t *\n\t\t\t * @since 7.4.0\n\t\t\t *\n\t\t\t * @param bool              $randomize The randomize boolean value.\n\t\t\t * @param LLMS_Quiz         $quiz      LLMS_Quiz instance.\n\t\t\t * @param LLMS_Quiz_Attempt $attempt   LLMS_Quiz_Attempt instance.\n\t\t\t */\n\t\t\t$randomize = apply_filters( 'llms_quiz_attempt_questions_randomize', llms_parse_bool( $quiz->get( 'random_questions' ) ), $quiz, $this );\n\n\t\t\t/**\n\t\t\t * Filter questions for the quiz.\n\t\t\t *\n\t\t\t * Sets the questions to be used for the quiz.\n\t\t\t *\n\t\t\t * @since 7.4.0\n\t\t\t *\n\t\t\t * @param array             $questions Array of LLMS_Question objects.\n\t\t\t * @param LLMS_Quiz         $quiz      LLMS_Quiz instance.\n\t\t\t * @param LLMS_Quiz_Attempt $attempt   LLMS_Quiz_Attempt instance.\n\t\t\t */\n\t\t\t$quiz_questions = apply_filters( 'llms_quiz_attempt_questions', $quiz->get_questions(), $quiz, $this );\n\n\t\t\tforeach ( $quiz_questions as $index => $question ) {\n\n\t\t\t\t$questions[] = array(\n\t\t\t\t\t'id'      => $question->get( 'id' ),\n\t\t\t\t\t'earned'  => 0,\n\t\t\t\t\t'points'  => $question->supports( 'points' ) ? $question->get( 'points' ) : 0,\n\t\t\t\t\t'answer'  => null,\n\t\t\t\t\t'correct' => null,\n\t\t\t\t);\n\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filter attempt's questions array for the quiz.\n\t\t\t *\n\t\t\t * @since 7.4.1\n\t\t\t *\n\t\t\t * @param array             $questions Array of question (each question is an array itself).\n\t\t\t * @param LLMS_Quiz         $quiz      LLMS_Quiz instance.\n\t\t\t * @param LLMS_Quiz_Attempt $attempt   LLMS_Quiz_Attempt instance.\n\t\t\t */\n\t\t\t$questions = apply_filters( 'llms_quiz_attempt_questions_array', $questions, $quiz, $this );\n\n\t\t\tif ( $randomize ) {\n\t\t\t\t$questions = self::randomize_attempt_questions( $questions );\n\t\t\t}\n\t\t}\n\n\t\treturn $questions;\n\t}\n\n\t/**\n\t * Retrieve the next unanswered question in the attempt\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t * @since 4.2.0 Use strict type comparison.\n\t * @since 7.8.0 Added `$return` param, by default `\"id\"`.\n\t *\n\t * @param int    $last_question Optional. WP Post ID of the current LLMS_Question the \"next\" refers to. Default `null`.\n\t * @param string $return        Optional. Return type 'id|array'. Default 'id'.\n\t * @return int|array|false\n\t */\n\tpublic function get_next_question( $last_question = null, $return = 'id' ) {\n\n\t\t$next = false;\n\n\t\tforeach ( $this->get_questions() as $question ) {\n\n\t\t\tif ( $next || is_null( $question['answer'] ) ) {\n\n\t\t\t\treturn 'id' === $return ? $question['id'] : $question;\n\n\t\t\t\t// When rewinding and moving back through we don't want to skip questions.\n\t\t\t} elseif ( $last_question && absint( $last_question ) === absint( $question['id'] ) ) {\n\t\t\t\t$next = true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve the previous question in the attempt relative to a given question ID.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param int $question_id WP Post ID of the current LLMS_Question.\n\t * @return int|false\n\t */\n\tpublic function get_previous_question( $question_id ) {\n\n\t\t$next = false;\n\n\t\tforeach ( array_reverse( $this->get_questions() ) as $question ) {\n\n\t\t\tif ( $next ) {\n\n\t\t\t\treturn $question['id'];\n\n\t\t\t} elseif ( $question_id && absint( $question_id ) === absint( $question['id'] ) ) {\n\t\t\t\t$next = true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\n\t/**\n\t * Retrieve the question in the attempt.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param int    $question_id WP Post ID of the required LLMS_Question.\n\t * @param string $return      Optional. Return type 'id|array'. Default 'id'.\n\t * @return int|array|false\n\t */\n\tpublic function get_question( $question_id, $return = 'id' ) {\n\n\t\t$ids   = array_map( 'intval', array_column( $this->get_questions(), 'id' ) );\n\t\t$index = array_search( (int) $question_id, $ids, true );\n\t\treturn $index >= 0 ?\n\t\t\t'id' === $return ? $question_id : $this->get_questions()[ $index ]\n\t\t\t:\n\t\t\tfalse;\n\t}\n\n\t/**\n\t * Retrieve a permalink for the attempt\n\t *\n\t * @since 3.9.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_permalink() {\n\t\tif ( ! $this->get_quiz() ) {\n\t\t\treturn '';\n\t\t}\n\t\treturn add_query_arg( 'attempt_key', $this->get_key(), get_permalink( $this->get_quiz()->get( 'id' ) ) );\n\t}\n\n\t/**\n\t * Get array of serialized questions\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param boolean $cache Optional. If `true`, save data to to the object for future gets. Default `true`.\n\t * @return array\n\t */\n\tpublic function get_questions( $cache = true ) {\n\n\t\t$questions = $this->get( 'questions', $cache );\n\t\tif ( $questions ) {\n\t\t\treturn unserialize( $questions );\n\t\t}\n\t\treturn array();\n\t}\n\n\t/**\n\t * Retrieve an array of attempt question objects\n\t *\n\t * @since 3.16.0\n\t * @since 5.3.0 Add a parameter to filter out removed questions.\n\t *\n\t * @param boolean $cache          Optional. If `true`, save data to to the object for future gets. Default `true`.\n\t *                                Cached questions won't take into account the `$filte_removed` parameter.\n\t * @param boolean $filter_removed Optional. If `true`, removed questions will be filtered out. Default `false`.\n\t * @return array\n\t */\n\tpublic function get_question_objects( $cache = true, $filter_removed = false ) {\n\n\t\t$questions = array();\n\t\tforeach ( $this->get_questions( $cache ) as $qdata ) {\n\t\t\t$question = new LLMS_Quiz_Attempt_Question( $qdata );\n\t\t\tif ( ! $filter_removed || $question->get_question() instanceof LLMS_Question ) {\n\t\t\t\t$questions[] = $question;\n\t\t\t}\n\t\t}\n\t\treturn $questions;\n\t}\n\n\t/**\n\t * Retrieve an an answer to a specific question.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param int     $question_id    Question ID.\n\t * @param boolean $cache          Optional. If `true`, save data to to the object for future gets. Default `true`.\n\t *                                Cached questions won't take into account the `$filte_removed` parameter.\n\t * @param boolean $filter_removed Optional. If `true`, removed questions will be filtered out. Default `false`.\n\t * @return mixed\n\t */\n\tpublic function get_question_answer( $question_id, $cache = true, $filter_removed = false ) {\n\n\t\t$question_objects = $this->get_question_objects( $cache, $filter_removed );\n\t\tif ( ! $question_objects ) {\n\t\t\treturn array();\n\t\t}\n\t\tforeach ( $question_objects as $attempt_question ) {\n\t\t\t$quiz_question = $attempt_question->get_question();\n\t\t\tif ( $attempt_question->get_question()->get( 'id' ) === $question_id ) {\n\t\t\t\t$answer = $attempt_question->get( 'answer' );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn $answer ?? array();\n\t}\n\n\t/**\n\t * Get an instance of the LLMS_Quiz for the attempt\n\t *\n\t * @since 3.9.0\n\t *\n\t * @return LLMS_Quiz\n\t */\n\tpublic function get_quiz() {\n\t\treturn llms_get_post( $this->get( 'quiz_id' ) );\n\t}\n\n\t/**\n\t * Get an LLMS_Student for the quiz\n\t *\n\t * @since 3.9.0\n\t *\n\t * @return LLMS_Student\n\t */\n\tpublic function get_student() {\n\t\treturn llms_get_student( $this->get( 'student_id' ) );\n\t}\n\n\t/**\n\t * Get the time spent on the quiz from start to end\n\t *\n\t * @since 3.9.0\n\t *\n\t * @param integer $precision Precision passed to `llms_get_date_diff()`.\n\t * @return string\n\t */\n\tpublic function get_time( $precision = 2 ) {\n\t\tif ( ! $this->get( 'end_date' ) ) {\n\t\t\treturn esc_html__( 'Unknown', 'lifterlms' );\n\t\t}\n\t\treturn llms_get_date_diff( $this->get_date( 'start', 'U' ), $this->get_date( 'end', 'U' ), $precision );\n\t}\n\n\t/**\n\t * Retrieve a title-like string\n\t *\n\t * @since 3.16.0\n\t * @since 3.26.3 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\t$student = $this->get_student();\n\t\t$name    = $student ? $this->get_student()->get_name() : apply_filters( 'llms_quiz_attempt_deleted_student_name', __( '[Deleted]', 'lifterlms' ) );\n\t\treturn sprintf( __( 'Quiz Attempt #%1$d by %2$s', 'lifterlms' ), $this->get( 'attempt' ), $name );\n\t}\n\n\t/**\n\t * Initialize a new quiz attempt by quiz and lesson for a user.\n\t *\n\t * If no user found throws an Exception.\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t * @since 7.8.0 Set the property `can_be_resumed` on init.\n\t *\n\t * @throws Exception When the user is not logged in.\n\t *\n\t * @param int   $quiz_id   WP Post ID of the quiz.\n\t * @param int   $lesson_id WP Post ID of the lesson.\n\t * @param mixed $student   Optional. Accepts anything that can be passed to llms_get_student.\n\t *                         If no user is passed the current user will be used. Default `null`.\n\t *\n\t * @return obj\n\t */\n\tpublic static function init( $quiz_id, $lesson_id, $student = null ) {\n\n\t\t$student = llms_get_student( $student );\n\t\tif ( ! $student ) {\n\t\t\tthrow new Exception( esc_html__( 'You must be logged in to take a quiz!', 'lifterlms' ) );\n\t\t}\n\n\t\t// Initialize a new attempt.\n\t\t$attempt = new self();\n\t\t$quiz    = llms_get_post( $quiz_id );\n\t\t$attempt->set( 'quiz_id', $quiz_id );\n\t\t$attempt->set( 'lesson_id', $lesson_id );\n\t\t$attempt->set( 'student_id', $student->get_id() );\n\t\t$attempt->set_status( 'incomplete' );\n\t\t$attempt->set( 'can_be_resumed', $quiz->can_be_resumed() );\n\t\t$attempt->set_questions( $attempt->get_new_questions() );\n\n\t\t$number = 1;\n\n\t\t$last_attempt = $student->quizzes()->get_last_attempt( $quiz_id );\n\t\tif ( $last_attempt ) {\n\t\t\t$number = absint( $last_attempt->get( 'attempt' ) ) + 1;\n\t\t}\n\t\t$attempt->set( 'attempt', $number );\n\n\t\treturn $attempt;\n\t}\n\n\n\t/**\n\t * Randomize attempt questions.\n\t *\n\t * Logic moved from `LLMS_Quiz_Attempt::get_new_questions()`.\n\t *\n\t * @since 7.4.1\n\t *\n\t * @param array $questions Array of attempt's questions (each question is an array itself).\n\t * @return array.\n\t */\n\tpublic static function randomize_attempt_questions( $questions ) {\n\n\t\tif ( empty( $questions ) ) {\n\t\t\treturn $questions;\n\t\t}\n\n\t\t// Array of indexes that will be locked during shuffling.\n\t\t$locks = array();\n\t\tforeach ( $questions as $index => $question_array ) {\n\t\t\t$question = llms_get_post( $question_array['id'] );\n\t\t\t// If randomization is enabled, store the questions index so we can lock it during randomization.\n\t\t\tif ( $question->supports( 'random_lock' ) ) {\n\t\t\t\t$locks[] = $index;\n\t\t\t}\n\t\t}\n\n\t\t// Lifted from https://stackoverflow.com/a/28491007/400568.\n\t\t// I generally comprehend this code but also in a truer way i have no idea...\n\t\t$inc = array();\n\t\t$i   = 0;\n\t\t$j   = 0;\n\t\t$l   = count( $questions );\n\t\t$le  = count( $locks );\n\t\twhile ( $i < $l ) {\n\t\t\tif ( $j >= $le || $i < $locks[ $j ] ) {\n\t\t\t\t$inc[] = $i;\n\t\t\t} else {\n\t\t\t\t++$j;\n\t\t\t}\n\t\t\t++$i;\n\t\t}\n\n\t\t// Fisher-yates-knuth shuffle variation O(n).\n\t\t$num = count( $inc );\n\t\twhile ( $num-- ) {\n\t\t\t$perm                       = wp_rand( 0, $num );\n\t\t\t$swap                       = $questions[ $inc[ $num ] ];\n\t\t\t$questions[ $inc[ $num ] ]  = $questions[ $inc[ $perm ] ];\n\t\t\t$questions[ $inc[ $perm ] ] = $swap;\n\t\t}\n\n\t\treturn $questions;\n\t}\n\n\t/**\n\t * Determine if the attempt can be autograded.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return bool\n\t */\n\tprivate function is_auto_gradeable() {\n\n\t\tforeach ( $this->get_question_objects() as $question ) {\n\n\t\t\tif ( 'waiting' === $question->get_status() ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Determine if the attempt is the last attempt of its quiz.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_last_attempt() {\n\n\t\t$student = llms_get_student( $this->get( 'student_id' ) );\n\t\tif ( ! $student ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$last_attempt = $student->quizzes()->get_last_attempt( $this->get( 'quiz_id' ) );\n\t\treturn $last_attempt && ( $this->get( 'id' ) === $last_attempt->get( 'id' ) );\n\t}\n\n\t/**\n\t * Determine if the attempt was passing.\n\t *\n\t * @since 3.9.2\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_passing() {\n\t\treturn ( 'pass' === $this->get( 'status' ) );\n\t}\n\n\t/**\n\t * Determine if the attempt can be resumed.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @return boolean\n\t */\n\tpublic function can_be_resumed() {\n\n\t\treturn 1 === (int) $this->get( 'can_be_resumed' ) && 'incomplete' === $this->get( 'status' ) && ! $this->has_resume_attempt_time_expired() && $this->is_last_attempt();\n\t}\n\n\t/**\n\t * Determine if the student's resume quiz attempt time limit is expired.\n\t *\n\t * The default resume quiz attempt time limit for a student is 1 day.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @return bool\n\t */\n\tpublic function has_resume_attempt_time_expired() {\n\n\t\t$student = llms_get_student();\n\n\t\tif ( ! $student ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( ! $this->get( 'start_date' ) ) {\n\t\t\treturn false;\n\t\t}\n\t\t$start_date = $this->get( 'start_date' );\n\n\t\t/**\n\t\t * Filters the X time for resuming quiz.\n\t\t *\n\t\t * @since 7.8.0\n\t\t *\n\t\t * @param int $resume_time_period The time period in hours.\n\t\t */\n\t\t$resume_time_period = apply_filters( 'llms_quiz_attempt_resume_time_period', 24, $this );\n\n\t\t$start_date_obj   = new DateTime( $start_date, wp_timezone() );\n\t\t$current_date_obj = current_datetime();\n\n\t\treturn $current_date_obj > $start_date_obj->modify( sprintf( '+%d hours', intval( $resume_time_period ) ) );\n\t}\n\n\t/**\n\t * Translate attempt related strings.\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t * @since 4.2.0 Made sure the status key exists to avoid trying to access to array's undefined index.\n\t *\n\t * @param string $key Key to translate.\n\t * @return string\n\t */\n\tpublic function l10n( $key ) {\n\n\t\t$tkey = '';\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'passed': // Deprecated.\n\t\t\tcase 'status':\n\t\t\t\t$statuses = llms_get_quiz_attempt_statuses();\n\t\t\t\t$status   = $this->get( 'status' );\n\t\t\t\t$tkey     = ( $status && isset( $statuses[ $status ] ) ) ? $statuses[ $status ] : $tkey;\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $tkey;\n\t}\n\n\t/**\n\t * Setter for serialized questions array.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array   $questions Question data.\n\t * @param boolean $save      Optional. If `true`, immediately persists to database. Default `false`.\n\t * @return LLMS_Quiz_Attempt This quiz attempt instance (for chaining).\n\t */\n\tpublic function set_questions( $questions = array(), $save = false ) {\n\t\treturn $this->set( 'questions', serialize( $questions ), $save );\n\t}\n\n\t/**\n\t * Set the status of the attempt.\n\t *\n\t * @since 3.16.0\n\t * @since 4.0.0 Use strict comparisons.\n\t *\n\t * @param string  $status Status value.\n\t * @param boolean $save   If `true`, immediately persists to database.\n\t * @return false|LLMS_Quiz_Attempt\n\t */\n\tpublic function set_status( $status, $save = false ) {\n\n\t\t$statuses = array_keys( llms_get_quiz_attempt_statuses() );\n\t\tif ( ! in_array( $status, $statuses, true ) ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn $this->set( 'status', $status );\n\t}\n\n\t/**\n\t * Record the attempt as started\n\t *\n\t * @since 3.9.0\n\t *\n\t * @return LLMS_Quiz_Attempt Instance of the current quiz attempt object.\n\t */\n\tpublic function start() {\n\n\t\t$this->set( 'start_date', current_time( 'mysql' ) );\n\t\t$this->save();\n\t\treturn $this;\n\t}\n\n\t/**\n\t * Retrieve the private data array\n\t *\n\t * @since 3.9.0\n\t *\n\t * @return array\n\t */\n\tpublic function to_array() {\n\t\treturn $this->data;\n\t}\n\n\t/**\n\t * Delete the object from the database.\n\t *\n\t * Overrides the parent method to perform other actions before deletion.\n\t *\n\t * @since 4.2.0\n\t *\n\t * @return bool `true` on success, `false` otherwise.\n\t */\n\tpublic function delete() {\n\n\t\tif ( ! $this->id ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$lesson = llms_get_post( $this->get( 'lesson_id' ) );\n\n\t\t// No lesson, or lesson incomplete, nothing special to do here.\n\t\tif ( ! $lesson || ! ( $lesson instanceof LLMS_Lesson ) || ! llms_is_complete( $this->get( 'student_id' ), $this->get( 'lesson_id' ), 'lesson' ) ) {\n\t\t\treturn parent::delete();\n\t\t}\n\n\t\t/**\n\t\t * Prepare the query args to retrieve at least another sibling attempt,\n\t\t * excluding the current one.\n\t\t */\n\t\t$sibling_query_args = array(\n\t\t\t'exclude'  => $this->get_id( 'id' ),\n\t\t\t'per_page' => 1,\n\t\t);\n\n\t\t/**\n\t\t * If this lesson requires a passing grade, then retrieve only the possible passed sibling\n\t\t * that might have been triggered the lesson completion.\n\t\t */\n\t\tif ( llms_parse_bool( $lesson->get( 'require_passing_grade' ) ) ) {\n\t\t\t$sibling_query_args['status'] = array(\n\t\t\t\t'pass',\n\t\t\t);\n\t\t}\n\n\t\t$sibling_attempts = $this->get_siblings( $sibling_query_args, 'ids' );\n\n\t\t// If this is the only one relevant left attempt.\n\t\tif ( empty( $sibling_attempts ) ) {\n\t\t\tllms_mark_incomplete(\n\t\t\t\t$this->get( 'student_id' ),\n\t\t\t\t$this->get( 'lesson_id' ),\n\t\t\t\t'lesson',\n\t\t\t\t'quiz_' . $this->get( 'quiz_id' )\n\t\t\t);\n\t\t}\n\n\t\treturn parent::delete();\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.quiz.attempt.question.php",
    "content": "<?php\n/**\n * Quiz Attempt Answer Question\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.16.0\n * @version 5.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Quiz_Attempt_Question model class\n *\n * @since 3.16.0\n * @since 3.16.15 Unknown.\n */\nclass LLMS_Quiz_Attempt_Question {\n\n\tprivate $data = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param array $data Question data array from attempt record.\n\t * @return void\n\t */\n\tpublic function __construct( $data = array() ) {\n\n\t\t$this->data = wp_parse_args(\n\t\t\t$data,\n\t\t\tarray(\n\t\t\t\t'id'      => null,\n\t\t\t\t'earned'  => null,\n\t\t\t\t'points'  => null,\n\t\t\t\t'remarks' => null,\n\t\t\t\t'answer'  => null,\n\t\t\t\t'correct' => null,\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Determine if it's possible to manually grade the question\n\t *\n\t * @since 3.16.8\n\t * @since 3.16.9 Unknown.\n\t * @since 5.3.0 Early bail for deleted questions.\n\t *\n\t * @return boolean\n\t */\n\tpublic function can_be_manually_graded() {\n\n\t\t$question = $this->get_question();\n\n\t\tif ( $question && $this->get( 'points' ) >= 1 ) {\n\n\t\t\t// The question is auto-gradable so it cannot be manually graded.\n\t\t\tif ( $question->get_auto_grade_type() ) {\n\t\t\t\treturn false;\n\t\t\t} elseif ( $question->supports( 'grading', 'manual' ) || $question->supports( 'grading', 'conditional' ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Getter\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $key     Data key name\n\t * @param mixed  $default Optional. Default fallback value if key is unset. Default is empty string.\n\t * @return mixed\n\t */\n\tpublic function get( $key, $default = '' ) {\n\t\tif ( isset( $this->data[ $key ] ) ) {\n\t\t\treturn $this->data[ $key ];\n\t\t}\n\t\treturn $default;\n\t}\n\n\t/**\n\t * Retrieve answer HTML for the question answers\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.15 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_answer() {\n\n\t\t$question = $this->get_question();\n\t\t$answers  = $this->get_answer_array();\n\t\t$ret      = apply_filters( 'llms_quiz_attempt_question_get_answer_pre', '', $answers, $question, $this );\n\n\t\tif ( ! $ret ) {\n\n\t\t\tif ( $answers ) {\n\n\t\t\t\t$ret = '<ul class=\"llms-quiz-attempt-answers\">';\n\t\t\t\tforeach ( $answers as $answer ) {\n\t\t\t\t\t$ret .= sprintf( '<li class=\"llms-quiz-attempt-answer\">%s</li>', wp_kses_post( $answer ) );\n\t\t\t\t}\n\t\t\t\t$ret .= '</ul>';\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_quiz_attempt_question_get_answer', $ret, $answers, $question, $this );\n\n\t}\n\n\t/**\n\t * Get answer(s) as an array\n\t *\n\t * @since 3.16.15\n\t * @since 3.27.0 Unknown.\n\t *\n\t * @return array\n\t */\n\tpublic function get_answer_array() {\n\n\t\t$ret      = array();\n\t\t$question = $this->get_question();\n\t\t$answers  = $this->get( 'answer' );\n\n\t\tif ( $answers ) {\n\n\t\t\tif ( $question->supports( 'choices' ) && $question->supports( 'grading', 'auto' ) ) {\n\n\t\t\t\tforeach ( $answers as $aid ) {\n\n\t\t\t\t\t$choice = $question->get_choice( $aid );\n\t\t\t\t\t$ret[]  = $choice ? $choice->get_choice() : _x( '[Deleted]', 'Selected quiz choice has been deleted.', 'lifterlms' );\n\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t$ret = $answers;\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_quiz_attempt_question_get_answer_array', $ret, $answers, $question, $this );\n\n\t}\n\n\t/**\n\t * Retrieve answer HTML for the question correct answers\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.15 Unknown.\n\t *\n\t * @return string\n\t */\n\tpublic function get_correct_answer() {\n\n\t\t$ret     = '';\n\t\t$answers = $this->get_correct_answer_array();\n\n\t\tif ( $answers ) {\n\n\t\t\t$ret = '<ul class=\"llms-quiz-attempt-answers\">';\n\t\t\tforeach ( $answers as $answer ) {\n\t\t\t\t$ret .= sprintf( '<li class=\"llms-quiz-attempt-answer\">%s</li>', wp_kses_post( $answer ) );\n\t\t\t}\n\t\t\t$ret .= '</ul>';\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_quiz_attempt_question_get_correct_answer', $ret, $answers, $this->get_question(), $this );\n\n\t}\n\n\t/**\n\t * Get correct answer(s) as an array\n\t *\n\t * @since 3.16.15\n\t *\n\t * @return array\n\t */\n\tpublic function get_correct_answer_array() {\n\n\t\t$ret      = array();\n\t\t$question = $this->get_question();\n\t\t$type     = $question->get_auto_grade_type();\n\n\t\tif ( 'choices' === $type ) {\n\n\t\t\tforeach ( $question->get_correct_choice() as $aid ) {\n\t\t\t\t$choice = $question->get_choice( $aid );\n\t\t\t\t$ret[]  = $choice->get_choice();\n\t\t\t}\n\t\t} elseif ( 'conditional' === $type ) {\n\n\t\t\t$ret = $question->get_conditional_correct_value();\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_quiz_attempt_question_get_correct_answer_array', $ret, $question, $this );\n\n\t}\n\n\t/**\n\t * Retrieve an instance of the LLMS_Question\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return LLMS_Question\n\t */\n\tpublic function get_question() {\n\t\treturn llms_get_post( $this->get( 'id' ) );\n\t}\n\n\t/**\n\t * Retrieve the status icon HTML based on the question's status/answer\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_status_icon() {\n\n\t\t$icon = '';\n\n\t\tswitch ( $this->get_status() ) {\n\n\t\t\tcase 'graded':\n\t\t\t\tif ( $this->is_correct() ) {\n\t\t\t\t\t$icon = 'check';\n\t\t\t\t\t$tip  = esc_attr__( 'Correct answer', 'lifterlms' );\n\t\t\t\t} else {\n\t\t\t\t\t$icon = 'times';\n\t\t\t\t\t$tip  = esc_attr__( 'Incorrect answer', 'lifterlms' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase 'waiting':\n\t\t\t\t$icon = 'clock-o';\n\t\t\t\t$tip  = esc_attr__( 'Awaiting review', 'lifterlms' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\tif ( $icon ) {\n\t\t\treturn sprintf( '<span class=\"llms-status-icon-tip tip--top-left\" data-tip=\"%1$s\"><i class=\"llms-status-icon fa fa-%2$s\"></i><span>', $tip, $icon );\n\t\t}\n\n\t\treturn '';\n\n\t}\n\n\t/**\n\t * Receive the graded status of the question\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.9 Unknown.\n\t * @since 5.3.0 Account for deleted questions.\n\t *\n\t * @return string Attempt's question status [graded|waiting|none].\n\t */\n\tpublic function get_status() {\n\n\t\t$question = $this->get_question();\n\n\t\tif ( ! $question ) {\n\t\t\treturn 'graded';\n\t\t}\n\n\t\t$status = 'none';\n\n\t\tif ( $this->get( 'points' ) >= 1 ) {\n\n\t\t\tif ( $question->get_auto_grade_type() ) {\n\n\t\t\t\t$status = 'graded';\n\n\t\t\t} elseif ( $question->supports( 'grading', 'manual' ) || $question->supports( 'grading', 'conditional' ) ) {\n\n\t\t\t\tif ( ! $this->get( 'correct' ) ) {\n\t\t\t\t\t$status = 'waiting';\n\t\t\t\t} else {\n\t\t\t\t\t$status = 'graded';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn $status;\n\t}\n\n\t/**\n\t * Determine if remarks are available for the question\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return bool\n\t */\n\tpublic function has_remarks() {\n\n\t\treturn ( $this->get( 'remarks' ) );\n\n\t}\n\n\t/**\n\t * Determine if a question is correct\n\t *\n\t * @since 3.16.8\n\t *\n\t * @return bool\n\t */\n\tpublic function is_correct() {\n\n\t\tif ( 'graded' === $this->get_status() ) {\n\t\t\treturn llms_parse_bool( $this->get( 'correct' ) );\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Setter\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $key Data key name.\n\t * @param mixed  $val Value.\n\t * @return void\n\t */\n\tpublic function set( $key, $val ) {\n\t\t$this->data[ $key ] = $val;\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.quiz.php",
    "content": "<?php\n/**\n * LifterLMS Quiz Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.3.0\n * @version 7.6.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Quiz model class.\n *\n * @property $allowed_attempts (int) Number of times a student is allowed to take the quiz before being locked out of it.\n * @property $passing_percent (float) Grade required for a student to \"pass\" the quiz.\n * @property $random_questions (yesno) Whether or not to randomize the order of questions for each attempt.\n * @property $show_correct_answer (yesno) Whether or not to show the correct answer(s) to students on the quiz results screen.\n * @property $show_options_description_right_answer (yesno) If yes, displays the question description when the student chooses the correct answer.\n * @property $show_options_description_wrong_answer (yesno) If yes, displays the question description when the student chooses the wrong answer.\n * @property $show_results (yesno) If yes, results will be shown to the student at the conclusion of the quiz.\n * @property $time_limit (int) Quiz time limit (in minutes), empty denotes unlimited (untimed) quiz.\n * @property $can_be_resumed (yesno) If yes, the latest incomplete quiz attempt can be resumed.\n *\n * @since 3.3.0\n * @since 3.19.2 Unkwnown.\n * @since 3.37.2 Added `llms_quiz_is_open` filter hook.\n * @since 3.38.0 Only add theme metadata to the quiz array when the `llms_get_quiz_theme_settings` filter is being used.\n * @since 4.0.0 Remove deprecated methods.\n * @since 4.2.0 Added a parameter to the `is_orphan()` method to deeply check the quiz is not really attached to any lesson.\n * @since 5.0.0 Remove previously deprecated method `LLMS_Quiz::get_lessons()`.\n */\nclass LLMS_Quiz extends LLMS_Post_Model {\n\n\t/**\n\t * Post Type Database name (as registered via `register_post_type()`).\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_quiz';\n\n\t/**\n\t * Post type name (without prefix).\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'quiz';\n\n\t/**\n\t * Post type meta properties.\n\t *\n\t * Array key is the meta_key and array values is property's type.\n\t *\n\t * @since Unknown.\n\t * @since 7.6.2 Added the `disable_retake` property.\n\t * @since 7.8.0 Added `can_be_resumed` property.\n\t * @var string[] Array key is the meta_key and array values is property's type.\n\t */\n\tprotected $properties = array(\n\t\t'lesson_id'           => 'absint',\n\t\t'allowed_attempts'    => 'int',\n\t\t'limit_attempts'      => 'yesno',\n\t\t'limit_time'          => 'yesno',\n\t\t'passing_percent'     => 'float',\n\t\t'random_questions'    => 'yesno',\n\t\t'show_correct_answer' => 'yesno',\n\t\t'time_limit'          => 'int',\n\t\t'can_be_resumed'      => 'yesno',\n\t\t'disable_retake'      => 'yesno',\n\t);\n\n\t/**\n\t * Retrieve the LLMS_Course for the quiz.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return LLMS_Course|false\n\t */\n\tpublic function get_course() {\n\t\t$lesson = $this->get_lesson();\n\t\tif ( $lesson ) {\n\t\t\treturn $lesson->get_course();\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve LLMS_Lesson for the quiz's parent lesson.\n\t *\n\t * @since 3.16.0\n\t * @since 3.16.12 Unknown.\n\t *\n\t * @return LLMS_Lesson|false|null The lesson object on success, `false` if no id stored, and `null` if the stored ID doesn't exist.\n\t */\n\tpublic function get_lesson() {\n\t\t$id = $this->get( 'lesson_id' );\n\t\tif ( ! $id ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn llms_get_post( $id );\n\t}\n\n\t/**\n\t * Retrieve the quizzes child questions.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @param string $return Optional. Type of return [ids|posts|questions]. Default `'questions'`.\n\t * @return array\n\t */\n\tpublic function get_questions( $return = 'questions' ) {\n\t\treturn $this->questions()->get_questions( $return );\n\t}\n\n\t/**\n\t * Get questions count.\n\t *\n\t * @since 7.4.0\n\t *\n\t * @return int Question Count.\n\t */\n\tpublic function get_questions_count() {\n\n\t\t/**\n\t\t * Filter the count of questions in a quiz.\n\t\t *\n\t\t * @since 7.4.0\n\t\t *\n\t\t * @param int       $questions_count Number of questions in a quiz.\n\t\t * @param LLMS_Quiz $quiz            Current quiz object.\n\t\t */\n\t\treturn apply_filters( 'llms_quiz_questions_count', count( $this->get_questions( 'ids' ) ), $this );\n\t}\n\n\t/**\n\t * Retrieve the time limit formatted as a human readable string.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_time_limit_string() {\n\t\treturn LLMS_Date::convert_to_hours_minutes_string( $this->get( 'time_limit' ) );\n\t}\n\n\t/**\n\t * Determine if the quiz defines limited attempts.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return bool\n\t */\n\tpublic function has_attempt_limit() {\n\t\treturn ( 'yes' === $this->get( 'limit_attempts' ) );\n\t}\n\n\t/**\n\t * Determine if a time limit is enabled for the quiz.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return bool\n\t */\n\tpublic function has_time_limit() {\n\t\treturn ( 'yes' === $this->get( 'limit_time' ) );\n\t}\n\n\t/**\n\t * Determine if the quiz is an orphan.\n\t *\n\t * @since 3.16.12\n\t * @since 4.2.0 Added the $deep parameter.\n\t *\n\t * @param bool $deep Optional. Whether or not deeply check this quiz is orphan. Default `false`.\n\t *                   When set to true will ensure not only that this quiz as a `lesson_id` property set\n\t *                   But also that the lesson with id `lesson_id` has a `quiz` property as equal as this quiz id.\n\t * @return bool\n\t */\n\tpublic function is_orphan( $deep = false ) {\n\n\t\t$parent_id = $this->get( 'lesson_id' );\n\n\t\tif ( ! $parent_id ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t/**\n\t\t * This is to take into account possible data inconsistency.\n\t\t *\n\t\t * @link https://github.com/gocodebox/lifterlms/issues/1039\n\t\t */\n\t\tif ( $deep ) {\n\t\t\t$lesson = llms_get_post( $parent_id );\n\t\t\t// Both the ids are already absint, see LLMS_Post_Model::___get().\n\t\t\tif ( ! $lesson || $this->get( 'id' ) !== $lesson->get( 'quiz' ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determine if a quiz can be resumed.\n\t *\n\t * A quiz can only be resumed if it's set to be resumed\n\t * and has no time limit.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @return bool\n\t */\n\tpublic function can_be_resumed() {\n\n\t\treturn llms_parse_bool( $this->get( 'can_be_resumed' ) ) && ! $this->has_time_limit();\n\t}\n\n\t/**\n\t * Determine if a student can resume the quiz.\n\t *\n\t * A student can resume the quiz only if their latest attempt can be resumed.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param int $user_id Optional. WP User ID, none supplied uses current user. Default `null`.\n\t * @return bool\n\t */\n\tpublic function can_be_resumed_by_student( $user_id = null ) {\n\n\t\t$can_be_resumed_by_student = false;\n\n\t\t$student = llms_get_student( $user_id );\n\t\tif ( $student ) {\n\t\t\t$last_attempt              = $student->quizzes()->get_last_attempt( $this->get( 'id' ) );\n\t\t\t$can_be_resumed_by_student = $last_attempt && $last_attempt->can_be_resumed();\n\t\t}\n\n\t\treturn $can_be_resumed_by_student;\n\t}\n\n\t/**\n\t * Gets quiz's last attempt key of a user.\n\t *\n\t * @since 7.8.0\n\t *\n\t * @param int $user_id Optional. WP User ID, none supplied uses current user. Default `null`.\n\t * @return string|bool\n\t */\n\tpublic function get_student_last_attempt_key( $user_id = null ) {\n\n\t\t$student          = llms_get_student( $user_id );\n\t\t$last_attempt_key = false;\n\n\t\tif ( $student ) {\n\t\t\t$last_attempt = $student->quizzes()->get_last_attempt( $this->get( 'id' ) );\n\t\t\tif ( $last_attempt ) {\n\t\t\t\t$last_attempt_key = $last_attempt->get_key();\n\t\t\t}\n\t\t}\n\n\t\treturn $last_attempt_key;\n\t}\n\n\t/**\n\t * Determine if a student can take the quiz.\n\t *\n\t * @since 3.0.0\n\t * @since 3.16.0 Unkwnown.\n\t * @since 3.37.2 Added `llms_quiz_is_open` filter hook.\n\t *\n\t * @param int $user_id Optional. WP User ID, none supplied uses current user. Default `null`.\n\t * @return boolean\n\t */\n\tpublic function is_open( $user_id = null ) {\n\n\t\t$student = llms_get_student( $user_id );\n\t\tif ( ! $student ) {\n\t\t\t$quiz_open = false;\n\t\t} else {\n\n\t\t\t$remaining = $student->quizzes()->get_attempts_remaining_for_quiz( $this->get( 'id' ) );\n\n\t\t\t// string for \"unlimited\" or number of attempts.\n\t\t\t$quiz_open = ! is_numeric( $remaining ) || $remaining > 0;\n\n\t\t\t// Check for a passed attempt and disable the quiz.\n\t\t\tif ( $quiz_open && llms_parse_bool( $this->get( 'disable_retake' ) ) ) {\n\t\t\t\t$passed_attempts = $student->quizzes()->get_attempts_by_quiz(\n\t\t\t\t\t$this->get( 'id' ),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'status' => array( 'pass' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\tif ( count( $passed_attempts ) ) {\n\t\t\t\t\t$quiz_open = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters whether the quiz is open to a student or not.\n\t\t *\n\t\t * @param boolean            $quiz_open Whether the quiz is open.\n\t\t * @param int|null           $user_id   WP User ID, can be `null`.\n\t\t * @param int                $quiz_id   The Quiz id.\n\t\t * @param LLMS_Quiz          $quiz      The LLMS_Quiz instance.\n\t\t * @param LLMS_Student|false $student   LLMS_Student instance or false if user not found.\n\t\t */\n\t\treturn apply_filters( 'llms_quiz_is_open', $quiz_open, $user_id, $this->get( 'id' ), $this, $student );\n\t}\n\n\t/**\n\t * Retrieve an instance of the question manager for the quiz.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return LLMS_Question_Manager\n\t */\n\tpublic function questions() {\n\t\treturn new LLMS_Question_Manager( $this );\n\t}\n\n\t/**\n\t * Called before data is sorted and returned by $this->toArray().\n\t * Extending classes should override this data if custom data should\n\t * be added when object is converted to an array or json.\n\t *\n\t * @since 3.3.0\n\t * @since 3.19.2 Unknown.\n\t * @since 3.38.0 Only add theme metadata to the quiz array when the `llms_get_quiz_theme_settings` filter is being used.\n\t *\n\t * @param array $arr Array of data to be serialized.\n\t * @return array\n\t */\n\tprotected function toArrayAfter( $arr ) {\n\n\t\t$arr['questions'] = array();\n\n\t\t// Builder lazy loads questions via ajax.\n\t\tglobal $llms_builder_lazy_load;\n\t\tif ( ! $llms_builder_lazy_load ) {\n\t\t\tforeach ( $this->get_questions() as $question ) {\n\t\t\t\t$arr['questions'][] = $question->toArray();\n\t\t\t}\n\t\t}\n\n\t\t// If theme has legacy support quiz layouts, add theme metadata to the array.\n\t\tif ( get_theme_support( 'lifterlms-quizzes' ) && has_filter( 'llms_get_quiz_theme_settings' ) ) {\n\t\t\t$layout = llms_get_quiz_theme_setting( 'layout' );\n\t\t\tif ( $layout ) {\n\t\t\t\t$arr[ $layout['id'] ] = get_post_meta( $this->get( 'id' ), $layout['id'], true );\n\t\t\t}\n\t\t}\n\n\t\treturn $arr;\n\t}\n\n\t/**\n\t * Get the (points) value of a question.\n\t *\n\t * @since 3.3.0\n\t * @since 3.37.2 Use strict comparison '===' in place of '=='.\n\t *\n\t * @param int $question_id  WP Post ID of the LLMS_Question.\n\t * @return int\n\t */\n\tpublic function get_question_value( $question_id ) {\n\n\t\tforeach ( $this->get_questions_raw() as $q ) {\n\t\t\tif ( $question_id === $q['id'] ) {\n\t\t\t\treturn absint( $q['points'] );\n\t\t\t}\n\t\t}\n\n\t\treturn 0;\n\t}\n\n\t/**\n\t * Retrieve the array of raw question data from the postmeta table.\n\t *\n\t * @since 3.3.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_questions_raw() {\n\n\t\t$q = get_post_meta( $this->get( 'id' ), $this->meta_prefix . 'questions', true );\n\t\treturn $q ? $q : array();\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.section.php",
    "content": "<?php\n/**\n * LLMS Section Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Section model class\n *\n * @since 1.0.0\n * @since 4.0.0 Remove deprecated class methods.\n * @since 5.7.0 Informed developers about the deprecated `LLMS_Section::get_next_available_lesson_order()` method.\n *              Informed developers about the deprecated `LLMS_Section::get_order()` method.\n *              Informed developers about the deprecated `LLMS_Section::get_parent_course()` method.\n *              Informed developers about the deprecated `LLMS_Section::set_parent_course()` method.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Section::get_next_available_lesson_order()` method\n *              - `LLMS_Section::get_order()` method\n *              - `LLMS_Section::get_parent_course()` method\n *              - `LLMS_Section::set_parent_course()` method\n *\n * @property int    $order         The section's order within its parent course.\n * @property int    $parent_course The WP_Post ID of the section's parent course.\n * @property string $title         The title / display name of the section.\n */\nclass LLMS_Section extends LLMS_Post_Model {\n\n\t/**\n\t * Post model properties.\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\t\t'order'         => 'absint',\n\t\t'parent_course' => 'absint',\n\t);\n\n\t/**\n\t * Database post type name.\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'section';\n\n\t/**\n\t * Model post type name.\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'section';\n\n\t/**\n\t * Retrieve the total number of elements in the section\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return int\n\t */\n\tpublic function count_elements() {\n\t\treturn count( $this->get_lessons( 'ids' ) );\n\t}\n\n\t/**\n\t * Retrieve an instance of LLMS_Course for the sections's parent course\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return LLMS_Course|null|false Course object, `null` if `get_post()` fails, or `false` if LLMS_Course class isn't found.\n\t */\n\tpublic function get_course() {\n\t\treturn llms_get_post( $this->get( 'parent_course' ) );\n\t}\n\n\t/**\n\t * An array of default arguments to pass to $this->create() when creating a new section\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param array $args Data to be passed to `wp_insert_post()`.\n\t * @return array\n\t */\n\tprotected function get_creation_args( $args = null ) {\n\n\t\t// Allow nothing to be passed in.\n\t\tif ( empty( $args ) ) {\n\t\t\t$args = array();\n\t\t}\n\n\t\t// Backwards compat to original 3.0.0 format when just a title was passed in.\n\t\tif ( is_string( $args ) ) {\n\t\t\t$args = array(\n\t\t\t\t'post_title' => $args,\n\t\t\t);\n\t\t}\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'comment_status' => 'closed',\n\t\t\t\t'ping_status'    => 'closed',\n\t\t\t\t'post_author'    => get_current_user_id(),\n\t\t\t\t'post_content'   => '',\n\t\t\t\t'post_excerpt'   => '',\n\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t'post_title'     => '',\n\t\t\t\t'post_type'      => $this->get( 'db_post_type' ),\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Filter arguments used to create a new section post\n\t\t *\n\t\t * @since 4.11.0\n\t\t *\n\t\t * @param array        $args    Data to be passed to `wp_insert_post()`.\n\t\t * @param LLMS_Section $section Instance of the section object.\n\t\t */\n\t\treturn apply_filters( 'llms_section_get_creation_args', $args, $this );\n\n\t}\n\n\t/**\n\t * Retrieve the previous section\n\t *\n\t * @since 3.13.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @return LLMS_Section|false\n\t */\n\tpublic function get_next() {\n\n\t\t$siblings = $this->get_siblings( 'ids' );\n\t\t$index    = array_search( $this->get( 'id' ), $siblings );\n\n\t\t/**\n\t\t * The `$index` var will be false if the current section isn't found and\n\t\t * will equal the length of the array if it's the last one (and there is no next).\n\t\t */\n\t\tif ( false === $index || count( $siblings ) - 1 === $index ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn llms_get_post( $siblings[ $index + 1 ] );\n\n\t}\n\n\t/**\n\t * Retrieve section completion percentage\n\t *\n\t * @since 3.24.0\n\t *\n\t * @see LLMS_Student::get_progress()\n\t *\n\t * @param string $user_id   Optional. WP_User ID, if none supplied uses current user (if exists). Default is empty string.\n\t * @param bool   $use_cache Optional. When true, uses results from from the wp object cache (if available). Default is `false`.\n\t * @return float\n\t */\n\tpublic function get_percent_complete( $user_id = '', $use_cache = true ) {\n\n\t\t$student = llms_get_student( $user_id );\n\t\tif ( ! $student ) {\n\t\t\t/** This filter is documented in includes/models/model.llms.student.php */\n\t\t\treturn apply_filters( 'llms_student_get_progress', 0, $this->get( 'id' ), 'section', $user_id );\n\t\t}\n\t\treturn $student->get_progress( $this->get( 'id' ), 'section', $use_cache );\n\n\t}\n\n\t/**\n\t * Retrieve the previous section\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return LLMS_Section|false\n\t */\n\tpublic function get_previous() {\n\n\t\t$siblings = $this->get_siblings( 'ids' );\n\t\t$index    = array_search( $this->get( 'id' ), $siblings );\n\n\t\t/**\n\t\t * The `$index` var will be `0` if we're on the first section and\n\t\t * will be `false` if the current section isn't found.\n\t\t */\n\t\tif ( $index ) {\n\t\t\treturn llms_get_post( $siblings[ $index - 1 ] );\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Get all lessons in the section\n\t *\n\t * @since 3.3.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $return Optional. Type of return [ids|posts|lessons]. Default is `lessons`.\n\t * @return int[]|WP_Post[]|LLMS_Lesson[] Return ty depends on value of `$return` argument.\n\t */\n\tpublic function get_lessons( $return = 'lessons' ) {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'meta_key'       => '_llms_order',\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_parent_section',\n\t\t\t\t\t\t'value' => $this->get( 'id' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'order'          => 'ASC',\n\t\t\t\t'orderby'        => 'meta_value_num',\n\t\t\t\t'post_type'      => 'lesson',\n\t\t\t\t'posts_per_page' => 500,\n\t\t\t)\n\t\t);\n\n\t\tif ( 'ids' === $return ) {\n\t\t\t$ret = wp_list_pluck( $query->posts, 'ID' );\n\t\t} elseif ( 'posts' === $return ) {\n\t\t\t$ret = $query->posts;\n\t\t} else {\n\t\t\t$ret = array_map( 'llms_get_post', $query->posts );\n\t\t}\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Get sibling sections\n\t *\n\t * @since 3.13.0\n\t *\n\t * @param string $return Optional. Type of return [ids|posts|sections]. Default is `sections`.\n\t * @return int[]|WP_Post[]|LLMS_Section[] Return type depends on value of `$return` argument.\n\t */\n\tpublic function get_siblings( $return = 'sections' ) {\n\t\t$course = $this->get_course();\n\t\treturn $course->get_sections( $return );\n\t}\n\n\t/**\n\t * Add data to the course model when converted to array\n\t *\n\t * Called before data is sorted and returned by $this->jsonSerialize().\n\t *\n\t * @since 3.3.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param array $arr Data to be serialized.\n\t * @return array\n\t */\n\tpublic function toArrayAfter( $arr ) {\n\n\t\t$arr['lessons'] = array();\n\n\t\tforeach ( $this->get_lessons() as $lesson ) {\n\t\t\t$arr['lessons'][] = $lesson->toArray();\n\t\t}\n\n\t\treturn $arr;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.student.php",
    "content": "<?php\n/**\n * Student Model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 2.2.3\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Student model class\n *\n * Manages data and interactions with a LifterLMS Student.\n *\n * @since 2.2.3\n * @since 3.33.0 Added the `delete_student_enrollment` public method that allows student's enrollment unrollment and deletion.\n * @since 3.33.0 Added the `delete_enrollment_postmeta` private method that allows student's enrollment postmeta deletion.\n * @since 3.34.0 Added new filters for differentiating between enrollment update and creation; Added the ability to check enrollment from a section.\n * @since 3.35.0 Prepare all variables when querying for enrollment date.\n * @since 3.36.2 Added logic to physically remove from the membership level and remove enrollments data on related products, when deleting a membership enrollment.\n * @since 3.37.9 Added filters `llms_user_enrollment_allowed_post_types` & `llms_user_enrollment_status_allowed_post_types` which allow 3rd parties to enroll users into additional post types via core enrollment methods.\n * @since 4.0.0 Remove previously deprecated methods.\n * @since 4.2.0 The `$enrollment_trigger` parameter was added to the `'llms_user_enrollment_deleted'` action hook.\n *              Added new filter to allow customization of object completion data.\n * @since 5.2.0 Changed the date to be relative to the local time zone in `get_registration_date`.\n * @since 6.0.0 Removed the deprecated `llms_user_removed_from_membership_level` action hook from the `LLMS_Student::unenroll()` method.\n * @since 7.5.0 Added the logic to add and remove lesson favorite.\n */\nclass LLMS_Student extends LLMS_Abstract_User_Data {\n\n\tuse LLMS_Trait_Student_Awards;\n\n\t/**\n\t * Retrieve an instance of the LLMS_Instructor model for the current user\n\t *\n\t * @return   LLMS_Instructor|false\n\t * @since    3.14.0\n\t * @version  3.14.0\n\t */\n\tpublic function instructor() {\n\t\tif ( $this->is_instructor() ) {\n\t\t\treturn llms_get_instructor( $this->get_id() );\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve an instance of the student quiz data model\n\t *\n\t * @return   LLMS_Student_Quizzes\n\t * @since    3.9.0\n\t * @version  3.9.0\n\t */\n\tpublic function quizzes() {\n\t\treturn new LLMS_Student_Quizzes( $this->get_id() );\n\t}\n\n\t/**\n\t * Add the student to a LifterLMS Membership\n\t *\n\t * @param int $membership_id   WP Post ID of the membership\n\t * @return  void\n\t *\n\t * @since  2.2.3\n\t */\n\tprivate function add_membership_level( $membership_id ) {\n\n\t\t// Add the user to the membership level.\n\t\t$membership_levels = $this->get_membership_levels();\n\t\tarray_push( $membership_levels, $membership_id );\n\t\tupdate_user_meta( $this->get_id(), '_llms_restricted_levels', $membership_levels );\n\n\t\t// If there's auto-enroll courses, enroll the user in those courses.\n\t\t$autoenroll_courses = get_post_meta( $membership_id, '_llms_auto_enroll', true );\n\t\tif ( $autoenroll_courses ) {\n\n\t\t\tforeach ( $autoenroll_courses as $course_id ) {\n\n\t\t\t\t$this->enroll( $course_id, 'membership_' . $membership_id );\n\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Enroll the student into a course or membership\n\t *\n\t * @since 2.2.3\n\t * @since 3.17.0 Unknown.\n\t * @since 3.34.0 Added new actions to differentiate between first-time enrollment and enrollment status updates.\n\t * @since 3.37.9 Added filter `llms_user_enrollment_allowed_post_types` to customize the post types a user can be enrolled into.\n\t * @since 4.4.1 Moved filter `llms_user_enrollment_allowed_post_types` to function `llms_get_enrollable_post_types()`.\n\t *\n\t * @see llms_enroll_student()\n\t *\n\t * @param  int    $product_id WP Post ID of the course or membership\n\t * @param  string $trigger    String describing the reason for enrollment\n\t * @return bool\n\t */\n\tpublic function enroll( $product_id, $trigger = 'unspecified' ) {\n\n\t\t/**\n\t\t * Fires before a user is enrolled into a course or membership.\n\t\t *\n\t\t * @param int $user_id WP User ID.\n\t\t * @param int $product_id WP Post ID of the course or membership.\n\t\t */\n\t\tdo_action( 'before_llms_user_enrollment', $this->get_id(), $product_id );\n\n\t\t// Users can only be enrolled into the following post types.\n\t\tif ( ! in_array( get_post_type( $product_id ), llms_get_enrollable_post_types(), true ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Check enrollment before enrolling to prevent duplicates.\n\t\tif ( llms_is_user_enrolled( $this->get_id(), $product_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// If the student has been previously enrolled, simply update don't run a full enrollment.\n\t\tif ( $this->get_enrollment_status( $product_id, false ) ) {\n\t\t\t$insert      = $this->insert_status_postmeta( $product_id, 'enrolled', $trigger );\n\t\t\t$action_type = 'updated';\n\t\t} else {\n\t\t\t$insert      = $this->insert_enrollment_postmeta( $product_id, $trigger );\n\t\t\t$action_type = 'created';\n\t\t}\n\n\t\t// Add the user postmeta for the enrollment.\n\t\tif ( ! empty( $insert ) ) {\n\n\t\t\t// Update the cache.\n\t\t\t$this->cache_set( sprintf( 'enrollment_status_%d', $product_id ), 'enrolled' );\n\t\t\t$this->cache_delete( sprintf( 'date_enrolled_%d', $product_id ) );\n\t\t\t$this->cache_delete( sprintf( 'date_updated_%d', $product_id ) );\n\n\t\t\t$post_type = str_replace( 'llms_', '', get_post_type( $product_id ) );\n\n\t\t\tif ( 'course' === $post_type ) {\n\n\t\t\t\t/**\n\t\t\t\t * Fires after a user is enrolled in course\n\t\t\t\t *\n\t\t\t\t * @param int $user_id    WP User ID.\n\t\t\t\t * @param int $product_id WP Post ID of the course or membership.\n\t\t\t\t */\n\t\t\t\tdo_action( 'llms_user_enrolled_in_course', $this->get_id(), $product_id );\n\n\t\t\t} elseif ( 'membership' === $post_type ) {\n\n\t\t\t\t$this->add_membership_level( $product_id );\n\n\t\t\t\t/**\n\t\t\t\t * Fires after a user is enrolled in membership\n\t\t\t\t *\n\t\t\t\t * @param int $user_id    WP User ID.\n\t\t\t\t * @param int $product_id WP Post ID of the course or membership.\n\t\t\t\t */\n\t\t\t\tdo_action( 'llms_user_added_to_membership_level', $this->get_id(), $product_id );\n\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Fires after a user's enrollment is created or updated.\n\t\t\t *\n\t\t\t * `$post_type` refers to the type of item the user is enrolled in, either 'course' or 'membership'\n\t\t\t * `$action_type` refers to the type of action taking place, either \"created\" or \"updated\".\n\t\t\t *\n\t\t\t * @param int $user_id WP User ID.\n\t\t\t * @param int $product_id WP Post ID of the course or membership.\n\t\t\t */\n\t\t\tdo_action( \"llms_user_{$post_type}_enrollment_{$action_type}\", $this->get_id(), $product_id );\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic function get_avatar( $size = 96 ) {\n\t\treturn '<span class=\"llms-student-avatar\">' . get_avatar( $this->get_id(), $size, null, $this->get_name() ) . '</span>';\n\t}\n\n\t/**\n\t * Retrieve the order which enrolled a student in a given course or membership.\n\t *\n\t * Retrieves the most recently updated order for the given product.\n\t *\n\t * @since 3.0.0\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n\t *\n\t * @param int $product_id WP Post ID of the LifterLMS Product (course, lesson, or membership)\n\t * @return LLMS_Order|false Instance of the LLMS_Order or false if none found\n\t */\n\tpublic function get_enrollment_order( $product_id ) {\n\n\t\t// If a lesson id was passed in, cascade up to the course for order retrieval.\n\t\tif ( 'lesson' === get_post_type( $product_id ) ) {\n\t\t\t$lesson     = new LLMS_Lesson( $product_id );\n\t\t\t$product_id = $lesson->get( 'parent_course' );\n\t\t}\n\n\t\t// Attempt to locate the order via the enrollment trigger.\n\t\t$trigger = $this->get_enrollment_trigger( $product_id );\n\t\tif ( strpos( $trigger, 'order_' ) !== false ) {\n\n\t\t\t$id = str_replace( array( 'order_', 'wc_' ), '', $trigger );\n\t\t\tif ( is_numeric( $id ) ) {\n\t\t\t\tif ( 'llms_order' === get_post_type( $id ) ) {\n\t\t\t\t\treturn new LLMS_Order( $id );\n\t\t\t\t} else {\n\n\t\t\t\t\treturn get_post( $id );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Couldn't find via enrollment trigger, do a WP_Query.\n\t\t$q = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'order'          => 'DESC',\n\t\t\t\t'orderby'        => 'modified',\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\t'relation' => 'AND',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_user_id',\n\t\t\t\t\t\t'value' => $this->get_id(),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_product_id',\n\t\t\t\t\t\t'value' => $product_id,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'posts_per_page' => 1,\n\t\t\t\t'post_type'      => 'llms_order',\n\t\t\t)\n\t\t);\n\n\t\tif ( $q->have_posts() ) {\n\t\t\treturn new LLMS_Order( $q->posts[0] );\n\t\t}\n\n\t\t// Couldn't find an order, return false.\n\t\treturn false;\n\t}\n\n\t/**\n\t * Retrieve IDs of user's courses based on supplied criteria\n\t *\n\t * @param    array $args   see `get_enrollments`\n\t * @return   array\n\t * @since    3.0.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_courses( $args = array() ) {\n\n\t\treturn $this->get_enrollments( 'course', $args );\n\t}\n\n\t/**\n\t * Retrieve user's favorites based on supplied criteria.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param string $order_by Result set ordering field. Default \"updated_date\".\n\t * @param string $order    Result set order. Default \"DESC\". Accepts \"DESC\" or \"ASC\".\n\t * @param int    $limit    Number of favorites to return. Default is infinite.\n\t * @return bool|array\n\t */\n\tpublic function get_favorites( $order_by = 'updated_date', $order = 'DESC', $limit = -1 ) {\n\n\t\tglobal $wpdb;\n\n\t\t$limit_clause = $limit < 1 ? '' : 'LIMIT 0, ' . intval( $limit );\n\n\t\t$res = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT * FROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\t\t\tWHERE meta_key = %s AND user_id = %d ORDER BY %s %s %s;\",\n\t\t\t\t'_favorite',\n\t\t\t\tget_current_user_id(),\n\t\t\t\t$order_by,\n\t\t\t\t'DESC' === $order ? 'DESC' : 'ASC',\n\t\t\t\t$limit_clause\n\t\t\t)\n\t\t);\n\n\t\treturn empty( $res ) ? false : $res;\n\t}\n\n\t/**\n\t * Retrieve IDs of courses a user has completed\n\t *\n\t * @param  array $args query arguments\n\t *                      @arg int    $limit    number of courses to return\n\t *                      @arg string $orderby  table reference and field to order results by\n\t *                      @arg string $order    result order (DESC, ASC)\n\t *                      @arg int    $skip     number of results to skip for pagination purposes\n\t * @return array        \"courses\" will contain an array of course ids\n\t *                      \"more\" will contain a boolean determining whether or not more courses are available beyond supplied limit/skip criteria\n\t * @since   ??\n\t * @version 3.24.0\n\t */\n\tpublic function get_completed_courses( $args = array() ) {\n\n\t\tglobal $wpdb;\n\n\t\t$args = array_merge(\n\t\t\tarray(\n\t\t\t\t'limit'   => 20,\n\t\t\t\t'orderby' => 'upm.updated_date',\n\t\t\t\t'order'   => 'DESC',\n\t\t\t\t'skip'    => 0,\n\t\t\t),\n\t\t\t$args\n\t\t);\n\n\t\t// Add one to the limit to see if there's pagination.\n\t\t++$args['limit'];\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$q = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT upm.post_id AS id\n\t\t\t FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm\n\t\t\t JOIN {$wpdb->posts} AS p ON p.ID = upm.post_id\n\t\t\t WHERE p.post_type = 'course'\n\t\t\t   AND upm.meta_key = '_is_complete'\n\t\t\t   AND upm.meta_value = 'yes'\n\t\t\t   AND upm.user_id = %d\n\t\t\t ORDER BY {$args['orderby']} {$args['order']}\n\t\t\t LIMIT %d, %d;\n\t\t\t\",\n\t\t\t\tarray(\n\t\t\t\t\t$this->get_id(),\n\t\t\t\t\t$args['skip'],\n\t\t\t\t\t$args['limit'],\n\t\t\t\t)\n\t\t\t),\n\t\t\t'OBJECT_K'\n\t\t); // db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\t$ids  = array_keys( $q );\n\t\t$more = false;\n\n\t\t// If we hit our limit we have too many results, pop the last one.\n\t\tif ( count( $ids ) === $args['limit'] ) {\n\t\t\tarray_pop( $ids );\n\t\t\t$more = true;\n\t\t}\n\n\t\t// Reset args to pass back for pagination.\n\t\t--$args['limit'];\n\n\t\t$r = array(\n\t\t\t'limit'   => $args['limit'],\n\t\t\t'more'    => $more,\n\t\t\t'results' => $ids,\n\t\t\t'skip'    => $args['skip'],\n\t\t);\n\n\t\treturn $r;\n\t}\n\n\t/**\n\t * Get the formatted date when a course or lesson was completed by the student\n\t *\n\t * @param    int    $object_id  WP Post ID of a course or lesson\n\t * @param    string $format     date format as accepted by php date()\n\t * @return   false|string            will return false if the user is not enrolled\n\t * @since    ??\n\t * @version  ??\n\t */\n\tpublic function get_completion_date( $object_id, $format = 'F d, Y' ) {\n\n\t\tglobal $wpdb;\n\n\t\t$q = $wpdb->get_var(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT updated_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_is_complete' AND meta_value = 'yes' AND user_id = %d AND post_id = %d ORDER BY updated_date DESC LIMIT 1\",\n\t\t\t\tarray( $this->get_id(), $object_id )\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t\treturn ( $q ) ? date_i18n( $format, strtotime( $q ) ) : false;\n\t}\n\n\t/**\n\t * Retrieve IDs of user's enrollments by post type (and additional criteria)\n\t *\n\t * @param  string $post_type  name of the post type (course|membership)\n\t * @param  array  $args query arguments\n\t *                      @arg int    $limit    number of courses to return\n\t *                      @arg string $orderby  table reference and field to order results by\n\t *                      @arg string $order    result order (DESC, ASC)\n\t *                      @arg int    $skip     number of results to skip for pagination purposes\n\t *                      @arg string $status   filter results by enrollment status, \"any\", \"enrolled\", \"cancelled\", or \"expired\"\n\t * @return array        \"results\" will contain an array of course ids\n\t *                      \"more\" will contain a boolean determining whether or not more courses are available beyond supplied limit/skip criteria\n\t *                      \"found\" will contain the total possible FOUND_ROWS() for the query\n\t * @since    3.0.0\n\t * @version  3.15.1\n\t */\n\tpublic function get_enrollments( $post_type = 'course', $args = array() ) {\n\n\t\tglobal $wpdb;\n\n\t\t$args = wp_parse_args(\n\t\t\t$args,\n\t\t\tarray(\n\t\t\t\t'limit'   => 20,\n\t\t\t\t'orderby' => 'upm.updated_date',\n\t\t\t\t'order'   => 'DESC',\n\t\t\t\t'skip'    => 0,\n\t\t\t\t'status'  => 'any', // Any, enrolled, cancelled, expired.\n\t\t\t)\n\t\t);\n\n\t\t// Prefix membership.\n\t\tif ( 'membership' === $post_type ) {\n\t\t\t$post_type = 'llms_membership';\n\t\t}\n\n\t\t// Sanitize order & orderby.\n\t\t$args['orderby'] = preg_replace( '/[^a-zA-Z_.]/', '', $args['orderby'] );\n\t\t$args['order']   = preg_replace( '/[^a-zA-Z_.]/', '', $args['order'] );\n\n\t\t// Allow \"short\" orderby's to be passed in without a table reference.\n\t\tswitch ( $args['orderby'] ) {\n\t\t\tcase 'date':\n\t\t\t\t$args['orderby'] = 'upm.updated_date';\n\t\t\t\tbreak;\n\t\t\tcase 'order':\n\t\t\t\t$args['orderby'] = 'p.menu_order';\n\t\t\t\tbreak;\n\t\t\tcase 'title':\n\t\t\t\t$args['orderby'] = 'p.post_title';\n\t\t\t\tbreak;\n\t\t}\n\n\t\t// Prepare additional status AND clauses.\n\t\tif ( 'any' !== $args['status'] ) {\n\t\t\t$status = $wpdb->prepare(\n\t\t\t\t\"\n\t\t\t\tAND upm.meta_value = %s\n\t\t\t\tAND upm.updated_date = (\n\t\t\t\t\tSELECT MAX( upm2.updated_date )\n\t\t\t\t\t  FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm2\n\t\t\t\t\t WHERE upm2.meta_key = '_status'\n\t\t\t\t\t   AND upm2.user_id = %d\n\t\t\t\t\t   AND upm2.post_id = upm.post_id\n\t\t\t\t\t)\",\n\t\t\t\t$args['status'],\n\t\t\t\t$this->get_id()\n\t\t\t);\n\t\t} else {\n\t\t\t$status = '';\n\t\t}\n\n\t\t$from_where = \"FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm\n\t\t\t JOIN {$wpdb->posts} AS p ON p.ID = upm.post_id\n\t\t\t WHERE p.post_type = %s\n\t\t\t   AND p.post_status = 'publish'\n\t\t\t   AND upm.meta_key = '_status'\n\t\t\t   AND upm.user_id = %d\n\t\t\t   {$status}\";\n\n\t\t$prepare_args = array( $post_type, $this->get_id() );\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$found = absint(\n\t\t\t$wpdb->get_var(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"SELECT COUNT(DISTINCT upm.post_id) {$from_where}\",\n\t\t\t\t\t$prepare_args\n\t\t\t\t)\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\n\t\t$query = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT DISTINCT upm.post_id AS id\n\t\t\t {$from_where}\n\t\t\t ORDER BY {$args['orderby']} {$args['order']}\n\t\t\t LIMIT %d, %d;\n\t\t\t\",\n\t\t\t\tarray_merge(\n\t\t\t\t\t$prepare_args,\n\t\t\t\t\tarray( $args['skip'], $args['limit'] )\n\t\t\t\t)\n\t\t\t),\n\t\t\t'OBJECT_K'\n\t\t); // db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\treturn array(\n\t\t\t'found'   => $found,\n\t\t\t'limit'   => $args['limit'],\n\t\t\t'more'    => ( $found > ( ( $args['skip'] / $args['limit'] + 1 ) * $args['limit'] ) ),\n\t\t\t'skip'    => $args['skip'],\n\t\t\t'results' => array_keys( $query ),\n\t\t);\n\t}\n\n\t/**\n\t * Get the formatted date when a user initially enrolled in a product or when they were last updated\n\t *\n\t * @since 3.0.0\n\t * @since 3.35.0 Prepare SQL properly.\n\t *\n\t * @param   int    $product_id  WP Post ID of a course or membership\n\t * @param   string $date        \"enrolled\" will get the most recent start date, \"updated\" will get the most recent status change date\n\t * @param   string $format      date format as accepted by php date(), if none supplied uses the WP core \"date_format\" option\n\t * @return  false|string        will return false if the user is not enrolled\n\t */\n\tpublic function get_enrollment_date( $product_id, $date = 'enrolled', $format = null ) {\n\n\t\tif ( ! $format ) {\n\t\t\t$format = get_option( 'date_format', 'M d, Y' );\n\t\t}\n\n\t\t$cache_key = sprintf( 'date_%1$s_%2$s', $date, $product_id );\n\t\t$res       = $this->cache_get( $cache_key );\n\n\t\tif ( false === $res ) {\n\n\t\t\t$key = ( 'enrolled' === $date ) ? '_start_date' : '_status';\n\n\t\t\tglobal $wpdb;\n\n\t\t\t// Get the oldest recorded Enrollment date.\n\t\t\t$res = $wpdb->get_var(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"SELECT updated_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = %s AND user_id = %d AND post_id = %d ORDER BY updated_date DESC LIMIT 1\",\n\t\t\t\t\tarray( $key, $this->get_id(), $product_id )\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$this->cache_set( $cache_key, $res );\n\n\t\t}\n\n\t\treturn ( $res ) ? date_i18n( $format, strtotime( $res ) ) : false;\n\t}\n\n\t/**\n\t * Get the current enrollment status of a student for a particular product\n\t *\n\t * @since 3.0.0\n\t * @since 3.17.0 Unknown.\n\t * @since 3.37.9 Added filter `llms_user_enrollment_status_allowed_post_types`.\n\t * @since 4.4.1 Moved filter `llms_user_enrollment_status_allowed_post_types` to function `llms_get_enrollable_status_check_post_types()`.\n\t * @since 4.18.0 Added a tie-breaker when there are multiple enrollment statuses with the same date & time.\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n\t *\n\t * @param  int  $product_id  WP Post ID of a Course, Section, Lesson, or Membership\n\t * @param  bool $use_cache   If true, returns cached data if available, if false will run a db query\n\t * @return false|string      When no enrollment status exists, returns `false`. Otherwise returns the\n\t *                           enrollment status as a string.\n\t */\n\tpublic function get_enrollment_status( $product_id, $use_cache = true ) {\n\n\t\t$status       = false;\n\t\t$product_type = get_post_type( $product_id );\n\n\t\tif ( ! in_array( $product_type, llms_get_enrollable_status_check_post_types(), true ) ) {\n\t\t\t/* This filter is documented at the end of this method. */\n\t\t\treturn apply_filters( 'llms_get_enrollment_status', $status, $this->get_id(), $product_id, $use_cache );\n\t\t}\n\n\t\t// Get course ID if we're looking at a lesson or section.\n\t\tif ( in_array( $product_type, array( 'section', 'lesson' ), true ) ) {\n\n\t\t\t$llms_post = llms_get_post( $product_id );\n\t\t\tif ( $llms_post ) {\n\t\t\t\t$product_id = $llms_post->get( 'parent_course' );\n\t\t\t}\n\t\t}\n\n\t\tif ( $use_cache ) {\n\t\t\t$status = $this->cache_get( sprintf( 'enrollment_status_%d', $product_id ) );\n\t\t}\n\n\t\t/**\n\t\t * After checking the cache, $status will be:\n\t\t *     + `false` if there was nothing in the cache or the function was instructed to not use the cache: Query the database to get the status.\n\t\t *     + a string if there was a status: No need to query the database.\n\t\t *     + `null` if there's no status: No need to query the database.\n\t\t */\n\t\tif ( false === $status ) {\n\n\t\t\tglobal $wpdb;\n\n\t\t\t// Get the most recent recorded status.\n\t\t\t$status = $wpdb->get_var(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"SELECT meta_value FROM {$wpdb->prefix}lifterlms_user_postmeta\n\t\t\t\t\t WHERE meta_key = '_status' AND user_id = %d AND post_id = %d\n\t\t\t\t\t ORDER BY updated_date DESC, meta_id DESC LIMIT 1;\",\n\t\t\t\t\tarray( $this->get_id(), $product_id )\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Cache the data: `null` will be stored if the student has no status.\n\t\t\t$this->cache_set( sprintf( 'enrollment_status_%d', $product_id ), $status );\n\n\t\t}\n\n\t\t// Don't return `null` values from the database.\n\t\t$status = $status ? $status : false;\n\n\t\t/**\n\t\t * Filter a user's enrollment status for a specific post.\n\t\t *\n\t\t * Note that if a value is modified by this filter the modified value is *not* cached. Therefore you should\n\t\t * consider implementing caching of your modified value which matches the caching implemented by this method\n\t\t * so that the modified value obeys the default caching behavior.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param false|string $status     When no enrollment status exists, returns `false`. Otherwise returns the\n\t\t *                                     enrollment status as a string.\n\t\t * @param int          $user_id    WP_User ID of the student\n\t\t * @param int          $product_id WP_Post ID of the post used to check the enrollment status.\n\t\t * @param boolean      $use_cache  Whether or not to use the local cache.\n\t\t */\n\t\treturn apply_filters( 'llms_get_enrollment_status', $status, $this->get_id(), $product_id, $use_cache );\n\t}\n\n\t/**\n\t * Get the enrollment trigger for a the student's enrollment in a course\n\t *\n\t * @param    int $product_id   WP Post ID of the course or membership\n\t * @return   string|false\n\t * @since    ??\n\t * @version  3.21.0\n\t */\n\tpublic function get_enrollment_trigger( $product_id ) {\n\n\t\t$trigger = llms_get_user_postmeta( $this->get_id(), $product_id, '_enrollment_trigger', true );\n\t\treturn $trigger ? $trigger : false;\n\t}\n\n\t/**\n\t * Get the enrollment trigger id for a the student's enrollment in a course\n\t *\n\t * @param    int $product_id  WP Post ID of the course or membership\n\t * @return   int|false\n\t * @since    3.0.0\n\t * @version  3.17.2\n\t */\n\tpublic function get_enrollment_trigger_id( $product_id ) {\n\n\t\t$trigger = $this->get_enrollment_trigger( $product_id );\n\t\t$id      = false;\n\t\tif ( $trigger && false !== strpos( $trigger, 'order_' ) ) {\n\t\t\t$trigger_obj = $this->get_enrollment_order( $product_id );\n\t\t\tif ( $trigger_obj instanceof LLMS_Order ) {\n\t\t\t\t$id = $trigger_obj->get( 'id' );\n\t\t\t} elseif ( $trigger_obj instanceof WP_Post ) {\n\t\t\t\t$id = $trigger_obj->ID;\n\t\t\t}\n\t\t} elseif ( $trigger && false !== strpos( $trigger, 'admin_' ) ) {\n\t\t\t$id = absint( str_replace( 'admin_', '', $trigger ) );\n\t\t}\n\t\treturn $id;\n\t}\n\n\t/**\n\t * Retrieve postmeta events related to the student\n\t *\n\t * @param    array $args  default args, see LLMS_Query_User_Postmeta\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_events( $args = array() ) {\n\n\t\t$query = new LLMS_Query_User_Postmeta(\n\t\t\twp_parse_args(\n\t\t\t\t$args,\n\t\t\t\tarray(\n\t\t\t\t\t'types'         => 'all',\n\t\t\t\t\t'per_page'      => 10,\n\t\t\t\t\t'user_id'       => $this->get_id(),\n\t\t\t\t\t'no_found_rows' => true,\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\treturn $query->get_metas();\n\t}\n\n\t/**\n\t * Get the students grade for a lesson / course\n\t * All grades are based on quizzes assigned to lessons\n\t *\n\t * @param    int  $object_id  WP Post ID of a course or lesson\n\t * @param    bool $use_cache  If true, uses cached results\n\t * @return   mixed\n\t * @since    ??\n\t * @version  3.24.0\n\t */\n\tpublic function get_grade( $object_id, $use_cache = true ) {\n\t\t$grade = llms()->grades()->get_grade( $object_id, $this, $use_cache );\n\t\tif ( is_null( $grade ) ) {\n\t\t\t$grade = _x( 'N/A', 'Grade to display when no quizzes taken or available', 'lifterlms' );\n\t\t}\n\t\treturn apply_filters( 'llms_student_get_grade', $grade, $this, $object_id, get_post_type( $object_id ) );\n\t}\n\n\t/**\n\t * Retrieve IDs of user's memberships based on supplied criteria\n\t *\n\t * @param    array $args   see `get_enrollments`\n\t * @return   array\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function get_memberships( $args = array() ) {\n\n\t\treturn $this->get_enrollments( 'membership', $args );\n\t}\n\n\t/**\n\t * Retrieve a user's notification subscription preferences for a given type & trigger\n\t *\n\t * @param    string $type     notification type: email, basic, etc...\n\t * @param    string $trigger  notification trigger: eg purchase_receipt, lesson_complete, etc...\n\t * @param    string $default  value to return if no setting is saved in the db\n\t * @return   string             yes or no\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tpublic function get_notification_subscription( $type, $trigger, $default = 'no' ) {\n\n\t\t$prefs = $this->get( 'notification_subscriptions' );\n\t\tif ( ! $prefs ) {\n\t\t\t$prefs = array();\n\t\t}\n\n\t\tif ( isset( $prefs[ $type ] ) && isset( $prefs[ $type ][ $trigger ] ) ) {\n\t\t\treturn $prefs[ $type ][ $trigger ];\n\t\t}\n\n\t\treturn $default;\n\t}\n\n\t/**\n\t * Retrieve the student's overall grade\n\t *\n\t * Grade = sum of grades for all courses divided by number of enrolled courses\n\t * if a course has no quizzes in it, it cannot be graded and is therefore excluded from the calculation.\n\t *\n\t * Cached data is automatically cleared when a student completes a quiz.\n\t *\n\t * @since 3.2.0\n\t *\n\t * @param boolean $use_cache If `false`, calculates the grade, otherwise utilizes cached data (if available)\n\t * @return float|string Grade as float or \"N/A\"\n\t */\n\tpublic function get_overall_grade( $use_cache = true ) {\n\n\t\t$grade = null;\n\n\t\t// Attempt to pull from the cache first.\n\t\tif ( $use_cache ) {\n\n\t\t\t$grade = $this->get( $this->meta_prefix . 'overall_grade' );\n\n\t\t\tif ( is_numeric( $grade ) ) {\n\t\t\t\t$grade = floatval( $grade );\n\t\t\t}\n\t\t}\n\n\t\t// Cache disabled or no cached data available.\n\t\tif ( ! $use_cache || null === $grade || '' === $grade ) {\n\n\t\t\t$grades = array();\n\n\t\t\t// Get courses.\n\t\t\t$courses = $this->get_courses(\n\t\t\t\tarray(\n\t\t\t\t\t'limit' => 9999,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Loop through courses.\n\t\t\tforeach ( $courses['results'] as $course_id ) {\n\n\t\t\t\t// Get course grade.\n\t\t\t\t$g = $this->get_grade( $course_id );\n\n\t\t\t\t// If an actual grade (not N/A) is returned.\n\t\t\t\tif ( is_numeric( $g ) ) {\n\t\t\t\t\tarray_push( $grades, $g );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If we have at least one grade.\n\t\t\t$count = count( $grades );\n\t\t\tif ( $count ) {\n\n\t\t\t\t$grade = round( array_sum( $grades ) / $count, 2 );\n\n\t\t\t} else {\n\n\t\t\t\t$grade = _x( 'N/A', 'overall grade when no quizzes', 'lifterlms' );\n\n\t\t\t}\n\n\t\t\t// Cache the grade.\n\t\t\t$this->set( 'overall_grade', $grade );\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_student_get_overall_grade', $grade, $this );\n\t}\n\n\t/**\n\t * Retrieve a student's overall progress\n\t * Overall progress is the total percentage completed based on all courses the student is enrolled in\n\t * Cached data is cleared every time the student completes a lesson\n\t *\n\t * @param    boolean $use_cache  if false, calculates the progress, otherwise utilizes cached data (if available)\n\t * @return   float\n\t * @since    3.2.0\n\t * @version  3.2.0\n\t */\n\tpublic function get_overall_progress( $use_cache = true ) {\n\n\t\t$progress = null;\n\n\t\t// Attempt to pull from the cache first.\n\t\tif ( $use_cache ) {\n\n\t\t\t$progress = $this->get( $this->meta_prefix . 'overall_progress' );\n\n\t\t\tif ( is_numeric( $progress ) ) {\n\t\t\t\t$progress = floatval( $progress );\n\t\t\t}\n\t\t}\n\n\t\t// Cache disabled or no cached data available.\n\t\tif ( ! $use_cache || null === $progress || '' === $progress ) {\n\n\t\t\t$progresses = array();\n\n\t\t\t// Get courses.\n\t\t\t$courses = $this->get_courses(\n\t\t\t\tarray(\n\t\t\t\t\t'limit' => 9999,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Loop through courses.\n\t\t\tforeach ( $courses['results'] as $course_id ) {\n\t\t\t\tarray_push( $progresses, $this->get_progress( $course_id, 'course' ) );\n\t\t\t}\n\n\t\t\t$count = count( $progresses );\n\t\t\tif ( $count ) {\n\n\t\t\t\t$progress = round( array_sum( $progresses ) / $count, 2 );\n\n\t\t\t} else {\n\n\t\t\t\t$progress = 0;\n\n\t\t\t}\n\n\t\t\t// Cache the grade.\n\t\t\t$this->set( 'overall_progress', $progress );\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_student_get_overall_progress', $progress, $this );\n\t}\n\n\t/**\n\t * Get the students last completed lesson in a course\n\t *\n\t * @param    int $course_id    WP_Post ID of the course\n\t * @return   int                   WP_Post ID of the lesson or false if no progress has been made\n\t * @since    3.0.0\n\t * @version  3.0.0\n\t */\n\tpublic function get_last_completed_lesson( $course_id ) {\n\n\t\t$course  = new LLMS_Course( $course_id );\n\t\t$lessons = array_reverse( $course->get_lessons( 'ids' ) );\n\n\t\tforeach ( $lessons as $lesson ) {\n\t\t\tif ( $this->is_complete( $lesson, 'lesson' ) ) {\n\t\t\t\treturn $lesson;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic function has_unlimited_quiz_time() {\n\t\t$unlimited = get_user_option( 'llms_allow_unlimited_quiz_time', $this->get_id() );\n\t\treturn ( 'yes' === $unlimited );\n\t}\n\n\t/**\n\t * Retrieve an array of Membership Levels for a user\n\t *\n\t * @return array\n\t * @since   2.2.3\n\t * @version 2.2.3\n\t */\n\tpublic function get_membership_levels() {\n\n\t\t$levels = get_user_meta( $this->get_id(), '_llms_restricted_levels', true );\n\n\t\tif ( empty( $levels ) ) {\n\n\t\t\t$levels = array();\n\n\t\t}\n\n\t\treturn $levels;\n\t}\n\n\t/**\n\t * Get the full name of a student\n\t *\n\t * @return   string\n\t * @since    3.0.4\n\t * @version  3.5.1\n\t */\n\tpublic function get_name() {\n\n\t\t$name = trim( $this->get( 'first_name' ) . ' ' . $this->get( 'last_name' ) );\n\n\t\tif ( ! $name ) {\n\t\t\t$name = $this->display_name;\n\t\t}\n\n\t\treturn apply_filters( 'llms_student_get_name', $name, $this->get_id(), $this );\n\t}\n\n\t/**\n\t * Get the next lesson a student needs to complete in a course\n\t *\n\t * @param    int $course_id    WP_Post ID of the course\n\t * @return   int                   WP_Post ID of the lesson or false if all courses are complete\n\t * @since    3.0.1\n\t * @version  3.0.1\n\t */\n\tpublic function get_next_lesson( $course_id ) {\n\n\t\t$course  = new LLMS_Course( $course_id );\n\t\t$lessons = $course->get_lessons( 'ids' );\n\n\t\tforeach ( $lessons as $lesson ) {\n\t\t\tif ( ! $this->is_complete( $lesson, 'lesson' ) ) {\n\t\t\t\treturn $lesson;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic function get_orders( $params = array() ) {\n\n\t\t$params = wp_parse_args(\n\t\t\t$params,\n\t\t\tarray(\n\n\t\t\t\t'count'    => 25,\n\t\t\t\t'page'     => 1,\n\t\t\t\t'statuses' => array_keys( llms_get_order_statuses() ),\n\n\t\t\t)\n\t\t);\n\n\t\textract( $params );\n\n\t\t$q = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'order'          => 'DESC',\n\t\t\t\t'orderby'        => 'date',\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'   => '_llms_user_id',\n\t\t\t\t\t\t'value' => $this->get_id(),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'paged'          => $page,\n\t\t\t\t'posts_per_page' => $count,\n\t\t\t\t'post_status'    => $statuses,\n\t\t\t\t'post_type'      => 'llms_order',\n\t\t\t)\n\t\t);\n\n\t\t$orders = array();\n\n\t\tif ( $q->have_posts() ) {\n\n\t\t\tforeach ( $q->posts as $post ) {\n\n\t\t\t\t$orders[ $post->ID ] = new LLMS_Order( $post );\n\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\t'count'  => count( $q->posts ),\n\t\t\t'page'   => $page,\n\t\t\t'pages'  => $q->max_num_pages,\n\t\t\t'orders' => $orders,\n\t\t);\n\t}\n\n\t/**\n\t * Get students progress through a course or track\n\t *\n\t * @param    int     $object_id  course or track id\n\t * @param    string  $type       object type [course|course_track|section]\n\t * @param    boolean $use_cache  if true, will use cached data from the usermeta table (if available)\n\t *                               if false, will bypass cached data and recalculate the progress from scratch\n\t * @return   float\n\t * @since    3.0.0\n\t * @version  3.24.0\n\t */\n\tpublic function get_progress( $object_id, $type = 'course', $use_cache = true ) {\n\n\t\t$ret       = 0;\n\t\t$cache_key = sprintf( '%1$s_%2$d_progress', $type, $object_id );\n\t\t$cached    = $use_cache ? $this->get( $cache_key ) : '';\n\n\t\tif ( '' === $cached ) {\n\n\t\t\t$total     = 0;\n\t\t\t$completed = 0;\n\n\t\t\tif ( 'course' === $type ) {\n\n\t\t\t\t$course  = new LLMS_Course( $object_id );\n\t\t\t\t$lessons = $course->get_lessons( 'ids' );\n\t\t\t\t$total   = count( $lessons );\n\t\t\t\tforeach ( $lessons as $lesson ) {\n\t\t\t\t\tif ( $this->is_complete( $lesson, 'lesson' ) ) {\n\t\t\t\t\t\t++$completed;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} elseif ( 'course_track' === $type ) {\n\n\t\t\t\t$track   = new LLMS_Track( $object_id );\n\t\t\t\t$courses = $track->get_courses();\n\t\t\t\t$total   = count( $courses );\n\t\t\t\tforeach ( $courses as $course ) {\n\t\t\t\t\tif ( $this->is_complete( $course->ID, 'course' ) ) {\n\t\t\t\t\t\t++$completed;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} elseif ( 'section' === $type ) {\n\n\t\t\t\t$section = new LLMS_Section( $object_id );\n\t\t\t\t$lessons = $section->get_lessons( 'ids' );\n\t\t\t\t$total   = count( $lessons );\n\t\t\t\tforeach ( $lessons as $lesson ) {\n\t\t\t\t\tif ( $this->is_complete( $lesson, 'lesson' ) ) {\n\t\t\t\t\t\t++$completed;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$ret = ( ! $completed || ! $total ) ? 0 : round( 100 / ( $total / $completed ), 2 );\n\t\t\t$this->set( $cache_key, $ret );\n\n\t\t} else {\n\t\t\t$ret = $cached;\n\t\t}// End if().\n\n\t\t/**\n\t\t * @filter llms_student_get_progress\n\t\t * Filters the return of get_progress method\n\t\t * @param    float   $ret        student's progress\n\t\t * @param    int     $object_id  WP_Post ID of the object\n\t\t * @param    string  $type       object post type [course|course_track|section]\n\t\t * @param    int     $user_id    WP_User ID of the student\n\t\t * @since    unknown\n\t\t * @version  3.24.0\n\t\t */\n\t\treturn apply_filters( 'llms_student_get_progress', $ret, $object_id, $type, $this->get_id() );\n\t}\n\n\t/**\n\t * Retrieve the student's original registration date in the chosen format.\n\t *\n\t * @since Unknown\n\t * @since 5.2.0 Changed the date to be relative to the local time zone.\n\t *\n\t * @param string $format Any date format that can be passed to date().\n\t * @return string\n\t */\n\tpublic function get_registration_date( $format = '' ) {\n\n\t\tif ( ! $format ) {\n\t\t\t$format = get_option( 'date_format' );\n\t\t}\n\n\t\treturn wp_date( $format, strtotime( $this->get( 'user_registered' ) ) );\n\t}\n\n\t/**\n\t * Determine if the student is active in at least one course or membership\n\t *\n\t * @since 3.14.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_active() {\n\n\t\t// Check memberships first, it's a faster query.\n\t\tif ( $this->get_membership_levels() ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Check for at least one enrolled course.\n\t\t$courses = $this->get_courses(\n\t\t\tarray(\n\t\t\t\t'limit'  => 1,\n\t\t\t\t'status' => 'enrolled',\n\t\t\t)\n\t\t);\n\n\t\tif ( $courses['results'] ) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Not active.\n\t\treturn false;\n\t}\n\n\t/**\n\t * Determine if the student has completed a course, track, or lesson\n\t *\n\t * @param    int    $object_id  WP Post ID of a course or lesson or section or the term id of the track\n\t * @param    string $type    Object type (course, lesson, section, or track)\n\t * @return   boolean\n\t * @since    3.0.0\n\t * @version  3.24.0\n\t */\n\tpublic function is_complete( $object_id, $type = 'course' ) {\n\n\t\t// check tracks by progress\n\t\t// this is done because tracks can have the same id as another object...\n\t\t// @todo tracks should have a different table or format since the post_id col won't guarantee uniqueness...\n\t\tif ( 'course_track' === $type ) {\n\n\t\t\t$ret = ( 100 == $this->get_progress( $object_id, $type ) );\n\n\t\t\t// Everything else can be checked on the postmeta table.\n\t\t} else {\n\n\t\t\t$query = new LLMS_Query_User_Postmeta(\n\t\t\t\tarray(\n\t\t\t\t\t'types'                 => 'completion',\n\t\t\t\t\t'include_post_children' => false,\n\t\t\t\t\t'user_id'               => $this->get_id(),\n\t\t\t\t\t'post_id'               => $object_id,\n\t\t\t\t\t'per_page'              => 1,\n\t\t\t\t\t'no_found_rows'         => true,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$ret = $query->has_results();\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_is_' . $type . '_complete', $ret, $object_id, $type, $this );\n\t}\n\n\t/**\n\t * Determine if the student is a LifterLMS Instructor (of any kind)\n\t *\n\t * Can be admin, manager, instructor, assistant.\n\t *\n\t * @return   boolean\n\t * @since    3.14.0\n\t * @version  3.14.0\n\t */\n\tpublic function is_instructor() {\n\t\treturn $this->user->has_cap( 'lifterlms_instructor' );\n\t}\n\n\t/**\n\t * Add student postmeta data for completion of a lesson, section, course or track\n\t *\n\t * @param  int    $object_id    WP Post ID of the lesson, section, course or track\n\t * @param  string $trigger      String describing the reason for mark completion\n\t * @return bool\n\t * @since    3.3.1\n\t * @version  3.21.0\n\t */\n\tprivate function insert_completion_postmeta( $object_id, $trigger = 'unspecified' ) {\n\n\t\t// Add info to the user postmeta table.\n\t\t$user_metadatas = array(\n\t\t\t'_is_complete'        => 'yes',\n\t\t\t'_completion_trigger' => $trigger,\n\t\t);\n\n\t\t$update = llms_bulk_update_user_postmeta( $this->get_id(), $object_id, $user_metadatas, false );\n\n\t\t// Returns an array with errored keys or true on success.\n\t\treturn is_array( $update ) ? false : true;\n\t}\n\n\t/**\n\t * Add student postmeta data for incompletion of a lesson, section, course or track\n\t * An \"_is_complete\" value of \"no\" is inserted into postmeta\n\t *\n\t * @param    int    $object_id    WP Post ID of the lesson, section, course or track\n\t * @param    string $trigger      String describing the reason for mark incompletion\n\t * @return   boolean\n\t * @since    3.5.0\n\t * @version  3.24.0\n\t */\n\tprivate function insert_incompletion_postmeta( $object_id, $trigger = 'unspecified' ) {\n\n\t\tglobal $wpdb;\n\n\t\t// Add '_is_complete' to the user postmeta table for object.\n\t\t$user_metadatas = array(\n\t\t\t'_is_complete'        => 'no',\n\t\t\t'_completion_trigger' => $trigger,\n\t\t);\n\n\t\tforeach ( $user_metadatas as $key => $value ) {\n\n\t\t\t/**\n\t\t\t * It's too difficult to keep track of multiple postmetas for each lesson incomplete\n\t\t\t * Instead, I'm just replacing the old '_is_complete' value with 'no'\n\t\t\t *\n\t\t\t * Lessons that have never been complete will not have an '_is_complete' record,\n\t\t\t * Lessons that were completed will have an '_is_complete' record of 'yes',\n\t\t\t * Lessons that have been completed once but were marked incomplete will have an '_is_complete' record of 'no'\n\t\t\t */\n\t\t\t$update = $wpdb->update(\n\t\t\t\t$wpdb->prefix . 'lifterlms_user_postmeta',\n\t\t\t\tarray(\n\t\t\t\t\t'user_id'      => $this->get_id(),\n\t\t\t\t\t'post_id'      => $object_id,\n\t\t\t\t\t'meta_key'     => $key,\n\t\t\t\t\t'meta_value'   => $value,\n\t\t\t\t\t'updated_date' => current_time( 'mysql' ),\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'user_id'  => $this->get_id(),\n\t\t\t\t\t'post_id'  => $object_id,\n\t\t\t\t\t'meta_key' => $key,\n\t\t\t\t),\n\t\t\t\tarray( '%d', '%d', '%s', '%s', '%s' )\n\t\t\t); // db call ok; no-cache ok.\n\n\t\t\tif ( false === $update ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Add student postmeta data when lesson is favorited.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @see LLMS_Student->mark_favorite()\n\t *\n\t * @param int $object_id WP Post ID of the object to mark/unmark as favorite.\n\t * @return bool\n\t */\n\tprivate function insert_favorite_postmeta( $object_id ) {\n\n\t\t$update = llms_update_user_postmeta( $this->get_id(), $object_id, '_favorite', true );\n\n\t\t// Returns boolean if postmeta update is successful.\n\t\treturn is_array( $update ) ? false : true;\n\t}\n\n\t/**\n\t * Remove student postmeta data when lesson is unfavorited.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param int $object_id WP Post ID of the object to mark/unmark as favorite.\n\t * @return bool\n\t */\n\tprivate function remove_favorite_postmeta( $object_id ) {\n\n\t\t$update = llms_delete_user_postmeta( $this->get_id(), $object_id, '_favorite', true );\n\n\t\t// Returns boolean if postmeta update is successful.\n\t\treturn is_array( $update ) ? false : true;\n\t}\n\n\t/**\n\t * Add student postmeta data for enrollment into a course or membership\n\t *\n\t * @param    int    $product_id   WP Post ID of the course or membership\n\t * @param    string $trigger      String describing the reason for enrollment\n\t * @return   boolean\n\t * @since    2.2.3\n\t * @version  3.21.0\n\t */\n\tprivate function insert_enrollment_postmeta( $product_id, $trigger = 'unspecified' ) {\n\n\t\t// Add info to the user postmeta table.\n\t\t$user_metadatas = array(\n\t\t\t'_enrollment_trigger' => $trigger,\n\t\t\t'_start_date'         => 'yes',\n\t\t\t'_status'             => 'enrolled',\n\t\t);\n\n\t\t$update = llms_bulk_update_user_postmeta( $this->get_id(), $product_id, $user_metadatas, false );\n\n\t\t// Returns an array with errored keys or true on success.\n\t\treturn is_array( $update ) ? false : true;\n\t}\n\n\t/**\n\t * Remove student enrollment postmeta for a given product.\n\t *\n\t * @since 3.33.0\n\t *\n\t * @param int    $product_id WP Post ID of the course or membership.\n\t * @param string $trigger    Optional. String the reason for enrollment. Default `null`\n\t * @return bool Whether or not the enrollment records have been succesfully removed.\n\t */\n\tprivate function delete_enrollment_postmeta( $product_id, $trigger = null ) {\n\n\t\t// Delete info from the user postmeta table.\n\t\t$user_metadatas = array(\n\t\t\t'_enrollment_trigger' => $trigger,\n\t\t\t'_start_date'         => null,\n\t\t\t'_status'             => null,\n\t\t);\n\n\t\t$delete = llms_bulk_delete_user_postmeta( $this->get_id(), $product_id, $user_metadatas );\n\n\t\treturn is_array( $delete ) ? false : true;\n\t}\n\n\t/**\n\t * Add a new status record to the user postmeta table for a specific product\n\t *\n\t * @param    int    $product_id   WP Post ID of the course or membership\n\t * @param    string $status       string describing the new status\n\t * @param    string $trigger  String describing the reason for enrollment (optional)\n\t * @return   boolean\n\t * @since    3.0.0\n\t * @version  3.21.0\n\t */\n\tprivate function insert_status_postmeta( $product_id, $status = '', $trigger = null ) {\n\n\t\t$update = llms_update_user_postmeta( $this->get_id(), $product_id, '_status', $status, false );\n\n\t\tif ( $update && $trigger ) {\n\t\t\t$update = llms_update_user_postmeta( $this->get_id(), $product_id, '_enrollment_trigger', $trigger, false );\n\t\t}\n\n\t\treturn $update;\n\t}\n\n\t/**\n\t * Determine if a student is enrolled in a Course or Membership.\n\t *\n\t * @see     llms_is_user_enrolled()\n\t *\n\t * @param   int|array $product_ids WP Post ID of a Course, Section, Lesson, or Membership or array of multiple IDs.\n\t * @param   string    $relation    Comparator for enrollment check.\n\t *                                     All = user must be enrolled in all $product_ids.\n\t *                                     Any = user must be enrolled in at least one of the $product_ids.\n\t * @param   bool      $use_cache  If true, returns cached data if available, if false will run a db query.\n\t *\n\t * @return  boolean\n\t *\n\t * @since   3.0.0\n\t * @version 3.25.0\n\t */\n\tpublic function is_enrolled( $product_ids = null, $relation = 'all', $use_cache = true ) {\n\n\t\t// Assume enrollment unless we find otherwise.\n\t\t$ret = true;\n\n\t\t// Allow a single product ID to be submitted (backwards compat).\n\t\t$product_ids = ! is_array( $product_ids ) ? array( $product_ids ) : $product_ids;\n\n\t\tforeach ( $product_ids as $id ) {\n\n\t\t\t$enrolled = ( 'enrolled' === strtolower( $this->get_enrollment_status( $id, $use_cache ) ) );\n\n\t\t\t// If use must be enrolled in all products and one is not enrolled: quit the loop & return false.\n\t\t\tif ( 'all' === $relation && ! $enrolled ) {\n\t\t\t\t$ret = false;\n\t\t\t\tbreak;\n\n\t\t\t\t// If user must be enrolled in any.\n\t\t\t} elseif ( 'any' === $relation ) {\n\n\t\t\t\t// If we find an enrollment: return true and quit the loop.\n\t\t\t\tif ( $enrolled ) {\n\t\t\t\t\t$ret = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\t\t// If not switch return to false but keep looking.\n\t\t\t\t} else {\n\t\t\t\t\t$ret = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_is_user_enrolled', $ret, $this, $product_ids, $relation, $use_cache );\n\t}\n\n\t/**\n\t * Mark a lesson, section, course, or track complete for the given user.\n\t *\n\t * @param  int    $object_id    WP Post ID of the lesson, section, course, or track\n\t * @param  string $object_type  object type [lesson|section|course|track]\n\t * @param  string $trigger      String describing the reason for marking complete\n\t * @return bool\n\t *\n\t * @see    llms_mark_complete() calls this function without having to instantiate the LLMS_Student class first\n\t *\n\t * @since    3.3.1\n\t * @version  3.17.1\n\t */\n\tpublic function mark_complete( $object_id, $object_type, $trigger = 'unspecified' ) {\n\n\t\t// Short circuit if it's already completed.\n\t\tif ( $this->is_complete( $object_id, $object_type ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn $this->update_completion_status( 'complete', $object_id, $object_type, $trigger );\n\t}\n\n\t/**\n\t * Mark a lesson, section, course, or track incomplete for the given user\n\t * Gives an \"_is_complete\" value of \"no\" for the given object\n\t *\n\t * @param  int    $object_id    WP Post ID of the lesson, section, course, or track\n\t * @param  string $object_type  object type [lesson|section|course|track]\n\t * @param  string $trigger      String describing the reason for marking incomplete\n\t * @return bool\n\t *\n\t * @see    llms_mark_incomplete() calls this function without having to instantiate the LLMS_Student class first\n\t *\n\t * @since    3.5.0\n\t * @version  3.17.0\n\t */\n\tpublic function mark_incomplete( $object_id, $object_type, $trigger = 'unspecified' ) {\n\n\t\treturn $this->update_completion_status( 'incomplete', $object_id, $object_type, $trigger );\n\t}\n\n\t/**\n\t * Remove a student from a membership level.\n\t *\n\t * @since 2.7\n\t * @since 3.7.5 Unknown.\n\t * @since 3.36.2 Added the $delete parameter, that will allow related courses enrollments data deletion.\n\t *\n\t * @param  int     $membership_id WP Post ID of the membership.\n\t * @param  string  $status        Optional. Status to update the removal to. Default is `expired`.\n\t * @param  boolean $delete        Optional. Status to update the removal to. Default is `false`.\n\t * @return void\n\t */\n\tprivate function remove_membership_level( $membership_id, $status = 'expired', $delete = false ) {\n\n\t\t// Remove the user from the membership level.\n\t\t$membership_levels = $this->get_membership_levels();\n\t\t$key               = array_search( $membership_id, $membership_levels );\n\t\tif ( false !== $key ) {\n\t\t\tunset( $membership_levels[ $key ] );\n\t\t}\n\t\tupdate_user_meta( $this->get_id(), '_llms_restricted_levels', $membership_levels );\n\n\t\tglobal $wpdb;\n\t\t// Locate all enrollments triggered by this membership level.\n\t\t$q = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT post_id FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d AND meta_key = '_enrollment_trigger' AND meta_value = %s\",\n\t\t\t\tarray( $this->get_id(), 'membership_' . $membership_id )\n\t\t\t),\n\t\t\t'OBJECT_K'\n\t\t); // db call ok; no-cache ok.\n\n\t\t$courses = array_keys( $q );\n\n\t\tif ( $courses ) {\n\n\t\t\t// Loop through all the courses and update the enrollment status.\n\t\t\tforeach ( $courses  as $course_id ) {\n\t\t\t\t// See if they should continue to have access to this course via another membership's current auto-enroll settings.\n\t\t\t\t$membership_levels    = $this->get_membership_levels();\n\t\t\t\t$unenroll_from_course = true;\n\n\t\t\t\tforeach ( $membership_levels as $membership_level_id ) {\n\t\t\t\t\tif ( $membership_id === $membership_level_id ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t$membership         = new LLMS_Membership( $membership_level_id );\n\t\t\t\t\t$autoenroll_courses = $membership->get_auto_enroll_courses();\n\n\t\t\t\t\tif ( $autoenroll_courses ) {\n\t\t\t\t\t\tif ( in_array( $course_id, $autoenroll_courses ) ) {\n\t\t\t\t\t\t\t// Update the enrollment trigger to the membership that is keeping them enrolled in the course.\n\t\t\t\t\t\t\tllms_update_user_postmeta( $this->get_id(), $course_id, '_enrollment_trigger', 'membership_' . $membership_level_id, false );\n\n\t\t\t\t\t\t\t$unenroll_from_course = false;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( ! $unenroll_from_course ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( ! $delete ) {\n\t\t\t\t\t$this->unenroll( $course_id, 'membership_' . $membership_id, $status );\n\t\t\t\t} else {\n\t\t\t\t\t$this->delete_enrollment( $course_id, 'membership_' . $membership_id );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Remove a student from a LifterLMS course or membership\n\t *\n\t * @since 3.0.0\n\t * @since 3.26.0 Unknown.\n\t * @since 3.37.9 Update to accommodate custom post type enrollments added through new filters.\n\t *               Marked action `llms_user_removed_from_membership_level` as deprecated, use `llms_user_removed_from_membership` instead.\n\t * @since 6.0.0 Removed the deprecated `llms_user_removed_from_membership_level` action hook\n\t *              and moved the call to `LLMS_Student::remove_membership_level()` to be before triggering the\n\t *              `llms_user_removed_from_{$post_type}` action hook.\n\t *\n\t * @see llms_unenroll_student()\n\t *\n\t * @param  int    $product_id WordPress Post ID of the course or membership.\n\t * @param  string $trigger    Only remove the student if the original enrollment trigger matches the submitted value.\n\t *                            Passing `any` will remove regardless of enrollment trigger.\n\t * @param  string $new_status the value to update the new status with after removal is complete.\n\t * @return bool\n\t */\n\tpublic function unenroll( $product_id, $trigger = 'any', $new_status = 'expired' ) {\n\n\t\t// Can only unenroll those that are a currently enrolled.\n\t\tif ( ! $this->is_enrolled( $product_id, 'all', false ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Assume we can't unenroll.\n\t\t$update = false;\n\n\t\t// If trigger is \"any\" we'll unenroll regardless of the trigger.\n\t\tif ( 'any' === $trigger ) {\n\n\t\t\t$update = true;\n\n\t\t} else {\n\n\t\t\t$enrollment_trigger = $this->get_enrollment_trigger( $product_id );\n\n\t\t\t// No enrollment trigger exists b/c pre 3.0.0 enrollment, unenroll the user as if it was an 'any' trigger.\n\t\t\tif ( ! $enrollment_trigger ) {\n\n\t\t\t\t/**\n\t\t\t\t * This filter allows customization of enrollments created prior to version 3.0.0\n\t\t\t\t *\n\t\t\t\t * Prior to 3.0.0 enrollments did not track an enrollment trigger so any unenrollments\n\t\t\t\t * performed on an enrollment in this state will automatically be unenrolled.\n\t\t\t\t *\n\t\t\t\t * Returning `false` will prevent unenrollments against enrollments which don't have\n\t\t\t\t * an enrollment trigger.\n\t\t\t\t *\n\t\t\t\t * @since 3.0.0\n\t\t\t\t *\n\t\t\t\t * @param bool $allow_unenrollment If true, allows unenrollment, otherwise prevents unenrollment.\n\t\t\t\t */\n\t\t\t\t$update = apply_filters( 'lifterlms_legacy_unenrollment_action', true );\n\n\t\t\t} elseif ( $enrollment_trigger === $trigger ) {\n\n\t\t\t\t$update = true;\n\n\t\t\t}\n\t\t}\n\n\t\t// Update if we can.\n\t\tif ( $update ) {\n\n\t\t\t// Update enrollment for the product.\n\t\t\tif ( $this->insert_status_postmeta( $product_id, $new_status ) ) {\n\n\t\t\t\t// Update the cache.\n\t\t\t\t$this->cache_set( sprintf( 'enrollment_status_%d', $product_id ), $new_status );\n\t\t\t\t$this->cache_delete( sprintf( 'date_enrolled_%d', $product_id ) );\n\t\t\t\t$this->cache_delete( sprintf( 'date_updated_%d', $product_id ) );\n\n\t\t\t\t$post_type = str_replace( 'llms_', '', get_post_type( $product_id ) );\n\n\t\t\t\t// Run legacy action and trigger cascading unenrollments for membership relationships.\n\t\t\t\tif ( 'membership' === $post_type ) {\n\n\t\t\t\t\t// Users should be unenrolled from all courses they accessed through this membership.\n\t\t\t\t\t$this->remove_membership_level( $product_id, $new_status );\n\t\t\t\t}\n\n\t\t\t\t/**\n\t\t\t\t * Trigger an action immediately following user unenrollment\n\t\t\t\t *\n\t\t\t\t * The dynamic portion of this hook, `{$post_type}` corresponds to the post type of the\n\t\t\t\t * `$product_id`. Note that any post type prefixed with `llms_` is stripped. For example\n\t\t\t\t * when triggered by a membership (`llms_membership`) the hook will be `llms_user_removed_from_membership`.\n\t\t\t\t *\n\t\t\t\t * @since 3.37.9\n\t\t\t\t *\n\t\t\t\t * @param int    $user_id    WP_User ID of the student\n\t\t\t\t * @param int    $product_id WP_Post ID of the product.\n\t\t\t\t * @param string $trigger    Enrollment trigger.\n\t\t\t\t * @param string $new_status New enrollment status of the student after the unenrollment has taken place.\n\t\t\t\t */\n\t\t\t\tdo_action( \"llms_user_removed_from_{$post_type}\", $this->get_id(), $product_id, $trigger, $new_status );\n\n\t\t\t\treturn true;\n\n\t\t\t}\n\t\t}\n\n\t\t// Update was prevented.\n\t\treturn false;\n\t}\n\n\t/**\n\t * Delete a student enrollment.\n\t *\n\t * @since 3.33.0\n\t * @since 3.36.2 Added logic to physically remove from the membership level and remove enrollments data on related products.\n\t * @since 4.2.0 The `$enrollment_trigger` parameter was added to the `llms_user_enrollment_deleted` action hook.\n\t *\n\t * @see `llms_delete_student_enrollment()` calls this function without having to instantiate the LLMS_Student class first.\n\t *\n\t * @param int    $product_id WP Post ID of the course or membership.\n\t * @param string $trigger    Optional. Only delete the student's enrollment if the original enrollment trigger matches the submitted value.\n\t *                           \"any\" will remove regardless of enrollment trigger. Default \"any\".\n\t * @return bool Whether or not the enrollment records have been successfully removed.\n\t */\n\tpublic function delete_enrollment( $product_id, $trigger = 'any' ) {\n\n\t\t// Assume we can't delete the enrollment.\n\t\t$delete = false;\n\n\t\t// Get the stored trigger.\n\t\t$enrollment_trigger = $this->get_enrollment_trigger( $product_id );\n\n\t\t// Okay to delete if trigger is \"any\" or if it matches the stored enrollment trigger.\n\t\tif ( 'any' === $trigger || $enrollment_trigger === $trigger ) {\n\n\t\t\t$delete = true;\n\n\t\t} elseif ( ! $enrollment_trigger ) {\n\n\t\t\t/**\n\t\t\t * Customize the behavior of enrollment deletion for \"legacy\" orders.\n\t\t\t *\n\t\t\t * These orders were created before version 3.0.0 when there was no stored\n\t\t\t * enrollment trigger.\n\t\t\t *\n\t\t\t * By default, we'll automatically delete these enrollments regardless of trigger.\n\t\t\t *\n\t\t\t * @since 3.33.0\n\t\t\t *\n\t\t\t * @param boolean $delete Whether or not to delete the enrollment.\n\t\t\t */\n\t\t\t$delete = apply_filters( 'lifterlms_legacy_delete_enrollment_action', true );\n\n\t\t\t// Ensure we have an `$enrollment_trigger` when firing the `llms_user_enrollment_deleted` hook.\n\t\t\t$enrollment_trigger = $trigger;\n\n\t\t}\n\n\t\t// Delete the enrollment.\n\t\tif ( $delete && $this->delete_enrollment_postmeta( $product_id ) ) {\n\n\t\t\t// Clean the cache.\n\t\t\t$this->cache_delete( sprintf( 'enrollment_status_%d', $product_id ) );\n\t\t\t$this->cache_delete( sprintf( 'date_enrolled_%d', $product_id ) );\n\t\t\t$this->cache_delete( sprintf( 'date_updated_%d', $product_id ) );\n\n\t\t\tif ( 'llms_membership' === get_post_type( $product_id ) ) {\n\t\t\t\t// Physically remove from the membership level & remove enrollments data on related products.\n\t\t\t\t$this->remove_membership_level( $product_id, '', true );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Fires after an user enrollment has been deleted.\n\t\t\t *\n\t\t\t * @since 3.33.0\n\t\t\t * @since 4.2.0 The `$enrollment_trigger` parameter was added.\n\t\t\t *\n\t\t\t * @param int    $user_id            WP User ID.\n\t\t\t * @param int    $product_id         WP Post ID of the course or membership.\n\t\t\t * @param string $enrollment_trigger The enrollment trigger.\n\t\t\t */\n\t\t\tdo_action( 'llms_user_enrollment_deleted', $this->get_id(), $product_id, $enrollment_trigger );\n\n\t\t\t// Success.\n\t\t\treturn true;\n\n\t\t}\n\n\t\t// Nothing was deleted.\n\t\treturn false;\n\t}\n\n\t/**\n\t * Update the completion status of a track, course, section, or lesson for the current student\n\t *\n\t * Cascades up to parents and clears progress caches for parents.\n\t *\n\t * Triggers actions for completion/incompletion.\n\t *\n\t * Inserts / updates necessary user postmeta data.\n\t *\n\t * @since 3.17.0\n\t * @since 4.2.0 Use filterable functions to determine if the object is completable.\n\t *              Added filter to allow customization of object parent data.\n\t *\n\t * @param string $status      New status to update to, either \"complete\" or \"incomplete\".\n\t * @param int    $object_id   WP_Post ID of the object.\n\t * @param string $object_type The type of object. A lesson, section, course, or course_track.\n\t * @param string $trigger     String describing the reason for the status change.\n\t * @return bool\n\t */\n\tprivate function update_completion_status( $status, $object_id, $object_type, $trigger = 'unspecified' ) {\n\n\t\t$student_id = $this->get_id();\n\n\t\t/**\n\t\t * Fires before a student's object completion status is updated.\n\t\t *\n\t\t * The dynamic portion of this hook, `$status`, refers to the new completion status of the object,\n\t\t * either \"complete\" or \"incomplete\"\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param int    $student_id  WP_User ID of the student.\n\t\t * @param int    $object_id   WP_Post ID of the object.\n\t\t * @param string $object_type The type of object. A lesson, section, course, or course_track.\n\t\t * @param string $trigger     String describing the reason for the status change.\n\t\t */\n\t\tdo_action( \"before_llms_mark_{$status}\", $student_id, $object_id, $object_type, $trigger );\n\n\t\t// Retrieve an instance of the objec we're acting on.\n\t\tif ( in_array( $object_type, llms_get_completable_post_types(), true ) ) {\n\t\t\t$object = llms_get_post( $object_id );\n\t\t} elseif ( in_array( $object_type, llms_get_completable_taxonomies(), true ) ) {\n\t\t\t$object = get_term( $object_id, $object_type );\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\n\t\t/**\n\t\t * Lessons have binary completion (complete or incomplete).\n\t\t *\n\t\t * Other objects are dependent on their children's statuses. These other object types\n\t\t * must check the combined progress of their children to see if it's complete / incomplete.\n\t\t */\n\t\t$complete = ( 'lesson' === $object_type ) ? ( 'complete' === $status ) : ( 100 == $this->get_progress( $object_id, $object_type, false ) );\n\n\t\t// Get parent information.\n\t\t$parent_data = array(\n\t\t\t'ids'  => array(),\n\t\t\t'type' => false,\n\t\t);\n\n\t\t// Get the immediate parent so we can cascade up and maybe update the parent's status.\n\t\tswitch ( $object_type ) {\n\n\t\t\tcase 'lesson':\n\t\t\t\t$parent_data['ids']  = array( $object->get( 'parent_section' ) );\n\t\t\t\t$parent_data['type'] = 'section';\n\t\t\t\tbreak;\n\n\t\t\tcase 'section':\n\t\t\t\t$parent_data['ids']  = array( $object->get( 'parent_course' ) );\n\t\t\t\t$parent_data['type'] = 'course';\n\t\t\t\tbreak;\n\n\t\t\tcase 'course':\n\t\t\t\t$parent_data['ids']  = wp_list_pluck( $object->get_tracks(), 'term_id' );\n\t\t\t\t$parent_data['type'] = 'course_track';\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the parent data used to cascade object completion up to an object's parent(s).\n\t\t *\n\t\t * @since 4.2.0\n\t\t *\n\t\t * @param array  $parent_data {\n\t\t *     Array of the object's parent information.\n\t\t *\n\t\t *     @type int[]  $ids  Object ids for the parent object(s).\n\t\t *     @type string $type Object type (course, course_track, etc...).\n\t\t * }\n\t\t * @param object $object      The object. An `LLMS_Course`, for example.\n\t\t * @param int    $ojbect_id   The object's ID.\n\t\t * @param string $object_type The object's type.\n\t\t */\n\t\t$parent_data = apply_filters( 'llms_mark_complete_parent_data', $parent_data, $object, $object_id, $object_type );\n\n\t\t// Reset the cached progress for any objects with children.\n\t\tif ( 'lesson' !== $object_type ) {\n\t\t\t$this->set( sprintf( '%1$s_%2$d_progress', $object_type, $object_id ), '' );\n\t\t}\n\n\t\t// Reset cache for all parents.\n\t\tif ( $parent_data['ids'] && $parent_data['type'] ) {\n\t\t\tforeach ( $parent_data['ids'] as $pid ) {\n\t\t\t\t$this->set( sprintf( '%1$s_%2$d_progress', $parent_data['type'], $pid ), '' );\n\t\t\t}\n\t\t}\n\n\t\t// Determine if an update should be made.\n\t\t$update = ( 'complete' === $status && $complete ) || ( 'incomplete' === $status && ! $complete );\n\n\t\tif ( $update ) {\n\n\t\t\t// Insert meta data.\n\t\t\tif ( 'complete' === $status ) {\n\t\t\t\t$this->insert_completion_postmeta( $object_id, $trigger );\n\t\t\t} elseif ( 'incomplete' === $status ) {\n\t\t\t\t$this->insert_incompletion_postmeta( $object_id, $trigger );\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Hook that fires when a student's completion status is updated for any object.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$status`, refers to the new completion status of the object,\n\t\t\t * either \"complete\" or \"incomplete\"\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int    $student_id  WP_User ID of the student.\n\t\t\t * @param int    $object_id   WP_Post ID of the object.\n\t\t\t * @param string $object_type The type of object. A lesson, section, course, or course_track.\n\t\t\t * @param string $trigger     String describing the reason for the status change.\n\t\t\t */\n\t\t\tdo_action( \"llms_mark_{$status}\", $student_id, $object_id, $object_type, $trigger );\n\n\t\t\t/**\n\t\t\t * Hook that fires when a student's completion status is updated for a specific object type.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$object_type` refers to the WP_Post post_type of the object\n\t\t\t * which the student's completion status is being updated for.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$status`, refers to the new completion status of the object,\n\t\t\t * either \"complete\" or \"incomplete\"\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int $student_id WP_User ID of the student.\n\t\t\t * @param int $object_id  WP_Post ID of the object.\n\t\t\t */\n\t\t\tdo_action( \"lifterlms_{$object_type}_{$status}d\", $student_id, $object_id );\n\n\t\t\t// Cascade up for parents.\n\t\t\tif ( $parent_data['ids'] && $parent_data['type'] ) {\n\t\t\t\tforeach ( $parent_data['ids'] as $pid ) {\n\t\t\t\t\t$this->update_completion_status( $status, $pid, $parent_data['type'], $trigger );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Hook that fires after a student's completion status for an object and it's parents have\n\t\t\t * been updated.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `$status`, refers to the new completion status of the object,\n\t\t\t * either \"complete\" or \"incomplete\"\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int    $student_id  WP_User ID of the student.\n\t\t\t * @param int    $object_id   WP_Post ID of the object.\n\t\t\t * @param string $object_type The type of object. A lesson, section, course, or course_track.\n\t\t\t * @param string $trigger     String describing the reason for the status change.\n\t\t\t */\n\t\t\tdo_action( \"after_llms_mark_{$status}\", $student_id, $object_id, $object_type, $trigger );\n\n\t\t}\n\n\t\treturn $update;\n\t}\n\n\t/**\n\t * Determine if the student has favorited a lesson.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t * @param string $object_type The object type, currently only 'lesson'.\n\t * @return bool\n\t */\n\tpublic function is_favorite( $object_id, $object_type = 'lesson' ) {\n\n\t\t$query = new LLMS_Query_User_Postmeta(\n\t\t\tarray(\n\t\t\t\t'types'                 => 'favorites',\n\t\t\t\t'include_post_children' => false,\n\t\t\t\t'user_id'               => $this->get_id(),\n\t\t\t\t'post_id'               => $object_id,\n\t\t\t\t'per_page'              => 1,\n\t\t\t\t'no_found_rows'         => true,\n\t\t\t)\n\t\t);\n\n\t\t$ret = $query->has_results();\n\n\t\t/**\n\t\t * Filter object favorite boolean value prior to returning.\n\t\t *\n\t\t * The dynamic portion of this filter, `{$object_type}`, refers to the Lesson.\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @param array|false  $ret         Array of favorite data or `false` if no favorite is found.\n\t\t * @param int          $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t\t * @param string       $object_type The object type, currently only 'lesson'.\n\t\t * @param LLMS_Student $instance    The Student Instance\n\t\t */\n\t\treturn apply_filters( 'llms_is_' . $object_type . '_favorite', $ret, $object_id, $object_type, $this );\n\t}\n\n\t/**\n\t * Mark a lesson favorite for the given user.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @see llms_mark_favorite() calls this function without having to instantiate the LLMS_Student class first.\n\t *\n\t * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t * @param string $object_type The object type, currently only 'lesson'.\n\t * @return bool\n\t */\n\tpublic function mark_favorite( $object_id, $object_type ) {\n\n\t\t// Short circuit if it's already favorited.\n\t\tif ( $this->is_favorite( $object_id, $object_type ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn $this->update_favorite_status( 'favorite', $object_id, $object_type );\n\t}\n\n\t/**\n\t * Mark a lesson unfavorite for the given user.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @see llms_mark_unfavorite() calls this function without having to instantiate the LLMS_Student class first.\n\t *\n\t * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t * @param string $object_type The object type, currently only 'lesson'.\n\t * @return bool\n\t */\n\tpublic function mark_unfavorite( $object_id, $object_type ) {\n\n\t\t// Short circuit if it's not favorited.\n\t\tif ( ! $this->is_favorite( $object_id, $object_type ) ) {\n\t\t\treturn true;\n\t\t}\n\n\t\treturn $this->update_favorite_status( 'unfavorite', $object_id, $object_type );\n\t}\n\n\t/**\n\t * Triggers actions for favorite/unfavorite.\n\t *\n\t * Update the favorite status of a lesson for the current student.\n\t * Inserts / updates necessary user postmeta data.\n\t *\n\t * @since 7.5.0 Use filterable functions to determine if the object can be marked favorite.\n\t *\n\t * @param string $status      New status to update to, either \"favorite\" or \"unfavorite\".\n\t * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t * @param string $object_type The object type, currently only 'lesson'.\n\t * @return bool\n\t */\n\tprivate function update_favorite_status( $status, $object_id, $object_type ) {\n\n\t\t$student_id = $this->get_id();\n\n\t\t/**\n\t\t * Fires before a student's object favorite status is updated.\n\t\t *\n\t\t * The dynamic portion of this hook, `$status`, refers to the new completion status of the object,\n\t\t * either \"favorite\" or \"unfavorite\".\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @param int    $student_id  WP_User ID of the student.\n\t\t * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t\t * @param string $object_type The object type, currently only 'lesson'.\n\t\t */\n\t\tdo_action( \"before_llms_mark_{$status}\", $student_id, $object_id, $object_type );\n\n\t\t// Insert / Remove meta data.\n\t\tif ( 'favorite' === $status ) {\n\t\t\t$this->insert_favorite_postmeta( $object_id );\n\t\t} elseif ( 'unfavorite' === $status ) {\n\t\t\t$this->remove_favorite_postmeta( $object_id );\n\t\t}\n\n\t\t/**\n\t\t * Hook that fires when a student's favorite status is updated for any object.\n\t\t *\n\t\t * The dynamic portion of this hook, `$status`, refers to the new favorite status of the object,\n\t\t * either \"favorite\" or \"unfavorite\".\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @param int    $student_id  WP_User ID of the student.\n\t\t * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n\t\t * @param string $object_type The object type, currently only 'lesson'.\n\t\t */\n\t\tdo_action( \"llms_mark_{$status}\", $student_id, $object_id, $object_type );\n\n\t\t/**\n\t\t * Hook that fires when a student's favorite status is updated for a specific object type.\n\t\t *\n\t\t * The dynamic portion of this hook, `$object_type` refers to the WP_Post post_type of the object\n\t\t * which the student's completion status is being updated for.\n\t\t *\n\t\t * The dynamic portion of this hook, `$status`, refers to the new completion status of the object,\n\t\t * either \"favorite\" or \"unfavorite\".\n\t\t *\n\t\t * @since 7.5.0\n\t\t *\n\t\t * @param int $student_id WP_User ID of the student.\n\t\t * @param int $object_id  WP_Post ID of the object.\n\t\t */\n\t\tdo_action( \"lifterlms_{$object_type}_{$status}d\", $student_id, $object_id );\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "includes/models/model.llms.student.quizzes.php",
    "content": "<?php\n/**\n * LLMS_Student_Quizzes model class file\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.9.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Access student quiz attempt data\n *\n * @see LLMS_Student->quizzes()\n *\n * @since 3.9.0\n */\nclass LLMS_Student_Quizzes extends LLMS_Abstract_User_Data {\n\n\t/**\n\t * Retrieve the number of quiz attempts for a quiz\n\t *\n\t * @since 3.16.0\n\t * @since 6.0.0 Don't access `LLMS_Query_Quiz_Attempt` properties directly.\n\t *\n\t * @param int $quiz_id WP Post ID of the quiz.\n\t * @return int\n\t */\n\tpublic function count_attempts_by_quiz( $quiz_id ) {\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'student_id' => $this->get_id(),\n\t\t\t\t'quiz_id'    => $quiz_id,\n\t\t\t\t'count_only' => true,\n\t\t\t)\n\t\t);\n\n\t\treturn $query->get_count_only_result();\n\n\t}\n\n\t/**\n\t * Remove Student Quiz attempt by ID\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.11 Unknown.\n\t *\n\t * @param int $attempt_id Attempt ID.\n\t * @return boolean Returns `true` on success and `false` on error.\n\t */\n\tpublic function delete_attempt( $attempt_id ) {\n\n\t\t$attempt = $this->get_attempt_by_id( $attempt_id );\n\t\treturn $attempt ? $attempt->delete() : false;\n\n\t}\n\n\t/**\n\t * Retrieve quiz data for a student and optionally filter by quiz_id(s)\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.11 Unknown.\n\t * @since 4.21.2 Retrieve only attempts for the initialized student.\n\t *\n\t * @param int[]|Int $quiz Array or single WP_Post ID for quizzes to retrieve attempts for.\n\t * @return LLMS_Quiz_Attempt[] Array of quiz attempts for the requested quiz or quizzes.\n\t */\n\tpublic function get_all( $quiz = array() ) {\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'quiz_id'    => $quiz,\n\t\t\t\t'per_page'   => 5000,\n\t\t\t\t'student_id' => $this->get( 'id' ),\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Filters the list of quiz attempts for a student\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param int[]|Int $quiz Array or single WP_Post ID for quizzes to retrieve attempts for.\n\t\t */\n\t\treturn apply_filters( 'llms_student_get_quiz_data', $query->get_attempts(), $quiz );\n\n\t}\n\n\t/**\n\t * Retrieve quiz attempts\n\t *\n\t * @since    3.16.0\n\t *\n\t * @param int   $quiz_id WP Post ID of the quiz.\n\t * @param array $args    Additional args to pass to LLMS_Query_Quiz_Attempt.\n\t * @return LLMS_Quiz_Attempt[]\n\t */\n\tpublic function get_attempts_by_quiz( $quiz_id, $args = array() ) {\n\n\t\t$args = wp_parse_args(\n\t\t\tarray(\n\t\t\t\t'student_id' => $this->get_id(),\n\t\t\t\t'quiz_id'    => $quiz_id,\n\t\t\t),\n\t\t\t$args\n\t\t);\n\n\t\t$query = new LLMS_Query_Quiz_Attempt( $args );\n\n\t\tif ( $query->has_results() ) {\n\t\t\treturn $query->get_attempts();\n\t\t}\n\n\t\treturn array();\n\n\t}\n\n\t/**\n\t * Retrieve an attempt by attempt id\n\t *\n\t * @since 3.16.0\n\t * @since 4.21.2 Return `false` for invalid IDs & check permissions before returning the attempt.\n\t *\n\t * @param int $attempt_id Attempt ID.\n\t * @return LLMS_Quiz_Attempt|boolean Returns the quiz attempt or `false` if the attempt doesn't exist or\n\t *                                   doesn't belong to the initialized student.\n\t */\n\tpublic function get_attempt_by_id( $attempt_id ) {\n\n\t\t$attempt = new LLMS_Quiz_Attempt( $attempt_id );\n\n\t\t// Invalid ID.\n\t\tif ( ! $attempt->exists() || ! current_user_can( 'view_grades', absint( $attempt->get( 'student_id' ) ), absint( $attempt->get( 'quiz_id' ) ) ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn $attempt;\n\n\t}\n\n\t/**\n\t * Decodes an attempt string and returns the associated attempt\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param string $attempt_key Encoded attempt key.\n\t * @return LLMS_Quiz_Attempt|false\n\t */\n\tpublic function get_attempt_by_key( $attempt_key ) {\n\n\t\t$id = $this->parse_attempt_key( $attempt_key );\n\t\tif ( ! $id ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn $this->get_attempt_by_id( $id );\n\n\t}\n\n\t/**\n\t * Get the number of attempts remaining by a student for a given quiz.\n\t *\n\t * @since 3.16.0\n\t * @since 6.4.0 Added parameter `$allow_negative` to allow remaining negative remaining attempts.\n\t *               It can happen when the allowed attempts number is decreased to a number lower than\n\t *               the number of the attempts already made by a given student.\n\t *\n\t * @param int  $quiz_id        WP Post ID of the Quiz.\n\t * @param bool $allow_negative Allow returning negative remaining attempts.\n\t * @return mixed\n\t */\n\tpublic function get_attempts_remaining_for_quiz( $quiz_id, $allow_negative = false ) {\n\n\t\t$quiz = llms_get_post( $quiz_id );\n\n\t\t$ret = _x( 'Unlimited', 'quiz attempts remaining', 'lifterlms' );\n\n\t\tif ( $quiz->has_attempt_limit() ) {\n\n\t\t\t$allowed = $quiz->get( 'allowed_attempts' );\n\t\t\t$used    = $this->count_attempts_by_quiz( $quiz->get( 'id' ) );\n\n\t\t\t// Ensure undefined, null, '', etc. show as an int.\n\t\t\tif ( ! $allowed ) {\n\t\t\t\t$allowed = 0;\n\t\t\t}\n\n\t\t\t$remaining = ( $allowed - $used );\n\n\t\t\t// Don't show negative attempts.\n\t\t\t$ret = $allow_negative ? $remaining : max( 0, $remaining );\n\n\t\t}\n\n\t\t/**\n\t\t * Filters the number of attempts remaining by a student for a given quiz.\n\t\t *\n\t\t * @since 3.16.0\n\t\t *\n\t\t * @param mixed                $ret             The number of attempts remaining by a student for a given quiz,\n\t\t *                                              or 'Unlimited' for quizzes with no attempts limit.\n\t\t * @param LLMS_Quiz            $quiz            Quiz object.\n\t\t * @param LLMS_Student_Quizzes $student_quizzes Student quizzes object.\n\t\t */\n\t\treturn apply_filters( 'llms_student_quiz_attempts_remaining_for_quiz', $ret, $quiz, $this );\n\n\t}\n\n\t/**\n\t * Get all the attempts for a given quiz/lesson from an attempt key\n\t *\n\t * @since 3.9.0\n\t *\n\t * @param string $attempt_key An encoded attempt key.\n\t * @return false|array\n\t */\n\tpublic function get_sibling_attempts_by_key( $attempt_key ) {\n\n\t\t$id = $this->parse_attempt_key( $attempt_key );\n\t\tif ( ! $id ) {\n\t\t\treturn false;\n\t\t}\n\n\t}\n\n\t/**\n\t * Get the quiz attempt with the highest grade for a given quiz and lesson combination\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param int  $quiz_id    WP Post ID of a Quiz.\n\t * @param null $deprecated Deprecated.\n\t * @return false|LLMS_Quiz_Attempt\n\t */\n\tpublic function get_best_attempt( $quiz_id = null, $deprecated = null ) {\n\n\t\t$attempts = $this->get_attempts_by_quiz(\n\t\t\t$quiz_id,\n\t\t\tarray(\n\t\t\t\t'per_page' => 1,\n\t\t\t\t'sort'     => array(\n\t\t\t\t\t'grade'       => 'DESC',\n\t\t\t\t\t'update_date' => 'DESC',\n\t\t\t\t\t'id'          => 'DESC',\n\t\t\t\t),\n\t\t\t\t'status'   => array( 'pass', 'fail' ),\n\t\t\t)\n\t\t);\n\n\t\tif ( $attempts ) {\n\t\t\treturn $attempts[0];\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Retrieve the last recorded attempt for a student for a given quiz/lesson\n\t *\n\t * \"Last\" is defined as the attempt with the highest attempt number\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param int $quiz_id WP Post ID of the quiz.\n\t * @return LLMS_Quiz_Attempt|false\n\t */\n\tpublic function get_last_attempt( $quiz_id ) {\n\n\t\t$attempts = $this->get_attempts_by_quiz(\n\t\t\t$quiz_id,\n\t\t\tarray(\n\t\t\t\t'per_page' => 1,\n\t\t\t\t'sort'     => array(\n\t\t\t\t\t'attempt' => 'DESC',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tif ( $attempts ) {\n\t\t\treturn $attempts[0];\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Get the last completed attempt for a given quiz or quiz/lesson combination\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.0 Unknown.\n\t *\n\t * @param int $quiz_id    WP Post ID of a Quiz.\n\t * @param int $deprecated Deprecated.\n\t * @return false|LLMS_Quiz_Attempt\n\t */\n\tpublic function get_last_completed_attempt( $quiz_id = null, $deprecated = null ) {\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'student_id'     => $this->get_id(),\n\t\t\t\t'quiz_id'        => $quiz_id,\n\t\t\t\t'per_page'       => 1,\n\t\t\t\t'status_exclude' => array( 'incomplete' ),\n\t\t\t\t'sort'           => array(\n\t\t\t\t\t'end_date' => 'DESC',\n\t\t\t\t\t'id'       => 'DESC',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\tif ( $query->has_results() ) {\n\t\t\treturn $query->get_attempts()[0];\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Parse an attempt key into it's parts\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.7 Unknown.\n\t *\n\t * @param string $attempt_key An encoded attempt key.\n\t * @return int\n\t */\n\tprivate function parse_attempt_key( $attempt_key ) {\n\t\treturn LLMS_Hasher::unhash( $attempt_key );\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.transaction.php",
    "content": "<?php\n/**\n * LLMS_Transaction model class file\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.0.0\n * @version 7.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS order transactions\n *\n * @since 3.0.0\n *\n * @property string $api_mode                   API Mode of the gateway when the transaction was made [test|live].\n * @property float  $amount                     Transaction charge amount.\n * @property string $currency                   Transaction's currency code.\n * @property string $gateway_completed_date     Datetime string when the transaction was completed by the gateway (if gateway supports).\n * @property string $gateway_customer_id        Gateway's unique ID for the customer who placed the order.\n * @property float  $gateway_fee_amount         Fee charged to the user by the gateway for the transaction (if gateway supports).\n * @property string $gateway_source_id          Source Identifier from the gateway -- eg: credit card id or account id.\n * @property string $gateway_source_description Short description of the source from the gateway. EG: Visa 1234.\n * @property string $gateway_transaction_id     Gateway's unique ID for the transaction.\n * @property int    $order_id                   ID of the related LLMS_Order.\n * @property string $payment_type               Type of payment. [recurring|single|trial].\n * @property string $payment_gateway            LifterLMS Payment Gateway ID (eg \"paypal\" or \"stripe\").\n * @property float  $refund_amount              Amount refunded, will always be 0 until a refund is actually recorded.\n * @property array  $refund_data                Array of arrays. Contains refund data for each refund recorded for this transaction.\n */\nclass LLMS_Transaction extends LLMS_Post_Model {\n\n\t/**\n\t * DB Post Type.\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_transaction';\n\n\t/**\n\t * Model Name/Type.\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'transaction';\n\n\t/**\n\t * Post model properties.\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\t\t'api_mode'                   => 'text',\n\t\t'amount'                     => 'float',\n\t\t'currency'                   => 'text',\n\t\t'gateway_completed_date'     => 'text',\n\t\t'gateway_customer_id'        => 'text',\n\t\t'gateway_fee_amount'         => 'float',\n\t\t'gateway_source_id'          => 'text',\n\t\t'gateway_source_description' => 'text',\n\t\t'gateway_transaction_id'     => 'text',\n\t\t'order_id'                   => 'absint',\n\t\t'payment_type'               => 'text',\n\t\t'payment_gateway'            => 'text',\n\t\t'refund_amount'              => 'float',\n\t\t'refund_data'                => 'array',\n\t);\n\n\t/**\n\t * Determines if the transaction can be refunded.\n\t *\n\t * Status must not be \"failed\" and total refunded amount must be less than order amount.\n\t *\n\t * @since 3.0.0\n\t * @since 7.0.0 Made the return value filterable via the `llms_transaction_can_be_refunded` hook.\n\t *\n\t * @return boolean\n\t */\n\tpublic function can_be_refunded() {\n\n\t\t$can_be_refunded = true;\n\n\t\tif ( in_array( $this->get( 'status' ), array( 'llms-txn-failed', 'llms-txn-pending' ), true ) ) {\n\t\t\t$can_be_refunded = false;\n\t\t} elseif ( $this->get_refundable_amount( array(), 'float' ) <= 0 ) {\n\t\t\t$can_be_refunded = false;\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not a transaction can be refunded.\n\t\t *\n\t\t * @since 7.0.0\n\t\t *\n\t\t * @param boolean          $can_be_refunded Whether the transaction can be refunded.\n\t\t * @param LLMS_Transaction $transaction     The transaction object.\n\t\t */\n\t\treturn apply_filters( 'llms_transaction_can_be_refunded', $can_be_refunded, $this );\n\n\t}\n\n\t/**\n\t * Generates a refund ID based on the refund method.\n\t *\n\t * When manually processing a refund an ID is generated, if processing via a gateway the data\n\t * is passed to the {@see LLMS_Transaction::process_refund_via_gateway()} and ultimately passed\n\t * to the gateway for processing (via the gateway's API). For custom methods processing is handled\n\t * via the `llms_{$method}_refund_id` filter.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $method Refund processing method ID.\n\t * @param float  $amount The amount to refund.\n\t * @param string $note   Refund notes.\n\t * @return string|boolean|WP_Error Returns the generated refund ID string or an error object. If a falsy value\n\t *                                 is returned the refund processing will fail with a generic error message.\n\t */\n\tprotected function generate_refund_id( $method, $amount, $note = '' ) {\n\n\t\tif ( 'manual' === $method ) {\n\t\t\t/**\n\t\t\t * Filters the refund id for manual refunds.\n\t\t\t *\n\t\t\t * The default refund ID is a microtime string generated by `uniqid()`.\n\t\t\t *\n\t\t\t * @since 3.0.0\n\t\t\t *\n\t\t\t * @param string $refund_id The refund ID.\n\t\t\t */\n\t\t\t$refund_id = apply_filters( 'llms_manual_refund_id', (string) uniqid() );\n\t\t} elseif ( 'gateway' === $method ) {\n\t\t\t$refund_id = $this->process_refund_via_gateway( $amount, $note );\n\t\t} else {\n\t\t\t/**\n\t\t\t * Filters the refund ID for custom refund methods.\n\t\t\t *\n\t\t\t * This filter should return a string representing the refund ID as generated by the custom refund method.\n\t\t\t *\n\t\t\t * The dynamic portion of this hook, `{$method}`, represents the ID of the custom refund method.\n\t\t\t *\n\t\t\t * @since 3.0.0\n\t\t\t *\n\t\t\t * @param string|boolean|WP_Error $refund_id   The generated refund ID or an error object. Returning a falsy value\n\t\t\t *                                             will result in the default error handling and no refund being recorded.\n\t\t\t * @param string                  $method      The method ID.\n\t\t\t * @param LLMS_Transaction        $transaction The transaction object.\n\t\t\t * @param float                   $amount      The refund amount.\n\t\t\t * @param string                  $note        The user-submitted refund note.\n\t\t\t */\n\t\t\t$refund_id = apply_filters( \"llms_{$method}_refund_id\", false, $method, $this, $amount, $note );\n\t\t}\n\n\t\treturn $refund_id;\n\n\t}\n\n\t/**\n\t * Retrieves the amount of the transaction that can be refunded.\n\t *\n\t * @since 3.0.0\n\t * @return float\n\t */\n\tpublic function get_refundable_amount() {\n\t\t$amount   = $this->get_price( 'amount', array(), 'float' );\n\t\t$refunded = $this->get_price( 'refund_amount', array(), 'float' );\n\t\treturn $amount - $refunded;\n\t}\n\n\t/**\n\t * Retrieves the array of default arguments to pass to {@see LLMS_Transaction::create()} when creating a new post.\n\t *\n\t * @since 3.0.0\n\t * @since 3.37.6 Add a default date information using `llms_current_time()`.\n\t *               Remove ordering placeholders from strftime().\n\t * @since 5.9.0 Remove usage of deprecated `strftime()`.\n\t *\n\t * @param int $order_id LLMS_Order ID of the related order.\n\t * @return array\n\t */\n\tprotected function get_creation_args( $order_id = 0 ) {\n\n\t\t$date = llms_current_time( 'mysql' );\n\n\t\t$title = sprintf(\n\t\t\t// Translators: %1$d = Order ID; %2$s = Transaction creation date.\n\t\t\t__( 'Transaction for Order #%1$d &ndash; %2$s', 'lifterlms' ),\n\t\t\t$order_id,\n\t\t\tdate_format( date_create( $date ), 'M d, Y @ h:i A' )\n\t\t);\n\n\t\t// This filter is documented in includes/abstracts/abstract.llms.post.model.php.\n\t\treturn apply_filters(\n\t\t\t\"llms_{$this->model_post_type}_get_creation_args\",\n\t\t\tarray(\n\t\t\t\t'comment_status' => 'closed',\n\t\t\t\t'ping_status'    => 'closed',\n\t\t\t\t'post_author'    => 0,\n\t\t\t\t'post_content'   => '',\n\t\t\t\t'post_date'      => $date,\n\t\t\t\t'post_excerpt'   => '',\n\t\t\t\t'post_password'  => uniqid( 'order_' ),\n\t\t\t\t'post_status'    => 'llms-' . apply_filters( 'llms_default_order_status', 'txn-pending' ),\n\t\t\t\t'post_title'     => $title,\n\t\t\t\t'post_type'      => $this->get( 'db_post_type' ),\n\t\t\t),\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Get the total amount of the transaction after deducting refunds\n\t *\n\t * @param  array  $price_args  optional array of arguments that can be passed to llms_price()\n\t * @param  string $format      optional format conversion method [html|raw|float]\n\t * @return mixed\n\t * @since  3.0.0\n\t */\n\tpublic function get_net_amount( $price_args = array(), $format = 'html' ) {\n\t\t$amount = $this->get_price( 'amount', array(), 'float' );\n\t\t$refund = $this->get_price( 'refund_amount', array(), 'float' );\n\t\treturn llms_price( $amount - $refund, $price_args, $format );\n\t}\n\n\t/**\n\t * Retrieves an instance of LLMS_Order for the transaction's parent order.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return LLMS_Order\n\t */\n\tpublic function get_order() {\n\t\treturn new LLMS_Order( $this->get( 'order_id' ) );\n\t}\n\n\t/**\n\t * Retrieves the payment gateway instance for the transactions payment gateway.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @return LLMS_Payment_Gateway|WP_Error\n\t */\n\tpublic function get_gateway() {\n\n\t\t$gateways = llms()->payment_gateways();\n\t\t$gateway  = $gateways->get_gateway_by_id( $this->get( 'payment_gateway' ) );\n\t\tif ( $gateway && $gateway->is_enabled() || is_admin() ) {\n\t\t\treturn $gateway;\n\t\t}\n\n\t\t// Translators: %s = The payment gateway ID.\n\t\treturn new WP_Error(\n\t\t\t'error',\n\t\t\tsprintf(\n\t\t\t\t__( 'Payment gateway %s could not be located or is no longer enabled', 'lifterlms' ),\n\t\t\t\t$this->get( 'payment_gateway' )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieves the title of the refund method using during refund processing.\n\t *\n\t * This method records the method used to process a refund in the refund order note.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $method The refund method ID.\n\t * @return string\n\t */\n\tprotected function get_refund_method_title( $method ) {\n\n\t\tswitch ( $method ) {\n\n\t\t\tcase 'manual':\n\t\t\t\t$method_title = __( 'manual refund', 'lifterlms' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'gateway':\n\t\t\t\t$method_title = $this->get_gateway()->get_admin_title();\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t/**\n\t\t\t\t * Filters the refund method title for custom refund methods.\n\t\t\t\t *\n\t\t\t\t * The dynamic portion of this hook, `{$method}`, represents the ID of the custom refund method.\n\t\t\t\t *\n\t\t\t\t * @since 3.0.0\n\t\t\t\t * @deprecated 7.0.0 Replaced with `llms_{$method}_refund_title`.\n\t\t\t\t *\n\t\t\t\t * @param string $method The method ID.\n\t\t\t\t */\n\t\t\t\t$method_title = apply_filters_deprecated( \"llms_{$method}_title\", array( $method ), '7.0.0', \"llms_{$method}_refund_title\" );\n\t\t\t\tif ( $method_title !== $method ) {\n\t\t\t\t\treturn $method_title;\n\t\t\t\t}\n\n\t\t\t\t/**\n\t\t\t\t * Filters the refund method title for custom refund methods.\n\t\t\t\t *\n\t\t\t\t * The dynamic portion of this hook, `{$method}`, represents the ID of the custom refund method.\n\t\t\t\t *\n\t\t\t\t * @since Unknown\n\t\t\t\t *\n\t\t\t\t * @param string           $method      The method ID.\n\t\t\t\t * @param LLMS_Transaction $transaction The transaction object.\n\t\t\t\t */\n\t\t\t\t$method_title = apply_filters( \"llms_{$method}_refund_title\", $method, $this );\n\n\t\t}\n\n\t\treturn $method_title;\n\n\t}\n\n\t/**\n\t * Retrieves a single refund by ID.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $id The refund ID.\n\t * @return array|boolean {\n\t *     An array of refund data. Returns `false` if the ID isn't found.\n\t *\n\t *     @type string id     The refund ID\n\t *     @type string date   The date the refund was recorded in MySQL date format `Y-m-d H:i:s`.\n\t *     @type string method The processing method ID. Defaults are \"manual\" or \"gateway\". Custom values can be implemented via hooks.\n\t *     @type float  amount The amount of the refund.\n\t * }\n\t */\n\tpublic function get_refund( $id ) {\n\t\t$refunds = $this->get_refunds();\n\t\treturn $refunds[ $id ] ?? false;\n\t}\n\n\t/**\n\t * Retrieves a list of refunds against the transaction.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return array[] An array of refund arrays as described by {@see LLMS_Transaction::get_refund()}.\n\t */\n\tpublic function get_refunds() {\n\t\treturn $this->get_array( 'refund_data' );\n\t}\n\n\t/**\n\t * Processes a refund against the transaction.\n\t *\n\t * This method is called called from the admin panel by clicking a refund (manual or gateway) button.\n\t *\n\t * @since 3.0.0\n\t * @since 7.0.0 Refactored code into multiple methods.\n\t *\n\t * @see LLMS_Meta_Box_Order_Transactions::save_refund()\n\t *\n\t * @param float  $amount Amount to refund.\n\t * @param string $note   Optional note to record as an order note. This is passed to the gateway to do store in the gateway if available.\n\t * @param string $method Method used to refund, either \"manual\" (available for all transactions) or \"gateway\" (when supported by the gateway that processed the transaction).\n\t * @return string|WP_Error A refund ID on success or an error object.\n\t */\n\tpublic function process_refund( $amount, $note = '', $method = 'manual' ) {\n\n\t\t// Ensure the transaction is still eligible for a refund.\n\t\tif ( ! $this->can_be_refunded() ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t'llms-txn-refund-not-eligible',\n\t\t\t\t__( 'The selected transaction is not eligible for a refund.', 'lifterlms' )\n\t\t\t);\n\t\t}\n\n\t\t$amount = floatval( $amount );\n\n\t\t// Ensure we can refund the requested amount.\n\t\t$refundable = $this->get_refundable_amount();\n\t\tif ( $amount > $refundable ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t'llms-txn-refund-amount-too-high',\n\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %1$s = The requested refund amount; %2$s = the available refundable amount.\n\t\t\t\t\t__( 'Requested refund amount was %1$s, the maximum possible refund for this transaction is %2$s.', 'lifterlms' ),\n\t\t\t\t\tllms_price( $amount ),\n\t\t\t\t\tllms_price( $refundable )\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t$id = $this->generate_refund_id( $method, $amount, $note );\n\t\tif ( is_string( $id ) ) {\n\t\t\t$this->record_refund( compact( 'amount', 'id', 'method' ), $note );\n\t\t} elseif ( ! is_wp_error( $id ) ) {\n\t\t\t$id = new WP_Error( 'llms-txn-refund-unknown-error', __( 'An unknown error occurred while processing the refund.', 'lifterlms' ) );\n\t\t}\n\n\t\treturn $id;\n\n\t}\n\n\t/**\n\t * Processes a refund via the gateway that processed the transaction.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param float  $amount The refund amount.\n\t * @param string $note   Refund order note.\n\t * @return string|WP_Error The refund ID or an error object.\n\t */\n\tprotected function process_refund_via_gateway( $amount, $note = '' ) {\n\n\t\t$gateway = $this->get_gateway();\n\t\tif ( is_wp_error( $gateway ) ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t'llms-txn-refund-gateway-invalid',\n\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %s = the payment gateway ID.\n\t\t\t\t\t__( 'Selected gateway \"%s\" is inactive or invalid.', 'lifterlms' ),\n\t\t\t\t\t$this->get( 'payment_gateway' )\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\tif ( ! $gateway->supports( 'refunds' ) ) {\n\t\t\treturn new WP_Error(\n\t\t\t\t'llms-txn-refund-gateway-support',\n\t\t\t\tsprintf(\n\t\t\t\t\t// Translators: %s = the payment gateway admin title.\n\t\t\t\t\t__( 'Selected gateway \"%s\" does not support refunds.', 'lifterlms' ),\n\t\t\t\t\t$gateway->get_admin_title()\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn $gateway->process_refund( $this, $amount, $note );\n\n\t}\n\n\t/**\n\t * Records a refund against the transaction.\n\t *\n\t * This method performs no validations and assumes that the refund has already been verified against\n\t * the refund method and current transaction restrictions.\n\t *\n\t * If the refund data isn't validated, try using {@see LLMS_Transaction::process_refund()} instead.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param array  $refund {\n\t *      Refund arguments.\n\t *\n\t *     @type float  $amount The refund amount.\n\t *     @type string $id     The generated refund ID.\n\t *     @type string $method The refund processing method ID.\n\t *     @type string $date   The refund date in MySQL date format. If not supplied, the current time is used.\n\t * }\n\t * @param string $note User-submitted refund note to add to the order alongside the refund.\n\t * @return void\n\t */\n\tpublic function record_refund( $refund, $note = '' ) {\n\n\t\t$refund = wp_parse_args(\n\t\t\t$refund,\n\t\t\tarray(\n\t\t\t\t'amount' => 0.00,\n\t\t\t\t'id'     => '',\n\t\t\t\t'method' => '',\n\t\t\t\t'date'   => llms_current_time( 'mysql' ),\n\t\t\t)\n\t\t);\n\n\t\t// Record the note.\n\t\t$this->record_refund_note( $note, $refund['amount'], $refund['id'], $refund['method'] );\n\n\t\t// Update the refunded amount.\n\t\t$refund_amount = $this->get( 'refund_amount' );\n\t\t$new_amount    = ! $refund_amount ? $refund['amount'] : $refund_amount + $refund['amount'];\n\t\t$this->set( 'refund_amount', $new_amount );\n\n\t\t// Record refund metadata.\n\t\t$refund_data = $this->get_refunds();\n\n\t\t/**\n\t\t * Filters the stored refund data before saving it.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param array            $refund {\n\t\t *     An associative array of refund data.\n\t\t *\n\t\t *     @type float  $amount The refund amount.\n\t\t *     @type string $date   The refund date in MySQL date format: `Y-m-d H:i:s`.\n\t\t *     @type string $id     The refund ID.\n\t\t *     @type string $method The refund method ID.\n\t\t * }\n\t\t * @param LLMS_Transaction $transaction The transaction object.\n\t\t * @param float            $amount      The refund amount.\n\t\t * @param string           $method      The refund method ID\n\t\t */\n\t\t$refund_data[ $refund['id'] ] = apply_filters( 'llms_transaction_refund_data', $refund, $this, $refund['amount'], $refund['method'] );\n\t\t$this->set( 'refund_data', $refund_data );\n\n\t\t// Update status.\n\t\t$this->set( 'status', 'llms-txn-refunded' );\n\n\t}\n\n\t/**\n\t * Records an order note associated with a refund.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $note      User-submitted refund note data to add to the order alongside the refund.\n\t * @param float  $amount    The refund amount.\n\t * @param string $refund_id The generated refund ID.\n\t * @param string $method    The refund processing method ID.\n\t * @return int The WP_Comment ID of the recorded order note.\n\t */\n\tprivate function record_refund_note( $note, $amount, $refund_id, $method ) {\n\n\t\t/**\n\t\t * Filters user-submitted transaction refund order note.\n\t\t *\n\t\t * @since Unknown.\n\t\t *\n\t\t * @param string           $note        The user-submitted order note text.\n\t\t * @param LLMS_Transaction $transaction The transaction object.\n\t\t * @param float            $amount      The refund amount.\n\t\t * @param string           $method      The ID of the refund method.\n\t\t */\n\t\t$orig_note = apply_filters( 'llms_transaction_refund_note', $note, $this, $amount, $method );\n\n\t\t$note = sprintf(\n\t\t\t// Translators: %1$s = The refund amount; %2$d the transaction ID; %3$s The refund method name; %4$s = the refund ID.\n\t\t\t__( 'Refunded %1$s for transaction #%2$d via %3$s [Refund ID: %4$s]', 'lifterlms' ),\n\t\t\twp_strip_all_tags( llms_price( $amount ) ),\n\t\t\t$this->get( 'id' ),\n\t\t\t$this->get_refund_method_title( $method ),\n\t\t\t$refund_id\n\t\t);\n\n\t\tif ( $orig_note ) {\n\t\t\t$note .= \"\\r\\n\";\n\t\t\t$note .= __( 'Refund Notes: ', 'lifterlms' );\n\t\t\t$note .= \"\\r\\n\";\n\t\t\t$note .= $orig_note;\n\t\t}\n\n\t\t// Record the note.\n\t\treturn $this->get_order()->add_note( $note, true );\n\n\t}\n\n\t/**\n\t * Translation wrapper for {@see LLMS_Transaction::get()` which enables l10n of database values.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $key Key to retrieve.\n\t * @return string\n\t */\n\tpublic function translate( $key ) {\n\n\t\t$val = $this->get( $key );\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase 'payment_type':\n\t\t\t\tif ( 'single' === $val ) {\n\t\t\t\t\t$val = __( 'Single', 'lifterlms' );\n\t\t\t\t} elseif ( 'recurring' === $val ) {\n\t\t\t\t\t$val = __( 'Recurring', 'lifterlms' );\n\t\t\t\t} elseif ( 'trial' === $val ) {\n\t\t\t\t\t$val = __( 'Trial', 'lifterlms' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$val = $val;\n\t\t}\n\n\t\treturn $val;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.user.achievement.php",
    "content": "<?php\n/**\n * LLMS_User_Achievement model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.8.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * An achievement awarded to a student.\n *\n * @since 3.8.0\n * @since 6.0.0 Utilize `LLMS_Abstract_User_Engagement` abstract.\n *\n * @property int    $author     WP_User ID of the user who the achievement belongs to.\n * @property string $awarded    MySQL timestamp recorded when the achievement was first awarded.\n * @property string $content    The achievement content.\n * @property int    $engagement WP_Post ID of the `llms_engagement` post used to trigger the achievement.\n *                              An empty value or `0` indicates the achievement was awarded manually or\n *                              before the engagement value was stored.\n * @property int    $parent     WP_Post ID of the template `llms_achievement` post.\n * @property int    $related    WP_Post ID of the related post.\n * @property string $title      Achievement title.\n */\nclass LLMS_User_Achievement extends LLMS_Abstract_User_Engagement {\n\n\t/**\n\t * Database (WP) post type name\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_my_achievement';\n\n\t/**\n\t * Post type model name\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'achievement';\n\n\t/**\n\t * Object properties\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\t\t'awarded'    => 'string',\n\t\t'engagement' => 'absint',\n\t\t'related'    => 'absint',\n\t);\n\n\t/**\n\t * Retrieve the image source for the achievement.\n\t *\n\t * @since 3.14.0\n\t * @since 6.0.0 Set a default size when an empty array is passed and use global default image when possible.\n\t *\n\t * @param int[] $size   Dimensions of the image to return passed as [ width, height ] (in pixels).\n\t * @param null  $unused Unused parameter inherited from the parent method.\n\t * @return string Image source URL.\n\t */\n\tpublic function get_image( $size = array(), $unused = null ) {\n\n\t\t$id     = $this->get( 'id' );\n\t\t$img_id = get_post_thumbnail_id( $id );\n\n\t\t$size = empty( $size ) ? array( 380, 380 ) : $size;\n\n\t\t/**\n\t\t * Filters the size used to retrieve an achievement image.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param int[] $size Dimensions of the image passed as [ width, height ] (in pixels).\n\t\t */\n\t\t$size = apply_filters( 'llms_achievement_image_size', $size );\n\n\t\tif ( ! $img_id ) {\n\t\t\t$src = llms()->achievements()->get_default_image( $id );\n\t\t} else {\n\t\t\tlist( $src ) = wp_get_attachment_image_src( $img_id, $size );\n\t\t}\n\n\t\t/**\n\t\t * Filter the image source URL for the achievement.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param string                $src         Image source URL.\n\t\t * @param LLMS_User_Achievement $achievement The achievement object.\n\t\t * @param int[]                 $size        Dimensions of the image to return passed as [ width, height ] (in pixels).\n\t\t */\n\t\treturn apply_filters( 'llms_achievement_get_image', $src, $this, $size );\n\n\t}\n\n\t/**\n\t * Retrieve the HTML <img> for the achievement.\n\t *\n\t * @since 3.14.0\n\t *\n\t * @param array $size Dimensions of the image to return passed as [ width, height ] (in pixels).\n\t * @return string\n\t */\n\tpublic function get_image_html( $size = array() ) {\n\n\t\t/**\n\t\t * Filters the HTML used to display an achievement image.\n\t\t *\n\t\t * @since 3.14.0\n\t\t * @since 6.0.0 Added `$size` parameter.\n\t\t *\n\t\t * @param string                $html        Image HTML.\n\t\t * @param LLMS_User_Achievement $achievement The achievement object.\n\t\t * @param int[]                 $size        Dimensions of the image to return passed as [ width, height ] (in pixels).\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_achievement_get_image_html',\n\t\t\tsprintf(\n\t\t\t\t'<img alt=\"%1$s\" class=\"llms-achievement-img\" src=\"%2$s\">',\n\t\t\t\tesc_attr( $this->get( 'title' ) ),\n\t\t\t\t$this->get_image( $size )\n\t\t\t),\n\t\t\t$this,\n\t\t\t$size\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.user.certificate.php",
    "content": "<?php\n/**\n * LLMS_User_Certificate model class\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.8.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * A certificate awarded to a student.\n *\n * @since 3.8.0\n * @since 6.0.0 Utilize `LLMS_Abstract_User_Engagement` abstract.\n *\n * @property string  $allow_sharing Whether or not public certificate sharing is enabled for the certificate.\n *                                  Either \"yes\" or \"no\".\n * @property string  $awarded       MySQL timestamp recorded when the certificate was first awarded.\n * @property string  $background    The CSS background color for the certificate.\n * @property int     $author        WP_User ID of the user who the certificate belongs to.\n * @property string  $content       The merged certificate content.\n * @property int     $engagement    WP_Post ID of the `llms_engagement` post used to trigger the certificate.\n *                                  An empty value or `0` indicates the certificate was awarded manually or\n *                                  before the engagement value was stored.\n * @property float   $height        The certificate's height.\n * @property float[] $margins       The certificate's margins.\n * @property string  $orientation   The certificate's orientation.\n * @property int     $parent        WP_Post ID of the template `llms_certificate` post.\n * @property int     $related       WP_Post ID of the related post.\n * @property int     $sequential_id The sequential certificate ID.\n * @property string  $size          The certificate's registered size ID.\n * @property string  $title         Certificate title.\n * @property string  $unit          The certificate's registered unit ID.\n * @property float   $width         The certificate's width.\n */\nclass LLMS_User_Certificate extends LLMS_Abstract_User_Engagement {\n\n\t/**\n\t * Database (WP) post type name\n\t *\n\t * @var string\n\t */\n\tprotected $db_post_type = 'llms_my_certificate';\n\n\t/**\n\t * Post type model name\n\t *\n\t * @var string\n\t */\n\tprotected $model_post_type = 'certificate';\n\n\t/**\n\t * Object properties\n\t *\n\t * @var array\n\t */\n\tprotected $properties = array(\n\t\t'allow_sharing' => 'string',\n\t\t'awarded'       => 'string',\n\t\t'background'    => 'string',\n\t\t'engagement'    => 'absint',\n\t\t'height'        => 'float',\n\t\t'margins'       => 'array',\n\t\t'orientation'   => 'string',\n\t\t'related'       => 'absint',\n\t\t'sequential_id' => 'absint',\n\t\t'size'          => 'string',\n\t\t'unit'          => 'string',\n\t\t'width'         => 'float',\n\t);\n\n\t/**\n\t * Array of default property values.\n\t *\n\t * In the form of key => default value.\n\t *\n\t * @var array\n\t */\n\tprotected $property_defaults = array(\n\t\t'background'    => '#ffffff',\n\t\t'orientation'   => 'landscape',\n\t\t'margins'       => array( 5, 5, 5, 5 ),\n\t\t'sequential_id' => 1,\n\t);\n\n\t/**\n\t * Constructor.\n\t *\n\t * Overrides parent method to setup default properties that depend on other property values.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string|int|LLMS_Post_Model|WP_Post $model Existing post or model object or ID\n\t * @param array                              $args  Args to create the post, only applies when $model is 'new'.\n\t * @return void\n\t */\n\tpublic function __construct( $model, $args = array() ) {\n\n\t\t$this->set_property_defaults();\n\n\t\tparent::__construct( $model, $args );\n\n\t}\n\n\t/**\n\t * Set this awarded certificate sequential id based on the parent's meta.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int|false Returns the awarded certificate sequenatial id.\n\t *                   Returns false if the awarded certificate has no parent template.\n\t */\n\tpublic function update_sequential_id() {\n\n\t\t$parent = $this->get( 'parent' );\n\t\tif ( ! $parent ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$next_sequential_id = llms_get_certificate_sequential_id( $parent, true );\n\t\t$this->set( 'sequential_id', $next_sequential_id );\n\n\t\treturn $next_sequential_id;\n\n\t}\n\n\t/**\n\t * Can user manage and make some actions on the certificate\n\t *\n\t * @since 4.5.0\n\t * @since 6.0.0 Prevent logged out users from managing certificates not assigned to a user.\n\t *\n\t * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n\t * @return bool\n\t */\n\tpublic function can_user_manage( $user_id = null ) {\n\n\t\t$user_id = $user_id ? $user_id : get_current_user_id();\n\t\t$result  = ( $user_id && ( $user_id === $this->get_user_id() || llms_can_user_bypass_restrictions( $user_id ) ) );\n\n\t\t/**\n\t\t * Filter whether or not a user can manage a given certificate.\n\t\t *\n\t\t * @since 4.5.0\n\t\t *\n\t\t * @param boolean               $result      Whether or not the user can manage certificate.\n\t\t * @param int                   $user_id     WP_User ID of the user viewing the certificate.\n\t\t * @param LLMS_User_Certificate $certificate Certificate class instance.\n\t\t */\n\t\treturn apply_filters( 'llms_certificate_can_user_manage', $result, $user_id, $this );\n\n\t}\n\n\t/**\n\t * Can user view the certificate\n\t *\n\t * @since 4.5.0\n\t *\n\t * @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.\n\t * @return bool\n\t */\n\tpublic function can_user_view( $user_id = null ) {\n\n\t\t$user_id = $user_id ? $user_id : get_current_user_id();\n\t\t$result  = $this->can_user_manage( $user_id ) || $this->is_sharing_enabled();\n\n\t\t/**\n\t\t * Filter whether or not a user can view a user's certificate.\n\t\t *\n\t\t * @since 4.5.0\n\t\t *\n\t\t * @param boolean               $result      Whether or not the user can view the certificate.\n\t\t * @param int                   $user_id     WP_User ID of the user viewing the certificate.\n\t\t * @param LLMS_User_Certificate $certificate Certificate class instance.\n\t\t */\n\t\treturn apply_filters( 'llms_certificate_can_user_view', $result, $user_id, $this );\n\n\t}\n\n\t/**\n\t * Retrieves the certificate background color value.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_background() {\n\t\treturn $this->get( 'background' );\n\t}\n\n\t/**\n\t * Retrieve information about the certificate background image.\n\t *\n\t * This function returns an array of information used for legacy certificates using the v1 template.\n\t *\n\t * When using the v2 template, only the `$src` value is utilized and the background image itself is\n\t * always set to 100% width and height of certificate as defined by the certificate's sizing settings.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array {\n\t *     Returns an associative array of information about the background image.\n\t *\n\t *     @type string $src        The image source url.\n\t *     @type int    $width      The image display width, in pixels.\n\t *     @type int    $height     The image display height, in pixels.\n\t *     @type bool   $is_default Whether or not the default image was returned.\n\t * }\n\t */\n\tpublic function get_background_image() {\n\n\t\t$id     = $this->get( 'id' );\n\t\t$img_id = get_post_thumbnail_id( $id );\n\n\t\t$size = 'full';\n\t\tif ( 1 === $this->get_template_version() ) {\n\t\t\t$size = llms_parse_bool( get_option( 'lifterlms_certificate_legacy_image_size', 'yes' ) ) ? 'full' : 'lifterlms_certificate_background';\n\t\t}\n\n\t\tif ( ! $img_id ) {\n\n\t\t\t// Get the source.\n\t\t\t$src = llms()->certificates()->get_default_image( $id );\n\n\t\t\t// Denote it's the default image in the return.\n\t\t\t$is_default = true;\n\n\t\t\t/**\n\t\t\t * Filters the display height of the default certificate background image.\n\t\t\t *\n\t\t\t * This filter is used by legacy certificates only. If the certificate is utilizing\n\t\t\t * the block editor the filtered value does not affect the size of the background image as\n\t\t\t * the image is always set to fill the width and height of the certificate itself.\n\t\t\t *\n\t\t\t * @since 2.2.0\n\t\t\t *\n\t\t\t * @param int $height         Display height of the image, in pixels.\n\t\t\t * @param int $certificate_id WP_Post ID of the awarded certificate.\n\t\t\t */\n\t\t\t$height = apply_filters( 'lifterlms_certificate_background_image_placeholder_height', 616, $id );\n\n\t\t\t/**\n\t\t\t * Filters the display width of the default certificate background image.\n\t\t\t *\n\t\t\t * This filter is used by legacy certificates only. If the certificate is utilizing\n\t\t\t * the block editor the filtered value does not affect the size of the background image as\n\t\t\t * the image is always set to fill the width and height of the certificate itself.\n\t\t\t *\n\t\t\t * @since 2.2.0\n\t\t\t *\n\t\t\t * @param int $width          Display width of the image, in pixels.\n\t\t\t * @param int $certificate_id WP_Post ID of the awarded certificate.\n\t\t\t */\n\t\t\t$width = apply_filters( 'lifterlms_certificate_background_image_placeholder_width', 800, $id );\n\n\t\t} else {\n\n\t\t\tlist( $src, $width, $height ) = wp_get_attachment_image_src( $img_id, $size );\n\n\t\t\t// Denote it's not the default image in the return.\n\t\t\t$is_default = false;\n\n\t\t\t/**\n\t\t\t * Filters the image source of the certificate background image.\n\t\t\t *\n\t\t\t * @since 2.2.0\n\t\t\t *\n\t\t\t * @param string $src            The image source url.\n\t\t\t * @param int    $certificate_id WP_Post ID of the awarded certificate.\n\t\t\t */\n\t\t\t$src = apply_filters( 'lifterlms_certificate_background_image_src', $src, $id );\n\n\t\t\t/**\n\t\t\t * Filters the display height of the certificate background image.\n\t\t\t *\n\t\t\t * This filter is used by legacy certificates only. If the certificate is utilizing\n\t\t\t * the block editor the filtered value does not affect the size of the background image as\n\t\t\t * the image is always set to fill the width and height of the certificate itself.\n\t\t\t *\n\t\t\t * @since 2.2.0\n\t\t\t *\n\t\t\t * @param int $height         Display height of the image, in pixels.\n\t\t\t * @param int $certificate_id WP_Post ID of the awarded certificate.\n\t\t\t */\n\t\t\t$height = apply_filters( 'lifterlms_certificate_background_image_height', $height, $id );\n\n\t\t\t/**\n\t\t\t * Filters the display width of the certificate background image.\n\t\t\t *\n\t\t\t * This filter is used by legacy certificates only. If the certificate is utilizing\n\t\t\t * the block editor the filtered value does not affect the size of the background image as\n\t\t\t * the image is always set to fill the width and height of the certificate itself.\n\t\t\t *\n\t\t\t * @since 2.2.0\n\t\t\t *\n\t\t\t * @param int $width          Display width of the image, in pixels.\n\t\t\t * @param int $certificate_id WP_Post ID of the awarded certificate.\n\t\t\t */\n\t\t\t$width = apply_filters( 'lifterlms_certificate_background_image_width', $width, $id );\n\n\t\t}\n\n\t\treturn compact( 'src', 'width', 'height', 'is_default' );\n\n\t}\n\n\t/**\n\t * Retrieves a list of the fonts used by the certificate.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @see llms_get_certificate_fonts()\n\t *\n\t * @param array|null $blocks A list of parsed block arrays or null. If none supplied the certificate's\n\t *                           content is parsed and used instead.\n\t * @return array[] Array of fonts by the certificate. Each array is a font definition with the font's\n\t *                 id added to the array.\n\t */\n\tpublic function get_custom_fonts( $blocks = null ) {\n\n\t\t$fonts = array();\n\n\t\t$blocks = is_null( $blocks ) ? parse_blocks( $this->get( 'content', true ) ) : $blocks;\n\t\tforeach ( $blocks as $block ) {\n\n\t\t\tif ( ! empty( $block['attrs']['fontFamily'] ) ) {\n\t\t\t\t$fonts[] = $block['attrs']['fontFamily'];\n\t\t\t}\n\n\t\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\t\t\t\t$fonts = array_merge( $fonts, wp_list_pluck( $this->get_custom_fonts( $block['innerBlocks'] ), 'id' ) );\n\t\t\t}\n\t\t}\n\n\t\t$valid_fonts = llms_get_certificate_fonts();\n\n\t\treturn array_filter(\n\t\t\tarray_map(\n\t\t\t\tfunction( $font ) use ( $valid_fonts ) {\n\t\t\t\t\tif ( 'default' === $font ) {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t\t$ret = $valid_fonts[ $font ] ?? null;\n\t\t\t\t\tif ( $ret ) {\n\t\t\t\t\t\t$ret['id'] = $font;\n\t\t\t\t\t}\n\t\t\t\t\treturn $ret;\n\t\t\t\t},\n\t\t\t\tarray_unique( $fonts )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieves the value for either the width or height.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string  $dimension Dimension key, either \"width\" or \"height\".\n\t * @param boolean $with_unit Whether or not to include the unit in the return.\n\t * @return string|float If `$with_unit` is `true`, returns a string with the unit, otherwise returns the dimension as a float.\n\t */\n\tprivate function get_dimension( $dimension, $with_unit = false ) {\n\n\t\t$ret = 0;\n\t\tif ( 'CUSTOM' === $this->get_size() ) {\n\t\t\t$ret = $this->get( $dimension );\n\t\t} else {\n\t\t\t$size_info = $this->get_registered_size_data();\n\t\t\t$ret       = $size_info[ $dimension ];\n\t\t}\n\n\t\treturn $with_unit ? sprintf( '%1$s%2$s', $ret, $this->get_unit() ) : $ret;\n\n\t}\n\n\t/**\n\t * Retrieve dimensions adjusted for orientation.\n\t *\n\t * The width and height are always stored as if the certificate were to be displayed in portrait\n\t * mode. This method will return the dimensions as necessary to use in styling rules.\n\t *\n\t * When the certificate is displaying in landscape the width and height are transposed\n\t * automatically by this method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param bool $with_units Whether or not to include the unit in the return.\n\t * @return array {\n\t *     Array of dimensions.\n\t *\n\t *     @type string|float $width  The display width.\n\t *     @type string|float $height The display height.\n\t * }\n\t */\n\tpublic function get_dimensions_for_display( $with_units = true ) {\n\n\t\t$orientation = $this->get_orientation();\n\t\t$width       = $this->get_width( $with_units );\n\t\t$height      = $this->get_height( $with_units );\n\n\t\treturn array(\n\t\t\t'width'  => 'portrait' === $orientation ? $width : $height,\n\t\t\t'height' => 'portrait' === $orientation ? $height : $width,\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the height dimension.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param boolean $with_unit Whether or not to include the unit in the return.\n\t * @return string|float If `$with_unit` is `true`, returns a string with the unit, otherwise returns the height as a float.\n\t */\n\tpublic function get_height( $with_unit = false ) {\n\t\treturn $this->get_dimension( 'height', $with_unit );\n\t}\n\n\t/**\n\t * Retrieves the certificate's margins.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param boolean $with_units Whether or not to include the percent sign unit in the return.\n\t * @return float[] Array of floats representing the margins. The margins are listed as they would be\n\t *                 when defining the margins of an element in CSS: `array( $left, $top, $right, $bottom )`.\n\t */\n\tpublic function get_margins( $with_units = false ) {\n\n\t\t$margins = $this->get( 'margins' );\n\n\t\tif ( $with_units ) {\n\t\t\t$margins = array_map(\n\t\t\t\tfunction( $margin ) {\n\t\t\t\t\treturn $margin . '%';\n\t\t\t\t},\n\t\t\t\t$margins\n\t\t\t);\n\t\t}\n\n\t\treturn $margins;\n\t}\n\n\t/**\n\t * Retrieve merge codes and data.\n\t *\n\t * @since 6.0.0\n\t * @since 6.1.0 Added `{earned_date}` merge code.\n\t *              Allowed `{current_date}` to be mocked.\n\t *\n\t * @return string[] Array mapping merge codes to the merge data.\n\t */\n\tprotected function get_merge_data() {\n\n\t\t$template_id   = $this->get( 'parent' );\n\t\t$user_id       = $this->get_user_id();\n\t\t$related_id    = $this->get( 'related' );\n\t\t$engagement_id = $this->get( 'engagement' );\n\t\t$date_format   = get_option( 'date_format' );\n\n\t\t$user = get_userdata( $user_id );\n\n\t\t$codes = array(\n\t\t\t// Site.\n\t\t\t'{site_title}'     => wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ),\n\t\t\t'{site_url}'       => get_permalink( llms_get_page_id( 'myaccount' ) ),\n\t\t\t// User.\n\t\t\t'{user_login}'     => $user ? $user->user_login : '',\n\t\t\t'{first_name}'     => $user ? $user->first_name : '',\n\t\t\t'{last_name}'      => $user ? $user->last_name : '',\n\t\t\t'{email_address}'  => $user ? $user->user_email : '',\n\t\t\t'{student_id}'     => $user ? $user_id : '',\n\t\t\t// Certificate.\n\t\t\t'{current_date}'   => wp_date( $date_format, llms_current_time( 'timestamp' ) ),\n\t\t\t'{earned_date}'    => $this->get_date( 'date', $date_format ),\n\t\t\t'{certificate_id}' => $this->get( 'id' ),\n\t\t\t'{sequential_id}'  => $this->get_sequential_id(),\n\t\t);\n\n\t\t$codes = LLMS_Engagement_Handler::do_deprecated_filter(\n\t\t\t$codes,\n\t\t\tarray( $template_id, $user_id, $related_id ),\n\t\t\t'certificate',\n\t\t\t'llms_certificate_merge_codes',\n\t\t\t'llms_certificate_merge_data'\n\t\t);\n\n\t\t/**\n\t\t * Filters the certificate merge data.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param array $codes      {\n\t\t *    Merge codes and data.\n\t\t *\n\t\t *    @type string          $code The merge code. E.g. {first_name}.\n\t\t *    @type int|string|bool $data The merga data to replace the merge code with. E.g. 'Dude'.\n\t\t * }\n\t\t * @param int   $user_id     WP User ID of the user who earned the certificate.\n\t\t * @param int   $template_id WP_Post ID of the certificate template.\n\t\t * @param int   $related_id  WP Post ID of the post which triggered the certificate to be awarded.\n\t\t */\n\t\treturn apply_filters( 'llms_certificate_merge_data', $codes, $user_id, $template_id, $related_id );\n\n\t}\n\n\t/**\n\t * Retrieves the certificate's orientation value.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @see llms_get_certificate_orientations()\n\t *\n\t * @return string\n\t */\n\tpublic function get_orientation() {\n\t\treturn $this->get( 'orientation' );\n\t}\n\n\t/**\n\t * Retrieves the registered size data array for the certificate's size.\n\t *\n\t * This method should not be used without first verifying that the certificate's\n\t * size is not set to CUSTOM as this is not a valid size and the sitewide default\n\t * will be returned.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @see llms_get_certificate_sizes()\n\t *\n\t * @return array\n\t */\n\tprivate function get_registered_size_data() {\n\n\t\t$size  = $this->get_size();\n\t\t$sizes = llms_get_certificate_sizes();\n\t\tif ( ! $size || empty( $sizes[ $size ] ) ) {\n\t\t\t$size = get_option( 'lifterlms_certificate_default_size', 'LETTER' );\n\t\t}\n\n\t\treturn $sizes[ $size ] ?? array_values( $sizes )[0];\n\n\t}\n\n\t/**\n\t * Retrieve the formatted sequential id for the certificate.\n\t *\n\t * The sequential ID is stored as an integer and formatted for display according the filterable\n\t * settings found in this method.\n\t *\n\t * By default, the sequential ID will appear as a 6 character number, left-side padded with zeros.\n\t *\n\t * Examples:\n\t *   + 1      = 000001\n\t *   + 20     = 000020\n\t *   + 12345  = 012345\n\t *   + 999999 = 999999\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_sequential_id() {\n\n\t\t/**\n\t\t * Filter certificate sequential id formatting settings.\n\t\t *\n\t\t * These settings are passed as arguments to `str_pad()`.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @link https://www.php.net/manual/en/function.str-pad.php\n\t\t *\n\t\t * @param array {\n\t\t *    Array of formatting settings.\n\t\t *\n\t\t *    @type int    $length    Number of characters for the ID.\n\t\t *    @type string $character Padding character.\n\t\t *    @type int    $type      String padding type. Expects a valid `pad_type` PHP constant: STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH.\n\t\t * }\n\t\t * @param LLMS_User_Certificate $certificate Instance of the certificate object.\n\t\t */\n\t\t$formatting = apply_filters(\n\t\t\t'llms_certificate_sequential_id_format',\n\t\t\tarray(\n\t\t\t\t'length'    => 6,\n\t\t\t\t'character' => '0',\n\t\t\t\t'type'      => STR_PAD_LEFT,\n\t\t\t),\n\t\t\t$this\n\t\t);\n\n\t\t$raw_id = $this->get( 'sequential_id' );\n\n\t\t$id = str_pad(\n\t\t\t(string) $raw_id,\n\t\t\t$formatting['length'],\n\t\t\t$formatting['character'],\n\t\t\t$formatting['type']\n\t\t);\n\n\t\t/**\n\t\t * Filters the formatted certificate sequential ID string.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param string                $id          The formatted sequential ID.\n\t\t * @param int                   $raw_id      The raw ID before formatting was applied.\n\t\t * @param array                 $formatting  Array of formatting settings, see `llms_certificate_sequential_id_format`.\n\t\t * @param LLMS_User_Certificate $certificate Instance of the certificate object.\n\t\t */\n\t\treturn apply_filters( 'llms_certificate_sequential_id', $id, $raw_id, $formatting, $this );\n\n\t}\n\n\t/**\n\t * Retrieves the ID of the certificate's size.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @see llms_get_certificate_sizes()\n\t *\n\t * @return string\n\t */\n\tpublic function get_size() {\n\t\treturn $this->get( 'size' );\n\t}\n\n\t/**\n\t * Retrieves the certificate's template version.\n\t *\n\t * Since LifterLMS 6.0.0, certificates are created using the block editor.\n\t *\n\t * Certificates created in the classic editor will use template version 1 while any certificates\n\t * created in the block editor use template version 2. Therefore a certificate that has content\n\t * and no blocks will use template version 1 and any empty certificates or those containing blocks\n\t * will use template version 2.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return integer\n\t */\n\tpublic function get_template_version() {\n\n\t\t$version = empty( $this->get( 'content', true ) ) || has_blocks( $this->get( 'id' ) ) ? 2 : 1;\n\n\t\t/**\n\t\t * Filters a certificate's template version.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param int $version The template version.\n\t\t */\n\t\treturn apply_filters( 'llms_certificate_template_version', $version, $this );\n\n\t}\n\n\t/**\n\t * Retrieves the ID of the certificate's unit.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @see llms_get_certificate_units()\n\t *\n\t * @return string\n\t */\n\tpublic function get_unit() {\n\n\t\tif ( 'CUSTOM' === $this->get_size() ) {\n\t\t\treturn $this->get( 'unit' );\n\t\t}\n\n\t\t$size_info = $this->get_registered_size_data();\n\t\treturn $size_info['unit'];\n\n\t}\n\n\t/**\n\t * Retrieve the width dimension.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param boolean $with_unit Whether or not to include the unit in the return.\n\t * @return string|float If `$with_unit` is `true`, returns a string with the unit, otherwise returns the width as a float.\n\t */\n\tpublic function get_width( $with_unit = false ) {\n\t\treturn $this->get_dimension( 'width', $with_unit );\n\t}\n\n\t/**\n\t * Is sharing enabled\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_sharing_enabled() {\n\n\t\t/**\n\t\t * Filter whether or not sharing is enabled for a certificate.\n\t\t *\n\t\t * @since 4.5.0\n\t\t *\n\t\t * @param boolean               $enabled     Whether or not sharing is enabled.\n\t\t * @param LLMS_User_Certificate $certificate Certificate class instance.\n\t\t */\n\t\treturn apply_filters( 'llms_certificate_is_sharing_enabled', llms_parse_bool( $this->get( 'allow_sharing' ) ), $this );\n\n\t}\n\n\t/**\n\t * Merges the post content based on content from the template.\n\t *\n\t * @since 6.0.0\n\t * @since 6.4.0 Added optional `$content` and `$load_reusable_blocks` parameters.\n\t *              Removed initialization of shortcodes now that they are registered earlier.\n\t *\n\t * @param string $content              Optionally use the given content instead of `$this->content`.\n\t * @param bool   $load_reusable_blocks Optionally replace reusable blocks with their actual blocks.\n\t * @return string\n\t */\n\tpublic function merge_content( $content = null, $load_reusable_blocks = false ) {\n\n\t\t$content = parent::merge_content( $content, $load_reusable_blocks );\n\n\t\t// Merge.\n\t\t$merge   = $this->get_merge_data();\n\t\t$content = str_replace( array_keys( $merge ), array_values( $merge ), $content );\n\n\t\t// Do shortcodes.\n\t\tadd_filter( 'llms_user_info_shortcode_user_id', array( $this, 'get_user_id' ) );\n\t\t$content = do_shortcode( $content );\n\t\tremove_filter( 'llms_user_info_shortcode_user_id', array( $this, 'get_user_id' ) );\n\n\t\t// Preserve legacy functionality which wraps the post content in the HTML specified in the template file.\n\t\t$use_template = apply_filters_deprecated(\n\t\t\t'llms_certificate_use_legacy_template',\n\t\t\tarray( false, $this ),\n\t\t\t'6.0.0',\n\t\t\t'', // There is no direct replacement.\n\t\t\t__( 'Loading custom HTML from the certificate template is deprecated. All HTML should be added to the certificate directly via the editor or applied via post content filters.', 'lifterlms' )\n\t\t);\n\t\tif ( $use_template ) {\n\t\t\tob_start();\n\t\t\tllms_get_template(\n\t\t\t\t'certificates/template.php',\n\t\t\t\tarray(\n\t\t\t\t\t'email_message' => $content,\n\t\t\t\t\t'title'         => $this->get( 'title' ),\n\t\t\t\t\t'image'         => $this->get( 'certificate_image' ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t$content = ob_get_clean();\n\t\t}\n\n\t\treturn $content;\n\n\t}\n\n\t/**\n\t * Configure non-static property defaults.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function set_property_defaults() {\n\n\t\t// Default size is configured via a site option.\n\t\t$default_size                    = get_option( 'lifterlms_certificate_default_size', 'LETTER' );\n\t\t$this->property_defaults['size'] = ! $default_size ? 'LETTER' : $default_size;\n\n\t}\n\n\t/**\n\t * Sync block editor layout properties.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_User_Certificate $template\n\t * @return void\n\t */\n\tprotected function sync_meta( $template ) {\n\n\t\tif ( 1 === $template->get_template_version() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$props = array(\n\t\t\t'background',\n\t\t\t'height',\n\t\t\t'margins',\n\t\t\t'orientation',\n\t\t\t'size',\n\t\t\t'unit',\n\t\t\t'width',\n\t\t);\n\n\t\tforeach ( $props as $prop ) {\n\t\t\t$this->set( $prop, $template->get( $prop ) );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/models/model.llms.user.postmeta.php",
    "content": "<?php\n/**\n * LLMS_User_Postmeta data model\n *\n * @package LifterLMS/Models/Classes\n *\n * @since 3.15.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_User_Postmeta model class\n *\n * @since 3.15.0\n * @since 4.3.0 Added `$type` property declaration.\n */\nclass LLMS_User_Postmeta extends LLMS_Abstract_Database_Store {\n\n\t/**\n\t * Created date column key.\n\t *\n\t * Disabled for this record type.\n\t *\n\t * @var null\n\t */\n\tprotected $date_created = null;\n\n\t/**\n\t * Updated date column key.\n\t *\n\t * @var string\n\t */\n\tprotected $date_updated = 'updated_date';\n\n\t/**\n\t * Array of table column name => format.\n\t *\n\t * @var array\n\t */\n\tprotected $columns = array(\n\t\t'user_id'      => '%d',\n\t\t'post_id'      => '%d',\n\t\t'meta_key'     => '%s',\n\t\t'meta_value'   => '%s',\n\t\t'updated_date' => '%s',\n\t);\n\n\t/**\n\t * Primary Key column name => format.\n\t *\n\t * @var array\n\t */\n\tprotected $primary_key = array(\n\t\t'meta_id' => '%d',\n\t);\n\n\t/**\n\t * Database Table Name.\n\t *\n\t * @var string\n\t */\n\tprotected $table = 'user_postmeta';\n\n\t/**\n\t * The record type.\n\t *\n\t * @var string\n\t */\n\tprotected $type = 'user_postmeta';\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.15.0\n\t * @since 3.21.0 Unknown.\n\t *\n\t * @param mixed   $item    Meta_id of a user postmeta item or an object with at least an \"id\".\n\t * @param boolean $hydrate If true, hydrates the object on instantiation (if an ID was found via $item).\n\t */\n\tpublic function __construct( $item = null, $hydrate = true ) {\n\n\t\tif ( is_numeric( $item ) ) {\n\n\t\t\t$this->id = $item;\n\n\t\t} elseif ( is_object( $item ) && isset( $item->id ) ) {\n\n\t\t\t$this->id = $item->id;\n\n\t\t}\n\n\t\tparent::__construct();\n\n\t\tif ( $this->id && $hydrate ) {\n\t\t\t$this->hydrate();\n\t\t}\n\n\t}\n\n\t/**\n\t * Get a string used to describe the postmeta item.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $context Display context either \"course\" or \"student\".\n\t * @return string\n\t */\n\tpublic function get_description( $context = 'course' ) {\n\n\t\t$key = $this->get( 'meta_key' );\n\n\t\t$student = $this->get_student();\n\t\t$name    = $student ? $student->get( 'display_name' ) : __( '[Deleted]', 'lifterlms' );\n\n\t\t$post      = llms_get_post( $this->get( 'post_id' ) );\n\t\t$label     = is_a( $post, 'LLMS_Post_Model' ) ? strtolower( $post->get_post_type_label() ) : __( 'quiz', 'lifterlms' );\n\t\t$post_name = ( 'course' === $context ) ? $label : sprintf( '%1$s \"%2$s\"', $label, get_the_title( $this->get( 'post_id' ) ) );\n\n\t\t$string = '';\n\n\t\tswitch ( $key ) {\n\n\t\t\tcase '_achievement_earned':\n\t\t\t\t$string = sprintf( __( '%1$s earned the achievement \"%2$s\"', 'lifterlms' ), $name, get_the_title( $this->get( 'meta_value' ) ) );\n\n\t\t\t\tbreak;\n\n\t\t\tcase '_certificate_earned':\n\t\t\t\t$string = sprintf( __( '%1$s earned the certificate \"%2$s\"', 'lifterlms' ), $name, get_the_title( $this->get( 'meta_value' ) ) );\n\n\t\t\t\tbreak;\n\n\t\t\tcase '_email_sent':\n\t\t\t\t$string = sprintf( __( 'Email \"%1$s\" was sent to %2$s', 'lifterlms' ), get_the_title( $this->get( 'meta_value' ) ), $name );\n\n\t\t\t\tbreak;\n\n\t\t\tcase '_enrollment_trigger':\n\t\t\t\t$string = sprintf( __( '%1$s purchased the %2$s', 'lifterlms' ), $name, $post_name );\n\n\t\t\t\tbreak;\n\n\t\t\tcase '_status':\n\t\t\t\tif ( 'enrolled' === $this->get( 'meta_value' ) ) {\n\t\t\t\t\t$string = sprintf( __( '%1$s enrolled into the %2$s', 'lifterlms' ), $name, $post_name );\n\t\t\t\t} else {\n\t\t\t\t\t$string = sprintf( __( '%1$s unenrolled from the %2$s', 'lifterlms' ), $name, $post_name );\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t\tcase '_is_complete':\n\t\t\t\t$string = sprintf( __( '%1$s completed the %2$s', 'lifterlms' ), $name, $post_name );\n\n\t\t\t\tbreak;\n\n\t\t}// End switch().\n\n\t\treturn $string;\n\n\t}\n\n\t/**\n\t * Retrieve a link for the item on the admin panel\n\t *\n\t * @since 3.15.0\n\t * @since 6.0.0 Don't use deprecated achievement and certificate meta data.\n\t *               Combined redundant cases into a single case.\n\t *               Fixed return value.\n\t *\n\t * @param string $context Display context either \"course\" or \"student\".\n\t * @return string\n\t */\n\tpublic function get_link( $context = 'course' ) {\n\n\t\t$url = '';\n\n\t\tswitch ( $this->get( 'meta_key' ) ) {\n\n\t\t\tcase '_achievement_earned':\n\t\t\tcase '_certificate_earned':\n\t\t\tcase '_email_sent':\n\t\t\t\t$url = get_edit_post_link( $this->get( 'meta_value' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase '_enrollment_trigger':\n\t\t\t\t$url = get_edit_post_link( str_replace( 'order_', '', $this->get( 'meta_value' ) ) );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$student = $this->get_student();\n\t\t\t\tif ( ! $student ) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\n\t\t\t\t$course = false;\n\t\t\t\tif ( 'course' === get_post_type( $this->get( 'post_id' ) ) ) {\n\t\t\t\t\t$course = llms_get_post( $this->get( 'post_id' ) );\n\t\t\t\t} else {\n\t\t\t\t\t$course = llms_get_post_parent_course( $this->get( 'post_id' ) );\n\t\t\t\t}\n\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$url = LLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'course_id'  => $course->get( 'id' ),\n\t\t\t\t\t\t\t'stab'       => 'courses',\n\t\t\t\t\t\t\t'student_id' => $student->get_id(),\n\t\t\t\t\t\t\t'tab'        => 'students',\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t}\n\n\t\treturn $url;\n\n\t}\n\n\t/**\n\t * Retrieve a student obj for the meta item.\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return LLMS_Student|false\n\t */\n\tpublic function get_student() {\n\t\treturn llms_get_student( $this->get( 'user_id' ) );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/class.llms.notifications.php",
    "content": "<?php\n/**\n * LifterLMS Notifications Management and Interface\n *\n * @package LifterLMS/Notifications/Classes\n *\n * @since 3.8.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Notifications Management and Interface\n *\n * Loads and allows interactions with notification views, controllers, and processors.\n *\n * @since 3.8.0\n * @since 3.24.0 Unknown.\n * @since 3.36.1 Record notifications as read during the `wp_print_footer_scripts` hook.\n * @since 3.38.0 Updated processor scheduling for increased performance and reliability.\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_Notifications::dispatch_processors()` method\n *              - `LLMS_Notifications::$_instance` property\n */\nclass LLMS_Notifications {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Controller instances\n\t *\n\t * @var LLMS_Abstract_Notification_Controller[]\n\t */\n\tprivate $controllers = array();\n\n\t/**\n\t * Notifications being displayed on this page load.\n\t *\n\t * @var array\n\t */\n\tprivate $displayed = array();\n\n\t/**\n\t * Background processor instances\n\t *\n\t * @var LLMS_Abstract_Notification_Processor[]\n\t */\n\tprivate $processors = array();\n\n\t/**\n\t * Array of processors needing to be dispatched on shutdown\n\t *\n\t * @var string[]\n\t */\n\tprivate $processors_to_dispatch = array();\n\n\t/**\n\t * [string $view_classname => string $trigger ]\n\t *\n\t * @var string[]\n\t */\n\tprivate $views = array();\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.8.0\n\t * @since 3.22.0 Unknown.\n\t * @since 3.36.1 Record basic notifications as read during `wp_print_footer_scripts`.\n\t * @since 3.38.0 Schedule processors using an async scheduled action.\n\t * @since 6.0.0 Do not load / enqueue basic notifications on the admin panel.\n\t *              Removed the deprecated `llms_processors_async_dispatching` filter hook.\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t$this->load();\n\n\t\tif ( ! is_admin() ) {\n\t\t\tadd_action( 'wp', array( $this, 'enqueue_basic' ) );\n\t\t\tadd_action( 'wp_print_footer_scripts', array( $this, 'mark_displayed_basics_as_read' ) );\n\t\t}\n\n\t\tadd_action( 'shutdown', array( $this, 'schedule_processors_dispatch' ) );\n\t\tadd_action( 'llms_dispatch_notification_processor_async', array( $this, 'dispatch_processor_async' ) );\n\n\t}\n\n\t/**\n\t * Async callback to dispatch processors\n\t *\n\t * Locates the processor by ID and dispatches it for processing.\n\t *\n\t * The trigger hook `llms_dispatch_notification_processor_async` is called by the action scheduler library.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @see llms_dispatch_notification_processor_async\n\t *\n\t * @param string $id Processor ID.\n\t * @return array|WP_Error\n\t */\n\tpublic function dispatch_processor_async( $id ) {\n\n\t\t$processor = $this->get_processor( $id );\n\t\tif ( $processor ) {\n\t\t\treturn $processor->dispatch();\n\t\t}\n\n\t\t// Translators: %s = Processor ID.\n\t\treturn new WP_Error( 'invalid-processor', sprintf( __( 'The processor \"%s\" does not exist.', 'lifterlms' ), $id ) );\n\n\t}\n\n\t/**\n\t * Enqueue basic notifications for onscreen display.\n\t *\n\t * @since 3.22.0\n\t * @since 3.36.1 Don't automatically mark notifications as read.\n\t * @since 3.38.0 Use `wp_json_decode()` in favor of `json_decode()`.\n\t * @since 4.4.0 Use `LLMS_Assets::enqueue_inline()` in favor of deprecated `LLMS_Frontend_Assets::enqueue_inline_script()`.\n\t * @since 7.1.0 Improve notifications query performance by not calculating unneeded found rows.\n\t *\n\t * @return void\n\t */\n\tpublic function enqueue_basic() {\n\n\t\t$user_id = get_current_user_id();\n\t\tif ( ! $user_id ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Get 5 most recent new notifications for the current user.\n\t\t$query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'per_page'      => 5,\n\t\t\t\t'statuses'      => 'new',\n\t\t\t\t'types'         => 'basic',\n\t\t\t\t'subscriber'    => $user_id,\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->displayed = $query->get_notifications();\n\n\t\t// Push to JS.\n\t\tllms()->assets->enqueue_inline(\n\t\t\t'llms-queued-notifications',\n\t\t\t'window.llms.queued_notifications = ' . wp_json_encode( $this->displayed ) . ';',\n\t\t\t'footer'\n\t\t);\n\n\t}\n\n\t/**\n\t * Record notifications as read.\n\t *\n\t * Ensures that notifications are not missed due to redirects that happen after `wp`.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return void\n\t */\n\tpublic function mark_displayed_basics_as_read() {\n\n\t\tif ( $this->displayed ) {\n\t\t\tforeach ( $this->displayed as $notification ) {\n\t\t\t\t$notification->set( 'status', 'read' );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Get the directory path for core notification classes\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprivate function get_directory() {\n\t\treturn LLMS_PLUGIN_DIR . 'includes/notifications/';\n\t}\n\n\t/**\n\t * Get a single controller instance\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $controller Trigger id (eg: lesson_complete).\n\t * @return LLMS_Abstract_Notification_Controller|false\n\t */\n\tpublic function get_controller( $controller ) {\n\t\tif ( isset( $this->controllers[ $controller ] ) ) {\n\t\t\treturn $this->controllers[ $controller ];\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get loaded controllers\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return LLMS_Abstract_Notification_Controller[]\n\t */\n\tpublic function get_controllers() {\n\t\treturn $this->controllers;\n\t}\n\n\t/**\n\t * Retrieve a single processor instance\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $processor Name of the processor (eg: email).\n\t * @return LLMS_Abstract_Notification_Processor|false\n\t */\n\tpublic function get_processor( $processor ) {\n\t\tif ( isset( $this->processors[ $processor ] ) ) {\n\t\t\treturn $this->processors[ $processor ];\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get loaded processors\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return LLMS_Abstract_Notification_Processor[]\n\t */\n\tpublic function get_processors() {\n\t\treturn $this->processors;\n\t}\n\n\t/**\n\t * Retrieve a view instance of a notification\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t * @since 3.38.0 Use strict comparison.\n\t *\n\t * @param LLMS_Notification $notification Notification instance.\n\t * @return LLMS_Abstract_Notification_View|false\n\t */\n\tpublic function get_view( $notification ) {\n\n\t\t$trigger = $notification->get( 'trigger_id' );\n\n\t\tif ( in_array( $trigger, $this->views, true ) ) {\n\t\t\t$views = array_flip( $this->views );\n\t\t\t$class = $views[ $trigger ];\n\t\t\t$view  = new $class( $notification );\n\t\t\treturn $view;\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Get the classname for the view of a given notification based off it's trigger\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t *\n\t * @param string $trigger Trigger id (eg: lesson_complete).\n\t * @param string $prefix  Default = 'LLMS'.\n\t * @return string\n\t */\n\tprivate function get_view_classname( $trigger, $prefix = null ) {\n\n\t\t$prefix = $prefix ? $prefix : 'LLMS';\n\t\t$name   = str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $trigger ) ) );\n\t\treturn sprintf( '%1$s_Notification_View_%2$s', $prefix, $name );\n\n\t}\n\n\t/**\n\t * Load all notifications\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t * @since 5.2.0 Added 'upcoming_payment_reminder'.\n\t *\n\t * @return void\n\t */\n\tprivate function load() {\n\n\t\t$triggers = array(\n\t\t\t'achievement_earned',\n\t\t\t'certificate_earned',\n\t\t\t'course_complete',\n\t\t\t'course_track_complete',\n\t\t\t'enrollment',\n\t\t\t'lesson_complete',\n\t\t\t'manual_payment_due',\n\t\t\t'payment_retry',\n\t\t\t'purchase_receipt',\n\t\t\t'quiz_failed',\n\t\t\t'quiz_graded',\n\t\t\t'quiz_passed',\n\t\t\t'section_complete',\n\t\t\t'student_welcome',\n\t\t\t'subscription_cancelled',\n\t\t\t'upcoming_payment_reminder',\n\t\t);\n\n\t\tforeach ( $triggers as $name ) {\n\n\t\t\t$this->load_controller( $name );\n\t\t\t$this->load_view( $name );\n\n\t\t}\n\n\t\t$processors = array(\n\t\t\t'email',\n\t\t);\n\n\t\tforeach ( $processors as $name ) {\n\t\t\t$this->load_processor( $name );\n\t\t}\n\n\t\t/**\n\t\t * Run an action after all core notification classes are loaded.\n\t\t *\n\t\t * Third party notifications can hook into this action.\n\t\t * Use `load_view()`, `load_controller()`, and `load_processor()` methods\n\t\t * to load notifications into the class and be auto-called by the core APIs.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param LLMS_Notifications $this Instance of the notifications singleton.\n\t\t */\n\t\tdo_action( 'llms_notifications_loaded', $this );\n\n\t}\n\n\t/**\n\t * Load and initialize a single controller\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $trigger Trigger id (eg: lesson_complete).\n\t * @param string $path    Full path to the controller file, allows third parties to load external controllers.\n\t * @return boolean `true` if the controller is added and loaded, `false` otherwise.\n\t */\n\tpublic function load_controller( $trigger, $path = null ) {\n\n\t\t// Default path for core views.\n\t\tif ( ! $path ) {\n\t\t\t$path = $this->get_directory() . 'controllers/class.llms.notification.controller.' . $this->name_to_file( $trigger ) . '.php';\n\t\t}\n\n\t\tif ( file_exists( $path ) ) {\n\n\t\t\t$this->controllers[ $trigger ] = require_once $path;\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Load a single processor\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type Processor type id.\n\t * @param string $path Optional path (for allowing 3rd party processor loading).\n\t * @return boolean\n\t */\n\tpublic function load_processor( $type, $path = null ) {\n\n\t\t// Default path for core processors.\n\t\tif ( ! $path ) {\n\t\t\t$path = $this->get_directory() . 'processors/class.llms.notification.processor.' . $type . '.php';\n\t\t}\n\n\t\tif ( file_exists( $path ) ) {\n\n\t\t\t$this->processors[ $type ] = require_once $path;\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Validate trigger and load its view.\n\t *\n\t * @since 3.8.0\n\t * @since 3.24.0 Unknown.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @param  string $trigger Trigger id (eg: lesson_complete).\n\t * @param  string $path    Full path to the view file, allows third parties to load external views.\n\t * @param  string $prefix  Classname prefix. Defaults to \"LLMS\". Can be used by 3rd parties to adjust\n\t *                         the prefix in accordance with the projects standards.\n\t * @return boolean `true` if the view is added and loaded, `false` otherwise.\n\t */\n\tpublic function load_view( $trigger, $path = null, $prefix = null ) {\n\n\t\t// Default path for core views.\n\t\tif ( ! $path ) {\n\t\t\t$path = $this->get_directory() . 'views/class.llms.notification.view.' . $this->name_to_file( $trigger ) . '.php';\n\t\t}\n\n\t\tif ( file_exists( $path ) ) {\n\n\t\t\tif ( ! is_null( $prefix ) ) {\n\t\t\t\trequire_once $path;\n\t\t\t}\n\t\t\t$this->views[ $this->get_view_classname( $trigger, $prefix ) ] = $trigger;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Convert a trigger name to a filename string\n\t *\n\t * EG: \"lesson_complete\" to \"lesson.complete\".\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $name Trigger name.\n\t * @return string\n\t */\n\tprivate function name_to_file( $name ) {\n\t\treturn str_replace( '_', '.', $name );\n\t}\n\n\t/**\n\t * Schedule a processor to dispatch its queue on shutdown\n\t *\n\t * @since 3.8.0\n\t * @since 3.38.0 Use strict comparisons.\n\t *\n\t * @param string $id Processor ID (eg: email).\n\t * @return void\n\t */\n\tpublic function schedule_processing( $id ) {\n\n\t\tif ( ! in_array( $id, $this->processors_to_dispatch, true ) ) {\n\n\t\t\t$this->processors_to_dispatch[] = $id;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Check for processors that have items in the queue\n\t *\n\t * For any found processors, saves their queue and schedules them to be processes via a scheduled event.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return array Array containing information about the scheduled processors.\n\t *               The array keys will be the processor ID and the values will be the timestamp of the event or a WP_Error object.\n\t */\n\tpublic function schedule_processors_dispatch() {\n\n\t\t$scheduled = array();\n\n\t\tif ( $this->processors_to_dispatch ) {\n\n\t\t\tforeach ( $this->processors_to_dispatch as $key => $id ) {\n\n\t\t\t\t// Retrieve the processor.\n\t\t\t\t$processor = $this->get_processor( $id );\n\n\t\t\t\t// Remove it from the list of processors to dispatch.\n\t\t\t\tunset( $this->processors_to_dispatch[ $key ] );\n\n\t\t\t\t$scheduled[ $id ] = $processor ? $this->schedule_single_processor( $processor, $id ) : new WP_Error(\n\t\t\t\t\t'invalid-processor',\n\t\t\t\t\t// Translators: %s = Processor ID.\n\t\t\t\t\tsprintf( __( 'The processor \"%s\" does not exist.', 'lifterlms' ), $id )\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t\treturn $scheduled;\n\n\t}\n\n\t/**\n\t * Save pending batches and schedule the async dispatching of a processor.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @param LLMS_Abstract_Notification_Processor $processor Notification processor object.\n\t * @param string                               $id        Processor ID.\n\t * @return int|WP_Error Timestamp of the scheduled event or an error object.\n\t */\n\tprotected function schedule_single_processor( $processor, $id ) {\n\n\t\t$hook = 'llms_dispatch_notification_processor_async';\n\t\t$args = array( $id );\n\n\t\t// Save items in the queue.\n\t\t$processor->save();\n\n\t\t// Check if there's already a scheduled event.\n\t\t$timestamp = as_next_scheduled_action( $hook, $args );\n\n\t\t// If there's no event scheduled already, schedule one.\n\t\tif ( ! $timestamp ) {\n\n\t\t\t$timestamp = llms_current_time( 'timestamp', 1 );\n\n\t\t\t// Error encountered scheduling the event.\n\t\t\tif ( ! as_schedule_single_action( $timestamp, $hook, $args ) ) {\n\t\t\t\t$timestamp = new WP_Error(\n\t\t\t\t\t'schedule-error',\n\t\t\t\t\t// Translators: %s = Processor ID.\n\t\t\t\t\tsprintf( __( 'There was an error dispatching the \"%s\" processor.', 'lifterlms' ), $id )\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn $timestamp;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/class.llms.notifications.query.php",
    "content": "<?php\n/**\n * LLMS_Notifications_Query class file\n *\n * @package LifterLMS/Notifications/Classes\n *\n * @since 3.8.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Notifications_Query class.\n *\n * Query LifterLMS Students for a given course/membership.\n *\n * @example\n *   $query = new LLMS_Notifications_Query( array(\n *       'subscriber' => 123, // null\n *       'per_page' => 10,\n *       'statuses' => 'new', // array( 'new', 'read', '...' )\n *       'types' => 'basic', // array( 'basic', 'email', '...' )\n *   ) );\n *\n * @since 3.8.0\n * @since 3.14.0 Unknown.\n */\nclass LLMS_Notifications_Query extends LLMS_Database_Query {\n\n\t/**\n\t * Identify the extending query\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'notifications';\n\n\t/**\n\t * Get an array of allowed notification statuses.\n\t *\n\t * @since 3.8.0\n\t * @since 7.1.0 Added 'error' among the available statuses.\n\t *\n\t * @return string[]\n\t */\n\tprivate function get_available_statuses() {\n\t\treturn array( 'new', 'sent', 'read', 'unread', 'deleted', 'failed', 'error' );\n\t}\n\n\t/**\n\t * Get the available notification types\n\t *\n\t * @return   string[]\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprivate function get_available_types() {\n\t\treturn array( 'basic', 'email' );\n\t}\n\n\t/**\n\t * Retrieve default arguments for a student query.\n\t *\n\t * @since 3.8.0\n\t * @since 3.11.0 Unknown.\n\t * @since 7.1.0 Explicitly exclude 'error' status.\n\t *               Drop usage of `this->get_filter( 'default_args' )` in favor of `'llms_notification_query_default_args'`.\n\t * @return array\n\t */\n\tprotected function get_default_args() {\n\n\t\t$args = array(\n\t\t\t'post_id'    => null,\n\t\t\t'subscriber' => null,\n\t\t\t'sort'       => array(\n\t\t\t\t'updated' => 'DESC',\n\t\t\t\t'id'      => 'DESC',\n\t\t\t),\n\t\t\t'statuses'   => array_values( array_diff( $this->get_available_statuses(), array( 'error' ) ) ),\n\t\t\t'triggers'   => array(),\n\t\t\t'types'      => array(),\n\t\t\t'user_id'    => null,\n\t\t);\n\n\t\t$args = wp_parse_args( $args, parent::get_default_args() );\n\n\t\t/**\n\t\t * Filters the notifications query's default args.\n\t\t *\n\t\t * @since 3.8.0\n\t\t * @since 7.1.0 Added `$notifications_query` parameter.\n\t\t *\n\t\t * @param array                    $args                Array of default arguments to set up the query with.\n\t\t * @param LLMS_Notifications_Query $notifications_query Instance of `LLMS_Notifications_Query`.\n\t\t */\n\t\treturn apply_filters( 'llms_notifications_query_default_args', $args, $this );\n\t}\n\n\t/**\n\t * Convert raw results to notification objects.\n\t *\n\t * @since 3.8.0\n\t * @since 7.1.0 When loading a notification, if errored, exclude it when not explictly requested.\n\t *              Drop usage of `this->get_filter( 'default_args' )` in favor of `llms_notifications_query_get_notifications`.\n\t *\n\t * @return LLMS_Notification[]\n\t */\n\tpublic function get_notifications() {\n\n\t\t$notifications = array();\n\t\t$results       = $this->get_results();\n\n\t\tif ( $results ) {\n\n\t\t\tforeach ( $results as $result ) {\n\t\t\t\t$notification = ( new LLMS_Notification( $result->id ) )->load();\n\n\t\t\t\t// If the notification status is 'error' and errored notifications were not requested, skip it.\n\t\t\t\tif ( 'error' === $notification->get( 'status' ) && ! in_array( 'error', $this->arguments['statuses'], true ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t$notifications[] = $notification;\n\n\t\t\t}\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $notifications;\n\t\t}\n\n\t\t/**\n\t\t * Filters the list of notifications.\n\t\t *\n\t\t * @since 3.8.0\n\t\t *\n\t\t * @param array                    $notifications       Array of {@see LLMS_Notification} instances.\n\t\t * @param LLMS_Notifications_Query $notifications_query Instance of `LLMS_Notifications_Query`.\n\t\t */\n\t\treturn apply_filters( 'llms_notifications_query_get_notifications', $notifications, $this );\n\t}\n\n\t/**\n\t * Parse arguments needed for the query\n\t *\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function parse_args() {\n\n\t\t$this->parse_statuses();\n\t\t$this->parse_types();\n\t}\n\n\t/**\n\t * Parse submitted statuses\n\t *\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprivate function parse_statuses() {\n\n\t\t$statuses = $this->arguments['statuses'];\n\n\t\t// allow strings to be submitted when only requesting one status\n\t\tif ( is_string( $statuses ) ) {\n\t\t\t$statuses = array( $statuses );\n\t\t}\n\n\t\t// ensure only valid statuses are used\n\t\t$statuses = array_intersect( $statuses, $this->get_available_statuses() );\n\n\t\t$this->arguments['statuses'] = $statuses;\n\t}\n\n\t/**\n\t * Parse submitted types\n\t *\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprivate function parse_types() {\n\n\t\t$types = $this->arguments['types'];\n\n\t\t// allow strings to be submitted when only requesting one status\n\t\tif ( is_string( $types ) ) {\n\t\t\t$types = array( $types );\n\t\t}\n\n\t\t// ensure only valid types are used\n\t\t$types                    = array_intersect( $types, $this->get_available_types() );\n\t\t$this->arguments['types'] = $types;\n\t}\n\n\t/**\n\t * Parse submitted triggers\n\t *\n\t * @return   void\n\t * @since    3.11.0\n\t * @version  3.11.0\n\t */\n\tprivate function parse_triggers() {\n\n\t\t$triggers = $this->arguments['triggers'];\n\n\t\t// allow strings to be submitted when only requesting one status\n\t\tif ( is_string( $triggers ) ) {\n\t\t\t$triggers = array( $triggers );\n\t\t}\n\n\t\t$this->arguments['triggers'] = $triggers;\n\t}\n\n\t/**\n\t * Prepare the SQL for the query.\n\t *\n\t * @since 3.8.0\n\t * @since 3.9.4 Unknown.\n\t * @since 6.0.0 Renamed from `preprare_query()`.\n\t * @since 7.1.0 Use `$this->sql_select_columns({columns})` to determine the columns to select.\n\t * @since 10.0.0 Build count_query from shared clauses instead of using SQL_CALC_FOUND_ROWS.\n\t *\n\t * @return string\n\t */\n\tprotected function prepare_query() {\n\n\t\tglobal $wpdb;\n\n\t\t$from  = \"FROM {$wpdb->prefix}lifterlms_notifications AS n\";\n\t\t$join  = \"LEFT JOIN {$wpdb->posts} AS p on p.ID = n.post_id\";\n\t\t$where = $this->sql_where();\n\n\t\tif ( ! $this->get( 'no_found_rows' ) ) {\n\t\t\t$this->count_query = \"SELECT COUNT(*) {$from} {$join} {$where}\";\n\t\t}\n\n\t\t$vars = array(\n\t\t\t$this->get_skip(),\n\t\t\t$this->get( 'per_page' ),\n\t\t);\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- SQL is prepared in other functions.\n\t\t$sql = $wpdb->prepare(\n\t\t\t\"SELECT {$this->sql_select_columns()}\n\t\t\t{$from}\n\t\t\t{$join}\n\t\t\t{$where}\n\t\t\t{$this->sql_orderby()}\n\t\t\tLIMIT %d, %d\n\t\t\t;\",\n\t\t\t$vars\n\t\t);\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\treturn $sql;\n\t}\n\n\t/**\n\t * Retrieve the prepared SQL for the ORDER clause.\n\t *\n\t * Slightly modified from abstract to include the table name to prevent ambiguous errors.\n\t *\n\t * @since 3.9.2\n\t * @since 7.1.0 Drop usage of `$this->get_filter('where')` in favor of `llms_notifications_query_where`.\n\t *\n\t * @return string\n\t */\n\tprotected function sql_orderby() {\n\n\t\t$sql = 'ORDER BY';\n\n\t\t$comma = false;\n\n\t\tforeach ( $this->get( 'sort' ) as $orderby => $order ) {\n\t\t\t$pre   = ( $comma ) ? ', ' : ' ';\n\t\t\t$sql  .= $pre . 'n.' . sanitize_sql_orderby( \"{$orderby} {$order}\" );\n\t\t\t$comma = true;\n\t\t}\n\n\t\tif ( $this->get( 'suppress_filters' ) ) {\n\t\t\treturn $sql;\n\t\t}\n\n\t\t/**\n\t\t * Filters the query WHERE clause.\n\t\t *\n\t\t * @since 7.1.0\n\t\t *\n\t\t * @param string                   $sql                 The WHERE clause of the query.\n\t\t * @param LLMS_Notifications_Query $notifications_query Instance of LLMS_Events_Query.\n\t\t */\n\t\treturn apply_filters( 'llms_notifications_query_where', $sql, $this );\n\t}\n\n\t/**\n\t * Retrieve the prepared SQL for the WHERE clause\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.14.0\n\t */\n\tprivate function sql_where() {\n\n\t\tglobal $wpdb;\n\n\t\t$where = 'WHERE 1';\n\n\t\t$post_statuses = array_merge( array( 'publish' ), array_keys( llms_get_order_statuses() ) );\n\t\t$post_statuses = array_map( array( $this, 'escape_and_quote_string' ), $post_statuses );\n\t\t$where        .= sprintf( ' AND p.post_status IN ( %s )', implode( ', ', $post_statuses ) );\n\n\t\t// these args are all \"whered\" in the same way\n\t\t$wheres = array(\n\t\t\t'statuses' => 'status',\n\t\t\t'triggers' => 'trigger_id',\n\t\t\t'types'    => 'type',\n\t\t);\n\n\t\t// loop through them and build the where clauses based off the submitted data\n\t\tforeach ( $wheres as $arg_name => $col_name ) {\n\n\t\t\t$arg = $this->get( $arg_name );\n\t\t\tif ( $arg ) {\n\t\t\t\t$prepped = array_map( array( $this, 'escape_and_quote_string' ), $arg );\n\t\t\t\t$where  .= sprintf( ' AND n.%1$s IN( %2$s )', $col_name, implode( ', ', $prepped ) );\n\t\t\t}\n\t\t}\n\n\t\t// add subscriber info if set\n\t\t$subscriber = $this->get( 'subscriber' );\n\t\tif ( $subscriber ) {\n\t\t\t$where .= $wpdb->prepare( ' AND n.subscriber = %s', $subscriber );\n\t\t}\n\n\t\t// add post and user id checks\n\t\tforeach ( array( 'post_id', 'user_id' ) as $var ) {\n\t\t\t$arg = $this->get( $var );\n\t\t\tif ( $arg ) {\n\t\t\t\t$where .= sprintf( ' AND n.%1$s = %2$d', esc_sql( $var ), absint( $arg ) );\n\t\t\t}\n\t\t}\n\n\t\treturn $where;\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.achievement.earned.php",
    "content": "<?php\n/**\n * Notification Controller: Achievement Earned\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Achievement Earned\n *\n * @since 3.8.0\n */\nclass LLMS_Notification_Controller_Achievement_Earned extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'achievement_earned';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 3;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'llms_user_earned_achievement' );\n\n\t/**\n\t * Callback function, called upon achievement post generation\n\t *\n\t * @param    int $user_id          WP User ID of the user who earned the achievement\n\t * @param    int $achievement_id   WP Post ID of the new achievement post\n\t * @param    int $related_post_id  WP Post ID of the post which triggered the achievement to be awarded\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function action_callback( $user_id = null, $achievement_id = null, $related_post_id = null ) {\n\n\t\t$this->user_id         = $user_id;\n\t\t$this->post_id         = $achievement_id;\n\t\t$this->related_post_id = $related_post_id;\n\n\t\t$this->send();\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Achievement Earned', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\t}\n\n\t/**\n\t * Determine what types are supported\n\t * Extending classes can override this function in order to add or remove support\n\t * 3rd parties should add support via filter on $this->get_supported_types()\n\t *\n\t * @return   array        associative array, keys are the ID/db type, values should be translated display types\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_supported_types() {\n\t\treturn array(\n\t\t\t'basic' => __( 'Popup', 'lifterlms' ),\n\t\t);\n\t}\n}\n\nreturn LLMS_Notification_Controller_Achievement_Earned::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php",
    "content": "<?php\n/**\n * Notification Controller: Certificate Earned\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Certificate Earned\n *\n * @since 3.8.0\n */\nclass LLMS_Notification_Controller_Certificate_Earned extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'certificate_earned';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 3;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'llms_user_earned_certificate' );\n\n\t/**\n\t * Callback function, called upon certificate post generation\n\t *\n\t * @param    int $user_id          WP User ID of the user who earned the certificate\n\t * @param    int $certificate_id   WP Post ID of the new achievement post\n\t * @param    int $related_post_id  WP Post ID of the post which triggered the certificate to be awarded\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function action_callback( $user_id = null, $certificate_id = null, $related_post_id = null ) {\n\n\t\t$this->user_id         = $user_id;\n\t\t$this->post_id         = $certificate_id;\n\t\t$this->related_post_id = $related_post_id;\n\n\t\t$this->send();\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Certificate Earned', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\t}\n\n\t/**\n\t * Determine what types are supported\n\t * Extending classes can override this function in order to add or remove support\n\t * 3rd parties should add support via filter on $this->get_supported_types()\n\t *\n\t * @return   array        associative array, keys are the ID/db type, values should be translated display types\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_supported_types() {\n\t\treturn array(\n\t\t\t'basic' => __( 'Popup', 'lifterlms' ),\n\t\t);\n\t}\n}\n\nreturn LLMS_Notification_Controller_Certificate_Earned::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.course.complete.php",
    "content": "<?php\n/**\n * Notification Controller: Course Complete\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Course Complete\n *\n * @since 3.8.0\n */\nclass LLMS_Notification_Controller_Course_Complete extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'course_complete';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'lifterlms_course_completed' );\n\n\t/**\n\t * Callback function called when a course is completed by a student\n\t *\n\t * @param    int $student_id  WP User ID of a LifterLMS Student\n\t * @param    int $course_id   WP Post ID of a LifterLMS Course\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function action_callback( $student_id = null, $course_id = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $course_id;\n\t\t$this->course  = llms_get_post( $course_id );\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'course_author':\n\t\t\t\t$uid = $this->course->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Course Complete', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'course_author', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Course_Complete::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.course.track.complete.php",
    "content": "<?php\n/**\n * Notification Controller: Course Track Complete\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Course Track Complete\n *\n * @since 3.8.0\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Notification_Controller_Course_Track_Complete extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'course_track_complete';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'lifterlms_course_track_completed' );\n\n\t/**\n\t * @var LLMS_Track\n\t * @since 3.8.0\n\t */\n\tpublic $track;\n\n\t/**\n\t * Callback function called when a course track is completed by a student\n\t *\n\t * @param    int $student_id  WP User ID of a LifterLMS Student\n\t * @param    int $course_track_id   WP Post ID of a LifterLMS Course\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function action_callback( $student_id = null, $course_track_id = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $course_track_id;\n\t\t$this->track   = new LLMS_Track( $course_track_id );\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Course Track Complete', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Course_Track_Complete::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.enrollment.php",
    "content": "<?php\n/**\n * Notification Controller: Enrollment\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Enrollment\n *\n * @since 3.8.0\n */\nclass LLMS_Notification_Controller_Enrollment extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var [type]\n\t */\n\tpublic $id = 'enrollment';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array(\n\t\t'llms_user_enrolled_in_course',\n\t\t'llms_user_added_to_membership_level',\n\t);\n\n\t/**\n\t * Callback function, called after enrollment into a course\n\t *\n\t * @param    int $user_id     WP User ID of the user\n\t * @param    int $post_id   WP Post ID of the course or membership\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function action_callback( $user_id = null, $post_id = null ) {\n\n\t\t$this->user_id = $user_id;\n\t\t$this->post_id = $post_id;\n\t\t$this->course  = llms_get_post( $post_id );\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'author':\n\t\t\t\t$uid = $this->course->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Enrollment', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'author', 'no' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Enrollment::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.lesson.complete.php",
    "content": "<?php\n/**\n * Notification Controller: Lesson Complete\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Lesson Complete\n *\n * @since 3.8.0\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Notification_Controller_Lesson_Complete extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'lesson_complete';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'lifterlms_lesson_completed' );\n\n\t/**\n\t * @var LLMS_Lesson\n\t * @since 3.8.0\n\t */\n\tpublic $lesson;\n\n\t/**\n\t * Callback function called when a lesson is completed by a student\n\t *\n\t * @param    int $student_id  WP User ID of a LifterLMS Student\n\t * @param    int $lesson_id   WP Post ID of a LifterLMS Lesson\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function action_callback( $student_id = null, $lesson_id = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $lesson_id;\n\t\t$this->lesson  = llms_get_post( $lesson_id );\n\t\t$this->course  = $this->lesson->get_course();\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'course_author':\n\t\t\t\t$uid = $this->course->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'lesson_author':\n\t\t\t\t$uid = $this->lesson->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Lesson Complete', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'lesson_author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'course_author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Lesson_Complete::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.manual.payment.due.php",
    "content": "<?php\n/**\n * Notification Controller: Manual Gateway Payment Due\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.10.0\n * @version 3.10.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Manual Gateway Payment Due\n *\n * @since 3.10.0\n */\nclass LLMS_Notification_Controller_Manual_Payment_Due extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var [type]\n\t */\n\tpublic $id = 'manual_payment_due';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array(\n\t\t'llms_manual_payment_due',\n\t);\n\n\t/**\n\t * Callback function called when a payment retry is scheduled\n\t *\n\t * @param    int $order   Instance of an LLMS_Order\n\t * @return   void\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tpublic function action_callback( $order = null ) {\n\n\t\t$this->user_id = $order->get( 'user_id' );\n\t\t$this->post_id = $order->get( 'id' );\n\n\t\t$this->send();\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'author':\n\t\t\t\t$order = llms_get_post( $this->post_id );\n\t\t\t\tif ( ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$product = $order->get_product();\n\t\t\t\tif ( is_a( $product, 'WP_Post' ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$uid = $product->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\t}\n\n\t/**\n\t * Determine what types are supported\n\t * Extending classes can override this function in order to add or remove support\n\t * 3rd parties should add support via filter on $this->get_supported_types()\n\t *\n\t * @return   array        associative array, keys are the ID/db type, values should be translated display types\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tprotected function set_supported_types() {\n\t\treturn array(\n\t\t\t'basic' => __( 'Popup', 'lifterlms' ),\n\t\t\t'email' => __( 'Email', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Gateway: Manual - Payment Due', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.10.0\n\t * @version  3.10.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\t}\n}\n\nreturn LLMS_Notification_Controller_Manual_Payment_Due::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.payment.retry.php",
    "content": "<?php\n/**\n * Notification Controller: Payment Retry Scheduled\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.10.0\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Payment Retry Scheduled\n *\n * @since 3.10.0\n */\nclass LLMS_Notification_Controller_Payment_Retry extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id = 'payment_retry';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var integer\n\t */\n\tprotected $action_accepted_args = 1;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var array\n\t */\n\tprotected $action_hooks = array(\n\t\t'llms_send_automatic_payment_retry_notification',\n\t);\n\n\t/**\n\t * Callback function called when a payment retry is scheduled\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param int $order Instance of an LLMS_Order.\n\t * @return void\n\t */\n\tpublic function action_callback( $order = null ) {\n\n\t\t$this->user_id = $order->get( 'user_id' );\n\t\t$this->post_id = $order->get( 'id' );\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param string $subscriber Subscriber type string.\n\t * @return int|false\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'author':\n\t\t\t\t$order = llms_get_post( $this->post_id );\n\t\t\t\tif ( ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$product = $order->get_product();\n\t\t\t\tif ( is_a( $product, 'WP_Post' ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$uid = $product->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t *\n\t * Used on settings screens.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Payment Retry Scheduled', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @since 3.10.0\n\t *\n\t * @param string $type Notification type id.\n\t * @return array\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Payment_Retry::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.purchase.receipt.php",
    "content": "<?php\n/**\n * Notification Controller: Transaction Success\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Transaction Success\n *\n * @since 3.8.0\n * @since 3.24.0 Unknown\n */\nclass LLMS_Notification_Controller_Purchase_Receipt extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id = 'purchase_receipt';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var integer\n\t */\n\tprotected $action_accepted_args = 1;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var array\n\t */\n\tprotected $action_hooks = array(\n\t\t'lifterlms_resend_transaction_receipt',\n\t\t'lifterlms_transaction_status_succeeded',\n\t);\n\n\t/**\n\t * Determines if test notifications can be sent\n\t *\n\t * @var bool\n\t */\n\tprotected $testable = array(\n\t\t'basic' => false,\n\t\t'email' => true,\n\t);\n\n\t/**\n\t * Callback function called when a lesson is completed by a student\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param int $transaction Instance of a LLMS_Transaction.\n\t * @return void\n\t */\n\tpublic function action_callback( $transaction = null ) {\n\n\t\t$order = $transaction->get_order();\n\n\t\t/**\n\t\t * Filter to avoid sending notification for purchase receipts.\n\t\t *\n\t\t * @since 9.1.2\n\t\t */\n\t\tif ( ! apply_filters( 'llms_send_purchase_receipt_notification', true, $order, $transaction ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->user_id = $order->get( 'user_id' );\n\t\t$this->post_id = $transaction->get( 'id' );\n\n\t\t$this->send();\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @since 3.8.0\n\t * @since 3.10.2 Unknown.\n\t *\n\t * @param string $subscriber Subscriber type string.\n\t * @return int|false\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'author':\n\t\t\t\t$txn   = llms_get_post( $this->post_id );\n\t\t\t\t$order = $txn->get_order();\n\t\t\t\tif ( ! $order ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$product = $order->get_product();\n\t\t\t\tif ( ! $product ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$uid = $product->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\t}\n\n\t/**\n\t * Determine what types are supported\n\t *\n\t * Extending classes can override this function in order to add or remove support.\n\t * 3rd parties should add support via filter on $this->get_supported_types().\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array Associative array, keys are the ID/db type, values should be translated display types.\n\t */\n\tprotected function set_supported_types() {\n\t\treturn array(\n\t\t\t'email' => __( 'Email', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Get an array of LifterLMS Admin Page settings to send test notifications\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @return array\n\t */\n\tpublic function get_test_settings( $type ) {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'llms_transaction',\n\t\t\t\t'posts_per_page' => 25,\n\t\t\t)\n\t\t);\n\n\t\t$options = array(\n\t\t\t'' => '',\n\t\t);\n\t\tforeach ( $query->posts as $post ) {\n\t\t\t$transaction = llms_get_post( $post );\n\t\t\t$order       = $transaction->get_order();\n\t\t\t$student     = llms_get_student( $order->get( 'user_id' ) );\n\t\t\tif ( $transaction && $student ) {\n\t\t\t\t$options[ $transaction->get( 'id' ) ] = esc_attr(\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t// Translators: %1$d = The Order ID; %2$s The customer's full name; %3$s The product title.\n\t\t\t\t\t\t__( 'Order #%1$d from %2$s for \"%3$s\"', 'lifterlms' ),\n\t\t\t\t\t\t$order->get( 'id' ),\n\t\t\t\t\t\t$student->get_name(),\n\t\t\t\t\t\t$order->get( 'product_title' )\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'class'             => 'llms-select2',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t'data-placeholder' => __( 'Select a transaction', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'default'           => '',\n\t\t\t\t'id'                => 'transaction_id',\n\t\t\t\t'desc'              => '<br/>' . __( 'Send yourself a test notification using information from the selected transaction.', 'lifterlms' ),\n\t\t\t\t'options'           => $options,\n\t\t\t\t'title'             => __( 'Send a Test', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t\t// 'selected' => false,\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t *\n\t * Used on settings screens.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Purchase Receipt', 'lifterlms' );\n\t}\n\n\t/**\n\t * Send a test notification to the currently logged in users\n\t *\n\t * Extending classes should redefine this in order to properly setup the controller with post_id and user_id data.\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @param array  $data Array of test notification data as specified by $this->get_test_data().\n\t * @return int|false\n\t */\n\tpublic function send_test( $type, $data = array() ) {\n\n\t\tif ( empty( $data['transaction_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$transaction   = llms_get_post( $data['transaction_id'] );\n\t\t$order         = $transaction->get_order();\n\t\t$this->user_id = $order->get( 'user_id' );\n\t\t$this->post_id = $transaction->get( 'id' );\n\n\t\treturn parent::send_test( $type );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type Notification type id.\n\t * @return array\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'author', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\t}\n}\n\nreturn LLMS_Notification_Controller_Purchase_Receipt::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.quiz.failed.php",
    "content": "<?php\n/**\n * Notification Controller: Quiz Failed\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 6.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Quiz Failed\n *\n * @since 3.8.0\n * @since 3.24.0 Unknown.\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Notification_Controller_Quiz_Failed extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var   string\n\t */\n\tpublic $id = 'quiz_failed';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'lifterlms_quiz_failed' );\n\n\t/**\n\t * @var LLMS_Quiz\n\t * @since 3.8.0\n\t */\n\tpublic $quiz;\n\n\t/**\n\t * Determines if test notifications can be sent\n\t *\n\t * @var  array\n\t */\n\tprotected $testable = array(\n\t\t'basic' => false,\n\t\t'email' => true,\n\t);\n\n\t/**\n\t * Callback function called when a quiz is failed by a student\n\t *\n\t * @param    int   $student_id  WP User ID of a LifterLMS Student\n\t * @param    array $quiz_id     WP Post ID of a LifterLMS quiz\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.16.6\n\t */\n\tpublic function action_callback( $student_id = null, $quiz_id = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $quiz_id;\n\t\t$this->quiz    = llms_get_post( $quiz_id );\n\t\tif ( ! $this->quiz ) {\n\t\t\treturn;\n\t\t}\n\t\t$this->course = $this->quiz->get_course();\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Get an array of LifterLMS Admin Page settings to send test notifications.\n\t *\n\t * @since 3.24.0\n\t * @since 6.1.0 Fixed access of protected LLMS_Abstract_Query properties.\n\t *              Fixed issue where void was returned instead of an empty array if the type was 'email'.\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @return array\n\t */\n\tpublic function get_test_settings( $type ) {\n\n\t\tif ( 'email' !== $type ) {\n\t\t\treturn array();\n\t\t}\n\n\t\t$query    = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'per_page' => 25,\n\t\t\t\t'status'   => 'fail',\n\t\t\t)\n\t\t);\n\t\t$options  = array(\n\t\t\t'' => '',\n\t\t);\n\t\t$attempts = array();\n\t\t$results  = $query->get_results();\n\t\tif ( $query->has_results() ) {\n\t\t\tforeach ( $query->get_attempts() as $attempt ) {\n\t\t\t\t$quiz    = llms_get_post( $attempt->get( 'quiz_id' ) );\n\t\t\t\t$student = llms_get_student( $attempt->get( 'student_id' ) );\n\t\t\t\tif ( $attempt && $student ) {\n\t\t\t\t\t$options[ $attempt->get( 'id' ) ] = esc_attr( sprintf( __( 'Attempt #%1$d for Quiz \"%2$s\" by %3$s', 'lifterlms' ), $attempt->get( 'id' ), $quiz->get( 'title' ), $student->get_name() ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'class'             => 'llms-select2',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t'data-placeholder' => __( 'Select a failed quiz', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'default'           => '',\n\t\t\t\t'id'                => 'attempt_id',\n\t\t\t\t'desc'              => '<br/>' . __( 'Send yourself a test notification using information from the selected quiz.', 'lifterlms' ),\n\t\t\t\t'options'           => $options,\n\t\t\t\t'title'             => __( 'Send a Test', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t),\n\t\t);\n\t}\n\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.13.1\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'course_author':\n\t\t\t\tif ( $this->course ) {\n\t\t\t\t\t$uid = $this->course->get( 'author' );\n\t\t\t\t} else {\n\t\t\t\t\t$uid = $this->quiz->post->post_author;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.24.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Quizzes: Quiz Failed', 'lifterlms' );\n\t}\n\n\t/**\n\t * Send a test notification to the currently logged in users\n\t * Extending classes should redefine this in order to properly setup the controller with post_id and user_id data\n\t *\n\t * @param    string $type  notification type [basic|email]\n\t * @param    array  $data  array of test notification data as specified by $this->get_test_data()\n\t * @return   int|false\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function send_test( $type, $data = array() ) {\n\t\tif ( empty( $data['attempt_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\t\t$attempt       = new LLMS_Quiz_Attempt( $data['attempt_id'] );\n\t\t$this->user_id = $attempt->get( 'student_id' );\n\t\t$this->post_id = $attempt->get( 'quiz_id' );\n\t\t$this->quiz    = llms_get_post( $attempt->get( 'quiz_id' ) );\n\t\t$this->course  = $this->quiz->get_course();\n\t\treturn parent::send_test( $type );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'course_author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Quiz_Failed::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.quiz.graded.php",
    "content": "<?php\n/**\n * Notification Controller: Quiz Graded\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.24.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Quiz Graded\n *\n * @since 3.24.0\n */\nclass LLMS_Notification_Controller_Quiz_Graded extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'quiz_graded';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 3;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'llms_quiz_graded' );\n\n\t/**\n\t * Determines if test notifications can be sent\n\t *\n\t * @var  bool\n\t */\n\tprotected $testable = array(\n\t\t'basic' => false,\n\t\t'email' => true,\n\t);\n\n\t/**\n\t * Callback function called when a quiz is failed by a student\n\t *\n\t * @param    int   $student_id  WP User ID of a LifterLMS Student\n\t * @param    array $quiz_id     WP Post ID of a LifterLMS quiz\n\t * @param    obj   $attempt     LLMS_Quiz_Attempt\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function action_callback( $student_id = null, $quiz_id = null, $attempt = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $attempt->get( 'id' );\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Get an array of LifterLMS Admin Page settings to send test notifications\n\t *\n\t * @param    string $type  notification type [basic|email]\n\t * @return   array\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function get_test_settings( $type ) {\n\n\t\tif ( 'email' !== $type ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'per_page' => 25,\n\t\t\t)\n\t\t);\n\n\t\t$options = array(\n\t\t\t'' => '',\n\t\t);\n\n\t\t$attempts = array();\n\n\t\tif ( $query->has_results() ) {\n\t\t\tforeach ( $query->get_attempts() as $attempt ) {\n\t\t\t\t$quiz    = llms_get_post( $attempt->get( 'quiz_id' ) );\n\t\t\t\t$student = llms_get_student( $attempt->get( 'student_id' ) );\n\t\t\t\tif ( $attempt && $student && $quiz ) {\n\t\t\t\t\t$options[ $attempt->get( 'id' ) ] = esc_attr( sprintf( __( 'Attempt #%1$d for Quiz \"%2$s\" by %3$s', 'lifterlms' ), $attempt->get( 'id' ), $quiz->get( 'title' ), $student->get_name() ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'class'             => 'llms-select2',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t'data-placeholder' => __( 'Select a passed quiz', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'default'           => '',\n\t\t\t\t'id'                => 'attempt_id',\n\t\t\t\t'desc'              => '<br/>' . __( 'Send yourself a test notification using information from the selected quiz.', 'lifterlms' ),\n\t\t\t\t'options'           => $options,\n\t\t\t\t'title'             => __( 'Send a Test', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t),\n\t\t);\n\t}\n\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Quizzes: Quiz Graded', 'lifterlms' );\n\t}\n\n\t/**\n\t * Send a test notification to the currently logged in users\n\t * Extending classes should redefine this in order to properly setup the controller with post_id and user_id data\n\t *\n\t * @param    string $type  notification type [basic|email]\n\t * @param    array  $data  array of test notification data as specified by $this->get_test_data()\n\t * @return   int|false\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function send_test( $type, $data = array() ) {\n\n\t\tif ( empty( $data['attempt_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$attempt       = new LLMS_Quiz_Attempt( $data['attempt_id'] );\n\t\t$this->user_id = $attempt->get( 'student_id' );\n\t\t$this->post_id = $attempt->get( 'id' );\n\t\treturn parent::send_test( $type );\n\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Quiz_Graded::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.quiz.passed.php",
    "content": "<?php\n/**\n * Notification Controller: Quiz Passed\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Quiz Passed\n *\n * @since 3.8.0\n * @since 3.24.0 Unknown.\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Notification_Controller_Quiz_Passed extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'quiz_passed';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'lifterlms_quiz_passed' );\n\n\t/**\n\t * @var LLMS_Quiz\n\t * @since 3.8.0\n\t */\n\tpublic $quiz;\n\n\t/**\n\t * Determines if test notifications can be sent\n\t *\n\t * @var  bool\n\t */\n\tprotected $testable = array(\n\t\t'basic' => false,\n\t\t'email' => true,\n\t);\n\n\t/**\n\t * Callback function called when a quiz is failed by a student\n\t *\n\t * @param    int   $student_id  WP User ID of a LifterLMS Student\n\t * @param    array $quiz_id     WP Post ID of a LifterLMS quiz\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.16.6\n\t */\n\tpublic function action_callback( $student_id = null, $quiz_id = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $quiz_id;\n\t\t$this->quiz    = llms_get_post( $quiz_id );\n\t\tif ( ! $this->quiz ) {\n\t\t\treturn;\n\t\t}\n\t\t$this->course = $this->quiz->get_course();\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Get an array of LifterLMS Admin Page settings to send test notifications\n\t *\n\t * @param    string $type  notification type [basic|email]\n\t * @return   array\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function get_test_settings( $type ) {\n\n\t\tif ( 'email' !== $type ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'per_page' => 25,\n\t\t\t\t'status'   => 'pass',\n\t\t\t)\n\t\t);\n\n\t\t$options = array(\n\t\t\t'' => '',\n\t\t);\n\n\t\t$attempts = array();\n\n\t\tif ( $query->has_results() ) {\n\t\t\tforeach ( $query->get_attempts() as $attempt ) {\n\t\t\t\t$quiz    = llms_get_post( $attempt->get( 'quiz_id' ) );\n\t\t\t\t$student = llms_get_student( $attempt->get( 'student_id' ) );\n\t\t\t\tif ( $attempt && $student && $quiz ) {\n\t\t\t\t\t$options[ $attempt->get( 'id' ) ] = esc_attr( sprintf( __( 'Attempt #%1$d for Quiz \"%2$s\" by %3$s', 'lifterlms' ), $attempt->get( 'id' ), $quiz->get( 'title' ), $student->get_name() ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'class'             => 'llms-select2',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t'data-placeholder' => __( 'Select a passed quiz', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'default'           => '',\n\t\t\t\t'id'                => 'attempt_id',\n\t\t\t\t'desc'              => '<br/>' . __( 'Send yourself a test notification using information from the selected quiz.', 'lifterlms' ),\n\t\t\t\t'options'           => $options,\n\t\t\t\t'title'             => __( 'Send a Test', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t),\n\t\t);\n\t}\n\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'course_author':\n\t\t\t\tif ( $this->course ) {\n\t\t\t\t\t$uid = $this->course->get( 'author' );\n\t\t\t\t} else {\n\t\t\t\t\t$uid = $this->quiz->post->post_author;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.24.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Quizzes: Quiz Passed', 'lifterlms' );\n\t}\n\n\t/**\n\t * Send a test notification to the currently logged in users\n\t * Extending classes should redefine this in order to properly setup the controller with post_id and user_id data\n\t *\n\t * @param    string $type  notification type [basic|email]\n\t * @param    array  $data  array of test notification data as specified by $this->get_test_data()\n\t * @return   int|false\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function send_test( $type, $data = array() ) {\n\n\t\tif ( empty( $data['attempt_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$attempt       = new LLMS_Quiz_Attempt( $data['attempt_id'] );\n\t\t$this->user_id = $attempt->get( 'student_id' );\n\t\t$this->post_id = $attempt->get( 'quiz_id' );\n\t\t$this->quiz    = llms_get_post( $attempt->get( 'quiz_id' ) );\n\t\t$this->course  = $this->quiz->get_course();\n\t\treturn parent::send_test( $type );\n\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'course_author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Quiz_Passed::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.section.complete.php",
    "content": "<?php\n/**\n * Notification Controller: Section Complete\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Section Complete\n *\n * @since 3.8.0\n * @since 3.30.3 Explicitly define class properties.\n */\nclass LLMS_Notification_Controller_Section_Complete extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'section_complete';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'lifterlms_section_completed' );\n\n\t/**\n\t * @var LLMS_Section\n\t * @since 3.8.0\n\t */\n\tpublic $section;\n\n\t/**\n\t * Callback function called when a section is completed by a student\n\t *\n\t * @param    int $student_id  WP User ID of a LifterLMS Student\n\t * @param    int $section_id   WP Post ID of a LifterLMS Section\n\t * @return   void\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function action_callback( $student_id = null, $section_id = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $section_id;\n\t\t$this->section = llms_get_post( $section_id );\n\t\t$this->course  = $this->section->get_course();\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'course_author':\n\t\t\t\t$uid = $this->course->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Section Complete', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'course_author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Section_Complete::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.student.welcome.php",
    "content": "<?php\n/**\n * Notification Controller: Student Welcome\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.8.0\n * @version 3.39.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Student Welcome\n *\n * @since 3.8.0\n * @since 3.33.2 Add test send functionality.\n * @since 3.39.0 Added `llms_rest_student_registered` as action hook.\n */\nclass LLMS_Notification_Controller_Student_Welcome extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var  [type]\n\t */\n\tpublic $id = 'student_welcome';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var integer\n\t */\n\tprotected $action_accepted_args = 1;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var array\n\t */\n\tprotected $action_hooks = array(\n\t\t'lifterlms_user_registered',\n\t\t'llms_rest_student_registered',\n\t);\n\n\t/**\n\t * Determines if test notifications can be sent\n\t *\n\t * @var bool\n\t */\n\tprotected $testable = array(\n\t\t'basic' => false,\n\t\t'email' => true,\n\t);\n\n\t/**\n\t * Callback function called when a lesson is completed by a student\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param int $user_id WP_User ID.\n\t * @return void\n\t */\n\tpublic function action_callback( $user_id = null ) {\n\n\t\t$this->user_id = $user_id;\n\t\t$this->post_id = null;\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $subscriber Subscriber type string.\n\t * @return int|false\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Determine what types are supported\n\t *\n\t * Extending classes can override this function in order to add or remove support\n\t * 3rd parties should add support via filter on $this->get_supported_types().\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array Associative array, keys are the ID/db type, values should be translated display types.\n\t */\n\tprotected function set_supported_types() {\n\t\treturn array(\n\t\t\t'email' => __( 'Email', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Get an array of LifterLMS Admin Page settings to send test notifications\n\t *\n\t * @since 3.33.2\n\t *\n\t * @param string $type Notification type [basic|email]\n\t * @return array\n\t */\n\tpublic function get_test_settings( $type ) {\n\n\t\t$query = new WP_User_Query(\n\t\t\tarray(\n\t\t\t\t'number' => 25,\n\t\t\t)\n\t\t);\n\n\t\t$options = array(\n\t\t\t'' => '',\n\t\t);\n\t\tforeach ( $query->get_results() as $user ) {\n\t\t\t$student = llms_get_student( $user );\n\t\t\tif ( $student ) {\n\t\t\t\t$options[ $student->get_id() ] = esc_attr( sprintf( __( '%1$s <%2$s>', 'lifterlms' ), $student->get_name(), $student->get( 'user_email' ) ) );\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'class'             => 'llms-select2',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t'data-placeholder' => __( 'Select a user', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'default'           => '',\n\t\t\t\t'id'                => 'user_id',\n\t\t\t\t'desc'              => '<br/>' . __( 'Send yourself a test notification using information for the selected user.', 'lifterlms' ),\n\t\t\t\t'options'           => $options,\n\t\t\t\t'title'             => __( 'Send a Test', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification used on settings screens\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Student Welcome', 'lifterlms' );\n\t}\n\n\t/**\n\t * Send a test notification to the currently logged in users\n\t *\n\t * Extending classes should redefine this in order to properly setup the controller with post_id and user_id data.\n\t *\n\t * @since 3.33.2\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @param array  $data Array of test notification data as specified by $this->get_test_data().\n\t *\n\t * @return int|false\n\t */\n\tpublic function send_test( $type, $data = array() ) {\n\n\t\tif ( empty( $data['user_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$this->user_id = $data['user_id'];\n\t\t$this->post_id = null;\n\n\t\treturn parent::send_test( $type );\n\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @param string $type Notification type id.\n\t * @return array\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Student_Welcome::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.subscription.cancelled.php",
    "content": "<?php\n/**\n * Notification Controller: Subscription Cancelled (by Student)\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 3.17.8\n * @version 3.17.8\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Subscription Cancelled (by Student)\n *\n * @since 3.17.8\n */\nclass LLMS_Notification_Controller_Subscription_Cancelled extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var [type]\n\t */\n\tpublic $id = 'subscription_cancelled';\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var  integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var  array\n\t */\n\tprotected $action_hooks = array( 'llms_subscription_cancelled_by_student' );\n\n\t/**\n\t * Callback function, called upon student subscription cancellation\n\t *\n\t * @param    obj $order       Instance of the LLMS_Order\n\t * @param    int $student_id  WP User ID of the Student\n\t * @return   void\n\t * @since    3.17.8\n\t * @version  3.17.8\n\t */\n\tpublic function action_callback( $order = null, $student_id = null ) {\n\n\t\t$this->user_id = $student_id;\n\t\t$this->post_id = $order->get( 'id' );\n\n\t\t$this->send();\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID\n\t *\n\t * @param    string $subscriber  subscriber type string\n\t * @return   int|false\n\t * @since    3.17.8\n\t * @version  3.17.8\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'author':\n\t\t\t\t$order = llms_get_post( $this->post_id );\n\t\t\t\tif ( ! $order ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$product = $order->get_product();\n\t\t\t\tif ( ! $product ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$uid = $product->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t * used on settings screens\n\t *\n\t * @return   string\n\t * @since    3.17.8\n\t * @version  3.17.8\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Subscription Cancellation Notice', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @param    string $type  notification type id\n\t * @return   array\n\t * @since    3.17.8\n\t * @version  3.17.8\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'author', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n\t/**\n\t * Determine what types are supported\n\t * Extending classes can override this function in order to add or remove support\n\t * 3rd parties should add support via filter on $this->get_supported_types()\n\t *\n\t * @return   array        associative array, keys are the ID/db type, values should be translated display types\n\t * @since    3.17.8\n\t * @version  3.17.8\n\t */\n\tprotected function set_supported_types() {\n\t\treturn array(\n\t\t\t'email' => __( 'Email', 'lifterlms' ),\n\t\t);\n\t}\n\n}\n\nreturn LLMS_Notification_Controller_Subscription_Cancelled::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/class.llms.notification.controller.upcoming.payment.reminder.php",
    "content": "<?php\n/**\n * Notification Controller: Upcoming Payment Reminder\n *\n * @package LifterLMS/Notifications/Controllers/Classes\n *\n * @since 5.2.0\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Controller: Upcoming Payment Reminder\n *\n * @since 5.2.0\n */\nclass LLMS_Notification_Controller_Upcoming_Payment_Reminder extends LLMS_Abstract_Notification_Controller {\n\n\t/**\n\t * Trigger Identifier\n\t *\n\t * @var string\n\t */\n\tpublic $id = 'upcoming_payment_reminder';\n\n\t/**\n\t * Action hooks used to trigger sending of the notification\n\t *\n\t * @var array\n\t */\n\tprotected $action_hooks = array(\n\t\t'llms_send_upcoming_payment_reminder_notification',\n\t);\n\n\t/**\n\t * Determines if test notifications can be sent\n\t *\n\t * @var bool\n\t */\n\tprotected $testable = array(\n\t\t'basic' => false,\n\t\t'email' => true,\n\t);\n\n\t/**\n\t * Number of accepted arguments passed to the callback function\n\t *\n\t * @var integer\n\t */\n\tprotected $action_accepted_args = 2;\n\n\t/**\n\t * Add an action to trigger the notification to send\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tprotected function add_actions() {\n\n\t\tparent::add_actions();\n\n\t\t// Add actions to recurring payment scheduling/unscheduling.\n\t\tadd_action( 'llms_charge_recurring_payment_scheduled', array( $this, 'schedule_upcoming_payment_reminders' ), 10, 2 );\n\t\tadd_action( 'llms_charge_recurring_payment_unscheduled', array( $this, 'unschedule_upcoming_payment_reminders' ) );\n\n\t}\n\n\t/**\n\t * Callback function called when the upcoming payment reminder notification is fired\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param int    $order_id WP Post ID of the order.\n\t * @param string $type     The notification type identifier.\n\t * @return boolean\n\t */\n\tpublic function action_callback( $order_id = null, $type = null ) {\n\n\t\t// Make sure order_id and type have been provided.\n\t\tif ( ! $order_id || ! $type ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// These checks are basically the same we do in LLMS_Controller_Orders::recurring_charge().\n\n\t\t// Recurring payments disabled as a site feature when in staging mode.\n\t\tif ( ! LLMS_Site::get_feature( 'recurring_payments' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$order = llms_get_post( $order_id );\n\n\t\t// Make sure the order still exists.\n\t\tif ( ! $order || ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$user_id = $order->get( 'user_id' );\n\n\t\t// Check the user still exists.\n\t\tif ( ! get_user_by( 'id', $user_id ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Ensure Gateway is still available and supports recurring payments.\n\t\t$gateway = $order->get_gateway();\n\t\tif ( is_wp_error( $gateway ) || ! $gateway->supports( 'recurring_payments' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$this->user_id = $user_id;\n\t\t$this->post_id = $order->get( 'id' );\n\n\t\t$this->send( false, array( $type ) );\n\n\t\treturn true;\n\n\t}\n\n\t/**\n\t * Takes a subscriber type (student, author, etc) and retrieves a User ID.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $subscriber Subscriber type string.\n\t * @return int|false\n\t */\n\tprotected function get_subscriber( $subscriber ) {\n\n\t\tswitch ( $subscriber ) {\n\n\t\t\tcase 'author':\n\t\t\t\t$order = llms_get_post( $this->post_id );\n\t\t\t\tif ( ! is_a( $order, 'LLMS_Order' ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$product = $order->get_product();\n\t\t\t\tif ( is_a( $product, 'WP_Post' ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$uid = $product->get( 'author' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'student':\n\t\t\t\t$uid = $this->user_id;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t$uid = false;\n\n\t\t}\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * Get the translatable title for the notification\n\t *\n\t * Used on settings screens.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tpublic function get_title() {\n\t\treturn __( 'Upcoming Payment Reminder', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup the subscriber options for the notification\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $type The notification type identifier.\n\t * @return array\n\t */\n\tprotected function set_subscriber_options( $type ) {\n\n\t\t$options = array();\n\n\t\tswitch ( $type ) {\n\n\t\t\tcase 'basic':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\tbreak;\n\n\t\t\tcase 'email':\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'author', 'no' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'student', 'yes' );\n\t\t\t\t$options[] = $this->get_subscriber_option_array( 'custom', 'no' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $options;\n\n\t}\n\n\t/**\n\t * Cancels scheduled upcoming payment reminder notifications\n\t *\n\t * Does nothing if no payments are scheduled.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param LLMS_Order $order Instance of the LLMS_Order which we'll schedule the payment reminder for.\n\t * @return void\n\t */\n\tpublic function unschedule_upcoming_payment_reminders( $order ) {\n\n\t\t$types = array_keys( $this->get_supported_types() );\n\n\t\tforeach ( $types as $type ) {\n\t\t\t$this->unschedule_upcoming_payment_reminder( $order, $type );\n\t\t}\n\n\t}\n\n\t/**\n\t * Cancels a scheduled upcoming payment reminder notification type\n\t *\n\t * Does nothing if no payments are scheduled.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param LLMS_Order $order Instance of the LLMS_Order which we'll schedule the payment reminder for.\n\t * @param string     $type  The notification type identifier.\n\t * @return void\n\t */\n\tpublic function unschedule_upcoming_payment_reminder( $order, $type ) {\n\n\t\t$action_args = $this->get_recurring_payment_reminder_action_args( $order, $type );\n\n\t\tif ( as_next_scheduled_action( 'llms_send_upcoming_payment_reminder_notification', $action_args ) ) {\n\t\t\tas_unschedule_action( 'llms_send_upcoming_payment_reminder_notification', $action_args );\n\t\t}\n\n\t}\n\n\t/**\n\t * Schedule upcoming payment reminder notification\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param LLMS_Order $order        Instance of the LLMS_Order which we'll schedule the payment reminder for.\n\t * @param int        $payment_date Optional. The upcoming payment due date in Unix time format and UTC. Default is 0.\n\t *                                 When not provided it'll be calculated from the order.\n\t * @return array\n\t */\n\tpublic function schedule_upcoming_payment_reminders( $order, $payment_date = 0 ) {\n\n\t\t$types  = array_keys( $this->get_supported_types() );\n\t\t$return = array();\n\t\tforeach ( $types as $type ) {\n\t\t\t$return[ $type ] = $this->schedule_upcoming_payment_reminder( $order, $type, $payment_date );\n\t\t}\n\n\t\treturn $return;\n\n\t}\n\n\t/**\n\t * Schedule upcoming payment reminder notification\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param LLMS_Order $order        Instance of the LLMS_Order which we'll schedule the payment reminder for.\n\t * @param string     $type         The notification type identifier.\n\t * @param int        $payment_date Optional. The upcoming payment due date in Unix time format and UTC. Default is 0.\n\t *                                 When not provided it'll be calculated from the order.\n\t * @return WP_Error|int WP_Error either if there's no reminder date or if it's passed. Otherwise returns the return value of `as_schedule_single_action`: the action's ID.\n\t */\n\tpublic function schedule_upcoming_payment_reminder( $order, $type, $payment_date = 0 ) {\n\n\t\t$action_args = $this->get_recurring_payment_reminder_action_args( $order, $type );\n\n\t\t// Unschedule upcoming payment reminder (does nothing if no action scheduled).\n\t\t$this->unschedule_upcoming_payment_reminder( $order, $type );\n\n\t\t// Convert our reminder date to Unix Time and UTC before passing to the scheduler.\n\t\t$reminder_date = $this->get_upcoming_payment_reminder_date( $order, $type, $payment_date );\n\n\t\t// If no reminder date.\n\t\tif ( is_wp_error( $reminder_date ) ) {\n\t\t\treturn $reminder_date;\n\t\t}\n\n\t\t// Or reminder date set in the past.\n\t\tif ( $reminder_date < llms_current_time( 'U', true ) ) {\n\t\t\treturn new WP_Error( 'upcoming-payment-reminder-passed', __( 'Upcoming payment reminder passed', 'lifterlms' ) );\n\t\t}\n\n\t\t// Schedule upcoming payment reminder.\n\t\treturn as_schedule_single_action(\n\t\t\t$reminder_date,\n\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t$action_args\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the date to remind user before actual payment\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param LLMS_Order $order        Instance of the LLMS_Order which we'll schedule the payment reminder for.\n\t * @param string     $type         The notification type identifier.\n\t * @param integer    $payment_date Optional. The upcoming payment due date in Unix time format and UTC. Default is 0.\n\t *                                 When not provided it'll be calculated from the order.\n\t * @return WP_Error|integer Returns a WP_Error if there's no payment scheduled, otherwise the reminder date in Unix format and UTC.\n\t */\n\tprivate function get_upcoming_payment_reminder_date( $order, $type, $payment_date = 0 ) {\n\n\t\t$next_payment_date = $payment_date ? $payment_date : $order->get_recurring_payment_due_date_for_scheduler();\n\t\tif ( is_wp_error( $next_payment_date ) ) {\n\t\t\treturn $next_payment_date;\n\t\t}\n\n\t\t/**\n\t\t * Filters the number of days before the upcoming payment due date when to notify the customer\n\t\t *\n\t\t * The dynamic portion of this filter, `$this->id`, refers to the notification trigger identifier.\n\t\t *\n\t\t * @since 5.2.0\n\t\t *\n\t\t * @param integer    $days  The number of days before the upcoming payment due date when to notify the customer.\n\t\t * @param LLMS_Order $order Order object.\n\t\t * @param string     $type  The notification type identifier.\n\t\t */\n\t\t$days = apply_filters( \"llms_notification_{$this->id}_reminder_days\", $this->get_reminder_days( $type ), $order, $type );\n\n\t\t// Sanitize: makes sure it's always a negative number.\n\t\t$days = -1 * max( 1, absint( $days ) );\n\n\t\t/**\n\t\t * Filters the next upcoming payment reminder date\n\t\t *\n\t\t * The dynamic portion of this filter, `$this->id`, refers to the notification trigger identifier.\n\t\t *\n\t\t * @since 5.2.0\n\t\t *\n\t\t * @param integer    $upcoming_payment_reminder_time Unix timestamp for the next payment due date.\n\t\t * @param LLMS_Order $order                          Order object.\n\t\t * @param string     $type                           The notification type identifier.\n\t\t */\n\t\t$upcoming_payment_reminder_time = apply_filters( \"llms_notification_{$this->id}_reminder_date\", strtotime( \"{$days} day\", $next_payment_date ), $order, $type );\n\n\t\treturn $upcoming_payment_reminder_time;\n\n\t}\n\n\n\t/**\n\t * Retrieve arguments passed to order-related events processed by the action scheduler\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param LLMS_Order $order Instance of the LLMS_Order which we'll schedule the payment reminder for.\n\t */\n\tprivate function get_recurring_payment_reminder_action_args( $order, $type ) {\n\t\treturn array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t'type'     => $type,\n\t\t);\n\t}\n\n\t/**\n\t * Set array of additional options to be added to the notification view in the admin panel\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $type Type of the notification.\n\t * @return array\n\t */\n\tprotected function set_additional_options( $type ) {\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'id'                => $this->get_option_name( $type . '_reminder_days' ),\n\t\t\t\t'title'             => __( 'Reminder days', 'lifterlms' ),\n\t\t\t\t'desc'              => '<br>' . __( 'The number of days before the upcoming payment due date when to notify the customer.', 'lifterlms' ),\n\t\t\t\t'type'              => 'number',\n\t\t\t\t'value'             => $this->get_reminder_days( $type ),\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'min' => 1,\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Get an array of LifterLMS Admin Page settings to send test notifications\n\t *\n\t * Retrieves 25 recurring orders with an existing next payment date.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @return array\n\t */\n\tpublic function get_test_settings( $type ) {\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'llms_order',\n\t\t\t\t'posts_per_page' => 25,\n\t\t\t\t'post_status'    => array( 'llms-active', 'llms-failed', 'llms-on-hold', 'llms-pending', 'llms-pending-cancel' ),\n\t\t\t\t'meta_query'     => array(\n\t\t\t\t\t'relation' => 'and',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => '_llms_order_type',\n\t\t\t\t\t\t'value'   => 'recurring',\n\t\t\t\t\t\t'compare' => '=',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'key'     => '_llms_date_next_payment',\n\t\t\t\t\t\t'compare' => 'EXISTS',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t'no_found_rows'  => true,\n\t\t\t\t'order_by'       => 'ID',\n\t\t\t)\n\t\t);\n\n\t\t$options = array(\n\t\t\t'' => '',\n\t\t);\n\t\tforeach ( $query->posts as $post ) {\n\t\t\t$order   = llms_get_post( $post );\n\t\t\t$student = llms_get_student( $order->get( 'user_id' ) );\n\t\t\tif ( $order && $student ) {\n\t\t\t\t$options[ $order->get( 'id' ) ] = esc_attr(\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t// Translators: %1$d = The Order ID; %2$s The customer's full name; %3$s The product title.\n\t\t\t\t\t\t__( 'Order #%1$d from %2$s for \"%3$s\"', 'lifterlms' ),\n\t\t\t\t\t\t$order->get( 'id' ),\n\t\t\t\t\t\t$student->get_name(),\n\t\t\t\t\t\t$order->get( 'product_title' )\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'class'             => 'llms-select2',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'data-allow-clear' => true,\n\t\t\t\t\t'data-placeholder' => __( 'Select a recurring order', 'lifterlms' ),\n\t\t\t\t),\n\t\t\t\t'default'           => '',\n\t\t\t\t'id'                => 'order_id',\n\t\t\t\t'desc'              => '<br/>' . __( 'Send yourself a test notification using information from the selected recurring order.', 'lifterlms' ),\n\t\t\t\t'options'           => $options,\n\t\t\t\t'title'             => __( 'Send a Test', 'lifterlms' ),\n\t\t\t\t'type'              => 'select',\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Send a test notification to the currently logged in users\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $type Notification type [basic|email].\n\t * @param array  $data Array of test notification data as specified by $this->get_test_data().\n\t *\n\t * @return int|false\n\t */\n\tpublic function send_test( $type, $data = array() ) {\n\n\t\tif ( empty( $data['order_id'] ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$order         = llms_get_post( $data['order_id'] );\n\t\t$this->user_id = $order->get( 'user_id' );\n\t\t$this->post_id = $order->get( 'id' );\n\n\t\treturn parent::send_test( $type );\n\n\t}\n\n\t/**\n\t * Undocumented function\n\t *\n\t * @since 5.2.0\n\t *\n\t * @param string $type    The notification type identifier.\n\t * @param int    $default Opional. The default value. Default is `1`.\n\t * @return int\n\t */\n\tprivate function get_reminder_days( $type, $default = 1 ) {\n\t\treturn $this->get_option( $type . '_reminder_days', $default );\n\t}\n}\n\nreturn LLMS_Notification_Controller_Upcoming_Payment_Reminder::instance();\n"
  },
  {
    "path": "includes/notifications/controllers/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/notifications/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/notifications/processors/class.llms.notification.processor.email.php",
    "content": "<?php\n/**\n * Notification Background Processor: Emails\n *\n * @package LifterLMS/Notifications/Processors/Classes\n *\n * @since 3.8.0\n * @version 7.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification Background Processor: Emails\n *\n * @since 3.8.0\n * @since 3.10.1 Unknown.\n * @since 3.33.2 Improve data logged during errors.\n */\nclass LLMS_Notification_Processor_Email extends LLMS_Abstract_Notification_Processor {\n\n\t/**\n\t * action name\n\t *\n\t * @var  string\n\t */\n\tprotected $action = 'llms_notification_processor_email';\n\n\t/**\n\t * Processes an item in the queue.\n\t *\n\t * @since 3.8.0\n\t * @since 3.10.1 Unknown.\n\t * @since 3.33.2 Log additional data during errors.\n\t * @since 7.1.0 Catch possible fatals and in that case remove from the queue the item that produced them.\n\t *\n\t * @param int $notification_id ID of an LLMS_Notification.\n\t * @return bool `false` removes item from queue, `true` retain for further processing.\n\t */\n\tprotected function task( $notification_id ) {\n\n\t\t$this->log( sprintf( 'sending email notification ID #%d', $notification_id ) );\n\t\ttry {\n\n\t\t\t$notification = new LLMS_Notification( $notification_id );\n\n\t\t\t$view = $notification->get_view();\n\n\t\t\tif ( ! $view ) {\n\t\t\t\t$this->log( 'ID#' . $notification_id );\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Setup the email.\n\t\t\t$mailer = llms()->mailer()->get_email( 'notification' );\n\n\t\t\tif ( ! $mailer->add_recipient( $notification->get( 'subscriber' ), 'to' ) ) {\n\t\t\t\t$this->log( sprintf( 'Error sending email notification ID #%d - subscriber does not exist', $notification_id ) );\n\t\t\t\t$this->log( $notification->toArray() );\n\t\t\t\t$notification->set( 'status', 'error' );\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t$mailer->set_subject( $view->get_subject() )->set_heading( $view->get_title() )->set_body( $view->get_html() );\n\n\t\t} catch ( Error $e ) {\n\t\t\t$this->log( sprintf( 'Error sending email notification ID #%d', $notification_id ) );\n\t\t\t$this->log( sprintf( 'Error caught %1$s in %2$s on line %3$s', $e->getMessage(), $e->getFile(), $e->getLine() ) );\n\t\t\t$notification->set( 'status', 'error' );\n\t\t\treturn false;\n\t\t}\n\n\t\t// Log when wp_mail fails.\n\t\tif ( $mailer->send() ) {\n\t\t\t$notification->set( 'status', 'sent' );\n\t\t} else {\n\t\t\t$this->log( sprintf( 'Error sending email notification ID #%d', $notification_id ) );\n\t\t\t$this->log( $notification->toArray() );\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n}\n\nreturn new LLMS_Notification_Processor_Email();\n"
  },
  {
    "path": "includes/notifications/processors/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.achievement.earned.php",
    "content": "<?php\n/**\n * Notification View: Achievement Earned\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Achievement Earned\n *\n * @since 3.8.0\n * @since 3.17.6 Unknown.\n * @since 3.30.3 Fixed spelling errors.\n */\nclass LLMS_Notification_View_Achievement_Earned extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'achievement_earned';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\t\tob_start();\n\t\t?>\n\t\t<p style=\"text-align: center;\">{{ACHIEVEMENT_IMAGE}}</p>\n\t\t<h2 style=\"text-align: center;\"><strong>{{ACHIEVEMENT_TITLE}}</strong></h2>\n\t\t<p style=\"text-align: center;\">{{ACHIEVEMENT_CONTENT}}</p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @since 3.8.0\n\t * @since 3.30.3 Fixed spelling errors.\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{ACHIEVEMENT_CONTENT}}'   => __( 'Achievement Content', 'lifterlms' ),\n\t\t\t'{{ACHIEVEMENT_IMAGE}}'     => __( 'Achievement Image', 'lifterlms' ),\n\t\t\t'{{ACHIEVEMENT_IMAGE_URL}}' => __( 'Achievement Image URL', 'lifterlms' ),\n\t\t\t'{{ACHIEVEMENT_TITLE}}'     => __( 'Achievement Title', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}'          => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @since 3.8.0\n\t * @since 3.8.2 Unknown.\n\t * @since 4.7.0 Use `achievement_title` in favor of `title` for the {{ACHIEVEMENT_TITLE}} merge code.\n\t * @since 6.0.0 Use `title` in favor of deprecated `achievement_title` meta key for the {{ACHIEVEMENT_TITLE}} merge code.\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$achievement = new LLMS_User_Achievement( $this->post );\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{ACHIEVEMENT_CONTENT}}':\n\t\t\t\t$code = $achievement->get( 'content' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ACHIEVEMENT_IMAGE}}':\n\t\t\t\t$title = $this->set_merge_data( '{{ACHIEVEMENT_TITLE}}' );\n\t\t\t\t$url   = $this->set_merge_data( '{{ACHIEVEMENT_IMAGE_URL}}' );\n\t\t\t\t$code  = '<img alt=\"' . sprintf( _x( '%s Icon', 'Achievement icon alt text', 'lifterlms' ), $title ) . '\" src=\"' . $url . '\">';\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ACHIEVEMENT_IMAGE_URL}}':\n\t\t\t\t$code = $achievement->get_image( 'medium', 'achievement_image' );\n\t\t\t\tif ( ! $code ) {\n\t\t\t\t\t$code = apply_filters( 'lifterlms_placeholder_img_src', llms()->plugin_url() . '/assets/images/optional_achievement.png' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ACHIEVEMENT_TITLE}}':\n\t\t\t\t$code = $achievement->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\treturn __( 'You\\'ve been awarded an achievement!', 'lifterlms' );\n\t}\n\n\t/**\n\t * Define field support for the view\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_supported_fields() {\n\t\treturn array(\n\t\t\t'basic' => array(\n\t\t\t\t'body'  => true,\n\t\t\t\t'title' => true,\n\t\t\t\t'icon'  => false,\n\t\t\t),\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.certificate.earned.php",
    "content": "<?php\n/**\n * Notification View: Certificate Earned\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Certificate Earned\n *\n * @since 3.8.0\n */\nclass LLMS_Notification_View_Certificate_Earned extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'certificate_earned';\n\n\t/**\n\t * Get the HTML for the mini certificate preview.\n\t *\n\t * @since Unknown\n\t * @since 6.0.0 Removed `$content` parameter & updated HTML to display a placeholder.\n\t *\n\t * @param string $title The (merged) certificate title.\n\t * @return string\n\t */\n\tprivate function get_mini_html( $title ) {\n\t\tob_start();\n\t\t?>\n\t\t<div class=\"llms-mini-cert\">\n\n\t\t\t<h2 class=\"llms-mini-cert-title\"><?php echo wp_kses_post( $title ); ?></h2>\n\n\t\t\t<div class=\"llms-mini-cert--body \">\n\t\t\t\t<div class=\"llms-mini-cert--mock-line\"></div>\n\t\t\t\t<div class=\"llms-mini-cert--mock-line\"></div>\n\t\t\t\t<div class=\"llms-mini-cert--mock-line\"></div>\n\n\t\t\t\t<div class=\"llms-mini-cert--mock-line\"></div>\n\t\t\t\t<div class=\"llms-mini-cert--mock-line\"></div>\n\n\t\t\t\t<div class=\"llms-mini-cert--mock-dot\"></div>\n\t\t\t\t<div class=\"llms-mini-cert--mock-line\"></div>\n\t\t\t</div>\n\n\t\t</div>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup body content for output.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\t\treturn '{{MINI_CERTIFICATE}}';\n\t}\n\n\t/**\n\t * Setup footer content for output.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\t$url = $this->set_merge_data( '{{CERTIFICATE_URL}}' );\n\t\treturn '<a href=\"' . esc_url( $url ) . '\">' . __( 'View Full Certificate', 'lifterlms' ) . '</a>';\n\t}\n\n\t/**\n\t * Setup notification icon for output.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{CERTIFICATE_CONTENT}}' => __( 'Certificate Content', 'lifterlms' ),\n\t\t\t'{{CERTIFICATE_TITLE}}'   => __( 'Certificate Title', 'lifterlms' ),\n\t\t\t'{{CERTIFICATE_URL}}'     => __( 'Certificate URL', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}'        => __( 'Student Name', 'lifterlms' ),\n\t\t\t'{{MINI_CERTIFICATE}}'    => __( 'Mini Certificate', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values.\n\t *\n\t * @since 3.8.0\n\t * @since 3.16.6 Unknown.\n\t * @since 6.0.0 Refactor to give each merge code it's own method.\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string The merged string or the original code for invalid merge codes.\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\tif ( in_array( $code, array_keys( $this->set_merge_codes() ), true ) ) {\n\t\t\t$method = 'set_merge_data_' . strtolower( str_replace( array( '{{', '}}' ), '', $code ) );\n\t\t\t$code   = method_exists( $this, $method ) ? $this->$method( new LLMS_User_Certificate( $this->notification->post_id ) ) : $code;\n\t\t}\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Get merge data for the {{CERTIFICATE_CONTENT}} merge code.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_User_Certificate $cert Earned certificate object.\n\t * @return string\n\t */\n\tprivate function set_merge_data_certificate_content( $cert ) {\n\t\treturn $cert->get( 'content' );\n\t}\n\n\t/**\n\t * Get merge data for the {{CERTIFICATE_TITLE}} merge code.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_User_Certificate $cert Earned certificate object.\n\t * @return string\n\t */\n\tprivate function set_merge_data_certificate_title( $cert ) {\n\t\treturn $cert->get( 'title' );\n\t}\n\n\t/**\n\t * Get merge data for the {{CERTIFICATE_URL}} merge code.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_User_Certificate $cert Earned certificate object.\n\t * @return string\n\t */\n\tprivate function set_merge_data_certificate_url( $cert ) {\n\t\treturn get_permalink( $cert->get( 'id' ) );\n\t}\n\n\t/**\n\t * Get merge data for the {{MINI_CERTIFICATE}} merge code.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_User_Certificate $cert Earned certificate object.\n\t * @return string\n\t */\n\tprivate function set_merge_data_mini_certificate( $cert ) {\n\t\treturn $this->get_mini_html( $this->set_merge_data( '{{CERTIFICATE_TITLE}}' ) );\n\t}\n\n\t/**\n\t * Get merge data for the {{STUDENT_NAME}} merge code.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param LLMS_User_Certificate $cert Earned certificate object.\n\t * @return string\n\t */\n\tprivate function set_merge_data_student_name( $cert ) {\n\t\treturn $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t}\n\n\t/**\n\t * Setup notification subject for output.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification title for output.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\treturn __( 'You\\'ve earned a certificate!', 'lifterlms' );\n\t}\n\n\t/**\n\t * Defines field support for the view.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_supported_fields() {\n\t\treturn array(\n\t\t\t'basic' => array(\n\t\t\t\t'body'  => true,\n\t\t\t\t'title' => true,\n\t\t\t\t'icon'  => true,\n\t\t\t),\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.course.complete.php",
    "content": "<?php\n/**\n * Notification View: Course Complete\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.8.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Course Complete\n *\n * @since 3.8.0\n * @since 3.8.2 Unknown.\n */\nclass LLMS_Notification_View_Course_Complete extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var  array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var  [type]\n\t */\n\tpublic $trigger_id = 'course_complete';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_body() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{COURSE_TITLE}}' );\n\t\t}\n\t\treturn sprintf( __( 'Congratulations! You finished %s', 'lifterlms' ), '{{COURSE_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{COURSE_TITLE}}' => __( 'Course Title', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}' => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @param    string $code  the merge code to ge merged data for\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.2\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{COURSE_TITLE}}':\n\t\t\t\t$code = $this->post->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{COURSE_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( '%s Completed a Course', 'lifterlms' ), '{{STUDENT_NAME}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.course.track.complete.php",
    "content": "<?php\n/**\n * Notification View: Course Track Complete\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.8.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Course Track Complete\n *\n * @since 3.8.0\n * @since 3.8.2 Unknown.\n */\nclass LLMS_Notification_View_Course_Track_Complete extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var  array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var  [type]\n\t */\n\tpublic $trigger_id = 'course_track_complete';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_body() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{TRACK_TITLE}}' );\n\t\t}\n\t\treturn sprintf( __( 'Congratulations! You finished %s', 'lifterlms' ), '{{TRACK_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{TRACK_TITLE}}'  => __( 'Track Title', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}' => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @param    string $code  the merge code to ge merged data for\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.2\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{TRACK_TITLE}}':\n\t\t\t\t$track = new LLMS_Track( $this->notification->get( 'post_id' ) );\n\t\t\t\t$code  = $track->get_title();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{TRACK_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( '%s Completed a Track', 'lifterlms' ), '{{STUDENT_NAME}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.enrollment.php",
    "content": "<?php\n/**\n * Notification View: Course/Membership Enrollment\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.8.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Course/Membership Enrollment\n *\n * @since 3.8.0\n * @since 3.8.2 Unknown.\n */\nclass LLMS_Notification_View_Enrollment extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var  array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var  [type]\n\t */\n\tpublic $trigger_id = 'enrollment';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_body() {\n\t\treturn sprintf( __( 'Congratulations! %1$s enrolled in %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{TITLE}}' );\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{TITLE}}'        => __( 'Title', 'lifterlms' ),\n\t\t\t'{{TYPE}}'         => __( 'Type (Course or Membership)', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}' => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @param    string $code  the merge code to ge merged data for\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.2\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{TITLE}}':\n\t\t\t\t$code = $this->post->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{TYPE}}':\n\t\t\t\t$code = $this->post->get_post_type_label();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( '%1$s enrolled in %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( '%1$s enrollment success!', 'lifterlms' ), '{{TYPE}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.lesson.complete.php",
    "content": "<?php\n/**\n * Notification View: Lesson Complete\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.10.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Lesson Complete\n *\n * @since 3.8.0\n * @since 3.10.1 Unknown.\n */\nclass LLMS_Notification_View_Lesson_Complete extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var  array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var  [type]\n\t */\n\tpublic $trigger_id = 'lesson_complete';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_body() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{LESSON_TITLE}}' );\n\t\t}\n\t\t$content  = sprintf( __( 'Congratulations! You finished %s', 'lifterlms' ), '{{LESSON_TITLE}}' );\n\t\t$content .= \"\\r\\n\\r\\n{{COURSE_PROGRESS}}\";\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{COURSE_PROGRESS}}' => __( 'Course Progress Bar', 'lifterlms' ),\n\t\t\t'{{COURSE_TITLE}}'    => __( 'Course Title', 'lifterlms' ),\n\t\t\t'{{LESSON_TITLE}}'    => __( 'Lesson Title', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}'    => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @param    string $code  the merge code to ge merged data for\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.10.1\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{COURSE_PROGRESS}}':\n\t\t\t\t$progress = $this->user->get_progress( $this->post->get( 'parent_course' ), 'course' );\n\t\t\t\t$code     = lifterlms_course_progress_bar( $progress, false, false, false );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{COURSE_TITLE}}':\n\t\t\t\t$course = $this->post->get_course();\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$code = $course->get( 'title' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = '';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{LESSON_TITLE}}':\n\t\t\t\t$code = $this->post->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{LESSON_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( '%s Completed a Lesson', 'lifterlms' ), '{{STUDENT_NAME}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.manual.payment.due.php",
    "content": "<?php\n/**\n * Notification View: Payment Due.\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.10.0\n * @version 5.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Payment Due.\n *\n * @since 3.10.0\n */\nclass LLMS_Notification_View_Manual_Payment_Due extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications.\n\t *\n\t * @var array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\n\t/**\n\t * Notification Trigger ID.\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'manual_payment_due';\n\n\t/**\n\t * Setup body content for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn $this->set_body_email();\n\t\t}\n\t\treturn $this->set_body_basic();\n\t}\n\n\t/**\n\t * Setup default notification body for basic notifications.\n\t *\n\t * @since 3.10.0\n\t */\n\tprivate function set_body_basic() {\n\t\treturn __( 'Head over to your dashboard for payment instructions.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup default notification body for email notifications.\n\t *\n\t * @since 3.10.0\n\t * @since 5.2.0 Build the table with mailer helper.\n\t */\n\tprivate function set_body_email() {\n\t\t$mailer = llms()->mailer();\n\n\t\t$rows = array(\n\t\t\t'NEXT_PAYMENT_DATE'  => __( 'Payment Due Date', 'lifterlms' ),\n\t\t\t'PRODUCT_TITLE_LINK' => '{{PRODUCT_TYPE}}',\n\t\t\t'PLAN_TITLE'         => __( 'Plan', 'lifterlms' ),\n\t\t\t'PAYMENT_AMOUNT'     => __( 'Amount', 'lifterlms' ),\n\t\t);\n\n\t\tob_start();\n\t\t?><p><?php printf( esc_html__( 'Hello %s,', 'lifterlms' ), '{{CUSTOMER_NAME}}' ); ?></p>\n\t\t<p><?php printf( esc_html__( 'A payment for your subscription to %1$s is due.', 'lifterlms' ), '{{PRODUCT_TITLE}}' ); ?></p>\n\t\t<p><?php printf( esc_html__( 'Sign in to your account and %1$spay now%2$s.', 'lifterlms' ), '<a href=\"{{ORDER_URL}}\">', '</a>' ); ?></p>\n\t\t<h4><?php printf( esc_html__( 'Order #%s', 'lifterlms' ), '{{ORDER_ID}}' ); ?></h4>\n\t\t<?php\n\t\t$mailer->output_table_html( $rows );\n\t\t?>\n\t\t<p><a href=\"{{ORDER_URL}}\"><?php esc_html_e( 'Pay Invoice', 'lifterlms' ); ?></a></p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup footer content for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\t$url = $this->set_merge_data( '{{ORDER_URL}}' );\n\t\treturn '<a href=\"' . esc_url( $url ) . '\">' . esc_html__( 'Pay Now', 'lifterlms' ) . '</a>';\n\t}\n\n\t/**\n\t * Setup notification icon for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'warning' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{CUSTOMER_ADDRESS}}'   => __( 'Customer Address', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_NAME}}'      => __( 'Customer Name', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_PHONE}}'     => __( 'Customer Phone', 'lifterlms' ),\n\t\t\t'{{NEXT_PAYMENT_DATE}}'  => __( 'Next Payment Date', 'lifterlms' ),\n\t\t\t'{{ORDER_ID}}'           => __( 'Order ID', 'lifterlms' ),\n\t\t\t'{{ORDER_URL}}'          => __( 'Order URL', 'lifterlms' ),\n\t\t\t'{{PAYMENT_AMOUNT}}'     => __( 'Payment Amount', 'lifterlms' ),\n\t\t\t'{{PLAN_TITLE}}'         => __( 'Plan Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE}}'      => __( 'Product Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TYPE}}'       => __( 'Product Type', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE_LINK}}' => __( 'Product Title (Link)', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values.\n\t *\n\t * @since 3.10.0\n\t * @since 5.2.0 Retrieve the customer's full address using the proper order's method.\n\t * @since 5.4.0 Account for deleted products.\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$order = $this->post;\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{CUSTOMER_ADDRESS}}':\n\t\t\t\t$code = $order->get_customer_full_address();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_NAME}}':\n\t\t\t\t$code = $order->get_customer_name();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_PHONE}}':\n\t\t\t\t$code = $order->get( 'billing_phone' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{NEXT_PAYMENT_DATE}}':\n\t\t\t\t$code = $order->get_date( 'date_next_payment', get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_ID}}':\n\t\t\t\t$code = $order->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_URL}}':\n\t\t\t\t$code = esc_url( $order->get_view_link() );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PAYMENT_AMOUNT}}':\n\t\t\t\t$code = $order->get_price( 'total' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PLAN_TITLE}}':\n\t\t\t\t$code = $order->get( 'plan_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE}}':\n\t\t\t\t$code = $order->get( 'product_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE_LINK}}':\n\t\t\t\t$permalink = esc_url( get_permalink( $order->get( 'product_id' ) ) );\n\t\t\t\tif ( $permalink ) {\n\t\t\t\t\t$title = $this->set_merge_data( '{{PRODUCT_TITLE}}' );\n\t\t\t\t\t$code  = '<a href=\"' . $permalink . '\">' . $title . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TYPE}}':\n\t\t\t\t$obj = $order->get_product();\n\t\t\t\tif ( empty( $obj ) ) {\n\t\t\t\t\t$code = __( '[DELETED ITEM]', 'lifterlms' );\n\t\t\t\t} elseif ( is_a( $obj, 'WP_Post' ) ) {\n\t\t\t\t\t$code = _x( 'Item', 'generic product type description', 'lifterlms' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = $obj->get_post_type_label( 'singular_name' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Setup notification subject for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'A payment is due for your subscription to %s', 'lifterlms' ), '{{PRODUCT_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\t// Translators: %s = The order ID.\n\t\t\treturn sprintf( __( 'Payment Due for Order #%s', 'lifterlms' ), '{{ORDER_ID}}' );\n\t\t}\n\t\t// Translators: %s = The product title.\n\t\treturn sprintf( __( 'A payment is due for your subscription to %s', 'lifterlms' ), '{{PRODUCT_TITLE}}' );\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.payment.retry.php",
    "content": "<?php\n/**\n * Notification View: Payment Retry.\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.10.0\n * @version 5.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Payment Retry.\n *\n * @since 3.10.0\n */\nclass LLMS_Notification_View_Payment_Retry extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications.\n\t *\n\t * @var array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification before automatically dismissing it.\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\n\t/**\n\t * Notification Trigger ID.\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'payment_retry';\n\n\t/**\n\t * Setup body content for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn $this->set_body_email();\n\t\t}\n\t\treturn $this->set_body_basic();\n\t}\n\n\t/**\n\t * Setup default notification body for basic notifications.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprivate function set_body_basic() {\n\t\treturn esc_html__( 'Head over to the order to see what went wrong and update your payment method to reactivate your subscription.', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup default notification body for email notifications.\n\t *\n\t * @since 3.10.0\n\t * @since 5.2.0 Build the table with mailer helper.\n\t *\n\t * @return void\n\t */\n\tprivate function set_body_email() {\n\t\t$mailer = llms()->mailer();\n\n\t\t$rows = array(\n\t\t\t'NEXT_PAYMENT_DATE'  => __( 'Payment Due Date', 'lifterlms' ),\n\t\t\t'PRODUCT_TITLE_LINK' => '{{PRODUCT_TYPE}}',\n\t\t\t'PLAN_TITLE'         => __( 'Plan', 'lifterlms' ),\n\t\t\t'PAYMENT_AMOUNT'     => __( 'Amount', 'lifterlms' ),\n\t\t);\n\n\t\tob_start();\n\t\t?><p><?php printf( esc_html__( 'Hello %s,', 'lifterlms' ), '{{CUSTOMER_NAME}}' ); ?></p>\n\t\t<p><?php printf( esc_html__( 'The automatic payment for your subscription to %1$s has failed. We\\'ll automatically retry this charge on %2$s.', 'lifterlms' ), '{{PRODUCT_TITLE}}', '{{NEXT_PAYMENT_DATE}}' ); ?></p>\n\t\t<p><?php printf( esc_html__( 'To reactivate your subscription you can login to your account and %1$spay now%2$s.', 'lifterlms' ), '<a href=\"{{ORDER_URL}}\">', '</a>' ); ?></p>\n\t\t<h4><?php printf( esc_html__( 'Order #%s', 'lifterlms' ), '{{ORDER_ID}}' ); ?></h4>\n\t\t<?php $mailer->output_table_html( $rows ); ?>\n\t\t<p><a href=\"{{ORDER_URL}}\"><?php esc_html_e( 'Update Payment Method', 'lifterlms' ); ?></a></p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup footer content for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\t$url = $this->set_merge_data( '{{ORDER_URL}}' );\n\t\treturn '<a href=\"' . esc_url( $url ) . '\">' . esc_html__( 'Update Payment Method', 'lifterlms' ) . '</a>';\n\t}\n\n\t/**\n\t * Setup notification icon for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'warning' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{CUSTOMER_ADDRESS}}'   => __( 'Customer Address', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_NAME}}'      => __( 'Customer Name', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_PHONE}}'     => __( 'Customer Phone', 'lifterlms' ),\n\t\t\t'{{NEXT_PAYMENT_DATE}}'  => __( 'Next Payment Date', 'lifterlms' ),\n\t\t\t'{{ORDER_ID}}'           => __( 'Order ID', 'lifterlms' ),\n\t\t\t'{{ORDER_URL}}'          => __( 'Order URL', 'lifterlms' ),\n\t\t\t'{{PAYMENT_AMOUNT}}'     => __( 'Payment Amount', 'lifterlms' ),\n\t\t\t'{{PLAN_TITLE}}'         => __( 'Plan Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE}}'      => __( 'Product Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TYPE}}'       => __( 'Product Type', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE_LINK}}' => __( 'Product Title (Link)', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values.\n\t *\n\t * @since 3.10.0\n\t * @since 5.2.0 Retrieve the customer's full address using the proper order's method.\n\t * @since 5.4.0 Account for deleted products.\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$order = $this->post;\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{CUSTOMER_ADDRESS}}':\n\t\t\t\t$code = $order->get_customer_full_address();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_NAME}}':\n\t\t\t\t$code = $order->get_customer_name();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_PHONE}}':\n\t\t\t\t$code = $order->get( 'billing_phone' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{NEXT_PAYMENT_DATE}}':\n\t\t\t\t$code = $order->get_date( 'date_next_payment', get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_ID}}':\n\t\t\t\t$code = $order->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_URL}}':\n\t\t\t\t$code = esc_url( $order->get_view_link() );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PAYMENT_AMOUNT}}':\n\t\t\t\t$code = $order->get_price( 'total' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PLAN_TITLE}}':\n\t\t\t\t$code = $order->get( 'plan_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE}}':\n\t\t\t\t$code = $order->get( 'product_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE_LINK}}':\n\t\t\t\t$permalink = esc_url( get_permalink( $order->get( 'product_id' ) ) );\n\t\t\t\tif ( $permalink ) {\n\t\t\t\t\t$title = $this->set_merge_data( '{{PRODUCT_TITLE}}' );\n\t\t\t\t\t$code  = '<a href=\"' . $permalink . '\">' . $title . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TYPE}}':\n\t\t\t\t$obj = $order->get_product();\n\t\t\t\tif ( empty( $obj ) ) {\n\t\t\t\t\t$code = __( '[DELETED ITEM]', 'lifterlms' );\n\t\t\t\t} elseif ( is_a( $obj, 'WP_Post' ) ) {\n\t\t\t\t\t$code = _x( 'Item', 'generic product type description', 'lifterlms' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = $obj->get_post_type_label( 'singular_name' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Setup notification subject for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'Automatic payment for %1$s failed, retry scheduled for %2$s', 'lifterlms' ), '{{PRODUCT_TITLE}}', '{{NEXT_PAYMENT_DATE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\t// Translators: %s = The order ID.\n\t\t\treturn sprintf( __( 'Automatic payment failed for order #%s', 'lifterlms' ), '{{ORDER_ID}}' );\n\t\t}\n\t\t// Translators: %s = The product title.\n\t\treturn sprintf( __( 'An automatic payment failed for your subscription to %s', 'lifterlms' ), '{{PRODUCT_TITLE}}' );\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.purchase.receipt.php",
    "content": "<?php\n/**\n * Notification View: Payment Receipt\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Purchase Receipt\n *\n * @since 3.8.0\n * @since 3.8.2 Unknown.\n */\nclass LLMS_Notification_View_Purchase_Receipt extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'purchase_receipt';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @since 3.8.0\n\t * @since 5.2.0 Build the table with mailer helper.\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\n\t\t$mailer = llms()->mailer();\n\n\t\t$rows = array(\n\t\t\t'TRANSACTION_DATE'   => __( 'Date', 'lifterlms' ),\n\t\t\t'PRODUCT_TITLE_LINK' => '{{PRODUCT_TYPE}}',\n\t\t\t'PLAN_TITLE'         => __( 'Plan', 'lifterlms' ),\n\t\t\t'TRANSACTION_AMOUNT' => __( 'Amount', 'lifterlms' ),\n\t\t\t'TRANSACTION_SOURCE' => __( 'Payment Method', 'lifterlms' ),\n\t\t\t'TRANSACTION_ID'     => __( 'Transaction ID', 'lifterlms' ),\n\t\t);\n\n\t\tob_start();\n\t\t$mailer->output_table_html( $rows );\n\t\t?>\n\t\t<p><a href=\"{{ORDER_URL}}\"><?php esc_html_e( 'View Order Details', 'lifterlms' ); ?></a></p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{CUSTOMER_ADDRESS}}'   => __( 'Customer Address', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_NAME}}'      => __( 'Customer Name', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_PHONE}}'     => __( 'Customer Phone', 'lifterlms' ),\n\t\t\t'{{ORDER_ID}}'           => __( 'Order ID', 'lifterlms' ),\n\t\t\t'{{ORDER_URL}}'          => __( 'Order URL', 'lifterlms' ),\n\t\t\t'{{PLAN_TITLE}}'         => __( 'Plan Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE}}'      => __( 'Product Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TYPE}}'       => __( 'Product Type', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE_LINK}}' => __( 'Product Title (Link)', 'lifterlms' ),\n\t\t\t'{{TRANSACTION_AMOUNT}}' => __( 'Transaction Amount', 'lifterlms' ),\n\t\t\t'{{TRANSACTION_DATE}}'   => __( 'Transaction Date', 'lifterlms' ),\n\t\t\t'{{TRANSACTION_ID}}'     => __( 'Transaction ID', 'lifterlms' ),\n\t\t\t'{{TRANSACTION_SOURCE}}' => __( 'Transaction Source', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @since 3.8.0\n\t * @since 3.8.2 Unknown.\n\t * @since 5.2.0 Retrieve the customer's full address using the proper order's method.\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$transaction = $this->post;\n\t\t$order       = $transaction->get_order();\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{CUSTOMER_ADDRESS}}':\n\t\t\t\t$code = $order->get_customer_full_address();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_NAME}}':\n\t\t\t\t$code = $order->get_customer_name();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_PHONE}}':\n\t\t\t\t$code = $order->get( 'billing_phone' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_ID}}':\n\t\t\t\t$code = $order->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_URL}}':\n\t\t\t\t$code = esc_url( $order->get_view_link() );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PLAN_TITLE}}':\n\t\t\t\t$code = $order->get( 'plan_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE}}':\n\t\t\t\t$code = $order->get( 'product_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE_LINK}}':\n\t\t\t\t$permalink = esc_url( get_permalink( $order->get( 'product_id' ) ) );\n\t\t\t\tif ( $permalink ) {\n\t\t\t\t\t$title = $this->set_merge_data( '{{PRODUCT_TITLE}}' );\n\t\t\t\t\t$code  = '<a href=\"' . $permalink . '\">' . $title . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TYPE}}':\n\t\t\t\t$obj = $order->get_product();\n\t\t\t\tif ( $obj ) {\n\t\t\t\t\t$code = $obj->get_post_type_label( 'singular_name' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = _x( 'Item', 'generic product type description', 'lifterlms' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{TRANSACTION_AMOUNT}}':\n\t\t\t\t$code = $transaction->get_price( 'amount' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{TRANSACTION_DATE}}':\n\t\t\t\t$code = $transaction->get_date( 'date', get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{TRANSACTION_ID}}':\n\t\t\t\t$code = $transaction->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{TRANSACTION_SOURCE}}':\n\t\t\t\t$code = $transaction->get( 'gateway_source_description' );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\t// Translators: %s = Product Title.\n\t\treturn sprintf( __( 'Purchase Receipt for %s', 'lifterlms' ), '{{PRODUCT_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\t// Translators: %s = Order ID.\n\t\treturn sprintf( __( 'Purchase Receipt for Order #%s', 'lifterlms' ), '{{ORDER_ID}}' );\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.quiz.failed.php",
    "content": "<?php\n/**\n * Notification View: Quiz Failed\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Quiz Failed\n *\n * @since 3.8.0\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Notification_View_Quiz_Failed extends LLMS_Abstract_Notification_View_Quiz_Completion {\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'quiz_failed';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.24.0\n\t */\n\tprotected function set_body() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn $this->set_body_email();\n\t\t}\n\t\t$content  = sprintf( __( 'You failed %s!', 'lifterlms' ), '{{QUIZ_TITLE}}' );\n\t\t$content .= \"\\r\\n\\r\\n{{GRADE_BAR}}\";\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'negative' );\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( '%1$s failed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{QUIZ_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( '%s failed a quiz', 'lifterlms' ), '{{STUDENT_NAME}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.quiz.graded.php",
    "content": "<?php\n/**\n * Notification View: Quiz Graded\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.24.0\n * @version 5.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Notification_View_Quiz_Graded class\n *\n * @since 3.24.0\n * @since 3.29.0 Unknown.\n */\nclass LLMS_Notification_View_Quiz_Graded extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'quiz_graded';\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn $this->set_body_email();\n\t\t}\n\t\t// Translators: %s = Quiz attempt grade.\n\t\t$content = sprintf( __( 'You received a %1$s', 'lifterlms' ), '{{GRADE}}' );\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Setup body for email notification\n\t *\n\t * @since 3.24.0\n\t * @since 5.2.0 Build the table with mailer helper.\n\t *\n\t * @return string\n\t */\n\tprotected function set_body_email() {\n\n\t\t$mailer = llms()->mailer();\n\n\t\t$btn_style = $mailer->get_button_style();\n\n\t\t$rows = array(\n\t\t\t'QUIZ_TITLE'   => __( 'Quiz', 'lifterlms' ),\n\t\t\t'LESSON_TITLE' => __( 'Lesson', 'lifterlms' ),\n\t\t\t'COURSE_TITLE' => __( 'Course', 'lifterlms' ),\n\t\t\t'GRADE'        => __( 'Grade', 'lifterlms' ),\n\t\t\t'STATUS'       => __( 'Status', 'lifterlms' ),\n\t\t);\n\n\t\tob_start();\n\t\t$mailer->output_table_html( $rows );\n\t\t?>\n\t\t<p><a href=\"{{REVIEW_URL}}\" style=\"<?php echo esc_attr( $btn_style ); ?>\"><?php esc_html_e( 'View the whole attempt', 'lifterlms' ); ?></a></p>\n\t\t<p><small><?php esc_html_e( 'Trouble clicking? Copy and paste this URL into your browser:', 'lifterlms' ); ?><br><a href=\"{{REVIEW_URL}}\">{{REVIEW_URL}}</a></small></p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'warning' );\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @since 3.24.0\n\t * @since 3.29.0 Unknown.\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\n\t\t$attempt = new LLMS_Quiz_Attempt( $this->notification->get( 'post_id' ) );\n\t\tif ( ! $attempt->exists() ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$permalink = $attempt->get_permalink();\n\t\tif ( ! $permalink ) {\n\t\t\treturn '';\n\t\t}\n\n\t\treturn '<a href=\"' . esc_url( $permalink ) . '\">' . __( 'View the attempt', 'lifterlms' ) . '</a>';\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{COURSE_TITLE}}' => __( 'Course Title', 'lifterlms' ),\n\t\t\t'{{GRADE}}'        => __( 'Grade', 'lifterlms' ),\n\t\t\t'{{LESSON_TITLE}}' => __( 'Lesson Title', 'lifterlms' ),\n\t\t\t'{{QUIZ_TITLE}}'   => __( 'Quiz Title', 'lifterlms' ),\n\t\t\t'{{REVIEW_URL}}'   => __( 'Review URL', 'lifterlms' ),\n\t\t\t'{{STATUS}}'       => __( 'Quiz Status', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}' => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @since 3.24.0\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$attempt = new LLMS_Quiz_Attempt( $this->notification->get( 'post_id' ) );\n\t\tif ( ! $attempt->exists() ) {\n\t\t\treturn '';\n\t\t}\n\t\t$lesson = llms_get_post( $attempt->get( 'lesson_id' ) );\n\t\tif ( ! $lesson ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{COURSE_TITLE}}':\n\t\t\t\t$course = $lesson->get_course();\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$code = $course->get( 'title' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = '';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{GRADE}}':\n\t\t\t\t$code = llms()->grades()->round( $attempt->get( 'grade' ) ) . '%';\n\t\t\t\tbreak;\n\n\t\t\tcase '{{LESSON_TITLE}}':\n\t\t\t\t$code = $lesson->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{QUIZ_TITLE}}':\n\t\t\t\t$code = get_the_title( $attempt->get( 'quiz_id' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{REVIEW_URL}}':\n\t\t\t\t$code = $attempt->get_permalink();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STATUS}}':\n\t\t\t\t$code = $attempt->l10n( 'status' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}// End switch().\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\t// Translators: %s = Quiz Title.\n\t\treturn sprintf( __( 'Your quiz \"%s\" has been reviewed', 'lifterlms' ), '{{QUIZ_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\treturn __( 'Quiz Review Details', 'lifterlms' );\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.quiz.passed.php",
    "content": "<?php\n/**\n * Notification View: Quiz Passed\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Quiz Passed\n *\n * @since 3.8.0\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Notification_View_Quiz_Passed extends LLMS_Abstract_Notification_View_Quiz_Completion {\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var  string\n\t */\n\tpublic $trigger_id = 'quiz_passed';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.24.0\n\t */\n\tprotected function set_body() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn $this->set_body_email();\n\t\t}\n\t\t$content  = sprintf( __( 'Congratulations! You passed %s!', 'lifterlms' ), '{{QUIZ_TITLE}}' );\n\t\t$content .= \"\\r\\n\\r\\n{{GRADE_BAR}}\";\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'Congratulations! %1$s passed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{QUIZ_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( '%s passed a quiz', 'lifterlms' ), '{{STUDENT_NAME}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.section.complete.php",
    "content": "<?php\n/**\n * Notification View: Section Complete\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.10.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Section Complete\n *\n * @since 3.8.0\n * @since 3.10.1 Unknown.\n */\nclass LLMS_Notification_View_Section_Complete extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications\n\t *\n\t * @var  array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var  [type]\n\t */\n\tpublic $trigger_id = 'section_complete';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_body() {\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{SECTION_TITLE}}' );\n\t\t}\n\t\treturn sprintf( __( 'Congratulations! You finished %s', 'lifterlms' ), '{{SECTION_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{COURSE_PROGRESS}}' => __( 'Course Progress Bar', 'lifterlms' ),\n\t\t\t'{{COURSE_TITLE}}'    => __( 'Course Title', 'lifterlms' ),\n\t\t\t'{{SECTION_TITLE}}'   => __( 'Section Title', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}'    => __( 'Student Name', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @param    string $code  the merge code to ge merged data for\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.10.1\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{COURSE_PROGRESS}}':\n\t\t\t\t$progress = $this->user->get_progress( $this->post->get( 'parent_course' ), 'course' );\n\t\t\t\t$code     = lifterlms_course_progress_bar( $progress, false, false, false );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{COURSE_TITLE}}':\n\t\t\t\t$course = $this->post->get_course();\n\t\t\t\tif ( $course ) {\n\t\t\t\t\t$code = $course->get( 'title' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = '';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{SECTION_TITLE}}':\n\t\t\t\t$code = $this->post->get( 'title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->is_for_self() ? __( 'you', 'lifterlms' ) : $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'Congratulations! %1$s completed %2$s', 'lifterlms' ), '{{STUDENT_NAME}}', '{{SECTION_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( '%s Completed a Section', 'lifterlms' ), '{{STUDENT_NAME}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.student.welcome.php",
    "content": "<?php\n/**\n * Notification View: Student Welcome\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.8.0\n * @version 3.10.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Student Welcome\n *\n * @since 3.8.0\n * @version 3.10.1\n */\nclass LLMS_Notification_View_Student_Welcome extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Notification Trigger ID\n\t *\n\t * @var [type]\n\t */\n\tpublic $trigger_id = 'student_welcome';\n\n\t/**\n\t * Setup body content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_body() {\n\n\t\tob_start();\n\t\t?><p><?php printf( esc_html__( 'Hello %s,', 'lifterlms' ), '{{STUDENT_NAME}}' ); ?></p>\n\t\t<p><?php printf( esc_html__( 'Here\\'s some helpful information to help you get started at %s.', 'lifterlms' ), '{{SITE_TITLE}}' ); ?></p>\n\t\t<p><b><?php esc_html_e( 'Your Login', 'lifterlms' ); ?></b>: {{STUDENT_LOGIN}}</p>\n\t\t<p><b><?php esc_html_e( 'Your Dashboard', 'lifterlms' ); ?></b>: <a href=\"{{DASHBOARD_URL}}\">{{DASHBOARD_URL}}</a></p>\n\t\t<p><?php esc_html_e( 'If you forgot or don\\'t have a password you can reset it now so you can login and get started:', 'lifterlms' ); ?> <a href=\"{{PASSWORD_RESET_URL}}\">{{PASSWORD_RESET_URL}}</a></p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup footer content for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'positive' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification\n\t *\n\t * @return   array\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{DASHBOARD_URL}}'      => __( 'Dashboard URL', 'lifterlms' ),\n\t\t\t'{{PASSWORD_RESET_URL}}' => __( 'Password Reset URL', 'lifterlms' ),\n\t\t\t'{{SITE_TITLE}}'         => __( 'Site Title', 'lifterlms' ),\n\t\t\t'{{STUDENT_NAME}}'       => __( 'Student Name', 'lifterlms' ),\n\t\t\t'{{STUDENT_LOGIN}}'      => __( 'Student Login', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values\n\t *\n\t * @param    string $code  the merge code to ge merged data for\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.2\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{DASHBOARD_URL}}':\n\t\t\t\t$code = llms_get_page_url( 'myaccount' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PASSWORD_RESET_URL}}':\n\t\t\t\t$code = wp_lostpassword_url();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{SITE_TITLE}}':\n\t\t\t\t$code = get_bloginfo( 'name', 'display' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_NAME}}':\n\t\t\t\t$code = $this->user->get_name();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{STUDENT_LOGIN}}':\n\t\t\t\t$field = ( LLMS_Forms::instance()->are_usernames_enabled() ) ? 'user_login' : 'user_email';\n\t\t\t\t$code  = $this->user->get( $field );\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Setup notification subject for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_subject() {\n\t\treturn sprintf( __( 'Welcome to %s', 'lifterlms' ), '{{SITE_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @return   string\n\t * @since    3.8.0\n\t * @version  3.8.0\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( __( 'Let\\'s get started %s', 'lifterlms' ), '{{STUDENT_NAME}}' );\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.subscription.cancelled.php",
    "content": "<?php\n/**\n * Notification View: Student Welcome.\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 3.17.8\n * @version 5.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Purchase Receipt.\n *\n * @since 3.17.8\n */\nclass LLMS_Notification_View_Subscription_Cancelled extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Notification Trigger ID.\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'subscription_cancelled';\n\n\t/**\n\t * Setup body content for output.\n\t *\n\t * @since 3.17.8\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\n\t\treturn sprintf(\n\t\t\t__( '%1$s has cancelled their subscription (#%2$s) to the %3$s %4$s', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_NAME}}',\n\t\t\t'{{ORDER_ID}}',\n\t\t\t'{{PRODUCT_TYPE}}',\n\t\t\t'{{PRODUCT_TITLE_LINK}}'\n\t\t);\n\n\t}\n\n\t/**\n\t * Setup footer content for output.\n\t *\n\t * @since 3.17.8\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup notification icon for output.\n\t *\n\t * @since 3.17.8\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn '';\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification.\n\t *\n\t * @since 3.17.8\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{CUSTOMER_NAME}}'      => __( 'Customer Name', 'lifterlms' ),\n\t\t\t'{{ORDER_ID}}'           => __( 'Order ID', 'lifterlms' ),\n\t\t\t'{{PLAN_TITLE}}'         => __( 'Plan Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE}}'      => __( 'Product Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TYPE}}'       => __( 'Product Type', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE_LINK}}' => __( 'Product Title (Link)', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values.\n\t *\n\t * @since 3.17.8\n\t * @since 5.4.0 Account for deleted products.\n\t *\n\t * @param string $code The merge code to ge merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$order = $this->post;\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{CUSTOMER_NAME}}':\n\t\t\t\t$code = $order->get_customer_name();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_ID}}':\n\t\t\t\t$code = $order->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PLAN_TITLE}}':\n\t\t\t\t$code = $order->get( 'plan_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE}}':\n\t\t\t\t$code = $order->get( 'product_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE_LINK}}':\n\t\t\t\t$permalink = esc_url( get_permalink( $order->get( 'product_id' ) ) );\n\t\t\t\tif ( $permalink ) {\n\t\t\t\t\t$title = $this->set_merge_data( '{{PRODUCT_TITLE}}' );\n\t\t\t\t\t$code  = '<a href=\"' . $permalink . '\">' . $title . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TYPE}}':\n\t\t\t\t$obj = $order->get_product();\n\t\t\t\tif ( empty( $obj ) ) {\n\t\t\t\t\t$code = __( '[DELETED ITEM]', 'lifterlms' );\n\t\t\t\t} elseif ( is_a( $obj, 'WP_Post' ) ) {\n\t\t\t\t\t$code = _x( 'Item', 'generic product type description', 'lifterlms' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = $obj->get_post_type_label( 'singular_name' );\n\t\t\t\t}\n\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\n\t}\n\n\t/**\n\t * Setup notification subject for output.\n\t *\n\t * @since 3.17.8\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\treturn esc_html__( 'Subscription Cancellation Notice', 'lifterlms' );\n\t}\n\n\t/**\n\t * Setup notification title for output.\n\t *\n\t * @since 3.17.8\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\treturn sprintf( esc_html__( '%1$s subscription cancellation', 'lifterlms' ), '{{PRODUCT_TYPE}}' );\n\t}\n\n}\n"
  },
  {
    "path": "includes/notifications/views/class.llms.notification.view.upcoming.payment.reminder.php",
    "content": "<?php\n/**\n * Notification View: Upcoming Payment Reminder.\n *\n * @package LifterLMS/Notifications/Views/Classes\n *\n * @since 5.2.0\n * @version 5.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Notification View: Payment Retry.\n *\n * @since 5.2.0\n */\nclass LLMS_Notification_View_Upcoming_Payment_Reminder extends LLMS_Abstract_Notification_View {\n\n\t/**\n\t * Settings for basic notifications.\n\t *\n\t * @var array\n\t */\n\tprotected $basic_options = array(\n\t\t/**\n\t\t * Time in milliseconds to show a notification\n\t\t * before automatically dismissing it.\n\t\t */\n\t\t'auto_dismiss' => 10000,\n\t\t/**\n\t\t * Enables manual dismissal of notifications.\n\t\t */\n\t\t'dismissible'  => true,\n\t);\n\n\n\t/**\n\t * Notification Trigger ID.\n\t *\n\t * @var string\n\t */\n\tpublic $trigger_id = 'upcoming_payment_reminder';\n\n\t/**\n\t * Setup body content for output.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_body() {\n\n\t\tif ( 'email' === $this->notification->get( 'type' ) ) {\n\t\t\treturn $this->set_body_email();\n\t\t}\n\t\treturn $this->set_body_basic();\n\t}\n\n\t/**\n\t * Setup default notification body for basic notifications.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprivate function set_body_basic() {\n\t\treturn sprintf( esc_html__( 'You will be charged for your subscription to %1$s tomorrow.', 'lifterlms' ), '{{PRODUCT_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup default notification body for email notifications.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprivate function set_body_email() {\n\n\t\t$mailer = llms()->mailer();\n\n\t\t$rows = array(\n\t\t\t'NEXT_PAYMENT_DATE'  => __( 'Payment Due Date', 'lifterlms' ),\n\t\t\t'PRODUCT_TITLE_LINK' => '{{PRODUCT_TYPE}}',\n\t\t\t'PLAN_TITLE'         => __( 'Plan', 'lifterlms' ),\n\t\t\t'PAYMENT_AMOUNT'     => __( 'Amount', 'lifterlms' ),\n\t\t);\n\n\t\tob_start();\n\t\t?>\n\t\t<p>\n\t\t<?php\n\t\t\t// Translators: %s= The customer name.\n\t\t\tprintf( esc_html__( 'Hello %s,', 'lifterlms' ), '{{CUSTOMER_NAME}}' );\n\t\t?>\n\t\t</p>\n\t\t<p>\n\t\t<?php\n\t\t\t// Translators: %1$s = The product title, %2$s The upcoming payment due date.\n\t\t\tprintf( esc_html__( 'You will be charged for your subscription to %1$s on %2$s.', 'lifterlms' ), '{{PRODUCT_TITLE}}', '{{NEXT_PAYMENT_DATE}}' );\n\t\t?>\n\t\t</p>\n\t\t<h4>\n\t\t<?php\n\t\t\t// Translators: %s= The order ID.\n\t\t\tprintf( esc_html__( 'Order #%s', 'lifterlms' ), '{{ORDER_ID}}' );\n\t\t?>\n\t\t</h4>\n\t\t<?php\n\t\t$mailer->output_table_html( $rows );\n\t\t?>\n\t\t<p><a href=\"{{ORDER_URL}}\"><?php esc_html_e( 'Update Payment Method', 'lifterlms' ); ?></a></p>\n\t\t<?php\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Setup footer content for output.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_footer() {\n\t\t$url = $this->set_merge_data( '{{ORDER_URL}}' );\n\t\treturn '<a href=\"' . esc_url( $url ) . '\">' . esc_html__( 'Update Payment Method', 'lifterlms' ) . '</a>';\n\t}\n\n\t/**\n\t * Setup notification icon for output.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_icon() {\n\t\treturn $this->get_icon_default( 'warning' );\n\t}\n\n\t/**\n\t * Setup merge codes that can be used with the notification.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return array\n\t */\n\tprotected function set_merge_codes() {\n\t\treturn array(\n\t\t\t'{{CUSTOMER_ADDRESS}}'   => __( 'Customer Address', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_NAME}}'      => __( 'Customer Name', 'lifterlms' ),\n\t\t\t'{{CUSTOMER_PHONE}}'     => __( 'Customer Phone', 'lifterlms' ),\n\t\t\t'{{NEXT_PAYMENT_DATE}}'  => __( 'Next Payment Date', 'lifterlms' ),\n\t\t\t'{{ORDER_ID}}'           => __( 'Order ID', 'lifterlms' ),\n\t\t\t'{{ORDER_URL}}'          => __( 'Order URL', 'lifterlms' ),\n\t\t\t'{{PAYMENT_AMOUNT}}'     => __( 'Payment Amount', 'lifterlms' ),\n\t\t\t'{{PLAN_TITLE}}'         => __( 'Plan Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE}}'      => __( 'Product Title', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TYPE}}'       => __( 'Product Type', 'lifterlms' ),\n\t\t\t'{{PRODUCT_TITLE_LINK}}' => __( 'Product Title (Link)', 'lifterlms' ),\n\t\t);\n\t}\n\n\t/**\n\t * Replace merge codes with actual values.\n\t *\n\t * @since 5.2.0\n\t * @since 5.4.0 Account for deleted products.\n\t *\n\t * @param string $code The merge code to get merged data for.\n\t * @return string\n\t */\n\tprotected function set_merge_data( $code ) {\n\n\t\t$order = $this->post;\n\n\t\tswitch ( $code ) {\n\n\t\t\tcase '{{CUSTOMER_ADDRESS}}':\n\t\t\t\t$code = $order->get_customer_full_address();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_NAME}}':\n\t\t\t\t$code = $order->get_customer_name();\n\t\t\t\tbreak;\n\n\t\t\tcase '{{CUSTOMER_PHONE}}':\n\t\t\t\t$code = $order->get( 'billing_phone' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{NEXT_PAYMENT_DATE}}':\n\t\t\t\t$code = $order->get_date( 'date_next_payment', get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_ID}}':\n\t\t\t\t$code = $order->get( 'id' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{ORDER_URL}}':\n\t\t\t\t$code = esc_url( $order->get_view_link() );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PAYMENT_AMOUNT}}':\n\t\t\t\t$code = $order->get_price( 'total' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PLAN_TITLE}}':\n\t\t\t\t$code = $order->get( 'plan_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE}}':\n\t\t\t\t$code = $order->get( 'product_title' );\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TITLE_LINK}}':\n\t\t\t\t$permalink = esc_url( get_permalink( $order->get( 'product_id' ) ) );\n\t\t\t\tif ( $permalink ) {\n\t\t\t\t\t$title = $this->set_merge_data( '{{PRODUCT_TITLE}}' );\n\t\t\t\t\t$code  = '<a href=\"' . $permalink . '\">' . $title . '</a>';\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase '{{PRODUCT_TYPE}}':\n\t\t\t\t$obj = $order->get_product();\n\t\t\t\tif ( empty( $obj ) ) {\n\t\t\t\t\t$code = __( '[DELETED ITEM]', 'lifterlms' );\n\t\t\t\t} elseif ( is_a( $obj, 'WP_Post' ) ) {\n\t\t\t\t\t$code = _x( 'Item', 'generic product type description', 'lifterlms' );\n\t\t\t\t} else {\n\t\t\t\t\t$code = $obj->get_post_type_label( 'singular_name' );\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\treturn $code;\n\t}\n\n\t/**\n\t * Setup notification subject for output.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_subject() {\n\t\t// Translators: %s = The product title.\n\t\treturn sprintf( __( 'Upcoming payment reminder for your subscription to %1$s', 'lifterlms' ), '{{PRODUCT_TITLE}}' );\n\t}\n\n\t/**\n\t * Setup notification title for output\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return string\n\t */\n\tprotected function set_title() {\n\t\treturn __( 'Upcoming Subscription Payment', 'lifterlms' );\n\t}\n}\n"
  },
  {
    "path": "includes/notifications/views/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/privacy/class-llms-privacy-erasers.php",
    "content": "<?php\n/**\n * LifterLMS Privacy Eraser\n *\n * @package LifterLMS/Privacy/Classes\n *\n * @since 3.18.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Privacy Eraser class\n *\n * @since 3.18.0\n * @since 3.30.3 Fixed spelling errors.\n */\nclass LLMS_Privacy_Erasers extends LLMS_Privacy {\n\n\t/**\n\t * Erase student certificate data by email address\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   array\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function achievement_data( $email_address, $page ) {\n\n\t\t$ret = self::get_return();\n\n\t\t$student = parent::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\t$messages     = array();\n\t\t$achievements = self::get_student_achievements( $student );\n\t\tif ( $achievements ) {\n\n\t\t\tforeach ( $achievements as $achievement ) {\n\t\t\t\t$messages[] = sprintf( 'Achievement %d deleted.', $achievement->get( 'id' ) );\n\t\t\t\t$achievement->delete();\n\t\t\t}\n\t\t}\n\n\t\treturn self::get_return( $messages, true, ( $messages ) );\n\n\t}\n\n\t/**\n\t * Setup anonymous values for anonymized data\n\t *\n\t * @param    string $val   default anonymous value ('')\n\t * @param    string $prop  key name of the property\n\t * @param    obj    $obj   related object\n\t * @return   mixed\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function anonymize_prop( $val, $prop, $obj = null ) {\n\n\t\tswitch ( $prop ) {\n\t\t\tcase 'user_id':\n\t\t\t\t$val = 0;\n\t\t\t\tbreak;\n\t\t}\n\n\t\treturn $val;\n\t}\n\n\t/**\n\t * Erase student certificate data by email address\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   array\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function certificate_data( $email_address, $page ) {\n\n\t\t$ret = self::get_return();\n\n\t\t$student = parent::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\t$messages = array();\n\t\t$certs    = self::get_student_certificates( $student );\n\t\tif ( $certs ) {\n\n\t\t\tforeach ( $certs as $cert ) {\n\t\t\t\t$messages[] = sprintf( 'Certificate %d deleted.', $cert->get( 'id' ) );\n\t\t\t\t$cert->delete();\n\t\t\t}\n\t\t}\n\n\t\treturn self::get_return( $messages, true, ( $messages ) );\n\n\t}\n\n\t/**\n\t * Return export data to an exporter\n\t *\n\t * @param    array $messages  array of messages\n\t * @return   array\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tprivate static function get_return( $messages = array(), $done = true, $removed = false, $retained = false ) {\n\t\treturn array(\n\t\t\t'messages'       => $messages,\n\t\t\t'done'           => $done,\n\t\t\t'items_removed'  => $removed,\n\t\t\t'items_retained' => $retained,\n\t\t);\n\t}\n\n\t/**\n\t * Erase notifications for a student\n\t *\n\t * @param    LLMS_Student $student\n\t * @return   array\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tprivate static function erase_notification_data( $student ) {\n\n\t\t$messages = array();\n\t\tglobal $wpdb;\n\t\t$deleted = $wpdb->query(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"DELETE FROM {$wpdb->prefix}lifterlms_notifications WHERE user_id = %d OR subscriber = %d\",\n\t\t\t\t$student->get( 'id' ),\n\t\t\t\t$student->get( 'id' )\n\t\t\t)\n\t\t);\n\n\t\tif ( $deleted ) {\n\n\t\t\t// Translators: %d = number of notifications.\n\t\t\t$messages[] = sprintf( __( 'Removed %d notifications.', 'lifterlms' ), $deleted );\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_privacy_erase_notification_data', $messages, $student );\n\n\t}\n\n\t/**\n\t * Erase and anonymize an order\n\t *\n\t * @since 3.18.0\n\t * @since 3.30.3 Fixed spelling error.\n\t *\n\t * @param LLMS_Order $order Order object.\n\t * @return void\n\t */\n\tprivate static function erase_order_data( $order ) {\n\n\t\t// Cancel recurring orders.\n\t\tif ( $order->is_recurring() && in_array( $order->get( 'status' ), array( 'llms-on-hold', 'llms-active', 'llms-pending-cancel' ) ) ) {\n\t\t\t$order->set_status( 'cancelled' );\n\t\t\t$order->add_note( __( 'Order cancelled during personal data erasure.', 'lifterlms' ) );\n\t\t}\n\n\t\t$props = array_keys( self::get_order_data_props( 'erasure' ) );\n\t\tforeach ( $props as $prop ) {\n\n\t\t\t$val = self::get_anon_prop_value( $prop );\n\t\t\t$order->set( $prop, $val );\n\n\t\t}\n\n\t\t$order->set( 'anonymized', 'yes' );\n\t\t$order->add_note( __( 'Personal data removed during personal data erasure.', 'lifterlms' ) );\n\n\t}\n\n\t/**\n\t * Erase student data\n\t *\n\t * @param    LLMS_Student $student\n\t * @return   array\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tprivate static function erase_student_data( $student ) {\n\n\t\t$messages = array();\n\n\t\t$props = parent::get_student_data_props();\n\n\t\tforeach ( $props as $prop => $name ) {\n\n\t\t\t$erased = false;\n\n\t\t\t$val = $student->get( $prop );\n\t\t\tif ( $val ) {\n\t\t\t\t$student->set( $prop, '' );\n\t\t\t\t$erased = true;\n\t\t\t}\n\n\t\t\tif ( apply_filters( 'llms_privacy_erase_student_data_prop', $erased, $prop, $student ) ) {\n\n\t\t\t\t/* Translators: %s Prop name. */\n\t\t\t\t$messages[] = sprintf( __( 'Removed student \"%s\"', 'lifterlms' ), $name );\n\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_privacy_erase_student_data', $messages, $student );\n\n\t}\n\n\t/**\n\t * Erase student notification data by email address\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   [type]\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function notification_data( $email_address, $page ) {\n\n\t\t$ret = self::get_return();\n\n\t\t$student = parent::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\t$messages = self::erase_notification_data( $student );\n\t\treturn self::get_return( $messages, true, ( $messages ) );\n\n\t}\n\n\t/**\n\t * Erase student order data by email address\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   array\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function order_data( $email_address, $page ) {\n\n\t\t$ret = self::get_return();\n\n\t\t$student = parent::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\t$enabled = llms_parse_bool( get_option( 'llms_erasure_request_removes_order_data', 'no' ) );\n\t\t$orders  = self::get_student_orders( $student, $page );\n\n\t\tforeach ( $orders['orders'] as $order ) {\n\n\t\t\tif ( apply_filters( 'llms_privacy_erase_order_data', $enabled, $order ) ) {\n\n\t\t\t\tself::erase_order_data( $order );\n\n\t\t\t\t/* Translators: %d Order number. */\n\t\t\t\t$ret['messages'][]    = sprintf( __( 'Removed personal data from order #%d.', 'lifterlms' ), $order->get( 'id' ) );\n\t\t\t\t$ret['items_removed'] = true;\n\n\t\t\t} else {\n\n\t\t\t\t/* Translators: %d Order number. */\n\t\t\t\t$ret['messages'][]     = sprintf( __( 'Personal data within order #%d has been retained.', 'lifterlms' ), $order->get( 'id' ) );\n\t\t\t\t$ret['items_retained'] = true;\n\n\t\t\t}\n\t\t}\n\n\t\t$ret['done'] = isset( $orders['done'] ) ? $orders['done'] : true;\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Erase student postmeta data by email address\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   [type]\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function postmeta_data( $email_address, $page ) {\n\n\t\t$ret = self::get_return();\n\n\t\t$student = parent::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\t$messages = array();\n\t\t$enabled  = llms_parse_bool( get_option( 'llms_erasure_request_removes_lms_data', 'no' ) );\n\n\t\tif ( apply_filters( 'llms_privacy_erase_postmeta_data', $enabled, $attempt ) ) {\n\n\t\t\tglobal $wpdb;\n\t\t\t$deleted = $wpdb->query(\n\t\t\t\t$wpdb->prepare(\n\t\t\t\t\t\"DELETE FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d\",\n\t\t\t\t\t$student->get( 'id' )\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$ret['messages'][]    = __( 'Removed all student course and membership enrollment and activity data.', 'lifterlms' );\n\t\t\t$ret['items_removed'] = true;\n\n\t\t} else {\n\n\t\t\t$ret['messages'][]     = __( 'Retained all student course and membership enrollment and activity data.', 'lifterlms' );\n\t\t\t$ret['items_retained'] = true;\n\n\t\t}\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Erase student quiz attempt data by email address\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   array\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function quiz_data( $email_address, $page ) {\n\n\t\t$ret = self::get_return();\n\n\t\t$student = parent::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\t$enabled = llms_parse_bool( get_option( 'llms_erasure_request_removes_lms_data', 'no' ) );\n\t\t$query   = self::get_student_quizzes( $student, $page );\n\n\t\tforeach ( $query->get_attempts() as $attempt ) {\n\n\t\t\tif ( apply_filters( 'llms_privacy_erase_quiz_data', $enabled, $attempt ) ) {\n\n\t\t\t\t/* Translators: %d quiz attempt id. */\n\t\t\t\t$ret['messages'][]    = sprintf( __( 'Quiz attempt #%d removed.', 'lifterlms' ), $attempt->get_id() );\n\t\t\t\t$ret['items_removed'] = true;\n\n\t\t\t\t$attempt->delete();\n\n\t\t\t} else {\n\n\t\t\t\t/* Translators: %d quiz attempt id. */\n\t\t\t\t$ret['messages'][]     = sprintf( __( 'Quiz attempt #%d retained.', 'lifterlms' ), $attempt->get_id() );\n\t\t\t\t$ret['items_retained'] = true;\n\n\t\t\t}\n\t\t}\n\n\t\t$ret['done'] = $query->has_results() ? $query->is_last_page() : true;\n\n\t\treturn $ret;\n\n\t}\n\n\t/**\n\t * Erase student data by email address\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   [type]\n\t * @since    3.18.0\n\t * @version  3.18.0\n\t */\n\tpublic static function student_data( $email_address, $page ) {\n\n\t\t$ret = self::get_return();\n\n\t\t$student = parent::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn $ret;\n\t\t}\n\n\t\t$messages = self::erase_student_data( $student );\n\t\treturn self::get_return( $messages, true, ( $messages ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/privacy/class-llms-privacy-exporters.php",
    "content": "<?php\n/**\n * LifterLMS Privacy Exporter\n *\n * @package LifterLMS/Privacy/Classes\n *\n * @since 3.18.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Privacy Exporter class\n *\n * @since 3.18.0\n * @since 3.30.3 Fixed spelling error.\n * @since 3.37.9 Add export group descriptions.\n */\nclass LLMS_Privacy_Exporters extends LLMS_Privacy {\n\n\t/**\n\t * Export student achievement data by email address\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.9 Added `$group_description` to the group exporter.\n\t *\n\t * @param    string $email_address  Email address of the user to retrieve data for.\n\t * @param    int    $page           Process page number.\n\t * @return   array\n\t */\n\tpublic static function achievement_data( $email_address, $page ) {\n\n\t\t$data = array();\n\n\t\t$student = self::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn self::get_return( $data );\n\t\t}\n\n\t\t$achievements = self::get_student_achievements( $student );\n\t\tif ( $achievements ) {\n\n\t\t\t$group_label       = __( 'Achievements', 'lifterlms' );\n\t\t\t$group_description = __( 'Student achievement data.', 'lifterlms' );\n\t\t\tforeach ( $achievements as $achievement ) {\n\n\t\t\t\t$data[] = array(\n\t\t\t\t\t'group_id'          => 'lifterlms_achievements',\n\t\t\t\t\t'group_label'       => $group_label,\n\t\t\t\t\t'group_description' => $group_description,\n\t\t\t\t\t'item_id'           => sprintf( 'achievement-%d', $achievement->get( 'id' ) ),\n\t\t\t\t\t'data'              => self::get_achievement_data( $achievement ),\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t\treturn self::get_return( $data );\n\n\t}\n\n\t/**\n\t * Export student certificate data by email address\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.9 Added `$group_description` to the group exporter.\n\t *\n\t * @param    string $email_address Email address of the user to retrieve data for.\n\t * @param    int    $page          Process page number.\n\t * @return   array\n\t */\n\tpublic static function certificate_data( $email_address, $page ) {\n\n\t\t$data = array();\n\n\t\t$student = self::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn self::get_return( $data );\n\t\t}\n\n\t\t$certs = self::get_student_certificates( $student );\n\t\tif ( $certs ) {\n\n\t\t\t$group_label       = __( 'Certificates', 'lifterlms' );\n\t\t\t$group_description = __( 'Student certificate data.', 'lifterlms' );\n\t\t\tforeach ( $certs as $cert ) {\n\n\t\t\t\t$data[] = array(\n\t\t\t\t\t'group_id'          => 'lifterlms_certificates',\n\t\t\t\t\t'group_label'       => $group_label,\n\t\t\t\t\t'group_description' => $group_description,\n\t\t\t\t\t'item_id'           => sprintf( 'certificate-%d', $cert->get( 'id' ) ),\n\t\t\t\t\t'data'              => self::get_certificate_data( $cert ),\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t\treturn self::get_return( $data );\n\n\t}\n\n\t/**\n\t * Get data for a certificate\n\t *\n\t * @since    3.18.0\n\t *\n\t * @param    obj $achievement  LLMS_User_Certificate.\n\t * @return   array\n\t */\n\tprivate static function get_achievement_data( $achievement ) {\n\n\t\t$data = array();\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Title', 'lifterlms' ),\n\t\t\t'value' => $achievement->get( 'title' ),\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Description', 'lifterlms' ),\n\t\t\t'value' => $achievement->get( 'content' ),\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Earned Date', 'lifterlms' ),\n\t\t\t'value' => $achievement->get_earned_date( 'Y-m-d H:i:s' ),\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Image', 'lifterlms' ),\n\t\t\t'value' => $achievement->get_image(),\n\t\t);\n\n\t\treturn $data;\n\n\t}\n\n\n\n\t/**\n\t * Get data for a certificate.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Replaced the use of the deprecated `certificate_title` meta key with the post's title property.\n\t *\n\t * @param LLMS_User_Certificate $cert Certificate object.\n\t * @return array\n\t */\n\tprivate static function get_certificate_data( $cert ) {\n\n\t\t$data = array();\n\n\t\t$title = $cert->get( 'title' );\n\n\t\t$filename = llms()->certificates()->get_export( $cert->get( 'id' ), true );\n\t\tif ( ! is_wp_error( $filename ) ) {\n\t\t\t$title = '<a href=\"certificates/' . basename( $filename ) . '\">' . $title . '</a>';\n\t\t}\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Title', 'lifterlms' ),\n\t\t\t'value' => $title,\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Earned Date', 'lifterlms' ),\n\t\t\t'value' => $cert->get_earned_date( 'Y-m-d H:i:s' ),\n\t\t);\n\n\t\treturn $data;\n\n\t}\n\n\t/**\n\t * Get an array of enrollment data for a course or membership\n\t *\n\t * @since 3.18.0\n\t * @since 3.30.3 Fixed spelling errors.\n\t *\n\t * @param int $post_id WP Post ID of course or membership.\n\t * @param obj $student LLMS_Student.\n\t * @param obj $post_type_object WP post type object.\n\t * @return array\n\t */\n\tprivate static function get_enrollment_data( $post_id, $student, $post_type_object ) {\n\n\t\t$data = array();\n\n\t\t$data[] = array(\n\t\t\t// Translators: %s = post type singular name label (Course or Membership).\n\t\t\t'name'  => sprintf( __( '%s Title', 'lifterlms' ), $post_type_object->labels->singular_name ),\n\t\t\t'value' => get_the_title( $post_id ),\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Enrollment Status', 'lifterlms' ),\n\t\t\t'value' => llms_get_enrollment_status_name( $student->get_enrollment_status( $post_id ) ),\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Enrollment Date', 'lifterlms' ),\n\t\t\t'value' => $student->get_enrollment_date( $post_id, 'enrolled', 'Y-m-d H:i:s' ),\n\t\t);\n\n\t\tif ( 'course' === $post_type_object->name ) {\n\n\t\t\t$data[] = array(\n\t\t\t\t'name'  => __( 'Last Activity', 'lifterlms' ),\n\t\t\t\t'value' => $student->get_enrollment_date( $post_id, 'updated', 'Y-m-d H:i:s' ),\n\t\t\t);\n\n\t\t\t$progress = $student->get_progress( $post_id, 'course' );\n\t\t\tif ( is_numeric( $progress ) ) {\n\t\t\t\t$progress .= '%';\n\t\t\t}\n\t\t\t$data[] = array(\n\t\t\t\t'name'  => __( 'Progress', 'lifterlms' ),\n\t\t\t\t'value' => $progress,\n\t\t\t);\n\n\t\t\t$grade = $student->get_grade( $post_id );\n\t\t\tif ( is_numeric( $grade ) ) {\n\t\t\t\t$grade .= '%';\n\t\t\t}\n\t\t\t$data[] = array(\n\t\t\t\t'name'  => __( 'Grade', 'lifterlms' ),\n\t\t\t\t'value' => $grade,\n\t\t\t);\n\n\t\t}\n\n\t\treturn apply_filters( 'llms_privacy_export_enrollment_data', $data, $post_id, $student, $post_type_object );\n\n\t}\n\n\t/**\n\t * Retrieve export data for a single order\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param  LLMS_Order $order Order object.\n\t * @return array\n\t */\n\tprivate static function get_order_data( $order ) {\n\n\t\t$data = array();\n\n\t\t$props = self::get_order_data_props( 'export' );\n\n\t\tforeach ( $props as $prop => $name ) {\n\n\t\t\t$value = apply_filters( 'llms_privacy_export_order_data_prop_value', $order->get( $prop ), $prop, $order );\n\n\t\t\tif ( $value ) {\n\t\t\t\t$data[] = array(\n\t\t\t\t\t'name'  => $name,\n\t\t\t\t\t'value' => $value,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t$transactions = $order->get_transactions(\n\t\t\tarray(\n\t\t\t\t'per_page' => 500,\n\t\t\t)\n\t\t);\n\t\tif ( $transactions['transactions'] ) {\n\t\t\t$txns = array();\n\t\t\tforeach ( $transactions['transactions'] as $txn ) {\n\t\t\t\t$txns[] = sprintf( '%1$s &mdash; %2$s (#%3$d)', $txn->get( 'date' ), $txn->get_price( 'amount' ), $txn->get( 'id' ) );\n\t\t\t}\n\t\t\t$data[] = array(\n\t\t\t\t'name'  => __( 'Transactions', 'lifterlms' ),\n\t\t\t\t'value' => implode( '<br>', $txns ),\n\t\t\t);\n\t\t}\n\n\t\treturn apply_filters( 'llms_privacy_export_order_data', $data, $order );\n\t}\n\n\t/**\n\t * Get export data for a single quiz attempt\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param LLMS_Quiz_Attempt $attempt Quiz attempt object.\n\t * @return array\n\t */\n\tprivate static function get_quiz_attempt_data( $attempt ) {\n\n\t\t$data = array();\n\n\t\t$quiz = $attempt->get_quiz();\n\t\tif ( $quiz ) {\n\t\t\t$data[] = array(\n\t\t\t\t'name'  => __( 'Title', 'lifterlms' ),\n\t\t\t\t'value' => $quiz->get( 'title' ),\n\t\t\t);\n\t\t}\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Attempt ID', 'lifterlms' ),\n\t\t\t'value' => $attempt->get_key(),\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Attempt Number', 'lifterlms' ),\n\t\t\t'value' => $attempt->get( 'attempt' ),\n\t\t);\n\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Status', 'lifterlms' ),\n\t\t\t'value' => $attempt->l10n( 'status' ),\n\t\t);\n\n\t\t$grade  = $attempt->get( 'grade' );\n\t\t$data[] = array(\n\t\t\t'name'  => __( 'Grade', 'lifterlms' ),\n\t\t\t'value' => is_numeric( $grade ) ? $grade . '%' : '&ndash;',\n\t\t);\n\n\t\treturn $data;\n\t}\n\n\t/**\n\t * Return export data to an exporter\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param array $data Array of data.\n\t * @return array\n\t */\n\tprivate static function get_return( $data = array(), $done = true ) {\n\t\treturn array(\n\t\t\t'data' => $data,\n\t\t\t'done' => $done,\n\t\t);\n\t}\n\n\t/**\n\t * Get student data to export for a user\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param  LLMS_Student $student Student object.\n\t * @return array\n\t */\n\tprivate static function get_student_data( $student ) {\n\n\t\t$data = array();\n\n\t\t$props = self::get_student_data_props();\n\n\t\tforeach ( $props as $prop => $name ) {\n\n\t\t\t$value = apply_filters( 'llms_privacy_export_student_data_prop_value', $student->get( $prop ), $prop, $student );\n\n\t\t\tif ( $value ) {\n\t\t\t\t$data[] = array(\n\t\t\t\t\t'name'  => $name,\n\t\t\t\t\t'value' => $value,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn apply_filters( 'llms_privacy_export_student_data', $data, $student );\n\n\t}\n\n\t/**\n\t * Export student course data by email address\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param string $email_address Email address of the user to retrieve data for.\n\t * @param int    $page          Process page number.\n\t * @return array\n\t */\n\tpublic static function course_data( $email_address, $page ) {\n\t\treturn self::enrollment_data( $email_address, $page, 'course' );\n\t}\n\n\t/**\n\t * General exporter for handling course and membership enrollment data\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.9 Added `$group_description` to the group exporter.\n\t *\n\t * @param    string $email_address  Requested user's email address\n\t * @param    int    $page           process page number\n\t * @param    string $post_type      name of the post type\n\t * @return   array\n\t */\n\tprivate static function enrollment_data( $email_address, $page, $post_type ) {\n\n\t\t$data = array();\n\n\t\t$student = self::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn self::get_return( $data );\n\t\t}\n\n\t\t$enrollments = self::get_student_enrollments( $student, $page, $post_type );\n\t\tif ( $enrollments['results'] ) {\n\n\t\t\t$post_type_obj = get_post_type_object( $post_type );\n\t\t\t$group_id      = 'lifterlms_' . $post_type;\n\n\t\t\tforeach ( $enrollments['results'] as $post_id ) {\n\n\t\t\t\t$data[] = array(\n\t\t\t\t\t'group_id'          => $group_id,\n\t\t\t\t\t'group_label'       => $post_type_obj->labels->name,\n\t\t\t\t\t/* translators: %s: The name of the enrollment post type. */\n\t\t\t\t\t'group_description' => sprintf( __( 'Student %s enrollment data.', 'lifterlms' ), $post_type_obj->labels->name ),\n\t\t\t\t\t'item_id'           => sprintf( '%1$s-%2$d', $post_type, $post_id ),\n\t\t\t\t\t'data'              => self::get_enrollment_data( $post_id, $student, $post_type_obj ),\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t\treturn self::get_return( $data, $enrollments['done'] );\n\n\t}\n\n\t/**\n\t * Add files to the zip file for a data export request.\n\t *\n\t * Adds certificate files into the `/certificates/` directory within the archive.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Replaced the use of the deprecated `wp_get_user_request_data()` function with `wp_get_user_request()`.\n\t *\n\t * @param string $archive_pathname     Full path to the zip archive.\n\t * @param string $archive_url          Full URI to the zip archive.\n\t * @param string $html_report_pathname Full path to the .html file within the archive.\n\t * @param int    $request_id           WP Post ID of the export request.\n\t * @return void\n\t */\n\tpublic static function maybe_add_export_files( $archive_pathname, $archive_url, $html_report_pathname, $request_id ) {\n\n\t\tif ( ! class_exists( 'ZipArchive' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$request = wp_get_user_request( $request_id );\n\t\t$student = self::get_student_by_email( $request->email );\n\n\t\tif ( ! $student ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$certs = self::get_student_certificates( $student );\n\t\tif ( ! $certs ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$zip    = new ZipArchive();\n\t\t$delete = array();\n\t\tif ( true === $zip->open( $archive_pathname ) ) {\n\t\t\tforeach ( $certs as $cert ) {\n\t\t\t\t$filepath                        = llms()->certificates()->get_export( $cert->get( 'id' ), true );\n\t\t\t\t$delete[ $cert->certificate_id ] = $filepath;\n\t\t\t\tif ( is_wp_error( $filepath ) ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t$zip->addFile( $filepath, '/certificates/' . basename( $filepath ) );\n\t\t\t}\n\t\t}\n\n\t\t$zip->close();\n\n\t\t// cleanup all files\n\t\tforeach ( $delete as $id => $path ) {\n\t\t\twp_delete_file( $path );\n\t\t\tdelete_post_meta( $id, '_llms_export_filepath' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Export student membership data by email address\n\t *\n\t * @since    3.18.0\n\t *\n\t * @param    string $email_address  email address of the user to retrieve data for\n\t * @param    int    $page           process page number\n\t * @return   array\n\t */\n\tpublic static function membership_data( $email_address, $page ) {\n\t\treturn self::enrollment_data( $email_address, $page, 'llms_membership' );\n\t}\n\n\t/**\n\t * Export student orders data by email address\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.9 Added `$group_description` to the group exporter.\n\t *\n\t * @param  string $email_address Email address of the user to retrieve data for.\n\t * @param  int    $page          Process page number.\n\t * @return array\n\t */\n\tpublic static function order_data( $email_address, $page ) {\n\n\t\t$data = array();\n\n\t\t$student = self::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn self::get_return( $data );\n\t\t}\n\n\t\t$orders = self::get_student_orders( $student, $page );\n\n\t\t$group_label       = __( 'Orders', 'lifterlms' );\n\t\t$group_description = __( 'Student orders data.', 'lifterlms' );\n\t\tforeach ( $orders['orders'] as $order ) {\n\n\t\t\t$data[] = array(\n\t\t\t\t'group_id'          => 'lifterlms_orders',\n\t\t\t\t'group_label'       => $group_label,\n\t\t\t\t'group_description' => $group_description,\n\t\t\t\t'item_id'           => sprintf( 'order-%d', $order->get( 'id' ) ),\n\t\t\t\t'data'              => self::get_order_data( $order ),\n\t\t\t);\n\n\t\t}\n\n\t\treturn self::get_return( $data, $orders['done'] );\n\n\t}\n\n\t/**\n\t * Export student data by email address\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.9 Added `$group_description` to the group exporter.\n\t *\n\t * @param  string $email_address Email address of the user to retrieve data for.\n\t * @param  int    $page          Process page number.\n\t * @return array\n\t */\n\tpublic static function student_data( $email_address, $page ) {\n\n\t\t$data = array();\n\n\t\t$student = self::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn self::get_return( $data );\n\t\t}\n\n\t\t$data[] = array(\n\t\t\t'group_id'          => 'lifterlms_student',\n\t\t\t'group_label'       => __( 'Personal Information', 'lifterlms' ),\n\t\t\t'group_description' => __( 'Student personal information data.', 'lifterlms' ),\n\t\t\t'item_id'           => sprintf( 'student-%d', $student->get( 'id' ) ),\n\t\t\t'data'              => self::get_student_data( $student ),\n\t\t);\n\n\t\treturn self::get_return( $data );\n\n\t}\n\n\t/**\n\t * Export quiz attempt data by email address\n\t *\n\t * @since    3.18.0\n\t * @since    3.37.9 Added `$group_description` to the group exporter.\n\t *\n\t * @param    string $email_address Email address of the user to retrieve data for.\n\t * @param    int    $page          Process page number.\n\t * @return   array\n\t */\n\tpublic static function quiz_data( $email_address, $page ) {\n\n\t\t$data = array();\n\n\t\t$student = self::get_student_by_email( $email_address );\n\t\tif ( ! $student ) {\n\t\t\treturn self::get_return( $data );\n\t\t}\n\n\t\t$query = self::get_student_quizzes( $student, $page );\n\t\t$done  = true;\n\t\tif ( $query->has_results() ) {\n\n\t\t\t$group_label        = __( 'Quiz Attempts', 'lifterlms' );\n\t\t\t$group_descriptions = __( 'Student quiz attempt data', 'lifterlms' );\n\t\t\tforeach ( $query->get_attempts() as $attempt ) {\n\n\t\t\t\t$data[] = array(\n\t\t\t\t\t'group_id'          => 'lifterlms_quizzes',\n\t\t\t\t\t'group_label'       => $group_label,\n\t\t\t\t\t'group_description' => $group_description,\n\t\t\t\t\t'item_id'           => sprintf( 'order-%d', $attempt->get( 'id' ) ),\n\t\t\t\t\t'data'              => self::get_quiz_attempt_data( $attempt ),\n\t\t\t\t);\n\n\t\t\t}\n\n\t\t\t$done = $query->is_last_page();\n\n\t\t}\n\n\t\treturn self::get_return( $data, $done );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/privacy/class-llms-privacy.php",
    "content": "<?php\n/**\n * Main Privacy Class\n *\n * @package LifterLMS/Privacy/Classes\n *\n * @since 3.18.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Privacy class\n *\n * Hooks into WP Core data exporters and erasers to export / erase LifterLMS data.\n *\n * @since 3.18.0\n * @since 3.37.9 Update CSS classes used in privacy text suggestions.\n */\nclass LLMS_Privacy extends LLMS_Abstract_Privacy {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tparent::__construct( __( 'LifterLMS', 'lifterlms' ) );\n\n\t\t/**\n\t\t * Exporters\n\t\t */\n\t\t$this->add_exporter( 'lifterlms-student-data', __( 'Student Data', 'lifterlms' ), array( 'LLMS_Privacy_Exporters', 'student_data' ) );\n\t\t$this->add_exporter( 'lifterlms-course-data', __( 'Course Data', 'lifterlms' ), array( 'LLMS_Privacy_Exporters', 'course_data' ) );\n\t\t$this->add_exporter( 'lifterlms-quiz-data', __( 'Quiz Data', 'lifterlms' ), array( 'LLMS_Privacy_Exporters', 'quiz_data' ) );\n\t\t$this->add_exporter( 'lifterlms-membership-data', __( 'Membership Data', 'lifterlms' ), array( 'LLMS_Privacy_Exporters', 'membership_data' ) );\n\t\t$this->add_exporter( 'lifterlms-order-data', __( 'Order Data', 'lifterlms' ), array( 'LLMS_Privacy_Exporters', 'order_data' ) );\n\t\t$this->add_exporter( 'lifterlms-achievement-data', __( 'Achievement Data', 'lifterlms' ), array( 'LLMS_Privacy_Exporters', 'achievement_data' ) );\n\t\t$this->add_exporter( 'lifterlms-certificate-data', __( 'Certificate Data', 'lifterlms' ), array( 'LLMS_Privacy_Exporters', 'certificate_data' ) );\n\n\t\t/**\n\t\t * Erasers\n\t\t */\n\t\t$this->add_eraser( 'lifterlms-student-data', __( 'Student Data', 'lifterlms' ), array( 'LLMS_Privacy_Erasers', 'student_data' ) );\n\t\t$this->add_eraser( 'lifterlms-quiz-data', __( 'Quiz Data', 'lifterlms' ), array( 'LLMS_Privacy_Erasers', 'quiz_data' ) );\n\t\t$this->add_eraser( 'lifterlms-order-data', __( 'Order Data', 'lifterlms' ), array( 'LLMS_Privacy_Erasers', 'order_data' ) );\n\t\t$this->add_eraser( 'lifterlms-achievement-data', __( 'Achievement Data', 'lifterlms' ), array( 'LLMS_Privacy_Erasers', 'achievement_data' ) );\n\t\t$this->add_eraser( 'lifterlms-certificate-data', __( 'Order Data', 'lifterlms' ), array( 'LLMS_Privacy_Erasers', 'certificate_data' ) );\n\t\t$this->add_eraser( 'lifterlms-notification-data', __( 'Notification Data', 'lifterlms' ), array( 'LLMS_Privacy_Erasers', 'notification_data' ) );\n\t\t// This eraser should always be last because some of the items above rely on postmeta data to function.\n\t\t$this->add_eraser( 'lifterlms-postmeta-data', __( 'Postmeta Data', 'lifterlms' ), array( 'LLMS_Privacy_Erasers', 'postmeta_data' ) );\n\n\t\t/**\n\t\t * Hooks\n\t\t */\n\t\t// Add individual cert HTML files to the export directory.\n\t\tadd_action( 'wp_privacy_personal_data_export_file_created', array( 'LLMS_Privacy_Exporters', 'maybe_add_export_files' ), 100, 4 );\n\n\t\t// Anonymize erased order properties.\n\t\tadd_filter( 'llms_privacy_get_anon_prop_value', array( 'LLMS_Privacy_Erasers', 'anonymize_prop' ), 10, 3 );\n\t}\n\n\t/**\n\t * Anonymize a property value\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param string $prop Property name.\n\t * @param object $obj  Associated object (if any).\n\t * @return string\n\t */\n\tpublic static function get_anon_prop_value( $prop, $obj = null ) {\n\t\treturn apply_filters( 'llms_privacy_get_anon_prop_value', '', $prop, $obj );\n\t}\n\n\t/**\n\t * Retrieve an array of student data properties which should be exported & erased\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param string $type Request type [export|erasure].\n\t * @return array\n\t */\n\tprotected static function get_order_data_props( $type ) {\n\n\t\t$props = array();\n\n\t\t// don't erase these fields, only export them\n\t\tif ( 'export' === $type ) {\n\t\t\t$props = array(\n\t\t\t\t'id'            => __( 'Order Number', 'lifterlms' ),\n\t\t\t\t'date'          => __( 'Order Date', 'lifterlms' ),\n\t\t\t\t'product_title' => __( 'Product', 'lifterlms' ),\n\t\t\t\t'plan_title'    => __( 'Plan', 'lifterlms' ),\n\t\t\t);\n\t\t} elseif ( 'erasure' === $type ) {\n\t\t\t$props = array(\n\t\t\t\t'user_id' => __( 'User ID', 'lifterlms' ),\n\t\t\t);\n\t\t}\n\n\t\t$props = array_merge(\n\t\t\t$props,\n\t\t\tarray(\n\t\t\t\t'billing_first_name' => __( 'Billing First Name', 'lifterlms' ),\n\t\t\t\t'billing_last_name'  => __( 'Billing Last Name', 'lifterlms' ),\n\t\t\t\t'billing_email'      => __( 'Billing Email', 'lifterlms' ),\n\t\t\t\t'billing_address_1'  => __( 'Billing Address 1', 'lifterlms' ),\n\t\t\t\t'billing_address_2'  => __( 'Billing Address 2', 'lifterlms' ),\n\t\t\t\t'billing_city'       => __( 'Billing City', 'lifterlms' ),\n\t\t\t\t'billing_state'      => __( 'Billing State', 'lifterlms' ),\n\t\t\t\t'billing_zip'        => __( 'Billing Zip Code', 'lifterlms' ),\n\t\t\t\t'billing_country'    => __( 'Billing Country', 'lifterlms' ),\n\t\t\t\t'billing_phone'      => __( 'Phone', 'lifterlms' ),\n\t\t\t\t'user_ip_address'    => __( 'IP Address', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\treturn apply_filters( 'llms_privacy_order_data_props', $props, $type );\n\t}\n\n\t/**\n\t * Get the privacy message sample content\n\t *\n\t * This stub can be overloaded.\n\t *\n\t * @since 3.18.0\n\t * @since 3.37.9 Replaced deprecated `.wp-policy-help` class with `.privacy-policy-tutorial`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_privacy_message() {\n\t\t$content = '\n\t\t\t<div class=\"wp-suggested-text\">' .\n\t\t\t\t'<p class=\"privacy-policy-tutorial\">' .\n\t\t\t\t\t__( 'This sample language includes the basics around what personal data your learning platform may be collecting, storing and sharing, as well as who may have access to that data. Depending on what settings are enabled and which additional add-ons are used, the specific information shared by your site will vary. We recommend consulting with a lawyer when deciding what information to disclose on your privacy policy.', 'lifterlms' ) .\n\t\t\t\t'</p>' .\n\t\t\t\t'<p>' . __( 'We collect information about you during the registration, enrollment, and checkout processes on our site.', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<h2>' . __( 'What we collect and store', 'lifterlms' ) . '</h2>' .\n\t\t\t\t'<p>' . __( 'When you register an account with us, we’ll ask you to provide information including your name, billing address, email address, phone number, credit card/payment details and optional account information like username and password. We’ll use this information for purposes, such as, to:', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<ul>' .\n\t\t\t\t\t'<li>' . __( 'Send you information about your account, orders, courses, and memberships', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Communicate with you about courses and memberships that you’re enrolled in', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Respond to your requests, including refunds and complaints', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Process payments and prevent fraud', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Set up your account for our site', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Comply with any legal obligations we have', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Improve our site’s offerings', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Send you marketing messages, if you choose to receive them', 'lifterlms' ) . '</li>' .\n\t\t\t\t'</ul>' .\n\t\t\t\t'<p>' . __( 'When you create an account, we will store your name, address, email and phone number, which will be used to populate the enrollment and checkout for future purchases and enrollments.', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<p>' . __( 'We generally store information about you for as long as we need the information for the purposes for which we collect and use it, and we are not legally required to continue to keep it. For example, we will store order information for XXX years for tax and accounting purposes. This includes your name, email address and billing address.', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<p>' . __( 'We will also store comments or reviews, if you chose to leave them.', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<h2>' . __( 'Who on our team has access', 'lifterlms' ) . '</h2>' .\n\t\t\t\t'<p>' . __( 'Members of our team have access to the information you provide us. For example, both Administrators and Site Managers can access:', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<ul>' .\n\t\t\t\t\t'<li>' . __( 'Order information like what was purchased, when it was purchased and where it should be sent, and', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Customer information like your name, email address, and billing information.', 'lifterlms' ) . '</li>' .\n\t\t\t\t'</ul>' .\n\t\t\t\t'<p>' . __( 'Course and membership instructors can access your course progress and activities including:', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<ul>' .\n\t\t\t\t\t'<li>' . __( 'Enrollment dates for their courses and memberships', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Course progress and status information for their courses', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Quiz and assignments answers and grades for their courses', 'lifterlms' ) . '</li>' .\n\t\t\t\t\t'<li>' . __( 'Comments and reviews made on their memberships and courses', 'lifterlms' ) . '</li>' .\n\t\t\t\t'</ul>' .\n\t\t\t\t'<p>' . __( 'Our team members have access to this information to help fulfill orders, process refunds, and support you.', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<h2>' . __( 'What we share with others', 'lifterlms' ) . '</h2>' .\n\t\t\t\t'<p class=\"privacy-policy-tutorial\">' . __( 'In this section you should list who you’re sharing data with, and for what purpose. This could include, but may not be limited to, analytics, marketing, payment gateways, and third party embeds.', 'lifterlms' ) . '</p>' .\n\t\t\t\t'<p>' . __( 'We share information with third parties who help us provide our orders and store services to you; for example --', 'lifterlms' ) . '</p>' .\n\t\t\t'</div>';\n\n\t\t/**\n\t\t * Customize the default privacy policy content provided by LifterLMS.\n\t\t *\n\t\t * @since 3.18.0\n\t\t *\n\t\t * @param string $content Privacy policy content as an html string.\n\t\t */\n\t\treturn apply_filters( 'llms_privacy_policy_content', $content );\n\t}\n\n\t/**\n\t * Retrieve student achievements.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Updated the use of `LLMS_Student::get_achievements()` with its new behavior.\n\t *\n\t * @param LLMS_Student $student Student object.\n\t * @return LLMS_User_Achievement[]\n\t */\n\tprotected static function get_student_achievements( $student ) {\n\n\t\t$query = $student->get_achievements( array( 'sort' => array( 'date' => 'DESC' ) ) );\n\n\t\treturn $query->get_awards();\n\t}\n\n\t/**\n\t * Retrieve student certificates.\n\t *\n\t * @since 3.18.0\n\t * @since 6.0.0 Updated the use of `LLMS_Student::get_certificates()` with its new behavior.\n\t *\n\t * @param LLMS_Student $student Student object.\n\t * @return LLMS_User_Certificate[]\n\t */\n\tprotected static function get_student_certificates( $student ) {\n\n\t\t$query = $student->get_certificates( array( 'sort' => array( 'date' => 'DESC' ) ) );\n\n\t\treturn $query->get_awards();\n\t}\n\n\t/**\n\t * Retrieve an array of student data properties which should be exported & erased\n\t *\n\t * @since 3.18.0\n\t *\n\t * @return array\n\t */\n\tprotected static function get_student_data_props() {\n\n\t\treturn apply_filters(\n\t\t\t'llms_privacy_get_student_data_props',\n\t\t\tarray(\n\t\t\t\t'billing_address_1' => __( 'Billing Address 1', 'lifterlms' ),\n\t\t\t\t'billing_address_2' => __( 'Billing Address 2', 'lifterlms' ),\n\t\t\t\t'billing_city'      => __( 'Billing City', 'lifterlms' ),\n\t\t\t\t'billing_state'     => __( 'Billing State', 'lifterlms' ),\n\t\t\t\t'billing_zip'       => __( 'Billing Zip Code', 'lifterlms' ),\n\t\t\t\t'billing_country'   => __( 'Billing Country', 'lifterlms' ),\n\t\t\t\t'phone'             => __( 'Phone', 'lifterlms' ),\n\t\t\t\t'ip_address'        => __( 'IP Address', 'lifterlms' ),\n\t\t\t\t'last_login'        => __( 'Last Login Date', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve student course & membership enrollment data\n\t *\n\t * @since    3.18.0\n\t *\n\t * @param LLMS_Student $student    Student object.\n\t * @param int          $page       Page number.\n\t * @param string       $post_type  WP Post type (course/membership).\n\t * @return   array\n\t */\n\tprotected static function get_student_enrollments( $student, $page, $post_type ) {\n\n\t\t$limit = 250;\n\n\t\t$enrollments = $student->get_enrollments(\n\t\t\t$post_type,\n\t\t\tarray(\n\t\t\t\t'limit' => $limit,\n\t\t\t\t'skip'  => ( $page - 1 ) * $limit,\n\t\t\t)\n\t\t);\n\n\t\treturn array(\n\t\t\t'done'    => ( ! $enrollments['more'] ),\n\t\t\t'results' => $enrollments['results'],\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve student orders\n\t *\n\t * @since 3.18.0\n\t *\n\t * @param LLMS_Student $student Student object.\n\t * @param int          $page    Page number.\n\t * @return array\n\t */\n\tprotected static function get_student_orders( $student, $page ) {\n\n\t\t$done    = true;\n\t\t$results = array();\n\n\t\t$orders = $student->get_orders(\n\t\t\tarray(\n\t\t\t\t'count' => 250,\n\t\t\t\t'page'  => $page,\n\t\t\t)\n\t\t);\n\t\tif ( $orders && $orders['pages'] ) {\n\t\t\t$results = $orders['orders'];\n\t\t\t$done    = ( absint( $page ) === absint( $orders['pages'] ) );\n\t\t}\n\n\t\treturn array(\n\t\t\t'done'   => $done,\n\t\t\t'orders' => $results,\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve student quizzes.\n\t *\n\t * @since  3.18.0\n\t *\n\t * @param LLMS_Student $student Student object.\n\t * @param int          $page    Page number.\n\t * @return LLMS_Query_Quiz_Attempt\n\t */\n\tprotected static function get_student_quizzes( $student, $page ) {\n\n\t\treturn new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'page'       => $page,\n\t\t\t\t'per_page'   => 500,\n\t\t\t\t'quiz_id'    => array(),\n\t\t\t\t'student_id' => $student->get( 'id' ),\n\t\t\t)\n\t\t);\n\t}\n}\n\nfunction llms_load_privacy() {\n\treturn new LLMS_Privacy();\n}\nadd_action( 'init', 'llms_load_privacy' );\n"
  },
  {
    "path": "includes/privacy/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/processors/class-llms-processor-achievement-sync.php",
    "content": "<?php\n/**\n * LLMS_Processor_Achievement_Sync class\n *\n * @package LifterLMS/Processors/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Processor: Sync awarded achievements to their achievement template.\n *\n * @since 6.0.0\n */\nclass LLMS_Processor_Achievement_Sync extends LLMS_Abstract_Processor_User_Engagement_Sync {\n\n\t/**\n\t * The type of the user engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $engagement_type = 'achievement';\n\n\t/**\n\t * Unique identifier for the processor.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'awarded_achievements_bulk_sync';\n\n\t/**\n\t * WP Cron Hook for scheduling the background process.\n\t *\n\t * @var string\n\t */\n\tprotected $schedule_hook = 'llms_awarded_achievements_bulk_sync';\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Processor_User_Engagement_Sync::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\t$engagement_template_id = $variables['engagement_template_id'] ?? 0;\n\n\t\tswitch ( $text_type ) {\n\t\t\tcase self::TEXT_SYNC_NOTICE_ALREADY_SCHEDULED:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the achievement template, %2$s: achievement template name, #%3$d: achievement template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'Awarded achievements sync already scheduled for the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_NOTICE_AWARDED_ENGAGEMENTS_COMPLETE:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the achievement template, %2$s: achievement template name, %3$d: achievement template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'Awarded achievements sync completed for the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . $this->get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_NOTICE_NO_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the achievement template, %2$s: achievement template name, #%3$d: achievement template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'There are no awarded achievements to sync with the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_NOTICE_SCHEDULED:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the achievement template, %2$s: achievement template name, #%3$d: achievement template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'Awarded achievements sync scheduled for the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tdefault:\n\t\t\t\treturn parent::get_text( $text_type );\n\t\t}\n\t}\n}\n\nreturn new LLMS_Processor_Achievement_Sync();\n"
  },
  {
    "path": "includes/processors/class-llms-processor-certificate-sync.php",
    "content": "<?php\n/**\n * LLMS_Processor_Certificate_Sync class\n *\n * @package LifterLMS/Processors/Classes\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Processor: Sync awarded certificates to their certificate template.\n *\n * @since 6.0.0\n */\nclass LLMS_Processor_Certificate_Sync extends LLMS_Abstract_Processor_User_Engagement_Sync {\n\n\t/**\n\t * The type of the user engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $engagement_type = 'certificate';\n\n\t/**\n\t * Unique identifier for the processor.\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'awarded_certificates_bulk_sync';\n\n\t/**\n\t * WP Cron Hook for scheduling the background process.\n\t *\n\t * @var string\n\t */\n\tprotected $schedule_hook = 'llms_awarded_certificates_bulk_sync';\n\n\t/**\n\t * Returns a translated text of the given type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int   $text_type One of the LLMS_Abstract_Processor_User_Engagement_Sync::TEXT_ constants.\n\t * @param array $variables Optional variables that are used in sprintf().\n\t * @return string\n\t */\n\tprotected function get_text( $text_type, $variables = array() ) {\n\n\t\t$engagement_template_id = $variables['engagement_template_id'] ?? 0;\n\n\t\tswitch ( $text_type ) {\n\t\t\tcase self::TEXT_SYNC_NOTICE_ALREADY_SCHEDULED:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the certificate template, %2$s: certificate template name, #%3$d: certificate template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'Awarded certificates sync already scheduled for the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_NOTICE_AWARDED_ENGAGEMENTS_COMPLETE:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the certificate template, %2$s: certificate template name, %3$d: certificate template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'Awarded certificates sync completed for the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . $this->get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_NOTICE_NO_AWARDED_ENGAGEMENTS:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the certificate template, %2$s: certificate template name, #%3$d: certificate template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'There are no awarded certificates to sync with the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tcase self::TEXT_SYNC_NOTICE_SCHEDULED:\n\t\t\t\treturn sprintf(\n\t\t\t\t\t/* translators: %1$s: opening anchor tag that links to the certificate template, %2$s: certificate template name, #%3$d: certificate template ID, %4$s: closing anchor tag */\n\t\t\t\t\t__( 'Awarded certificates sync scheduled for the template %1$s%2$s (#%3$d)%4$s.', 'lifterlms' ),\n\t\t\t\t\t'<a href=\"' . get_edit_post_link( $engagement_template_id ) . '\" target=\"_blank\">',\n\t\t\t\t\tget_the_title( $engagement_template_id ),\n\t\t\t\t\t$engagement_template_id,\n\t\t\t\t\t'</a>'\n\t\t\t\t);\n\t\t\tdefault:\n\t\t\t\treturn parent::get_text( $text_type );\n\t\t}\n\t}\n}\n\nreturn new LLMS_Processor_Certificate_Sync();\n"
  },
  {
    "path": "includes/processors/class.llms.processor.course.data.php",
    "content": "<?php\n/**\n * Processor: Course Data\n *\n * @package LifterLMS/Processors/Classes\n *\n * @since 3.15.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Processor_Course_Data\n *\n * Handle background processing of average progress & average grade for courses.\n *\n * The background process calculates \"expensive\" aggregate course data and stores them\n * on the `wp_postmeta` table so the data can be access later with a single\n * database read.\n *\n * The process is queued for recalculation when:\n *\n *   + Students enroll.\n *   + Students unenroll.\n *   + Students complete lessons.\n *   + Students complete quizzes.\n *\n * Upon completion, the following values can be accessed via the `LLMS_Course` model\n * to retrieve the aggregate data for the course:\n *\n *   + Average grade: `LLMS_Course::get( 'average_grade' )`\n *   + Average progress: `LLMS_Course::get( 'average_progress' )`\n *   + Number of currently enrolled students: `LLMS_Course::get( 'enrolled_students' )`\n *\n * @since 3.15.0\n * @since 4.12.0 Remove (protected) method `LLMS_Processor_Course_Data::complete()`, the override of the parent method is no longer needed.\n */\nclass LLMS_Processor_Course_Data extends LLMS_Abstract_Processor {\n\n\t/**\n\t * Unique identifier for the processor\n\t *\n\t * @var string\n\t */\n\tprotected $id = 'course_data';\n\n\t/**\n\t * WP Cron Hook for scheduling the bg process\n\t *\n\t * @var string\n\t */\n\tprivate $schedule_hook = 'llms_calculate_course_data';\n\n\t/**\n\t * Maximum number of students allowed in a course\n\t *\n\t * When enrollment is higher than this number\n\t * throttling the calculations will be delayed.\n\t *\n\t * @var int\n\t */\n\tprivate $throttle_max_students;\n\n\t/**\n\t * Frequency of calculation process when the process is throttled.\n\t *\n\t * @var int\n\t */\n\tprivate $throttle_frequency;\n\n\t/**\n\t * Action triggered to queue queries needed to make the calculation\n\t *\n\t * @since 3.15.0\n\t * @since 4.12.0 Add throttling by course in progress and adjust last_run calculation to be specific to the course.\n\t *               Improve performance of the student query by removing unneeded sort columns.\n\t * @since 4.21.0 When there's no students found in the course, run the `task_complete()` method to ensure data\n\t *               from a previous calculation is cleared.\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @param int $course_id WP Post ID of the course.\n\t * @return void|null\n\t */\n\tpublic function dispatch_calc( $course_id ) {\n\t\t$this->log( sprintf( 'Course data calculation dispatched for course %d.', $course_id ) );\n\n\t\t// Make sure we have a course.\n\t\t$course = llms_get_post( $course_id );\n\t\tif ( ! $course instanceof LLMS_Course ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Return early if we're already processing data for the given course.\n\t\tif ( $this->is_already_processing_course( $course_id ) ) {\n\t\t\treturn $this->dispatch_calc_throttled( $course_id );\n\t\t}\n\n\t\t// Retrieve args.\n\t\t$args = $this->get_student_query_args( $course_id );\n\n\t\t// Get the results for the current set of arguments.\n\t\t$query = new LLMS_Student_Query( $args );\n\n\t\t// No students in the course, run task completion.\n\t\tif ( ! $query->get_number_results() ) {\n\t\t\treturn $this->task_complete( $course, $this->get_task_data(), true );\n\t\t}\n\n\t\t// Get the total number of students as a separate query since deprecating SQL_CALC_FOUND_ROWS usage.\n\t\t$count_query = new LLMS_Student_Query( $this->get_student_count_query_from_args( $args ) );\n\t\tif ( ! $count_query->get_count_only_result() ) {\n\t\t\treturn $this->task_complete( $course, $this->get_task_data(), true );\n\t\t}\n\t\t$course->set( 'enrolled_students', $count_query->get_count_only_result() );\n\n\t\t// Throttle processing.\n\t\tif ( $this->maybe_throttle( $count_query->get_count_only_result(), $course_id ) ) {\n\t\t\treturn $this->dispatch_calc_throttled( $course_id );\n\t\t}\n\n\t\t// Add each page to the queue.\n\t\t$max_pages = absint( ceil( $count_query->get_count_only_result() / $args['per_page'] ) );\n\t\t// Pass in the max pages as an argument to the task so we don't need to run the count query each time.\n\t\t$args['max_pages'] = $max_pages;\n\t\twhile ( $args['page'] <= $max_pages ) {\n\t\t\t$this->push_to_queue( $args );\n\t\t\t++$args['page'];\n\t\t}\n\n\t\t// Save queue and dispatch the process.\n\t\t$this->save()->dispatch();\n\t}\n\n\t/**\n\t * Schedule data calculation for the future\n\t *\n\t * This method is called when data processing is triggered for a course that is currently being processed\n\t * or for a course that qualifies for process throttling based on the number of students in the course.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @param int $course_id WP_Post ID of the course.\n\t * @return void\n\t */\n\tprotected function dispatch_calc_throttled( $course_id ) {\n\n\t\t$this->schedule_calculation( $course_id, time() + $this->throttle_frequency );\n\t\t$this->log( sprintf( 'Course data calculation throttled for course %d.', $course_id ) );\n\t}\n\n\t/**\n\t * Modify the query arguments for calculating the total count.\n\t *\n\t * @param $args\n\t *\n\t * @return array Array of arguments passed to an LLMS_Student_Query for calculating the student count.\n\t */\n\tprotected function get_student_count_query_from_args( $args ) {\n\t\t$count_args = array_merge( $args, array( 'count_only' => true ) );\n\n\t\treturn $count_args;\n\t}\n\n\t/**\n\t * Retrieve arguments used to perform an LLMS_Student_Query for background data processing\n\t *\n\t * @since 4.12.0\n\t *\n\t * @param int $course_id WP_Post ID of the course.\n\t * @return array Array of arguments passed to an LLMS_Student_Query.\n\t */\n\tprotected function get_student_query_args( $course_id ) {\n\n\t\t/**\n\t\t * Filter the query arguments used when calculating course data\n\t\t *\n\t\t * @since 4.12.0\n\t\t *\n\t\t * @param array                      $args      Query arguments passed to LLMS_Student_Query.\n\t\t * @param LLMS_Processor_Course_Data $processor Instance of the data processor class.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_data_processor_course_data_student_query_args',\n\t\t\tarray(\n\t\t\t\t'post_id'       => $course_id,\n\t\t\t\t'statuses'      => array( 'enrolled' ),\n\t\t\t\t'page'          => 1,\n\t\t\t\t'per_page'      => 100,\n\t\t\t\t'sort'          => array(\n\t\t\t\t\t'id' => 'ASC',\n\t\t\t\t),\n\t\t\t\t'no_found_rows' => true,\n\t\t\t),\n\t\t\t$this\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve a timestamp for the last time data calculation was completed for a given course\n\t *\n\t * @since 4.12.0\n\t *\n\t * @param int $course_id WP_Post ID of the course.\n\t * @return int The timestamp of the last run. Returns `0` when no data recorded.\n\t */\n\tprotected function get_last_run( $course_id ) {\n\t\treturn absint( get_post_meta( $course_id, '_llms_last_data_calc_run', true ) );\n\t}\n\n\t/**\n\t * Retrieve structured task data array.\n\t *\n\t * Ensures the expected required array keys are found on the task array\n\t * and optionally merges in an existing array of day with the (empty) defaults.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param array $data Existing array of day (from a previous task).\n\t * @return array\n\t */\n\tprotected function get_task_data( $data = array() ) {\n\t\treturn wp_parse_args(\n\t\t\t$data,\n\t\t\tarray(\n\t\t\t\t'students' => 0,\n\t\t\t\t'progress' => 0,\n\t\t\t\t'quizzes'  => 0,\n\t\t\t\t'grade'    => 0,\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Initializer\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return void\n\t */\n\tprotected function init() {\n\n\t\t// For the cron.\n\t\tadd_action( $this->schedule_hook, array( $this, 'dispatch_calc' ), 10, 1 );\n\n\t\t// For LifterLMS actions which trigger recalculation.\n\t\t$this->actions = array(\n\t\t\t'llms_course_calculate_data'    => array(\n\t\t\t\t'arguments' => 1,\n\t\t\t\t'callback'  => 'schedule_calculation',\n\t\t\t\t'priority'  => 10,\n\t\t\t),\n\t\t\t'llms_user_enrolled_in_course'  => array(\n\t\t\t\t'arguments' => 2,\n\t\t\t\t'callback'  => 'schedule_from_course',\n\t\t\t\t'priority'  => 10,\n\t\t\t),\n\t\t\t'llms_user_removed_from_course' => array(\n\t\t\t\t'arguments' => 2,\n\t\t\t\t'callback'  => 'schedule_from_course',\n\t\t\t\t'priority'  => 10,\n\t\t\t),\n\t\t\t'lifterlms_lesson_completed'    => array(\n\t\t\t\t'arguments' => 2,\n\t\t\t\t'callback'  => 'schedule_from_lesson',\n\t\t\t\t'priority'  => 10,\n\t\t\t),\n\t\t\t'lifterlms_quiz_completed'      => array(\n\t\t\t\t'arguments' => 3,\n\t\t\t\t'callback'  => 'schedule_from_quiz',\n\t\t\t\t'priority'  => 10,\n\t\t\t),\n\t\t);\n\n\t\t/**\n\t\t * Throttles course data processing based on the number of a students in a course.\n\t\t *\n\t\t * If the number of students in a course is greater than or equal to this number, the background\n\t\t * process will be throttled to run only once every N hours where N is equal to the number of hours\n\t\t * defined by the `llms_data_processor_course_data_throttle_frequency` filter.\n\t\t *\n\t\t * @since 3.15.0\n\t\t * @since 4.12.0 Reduced default value of `$number_students` from 2500 to 500.\n\t\t *\n\t\t * @see llms_data_processor_course_data_throttle_frequency\n\t\t *\n\t\t * @param int                        $number_students The number of students. Default is `500`.\n\t\t * @param LLMS_Processor_Course_Data $processor       Instance of the data processor class.\n\t\t */\n\t\t$this->throttle_max_students = apply_filters( 'llms_data_processor_course_data_throttle_count', 500, $this );\n\n\t\t/**\n\t\t * Frequency to run the processor for a given course when processing is throttled\n\t\t *\n\t\t * @since 3.15.0\n\t\t *\n\t\t * @see llms_data_processor_course_data_throttle_count\n\t\t *\n\t\t * @param int                        $frequency Frequency of the calculation process in seconds. Default `HOUR_IN_SECONDS * 4`.\n\t\t * @param LLMS_Processor_Course_Data $processor Instance of the data processor class.\n\t\t */\n\t\t$this->throttle_frequency = apply_filters( 'llms_data_processor_course_data_throttle_frequency', HOUR_IN_SECONDS * 4, $this );\n\t}\n\n\t/**\n\t * Determines if the supplied course is already being processed.\n\t *\n\t * If it's already being processed we'll throttle the processing so we'll wait until the course\n\t * completes its current data processing and start again later.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @param int $course_id WP_Post ID of the course.\n\t * @return boolean\n\t */\n\tprotected function is_already_processing_course( $course_id ) {\n\t\treturn llms_parse_bool( get_post_meta( $course_id, '_llms_temp_calc_data_lock', true ) );\n\t}\n\n\t/**\n\t * For large courses, only recalculate once every 4 hours\n\t *\n\t * @since 3.15.0\n\t * @since 4.12.0 Adjusted access from private to protected.\n\t *               Pull last run data on a per-course basis.\n\t *               Added parameter `$course_id`.\n\t *\n\t * @param int $num_students Number of students in the current course.\n\t * @param int $course_id    WP_Post ID of the course.\n\t * @return boolean When `true` the dispatch is throttled and when `false` it will run.\n\t */\n\tprotected function maybe_throttle( $num_students, $course_id ) {\n\n\t\t$throttled = false;\n\n\t\tif ( $num_students >= $this->throttle_max_students ) {\n\n\t\t\t$throttled = ( time() - $this->get_last_run( $course_id ) <= $this->throttle_frequency );\n\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not data processing is throttled for a request\n\t\t *\n\t\t * @since 4.12.0\n\t\t *\n\t\t * @param boolean $throttled    If `true`, the processing for the current request is throttled, otherwise data processing will begin.\n\t\t * @param int     $num_students Number of students in the current course.\n\t\t * @param int     $course_id    WP_Post ID of the course.\n\t\t * $param int     $max_students Maximum number of students in the course before processing is throttled.\n\t\t */\n\t\treturn apply_filters( 'llms_data_processor_course_data_throttled', $throttled, $num_students, $course_id, $this->throttle_max_students );\n\t}\n\n\t/**\n\t * Schedule recalculation from actions triggered against a course\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param int $user_id   WP user id of the student.\n\t * @param int $course_id WP Post ID of the course.\n\t * @return void\n\t */\n\tpublic function schedule_from_course( $user_id, $course_id ) {\n\t\t$this->schedule_calculation( $course_id );\n\t}\n\n\t/**\n\t * Schedule recalculation from actions triggered against a lesson\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param int $user_id   WP user id of the student.\n\t * @param int $lesson_id WP Post ID of the lesson.\n\t * @return void\n\t */\n\tpublic function schedule_from_lesson( $user_id, $lesson_id ) {\n\t\t$lesson = llms_get_post( $lesson_id );\n\t\t$this->schedule_calculation( $lesson->get( 'parent_course' ) );\n\t}\n\n\t/**\n\t * Schedule recalculation from actions triggered against a quiz\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param int               $user_id WP user id of the student.\n\t * @param int               $quiz_id WP Post ID of the quiz.\n\t * @param LLMS_Quiz_Attempt $attempt Quiz attempt object.\n\t * @return void\n\t */\n\tpublic function schedule_from_quiz( $user_id, $quiz_id, $attempt ) {\n\t\t$this->schedule_from_lesson( $user_id, $attempt->get( 'lesson_id' ) );\n\t}\n\n\t/**\n\t * Schedule a calculation to execute\n\t *\n\t * This will schedule an event that will setup the queue of items for the background process.\n\t *\n\t * @since 3.15.0\n\t * @since 4.21.0 Force `$course_id` to an absolute integer to avoid duplicate scheduling resulting from loose variable typing.\n\t *\n\t * @param int $course_id WP Post ID of the course.\n\t * @param int $time      Optionally pass a timestamp for when the event should be run.\n\t * @return void\n\t */\n\tpublic function schedule_calculation( $course_id, $time = null ) {\n\n\t\t$course_id = absint( $course_id );\n\n\t\t$this->log( sprintf( 'Course data calculation triggered for course %d.', $course_id ) );\n\n\t\t$args = array( $course_id );\n\n\t\tif ( ! wp_next_scheduled( $this->schedule_hook, $args ) ) {\n\n\t\t\t$time = ! $time ? time() : $time;\n\n\t\t\twp_schedule_single_event( $time, $this->schedule_hook, $args );\n\t\t\t$this->log( sprintf( 'Course data calculation scheduled for course %d.', $course_id ) );\n\n\t\t}\n\t}\n\n\n\t/**\n\t * Execute calculation for each item in the queue until all students in the course have been polled\n\t *\n\t * Stores the data in the postmeta table to be accessible via LLMS_Course.\n\t *\n\t * @since 3.15.0\n\t * @since 4.12.0 Moved task completion logic to `task_complete()`.\n\t * @since 4.16.0 Fix log string to properly record the post_id.\n\t * @since 4.21.0 Use `get_task_data()` to merge/retrieve aggregate task data.\n\t *               Return early for non-courses.\n\t *\n\t * @param array $args Query arguments passed to LLMS_Student_Query.\n\t * @return boolean Always returns `false` to remove the item from the queue when processing is complete.\n\t */\n\tpublic function task( $args ) {\n\n\t\t$this->log( sprintf( 'Course data calculation task called for course %1$d with args: %2$s', $args['post_id'], wp_json_encode( $args ) ) );\n\n\t\t$course = llms_get_post( $args['post_id'] );\n\n\t\t// Only process existing courses.\n\t\tif ( ! $course instanceof LLMS_Course ) {\n\t\t\t$this->log( sprintf( 'Course data calculation task skipped for course %1$d.', $args['post_id'] ) );\n\t\t\treturn false;\n\t\t}\n\n\t\t// Lock the course against duplicate processing.\n\t\t$course->set( 'temp_calc_data_lock', 'yes' );\n\n\t\t// Get saved data or empty array when on first page.\n\t\t$data = ( 1 !== $args['page'] ) ? $course->get( 'temp_calc_data' ) : array();\n\n\t\t// Merge with the defaults.\n\t\t$data = $this->get_task_data( $data );\n\n\t\t/**\n\t\t * Save the max number of pages value passed in when the tasks were pushed to the queue.\n\t\t *\n\t\t * If a task was dispatched without it, set the max pages to 0 so we can try to calculate the count\n\t\t * using a new count-only query.\n\t\t */\n\t\t$max_pages = isset( $args['max_pages'] ) ? $args['max_pages'] : 0;\n\t\tunset( $args['max_pages'] );\n\n\t\t// Perform the query.\n\t\t$query = new LLMS_Student_Query( $args );\n\n\t\tforeach ( $query->get_students() as $student ) {\n\n\t\t\t// Progress, all students counted here.\n\t\t\t++$data['students'];\n\t\t\t$data['progress'] = $data['progress'] + $student->get_progress( $args['post_id'] );\n\n\t\t\t// Grades only counted when a student has taken a quiz.\n\t\t\t// If a student hasn't taken it, we don't count it as a 0 on the quiz.\n\t\t\t$grade = $student->get_grade( $args['post_id'] );\n\n\t\t\t// Only check actual quiz grades.\n\t\t\tif ( is_numeric( $grade ) ) {\n\t\t\t\t++$data['quizzes'];\n\t\t\t\t$data['grade'] = $data['grade'] + $grade;\n\t\t\t}\n\t\t}\n\n\t\tif ( $max_pages === 0 ) {\n\t\t\t$count_query = new LLMS_Student_Query( $this->get_student_count_query_from_args( $args ) );\n\t\t\tif ( ! $count_query->get_count_only_result() ) {\n\t\t\t\t// End processing when we can't get the count.\n\t\t\t\t$is_last_page = true;\n\t\t\t\treturn $this->task_complete( $course, $data, $is_last_page );\n\t\t\t}\n\n\t\t\t$max_pages = absint( ceil( $count_query->get_count_only_result() / $args['per_page'] ) );\n\t\t}\n\t\t$is_last_page = ( absint( $max_pages ) === absint( $args['page'] ) );\n\n\t\treturn $this->task_complete( $course, $data, $is_last_page );\n\t}\n\n\t/**\n\t * Complete a task\n\t *\n\t * Stores the current (incomplete) array of course data on the postmeta table for use\n\t * by the next task in the queue.\n\t *\n\t * Upon completion, uses the data array to calculate the final aggregate values and store\n\t * them on the postmeta table for the course for quick retrieval later.\n\t *\n\t * @since 4.12.0\n\t * @since 4.16.0 Fix log string to properly log the course id.\n\t *\n\t * @param LLMS_Course $course    Course object.\n\t * @param array       $data      Aggregate calculation data array.\n\t * @param boolean     $last_page Whether or not this is the last page set of students for the process.\n\t * @return boolean Always returns false.\n\t */\n\tprotected function task_complete( $course, $data, $last_page ) {\n\n\t\t$this->log( sprintf( 'Course data calculation task completed for course %1$d with data: %2$s', $course->get( 'id' ), wp_json_encode( $data ) ) );\n\n\t\t// Save our work on the last run.\n\t\tif ( $last_page ) {\n\n\t\t\t// Calculate.\n\t\t\t$grade    = $data['quizzes'] ? round( $data['grade'] / $data['quizzes'], 2 ) : 0;\n\t\t\t$progress = $data['students'] ? round( $data['progress'] / $data['students'], 2 ) : 0;\n\n\t\t\t// Save the data to the course.\n\t\t\t$course->set( 'average_grade', $grade );\n\t\t\t$course->set( 'average_progress', $progress );\n\t\t\t$course->set( 'enrolled_students', $data['students'] );\n\t\t\t$course->set( 'last_data_calc_run', time() );\n\n\t\t\t// Delete the temporary data so its fresh for next time.\n\t\t\tdelete_post_meta( $course->get( 'id' ), '_llms_temp_calc_data' );\n\n\t\t\t// Unlock the course.\n\t\t\tdelete_post_meta( $course->get( 'id' ), '_llms_temp_calc_data_lock' );\n\n\t\t\t$this->log( sprintf( 'Course data calculation completed for course %d.', $course->get( 'id' ) ) );\n\n\t\t} else {\n\n\t\t\t// Save temporary data so it can be used by the next run in the process.\n\t\t\t$course->set( 'temp_calc_data', $data );\n\n\t\t}\n\n\t\treturn false;\n\t}\n}\n\nreturn new LLMS_Processor_Course_Data();\n"
  },
  {
    "path": "includes/processors/class.llms.processor.membership.bulk.enroll.php",
    "content": "<?php\n/**\n * Processor: Membership Bulk Enroll\n *\n * @package LifterLMS/Processors/Classes\n *\n * @since 3.15.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Processor_Membership_Bulk_Enroll class\n *\n * Handle background processing of average progress & average grade for LifterLMS Courses.\n * This triggers a bg process which gets the current progress\n * of all students in a course.\n *\n * Progress is queued for recalculation when:\n *      students enroll\n *      students unenroll\n *      students complete lessons\n *\n * @since 3.15.0\n * @since 3.26.1 Unknown.\n */\nclass LLMS_Processor_Membership_Bulk_Enroll extends LLMS_Abstract_Processor {\n\n\t/**\n\t * Unique identifier for the processor\n\t *\n\t * @var  string\n\t */\n\tprotected $id = 'membership_bulk_enroll';\n\n\t/**\n\t * WP Cron Hook for scheduling the bg process\n\t *\n\t * @var  string\n\t */\n\tprivate $schedule_hook = 'llms_membership_bulk_enroll';\n\n\t/**\n\t * Action triggered to queue all students who need to be enrolled\n\t *\n\t * @since 3.15.0\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @param int $membership_id WP Post ID of the membership.\n\t * @param int $course_id     WP Post ID of the course to enroll members into.\n\t * @return void\n\t */\n\tpublic function dispatch_enrollment( $membership_id, $course_id ) {\n\n\t\t$this->log( sprintf( 'membership bulk enrollment dispatched for membership %1$d into course %2$d', $membership_id, $course_id ) );\n\n\t\t// cancel process in case it's currently running\n\t\t$this->cancel_process();\n\n\t\t$args = array(\n\t\t\t'post_id'  => $membership_id,\n\t\t\t'statuses' => 'enrolled',\n\t\t\t'page'     => 1,\n\t\t\t'per_page' => 250,\n\t\t);\n\n\t\t$query = new LLMS_Student_Query( $args );\n\n\t\tif ( $query->has_results() ) {\n\n\t\t\twhile ( $args['page'] <= $query->get_max_pages() ) {\n\n\t\t\t\t$this->push_to_queue(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'course_id'  => $course_id,\n\t\t\t\t\t\t'query_args' => $args,\n\t\t\t\t\t\t'trigger'    => sprintf( 'membership_%d', $membership_id ),\n\t\t\t\t\t)\n\t\t\t\t);\n\n\t\t\t\t$args['page']++;\n\n\t\t\t}\n\n\t\t\t$this->save()->dispatch();\n\n\t\t\t$this->log( sprintf( 'membership bulk enrollment started for membership %1$d into course %2$d', $membership_id, $course_id ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Initializer\n\t *\n\t * @return   void\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tprotected function init() {\n\n\t\t// for the cron\n\t\tadd_action( $this->schedule_hook, array( $this, 'dispatch_enrollment' ), 10, 2 );\n\n\t\t// for LifterLMS actions which trigger bulk enrollment\n\t\t$this->actions = array(\n\t\t\t'llms_membership_do_bulk_course_enrollment' => array(\n\t\t\t\t'arguments' => 2,\n\t\t\t\t'callback'  => 'schedule_enrollment',\n\t\t\t\t'priority'  => 10,\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Schedule bulk enrollment\n\t * This will schedule an event that will setup the queue of items for the background process\n\t *\n\t * @param    int $membership_id  WP Post ID of the membership\n\t * @param    int $course_id      WP Post ID of the course to enroll members into\n\t * @return   void\n\t * @since    3.15.0\n\t * @version  3.15.0\n\t */\n\tpublic function schedule_enrollment( $membership_id, $course_id ) {\n\n\t\t$this->log( sprintf( 'membership bulk enrollment triggered for membership %1$d into course %2$d', $membership_id, $course_id ) );\n\n\t\t$args = array( $membership_id, $course_id );\n\n\t\tif ( ! wp_next_scheduled( $this->schedule_hook, $args ) ) {\n\n\t\t\twp_schedule_single_event( time(), $this->schedule_hook, $args );\n\t\t\t$this->log( sprintf( 'membership bulk enrollment scheduled for membership %1$d into course %2$d', $membership_id, $course_id ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Execute calculation for each item in the queue until all students in the course have been polled.\n\t *\n\t * Stores the data in the postmeta table to be accessible via LLMS_Course.\n\t *\n\t * @since 3.15.0\n\t * @since 6.0.0 Replaced access of LLMS_Student_Query::$found_results protected property with LLMS_Student_Query::has_results().\n\t *\n\t * @param array $item Array of processing data.\n\t * @return boolean True to keep the item in the queue and process again.\n\t *                 False to remove the item from the queue.\n\t */\n\tpublic function task( $item ) {\n\n\t\t$this->log( sprintf( 'membership bulk enrollment task started for membership %1$d into course %2$d', $item['query_args']['post_id'], $item['course_id'] ) );\n\t\t$this->log( $item );\n\n\t\t// ensure the item has all the data we need to process it\n\t\tif ( ! is_array( $item ) || ! isset( $item['course_id'] ) || ! isset( $item['query_args'] ) || ! isset( $item['trigger'] ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// turn the course data processor off\n\t\t$course_data_processor = llms()->processors()->get( 'course_data' );\n\t\tif ( $course_data_processor ) {\n\t\t\t$course_data_processor->disable();\n\t\t}\n\n\t\t$query = new LLMS_Student_Query( $item['query_args'] );\n\n\t\tif ( $query->has_results() ) {\n\t\t\tforeach ( $query->get_students() as $student ) {\n\t\t\t\t$student->enroll( $item['course_id'], $item['trigger'] );\n\t\t\t}\n\t\t}\n\n\t\tif ( $query->is_last_page() ) {\n\n\t\t\t$this->log( sprintf( 'membership bulk enrollment completed for membership %1$d into course %2$d', $item['query_args']['post_id'], $item['course_id'] ) );\n\n\t\t\t// turn the course data processor back on\n\t\t\tif ( $course_data_processor ) {\n\t\t\t\t$course_data_processor->add_actions();\n\t\t\t}\n\n\t\t\t// process the course data\n\t\t\tdo_action( 'llms_course_calculate_data', $item['course_id'] );\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n}\n\nreturn new LLMS_Processor_Membership_Bulk_Enroll();\n"
  },
  {
    "path": "includes/processors/class.llms.processors.php",
    "content": "<?php\n/**\n * Processors.\n *\n * @package LifterLMS/Processors/Classes\n *\n * @since 3.15.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Processors class\n *\n * Load, access, and manage LifterLMS Processors\n *\n * @since 3.15.0\n * @since 5.0.0 Removed private method `includes()`.\n *              Stop loading removed processor \"table_to_csv\".\n * @since 5.3.0 Replace singleton code with `LLMS_Trait_Singleton`.\n * @since 6.0.0 Added the awarded certificates bulk sync processor.\n *              Removed the deprecated `LLMS_Processors::$_instance` property.\n */\nclass LLMS_Processors {\n\n\tuse LLMS_Trait_Singleton;\n\n\t/**\n\t * Processor classes that should be loaded\n\t *\n\t * This should match the classname of a processor.\n\t *\n\t * @var array\n\t */\n\tprivate $classes = array(\n\t\t'achievement_sync',\n\t\t'certificate_sync',\n\t\t'course_data',\n\t\t'membership_bulk_enroll',\n\t);\n\n\t/**\n\t * Array of available processors loaded via $this->load_all()\n\t *\n\t * @var LLMS_Abstract_Processor[]\n\t */\n\tprivate $processors = array();\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.15.0\n\t * @since 5.0.0 Remove call to removed method `includes()`.\n\t * @since 6.0.0 Made sure the admin notices file is required.\n\t *\n\t * @return void\n\t */\n\tprivate function __construct() {\n\n\t\t// Processors may trigger a notice during a cron and notices might not be available.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\t\t$this->load_all();\n\n\t}\n\n\t/**\n\t * Access a single loaded processor instance\n\t *\n\t * @since 3.15.0\n\t *\n\t * @param string $name Name of the processor.\n\t * @return LLMS_Abstract_Processor|false Instance of the processor if found, otherwise false.\n\t */\n\tpublic function get( $name ) {\n\n\t\tif ( isset( $this->processors[ $name ] ) ) {\n\t\t\treturn $this->processors[ $name ];\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Load all processors.\n\t *\n\t * @since 3.15.0\n\t * @since 5.8.0 Use the value from the `llms_load_processors` filter.\n\t *\n\t * @return void\n\t */\n\tprivate function load_all() {\n\n\t\t/**\n\t\t * Filter the list of available processors to be loaded.\n\t\t *\n\t\t * Third parties can use this filter to load custom processors.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @see llms_load_processor_path To add a custom load path for the loaded processor.\n\t\t *\n\t\t * @param string[] $classes A list of processor class ids/slugs.\n\t\t */\n\t\t$classes = apply_filters( 'llms_load_processors', $this->classes );\n\n\t\tforeach ( $classes as $name ) {\n\n\t\t\t$class = $this->load_processor( $name );\n\n\t\t\tif ( $class ) {\n\n\t\t\t\t$this->processors[ $name ] = $class;\n\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Load a single processor\n\t *\n\t * @since 3.15.0\n\t * @since 6.0.0 Added the ability to load processor class files with dashes in their file name.\n\t *\n\t * @param string $name Name of the processor.\n\t * @return LLMS_Abstract_Processor|boolean Instance of the processor if found and not yet included, `false` if\n\t *                                         the processor can't be found, and `true` if it has already been included.\n\t */\n\tpublic function load_processor( $name ) {\n\n\t\t/**\n\t\t * Filter the path of a processor class.\n\t\t *\n\t\t * If the returned path isn't the full path to a PHP file the file will be attempted to be\n\t\t * loaded from the LifterLMS core's processor directory by replacing underscores with dashes\n\t\t * and prepending `class-llms-processor-` and appending `.php`.\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @see LLMS_Processors::load_all() For the `llms_load_processors` filter used to register custom processors.\n\t\t *\n\t\t * @param string $name Processor class name ID/slug.\n\t\t */\n\t\t$path = apply_filters( 'llms_load_processor_path', $name );\n\n\t\t// Try loading the filtered processor path.\n\t\tif ( $path !== $name ) {\n\t\t\treturn file_exists( $name ) ? require_once $name : false;\n\t\t}\n\n\t\t$file = 'class-llms-processor-' . str_replace( '_', '-', $name ) . '.php';\n\t\t$path = LLMS_PLUGIN_DIR . 'includes/processors/';\n\n\t\t// Try loading a LifterLMS processor with a dashed file name.\n\t\tif ( file_exists( $path . $file ) ) {\n\t\t\treturn require_once $path . $file;\n\t\t}\n\n\t\t// Try loading a LifterLMS processor with a dotted file name.\n\t\t$file = str_replace( '-', '.', $file );\n\t\tif ( file_exists( $path . $file ) ) {\n\t\t\treturn require_once $path . $file;\n\t\t}\n\n\t\treturn false;\n\t}\n}\n"
  },
  {
    "path": "includes/processors/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "includes/schemas/index.php",
    "content": "<?php // No.\n"
  },
  {
    "path": "includes/schemas/llms-block-templates.php",
    "content": "<?php\n/**\n * Post type block templates.\n *\n * Returns an array of post type block types for use in post type registration.\n *\n * @package LifterLMS/Schemas\n *\n * @since 6.0.0\n * @version 7.3.0\n *\n * @see LLMS_Post_Types::get_template().\n * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-templates/\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $wp_version;\n\n$blocks_styles = array(\n\t'certificate' => array(\n\t\t'title'  => array(\n\t\t\t'style'     => array(\n\t\t\t\t'typography' => array(\n\t\t\t\t\t'fontSize'   => '90px',\n\t\t\t\t\t'lineHeight' => '1.1',\n\t\t\t\t),\n\t\t\t\t'spacing'    => array(\n\t\t\t\t\t'margin' => array(\n\t\t\t\t\t\t'top'    => '40px',\n\t\t\t\t\t\t'bottom' => '0px',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\t'textColor' => 'black',\n\t\t),\n\t\t'h2'     => array(\n\t\t\t'style'     => array(\n\t\t\t\t'typography' => array(\n\t\t\t\t\t'fontSize'   => '48px',\n\t\t\t\t\t'lineHeight' => '1.3',\n\t\t\t\t),\n\t\t\t\t'spacing'    => array(\n\t\t\t\t\t'margin' => array(\n\t\t\t\t\t\t'top'    => '0px',\n\t\t\t\t\t\t'bottom' => '0px',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\t'textColor' => 'black',\n\t\t),\n\t\t'h3'     => array(\n\t\t\t'style'     => array(\n\t\t\t\t'typography' => array(\n\t\t\t\t\t'fontSize'   => '32px',\n\t\t\t\t\t'lineHeight' => '1.3',\n\t\t\t\t),\n\t\t\t\t'spacing'    => array(\n\t\t\t\t\t'margin' => array(\n\t\t\t\t\t\t'top'    => '0px',\n\t\t\t\t\t\t'bottom' => '0px',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\t'textColor' => 'black',\n\t\t),\n\t\t'p'      => array(\n\t\t\t'style'     => array(\n\t\t\t\t'typography' => array(\n\t\t\t\t\t'fontSize'   => '18px',\n\t\t\t\t\t'lineHeight' => '1.6',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'textColor' => 'black',\n\t\t),\n\t\t'spacer' => array(\n\t\t\t'height' => version_compare( $wp_version, '6.3-beta2', '>=' ) ? '100px' : 100,\n\t\t),\n\t),\n);\n\n\n/**\n * Filters the template blocks styling.\n *\n * @since 6.0.0\n *\n * @param array $blocks_styles Array of blocks styles.\n */\n$blocks_styles = apply_filters( 'llms_block_templates_styling', $blocks_styles );\n\n/**\n * Shared block template for the `llms_certificate` and `llms_my_certificate` post types.\n *\n * @since 6.0.0\n * @since 6.1.0 Changed the certificate template's use of the `{current_date}` merge code to `{earned_date}`.\n */\n$certificates = array(\n\tarray(\n\t\t'llms/certificate-title',\n\t\tarray(\n\t\t\t'style'     => $blocks_styles['certificate']['title']['style'],\n\t\t\t'textColor' => $blocks_styles['certificate']['title']['textColor'],\n\t\t),\n\t),\n\tarray(\n\t\t'core/spacer',\n\t\tarray(\n\t\t\t'height' => $blocks_styles['certificate']['spacer']['height'],\n\t\t),\n\t),\n\tarray(\n\t\t'core/heading',\n\t\tarray(\n\t\t\t'content'   => __( 'Presented to', 'lifterlms' ),\n\t\t\t'level'     => 3,\n\t\t\t'textAlign' => 'center',\n\t\t\t'style'     => $blocks_styles['certificate']['h3']['style'],\n\t\t\t'textColor' => $blocks_styles['certificate']['h3']['textColor'],\n\t\t),\n\t),\n\tarray(\n\t\t'core/heading',\n\t\tarray(\n\t\t\t'content'   => '[llms-user display_name]',\n\t\t\t'level'     => 2,\n\t\t\t'textAlign' => 'center',\n\t\t\t'style'     => $blocks_styles['certificate']['h2']['style'],\n\t\t\t'textColor' => $blocks_styles['certificate']['h2']['textColor'],\n\t\t),\n\t),\n\tarray(\n\t\t'core/heading',\n\t\tarray(\n\t\t\t'content'   => __( 'for demonstration of excellence', 'lifterlms' ),\n\t\t\t'level'     => 3,\n\t\t\t'textAlign' => 'center',\n\t\t\t'style'     => $blocks_styles['certificate']['h3']['style'],\n\t\t\t'textColor' => $blocks_styles['certificate']['h3']['textColor'],\n\t\t),\n\t),\n\tarray(\n\t\t'core/spacer',\n\t\tarray(\n\t\t\t'height' => $blocks_styles['certificate']['spacer']['height'],\n\t\t),\n\t),\n\tarray(\n\t\t'core/columns',\n\t\tarray(\n\t\t\t'isStackedOnMobile' => false,\n\t\t),\n\t\tarray(\n\t\t\tarray(\n\t\t\t\t'core/column',\n\t\t\t\tarray(),\n\t\t\t\tarray(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'core/paragraph',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'align'     => 'center',\n\t\t\t\t\t\t\t'content'   => '{earned_date}',\n\t\t\t\t\t\t\t'style'     => $blocks_styles['certificate']['p']['style'],\n\t\t\t\t\t\t\t'textColor' => $blocks_styles['certificate']['p']['textColor'],\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'core/separator',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'align' => 'center',\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'core/paragraph',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'align'     => 'center',\n\t\t\t\t\t\t\t'content'   => __( 'DATE', 'lifterlms' ),\n\t\t\t\t\t\t\t'style'     => $blocks_styles['certificate']['p']['style'],\n\t\t\t\t\t\t\t'textColor' => $blocks_styles['certificate']['p']['textColor'],\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray( 'core/column' ),\n\t\t\tarray(\n\t\t\t\t'core/column',\n\t\t\t\tarray(),\n\t\t\t\tarray(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'core/paragraph',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'align'     => 'center',\n\t\t\t\t\t\t\t'content'   => '{site_title}',\n\t\t\t\t\t\t\t'style'     => $blocks_styles['certificate']['p']['style'],\n\t\t\t\t\t\t\t'textColor' => $blocks_styles['certificate']['p']['textColor'],\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'core/separator',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'align' => 'center',\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'core/paragraph',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'align'     => 'center',\n\t\t\t\t\t\t\t'content'   => __( 'SIGNED', 'lifterlms' ),\n\t\t\t\t\t\t\t'style'     => $blocks_styles['certificate']['p']['style'],\n\t\t\t\t\t\t\t'textColor' => $blocks_styles['certificate']['p']['textColor'],\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t),\n);\n\nreturn array(\n\t'llms_certificate'    => $certificates,\n\t'llms_my_certificate' => $certificates,\n);\n"
  },
  {
    "path": "includes/schemas/llms-db-updates.php",
    "content": "<?php\n/**\n * LifterLMS database migrations\n *\n * Lists database updates to be run during a plugin upgrade.\n *\n * Each array key should map to the LifterLMS version when the update should be run.\n *\n * Each array should contain a 'type' and 'updates' key\n *\n * The 'type' is either or either 'automatic' or 'manual'. Specifying whether\n * or not the user is prompted to run the upgrade (manual) or if it just runs automatically during\n * an upgrade (automatic).\n *\n * The 'updates' is an array of functions to be called to run the upgrade. They will be run in the order\n * they are listed. A function to upgrade to the version should always be included as the last update in the list.\n * This is important if multiple sets of updates need to be run during the same upgrade. For example a user upgrading from\n * versions less than 3.0.0 would have to run the entire list of upgrades for each version and upgrading the version at the end\n * of the set to the specific version will ensure that the next set of upgrades runs and then the next and so on.\n *\n * @package LifterLMS/Schemas\n *\n * @since 5.2.0\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'3.0.0'  => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_300_create_access_plans',\n\t\t\t'llms_update_300_del_deprecated_options',\n\t\t\t'llms_update_300_migrate_account_field_options',\n\t\t\t'llms_update_300_migrate_coupon_data',\n\t\t\t'llms_update_300_migrate_course_postmeta',\n\t\t\t'llms_update_300_migrate_lesson_postmeta',\n\t\t\t'llms_update_300_migrate_order_data',\n\t\t\t'llms_update_300_migrate_email_postmeta',\n\t\t\t'llms_update_300_update_orders',\n\t\t\t'llms_update_300_update_db_version',\n\t\t),\n\t),\n\t'3.0.3'  => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_303_update_students_role',\n\t\t\t'llms_update_303_update_db_version',\n\t\t),\n\t),\n\t'3.4.3'  => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_343_update_relationships',\n\t\t\t'llms_update_343_update_db_version',\n\t\t),\n\t),\n\t'3.6.0'  => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_360_set_product_visibility',\n\t\t\t'llms_update_360_update_db_version',\n\t\t),\n\t),\n\t'3.8.0'  => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_380_set_access_plan_visibility',\n\t\t\t'llms_update_380_update_db_version',\n\t\t),\n\t),\n\t'3.12.0' => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_3120_update_order_end_dates',\n\t\t\t'llms_update_3120_update_integration_options',\n\t\t\t'llms_update_3120_update_db_version',\n\t\t),\n\t),\n\t'3.13.0' => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_3130_create_default_instructors',\n\t\t\t'llms_update_3130_builder_notice',\n\t\t\t'llms_update_3130_update_db_version',\n\t\t),\n\t),\n\t'3.16.0' => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_3160_update_quiz_settings',\n\t\t\t'llms_update_3160_lesson_to_quiz_relationships_migration',\n\t\t\t'llms_update_3160_attempt_migration',\n\t\t\t'llms_update_3160_ensure_no_dupe_question_rels',\n\t\t\t'llms_update_3160_ensure_no_lesson_dupe_rels',\n\t\t\t'llms_update_3160_update_question_data',\n\t\t\t'llms_update_3160_update_attempt_question_data',\n\t\t\t'llms_update_3160_update_quiz_to_lesson_rels',\n\t\t\t'llms_update_3160_builder_notice',\n\t\t\t'llms_update_3160_update_db_version',\n\t\t),\n\t),\n\t'3.28.0' => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_3280_clear_session_cleanup_cron',\n\t\t\t'llms_update_3280_update_db_version',\n\t\t),\n\t),\n\t'4.0.0'  => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_400_remove_session_options',\n\t\t\t'llms_update_400_clear_session_cron',\n\t\t\t'llms_update_400_update_db_version',\n\t\t),\n\t),\n\t'4.5.0'  => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_450_migrate_events_open_sessions',\n\t\t\t'llms_update_450_update_db_version',\n\t\t),\n\t),\n\t'4.15.0' => array(\n\t\t'type'    => 'manual',\n\t\t'updates' => array(\n\t\t\t'llms_update_4150_remove_orphan_access_plans',\n\t\t\t'llms_update_4150_update_db_version',\n\t\t),\n\t),\n\t'5.0.0'  => array(\n\t\t'type'    => 'auto',\n\t\t'updates' => array(\n\t\t\t'llms_update_500_legacy_options_autoload_off',\n\t\t\t'llms_update_500_update_db_version',\n\t\t\t'llms_update_500_add_admin_notice',\n\t\t),\n\t),\n\t'5.2.0'  => array(\n\t\t'type'    => 'auto',\n\t\t'updates' => array(\n\t\t\t'llms_update_520_upcoming_reminder_notification_backward_compat',\n\t\t\t'llms_update_520_update_db_version',\n\t\t),\n\t),\n\t'6.0.0'  => array(\n\t\t'type'      => 'manual',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'migrate_achievements',\n\t\t\t'migrate_certificates',\n\t\t\t'migrate_award_templates',\n\t\t\t'show_notice',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n\t'6.3.0'  => array(\n\t\t'type'      => 'auto',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'buddypress_profile_endpoints_bc',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n\t'6.10.0' => array(\n\t\t'type'      => 'auto',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'migrate_spanish_users',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n\t'7.5.0'  => array(\n\t\t'type'      => 'auto',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'favorites_feature_bc',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n\t'7.8.0'  => array(\n\t\t'type'      => 'auto',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'maybe_set_option_llms_access_plans_allow_skus',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n\t'7.8.5'  => array(\n\t\t'type'      => 'auto',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'maybe_remove_pwc',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n\t'9.0.0'  => array(\n\t\t'type'      => 'auto',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'show_notice',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n\t'9.2.1'  => array(\n\t\t'type'      => 'auto',\n\t\t'namespace' => true,\n\t\t'updates'   => array(\n\t\t\t'reset_course_calc_data_locks',\n\t\t\t'update_db_version',\n\t\t),\n\t),\n);\n"
  },
  {
    "path": "includes/schemas/llms-form-locations.php",
    "content": "<?php\n/**\n * Default LifterLMS Form location definitions\n *\n * This file returns a list of the default LifterLMS form locations required\n * by LifterLMS for allowing users to create and manage accounts on the website.\n *\n * @package LifterLMS/Schemas\n *\n * @since 5.0.0\n * @version 5.0.0\n *\n * @see LLMS_Forms::get_locations() The core method used to retrieve the form locations schema.\n * @see llms_forms_get_locations Filter the form locations schema.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'checkout'     => array(\n\t\t'name'        => __( 'Checkout', 'lifterlms' ),\n\t\t'description' => __( 'Handles new user registration and existing user information updates during checkout and enrollment.', 'lifterlms' ),\n\t\t'title'       => __( 'Billing Information', 'lifterlms' ),\n\t\t'template'    => LLMS_Form_Templates::get_template( 'checkout' ),\n\t\t'required'    => array(\n\t\t\tarray(\n\t\t\t\t'fieldName' => 'email_address',\n\t\t\t\t'blockName' => 'llms/form-field-user-email',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'fieldName' => 'password',\n\t\t\t\t'blockName' => 'llms/form-field-user-password',\n\t\t\t),\n\t\t),\n\t\t'meta'        => array(\n\t\t\t'_llms_form_location'   => 'checkout',\n\t\t\t'_llms_form_show_title' => 'yes',\n\t\t\t'_llms_form_is_core'    => 'yes',\n\t\t),\n\t),\n\t'registration' => array(\n\t\t'name'        => __( 'Registration', 'lifterlms' ),\n\t\t'description' => __( 'Handles new user registration and existing user information updates for open registration on the student dashboard and wherever the [lifterlms_registration] shortcode is used.', 'lifterlms' ),\n\t\t'title'       => __( 'Register', 'lifterlms' ),\n\t\t'template'    => LLMS_Form_Templates::get_template( 'registration' ),\n\t\t'required'    => array(\n\t\t\tarray(\n\t\t\t\t'fieldName' => 'email_address',\n\t\t\t\t'blockName' => 'llms/form-field-user-email',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'fieldName' => 'password',\n\t\t\t\t'blockName' => 'llms/form-field-user-password',\n\t\t\t),\n\t\t),\n\t\t'meta'        => array(\n\t\t\t'_llms_form_location'   => 'registration',\n\t\t\t'_llms_form_show_title' => 'yes',\n\t\t\t'_llms_form_is_core'    => 'yes',\n\t\t),\n\t),\n\t'account'      => array(\n\t\t'name'        => __( 'Account', 'lifterlms' ),\n\t\t'description' => __( 'Handles user account information updates on the edit account area of the student dashboard.', 'lifterlms' ),\n\t\t'title'       => __( 'Edit Account Information', 'lifterlms' ),\n\t\t'template'    => LLMS_Form_Templates::get_template( 'account' ),\n\t\t'required'    => array(\n\t\t\tarray(\n\t\t\t\t'fieldName' => 'email_address',\n\t\t\t\t'blockName' => 'llms/form-field-user-email',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'fieldName' => 'password',\n\t\t\t\t'blockName' => 'llms/form-field-user-password',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'fieldName' => 'display_name',\n\t\t\t\t'blockName' => 'llms/form-field-user-display-name',\n\t\t\t),\n\t\t),\n\t\t'meta'        => array(\n\t\t\t'_llms_form_location'   => 'account',\n\t\t\t'_llms_form_show_title' => 'no',\n\t\t\t'_llms_form_is_core'    => 'yes',\n\t\t),\n\t),\n);\n"
  },
  {
    "path": "includes/schemas/llms-reusable-blocks.php",
    "content": "<?php\n/**\n * Default form field blocks schema\n *\n * This file returns a list of the default LifterLMS form fields\n * used to build an initial set of reusable blocks used across the\n * core user information forms (checkout, registration, and account).\n *\n * Each field block is an incomplete form field definition. Each field\n * is linked to a user information form field by its name attribute which\n * will match an info field by its id attribute.\n *\n * User information fields are defined in `includes/schemas/llms-user-information-fields.php.\n *\n * @package LifterLMS/Schemas\n *\n * @since 5.0.0\n * @version 5.3.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'username'     => array(\n\t\t'title'     => _x( 'Username (Reusable)', 'Default form field reusable block title', 'lifterlms' ),\n\t\t'blockName' => 'llms/form-field-user-login',\n\t\t'attrs'     => array(\n\t\t\t'required'        => true,\n\t\t\t'id'              => 'user_login',\n\t\t\t'llms_visibility' => 'logged_out',\n\t\t),\n\t),\n\t'email'        => array(\n\t\t'title'     => _x( 'Email Address (Reusable)', 'Default form field reusable block title', 'lifterlms' ),\n\t\t'blockName' => 'llms/form-field-user-email',\n\t\t'attrs'     => array(\n\t\t\t'required'        => true,\n\t\t\t'id'              => 'email_address',\n\t\t\t'llms_visibility' => 'logged_out',\n\t\t),\n\t\t'confirm'   => 'email',\n\t),\n\t'password'     => array(\n\t\t'title'     => _x( 'Password (Reusable)', 'Default form field reusable block title', 'lifterlms' ),\n\t\t'blockName' => 'llms/form-field-user-password',\n\t\t'attrs'     => array(\n\t\t\t'required'        => true,\n\t\t\t'id'              => 'password',\n\t\t\t'llms_visibility' => 'logged_out',\n\t\t),\n\t\t'confirm'   => 'password',\n\t),\n\t'name'         => array(\n\t\t'title'       => _x( 'First and Last Name (Reusable)', 'Default form field reusable block title', 'lifterlms' ),\n\t\t'blockName'   => 'llms/form-field-user-name',\n\t\t'innerBlocks' => array(\n\t\t\tarray(\n\t\t\t\t'blockName' => 'llms/form-field-user-first-name',\n\t\t\t\t'attrs'     => array(\n\t\t\t\t\t'id'          => 'first_name',\n\t\t\t\t\t'required'    => true,\n\t\t\t\t\t'columns'     => 6,\n\t\t\t\t\t'last_column' => false,\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'blockName' => 'llms/form-field-user-last-name',\n\t\t\t\t'attrs'     => array(\n\t\t\t\t\t'id'          => 'last_name',\n\t\t\t\t\t'required'    => true,\n\t\t\t\t\t'columns'     => 6,\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t),\n\t'display_name' => array(\n\t\t'title'     => _x( 'Public Display Name (Reusable)', 'Default form field reusable block title', 'lifterlms' ),\n\t\t'blockName' => 'llms/form-field-user-display-name',\n\t\t'attrs'     => array(\n\t\t\t'required' => true,\n\t\t\t'id'       => 'display_name',\n\t\t),\n\t),\n\t'address'      => array(\n\t\t'title'       => _x( 'Address (Reusable)', 'Default form field reusable block title', 'lifterlms' ),\n\t\t'blockName'   => 'llms/form-field-user-address',\n\t\t'innerBlocks' => array(\n\t\t\tarray(\n\t\t\t\t'blockName'   => 'llms/form-field-user-address-street',\n\t\t\t\t'innerBlocks' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'blockName' => 'llms/form-field-user-address-street-primary',\n\t\t\t\t\t\t'attrs'     => array(\n\t\t\t\t\t\t\t'id'          => 'llms_billing_address_1',\n\t\t\t\t\t\t\t'required'    => true,\n\t\t\t\t\t\t\t'columns'     => 8,\n\t\t\t\t\t\t\t'last_column' => false,\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'blockName' => 'llms/form-field-user-address-street-secondary',\n\t\t\t\t\t\t'attrs'     => array(\n\t\t\t\t\t\t\t'id'          => 'llms_billing_address_2',\n\t\t\t\t\t\t\t'required'    => false,\n\t\t\t\t\t\t\t'columns'     => 4,\n\t\t\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'blockName' => 'llms/form-field-user-address-city',\n\t\t\t\t'attrs'     => array(\n\t\t\t\t\t'id'       => 'llms_billing_city',\n\t\t\t\t\t'required' => true,\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'blockName' => 'llms/form-field-user-address-country',\n\t\t\t\t'attrs'     => array(\n\t\t\t\t\t'id'       => 'llms_billing_country',\n\t\t\t\t\t'required' => true,\n\t\t\t\t),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'blockName'   => 'llms/form-field-user-address-region',\n\t\t\t\t'innerBlocks' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'blockName' => 'llms/form-field-user-address-state',\n\t\t\t\t\t\t'attrs'     => array(\n\t\t\t\t\t\t\t'id'          => 'llms_billing_state',\n\t\t\t\t\t\t\t'required'    => true,\n\t\t\t\t\t\t\t'columns'     => 6,\n\t\t\t\t\t\t\t'last_column' => false,\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'blockName' => 'llms/form-field-user-address-postal-code',\n\t\t\t\t\t\t'attrs'     => array(\n\t\t\t\t\t\t\t'id'          => 'llms_billing_zip',\n\t\t\t\t\t\t\t'required'    => true,\n\t\t\t\t\t\t\t'columns'     => 6,\n\t\t\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t),\n\t'phone'        => array(\n\t\t'title'     => _x( 'Phone Number (Reusable)', 'Default form field reusable block title', 'lifterlms' ),\n\t\t'blockName' => 'llms/form-field-user-phone',\n\t\t'attrs'     => array(\n\t\t\t'id'       => 'llms_phone',\n\t\t\t'required' => false,\n\t\t),\n\t),\n);\n"
  },
  {
    "path": "includes/schemas/llms-user-information-fields.php",
    "content": "<?php\n/**\n * User information fields schema\n *\n * A list of user information fields used by LifterLMS in various places, namely to build\n * the editable user information forms (Checkout, Registration, and Edit Account).\n *\n * Each item in this list should be an array compatible with the `LLMS_Form_Field` class'\n * settings array.\n *\n * Fields can be added and modified using the `llms_user_information_fields` filter.\n *\n * @package LifterLMS/Schemas\n *\n * @since 5.0.0\n * @version 5.0.0\n *\n * @see llms_get_user_information_fields() Retrieves the (filtered) schema.\n * @see llms_get_user_information_field() Retrieve a single field from this schema by ID.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\n\t// WordPress Core.\n\tarray(\n\t\t'id'             => 'user_login',\n\t\t'name'           => 'user_login',\n\t\t'type'           => 'text',\n\t\t'label'          => __( 'Username', 'lifterlms' ),\n\t\t'data_store'     => 'users',\n\t\t'data_store_key' => 'user_login',\n\t),\n\tarray(\n\t\t'id'             => 'email_address',\n\t\t'name'           => 'email_address',\n\t\t'type'           => 'email',\n\t\t'label'          => __( 'Email Address', 'lifterlms' ),\n\t\t'data_store'     => 'users',\n\t\t'data_store_key' => 'user_email',\n\t),\n\tarray(\n\t\t'id'                => 'password',\n\t\t'name'              => 'password',\n\t\t'type'              => 'password',\n\t\t'label'             => __( 'Password', 'lifterlms' ),\n\t\t'data_store'        => 'users',\n\t\t'data_store_key'    => 'user_pass',\n\t\t'meter'             => llms_parse_bool( get_option( 'lifterlms_registration_password_strength', 'yes' ) ),\n\t\t'min_strength'      => get_option( 'lifterlms_registration_password_min_strength', 'weak' ),\n\t\t'html_attrs'        => array(\n\t\t\t'minlength' => 8,\n\t\t),\n\t\t'meter_description' => sprintf(\n\t\t\t// Translators: %s = Minimum password strength.\n\t\t\t__(\n\t\t\t\t'A %s password is required with at least 8 characters. To make it stronger, use both upper and lower case letters, numbers, and symbols.',\n\t\t\t\t'lifterlms'\n\t\t\t),\n\t\t\tllms_get_minimum_password_strength_name( get_option( 'lifterlms_registration_password_min_strength', 'weak' ) )\n\t\t),\n\t),\n\tarray(\n\t\t'id'             => 'first_name',\n\t\t'name'           => 'first_name',\n\t\t'type'           => 'text',\n\t\t'label'          => __( 'First Name', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'first_name',\n\t),\n\tarray(\n\t\t'id'             => 'last_name',\n\t\t'name'           => 'last_name',\n\t\t'type'           => 'text',\n\t\t'label'          => __( 'Last Name', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'last_name',\n\t),\n\tarray(\n\t\t'id'             => 'display_name',\n\t\t'name'           => 'display_name',\n\t\t'type'           => 'text',\n\t\t'label'          => __( 'Display Name', 'lifterlms' ),\n\t\t'data_store'     => 'users',\n\t\t'data_store_key' => 'display_name',\n\t),\n\n\t// LifterLMS core.\n\tarray(\n\t\t'id'             => 'llms_billing_address_1',\n\t\t'name'           => 'llms_billing_address_1',\n\t\t'type'           => 'text',\n\t\t'label'          => __( 'Address', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'llms_billing_address_1',\n\t),\n\tarray(\n\t\t'id'               => 'llms_billing_address_2',\n\t\t'name'             => 'llms_billing_address_2',\n\t\t'type'             => 'text',\n\t\t'label'            => '',\n\t\t'label_show_empty' => true,\n\t\t'data_store'       => 'usermeta',\n\t\t'data_store_key'   => 'llms_billing_address_2',\n\t\t'placeholder'      => __( 'Apartment, suite, etc...', 'lifterlms' ),\n\t),\n\tarray(\n\t\t'id'             => 'llms_billing_city',\n\t\t'name'           => 'llms_billing_city',\n\t\t'type'           => 'text',\n\t\t'label'          => __( 'City', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'llms_billing_city',\n\t),\n\tarray(\n\t\t'id'             => 'llms_billing_country',\n\t\t'name'           => 'llms_billing_country',\n\t\t'type'           => 'select',\n\t\t'label'          => __( 'Country', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'llms_billing_country',\n\t\t'options_preset' => 'countries',\n\t\t'placeholder'    => __( 'Select a Country', 'lifterlms' ),\n\t\t'classes'        => 'llms-select2',\n\t),\n\tarray(\n\t\t'id'             => 'llms_billing_state',\n\t\t'name'           => 'llms_billing_state',\n\t\t'type'           => 'select',\n\t\t'label'          => __( 'State / Region', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'llms_billing_state',\n\t\t'options_preset' => 'states',\n\t\t'placeholder'    => __( 'Select a State / Region', 'lifterlms' ),\n\t\t'classes'        => 'llms-select2',\n\t),\n\tarray(\n\t\t'id'             => 'llms_billing_zip',\n\t\t'name'           => 'llms_billing_zip',\n\t\t'type'           => 'text',\n\t\t'label'          => __( 'Postal / Zip Code', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'llms_billing_zip',\n\t),\n\tarray(\n\t\t'id'             => 'llms_phone',\n\t\t'name'           => 'llms_phone',\n\t\t'type'           => 'tel',\n\t\t'label'          => __( 'Phone Number', 'lifterlms' ),\n\t\t'data_store'     => 'usermeta',\n\t\t'data_store_key' => 'llms_phone',\n\t),\n\n);\n"
  },
  {
    "path": "includes/shortcodes/class-llms-shortcode-user-info.php",
    "content": "<?php\n/**\n * LLMS_Shortcode_User_Info class.\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS User Information Shortcode.\n *\n * Shortcode: [llms-user]\n *\n * @since 5.0.0\n */\nclass LLMS_Shortcode_User_Info extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var  string\n\t */\n\tpublic $tag = 'llms-user';\n\n\t/**\n\t * Retrieves a list of keys that cannot be displayed by the shortcode.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string[]\n\t */\n\tprotected function get_blocklist() {\n\n\t\t/**\n\t\t * Filters the list of keys which cannot be displayed using the [user] shortcode\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param string[] $keys List of user and usermeta keys.\n\t\t */\n\t\treturn apply_filters( 'llms_user_info_shortcode_blocked_keys', array( 'user_pass' ) );\n\n\t}\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'key' => '',\n\t\t\t'or'  => '',\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\n\t\t/**\n\t\t * Filters the user used to retrieve user information displayed by the [llms-user] shortcode\n\t\t *\n\t\t * @since 5.0.0\n\t\t *\n\t\t * @param integer $user_id The WP_User ID of the currently logged-in user or `0` if no user logged in.\n\t\t */\n\t\t$user_id = apply_filters( 'llms_user_info_shortcode_user_id', get_current_user_id() );\n\t\t$key     = $this->get_attribute( 'key' );\n\t\t$default = $this->get_attribute( 'or' );\n\n\t\tif ( in_array( $key, $this->get_blocklist(), true ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t// No user OR no key provided.\n\t\tif ( ! $user_id || ! $key ) {\n\t\t\treturn $default;\n\t\t}\n\n\t\t$user = new WP_User( $user_id );\n\t\t$val  = $user->exists() ? $user->get( $key ) : null;\n\n\t\treturn ! empty( $val ) && is_scalar( $val ) ? $val : $default;\n\n\t}\n\n\t/**\n\t * Merge user attributes with default attributes.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $atts User-submitted shortcode attributes.\n\t *\n\t * @return array\n\t */\n\tprotected function set_attributes( $atts = array() ) {\n\n\t\t// Allow `key` attribute to be submitted without a key, eg: [llms-user first_name].\n\t\tif ( isset( $atts[0] ) ) {\n\t\t\t$atts['key'] = $atts[0];\n\t\t\tunset( $atts[0] );\n\t\t}\n\n\t\treturn parent::set_attributes( $atts );\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_User_Info::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.bbp.shortcode.course.forums.list.php",
    "content": "<?php\n/**\n * LifterLMS Course Meta Information Shortcode\n *\n * [lifterlms_bbp_course_forums]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.12.0\n * @version 3.12.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_BBP_Shortcode_Course_Forums_List\n *\n * @since 3.12.0\n * @since 3.12.1 Unknown.\n */\nclass LLMS_BBP_Shortcode_Course_Forums_List extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var  string\n\t */\n\tpublic $tag = 'lifterlms_bbp_course_forums';\n\n\t/**\n\t * Retrieve the forum ids associated with the course\n\t *\n\t * @since 3.12.0\n\t * @since 3.12.1 Unknown.\n\t *\n\t * @return array\n\t */\n\tprivate function get_forums() {\n\n\t\tglobal $post;\n\n\t\t$course = llms_get_post( $post );\n\t\tif ( $course ) {\n\t\t\treturn $course->get( 'bbp_forum_ids' );\n\t\t}\n\n\t\treturn array();\n\n\t}\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 3.12.0\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\n\t\t$forums = $this->get_forums();\n\n\t\tif ( $forums ) {\n\t\t\techo '<div class=\"llms-bbp-course-forums-wrap\">';\n\t\t\t\techo '<ul class=\"llms-bbp-course-forums-list\">';\n\t\t\tforeach ( $forums as $forum_id ) : ?>\n\t\t\t\t\t<li><a class=\"llms-bbp-forum-title\" href=\"<?php bbp_forum_permalink( $forum_id ); ?>\">\n\t\t\t\t\t\t<?php bbp_forum_title( $forum_id ); ?>\n\t\t\t\t\t</a></li>\n\t\t\t\t<?php\n\t\t\t\tendforeach;\n\t\t\t\techo '</ul>';\n\t\t\techo '</div>';\n\t\t}\n\n\t}\n\n}\n\nreturn LLMS_BBP_Shortcode_Course_Forums_List::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.checkout.php",
    "content": "<?php\n/**\n * LifterLMS Checkout Page Shortcode\n *\n * Controls functionality associated with shortcode [llms_checkout].\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 1.0.0\n * @version 7.0.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Checkout class.\n *\n * @since 1.0.0\n * @since 3.30.1 Added check via llms_locate_order_for_user_and_plan() to automatically resume an existing pending order for logged in users if one exists.\n * @since 3.33.0 Checkout form not displayed to users already enrolled in the product being purchased, a notice informing them of that is displayed instead.\n * @since 3.35.0 Sanitize input data.\n * @since 3.36.3 Added l10n function to membership restriction error message.\n * @since 4.2.0 Added filter to control the displaying of the notice informing the students they're already enrolled in the product being purchased.\n * @since 5.0.0 Add support for LLMS_Form field management.\n */\nclass LLMS_Shortcode_Checkout {\n\n\t/**\n\t * Current User ID.\n\t *\n\t * @var int\n\t */\n\tpublic static $uid;\n\n\t/**\n\t * Renders the checkout template.\n\t *\n\t * @since 1.0.0\n\t * @since 3.33.0 Do not display the checkout form but a notice to a logged in user enrolled in the product being purchased.\n\t * @since 3.36.3 Added l10n function to membership restriction error message.\n\t * @since 4.2.0 Added filter to control the displaying of the notice informing the students they're already enrolled in the product being purchased.\n\t *\n\t * @param array $atts Shortcode attributes array.\n\t * @return void\n\t */\n\tprivate static function checkout( $atts ) {\n\n\t\t// if there are membership restrictions, check the user is in at least one membership.\n\t\t// this is to combat CHEATERS.\n\t\tif ( $atts['plan']->has_availability_restrictions() ) {\n\t\t\t$access = false;\n\t\t\tforeach ( $atts['plan']->get_array( 'availability_restrictions' ) as $mid ) {\n\n\t\t\t\t// once we find a membership, exit.\n\t\t\t\tif ( llms_is_user_enrolled( self::$uid, $mid ) ) {\n\t\t\t\t\t$access = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( ! $access ) {\n\t\t\t\tllms_print_notice( __( 'You must be a member in order to purchase this access plan.', 'lifterlms' ), 'error' );\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif ( self::$uid ) {\n\t\t\t// ensure the user isn't enrolled in the product being purchased.\n\t\t\tif ( isset( $atts['product'] ) && llms_is_user_enrolled( self::$uid, $atts['product']->get( 'id' ) ) ) {\n\n\t\t\t\t/**\n\t\t\t\t * Filter the displaying of the checkout form notice for already enrolled in the product being purchased.\n\t\t\t\t *\n\t\t\t\t * @param bool $display_notice Whether or not displaying the checkout form notice for already enrolled students in the product being purchased.\n\t\t\t\t * @param LLMS_Access_Plan $plan The access plan.\n\t\t\t\t *\n\t\t\t\t * @since 4.2.0\n\t\t\t\t */\n\t\t\t\tif ( apply_filters( 'llms_display_checkout_form_enrolled_students_notice', true, $atts['plan'] ) ) {\n\t\t\t\t\tllms_print_notice(\n\t\t\t\t\t\tsprintf(\n\t\t\t\t\t\t\t// Translators: %2$s = The product type (course/membership); %1$s = product permalink.\n\t\t\t\t\t\t\t__( 'You already have access to this %2$s! Visit your dashboard <a href=\"%1$s\">here.</a>', 'lifterlms' ),\n\t\t\t\t\t\t\tllms_get_page_url( 'myaccount' ),\n\t\t\t\t\t\t\t$atts['product']->get_post_type_label()\n\t\t\t\t\t\t),\n\t\t\t\t\t\t'notice'\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t/**\n\t\t\t\t * Filter to block checkout when the student is enrolled. Defaults to true.\n\t\t\t\t *\n\t\t\t\t * @param bool $block_checkout Whether or not blocking the checkout form for already enrolled students in the product being purchased.\n\t\t\t\t * @param LLMS_Access_Plan $plan The access plan.\n\t\t\t\t *\n\t\t\t\t * @since 9.1.2\n\t\t\t\t */\n\t\t\t\tif ( apply_filters( 'llms_checkout_block_enrolled_checkout', true, $atts['plan'] ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$user = get_userdata( self::$uid );\n\t\t\tllms_print_notice( sprintf( __( 'You are currently logged in as <em>%1$s</em>. <a href=\"%2$s\">Click here to logout</a>', 'lifterlms' ), $user->user_email, wp_logout_url( $atts['plan']->get_checkout_url() ) ), 'notice' );\n\t\t} else {\n\t\t\tllms_get_login_form( sprintf( __( 'Already have an account? <a href=\"%s\">Click here to login</a>', 'lifterlms' ), '#llms-show-login' ), $atts['plan']->get_checkout_url() );\n\t\t}\n\n\t\tif ( isset( $atts['product'] ) && 'course' === $atts['product']->get( 'type' ) && ( $course = new LLMS_Course( $atts['product']->post ) ) && ! $course->has_capacity() ) {\n\t\t\t/**\n\t\t\t * Filter the displaying of the checkout form notice for already enrolled in the product being purchased.\n\t\t\t *\n\t\t\t * @param bool $display_notice Whether or not displaying the checkout form notice for already enrolled students in the product being purchased.\n\t\t\t * @param LLMS_Access_Plan $plan The access plan.\n\t\t\t *\n\t\t\t * @since 4.2.0\n\t\t\t */\n\t\t\tif ( apply_filters( 'llms_display_checkout_form_course_capacity_notice', true, $atts['plan'] ) ) {\n\t\t\t\tllms_print_notice(\n\t\t\t\t\t$course->get( 'capacity_message' ),\n\t\t\t\t\t'error'\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Filter to block checkout when the course capacity has been reached. Defaults to true.\n\t\t\t *\n\t\t\t * @param bool $block_checkout Whether or not blocking the checkout form when a course is at capacity.\n\t\t\t * @param LLMS_Access_Plan $plan The access plan.\n\t\t\t *\n\t\t\t * @since 10.0.0\n\t\t\t */\n\t\t\tif ( apply_filters( 'llms_checkout_block_course_capacity_checkout', true, $atts['plan'] ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tllms_get_template( 'checkout/form-checkout.php', $atts );\n\t}\n\n\t/**\n\t * Renders the confirm payment checkout template.\n\t *\n\t * @since 1.0.0\n\t * @version 3.0.0\n\t *\n\t * @param array $atts shortcode attributes.\n\t * @return void\n\t */\n\tprivate static function confirm_payment( $atts ) {\n\n\t\tllms_get_template( 'checkout/form-confirm-payment.php', $atts );\n\t}\n\n\t/**\n\t * Output error messages when they're encountered.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @param string $message The error message.\n\t * @return void\n\t */\n\tprivate static function error( $message ) {\n\t\t/**\n\t\t * Filters error messages displayed on the checkout screen.\n\t\t *\n\t\t * @since 3.0.0\n\t\t *\n\t\t * @param string $message The error message.\n\t\t */\n\t\techo wp_kses_post( apply_filters( 'llms_checkout_error_output', $message ) );\n\t}\n\n\t/**\n\t * Retrieve the shortcode content.\n\t *\n\t * @since 1.0.0\n\t *\n\t * @param array $atts Shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function get( $atts ) {\n\t\treturn LLMS_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts );\n\t}\n\n\t/**\n\t * Gather a bunch of information and output the actual content for the shortcode.\n\t *\n\t * @since 1.0.0\n\t * @since 3.30.1 Added check via llms_locate_order_for_user_and_plan() to automatically resume an existing pending order for logged in users if one exists.\n\t * @since 3.35.0 Sanitize input data.\n\t * @since 5.0.0 Organize attribute configuration and add new dynamic attributes related to the LLMS_Form post.\n\t * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t * @since 7.0.0 Fixed unclosed `div.llms-checkout-wrapper` on empty cart.\n\t * @since 7.0.1 Fixed issue encountered when trying to confirm payment for a non-existent order.\n\t *\n\t * @param array $atts Shortcode atts from originating shortcode.\n\t * @return void\n\t */\n\tpublic static function output( $atts ) {\n\n\t\tglobal $wp;\n\n\t\t$atts = $atts ? $atts : array();\n\n\t\t$atts['cols'] = isset( $atts['cols'] ) ? $atts['cols'] : 2;\n\n\t\tself::$uid = get_current_user_id();\n\n\t\t$atts['gateways']         = llms()->payment_gateways()->get_enabled_payment_gateways();\n\t\t$atts['selected_gateway'] = llms()->payment_gateways()->get_default_gateway();\n\n\t\t$atts['order_key'] = '';\n\n\t\t$atts['field_data'] = array();\n\t\tif ( isset( $_POST ) && isset( $_POST['action'] ) && 'create_pending_order' === $_POST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing\n\t\t\t$atts['field_data'] = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing\n\t\t} elseif ( self::$uid ) {\n\t\t\t$atts['field_data'] = get_current_user_id();\n\t\t}\n\n\t\tself::checkout_wrapper_start();\n\n\t\t/**\n\t\t * Allows gateways or third parties to output custom errors before\n\t\t * any core logic is executed.\n\t\t *\n\t\t * This filter returns `false` by default. To output custom errors return\n\t\t * the error message as a string that will be displayed on screen.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param bool|string $pre_error A custom error message.\n\t\t */\n\t\t$err = apply_filters( 'lifterlms_pre_checkout_error', false );\n\t\tif ( $err ) {\n\t\t\tself::error( $err );\n\t\t\tself::checkout_wrapper_end();\n\t\t\treturn;\n\t\t}\n\n\t\tllms_print_notices();\n\n\t\t// purchase step 1.\n\t\tif ( isset( $_GET['plan'] ) && is_numeric( $_GET['plan'] ) ) {\n\n\t\t\t$plan_id = llms_filter_input( INPUT_GET, 'plan', FILTER_SANITIZE_NUMBER_INT );\n\n\t\t\t// Only retrieve if plan is a llms_access_plan and is published.\n\t\t\tif ( 0 === strcmp( get_post_status( $plan_id ), 'publish' ) && 0 === strcmp( get_post_type( $plan_id ), 'llms_access_plan' ) ) {\n\n\t\t\t\t$coupon = llms()->session->get( 'llms_coupon' );\n\n\t\t\t\tif ( isset( $coupon['coupon_id'] ) && isset( $coupon['plan_id'] ) ) {\n\t\t\t\t\tif ( $coupon['plan_id'] == $_GET['plan'] ) {\n\t\t\t\t\t\t$atts['coupon'] = new LLMS_Coupon( $coupon['coupon_id'] );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tllms()->session->set( 'llms_coupon', false );\n\t\t\t\t\t\t$atts['coupon'] = false;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t$atts['coupon'] = false;\n\t\t\t\t}\n\n\t\t\t\t// Use posted order key to resume a pending order.\n\t\t\t\tif ( isset( $_POST['llms_order_key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing\n\t\t\t\t\t$atts['order_key'] = llms_filter_input_sanitize_string( INPUT_POST, 'llms_order_key' );\n\n\t\t\t\t\t// Attempt to locate a pending order.\n\t\t\t\t} elseif ( self::$uid ) {\n\t\t\t\t\t$pending_order = llms_locate_order_for_user_and_plan( self::$uid, $plan_id );\n\t\t\t\t\tif ( $pending_order ) {\n\t\t\t\t\t\t$order             = llms_get_post( $pending_order );\n\t\t\t\t\t\t$atts['order_key'] = ( 'llms-pending' === $order->get( 'status' ) ) ? $order->get( 'order_key' ) : '';\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$atts = self::setup_plan_and_form_atts( $plan_id, $atts );\n\n\t\t\t\t/**\n\t\t\t\t * Filter the number of columns used to render the checkout/enrollment form.\n\t\t\t\t *\n\t\t\t\t * @since Unknown.\n\t\t\t\t * @since 5.0.0 Added `$form_location` parameter.\n\t\t\t\t *\n\t\t\t\t * @param int $cols Number of columns. Accepts 1 or 2.\n\t\t\t\t * @param LLMS_Access_Plan $plan Access plan object.\n\t\t\t\t * @param string $form_location Form location ID.\n\t\t\t\t */\n\t\t\t\t$atts['cols'] = apply_filters( 'llms_checkout_columns', ( $atts['is_free'] || ! $atts['form_fields'] ) ? 1 : $atts['cols'], $atts['plan'], $atts['form_location'] );\n\n\t\t\t\tself::checkout( $atts );\n\n\t\t\t} else {\n\n\t\t\t\tself::error( __( 'Invalid access plan.', 'lifterlms' ) );\n\n\t\t\t}\n\t\t} elseif ( isset( $wp->query_vars['confirm-payment'] ) ) {\n\n\t\t\t$order_key = llms_filter_input_sanitize_string( INPUT_GET, 'order' );\n\t\t\t$order     = $order_key ? llms_get_order_by_key( $order_key ) : false;\n\t\t\tif ( ! $order ) {\n\t\t\t\tself::error( __( 'Could not locate an order to confirm.', 'lifterlms' ) );\n\t\t\t\tself::checkout_wrapper_end();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$atts = self::setup_plan_and_form_atts( $order->get( 'plan_id' ), $atts );\n\n\t\t\tif ( $order->get( 'coupon_id' ) ) {\n\t\t\t\t$atts['coupon'] = new LLMS_Coupon( $order->get( 'coupon_id' ) );\n\t\t\t} else {\n\t\t\t\t$atts['coupon'] = false;\n\t\t\t}\n\n\t\t\t$atts['selected_gateway'] = llms()->payment_gateways()->get_gateway_by_id( $order->get( 'payment_gateway' ) );\n\n\t\t\tself::confirm_payment( $atts );\n\n\t\t} else {\n\n\t\t\tself::error( sprintf( __( 'Your cart is currently empty. Click <a href=\"%s\">here</a> to get started.', 'lifterlms' ), llms_get_page_url( 'courses' ) ) );\n\n\t\t}\n\n\t\tself::checkout_wrapper_end();\n\t}\n\n\t/**\n\t * Setup attributes for plan and form information.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Properly detect empty form fields when the html is only composed of blanks and empty paragraphs.\n\t * @since 7.0.0 Add 'redirect' hidden field to be used on purchase completion.\n\t *\n\t * @param int   $plan_id LLMS_Access_Plan post id.\n\t * @param array $atts    Existing attributes.\n\t * @return array Modified attributes array.\n\t */\n\tprotected static function setup_plan_and_form_atts( $plan_id, $atts ) {\n\n\t\t$plan = new LLMS_Access_Plan( $plan_id );\n\n\t\t$atts['plan']    = $plan;\n\t\t$atts['product'] = $plan->get_product();\n\t\t$atts['is_free'] = $plan->has_free_checkout();\n\n\t\t$atts['form_location'] = 'checkout';\n\t\t$atts['form_title']    = llms_get_form_title( $atts['form_location'], array( 'plan' => $plan ) );\n\t\t$atts['form_fields']   = self::clean_form_fields( llms_get_form_html( $atts['form_location'], array( 'plan' => $plan ) ) );\n\n\t\t// Add 'redirect' URL hidden field to be used on purchase completion.\n\t\t$plan_redirection_url = $plan->get_redirection_url( false );\n\t\tif ( $plan_redirection_url ) {\n\t\t\t$atts['form_fields'] .= ( new LLMS_Form_Field(\n\t\t\t\tarray(\n\t\t\t\t\t'id'             => 'llms-redirect',\n\t\t\t\t\t'name'           => 'redirect',\n\t\t\t\t\t'type'           => 'hidden',\n\t\t\t\t\t'value'          => $plan_redirection_url,\n\t\t\t\t\t'data_store_key' => false,\n\t\t\t\t)\n\t\t\t) )->get_html();\n\t\t}\n\n\t\treturn $atts;\n\t}\n\n\t/**\n\t * Clean form fields html\n\t *\n\t * Properly detects empty form fields when the html is only composed of blanks and empty paragraphs.\n\t * In this case the form fields html is turned into an empty string.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @param array $fields_html Form Fields.\n\t * @return array\n\t */\n\tprivate static function clean_form_fields( $fields_html ) {\n\t\t// If fields html has only blanks and emoty paragraphs (autop?), clean it.\n\t\tif ( empty( preg_replace( '/(\\s)*(<p><\\/p>)*/m', '', $fields_html ) ) ) {\n\t\t\t$fields_html = '';\n\t\t}\n\t\treturn $fields_html;\n\t}\n\n\t/**\n\t * Output the checkout wrapper opening tags.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tprivate static function checkout_wrapper_start() {\n\t\techo '<div class=\"llms-checkout-wrapper\">';\n\t}\n\n\t/**\n\t * Output the checkout wrapper closing tags.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tprivate static function checkout_wrapper_end() {\n\t\techo '</div><!-- .llms-checkout-wrapper -->';\n\t}\n}\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.author.php",
    "content": "<?php\n/**\n * LifterLMS Course Meta Information Shortcode\n *\n * [lifterlms_course_author]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.6.0\n * @version 3.11.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Author\n *\n * @since 3.6.0\n * @since 3.11.1 Unknown.\n */\nclass LLMS_Shortcode_Course_Author extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_author';\n\n\t/**\n\t * Get default shortcode attributes.\n\t *\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'avatar_size' => 48,\n\t\t\t'bio'         => 'yes',\n\t\t\t'course_id'   => get_the_ID(),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the author ID of the course\n\t *\n\t * Lessons and Quizzes cascade up\n\t *\n\t * @since 3.11.1\n\t *\n\t * @return int|null\n\t */\n\tprivate function get_author_id() {\n\n\t\t$post = llms_get_post( $this->get_attribute( 'course_id' ) );\n\t\tif ( ! $post ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( in_array( $post, array( 'lesson', 'quiz' ) ) ) {\n\t\t\t$course = llms_get_post_parent_course( $post->get( 'id' ) );\n\t\t\tif ( ! $course ) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t\treturn $post->get( 'author' );\n\t}\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 3.6.0\n\t * @since 3.11.1\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\n\t\techo '<div class=\"llms-meta-info\">';\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in function.\n\t\techo llms_get_author(\n\t\t\tarray(\n\t\t\t\t'avatar_size' => $this->get_attribute( 'avatar_size' ),\n\t\t\t\t'bio'         => ( 'yes' === $this->get_attribute( 'bio' ) ) ? true : false,\n\t\t\t\t'user_id'     => $this->get_author_id(),\n\t\t\t)\n\t\t);\n\t\techo '</div><!-- .llms-meta-info -->';\n\t}\n}\n\nreturn LLMS_Shortcode_Course_Author::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.continue.button.php",
    "content": "<?php\n/**\n * LifterLMS Course Continue Button\n *\n * [lifterlms_course_continue_button]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.11.1\n * @version 3.11.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Element\n *\n * @since 3.11.1\n */\nclass LLMS_Shortcode_Course_Continue_Button extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_continue_button';\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @since 3.11.1\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'course_id' => get_the_ID(),\n\t\t);\n\t}\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 3.11.1\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\n\t\tlifterlms_course_continue_button( $this->get_attribute( 'course_id' ) );\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_Course_Continue_Button::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.continue.php",
    "content": "<?php\n/**\n * LifterLMS Course Progress & Continue Button Shortcode\n *\n * [lifterlms_course_continue]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.6.0\n * @version 3.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Continue\n *\n * @since 3.6.0\n */\nclass LLMS_Shortcode_Course_Continue extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_continue';\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\n\t\tlifterlms_template_single_course_progress();\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_Course_Continue::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.instructors.php",
    "content": "<?php\n/**\n * LifterLMS Course Instructors\n *\n * [lifterlms_course_instructors]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 7.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Instructors\n *\n * @since 7.7.0\n */\nclass LLMS_Shortcode_Course_Instructors extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_instructors';\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 7.7.0\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\n\t\tllms_template_instructors();\n\t}\n}\n\nreturn LLMS_Shortcode_Course_Instructors::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.meta.info.php",
    "content": "<?php\n/**\n * LifterLMS Course Meta Information Shortcode\n *\n * [lifterlms_course_meta_info]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.6.0\n * @version 3.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Meta_Info\n *\n * @since 3.6.0\n */\nclass LLMS_Shortcode_Course_Meta_Info extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_meta_info';\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\n\t\techo '<div class=\"llms-meta-info\">';\n\t\tlifterlms_template_single_length();\n\t\tlifterlms_template_single_difficulty();\n\t\tlifterlms_template_single_course_tracks();\n\t\tlifterlms_template_single_course_categories();\n\t\tlifterlms_template_single_course_tags();\n\t\techo '</div><!-- .llms-meta-info -->';\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_Course_Meta_Info::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.outline.php",
    "content": "<?php\n/**\n * LifterLMS Course Outline Shortcode\n *\n * [lifterlms_course_outline]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.5.1\n * @version 3.19.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Outline\n *\n * @since 3.5.1\n * @since 3.19.2 Unknown.\n */\nclass LLMS_Shortcode_Course_Outline extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var  string\n\t */\n\tpublic $tag = 'lifterlms_course_outline';\n\n\t/**\n\t * Retrieve the default course id depending on the current post\n\t *\n\t * @return   int|null\n\t * @since    3.5.1\n\t * @version  3.17.7\n\t */\n\tprivate function get_course_id() {\n\n\t\tglobal $post;\n\t\t$post_id = isset( $post->ID ) ? $post->ID : null;\n\n\t\t$course_id = null;\n\n\t\tif ( $post_id ) {\n\n\t\t\tif ( 'course' !== $post->post_type ) {\n\n\t\t\t\t// get the parent\n\t\t\t\t$parent = llms_get_post_parent_course( $post );\n\t\t\t\tif ( $parent ) {\n\t\t\t\t\t$course_id = $parent->get( 'id' );\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t$course_id = $post_id;\n\n\t\t\t}\n\t\t}\n\n\t\treturn $course_id;\n\n\t}\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @return   array\n\t * @since    3.5.1\n\t * @version  3.5.1\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'collapse'     => 0, // outputs a collapsible syllabus when true\n\t\t\t'course_id'    => $this->get_course_id(),\n\t\t\t'outline_type' => 'full', // full, current_section\n\t\t\t'toggles'      => 0, // outputs open/close all toggles when true\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @return   string\n\t * @since    3.5.1\n\t * @version  3.19.2\n\t */\n\tprotected function get_output() {\n\n\t\t$course  = new LLMS_Course( $this->get_attribute( 'course_id' ) );\n\t\t$student = llms_get_student();\n\n\t\t$args = array(\n\t\t\t'collapse'        => $this->get_attribute( 'collapse' ),\n\t\t\t'course'          => $course,\n\t\t\t'current_section' => null,\n\t\t\t'current_lesson'  => null,\n\t\t\t'sections'        => array(),\n\t\t\t'student'         => $student,\n\t\t\t'toggles'         => $this->get_attribute( 'toggles' ),\n\t\t);\n\n\t\tif ( ! $course ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$next_lesson = $student ? llms_get_post( $student->get_next_lesson( $course->get( 'id' ) ) ) : false;\n\n\t\tif ( 'lesson' === get_post_type() ) {\n\t\t\t$args['current_lesson'] = get_the_ID();\n\t\t}\n\n\t\t// show only the current section\n\t\tif ( $next_lesson && 'current_section' === $this->get_attribute( 'outline_type' ) ) {\n\n\t\t\t$section = llms_get_post( $next_lesson->get( 'parent_section' ) );\n\n\t\t\t$args['sections'][]      = $section;\n\t\t\t$args['current_section'] = $section->get( 'id' );\n\n\t\t} else {\n\n\t\t\tif ( 'lesson' === get_post_type() ) {\n\t\t\t\t$lesson = llms_get_post( get_the_ID() );\n\t\t\t} else {\n\t\t\t\t$lesson = $next_lesson;\n\t\t\t}\n\n\t\t\t$args['sections']        = $course->get_sections();\n\t\t\t$args['current_section'] = ! empty( $lesson ) && is_a( $lesson, 'LLMS_Post_Model' ) ? $lesson->get( 'parent_section' ) : false;\n\n\t\t}\n\n\t\tob_start();\n\t\tllms_get_template( 'course/outline-list-small.php', $args );\n\t\treturn ob_get_clean();\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_Course_Outline::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.prerequisites.php",
    "content": "<?php\n/**\n * LifterLMS Course Prerequisites notice Shortcode\n *\n * [lifterlms_course_prerequisites]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.6.0\n * @version 3.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Prerequisites\n *\n * @since 3.6.0\n */\nclass LLMS_Shortcode_Course_Prerequisites extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_prerequisites';\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\t\tlifterlms_template_single_prerequisites();\n\t}\n\n}\n\nreturn LLMS_Shortcode_Course_Prerequisites::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.reviews.php",
    "content": "<?php\n/**\n * LifterLMS Course Reviews notice Shortcode\n *\n * [lifterlms_course_reviews]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.6.0\n * @version 3.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Reviews\n *\n * @since 3.6.0\n */\nclass LLMS_Shortcode_Course_Reviews extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_reviews';\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @return   void\n\t * @since    3.6.0\n\t * @version  3.6.0\n\t */\n\tprotected function template_function() {\n\n\t\tlifterlms_template_single_reviews();\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_Course_Reviews::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.course.syllabus.php",
    "content": "<?php\n/**\n * LifterLMS Course Syllabus Shortcode\n *\n * [lifterlms_course_syllabus]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.6.0\n * @version 3.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Course_Syllabus\n *\n * @since 3.6.0\n */\nclass LLMS_Shortcode_Course_Syllabus extends LLMS_Shortcode_Course_Element {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_course_syllabus';\n\n\t/**\n\t * Call the template function for the course element\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return void\n\t */\n\tprotected function template_function() {\n\n\t\t// TODO: Don't render if an elementor page?\n\t\tlifterlms_template_single_syllabus();\n\t}\n}\n\nreturn LLMS_Shortcode_Course_Syllabus::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.courses.php",
    "content": "<?php\n/**\n * LifterLMS Courses Shortcode\n *\n * [lifterlms_courses]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.14.0\n * @version 4.12.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Courses Shortcode class.\n *\n * @since 3.14.0\n * @since 3.30.2 Output a message instead of the entire course catalog when \"mine\" is used and and current student is not enrolled in any courses.\n * @since 3.31.0 Adjusted several private methods to be protected.\n * @since 3.37.17 Use strict comparisons for `in_array()`.\n */\nclass LLMS_Shortcode_Courses extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_courses';\n\n\t/**\n\t * Get shortcode attributes\n\t *\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output().\n\t *\n\t * @since 3.14.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'category'       => '',\n\t\t\t'hidden'         => 'yes',\n\t\t\t'id'             => '', // Allow comma-separated list of course ids.\n\t\t\t'mine'           => 'no',\n\t\t\t'post_status'    => 'publish',\n\t\t\t'posts_per_page' => -1,\n\t\t\t'order'          => 'ASC',\n\t\t\t'orderby'        => 'title',\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an array of post ids based on submitted ID parameter and the mine parameter\n\t *\n\t * @since 3.14.0\n\t * @since 3.31.0 Changed access from private to protected.\n\t * @since 3.37.17 Use strict comparisons for `in_array()`.\n\t *\n\t * @return array\n\t */\n\tprotected function get_post__in() {\n\n\t\t$ids     = array();\n\t\t$post_id = $this->get_attribute( 'id' );\n\t\tif ( $post_id ) {\n\t\t\t$ids = explode( ',', $post_id ); // Allow multiple ids to be passed.\n\t\t\t$ids = array_map( 'trim', $ids );\n\t\t}\n\n\t\t$student = llms_get_student();\n\n\t\t$mine = $this->get_attribute( 'mine' );\n\t\tif ( in_array( $mine, array( 'any', 'cancelled', 'enrolled', 'expired' ), true ) ) {\n\n\t\t\t$courses = $student->get_courses(\n\t\t\t\tarray(\n\t\t\t\t\t'limit'  => 1000,\n\t\t\t\t\t'status' => $this->get_attribute( 'mine' ),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$ids = $ids ? array_intersect( $ids, $courses['results'] ) : $courses['results'];\n\n\t\t}\n\n\t\t/**\n\t\t * Filter the array of IDs returned for use in querying courses to display.\n\t\t *\n\t\t * @since 4.16.0\n\t\t *\n\t\t * @param array        $ids     The IDs of courses that will be displayed.\n\t\t * @param LLMS_Student $student The student object for the current user.\n\t\t * @param string       $mine    The \"mine\" attribute of the shortcode.\n\t\t */\n\t\treturn apply_filters( 'llms_courses_shortcode_get_post__in', $ids, $student, $mine );\n\t}\n\n\t/**\n\t * Retrieve the tax query based on submitted category & visibility\n\t *\n\t * @since 3.14.0\n\t * @since 3.31.0 Changed access from private to protected.\n\t *\n\t * @return array|string\n\t */\n\tprotected function get_tax_query() {\n\n\t\t$has_tax_query = false;\n\n\t\t$tax_query = array(\n\t\t\t'relation' => 'AND',\n\t\t);\n\n\t\t$category = $this->get_attribute( 'category' );\n\t\tif ( $category ) {\n\t\t\t$tax_query[]   = array(\n\t\t\t\t'taxonomy' => 'course_cat',\n\t\t\t\t'field'    => 'slug',\n\t\t\t\t'terms'    => $category,\n\t\t\t);\n\t\t\t$has_tax_query = true;\n\t\t}\n\n\t\t$hidden = $this->get_attribute( 'hidden' );\n\t\tif ( 'no' === $hidden || false === $hidden || '' === $hidden ) {\n\n\t\t\t$terms = wp_list_pluck(\n\t\t\t\tget_terms(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'taxonomy'   => 'llms_product_visibility',\n\t\t\t\t\t\t'hide_empty' => false,\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t'term_taxonomy_id',\n\t\t\t\t'name'\n\t\t\t);\n\n\t\t\t$tax_query[]   = array(\n\t\t\t\t'field'    => 'term_taxonomy_id',\n\t\t\t\t'operator' => 'NOT IN',\n\t\t\t\t'taxonomy' => 'llms_product_visibility',\n\t\t\t\t'terms'    => array( $terms['hidden'] ),\n\t\t\t);\n\t\t\t$has_tax_query = true;\n\t\t}\n\n\t\treturn $has_tax_query ? $tax_query : '';\n\t}\n\n\t/**\n\t * Retrieve a WP_Query based on all submitted parameters\n\t *\n\t * @since 3.14.0\n\t * @since 3.31.0 Changed access from private to protected.\n\t * @since 4.12.0 Handle pagination when the shortcode is used on the static front page.\n\t *\n\t * @return WP_Query\n\t */\n\tprotected function get_wp_query() {\n\n\t\t$args = array(\n\t\t\t'paged'          => is_front_page() ? get_query_var( 'page' ) : get_query_var( 'paged' ),\n\t\t\t'post__in'       => $this->get_post__in(),\n\t\t\t'post_type'      => 'course',\n\t\t\t'post_status'    => $this->get_attribute( 'post_status' ),\n\t\t\t'tax_query'      => $this->get_tax_query(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query\n\t\t\t'posts_per_page' => $this->get_attribute( 'posts_per_page' ),\n\t\t\t'order'          => $this->get_attribute( 'order' ),\n\t\t\t'orderby'        => $this->get_attribute( 'orderby' ),\n\t\t);\n\n\t\treturn new WP_Query( $args );\n\t}\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter.\n\t *\n\t * @since 3.14.0\n\t * @since 3.30.2 Output a message instead of the entire course catalog when \"mine\" is used and and current student is not enrolled in any courses.\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\n\t\t$this->enqueue_script( 'llms-jquery-matchheight' );\n\n\t\tob_start();\n\n\t\t// If we're outputting a \"My Courses\" list and we don't have a student output login info.\n\t\tif ( 'no' !== $this->get_attribute( 'mine' ) && ! llms_get_student() ) {\n\n\t\t\tprintf(\n\t\t\t\tesc_html__( 'You must be logged in to view this information. Click %1$shere%2$s to login.', 'lifterlms' ),\n\t\t\t\t'<a href=\"' . esc_url( llms_get_page_url( 'myaccount' ) ) . '\">',\n\t\t\t\t'</a>'\n\t\t\t);\n\n\t\t} elseif ( 'no' !== $this->get_attribute( 'mine' ) && ! $this->get_post__in() ) {\n\n\t\t\t\tprintf( '<p>%s</p>', esc_html__( 'No courses found.', 'lifterlms' ) );\n\n\t\t} else {\n\n\t\t\tlifterlms_loop( $this->get_wp_query() );\n\t\t}\n\n\t\treturn ob_get_clean();\n\t}\n}\n\nreturn LLMS_Shortcode_Courses::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.favorites.php",
    "content": "<?php\n/**\n * LifterLMS Favorites Shortcode.\n *\n * [lifterlms_favorites]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 7.5.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Favorites Shortcode class.\n *\n * @since version\n */\nclass LLMS_Shortcode_Favorites extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag.\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_favorites';\n\n\t/**\n\t * Get shortcode attributes.\n\t *\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output().\n\t *\n\t * @since 7.5.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\n\t\treturn array(\n\t\t\t'orderby' => 'updated_date',\n\t\t\t'order'   => 'ASC',\n\t\t\t'limit'   => '',\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve an array of Favorites from `lifterlms_user_postmeta`.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @return WP_Query\n\t */\n\tprotected function get_favorites() {\n\n\t\t$student = llms_get_student();\n\n\t\t$order_by = $this->get_attribute( 'orderby' );\n\t\t$order    = $this->get_attribute( 'order' );\n\t\t$limit    = $this->get_attribute( 'limit' );\n\n\t\treturn $student->get_favorites( $order_by, $order, $limit );\n\t}\n\n\t/**\n\t * Retrieve the actual content of the shortcode.\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\n\t\tob_start();\n\n\t\t// If we're outputting a \"My Favorites\" list and we don't have a student output login info.\n\t\tif ( ! llms_get_student() ) {\n\n\t\t\tprintf(\n\t\t\t\t// Translators: 1%$s = Opening anchor tag; %2$s = Closing anchor tag.\n\t\t\t\tesc_html__( 'You must be logged in to view this information. Click %1$shere%2$s to login.', 'lifterlms' ),\n\t\t\t\t'<a href=\"' . esc_url( llms_get_page_url( 'myaccount' ) ) . '\">',\n\t\t\t\t'</a>'\n\t\t\t);\n\t\t} else {\n\t\t\t$favorites = $this->get_favorites();\n\t\t\tllms_template_my_favorites_loop( get_current_user_id(), $favorites );\n\t\t}\n\n\t\treturn ob_get_clean();\n\t}\n}\n\nreturn LLMS_Shortcode_Favorites::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.hide.content.php",
    "content": "<?php\n/**\n * LifterLMS Hide Content Shortcode\n *\n * [lifterlms_hide_content]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.5.1\n * @version 3.24.1\n *\n * @example\n *      [hide_content id=\"1\"] allows user with access to 1 to access content\n *      [hide_content id=\"1,2,3,4\" relation=\"any\"] allows user with access to 1,2,3, OR 4 to access content\n *      [hide_content id=\"1,2,3,4\" relation=\"all\"] allows only users with access 1,2,3 AND 4 to access\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Hide_Content\n *\n * @since 3.5.1\n * @since 3.24.1 Unknown.\n */\nclass LLMS_Shortcode_Hide_Content extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_hide_content';\n\n\t/**\n\t * Get default shortcode attributes\n\t *\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @since 3.5.1\n\t * @since 3.24.1 Unknown.\n\t *\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'membership' => '', // For backwards compat, use id moving forward/\n\t\t\t'message'    => '',\n\t\t\t'id'         => get_the_ID(),\n\t\t\t'relation'   => 'all',\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @since 3.5.1\n\t * @since 3.24.1 Unknown.\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\n\t\t// Backwards compatibility, get membership if set and fallback to the id.\n\t\t$ids = $this->get_attribute( 'membership' ) ? $this->get_attribute( 'membership' ) : $this->get_attribute( 'id' );\n\n\t\t// Explode, trim whitespace and remove empty values.\n\t\t$ids = (array) array_map( 'trim', array_filter( explode( ',', $ids ) ) );\n\n\t\t// Assume content is hidden.\n\t\t$hidden = true;\n\n\t\tif ( 'any' === $this->get_attribute( 'relation' ) && ! empty( $ids ) ) {\n\t\t\tforeach ( $ids as $id ) {\n\t\t\t\tif ( llms_is_user_enrolled( get_current_user_id(), $id ) ) {\n\t\t\t\t\t$hidden = false;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} elseif ( 'all' === $this->get_attribute( 'relation' ) && ! empty( $ids ) ) {\n\t\t\t$inc = 0;\n\t\t\tforeach ( $ids as $id ) {\n\t\t\t\tif ( llms_is_user_enrolled( get_current_user_id(), $id ) ) {\n\t\t\t\t\t$inc++;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( count( $ids ) === $inc ) {\n\t\t\t\t$hidden = false;\n\t\t\t}\n\t\t}\n\n\t\treturn ! $hidden ? do_shortcode( $this->get_content() ) : $this->get_attribute( 'message' );\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_Hide_Content::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.lesson.mark.complete.php",
    "content": "<?php\n/**\n * LifterLMS Hide Content Shortcode\n *\n * [lifterlms_lesson_mark_complete]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.11.1\n * @version 3.11.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Lesson_Mark_Complete\n *\n * @since 3.11.1\n */\nclass LLMS_Shortcode_Lesson_Mark_Complete extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_lesson_mark_complete';\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @since 3.11.1\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\n\t\tob_start();\n\t\tlifterlms_template_complete_lesson_link();\n\t\treturn ob_get_clean();\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_Lesson_Mark_Complete::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.lesson.navigation.php",
    "content": "<?php\n/**\n * LifterLMS Lesson Navigation Shortcode\n *\n * [lifterlms_lesson_navigation]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 10.0.0\n * @version 10.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Lesson_Navigation\n *\n * @since 10.0.0\n */\nclass LLMS_Shortcode_Lesson_Navigation extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_lesson_navigation';\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\n\t\tob_start();\n\t\tlifterlms_template_lesson_navigation();\n\t\treturn ob_get_clean();\n\t}\n}\n\nreturn LLMS_Shortcode_Lesson_Navigation::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.membership.instructors.php",
    "content": "<?php\n/**\n * LifterLMS Membership Instructors Shortcode\n *\n * Output an anchor link for a membership.\n *\n * [lifterlms_membership_instructors]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 8.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Membership_Link\n *\n * @since 3.0.0\n * @since 3.4.3 Unknown.\n */\nclass LLMS_Shortcode_Membership_Instructors extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var  string\n\t */\n\tpublic $tag = 'lifterlms_membership_instructors';\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @return   string\n\t * @since    8.0.0\n\t */\n\tprotected function get_output() {\n\t\tif ( 'llms_membership' !== get_post_type( get_the_ID() ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tob_start();\n\n\t\tllms_template_instructors();\n\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @return   array\n\t * @since    8.0.0\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array();\n\t}\n}\n\nreturn LLMS_Shortcode_Membership_Instructors::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.membership.link.php",
    "content": "<?php\n/**\n * LifterLMS Membership Link Shortcode\n *\n * Output an anchor link for a membership.\n *\n * [lifterlms_membership_link]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.0.0\n * @version 3.4.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Membership_Link\n *\n * @since 3.0.0\n * @since 3.4.3 Unknown.\n */\nclass LLMS_Shortcode_Membership_Link extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var  string\n\t */\n\tpublic $tag = 'lifterlms_membership_link';\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @return   string\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tprotected function get_output() {\n\t\tif ( 'publish' !== get_post_status( $this->get_attribute( 'id' ) ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\treturn '<a href=\"' . get_permalink( $this->get_attribute( 'id' ) ) . '\">' . $this->get_content() . '</a>';\n\t}\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @return   array\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'id' => get_the_ID(),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieves a string used for default content which is used if no content is supplied\n\t *\n\t * @return   string\n\t * @since    3.4.3\n\t * @version  3.4.3\n\t */\n\tprotected function get_default_content( $atts = array() ) {\n\t\t$default_content = 'publish' === get_post_status( $this->get_attribute( 'id' ) ) ? get_the_title( $this->get_attribute( 'id' ) ) : '';\n\t\treturn apply_filters( 'lifterlms_membership_link_text', $default_content, $this->get_attribute( 'id' ) );\n\t}\n}\n\nreturn LLMS_Shortcode_Membership_Link::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.my.account.php",
    "content": "<?php\n/**\n * My Account Shortcode\n *\n * Shortcode: [lifterlms_my_account].\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 1.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_My_Account class.\n *\n * @since 1.0.0\n * @since 3.25.1 Deprecated method `LLMS_Shortcode_My_Account::lost_password()`.\n * @since 4.0.0 Removed previously deprecated method `LLMS_Shortcode_My_Account::lost_password()`.\n */\nclass LLMS_Shortcode_My_Account {\n\n\t/**\n\t * Get shortcode content\n\t *\n\t * @since Unknown\n\t *\n\t * @param array $atts Shortcode attributes array.\n\t * @return array $messages\n\t */\n\tpublic static function get( $atts ) {\n\t\treturn LLMS_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts );\n\t}\n\n\t/**\n\t * Determines what content to output to user based on status\n\t *\n\t * @since 1.0.0\n\t * @since 3.25.1\n\t *\n\t * @param array $atts Array of user submitted shortcode attributes.\n\t * @return void\n\t */\n\tpublic static function output( $atts ) {\n\n\t\t$atts = shortcode_atts(\n\t\t\tarray(\n\t\t\t\t'layout'         => 'columns',\n\t\t\t\t'login_redirect' => null,\n\t\t\t),\n\t\t\t$atts,\n\t\t\t'lifterlms_my_account'\n\t\t);\n\n\t\tlifterlms_student_dashboard( $atts );\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.my.achievements.php",
    "content": "<?php\n/**\n * LifterLMS My Achievements\n *\n * [lifterlms_my_achievements]\n *\n * @package LifterLMS/Shortcodes/Classes\n *\n * @since 3.14.1\n * @version 3.14.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_My_Achievements\n *\n * @since 3.14.1\n */\nclass LLMS_Shortcode_My_Achievements extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_my_achievements';\n\n\t/**\n\t * Retrieves an array of default attributes which are automatically merged\n\t * with the user submitted attributes and passed to $this->get_output()\n\t *\n\t * @since 3.14.1\n\t * @return array\n\t */\n\tprotected function get_default_attributes() {\n\t\treturn array(\n\t\t\t'count'   => null,\n\t\t\t'columns' => 5,\n\t\t\t'user_id' => get_current_user_id(),\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * $atts & $content are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter\n\t *\n\t * @since 3.14.1\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\n\t\tif ( ! $this->get_attribute( 'user_id' ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$student = llms_get_student( $this->get_attribute( 'user_id' ) );\n\t\tif ( ! $student ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$course = new LLMS_Course( $this->get_attribute( 'course_id' ) );\n\n\t\tob_start();\n\t\tlifterlms_template_achievements_loop( $student, $this->get_attribute( 'count' ), $this->get_attribute( 'columns' ) );\n\t\treturn ob_get_clean();\n\n\t}\n\n}\n\nreturn LLMS_Shortcode_My_Achievements::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcode.registration.php",
    "content": "<?php\n/**\n * LifterLMS Registration Shortcode\n *\n * [lifterlms_registration]\n *\n * @package LifterLMS/Classes/Shortcodes\n *\n * @since 3.0.0\n * @version 5.0.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcode_Registration\n *\n * @since 3.0.0\n * @since 3.4.3 Migrated to utilize `LLMS_Shortcode` abstract.\n */\nclass LLMS_Shortcode_Registration extends LLMS_Shortcode {\n\n\t/**\n\t * Shortcode tag\n\t *\n\t * @var string\n\t */\n\tpublic $tag = 'lifterlms_registration';\n\n\t/**\n\t * Retrieve the actual content of the shortcode\n\t *\n\t * The variables `$atts` & `$content` are both filtered before being passed to get_output()\n\t * output is filtered so the return of get_output() doesn't need its own filter.\n\t *\n\t * @since 3.4.3\n\t * @since 5.0.0 Remove password strength enqueue script.\n\t * @since 5.0.2 Added select enqueue script and inline script for address info.\n\t *\n\t * @return string\n\t */\n\tprotected function get_output() {\n\t\t/**\n\t\t * Enqueue select2 scripts and styles.\n\t\t */\n\t\tllms()->assets->enqueue_script( 'llms-select2' );\n\t\tllms()->assets->enqueue_style( 'llms-select2-styles' );\n\n\t\tif ( ! wp_script_is( 'llms' ) ) {\n\t\t\t// If the main LifterLMS script isn't enqueued, adding inline script below will fail.\n\t\t\tllms()->assets->enqueue_script( 'llms' );\n\t\t}\n\n\t\twp_add_inline_script(\n\t\t\t'llms',\n\t\t\t\"window.llms.address_info = '\" . wp_json_encode( llms_get_countries_address_info() ) . \"';\"\n\t\t);\n\n\t\tob_start();\n\t\tinclude llms_get_template_part_contents( 'global/form', 'registration' );\n\t\treturn ob_get_clean();\n\t}\n}\n\nreturn LLMS_Shortcode_Registration::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcodes.blocks.php",
    "content": "<?php\n/**\n * LifterLMS Shortcodes Blocks\n *\n * @package LifterLMS/Classes/Shortcodes\n *\n * @since 7.2.0\n * @version 7.2.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcodes_Blocks class.\n *\n * @since 7.2.0\n */\nclass LLMS_Shortcodes_Blocks {\n\n\t/**\n\t * Instance of the class.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @var self\n\t */\n\tprivate static $instance;\n\n\t/**\n\t * Get instance of the class.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return self\n\t */\n\tpublic static function instance(): LLMS_Shortcodes_Blocks {\n\t\tif ( ! isset( self::$instance ) ) {\n\t\t\tself::$instance = new LLMS_Shortcodes_Blocks();\n\t\t}\n\t\treturn self::$instance;\n\t}\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'init', array( $this, 'register_blocks' ) );\n\t\tadd_action( 'init', array( __CLASS__, 'add_editor_styles' ) );\n\t\tadd_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_editor_styles' ) );\n\t\tadd_filter( 'llms_hide_registration_form', array( $this, 'show_form_preview' ) );\n\t\tadd_filter( 'llms_hide_login_form', array( $this, 'show_form_preview' ) );\n\t}\n\n\t/**\n\t * Available shortcode blocks.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_config(): array {\n\t\t$config = array(\n\t\t\t'access-plan-button'   => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcodes', 'access_plan_button' ),\n\t\t\t),\n\t\t\t'checkout'             => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcodes', 'checkout' ),\n\t\t\t),\n\t\t\t'courses'              => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Courses', 'output' ),\n\t\t\t),\n\t\t\t'course-author'        => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Course_Author', 'output' ),\n\t\t\t),\n\t\t\t'course-continue'      => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Course_Continue', 'output' ),\n\t\t\t),\n\t\t\t'course-meta-info'     => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Course_Meta_Info', 'output' ),\n\t\t\t),\n\t\t\t'course-outline'       => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Course_Outline', 'output' ),\n\t\t\t),\n\t\t\t'course-prerequisites' => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Course_Prerequisites', 'output' ),\n\t\t\t),\n\t\t\t'course-reviews'       => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Course_Reviews', 'output' ),\n\t\t\t),\n\t\t\t'course-syllabus'      => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Course_Syllabus', 'output' ),\n\t\t\t),\n\t\t\t'login'                => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcodes', 'login' ),\n\t\t\t),\n\t\t\t'memberships'          => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcodes', 'memberships' ),\n\t\t\t),\n\t\t\t'my-account'           => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcodes', 'my_account' ),\n\t\t\t),\n\t\t\t'my-achievements'      => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_My_Achievements', 'output' ),\n\t\t\t),\n\t\t\t'registration'         => array(\n\t\t\t\t'render' => array( 'LLMS_Shortcode_Registration', 'output' ),\n\t\t\t),\n\t\t);\n\n\t\t/**\n\t\t * Filters shortcode blocks config.\n\t\t *\n\t\t * @since 7.2.0\n\t\t *\n\t\t * @param array $config Array of shortcode blocks.\n\t\t */\n\t\treturn apply_filters( 'llms_shortcode_blocks', $config );\n\t}\n\n\t/**\n\t * Registers shortcode blocks.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function register_blocks(): void {\n\t\t$blocks = $this->get_config();\n\n\t\tforeach ( $blocks as $name => $args ) {\n\t\t\t$block_dir = $args['path'] ?? LLMS_PLUGIN_DIR . \"blocks/$name\";\n\n\t\t\tif ( file_exists( \"$block_dir/block.json\" ) ) {\n\t\t\t\tregister_block_type(\n\t\t\t\t\t$block_dir,\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'render_callback' => array( $this, 'render_block' ),\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Loads front end CSS in the editor.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic static function add_editor_styles(): void {\n\t\t$plugins_dir = basename( WP_PLUGIN_DIR );\n\t\t$plugin_dir  = basename( LLMS_PLUGIN_DIR );\n\t\t$rtl         = is_rtl() ? '-rtl' : '';\n\t\t$path        = \"../../$plugins_dir/$plugin_dir/assets/css/lifterlms{$rtl}.min.css\";\n\n\t\tadd_editor_style( $path );\n\t}\n\n\t/**\n\t * Enqueues editor styles.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @return void\n\t */\n\tpublic static function enqueue_editor_styles(): void {\n\t\tif ( ! llms_is_block_editor() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$path = '/assets/css/editor.min.css';\n\n\t\tif ( ! file_exists( LLMS()->plugin_path() . $path ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\twp_enqueue_style(\n\t\t\t'llms-editor',\n\t\t\tLLMS()->plugin_url() . $path,\n\t\t\tarray(),\n\t\t\tfilemtime( LLMS()->plugin_path() . $path )\n\t\t);\n\t}\n\n\t/**\n\t * Determines whether to show the registration and login form in editor preview.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @param bool $hide Whether to hide the registration form.\n\t * @return bool\n\t */\n\tpublic function show_form_preview( bool $hide ): bool {\n\t\tif ( llms_is_editor_block_rendering() ) {\n\t\t\t$hide = false;\n\t\t}\n\n\t\treturn $hide;\n\t}\n\n\t/**\n\t * Renders a shortcode block.\n\t *\n\t * @since 7.2.0\n\t *\n\t * @param array    $attributes The block attributes.\n\t * @param string   $content    The block default content.\n\t * @param WP_Block $block      The block instance.\n\t * @return string\n\t */\n\tpublic function render_block( array $attributes, string $content, WP_Block $block ): string {\n\t\tif ( ! property_exists( $block, 'name' ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$name   = str_replace( 'llms/', '', $block->name );\n\t\t$config = $this->get_config();\n\t\t$render = $config[ $name ]['render'] ?? array();\n\n\t\tif ( method_exists( $render[0] ?? '', 'instance' ) ) {\n\t\t\t$render = array( $render[0]::instance(), $render[1] ?? '' );\n\t\t}\n\n\t\tif ( ! is_callable( $render ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tif ( ! apply_filters( 'llms_render_block', true, $block ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$text = $attributes['text'] ?? '';\n\n\t\tunset( $attributes['text'] );\n\n\t\t$args = array( $attributes );\n\n\t\tif ( $text ) {\n\t\t\t$args[] = $text;\n\t\t}\n\n\t\t$html = call_user_func( $render, ...$args );\n\n\t\t// This allows emptyResponsePlaceholder to be used when no content is returned.\n\t\tif ( ! $html ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t// Use emptyResponsePlaceholder for Courses block instead of shortcode message.\n\t\tif ( false !== strpos( $html, __( 'No products were found matching your selection.', 'lifterlms' ) ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\treturn sprintf(\n\t\t\t'<div %1$s>%2$s</div>',\n\t\t\tget_block_wrapper_attributes(),\n\t\t\ttrim( $html )\n\t\t);\n\t}\n}\n\nreturn LLMS_Shortcodes_Blocks::instance();\n"
  },
  {
    "path": "includes/shortcodes/class.llms.shortcodes.php",
    "content": "<?php\n/**\n * LifterLMS Shortcodes\n *\n * @package LifterLMS/Classes/Shortcodes\n *\n * @since 1.0.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Shortcodes\n *\n * @since 1.0.0\n * @since 4.0.0 Remove reliance on deprecated class `LLMS_Quiz_Legacy` & stop registering deprecated shortcode `[courses]` and `[lifterlms_user_statistics]`.\n */\nclass LLMS_Shortcodes {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'init', array( 'LLMS_Shortcodes', 'init' ) );\n\t}\n\n\t/**\n\t * Initialize shortcodes array.\n\t *\n\t * @since 1.0.0\n\t * @since 3.11.1 Unknown.\n\t * @since 4.0.0 Stop registering previously deprecated shortcode `[courses]` and `[lifterlms_user_statistics]`.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t * @since 6.4.0 Allowed `LLMS_Shortcode_User_Info` class to be filtered.\n\t * @since 7.5.0 Added `LLMS_Shortcode_Favorites` class in shortcodes array.\n\t * @since 10.0.0 Added `LLMS_Shortcode_Lesson_Navigation` class in shortcodes array.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\t// New method.\n\t\t$scs = apply_filters(\n\t\t\t/**\n\t\t\t * Filters the shortcodes to initialize.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param string[] $shortcodes Array of shortcode class names to initialize.\n\t\t\t */\n\t\t\t'llms_load_shortcodes',\n\t\t\tarray(\n\t\t\t\t'LLMS_Shortcode_Course_Author',\n\t\t\t\t'LLMS_Shortcode_Course_Continue',\n\t\t\t\t'LLMS_Shortcode_Course_Continue_Button',\n\t\t\t\t'LLMS_Shortcode_Course_Instructors',\n\t\t\t\t'LLMS_Shortcode_Course_Meta_Info',\n\t\t\t\t'LLMS_Shortcode_Course_Outline',\n\t\t\t\t'LLMS_Shortcode_Course_Prerequisites',\n\t\t\t\t'LLMS_Shortcode_Course_Reviews',\n\t\t\t\t'LLMS_Shortcode_Course_Syllabus',\n\t\t\t\t'LLMS_Shortcode_Courses',\n\t\t\t\t'LLMS_Shortcode_Hide_Content',\n\t\t\t\t'LLMS_Shortcode_Lesson_Mark_Complete',\n\t\t\t\t'LLMS_Shortcode_Lesson_Navigation',\n\t\t\t\t'LLMS_Shortcode_Membership_Link',\n\t\t\t\t'LLMS_Shortcode_Membership_Instructors',\n\t\t\t\t'LLMS_Shortcode_My_Achievements',\n\t\t\t\t'LLMS_Shortcode_Registration',\n\t\t\t\t'LLMS_Shortcode_User_Info',\n\t\t\t\t'LLMS_Shortcode_Favorites',\n\t\t\t)\n\t\t);\n\n\t\t$hyphenated_file_classes = array(\n\t\t\t'LLMS_Shortcode_User_Info',\n\t\t);\n\n\t\tforeach ( $scs as $class ) {\n\n\t\t\t$separator = in_array( $class, $hyphenated_file_classes, true ) ? '-' : '.';\n\t\t\t$filename  = \"class{$separator}\" . strtolower( str_replace( '_', $separator, $class ) );\n\t\t\t/**\n\t\t\t * Filters the path of the shortcode class file.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param string $file  The shortcode class file name.\n\t\t\t * @param string $class The shortcode class name.\n\t\t\t */\n\t\t\t$path = apply_filters( 'llms_load_shortcode_path', LLMS_PLUGIN_DIR . \"includes/shortcodes/{$filename}.php\", $class );\n\n\t\t\tif ( file_exists( $path ) ) {\n\t\t\t\trequire_once $path;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * @deprecated 2.0.0\n\t\t * @todo       deprecate\n\t\t */\n\t\tadd_shortcode( 'courses', array( LLMS_Shortcode_Courses::instance(), 'output' ) );\n\n\t\t// Old method.\n\t\t$shortcodes = array(\n\t\t\t'lifterlms_access_plan_button' => __CLASS__ . '::access_plan_button',\n\t\t\t'lifterlms_my_account'         => __CLASS__ . '::my_account',\n\t\t\t'lifterlms_checkout'           => __CLASS__ . '::checkout',\n\t\t\t'lifterlms_course_info'        => __CLASS__ . '::course_info',\n\t\t\t'lifterlms_course_progress'    => __CLASS__ . '::course_progress',\n\t\t\t'lifterlms_course_title'       => __CLASS__ . '::course_title',\n\t\t\t'lifterlms_related_courses'    => __CLASS__ . '::related_courses',\n\t\t\t'lifterlms_login'              => __CLASS__ . '::login',\n\t\t\t'lifterlms_pricing_table'      => __CLASS__ . '::pricing_table',\n\t\t\t'lifterlms_memberships'        => __CLASS__ . '::memberships',\n\t\t);\n\n\t\tforeach ( $shortcodes as $shortcode => $function ) {\n\n\t\t\tadd_shortcode(\n\t\t\t\t/**\n\t\t\t\t * Filters the shortcode tag.\n\t\t\t\t *\n\t\t\t\t * The dynamic portion of the hook name, `$shortcode` refers to the shortcode tag itself.\n\t\t\t\t *\n\t\t\t\t * @since Unknown\n\t\t\t\t *\n\t\t\t\t * @param string $shortcode The shortcode tag.\n\t\t\t\t */\n\t\t\t\tapply_filters( \"{$shortcode}_shortcode_tag\", $shortcode ),\n\t\t\t\t$function\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Allows shortcodes to enqueue a script by handle\n\t *\n\t * Ensures the handle is registered and that it hasn't already been enqueued.\n\t *\n\t * @since 3.0.2\n\t *\n\t * @param string $handle Script handle used to register the script.\n\t *                       The script should be registered in `LLMS_Frontend_Assets`.\n\t * @return void\n\t */\n\tprivate static function enqueue_script( $handle ) {\n\n\t\tif ( wp_script_is( $handle, 'registered' ) && ! wp_script_is( $handle, 'enqueued' ) ) {\n\n\t\t\twp_enqueue_script( $handle );\n\n\t\t}\n\t}\n\n\t/**\n\t * Retrieve the course ID from within a course, lesson, or quiz\n\t *\n\t * @since 2.7.9\n\t * @since 3.16.0 Unknown.\n\t * @since 4.0.0 Remove reliance on deprecated class `LLMS_Quiz_Legacy`.\n\t *\n\t * @return int\n\t */\n\tprivate static function get_course_id() {\n\n\t\t$id = get_the_ID();\n\n\t\tif ( is_course() ) {\n\t\t\treturn $id;\n\t\t}\n\n\t\t$course = llms_get_post_parent_course( $id );\n\t\tif ( $course ) {\n\t\t\treturn $course->get( 'id' );\n\t\t}\n\n\t\treturn 0;\n\t}\n\n\t/**\n\t * Creates a wrapper for shortcode.\n\t *\n\t * @return string\n\t */\n\tpublic static function shortcode_wrapper(\n\t\t$function,\n\t\t$atts = array(),\n\t\t$wrapper = array(\n\t\t\t'class'  => 'lifterlms',\n\t\t\t'before' => null,\n\t\t\t'after'  => null,\n\t\t)\n\t) {\n\n\t\t\tob_start();\n\n\t\t\techo empty( $wrapper['before'] ) ? '<div class=\"' . esc_attr( $wrapper['class'] ) . '\">' : wp_kses_post( $wrapper['before'] );\n\t\t\tcall_user_func( $function, $atts );\n\t\t\techo empty( $wrapper['after'] ) ? '</div>' : wp_kses_post( $wrapper['after'] );\n\n\t\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Create a button for an Access Plan\n\t *\n\t * @since 3.2.5\n\t * @since 3.4.1 Unknown.\n\t *\n\t * @param array  $atts    Associative array of shortcode attributes.\n\t * @param string $content Optional. Shortcode content, enables custom text/html in the button. Default empty string.\n\t * @return string\n\t */\n\tpublic static function access_plan_button( $atts, $content = '' ) {\n\n\t\t$atts = shortcode_atts(\n\t\t\tarray(\n\t\t\t\t'classes' => '',\n\t\t\t\t'id'      => null,\n\t\t\t\t'size'    => '', // Can be: small, large.\n\t\t\t\t'type'    => 'primary', // Can be: primary, secondary, action, danger.\n\t\t\t),\n\t\t\t$atts,\n\t\t\t'lifterlms_access_plan_button'\n\t\t);\n\n\t\t$ret = '';\n\n\t\tif ( ! empty( $atts['id'] ) && is_numeric( $atts['id'] ) ) {\n\t\t\t$plan = new LLMS_Access_Plan( $atts['id'] );\n\n\t\t\t$classes  = 'llms-button-' . $atts['type'];\n\t\t\t$classes .= ! empty( $atts['size'] ) ? ' ' . $atts['size'] : '';\n\t\t\t$classes .= ! empty( $atts['classes'] ) ? ' ' . $atts['classes'] : '';\n\n\t\t\t$text = empty( $content ) ? $plan->get_enroll_text() : $content;\n\n\t\t\t$ret = '<a class=\"' . esc_attr( $classes ) . '\" href=\"' . esc_url( $plan->get_checkout_url() ) . '\" title=\"' . esc_attr( $plan->get( 'title' ) ) . '\" aria-label=\"' . esc_attr( $plan->get_enroll_text( true ) ) . '\">' . $text . '</a>';\n\t\t}\n\n\t\t/**\n\t\t * Filters the access plan button shortcode output\n\t\t *\n\t\t * @since unknown\n\t\t *\n\t\t * @param string $ret     The shortcode output.\n\t\t * @param array  $atts    Associative array of shortcode attributes.\n\t\t * @param string $content Shortcode content, enables custom text/html in the button. Default empty string.\n\t\t */\n\t\treturn apply_filters( 'llms_shortcode_access_plan_button', $ret, $atts, $content );\n\t}\n\n\t/**\n\t * Add a login form\n\t *\n\t * @since 3.0.4\n\t * @since 3.19.4 Unknown.\n\t *\n\t * @param array $atts Associative array of shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function login( $atts ) {\n\n\t\textract(\n\t\t\tshortcode_atts(\n\t\t\t\tarray(\n\t\t\t\t\t'layout'   => 'columns',\n\t\t\t\t\t'redirect' => get_permalink(),\n\t\t\t\t),\n\t\t\t\t$atts,\n\t\t\t\t'lifterlms_login'\n\t\t\t)\n\t\t);\n\n\t\tob_start();\n\t\tllms_print_notices();\n\t\tllms_get_login_form( null, $redirect, $layout );\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * My account shortcode\n\t *\n\t * Used for displaying account.\n\t *\n\t * @see self::shortcode_wrapper()\n\t *\n\t * @return string\n\t */\n\tpublic static function my_account( $atts ) {\n\n\t\treturn self::shortcode_wrapper( array( 'LLMS_Shortcode_My_Account', 'output' ), $atts );\n\t}\n\n\n\n\t/**\n\t * Memberships Shortcode\n\t *\n\t * Used for shortcode [lifterlms_memberships].\n\t *\n\t * @since 1.4.4\n\t * @since 3.0.2\n\t * @since 4.12.0 Handle pagination when the shortcode is used on the static front page.\n\t *\n\t * @param array $atts Associative array of shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function memberships( $atts ) {\n\n\t\t// Enqueue match height so the loop isn't all messed up visually.\n\t\tself::enqueue_script( 'llms-jquery-matchheight' );\n\n\t\tif ( isset( $atts['category'] ) ) {\n\t\t\t$tax = array(\n\t\t\t\tarray(\n\t\t\t\t\t'taxonomy' => 'membership_cat',\n\t\t\t\t\t'field'    => 'slug',\n\t\t\t\t\t'terms'    => $atts['category'],\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t$args = array(\n\t\t\t'paged'          => is_front_page() ? get_query_var( 'page' ) : get_query_var( 'paged' ),\n\t\t\t'post_type'      => 'llms_membership',\n\t\t\t'post_status'    => 'publish',\n\t\t\t'posts_per_page' => isset( $atts['posts_per_page'] ) ? $atts['posts_per_page'] : -1,\n\t\t\t'order'          => isset( $atts['order'] ) ? $atts['order'] : 'ASC',\n\t\t\t'orderby'        => isset( $atts['orderby'] ) ? $atts['orderby'] : 'title',\n\t\t\t'tax_query'      => isset( $tax ) ? $tax : '',\n\t\t);\n\n\t\tif ( isset( $atts['id'] ) ) {\n\n\t\t\t$args['p'] = $atts['id'];\n\n\t\t}\n\n\t\t$query = new WP_Query( $args );\n\n\t\tob_start();\n\n\t\tif ( $query->have_posts() ) :\n\n\t\t\t/**\n\t\t\t * lifterlms_before_loop hook\n\t\t\t *\n\t\t\t * @hooked lifterlms_loop_start - 10\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_before_loop' );\n\n\t\t\twhile ( $query->have_posts() ) :\n\t\t\t\t$query->the_post();\n\n\t\t\t\tllms_get_template_part( 'loop/content', get_post_type() );\n\n\t\t\tendwhile;\n\n\t\t\t/**\n\t\t\t * lifterlms_before_loop hook\n\t\t\t *\n\t\t\t * @hooked lifterlms_loop_end - 10\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_after_loop' );\n\n\t\t\techo '<nav class=\"llms-pagination\">';\n\t\t\t$pagination = paginate_links(\n\t\t\t\tarray(\n\t\t\t\t\t'base'      => str_replace( 999999, '%#%', esc_url( get_pagenum_link( 999999 ) ) ),\n\t\t\t\t\t'format'    => '?page=%#%',\n\t\t\t\t\t'total'     => $query->max_num_pages,\n\t\t\t\t\t'current'   => max( 1, $args['paged'] ),\n\t\t\t\t\t'prev_next' => true,\n\t\t\t\t\t'prev_text' => '«' . __( 'Previous', 'lifterlms' ),\n\t\t\t\t\t'next_text' => __( 'Next', 'lifterlms' ) . '»',\n\t\t\t\t\t'type'      => 'list',\n\t\t\t\t)\n\t\t\t);\n\t\t\tif ( ! empty( $pagination ) ) {\n\t\t\t\techo wp_kses_post( $pagination );\n\t\t\t}\n\t\t\techo '</nav>';\n\n\t\telse :\n\n\t\t\tllms_get_template( 'loop/none-found.php' );\n\n\t\tendif;\n\n\t\twp_reset_postdata();\n\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t * Checkout shortcode\n\t *\n\t * Used for displaying checkout form.\n\t *\n\t * @see self::shortcode_wrapper\n\t *\n\t * @param array $atts Associative array of shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function checkout( $atts ) {\n\n\t\treturn self::shortcode_wrapper( array( 'LLMS_Shortcode_Checkout', 'output' ), $atts );\n\t}\n\n\t/**\n\t * Output various pieces of metadata about a course\n\t *\n\t * @since 3.0.0\n\t * @since 3.4.1 Unknown.\n\t *\n\t * @param array $atts Array of user-submitted shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function course_info( $atts ) {\n\n\t\t$default_type = '';\n\t\tif ( isset( $atts['key'] ) && false !== strpos( $atts['key'], '_date' ) ) {\n\t\t\t$default_type = 'date';\n\t\t}\n\n\t\textract(\n\t\t\tshortcode_atts(\n\t\t\t\tarray(\n\t\t\t\t\t'date_format' => get_option( 'date_format' ), // If $type is date, a custom date format can be supplied.\n\t\t\t\t\t'id'          => get_the_ID(),\n\t\t\t\t\t'key'         => '',\n\t\t\t\t\t'type'        => $default_type, // Can either be: date, price or empty string.\n\t\t\t\t),\n\t\t\t\t$atts,\n\t\t\t\t'lifterlms_course_info'\n\t\t\t)\n\t\t);\n\n\t\t$ret = '';\n\n\t\tif ( $key ) {\n\n\t\t\t$course = new LLMS_Course( $id );\n\n\t\t\tswitch ( $type ) {\n\n\t\t\t\tcase 'date':\n\t\t\t\t\t$ret = $course->get_date( $key, $date_format );\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'price':\n\t\t\t\t\t$ret = $course->get_price( $key );\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t$ret = $course->get( $key );\n\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Filters the course info shortcode output\n\t\t *\n\t\t * @since unknown\n\t\t *\n\t\t * @param string $ret  The shortcode output.\n\t\t * @param array  $atts Associative array of shortcode attributes.\n\t\t */\n\t\treturn apply_filters( 'llms_shortcode_course_info', $ret, $atts );\n\t}\n\n\t/**\n\t * Course Progress Bar Shortcode\n\t *\n\t * @since unknown\n\t * @since 3.38.0 Added logic to display the bar only to enrolled user.\n\t *\n\t * @param array $atts Associative array of shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function course_progress( $atts ) {\n\n\t\t$course_id = self::get_course_id();\n\t\tif ( ! $course_id ) {\n\t\t\treturn '';\n\t\t}\n\n\t\tif ( ! empty( $atts['check_enrollment'] ) && ! llms_is_user_enrolled( get_current_user_id(), $course_id ) ) {\n\t\t\treturn '';\n\t\t}\n\n\t\t$course = new LLMS_Course( $course_id );\n\n\t\t$course_progress = $course->get_percent_complete();\n\n\t\treturn lifterlms_course_progress_bar( $course_progress, false, false, false );\n\t}\n\n\t/**\n\t * Retrieve the Course Title\n\t *\n\t * @since unknown\n\t * @since 2.7.9 Unknown\n\t *\n\t * @param array $atts Associative array of shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function course_title( $atts ) {\n\t\t$course_id = self::get_course_id();\n\t\tif ( ! $course_id ) {\n\t\t\treturn '';\n\t\t}\n\t\treturn get_the_title( $course_id );\n\t}\n\n\t/**\n\t * Courses shortcode\n\t *\n\t * Used for [lifterlms_related_courses].\n\t *\n\t * @since unknown\n\t *\n\t * @param array $atts Associative array of shortcode attributes.\n\t * @return array\n\t */\n\tpublic static function related_courses( $atts ) {\n\n\t\tob_start();\n\n\t\tif ( isset( $atts['category'] ) ) {\n\t\t\t$tax = array(\n\t\t\t\tarray(\n\t\t\t\t\t'taxonomy' => 'course_cat',\n\t\t\t\t\t'field'    => 'slug',\n\t\t\t\t\t'terms'    => $atts['category'],\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t$query = new WP_Query(\n\t\t\tarray(\n\t\t\t\t'post_type'      => 'course',\n\t\t\t\t'post_status'    => 'publish',\n\t\t\t\t'posts_per_page' => isset( $atts['per_page'] ) ? $atts['per_page'] : -1,\n\t\t\t\t'order'          => isset( $atts['order'] ) ? $atts['order'] : 'ASC',\n\t\t\t\t'orderby'        => isset( $atts['orderby'] ) ? $atts['orderby'] : 'title',\n\t\t\t\t'tax_query'      => isset( $tax ) ? $tax : '',\n\t\t\t)\n\t\t);\n\n\t\tif ( $query->have_posts() ) {\n\n\t\t\tlifterlms_course_loop_start();\n\n\t\t\twhile ( $query->have_posts() ) :\n\t\t\t\t$query->the_post();\n\n\t\t\t\tllms_get_template_part( 'content', 'course' );\n\n\t\t\tendwhile;\n\n\t\t\tlifterlms_course_loop_end();\n\n\t\t\t$courses = ob_get_clean();\n\t\t\twp_reset_postdata();\n\t\t\treturn $courses;\n\t\t}\n\t}\n\n\t/**\n\t * Output a Pricing Table anywhere a shortcode can be output\n\t *\n\t * @since 3.2.5\n\t * @since 3.23.0 Unknown.\n\t * @since 3.38.0 Use `in_array()` with strict comparison.\n\t *\n\t * @param array $atts Associative array of shortcode attributes.\n\t * @return string\n\t */\n\tpublic static function pricing_table( $atts ) {\n\n\t\t$atts = shortcode_atts(\n\t\t\tarray(\n\t\t\t\t'product' => null,\n\t\t\t),\n\t\t\t$atts,\n\t\t\t'lifterlms_pricing_table'\n\t\t);\n\n\t\t$ret = '';\n\n\t\t// get product id from loop if used from within a course or membership.\n\t\tif ( ! $atts['product'] ) {\n\t\t\t$id = get_the_ID();\n\t\t\tif ( in_array( get_post_type( $id ), array( 'course', 'llms_membership' ), true ) ) {\n\t\t\t\t$atts['product'] = get_the_ID();\n\t\t\t}\n\t\t}\n\n\t\tif ( ! empty( $atts['product'] ) && is_numeric( $atts['product'] ) ) {\n\n\t\t\t// enqueue match height for height alignments.\n\t\t\tself::enqueue_script( 'llms-jquery-matchheight' );\n\n\t\t\tob_start();\n\t\t\tlifterlms_template_pricing_table( $atts['product'] );\n\t\t\t$ret = ob_get_clean();\n\t\t}\n\n\t\t/**\n\t\t * Filters the pricing table shortcode output\n\t\t *\n\t\t * @since unknown\n\t\t *\n\t\t * @param string $ret  The shortcode output.\n\t\t * @param array  $atts Associative array of shortcode attributes.\n\t\t */\n\t\treturn apply_filters( 'llms_shortcode_pricing_table', $ret, $atts );\n\t}\n}\n\nreturn new LLMS_Shortcodes();\n"
  },
  {
    "path": "includes/shortcodes/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/spam/class-llms-akismet.php",
    "content": "<?php\n/**\n * LifterLMS Google reCAPTCHA integration.\n *\n * This class integrates Google's reCAPTCHA into LifterLMS checkout and registration forms.\n *\n * @package LifterLMS/Includes/Spam\n * @since 9.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nclass LLMS_Akismet extends LLMS_Captcha {\n\n\tuse LLMS_Trait_Singleton;\n\n\tpublic function get_slug() {\n\t\treturn 'akismet';\n\t}\n\n\tpublic function render() {\n\t\t// Add in a honey pot field to help prevent spam bots.\n\t\tif ( ! $this->is_enabled() || is_admin() ) {\n\t\t\treturn;\n\t\t}\n\t\techo '<input type=\"text\" aria-hidden=\"true\" class=\"sr-only\" name=\"llms_hp_fullname\" style=\"display:none;\" autocomplete=\"off\" />';\n\t}\n\n\tpublic function is_available() {\n\t\t// Check if the Akismet plugin is active and the API key is set.\n\t\treturn defined( 'AKISMET_VERSION' ) && class_exists( 'Akismet' ) && method_exists( 'Akismet', 'get_api_key' ) && ! empty( Akismet::get_api_key() );\n\t}\n\n\tpublic function is_enabled() {\n\n\t\treturn $this->is_available() && llms_parse_bool( get_option( 'lifterlms_akismet_enabled', false ) );\n\t}\n\n\tpublic function validate( $valid ) {\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\t// If $valid is already a truthy, return early since something else already encountered a validation issue.\n\t\tif ( $valid ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\t$data_to_check = array(\n\t\t\t'user_ip'              => sanitize_text_field( llms_get_ip_address() ),\n\t\t\t'user_agent'           => sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ),\n\t\t\t'referrer'             => sanitize_text_field( $_SERVER['HTTP_REFERER'] ),\n\t\t\t'blog'                 => get_option( 'home' ),\n\t\t\t'blog_lang'            => get_locale(),\n\t\t\t'blog_charset'         => get_option( 'blog_charset' ),\n\t\t\t'permalink'            => get_permalink(),\n\t\t\t'comment_type'         => 'signup',\n\t\t\t'comment_author'       => sanitize_text_field( $_REQUEST['email_address'] ?? wp_get_current_user()->user_email ?? '' ),\n\t\t\t'comment_author_email' => sanitize_email( $_REQUEST['email_address'] ?? wp_get_current_user()->user_email ?? '' ),\n\t\t\t'honeypot_field_name'  => 'llms_hp_fullname',\n\t\t);\n\n\t\t$response = Akismet::http_post( build_query( $data_to_check ), 'comment-check' );\n\n\t\t// If the response is empty, we can't determine if it's spam or not.\n\t\tif ( empty( $response ) ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\t$passed = true;\n\n\t\t// If the X-akismet-pro-tip is set to 'discard' return 2 as blatant spam.\n\t\tif ( ! empty( $response[0] ) ) {\n\t\t\t$headers = $response[0]->getAll();\n\t\t\tif ( isset( $headers['x-akismet-pro-tip'] ) && $headers['x-akismet-pro-tip'] === 'discard' ) {\n\t\t\t\t$passed = false;\n\t\t\t}\n\t\t}\n\n\t\t// If the response is true, return 1 as likely spam.\n\t\tif ( ! empty( $response[1] ) && $response[1] == 'true' ) {\n\t\t\t// Check if this is a free plan or paid. If it's a free plan or not a checkout, we treat it as spam.\n\t\t\t$plan_id = absint( $_POST['llms_plan_id'] ?? 0 );\n\n\t\t\tif ( ! $plan_id ) {\n\t\t\t\t// If no plan ID is provided, we assume it's open registration.\n\t\t\t\t$passed = false;\n\t\t\t}\n\n\t\t\tif ( $plan_id && ( $plan = new LLMS_Access_Plan( $plan_id ) ) && $plan->is_free() ) {\n\t\t\t\t$passed = false;\n\t\t\t}\n\t\t}\n\n\t\tif ( ! $passed ) {\n\t\t\tif ( apply_filters( 'llms_enable_spam_logs', false ) ) {\n\t\t\t\terror_log( 'LLMS_Akismet verification failed: ' . print_r( $response, true ) );\n\t\t\t}\n\n\t\t\tllms_add_notice( __( 'Suspicious activity detected. Try again in a few minutes.', 'lifterlms' ), 'error' );\n\t\t\treturn true;\n\t\t}\n\n\t\t// We're okay to proceed.\n\t\treturn $valid;\n\t}\n}\n\nreturn LLMS_Akismet::instance();\n"
  },
  {
    "path": "includes/spam/class-llms-captcha.php",
    "content": "<?php\n/**\n * LifterLMS Google reCAPTCHA integration.\n *\n * This class integrates Google's reCAPTCHA into LifterLMS checkout and registration forms.\n *\n * @package LifterLMS/Includes/Spam\n * @since 9.0.0\n */\nabstract class LLMS_Captcha {\n\n\tprotected $site_key;\n\n\tprotected $secret_key;\n\n\tpublic function __construct() {\n\t\t$slug             = sanitize_title( $this->get_slug() );\n\t\t$constant_prefix  = 'LLMS_' . strtoupper( $slug );\n\t\t$this->site_key   = defined( $constant_prefix . '_SITE_KEY' ) ? constant( $constant_prefix . '_SITE_KEY' ) : get_option( 'lifterlms_' . $slug . '_site_key' );\n\t\t$this->secret_key = defined( $constant_prefix . '_SECRET_KEY' ) ? constant( $constant_prefix . '_SECRET_KEY' ) : get_option( 'lifterlms_' . $slug . '_secret_key' );\n\n\t\tadd_action( 'llms_checkout_footer_before', array( $this, 'render' ) );\n\t\tadd_action( 'lifterlms_after_registration_fields', array( $this, 'render' ) );\n\t\tadd_action( 'lifterlms_after_free_enroll_fields', array( $this, 'render' ) );\n\n\t\tadd_action( 'llms_before_checkout_validation', array( $this, 'validate' ) );\n\t\tadd_filter( 'llms_before_registration_validation', array( $this, 'validate' ) );\n\n\t\tadd_action( 'lifterlms_after_free_enroll_fields', array( $this, 'show_notices' ) );\n\t}\n\n\t/**\n\t * Get the name of the captcha service.\n\t *\n\t * @return string\n\t */\n\tabstract public function get_slug();\n\n\t/**\n\t * Render the captcha in the footer.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return void\n\t */\n\tabstract public function render();\n\n\t/**\n\t * Validate the captcha response.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @param array $data Form data.\n\t *\n\t * @return array\n\t */\n\tabstract public function validate( $data );\n\n\t/**\n\t * Check if the captcha is enabled.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return bool\n\t */\n\tpublic function is_enabled() {\n\t\treturn $this->get_slug() === get_option( 'lifterlms_captcha' );\n\t}\n\n\t/**\n\t * Show notices if enabled.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return void\n\t */\n\tfunction show_notices() {\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_admin() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tllms_print_notices();\n\t}\n}\n"
  },
  {
    "path": "includes/spam/class-llms-recaptcha.php",
    "content": "<?php\n/**\n * LifterLMS Google reCAPTCHA integration.\n *\n * This class integrates Google's reCAPTCHA into LifterLMS checkout and registration forms.\n *\n * @package LifterLMS/Includes/Spam\n * @since 9.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nclass LLMS_Google_Recaptcha extends LLMS_Captcha {\n\n\tuse LLMS_Trait_Singleton;\n\n\tprotected $min_score;\n\n\tprotected $action;\n\n\tpublic function __construct() {\n\n\t\tparent::__construct();\n\n\t\t/**\n\t\t * Minimum score for reCAPTCHA validation.\n\t\t *\n\t\t * @since 9.0.0\n\t\t */\n\t\t$this->min_score = apply_filters( 'lifterlms_recaptcha_min_score', ( absint( get_option( 'lifterlms_recaptcha_min_score', 5 ) ) / 10 ) );\n\n\t\t/**\n\t\t * Action name for reCAPTCHA validation.\n\t\t *\n\t\t * @since 9.0.0\n\t\t */\n\t\t$this->action = apply_filters( 'lifterlms_recaptcha_action', 'submit' );\n\t}\n\n\tpublic function get_slug() {\n\t\treturn 'recaptcha';\n\t}\n\n\tpublic function render() {\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_admin() ) {\n\t\t\treturn;\n\t\t}\n\n\t\techo '<input type=\"hidden\" name=\"g-recaptcha-response\" class=\"llms-google-recaptcha g-recaptcha-response\" />';\n\n\t\twp_enqueue_script(\n\t\t\t'llms-google-recaptcha',\n\t\t\t'https://www.google.com/recaptcha/api.js?render=' . $this->site_key,\n\t\t\tarray(),\n\t\t\tnull,\n\t\t\ttrue\n\t\t);\n\t\twp_add_inline_script(\n\t\t\t'llms-google-recaptcha',\n\t\t\t'\n\n\n\t\tdocument.querySelectorAll( \"form\" ).forEach( function( form ) {\n\t\t\tif ( form.querySelector( \".llms-google-recaptcha\" ) === null ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfunction checkout_before_submit( self, callback ) {\n\t\t\t\tgrecaptcha.ready(() => {\n\t\t\t\t\tgrecaptcha.execute( \"' . esc_js( $this->site_key ) . '\", { action: \"' . esc_js( $this->action ) . '\" } ).then( token => {\n\t\t\t\t\t\tself.querySelector(\"[name=g-recaptcha-response]\").value = token;\n\t\t\t\t\t\tcallback( true );\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\tif ( window.llms && \"llms-product-purchase-form\" === form.id ) {\n\t\t\t\t// If this is the checkout form, use the before_submit method to handle reCAPTCHA.\n\t\t\t\tif ( window.llms.checkout && window.llms.checkout.add_before_submit_event ) {\n\t\t\t\t\twindow.llms.checkout.add_before_submit_event( { data: form, handler: checkout_before_submit } );\n\t\t\t\t} else {\n\t\t\t\t\tconst interval = setInterval( function() {\n\t\t\t\t\t\tif ( window.llms.checkout && window.llms.checkout.add_before_submit_event ) {\n\t\t\t\t\t\t\twindow.llms.checkout.add_before_submit_event( { data: form, handler: checkout_before_submit } );\n\t\t\t\t\t\t\tclearInterval( interval );\n\t\t\t\t\t\t}\n\t\t\t\t\t}, 100 );\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tform.addEventListener( \"submit\", function( event ) {\n\t\t\t\tevent.preventDefault();\n\n\t\t\t\tif ( form.querySelector( \".llms-password-strength-meter\" ) &&\n\t\t\t\t\twindow.LLMS &&\n\t\t\t\t\twindow.LLMS.PasswordStrength &&\n\t\t\t\t\twindow.LLMS.PasswordStrength.get_current_strength_status &&\n\t\t\t\t\t! window.LLMS.PasswordStrength.get_current_strength_status() ) {\n\t\t\t\t\tconsole.log( \"Password strength validation failed.\" );\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tgrecaptcha.ready(() => {\n\t\t\t\t\tgrecaptcha.execute( \"' . esc_js( $this->site_key ) . '\", { action: \"' . esc_js( $this->action ) . '\" } ).then( token => {\n\t\t\t\t\t\tform.querySelector( \"[name=g-recaptcha-response]\" ).value = token;\n\t\t\t\t\t\tform.submit();\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t} );\n\t\t} );\n\t\t'\n\t\t);\n\t}\n\n\tpublic function validate( $valid ) {\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\t// If $valid is already a truthy, return early since something else already encountered a validation issue.\n\t\tif ( $valid ) {\n\t\t\treturn $valid;\n\t\t}\n\t\t$token = isset( $_POST['g-recaptcha-response'] )\n\t\t\t? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) )\n\t\t\t: '';\n\n\t\tif ( ! $token ) {\n\t\t\tif ( apply_filters( 'llms_enable_recaptcha_logs', false ) ) {\n\t\t\t\terror_log( 'LifterLMS form blocked due to missing captcha' );\n\t\t\t}\n\t\t\tllms_add_notice( __( 'CAPTCHA token missing, please refresh and try again.', 'lifterlms' ), 'error' );\n\t\t\treturn true;\n\t\t}\n\n\t\t$response = wp_remote_post(\n\t\t\t'https://www.google.com/recaptcha/api/siteverify',\n\t\t\tarray(\n\t\t\t\t'body'    => array(\n\t\t\t\t\t'secret'   => $this->secret_key,\n\t\t\t\t\t'response' => $token,\n\t\t\t\t\t'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',\n\t\t\t\t),\n\t\t\t\t'timeout' => 15,\n\t\t\t)\n\t\t);\n\n\t\t$body = ! is_wp_error( $response ) ? json_decode( wp_remote_retrieve_body( $response ), true ) : null;\n\n\t\t$passed = $body\n\t\t\t\t\t&& ! empty( $body['success'] )\n\t\t\t\t\t&& $body['score'] >= $this->min_score\n\t\t\t\t\t&& ( empty( $body['action'] ) || $body['action'] === $this->action ); // action check is optional but recommended\n\n\t\tif ( ! $passed ) {\n\t\t\tif ( apply_filters( 'llms_enable_spam_logs', false ) ) {\n\t\t\t\terror_log( 'LLMS_Google_Recaptcha verification failed: ' . ( $body ? print_r( $body, true ) : '' ) );\n\t\t\t}\n\n\t\t\tllms_add_notice( __( 'CAPTCHA validation failed — please try again.', 'lifterlms' ), 'error' );\n\t\t\treturn true;\n\t\t}\n\n\t\t// We're okay to proceed.\n\t\treturn $valid;\n\t}\n}\n\nreturn LLMS_Google_Recaptcha::instance();\n"
  },
  {
    "path": "includes/spam/class-llms-turnstile.php",
    "content": "<?php\n\n/**\n * LifterLMS Turnstile integration.\n *\n * This class integrates Cloudflare's Turnstile captcha into LifterLMS checkout and registration forms.\n *\n * @package LifterLMS/Includes/Spam\n * @since 9.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nclass LLMS_Turnstile extends LLMS_Captcha {\n\n\tuse LLMS_Trait_Singleton;\n\n\tpublic function __construct() {\n\n\t\tparent::__construct();\n\n\t\tadd_action( 'wp_head', array( $this, 'add_turnstile_script' ) );\n\t}\n\n\tpublic function get_slug() {\n\t\treturn 'turnstile';\n\t}\n\n\t/**\n\t * Enqueue the Cloudflare Turnstile script.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function add_turnstile_script() {\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_admin() ) {\n\t\t\treturn;\n\t\t}\n\n\t\twp_enqueue_script( 'cloudflare-turnstile', 'https://challenges.cloudflare.com/turnstile/v0/api.js' );\n\t}\n\n\t/**\n\t * Add the Turnstile widget to the checkout and registration forms.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function render() {\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( is_admin() ) {\n\t\t\treturn;\n\t\t}\n\n\t\t?>\n\t\t<div class=\"cf-turnstile\" data-sitekey=\"<?php echo esc_attr( $this->site_key ); ?>\"></div>\n\t\t<?php\n\t}\n\n\t/**\n\t * Validate the Turnstile captcha response.\n\t *\n\t * @since 9.0.0\n\t *\n\t * @param mixed $valid The current validation status.\n\t * @return mixed True if validation fails, otherwise the original $valid value.\n\t */\n\tpublic function validate( $valid ) {\n\t\tif ( ! $this->is_enabled() ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\t// If $valid is already a truthy, return early since something else already encountered a validation issue.\n\t\tif ( $valid ) {\n\t\t\treturn $valid;\n\t\t}\n\n\t\t// If we don't have a response to test, return an error and stop registration.\n\t\t$captcha = llms_filter_input_sanitize_string( INPUT_POST, 'cf-turnstile-response' );\n\t\tif ( ! $captcha ) {\n\t\t\tif ( apply_filters( 'llms_enable_recaptcha_logs', false ) ) {\n\t\t\t\terror_log( 'LifterLMS form blocked due to missing captcha' );\n\t\t\t}\n\t\t\t// Customize the error message displayed when a registration is blocked.\n\t\t\tllms_add_notice( __( 'Blocked.', 'lifterlms' ), 'error' );\n\t\t\treturn true;\n\t\t}\n\n\t\t// Ok, try to validate the captcha.\n\t\tif ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) && filter_var( $_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP ) ) {\n\t\t\t// Use the CloudFlare IP if it exists.\n\t\t\t$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];\n\t\t} else {\n\t\t\t$ip = $_SERVER['REMOTE_ADDR'];\n\t\t}\n\t\t$url_path      = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';\n\t\t$data          = array(\n\t\t\t'secret'   => $this->secret_key,\n\t\t\t'response' => $captcha,\n\t\t\t'remoteip' => $ip,\n\t\t);\n\t\t$options       = array(\n\t\t\t'http' => array(\n\t\t\t\t'header'  => \"Content-Type: application/x-www-form-urlencoded\\r\\n\" .\n\t\t\t\t\t\t\t\"User-Agent: PHP Script\\r\\n\",\n\t\t\t\t'method'  => 'POST',\n\t\t\t\t'content' => http_build_query( $data ),\n\t\t\t),\n\t\t);\n\t\t$stream        = stream_context_create( $options );\n\t\t$result        = file_get_contents( $url_path, false, $stream );\n\t\t$response      = $result;\n\t\t$response_keys = json_decode( $response, true );\n\n\t\tif ( intval( $response_keys['success'] ) !== 1 ) {\n\t\t\tif ( apply_filters( 'llms_enable_spam_logs', false ) ) {\n\t\t\t\terror_log( 'LLMS_Turnstile verification failed: ' . print_r( $response, true ) );\n\t\t\t}\n\n\t\t\tllms_add_notice( __( 'Verification failed. Please try again.', 'lifterlms' ), 'error' );\n\t\t\treturn true;\n\t\t}\n\n\t\t// We're okay to proceed.\n\t\treturn $valid;\n\t}\n}\n\nreturn LLMS_Turnstile::instance();\n"
  },
  {
    "path": "includes/theme-support/class-llms-theme-support.php",
    "content": "<?php\n/**\n * Manage Theme Support classes\n *\n * @package LifterLMS/ThemeSupport/Classes\n *\n * @since 3.37.0\n * @version 5.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Twenty_Twenty class.\n *\n * @since 3.37.0\n */\nclass LLMS_Theme_Support {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.37.0\n\t * @since 4.3.0 Load includes during `after_setup_theme` instead of `plugins_loaded`.\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\t\tadd_action( 'after_setup_theme', array( $this, 'includes' ) );\n\t}\n\n\t/**\n\t * Retrieve formatted inline CSS for a given list of selectors and rules\n\t *\n\t * @since 4.10.0\n\t *\n\t * @param string[] $selectors       Array of CSS selectors.\n\t * @param string[] $rules           Associative array of CSS rules and properties. For example: `array( 'color' => '#fff' )`.\n\t * @param string   $selector_prefix A CSS selector to prefix each item in $selectors with.\n\t * @return string\n\t */\n\tpublic static function get_css( $selectors, $rules, $selector_prefix = '' ) {\n\n\t\t// Convert the $rules array to a list of CSS strings.\n\t\t$rules_list = array();\n\t\tforeach ( $rules as $prop => $val ) {\n\t\t\t$val = is_array( $val ) ? $val : array( $val );\n\t\t\tforeach ( $val as $value ) {\n\t\t\t\t$rules_list[] = sprintf( '%1$s: %2$s;', $prop, $value );\n\t\t\t}\n\t\t}\n\n\t\t// When supplied, prefix each selector.\n\t\tif ( $selector_prefix ) {\n\t\t\tforeach ( $selectors as &$selector ) {\n\t\t\t\t$selector = $selector_prefix . ' ' . $selector;\n\t\t\t}\n\t\t}\n\n\t\t// Return the formatted CSS.\n\t\treturn implode( ', ', $selectors ) . ' { ' . implode( ' ', $rules_list ) . ' }';\n\t}\n\n\t/**\n\t * Retrieve a list of CSS selectors for elements where the primary color is used as the background\n\t *\n\t * The primary color is a bright blue (#2295ff).\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return string[] A list of CSS selectors.\n\t */\n\tpublic static function get_selectors_primary_color_background() {\n\n\t\t/**\n\t\t * Filter the list of CSS selectors for elements where the primary color is used as the background\n\t\t *\n\t\t * @since 4.10.0\n\t\t *\n\t\t * @param string[] $selectors A list of CSS selectors.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_theme_support_get_selectors_primary_color_background',\n\t\t\tarray(\n\n\t\t\t\t// Buttons.\n\t\t\t\t'.llms-button-primary',\n\t\t\t\t'.llms-button-primary:hover',\n\t\t\t\t'.llms-button-primary.clicked',\n\t\t\t\t'.llms-button-primary:focus',\n\t\t\t\t'.llms-button-primary:active',\n\t\t\t\t'.llms-button-action',\n\t\t\t\t'.llms-button-action:hover',\n\t\t\t\t'.llms-button-action.clicked',\n\t\t\t\t'.llms-button-action:focus',\n\t\t\t\t'.llms-button-action:active',\n\n\t\t\t\t// Pricing Tables.\n\t\t\t\t'.llms-access-plan-title',\n\t\t\t\t'.llms-access-plan .stamp',\n\t\t\t\t'.llms-access-plan.featured .llms-access-plan-featured',\n\n\t\t\t\t// Checkout.\n\t\t\t\t'.llms-checkout-wrapper .llms-form-heading',\n\n\t\t\t\t// Notices.\n\t\t\t\t'.llms-notice:not(.llms-debug)',\n\n\t\t\t\t// Progress Bar.\n\t\t\t\t'.llms-progress .progress-bar-complete',\n\n\t\t\t\t// My Grades.\n\t\t\t\t'.llms-sd-widgets .llms-sd-widget .llms-sd-widget-title',\n\n\t\t\t\t// Instructor.\n\t\t\t\t'.llms-instructor-info .llms-instructors .llms-author .avatar',\n\n\t\t\t\t// Quizzes.\n\t\t\t\t'.llms-question-wrapper ol.llms-question-choices li.llms-choice input:checked + .llms-marker',\n\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve a list of CSS selectors for elements where the primary color is used as the border color\n\t *\n\t * The primary color is a bright blue (#2295ff).\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return string[] A list of CSS selectors.\n\t */\n\tpublic static function get_selectors_primary_color_border() {\n\n\t\t/**\n\t\t * Filter the list of CSS selectors for elements where the primary color is used as the border\n\t\t *\n\t\t * @since 4.10.0\n\t\t *\n\t\t * @param string[] $selectors A list of CSS selectors.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_theme_support_get_selectors_primary_color_background',\n\t\t\tarray(\n\n\t\t\t\t// Notifications.\n\t\t\t\t'.llms-notification',\n\n\t\t\t\t// Featured access plan.\n\t\t\t\t'.llms-access-plan.featured .llms-access-plan-content',\n\t\t\t\t'.llms-access-plan.featured .llms-access-plan-footer',\n\n\t\t\t\t// Checkout.\n\t\t\t\t'.llms-checkout-section',\n\t\t\t\t'.llms-checkout-wrapper form.llms-login',\n\n\t\t\t\t// Notices.\n\t\t\t\t'.llms-notice:not(.llms-debug)',\n\n\t\t\t\t// Instructor.\n\t\t\t\t'.llms-instructor-info .llms-instructors .llms-author',\n\t\t\t\t'.llms-instructor-info .llms-instructors .llms-author .avatar',\n\n\t\t\t)\n\t\t);\n\t}\n\n\n\t/**\n\t * Retrieve a list of CSS selectors for elements where the primary color is used as the text color\n\t *\n\t * The primary color is a bright blue (#2295ff).\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return string[] A list of CSS selectors.\n\t */\n\tpublic static function get_selectors_primary_color_text() {\n\n\t\t/**\n\t\t * Filter the list of CSS selectors for elements where the primary color is used as the text color\n\t\t *\n\t\t * @since 4.10.0\n\t\t *\n\t\t * @param string[] $selectors A list of CSS selectors.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t'llms_theme_support_get_selectors_primary_color_background',\n\t\t\tarray(\n\n\t\t\t\t// Pricing Tables.\n\t\t\t\t'.llms-access-plan-restrictions a',\n\t\t\t\t'.llms-access-plan-restrictions a:hover',\n\n\t\t\t\t// Loop.\n\t\t\t\t'.llms-loop-item-content .llms-loop-title:hover',\n\n\t\t\t\t// Donuts.\n\t\t\t\t'.llms-donut',\n\n\t\t\t\t// Checks on Syllabus.\n\t\t\t\t'.llms-lesson-preview.is-free .llms-lesson-complete',\n\t\t\t\t'.llms-lesson-preview.is-complete .llms-lesson-complete',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Conditionally require additional theme support classes.\n\t *\n\t * @since 3.37.0\n\t * @since 4.3.0 Method access changed to `public`.\n\t * @since 5.8.0 Added twenty-twenty-two compatibility.\n\t *\n\t * @return void\n\t */\n\tpublic function includes() {\n\n\t\tswitch ( get_template() ) {\n\n\t\t\tcase 'twentynineteen':\n\t\t\t\trequire_once 'class-llms-twenty-nineteen.php';\n\t\t\t\tbreak;\n\n\t\t\tcase 'twentytwenty':\n\t\t\t\trequire_once 'class-llms-twenty-twenty.php';\n\t\t\t\tbreak;\n\n\t\t\tcase 'twentytwentyone':\n\t\t\t\trequire_once 'class-llms-twenty-twenty-one.php';\n\t\t\t\tbreak;\n\n\t\t\tcase 'twentytwentytwo':\n\t\t\t\trequire_once 'class-llms-twenty-twenty-two.php';\n\t\t\t\tbreak;\n\n\t\t}\n\t}\n}\n\nreturn new LLMS_Theme_Support();\n"
  },
  {
    "path": "includes/theme-support/class-llms-twenty-nineteen.php",
    "content": "<?php\n/**\n * Theme Support: Twenty Nineteen\n *\n * @package LifterLMS/ThemeSupport/Classes\n *\n * @since 3.31.0\n * @version 3.31.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Twenty Nineteen Theme Support\n *\n * @since 3.31.0\n */\nclass LLMS_Twenty_Nineteen {\n\n\t/**\n\t * Static Constructor.\n\t *\n\t * @since 3.31.0\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\t// This theme doesn't have a sidebar.\n\t\tremove_action( 'lifterlms_sidebar', 'lifterlms_get_sidebar', 10 );\n\n\t\t// Handle content wrappers.\n\t\tremove_action( 'lifterlms_before_main_content', 'lifterlms_output_content_wrapper', 10 );\n\t\tremove_action( 'lifterlms_after_main_content', 'lifterlms_output_content_wrapper_end', 10 );\n\n\t\tadd_action( 'lifterlms_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 );\n\t\tadd_action( 'lifterlms_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 );\n\n\t}\n\n\t/**\n\t * Output Twentynineteen theme wrapper openers\n\t *\n\t * @since 3.31.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_content_wrapper() {\n\t\techo '<section id=\"primary\" class=\"content-area\"><main id=\"main\" class=\"site-main\"><div class=\"entry\"><div class=\"entry-content\">';\n\t}\n\n\t/**\n\t * Output Twentynineteen theme wrapper closers\n\t *\n\t * @since 3.31.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_content_wrapper_end() {\n\t\techo '</div></div></main></section>';\n\t}\n\n}\n\nreturn LLMS_Twenty_Nineteen::init();\n"
  },
  {
    "path": "includes/theme-support/class-llms-twenty-twenty-one.php",
    "content": "<?php\n/**\n * Theme Support: Twenty Twenty-One\n *\n * @package LifterLMS/ThemeSupport/Classes\n *\n * @since 4.10.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Twenty_Twenty_One class.\n *\n * @since 4.10.0\n */\nclass LLMS_Twenty_Twenty_One {\n\n\t/**\n\t * Static \"constructor\"\n\t *\n\t * @since 4.10.0\n\t * @since 6.0.0 Add `handle_certificicate_title` callback.\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\t// This theme doesn't have a sidebar.\n\t\tremove_action( 'lifterlms_sidebar', 'lifterlms_get_sidebar', 10 );\n\n\t\t// Handle content wrappers.\n\t\tremove_action( 'lifterlms_before_main_content', 'lifterlms_output_content_wrapper', 10 );\n\t\tremove_action( 'lifterlms_after_main_content', 'lifterlms_output_content_wrapper_end', 10 );\n\n\t\tadd_action( 'lifterlms_before_main_content', array( __CLASS__, 'handle_page_header_wrappers' ) );\n\n\t\t// Theme has no extra wrappers, add this class to the main list element to fix the layout.\n\t\tadd_filter( 'llms_get_loop_list_classes', array( __CLASS__, 'add_max_width_class' ) );\n\t\tadd_filter( 'llms_get_pagination_wrapper_classes', array( __CLASS__, 'add_pagination_classes' ) );\n\n\t\t// Modify catalog & checkout columns when the catalog page isn't full width.\n\t\tadd_filter( 'lifterlms_loop_columns', array( __CLASS__, 'modify_columns_count' ) );\n\t\tadd_filter( 'llms_checkout_columns', array( __CLASS__, 'modify_columns_count' ) );\n\n\t\tadd_filter( 'navigation_markup_template', array( __CLASS__, 'maybe_disable_post_navigation' ) );\n\n\t\t// Use theme colors for various LifterLMS elements.\n\t\tadd_action( 'wp_enqueue_scripts', array( __CLASS__, 'add_inline_styles' ) );\n\t\tadd_action( 'enqueue_block_editor_assets', array( __CLASS__, 'add_inline_editor_styles' ) );\n\n\t\tadd_action( 'wp', array( __CLASS__, 'handle_certificate_title' ) );\n\n\t}\n\n\t/**\n\t * Enqueue inline styles for the block editor.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function add_inline_editor_styles() {\n\t\twp_add_inline_style( 'twenty-twenty-one-custom-color-overrides', self::generate_inline_styles( 'editor' ) );\n\t}\n\n\t/**\n\t * Enqueue inline styles on the frontend\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function add_inline_styles() {\n\t\twp_add_inline_style( 'twenty-twenty-one-style', self::generate_inline_styles() );\n\t}\n\n\t/**\n\t * Adds the 2021 theme max-width class around the catalog loop element.\n\t *\n\t * This adds catalog theme support.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @param string[] $classes List of CSS classes.\n\t * @return string[]\n\t */\n\tpublic static function add_max_width_class( $classes ) {\n\t\t$classes[] = 'default-max-width';\n\t\treturn $classes;\n\t}\n\n\t/**\n\t * Add 2021 theme classes to the LLMS pagnation element\n\t *\n\t * Makes the pagination on catalogs look like the 2021 pagination on post type archives\n\t *\n\t * @since 4.10.0\n\t *\n\t * @param string[] $classes List of CSS classes.\n\t * @return string[]\n\t */\n\tpublic static function add_pagination_classes( $classes ) {\n\t\t$classes[] = 'navigation';\n\t\t$classes[] = 'pagination';\n\t\treturn $classes;\n\t}\n\n\t/**\n\t * Generate inline CSS for a given context\n\t *\n\t * @since 4.10.0\n\t *\n\t * @param string|null $context Inline CSS context. Accepts \"editor\" to define styles loaded within the block editor or `null` for frontend styles.\n\t * @return string\n\t */\n\tprotected static function generate_inline_styles( $context = null ) {\n\n\t\t$selector_prefix = ( 'editor' === $context ) ? '.editor-styles-wrapper' : '';\n\n\t\t$styles = array();\n\n\t\t// Frontend only.\n\t\tif ( is_null( $context ) ) {\n\n\t\t\t// Fix alignment of content in an access plan.\n\t\t\t$styles[] = '.llms-access-plan-description ul { padding-left: 0; }';\n\n\t\t\t// Fix checkboxes.\n\t\t\t$styles[] = '.llms-form-field.type-checkbox input { width: 25px; display: inline-block; }';\n\n\t\t\t// Hide header/footer on certificate pages.\n\t\t\t$styles[] = '.single-llms_certificate .site-header, .single-llms_my_certificate .site-header, .single-llms_certificate .widget-area, .single-llms_my_certificate .widget-area { display: none; }';\n\n\t\t\t// Question layout.\n\t\t\t$styles[] = '.llms-question-wrapper ol.llms-question-choices li.llms-choice .llms-choice-text { width: calc( 100% - 110px); }';\n\n\t\t\t// Payment gateway stylized radio buttons.\n\t\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\t\tarray( '.llms-form-field.type-radio input[type=radio]:checked+label:before' ),\n\t\t\t\tarray(\n\t\t\t\t\t'background-image' => '-webkit-radial-gradient(center,ellipse,var( --global--color-secondary ) 0,var( --global--color-secondary ) 40%,#fafafa 45%)',\n\t\t\t\t\t'background-image' => 'radial-gradient(ellipse at center,var( --global--color-secondary ) 0,var( --global--color-secondary ) 40%,#fafafa 45%)',\n\t\t\t\t)\n\t\t\t);\n\t\t\t// Darkmode fix.\n\t\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\t\tarray( '.is-dark-theme .llms-form-field.type-radio input[type=radio]:checked+label:before' ),\n\t\t\t\tarray(\n\t\t\t\t\t'background-image' => array(\n\t\t\t\t\t\t'-webkit-radial-gradient(center,ellipse,var( --global--color-background ) 0,var( --global--color-background ) 40%,#fafafa 45%)',\n\t\t\t\t\t\t'radial-gradient(ellipse at center,var( --global--color-background ) 0,var( --global--color-background ) 40%,#fafafa 45%)',\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Donuts.\n\t\t\t$styles[] = '.llms-donut svg path { stroke: var( --global--color-secondary ); }';\n\t\t\t$styles[] = '.is-dark-theme .llms-donut svg path { stroke: var( --global--color-background ); opacity: 0.5; }';\n\t\t\t$styles[] = '.is-dark-theme .llms-donut { color: var( --global--color-background ); }';\n\t\t}\n\n\t\t// Editor only.\n\t\tif ( 'editor' === $context ) {\n\n\t\t\t// Elements with a light background that become unreadable in darkmode in the block editor.\n\t\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\t\tarray(\n\t\t\t\t\t'.llms-lesson-link .llms-lesson-title',\n\t\t\t\t\t'.llms-lesson-link .llms-main > *',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'color' => 'var( --global--color-background )',\n\t\t\t\t),\n\t\t\t\t'.is-dark-theme ' . $selector_prefix\n\t\t\t);\n\t\t}\n\n\t\t$styles[] = '.llms-quiz-ui { background: transparent; }';\n\n\t\t// Fix anchor buttons.\n\t\t$styles[] = 'a.llms-button-action, a.llms-button-danger, a.llms-button-primary, a.llms-button-secondary { display: inline-block; }';\n\n\t\t// Elements with a light background that become unreadable in darkmode.\n\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\tarray(\n\t\t\t\t'.is-dark-theme .llms-notification',\n\t\t\t\t'.is-dark-theme .llms-table tbody tr:nth-child(odd) td',\n\t\t\t\t'.is-dark-theme .llms-table tbody tr:nth-child(odd) td a',\n\t\t\t\t'.is-dark-theme .llms-certificate-container',\n\t\t\t\t'.is-dark-theme a.llms-certificate',\n\t\t\t\t'.is-dark-theme .llms-instructor-info .llms-instructors',\n\t\t\t\t'.is-dark-theme .llms-achievement-loop-item.achievement-item',\n\t\t\t\t'.is-dark-theme .llms-achievement',\n\t\t\t\t'.is-dark-theme .llms-loop-item-content .llms-loop-title:hover',\n\t\t\t\t'.llms-notice a',\n\t\t\t\t'.is-dark-theme .llms-question-wrapper ol.llms-question-choices li.llms-choice .llms-marker',\n\t\t\t\t'.is-dark-theme .llms-table tbody tr:nth-child(odd) td',\n\t\t\t\t'.is-dark-theme .llms-table tbody tr:nth-child(odd) th',\n\t\t\t\t'.is-dark-theme .llms-lesson-preview.is-complete .llms-lesson-complete',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'color' => 'var( --global--color-background )',\n\t\t\t),\n\t\t\t$selector_prefix\n\t\t);\n\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\tarray(\n\t\t\t\t'.llms-checkout',\n\t\t\t\t'.llms-access-plan .llms-access-plan-footer',\n\t\t\t\t'.llms-access-plan .llms-access-plan-content',\n\t\t\t\t'.is-dark-theme .llms-progress .progress-bar-complete',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'background-color' => 'var( --global--color-background )',\n\t\t\t),\n\t\t\t$selector_prefix\n\t\t);\n\n\t\t// Add background color and color to qualifying elements.\n\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\tLLMS_Theme_Support::get_selectors_primary_color_background(),\n\t\t\tarray(\n\t\t\t\t'color'            => 'var( --global--color-background )',\n\t\t\t\t'background-color' => 'var( --global--color-secondary )',\n\t\t\t),\n\t\t\t$selector_prefix\n\t\t);\n\n\t\t// Add border color to qualifying elements.\n\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\tLLMS_Theme_Support::get_selectors_primary_color_border(),\n\t\t\tarray(\n\t\t\t\t'border-color' => 'var( --global--color-secondary )',\n\t\t\t),\n\t\t\t$selector_prefix\n\t\t);\n\n\t\t// Add color to qualifying elements.\n\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\tLLMS_Theme_Support::get_selectors_primary_color_text(),\n\t\t\tarray(\n\t\t\t\t'color' => 'var( --global--color-secondary )',\n\t\t\t),\n\t\t\t$selector_prefix\n\t\t);\n\n\t\t// Fix progress bars.\n\t\t$styles[] = '.llms-progress { color: var( --global--color-background ); }';\n\t\t$styles[] = '.is-dark-theme .llms-progress .progress-bar-complete { opacity: 0.5; }';\n\n\t\treturn implode( \"\\r\", $styles );\n\n\t}\n\n\t/**\n\t * Don't show the default \"Untitled\" post title for certificates without a title.\n\t *\n\t * Designers may opt to exclude the certificate title for aesthetic reasons, in this scenario\n\t * we should simply *not display* a title.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function handle_certificate_title() {\n\n\t\tif ( in_array( get_post_type(), array( 'llms_certificate', 'llms_my_certificate' ), true ) ) {\n\t\t\tremove_filter( 'the_title', 'twenty_twenty_one_post_title' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Handle wrapping the catalog page header in 2021 theme elements.\n\t *\n\t * This method determines if the catalog title are to be displayed and adds additional actions\n\t * which will wrap the elements in 2021 theme elements depending on what is meant to be displayed.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function handle_page_header_wrappers() {\n\n\t\t/** This filter is documented in templates/loop.php */\n\t\t$show_title = apply_filters( 'lifterlms_show_page_title', true );\n\n\t\tif ( $show_title ) {\n\t\t\tadd_action( 'lifterlms_before_main_content', array( __CLASS__, 'page_header_wrap' ), 11 );\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'page_header_wrap_end' ), 99999999 );\n\t\t}\n\n\t\tif ( $show_title && ! empty( lifterlms_get_archive_description() ) ) {\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'output_archive_description_wrapper' ), -1 );\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'output_archive_description_wrapper_end' ), 99999998 );\n\t\t}\n\n\t}\n\n\t/**\n\t * Modify the number of catalog & checkout columns.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @param int $cols Number of columns.\n\t * @return int\n\t */\n\tpublic static function modify_columns_count( $cols ) {\n\t\treturn 1;\n\t}\n\n\t/**\n\t * Disable 2021 theme post navigation on LifterLMS post types\n\t *\n\t * @since 4.10.0\n\t *\n\t * @param string $html Post navigation HTML.\n\t * @return string\n\t */\n\tpublic static function maybe_disable_post_navigation( $html ) {\n\n\t\tif ( in_array( get_post_type(), array( 'course', 'llms_membership', 'lesson', 'llms_quiz', 'llms_assignment', 'llms_group' ), true ) ) {\n\t\t\treturn '';\n\t\t}\n\t\treturn $html;\n\n\t}\n\n\t/**\n\t * Output the catalog archive description 2021 theme wrapper opener\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_archive_description_wrapper() {\n\t\techo '<div class=\"archive-description\">';\n\t}\n\n\t/**\n\t * Output the catalog archive description 2021 theme wrapper closer\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_archive_description_wrapper_end() {\n\t\techo '</div><!-- .archive-description -->';\n\t}\n\n\t/**\n\t * Output the catalog page header 2021 theme wrapper opener\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function page_header_wrap() {\n\t\techo '<header class=\"page-header alignwide\">';\n\t}\n\n\t/**\n\t * Output the catalog page header 2021 theme wrapper closer\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function page_header_wrap_end() {\n\t\techo '</header><!-- .page-header -->';\n\t}\n\n}\n\nreturn LLMS_Twenty_Twenty_One::init();\n"
  },
  {
    "path": "includes/theme-support/class-llms-twenty-twenty-two.php",
    "content": "<?php\n/**\n * LLMS_Twenty_Twenty_Two class file\n *\n * @package LifterLMS/ThemeSupport/Classes\n *\n * @since 5.8.0\n * @version 6.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Theme Support: Twenty Twenty-Two.\n *\n * @since 5.8.0\n */\nclass LLMS_Twenty_Twenty_Two {\n\n\t/**\n\t * Static \"constructor\".\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\t// This theme doesn't have a sidebar.\n\t\tremove_action( 'lifterlms_sidebar', 'lifterlms_get_sidebar', 10 );\n\n\t\t// Handle content wrappers.\n\t\tremove_action( 'lifterlms_before_main_content', 'lifterlms_output_content_wrapper', 10 );\n\t\tremove_action( 'lifterlms_after_main_content', 'lifterlms_output_content_wrapper_end', 10 );\n\n\t\tadd_action( 'lifterlms_before_main_content', array( __CLASS__, 'handle_page_header_wrappers' ) );\n\n\t\t// Modify catalog & checkout columns when the catalog page isn't full width.\n\t\tadd_filter( 'lifterlms_loop_columns', array( __CLASS__, 'modify_columns_count' ) );\n\t\tadd_filter( 'llms_checkout_columns', array( __CLASS__, 'modify_columns_count' ) );\n\n\t\t// Use theme colors for various LifterLMS elements.\n\t\tadd_action( 'wp_enqueue_scripts', array( __CLASS__, 'add_inline_styles' ) );\n\t\tadd_action( 'enqueue_block_editor_assets', array( __CLASS__, 'add_inline_editor_styles' ) );\n\n\t}\n\n\t/**\n\t * Enqueue inline styles for the block editor.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function add_inline_editor_styles() {\n\t\twp_add_inline_style( 'llms-blocks-editor', self::generate_inline_styles( 'editor' ) );\n\t}\n\n\t/**\n\t * Enqueue inline styles on the frontend.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function add_inline_styles() {\n\t\twp_add_inline_style( 'twentytwentytwo-style', self::generate_inline_styles() );\n\t}\n\n\t/**\n\t * Generate inline CSS for a given context.\n\t *\n\t * @since 5.8.0\n\t * @since 5.9.0 Fixed stretched images in questions with pictures, and images in quiz/questions description.\n\t * @since 6.8.0 Fixed label/text alignment by removing text’s margin top. Also, removed now outdated width rule.\n\t *\n\t * @param string|null $context Inline CSS context. Accepts \"editor\" to define styles loaded within the block editor or `null` for frontend styles.\n\t * @return string\n\t */\n\tprotected static function generate_inline_styles( $context = null ) {\n\n\t\t$selector_prefix = ( 'editor' === $context ) ? '.editor-styles-wrapper' : '';\n\n\t\t$styles = array();\n\n\t\t// Frontend only.\n\t\tif ( is_null( $context ) ) {\n\n\t\t\t// Fix alignment of content in an access plan, and navigation.\n\t\t\t$styles[] = '.llms-access-plan-description ul, .llms-pagination ul { padding-left: 0; }';\n\n\t\t\t// Fix form input padding.\n\t\t\t$styles[] = '.llms-form-field input, .llms-form-field textarea, .llms-form-field select { padding: 6px 10px }';\n\n\t\t\t// Question layout.\n\t\t\t$styles[] = '.llms-question-wrapper ol.llms-question-choices li.llms-choice .llms-choice-text { margin-top: 0; }';\n\n\t\t\t// Payment gateway stylized radio buttons.\n\t\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\t\tarray( '.llms-form-field.type-radio:not(.is-group) input[type=radio]:checked+label:before' ),\n\t\t\t\tarray(\n\t\t\t\t\t'background-image' => '-webkit-radial-gradient(center,ellipse,var(--wp--preset--color--primary) 0,var(--wp--preset--color--primary) 40%,#fafafa 45%)',\n\t\t\t\t\t'background-image' => 'radial-gradient(ellipse at center,var(--wp--preset--color--primary) 0,var(--wp--preset--color--primary) 40%,#fafafa 45%)',\n\t\t\t\t)\n\t\t\t);\n\t\t\t// Completed lesson check.\n\t\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\t\tarray(\n\t\t\t\t\t'.llms-lesson-preview.is-free .llms-lesson-complete',\n\t\t\t\t\t'.llms-lesson-preview.is-complete .llms-lesson-complete',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'color' => 'var(--wp--preset--color--primary)',\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Editor only.\n\t\tif ( 'editor' === $context ) {\n\n\t\t\t// Elements with a light background that become unreadable in darkmode in the block editor.\n\t\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\t\tarray(\n\t\t\t\t\t'.wp-block-llms-course-progress .progress-bar .progress--fill',\n\t\t\t\t\t'.wp-block[data-type=\"llms/course-continue-button\"] button',\n\t\t\t\t\t'.wp-block[data-type=\"llms/lesson-progression\"] button',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'background-color' => 'var(--wp--preset--color--primary)',\n\t\t\t\t\t'color'            => 'var(--wp--preset--color--background)',\n\t\t\t\t),\n\t\t\t\t$selector_prefix\n\t\t\t);\n\n\t\t}\n\n\t\t// Fix lesson preview titles.\n\t\t$styles[] = '.llms-lesson-preview h6 { margin: 0 0 10px; }';\n\n\t\t// Primary background color.\n\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\tLLMS_Theme_Support::get_selectors_primary_color_background(),\n\t\t\tarray(\n\t\t\t\t'background-color' => 'var(--wp--preset--color--primary)',\n\t\t\t\t'color'            => 'var(--wp--preset--color--background)',\n\t\t\t)\n\t\t);\n\n\t\t// Add border color to qualifying elements.\n\t\t$styles[] = LLMS_Theme_Support::get_css(\n\t\t\tLLMS_Theme_Support::get_selectors_primary_color_border(),\n\t\t\tarray(\n\t\t\t\t'border-color' => 'var(--wp--preset--color--primary)',\n\t\t\t)\n\t\t);\n\n\t\t// Quiz.\n\t\t$styles[] = '.llms-quiz-ui { background: transparent; }';\n\t\t// Fix questions with pictures, and images in quiz/questions description.\n\t\t$styles[] = '.llms-quiz-wrapper img, .llms-quiz-question-wrapper img { max-width: 100%; height: auto; }';\n\n\t\t// Fix anchor buttons.\n\t\t$styles[] = 'a.llms-button-action, a.llms-button-danger, a.llms-button-primary, a.llms-button-secondary { display: inline-block; }';\n\n\t\treturn implode( \"\\r\", $styles );\n\n\t}\n\n\t/**\n\t * Handle wrapping the catalog page header in 2022 theme elements.\n\t *\n\t * This method determines if the catalog title are to be displayed and adds additional actions\n\t * which will wrap the elements in 2022 theme elements depending on what is meant to be displayed.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function handle_page_header_wrappers() {\n\n\t\t/** This filter is documented in templates/loop.php */\n\t\t$show_title = apply_filters( 'lifterlms_show_page_title', true );\n\n\t\tif ( $show_title ) {\n\t\t\tadd_action( 'lifterlms_before_main_content', array( __CLASS__, 'page_header_wrap' ), 11 );\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'page_header_wrap_end' ), 99999999 );\n\t\t}\n\n\t\tif ( $show_title && ! empty( lifterlms_get_archive_description() ) ) {\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'output_archive_description_wrapper' ), -1 );\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'output_archive_description_wrapper_end' ), 99999998 );\n\t\t}\n\n\t}\n\n\t/**\n\t * Modify the number of catalog & checkout columns.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @param int $cols Number of columns.\n\t * @return int\n\t */\n\tpublic static function modify_columns_count( $cols ) {\n\t\treturn 1;\n\t}\n\n\t/**\n\t * Output the catalog archive description 2022 theme wrapper opener.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_archive_description_wrapper() {\n\t\techo '<div class=\"archive-description\">';\n\t}\n\n\t/**\n\t * Output the catalog archive description 2022 theme wrapper closer.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_archive_description_wrapper_end() {\n\t\techo '</div><!-- .archive-description -->';\n\t}\n\n\t/**\n\t * Output the catalog page header 2022 theme wrapper opener.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function page_header_wrap() {\n\t\techo '<header class=\"page-header alignwide\">';\n\t}\n\n\t/**\n\t * Output the catalog page header 2022 theme wrapper closer.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic static function page_header_wrap_end() {\n\t\techo '</header><!-- .page-header -->';\n\t}\n\n}\n\nreturn LLMS_Twenty_Twenty_Two::init();\n"
  },
  {
    "path": "includes/theme-support/class-llms-twenty-twenty.php",
    "content": "<?php\n/**\n * Theme Support: Twenty Twenty\n *\n * @package LifterLMS/ThemeSupport/Classes\n *\n * @since 3.37.0\n * @version 4.10.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Twenty_Twenty class..\n *\n * @since 3.37.0\n * @since 3.37.1 Fixed course information block misalignment.\n * @since 3.37.2 Updated to use `background-color` property instead of `background` shorthand\n *               when adding custom elements to style.\n * @since 3.37.3 Hide site header and footer, and set a white body background in\n *               single certificates.\n */\nclass LLMS_Twenty_Twenty {\n\n\t/**\n\t * Constructor.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic static function init() {\n\n\t\t// This theme doesn't have a sidebar.\n\t\tremove_action( 'lifterlms_sidebar', 'lifterlms_get_sidebar', 10 );\n\n\t\t// Handle content wrappers.\n\t\tremove_action( 'lifterlms_before_main_content', 'lifterlms_output_content_wrapper', 10 );\n\t\tremove_action( 'lifterlms_after_main_content', 'lifterlms_output_content_wrapper_end', 10 );\n\n\t\tadd_action( 'lifterlms_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 );\n\t\tadd_action( 'lifterlms_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 );\n\n\t\t// Add the proper Twenty Twenty Body class on the catalogs.\n\t\tadd_filter( 'body_class', array( __CLASS__, 'body_classes' ) );\n\n\t\t// Modify catalog & checkout columns when the catalog page isn't full width.\n\t\tadd_filter( 'lifterlms_loop_columns', array( __CLASS__, 'modify_columns_count' ) );\n\t\tadd_filter( 'llms_checkout_columns', array( __CLASS__, 'modify_columns_count' ) );\n\n\t\t// Prevent meta output for LifterLMS custom Post Types.\n\t\tadd_filter( 'twentytwenty_disallowed_post_types_for_meta_output', array( __CLASS__, 'hide_meta_output' ) );\n\n\t\tadd_filter( 'twentytwenty_get_elements_array', array( __CLASS__, 'add_elements' ) );\n\n\t\tadd_action( 'wp_head', array( __CLASS__, 'add_inline_styles' ), 100 );\n\t}\n\n\t/**\n\t * Generate inline CSS using colors from the TwenyTwenty Theme settings.\n\t *\n\t * @since 3.37.0\n\t * @since 3.37.1 Fixed course information block misalignment.\n\t * @since 3.37.3 Hide site header and footer, and set a white body background in single certificates.\n\t *\n\t * @return void\n\t */\n\tpublic static function add_inline_styles() {\n\n\t\tglobal $post_type;\n\t\t$accent = twentytwenty_get_color_for_area( 'content', 'accent' );\n\n\t\t?>\n\t\t<style id=\"llms-twentytweny-style\">\n\n\t\t.llms-access-plan.featured .llms-access-plan-content,\n\t\t.llms-access-plan.featured .llms-access-plan-footer {\n\t\t\tborder-left-color: <?php echo sanitize_hex_color( $accent ); ?>;\n\t\t\tborder-right-color: <?php echo sanitize_hex_color( $accent ); ?>;\n\t\t}\n\t\t.llms-access-plan.featured .llms-access-plan-footer {\n\t\t\tborder-bottom-color: <?php echo sanitize_hex_color( $accent ); ?>;\n\t\t}\n\t\t.llms-form-field.type-radio input[type=radio]:checked+label:before {\n\t\t\tbackground-image: -webkit-radial-gradient(center,ellipse,<?php echo sanitize_hex_color( $accent ); ?> 0,<?php echo sanitize_hex_color( $accent ); ?> 40%,#fafafa 45%);\n\t\t\tbackground-image: radial-gradient(ellipse at center,<?php echo sanitize_hex_color( $accent ); ?> 0,<?php echo sanitize_hex_color( $accent ); ?> 40%,#fafafa 45%);\n\t\t}\n\t\t.llms-checkout-section,\n\t\t.llms-lesson-preview section.llms-main  {\n\t\t\tpadding-bottom: 0;\n\t\t\tpadding-top: 0;\n\t\t}\n\t\t.llms-lesson-link .llms-pre-text,\n\t\t.llms-access-plan .llms-access-plan-title {\n\t\t\tmargin-top: 0;\n\t\t}\n\t\t.llms-donut svg path {\n\t\t\tstroke: <?php echo sanitize_hex_color( $accent ); ?>;\n\t\t}\n\t\t.llms-notification,\n\t\t.llms-instructor-info .llms-instructors .llms-author {\n\t\t\tborder-top-color: <?php echo sanitize_hex_color( $accent ); ?>;\n\t\t}\n\t\t.llms-pagination ul li:first-of-type,\n\t\t.llms-pagination ul {\n\t\t\tmargin-left: 0;\n\t\t\tmargin-right: 0;\n\t\t}\n\t\t.course .llms-meta-info {\n\t\t\tmargin-left: auto;\n\t\t\tmargin-right: auto;\n\t\t}\n\t\t<?php if ( 'llms_my_certificate' === $post_type || 'llms_certificate' === $post_type ) : ?>\n\t\tbody {\n\t\t\tbackground-color: #fff;\n\t\t\tbackground-image: none;\n\t\t}\n\t\t#site-header,\n\t\t#site-footer {\n\t\t\tdisplay: none;\n\t\t}\n\t\t<?php endif; ?>\n\t\t</style>\n\t\t<?php\n\t}\n\n\t/**\n\t * Add LifterLMS Elments to the array of Twenty Twenty elements.\n\t *\n\t * This is used to automatically generate inline CSS via the Twenty Twenty Theme.\n\t *\n\t * @since 3.37.0\n\t * @since 3.37.2 Updated to use `background-color` property instead of `background` shorthand.\n\t *\n\t * @param array $elements Multidimensional array of CSS selectors.\n\t * @return array\n\t */\n\tpublic static function add_elements( $elements ) {\n\n\t\t// Accent Background.\n\t\t$elements['content']['accent']['background-color'] = array_merge(\n\t\t\t$elements['content']['accent']['background-color'],\n\t\t\tself::add_elements_content_accent_background()\n\t\t);\n\n\t\t// Accent Border Color.\n\t\t$elements['content']['accent']['border-color'] = array_merge(\n\t\t\t$elements['content']['accent']['border-color'],\n\t\t\tself::add_elements_content_accent_border()\n\t\t);\n\n\t\t// Accent Color.\n\t\t$elements['content']['accent']['color'] = array_merge(\n\t\t\t$elements['content']['accent']['color'],\n\t\t\tself::add_elements_content_accent_color()\n\t\t);\n\n\t\t// Background Text Color.\n\t\t$elements['content']['background']['color'] = array_merge(\n\t\t\t$elements['content']['background']['color'],\n\t\t\tself::add_elements_content_background_color()\n\t\t);\n\n\t\t// Background Background Color.\n\t\t$elements['content']['background']['background-color'] = array_merge(\n\t\t\t$elements['content']['background']['background-color'],\n\t\t\tarray( '.llms-checkout' )\n\t\t);\n\n\t\t// Text Color.\n\t\t$elements['content']['text']['color'] = array_merge(\n\t\t\t$elements['content']['text']['color'],\n\t\t\tarray(\n\t\t\t\t'.llms-notice.llms-debug',\n\t\t\t\t'.llms-notice.llms-debug a',\n\t\t\t)\n\t\t);\n\n\t\treturn $elements;\n\t}\n\n\t/**\n\t * Get an array of selectors for items that have the accent color as the background.\n\t *\n\t * @since 3.37.0\n\t * @since 4.10.0 Use LLMS_Theme_Support utility classes.\n\t *\n\t * @return string[]\n\t */\n\tprotected static function add_elements_content_accent_background() {\n\t\treturn LLMS_Theme_Support::get_selectors_primary_color_background();\n\t}\n\n\t/**\n\t * Get an array of selectors for items that have the accent color as the border.\n\t *\n\t * @since 3.37.0\n\t * @since 4.10.0 Use LLMS_Theme_Support utility classes.\n\t *\n\t * @return string[]\n\t */\n\tprotected static function add_elements_content_accent_border() {\n\t\treturn LLMS_Theme_Support::get_selectors_primary_color_border();\n\t}\n\n\t/**\n\t * Get an array of selectors for items that have the accent color as the text color.\n\t *\n\t * @since 3.37.0\n\t * @since 4.10.0 Use LLMS_Theme_Support utility classes.\n\t *\n\t * @return string[]\n\t */\n\tprotected static function add_elements_content_accent_color() {\n\t\treturn LLMS_Theme_Support::get_selectors_primary_color_text();\n\t}\n\n\t/**\n\t * Get an array of selectors for items that have the background color as the text color.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return string[]\n\t */\n\tprotected static function add_elements_content_background_color() {\n\n\t\treturn array(\n\n\t\t\t// Buttons.\n\t\t\t'.llms-button-primary',\n\t\t\t'.llms-button-primary:hover',\n\t\t\t'.llms-button-primary.clicked',\n\t\t\t'.llms-button-primary:focus',\n\t\t\t'.llms-button-primary:active',\n\t\t\t'.llms-button-action',\n\t\t\t'.llms-button-action:hover',\n\t\t\t'.llms-button-action.clicked',\n\t\t\t'.llms-button-action:focus',\n\t\t\t'.llms-button-action:active',\n\n\t\t\t// Pricing Tables.\n\t\t\t'.llms-access-plan-title',\n\t\t\t'.llms-access-plan .stamp',\n\t\t\t'.llms-access-plan.featured .llms-access-plan-featured',\n\n\t\t\t// Checkout.\n\t\t\t'.llms-checkout-wrapper .llms-form-heading',\n\n\t\t\t// Notices.\n\t\t\t'.llms-notice',\n\t\t\t'.llms-notice a',\n\n\t\t\t// My Grades.\n\t\t\t'.llms-sd-widgets .llms-sd-widget .llms-sd-widget-title',\n\n\t\t);\n\t}\n\n\t/**\n\t * Add Twenty Twenty's full-width template body class on catalogs where the page is set to use the Full Width template.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @param string[] $classes Array of body classes.\n\t * @return string[]\n\t */\n\tpublic static function body_classes( $classes ) {\n\n\t\t$page_id = self::get_archive_page_id();\n\t\tif ( $page_id && self::is_page_full_width( $page_id ) ) {\n\t\t\t$classes[] = 'template-full-width';\n\t\t}\n\n\t\treturn $classes;\n\t}\n\n\t/**\n\t * Retrieve the page ID of a a catalog page.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return int|false\n\t */\n\tprotected static function get_archive_page_id() {\n\n\t\t$page_id = false;\n\n\t\tif ( is_courses() ) {\n\t\t\t$page_id = llms_get_page_id( 'courses' );\n\t\t} elseif ( is_memberships() ) {\n\t\t\t$page_id = llms_get_page_id( 'memberships' );\n\t\t}\n\n\t\treturn $page_id;\n\t}\n\n\t/**\n\t * Get the twenty twenty theme's \"width\" class for use in wrapper elements.\n\t *\n\t * If the \"Full Width\" template is utilized, there's no class, otherwise the class `thin` is used.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return string\n\t */\n\tprotected static function get_page_template_class() {\n\n\t\t$template_class = 'thin';\n\t\t$page_id        = self::get_archive_page_id();\n\n\t\tif ( $page_id ) {\n\t\t\t$template_class = self::is_page_full_width( $page_id ) ? '' : 'thin';\n\t\t} else {\n\t\t\t$template_class = is_page_template( 'templates/template-full-width.php' ) ? '' : 'thin';\n\t\t}\n\n\t\treturn $template_class;\n\t}\n\n\t/**\n\t * Prevent theme meta information from being output on LifterLMS Custom Post Types.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @param string[] $post_types Array of post type names.\n\t * @return string[]\n\t */\n\tpublic static function hide_meta_output( $post_types ) {\n\n\t\treturn array_merge( $post_types, array( 'course', 'llms_membership', 'lesson', 'llms_quiz' ) );\n\t}\n\n\t/**\n\t * Determine if the given page is utilizing the twenty twenty full-width page template.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @param int $page_id WP_Post ID of the catalog page.\n\t * @return bool\n\t */\n\tprotected static function is_page_full_width( $page_id ) {\n\n\t\treturn 'templates/template-full-width.php' === get_page_template_slug( $page_id );\n\t}\n\n\t/**\n\t * Modify the number of catalog & checkout columns.\n\t *\n\t * If the default template is used, drop to a single column.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @param int $cols Number of columns.\n\t * @return int\n\t */\n\tpublic static function modify_columns_count( $cols ) {\n\n\t\tif ( 'thin' === self::get_page_template_class() ) {\n\t\t\treturn 1;\n\t\t}\n\n\t\treturn $cols;\n\t}\n\n\t/**\n\t * Output the opening wrapper for the content description element in the theme's header.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_archive_description_wrapper() {\n\t\techo '<div class=\"archive-subtitle section-inner thin max-percentage intro-text\">';\n\t}\n\n\t/**\n\t * Output the closing wrapper for the content description element in the theme's header.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_archive_description_wrapper_end() {\n\t\techo '</div><!-- .archive-subtitle -->';\n\t}\n\n\t/**\n\t * Output Twenty Twenty theme wrapper openers\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_content_wrapper() {\n\n\t\t$show_title = apply_filters( 'lifterlms_show_page_title', true );\n\t\t$has_desc   = has_action( 'lifterlms_archive_description' );\n\n\t\tif ( $has_desc ) {\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'output_archive_description_wrapper' ), -1 );\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'output_archive_description_wrapper_end' ), 99999999 );\n\t\t}\n\n\t\tif ( $show_title ) {\n\t\t\tadd_filter( 'lifterlms_show_page_title', '__return_false' );\n\t\t}\n\n\t\t?>\n\t\t<main id=\"site-content\" role=\"main\">\n\n\t\t\t<?php if ( $show_title || $has_desc ) : ?>\n\t\t\t\t<header class=\"archive-header has-text-align-center header-footer-group\">\n\n\t\t\t\t\t<div class=\"archive-header-inner section-inner medium\">\n\t\t\t\t\t\t<?php if ( $show_title ) : ?>\n\t\t\t\t\t\t\t<h1 class=\"archive-title\"><?php lifterlms_page_title(); ?></h1>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t<?php endif; ?>\n\t\t<?php\n\n\t\t// If there's no description, output the end wrapper now.\n\t\tif ( $show_title && ! $has_desc ) {\n\t\t\tself::output_content_wrapper_part_two();\n\t\t} else {\n\t\t\t// Otherwise output the wrapper after the end wrapper for the description wrapper div.\n\t\t\tadd_action( 'lifterlms_archive_description', array( __CLASS__, 'output_content_wrapper_part_two' ), 99999999 );\n\t\t}\n\t}\n\n\t/**\n\t * Outputs header closing wrappers and inner element opening wrappers for the theme wrappers.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_content_wrapper_part_two() {\n\t\t?>\n\t\t\t</div><!-- .archive-header-inner -->\n\t\t</header><!-- .archive-header -->\n\t\t<article <?php post_class(); ?> id=\"post-<?php the_ID(); ?>\">\n\t\t\t<div class=\"post-inner section-inner <?php echo esc_attr( self::get_page_template_class() ); ?> \">\n\t\t\t\t<div class=\"entry-content\">\n\t\t<?php\n\t}\n\n\t/**\n\t * Output Twenty Twenty theme wrapper closers\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic static function output_content_wrapper_end() {\n\t\t?>\n\t\t\t\t\t</div><!-- .entry-content -->\n\t\t\t\t</div><!-- .post-inner -->\n\t\t\t</article><!-- .post -->\n\t\t</main><!-- #site-content -->\n\t\t<?php\n\t}\n}\n\nreturn LLMS_Twenty_Twenty::init();\n"
  },
  {
    "path": "includes/theme-support/index.php",
    "content": "<?php // silence.\n"
  },
  {
    "path": "includes/traits/llms-trait-audio-video-embed.php",
    "content": "<?php\n/**\n * LifterLMS audio video embed trait\n *\n * @package LifterLMS/Traits\n *\n * @since 5.3.0\n * @version 5.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS audio video embed trait.\n *\n * **Classes that use this trait must call {@see LLMS_Trait_Audio_Video_Embed::construct_audio_video_embed()}\n * in their constructor.**\n *\n * @since 5.3.0\n *\n * @property string $audio_embed URL to an oEmbed enable audio URL.\n * @property string $video_embed URL to an oEmbed enable video URL.\n */\ntrait LLMS_Trait_Audio_Video_Embed {\n\t/**\n\t * @inheritdoc\n\t */\n\tabstract protected function add_properties( $props = array() );\n\n\t/**\n\t * Setup properties used by this trait.\n\t *\n\t * **Must be called by the constructor of the class that uses this trait.**\n\t *\n\t * @since 5.3.0\n\t */\n\tprotected function construct_audio_video_embed() {\n\n\t\t$this->add_properties(\n\t\t\tarray(\n\t\t\t\t'audio_embed' => 'url',\n\t\t\t\t'video_embed' => 'url',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * @inheritdoc\n\t */\n\tabstract public function get( $key, $raw = false );\n\n\t/**\n\t * Attempt to get oEmbed for an audio provider.\n\t *\n\t * Falls back to the [audio] shortcode if the oEmbed fails.\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.0 Unknown.\n\t * @since 5.3.0 Refactored from `LLMS_Course` and `LLMS_Lesson`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_audio() {\n\t\treturn $this->get_embed( 'audio' );\n\t}\n\n\t/**\n\t * @inheritdoc\n\t */\n\tabstract protected function get_embed( $type = 'video', $prop = '' );\n\n\t/**\n\t * Attempt to get oEmbed for a video provider.\n\t *\n\t * Falls back to the [video] shortcode if the oEmbed fails.\n\t *\n\t * @since 1.0.0\n\t * @since 3.17.0 Unknown.\n\t * @since 5.3.0 Refactored from `LLMS_Course` and `LLMS_Lesson`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_video() {\n\t\treturn $this->get_embed( 'video' );\n\t}\n}\n"
  },
  {
    "path": "includes/traits/llms-trait-award-default-images.php",
    "content": "<?php\n/**\n * LLMS_Trait_Award_Default_Images\n *\n * @package LifterLMS/Traits\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Default image getters for LifterLMS awards.\n *\n * Classes that utilize this trait should declare a protected class property `$award_type`, which\n * is used to define the award's type ID. Core supported types are 'achievement' and 'certificate'.\n *\n * @since 6.0.0\n */\ntrait LLMS_Trait_Award_Default_Images {\n\n\t/**\n\t * Retrieve the default image source for the given engagement type.\n\t *\n\t * The method name is not a typo. This retrieves the default image used when the default image\n\t * option is not set or doesn't exist. Thus, it retrieves the default default. Thumbs up.\n\t *\n\t * This method retrieves the image stored in the plugin's assets directory. The images\n\t * were updated with the release of this method and allows usage of the previous version's\n\t * images via the filter {@see llms_use_legacy_engagement_images}.\n\t *\n\t * The legacy default image URL is returned if the current certificate version is 1 and the\n\t * 'llms_has_achievements_with_legacy_default_image' option is 'yes' or the type is achievement and the\n\t * 'llms_has_certificates_with_legacy_default_image' option is 'yes. These options are set when LifterLMS is updated\n\t * to 6.0.0 and there are legacy user engagements ({@see \\LLMS\\Updates\\Version_6_0_0\\_add_legacy_opt()}).\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return string The URL to the default image.\n\t */\n\tprotected function get_default_default_image_src() {\n\n\t\t$img        = \"default-{$this->award_type}.png\";\n\t\t$use_legacy = false;\n\n\t\tswitch ( $this->award_type ) {\n\t\t\tcase 'achievement':\n\t\t\t\t$use_legacy = llms_parse_bool( get_option( 'llms_has_achievements_with_legacy_default_image', 'no' ) );\n\t\t\t\tbreak;\n\t\t\tcase 'certificate':\n\t\t\t\t$certificate = llms_get_certificate( null, true );\n\t\t\t\tif ( $certificate && 1 === $certificate->get_template_version() ) {\n\t\t\t\t\t$use_legacy = llms_parse_bool( get_option( 'llms_has_certificates_with_legacy_default_image', 'no' ) );\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\n\t\t/**\n\t\t * Filter whether or not the legacy default images should be used for achievement and certificates.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @example add_filter( 'llms_use_legacy_award_images', '__return_true' );\n\t\t *\n\t\t * @param boolean $use_legacy If `true`, the legacy image will be used.\n\t\t * @param string  $award_type The type of award, either \"achievement\" or \"certificate\".\n\t\t */\n\t\tif ( apply_filters( 'llms_use_legacy_award_images', $use_legacy, $this->award_type ) ) {\n\t\t\t$img = \"optional_{$this->award_type}.png\";\n\t\t}\n\n\t\treturn llms()->plugin_url() . '/assets/images/' . $img;\n\n\t}\n\n\t/**\n\t * Retrieve the default image for a given object.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $object_id WP_Post ID of the earned achievement. This is passed so that anyone filtering the default image could\n\t *                       provide a different default image based on the achievement.\n\t * @return string The full image source url.\n\t */\n\tpublic function get_default_image( $object_id ) {\n\n\t\t$src = '';\n\n\t\t// Retrieve the stored value from the database.\n\t\t$id = $this->get_default_image_id();\n\t\tif ( $id ) {\n\t\t\t$src = wp_get_attachment_url( $id );\n\t\t}\n\n\t\t// Use the attachment stored for the option in the DB and fallback to the default image from the plugin's assets dir.\n\t\t$src = $src ? $src : $this->get_default_default_image_src();\n\n\t\t/**\n\t\t * Filters the default image source for an award.\n\t\t *\n\t\t * The dynamic portion of this hook, {$this->award_type}, refers to the award type, either \"achievement\" or \"certificate\".\n\t\t *\n\t\t * @since 2.2.0\n\t\t * @since 6.0.0 Merged achievement and certificate filters into a single dynamic filter.\n\t\t *\n\t\t * @param string $src       The full image source url.\n\t\t * @param int    $object_id The WP_Post ID of the award.\n\t\t */\n\t\treturn apply_filters(\n\t\t\t\"lifterlms_{$this->award_type}_image_placeholder_src\",\n\t\t\t$src,\n\t\t\t$object_id\n\t\t);\n\t}\n\n\t/**\n\t * Retrieve attachment ID of the default achievement image.\n\t *\n\t * If the attachment post doesn't exist will return false. This would happen\n\t * if the post is deleted from the media library.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int Returns the WP_Post ID of the attachment or `0` if not set.\n\t */\n\tpublic function get_default_image_id() {\n\t\t$id = get_option( \"lifterlms_{$this->award_type}_default_img\", 0 );\n\t\treturn $id && get_post( $id ) ? absint( $id ) : 0;\n\t}\n\n}\n"
  },
  {
    "path": "includes/traits/llms-trait-award-templates-post-list-table.php",
    "content": "<?php\n/**\n * LifterLMS Certificate/Achievement Templates Post List Table trait\n *\n * @package LifterLMS/Traits\n *\n * @since 6.0.0\n * @version 6.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Certificate/Achievement Templates Post List Table trait.\n *\n * @since 6.0.0\n */\ntrait LLMS_Trait_Award_Templates_Post_List_Table {\n\n\t/**\n\t * Add post row actions filter callback.\n\t *\n\t * @since 6.0.0\n\t * @since 6.4.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n\t *\n\t * @return void\n\t */\n\tprotected function award_template_row_actions() {\n\n\t\tif ( \"llms_{$this->engagement_type}\" === llms_filter_input( INPUT_GET, 'post_type' ) ) {\n\t\t\tadd_filter( 'post_row_actions', array( $this, 'add_post_actions' ), 20, 2 );\n\t\t}\n\n\t}\n\n\t/**\n\t * Add post row actions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array   $actions Array of post row actions.\n\t * @param WP_Post $post    Post object for the row.\n\t * @return array\n\t */\n\tpublic function add_post_actions( $actions, $post ) {\n\n\t\tif ( ! $post || \"llms_{$this->engagement_type}\" !== $post->post_type ) {\n\t\t\treturn $actions;\n\t\t}\n\n\t\t$post_type_object = get_post_type_object( $post->post_type );\n\t\tif ( ! $post_type_object->show_ui ) {\n\t\t\treturn $actions;\n\t\t}\n\n\t\t$award_post_type = str_replace( 'llms_', 'llms_my_', $post->post_type );\n\n\t\t$actions['llms-awards-list'] = sprintf(\n\t\t\t'<a href=\"%1$s\">%2$s</a>',\n\t\t\tadd_query_arg(\n\t\t\t\tarray(\n\t\t\t\t\tLLMS_Admin_Post_Table_Awards::TEMPLATE_FILTER_QUERY_VAR => $post->ID,\n\t\t\t\t\t'post_type' => $award_post_type,\n\t\t\t\t),\n\t\t\t\tadmin_url( 'edit.php' )\n\t\t\t),\n\t\t\tsprintf(\n\t\t\t\t// Translators: %1$s the awarded post type name label.\n\t\t\t\t__( 'View %1$s', 'lifterlms' ),\n\t\t\t\tget_post_type_labels(\n\t\t\t\t\tget_post_type_object( $award_post_type )\n\t\t\t\t)->name\n\t\t\t)\n\t\t);\n\n\t\t$sync_action = \"sync_awarded_{$this->engagement_type}s\";\n\t\t$sync_url    = add_query_arg(\n\t\t\tarray(\n\t\t\t\t'action' => $sync_action,\n\t\t\t\t\"_llms_{$this->engagement_type}_sync_actions_nonce\" => wp_create_nonce( \"llms-{$this->engagement_type}-sync-actions\" ),\n\t\t\t),\n\t\t\tget_edit_post_link( $post, 'raw' )\n\t\t);\n\n\t\t$text = sprintf(\n\t\t\t/* translators: %1$s: the plural awarded post type name label */\n\t\t\t__( 'Sync %1$s', 'lifterlms' ),\n\t\t\tget_post_type_labels( get_post_type_object( $award_post_type ) )->name\n\t\t);\n\n\t\t$actions[ $sync_action ] = '<a href=\"' . esc_html( $sync_url ) . '\">' . $text . '</a>';\n\n\t\treturn $actions;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/traits/llms-trait-earned-engagement-reporting-table.php",
    "content": "<?php\n/**\n * LifterLMS Eearned Engagements (Certificate/Achievement) Reporting Table trait.\n *\n * @package LifterLMS/Traits\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Eearned Engagements (Certificate/Achievement) Reporting Table trait.\n *\n * This trait should only be used by classes that extend from the {@see LLMS_Admin_Table} class.\n *\n * @since 6.0.0\n */\ntrait LLMS_Trait_Earned_Engagement_Reporting_Table {\n\n\t/**\n\t * Add award engagement button above the table.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function output_table_html() {\n\n\t\t$post_type = null;\n\t\tif ( 'certificates' === $this->id ) {\n\t\t\t$post_type = 'llms_my_certificate';\n\t\t} elseif ( 'achievements' === $this->id ) {\n\t\t\t$post_type = 'llms_my_achievement';\n\t\t}\n\t\tif ( empty( $post_type ) ) {\n\t\t\tparent::output_table_html();\n\n\t\t\treturn;\n\t\t}\n\n\t\t$post_type_object = get_post_type_object( $post_type );\n\n\t\tif ( ! current_user_can( $post_type_object->cap->edit_post ) ) {\n\t\t\tparent::output_table_html();\n\n\t\t\treturn;\n\t\t}\n\n\t\t$student = false;\n\t\tif ( ! empty( $this->student ) ) {\n\t\t\t$student = $this->student->get_id();\n\t\t} elseif ( ! empty( $_GET['student_id'] ) ) { //phpcs:ignore -- Nonce verification not needed.\n\t\t\t$student = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );\n\t\t}\n\n\t\t$post_new_file = \"post-new.php?post_type=$post_type\";\n\t\t?>\n\t\t<a id=\"llms-new-award-button\" style=\"display:inline-block;margin-bottom:20px\" href=\"<?php echo esc_url( add_query_arg( 'sid', $student, admin_url( $post_new_file ) ) ); ?>\" class=\"llms-button-secondary small\"><?php echo esc_html( $post_type_object->labels->add_new ); ?></a>\n\t\t<?php\n\t\tparent::output_table_html();\n\t}\n}\n"
  },
  {
    "path": "includes/traits/llms-trait-sales-page.php",
    "content": "<?php\n/**\n * LifterLMS Sales Page trait\n *\n * @package LifterLMS/Traits\n *\n * @since 5.3.0\n * @version 5.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS Sales Page trait.\n *\n * **This trait should only be used by classes that extend from the {@see LLMS_Post_Model} class.**\n * **Classes that use this trait must call {@see LLMS_Trait_Sales_Page::construct_sales_page()} in their constructor.**\n *\n * @since 5.3.0\n *\n * @property int    $sales_page_content_page_id WP Post ID of the WP page to redirect to when $sales_page_content_type is 'page'.\n * @property string $sales_page_content_type    Sales page behavior [none,content,page,url].\n * @property string $sales_page_content_url     Redirect URL for a sales page, when $sales_page_content_type is 'url'.\n */\ntrait LLMS_Trait_Sales_Page {\n\t/**\n\t * @inheritdoc\n\t */\n\tabstract protected function add_properties( $props = array() );\n\n\t/**\n\t * Setup properties used by this trait.\n\t *\n\t * **Must be called by the constructor of the class that uses this trait.**\n\t *\n\t * @since 5.3.0\n\t */\n\tprotected function construct_sales_page() {\n\n\t\t$this->add_properties(\n\t\t\tarray(\n\t\t\t\t'sales_page_content_page_id' => 'absint',\n\t\t\t\t'sales_page_content_type'    => 'string',\n\t\t\t\t'sales_page_content_url'     => 'string',\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * @inheritdoc\n\t */\n\tabstract public function get( $key, $raw = false );\n\n\t/**\n\t * Get the URL to a WP page or custom URL when sales page redirection is enabled.\n\t *\n\t * **The class that uses this trait must have the {@see LLMS_Post_Model::$model_post_type} property.**\n\t *\n\t * @since 3.20.0\n\t * @since 5.3.0 Check for an empty  URL or ID.\n\t *              Refactored from `LLMS_Course` and `LLMS_Membership`.\n\t *\n\t * @return string\n\t */\n\tpublic function get_sales_page_url() {\n\n\t\t$type = $this->get( 'sales_page_content_type' );\n\t\tswitch ( $type ) {\n\t\t\tcase 'page':\n\t\t\t\t$url = get_permalink( $this->get( 'sales_page_content_page_id' ) );\n\t\t\t\tbreak;\n\t\t\tcase 'url':\n\t\t\t\t$url = $this->get( 'sales_page_content_url' );\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t$url = get_permalink( $this->get( 'id' ) );\n\t\t}\n\n\t\t/**\n\t\t * Filters the model's sales page URL\n\t\t *\n\t\t * The dynamic portion of the hook name, $this->model_post_type,\n\t\t * refers to the model's post type, e.g. 'course' or 'membership'.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param string          $url    Sales page URL.\n\t\t * @param LLMS_Post_Model $object The LLMS_Course or LLMS_Membership object.\n\t\t * @param string          $type   The model's $sales_page_content_type property.\n\t\t */\n\t\t$url = apply_filters( \"llms_{$this->model_post_type}_get_sales_page_url\", $url, $this, $type );\n\n\t\treturn $url;\n\t}\n\n\t/**\n\t * Determine if sales page redirection is enabled.\n\t *\n\t * **The class that uses this trait must have the {@see LLMS_Post_Model::$model_post_type} property.**\n\t *\n\t * @since 5.3.0 Refactored from `LLMS_Course` and `LLMS_Membership`.\n\t *\n\t * @return boolean\n\t */\n\tpublic function has_sales_page_redirect() {\n\n\t\t$type = $this->get( 'sales_page_content_type' );\n\t\tswitch ( $type ) {\n\t\t\tcase 'page':\n\t\t\t\t$has_redirect = (bool) $this->get( 'sales_page_content_page_id' );\n\t\t\t\tbreak;\n\t\t\tcase 'url':\n\t\t\t\t$has_redirect = (bool) $this->get( 'sales_page_content_url' );\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t$has_redirect = false;\n\t\t}\n\n\t\t/**\n\t\t * Filters whether or not the model has a sales page redirect.\n\t\t *\n\t\t * The dynamic portion of the hook name, $this->model_post_type,\n\t\t * refers to the model's post type, e.g. 'course' or 'membership'.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param boolean         $has_redirect Whether or not the model has a sales page redirect.\n\t\t * @param LLMS_Post_Model $object       The LLMS_Course or LLMS_Membership object.\n\t\t * @param string          $type         The model's $sales_page_content_type property.\n\t\t */\n\t\t$has_redirect = apply_filters(\n\t\t\t\"llms_{$this->model_post_type}_has_sales_page_redirect\",\n\t\t\t$has_redirect,\n\t\t\t$this,\n\t\t\t$type\n\t\t);\n\n\t\treturn $has_redirect;\n\t}\n}\n"
  },
  {
    "path": "includes/traits/llms-trait-singleton.php",
    "content": "<?php\n/**\n * LifterLMS singleton trait\n *\n * @package LifterLMS/Traits\n *\n * @since 5.3.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS singleton trait.\n *\n * @since 5.3.0\n */\ntrait LLMS_Trait_Singleton {\n\n\t/**\n\t * Singleton instance of the class.\n\t *\n\t * @var object\n\t */\n\tprivate static $instance = null;\n\n\t/**\n\t * Returns a singleton instance of the class that uses this trait.\n\t *\n\t * @since 5.3.0\n\t * @since 6.0.0 Removed backward compatible use of the removed `$_instance` property.\n\t *\n\t * @return self\n\t */\n\tpublic static function instance() {\n\n\t\tif ( is_null( self::$instance ) ) {\n\t\t\tself::$instance = new self();\n\t\t}\n\n\t\treturn self::$instance;\n\t}\n}\n"
  },
  {
    "path": "includes/traits/llms-trait-student-awards.php",
    "content": "<?php\n/**\n * LifterLMS singleton trait.\n *\n * @package LifterLMS/Traits\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Retrieve data related to awards earned by a student.\n *\n * This trait should only be used by classes that extend from the {@see LLMS_Abstract_User_Data} class.\n *\n * @since 6.0.0\n */\ntrait LLMS_Trait_Student_Awards {\n\n\t/**\n\t * Retrieve achievements that a user has earned.\n\t *\n\t * @since 2.4.0\n\t * @since 3.14.0 Unknown.\n\t * @since 6.0.0 Moved from `LLMS_Student` class.\n\t *              Introduced alternate usage via `LLMS_Awards_Query` and deprecated previous behavior.\n\t *\n\t * @param string|array $args_or_orderby An array of arguments to pass to LLMS_Awards_Query. The deprecated method\n\t *                                      signature accepts a string representing the field to order the returned results by.\n\t * @param string       $order           Deprecated signature only: Ordering method for returned results (ASC or DESC).\n\t * @param string       $return          Deprecated signature only: Return type. Accepts \"obj\" for an array of objects from\n\t *                                      $wpdb->get_results and \"certificates\" for an array of LLMS_User_Certificate instances.\n\t * @return LLMS_Awards_Query|object[]|LLMS_User_Achievement[]\n\t */\n\tpublic function get_achievements( $args_or_orderby = 'updated_date', $order = 'DESC', $return = 'obj' ) {\n\n\t\t// New behavior.\n\t\tif ( is_array( $args_or_orderby ) ) {\n\t\t\treturn $this->get_awards( $args_or_orderby, 'achievement' );\n\t\t}\n\n\t\t_deprecated_function( 'LLMS_Student::get_achievements()', '6.0.0', 'The behavior of this method has changed. Please refer to https://developer.lifterlms.com/reference/classes/llms_student/get_achievements/ for more information.' );\n\n\t\t$orderby = esc_sql( $args_or_orderby );\n\t\t$order   = esc_sql( $order );\n\n\t\tglobal $wpdb;\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$query = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT post_id, meta_value AS achievement_id, updated_date AS earned_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d and meta_key = '_achievement_earned' ORDER BY $orderby $order\",\n\t\t\t\t$this->get_id()\n\t\t\t)\n\t\t);// db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\tif ( 'achievements' === $return ) {\n\t\t\t$ret = array();\n\t\t\tforeach ( $query as $obj ) {\n\t\t\t\t$ret[] = new LLMS_User_Achievement( $obj->achievement_id );\n\t\t\t}\n\t\t\treturn $ret;\n\t\t}\n\n\t\treturn $query;\n\n\t}\n\n\t/**\n\t * Query student awards.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array  $args Query arguments to pass into `LLMS_Awards_Query`.\n\t * @param string $type Award type. Accepts \"any\", \"achievement\", or \"certificate\".\n\t * @return LLMS_Awards_Query\n\t */\n\tpublic function get_awards( $args = array(), $type = 'any' ) {\n\n\t\t$args['type']  = $type;\n\t\t$args['users'] = $this->get_id();\n\n\t\t// Prevent potential funny business.\n\t\tunset( $args['users__exclude'] );\n\n\t\treturn new LLMS_Awards_Query( $args );\n\t}\n\n\t/**\n\t * Retrieve the total number of awards earned by the student.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $type Award type. Accepts \"any\", \"achievement\", or \"certificate\".\n\t * @return int\n\t */\n\tpublic function get_awards_count( $type = 'any' ) {\n\n\t\t$query = $this->get_awards(\n\t\t\tarray(\n\t\t\t\t'per_page' => 1,\n\t\t\t\t'fields'   => 'ids',\n\t\t\t),\n\t\t\t$type\n\t\t);\n\t\treturn $query->get_found_results();\n\n\t}\n\n\t/**\n\t * Retrieve certificates that the student has been awarded.\n\t *\n\t * The default behavior of this method is deprecated since version 6.0.0. The previous behavior\n\t * is retained for backwards compatibility but will be removed in the next major release.\n\t *\n\t * @since 2.4.0\n\t * @since 3.14.1 Unknown.\n\t * @since 6.0.0 Moved from `LLMS_Student` class.\n\t *              Introduced alternate usage via `LLMS_Awards_Query` and deprecated previous behavior.\n\t *\n\t * @param string|array $args_or_orderby An array of arguments to pass to LLMS_Awards_Query. The deprecated method\n\t *                                      signature accepts a string representing the field to order the returned results by.\n\t * @param string       $order           Deprecated signature only: Ordering method for returned results (ASC or DESC).\n\t * @param string       $return          Deprecated signature only: Return type. Accepts \"obj\" for an array of objects from\n\t *                                      $wpdb->get_results and \"certificates\" for an array of LLMS_User_Certificate instances.\n\t * @return LLMS_Awards_Query|object[]|LLMS_User_Certificate[]\n\t */\n\tpublic function get_certificates( $args_or_orderby = 'updated_date', $order = 'DESC', $return = 'obj' ) {\n\n\t\t// New behavior.\n\t\tif ( is_array( $args_or_orderby ) ) {\n\t\t\treturn $this->get_awards( $args_or_orderby, 'certificate' );\n\t\t}\n\n\t\t_deprecated_function( 'LLMS_Student::get_certificates()', '6.0.0', 'The behavior of this method has changed. Please refer to https://developer.lifterlms.com/reference/classes/llms_student/get_certificates/ for more information.' );\n\n\t\t$orderby = esc_sql( $args_or_orderby );\n\t\t$order   = esc_sql( $order );\n\n\t\tglobal $wpdb;\n\n\t\t// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\t\t$query = $wpdb->get_results(\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"SELECT post_id, meta_value AS certificate_id, updated_date AS earned_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d and meta_key = '_certificate_earned' ORDER BY $orderby $order\",\n\t\t\t\t$this->get_id()\n\t\t\t)\n\t\t); // db call ok; no-cache ok.\n\t\t// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared\n\n\t\tif ( 'certificates' === $return ) {\n\t\t\t$ret = array();\n\t\t\tforeach ( $query as $obj ) {\n\t\t\t\t$ret[] = new LLMS_User_Certificate( $obj->certificate_id );\n\t\t\t}\n\t\t\treturn $ret;\n\t\t}\n\n\t\treturn $query;\n\n\t}\n\n}\n"
  },
  {
    "path": "includes/traits/llms-trait-user-engagement-type.php",
    "content": "<?php\n/**\n * LLMS_Trait_User_Engagement_Type definition\n *\n * @package LifterLMS/Traits\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Methods and properties to help with user engagements.\n *\n * @since 6.0.0\n */\ntrait LLMS_Trait_User_Engagement_Type {\n\n\t/**\n\t * The type of user engagement, e.g. 'achievement' or 'certificate'.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprotected $engagement_type;\n\n\t/**\n\t * Returns the number of user engagements that have been awarded from the template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int $template_id The post ID of the template.\n\t * @return int\n\t */\n\tprotected function count_awarded_engagements( $template_id ) {\n\n\t\t$awarded_engagements_query = new LLMS_Awards_Query(\n\t\t\tarray(\n\t\t\t\t'fields'    => 'ids',\n\t\t\t\t'templates' => $template_id,\n\t\t\t\t'per_page'  => 1,\n\t\t\t\t'status'    => array(\n\t\t\t\t\t'draft',\n\t\t\t\t\t'future',\n\t\t\t\t\t'publish',\n\t\t\t\t),\n\t\t\t\t'type'      => $this->engagement_type,\n\t\t\t)\n\t\t);\n\n\t\treturn $awarded_engagements_query->get_found_results();\n\t}\n\n\t/**\n\t * Returns an awarded engagement or an engagement template, based on a LLMS_Post_Model object, for the given post or false if not found.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param WP_Post|int|null $post       A WP_Post object or a WP_Post ID. A falsy value will use\n\t *                                     the current global `$post` object (if one exists).\n\t * @param int              $is_awarded If true, returns an awarded engagement, else an engagement template.\n\t * @return LLMS_Abstract_User_Engagement|false\n\t */\n\tprotected function get_user_engagement( $post, $is_awarded ) {\n\n\t\t$is_awarded = $is_awarded ? 'my_' : null;\n\t\t$post       = get_post( $post );\n\t\tif ( ! $post || \"llms_{$is_awarded}{$this->engagement_type}\" !== $post->post_type ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$class = 'LLMS_User_' . ucwords( $this->engagement_type, '_' );\n\n\t\treturn new $class( $post );\n\t}\n}\n"
  },
  {
    "path": "includes/widgets/class.llms.bbp.widget.course.forums.list.php",
    "content": "<?php\n/**\n * LifterLMS bbPress forms list widget\n *\n * @package LifterLMS/Integrations/bbPress\n *\n * @since 3.12.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LifterLMS bbPress forms list widget\n *\n * @since 3.12.0\n * @since 3.24.0 Unknown.\n */\nclass LLMS_BBP_Widget_Course_Forums_List extends WP_Widget {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since    3.12.0\n\t * @version  3.12.0\n\t */\n\tpublic function __construct() {\n\n\t\t$options = array(\n\t\t\t'classname'   => 'llms-bbp-widget-course-forums',\n\t\t\t'description' => esc_html__( 'Displays a list of bbPress forums associated with the course.', 'lifterlms' ),\n\t\t);\n\n\t\tparent::__construct( 'llms_bbp_widget_course_forums_list', esc_html__( 'LifterLMS Course Forums List', 'lifterlms' ), $options );\n\t}\n\n\t/**\n\t * Output the widget\n\t *\n\t * @param    array $args      arguments passed to the widget\n\t * @param    array $instance  instance information\n\t * @return   void\n\t * @since    3.12.0\n\t * @version  3.12.0\n\t */\n\tpublic function widget( $args, $instance ) {\n\n\t\t$id = get_the_ID();\n\t\tif ( 'course' !== get_post_type( $id ) ) {\n\t\t\t$course = llms_get_post_parent_course( $id );\n\t\t} else {\n\t\t\t$course = llms_get_post( $id );\n\t\t}\n\n\t\tif ( ! $course ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! $course->get( 'bbp_forum_ids' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\techo wp_kses_post( $args['before_widget'] );\n\n\t\tif ( ! empty( $instance['title'] ) ) {\n\t\t\techo wp_kses_post( $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'] );\n\t\t}\n\n\t\t\techo do_shortcode( '[lifterlms_bbp_course_forums]' );\n\n\t\techo wp_kses_post( $args['after_widget'] );\n\t}\n\n\t/**\n\t * Output widget options form\n\t *\n\t * @param    array $instance  instance data\n\t * @return   void\n\t * @since    3.12.0\n\t * @version  3.24.0\n\t */\n\tpublic function form( $instance ) {\n\n\t\t$title = isset( $instance['title'] ) ? $instance['title'] : __( 'Course Forums', 'lifterlms' );\n\t\t?>\n\t\t<p>\n\t\t\t<label for=\"<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>\"><?php esc_html_e( 'Title:', 'lifterlms' ); ?></label>\n\t\t\t<input class=\"widefat\" id=\"<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>\" name=\"<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>\" type=\"text\" value=\"<?php echo esc_attr( $title ); ?>\">\n\t\t</p>\n\t\t<?php\n\t}\n}\n"
  },
  {
    "path": "includes/widgets/class.llms.widget.course.progress.php",
    "content": "<?php\n/**\n * Course progress widget\n *\n * Displays course progress.\n *\n * @package LifterLMS/Widgets/Classes\n *\n * @since 1.0.0\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Widget_Course_Progress\n *\n * @since 1.0.0\n * @since 3.38.0 Introduced a new option to display/hide the course progress widget to enrolled students only.\n *               Hidden to not enrolled students by default.\n * @since 4.0.0 Remove previously deprecated method `LLMS_Widget_Course_Progress::widget_contents()`.\n */\nclass LLMS_Widget_Course_Progress extends LLMS_Widget {\n\n\t/**\n\t * Register widget with WordPress.\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tWP_Widget::__construct(\n\t\t\t'course_progress',\n\t\t\t__( 'Course Progress', 'lifterlms' ),\n\t\t\tarray(\n\t\t\t\t'description' => __( 'Displays Course Progress on Course or Lesson', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Back-end widget form.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @see WP_Widget::form()\n\t *\n\t * @param array $instance Previously saved values from database.\n\t */\n\tpublic function form( $instance ) {\n\n\t\t// Call widget defaults from parent.\n\t\tparent::form( $instance );\n\t\t$check_enrollment = ( isset( $instance['check_enrollment'] ) ) ? $instance['check_enrollment'] : 1;\n\t\t?>\n\t\t<p>\n\t\t\t<input <?php checked( 1, $check_enrollment ); ?> class=\"checkbox llms-course-progress-check-enrollment\" id=\"<?php echo esc_attr( $this->get_field_id( 'check_enrollment' ) ); ?>\" name=\"<?php echo esc_attr( $this->get_field_name( 'check_enrollment' ) ); ?>\" type=\"checkbox\" value=\"1\">\n\t\t\t<label for=\"<?php echo esc_attr( $this->get_field_id( 'check_enrollment' ) ); ?>\">\n\t\t\t\t<?php esc_html_e( 'Display to enrolled students only?', 'lifterlms' ); ?><br>\n\t\t\t\t<em><?php esc_html_e( 'When checked the course progress bar will be shown only to those students enrolled in the course.', 'lifterlms' ); ?></em>\n\t\t\t</label>\n\t\t</p>\n\t\t<?php\n\t}\n\n\t/**\n\t * Front-end display of widget\n\t *\n\t * @since 3.38.0\n\t *\n\t * @see WP_Widget::widget()\n\t *\n\t * @param array $args     Widget arguments.\n\t * @param array $instance Saved values from database.\n\t * @return void\n\t */\n\tpublic function widget( $args, $instance ) {\n\t\t$check_enrollment = ( ! isset( $instance['check_enrollment'] ) ) ? 1 : $instance['check_enrollment'];\n\t\t$course_progress  = do_shortcode( '[lifterlms_course_progress check_enrollment=' . $check_enrollment . ']' );\n\n\t\t// Do not show the widget title if no progress bar is displayed.\n\t\tif ( empty( $course_progress ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\techo wp_kses_post( $args['before_widget'] );\n\n\t\tif ( ! empty( $instance['title'] ) ) {\n\t\t\techo wp_kses_post( $args['before_title'] ) . wp_kses_post( apply_filters( 'widget_title', $instance['title'] ) ) . wp_kses_post( $args['after_title'] );\n\t\t}\n\n\t\techo wp_kses_post( $course_progress );\n\n\t\techo wp_kses_post( $args['after_widget'] );\n\t}\n\n\t/**\n\t * Sanitize widget form values as they are saved.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @see WP_Widget::update()\n\t *\n\t * @param array $new_instance Values just sent to be saved.\n\t * @param array $old_instance Previously saved values from database.\n\t * @return array Updated safe values to be saved.\n\t */\n\tpublic function update( $new_instance, $old_instance ) {\n\n\t\t$instance = parent::update( $new_instance, $old_instance );\n\n\t\t$instance['check_enrollment'] = ( ! empty( $new_instance['check_enrollment'] ) ) ? 1 : 0;\n\n\t\treturn $instance;\n\t}\n}\n"
  },
  {
    "path": "includes/widgets/class.llms.widget.course.syllabus.php",
    "content": "<?php\n/**\n * Course syllabus widget\n *\n * Displays all lessons in the course\n *\n * @package LifterLMS/Widgets/Classes\n *\n * @since 1.0.0\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Widget_Course_Syllabus\n *\n * @since 1.0.0\n */\nclass LLMS_Widget_Course_Syllabus extends LLMS_Widget {\n\n\t/**\n\t * Register widget with WordPress.\n\t */\n\tpublic function __construct() {\n\n\t\tWP_Widget::__construct(\n\t\t\t'course_syllabus',\n\t\t\t__( 'Course Syllabus', 'lifterlms' ),\n\t\t\tarray(\n\t\t\t\t'description' => __( 'Displays All Course lessons on Course or Lesson page', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Back-end widget form.\n\t *\n\t * @see WP_Widget::form()\n\t *\n\t * @param array $instance Previously saved values from database.\n\t */\n\tpublic function form( $instance ) {\n\n\t\t// Call widget defaults from parent.\n\t\tparent::form( $instance );\n\n\t\t$collapse       = ( ! empty( $instance['collapse'] ) ) ? $instance['collapse'] : 0;\n\t\t$toggles        = ( ! empty( $instance['toggles'] ) ) ? $instance['toggles'] : 0;\n\t\t?>\n\t\t<p>\n\t\t\t<input <?php checked( 1, $collapse ); ?> class=\"checkbox llms-course-outline-collapse\" id=\"<?php echo esc_attr( $this->get_field_id( 'collapse' ) ); ?>\" name=\"<?php echo esc_attr( $this->get_field_name( 'collapse' ) ); ?>\" type=\"checkbox\" value=\"1\">\n\t\t\t<label for=\"<?php echo esc_attr( $this->get_field_id( 'collapse' ) ); ?>\">\n\t\t\t\t<?php esc_html_e( 'Make outline collapsible?', 'lifterlms' ); ?><br>\n\t\t\t\t<em><?php esc_html_e( 'Allow students to hide lessons within a section by clicking the section titles.', 'lifterlms' ); ?></em>\n\t\t\t</label>\n\t\t</p>\n\n\t\t<p class=\"llms-course-outline-toggle-wrapper\"<?php echo ( ! $collapse ) ? ' style=\"display:none;\"' : '';?>>\n\t\t\t<input <?php checked( 1, $toggles ); ?> class=\"checkbox\" id=\"<?php echo esc_attr( $this->get_field_id( 'toggles' ) ); ?>\" name=\"<?php echo esc_attr( $this->get_field_name( 'toggles' ) ); ?>\" type=\"checkbox\" value=\"1\">\n\t\t\t<label for=\"<?php echo esc_attr( $this->get_field_id( 'toggles' ) ); ?>\">\n\t\t\t\t<?php esc_html_e( 'Display open and close all toggles', 'lifterlms' ); ?><br>\n\t\t\t\t<em><?php esc_html_e( 'Display \"Open All\" and \"Close All\" toggles at the bottom of the outline.', 'lifterlms' ); ?></em>\n\t\t\t</label>\n\t\t</p>\n\t\t<?php\n\t}\n\n\t/**\n\t * Widget Content\n\t * Overrides parent class\n\t *\n\t * @see  LLMS_Widget()\n\t * @return void\n\t */\n\tpublic function widget_contents( $args, $instance ) {\n\t\t$collapse = ( isset( $instance['collapse'] ) ) ? $instance['collapse'] : 0;\n\t\t$toggles  = ( isset( $instance['toggles'] ) ) ? $instance['toggles'] : 0;\n\t\techo do_shortcode( '[lifterlms_course_outline collapse=\"' . $collapse . '\" toggles=\"' . $toggles . '\"]' );\n\t}\n\n\t/**\n\t * Sanitize widget form values as they are saved.\n\t *\n\t * @see WP_Widget::update()\n\t *\n\t * @param array $new_instance Values just sent to be saved.\n\t * @param array $old_instance Previously saved values from database.\n\t *\n\t * @return array Updated safe values to be saved.\n\t */\n\tpublic function update( $new_instance, $old_instance ) {\n\n\t\t$instance = parent::update( $new_instance, $old_instance );\n\n\t\t$instance['collapse'] = ( ! empty( $new_instance['collapse'] ) ) ? 1 : 0;\n\t\t$instance['toggles']  = ( ! empty( $new_instance['toggles'] ) ) ? 1 : 0;\n\n\t\treturn $instance;\n\t}\n}\n"
  },
  {
    "path": "includes/widgets/class.llms.widget.php",
    "content": "<?php\n/**\n * Base LifterLMS Widget Class\n *\n * @package LifterLMS/Widgets/Classes\n *\n * @since 1.0.0\n * @version 3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Base LifterLMS Widget Class\n *\n * @since 1.0.0\n * @since 3.24.0 Unknown.\n */\nclass LLMS_Widget extends WP_Widget {\n\n\t/**\n\t * Register widget with WordPress.\n\t *\n\t * @see WP_Widget::__construct()\n\t */\n\tpublic function __construct() {}\n\n\t/**\n\t * Front-end display of widget.\n\t *\n\t * @see WP_Widget::widget()\n\t *\n\t * @param array $args     Widget arguments.\n\t * @param array $instance Saved values from database.\n\t *\n\t * @return  void\n\t */\n\tpublic function widget( $args, $instance ) {\n\t\techo wp_kses_post( $args['before_widget'] );\n\t\tif ( ! empty( $instance['title'] ) ) {\n\t\t\techo wp_kses_post( $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'] );\n\t\t}\n\n\t\t$this->widget_contents( $args, $instance );\n\n\t\techo wp_kses_post( $args['after_widget'] );\n\t}\n\n\t/**\n\t * Echo Widget content.\n\t * This is called in widget()\n\t *\n\t * @return void\n\t */\n\tpublic function widget_contents( $args, $instance ) {}\n\n\t/**\n\t * Back-end widget form.\n\t *\n\t * @see WP_Widget::form()\n\t * @param array $instance Previously saved values from database.\n\t * @return  void\n\t * @since    1.0.0\n\t * @version  3.24.0\n\t */\n\tpublic function form( $instance ) {\n\t\t$title = ! empty( $instance['title'] ) ? $instance['title'] : '';\n\t\t?>\n\t\t<p>\n\t\t<label for=\"<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>\"><?php esc_html_e( 'Title:', 'lifterlms' ); ?></label>\n\t\t<input class=\"widefat\" id=\"<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>\" name=\"<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>\" type=\"text\" value=\"<?php echo esc_attr( $title ); ?>\">\n\t\t</p>\n\t\t<?php\n\t}\n\n\t/**\n\t * Sanitize widget form values as they are saved.\n\t *\n\t * @see WP_Widget::update()\n\t *\n\t * @param array $new_instance Values just sent to be saved.\n\t * @param array $old_instance Previously saved values from database.\n\t *\n\t * @return array Updated safe values to be saved.\n\t */\n\tpublic function update( $new_instance, $old_instance ) {\n\t\t$instance          = array();\n\t\t$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? wp_strip_all_tags( $new_instance['title'] ) : '';\n\n\t\treturn $instance;\n\t}\n}\n\nreturn new LLMS_Widget();\n"
  },
  {
    "path": "includes/widgets/class.llms.widgets.php",
    "content": "<?php\n/**\n * Base Widgets Class\n *\n * Calls WP register widgets for each widget in filter lifterlms_widgets\n *\n * @package LifterLMS/Widgets/Classes\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * LLMS_Widgets\n *\n * @since 1.0.0\n * @since 3.12.0 Unknown.\n */\nclass LLMS_Widgets {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 1.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\tadd_action( 'widgets_init', array( $this, 'register_widgets' ) );\n\n\t}\n\n\t/**\n\t * Registers all lifterlms_widgets\n\t *\n\t * @since 1.0.0\n\t * @since 3.12.0 Unknown.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic function register_widgets() {\n\n\t\t$widgets = apply_filters(\n\t\t\t'lifterlms_widgets',\n\t\t\tarray(\n\t\t\t\t'LLMS_Widget_Course_Progress',\n\t\t\t\t'LLMS_Widget_Course_Syllabus',\n\t\t\t)\n\t\t);\n\n\t\tif ( class_exists( 'bbPress' ) && 'yes' === get_option( 'llms_integration_bbpress_enabled', 'no' ) ) {\n\n\t\t\t$widgets[] = 'LLMS_BBP_Widget_Course_Forums_List';\n\t\t}\n\n\t\tforeach ( $widgets as $widget ) {\n\n\t\t\tregister_widget( $widget );\n\t\t}\n\t}\n}\n\nreturn new LLMS_Widgets();\n"
  },
  {
    "path": "includes/widgets/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "index.php",
    "content": "<?php // shhhh.\n"
  },
  {
    "path": "languages/README.md",
    "content": "LifterLMS Localization and Language Files\n=========================================\n\nThis directory contains localization and language files for the LifterLMS plugin.\n\n## Translating LifterLMS\n\nLifterLMS is fully translatable. The main `.pot` file contained in this directory ([lifterlms.pot](lifterlms.pot)) contains all translatable strings available in the source code. This file is automatically generated on release.\n\n\n## Localization Information Files\n\nThe `.php` files contained within this directory contain lists of localization information (such as country, address, and currency formatting data). These files are loaded by LifterLMS core functions to various areas of the LifterLMS plugin.\n\nThe data contained within these files is compiled from regularly updated sources and converted into a format used by our internal API. These files are automatically generated during a release step.\n\nInformation for these files is derived from the following projects and sources:\n\n+ [Countries States Cities Database](https://github.com/dr5hn/countries-states-cities-database)\n+ [Currency Formatter](https://github.com/smirzaei/currency-formatter)\n+ [addressfield.json](https://github.com/tableau-mkt/addressfield.json)\n+ [LocalePlanet](https://www.localeplanet.com/)\n\nIf you locate any incorrect information in any of these files, please let us know by opening [a new issue](https://github.com/gocodebox/lifterlms/issues/new/choose).\n"
  },
  {
    "path": "languages/countries-address-info.php",
    "content": "<?php\n/**\n * Countries Address Info\n *\n * Returns a map of countries and localization information related to address formatting.\n *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *                                                                         *\n * The data contained within this file is derived from various open-source *\n * projects and libraries.                                                 *\n *                                                                         *\n * See the README.md file in this directory for credits and more.          *\n *                                                                         *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *\n *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *                                                                         *\n * Note to contributors:                                                   *\n *                                                                         *\n * The data contained within this file is automatically generated. Do not  *\n * modify or submit pull requests on this file directly. If you've located *\n * an issue with any of the data contained within this file please open a  *\n * new issue at https://github.com/gocodebox/lifterlms/issues/new/choose.  *\n *                                                                         *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *\n * @package LifterLMS/i18n\n *\n * @see llms_get_countries_address_info()\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'AD' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AE' => array(\n\t\t'city'     => false,\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'AF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'AI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'AM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'AQ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'AT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AU' => array(\n\t\t'city'     => __( 'City / Suburb', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postcode', 'lifterlms' ),\n\t),\n\t'AW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AX' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'AZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BB' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'BD' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'BJ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'BL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'BQ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal Code', 'lifterlms' ),\n\t),\n\t'BR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'BT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BV' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'BY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'BZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'CA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CD' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'CG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'CH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'CN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'CR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CV' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CX' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'CZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'DE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'DJ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'DK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'DM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'DO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'DZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'EC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'EE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'EG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Governorate', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'EH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ER' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'ES' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ET' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'FI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'FJ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'FK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'FM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'FO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'FR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GB' => array(\n\t\t'city'     => __( 'Town / City', 'lifterlms' ),\n\t\t'state' => false,\n\t\t'postcode' => __( 'Postcode', 'lifterlms' ),\n\t),\n\t'GD' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'GE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'GI' => array(\n\t\t'city'     => false,\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'GN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GP' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GQ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'GR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'GW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'GY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'HK' => array(\n\t\t'city'     => __( 'District', 'lifterlms' ),\n\t\t'state'    => __( 'Area', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'HM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'HN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'HR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'HT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'HU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ID' => array(\n\t\t'city'     => __( 'City / Regency', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'IE' => array(\n\t\t'city'     => __( 'Town / City', 'lifterlms' ),\n\t\t'state'    => __( 'County', 'lifterlms' ),\n\t\t'postcode' => __( 'Eircode', 'lifterlms' ),\n\t),\n\t'IL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'IM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'IN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'PIN code', 'lifterlms' ),\n\t),\n\t'IO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'IQ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'IR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'IS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'IT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'JE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'JM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Parish', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'JO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'JP' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Prefecture', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'KE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'KG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'KH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'KI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'KM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'KN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Island', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'KP' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'KR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'KW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'KY' => array(\n\t\t'city'     => false,\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'KZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LB' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'LI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LV' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'LY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'MA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MD' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ME' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'MK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ML' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'MM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MP' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'MQ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'MS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MV' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MX' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'MZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'NC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Department', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NP' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NR' => array(\n\t\t'city'     => false,\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'NU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'NZ' => array(\n\t\t'city'     => __( 'Town / City', 'lifterlms' ),\n\t\t'state'    => __( 'Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postcode', 'lifterlms' ),\n\t),\n\t'OM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'PE' => array(\n\t\t'city'     => __( 'District', 'lifterlms' ),\n\t\t'state'    => __( 'Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'PS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'PW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'PY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'QA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'RE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'RO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'RS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'RU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'RW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'SA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SB' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'SC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'SD' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SG' => array(\n\t\t'city'     => false,\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SJ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'SM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'SS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal Code', 'lifterlms' ),\n\t),\n\t'ST' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'SV' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SX' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal Code', 'lifterlms' ),\n\t),\n\t'SY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'SZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TD' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'TF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'TH' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TJ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TL' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'TM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TO' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'TR' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'TV' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'TW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'TZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'UA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'UG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => false,\n\t),\n\t'UM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'US' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'ZIP code', 'lifterlms' ),\n\t),\n\t'UY' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'UZ' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'VA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'VC' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'VE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'VG' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'VI' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'VN' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'VU' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'WF' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'WS' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n\t'XK' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal Code', 'lifterlms' ),\n\t),\n\t'YE' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'YT' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ZA' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'Province', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ZM' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => __( 'State / Region', 'lifterlms' ),\n\t\t'postcode' => __( 'Postal code', 'lifterlms' ),\n\t),\n\t'ZW' => array(\n\t\t'city'     => __( 'City', 'lifterlms' ),\n\t\t'state'    => false,\n\t\t'postcode' => false,\n\t),\n);\n"
  },
  {
    "path": "languages/countries.php",
    "content": "<?php\n/**\n * Countries\n *\n * Returns an array of countries and codes.\n * Country codes and names should follow the Unicode CLDR recommendation (https://cldr.unicode.org/translation/displaynames/countryregion-territory-names).\n *\n * See https://github.com/unicode-org/cldr/blob/master/common/subdivisions/en.xml\n *\n * @package WooCommerce\\i18n\n * @version 3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'AF' => __( 'Afghanistan', 'lifterlms' ),\n\t'AX' => __( 'Åland Islands', 'lifterlms' ),\n\t'AL' => __( 'Albania', 'lifterlms' ),\n\t'DZ' => __( 'Algeria', 'lifterlms' ),\n\t'AS' => __( 'American Samoa', 'lifterlms' ),\n\t'AD' => __( 'Andorra', 'lifterlms' ),\n\t'AO' => __( 'Angola', 'lifterlms' ),\n\t'AI' => __( 'Anguilla', 'lifterlms' ),\n\t'AQ' => __( 'Antarctica', 'lifterlms' ),\n\t'AG' => __( 'Antigua and Barbuda', 'lifterlms' ),\n\t'AR' => __( 'Argentina', 'lifterlms' ),\n\t'AM' => __( 'Armenia', 'lifterlms' ),\n\t'AW' => __( 'Aruba', 'lifterlms' ),\n\t'AU' => __( 'Australia', 'lifterlms' ),\n\t'AT' => __( 'Austria', 'lifterlms' ),\n\t'AZ' => __( 'Azerbaijan', 'lifterlms' ),\n\t'BS' => __( 'Bahamas', 'lifterlms' ),\n\t'BH' => __( 'Bahrain', 'lifterlms' ),\n\t'BD' => __( 'Bangladesh', 'lifterlms' ),\n\t'BB' => __( 'Barbados', 'lifterlms' ),\n\t'BY' => __( 'Belarus', 'lifterlms' ),\n\t'BE' => __( 'Belgium', 'lifterlms' ),\n\t'PW' => __( 'Belau', 'lifterlms' ),\n\t'BZ' => __( 'Belize', 'lifterlms' ),\n\t'BJ' => __( 'Benin', 'lifterlms' ),\n\t'BM' => __( 'Bermuda', 'lifterlms' ),\n\t'BT' => __( 'Bhutan', 'lifterlms' ),\n\t'BO' => __( 'Bolivia', 'lifterlms' ),\n\t'BQ' => __( 'Bonaire, Saint Eustatius and Saba', 'lifterlms' ),\n\t'BA' => __( 'Bosnia and Herzegovina', 'lifterlms' ),\n\t'BW' => __( 'Botswana', 'lifterlms' ),\n\t'BV' => __( 'Bouvet Island', 'lifterlms' ),\n\t'BR' => __( 'Brazil', 'lifterlms' ),\n\t'IO' => __( 'British Indian Ocean Territory', 'lifterlms' ),\n\t'BN' => __( 'Brunei', 'lifterlms' ),\n\t'BG' => __( 'Bulgaria', 'lifterlms' ),\n\t'BF' => __( 'Burkina Faso', 'lifterlms' ),\n\t'BI' => __( 'Burundi', 'lifterlms' ),\n\t'KH' => __( 'Cambodia', 'lifterlms' ),\n\t'CM' => __( 'Cameroon', 'lifterlms' ),\n\t'CA' => __( 'Canada', 'lifterlms' ),\n\t'CV' => __( 'Cape Verde', 'lifterlms' ),\n\t'KY' => __( 'Cayman Islands', 'lifterlms' ),\n\t'CF' => __( 'Central African Republic', 'lifterlms' ),\n\t'TD' => __( 'Chad', 'lifterlms' ),\n\t'CL' => __( 'Chile', 'lifterlms' ),\n\t'CN' => __( 'China', 'lifterlms' ),\n\t'CX' => __( 'Christmas Island', 'lifterlms' ),\n\t'CC' => __( 'Cocos (Keeling) Islands', 'lifterlms' ),\n\t'CO' => __( 'Colombia', 'lifterlms' ),\n\t'KM' => __( 'Comoros', 'lifterlms' ),\n\t'CG' => __( 'Congo (Brazzaville)', 'lifterlms' ),\n\t'CD' => __( 'Congo (Kinshasa)', 'lifterlms' ),\n\t'CK' => __( 'Cook Islands', 'lifterlms' ),\n\t'CR' => __( 'Costa Rica', 'lifterlms' ),\n\t'HR' => __( 'Croatia', 'lifterlms' ),\n\t'CU' => __( 'Cuba', 'lifterlms' ),\n\t'CW' => __( 'Cura&ccedil;ao', 'lifterlms' ),\n\t'CY' => __( 'Cyprus', 'lifterlms' ),\n\t'CZ' => __( 'Czech Republic', 'lifterlms' ),\n\t'DK' => __( 'Denmark', 'lifterlms' ),\n\t'DJ' => __( 'Djibouti', 'lifterlms' ),\n\t'DM' => __( 'Dominica', 'lifterlms' ),\n\t'DO' => __( 'Dominican Republic', 'lifterlms' ),\n\t'EC' => __( 'Ecuador', 'lifterlms' ),\n\t'EG' => __( 'Egypt', 'lifterlms' ),\n\t'SV' => __( 'El Salvador', 'lifterlms' ),\n\t'GQ' => __( 'Equatorial Guinea', 'lifterlms' ),\n\t'ER' => __( 'Eritrea', 'lifterlms' ),\n\t'EE' => __( 'Estonia', 'lifterlms' ),\n\t'ET' => __( 'Ethiopia', 'lifterlms' ),\n\t'FK' => __( 'Falkland Islands', 'lifterlms' ),\n\t'FO' => __( 'Faroe Islands', 'lifterlms' ),\n\t'FJ' => __( 'Fiji', 'lifterlms' ),\n\t'FI' => __( 'Finland', 'lifterlms' ),\n\t'FR' => __( 'France', 'lifterlms' ),\n\t'GF' => __( 'French Guiana', 'lifterlms' ),\n\t'PF' => __( 'French Polynesia', 'lifterlms' ),\n\t'TF' => __( 'French Southern Territories', 'lifterlms' ),\n\t'GA' => __( 'Gabon', 'lifterlms' ),\n\t'GM' => __( 'Gambia', 'lifterlms' ),\n\t'GE' => __( 'Georgia', 'lifterlms' ),\n\t'DE' => __( 'Germany', 'lifterlms' ),\n\t'GH' => __( 'Ghana', 'lifterlms' ),\n\t'GI' => __( 'Gibraltar', 'lifterlms' ),\n\t'GR' => __( 'Greece', 'lifterlms' ),\n\t'GL' => __( 'Greenland', 'lifterlms' ),\n\t'GD' => __( 'Grenada', 'lifterlms' ),\n\t'GP' => __( 'Guadeloupe', 'lifterlms' ),\n\t'GU' => __( 'Guam', 'lifterlms' ),\n\t'GT' => __( 'Guatemala', 'lifterlms' ),\n\t'GG' => __( 'Guernsey', 'lifterlms' ),\n\t'GN' => __( 'Guinea', 'lifterlms' ),\n\t'GW' => __( 'Guinea-Bissau', 'lifterlms' ),\n\t'GY' => __( 'Guyana', 'lifterlms' ),\n\t'HT' => __( 'Haiti', 'lifterlms' ),\n\t'HM' => __( 'Heard Island and McDonald Islands', 'lifterlms' ),\n\t'HN' => __( 'Honduras', 'lifterlms' ),\n\t'HK' => __( 'Hong Kong', 'lifterlms' ),\n\t'HU' => __( 'Hungary', 'lifterlms' ),\n\t'IS' => __( 'Iceland', 'lifterlms' ),\n\t'IN' => __( 'India', 'lifterlms' ),\n\t'ID' => __( 'Indonesia', 'lifterlms' ),\n\t'IR' => __( 'Iran', 'lifterlms' ),\n\t'IQ' => __( 'Iraq', 'lifterlms' ),\n\t'IE' => __( 'Ireland', 'lifterlms' ),\n\t'IM' => __( 'Isle of Man', 'lifterlms' ),\n\t'IL' => __( 'Israel', 'lifterlms' ),\n\t'IT' => __( 'Italy', 'lifterlms' ),\n\t'CI' => __( 'Ivory Coast', 'lifterlms' ),\n\t'JM' => __( 'Jamaica', 'lifterlms' ),\n\t'JP' => __( 'Japan', 'lifterlms' ),\n\t'JE' => __( 'Jersey', 'lifterlms' ),\n\t'JO' => __( 'Jordan', 'lifterlms' ),\n\t'KZ' => __( 'Kazakhstan', 'lifterlms' ),\n\t'KE' => __( 'Kenya', 'lifterlms' ),\n\t'KI' => __( 'Kiribati', 'lifterlms' ),\n\t'KW' => __( 'Kuwait', 'lifterlms' ),\n\t'KG' => __( 'Kyrgyzstan', 'lifterlms' ),\n\t'LA' => __( 'Laos', 'lifterlms' ),\n\t'LV' => __( 'Latvia', 'lifterlms' ),\n\t'LB' => __( 'Lebanon', 'lifterlms' ),\n\t'LS' => __( 'Lesotho', 'lifterlms' ),\n\t'LR' => __( 'Liberia', 'lifterlms' ),\n\t'LY' => __( 'Libya', 'lifterlms' ),\n\t'LI' => __( 'Liechtenstein', 'lifterlms' ),\n\t'LT' => __( 'Lithuania', 'lifterlms' ),\n\t'LU' => __( 'Luxembourg', 'lifterlms' ),\n\t'MO' => __( 'Macao', 'lifterlms' ),\n\t'MK' => __( 'North Macedonia', 'lifterlms' ),\n\t'MG' => __( 'Madagascar', 'lifterlms' ),\n\t'MW' => __( 'Malawi', 'lifterlms' ),\n\t'MY' => __( 'Malaysia', 'lifterlms' ),\n\t'MV' => __( 'Maldives', 'lifterlms' ),\n\t'ML' => __( 'Mali', 'lifterlms' ),\n\t'MT' => __( 'Malta', 'lifterlms' ),\n\t'MH' => __( 'Marshall Islands', 'lifterlms' ),\n\t'MQ' => __( 'Martinique', 'lifterlms' ),\n\t'MR' => __( 'Mauritania', 'lifterlms' ),\n\t'MU' => __( 'Mauritius', 'lifterlms' ),\n\t'YT' => __( 'Mayotte', 'lifterlms' ),\n\t'MX' => __( 'Mexico', 'lifterlms' ),\n\t'FM' => __( 'Micronesia', 'lifterlms' ),\n\t'MD' => __( 'Moldova', 'lifterlms' ),\n\t'MC' => __( 'Monaco', 'lifterlms' ),\n\t'MN' => __( 'Mongolia', 'lifterlms' ),\n\t'ME' => __( 'Montenegro', 'lifterlms' ),\n\t'MS' => __( 'Montserrat', 'lifterlms' ),\n\t'MA' => __( 'Morocco', 'lifterlms' ),\n\t'MZ' => __( 'Mozambique', 'lifterlms' ),\n\t'MM' => __( 'Myanmar', 'lifterlms' ),\n\t'NA' => __( 'Namibia', 'lifterlms' ),\n\t'NR' => __( 'Nauru', 'lifterlms' ),\n\t'NP' => __( 'Nepal', 'lifterlms' ),\n\t'NL' => __( 'Netherlands', 'lifterlms' ),\n\t'NC' => __( 'New Caledonia', 'lifterlms' ),\n\t'NZ' => __( 'New Zealand', 'lifterlms' ),\n\t'NI' => __( 'Nicaragua', 'lifterlms' ),\n\t'NE' => __( 'Niger', 'lifterlms' ),\n\t'NG' => __( 'Nigeria', 'lifterlms' ),\n\t'NU' => __( 'Niue', 'lifterlms' ),\n\t'NF' => __( 'Norfolk Island', 'lifterlms' ),\n\t'MP' => __( 'Northern Mariana Islands', 'lifterlms' ),\n\t'KP' => __( 'North Korea', 'lifterlms' ),\n\t'NO' => __( 'Norway', 'lifterlms' ),\n\t'OM' => __( 'Oman', 'lifterlms' ),\n\t'PK' => __( 'Pakistan', 'lifterlms' ),\n\t'PS' => __( 'Palestinian Territory', 'lifterlms' ),\n\t'PA' => __( 'Panama', 'lifterlms' ),\n\t'PG' => __( 'Papua New Guinea', 'lifterlms' ),\n\t'PY' => __( 'Paraguay', 'lifterlms' ),\n\t'PE' => __( 'Peru', 'lifterlms' ),\n\t'PH' => __( 'Philippines', 'lifterlms' ),\n\t'PN' => __( 'Pitcairn', 'lifterlms' ),\n\t'PL' => __( 'Poland', 'lifterlms' ),\n\t'PT' => __( 'Portugal', 'lifterlms' ),\n\t'PR' => __( 'Puerto Rico', 'lifterlms' ),\n\t'QA' => __( 'Qatar', 'lifterlms' ),\n\t'RE' => __( 'Reunion', 'lifterlms' ),\n\t'RO' => __( 'Romania', 'lifterlms' ),\n\t'RU' => __( 'Russia', 'lifterlms' ),\n\t'RW' => __( 'Rwanda', 'lifterlms' ),\n\t'BL' => __( 'Saint Barth&eacute;lemy', 'lifterlms' ),\n\t'SH' => __( 'Saint Helena', 'lifterlms' ),\n\t'KN' => __( 'Saint Kitts and Nevis', 'lifterlms' ),\n\t'LC' => __( 'Saint Lucia', 'lifterlms' ),\n\t'MF' => __( 'Saint Martin (French part)', 'lifterlms' ),\n\t'SX' => __( 'Saint Martin (Dutch part)', 'lifterlms' ),\n\t'PM' => __( 'Saint Pierre and Miquelon', 'lifterlms' ),\n\t'VC' => __( 'Saint Vincent and the Grenadines', 'lifterlms' ),\n\t'SM' => __( 'San Marino', 'lifterlms' ),\n\t'ST' => __( 'S&atilde;o Tom&eacute; and Pr&iacute;ncipe', 'lifterlms' ),\n\t'SA' => __( 'Saudi Arabia', 'lifterlms' ),\n\t'SN' => __( 'Senegal', 'lifterlms' ),\n\t'RS' => __( 'Serbia', 'lifterlms' ),\n\t'SC' => __( 'Seychelles', 'lifterlms' ),\n\t'SL' => __( 'Sierra Leone', 'lifterlms' ),\n\t'SG' => __( 'Singapore', 'lifterlms' ),\n\t'SK' => __( 'Slovakia', 'lifterlms' ),\n\t'SI' => __( 'Slovenia', 'lifterlms' ),\n\t'SB' => __( 'Solomon Islands', 'lifterlms' ),\n\t'SO' => __( 'Somalia', 'lifterlms' ),\n\t'ZA' => __( 'South Africa', 'lifterlms' ),\n\t'GS' => __( 'South Georgia/Sandwich Islands', 'lifterlms' ),\n\t'KR' => __( 'South Korea', 'lifterlms' ),\n\t'SS' => __( 'South Sudan', 'lifterlms' ),\n\t'ES' => __( 'Spain', 'lifterlms' ),\n\t'LK' => __( 'Sri Lanka', 'lifterlms' ),\n\t'SD' => __( 'Sudan', 'lifterlms' ),\n\t'SR' => __( 'Suriname', 'lifterlms' ),\n\t'SJ' => __( 'Svalbard and Jan Mayen', 'lifterlms' ),\n\t'SZ' => __( 'Eswatini', 'lifterlms' ),\n\t'SE' => __( 'Sweden', 'lifterlms' ),\n\t'CH' => __( 'Switzerland', 'lifterlms' ),\n\t'SY' => __( 'Syria', 'lifterlms' ),\n\t'TW' => __( 'Taiwan', 'lifterlms' ),\n\t'TJ' => __( 'Tajikistan', 'lifterlms' ),\n\t'TZ' => __( 'Tanzania', 'lifterlms' ),\n\t'TH' => __( 'Thailand', 'lifterlms' ),\n\t'TL' => __( 'Timor-Leste', 'lifterlms' ),\n\t'TG' => __( 'Togo', 'lifterlms' ),\n\t'TK' => __( 'Tokelau', 'lifterlms' ),\n\t'TO' => __( 'Tonga', 'lifterlms' ),\n\t'TT' => __( 'Trinidad and Tobago', 'lifterlms' ),\n\t'TN' => __( 'Tunisia', 'lifterlms' ),\n\t'TR' => __( 'Turkey', 'lifterlms' ),\n\t'TM' => __( 'Turkmenistan', 'lifterlms' ),\n\t'TC' => __( 'Turks and Caicos Islands', 'lifterlms' ),\n\t'TV' => __( 'Tuvalu', 'lifterlms' ),\n\t'UG' => __( 'Uganda', 'lifterlms' ),\n\t'UA' => __( 'Ukraine', 'lifterlms' ),\n\t'AE' => __( 'United Arab Emirates', 'lifterlms' ),\n\t'GB' => __( 'United Kingdom (UK)', 'lifterlms' ),\n\t'US' => __( 'United States (US)', 'lifterlms' ),\n\t'UM' => __( 'United States (US) Minor Outlying Islands', 'lifterlms' ),\n\t'UY' => __( 'Uruguay', 'lifterlms' ),\n\t'UZ' => __( 'Uzbekistan', 'lifterlms' ),\n\t'VU' => __( 'Vanuatu', 'lifterlms' ),\n\t'VA' => __( 'Vatican', 'lifterlms' ),\n\t'VE' => __( 'Venezuela', 'lifterlms' ),\n\t'VN' => __( 'Vietnam', 'lifterlms' ),\n\t'VG' => __( 'Virgin Islands (British)', 'lifterlms' ),\n\t'VI' => __( 'Virgin Islands (US)', 'lifterlms' ),\n\t'WF' => __( 'Wallis and Futuna', 'lifterlms' ),\n\t'EH' => __( 'Western Sahara', 'lifterlms' ),\n\t'WS' => __( 'Samoa', 'lifterlms' ),\n\t'YE' => __( 'Yemen', 'lifterlms' ),\n\t'ZM' => __( 'Zambia', 'lifterlms' ),\n\t'ZW' => __( 'Zimbabwe', 'lifterlms' ),\n);\n"
  },
  {
    "path": "languages/currencies.php",
    "content": "<?php\n/**\n * Currencies\n *\n * currenciesList a map of currency codes and their names.\n *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *                                                                         *\n * The data contained within this file is derived from various open-source *\n * projects and libraries.                                                 *\n *                                                                         *\n * See the README.md file in this directory for credits and more.          *\n *                                                                         *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *\n *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *                                                                         *\n * Note to contributors:                                                   *\n *                                                                         *\n * The data contained within this file is automatically generated. Do not  *\n * modify or submit pull requests on this file directly. If you've located *\n * an issue with any of the data contained within this file please open a  *\n * new issue at https://github.com/gocodebox/lifterlms/issues/new/choose.  *\n *                                                                         *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *\n * @package LifterLMS/i18n\n *\n * @see get_lifterlms_currencies()\n *\n * @since 5.0.0\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'AFN' => __( 'Afghan Afghani', 'lifterlms' ),\n\t'ALL' => __( 'Albanian Lek', 'lifterlms' ),\n\t'DZD' => __( 'Algerian Dinar', 'lifterlms' ),\n\t'AOA' => __( 'Angolan Kwanza', 'lifterlms' ),\n\t'ARS' => __( 'Argentine Peso', 'lifterlms' ),\n\t'AMD' => __( 'Armenian Dram', 'lifterlms' ),\n\t'AWG' => __( 'Aruban Florin', 'lifterlms' ),\n\t'AUD' => __( 'Australian Dollar', 'lifterlms' ),\n\t'AZN' => __( 'Azerbaijani Manat', 'lifterlms' ),\n\t'BSD' => __( 'Bahamian Dollar', 'lifterlms' ),\n\t'BHD' => __( 'Bahraini Dinar', 'lifterlms' ),\n\t'BDT' => __( 'Bangladeshi Taka', 'lifterlms' ),\n\t'BBD' => __( 'Barbadian Dollar', 'lifterlms' ),\n\t'BYN' => __( 'Belarusian Ruble', 'lifterlms' ),\n\t'BZD' => __( 'Belize Dollar', 'lifterlms' ),\n\t'BMD' => __( 'Bermudan Dollar', 'lifterlms' ),\n\t'BTN' => __( 'Bhutanese Ngultrum', 'lifterlms' ),\n\t'BTC' => __( 'Bitcoin', 'lifterlms' ),\n\t'BOB' => __( 'Bolivian Boliviano', 'lifterlms' ),\n\t'BAM' => __( 'Bosnia-Herzegovina Convertible Mark', 'lifterlms' ),\n\t'BWP' => __( 'Botswanan Pula', 'lifterlms' ),\n\t'BRL' => __( 'Brazilian Real', 'lifterlms' ),\n\t'GBP' => __( 'British Pound', 'lifterlms' ),\n\t'BND' => __( 'Brunei Dollar', 'lifterlms' ),\n\t'BGN' => __( 'Bulgarian Lev', 'lifterlms' ),\n\t'BIF' => __( 'Burundian Franc', 'lifterlms' ),\n\t'XPF' => __( 'CFP Franc', 'lifterlms' ),\n\t'KHR' => __( 'Cambodian Riel', 'lifterlms' ),\n\t'CAD' => __( 'Canadian Dollar', 'lifterlms' ),\n\t'CVE' => __( 'Cape Verdean Escudo', 'lifterlms' ),\n\t'KYD' => __( 'Cayman Islands Dollar', 'lifterlms' ),\n\t'XAF' => __( 'Central African CFA Franc', 'lifterlms' ),\n\t'CLP' => __( 'Chilean Peso', 'lifterlms' ),\n\t'CNY' => __( 'Chinese Yuan', 'lifterlms' ),\n\t'COP' => __( 'Colombian Peso', 'lifterlms' ),\n\t'KMF' => __( 'Comorian Franc', 'lifterlms' ),\n\t'CDF' => __( 'Congolese Franc', 'lifterlms' ),\n\t'CRC' => __( 'Costa Rican Colón', 'lifterlms' ),\n\t'HRK' => __( 'Croatian Kuna', 'lifterlms' ),\n\t'CUC' => __( 'Cuban Convertible Peso', 'lifterlms' ),\n\t'CUP' => __( 'Cuban Peso', 'lifterlms' ),\n\t'CZK' => __( 'Czech Koruna', 'lifterlms' ),\n\t'DKK' => __( 'Danish Krone', 'lifterlms' ),\n\t'DJF' => __( 'Djiboutian Franc', 'lifterlms' ),\n\t'DOP' => __( 'Dominican Peso', 'lifterlms' ),\n\t'XCD' => __( 'East Caribbean Dollar', 'lifterlms' ),\n\t'EGP' => __( 'Egyptian Pound', 'lifterlms' ),\n\t'ERN' => __( 'Eritrean Nakfa', 'lifterlms' ),\n\t'ETH' => __( 'Ethereum', 'lifterlms' ),\n\t'ETB' => __( 'Ethiopian Birr', 'lifterlms' ),\n\t'EUR' => __( 'Euro', 'lifterlms' ),\n\t'FKP' => __( 'Falkland Islands Pound', 'lifterlms' ),\n\t'FJD' => __( 'Fijian Dollar', 'lifterlms' ),\n\t'GMD' => __( 'Gambian Dalasi', 'lifterlms' ),\n\t'GEL' => __( 'Georgian Lari', 'lifterlms' ),\n\t'GHS' => __( 'Ghanaian Cedi', 'lifterlms' ),\n\t'GIP' => __( 'Gibraltar Pound', 'lifterlms' ),\n\t'GTQ' => __( 'Guatemalan Quetzal', 'lifterlms' ),\n\t'GPP' => __( 'Guernsey Pound', 'lifterlms' ),\n\t'GNF' => __( 'Guinean Franc', 'lifterlms' ),\n\t'GYD' => __( 'Guyanaese Dollar', 'lifterlms' ),\n\t'HTG' => __( 'Haitian Gourde', 'lifterlms' ),\n\t'HNL' => __( 'Honduran Lempira', 'lifterlms' ),\n\t'HKD' => __( 'Hong Kong Dollar', 'lifterlms' ),\n\t'HUF' => __( 'Hungarian Forint', 'lifterlms' ),\n\t'ISK' => __( 'Icelandic Króna', 'lifterlms' ),\n\t'INR' => __( 'Indian Rupee', 'lifterlms' ),\n\t'IDR' => __( 'Indonesian Rupiah', 'lifterlms' ),\n\t'IRR' => __( 'Iranian Rial', 'lifterlms' ),\n\t'IQD' => __( 'Iraqi Dinar', 'lifterlms' ),\n\t'ILS' => __( 'Israeli New Shekel', 'lifterlms' ),\n\t'JMD' => __( 'Jamaican Dollar', 'lifterlms' ),\n\t'JPY' => __( 'Japanese Yen', 'lifterlms' ),\n\t'JEP' => __( 'Jersey Pound', 'lifterlms' ),\n\t'JOD' => __( 'Jordanian Dinar', 'lifterlms' ),\n\t'KZT' => __( 'Kazakhstani Tenge', 'lifterlms' ),\n\t'KES' => __( 'Kenyan Shilling', 'lifterlms' ),\n\t'KWD' => __( 'Kuwaiti Dinar', 'lifterlms' ),\n\t'KGS' => __( 'Kyrgystani Som', 'lifterlms' ),\n\t'LAK' => __( 'Laotian Kip', 'lifterlms' ),\n\t'LBP' => __( 'Lebanese Pound', 'lifterlms' ),\n\t'LSL' => __( 'Lesotho Loti', 'lifterlms' ),\n\t'LRD' => __( 'Liberian Dollar', 'lifterlms' ),\n\t'LYD' => __( 'Libyan Dinar', 'lifterlms' ),\n\t'LTC' => __( 'Litecoin', 'lifterlms' ),\n\t'MOP' => __( 'Macanese Pataca', 'lifterlms' ),\n\t'MKD' => __( 'Macedonian Denar', 'lifterlms' ),\n\t'MGA' => __( 'Malagasy Ariary', 'lifterlms' ),\n\t'MWK' => __( 'Malawian Kwacha', 'lifterlms' ),\n\t'MYR' => __( 'Malaysian Ringgit', 'lifterlms' ),\n\t'MVR' => __( 'Maldivian rufiyaa', 'lifterlms' ),\n\t'IMP' => __( 'Manx Pound', 'lifterlms' ),\n\t'MRO' => __( 'Mauritanian Ouguiya', 'lifterlms' ),\n\t'MUR' => __( 'Mauritian Rupee', 'lifterlms' ),\n\t'MXN' => __( 'Mexican Peso', 'lifterlms' ),\n\t'MDL' => __( 'Moldovan Leu', 'lifterlms' ),\n\t'MNT' => __( 'Mongolian Tugrik', 'lifterlms' ),\n\t'MAD' => __( 'Moroccan Dirham', 'lifterlms' ),\n\t'MZN' => __( 'Mozambican Metical', 'lifterlms' ),\n\t'MMK' => __( 'Myanmar Kyat', 'lifterlms' ),\n\t'NAD' => __( 'Namibian Dollar', 'lifterlms' ),\n\t'NPR' => __( 'Nepalese Rupee', 'lifterlms' ),\n\t'ANG' => __( 'Netherlands Antillean Guilder', 'lifterlms' ),\n\t'TWD' => __( 'New Taiwan Dollar', 'lifterlms' ),\n\t'NZD' => __( 'New Zealand Dollar', 'lifterlms' ),\n\t'NIO' => __( 'Nicaraguan Córdoba', 'lifterlms' ),\n\t'NGN' => __( 'Nigerian Naira', 'lifterlms' ),\n\t'KPW' => __( 'North Korean Won', 'lifterlms' ),\n\t'NOK' => __( 'Norwegian Krone', 'lifterlms' ),\n\t'OMR' => __( 'Omani Rial', 'lifterlms' ),\n\t'PKR' => __( 'Pakistani Rupee', 'lifterlms' ),\n\t'PAB' => __( 'Panamanian Balboa', 'lifterlms' ),\n\t'PGK' => __( 'Papua New Guinean Kina', 'lifterlms' ),\n\t'PYG' => __( 'Paraguayan Guarani', 'lifterlms' ),\n\t'PEN' => __( 'Peruvian Sol', 'lifterlms' ),\n\t'PHP' => __( 'Philippine Piso', 'lifterlms' ),\n\t'PLN' => __( 'Polish Zloty', 'lifterlms' ),\n\t'QAR' => __( 'Qatari Rial', 'lifterlms' ),\n\t'RON' => __( 'Romanian Leu', 'lifterlms' ),\n\t'RUB' => __( 'Russian Ruble', 'lifterlms' ),\n\t'RWF' => __( 'Rwandan Franc', 'lifterlms' ),\n\t'WST' => __( 'Samoan Tala', 'lifterlms' ),\n\t'SAR' => __( 'Saudi Riyal', 'lifterlms' ),\n\t'RSD' => __( 'Serbian Dinar', 'lifterlms' ),\n\t'SCR' => __( 'Seychellois Rupee', 'lifterlms' ),\n\t'SLL' => __( 'Sierra Leonean Leone', 'lifterlms' ),\n\t'SGD' => __( 'Singapore Dollar', 'lifterlms' ),\n\t'SBD' => __( 'Solomon Islands Dollar', 'lifterlms' ),\n\t'SOS' => __( 'Somali Shilling', 'lifterlms' ),\n\t'ZAR' => __( 'South African Rand', 'lifterlms' ),\n\t'KRW' => __( 'South Korean Won', 'lifterlms' ),\n\t'SSP' => __( 'South Sudanese Pound', 'lifterlms' ),\n\t'LKR' => __( 'Sri Lankan Rupee', 'lifterlms' ),\n\t'SHP' => __( 'St. Helena Pound', 'lifterlms' ),\n\t'SDG' => __( 'Sudanese Pound', 'lifterlms' ),\n\t'SRD' => __( 'Surinamese Dollar', 'lifterlms' ),\n\t'SZL' => __( 'Swazi Lilangeni', 'lifterlms' ),\n\t'SEK' => __( 'Swedish Krona', 'lifterlms' ),\n\t'CHF' => __( 'Swiss Franc', 'lifterlms' ),\n\t'SYP' => __( 'Syrian Pound', 'lifterlms' ),\n\t'STD' => __( 'São Tomé and Príncipe dobra', 'lifterlms' ),\n\t'TJS' => __( 'Tajikistani Somoni', 'lifterlms' ),\n\t'TZS' => __( 'Tanzanian Shilling', 'lifterlms' ),\n\t'THB' => __( 'Thai Baht', 'lifterlms' ),\n\t'TOP' => __( 'Tongan Paʻanga', 'lifterlms' ),\n\t'PRB' => __( 'Transnistrian Ruble', 'lifterlms' ),\n\t'TTD' => __( 'Trinidad & Tobago Dollar', 'lifterlms' ),\n\t'TND' => __( 'Tunisian Dinar', 'lifterlms' ),\n\t'TRY' => __( 'Turkish Lira', 'lifterlms' ),\n\t'TMT' => __( 'Turkmenistan manat', 'lifterlms' ),\n\t'UGX' => __( 'Ugandan Shilling', 'lifterlms' ),\n\t'UAH' => __( 'Ukrainian Hryvnia', 'lifterlms' ),\n\t'AED' => __( 'United Arab Emirates Dirham', 'lifterlms' ),\n\t'USD' => __( 'United States Dollar', 'lifterlms' ),\n\t'UYU' => __( 'Uruguayan Peso', 'lifterlms' ),\n\t'UZS' => __( 'Uzbekistani Som', 'lifterlms' ),\n\t'VUV' => __( 'Vanuatu Vatu', 'lifterlms' ),\n\t'VEF' => __( 'Venezuelan Bolívar', 'lifterlms' ),\n\t'VND' => __( 'Vietnamese Dong', 'lifterlms' ),\n\t'XOF' => __( 'West African CFA Franc', 'lifterlms' ),\n\t'YER' => __( 'Yemeni Rial', 'lifterlms' ),\n\t'ZMW' => __( 'Zambian Kwacha', 'lifterlms' ),\n\t'ZWL' => __( 'Zimbabwean Dollar', 'lifterlms' ),\n);\n"
  },
  {
    "path": "languages/currency-symbols.php",
    "content": "<?php\n/**\n * Currency Symbols\n *\n * Returns a map of currency codes and their symbols.\n *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *                                                                         *\n * The data contained within this file is derived from various open-source *\n * projects and libraries.                                                 *\n *                                                                         *\n * See the README.md file in this directory for credits and more.          *\n *                                                                         *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *\n *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *                                                                         *\n * Note to contributors:                                                   *\n *                                                                         *\n * The data contained within this file is automatically generated. Do not  *\n * modify or submit pull requests on this file directly. If you've located *\n * an issue with any of the data contained within this file please open a  *\n * new issue at https://github.com/gocodebox/lifterlms/issues/new/choose.  *\n *                                                                         *\n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n *\n * @package LifterLMS/i18n\n *\n * @see llms_get_currency_symbols()\n *\n * @since 5.0.0\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'AED' => __( '&#1573;.&#1583;', 'lifterlms' ),\n\t'AFN' => __( '&#1547;', 'lifterlms' ),\n\t'ALL' => __( 'Lek', 'lifterlms' ),\n\t'AMD' => __( '&#1423;', 'lifterlms' ),\n\t'ANG' => __( '&#402;', 'lifterlms' ),\n\t'AOA' => __( 'Kz', 'lifterlms' ),\n\t'ARS' => __( '&#36;', 'lifterlms' ),\n\t'AUD' => __( '&#36;', 'lifterlms' ),\n\t'AWG' => __( '&#402;', 'lifterlms' ),\n\t'AZN' => __( 'm', 'lifterlms' ),\n\t'BAM' => __( 'KM', 'lifterlms' ),\n\t'BBD' => __( 'Bds&#36;', 'lifterlms' ),\n\t'BDT' => __( '&#2547;', 'lifterlms' ),\n\t'BGN' => __( '&#1051;&#1074;.', 'lifterlms' ),\n\t'BHD' => __( '.&#1583;.&#1576;', 'lifterlms' ),\n\t'BIF' => __( 'FBu', 'lifterlms' ),\n\t'BMD' => __( '&#36;', 'lifterlms' ),\n\t'BND' => __( 'B&#36;', 'lifterlms' ),\n\t'BOB' => __( 'Bs.', 'lifterlms' ),\n\t'BRL' => __( 'R&#36;', 'lifterlms' ),\n\t'BSD' => __( 'B&#36;', 'lifterlms' ),\n\t'BTC' => __( '&#8383;', 'lifterlms' ),\n\t'BTN' => __( 'Nu.', 'lifterlms' ),\n\t'BWP' => __( 'P', 'lifterlms' ),\n\t'BYN' => __( 'Br', 'lifterlms' ),\n\t'BZD' => __( '&#36;', 'lifterlms' ),\n\t'CAD' => __( '&#36;', 'lifterlms' ),\n\t'CDF' => __( 'FC', 'lifterlms' ),\n\t'CHF' => __( 'CHF', 'lifterlms' ),\n\t'CLP' => __( '&#36;', 'lifterlms' ),\n\t'CNY' => __( '&#165;', 'lifterlms' ),\n\t'COP' => __( '&#36;', 'lifterlms' ),\n\t'CRC' => __( '&#8353;', 'lifterlms' ),\n\t'CUC' => __( 'CUC', 'lifterlms' ),\n\t'CUP' => __( '&#36;', 'lifterlms' ),\n\t'CVE' => __( '&#36;', 'lifterlms' ),\n\t'CZK' => __( 'K&#269;', 'lifterlms' ),\n\t'DJF' => __( 'Fdj', 'lifterlms' ),\n\t'DKK' => __( 'Kr.', 'lifterlms' ),\n\t'DOP' => __( '&#36;', 'lifterlms' ),\n\t'DZD' => __( '&#1583;&#1580;', 'lifterlms' ),\n\t'EGP' => __( '&#1580;.&#1605;', 'lifterlms' ),\n\t'ERN' => __( 'Nfk', 'lifterlms' ),\n\t'ETB' => __( 'Nkf', 'lifterlms' ),\n\t'ETH' => __( 'ETH', 'lifterlms' ),\n\t'EUR' => __( '&#8364;', 'lifterlms' ),\n\t'FJD' => __( 'FJ&#36;', 'lifterlms' ),\n\t'FKP' => __( '&#163;', 'lifterlms' ),\n\t'GBP' => __( '&#163;', 'lifterlms' ),\n\t'GEL' => __( '&#4314;', 'lifterlms' ),\n\t'GHS' => __( 'GH&#8373;', 'lifterlms' ),\n\t'GIP' => __( '&#163;', 'lifterlms' ),\n\t'GMD' => __( 'D', 'lifterlms' ),\n\t'GNF' => __( 'FG', 'lifterlms' ),\n\t'GPP' => __( '&#163;', 'lifterlms' ),\n\t'GTQ' => __( 'Q', 'lifterlms' ),\n\t'GYD' => __( '&#36;', 'lifterlms' ),\n\t'HKD' => __( '&#36;', 'lifterlms' ),\n\t'HNL' => __( 'L', 'lifterlms' ),\n\t'HRK' => __( 'kn', 'lifterlms' ),\n\t'HTG' => __( 'G', 'lifterlms' ),\n\t'HUF' => __( 'Ft', 'lifterlms' ),\n\t'IDR' => __( 'Rp', 'lifterlms' ),\n\t'ILS' => __( '&#8362;', 'lifterlms' ),\n\t'IMP' => __( '&#163;', 'lifterlms' ),\n\t'INR' => __( '&#8377;', 'lifterlms' ),\n\t'IQD' => __( '&#1583;.&#1593;', 'lifterlms' ),\n\t'IRR' => __( '&#65020;', 'lifterlms' ),\n\t'ISK' => __( 'kr', 'lifterlms' ),\n\t'JEP' => __( '&#163;', 'lifterlms' ),\n\t'JMD' => __( 'J&#36;', 'lifterlms' ),\n\t'JOD' => __( '&#1575;.&#1583;', 'lifterlms' ),\n\t'JPY' => __( '&#165;', 'lifterlms' ),\n\t'KES' => __( 'KSh', 'lifterlms' ),\n\t'KGS' => __( '&#1083;&#1074;', 'lifterlms' ),\n\t'KHR' => __( 'KHR', 'lifterlms' ),\n\t'KMF' => __( 'CF', 'lifterlms' ),\n\t'KPW' => __( '&#8361;', 'lifterlms' ),\n\t'KRW' => __( '&#8361;', 'lifterlms' ),\n\t'KWD' => __( '&#1603;.&#1583;', 'lifterlms' ),\n\t'KYD' => __( '&#36;', 'lifterlms' ),\n\t// 'KZT' symbol manually updated, see https://github.com/gocodebox/lifterlms/issues/2475.\n\t'KZT' => __( '&#8376;', 'lifterlms' ),\n\t'LAK' => __( '&#8365;', 'lifterlms' ),\n\t'LBP' => __( '&#163;', 'lifterlms' ),\n\t'LKR' => __( 'Rs', 'lifterlms' ),\n\t'LRD' => __( '&#36;', 'lifterlms' ),\n\t'LSL' => __( 'L', 'lifterlms' ),\n\t'LTC' => __( 'LTC', 'lifterlms' ),\n\t'LYD' => __( '&#1583;.&#1604;', 'lifterlms' ),\n\t'MAD' => __( 'MAD', 'lifterlms' ),\n\t'MDL' => __( 'L', 'lifterlms' ),\n\t'MGA' => __( 'Ar', 'lifterlms' ),\n\t'MKD' => __( '&#1076;&#1077;&#1085;', 'lifterlms' ),\n\t'MMK' => __( 'K', 'lifterlms' ),\n\t'MNT' => __( '&#8366;', 'lifterlms' ),\n\t'MOP' => __( '&#36;', 'lifterlms' ),\n\t'MRO' => __( 'MRU', 'lifterlms' ),\n\t'MUR' => __( '&#8360;', 'lifterlms' ),\n\t'MVR' => __( 'Rf', 'lifterlms' ),\n\t'MWK' => __( 'MK', 'lifterlms' ),\n\t'MXN' => __( '&#36;', 'lifterlms' ),\n\t'MYR' => __( 'RM', 'lifterlms' ),\n\t'MZN' => __( 'MT', 'lifterlms' ),\n\t'NAD' => __( '&#36;', 'lifterlms' ),\n\t'NGN' => __( '&#8358;', 'lifterlms' ),\n\t'NIO' => __( 'C&#36;', 'lifterlms' ),\n\t'NOK' => __( 'kr', 'lifterlms' ),\n\t'NPR' => __( '&#8360;', 'lifterlms' ),\n\t'NZD' => __( '&#36;', 'lifterlms' ),\n\t'OMR' => __( '.&#1593;.&#1585;', 'lifterlms' ),\n\t'PAB' => __( 'B&#47;.', 'lifterlms' ),\n\t'PEN' => __( 'S&#47;.', 'lifterlms' ),\n\t'PGK' => __( 'K', 'lifterlms' ),\n\t'PHP' => __( '&#8369;', 'lifterlms' ),\n\t'PKR' => __( '&#8360;', 'lifterlms' ),\n\t'PLN' => __( 'z&#322;', 'lifterlms' ),\n\t'PRB' => __( '&#8381;', 'lifterlms' ),\n\t'PYG' => __( '&#8370;', 'lifterlms' ),\n\t'QAR' => __( '&#1602;.&#1585;', 'lifterlms' ),\n\t'RON' => __( 'lei', 'lifterlms' ),\n\t'RSD' => __( 'din', 'lifterlms' ),\n\t'RUB' => __( '&#8381;', 'lifterlms' ),\n\t'RWF' => __( 'FRw', 'lifterlms' ),\n\t'SAR' => __( '&#65020;', 'lifterlms' ),\n\t'SBD' => __( 'Si&#36;', 'lifterlms' ),\n\t'SCR' => __( 'SRe', 'lifterlms' ),\n\t'SDG' => __( '.&#1587;.&#1580;', 'lifterlms' ),\n\t'SEK' => __( 'kr', 'lifterlms' ),\n\t'SGD' => __( '&#36;', 'lifterlms' ),\n\t'SHP' => __( '&#163;', 'lifterlms' ),\n\t'SLL' => __( 'Le', 'lifterlms' ),\n\t'SOS' => __( 'Sh.so.', 'lifterlms' ),\n\t'SRD' => __( '&#36;', 'lifterlms' ),\n\t'SSP' => __( '&#163;', 'lifterlms' ),\n\t'STD' => __( 'Db', 'lifterlms' ),\n\t'SYP' => __( 'LS', 'lifterlms' ),\n\t'SZL' => __( 'E', 'lifterlms' ),\n\t'THB' => __( '&#3647;', 'lifterlms' ),\n\t'TJS' => __( 'SM', 'lifterlms' ),\n\t'TMT' => __( 'T', 'lifterlms' ),\n\t'TND' => __( '&#1578;.&#1583;', 'lifterlms' ),\n\t'TOP' => __( '&#36;', 'lifterlms' ),\n\t'TRY' => __( '&#8378;', 'lifterlms' ),\n\t'TTD' => __( '&#36;', 'lifterlms' ),\n\t'TWD' => __( '&#36;', 'lifterlms' ),\n\t'TZS' => __( 'TSh', 'lifterlms' ),\n\t'UAH' => __( '&#8372;', 'lifterlms' ),\n\t'UGX' => __( 'USh', 'lifterlms' ),\n\t'USD' => __( '&#36;', 'lifterlms' ),\n\t'UYU' => __( '&#36;', 'lifterlms' ),\n\t'UZS' => __( '&#1083;&#1074;', 'lifterlms' ),\n\t'VEF' => __( 'Bs', 'lifterlms' ),\n\t'VND' => __( '&#8363;', 'lifterlms' ),\n\t'VUV' => __( 'VT', 'lifterlms' ),\n\t'WST' => __( 'SAT', 'lifterlms' ),\n\t'XAF' => __( 'FCFA', 'lifterlms' ),\n\t'XCD' => __( '&#36;', 'lifterlms' ),\n\t'XOF' => __( 'CFA', 'lifterlms' ),\n\t'XPF' => __( '&#8355;', 'lifterlms' ),\n\t'YER' => __( '&#65020;', 'lifterlms' ),\n\t'ZAR' => __( 'R', 'lifterlms' ),\n\t'ZMW' => __( 'ZK', 'lifterlms' ),\n\t'ZWL' => __( '&#36;', 'lifterlms' ),\n);\n"
  },
  {
    "path": "languages/states.php",
    "content": "<?php\n/**\n * States\n *\n * Returns an array of country states. This deprecates and replaces the /states/ directory found in older versions.\n *\n * States should be defined in English and translated native through localisation files.\n *\n * Country codes and states (or province) names should follow the Unicode CLDR recommendation (https://cldr.unicode.org/translation/displaynames/countryregion-territory-names).\n *\n * Countries defined with empty arrays have no states. These should also be defined in countries-address-info.php to mark the state field as not required and hidden.\n *\n * @package WooCommerce\\i18n\n * @version 3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n\t'AF' => array(),\n\t'AL' => array( // Albanian states.\n\t\t'AL-01' => __( 'Berat', 'lifterlms' ),\n\t\t'AL-09' => __( 'Dibër', 'lifterlms' ),\n\t\t'AL-02' => __( 'Durrës', 'lifterlms' ),\n\t\t'AL-03' => __( 'Elbasan', 'lifterlms' ),\n\t\t'AL-04' => __( 'Fier', 'lifterlms' ),\n\t\t'AL-05' => __( 'Gjirokastër', 'lifterlms' ),\n\t\t'AL-06' => __( 'Korçë', 'lifterlms' ),\n\t\t'AL-07' => __( 'Kukës', 'lifterlms' ),\n\t\t'AL-08' => __( 'Lezhë', 'lifterlms' ),\n\t\t'AL-10' => __( 'Shkodër', 'lifterlms' ),\n\t\t'AL-11' => __( 'Tirana', 'lifterlms' ),\n\t\t'AL-12' => __( 'Vlorë', 'lifterlms' ),\n\t),\n\t'AO' => array( // Angolan states.\n\t\t'BGO' => __( 'Bengo', 'lifterlms' ),\n\t\t'BLU' => __( 'Benguela', 'lifterlms' ),\n\t\t'BIE' => __( 'Bié', 'lifterlms' ),\n\t\t'CAB' => __( 'Cabinda', 'lifterlms' ),\n\t\t'CNN' => __( 'Cunene', 'lifterlms' ),\n\t\t'HUA' => __( 'Huambo', 'lifterlms' ),\n\t\t'HUI' => __( 'Huíla', 'lifterlms' ),\n\t\t'CCU' => __( 'Kuando Kubango', 'lifterlms' ),\n\t\t'CNO' => __( 'Kwanza-Norte', 'lifterlms' ),\n\t\t'CUS' => __( 'Kwanza-Sul', 'lifterlms' ),\n\t\t'LUA' => __( 'Luanda', 'lifterlms' ),\n\t\t'LNO' => __( 'Lunda-Norte', 'lifterlms' ),\n\t\t'LSU' => __( 'Lunda-Sul', 'lifterlms' ),\n\t\t'MAL' => __( 'Malanje', 'lifterlms' ),\n\t\t'MOX' => __( 'Moxico', 'lifterlms' ),\n\t\t'NAM' => __( 'Namibe', 'lifterlms' ),\n\t\t'UIG' => __( 'Uíge', 'lifterlms' ),\n\t\t'ZAI' => __( 'Zaire', 'lifterlms' ),\n\t),\n\t'AR' => array( // Argentinian provinces.\n\t\t'C' => __( 'Ciudad Autónoma de Buenos Aires', 'lifterlms' ),\n\t\t'B' => __( 'Buenos Aires', 'lifterlms' ),\n\t\t'K' => __( 'Catamarca', 'lifterlms' ),\n\t\t'H' => __( 'Chaco', 'lifterlms' ),\n\t\t'U' => __( 'Chubut', 'lifterlms' ),\n\t\t'X' => __( 'Córdoba', 'lifterlms' ),\n\t\t'W' => __( 'Corrientes', 'lifterlms' ),\n\t\t'E' => __( 'Entre Ríos', 'lifterlms' ),\n\t\t'P' => __( 'Formosa', 'lifterlms' ),\n\t\t'Y' => __( 'Jujuy', 'lifterlms' ),\n\t\t'L' => __( 'La Pampa', 'lifterlms' ),\n\t\t'F' => __( 'La Rioja', 'lifterlms' ),\n\t\t'M' => __( 'Mendoza', 'lifterlms' ),\n\t\t'N' => __( 'Misiones', 'lifterlms' ),\n\t\t'Q' => __( 'Neuquén', 'lifterlms' ),\n\t\t'R' => __( 'Río Negro', 'lifterlms' ),\n\t\t'A' => __( 'Salta', 'lifterlms' ),\n\t\t'J' => __( 'San Juan', 'lifterlms' ),\n\t\t'D' => __( 'San Luis', 'lifterlms' ),\n\t\t'Z' => __( 'Santa Cruz', 'lifterlms' ),\n\t\t'S' => __( 'Santa Fe', 'lifterlms' ),\n\t\t'G' => __( 'Santiago del Estero', 'lifterlms' ),\n\t\t'V' => __( 'Tierra del Fuego', 'lifterlms' ),\n\t\t'T' => __( 'Tucumán', 'lifterlms' ),\n\t),\n\t'AT' => array(),\n\t'AU' => array( // Australian states.\n\t\t'ACT' => __( 'Australian Capital Territory', 'lifterlms' ),\n\t\t'NSW' => __( 'New South Wales', 'lifterlms' ),\n\t\t'NT'  => __( 'Northern Territory', 'lifterlms' ),\n\t\t'QLD' => __( 'Queensland', 'lifterlms' ),\n\t\t'SA'  => __( 'South Australia', 'lifterlms' ),\n\t\t'TAS' => __( 'Tasmania', 'lifterlms' ),\n\t\t'VIC' => __( 'Victoria', 'lifterlms' ),\n\t\t'WA'  => __( 'Western Australia', 'lifterlms' ),\n\t),\n\t'AX' => array(),\n\t'BD' => array( // Bangladeshi districts.\n\t\t'BD-05' => __( 'Bagerhat', 'lifterlms' ),\n\t\t'BD-01' => __( 'Bandarban', 'lifterlms' ),\n\t\t'BD-02' => __( 'Barguna', 'lifterlms' ),\n\t\t'BD-06' => __( 'Barishal', 'lifterlms' ),\n\t\t'BD-07' => __( 'Bhola', 'lifterlms' ),\n\t\t'BD-03' => __( 'Bogura', 'lifterlms' ),\n\t\t'BD-04' => __( 'Brahmanbaria', 'lifterlms' ),\n\t\t'BD-09' => __( 'Chandpur', 'lifterlms' ),\n\t\t'BD-10' => __( 'Chattogram', 'lifterlms' ),\n\t\t'BD-12' => __( 'Chuadanga', 'lifterlms' ),\n\t\t'BD-11' => __( \"Cox's Bazar\", 'lifterlms' ),\n\t\t'BD-08' => __( 'Cumilla', 'lifterlms' ),\n\t\t'BD-13' => __( 'Dhaka', 'lifterlms' ),\n\t\t'BD-14' => __( 'Dinajpur', 'lifterlms' ),\n\t\t'BD-15' => __( 'Faridpur ', 'lifterlms' ),\n\t\t'BD-16' => __( 'Feni', 'lifterlms' ),\n\t\t'BD-19' => __( 'Gaibandha', 'lifterlms' ),\n\t\t'BD-18' => __( 'Gazipur', 'lifterlms' ),\n\t\t'BD-17' => __( 'Gopalganj', 'lifterlms' ),\n\t\t'BD-20' => __( 'Habiganj', 'lifterlms' ),\n\t\t'BD-21' => __( 'Jamalpur', 'lifterlms' ),\n\t\t'BD-22' => __( 'Jashore', 'lifterlms' ),\n\t\t'BD-25' => __( 'Jhalokati', 'lifterlms' ),\n\t\t'BD-23' => __( 'Jhenaidah', 'lifterlms' ),\n\t\t'BD-24' => __( 'Joypurhat', 'lifterlms' ),\n\t\t'BD-29' => __( 'Khagrachhari', 'lifterlms' ),\n\t\t'BD-27' => __( 'Khulna', 'lifterlms' ),\n\t\t'BD-26' => __( 'Kishoreganj', 'lifterlms' ),\n\t\t'BD-28' => __( 'Kurigram', 'lifterlms' ),\n\t\t'BD-30' => __( 'Kushtia', 'lifterlms' ),\n\t\t'BD-31' => __( 'Lakshmipur', 'lifterlms' ),\n\t\t'BD-32' => __( 'Lalmonirhat', 'lifterlms' ),\n\t\t'BD-36' => __( 'Madaripur', 'lifterlms' ),\n\t\t'BD-37' => __( 'Magura', 'lifterlms' ),\n\t\t'BD-33' => __( 'Manikganj ', 'lifterlms' ),\n\t\t'BD-39' => __( 'Meherpur', 'lifterlms' ),\n\t\t'BD-38' => __( 'Moulvibazar', 'lifterlms' ),\n\t\t'BD-35' => __( 'Munshiganj', 'lifterlms' ),\n\t\t'BD-34' => __( 'Mymensingh', 'lifterlms' ),\n\t\t'BD-48' => __( 'Naogaon', 'lifterlms' ),\n\t\t'BD-43' => __( 'Narail', 'lifterlms' ),\n\t\t'BD-40' => __( 'Narayanganj', 'lifterlms' ),\n\t\t'BD-42' => __( 'Narsingdi', 'lifterlms' ),\n\t\t'BD-44' => __( 'Natore', 'lifterlms' ),\n\t\t'BD-45' => __( 'Nawabganj', 'lifterlms' ),\n\t\t'BD-41' => __( 'Netrakona', 'lifterlms' ),\n\t\t'BD-46' => __( 'Nilphamari', 'lifterlms' ),\n\t\t'BD-47' => __( 'Noakhali', 'lifterlms' ),\n\t\t'BD-49' => __( 'Pabna', 'lifterlms' ),\n\t\t'BD-52' => __( 'Panchagarh', 'lifterlms' ),\n\t\t'BD-51' => __( 'Patuakhali', 'lifterlms' ),\n\t\t'BD-50' => __( 'Pirojpur', 'lifterlms' ),\n\t\t'BD-53' => __( 'Rajbari', 'lifterlms' ),\n\t\t'BD-54' => __( 'Rajshahi', 'lifterlms' ),\n\t\t'BD-56' => __( 'Rangamati', 'lifterlms' ),\n\t\t'BD-55' => __( 'Rangpur', 'lifterlms' ),\n\t\t'BD-58' => __( 'Satkhira', 'lifterlms' ),\n\t\t'BD-62' => __( 'Shariatpur', 'lifterlms' ),\n\t\t'BD-57' => __( 'Sherpur', 'lifterlms' ),\n\t\t'BD-59' => __( 'Sirajganj', 'lifterlms' ),\n\t\t'BD-61' => __( 'Sunamganj', 'lifterlms' ),\n\t\t'BD-60' => __( 'Sylhet', 'lifterlms' ),\n\t\t'BD-63' => __( 'Tangail', 'lifterlms' ),\n\t\t'BD-64' => __( 'Thakurgaon', 'lifterlms' ),\n\t),\n\t'BE' => array(),\n\t'BG' => array( // Bulgarian states.\n\t\t'BG-01' => __( 'Blagoevgrad', 'lifterlms' ),\n\t\t'BG-02' => __( 'Burgas', 'lifterlms' ),\n\t\t'BG-08' => __( 'Dobrich', 'lifterlms' ),\n\t\t'BG-07' => __( 'Gabrovo', 'lifterlms' ),\n\t\t'BG-26' => __( 'Haskovo', 'lifterlms' ),\n\t\t'BG-09' => __( 'Kardzhali', 'lifterlms' ),\n\t\t'BG-10' => __( 'Kyustendil', 'lifterlms' ),\n\t\t'BG-11' => __( 'Lovech', 'lifterlms' ),\n\t\t'BG-12' => __( 'Montana', 'lifterlms' ),\n\t\t'BG-13' => __( 'Pazardzhik', 'lifterlms' ),\n\t\t'BG-14' => __( 'Pernik', 'lifterlms' ),\n\t\t'BG-15' => __( 'Pleven', 'lifterlms' ),\n\t\t'BG-16' => __( 'Plovdiv', 'lifterlms' ),\n\t\t'BG-17' => __( 'Razgrad', 'lifterlms' ),\n\t\t'BG-18' => __( 'Ruse', 'lifterlms' ),\n\t\t'BG-27' => __( 'Shumen', 'lifterlms' ),\n\t\t'BG-19' => __( 'Silistra', 'lifterlms' ),\n\t\t'BG-20' => __( 'Sliven', 'lifterlms' ),\n\t\t'BG-21' => __( 'Smolyan', 'lifterlms' ),\n\t\t'BG-23' => __( 'Sofia District', 'lifterlms' ),\n\t\t'BG-22' => __( 'Sofia', 'lifterlms' ),\n\t\t'BG-24' => __( 'Stara Zagora', 'lifterlms' ),\n\t\t'BG-25' => __( 'Targovishte', 'lifterlms' ),\n\t\t'BG-03' => __( 'Varna', 'lifterlms' ),\n\t\t'BG-04' => __( 'Veliko Tarnovo', 'lifterlms' ),\n\t\t'BG-05' => __( 'Vidin', 'lifterlms' ),\n\t\t'BG-06' => __( 'Vratsa', 'lifterlms' ),\n\t\t'BG-28' => __( 'Yambol', 'lifterlms' ),\n\t),\n\t'BH' => array(),\n\t'BI' => array(),\n\t'BJ' => array( // Beninese states.\n\t\t'AL' => __( 'Alibori', 'lifterlms' ),\n\t\t'AK' => __( 'Atakora', 'lifterlms' ),\n\t\t'AQ' => __( 'Atlantique', 'lifterlms' ),\n\t\t'BO' => __( 'Borgou', 'lifterlms' ),\n\t\t'CO' => __( 'Collines', 'lifterlms' ),\n\t\t'KO' => __( 'Kouffo', 'lifterlms' ),\n\t\t'DO' => __( 'Donga', 'lifterlms' ),\n\t\t'LI' => __( 'Littoral', 'lifterlms' ),\n\t\t'MO' => __( 'Mono', 'lifterlms' ),\n\t\t'OU' => __( 'Ouémé', 'lifterlms' ),\n\t\t'PL' => __( 'Plateau', 'lifterlms' ),\n\t\t'ZO' => __( 'Zou', 'lifterlms' ),\n\t),\n\t'BO' => array( // Bolivian states.\n\t\t'BO-B' => __( 'Beni', 'lifterlms' ),\n\t\t'BO-H' => __( 'Chuquisaca', 'lifterlms' ),\n\t\t'BO-C' => __( 'Cochabamba', 'lifterlms' ),\n\t\t'BO-L' => __( 'La Paz', 'lifterlms' ),\n\t\t'BO-O' => __( 'Oruro', 'lifterlms' ),\n\t\t'BO-N' => __( 'Pando', 'lifterlms' ),\n\t\t'BO-P' => __( 'Potosí', 'lifterlms' ),\n\t\t'BO-S' => __( 'Santa Cruz', 'lifterlms' ),\n\t\t'BO-T' => __( 'Tarija', 'lifterlms' ),\n\t),\n\t'BR' => array( // Brazilian states.\n\t\t'AC' => __( 'Acre', 'lifterlms' ),\n\t\t'AL' => __( 'Alagoas', 'lifterlms' ),\n\t\t'AP' => __( 'Amapá', 'lifterlms' ),\n\t\t'AM' => __( 'Amazonas', 'lifterlms' ),\n\t\t'BA' => __( 'Bahia', 'lifterlms' ),\n\t\t'CE' => __( 'Ceará', 'lifterlms' ),\n\t\t'DF' => __( 'Distrito Federal', 'lifterlms' ),\n\t\t'ES' => __( 'Espírito Santo', 'lifterlms' ),\n\t\t'GO' => __( 'Goiás', 'lifterlms' ),\n\t\t'MA' => __( 'Maranhão', 'lifterlms' ),\n\t\t'MT' => __( 'Mato Grosso', 'lifterlms' ),\n\t\t'MS' => __( 'Mato Grosso do Sul', 'lifterlms' ),\n\t\t'MG' => __( 'Minas Gerais', 'lifterlms' ),\n\t\t'PA' => __( 'Pará', 'lifterlms' ),\n\t\t'PB' => __( 'Paraíba', 'lifterlms' ),\n\t\t'PR' => __( 'Paraná', 'lifterlms' ),\n\t\t'PE' => __( 'Pernambuco', 'lifterlms' ),\n\t\t'PI' => __( 'Piauí', 'lifterlms' ),\n\t\t'RJ' => __( 'Rio de Janeiro', 'lifterlms' ),\n\t\t'RN' => __( 'Rio Grande do Norte', 'lifterlms' ),\n\t\t'RS' => __( 'Rio Grande do Sul', 'lifterlms' ),\n\t\t'RO' => __( 'Rondônia', 'lifterlms' ),\n\t\t'RR' => __( 'Roraima', 'lifterlms' ),\n\t\t'SC' => __( 'Santa Catarina', 'lifterlms' ),\n\t\t'SP' => __( 'São Paulo', 'lifterlms' ),\n\t\t'SE' => __( 'Sergipe', 'lifterlms' ),\n\t\t'TO' => __( 'Tocantins', 'lifterlms' ),\n\t),\n\t'CA' => array( // Canadian states.\n\t\t'AB' => __( 'Alberta', 'lifterlms' ),\n\t\t'BC' => __( 'British Columbia', 'lifterlms' ),\n\t\t'MB' => __( 'Manitoba', 'lifterlms' ),\n\t\t'NB' => __( 'New Brunswick', 'lifterlms' ),\n\t\t'NL' => __( 'Newfoundland and Labrador', 'lifterlms' ),\n\t\t'NT' => __( 'Northwest Territories', 'lifterlms' ),\n\t\t'NS' => __( 'Nova Scotia', 'lifterlms' ),\n\t\t'NU' => __( 'Nunavut', 'lifterlms' ),\n\t\t'ON' => __( 'Ontario', 'lifterlms' ),\n\t\t'PE' => __( 'Prince Edward Island', 'lifterlms' ),\n\t\t'QC' => __( 'Quebec', 'lifterlms' ),\n\t\t'SK' => __( 'Saskatchewan', 'lifterlms' ),\n\t\t'YT' => __( 'Yukon Territory', 'lifterlms' ),\n\t),\n\t'CH' => array( // Swiss cantons.\n\t\t'AG' => __( 'Aargau', 'lifterlms' ),\n\t\t'AR' => __( 'Appenzell Ausserrhoden', 'lifterlms' ),\n\t\t'AI' => __( 'Appenzell Innerrhoden', 'lifterlms' ),\n\t\t'BL' => __( 'Basel-Landschaft', 'lifterlms' ),\n\t\t'BS' => __( 'Basel-Stadt', 'lifterlms' ),\n\t\t'BE' => __( 'Bern', 'lifterlms' ),\n\t\t'FR' => __( 'Fribourg', 'lifterlms' ),\n\t\t'GE' => __( 'Geneva', 'lifterlms' ),\n\t\t'GL' => __( 'Glarus', 'lifterlms' ),\n\t\t'GR' => __( 'Graubünden', 'lifterlms' ),\n\t\t'JU' => __( 'Jura', 'lifterlms' ),\n\t\t'LU' => __( 'Luzern', 'lifterlms' ),\n\t\t'NE' => __( 'Neuchâtel', 'lifterlms' ),\n\t\t'NW' => __( 'Nidwalden', 'lifterlms' ),\n\t\t'OW' => __( 'Obwalden', 'lifterlms' ),\n\t\t'SH' => __( 'Schaffhausen', 'lifterlms' ),\n\t\t'SZ' => __( 'Schwyz', 'lifterlms' ),\n\t\t'SO' => __( 'Solothurn', 'lifterlms' ),\n\t\t'SG' => __( 'St. Gallen', 'lifterlms' ),\n\t\t'TG' => __( 'Thurgau', 'lifterlms' ),\n\t\t'TI' => __( 'Ticino', 'lifterlms' ),\n\t\t'UR' => __( 'Uri', 'lifterlms' ),\n\t\t'VS' => __( 'Valais', 'lifterlms' ),\n\t\t'VD' => __( 'Vaud', 'lifterlms' ),\n\t\t'ZG' => __( 'Zug', 'lifterlms' ),\n\t\t'ZH' => __( 'Zürich', 'lifterlms' ),\n\t),\n\t'CL' => array( // Chilean states.\n\t\t'CL-AI' => __( 'Aisén del General Carlos Ibañez del Campo', 'lifterlms' ),\n\t\t'CL-AN' => __( 'Antofagasta', 'lifterlms' ),\n\t\t'CL-AP' => __( 'Arica y Parinacota', 'lifterlms' ),\n\t\t'CL-AR' => __( 'La Araucanía', 'lifterlms' ),\n\t\t'CL-AT' => __( 'Atacama', 'lifterlms' ),\n\t\t'CL-BI' => __( 'Biobío', 'lifterlms' ),\n\t\t'CL-CO' => __( 'Coquimbo', 'lifterlms' ),\n\t\t'CL-LI' => __( 'Libertador General Bernardo O\\'Higgins', 'lifterlms' ),\n\t\t'CL-LL' => __( 'Los Lagos', 'lifterlms' ),\n\t\t'CL-LR' => __( 'Los Ríos', 'lifterlms' ),\n\t\t'CL-MA' => __( 'Magallanes', 'lifterlms' ),\n\t\t'CL-ML' => __( 'Maule', 'lifterlms' ),\n\t\t'CL-NB' => __( 'Ñuble', 'lifterlms' ),\n\t\t'CL-RM' => __( 'Región Metropolitana de Santiago', 'lifterlms' ),\n\t\t'CL-TA' => __( 'Tarapacá', 'lifterlms' ),\n\t\t'CL-VS' => __( 'Valparaíso', 'lifterlms' ),\n\t),\n\t'CN' => array( // Chinese states.\n\t\t'CN1'  => __( 'Yunnan / 云南', 'lifterlms' ),\n\t\t'CN2'  => __( 'Beijing / 北京', 'lifterlms' ),\n\t\t'CN3'  => __( 'Tianjin / 天津', 'lifterlms' ),\n\t\t'CN4'  => __( 'Hebei / 河北', 'lifterlms' ),\n\t\t'CN5'  => __( 'Shanxi / 山西', 'lifterlms' ),\n\t\t'CN6'  => __( 'Inner Mongolia / 內蒙古', 'lifterlms' ),\n\t\t'CN7'  => __( 'Liaoning / 辽宁', 'lifterlms' ),\n\t\t'CN8'  => __( 'Jilin / 吉林', 'lifterlms' ),\n\t\t'CN9'  => __( 'Heilongjiang / 黑龙江', 'lifterlms' ),\n\t\t'CN10' => __( 'Shanghai / 上海', 'lifterlms' ),\n\t\t'CN11' => __( 'Jiangsu / 江苏', 'lifterlms' ),\n\t\t'CN12' => __( 'Zhejiang / 浙江', 'lifterlms' ),\n\t\t'CN13' => __( 'Anhui / 安徽', 'lifterlms' ),\n\t\t'CN14' => __( 'Fujian / 福建', 'lifterlms' ),\n\t\t'CN15' => __( 'Jiangxi / 江西', 'lifterlms' ),\n\t\t'CN16' => __( 'Shandong / 山东', 'lifterlms' ),\n\t\t'CN17' => __( 'Henan / 河南', 'lifterlms' ),\n\t\t'CN18' => __( 'Hubei / 湖北', 'lifterlms' ),\n\t\t'CN19' => __( 'Hunan / 湖南', 'lifterlms' ),\n\t\t'CN20' => __( 'Guangdong / 广东', 'lifterlms' ),\n\t\t'CN21' => __( 'Guangxi Zhuang / 广西壮族', 'lifterlms' ),\n\t\t'CN22' => __( 'Hainan / 海南', 'lifterlms' ),\n\t\t'CN23' => __( 'Chongqing / 重庆', 'lifterlms' ),\n\t\t'CN24' => __( 'Sichuan / 四川', 'lifterlms' ),\n\t\t'CN25' => __( 'Guizhou / 贵州', 'lifterlms' ),\n\t\t'CN26' => __( 'Shaanxi / 陕西', 'lifterlms' ),\n\t\t'CN27' => __( 'Gansu / 甘肃', 'lifterlms' ),\n\t\t'CN28' => __( 'Qinghai / 青海', 'lifterlms' ),\n\t\t'CN29' => __( 'Ningxia Hui / 宁夏', 'lifterlms' ),\n\t\t'CN30' => __( 'Macao / 澳门', 'lifterlms' ),\n\t\t'CN31' => __( 'Tibet / 西藏', 'lifterlms' ),\n\t\t'CN32' => __( 'Xinjiang / 新疆', 'lifterlms' ),\n\t),\n\t'CO' => array( // Colombian states.\n\t\t'CO-AMA' => __( 'Amazonas', 'lifterlms' ),\n\t\t'CO-ANT' => __( 'Antioquia', 'lifterlms' ),\n\t\t'CO-ARA' => __( 'Arauca', 'lifterlms' ),\n\t\t'CO-ATL' => __( 'Atlántico', 'lifterlms' ),\n\t\t'CO-BOL' => __( 'Bolívar', 'lifterlms' ),\n\t\t'CO-BOY' => __( 'Boyacá', 'lifterlms' ),\n\t\t'CO-CAL' => __( 'Caldas', 'lifterlms' ),\n\t\t'CO-CAQ' => __( 'Caquetá', 'lifterlms' ),\n\t\t'CO-CAS' => __( 'Casanare', 'lifterlms' ),\n\t\t'CO-CAU' => __( 'Cauca', 'lifterlms' ),\n\t\t'CO-CES' => __( 'Cesar', 'lifterlms' ),\n\t\t'CO-CHO' => __( 'Chocó', 'lifterlms' ),\n\t\t'CO-COR' => __( 'Córdoba', 'lifterlms' ),\n\t\t'CO-CUN' => __( 'Cundinamarca', 'lifterlms' ),\n\t\t'CO-DC' => __( 'Capital District', 'lifterlms' ),\n\t\t'CO-GUA' => __( 'Guainía', 'lifterlms' ),\n\t\t'CO-GUV' => __( 'Guaviare', 'lifterlms' ),\n\t\t'CO-HUI' => __( 'Huila', 'lifterlms' ),\n\t\t'CO-LAG' => __( 'La Guajira', 'lifterlms' ),\n\t\t'CO-MAG' => __( 'Magdalena', 'lifterlms' ),\n\t\t'CO-MET' => __( 'Meta', 'lifterlms' ),\n\t\t'CO-NAR' => __( 'Nariño', 'lifterlms' ),\n\t\t'CO-NSA' => __( 'Norte de Santander', 'lifterlms' ),\n\t\t'CO-PUT' => __( 'Putumayo', 'lifterlms' ),\n\t\t'CO-QUI' => __( 'Quindío', 'lifterlms' ),\n\t\t'CO-RIS' => __( 'Risaralda', 'lifterlms' ),\n\t\t'CO-SAN' => __( 'Santander', 'lifterlms' ),\n\t\t'CO-SAP' => __( 'San Andrés & Providencia', 'lifterlms' ),\n\t\t'CO-SUC' => __( 'Sucre', 'lifterlms' ),\n\t\t'CO-TOL' => __( 'Tolima', 'lifterlms' ),\n\t\t'CO-VAC' => __( 'Valle del Cauca', 'lifterlms' ),\n\t\t'CO-VAU' => __( 'Vaupés', 'lifterlms' ),\n\t\t'CO-VID' => __( 'Vichada', 'lifterlms' ),\n\t),\n\t'CR' => array( // Costa Rican states.\n\t\t'CR-A' => __( 'Alajuela', 'lifterlms' ),\n\t\t'CR-C' => __( 'Cartago', 'lifterlms' ),\n\t\t'CR-G' => __( 'Guanacaste', 'lifterlms' ),\n\t\t'CR-H' => __( 'Heredia', 'lifterlms' ),\n\t\t'CR-L' => __( 'Limón', 'lifterlms' ),\n\t\t'CR-P' => __( 'Puntarenas', 'lifterlms' ),\n\t\t'CR-SJ' => __( 'San José', 'lifterlms' ),\n\t),\n\t'CZ' => array(),\n\t'DE' => array( // German states.\n\t\t'DE-BW' => __( 'Baden-Württemberg', 'lifterlms' ),\n\t\t'DE-BY' => __( 'Bavaria', 'lifterlms' ),\n\t\t'DE-BE' => __( 'Berlin', 'lifterlms' ),\n\t\t'DE-BB' => __( 'Brandenburg', 'lifterlms' ),\n\t\t'DE-HB' => __( 'Bremen', 'lifterlms' ),\n\t\t'DE-HH' => __( 'Hamburg', 'lifterlms' ),\n\t\t'DE-HE' => __( 'Hesse', 'lifterlms' ),\n\t\t'DE-MV' => __( 'Mecklenburg-Vorpommern', 'lifterlms' ),\n\t\t'DE-NI' => __( 'Lower Saxony', 'lifterlms' ),\n\t\t'DE-NW' => __( 'North Rhine-Westphalia', 'lifterlms' ),\n\t\t'DE-RP' => __( 'Rhineland-Palatinate', 'lifterlms' ),\n\t\t'DE-SL' => __( 'Saarland', 'lifterlms' ),\n\t\t'DE-SN' => __( 'Saxony', 'lifterlms' ),\n\t\t'DE-ST' => __( 'Saxony-Anhalt', 'lifterlms' ),\n\t\t'DE-SH' => __( 'Schleswig-Holstein', 'lifterlms' ),\n\t\t'DE-TH' => __( 'Thuringia', 'lifterlms' ),\n\t),\n\t'DK' => array(),\n\t'DO' => array( // Dominican states.\n\t\t'DO-01' => __( 'Distrito Nacional', 'lifterlms' ),\n\t\t'DO-02' => __( 'Azua', 'lifterlms' ),\n\t\t'DO-03' => __( 'Baoruco', 'lifterlms' ),\n\t\t'DO-04' => __( 'Barahona', 'lifterlms' ),\n\t\t'DO-33' => __( 'Cibao Nordeste', 'lifterlms' ),\n\t\t'DO-34' => __( 'Cibao Noroeste', 'lifterlms' ),\n\t\t'DO-35' => __( 'Cibao Norte', 'lifterlms' ),\n\t\t'DO-36' => __( 'Cibao Sur', 'lifterlms' ),\n\t\t'DO-05' => __( 'Dajabón', 'lifterlms' ),\n\t\t'DO-06' => __( 'Duarte', 'lifterlms' ),\n\t\t'DO-08' => __( 'El Seibo', 'lifterlms' ),\n\t\t'DO-37' => __( 'El Valle', 'lifterlms' ),\n\t\t'DO-07' => __( 'Elías Piña', 'lifterlms' ),\n\t\t'DO-38' => __( 'Enriquillo', 'lifterlms' ),\n\t\t'DO-09' => __( 'Espaillat', 'lifterlms' ),\n\t\t'DO-30' => __( 'Hato Mayor', 'lifterlms' ),\n\t\t'DO-19' => __( 'Hermanas Mirabal', 'lifterlms' ),\n\t\t'DO-39' => __( 'Higüamo', 'lifterlms' ),\n\t\t'DO-10' => __( 'Independencia', 'lifterlms' ),\n\t\t'DO-11' => __( 'La Altagracia', 'lifterlms' ),\n\t\t'DO-12' => __( 'La Romana', 'lifterlms' ),\n\t\t'DO-13' => __( 'La Vega', 'lifterlms' ),\n\t\t'DO-14' => __( 'María Trinidad Sánchez', 'lifterlms' ),\n\t\t'DO-28' => __( 'Monseñor Nouel', 'lifterlms' ),\n\t\t'DO-15' => __( 'Monte Cristi', 'lifterlms' ),\n\t\t'DO-29' => __( 'Monte Plata', 'lifterlms' ),\n\t\t'DO-40' => __( 'Ozama', 'lifterlms' ),\n\t\t'DO-16' => __( 'Pedernales', 'lifterlms' ),\n\t\t'DO-17' => __( 'Peravia', 'lifterlms' ),\n\t\t'DO-18' => __( 'Puerto Plata', 'lifterlms' ),\n\t\t'DO-20' => __( 'Samaná', 'lifterlms' ),\n\t\t'DO-21' => __( 'San Cristóbal', 'lifterlms' ),\n\t\t'DO-31' => __( 'San José de Ocoa', 'lifterlms' ),\n\t\t'DO-22' => __( 'San Juan', 'lifterlms' ),\n\t\t'DO-23' => __( 'San Pedro de Macorís', 'lifterlms' ),\n\t\t'DO-24' => __( 'Sánchez Ramírez', 'lifterlms' ),\n\t\t'DO-25' => __( 'Santiago', 'lifterlms' ),\n\t\t'DO-26' => __( 'Santiago Rodríguez', 'lifterlms' ),\n\t\t'DO-32' => __( 'Santo Domingo', 'lifterlms' ),\n\t\t'DO-41' => __( 'Valdesia', 'lifterlms' ),\n\t\t'DO-27' => __( 'Valverde', 'lifterlms' ),\n\t\t'DO-42' => __( 'Yuma', 'lifterlms' ),\n\t),\n\t'DZ' => array( // Algerian states.\n\t\t'DZ-01' => __( 'Adrar', 'lifterlms' ),\n\t\t'DZ-02' => __( 'Chlef', 'lifterlms' ),\n\t\t'DZ-03' => __( 'Laghouat', 'lifterlms' ),\n\t\t'DZ-04' => __( 'Oum El Bouaghi', 'lifterlms' ),\n\t\t'DZ-05' => __( 'Batna', 'lifterlms' ),\n\t\t'DZ-06' => __( 'Béjaïa', 'lifterlms' ),\n\t\t'DZ-07' => __( 'Biskra', 'lifterlms' ),\n\t\t'DZ-08' => __( 'Béchar', 'lifterlms' ),\n\t\t'DZ-09' => __( 'Blida', 'lifterlms' ),\n\t\t'DZ-10' => __( 'Bouira', 'lifterlms' ),\n\t\t'DZ-11' => __( 'Tamanghasset', 'lifterlms' ),\n\t\t'DZ-12' => __( 'Tébessa', 'lifterlms' ),\n\t\t'DZ-13' => __( 'Tlemcen', 'lifterlms' ),\n\t\t'DZ-14' => __( 'Tiaret', 'lifterlms' ),\n\t\t'DZ-15' => __( 'Tizi Ouzou', 'lifterlms' ),\n\t\t'DZ-16' => __( 'Algiers', 'lifterlms' ),\n\t\t'DZ-17' => __( 'Djelfa', 'lifterlms' ),\n\t\t'DZ-18' => __( 'Jijel', 'lifterlms' ),\n\t\t'DZ-19' => __( 'Sétif', 'lifterlms' ),\n\t\t'DZ-20' => __( 'Saïda', 'lifterlms' ),\n\t\t'DZ-21' => __( 'Skikda', 'lifterlms' ),\n\t\t'DZ-22' => __( 'Sidi Bel Abbès', 'lifterlms' ),\n\t\t'DZ-23' => __( 'Annaba', 'lifterlms' ),\n\t\t'DZ-24' => __( 'Guelma', 'lifterlms' ),\n\t\t'DZ-25' => __( 'Constantine', 'lifterlms' ),\n\t\t'DZ-26' => __( 'Médéa', 'lifterlms' ),\n\t\t'DZ-27' => __( 'Mostaganem', 'lifterlms' ),\n\t\t'DZ-28' => __( 'M’Sila', 'lifterlms' ),\n\t\t'DZ-29' => __( 'Mascara', 'lifterlms' ),\n\t\t'DZ-30' => __( 'Ouargla', 'lifterlms' ),\n\t\t'DZ-31' => __( 'Oran', 'lifterlms' ),\n\t\t'DZ-32' => __( 'El Bayadh', 'lifterlms' ),\n\t\t'DZ-33' => __( 'Illizi', 'lifterlms' ),\n\t\t'DZ-34' => __( 'Bordj Bou Arréridj', 'lifterlms' ),\n\t\t'DZ-35' => __( 'Boumerdès', 'lifterlms' ),\n\t\t'DZ-36' => __( 'El Tarf', 'lifterlms' ),\n\t\t'DZ-37' => __( 'Tindouf', 'lifterlms' ),\n\t\t'DZ-38' => __( 'Tissemsilt', 'lifterlms' ),\n\t\t'DZ-39' => __( 'El Oued', 'lifterlms' ),\n\t\t'DZ-40' => __( 'Khenchela', 'lifterlms' ),\n\t\t'DZ-41' => __( 'Souk Ahras', 'lifterlms' ),\n\t\t'DZ-42' => __( 'Tipasa', 'lifterlms' ),\n\t\t'DZ-43' => __( 'Mila', 'lifterlms' ),\n\t\t'DZ-44' => __( 'Aïn Defla', 'lifterlms' ),\n\t\t'DZ-45' => __( 'Naama', 'lifterlms' ),\n\t\t'DZ-46' => __( 'Aïn Témouchent', 'lifterlms' ),\n\t\t'DZ-47' => __( 'Ghardaïa', 'lifterlms' ),\n\t\t'DZ-48' => __( 'Relizane', 'lifterlms' ),\n\t),\n\t'EE' => array(),\n\t'EC' => array( // Ecuadorian states.\n\t\t'EC-A' => __( 'Azuay', 'lifterlms' ),\n\t\t'EC-B' => __( 'Bolívar', 'lifterlms' ),\n\t\t'EC-F' => __( 'Cañar', 'lifterlms' ),\n\t\t'EC-C' => __( 'Carchi', 'lifterlms' ),\n\t\t'EC-H' => __( 'Chimborazo', 'lifterlms' ),\n\t\t'EC-X' => __( 'Cotopaxi', 'lifterlms' ),\n\t\t'EC-O' => __( 'El Oro', 'lifterlms' ),\n\t\t'EC-E' => __( 'Esmeraldas', 'lifterlms' ),\n\t\t'EC-W' => __( 'Galápagos', 'lifterlms' ),\n\t\t'EC-G' => __( 'Guayas', 'lifterlms' ),\n\t\t'EC-I' => __( 'Imbabura', 'lifterlms' ),\n\t\t'EC-L' => __( 'Loja', 'lifterlms' ),\n\t\t'EC-R' => __( 'Los Ríos', 'lifterlms' ),\n\t\t'EC-M' => __( 'Manabí', 'lifterlms' ),\n\t\t'EC-S' => __( 'Morona-Santiago', 'lifterlms' ),\n\t\t'EC-N' => __( 'Napo', 'lifterlms' ),\n\t\t'EC-D' => __( 'Orellana', 'lifterlms' ),\n\t\t'EC-Y' => __( 'Pastaza', 'lifterlms' ),\n\t\t'EC-P' => __( 'Pichincha', 'lifterlms' ),\n\t\t'EC-SE' => __( 'Santa Elena', 'lifterlms' ),\n\t\t'EC-SD' => __( 'Santo Domingo de los Tsáchilas', 'lifterlms' ),\n\t\t'EC-U' => __( 'Sucumbíos', 'lifterlms' ),\n\t\t'EC-T' => __( 'Tungurahua', 'lifterlms' ),\n\t\t'EC-Z' => __( 'Zamora-Chinchipe', 'lifterlms' ),\n\t),\n\t'EG' => array( // Egyptian states.\n\t\t'EGALX' => __( 'Alexandria', 'lifterlms' ),\n\t\t'EGASN' => __( 'Aswan', 'lifterlms' ),\n\t\t'EGAST' => __( 'Asyut', 'lifterlms' ),\n\t\t'EGBA'  => __( 'Red Sea', 'lifterlms' ),\n\t\t'EGBH'  => __( 'Beheira', 'lifterlms' ),\n\t\t'EGBNS' => __( 'Beni Suef', 'lifterlms' ),\n\t\t'EGC'   => __( 'Cairo', 'lifterlms' ),\n\t\t'EGDK'  => __( 'Dakahlia', 'lifterlms' ),\n\t\t'EGDT'  => __( 'Damietta', 'lifterlms' ),\n\t\t'EGFYM' => __( 'Faiyum', 'lifterlms' ),\n\t\t'EGGH'  => __( 'Gharbia', 'lifterlms' ),\n\t\t'EGGZ'  => __( 'Giza', 'lifterlms' ),\n\t\t'EGIS'  => __( 'Ismailia', 'lifterlms' ),\n\t\t'EGJS'  => __( 'South Sinai', 'lifterlms' ),\n\t\t'EGKB'  => __( 'Qalyubia', 'lifterlms' ),\n\t\t'EGKFS' => __( 'Kafr el-Sheikh', 'lifterlms' ),\n\t\t'EGKN'  => __( 'Qena', 'lifterlms' ),\n\t\t'EGLX'  => __( 'Luxor', 'lifterlms' ),\n\t\t'EGMN'  => __( 'Minya', 'lifterlms' ),\n\t\t'EGMNF' => __( 'Monufia', 'lifterlms' ),\n\t\t'EGMT'  => __( 'Matrouh', 'lifterlms' ),\n\t\t'EGPTS' => __( 'Port Said', 'lifterlms' ),\n\t\t'EGSHG' => __( 'Sohag', 'lifterlms' ),\n\t\t'EGSHR' => __( 'Al Sharqia', 'lifterlms' ),\n\t\t'EGSIN' => __( 'North Sinai', 'lifterlms' ),\n\t\t'EGSUZ' => __( 'Suez', 'lifterlms' ),\n\t\t'EGWAD' => __( 'New Valley', 'lifterlms' ),\n\t),\n\t'ES' => array( // Spanish states.\n\t\t'C'  => __( 'A Coruña', 'lifterlms' ),\n\t\t'VI' => __( 'Araba/Álava', 'lifterlms' ),\n\t\t'AB' => __( 'Albacete', 'lifterlms' ),\n\t\t'A'  => __( 'Alicante', 'lifterlms' ),\n\t\t'AL' => __( 'Almería', 'lifterlms' ),\n\t\t'O'  => __( 'Asturias', 'lifterlms' ),\n\t\t'AV' => __( 'Ávila', 'lifterlms' ),\n\t\t'BA' => __( 'Badajoz', 'lifterlms' ),\n\t\t'PM' => __( 'Baleares', 'lifterlms' ),\n\t\t'B'  => __( 'Barcelona', 'lifterlms' ),\n\t\t'BU' => __( 'Burgos', 'lifterlms' ),\n\t\t'CC' => __( 'Cáceres', 'lifterlms' ),\n\t\t'CA' => __( 'Cádiz', 'lifterlms' ),\n\t\t'S'  => __( 'Cantabria', 'lifterlms' ),\n\t\t'CS' => __( 'Castellón', 'lifterlms' ),\n\t\t'CE' => __( 'Ceuta', 'lifterlms' ),\n\t\t'CR' => __( 'Ciudad Real', 'lifterlms' ),\n\t\t'CO' => __( 'Córdoba', 'lifterlms' ),\n\t\t'CU' => __( 'Cuenca', 'lifterlms' ),\n\t\t'GI' => __( 'Girona', 'lifterlms' ),\n\t\t'GR' => __( 'Granada', 'lifterlms' ),\n\t\t'GU' => __( 'Guadalajara', 'lifterlms' ),\n\t\t'SS' => __( 'Gipuzkoa', 'lifterlms' ),\n\t\t'H'  => __( 'Huelva', 'lifterlms' ),\n\t\t'HU' => __( 'Huesca', 'lifterlms' ),\n\t\t'J'  => __( 'Jaén', 'lifterlms' ),\n\t\t'LO' => __( 'La Rioja', 'lifterlms' ),\n\t\t'GC' => __( 'Las Palmas', 'lifterlms' ),\n\t\t'LE' => __( 'León', 'lifterlms' ),\n\t\t'L'  => __( 'Lleida', 'lifterlms' ),\n\t\t'LU' => __( 'Lugo', 'lifterlms' ),\n\t\t'M'  => __( 'Madrid', 'lifterlms' ),\n\t\t'MA' => __( 'Málaga', 'lifterlms' ),\n\t\t'ML' => __( 'Melilla', 'lifterlms' ),\n\t\t'MU' => __( 'Murcia', 'lifterlms' ),\n\t\t'NA' => __( 'Navarra', 'lifterlms' ),\n\t\t'OR' => __( 'Ourense', 'lifterlms' ),\n\t\t'P'  => __( 'Palencia', 'lifterlms' ),\n\t\t'PO' => __( 'Pontevedra', 'lifterlms' ),\n\t\t'SA' => __( 'Salamanca', 'lifterlms' ),\n\t\t'TF' => __( 'Santa Cruz de Tenerife', 'lifterlms' ),\n\t\t'SG' => __( 'Segovia', 'lifterlms' ),\n\t\t'SE' => __( 'Sevilla', 'lifterlms' ),\n\t\t'SO' => __( 'Soria', 'lifterlms' ),\n\t\t'T'  => __( 'Tarragona', 'lifterlms' ),\n\t\t'TE' => __( 'Teruel', 'lifterlms' ),\n\t\t'TO' => __( 'Toledo', 'lifterlms' ),\n\t\t'V'  => __( 'Valencia', 'lifterlms' ),\n\t\t'VA' => __( 'Valladolid', 'lifterlms' ),\n\t\t'BI' => __( 'Biscay', 'lifterlms' ),\n\t\t'ZA' => __( 'Zamora', 'lifterlms' ),\n\t\t'Z'  => __( 'Zaragoza', 'lifterlms' ),\n\t),\n\t'ET' => array(),\n\t'FI' => array(),\n\t'FR' => array(),\n\t'GF' => array(),\n\t'GH' => array( // Ghanaian regions.\n\t\t'AF' => __( 'Ahafo', 'lifterlms' ),\n\t\t'AH' => __( 'Ashanti', 'lifterlms' ),\n\t\t'BA' => __( 'Brong-Ahafo', 'lifterlms' ),\n\t\t'BO' => __( 'Bono', 'lifterlms' ),\n\t\t'BE' => __( 'Bono East', 'lifterlms' ),\n\t\t'CP' => __( 'Central', 'lifterlms' ),\n\t\t'EP' => __( 'Eastern', 'lifterlms' ),\n\t\t'AA' => __( 'Greater Accra', 'lifterlms' ),\n\t\t'NE' => __( 'North East', 'lifterlms' ),\n\t\t'NP' => __( 'Northern', 'lifterlms' ),\n\t\t'OT' => __( 'Oti', 'lifterlms' ),\n\t\t'SV' => __( 'Savannah', 'lifterlms' ),\n\t\t'UE' => __( 'Upper East', 'lifterlms' ),\n\t\t'UW' => __( 'Upper West', 'lifterlms' ),\n\t\t'TV' => __( 'Volta', 'lifterlms' ),\n\t\t'WP' => __( 'Western', 'lifterlms' ),\n\t\t'WN' => __( 'Western North', 'lifterlms' ),\n\t),\n\t'GP' => array(),\n\t'GR' => array( // Greek regions.\n\t\t'I' => __( 'Attica', 'lifterlms' ),\n\t\t'A' => __( 'East Macedonia and Thrace', 'lifterlms' ),\n\t\t'B' => __( 'Central Macedonia', 'lifterlms' ),\n\t\t'C' => __( 'West Macedonia', 'lifterlms' ),\n\t\t'D' => __( 'Epirus', 'lifterlms' ),\n\t\t'E' => __( 'Thessaly', 'lifterlms' ),\n\t\t'F' => __( 'Ionian Islands', 'lifterlms' ),\n\t\t'G' => __( 'West Greece', 'lifterlms' ),\n\t\t'H' => __( 'Central Greece', 'lifterlms' ),\n\t\t'J' => __( 'Peloponnese', 'lifterlms' ),\n\t\t'K' => __( 'North Aegean', 'lifterlms' ),\n\t\t'L' => __( 'South Aegean', 'lifterlms' ),\n\t\t'M' => __( 'Crete', 'lifterlms' ),\n\t),\n\t'GT' => array( // Guatemalan states.\n\t\t'GT-AV' => __( 'Alta Verapaz', 'lifterlms' ),\n\t\t'GT-BV' => __( 'Baja Verapaz', 'lifterlms' ),\n\t\t'GT-CM' => __( 'Chimaltenango', 'lifterlms' ),\n\t\t'GT-CQ' => __( 'Chiquimula', 'lifterlms' ),\n\t\t'GT-PR' => __( 'El Progreso', 'lifterlms' ),\n\t\t'GT-ES' => __( 'Escuintla', 'lifterlms' ),\n\t\t'GT-GU' => __( 'Guatemala', 'lifterlms' ),\n\t\t'GT-HU' => __( 'Huehuetenango', 'lifterlms' ),\n\t\t'GT-IZ' => __( 'Izabal', 'lifterlms' ),\n\t\t'GT-JA' => __( 'Jalapa', 'lifterlms' ),\n\t\t'GT-JU' => __( 'Jutiapa', 'lifterlms' ),\n\t\t'GT-PE' => __( 'Petén', 'lifterlms' ),\n\t\t'GT-QZ' => __( 'Quetzaltenango', 'lifterlms' ),\n\t\t'GT-QC' => __( 'Quiché', 'lifterlms' ),\n\t\t'GT-RE' => __( 'Retalhuleu', 'lifterlms' ),\n\t\t'GT-SA' => __( 'Sacatepéquez', 'lifterlms' ),\n\t\t'GT-SM' => __( 'San Marcos', 'lifterlms' ),\n\t\t'GT-SR' => __( 'Santa Rosa', 'lifterlms' ),\n\t\t'GT-SO' => __( 'Sololá', 'lifterlms' ),\n\t\t'GT-SU' => __( 'Suchitepéquez', 'lifterlms' ),\n\t\t'GT-TO' => __( 'Totonicapán', 'lifterlms' ),\n\t\t'GT-ZA' => __( 'Zacapa', 'lifterlms' ),\n\t),\n\t'HK' => array( // Hong Kong states.\n\t\t'HONG KONG'       => __( 'Hong Kong Island', 'lifterlms' ),\n\t\t'KOWLOON'         => __( 'Kowloon', 'lifterlms' ),\n\t\t'NEW TERRITORIES' => __( 'New Territories', 'lifterlms' ),\n\t),\n\t'HN' => array( // Honduran states.\n\t\t'HN-AT' => __( 'Atlántida', 'lifterlms' ),\n\t\t'HN-IB' => __( 'Bay Islands', 'lifterlms' ),\n\t\t'HN-CH' => __( 'Choluteca', 'lifterlms' ),\n\t\t'HN-CL' => __( 'Colón', 'lifterlms' ),\n\t\t'HN-CM' => __( 'Comayagua', 'lifterlms' ),\n\t\t'HN-CP' => __( 'Copán', 'lifterlms' ),\n\t\t'HN-CR' => __( 'Cortés', 'lifterlms' ),\n\t\t'HN-EP' => __( 'El Paraíso', 'lifterlms' ),\n\t\t'HN-FM' => __( 'Francisco Morazán', 'lifterlms' ),\n\t\t'HN-GD' => __( 'Gracias a Dios', 'lifterlms' ),\n\t\t'HN-IN' => __( 'Intibucá', 'lifterlms' ),\n\t\t'HN-LE' => __( 'Lempira', 'lifterlms' ),\n\t\t'HN-LP' => __( 'La Paz', 'lifterlms' ),\n\t\t'HN-OC' => __( 'Ocotepeque', 'lifterlms' ),\n\t\t'HN-OL' => __( 'Olancho', 'lifterlms' ),\n\t\t'HN-SB' => __( 'Santa Bárbara', 'lifterlms' ),\n\t\t'HN-VA' => __( 'Valle', 'lifterlms' ),\n\t\t'HN-YO' => __( 'Yoro', 'lifterlms' ),\n\t),\n\t'HR' => array( // Croatian counties.\n\t\t'HR-01' => __( 'Zagreb County', 'lifterlms' ),\n\t\t'HR-02' => __( 'Krapina-Zagorje County', 'lifterlms' ),\n\t\t'HR-03' => __( 'Sisak-Moslavina County', 'lifterlms' ),\n\t\t'HR-04' => __( 'Karlovac County', 'lifterlms' ),\n\t\t'HR-05' => __( 'Varaždin County', 'lifterlms' ),\n\t\t'HR-06' => __( 'Koprivnica-Križevci County', 'lifterlms' ),\n\t\t'HR-07' => __( 'Bjelovar-Bilogora County', 'lifterlms' ),\n\t\t'HR-08' => __( 'Primorje-Gorski Kotar County', 'lifterlms' ),\n\t\t'HR-09' => __( 'Lika-Senj County', 'lifterlms' ),\n\t\t'HR-10' => __( 'Virovitica-Podravina County', 'lifterlms' ),\n\t\t'HR-11' => __( 'Požega-Slavonia County', 'lifterlms' ),\n\t\t'HR-12' => __( 'Brod-Posavina County', 'lifterlms' ),\n\t\t'HR-13' => __( 'Zadar County', 'lifterlms' ),\n\t\t'HR-14' => __( 'Osijek-Baranja County', 'lifterlms' ),\n\t\t'HR-15' => __( 'Šibenik-Knin County', 'lifterlms' ),\n\t\t'HR-16' => __( 'Vukovar-Srijem County', 'lifterlms' ),\n\t\t'HR-17' => __( 'Split-Dalmatia County', 'lifterlms' ),\n\t\t'HR-18' => __( 'Istria County', 'lifterlms' ),\n\t\t'HR-19' => __( 'Dubrovnik-Neretva County', 'lifterlms' ),\n\t\t'HR-20' => __( 'Međimurje County', 'lifterlms' ),\n\t\t'HR-21' => __( 'Zagreb City', 'lifterlms' ),\n\t),\n\t'HU' => array( // Hungarian states.\n\t\t'BK' => __( 'Bács-Kiskun', 'lifterlms' ),\n\t\t'BE' => __( 'Békés', 'lifterlms' ),\n\t\t'BA' => __( 'Baranya', 'lifterlms' ),\n\t\t'BZ' => __( 'Borsod-Abaúj-Zemplén', 'lifterlms' ),\n\t\t'BU' => __( 'Budapest', 'lifterlms' ),\n\t\t'CS' => __( 'Csongrád-Csanád', 'lifterlms' ),\n\t\t'FE' => __( 'Fejér', 'lifterlms' ),\n\t\t'GS' => __( 'Győr-Moson-Sopron', 'lifterlms' ),\n\t\t'HB' => __( 'Hajdú-Bihar', 'lifterlms' ),\n\t\t'HE' => __( 'Heves', 'lifterlms' ),\n\t\t'JN' => __( 'Jász-Nagykun-Szolnok', 'lifterlms' ),\n\t\t'KE' => __( 'Komárom-Esztergom', 'lifterlms' ),\n\t\t'NO' => __( 'Nógrád', 'lifterlms' ),\n\t\t'PE' => __( 'Pest', 'lifterlms' ),\n\t\t'SO' => __( 'Somogy', 'lifterlms' ),\n\t\t'SZ' => __( 'Szabolcs-Szatmár-Bereg', 'lifterlms' ),\n\t\t'TO' => __( 'Tolna', 'lifterlms' ),\n\t\t'VA' => __( 'Vas', 'lifterlms' ),\n\t\t'VE' => __( 'Veszprém', 'lifterlms' ),\n\t\t'ZA' => __( 'Zala', 'lifterlms' ),\n\t),\n\t'ID' => array( // Indonesian provinces.\n\t\t'AC' => __( 'Daerah Istimewa Aceh', 'lifterlms' ),\n\t\t'SU' => __( 'Sumatera Utara', 'lifterlms' ),\n\t\t'SB' => __( 'Sumatera Barat', 'lifterlms' ),\n\t\t'RI' => __( 'Riau', 'lifterlms' ),\n\t\t'KR' => __( 'Kepulauan Riau', 'lifterlms' ),\n\t\t'JA' => __( 'Jambi', 'lifterlms' ),\n\t\t'SS' => __( 'Sumatera Selatan', 'lifterlms' ),\n\t\t'BB' => __( 'Bangka Belitung', 'lifterlms' ),\n\t\t'BE' => __( 'Bengkulu', 'lifterlms' ),\n\t\t'LA' => __( 'Lampung', 'lifterlms' ),\n\t\t'JK' => __( 'DKI Jakarta', 'lifterlms' ),\n\t\t'JB' => __( 'Jawa Barat', 'lifterlms' ),\n\t\t'BT' => __( 'Banten', 'lifterlms' ),\n\t\t'JT' => __( 'Jawa Tengah', 'lifterlms' ),\n\t\t'JI' => __( 'Jawa Timur', 'lifterlms' ),\n\t\t'YO' => __( 'Daerah Istimewa Yogyakarta', 'lifterlms' ),\n\t\t'BA' => __( 'Bali', 'lifterlms' ),\n\t\t'NB' => __( 'Nusa Tenggara Barat', 'lifterlms' ),\n\t\t'NT' => __( 'Nusa Tenggara Timur', 'lifterlms' ),\n\t\t'KB' => __( 'Kalimantan Barat', 'lifterlms' ),\n\t\t'KT' => __( 'Kalimantan Tengah', 'lifterlms' ),\n\t\t'KI' => __( 'Kalimantan Timur', 'lifterlms' ),\n\t\t'KS' => __( 'Kalimantan Selatan', 'lifterlms' ),\n\t\t'KU' => __( 'Kalimantan Utara', 'lifterlms' ),\n\t\t'SA' => __( 'Sulawesi Utara', 'lifterlms' ),\n\t\t'ST' => __( 'Sulawesi Tengah', 'lifterlms' ),\n\t\t'SG' => __( 'Sulawesi Tenggara', 'lifterlms' ),\n\t\t'SR' => __( 'Sulawesi Barat', 'lifterlms' ),\n\t\t'SN' => __( 'Sulawesi Selatan', 'lifterlms' ),\n\t\t'GO' => __( 'Gorontalo', 'lifterlms' ),\n\t\t'MA' => __( 'Maluku', 'lifterlms' ),\n\t\t'MU' => __( 'Maluku Utara', 'lifterlms' ),\n\t\t'PA' => __( 'Papua', 'lifterlms' ),\n\t\t'PB' => __( 'Papua Barat', 'lifterlms' ),\n\t),\n\t'IE' => array( // Irish states.\n\t\t'CW' => __( 'Carlow', 'lifterlms' ),\n\t\t'CN' => __( 'Cavan', 'lifterlms' ),\n\t\t'CE' => __( 'Clare', 'lifterlms' ),\n\t\t'CO' => __( 'Cork', 'lifterlms' ),\n\t\t'DL' => __( 'Donegal', 'lifterlms' ),\n\t\t'D'  => __( 'Dublin', 'lifterlms' ),\n\t\t'G'  => __( 'Galway', 'lifterlms' ),\n\t\t'KY' => __( 'Kerry', 'lifterlms' ),\n\t\t'KE' => __( 'Kildare', 'lifterlms' ),\n\t\t'KK' => __( 'Kilkenny', 'lifterlms' ),\n\t\t'LS' => __( 'Laois', 'lifterlms' ),\n\t\t'LM' => __( 'Leitrim', 'lifterlms' ),\n\t\t'LK' => __( 'Limerick', 'lifterlms' ),\n\t\t'LD' => __( 'Longford', 'lifterlms' ),\n\t\t'LH' => __( 'Louth', 'lifterlms' ),\n\t\t'MO' => __( 'Mayo', 'lifterlms' ),\n\t\t'MH' => __( 'Meath', 'lifterlms' ),\n\t\t'MN' => __( 'Monaghan', 'lifterlms' ),\n\t\t'OY' => __( 'Offaly', 'lifterlms' ),\n\t\t'RN' => __( 'Roscommon', 'lifterlms' ),\n\t\t'SO' => __( 'Sligo', 'lifterlms' ),\n\t\t'TA' => __( 'Tipperary', 'lifterlms' ),\n\t\t'WD' => __( 'Waterford', 'lifterlms' ),\n\t\t'WH' => __( 'Westmeath', 'lifterlms' ),\n\t\t'WX' => __( 'Wexford', 'lifterlms' ),\n\t\t'WW' => __( 'Wicklow', 'lifterlms' ),\n\t),\n\t'IN' => array( // Indian states.\n\t\t'AP' => __( 'Andhra Pradesh', 'lifterlms' ),\n\t\t'AR' => __( 'Arunachal Pradesh', 'lifterlms' ),\n\t\t'AS' => __( 'Assam', 'lifterlms' ),\n\t\t'BR' => __( 'Bihar', 'lifterlms' ),\n\t\t'CT' => __( 'Chhattisgarh', 'lifterlms' ),\n\t\t'GA' => __( 'Goa', 'lifterlms' ),\n\t\t'GJ' => __( 'Gujarat', 'lifterlms' ),\n\t\t'HR' => __( 'Haryana', 'lifterlms' ),\n\t\t'HP' => __( 'Himachal Pradesh', 'lifterlms' ),\n\t\t'JK' => __( 'Jammu and Kashmir', 'lifterlms' ),\n\t\t'JH' => __( 'Jharkhand', 'lifterlms' ),\n\t\t'KA' => __( 'Karnataka', 'lifterlms' ),\n\t\t'KL' => __( 'Kerala', 'lifterlms' ),\n\t\t'LA' => __( 'Ladakh', 'lifterlms' ),\n\t\t'MP' => __( 'Madhya Pradesh', 'lifterlms' ),\n\t\t'MH' => __( 'Maharashtra', 'lifterlms' ),\n\t\t'MN' => __( 'Manipur', 'lifterlms' ),\n\t\t'ML' => __( 'Meghalaya', 'lifterlms' ),\n\t\t'MZ' => __( 'Mizoram', 'lifterlms' ),\n\t\t'NL' => __( 'Nagaland', 'lifterlms' ),\n\t\t'OD' => __( 'Odisha', 'lifterlms' ),\n\t\t'PB' => __( 'Punjab', 'lifterlms' ),\n\t\t'RJ' => __( 'Rajasthan', 'lifterlms' ),\n\t\t'SK' => __( 'Sikkim', 'lifterlms' ),\n\t\t'TN' => __( 'Tamil Nadu', 'lifterlms' ),\n\t\t'TS' => __( 'Telangana', 'lifterlms' ),\n\t\t'TR' => __( 'Tripura', 'lifterlms' ),\n\t\t'UK' => __( 'Uttarakhand', 'lifterlms' ),\n\t\t'UP' => __( 'Uttar Pradesh', 'lifterlms' ),\n\t\t'WB' => __( 'West Bengal', 'lifterlms' ),\n\t\t'AN' => __( 'Andaman and Nicobar Islands', 'lifterlms' ),\n\t\t'CH' => __( 'Chandigarh', 'lifterlms' ),\n\t\t'DN' => __( 'Dadra and Nagar Haveli', 'lifterlms' ),\n\t\t'DD' => __( 'Daman and Diu', 'lifterlms' ),\n\t\t'DL' => __( 'Delhi', 'lifterlms' ),\n\t\t'LD' => __( 'Lakshadeep', 'lifterlms' ),\n\t\t'PY' => __( 'Pondicherry (Puducherry)', 'lifterlms' ),\n\t),\n\t'IR' => array( // Iranian states.\n\t\t'KHZ' => __( 'Khuzestan (خوزستان)', 'lifterlms' ),\n\t\t'THR' => __( 'Tehran (تهران)', 'lifterlms' ),\n\t\t'ILM' => __( 'Ilaam (ایلام)', 'lifterlms' ),\n\t\t'BHR' => __( 'Bushehr (بوشهر)', 'lifterlms' ),\n\t\t'ADL' => __( 'Ardabil (اردبیل)', 'lifterlms' ),\n\t\t'ESF' => __( 'Isfahan (اصفهان)', 'lifterlms' ),\n\t\t'YZD' => __( 'Yazd (یزد)', 'lifterlms' ),\n\t\t'KRH' => __( 'Kermanshah (کرمانشاه)', 'lifterlms' ),\n\t\t'KRN' => __( 'Kerman (کرمان)', 'lifterlms' ),\n\t\t'HDN' => __( 'Hamadan (همدان)', 'lifterlms' ),\n\t\t'GZN' => __( 'Ghazvin (قزوین)', 'lifterlms' ),\n\t\t'ZJN' => __( 'Zanjan (زنجان)', 'lifterlms' ),\n\t\t'LRS' => __( 'Luristan (لرستان)', 'lifterlms' ),\n\t\t'ABZ' => __( 'Alborz (البرز)', 'lifterlms' ),\n\t\t'EAZ' => __( 'East Azarbaijan (آذربایجان شرقی)', 'lifterlms' ),\n\t\t'WAZ' => __( 'West Azarbaijan (آذربایجان غربی)', 'lifterlms' ),\n\t\t'CHB' => __( 'Chaharmahal and Bakhtiari (چهارمحال و بختیاری)', 'lifterlms' ),\n\t\t'SKH' => __( 'South Khorasan (خراسان جنوبی)', 'lifterlms' ),\n\t\t'RKH' => __( 'Razavi Khorasan (خراسان رضوی)', 'lifterlms' ),\n\t\t'NKH' => __( 'North Khorasan (خراسان شمالی)', 'lifterlms' ),\n\t\t'SMN' => __( 'Semnan (سمنان)', 'lifterlms' ),\n\t\t'FRS' => __( 'Fars (فارس)', 'lifterlms' ),\n\t\t'QHM' => __( 'Qom (قم)', 'lifterlms' ),\n\t\t'KRD' => __( 'Kurdistan / کردستان)', 'lifterlms' ),\n\t\t'KBD' => __( 'Kohgiluyeh and BoyerAhmad (کهگیلوییه و بویراحمد)', 'lifterlms' ),\n\t\t'GLS' => __( 'Golestan (گلستان)', 'lifterlms' ),\n\t\t'GIL' => __( 'Gilan (گیلان)', 'lifterlms' ),\n\t\t'MZN' => __( 'Mazandaran (مازندران)', 'lifterlms' ),\n\t\t'MKZ' => __( 'Markazi (مرکزی)', 'lifterlms' ),\n\t\t'HRZ' => __( 'Hormozgan (هرمزگان)', 'lifterlms' ),\n\t\t'SBN' => __( 'Sistan and Baluchestan (سیستان و بلوچستان)', 'lifterlms' ),\n\t),\n\t'IS' => array(),\n\t'IT' => array( // Italian provinces.\n\t\t'AG' => __( 'Agrigento', 'lifterlms' ),\n\t\t'AL' => __( 'Alessandria', 'lifterlms' ),\n\t\t'AN' => __( 'Ancona', 'lifterlms' ),\n\t\t'AO' => __( 'Aosta', 'lifterlms' ),\n\t\t'AR' => __( 'Arezzo', 'lifterlms' ),\n\t\t'AP' => __( 'Ascoli Piceno', 'lifterlms' ),\n\t\t'AT' => __( 'Asti', 'lifterlms' ),\n\t\t'AV' => __( 'Avellino', 'lifterlms' ),\n\t\t'BA' => __( 'Bari', 'lifterlms' ),\n\t\t'BT' => __( 'Barletta-Andria-Trani', 'lifterlms' ),\n\t\t'BL' => __( 'Belluno', 'lifterlms' ),\n\t\t'BN' => __( 'Benevento', 'lifterlms' ),\n\t\t'BG' => __( 'Bergamo', 'lifterlms' ),\n\t\t'BI' => __( 'Biella', 'lifterlms' ),\n\t\t'BO' => __( 'Bologna', 'lifterlms' ),\n\t\t'BZ' => __( 'Bolzano', 'lifterlms' ),\n\t\t'BS' => __( 'Brescia', 'lifterlms' ),\n\t\t'BR' => __( 'Brindisi', 'lifterlms' ),\n\t\t'CA' => __( 'Cagliari', 'lifterlms' ),\n\t\t'CL' => __( 'Caltanissetta', 'lifterlms' ),\n\t\t'CB' => __( 'Campobasso', 'lifterlms' ),\n\t\t'CE' => __( 'Caserta', 'lifterlms' ),\n\t\t'CT' => __( 'Catania', 'lifterlms' ),\n\t\t'CZ' => __( 'Catanzaro', 'lifterlms' ),\n\t\t'CH' => __( 'Chieti', 'lifterlms' ),\n\t\t'CO' => __( 'Como', 'lifterlms' ),\n\t\t'CS' => __( 'Cosenza', 'lifterlms' ),\n\t\t'CR' => __( 'Cremona', 'lifterlms' ),\n\t\t'KR' => __( 'Crotone', 'lifterlms' ),\n\t\t'CN' => __( 'Cuneo', 'lifterlms' ),\n\t\t'EN' => __( 'Enna', 'lifterlms' ),\n\t\t'FM' => __( 'Fermo', 'lifterlms' ),\n\t\t'FE' => __( 'Ferrara', 'lifterlms' ),\n\t\t'FI' => __( 'Firenze', 'lifterlms' ),\n\t\t'FG' => __( 'Foggia', 'lifterlms' ),\n\t\t'FC' => __( 'Forlì-Cesena', 'lifterlms' ),\n\t\t'FR' => __( 'Frosinone', 'lifterlms' ),\n\t\t'GE' => __( 'Genova', 'lifterlms' ),\n\t\t'GO' => __( 'Gorizia', 'lifterlms' ),\n\t\t'GR' => __( 'Grosseto', 'lifterlms' ),\n\t\t'IM' => __( 'Imperia', 'lifterlms' ),\n\t\t'IS' => __( 'Isernia', 'lifterlms' ),\n\t\t'SP' => __( 'La Spezia', 'lifterlms' ),\n\t\t'AQ' => __( \"L'Aquila\", 'lifterlms' ),\n\t\t'LT' => __( 'Latina', 'lifterlms' ),\n\t\t'LE' => __( 'Lecce', 'lifterlms' ),\n\t\t'LC' => __( 'Lecco', 'lifterlms' ),\n\t\t'LI' => __( 'Livorno', 'lifterlms' ),\n\t\t'LO' => __( 'Lodi', 'lifterlms' ),\n\t\t'LU' => __( 'Lucca', 'lifterlms' ),\n\t\t'MC' => __( 'Macerata', 'lifterlms' ),\n\t\t'MN' => __( 'Mantova', 'lifterlms' ),\n\t\t'MS' => __( 'Massa-Carrara', 'lifterlms' ),\n\t\t'MT' => __( 'Matera', 'lifterlms' ),\n\t\t'ME' => __( 'Messina', 'lifterlms' ),\n\t\t'MI' => __( 'Milano', 'lifterlms' ),\n\t\t'MO' => __( 'Modena', 'lifterlms' ),\n\t\t'MB' => __( 'Monza e della Brianza', 'lifterlms' ),\n\t\t'NA' => __( 'Napoli', 'lifterlms' ),\n\t\t'NO' => __( 'Novara', 'lifterlms' ),\n\t\t'NU' => __( 'Nuoro', 'lifterlms' ),\n\t\t'OR' => __( 'Oristano', 'lifterlms' ),\n\t\t'PD' => __( 'Padova', 'lifterlms' ),\n\t\t'PA' => __( 'Palermo', 'lifterlms' ),\n\t\t'PR' => __( 'Parma', 'lifterlms' ),\n\t\t'PV' => __( 'Pavia', 'lifterlms' ),\n\t\t'PG' => __( 'Perugia', 'lifterlms' ),\n\t\t'PU' => __( 'Pesaro e Urbino', 'lifterlms' ),\n\t\t'PE' => __( 'Pescara', 'lifterlms' ),\n\t\t'PC' => __( 'Piacenza', 'lifterlms' ),\n\t\t'PI' => __( 'Pisa', 'lifterlms' ),\n\t\t'PT' => __( 'Pistoia', 'lifterlms' ),\n\t\t'PN' => __( 'Pordenone', 'lifterlms' ),\n\t\t'PZ' => __( 'Potenza', 'lifterlms' ),\n\t\t'PO' => __( 'Prato', 'lifterlms' ),\n\t\t'RG' => __( 'Ragusa', 'lifterlms' ),\n\t\t'RA' => __( 'Ravenna', 'lifterlms' ),\n\t\t'RC' => __( 'Reggio Calabria', 'lifterlms' ),\n\t\t'RE' => __( 'Reggio Emilia', 'lifterlms' ),\n\t\t'RI' => __( 'Rieti', 'lifterlms' ),\n\t\t'RN' => __( 'Rimini', 'lifterlms' ),\n\t\t'RM' => __( 'Roma', 'lifterlms' ),\n\t\t'RO' => __( 'Rovigo', 'lifterlms' ),\n\t\t'SA' => __( 'Salerno', 'lifterlms' ),\n\t\t'SS' => __( 'Sassari', 'lifterlms' ),\n\t\t'SV' => __( 'Savona', 'lifterlms' ),\n\t\t'SI' => __( 'Siena', 'lifterlms' ),\n\t\t'SR' => __( 'Siracusa', 'lifterlms' ),\n\t\t'SO' => __( 'Sondrio', 'lifterlms' ),\n\t\t'SU' => __( 'Sud Sardegna', 'lifterlms' ),\n\t\t'TA' => __( 'Taranto', 'lifterlms' ),\n\t\t'TE' => __( 'Teramo', 'lifterlms' ),\n\t\t'TR' => __( 'Terni', 'lifterlms' ),\n\t\t'TO' => __( 'Torino', 'lifterlms' ),\n\t\t'TP' => __( 'Trapani', 'lifterlms' ),\n\t\t'TN' => __( 'Trento', 'lifterlms' ),\n\t\t'TV' => __( 'Treviso', 'lifterlms' ),\n\t\t'TS' => __( 'Trieste', 'lifterlms' ),\n\t\t'UD' => __( 'Udine', 'lifterlms' ),\n\t\t'VA' => __( 'Varese', 'lifterlms' ),\n\t\t'VE' => __( 'Venezia', 'lifterlms' ),\n\t\t'VB' => __( 'Verbano-Cusio-Ossola', 'lifterlms' ),\n\t\t'VC' => __( 'Vercelli', 'lifterlms' ),\n\t\t'VR' => __( 'Verona', 'lifterlms' ),\n\t\t'VV' => __( 'Vibo Valentia', 'lifterlms' ),\n\t\t'VI' => __( 'Vicenza', 'lifterlms' ),\n\t\t'VT' => __( 'Viterbo', 'lifterlms' ),\n\t),\n\t'IL' => array(),\n\t'IM' => array(),\n\t'JM' => array( // Jamaican parishes.\n\t\t'JM-01' => __( 'Kingston', 'lifterlms' ),\n\t\t'JM-02' => __( 'Saint Andrew', 'lifterlms' ),\n\t\t'JM-03' => __( 'Saint Thomas', 'lifterlms' ),\n\t\t'JM-04' => __( 'Portland', 'lifterlms' ),\n\t\t'JM-05' => __( 'Saint Mary', 'lifterlms' ),\n\t\t'JM-06' => __( 'Saint Ann', 'lifterlms' ),\n\t\t'JM-07' => __( 'Trelawny', 'lifterlms' ),\n\t\t'JM-08' => __( 'Saint James', 'lifterlms' ),\n\t\t'JM-09' => __( 'Hanover', 'lifterlms' ),\n\t\t'JM-10' => __( 'Westmoreland', 'lifterlms' ),\n\t\t'JM-11' => __( 'Saint Elizabeth', 'lifterlms' ),\n\t\t'JM-12' => __( 'Manchester', 'lifterlms' ),\n\t\t'JM-13' => __( 'Clarendon', 'lifterlms' ),\n\t\t'JM-14' => __( 'Saint Catherine', 'lifterlms' ),\n\t),\n\n\t/**\n\t * Japanese states.\n\t *\n\t * English notation of prefectures conform to the notation of Japan Post.\n\t * The suffix corresponds with the Japanese translation file.\n\t */\n\t'JP' => array(\n\t\t'JP01' => __( 'Hokkaido', 'lifterlms' ),\n\t\t'JP02' => __( 'Aomori', 'lifterlms' ),\n\t\t'JP03' => __( 'Iwate', 'lifterlms' ),\n\t\t'JP04' => __( 'Miyagi', 'lifterlms' ),\n\t\t'JP05' => __( 'Akita', 'lifterlms' ),\n\t\t'JP06' => __( 'Yamagata', 'lifterlms' ),\n\t\t'JP07' => __( 'Fukushima', 'lifterlms' ),\n\t\t'JP08' => __( 'Ibaraki', 'lifterlms' ),\n\t\t'JP09' => __( 'Tochigi', 'lifterlms' ),\n\t\t'JP10' => __( 'Gunma', 'lifterlms' ),\n\t\t'JP11' => __( 'Saitama', 'lifterlms' ),\n\t\t'JP12' => __( 'Chiba', 'lifterlms' ),\n\t\t'JP13' => __( 'Tokyo', 'lifterlms' ),\n\t\t'JP14' => __( 'Kanagawa', 'lifterlms' ),\n\t\t'JP15' => __( 'Niigata', 'lifterlms' ),\n\t\t'JP16' => __( 'Toyama', 'lifterlms' ),\n\t\t'JP17' => __( 'Ishikawa', 'lifterlms' ),\n\t\t'JP18' => __( 'Fukui', 'lifterlms' ),\n\t\t'JP19' => __( 'Yamanashi', 'lifterlms' ),\n\t\t'JP20' => __( 'Nagano', 'lifterlms' ),\n\t\t'JP21' => __( 'Gifu', 'lifterlms' ),\n\t\t'JP22' => __( 'Shizuoka', 'lifterlms' ),\n\t\t'JP23' => __( 'Aichi', 'lifterlms' ),\n\t\t'JP24' => __( 'Mie', 'lifterlms' ),\n\t\t'JP25' => __( 'Shiga', 'lifterlms' ),\n\t\t'JP26' => __( 'Kyoto', 'lifterlms' ),\n\t\t'JP27' => __( 'Osaka', 'lifterlms' ),\n\t\t'JP28' => __( 'Hyogo', 'lifterlms' ),\n\t\t'JP29' => __( 'Nara', 'lifterlms' ),\n\t\t'JP30' => __( 'Wakayama', 'lifterlms' ),\n\t\t'JP31' => __( 'Tottori', 'lifterlms' ),\n\t\t'JP32' => __( 'Shimane', 'lifterlms' ),\n\t\t'JP33' => __( 'Okayama', 'lifterlms' ),\n\t\t'JP34' => __( 'Hiroshima', 'lifterlms' ),\n\t\t'JP35' => __( 'Yamaguchi', 'lifterlms' ),\n\t\t'JP36' => __( 'Tokushima', 'lifterlms' ),\n\t\t'JP37' => __( 'Kagawa', 'lifterlms' ),\n\t\t'JP38' => __( 'Ehime', 'lifterlms' ),\n\t\t'JP39' => __( 'Kochi', 'lifterlms' ),\n\t\t'JP40' => __( 'Fukuoka', 'lifterlms' ),\n\t\t'JP41' => __( 'Saga', 'lifterlms' ),\n\t\t'JP42' => __( 'Nagasaki', 'lifterlms' ),\n\t\t'JP43' => __( 'Kumamoto', 'lifterlms' ),\n\t\t'JP44' => __( 'Oita', 'lifterlms' ),\n\t\t'JP45' => __( 'Miyazaki', 'lifterlms' ),\n\t\t'JP46' => __( 'Kagoshima', 'lifterlms' ),\n\t\t'JP47' => __( 'Okinawa', 'lifterlms' ),\n\t),\n\t'KE' => array( // Kenyan counties.\n\t\t'KE01' => __( 'Baringo', 'lifterlms' ),\n\t\t'KE02' => __( 'Bomet', 'lifterlms' ),\n\t\t'KE03' => __( 'Bungoma', 'lifterlms' ),\n\t\t'KE04' => __( 'Busia', 'lifterlms' ),\n\t\t'KE05' => __( 'Elgeyo-Marakwet', 'lifterlms' ),\n\t\t'KE06' => __( 'Embu', 'lifterlms' ),\n\t\t'KE07' => __( 'Garissa', 'lifterlms' ),\n\t\t'KE08' => __( 'Homa Bay', 'lifterlms' ),\n\t\t'KE09' => __( 'Isiolo', 'lifterlms' ),\n\t\t'KE10' => __( 'Kajiado', 'lifterlms' ),\n\t\t'KE11' => __( 'Kakamega', 'lifterlms' ),\n\t\t'KE12' => __( 'Kericho', 'lifterlms' ),\n\t\t'KE13' => __( 'Kiambu', 'lifterlms' ),\n\t\t'KE14' => __( 'Kilifi', 'lifterlms' ),\n\t\t'KE15' => __( 'Kirinyaga', 'lifterlms' ),\n\t\t'KE16' => __( 'Kisii', 'lifterlms' ),\n\t\t'KE17' => __( 'Kisumu', 'lifterlms' ),\n\t\t'KE18' => __( 'Kitui', 'lifterlms' ),\n\t\t'KE19' => __( 'Kwale', 'lifterlms' ),\n\t\t'KE20' => __( 'Laikipia', 'lifterlms' ),\n\t\t'KE21' => __( 'Lamu', 'lifterlms' ),\n\t\t'KE22' => __( 'Machakos', 'lifterlms' ),\n\t\t'KE23' => __( 'Makueni', 'lifterlms' ),\n\t\t'KE24' => __( 'Mandera', 'lifterlms' ),\n\t\t'KE25' => __( 'Marsabit', 'lifterlms' ),\n\t\t'KE26' => __( 'Meru', 'lifterlms' ),\n\t\t'KE27' => __( 'Migori', 'lifterlms' ),\n\t\t'KE28' => __( 'Mombasa', 'lifterlms' ),\n\t\t'KE29' => __( 'Murang’a', 'lifterlms' ),\n\t\t'KE30' => __( 'Nairobi County', 'lifterlms' ),\n\t\t'KE31' => __( 'Nakuru', 'lifterlms' ),\n\t\t'KE32' => __( 'Nandi', 'lifterlms' ),\n\t\t'KE33' => __( 'Narok', 'lifterlms' ),\n\t\t'KE34' => __( 'Nyamira', 'lifterlms' ),\n\t\t'KE35' => __( 'Nyandarua', 'lifterlms' ),\n\t\t'KE36' => __( 'Nyeri', 'lifterlms' ),\n\t\t'KE37' => __( 'Samburu', 'lifterlms' ),\n\t\t'KE38' => __( 'Siaya', 'lifterlms' ),\n\t\t'KE39' => __( 'Taita-Taveta', 'lifterlms' ),\n\t\t'KE40' => __( 'Tana River', 'lifterlms' ),\n\t\t'KE41' => __( 'Tharaka-Nithi', 'lifterlms' ),\n\t\t'KE42' => __( 'Trans Nzoia', 'lifterlms' ),\n\t\t'KE43' => __( 'Turkana', 'lifterlms' ),\n\t\t'KE44' => __( 'Uasin Gishu', 'lifterlms' ),\n\t\t'KE45' => __( 'Vihiga', 'lifterlms' ),\n\t\t'KE46' => __( 'Wajir', 'lifterlms' ),\n\t\t'KE47' => __( 'West Pokot', 'lifterlms' ),\n\t),\n\t'KN' => array( // Saint Kitts and Nevis parishes.\n\t\t'KNK'  => __( 'Saint Kitts', 'lifterlms' ),\n\t\t'KNN'  => __( 'Nevis', 'lifterlms' ),\n\t\t'KN01' => __( 'Christ Church Nichola Town', 'lifterlms' ),\n\t\t'KN02' => __( 'Saint Anne Sandy Point', 'lifterlms' ),\n\t\t'KN03' => __( 'Saint George Basseterre', 'lifterlms' ),\n\t\t'KN04' => __( 'Saint George Gingerland', 'lifterlms' ),\n\t\t'KN05' => __( 'Saint James Windward', 'lifterlms' ),\n\t\t'KN06' => __( 'Saint John Capisterre', 'lifterlms' ),\n\t\t'KN07' => __( 'Saint John Figtree', 'lifterlms' ),\n\t\t'KN08' => __( 'Saint Mary Cayon', 'lifterlms' ),\n\t\t'KN09' => __( 'Saint Paul Capisterre', 'lifterlms' ),\n\t\t'KN10' => __( 'Saint Paul Charlestown', 'lifterlms' ),\n\t\t'KN11' => __( 'Saint Peter Basseterre', 'lifterlms' ),\n\t\t'KN12' => __( 'Saint Thomas Lowland', 'lifterlms' ),\n\t\t'KN13' => __( 'Saint Thomas Middle Island', 'lifterlms' ),\n\t\t'KN15' => __( 'Trinity Palmetto Point', 'lifterlms' ),\n\t),\n\t'KR' => array(),\n\t'KW' => array(),\n\t'LA' => array( // Laotian provinces.\n\t\t'AT' => __( 'Attapeu', 'lifterlms' ),\n\t\t'BK' => __( 'Bokeo', 'lifterlms' ),\n\t\t'BL' => __( 'Bolikhamsai', 'lifterlms' ),\n\t\t'CH' => __( 'Champasak', 'lifterlms' ),\n\t\t'HO' => __( 'Houaphanh', 'lifterlms' ),\n\t\t'KH' => __( 'Khammouane', 'lifterlms' ),\n\t\t'LM' => __( 'Luang Namtha', 'lifterlms' ),\n\t\t'LP' => __( 'Luang Prabang', 'lifterlms' ),\n\t\t'OU' => __( 'Oudomxay', 'lifterlms' ),\n\t\t'PH' => __( 'Phongsaly', 'lifterlms' ),\n\t\t'SL' => __( 'Salavan', 'lifterlms' ),\n\t\t'SV' => __( 'Savannakhet', 'lifterlms' ),\n\t\t'VI' => __( 'Vientiane Province', 'lifterlms' ),\n\t\t'VT' => __( 'Vientiane', 'lifterlms' ),\n\t\t'XA' => __( 'Sainyabuli', 'lifterlms' ),\n\t\t'XE' => __( 'Sekong', 'lifterlms' ),\n\t\t'XI' => __( 'Xiangkhouang', 'lifterlms' ),\n\t\t'XS' => __( 'Xaisomboun', 'lifterlms' ),\n\t),\n\t'LB' => array(),\n\t'LI' => array(),\n\t'LR' => array( // Liberian provinces.\n\t\t'BM' => __( 'Bomi', 'lifterlms' ),\n\t\t'BN' => __( 'Bong', 'lifterlms' ),\n\t\t'GA' => __( 'Gbarpolu', 'lifterlms' ),\n\t\t'GB' => __( 'Grand Bassa', 'lifterlms' ),\n\t\t'GC' => __( 'Grand Cape Mount', 'lifterlms' ),\n\t\t'GG' => __( 'Grand Gedeh', 'lifterlms' ),\n\t\t'GK' => __( 'Grand Kru', 'lifterlms' ),\n\t\t'LO' => __( 'Lofa', 'lifterlms' ),\n\t\t'MA' => __( 'Margibi', 'lifterlms' ),\n\t\t'MY' => __( 'Maryland', 'lifterlms' ),\n\t\t'MO' => __( 'Montserrado', 'lifterlms' ),\n\t\t'NM' => __( 'Nimba', 'lifterlms' ),\n\t\t'RV' => __( 'Rivercess', 'lifterlms' ),\n\t\t'RG' => __( 'River Gee', 'lifterlms' ),\n\t\t'SN' => __( 'Sinoe', 'lifterlms' ),\n\t),\n\t'LU' => array(),\n\t'MA' => array( // Moroccan regions.\n\t\t'maagd' => __( 'Agadir-Ida Ou Tanane', 'lifterlms' ),\n\t\t'maazi' => __( 'Azilal', 'lifterlms' ),\n\t\t'mabem' => __( 'Béni-Mellal', 'lifterlms' ),\n\t\t'maber' => __( 'Berkane', 'lifterlms' ),\n\t\t'mabes' => __( 'Ben Slimane', 'lifterlms' ),\n\t\t'mabod' => __( 'Boujdour', 'lifterlms' ),\n\t\t'mabom' => __( 'Boulemane', 'lifterlms' ),\n\t\t'mabrr' => __( 'Berrechid', 'lifterlms' ),\n\t\t'macas' => __( 'Casablanca', 'lifterlms' ),\n\t\t'mache' => __( 'Chefchaouen', 'lifterlms' ),\n\t\t'machi' => __( 'Chichaoua', 'lifterlms' ),\n\t\t'macht' => __( 'Chtouka Aït Baha', 'lifterlms' ),\n\t\t'madri' => __( 'Driouch', 'lifterlms' ),\n\t\t'maedi' => __( 'Essaouira', 'lifterlms' ),\n\t\t'maerr' => __( 'Errachidia', 'lifterlms' ),\n\t\t'mafah' => __( 'Fahs-Beni Makada', 'lifterlms' ),\n\t\t'mafes' => __( 'Fès-Dar-Dbibegh', 'lifterlms' ),\n\t\t'mafig' => __( 'Figuig', 'lifterlms' ),\n\t\t'mafqh' => __( 'Fquih Ben Salah', 'lifterlms' ),\n\t\t'mafes' => __( 'Fès-Dar-Dbibegh', 'lifterlms' ),\n\t\t'mague' => __( 'Guelmim', 'lifterlms' ),\n\t\t'maguf' => __( 'Guercif', 'lifterlms' ),\n\t\t'mahaj' => __( 'El Hajeb', 'lifterlms' ),\n\t\t'mahao' => __( 'Al Haouz', 'lifterlms' ),\n\t\t'mahoc' => __( 'Al Hoceïma', 'lifterlms' ),\n\t\t'maifr' => __( 'Ifrane', 'lifterlms' ),\n\t\t'maine' => __( 'Inezgane-Aït Melloul', 'lifterlms' ),\n\t\t'majdi' => __( 'El Jadida', 'lifterlms' ),\n\t\t'majra' => __( 'Jerada', 'lifterlms' ),\n\t\t'maken' => __( 'Kénitra', 'lifterlms' ),\n\t\t'makes' => __( 'Kelaat Sraghna', 'lifterlms' ),\n\t\t'makhe' => __( 'Khemisset', 'lifterlms' ),\n\t\t'makhn' => __( 'Khénifra', 'lifterlms' ),\n\t\t'makho' => __( 'Khouribga', 'lifterlms' ),\n\t\t'malaa' => __( 'Laâyoune', 'lifterlms' ),\n\t\t'malar' => __( 'Larache', 'lifterlms' ),\n\t\t'mamar' => __( 'Marrakech', 'lifterlms' ),\n\t\t'mamdf' => __( 'M’diq-Fnideq', 'lifterlms' ),\n\t\t'mamed' => __( 'Médiouna', 'lifterlms' ),\n\t\t'mamek' => __( 'Meknès', 'lifterlms' ),\n\t\t'mamid' => __( 'Midelt', 'lifterlms' ),\n\t\t'mammd' => __( 'Marrakech-Medina', 'lifterlms' ),\n\t\t'mammn' => __( 'Marrakech-Menara', 'lifterlms' ),\n\t\t'mamoh' => __( 'Mohammedia', 'lifterlms' ),\n\t\t'mamou' => __( 'Moulay Yacoub', 'lifterlms' ),\n\t\t'manad' => __( 'Nador', 'lifterlms' ),\n\t\t'manou' => __( 'Nouaceur', 'lifterlms' ),\n\t\t'maoua' => __( 'Ouarzazate', 'lifterlms' ),\n\t\t'maoud' => __( 'Oued Ed-Dahab', 'lifterlms' ),\n\t\t'maouj' => __( 'Oujda-Angad', 'lifterlms' ),\n\t\t'maouz' => __( 'Ouezzane', 'lifterlms' ),\n\t\t'marab' => __( 'Rabat', 'lifterlms' ),\n\t\t'mareh' => __( 'Rehamna', 'lifterlms' ),\n\t\t'masaf' => __( 'Safi', 'lifterlms' ),\n\t\t'masal' => __( 'Salé', 'lifterlms' ),\n\t\t'masef' => __( 'Sefrou', 'lifterlms' ),\n\t\t'maset' => __( 'Settat', 'lifterlms' ),\n\t\t'masib' => __( 'Sidi Bennour', 'lifterlms' ),\n\t\t'masif' => __( 'Sidi Ifni', 'lifterlms' ),\n\t\t'masik' => __( 'Sidi Kacem', 'lifterlms' ),\n\t\t'masil' => __( 'Sidi Slimane', 'lifterlms' ),\n\t\t'maskh' => __( 'Skhirat-Témara', 'lifterlms' ),\n\t\t'masyb' => __( 'Sidi Youssef Ben Ali', 'lifterlms' ),\n\t\t'mataf' => __( 'Tarfaya (EH-partial)', 'lifterlms' ),\n\t\t'matai' => __( 'Taourirt', 'lifterlms' ),\n\t\t'matao' => __( 'Taounate', 'lifterlms' ),\n\t\t'matar' => __( 'Taroudant', 'lifterlms' ),\n\t\t'matat' => __( 'Tata', 'lifterlms' ),\n\t\t'mataz' => __( 'Taza', 'lifterlms' ),\n\t\t'matet' => __( 'Tétouan', 'lifterlms' ),\n\t\t'matin' => __( 'Tinghir', 'lifterlms' ),\n\t\t'matiz' => __( 'Tiznit', 'lifterlms' ),\n\t\t'matng' => __( 'Tangier-Assilah', 'lifterlms' ),\n\t\t'matnt' => __( 'Tan-Tan', 'lifterlms' ),\n\t\t'mayus' => __( 'Youssoufia', 'lifterlms' ),\n\t\t'mazag' => __( 'Zagora', 'lifterlms' )\n\t),\n\t'MD' => array( // Moldovan states.\n\t\t'C'  => __( 'Chișinău', 'lifterlms' ),\n\t\t'BL' => __( 'Bălți', 'lifterlms' ),\n\t\t'AN' => __( 'Anenii Noi', 'lifterlms' ),\n\t\t'BS' => __( 'Basarabeasca', 'lifterlms' ),\n\t\t'BR' => __( 'Briceni', 'lifterlms' ),\n\t\t'CH' => __( 'Cahul', 'lifterlms' ),\n\t\t'CT' => __( 'Cantemir', 'lifterlms' ),\n\t\t'CL' => __( 'Călărași', 'lifterlms' ),\n\t\t'CS' => __( 'Căușeni', 'lifterlms' ),\n\t\t'CM' => __( 'Cimișlia', 'lifterlms' ),\n\t\t'CR' => __( 'Criuleni', 'lifterlms' ),\n\t\t'DN' => __( 'Dondușeni', 'lifterlms' ),\n\t\t'DR' => __( 'Drochia', 'lifterlms' ),\n\t\t'DB' => __( 'Dubăsari', 'lifterlms' ),\n\t\t'ED' => __( 'Edineț', 'lifterlms' ),\n\t\t'FL' => __( 'Fălești', 'lifterlms' ),\n\t\t'FR' => __( 'Florești', 'lifterlms' ),\n\t\t'GE' => __( 'UTA Găgăuzia', 'lifterlms' ),\n\t\t'GL' => __( 'Glodeni', 'lifterlms' ),\n\t\t'HN' => __( 'Hîncești', 'lifterlms' ),\n\t\t'IL' => __( 'Ialoveni', 'lifterlms' ),\n\t\t'LV' => __( 'Leova', 'lifterlms' ),\n\t\t'NS' => __( 'Nisporeni', 'lifterlms' ),\n\t\t'OC' => __( 'Ocnița', 'lifterlms' ),\n\t\t'OR' => __( 'Orhei', 'lifterlms' ),\n\t\t'RZ' => __( 'Rezina', 'lifterlms' ),\n\t\t'RS' => __( 'Rîșcani', 'lifterlms' ),\n\t\t'SG' => __( 'Sîngerei', 'lifterlms' ),\n\t\t'SR' => __( 'Soroca', 'lifterlms' ),\n\t\t'ST' => __( 'Strășeni', 'lifterlms' ),\n\t\t'SD' => __( 'Șoldănești', 'lifterlms' ),\n\t\t'SV' => __( 'Ștefan Vodă', 'lifterlms' ),\n\t\t'TR' => __( 'Taraclia', 'lifterlms' ),\n\t\t'TL' => __( 'Telenești', 'lifterlms' ),\n\t\t'UN' => __( 'Ungheni', 'lifterlms' ),\n\t),\n\t'MF' => array(),\n\t'MQ' => array(),\n\t'MT' => array(),\n\t'MX' => array( // Mexican states.\n\t\t'DF' => __( 'Ciudad de México', 'lifterlms' ),\n\t\t'JA' => __( 'Jalisco', 'lifterlms' ),\n\t\t'NL' => __( 'Nuevo León', 'lifterlms' ),\n\t\t'AG' => __( 'Aguascalientes', 'lifterlms' ),\n\t\t'BC' => __( 'Baja California', 'lifterlms' ),\n\t\t'BS' => __( 'Baja California Sur', 'lifterlms' ),\n\t\t'CM' => __( 'Campeche', 'lifterlms' ),\n\t\t'CS' => __( 'Chiapas', 'lifterlms' ),\n\t\t'CH' => __( 'Chihuahua', 'lifterlms' ),\n\t\t'CO' => __( 'Coahuila', 'lifterlms' ),\n\t\t'CL' => __( 'Colima', 'lifterlms' ),\n\t\t'DG' => __( 'Durango', 'lifterlms' ),\n\t\t'GT' => __( 'Guanajuato', 'lifterlms' ),\n\t\t'GR' => __( 'Guerrero', 'lifterlms' ),\n\t\t'HG' => __( 'Hidalgo', 'lifterlms' ),\n\t\t'MX' => __( 'Estado de México', 'lifterlms' ),\n\t\t'MI' => __( 'Michoacán', 'lifterlms' ),\n\t\t'MO' => __( 'Morelos', 'lifterlms' ),\n\t\t'NA' => __( 'Nayarit', 'lifterlms' ),\n\t\t'OA' => __( 'Oaxaca', 'lifterlms' ),\n\t\t'PU' => __( 'Puebla', 'lifterlms' ),\n\t\t'QT' => __( 'Querétaro', 'lifterlms' ),\n\t\t'QR' => __( 'Quintana Roo', 'lifterlms' ),\n\t\t'SL' => __( 'San Luis Potosí', 'lifterlms' ),\n\t\t'SI' => __( 'Sinaloa', 'lifterlms' ),\n\t\t'SO' => __( 'Sonora', 'lifterlms' ),\n\t\t'TB' => __( 'Tabasco', 'lifterlms' ),\n\t\t'TM' => __( 'Tamaulipas', 'lifterlms' ),\n\t\t'TL' => __( 'Tlaxcala', 'lifterlms' ),\n\t\t'VE' => __( 'Veracruz', 'lifterlms' ),\n\t\t'YU' => __( 'Yucatán', 'lifterlms' ),\n\t\t'ZA' => __( 'Zacatecas', 'lifterlms' ),\n\t),\n\t'MY' => array( // Malaysian states.\n\t\t'JHR' => __( 'Johor', 'lifterlms' ),\n\t\t'KDH' => __( 'Kedah', 'lifterlms' ),\n\t\t'KTN' => __( 'Kelantan', 'lifterlms' ),\n\t\t'LBN' => __( 'Labuan', 'lifterlms' ),\n\t\t'MLK' => __( 'Malacca (Melaka)', 'lifterlms' ),\n\t\t'NSN' => __( 'Negeri Sembilan', 'lifterlms' ),\n\t\t'PHG' => __( 'Pahang', 'lifterlms' ),\n\t\t'PNG' => __( 'Penang (Pulau Pinang)', 'lifterlms' ),\n\t\t'PRK' => __( 'Perak', 'lifterlms' ),\n\t\t'PLS' => __( 'Perlis', 'lifterlms' ),\n\t\t'SBH' => __( 'Sabah', 'lifterlms' ),\n\t\t'SWK' => __( 'Sarawak', 'lifterlms' ),\n\t\t'SGR' => __( 'Selangor', 'lifterlms' ),\n\t\t'TRG' => __( 'Terengganu', 'lifterlms' ),\n\t\t'PJY' => __( 'Putrajaya', 'lifterlms' ),\n\t\t'KUL' => __( 'Kuala Lumpur', 'lifterlms' ),\n\t),\n\t'MZ' => array( // Mozambican provinces.\n\t\t'MZP'   => __( 'Cabo Delgado', 'lifterlms' ),\n\t\t'MZG'   => __( 'Gaza', 'lifterlms' ),\n\t\t'MZI'   => __( 'Inhambane', 'lifterlms' ),\n\t\t'MZB'   => __( 'Manica', 'lifterlms' ),\n\t\t'MZL'   => __( 'Maputo Province', 'lifterlms' ),\n\t\t'MZMPM' => __( 'Maputo', 'lifterlms' ),\n\t\t'MZN'   => __( 'Nampula', 'lifterlms' ),\n\t\t'MZA'   => __( 'Niassa', 'lifterlms' ),\n\t\t'MZS'   => __( 'Sofala', 'lifterlms' ),\n\t\t'MZT'   => __( 'Tete', 'lifterlms' ),\n\t\t'MZQ'   => __( 'Zambézia', 'lifterlms' ),\n\t),\n\t'NA' => array( // Namibian regions.\n\t\t'ER' => __( 'Erongo', 'lifterlms' ),\n\t\t'HA' => __( 'Hardap', 'lifterlms' ),\n\t\t'KA' => __( 'Karas', 'lifterlms' ),\n\t\t'KE' => __( 'Kavango East', 'lifterlms' ),\n\t\t'KW' => __( 'Kavango West', 'lifterlms' ),\n\t\t'KH' => __( 'Khomas', 'lifterlms' ),\n\t\t'KU' => __( 'Kunene', 'lifterlms' ),\n\t\t'OW' => __( 'Ohangwena', 'lifterlms' ),\n\t\t'OH' => __( 'Omaheke', 'lifterlms' ),\n\t\t'OS' => __( 'Omusati', 'lifterlms' ),\n\t\t'ON' => __( 'Oshana', 'lifterlms' ),\n\t\t'OT' => __( 'Oshikoto', 'lifterlms' ),\n\t\t'OD' => __( 'Otjozondjupa', 'lifterlms' ),\n\t\t'CA' => __( 'Zambezi', 'lifterlms' ),\n\t),\n\t'NG' => array( // Nigerian provinces.\n\t\t'AB' => __( 'Abia', 'lifterlms' ),\n\t\t'FC' => __( 'Abuja', 'lifterlms' ),\n\t\t'AD' => __( 'Adamawa', 'lifterlms' ),\n\t\t'AK' => __( 'Akwa Ibom', 'lifterlms' ),\n\t\t'AN' => __( 'Anambra', 'lifterlms' ),\n\t\t'BA' => __( 'Bauchi', 'lifterlms' ),\n\t\t'BY' => __( 'Bayelsa', 'lifterlms' ),\n\t\t'BE' => __( 'Benue', 'lifterlms' ),\n\t\t'BO' => __( 'Borno', 'lifterlms' ),\n\t\t'CR' => __( 'Cross River', 'lifterlms' ),\n\t\t'DE' => __( 'Delta', 'lifterlms' ),\n\t\t'EB' => __( 'Ebonyi', 'lifterlms' ),\n\t\t'ED' => __( 'Edo', 'lifterlms' ),\n\t\t'EK' => __( 'Ekiti', 'lifterlms' ),\n\t\t'EN' => __( 'Enugu', 'lifterlms' ),\n\t\t'GO' => __( 'Gombe', 'lifterlms' ),\n\t\t'IM' => __( 'Imo', 'lifterlms' ),\n\t\t'JI' => __( 'Jigawa', 'lifterlms' ),\n\t\t'KD' => __( 'Kaduna', 'lifterlms' ),\n\t\t'KN' => __( 'Kano', 'lifterlms' ),\n\t\t'KT' => __( 'Katsina', 'lifterlms' ),\n\t\t'KE' => __( 'Kebbi', 'lifterlms' ),\n\t\t'KO' => __( 'Kogi', 'lifterlms' ),\n\t\t'KW' => __( 'Kwara', 'lifterlms' ),\n\t\t'LA' => __( 'Lagos', 'lifterlms' ),\n\t\t'NA' => __( 'Nasarawa', 'lifterlms' ),\n\t\t'NI' => __( 'Niger', 'lifterlms' ),\n\t\t'OG' => __( 'Ogun', 'lifterlms' ),\n\t\t'ON' => __( 'Ondo', 'lifterlms' ),\n\t\t'OS' => __( 'Osun', 'lifterlms' ),\n\t\t'OY' => __( 'Oyo', 'lifterlms' ),\n\t\t'PL' => __( 'Plateau', 'lifterlms' ),\n\t\t'RI' => __( 'Rivers', 'lifterlms' ),\n\t\t'SO' => __( 'Sokoto', 'lifterlms' ),\n\t\t'TA' => __( 'Taraba', 'lifterlms' ),\n\t\t'YO' => __( 'Yobe', 'lifterlms' ),\n\t\t'ZA' => __( 'Zamfara', 'lifterlms' ),\n\t),\n\t'NL' => array(),\n\t'NO' => array(),\n\t'NP' => array( // Nepalese zones.\n\t\t'BAG' => __( 'Bagmati', 'lifterlms' ),\n\t\t'BHE' => __( 'Bheri', 'lifterlms' ),\n\t\t'DHA' => __( 'Dhaulagiri', 'lifterlms' ),\n\t\t'GAN' => __( 'Gandaki', 'lifterlms' ),\n\t\t'JAN' => __( 'Janakpur', 'lifterlms' ),\n\t\t'KAR' => __( 'Karnali', 'lifterlms' ),\n\t\t'KOS' => __( 'Koshi', 'lifterlms' ),\n\t\t'LUM' => __( 'Lumbini', 'lifterlms' ),\n\t\t'MAH' => __( 'Mahakali', 'lifterlms' ),\n\t\t'MEC' => __( 'Mechi', 'lifterlms' ),\n\t\t'NAR' => __( 'Narayani', 'lifterlms' ),\n\t\t'RAP' => __( 'Rapti', 'lifterlms' ),\n\t\t'SAG' => __( 'Sagarmatha', 'lifterlms' ),\n\t\t'SET' => __( 'Seti', 'lifterlms' ),\n\t),\n\t'NI' => array( // Nicaraguan states.\n\t\t'NI-AN' => __( 'Atlántico Norte', 'lifterlms' ),\n\t\t'NI-AS' => __( 'Atlántico Sur', 'lifterlms' ),\n\t\t'NI-BO' => __( 'Boaco', 'lifterlms' ),\n\t\t'NI-CA' => __( 'Carazo', 'lifterlms' ),\n\t\t'NI-CI' => __( 'Chinandega', 'lifterlms' ),\n\t\t'NI-CO' => __( 'Chontales', 'lifterlms' ),\n\t\t'NI-ES' => __( 'Estelí', 'lifterlms' ),\n\t\t'NI-GR' => __( 'Granada', 'lifterlms' ),\n\t\t'NI-JI' => __( 'Jinotega', 'lifterlms' ),\n\t\t'NI-LE' => __( 'León', 'lifterlms' ),\n\t\t'NI-MD' => __( 'Madriz', 'lifterlms' ),\n\t\t'NI-MN' => __( 'Managua', 'lifterlms' ),\n\t\t'NI-MS' => __( 'Masaya', 'lifterlms' ),\n\t\t'NI-MT' => __( 'Matagalpa', 'lifterlms' ),\n\t\t'NI-NS' => __( 'Nueva Segovia', 'lifterlms' ),\n\t\t'NI-RI' => __( 'Rivas', 'lifterlms' ),\n\t\t'NI-SJ' => __( 'Río San Juan', 'lifterlms' ),\n\t),\n\t'NZ' => array( // New Zealand states.\n\t\t'NTL' => __( 'Northland', 'lifterlms' ),\n\t\t'AUK' => __( 'Auckland', 'lifterlms' ),\n\t\t'WKO' => __( 'Waikato', 'lifterlms' ),\n\t\t'BOP' => __( 'Bay of Plenty', 'lifterlms' ),\n\t\t'TKI' => __( 'Taranaki', 'lifterlms' ),\n\t\t'GIS' => __( 'Gisborne', 'lifterlms' ),\n\t\t'HKB' => __( 'Hawke’s Bay', 'lifterlms' ),\n\t\t'MWT' => __( 'Manawatu-Whanganui', 'lifterlms' ),\n\t\t'WGN' => __( 'Wellington', 'lifterlms' ),\n\t\t'NSN' => __( 'Nelson', 'lifterlms' ),\n\t\t'MBH' => __( 'Marlborough', 'lifterlms' ),\n\t\t'TAS' => __( 'Tasman', 'lifterlms' ),\n\t\t'WTC' => __( 'West Coast', 'lifterlms' ),\n\t\t'CAN' => __( 'Canterbury', 'lifterlms' ),\n\t\t'OTA' => __( 'Otago', 'lifterlms' ),\n\t\t'STL' => __( 'Southland', 'lifterlms' ),\n\t),\n\t'PA' => array( // Panamanian states.\n\t\t'PA-1' => __( 'Bocas del Toro', 'lifterlms' ),\n\t\t'PA-2' => __( 'Coclé', 'lifterlms' ),\n\t\t'PA-3' => __( 'Colón', 'lifterlms' ),\n\t\t'PA-4' => __( 'Chiriquí', 'lifterlms' ),\n\t\t'PA-5' => __( 'Darién', 'lifterlms' ),\n\t\t'PA-6' => __( 'Herrera', 'lifterlms' ),\n\t\t'PA-7' => __( 'Los Santos', 'lifterlms' ),\n\t\t'PA-8' => __( 'Panamá', 'lifterlms' ),\n\t\t'PA-9' => __( 'Veraguas', 'lifterlms' ),\n\t\t'PA-10' => __( 'West Panamá', 'lifterlms' ),\n\t\t'PA-EM' => __( 'Emberá', 'lifterlms' ),\n\t\t'PA-KY' => __( 'Guna Yala', 'lifterlms' ),\n\t\t'PA-NB' => __( 'Ngöbe-Buglé', 'lifterlms' ),\n\t),\n\t'PE' => array( // Peruvian states.\n\t\t'CAL' => __( 'El Callao', 'lifterlms' ),\n\t\t'LMA' => __( 'Municipalidad Metropolitana de Lima', 'lifterlms' ),\n\t\t'AMA' => __( 'Amazonas', 'lifterlms' ),\n\t\t'ANC' => __( 'Ancash', 'lifterlms' ),\n\t\t'APU' => __( 'Apurímac', 'lifterlms' ),\n\t\t'ARE' => __( 'Arequipa', 'lifterlms' ),\n\t\t'AYA' => __( 'Ayacucho', 'lifterlms' ),\n\t\t'CAJ' => __( 'Cajamarca', 'lifterlms' ),\n\t\t'CUS' => __( 'Cusco', 'lifterlms' ),\n\t\t'HUV' => __( 'Huancavelica', 'lifterlms' ),\n\t\t'HUC' => __( 'Huánuco', 'lifterlms' ),\n\t\t'ICA' => __( 'Ica', 'lifterlms' ),\n\t\t'JUN' => __( 'Junín', 'lifterlms' ),\n\t\t'LAL' => __( 'La Libertad', 'lifterlms' ),\n\t\t'LAM' => __( 'Lambayeque', 'lifterlms' ),\n\t\t'LIM' => __( 'Lima', 'lifterlms' ),\n\t\t'LOR' => __( 'Loreto', 'lifterlms' ),\n\t\t'MDD' => __( 'Madre de Dios', 'lifterlms' ),\n\t\t'MOQ' => __( 'Moquegua', 'lifterlms' ),\n\t\t'PAS' => __( 'Pasco', 'lifterlms' ),\n\t\t'PIU' => __( 'Piura', 'lifterlms' ),\n\t\t'PUN' => __( 'Puno', 'lifterlms' ),\n\t\t'SAM' => __( 'San Martín', 'lifterlms' ),\n\t\t'TAC' => __( 'Tacna', 'lifterlms' ),\n\t\t'TUM' => __( 'Tumbes', 'lifterlms' ),\n\t\t'UCA' => __( 'Ucayali', 'lifterlms' ),\n\t),\n\t'PH' => array( // Philippine provinces.\n\t\t'ABR' => __( 'Abra', 'lifterlms' ),\n\t\t'AGN' => __( 'Agusan del Norte', 'lifterlms' ),\n\t\t'AGS' => __( 'Agusan del Sur', 'lifterlms' ),\n\t\t'AKL' => __( 'Aklan', 'lifterlms' ),\n\t\t'ALB' => __( 'Albay', 'lifterlms' ),\n\t\t'ANT' => __( 'Antique', 'lifterlms' ),\n\t\t'APA' => __( 'Apayao', 'lifterlms' ),\n\t\t'AUR' => __( 'Aurora', 'lifterlms' ),\n\t\t'BAS' => __( 'Basilan', 'lifterlms' ),\n\t\t'BAN' => __( 'Bataan', 'lifterlms' ),\n\t\t'BTN' => __( 'Batanes', 'lifterlms' ),\n\t\t'BTG' => __( 'Batangas', 'lifterlms' ),\n\t\t'BEN' => __( 'Benguet', 'lifterlms' ),\n\t\t'BIL' => __( 'Biliran', 'lifterlms' ),\n\t\t'BOH' => __( 'Bohol', 'lifterlms' ),\n\t\t'BUK' => __( 'Bukidnon', 'lifterlms' ),\n\t\t'BUL' => __( 'Bulacan', 'lifterlms' ),\n\t\t'CAG' => __( 'Cagayan', 'lifterlms' ),\n\t\t'CAN' => __( 'Camarines Norte', 'lifterlms' ),\n\t\t'CAS' => __( 'Camarines Sur', 'lifterlms' ),\n\t\t'CAM' => __( 'Camiguin', 'lifterlms' ),\n\t\t'CAP' => __( 'Capiz', 'lifterlms' ),\n\t\t'CAT' => __( 'Catanduanes', 'lifterlms' ),\n\t\t'CAV' => __( 'Cavite', 'lifterlms' ),\n\t\t'CEB' => __( 'Cebu', 'lifterlms' ),\n\t\t'COM' => __( 'Compostela Valley', 'lifterlms' ),\n\t\t'NCO' => __( 'Cotabato', 'lifterlms' ),\n\t\t'DAV' => __( 'Davao del Norte', 'lifterlms' ),\n\t\t'DAS' => __( 'Davao del Sur', 'lifterlms' ),\n\t\t'DAC' => __( 'Davao Occidental', 'lifterlms' ),\n\t\t'DAO' => __( 'Davao Oriental', 'lifterlms' ),\n\t\t'DIN' => __( 'Dinagat Islands', 'lifterlms' ),\n\t\t'EAS' => __( 'Eastern Samar', 'lifterlms' ),\n\t\t'GUI' => __( 'Guimaras', 'lifterlms' ),\n\t\t'IFU' => __( 'Ifugao', 'lifterlms' ),\n\t\t'ILN' => __( 'Ilocos Norte', 'lifterlms' ),\n\t\t'ILS' => __( 'Ilocos Sur', 'lifterlms' ),\n\t\t'ILI' => __( 'Iloilo', 'lifterlms' ),\n\t\t'ISA' => __( 'Isabela', 'lifterlms' ),\n\t\t'KAL' => __( 'Kalinga', 'lifterlms' ),\n\t\t'LUN' => __( 'La Union', 'lifterlms' ),\n\t\t'LAG' => __( 'Laguna', 'lifterlms' ),\n\t\t'LAN' => __( 'Lanao del Norte', 'lifterlms' ),\n\t\t'LAS' => __( 'Lanao del Sur', 'lifterlms' ),\n\t\t'LEY' => __( 'Leyte', 'lifterlms' ),\n\t\t'MAG' => __( 'Maguindanao', 'lifterlms' ),\n\t\t'MAD' => __( 'Marinduque', 'lifterlms' ),\n\t\t'MAS' => __( 'Masbate', 'lifterlms' ),\n\t\t'MSC' => __( 'Misamis Occidental', 'lifterlms' ),\n\t\t'MSR' => __( 'Misamis Oriental', 'lifterlms' ),\n\t\t'MOU' => __( 'Mountain Province', 'lifterlms' ),\n\t\t'NEC' => __( 'Negros Occidental', 'lifterlms' ),\n\t\t'NER' => __( 'Negros Oriental', 'lifterlms' ),\n\t\t'NSA' => __( 'Northern Samar', 'lifterlms' ),\n\t\t'NUE' => __( 'Nueva Ecija', 'lifterlms' ),\n\t\t'NUV' => __( 'Nueva Vizcaya', 'lifterlms' ),\n\t\t'MDC' => __( 'Occidental Mindoro', 'lifterlms' ),\n\t\t'MDR' => __( 'Oriental Mindoro', 'lifterlms' ),\n\t\t'PLW' => __( 'Palawan', 'lifterlms' ),\n\t\t'PAM' => __( 'Pampanga', 'lifterlms' ),\n\t\t'PAN' => __( 'Pangasinan', 'lifterlms' ),\n\t\t'QUE' => __( 'Quezon', 'lifterlms' ),\n\t\t'QUI' => __( 'Quirino', 'lifterlms' ),\n\t\t'RIZ' => __( 'Rizal', 'lifterlms' ),\n\t\t'ROM' => __( 'Romblon', 'lifterlms' ),\n\t\t'WSA' => __( 'Samar', 'lifterlms' ),\n\t\t'SAR' => __( 'Sarangani', 'lifterlms' ),\n\t\t'SIQ' => __( 'Siquijor', 'lifterlms' ),\n\t\t'SOR' => __( 'Sorsogon', 'lifterlms' ),\n\t\t'SCO' => __( 'South Cotabato', 'lifterlms' ),\n\t\t'SLE' => __( 'Southern Leyte', 'lifterlms' ),\n\t\t'SUK' => __( 'Sultan Kudarat', 'lifterlms' ),\n\t\t'SLU' => __( 'Sulu', 'lifterlms' ),\n\t\t'SUN' => __( 'Surigao del Norte', 'lifterlms' ),\n\t\t'SUR' => __( 'Surigao del Sur', 'lifterlms' ),\n\t\t'TAR' => __( 'Tarlac', 'lifterlms' ),\n\t\t'TAW' => __( 'Tawi-Tawi', 'lifterlms' ),\n\t\t'ZMB' => __( 'Zambales', 'lifterlms' ),\n\t\t'ZAN' => __( 'Zamboanga del Norte', 'lifterlms' ),\n\t\t'ZAS' => __( 'Zamboanga del Sur', 'lifterlms' ),\n\t\t'ZSI' => __( 'Zamboanga Sibugay', 'lifterlms' ),\n\t\t'00'  => __( 'Metro Manila', 'lifterlms' ),\n\t),\n\t'PK' => array( // Pakistani states.\n\t\t'JK' => __( 'Azad Kashmir', 'lifterlms' ),\n\t\t'BA' => __( 'Balochistan', 'lifterlms' ),\n\t\t'TA' => __( 'FATA', 'lifterlms' ),\n\t\t'GB' => __( 'Gilgit Baltistan', 'lifterlms' ),\n\t\t'IS' => __( 'Islamabad Capital Territory', 'lifterlms' ),\n\t\t'KP' => __( 'Khyber Pakhtunkhwa', 'lifterlms' ),\n\t\t'PB' => __( 'Punjab', 'lifterlms' ),\n\t\t'SD' => __( 'Sindh', 'lifterlms' ),\n\t),\n\t'PL' => array(),\n\t'PR' => array(),\n\t'PT' => array(),\n\t'PY' => array( // Paraguayan states.\n\t\t'PY-ASU' => __( 'Asunción', 'lifterlms' ),\n\t\t'PY-1'   => __( 'Concepción', 'lifterlms' ),\n\t\t'PY-2'   => __( 'San Pedro', 'lifterlms' ),\n\t\t'PY-3'   => __( 'Cordillera', 'lifterlms' ),\n\t\t'PY-4'   => __( 'Guairá', 'lifterlms' ),\n\t\t'PY-5'   => __( 'Caaguazú', 'lifterlms' ),\n\t\t'PY-6'   => __( 'Caazapá', 'lifterlms' ),\n\t\t'PY-7'   => __( 'Itapúa', 'lifterlms' ),\n\t\t'PY-8'   => __( 'Misiones', 'lifterlms' ),\n\t\t'PY-9'   => __( 'Paraguarí', 'lifterlms' ),\n\t\t'PY-10'  => __( 'Alto Paraná', 'lifterlms' ),\n\t\t'PY-11'  => __( 'Central', 'lifterlms' ),\n\t\t'PY-12'  => __( 'Ñeembucú', 'lifterlms' ),\n\t\t'PY-13'  => __( 'Amambay', 'lifterlms' ),\n\t\t'PY-14'  => __( 'Canindeyú', 'lifterlms' ),\n\t\t'PY-15'  => __( 'Presidente Hayes', 'lifterlms' ),\n\t\t'PY-16'  => __( 'Alto Paraguay', 'lifterlms' ),\n\t\t'PY-17'  => __( 'Boquerón', 'lifterlms' ),\n\t),\n\t'RE' => array(),\n\t'RO' => array( // Romanian states.\n\t\t'AB' => __( 'Alba', 'lifterlms' ),\n\t\t'AR' => __( 'Arad', 'lifterlms' ),\n\t\t'AG' => __( 'Argeș', 'lifterlms' ),\n\t\t'BC' => __( 'Bacău', 'lifterlms' ),\n\t\t'BH' => __( 'Bihor', 'lifterlms' ),\n\t\t'BN' => __( 'Bistrița-Năsăud', 'lifterlms' ),\n\t\t'BT' => __( 'Botoșani', 'lifterlms' ),\n\t\t'BR' => __( 'Brăila', 'lifterlms' ),\n\t\t'BV' => __( 'Brașov', 'lifterlms' ),\n\t\t'B'  => __( 'București', 'lifterlms' ),\n\t\t'BZ' => __( 'Buzău', 'lifterlms' ),\n\t\t'CL' => __( 'Călărași', 'lifterlms' ),\n\t\t'CS' => __( 'Caraș-Severin', 'lifterlms' ),\n\t\t'CJ' => __( 'Cluj', 'lifterlms' ),\n\t\t'CT' => __( 'Constanța', 'lifterlms' ),\n\t\t'CV' => __( 'Covasna', 'lifterlms' ),\n\t\t'DB' => __( 'Dâmbovița', 'lifterlms' ),\n\t\t'DJ' => __( 'Dolj', 'lifterlms' ),\n\t\t'GL' => __( 'Galați', 'lifterlms' ),\n\t\t'GR' => __( 'Giurgiu', 'lifterlms' ),\n\t\t'GJ' => __( 'Gorj', 'lifterlms' ),\n\t\t'HR' => __( 'Harghita', 'lifterlms' ),\n\t\t'HD' => __( 'Hunedoara', 'lifterlms' ),\n\t\t'IL' => __( 'Ialomița', 'lifterlms' ),\n\t\t'IS' => __( 'Iași', 'lifterlms' ),\n\t\t'IF' => __( 'Ilfov', 'lifterlms' ),\n\t\t'MM' => __( 'Maramureș', 'lifterlms' ),\n\t\t'MH' => __( 'Mehedinți', 'lifterlms' ),\n\t\t'MS' => __( 'Mureș', 'lifterlms' ),\n\t\t'NT' => __( 'Neamț', 'lifterlms' ),\n\t\t'OT' => __( 'Olt', 'lifterlms' ),\n\t\t'PH' => __( 'Prahova', 'lifterlms' ),\n\t\t'SJ' => __( 'Sălaj', 'lifterlms' ),\n\t\t'SM' => __( 'Satu Mare', 'lifterlms' ),\n\t\t'SB' => __( 'Sibiu', 'lifterlms' ),\n\t\t'SV' => __( 'Suceava', 'lifterlms' ),\n\t\t'TR' => __( 'Teleorman', 'lifterlms' ),\n\t\t'TM' => __( 'Timiș', 'lifterlms' ),\n\t\t'TL' => __( 'Tulcea', 'lifterlms' ),\n\t\t'VL' => __( 'Vâlcea', 'lifterlms' ),\n\t\t'VS' => __( 'Vaslui', 'lifterlms' ),\n\t\t'VN' => __( 'Vrancea', 'lifterlms' ),\n\t),\n\t'SN' => array( // Regions of Senegal. Ref: https://github.com/unicode-org/cldr/blob/release-42/common/subdivisions/en.xml#L4801.\n\t\t'SNDB' => __( 'Diourbel', 'lifterlms' ),\n\t\t'SNDK' => __( 'Dakar', 'lifterlms' ),\n\t\t'SNFK' => __( 'Fatick', 'lifterlms' ),\n\t\t'SNKA' => __( 'Kaffrine', 'lifterlms' ),\n\t\t'SNKD' => __( 'Kolda', 'lifterlms' ),\n\t\t'SNKE' => __( 'Kédougou', 'lifterlms' ),\n\t\t'SNKL' => __( 'Kaolack', 'lifterlms' ),\n\t\t'SNLG' => __( 'Louga', 'lifterlms' ),\n\t\t'SNMT' => __( 'Matam', 'lifterlms' ),\n\t\t'SNSE' => __( 'Sédhiou', 'lifterlms' ),\n\t\t'SNSL' => __( 'Saint-Louis', 'lifterlms' ),\n\t\t'SNTC' => __( 'Tambacounda', 'lifterlms' ),\n\t\t'SNTH' => __( 'Thiès', 'lifterlms' ),\n\t\t'SNZG' => __( 'Ziguinchor', 'lifterlms' ),\n\t),\n\t'SG' => array(),\n\t'SK' => array(),\n\t'SI' => array(),\n\t'SV' => array( // Salvadoran states.\n\t\t'SV-AH' => __( 'Ahuachapán', 'lifterlms' ),\n\t\t'SV-CA' => __( 'Cabañas', 'lifterlms' ),\n\t\t'SV-CH' => __( 'Chalatenango', 'lifterlms' ),\n\t\t'SV-CU' => __( 'Cuscatlán', 'lifterlms' ),\n\t\t'SV-LI' => __( 'La Libertad', 'lifterlms' ),\n\t\t'SV-MO' => __( 'Morazán', 'lifterlms' ),\n\t\t'SV-PA' => __( 'La Paz', 'lifterlms' ),\n\t\t'SV-SA' => __( 'Santa Ana', 'lifterlms' ),\n\t\t'SV-SM' => __( 'San Miguel', 'lifterlms' ),\n\t\t'SV-SO' => __( 'Sonsonate', 'lifterlms' ),\n\t\t'SV-SS' => __( 'San Salvador', 'lifterlms' ),\n\t\t'SV-SV' => __( 'San Vicente', 'lifterlms' ),\n\t\t'SV-UN' => __( 'La Unión', 'lifterlms' ),\n\t\t'SV-US' => __( 'Usulután', 'lifterlms' ),\n\t),\n\t'TH' => array( // Thai states.\n\t\t'TH-37' => __( 'Amnat Charoen', 'lifterlms' ),\n\t\t'TH-15' => __( 'Ang Thong', 'lifterlms' ),\n\t\t'TH-14' => __( 'Ayutthaya', 'lifterlms' ),\n\t\t'TH-10' => __( 'Bangkok', 'lifterlms' ),\n\t\t'TH-38' => __( 'Bueng Kan', 'lifterlms' ),\n\t\t'TH-31' => __( 'Buri Ram', 'lifterlms' ),\n\t\t'TH-24' => __( 'Chachoengsao', 'lifterlms' ),\n\t\t'TH-18' => __( 'Chai Nat', 'lifterlms' ),\n\t\t'TH-36' => __( 'Chaiyaphum', 'lifterlms' ),\n\t\t'TH-22' => __( 'Chanthaburi', 'lifterlms' ),\n\t\t'TH-50' => __( 'Chiang Mai', 'lifterlms' ),\n\t\t'TH-57' => __( 'Chiang Rai', 'lifterlms' ),\n\t\t'TH-20' => __( 'Chonburi', 'lifterlms' ),\n\t\t'TH-86' => __( 'Chumphon', 'lifterlms' ),\n\t\t'TH-46' => __( 'Kalasin', 'lifterlms' ),\n\t\t'TH-62' => __( 'Kamphaeng Phet', 'lifterlms' ),\n\t\t'TH-71' => __( 'Kanchanaburi', 'lifterlms' ),\n\t\t'TH-40' => __( 'Khon Kaen', 'lifterlms' ),\n\t\t'TH-81' => __( 'Krabi', 'lifterlms' ),\n\t\t'TH-52' => __( 'Lampang', 'lifterlms' ),\n\t\t'TH-51' => __( 'Lamphun', 'lifterlms' ),\n\t\t'TH-42' => __( 'Loei', 'lifterlms' ),\n\t\t'TH-16' => __( 'Lopburi', 'lifterlms' ),\n\t\t'TH-58' => __( 'Mae Hong Son', 'lifterlms' ),\n\t\t'TH-44' => __( 'Maha Sarakham', 'lifterlms' ),\n\t\t'TH-49' => __( 'Mukdahan', 'lifterlms' ),\n\t\t'TH-26' => __( 'Nakhon Nayok', 'lifterlms' ),\n\t\t'TH-73' => __( 'Nakhon Pathom', 'lifterlms' ),\n\t\t'TH-48' => __( 'Nakhon Phanom', 'lifterlms' ),\n\t\t'TH-30' => __( 'Nakhon Ratchasima', 'lifterlms' ),\n\t\t'TH-60' => __( 'Nakhon Sawan', 'lifterlms' ),\n\t\t'TH-80' => __( 'Nakhon Si Thammarat', 'lifterlms' ),\n\t\t'TH-55' => __( 'Nan', 'lifterlms' ),\n\t\t'TH-96' => __( 'Narathiwat', 'lifterlms' ),\n\t\t'TH-39' => __( 'Nong Bua Lam Phu', 'lifterlms' ),\n\t\t'TH-43' => __( 'Nong Khai', 'lifterlms' ),\n\t\t'TH-12' => __( 'Nonthaburi', 'lifterlms' ),\n\t\t'TH-13' => __( 'Pathum Thani', 'lifterlms' ),\n\t\t'TH-94' => __( 'Pattani', 'lifterlms' ),\n\t\t'TH-82' => __( 'Phang Nga', 'lifterlms' ),\n\t\t'TH-93' => __( 'Phatthalung', 'lifterlms' ),\n\t\t'TH-56' => __( 'Phayao', 'lifterlms' ),\n\t\t'TH-67' => __( 'Phetchabun', 'lifterlms' ),\n\t\t'TH-76' => __( 'Phetchaburi', 'lifterlms' ),\n\t\t'TH-66' => __( 'Phichit', 'lifterlms' ),\n\t\t'TH-65' => __( 'Phitsanulok', 'lifterlms' ),\n\t\t'TH-54' => __( 'Phrae', 'lifterlms' ),\n\t\t'TH-83' => __( 'Phuket', 'lifterlms' ),\n\t\t'TH-25' => __( 'Prachin Buri', 'lifterlms' ),\n\t\t'TH-77' => __( 'Prachuap Khiri Khan', 'lifterlms' ),\n\t\t'TH-85' => __( 'Ranong', 'lifterlms' ),\n\t\t'TH-70' => __( 'Ratchaburi', 'lifterlms' ),\n\t\t'TH-21' => __( 'Rayong', 'lifterlms' ),\n\t\t'TH-45' => __( 'Roi Et', 'lifterlms' ),\n\t\t'TH-27' => __( 'Sa Kaeo', 'lifterlms' ),\n\t\t'TH-47' => __( 'Sakon Nakhon', 'lifterlms' ),\n\t\t'TH-11' => __( 'Samut Prakan', 'lifterlms' ),\n\t\t'TH-74' => __( 'Samut Sakhon', 'lifterlms' ),\n\t\t'TH-75' => __( 'Samut Songkhram', 'lifterlms' ),\n\t\t'TH-19' => __( 'Saraburi', 'lifterlms' ),\n\t\t'TH-91' => __( 'Satun', 'lifterlms' ),\n\t\t'TH-17' => __( 'Sing Buri', 'lifterlms' ),\n\t\t'TH-33' => __( 'Sisaket', 'lifterlms' ),\n\t\t'TH-90' => __( 'Songkhla', 'lifterlms' ),\n\t\t'TH-64' => __( 'Sukhothai', 'lifterlms' ),\n\t\t'TH-72' => __( 'Suphan Buri', 'lifterlms' ),\n\t\t'TH-84' => __( 'Surat Thani', 'lifterlms' ),\n\t\t'TH-32' => __( 'Surin', 'lifterlms' ),\n\t\t'TH-63' => __( 'Tak', 'lifterlms' ),\n\t\t'TH-92' => __( 'Trang', 'lifterlms' ),\n\t\t'TH-23' => __( 'Trat', 'lifterlms' ),\n\t\t'TH-34' => __( 'Ubon Ratchathani', 'lifterlms' ),\n\t\t'TH-41' => __( 'Udon Thani', 'lifterlms' ),\n\t\t'TH-61' => __( 'Uthai Thani', 'lifterlms' ),\n\t\t'TH-53' => __( 'Uttaradit', 'lifterlms' ),\n\t\t'TH-95' => __( 'Yala', 'lifterlms' ),\n\t\t'TH-35' => __( 'Yasothon', 'lifterlms' ),\n\t),\n\t'TR' => array( // Turkish states.\n\t\t'TR01' => __( 'Adana', 'lifterlms' ),\n\t\t'TR02' => __( 'Adıyaman', 'lifterlms' ),\n\t\t'TR03' => __( 'Afyon', 'lifterlms' ),\n\t\t'TR04' => __( 'Ağrı', 'lifterlms' ),\n\t\t'TR05' => __( 'Amasya', 'lifterlms' ),\n\t\t'TR06' => __( 'Ankara', 'lifterlms' ),\n\t\t'TR07' => __( 'Antalya', 'lifterlms' ),\n\t\t'TR08' => __( 'Artvin', 'lifterlms' ),\n\t\t'TR09' => __( 'Aydın', 'lifterlms' ),\n\t\t'TR10' => __( 'Balıkesir', 'lifterlms' ),\n\t\t'TR11' => __( 'Bilecik', 'lifterlms' ),\n\t\t'TR12' => __( 'Bingöl', 'lifterlms' ),\n\t\t'TR13' => __( 'Bitlis', 'lifterlms' ),\n\t\t'TR14' => __( 'Bolu', 'lifterlms' ),\n\t\t'TR15' => __( 'Burdur', 'lifterlms' ),\n\t\t'TR16' => __( 'Bursa', 'lifterlms' ),\n\t\t'TR17' => __( 'Çanakkale', 'lifterlms' ),\n\t\t'TR18' => __( 'Çankırı', 'lifterlms' ),\n\t\t'TR19' => __( 'Çorum', 'lifterlms' ),\n\t\t'TR20' => __( 'Denizli', 'lifterlms' ),\n\t\t'TR21' => __( 'Diyarbakır', 'lifterlms' ),\n\t\t'TR22' => __( 'Edirne', 'lifterlms' ),\n\t\t'TR23' => __( 'Elazığ', 'lifterlms' ),\n\t\t'TR24' => __( 'Erzincan', 'lifterlms' ),\n\t\t'TR25' => __( 'Erzurum', 'lifterlms' ),\n\t\t'TR26' => __( 'Eskişehir', 'lifterlms' ),\n\t\t'TR27' => __( 'Gaziantep', 'lifterlms' ),\n\t\t'TR28' => __( 'Giresun', 'lifterlms' ),\n\t\t'TR29' => __( 'Gümüşhane', 'lifterlms' ),\n\t\t'TR30' => __( 'Hakkari', 'lifterlms' ),\n\t\t'TR31' => __( 'Hatay', 'lifterlms' ),\n\t\t'TR32' => __( 'Isparta', 'lifterlms' ),\n\t\t'TR33' => __( 'İçel', 'lifterlms' ),\n\t\t'TR34' => __( 'İstanbul', 'lifterlms' ),\n\t\t'TR35' => __( 'İzmir', 'lifterlms' ),\n\t\t'TR36' => __( 'Kars', 'lifterlms' ),\n\t\t'TR37' => __( 'Kastamonu', 'lifterlms' ),\n\t\t'TR38' => __( 'Kayseri', 'lifterlms' ),\n\t\t'TR39' => __( 'Kırklareli', 'lifterlms' ),\n\t\t'TR40' => __( 'Kırşehir', 'lifterlms' ),\n\t\t'TR41' => __( 'Kocaeli', 'lifterlms' ),\n\t\t'TR42' => __( 'Konya', 'lifterlms' ),\n\t\t'TR43' => __( 'Kütahya', 'lifterlms' ),\n\t\t'TR44' => __( 'Malatya', 'lifterlms' ),\n\t\t'TR45' => __( 'Manisa', 'lifterlms' ),\n\t\t'TR46' => __( 'Kahramanmaraş', 'lifterlms' ),\n\t\t'TR47' => __( 'Mardin', 'lifterlms' ),\n\t\t'TR48' => __( 'Muğla', 'lifterlms' ),\n\t\t'TR49' => __( 'Muş', 'lifterlms' ),\n\t\t'TR50' => __( 'Nevşehir', 'lifterlms' ),\n\t\t'TR51' => __( 'Niğde', 'lifterlms' ),\n\t\t'TR52' => __( 'Ordu', 'lifterlms' ),\n\t\t'TR53' => __( 'Rize', 'lifterlms' ),\n\t\t'TR54' => __( 'Sakarya', 'lifterlms' ),\n\t\t'TR55' => __( 'Samsun', 'lifterlms' ),\n\t\t'TR56' => __( 'Siirt', 'lifterlms' ),\n\t\t'TR57' => __( 'Sinop', 'lifterlms' ),\n\t\t'TR58' => __( 'Sivas', 'lifterlms' ),\n\t\t'TR59' => __( 'Tekirdağ', 'lifterlms' ),\n\t\t'TR60' => __( 'Tokat', 'lifterlms' ),\n\t\t'TR61' => __( 'Trabzon', 'lifterlms' ),\n\t\t'TR62' => __( 'Tunceli', 'lifterlms' ),\n\t\t'TR63' => __( 'Şanlıurfa', 'lifterlms' ),\n\t\t'TR64' => __( 'Uşak', 'lifterlms' ),\n\t\t'TR65' => __( 'Van', 'lifterlms' ),\n\t\t'TR66' => __( 'Yozgat', 'lifterlms' ),\n\t\t'TR67' => __( 'Zonguldak', 'lifterlms' ),\n\t\t'TR68' => __( 'Aksaray', 'lifterlms' ),\n\t\t'TR69' => __( 'Bayburt', 'lifterlms' ),\n\t\t'TR70' => __( 'Karaman', 'lifterlms' ),\n\t\t'TR71' => __( 'Kırıkkale', 'lifterlms' ),\n\t\t'TR72' => __( 'Batman', 'lifterlms' ),\n\t\t'TR73' => __( 'Şırnak', 'lifterlms' ),\n\t\t'TR74' => __( 'Bartın', 'lifterlms' ),\n\t\t'TR75' => __( 'Ardahan', 'lifterlms' ),\n\t\t'TR76' => __( 'Iğdır', 'lifterlms' ),\n\t\t'TR77' => __( 'Yalova', 'lifterlms' ),\n\t\t'TR78' => __( 'Karabük', 'lifterlms' ),\n\t\t'TR79' => __( 'Kilis', 'lifterlms' ),\n\t\t'TR80' => __( 'Osmaniye', 'lifterlms' ),\n\t\t'TR81' => __( 'Düzce', 'lifterlms' ),\n\t),\n\t'TZ' => array( // Tanzanian states.\n\t\t'TZ01' => __( 'Arusha', 'lifterlms' ),\n\t\t'TZ02' => __( 'Dar es Salaam', 'lifterlms' ),\n\t\t'TZ03' => __( 'Dodoma', 'lifterlms' ),\n\t\t'TZ04' => __( 'Iringa', 'lifterlms' ),\n\t\t'TZ05' => __( 'Kagera', 'lifterlms' ),\n\t\t'TZ06' => __( 'Pemba North', 'lifterlms' ),\n\t\t'TZ07' => __( 'Zanzibar North', 'lifterlms' ),\n\t\t'TZ08' => __( 'Kigoma', 'lifterlms' ),\n\t\t'TZ09' => __( 'Kilimanjaro', 'lifterlms' ),\n\t\t'TZ10' => __( 'Pemba South', 'lifterlms' ),\n\t\t'TZ11' => __( 'Zanzibar South', 'lifterlms' ),\n\t\t'TZ12' => __( 'Lindi', 'lifterlms' ),\n\t\t'TZ13' => __( 'Mara', 'lifterlms' ),\n\t\t'TZ14' => __( 'Mbeya', 'lifterlms' ),\n\t\t'TZ15' => __( 'Zanzibar West', 'lifterlms' ),\n\t\t'TZ16' => __( 'Morogoro', 'lifterlms' ),\n\t\t'TZ17' => __( 'Mtwara', 'lifterlms' ),\n\t\t'TZ18' => __( 'Mwanza', 'lifterlms' ),\n\t\t'TZ19' => __( 'Coast', 'lifterlms' ),\n\t\t'TZ20' => __( 'Rukwa', 'lifterlms' ),\n\t\t'TZ21' => __( 'Ruvuma', 'lifterlms' ),\n\t\t'TZ22' => __( 'Shinyanga', 'lifterlms' ),\n\t\t'TZ23' => __( 'Singida', 'lifterlms' ),\n\t\t'TZ24' => __( 'Tabora', 'lifterlms' ),\n\t\t'TZ25' => __( 'Tanga', 'lifterlms' ),\n\t\t'TZ26' => __( 'Manyara', 'lifterlms' ),\n\t\t'TZ27' => __( 'Geita', 'lifterlms' ),\n\t\t'TZ28' => __( 'Katavi', 'lifterlms' ),\n\t\t'TZ29' => __( 'Njombe', 'lifterlms' ),\n\t\t'TZ30' => __( 'Simiyu', 'lifterlms' ),\n\t),\n\t'LK' => array(),\n\t'RS' => array( // Serbian districts.\n\t\t'RS00' => _x( 'Belgrade', 'district', 'lifterlms' ),\n\t\t'RS14' => _x( 'Bor', 'district', 'lifterlms' ),\n\t\t'RS11' => _x( 'Braničevo', 'district', 'lifterlms' ),\n\t\t'RS02' => _x( 'Central Banat', 'district', 'lifterlms' ),\n\t\t'RS10' => _x( 'Danube', 'district', 'lifterlms' ),\n\t\t'RS23' => _x( 'Jablanica', 'district', 'lifterlms' ),\n\t\t'RS09' => _x( 'Kolubara', 'district', 'lifterlms' ),\n\t\t'RS08' => _x( 'Mačva', 'district', 'lifterlms' ),\n\t\t'RS17' => _x( 'Morava', 'district', 'lifterlms' ),\n\t\t'RS20' => _x( 'Nišava', 'district', 'lifterlms' ),\n\t\t'RS01' => _x( 'North Bačka', 'district', 'lifterlms' ),\n\t\t'RS03' => _x( 'North Banat', 'district', 'lifterlms' ),\n\t\t'RS24' => _x( 'Pčinja', 'district', 'lifterlms' ),\n\t\t'RS22' => _x( 'Pirot', 'district', 'lifterlms' ),\n\t\t'RS13' => _x( 'Pomoravlje', 'district', 'lifterlms' ),\n\t\t'RS19' => _x( 'Rasina', 'district', 'lifterlms' ),\n\t\t'RS18' => _x( 'Raška', 'district', 'lifterlms' ),\n\t\t'RS06' => _x( 'South Bačka', 'district', 'lifterlms' ),\n\t\t'RS04' => _x( 'South Banat', 'district', 'lifterlms' ),\n\t\t'RS07' => _x( 'Srem', 'district', 'lifterlms' ),\n\t\t'RS12' => _x( 'Šumadija', 'district', 'lifterlms' ),\n\t\t'RS21' => _x( 'Toplica', 'district', 'lifterlms' ),\n\t\t'RS05' => _x( 'West Bačka', 'district', 'lifterlms' ),\n\t\t'RS15' => _x( 'Zaječar', 'district', 'lifterlms' ),\n\t\t'RS16' => _x( 'Zlatibor', 'district', 'lifterlms' ),\n\t\t'RS25' => _x( 'Kosovo', 'district', 'lifterlms' ),\n\t\t'RS26' => _x( 'Peć', 'district', 'lifterlms' ),\n\t\t'RS27' => _x( 'Prizren', 'district', 'lifterlms' ),\n\t\t'RS28' => _x( 'Kosovska Mitrovica', 'district', 'lifterlms' ),\n\t\t'RS29' => _x( 'Kosovo-Pomoravlje', 'district', 'lifterlms' ),\n\t\t'RSKM' => _x( 'Kosovo-Metohija', 'district', 'lifterlms' ),\n\t\t'RSVO' => _x( 'Vojvodina', 'district', 'lifterlms' ),\n\t),\n\t'RW' => array(),\n\t'SE' => array(),\n\t'UA' => array( // Ukrainian oblasts. https://github.com/unicode-org/cldr/blob/release-42/common/subdivisions/en.xml#L5243.\n\t\t'UA05' => __( 'Vinnychchyna', 'lifterlms' ),\n\t\t'UA07' => __( 'Volyn', 'lifterlms' ),\n\t\t'UA09' => __( 'Luhanshchyna', 'lifterlms' ),\n\t\t'UA12' => __( 'Dnipropetrovshchyna', 'lifterlms' ),\n\t\t'UA14' => __( 'Donechchyna', 'lifterlms' ),\n\t\t'UA18' => __( 'Zhytomyrshchyna', 'lifterlms' ),\n\t\t'UA21' => __( 'Zakarpattia', 'lifterlms' ),\n\t\t'UA23' => __( 'Zaporizhzhya', 'lifterlms' ),\n\t\t'UA26' => __( 'Prykarpattia', 'lifterlms' ),\n\t\t'UA30' => __( 'Kyiv', 'lifterlms' ),\n\t\t'UA32' => __( 'Kyivshchyna', 'lifterlms' ),\n\t\t'UA35' => __( 'Kirovohradschyna', 'lifterlms' ),\n\t\t'UA40' => __( 'Sevastopol', 'lifterlms' ),\n\t\t'UA43' => __( 'Crimea', 'lifterlms' ),\n\t\t'UA46' => __( 'Lvivshchyna', 'lifterlms' ),\n\t\t'UA48' => __( 'Mykolayivschyna', 'lifterlms' ),\n\t\t'UA51' => __( 'Odeshchyna', 'lifterlms' ),\n\t\t'UA53' => __( 'Poltavshchyna', 'lifterlms' ),\n\t\t'UA56' => __( 'Rivnenshchyna', 'lifterlms' ),\n\t\t'UA59' => __( 'Sumshchyna', 'lifterlms' ),\n\t\t'UA61' => __( 'Ternopilshchyna', 'lifterlms' ),\n\t\t'UA63' => __( 'Kharkivshchyna', 'lifterlms' ),\n\t\t'UA65' => __( 'Khersonshchyna', 'lifterlms' ),\n\t\t'UA68' => __( 'Khmelnychchyna', 'lifterlms' ),\n\t\t'UA71' => __( 'Cherkashchyna', 'lifterlms' ),\n\t\t'UA74' => __( 'Chernihivshchyna', 'lifterlms' ),\n\t\t'UA77' => __( 'Chernivtsi Oblast', 'lifterlms' ),\n\t),\n\t'UG' => array( // Ugandan districts.\n\t\t'UG314' => __( 'Abim', 'lifterlms' ),\n\t\t'UG301' => __( 'Adjumani', 'lifterlms' ),\n\t\t'UG322' => __( 'Agago', 'lifterlms' ),\n\t\t'UG323' => __( 'Alebtong', 'lifterlms' ),\n\t\t'UG315' => __( 'Amolatar', 'lifterlms' ),\n\t\t'UG324' => __( 'Amudat', 'lifterlms' ),\n\t\t'UG216' => __( 'Amuria', 'lifterlms' ),\n\t\t'UG316' => __( 'Amuru', 'lifterlms' ),\n\t\t'UG302' => __( 'Apac', 'lifterlms' ),\n\t\t'UG303' => __( 'Arua', 'lifterlms' ),\n\t\t'UG217' => __( 'Budaka', 'lifterlms' ),\n\t\t'UG218' => __( 'Bududa', 'lifterlms' ),\n\t\t'UG201' => __( 'Bugiri', 'lifterlms' ),\n\t\t'UG235' => __( 'Bugweri', 'lifterlms' ),\n\t\t'UG420' => __( 'Buhweju', 'lifterlms' ),\n\t\t'UG117' => __( 'Buikwe', 'lifterlms' ),\n\t\t'UG219' => __( 'Bukedea', 'lifterlms' ),\n\t\t'UG118' => __( 'Bukomansimbi', 'lifterlms' ),\n\t\t'UG220' => __( 'Bukwa', 'lifterlms' ),\n\t\t'UG225' => __( 'Bulambuli', 'lifterlms' ),\n\t\t'UG416' => __( 'Buliisa', 'lifterlms' ),\n\t\t'UG401' => __( 'Bundibugyo', 'lifterlms' ),\n\t\t'UG430' => __( 'Bunyangabu', 'lifterlms' ),\n\t\t'UG402' => __( 'Bushenyi', 'lifterlms' ),\n\t\t'UG202' => __( 'Busia', 'lifterlms' ),\n\t\t'UG221' => __( 'Butaleja', 'lifterlms' ),\n\t\t'UG119' => __( 'Butambala', 'lifterlms' ),\n\t\t'UG233' => __( 'Butebo', 'lifterlms' ),\n\t\t'UG120' => __( 'Buvuma', 'lifterlms' ),\n\t\t'UG226' => __( 'Buyende', 'lifterlms' ),\n\t\t'UG317' => __( 'Dokolo', 'lifterlms' ),\n\t\t'UG121' => __( 'Gomba', 'lifterlms' ),\n\t\t'UG304' => __( 'Gulu', 'lifterlms' ),\n\t\t'UG403' => __( 'Hoima', 'lifterlms' ),\n\t\t'UG417' => __( 'Ibanda', 'lifterlms' ),\n\t\t'UG203' => __( 'Iganga', 'lifterlms' ),\n\t\t'UG418' => __( 'Isingiro', 'lifterlms' ),\n\t\t'UG204' => __( 'Jinja', 'lifterlms' ),\n\t\t'UG318' => __( 'Kaabong', 'lifterlms' ),\n\t\t'UG404' => __( 'Kabale', 'lifterlms' ),\n\t\t'UG405' => __( 'Kabarole', 'lifterlms' ),\n\t\t'UG213' => __( 'Kaberamaido', 'lifterlms' ),\n\t\t'UG427' => __( 'Kagadi', 'lifterlms' ),\n\t\t'UG428' => __( 'Kakumiro', 'lifterlms' ),\n\t\t'UG101' => __( 'Kalangala', 'lifterlms' ),\n\t\t'UG222' => __( 'Kaliro', 'lifterlms' ),\n\t\t'UG122' => __( 'Kalungu', 'lifterlms' ),\n\t\t'UG102' => __( 'Kampala', 'lifterlms' ),\n\t\t'UG205' => __( 'Kamuli', 'lifterlms' ),\n\t\t'UG413' => __( 'Kamwenge', 'lifterlms' ),\n\t\t'UG414' => __( 'Kanungu', 'lifterlms' ),\n\t\t'UG206' => __( 'Kapchorwa', 'lifterlms' ),\n\t\t'UG236' => __( 'Kapelebyong', 'lifterlms' ),\n\t\t'UG126' => __( 'Kasanda', 'lifterlms' ),\n\t\t'UG406' => __( 'Kasese', 'lifterlms' ),\n\t\t'UG207' => __( 'Katakwi', 'lifterlms' ),\n\t\t'UG112' => __( 'Kayunga', 'lifterlms' ),\n\t\t'UG407' => __( 'Kibaale', 'lifterlms' ),\n\t\t'UG103' => __( 'Kiboga', 'lifterlms' ),\n\t\t'UG227' => __( 'Kibuku', 'lifterlms' ),\n\t\t'UG432' => __( 'Kikuube', 'lifterlms' ),\n\t\t'UG419' => __( 'Kiruhura', 'lifterlms' ),\n\t\t'UG421' => __( 'Kiryandongo', 'lifterlms' ),\n\t\t'UG408' => __( 'Kisoro', 'lifterlms' ),\n\t\t'UG305' => __( 'Kitgum', 'lifterlms' ),\n\t\t'UG319' => __( 'Koboko', 'lifterlms' ),\n\t\t'UG325' => __( 'Kole', 'lifterlms' ),\n\t\t'UG306' => __( 'Kotido', 'lifterlms' ),\n\t\t'UG208' => __( 'Kumi', 'lifterlms' ),\n\t\t'UG333' => __( 'Kwania', 'lifterlms' ),\n\t\t'UG228' => __( 'Kween', 'lifterlms' ),\n\t\t'UG123' => __( 'Kyankwanzi', 'lifterlms' ),\n\t\t'UG422' => __( 'Kyegegwa', 'lifterlms' ),\n\t\t'UG415' => __( 'Kyenjojo', 'lifterlms' ),\n\t\t'UG125' => __( 'Kyotera', 'lifterlms' ),\n\t\t'UG326' => __( 'Lamwo', 'lifterlms' ),\n\t\t'UG307' => __( 'Lira', 'lifterlms' ),\n\t\t'UG229' => __( 'Luuka', 'lifterlms' ),\n\t\t'UG104' => __( 'Luwero', 'lifterlms' ),\n\t\t'UG124' => __( 'Lwengo', 'lifterlms' ),\n\t\t'UG114' => __( 'Lyantonde', 'lifterlms' ),\n\t\t'UG223' => __( 'Manafwa', 'lifterlms' ),\n\t\t'UG320' => __( 'Maracha', 'lifterlms' ),\n\t\t'UG105' => __( 'Masaka', 'lifterlms' ),\n\t\t'UG409' => __( 'Masindi', 'lifterlms' ),\n\t\t'UG214' => __( 'Mayuge', 'lifterlms' ),\n\t\t'UG209' => __( 'Mbale', 'lifterlms' ),\n\t\t'UG410' => __( 'Mbarara', 'lifterlms' ),\n\t\t'UG423' => __( 'Mitooma', 'lifterlms' ),\n\t\t'UG115' => __( 'Mityana', 'lifterlms' ),\n\t\t'UG308' => __( 'Moroto', 'lifterlms' ),\n\t\t'UG309' => __( 'Moyo', 'lifterlms' ),\n\t\t'UG106' => __( 'Mpigi', 'lifterlms' ),\n\t\t'UG107' => __( 'Mubende', 'lifterlms' ),\n\t\t'UG108' => __( 'Mukono', 'lifterlms' ),\n\t\t'UG334' => __( 'Nabilatuk', 'lifterlms' ),\n\t\t'UG311' => __( 'Nakapiripirit', 'lifterlms' ),\n\t\t'UG116' => __( 'Nakaseke', 'lifterlms' ),\n\t\t'UG109' => __( 'Nakasongola', 'lifterlms' ),\n\t\t'UG230' => __( 'Namayingo', 'lifterlms' ),\n\t\t'UG234' => __( 'Namisindwa', 'lifterlms' ),\n\t\t'UG224' => __( 'Namutumba', 'lifterlms' ),\n\t\t'UG327' => __( 'Napak', 'lifterlms' ),\n\t\t'UG310' => __( 'Nebbi', 'lifterlms' ),\n\t\t'UG231' => __( 'Ngora', 'lifterlms' ),\n\t\t'UG424' => __( 'Ntoroko', 'lifterlms' ),\n\t\t'UG411' => __( 'Ntungamo', 'lifterlms' ),\n\t\t'UG328' => __( 'Nwoya', 'lifterlms' ),\n\t\t'UG331' => __( 'Omoro', 'lifterlms' ),\n\t\t'UG329' => __( 'Otuke', 'lifterlms' ),\n\t\t'UG321' => __( 'Oyam', 'lifterlms' ),\n\t\t'UG312' => __( 'Pader', 'lifterlms' ),\n\t\t'UG332' => __( 'Pakwach', 'lifterlms' ),\n\t\t'UG210' => __( 'Pallisa', 'lifterlms' ),\n\t\t'UG110' => __( 'Rakai', 'lifterlms' ),\n\t\t'UG429' => __( 'Rubanda', 'lifterlms' ),\n\t\t'UG425' => __( 'Rubirizi', 'lifterlms' ),\n\t\t'UG431' => __( 'Rukiga', 'lifterlms' ),\n\t\t'UG412' => __( 'Rukungiri', 'lifterlms' ),\n\t\t'UG111' => __( 'Sembabule', 'lifterlms' ),\n\t\t'UG232' => __( 'Serere', 'lifterlms' ),\n\t\t'UG426' => __( 'Sheema', 'lifterlms' ),\n\t\t'UG215' => __( 'Sironko', 'lifterlms' ),\n\t\t'UG211' => __( 'Soroti', 'lifterlms' ),\n\t\t'UG212' => __( 'Tororo', 'lifterlms' ),\n\t\t'UG113' => __( 'Wakiso', 'lifterlms' ),\n\t\t'UG313' => __( 'Yumbe', 'lifterlms' ),\n\t\t'UG330' => __( 'Zombo', 'lifterlms' ),\n\t),\n\t'UM' => array(\n\t\t'81' => __( 'Baker Island', 'lifterlms' ),\n\t\t'84' => __( 'Howland Island', 'lifterlms' ),\n\t\t'86' => __( 'Jarvis Island', 'lifterlms' ),\n\t\t'67' => __( 'Johnston Atoll', 'lifterlms' ),\n\t\t'89' => __( 'Kingman Reef', 'lifterlms' ),\n\t\t'71' => __( 'Midway Atoll', 'lifterlms' ),\n\t\t'76' => __( 'Navassa Island', 'lifterlms' ),\n\t\t'95' => __( 'Palmyra Atoll', 'lifterlms' ),\n\t\t'79' => __( 'Wake Island', 'lifterlms' ),\n\t),\n\t'US' => array( // U.S. states.\n\t\t'AL' => __( 'Alabama', 'lifterlms' ),\n\t\t'AK' => __( 'Alaska', 'lifterlms' ),\n\t\t'AZ' => __( 'Arizona', 'lifterlms' ),\n\t\t'AR' => __( 'Arkansas', 'lifterlms' ),\n\t\t'CA' => __( 'California', 'lifterlms' ),\n\t\t'CO' => __( 'Colorado', 'lifterlms' ),\n\t\t'CT' => __( 'Connecticut', 'lifterlms' ),\n\t\t'DE' => __( 'Delaware', 'lifterlms' ),\n\t\t'DC' => __( 'District of Columbia', 'lifterlms' ),\n\t\t'FL' => __( 'Florida', 'lifterlms' ),\n\t\t'GA' => _x( 'Georgia', 'US state of Georgia', 'lifterlms' ),\n\t\t'HI' => __( 'Hawaii', 'lifterlms' ),\n\t\t'ID' => __( 'Idaho', 'lifterlms' ),\n\t\t'IL' => __( 'Illinois', 'lifterlms' ),\n\t\t'IN' => __( 'Indiana', 'lifterlms' ),\n\t\t'IA' => __( 'Iowa', 'lifterlms' ),\n\t\t'KS' => __( 'Kansas', 'lifterlms' ),\n\t\t'KY' => __( 'Kentucky', 'lifterlms' ),\n\t\t'LA' => __( 'Louisiana', 'lifterlms' ),\n\t\t'ME' => __( 'Maine', 'lifterlms' ),\n\t\t'MD' => __( 'Maryland', 'lifterlms' ),\n\t\t'MA' => __( 'Massachusetts', 'lifterlms' ),\n\t\t'MI' => __( 'Michigan', 'lifterlms' ),\n\t\t'MN' => __( 'Minnesota', 'lifterlms' ),\n\t\t'MS' => __( 'Mississippi', 'lifterlms' ),\n\t\t'MO' => __( 'Missouri', 'lifterlms' ),\n\t\t'MT' => __( 'Montana', 'lifterlms' ),\n\t\t'NE' => __( 'Nebraska', 'lifterlms' ),\n\t\t'NV' => __( 'Nevada', 'lifterlms' ),\n\t\t'NH' => __( 'New Hampshire', 'lifterlms' ),\n\t\t'NJ' => __( 'New Jersey', 'lifterlms' ),\n\t\t'NM' => __( 'New Mexico', 'lifterlms' ),\n\t\t'NY' => __( 'New York', 'lifterlms' ),\n\t\t'NC' => __( 'North Carolina', 'lifterlms' ),\n\t\t'ND' => __( 'North Dakota', 'lifterlms' ),\n\t\t'OH' => __( 'Ohio', 'lifterlms' ),\n\t\t'OK' => __( 'Oklahoma', 'lifterlms' ),\n\t\t'OR' => __( 'Oregon', 'lifterlms' ),\n\t\t'PA' => __( 'Pennsylvania', 'lifterlms' ),\n\t\t'RI' => __( 'Rhode Island', 'lifterlms' ),\n\t\t'SC' => __( 'South Carolina', 'lifterlms' ),\n\t\t'SD' => __( 'South Dakota', 'lifterlms' ),\n\t\t'TN' => __( 'Tennessee', 'lifterlms' ),\n\t\t'TX' => __( 'Texas', 'lifterlms' ),\n\t\t'UT' => __( 'Utah', 'lifterlms' ),\n\t\t'VT' => __( 'Vermont', 'lifterlms' ),\n\t\t'VA' => __( 'Virginia', 'lifterlms' ),\n\t\t'WA' => __( 'Washington', 'lifterlms' ),\n\t\t'WV' => __( 'West Virginia', 'lifterlms' ),\n\t\t'WI' => __( 'Wisconsin', 'lifterlms' ),\n\t\t'WY' => __( 'Wyoming', 'lifterlms' ),\n\t\t'AA' => __( 'Armed Forces (AA)', 'lifterlms' ),\n\t\t'AE' => __( 'Armed Forces (AE)', 'lifterlms' ),\n\t\t'AP' => __( 'Armed Forces (AP)', 'lifterlms' ),\n\t),\n\t'UY' => array( // Uruguayan states.\n\t\t'UY-AR' => __( 'Artigas', 'lifterlms' ),\n\t\t'UY-CA' => __( 'Canelones', 'lifterlms' ),\n\t\t'UY-CL' => __( 'Cerro Largo', 'lifterlms' ),\n\t\t'UY-CO' => __( 'Colonia', 'lifterlms' ),\n\t\t'UY-DU' => __( 'Durazno', 'lifterlms' ),\n\t\t'UY-FS' => __( 'Flores', 'lifterlms' ),\n\t\t'UY-FD' => __( 'Florida', 'lifterlms' ),\n\t\t'UY-LA' => __( 'Lavalleja', 'lifterlms' ),\n\t\t'UY-MA' => __( 'Maldonado', 'lifterlms' ),\n\t\t'UY-MO' => __( 'Montevideo', 'lifterlms' ),\n\t\t'UY-PA' => __( 'Paysandú', 'lifterlms' ),\n\t\t'UY-RN' => __( 'Río Negro', 'lifterlms' ),\n\t\t'UY-RV' => __( 'Rivera', 'lifterlms' ),\n\t\t'UY-RO' => __( 'Rocha', 'lifterlms' ),\n\t\t'UY-SA' => __( 'Salto', 'lifterlms' ),\n\t\t'UY-SJ' => __( 'San José', 'lifterlms' ),\n\t\t'UY-SO' => __( 'Soriano', 'lifterlms' ),\n\t\t'UY-TA' => __( 'Tacuarembó', 'lifterlms' ),\n\t\t'UY-TT' => __( 'Treinta y Tres', 'lifterlms' ),\n\t),\n\t'VE' => array( // Venezuelan states.\n\t\t'VE-A' => __( 'Capital', 'lifterlms' ),\n\t\t'VE-B' => __( 'Anzoátegui', 'lifterlms' ),\n\t\t'VE-C' => __( 'Apure', 'lifterlms' ),\n\t\t'VE-D' => __( 'Aragua', 'lifterlms' ),\n\t\t'VE-E' => __( 'Barinas', 'lifterlms' ),\n\t\t'VE-F' => __( 'Bolívar', 'lifterlms' ),\n\t\t'VE-G' => __( 'Carabobo', 'lifterlms' ),\n\t\t'VE-H' => __( 'Cojedes', 'lifterlms' ),\n\t\t'VE-I' => __( 'Falcón', 'lifterlms' ),\n\t\t'VE-J' => __( 'Guárico', 'lifterlms' ),\n\t\t'VE-K' => __( 'Lara', 'lifterlms' ),\n\t\t'VE-L' => __( 'Mérida', 'lifterlms' ),\n\t\t'VE-M' => __( 'Miranda', 'lifterlms' ),\n\t\t'VE-N' => __( 'Monagas', 'lifterlms' ),\n\t\t'VE-O' => __( 'Nueva Esparta', 'lifterlms' ),\n\t\t'VE-P' => __( 'Portuguesa', 'lifterlms' ),\n\t\t'VE-R' => __( 'Sucre', 'lifterlms' ),\n\t\t'VE-S' => __( 'Táchira', 'lifterlms' ),\n\t\t'VE-T' => __( 'Trujillo', 'lifterlms' ),\n\t\t'VE-U' => __( 'Yaracuy', 'lifterlms' ),\n\t\t'VE-V' => __( 'Zulia', 'lifterlms' ),\n\t\t'VE-W' => __( 'Federal Dependencies', 'lifterlms' ),\n\t\t'VE-X' => __( 'La Guaira (Vargas)', 'lifterlms' ),\n\t\t'VE-Y' => __( 'Delta Amacuro', 'lifterlms' ),\n\t\t'VE-Z' => __( 'Amazonas', 'lifterlms' ),\n\t),\n\t'VN' => array(),\n\t'YT' => array(),\n\t'ZA' => array( // South African states.\n\t\t'EC'  => __( 'Eastern Cape', 'lifterlms' ),\n\t\t'FS'  => __( 'Free State', 'lifterlms' ),\n\t\t'GP'  => __( 'Gauteng', 'lifterlms' ),\n\t\t'KZN' => __( 'KwaZulu-Natal', 'lifterlms' ),\n\t\t'LP'  => __( 'Limpopo', 'lifterlms' ),\n\t\t'MP'  => __( 'Mpumalanga', 'lifterlms' ),\n\t\t'NC'  => __( 'Northern Cape', 'lifterlms' ),\n\t\t'NW'  => __( 'North West', 'lifterlms' ),\n\t\t'WC'  => __( 'Western Cape', 'lifterlms' ),\n\t),\n\t'ZM' => array( // Zambian provinces.\n\t\t'ZM-01' => __( 'Western', 'lifterlms' ),\n\t\t'ZM-02' => __( 'Central', 'lifterlms' ),\n\t\t'ZM-03' => __( 'Eastern', 'lifterlms' ),\n\t\t'ZM-04' => __( 'Luapula', 'lifterlms' ),\n\t\t'ZM-05' => __( 'Northern', 'lifterlms' ),\n\t\t'ZM-06' => __( 'North-Western', 'lifterlms' ),\n\t\t'ZM-07' => __( 'Southern', 'lifterlms' ),\n\t\t'ZM-08' => __( 'Copperbelt', 'lifterlms' ),\n\t\t'ZM-09' => __( 'Lusaka', 'lifterlms' ),\n\t\t'ZM-10' => __( 'Muchinga', 'lifterlms' ),\n\t),\n);\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"packages\": [\n    \"packages/*\"\n  ],\n  \"version\": \"independent\"\n}\n"
  },
  {
    "path": "libraries/README.md",
    "content": "External Libraries\n==================\n\nInstallation directory for plugin libraries included in the core plugin but developed outside of this repository.\n\nSee [Installing for Development](../docs/installing.md) for installation instructions.\n"
  },
  {
    "path": "libraries/index.php",
    "content": "<?php // Quiet.\n"
  },
  {
    "path": "lifterlms.php",
    "content": "<?php\n/**\n * Main LifterLMS plugin file\n *\n * @package LifterLMS/Main\n *\n * @since 1.0.0\n * @version 5.3.0\n *\n * Plugin Name: LifterLMS\n * Plugin URI: https://lifterlms.com/\n * Description: Complete e-learning platform to sell online courses, protect lessons, offer memberships, and quiz students. WP Learning Management System.\n * Version: 10.0.0\n * Author: LifterLMS\n * Author URI: https://lifterlms.com/\n * Text Domain: lifterlms\n * Domain Path: /languages\n * License: GPLv3\n * License URI: https://www.gnu.org/licenses/gpl-3.0.html\n * Requires at least: 5.9\n * Tested up to: 6.9\n * Requires PHP: 7.4\n *\n * * * * * * * * * * * * * * * * * * * * * *\n *                                         *\n * Reporting a Security Vulnerability      *\n *                                         *\n * Please disclose any security issues or  *\n * vulnerabilities to team@lifterlms.com   *\n *                                         *\n * See our full Security Policy at         *\n * https://lifterlms.com/security-policy   *\n *                                         *\n * * * * * * * * * * * * * * * * * * * * * *\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! defined( 'LLMS_PLUGIN_FILE' ) ) {\n\tdefine( 'LLMS_PLUGIN_FILE', __FILE__ );\n}\n\nif ( ! defined( 'LLMS_PLUGIN_DIR' ) ) {\n\tdefine( 'LLMS_PLUGIN_DIR', __DIR__ . '/' );\n}\n\n// Autoloader.\nrequire_once LLMS_PLUGIN_DIR . 'vendor/autoload.php';\nrequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-loader.php';\n\nif ( ! class_exists( 'LifterLMS' ) ) {\n\trequire_once LLMS_PLUGIN_DIR . 'class-lifterlms.php';\n}\n\nregister_activation_hook( __FILE__, array( 'LLMS_Install', 'install' ) );\n\nrequire_once LLMS_PLUGIN_DIR . 'includes/llms-notifications.php';\n\n/**\n * Returns the main instance of LifterLMS\n *\n * @since 4.0.0\n *\n * @return LifterLMS\n */\nfunction llms() {\n\treturn LifterLMS::instance();\n}\nreturn llms();\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"lifterlms\",\n  \"version\": \"10.0.0\",\n  \"description\": \"LifterLMS by codeBOX\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\"\n  },\n  \"author\": \"Team LifterLMS <team@lifterlms.com>\",\n  \"license\": \"GPL-3.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/issues\"\n  },\n  \"homepage\": \"https://lifterlms.com\",\n  \"engines\": {\n    \"node\": \">=16.0.0\",\n    \"npm\": \">=8.0.0\"\n  },\n  \"dependencies\": {\n    \"@emotion/styled\": \"^11.6.0\",\n    \"classnames\": \"^2.3.1\"\n  },\n  \"devDependencies\": {\n    \"@lifterlms/brand\": \"file:packages/brand\",\n    \"@lifterlms/components\": \"file:packages/components\",\n    \"@lifterlms/dev\": \"file:packages/dev\",\n    \"@lifterlms/fontawesome\": \"file:packages/fontawesome\",\n    \"@lifterlms/icons\": \"file:packages/icons\",\n    \"@lifterlms/llms-e2e-test-utils\": \"file:packages/llms-e2e-test-utils\",\n    \"@lifterlms/scripts\": \"file:packages/scripts\",\n    \"@lifterlms/utils\": \"file:packages/utils\",\n    \"@wordpress/docgen\": \"^1.18.0\",\n    \"gulp-header\": \"^2.0.9\",\n    \"gulp-replace\": \"^0.5.4\",\n    \"gulp-requirejs-optimize\": \"^1.2.0\",\n    \"gulp-uglify\": \"^3.0.2\",\n    \"lerna\": \"^5.5.2\",\n    \"lifterlms-lib-tasks\": \"^4.0.1\",\n    \"postcss\": \"^8.4.6\"\n  },\n  \"scripts\": {\n    \"clean\": \"rm -f assets/js/*.min.js assets/js/*.asset.php assets/css/*.map assets/js/*.map assets/maps/*.map assets/css/*.map\",\n    \"build\": \"npm run clean && npm run build:scripts && npm run build:scripts:legacy && npm run build:styles && npm run build:pot && llms-dev readme\",\n    \"build:pot\": \"gulp pot-js && llms-dev pot\",\n    \"build:scripts\": \"wp-scripts build\",\n    \"build:scripts:legacy\": \"gulp scripts && gulp js-additional && gulp js-builder\",\n    \"build:styles\": \"gulp styles && gulp styles-rtl && gulp hacky-clean\",\n    \"build:blocks\": \"wp-scripts build --config packages/scripts/config/blocks-webpack.config.js\",\n    \"dev\": \"llms-dev\",\n    \"lerna\": \"lerna\",\n    \"lint:js\": \"wp-scripts lint-js ./src/js ./src/blocks\",\n    \"test\": \"wp-scripts test-e2e --config packages/scripts/e2e/jest.config.js\",\n    \"test:dev\": \"npm run test -- --puppeteer-interactive\",\n    \"test:unit\": \"wp-scripts test-unit-js ./src/js --config packages/scripts/config/jest-unit.config.js --verbose\",\n    \"pkg:docgen\": \"lerna run docgen\",\n    \"pkg:hoist\": \"lerna bootstrap --hoist\",\n    \"pkg:lint:js\": \"lerna run lint:js\",\n    \"pkg:test\": \"wp-scripts test-unit-js ./packages --config packages/scripts/config/jest-unit.config.js --verbose\",\n    \"postinstall\": \"npm run pkg:hoist\",\n    \"start\": \"wp-scripts start\",\n    \"start:legacy\": \"gulp watch\",\n    \"start:blocks\": \"wp-scripts start --config packages/scripts/config/blocks-webpack.config.js\"\n  }\n}\n"
  },
  {
    "path": "packages/README.md",
    "content": "LifterLMS Javascript Packages\n=============================\n\nThis repository uses [lerna](https://lerna.js.org/) to manage LifterLMS modules and publish them as packages to [npm](https://www.npmjs.com/).\n\n---\n\n## Package Changelogs\n\nEach package is versioned independently and maintains its own changelog.\n\nWhen updating packages, an update to the changelog should be included outlining the changes. This should be added to the \"Unreleased\" heading at the top of the changelog file. If the heading doesn't already exist it should be added.\n\nAdditionally, methods and functions keep their own changelogs (like the LifterLMS core plugin) and `[version]` placeholders should be used for unreleased changes.\n\n\n## Inclusion in the LifterLMS Core Plugin\n\nSome packages are included in the LifterLMS Core Plugin. See package details for usage instructions. Each package that is included this way maintains a table outlining the package version included in various LifterLMS versions.\n\n\n## API Documentation\n\nWhere applicable, each package maintains its own independent API documentation which is published to the package's README.md file. The API documentation is automatically generated using the `docgen` script for each package.\n\n\n## Scripts\n\nMost packages should include at least a `docgen`, `lint:js`, and `test` script. These can be run within the package itself and in bulk via the associated `pkg:*` commands from with the root directory of the repository.\n\n\n## Publishing Releases\n\nReleases are published using the `npm run lerna publish` command from the repository root on the `trunk` branch.\n\n_Note: Packages which are included with the LifterLMS core should *always* be released alongside LifterLMS core releases._\n\nTo publish a release:\n\n+ Make sure you are on the trunk branch.\n+ Make sure you are logged into npmjs. Run `npm adduser` and follow the prompt.\n+ Run `npm run lerna changed` to see which packages have changes to be published.\n+ Ensure the `lint:js` and `test` scripts pass.\n+ Ensure CHANGELOG.md in each package directory has been updated to the appropriate version and the \"Unreleased\" header has been removed.\n+ Run `npm run dev update-version -- -F {version}` from each packages directory to update `[version]` placeholders to the appropriate version.\n+ Commit and push changes to git from the base lifterlms directory.\n+ Run `npm run lerna publish` and follow the prompts. (Alternatively, publish with `npm publish .` from each packages directory.\n\n"
  },
  {
    "path": "packages/brand/README.md",
    "content": "LifterLMS Brand\n===============\n\nLifterLMS brand icons, colors, and more.\n\n## Installation\n\nInstall the module\n\n```\nnpm install --save @lifterlms/brand\n```\n\n## Usage\n\n### SCSS Colors\n\nImport LifterLMS brand colors and WordPress core admin colors:\n\n_Note: Ensure that `node_modules` is included in your SASS load path!_\n\n```scss\n// Import all brand files.\n@import '@lifterlms/brand/sass/brand'\n\n// Import colors only.\n@import '@lifterlms/brand/sass/colors'\n\n// Import typography only.\n@import '@lifterlms/brand/sass/typography'\n\n// Use a color.\nbody {\n  background: llms-color( llms-blue );\n}\n\n// Use the gradient mixin.\n.banner {\n  @include llms-gradient-bg();\n}\n\n// Use a font.\nbody {\n  font-family: llms-font( llms-sans );\n}\n`\n"
  },
  {
    "path": "packages/brand/package.json",
    "content": "{\n  \"name\": \"@lifterlms/brand\",\n  \"version\": \"0.0.2\",\n  \"description\": \"LifterLMS brand icons, colors, and more.\",\n  \"author\": \"Team LifterLMS <team@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/master/packages/brand\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/brand\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%20brand\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "packages/brand/sass/brand.scss",
    "content": "@import 'colors';\n@import 'typography';\n"
  },
  {
    "path": "packages/brand/sass/colors.scss",
    "content": "// Color Map.\n$colors: (\n\n\t// LifterLMS Brand Colors.\n\tllms-blue: #466dd8,\n\tllms-blue-alt: #2295ff,\n\tllms-orange: #f8954f,\n\n\t// Standard colors.\n\twhite: #fff,\n\tblack: #000,\n\n\t// WP Colors.\n\t// @link https://make.wordpress.org/core/2021/02/23/standardization-of-wp-admin-colors-in-wordpress-5-7/\n\twp-gray-0: #f6f7f7,\n\twp-gray-2: #f0f0f1,\n\twp-gray-5: #dcdcde,\n\twp-gray-10: #c3c4c7,\n\twp-gray-20: #a7aaad,\n\twp-gray-30: #8c8f94,\n\twp-gray-40: #787c82,\n\twp-gray-50: #646970,\n\twp-gray-60: #50575e,\n\twp-gray-70: #3c434a,\n\twp-gray-80: #2c3338,\n\twp-gray-90: #1d2327,\n\twp-gray-100: #101517,\n\twp-blue-0: #f0f6fc,\n\twp-blue-5: #c5d9ed,\n\twp-blue-10: #9ec2e6,\n\twp-blue-20: #72aee6,\n\twp-blue-30: #4f94d4,\n\twp-blue-40: #3582c4,\n\twp-blue-50: #2271b1,\n\twp-blue-60: #135e96,\n\twp-blue-70: #0a4b78,\n\twp-blue-80: #043959,\n\twp-blue-90: #01263a,\n\twp-blue-100: #00131c,\n\twp-red-0: #fcf0f1,\n\twp-red-5: #facfd2,\n\twp-red-10: #ffabaf,\n\twp-red-20: #ff8085,\n\twp-red-30: #f86368,\n\twp-red-40: #e65054,\n\twp-red-50: #d63638,\n\twp-red-60: #b32d2e,\n\twp-red-70: #8a2424,\n\twp-red-80: #691c1c,\n\twp-red-90: #451313,\n\twp-red-100: #240a0a,\n\twp-yellow-0: #fcf9e8,\n\twp-yellow-5: #f5e6ab,\n\twp-yellow-10: #f2d675,\n\twp-yellow-20: #f0c33c,\n\twp-yellow-30: #dba617,\n\twp-yellow-40: #bd8600,\n\twp-yellow-50: #996800,\n\twp-yellow-60: #755100,\n\twp-yellow-70: #614200,\n\twp-yellow-80: #4a3200,\n\twp-yellow-90: #362400,\n\twp-yellow-100: #211600,\n\twp-green-0: #edfaef,\n\twp-green-5: #b8e6bf,\n\twp-green-10: #68de7c,\n\twp-green-20: #1ed14b,\n\twp-green-30: #00ba37,\n\twp-green-40: #00a32a,\n\twp-green-50: #008a20,\n\twp-green-60: #007017,\n\twp-green-70: #005c12,\n\twp-green-80: #00450c,\n\twp-green-90: #003008,\n\twp-green-100: #001c05\n\n);\n\n// Simple function to retreive colors in the $colors map.\n// e.g. `background-color: color( gray-50 );`\n@function llms-color( $key ) {\n  @if map-has-key( $colors, $key ) {\n    @return map-get( $colors, $key );\n  }\n\n  @warn \"Unknown `#{$key}` in $colors.\";\n  @return null;\n}\n\n\n@function llms-gradient() {\n\t@return linear-gradient( 45deg, llms-color( llms-blue-alt ), llms-color( llms-blue ) );\n}\n\n// Use the LifterLMS brand gradient as the background of an element.\n// e.g.: `@include llms-gradient-bg()`\n@mixin llms-gradient-bg() {\n\tbackground: llms-color( llms-blue );\n\tbackground: llms-gradient();\n}\n"
  },
  {
    "path": "packages/brand/sass/typography.scss",
    "content": "$fonts: (\n\tllms-sans: 'Montserrat, sans-serif',\n\tllms-mono: monospace,\n);\n\n@function llms-font( $key ) {\n  @if map-has-key( $fonts, $key ) {\n    @return map-get( $fonts, $key );\n  }\n\n  @warn \"Unknown `#{$key}` in $fonts.\";\n  @return null;\n}\n"
  },
  {
    "path": "packages/components/.llmsdev.yml",
    "content": "update-version:\n  skip-config: true\n"
  },
  {
    "path": "packages/components/.npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "packages/components/CHANGELOG.md",
    "content": "@lifterlms/components CHANGELOG\n===============================\n\nv1.1.0 - 2022-10-12\n-------------------\n\n+ Adds a spinner component intended to replace `LLMS.Spinner`.\n\n\nv1.0.0 - 2022-03-08\n-------------------\n\n+ Initial public release.\n"
  },
  {
    "path": "packages/components/README.md",
    "content": "# LifterLMS UI Components\n\nUI components for use in LifterLMS and LifterLMS add-ons. This package extends functionality provided by [@wordpress/components](https://github.com/WordPress/gutenberg/tree/master/packages/components), adding additional components specific to the LifterLMS project.\n\n## Usage in a LifterLMS add-on\n\nThis package is included in the LifterLMS core plugin as a module and registered using the WordPress scripts dependency API (see [wp_register_script()][https://developer.wordpress.org/reference/functions/wp_register_script/]).\n\nTo use components you can add `llms-components` as a dependency to your script and access the module via `window.llms.components`, for example:\n\n```js\nconst { ButtonGroupControl } = window.llms.components;\n```\n\n## LifterLMS Versions\n\nThe following table records the module version and which LifterLMS versions it has been included in.\n\n| Module Version | LifterLMS Version |\n| -------------- | ----------------- |\n| 1.0.0          | 6.0.0             |\n\n\n## Installation\n\nInstall the module as a dependency in your project:\n\n```bash\nnpm install --save-dev @lifterlms/components`\n```\n\n## Changelog\n\n[View the Changelog](./CHANGELOG.md)\n\n## API Docs\n\n<!-- START TOKEN(Autogenerated API docs) -->\n\n### BaseSearchControl\n\nSearchable <select> element powered by a WordPress REST API endpoint.\n\n_Parameters_\n\n-   _args_ `Object`: Component arguments.\n-   _args.searchPath_ `string`: Required. API path used to perform the search.\n-   _args.onUpdate_ `Function`: Callback function invoked when the value of the select changes. The callback function is passed a single parameter, the new selected value object(s). For multiselects it will be an array of objects. If the select is clearable, the value will be `null` when the select is cleared.\n-   _args.selectedValue_ `Array`: The currently selected value(s). If an object is passed, it should contain at least a `label` and `value` key. Can pass IDs as integers and the values will be automatically hydrated.\n-   _args.placeholder_ `string`: The placeholder displayed within an empty search control.\n-   _args.className_ `string`: HTML class attribute added to the select control.\n-   _args.classNamePrefix_ `string`: Prefix added to select control subcomponent classnames. In most circumstances this should not be changed as it is used to style the compontents.\n-   _args.searchDebounceDelay_ `number`: Search debounce delay, in milliseconds.\n-   _args.additionalSearchArgs_ `Object`: Object of additional query string arguments to use with the API request.\n-   _args.label_ `?string`: Search control label, passed to <BaseControl>.\n-   _args.id_ `?string`: Search control HTML ID attribute, passed to <BaseControl>.\n-   _args.defaultOptions_ `?Object[]`: Array of hydrated objects to preload into the select as options.\n-   _args.getSearchArgs_ `?Function`: Function invoked to generate the query string arguments used when fetching results from the API. The callback function is passed the search string.\n-   _args.getSearchURL_ `?Function`: Function invoked to create the search URL used to fetch results. The function is passed the `searchPath` and generated query string arguments from `getSearchArgs()`.\n-   _args.hydrateValues_ `?Function`: Function invoked to hydrate integer values. The function is passed the currently selected values, the `searchPath`, and an array of cached (and hydrated) objects previously loaded from the server.\n-   _args.formatSearchResults_ `?Function`: Function invoked to format results retrieved from the server. The function is passed an array of objects from the server. It should return an array of objects, each containing at least a value and label property.\n-   _args.formatSearchResultLabel_ `?Function`: Function invoked to format the display label for a result. The function is passed an object representing a single result and should return a string.\n-   _args.formatSearchResultValue_ `?Function`: Function invoked to format the saved value for a result. The function is passed an object representing a single result and should the value to be stored.\n-   _args.selectProps_ `...*`: Any remaining properties are passed to the <Select> component, {@link <https://react-select.com/props#select-props}>.\n\n_Returns_\n\n-   `StyledBaseControl`: The component.\n\n### ButtonGroupControl\n\nButton Group Control component\n\nSimilar to the experimental `<RadioGroup>` component from @wordpress/components but it allows\npassing in an array of options.\n\n_Related_\n\n-   BaseControl <https://github.com/WordPress/gutenberg/tree/trunk/packages/components/src/base-control>\n\n_Parameters_\n\n-   _props_ `Object`: Component properties object.\n-   _props.label_ `string`: Control label text.\n-   _props.className_ `string`: Control element css class name attribute.\n-   _props.id_ `string`: Control element ID attribute.\n-   _props.onClick_ `Function`: Callback function when a button in the group is clicked.\n-   _props.selected_ `string`: The value of the currently selected option.\n-   _props.options_ `Object[]`: An array of objects used to create the buttons in the group. Each object should contain at least a \"label\" and \"value\" property and can optionally include an \"icon\" property.\n\n_Returns_\n\n-   `BaseControl`: The rendered component.\n\n### CopyButton\n\nA \"click to copy\" button.\n\nUses the `useCopyToClipboard()` hook with a <Button> on WP 5.8 & later, otherwise falls back\nto the deprecated <ClipboardButton>.\n\n_Parameters_\n\n-   _args_ `Object`: Component arguments.\n-   _args.buttonText_ `string`: Text to to display within the button.\n-   _args.copyText_ `string`: Text to copy to the clipboard.\n-   _args.tooltipText_ `string`: Text to use in the tooltip wrapper around the button.\n-   _args.onCopy_ `Function`: Copy success callback function.\n-   _args.buttonProps_ `...*`: Remaining properties passed to the underlying <Button> component.\n\n_Returns_\n\n-   `Object`: The copy button fragment.\n\n### PostSearchControl\n\nSearchable <select> element powered by a WordPress REST API users endpoint.\n\nThis component is a wrapper around the <BaseSearchControl> component. It is configured\nto search users via the WordPress user REST API endpoint.\n\n_Parameters_\n\n-   _args_ `Object`: Component arguments.\n-   _args.postType_ `string`: Post type endpoint.\n-   _args.baseSearchPath_ `string`: Base search path used to create the searchPath.\n-   _args.searchPath_ `?string`: API path used to perform the search. If passed, will be used instead of the path generated from `args.postType` and `args.baseSearchPath`.\n-   _args.placeholder_ `string`: The placeholder displayed within an empty search control.\n-   _args.className_ `string`: HTML class attribute added to the select control.\n-   _args.formatSearchResultLabel_ `?Function`: Function invoked to format the display label for a result. The function is passed\n-   _args.additionalSearchArgs_ `Object`: An object representing a single result and should return a string.\n-   _args.baseProps_ `...*`: Any remaining properties are passed to the <BaseSearchControl> component.\n\n_Returns_\n\n-   `BaseSearchControl`: The component.\n\n### UserSearchControl\n\nSearchable <select> element powered by a WordPress REST API users endpoint.\n\nThis component is a wrapper around the <BaseSearchControl> component. It is configured\nto search users via the WordPress user REST API endpoint.\n\n_Parameters_\n\n-   _args_ `Object`: Component arguments.\n-   _args.searchPath_ `string`: Required. API path used to perform the search.\n-   _args.placeholder_ `string`: The placeholder displayed within an empty search control.\n-   _args.className_ `string`: HTML class attribute added to the select control.\n-   _args.formatSearchResultLabel_ `?Function`: Function invoked to format the display label for a result. The function is passed\n-   _args.additionalSearchArgs_ `Object`: An object representing a single result and should return a string.\n-   _args.baseProps_ `...*`: Any remaining properties are passed to the <BaseSearchControl> component.\n\n_Returns_\n\n-   `BaseSearchControl`: The component.\n\n\n<!-- END TOKEN(Autogenerated API docs) -->\n"
  },
  {
    "path": "packages/components/babel.config.js",
    "content": "/**\n * Babel config\n *\n * @package\n *\n * @since 1.0.0\n * @version 1.0.0\n */\n\nconst presets = [ '@wordpress/default' ];\n\nmodule.exports = { presets };\n"
  },
  {
    "path": "packages/components/package.json",
    "content": "{\n  \"name\": \"@lifterlms/components\",\n  \"version\": \"1.1.0\",\n  \"description\": \"UI components for LifterLMS.\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/trunk/packages/components\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"components\",\n    \"ui\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/components\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%components\"\n  },\n  \"main\": \"src/index.js\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"@emotion/styled\": \"^11.6.0\",\n    \"@wordpress/api-fetch\": \"^5.2.6\",\n    \"@wordpress/components\": \"^19.5.0\",\n    \"@wordpress/compose\": \"^5.0.6\",\n    \"@wordpress/element\": \"^4.14.0\",\n    \"@wordpress/i18n\": \"^4.2.4\",\n    \"@wordpress/url\": \"^3.3.1\",\n    \"lodash\": \"^4.17.21\",\n    \"react-select\": \"^5.2.1\",\n    \"throttle-debounce\": \"^3.0.1\"\n  },\n  \"devDependencies\": {\n    \"@testing-library/react\": \"^12.1.2\"\n  },\n  \"scripts\": {\n    \"docgen\": \"docgen src/index.js --output README.md --to-token\",\n    \"dev\": \"./../dev/src/index.js\",\n    \"lint:js\": \"wp-scripts lint-js ./ --config ../../.eslintrc.js\",\n    \"test\": \"wp-scripts test-unit-js ./ --config ../scripts/config/jest-unit.config.js --verbose\"\n  }\n}\n"
  },
  {
    "path": "packages/components/src/button-group-control/index.js",
    "content": "// WP deps.\nimport { BaseControl, Button, ButtonGroup } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\n\n/**\n * Button Group Control component\n *\n * Similar to the experimental `<RadioGroup>` component from @wordpress/components but it allows\n * passing in an array of options.\n *\n * @since 1.0.0\n *\n * @see BaseControl https://github.com/WordPress/gutenberg/tree/trunk/packages/components/src/base-control\n *\n * @param {Object}   props           Component properties object.\n * @param {string}   props.label     Control label text.\n * @param {string}   props.className Control element css class name attribute.\n * @param {string}   props.id        Control element ID attribute.\n * @param {Function} props.onClick   Callback function when a button in the group is clicked.\n * @param {string}   props.selected  The value of the currently selected option.\n * @param {Object[]} props.options   An array of objects used to create the buttons in the group.\n *                                   Each object should contain at least a \"label\" and \"value\" property and\n *                                   can optionally include an \"icon\" property.\n * @return {BaseControl} The rendered component.\n */\nexport default function( {\n\tlabel,\n\tonClick = () => {},\n\tclassName = null,\n\tid = null,\n\tselected = '',\n\toptions = [],\n} ) {\n\tconst [ selectedValue, setSelectedValue ] = useState( selected );\n\n\tclassName = className ? ` ${ className }` : '';\n\n\treturn (\n\t\t<BaseControl\n\t\t\tlabel={ label }\n\t\t\tclassName={ `llms-button-group-control${ className }` }\n\t\t\tid={ id }\n\t\t>\n\t\t\t<ButtonGroup style={ { display: 'flex' } }>\n\t\t\t\t{ options.map(\n\t\t\t\t\t( { label: buttonLabel, value, icon = null } ) => (\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tstyle={ { padding: '6px 8px' } }\n\t\t\t\t\t\t\tkey={ value }\n\t\t\t\t\t\t\tisPrimary={ value === selectedValue }\n\t\t\t\t\t\t\tisSecondary={ value !== selectedValue }\n\t\t\t\t\t\t\ticon={ icon }\n\t\t\t\t\t\t\tonClick={ () => {\n\t\t\t\t\t\t\t\tsetSelectedValue( value );\n\t\t\t\t\t\t\t\tonClick( value );\n\t\t\t\t\t\t\t} }\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{ buttonLabel }\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t)\n\t\t\t\t) }\n\t\t\t</ButtonGroup>\n\t\t</BaseControl>\n\t);\n}\n"
  },
  {
    "path": "packages/components/src/button-group-control/test/index.js",
    "content": "/**\n * External dependencies\n */\nimport { render } from '@testing-library/react';\n\n/**\n * Internal dependencies\n */\nimport ButtonGroupControl from '../';\n\ndescribe( 'ButtonGroupControl', () => {\n\tit( 'should render a <ButtonGroupControl> component', () => {\n\t\tconst args = {\n\t\t\t\tid: 'group-id',\n\t\t\t\tclassName: 'extra-class',\n\t\t\t\tlabel: 'Label',\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Button 1',\n\t\t\t\t\t\tvalue: 'button-1',\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Button 2',\n\t\t\t\t\t\tvalue: 'button-2',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tselected: 'button-1',\n\t\t\t},\n\t\t\t{ container } = render( <ButtonGroupControl { ...args } /> ),\n\t\t\tcontrol = container.firstChild,\n\t\t\tlabel = control.querySelector( 'label' ),\n\t\t\tbuttons = control.querySelectorAll( 'button' );\n\n\t\texpect(\n\t\t\tcontrol.classList.contains( 'llms-button-group-control' )\n\t\t).toBe( true );\n\t\texpect( control.classList.contains( 'components-base-control' ) ).toBe(\n\t\t\ttrue\n\t\t);\n\t\texpect( control.classList.contains( 'extra-class' ) ).toBe( true );\n\n\t\texpect( label.getAttribute( 'for' ) ).toBe( args.id );\n\t\texpect( label.textContent ).toBe( args.label );\n\n\t\texpect( buttons.length ).toBe( args.options.length );\n\t\texpect( buttons[ 0 ].textContent ).toBe( args.options[ 0 ].label );\n\t\texpect( buttons[ 0 ].classList.contains( 'is-primary' ) ).toBe( true );\n\t\texpect( buttons[ 1 ].textContent ).toBe( args.options[ 1 ].label );\n\t\texpect( buttons[ 1 ].classList.contains( 'is-secondary' ) ).toBe(\n\t\t\ttrue\n\t\t);\n\t} );\n} );\n"
  },
  {
    "path": "packages/components/src/copy-button/index.js",
    "content": "import { __ } from '@wordpress/i18n';\nimport { ClipboardButton, Button, Tooltip } from '@wordpress/components';\nimport { useCopyToClipboard } from '@wordpress/compose';\n\n/**\n * A \"click to copy\" button.\n *\n * Uses the `useCopyToClipboard()` hook with a <Button> on WP 5.8 & later, otherwise falls back\n * to the deprecated <ClipboardButton>.\n *\n * @since 1.0.0\n *\n * @param {Object}   args             Component arguments.\n * @param {string}   args.buttonText  Text to to display within the button.\n * @param {string}   args.copyText    Text to copy to the clipboard.\n * @param {string}   args.tooltipText Text to use in the tooltip wrapper around the button.\n * @param {Function} args.onCopy      Copy success callback function.\n * @param {...*}     args.buttonProps Remaining properties passed to the underlying <Button> component.\n * @return {Object} The copy button fragment.\n */\nexport default function( {\n\tbuttonText,\n\tcopyText,\n\tonCopy,\n\ttooltipText = null,\n\t...buttonProps\n} ) {\n\ttooltipText = tooltipText || __( 'Click to copy.', 'lifterlms' );\n\n\tconst canUseHook = 'undefined' !== typeof useCopyToClipboard;\n\n\t// WP 5.8+.\n\tconst HookButton = () => {\n\t\tconst ref = useCopyToClipboard( copyText, onCopy );\n\t\treturn (\n\t\t\t<Button { ...buttonProps } ref={ ref }>\n\t\t\t\t{ buttonText }\n\t\t\t</Button>\n\t\t);\n\t};\n\n\t// WP < 5.8.\n\tconst BackwardsButton = () => {\n\t\treturn (\n\t\t\t<ClipboardButton\n\t\t\t\t{ ...buttonProps }\n\t\t\t\ttext={ copyText }\n\t\t\t\tonCopy={ onCopy }\n\t\t\t>\n\t\t\t\t{ buttonText }\n\t\t\t</ClipboardButton>\n\t\t);\n\t};\n\n\treturn (\n\t\t<Tooltip text={ tooltipText }>\n\t\t\t{ canUseHook && <HookButton /> }\n\t\t\t{ ! canUseHook && <BackwardsButton /> }\n\t\t</Tooltip>\n\t);\n}\n"
  },
  {
    "path": "packages/components/src/index.js",
    "content": "export { default as ButtonGroupControl } from './button-group-control';\nexport { default as CopyButton } from './copy-button';\n\nexport * from './search-control';\n\nexport * as Spinner from './spinner';\n"
  },
  {
    "path": "packages/components/src/post-select/index.js",
    "content": "import { __, sprintf } from '@wordpress/i18n';\nimport { useSelect } from '@wordpress/data';\nimport { PanelRow, SelectControl } from '@wordpress/components';\n\nexport const llmsPostTypes = [\n\t'course',\n\t'lesson',\n\t'llms_quiz'\n];\n\nexport const getPostTypeName = ( slug, format = 'name' ) => {\n\tconst name = slug?.replace( 'llms_', '' );\n\tconst title = name.charAt( 0 ).toUpperCase() + name.slice( 1 );\n\n\treturn format === 'name' ? name : title;\n};\n\nexport const useLlmsPostType = () => {\n\tconst postType = useSelect( ( select ) => select( 'core/editor' )?.getCurrentPostType(), [] );\n\n\treturn llmsPostTypes.includes( postType );\n};\n\nexport const usePostOptions = ( postType = 'course' ) => {\n\tconst { posts, currentPostType } = useSelect( ( select ) => {\n\t\treturn {\n\t\t\tposts: select( 'core' ).getEntityRecords( 'postType', postType ),\n\t\t\tcurrentPostType: select( 'core/editor' )?.getCurrentPostType(),\n\t\t};\n\t}, [] );\n\n\tconst postTypeName = getPostTypeName( postType );\n\n\tconst options = [];\n\n\tif ( ! llmsPostTypes.includes( currentPostType ) ) {\n\t\toptions.push( {\n\t\t\tlabel: __( 'Select course', 'lifterlms' ),\n\t\t\tvalue: 0,\n\t\t} );\n\t}\n\n\tif ( posts?.length ) {\n\t\tposts.forEach( ( post ) => {\n\t\t\toptions.push( {\n\t\t\t\tlabel: post.title.rendered + ' (ID: ' + post.id + ')',\n\t\t\t\tvalue: post.id,\n\t\t\t} );\n\t\t} );\n\t}\n\n\tif ( llmsPostTypes.includes( currentPostType ) ) {\n\t\toptions.unshift( {\n\t\t\tlabel: sprintf(\n\t\t\t\t// Translators: %s = Post type name.\n\t\t\t\t__( 'Inherit from current %s', 'lifterlms' ),\n\t\t\t\tgetPostTypeName( currentPostType )\n\t\t\t),\n\t\t\tvalue: 0,\n\t\t} );\n\t}\n\n\tif ( ! options?.length ) {\n\t\toptions.push( {\n\t\t\tlabel: __( 'Loading', 'lifterlms' ),\n\t\t\tvalue: 0,\n\t\t} );\n\t}\n\n\treturn options;\n};\n\nexport const PostSelect = (\n\t{\n\t\tattributes,\n\t\tsetAttributes,\n\t\tpostType = 'course',\n\t\tattribute = 'course_id',\n\t}\n) => {\n\tconst options = usePostOptions( postType );\n\tconst postTypeName = getPostTypeName( postType );\n\tconst postTypeTitle = getPostTypeName( postType, 'title' );\n\n\tconst helpText = sprintf(\n\t\t// Translators: %s = Post type name.\n\t\t__( 'Select the %s to associate with this block.', 'lifterlms' ),\n\t\tpostTypeName\n\t);\n\n\treturn <PanelRow>\n\t\t<SelectControl\n\t\t\tlabel={ postTypeTitle }\n\t\t\thelp={ helpText }\n\t\t\tvalue={ attributes?.[ attribute ] ?? options?.[ 0 ]?.value }\n\t\t\toptions={ options }\n\t\t\tonChange={ ( value ) => {\n\t\t\t\tsetAttributes( {\n\t\t\t\t\t[ attribute ]: parseInt( value, 10 ),\n\t\t\t\t} );\n\t\t\t} }\n\t\t/>\n\t</PanelRow>;\n};\n"
  },
  {
    "path": "packages/components/src/search-control/base-search-control.js",
    "content": "// External Deps.\nimport Select from 'react-select/async';\nimport { debounce } from 'throttle-debounce';\nimport { uniqueId, differenceBy, uniqBy } from 'lodash';\n\n// WP Deps.\nimport { __ } from '@wordpress/i18n';\nimport { useState, useEffect } from '@wordpress/element';\nimport apiFetch from '@wordpress/api-fetch';\nimport { addQueryArgs } from '@wordpress/url';\n\n// Internal Deps.\nimport { StyledBaseControl } from './styled-base-control';\nimport {\n\tdefaultHydrateValues,\n\tdefaultStyles,\n\tdefaultTheme,\n\tdefaultFormatSearchResults,\n} from './defaults';\n\n/**\n * Searchable <select> element powered by a WordPress REST API endpoint.\n *\n * @since 1.0.0\n *\n * @param {Object}    args                         Component arguments.\n * @param {string}    args.searchPath              Required. API path used to perform the search.\n * @param {Function}  args.onUpdate                Callback function invoked when the value of the select changes.\n *                                                 The callback function is passed a single parameter, the new selected\n *                                                 value object(s). For multiselects it will be an array of objects.\n *                                                 If the select is clearable, the value will be `null` when the select\n *                                                 is cleared.\n * @param {Array}     args.selectedValue           The currently selected value(s). If an object is passed, it should contain at least\n *                                                 a `label` and `value` key. Can pass IDs as integers and the values will be automatically\n *                                                 hydrated.\n * @param {string}    args.placeholder             The placeholder displayed within an empty search control.\n * @param {string}    args.className               HTML class attribute added to the select control.\n * @param {string}    args.classNamePrefix         Prefix added to select control subcomponent classnames. In most circumstances this should not\n *                                                 be changed as it is used to style the compontents.\n * @param {number}    args.searchDebounceDelay     Search debounce delay, in milliseconds.\n * @param {Object}    args.additionalSearchArgs    Object of additional query string arguments to use with the API request.\n * @param {?string}   args.label                   Search control label, passed to <BaseControl>.\n * @param {?string}   args.id                      Search control HTML ID attribute, passed to <BaseControl>.\n * @param {?Object[]} args.defaultOptions          Array of hydrated objects to preload into the select as options.\n * @param {?Function} args.getSearchArgs           Function invoked to generate the query string arguments used when fetching\n *                                                 results from the API. The callback function is passed the search string.\n * @param {?Function} args.getSearchURL            Function invoked to create the search URL used to fetch results. The function\n *                                                 is passed the `searchPath` and generated query string arguments from `getSearchArgs()`.\n * @param {?Function} args.hydrateValues           Function invoked to hydrate integer values. The function is passed the currently selected values,\n *                                                 the `searchPath`, and an array of cached (and hydrated) objects previously loaded from the server.\n * @param {?Function} args.formatSearchResults     Function invoked to format results retrieved from the server. The function is passed an array\n *                                                 of objects from the server. It should return an array of objects, each containing at least a\n *                                                 value and label property.\n * @param {?Function} args.formatSearchResultLabel Function invoked to format the display label for a result. The function is passed\n *                                                 an object representing a single result and should return a string.\n * @param {?Function} args.formatSearchResultValue Function invoked to format the saved value for a result. The function is passed\n *                                                 an object representing a single result and should the value to be stored.\n * @param {...*}      args.selectProps             Any remaining properties are passed to the <Select> component, {@link https://react-select.com/props#select-props}.\n * @return {StyledBaseControl} The component.\n */\nexport default function BaseSearchControl( {\n\tsearchPath,\n\tonUpdate = () => {},\n\tselectedValue = [],\n\tadditionalSearchArgs = {},\n\tdefaultOptions = null,\n\tgetSearchArgs = null,\n\tlabel = null,\n\tgetSearchURL = null,\n\thydrateValues = null,\n\tformatSearchResults = null,\n\tformatSearchResultLabel = null,\n\tformatSearchResultValue = null,\n\tid = null,\n\tplaceholder = __( 'Search…', 'lifterlms' ),\n\tclassName = 'llms-base-search-control',\n\tclassNamePrefix = 'llms-search-control',\n\tsearchDebounceDelay = 300,\n\t...selectProps\n} ) {\n\t// Setup state variables.\n\tconst [ loadedResults, setLoadedResults ] = useState( [] ),\n\t\taddLoadedResults = ( newResults ) =>\n\t\t\tsetLoadedResults( loadedResults.concat( newResults ) ),\n\t\t[ value, setValue ] = useState(\n\t\t\tArray.isArray( selectedValue ) ? selectedValue : [ selectedValue ]\n\t\t);\n\n\t// If an ID is stored and passed into component as the selectedValue, hydrate the value from cached results or the API.\n\tuseEffect( () => {\n\t\t// Nothing to hydrate.\n\t\tif ( ! selectedValue.length ) {\n\t\t\treturn;\n\t\t}\n\t\thydrateValues = hydrateValues || defaultHydrateValues;\n\t\thydrateValues( value, searchPath, loadedResults ).then(\n\t\t\t( newValues ) => {\n\t\t\t\tnewValues = formatSearchResults(\n\t\t\t\t\tnewValues,\n\t\t\t\t\tformatSearchResultLabel,\n\t\t\t\t\tformatSearchResultValue\n\t\t\t\t);\n\t\t\t\tconst toAdd = differenceBy( newValues, loadedResults, 'id' );\n\t\t\t\tif ( toAdd.length ) {\n\t\t\t\t\taddLoadedResults( toAdd );\n\t\t\t\t}\n\t\t\t\tsetValue( newValues );\n\t\t\t\treturn newValues;\n\t\t\t}\n\t\t);\n\t}, [ selectedValue ] );\n\n\t/**\n\t * On change function callback.\n\t *\n\t * Updates the current value's state and calls the `onUpdate()` user function.\n\t *\n\t * @since 1.0.0\n\t *\n\t * @param {?Object[]} newValues Newly selected values.\n\t * @return {void}\n\t */\n\tconst onChange = ( newValues ) => {\n\t\tsetValue( Array.isArray( newValues ) ? newValues : [ newValues ] );\n\t\tonUpdate( newValues );\n\t};\n\n\t/**\n\t * Load options from the server.\n\t *\n\t * On search term update callback function.\n\t *\n\t * @since 1.0.0\n\t */\n\tconst loadOptions = debounce(\n\t\tsearchDebounceDelay,\n\t\t( searchQuery, callback ) => {\n\t\t\tapiFetch( {\n\t\t\t\tpath: getSearchURL( searchPath, getSearchArgs( searchQuery ) ),\n\t\t\t} ).then( ( results ) => {\n\t\t\t\tconst formatted = formatSearchResults(\n\t\t\t\t\tresults,\n\t\t\t\t\tformatSearchResultLabel,\n\t\t\t\t\tformatSearchResultValue\n\t\t\t\t);\n\t\t\t\taddLoadedResults( formatted );\n\t\t\t\tcallback( formatted );\n\t\t\t} );\n\t\t}\n\t);\n\n\t// Setup defaults.\n\tid = id || uniqueId( `${ className }--` );\n\n\tformatSearchResults = formatSearchResults || defaultFormatSearchResults;\n\tformatSearchResultLabel = formatSearchResultLabel\n\t\t? formatSearchResultLabel\n\t\t: ( res ) => res?.id;\n\tformatSearchResultValue = formatSearchResultValue\n\t\t? formatSearchResultValue\n\t\t: ( res ) => res?.id;\n\n\tgetSearchArgs = getSearchArgs\n\t\t? getSearchArgs\n\t\t: ( searchQuery ) => ( {\n\t\t\tper_page: 10,\n\t\t\tsearch: searchQuery,\n\t\t\t...additionalSearchArgs,\n\t\t} );\n\n\tgetSearchURL = getSearchURL\n\t\t? getSearchURL\n\t\t: ( path, args ) => addQueryArgs( path, args );\n\n\tselectProps.styles = selectProps.styles || defaultStyles;\n\tselectProps.theme = selectProps.theme || defaultTheme;\n\n\tif ( null === defaultOptions && value.length ) {\n\t\tdefaultOptions = loadedResults.length\n\t\t\t? uniqBy( loadedResults, 'id' )\n\t\t\t: true;\n\t}\n\n\treturn (\n\t\t<StyledBaseControl { ...{ id, label } }>\n\t\t\t<Select\n\t\t\t\t{ ...{\n\t\t\t\t\tclassName,\n\t\t\t\t\tclassNamePrefix,\n\t\t\t\t\tvalue,\n\t\t\t\t\tplaceholder,\n\t\t\t\t\tloadOptions,\n\t\t\t\t\tdefaultOptions,\n\t\t\t\t\tonChange,\n\t\t\t\t\t...selectProps,\n\t\t\t\t} }\n\t\t\t/>\n\t\t</StyledBaseControl>\n\t);\n}\n"
  },
  {
    "path": "packages/components/src/search-control/defaults.js",
    "content": "// External deps.\nimport { isPlainObject } from 'lodash';\n\n// WP deps.\nimport apiFetch from '@wordpress/api-fetch';\n\n/**\n * Default function used to hydrate stored numeric IDs to the equivalent object.\n *\n * @since 1.0.0\n *\n * @param {Array}    values        Array of values.\n * @param {string}   path          API request path.\n * @param {Object[]} loadedResults Array of already-hydrated API results.\n * @return {Object[]} Hydrated result array.\n */\nexport async function defaultHydrateValues( values, path, loadedResults ) {\n\tconst isResultHydrated = ( result ) =>\n\t\tisPlainObject( result ) && result.label && result.value;\n\n\treturn Promise.all(\n\t\tvalues.map( async ( value ) => {\n\t\t\tif ( ! isResultHydrated( value ) && Number.isInteger( value ) ) {\n\t\t\t\tvalue =\n\t\t\t\t\tloadedResults.find( ( { id } ) => id === value ) ||\n\t\t\t\t\t( await apiFetch( { path: `${ path }/${ value }` } ) );\n\t\t\t}\n\n\t\t\treturn value;\n\t\t} )\n\t);\n}\n\n/**\n * Default styles object passed to the underlying <Select> component.\n *\n * @type {Object}\n */\nexport const defaultStyles = {\n\tcontrol: ( control ) => ( {\n\t\t...control,\n\t\tborderColor: '#8d96a0',\n\t\t'&:hover': {\n\t\t\t...control[ '&:hover' ],\n\t\t\tborderColor: '#8d96a0',\n\t\t},\n\t} ),\n};\n\n/**\n * Default <Select> component theme callback function.\n *\n * Customizes the theme of the component to better match the WordPress editor UI.\n *\n * Uses the default UI color from the current admin theme. The theme doesn't work that well\n * with the other provided theme colors (which are darker than the Select component options which\n * are lighter highlights). So if you're using a non-default admin color scheme the select will probably\n * look a bit weird. I'm sorry.\n *\n * @since 1.0.0\n *\n * @see https://react-select.com/styles#overriding-the-theme\n *\n * @param {Object} theme Theme object.\n * @return {Object} Theme object.\n */\nexport function defaultTheme( theme ) {\n\treturn {\n\t\t...theme,\n\t\tcolors: {\n\t\t\t...theme.colors,\n\t\t\tprimary: 'var( --wp-admin-theme-color )',\n\t\t\t// primary25: '#ccf2ff',\n\t\t\t// primary50: '#b3ecff',\n\t\t\t// primary75: '#4dd2ff',\n\t\t},\n\t\tspacing: {\n\t\t\t...theme.spacing,\n\t\t\tbaseUnit: 2,\n\t\t\tcontrolHeight: 28,\n\t\t\tmenuGutter: 4,\n\t\t},\n\t};\n}\n\n/**\n * Default format search results function.\n *\n * Accepts an array of raw API results adds a label and value for use by the <Select>\n * component.\n *\n * @since 1.0.0\n *\n * @param {Object[]} results                 API result array.\n * @param {Function} formatSearchResultLabel Label formatting function.\n * @param {Function} formatSearchResultValue Value formatting function.\n * @return {Object[]} Formatted results.\n */\nexport function defaultFormatSearchResults(\n\tresults,\n\tformatSearchResultLabel,\n\tformatSearchResultValue\n) {\n\treturn results.map( ( result ) => ( {\n\t\t...result,\n\t\tlabel: formatSearchResultLabel( result ),\n\t\tvalue: formatSearchResultValue( result ),\n\t} ) );\n}\n"
  },
  {
    "path": "packages/components/src/search-control/index.js",
    "content": "import BaseSearchControl from './base-search-control';\nimport PostSearchControl from './post-search-control';\nimport UserSearchControl from './user-search-control';\n\nexport { BaseSearchControl, PostSearchControl, UserSearchControl };\n"
  },
  {
    "path": "packages/components/src/search-control/post-search-control.js",
    "content": "import { __, _x, sprintf } from '@wordpress/i18n';\n\nimport BaseSearchControl from './base-search-control';\n\n/**\n * Searchable <select> element powered by a WordPress REST API users endpoint.\n *\n * This component is a wrapper around the <BaseSearchControl> component. It is configured\n * to search users via the WordPress user REST API endpoint.\n *\n * @since 1.0.0\n *\n * @param {Object}    args                         Component arguments.\n * @param {string}    args.postType                Post type endpoint.\n * @param {string}    args.baseSearchPath          Base search path used to create the searchPath.\n * @param {?string}   args.searchPath              API path used to perform the search. If passed, will be used instead of the\n *                                                 path generated from `args.postType` and `args.baseSearchPath`.\n * @param {string}    args.placeholder             The placeholder displayed within an empty search control.\n * @param {string}    args.className               HTML class attribute added to the select control.\n * @param {?Function} args.formatSearchResultLabel Function invoked to format the display label for a result. The function is passed\n * @param {Object}    args.additionalSearchArgs    An object representing a single result and should return a string.\n * @param {...*}      args.baseProps               Any remaining properties are passed to the <BaseSearchControl> component.\n * @return {BaseSearchControl} The component.\n */\nexport default function PostSearchControl( {\n\tpostType = 'posts',\n\tbaseSearchPath = '/wp/v2/',\n\tsearchPath = null,\n\tclassName = 'llms-post-search-control',\n\tplaceholder = __( 'Search for posts…', 'lifterlms' ),\n\tformatSearchResultLabel = null,\n\tadditionalSearchArgs = {},\n\t...baseProps\n} ) {\n\t// Default result label.\n\tformatSearchResultLabel = formatSearchResultLabel\n\t\t? formatSearchResultLabel\n\t\t: ( { title, id } ) =>\n\t\t\tsprintf(\n\t\t\t\t// Translators: %1$s = Post title; %2$s = Post id.\n\t\t\t\t_x(\n\t\t\t\t\t'%1$s (ID# %2$d)',\n\t\t\t\t\t'Post search result label',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t),\n\t\t\t\ttitle.rendered,\n\t\t\t\tid\n\t\t\t);\n\n\treturn (\n\t\t<BaseSearchControl\n\t\t\t{ ...{\n\t\t\t\tsearchPath: searchPath || `${ baseSearchPath }${ postType }`,\n\t\t\t\tclassName,\n\t\t\t\tplaceholder,\n\t\t\t\tformatSearchResultLabel,\n\t\t\t\tadditionalSearchArgs,\n\t\t\t\t...baseProps,\n\t\t\t} }\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "packages/components/src/search-control/styled-base-control.js",
    "content": "// External deps.\nimport styled from '@emotion/styled';\n\n// WP deps.\nimport { BaseControl } from '@wordpress/components';\n\n/**\n * A <BaseControl> component with styles targeting the <BaseSearchControl> components within it.\n *\n * Addresses issues arising from WP core styles loaded in the block editor that create visual\n * issues with our components.\n *\n * @since 1.0.0\n */\nexport const StyledBaseControl = styled( BaseControl )`\n\twidth: 100%;\n\t& .llms-search-control__input:focus {\n\t\tbox-shadow: none;\n\t}\n\t& .llms-search-control__menu {\n\t\tbackground: #fff !important;\n\t\tz-index: 9999999 !important;\n\t}\n\t& .llms-search-control__value-container {\n\t\twidth: 100%;\n\t}\n`;\n"
  },
  {
    "path": "packages/components/src/search-control/user-search-control.js",
    "content": "import { __, _x, sprintf } from '@wordpress/i18n';\n\nimport BaseSearchControl from './base-search-control';\n\n/**\n * Searchable <select> element powered by a WordPress REST API users endpoint.\n *\n * This component is a wrapper around the <BaseSearchControl> component. It is configured\n * to search users via the WordPress user REST API endpoint.\n *\n * @since 1.0.0\n *\n * @param {Object}    args                         Component arguments.\n * @param {string}    args.searchPath              Required. API path used to perform the search.\n * @param {string}    args.placeholder             The placeholder displayed within an empty search control.\n * @param {string}    args.className               HTML class attribute added to the select control.\n * @param {?Function} args.formatSearchResultLabel Function invoked to format the display label for a result. The function is passed\n * @param {Object}    args.additionalSearchArgs    An object representing a single result and should return a string.\n * @param {...*}      args.baseProps               Any remaining properties are passed to the <BaseSearchControl> component.\n * @return {BaseSearchControl} The component.\n */\nexport default function UserSearchControl( {\n\tsearchPath = '/wp/v2/users',\n\tclassName = 'llms-user-search-control',\n\tplaceholder = __( 'Search users by email or name…', 'lifterlms' ),\n\tformatSearchResultLabel = null,\n\tadditionalSearchArgs = {},\n\t...baseProps\n} ) {\n\t// Default result label.\n\tformatSearchResultLabel = formatSearchResultLabel\n\t\t? formatSearchResultLabel\n\t\t: ( { name, id } ) =>\n\t\t\tsprintf(\n\t\t\t\t// Translators: %1$s = User's name; %2$s = User's id.\n\t\t\t\t_x(\n\t\t\t\t\t'%1$s (ID# %2$d)',\n\t\t\t\t\t'User search result label',\n\t\t\t\t\t'lifterlms'\n\t\t\t\t),\n\t\t\t\tname,\n\t\t\t\tid\n\t\t\t);\n\n\treturn (\n\t\t<BaseSearchControl\n\t\t\t{ ...{\n\t\t\t\tsearchPath,\n\t\t\t\tclassName,\n\t\t\t\tplaceholder,\n\t\t\t\tformatSearchResultLabel,\n\t\t\t\tadditionalSearchArgs,\n\t\t\t\t...baseProps,\n\t\t\t} }\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "packages/components/src/spinner/.eslintrc.js",
    "content": "module.exports = {\n\tenv: {\n\t\tbrowser: true,\n\t},\n\tglobals: {\n\t\tjQuery: true,\n\t},\n};\n"
  },
  {
    "path": "packages/components/src/spinner/constants.js",
    "content": "/**\n * Spinner wrapper element class name.\n *\n * @type {string}\n */\nexport const WRAPPER_CLASSNAME = 'llms-spinning';\n\n/**\n * Spinner element class name.\n *\n * @type {string}\n */\nexport const CLASSNAME = 'llms-spinner';\n\n/**\n * Small-sized spinner identifier / class name.\n *\n * @type {string}\n */\nexport const SIZE_SMALL = 'small';\n\n/**\n * Default-sized spinner identifier / class name.\n *\n * @type {string}\n */\nexport const SIZE_DEFAULT = 'default';\n"
  },
  {
    "path": "packages/components/src/spinner/index.js",
    "content": "/* eslint-env jquery */\n\n// Internal deps.\nimport { SIZE_DEFAULT } from './constants';\nimport { create, ensureElementList, find, loadStyles } from './utils';\n\n/**\n * This module was originally included in the LifterLMS Core Javascript in the `LLMS` global object as `LLMS.Spinner`.\n *\n * It has since been relocated here as a module in the `@lifterlms/components` package. During the move it was upgraded\n * to enable usage without the requirement of passing in jQuery selectors.\n *\n * In the future, passing native jQuery selectors into any of these modules will be deprecated in favor of native/vanilla Javascript\n * Elements or selector strings that can be passed into `document.querySelector()` or `document.querySelectorAll()`.\n */\n\n/**\n * Retrieves spinner(s) inside a given element.\n *\n * If the spinner element doesn't already exist it will be created.\n *\n * @since 1.1.0\n *\n * @param {jQuery|Element|string} selector  A selector to be parsed by `jQuery()`, an existing `jQuery()` selection, a DOM Element. The first spinner\n *                                          found within the element will be returned. If none found it will be created, appended to the element, and\n *                                          then returned.\n * @param {string}                size      Size or the spinner element. Accepts \"default\" (40px) or \"small\" (20px).\n * @param {boolean}               useJQuery If `true`, the return value will be a jQuery selection as opposed to an Element. This is the default behavior\n *                                          for backwards compatibility but will be removed in a future version.\n * @return {null|Element|jQuery} Returns `null` when the selector cannot be located, otherwise returns an `Element` or `jQuery` selection based on the value\n *                               of `useJQuery`.\n */\nexport function get( selector, size = SIZE_DEFAULT, useJQuery = true ) {\n\tloadStyles();\n\n\tconst nodeList = ensureElementList( selector );\n\tif ( ! nodeList.length ) {\n\t\treturn null;\n\t}\n\n\tconst wrapper = nodeList[ 0 ],\n\t\t// Find an existing spinner and create it if one doesn't exist.\n\t\tspinner = find( wrapper ) || create( wrapper, size );\n\n\t// Return it.\n\treturn useJQuery && typeof jQuery !== 'undefined'\n\t\t? jQuery( spinner )\n\t\t: spinner;\n}\n\n/**\n * Starts spinner(s) inside a given element or element list.\n *\n * If the spinner element doesn't already exist it will be created.\n *\n * @since 1.1.0\n *\n * @param {jQuery|Element|NodeList|string} selector A selector to be parsed by `jQuery()`, an existing `jQuery()` selection, a DOM Element, or an\n *                                                  array of DOM Elements. Each element in the list will have it's spinner started. If a spinner doesn't\n *                                                  exist within the element, it will be appended and then started.\n * @param {string}                         size     Size or the spinner element. Accepts \"default\" (40px) or \"small\" (20px).\n * @return {void}\n */\nexport function start( selector, size = SIZE_DEFAULT ) {\n\tensureElementList( selector ).forEach( ( el ) => {\n\t\tconst spinner = get( el, size, false );\n\t\tif ( spinner ) {\n\t\t\tspinner.style.display = 'block';\n\t\t}\n\t} );\n}\n\n/**\n * Stops spinner(s) inside a given element or element list.\n *\n * @since 1.1.0\n *\n * @param {jQuery|Element|NodeList|string} selector A selector to be parsed by `jQuery()`, an existing `jQuery()` selection, a DOM Element, or an\n *                                                  array of DOM Elements. Each element in the list will have it's spinner stopped.\n * @return {void}\n */\nexport function stop( selector ) {\n\tensureElementList( selector ).forEach( ( el ) => {\n\t\tconst spinner = get( el, SIZE_DEFAULT, false );\n\t\tif ( spinner ) {\n\t\t\tspinner.style.display = 'none';\n\t\t}\n\t} );\n}\n"
  },
  {
    "path": "packages/components/src/spinner/styles.js",
    "content": "import { WRAPPER_CLASSNAME, CLASSNAME, SIZE_SMALL } from './constants';\n\n/**\n * CSS Styles for the components.\n *\n * @type {string}\n */\nexport const STYLES = `\n\t.${ WRAPPER_CLASSNAME } {\n\t\tbackground: rgba( 250, 250, 250, 0.7 );\n\t\tbottom: 0;\n\t\tdisplay: none;\n\t\tleft: 0;\n\t\tposition: absolute;\n\t\tright: 0;\n\t\ttop: 0;\n\t\tz-index: 2;\n\t}\n\n\t.${ CLASSNAME } {\n\t\tanimation: llms-spinning 1.5s linear infinite;\n\t\tbox-sizing: border-box;\n\t\tborder: 4px solid #313131;\n\t\tborder-radius: 50%;\n\t\theight: 40px;\n\t\tleft: 50%;\n\t\tmargin-left: -20px;\n\t\tmargin-top: -20px;\n\t\tposition: absolute;\n\t\ttop: 50%;\n\t\twidth: 40px;\n\n\t}\n\n\t.${ CLASSNAME }.${ SIZE_SMALL } {\n\t\tborder-width: 2px;\n\t\theight: 20px;\n\t\tmargin-left: -10px;\n\t\tmargin-top: -10px;\n\t\twidth: 20px;\n\t}\n\n\t@keyframes llms-spinning {\n\t\t0% {\n\t\t\ttransform: rotate( 0deg )\n\t\t}\n\t\t50% {\n\t\t\tborder-radius: 5%;\n\t\t}\n\t\t100% {\n\t\t\ttransform: rotate( 220deg) \n\t\t}\n\t}\n`;\n"
  },
  {
    "path": "packages/components/src/spinner/test/__MOCKS__/jquery.js",
    "content": "/**\n * Mock jQuery class.\n *\n * Generates a minimal mocked jQuery object for use by unit tests.\n *\n * @since 1.1.0\n *\n * @param {string} selector A selector string that can be passed to `document.querySelectorAll()`.\n * @return {Object} A mock jQuery object.\n */\nfunction fakeQuery( selector ) {\n\treturn {\n\t\tisFakeQuery: true,\n\t\ttoArray: () => document.querySelectorAll( selector ),\n\t};\n}\nObject.defineProperty( fakeQuery, Symbol.hasInstance, {\n\tvalue: ( instance ) => {\n\t\treturn instance.isFakeQuery;\n\t},\n} );\n\nexport { fakeQuery };\n"
  },
  {
    "path": "packages/components/src/spinner/test/__snapshots__/index.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`Spinner: public API get() Autoload styles 1`] = `\"<style id=\\\\\"llms-spinner-styles\\\\\"> .llms-spinning { background: rgba( 250, 250, 250, 0.7 ); bottom: 0; display: none; left: 0; position: absolute; right: 0; top: 0; z-index: 2; } .llms-spinner { animation: llms-spinning 1.5s linear infinite; box-sizing: border-box; border: 4px solid #313131; border-radius: 50%; height: 40px; left: 50%; margin-left: -20px; margin-top: -20px; position: absolute; top: 50%; width: 40px; } .llms-spinner.small { border-width: 2px; height: 20px; margin-left: -10px; margin-top: -10px; width: 20px; } @keyframes llms-spinning { 0% { transform: rotate( 0deg ) } 50% { border-radius: 5%; } 100% { transform: rotate( 220deg) } }</style>\"`;\n\nexports[`Spinner: public API get() Create and return 1`] = `\"<div class=\\\\\"abc\\\\\"><div class=\\\\\"llms-spinning\\\\\"><i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div></div>\"`;\n\nexports[`Spinner: public API get() Create and return 2`] = `\n<div\n  class=\"llms-spinning\"\n>\n  <i\n    aria-live=\"assertive\"\n    class=\"llms-spinner small\"\n    role=\"alert\"\n  >\n    <span\n      class=\"screen-reader-text\"\n    >\n      Loading…\n    </span>\n  </i>\n</div>\n`;\n\nexports[`Spinner: public API get() Create and return 3`] = `\"<div class=\\\\\"abc\\\\\"><div class=\\\\\"llms-spinning\\\\\"><i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div></div><div class=\\\\\"llms-spinning\\\\\"><i class=\\\\\"llms-spinner small\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div>\"`;\n\nexports[`Spinner: public API get() Create small size 1`] = `\"<div class=\\\\\"abc\\\\\"><div class=\\\\\"llms-spinning\\\\\"><i class=\\\\\"llms-spinner small\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div></div>\"`;\n\nexports[`Spinner: public API start() and stop() 1`] = `\"<div class=\\\\\"abc xyz\\\\\"><div class=\\\\\"llms-spinning\\\\\" style=\\\\\"display: block;\\\\\"><i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div></div><div class=\\\\\"def xyz\\\\\"><div class=\\\\\"llms-spinning\\\\\" style=\\\\\"display: block;\\\\\"><i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div></div>\"`;\n\nexports[`Spinner: public API start() and stop() 2`] = `\"<div class=\\\\\"abc xyz\\\\\"><div class=\\\\\"llms-spinning\\\\\" style=\\\\\"display: none;\\\\\"><i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div></div><div class=\\\\\"def xyz\\\\\"><div class=\\\\\"llms-spinning\\\\\" style=\\\\\"display: none;\\\\\"><i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i></div></div>\"`;\n"
  },
  {
    "path": "packages/components/src/spinner/test/__snapshots__/utils.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`Spinner: utils create() Default size (not specified) 1`] = `\"<i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i>\"`;\n\nexports[`Spinner: utils create() Default size (passed) 1`] = `\"<i class=\\\\\"llms-spinner default\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i>\"`;\n\nexports[`Spinner: utils create() Small size 1`] = `\"<i class=\\\\\"llms-spinner small\\\\\" role=\\\\\"alert\\\\\" aria-live=\\\\\"assertive\\\\\"><span class=\\\\\"screen-reader-text\\\\\">Loading…</span></i>\"`;\n\nexports[`Spinner: utils ensureElementList() Element input 1`] = `\nArray [\n  <div\n    class=\"abc\"\n  />,\n]\n`;\n\nexports[`Spinner: utils ensureElementList() Element input 2`] = `\n\"\n\t\t\t\t<div class=\\\\\"abc\\\\\"></div>\n\t\t\t\t<div class=\\\\\"def\\\\\"></div>\n\t\t\t\"\n`;\n\nexports[`Spinner: utils ensureElementList() NodeList input 1`] = `\nArray [\n  <div\n    class=\"abc\"\n  />,\n  <div\n    class=\"def\"\n  />,\n]\n`;\n\nexports[`Spinner: utils ensureElementList() NodeList input 2`] = `\n\"\n\t\t\t\t<div class=\\\\\"abc\\\\\"></div>\n\t\t\t\t<div class=\\\\\"def\\\\\"></div>\n\t\t\t\"\n`;\n\nexports[`Spinner: utils ensureElementList() String (multiple) input 1`] = `\nArray [\n  <div\n    class=\"abc\"\n  />,\n  <div\n    class=\"def\"\n  />,\n]\n`;\n\nexports[`Spinner: utils ensureElementList() String (multiple) input 2`] = `\n\"\n\t\t\t\t<div class=\\\\\"abc\\\\\"></div>\n\t\t\t\t<div class=\\\\\"def\\\\\"></div>\n\t\t\t\"\n`;\n\nexports[`Spinner: utils ensureElementList() String (single) input 1`] = `\nArray [\n  <div\n    class=\"def\"\n  />,\n]\n`;\n\nexports[`Spinner: utils ensureElementList() String (single) input 2`] = `\n\"\n\t\t\t\t<div class=\\\\\"abc\\\\\"></div>\n\t\t\t\t<div class=\\\\\"def\\\\\"></div>\n\t\t\t\"\n`;\n\nexports[`Spinner: utils ensureElementList() jQuery input 1`] = `\nArray [\n  <div\n    class=\"abc\"\n  />,\n  <div\n    class=\"def\"\n  />,\n]\n`;\n\nexports[`Spinner: utils ensureElementList() jQuery input 2`] = `\n\"\n\t\t\t\t<div class=\\\\\"abc\\\\\"></div>\n\t\t\t\t<div class=\\\\\"def\\\\\"></div>\n\t\t\t\"\n`;\n\nexports[`Spinner: utils loadStyles() 1`] = `\"<style id=\\\\\"llms-spinner-styles\\\\\"> .llms-spinning { background: rgba( 250, 250, 250, 0.7 ); bottom: 0; display: none; left: 0; position: absolute; right: 0; top: 0; z-index: 2; } .llms-spinner { animation: llms-spinning 1.5s linear infinite; box-sizing: border-box; border: 4px solid #313131; border-radius: 50%; height: 40px; left: 50%; margin-left: -20px; margin-top: -20px; position: absolute; top: 50%; width: 40px; } .llms-spinner.small { border-width: 2px; height: 20px; margin-left: -10px; margin-top: -10px; width: 20px; } @keyframes llms-spinning { 0% { transform: rotate( 0deg ) } 50% { border-radius: 5%; } 100% { transform: rotate( 220deg) } }</style>\"`;\n"
  },
  {
    "path": "packages/components/src/spinner/test/index.test.js",
    "content": "import { fakeQuery } from './__MOCKS__/jquery';\n\nimport { get, start, stop } from '../';\nimport { SIZE_SMALL, SIZE_DEFAULT } from '../constants';\n\nglobal.jQuery = fakeQuery;\n\ndescribe( 'Spinner: public API', () => {\n\tdescribe( 'get()', () => {\n\t\ttest( 'Autoload styles', () => {\n\t\t\tdocument.head.innerHTML = '';\n\t\t\tget( 'div', SIZE_DEFAULT, false );\n\t\t\texpect( document.head.innerHTML ).toMatchSnapshot();\n\t\t} );\n\n\t\ttest( 'Empty return', () => {\n\t\t\tdocument.body.innerHTML = '<div class=\"abc\"></div>';\n\t\t\texpect( get( '.xyz', SIZE_DEFAULT, false ) ).toBeNull();\n\t\t} );\n\n\t\ttest( 'Create and return', () => {\n\t\t\tdocument.body.innerHTML = '<div class=\"abc\"></div>';\n\t\t\tconst createResult = get( '.abc', SIZE_DEFAULT, false ),\n\t\t\t\tbody = document.body.innerHTML;\n\t\t\texpect( createResult ).toBeInstanceOf( Element );\n\t\t\texpect( body ).toMatchSnapshot();\n\n\t\t\t// Don't need to create it again.\n\t\t\tconst alreadExistsResult = get( '.abc', SIZE_DEFAULT, false );\n\t\t\texpect( alreadExistsResult ).toBe( createResult );\n\t\t\texpect( document.body.innerHTML ).toBe( body );\n\n\t\t\tconst differentParentResult = get( 'body', SIZE_SMALL, false );\n\t\t\texpect( differentParentResult ).toMatchSnapshot();\n\t\t\texpect( document.body.innerHTML ).toMatchSnapshot();\n\t\t} );\n\n\t\ttest( 'Create small size', () => {\n\t\t\tdocument.body.innerHTML = '<div class=\"abc\"></div>';\n\t\t\tconst res = get( '.abc', SIZE_SMALL, false ),\n\t\t\t\tbody = document.body.innerHTML;\n\t\t\texpect( res ).toBeInstanceOf( Element );\n\t\t\texpect( body ).toMatchSnapshot();\n\t\t} );\n\n\t\ttest( 'Return a jQuery selection', () => {\n\t\t\tdocument.body.innerHTML = '<div class=\"abc\"></div>';\n\t\t\texpect( get( '.abc', SIZE_DEFAULT ) ).toBeInstanceOf( jQuery );\n\t\t} );\n\t} );\n\n\ttest( 'start() and stop()', () => {\n\t\tdocument.body.innerHTML =\n\t\t\t'<div class=\"abc xyz\"></div><div class=\"def xyz\"></div>';\n\t\tget( '.abc' );\n\t\tget( '.def' );\n\n\t\tstart( '.xyz' );\n\t\texpect( document.body.innerHTML ).toMatchSnapshot();\n\n\t\tstop( '.xyz' );\n\t\texpect( document.body.innerHTML ).toMatchSnapshot();\n\t} );\n} );\n"
  },
  {
    "path": "packages/components/src/spinner/test/utils.test.js",
    "content": "import { fakeQuery } from './__MOCKS__/jquery';\n\nimport { create, ensureElementList, find, loadStyles } from '../utils';\nimport { SIZE_SMALL, SIZE_DEFAULT, WRAPPER_CLASSNAME } from '../constants';\n\nglobal.jQuery = fakeQuery;\n\ndescribe( 'Spinner: utils', () => {\n\tdescribe( 'create()', () => {\n\t\tconst tests = [\n\t\t\t{\n\t\t\t\tname: 'Default size (not specified)',\n\t\t\t\tsize: undefined,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'Default size (passed)',\n\t\t\t\tsize: SIZE_DEFAULT,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'Small size',\n\t\t\t\tsize: SIZE_SMALL,\n\t\t\t},\n\t\t];\n\n\t\ttest.each( tests )( '$name', ( { size } ) => {\n\t\t\tdocument.body.innerHTML = '<div id=\"wrapper\"></div>';\n\t\t\tconst el = create( document.querySelector( '#wrapper' ), size );\n\t\t\texpect( el ).toBeInstanceOf( Element );\n\t\t\texpect( el.innerHTML ).toMatchSnapshot();\n\t\t} );\n\t} );\n\n\tdescribe( 'ensureElementList()', () => {\n\t\tconst tests = [\n\t\t\t{\n\t\t\t\tname: 'NodeList',\n\t\t\t\tgetInput: () => document.querySelectorAll( 'div' ),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'Element',\n\t\t\t\tgetInput: () => document.querySelector( '.abc' ),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'String (single)',\n\t\t\t\tgetInput: () => '.def',\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'String (multiple)',\n\t\t\t\tgetInput: () => 'div',\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'jQuery',\n\t\t\t\tgetInput: () => global.jQuery( 'div' ),\n\t\t\t},\n\t\t];\n\n\t\ttest.each( tests )( '$name input', ( { getInput } ) => {\n\t\t\tdocument.body.innerHTML = `\n\t\t\t\t<div class=\"abc\"></div>\n\t\t\t\t<div class=\"def\"></div>\n\t\t\t`;\n\n\t\t\tconst input = getInput(),\n\t\t\t\tlist = ensureElementList( input );\n\n\t\t\texpect( list ).toMatchSnapshot();\n\t\t\tlist.forEach( ( el ) => {\n\t\t\t\texpect( el ).toBeInstanceOf( Element );\n\t\t\t} );\n\n\t\t\t// Make sure Elements aren't removed from the DOM.\n\t\t\texpect( document.body.innerHTML ).toMatchSnapshot();\n\t\t} );\n\t} );\n\n\tdescribe( 'find()', () => {\n\t\ttest( 'No spinners found in wrapper', () => {\n\t\t\tdocument.body.innerHTML = `\n\t\t\t\t<div id=\"wrap\">\n\t\t\t\t\t<div class=\"abc\"></div>\n\t\t\t\t</div>\n\t\t\t`;\n\n\t\t\texpect( find( document.getElementById( 'wrap' ) ) ).toBeNull();\n\t\t} );\n\n\t\ttest( 'No spinners that are direct children of the wrapper', () => {\n\t\t\tdocument.body.innerHTML = `\n\t\t\t\t<div id=\"wrap\">\n\t\t\t\t\t<div class=\"abc\"><div class=\"${ WRAPPER_CLASSNAME }\"></div></div>\n\t\t\t\t</div>\n\t\t\t`;\n\n\t\t\texpect( find( document.getElementById( 'wrap' ) ) ).toBeUndefined();\n\t\t} );\n\n\t\ttest( 'Spinner found', () => {\n\t\t\tdocument.body.innerHTML = `\n\t\t\t\t<div id=\"wrap\">\n\t\t\t\t\t<div class=\"${ WRAPPER_CLASSNAME }\" id=\"shouldbefound\"></div>\n\t\t\t\t</div>\n\t\t\t`;\n\n\t\t\tconst spinner = find( document.getElementById( 'wrap' ) );\n\t\t\texpect( spinner ).toBeInstanceOf( Element );\n\t\t\texpect( spinner.id ).toBe( 'shouldbefound' );\n\t\t} );\n\t} );\n\n\ttest( 'loadStyles()', () => {\n\t\tdocument.head.innerHTML = '';\n\n\t\t// Load them.\n\t\tloadStyles();\n\t\tconst headHTML = document.head.innerHTML;\n\t\texpect( headHTML ).toMatchSnapshot();\n\n\t\t// Doesn't load them again.\n\t\tloadStyles();\n\t\texpect( document.head.innerHTML ).toBe( headHTML );\n\t} );\n} );\n"
  },
  {
    "path": "packages/components/src/spinner/utils.js",
    "content": "// WP deps.\nimport { __ } from '@wordpress/i18n';\n\n// Internal deps.\nimport { WRAPPER_CLASSNAME, CLASSNAME, SIZE_DEFAULT } from './constants';\nimport { STYLES } from './styles';\n\n/**\n * Creates a spinner element inside the specified wrapper Element.\n *\n * @since 1.1.0\n *\n * @param {Element} wrapper DOM node to append the created spinner element to.\n * @param {string}  size    Spinner element size.\n * @return {Element} Returns the created spinner node.\n */\nexport function create( wrapper, size = SIZE_DEFAULT ) {\n\tconst spinner = document.createElement( 'div' ),\n\t\tloadingMsg = __( 'Loading…', 'lifterlms' );\n\n\tspinner.innerHTML = `<i class=\"${ CLASSNAME } ${ size }\" role=\"alert\" aria-live=\"assertive\"><span class=\"screen-reader-text\">${ loadingMsg }</span></i>`;\n\tspinner.classList.add( WRAPPER_CLASSNAME );\n\n\twrapper.appendChild( spinner );\n\n\treturn spinner;\n}\n\n/**\n * Normalizes accepted selector inputs and returns a `NodeList`.\n *\n * When jQuery selection is detected, adds a console deprecation warning as well.\n *\n * @since 1.1.0\n *\n * @param {NodeList|Element|string|jQuery} selector The input selector.\n * @return {Element[]} An array of `Element` objects derived from the selector input.\n */\nexport function ensureElementList( selector ) {\n\tselector =\n\t\ttypeof selector === 'string'\n\t\t\t? document.querySelectorAll( selector )\n\t\t\t: selector;\n\n\t// Already a NodeList.\n\tif ( selector instanceof NodeList ) {\n\t\treturn Array.from( selector );\n\t}\n\n\tconst list = [];\n\tif ( selector instanceof Element ) {\n\t\tlist.push( selector );\n\t} else if ( typeof jQuery !== 'undefined' && selector instanceof jQuery ) {\n\t\tselector.toArray().forEach( ( el ) => list.push( el ) );\n\t}\n\n\treturn list;\n}\n\n/**\n * Locates an existing spinner element which is a direct child of the specified wrapper element.\n *\n * @since 1.1.0\n *\n * @param {Element} wrapper Node element for the wrapper.\n * @return {null|undefined|Element} Returns `null` if no spinners exist within the wrapper, undefined if no spinners are\n *                                  direct descendants of the wrapper, otherwise returns the spinner element.\n */\nexport function find( wrapper ) {\n\tconst spinners = wrapper.querySelectorAll( `.${ WRAPPER_CLASSNAME }` );\n\tif ( ! spinners.length ) {\n\t\treturn null;\n\t}\n\n\treturn Array.from( spinners ).find( ( el ) => wrapper === el.parentNode );\n}\n\n/**\n * Loads CSS styles and appends them to the document's <head>.\n *\n * Attaching CSS directly to the `get()` method means that we don't have to worry about loading CSS files (or relying on CSS) included\n * by the LifterLMS core plugin in order to use this styled component.\n *\n * @since 1.1.0\n *\n * @return {void}\n */\nexport function loadStyles() {\n\tconst STYLE_ID = 'llms-spinner-styles';\n\n\tif ( ! document.getElementById( STYLE_ID ) ) {\n\t\tconst style = document.createElement( 'style' );\n\t\tstyle.textContent = STYLES.replace( /\\n/g, '' )\n\t\t\t.replace( /\\t/g, ' ' )\n\t\t\t.replace( /\\s\\s+/g, ' ' );\n\t\tstyle.id = STYLE_ID;\n\t\tdocument.head.appendChild( style );\n\t}\n}\n"
  },
  {
    "path": "packages/dev/.llmsdev.yml",
    "content": "update-version:\n  skip-config: true\n"
  },
  {
    "path": "packages/dev/.npmrc",
    "content": "package-lock=false\nengine-strict=true\n"
  },
  {
    "path": "packages/dev/CHANGELOG.md",
    "content": "@lifterlms/dev CHANGELOG\n========================\nv0.2.4 - 2025-12-19\n----------\n\n* Avoid deleting the `composer.lock` file.\n\n\nv0.2.3 - 2025-12-19\n----------\n\n* Use `composer install` vs. `composer update` during zip creation.\n\n\nv0.2.2 - 2024-04-18\n----------\n\n* Changes the default to not merge PRs against dev.\n\n\nv0.2.1 - 2023-04-18\n----------\n\n+ Fixed an issue with paths containing spaces on Windows OS.\n+ Fixed an issue when trying to create a folder on Windows OS.\n\n\nv0.2.0 - 2022-10-12\n-------------------\n\n+ When using `update-version` with a prerelease version, certain replacements will be automatically excluded.\n\n\nv0.1.0 - 2022-08-11\n-------------------\n\n+ Updated: [**BREAKING**] During prerelease builds the `readme` command will now exit with code `0` and output a warning message instead of previous behavior: exit code `1` with an error message.\n+ Added: The `readme` command now merges additional merge codes as derived from the `parseMainFileMetadata()` utility function.\n+ Added: A New command `meta` has been added.\n+ Added: `docgen` will now include `beforeHelp` and `afterHelp` text added via `Command.addHelpText()`.\n\n\nv0.0.5 - 2022-05-23\n-------------------\n\n+ Added: `update-version` default replacements will now additionally replace the `[version]` placeholder in the following functions: `_deprecated_argument`, `_deprecated_constructor`, `_deprecated_hook`, `_doing_it_wrong`, `apply_filters_deprecated`, and `do_action_deprecated`.\n+ Added: `update-version` command RegEx lists now accept an optional 3rd item used to specify the `RegExp` flags argument. If not supplied the flags list defaults to `g`.\n\n\nv0.0.4 - 2022-02-15\n-------------------\n\n+ Added: New utility methods for generating links to the project's GitHub repository.\n+ Fixed: Incorrect issue link URL generated during `changelog write` command.\n\n\nv0.0.3 - 2021-12-23\n-------------------\n\n+ Bugfix: [**Breaking**] The short option `-t` for the `--title` option for the `changelog add` command has been changed to `-T`.\n\n\nv0.0.2 - 2021-11-10\n-------------------\n\n+ Added flag `--links` to `changelog write` command in order to allow default flag configuration via the `.yml` config file. The flag is enabled by default for public repos and disabled for private ones.\n+ Fixed an OSX issue encountered in the `changelog write` command resulting from the use of `xargs -d`.\n+ Fixed an issue causing `changelog version next` to fail when passing the `--preid` flag.\n+ Don't provide a default value to the `update-version` command option `--preid`.\n\n\nv0.0.1 - 2021-11-05\n-------------------\n\n+ Initial release.\n"
  },
  {
    "path": "packages/dev/README.md",
    "content": "LifterLMS Dev CLI\n-----------------\n\nA command-line interface (CLI) for LifterLMS contributors and maintainers. This packages provides a reusable set of tools to reduce redundant dev tasks and chores such as release publication, changelog maintenance, pot file generation, and etc...\n\n---\n\n## CHANGELOG\n\n[CHANGELOG](./CHANGELOG.md)\n\n\n## Installation\n\n```bash\nnpm install --save-dev @lifterlms/dev`\n```\n\n\n## Setup\n\nThis packages offers a command-line interfaces and exposes the `llms-dev` binary. It is recommended to add a shorthand script to access the binary by adding a new script to your `package.json` file:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"llms-dev\"\n  }\n}\n```\n\nAfter adding this you may access any of the CLI's commands by running:\n\n```bash\nnpm run dev <command> [options...]\n```\n\n\n## Available Commands and Usage\n\nList available commands by running: `llms-dev help`.\n\nGet help with a specific command by running: `llms-dev help [command]`.\n\n<!-- START TOKEN(Autogenerated API docs) -->\n\n### changelog add\n\n```bash\nUsage: index changelog add [options]\n\nCreate a new changelog entry.\n\nOptions:\n  -s, --significance <level>     The semantic version significance of the\n                                 change. Accepts: major, minor, patch.\n                                 (default: \"patch\")\n  -t, --type <type>              The type of change. Accepts: added, changed,\n                                 fixed, deprecated, removed, dev, performance,\n                                 security. (default: \"changed\")\n  -c, --comment <comment>        An internal-use comment to include with the\n                                 changelog entry which is not published with\n                                 the final changelog.\n  -l, --links <issues...>        Link the changelog to one or more GitHub\n                                 issues. Can be provided multiple times to link\n                                 to multiple issues.\n  -a, --attributions <users...>  Attribute the changelog entry to one or more\n                                 individuals. Attributions are provided to\n                                 thank contributions which originate from\n                                 outside the LifterLMS organization. Provide a\n                                 GitHub username or a markdown-formatted\n                                 anchor. Can be provided multiple times to\n                                 attribute to multiple users.\n  -e, --entry <entry>            The changelog entry.\n  -T, --title <title>            Changelog entry file name. Uses the current\n                                 git branch name as the default. Automatically\n                                 appends a number to the title if the title\n                                 already exists. (default: \"trunk\")\n  -i, --interactive              Create the changelog interactively. (default:\n                                 false)\n  -E, --use-editor               When creating a changelog interactively, will\n                                 open an editor to write the entry, This is\n                                 useful when creating multi-line entries.\n  -d, --dir <directory>          Directory where changelog entries are stored.\n                                 (default: \".changelogs\")\n  -h, --help                     display help for command\n\n```\n\n### changelog list\n\n```bash\nUsage: index changelog list [options]\n\nList existing changelog entries.\n\nOptions:\n  -d, --dir <directory>  Directory where changelog entries are stored.\n                         (default: \".changelogs\")\n  -h, --help             display help for command\n\n```\n\n### changelog validate\n\n```bash\nUsage: index changelog validate [options] [entries...]\n\nValidate existing changelog entries.\n\nArguments:\n  entries                Optionally specify a list of changelog entries to\n                         validate. If omitted will validate all existing\n                         entries.\n\nOptions:\n  -f, --format [format]  Output format. Accepts: list, json, yaml. (default:\n                         \"list\")\n  -s, --silent           Skip validation output and communicate validation\n                         status only through the exit status of the command.\n  -d, --dir <directory>  Directory where changelog entries are stored.\n                         (default: \".changelogs\")\n  -h, --help             display help for command\n\n```\n\n### changelog version\n\n```bash\nUsage: index changelog version [options] <which>\n\nList existing changelog entries.\n\nArguments:\n  which                     Which version to retrieve. Accepts: current, next.\n\nOptions:\n  -p, --preid <identifier>  Identifier to be used to prefix premajor, preminor,\n                            prepatch or prerelease version increments.\n  -d, --dir <directory>     Directory where changelog entries are stored.\n                            (default: \".changelogs\")\n  -h, --help                display help for command\n\n```\n\n### changelog write\n\n```bash\nUsage: index changelog write [options]\n\nWrite existing changelog entries to the changelog file.\n\nOptions:\n  -p, --preid <identifier>  Identifier to be used to prefix premajor, preminor,\n                            prepatch or prerelease version increments.\n  -F, --force <version>     Use the specified version string instead of\n                            determining the version based on changelog entry\n                            significance.\n  -l, --log-file <file>     The changelog file. (default: \"CHANGELOG.md\")\n  -d, --date <YYYY-MM-DD>   Changelog publication date. (default: \"2022-08-11\")\n  -L, --links               Add GitHub links to templates and issues in\n                            changelog entries. (default: false)\n  -n, --no-links            Do not add GitHub links in changelog entries. Use\n                            this option to override the --links flag.\n  -D, --dry-run             Output what would be written to the changelog\n                            instead of writing it to the changelog file.\n  -k, --keep-entries        Preserve entry files deletion after the changelog\n                            is written.\n  -d, --dir <directory>     Directory where changelog entries are stored.\n                            (default: \".changelogs\")\n  -h, --help                display help for command\n\n```\n\n### docgen\n\n```bash\nUsage: index docgen [options]\n\nGenerates documentation for the CLI.\n\nOptions:\n  -h, --help  display help for command\n\n```\n\n### meta parse\n\n```bash\nUsage: index meta parse [options]\n\nRetrieves metadata from the project's main file.\n\nOptions:\n  -F, --file <file>      Main project file name. (default: \"dev.php\")\n  -k, --key <key>        Retrieves a single metadata by key name.\n  -f, --format [format]  Output format. Accepts: table, json, yaml. (default:\n                         \"table\")\n  -h, --help             display help for command\n\n```\n\n### pot\n\n```bash\nUsage: index pot [options]\n\nGenerate i18n pot and json files using the WP-CLI.\n\nOptions:\n  -d, --text-domain <text-domain>  Specify the text domain. Used to generate\n                                   the filenames for generated files. (default:\n                                   \"dev\")\n  -e, --exclude <glob...>          Specify files to exclude from scanning.\n                                   (default: \"vendor/**, node_modules/**,\n                                   tmp/**, dist/**, docs/**, src/**, tests/**,\n                                   *.js.map\")\n  -ee, --extra-exclude <glob...>   Additional files to add to the --exclude\n                                   option.\n  -d, --dir <directory>            Output directory where generated files will\n                                   be stored. (default: \"i18n\")\n  -t, --translator <translator>    Customize the Last Translator header.\n                                   (default: \"Team LifterLMS\n                                   <team@lifterlms.com>\")\n  -b, --bugs <url>                 Customize the bug report location header.\n                                   (default:\n                                   \"https://lifterlms.com/my-account/my-tickets\")\n  -h, --help                       display help for command\n\n```\n\n### readme\n\n```bash\nUsage: index readme [options]\n\nCreate a readme.txt file suitable for the WordPress.org plugin repository.\n\nOptions:\n  -o, --output-file <filename>     Specify the output readme file name.\n                                   (default: \"readme.txt\")\n  -i, --input-file <filename>      Specify the input changelog file name.\n                                   (default: \"CHANGELOG.md\")\n  -m, --main-file <filename>       Specify the project main file name where\n                                   metadata is stored. (default: \"dev.php\")\n  -d, --dir <directory>            Directory where the readme part files are\n                                   stored (default: \".wordpress-org/readme\")\n  -l, --changelog-length <number>  Specify the number of versions to display\n                                   before truncating the changelog. (default:\n                                   10)\n  -r, --read-more <url>            Specify the \"Read More\" url where changelogs\n                                   are published. (default:\n                                   \"https://make.lifterlms.com/tag/dev\")\n  -h, --help                       display help for command\n\nMerge codes:\n  The following merge codes can be used in any of the readme part markdown files.\n\n  | Merge Code                    | Description                                                            | Source       |\n  | ----------------------------- | -----------------------------------------------------------------------| ------------ |\n  | {{__CHANGELOG_ENTRIES__}}     | The most recent 10 changelog entries.                                  | --input-file |\n  | {{__LICENSE__}}               | The project's license (GPLv3).                                         | --main-file  |\n  | {{__LICENSE_URI__}}           | The URI to the project's license.                                      | --main-file  |\n  | {{__MIN_WP_VERSION__}}        | The minimum required WordPress core version.                           | --main-file  |\n  | {{__MIN_LLMS_VERSION__}}      | The minimum required LifterLMS version.                                | --main-file  |\n  | {{__MIN_PHP_VERSION__}}       | The minimum required PHP version.                                      | --main-file  |\n  | {{__PROJECT_URI__}}           | The project's URI.                                                     | --main-file  |\n  | {{__READ_MORE_LINK__}}        | A link to the full project changelog.                                  | --main-file  |\n  | {{__SHORT_DESCRIPTION__}}     | A short description of the project.                                    | --main-file  |\n  | {{__TESTED_LLMS_VERSION__}}   | The latest LifterLMS version the project has been tested against.      | --main-file  |\n  | {{__TESTED_WP_VERSION__}}     | The latest WordPress core version the project has been tested against. | --main-file  |\n  | {{__VERSION__}}               | The current project version.                                           | package.json |\n\t\n\n```\n\n### release archive\n\n```bash\nUsage: index release archive [options]\n\nBuild a distribution archive (.zip) file for the project.\n\nOptions:\n  -i, --inspect    Automatically unzip the zip file after creation. (default:\n                   false)\n  -d, --dir <dir>  Directory where the generated archive file will be saved,\n                   relative to the project root directory. (default: \"dist\")\n  -v, --verbose    Output extra information with result messages. (default:\n                   false)\n  -h, --help       display help for command\n\n```\n\n### release create\n\n```bash\nUsage: index release create [options]\n\nCreate a GitHub release and tag from a specified file or branch.\n\nOptions:\n  -a, --archive <zip>               If specified, the zip file will be\n                                    committed and force-pushed to the specified\n                                    branch before creating the release. Pass\n                                    --no-archive to skip this step. (default:\n                                    \"dev-0.0.5.zip\")\n  -A, --no-archive                  Skip creation from an archive file and use\n                                    the target --branch for release creation.\n  -c, --commit-message <message>    Customize the commit message used when\n                                    pushing to the target branch. Used only\n                                    when releasing from an archive. The\n                                    placeholder \"%s\" is replaced with the\n                                    release version. (default: \"Release v%s [ci\n                                    skip]\")\n  -d, --dir <directory>             Directory where distribution files are\n                                    stored. (default: \"dist\")\n  -b, --branch <branch>             Target branch to use when creating the\n                                    release. (default: \"release\")\n  -l, --logfile <file>              Specify the changelog file. (default:\n                                    \"CHANGELOG.md\")\n  -p, --prerelease                  Mark the GitHub release as a prerelease and\n                                    skip merging.\n  -P, --prerelease-branch <branch>  When creating a prerelease, use this branch\n                                    as the target branch in favor of the\n                                    default branch specified via the --branch\n                                    option. (default: \"prerelease\")\n  -D, --draft                       Create the release as an unpublished draft\n                                    and skip merging.\n  -M, --merge <branch>              Merge open PRs on the specified branch\n                                    before creating the release. If publishing\n                                    a prerelease, or draft merging is\n                                    automatically disabled as if passing\n                                    \"--no-merge\". (default: \"dev\")\n  -n, --no-merge                    Disable merging before release creation.\n                                    Automatically passed when publishing a\n                                    prerelease.\n  -Y, --yes                         Skip confirmations.\n  -v, --verbose                     Output extra information with result\n                                    messages.\n  -h, --help                        display help for command\n\n```\n\n### release prepare\n\n```bash\nUsage: index release prepare [options]\n\nPrepare and build a release.\n\nOptions:\n  -F, --force <version>     Specify a version to use. If not specified uses\n                            `changelog version next` to determine the version.\n  -p, --preid <identifier>  Identifier to be used to prefix premajor, preminor,\n                            prepatch or prerelease version increments.\n  -y, --yes                 Specify no-interaction mode. Responds \"yes\" to all\n                            confirmation prompts.\n  -b, --build <cmd>         Specify an npm script to use for the build command.\n                            (default: \"build\")\n  -B, --no-build            Disabled build script.\n  -h, --help                display help for command\n\n```\n\n### update-version\n\n```bash\nUsage: index update-version [options]\n\nUpdate the project version and replace all [version] placeholders.\n\nOptions:\n  -i, --increment <level>                     Increment the version by the specified level. Accepts: major, minor, patch, premajor, preminor, prepatch, or prerelease. (default: \"patch\")\n  -p, --preid <identifier>                    Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.\n  -F, --force <version>                       Specify an explicit version instead of incrementing the current version with --increment.\n  -r, --replacements <replacement...>]        Replacements to be made. Each replacement is an array containing a list of globs for the files to be tested, a regex used to perform the replacement, and an optional list of RegEx flags (defaults to `g` if not supplied). It is recommended that this argument to configured via a configuration file as opposed to being passed via a CLI flag. (default: [[\"./**\",\"(?<=@(?:since|version|deprecated) +)(\\\\[version\\\\])\"],[\"./*.php,./**/*.php\",\"(?<=(?:_deprecated_argument|_deprecated_constructor|_deprecated_hook|_deprecated_file|_deprecated_function|_doing_it_wrong|apply_filters_deprecated|do_action_deprecated|llms_deprecated_function\\\\().+)(?<=')(\\\\[version\\\\])(?=')\"],[\"*lifterlms*.php\",\"(?<=[Vv]ersion *[:=] *[ '\\\"])(0|[1-9]d*)\\\\.(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)(?:-((?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+([0-9a-zA-Z-]+(?:\\\\.[0-9a-zA-Z-]+)*))?\"],[\"*lifterlms*.php\",\"(?<=define\\\\( '(?:LLMS|LIFTERLMS).*_VERSION', ')(.*)(?=' \\\\);)\"],[\"./style.css\",\"(?<=Version: )(.+)\"]])\n  -e, --extra-replacements <replacement...>]  Additional replacements added to --replacements array. This option allows adding to the default replacements instead of overwriting them. (default: [])\n  -E, --exclude <glob...>                     Specify files to exclude from the update. (default: \"./vendor/**, ./node_modules/**, ./tmp/**, ./dist/**, ./docs/**, ./packages/**\")\n  -s, --skip-config                           Skip updating the version of the package.json or composer.json file. (default: true)\n  -h, --help                                  display help for command\n\n```\n\n<!-- END TOKEN(Autogenerated API docs) -->\n\n\n## Configuration Files\n\nThe default values for all command options may be customized by placing a YAML configuration file in the project's root directory name `.llmsdev.yml`.\n\nThe configuration file may contain any number of objects, the top-level keys correspond to the command being configured. The keys of each object represent the long form (eg: `--option-name`) option flag for the option being configured. When configuring a subcommand, separate the parent and child commands with a dot `.`.\n\nThe following is an annotated configuration example:\n\n```yaml\n# Configure the archive command defaults, eg `llms-dev archive`.\narchive:\n  # Equivalent to always passing `llms-dev archive --inspect`.\n  inspect: true\n\n# Configure the the changelog add command, eg `llms-dev changelog add`\nchangelog.add\n  type: added\n\n```\n"
  },
  {
    "path": "packages/dev/package.json",
    "content": "{\n  \"name\": \"@lifterlms/dev\",\n  \"version\": \"0.2.4\",\n  \"description\": \"Developer's CLI for managing, building, and deploying LifterLMS projects.\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/master/packages/dev\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"scripts\",\n    \"utils\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/dev\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%dev\"\n  },\n  \"bin\": {\n    \"llms-dev\": \"src/index.js\"\n  },\n  \"main\": \"src/index.js\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"engines\": {\n    \"node\": \">=14.14.0\"\n  },\n  \"dependencies\": {\n    \"chalk\": \"^4.1.2\",\n    \"columnify\": \"^1.5.4\",\n    \"commander\": \"^8.2.0\",\n    \"inquirer\": \"^8.2.0\",\n    \"replace-in-file\": \"^6.3.1\",\n    \"semver\": \"^7.3.5\",\n    \"yaml\": \"^2.2.2\"\n  },\n  \"scripts\": {\n    \"docgen\": \"npm run dev docgen\",\n    \"dev\": \"./src/index.js\",\n    \"test\": \"cd ../../ && wp-scripts test-unit-js ./packages/dev --config ./packages/scripts/config/jest-unit.config.js --verbose && cd packages/dev\"\n  }\n}\n"
  },
  {
    "path": "packages/dev/src/.eslintrc.js",
    "content": "module.exports = {\n\trules: {\n\t\t// The CLI program utilizes console to output responses.\n\t\t'no-console': 'off',\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/changelog/add.js",
    "content": "const\n\tinquirer = require( 'inquirer' ),\n\tchalk = require( 'chalk' ),\n\tpath = require( 'path' ),\n\tYAML = require( 'yaml' ),\n\t{ existsSync, mkdirSync, writeFileSync } = require( 'fs' ),\n\t{\n\t\tChangelogEntry,\n\t\tgetChangelogOptions,\n\t\tlogResult,\n\t\texecSync,\n\t\tisAttributionValid,\n\t\tisEntryValid,\n\t\tisLinkValid,\n\t\tgetChangelogValidationIssues,\n\t} = require( '../../utils' ),\n\topts = getChangelogOptions();\n\n/**\n * Generate a list for the given option key.\n *\n * @since 0.0.1\n *\n * @param {string} option Option key.\n * @return {Object[]} Array of objects used for the list.\n */\nfunction generateList( option ) {\n\treturn Object.entries( opts[ option ] )\n\t\t.map( ( [ value, desc ] ) => ( {\n\t\t\tname: `${ value.charAt( 0 ).toUpperCase() }${ value.slice( 1 ) } [${ desc }]`,\n\t\t\tvalue,\n\t\t} ) );\n}\n\n/**\n * Coerces a numeric value to a valid link value.\n *\n * @since 0.0.1\n *\n * @param {any} link User-submitted link value.\n * @return {any} The link as a valid link value if it can be coerced or the user-submitted value if it cannot.\n */\nfunction coerceLink( link ) {\n\treturn ! isNaN( parseInt( link ) ) ? `#${ link }` : link;\n}\n\n/**\n * Create the changelog entry from the given entry object.\n *\n * @since 0.0.1\n *\n * @param {ChangelogEntry} log Changelog entry object.\n * @return {void}\n */\nfunction writeChangelog( log ) {\n\tconst { dir } = log;\n\tlet { title } = log;\n\tdelete log.dir;\n\tdelete log.title;\n\n\tconst logDir = path.join( process.cwd(), dir );\n\tif ( ! existsSync( logDir ) ) {\n\t\tmkdirSync( logDir, { recursive: true } );\n\t}\n\n\tif ( log.links ) {\n\t\tlog.links = log.links.map( coerceLink );\n\t}\n\n\tconst validation = getChangelogValidationIssues( log );\n\tif ( ! validation.valid ) {\n\t\tconst errs = validation.errors.map( ( err ) => `\\n  - ${ err }` ).join( '' );\n\n\t\tlogResult( `The changelog entry could not be written due to validation errors:${ errs }`, 'error' );\n\n\t\tprocess.exit( 1 );\n\t}\n\n\t// Remove optional empty values.\n\tObject.keys( log ).forEach( ( key ) => ( ! log[ key ] || ( Array.isArray( log[ key ] ) && ! log[ key ].length ) ) && delete log[ key ] );\n\n\t// Make sure filenames are unique.\n\tlet i = 1;\n\ttitle = path.join( dir, title );\n\tconst baseTitle = title;\n\twhile ( existsSync( title + '.yml' ) ) {\n\t\ttitle = `${ baseTitle }-${ i }`;\n\t\t++i;\n\t}\n\ttitle += '.yml';\n\n\twriteFileSync( title, YAML.stringify( log ) );\n\n\tlogResult( `New changelog entry written to ${ chalk.bold( title ) }.`, 'success' );\n}\n\nconst defaultTitle = execSync( `git branch --show-current`, true ).replace( '/', '_' );\n\nmodule.exports = {\n\tcommand: 'add',\n\tdescription: 'Create a new changelog entry.',\n\toptions: [\n\t\t[ '-s, --significance <level>', `The semantic version significance of the change. Accepts: ${ Object.keys( opts.significance ).join( ', ' ) }.`, 'patch' ],\n\t\t[ '-t, --type <type>', `The type of change. Accepts: ${ Object.keys( opts.type ).join( ', ' ) }.`, 'changed' ],\n\t\t[ '-c, --comment <comment>', 'An internal-use comment to include with the changelog entry which is not published with the final changelog.' ],\n\t\t[ '-l, --links <issues...>', 'Link the changelog to one or more GitHub issues. Can be provided multiple times to link to multiple issues.' ],\n\t\t[ '-a, --attributions <users...>', 'Attribute the changelog entry to one or more individuals. Attributions are provided to thank contributions which originate from outside the LifterLMS organization. Provide a GitHub username or a markdown-formatted anchor. Can be provided multiple times to attribute to multiple users.' ],\n\t\t[ '-e, --entry <entry>', 'The changelog entry.' ],\n\t\t[ '-T, --title <title>', 'Changelog entry file name. Uses the current git branch name as the default. Automatically appends a number to the title if the title already exists.', defaultTitle ],\n\t\t[ '-i, --interactive', 'Create the changelog interactively.', false ],\n\t\t[ '-E, --use-editor', 'When creating a changelog interactively, will open an editor to write the entry, This is useful when creating multi-line entries.' ],\n\t],\n\taction: ( { significance, type, comment, entry, interactive, links, attributions, dir, title, useEditor } ) => {\n\t\tif ( ! entry && ! interactive ) {\n\t\t\tlogResult( 'A changelog entry is required.', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tif ( interactive ) {\n\t\t\tconst commasToArray = ( arr ) => arr.split( ',' ).filter( ( part ) => part ).map( ( str ) => str.trim() );\n\n\t\t\tconst questions = [\n\t\t\t\t{\n\t\t\t\t\ttype: 'list',\n\t\t\t\t\tname: 'significance',\n\t\t\t\t\tmessage: 'Change Significance',\n\t\t\t\t\tdefault: significance,\n\t\t\t\t\tchoices: generateList( 'significance' ),\n\t\t\t\t\tpageSize: Object.keys( opts.significance ).length,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: 'list',\n\t\t\t\t\tname: 'type',\n\t\t\t\t\tmessage: 'Change Type',\n\t\t\t\t\tdefault: significance,\n\t\t\t\t\tchoices: generateList( 'type' ),\n\t\t\t\t\tpageSize: Object.keys( opts.type ).length,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: 'input',\n\t\t\t\t\tname: 'comment',\n\t\t\t\t\tmessage: 'Comment [For internal use only]',\n\t\t\t\t\tdefault: comment,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: 'input',\n\t\t\t\t\tname: 'links',\n\t\t\t\t\tmessage: 'Linked Issues [Separate multiple issues with a comma]',\n\t\t\t\t\tdefault: links ? links.join( ', ' ) : null,\n\t\t\t\t\tfilter: ( vals ) => commasToArray( vals ).map( coerceLink ),\n\t\t\t\t\tvalidate: ( userVal ) => userVal.every( ( val ) => isLinkValid( val ) ) ? true : chalk.red( 'Error: Invalid link' ),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: 'input',\n\t\t\t\t\tname: 'attributions',\n\t\t\t\t\tmessage: 'Attributions [Separate multiple individuals with a comma]',\n\t\t\t\t\tdefault: attributions ? attributions.join( ', ' ) : null,\n\t\t\t\t\tfilter: commasToArray,\n\t\t\t\t\tvalidate: ( userVal ) => userVal.every( ( val ) => isAttributionValid( val ) ) ? true : chalk.red( 'Error: Invalid attribution' ),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: useEditor ? 'editor' : 'input',\n\t\t\t\t\tname: 'entry',\n\t\t\t\t\tmessage: 'Changelog Entry Content',\n\t\t\t\t\tdefault: entry,\n\t\t\t\t\tvalidate: ( val ) => isEntryValid( val ) ? true : chalk.red( 'Error: Invalid entry.' ),\n\t\t\t\t},\n\t\t\t];\n\n\t\t\tinquirer.prompt( questions )\n\t\t\t\t.then( ( answers ) => writeChangelog( { ...answers, dir, title } ) )\n\t\t\t\t.catch( ( err ) => console.log( err ) );\n\t\t} else {\n\t\t\twriteChangelog( { significance, type, comment, links, attributions, entry, dir, title } );\n\t\t}\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/changelog/index.js",
    "content": "module.exports = {\n\tcommand: 'changelog',\n\tdescription: \"Mange the project's changelog.\",\n\toptionsShared: [\n\t\t[ '-d, --dir <directory>', 'Directory where changelog entries are stored.', '.changelogs' ],\n\t],\n\targs: [\n\t\t[ '<command>', 'The changelog subcommand to execute.' ],\n\t],\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/changelog/list.js",
    "content": "const\n\tchalk = require( 'chalk' ),\n\tcolumnify = require( 'columnify' ),\n\t{ getChangelogEntries, logResult } = require( '../../utils' );\n\nmodule.exports = {\n\tcommand: 'list',\n\tdescription: 'List existing changelog entries.',\n\taction: ( { dir } ) => {\n\t\tconst val = {\n\t\t\tmajor: 2,\n\t\t\tminor: 1,\n\t\t\tpatch: 0,\n\t\t};\n\n\t\tconst entries = getChangelogEntries( dir )\n\t\t\t// Group by significance and then sort by title.\n\t\t\t.sort( ( { significance: aSig, title: aTitle }, { significance: bSig, title: bTitle } ) => {\n\t\t\t\tif ( val[ aSig ] < val[ bSig ] ) {\n\t\t\t\t\treturn 1;\n\t\t\t\t}\n\t\t\t\tif ( val[ aSig ] > val[ bSig ] ) {\n\t\t\t\t\treturn -1;\n\t\t\t\t}\n\t\t\t\treturn aTitle > bTitle ? -1 : 1;\n\t\t\t} )\n\t\t\t.map( ( entry ) => {\n\t\t\t\tif ( 'major' === entry.significance ) {\n\t\t\t\t\tObject.keys( entry ).forEach( ( key ) => entry[ key ] = chalk.bold( entry[ key ] ) );\n\t\t\t\t} else if ( 'patch' === entry.significance ) {\n\t\t\t\t\tObject.keys( entry ).forEach( ( key ) => entry[ key ] = chalk.dim( entry[ key ] ) );\n\t\t\t\t}\n\t\t\t\treturn entry;\n\t\t\t} );\n\n\t\tif ( ! entries.length ) {\n\t\t\tlogResult( 'No changelog entries found.', 'warning' );\n\t\t\tprocess.exit( 0 );\n\t\t}\n\n\t\tconsole.log( columnify(\n\t\t\tentries,\n\t\t\t{\n\t\t\t\theadingTransform: ( heading ) => chalk.bold.underline( heading.toUpperCase() ),\n\t\t\t\tpreserveNewLines: true,\n\t\t\t\ttruncate: true,\n\t\t\t\tmaxWidth: 18,\n\t\t\t\tconfig: {\n\t\t\t\t\tentry: {\n\t\t\t\t\t\tmaxWidth: 40,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t) );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/changelog/validate.js",
    "content": "const\n\tchalk = require( 'chalk' ),\n\tpath = require( 'path' ),\n\tYAML = require( 'yaml' ),\n\t{ getChangelogEntries, getChangelogValidationIssues, logResult } = require( '../../utils' );\n\n/**\n * Retrieve a symbol describing the status type.\n *\n * @since 0.0.1\n *\n * @param {string} type Status type.\n * @return {string} The UTF8 symbol for the requested status.\n */\nfunction getSymbol( type ) {\n\tlet symbol = '';\n\tswitch ( type ) {\n\t\tcase 'error':\n\t\t\tsymbol = chalk.red( '✘' );\n\t\t\tbreak;\n\t\tcase 'success':\n\t\t\tsymbol = chalk.green( '✔' );\n\t\t\tbreak;\n\t\tcase 'warning':\n\t\t\tsymbol = chalk.yellow( '▲' );\n\t\t\tbreak;\n\t}\n\treturn symbol;\n}\n\n/**\n * Log a message with a status symbol prefix.\n *\n * @since 0.0.1\n *\n * @param {string} msg  The message to log.\n * @param {string} type The status type.\n * @return {void}\n */\nfunction logWithSymbol( msg, type ) {\n\tconsole.log( chalk.italic( ` ${ getSymbol( type ) } ${ msg }` ) );\n}\n\n/**\n * Determine the overall status for a given changelog entry\n *\n * @since 0.0.1\n *\n * @param {string[]} errors   Array of encountered error messages.\n * @param {string[]} warnings Array of encountered warning messages.\n * @return {string} The overall status as a string.\n */\nfunction determineOverallStatus( errors, warnings ) {\n\tif ( errors.length ) {\n\t\treturn 'error';\n\t}\n\n\tif ( warnings.length ) {\n\t\treturn 'warning';\n\t}\n\n\treturn 'success';\n}\n\nmodule.exports = {\n\tcommand: 'validate',\n\tdescription: 'Validate existing changelog entries.',\n\targs: [\n\t\t[ '[entries...]', 'Optionally specify a list of changelog entries to validate. If omitted will validate all existing entries.' ],\n\t],\n\toptions: [\n\t\t[ '-f, --format [format]', 'Output format. Accepts: list, json, yaml.', 'list' ],\n\t\t[ '-s, --silent', 'Skip validation output and communicate validation status only through the exit status of the command.' ],\n\t],\n\taction: ( entries, { dir, silent, format } ) => {\n\t\tlet all;\n\n\t\ttry {\n\t\t\tall = getChangelogEntries( dir );\n\t\t} catch ( { name, message } ) {\n\t\t\tlogResult( `${ name }: ${ message }`, 'error' );\n\t\t\tif ( 'YAMLSyntaxError' === name ) {\n\t\t\t\tconsole.log( chalk.red( '       This usually means that one or more existing changelog entries contains invalid YAML.' ) );\n\t\t\t}\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\t// Reduce the list to only the requested entries.\n\t\tif ( entries.length ) {\n\t\t\tall = all.filter( ( { title } ) => entries.includes( path.parse( title ).name ) );\n\t\t}\n\n\t\tconst res = {};\n\n\t\tlet exitStatus = 0;\n\n\t\tall.forEach( ( log ) => {\n\t\t\tconst validation = getChangelogValidationIssues( log, 'list' === format ),\n\t\t\t\t{ errors, warnings } = validation,\n\t\t\t\toverallStatus = determineOverallStatus( errors, warnings );\n\n\t\t\tif ( ! silent && 'list' === format ) {\n\t\t\t\tconsole.log( '' );\n\t\t\t\tconsole.log( `${ getSymbol( overallStatus ) } ${ chalk.bold( log.title ) }` );\n\t\t\t\tconsole.log( chalk.dim( '-'.repeat( log.title.length + 2 ) ) );\n\n\t\t\t\tif ( 'success' === overallStatus ) {\n\t\t\t\t\tconsole.log( '  No issues.' );\n\t\t\t\t}\n\n\t\t\t\terrors.forEach( ( err ) => logWithSymbol( err, 'error' ) );\n\t\t\t\twarnings.forEach( ( warn ) => logWithSymbol( warn, 'warning' ) );\n\t\t\t}\n\n\t\t\tif ( 'error' === overallStatus ) {\n\t\t\t\texitStatus = 1;\n\t\t\t}\n\n\t\t\tres[ log.title ] = validation;\n\t\t} );\n\n\t\tif ( ! silent ) {\n\t\t\tif ( 'json' === format ) {\n\t\t\t\tconsole.log( JSON.stringify( res ) );\n\t\t\t} else if ( 'yaml' === format ) {\n\t\t\t\tconsole.log( YAML.stringify( res ) );\n\t\t\t}\n\t\t}\n\n\t\tprocess.exit( exitStatus );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/changelog/version.js",
    "content": "const\n\tchalk = require( 'chalk' ),\n\t{ getNextVersion, getCurrentVersion, determineVersionIncrement, logResult } = require( '../../utils' );\n\nconst whichOpts = [ 'current', 'next' ];\n\nmodule.exports = {\n\tcommand: 'version',\n\tdescription: 'List existing changelog entries.',\n\targs: [\n\t\t[ '<which>', `Which version to retrieve. Accepts: ${ whichOpts.join( ', ' ) }.` ],\n\t],\n\toptions: [\n\t\t[ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ],\n\t],\n\taction: ( which, { dir, preid } ) => {\n\t\tif ( ! whichOpts.includes( which ) ) {\n\t\t\tlogResult( `Unknown argument: \"${ chalk.bold( which ) }\".`, 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tconst currentVersion = getCurrentVersion();\n\t\tif ( ! currentVersion ) {\n\t\t\tlogResult( 'No current version found.\\n       A version number must defined in the package.json file or in the composer.json file at \".extra.llms.version\".', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tif ( 'current' === which ) {\n\t\t\tconsole.log( currentVersion );\n\t\t\tprocess.exit( 0 );\n\t\t}\n\n\t\tconsole.log( getNextVersion( currentVersion, determineVersionIncrement( dir, currentVersion, preid ), preid ) );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/changelog/write.js",
    "content": "const\n\tpath = require( 'path' ),\n\t{ readFileSync, writeFileSync, readdirSync, rmSync } = require( 'fs' ),\n\tchalk = require( 'chalk' ),\n\tsemver = require( 'semver' ),\n\t{\n\t\tChangelogEntry,\n\t\tgetNextVersion,\n\t\tgetCurrentVersion,\n\t\tgetChangelogOptions,\n\t\tgetChangelogValidationIssues,\n\t\tgetChangelogEntries,\n\t\tgetFileLink,\n\t\tgetIssueLink,\n\t\tisProjectPublic,\n\t\tdetermineVersionIncrement,\n\t\tlogResult,\n\t\texecSync,\n\t} = require( '../../utils' );\n\n/**\n * Accepts a date/time string and converts it to YYYY-MM-DD format used in changelog version titles.\n *\n * @since 0.0.1\n *\n * @param {string|number} date Timestamp or datetime string parseable by `Date.parse()`.\n * @return {string} Date string in YYYY-MM-DD format.\n */\nconst formatDate = ( date ) => new Date( date ).toISOString().split( 'T' )[ 0 ];\n\n/**\n * Retrieve the an array of lines for the changelog entry's header.\n *\n * @since 0.0.1\n *\n * @param {string} version A semver string.\n * @param {string} date    A date string.\n * @return {string[]} Array of lines.\n */\nfunction getHeaderLines( version, date ) {\n\tconst lines = [ `v${ version } - ${ date }` ];\n\tlines.push( '-'.repeat( lines[ 0 ].length ) );\n\n\treturn lines;\n}\n\n/**\n * Retrieve the title for the changelog item's type.\n *\n * @since 0.0.1\n *\n * @param {string} type The changelog item type key.\n * @return {string} The changelog item type title.\n */\nfunction getTypeTitle( type ) {\n\tconst map = {\n\t\tadded: 'New Features',\n\t\tchanged: 'Updates and Enhancements',\n\t\tfixed: 'Bug Fixes',\n\t\tdeprecated: 'Deprecations',\n\t\tremoved: 'Breaking Changes',\n\t\tdev: 'Developer Notes',\n\t\tperformance: 'Performance Improvements',\n\t\tsecurity: 'Security Fixes',\n\t\ttemplate: 'Updated Templates',\n\t};\n\n\treturn `\\n##### ${ map[ type ] }\\n`;\n}\n\n/**\n * Formats a single changelog item.\n *\n * @since 0.0.1\n * @since 0.1.0 Use `getIssueLink()` for generation of issue links.\n * @since 0.2.1 Remove trailing `@` from the GitHub handler when building the contributor's profile URL.\n *\n * @param {ChangelogEntry} args              The changelog entry object.\n * @param {string}         args.entry        The content of the changelog entry.\n * @param {string}         args.type         Entry type.\n * @param {string[]}       args.attributions List of individuals attributed to the entry.\n * @param {string[]}       args.links        of GitHub issues linked to the entry.\n * @param {boolean}        includeLinks      Whether or not to include links.\n * @return {string} The formatted changelog entry line.\n */\nfunction formatChangelogItem( { entry, type, attributions = [], links = [] }, includeLinks ) {\n\tentry = entry.trim();\n\n\t// Entries should always end in a full stop.\n\tif ( 'template' !== type && ! [ '.', '?', '!' ].includes( entry.split( '' ).reverse()[ 0 ] ) ) {\n\t\tentry += '.';\n\t}\n\n\tlet line = '';\n\n\t// Single entry, add a bullet.\n\tif ( ! entry.includes( '\\n' ) ) {\n\t\tline += '+ ';\n\t}\n\n\t// Add the line(s).\n\tline += entry;\n\n\t// Add formatted attribution links.\n\tif ( attributions.length ) {\n\t\tattributions = attributions.map( ( v ) => {\n\t\t\tif ( '@' === v.charAt( 0 ) ) {\n\t\t\t\tv = `[${ v }](https://github.com/${ v.slice( 1 ) })`;\n\t\t\t}\n\t\t\treturn v;\n\t\t} );\n\t\tline += ` Thanks ${ new Intl.ListFormat( 'en', { style: 'long', type: 'conjunction' } ).format( attributions ) }!`;\n\t}\n\n\t// Add issue links.\n\tif ( includeLinks && links.length ) {\n\t\tline += ' ' + links.map( ( iss ) => `[${ iss }](${ getIssueLink( iss ) })` ).join( ', ' );\n\t}\n\n\treturn line;\n}\n\n/**\n * Retrieve a list changelog entry objects for all the template files that have been modified.\n *\n * Compares the current git branch against the `trunk` branch in order to find all files in the `templates/` directory\n * which have been modified.\n *\n * @since 0.0.1\n * @since 0.1.0 Use `getFileLink()` to generate links to the template file.\n *\n * @param {boolean} includeLinks Whether or not the entry items should be formatted as links to the GitHub repository.\n * @param {string}  version      A semver string.\n * @return {ChangelogEntry[]} Array of changelog entry objects.\n */\nfunction getUpdatedTemplates( includeLinks, version ) {\n\ttry {\n\t\treturn execSync( 'git diff --name-only trunk | grep \"^templates/\"', true ).split( '\\n' ).map( ( template ) => {\n\t\t\treturn {\n\t\t\t\ttype: 'template',\n\t\t\t\tentry: includeLinks ? `[${ template }](${ getFileLink( template, version ) })` : template,\n\t\t\t};\n\t\t} );\n\t} catch ( e ) {}\n\treturn [];\n}\n\n/**\n * Format the changelog entry for the given version.\n *\n * @since 0.0.1\n *\n * @param {string}           version A semver string.\n * @param {string}           date    Version release date in YYYY-MM-DD format.\n * @param {ChangelogEntry[]} entries All entry objects to be included.\n * @param {boolean}          links   Whether or not to add links to GitHub issues and templates. For public repos we want to show links, otherwise we don't bother.\n * @return {string[]} Array of lines to be added to the changelog.\n */\nfunction formatChangelogVersionEntry( version, date, entries, links ) {\n\tconst\n\t\tgroups = {},\n\t\t{ type } = getChangelogOptions();\n\n\tObject.keys( type ).forEach( ( groupKey ) => {\n\t\tgroups[ groupKey ] = [];\n\t} );\n\tgroups.template = [];\n\n\t// Add updated template list.\n\tentries = [ ...entries, ...getUpdatedTemplates( links, version ) ];\n\n\tentries.forEach( ( entry ) => {\n\t\tgroups[ entry.type ].push( entry );\n\t} );\n\n\tconst lines = [\n\t\t...getHeaderLines( version, date ),\n\t];\n\n\tObject.entries( groups ).forEach( ( [ groupType, groupEntries ] ) => {\n\t\tif ( ! groupEntries.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\tlines.push( getTypeTitle( groupType ) );\n\t\tgroupEntries.forEach( ( entry ) => {\n\t\t\tlines.push( formatChangelogItem( entry, links ) );\n\t\t} );\n\t} );\n\n\treturn lines;\n}\n\n/**\n * Delete all changelog entry files from the changelog directory.\n *\n * @since 0.0.1\n *\n * @param {string} dir Changelog directory.\n * @return {void}\n */\nfunction cleanupLogs( dir ) {\n\treaddirSync( dir ).forEach( ( file ) => {\n\t\tif ( file.endsWith( '.yml' ) ) {\n\t\t\trmSync( path.join( dir, file ) );\n\t\t}\n\t} );\n}\n\nmodule.exports = {\n\tcommand: 'write',\n\tdescription: 'Write existing changelog entries to the changelog file.',\n\toptions: [\n\t\t[ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ],\n\t\t[ '-F, --force <version>', 'Use the specified version string instead of determining the version based on changelog entry significance.' ],\n\t\t[ '-l, --log-file <file>', 'The changelog file.', 'CHANGELOG.md' ],\n\t\t[ '-d, --date <YYYY-MM-DD>', 'Changelog publication date.', formatDate( Date.now() ) ],\n\t\t[ '-L, --links', 'Add GitHub links to templates and issues in changelog entries.', true === isProjectPublic() ],\n\t\t[ '-n, --no-links', 'Do not add GitHub links in changelog entries. Use this option to override the --links flag.' ],\n\t\t[ '-D, --dry-run', 'Output what would be written to the changelog instead of writing it to the changelog file.' ],\n\t\t[ '-k, --keep-entries', 'Preserve entry files deletion after the changelog is written.' ],\n\t],\n\taction: ( { dir, preid, force, logFile, date, links, dryRun, keepEntries } ) => {\n\t\ttry {\n\t\t\tdate = formatDate( date );\n\t\t} catch ( e ) {\n\t\t\tlogResult( 'Invalid date supplied. Please provide a date in YYYY-MM-DD format.', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tconst currentVersion = getCurrentVersion();\n\t\tif ( ! currentVersion ) {\n\t\t\tlogResult( 'No current version found.\\n       A version number must defined in the package.json file or in the composer.json file at \".extra.llms.version\".', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tconst entries = getChangelogEntries( dir );\n\n\t\tconst areEntriesValid = entries.every( ( entry ) => {\n\t\t\tconst { valid } = getChangelogValidationIssues( entry );\n\t\t\treturn valid;\n\t\t} );\n\n\t\tif ( ! areEntriesValid ) {\n\t\t\tlogResult( 'One or more invalid changelog entries were found. Please resolve all validation issues and try again.', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tlet version = force;\n\n\t\tif ( ! version ) {\n\t\t\tversion = getNextVersion( currentVersion, determineVersionIncrement( dir, currentVersion, preid ), preid );\n\t\t} else if ( ! semver.valid( version ) ) {\n\t\t\tlogResult( `The supplied version string ${ chalk.bold( version ) } is invalid.`, 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tlogResult( `${ dryRun ? 'Generating' : 'Writing' } changelog for version ${ chalk.bold( version ) }.` );\n\n\t\tconst logFileContents = readFileSync( logFile, 'utf8' );\n\n\t\tconst\n\t\t\tlogFileParts = logFileContents.split( '\\n\\n' ),\n\t\t\t[ header, ...body ] = logFileParts,\n\t\t\titems = formatChangelogVersionEntry( version, date, entries, links ).join( '\\n' ) + '\\n';\n\n\t\tif ( dryRun ) {\n\t\t\tconsole.log( items );\n\t\t\tprocess.exit( 0 );\n\t\t}\n\n\t\twriteFileSync( logFile, [ header, items, ...body ].join( '\\n\\n' ) );\n\t\tlogResult( `Changelog for version ${ chalk.bold( version ) } written.` );\n\n\t\tif ( ! keepEntries ) {\n\t\t\tlogResult( `Peforming entry file cleanup`, 'warning' );\n\t\t\tcleanupLogs( dir );\n\t\t}\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/docgen.js",
    "content": "const path = require( 'path' ),\n\t{ Command } = require( 'commander' ), // Including for the type definition.\n\t{ readFileSync, writeFileSync } = require( 'fs' );\n\n/**\n * Generate a doc section for the specified command.\n *\n * @since 0.0.1\n * @since 0.1.0 Include beforeHelp and afterHelp data added by `Command.addHelpText()`.\n *\n * @param {Command} command    A commander command instance.\n * @param {string}  parentName Name of the parent command (used for subcommands).\n * @return {string} Documentation section MD text.\n */\nfunction createCommandSection( command, parentName = '' ) {\n\tparentName = parentName ? `${ parentName } ` : '';\n\n\tconst commandName = command.name();\n\tlet text = '';\n\n\tif ( ! command.commands.length ) {\n\t\ttext = `\\n### ${ parentName }${ commandName }\\n\\n`;\n\n\t\ttext += '```bash\\n';\n\t\tcommand.emit( 'beforeHelp', { write: ( helpStr ) => text += helpStr } );\n\t\ttext += command.helpInformation();\n\t\tcommand.emit( 'afterHelp', { write: ( helpStr ) => text += helpStr } );\n\t\ttext += '\\n```\\n';\n\t} else {\n\t\tcommand.commands.forEach( ( subcommand ) => {\n\t\t\ttext += createCommandSection( subcommand, commandName );\n\t\t} );\n\t}\n\n\treturn text;\n}\n\nmodule.exports = {\n\tcommand: 'docgen',\n\tdescription: 'Generates documentation for the CLI.',\n\taction: ( env, { parent } ) => {\n\t\tconst readmeFile = path.join( __dirname, '../../README.md' ),\n\t\t\treadmeContents = readFileSync( readmeFile, 'utf8' ),\n\t\t\tstartToken = '<!-- START TOKEN(Autogenerated API docs) -->',\n\t\t\tendToken = '<!-- END TOKEN(Autogenerated API docs) -->',\n\t\t\tdocsToken = '<!-- DOCS TOKEN -->';\n\n\t\tlet newReadme = [],\n\t\t\taddLine = true;\n\n\t\treadmeContents.split( '\\n' ).forEach( ( line ) => {\n\t\t\tif ( line === startToken ) {\n\t\t\t\tnewReadme.push( line );\n\t\t\t\tnewReadme.push( docsToken );\n\t\t\t\taddLine = false;\n\t\t\t} else if ( line === endToken ) {\n\t\t\t\taddLine = true;\n\t\t\t}\n\n\t\t\tif ( addLine ) {\n\t\t\t\tnewReadme.push( line );\n\t\t\t}\n\t\t} );\n\n\t\tnewReadme = newReadme.join( '\\n' );\n\n\t\tlet docs = '';\n\t\tparent.commands.forEach( ( command ) => {\n\t\t\tdocs += createCommandSection( command );\n\t\t} );\n\n\t\twriteFileSync( readmeFile, newReadme.replace( docsToken, docs ) );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/meta/index.js",
    "content": "module.exports = {\n\tcommand: 'meta',\n\tdescription: \"Mange the project's metadata.\",\n\toptionsShared: [\n\t],\n\targs: [\n\t\t[ '<command>', 'The meta subcommand to execute.' ],\n\t],\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/meta/parse.js",
    "content": "const\n\tchalk = require( 'chalk' ),\n\tcolumnify = require( 'columnify' ),\n\tYAML = require( 'yaml' ),\n\t{ existsSync } = require( 'fs' ),\n\t{ getProjectSlug, logResult, parseMainFileMetadata } = require( '../../utils' );\n\nmodule.exports = {\n\tcommand: 'parse',\n\tdescription: \"Retrieves metadata from the project's main file.\",\n\toptions: [\n\t\t[ '-F, --file <file>', 'Main project file name.', `${ getProjectSlug() }.php` ],\n\t\t[ '-k, --key <key>', 'Retrieves a single metadata by key name.' ],\n\t\t[ '-f, --format [format]', 'Output format. Accepts: table, json, yaml.', 'table' ],\n\t],\n\taction: ( { file, key, format } ) => {\n\n\t\tif ( ! existsSync( file ) ) {\n\t\t\tlogResult( `Invalid file: \"${ chalk.bold( file ) }\".`, 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tconst metas = parseMainFileMetadata( file );\n\t\t\n\t\tif ( key ) {\n\t\t\tif ( metas[ key ] ) {\n\t\t\t\tconsole.log( metas[ key ] );\n\t\t\t\tprocess.exit( 0 );\n\t\t\t} else {\n\t\t\t\tlogResult( `No metadata found for key name \"${ chalk.bold( key ) }\".`, 'error' );\n\t\t\t\tprocess.exit( 1 );\n\t\t\t}\n\t\t}\n\n\t\tif ( 'json' === format ) {\n\t\t\tconsole.log( JSON.stringify( metas ) );\n\t\t} else if ( 'yaml' === format || 'yml' === format ) {\n\t\t\tconsole.log( YAML.stringify( metas ) );\n\t\t} else if ( 'table' === format ) {\n\t\t\tconsole.log(\n\t\t\t\tcolumnify(\n\t\t\t\t\tmetas,\n\t\t\t\t\t{\n\t\t\t\t\t\theadingTransform: ( heading ) => chalk.bold.underline( heading.toUpperCase() ),\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/pot.js",
    "content": "const\n\tpath = require( 'path' ),\n\t{ Command } = require( 'commander' ), // Including for the type definition.\n\t{ readFileSync, writeFileSync } = require( 'fs' ),\n\t{ execSync, logResult, getProjectSlug } = require( '../utils' ),\n\tdefaultExclude = 'vendor/**, node_modules/**, tmp/**, dist/**, docs/**, src/**, tests/**, *.js.map';\n\n/**\n * Command: pot\n *\n * @since 0.0.1\n *\n * @type {Object}\n */\nmodule.exports = {\n\tcommand: 'pot',\n\tdescription: 'Generate i18n pot and json files using the WP-CLI.',\n\toptions: [\n\t\t[ '-d, --text-domain <text-domain>', 'Specify the text domain. Used to generate the filenames for generated files.', getProjectSlug() ],\n\t\t[ '-e, --exclude <glob...>', 'Specify files to exclude from scanning.', defaultExclude ],\n\t\t[ '-ee, --extra-exclude <glob...>', 'Additional files to add to the --exclude option.' ],\n\t\t[ '-d, --dir <directory>', 'Output directory where generated files will be stored.', 'i18n' ],\n\t\t[ '-t, --translator <translator>', 'Customize the Last Translator header.', 'Team LifterLMS <team@lifterlms.com>' ],\n\t\t[ '-b, --bugs <url>', 'Customize the bug report location header.', 'https://lifterlms.com/my-account/my-tickets' ],\n\t],\n\t/**\n\t * Callback action for the pot command\n\t *\n\t * @since 0.0.1\n\t *\n\t * @param {Object}  options              Command options.\n\t * @param {string}  options.textDomain   Project text domain.\n\t * @param {string}  options.exclude      Comma separated list of globs used to exclude files from the pot file generation.\n\t * @param {string}  options.extraExclude Extra globs to be added to exclude.\n\t * @param {string}  options.dir          Output directory where the generated files will be saved.\n\t * @param {string}  options.translator   Translator name and email.\n\t * @param {string}  options.bugs         Bug report URL.\n\t * @param {Command} command              The command instance.\n\t * @param {Command} command.parent       The command's parent command.\n\t * @return {void}\n\t */\n\taction: ( { textDomain, exclude, extraExclude, dir, translator, bugs }, { parent } ) => {\n\t\t// Ensure WP-CLI is available.\n\t\ttry {\n\t\t\texecSync( 'which wp', true );\n\t\t} catch ( e ) {\n\t\t\tlogResult( 'WP-CLI must be installed in your $PATH in order to use this command.', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tconst\n\t\t\t// Replace the WP CLI generator with our own generator string.\n\t\t\tgenerator = `llms/dev ${ parent.version() }`,\n\t\t\t// Get the year of the first commit to the repo.\n\t\t\tinitYear = parseInt( execSync( 'git log --reverse --format=\"format:%cd\" --date=\"format:%Y\" | sed -n 1p', true ) ),\n\t\t\tcurrDate = new Date(),\n\t\t\tcurrYear = currDate.getFullYear(),\n\t\t\tpot = path.join( dir, `${ textDomain }.pot` ),\n\t\t\t// Custom Headers.\n\t\t\theaders = {\n\t\t\t\t'Last-Translator': translator,\n\t\t\t\t'Language-Team': translator,\n\t\t\t\t'Report-Msgid-Bugs-To': bugs,\n\t\t\t\t'X-Generator': generator,\n\t\t\t};\n\n\t\t// Add extra exclude globs, if defined.\n\t\tif ( extraExclude ) {\n\t\t\texclude = exclude + ', ' + extraExclude;\n\t\t}\n\n\t\t// Update excludes glob formatting to a format acceptable by WP CLI.\n\t\texclude = exclude.replace( /\\/\\*\\*/g, '/' ).replace( /\\.\\//g, '' );\n\n\t\tconst cmdOpts = `--exclude=\"${ exclude }\" --headers='${ JSON.stringify( headers ) }'`;\n\n\t\t// Generate the POT file.\n\t\texecSync( `wp i18n make-pot ./ ${ pot } ${ cmdOpts }` );\n\n\t\t// Get the original header comment.\n\t\tlet headerComment = execSync( `head -2 ${ pot }`, true ),\n\t\t\tpotContents = readFileSync( pot ).toString();\n\n\t\t// If the initial commit date is not equal to the current year, update the copyright to include the date range.\n\t\tif ( initYear !== currYear ) {\n\t\t\tpotContents = potContents.replace( headerComment, '' );\n\t\t\theaderComment = headerComment.replace( `(C) ${ currYear }`, `(C) ${ initYear }-${ currYear }` );\n\t\t}\n\n\t\t// Write the header back to the file.\n\t\twriteFileSync( pot, headerComment + potContents );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/readme.js",
    "content": "const\n\tpath = require( 'path' ),\n\tchalk = require( 'chalk' ),\n\tsemver = require( 'semver' ),\n\t{ readdirSync, readFileSync, writeFileSync } = require( 'fs' ),\n\t{ parseChangelogFile, parseMainFileMetadata, getCurrentVersion, logResult, getProjectSlug } = require( '../utils' );\n\n/**\n * Generate the truncated changelog section content.\n *\n * @since 0.0.1\n *\n * @param {string} file   Changelog file.\n * @param {number} length Number of versions to include.\n * @return {string} The truncated changelog section content.\n */\nfunction getChangelogSection( file, length ) {\n\tconst entries = parseChangelogFile( file ),\n\t\ttotal = entries.length,\n\t\tlines = [];\n\n\tlet i = 0,\n\t\tadded = 0;\n\n\twhile ( added < length && i < total ) {\n\t\tconst currLog = entries[ i ];\n\n\t\t// Don't add prereleases.\n\t\tif ( ! semver.prerelease( currLog.version ) ) {\n\t\t\tlines.push( `= v${ currLog.version } - ${ currLog.date } =\\n\\n` );\n\t\t\tlines.push( currLog.logs );\n\n\t\t\t++added;\n\n\t\t\tif ( added < length ) {\n\t\t\t\tlines.push( '\\n\\n\\n' );\n\t\t\t}\n\t\t}\n\n\t\t++i;\n\t}\n\n\treturn lines.join( '' );\n}\n\n/**\n * Generates the merge code help text.\n *\n * @since 0.1.0\n *\n * @return {string} Help text.\n */\nfunction getMergeCodeHelp() {\n\n\treturn `\nMerge codes:\n  The following merge codes can be used in any of the readme part markdown files.\n\n  | Merge Code                    | Description                                                            | Source       |\n  | ----------------------------- | -----------------------------------------------------------------------| ------------ |\n  | {{__CHANGELOG_ENTRIES__}}     | The most recent 10 changelog entries.                                  | --input-file |\n  | {{__LICENSE__}}               | The project's license (GPLv3).                                         | --main-file  |\n  | {{__LICENSE_URI__}}           | The URI to the project's license.                                      | --main-file  |\n  | {{__MIN_WP_VERSION__}}        | The minimum required WordPress core version.                           | --main-file  |\n  | {{__MIN_LLMS_VERSION__}}      | The minimum required LifterLMS version.                                | --main-file  |\n  | {{__MIN_PHP_VERSION__}}       | The minimum required PHP version.                                      | --main-file  |\n  | {{__PROJECT_URI__}}           | The project's URI.                                                     | --main-file  |\n  | {{__READ_MORE_LINK__}}        | A link to the full project changelog.                                  | --main-file  |\n  | {{__SHORT_DESCRIPTION__}}     | A short description of the project.                                    | --main-file  |\n  | {{__TESTED_LLMS_VERSION__}}   | The latest LifterLMS version the project has been tested against.      | --main-file  |\n  | {{__TESTED_WP_VERSION__}}     | The latest WordPress core version the project has been tested against. | --main-file  |\n  | {{__VERSION__}}               | The current project version.                                           | package.json |\n\t`;\n}\n\n/**\n * Command: readme\n *\n * @since 0.0.1\n * @since 0.1.0 Added the `--main-file` option as well as additional merge codes derived from metadata found in the main file's header comment.\n *              Exits with a warning and exit code `0` (instead of an error and exit code `1`) when running this command against a prerelease. \n *\n * @type {Object}\n */\nmodule.exports = {\n\tcommand: 'readme',\n\tdescription: 'Create a readme.txt file suitable for the WordPress.org plugin repository.',\n\toptions: [\n\t\t[ '-o, --output-file <filename>', 'Specify the output readme file name.', 'readme.txt' ],\n\t\t[ '-i, --input-file <filename>', 'Specify the input changelog file name.', 'CHANGELOG.md' ],\n\t\t[ '-m, --main-file <filename>', 'Specify the project main file name where metadata is stored.', `${ getProjectSlug() }.php` ],\n\t\t[ '-d, --dir <directory>', 'Directory where the readme part files are stored', '.wordpress-org/readme' ],\n\t\t[ '-l, --changelog-length <number>', 'Specify the number of versions to display before truncating the changelog.', 10 ],\n\t\t[ '-r, --read-more <url>', 'Specify the \"Read More\" url where changelogs are published.', `https://make.lifterlms.com/tag/${ getProjectSlug() }` ],\n\t],\n\thelp: [\n\t\t[ 'after', getMergeCodeHelp() ]\n\t],\n\taction: ( { outputFile, inputFile, mainFile, dir, readMore, changelogLength } ) => {\n\t\tconst version = getCurrentVersion();\n\n\t\t// Don't generate readme files for pre-releases.\n\t\tif ( semver.prerelease( version ) ) {\n\t\t\tlogResult( 'Cannot generate a readme for prereleases.', 'warning' );\n\t\t\tprocess.exit( 0 );\n\t\t}\n\n\t\tconst metas = parseMainFileMetadata( mainFile );\n\n\t\tconst replacements = {\n\t\t\t\tPROJECT_URI: metas['Plugin URI'] ?? '',\n\t\t\t\tLICENSE: metas['License'] ?? '',\n\t\t\t\tLICENSE_URI: metas['License URI'] ?? '',\n\t\t\t\tMIN_WP_VERSION: metas['Requires at least'] ?? '',\n\t\t\t\tTESTED_WP_VERSION: metas['Tested up to'] ?? '',\n\t\t\t\tMIN_LLMS_VERSION: metas['LLMS requires at least'] ?? '',\n\t\t\t\tTESTED_LLMS_VERSION: metas['LLMS tested up to'] ?? '',\n\t\t\t\tMIN_PHP_VERSION: metas['Requires PHP'] ?? '',\n\t\t\t\tSHORT_DESCRIPTION: metas['Description'] ?? '',\n\t\t\t\tVERSION: version,\n\t\t\t\tCHANGELOG_ENTRIES: getChangelogSection( inputFile, changelogLength ),\n\t\t\t\tREAD_MORE_LINK: readMore,\n\t\t\t},\n\t\t\tfiles = readdirSync( dir );\n\n\t\tlet readme = '';\n\n\t\tfiles.forEach( ( filename, i ) => {\n\t\t\tconst file = readFileSync( path.join( dir, filename ), 'utf8' );\n\n\t\t\treadme += file;\n\n\t\t\t// Add newlines if it's not the last section.\n\t\t\tif ( files.length - 1 !== i ) {\n\t\t\t\treadme += '\\n\\n';\n\t\t\t}\n\t\t} );\n\n\t\t// Replace variables.\n\t\tObject.keys( replacements ).forEach( ( varname ) => {\n\t\t\treadme = readme.replace( new RegExp( `{{__${ varname }__}}`, 'g' ), replacements[ varname ] );\n\t\t} );\n\n\t\twriteFileSync( outputFile, readme );\n\n\t\tlogResult( `Generated ${ chalk.bold( outputFile ) } for version ${ chalk.bold( version ) }.`, 'success' );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/release/archive.js",
    "content": "const\n\tchalk = require( 'chalk' ),\n\t{ createDistFile, execSync, logResult } = require( '../../utils' );\n\n/**\n * Build distribution archive.\n *\n * @since 0.0.1\n * @since 0.2.1 Windows compatibility: account for spaces in file paths.\n *\n * @return {string}\n */\nmodule.exports = {\n\tcommand: 'archive',\n\tdescription: 'Build a distribution archive (.zip) file for the project.',\n\toptions: [\n\t\t[ '-i, --inspect', 'Automatically unzip the zip file after creation.', false ],\n\t\t[ '-d, --dir <dir>', 'Directory where the generated archive file will be saved, relative to the project root directory.', 'dist' ],\n\t\t[ '-v, --verbose', 'Output extra information with result messages.', false ],\n\t],\n\taction: ( { inspect, dir, verbose } ) => {\n\t\tconst distDir = `${ process.cwd() }/${ dir }`,\n\t\t\tfileName = createDistFile(\n\t\t\t\tdistDir,\n\t\t\t\t! verbose,\n\t\t\t\t( msg ) => logResult( msg, 'info' )\n\t\t\t);\n\t\t// Unzip the archive for inspection.\n\t\tif ( inspect ) {\n\t\t\texecSync( `unzip \"${ fileName }\"`, ! verbose, { cwd: distDir } );\n\t\t}\n\n\t\tlogResult( `Distribution file ${ chalk.bold( fileName ) } created successfully.`, 'success' );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/release/create.js",
    "content": "const\n\tpath = require( 'path' ),\n\t{ existsSync, writeFileSync } = require( 'fs' ),\n\tchalk = require( 'chalk' ),\n\tinquirer = require( 'inquirer' ),\n\t{ getCurrentVersion, getChangelogForVersion, getArchiveFilename, logResult, pushDistFile, execSync } = require( '../../utils' );\n\n/**\n * Create a temporary changelog file used to add the changelog to the GitHub release.\n *\n * @since 0.0.1\n *\n * @param {string} version Semver string for the version being published.\n * @param {string} logfile Path to the the changelog file.\n * @return {string} Path to the temporary notes file.\n */\nfunction writeTempNotesFile( version, logfile ) {\n\tconst { date, logs } = getChangelogForVersion( version, logfile ),\n\t\ttmpFile = path.join( process.cwd(), 'tmp', 'release-notes.txt' );\n\n\tlet header = `v${ version } - ${ date }`;\n\theader = `${ header }\\n${ '-'.repeat( header.length ) }`;\n\n\twriteFileSync( tmpFile, `${ header }\\n\\n${ logs }` );\n\n\treturn tmpFile;\n}\n\n\nmodule.exports = {\n\tcommand: 'create',\n\tdescription: 'Create a GitHub release and tag from a specified file or branch.',\n\toptions: [\n\t\t[ '-a, --archive <zip>', 'If specified, the zip file will be committed and force-pushed to the specified branch before creating the release. Pass --no-archive to skip this step.', getArchiveFilename() ],\n\t\t[ '-A, --no-archive', 'Skip creation from an archive file and use the target --branch for release creation.' ],\n\t\t[ '-c, --commit-message <message>', 'Customize the commit message used when pushing to the target branch. Used only when releasing from an archive. The placeholder \"%s\" is replaced with the release version.', 'Release v%s [ci skip]' ],\n\t\t[ '-d, --dir <directory>', 'Directory where distribution files are stored.', 'dist' ],\n\t\t[ '-b, --branch <branch>', 'Target branch to use when creating the release.', 'release' ],\n\t\t[ '-l, --logfile <file>', 'Specify the changelog file.', 'CHANGELOG.md' ],\n\t\t[ '-p, --prerelease', 'Mark the GitHub release as a prerelease and skip merging.' ],\n\t\t[ '-P, --prerelease-branch <branch>', 'When creating a prerelease, use this branch as the target branch in favor of the default branch specified via the --branch option.', 'prerelease' ],\n\t\t[ '-D, --draft', 'Create the release as an unpublished draft and skip merging.' ],\n\t\t[ '-M, --merge <branch>', 'Merge open PRs on the specified branch before creating the release. If publishing a prerelease, or draft merging is automatically disabled as if passing \"--no-merge\".', '' ],\n\t\t[ '-n, --no-merge', 'Disable merging before release creation. Automatically passed when publishing a prerelease.' ],\n\t\t[ '-Y, --yes', 'Skip confirmations.' ],\n\t\t[ '-v, --verbose', 'Output extra information with result messages.' ],\n\t],\n\t/**\n\t * The `create` command.\n\t *\n\t * @since 0.0.1\n\t * @since 0.2.1 Windows compatibility: account for spaces in file paths.\n\t *\n\t * @return {void}\n\t */\n\taction: async ( { archive, dir, commitMessage, branch, logfile, prerelease, prereleaseBranch, draft, merge, yes, verbose } ) => {\n\t\t// Ensure the CLI is installed before proceeding.\n\t\ttry {\n\t\t\texecSync( 'which gh', true );\n\t\t} catch ( Error ) {\n\t\t\tlogResult( 'The GitHub CLI client \"gh\" must be installed to use this command.', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\t// If there's untracked files or the working tree is dirty.\n\t\tif ( execSync( 'git status -s', true ) ) {\n\t\t\tlogResult( 'The working tree must be clean before publishing.', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tif ( archive ) {\n\t\t\tarchive = path.join( dir, archive );\n\n\t\t\tif ( ! existsSync( archive ) ) {\n\t\t\t\tlogResult( `The distribution file ${ chalk.bold( archive ) } doesn't exist.`, 'error' );\n\t\t\t\tprocess.exit( 1 );\n\t\t\t}\n\t\t}\n\n\t\tconst version = getCurrentVersion();\n\n\t\tcommitMessage = commitMessage.replace( '%s', version );\n\n\t\t// Use the prerelease branch when publishing a prerelease.\n\t\tif ( prerelease ) {\n\t\t\tbranch = prereleaseBranch;\n\t\t\tmerge = false;\n\t\t}\n\n\t\t// Disable merging if publishing a draft.\n\t\tif ( draft ) {\n\t\t\tmerge = false;\n\t\t}\n\n\t\t// Output information and confirm the release (unless `--yes` is passed);\n\t\tif ( ! yes ) {\n\t\t\tlogResult( `About to publish a new ${ prerelease ? 'prerelease' : 'release' }${ draft ? ' (draft)' : '' }:`, 'warning' );\n\t\t\tlogResult( `${ chalk.bold( version ) }`, ' + Version' );\n\t\t\tif ( archive ) {\n\t\t\t\tlogResult( `${ chalk.bold( archive ) }`, ' + Archive' );\n\t\t\t}\n\t\t\tlogResult( `${ chalk.bold( branch ) }`, ' + Branch' );\n\t\t\tif ( merge ) {\n\t\t\t\tlogResult( `${ chalk.bold( merge ) }`, ' + Merge from branch' );\n\t\t\t}\n\n\t\t\tyes = await inquirer.prompt( [ {\n\t\t\t\ttype: 'expand',\n\t\t\t\tmessage: 'Are you sure you wish to proceed?',\n\t\t\t\tname: 'yes',\n\t\t\t\tchoices: [\n\t\t\t\t\t{\n\t\t\t\t\t\tkey: 'y',\n\t\t\t\t\t\tname: 'Yes',\n\t\t\t\t\t\tvalue: true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tkey: 'n',\n\t\t\t\t\t\tname: 'No',\n\t\t\t\t\t\tvalue: false,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t} ] )\n\t\t\t\t.then( ( answers ) => answers.yes )\n\t\t\t\t.catch( ( err ) => console.log( err ) );\n\t\t}\n\n\t\tif ( ! yes ) {\n\t\t\tlogResult( 'Release aborted.', 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tlogResult( `Releasing version ${ chalk.bold( version ) } to the ${ chalk.bold( branch ) } branch.` );\n\n\t\t// Push the distfile to the release branch.\n\t\tif ( archive ) {\n\t\t\tpushDistFile( archive, branch, commitMessage, ! verbose );\n\t\t}\n\n\t\t// Merge open PRs against the specified branch.\n\t\tif ( merge ) {\n\t\t\texecSync( `gh pr merge ${ merge } --merge`, true );\n\t\t}\n\n\t\t// Setup the release to pass to the GH CLI.\n\t\tconst\n\t\t\tnotesFile = writeTempNotesFile( version, logfile ),\n\t\t\tcreateArgs = [];\n\n\t\tif ( archive ) {\n\t\t\tcreateArgs.push( archive );\n\t\t}\n\n\t\tcreateArgs.push( `--title \"Version ${ version }\"` );\n\t\tcreateArgs.push( `--target ${ branch }` );\n\t\tcreateArgs.push( `--notes-file \"${ notesFile }\"` );\n\n\t\tif ( draft ) {\n\t\t\tcreateArgs.push( '--draft' );\n\t\t}\n\n\t\tif ( prerelease ) {\n\t\t\tcreateArgs.push( '--prerelease' );\n\t\t}\n\n\t\t// Create the release.\n\t\tconst res = execSync( `gh release create ${ version } ${ createArgs.join( ' ' ) }`, true );\n\t\tlogResult( `Release v${ chalk.bold( version ) } published. Permalink: ${ chalk.underline( res ) }.` );\n\n\t\t// Cleanup the tmp notesfile.\n\t\texecSync( `rm \"${ notesFile }\"` );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/release/index.js",
    "content": "module.exports = {\n\tcommand: 'release',\n\tdescription: 'Prepare and deploy releases.',\n\targs: [\n\t\t[ '<command>', 'The changelog subcommand to execute.' ],\n\t],\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/release/prepare.js",
    "content": "const\n\tchalk = require( 'chalk' ),\n\tinquirer = require( 'inquirer' ),\n\tsemver = require( 'semver' ),\n\t{ execSync, logResult } = require( '../../utils' );\n\n/**\n * Call the cli from within the cli.\n *\n * @since 0.0.1\n * @since 0.2.1 Account for spaces in the file paths.\n *\n * @param {string}  cmd    CLI command and options.\n * @param {boolean} silent If `true`, silence STDOUT.\n * @return {?string} The STDOUT content if `silent` is `true`, otherwise `null`.\n */\nfunction callSelf( cmd, silent = true ) {\n\tconst [ node, cli ] = process.argv;\n\tlet ret = null;\n\ttry {\n\t\tret = execSync( `\"${ node }\" \"${ cli }\" ${ cmd }`, silent );\n\t} catch ( e ) {\n\t\tlogResult( `${ e.type }: ${ e.message }.`, 'error' );\n\t\tconsole.error( e );\n\t\tprocess.exit( 1 );\n\t}\n\treturn ret;\n}\n\n/**\n * Open a CLI prompt and await user confirmation.\n *\n * @since 0.0.1\n *\n * @param {string}  message Message to prompt for confirmation.\n * @param {boolean} skip    If true, the script is being run with `--yes` and no prompt should be made.\n * @return {Promise} Returns a promise from the inquirer prompt.\n */\nfunction prompt( message, skip = false ) {\n\tif ( skip ) {\n\t\treturn true;\n\t}\n\n\tconst questions = [\n\t\t{\n\t\t\ttype: 'confirm',\n\t\t\tname: 'confirm',\n\t\t\tmessage,\n\t\t\tdefault: true,\n\t\t},\n\t];\n\n\treturn inquirer.prompt( questions )\n\t\t.then( ( { confirm } ) => confirm );\n}\n\nmodule.exports = {\n\tcommand: 'prepare',\n\tdescription: 'Prepare and build a release.',\n\toptions: [\n\t\t[ '-F, --force <version>', 'Specify a version to use. If not specified uses `changelog version next` to determine the version.' ],\n\t\t[ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ],\n\t\t[ '-y, --yes', 'Specify no-interaction mode. Responds \"yes\" to all confirmation prompts.' ],\n\t\t[ '-b, --build <cmd>', 'Specify an npm script to use for the build command.', 'build' ],\n\t\t[ '-B, --no-build', 'Disabled build script.' ],\n\t],\n\taction: async ( { force, preid, build, yes } ) => {\n\t\tpreid = preid ? ` --preid ${ preid }` : '';\n\n\t\t// Prepare release version.\n\t\tconst version = force ? force : callSelf( `changelog version next${ preid }` );\n\n\t\tif ( ! semver.valid( version ) ) {\n\t\t\tlogResult( `The supplied version string ${ chalk.bold( version ) } is invalid.`, 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\t// Confirm version.\n\t\tif ( ! await prompt( `Proceed using version ${ chalk.bold( version ) }?`, yes ) ) {\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\t// Get the changelog.\n\t\tif ( ! yes ) {\n\t\t\tcallSelf( `changelog write --dry-run --force ${ version }`, false );\n\t\t\tif ( ! await prompt( 'Use the above output for the changelog and build the release?' ) ) {\n\t\t\t\tprocess.exit( 1 );\n\t\t\t}\n\t\t}\n\n\t\t// Update files.\n\t\tcallSelf( `changelog write --force ${ version }`, false );\n\n\t\t// Update version.\n\t\tcallSelf( `update-version --force ${ version }` );\n\n\t\t// Build.\n\t\tif ( build ) {\n\t\t\texecSync( `npm run ${ build }` );\n\t\t}\n\n\t\tlogResult( `Release preparation for version ${ chalk.bold( version ) } complete.`, 'success' );\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/cmds/update-version.js",
    "content": "const\n\tchalk = require( 'chalk' ),\n\tsemver = require( 'semver' ),\n\tcolumnify = require( 'columnify' ),\n\treplace = require( 'replace-in-file' ),\n\t{ writeFileSync } = require( 'fs' ),\n\t{ getCurrentVersion, getNextVersion, logResult, getConfig, hasConfig, execSync } = require( '../utils' );\n\n/**\n * Update [version] placeholders via a regex against a list of file globs\n *\n * @since 0.0.1\n * @since 0.0.5 Added `flags` argument to allow customization of the regex flags.\n *\n * @param {string} files  Comma separated list of file globs.\n * @param {regex}  regex  A regular expression to use for the replacements.\n * @param {string  flags  A regex options string.\n * @param {string} ignore A comma separated list of file globs to be ignored.\n * @param {string} ver   The semantic version string to replace the placeholder with.\n * @return {Object} Replacement result object from `replace.sync()`.\n */\nfunction updateVersions( files, regex, flags, ignore, ver ) {\n\tconst commasToArray = ( string ) => string.split( ',' ).map( ( s ) => s.trim() );\n\n\tfiles = commasToArray( files );\n\n\tlogResult( `Replacing ${ chalk.bold( files ) } using regex ${ chalk.bold( regex ) }.` );\n\n\tconst\n\t\topts = {\n\t\t\tfiles,\n\t\t\tfrom: new RegExp( regex, flags ),\n\t\t\tto: ver,\n\t\t\tignore: ignore ? commasToArray( ignore ) : null,\n\t\t\tcountMatches: true,\n\t\t};\n\n\treturn replace.sync( opts );\n}\n\n/**\n * Updates the version number in the package's config file.\n *\n * If a package.json file is present, uses `npm version` to update the project's version.\n *\n * If there is no package.json, will attempt to update the `extra.llms.version` item in the\n * project's composer.json file.\n *\n * @since 0.0.1\n *\n * @param {string} ver Semantic version string.\n * @return {Object} A replacement result string.\n */\nfunction updateConfig( ver ) {\n\tconst ret = {\n\t\tMatches: chalk.yellow( 1 ),\n\t\tReplacements: chalk.yellow( 1 ),\n\t};\n\n\tif ( hasConfig( 'package' ) ) {\n\t\t// Silence update errors. When updating new files and the package has already been updated the CLI throws an error which we can ignore.\n\t\ttry {\n\t\t\tlogResult( 'Updating package.json.' );\n\t\t\texecSync( `npm version --no-git-tag-version ${ ver }`, true );\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tFile: chalk.green( 'package.json' ),\n\t\t\t\t\t...ret,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFile: chalk.green( 'package-lock.json' ),\n\t\t\t\t\t...ret,\n\t\t\t\t},\n\t\t\t];\n\t\t} catch ( e ) {}\n\t} else if ( hasConfig( 'composer' ) ) {\n\t\tconst composer = getConfig( 'composer' );\n\t\tif ( composer?.extra?.llms?.version ) {\n\t\t\tlogResult( 'Updating composer.json.' );\n\t\t\tcomposer.extra.llms.version = ver;\n\t\t\twriteFileSync( `${ process.cwd() }/composer.json`, JSON.stringify( composer, null, 2 ) );\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tFile: chalk.green( 'composer.json' ),\n\t\t\t\t\t...ret,\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\t}\n\n\treturn false;\n}\n\nconst deprecatedFunctions = [\n\t// WP deprecation warnings.\n\t'_deprecated_argument',\n\t'_deprecated_constructor',\n\t'_deprecated_hook',\n\t'_deprecated_file',\n\t'_deprecated_function',\n\n\t// Doing it wrong warning.\n\t'_doing_it_wrong',\n\n\t// Deprecated hook callers.\n\t'apply_filters_deprecated',\n\t'do_action_deprecated',\n\t\n\t// LLMS deprecation warnings.\n\t'llms_deprecated_function',\n].join( '|' );\n\n\nconst defaultReplacements = [\n\t// 1. Replace [version] placeholder in all @since, @version, and @deprecated tags.\n\t[ \n\t\t'./**',\n\t\t'(?<=@(?:since|version|deprecated) +)(\\\\[version\\\\])',\n\t\t'g',\n\t\tfalse,\n\t],\n\n\t// 2. Replace [version] placeholder in all deprecate function methods tags.\n\t[ \n\t\t'./*.php,./**/*.php',\n\t\t`(?<=(?:${ deprecatedFunctions }\\\\().+)(?<=\\')(\\\\[version\\\\])(?=\\')`,\n\t\t'g',\n\t\tfalse,\n\t],\n\n\t// 3. Replace plugin metadata \"Version\" with current version.\n\t[ \n\t\t'*lifterlms*.php',\n\t\t'(?<=[Vv]ersion *[:=] *[ \\'\\\"])(0|[1-9]\\d*)\\\\.(0|[1-9]\\\\d*)\\\\.(0|[1-9]\\\\d*)(?:-((?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+([0-9a-zA-Z-]+(?:\\\\.[0-9a-zA-Z-]+)*))?',\n\t\t'g',\n\t\ttrue,\n\t],\n\n\t// 4. Replace LIFTERLMS*_VERSION constants with the current version.\n\t[ \n\t\t'*lifterlms*.php',\n\t\t'(?<=define\\\\( \\'(?:LLMS|LIFTERLMS).*_VERSION\\', \\')(.*)(?=\\' \\\\);)',\n\t\t'g',\n\t\ttrue,\n\t],\n\n\t// 5. Replace theme stylesheet's version number with the current version.\n\t[ \n\t\t'./style.css',\n\t\t'(?<=Version: )(.+)',\n\t\t'g',\n\t\ttrue,\n\t],\n];\n\n/**\n * Command: update-version\n *\n * @since 0.0.1\n * @since 0.2.0 Added 4th parameter to the replacement arrays to allow excluding replacement sets when building a prerelease.\n *\n * @type {Object}\n */\nmodule.exports = {\n\tcommand: 'update-version',\n\tdescription: 'Update the project version and replace all [version] placeholders.',\n\toptions: [\n\t\t[ '-i, --increment <level>', 'Increment the version by the specified level. Accepts: major, minor, patch, premajor, preminor, prepatch, or prerelease.', 'patch' ],\n\t\t[ '-p, --preid <identifier>', 'Identifier to be used to prefix premajor, preminor, prepatch or prerelease version increments.' ],\n\t\t[ '-F, --force <version>', 'Specify an explicit version instead of incrementing the current version with --increment.' ],\n\t\t[ '-r, --replacements <replacement...>]', 'Replacements to be made. Each replacement is an array containing a list of globs for the files to be tested, a regex used to perform the replacement, an optional list of RegEx flags (defaults to `g` if not supplied), and a boolean used to specify if the replacement should be run when building a prerelease (defaults to `false` if not supplied). It is recommended that this argument to configured via a configuration file as opposed to being passed via a CLI flag.', defaultReplacements ],\n\t\t[ '-e, --extra-replacements <replacement...>]', 'Additional replacements added to --replacements array. This option allows adding to the default replacements instead of overwriting them.', [] ],\n\t\t[ '-E, --exclude <glob...>', 'Specify files to exclude from the update.', './vendor/**, ./node_modules/**, ./tmp/**, ./dist/**, ./docs/**, ./packages/**' ],\n\t\t[ '-s, --skip-config', 'Skip updating the version of the package.json or composer.json file.' ],\n\t],\n\taction: ( { increment, preid, exclude, force, skipConfig, replacements, extraReplacements } ) => {\n\t\tconst version = force ? force : getNextVersion( getCurrentVersion(), increment, preid );\n\n\t\tif ( ! semver.valid( version ) ) {\n\t\t\tlogResult( `The supplied version string ${ chalk.bold( version ) } is invalid.`, 'error' );\n\t\t\tprocess.exit( 1 );\n\t\t}\n\n\t\tconst isPrerelease = semver.prerelease( version )?.length ? true : false;\n\n\t\t// Add extraReplacements.\n\t\treplacements = [ ...replacements, ...extraReplacements ];\n\n\t\tconst res = [];\n\t\tif ( ! skipConfig ) {\n\t\t\tconst configUpdate = updateConfig( version );\n\t\t\tif ( configUpdate ) {\n\t\t\t\tconfigUpdate.forEach( ( configRes ) => res.push( configRes ) );\n\t\t\t}\n\t\t}\n\n\t\tlogResult( `Updating project files to version ${ chalk.bold( version ) }.` );\n\n\t\tfor ( let i = 0; i < replacements.length; i++ ) {\n\t\t\tif ( replacements[ i ].length < 3 ) {\n\t\t\t\treplacements[ i ].push( 'g' );\n\t\t\t}\n\n\t\t\tconst [ glob, regex, flags, forPreleases ] = replacements[ i ];\n\n\t\t\tif ( isPrerelease && ! forPreleases ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tupdateVersions( glob, regex, flags, exclude, version )\n\t\t\t\t.filter( ( { hasChanged } ) => hasChanged )\n\t\t\t\t.forEach( ( update ) => {\n\t\t\t\t\tres.push( {\n\t\t\t\t\t\tFile: chalk.green( update.file ),\n\t\t\t\t\t\tMatches: chalk.yellow( update.numMatches ),\n\t\t\t\t\t\tReplacements: chalk.yellow( update.numReplacements ),\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t\t);\n\t\t}\n\n\t\tif ( ! res.length ) {\n\t\t\tlogResult( 'No updates made.', 'warning' );\n\t\t} else {\n\t\t\tlogResult( 'Version update completed.', 'success' );\n\t\t\tconsole.log(\n\t\t\t\tcolumnify(\n\t\t\t\t\tres,\n\t\t\t\t\t{\n\t\t\t\t\t\theadingTransform: ( heading ) => chalk.bold.underline( heading ),\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t},\n};\n"
  },
  {
    "path": "packages/dev/src/index.js",
    "content": "#!/usr/bin/env node\n\nconst { argv } = process,\n\t{ readFileSync, readdirSync, lstatSync } = require( 'fs' ),\n\tpath = require( 'path' ),\n\t{ Command } = require( 'commander' ),\n\tprogram = new Command(),\n\tpkg = JSON.parse( readFileSync( path.join( __dirname, '../package.json' ), 'utf8' ) ),\n\t{ getDefault } = require( './utils' );\n\n// Setup the CLI program.\nprogram\n\t.description( pkg.description )\n\t.version( pkg.version )\n\t.addHelpCommand( 'help [command]', 'Display help for command.' );\n\n/**\n * Read the contents of the specified directory, registering all as subcommands of the specified parent command.\n *\n * @since 0.0.1\n *\n * @param {Command} parent        Parent command instance.\n * @param {string}  dir           Path to the directory where the command modules should be loaded from.\n * @param {Array[]} optionsParent Array of options shared from the parent to all subcommands.\n * @return {void}\n */\nfunction registerCommands( parent, dir, optionsParent = [] ) {\n\treaddirSync( dir )\n\t\t// Exclude index files, they're picked up automatically so we don't want to double register them.\n\t\t.filter( ( file ) => 'index.js' !== file )\n\t\t.forEach( ( file ) => {\n\t\t\tconst filePath = path.join( dir, file );\n\n\t\t\t// Register the command.\n\t\t\tregisterCommand( parent, filePath, optionsParent );\n\t\t} );\n}\n\n/**\n * Register a command with the specified parent.\n *\n * @since 0.0.1\n *\n * @param {Command} parent        Parent command instance.\n * @param {string}  filePath      Path to the directory where the command modules should be loaded from.\n * @param {Array[]} optionsParent Array of options shared from the parent to all subcommands.\n * @return {void}\n */\nfunction registerCommand( parent, filePath, optionsParent = [] ) {\n\tconst {\n\t\tcommand,\n\t\tdescription,\n\t\taction,\n\t\targs = [],\n\t\toptions = [],\n\t\toptionsShared = [],\n\t\thelp = [],\n\t} = require( filePath );\n\n\tconst cmd = parent\n\t\t.command( command )\n\t\t.description( description );\n\n\tif ( action ) {\n\t\tcmd.action( action );\n\t}\n\n\targs.forEach( ( cmdArgs ) => cmd.argument( ...cmdArgs ) );\n\n\t[ ...options, ...optionsParent, ...optionsShared ].forEach( ( opts ) => {\n\t\t// Attempts to parse default values from the config file.\n\t\topts[ 2 ] = getDefault( parent._name ? parent._name + '.' + command : command, opts[ 0 ], opts[ 2 ] );\n\t\tcmd.option( ...opts );\n\t} );\n\n\thelp.forEach( ( helpText ) => cmd.addHelpText( ...helpText ) );\n\n\t// If it's a directory, recursively register files in the directory.\n\tif ( lstatSync( filePath ).isDirectory() ) {\n\t\tregisterCommands( cmd, filePath, optionsShared );\n\t\tcmd.addHelpCommand( 'help [command]', 'Display help for command.' );\n\t}\n}\n\n// Register all commands.\nregisterCommands( program, path.join( __dirname, 'cmds' ) );\n\n// Parse incoming arguments.\nprogram.parse( argv );\n"
  },
  {
    "path": "packages/dev/src/utils/changelog-entry.js",
    "content": "/**\n * A changelog entry object.\n *\n * @typedef {Object} ChangelogEntry\n * @property {string}   title        Title of the changelog entry. Used as the filename (excluding the extension) for the changelog file.\n * @property {string}   significance Entry significance.\n * @property {string}   type         Entry type.\n * @property {string}   comment      Internal-use comment accompanying the entry.\n * @property {string[]} links        List of GitHub issues linked to the entry.\n * @property {string[]} attributions List of individuals attributed to the entry.\n * @property {string}   entry        The content of the changelog entry.\n */\nmodule.exports = {\n\ttitle: '',\n\tsignificance: '',\n\ttype: '',\n\tcomment: '',\n\tlinks: [],\n\tattributions: [],\n\tentry: '',\n};\n"
  },
  {
    "path": "packages/dev/src/utils/configs.js",
    "content": "const { existsSync } = require( 'fs' );\n\n/**\n * Retrieve a JS object for the specified JSON config file.\n *\n * Returns an empty object if the config file can't be found.\n *\n * @since 0.0.1\n *\n * @param {string} filename The JSON config filename, eg \"composer\" or \"package\".\n * @return {Object} The config file as a JS object.\n */\nfunction getConfig( filename ) {\n\tconst path = `${ process.cwd() }/${ filename }.json`;\n\tif ( existsSync( path ) ) {\n\t\treturn require( path );\n\t}\n\treturn {};\n}\n\n/**\n * Determines if the specified JSON config file exists.\n *\n * @since 0.0.1\n *\n * @param {string} filename The JSON config file name, eg \"composer\" or \"package\".\n * @return {boolean} Returns true if the config file exists, otherwise false.\n */\nfunction hasConfig( filename ) {\n\treturn Object.keys( getConfig( filename ) ).length >= 1;\n}\n\nmodule.exports = {\n\tgetConfig,\n\thasConfig,\n};\n"
  },
  {
    "path": "packages/dev/src/utils/create-dist-file.js",
    "content": "const\n\t{ existsSync } = require( 'fs' ),\n\tgetArchiveFilename = require( './get-archive-filename' ),\n\t{ getConfig } = require( './configs' ),\n\tgetProjectSlug = require( './get-project-slug' ),\n\texecSync = require( './exec-sync' );\n\n/**\n * Determine if the project has composer production dependencies warranting a `composer install` during builds.\n *\n * @since 0.0.1\n *\n * @return {boolean} Whether or not a composer install is required.\n */\nfunction requiresComposerInstall() {\n\tconst\n\t\tpkg = getConfig( 'composer' ),\n\t\tkeys = pkg.require ? Object.keys( pkg.require ) : [],\n\t\tautoload = pkg.autoload ? Object.keys( pkg.autoload ) : [];\n\n\t// If we have autoloading enabled we need to build for production.\n\tif ( 0 !== autoload.length ) {\n\t\treturn true;\n\t}\n\n\t// Not defined or empty.\n\tif ( 0 === keys.length ) {\n\t\treturn false;\n\t}\n\n\t// Has only a php (platform) requirement.\n\tif ( 1 === keys.length && 'php' === keys[ 0 ] ) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Create dist file.\n *\n * @since 0.0.1\n * @since 0.2.1 Windows compatibility: account for spaces in file paths.\n *\n * @return {string}\n */\nmodule.exports = ( distDir, silent, log = () => {} ) => {\n\tconst name = getArchiveFilename(),\n\t\tslug = getProjectSlug(),\n\t\tcomposer = requiresComposerInstall(),\n\t\tcwd = distDir;\n\n\t// If we have composer dependencies, reinstall with no dev requirements or scripts.\n\tif ( composer ) {\n\t\tlog( 'Installing composer production dependencies...' );\n\t\texecSync( `composer install --no-dev --no-scripts`, silent );\n\t}\n\n\t// Empty inspected directories in the distribution directory (if any are leftover from the last run of the command).\n\tif ( existsSync( distDir ) ) {\n\t\texecSync( `rm -rf ${ slug }`, silent, { cwd } );\n\t}\n\n\t// Create the initial archive using composer.\n\texecSync( `composer archive --format=zip --dir=\"${ distDir }\" --file=${ name.replace( '.zip', '' ) }`, true );\n\n\t// Unzip the initial archive into a subdirectory matching the project's slug.\n\texecSync( `unzip ${ name } -d ${ slug }`, silent, { cwd } );\n\n\t// Remove the original zip file.\n\texecSync( `rm ${ name }`, true, { cwd } );\n\n\t// Zip up the subdirectory.\n\texecSync( `zip -r ${ name } ${ slug }/`, silent, { cwd } );\n\n\t// Remove the subdirectory.\n\texecSync( `rm -rf ${ slug }/`, silent, { cwd } );\n\n\t// If we have composer dependencies, reinstall with dev requirements when we're done.\n\tif ( composer ) {\n\t\tlog( 'Reinstalling all composer dependencies...' );\n\t\texecSync( `composer install`, silent );\n\t}\n\n\treturn name;\n};\n"
  },
  {
    "path": "packages/dev/src/utils/determine-version-increment.js",
    "content": "const\n\tsemver = require( 'semver' ),\n\tgetChangelogEntries = require( './get-changelog-entries' );\n\n/**\n * Determine a version increment level.\n *\n * Uses existing changelog entries, the current version, and the requested preid to determine the increment to\n * be made.\n *\n * Finds the highest significance changelog entry and uses that significance for the increment.\n *\n * When a preid is passed and the current version is a prerelease, significance will be disregarded and \"prerelease\"\n * will be used for the increment.\n *\n * @since 0.0.1\n * @since 0.0.2 Added currentVersion and preid parameters.\n *              Add `pre` prefix when a `preid` is specified.\n *              Return `prerelease` when a `preid` is specified and `currentVersion` is already a prerelease.\n *\n * @param {string}  dir            Path to the directory where changelog entries are stored.\n * @param {string}  currentVersion Current project version.\n * @param {?string} preid          Preid identifier, eg \"alpha\", \"beta\", etc... And `null` when not requesting a prerelease.\n * @return {string} A version increment string.\n */\nmodule.exports = ( dir, currentVersion, preid = null ) => {\n\tif ( preid && null !== semver.prerelease( currentVersion ) ) {\n\t\treturn 'prerelease';\n\t}\n\n\tconst\n\t\tlogs = Array.from( new Set( getChangelogEntries( dir ).map( ( { significance } ) => significance ) ) ),\n\t\tincrement = [ 'major', 'minor', 'patch' ].find( ( level ) => logs.includes( level ) ) || 'patch';\n\n\treturn preid ? `pre${ increment }` : increment;\n};\n"
  },
  {
    "path": "packages/dev/src/utils/exec-sync.js",
    "content": "const { execSync } = require( 'child_process' );\n\n/**\n * Execute a command in a child process.\n *\n * This is a wrapper for node's child_process.execSync() with some\n * quality of life improvements to reduce the necessity of specifying\n * an options object to silence output.\n *\n * @since 0.0.1\n *\n * @param {string}  cmd   Command to execute.\n * @param {boolean} quiet If true, silences stdio output.\n * @param {Object}  opts  Additional options object passed to `execSync()`.\n * @return {string} The stdout from the command.\n */\nmodule.exports = ( cmd, quiet = false, opts = {} ) => {\n\tconst stdio = quiet ? 'pipe' : 'inherit',\n\t\tstdout = execSync( cmd, { stdio, ...opts } );\n\n\treturn quiet ? stdout.toString().trim() : '';\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-archive-filename.js",
    "content": "const getCurrentVersion = require( './get-current-version' ),\n\tgetProjectSlug = require( './get-project-slug' );\n\n/**\n * Retrieve the filename of the project's archive/distribution zip file.\n *\n * @since 0.0.1\n *\n * @param {?string} version The version number. If not supplied uses the current version.\n * @return {string} The archive filename.\n */\nmodule.exports = ( version = null ) => {\n\tversion = version ? version : getCurrentVersion();\n\treturn `${ getProjectSlug() }-${ version }.zip`;\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-changelog-entries.js",
    "content": "const ChangelogEntry = require( './changelog-entry' ),\n\t{ readdirSync, readFileSync, existsSync } = require( 'fs' ),\n\tpath = require( 'path' ),\n\tYAML = require( 'yaml' );\n\n/**\n * Retrieve all changelog entry files from the specified directory.\n *\n * This will attempt to parse all .y[a]ml files found in the specified directory.\n *\n * @since 0.0.1\n *\n * @param {string} dir Path to the directory.\n * @return {ChangelogEntry[]} Array of changelog entry objects.\n */\nmodule.exports = ( dir ) => {\n\tconst res = [];\n\n\tif ( ! existsSync( dir ) ) {\n\t\treturn res;\n\t}\n\n\treaddirSync( dir ).forEach( ( file ) => {\n\t\t// Only parse valid changelog files.\n\t\tif ( ! file.includes( '.yml' ) && ! file.includes( '.yaml' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst log = YAML.parse( readFileSync( path.join( dir, file ), 'utf8' ) ),\n\t\t\t{ comment = '', links = '', attributions = '' } = log;\n\t\tdelete log.links;\n\t\tdelete log.comment;\n\t\tdelete log.attributions;\n\t\tres.push( {\n\t\t\ttitle: path.parse( file ).name,\n\t\t\t...log,\n\t\t\tcomment,\n\t\t\tlinks,\n\t\t\tattributions,\n\t\t} );\n\t} );\n\n\treturn res;\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-changelog-for-version.js",
    "content": "const parseChangelogFile = require( './parse-changelog-file' );\n\n/**\n * Retrieve a changelog for the given version.\n *\n * @since 0.0.1\n *\n * @param {string} ver  A semver string for the version to retrieve.\n * @param {string} file Changelog file path.\n * @return {Object|undefined} Returns the changelog version entry object or undefined if not found.\n */\nmodule.exports = ( ver, file ) => {\n\treturn parseChangelogFile( file ).find( ( { version } ) => ver === version );\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-changelog-options.js",
    "content": "module.exports = () => {\n\treturn {\n\t\tsignificance: {\n\t\t\tmajor: 'Backwards incompatible or breaking changes',\n\t\t\tminor: 'New features or backwards-compatible deprecations',\n\t\t\tpatch: 'Backwards-compatible bug fixes',\n\t\t},\n\t\ttype: {\n\t\t\tadded: 'New features',\n\t\t\tchanged: 'Updates to existing features',\n\t\t\tfixed: 'Any bug fixes',\n\t\t\tdeprecated: 'Features to be removed',\n\t\t\tremoved: 'Features that are being removed',\n\t\t\tdev: 'Developer-related notes or changes',\n\t\t\tperformance: 'Performance improvements or fixes',\n\t\t\tsecurity: 'Changes related to security vulnerabilities',\n\t\t},\n\t};\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-current-version.js",
    "content": "const { getConfig } = require( './configs' );\n\n/**\n * Retrieve the current version number of the project\n *\n * @since 0.0.1\n *\n * @return {string} A semver string or an empty string if no version could be parsed.\n */\nmodule.exports = () => {\n\tconst npm = getConfig( 'package' );\n\tif ( npm.version ) {\n\t\treturn npm.version;\n\t}\n\n\tconst composer = getConfig( 'composer' );\n\tif ( composer?.extra?.llms?.version ) {\n\t\treturn composer.extra.llms.version;\n\t}\n\n\treturn '';\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-default.js",
    "content": "const\n\tpath = require( 'path' ),\n\tparseYaml = require( 'yaml' ).parse,\n\t{ existsSync, readFileSync } = require( 'fs' );\n\n/**\n * Find the config file.\n *\n * Looks in the project's root directory for .llmsdev.yml or .llmsdev.yaml.\n *\n * @since 0.0.1\n *\n * @return {string} Returns the full path to the config file or an empty string if none can be found.\n */\nfunction getConfigFilePath() {\n\tconst basePath = path.join( process.cwd(), '.llmsdev' );\n\n\tlet configFilePath = '';\n\n\t[ '.yml', '.yaml' ].some( ( ext ) => {\n\t\tconst testPath = basePath + ext;\n\t\tif ( existsSync( testPath ) ) {\n\t\t\tconfigFilePath = testPath;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t} );\n\n\treturn configFilePath;\n}\n\n/**\n * Load the gloabl config file.\n *\n * @since 0.0.1\n *\n * @return {Object} Returns the parsed config file as a JS object or an empty object if none found.\n */\nfunction loadConfigFile() {\n\tconst filePath = getConfigFilePath();\n\tif ( ! filePath ) {\n\t\treturn {};\n\t}\n\n\treturn parseYaml( readFileSync( filePath, 'utf8' ) );\n}\n\n/**\n * Get a default value for a given command and option.\n *\n * @since\n *\n * @param {string} command      Name of the command. When accessing subcommands the command name will be \"parent.subcommand\".\n * @param {string} setting      The option value, eg: \"-v --verbose\" or \"-m --mode <mode>\".\n *                              This string will be parsed and use the value following the two hyphens.\n *                              Using the examples the value from the config would accept the value of \"verbose\" or \"mode\".\n * @param {any}    defaultValue The default value as specified in the command options.\n * @return {any} The default value of the option.\n */\nmodule.exports = ( command, setting, defaultValue = undefined ) => {\n\tsetting = setting.split( ' ' )[ 1 ].replace( '--', '' );\n\n\tconst config = loadConfigFile();\n\tif (\n\t\t! config ||\n\t\t0 === Object.keys( config ) ||\n\t\tundefined === config[ command ] ||\n\t\tundefined === config[ command ][ setting ] ) {\n\t\treturn defaultValue;\n\t}\n\n\treturn config[ command ][ setting ];\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-next-version.js",
    "content": "const semver = require( 'semver' );\n\n/**\n * Determine the next version for a release given the current version and an increment level + preid.\n *\n * This function is a wrapper around `semver.inc()` with some modifications:\n *   + If a `preid` is provided, `pre` will automatically be added to `increment` (unless it's already been added).\n *   + When creating the first prerelease, eg 1.0.0 -> 2.0.0-beta.1, this function skips beta.0 and makes beta.1.\n *\n * @since 0.0.1\n * @since 0.0.2 Only add \"pre\" to the increment if it is already added.\n *\n * @param {string} version   Version to increment.\n * @param {string} increment Increment level: major, premajor, minor, preminor, patch, prepatch, or prerelease.\n * @param {string} preid     Prerelease identifier when using `pre*` increment levels. EG: \"alpha\", \"beta\", \"rc\".\n * @return {string} The incremented string.\n */\nmodule.exports = ( version, increment, preid = null ) => {\n\tincrement = preid && ! increment.startsWith( 'pre' ) ? `pre${ increment }` : increment;\n\n\t// When incrementing a prerelease we want to skip versions like \"-beta.0\" and go right to \"-beta.1\".\n\tif ( increment.includes( 'pre' ) ) {\n\t\tconst prever = semver.inc( version, increment, preid );\n\t\tif ( 0 === semver.prerelease( prever ).reverse()[ 0 ] ) {\n\t\t\tversion = prever;\n\t\t\tincrement = 'prerelease';\n\t\t}\n\t}\n\n\treturn semver.inc( version, increment, preid );\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-project-privacy.js",
    "content": "const\n\tgetProjectSlug = require( './get-project-slug' ),\n\texecSync = require( './exec-sync' );\n\n/**\n * Get the project's repo privacy status.\n *\n * Uses the GitHub CLI client (gh) to lookup the project's status via the GitHub api. If the API\n * encounters errors (like a 404 or an authentication error) it will fail silently and result\n * in an \"unknown\" response.\n *\n * @since 0.0.2\n *\n * @return {string} Returns 'public' or 'private'. If the repo cannot be found, returns 'unknown'.\n */\nfunction getProjectPrivacy() {\n\tlet status = 'unknown';\n\n\ttry {\n\t\tconst res = JSON.parse( execSync( `gh api repos/gocodebox/${ getProjectSlug() }`, true ) );\n\t\tstatus = res.private ? 'private' : 'public';\n\t} catch ( e ) {}\n\n\treturn status;\n}\n\n/**\n * Determine if the project is private.\n *\n * @since 0.0.2\n *\n * @return {boolean | undefined} Returns `true` for private repos, `false` for public repos, and `undefined` for unknown repos.\n */\nfunction isProjectPrivate() {\n\tconst privacy = getProjectPrivacy();\n\tif ( 'unknown' === privacy ) {\n\t\treturn undefined;\n\t}\n\treturn 'private' === privacy;\n}\n\n/**\n * Determine if the project is private.\n *\n * @since 0.0.2\n *\n * @return {boolean | undefined} Returns `false` for private repos, `true` for public repos, and `undefined` for unknown repos.\n */\nfunction isProjectPublic() {\n\tconst privacy = getProjectPrivacy();\n\tif ( 'unknown' === privacy ) {\n\t\treturn undefined;\n\t}\n\treturn 'public' === privacy;\n}\n\nmodule.exports = {\n\n\tisProjectPrivate,\n\tisProjectPublic,\n\tgetProjectPrivacy,\n\n};\n"
  },
  {
    "path": "packages/dev/src/utils/get-project-slug.js",
    "content": "const { basename } = require( 'path' );\n\n/**\n * Retrieve the package's \"slug\".\n *\n * This will always be equal to the directory name.\n *\n * For example \"lifterlms\" or \"lifterlms-integration-woocommerce\".\n *\n * @since 0.0.1\n *\n * @return {string} The project's slug.\n */\nmodule.exports = () => {\n\treturn basename( process.cwd() );\n};\n"
  },
  {
    "path": "packages/dev/src/utils/index.js",
    "content": "const\n\tChangelogEntry = require( './changelog-entry' ),\n\tcreateDistFile = require( './create-dist-file' ),\n\tdetermineVersionIncrement = require( './determine-version-increment' ),\n\texecSync = require( './exec-sync' ),\n\tgetArchiveFilename = require( './get-archive-filename' ),\n\tgetChangelogEntries = require( './get-changelog-entries' ),\n\tgetChangelogForVersion = require( './get-changelog-for-version' ),\n\tgetChangelogOptions = require( './get-changelog-options' ),\n\tgetCurrentVersion = require( './get-current-version' ),\n\tgetDefault = require( './get-default' ),\n\tgetNextVersion = require( './get-next-version' ),\n\t{ isProjectPrivate, isProjectPublic, getProjectPrivacy } = require( './get-project-privacy' ),\n\tgetProjectSlug = require( './get-project-slug' ),\n\t{ getConfig, hasConfig } = require( './configs' ),\n\t{ getFileLink, getRepoLink, getIssueLink } = require( './repo-links' ),\n\tlogResult = require( './log-result' ),\n\tparseChangelogFile = require( './parse-changelog-file' ),\n\tparseIssueString = require( './parse-issue-string' ),\n\tparseMainFileMetadata = require( './pare-main-file-metadata' ), \n\tpushDistFile = require( './push-dist-file' ),\n\t{ isAttributionValid, isEntryValid, isLinkValid, getChangelogValidationIssues } = require( './validate-changelog' );\n\nmodule.exports = {\n\tChangelogEntry,\n\tcreateDistFile,\n\tdetermineVersionIncrement,\n\texecSync,\n\tgetArchiveFilename,\n\tgetChangelogEntries,\n\tgetChangelogForVersion,\n\tgetChangelogOptions,\n\tgetConfig,\n\tgetCurrentVersion,\n\tgetDefault,\n\tgetFileLink,\n\tgetIssueLink,\n\tgetNextVersion,\n\tgetProjectSlug,\n\tgetRepoLink,\n\tisProjectPrivate,\n\tisProjectPublic,\n\tgetProjectPrivacy,\n\thasConfig,\n\tlogResult,\n\tisAttributionValid,\n\tisEntryValid,\n\tisLinkValid,\n\tgetChangelogValidationIssues,\n\tparseChangelogFile,\n\tparseIssueString,\n\tparseMainFileMetadata,\n\tpushDistFile,\n};\n"
  },
  {
    "path": "packages/dev/src/utils/log-result.js",
    "content": "const chalk = require( 'chalk' );\n\n/**\n * Log a result to the console\n *\n * @since 0.0.1\n *\n * @param {string} msg  Message to log.\n * @param {string} type Message type. Accepts success, warning, error, or info.\n * @return {void}\n */\nmodule.exports = ( msg, type = 'info' ) => {\n\tmsg = chalk.bold( type.charAt( 0 ).toUpperCase() + type.slice( 1 ) ) + ': ' + msg;\n\n\tswitch ( type ) {\n\t\tcase 'success':\n\t\t\tmsg = chalk.green( msg );\n\t\t\tbreak;\n\n\t\tcase 'warning':\n\t\t\tmsg = chalk.yellow( msg );\n\t\t\tbreak;\n\n\t\tcase 'error':\n\t\t\tmsg = chalk.red( msg );\n\t\t\tbreak;\n\n\t\tcase 'info':\n\t\t\tmsg = chalk.blue( msg );\n\t\t\tbreak;\n\t}\n\n\tconsole.log( msg );\n};\n"
  },
  {
    "path": "packages/dev/src/utils/pare-main-file-metadata.js",
    "content": "const { readFileSync } = require( 'fs' );\n\n/**\n * Parses WordPress project metadata from a file header comment.\n *\n * @since 0.1.0\n *\n * @return {Object} A key/value object containing the metadata found within the file.\n */\nmodule.exports = ( file ) => {\n\n\tconst contents = readFileSync( file ).toString(),\n\t\tregex = new RegExp( / \\* (?<key>[A-Z][A-Za-z ]+)\\: (?<val>.*)\\n/g, 'g' );\n\n\tlet matches, metas = {};\n\twhile ( matches = regex.exec( contents ) ) {\n\t\tmetas[ matches.groups.key ] = matches.groups.val;\n\t}\n\n\treturn metas;\n\n}\n"
  },
  {
    "path": "packages/dev/src/utils/parse-changelog-file.js",
    "content": "const { readFileSync } = require( 'fs' );\n\n/**\n * Retrieve an entry object stub with the given date and version.\n *\n * @since 0.0.1\n *\n * @param {string} date    Release date in `YYYY-MM-DD` format.\n * @param {string} version A semver string.\n * @return {Object} Entry object.\n */\nfunction getEntryObject( date, version ) {\n\treturn {\n\t\tdate,\n\t\tversion,\n\t\tlogs: [],\n\t};\n}\n\n/**\n * Convert an entry item list into a string, preserving newlines within the list but stripping them from the start and end.\n *\n * @since 0.0.1\n *\n * @param {string[]} entry Array of lines.\n * @return {string} Joined entry string.\n */\nfunction finalizeEntry( entry ) {\n\t// Trim newlines from the beginning of the entry list.\n\tif ( ! entry.logs[ 0 ] ) {\n\t\tentry.logs.splice( 0, 1 );\n\t}\n\n\t// Trim trailing nnewlines from the end of the entry list.\n\twhile ( ! entry.logs[ entry.logs.length - 1 ] ) {\n\t\tentry.logs.splice( entry.logs.length - 1, 1 );\n\t}\n\n\t// Join them all together with a new line.\n\tentry.logs = entry.logs.join( '\\n' );\n\n\treturn entry;\n}\n\n/**\n * Convert a changelog file to a JSON object\n *\n * @since 0.0.1\n *\n * @param {string} file Path to the changelog MD file.\n * @return {Object[]} Changelog as an array of JSON objects.\n */\nmodule.exports = ( file ) => {\n\tconst changelog = readFileSync( file, 'utf8' ),\n\t\tlines = changelog.split( '\\n' ),\n\t\tlogs = [],\n\t\tregex = /v(?<version>[0-9]\\d*\\.[0-9]\\d*\\.[0-9]\\d*(?:-(?:[0-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:[0-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\\+[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*)?) - (?<date>\\d{4}\\-\\d{2}\\-\\d{2})/;\n\n\t// Remove title, title underline, and first blank line.\n\tlines.splice( 0, 3 );\n\n\tlet currEntry = {};\n\n\tlines.forEach( ( line ) => {\n\t\tif ( line.startsWith( '====' ) || line.startsWith( '----' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst parsed = regex.exec( line );\n\n\t\t// This is a header line.\n\t\tif ( parsed ) {\n\t\t\t// Before we start processing the next log item, finalize what we've compiled from the previous.\n\t\t\tif ( currEntry.logs && currEntry.logs.length ) {\n\t\t\t\tlogs.push( finalizeEntry( currEntry ) );\n\t\t\t}\n\n\t\t\tcurrEntry = getEntryObject( parsed.groups.date, parsed.groups.version );\n\t\t} else {\n\t\t\tcurrEntry.logs.push( line );\n\t\t}\n\t} );\n\n\t// The final entry in the changelog won't get caught by the next parsed date so we'll finalize that entry here at the end.\n\tlogs.push( finalizeEntry( currEntry ) );\n\n\treturn logs;\n};\n"
  },
  {
    "path": "packages/dev/src/utils/parse-issue-string.js",
    "content": "const getProjectSlug = require( './get-project-slug' );\n\n/**\n * A GitHub-style issue reference object.\n *\n * @typedef {Object} GitHubIssueRef\n * @property {string} org  The GitHub organization slug, eg \"gocodebox\".\n * @property {string} repo The GitHub repo slug, eg \"lifterlms\".\n * @property {string} num  The issue number, eg \"1234\".\n */\n\n/**\n * Parses a GitHub-style issue reference string into it's parts.\n *\n * @since 0.1.0\n *\n * @param {string} issue A GitHub-style issue reference string. Formatted as either \"#123\" or \"organization/repository#123\".\n * @return {GitHubIssueRef} An issue object.\n */\nmodule.exports = ( issue ) => {\n\tlet org = 'gocodebox',\n\t\trepo = getProjectSlug(),\n\t\tnum = '';\n\n\t// Is an external reference.\n\tif ( issue.includes( '/' ) ) {\n\t\tconst split = issue.split( '/' );\n\t\torg = split[ 0 ];\n\t\t[ repo, num ] = split[ 1 ].split( '#' );\n\t} else {\n\t\tnum = issue.slice( 1 );\n\t}\n\n\treturn { org, repo, num };\n};\n"
  },
  {
    "path": "packages/dev/src/utils/push-dist-file.js",
    "content": "// Deps.\nconst\n\tfs = require( 'fs' ),\n\texecSync = require( './exec-sync' ),\n\tgetProjectSlug = require( './get-project-slug' );\n\n/**\n * Commit and push a specified zip file to a git branch.\n *\n * This is used, primarily, to publish the distribution archive of a project\n * to the \"release\" branch which is used to create and publish installable releases.\n *\n * @since 0.0.1\n * @since 0.0.2 OSX compatibility: don't use `xargs -d`.\n * @since 0.2.1 Windows compatibility:\n *              - account for spaces in file paths\n *              - use node fs utility to create a directory in place of `mkdir` to\n *                override Windows' restrictions.\n *\n * @param {string}  distFile Distribution file used as the source of the commit.\n * @param {string}  branch   Branch to commit and push to.\n * @param {string}  message  Commit message.\n * @param {boolean} silent   Whether or not to output child process stdout.\n * @return {void}\n */\nmodule.exports = ( distFile, branch, message, silent = true ) => {\n\tconst slug = getProjectSlug();\n\n\tif ( ! fs.existsSync( process.cwd() + '/tmp' ) ) {\n\t\tfs.mkdirSync( process.cwd() + '/tmp', true );\n\t};\n\n\tconst\n\t\tcwd = process.cwd() + '/tmp/git',\n\t\turl = execSync( 'git config --get remote.origin.url', true );\n\n\t// Clone the repo into a temp directory.\n\texecSync( `git clone ${ url } \"${ cwd }\"`, silent );\n\n\t// Checkout to the publication branch.\n\texecSync( `git checkout -b ${ branch }`, silent, { cwd } );\n\n\t// Empty everything except the git directory.\n\texecSync( `mv .git ../ && cd ../ && rm -rf ./git && mkdir git && mv .git ./git && cd git`, silent, { cwd } );\n\n\t// Extract the distribution file.\n\texecSync( `unzip \"${ distFile }\" -d ./tmp/git/`, silent );\n\n\t// Move all the contents into the publication branch.\n\texecSync( `mv ./${ slug }/* ./ && rm -rf ${ slug }/`, silent, { cwd } );\n\n\t// Add all files.\n\texecSync( `git add -A`, silent, { cwd } );\n\n\t// Commit.\n\texecSync( `git commit --allow-empty -m \"${ message }\"`, silent, { cwd } );\n\n\t// Force push.\n\texecSync( `git push origin ${ branch } -f`, silent, { cwd } );\n\n\t// Remove temp repo dir.\n\texecSync( `rm -rf ./tmp/git`, silent );\n};\n"
  },
  {
    "path": "packages/dev/src/utils/repo-links.js",
    "content": "const getProjectSlug = require( './get-project-slug' ),\n\tparseIssueString = require( './parse-issue-string' );\n\n/**\n * Retrieves a link to the specified file on the project's GitHub repository.\n *\n * @since 0.1.0\n *\n * @param {string} path   Path to the file (relative to the root directory), eg: \"includes/file.php\" or \"main.php\".\n * @param {string} branch Branch or version number. Defaults to \"trunk\".\n * @return {string} The full URL to a file on the project's GitHub repository.\n */\nfunction getFileLink( path, branch = 'trunk' ) {\n\treturn `${ getRepoLink() }/blob/${ branch }/${ path }`;\n}\n\n/**\n * Retrieves a link to the specified issue on the project's GitHub repository.\n *\n * @since 0.1.0\n *\n * @param {string} issue A GitHub-style issue reference string. Formatted as either \"#123\" or \"organization/repository#123\".\n * @return {string} The full URL to the specified issue.\n */\nfunction getIssueLink( issue ) {\n\tconst { org, repo, num } = parseIssueString( issue );\n\treturn `${ getRepoLink( repo, org ) }/issues/${ num }`;\n}\n\n/**\n * Retrieves the base link to a project's GitHub repository.\n *\n * @since 0.1.0\n *\n * @param {string} project The project slug.\n * @param {string} org     The project organization.\n * @return {string} The full URL to the current project's GitHub repository.\n */\nfunction getRepoLink( project, org ) {\n\tproject = project || getProjectSlug();\n\torg = org || 'gocodebox';\n\treturn `https://github.com/${ org }/${ project }`;\n}\n\nmodule.exports = {\n\tgetFileLink,\n\tgetIssueLink,\n\tgetRepoLink,\n};\n"
  },
  {
    "path": "packages/dev/src/utils/validate-changelog.js",
    "content": "require( 'url' );\n\nconst\n\tChangelogEntry = require( './changelog-entry' ),\n\tchalk = require( 'chalk' ),\n\tgetChangelogOptions = require( './get-changelog-options' );\n\n/**\n * Highlights text depending on the formatting request\n *\n * When formatting is disabled, the text is wrapped in quotes.\n *\n * When formatting is enabled, the text will be quoted and emboldened.\n *\n * @since 0.0.1\n *\n * @param {string}  text       The text to highlight.\n * @param {boolean} formatting Whether or not rich formatting should be used.\n * @return {string} The highlighted text.\n */\nfunction highlight( text, formatting = true ) {\n\ttext = formatting ? chalk.bold( text ) : text;\n\treturn `\"${ text }\"`;\n}\n\n/**\n * Determines if an attribution string is valid.\n *\n * Attributions are valid in the following formats:\n *   + GitHub username reference: @thomasplevy\n *   + Markdown link: [Jeffrey Lebowski](https://elduderino.geocites.com/)\n *\n * @since 0.0.1\n *\n * @param {string} attr User-submitted attribution string.\n * @return {boolean} Returns `true` if the attribution string is valid, otherwise `false`.\n */\nfunction isAttributionValid( attr ) {\n\tattr = attr.toString();\n\n\tconst firstChar = attr.charAt( 0 );\n\n\t// GitHub username.\n\tif ( '@' === firstChar ) {\n\t\treturn true;\n\t}\n\n\tconst\n\t\tmatch = attr.match( /\\[[^\\]]*\\]\\(([^)]*)\\)*/ );\n\n\tif ( ! match ) {\n\t\treturn false;\n\t}\n\n\ttry {\n\t\tnew URL( match[ 1 ] );\n\t\treturn true;\n\t} catch ( e ) {}\n\n\treturn false;\n}\n\n/**\n * Determine if a supplied link is valid\n *\n * Links are valid in the following formats:\n *   + GitHub issue reference in the current repo: #12345\n *   + GitHub issue reference to another repo: organization/repository#12345\n *\n * @since 0.0.1\n *\n * @param {string} link User-submitted link string.\n * @return {boolean} Returns `true` if the link is valid and false otherwise.\n */\nfunction isLinkValid( link ) {\n\t// Force values to a string.\n\tlink = link.toString();\n\n\tconst isValidHash = ( hash ) => ! isNaN( parseInt( hash.slice( 1 ) ) );\n\n\tlet valid = false;\n\n\t// Valid hash string, eg \"#123\".\n\tif ( '#' === link.charAt( '0' ) ) {\n\t\tvalid = isValidHash( link );\n\t} else {\n\t\t// Valid reference to another repo, eg: \"org/repo#123\".\n\t\tconst split = link.split( '/' );\n\t\tif ( 2 !== split.length ) {\n\t\t\tvalid = false;\n\t\t} else {\n\t\t\tvalid = split[ 1 ].includes( '#' ) && isValidHash( '#' + split[ 1 ].split( '#' )[ 1 ] );\n\t\t}\n\t}\n\n\treturn valid;\n}\n\n/**\n * Determine if the supplied entry is valid.\n *\n * A valid entry can be single or multiple lines.\n *\n * A single-line must:\n * \t + Begin with a capital letter (the bullet character should be omitted).\n * \t + End with a full stop: period, question mark, or exclamation point.\n *\n * A multi-line:\n *   + Each line must start with a plus sign bullet character: `+`.\n *   + A single space must follow the bullet.\n *   + The remaining portion of each line follows the same rules as a single-line entry.\n *   + Additionally, a line may end in a colon.\n *\n * @since 0.0.1\n *\n * @param {string} entry The changelog entry string.\n * @return {boolean} Returns `true` if the entry is valid and `false` otherwise.\n */\nfunction isEntryValid( entry ) {\n\tconst singleLineRegex = /^[A-Z].*[.!?]$/,\n\t\tmultiLineRegex = /^(  )?[+] [A-Z].*[:.!?]$/;\n\n\tconst test = ( line, regex ) => null !== line.match( regex );\n\n\tif ( entry.includes( '\\n' ) ) {\n\t\treturn entry.split( '\\n' ).filter( ( line ) => line ).every( ( line ) => test( line, multiLineRegex ) );\n\t}\n\n\treturn test( entry, singleLineRegex );\n}\n\n/**\n * Object describing changelog validation issues found with a specified ChangelogEntry.\n *\n * @typedef {Object} ChangelogValidationIssues\n * @property {boolean}  valid    Whether or not validation errors were found.\n * @property {string[]} errors   Array of validation error messages.\n * @property {string[]} warnings Array of validation warning messages.\n */\n\n/**\n * Retrieve a list of changelog validation issues.\n *\n * @since 0.0.1\n *\n * @param {ChangelogEntry} logEntry   The changelog entry object to validate.\n * @param {boolean}        formatting Whether or not messages should include formatting (colors and bold).\n * @return {ChangelogValidationIssues} Encountered validation issues\n */\nfunction getChangelogValidationIssues( logEntry, formatting = true ) {\n\tconst options = getChangelogOptions(),\n\t\terrors = [],\n\t\twarnings = [];\n\n\t// Check required fields.\n\t[ 'significance', 'type', 'entry' ].forEach( ( key ) => {\n\t\tif ( ! logEntry[ key ] ) {\n\t\t\terrors.push( `Missing required field: ${ highlight( key, formatting ) }.` );\n\t\t}\n\t} );\n\n\t// Validate the entry.\n\tif ( logEntry.entry && ! isEntryValid( logEntry.entry ) ) {\n\t\terrors.push( `The submitted entry text did not pass validation.` );\n\t}\n\n\t// Validate enum values.\n\t[ 'significance', 'type' ].forEach( ( key ) => {\n\t\tif ( logEntry[ key ] && ! Object.keys( options[ key ] ).includes( logEntry[ key ] ) ) {\n\t\t\terrors.push( `Invalid value ${ highlight( logEntry[ key ], formatting ) } supplied for field: ${ highlight( key, formatting ) }.` );\n\t\t}\n\t} );\n\n\t// Warn when encountering extra/non-standard keys.\n\tObject.keys( logEntry )\n\t\t// Expected keys.\n\t\t.filter( ( k ) => ! Object.keys( ChangelogEntry ).includes( k ) )\n\t\t.forEach( ( key ) => {\n\t\t\twarnings.push( `Unexpected key: ${ highlight( key, formatting ) }.` );\n\t\t} );\n\n\t// Ensure array fields are arrays.\n\t[ 'links', 'attributions' ].forEach( ( key ) => {\n\t\tif ( logEntry[ key ] && ! Array.isArray( logEntry[ key ] ) ) {\n\t\t\terrors.push( `The ${ highlight( key, formatting ) } field must be an array.` );\n\t\t}\n\t} );\n\n\t// Validate all links.\n\tif ( Array.isArray( logEntry.links ) ) {\n\t\tlogEntry.links.forEach( ( link ) => {\n\t\t\tif ( ! isLinkValid( link ) ) {\n\t\t\t\terrors.push( `The link ${ highlight( link, formatting ) } is invalid.` );\n\t\t\t}\n\t\t} );\n\t}\n\n\t// Validate all attributions.\n\tif ( Array.isArray( logEntry.attributions ) ) {\n\t\tlogEntry.attributions.forEach( ( attribution ) => {\n\t\t\tif ( ! isAttributionValid( attribution ) ) {\n\t\t\t\terrors.push( `The attribution ${ highlight( attribution, formatting ) } is invalid.` );\n\t\t\t}\n\t\t} );\n\t}\n\n\treturn {\n\t\tvalid: 0 === errors.length,\n\t\terrors,\n\t\twarnings,\n\t};\n}\n\nmodule.exports = {\n\tisAttributionValid,\n\tisEntryValid,\n\tisLinkValid,\n\tgetChangelogValidationIssues,\n};\n"
  },
  {
    "path": "packages/dev/test/utils/configs.test.js",
    "content": "const { getConfig, hasConfig } = require( '../../src/utils' );\n\ndescribe( 'hasConfig', () => {\n\tit( 'should return `true` if the project has the specified config file', () => {\n\t\texpect( hasConfig( 'package' ) ).toBe( true );\n\t\texpect( hasConfig( 'composer' ) ).toBe( true );\n\t} );\n\n\tit( 'should return `false` if the project does not have the config or the config is empty', () => {\n\t\texpect( hasConfig( 'fake' ) ).toBe( false );\n\t} );\n} );\n\ndescribe( 'getConfig', () => {\n\tit( 'should return the config file as a JS object', () => {\n\t\tconst expectedPkg = require( '../../../../package.json' ),\n\t\t\texpectedComposer = require( '../../../../composer.json' );\n\n\t\texpect( getConfig( 'package' ) ).toStrictEqual( expectedPkg );\n\t\texpect( getConfig( 'composer' ) ).toStrictEqual( expectedComposer );\n\t} );\n\n\tit( 'should return an empty object if the project does not have the specified config', () => {\n\t\texpect( getConfig( 'fake' ) ).toStrictEqual( {} );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/determine-version-increment.test.js",
    "content": "jest.mock( '../../src/utils/get-changelog-entries' );\n\nconst getChangelogEntries = require( '../../src/utils/get-changelog-entries' ),\n\tdetermineVersionIncrement = require( '../../src/utils/determine-version-increment' );\n\nlet mockedEntries;\n\n/**\n * Create mock entries for the return of getChangelogEntries().\n *\n * @since 0.0.2\n *\n * @param {string} significance Highest significance to add to the list.\n * @return {Object[]} Array of partial log entry objects.\n */\nfunction setupMockEntries( significance ) {\n\tconst entries = [];\n\n\tfor ( let i = 0; i <= 3; i++ ) {\n\t\tentries.push( { significance: 'patch' } );\n\t}\n\n\tentries.push( { significance } );\n\n\t// Shuffle entries.\n\treturn entries.slice().sort( () => Math.random() - 0.5 );\n}\n\ngetChangelogEntries.mockImplementation( () => mockedEntries );\n\ndescribe( 'determineVersionIncrement', () => {\n\tit.each( [ 'major', 'minor', 'patch' ] )( 'Should return \"%s\" when it is the highest significance', ( significance ) => {\n\t\tmockedEntries = setupMockEntries( significance );\n\t\texpect( determineVersionIncrement( 'dir', '1.0.0' ) ).toBe( significance );\n\t} );\n\n\tit.each( [ 'major', 'minor', 'patch' ] )( 'Should return \"pre%s\" when it is the highest significance, a preid is specified, and the current version is not a prerelease', ( significance ) => {\n\t\tmockedEntries = setupMockEntries( significance );\n\t\texpect( determineVersionIncrement( 'dir', '1.0.0', 'beta' ) ).toBe( `pre${ significance }` );\n\t} );\n\n\tconst testData = [\n\t\t[ '1.0.0-beta.1', 'beta' ],\n\t\t[ '1.3.0-rc.3', 'alpha' ],\n\t\t[ '5.2.0-alpha.23', 'rc' ],\n\t];\n\tit.each( testData )( 'Should return \"prerelease\" for currentVersion=%s and preid=%s', ( currVersion, preid ) => {\n\t\texpect( determineVersionIncrement( 'dir', currVersion, preid ) ).toBe( 'prerelease' );\n\t} );\n\n\tit( 'Should return \"patch\" when no entries can be found', () => {\n\t\tmockedEntries = [];\n\t\texpect( determineVersionIncrement( 'dir', '1.0.0' ) ).toBe( 'patch' );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/exec-sync.test.js",
    "content": "// eslint-disable-next-line camelcase\nconst childProcess = require( 'child_process' ),\n\t{ execSync } = require( '../../src/utils' );\n\njest.mock( 'child_process' );\n\n// Mock submitted command options.\nlet cmdOpts = {};\n\nchildProcess.execSync.mockImplementation( ( cmd, opts ) => cmdOpts = opts );\n\n/**\n * Test execSync utility.\n *\n * Note this test doesn't attempt to ensure the output is correct as it assumes\n * that child_proccess.execSync() works as expected. This tests only our logic\n * to ensure that we properly merge the options passed to execSync.\n */\ndescribe( 'execSync', () => {\n\t// Reset mocked options.\n\tbeforeEach( () => {\n\t\tcmdOpts = {};\n\t} );\n\n\tit( 'should output command output by default', () => {\n\t\texecSync( 'echo \"HELLO\"' );\n\t\texpect( cmdOpts ).toStrictEqual( { stdio: 'inherit' } );\n\t} );\n\n\tit( 'should silence output', () => {\n\t\texecSync( 'echo \"HELLO\"', true );\n\t\texpect( cmdOpts ).toStrictEqual( { stdio: 'pipe' } );\n\t} );\n\n\tit( 'should add accept additional arguments', () => {\n\t\texecSync( 'echo \"HELLO\"', true, { timeout: 1000 } );\n\t\texpect( cmdOpts ).toStrictEqual( { stdio: 'pipe', timeout: 1000 } );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/get-archive-filename.test.js",
    "content": "const { getArchiveFilename } = require( '../../src/utils' );\n\ndescribe( 'getArchiveFilename', () => {\n\tit( 'should assume the current version when no version is specified', () => {\n\t\tconst { version } = require( '../../../../package.json' );\n\t\texpect( getArchiveFilename() ).toBe( `lifterlms-${ version }.zip` );\n\t} );\n\n\tit( 'should add the specified version', () => {\n\t\texpect( getArchiveFilename( '9.9.9' ) ).toBe( 'lifterlms-9.9.9.zip' );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/get-changelog-for-version.test.js",
    "content": "const { getChangelogForVersion } = require( '../../src/utils' );\n\ndescribe( 'getChangelogForVersion', () => {\n\tit.each( [ '5.4.0', '3.0.0', '1.0.1' ] )( 'should retrieve the changelog for the existing version %s', ( version ) => {\n\t\tconst entry = getChangelogForVersion( version, process.cwd() + '/CHANGELOG.md' );\n\t\texpect( typeof entry ).toBe( 'object' );\n\t\texpect( entry.version ).toStrictEqual( version );\n\t\texpect( Object.keys( entry ) ).toStrictEqual( [ 'date', 'version', 'logs' ] );\n\t} );\n\n\tit( 'should return undefined for non-existent versions', () => {\n\t\tconst entry = getChangelogForVersion( '0.0.1', process.cwd() + '/CHANGELOG.md' );\n\t\texpect( entry ).toBeUndefined();\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/get-next-version.test.js",
    "content": "const\n\t{ getNextVersion } = require( '../../src/utils' );\n\ndescribe( 'getNextVersion', () => {\n\tdescribe( 'increment=patch', () => {\n\t\tconst testData = [\n\t\t\t[ '1.0.0', '1.0.1' ],\n\t\t\t[ '5.1.2', '5.1.3' ],\n\t\t\t[ '0.12.9', '0.12.10' ],\n\t\t\t[ '1.0.0-beta.1', '1.0.0' ],\n\t\t\t[ '999.9.9-rc.1', '999.9.9' ],\n\t\t];\n\t\tit.each( testData )( 'Should increment %s to %s', ( current, next ) => {\n\t\t\texpect( getNextVersion( current, 'patch' ) ).toBe( next );\n\t\t} );\n\t} );\n\n\tdescribe( 'increment=minor', () => {\n\t\tconst testData = [\n\t\t\t[ '1.0.0', '1.1.0' ],\n\t\t\t[ '5.3.9', '5.4.0' ],\n\t\t\t[ '0.54.3', '0.55.0' ],\n\t\t\t[ '1.0.0-beta.1', '1.0.0' ],\n\t\t\t[ '999.9.9-rc.1', '999.10.0' ],\n\t\t];\n\t\tit.each( testData )( 'Should increment %s to %s', ( current, next ) => {\n\t\t\texpect( getNextVersion( current, 'minor' ) ).toBe( next );\n\t\t} );\n\t} );\n\n\tdescribe( 'increment=major', () => {\n\t\tconst testData = [\n\t\t\t[ '1.0.0', '2.0.0' ],\n\t\t\t[ '1.15.999', '2.0.0' ],\n\t\t\t[ '5.3.9', '6.0.0' ],\n\t\t\t[ '0.54.3', '1.0.0' ],\n\t\t\t[ '1.0.0-beta.1', '1.0.0' ],\n\t\t\t[ '999.9.9-rc.1', '1000.0.0' ],\n\t\t];\n\t\tit.each( testData )( 'Should increment %s to %s', ( current, next ) => {\n\t\t\texpect( getNextVersion( current, 'major' ) ).toBe( next );\n\t\t} );\n\t} );\n\n\tdescribe( 'increment=prerelease preid=beta', () => {\n\t\tconst testData = [\n\t\t\t[ '1.0.0', '1.0.1-beta.1' ],\n\t\t\t[ '1.0.0-beta.1', '1.0.0-beta.2' ],\n\t\t\t[ '3.2.5', '3.2.6-beta.1' ],\n\t\t];\n\t\tit.each( testData )( 'Should increment %s to %s', ( current, next ) => {\n\t\t\texpect( getNextVersion( current, 'prerelease', 'beta' ) ).toBe( next );\n\t\t} );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/get-project-privacy.test.js",
    "content": "jest.mock( '../../src/utils/get-project-slug' );\n\n// eslint-disable-next-line camelcase\nconst childProcess = require( 'child_process' ),\n\tgetProjectSlug = require( '../../src/utils/get-project-slug' ),\n\t{ getProjectPrivacy, isProjectPublic, isProjectPrivate } = require( '../../src/utils/get-project-privacy' );\n\n// Mocked return values.\nlet mockedSlug,\n\tmockedApiReturn;\n\njest.mock( 'child_process' );\nchildProcess.execSync.mockImplementation( () => mockedApiReturn );\n\ngetProjectSlug.mockImplementation( () => mockedSlug );\n\n/**\n * Mock the API return retrieved by `gh api...`.\n *\n * @since 0.1.0\n *\n * @param {boolean} isPublic Whether or not the repo is public. Pass `undefined` to for an \"unknown\" return.\n * @return {string|Object} A JSON string or object to be parsed. Returning an object causes an error for the test unknown responses.\n */\nfunction getMockApiReturn( isPublic ) {\n\tif ( undefined === isPublic ) {\n\t\treturn {}; // Causes an error which is enough to get the proper return.\n\t}\n\treturn JSON.stringify( { private: ! isPublic } );\n}\n\ndescribe( 'getProjectPrivacy', () => {\n\tconst testData = [\n\t\t[ 'Should return \"public\" for public repos', 'lifterlms', 'public', true ],\n\t\t[ 'Should return \"private\" for private repos', 'lifterlms-groups', 'private', false ],\n\t\t[ 'Should return \"unknown\" for invalid repos', 'fake-repo', 'unknown', undefined ],\n\t];\n\tit.each( testData )( '%s', ( name, slug, expected, isPublic ) => {\n\t\tmockedApiReturn = getMockApiReturn( isPublic );\n\t\tmockedSlug = slug;\n\t\texpect( getProjectPrivacy() ).toBe( expected );\n\t} );\n} );\n\ndescribe( 'isProjectPublic', () => {\n\tconst testData = [\n\t\t[ 'Should return true for public repos', 'lifterlms', true ],\n\t\t[ 'Should return false for private repos', 'lifterlms-groups', false ],\n\t\t[ 'Should return undefined for invalid repos', 'fake-repo', undefined ],\n\t];\n\tit.each( testData )( '%s', ( name, slug, expected ) => {\n\t\tmockedApiReturn = getMockApiReturn( expected );\n\t\tmockedSlug = slug;\n\t\texpect( isProjectPublic() ).toBe( expected );\n\t} );\n} );\n\ndescribe( 'isProjectPrivate', () => {\n\tconst testData = [\n\t\t[ 'Should return false for public repos', 'lifterlms', false, true ],\n\t\t[ 'Should return true for private repos', 'lifterlms-groups', true, false ],\n\t\t[ 'Should return undefined for invalid repos', 'fake-repo', undefined, undefined ],\n\t];\n\tit.each( testData )( '%s', ( name, slug, expected, isPublic ) => {\n\t\tmockedApiReturn = getMockApiReturn( isPublic );\n\t\tmockedSlug = slug;\n\t\texpect( isProjectPrivate() ).toBe( expected );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/get-project-slug.test.js",
    "content": "const { getProjectSlug } = require( '../../src/utils' );\n\ndescribe( 'getProjectSlug', () => {\n\tit( 'should return the directory name of the project', () => {\n\t\texpect( getProjectSlug() ).toBe( 'lifterlms' );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/parse-changelog-file.test.js",
    "content": "const\n\tsemver = require( 'semver' ),\n\t{ parseChangelogFile } = require( '../../src/utils' );\n\ndescribe( 'parseChangelogFile', () => {\n\tit( 'should parse the changelog file', () => {\n\t\tconst parsed = parseChangelogFile( process.cwd() + '/CHANGELOG.md' );\n\n\t\tparsed.forEach( ( { date, version, logs } ) => {\n\t\t\t// Valid version.\n\t\t\texpect( semver.valid( version ) ).toStrictEqual( version );\n\n\t\t\t// Valid date.\n\t\t\texpect( Date.parse( date ) ).not.toBeNaN();\n\n\t\t\t// Should be a string.\n\t\t\texpect( typeof logs ).toStrictEqual( 'string' );\n\t\t} );\n\t} );\n} );\n"
  },
  {
    "path": "packages/dev/test/utils/parse-issue-string.test.js",
    "content": "const { parseIssueString } = require( '../../src/utils' );\n\ndescribe( 'parseIssueString', () => {\n\tconst testData = [\n\t\t[ 'Should accept issue references to the current project', { org: 'gocodebox', repo: 'lifterlms', num: '123' }, '#123' ],\n\t\t[ 'Should accept issue references to the current project', { org: 'org', repo: 'repo', num: '456' }, 'org/repo#456' ],\n\t];\n\tit.each( testData )( '%s', ( name, expected, issue ) => {\n\t\texpect( parseIssueString( issue ) ).toStrictEqual( expected );\n\t} );\n} );\n\n"
  },
  {
    "path": "packages/dev/test/utils/repo-links.test.js",
    "content": "jest.mock( '../../src/utils/get-project-slug' );\n\nconst { getFileLink, getRepoLink, getIssueLink } = require( '../../src/utils' ),\n\tgetProjectSlug = require( '../../src/utils/get-project-slug' );\n\nlet mockedSlug = '';\ngetProjectSlug.mockImplementation( () => mockedSlug ? mockedSlug : 'lifterlms' );\n\ndescribe( 'repoLinks', () => {\n\tbeforeEach( () => {\n\t\tmockedSlug = 'lifterlms';\n\t} );\n\n\tdescribe( 'getFileLink', () => {\n\t\tconst path = 'inc/file.php',\n\t\t\ttestData = [\n\t\t\t\t[ 'Should use trunk if a branch is not specified', 'https://github.com/gocodebox/lifterlms/blob/trunk/inc/file.php' ],\n\t\t\t\t[ 'Should use the specified branch', 'https://github.com/gocodebox/lifterlms/blob/dev-123/inc/file.php', 'dev-123' ],\n\t\t\t\t[ 'Should use the specified version tag', 'https://github.com/gocodebox/lifterlms/blob/1.0.0/inc/file.php', '1.0.0' ],\n\t\t\t\t[ 'Should use the specified prerelease version tag', 'https://github.com/gocodebox/lifterlms/blob/1.0.0-beta.3/inc/file.php', '1.0.0-beta.3' ],\n\t\t\t];\n\t\tit.each( testData )( '%s', ( name, expected, branch = undefined ) => {\n\t\t\texpect( getFileLink( path, branch ) ).toBe( expected );\n\t\t} );\n\t} );\n\n\tdescribe( 'getIssueLink', () => {\n\t\tconst testData = [\n\t\t\t[ 'Should accept issue references to the current project', 'https://github.com/gocodebox/lifterlms/issues/123', '#123' ],\n\t\t\t[ 'Should accept issue references to the another project', 'https://github.com/org/repo/issues/456', 'org/repo#456' ],\n\t\t];\n\t\tit.each( testData )( '%s', ( name, expected, issue ) => {\n\t\t\texpect( getIssueLink( issue ) ).toBe( expected );\n\t\t} );\n\t} );\n\n\tdescribe( 'getRepoLink', () => {\n\t\tconst testData = [\n\t\t\t[ 'Should use the default slug and organization when no values (undefined) are provided', 'https://github.com/gocodebox/lifterlms', undefined, undefined ],\n\t\t\t[ 'Should use the default slug and organization when null values are provided', 'https://github.com/gocodebox/lifterlms', null, null ],\n\t\t\t[ 'Should use the default slug and organization when empty values are provided', 'https://github.com/gocodebox/lifterlms', '', false ],\n\t\t\t[ 'Should use the specified slug and organization when provided', 'https://github.com/org/slug', 'slug', 'org' ],\n\t\t];\n\t\tit.each( testData )( '%s', ( name, expected, project, org ) => {\n\t\t\tmockedSlug = project;\n\t\t\texpect( getRepoLink( project, org ) ).toBe( expected );\n\t\t} );\n\t} );\n} );\n\n"
  },
  {
    "path": "packages/dev/test/utils/validate-changelog.test.js",
    "content": "const {\n\tisAttributionValid,\n\tisEntryValid,\n\tisLinkValid,\n\tgetChangelogValidationIssues,\n} = require( '../../src/utils' );\n\ndescribe( 'isAttributionValid', () => {\n\tconst testData = [\n\t\t// Valid data.\n\t\t[ 'Should accept a GitHub username', '@username', true ],\n\t\t[ 'Should accept a markdown link', '[username](https://fake.tld)', true ],\n\t\t// Invalid data.\n\t\t[ 'Should not accept a username without a leading @ symbol', 'username', false ],\n\t\t[ 'Should not accept a markdown link without a fully qualified URL', '[username](www.fake.tld)', false ],\n\t\t[ 'Should not accept a markdown reference link', '[username][link]', false ],\n\t\t// Weird types.\n\t\t[ 'Should not accept an integer', 123, false ],\n\t\t[ 'Should not accept an object', {}, false ],\n\t\t[ 'Should not accept an array', [], false ],\n\t];\n\tit.each( testData )( '%s', ( name, input, expected ) => {\n\t\texpect( isAttributionValid( input ) ).toStrictEqual( expected );\n\t} );\n} );\n\ndescribe( 'isEntryValid', () => {\n\tdescribe( 'Single-line entries', () => {\n\t\tconst testData = [\n\t\t\t// Valid.\n\t\t\t[ 'Should accept any capital letter and full stop at the end: A -> period', 'A valid line.', true ],\n\t\t\t[ 'Should accept any capital letter and full stop at the end: T -> period', 'This is also valid.', true ],\n\t\t\t[ 'Should accept any capital letter and full stop at the end: Z -> question mark', 'Z This is also valid?', true ],\n\t\t\t[ 'Should accept any capital letter and full stop at the end: P -> exclamation point', 'Plus this is too!', true ],\n\t\t\t[ 'Contains multiple sentences', 'Are multiple sentences okay? Yes, This is okay.', true ],\n\t\t\t// Invalid.\n\t\t\t[ 'Should not start with a plus bullet character', '+ The bullet is added automatically so this is invalid.', false ],\n\t\t\t[ 'Should not start with a minus bullet character', '- Other types of bullets are also invalid.', false ],\n\t\t\t[ 'Should not start with a lowercase letter', 'must be capital letter.', false ],\n\t\t\t[ 'Should not end without a full stop character', 'Must end in a fullstop', false ],\n\t\t\t[ 'Should not start with a number', '1 is numeric so it\\'s invalid!', false ],\n\t\t\t[ 'Should not start with leading spaces', ' Leading spaces are invalid.', false ],\n\t\t\t[ 'Should not start with leading tabs', '\tLeading tabs are invalid.', false ],\n\t\t\t[ 'Should not end with trailing new lines', 'Trailing tabs are invalid.\\n', false ],\n\t\t\t[ 'Should not end with trailing tabs', 'Trailing tabs are invalid.\t', false ],\n\t\t\t[ 'Should not end with trailing space', 'Trailing spaces are invalid. ', false ],\n\t\t\t[ 'Should not end have multiple sentences without a full stop at the end', 'Trailing characters are invalid. Another', false ],\n\t\t];\n\t\tit.each( testData )( '%s', ( name, input, expected ) => {\n\t\t\texpect( isEntryValid( input ) ).toStrictEqual( expected );\n\t\t} );\n\t} );\n\tdescribe( 'Multi-line entries', () => {\n\t\tconst testData = [\n\t\t\t// Valid.\n\t\t\t[ 'Should accept a multi-line entry with only a single valid line', '+ A single valid line.\\n', true ],\n\t\t\t[ 'Should accept any number of valid lines', '+ Multiple valid lines?\\n+ Of course.\\n+ They\\'re okay!', true ],\n\t\t\t[ 'Should accept nested indented lines and lines ending in a colon', '+ Nested indentations are okay:\\n  + Yes.\\n  + Me 2.', true ],\n\t\t\t// Invalid.\n\t\t\t[ 'Should not allow the minus character to be used as a bullet', '- No minus signs allowed.\\n', false ],\n\t\t\t[ 'Should not allow indentation greater than one level deep', '   + This has three spaces.\\n', false ],\n\t\t\t[ 'Should not allow any lines missing a full stop', '+ Not okay\\n+ Okay!', false ],\n\t\t\t[ 'Should not allow any lines with improper capitalization', '+ capitalization is still important.\\n+ Okay!', false ],\n\t\t];\n\t\tit.each( testData )( '%s', ( name, input, expected ) => {\n\t\t\texpect( isEntryValid( input ) ).toStrictEqual( expected );\n\t\t} );\n\t} );\n} );\n\ndescribe( 'isLinkValid', () => {\n\tconst testData = [\n\t\t// Valid data.\n\t\t[ 'Should accept a reference to the current repo', '#123', true ],\n\t\t[ 'Should accept a reference to another repo', 'org/repo#123', true ],\n\t\t// Invalid data.\n\t\t[ 'Should not accept a local reference without a # symbol', '123', false ],\n\t\t[ 'Should not accept a reference to another repo without a # symbol', 'org/repo123', false ],\n\t\t[ 'Should not accept a reference to another repo without an organization', 'repo#123', false ],\n\t\t// Weird types.\n\t\t[ 'Should not accept an integer', 123, false ],\n\t\t[ 'Should not accept an object', {}, false ],\n\t\t[ 'Should not accept an array', [], false ],\n\t];\n\tit.each( testData )( '%s', ( name, input, expected ) => {\n\t\texpect( isLinkValid( input ) ).toStrictEqual( expected );\n\t} );\n} );\n\ndescribe( 'getChangelogValidationIssues', () => {\n\tit( 'should return errors when missing required fields', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( {}, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'Missing required field: \"significance\".', 'Missing required field: \"type\".', 'Missing required field: \"entry\".' ] );\n\t} );\n\n\tit( 'should return errors for invalid entry values', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( { significance: 'patch', type: 'changed', entry: 'invalid' }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'The submitted entry text did not pass validation.' ] );\n\t} );\n\n\tit( 'should return errors for invalid significance values', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( { significance: 'fake', type: 'changed', entry: 'Valid.' }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'Invalid value \"fake\" supplied for field: \"significance\".' ] );\n\t} );\n\n\tit( 'should return errors for invalid type values', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( { type: 'fake', significance: 'patch', entry: 'Valid.' }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'Invalid value \"fake\" supplied for field: \"type\".' ] );\n\t} );\n\n\tit( 'should return warnings when non-standard keys are found in the entry object', () => {\n\t\tconst { valid, warnings } = getChangelogValidationIssues( { extra: 1 }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [ 'Unexpected key: \"extra\".' ] );\n\t} );\n\n\tit( 'should return errors when an array is not submitted for the attributions list', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( { attributions: 1, type: 'changed', significance: 'patch', entry: 'Valid.' }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'The \"attributions\" field must be an array.' ] );\n\t} );\n\n\tit( 'should return errors when an array is not submitted for the links list', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( { links: 1, type: 'changed', significance: 'patch', entry: 'Valid.' }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'The \"links\" field must be an array.' ] );\n\t} );\n\n\tit( 'should return errors when an invalid attribution is submitted', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( { attributions: [ 'abc' ], type: 'changed', significance: 'patch', entry: 'Valid.' }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'The attribution \"abc\" is invalid.' ] );\n\t} );\n\n\tit( 'should return errors when an invalid link is submitted', () => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( { links: [ 'abc' ], type: 'changed', significance: 'patch', entry: 'Valid.' }, false );\n\n\t\texpect( valid ).toStrictEqual( false );\n\t\texpect( warnings ).toStrictEqual( [] );\n\t\texpect( errors ).toStrictEqual( [ 'The link \"abc\" is invalid.' ] );\n\t} );\n\n\tconst testData = [\n\t\t[\n\t\t\t'should validate a valid entry that is missing optional fields',\n\t\t\t{\n\t\t\t\tsignificance: 'major',\n\t\t\t\ttype: 'added',\n\t\t\t\tentry: 'Entry content.',\n\t\t\t},\n\t\t\t[],\n\t\t],\n\t\t[\n\t\t\t'should validate a valid entry that includes valid optional fields',\n\t\t\t{\n\t\t\t\tsignificance: 'major',\n\t\t\t\ttype: 'added',\n\t\t\t\tentry: 'Entry content.',\n\t\t\t\tcomment: 'A comment',\n\t\t\t\ttitle: 'title',\n\t\t\t\tattributions: [ '@username', '[user](https://fake.tld)' ],\n\t\t\t\tlinks: [ '#1234', 'org/repo#123' ],\n\t\t\t},\n\t\t\t[],\n\t\t],\n\t\t[\n\t\t\t'should validate a valid entry that has warnings and no errors',\n\t\t\t{\n\t\t\t\tsignificance: 'major',\n\t\t\t\ttype: 'added',\n\t\t\t\tentry: 'Entry content.',\n\t\t\t\tfake: 'extra-field-generates-warning',\n\t\t\t},\n\t\t\t[ 'Unexpected key: \"fake\".' ],\n\t\t],\n\t];\n\tit.each( testData )( '%s', ( name, entry, expectedWarnings ) => {\n\t\tconst { valid, errors, warnings } = getChangelogValidationIssues( entry, false );\n\t\texpect( valid ).toStrictEqual( true );\n\t\texpect( warnings ).toStrictEqual( expectedWarnings );\n\t\texpect( errors ).toStrictEqual( [] );\n\t} );\n} );\n"
  },
  {
    "path": "packages/fontawesome/.eslintrc.js",
    "content": "const config = require( '../scripts/config/.eslintrc.js' );\nmodule.exports = config;\n"
  },
  {
    "path": "packages/fontawesome/.npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "packages/fontawesome/CHANGELOG.md",
    "content": "@lifterlms/fontawesome CHANGELOG\n================================\n\nv0.0.1 - 2022-08-11\n-------------------\n\n+ Initial public release.\n"
  },
  {
    "path": "packages/fontawesome/README.md",
    "content": "# LifterLMS Font Awesome\n\nA wrapper around [Font Awesome](https://github.com/FortAwesome/Font-Awesome) and a collection of related React components for use in LifterLMS projects.\n\n* * *\n\n## Changelog\n\n[View the Changelog](./CHANGELOG.md)\n\n## Configure and generate CSS files\n\nTo create the relevant CSS file which includes all the free icons and necessary CSS classes, create an SCSS file:\n\n```scss\n$llms-css-prefix: my-prefix-fa;\n@import '@lifterlms/fontawesome/src/fontawesome';\n```\n\nThe `$llms-css-prefix` variable allows creation of the Font Awesome CSS file in a \"no-conflict\" mode using a different prefix than the default `fa` prefix commonly used with Font Awesome. The default prefix, `llms-fa` is used by the LifterLMS core plugin. Any other projects should choose a unique prefix in order to avoid conflicts with other plugins (or LifterLMS) which may be loading various other versions of Font Awesome.\n\nThen add an entry to your webpack config file, if you're using `@lifterlms/scripts/config/webpack.config.js`:\n\n```js\nconst { resolve } = require( 'path' ),\n\tconfig = generate( {} );\n\nconfig.entry.fontawesome = resolve( './src/scss/fa-file.scss' );\n\nmodule.exports = config;\n```\n\nWhen building you'll now find the Font Awesome CSS file at `assets/css/fa-file.scss` and the `assets/fonts` directory will contain copies of the Font Awesome font files.\n\n## Using SVGs\n\nThe above steps enable using Font Awesome as a webfont. If you wish to instead use SVGs, you may wish to copy the SVGs to your project's directory. The [svg](./bin/svg.js) script can be used to copy the source SVGs into you project.\n\n```bash\nnode ./node_modules/@lifterlms/fontawesome/bin/svg.js [destDir]    \n```\n\nThe `destDir` parameter defaults to `./src/img/fontawesome` if omitted.\n\n## Component and API Docs\n\n<!-- START TOKEN(Autogenerated API docs) -->\n\n### getMetadata\n\nRetrieves metadata for a given icon.\n\n_Parameters_\n\n-   _iconId_ `string`: The icon ID.\n\n_Returns_\n\n-   `IconMeta|boolean`: An icon metadata object or `false` if the icon can't be found.\n\n### Icon\n\nRenders a Font Awesome icon.\n\n_Parameters_\n\n-   _props_ `Object`: Component properties.\n-   _props.icon_ `string`: The Icon ID.\n-   _props.iconStyle_ `string`: The icon style, enum: \"solid\", \"regular\", or \"brands\".\n-   _props.iconPrefix_ `string`: The project's icon prefix.\n-   _props.label_ `string`: The (optional) accessibility label to display for the icon.\n-   _props.wrapperProps_ `...Object`: Any remaining properties which are passed to the icon wrapper component.\n\n_Returns_\n\n-   `WPElement`: The component.\n\n### IconPicker\n\nRenders an icon picker component, intended to be used within the WordPress block editor.\n\n_Parameters_\n\n-   _props_ `Object`: Component properties.\n-   _props.icon_ `string`: The Icon ID.\n-   _props.iconStyle_ `string`: The icon style, enum: \"solid\", \"regular\", or \"brands\".\n-   _props.iconPrefix_ `string`: The project's icon prefix.\n-   _props.controlProps_ `Object`: Properties to pass through to the <BaseControl> component.\n-   _props.onChange_ `Function`: Function called when an icon is selected from the picker. The function is passed three properties: The icon ID, the currently selected style, and the icon's predefined label.\n\n_Returns_\n\n-   `BaseControl`: A BaseControl containing the icon picker component.\n\n\n<!-- END TOKEN(Autogenerated API docs) -->\n"
  },
  {
    "path": "packages/fontawesome/babel.config.js",
    "content": "/**\n * Babel config\n *\n * @package\n *\n * @since 1.0.0\n * @version 1.0.0\n */\n\nconst presets = [ '@wordpress/default' ];\n\nmodule.exports = { presets };\n"
  },
  {
    "path": "packages/fontawesome/bin/.eslintrc.js",
    "content": "module.exports = {\n\trules: {\n\t\t'no-console': 'off',\n\t},\n};\n"
  },
  {
    "path": "packages/fontawesome/bin/metadata.js",
    "content": "#!/usr/bin/node\n\n/**\n * A CLI utility used to create the FontAwesome icon metadata file located at `src/metadata.json`\n *\n * This utility parses YAML metadata included with the FA package and converts it to our desired format.\n *\n * This is an internal script intended to be run during package builds before release or after updating the FontAwesome dependency.\n *\n * @since 0.0.1\n * @version 0.0.1\n *\n * Example usage:\n *\n * node ./bin/metadata.js\n */\n\nconst\n\t{ resolve } = require( 'path' ),\n\tYAML = require( 'yaml' ),\n\t{ existsSync, readFileSync, writeFileSync } = require( 'fs' ),\n\tMETADATA_FILE = resolve( './node_modules/@fortawesome/fontawesome-free/metadata/icons.yml' ),\n\tSRC_DIR = resolve( './src' );\n\nif ( ! existsSync( METADATA_FILE ) ) {\n\tconsole.error( 'Cannot locate the source metadata file, please run `npm i` and try again.' );\n\tprocess.exit( 1 );\n}\n\nconst origMetadata = YAML.parse( readFileSync( METADATA_FILE, 'utf8' ) ),\n\tmetadata = {};\n\nObject.keys( origMetadata ).forEach( ( id ) => {\n\tconst { styles, label, aliases } = origMetadata[ id ],\n\t\tterms = aliases?.names || [];\n\n\tmetadata[ id ] = { styles, label, terms: [ ...terms, label, id ] };\n} );\n\nwriteFileSync( `${ SRC_DIR }/metadata.json`, JSON.stringify( metadata, null, 2 ) );\n\nconsole.log( `Successfully created ${ SRC_DIR }/metadata.json` );\n"
  },
  {
    "path": "packages/fontawesome/bin/svg.js",
    "content": "#!/usr/bin/node\n\n/**\n * A CLI utility used to copy source SVGs files from the FontAwesome package to a specified location.\n *\n * This script is intended to be used by projects including SVGs through this package.\n *\n * @since 0.0.1\n * @version 0.0.1\n *\n * Example usage:\n *\n *   node ./node_modules/@lifterlms/fontawesome/bin/svg.js [distDirectory]\n *\n * The distDirectory parameter is optional and defaults to `./src/img/fontawesome`.\n */\n\nconst\n\t{ argv } = process,\n\tsrcDir = argv[ 2 ] || './src/img/fontawesome',\n\t{ resolve } = require( 'path' ),\n\t{ cpSync, existsSync } = require( 'fs' ),\n\tSVG_DIR = resolve( './node_modules/@fortawesome/fontawesome-free/svgs' ),\n\tSRC_DIR = resolve( srcDir );\n\nconst bold = '\\x1b[32m\\x1b[1m',\n\treset = '\\x1b[0m';\n\nif ( ! existsSync( SVG_DIR ) ) {\n\tconsole.error( 'Cannot locate the SVG source directory, please run `npm i` and try again.' );\n\tprocess.exit( 1 );\n}\n\nconsole.log( `Copying SVG files from ${ bold }${ SVG_DIR }${ reset } to ${ bold }${ SRC_DIR }${ reset }.` );\n\ncpSync( SVG_DIR, SRC_DIR, { recursive: true } );\n"
  },
  {
    "path": "packages/fontawesome/package.json",
    "content": "{\n  \"name\": \"@lifterlms/fontawesome\",\n  \"version\": \"0.0.1\",\n  \"description\": \"A no-conflict fork of Font Awesome for use in LifterLMS projects.\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/trunk/packages/fontawesome\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"fontawesome\",\n    \"ui\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/fontawesome\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%fontawesome\"\n  },\n  \"main\": \"src/index.js\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"@emotion/styled\": \"^11.10.0\",\n    \"@fortawesome/fontawesome-free\": \"^6.1.2\",\n    \"react-infinite-scroller\": \"^1.2.6\"\n  },\n  \"devDependencies\": {\n    \"yaml\": \"^2.1.1\"\n  },\n  \"scripts\": {\n    \"build\": \"node ./bin/metadata.js\",\n    \"docgen\": \"docgen src/index.js --output README.md --to-token\",\n    \"dev\": \"./../dev/src/index.js\",\n    \"lint:js\": \"wp-scripts lint-js ./\",\n    \"test\": \"wp-scripts test-unit-js ./ --config ../scripts/config/jest-unit.config.js --verbose\"\n  }\n}\n"
  },
  {
    "path": "packages/fontawesome/src/components/icon-list.js",
    "content": "// External deps.\nimport styled from '@emotion/styled';\nimport InfiniteScroll from 'react-infinite-scroller';\nimport { debounce } from 'lodash';\n\nimport { __ } from '@wordpress/i18n';\nimport { Button, ButtonGroup, SearchControl } from '@wordpress/components';\nimport { useEffect, useState } from '@wordpress/element';\n\nimport iconMetadata from '../metadata.json';\n\nimport Icon from './icon';\n\n/**\n * A styled div wrapper component.\n */\nconst Wrapper = styled.div`\t\t\n\t& .llms-fa-icon-picker--content-header {\n\t\tborder-bottom: 1px solid rgb( 204, 204, 204 );\n\t\tmargin: 0 -8px;\n\t\tpadding: 0 8px 8px;\n\t}\n\t& .llms-fa-icon-picker--list {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tpadding-top: 8px;\n\t\tmax-height: 440px;\n\t\toverflow: auto;\n\t\t> div {\n\t\t\twidth: 100%;\n\t\t}\n\t}\n\t& .llms-fa-icon-picker--icon-button {\n\t\tborder: 1px solid rgba( 0, 0, 0, 0.1 );\n\t\tdisplay: inline-block;\n\t\theight: 90px;\n\t\tpadding: 12px 12px 8px;\n\t\tmargin: 2px;\n\t\twidth: calc( 25% - 4px );\n\t\tvertical-align: middle;\n\t}\n\t& .llms-fa-icon-picker--icon-button i {\n\t\tdisplay: block;\n\t\tfont-size: 24px;\n\t}\n\t& .llms-fa-icon-picker--icon-button span {\n\t\tdisplay: block;\n\t\tfont-size: 10px;\n\t}\n\t\n`;\n\nconst STYLES = [\n\t{\n\t\tlabel: __( 'Solid', 'lifterlms' ),\n\t\tid: 'solid',\n\t},\n\t{\n\t\tlabel: __( 'Regular', 'lifterlms' ),\n\t\tid: 'regular',\n\t},\n\t{\n\t\tlabel: __( 'Brands', 'lifterlms' ),\n\t\tid: 'brands',\n\t},\n];\n\n/**\n * Renders an infinitely scrollable list of the available icons.\n *\n * @since 0.0.1\n *\n * @param {Object}   props                Component properties.\n * @param {Function} props.onChange       Function to call when a new icon is selected.\n * @param {string}   props.selectedStyle  The currently selected icon style.\n * @param {Object}   props.availableIcons The available icons to display.\n * @param {string}   props.iconPrefix     The project icon prefix.\n * @param {number}   props.perPage        Number of icons to display per \"page\" of results.\n * @return {WPElement} A scrollable list.\n */\nfunction List( { onChange, selectedStyle, availableIcons, iconPrefix, perPage } ) {\n\tconst [ endIndex, setEndIndex ] = useState( perPage ),\n\t\tnumIcons = Object.keys( availableIcons ).length;\n\treturn (\n\t\t<div className=\"llms-fa-icon-picker--list\">\n\t\t\t<InfiniteScroll\n\t\t\t\tloadMore={ debounce( () => setEndIndex( endIndex + perPage ), 300 ) }\n\t\t\t\thasMore={ ! numIcons || endIndex <= numIcons ? true : false }\n\t\t\t\tuseWindow={ false }\n\t\t\t>\n\t\t\t\t{ Object.keys( availableIcons ).slice( 0, endIndex ).map( ( icon, key ) => (\n\t\t\t\t\t<Button\n\t\t\t\t\t\tkey={ key }\n\t\t\t\t\t\tclassName=\"llms-fa-icon-picker--icon-button\"\n\t\t\t\t\t\tonClick={ () => onChange( icon, selectedStyle, availableIcons[ icon ].label ) }\n\t\t\t\t\t>\n\t\t\t\t\t\t<Icon { ...{ icon, iconStyle: selectedStyle, iconPrefix } } />\n\t\t\t\t\t\t<span>{ availableIcons[ icon ].label }</span>\n\t\t\t\t\t</Button>\n\t\t\t\t) ) }\n\t\t\t</InfiniteScroll>\n\t\t</div>\n\t);\n}\n\n/**\n * Renders the icon picker list.\n *\n * @since 0.0.1\n *\n * @param {Object}   props            Component properties.\n * @param {Function} props.onChange   Icon select callback function.\n * @param {string}   props.iconPrefix Project icon prefix.\n * @param {number}   props.perPage    Number of icons to display per page of results.\n * @return {WPElement} The list component.\n */\nexport default function( { onChange, iconPrefix, perPage = 48 } ) {\n\tconst [ availableIcons, setAvailableIcons ] = useState( [] ),\n\t\t[ search, setSearch ] = useState( '' ),\n\t\t[ selectedStyle, setSelectedStyle ] = useState( STYLES[ 0 ].id );\n\n\tuseEffect( () => {\n\t\tconst normalSearch = search.toLowerCase(),\n\t\t\tfilteredIcons = Object.fromEntries(\n\t\t\t\tObject.entries( iconMetadata ).filter( ( [ , { terms, styles } ] ) => {\n\t\t\t\t\tif ( ! styles.includes( selectedStyle ) ) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn ! search || terms.map( ( term ) => term.toLowerCase() ).some( ( term ) => term.includes( normalSearch ) );\n\t\t\t\t} )\n\t\t\t);\n\n\t\tsetAvailableIcons( { ...filteredIcons } );\n\t}, [ search, selectedStyle ] );\n\n\treturn (\n\t\t<Wrapper>\n\t\t\t<header className=\"llms-fa-icon-picker--content-header\">\n\t\t\t\t<SearchControl\n\t\t\t\t\tvalue={ search }\n\t\t\t\t\tonChange={ ( newSearch ) => setSearch( newSearch ) }\n\t\t\t\t/>\n\t\t\t\t<span>{ __( 'Styles:', 'lifterlms' ) } </span>\n\t\t\t\t<ButtonGroup>\n\t\t\t\t\t{ STYLES.map( ( { label, id } ) => (\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tkey={ id }\n\t\t\t\t\t\t\ttext={ label }\n\t\t\t\t\t\t\tvariant={ selectedStyle === id ? 'primary' : 'secondary' }\n\t\t\t\t\t\t\tonClick={ () => setSelectedStyle( id ) }\n\t\t\t\t\t\t/>\n\t\t\t\t\t) ) }\n\t\t\t\t</ButtonGroup>\n\t\t\t</header>\n\t\t\t<List { ...{ onChange, selectedStyle, availableIcons, iconPrefix, perPage } } />\n\t\t</Wrapper>\n\t);\n}\n"
  },
  {
    "path": "packages/fontawesome/src/components/icon-picker.js",
    "content": "// External deps.\nimport styled from '@emotion/styled';\n\n// WP deps.\nimport { BaseControl, Button, Dropdown } from '@wordpress/components';\n\n// Internal deps.\nimport { getMetadata } from '../';\nimport Icon from './icon';\nimport IconList from './icon-list';\n\n/**\n * A <Dropdown> component with styles targeting the sub-components within it.\n *\n * @since 0.0.1\n */\nconst StyledDropdown = styled( Dropdown )`\n\tdisplay: block;\n\twidth: 100%;\n\t> .components-button {\n\t\tborder: 1px solid rgba( 0, 0, 0, 0.1 );\n\t\tborder-radius: 2px;\n\t\tpadding-bottom: 24px;\n\t\tpadding-top: 24px;\n\t\twidth: 100%;\n\t}\n`;\n\n/**\n * Renders an icon picker component, intended to be used within the WordPress block editor.\n *\n * @since 0.0.1\n *\n * @param {Object}   props              Component properties.\n * @param {string}   props.icon         The Icon ID.\n * @param {string}   props.iconStyle    The icon style, enum: \"solid\", \"regular\", or \"brands\".\n * @param {string}   props.iconPrefix   The project's icon prefix.\n * @param {Object}   props.controlProps Properties to pass through to the <BaseControl> component.\n * @param {Function} props.onChange     Function called when an icon is selected from the picker. The function is passed three properties:\n *                                      The icon ID, the currently selected style, and the icon's predefined label.\n * @return {BaseControl} A BaseControl containing the icon picker component.\n */\nexport default function( { icon, iconStyle, controlProps = {}, onChange = () => {}, iconPrefix = 'llms-fa-' } ) {\n\tconst { label } = getMetadata( icon );\n\n\treturn (\n\t\t<BaseControl { ...controlProps }>\n\t\t\t<StyledDropdown\n\t\t\t\t// position=\"bottom left\"\n\t\t\t\tclassName=\"llms-fa-icon-picker--dropdown\"\n\t\t\t\tcontentClassName=\"llms-fa-icon-picker--content\"\n\t\t\t\tpopoverProps={ {\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tmarginTop: '-50px',\n\t\t\t\t\t\tmarginLeft: '-180px',\n\t\t\t\t\t},\n\t\t\t\t\t// placement: 'left-start',\n\t\t\t\t\t// offset: 200,\n\t\t\t\t} }\n\t\t\t\trenderToggle={ ( { isOpen, onToggle } ) => (\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={ onToggle }\n\t\t\t\t\t\taria-expanded={ isOpen }\n\t\t\t\t\t\tstyle={ isOpen ? { background: '#f0f0f0' } : {} }\n\t\t\t\t\t>\n\t\t\t\t\t\t<Icon { ...{ icon, iconStyle, iconPrefix, style: { fontSize: '18px', marginRight: '8px' } } } />\n\t\t\t\t\t\t<span>{ label }</span>\n\t\t\t\t\t</Button>\n\t\t\t\t) }\n\t\t\t\trenderContent={ () => (\n\t\t\t\t\t<div style={ { width: '380px', maxWidth: '100%' } }>\n\t\t\t\t\t\t<IconList { ...{ iconPrefix, onChange } } />\n\t\t\t\t\t</div>\n\t\t\t\t) }\n\t\t\t/>\n\t\t</BaseControl>\n\t);\n}\n"
  },
  {
    "path": "packages/fontawesome/src/components/icon.js",
    "content": "/**\n * Renders a Font Awesome icon.\n *\n * @since 0.0.1\n *\n * @param {Object}    props              Component properties.\n * @param {string}    props.icon         The Icon ID.\n * @param {string}    props.iconStyle    The icon style, enum: \"solid\", \"regular\", or \"brands\".\n * @param {string}    props.iconPrefix   The project's icon prefix.\n * @param {string}    props.label        The (optional) accessibility label to display for the icon.\n * @param {...Object} props.wrapperProps Any remaining properties which are passed to the icon wrapper component.\n * @return {WPElement} The component.\n */\nexport default function( { icon, iconStyle = 'solid', iconPrefix = 'llms-fa-', label = '', ...wrapperProps } ) {\n\treturn (\n\t\t<i\n\t\t\tclassName={ `${ iconPrefix }${ iconStyle } ${ iconPrefix }${ icon }` }\n\t\t\t{ ...wrapperProps }\n\t\t>\n\t\t\t{ label && (\n\t\t\t\t<span className=\"screen-reader-text\">{ label }</span>\n\t\t\t) }\n\t\t</i>\n\t);\n}\n"
  },
  {
    "path": "packages/fontawesome/src/fontawesome.scss",
    "content": "$llms-css-prefix: llms-fa !default;\n$llms-font-path: \"~@fortawesome/fontawesome-free/webfonts\" !default;\n\n$fa-css-prefix: $llms-css-prefix;\n$fa-font-path : $llms-font-path;\n\n@import \"@fortawesome/fontawesome-free/scss/fontawesome.scss\";\n@import \"@fortawesome/fontawesome-free/scss/regular.scss\";\n@import \"@fortawesome/fontawesome-free/scss/solid.scss\";\n@import \"@fortawesome/fontawesome-free/scss/brands.scss\";"
  },
  {
    "path": "packages/fontawesome/src/index.js",
    "content": "import icons from './metadata.json';\n\nexport { default as Icon } from './components/icon';\nexport { default as IconPicker } from './components/icon-picker';\n\n/**\n * An icon metadata object.\n *\n * @typedef {Object} IconMeta\n * @property {string[]} styles An array of icon styles available for the icon. Enum: \"solid\", \"regular\", or \"brands\".\n * @property {string}   label  The human-readable name / title of the icon.\n * @property {string[]} terms  A list of keywords or terms for the icon.\n */\n\n/**\n * Retrieves metadata for a given icon.\n *\n * @since 0.0.1\n *\n * @param {string} iconId The icon ID.\n * @return {IconMeta|boolean} An icon metadata object or `false` if the icon can't be found.\n */\nexport function getMetadata( iconId ) {\n\treturn icons[ iconId ] ?? false;\n}\n"
  },
  {
    "path": "packages/fontawesome/src/metadata.json",
    "content": "{\n  \"0\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"0\",\n    \"terms\": [\n      \"0\",\n      \"0\"\n    ]\n  },\n  \"1\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"1\",\n    \"terms\": [\n      \"1\",\n      \"1\"\n    ]\n  },\n  \"2\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"2\",\n    \"terms\": [\n      \"2\",\n      \"2\"\n    ]\n  },\n  \"3\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"3\",\n    \"terms\": [\n      \"3\",\n      \"3\"\n    ]\n  },\n  \"4\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"4\",\n    \"terms\": [\n      \"4\",\n      \"4\"\n    ]\n  },\n  \"5\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"5\",\n    \"terms\": [\n      \"5\",\n      \"5\"\n    ]\n  },\n  \"6\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"6\",\n    \"terms\": [\n      \"6\",\n      \"6\"\n    ]\n  },\n  \"7\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"7\",\n    \"terms\": [\n      \"7\",\n      \"7\"\n    ]\n  },\n  \"8\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"8\",\n    \"terms\": [\n      \"8\",\n      \"8\"\n    ]\n  },\n  \"9\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"9\",\n    \"terms\": [\n      \"9\",\n      \"9\"\n    ]\n  },\n  \"42-group\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"42.group\",\n    \"terms\": [\n      \"innosoft\",\n      \"42.group\",\n      \"42-group\"\n    ]\n  },\n  \"500px\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"500px\",\n    \"terms\": [\n      \"500px\",\n      \"500px\"\n    ]\n  },\n  \"a\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"A\",\n    \"terms\": [\n      \"A\",\n      \"a\"\n    ]\n  },\n  \"accessible-icon\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Accessible Icon\",\n    \"terms\": [\n      \"Accessible Icon\",\n      \"accessible-icon\"\n    ]\n  },\n  \"accusoft\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Accusoft\",\n    \"terms\": [\n      \"Accusoft\",\n      \"accusoft\"\n    ]\n  },\n  \"address-book\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Address Book\",\n    \"terms\": [\n      \"contact-book\",\n      \"Address Book\",\n      \"address-book\"\n    ]\n  },\n  \"address-card\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Address Card\",\n    \"terms\": [\n      \"contact-card\",\n      \"vcard\",\n      \"Address Card\",\n      \"address-card\"\n    ]\n  },\n  \"adn\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"App.net\",\n    \"terms\": [\n      \"App.net\",\n      \"adn\"\n    ]\n  },\n  \"adversal\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Adversal\",\n    \"terms\": [\n      \"Adversal\",\n      \"adversal\"\n    ]\n  },\n  \"affiliatetheme\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"affiliatetheme\",\n    \"terms\": [\n      \"affiliatetheme\",\n      \"affiliatetheme\"\n    ]\n  },\n  \"airbnb\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Airbnb\",\n    \"terms\": [\n      \"Airbnb\",\n      \"airbnb\"\n    ]\n  },\n  \"algolia\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Algolia\",\n    \"terms\": [\n      \"Algolia\",\n      \"algolia\"\n    ]\n  },\n  \"align-center\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"align-center\",\n    \"terms\": [\n      \"align-center\",\n      \"align-center\"\n    ]\n  },\n  \"align-justify\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"align-justify\",\n    \"terms\": [\n      \"align-justify\",\n      \"align-justify\"\n    ]\n  },\n  \"align-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"align-left\",\n    \"terms\": [\n      \"align-left\",\n      \"align-left\"\n    ]\n  },\n  \"align-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"align-right\",\n    \"terms\": [\n      \"align-right\",\n      \"align-right\"\n    ]\n  },\n  \"alipay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Alipay\",\n    \"terms\": [\n      \"Alipay\",\n      \"alipay\"\n    ]\n  },\n  \"amazon\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Amazon\",\n    \"terms\": [\n      \"Amazon\",\n      \"amazon\"\n    ]\n  },\n  \"amazon-pay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Amazon Pay\",\n    \"terms\": [\n      \"Amazon Pay\",\n      \"amazon-pay\"\n    ]\n  },\n  \"amilia\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Amilia\",\n    \"terms\": [\n      \"Amilia\",\n      \"amilia\"\n    ]\n  },\n  \"anchor\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Anchor\",\n    \"terms\": [\n      \"Anchor\",\n      \"anchor\"\n    ]\n  },\n  \"anchor-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Anchor Circle-check\",\n    \"terms\": [\n      \"Anchor Circle-check\",\n      \"anchor-circle-check\"\n    ]\n  },\n  \"anchor-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Anchor Circle-exclamation\",\n    \"terms\": [\n      \"Anchor Circle-exclamation\",\n      \"anchor-circle-exclamation\"\n    ]\n  },\n  \"anchor-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Anchor Circle-xmark\",\n    \"terms\": [\n      \"Anchor Circle-xmark\",\n      \"anchor-circle-xmark\"\n    ]\n  },\n  \"anchor-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Anchor Lock\",\n    \"terms\": [\n      \"Anchor Lock\",\n      \"anchor-lock\"\n    ]\n  },\n  \"android\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Android\",\n    \"terms\": [\n      \"Android\",\n      \"android\"\n    ]\n  },\n  \"angellist\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"AngelList\",\n    \"terms\": [\n      \"AngelList\",\n      \"angellist\"\n    ]\n  },\n  \"angle-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"angle-down\",\n    \"terms\": [\n      \"angle-down\",\n      \"angle-down\"\n    ]\n  },\n  \"angle-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"angle-left\",\n    \"terms\": [\n      \"angle-left\",\n      \"angle-left\"\n    ]\n  },\n  \"angle-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"angle-right\",\n    \"terms\": [\n      \"angle-right\",\n      \"angle-right\"\n    ]\n  },\n  \"angle-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"angle-up\",\n    \"terms\": [\n      \"angle-up\",\n      \"angle-up\"\n    ]\n  },\n  \"angles-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Angles down\",\n    \"terms\": [\n      \"angle-double-down\",\n      \"Angles down\",\n      \"angles-down\"\n    ]\n  },\n  \"angles-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Angles left\",\n    \"terms\": [\n      \"angle-double-left\",\n      \"Angles left\",\n      \"angles-left\"\n    ]\n  },\n  \"angles-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Angles right\",\n    \"terms\": [\n      \"angle-double-right\",\n      \"Angles right\",\n      \"angles-right\"\n    ]\n  },\n  \"angles-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Angles up\",\n    \"terms\": [\n      \"angle-double-up\",\n      \"Angles up\",\n      \"angles-up\"\n    ]\n  },\n  \"angrycreative\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Angry Creative\",\n    \"terms\": [\n      \"Angry Creative\",\n      \"angrycreative\"\n    ]\n  },\n  \"angular\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Angular\",\n    \"terms\": [\n      \"Angular\",\n      \"angular\"\n    ]\n  },\n  \"ankh\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ankh\",\n    \"terms\": [\n      \"Ankh\",\n      \"ankh\"\n    ]\n  },\n  \"app-store\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"App Store\",\n    \"terms\": [\n      \"App Store\",\n      \"app-store\"\n    ]\n  },\n  \"app-store-ios\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"iOS App Store\",\n    \"terms\": [\n      \"iOS App Store\",\n      \"app-store-ios\"\n    ]\n  },\n  \"apper\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Apper Systems AB\",\n    \"terms\": [\n      \"Apper Systems AB\",\n      \"apper\"\n    ]\n  },\n  \"apple\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Apple\",\n    \"terms\": [\n      \"Apple\",\n      \"apple\"\n    ]\n  },\n  \"apple-pay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Apple Pay\",\n    \"terms\": [\n      \"Apple Pay\",\n      \"apple-pay\"\n    ]\n  },\n  \"apple-whole\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Apple whole\",\n    \"terms\": [\n      \"apple-alt\",\n      \"Apple whole\",\n      \"apple-whole\"\n    ]\n  },\n  \"archway\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Archway\",\n    \"terms\": [\n      \"Archway\",\n      \"archway\"\n    ]\n  },\n  \"arrow-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down\",\n    \"terms\": [\n      \"Arrow down\",\n      \"arrow-down\"\n    ]\n  },\n  \"arrow-down-1-9\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down 1 9\",\n    \"terms\": [\n      \"sort-numeric-asc\",\n      \"sort-numeric-down\",\n      \"Arrow down 1 9\",\n      \"arrow-down-1-9\"\n    ]\n  },\n  \"arrow-down-9-1\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down 9 1\",\n    \"terms\": [\n      \"sort-numeric-desc\",\n      \"sort-numeric-down-alt\",\n      \"Arrow down 9 1\",\n      \"arrow-down-9-1\"\n    ]\n  },\n  \"arrow-down-a-z\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down a z\",\n    \"terms\": [\n      \"sort-alpha-asc\",\n      \"sort-alpha-down\",\n      \"Arrow down a z\",\n      \"arrow-down-a-z\"\n    ]\n  },\n  \"arrow-down-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down long\",\n    \"terms\": [\n      \"long-arrow-down\",\n      \"Arrow down long\",\n      \"arrow-down-long\"\n    ]\n  },\n  \"arrow-down-short-wide\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down short wide\",\n    \"terms\": [\n      \"sort-amount-desc\",\n      \"sort-amount-down-alt\",\n      \"Arrow down short wide\",\n      \"arrow-down-short-wide\"\n    ]\n  },\n  \"arrow-down-up-across-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Down-up-across-line\",\n    \"terms\": [\n      \"Arrow Down-up-across-line\",\n      \"arrow-down-up-across-line\"\n    ]\n  },\n  \"arrow-down-up-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Down-up-lock\",\n    \"terms\": [\n      \"Arrow Down-up-lock\",\n      \"arrow-down-up-lock\"\n    ]\n  },\n  \"arrow-down-wide-short\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down wide short\",\n    \"terms\": [\n      \"sort-amount-asc\",\n      \"sort-amount-down\",\n      \"Arrow down wide short\",\n      \"arrow-down-wide-short\"\n    ]\n  },\n  \"arrow-down-z-a\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow down z a\",\n    \"terms\": [\n      \"sort-alpha-desc\",\n      \"sort-alpha-down-alt\",\n      \"Arrow down z a\",\n      \"arrow-down-z-a\"\n    ]\n  },\n  \"arrow-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"arrow-left\",\n    \"terms\": [\n      \"arrow-left\",\n      \"arrow-left\"\n    ]\n  },\n  \"arrow-left-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow left long\",\n    \"terms\": [\n      \"long-arrow-left\",\n      \"Arrow left long\",\n      \"arrow-left-long\"\n    ]\n  },\n  \"arrow-pointer\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow pointer\",\n    \"terms\": [\n      \"mouse-pointer\",\n      \"Arrow pointer\",\n      \"arrow-pointer\"\n    ]\n  },\n  \"arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"arrow right\",\n    \"terms\": [\n      \"arrow right\",\n      \"arrow-right\"\n    ]\n  },\n  \"arrow-right-arrow-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow right arrow left\",\n    \"terms\": [\n      \"exchange\",\n      \"Arrow right arrow left\",\n      \"arrow-right-arrow-left\"\n    ]\n  },\n  \"arrow-right-from-bracket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow right from bracket\",\n    \"terms\": [\n      \"sign-out\",\n      \"Arrow right from bracket\",\n      \"arrow-right-from-bracket\"\n    ]\n  },\n  \"arrow-right-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow right long\",\n    \"terms\": [\n      \"long-arrow-right\",\n      \"Arrow right long\",\n      \"arrow-right-long\"\n    ]\n  },\n  \"arrow-right-to-bracket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow right to bracket\",\n    \"terms\": [\n      \"sign-in\",\n      \"Arrow right to bracket\",\n      \"arrow-right-to-bracket\"\n    ]\n  },\n  \"arrow-right-to-city\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Right-to-city\",\n    \"terms\": [\n      \"Arrow Right-to-city\",\n      \"arrow-right-to-city\"\n    ]\n  },\n  \"arrow-rotate-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Rotate Left\",\n    \"terms\": [\n      \"arrow-left-rotate\",\n      \"arrow-rotate-back\",\n      \"arrow-rotate-backward\",\n      \"undo\",\n      \"Arrow Rotate Left\",\n      \"arrow-rotate-left\"\n    ]\n  },\n  \"arrow-rotate-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Rotate Right\",\n    \"terms\": [\n      \"arrow-right-rotate\",\n      \"arrow-rotate-forward\",\n      \"redo\",\n      \"Arrow Rotate Right\",\n      \"arrow-rotate-right\"\n    ]\n  },\n  \"arrow-trend-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow trend down\",\n    \"terms\": [\n      \"Arrow trend down\",\n      \"arrow-trend-down\"\n    ]\n  },\n  \"arrow-trend-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow trend up\",\n    \"terms\": [\n      \"Arrow trend up\",\n      \"arrow-trend-up\"\n    ]\n  },\n  \"arrow-turn-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow turn down\",\n    \"terms\": [\n      \"level-down\",\n      \"Arrow turn down\",\n      \"arrow-turn-down\"\n    ]\n  },\n  \"arrow-turn-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow turn up\",\n    \"terms\": [\n      \"level-up\",\n      \"Arrow turn up\",\n      \"arrow-turn-up\"\n    ]\n  },\n  \"arrow-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up\",\n    \"terms\": [\n      \"Arrow up\",\n      \"arrow-up\"\n    ]\n  },\n  \"arrow-up-1-9\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up 1 9\",\n    \"terms\": [\n      \"sort-numeric-up\",\n      \"Arrow up 1 9\",\n      \"arrow-up-1-9\"\n    ]\n  },\n  \"arrow-up-9-1\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up 9 1\",\n    \"terms\": [\n      \"sort-numeric-up-alt\",\n      \"Arrow up 9 1\",\n      \"arrow-up-9-1\"\n    ]\n  },\n  \"arrow-up-a-z\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up a z\",\n    \"terms\": [\n      \"sort-alpha-up\",\n      \"Arrow up a z\",\n      \"arrow-up-a-z\"\n    ]\n  },\n  \"arrow-up-from-bracket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up from bracket\",\n    \"terms\": [\n      \"Arrow up from bracket\",\n      \"arrow-up-from-bracket\"\n    ]\n  },\n  \"arrow-up-from-ground-water\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Up-from-ground-water\",\n    \"terms\": [\n      \"Arrow Up-from-ground-water\",\n      \"arrow-up-from-ground-water\"\n    ]\n  },\n  \"arrow-up-from-water-pump\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Up-from-water-pump\",\n    \"terms\": [\n      \"Arrow Up-from-water-pump\",\n      \"arrow-up-from-water-pump\"\n    ]\n  },\n  \"arrow-up-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up long\",\n    \"terms\": [\n      \"long-arrow-up\",\n      \"Arrow up long\",\n      \"arrow-up-long\"\n    ]\n  },\n  \"arrow-up-right-dots\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow Up-right-dots\",\n    \"terms\": [\n      \"Arrow Up-right-dots\",\n      \"arrow-up-right-dots\"\n    ]\n  },\n  \"arrow-up-right-from-square\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up right from square\",\n    \"terms\": [\n      \"external-link\",\n      \"Arrow up right from square\",\n      \"arrow-up-right-from-square\"\n    ]\n  },\n  \"arrow-up-short-wide\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up short wide\",\n    \"terms\": [\n      \"sort-amount-up-alt\",\n      \"Arrow up short wide\",\n      \"arrow-up-short-wide\"\n    ]\n  },\n  \"arrow-up-wide-short\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up wide short\",\n    \"terms\": [\n      \"sort-amount-up\",\n      \"Arrow up wide short\",\n      \"arrow-up-wide-short\"\n    ]\n  },\n  \"arrow-up-z-a\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrow up z a\",\n    \"terms\": [\n      \"sort-alpha-up-alt\",\n      \"Arrow up z a\",\n      \"arrow-up-z-a\"\n    ]\n  },\n  \"arrows-down-to-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Down-to-line\",\n    \"terms\": [\n      \"Arrows Down-to-line\",\n      \"arrows-down-to-line\"\n    ]\n  },\n  \"arrows-down-to-people\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Down-to-people\",\n    \"terms\": [\n      \"Arrows Down-to-people\",\n      \"arrows-down-to-people\"\n    ]\n  },\n  \"arrows-left-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows left right\",\n    \"terms\": [\n      \"arrows-h\",\n      \"Arrows left right\",\n      \"arrows-left-right\"\n    ]\n  },\n  \"arrows-left-right-to-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Left-right-to-line\",\n    \"terms\": [\n      \"Arrows Left-right-to-line\",\n      \"arrows-left-right-to-line\"\n    ]\n  },\n  \"arrows-rotate\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows rotate\",\n    \"terms\": [\n      \"refresh\",\n      \"sync\",\n      \"Arrows rotate\",\n      \"arrows-rotate\"\n    ]\n  },\n  \"arrows-spin\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Spin\",\n    \"terms\": [\n      \"Arrows Spin\",\n      \"arrows-spin\"\n    ]\n  },\n  \"arrows-split-up-and-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Split-up-and-left\",\n    \"terms\": [\n      \"Arrows Split-up-and-left\",\n      \"arrows-split-up-and-left\"\n    ]\n  },\n  \"arrows-to-circle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows To-circle\",\n    \"terms\": [\n      \"Arrows To-circle\",\n      \"arrows-to-circle\"\n    ]\n  },\n  \"arrows-to-dot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows To-dot\",\n    \"terms\": [\n      \"Arrows To-dot\",\n      \"arrows-to-dot\"\n    ]\n  },\n  \"arrows-to-eye\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows To-eye\",\n    \"terms\": [\n      \"Arrows To-eye\",\n      \"arrows-to-eye\"\n    ]\n  },\n  \"arrows-turn-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Turn-right\",\n    \"terms\": [\n      \"Arrows Turn-right\",\n      \"arrows-turn-right\"\n    ]\n  },\n  \"arrows-turn-to-dots\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Turn-to-dots\",\n    \"terms\": [\n      \"Arrows Turn-to-dots\",\n      \"arrows-turn-to-dots\"\n    ]\n  },\n  \"arrows-up-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows up down\",\n    \"terms\": [\n      \"arrows-v\",\n      \"Arrows up down\",\n      \"arrows-up-down\"\n    ]\n  },\n  \"arrows-up-down-left-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows up down left right\",\n    \"terms\": [\n      \"arrows\",\n      \"Arrows up down left right\",\n      \"arrows-up-down-left-right\"\n    ]\n  },\n  \"arrows-up-to-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Arrows Up-to-line\",\n    \"terms\": [\n      \"Arrows Up-to-line\",\n      \"arrows-up-to-line\"\n    ]\n  },\n  \"artstation\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Artstation\",\n    \"terms\": [\n      \"Artstation\",\n      \"artstation\"\n    ]\n  },\n  \"asterisk\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"asterisk\",\n    \"terms\": [\n      \"asterisk\",\n      \"asterisk\"\n    ]\n  },\n  \"asymmetrik\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Asymmetrik, Ltd.\",\n    \"terms\": [\n      \"Asymmetrik, Ltd.\",\n      \"asymmetrik\"\n    ]\n  },\n  \"at\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"At\",\n    \"terms\": [\n      \"At\",\n      \"at\"\n    ]\n  },\n  \"atlassian\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Atlassian\",\n    \"terms\": [\n      \"Atlassian\",\n      \"atlassian\"\n    ]\n  },\n  \"atom\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Atom\",\n    \"terms\": [\n      \"Atom\",\n      \"atom\"\n    ]\n  },\n  \"audible\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Audible\",\n    \"terms\": [\n      \"Audible\",\n      \"audible\"\n    ]\n  },\n  \"audio-description\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rectangle audio description\",\n    \"terms\": [\n      \"Rectangle audio description\",\n      \"audio-description\"\n    ]\n  },\n  \"austral-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Austral Sign\",\n    \"terms\": [\n      \"Austral Sign\",\n      \"austral-sign\"\n    ]\n  },\n  \"autoprefixer\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Autoprefixer\",\n    \"terms\": [\n      \"Autoprefixer\",\n      \"autoprefixer\"\n    ]\n  },\n  \"avianex\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"avianex\",\n    \"terms\": [\n      \"avianex\",\n      \"avianex\"\n    ]\n  },\n  \"aviato\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Aviato\",\n    \"terms\": [\n      \"Aviato\",\n      \"aviato\"\n    ]\n  },\n  \"award\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Award\",\n    \"terms\": [\n      \"Award\",\n      \"award\"\n    ]\n  },\n  \"aws\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Amazon Web Services (AWS)\",\n    \"terms\": [\n      \"Amazon Web Services (AWS)\",\n      \"aws\"\n    ]\n  },\n  \"b\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"B\",\n    \"terms\": [\n      \"B\",\n      \"b\"\n    ]\n  },\n  \"baby\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Baby\",\n    \"terms\": [\n      \"Baby\",\n      \"baby\"\n    ]\n  },\n  \"baby-carriage\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Baby Carriage\",\n    \"terms\": [\n      \"carriage-baby\",\n      \"Baby Carriage\",\n      \"baby-carriage\"\n    ]\n  },\n  \"backward\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"backward\",\n    \"terms\": [\n      \"backward\",\n      \"backward\"\n    ]\n  },\n  \"backward-fast\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Backward fast\",\n    \"terms\": [\n      \"fast-backward\",\n      \"Backward fast\",\n      \"backward-fast\"\n    ]\n  },\n  \"backward-step\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Backward step\",\n    \"terms\": [\n      \"step-backward\",\n      \"Backward step\",\n      \"backward-step\"\n    ]\n  },\n  \"bacon\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bacon\",\n    \"terms\": [\n      \"Bacon\",\n      \"bacon\"\n    ]\n  },\n  \"bacteria\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bacteria\",\n    \"terms\": [\n      \"Bacteria\",\n      \"bacteria\"\n    ]\n  },\n  \"bacterium\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bacterium\",\n    \"terms\": [\n      \"Bacterium\",\n      \"bacterium\"\n    ]\n  },\n  \"bag-shopping\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bag shopping\",\n    \"terms\": [\n      \"shopping-bag\",\n      \"Bag shopping\",\n      \"bag-shopping\"\n    ]\n  },\n  \"bahai\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bahá'í\",\n    \"terms\": [\n      \"haykal\",\n      \"Bahá'í\",\n      \"bahai\"\n    ]\n  },\n  \"baht-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Baht Sign\",\n    \"terms\": [\n      \"Baht Sign\",\n      \"baht-sign\"\n    ]\n  },\n  \"ban\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"ban\",\n    \"terms\": [\n      \"cancel\",\n      \"ban\",\n      \"ban\"\n    ]\n  },\n  \"ban-smoking\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ban smoking\",\n    \"terms\": [\n      \"smoking-ban\",\n      \"Ban smoking\",\n      \"ban-smoking\"\n    ]\n  },\n  \"bandage\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bandage\",\n    \"terms\": [\n      \"band-aid\",\n      \"Bandage\",\n      \"bandage\"\n    ]\n  },\n  \"bandcamp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bandcamp\",\n    \"terms\": [\n      \"Bandcamp\",\n      \"bandcamp\"\n    ]\n  },\n  \"barcode\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"barcode\",\n    \"terms\": [\n      \"barcode\",\n      \"barcode\"\n    ]\n  },\n  \"bars\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bars\",\n    \"terms\": [\n      \"navicon\",\n      \"Bars\",\n      \"bars\"\n    ]\n  },\n  \"bars-progress\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bars progress\",\n    \"terms\": [\n      \"tasks-alt\",\n      \"Bars progress\",\n      \"bars-progress\"\n    ]\n  },\n  \"bars-staggered\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bars staggered\",\n    \"terms\": [\n      \"reorder\",\n      \"stream\",\n      \"Bars staggered\",\n      \"bars-staggered\"\n    ]\n  },\n  \"baseball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Baseball Ball\",\n    \"terms\": [\n      \"baseball-ball\",\n      \"Baseball Ball\",\n      \"baseball\"\n    ]\n  },\n  \"baseball-bat-ball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Baseball bat ball\",\n    \"terms\": [\n      \"Baseball bat ball\",\n      \"baseball-bat-ball\"\n    ]\n  },\n  \"basket-shopping\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Basket shopping\",\n    \"terms\": [\n      \"shopping-basket\",\n      \"Basket shopping\",\n      \"basket-shopping\"\n    ]\n  },\n  \"basketball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Basketball Ball\",\n    \"terms\": [\n      \"basketball-ball\",\n      \"Basketball Ball\",\n      \"basketball\"\n    ]\n  },\n  \"bath\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bath\",\n    \"terms\": [\n      \"bathtub\",\n      \"Bath\",\n      \"bath\"\n    ]\n  },\n  \"battery-empty\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Battery Empty\",\n    \"terms\": [\n      \"battery-0\",\n      \"Battery Empty\",\n      \"battery-empty\"\n    ]\n  },\n  \"battery-full\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Battery Full\",\n    \"terms\": [\n      \"battery\",\n      \"battery-5\",\n      \"Battery Full\",\n      \"battery-full\"\n    ]\n  },\n  \"battery-half\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Battery 1/2 Full\",\n    \"terms\": [\n      \"battery-3\",\n      \"Battery 1/2 Full\",\n      \"battery-half\"\n    ]\n  },\n  \"battery-quarter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Battery 1/4 Full\",\n    \"terms\": [\n      \"battery-2\",\n      \"Battery 1/4 Full\",\n      \"battery-quarter\"\n    ]\n  },\n  \"battery-three-quarters\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Battery 3/4 Full\",\n    \"terms\": [\n      \"battery-4\",\n      \"Battery 3/4 Full\",\n      \"battery-three-quarters\"\n    ]\n  },\n  \"battle-net\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Battle.net\",\n    \"terms\": [\n      \"Battle.net\",\n      \"battle-net\"\n    ]\n  },\n  \"bed\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bed\",\n    \"terms\": [\n      \"Bed\",\n      \"bed\"\n    ]\n  },\n  \"bed-pulse\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bed pulse\",\n    \"terms\": [\n      \"procedures\",\n      \"Bed pulse\",\n      \"bed-pulse\"\n    ]\n  },\n  \"beer-mug-empty\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Beer mug empty\",\n    \"terms\": [\n      \"beer\",\n      \"Beer mug empty\",\n      \"beer-mug-empty\"\n    ]\n  },\n  \"behance\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Behance\",\n    \"terms\": [\n      \"Behance\",\n      \"behance\"\n    ]\n  },\n  \"bell\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"bell\",\n    \"terms\": [\n      \"bell\",\n      \"bell\"\n    ]\n  },\n  \"bell-concierge\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bell concierge\",\n    \"terms\": [\n      \"concierge-bell\",\n      \"Bell concierge\",\n      \"bell-concierge\"\n    ]\n  },\n  \"bell-slash\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Bell Slash\",\n    \"terms\": [\n      \"Bell Slash\",\n      \"bell-slash\"\n    ]\n  },\n  \"bezier-curve\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bezier Curve\",\n    \"terms\": [\n      \"Bezier Curve\",\n      \"bezier-curve\"\n    ]\n  },\n  \"bicycle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bicycle\",\n    \"terms\": [\n      \"Bicycle\",\n      \"bicycle\"\n    ]\n  },\n  \"bilibili\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bilibili\",\n    \"terms\": [\n      \"Bilibili\",\n      \"bilibili\"\n    ]\n  },\n  \"bimobject\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"BIMobject\",\n    \"terms\": [\n      \"BIMobject\",\n      \"bimobject\"\n    ]\n  },\n  \"binoculars\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Binoculars\",\n    \"terms\": [\n      \"Binoculars\",\n      \"binoculars\"\n    ]\n  },\n  \"biohazard\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Biohazard\",\n    \"terms\": [\n      \"Biohazard\",\n      \"biohazard\"\n    ]\n  },\n  \"bitbucket\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bitbucket\",\n    \"terms\": [\n      \"Bitbucket\",\n      \"bitbucket\"\n    ]\n  },\n  \"bitcoin\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bitcoin\",\n    \"terms\": [\n      \"Bitcoin\",\n      \"bitcoin\"\n    ]\n  },\n  \"bitcoin-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bitcoin Sign\",\n    \"terms\": [\n      \"Bitcoin Sign\",\n      \"bitcoin-sign\"\n    ]\n  },\n  \"bity\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bity\",\n    \"terms\": [\n      \"Bity\",\n      \"bity\"\n    ]\n  },\n  \"black-tie\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Font Awesome Black Tie\",\n    \"terms\": [\n      \"Font Awesome Black Tie\",\n      \"black-tie\"\n    ]\n  },\n  \"blackberry\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"BlackBerry\",\n    \"terms\": [\n      \"BlackBerry\",\n      \"blackberry\"\n    ]\n  },\n  \"blender\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Blender\",\n    \"terms\": [\n      \"Blender\",\n      \"blender\"\n    ]\n  },\n  \"blender-phone\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Blender Phone\",\n    \"terms\": [\n      \"Blender Phone\",\n      \"blender-phone\"\n    ]\n  },\n  \"blog\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Blog\",\n    \"terms\": [\n      \"Blog\",\n      \"blog\"\n    ]\n  },\n  \"blogger\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Blogger\",\n    \"terms\": [\n      \"Blogger\",\n      \"blogger\"\n    ]\n  },\n  \"blogger-b\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Blogger B\",\n    \"terms\": [\n      \"Blogger B\",\n      \"blogger-b\"\n    ]\n  },\n  \"bluetooth\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bluetooth\",\n    \"terms\": [\n      \"Bluetooth\",\n      \"bluetooth\"\n    ]\n  },\n  \"bluetooth-b\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bluetooth\",\n    \"terms\": [\n      \"Bluetooth\",\n      \"bluetooth-b\"\n    ]\n  },\n  \"bold\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"bold\",\n    \"terms\": [\n      \"bold\",\n      \"bold\"\n    ]\n  },\n  \"bolt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bolt\",\n    \"terms\": [\n      \"zap\",\n      \"Bolt\",\n      \"bolt\"\n    ]\n  },\n  \"bolt-lightning\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Lightning Bolt\",\n    \"terms\": [\n      \"Lightning Bolt\",\n      \"bolt-lightning\"\n    ]\n  },\n  \"bomb\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bomb\",\n    \"terms\": [\n      \"Bomb\",\n      \"bomb\"\n    ]\n  },\n  \"bone\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bone\",\n    \"terms\": [\n      \"Bone\",\n      \"bone\"\n    ]\n  },\n  \"bong\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bong\",\n    \"terms\": [\n      \"Bong\",\n      \"bong\"\n    ]\n  },\n  \"book\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"book\",\n    \"terms\": [\n      \"book\",\n      \"book\"\n    ]\n  },\n  \"book-atlas\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book atlas\",\n    \"terms\": [\n      \"atlas\",\n      \"Book atlas\",\n      \"book-atlas\"\n    ]\n  },\n  \"book-bible\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book bible\",\n    \"terms\": [\n      \"bible\",\n      \"Book bible\",\n      \"book-bible\"\n    ]\n  },\n  \"book-bookmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book Bookmark\",\n    \"terms\": [\n      \"Book Bookmark\",\n      \"book-bookmark\"\n    ]\n  },\n  \"book-journal-whills\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book journal whills\",\n    \"terms\": [\n      \"journal-whills\",\n      \"Book journal whills\",\n      \"book-journal-whills\"\n    ]\n  },\n  \"book-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Medical Book\",\n    \"terms\": [\n      \"Medical Book\",\n      \"book-medical\"\n    ]\n  },\n  \"book-open\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book Open\",\n    \"terms\": [\n      \"Book Open\",\n      \"book-open\"\n    ]\n  },\n  \"book-open-reader\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book open reader\",\n    \"terms\": [\n      \"book-reader\",\n      \"Book open reader\",\n      \"book-open-reader\"\n    ]\n  },\n  \"book-quran\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book quran\",\n    \"terms\": [\n      \"quran\",\n      \"Book quran\",\n      \"book-quran\"\n    ]\n  },\n  \"book-skull\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book skull\",\n    \"terms\": [\n      \"book-dead\",\n      \"Book skull\",\n      \"book-skull\"\n    ]\n  },\n  \"book-tanakh\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Book tanakh\",\n    \"terms\": [\n      \"tanakh\",\n      \"Book tanakh\",\n      \"book-tanakh\"\n    ]\n  },\n  \"bookmark\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"bookmark\",\n    \"terms\": [\n      \"bookmark\",\n      \"bookmark\"\n    ]\n  },\n  \"bootstrap\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bootstrap\",\n    \"terms\": [\n      \"Bootstrap\",\n      \"bootstrap\"\n    ]\n  },\n  \"border-all\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Border All\",\n    \"terms\": [\n      \"Border All\",\n      \"border-all\"\n    ]\n  },\n  \"border-none\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Border None\",\n    \"terms\": [\n      \"Border None\",\n      \"border-none\"\n    ]\n  },\n  \"border-top-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Border top left\",\n    \"terms\": [\n      \"border-style\",\n      \"Border top left\",\n      \"border-top-left\"\n    ]\n  },\n  \"bore-hole\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bore Hole\",\n    \"terms\": [\n      \"Bore Hole\",\n      \"bore-hole\"\n    ]\n  },\n  \"bots\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Bots\",\n    \"terms\": [\n      \"Bots\",\n      \"bots\"\n    ]\n  },\n  \"bottle-droplet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bottle Droplet\",\n    \"terms\": [\n      \"Bottle Droplet\",\n      \"bottle-droplet\"\n    ]\n  },\n  \"bottle-water\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bottle Water\",\n    \"terms\": [\n      \"Bottle Water\",\n      \"bottle-water\"\n    ]\n  },\n  \"bowl-food\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bowl Food\",\n    \"terms\": [\n      \"Bowl Food\",\n      \"bowl-food\"\n    ]\n  },\n  \"bowl-rice\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bowl Rice\",\n    \"terms\": [\n      \"Bowl Rice\",\n      \"bowl-rice\"\n    ]\n  },\n  \"bowling-ball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bowling Ball\",\n    \"terms\": [\n      \"Bowling Ball\",\n      \"bowling-ball\"\n    ]\n  },\n  \"box\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Box\",\n    \"terms\": [\n      \"Box\",\n      \"box\"\n    ]\n  },\n  \"box-archive\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Box archive\",\n    \"terms\": [\n      \"archive\",\n      \"Box archive\",\n      \"box-archive\"\n    ]\n  },\n  \"box-open\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Box Open\",\n    \"terms\": [\n      \"Box Open\",\n      \"box-open\"\n    ]\n  },\n  \"box-tissue\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tissue Box\",\n    \"terms\": [\n      \"Tissue Box\",\n      \"box-tissue\"\n    ]\n  },\n  \"boxes-packing\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Boxes Packing\",\n    \"terms\": [\n      \"Boxes Packing\",\n      \"boxes-packing\"\n    ]\n  },\n  \"boxes-stacked\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Boxes stacked\",\n    \"terms\": [\n      \"boxes\",\n      \"boxes-alt\",\n      \"Boxes stacked\",\n      \"boxes-stacked\"\n    ]\n  },\n  \"braille\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Braille\",\n    \"terms\": [\n      \"Braille\",\n      \"braille\"\n    ]\n  },\n  \"brain\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Brain\",\n    \"terms\": [\n      \"Brain\",\n      \"brain\"\n    ]\n  },\n  \"brazilian-real-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Brazilian Real Sign\",\n    \"terms\": [\n      \"Brazilian Real Sign\",\n      \"brazilian-real-sign\"\n    ]\n  },\n  \"bread-slice\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bread Slice\",\n    \"terms\": [\n      \"Bread Slice\",\n      \"bread-slice\"\n    ]\n  },\n  \"bridge\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bridge\",\n    \"terms\": [\n      \"Bridge\",\n      \"bridge\"\n    ]\n  },\n  \"bridge-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bridge Circle-check\",\n    \"terms\": [\n      \"Bridge Circle-check\",\n      \"bridge-circle-check\"\n    ]\n  },\n  \"bridge-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bridge Circle-exclamation\",\n    \"terms\": [\n      \"Bridge Circle-exclamation\",\n      \"bridge-circle-exclamation\"\n    ]\n  },\n  \"bridge-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bridge Circle-xmark\",\n    \"terms\": [\n      \"Bridge Circle-xmark\",\n      \"bridge-circle-xmark\"\n    ]\n  },\n  \"bridge-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bridge Lock\",\n    \"terms\": [\n      \"Bridge Lock\",\n      \"bridge-lock\"\n    ]\n  },\n  \"bridge-water\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bridge Water\",\n    \"terms\": [\n      \"Bridge Water\",\n      \"bridge-water\"\n    ]\n  },\n  \"briefcase\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Briefcase\",\n    \"terms\": [\n      \"Briefcase\",\n      \"briefcase\"\n    ]\n  },\n  \"briefcase-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Medical Briefcase\",\n    \"terms\": [\n      \"Medical Briefcase\",\n      \"briefcase-medical\"\n    ]\n  },\n  \"broom\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Broom\",\n    \"terms\": [\n      \"Broom\",\n      \"broom\"\n    ]\n  },\n  \"broom-ball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Broom and Ball\",\n    \"terms\": [\n      \"quidditch\",\n      \"quidditch-broom-ball\",\n      \"Broom and Ball\",\n      \"broom-ball\"\n    ]\n  },\n  \"brush\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Brush\",\n    \"terms\": [\n      \"Brush\",\n      \"brush\"\n    ]\n  },\n  \"btc\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"BTC\",\n    \"terms\": [\n      \"BTC\",\n      \"btc\"\n    ]\n  },\n  \"bucket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bucket\",\n    \"terms\": [\n      \"Bucket\",\n      \"bucket\"\n    ]\n  },\n  \"buffer\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Buffer\",\n    \"terms\": [\n      \"Buffer\",\n      \"buffer\"\n    ]\n  },\n  \"bug\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bug\",\n    \"terms\": [\n      \"Bug\",\n      \"bug\"\n    ]\n  },\n  \"bug-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bug Slash\",\n    \"terms\": [\n      \"Bug Slash\",\n      \"bug-slash\"\n    ]\n  },\n  \"bugs\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bugs\",\n    \"terms\": [\n      \"Bugs\",\n      \"bugs\"\n    ]\n  },\n  \"building\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Building\",\n    \"terms\": [\n      \"Building\",\n      \"building\"\n    ]\n  },\n  \"building-circle-arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Circle-arrow-right\",\n    \"terms\": [\n      \"Building Circle-arrow-right\",\n      \"building-circle-arrow-right\"\n    ]\n  },\n  \"building-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Circle-check\",\n    \"terms\": [\n      \"Building Circle-check\",\n      \"building-circle-check\"\n    ]\n  },\n  \"building-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Circle-exclamation\",\n    \"terms\": [\n      \"Building Circle-exclamation\",\n      \"building-circle-exclamation\"\n    ]\n  },\n  \"building-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Circle-xmark\",\n    \"terms\": [\n      \"Building Circle-xmark\",\n      \"building-circle-xmark\"\n    ]\n  },\n  \"building-columns\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building with Columns\",\n    \"terms\": [\n      \"bank\",\n      \"institution\",\n      \"museum\",\n      \"university\",\n      \"Building with Columns\",\n      \"building-columns\"\n    ]\n  },\n  \"building-flag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Flag\",\n    \"terms\": [\n      \"Building Flag\",\n      \"building-flag\"\n    ]\n  },\n  \"building-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Lock\",\n    \"terms\": [\n      \"Building Lock\",\n      \"building-lock\"\n    ]\n  },\n  \"building-ngo\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Ngo\",\n    \"terms\": [\n      \"Building Ngo\",\n      \"building-ngo\"\n    ]\n  },\n  \"building-shield\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Shield\",\n    \"terms\": [\n      \"Building Shield\",\n      \"building-shield\"\n    ]\n  },\n  \"building-un\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Un\",\n    \"terms\": [\n      \"Building Un\",\n      \"building-un\"\n    ]\n  },\n  \"building-user\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building User\",\n    \"terms\": [\n      \"Building User\",\n      \"building-user\"\n    ]\n  },\n  \"building-wheat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Building Wheat\",\n    \"terms\": [\n      \"Building Wheat\",\n      \"building-wheat\"\n    ]\n  },\n  \"bullhorn\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"bullhorn\",\n    \"terms\": [\n      \"bullhorn\",\n      \"bullhorn\"\n    ]\n  },\n  \"bullseye\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bullseye\",\n    \"terms\": [\n      \"Bullseye\",\n      \"bullseye\"\n    ]\n  },\n  \"burger\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Burger\",\n    \"terms\": [\n      \"hamburger\",\n      \"Burger\",\n      \"burger\"\n    ]\n  },\n  \"buromobelexperte\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Büromöbel-Experte GmbH & Co. KG.\",\n    \"terms\": [\n      \"Büromöbel-Experte GmbH & Co. KG.\",\n      \"buromobelexperte\"\n    ]\n  },\n  \"burst\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Burst\",\n    \"terms\": [\n      \"Burst\",\n      \"burst\"\n    ]\n  },\n  \"bus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bus\",\n    \"terms\": [\n      \"Bus\",\n      \"bus\"\n    ]\n  },\n  \"bus-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Bus simple\",\n    \"terms\": [\n      \"bus-alt\",\n      \"Bus simple\",\n      \"bus-simple\"\n    ]\n  },\n  \"business-time\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Briefcase clock\",\n    \"terms\": [\n      \"briefcase-clock\",\n      \"Briefcase clock\",\n      \"business-time\"\n    ]\n  },\n  \"buy-n-large\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Buy n Large\",\n    \"terms\": [\n      \"Buy n Large\",\n      \"buy-n-large\"\n    ]\n  },\n  \"buysellads\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"BuySellAds\",\n    \"terms\": [\n      \"BuySellAds\",\n      \"buysellads\"\n    ]\n  },\n  \"c\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"C\",\n    \"terms\": [\n      \"C\",\n      \"c\"\n    ]\n  },\n  \"cable-car\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cable Car\",\n    \"terms\": [\n      \"tram\",\n      \"Cable Car\",\n      \"cable-car\"\n    ]\n  },\n  \"cake-candles\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cake candles\",\n    \"terms\": [\n      \"birthday-cake\",\n      \"cake\",\n      \"Cake candles\",\n      \"cake-candles\"\n    ]\n  },\n  \"calculator\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Calculator\",\n    \"terms\": [\n      \"Calculator\",\n      \"calculator\"\n    ]\n  },\n  \"calendar\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Calendar\",\n    \"terms\": [\n      \"Calendar\",\n      \"calendar\"\n    ]\n  },\n  \"calendar-check\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Calendar Check\",\n    \"terms\": [\n      \"Calendar Check\",\n      \"calendar-check\"\n    ]\n  },\n  \"calendar-day\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Calendar with Day Focus\",\n    \"terms\": [\n      \"Calendar with Day Focus\",\n      \"calendar-day\"\n    ]\n  },\n  \"calendar-days\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Calendar Days\",\n    \"terms\": [\n      \"calendar-alt\",\n      \"Calendar Days\",\n      \"calendar-days\"\n    ]\n  },\n  \"calendar-minus\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Calendar Minus\",\n    \"terms\": [\n      \"Calendar Minus\",\n      \"calendar-minus\"\n    ]\n  },\n  \"calendar-plus\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Calendar Plus\",\n    \"terms\": [\n      \"Calendar Plus\",\n      \"calendar-plus\"\n    ]\n  },\n  \"calendar-week\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Calendar with Week Focus\",\n    \"terms\": [\n      \"Calendar with Week Focus\",\n      \"calendar-week\"\n    ]\n  },\n  \"calendar-xmark\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Calendar X Mark\",\n    \"terms\": [\n      \"calendar-times\",\n      \"Calendar X Mark\",\n      \"calendar-xmark\"\n    ]\n  },\n  \"camera\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"camera\",\n    \"terms\": [\n      \"camera-alt\",\n      \"camera\",\n      \"camera\"\n    ]\n  },\n  \"camera-retro\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Retro Camera\",\n    \"terms\": [\n      \"Retro Camera\",\n      \"camera-retro\"\n    ]\n  },\n  \"camera-rotate\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Camera Rotate\",\n    \"terms\": [\n      \"Camera Rotate\",\n      \"camera-rotate\"\n    ]\n  },\n  \"campground\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Campground\",\n    \"terms\": [\n      \"Campground\",\n      \"campground\"\n    ]\n  },\n  \"canadian-maple-leaf\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Canadian Maple Leaf\",\n    \"terms\": [\n      \"Canadian Maple Leaf\",\n      \"canadian-maple-leaf\"\n    ]\n  },\n  \"candy-cane\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Candy Cane\",\n    \"terms\": [\n      \"Candy Cane\",\n      \"candy-cane\"\n    ]\n  },\n  \"cannabis\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cannabis\",\n    \"terms\": [\n      \"Cannabis\",\n      \"cannabis\"\n    ]\n  },\n  \"capsules\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Capsules\",\n    \"terms\": [\n      \"Capsules\",\n      \"capsules\"\n    ]\n  },\n  \"car\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Car\",\n    \"terms\": [\n      \"automobile\",\n      \"Car\",\n      \"car\"\n    ]\n  },\n  \"car-battery\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Car Battery\",\n    \"terms\": [\n      \"battery-car\",\n      \"Car Battery\",\n      \"car-battery\"\n    ]\n  },\n  \"car-burst\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Car Crash\",\n    \"terms\": [\n      \"car-crash\",\n      \"Car Crash\",\n      \"car-burst\"\n    ]\n  },\n  \"car-on\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Car On\",\n    \"terms\": [\n      \"Car On\",\n      \"car-on\"\n    ]\n  },\n  \"car-rear\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Car rear\",\n    \"terms\": [\n      \"car-alt\",\n      \"Car rear\",\n      \"car-rear\"\n    ]\n  },\n  \"car-side\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Car Side\",\n    \"terms\": [\n      \"Car Side\",\n      \"car-side\"\n    ]\n  },\n  \"car-tunnel\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Car Tunnel\",\n    \"terms\": [\n      \"Car Tunnel\",\n      \"car-tunnel\"\n    ]\n  },\n  \"caravan\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Caravan\",\n    \"terms\": [\n      \"Caravan\",\n      \"caravan\"\n    ]\n  },\n  \"caret-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Caret Down\",\n    \"terms\": [\n      \"Caret Down\",\n      \"caret-down\"\n    ]\n  },\n  \"caret-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Caret Left\",\n    \"terms\": [\n      \"Caret Left\",\n      \"caret-left\"\n    ]\n  },\n  \"caret-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Caret Right\",\n    \"terms\": [\n      \"Caret Right\",\n      \"caret-right\"\n    ]\n  },\n  \"caret-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Caret Up\",\n    \"terms\": [\n      \"Caret Up\",\n      \"caret-up\"\n    ]\n  },\n  \"carrot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Carrot\",\n    \"terms\": [\n      \"Carrot\",\n      \"carrot\"\n    ]\n  },\n  \"cart-arrow-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shopping Cart Arrow Down\",\n    \"terms\": [\n      \"Shopping Cart Arrow Down\",\n      \"cart-arrow-down\"\n    ]\n  },\n  \"cart-flatbed\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cart flatbed\",\n    \"terms\": [\n      \"dolly-flatbed\",\n      \"Cart flatbed\",\n      \"cart-flatbed\"\n    ]\n  },\n  \"cart-flatbed-suitcase\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cart flatbed suitcase\",\n    \"terms\": [\n      \"luggage-cart\",\n      \"Cart flatbed suitcase\",\n      \"cart-flatbed-suitcase\"\n    ]\n  },\n  \"cart-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Add to Shopping Cart\",\n    \"terms\": [\n      \"Add to Shopping Cart\",\n      \"cart-plus\"\n    ]\n  },\n  \"cart-shopping\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cart shopping\",\n    \"terms\": [\n      \"shopping-cart\",\n      \"Cart shopping\",\n      \"cart-shopping\"\n    ]\n  },\n  \"cash-register\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cash Register\",\n    \"terms\": [\n      \"Cash Register\",\n      \"cash-register\"\n    ]\n  },\n  \"cat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cat\",\n    \"terms\": [\n      \"Cat\",\n      \"cat\"\n    ]\n  },\n  \"cc-amazon-pay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Amazon Pay Credit Card\",\n    \"terms\": [\n      \"Amazon Pay Credit Card\",\n      \"cc-amazon-pay\"\n    ]\n  },\n  \"cc-amex\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"American Express Credit Card\",\n    \"terms\": [\n      \"American Express Credit Card\",\n      \"cc-amex\"\n    ]\n  },\n  \"cc-apple-pay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Apple Pay Credit Card\",\n    \"terms\": [\n      \"Apple Pay Credit Card\",\n      \"cc-apple-pay\"\n    ]\n  },\n  \"cc-diners-club\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Diner's Club Credit Card\",\n    \"terms\": [\n      \"Diner's Club Credit Card\",\n      \"cc-diners-club\"\n    ]\n  },\n  \"cc-discover\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Discover Credit Card\",\n    \"terms\": [\n      \"Discover Credit Card\",\n      \"cc-discover\"\n    ]\n  },\n  \"cc-jcb\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"JCB Credit Card\",\n    \"terms\": [\n      \"JCB Credit Card\",\n      \"cc-jcb\"\n    ]\n  },\n  \"cc-mastercard\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"MasterCard Credit Card\",\n    \"terms\": [\n      \"MasterCard Credit Card\",\n      \"cc-mastercard\"\n    ]\n  },\n  \"cc-paypal\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Paypal Credit Card\",\n    \"terms\": [\n      \"Paypal Credit Card\",\n      \"cc-paypal\"\n    ]\n  },\n  \"cc-stripe\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Stripe Credit Card\",\n    \"terms\": [\n      \"Stripe Credit Card\",\n      \"cc-stripe\"\n    ]\n  },\n  \"cc-visa\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Visa Credit Card\",\n    \"terms\": [\n      \"Visa Credit Card\",\n      \"cc-visa\"\n    ]\n  },\n  \"cedi-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cedi Sign\",\n    \"terms\": [\n      \"Cedi Sign\",\n      \"cedi-sign\"\n    ]\n  },\n  \"cent-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cent Sign\",\n    \"terms\": [\n      \"Cent Sign\",\n      \"cent-sign\"\n    ]\n  },\n  \"centercode\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Centercode\",\n    \"terms\": [\n      \"Centercode\",\n      \"centercode\"\n    ]\n  },\n  \"centos\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Centos\",\n    \"terms\": [\n      \"Centos\",\n      \"centos\"\n    ]\n  },\n  \"certificate\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"certificate\",\n    \"terms\": [\n      \"certificate\",\n      \"certificate\"\n    ]\n  },\n  \"chair\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chair\",\n    \"terms\": [\n      \"Chair\",\n      \"chair\"\n    ]\n  },\n  \"chalkboard\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chalkboard\",\n    \"terms\": [\n      \"blackboard\",\n      \"Chalkboard\",\n      \"chalkboard\"\n    ]\n  },\n  \"chalkboard-user\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chalkboard user\",\n    \"terms\": [\n      \"chalkboard-teacher\",\n      \"Chalkboard user\",\n      \"chalkboard-user\"\n    ]\n  },\n  \"champagne-glasses\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Champagne glasses\",\n    \"terms\": [\n      \"glass-cheers\",\n      \"Champagne glasses\",\n      \"champagne-glasses\"\n    ]\n  },\n  \"charging-station\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Charging Station\",\n    \"terms\": [\n      \"Charging Station\",\n      \"charging-station\"\n    ]\n  },\n  \"chart-area\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Area Chart\",\n    \"terms\": [\n      \"area-chart\",\n      \"Area Chart\",\n      \"chart-area\"\n    ]\n  },\n  \"chart-bar\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Bar Chart\",\n    \"terms\": [\n      \"bar-chart\",\n      \"Bar Chart\",\n      \"chart-bar\"\n    ]\n  },\n  \"chart-column\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chart Column\",\n    \"terms\": [\n      \"Chart Column\",\n      \"chart-column\"\n    ]\n  },\n  \"chart-gantt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chart Gantt\",\n    \"terms\": [\n      \"Chart Gantt\",\n      \"chart-gantt\"\n    ]\n  },\n  \"chart-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Line Chart\",\n    \"terms\": [\n      \"line-chart\",\n      \"Line Chart\",\n      \"chart-line\"\n    ]\n  },\n  \"chart-pie\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pie Chart\",\n    \"terms\": [\n      \"pie-chart\",\n      \"Pie Chart\",\n      \"chart-pie\"\n    ]\n  },\n  \"chart-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chart Simple\",\n    \"terms\": [\n      \"Chart Simple\",\n      \"chart-simple\"\n    ]\n  },\n  \"check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Check\",\n    \"terms\": [\n      \"Check\",\n      \"check\"\n    ]\n  },\n  \"check-double\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Double Check\",\n    \"terms\": [\n      \"Double Check\",\n      \"check-double\"\n    ]\n  },\n  \"check-to-slot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Check to Slot\",\n    \"terms\": [\n      \"vote-yea\",\n      \"Check to Slot\",\n      \"check-to-slot\"\n    ]\n  },\n  \"cheese\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cheese\",\n    \"terms\": [\n      \"Cheese\",\n      \"cheese\"\n    ]\n  },\n  \"chess\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chess\",\n    \"terms\": [\n      \"Chess\",\n      \"chess\"\n    ]\n  },\n  \"chess-bishop\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Chess Bishop\",\n    \"terms\": [\n      \"Chess Bishop\",\n      \"chess-bishop\"\n    ]\n  },\n  \"chess-board\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Chess Board\",\n    \"terms\": [\n      \"Chess Board\",\n      \"chess-board\"\n    ]\n  },\n  \"chess-king\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Chess King\",\n    \"terms\": [\n      \"Chess King\",\n      \"chess-king\"\n    ]\n  },\n  \"chess-knight\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Chess Knight\",\n    \"terms\": [\n      \"Chess Knight\",\n      \"chess-knight\"\n    ]\n  },\n  \"chess-pawn\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Chess Pawn\",\n    \"terms\": [\n      \"Chess Pawn\",\n      \"chess-pawn\"\n    ]\n  },\n  \"chess-queen\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Chess Queen\",\n    \"terms\": [\n      \"Chess Queen\",\n      \"chess-queen\"\n    ]\n  },\n  \"chess-rook\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Chess Rook\",\n    \"terms\": [\n      \"Chess Rook\",\n      \"chess-rook\"\n    ]\n  },\n  \"chevron-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"chevron-down\",\n    \"terms\": [\n      \"chevron-down\",\n      \"chevron-down\"\n    ]\n  },\n  \"chevron-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"chevron-left\",\n    \"terms\": [\n      \"chevron-left\",\n      \"chevron-left\"\n    ]\n  },\n  \"chevron-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"chevron-right\",\n    \"terms\": [\n      \"chevron-right\",\n      \"chevron-right\"\n    ]\n  },\n  \"chevron-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"chevron-up\",\n    \"terms\": [\n      \"chevron-up\",\n      \"chevron-up\"\n    ]\n  },\n  \"child\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Child\",\n    \"terms\": [\n      \"Child\",\n      \"child\"\n    ]\n  },\n  \"child-dress\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Child Dress\",\n    \"terms\": [\n      \"Child Dress\",\n      \"child-dress\"\n    ]\n  },\n  \"child-reaching\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Child Reaching\",\n    \"terms\": [\n      \"Child Reaching\",\n      \"child-reaching\"\n    ]\n  },\n  \"child-rifle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Child Rifle\",\n    \"terms\": [\n      \"Child Rifle\",\n      \"child-rifle\"\n    ]\n  },\n  \"children\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Children\",\n    \"terms\": [\n      \"Children\",\n      \"children\"\n    ]\n  },\n  \"chrome\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Chrome\",\n    \"terms\": [\n      \"Chrome\",\n      \"chrome\"\n    ]\n  },\n  \"chromecast\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Chromecast\",\n    \"terms\": [\n      \"Chromecast\",\n      \"chromecast\"\n    ]\n  },\n  \"church\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Church\",\n    \"terms\": [\n      \"Church\",\n      \"church\"\n    ]\n  },\n  \"circle\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle\",\n    \"terms\": [\n      \"Circle\",\n      \"circle\"\n    ]\n  },\n  \"circle-arrow-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle arrow down\",\n    \"terms\": [\n      \"arrow-circle-down\",\n      \"Circle arrow down\",\n      \"circle-arrow-down\"\n    ]\n  },\n  \"circle-arrow-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle arrow left\",\n    \"terms\": [\n      \"arrow-circle-left\",\n      \"Circle arrow left\",\n      \"circle-arrow-left\"\n    ]\n  },\n  \"circle-arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle arrow right\",\n    \"terms\": [\n      \"arrow-circle-right\",\n      \"Circle arrow right\",\n      \"circle-arrow-right\"\n    ]\n  },\n  \"circle-arrow-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle arrow up\",\n    \"terms\": [\n      \"arrow-circle-up\",\n      \"Circle arrow up\",\n      \"circle-arrow-up\"\n    ]\n  },\n  \"circle-check\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle check\",\n    \"terms\": [\n      \"check-circle\",\n      \"Circle check\",\n      \"circle-check\"\n    ]\n  },\n  \"circle-chevron-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle chevron down\",\n    \"terms\": [\n      \"chevron-circle-down\",\n      \"Circle chevron down\",\n      \"circle-chevron-down\"\n    ]\n  },\n  \"circle-chevron-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle chevron left\",\n    \"terms\": [\n      \"chevron-circle-left\",\n      \"Circle chevron left\",\n      \"circle-chevron-left\"\n    ]\n  },\n  \"circle-chevron-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle chevron right\",\n    \"terms\": [\n      \"chevron-circle-right\",\n      \"Circle chevron right\",\n      \"circle-chevron-right\"\n    ]\n  },\n  \"circle-chevron-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle chevron up\",\n    \"terms\": [\n      \"chevron-circle-up\",\n      \"Circle chevron up\",\n      \"circle-chevron-up\"\n    ]\n  },\n  \"circle-dollar-to-slot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle dollar to slot\",\n    \"terms\": [\n      \"donate\",\n      \"Circle dollar to slot\",\n      \"circle-dollar-to-slot\"\n    ]\n  },\n  \"circle-dot\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle dot\",\n    \"terms\": [\n      \"dot-circle\",\n      \"Circle dot\",\n      \"circle-dot\"\n    ]\n  },\n  \"circle-down\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle down\",\n    \"terms\": [\n      \"arrow-alt-circle-down\",\n      \"Circle down\",\n      \"circle-down\"\n    ]\n  },\n  \"circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle exclamation\",\n    \"terms\": [\n      \"exclamation-circle\",\n      \"Circle exclamation\",\n      \"circle-exclamation\"\n    ]\n  },\n  \"circle-h\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle h\",\n    \"terms\": [\n      \"hospital-symbol\",\n      \"Circle h\",\n      \"circle-h\"\n    ]\n  },\n  \"circle-half-stroke\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle half stroke\",\n    \"terms\": [\n      \"adjust\",\n      \"Circle half stroke\",\n      \"circle-half-stroke\"\n    ]\n  },\n  \"circle-info\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle info\",\n    \"terms\": [\n      \"info-circle\",\n      \"Circle info\",\n      \"circle-info\"\n    ]\n  },\n  \"circle-left\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle left\",\n    \"terms\": [\n      \"arrow-alt-circle-left\",\n      \"Circle left\",\n      \"circle-left\"\n    ]\n  },\n  \"circle-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle minus\",\n    \"terms\": [\n      \"minus-circle\",\n      \"Circle minus\",\n      \"circle-minus\"\n    ]\n  },\n  \"circle-nodes\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle Nodes\",\n    \"terms\": [\n      \"Circle Nodes\",\n      \"circle-nodes\"\n    ]\n  },\n  \"circle-notch\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle Notched\",\n    \"terms\": [\n      \"Circle Notched\",\n      \"circle-notch\"\n    ]\n  },\n  \"circle-pause\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle pause\",\n    \"terms\": [\n      \"pause-circle\",\n      \"Circle pause\",\n      \"circle-pause\"\n    ]\n  },\n  \"circle-play\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle play\",\n    \"terms\": [\n      \"play-circle\",\n      \"Circle play\",\n      \"circle-play\"\n    ]\n  },\n  \"circle-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle plus\",\n    \"terms\": [\n      \"plus-circle\",\n      \"Circle plus\",\n      \"circle-plus\"\n    ]\n  },\n  \"circle-question\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle question\",\n    \"terms\": [\n      \"question-circle\",\n      \"Circle question\",\n      \"circle-question\"\n    ]\n  },\n  \"circle-radiation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Circle radiation\",\n    \"terms\": [\n      \"radiation-alt\",\n      \"Circle radiation\",\n      \"circle-radiation\"\n    ]\n  },\n  \"circle-right\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle right\",\n    \"terms\": [\n      \"arrow-alt-circle-right\",\n      \"Circle right\",\n      \"circle-right\"\n    ]\n  },\n  \"circle-stop\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle stop\",\n    \"terms\": [\n      \"stop-circle\",\n      \"Circle stop\",\n      \"circle-stop\"\n    ]\n  },\n  \"circle-up\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle up\",\n    \"terms\": [\n      \"arrow-alt-circle-up\",\n      \"Circle up\",\n      \"circle-up\"\n    ]\n  },\n  \"circle-user\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle user\",\n    \"terms\": [\n      \"user-circle\",\n      \"Circle user\",\n      \"circle-user\"\n    ]\n  },\n  \"circle-xmark\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Circle X Mark\",\n    \"terms\": [\n      \"times-circle\",\n      \"xmark-circle\",\n      \"Circle X Mark\",\n      \"circle-xmark\"\n    ]\n  },\n  \"city\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"City\",\n    \"terms\": [\n      \"City\",\n      \"city\"\n    ]\n  },\n  \"clapperboard\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Clapperboard\",\n    \"terms\": [\n      \"Clapperboard\",\n      \"clapperboard\"\n    ]\n  },\n  \"clipboard\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Clipboard\",\n    \"terms\": [\n      \"Clipboard\",\n      \"clipboard\"\n    ]\n  },\n  \"clipboard-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Clipboard with Check\",\n    \"terms\": [\n      \"Clipboard with Check\",\n      \"clipboard-check\"\n    ]\n  },\n  \"clipboard-list\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Clipboard List\",\n    \"terms\": [\n      \"Clipboard List\",\n      \"clipboard-list\"\n    ]\n  },\n  \"clipboard-question\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Clipboard Question\",\n    \"terms\": [\n      \"Clipboard Question\",\n      \"clipboard-question\"\n    ]\n  },\n  \"clipboard-user\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Clipboard with User\",\n    \"terms\": [\n      \"Clipboard with User\",\n      \"clipboard-user\"\n    ]\n  },\n  \"clock\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Clock\",\n    \"terms\": [\n      \"clock-four\",\n      \"Clock\",\n      \"clock\"\n    ]\n  },\n  \"clock-rotate-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Clock Rotate Left\",\n    \"terms\": [\n      \"history\",\n      \"Clock Rotate Left\",\n      \"clock-rotate-left\"\n    ]\n  },\n  \"clone\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Clone\",\n    \"terms\": [\n      \"Clone\",\n      \"clone\"\n    ]\n  },\n  \"closed-captioning\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Closed Captioning\",\n    \"terms\": [\n      \"Closed Captioning\",\n      \"closed-captioning\"\n    ]\n  },\n  \"cloud\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud\",\n    \"terms\": [\n      \"Cloud\",\n      \"cloud\"\n    ]\n  },\n  \"cloud-arrow-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud arrow down\",\n    \"terms\": [\n      \"cloud-download\",\n      \"cloud-download-alt\",\n      \"Cloud arrow down\",\n      \"cloud-arrow-down\"\n    ]\n  },\n  \"cloud-arrow-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud arrow up\",\n    \"terms\": [\n      \"cloud-upload\",\n      \"cloud-upload-alt\",\n      \"Cloud arrow up\",\n      \"cloud-arrow-up\"\n    ]\n  },\n  \"cloud-bolt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud bolt\",\n    \"terms\": [\n      \"thunderstorm\",\n      \"Cloud bolt\",\n      \"cloud-bolt\"\n    ]\n  },\n  \"cloud-meatball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud with (a chance of) Meatball\",\n    \"terms\": [\n      \"Cloud with (a chance of) Meatball\",\n      \"cloud-meatball\"\n    ]\n  },\n  \"cloud-moon\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud with Moon\",\n    \"terms\": [\n      \"Cloud with Moon\",\n      \"cloud-moon\"\n    ]\n  },\n  \"cloud-moon-rain\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud with Moon and Rain\",\n    \"terms\": [\n      \"Cloud with Moon and Rain\",\n      \"cloud-moon-rain\"\n    ]\n  },\n  \"cloud-rain\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud with Rain\",\n    \"terms\": [\n      \"Cloud with Rain\",\n      \"cloud-rain\"\n    ]\n  },\n  \"cloud-showers-heavy\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud with Heavy Showers\",\n    \"terms\": [\n      \"Cloud with Heavy Showers\",\n      \"cloud-showers-heavy\"\n    ]\n  },\n  \"cloud-showers-water\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud Showers-water\",\n    \"terms\": [\n      \"Cloud Showers-water\",\n      \"cloud-showers-water\"\n    ]\n  },\n  \"cloud-sun\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud with Sun\",\n    \"terms\": [\n      \"Cloud with Sun\",\n      \"cloud-sun\"\n    ]\n  },\n  \"cloud-sun-rain\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cloud with Sun and Rain\",\n    \"terms\": [\n      \"Cloud with Sun and Rain\",\n      \"cloud-sun-rain\"\n    ]\n  },\n  \"cloudflare\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Cloudflare\",\n    \"terms\": [\n      \"Cloudflare\",\n      \"cloudflare\"\n    ]\n  },\n  \"cloudscale\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"cloudscale.ch\",\n    \"terms\": [\n      \"cloudscale.ch\",\n      \"cloudscale\"\n    ]\n  },\n  \"cloudsmith\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Cloudsmith\",\n    \"terms\": [\n      \"Cloudsmith\",\n      \"cloudsmith\"\n    ]\n  },\n  \"cloudversify\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"cloudversify\",\n    \"terms\": [\n      \"cloudversify\",\n      \"cloudversify\"\n    ]\n  },\n  \"clover\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Clover\",\n    \"terms\": [\n      \"Clover\",\n      \"clover\"\n    ]\n  },\n  \"cmplid\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Cmplid\",\n    \"terms\": [\n      \"Cmplid\",\n      \"cmplid\"\n    ]\n  },\n  \"code\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Code\",\n    \"terms\": [\n      \"Code\",\n      \"code\"\n    ]\n  },\n  \"code-branch\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Code Branch\",\n    \"terms\": [\n      \"Code Branch\",\n      \"code-branch\"\n    ]\n  },\n  \"code-commit\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Code Commit\",\n    \"terms\": [\n      \"Code Commit\",\n      \"code-commit\"\n    ]\n  },\n  \"code-compare\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Code Compare\",\n    \"terms\": [\n      \"Code Compare\",\n      \"code-compare\"\n    ]\n  },\n  \"code-fork\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Code Fork\",\n    \"terms\": [\n      \"Code Fork\",\n      \"code-fork\"\n    ]\n  },\n  \"code-merge\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Code Merge\",\n    \"terms\": [\n      \"Code Merge\",\n      \"code-merge\"\n    ]\n  },\n  \"code-pull-request\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Code Pull Request\",\n    \"terms\": [\n      \"Code Pull Request\",\n      \"code-pull-request\"\n    ]\n  },\n  \"codepen\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Codepen\",\n    \"terms\": [\n      \"Codepen\",\n      \"codepen\"\n    ]\n  },\n  \"codiepie\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Codie Pie\",\n    \"terms\": [\n      \"Codie Pie\",\n      \"codiepie\"\n    ]\n  },\n  \"coins\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Coins\",\n    \"terms\": [\n      \"Coins\",\n      \"coins\"\n    ]\n  },\n  \"colon-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Colon Sign\",\n    \"terms\": [\n      \"Colon Sign\",\n      \"colon-sign\"\n    ]\n  },\n  \"comment\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"comment\",\n    \"terms\": [\n      \"comment\",\n      \"comment\"\n    ]\n  },\n  \"comment-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Comment Dollar\",\n    \"terms\": [\n      \"Comment Dollar\",\n      \"comment-dollar\"\n    ]\n  },\n  \"comment-dots\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Comment Dots\",\n    \"terms\": [\n      \"commenting\",\n      \"Comment Dots\",\n      \"comment-dots\"\n    ]\n  },\n  \"comment-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Alternate Medical Chat\",\n    \"terms\": [\n      \"Alternate Medical Chat\",\n      \"comment-medical\"\n    ]\n  },\n  \"comment-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Comment Slash\",\n    \"terms\": [\n      \"Comment Slash\",\n      \"comment-slash\"\n    ]\n  },\n  \"comment-sms\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Comment sms\",\n    \"terms\": [\n      \"sms\",\n      \"Comment sms\",\n      \"comment-sms\"\n    ]\n  },\n  \"comments\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"comments\",\n    \"terms\": [\n      \"comments\",\n      \"comments\"\n    ]\n  },\n  \"comments-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Comments Dollar\",\n    \"terms\": [\n      \"Comments Dollar\",\n      \"comments-dollar\"\n    ]\n  },\n  \"compact-disc\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Compact Disc\",\n    \"terms\": [\n      \"Compact Disc\",\n      \"compact-disc\"\n    ]\n  },\n  \"compass\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Compass\",\n    \"terms\": [\n      \"Compass\",\n      \"compass\"\n    ]\n  },\n  \"compass-drafting\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Compass drafting\",\n    \"terms\": [\n      \"drafting-compass\",\n      \"Compass drafting\",\n      \"compass-drafting\"\n    ]\n  },\n  \"compress\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Compress\",\n    \"terms\": [\n      \"Compress\",\n      \"compress\"\n    ]\n  },\n  \"computer\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Computer\",\n    \"terms\": [\n      \"Computer\",\n      \"computer\"\n    ]\n  },\n  \"computer-mouse\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Computer mouse\",\n    \"terms\": [\n      \"mouse\",\n      \"Computer mouse\",\n      \"computer-mouse\"\n    ]\n  },\n  \"confluence\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Confluence\",\n    \"terms\": [\n      \"Confluence\",\n      \"confluence\"\n    ]\n  },\n  \"connectdevelop\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Connect Develop\",\n    \"terms\": [\n      \"Connect Develop\",\n      \"connectdevelop\"\n    ]\n  },\n  \"contao\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Contao\",\n    \"terms\": [\n      \"Contao\",\n      \"contao\"\n    ]\n  },\n  \"cookie\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cookie\",\n    \"terms\": [\n      \"Cookie\",\n      \"cookie\"\n    ]\n  },\n  \"cookie-bite\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cookie Bite\",\n    \"terms\": [\n      \"Cookie Bite\",\n      \"cookie-bite\"\n    ]\n  },\n  \"copy\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Copy\",\n    \"terms\": [\n      \"Copy\",\n      \"copy\"\n    ]\n  },\n  \"copyright\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Copyright\",\n    \"terms\": [\n      \"Copyright\",\n      \"copyright\"\n    ]\n  },\n  \"cotton-bureau\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Cotton Bureau\",\n    \"terms\": [\n      \"Cotton Bureau\",\n      \"cotton-bureau\"\n    ]\n  },\n  \"couch\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Couch\",\n    \"terms\": [\n      \"Couch\",\n      \"couch\"\n    ]\n  },\n  \"cow\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cow\",\n    \"terms\": [\n      \"Cow\",\n      \"cow\"\n    ]\n  },\n  \"cpanel\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"cPanel\",\n    \"terms\": [\n      \"cPanel\",\n      \"cpanel\"\n    ]\n  },\n  \"creative-commons\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons\",\n    \"terms\": [\n      \"Creative Commons\",\n      \"creative-commons\"\n    ]\n  },\n  \"creative-commons-by\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Attribution\",\n    \"terms\": [\n      \"Creative Commons Attribution\",\n      \"creative-commons-by\"\n    ]\n  },\n  \"creative-commons-nc\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Noncommercial\",\n    \"terms\": [\n      \"Creative Commons Noncommercial\",\n      \"creative-commons-nc\"\n    ]\n  },\n  \"creative-commons-nc-eu\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Noncommercial (Euro Sign)\",\n    \"terms\": [\n      \"Creative Commons Noncommercial (Euro Sign)\",\n      \"creative-commons-nc-eu\"\n    ]\n  },\n  \"creative-commons-nc-jp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Noncommercial (Yen Sign)\",\n    \"terms\": [\n      \"Creative Commons Noncommercial (Yen Sign)\",\n      \"creative-commons-nc-jp\"\n    ]\n  },\n  \"creative-commons-nd\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons No Derivative Works\",\n    \"terms\": [\n      \"Creative Commons No Derivative Works\",\n      \"creative-commons-nd\"\n    ]\n  },\n  \"creative-commons-pd\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Public Domain\",\n    \"terms\": [\n      \"Creative Commons Public Domain\",\n      \"creative-commons-pd\"\n    ]\n  },\n  \"creative-commons-pd-alt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Alternate Creative Commons Public Domain\",\n    \"terms\": [\n      \"Alternate Creative Commons Public Domain\",\n      \"creative-commons-pd-alt\"\n    ]\n  },\n  \"creative-commons-remix\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Remix\",\n    \"terms\": [\n      \"Creative Commons Remix\",\n      \"creative-commons-remix\"\n    ]\n  },\n  \"creative-commons-sa\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Share Alike\",\n    \"terms\": [\n      \"Creative Commons Share Alike\",\n      \"creative-commons-sa\"\n    ]\n  },\n  \"creative-commons-sampling\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Sampling\",\n    \"terms\": [\n      \"Creative Commons Sampling\",\n      \"creative-commons-sampling\"\n    ]\n  },\n  \"creative-commons-sampling-plus\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Sampling +\",\n    \"terms\": [\n      \"Creative Commons Sampling +\",\n      \"creative-commons-sampling-plus\"\n    ]\n  },\n  \"creative-commons-share\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons Share\",\n    \"terms\": [\n      \"Creative Commons Share\",\n      \"creative-commons-share\"\n    ]\n  },\n  \"creative-commons-zero\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Creative Commons CC0\",\n    \"terms\": [\n      \"Creative Commons CC0\",\n      \"creative-commons-zero\"\n    ]\n  },\n  \"credit-card\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Credit Card\",\n    \"terms\": [\n      \"credit-card-alt\",\n      \"Credit Card\",\n      \"credit-card\"\n    ]\n  },\n  \"critical-role\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Critical Role\",\n    \"terms\": [\n      \"Critical Role\",\n      \"critical-role\"\n    ]\n  },\n  \"crop\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"crop\",\n    \"terms\": [\n      \"crop\",\n      \"crop\"\n    ]\n  },\n  \"crop-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Crop simple\",\n    \"terms\": [\n      \"crop-alt\",\n      \"Crop simple\",\n      \"crop-simple\"\n    ]\n  },\n  \"cross\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cross\",\n    \"terms\": [\n      \"Cross\",\n      \"cross\"\n    ]\n  },\n  \"crosshairs\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Crosshairs\",\n    \"terms\": [\n      \"Crosshairs\",\n      \"crosshairs\"\n    ]\n  },\n  \"crow\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Crow\",\n    \"terms\": [\n      \"Crow\",\n      \"crow\"\n    ]\n  },\n  \"crown\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Crown\",\n    \"terms\": [\n      \"Crown\",\n      \"crown\"\n    ]\n  },\n  \"crutch\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Crutch\",\n    \"terms\": [\n      \"Crutch\",\n      \"crutch\"\n    ]\n  },\n  \"cruzeiro-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cruzeiro Sign\",\n    \"terms\": [\n      \"Cruzeiro Sign\",\n      \"cruzeiro-sign\"\n    ]\n  },\n  \"css3\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"CSS 3 Logo\",\n    \"terms\": [\n      \"CSS 3 Logo\",\n      \"css3\"\n    ]\n  },\n  \"css3-alt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Alternate CSS3 Logo\",\n    \"terms\": [\n      \"Alternate CSS3 Logo\",\n      \"css3-alt\"\n    ]\n  },\n  \"cube\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cube\",\n    \"terms\": [\n      \"Cube\",\n      \"cube\"\n    ]\n  },\n  \"cubes\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cubes\",\n    \"terms\": [\n      \"Cubes\",\n      \"cubes\"\n    ]\n  },\n  \"cubes-stacked\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cubes Stacked\",\n    \"terms\": [\n      \"Cubes Stacked\",\n      \"cubes-stacked\"\n    ]\n  },\n  \"cuttlefish\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Cuttlefish\",\n    \"terms\": [\n      \"Cuttlefish\",\n      \"cuttlefish\"\n    ]\n  },\n  \"d\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"D\",\n    \"terms\": [\n      \"D\",\n      \"d\"\n    ]\n  },\n  \"d-and-d\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Dungeons & Dragons\",\n    \"terms\": [\n      \"Dungeons & Dragons\",\n      \"d-and-d\"\n    ]\n  },\n  \"d-and-d-beyond\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"D&D Beyond\",\n    \"terms\": [\n      \"D&D Beyond\",\n      \"d-and-d-beyond\"\n    ]\n  },\n  \"dailymotion\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"dailymotion\",\n    \"terms\": [\n      \"dailymotion\",\n      \"dailymotion\"\n    ]\n  },\n  \"dashcube\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"DashCube\",\n    \"terms\": [\n      \"DashCube\",\n      \"dashcube\"\n    ]\n  },\n  \"database\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Database\",\n    \"terms\": [\n      \"Database\",\n      \"database\"\n    ]\n  },\n  \"deezer\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Deezer\",\n    \"terms\": [\n      \"Deezer\",\n      \"deezer\"\n    ]\n  },\n  \"delete-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Delete left\",\n    \"terms\": [\n      \"backspace\",\n      \"Delete left\",\n      \"delete-left\"\n    ]\n  },\n  \"delicious\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Delicious\",\n    \"terms\": [\n      \"Delicious\",\n      \"delicious\"\n    ]\n  },\n  \"democrat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Democrat\",\n    \"terms\": [\n      \"Democrat\",\n      \"democrat\"\n    ]\n  },\n  \"deploydog\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"deploy.dog\",\n    \"terms\": [\n      \"deploy.dog\",\n      \"deploydog\"\n    ]\n  },\n  \"deskpro\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Deskpro\",\n    \"terms\": [\n      \"Deskpro\",\n      \"deskpro\"\n    ]\n  },\n  \"desktop\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Desktop\",\n    \"terms\": [\n      \"desktop-alt\",\n      \"Desktop\",\n      \"desktop\"\n    ]\n  },\n  \"dev\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"DEV\",\n    \"terms\": [\n      \"DEV\",\n      \"dev\"\n    ]\n  },\n  \"deviantart\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"deviantART\",\n    \"terms\": [\n      \"deviantART\",\n      \"deviantart\"\n    ]\n  },\n  \"dharmachakra\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dharmachakra\",\n    \"terms\": [\n      \"Dharmachakra\",\n      \"dharmachakra\"\n    ]\n  },\n  \"dhl\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"DHL\",\n    \"terms\": [\n      \"DHL\",\n      \"dhl\"\n    ]\n  },\n  \"diagram-next\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Diagram Next\",\n    \"terms\": [\n      \"Diagram Next\",\n      \"diagram-next\"\n    ]\n  },\n  \"diagram-predecessor\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Diagram Predecessor\",\n    \"terms\": [\n      \"Diagram Predecessor\",\n      \"diagram-predecessor\"\n    ]\n  },\n  \"diagram-project\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Project Diagram\",\n    \"terms\": [\n      \"project-diagram\",\n      \"Project Diagram\",\n      \"diagram-project\"\n    ]\n  },\n  \"diagram-successor\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Diagram Successor\",\n    \"terms\": [\n      \"Diagram Successor\",\n      \"diagram-successor\"\n    ]\n  },\n  \"diamond\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Diamond\",\n    \"terms\": [\n      \"Diamond\",\n      \"diamond\"\n    ]\n  },\n  \"diamond-turn-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Diamond turn right\",\n    \"terms\": [\n      \"directions\",\n      \"Diamond turn right\",\n      \"diamond-turn-right\"\n    ]\n  },\n  \"diaspora\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Diaspora\",\n    \"terms\": [\n      \"Diaspora\",\n      \"diaspora\"\n    ]\n  },\n  \"dice\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice\",\n    \"terms\": [\n      \"Dice\",\n      \"dice\"\n    ]\n  },\n  \"dice-d20\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice D20\",\n    \"terms\": [\n      \"Dice D20\",\n      \"dice-d20\"\n    ]\n  },\n  \"dice-d6\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice D6\",\n    \"terms\": [\n      \"Dice D6\",\n      \"dice-d6\"\n    ]\n  },\n  \"dice-five\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice Five\",\n    \"terms\": [\n      \"Dice Five\",\n      \"dice-five\"\n    ]\n  },\n  \"dice-four\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice Four\",\n    \"terms\": [\n      \"Dice Four\",\n      \"dice-four\"\n    ]\n  },\n  \"dice-one\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice One\",\n    \"terms\": [\n      \"Dice One\",\n      \"dice-one\"\n    ]\n  },\n  \"dice-six\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice Six\",\n    \"terms\": [\n      \"Dice Six\",\n      \"dice-six\"\n    ]\n  },\n  \"dice-three\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice Three\",\n    \"terms\": [\n      \"Dice Three\",\n      \"dice-three\"\n    ]\n  },\n  \"dice-two\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dice Two\",\n    \"terms\": [\n      \"Dice Two\",\n      \"dice-two\"\n    ]\n  },\n  \"digg\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Digg Logo\",\n    \"terms\": [\n      \"Digg Logo\",\n      \"digg\"\n    ]\n  },\n  \"digital-ocean\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Digital Ocean\",\n    \"terms\": [\n      \"Digital Ocean\",\n      \"digital-ocean\"\n    ]\n  },\n  \"discord\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Discord\",\n    \"terms\": [\n      \"Discord\",\n      \"discord\"\n    ]\n  },\n  \"discourse\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Discourse\",\n    \"terms\": [\n      \"Discourse\",\n      \"discourse\"\n    ]\n  },\n  \"disease\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Disease\",\n    \"terms\": [\n      \"Disease\",\n      \"disease\"\n    ]\n  },\n  \"display\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Display\",\n    \"terms\": [\n      \"Display\",\n      \"display\"\n    ]\n  },\n  \"divide\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Divide\",\n    \"terms\": [\n      \"Divide\",\n      \"divide\"\n    ]\n  },\n  \"dna\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"DNA\",\n    \"terms\": [\n      \"DNA\",\n      \"dna\"\n    ]\n  },\n  \"dochub\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"DocHub\",\n    \"terms\": [\n      \"DocHub\",\n      \"dochub\"\n    ]\n  },\n  \"docker\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Docker\",\n    \"terms\": [\n      \"Docker\",\n      \"docker\"\n    ]\n  },\n  \"dog\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dog\",\n    \"terms\": [\n      \"Dog\",\n      \"dog\"\n    ]\n  },\n  \"dollar-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dollar Sign\",\n    \"terms\": [\n      \"dollar\",\n      \"usd\",\n      \"Dollar Sign\",\n      \"dollar-sign\"\n    ]\n  },\n  \"dolly\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dolly\",\n    \"terms\": [\n      \"dolly-box\",\n      \"Dolly\",\n      \"dolly\"\n    ]\n  },\n  \"dong-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dong Sign\",\n    \"terms\": [\n      \"Dong Sign\",\n      \"dong-sign\"\n    ]\n  },\n  \"door-closed\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Door Closed\",\n    \"terms\": [\n      \"Door Closed\",\n      \"door-closed\"\n    ]\n  },\n  \"door-open\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Door Open\",\n    \"terms\": [\n      \"Door Open\",\n      \"door-open\"\n    ]\n  },\n  \"dove\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dove\",\n    \"terms\": [\n      \"Dove\",\n      \"dove\"\n    ]\n  },\n  \"down-left-and-up-right-to-center\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Down left and up right to center\",\n    \"terms\": [\n      \"compress-alt\",\n      \"Down left and up right to center\",\n      \"down-left-and-up-right-to-center\"\n    ]\n  },\n  \"down-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Down long\",\n    \"terms\": [\n      \"long-arrow-alt-down\",\n      \"Down long\",\n      \"down-long\"\n    ]\n  },\n  \"download\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Download\",\n    \"terms\": [\n      \"Download\",\n      \"download\"\n    ]\n  },\n  \"draft2digital\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Draft2digital\",\n    \"terms\": [\n      \"Draft2digital\",\n      \"draft2digital\"\n    ]\n  },\n  \"dragon\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dragon\",\n    \"terms\": [\n      \"Dragon\",\n      \"dragon\"\n    ]\n  },\n  \"draw-polygon\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Draw Polygon\",\n    \"terms\": [\n      \"Draw Polygon\",\n      \"draw-polygon\"\n    ]\n  },\n  \"dribbble\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Dribbble\",\n    \"terms\": [\n      \"Dribbble\",\n      \"dribbble\"\n    ]\n  },\n  \"dropbox\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Dropbox\",\n    \"terms\": [\n      \"Dropbox\",\n      \"dropbox\"\n    ]\n  },\n  \"droplet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Droplet\",\n    \"terms\": [\n      \"tint\",\n      \"Droplet\",\n      \"droplet\"\n    ]\n  },\n  \"droplet-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Droplet slash\",\n    \"terms\": [\n      \"tint-slash\",\n      \"Droplet slash\",\n      \"droplet-slash\"\n    ]\n  },\n  \"drum\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Drum\",\n    \"terms\": [\n      \"Drum\",\n      \"drum\"\n    ]\n  },\n  \"drum-steelpan\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Drum Steelpan\",\n    \"terms\": [\n      \"Drum Steelpan\",\n      \"drum-steelpan\"\n    ]\n  },\n  \"drumstick-bite\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Drumstick with Bite Taken Out\",\n    \"terms\": [\n      \"Drumstick with Bite Taken Out\",\n      \"drumstick-bite\"\n    ]\n  },\n  \"drupal\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Drupal Logo\",\n    \"terms\": [\n      \"Drupal Logo\",\n      \"drupal\"\n    ]\n  },\n  \"dumbbell\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dumbbell\",\n    \"terms\": [\n      \"Dumbbell\",\n      \"dumbbell\"\n    ]\n  },\n  \"dumpster\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dumpster\",\n    \"terms\": [\n      \"Dumpster\",\n      \"dumpster\"\n    ]\n  },\n  \"dumpster-fire\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dumpster Fire\",\n    \"terms\": [\n      \"Dumpster Fire\",\n      \"dumpster-fire\"\n    ]\n  },\n  \"dungeon\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Dungeon\",\n    \"terms\": [\n      \"Dungeon\",\n      \"dungeon\"\n    ]\n  },\n  \"dyalog\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Dyalog\",\n    \"terms\": [\n      \"Dyalog\",\n      \"dyalog\"\n    ]\n  },\n  \"e\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"E\",\n    \"terms\": [\n      \"E\",\n      \"e\"\n    ]\n  },\n  \"ear-deaf\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ear deaf\",\n    \"terms\": [\n      \"deaf\",\n      \"deafness\",\n      \"hard-of-hearing\",\n      \"Ear deaf\",\n      \"ear-deaf\"\n    ]\n  },\n  \"ear-listen\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ear listen\",\n    \"terms\": [\n      \"assistive-listening-systems\",\n      \"Ear listen\",\n      \"ear-listen\"\n    ]\n  },\n  \"earlybirds\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Earlybirds\",\n    \"terms\": [\n      \"Earlybirds\",\n      \"earlybirds\"\n    ]\n  },\n  \"earth-africa\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Earth Africa\",\n    \"terms\": [\n      \"globe-africa\",\n      \"Earth Africa\",\n      \"earth-africa\"\n    ]\n  },\n  \"earth-americas\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Earth americas\",\n    \"terms\": [\n      \"earth\",\n      \"earth-america\",\n      \"globe-americas\",\n      \"Earth americas\",\n      \"earth-americas\"\n    ]\n  },\n  \"earth-asia\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Earth Asia\",\n    \"terms\": [\n      \"globe-asia\",\n      \"Earth Asia\",\n      \"earth-asia\"\n    ]\n  },\n  \"earth-europe\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Earth Europe\",\n    \"terms\": [\n      \"globe-europe\",\n      \"Earth Europe\",\n      \"earth-europe\"\n    ]\n  },\n  \"earth-oceania\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Earth Oceania\",\n    \"terms\": [\n      \"globe-oceania\",\n      \"Earth Oceania\",\n      \"earth-oceania\"\n    ]\n  },\n  \"ebay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"eBay\",\n    \"terms\": [\n      \"eBay\",\n      \"ebay\"\n    ]\n  },\n  \"edge\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Edge Browser\",\n    \"terms\": [\n      \"Edge Browser\",\n      \"edge\"\n    ]\n  },\n  \"edge-legacy\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Edge Legacy Browser\",\n    \"terms\": [\n      \"Edge Legacy Browser\",\n      \"edge-legacy\"\n    ]\n  },\n  \"egg\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Egg\",\n    \"terms\": [\n      \"Egg\",\n      \"egg\"\n    ]\n  },\n  \"eject\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"eject\",\n    \"terms\": [\n      \"eject\",\n      \"eject\"\n    ]\n  },\n  \"elementor\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Elementor\",\n    \"terms\": [\n      \"Elementor\",\n      \"elementor\"\n    ]\n  },\n  \"elevator\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Elevator\",\n    \"terms\": [\n      \"Elevator\",\n      \"elevator\"\n    ]\n  },\n  \"ellipsis\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ellipsis\",\n    \"terms\": [\n      \"ellipsis-h\",\n      \"Ellipsis\",\n      \"ellipsis\"\n    ]\n  },\n  \"ellipsis-vertical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ellipsis vertical\",\n    \"terms\": [\n      \"ellipsis-v\",\n      \"Ellipsis vertical\",\n      \"ellipsis-vertical\"\n    ]\n  },\n  \"ello\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Ello\",\n    \"terms\": [\n      \"Ello\",\n      \"ello\"\n    ]\n  },\n  \"ember\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Ember\",\n    \"terms\": [\n      \"Ember\",\n      \"ember\"\n    ]\n  },\n  \"empire\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Galactic Empire\",\n    \"terms\": [\n      \"Galactic Empire\",\n      \"empire\"\n    ]\n  },\n  \"envelope\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Envelope\",\n    \"terms\": [\n      \"Envelope\",\n      \"envelope\"\n    ]\n  },\n  \"envelope-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Envelope Circle-check\",\n    \"terms\": [\n      \"Envelope Circle-check\",\n      \"envelope-circle-check\"\n    ]\n  },\n  \"envelope-open\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Envelope Open\",\n    \"terms\": [\n      \"Envelope Open\",\n      \"envelope-open\"\n    ]\n  },\n  \"envelope-open-text\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Envelope Open-text\",\n    \"terms\": [\n      \"Envelope Open-text\",\n      \"envelope-open-text\"\n    ]\n  },\n  \"envelopes-bulk\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Envelopes bulk\",\n    \"terms\": [\n      \"mail-bulk\",\n      \"Envelopes bulk\",\n      \"envelopes-bulk\"\n    ]\n  },\n  \"envira\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Envira Gallery\",\n    \"terms\": [\n      \"Envira Gallery\",\n      \"envira\"\n    ]\n  },\n  \"equals\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Equals\",\n    \"terms\": [\n      \"Equals\",\n      \"equals\"\n    ]\n  },\n  \"eraser\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"eraser\",\n    \"terms\": [\n      \"eraser\",\n      \"eraser\"\n    ]\n  },\n  \"erlang\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Erlang\",\n    \"terms\": [\n      \"Erlang\",\n      \"erlang\"\n    ]\n  },\n  \"ethereum\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Ethereum\",\n    \"terms\": [\n      \"Ethereum\",\n      \"ethereum\"\n    ]\n  },\n  \"ethernet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ethernet\",\n    \"terms\": [\n      \"Ethernet\",\n      \"ethernet\"\n    ]\n  },\n  \"etsy\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Etsy\",\n    \"terms\": [\n      \"Etsy\",\n      \"etsy\"\n    ]\n  },\n  \"euro-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Euro Sign\",\n    \"terms\": [\n      \"eur\",\n      \"euro\",\n      \"Euro Sign\",\n      \"euro-sign\"\n    ]\n  },\n  \"evernote\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Evernote\",\n    \"terms\": [\n      \"Evernote\",\n      \"evernote\"\n    ]\n  },\n  \"exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"exclamation\",\n    \"terms\": [\n      \"exclamation\",\n      \"exclamation\"\n    ]\n  },\n  \"expand\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Expand\",\n    \"terms\": [\n      \"Expand\",\n      \"expand\"\n    ]\n  },\n  \"expeditedssl\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"ExpeditedSSL\",\n    \"terms\": [\n      \"ExpeditedSSL\",\n      \"expeditedssl\"\n    ]\n  },\n  \"explosion\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Explosion\",\n    \"terms\": [\n      \"Explosion\",\n      \"explosion\"\n    ]\n  },\n  \"eye\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Eye\",\n    \"terms\": [\n      \"Eye\",\n      \"eye\"\n    ]\n  },\n  \"eye-dropper\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Eye Dropper\",\n    \"terms\": [\n      \"eye-dropper-empty\",\n      \"eyedropper\",\n      \"Eye Dropper\",\n      \"eye-dropper\"\n    ]\n  },\n  \"eye-low-vision\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Eye low vision\",\n    \"terms\": [\n      \"low-vision\",\n      \"Eye low vision\",\n      \"eye-low-vision\"\n    ]\n  },\n  \"eye-slash\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Eye Slash\",\n    \"terms\": [\n      \"Eye Slash\",\n      \"eye-slash\"\n    ]\n  },\n  \"f\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"F\",\n    \"terms\": [\n      \"F\",\n      \"f\"\n    ]\n  },\n  \"face-angry\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face angry\",\n    \"terms\": [\n      \"angry\",\n      \"Face angry\",\n      \"face-angry\"\n    ]\n  },\n  \"face-dizzy\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face dizzy\",\n    \"terms\": [\n      \"dizzy\",\n      \"Face dizzy\",\n      \"face-dizzy\"\n    ]\n  },\n  \"face-flushed\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face flushed\",\n    \"terms\": [\n      \"flushed\",\n      \"Face flushed\",\n      \"face-flushed\"\n    ]\n  },\n  \"face-frown\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face frown\",\n    \"terms\": [\n      \"frown\",\n      \"Face frown\",\n      \"face-frown\"\n    ]\n  },\n  \"face-frown-open\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face frown open\",\n    \"terms\": [\n      \"frown-open\",\n      \"Face frown open\",\n      \"face-frown-open\"\n    ]\n  },\n  \"face-grimace\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grimace\",\n    \"terms\": [\n      \"grimace\",\n      \"Face grimace\",\n      \"face-grimace\"\n    ]\n  },\n  \"face-grin\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin\",\n    \"terms\": [\n      \"grin\",\n      \"Face grin\",\n      \"face-grin\"\n    ]\n  },\n  \"face-grin-beam\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin beam\",\n    \"terms\": [\n      \"grin-beam\",\n      \"Face grin beam\",\n      \"face-grin-beam\"\n    ]\n  },\n  \"face-grin-beam-sweat\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin beam sweat\",\n    \"terms\": [\n      \"grin-beam-sweat\",\n      \"Face grin beam sweat\",\n      \"face-grin-beam-sweat\"\n    ]\n  },\n  \"face-grin-hearts\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin hearts\",\n    \"terms\": [\n      \"grin-hearts\",\n      \"Face grin hearts\",\n      \"face-grin-hearts\"\n    ]\n  },\n  \"face-grin-squint\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin squint\",\n    \"terms\": [\n      \"grin-squint\",\n      \"Face grin squint\",\n      \"face-grin-squint\"\n    ]\n  },\n  \"face-grin-squint-tears\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin squint tears\",\n    \"terms\": [\n      \"grin-squint-tears\",\n      \"Face grin squint tears\",\n      \"face-grin-squint-tears\"\n    ]\n  },\n  \"face-grin-stars\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin stars\",\n    \"terms\": [\n      \"grin-stars\",\n      \"Face grin stars\",\n      \"face-grin-stars\"\n    ]\n  },\n  \"face-grin-tears\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin tears\",\n    \"terms\": [\n      \"grin-tears\",\n      \"Face grin tears\",\n      \"face-grin-tears\"\n    ]\n  },\n  \"face-grin-tongue\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin tongue\",\n    \"terms\": [\n      \"grin-tongue\",\n      \"Face grin tongue\",\n      \"face-grin-tongue\"\n    ]\n  },\n  \"face-grin-tongue-squint\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin tongue squint\",\n    \"terms\": [\n      \"grin-tongue-squint\",\n      \"Face grin tongue squint\",\n      \"face-grin-tongue-squint\"\n    ]\n  },\n  \"face-grin-tongue-wink\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin tongue wink\",\n    \"terms\": [\n      \"grin-tongue-wink\",\n      \"Face grin tongue wink\",\n      \"face-grin-tongue-wink\"\n    ]\n  },\n  \"face-grin-wide\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin wide\",\n    \"terms\": [\n      \"grin-alt\",\n      \"Face grin wide\",\n      \"face-grin-wide\"\n    ]\n  },\n  \"face-grin-wink\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face grin wink\",\n    \"terms\": [\n      \"grin-wink\",\n      \"Face grin wink\",\n      \"face-grin-wink\"\n    ]\n  },\n  \"face-kiss\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face kiss\",\n    \"terms\": [\n      \"kiss\",\n      \"Face kiss\",\n      \"face-kiss\"\n    ]\n  },\n  \"face-kiss-beam\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Kiss Beam\",\n    \"terms\": [\n      \"kiss-beam\",\n      \"Face Kiss Beam\",\n      \"face-kiss-beam\"\n    ]\n  },\n  \"face-kiss-wink-heart\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Kiss Wink Heart\",\n    \"terms\": [\n      \"kiss-wink-heart\",\n      \"Face Kiss Wink Heart\",\n      \"face-kiss-wink-heart\"\n    ]\n  },\n  \"face-laugh\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Laugh\",\n    \"terms\": [\n      \"laugh\",\n      \"Face Laugh\",\n      \"face-laugh\"\n    ]\n  },\n  \"face-laugh-beam\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Laugh Beam\",\n    \"terms\": [\n      \"laugh-beam\",\n      \"Face Laugh Beam\",\n      \"face-laugh-beam\"\n    ]\n  },\n  \"face-laugh-squint\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Laugh Squint\",\n    \"terms\": [\n      \"laugh-squint\",\n      \"Face Laugh Squint\",\n      \"face-laugh-squint\"\n    ]\n  },\n  \"face-laugh-wink\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Laugh Wink\",\n    \"terms\": [\n      \"laugh-wink\",\n      \"Face Laugh Wink\",\n      \"face-laugh-wink\"\n    ]\n  },\n  \"face-meh\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face meh\",\n    \"terms\": [\n      \"meh\",\n      \"Face meh\",\n      \"face-meh\"\n    ]\n  },\n  \"face-meh-blank\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Meh Blank\",\n    \"terms\": [\n      \"meh-blank\",\n      \"Face Meh Blank\",\n      \"face-meh-blank\"\n    ]\n  },\n  \"face-rolling-eyes\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Rolling Eyes\",\n    \"terms\": [\n      \"meh-rolling-eyes\",\n      \"Face Rolling Eyes\",\n      \"face-rolling-eyes\"\n    ]\n  },\n  \"face-sad-cry\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Sad Cry\",\n    \"terms\": [\n      \"sad-cry\",\n      \"Face Sad Cry\",\n      \"face-sad-cry\"\n    ]\n  },\n  \"face-sad-tear\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Sad Tear\",\n    \"terms\": [\n      \"sad-tear\",\n      \"Face Sad Tear\",\n      \"face-sad-tear\"\n    ]\n  },\n  \"face-smile\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Smile\",\n    \"terms\": [\n      \"smile\",\n      \"Face Smile\",\n      \"face-smile\"\n    ]\n  },\n  \"face-smile-beam\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Smile Beam\",\n    \"terms\": [\n      \"smile-beam\",\n      \"Face Smile Beam\",\n      \"face-smile-beam\"\n    ]\n  },\n  \"face-smile-wink\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Smile Wink\",\n    \"terms\": [\n      \"smile-wink\",\n      \"Face Smile Wink\",\n      \"face-smile-wink\"\n    ]\n  },\n  \"face-surprise\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Surprise\",\n    \"terms\": [\n      \"surprise\",\n      \"Face Surprise\",\n      \"face-surprise\"\n    ]\n  },\n  \"face-tired\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Face Tired\",\n    \"terms\": [\n      \"tired\",\n      \"Face Tired\",\n      \"face-tired\"\n    ]\n  },\n  \"facebook\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Facebook\",\n    \"terms\": [\n      \"Facebook\",\n      \"facebook\"\n    ]\n  },\n  \"facebook-f\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Facebook F\",\n    \"terms\": [\n      \"Facebook F\",\n      \"facebook-f\"\n    ]\n  },\n  \"facebook-messenger\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Facebook Messenger\",\n    \"terms\": [\n      \"Facebook Messenger\",\n      \"facebook-messenger\"\n    ]\n  },\n  \"fan\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fan\",\n    \"terms\": [\n      \"Fan\",\n      \"fan\"\n    ]\n  },\n  \"fantasy-flight-games\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Fantasy Flight-games\",\n    \"terms\": [\n      \"Fantasy Flight-games\",\n      \"fantasy-flight-games\"\n    ]\n  },\n  \"faucet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Faucet\",\n    \"terms\": [\n      \"Faucet\",\n      \"faucet\"\n    ]\n  },\n  \"faucet-drip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Faucet Drip\",\n    \"terms\": [\n      \"Faucet Drip\",\n      \"faucet-drip\"\n    ]\n  },\n  \"fax\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fax\",\n    \"terms\": [\n      \"Fax\",\n      \"fax\"\n    ]\n  },\n  \"feather\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Feather\",\n    \"terms\": [\n      \"Feather\",\n      \"feather\"\n    ]\n  },\n  \"feather-pointed\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Feather pointed\",\n    \"terms\": [\n      \"feather-alt\",\n      \"Feather pointed\",\n      \"feather-pointed\"\n    ]\n  },\n  \"fedex\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"FedEx\",\n    \"terms\": [\n      \"FedEx\",\n      \"fedex\"\n    ]\n  },\n  \"fedora\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Fedora\",\n    \"terms\": [\n      \"Fedora\",\n      \"fedora\"\n    ]\n  },\n  \"ferry\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ferry\",\n    \"terms\": [\n      \"Ferry\",\n      \"ferry\"\n    ]\n  },\n  \"figma\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Figma\",\n    \"terms\": [\n      \"Figma\",\n      \"figma\"\n    ]\n  },\n  \"file\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"File\",\n    \"terms\": [\n      \"File\",\n      \"file\"\n    ]\n  },\n  \"file-arrow-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File arrow down\",\n    \"terms\": [\n      \"file-download\",\n      \"File arrow down\",\n      \"file-arrow-down\"\n    ]\n  },\n  \"file-arrow-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File arrow up\",\n    \"terms\": [\n      \"file-upload\",\n      \"File arrow up\",\n      \"file-arrow-up\"\n    ]\n  },\n  \"file-audio\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Audio File\",\n    \"terms\": [\n      \"Audio File\",\n      \"file-audio\"\n    ]\n  },\n  \"file-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Circle-Check\",\n    \"terms\": [\n      \"File Circle-Check\",\n      \"file-circle-check\"\n    ]\n  },\n  \"file-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Circle-exclamation\",\n    \"terms\": [\n      \"File Circle-exclamation\",\n      \"file-circle-exclamation\"\n    ]\n  },\n  \"file-circle-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Circle-minus\",\n    \"terms\": [\n      \"File Circle-minus\",\n      \"file-circle-minus\"\n    ]\n  },\n  \"file-circle-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Circle-plus\",\n    \"terms\": [\n      \"File Circle-plus\",\n      \"file-circle-plus\"\n    ]\n  },\n  \"file-circle-question\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Circle-question\",\n    \"terms\": [\n      \"File Circle-question\",\n      \"file-circle-question\"\n    ]\n  },\n  \"file-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Circle-xmark\",\n    \"terms\": [\n      \"File Circle-xmark\",\n      \"file-circle-xmark\"\n    ]\n  },\n  \"file-code\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Code File\",\n    \"terms\": [\n      \"Code File\",\n      \"file-code\"\n    ]\n  },\n  \"file-contract\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Contract\",\n    \"terms\": [\n      \"File Contract\",\n      \"file-contract\"\n    ]\n  },\n  \"file-csv\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File CSV\",\n    \"terms\": [\n      \"File CSV\",\n      \"file-csv\"\n    ]\n  },\n  \"file-excel\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Excel File\",\n    \"terms\": [\n      \"Excel File\",\n      \"file-excel\"\n    ]\n  },\n  \"file-export\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Export\",\n    \"terms\": [\n      \"arrow-right-from-file\",\n      \"File Export\",\n      \"file-export\"\n    ]\n  },\n  \"file-image\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Image File\",\n    \"terms\": [\n      \"Image File\",\n      \"file-image\"\n    ]\n  },\n  \"file-import\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Import\",\n    \"terms\": [\n      \"arrow-right-to-file\",\n      \"File Import\",\n      \"file-import\"\n    ]\n  },\n  \"file-invoice\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Invoice\",\n    \"terms\": [\n      \"File Invoice\",\n      \"file-invoice\"\n    ]\n  },\n  \"file-invoice-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Invoice with US Dollar\",\n    \"terms\": [\n      \"File Invoice with US Dollar\",\n      \"file-invoice-dollar\"\n    ]\n  },\n  \"file-lines\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"File lines\",\n    \"terms\": [\n      \"file-alt\",\n      \"file-text\",\n      \"File lines\",\n      \"file-lines\"\n    ]\n  },\n  \"file-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Medical File\",\n    \"terms\": [\n      \"Medical File\",\n      \"file-medical\"\n    ]\n  },\n  \"file-pdf\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"PDF File\",\n    \"terms\": [\n      \"PDF File\",\n      \"file-pdf\"\n    ]\n  },\n  \"file-pen\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File pen\",\n    \"terms\": [\n      \"file-edit\",\n      \"File pen\",\n      \"file-pen\"\n    ]\n  },\n  \"file-powerpoint\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Powerpoint File\",\n    \"terms\": [\n      \"Powerpoint File\",\n      \"file-powerpoint\"\n    ]\n  },\n  \"file-prescription\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Prescription\",\n    \"terms\": [\n      \"File Prescription\",\n      \"file-prescription\"\n    ]\n  },\n  \"file-shield\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Shield\",\n    \"terms\": [\n      \"File Shield\",\n      \"file-shield\"\n    ]\n  },\n  \"file-signature\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File Signature\",\n    \"terms\": [\n      \"File Signature\",\n      \"file-signature\"\n    ]\n  },\n  \"file-video\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Video File\",\n    \"terms\": [\n      \"Video File\",\n      \"file-video\"\n    ]\n  },\n  \"file-waveform\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"File waveform\",\n    \"terms\": [\n      \"file-medical-alt\",\n      \"File waveform\",\n      \"file-waveform\"\n    ]\n  },\n  \"file-word\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Word File\",\n    \"terms\": [\n      \"Word File\",\n      \"file-word\"\n    ]\n  },\n  \"file-zipper\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"File zipper\",\n    \"terms\": [\n      \"file-archive\",\n      \"File zipper\",\n      \"file-zipper\"\n    ]\n  },\n  \"fill\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fill\",\n    \"terms\": [\n      \"Fill\",\n      \"fill\"\n    ]\n  },\n  \"fill-drip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fill Drip\",\n    \"terms\": [\n      \"Fill Drip\",\n      \"fill-drip\"\n    ]\n  },\n  \"film\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Film\",\n    \"terms\": [\n      \"Film\",\n      \"film\"\n    ]\n  },\n  \"filter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Filter\",\n    \"terms\": [\n      \"Filter\",\n      \"filter\"\n    ]\n  },\n  \"filter-circle-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Filter Circle Dollar\",\n    \"terms\": [\n      \"funnel-dollar\",\n      \"Filter Circle Dollar\",\n      \"filter-circle-dollar\"\n    ]\n  },\n  \"filter-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Filter Circle X Mark\",\n    \"terms\": [\n      \"Filter Circle X Mark\",\n      \"filter-circle-xmark\"\n    ]\n  },\n  \"fingerprint\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fingerprint\",\n    \"terms\": [\n      \"Fingerprint\",\n      \"fingerprint\"\n    ]\n  },\n  \"fire\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"fire\",\n    \"terms\": [\n      \"fire\",\n      \"fire\"\n    ]\n  },\n  \"fire-burner\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fire Burner\",\n    \"terms\": [\n      \"Fire Burner\",\n      \"fire-burner\"\n    ]\n  },\n  \"fire-extinguisher\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"fire-extinguisher\",\n    \"terms\": [\n      \"fire-extinguisher\",\n      \"fire-extinguisher\"\n    ]\n  },\n  \"fire-flame-curved\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fire flame curved\",\n    \"terms\": [\n      \"fire-alt\",\n      \"Fire flame curved\",\n      \"fire-flame-curved\"\n    ]\n  },\n  \"fire-flame-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fire flame simple\",\n    \"terms\": [\n      \"burn\",\n      \"Fire flame simple\",\n      \"fire-flame-simple\"\n    ]\n  },\n  \"firefox\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Firefox\",\n    \"terms\": [\n      \"Firefox\",\n      \"firefox\"\n    ]\n  },\n  \"firefox-browser\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Firefox Browser\",\n    \"terms\": [\n      \"Firefox Browser\",\n      \"firefox-browser\"\n    ]\n  },\n  \"first-order\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"First Order\",\n    \"terms\": [\n      \"First Order\",\n      \"first-order\"\n    ]\n  },\n  \"first-order-alt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Alternate First Order\",\n    \"terms\": [\n      \"Alternate First Order\",\n      \"first-order-alt\"\n    ]\n  },\n  \"firstdraft\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"firstdraft\",\n    \"terms\": [\n      \"firstdraft\",\n      \"firstdraft\"\n    ]\n  },\n  \"fish\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fish\",\n    \"terms\": [\n      \"Fish\",\n      \"fish\"\n    ]\n  },\n  \"fish-fins\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Fish Fins\",\n    \"terms\": [\n      \"Fish Fins\",\n      \"fish-fins\"\n    ]\n  },\n  \"flag\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"flag\",\n    \"terms\": [\n      \"flag\",\n      \"flag\"\n    ]\n  },\n  \"flag-checkered\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"flag-checkered\",\n    \"terms\": [\n      \"flag-checkered\",\n      \"flag-checkered\"\n    ]\n  },\n  \"flag-usa\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"United States of America Flag\",\n    \"terms\": [\n      \"United States of America Flag\",\n      \"flag-usa\"\n    ]\n  },\n  \"flask\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Flask\",\n    \"terms\": [\n      \"Flask\",\n      \"flask\"\n    ]\n  },\n  \"flask-vial\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Flask and Vial\",\n    \"terms\": [\n      \"Flask and Vial\",\n      \"flask-vial\"\n    ]\n  },\n  \"flickr\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Flickr\",\n    \"terms\": [\n      \"Flickr\",\n      \"flickr\"\n    ]\n  },\n  \"flipboard\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Flipboard\",\n    \"terms\": [\n      \"Flipboard\",\n      \"flipboard\"\n    ]\n  },\n  \"floppy-disk\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Floppy Disk\",\n    \"terms\": [\n      \"save\",\n      \"Floppy Disk\",\n      \"floppy-disk\"\n    ]\n  },\n  \"florin-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Florin Sign\",\n    \"terms\": [\n      \"Florin Sign\",\n      \"florin-sign\"\n    ]\n  },\n  \"fly\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Fly\",\n    \"terms\": [\n      \"Fly\",\n      \"fly\"\n    ]\n  },\n  \"folder\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Folder\",\n    \"terms\": [\n      \"folder-blank\",\n      \"Folder\",\n      \"folder\"\n    ]\n  },\n  \"folder-closed\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Folder Closed\",\n    \"terms\": [\n      \"Folder Closed\",\n      \"folder-closed\"\n    ]\n  },\n  \"folder-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Folder Minus\",\n    \"terms\": [\n      \"Folder Minus\",\n      \"folder-minus\"\n    ]\n  },\n  \"folder-open\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Folder Open\",\n    \"terms\": [\n      \"Folder Open\",\n      \"folder-open\"\n    ]\n  },\n  \"folder-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Folder Plus\",\n    \"terms\": [\n      \"Folder Plus\",\n      \"folder-plus\"\n    ]\n  },\n  \"folder-tree\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Folder Tree\",\n    \"terms\": [\n      \"Folder Tree\",\n      \"folder-tree\"\n    ]\n  },\n  \"font\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"font\",\n    \"terms\": [\n      \"font\",\n      \"font\"\n    ]\n  },\n  \"font-awesome\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\",\n      \"brands\"\n    ],\n    \"label\": \"Font Awesome\",\n    \"terms\": [\n      \"font-awesome-flag\",\n      \"font-awesome-logo-full\",\n      \"Font Awesome\",\n      \"font-awesome\"\n    ]\n  },\n  \"fonticons\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Fonticons\",\n    \"terms\": [\n      \"Fonticons\",\n      \"fonticons\"\n    ]\n  },\n  \"fonticons-fi\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Fonticons Fi\",\n    \"terms\": [\n      \"Fonticons Fi\",\n      \"fonticons-fi\"\n    ]\n  },\n  \"football\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Football Ball\",\n    \"terms\": [\n      \"football-ball\",\n      \"Football Ball\",\n      \"football\"\n    ]\n  },\n  \"fort-awesome\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Fort Awesome\",\n    \"terms\": [\n      \"Fort Awesome\",\n      \"fort-awesome\"\n    ]\n  },\n  \"fort-awesome-alt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Alternate Fort Awesome\",\n    \"terms\": [\n      \"Alternate Fort Awesome\",\n      \"fort-awesome-alt\"\n    ]\n  },\n  \"forumbee\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Forumbee\",\n    \"terms\": [\n      \"Forumbee\",\n      \"forumbee\"\n    ]\n  },\n  \"forward\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"forward\",\n    \"terms\": [\n      \"forward\",\n      \"forward\"\n    ]\n  },\n  \"forward-fast\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Forward fast\",\n    \"terms\": [\n      \"fast-forward\",\n      \"Forward fast\",\n      \"forward-fast\"\n    ]\n  },\n  \"forward-step\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Forward step\",\n    \"terms\": [\n      \"step-forward\",\n      \"Forward step\",\n      \"forward-step\"\n    ]\n  },\n  \"foursquare\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Foursquare\",\n    \"terms\": [\n      \"Foursquare\",\n      \"foursquare\"\n    ]\n  },\n  \"franc-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Franc Sign\",\n    \"terms\": [\n      \"Franc Sign\",\n      \"franc-sign\"\n    ]\n  },\n  \"free-code-camp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"freeCodeCamp\",\n    \"terms\": [\n      \"freeCodeCamp\",\n      \"free-code-camp\"\n    ]\n  },\n  \"freebsd\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"FreeBSD\",\n    \"terms\": [\n      \"FreeBSD\",\n      \"freebsd\"\n    ]\n  },\n  \"frog\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Frog\",\n    \"terms\": [\n      \"Frog\",\n      \"frog\"\n    ]\n  },\n  \"fulcrum\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Fulcrum\",\n    \"terms\": [\n      \"Fulcrum\",\n      \"fulcrum\"\n    ]\n  },\n  \"futbol\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Futbol ball\",\n    \"terms\": [\n      \"futbol-ball\",\n      \"soccer-ball\",\n      \"Futbol ball\",\n      \"futbol\"\n    ]\n  },\n  \"g\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"G\",\n    \"terms\": [\n      \"G\",\n      \"g\"\n    ]\n  },\n  \"galactic-republic\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Galactic Republic\",\n    \"terms\": [\n      \"Galactic Republic\",\n      \"galactic-republic\"\n    ]\n  },\n  \"galactic-senate\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Galactic Senate\",\n    \"terms\": [\n      \"Galactic Senate\",\n      \"galactic-senate\"\n    ]\n  },\n  \"gamepad\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gamepad\",\n    \"terms\": [\n      \"Gamepad\",\n      \"gamepad\"\n    ]\n  },\n  \"gas-pump\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gas Pump\",\n    \"terms\": [\n      \"Gas Pump\",\n      \"gas-pump\"\n    ]\n  },\n  \"gauge\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gauge med\",\n    \"terms\": [\n      \"dashboard\",\n      \"gauge-med\",\n      \"tachometer-alt-average\",\n      \"Gauge med\",\n      \"gauge\"\n    ]\n  },\n  \"gauge-high\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gauge\",\n    \"terms\": [\n      \"tachometer-alt\",\n      \"tachometer-alt-fast\",\n      \"Gauge\",\n      \"gauge-high\"\n    ]\n  },\n  \"gauge-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gauge simple med\",\n    \"terms\": [\n      \"gauge-simple-med\",\n      \"tachometer-average\",\n      \"Gauge simple med\",\n      \"gauge-simple\"\n    ]\n  },\n  \"gauge-simple-high\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gauge simple\",\n    \"terms\": [\n      \"tachometer\",\n      \"tachometer-fast\",\n      \"Gauge simple\",\n      \"gauge-simple-high\"\n    ]\n  },\n  \"gavel\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gavel\",\n    \"terms\": [\n      \"legal\",\n      \"Gavel\",\n      \"gavel\"\n    ]\n  },\n  \"gear\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gear\",\n    \"terms\": [\n      \"cog\",\n      \"Gear\",\n      \"gear\"\n    ]\n  },\n  \"gears\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gears\",\n    \"terms\": [\n      \"cogs\",\n      \"Gears\",\n      \"gears\"\n    ]\n  },\n  \"gem\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Gem\",\n    \"terms\": [\n      \"Gem\",\n      \"gem\"\n    ]\n  },\n  \"genderless\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Genderless\",\n    \"terms\": [\n      \"Genderless\",\n      \"genderless\"\n    ]\n  },\n  \"get-pocket\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Get Pocket\",\n    \"terms\": [\n      \"Get Pocket\",\n      \"get-pocket\"\n    ]\n  },\n  \"gg\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"GG Currency\",\n    \"terms\": [\n      \"GG Currency\",\n      \"gg\"\n    ]\n  },\n  \"gg-circle\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"GG Currency Circle\",\n    \"terms\": [\n      \"GG Currency Circle\",\n      \"gg-circle\"\n    ]\n  },\n  \"ghost\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ghost\",\n    \"terms\": [\n      \"Ghost\",\n      \"ghost\"\n    ]\n  },\n  \"gift\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"gift\",\n    \"terms\": [\n      \"gift\",\n      \"gift\"\n    ]\n  },\n  \"gifts\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gifts\",\n    \"terms\": [\n      \"Gifts\",\n      \"gifts\"\n    ]\n  },\n  \"git\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Git\",\n    \"terms\": [\n      \"Git\",\n      \"git\"\n    ]\n  },\n  \"git-alt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Git Alt\",\n    \"terms\": [\n      \"Git Alt\",\n      \"git-alt\"\n    ]\n  },\n  \"github\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"GitHub\",\n    \"terms\": [\n      \"GitHub\",\n      \"github\"\n    ]\n  },\n  \"github-alt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Alternate GitHub\",\n    \"terms\": [\n      \"Alternate GitHub\",\n      \"github-alt\"\n    ]\n  },\n  \"gitkraken\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"GitKraken\",\n    \"terms\": [\n      \"GitKraken\",\n      \"gitkraken\"\n    ]\n  },\n  \"gitlab\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"GitLab\",\n    \"terms\": [\n      \"GitLab\",\n      \"gitlab\"\n    ]\n  },\n  \"gitter\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Gitter\",\n    \"terms\": [\n      \"Gitter\",\n      \"gitter\"\n    ]\n  },\n  \"glass-water\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Glass Water\",\n    \"terms\": [\n      \"Glass Water\",\n      \"glass-water\"\n    ]\n  },\n  \"glass-water-droplet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Glass Water-droplet\",\n    \"terms\": [\n      \"Glass Water-droplet\",\n      \"glass-water-droplet\"\n    ]\n  },\n  \"glasses\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Glasses\",\n    \"terms\": [\n      \"Glasses\",\n      \"glasses\"\n    ]\n  },\n  \"glide\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Glide\",\n    \"terms\": [\n      \"Glide\",\n      \"glide\"\n    ]\n  },\n  \"glide-g\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Glide G\",\n    \"terms\": [\n      \"Glide G\",\n      \"glide-g\"\n    ]\n  },\n  \"globe\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Globe\",\n    \"terms\": [\n      \"Globe\",\n      \"globe\"\n    ]\n  },\n  \"gofore\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Gofore\",\n    \"terms\": [\n      \"Gofore\",\n      \"gofore\"\n    ]\n  },\n  \"golang\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Go\",\n    \"terms\": [\n      \"Go\",\n      \"golang\"\n    ]\n  },\n  \"golf-ball-tee\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Golf ball tee\",\n    \"terms\": [\n      \"golf-ball\",\n      \"Golf ball tee\",\n      \"golf-ball-tee\"\n    ]\n  },\n  \"goodreads\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Goodreads\",\n    \"terms\": [\n      \"Goodreads\",\n      \"goodreads\"\n    ]\n  },\n  \"goodreads-g\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Goodreads G\",\n    \"terms\": [\n      \"Goodreads G\",\n      \"goodreads-g\"\n    ]\n  },\n  \"google\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Logo\",\n    \"terms\": [\n      \"Google Logo\",\n      \"google\"\n    ]\n  },\n  \"google-drive\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Drive\",\n    \"terms\": [\n      \"Google Drive\",\n      \"google-drive\"\n    ]\n  },\n  \"google-pay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Pay\",\n    \"terms\": [\n      \"Google Pay\",\n      \"google-pay\"\n    ]\n  },\n  \"google-play\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Play\",\n    \"terms\": [\n      \"Google Play\",\n      \"google-play\"\n    ]\n  },\n  \"google-plus\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Plus\",\n    \"terms\": [\n      \"Google Plus\",\n      \"google-plus\"\n    ]\n  },\n  \"google-plus-g\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Plus G\",\n    \"terms\": [\n      \"Google Plus G\",\n      \"google-plus-g\"\n    ]\n  },\n  \"google-wallet\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Wallet\",\n    \"terms\": [\n      \"Google Wallet\",\n      \"google-wallet\"\n    ]\n  },\n  \"gopuram\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gopuram\",\n    \"terms\": [\n      \"Gopuram\",\n      \"gopuram\"\n    ]\n  },\n  \"graduation-cap\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Graduation Cap\",\n    \"terms\": [\n      \"mortar-board\",\n      \"Graduation Cap\",\n      \"graduation-cap\"\n    ]\n  },\n  \"gratipay\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Gratipay (Gittip)\",\n    \"terms\": [\n      \"Gratipay (Gittip)\",\n      \"gratipay\"\n    ]\n  },\n  \"grav\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Grav\",\n    \"terms\": [\n      \"Grav\",\n      \"grav\"\n    ]\n  },\n  \"greater-than\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Greater Than\",\n    \"terms\": [\n      \"Greater Than\",\n      \"greater-than\"\n    ]\n  },\n  \"greater-than-equal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Greater Than Equal To\",\n    \"terms\": [\n      \"Greater Than Equal To\",\n      \"greater-than-equal\"\n    ]\n  },\n  \"grip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Grip\",\n    \"terms\": [\n      \"grip-horizontal\",\n      \"Grip\",\n      \"grip\"\n    ]\n  },\n  \"grip-lines\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Grip Lines\",\n    \"terms\": [\n      \"Grip Lines\",\n      \"grip-lines\"\n    ]\n  },\n  \"grip-lines-vertical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Grip Lines Vertical\",\n    \"terms\": [\n      \"Grip Lines Vertical\",\n      \"grip-lines-vertical\"\n    ]\n  },\n  \"grip-vertical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Grip Vertical\",\n    \"terms\": [\n      \"Grip Vertical\",\n      \"grip-vertical\"\n    ]\n  },\n  \"gripfire\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Gripfire, Inc.\",\n    \"terms\": [\n      \"Gripfire, Inc.\",\n      \"gripfire\"\n    ]\n  },\n  \"group-arrows-rotate\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Group Arrows-rotate\",\n    \"terms\": [\n      \"Group Arrows-rotate\",\n      \"group-arrows-rotate\"\n    ]\n  },\n  \"grunt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Grunt\",\n    \"terms\": [\n      \"Grunt\",\n      \"grunt\"\n    ]\n  },\n  \"guarani-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Guarani Sign\",\n    \"terms\": [\n      \"Guarani Sign\",\n      \"guarani-sign\"\n    ]\n  },\n  \"guilded\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Guilded\",\n    \"terms\": [\n      \"Guilded\",\n      \"guilded\"\n    ]\n  },\n  \"guitar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Guitar\",\n    \"terms\": [\n      \"Guitar\",\n      \"guitar\"\n    ]\n  },\n  \"gulp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Gulp\",\n    \"terms\": [\n      \"Gulp\",\n      \"gulp\"\n    ]\n  },\n  \"gun\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Gun\",\n    \"terms\": [\n      \"Gun\",\n      \"gun\"\n    ]\n  },\n  \"h\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"H\",\n    \"terms\": [\n      \"H\",\n      \"h\"\n    ]\n  },\n  \"hacker-news\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hacker News\",\n    \"terms\": [\n      \"Hacker News\",\n      \"hacker-news\"\n    ]\n  },\n  \"hackerrank\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hackerrank\",\n    \"terms\": [\n      \"Hackerrank\",\n      \"hackerrank\"\n    ]\n  },\n  \"hammer\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hammer\",\n    \"terms\": [\n      \"Hammer\",\n      \"hammer\"\n    ]\n  },\n  \"hamsa\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hamsa\",\n    \"terms\": [\n      \"Hamsa\",\n      \"hamsa\"\n    ]\n  },\n  \"hand\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Paper (Hand)\",\n    \"terms\": [\n      \"hand-paper\",\n      \"Paper (Hand)\",\n      \"hand\"\n    ]\n  },\n  \"hand-back-fist\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Rock (Hand)\",\n    \"terms\": [\n      \"hand-rock\",\n      \"Rock (Hand)\",\n      \"hand-back-fist\"\n    ]\n  },\n  \"hand-dots\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand dots\",\n    \"terms\": [\n      \"allergies\",\n      \"Hand dots\",\n      \"hand-dots\"\n    ]\n  },\n  \"hand-fist\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Raised Fist\",\n    \"terms\": [\n      \"fist-raised\",\n      \"Raised Fist\",\n      \"hand-fist\"\n    ]\n  },\n  \"hand-holding\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand Holding\",\n    \"terms\": [\n      \"Hand Holding\",\n      \"hand-holding\"\n    ]\n  },\n  \"hand-holding-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand holding dollar\",\n    \"terms\": [\n      \"hand-holding-usd\",\n      \"Hand holding dollar\",\n      \"hand-holding-dollar\"\n    ]\n  },\n  \"hand-holding-droplet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand holding droplet\",\n    \"terms\": [\n      \"hand-holding-water\",\n      \"Hand holding droplet\",\n      \"hand-holding-droplet\"\n    ]\n  },\n  \"hand-holding-hand\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand Holding-hand\",\n    \"terms\": [\n      \"Hand Holding-hand\",\n      \"hand-holding-hand\"\n    ]\n  },\n  \"hand-holding-heart\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand Holding Heart\",\n    \"terms\": [\n      \"Hand Holding Heart\",\n      \"hand-holding-heart\"\n    ]\n  },\n  \"hand-holding-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand Holding Medical Cross\",\n    \"terms\": [\n      \"Hand Holding Medical Cross\",\n      \"hand-holding-medical\"\n    ]\n  },\n  \"hand-lizard\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Lizard (Hand)\",\n    \"terms\": [\n      \"Lizard (Hand)\",\n      \"hand-lizard\"\n    ]\n  },\n  \"hand-middle-finger\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand with Middle Finger Raised\",\n    \"terms\": [\n      \"Hand with Middle Finger Raised\",\n      \"hand-middle-finger\"\n    ]\n  },\n  \"hand-peace\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Peace (Hand)\",\n    \"terms\": [\n      \"Peace (Hand)\",\n      \"hand-peace\"\n    ]\n  },\n  \"hand-point-down\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Hand Pointing Down\",\n    \"terms\": [\n      \"Hand Pointing Down\",\n      \"hand-point-down\"\n    ]\n  },\n  \"hand-point-left\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Hand Pointing Left\",\n    \"terms\": [\n      \"Hand Pointing Left\",\n      \"hand-point-left\"\n    ]\n  },\n  \"hand-point-right\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Hand Pointing Right\",\n    \"terms\": [\n      \"Hand Pointing Right\",\n      \"hand-point-right\"\n    ]\n  },\n  \"hand-point-up\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Hand Pointing Up\",\n    \"terms\": [\n      \"Hand Pointing Up\",\n      \"hand-point-up\"\n    ]\n  },\n  \"hand-pointer\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Pointer (Hand)\",\n    \"terms\": [\n      \"Pointer (Hand)\",\n      \"hand-pointer\"\n    ]\n  },\n  \"hand-scissors\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Scissors (Hand)\",\n    \"terms\": [\n      \"Scissors (Hand)\",\n      \"hand-scissors\"\n    ]\n  },\n  \"hand-sparkles\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hand Sparkles\",\n    \"terms\": [\n      \"Hand Sparkles\",\n      \"hand-sparkles\"\n    ]\n  },\n  \"hand-spock\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Spock (Hand)\",\n    \"terms\": [\n      \"Spock (Hand)\",\n      \"hand-spock\"\n    ]\n  },\n  \"handcuffs\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Handcuffs\",\n    \"terms\": [\n      \"Handcuffs\",\n      \"handcuffs\"\n    ]\n  },\n  \"hands\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands\",\n    \"terms\": [\n      \"sign-language\",\n      \"signing\",\n      \"Hands\",\n      \"hands\"\n    ]\n  },\n  \"hands-asl-interpreting\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands american sign language interpreting\",\n    \"terms\": [\n      \"american-sign-language-interpreting\",\n      \"asl-interpreting\",\n      \"hands-american-sign-language-interpreting\",\n      \"Hands american sign language interpreting\",\n      \"hands-asl-interpreting\"\n    ]\n  },\n  \"hands-bound\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands Bound\",\n    \"terms\": [\n      \"Hands Bound\",\n      \"hands-bound\"\n    ]\n  },\n  \"hands-bubbles\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands bubbles\",\n    \"terms\": [\n      \"hands-wash\",\n      \"Hands bubbles\",\n      \"hands-bubbles\"\n    ]\n  },\n  \"hands-clapping\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands Clapping\",\n    \"terms\": [\n      \"Hands Clapping\",\n      \"hands-clapping\"\n    ]\n  },\n  \"hands-holding\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands holding\",\n    \"terms\": [\n      \"Hands holding\",\n      \"hands-holding\"\n    ]\n  },\n  \"hands-holding-child\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands Holding-child\",\n    \"terms\": [\n      \"Hands Holding-child\",\n      \"hands-holding-child\"\n    ]\n  },\n  \"hands-holding-circle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands Holding-circle\",\n    \"terms\": [\n      \"Hands Holding-circle\",\n      \"hands-holding-circle\"\n    ]\n  },\n  \"hands-praying\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hands praying\",\n    \"terms\": [\n      \"praying-hands\",\n      \"Hands praying\",\n      \"hands-praying\"\n    ]\n  },\n  \"handshake\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Handshake\",\n    \"terms\": [\n      \"Handshake\",\n      \"handshake\"\n    ]\n  },\n  \"handshake-angle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Handshake angle\",\n    \"terms\": [\n      \"hands-helping\",\n      \"Handshake angle\",\n      \"handshake-angle\"\n    ]\n  },\n  \"handshake-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Handshake simple\",\n    \"terms\": [\n      \"handshake-alt\",\n      \"Handshake simple\",\n      \"handshake-simple\"\n    ]\n  },\n  \"handshake-simple-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Handshake simple slash\",\n    \"terms\": [\n      \"handshake-alt-slash\",\n      \"Handshake simple slash\",\n      \"handshake-simple-slash\"\n    ]\n  },\n  \"handshake-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Handshake Slash\",\n    \"terms\": [\n      \"Handshake Slash\",\n      \"handshake-slash\"\n    ]\n  },\n  \"hanukiah\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hanukiah\",\n    \"terms\": [\n      \"Hanukiah\",\n      \"hanukiah\"\n    ]\n  },\n  \"hard-drive\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Hard drive\",\n    \"terms\": [\n      \"hdd\",\n      \"Hard drive\",\n      \"hard-drive\"\n    ]\n  },\n  \"hashnode\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hashnode\",\n    \"terms\": [\n      \"Hashnode\",\n      \"hashnode\"\n    ]\n  },\n  \"hashtag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hashtag\",\n    \"terms\": [\n      \"Hashtag\",\n      \"hashtag\"\n    ]\n  },\n  \"hat-cowboy\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cowboy Hat\",\n    \"terms\": [\n      \"Cowboy Hat\",\n      \"hat-cowboy\"\n    ]\n  },\n  \"hat-cowboy-side\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cowboy Hat Side\",\n    \"terms\": [\n      \"Cowboy Hat Side\",\n      \"hat-cowboy-side\"\n    ]\n  },\n  \"hat-wizard\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wizard's Hat\",\n    \"terms\": [\n      \"Wizard's Hat\",\n      \"hat-wizard\"\n    ]\n  },\n  \"head-side-cough\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Head Side Cough\",\n    \"terms\": [\n      \"Head Side Cough\",\n      \"head-side-cough\"\n    ]\n  },\n  \"head-side-cough-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Head Side-cough-slash\",\n    \"terms\": [\n      \"Head Side-cough-slash\",\n      \"head-side-cough-slash\"\n    ]\n  },\n  \"head-side-mask\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Head Side Mask\",\n    \"terms\": [\n      \"Head Side Mask\",\n      \"head-side-mask\"\n    ]\n  },\n  \"head-side-virus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Head Side Virus\",\n    \"terms\": [\n      \"Head Side Virus\",\n      \"head-side-virus\"\n    ]\n  },\n  \"heading\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"heading\",\n    \"terms\": [\n      \"header\",\n      \"heading\",\n      \"heading\"\n    ]\n  },\n  \"headphones\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"headphones\",\n    \"terms\": [\n      \"headphones\",\n      \"headphones\"\n    ]\n  },\n  \"headphones-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Headphones simple\",\n    \"terms\": [\n      \"headphones-alt\",\n      \"Headphones simple\",\n      \"headphones-simple\"\n    ]\n  },\n  \"headset\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Headset\",\n    \"terms\": [\n      \"Headset\",\n      \"headset\"\n    ]\n  },\n  \"heart\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Heart\",\n    \"terms\": [\n      \"Heart\",\n      \"heart\"\n    ]\n  },\n  \"heart-circle-bolt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart Circle-bolt\",\n    \"terms\": [\n      \"Heart Circle-bolt\",\n      \"heart-circle-bolt\"\n    ]\n  },\n  \"heart-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart Circle-check\",\n    \"terms\": [\n      \"Heart Circle-check\",\n      \"heart-circle-check\"\n    ]\n  },\n  \"heart-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart Circle-exclamation\",\n    \"terms\": [\n      \"Heart Circle-exclamation\",\n      \"heart-circle-exclamation\"\n    ]\n  },\n  \"heart-circle-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart Circle-minus\",\n    \"terms\": [\n      \"Heart Circle-minus\",\n      \"heart-circle-minus\"\n    ]\n  },\n  \"heart-circle-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart Circle-plus\",\n    \"terms\": [\n      \"Heart Circle-plus\",\n      \"heart-circle-plus\"\n    ]\n  },\n  \"heart-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart Circle-xmark\",\n    \"terms\": [\n      \"Heart Circle-xmark\",\n      \"heart-circle-xmark\"\n    ]\n  },\n  \"heart-crack\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart crack\",\n    \"terms\": [\n      \"heart-broken\",\n      \"Heart crack\",\n      \"heart-crack\"\n    ]\n  },\n  \"heart-pulse\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Heart pulse\",\n    \"terms\": [\n      \"heartbeat\",\n      \"Heart pulse\",\n      \"heart-pulse\"\n    ]\n  },\n  \"helicopter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Helicopter\",\n    \"terms\": [\n      \"Helicopter\",\n      \"helicopter\"\n    ]\n  },\n  \"helicopter-symbol\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Helicopter Symbol\",\n    \"terms\": [\n      \"Helicopter Symbol\",\n      \"helicopter-symbol\"\n    ]\n  },\n  \"helmet-safety\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Helmet safety\",\n    \"terms\": [\n      \"hard-hat\",\n      \"hat-hard\",\n      \"Helmet safety\",\n      \"helmet-safety\"\n    ]\n  },\n  \"helmet-un\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Helmet Un\",\n    \"terms\": [\n      \"Helmet Un\",\n      \"helmet-un\"\n    ]\n  },\n  \"highlighter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Highlighter\",\n    \"terms\": [\n      \"Highlighter\",\n      \"highlighter\"\n    ]\n  },\n  \"hill-avalanche\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hill Avalanche\",\n    \"terms\": [\n      \"Hill Avalanche\",\n      \"hill-avalanche\"\n    ]\n  },\n  \"hill-rockslide\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hill Rockslide\",\n    \"terms\": [\n      \"Hill Rockslide\",\n      \"hill-rockslide\"\n    ]\n  },\n  \"hippo\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hippo\",\n    \"terms\": [\n      \"Hippo\",\n      \"hippo\"\n    ]\n  },\n  \"hips\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hips\",\n    \"terms\": [\n      \"Hips\",\n      \"hips\"\n    ]\n  },\n  \"hire-a-helper\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"HireAHelper\",\n    \"terms\": [\n      \"HireAHelper\",\n      \"hire-a-helper\"\n    ]\n  },\n  \"hive\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hive Blockchain Network\",\n    \"terms\": [\n      \"Hive Blockchain Network\",\n      \"hive\"\n    ]\n  },\n  \"hockey-puck\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hockey Puck\",\n    \"terms\": [\n      \"Hockey Puck\",\n      \"hockey-puck\"\n    ]\n  },\n  \"holly-berry\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Holly Berry\",\n    \"terms\": [\n      \"Holly Berry\",\n      \"holly-berry\"\n    ]\n  },\n  \"hooli\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hooli\",\n    \"terms\": [\n      \"Hooli\",\n      \"hooli\"\n    ]\n  },\n  \"hornbill\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hornbill\",\n    \"terms\": [\n      \"Hornbill\",\n      \"hornbill\"\n    ]\n  },\n  \"horse\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Horse\",\n    \"terms\": [\n      \"Horse\",\n      \"horse\"\n    ]\n  },\n  \"horse-head\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Horse Head\",\n    \"terms\": [\n      \"Horse Head\",\n      \"horse-head\"\n    ]\n  },\n  \"hospital\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"hospital\",\n    \"terms\": [\n      \"hospital-alt\",\n      \"hospital-wide\",\n      \"hospital\",\n      \"hospital\"\n    ]\n  },\n  \"hospital-user\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hospital with User\",\n    \"terms\": [\n      \"Hospital with User\",\n      \"hospital-user\"\n    ]\n  },\n  \"hot-tub-person\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hot tub person\",\n    \"terms\": [\n      \"hot-tub\",\n      \"Hot tub person\",\n      \"hot-tub-person\"\n    ]\n  },\n  \"hotdog\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hot Dog\",\n    \"terms\": [\n      \"Hot Dog\",\n      \"hotdog\"\n    ]\n  },\n  \"hotel\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hotel\",\n    \"terms\": [\n      \"Hotel\",\n      \"hotel\"\n    ]\n  },\n  \"hotjar\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hotjar\",\n    \"terms\": [\n      \"Hotjar\",\n      \"hotjar\"\n    ]\n  },\n  \"hourglass\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Hourglass\",\n    \"terms\": [\n      \"hourglass-empty\",\n      \"Hourglass\",\n      \"hourglass\"\n    ]\n  },\n  \"hourglass-end\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hourglass End\",\n    \"terms\": [\n      \"hourglass-3\",\n      \"Hourglass End\",\n      \"hourglass-end\"\n    ]\n  },\n  \"hourglass-half\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Hourglass Half\",\n    \"terms\": [\n      \"hourglass-2\",\n      \"Hourglass Half\",\n      \"hourglass-half\"\n    ]\n  },\n  \"hourglass-start\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hourglass Start\",\n    \"terms\": [\n      \"hourglass-1\",\n      \"Hourglass Start\",\n      \"hourglass-start\"\n    ]\n  },\n  \"house\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House\",\n    \"terms\": [\n      \"home\",\n      \"home-alt\",\n      \"home-lg-alt\",\n      \"House\",\n      \"house\"\n    ]\n  },\n  \"house-chimney\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Chimney\",\n    \"terms\": [\n      \"home-lg\",\n      \"House Chimney\",\n      \"house-chimney\"\n    ]\n  },\n  \"house-chimney-crack\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House crack\",\n    \"terms\": [\n      \"house-damage\",\n      \"House crack\",\n      \"house-chimney-crack\"\n    ]\n  },\n  \"house-chimney-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House medical\",\n    \"terms\": [\n      \"clinic-medical\",\n      \"House medical\",\n      \"house-chimney-medical\"\n    ]\n  },\n  \"house-chimney-user\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House User\",\n    \"terms\": [\n      \"House User\",\n      \"house-chimney-user\"\n    ]\n  },\n  \"house-chimney-window\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House with Window + Chimney\",\n    \"terms\": [\n      \"House with Window + Chimney\",\n      \"house-chimney-window\"\n    ]\n  },\n  \"house-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Circle-check\",\n    \"terms\": [\n      \"House Circle-check\",\n      \"house-circle-check\"\n    ]\n  },\n  \"house-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Circle-exclamation\",\n    \"terms\": [\n      \"House Circle-exclamation\",\n      \"house-circle-exclamation\"\n    ]\n  },\n  \"house-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Circle-xmark\",\n    \"terms\": [\n      \"House Circle-xmark\",\n      \"house-circle-xmark\"\n    ]\n  },\n  \"house-crack\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Simple Crack\",\n    \"terms\": [\n      \"House Simple Crack\",\n      \"house-crack\"\n    ]\n  },\n  \"house-fire\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Fire\",\n    \"terms\": [\n      \"House Fire\",\n      \"house-fire\"\n    ]\n  },\n  \"house-flag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Flag\",\n    \"terms\": [\n      \"House Flag\",\n      \"house-flag\"\n    ]\n  },\n  \"house-flood-water\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Flood\",\n    \"terms\": [\n      \"House Flood\",\n      \"house-flood-water\"\n    ]\n  },\n  \"house-flood-water-circle-arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Flood-circle-arrow-right\",\n    \"terms\": [\n      \"House Flood-circle-arrow-right\",\n      \"house-flood-water-circle-arrow-right\"\n    ]\n  },\n  \"house-laptop\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House laptop\",\n    \"terms\": [\n      \"laptop-house\",\n      \"House laptop\",\n      \"house-laptop\"\n    ]\n  },\n  \"house-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Lock\",\n    \"terms\": [\n      \"House Lock\",\n      \"house-lock\"\n    ]\n  },\n  \"house-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Simple Medical\",\n    \"terms\": [\n      \"House Simple Medical\",\n      \"house-medical\"\n    ]\n  },\n  \"house-medical-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Medical-circle-check\",\n    \"terms\": [\n      \"House Medical-circle-check\",\n      \"house-medical-circle-check\"\n    ]\n  },\n  \"house-medical-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Medical-circle-exclamation\",\n    \"terms\": [\n      \"House Medical-circle-exclamation\",\n      \"house-medical-circle-exclamation\"\n    ]\n  },\n  \"house-medical-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Medical-circle-xmark\",\n    \"terms\": [\n      \"House Medical-circle-xmark\",\n      \"house-medical-circle-xmark\"\n    ]\n  },\n  \"house-medical-flag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Medical-flag\",\n    \"terms\": [\n      \"House Medical-flag\",\n      \"house-medical-flag\"\n    ]\n  },\n  \"house-signal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Signal\",\n    \"terms\": [\n      \"House Signal\",\n      \"house-signal\"\n    ]\n  },\n  \"house-tsunami\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"House Tsunami\",\n    \"terms\": [\n      \"House Tsunami\",\n      \"house-tsunami\"\n    ]\n  },\n  \"house-user\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Home User\",\n    \"terms\": [\n      \"home-user\",\n      \"Home User\",\n      \"house-user\"\n    ]\n  },\n  \"houzz\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Houzz\",\n    \"terms\": [\n      \"Houzz\",\n      \"houzz\"\n    ]\n  },\n  \"hryvnia-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hryvnia sign\",\n    \"terms\": [\n      \"hryvnia\",\n      \"Hryvnia sign\",\n      \"hryvnia-sign\"\n    ]\n  },\n  \"html5\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"HTML 5 Logo\",\n    \"terms\": [\n      \"HTML 5 Logo\",\n      \"html5\"\n    ]\n  },\n  \"hubspot\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"HubSpot\",\n    \"terms\": [\n      \"HubSpot\",\n      \"hubspot\"\n    ]\n  },\n  \"hurricane\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hurricane\",\n    \"terms\": [\n      \"Hurricane\",\n      \"hurricane\"\n    ]\n  },\n  \"i\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"I\",\n    \"terms\": [\n      \"I\",\n      \"i\"\n    ]\n  },\n  \"i-cursor\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"I Beam Cursor\",\n    \"terms\": [\n      \"I Beam Cursor\",\n      \"i-cursor\"\n    ]\n  },\n  \"ice-cream\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ice Cream\",\n    \"terms\": [\n      \"Ice Cream\",\n      \"ice-cream\"\n    ]\n  },\n  \"icicles\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Icicles\",\n    \"terms\": [\n      \"Icicles\",\n      \"icicles\"\n    ]\n  },\n  \"icons\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Icons\",\n    \"terms\": [\n      \"heart-music-camera-bolt\",\n      \"Icons\",\n      \"icons\"\n    ]\n  },\n  \"id-badge\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Identification Badge\",\n    \"terms\": [\n      \"Identification Badge\",\n      \"id-badge\"\n    ]\n  },\n  \"id-card\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Identification Card\",\n    \"terms\": [\n      \"drivers-license\",\n      \"Identification Card\",\n      \"id-card\"\n    ]\n  },\n  \"id-card-clip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Id card clip\",\n    \"terms\": [\n      \"id-card-alt\",\n      \"Id card clip\",\n      \"id-card-clip\"\n    ]\n  },\n  \"ideal\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"iDeal\",\n    \"terms\": [\n      \"iDeal\",\n      \"ideal\"\n    ]\n  },\n  \"igloo\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Igloo\",\n    \"terms\": [\n      \"Igloo\",\n      \"igloo\"\n    ]\n  },\n  \"image\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Image\",\n    \"terms\": [\n      \"Image\",\n      \"image\"\n    ]\n  },\n  \"image-portrait\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Image portrait\",\n    \"terms\": [\n      \"portrait\",\n      \"Image portrait\",\n      \"image-portrait\"\n    ]\n  },\n  \"images\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Images\",\n    \"terms\": [\n      \"Images\",\n      \"images\"\n    ]\n  },\n  \"imdb\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"IMDB\",\n    \"terms\": [\n      \"IMDB\",\n      \"imdb\"\n    ]\n  },\n  \"inbox\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"inbox\",\n    \"terms\": [\n      \"inbox\",\n      \"inbox\"\n    ]\n  },\n  \"indent\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Indent\",\n    \"terms\": [\n      \"Indent\",\n      \"indent\"\n    ]\n  },\n  \"indian-rupee-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Indian Rupee-sign\",\n    \"terms\": [\n      \"indian-rupee\",\n      \"inr\",\n      \"Indian Rupee-sign\",\n      \"indian-rupee-sign\"\n    ]\n  },\n  \"industry\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Industry\",\n    \"terms\": [\n      \"Industry\",\n      \"industry\"\n    ]\n  },\n  \"infinity\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Infinity\",\n    \"terms\": [\n      \"Infinity\",\n      \"infinity\"\n    ]\n  },\n  \"info\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Info\",\n    \"terms\": [\n      \"Info\",\n      \"info\"\n    ]\n  },\n  \"instagram\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Instagram\",\n    \"terms\": [\n      \"Instagram\",\n      \"instagram\"\n    ]\n  },\n  \"instalod\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"InstaLOD\",\n    \"terms\": [\n      \"InstaLOD\",\n      \"instalod\"\n    ]\n  },\n  \"intercom\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Intercom\",\n    \"terms\": [\n      \"Intercom\",\n      \"intercom\"\n    ]\n  },\n  \"internet-explorer\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Internet-explorer\",\n    \"terms\": [\n      \"Internet-explorer\",\n      \"internet-explorer\"\n    ]\n  },\n  \"invision\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"InVision\",\n    \"terms\": [\n      \"InVision\",\n      \"invision\"\n    ]\n  },\n  \"ioxhost\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"ioxhost\",\n    \"terms\": [\n      \"ioxhost\",\n      \"ioxhost\"\n    ]\n  },\n  \"italic\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"italic\",\n    \"terms\": [\n      \"italic\",\n      \"italic\"\n    ]\n  },\n  \"itch-io\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"itch.io\",\n    \"terms\": [\n      \"itch.io\",\n      \"itch-io\"\n    ]\n  },\n  \"itunes\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"iTunes\",\n    \"terms\": [\n      \"iTunes\",\n      \"itunes\"\n    ]\n  },\n  \"itunes-note\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Itunes Note\",\n    \"terms\": [\n      \"Itunes Note\",\n      \"itunes-note\"\n    ]\n  },\n  \"j\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"J\",\n    \"terms\": [\n      \"J\",\n      \"j\"\n    ]\n  },\n  \"jar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Jar\",\n    \"terms\": [\n      \"Jar\",\n      \"jar\"\n    ]\n  },\n  \"jar-wheat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Jar Wheat\",\n    \"terms\": [\n      \"Jar Wheat\",\n      \"jar-wheat\"\n    ]\n  },\n  \"java\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Java\",\n    \"terms\": [\n      \"Java\",\n      \"java\"\n    ]\n  },\n  \"jedi\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Jedi\",\n    \"terms\": [\n      \"Jedi\",\n      \"jedi\"\n    ]\n  },\n  \"jedi-order\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Jedi Order\",\n    \"terms\": [\n      \"Jedi Order\",\n      \"jedi-order\"\n    ]\n  },\n  \"jenkins\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Jenkis\",\n    \"terms\": [\n      \"Jenkis\",\n      \"jenkins\"\n    ]\n  },\n  \"jet-fighter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Jet fighter\",\n    \"terms\": [\n      \"fighter-jet\",\n      \"Jet fighter\",\n      \"jet-fighter\"\n    ]\n  },\n  \"jet-fighter-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Jet Fighter Up\",\n    \"terms\": [\n      \"Jet Fighter Up\",\n      \"jet-fighter-up\"\n    ]\n  },\n  \"jira\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Jira\",\n    \"terms\": [\n      \"Jira\",\n      \"jira\"\n    ]\n  },\n  \"joget\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Joget\",\n    \"terms\": [\n      \"Joget\",\n      \"joget\"\n    ]\n  },\n  \"joint\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Joint\",\n    \"terms\": [\n      \"Joint\",\n      \"joint\"\n    ]\n  },\n  \"joomla\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Joomla Logo\",\n    \"terms\": [\n      \"Joomla Logo\",\n      \"joomla\"\n    ]\n  },\n  \"js\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"JavaScript (JS)\",\n    \"terms\": [\n      \"JavaScript (JS)\",\n      \"js\"\n    ]\n  },\n  \"jsfiddle\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"jsFiddle\",\n    \"terms\": [\n      \"jsFiddle\",\n      \"jsfiddle\"\n    ]\n  },\n  \"jug-detergent\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Jug Detergent\",\n    \"terms\": [\n      \"Jug Detergent\",\n      \"jug-detergent\"\n    ]\n  },\n  \"k\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"K\",\n    \"terms\": [\n      \"K\",\n      \"k\"\n    ]\n  },\n  \"kaaba\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Kaaba\",\n    \"terms\": [\n      \"Kaaba\",\n      \"kaaba\"\n    ]\n  },\n  \"kaggle\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Kaggle\",\n    \"terms\": [\n      \"Kaggle\",\n      \"kaggle\"\n    ]\n  },\n  \"key\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"key\",\n    \"terms\": [\n      \"key\",\n      \"key\"\n    ]\n  },\n  \"keybase\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Keybase\",\n    \"terms\": [\n      \"Keybase\",\n      \"keybase\"\n    ]\n  },\n  \"keyboard\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Keyboard\",\n    \"terms\": [\n      \"Keyboard\",\n      \"keyboard\"\n    ]\n  },\n  \"keycdn\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"KeyCDN\",\n    \"terms\": [\n      \"KeyCDN\",\n      \"keycdn\"\n    ]\n  },\n  \"khanda\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Khanda\",\n    \"terms\": [\n      \"Khanda\",\n      \"khanda\"\n    ]\n  },\n  \"kickstarter\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Kickstarter\",\n    \"terms\": [\n      \"Kickstarter\",\n      \"kickstarter\"\n    ]\n  },\n  \"kickstarter-k\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Kickstarter K\",\n    \"terms\": [\n      \"Kickstarter K\",\n      \"kickstarter-k\"\n    ]\n  },\n  \"kip-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Kip Sign\",\n    \"terms\": [\n      \"Kip Sign\",\n      \"kip-sign\"\n    ]\n  },\n  \"kit-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Kit medical\",\n    \"terms\": [\n      \"first-aid\",\n      \"Kit medical\",\n      \"kit-medical\"\n    ]\n  },\n  \"kitchen-set\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Kitchen Set\",\n    \"terms\": [\n      \"Kitchen Set\",\n      \"kitchen-set\"\n    ]\n  },\n  \"kiwi-bird\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Kiwi Bird\",\n    \"terms\": [\n      \"Kiwi Bird\",\n      \"kiwi-bird\"\n    ]\n  },\n  \"korvue\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"KORVUE\",\n    \"terms\": [\n      \"KORVUE\",\n      \"korvue\"\n    ]\n  },\n  \"l\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"L\",\n    \"terms\": [\n      \"L\",\n      \"l\"\n    ]\n  },\n  \"land-mine-on\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Land Mine-on\",\n    \"terms\": [\n      \"Land Mine-on\",\n      \"land-mine-on\"\n    ]\n  },\n  \"landmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Landmark\",\n    \"terms\": [\n      \"Landmark\",\n      \"landmark\"\n    ]\n  },\n  \"landmark-dome\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Landmark dome\",\n    \"terms\": [\n      \"landmark-alt\",\n      \"Landmark dome\",\n      \"landmark-dome\"\n    ]\n  },\n  \"landmark-flag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Landmark Flag\",\n    \"terms\": [\n      \"Landmark Flag\",\n      \"landmark-flag\"\n    ]\n  },\n  \"language\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Language\",\n    \"terms\": [\n      \"Language\",\n      \"language\"\n    ]\n  },\n  \"laptop\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Laptop\",\n    \"terms\": [\n      \"Laptop\",\n      \"laptop\"\n    ]\n  },\n  \"laptop-code\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Laptop Code\",\n    \"terms\": [\n      \"Laptop Code\",\n      \"laptop-code\"\n    ]\n  },\n  \"laptop-file\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Laptop File\",\n    \"terms\": [\n      \"Laptop File\",\n      \"laptop-file\"\n    ]\n  },\n  \"laptop-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Laptop Medical\",\n    \"terms\": [\n      \"Laptop Medical\",\n      \"laptop-medical\"\n    ]\n  },\n  \"laravel\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Laravel\",\n    \"terms\": [\n      \"Laravel\",\n      \"laravel\"\n    ]\n  },\n  \"lari-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Lari Sign\",\n    \"terms\": [\n      \"Lari Sign\",\n      \"lari-sign\"\n    ]\n  },\n  \"lastfm\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"last.fm\",\n    \"terms\": [\n      \"last.fm\",\n      \"lastfm\"\n    ]\n  },\n  \"layer-group\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Layer Group\",\n    \"terms\": [\n      \"Layer Group\",\n      \"layer-group\"\n    ]\n  },\n  \"leaf\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"leaf\",\n    \"terms\": [\n      \"leaf\",\n      \"leaf\"\n    ]\n  },\n  \"leanpub\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Leanpub\",\n    \"terms\": [\n      \"Leanpub\",\n      \"leanpub\"\n    ]\n  },\n  \"left-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Left long\",\n    \"terms\": [\n      \"long-arrow-alt-left\",\n      \"Left long\",\n      \"left-long\"\n    ]\n  },\n  \"left-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Left right\",\n    \"terms\": [\n      \"arrows-alt-h\",\n      \"Left right\",\n      \"left-right\"\n    ]\n  },\n  \"lemon\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Lemon\",\n    \"terms\": [\n      \"Lemon\",\n      \"lemon\"\n    ]\n  },\n  \"less\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Less\",\n    \"terms\": [\n      \"Less\",\n      \"less\"\n    ]\n  },\n  \"less-than\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Less Than\",\n    \"terms\": [\n      \"Less Than\",\n      \"less-than\"\n    ]\n  },\n  \"less-than-equal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Less Than Equal To\",\n    \"terms\": [\n      \"Less Than Equal To\",\n      \"less-than-equal\"\n    ]\n  },\n  \"life-ring\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Life Ring\",\n    \"terms\": [\n      \"Life Ring\",\n      \"life-ring\"\n    ]\n  },\n  \"lightbulb\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Lightbulb\",\n    \"terms\": [\n      \"Lightbulb\",\n      \"lightbulb\"\n    ]\n  },\n  \"line\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Line\",\n    \"terms\": [\n      \"Line\",\n      \"line\"\n    ]\n  },\n  \"lines-leaning\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Lines Leaning\",\n    \"terms\": [\n      \"Lines Leaning\",\n      \"lines-leaning\"\n    ]\n  },\n  \"link\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Link\",\n    \"terms\": [\n      \"chain\",\n      \"Link\",\n      \"link\"\n    ]\n  },\n  \"link-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Link Slash\",\n    \"terms\": [\n      \"chain-broken\",\n      \"chain-slash\",\n      \"unlink\",\n      \"Link Slash\",\n      \"link-slash\"\n    ]\n  },\n  \"linkedin\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"LinkedIn\",\n    \"terms\": [\n      \"LinkedIn\",\n      \"linkedin\"\n    ]\n  },\n  \"linkedin-in\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"LinkedIn In\",\n    \"terms\": [\n      \"LinkedIn In\",\n      \"linkedin-in\"\n    ]\n  },\n  \"linode\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Linode\",\n    \"terms\": [\n      \"Linode\",\n      \"linode\"\n    ]\n  },\n  \"linux\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Linux\",\n    \"terms\": [\n      \"Linux\",\n      \"linux\"\n    ]\n  },\n  \"lira-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Turkish Lira Sign\",\n    \"terms\": [\n      \"Turkish Lira Sign\",\n      \"lira-sign\"\n    ]\n  },\n  \"list\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"List\",\n    \"terms\": [\n      \"list-squares\",\n      \"List\",\n      \"list\"\n    ]\n  },\n  \"list-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"List check\",\n    \"terms\": [\n      \"tasks\",\n      \"List check\",\n      \"list-check\"\n    ]\n  },\n  \"list-ol\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"list-ol\",\n    \"terms\": [\n      \"list-1-2\",\n      \"list-numeric\",\n      \"list-ol\",\n      \"list-ol\"\n    ]\n  },\n  \"list-ul\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"list-ul\",\n    \"terms\": [\n      \"list-dots\",\n      \"list-ul\",\n      \"list-ul\"\n    ]\n  },\n  \"litecoin-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Litecoin Sign\",\n    \"terms\": [\n      \"Litecoin Sign\",\n      \"litecoin-sign\"\n    ]\n  },\n  \"location-arrow\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"location-arrow\",\n    \"terms\": [\n      \"location-arrow\",\n      \"location-arrow\"\n    ]\n  },\n  \"location-crosshairs\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Location Crosshairs\",\n    \"terms\": [\n      \"location\",\n      \"Location Crosshairs\",\n      \"location-crosshairs\"\n    ]\n  },\n  \"location-dot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Location dot\",\n    \"terms\": [\n      \"map-marker-alt\",\n      \"Location dot\",\n      \"location-dot\"\n    ]\n  },\n  \"location-pin\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Location\",\n    \"terms\": [\n      \"map-marker\",\n      \"Location\",\n      \"location-pin\"\n    ]\n  },\n  \"location-pin-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Location Pin-lock\",\n    \"terms\": [\n      \"Location Pin-lock\",\n      \"location-pin-lock\"\n    ]\n  },\n  \"lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"lock\",\n    \"terms\": [\n      \"lock\",\n      \"lock\"\n    ]\n  },\n  \"lock-open\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Lock Open\",\n    \"terms\": [\n      \"Lock Open\",\n      \"lock-open\"\n    ]\n  },\n  \"locust\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Locust\",\n    \"terms\": [\n      \"Locust\",\n      \"locust\"\n    ]\n  },\n  \"lungs\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Lungs\",\n    \"terms\": [\n      \"Lungs\",\n      \"lungs\"\n    ]\n  },\n  \"lungs-virus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Lungs Virus\",\n    \"terms\": [\n      \"Lungs Virus\",\n      \"lungs-virus\"\n    ]\n  },\n  \"lyft\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"lyft\",\n    \"terms\": [\n      \"lyft\",\n      \"lyft\"\n    ]\n  },\n  \"m\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"M\",\n    \"terms\": [\n      \"M\",\n      \"m\"\n    ]\n  },\n  \"magento\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Magento\",\n    \"terms\": [\n      \"Magento\",\n      \"magento\"\n    ]\n  },\n  \"magnet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"magnet\",\n    \"terms\": [\n      \"magnet\",\n      \"magnet\"\n    ]\n  },\n  \"magnifying-glass\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Magnifying glass\",\n    \"terms\": [\n      \"search\",\n      \"Magnifying glass\",\n      \"magnifying-glass\"\n    ]\n  },\n  \"magnifying-glass-arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Magnifying Glass-arrow-right\",\n    \"terms\": [\n      \"Magnifying Glass-arrow-right\",\n      \"magnifying-glass-arrow-right\"\n    ]\n  },\n  \"magnifying-glass-chart\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Magnifying Glass-chart\",\n    \"terms\": [\n      \"Magnifying Glass-chart\",\n      \"magnifying-glass-chart\"\n    ]\n  },\n  \"magnifying-glass-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Magnifying glass dollar\",\n    \"terms\": [\n      \"search-dollar\",\n      \"Magnifying glass dollar\",\n      \"magnifying-glass-dollar\"\n    ]\n  },\n  \"magnifying-glass-location\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Magnifying glass location\",\n    \"terms\": [\n      \"search-location\",\n      \"Magnifying glass location\",\n      \"magnifying-glass-location\"\n    ]\n  },\n  \"magnifying-glass-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Magnifying glass minus\",\n    \"terms\": [\n      \"search-minus\",\n      \"Magnifying glass minus\",\n      \"magnifying-glass-minus\"\n    ]\n  },\n  \"magnifying-glass-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Magnifying glass plus\",\n    \"terms\": [\n      \"search-plus\",\n      \"Magnifying glass plus\",\n      \"magnifying-glass-plus\"\n    ]\n  },\n  \"mailchimp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mailchimp\",\n    \"terms\": [\n      \"Mailchimp\",\n      \"mailchimp\"\n    ]\n  },\n  \"manat-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Manat Sign\",\n    \"terms\": [\n      \"Manat Sign\",\n      \"manat-sign\"\n    ]\n  },\n  \"mandalorian\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mandalorian\",\n    \"terms\": [\n      \"Mandalorian\",\n      \"mandalorian\"\n    ]\n  },\n  \"map\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Map\",\n    \"terms\": [\n      \"Map\",\n      \"map\"\n    ]\n  },\n  \"map-location\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Map location\",\n    \"terms\": [\n      \"map-marked\",\n      \"Map location\",\n      \"map-location\"\n    ]\n  },\n  \"map-location-dot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Map location dot\",\n    \"terms\": [\n      \"map-marked-alt\",\n      \"Map location dot\",\n      \"map-location-dot\"\n    ]\n  },\n  \"map-pin\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Map Pin\",\n    \"terms\": [\n      \"Map Pin\",\n      \"map-pin\"\n    ]\n  },\n  \"markdown\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Markdown\",\n    \"terms\": [\n      \"Markdown\",\n      \"markdown\"\n    ]\n  },\n  \"marker\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Marker\",\n    \"terms\": [\n      \"Marker\",\n      \"marker\"\n    ]\n  },\n  \"mars\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mars\",\n    \"terms\": [\n      \"Mars\",\n      \"mars\"\n    ]\n  },\n  \"mars-and-venus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mars and Venus\",\n    \"terms\": [\n      \"Mars and Venus\",\n      \"mars-and-venus\"\n    ]\n  },\n  \"mars-and-venus-burst\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mars and Venus Burst\",\n    \"terms\": [\n      \"Mars and Venus Burst\",\n      \"mars-and-venus-burst\"\n    ]\n  },\n  \"mars-double\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mars Double\",\n    \"terms\": [\n      \"Mars Double\",\n      \"mars-double\"\n    ]\n  },\n  \"mars-stroke\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mars Stroke\",\n    \"terms\": [\n      \"Mars Stroke\",\n      \"mars-stroke\"\n    ]\n  },\n  \"mars-stroke-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mars stroke right\",\n    \"terms\": [\n      \"mars-stroke-h\",\n      \"Mars stroke right\",\n      \"mars-stroke-right\"\n    ]\n  },\n  \"mars-stroke-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mars stroke up\",\n    \"terms\": [\n      \"mars-stroke-v\",\n      \"Mars stroke up\",\n      \"mars-stroke-up\"\n    ]\n  },\n  \"martini-glass\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Martini glass\",\n    \"terms\": [\n      \"glass-martini-alt\",\n      \"Martini glass\",\n      \"martini-glass\"\n    ]\n  },\n  \"martini-glass-citrus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Martini glass citrus\",\n    \"terms\": [\n      \"cocktail\",\n      \"Martini glass citrus\",\n      \"martini-glass-citrus\"\n    ]\n  },\n  \"martini-glass-empty\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Martini glass empty\",\n    \"terms\": [\n      \"glass-martini\",\n      \"Martini glass empty\",\n      \"martini-glass-empty\"\n    ]\n  },\n  \"mask\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mask\",\n    \"terms\": [\n      \"Mask\",\n      \"mask\"\n    ]\n  },\n  \"mask-face\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Face Mask\",\n    \"terms\": [\n      \"Face Mask\",\n      \"mask-face\"\n    ]\n  },\n  \"mask-ventilator\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mask Ventilator\",\n    \"terms\": [\n      \"Mask Ventilator\",\n      \"mask-ventilator\"\n    ]\n  },\n  \"masks-theater\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Masks theater\",\n    \"terms\": [\n      \"theater-masks\",\n      \"Masks theater\",\n      \"masks-theater\"\n    ]\n  },\n  \"mastodon\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mastodon\",\n    \"terms\": [\n      \"Mastodon\",\n      \"mastodon\"\n    ]\n  },\n  \"mattress-pillow\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mattress Pillow\",\n    \"terms\": [\n      \"Mattress Pillow\",\n      \"mattress-pillow\"\n    ]\n  },\n  \"maxcdn\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"MaxCDN\",\n    \"terms\": [\n      \"MaxCDN\",\n      \"maxcdn\"\n    ]\n  },\n  \"maximize\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Maximize\",\n    \"terms\": [\n      \"expand-arrows-alt\",\n      \"Maximize\",\n      \"maximize\"\n    ]\n  },\n  \"mdb\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Material Design for Bootstrap\",\n    \"terms\": [\n      \"Material Design for Bootstrap\",\n      \"mdb\"\n    ]\n  },\n  \"medal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Medal\",\n    \"terms\": [\n      \"Medal\",\n      \"medal\"\n    ]\n  },\n  \"medapps\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"MedApps\",\n    \"terms\": [\n      \"MedApps\",\n      \"medapps\"\n    ]\n  },\n  \"medium\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Medium\",\n    \"terms\": [\n      \"medium-m\",\n      \"Medium\",\n      \"medium\"\n    ]\n  },\n  \"medrt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"MRT\",\n    \"terms\": [\n      \"MRT\",\n      \"medrt\"\n    ]\n  },\n  \"meetup\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Meetup\",\n    \"terms\": [\n      \"Meetup\",\n      \"meetup\"\n    ]\n  },\n  \"megaport\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Megaport\",\n    \"terms\": [\n      \"Megaport\",\n      \"megaport\"\n    ]\n  },\n  \"memory\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Memory\",\n    \"terms\": [\n      \"Memory\",\n      \"memory\"\n    ]\n  },\n  \"mendeley\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mendeley\",\n    \"terms\": [\n      \"Mendeley\",\n      \"mendeley\"\n    ]\n  },\n  \"menorah\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Menorah\",\n    \"terms\": [\n      \"Menorah\",\n      \"menorah\"\n    ]\n  },\n  \"mercury\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mercury\",\n    \"terms\": [\n      \"Mercury\",\n      \"mercury\"\n    ]\n  },\n  \"message\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Message\",\n    \"terms\": [\n      \"comment-alt\",\n      \"Message\",\n      \"message\"\n    ]\n  },\n  \"meta\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Meta\",\n    \"terms\": [\n      \"Meta\",\n      \"meta\"\n    ]\n  },\n  \"meteor\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Meteor\",\n    \"terms\": [\n      \"Meteor\",\n      \"meteor\"\n    ]\n  },\n  \"microblog\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Micro.blog\",\n    \"terms\": [\n      \"Micro.blog\",\n      \"microblog\"\n    ]\n  },\n  \"microchip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Microchip\",\n    \"terms\": [\n      \"Microchip\",\n      \"microchip\"\n    ]\n  },\n  \"microphone\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"microphone\",\n    \"terms\": [\n      \"microphone\",\n      \"microphone\"\n    ]\n  },\n  \"microphone-lines\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Microphone lines\",\n    \"terms\": [\n      \"microphone-alt\",\n      \"Microphone lines\",\n      \"microphone-lines\"\n    ]\n  },\n  \"microphone-lines-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Microphone lines slash\",\n    \"terms\": [\n      \"microphone-alt-slash\",\n      \"Microphone lines slash\",\n      \"microphone-lines-slash\"\n    ]\n  },\n  \"microphone-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Microphone Slash\",\n    \"terms\": [\n      \"Microphone Slash\",\n      \"microphone-slash\"\n    ]\n  },\n  \"microscope\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Microscope\",\n    \"terms\": [\n      \"Microscope\",\n      \"microscope\"\n    ]\n  },\n  \"microsoft\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Microsoft\",\n    \"terms\": [\n      \"Microsoft\",\n      \"microsoft\"\n    ]\n  },\n  \"mill-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mill Sign\",\n    \"terms\": [\n      \"Mill Sign\",\n      \"mill-sign\"\n    ]\n  },\n  \"minimize\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Minimize\",\n    \"terms\": [\n      \"compress-arrows-alt\",\n      \"Minimize\",\n      \"minimize\"\n    ]\n  },\n  \"minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"minus\",\n    \"terms\": [\n      \"subtract\",\n      \"minus\",\n      \"minus\"\n    ]\n  },\n  \"mitten\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mitten\",\n    \"terms\": [\n      \"Mitten\",\n      \"mitten\"\n    ]\n  },\n  \"mix\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mix\",\n    \"terms\": [\n      \"Mix\",\n      \"mix\"\n    ]\n  },\n  \"mixcloud\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mixcloud\",\n    \"terms\": [\n      \"Mixcloud\",\n      \"mixcloud\"\n    ]\n  },\n  \"mixer\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mixer\",\n    \"terms\": [\n      \"Mixer\",\n      \"mixer\"\n    ]\n  },\n  \"mizuni\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Mizuni\",\n    \"terms\": [\n      \"Mizuni\",\n      \"mizuni\"\n    ]\n  },\n  \"mobile\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mobile\",\n    \"terms\": [\n      \"mobile-android\",\n      \"mobile-phone\",\n      \"Mobile\",\n      \"mobile\"\n    ]\n  },\n  \"mobile-button\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mobile button\",\n    \"terms\": [\n      \"Mobile button\",\n      \"mobile-button\"\n    ]\n  },\n  \"mobile-retro\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mobile Retro\",\n    \"terms\": [\n      \"Mobile Retro\",\n      \"mobile-retro\"\n    ]\n  },\n  \"mobile-screen\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mobile screen\",\n    \"terms\": [\n      \"mobile-android-alt\",\n      \"Mobile screen\",\n      \"mobile-screen\"\n    ]\n  },\n  \"mobile-screen-button\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mobile screen button\",\n    \"terms\": [\n      \"mobile-alt\",\n      \"Mobile screen button\",\n      \"mobile-screen-button\"\n    ]\n  },\n  \"modx\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"MODX\",\n    \"terms\": [\n      \"MODX\",\n      \"modx\"\n    ]\n  },\n  \"monero\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Monero\",\n    \"terms\": [\n      \"Monero\",\n      \"monero\"\n    ]\n  },\n  \"money-bill\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money Bill\",\n    \"terms\": [\n      \"Money Bill\",\n      \"money-bill\"\n    ]\n  },\n  \"money-bill-1\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Money bill 1\",\n    \"terms\": [\n      \"money-bill-alt\",\n      \"Money bill 1\",\n      \"money-bill-1\"\n    ]\n  },\n  \"money-bill-1-wave\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money bill 1 wave\",\n    \"terms\": [\n      \"money-bill-wave-alt\",\n      \"Money bill 1 wave\",\n      \"money-bill-1-wave\"\n    ]\n  },\n  \"money-bill-transfer\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money Bill-transfer\",\n    \"terms\": [\n      \"Money Bill-transfer\",\n      \"money-bill-transfer\"\n    ]\n  },\n  \"money-bill-trend-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money Bill-trend-up\",\n    \"terms\": [\n      \"Money Bill-trend-up\",\n      \"money-bill-trend-up\"\n    ]\n  },\n  \"money-bill-wave\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wavy Money Bill\",\n    \"terms\": [\n      \"Wavy Money Bill\",\n      \"money-bill-wave\"\n    ]\n  },\n  \"money-bill-wheat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money Bill-wheat\",\n    \"terms\": [\n      \"Money Bill-wheat\",\n      \"money-bill-wheat\"\n    ]\n  },\n  \"money-bills\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money Bills\",\n    \"terms\": [\n      \"Money Bills\",\n      \"money-bills\"\n    ]\n  },\n  \"money-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money Check\",\n    \"terms\": [\n      \"Money Check\",\n      \"money-check\"\n    ]\n  },\n  \"money-check-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Money check dollar\",\n    \"terms\": [\n      \"money-check-alt\",\n      \"Money check dollar\",\n      \"money-check-dollar\"\n    ]\n  },\n  \"monument\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Monument\",\n    \"terms\": [\n      \"Monument\",\n      \"monument\"\n    ]\n  },\n  \"moon\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Moon\",\n    \"terms\": [\n      \"Moon\",\n      \"moon\"\n    ]\n  },\n  \"mortar-pestle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mortar Pestle\",\n    \"terms\": [\n      \"Mortar Pestle\",\n      \"mortar-pestle\"\n    ]\n  },\n  \"mosque\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mosque\",\n    \"terms\": [\n      \"Mosque\",\n      \"mosque\"\n    ]\n  },\n  \"mosquito\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mosquito\",\n    \"terms\": [\n      \"Mosquito\",\n      \"mosquito\"\n    ]\n  },\n  \"mosquito-net\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mosquito Net\",\n    \"terms\": [\n      \"Mosquito Net\",\n      \"mosquito-net\"\n    ]\n  },\n  \"motorcycle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Motorcycle\",\n    \"terms\": [\n      \"Motorcycle\",\n      \"motorcycle\"\n    ]\n  },\n  \"mound\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mound\",\n    \"terms\": [\n      \"Mound\",\n      \"mound\"\n    ]\n  },\n  \"mountain\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mountain\",\n    \"terms\": [\n      \"Mountain\",\n      \"mountain\"\n    ]\n  },\n  \"mountain-city\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mountain City\",\n    \"terms\": [\n      \"Mountain City\",\n      \"mountain-city\"\n    ]\n  },\n  \"mountain-sun\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mountain Sun\",\n    \"terms\": [\n      \"Mountain Sun\",\n      \"mountain-sun\"\n    ]\n  },\n  \"mug-hot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mug Hot\",\n    \"terms\": [\n      \"Mug Hot\",\n      \"mug-hot\"\n    ]\n  },\n  \"mug-saucer\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Mug saucer\",\n    \"terms\": [\n      \"coffee\",\n      \"Mug saucer\",\n      \"mug-saucer\"\n    ]\n  },\n  \"music\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Music\",\n    \"terms\": [\n      \"Music\",\n      \"music\"\n    ]\n  },\n  \"n\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"N\",\n    \"terms\": [\n      \"N\",\n      \"n\"\n    ]\n  },\n  \"naira-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Naira Sign\",\n    \"terms\": [\n      \"Naira Sign\",\n      \"naira-sign\"\n    ]\n  },\n  \"napster\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Napster\",\n    \"terms\": [\n      \"Napster\",\n      \"napster\"\n    ]\n  },\n  \"neos\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Neos\",\n    \"terms\": [\n      \"Neos\",\n      \"neos\"\n    ]\n  },\n  \"network-wired\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wired Network\",\n    \"terms\": [\n      \"Wired Network\",\n      \"network-wired\"\n    ]\n  },\n  \"neuter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Neuter\",\n    \"terms\": [\n      \"Neuter\",\n      \"neuter\"\n    ]\n  },\n  \"newspaper\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Newspaper\",\n    \"terms\": [\n      \"Newspaper\",\n      \"newspaper\"\n    ]\n  },\n  \"nfc-directional\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"NFC Directional\",\n    \"terms\": [\n      \"NFC Directional\",\n      \"nfc-directional\"\n    ]\n  },\n  \"nfc-symbol\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"NFC Simplified\",\n    \"terms\": [\n      \"NFC Simplified\",\n      \"nfc-symbol\"\n    ]\n  },\n  \"nimblr\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Nimblr\",\n    \"terms\": [\n      \"Nimblr\",\n      \"nimblr\"\n    ]\n  },\n  \"node\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Node.js\",\n    \"terms\": [\n      \"Node.js\",\n      \"node\"\n    ]\n  },\n  \"node-js\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Node.js JS\",\n    \"terms\": [\n      \"Node.js JS\",\n      \"node-js\"\n    ]\n  },\n  \"not-equal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Not Equal\",\n    \"terms\": [\n      \"Not Equal\",\n      \"not-equal\"\n    ]\n  },\n  \"notdef\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Notdef\",\n    \"terms\": [\n      \"Notdef\",\n      \"notdef\"\n    ]\n  },\n  \"note-sticky\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Note sticky\",\n    \"terms\": [\n      \"sticky-note\",\n      \"Note sticky\",\n      \"note-sticky\"\n    ]\n  },\n  \"notes-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Medical Notes\",\n    \"terms\": [\n      \"Medical Notes\",\n      \"notes-medical\"\n    ]\n  },\n  \"npm\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"npm\",\n    \"terms\": [\n      \"npm\",\n      \"npm\"\n    ]\n  },\n  \"ns8\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"NS8\",\n    \"terms\": [\n      \"NS8\",\n      \"ns8\"\n    ]\n  },\n  \"nutritionix\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Nutritionix\",\n    \"terms\": [\n      \"Nutritionix\",\n      \"nutritionix\"\n    ]\n  },\n  \"o\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"O\",\n    \"terms\": [\n      \"O\",\n      \"o\"\n    ]\n  },\n  \"object-group\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Object Group\",\n    \"terms\": [\n      \"Object Group\",\n      \"object-group\"\n    ]\n  },\n  \"object-ungroup\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Object Ungroup\",\n    \"terms\": [\n      \"Object Ungroup\",\n      \"object-ungroup\"\n    ]\n  },\n  \"octopus-deploy\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Octopus Deploy\",\n    \"terms\": [\n      \"Octopus Deploy\",\n      \"octopus-deploy\"\n    ]\n  },\n  \"odnoklassniki\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Odnoklassniki\",\n    \"terms\": [\n      \"Odnoklassniki\",\n      \"odnoklassniki\"\n    ]\n  },\n  \"oil-can\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Oil Can\",\n    \"terms\": [\n      \"Oil Can\",\n      \"oil-can\"\n    ]\n  },\n  \"oil-well\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Oil Well\",\n    \"terms\": [\n      \"Oil Well\",\n      \"oil-well\"\n    ]\n  },\n  \"old-republic\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Old Republic\",\n    \"terms\": [\n      \"Old Republic\",\n      \"old-republic\"\n    ]\n  },\n  \"om\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Om\",\n    \"terms\": [\n      \"Om\",\n      \"om\"\n    ]\n  },\n  \"opencart\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"OpenCart\",\n    \"terms\": [\n      \"OpenCart\",\n      \"opencart\"\n    ]\n  },\n  \"openid\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"OpenID\",\n    \"terms\": [\n      \"OpenID\",\n      \"openid\"\n    ]\n  },\n  \"opera\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Opera\",\n    \"terms\": [\n      \"Opera\",\n      \"opera\"\n    ]\n  },\n  \"optin-monster\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Optin Monster\",\n    \"terms\": [\n      \"Optin Monster\",\n      \"optin-monster\"\n    ]\n  },\n  \"orcid\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"ORCID\",\n    \"terms\": [\n      \"ORCID\",\n      \"orcid\"\n    ]\n  },\n  \"osi\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Open Source Initiative\",\n    \"terms\": [\n      \"Open Source Initiative\",\n      \"osi\"\n    ]\n  },\n  \"otter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Otter\",\n    \"terms\": [\n      \"Otter\",\n      \"otter\"\n    ]\n  },\n  \"outdent\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Outdent\",\n    \"terms\": [\n      \"dedent\",\n      \"Outdent\",\n      \"outdent\"\n    ]\n  },\n  \"p\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"P\",\n    \"terms\": [\n      \"P\",\n      \"p\"\n    ]\n  },\n  \"padlet\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Padlet\",\n    \"terms\": [\n      \"Padlet\",\n      \"padlet\"\n    ]\n  },\n  \"page4\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"page4 Corporation\",\n    \"terms\": [\n      \"page4 Corporation\",\n      \"page4\"\n    ]\n  },\n  \"pagelines\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pagelines\",\n    \"terms\": [\n      \"Pagelines\",\n      \"pagelines\"\n    ]\n  },\n  \"pager\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pager\",\n    \"terms\": [\n      \"Pager\",\n      \"pager\"\n    ]\n  },\n  \"paint-roller\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Paint Roller\",\n    \"terms\": [\n      \"Paint Roller\",\n      \"paint-roller\"\n    ]\n  },\n  \"paintbrush\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Paint Brush\",\n    \"terms\": [\n      \"paint-brush\",\n      \"Paint Brush\",\n      \"paintbrush\"\n    ]\n  },\n  \"palette\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Palette\",\n    \"terms\": [\n      \"Palette\",\n      \"palette\"\n    ]\n  },\n  \"palfed\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Palfed\",\n    \"terms\": [\n      \"Palfed\",\n      \"palfed\"\n    ]\n  },\n  \"pallet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pallet\",\n    \"terms\": [\n      \"Pallet\",\n      \"pallet\"\n    ]\n  },\n  \"panorama\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Panorama\",\n    \"terms\": [\n      \"Panorama\",\n      \"panorama\"\n    ]\n  },\n  \"paper-plane\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Paper Plane\",\n    \"terms\": [\n      \"Paper Plane\",\n      \"paper-plane\"\n    ]\n  },\n  \"paperclip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Paperclip\",\n    \"terms\": [\n      \"Paperclip\",\n      \"paperclip\"\n    ]\n  },\n  \"parachute-box\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Parachute Box\",\n    \"terms\": [\n      \"Parachute Box\",\n      \"parachute-box\"\n    ]\n  },\n  \"paragraph\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"paragraph\",\n    \"terms\": [\n      \"paragraph\",\n      \"paragraph\"\n    ]\n  },\n  \"passport\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Passport\",\n    \"terms\": [\n      \"Passport\",\n      \"passport\"\n    ]\n  },\n  \"paste\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Paste\",\n    \"terms\": [\n      \"file-clipboard\",\n      \"Paste\",\n      \"paste\"\n    ]\n  },\n  \"patreon\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Patreon\",\n    \"terms\": [\n      \"Patreon\",\n      \"patreon\"\n    ]\n  },\n  \"pause\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"pause\",\n    \"terms\": [\n      \"pause\",\n      \"pause\"\n    ]\n  },\n  \"paw\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Paw\",\n    \"terms\": [\n      \"Paw\",\n      \"paw\"\n    ]\n  },\n  \"paypal\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Paypal\",\n    \"terms\": [\n      \"Paypal\",\n      \"paypal\"\n    ]\n  },\n  \"peace\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Peace\",\n    \"terms\": [\n      \"Peace\",\n      \"peace\"\n    ]\n  },\n  \"pen\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pen\",\n    \"terms\": [\n      \"Pen\",\n      \"pen\"\n    ]\n  },\n  \"pen-clip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pen clip\",\n    \"terms\": [\n      \"pen-alt\",\n      \"Pen clip\",\n      \"pen-clip\"\n    ]\n  },\n  \"pen-fancy\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pen Fancy\",\n    \"terms\": [\n      \"Pen Fancy\",\n      \"pen-fancy\"\n    ]\n  },\n  \"pen-nib\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pen Nib\",\n    \"terms\": [\n      \"Pen Nib\",\n      \"pen-nib\"\n    ]\n  },\n  \"pen-ruler\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pen ruler\",\n    \"terms\": [\n      \"pencil-ruler\",\n      \"Pen ruler\",\n      \"pen-ruler\"\n    ]\n  },\n  \"pen-to-square\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Pen to square\",\n    \"terms\": [\n      \"edit\",\n      \"Pen to square\",\n      \"pen-to-square\"\n    ]\n  },\n  \"pencil\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"pencil\",\n    \"terms\": [\n      \"pencil-alt\",\n      \"pencil\",\n      \"pencil\"\n    ]\n  },\n  \"people-arrows\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"People arrows left right\",\n    \"terms\": [\n      \"people-arrows-left-right\",\n      \"People arrows left right\",\n      \"people-arrows\"\n    ]\n  },\n  \"people-carry-box\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"People carry box\",\n    \"terms\": [\n      \"people-carry\",\n      \"People carry box\",\n      \"people-carry-box\"\n    ]\n  },\n  \"people-group\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"People Group\",\n    \"terms\": [\n      \"People Group\",\n      \"people-group\"\n    ]\n  },\n  \"people-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"People Line\",\n    \"terms\": [\n      \"People Line\",\n      \"people-line\"\n    ]\n  },\n  \"people-pulling\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"People Pulling\",\n    \"terms\": [\n      \"People Pulling\",\n      \"people-pulling\"\n    ]\n  },\n  \"people-robbery\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"People Robbery\",\n    \"terms\": [\n      \"People Robbery\",\n      \"people-robbery\"\n    ]\n  },\n  \"people-roof\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"People Roof\",\n    \"terms\": [\n      \"People Roof\",\n      \"people-roof\"\n    ]\n  },\n  \"pepper-hot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hot Pepper\",\n    \"terms\": [\n      \"Hot Pepper\",\n      \"pepper-hot\"\n    ]\n  },\n  \"perbyte\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"PerByte\",\n    \"terms\": [\n      \"PerByte\",\n      \"perbyte\"\n    ]\n  },\n  \"percent\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Percent\",\n    \"terms\": [\n      \"percentage\",\n      \"Percent\",\n      \"percent\"\n    ]\n  },\n  \"periscope\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Periscope\",\n    \"terms\": [\n      \"Periscope\",\n      \"periscope\"\n    ]\n  },\n  \"person\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person\",\n    \"terms\": [\n      \"male\",\n      \"Person\",\n      \"person\"\n    ]\n  },\n  \"person-arrow-down-to-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Arrow-down-to-line\",\n    \"terms\": [\n      \"Person Arrow-down-to-line\",\n      \"person-arrow-down-to-line\"\n    ]\n  },\n  \"person-arrow-up-from-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Arrow-up-from-line\",\n    \"terms\": [\n      \"Person Arrow-up-from-line\",\n      \"person-arrow-up-from-line\"\n    ]\n  },\n  \"person-biking\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person biking\",\n    \"terms\": [\n      \"biking\",\n      \"Person biking\",\n      \"person-biking\"\n    ]\n  },\n  \"person-booth\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Entering Booth\",\n    \"terms\": [\n      \"Person Entering Booth\",\n      \"person-booth\"\n    ]\n  },\n  \"person-breastfeeding\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Breastfeeding\",\n    \"terms\": [\n      \"Person Breastfeeding\",\n      \"person-breastfeeding\"\n    ]\n  },\n  \"person-burst\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Burst\",\n    \"terms\": [\n      \"Person Burst\",\n      \"person-burst\"\n    ]\n  },\n  \"person-cane\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Cane\",\n    \"terms\": [\n      \"Person Cane\",\n      \"person-cane\"\n    ]\n  },\n  \"person-chalkboard\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Chalkboard\",\n    \"terms\": [\n      \"Person Chalkboard\",\n      \"person-chalkboard\"\n    ]\n  },\n  \"person-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Circle-check\",\n    \"terms\": [\n      \"Person Circle-check\",\n      \"person-circle-check\"\n    ]\n  },\n  \"person-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Circle-exclamation\",\n    \"terms\": [\n      \"Person Circle-exclamation\",\n      \"person-circle-exclamation\"\n    ]\n  },\n  \"person-circle-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Circle-minus\",\n    \"terms\": [\n      \"Person Circle-minus\",\n      \"person-circle-minus\"\n    ]\n  },\n  \"person-circle-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Circle-plus\",\n    \"terms\": [\n      \"Person Circle-plus\",\n      \"person-circle-plus\"\n    ]\n  },\n  \"person-circle-question\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Circle-question\",\n    \"terms\": [\n      \"Person Circle-question\",\n      \"person-circle-question\"\n    ]\n  },\n  \"person-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Circle-xmark\",\n    \"terms\": [\n      \"Person Circle-xmark\",\n      \"person-circle-xmark\"\n    ]\n  },\n  \"person-digging\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person digging\",\n    \"terms\": [\n      \"digging\",\n      \"Person digging\",\n      \"person-digging\"\n    ]\n  },\n  \"person-dots-from-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person dots from line\",\n    \"terms\": [\n      \"diagnoses\",\n      \"Person dots from line\",\n      \"person-dots-from-line\"\n    ]\n  },\n  \"person-dress\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person dress\",\n    \"terms\": [\n      \"female\",\n      \"Person dress\",\n      \"person-dress\"\n    ]\n  },\n  \"person-dress-burst\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Dress BUrst\",\n    \"terms\": [\n      \"Person Dress BUrst\",\n      \"person-dress-burst\"\n    ]\n  },\n  \"person-drowning\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Drowning\",\n    \"terms\": [\n      \"Person Drowning\",\n      \"person-drowning\"\n    ]\n  },\n  \"person-falling\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Falling\",\n    \"terms\": [\n      \"Person Falling\",\n      \"person-falling\"\n    ]\n  },\n  \"person-falling-burst\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Falling Burst\",\n    \"terms\": [\n      \"Person Falling Burst\",\n      \"person-falling-burst\"\n    ]\n  },\n  \"person-half-dress\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Half-dress\",\n    \"terms\": [\n      \"Person Half-dress\",\n      \"person-half-dress\"\n    ]\n  },\n  \"person-harassing\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Harassing\",\n    \"terms\": [\n      \"Person Harassing\",\n      \"person-harassing\"\n    ]\n  },\n  \"person-hiking\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person hiking\",\n    \"terms\": [\n      \"hiking\",\n      \"Person hiking\",\n      \"person-hiking\"\n    ]\n  },\n  \"person-military-pointing\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Military-pointing\",\n    \"terms\": [\n      \"Person Military-pointing\",\n      \"person-military-pointing\"\n    ]\n  },\n  \"person-military-rifle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Military-rifle\",\n    \"terms\": [\n      \"Person Military-rifle\",\n      \"person-military-rifle\"\n    ]\n  },\n  \"person-military-to-person\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Military-to-person\",\n    \"terms\": [\n      \"Person Military-to-person\",\n      \"person-military-to-person\"\n    ]\n  },\n  \"person-praying\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person praying\",\n    \"terms\": [\n      \"pray\",\n      \"Person praying\",\n      \"person-praying\"\n    ]\n  },\n  \"person-pregnant\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Pregnant\",\n    \"terms\": [\n      \"Person Pregnant\",\n      \"person-pregnant\"\n    ]\n  },\n  \"person-rays\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Rays\",\n    \"terms\": [\n      \"Person Rays\",\n      \"person-rays\"\n    ]\n  },\n  \"person-rifle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Rifle\",\n    \"terms\": [\n      \"Person Rifle\",\n      \"person-rifle\"\n    ]\n  },\n  \"person-running\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person running\",\n    \"terms\": [\n      \"running\",\n      \"Person running\",\n      \"person-running\"\n    ]\n  },\n  \"person-shelter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Shelter\",\n    \"terms\": [\n      \"Person Shelter\",\n      \"person-shelter\"\n    ]\n  },\n  \"person-skating\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person skating\",\n    \"terms\": [\n      \"skating\",\n      \"Person skating\",\n      \"person-skating\"\n    ]\n  },\n  \"person-skiing\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person skiing\",\n    \"terms\": [\n      \"skiing\",\n      \"Person skiing\",\n      \"person-skiing\"\n    ]\n  },\n  \"person-skiing-nordic\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person skiing nordic\",\n    \"terms\": [\n      \"skiing-nordic\",\n      \"Person skiing nordic\",\n      \"person-skiing-nordic\"\n    ]\n  },\n  \"person-snowboarding\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person snowboarding\",\n    \"terms\": [\n      \"snowboarding\",\n      \"Person snowboarding\",\n      \"person-snowboarding\"\n    ]\n  },\n  \"person-swimming\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person swimming\",\n    \"terms\": [\n      \"swimmer\",\n      \"Person swimming\",\n      \"person-swimming\"\n    ]\n  },\n  \"person-through-window\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Through-window\",\n    \"terms\": [\n      \"Person Through-window\",\n      \"person-through-window\"\n    ]\n  },\n  \"person-walking\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person walking\",\n    \"terms\": [\n      \"walking\",\n      \"Person walking\",\n      \"person-walking\"\n    ]\n  },\n  \"person-walking-arrow-loop-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Walking-arrow-loop-left\",\n    \"terms\": [\n      \"Person Walking-arrow-loop-left\",\n      \"person-walking-arrow-loop-left\"\n    ]\n  },\n  \"person-walking-arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Walking-arrow-right\",\n    \"terms\": [\n      \"Person Walking-arrow-right\",\n      \"person-walking-arrow-right\"\n    ]\n  },\n  \"person-walking-dashed-line-arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Walking-dashed-line-arrow-right\",\n    \"terms\": [\n      \"Person Walking-dashed-line-arrow-right\",\n      \"person-walking-dashed-line-arrow-right\"\n    ]\n  },\n  \"person-walking-luggage\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person Walking-luggage\",\n    \"terms\": [\n      \"Person Walking-luggage\",\n      \"person-walking-luggage\"\n    ]\n  },\n  \"person-walking-with-cane\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Person walking with cane\",\n    \"terms\": [\n      \"blind\",\n      \"Person walking with cane\",\n      \"person-walking-with-cane\"\n    ]\n  },\n  \"peseta-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Peseta Sign\",\n    \"terms\": [\n      \"Peseta Sign\",\n      \"peseta-sign\"\n    ]\n  },\n  \"peso-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Peso Sign\",\n    \"terms\": [\n      \"Peso Sign\",\n      \"peso-sign\"\n    ]\n  },\n  \"phabricator\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Phabricator\",\n    \"terms\": [\n      \"Phabricator\",\n      \"phabricator\"\n    ]\n  },\n  \"phoenix-framework\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Phoenix Framework\",\n    \"terms\": [\n      \"Phoenix Framework\",\n      \"phoenix-framework\"\n    ]\n  },\n  \"phoenix-squadron\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Phoenix Squadron\",\n    \"terms\": [\n      \"Phoenix Squadron\",\n      \"phoenix-squadron\"\n    ]\n  },\n  \"phone\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Phone\",\n    \"terms\": [\n      \"Phone\",\n      \"phone\"\n    ]\n  },\n  \"phone-flip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Phone flip\",\n    \"terms\": [\n      \"phone-alt\",\n      \"Phone flip\",\n      \"phone-flip\"\n    ]\n  },\n  \"phone-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Phone Slash\",\n    \"terms\": [\n      \"Phone Slash\",\n      \"phone-slash\"\n    ]\n  },\n  \"phone-volume\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Phone Volume\",\n    \"terms\": [\n      \"volume-control-phone\",\n      \"Phone Volume\",\n      \"phone-volume\"\n    ]\n  },\n  \"photo-film\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Photo film\",\n    \"terms\": [\n      \"photo-video\",\n      \"Photo film\",\n      \"photo-film\"\n    ]\n  },\n  \"php\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"PHP\",\n    \"terms\": [\n      \"PHP\",\n      \"php\"\n    ]\n  },\n  \"pied-piper\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pied Piper Logo\",\n    \"terms\": [\n      \"Pied Piper Logo\",\n      \"pied-piper\"\n    ]\n  },\n  \"pied-piper-alt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Alternate Pied Piper Logo (Old)\",\n    \"terms\": [\n      \"Alternate Pied Piper Logo (Old)\",\n      \"pied-piper-alt\"\n    ]\n  },\n  \"pied-piper-hat\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pied Piper Hat (Old)\",\n    \"terms\": [\n      \"Pied Piper Hat (Old)\",\n      \"pied-piper-hat\"\n    ]\n  },\n  \"pied-piper-pp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pied Piper PP Logo (Old)\",\n    \"terms\": [\n      \"Pied Piper PP Logo (Old)\",\n      \"pied-piper-pp\"\n    ]\n  },\n  \"piggy-bank\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Piggy Bank\",\n    \"terms\": [\n      \"Piggy Bank\",\n      \"piggy-bank\"\n    ]\n  },\n  \"pills\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pills\",\n    \"terms\": [\n      \"Pills\",\n      \"pills\"\n    ]\n  },\n  \"pinterest\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pinterest\",\n    \"terms\": [\n      \"Pinterest\",\n      \"pinterest\"\n    ]\n  },\n  \"pinterest-p\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pinterest P\",\n    \"terms\": [\n      \"Pinterest P\",\n      \"pinterest-p\"\n    ]\n  },\n  \"pix\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pix\",\n    \"terms\": [\n      \"Pix\",\n      \"pix\"\n    ]\n  },\n  \"pizza-slice\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pizza Slice\",\n    \"terms\": [\n      \"Pizza Slice\",\n      \"pizza-slice\"\n    ]\n  },\n  \"place-of-worship\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Place of Worship\",\n    \"terms\": [\n      \"Place of Worship\",\n      \"place-of-worship\"\n    ]\n  },\n  \"plane\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"plane\",\n    \"terms\": [\n      \"plane\",\n      \"plane\"\n    ]\n  },\n  \"plane-arrival\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Arrival\",\n    \"terms\": [\n      \"Plane Arrival\",\n      \"plane-arrival\"\n    ]\n  },\n  \"plane-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Circle-check\",\n    \"terms\": [\n      \"Plane Circle-check\",\n      \"plane-circle-check\"\n    ]\n  },\n  \"plane-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Circle-exclamation\",\n    \"terms\": [\n      \"Plane Circle-exclamation\",\n      \"plane-circle-exclamation\"\n    ]\n  },\n  \"plane-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Circle-xmark\",\n    \"terms\": [\n      \"Plane Circle-xmark\",\n      \"plane-circle-xmark\"\n    ]\n  },\n  \"plane-departure\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Departure\",\n    \"terms\": [\n      \"Plane Departure\",\n      \"plane-departure\"\n    ]\n  },\n  \"plane-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Lock\",\n    \"terms\": [\n      \"Plane Lock\",\n      \"plane-lock\"\n    ]\n  },\n  \"plane-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Slash\",\n    \"terms\": [\n      \"Plane Slash\",\n      \"plane-slash\"\n    ]\n  },\n  \"plane-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plane Up\",\n    \"terms\": [\n      \"Plane Up\",\n      \"plane-up\"\n    ]\n  },\n  \"plant-wilt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plant Wilt\",\n    \"terms\": [\n      \"Plant Wilt\",\n      \"plant-wilt\"\n    ]\n  },\n  \"plate-wheat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plate Wheat\",\n    \"terms\": [\n      \"Plate Wheat\",\n      \"plate-wheat\"\n    ]\n  },\n  \"play\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"play\",\n    \"terms\": [\n      \"play\",\n      \"play\"\n    ]\n  },\n  \"playstation\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"PlayStation\",\n    \"terms\": [\n      \"PlayStation\",\n      \"playstation\"\n    ]\n  },\n  \"plug\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plug\",\n    \"terms\": [\n      \"Plug\",\n      \"plug\"\n    ]\n  },\n  \"plug-circle-bolt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plug Circle-bolt\",\n    \"terms\": [\n      \"Plug Circle-bolt\",\n      \"plug-circle-bolt\"\n    ]\n  },\n  \"plug-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plug Circle-check\",\n    \"terms\": [\n      \"Plug Circle-check\",\n      \"plug-circle-check\"\n    ]\n  },\n  \"plug-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plug Circle-exclamation\",\n    \"terms\": [\n      \"Plug Circle-exclamation\",\n      \"plug-circle-exclamation\"\n    ]\n  },\n  \"plug-circle-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plug Circle-minus\",\n    \"terms\": [\n      \"Plug Circle-minus\",\n      \"plug-circle-minus\"\n    ]\n  },\n  \"plug-circle-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plug Circle-plus\",\n    \"terms\": [\n      \"Plug Circle-plus\",\n      \"plug-circle-plus\"\n    ]\n  },\n  \"plug-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plug Circle-xmark\",\n    \"terms\": [\n      \"Plug Circle-xmark\",\n      \"plug-circle-xmark\"\n    ]\n  },\n  \"plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"plus\",\n    \"terms\": [\n      \"add\",\n      \"plus\",\n      \"plus\"\n    ]\n  },\n  \"plus-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Plus Minus\",\n    \"terms\": [\n      \"Plus Minus\",\n      \"plus-minus\"\n    ]\n  },\n  \"podcast\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Podcast\",\n    \"terms\": [\n      \"Podcast\",\n      \"podcast\"\n    ]\n  },\n  \"poo\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Poo\",\n    \"terms\": [\n      \"Poo\",\n      \"poo\"\n    ]\n  },\n  \"poo-storm\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Poo bolt\",\n    \"terms\": [\n      \"poo-bolt\",\n      \"Poo bolt\",\n      \"poo-storm\"\n    ]\n  },\n  \"poop\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Poop\",\n    \"terms\": [\n      \"Poop\",\n      \"poop\"\n    ]\n  },\n  \"power-off\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Power Off\",\n    \"terms\": [\n      \"Power Off\",\n      \"power-off\"\n    ]\n  },\n  \"prescription\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Prescription\",\n    \"terms\": [\n      \"Prescription\",\n      \"prescription\"\n    ]\n  },\n  \"prescription-bottle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Prescription Bottle\",\n    \"terms\": [\n      \"Prescription Bottle\",\n      \"prescription-bottle\"\n    ]\n  },\n  \"prescription-bottle-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Prescription bottle medical\",\n    \"terms\": [\n      \"prescription-bottle-alt\",\n      \"Prescription bottle medical\",\n      \"prescription-bottle-medical\"\n    ]\n  },\n  \"print\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"print\",\n    \"terms\": [\n      \"print\",\n      \"print\"\n    ]\n  },\n  \"product-hunt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Product Hunt\",\n    \"terms\": [\n      \"Product Hunt\",\n      \"product-hunt\"\n    ]\n  },\n  \"pump-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pump Medical\",\n    \"terms\": [\n      \"Pump Medical\",\n      \"pump-medical\"\n    ]\n  },\n  \"pump-soap\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Pump Soap\",\n    \"terms\": [\n      \"Pump Soap\",\n      \"pump-soap\"\n    ]\n  },\n  \"pushed\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pushed\",\n    \"terms\": [\n      \"Pushed\",\n      \"pushed\"\n    ]\n  },\n  \"puzzle-piece\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Puzzle Piece\",\n    \"terms\": [\n      \"Puzzle Piece\",\n      \"puzzle-piece\"\n    ]\n  },\n  \"python\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Python\",\n    \"terms\": [\n      \"Python\",\n      \"python\"\n    ]\n  },\n  \"q\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Q\",\n    \"terms\": [\n      \"Q\",\n      \"q\"\n    ]\n  },\n  \"qq\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"QQ\",\n    \"terms\": [\n      \"QQ\",\n      \"qq\"\n    ]\n  },\n  \"qrcode\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"qrcode\",\n    \"terms\": [\n      \"qrcode\",\n      \"qrcode\"\n    ]\n  },\n  \"question\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Question\",\n    \"terms\": [\n      \"Question\",\n      \"question\"\n    ]\n  },\n  \"quinscape\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"QuinScape\",\n    \"terms\": [\n      \"QuinScape\",\n      \"quinscape\"\n    ]\n  },\n  \"quora\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Quora\",\n    \"terms\": [\n      \"Quora\",\n      \"quora\"\n    ]\n  },\n  \"quote-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"quote-left\",\n    \"terms\": [\n      \"quote-left-alt\",\n      \"quote-left\",\n      \"quote-left\"\n    ]\n  },\n  \"quote-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"quote-right\",\n    \"terms\": [\n      \"quote-right-alt\",\n      \"quote-right\",\n      \"quote-right\"\n    ]\n  },\n  \"r\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"R\",\n    \"terms\": [\n      \"R\",\n      \"r\"\n    ]\n  },\n  \"r-project\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"R Project\",\n    \"terms\": [\n      \"R Project\",\n      \"r-project\"\n    ]\n  },\n  \"radiation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Radiation\",\n    \"terms\": [\n      \"Radiation\",\n      \"radiation\"\n    ]\n  },\n  \"radio\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Radio\",\n    \"terms\": [\n      \"Radio\",\n      \"radio\"\n    ]\n  },\n  \"rainbow\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rainbow\",\n    \"terms\": [\n      \"Rainbow\",\n      \"rainbow\"\n    ]\n  },\n  \"ranking-star\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ranking Star\",\n    \"terms\": [\n      \"Ranking Star\",\n      \"ranking-star\"\n    ]\n  },\n  \"raspberry-pi\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Raspberry Pi\",\n    \"terms\": [\n      \"Raspberry Pi\",\n      \"raspberry-pi\"\n    ]\n  },\n  \"ravelry\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Ravelry\",\n    \"terms\": [\n      \"Ravelry\",\n      \"ravelry\"\n    ]\n  },\n  \"react\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"React\",\n    \"terms\": [\n      \"React\",\n      \"react\"\n    ]\n  },\n  \"reacteurope\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"ReactEurope\",\n    \"terms\": [\n      \"ReactEurope\",\n      \"reacteurope\"\n    ]\n  },\n  \"readme\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"ReadMe\",\n    \"terms\": [\n      \"ReadMe\",\n      \"readme\"\n    ]\n  },\n  \"rebel\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Rebel Alliance\",\n    \"terms\": [\n      \"Rebel Alliance\",\n      \"rebel\"\n    ]\n  },\n  \"receipt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Receipt\",\n    \"terms\": [\n      \"Receipt\",\n      \"receipt\"\n    ]\n  },\n  \"record-vinyl\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Record Vinyl\",\n    \"terms\": [\n      \"Record Vinyl\",\n      \"record-vinyl\"\n    ]\n  },\n  \"rectangle-ad\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rectangle ad\",\n    \"terms\": [\n      \"ad\",\n      \"Rectangle ad\",\n      \"rectangle-ad\"\n    ]\n  },\n  \"rectangle-list\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Rectangle list\",\n    \"terms\": [\n      \"list-alt\",\n      \"Rectangle list\",\n      \"rectangle-list\"\n    ]\n  },\n  \"rectangle-xmark\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Rectangle X Mark\",\n    \"terms\": [\n      \"rectangle-times\",\n      \"times-rectangle\",\n      \"window-close\",\n      \"Rectangle X Mark\",\n      \"rectangle-xmark\"\n    ]\n  },\n  \"recycle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Recycle\",\n    \"terms\": [\n      \"Recycle\",\n      \"recycle\"\n    ]\n  },\n  \"red-river\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"red river\",\n    \"terms\": [\n      \"red river\",\n      \"red-river\"\n    ]\n  },\n  \"reddit\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"reddit Logo\",\n    \"terms\": [\n      \"reddit Logo\",\n      \"reddit\"\n    ]\n  },\n  \"reddit-alien\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"reddit Alien\",\n    \"terms\": [\n      \"reddit Alien\",\n      \"reddit-alien\"\n    ]\n  },\n  \"redhat\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Redhat\",\n    \"terms\": [\n      \"Redhat\",\n      \"redhat\"\n    ]\n  },\n  \"registered\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Registered Trademark\",\n    \"terms\": [\n      \"Registered Trademark\",\n      \"registered\"\n    ]\n  },\n  \"renren\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Renren\",\n    \"terms\": [\n      \"Renren\",\n      \"renren\"\n    ]\n  },\n  \"repeat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Repeat\",\n    \"terms\": [\n      \"Repeat\",\n      \"repeat\"\n    ]\n  },\n  \"reply\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Reply\",\n    \"terms\": [\n      \"mail-reply\",\n      \"Reply\",\n      \"reply\"\n    ]\n  },\n  \"reply-all\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"reply-all\",\n    \"terms\": [\n      \"mail-reply-all\",\n      \"reply-all\",\n      \"reply-all\"\n    ]\n  },\n  \"replyd\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"replyd\",\n    \"terms\": [\n      \"replyd\",\n      \"replyd\"\n    ]\n  },\n  \"republican\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Republican\",\n    \"terms\": [\n      \"Republican\",\n      \"republican\"\n    ]\n  },\n  \"researchgate\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Researchgate\",\n    \"terms\": [\n      \"Researchgate\",\n      \"researchgate\"\n    ]\n  },\n  \"resolving\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Resolving\",\n    \"terms\": [\n      \"Resolving\",\n      \"resolving\"\n    ]\n  },\n  \"restroom\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Restroom\",\n    \"terms\": [\n      \"Restroom\",\n      \"restroom\"\n    ]\n  },\n  \"retweet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Retweet\",\n    \"terms\": [\n      \"Retweet\",\n      \"retweet\"\n    ]\n  },\n  \"rev\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Rev.io\",\n    \"terms\": [\n      \"Rev.io\",\n      \"rev\"\n    ]\n  },\n  \"ribbon\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ribbon\",\n    \"terms\": [\n      \"Ribbon\",\n      \"ribbon\"\n    ]\n  },\n  \"right-from-bracket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Right from bracket\",\n    \"terms\": [\n      \"sign-out-alt\",\n      \"Right from bracket\",\n      \"right-from-bracket\"\n    ]\n  },\n  \"right-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Right left\",\n    \"terms\": [\n      \"exchange-alt\",\n      \"Right left\",\n      \"right-left\"\n    ]\n  },\n  \"right-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Right long\",\n    \"terms\": [\n      \"long-arrow-alt-right\",\n      \"Right long\",\n      \"right-long\"\n    ]\n  },\n  \"right-to-bracket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Right to bracket\",\n    \"terms\": [\n      \"sign-in-alt\",\n      \"Right to bracket\",\n      \"right-to-bracket\"\n    ]\n  },\n  \"ring\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ring\",\n    \"terms\": [\n      \"Ring\",\n      \"ring\"\n    ]\n  },\n  \"road\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"road\",\n    \"terms\": [\n      \"road\",\n      \"road\"\n    ]\n  },\n  \"road-barrier\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Road Barrier\",\n    \"terms\": [\n      \"Road Barrier\",\n      \"road-barrier\"\n    ]\n  },\n  \"road-bridge\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Road Bridge\",\n    \"terms\": [\n      \"Road Bridge\",\n      \"road-bridge\"\n    ]\n  },\n  \"road-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Road Circle-check\",\n    \"terms\": [\n      \"Road Circle-check\",\n      \"road-circle-check\"\n    ]\n  },\n  \"road-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Road Circle-exclamation\",\n    \"terms\": [\n      \"Road Circle-exclamation\",\n      \"road-circle-exclamation\"\n    ]\n  },\n  \"road-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Road Circle-xmark\",\n    \"terms\": [\n      \"Road Circle-xmark\",\n      \"road-circle-xmark\"\n    ]\n  },\n  \"road-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Road Lock\",\n    \"terms\": [\n      \"Road Lock\",\n      \"road-lock\"\n    ]\n  },\n  \"road-spikes\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Road Spikes\",\n    \"terms\": [\n      \"Road Spikes\",\n      \"road-spikes\"\n    ]\n  },\n  \"robot\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Robot\",\n    \"terms\": [\n      \"Robot\",\n      \"robot\"\n    ]\n  },\n  \"rocket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"rocket\",\n    \"terms\": [\n      \"rocket\",\n      \"rocket\"\n    ]\n  },\n  \"rocketchat\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Rocket.Chat\",\n    \"terms\": [\n      \"Rocket.Chat\",\n      \"rocketchat\"\n    ]\n  },\n  \"rockrms\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Rockrms\",\n    \"terms\": [\n      \"Rockrms\",\n      \"rockrms\"\n    ]\n  },\n  \"rotate\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rotate\",\n    \"terms\": [\n      \"sync-alt\",\n      \"Rotate\",\n      \"rotate\"\n    ]\n  },\n  \"rotate-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rotate Left\",\n    \"terms\": [\n      \"rotate-back\",\n      \"rotate-backward\",\n      \"undo-alt\",\n      \"Rotate Left\",\n      \"rotate-left\"\n    ]\n  },\n  \"rotate-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rotate Right\",\n    \"terms\": [\n      \"redo-alt\",\n      \"rotate-forward\",\n      \"Rotate Right\",\n      \"rotate-right\"\n    ]\n  },\n  \"route\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Route\",\n    \"terms\": [\n      \"Route\",\n      \"route\"\n    ]\n  },\n  \"rss\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"rss\",\n    \"terms\": [\n      \"feed\",\n      \"rss\",\n      \"rss\"\n    ]\n  },\n  \"ruble-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ruble Sign\",\n    \"terms\": [\n      \"rouble\",\n      \"rub\",\n      \"ruble\",\n      \"Ruble Sign\",\n      \"ruble-sign\"\n    ]\n  },\n  \"rug\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rug\",\n    \"terms\": [\n      \"Rug\",\n      \"rug\"\n    ]\n  },\n  \"ruler\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ruler\",\n    \"terms\": [\n      \"Ruler\",\n      \"ruler\"\n    ]\n  },\n  \"ruler-combined\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ruler Combined\",\n    \"terms\": [\n      \"Ruler Combined\",\n      \"ruler-combined\"\n    ]\n  },\n  \"ruler-horizontal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ruler Horizontal\",\n    \"terms\": [\n      \"Ruler Horizontal\",\n      \"ruler-horizontal\"\n    ]\n  },\n  \"ruler-vertical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ruler Vertical\",\n    \"terms\": [\n      \"Ruler Vertical\",\n      \"ruler-vertical\"\n    ]\n  },\n  \"rupee-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Indian Rupee Sign\",\n    \"terms\": [\n      \"rupee\",\n      \"Indian Rupee Sign\",\n      \"rupee-sign\"\n    ]\n  },\n  \"rupiah-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Rupiah Sign\",\n    \"terms\": [\n      \"Rupiah Sign\",\n      \"rupiah-sign\"\n    ]\n  },\n  \"rust\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Rust\",\n    \"terms\": [\n      \"Rust\",\n      \"rust\"\n    ]\n  },\n  \"s\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"S\",\n    \"terms\": [\n      \"S\",\n      \"s\"\n    ]\n  },\n  \"sack-dollar\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sack of Money\",\n    \"terms\": [\n      \"Sack of Money\",\n      \"sack-dollar\"\n    ]\n  },\n  \"sack-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sack Xmark\",\n    \"terms\": [\n      \"Sack Xmark\",\n      \"sack-xmark\"\n    ]\n  },\n  \"safari\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Safari\",\n    \"terms\": [\n      \"Safari\",\n      \"safari\"\n    ]\n  },\n  \"sailboat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sailboat\",\n    \"terms\": [\n      \"Sailboat\",\n      \"sailboat\"\n    ]\n  },\n  \"salesforce\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Salesforce\",\n    \"terms\": [\n      \"Salesforce\",\n      \"salesforce\"\n    ]\n  },\n  \"sass\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sass\",\n    \"terms\": [\n      \"Sass\",\n      \"sass\"\n    ]\n  },\n  \"satellite\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Satellite\",\n    \"terms\": [\n      \"Satellite\",\n      \"satellite\"\n    ]\n  },\n  \"satellite-dish\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Satellite Dish\",\n    \"terms\": [\n      \"Satellite Dish\",\n      \"satellite-dish\"\n    ]\n  },\n  \"scale-balanced\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Scale balanced\",\n    \"terms\": [\n      \"balance-scale\",\n      \"Scale balanced\",\n      \"scale-balanced\"\n    ]\n  },\n  \"scale-unbalanced\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Scale unbalanced\",\n    \"terms\": [\n      \"balance-scale-left\",\n      \"Scale unbalanced\",\n      \"scale-unbalanced\"\n    ]\n  },\n  \"scale-unbalanced-flip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Scale unbalanced flip\",\n    \"terms\": [\n      \"balance-scale-right\",\n      \"Scale unbalanced flip\",\n      \"scale-unbalanced-flip\"\n    ]\n  },\n  \"schlix\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"SCHLIX\",\n    \"terms\": [\n      \"SCHLIX\",\n      \"schlix\"\n    ]\n  },\n  \"school\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"School\",\n    \"terms\": [\n      \"School\",\n      \"school\"\n    ]\n  },\n  \"school-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"School Circle-check\",\n    \"terms\": [\n      \"School Circle-check\",\n      \"school-circle-check\"\n    ]\n  },\n  \"school-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"School Circle-exclamation\",\n    \"terms\": [\n      \"School Circle-exclamation\",\n      \"school-circle-exclamation\"\n    ]\n  },\n  \"school-circle-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"School Circle-xmark\",\n    \"terms\": [\n      \"School Circle-xmark\",\n      \"school-circle-xmark\"\n    ]\n  },\n  \"school-flag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"School Flag\",\n    \"terms\": [\n      \"School Flag\",\n      \"school-flag\"\n    ]\n  },\n  \"school-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"School Lock\",\n    \"terms\": [\n      \"School Lock\",\n      \"school-lock\"\n    ]\n  },\n  \"scissors\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Scissors\",\n    \"terms\": [\n      \"cut\",\n      \"Scissors\",\n      \"scissors\"\n    ]\n  },\n  \"screenpal\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Screenpal\",\n    \"terms\": [\n      \"Screenpal\",\n      \"screenpal\"\n    ]\n  },\n  \"screwdriver\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Screwdriver\",\n    \"terms\": [\n      \"Screwdriver\",\n      \"screwdriver\"\n    ]\n  },\n  \"screwdriver-wrench\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Screwdriver wrench\",\n    \"terms\": [\n      \"tools\",\n      \"Screwdriver wrench\",\n      \"screwdriver-wrench\"\n    ]\n  },\n  \"scribd\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Scribd\",\n    \"terms\": [\n      \"Scribd\",\n      \"scribd\"\n    ]\n  },\n  \"scroll\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Scroll\",\n    \"terms\": [\n      \"Scroll\",\n      \"scroll\"\n    ]\n  },\n  \"scroll-torah\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Scroll torah\",\n    \"terms\": [\n      \"torah\",\n      \"Scroll torah\",\n      \"scroll-torah\"\n    ]\n  },\n  \"sd-card\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sd Card\",\n    \"terms\": [\n      \"Sd Card\",\n      \"sd-card\"\n    ]\n  },\n  \"searchengin\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Searchengin\",\n    \"terms\": [\n      \"Searchengin\",\n      \"searchengin\"\n    ]\n  },\n  \"section\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Section\",\n    \"terms\": [\n      \"Section\",\n      \"section\"\n    ]\n  },\n  \"seedling\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Seedling\",\n    \"terms\": [\n      \"sprout\",\n      \"Seedling\",\n      \"seedling\"\n    ]\n  },\n  \"sellcast\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sellcast\",\n    \"terms\": [\n      \"Sellcast\",\n      \"sellcast\"\n    ]\n  },\n  \"sellsy\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sellsy\",\n    \"terms\": [\n      \"Sellsy\",\n      \"sellsy\"\n    ]\n  },\n  \"server\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Server\",\n    \"terms\": [\n      \"Server\",\n      \"server\"\n    ]\n  },\n  \"servicestack\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Servicestack\",\n    \"terms\": [\n      \"Servicestack\",\n      \"servicestack\"\n    ]\n  },\n  \"shapes\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shapes\",\n    \"terms\": [\n      \"triangle-circle-square\",\n      \"Shapes\",\n      \"shapes\"\n    ]\n  },\n  \"share\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Share\",\n    \"terms\": [\n      \"arrow-turn-right\",\n      \"mail-forward\",\n      \"Share\",\n      \"share\"\n    ]\n  },\n  \"share-from-square\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Share from square\",\n    \"terms\": [\n      \"share-square\",\n      \"Share from square\",\n      \"share-from-square\"\n    ]\n  },\n  \"share-nodes\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Share nodes\",\n    \"terms\": [\n      \"share-alt\",\n      \"Share nodes\",\n      \"share-nodes\"\n    ]\n  },\n  \"sheet-plastic\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sheet Plastic\",\n    \"terms\": [\n      \"Sheet Plastic\",\n      \"sheet-plastic\"\n    ]\n  },\n  \"shekel-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shekel Sign\",\n    \"terms\": [\n      \"ils\",\n      \"shekel\",\n      \"sheqel\",\n      \"sheqel-sign\",\n      \"Shekel Sign\",\n      \"shekel-sign\"\n    ]\n  },\n  \"shield\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"shield\",\n    \"terms\": [\n      \"shield-blank\",\n      \"shield\",\n      \"shield\"\n    ]\n  },\n  \"shield-cat\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shield Cat\",\n    \"terms\": [\n      \"Shield Cat\",\n      \"shield-cat\"\n    ]\n  },\n  \"shield-dog\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shield Dog\",\n    \"terms\": [\n      \"Shield Dog\",\n      \"shield-dog\"\n    ]\n  },\n  \"shield-halved\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shield Halved\",\n    \"terms\": [\n      \"shield-alt\",\n      \"Shield Halved\",\n      \"shield-halved\"\n    ]\n  },\n  \"shield-heart\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shield Heart\",\n    \"terms\": [\n      \"Shield Heart\",\n      \"shield-heart\"\n    ]\n  },\n  \"shield-virus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shield Virus\",\n    \"terms\": [\n      \"Shield Virus\",\n      \"shield-virus\"\n    ]\n  },\n  \"ship\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ship\",\n    \"terms\": [\n      \"Ship\",\n      \"ship\"\n    ]\n  },\n  \"shirt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"T-Shirt\",\n    \"terms\": [\n      \"t-shirt\",\n      \"tshirt\",\n      \"T-Shirt\",\n      \"shirt\"\n    ]\n  },\n  \"shirtsinbulk\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Shirts in Bulk\",\n    \"terms\": [\n      \"Shirts in Bulk\",\n      \"shirtsinbulk\"\n    ]\n  },\n  \"shoe-prints\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shoe Prints\",\n    \"terms\": [\n      \"Shoe Prints\",\n      \"shoe-prints\"\n    ]\n  },\n  \"shop\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shop\",\n    \"terms\": [\n      \"store-alt\",\n      \"Shop\",\n      \"shop\"\n    ]\n  },\n  \"shop-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shop Lock\",\n    \"terms\": [\n      \"Shop Lock\",\n      \"shop-lock\"\n    ]\n  },\n  \"shop-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shop slash\",\n    \"terms\": [\n      \"store-alt-slash\",\n      \"Shop slash\",\n      \"shop-slash\"\n    ]\n  },\n  \"shopify\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Shopify\",\n    \"terms\": [\n      \"Shopify\",\n      \"shopify\"\n    ]\n  },\n  \"shopware\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Shopware\",\n    \"terms\": [\n      \"Shopware\",\n      \"shopware\"\n    ]\n  },\n  \"shower\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shower\",\n    \"terms\": [\n      \"Shower\",\n      \"shower\"\n    ]\n  },\n  \"shrimp\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shrimp\",\n    \"terms\": [\n      \"Shrimp\",\n      \"shrimp\"\n    ]\n  },\n  \"shuffle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shuffle\",\n    \"terms\": [\n      \"random\",\n      \"Shuffle\",\n      \"shuffle\"\n    ]\n  },\n  \"shuttle-space\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Shuttle space\",\n    \"terms\": [\n      \"space-shuttle\",\n      \"Shuttle space\",\n      \"shuttle-space\"\n    ]\n  },\n  \"sign-hanging\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sign hanging\",\n    \"terms\": [\n      \"sign\",\n      \"Sign hanging\",\n      \"sign-hanging\"\n    ]\n  },\n  \"signal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"signal\",\n    \"terms\": [\n      \"signal-5\",\n      \"signal-perfect\",\n      \"signal\",\n      \"signal\"\n    ]\n  },\n  \"signature\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Signature\",\n    \"terms\": [\n      \"Signature\",\n      \"signature\"\n    ]\n  },\n  \"signs-post\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Signs post\",\n    \"terms\": [\n      \"map-signs\",\n      \"Signs post\",\n      \"signs-post\"\n    ]\n  },\n  \"sim-card\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"SIM Card\",\n    \"terms\": [\n      \"SIM Card\",\n      \"sim-card\"\n    ]\n  },\n  \"simplybuilt\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"SimplyBuilt\",\n    \"terms\": [\n      \"SimplyBuilt\",\n      \"simplybuilt\"\n    ]\n  },\n  \"sink\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sink\",\n    \"terms\": [\n      \"Sink\",\n      \"sink\"\n    ]\n  },\n  \"sistrix\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"SISTRIX\",\n    \"terms\": [\n      \"SISTRIX\",\n      \"sistrix\"\n    ]\n  },\n  \"sitemap\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sitemap\",\n    \"terms\": [\n      \"Sitemap\",\n      \"sitemap\"\n    ]\n  },\n  \"sith\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sith\",\n    \"terms\": [\n      \"Sith\",\n      \"sith\"\n    ]\n  },\n  \"sitrox\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sitrox\",\n    \"terms\": [\n      \"Sitrox\",\n      \"sitrox\"\n    ]\n  },\n  \"sketch\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sketch\",\n    \"terms\": [\n      \"Sketch\",\n      \"sketch\"\n    ]\n  },\n  \"skull\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Skull\",\n    \"terms\": [\n      \"Skull\",\n      \"skull\"\n    ]\n  },\n  \"skull-crossbones\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Skull & Crossbones\",\n    \"terms\": [\n      \"Skull & Crossbones\",\n      \"skull-crossbones\"\n    ]\n  },\n  \"skyatlas\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"skyatlas\",\n    \"terms\": [\n      \"skyatlas\",\n      \"skyatlas\"\n    ]\n  },\n  \"skype\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Skype\",\n    \"terms\": [\n      \"Skype\",\n      \"skype\"\n    ]\n  },\n  \"slack\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Slack Logo\",\n    \"terms\": [\n      \"slack-hash\",\n      \"Slack Logo\",\n      \"slack\"\n    ]\n  },\n  \"slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Slash\",\n    \"terms\": [\n      \"Slash\",\n      \"slash\"\n    ]\n  },\n  \"sleigh\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sleigh\",\n    \"terms\": [\n      \"Sleigh\",\n      \"sleigh\"\n    ]\n  },\n  \"sliders\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sliders\",\n    \"terms\": [\n      \"sliders-h\",\n      \"Sliders\",\n      \"sliders\"\n    ]\n  },\n  \"slideshare\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Slideshare\",\n    \"terms\": [\n      \"Slideshare\",\n      \"slideshare\"\n    ]\n  },\n  \"smog\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Smog\",\n    \"terms\": [\n      \"Smog\",\n      \"smog\"\n    ]\n  },\n  \"smoking\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Smoking\",\n    \"terms\": [\n      \"Smoking\",\n      \"smoking\"\n    ]\n  },\n  \"snapchat\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Snapchat\",\n    \"terms\": [\n      \"snapchat-ghost\",\n      \"Snapchat\",\n      \"snapchat\"\n    ]\n  },\n  \"snowflake\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Snowflake\",\n    \"terms\": [\n      \"Snowflake\",\n      \"snowflake\"\n    ]\n  },\n  \"snowman\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Snowman\",\n    \"terms\": [\n      \"Snowman\",\n      \"snowman\"\n    ]\n  },\n  \"snowplow\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Snowplow\",\n    \"terms\": [\n      \"Snowplow\",\n      \"snowplow\"\n    ]\n  },\n  \"soap\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Soap\",\n    \"terms\": [\n      \"Soap\",\n      \"soap\"\n    ]\n  },\n  \"socks\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Socks\",\n    \"terms\": [\n      \"Socks\",\n      \"socks\"\n    ]\n  },\n  \"solar-panel\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Solar Panel\",\n    \"terms\": [\n      \"Solar Panel\",\n      \"solar-panel\"\n    ]\n  },\n  \"sort\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sort\",\n    \"terms\": [\n      \"unsorted\",\n      \"Sort\",\n      \"sort\"\n    ]\n  },\n  \"sort-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sort Down (Descending)\",\n    \"terms\": [\n      \"sort-desc\",\n      \"Sort Down (Descending)\",\n      \"sort-down\"\n    ]\n  },\n  \"sort-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sort Up (Ascending)\",\n    \"terms\": [\n      \"sort-asc\",\n      \"Sort Up (Ascending)\",\n      \"sort-up\"\n    ]\n  },\n  \"soundcloud\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"SoundCloud\",\n    \"terms\": [\n      \"SoundCloud\",\n      \"soundcloud\"\n    ]\n  },\n  \"sourcetree\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sourcetree\",\n    \"terms\": [\n      \"Sourcetree\",\n      \"sourcetree\"\n    ]\n  },\n  \"spa\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spa\",\n    \"terms\": [\n      \"Spa\",\n      \"spa\"\n    ]\n  },\n  \"space-awesome\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Space Awesome\",\n    \"terms\": [\n      \"Space Awesome\",\n      \"space-awesome\"\n    ]\n  },\n  \"spaghetti-monster-flying\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spaghetti monster flying\",\n    \"terms\": [\n      \"pastafarianism\",\n      \"Spaghetti monster flying\",\n      \"spaghetti-monster-flying\"\n    ]\n  },\n  \"speakap\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Speakap\",\n    \"terms\": [\n      \"Speakap\",\n      \"speakap\"\n    ]\n  },\n  \"speaker-deck\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Speaker Deck\",\n    \"terms\": [\n      \"Speaker Deck\",\n      \"speaker-deck\"\n    ]\n  },\n  \"spell-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spell Check\",\n    \"terms\": [\n      \"Spell Check\",\n      \"spell-check\"\n    ]\n  },\n  \"spider\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spider\",\n    \"terms\": [\n      \"Spider\",\n      \"spider\"\n    ]\n  },\n  \"spinner\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spinner\",\n    \"terms\": [\n      \"Spinner\",\n      \"spinner\"\n    ]\n  },\n  \"splotch\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Splotch\",\n    \"terms\": [\n      \"Splotch\",\n      \"splotch\"\n    ]\n  },\n  \"spoon\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spoon\",\n    \"terms\": [\n      \"utensil-spoon\",\n      \"Spoon\",\n      \"spoon\"\n    ]\n  },\n  \"spotify\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Spotify\",\n    \"terms\": [\n      \"Spotify\",\n      \"spotify\"\n    ]\n  },\n  \"spray-can\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spray Can\",\n    \"terms\": [\n      \"Spray Can\",\n      \"spray-can\"\n    ]\n  },\n  \"spray-can-sparkles\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Spray Can Sparkles\",\n    \"terms\": [\n      \"air-freshener\",\n      \"Spray Can Sparkles\",\n      \"spray-can-sparkles\"\n    ]\n  },\n  \"square\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square\",\n    \"terms\": [\n      \"Square\",\n      \"square\"\n    ]\n  },\n  \"square-arrow-up-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square arrow up right\",\n    \"terms\": [\n      \"external-link-square\",\n      \"Square arrow up right\",\n      \"square-arrow-up-right\"\n    ]\n  },\n  \"square-behance\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Behance Square\",\n    \"terms\": [\n      \"behance-square\",\n      \"Behance Square\",\n      \"square-behance\"\n    ]\n  },\n  \"square-caret-down\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square caret down\",\n    \"terms\": [\n      \"caret-square-down\",\n      \"Square caret down\",\n      \"square-caret-down\"\n    ]\n  },\n  \"square-caret-left\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square caret left\",\n    \"terms\": [\n      \"caret-square-left\",\n      \"Square caret left\",\n      \"square-caret-left\"\n    ]\n  },\n  \"square-caret-right\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square caret right\",\n    \"terms\": [\n      \"caret-square-right\",\n      \"Square caret right\",\n      \"square-caret-right\"\n    ]\n  },\n  \"square-caret-up\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square caret up\",\n    \"terms\": [\n      \"caret-square-up\",\n      \"Square caret up\",\n      \"square-caret-up\"\n    ]\n  },\n  \"square-check\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square check\",\n    \"terms\": [\n      \"check-square\",\n      \"Square check\",\n      \"square-check\"\n    ]\n  },\n  \"square-dribbble\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Dribbble Square\",\n    \"terms\": [\n      \"dribbble-square\",\n      \"Dribbble Square\",\n      \"square-dribbble\"\n    ]\n  },\n  \"square-envelope\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square envelope\",\n    \"terms\": [\n      \"envelope-square\",\n      \"Square envelope\",\n      \"square-envelope\"\n    ]\n  },\n  \"square-facebook\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Facebook Square\",\n    \"terms\": [\n      \"facebook-square\",\n      \"Facebook Square\",\n      \"square-facebook\"\n    ]\n  },\n  \"square-font-awesome\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Font Awesome in Square\",\n    \"terms\": [\n      \"Font Awesome in Square\",\n      \"square-font-awesome\"\n    ]\n  },\n  \"square-font-awesome-stroke\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Font Awesome in Square with Stroke Outline\",\n    \"terms\": [\n      \"font-awesome-alt\",\n      \"Font Awesome in Square with Stroke Outline\",\n      \"square-font-awesome-stroke\"\n    ]\n  },\n  \"square-full\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square Full\",\n    \"terms\": [\n      \"Square Full\",\n      \"square-full\"\n    ]\n  },\n  \"square-git\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Git Square\",\n    \"terms\": [\n      \"git-square\",\n      \"Git Square\",\n      \"square-git\"\n    ]\n  },\n  \"square-github\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"GitHub Square\",\n    \"terms\": [\n      \"github-square\",\n      \"GitHub Square\",\n      \"square-github\"\n    ]\n  },\n  \"square-gitlab\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Square Gitlab\",\n    \"terms\": [\n      \"gitlab-square\",\n      \"Square Gitlab\",\n      \"square-gitlab\"\n    ]\n  },\n  \"square-google-plus\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Google Plus Square\",\n    \"terms\": [\n      \"google-plus-square\",\n      \"Google Plus Square\",\n      \"square-google-plus\"\n    ]\n  },\n  \"square-h\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square h\",\n    \"terms\": [\n      \"h-square\",\n      \"Square h\",\n      \"square-h\"\n    ]\n  },\n  \"square-hacker-news\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Hacker News Square\",\n    \"terms\": [\n      \"hacker-news-square\",\n      \"Hacker News Square\",\n      \"square-hacker-news\"\n    ]\n  },\n  \"square-instagram\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Instagram Square\",\n    \"terms\": [\n      \"instagram-square\",\n      \"Instagram Square\",\n      \"square-instagram\"\n    ]\n  },\n  \"square-js\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"JavaScript (JS) Square\",\n    \"terms\": [\n      \"js-square\",\n      \"JavaScript (JS) Square\",\n      \"square-js\"\n    ]\n  },\n  \"square-lastfm\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"last.fm Square\",\n    \"terms\": [\n      \"lastfm-square\",\n      \"last.fm Square\",\n      \"square-lastfm\"\n    ]\n  },\n  \"square-minus\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square minus\",\n    \"terms\": [\n      \"minus-square\",\n      \"Square minus\",\n      \"square-minus\"\n    ]\n  },\n  \"square-nfi\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square Nfi\",\n    \"terms\": [\n      \"Square Nfi\",\n      \"square-nfi\"\n    ]\n  },\n  \"square-odnoklassniki\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Odnoklassniki Square\",\n    \"terms\": [\n      \"odnoklassniki-square\",\n      \"Odnoklassniki Square\",\n      \"square-odnoklassniki\"\n    ]\n  },\n  \"square-parking\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square parking\",\n    \"terms\": [\n      \"parking\",\n      \"Square parking\",\n      \"square-parking\"\n    ]\n  },\n  \"square-pen\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square pen\",\n    \"terms\": [\n      \"pen-square\",\n      \"pencil-square\",\n      \"Square pen\",\n      \"square-pen\"\n    ]\n  },\n  \"square-person-confined\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square Person-confined\",\n    \"terms\": [\n      \"Square Person-confined\",\n      \"square-person-confined\"\n    ]\n  },\n  \"square-phone\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square phone\",\n    \"terms\": [\n      \"phone-square\",\n      \"Square phone\",\n      \"square-phone\"\n    ]\n  },\n  \"square-phone-flip\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square phone flip\",\n    \"terms\": [\n      \"phone-square-alt\",\n      \"Square phone flip\",\n      \"square-phone-flip\"\n    ]\n  },\n  \"square-pied-piper\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pied Piper Square Logo (Old)\",\n    \"terms\": [\n      \"pied-piper-square\",\n      \"Pied Piper Square Logo (Old)\",\n      \"square-pied-piper\"\n    ]\n  },\n  \"square-pinterest\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Pinterest Square\",\n    \"terms\": [\n      \"pinterest-square\",\n      \"Pinterest Square\",\n      \"square-pinterest\"\n    ]\n  },\n  \"square-plus\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Square plus\",\n    \"terms\": [\n      \"plus-square\",\n      \"Square plus\",\n      \"square-plus\"\n    ]\n  },\n  \"square-poll-horizontal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square poll horizontal\",\n    \"terms\": [\n      \"poll-h\",\n      \"Square poll horizontal\",\n      \"square-poll-horizontal\"\n    ]\n  },\n  \"square-poll-vertical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square poll vertical\",\n    \"terms\": [\n      \"poll\",\n      \"Square poll vertical\",\n      \"square-poll-vertical\"\n    ]\n  },\n  \"square-reddit\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"reddit Square\",\n    \"terms\": [\n      \"reddit-square\",\n      \"reddit Square\",\n      \"square-reddit\"\n    ]\n  },\n  \"square-root-variable\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square root variable\",\n    \"terms\": [\n      \"square-root-alt\",\n      \"Square root variable\",\n      \"square-root-variable\"\n    ]\n  },\n  \"square-rss\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square rss\",\n    \"terms\": [\n      \"rss-square\",\n      \"Square rss\",\n      \"square-rss\"\n    ]\n  },\n  \"square-share-nodes\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square share nodes\",\n    \"terms\": [\n      \"share-alt-square\",\n      \"Square share nodes\",\n      \"square-share-nodes\"\n    ]\n  },\n  \"square-snapchat\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Snapchat Square\",\n    \"terms\": [\n      \"snapchat-square\",\n      \"Snapchat Square\",\n      \"square-snapchat\"\n    ]\n  },\n  \"square-steam\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Steam Square\",\n    \"terms\": [\n      \"steam-square\",\n      \"Steam Square\",\n      \"square-steam\"\n    ]\n  },\n  \"square-tumblr\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Tumblr Square\",\n    \"terms\": [\n      \"tumblr-square\",\n      \"Tumblr Square\",\n      \"square-tumblr\"\n    ]\n  },\n  \"square-twitter\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Twitter Square\",\n    \"terms\": [\n      \"twitter-square\",\n      \"Twitter Square\",\n      \"square-twitter\"\n    ]\n  },\n  \"square-up-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square up right\",\n    \"terms\": [\n      \"external-link-square-alt\",\n      \"Square up right\",\n      \"square-up-right\"\n    ]\n  },\n  \"square-viadeo\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Viadeo Square\",\n    \"terms\": [\n      \"viadeo-square\",\n      \"Viadeo Square\",\n      \"square-viadeo\"\n    ]\n  },\n  \"square-vimeo\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Vimeo Square\",\n    \"terms\": [\n      \"vimeo-square\",\n      \"Vimeo Square\",\n      \"square-vimeo\"\n    ]\n  },\n  \"square-virus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square Virus\",\n    \"terms\": [\n      \"Square Virus\",\n      \"square-virus\"\n    ]\n  },\n  \"square-whatsapp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"What's App Square\",\n    \"terms\": [\n      \"whatsapp-square\",\n      \"What's App Square\",\n      \"square-whatsapp\"\n    ]\n  },\n  \"square-xing\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Xing Square\",\n    \"terms\": [\n      \"xing-square\",\n      \"Xing Square\",\n      \"square-xing\"\n    ]\n  },\n  \"square-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square X Mark\",\n    \"terms\": [\n      \"times-square\",\n      \"xmark-square\",\n      \"Square X Mark\",\n      \"square-xmark\"\n    ]\n  },\n  \"square-youtube\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"YouTube Square\",\n    \"terms\": [\n      \"youtube-square\",\n      \"YouTube Square\",\n      \"square-youtube\"\n    ]\n  },\n  \"squarespace\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Squarespace\",\n    \"terms\": [\n      \"Squarespace\",\n      \"squarespace\"\n    ]\n  },\n  \"stack-exchange\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Stack Exchange\",\n    \"terms\": [\n      \"Stack Exchange\",\n      \"stack-exchange\"\n    ]\n  },\n  \"stack-overflow\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Stack Overflow\",\n    \"terms\": [\n      \"Stack Overflow\",\n      \"stack-overflow\"\n    ]\n  },\n  \"stackpath\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Stackpath\",\n    \"terms\": [\n      \"Stackpath\",\n      \"stackpath\"\n    ]\n  },\n  \"staff-snake\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Staff Aesculapius\",\n    \"terms\": [\n      \"rod-asclepius\",\n      \"rod-snake\",\n      \"staff-aesculapius\",\n      \"Staff Aesculapius\",\n      \"staff-snake\"\n    ]\n  },\n  \"stairs\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Stairs\",\n    \"terms\": [\n      \"Stairs\",\n      \"stairs\"\n    ]\n  },\n  \"stamp\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Stamp\",\n    \"terms\": [\n      \"Stamp\",\n      \"stamp\"\n    ]\n  },\n  \"stapler\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Stapler\",\n    \"terms\": [\n      \"Stapler\",\n      \"stapler\"\n    ]\n  },\n  \"star\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Star\",\n    \"terms\": [\n      \"Star\",\n      \"star\"\n    ]\n  },\n  \"star-and-crescent\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Star and Crescent\",\n    \"terms\": [\n      \"Star and Crescent\",\n      \"star-and-crescent\"\n    ]\n  },\n  \"star-half\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"star-half\",\n    \"terms\": [\n      \"star-half\",\n      \"star-half\"\n    ]\n  },\n  \"star-half-stroke\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Star half stroke\",\n    \"terms\": [\n      \"star-half-alt\",\n      \"Star half stroke\",\n      \"star-half-stroke\"\n    ]\n  },\n  \"star-of-david\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Star of David\",\n    \"terms\": [\n      \"Star of David\",\n      \"star-of-david\"\n    ]\n  },\n  \"star-of-life\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Star of Life\",\n    \"terms\": [\n      \"Star of Life\",\n      \"star-of-life\"\n    ]\n  },\n  \"staylinked\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"StayLinked\",\n    \"terms\": [\n      \"StayLinked\",\n      \"staylinked\"\n    ]\n  },\n  \"steam\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Steam\",\n    \"terms\": [\n      \"Steam\",\n      \"steam\"\n    ]\n  },\n  \"steam-symbol\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Steam Symbol\",\n    \"terms\": [\n      \"Steam Symbol\",\n      \"steam-symbol\"\n    ]\n  },\n  \"sterling-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sterling sign\",\n    \"terms\": [\n      \"gbp\",\n      \"pound-sign\",\n      \"Sterling sign\",\n      \"sterling-sign\"\n    ]\n  },\n  \"stethoscope\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Stethoscope\",\n    \"terms\": [\n      \"Stethoscope\",\n      \"stethoscope\"\n    ]\n  },\n  \"sticker-mule\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Sticker Mule\",\n    \"terms\": [\n      \"Sticker Mule\",\n      \"sticker-mule\"\n    ]\n  },\n  \"stop\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"stop\",\n    \"terms\": [\n      \"stop\",\n      \"stop\"\n    ]\n  },\n  \"stopwatch\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Stopwatch\",\n    \"terms\": [\n      \"Stopwatch\",\n      \"stopwatch\"\n    ]\n  },\n  \"stopwatch-20\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Stopwatch 20\",\n    \"terms\": [\n      \"Stopwatch 20\",\n      \"stopwatch-20\"\n    ]\n  },\n  \"store\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Store\",\n    \"terms\": [\n      \"Store\",\n      \"store\"\n    ]\n  },\n  \"store-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Store Slash\",\n    \"terms\": [\n      \"Store Slash\",\n      \"store-slash\"\n    ]\n  },\n  \"strava\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Strava\",\n    \"terms\": [\n      \"Strava\",\n      \"strava\"\n    ]\n  },\n  \"street-view\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Street View\",\n    \"terms\": [\n      \"Street View\",\n      \"street-view\"\n    ]\n  },\n  \"strikethrough\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Strikethrough\",\n    \"terms\": [\n      \"Strikethrough\",\n      \"strikethrough\"\n    ]\n  },\n  \"stripe\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Stripe\",\n    \"terms\": [\n      \"Stripe\",\n      \"stripe\"\n    ]\n  },\n  \"stripe-s\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Stripe S\",\n    \"terms\": [\n      \"Stripe S\",\n      \"stripe-s\"\n    ]\n  },\n  \"stroopwafel\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Stroopwafel\",\n    \"terms\": [\n      \"Stroopwafel\",\n      \"stroopwafel\"\n    ]\n  },\n  \"studiovinari\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Studio Vinari\",\n    \"terms\": [\n      \"Studio Vinari\",\n      \"studiovinari\"\n    ]\n  },\n  \"stumbleupon\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"StumbleUpon Logo\",\n    \"terms\": [\n      \"StumbleUpon Logo\",\n      \"stumbleupon\"\n    ]\n  },\n  \"stumbleupon-circle\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"StumbleUpon Circle\",\n    \"terms\": [\n      \"StumbleUpon Circle\",\n      \"stumbleupon-circle\"\n    ]\n  },\n  \"subscript\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"subscript\",\n    \"terms\": [\n      \"subscript\",\n      \"subscript\"\n    ]\n  },\n  \"suitcase\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Suitcase\",\n    \"terms\": [\n      \"Suitcase\",\n      \"suitcase\"\n    ]\n  },\n  \"suitcase-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Suitcase medical\",\n    \"terms\": [\n      \"medkit\",\n      \"Suitcase medical\",\n      \"suitcase-medical\"\n    ]\n  },\n  \"suitcase-rolling\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Suitcase Rolling\",\n    \"terms\": [\n      \"Suitcase Rolling\",\n      \"suitcase-rolling\"\n    ]\n  },\n  \"sun\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Sun\",\n    \"terms\": [\n      \"Sun\",\n      \"sun\"\n    ]\n  },\n  \"sun-plant-wilt\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Sun Plant-wilt\",\n    \"terms\": [\n      \"Sun Plant-wilt\",\n      \"sun-plant-wilt\"\n    ]\n  },\n  \"superpowers\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Superpowers\",\n    \"terms\": [\n      \"Superpowers\",\n      \"superpowers\"\n    ]\n  },\n  \"superscript\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"superscript\",\n    \"terms\": [\n      \"superscript\",\n      \"superscript\"\n    ]\n  },\n  \"supple\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Supple\",\n    \"terms\": [\n      \"Supple\",\n      \"supple\"\n    ]\n  },\n  \"suse\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Suse\",\n    \"terms\": [\n      \"Suse\",\n      \"suse\"\n    ]\n  },\n  \"swatchbook\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Swatchbook\",\n    \"terms\": [\n      \"Swatchbook\",\n      \"swatchbook\"\n    ]\n  },\n  \"swift\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Swift\",\n    \"terms\": [\n      \"Swift\",\n      \"swift\"\n    ]\n  },\n  \"symfony\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Symfony\",\n    \"terms\": [\n      \"Symfony\",\n      \"symfony\"\n    ]\n  },\n  \"synagogue\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Synagogue\",\n    \"terms\": [\n      \"Synagogue\",\n      \"synagogue\"\n    ]\n  },\n  \"syringe\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Syringe\",\n    \"terms\": [\n      \"Syringe\",\n      \"syringe\"\n    ]\n  },\n  \"t\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"T\",\n    \"terms\": [\n      \"T\",\n      \"t\"\n    ]\n  },\n  \"table\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"table\",\n    \"terms\": [\n      \"table\",\n      \"table\"\n    ]\n  },\n  \"table-cells\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Table cells\",\n    \"terms\": [\n      \"th\",\n      \"Table cells\",\n      \"table-cells\"\n    ]\n  },\n  \"table-cells-large\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Table cells large\",\n    \"terms\": [\n      \"th-large\",\n      \"Table cells large\",\n      \"table-cells-large\"\n    ]\n  },\n  \"table-columns\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Table columns\",\n    \"terms\": [\n      \"columns\",\n      \"Table columns\",\n      \"table-columns\"\n    ]\n  },\n  \"table-list\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Table list\",\n    \"terms\": [\n      \"th-list\",\n      \"Table list\",\n      \"table-list\"\n    ]\n  },\n  \"table-tennis-paddle-ball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Table tennis paddle ball\",\n    \"terms\": [\n      \"ping-pong-paddle-ball\",\n      \"table-tennis\",\n      \"Table tennis paddle ball\",\n      \"table-tennis-paddle-ball\"\n    ]\n  },\n  \"tablet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tablet\",\n    \"terms\": [\n      \"tablet-android\",\n      \"Tablet\",\n      \"tablet\"\n    ]\n  },\n  \"tablet-button\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tablet button\",\n    \"terms\": [\n      \"Tablet button\",\n      \"tablet-button\"\n    ]\n  },\n  \"tablet-screen-button\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tablet screen button\",\n    \"terms\": [\n      \"tablet-alt\",\n      \"Tablet screen button\",\n      \"tablet-screen-button\"\n    ]\n  },\n  \"tablets\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tablets\",\n    \"terms\": [\n      \"Tablets\",\n      \"tablets\"\n    ]\n  },\n  \"tachograph-digital\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tachograph digital\",\n    \"terms\": [\n      \"digital-tachograph\",\n      \"Tachograph digital\",\n      \"tachograph-digital\"\n    ]\n  },\n  \"tag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"tag\",\n    \"terms\": [\n      \"tag\",\n      \"tag\"\n    ]\n  },\n  \"tags\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"tags\",\n    \"terms\": [\n      \"tags\",\n      \"tags\"\n    ]\n  },\n  \"tape\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tape\",\n    \"terms\": [\n      \"Tape\",\n      \"tape\"\n    ]\n  },\n  \"tarp\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tarp\",\n    \"terms\": [\n      \"Tarp\",\n      \"tarp\"\n    ]\n  },\n  \"tarp-droplet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tarp Droplet\",\n    \"terms\": [\n      \"Tarp Droplet\",\n      \"tarp-droplet\"\n    ]\n  },\n  \"taxi\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Taxi\",\n    \"terms\": [\n      \"cab\",\n      \"Taxi\",\n      \"taxi\"\n    ]\n  },\n  \"teamspeak\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"TeamSpeak\",\n    \"terms\": [\n      \"TeamSpeak\",\n      \"teamspeak\"\n    ]\n  },\n  \"teeth\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Teeth\",\n    \"terms\": [\n      \"Teeth\",\n      \"teeth\"\n    ]\n  },\n  \"teeth-open\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Teeth Open\",\n    \"terms\": [\n      \"Teeth Open\",\n      \"teeth-open\"\n    ]\n  },\n  \"telegram\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Telegram\",\n    \"terms\": [\n      \"telegram-plane\",\n      \"Telegram\",\n      \"telegram\"\n    ]\n  },\n  \"temperature-arrow-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Temperature arrow down\",\n    \"terms\": [\n      \"temperature-down\",\n      \"Temperature arrow down\",\n      \"temperature-arrow-down\"\n    ]\n  },\n  \"temperature-arrow-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Temperature arrow up\",\n    \"terms\": [\n      \"temperature-up\",\n      \"Temperature arrow up\",\n      \"temperature-arrow-up\"\n    ]\n  },\n  \"temperature-empty\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Temperature empty\",\n    \"terms\": [\n      \"temperature-0\",\n      \"thermometer-0\",\n      \"thermometer-empty\",\n      \"Temperature empty\",\n      \"temperature-empty\"\n    ]\n  },\n  \"temperature-full\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Temperature full\",\n    \"terms\": [\n      \"temperature-4\",\n      \"thermometer-4\",\n      \"thermometer-full\",\n      \"Temperature full\",\n      \"temperature-full\"\n    ]\n  },\n  \"temperature-half\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Temperature half\",\n    \"terms\": [\n      \"temperature-2\",\n      \"thermometer-2\",\n      \"thermometer-half\",\n      \"Temperature half\",\n      \"temperature-half\"\n    ]\n  },\n  \"temperature-high\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"High Temperature\",\n    \"terms\": [\n      \"High Temperature\",\n      \"temperature-high\"\n    ]\n  },\n  \"temperature-low\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Low Temperature\",\n    \"terms\": [\n      \"Low Temperature\",\n      \"temperature-low\"\n    ]\n  },\n  \"temperature-quarter\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Temperature quarter\",\n    \"terms\": [\n      \"temperature-1\",\n      \"thermometer-1\",\n      \"thermometer-quarter\",\n      \"Temperature quarter\",\n      \"temperature-quarter\"\n    ]\n  },\n  \"temperature-three-quarters\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Temperature three quarters\",\n    \"terms\": [\n      \"temperature-3\",\n      \"thermometer-3\",\n      \"thermometer-three-quarters\",\n      \"Temperature three quarters\",\n      \"temperature-three-quarters\"\n    ]\n  },\n  \"tencent-weibo\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Tencent Weibo\",\n    \"terms\": [\n      \"Tencent Weibo\",\n      \"tencent-weibo\"\n    ]\n  },\n  \"tenge-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tenge sign\",\n    \"terms\": [\n      \"tenge\",\n      \"Tenge sign\",\n      \"tenge-sign\"\n    ]\n  },\n  \"tent\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tent\",\n    \"terms\": [\n      \"Tent\",\n      \"tent\"\n    ]\n  },\n  \"tent-arrow-down-to-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tent Arrow-down-to-line\",\n    \"terms\": [\n      \"Tent Arrow-down-to-line\",\n      \"tent-arrow-down-to-line\"\n    ]\n  },\n  \"tent-arrow-left-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tent Arrow-left-right\",\n    \"terms\": [\n      \"Tent Arrow-left-right\",\n      \"tent-arrow-left-right\"\n    ]\n  },\n  \"tent-arrow-turn-left\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tent Arrow-turn-left\",\n    \"terms\": [\n      \"Tent Arrow-turn-left\",\n      \"tent-arrow-turn-left\"\n    ]\n  },\n  \"tent-arrows-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tent Arrows-down\",\n    \"terms\": [\n      \"Tent Arrows-down\",\n      \"tent-arrows-down\"\n    ]\n  },\n  \"tents\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tents\",\n    \"terms\": [\n      \"Tents\",\n      \"tents\"\n    ]\n  },\n  \"terminal\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Terminal\",\n    \"terms\": [\n      \"Terminal\",\n      \"terminal\"\n    ]\n  },\n  \"text-height\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"text-height\",\n    \"terms\": [\n      \"text-height\",\n      \"text-height\"\n    ]\n  },\n  \"text-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Text slash\",\n    \"terms\": [\n      \"remove-format\",\n      \"Text slash\",\n      \"text-slash\"\n    ]\n  },\n  \"text-width\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Text Width\",\n    \"terms\": [\n      \"Text Width\",\n      \"text-width\"\n    ]\n  },\n  \"the-red-yeti\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"The Red Yeti\",\n    \"terms\": [\n      \"The Red Yeti\",\n      \"the-red-yeti\"\n    ]\n  },\n  \"themeco\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Themeco\",\n    \"terms\": [\n      \"Themeco\",\n      \"themeco\"\n    ]\n  },\n  \"themeisle\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"ThemeIsle\",\n    \"terms\": [\n      \"ThemeIsle\",\n      \"themeisle\"\n    ]\n  },\n  \"thermometer\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Thermometer\",\n    \"terms\": [\n      \"Thermometer\",\n      \"thermometer\"\n    ]\n  },\n  \"think-peaks\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Think Peaks\",\n    \"terms\": [\n      \"Think Peaks\",\n      \"think-peaks\"\n    ]\n  },\n  \"thumbs-down\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"thumbs-down\",\n    \"terms\": [\n      \"thumbs-down\",\n      \"thumbs-down\"\n    ]\n  },\n  \"thumbs-up\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"thumbs-up\",\n    \"terms\": [\n      \"thumbs-up\",\n      \"thumbs-up\"\n    ]\n  },\n  \"thumbtack\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Thumbtack\",\n    \"terms\": [\n      \"thumb-tack\",\n      \"Thumbtack\",\n      \"thumbtack\"\n    ]\n  },\n  \"ticket\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ticket\",\n    \"terms\": [\n      \"Ticket\",\n      \"ticket\"\n    ]\n  },\n  \"ticket-simple\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Ticket simple\",\n    \"terms\": [\n      \"ticket-alt\",\n      \"Ticket simple\",\n      \"ticket-simple\"\n    ]\n  },\n  \"tiktok\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"TikTok\",\n    \"terms\": [\n      \"TikTok\",\n      \"tiktok\"\n    ]\n  },\n  \"timeline\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Timeline\",\n    \"terms\": [\n      \"Timeline\",\n      \"timeline\"\n    ]\n  },\n  \"toggle-off\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toggle Off\",\n    \"terms\": [\n      \"Toggle Off\",\n      \"toggle-off\"\n    ]\n  },\n  \"toggle-on\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toggle On\",\n    \"terms\": [\n      \"Toggle On\",\n      \"toggle-on\"\n    ]\n  },\n  \"toilet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toilet\",\n    \"terms\": [\n      \"Toilet\",\n      \"toilet\"\n    ]\n  },\n  \"toilet-paper\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toilet Paper\",\n    \"terms\": [\n      \"Toilet Paper\",\n      \"toilet-paper\"\n    ]\n  },\n  \"toilet-paper-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toilet Paper Slash\",\n    \"terms\": [\n      \"Toilet Paper Slash\",\n      \"toilet-paper-slash\"\n    ]\n  },\n  \"toilet-portable\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toilet Portable\",\n    \"terms\": [\n      \"Toilet Portable\",\n      \"toilet-portable\"\n    ]\n  },\n  \"toilets-portable\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toilets Portable\",\n    \"terms\": [\n      \"Toilets Portable\",\n      \"toilets-portable\"\n    ]\n  },\n  \"toolbox\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Toolbox\",\n    \"terms\": [\n      \"Toolbox\",\n      \"toolbox\"\n    ]\n  },\n  \"tooth\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tooth\",\n    \"terms\": [\n      \"Tooth\",\n      \"tooth\"\n    ]\n  },\n  \"torii-gate\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Torii Gate\",\n    \"terms\": [\n      \"Torii Gate\",\n      \"torii-gate\"\n    ]\n  },\n  \"tornado\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tornado\",\n    \"terms\": [\n      \"Tornado\",\n      \"tornado\"\n    ]\n  },\n  \"tower-broadcast\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tower broadcast\",\n    \"terms\": [\n      \"broadcast-tower\",\n      \"Tower broadcast\",\n      \"tower-broadcast\"\n    ]\n  },\n  \"tower-cell\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tower Cell\",\n    \"terms\": [\n      \"Tower Cell\",\n      \"tower-cell\"\n    ]\n  },\n  \"tower-observation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tower Observation\",\n    \"terms\": [\n      \"Tower Observation\",\n      \"tower-observation\"\n    ]\n  },\n  \"tractor\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tractor\",\n    \"terms\": [\n      \"Tractor\",\n      \"tractor\"\n    ]\n  },\n  \"trade-federation\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Trade Federation\",\n    \"terms\": [\n      \"Trade Federation\",\n      \"trade-federation\"\n    ]\n  },\n  \"trademark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Trademark\",\n    \"terms\": [\n      \"Trademark\",\n      \"trademark\"\n    ]\n  },\n  \"traffic-light\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Traffic Light\",\n    \"terms\": [\n      \"Traffic Light\",\n      \"traffic-light\"\n    ]\n  },\n  \"trailer\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Trailer\",\n    \"terms\": [\n      \"Trailer\",\n      \"trailer\"\n    ]\n  },\n  \"train\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Train\",\n    \"terms\": [\n      \"Train\",\n      \"train\"\n    ]\n  },\n  \"train-subway\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Train subway\",\n    \"terms\": [\n      \"subway\",\n      \"Train subway\",\n      \"train-subway\"\n    ]\n  },\n  \"train-tram\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Train tram\",\n    \"terms\": [\n      \"Train tram\",\n      \"train-tram\"\n    ]\n  },\n  \"transgender\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Transgender\",\n    \"terms\": [\n      \"transgender-alt\",\n      \"Transgender\",\n      \"transgender\"\n    ]\n  },\n  \"trash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Trash\",\n    \"terms\": [\n      \"Trash\",\n      \"trash\"\n    ]\n  },\n  \"trash-arrow-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Trash arrow up\",\n    \"terms\": [\n      \"trash-restore\",\n      \"Trash arrow up\",\n      \"trash-arrow-up\"\n    ]\n  },\n  \"trash-can\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Trash can\",\n    \"terms\": [\n      \"trash-alt\",\n      \"Trash can\",\n      \"trash-can\"\n    ]\n  },\n  \"trash-can-arrow-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Trash can arrow up\",\n    \"terms\": [\n      \"trash-restore-alt\",\n      \"Trash can arrow up\",\n      \"trash-can-arrow-up\"\n    ]\n  },\n  \"tree\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tree\",\n    \"terms\": [\n      \"Tree\",\n      \"tree\"\n    ]\n  },\n  \"tree-city\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Tree City\",\n    \"terms\": [\n      \"Tree City\",\n      \"tree-city\"\n    ]\n  },\n  \"trello\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Trello\",\n    \"terms\": [\n      \"Trello\",\n      \"trello\"\n    ]\n  },\n  \"triangle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Triangle exclamation\",\n    \"terms\": [\n      \"exclamation-triangle\",\n      \"warning\",\n      \"Triangle exclamation\",\n      \"triangle-exclamation\"\n    ]\n  },\n  \"trophy\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"trophy\",\n    \"terms\": [\n      \"trophy\",\n      \"trophy\"\n    ]\n  },\n  \"trowel\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Trowel\",\n    \"terms\": [\n      \"Trowel\",\n      \"trowel\"\n    ]\n  },\n  \"trowel-bricks\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Trowel Bricks\",\n    \"terms\": [\n      \"Trowel Bricks\",\n      \"trowel-bricks\"\n    ]\n  },\n  \"truck\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"truck\",\n    \"terms\": [\n      \"truck\",\n      \"truck\"\n    ]\n  },\n  \"truck-arrow-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Arrow-right\",\n    \"terms\": [\n      \"Truck Arrow-right\",\n      \"truck-arrow-right\"\n    ]\n  },\n  \"truck-droplet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Droplet\",\n    \"terms\": [\n      \"Truck Droplet\",\n      \"truck-droplet\"\n    ]\n  },\n  \"truck-fast\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck fast\",\n    \"terms\": [\n      \"shipping-fast\",\n      \"Truck fast\",\n      \"truck-fast\"\n    ]\n  },\n  \"truck-field\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Field\",\n    \"terms\": [\n      \"Truck Field\",\n      \"truck-field\"\n    ]\n  },\n  \"truck-field-un\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Field-un\",\n    \"terms\": [\n      \"Truck Field-un\",\n      \"truck-field-un\"\n    ]\n  },\n  \"truck-front\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Front\",\n    \"terms\": [\n      \"Truck Front\",\n      \"truck-front\"\n    ]\n  },\n  \"truck-medical\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck medical\",\n    \"terms\": [\n      \"ambulance\",\n      \"Truck medical\",\n      \"truck-medical\"\n    ]\n  },\n  \"truck-monster\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Monster\",\n    \"terms\": [\n      \"Truck Monster\",\n      \"truck-monster\"\n    ]\n  },\n  \"truck-moving\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Moving\",\n    \"terms\": [\n      \"Truck Moving\",\n      \"truck-moving\"\n    ]\n  },\n  \"truck-pickup\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Side\",\n    \"terms\": [\n      \"Truck Side\",\n      \"truck-pickup\"\n    ]\n  },\n  \"truck-plane\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck Plane\",\n    \"terms\": [\n      \"Truck Plane\",\n      \"truck-plane\"\n    ]\n  },\n  \"truck-ramp-box\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Truck ramp box\",\n    \"terms\": [\n      \"truck-loading\",\n      \"Truck ramp box\",\n      \"truck-ramp-box\"\n    ]\n  },\n  \"tty\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"TTY\",\n    \"terms\": [\n      \"teletype\",\n      \"TTY\",\n      \"tty\"\n    ]\n  },\n  \"tumblr\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Tumblr\",\n    \"terms\": [\n      \"Tumblr\",\n      \"tumblr\"\n    ]\n  },\n  \"turkish-lira-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Turkish Lira-sign\",\n    \"terms\": [\n      \"try\",\n      \"turkish-lira\",\n      \"Turkish Lira-sign\",\n      \"turkish-lira-sign\"\n    ]\n  },\n  \"turn-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Turn down\",\n    \"terms\": [\n      \"level-down-alt\",\n      \"Turn down\",\n      \"turn-down\"\n    ]\n  },\n  \"turn-up\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Turn up\",\n    \"terms\": [\n      \"level-up-alt\",\n      \"Turn up\",\n      \"turn-up\"\n    ]\n  },\n  \"tv\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Television\",\n    \"terms\": [\n      \"television\",\n      \"tv-alt\",\n      \"Television\",\n      \"tv\"\n    ]\n  },\n  \"twitch\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Twitch\",\n    \"terms\": [\n      \"Twitch\",\n      \"twitch\"\n    ]\n  },\n  \"twitter\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Twitter\",\n    \"terms\": [\n      \"Twitter\",\n      \"twitter\"\n    ]\n  },\n  \"typo3\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Typo3\",\n    \"terms\": [\n      \"Typo3\",\n      \"typo3\"\n    ]\n  },\n  \"u\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"U\",\n    \"terms\": [\n      \"U\",\n      \"u\"\n    ]\n  },\n  \"uber\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Uber\",\n    \"terms\": [\n      \"Uber\",\n      \"uber\"\n    ]\n  },\n  \"ubuntu\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Ubuntu\",\n    \"terms\": [\n      \"Ubuntu\",\n      \"ubuntu\"\n    ]\n  },\n  \"uikit\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"UIkit\",\n    \"terms\": [\n      \"UIkit\",\n      \"uikit\"\n    ]\n  },\n  \"umbraco\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Umbraco\",\n    \"terms\": [\n      \"Umbraco\",\n      \"umbraco\"\n    ]\n  },\n  \"umbrella\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Umbrella\",\n    \"terms\": [\n      \"Umbrella\",\n      \"umbrella\"\n    ]\n  },\n  \"umbrella-beach\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Umbrella Beach\",\n    \"terms\": [\n      \"Umbrella Beach\",\n      \"umbrella-beach\"\n    ]\n  },\n  \"uncharted\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Uncharted Software\",\n    \"terms\": [\n      \"Uncharted Software\",\n      \"uncharted\"\n    ]\n  },\n  \"underline\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Underline\",\n    \"terms\": [\n      \"Underline\",\n      \"underline\"\n    ]\n  },\n  \"uniregistry\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Uniregistry\",\n    \"terms\": [\n      \"Uniregistry\",\n      \"uniregistry\"\n    ]\n  },\n  \"unity\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Unity 3D\",\n    \"terms\": [\n      \"Unity 3D\",\n      \"unity\"\n    ]\n  },\n  \"universal-access\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Universal Access\",\n    \"terms\": [\n      \"Universal Access\",\n      \"universal-access\"\n    ]\n  },\n  \"unlock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"unlock\",\n    \"terms\": [\n      \"unlock\",\n      \"unlock\"\n    ]\n  },\n  \"unlock-keyhole\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Unlock keyhole\",\n    \"terms\": [\n      \"unlock-alt\",\n      \"Unlock keyhole\",\n      \"unlock-keyhole\"\n    ]\n  },\n  \"unsplash\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Unsplash\",\n    \"terms\": [\n      \"Unsplash\",\n      \"unsplash\"\n    ]\n  },\n  \"untappd\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Untappd\",\n    \"terms\": [\n      \"Untappd\",\n      \"untappd\"\n    ]\n  },\n  \"up-down\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Up down\",\n    \"terms\": [\n      \"arrows-alt-v\",\n      \"Up down\",\n      \"up-down\"\n    ]\n  },\n  \"up-down-left-right\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Up down left right\",\n    \"terms\": [\n      \"arrows-alt\",\n      \"Up down left right\",\n      \"up-down-left-right\"\n    ]\n  },\n  \"up-long\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Up long\",\n    \"terms\": [\n      \"long-arrow-alt-up\",\n      \"Up long\",\n      \"up-long\"\n    ]\n  },\n  \"up-right-and-down-left-from-center\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Up right and down left from center\",\n    \"terms\": [\n      \"expand-alt\",\n      \"Up right and down left from center\",\n      \"up-right-and-down-left-from-center\"\n    ]\n  },\n  \"up-right-from-square\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Up right from square\",\n    \"terms\": [\n      \"external-link-alt\",\n      \"Up right from square\",\n      \"up-right-from-square\"\n    ]\n  },\n  \"upload\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Upload\",\n    \"terms\": [\n      \"Upload\",\n      \"upload\"\n    ]\n  },\n  \"ups\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"UPS\",\n    \"terms\": [\n      \"UPS\",\n      \"ups\"\n    ]\n  },\n  \"usb\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"USB\",\n    \"terms\": [\n      \"USB\",\n      \"usb\"\n    ]\n  },\n  \"user\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"User\",\n    \"terms\": [\n      \"User\",\n      \"user\"\n    ]\n  },\n  \"user-astronaut\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Astronaut\",\n    \"terms\": [\n      \"User Astronaut\",\n      \"user-astronaut\"\n    ]\n  },\n  \"user-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Check\",\n    \"terms\": [\n      \"User Check\",\n      \"user-check\"\n    ]\n  },\n  \"user-clock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Clock\",\n    \"terms\": [\n      \"User Clock\",\n      \"user-clock\"\n    ]\n  },\n  \"user-doctor\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User doctor\",\n    \"terms\": [\n      \"user-md\",\n      \"User doctor\",\n      \"user-doctor\"\n    ]\n  },\n  \"user-gear\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User gear\",\n    \"terms\": [\n      \"user-cog\",\n      \"User gear\",\n      \"user-gear\"\n    ]\n  },\n  \"user-graduate\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Graduate\",\n    \"terms\": [\n      \"User Graduate\",\n      \"user-graduate\"\n    ]\n  },\n  \"user-group\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User group\",\n    \"terms\": [\n      \"user-friends\",\n      \"User group\",\n      \"user-group\"\n    ]\n  },\n  \"user-injured\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Injured\",\n    \"terms\": [\n      \"User Injured\",\n      \"user-injured\"\n    ]\n  },\n  \"user-large\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User large\",\n    \"terms\": [\n      \"user-alt\",\n      \"User large\",\n      \"user-large\"\n    ]\n  },\n  \"user-large-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User large slash\",\n    \"terms\": [\n      \"user-alt-slash\",\n      \"User large slash\",\n      \"user-large-slash\"\n    ]\n  },\n  \"user-lock\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Lock\",\n    \"terms\": [\n      \"User Lock\",\n      \"user-lock\"\n    ]\n  },\n  \"user-minus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Minus\",\n    \"terms\": [\n      \"User Minus\",\n      \"user-minus\"\n    ]\n  },\n  \"user-ninja\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Ninja\",\n    \"terms\": [\n      \"User Ninja\",\n      \"user-ninja\"\n    ]\n  },\n  \"user-nurse\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Nurse\",\n    \"terms\": [\n      \"Nurse\",\n      \"user-nurse\"\n    ]\n  },\n  \"user-pen\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User pen\",\n    \"terms\": [\n      \"user-edit\",\n      \"User pen\",\n      \"user-pen\"\n    ]\n  },\n  \"user-plus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Plus\",\n    \"terms\": [\n      \"User Plus\",\n      \"user-plus\"\n    ]\n  },\n  \"user-secret\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Secret\",\n    \"terms\": [\n      \"User Secret\",\n      \"user-secret\"\n    ]\n  },\n  \"user-shield\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Shield\",\n    \"terms\": [\n      \"User Shield\",\n      \"user-shield\"\n    ]\n  },\n  \"user-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Slash\",\n    \"terms\": [\n      \"User Slash\",\n      \"user-slash\"\n    ]\n  },\n  \"user-tag\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Tag\",\n    \"terms\": [\n      \"User Tag\",\n      \"user-tag\"\n    ]\n  },\n  \"user-tie\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User Tie\",\n    \"terms\": [\n      \"User Tie\",\n      \"user-tie\"\n    ]\n  },\n  \"user-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"User X Mark\",\n    \"terms\": [\n      \"user-times\",\n      \"User X Mark\",\n      \"user-xmark\"\n    ]\n  },\n  \"users\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users\",\n    \"terms\": [\n      \"Users\",\n      \"users\"\n    ]\n  },\n  \"users-between-lines\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users Between-lines\",\n    \"terms\": [\n      \"Users Between-lines\",\n      \"users-between-lines\"\n    ]\n  },\n  \"users-gear\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users gear\",\n    \"terms\": [\n      \"users-cog\",\n      \"Users gear\",\n      \"users-gear\"\n    ]\n  },\n  \"users-line\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users Line\",\n    \"terms\": [\n      \"Users Line\",\n      \"users-line\"\n    ]\n  },\n  \"users-rays\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users Rays\",\n    \"terms\": [\n      \"Users Rays\",\n      \"users-rays\"\n    ]\n  },\n  \"users-rectangle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users Rectangle\",\n    \"terms\": [\n      \"Users Rectangle\",\n      \"users-rectangle\"\n    ]\n  },\n  \"users-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users Slash\",\n    \"terms\": [\n      \"Users Slash\",\n      \"users-slash\"\n    ]\n  },\n  \"users-viewfinder\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Users Viewfinder\",\n    \"terms\": [\n      \"Users Viewfinder\",\n      \"users-viewfinder\"\n    ]\n  },\n  \"usps\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"United States Postal Service\",\n    \"terms\": [\n      \"United States Postal Service\",\n      \"usps\"\n    ]\n  },\n  \"ussunnah\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"us-Sunnah Foundation\",\n    \"terms\": [\n      \"us-Sunnah Foundation\",\n      \"ussunnah\"\n    ]\n  },\n  \"utensils\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Utensils\",\n    \"terms\": [\n      \"cutlery\",\n      \"Utensils\",\n      \"utensils\"\n    ]\n  },\n  \"v\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"V\",\n    \"terms\": [\n      \"V\",\n      \"v\"\n    ]\n  },\n  \"vaadin\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Vaadin\",\n    \"terms\": [\n      \"Vaadin\",\n      \"vaadin\"\n    ]\n  },\n  \"van-shuttle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Van shuttle\",\n    \"terms\": [\n      \"shuttle-van\",\n      \"Van shuttle\",\n      \"van-shuttle\"\n    ]\n  },\n  \"vault\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Vault\",\n    \"terms\": [\n      \"Vault\",\n      \"vault\"\n    ]\n  },\n  \"vector-square\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Vector Square\",\n    \"terms\": [\n      \"Vector Square\",\n      \"vector-square\"\n    ]\n  },\n  \"venus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Venus\",\n    \"terms\": [\n      \"Venus\",\n      \"venus\"\n    ]\n  },\n  \"venus-double\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Venus Double\",\n    \"terms\": [\n      \"Venus Double\",\n      \"venus-double\"\n    ]\n  },\n  \"venus-mars\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Venus Mars\",\n    \"terms\": [\n      \"Venus Mars\",\n      \"venus-mars\"\n    ]\n  },\n  \"vest\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"vest\",\n    \"terms\": [\n      \"vest\",\n      \"vest\"\n    ]\n  },\n  \"vest-patches\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"vest-patches\",\n    \"terms\": [\n      \"vest-patches\",\n      \"vest-patches\"\n    ]\n  },\n  \"viacoin\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Viacoin\",\n    \"terms\": [\n      \"Viacoin\",\n      \"viacoin\"\n    ]\n  },\n  \"viadeo\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Viadeo\",\n    \"terms\": [\n      \"Viadeo\",\n      \"viadeo\"\n    ]\n  },\n  \"vial\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Vial\",\n    \"terms\": [\n      \"Vial\",\n      \"vial\"\n    ]\n  },\n  \"vial-circle-check\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Vial Circle-check\",\n    \"terms\": [\n      \"Vial Circle-check\",\n      \"vial-circle-check\"\n    ]\n  },\n  \"vial-virus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Vial Virus\",\n    \"terms\": [\n      \"Vial Virus\",\n      \"vial-virus\"\n    ]\n  },\n  \"vials\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Vials\",\n    \"terms\": [\n      \"Vials\",\n      \"vials\"\n    ]\n  },\n  \"viber\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Viber\",\n    \"terms\": [\n      \"Viber\",\n      \"viber\"\n    ]\n  },\n  \"video\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Video\",\n    \"terms\": [\n      \"video-camera\",\n      \"Video\",\n      \"video\"\n    ]\n  },\n  \"video-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Video Slash\",\n    \"terms\": [\n      \"Video Slash\",\n      \"video-slash\"\n    ]\n  },\n  \"vihara\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Vihara\",\n    \"terms\": [\n      \"Vihara\",\n      \"vihara\"\n    ]\n  },\n  \"vimeo\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Vimeo\",\n    \"terms\": [\n      \"Vimeo\",\n      \"vimeo\"\n    ]\n  },\n  \"vimeo-v\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Vimeo\",\n    \"terms\": [\n      \"Vimeo\",\n      \"vimeo-v\"\n    ]\n  },\n  \"vine\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Vine\",\n    \"terms\": [\n      \"Vine\",\n      \"vine\"\n    ]\n  },\n  \"virus\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Virus\",\n    \"terms\": [\n      \"Virus\",\n      \"virus\"\n    ]\n  },\n  \"virus-covid\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Virus Covid\",\n    \"terms\": [\n      \"Virus Covid\",\n      \"virus-covid\"\n    ]\n  },\n  \"virus-covid-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Virus Covid-slash\",\n    \"terms\": [\n      \"Virus Covid-slash\",\n      \"virus-covid-slash\"\n    ]\n  },\n  \"virus-slash\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Virus Slash\",\n    \"terms\": [\n      \"Virus Slash\",\n      \"virus-slash\"\n    ]\n  },\n  \"viruses\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Viruses\",\n    \"terms\": [\n      \"Viruses\",\n      \"viruses\"\n    ]\n  },\n  \"vk\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"VK\",\n    \"terms\": [\n      \"VK\",\n      \"vk\"\n    ]\n  },\n  \"vnv\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"VNV\",\n    \"terms\": [\n      \"VNV\",\n      \"vnv\"\n    ]\n  },\n  \"voicemail\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Voicemail\",\n    \"terms\": [\n      \"Voicemail\",\n      \"voicemail\"\n    ]\n  },\n  \"volcano\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Volcano\",\n    \"terms\": [\n      \"Volcano\",\n      \"volcano\"\n    ]\n  },\n  \"volleyball\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Volleyball Ball\",\n    \"terms\": [\n      \"volleyball-ball\",\n      \"Volleyball Ball\",\n      \"volleyball\"\n    ]\n  },\n  \"volume-high\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Volume high\",\n    \"terms\": [\n      \"volume-up\",\n      \"Volume high\",\n      \"volume-high\"\n    ]\n  },\n  \"volume-low\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Volume low\",\n    \"terms\": [\n      \"volume-down\",\n      \"Volume low\",\n      \"volume-low\"\n    ]\n  },\n  \"volume-off\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Volume Off\",\n    \"terms\": [\n      \"Volume Off\",\n      \"volume-off\"\n    ]\n  },\n  \"volume-xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Volume X Mark\",\n    \"terms\": [\n      \"volume-mute\",\n      \"volume-times\",\n      \"Volume X Mark\",\n      \"volume-xmark\"\n    ]\n  },\n  \"vr-cardboard\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Cardboard VR\",\n    \"terms\": [\n      \"Cardboard VR\",\n      \"vr-cardboard\"\n    ]\n  },\n  \"vuejs\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Vue.js\",\n    \"terms\": [\n      \"Vue.js\",\n      \"vuejs\"\n    ]\n  },\n  \"w\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"W\",\n    \"terms\": [\n      \"W\",\n      \"w\"\n    ]\n  },\n  \"walkie-talkie\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Walkie Talkie\",\n    \"terms\": [\n      \"Walkie Talkie\",\n      \"walkie-talkie\"\n    ]\n  },\n  \"wallet\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wallet\",\n    \"terms\": [\n      \"Wallet\",\n      \"wallet\"\n    ]\n  },\n  \"wand-magic\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wand magic\",\n    \"terms\": [\n      \"magic\",\n      \"Wand magic\",\n      \"wand-magic\"\n    ]\n  },\n  \"wand-magic-sparkles\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wand magic sparkles\",\n    \"terms\": [\n      \"magic-wand-sparkles\",\n      \"Wand magic sparkles\",\n      \"wand-magic-sparkles\"\n    ]\n  },\n  \"wand-sparkles\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wand sparkles\",\n    \"terms\": [\n      \"Wand sparkles\",\n      \"wand-sparkles\"\n    ]\n  },\n  \"warehouse\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Warehouse\",\n    \"terms\": [\n      \"Warehouse\",\n      \"warehouse\"\n    ]\n  },\n  \"watchman-monitoring\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Watchman Monitoring\",\n    \"terms\": [\n      \"Watchman Monitoring\",\n      \"watchman-monitoring\"\n    ]\n  },\n  \"water\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Water\",\n    \"terms\": [\n      \"Water\",\n      \"water\"\n    ]\n  },\n  \"water-ladder\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Water ladder\",\n    \"terms\": [\n      \"ladder-water\",\n      \"swimming-pool\",\n      \"Water ladder\",\n      \"water-ladder\"\n    ]\n  },\n  \"wave-square\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Square Wave\",\n    \"terms\": [\n      \"Square Wave\",\n      \"wave-square\"\n    ]\n  },\n  \"waze\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Waze\",\n    \"terms\": [\n      \"Waze\",\n      \"waze\"\n    ]\n  },\n  \"weebly\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Weebly\",\n    \"terms\": [\n      \"Weebly\",\n      \"weebly\"\n    ]\n  },\n  \"weibo\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Weibo\",\n    \"terms\": [\n      \"Weibo\",\n      \"weibo\"\n    ]\n  },\n  \"weight-hanging\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Hanging Weight\",\n    \"terms\": [\n      \"Hanging Weight\",\n      \"weight-hanging\"\n    ]\n  },\n  \"weight-scale\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Weight scale\",\n    \"terms\": [\n      \"weight\",\n      \"Weight scale\",\n      \"weight-scale\"\n    ]\n  },\n  \"weixin\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Weixin (WeChat)\",\n    \"terms\": [\n      \"Weixin (WeChat)\",\n      \"weixin\"\n    ]\n  },\n  \"whatsapp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"What's App\",\n    \"terms\": [\n      \"What's App\",\n      \"whatsapp\"\n    ]\n  },\n  \"wheat-awn\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wheat awn\",\n    \"terms\": [\n      \"wheat-alt\",\n      \"Wheat awn\",\n      \"wheat-awn\"\n    ]\n  },\n  \"wheat-awn-circle-exclamation\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wheat Awn-circle-exclamation\",\n    \"terms\": [\n      \"Wheat Awn-circle-exclamation\",\n      \"wheat-awn-circle-exclamation\"\n    ]\n  },\n  \"wheelchair\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wheelchair\",\n    \"terms\": [\n      \"Wheelchair\",\n      \"wheelchair\"\n    ]\n  },\n  \"wheelchair-move\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wheelchair Move\",\n    \"terms\": [\n      \"wheelchair-alt\",\n      \"Wheelchair Move\",\n      \"wheelchair-move\"\n    ]\n  },\n  \"whiskey-glass\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Whiskey glass\",\n    \"terms\": [\n      \"glass-whiskey\",\n      \"Whiskey glass\",\n      \"whiskey-glass\"\n    ]\n  },\n  \"whmcs\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"WHMCS\",\n    \"terms\": [\n      \"WHMCS\",\n      \"whmcs\"\n    ]\n  },\n  \"wifi\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"WiFi\",\n    \"terms\": [\n      \"wifi-3\",\n      \"wifi-strong\",\n      \"WiFi\",\n      \"wifi\"\n    ]\n  },\n  \"wikipedia-w\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Wikipedia W\",\n    \"terms\": [\n      \"Wikipedia W\",\n      \"wikipedia-w\"\n    ]\n  },\n  \"wind\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wind\",\n    \"terms\": [\n      \"Wind\",\n      \"wind\"\n    ]\n  },\n  \"window-maximize\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Window Maximize\",\n    \"terms\": [\n      \"Window Maximize\",\n      \"window-maximize\"\n    ]\n  },\n  \"window-minimize\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Window Minimize\",\n    \"terms\": [\n      \"Window Minimize\",\n      \"window-minimize\"\n    ]\n  },\n  \"window-restore\": {\n    \"styles\": [\n      \"solid\",\n      \"regular\"\n    ],\n    \"label\": \"Window Restore\",\n    \"terms\": [\n      \"Window Restore\",\n      \"window-restore\"\n    ]\n  },\n  \"windows\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Windows\",\n    \"terms\": [\n      \"Windows\",\n      \"windows\"\n    ]\n  },\n  \"wine-bottle\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wine Bottle\",\n    \"terms\": [\n      \"Wine Bottle\",\n      \"wine-bottle\"\n    ]\n  },\n  \"wine-glass\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wine Glass\",\n    \"terms\": [\n      \"Wine Glass\",\n      \"wine-glass\"\n    ]\n  },\n  \"wine-glass-empty\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wine glass empty\",\n    \"terms\": [\n      \"wine-glass-alt\",\n      \"Wine glass empty\",\n      \"wine-glass-empty\"\n    ]\n  },\n  \"wirsindhandwerk\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"wirsindhandwerk\",\n    \"terms\": [\n      \"wsh\",\n      \"wirsindhandwerk\",\n      \"wirsindhandwerk\"\n    ]\n  },\n  \"wix\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Wix\",\n    \"terms\": [\n      \"Wix\",\n      \"wix\"\n    ]\n  },\n  \"wizards-of-the-coast\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Wizards of the Coast\",\n    \"terms\": [\n      \"Wizards of the Coast\",\n      \"wizards-of-the-coast\"\n    ]\n  },\n  \"wodu\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Wodu\",\n    \"terms\": [\n      \"Wodu\",\n      \"wodu\"\n    ]\n  },\n  \"wolf-pack-battalion\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Wolf Pack Battalion\",\n    \"terms\": [\n      \"Wolf Pack Battalion\",\n      \"wolf-pack-battalion\"\n    ]\n  },\n  \"won-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Won Sign\",\n    \"terms\": [\n      \"krw\",\n      \"won\",\n      \"Won Sign\",\n      \"won-sign\"\n    ]\n  },\n  \"wordpress\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"WordPress Logo\",\n    \"terms\": [\n      \"WordPress Logo\",\n      \"wordpress\"\n    ]\n  },\n  \"wordpress-simple\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Wordpress Simple\",\n    \"terms\": [\n      \"Wordpress Simple\",\n      \"wordpress-simple\"\n    ]\n  },\n  \"worm\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Worm\",\n    \"terms\": [\n      \"Worm\",\n      \"worm\"\n    ]\n  },\n  \"wpbeginner\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"WPBeginner\",\n    \"terms\": [\n      \"WPBeginner\",\n      \"wpbeginner\"\n    ]\n  },\n  \"wpexplorer\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"WPExplorer\",\n    \"terms\": [\n      \"WPExplorer\",\n      \"wpexplorer\"\n    ]\n  },\n  \"wpforms\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"WPForms\",\n    \"terms\": [\n      \"WPForms\",\n      \"wpforms\"\n    ]\n  },\n  \"wpressr\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"wpressr\",\n    \"terms\": [\n      \"rendact\",\n      \"wpressr\",\n      \"wpressr\"\n    ]\n  },\n  \"wrench\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Wrench\",\n    \"terms\": [\n      \"Wrench\",\n      \"wrench\"\n    ]\n  },\n  \"x\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"X\",\n    \"terms\": [\n      \"X\",\n      \"x\"\n    ]\n  },\n  \"x-ray\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"X-Ray\",\n    \"terms\": [\n      \"X-Ray\",\n      \"x-ray\"\n    ]\n  },\n  \"xbox\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Xbox\",\n    \"terms\": [\n      \"Xbox\",\n      \"xbox\"\n    ]\n  },\n  \"xing\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Xing\",\n    \"terms\": [\n      \"Xing\",\n      \"xing\"\n    ]\n  },\n  \"xmark\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"X Mark\",\n    \"terms\": [\n      \"close\",\n      \"multiply\",\n      \"remove\",\n      \"times\",\n      \"X Mark\",\n      \"xmark\"\n    ]\n  },\n  \"xmarks-lines\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Xmarks Lines\",\n    \"terms\": [\n      \"Xmarks Lines\",\n      \"xmarks-lines\"\n    ]\n  },\n  \"y\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Y\",\n    \"terms\": [\n      \"Y\",\n      \"y\"\n    ]\n  },\n  \"y-combinator\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Y Combinator\",\n    \"terms\": [\n      \"Y Combinator\",\n      \"y-combinator\"\n    ]\n  },\n  \"yahoo\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Yahoo Logo\",\n    \"terms\": [\n      \"Yahoo Logo\",\n      \"yahoo\"\n    ]\n  },\n  \"yammer\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Yammer\",\n    \"terms\": [\n      \"Yammer\",\n      \"yammer\"\n    ]\n  },\n  \"yandex\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Yandex\",\n    \"terms\": [\n      \"Yandex\",\n      \"yandex\"\n    ]\n  },\n  \"yandex-international\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Yandex International\",\n    \"terms\": [\n      \"Yandex International\",\n      \"yandex-international\"\n    ]\n  },\n  \"yarn\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Yarn\",\n    \"terms\": [\n      \"Yarn\",\n      \"yarn\"\n    ]\n  },\n  \"yelp\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Yelp\",\n    \"terms\": [\n      \"Yelp\",\n      \"yelp\"\n    ]\n  },\n  \"yen-sign\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Yen Sign\",\n    \"terms\": [\n      \"cny\",\n      \"jpy\",\n      \"rmb\",\n      \"yen\",\n      \"Yen Sign\",\n      \"yen-sign\"\n    ]\n  },\n  \"yin-yang\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Yin Yang\",\n    \"terms\": [\n      \"Yin Yang\",\n      \"yin-yang\"\n    ]\n  },\n  \"yoast\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Yoast\",\n    \"terms\": [\n      \"Yoast\",\n      \"yoast\"\n    ]\n  },\n  \"youtube\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"YouTube\",\n    \"terms\": [\n      \"YouTube\",\n      \"youtube\"\n    ]\n  },\n  \"z\": {\n    \"styles\": [\n      \"solid\"\n    ],\n    \"label\": \"Z\",\n    \"terms\": [\n      \"Z\",\n      \"z\"\n    ]\n  },\n  \"zhihu\": {\n    \"styles\": [\n      \"brands\"\n    ],\n    \"label\": \"Zhihu\",\n    \"terms\": [\n      \"Zhihu\",\n      \"zhihu\"\n    ]\n  }\n}"
  },
  {
    "path": "packages/icons/.eslintrc.js",
    "content": "const config = require( '../scripts/config/.eslintrc.js' );\nmodule.exports = config;\n"
  },
  {
    "path": "packages/icons/.llmsdev.yml",
    "content": "update-version:\n  skip-config: true\n"
  },
  {
    "path": "packages/icons/.npmrc",
    "content": "package-lock=false\nengine-strict=true\n"
  },
  {
    "path": "packages/icons/CHANGELOG.md",
    "content": "LifterLMS Icons\n===============\n\nv1.0.0 - 2022-03-08\n-------------------\n\n+ Initial public release.\n"
  },
  {
    "path": "packages/icons/README.md",
    "content": "LifterLMS Icons\n---------------\n\nLifterLMS Icon library.\n\n---\n\n## CHANGELOG\n\n[CHANGELOG](./CHANGELOG.md)\n\n\n## Installation\n\n```bash\nnpm install --save @lifterlms/icons`\n```\n\n\n## Usage\n\n```jsx\nimport { Icon, lifterlms } from '@lifterlms/icons';\n\n<Icon icon={ lifterlms } />;\n```\n\n\n## LifterLMS Versions\n\nThe following table records the module version and which LifterLMS versions it has been included in.\n\n| Module Version | LifterLMS Version |\n| -------------- | ----------------- |\n| 1.0.0          | 6.0.0             |\n\n\n## Gallery\n\n<!-- START TOKEN(Autogenerated Icon Gallery) -->\n\n<table>\n\t<thead>\n\t\t<tr>\n\t\t\t<th>Icon</th>\n\t\t\t<th>ID</th>\n\t\t\t<th>Usage</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t<tr>\n\t\t\t<td><img src=\"raw/lifterlms.svg\" width=\"48\" height=\"48\" alt=\"lifterlms icon\" /></td>\n\t\t\t<td>lifterlms</td>\n\t\t\t<td><code>&lt;Icon icon={ lifterlms } /&gt;</code></td>\n\t\t</tr>\n\t</tbody>\n</table>\n\n<!-- END TOKEN(Autogenerated Icon Gallery) -->\n"
  },
  {
    "path": "packages/icons/babel.config.js",
    "content": "/**\n * Babel config\n *\n * @since 1.0.0\n * @version 1.0.0\n */\n\nmodule.exports = {\n\tpresets: [ '@babel/preset-env', '@babel/preset-react' ],\n};\n"
  },
  {
    "path": "packages/icons/docs/app.js",
    "content": "import { render } from 'react-dom';\nimport * as Icons from '../src';\n\nconst { Icon, ...icons } = Icons;\n\n/**\n * React App that automatically renders all icons in the library into a static file.\n *\n * This app is rendered as a static HTML file and generate.js pulls the HTML the icons\n * and stores them as raw SVG files for display use in the README.md file between\n * the appropriate comment tokens.\n *\n * @since 1.0.0\n *\n * @return {Object} React component fragment.\n */\nfunction App() {\n\treturn (\n\t\t<>\n\t\t\t{ Object.entries( icons ).map( ( [ id, icon ] ) => {\n\t\t\t\treturn (\n\t\t\t\t\t<div key={ id } id={ id }>\n\t\t\t\t\t\t<Icon icon={ icon } size=\"48\" />\n\t\t\t\t\t</div>\n\t\t\t\t);\n\t\t\t} ) }\n\t\t</>\n\t);\n}\nrender( <App />, document.getElementById( 'app' ) );\n"
  },
  {
    "path": "packages/icons/docs/generate.js",
    "content": "#!/usr/bin/node\n\n/**\n * Docgen script.\n *\n * After creating a temporary snapshot of the icon library React app, this script\n * parses the static HTML and adds the icon table to the README.md file in the\n * package root.\n */\n\nconst cheerio = require( 'cheerio' ),\n\t{ readFileSync, writeFileSync, rmSync, mkdirSync } = require( 'fs' ),\n\tBUILD_DIR = 'raw';\n\nrmSync( BUILD_DIR, { force: true, recursive: true } );\nmkdirSync( BUILD_DIR );\n\nconst $ = cheerio.load( readFileSync( 'docs/build/index.html', 'utf8' ) );\n\nlet docs = `<table>\n\t<thead>\n\t\t<tr>\n\t\t\t<th>Icon</th>\n\t\t\t<th>ID</th>\n\t\t\t<th>Usage</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>`;\n\n$( '#app div' ).each( ( i, el ) => {\n\tconst id = $( el ).attr( 'id' );\n\twriteFileSync( `${ BUILD_DIR }/${ id }.svg`, $( el ).html() );\n\n\tdocs += `\n\t\t<tr>\n\t\t\t<td><img src=\"${ BUILD_DIR }/${ id }.svg\" width=\"48\" height=\"48\" alt=\"${ id } icon\" /></td>\n\t\t\t<td>${ id }</td>\n\t\t\t<td><code>&lt;Icon icon={ ${ id } } /&gt;</code></td>\n\t\t</tr>\n\t`;\n} );\n\ndocs += `</tbody>\n</table>`;\n\nconst readmeFile = 'README.md',\n\treadmeContents = readFileSync( readmeFile, 'utf8' ),\n\tstartToken = '<!-- START TOKEN(Autogenerated Icon Gallery) -->',\n\tendToken = '<!-- END TOKEN(Autogenerated Icon Gallery) -->',\n\tdocsToken = '<!-- DOCS TOKEN -->';\n\nlet newReadme = [],\n\taddLine = true;\n\nreadmeContents.split( '\\n' ).forEach( ( line ) => {\n\tif ( line === startToken ) {\n\t\tnewReadme.push( line );\n\t\tnewReadme.push( docsToken );\n\t\taddLine = false;\n\t} else if ( line === endToken ) {\n\t\taddLine = true;\n\t}\n\n\tif ( addLine ) {\n\t\tnewReadme.push( line );\n\t}\n} );\n\nnewReadme = newReadme.join( '\\n' );\n\nwriteFileSync( readmeFile, newReadme.replace( docsToken, `\\n${ docs }\\n` ) );\n"
  },
  {
    "path": "packages/icons/docs/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t\t<title>@lifterlms/icons</title>\n\t\t<script src=\"https://unpkg.com/react@17/umd/react.production.min.js\" crossorigin></script>\n\t\t<script src=\"https://unpkg.com/react-dom@17/umd/react-dom.production.min.js\" crossorigin></script>\n\t</head>\n\t<body>\n\t\t<div id=\"app\"></div>\n\t\t<script src=\"bundle.js\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "packages/icons/package.json",
    "content": "{\n  \"name\": \"@lifterlms/icons\",\n  \"version\": \"1.0.1-alpha.2\",\n  \"description\": \"Icon library for LifterLMS\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/master/packages/icons\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"scripts\",\n    \"utils\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/icons\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%icons\"\n  },\n  \"main\": \"src/index.js\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"@wordpress/icons\": \"^6.1.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.16.5\",\n    \"babelify\": \"^10.0.0\",\n    \"browserify\": \"^17.0.0\",\n    \"cheerio\": \"^1.0.0-rc.10\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-snap\": \"^1.23.0\"\n  },\n  \"reactSnap\": {\n    \"source\": \"docs\",\n    \"destination\": \"docs/build\"\n  },\n  \"scripts\": {\n    \"docgen\": \"browserify docs/app.js -t [ babelify --presets [ @babel/preset-env @babel/preset-react ] ] > docs/bundle.js && react-snap && node docs/generate.js && rm -rf docs/build && rm docs/200.html && rm docs/bundle.js\",\n    \"dev\": \"./../dev/src/index.js\",\n    \"lint:js\": \"wp-scripts lint-js ./ --config ../../.eslintrc.js\"\n  }\n}\n"
  },
  {
    "path": "packages/icons/src/index.js",
    "content": "export { Icon } from '@wordpress/icons';\n\nexport { default as lifterlms } from './lifterlms';\n"
  },
  {
    "path": "packages/icons/src/lifterlms.js",
    "content": "import { SVG, G, Path } from '@wordpress/primitives';\n\nconst lifterlms = (\n\t<SVG\n\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\tviewBox=\"0 0 85 85\"\n\t\tstyle={ {\n\t\t\tfillRule: 'evenodd',\n\t\t\tclipRule: 'evenodd',\n\t\t\tstrokeLinejoin: 'round',\n\t\t\tstrokeMiterlimit: 1.41421,\n\t\t} }\n\t>\n\t\t<G id=\"lifterlms-icon\">\n\t\t\t<Path d=\"M29.061,50.631l-2.258,-1.29l-6.066,10.452c-5.483,-7.613 -6.58,-17.873 -2.322,-26.712l0.064,-0.065c0.258,-0.581 0.581,-1.097 0.839,-1.613c4.323,-7.485 11.873,-12.067 19.873,-12.905c1.42,-1.935 2.969,-3.614 4.711,-5.226c-11.421,-0.645 -22.843,5.032 -28.972,15.615c-7.872,13.679 -4.258,30.841 7.872,40.263l6.065,-18.003c0.065,-0.128 0.13,-0.323 0.194,-0.516m36.908,-16.712c3.227,7.421 3.033,16.195 -1.291,23.681c-0.257,0.516 -0.58,1.031 -0.903,1.548l-0.064,0.066c-5.549,8.129 -14.97,12.323 -24.326,11.355l6.066,-10.453l-2.259,-1.291c-0.129,0.13 -0.258,0.259 -0.387,0.389l-12.518,14.259c14.196,5.808 30.907,0.323 38.779,-13.357c6.13,-10.581 5.356,-23.293 -0.967,-32.842c-0.517,2.257 -1.162,4.516 -2.13,6.645\" />\n\t\t\t<Path d=\"M44.999,50.243c-1.614,2.13 -4.194,3.228 -6.968,3.485c-0.839,0.065 -1.614,-0.387 -2.001,-1.161c-1.162,-2.517 -1.548,-5.291 -0.451,-7.743l-12.648,-7.291c-0.838,-0.516 -1.225,-1.356 -0.967,-2.258c0.193,-0.904 0.967,-1.55 1.871,-1.55l12.84,-0.451c0.968,-3.936 2.581,-7.678 4.904,-11.163c3.678,-5.484 8.904,-9.549 15.034,-12.001c1.485,-0.581 2.968,-1.096 4.453,-1.484c1.096,-0.258 2.193,0.388 2.451,1.421c0.452,1.482 0.775,3.031 1.033,4.579c0.903,6.582 -0.065,13.163 -2.903,19.099c-1.807,3.743 -4.324,6.97 -7.228,9.808l6.001,11.292c0.452,0.839 0.323,1.807 -0.387,2.452c-0.645,0.645 -1.614,0.71 -2.387,0.258l-12.647,-7.292Zm9.549,-27.035c1.936,1.162 2.581,3.614 1.485,5.549c-1.098,1.936 -3.613,2.582 -5.55,1.485c-1.935,-1.098 -2.58,-3.614 -1.484,-5.55c1.162,-1.935 3.614,-2.581 5.549,-1.484\" />\n\t\t\t<Path d=\"M26.093,72.118l13.679,-15.551c-0.516,0.065 -1.032,0.129 -1.549,0.194c-2.064,0.129 -4,-0.968 -4.902,-2.903c-0.259,-0.452 -0.453,-0.904 -0.646,-1.42l-6.582,19.68Z\" />\n\t\t</G>\n\t</SVG>\n);\n\nexport default lifterlms;\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/.eslintrc.js",
    "content": "module.exports = {\n\tglobals: {\n\t\tpage: 'readonly',\n\t},\n};\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/.llmsdev.yml",
    "content": "update-version:\n  skip-config: true\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/.npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/CHANGELOG.md",
    "content": "LifterLMS E2E Test Utils Changelog\n==================================\n\nv4.0.1 - 2022-10-12\n-------------------\n\n+ Update: `activateTheme()` to remove reliance on `@wordpress/e2e-test-utils` `activateTheme()`.\n\n\nv4.0.0 - 2022-03-30\n-------------------\n\n+ **Breaking**: Upgraded `@wordpress/e2e-test-utils` to [v7.0.0](https://github.com/WordPress/gutenberg/blob/trunk/packages/e2e-test-utils/CHANGELOG.md#700-2022-03-11).\n\n\nv3.3.0 - 2022-03-08\n-------------------\n\n+ New functions:\n  + `activateTheme()`\n  + `getPostTitleSelector()`\n  + `getPostTitleTextContent()`\n  + `openSidebarPanelTab()`\n  + `publishPost()`\n  + `toggleSidebarPanel()`\n  + `updatePost()`\n  + `visitPostPermalink()`\n+ Update: `createCertificate()` now creates certificates in the block editor.\n+ Update: `createPost()` now sets the post's content programmatically in favor of passing it through a query string variable.\n+ Update: `fillField()` now waits for the selector prior to focusing on it.\n+ Update: `clickAndWait()` now returns a `Promise` in favor of void.\n\n\nv3.2.0 - 2022-01-31\n-------------------\n\n+ Added: New function `wpVersionCompare()` used to run version comparisons against the currently tested version of WordPress.\n+ Fixed: Tests failing when running `runSetupWizard()` on WordPress >= 5.9.\n+ Update: `@wordpress/e2e-test-utils` to version [6.0.0](https://github.com/WordPress/gutenberg/blob/trunk/packages/e2e-test-utils/CHANGELOG.md#600-2022-01-27).\n\n\nv3.1.0 - 2021-12-07\n-------------------\n\n+ Added new functions `highlightNode()` and `setCheckboxSetting()`.\n\n\nv3.0.0 - 2021-11-05\n-------------------\n\n+ **[Breaking]** Minimum required Puppeteer version raised from 3.0.0 to 5.3.0.\n+ Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n+ Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown to close after selecting an option.\n\n\nv3.0.0-beta.1 - 2021-09-10\n--------------------------\n\n+ **[Breaking]** Minimum required Puppeteer version raised from 3.0.0 to 5.3.0.\n+ Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n+ Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown to close after selecting an option.\n\n\nv2.3.1 - 2021-10-05\n-------------------\n\n+ Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown to close after selecting an option.\n\n\nv2.3.0 - 2021-06-22\n-------------------\n\n+ Bugfix: Focus on the search selector prior to typing in select2 search fields.\n\n\nv2.2.2 - 2021-02-04\n-------------------\n\n+ `click()` now always uses `waitForSelector()`. before clicking the element.\n+ Use `waitForSelector()` in favor of `waitFor()` when creating access plans.\n\n\nv2.2.1 - 2021-01-19\n-------------------\n\n+ Options object is now optional for the createUser() function.\n+ Added `args.voucher` to enable voucher usage during registration via the registerStudent() function.\n\n\nv2.2.0 - 2020-11-16\n-------------------\n\n+ `createCourse()` now uses generic `createPost()` for course creation.\n+ `createUser()` now returns the WP_User ID in the return object.\n+ `importCourse()` has been updated to accommodate changes in LifterLMS core version 4.8.0.\n+ `runSetupWizard()` has been updated to accommodate setup wizard changes in LifterLMS core version 4.8.0.\n\n\nv2.1.3 - 2020-08-06\n-------------------\n+ `logoutUser()`: Wait 1 second before navigating to logout page.\n+ `visitSettingsPage()`: Don't add null values to the query string.\n\nv2.1.1 - 2020-08-06\n-------------------\n\n+ `createCourse()` now uses `createPost()`.\n+ `createUser()` will now return the `WP_User` ID of the created user.\n\n+ Added new utility functions:\n\n  + `createMembership()`: Create and publish a new membership.\n  + `createPost()`: Create a publish a new post (of a defined post type).\n  + `enrollStudent()`: Enroll a user account into a course or membership.\n  + `importCourse()`: Import a course export file into the test environment.\n  + `setSelect2Option()`: Set the value of a select field powered by select2.js\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/README.md",
    "content": "# LifterLMS E2E Test Utilities\n\nEnd-To-End (E2E) test utilities for LifterLMS (and WordPress). This package extends functionality provided by [@wordpress/e2e-test-utils](https://github.com/WordPress/gutenberg/tree/master/packages/e2e-test-utils), adding functionality specifically for testing LifterLMS projects and add-ons.\n\n## Installation\n\nInstall the module\n\n```bash\nnpm install --save-dev @lifterlms/llms-e2e-test-utils`\n```\n\n## Changelog\n\n[View the Changelog](./CHANGELOG.md)\n\n## API Docs\n\n<!-- START TOKEN(Autogenerated API docs) -->\n\n### activateTheme\n\nActivates a theme.\n\n_Parameters_\n\n-   _theme_ `?string`: Accepts a theme slug. If not supplied, loads the default theme for the tested WP version.\n\n_Returns_\n\n-   `Promise`: Promise that resolves when the theme is activated.\n\n### clearBlocks\n\nDeletes all blocks in the editor.\n\n_Returns_\n\n-   `Promise`: Promise from page.evaluate().\n\n### click\n\nClick an elements by selector\n\n_Parameters_\n\n-   _selector_ `string`: Element selector string.\n\n_Returns_\n\n-   `void`: \n\n### clickAndWait\n\nClick an element and wait for navigation.\n\n_Parameters_\n\n-   _selector_ `string`: Query selector for the DOM element to click.\n-   _waitUntil_ `string`: Network connection to wait for, defaults to 'networkidle2'.\n\n_Returns_\n\n-   `void`: \n\n### clickElementByText\n\nClick an element by Text\n\n_Parameters_\n\n-   _string_ `string`: Case-insensitive string to search.\n-   _selector_ `string`: Selector to search. Default \"\\*\".\n\n_Returns_\n\n-   `void`: \n\n### createAccessPlan\n\nCreate and publish a new course\n\n_Parameters_\n\n-   _args_ `Object`: Creation arguments.\n-   _args.postId_ `number`: Post ID of the plan's course or membership.\n-   _args.price_ `number`: Plan price.\n-   _args.title_ `string`: Plan title.\n\n_Returns_\n\n-   `string`: The created plan's purchase link URL.\n\n### createCertificate\n\nCreate and publish a new certificate\n\n_Type_\n\n-   `number`certificateId    WP Post ID of the created certificate post.\n-   `number`engagementId    WP Post ID of the created engagement post. }\n\n_Parameters_\n\n-   _args_ `Object`: Optional creation arguments.\n-   _args.title_ `string`: Certificate title.\n-   _args.content_ `string`: HTML content of the certificate.\n-   _args.adminTitle_ `string`: Admin title.\n-   _args.engagement_ `string`: If supplied, also creates an engagement trigger. This should be the ID of a trigger\n\n_Returns_\n\n-   `Object`: { Object containing information about the created post(s).\n\n### createCoupon\n\nCreate and publish a new course\n\n_Parameters_\n\n-   _args_ `Object`: Creation arguments.\n-   _args.code_ `string`: Coupon code (post title).\n-   _args.discount_ `string`: The discount amount with either a leading `$` to specify dollar amount discounts or a trailing `%` for percentage discounts.\n\n_Returns_\n\n-   `string`: The coupon code.\n\n### createCourse\n\nCreate and publish a new course\n\n_Parameters_\n\n-   _title_ `string`: Course title.\n\n_Returns_\n\n-   `number`: The created course's WP_Post ID.\n\n### createEngagement\n\nCreate and publish a new certificate\n\n_Parameters_\n\n-   _engagementId_ `number`: WP_Post ID of the a certificate, email, or achievement post.\n-   _args_ `Object`: Optional creation arguments.\n-   _args.title_ `string`: Engagement title.\n-   _args.trigger_ `string`: ID of the engagement trigger event.\n-   _args.type_ `string`: Engagement type: certificate, email, or achievement.\n-   _args.delay_ `number`: Engagement delay, in days.\n\n_Returns_\n\n-   `number`: WP Post ID of the created certificate post.\n\n### createMembership\n\nCreate and publish a new membership\n\n_Parameters_\n\n-   _title_ `string`: Membership title.\n\n_Returns_\n\n-   `number`: The created membership's WP_Post ID.\n\n### createPost\n\nCreate and publish a new post\n\n_Parameters_\n\n-   _postType_ `string`: WP_Post type.\n-   _title_ `string`: Post title.\n-   _content_ `?string`: Post content.\n\n_Returns_\n\n-   `number`: The created post's WP_Post ID.\n\n### createUser\n\nCreate a new user.\n\n_Parameters_\n\n-   _opts_ `Object`: Hash of user information used to create the new user.\n\n_Returns_\n\n-   `Object`: Object of created user data.\n\n### createVoucher\n\nCreate and publish a new course\n\n_Parameters_\n\n-   _args_ `Object`: Creation arguments.\n-   _args.name_ `string`: Voucher (post) title.\n-   _args.course_ `string`: Name of a course to add to the voucher.\n-   _args.membership_ `string`: Name of a membership to add to the voucher.\n-   _args.codes_ `number`: Number of codes to generate.\n-   _args.uses_ `number`: Number of uses per code.\n\n_Returns_\n\n-   `string[]`: Array of the generated voucher codes.\n\n### dismissEditorWelcomeGuide\n\nDismiss the \"Welcome Guide\" in the block editor (if it's active)\n\n_Returns_\n\n-   `void`: \n\n### enrollStudent\n\nEnroll a student into a course\n\nThis performs as \"manual\" enrollment using the enrollment\narea on the course or membership.\n\n_Parameters_\n\n-   _postId_ `number`: WP_Post ID.\n-   _studentId_ `number`: WP_User ID.\n\n_Returns_\n\n-   `void`: \n\n### fillField\n\nType text into a field identified by a selector.\n\n_Parameters_\n\n-   _selector_ `string`: Query selector to identify the field element.\n-   _text_ `string`: Text to type into the field.\n\n_Returns_\n\n-   `void`: \n\n### findElementByText\n\nFind an element by Text\n\n_Related_\n\n-\n\n_Parameters_\n\n-   _string_ `string`: Case-insensitive string to search.\n-   _selector_ `string`: Selector to search. Default \"\\*\".\n\n_Returns_\n\n-   `Array`: Element.\n\n### getAllBlocks\n\nRetrieves a list of blocks in the editor, with or without client IDs.\n\nSpecifying `withClientIds=false` allows using the resulting array of block\nobjects in snapshots without having to specify a snapshot matcher\nthat excludes (possibly nested) blocks with clientIds that will not\nmatch future test runs.\n\n_Parameters_\n\n-   _withClientIds_ `boolean`: Whether or not to exclude clientIds.\n\n_Returns_\n\n-   `Object[]`: Array of block objects.\n\n### getPostTitleSelector\n\nRetrieves the CSS selector for the post's title element.\n\nOn 5.9+ we're testing against the 2022 theme, on 5.8 & earlier we're using 2021.\n\n_Returns_\n\n-   `Promise`: A promise that resolves to return the element's text content.\n\n### getPostTitleTextContent\n\nRetrieves the textContent of the lesson post's title element.\n\nThis function uses a dynamically-determined selector based on the current WP version (and assumed theme)\nrun by default with that version.\n\n_Returns_\n\n-   `Promise`: A promise that resolves to return the element's text content.\n\n### getWPVersion\n\nRetrieve the WP_VERSION environment variable\n\nWhen running tests locally this will likely be undefined unless running tests with\n`WP_VERSION=5.7.2 npm run test`.\n\nThe WP_VERSION env var is defined during CI tests automatically and this function\nis generally used to determine conditionals based on the WP Core version.\n\nFor example: block editor selectors change between WP core version, some features\naren't available on older versions, etc...\n\n_Returns_\n\n-   `?string`: WordPress version or null if not set.\n\n### highlightNode\n\nHighlight (selects) the contents of a node.\n\n_Parameters_\n\n-   _selector_ `string`: Query selector.\n-   _copySelection_ `boolean`: If `true`, copies the selected text and returns it. The browser clipboard-read permission must be granted in order to read from the clipboard.\n\n_Returns_\n\n-   `boolean|string`: Returns the copied text or `true` if `copySelection` is `false`.\n\n### importCourse\n\nImport a course JSON file\n\n_Parameters_\n\n-   _importFile_ `string`: Filename of the import.\n-   _importPath_ `string`: Local path where the file is located. By default uses `tests/assets/`.\n-   _navigate_ `boolean`: Whether or not to automatically navigate to the imported course when done.\n\n_Returns_\n\n-   `void`: \n\n### loginStudent\n\nLogin a user via the LifterLMS student dashboard.\n\n_Parameters_\n\n-   _login_ `string`: User login or email address.\n-   _pass_ `string`: User password.\n\n_Returns_\n\n-   `void`: \n\n### logoutUser\n\nLogout the current user.\n\n_Returns_\n\n-   `Promise`: Promise which resolves after the user is logged out and the page reloaded.\n\n### openSidebarPanelTab\n\nOpens a sidebar panel tab if it's not already open.\n\n_Parameters_\n\n-   _tab_ `string`: Tab to select, accepts \"primary\" to select the main document settings tab or \"block\" to select the block tab.\n\n_Returns_\n\n-   `Promise`: A promise that resolves when the desired panel becomes active.\n\n### publishPost\n\nDisables prepublish checks and clicks the post publish button.\n\n_Returns_\n\n-   `Promise`: Promise which resolves when the close button element is successfully clicked.\n\n### registerStudent\n\nRegister a new student via the LifterLMS Open Registration Page\n\n_Type_\n\n-   `string`email     User's email address.\n-   `string`pass     User's password. }\n\n_Parameters_\n\n-   _args_ `Object`: Function arguments object.\n-   _args.email_ `string`: Email address. If not supplied one will be created from the first name and last name.\n-   _args.pass_ `string`: User password. If not supplied one will be automatically generated.\n-   _args.first_ `string`: User's first name.\n-   _args.last_ `string`: User's last name.\n-   _args.voucher_ `string`: Voucher code to use during registration.\n-   _args.address1_ `string`: User's address line 1.\n-   _args.address2_ `string`: User's address line 2.\n-   _args.city_ `string`: User's city.\n-   _args.country_ `string`: User's country.\n-   _args.state_ `string`: User's state.\n-   _args.postcode_ `string`: User's postcode.\n-   _args.phone_ `string`: User's phone.\n\n_Returns_\n\n-   `Object`: { Object containing information about the newly created user.\n\n### runSetupWizard\n\nRun (and test) the LifterLMS Setup Wizard\n\n_Parameters_\n\n-   _options_ `Object`: Options object.\n-   _options.coursesToImport_ `string[]`: Titles of the course(s) to import through the setup wizard. Pass a falsy to skip import and \"Start from Scratch\".\n-   _options.exit_ `boolean`: Whether or not to exit the setup wizard at the conclusion of setup. If `true`, uses the \"Exit\" link to leave setup.\\`\n\n_Returns_\n\n-   `void`: \n\n### select2Select\n\nSelect a value from a select2 dropdown field\n\n_Parameters_\n\n-   _selector_ `string`: Query selector for the select element.\n-   _value_ `string`: Option value to select.\n\n_Returns_\n\n-   `void`: \n\n### setCheckboxSetting\n\nToggles a LifterLMS checkbox setting.\n\n_Parameters_\n\n-   _selector_ `string`: Selector for the setting checkbox.\n-   _status_ `boolean`: Requested setting status. Use `true` for checked and `false` for unchecked.\n-   _save_ `boolean`: Whether or not to perform a save after updating the setting.\n\n_Returns_\n\n-   `void`: \n\n### setSelect2Option\n\nSet the value of a select2 dropdown field\n\nThis does not actually test whether or not select2 is working,\ninstead it selects the value on the select element and artificially\ntriggers a change event.\n\n_Parameters_\n\n-   _selector_ `string`: Query selector for the select element.\n-   _value_ `string`: Option value to select.\n-   _create_ `boolean`: If `true`, the value will be added to the select element before being selected. This is a useful option for AJAX powered select2 elements that will be empty until interacted with.\n\n_Returns_\n\n-   `void`: \n\n### toggleOpenRegistration\n\nToggles the open registration setting on or off\n\n_Parameters_\n\n-   _status_ `boolean`: Whether to toggle on (`true`) or off (`false`).\n\n_Returns_\n\n-   `void`: \n\n### toggleSidebarPanel\n\nOpens or closes an editor sidebar panel based on the panel's title.\n\n_Parameters_\n\n-   _title_ `string`: The panel title to open or close.\n-   _shouldBeOpen_ `boolean`: Whether or not the panel should be open.\n\n_Returns_\n\n-   `Object|undefined`: A puppeteer ElementHandle object if found.\n\n### updatePost\n\nClicks the button to save / update a post in the block editor.\n\n_Returns_\n\n-   `Promise`: A promise that resolves when the button is successfully pressed.\n\n### visitPage\n\nVisits a page on the WordPress site.\n\n_Parameters_\n\n-   _path_ `string`: URL path. Eg: \"dashboard\" to visit mysite.com/dashboard.\n-   _query_ `string`: Query string to be added to the url. Eg: \"myvar=1&anothervar=2\".\n\n_Returns_\n\n-   `void`: \n\n### visitPostPermalink\n\nVisits a post on the frontend by from within the block editor.\n\n_Returns_\n\n-   `Promise`: A promise representing the link click.\n\n### visitSettingsPage\n\nVisit a LifterLMS Settings Page on the admin panel\n\n_Parameters_\n\n-   _args_ `Object`: Arguments object.\n-   _args.tab_ `string`: Settings page tab ID.\n-   _args.section_ `string`: Settings page section ID.\n\n_Returns_\n\n-   `void`: \n\n### wpVersionCompare\n\nRun a version compare against the currently tested version of WordPress.\n\n_Parameters_\n\n-   _version_ `string`: A version string.\n-   _comparator_ `string`: A comparison string, eg \">=\" or \"\\<\", etc...\n-   _majorMinorOnly_ `boolean`: If `true`, only uses the major and minor versions of the current WP version. For example, version 5.9.1 will be shortened to 5.9 for comparison purposes.\n\n_Returns_\n\n-   `boolean`: Comparison result.\n\n\n<!-- END TOKEN(Autogenerated API docs) -->\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/babel.config.js",
    "content": "/**\n * Babel config\n *\n * @since 3.3.0\n * @version 3.3.0\n */\n\nconst presets = [ '@wordpress/default' ];\n\nmodule.exports = { presets };\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/package.json",
    "content": "{\n  \"name\": \"@lifterlms/llms-e2e-test-utils\",\n  \"version\": \"4.1.0\",\n  \"description\": \"E2E testing utilities for LifterLMS projects.\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/master/packages/llms-e2e-test-utils\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"e2e\",\n    \"utils\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/llms-e2e-test-utils\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%20llms-e2e-test-utils\"\n  },\n  \"main\": \"src/index.js\",\n  \"dependencies\": {\n    \"@wordpress/e2e-test-utils\": \"^11.13.0\",\n    \"css-xpath\": \"^1.0.0\",\n    \"semver\": \"^7.3.5\"\n  },\n  \"peerDependencies\": {\n    \"lodash\": \"^4.17.21\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"scripts\": {\n    \"docgen\": \"docgen src/index.js --output README.md --to-token\",\n    \"dev\": \"./../dev/src/index.js\",\n    \"lint:js\": \"wp-scripts lint-js ./ --config ../../.eslintrc.js\"\n  }\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/.eslintrc.js",
    "content": "module.exports = {\n\tglobals: {\n\t\tEvent: true, // Native JS.\n\t\texpect: true, // Node native.\n\t\tpage: true, // From Jest.\n\t\tjQuery: true, // WP Core.\n\t},\n};\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/activate-theme.js",
    "content": "import { getWPVersion } from './get-wp-version';\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\n/**\n * Retrieves the default WP theme based on the WP core version.\n *\n * @since 3.3.0\n *\n * @return {string} Slug of the WP core theme.\n */\nfunction getThemeByCoreVersion() {\n\tlet theme;\n\n\tswitch ( getWPVersion().split( '.' ).slice( 0, 2 ).join( '.' ) ) {\n\t\tcase '5.5':\n\t\t\ttheme = 'twentytwenty';\n\t\t\tbreak;\n\n\t\tcase '5.6':\n\t\tcase '5.7':\n\t\tcase '5.8':\n\t\t\ttheme = 'twentytwentyone';\n\t\t\tbreak;\n\n\t\tcase '5.9':\n\t\tdefault:\n\t\t\ttheme = 'twentytwentytwo';\n\t\t\tbreak;\n\t}\n\n\treturn theme;\n}\n\n/**\n * Activates a theme.\n *\n * @since 3.3.0\n * @since 4.0.1 Don't use WP activateTheme, see https://github.com/WordPress/gutenberg/issues/39862.\n *\n * @param {?string} theme Accepts a theme slug. If not supplied, loads the default theme for the tested WP version.\n * @return {Promise} Promise that resolves when the theme is activated.\n */\nexport async function activateTheme( theme = null ) {\n\ttheme = theme || getThemeByCoreVersion();\n\n\tawait visitAdminPage( 'themes.php' );\n\tconst activateButton = await page.$(\n\t\t`div[data-slug=\"${ theme }\"] .button.activate`\n\t);\n\tif ( ! activateButton ) {\n\t\treturn Promise.resolve();\n\t}\n\n\tawait page.click( `div[data-slug=\"${ theme }\"] .button.activate` );\n\treturn page.waitForSelector( `div[data-slug=\"${ theme }\"].active` );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/clear-blocks.js",
    "content": "/**\n * Deletes all blocks in the editor.\n *\n * @since 3.3.0\n *\n * @return {Promise} Promise from page.evaluate().\n */\nexport async function clearBlocks() {\n\treturn page.evaluate( () => {\n\t\tconst\n\t\t\tblockEditorStore = 'core/block-editor',\n\t\t\t{ select, dispatch } = window.wp.data,\n\t\t\t{ removeBlocks } = dispatch( blockEditorStore ),\n\t\t\t{ getBlocks } = select( blockEditorStore );\n\n\t\treturn removeBlocks( getBlocks().map( ( { clientId } ) => clientId ) );\n\t} );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/click-and-wait.js",
    "content": "import { click } from './click';\n\n/**\n * Click an element and wait for navigation.\n *\n * @since 3.37.8\n *\n * @param {string} selector  Query selector for the DOM element to click.\n * @param {string} waitUntil Network connection to wait for, defaults to 'networkidle2'.\n * @return {void}\n */\nexport async function clickAndWait( selector, waitUntil ) {\n\twaitUntil = waitUntil || 'networkidle2';\n\tawait Promise.all( [\n\t\tclick( selector ),\n\t\tpage.waitForNavigation( { waitUntil } ),\n\t] );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/click-element-by-text.js",
    "content": "const { findElementByText } = require( './find-element-by-text' );\n\n/**\n * Click an element by Text\n *\n * @since 2.2.0\n *\n * @param {string} string   Case-insensitive string to search.\n * @param {string} selector Selector to search. Default \"*\".\n * @return {void}\n */\nexport async function clickElementByText( string, selector = '*' ) {\n\tconst el = await findElementByText( string, selector );\n\tawait el.click();\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/click.js",
    "content": "/**\n * Click an elements by selector\n *\n * @since 2.0.0\n * @since 2.2.2 Always waitForSelector before clicking the element.\n *\n * @param {string} selector Element selector string.\n * @return {void}\n */\nexport async function click( selector ) {\n\tawait page.waitForSelector( selector );\n\tawait page.$eval( selector, ( el ) => el.click() );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-access-plan.js",
    "content": "import { click } from './click';\nimport { clickAndWait } from './click-and-wait';\nimport { createCourse } from './create-course';\nimport { fillField } from './fill-field';\n\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\n/**\n * Create and publish a new course\n *\n * @since 2.0.0\n * @since 2.2.2 Use `waitForSelector()`` in favor of `waitFor()`.\n *\n * @param {Object} args        Creation arguments.\n * @param {number} args.postId Post ID of the plan's course or membership.\n * @param {number} args.price  Plan price.\n * @param {string} args.title  Plan title.\n * @return {string} The created plan's purchase link URL.\n */\nexport async function createAccessPlan( {\n\tpostId = null,\n\tprice = 0.0,\n\ttitle = 'Test Plan',\n} ) {\n\tpostId = postId || ( await createCourse() );\n\n\tawait visitAdminPage( 'post.php', `post=${ postId }&action=edit` );\n\n\tawait click( '#llms-new-access-plan' );\n\n\tconst selector = '#llms-access-plans .llms-access-plan';\n\tawait page.waitForSelector( selector );\n\n\tawait fillField( `${ selector }:last-child input.llms-plan-title`, title );\n\n\tif ( price > 0 ) {\n\t\tawait fillField(\n\t\t\t`${ selector }:last-child input.llms-plan-price`,\n\t\t\tprice\n\t\t);\n\t} else {\n\t\tawait click(\n\t\t\t`${ selector }:last-child input[type=\"checkbox\"][data-controller-id=\"llms-plan-is-free\"]`\n\t\t);\n\t}\n\n\tawait page.click( '#llms-save-access-plans' );\n\n\tawait page.waitForTimeout( 2000 );\n\n\t// Get the link to the specific access plan we just created, in case there's one already there\n\tawait page.waitForXPath(\n\t\t\"//*[@id='llms-access-plans']//*[contains(@class, 'llms-plan-title') and normalize-space(.)='\" + title + \"']/following-sibling::*[contains(@class, 'llms-plan-link')]\",\n\t\t{ hidden: true }\n\t);\n\n\tawait page.waitForXPath(\n\t\t\"//*[@id='llms-access-plans']//*[contains(@class, 'llms-plan-title') and normalize-space(.)='\" + title + \"']/following-sibling::*[contains(@class, 'llms-plan-link')]/a\"\n\t);\n\n\tconst planLinkContainers = await page.$x( \"//*[@id='llms-access-plans']//*[contains(@class, 'llms-plan-title') and normalize-space(.)='\" + title + \"']/following-sibling::*[contains(@class, 'llms-plan-link')]\" );\n\n\treturn await page.evaluate(\n\t\t( el ) => el.querySelector( 'a' ).href,\n\t\tplanLinkContainers[0]\n\t);\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-certificate.js",
    "content": "// Internal deps.\nimport { createEngagement } from './create-engagement';\nimport { createPost } from './create-post';\nimport { updatePost } from './update-post';\n\n/**\n * Retrieve default block editor content.\n *\n * @since 3.3.0\n *\n * @return {string} Block markup.\n */\nfunction getDefaultContent() {\n\treturn `<!-- wp:llms/certificate-title {\"placeholder\":\"Certificate of Achievement\"} -->\n<h1 class=\"has-text-align-center has-default-font-family\"></h1>\n<!-- /wp:llms/certificate-title -->\n\n<!-- wp:paragraph {\"align\":\"center\"} -->\n<p class=\"has-text-align-center\">Awarded to {first_name} {last_name}</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph {\"align\":\"center\"} -->\n<p class=\"has-text-align-center\">On {current_date}</p>\n<!-- /wp:paragraph -->`;\n}\n\n/**\n * Create and publish a new certificate\n *\n * @since 2.1.2\n * @since 3.3.0 Updated to utilize the block editor in favor of classic.\n *\n * @param {Object} args            Optional creation arguments.\n * @param {string} args.title      Certificate title.\n * @param {string} args.content    HTML content of the certificate.\n * @param {string} args.adminTitle Admin title.\n * @param {string} args.engagement If supplied, also creates an engagement trigger. This should be the ID of a trigger\n * @return {Object} {\n *    Object containing information about the created post(s).\n *    @type {number} certificateId WP Post ID of the created certificate post.\n *    @type {number} engagementId  WP Post ID of the created engagement post.\n * }\n */\nexport async function createCertificate( {\n\ttitle = 'Test Certificate',\n\tcontent = null,\n\tadminTitle = null,\n\tengagement = '',\n} = {} ) {\n\tlet engagementId;\n\n\tadminTitle = adminTitle || `${ title } Admin Title`;\n\tcontent = content || getDefaultContent( title );\n\n\tconst certificateId = await createPost( 'llms_certificate', adminTitle, content ),\n\t\tcertUrl = await page.url();\n\n\t// If we programmatically set the post content without physically entering the title we'll end up with an empty title later.\n\tif ( content.includes( '<!-- wp:llms/certificate-title' ) ) {\n\t\tconst TITLE_SELECTOR = '.is-root-container.block-editor-block-list__layout .wp-block-llms-certificate-title';\n\t\tawait page.waitForSelector( TITLE_SELECTOR );\n\t\tawait page.click( TITLE_SELECTOR );\n\t\tawait page.keyboard.type( title );\n\t\tawait page.waitForTimeout( 500 );\n\t\tawait updatePost();\n\t}\n\n\tif ( engagement ) {\n\t\tengagementId = await createEngagement( certificateId, {\n\t\t\ttrigger: engagement,\n\t\t\ttype: 'certificate',\n\t\t\ttitle: `Engagement for ${ title } (ID #${ certificateId })`,\n\t\t} );\n\n\t\t// Return to the certificate.\n\t\tawait page.goto( certUrl );\n\t}\n\n\treturn {\n\t\tcertificateId,\n\t\tengagementId,\n\t};\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-coupon.js",
    "content": "import { clickAndWait } from './click-and-wait';\nimport { fillField } from './fill-field';\n\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\n/**\n * Create and publish a new course\n *\n * @since 2.0.0\n *\n * @param {Object} args          Creation arguments.\n * @param {string} args.code     Coupon code (post title).\n * @param {string} args.discount The discount amount with either a leading `$` to specify dollar amount discounts or a trailing `%` for percentage discounts.\n * @return {string} The coupon code.\n */\nexport async function createCoupon( { code = null, discount = '10%' } ) {\n\tcode = code || Math.random().toString( 36 ).slice( 2 );\n\n\tawait visitAdminPage(\n\t\t'post-new.php',\n\t\t`post_type=llms_coupon&post_title=${ code }`\n\t);\n\n\tawait page.select(\n\t\t'#_llms_discount_type',\n\t\tdiscount.includes( '%' ) ? 'percent' : 'dollar'\n\t);\n\n\tawait fillField(\n\t\t'#_llms_coupon_amount',\n\t\tdiscount.replace( '%', '' ).replace( '$', '' )\n\t);\n\n\tawait clickAndWait( '#publish' );\n\n\treturn code;\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-course.js",
    "content": "import { createPost } from './create-post';\n\n/**\n * Create and publish a new course\n *\n * @since Unknown\n * @since 2.2.0 Use `createPost()`.\n *\n * @param {string} title Course title.\n * @return {number} The created course's WP_Post ID.\n */\nexport async function createCourse( title = 'Test Course' ) {\n\treturn createPost( 'course', title );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-engagement.js",
    "content": "import url from 'url'; // eslint-disable-line no-unused-vars\nimport { clickAndWait } from './click-and-wait';\nimport { fillField } from './fill-field';\nimport { setSelect2Option } from './set-select2-option';\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\n/**\n * Create and publish a new certificate\n *\n * @since 2.1.2\n *\n * @param {number} engagementId WP_Post ID of the a certificate, email, or achievement post.\n * @param {Object} args         Optional creation arguments.\n * @param {string} args.title   Engagement title.\n * @param {string} args.trigger ID of the engagement trigger event.\n * @param {string} args.type    Engagement type: certificate, email, or achievement.\n * @param {number} args.delay   Engagement delay, in days.\n * @return {number} WP Post ID of the created certificate post.\n */\nexport async function createEngagement(\n\tengagementId,\n\t{\n\t\ttitle = 'Test Engagement',\n\t\ttrigger = 'user_registration',\n\t\ttype = 'certificate',\n\t\tdelay = 0,\n\t} = {}\n) {\n\tawait visitAdminPage(\n\t\t'post-new.php',\n\t\t`post_type=llms_engagement&post_title=${ title }`\n\t);\n\n\tawait setSelect2Option( '#_llms_trigger_type', trigger );\n\tawait setSelect2Option( '#_llms_engagement_type', type );\n\tawait setSelect2Option( '#_llms_engagement', engagementId.toString() );\n\n\tawait fillField( '#_llms_engagement_delay', delay.toString() );\n\n\tawait clickAndWait( '#publish' );\n\n\tconst currUrl = new URL( await page.url() );\n\treturn currUrl.searchParams.get( 'post' );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-membership.js",
    "content": "import { createPost } from './create-post';\n\n/**\n * Create and publish a new membership\n *\n * @since 2.2.0\n *\n * @param {string} title Membership title.\n * @return {number} The created membership's WP_Post ID.\n */\nexport async function createMembership( title = 'Test Membership' ) {\n\treturn createPost( 'llms_membership', title );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-post.js",
    "content": "import { createNewPost, setPostContent } from '@wordpress/e2e-test-utils';\n\nimport { publishPost } from './publish-post';\n\n/**\n * Create and publish a new post\n *\n * @since 2.2.0\n * @since 3.3.0 Set the post's content with `setPostContent()`.\n *\n * @param {string}  postType WP_Post type.\n * @param {string}  title    Post title.\n * @param {?string} content  Post content.\n * @return {number} The created post's WP_Post ID.\n */\nexport async function createPost( postType, title = 'Test Course', content = null ) {\n\tawait createNewPost( {\n\t\ttitle,\n\t\tpostType,\n\t} );\n\n\tif ( content ) {\n\t\tawait setPostContent( content );\n\t}\n\n\tawait publishPost();\n\n\treturn await page.evaluate( () =>\n\t\twp.data.select( 'core/editor' ).getCurrentPostId()\n\t);\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-user.js",
    "content": "// Internal dependencies.\nimport { clickAndWait } from './click-and-wait';\nimport { fillField } from './fill-field';\n\n// External dependencies.\nimport url from 'url'; // eslint-disable-line no-unused-vars\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\n/**\n * Asynchronously loop through an Object\n *\n * @since 1.0.0\n *\n * @param {Object}   obj      Object to loop through.\n * @param {Function} callback Callback function, will be passed to params `key` and `val`.\n * @return {void}\n */\nconst forEach = async ( obj, callback ) => {\n\tconst keys = Object.keys( obj );\n\tfor ( let i = 0; i < keys.length; i++ ) {\n\t\tawait callback( keys[ i ], obj[ keys[ i ] ] );\n\t}\n};\n\n/**\n * Create a new user.\n *\n * @since 1.0.0\n * @since 2.2.0 Returns the WP_User ID in the return object.\n * @since 2.2.1 Options object is now optional.\n *\n * @param {Object} opts Hash of user information used to create the new user.\n * @return {Object} Object of created user data.\n */\nexport async function createUser( opts = {} ) {\n\tawait visitAdminPage( 'user-new.php' );\n\n\tconst login = `mock_${ Math.random().toString( 36 ).slice( 2 ) }`;\n\topts = Object.assign(\n\t\t{\n\t\t\tuser_login: login,\n\t\t\temail: `${ login }@mock.tld`,\n\t\t\trole: 'student',\n\t\t\tpassword: `${ Math.random()\n\t\t\t\t.toString( 36 )\n\t\t\t\t.slice( 2 ) }${ Math.random().toString( 36 ).slice( 2 ) }`,\n\t\t},\n\t\topts\n\t);\n\n\tawait forEach( opts, async ( key, val ) => {\n\t\tif ( 'role' === key ) {\n\t\t\tawait page.select( '#role', val );\n\t\t} else if ( 'password' === key ) {\n\t\t\tawait page.click( '.wp-generate-pw' );\n\t\t\tawait fillField( '#pass1', val );\n\t\t} else {\n\t\t\tawait fillField( `#${ key }`, val );\n\t\t}\n\t} );\n\n\tawait clickAndWait( '#createusersub' );\n\n\t// Add the user's ID.\n\tconst currUrl = new URL( await page.url() );\n\topts.id = currUrl.searchParams.get( 'id' );\n\n\treturn opts;\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/create-voucher.js",
    "content": "import { click } from './click';\nimport { clickAndWait } from './click-and-wait';\nimport { fillField } from './fill-field';\nimport { select2Select } from './select2-select';\n\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\n/**\n * Create and publish a new course\n *\n * @since 2.2.1\n * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n *\n * @param {Object} args            Creation arguments.\n * @param {string} args.name       Voucher (post) title.\n * @param {string} args.course     Name of a course to add to the voucher.\n * @param {string} args.membership Name of a membership to add to the voucher.\n * @param {number} args.codes      Number of codes to generate.\n * @param {number} args.uses       Number of uses per code.\n * @return {string[]} Array of the generated voucher codes.\n */\nexport async function createVoucher( {\n\tname = 'A Voucher',\n\tcourse = 'LifterLMS Quickstart Course',\n\tmembership = '',\n\tcodes = 5,\n\tuses = 5,\n} = {} ) {\n\tawait visitAdminPage(\n\t\t'post-new.php',\n\t\t`post_type=llms_voucher&post_title=${ name }`\n\t);\n\n\tif ( course ) {\n\t\tawait select2Select( '#_llms_voucher_courses', course );\n\t}\n\n\tif ( membership ) {\n\t\tawait select2Select( '#_llms_voucher_memberships', membership );\n\t}\n\n\tawait fillField( '#llms_voucher_add_quantity', codes );\n\tawait fillField( '#llms_voucher_add_uses', uses );\n\n\tawait click( '#llms_voucher_add_codes' );\n\n\tawait page.waitForSelector( '#llms_voucher_tbody tr' );\n\n\tawait clickAndWait( '#publish' );\n\tawait page.waitForTimeout( 1000 ); // Non-interactive tests aren't publishing without a delay, not sure why.\n\n\treturn await page.$$eval(\n\t\t'#llms_voucher_tbody input[name=\"llms_voucher_code[]\"',\n\t\t( inputs ) => inputs.map( ( { value } ) => value )\n\t);\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/dismiss-editor-welcome-guide.js",
    "content": "/**\n * Dismiss the \"Welcome Guide\" in the block editor (if it's active)\n *\n * @since 2.2.0\n *\n * @return {void}\n */\nexport async function dismissEditorWelcomeGuide() {\n\tconst isWelcomeGuideActive = await page.evaluate( () =>\n\t\twp.data.select( 'core/edit-post' ).isFeatureActive( 'welcomeGuide' )\n\t);\n\tif ( isWelcomeGuideActive ) {\n\t\tawait page.evaluate( () =>\n\t\t\twp.data.dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' )\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/enroll-student.js",
    "content": "// Internal dependencies.\nimport { click } from './click';\nimport { setSelect2Option } from './set-select2-option';\n\n// External dependencies.\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\n/**\n * Enroll a student into a course\n *\n * This performs as \"manual\" enrollment using the enrollment\n * area on the course or membership.\n *\n * @since 2.2.0\n * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n *\n * @param {number} postId    WP_Post ID.\n * @param {number} studentId WP_User ID.\n * @return {void}\n */\nexport async function enrollStudent( postId, studentId ) {\n\tawait visitAdminPage( 'post.php', `post=${ postId }&action=edit` );\n\n\tawait setSelect2Option( '#llms-add-student-select', studentId );\n\n\tawait click( '#llms-enroll-students' );\n\n\t// Lazy waiting for ajax save.\n\tawait page.waitForTimeout( 2000 );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/fill-field.js",
    "content": "import { pressKeyWithModifier } from '@wordpress/e2e-test-utils';\n\n/**\n * Type text into a field identified by a selector.\n *\n * @since 1.1.1\n * @since 2.0.0 Automatically cast `text` to a string.\n * @since 3.3.0 Wait for the selector before attempting to focus on it.\n *\n * @param {string} selector Query selector to identify the field element.\n * @param {string} text     Text to type into the field.\n * @return {void}\n */\nexport async function fillField( selector, text ) {\n\tawait page.waitForSelector( selector );\n\tawait page.focus( selector );\n\tawait pressKeyWithModifier( 'primary', 'a' );\n\tawait page.type( selector, text.toString() );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/find-element-by-text.js",
    "content": "const cssXPath = require( 'css-xpath' );\n\n/**\n * Find an element by Text\n *\n * @since 2.2.0\n *\n * @see {@link https://stackoverflow.com/a/47829000/400568}\n *\n * @param {string} string   Case-insensitive string to search.\n * @param {string} selector Selector to search. Default \"*\".\n * @return {Array} Element.\n */\nexport async function findElementByText( string, selector = '*' ) {\n\treturn await page.waitForXPath(\n\t\t`${ cssXPath( selector ) }[contains(text(), '${ string }')]`\n\t);\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/get-all-blocks.js",
    "content": "import { getAllBlocks as realGetAllBlocks } from '@wordpress/e2e-test-utils';\n\n/**\n * Removes clientIds from a block and its innerBlocks.\n *\n * @since 3.3.0\n *\n * @param {Object} options          Options object.\n * @param {string} options.clientId The block's clientId.\n * @param {...*}   options.block    The remaining block properties.\n * @return {Object} The original block without the clientId property.\n */\nfunction removeClientId( { clientId, ...block } ) { // eslint-disable-line no-unused-vars\n\tblock.innerBlocks = removeClientIds( block.innerBlocks );\n\treturn block;\n}\n\n/**\n * Removes clientIds from a list of blocks.\n *\n * @since 3.3.0\n *\n * @param {Object[]} blocks Array of WP_Block objects.\n * @return {Object[]} Original array with clientIds removed.\n */\nfunction removeClientIds( blocks ) {\n\treturn blocks.map( ( block ) => removeClientId( block ) );\n}\n\n/**\n * Retrieves a list of blocks in the editor, with or without client IDs.\n *\n * Specifying `withClientIds=false` allows using the resulting array of block\n * objects in snapshots without having to specify a snapshot matcher\n * that excludes (possibly nested) blocks with clientIds that will not\n * match future test runs.\n *\n * @since 3.3.0\n *\n * @param {boolean} withClientIds Whether or not to exclude clientIds.\n * @return {Object[]} Array of block objects.\n */\nexport async function getAllBlocks( withClientIds = true ) {\n\tconst blocks = await realGetAllBlocks();\n\n\tif ( withClientIds ) {\n\t\treturn blocks;\n\t}\n\n\treturn removeClientIds( blocks );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/get-post-title.js",
    "content": "import { wpVersionCompare } from './wp-version-compare';\n\n/**\n * Retrieves the textContent of the lesson post's title element.\n *\n * This function uses a dynamically-determined selector based on the current WP version (and assumed theme)\n * run by default with that version.\n *\n * @since 3.3.0\n *\n * @return {Promise} A promise that resolves to return the element's text content.\n */\nexport function getPostTitleTextContent() {\n\treturn page.$eval( getPostTitleSelector(), ( el ) => el.textContent );\n}\n\n/**\n * Retrieves the CSS selector for the post's title element.\n *\n * On 5.9+ we're testing against the 2022 theme, on 5.8 & earlier we're using 2021.\n *\n * @since 3.3.0\n *\n * @return {Promise} A promise that resolves to return the element's text content.\n */\nexport function getPostTitleSelector() {\n\treturn wpVersionCompare( '5.9' ) ? '.wp-block-post-title' : '.entry-title';\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/get-wp-version.js",
    "content": "/**\n * Retrieve the WP_VERSION environment variable\n *\n * When running tests locally this will likely be undefined unless running tests with\n * `WP_VERSION=5.7.2 npm run test`.\n *\n * The WP_VERSION env var is defined during CI tests automatically and this function\n * is generally used to determine conditionals based on the WP Core version.\n *\n * For example: block editor selectors change between WP core version, some features\n * aren't available on older versions, etc...\n *\n * @since 5.0.1\n *\n * @return {?string} WordPress version or null if not set.\n */\nexport function getWPVersion() {\n\tconst { WP_VERSION } = process.env;\n\treturn WP_VERSION || null;\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/highlight-node.js",
    "content": "/**\n * Highlight (selects) the contents of a node.\n *\n * @since 3.1.0\n *\n * @param {string}  selector      Query selector.\n * @param {boolean} copySelection If `true`, copies the selected text and returns it.\n *                                The browser clipboard-read permission must be granted in order to read from the clipboard.\n * @return {boolean|string} Returns the copied text or `true` if `copySelection` is `false`.\n */\nexport async function highlightNode( selector, copySelection = false ) {\n\tawait page.waitForSelector( selector );\n\n\tawait page.evaluate( ( _selector ) => {\n\t\tconst range = document.createRange(),\n\t\t\t// eslint-disable-next-line @wordpress/no-global-get-selection\n\t\t\tselection = window.getSelection();\n\n\t\trange.selectNodeContents( document.querySelector( _selector ) );\n\n\t\tselection.removeAllRanges();\n\n\t\tselection.addRange( range );\n\t}, selector );\n\n\tif ( copySelection ) {\n\t\tawait page.bringToFront();\n\n\t\treturn await page.evaluate( () => {\n\t\t\tdocument.execCommand( 'copy' );\n\t\t\treturn window.navigator.clipboard.readText();\n\t\t} );\n\t}\n\n\treturn true;\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/import-course.js",
    "content": "// Internal dependencies.\nimport { clickAndWait } from './click-and-wait';\n\n// External dependencies.\nimport { visitAdminPage, clickButton } from '@wordpress/e2e-test-utils';\n\n/**\n * Import a course JSON file\n *\n * @since 2.2.0\n * @since 2.2.0 Update to accommodate changes in the LifterLMS core.\n * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n *\n * @param {string}  importFile Filename of the import.\n * @param {string}  importPath Local path where the file is located. By default uses `tests/assets/`.\n * @param {boolean} navigate   Whether or not to automatically navigate to the imported course when done.\n * @return {void}\n */\nexport async function importCourse(\n\timportFile,\n\timportPath = '',\n\tnavigate = true\n) {\n\timportPath = importPath || `${ process.cwd() }/tests/assets/`;\n\n\tconst file = importPath + importFile;\n\n\tawait visitAdminPage( 'admin.php', 'page=llms-import' );\n\n\t// Upload button\n\tawait clickButton( 'Upload' );\n\n\tconst inputSelector = 'input[name=\"llms_import\"]';\n\tawait page.waitForSelector( inputSelector );\n\tconst fileUpload = await page.$( inputSelector );\n\n\tfileUpload.uploadFile( file );\n\tawait page.waitForTimeout( 1000 );\n\n\tawait clickButton( 'Import' );\n\n\tif ( navigate ) {\n\t\tawait page.waitForSelector( '.llms-admin-notice.notice-success a' );\n\t\tawait clickAndWait( '.llms-admin-notice.notice-success a' );\n\t}\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/index.js",
    "content": "export { activateTheme } from './activate-theme';\n\nexport { clearBlocks } from './clear-blocks';\n\nexport { click } from './click';\nexport { clickAndWait } from './click-and-wait';\nexport { clickElementByText } from './click-element-by-text';\n\nexport { createAccessPlan } from './create-access-plan';\nexport { createCertificate } from './create-certificate';\nexport { createCoupon } from './create-coupon';\nexport { createCourse } from './create-course';\nexport { createEngagement } from './create-engagement';\nexport { createMembership } from './create-membership';\nexport { createPost } from './create-post';\nexport { createUser } from './create-user';\nexport { createVoucher } from './create-voucher';\n\nexport { dismissEditorWelcomeGuide } from './dismiss-editor-welcome-guide';\n\nexport { enrollStudent } from './enroll-student';\n\nexport { fillField } from './fill-field';\n\nexport { getAllBlocks } from './get-all-blocks';\nexport { getPostTitleSelector, getPostTitleTextContent } from './get-post-title';\nexport { getWPVersion } from './get-wp-version';\n\nexport { highlightNode } from './highlight-node';\n\nexport { importCourse } from './import-course';\n\nexport { findElementByText } from './find-element-by-text';\n\nexport { loginStudent } from './login-student';\nexport { logoutUser } from './logout-user';\n\nexport { openSidebarPanelTab } from './open-sidebar-panel-tab';\n\nexport { publishPost } from './publish-post';\n\nexport { registerStudent } from './register-student';\nexport { runSetupWizard } from './run-setup-wizard';\n\nexport { select2Select } from './select2-select';\nexport { setCheckboxSetting } from './set-checkbox-setting';\nexport { setSelect2Option } from './set-select2-option';\n\nexport { toggleOpenRegistration } from './toggle-open-registration';\nexport { toggleSidebarPanel } from './toggle-sidebar-panel';\n\nexport { updatePost } from './update-post';\n\nexport { visitPage } from './visit-page';\nexport { visitPostPermalink } from './visit-post-permalink';\nexport { visitSettingsPage } from './visit-settings-page';\n\nexport { wpVersionCompare } from './wp-version-compare';\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/login-student.js",
    "content": "/**\n * Internal Dependencies.\n */\nconst { clickAndWait } = require( './click-and-wait' ),\n\t{ fillField } = require( './fill-field' ),\n\t{ visitPage } = require( './visit-page' );\n\n/**\n * Login a user via the LifterLMS student dashboard.\n *\n * @since 3.0.0\n *\n * @param {string} login User login or email address.\n * @param {string} pass  User password.\n * @return {void}\n */\nexport async function loginStudent( login, pass ) {\n\tawait visitPage( 'dashboard' );\n\n\tawait fillField( '#llms_login', login );\n\tawait fillField( '#llms_password', pass );\n\n\tawait clickAndWait( '#llms_login_button' );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/logout-user.js",
    "content": "/**\n * External Dependencies.\n */\nconst { createURL } = require( '@wordpress/e2e-test-utils' );\n\n/**\n * Internal Dependencies.\n */\nconst { clickAndWait } = require( './click-and-wait' );\n\n/**\n * Logout the current user.\n *\n * @since 3.37.8\n * @since 2.1.2 Wait 1 second before navigating to logout page.\n * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n * @since 3.3.0 Returns a promise rather than void.\n *\n * @return {Promise} Promise which resolves after the user is logged out and the page reloaded.\n */\nexport async function logoutUser() {\n\tawait page.waitForTimeout( 1000 );\n\tawait page.goto( createURL( 'wp-login.php', 'action=logout' ) );\n\treturn await clickAndWait( 'a' );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/open-sidebar-panel-tab.js",
    "content": "import {\n\tensureSidebarOpened,\n} from '@wordpress/e2e-test-utils';\n\n/**\n * Opens a sidebar panel tab if it's not already open.\n *\n * @since 3.3.0\n *\n * @param {string} tab Tab to select, accepts \"primary\" to select the main document settings tab or \"block\"\n *                     to select the block tab.\n * @return {Promise} A promise that resolves when the desired panel becomes active.\n */\nexport async function openSidebarPanelTab( tab = 'primary' ) {\n\tawait ensureSidebarOpened();\n\n\tlet selector = '.edit-post-sidebar__panel-tabs .components-button.edit-post-sidebar__panel-tab';\n\tif ( 'block' === tab ) {\n\t\tselector += '[data-label=\"Block\"]';\n\t}\n\n\tawait page.waitForSelector( selector );\n\n\tconst btn = await page.$( selector ),\n\t\tisOpen = await page.$eval( selector, ( { classList } ) => classList.contains( 'is-active' ) );\n\n\tif ( ! isOpen ) {\n\t\tawait btn.click();\n\t}\n\n\treturn page.waitForSelector( selector + '.is-active' );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/publish-post.js",
    "content": "import { arePrePublishChecksEnabled } from '@wordpress/e2e-test-utils';\n\nimport { updatePost } from './update-post';\n\n/**\n * Disables prepublish checks and clicks the post publish button.\n *\n * @since 3.3.0\n *\n * @return {Promise} Promise which resolves when the close button element is successfully clicked.\n */\nexport async function publishPost() {\n\tconst enabled = await arePrePublishChecksEnabled();\n\tif ( enabled ) {\n\t\tawait page.evaluate( () => window.wp.data.dispatch( 'core/editor' ).disablePublishSidebar() );\n\t}\n\n\treturn updatePost();\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/register-student.js",
    "content": "import { click } from './click';\nimport { clickAndWait } from './click-and-wait';\nimport { fillField } from './fill-field';\nimport { logoutUser } from './logout-user';\nimport { select2Select } from './select2-select';\nimport { visitPage } from './visit-page';\n\n/**\n * Register a new student via the LifterLMS Open Registration Page\n *\n * @since 2.1.2\n * @since 2.2.1 Add `args.voucher` to enable voucher usage during registration.\n * @since 5.0.0-alpha.2 Add arguments for address fields.\n *\n * @param {Object} args          Function arguments object.\n * @param {string} args.email    Email address. If not supplied one will be created from the first name and last name.\n * @param {string} args.pass     User password. If not supplied one will be automatically generated.\n * @param {string} args.first    User's first name.\n * @param {string} args.last     User's last name.\n * @param {string} args.voucher  Voucher code to use during registration.\n * @param {string} args.address1 User's address line 1.\n * @param {string} args.address2 User's address line 2.\n * @param {string} args.city     User's city.\n * @param {string} args.country  User's country.\n * @param {string} args.state    User's state.\n * @param {string} args.postcode User's postcode.\n * @param {string} args.phone    User's phone.\n * @return {Object} {\n *     Object containing information about the newly created user.\n *\n *     @type {string} email User's email address.\n *     @type {string} pass  User's password.\n * }\n */\nexport async function registerStudent( {\n\temail = null,\n\tpass = null,\n\tfirst = 'Jamie',\n\tlast = 'Doe',\n\tvoucher = '',\n\taddress1 = '1 Avenue Street',\n\taddress2 = '',\n\tcity = 'A City',\n\tcountry = 'United States',\n\tstate = 'Texas',\n\tpostcode = '52342',\n\tphone = '',\n} = {} ) {\n\tconst theInt = Math.floor( Math.random() * ( 99990 - 10000 + 1 ) ) + 10000;\n\n\temail = email || `${ first }.${ last }+${ theInt }@e2e-tests.tld`;\n\tpass =\n\t\tpass ||\n\t\tMath.random().toString( 36 ).slice( 2 ) +\n\t\t\tMath.random().toString( 36 ).slice( 2 );\n\n\tawait logoutUser();\n\tawait visitPage( 'dashboard' );\n\n\tawait fillField( '#email_address', email );\n\tawait fillField( '#email_address_confirm', email );\n\tawait fillField( '#password', pass );\n\tawait fillField( '#password_confirm', pass );\n\tawait fillField( '#first_name', first );\n\tawait fillField( '#last_name', last );\n\n\tif ( address1 ) {\n\t\tawait fillField( '#llms_billing_address_1', address1 );\n\t}\n\n\tif ( address2 ) {\n\t\tawait fillField( '#llms_billing_address_2', address2 );\n\t}\n\n\tif ( city ) {\n\t\tawait fillField( '#llms_billing_city', city );\n\t}\n\n\tif ( country && 'United States' !== country ) {\n\t\tawait select2Select( '#llms_billing_country', country );\n\t}\n\n\tif ( state ) {\n\t\tawait select2Select( '#llms_billing_state', state );\n\t}\n\n\tif ( postcode ) {\n\t\tawait fillField( '#llms_billing_zip', postcode );\n\t}\n\n\tif ( phone ) {\n\t\tawait fillField( '#llms_phone', phone );\n\t}\n\n\tif ( voucher ) {\n\t\tawait click( '#llms-voucher-toggle' );\n\t\tawait page.waitForSelector( '#llms_voucher' );\n\t\tawait fillField( '#llms_voucher', voucher );\n\t}\n\n\tawait clickAndWait( '#llms_register_person' );\n\n\treturn {\n\t\temail,\n\t\tpass,\n\t};\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/run-setup-wizard.js",
    "content": "import { clickAndWait } from './click-and-wait';\nimport { clickElementByText } from './click-element-by-text';\nimport { findElementByText } from './find-element-by-text';\nimport { wpVersionCompare } from './wp-version-compare';\nimport { dismissEditorWelcomeGuide } from './dismiss-editor-welcome-guide';\n\nimport { visitAdminPage, clickButton } from '@wordpress/e2e-test-utils';\n\n/**\n * Retrieve the Setup Wizard Page Title.\n *\n * @since 2.1.0\n *\n * @return {string} Content of the title element.\n */\nconst getTitle = async function() {\n\treturn await page.$eval(\n\t\t'.llms-setup-content > form > h1',\n\t\t( txt ) => txt.textContent\n\t);\n};\n\n/**\n * Run (and test) the LifterLMS Setup Wizard\n *\n * @since 2.1.0\n * @since 2.2.0 Rework to accommodate setup wizard changes in LifterLMS core.\n * @since 3.2.0 Fix title assertion on WordPress >= v5.9.\n *\n * @param {Object}   options                 Options object.\n * @param {string[]} options.coursesToImport Titles of the course(s) to import through the setup wizard. Pass a falsy to skip import and \"Start from Scratch\".\n * @param {boolean}  options.exit            Whether or not to exit the setup wizard at the conclusion of setup. If `true`, uses the \"Exit\" link to leave setup.`\n * @return {void}\n */\nexport async function runSetupWizard( {\n\tcoursesToImport = [ 'LifterLMS Quickstart Course' ],\n\texit = false,\n} = {} ) {\n\t// Launch the Setup Wizard.\n\tawait visitAdminPage( 'admin.php', 'page=llms-setup' );\n\n\t// Step One.\n\texpect( await getTitle() ).toBe( 'Welcome to LifterLMS!' );\n\n\t// Move to Step Two.\n\tawait clickAndWait( '.llms-setup-actions .llms-button-primary' );\n\texpect( await getTitle() ).toBe( 'Page Setup' );\n\n\t// Move to Step Three.\n\tawait clickAndWait( '.llms-setup-actions .llms-button-primary' );\n\texpect( await getTitle() ).toBe( 'Payments' );\n\n\t// Move to Step Four.\n\tawait clickAndWait( '.llms-setup-actions .llms-button-primary' );\n\texpect( await getTitle() ).toBe( 'Help Improve LifterLMS & Get a Coupon' );\n\n\t// Move to Step Five.\n\tawait clickAndWait( '.llms-setup-actions .llms-button-secondary' ); // Skip the coupon.\n\texpect( await getTitle() ).toBe( 'Setup Complete!' );\n\n\t// Import button should be disabled.\n\texpect(\n\t\tawait page.$eval( '#llms-setup-submit', ( el ) => el.disabled )\n\t).toBe( true );\n\n\tif ( exit ) {\n\t\t// Exit the wizard.\n\n\t\tawait clickAndWait( '.llms-exit-setup' );\n\t\texpect(\n\t\t\tawait page.url().includes( '/admin.php?page=llms-settings' )\n\t\t).toBe( true );\n\t} else if ( ! coursesToImport ) {\n\t\t// Start from scratch.\n\n\t\tawait clickAndWait( '.llms-setup-actions .llms-button-secondary' );\n\t\tawait dismissEditorWelcomeGuide();\n\t} else if ( coursesToImport ) {\n\t\t// Import courses.\n\n\t\t// Select specified courses.\n\t\tfor ( const courseTitle of coursesToImport ) {\n\t\t\tawait clickElementByText( courseTitle, 'h3' );\n\t\t}\n\n\t\tawait clickButton( 'Import Courses' );\n\n\t\tawait page.waitForNavigation();\n\n\t\tif ( 1 === coursesToImport.length ) {\n\t\t\t// Single course imported.\n\n\t\t\texpect(\n\t\t\t\tawait page.$eval(\n\t\t\t\t\t'.block-editor h1.screen-reader-text',\n\t\t\t\t\t( txt ) => txt.textContent\n\t\t\t\t)\n\t\t\t).toBe( 'Edit Course' );\n\n\t\t\tawait dismissEditorWelcomeGuide();\n\n\t\t\texpect(\n\t\t\t\tawait page.$eval(\n\t\t\t\t\t'.editor-post-title__input',\n\t\t\t\t\t// On >= WP 5.9, this is an <h1>, earlier is a <textarea>.\n\t\t\t\t\t( txt, isTextNode ) => isTextNode ? txt.textContent : txt.value,\n\t\t\t\t\twpVersionCompare( '5.9' )\n\t\t\t\t)\n\t\t\t).toBe( coursesToImport[ 0 ] );\n\t\t} else {\n\t\t\texpect(\n\t\t\t\tawait page.url().includes( '/edit.php?post_type=course' )\n\t\t\t).toBe( true );\n\n\t\t\t// All courses should be present in the post table list.\n\t\t\tfor ( const courseTitle of coursesToImport ) {\n\t\t\t\tawait findElementByText( courseTitle, '#the-list a.row-title' );\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/select2-select.js",
    "content": "/**\n * Select a value from a select2 dropdown field\n *\n * @since 2.2.1\n * @since 2.3.0 Focus on the search selector prior to typing.\n * @since 2.3.1 Wait for select2 to be loaded before attempting to open it and wait for select2 dropdown\n *              to close after selecting an option.\n * @since 3.0.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n * @param {string} selector Query selector for the select element.\n * @param {string} value    Option value to select.\n * @return {void}\n */\nexport async function select2Select( selector, value ) {\n\t// Wait for select2 to load on the element.\n\tawait page.waitForSelector( `${ selector }.select2-hidden-accessible` );\n\n\tawait page.$eval( selector, ( el ) => {\n\t\tjQuery( el ).select2( 'open' );\n\t} );\n\n\tconst SEARCH_SELECTOR = '.select2-search__field';\n\tawait page.waitForSelector( SEARCH_SELECTOR );\n\tawait page.focus( SEARCH_SELECTOR );\n\n\tawait page.keyboard.type( value );\n\tawait page.waitForTimeout( 1000 );\n\n\tawait page.keyboard.press( 'Enter' );\n\n\t// Wait for the selection box to close.\n\tawait page.waitForSelector(\n\t\t`${ selector } + .select2-container .select2-selection[aria-expanded=\"false\"]`\n\t);\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/set-checkbox-setting.js",
    "content": "// Internal dependencies.\nimport { clickAndWait } from './click-and-wait';\n\n/**\n * Toggles a LifterLMS checkbox setting.\n *\n * @since 3.1.0\n *\n * @param {string}  selector Selector for the setting checkbox.\n * @param {boolean} status   Requested setting status. Use `true` for checked and `false` for unchecked.\n * @param {boolean} save     Whether or not to perform a save after updating the setting.\n * @return {void}\n */\nexport async function setCheckboxSetting( selector, status = true, save = true ) {\n\tawait page.waitForSelector( selector );\n\n\tconst currStatus = await page.$eval( selector, ( el ) => el.checked );\n\n\tif ( status !== currStatus ) {\n\t\tawait page.click( selector );\n\n\t\tif ( save ) {\n\t\t\tawait clickAndWait( '.llms-save .llms-button-primary' );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/set-select2-option.js",
    "content": "/**\n * Set the value of a select2 dropdown field\n *\n * This does not actually test whether or not select2 is working,\n * instead it selects the value on the select element and artificially\n * triggers a change event.\n *\n * @since 2.2.0\n *\n * @param {string}  selector Query selector for the select element.\n * @param {string}  value    Option value to select.\n * @param {boolean} create   If `true`, the value will be added to the select element before being selected.\n *                           This is a useful option for AJAX powered select2 elements that will be empty until interacted with.\n * @return {void}\n */\nexport async function setSelect2Option( selector, value, create = true ) {\n\tawait page.$eval(\n\t\tselector,\n\t\t( el, _value, _create ) => {\n\t\t\tif ( _create ) {\n\t\t\t\tjQuery( el ).append(\n\t\t\t\t\t'<option value=\"' + _value + '\">' + _value + '</option>'\n\t\t\t\t);\n\t\t\t}\n\t\t\tel.value = _value.toString();\n\t\t\tel.dispatchEvent( new Event( 'change' ) );\n\t\t},\n\t\tvalue,\n\t\tcreate\n\t);\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/toggle-open-registration.js",
    "content": "import { clickAndWait } from './click-and-wait';\nimport { visitSettingsPage } from './visit-settings-page';\n\n/**\n * Toggles the open registration setting on or off\n *\n * @since 2.1.2\n *\n * @param {boolean} status Whether to toggle on (`true`) or off (`false`).\n * @return {void}\n */\nexport async function toggleOpenRegistration( status ) {\n\tawait visitSettingsPage( { tab: 'account' } );\n\n\tconst currStatus = await page.$eval(\n\t\t'#lifterlms_enable_myaccount_registration',\n\t\t( el ) => el.checked\n\t);\n\n\tif ( ( status && ! currStatus ) || ( ! status && currStatus ) ) {\n\t\tawait page.click( '#lifterlms_enable_myaccount_registration' );\n\t\tawait clickAndWait( '.llms-save .llms-button-primary' );\n\t}\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/toggle-sidebar-panel.js",
    "content": "import {\n\tfindSidebarPanelWithTitle,\n} from '@wordpress/e2e-test-utils';\n\nimport { openSidebarPanelTab } from './open-sidebar-panel-tab';\n\n/**\n * Opens or closes an editor sidebar panel based on the panel's title.\n *\n * @since 3.3.0\n *\n * @param {string}  title        The panel title to open or close.\n * @param {boolean} shouldBeOpen Whether or not the panel should be open.\n * @return {Object|undefined} A puppeteer ElementHandle object if found.\n */\nexport async function toggleSidebarPanel( title, shouldBeOpen = true ) {\n\tawait openSidebarPanelTab();\n\n\tconst btn = await findSidebarPanelWithTitle( title ),\n\t\tclassNames = await (\n\t\t\tawait btn.getProperty( 'className' )\n\t\t).jsonValue(),\n\t\tisOpen = -1 !== classNames.indexOf( 'is-opened' );\n\n\t// Open or close the panel as desired.\n\tif ( isOpen !== shouldBeOpen ) {\n\t\tawait btn.click();\n\t}\n\n\treturn btn;\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/update-post.js",
    "content": "import { click } from './click';\n\n/**\n * Clicks the button to save / update a post in the block editor.\n *\n * @since 3.3.0\n *\n * @return {Promise} A promise that resolves when the button is successfully pressed.\n */\nexport async function updatePost() {\n\treturn click( '.editor-post-publish-button__button' );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/visit-page.js",
    "content": "const { createURL } = require( '@wordpress/e2e-test-utils' );\n\n/**\n * Visits a page on the WordPress site.\n *\n * @since  3.37.8\n * @param {string} path  URL path. Eg: \"dashboard\" to visit mysite.com/dashboard.\n * @param {string} query Query string to be added to the url. Eg: \"myvar=1&anothervar=2\".\n * @return {void}\n */\nexport async function visitPage( path, query ) {\n\treturn await page.goto( createURL( path, query ) );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/visit-post-permalink.js",
    "content": "import { toggleSidebarPanel } from './toggle-sidebar-panel';\n\n/**\n * Visits a post on the frontend by from within the block editor.\n *\n * @since 3.3.0\n *\n * @return {Promise} A promise representing the link click.\n */\nexport async function visitPostPermalink() {\n\tconst SELECTOR = '.edit-post-header__settings a[aria-label=\"View Certificate Template\"]';\n\n\tawait page.waitForSelector( SELECTOR );\n\tconst permalink = await page.$eval( SELECTOR, ( el ) => el.href );\n\n\treturn page.goto( permalink );\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/visit-settings-page.js",
    "content": "/**\n * External Dependencies.\n */\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\nimport { pickBy } from 'lodash';\n\n/**\n * Visit a LifterLMS Settings Page on the admin panel\n *\n * @since 2.1.0\n * @since 2.1.2 Don't add null values to the query string.\n *\n * @param {Object} args         Arguments object.\n * @param {string} args.tab     Settings page tab ID.\n * @param {string} args.section Settings page section ID.\n * @return {void}\n */\nexport async function visitSettingsPage( { tab = null, section = null } = {} ) {\n\tawait visitAdminPage(\n\t\t'admin.php',\n\t\tnew URLSearchParams(\n\t\t\tpickBy( { page: 'llms-settings', tab, section } )\n\t\t).toString()\n\t);\n}\n"
  },
  {
    "path": "packages/llms-e2e-test-utils/src/wp-version-compare.js",
    "content": "import { cmp, coerce, parse } from 'semver';\n\nimport { getWPVersion } from './get-wp-version';\n\n/**\n * Run a version compare against the currently tested version of WordPress.\n *\n * @since 3.2.0\n * @since 3.3.0 Added `majorMinorOnly` argument option.\n *\n * @param {string}  version        A version string.\n * @param {string}  comparator     A comparison string, eg \">=\" or \"<\", etc...\n * @param {boolean} majorMinorOnly If `true`, only uses the major and minor versions of the current WP version.\n *                                 For example, version 5.9.1 will be shortened to 5.9 for comparison purposes.\n * @return {boolean} Comparison result.\n */\nexport function wpVersionCompare( version, comparator = '>=', majorMinorOnly = true ) {\n\tlet wpVersion = parse( coerce( getWPVersion() ) );\n\tif ( majorMinorOnly ) {\n\t\twpVersion = `${ wpVersion.major }.${ wpVersion.minor }.0`;\n\t}\n\treturn cmp( wpVersion, comparator, coerce( version ) );\n}\n"
  },
  {
    "path": "packages/quill-wordcount-module/.eslintrc.js",
    "content": "const config = require( '../scripts/config/.eslintrc.js' );\nmodule.exports = config;\n"
  },
  {
    "path": "packages/quill-wordcount-module/.npmrc",
    "content": "package-lock=false\nengine-strict=true\n"
  },
  {
    "path": "packages/quill-wordcount-module/CHANGELOG.md",
    "content": "@lifterlms/quill-wordcount-module Changelog\n===========================================\n\nv2.0.0 - 2022-08-29\n-------------------\n\n+ Migrated to ESNext package.\n\n\nv1.0.0 - 2018-04-04\n-------------------\n\n+ Initial public release."
  },
  {
    "path": "packages/quill-wordcount-module/README.md",
    "content": "# Quill Word Count Module\n\nWord count module for [Quill](https://quilljs.com/).\n\nUses [words-count](https://www.npmjs.com/package/words-count) to perform word counting in multiple languages and character sets.\n\n\n## Installation\n\nInstall the module\n\n```bash\nnpm install --save @lifterlms/quill-wordcount-module`\n```\n\nAfter loading Quill use the module:\n\n```jsx\nimport quillWordcountModule from '@lifterlms/quill-wordcount-module';\n\n( function() {\n\t// Registers the module with Quill.\n\tquillWordcountModule();\n\n\t// Loads the module in a new editor instance.\n\tconst ed = = new Quill( '#my-quill-container', {\n\t\tmodules: {\n\t\t\twordcount: {\n\t\t\t\tonChange: ( quill, options, wordCount ) => {\n\t\t\t\t\t// Do something when the editor's text updates and you need to know the new word count.\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t} );\n\n} )();\n```\n\n## Changelog\n\n[View the Changelog](./CHANGELOG.md)\n\n\n## Options\n\n| Option Key   | Type       | Default | Description                                                                    |\n| ------------ | ---------- | ------- | ------------------------------------------------------------------------------ |\n| min          | `?number`  | `null`  | The minimum required words. If `null` no minimum will be enforced.             |\n| max          | `?number`  | `null`  | The maximum required words. If `null` no maximum will be enforced.             |\n| colorWarning | `string`   | #ff922b | A CSS color code used when approaching the maximum word count.                 |\n| colorError   | `string`   | #e5554e | A CSS color code used when below the minimum or above the maximum word count.  |\n| onChange     | `Function` | -       | Callback function invoked when the quill text changes. This function is passed  3 parameters: the `quill` object, the module options object, and the current number of words. |\n| l10n         | `Object`   | -       | An object of language strings used in the module's UI. See Localization below. |\n\n### Options: Localization\n\n| Option Key    | Type       | Default | Description                                                                    |\n| ------------- | ---------- | ------- | ------------------------------------------------------------------------------ |\n| l10n.singular | `string`   | word    | The singular unit.                                                             |\n| l10n.plural   | `string`   | words   | The plural unit.                                                               |\n| l10n.min      | `string`   | Minimum | Text to display for minimum count.                                             |\n| l10n.max      | `string`   | Maximum | Text to display for maximum count.                                             |\n\n\n## API Docs\n\n<!-- START TOKEN(Autogenerated API docs) -->\n\n### default\n\nRegisters the Word Count module with Quill.\n\n_Returns_\n\n-   `Boolean`: Returns `true` when registered and `false` if Quill is not available.\n\n\n<!-- END TOKEN(Autogenerated API docs) -->\n"
  },
  {
    "path": "packages/quill-wordcount-module/babel.config.js",
    "content": "/**\n * Babel config.\n *\n * @since 2.0.0\n * @version 2.0.0\n */\n\nconst presets = [ '@wordpress/default' ];\n\nmodule.exports = { presets };\n"
  },
  {
    "path": "packages/quill-wordcount-module/package.json",
    "content": "{\n  \"name\": \"@lifterlms/quill-wordcount-module\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Wordcount module for Quill.js\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/trunk/packages/quill-wordcount-module\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"utils\",\n    \"ui\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/utils\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%quill-wordcount-module\"\n  },\n  \"main\": \"src/index.js\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"scripts\": {\n    \"docgen\": \"docgen src/index.js --output README.md --to-token\",\n    \"dev\": \"./../dev/src/index.js\",\n    \"lint:js\": \"wp-scripts lint-js ./\",\n    \"test\": \"wp-scripts test-unit-js ./ --config ../scripts/config/jest-unit.config.js --verbose\"\n  },\n  \"dependencies\": {\n    \"words-count\": \"^2.0.2\"\n  }\n}\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/create-container.js",
    "content": "import formatNumber from './format-number';\n\n/**\n * Creates a new DOM node for the min or max count display.\n *\n * @since 2.0.0\n *\n * @param {string} classNameSuffix Suffix to add to the element's classname.\n * @param {string} text            Text to display.\n * @param {number} limit           Word count limit.\n * @return {Element} A DOM node element.\n */\nfunction createCounterNode( classNameSuffix, text, limit ) {\n\tconst node = document.createElement( 'i' );\n\n\tnode.className = `ql-wordcount-${ classNameSuffix }`;\n\n\tnode.style.opacity = '0.5';\n\tnode.style.marginRight = '10px';\n\n\tnode.innerHTML = `${ text }: ${ formatNumber( limit ) }`;\n\n\treturn node;\n}\n\n/**\n * Creates a container element to house the wordcount module UI.\n *\n * @since 2.0.0\n *\n * @param {Object} options A `WordCountModuleOptions` options object.\n * @return {Element} The container DOM node element.\n */\nexport default function( options ) {\n\tconst { l10n, min, max } = options,\n\t\tcontainer = document.createElement( 'div' );\n\n\tcontainer.className = 'ql-wordcount ql-toolbar ql-snow';\n\tcontainer.style.marginTop = '-1px';\n\tcontainer.style.fontSize = '85%';\n\n\tif ( min ) {\n\t\tcontainer.appendChild( createCounterNode( 'min', l10n.min, min ) );\n\t}\n\n\tif ( max ) {\n\t\tcontainer.appendChild( createCounterNode( 'max', l10n.max, max ) );\n\t}\n\n\treturn container;\n}\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/format-number.js",
    "content": "/**\n * Formats a number using the builtin {@see Intl.NumberFormat}.\n *\n * @since 2.0.0\n *\n * @param {number} number An integer, float, or numerical string.\n * @return {string} The formatted number string.\n */\nexport default function( number ) {\n\treturn new Intl.NumberFormat().format( number );\n}\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/get-counter-text-color.js",
    "content": "/**\n * Retrieves the text-color to use to denote word count errors or warnings.\n *\n * @since 2.0.0\n *\n * @param {number} wordCount The current word count in the Quill editor instance.\n * @param {Object} options   A `WordCountModuleOptions` options object.\n * @return {string} The CSS color code to use.\n */\nexport default function( wordCount, options ) {\n\tconst { min, max, colorWarning, colorError } = options;\n\n\tlet color = 'initial';\n\n\tif ( ( min && wordCount < min ) || ( max && wordCount > max ) ) {\n\t\tcolor = colorError;\n\t} else if ( max && wordCount >= max * 0.9 ) {\n\t\tcolor = colorWarning;\n\t}\n\n\treturn color;\n}\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/index.js",
    "content": "import wordCountModule from './module';\n\n/**\n * Registers the Word Count module with Quill.\n *\n * @since 2.0.0\n *\n * @return {boolean} Returns `true` when registered and `false` if Quill is not available.\n */\nexport default function() {\n\tconst { Quill } = window;\n\tif ( undefined === Quill ) {\n\t\treturn false;\n\t}\n\n\tQuill.register( 'modules/wordcount', wordCountModule );\n\treturn true;\n}\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/module.js",
    "content": "import wordsCount from 'words-count';\n\nimport createContainer from './create-container';\nimport getCounterTextColor from './get-counter-text-color';\nimport formatNumber from './format-number';\n\n/**\n * The modules options object.\n *\n * @typedef {Object} WordCountModuleOptions\n *\n * @property {?number}  min           The minimum required words. If `null` no minimum will be enforced.\n * @property {?number}  max           The maximum required words. If `null` no maximum will be enforced.\n * @property {string}   colorWarning  A CSS color code used when approaching the maximum word count.\n * @property {string}   colorError    A CSS color code used when below the minimum or above the maximum word count.\n * @property {Function} onChange      Callback function invoked when the quill text changes. This function is passed\n *                                    3 parameters: the `quill` object, the module options object, and the current number of words.\n * @property {Object}   l10n          An object of language strings used in the module's UI.\n * @property {string}   l10n.singular The singular unit, default \"word\".\n * @property {string}   l10n.plural   The plurarl unit, default \"words\".\n * @property {string}   l10n.min      Text to display for minimum count, default \"Minimum\".\n * @property {string}   l10n.max      Text to display for maximum count, default \"Maximum\".\n */\n\n/**\n * Merges default options into the supplied options ensuring all necessary options in exist in the resulting object.\n *\n * @since 2.0.0\n *\n * @param {WordCountModuleOptions} options A full or partial options object.\n * @return {WordCountModuleOptions} A full options object.\n */\nexport function setOptions( options = {} ) {\n\toptions = {\n\t\t...{\n\t\t\tmin: null,\n\t\t\tmax: null,\n\t\t\tcolorWarning: '#ff922b', // Orange.\n\t\t\tcolorError: '#e5554e', // Red.\n\t\t\tonChange: () => {},\n\t\t\tl10n: {},\n\t\t},\n\t\t...options,\n\t};\n\n\toptions.l10n = {\n\t\t...{\n\t\t\tsingular: 'word',\n\t\t\tplural: 'words',\n\t\t\tmin: 'Minimum',\n\t\t\tmax: 'Maximum',\n\t\t},\n\t\t...options.l10n,\n\t};\n\n\treturn options;\n}\n\n/**\n * The Quill Word Count Module.\n *\n * @since 2.0.0\n *\n * @param {Object}                 quill   A `Quill` editor instance.\n * @param {WordCountModuleOptions} options A full or partial options object.\n * @return {void}\n */\nexport default function( quill, options = {} ) {\n\toptions = setOptions( options );\n\n\tconst container = createContainer( options ),\n\t\tcounter = document.createElement( 'span' );\n\n\tcounter.className = 'ql-wordcount-counter';\n\tcounter.style.float = 'right';\n\n\tcontainer.appendChild( counter );\n\n\tconst updateCounter = () => {\n\t\tconst wordCount = wordsCount( quill.getText() );\n\n\t\tcounter.style.color = getCounterTextColor( wordCount, options );\n\n\t\tconst unit = 1 === wordCount ? options.l10n.singular : options.l10n.plural;\n\t\tcounter.innerHTML = formatNumber( wordCount ) + ' ' + unit;\n\n\t\toptions.onChange( quill, options, wordCount );\n\t};\n\n\tupdateCounter();\n\n\tquill.container.parentNode.insertBefore( container, quill.container.nextSibling );\n\n\tquill.on( 'text-change', updateCounter );\n}\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/test/__snapshots__/create-container.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`createContainer() Max and min 1`] = `\n<div\n  class=\"ql-wordcount ql-toolbar ql-snow\"\n  style=\"margin-top: -1px; font-size: 85%;\"\n>\n  <i\n    class=\"ql-wordcount-min\"\n    style=\"opacity: 0.5; margin-right: 10px;\"\n  >\n    Min: 1\n  </i>\n  <i\n    class=\"ql-wordcount-max\"\n    style=\"opacity: 0.5; margin-right: 10px;\"\n  >\n    Max: 2\n  </i>\n</div>\n`;\n\nexports[`createContainer() Max and no min 1`] = `\n<div\n  class=\"ql-wordcount ql-toolbar ql-snow\"\n  style=\"margin-top: -1px; font-size: 85%;\"\n>\n  <i\n    class=\"ql-wordcount-max\"\n    style=\"opacity: 0.5; margin-right: 10px;\"\n  >\n    Max: 1,000\n  </i>\n</div>\n`;\n\nexports[`createContainer() Min and no max 1`] = `\n<div\n  class=\"ql-wordcount ql-toolbar ql-snow\"\n  style=\"margin-top: -1px; font-size: 85%;\"\n>\n  <i\n    class=\"ql-wordcount-min\"\n    style=\"opacity: 0.5; margin-right: 10px;\"\n  >\n    Min: 5\n  </i>\n</div>\n`;\n\nexports[`createContainer() No min and no max 1`] = `\n<div\n  class=\"ql-wordcount ql-toolbar ql-snow\"\n  style=\"margin-top: -1px; font-size: 85%;\"\n/>\n`;\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/test/__snapshots__/module.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`module module() Chinese characters and default options 1`] = `\"<div id=\\\\\"ql-editor\\\\\">我去了一家中餐馆，买了一条面包。</div><div class=\\\\\"ql-wordcount ql-toolbar ql-snow\\\\\" style=\\\\\"margin-top: -1px; font-size: 85%;\\\\\"><i class=\\\\\"ql-wordcount-min\\\\\" style=\\\\\"opacity: 0.5; margin-right: 10px;\\\\\">Minimum: 1</i><i class=\\\\\"ql-wordcount-max\\\\\" style=\\\\\"opacity: 0.5; margin-right: 10px;\\\\\">Maximum: 14</i><span class=\\\\\"ql-wordcount-counter\\\\\" style=\\\\\"float: right; color: rgb(255, 146, 43);\\\\\">14 words</span></div>\"`;\n\nexports[`module module() Latin characters and specified options 1`] = `\"<div id=\\\\\"ql-editor\\\\\">Lorem ipsum dolor sit.</div><div class=\\\\\"ql-wordcount ql-toolbar ql-snow\\\\\" style=\\\\\"margin-top: -1px; font-size: 85%;\\\\\"><i class=\\\\\"ql-wordcount-min\\\\\" style=\\\\\"opacity: 0.5; margin-right: 10px;\\\\\">Minimum: 1</i><i class=\\\\\"ql-wordcount-max\\\\\" style=\\\\\"opacity: 0.5; margin-right: 10px;\\\\\">Max: 1,000</i><span class=\\\\\"ql-wordcount-counter\\\\\" style=\\\\\"float: right;\\\\\">4 words</span></div>\"`;\n\nexports[`module module() No starting text and default options 1`] = `\"<div id=\\\\\"ql-editor\\\\\"></div><div class=\\\\\"ql-wordcount ql-toolbar ql-snow\\\\\" style=\\\\\"margin-top: -1px; font-size: 85%;\\\\\"><span class=\\\\\"ql-wordcount-counter\\\\\" style=\\\\\"float: right;\\\\\">0 words</span></div>\"`;\n\nexports[`module setOptions() Specify min and max 1`] = `\nObject {\n  \"colorError\": \"#e5554e\",\n  \"colorWarning\": \"#ff922b\",\n  \"l10n\": Object {\n    \"max\": \"Maximum\",\n    \"min\": \"Minimum\",\n    \"plural\": \"words\",\n    \"singular\": \"word\",\n  },\n  \"max\": 1000,\n  \"min\": 5,\n  \"onChange\": [Function],\n}\n`;\n\nexports[`module setOptions() Specify onChange function 1`] = `\nObject {\n  \"colorError\": \"#e5554e\",\n  \"colorWarning\": \"#ff922b\",\n  \"l10n\": Object {\n    \"max\": \"Maximum\",\n    \"min\": \"Minimum\",\n    \"plural\": \"words\",\n    \"singular\": \"word\",\n  },\n  \"max\": null,\n  \"min\": null,\n  \"onChange\": [Function],\n}\n`;\n\nexports[`module setOptions() Specify partial l10n object 1`] = `\nObject {\n  \"colorError\": \"#e5554e\",\n  \"colorWarning\": \"#ff922b\",\n  \"l10n\": Object {\n    \"max\": \"Maximum\",\n    \"min\": \"Min\",\n    \"plural\": \"words\",\n    \"singular\": \"word\",\n  },\n  \"max\": null,\n  \"min\": null,\n  \"onChange\": [Function],\n}\n`;\n\nexports[`module setOptions() Use defaults 1`] = `\nObject {\n  \"colorError\": \"#e5554e\",\n  \"colorWarning\": \"#ff922b\",\n  \"l10n\": Object {\n    \"max\": \"Maximum\",\n    \"min\": \"Minimum\",\n    \"plural\": \"words\",\n    \"singular\": \"word\",\n  },\n  \"max\": null,\n  \"min\": null,\n  \"onChange\": [Function],\n}\n`;\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/test/create-container.test.js",
    "content": "import createContainer from '../create-container';\n\ndescribe( 'createContainer()', () => {\n\tconst testData = [\n\t\t[ 'No min and no max', { min: null, max: null } ],\n\t\t[ 'Min and no max', { min: 5, max: null, l10n: { min: 'Min' } } ],\n\t\t[ 'Max and no min', { min: null, max: 1000, l10n: { max: 'Max' } } ],\n\t\t[ 'Max and min', { min: 1, max: 2, l10n: { min: 'Min', max: 'Max' } } ],\n\t];\n\ttest.each( testData )( '%s', ( testName, opts ) => {\n\t\texpect( createContainer( opts ) ).toMatchSnapshot();\n\t} );\n} );\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/test/format-number.test.js",
    "content": "import formatNumber from '../format-number';\n\ndescribe( 'formatNumber()', () => {\n\tconst testData = [\n\t\t[ 1000, '1,000' ],\n\t\t[ 1000.95, '1,000.95' ],\n\t\t[ '1000', '1,000' ],\n\t\t[ 1, '1' ],\n\t\t[ 1.00, '1' ],\n\t\t[ 0.01, '0.01' ],\n\t\t[ 9999999, '9,999,999' ],\n\t];\n\ttest.each( testData )( '%s', ( input, expected ) => {\n\t\texpect( formatNumber( input ) ).toStrictEqual( expected );\n\t} );\n} );\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/test/get-counter-text-color.js",
    "content": "import getCounterTextColor from '../get-counter-text-color';\n\ndescribe( 'getCounterTextColor()', () => {\n\tconst testData = [\n\t\t[ 'No min and no max', 'initial', 5, { min: null, max: null } ],\n\t\t[ 'Under min', 'red', 5, { min: 10, max: null, colorError: 'red' } ],\n\t\t[ 'Over max', 'red', 5, { min: null, max: 3, colorError: 'red' } ],\n\t\t[ 'Approaching max', 'orange', 9, { min: null, max: 10, colorWarning: 'orange' } ],\n\t\t[ 'Within min and max', 'initial', 8, { min: 7, max: 10 } ],\n\t];\n\ttest.each( testData )( '%s', ( testName, expected, count, opts ) => {\n\t\texpect( getCounterTextColor( count, opts ) ).toBe( expected );\n\t} );\n} );\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/test/index.test.js",
    "content": "import registerWordCountModule from '../index';\nimport wordCountModule from '../module';\n\ndescribe( 'registerWordCountModule()', () => {\n\ttest( 'Quill is not available', () => {\n\t\twindow.Quill = undefined;\n\t\texpect( registerWordCountModule() ).toStrictEqual( false );\n\t} );\n\n\ttest( 'Quill is available', () => {\n\t\twindow.Quill = {\n\t\t\tregister: jest.fn(),\n\t\t};\n\t\texpect( registerWordCountModule() ).toStrictEqual( true );\n\t\texpect( window.Quill.register ).toHaveBeenCalledWith( 'modules/wordcount', wordCountModule );\n\t} );\n} );\n"
  },
  {
    "path": "packages/quill-wordcount-module/src/test/module.test.js",
    "content": "import wordCountModule, { setOptions } from '../module';\n\n/**\n * Mock Quill class.\n *\n * @since 2.0.0\n *\n * @param {Element} container DOM element container.\n * @return {MockQuill} A mock Quill instance.\n */\nfunction MockQuill( container ) {\n\tthis.container = container;\n\n\tthis.getText = function() {\n\t\treturn container.innerHTML;\n\t};\n\n\tthis.on = function( action, callback ) {\n\t\tcallback();\n\t};\n\n\treturn this;\n}\n\ndescribe( 'module', () => {\n\tdescribe( 'setOptions()', () => {\n\t\tconst testData = [\n\t\t\t[ 'Use defaults', {}, undefined ],\n\t\t\t[ 'Specify min and max', { min: 5, max: 1000 }, undefined ],\n\t\t\t[ 'Specify partial l10n object', { l10n: { min: 'Min' } }, undefined ],\n\t\t\t[ 'Specify onChange function', { onChange: () => true }, true ],\n\t\t];\n\t\ttest.each( testData )( '%s', ( testName, opts, expectedCallbackResult ) => {\n\t\t\tconst merged = setOptions( opts );\n\t\t\texpect( setOptions( opts ) ).toMatchSnapshot();\n\t\t\texpect( merged.onChange() ).toStrictEqual( expectedCallbackResult );\n\t\t} );\n\t} );\n\n\tdescribe( 'module()', () => {\n\t\tconst testData = [\n\t\t\t[ 'No starting text and default options', '', {} ],\n\t\t\t[ 'Latin characters and specified options', 'Lorem ipsum dolor sit.', { min: 1, max: 1000, l10n: { max: 'Max' } } ],\n\t\t\t[ 'Chinese characters and default options', '我去了一家中餐馆，买了一条面包。', { min: 1, max: 14 } ],\n\t\t];\n\t\ttest.each( testData )( '%s', ( testName, startingText, opts ) => {\n\t\t\tconst container = document.createElement( 'div' );\n\n\t\t\tcontainer.id = 'ql-editor';\n\t\t\tcontainer.innerHTML = startingText;\n\n\t\t\tdocument.body.appendChild( container );\n\n\t\t\twordCountModule( new MockQuill( container ), opts );\n\t\t\texpect( document.body.innerHTML ).toMatchSnapshot();\n\n\t\t\tdocument.body.innerHTML = '';\n\t\t} );\n\t} );\n} );\n"
  },
  {
    "path": "packages/scripts/.eslintrc.js",
    "content": "const config = require( './config/.eslintrc.js' );\nmodule.exports = config;\n"
  },
  {
    "path": "packages/scripts/.llmsdev.yml",
    "content": "update-version:\n  skip-config: true\n"
  },
  {
    "path": "packages/scripts/.npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "packages/scripts/CHANGELOG.md",
    "content": "@lifterlms/scripts CHANGELOG\n============================\nUnreleased\n----------\n+ When allow the whole `@lifterlms` package to be transpiled by babel during builds, skip those module's rules which have no loader specified.\n\n\nv4.0.1 - 2023-04-18\n----------\n\n+ Allow the whole `@lifterlms` package to be transpiled by babel during builds.\n\n\nv4.0.0 - 2022-08-11\n-------------------\n\n+ **[Breaking]** Upgrade `@wordpress/scripts` to [23.6.0](https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/CHANGELOG.md#2360-2022-07-27).\n+ **[Breaking]** Removes the webpack copy plugin responsible for copying `block.json` files from the `src/` directory to the `${outputDir}/blocks` directory.\n+ Added a blocks-specific webpack config: `./config/blocks-webpack.config.js`.\n+ Ignore `lodash` as an undefined dependency when using `config/.eslintrc.js`.\n\n\nv3.2.0 - 2022-07-12\n-------------------\n\n+ Added eslint configuration overrides for jest unit and e2e tests.\n+ Updated webpack.config so to allow the deletion of files inside the protected directories (assets/{js|css}).\n+ Automatically exclude the local `./tmp` directory when running JS Unit tests using the `config/jest-unit.config.js` file.\n\n\nv3.1.0 - 2022-03-30\n-------------------\n\n+ Upgraded `@wordpress/scripts` to [v22.2.0](https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/CHANGELOG.md#2220-2022-03-11).\n\n\nv3.0.0 - 2022-03-08\n-------------------\n\n+ **[Breaking]** Upgrade `@wordpress/scripts` to [22.1.0](https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/CHANGELOG.md#2210-2022-03-03).\n+ Added a custom Jest matcher, `toMatchStringWithQuotes()` to allow easy testing for strings that may be texturized by `wp_texturize()` depending on the theme.\n+ Added jest testing helper functions, `testIf()` and `describeIf()` to allow simple conditional tests.\n+ Improved e2e test logging to filter out \"noisy\" console messages originating, primarily, from the WordPress core.\n+ Removed `e2e/global-teardown.js` in favor of using the `WP_ARTIFACTS_PATH` env var for determining the storage location of e2e test artifacts (screenshots and snapshots).\n+ Internal modules can be defined as WordPress script dependencies by using `llms-{$package_name}` and accessed via `window.llms.{$package_name}`.\n+ Any `@wordpress/*` modules are automatically resolved for the purposes of `eslint-plugin-import` rules.\n\n\nv2.2.0 - 2022-01-31\n-------------------\n\n+ Update: `@wordpress/scripts` to [20.0.2](https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/CHANGELOG.md#2002-2022-01-31).\n+ Update: `@jest/test-sequencer` to [27.4.6](https://github.com/facebook/jest/releases/tag/v27.4.6).\n+ Update: The e2e bootstrap file will automatically attempt to intuit the WordPress core version being tested and store it in the `process.env.WP_VERSION`.\n\n\nv2.1.0 - 2021-12-13\n-------------------\n\n+ Added webpack configuration option to customize the `cleanAfterEveryBuildPatterns` setting of the `CleanWebpackPlugin`.\n\n\nv2.0.0 - 2021-11-05\n-------------------\n\n+ **[Breaking]** Raised the minimum required `@wordpress/scripts` version to 18.1.0.\n+ **[Breaking]** Removes the failed test screenshot reporter in favor of the reporter included with `@wordpress/scripts`.\n+ **[Breaking]** Failed test screenshots are now stored in the `tmp/artifacts` directory.\n+ **[BREAKING]** Remove the default `DependencyExtractionWebpackPlugin` in favor of our custom loader from generated webpack configs.\n+ Adds env var loading from `.llmsenv` with a fallback to `.llmsenv.dist`. The former file intended to be excluded from version control systems.\n+ Adds a default `.eslintrc.js` configuration intended for use by LifterLMS and LifterLMS projects (via `wp-scripts lint-js`).\n\n\nv2.0.0-beta.1 - 2021-09-10\n--------------------------\n\n+ **[Breaking]** Raised the minimum required `@wordpress/scripts` version to 17.1.0.\n+ **[Breaking]** Removes the failed test screenshot reporter in favor of the reporter included with `@wordpress/scripts`.\n+ **[Breaking]** Failed test screenshots are now stored in the `tmp/artifacts` directory.\n+ Adds env var loading from `.llmsenv` with a fallback to `.llmsenv.dist`. The former file intended to be excluded from version control systems.\n+ Adds a default `.eslintrc.js` configuration intended for use by LifterLMS and LifterLMS projects (via `wp-scripts lint-js`).\n\n\nv1.3.3 - 2021-01-07\n-------------------\n\n+ Updated screenshot reporter function to include additional debugging information\n\n\nv1.3.1 - 2020-08-11\n-------------------\n\n+ Don't use imports.\n\n\nv1.3.0 - 2020-08-11\n-------------------\n\n+ Modify the `jest-puppeteer.config.js` to use defaults from `@wordpress/scripts`.\n\n\nv1.2.4 - 2020-08-10\n-------------------\n\n+ Resolve script files for better portability.\n\n\nv1.2.3 - 2020-08-10\n-------------------\n\n+ Add a configurable source file path option and set the default to `src/` instead of `assets/src` to the `webpack.config.js` generator.\n\n\nv1.2.1 - 2020-07-21\n-------------------\n\n+ Update webpack config code for reduced complexity.\n\n\nv1.2.0 - 2020-07-17\n-------------------\n\n+ Added webpack config \"generator\" method.\n"
  },
  {
    "path": "packages/scripts/README.md",
    "content": "LifterLMS Scripts\n=================\n\nTest, build, and development scripts for LifterLMS projects.\n\nThis package is inspired by and extends functionality provided by [@wordpress/scripts](https://github.com/WordPress/gutenberg/tree/master/packages/scripts), adding functionality specifically for testing, building, and developing LifterLMS projects and add-ons.\n\n## Installation\n\nInstall the module\n\n```\nnpm install --save-dev @lifterlms/scripts\n```\n\n## CHANGELOG\n\n[CHANGELOG](./CHANGELOG.md)\n\n## Configuration Files\n\n### WordPress Blocks Webpack Configuration File\n\nThe [blocks-webpack.config.js](./config/blocks-webpack.config.js) is a Webpack config file meant to build WordPress blocks found within the the project's `src/blocks` directory. The distribution directory is `blocks/`.\n\nThe config will automatically build blocks, compile SCSS to CSS, move the block.json file, and copy all PHP files for each block in the source directory.\n\n#### Example Usage\n\nCreate a `webpack.config.js` in your project's root with the following:\n\n```js \nconst blocksConfig = require( '@lifterlms/scripts/config/blocks-webpack.config' );\nmodule.exports = blocksConfig;\n````\n\n#### Configuration\n\nThe configuration assumes a project directory following this structure:\n\n```\na-plugin/\n|-- a-plugin.php\n|-- assets/\n|   |-- css/\n|   |-- js/\n|-- blocks/\n|   |-- block-a/\n|   |-- block-b/\n|-- README.md\n|-- includes/\n|-- src/\n|   |-- blocks/\n|   |   |-- block-a/\n|   |   |   |-- block.json\n|   |   |   |-- index.js\n|   |   |   |-- styles.scss\n|   |   |-- block-b/\n|   |       |-- block.json\n|   |       |-- index.js\n|   |       |-- index.php\n|   |       |-- styles.scss\n|   |-- js/\n|   |-- scss/\n|-- webpack.config.js\n```\n\n#### Expected filenames\n\nThe script builds scripts and styles according to definitions found in the `block.json` files.\n\n```\neditorScript - index.js\nviewScript   - view.js\nscript       - script.js\nstyle        - styles.scss (styles.css)\neditorStyle  - editor.scss (editor.css)\n*.php        - *.php\n```\n\n\n### ESLint Plugin\n\nThe [eslint](./config/.eslintrc.js) configuration file specifies a shared set of rules for linting Javascript files across LifterLMS projects.\n\nThe configuration is a modified version of the [@wordpress/eslint-plugin/recommended-with-formatting](https://github.com/WordPress/gutenberg/blob/trunk/packages/eslint-plugin/configs/recommended-with-formatting.js).\n\nExample usage `.eslintrc.js`\n\n```js\nconst config = require( '@lifterlms/scripts/config/.eslintrc.js' );\nmodule.exports = config;\n```\n"
  },
  {
    "path": "packages/scripts/babel.config.js",
    "content": "const presets = [ '@wordpress/default' ];\nmodule.exports = { presets };\n"
  },
  {
    "path": "packages/scripts/config/.eslintrc.js",
    "content": "/**\n * Default eslint config for LifterLMS projects\n *\n * @package\n *\n * @since Unknown\n * @version [version]\n */\n\nconst eslintConfig = {\n\troot: true,\n\textends: [\n\t\t'plugin:@wordpress/eslint-plugin/recommended-with-formatting',\n\t],\n\trules: {\n\t\t'jsdoc/tag-lines': [ 0 ],\n\t\t'jsdoc/require-jsdoc': 'error',\n\t\t'jsdoc/require-param-description': 'error',\n\t\t'jsdoc/require-returns': 'error',\n\t},\n\tsettings: {\n\t\t'import/core-modules': [\n\t\t\t// @todo: This list needs to be expanded to include other WP Core included modules.\n\t\t\t'jquery',\n\t\t\t'lodash',\n\t\t],\n\t\t'import/resolver': __dirname + '/import-resolver.js',\n\t},\n\t/**\n\t * Add overrides for test files.\n\t *\n\t * @see {@link https://github.com/WordPress/gutenberg/blob/1749166b9f5d7cb536d82e82a94ccffae53300eb/packages/eslint-plugin/configs/recommended-with-formatting.js#L53-L63}\n\t */\n\toverrides: [\n\t\t{\n\t\t\t// Unit test files and their helpers only.\n\t\t\tfiles: [ '**/@(test|__tests__)/**/*.js', '**/?(*.)test.js' ],\n\t\t\textends: [ 'plugin:@wordpress/eslint-plugin/test-unit' ],\n\t\t},\n\t\t{\n\t\t\t// End-to-end test files and their helpers only.\n\t\t\tfiles: [ 'tests/e2e/**/?(*.)test.js', '**/specs/**/*.js', '**/?(*.)spec.js' ],\n\t\t\textends: [ 'plugin:@wordpress/eslint-plugin/test-e2e' ],\n\t\t},\n\t],\n};\n\nmodule.exports = eslintConfig;\n"
  },
  {
    "path": "packages/scripts/config/blocks-webpack.config.js",
    "content": "/**\n * A Webpack configuration for building WordPress blocks.\n *\n * @since 4.0.0\n * @version 4.0.0\n */\n\n/* eslint-disable no-console */\n\nprocess.env.WP_SRC_DIRECTORY = process.env.WP_SRC_DIRECTORY || './src';\n\nconst BLOCK_METADATA_GLOB = '**/block.json';\n\nconst RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' ),\n\t{ readFileSync } = require( 'fs' ),\n\t{ sync: glob } = require( 'fast-glob' ),\n\t{ resolve, dirname, join, extname, sep } = require( 'path' );\n\nconst config = require( '@wordpress/scripts/config/webpack.config' ),\n\tblockEntries = 'function' === typeof config.entry ? config.entry() : config.entry;\n\n/**\n * Remove the leading blocks/ from all the block file entrypoints.\n *\n * This ensures that distributed blocks are in the blocks/ directory, not the blocks/blocks directory.\n *\n * @since 4.0.0\n * @version 4.0.0\n *\n * @return {Object} A webpack entries object.\n */\nconfig.entry = () => {\n\ttry {\n\t\tconst entries = Object.fromEntries(\n\t\t\tObject.entries( blockEntries ).map( ( [ key, val ] ) => {\n\t\t\t\treturn [ key.replace( 'blocks/', '' ), val ];\n\t\t\t} )\n\t\t);\n\n\t\tconst blockMetadataFiles = glob(\n\t\t\t`${ resolve( process.env.WP_SRC_DIRECTORY ) }${ sep }${ BLOCK_METADATA_GLOB }`,\n\t\t\t{\n\t\t\t\tabsolute: true,\n\t\t\t}\n\t\t);\n\t\tblockMetadataFiles.forEach( ( jsonFilePath ) => {\n\t\t\t/**\n\t\t\t * Add SCSS file entries for any block styles that aren't already compiled via JS import in the related JS file.\n\t\t\t *\n\t\t\t * By default each script file, script, viewScript, and editorScript will also build css files if the related .scss file is\n\t\t\t * imported in the JS file. The below enables us to utilize a stylesheet without an associated script, for example if we wish\n\t\t\t * to load viewStyle without having to have a viewScript file.\n\t\t\t */\n\t\t\tconst { style, editorStyle, viewStyle } = JSON.parse( readFileSync( jsonFilePath ) );\n\t\t\t[ style, editorStyle, viewStyle ]\n\t\t\t\t.flat()\n\t\t\t\t.filter( ( value ) => value && value.startsWith( 'file:' ) )\n\t\t\t\t.forEach( ( value ) => {\n\t\t\t\t\t// Removes the `file:` prefix.\n\t\t\t\t\tconst filepath = join(\n\t\t\t\t\t\tdirname( jsonFilePath ),\n\t\t\t\t\t\tvalue.replace( 'file:', '' )\n\t\t\t\t\t);\n\n\t\t\t\t\tconst entryName = filepath\n\t\t\t\t\t\t.replace( extname( filepath ), '' )\n\t\t\t\t\t\t.replace( resolve( process.env.WP_SRC_DIRECTORY ) + sep + 'blocks' + sep, '' )\n\t\t\t\t\t\t.replace( /\\\\/g, '/' );\n\n\t\t\t\t\tif ( ! Object.keys( entries ).includes( entryName ) ) {\n\t\t\t\t\t\tentries[ entryName ] = filepath.replace( '.css', '.scss' );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t} );\n\n\t\treturn entries;\n\t} catch ( e ) {\n\t\tconsole.log( 'Error: ' );\n\t\tconsole.log( '' );\n\t\tconsole.log( '' );\n\t\tconsole.log( '' );\n\t\tconsole.error( e );\n\t\tconsole.log( '' );\n\t\tconsole.log( '' );\n\t\tconsole.log( '' );\n\t\tconsole.log( '' );\n\t}\n\n\treturn {};\n};\n\n// Put blocks in the blocks/ dir not the build/ dir.\nconfig.output.path = resolve( process.cwd(), 'blocks' );\n\nconfig.plugins.forEach( ( plugin ) => {\n\tconst { name: pluginName } = plugin.constructor;\n\n\tif ( 'CopyPlugin' === pluginName && BLOCK_METADATA_GLOB === plugin.patterns[ 0 ].from ) {\n\t\t/**\n\t\t * Modifies the copy plugin that moves block.json files from src/blocks -> blocks/.\n\t\t *\n\t\t * The default plugin moves the block.json file to blocks/blocks/[block-dir]/block.json.\n\t\t */\n\t\tplugin.patterns[ 0 ].context = 'src/blocks';\n\n\t\t/**\n\t\t * Copies block PHP files.\n\t\t *\n\t\t * This is a pattern the `@wordpress/block-library` follows.\n\t\t */\n\t\tplugin.patterns.push( {\n\t\t\tfrom: '**/**.php',\n\t\t\tcontext: 'src/blocks',\n\t\t\tnoErrorOnMissing: true,\n\t\t} );\n\t}\n} );\n\n// Removes empty .js files created when adding SCSS files to the entries array.\nconfig.plugins.push( new RemoveEmptyScriptsPlugin() );\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/scripts/config/import-resolver.js",
    "content": "const { interfaceVersion, resolve: originalResolve } = require( 'eslint-import-resolver-node' );\n\nexports.interfaceVersion = interfaceVersion;\n\n/**\n * Determine if an imported module / path is a WordPress package.\n *\n * @since 3.0.0\n *\n * @param {string} source Module name or path.\n * @return {boolean} Returns `true` if the module looks like a WordPress package.\n */\nfunction isWordPress( source ) {\n\treturn source.startsWith( '@wordpress/' );\n}\n\n/**\n * Resolves an imported dependency.\n *\n * @since 3.0.0\n *\n * @param {string}  source Module name or path\n * @param {string}  file   File path.\n * @param {?Object} config Configuration object.\n * @return {Object} The resolution object.\n */\nexports.resolve = function( source, file, config ) {\n\tif ( isWordPress( source ) ) {\n\t\treturn {\n\t\t\tfound: true,\n\t\t\tpath: null,\n\t\t};\n\t}\n\treturn originalResolve( source, file, config );\n};\n"
  },
  {
    "path": "packages/scripts/config/jest-unit.config.js",
    "content": "/**\n * Main Jest config\n *\n * @since Unknown\n * @version 3.1.0\n */\n\nconst\n\t// Import the initial config to be moified.\n\tconfig = require( '@wordpress/scripts/config/jest-unit.config' ),\n\ttestPathIgnorePatterns = config.testPathIgnorePatterns || [];\n\n// Set the root directory to the project's root.\nconfig.rootDir = process.cwd();\n\n// Exclude dev tmp directory automatically.\nconfig.testPathIgnorePatterns = [\n\t...testPathIgnorePatterns,\n\t'/node_modules/',\n\t'<rootDir>/tmp/',\n];\n\n/**\n * Jest Config\n *\n * @see {@link https://jestjs.io/docs/en/configuration.html}\n */\nmodule.exports = config;\n"
  },
  {
    "path": "packages/scripts/config/webpack.config.js",
    "content": "/**\n * Webpack config\n *\n * @package\n *\n * @since Unknown\n * @version [version] \n */\n\n// Deps.\nconst\n\tcssExtract = require( 'mini-css-extract-plugin' ),\n\tcssRTL = require( 'webpack-rtl-plugin' ),\n\tconfig = require( '@wordpress/scripts/config/webpack.config' ),\n\tdepExtract = require( '@wordpress/dependency-extraction-webpack-plugin' ),\n\tpath = require( 'path' );\n\n/**\n * Used by dependency extractor to handle requests to convert names of scripts included in the LifterLMS Core.\n *\n * @since 1.2.1\n * @since 3.0.0 Load `@lifterlms/*` packages into the `window.llms` namespace.\n *\n * @param {string} request External script slug/id.\n * @return {string | Array} A string\n */\nfunction requestToExternal( request ) {\n\tif ( 'llms-quill' === request ) {\n\t\treturn 'Quill';\n\t} else if ( 'llms-izimodal' === request ) {\n\t\treturn [ 'jQuery', 'iziModal' ];\n\t} else if ( request.startsWith( 'llms/' ) || request.startsWith( 'LLMS/' ) ) {\n\t\treturn request.split( '/' );\n\t} else if ( request.startsWith( '@lifterlms/' ) ) {\n\t\treturn [ 'llms', request.replace( '@lifterlms/', '' ) ];\n\t}\n}\n\n/**\n * Used by dependency extractor to handle requests to scripts included in the LifterLMS Core.\n *\n * @since 1.2.1\n * @since 3.0.0 Use `llms-*` as the script ID for `@lifterlms/*` packages.\n *\n * @param {string} request External script slug/id.\n * @return {string | Array} A string\n */\nfunction requestToHandle( request ) {\n\tif ( request.startsWith( 'llms/' ) || request.startsWith( 'LLMS/' ) ) {\n\t\treturn 'llms';\n\t} else if ( request.startsWith( '@lifterlms/' ) ) {\n\t\treturn request.replace( '@lifterlms/', 'llms-' );\n\t}\n}\n\n/**\n * Configure the `entry` object of the webpack config file.\n *\n * @since 1.2.1\n * @since 1.2.3 Add a configurable source file path.\n *\n * @param {string[]} js      Array of JS file slugs.\n * @param {string}   srcPath Relative path to the base source file directory.\n * @return {Object} Webpack config entry object.\n */\nfunction setupEntry( js, srcPath ) {\n\tconst entry = {};\n\tjs.forEach( ( file ) => {\n\t\tentry[ file ] = path.resolve( process.cwd(), `${ srcPath }js/`, `${ file }.js` );\n\t} );\n\n\treturn entry;\n}\n\n/**\n * Setup the `plugins` array of the webpack config file.\n *\n * @since 1.2.1\n * @since 2.0.0 Remove default DependencyExtractionWebpackPlugin in favor of our custom loader.\n * @since 2.1.0 Added `cleanAfterEveryBuildPatterns` parameter.\n * @since 3.1.0 Add `protectWebpackAssets = false` to the `CleanWebpackPlugin` config.\n * @since 4.0.0 Remove the copy plugin pattern responsible for copying block.json files.\n *\n * @param {Object[]} plugins                      Array of plugin objects or classes.\n * @param {string[]} css                          Array of CSS file slugs.\n * @param {string}   prefix                       File prefix.\n * @param {string[]} cleanAfterEveryBuildPatterns List of patterns added to the CleanWebpackPlugin config.\n * @return {Object[]} Array of plugin objects or classes.\n */\nfunction setupPlugins( plugins, css, prefix, cleanAfterEveryBuildPatterns ) {\n\t// Modify the CleanWebpackPlugin's cleanAfterEveryBuildPatterns config.\n\tif ( cleanAfterEveryBuildPatterns.length ) {\n\t\tplugins = plugins.filter( ( plugin ) => {\n\t\t\tif ( 'CleanWebpackPlugin' === plugin.constructor.name ) {\n\t\t\t\tplugin.cleanAfterEveryBuildPatterns = [\n\t\t\t\t\t...plugin.cleanAfterEveryBuildPatterns,\n\t\t\t\t\t...cleanAfterEveryBuildPatterns,\n\t\t\t\t];\n\t\t\t\t// Allow removal of current webpack assets.\n\t\t\t\tplugin.protectWebpackAssets = false;\n\t\t\t}\n\n\t\t\treturn plugin;\n\t\t} );\n\t}\n\n\tconst REMOVE_PLUGINS = [\n\t\t/**\n\t\t * Remove the original WP Core dependency extractor. If we add an extractor\n\t\t * without removing the initial one core dependencies get lost when our\n\t\t * extractor runs.\n\t\t */\n\t\t'DependencyExtractionWebpackPlugin',\n\n\t\t/**\n\t\t * Remove the css extractor implemented in the default config.\n\t\t *\n\t\t * Our CSS extractor puts things in our preferred directory structure.\n\t\t */\n\t\t'MiniCssExtractPlugin',\n\t];\n\tplugins = plugins.filter( ( plugin ) => {\n\t\tconst { name: pluginName } = plugin.constructor;\n\n\t\t/**\n\t\t * Removes the copy plugin that copies block.json files from the src/ dir into the assets/blocks dir.\n\t\t *\n\t\t * Since we store blocks in the blocks/ dir we don't need this when compiling non-block assets.\n\t\t */\n\t\tif ( 'CopyPlugin' === pluginName && '**/block.json' === plugin.patterns[ 0 ].from ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn ! REMOVE_PLUGINS.includes( pluginName );\n\t} );\n\n\tcss.forEach( () => {\n\t\t// Extract CSS.\n\t\tplugins.push( new cssExtract( {\n\t\t\tfilename: `css/${ prefix }[name].css`,\n\t\t} ) );\n\n\t\t// Generate an RTL CSS file.\n\t\tplugins.push( new cssRTL( {\n\t\t\tfilename: `css/${ prefix }[name]-rtl.css`,\n\t\t} ) );\n\t} );\n\n\t// Add a custom dependency extractor.\n\tplugins.push( new depExtract( {\n\t\trequestToExternal,\n\t\trequestToHandle,\n\t\tinjectPolyfill: true,\n\t} ) );\n\n\treturn plugins;\n}\n\n/**\n * Allow babel transpilation for the whole `@lifterlms` package.\n *\n * By default the WordPress' webpack config excludes all the packages\n * in `node_modules/` from being transpiled by babel.\n * With this we allow the whole `@lifterlms` package to be transpiled by babel during builds.\n *\n * @since 4.0.1\n * @since [version] Fixed cases when the module's rule has no loader.\n *\n * @param {Object[]} config Webpack config.\n * @return {Object[]}\n */\nfunction allowBabelTranspilation( config ) {\n\tconfig.module.rules = config.module.rules.filter( ( rule ) => {\n\t\tif ( rule.exclude && '/node_modules/' === rule.exclude.toString() &&\n\t\t\t\trule.use[0].loader?.indexOf('node_modules/babel-loader') ) {\n\t\t\trule.exclude = /node_modules\\/(?!\\@lifterlms\\/)/;\n\t\t}\n\t\treturn rule;\n\t});\n\treturn config;\n}\n\n/**\n * Generates a Webpack config object.\n *\n * This is opinionated based on our opinions for directory structure.\n *\n * ESNext JS source files are located in `src/js`.\n *\n * SASS/SCSS source files are located in `src/sass`.\n *\n * SASS files should be imported via the JS source file.\n *\n * @since Unknown\n * @since 1.2.1 Reduce method size by using helper methods\n * @since 1.2.3 Add a configurable source file path option and set the default to `src/` instead of `assets/src`.\n * @since 2.1.0 Add configuration option added to the CleanWebpackPlugin.\n * @since 4.0.1 Parse the original WordPress' config to allow babel transpilation for the whole `@lifterlms` package.\n *\n * @param {Object}   options                              Configuration options.\n * @param {string[]} options.css                          Array of CSS file slugs.\n * @param {string[]} options.js                           Array of JS file slugs.\n * @param {string}   options.prefix                       File prefix.\n * @param {string}   options.outputPath                   Relative path to the output directory.\n * @param {string}   options.srcPath                      Relative path to the base source file directory.\n * @param {string[]} options.cleanAfterEveryBuildPatterns List of patterns added to the CleanWebpackPlugin config.\n * @return {Object} A webpack.config.js object.\n */\nmodule.exports = (\n\t{\n\t\tcss = [],\n\t\tjs = [],\n\t\tprefix = 'llms-',\n\t\toutputPath = 'assets/',\n\t\tsrcPath = 'src/',\n\t\tcleanAfterEveryBuildPatterns = [],\n\t}\n) => {\n\treturn {\n\t\t...allowBabelTranspilation( config ),\n\t\tentry: setupEntry( js, srcPath ),\n\t\toutput: {\n\t\t\tfilename: `js/${ prefix }[name].js`,\n\t\t\tpath: path.resolve( process.cwd(), outputPath ),\n\t\t},\n\t\tplugins: setupPlugins( config.plugins, css, prefix, cleanAfterEveryBuildPatterns ),\n\t};\n};\n"
  },
  {
    "path": "packages/scripts/e2e/bootstrap.js",
    "content": "/**\n * Tests Bootstrap.\n *\n * @since Unknown\n * @version 3.0.0\n */\n\n/* global jest, page, describe, test, beforeAll, expect  */\n/* eslint-disable no-console */\n\n/* eslint-disable-next-line import/no-extraneous-dependencies */\nrequire( 'regenerator-runtime' );\n\nconst { existsSync } = require( 'fs' ),\n\t{ execSync } = require( 'child_process' ),\n\t/* eslint-disable-next-line import/no-extraneous-dependencies */\n\t{ diff } = require( 'jest-diff' );\n\n// Load dotenv files.\nconst envFiles = [ '.llmsenv', '.llmsenv.dist' ];\nenvFiles.some( ( file ) => {\n\tconst path = `${ process.cwd() }/${ file }`;\n\tif ( existsSync( file ) ) {\n\t\trequire( 'dotenv' ).config( { path } );\n\t\treturn true;\n\t}\n\treturn false;\n} );\n\nif ( ! process.env.WP_VERSION ) {\n\ttry {\n\t\tconst wpVersion = execSync( 'composer run env wp core version', { stdio: 'pipe' } ).toString();\n\t\tif ( wpVersion ) {\n\t\t\tprocess.env.WP_VERSION = wpVersion;\n\t\t}\n\t} catch ( e ) {\n\t\tconsole.warn( 'Unable to automatically determine the WordPress Core Version. You can define the WP_VERSION as an environment variable. Otherwise \"latest\" is assumed as the WP_VERSION.' );\n\t\tprocess.env.WP_VERSION = 'latest';\n\t}\n}\n\n// Setup the WP Base URL for e2e Tests.\nif ( ! process.env.WORDPRESS_PORT ) {\n\tprocess.env.WORDPRESS_PORT = '8080';\n}\n\n// Allow easy override of the default base URL, for example if we want to point to a live URL.\nif ( ! process.env.WP_BASE_URL ) {\n\tprocess.env.WP_BASE_URL = `http://localhost:${ process.env.WORDPRESS_PORT }`;\n}\n\n// Retry tests automatically to prevent against false positives.\n// jest.retryTimes( 2 );\n\n// The Jest timeout is increased because these tests are a bit slow.\njest.setTimeout( process.env.PUPPETEER_TIMEOUT || 100000 );\n\nbeforeAll( async () => {\n\tpage.on( 'dialog', ( dialog ) => dialog.accept() );\n\n\tpage.on( 'console', ( log ) => {\n\t\tconst shouldLog = () => {\n\t\t\t// Skip logs by type.\n\t\t\tif ( [ 'info', 'log', 'endGroup' ].includes( log.type() ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst logText = log.text();\n\n\t\t\t// Skip 403s.\n\t\t\tif ( logText.includes( 'Failed to load resource: the server responded with a status of 403 (Forbidden)' ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif ( logText.includes( 'Failed to load resource: the server responded with a status of 404 (Not Found)' ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Skip core block update messages.\n\t\t\tif ( logText.includes( 'Updated Block: %s core/' ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\treturn true;\n\t\t};\n\n\t\tif ( ! shouldLog( log ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Ignore message about attribute width negative value is not valid\n\t\tif ( ! log.text().includes( 'A negative value is not valid' ) ) {\n\t\t\tconsole.log( `[${ log.type() }] ${ log.text() }` );\n\t\t}\n\t} );\n\n\tpage.on( 'pageerror', ( err ) => {\n\t\tconsole.log( `[pageerror] ${ err.message }` );\n\t} );\n\n\tpage.on( 'error', ( err ) => {\n\t\tconsole.log( `[error] ${ err.message }`, err );\n\t} );\n} );\n\nexpect.extend( {\n\n\t/**\n\t * A custom matcher for comparing strings that may or may not contain \"smart\" quotes.\n\t *\n\t * This helps us test code on FSE (block themes) when `wp_texturize()` is run against the HTML template. In\n\t * LifterLMS we have quite a few notices that are run through the function on FSE themes but not on PHP themes.\n\t *\n\t * This matcher allows us to check strings that have quotes in them that may or may not be texturized depending\n\t * on the theme being tested against.\n\t *\n\t * @since 3.0.0\n\t *\n\t * @see {@link https://github.com/WordPress/gutenberg/issues/37754}\n\t *\n\t * @param {string} received The received string. This string may or may not contain \"smart\" quotes.\n\t * @param {string} expected The expected string. This string should contain \"dumb\" quotes.\n\t * @return {Promise} Jest expect matcher return.\n\t */\n\tasync toMatchStringWithQuotes( received, expected ) {\n\t\treceived = received.replace( /[“”]/g, '\"' ).replace( /[‘’]/g, \"'\" );\n\n\t\tconst options = {\n\t\t\tcomment: 'String with quotes equality',\n\t\t\tisNot: this.isNot,\n\t\t\tpromise: this.promise,\n\t\t};\n\n\t\tconst pass = received === expected;\n\n\t\tconst message = pass\n\t\t\t? () =>\n\t\t\t\tthis.utils.matcherHint( 'toMatchStringWithQuotes', undefined, undefined, options ) +\n\t\t\t\t\t'\\n\\n' +\n\t\t\t\t\t`Expected: not ${ this.utils.printExpected( expected ) }\\n` +\n\t\t\t\t\t`Received: ${ this.utils.printReceived( received ) }`\n\t\t\t: () => {\n\t\t\t\tconst diffString = diff( expected, received, {\n\t\t\t\t\texpand: this.expand,\n\t\t\t\t} );\n\t\t\t\treturn (\n\t\t\t\t\tthis.utils.matcherHint( 'toMatchStringWithQuotes', undefined, undefined, options ) +\n\t\t\t\t\t\t'\\n\\n' +\n\t\t\t\t\t\t( diffString && diffString.includes( '- Expect' )\n\t\t\t\t\t\t\t? `Difference:\\n\\n${ diffString }`\n\t\t\t\t\t\t\t: `Expected: ${ this.utils.printExpected( expected ) }\\n` +\n\t\t\t\t\t\t\t\t`Received: ${ this.utils.printReceived( received ) }` )\n\t\t\t\t);\n\t\t\t};\n\n\t\treturn {\n\t\t\tactual: received,\n\t\t\tmessage,\n\t\t\tpass,\n\t\t};\n\t},\n} );\n\n/**\n * Global helper function that conditionally runs a describe() block if the condition is met.\n *\n * @since 3.0.0\n *\n * @example describeIf( true )( 'SuiteName', () => {} )\n *\n * @param {boolean} condition If truthy, the suite runs as normal, otherwise it's skipped.\n * @return {Function} Returns either `describe()` or `describe.skip()` depending on the condition.\n */\nglobal.describeIf = ( condition ) => condition ? describe : describe.skip;\n\n/**\n * Global helper function that conditionally runs a test() if the condition is met.\n *\n * @since 3.0.0\n *\n * @example testIf( true )( 'SuiteName', () => {} )\n *\n * @param {boolean} condition If truthy, the suite runs as normal, otherwise it's skipped.\n * @return {Function} Returns either `test()` or `test.skip()` depending on the condition.\n */\nglobal.testIf = ( condition ) => condition ? test : test.skip;\n"
  },
  {
    "path": "packages/scripts/e2e/jest-puppeteer.config.js",
    "content": "/**\n * Jest Puppeteer Config\n *\n * @since Unknown\n * @version Unknown\n *\n * @see {@link https://github.com/smooth-code/jest-puppeteer#jest-puppeteerconfigjs}\n */\n\nconst window = process.env.PUPPETEER_WINDOW || '1440x900',\n\tdimensions = window.split( 'x' ).map( ( int ) => parseInt( int, 10 ) );\n\nconst config = {\n\tlaunch: {\n\t\tignoreHTTPSErrors: true,\n\t\theadless: process.env.PUPPETEER_HEADLESS !== 'false',\n\t\tslowMo: parseInt( process.env.PUPPETEER_SLOWMO, 10 ) || 0,\n\t\tdefaultViewport: {\n\t\t\twidth: dimensions[ 0 ],\n\t\t\theight: dimensions[ 1 ],\n\t\t},\n\t},\n\texitOnPageError: false,\n};\n\nif ( false === config.launch.headless ) {\n\tconfig.launch.args = [\n\t\t`--window-size=${ dimensions[ 0 ] },${ dimensions[ 1 ] }`,\n\t];\n}\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/scripts/e2e/jest.config.js",
    "content": "/**\n * Main Jest config\n *\n * @since Unknown\n * @version 3.0.0\n */\n\n/**\n * Load the jest-puppeteer config file\n *\n * @see {@link https://github.com/smooth-code/jest-puppeteer/issues/160#issuecomment-491975158}\n */\nprocess.env.JEST_PUPPETEER_CONFIG = require.resolve( './jest-puppeteer.config.js' );\n\nprocess.env.WP_ARTIFACTS_PATH = `${ process.cwd() }/tmp/artifacts`;\n\nconst\n\t// Import the initial config to be moified.\n\tconfig = require( '@wordpress/scripts/config/jest-e2e.config' ),\n\n\t// List of uncompiled es modlues.\n\tesModules = [ '@lifterlms/llms-e2e-test-utils' ].join( '|' );\n\n// Setup files.\nconfig.setupFilesAfterEnv = [\n\trequire.resolve( './bootstrap.js' ),\n];\n\nconfig.rootDir = process.cwd();\n\n// Sort tests alphabetically by path. Ensures Tests in the \"activate\" directory run first.\nconfig.testSequencer = require.resolve( './sequencer.js' );\n\n// Look for tests with with \".test.js\" as a suffix.\nconfig.testMatch = [ '**/tests/**/*.test.[jt]s?(x)' ];\n\n// Don't transform specified modules.\nconfig.transformIgnorePatterns = [ `/node_modules/(?!${ esModules })` ];\n\n/**\n * Jest Config\n *\n * @see {@link https://jestjs.io/docs/en/configuration.html}\n */\nmodule.exports = config;\n"
  },
  {
    "path": "packages/scripts/e2e/sequencer.js",
    "content": "/**\n * Jest tests sequencer\n *\n * Runs our tests in alphabetical order by directory / filename.\n *\n * This allows us to do things like run the setup wizard tests to further bootstrap\n * the testing environment for other tests.\n *\n * @since Unknown\n * @version Unknown\n *\n * @see {@link https://jestjs.io/docs/en/next/configuration#testsequencer-string}\n */\n\nconst Sequencer = require( '@jest/test-sequencer' ).default;\n\nclass CustomSequencer extends Sequencer {\n\tsort( tests ) {\n\t\tconst copyTests = Array.from( tests );\n\t\treturn copyTests.sort( ( testA, testB ) => ( testA.path > testB.path ? 1 : -1 ) );\n\t}\n}\n\nmodule.exports = CustomSequencer;\n"
  },
  {
    "path": "packages/scripts/package.json",
    "content": "{\n  \"name\": \"@lifterlms/scripts\",\n  \"version\": \"4.0.1\",\n  \"description\": \"Test, build, and development scripts for LifterLMS projects.\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/master/packages/scripts\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"scripts\",\n    \"utils\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/scripts\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%20scripts\"\n  },\n  \"main\": \"src/index.js\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"dependencies\": {\n    \"@jest/test-sequencer\": \"^27.4.6\",\n    \"@wordpress/eslint-plugin\": \"^11.1.0\",\n    \"@wordpress/scripts\": \"^23.6.0\",\n    \"dotenv\": \"^8.2.0\",\n    \"eslint-import-resolver-node\": \"^0.3.6\",\n    \"fast-glob\": \"^3.2.11\",\n    \"mini-css-extract-plugin\": \"^2.6.1\",\n    \"webpack-remove-empty-scripts\": \"^0.8.1\",\n    \"webpack-rtl-plugin\": \"^2.0.0\"\n  },\n  \"scripts\": {\n    \"dev\": \"./../dev/src/index.js\",\n    \"lint:js\": \"wp-scripts lint-js ./\"\n  }\n}\n"
  },
  {
    "path": "packages/utils/.llmsdev.yml",
    "content": "update-version:\n  skip-config: true\n"
  },
  {
    "path": "packages/utils/.npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "packages/utils/CHANGELOG.md",
    "content": "@lifterlms/utils CHANGELOG\n==========================\n\nv1.0.0 - 2022-03-08\n-------------------\n\n+ Initial public release.\n"
  },
  {
    "path": "packages/utils/README.md",
    "content": "# LifterLMS JS Utilities\n\nJavascript utility library for use in LifterLMS and LifterLMS add-ons.\n\n## Usage in a LifterLMS add-on\n\nThis package is included in the LifterLMS core plugin as a module and registered using the WordPress scripts dependency API (see [wp_register_script()][https://developer.wordpress.org/reference/functions/wp_register_script/]).\n\nTo use components you can add `llms-utils` as a dependency to your script and access the module via `window.llms.components`, for example:\n\n```js\nconst { getAdminUrl } = window.llms.utils;\n```\n\n## Installation\n\nInstall the module as a dependency in your project:\n\n```bash\nnpm i -S @lifterlms/utils`\n```\n\n## LifterLMS Versions\n\nThe following table records the module version and which LifterLMS versions it has been included in.\n\n| Module Version | LifterLMS Version |\n| -------------- | ----------------- |\n| 1.0.0          | 6.0.0             |\n\n\n## Changelog\n\n[View the Changelog](./CHANGELOG.md)\n\n## API Docs\n\n<!-- START TOKEN(Autogenerated API docs) -->\n\n### getAdminUrl\n\nRetrieves the WordPress admin URL.\n\nThis function relies on the presence of localized data from the LifterLMS plugin which is only\npresent on the WordPress admin panel. If used out of context a default partial path url, `/wp-admin`\nwill be returned.\n\n_Returns_\n\n-   `string`: The WP Admin URL.\n\n### trailingSlashIt\n\nAdds a trailing forward slash to a given string.\n\n_Parameters_\n\n-   _str_ `string`: A string with or without a trailing forward slash.\n\n_Returns_\n\n-   `string`: The original string with a trailing forward slash added.\n\n### untrailingSlashIt\n\nRemove trailing forward slash from a given string.\n\n_Parameters_\n\n-   _str_ `string`: A string with or without a trailing forward slash.\n\n_Returns_\n\n-   `string`: The original string with the trailing forward slash removed.\n\n\n<!-- END TOKEN(Autogenerated API docs) -->\n"
  },
  {
    "path": "packages/utils/babel.config.js",
    "content": "/**\n * Babel config\n *\n * @since 1.0.0\n * @version 1.0.0\n */\n\nconst presets = [ '@wordpress/default' ];\n\nmodule.exports = { presets };\n"
  },
  {
    "path": "packages/utils/package.json",
    "content": "{\n  \"name\": \"@lifterlms/utils\",\n  \"version\": \"1.0.1-alpha.1\",\n  \"description\": \"JS Utility library for LifterLMS.\",\n  \"author\": \"Team LifterLMS <dev@lifterlms.com>\",\n  \"license\": \"GPL-3.0-or-later\",\n  \"homepage\": \"https://github.com/gocodebox/lifterlms/tree/trunk/packages/utils\",\n  \"keywords\": [\n    \"lifterlms\",\n    \"wordpress\",\n    \"utils\",\n    \"ui\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/gocodebox/lifterlms.git\",\n    \"directory\": \"packages/utils\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gocodebox/lifterlms/labels/package%3A%utils\"\n  },\n  \"main\": \"src/index.js\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"scripts\": {\n    \"docgen\": \"docgen src/index.js --output README.md --to-token\",\n    \"dev\": \"./../dev/src/index.js\",\n    \"lint:js\": \"wp-scripts lint-js ./ --config ../../.eslintrc.js\",\n    \"test\": \"wp-scripts test-unit-js ./ --config ../scripts/config/jest-unit.config.js --verbose\"\n  }\n}\n"
  },
  {
    "path": "packages/utils/src/formatting/index.js",
    "content": "export { trailingSlashIt } from './trailing-slash-it';\nexport { untrailingSlashIt } from './untrailing-slash-it';\n"
  },
  {
    "path": "packages/utils/src/formatting/test/index.js",
    "content": "import { trailingSlashIt, untrailingSlashIt } from '../';\n\ndescribe( 'Formatting', () => {\n\tdescribe( 'trailingSlashIt', () => {\n\t\tconst testData = [\n\t\t\t[\n\t\t\t\t'Should leave a string with a trailing slash unchanged',\n\t\t\t\t'string/',\n\t\t\t\t'string/',\n\t\t\t],\n\t\t\t[\n\t\t\t\t'Should add a trailing slash to a string without one',\n\t\t\t\t'string',\n\t\t\t\t'string/',\n\t\t\t],\n\t\t];\n\t\tit.each( testData )( '%s', ( name, input, expected ) => {\n\t\t\texpect( trailingSlashIt( input ) ).toBe( expected );\n\t\t} );\n\t} );\n\n\tdescribe( 'untrailingSlashIt', () => {\n\t\tconst testData = [\n\t\t\t[\n\t\t\t\t'Should leave a string without a trailing slash unchanged',\n\t\t\t\t'string',\n\t\t\t\t'string',\n\t\t\t],\n\t\t\t[\n\t\t\t\t'Should remove a trailing slash to a string with one',\n\t\t\t\t'string/',\n\t\t\t\t'string',\n\t\t\t],\n\t\t];\n\t\tit.each( testData )( '%s', ( name, input, expected ) => {\n\t\t\texpect( untrailingSlashIt( input ) ).toBe( expected );\n\t\t} );\n\t} );\n} );\n"
  },
  {
    "path": "packages/utils/src/formatting/trailing-slash-it.js",
    "content": "import { untrailingSlashIt } from './untrailing-slash-it';\n\n/**\n * Adds a trailing forward slash to a given string.\n *\n * @since 1.0.0\n *\n * @param {string} str A string with or without a trailing forward slash.\n * @return {string} The original string with a trailing forward slash added.\n */\nexport function trailingSlashIt( str ) {\n\treturn `${ untrailingSlashIt( str ) }/`;\n}\n"
  },
  {
    "path": "packages/utils/src/formatting/untrailing-slash-it.js",
    "content": "/**\n * Remove trailing forward slash from a given string.\n *\n * @since 1.0.0\n *\n * @param {string} str A string with or without a trailing forward slash.\n * @return {string} The original string with the trailing forward slash removed.\n */\nexport function untrailingSlashIt( str ) {\n\treturn str.endsWith( '/' ) ? str.slice( 0, -1 ) : str;\n}\n"
  },
  {
    "path": "packages/utils/src/index.js",
    "content": "export * from './formatting';\nexport * from './url';\n"
  },
  {
    "path": "packages/utils/src/url/get-admin-url.js",
    "content": "import { untrailingSlashIt } from '../formatting';\n\n/**\n * Retrieves the WordPress admin URL.\n *\n * This function relies on the presence of localized data from the LifterLMS plugin which is only\n * present on the WordPress admin panel. If used out of context a default partial path url, `/wp-admin`\n * will be returned.\n *\n * @since 1.0.0\n *\n * @return {string} The WP Admin URL.\n */\nexport function getAdminUrl() {\n\tconst { admin_url: url = '/wp-admin' } = window.llms || {};\n\treturn untrailingSlashIt( url );\n}\n"
  },
  {
    "path": "packages/utils/src/url/index.js",
    "content": "export { getAdminUrl } from './get-admin-url';\n"
  },
  {
    "path": "packages/utils/src/url/test/get-admin-url.test.js",
    "content": "import { getAdminUrl } from '../';\n\ndescribe( 'getAdminUrl', () => {\n\tafterEach( () => {\n\t\tdelete window.llms;\n\t} );\n\n\tconst testData = [\n\t\t[ 'https://example.tld/wp-admin/', 'https://example.tld/wp-admin' ],\n\t\t[ 'https://example.tld/wp-admin', 'https://example.tld/wp-admin' ],\n\t\t[ 'https://example.tld/custom/url/', 'https://example.tld/custom/url' ],\n\t];\n\tit.each( testData )(\n\t\t'Should return the window variable without a trailing slash (input: %s)',\n\t\t( input, expected ) => {\n\t\t\twindow.llms = {\n\t\t\t\tadmin_url: input,\n\t\t\t};\n\t\t\texpect( getAdminUrl() ).toBe( expected );\n\t\t}\n\t);\n\n\tit( 'Should return the default admin path if window.llms does not exist', () => {\n\t\texpect( getAdminUrl() ).toBe( '/wp-admin' );\n\t} );\n\n\tit( 'Should return the default admin path if window.llms.admin_url does not exist', () => {\n\t\twindow.llms = {};\n\t\texpect( getAdminUrl() ).toBe( '/wp-admin' );\n\t} );\n} );\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset name=\"LifterLMS Core\">\n\t<description>LifterLMS Rules for PHP_CodeSniffer</description>\n\n\t<file>.</file>\n\n    <!-- Exclude project directories -->\n    <exclude-pattern>.bin/</exclude-pattern>\n    <exclude-pattern>.config/</exclude-pattern>\n    <exclude-pattern>.github/</exclude-pattern>\n    <exclude-pattern>.wordpress-org/</exclude-pattern>\n\n    <!-- Exclude compile or minified JS files -->\n    <exclude-pattern>assets/js/llms.js</exclude-pattern>\n    <exclude-pattern>assets/js/llms-admin-addons.js</exclude-pattern>\n    <exclude-pattern>assets/js/llms-admin-certificate-editor.js</exclude-pattern>\n    <exclude-pattern>assets/js/llms-builder.js</exclude-pattern>\n    <exclude-pattern>assets/js/llms-components.js</exclude-pattern>\n    <exclude-pattern>assets/js/llms-icons.js</exclude-pattern>\n    <exclude-pattern>assets/js/llms-metaboxes.js</exclude-pattern>\n    <exclude-pattern>assets/js/llms-spinner.js</exclude-pattern>\n    <exclude-pattern>blocks/**/*.js</exclude-pattern>\n\n    <!-- Exclude PHP asset files -->\n    <exclude-pattern>*.asset.php</exclude-pattern>\n\n    <!-- Let ESLint handle ESNext JS -->\n    <exclude-pattern type=\"relative\">src/**/*</exclude-pattern>\n    <exclude-pattern>webpack.config.js</exclude-pattern>\n\n    <!-- Exclude node packages -->\n    <exclude-pattern>packages/</exclude-pattern>\n\n    <!-- Exclude external libraries -->\n    <exclude-pattern>libraries/</exclude-pattern>\n\n    <!-- Exclude deprecated/legacy files -->\n    <exclude-pattern>includes/functions/llms-functions-deprecated.php</exclude-pattern>\n\n    <!-- Exclude locale files that take forever to process -->\n    <exclude-pattern>languages/*.php</exclude-pattern>\n\n    <!-- Allow dynamic styles on the certificate template. -->\n\t<rule ref=\"WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet\">\n\t\t<exclude-pattern>templates/certificates/dynamic-styles.php</exclude-pattern>\n\t</rule>\n\n\t<rule ref=\"LifterLMS\">\n\n\t\t<!-- @todo: Apply coding standards to js -->\n\t\t<exclude-pattern>assets/js/*.js</exclude-pattern>\n\n\t\t<!-- @todo: Fix docs and comments to adhere to these rules -->\n\n\t\t<exclude name=\"Squiz.Commenting.FunctionComment.ParamCommentFullStop\" />\n\n\t\t<exclude name=\"Generic.Commenting.DocComment.MissingShort\" />\n\t\t<exclude name=\"Generic.Commenting.DocComment.ShortNotCapital\" />\n\n\t\t<exclude name=\"Squiz.Commenting.FunctionComment.Missing\" />\n\t\t<exclude name=\"Squiz.Commenting.FunctionComment.MissingParamComment\" />\n\t\t<exclude name=\"Squiz.Commenting.FunctionComment.MissingParamTag\" />\n\t\t<exclude name=\"Squiz.Commenting.FunctionComment.MissingParamName\" />\n\t\t<exclude name=\"Squiz.Commenting.VariableComment.Missing\" />\n\n\t\t<exclude name=\"Squiz.Commenting.FunctionComment.InvalidReturnVoid\" />\n\n\t\t<!-- @todo: Update these to use a prefix, see https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace -->\n\t\t<exclude name=\"WordPress.WP.GlobalVariablesOverride.Prohibited\" />\n\n\t\t<!-- @todo: extract is messy you're right, fix this -->\n\t\t<exclude name=\"WordPress.PHP.DontExtract.extract_extract\" />\n\n\t\t<!-- @todo: Most core files break this rule. -->\n\t\t<exclude name=\"WordPress.Files.FileName.InvalidClassFileName\" />\n\n\t\t<!-- @todo: This needs to be adjusted since WP 5.3 -->\n\t\t<exclude name=\"WordPress.DateTime.RestrictedFunctions.date_date\" />\n\n\t\t<!-- These templates follow WP Template style so they're okay -->\n\t\t<exclude name=\"WordPress.Files.FileName.NotHyphenatedLowercase\">\n\t\t\t<exclude-pattern>templates/taxonomy-*.php</exclude-pattern>\n\t\t</exclude>\n\t\t\n\t\t<!-- This rule is for PHP versions less than 7.3, but we require an higher PHP version. -->\n\t\t<exclude name=\"PHPCompatibility.Classes.NewTypedProperties.Found\" />\n\t\t\n\t</rule>\n\n\t<!--\n\t\t@todo The following 3 rule sets are disabled for the following files/directories\n\t\t\t  We are in the process of gradually fixing these in bulk.\n\t\t\t  See https://github.com/gocodebox/lifterlms/issues/946\n\t-->\n\t<rule ref=\"LifterLMS.Commenting.FileComment\">\n\t\t<exclude-pattern>includes/admin/views/*.php</exclude-pattern>\n\t\t<exclude-pattern>includes/admin/views/**/*.php</exclude-pattern>\n\n\t\t<exclude-pattern>templates/*.php</exclude-pattern>\n\t\t<exclude-pattern>templates/**/*.php</exclude-pattern>\n\t</rule>\n\t<rule ref=\"Squiz.Commenting.FileComment\">\n\t\t<exclude-pattern>includes/admin/views/*.php</exclude-pattern>\n\t\t<exclude-pattern>includes/admin/views/**/*.php</exclude-pattern>\n\n\t\t<exclude-pattern>templates/*.php</exclude-pattern>\n\t\t<exclude-pattern>templates/**/*.php</exclude-pattern>\n\t</rule>\n\t<rule ref=\"Squiz.Commenting.ClassComment.Missing\">\n\t\t<exclude-pattern>includes/admin/views/*.php</exclude-pattern>\n\t\t<exclude-pattern>includes/admin/views/**/*.php</exclude-pattern>\n\n\t\t<exclude-pattern>templates/*.php</exclude-pattern>\n\t\t<exclude-pattern>templates/**/*.php</exclude-pattern>\n\t</rule>\n\n\t<rule ref=\"Squiz.Commenting.InlineComment.InvalidEndChar\">\n\t\t<!-- To be fixed -->\n\t\t<exclude-pattern>includes/notifications/class.llms.notifications.query.php</exclude-pattern>\n\t\t<exclude-pattern>includes/privacy/class-llms-privacy-exporters.php</exclude-pattern>\n\t\t<exclude-pattern>includes/privacy/class-llms-privacy.php</exclude-pattern>\n\t\t<exclude-pattern>includes/processors/class.llms.processor.membership.bulk.enroll.php</exclude-pattern>\n\t\t<exclude-pattern>includes/processors/class.llms.processor.table.to.csv.php</exclude-pattern>\n\t\t<exclude-pattern>includes/shortcodes/class.llms.shortcode.course.outline.php</exclude-pattern>\n\t\t<exclude-pattern>includes/shortcodes/class.llms.shortcode.hide.content.php</exclude-pattern>\n\t</rule>\n\n\t<rule ref=\"WordPress.WP.I18n\">\n\t\t<!-- @todo: Fix all of these -->\n\t\t<exclude name=\"WordPress.WP.I18n.MissingTranslatorsComment\" />\n\n\t\t<properties>\n\t\t\t<property name=\"text_domain\" value=\"lifterlms\" />\n\t\t</properties>\n\t</rule>\n\n\t<!-- @todo: Fix these issues. -->\n\t<rule ref=\"Squiz.PHP.DisallowSizeFunctionsInLoops.Found\">\n\t    <exclude-pattern>assets/js/*.js</exclude-pattern>\n\t</rule>\n\n</ruleset>\n"
  },
  {
    "path": "phpmd.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset name=\"WordPress LifterLMS\"\n         xmlns=\"http://pmd.sf.net/ruleset/1.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd\"\n         xsi:noNamespaceSchemaLocation=\"http://pmd.sf.net/ruleset_xml_schema.xsd\">\n\t<description>LifterLMS PHPMD Standards</description>\n\n\t<rule ref=\"rulesets/cleancode.xml\">\n\n\t\t<!-- used all over -->\n\t\t<exclude name=\"BooleanArgumentFlag\" />\n\n\t\t<!-- in lack of real namespacing -->\n\t\t<exclude name=\"StaticAccess\" />\n\n\t\t<!-- I disagree with this -->\n\t\t<exclude name=\"ElseExpression\" />\n\n\t</rule>\n\n\n\t<rule ref=\"rulesets/codesize.xml\" />\n\n\t<rule ref=\"rulesets/design.xml\">\n\n\t\t<!-- normal in WP for redirects, etc -->\n\t\t<exclude name=\"ExitExpression\" />\n\n\t</rule>\n\n\n\t<rule ref=\"rulesets/naming.xml/ShortVariable\">\n\t\t<properties>\n\t\t\t<property name=\"exceptions\" value=\"id,wp,i\" />\n\t\t</properties>\n\t</rule>\n\n\n\t<rule ref=\"rulesets/naming.xml/LongVariable\" />\n\t<rule ref=\"rulesets/naming.xml/ShortMethodName\" />\n\t<rule ref=\"rulesets/naming.xml/ConstructorWithNameAsEnclosingClass\" />\n\t<rule ref=\"rulesets/naming.xml/ConstantNamingConventions\" />\n\t<rule ref=\"rulesets/naming.xml/BooleanGetMethodName\" />\n\n\t<rule ref=\"rulesets/unusedcode.xml\" />\n\n</ruleset>\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/|version|/phpunit.xsd\"\n\tbackupGlobals=\"false\"\n\tbootstrap=\"tests/phpunit/bootstrap.php\"\n\tcacheResultFile=\"tmp/.phpunit.result.cache\"\n\tcolors=\"true\"\n\tconvertErrorsToExceptions=\"true\"\n\tconvertNoticesToExceptions=\"true\"\n\tconvertWarningsToExceptions=\"true\"\n\ttimeoutForSmallTests=\"1\"\n\ttimeoutForMediumTests=\"10\"\n\ttimeoutForLargeTests=\"60\"\n\tverbose=\"true\">\n\n\t<testsuites>\n\t\t<testsuite name=\"LifterLMS Test Suite\">\n\t\t\t<directory suffix=\".php\">tests/phpunit/unit-tests</directory>\n\t\t</testsuite>\n\t</testsuites>\n\n\t<filter>\n\t\t<whitelist addUncoveredFilesFromWhitelist=\"true\">\n\t\t\t<directory suffix=\".php\">.</directory>\n\t\t\t<exclude>\n\t\t\t\t<directory suffix=\"index.php\">.</directory>\n\t\t\t\t<directory suffix=\".php\">./admin/views/</directory>\n\t\t\t\t<directory suffix=\".php\">./dist/</directory>\n\t\t\t\t<directory suffix=\".php\">./node_modules/</directory>\n\t\t\t\t<directory suffix=\".php\">./templates/</directory>\n\t\t\t\t<directory suffix=\".php\">./tests/</directory>\n\t\t\t\t<directory suffix=\".php\">./tmp/</directory>\n\t\t\t\t<directory suffix=\".php\">./vendor/</directory>\n\t\t\t\t<directory suffix=\".php\">./wordpress/</directory>\n\t\t\t</exclude>\n\t\t</whitelist>\n\t</filter>\n\n</phpunit>\n"
  },
  {
    "path": "sample-data/index.php",
    "content": "<?php // quiet.\n"
  },
  {
    "path": "sample-data/sample-course.json",
    "content": "{\"_generator\":\"LifterLMS/SingleCourseExporter\",\"_version\":\"3.19.2\",\"access_plans\":[{\"access_expiration\":\"lifetime\",\"access_expires\":\"\",\"access_length\":0,\"access_period\":\"\",\"availability\":\"open\",\"availability_restrictions\":[],\"content\":\"\",\"date\":\"2018-05-30 20:26:38\",\"enroll_text\":\"\",\"excerpt\":\"\",\"frequency\":0,\"id\":19164,\"is_free\":\"yes\",\"length\":0,\"menu_order\":1,\"modified\":\"2018-05-31 15:29:30\",\"name\":\"start-today\",\"on_sale\":\"no\",\"period\":\"\",\"price\":0,\"product_id\":19145,\"sale_end\":\"\",\"sale_price\":0,\"sale_start\":\"\",\"sku\":\"\",\"status\":\"publish\",\"title\":\"Start Today!\",\"trial_length\":0,\"trial_offer\":\"no\",\"trial_period\":\"\",\"trial_price\":0,\"type\":\"llms_access_plan\"}],\"audio_embed\":\"\",\"average_grade\":97.06,\"average_progress\":31.91,\"capacity\":0,\"capacity_message\":\"Enrollment has closed because the maximum number of allowed students has been reached.\",\"categories\":[\"Beginner\",\"Featured\"],\"content\":\"In this free course, you will discover how <a href=\\\"https://lifterlms.com\\\">LifterLMS</a> empowers you to easily create, sell and protect engaging online courses from your WordPress powered website.\\r\\n\\r\\nBy taking this course you will be able to quickly build and launch your first course with <a href=\\\"https://lifterlms.com\\\">LifterLMS</a>.\\r\\n\\r\\nFor best results follow along with Chris and build your course side by side with him.\",\"content_restricted_message\":\"You must enroll in this course to access course content.\",\"course_closed_message\":\"This course closed on [lifterlms_course_info id=\\\"19145\\\" key=\\\"end_date\\\"].\",\"course_opens_message\":\"This course opens on [lifterlms_course_info id=\\\"19145\\\" key=\\\"start_date\\\"].\",\"custom\":{\"_llms_post_course_difficulty\":[\"\"],\"_thumbnail_id\":[\"15\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:5:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:16:\\\"apply_tags_start\\\";a:1:{i:0;s:31:\\\"[Lead Magnet] Quickstart Course\\\";}s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_wp_old_slug\":[\"quickstart\"]},\"date\":\"2018-05-30 19:23:14\",\"difficulty\":\"\",\"enable_capacity\":\"no\",\"end_date\":\"\",\"enrollment_closed_message\":\"Enrollment in this course closed on [lifterlms_course_info id=\\\"19145\\\" key=\\\"enrollment_end_date\\\"].\",\"enrollment_end_date\":\"\",\"enrollment_opens_message\":\"Enrollment in this course opens on [lifterlms_course_info id=\\\"19145\\\" key=\\\"enrollment_start_date\\\"].\",\"enrollment_period\":\"no\",\"enrollment_start_date\":\"\",\"excerpt\":\"\",\"featured_image\":\"https://demo.lifterlms.com/wp-content/uploads/2015/02/lms-course-image-1.png\",\"has_prerequisite\":\"no\",\"id\":19145,\"length\":\"1 Day\",\"menu_order\":0,\"modified\":\"2018-05-31 15:29:30\",\"name\":\"how-to-build-a-learning-management-system-with-lifterlms\",\"permalink\":\"https://demo.lifterlms.com/course/how-to-build-a-learning-management-system-with-lifterlms/\",\"prerequisite\":0,\"prerequisite_track\":0,\"sections\":[{\"content\":\"\",\"custom\":[],\"date\":\"2018-05-30 19:28:03\",\"excerpt\":\"\",\"id\":19146,\"lessons\":[{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/4qjy28kpxt\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_c7e5bb94e874c6bc9c7c977abbfb35cf\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/4qjy28kpxt?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_c7e5bb94e874c6bc9c7c977abbfb35cf\":[\"1527711098\"],\"_oembed_e633bc50189f258c5857c8e31040d532\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/4qjy28kpxt?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_e633bc50189f258c5857c8e31040d532\":[\"1527711102\"],\"_oembed_c0dd352283a9d274e3674a92ad07d3be\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/4qjy28kpxt?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_c0dd352283a9d274e3674a92ad07d3be\":[\"1527711102\"],\"_oembed_8c6bbc5dab46fadb93337487368f26af\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/4qjy28kpxt?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_8c6bbc5dab46fadb93337487368f26af\":[\"1527711110\"]},\"date\":\"2018-05-30 19:28:33\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19147,\"menu_order\":0,\"modified\":\"2018-05-30 21:40:21\",\"name\":\"why-lifterlms-2\",\"order\":1,\"parent_course\":19145,\"parent_section\":19146,\"permalink\":\"https://demo.lifterlms.com/lesson/why-lifterlms-2/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Why LifterLMS\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/7sc31ss6nl\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_wp_old_slug\":[\"new-lesson\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_8ad4bd3b2b6627019c1cc1eb450a5194\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/7sc31ss6nl?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_8ad4bd3b2b6627019c1cc1eb450a5194\":[\"1527711297\"],\"_oembed_d6ce31fe2a1566d525e2ba50ed9582ed\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/7sc31ss6nl?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_d6ce31fe2a1566d525e2ba50ed9582ed\":[\"1527711302\"],\"_oembed_0d28ffc30e47d94fcb8aa83a625d8159\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/7sc31ss6nl?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_0d28ffc30e47d94fcb8aa83a625d8159\":[\"1527711302\"],\"_oembed_750f664c28ced4bab272eb9470afc484\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/7sc31ss6nl?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_750f664c28ced4bab272eb9470afc484\":[\"1527711304\"]},\"date\":\"2018-05-30 19:28:33\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19148,\"menu_order\":0,\"modified\":\"2018-05-30 21:20:50\",\"name\":\"who-lifterlms-is-for\",\"order\":2,\"parent_course\":19145,\"parent_section\":19146,\"permalink\":\"https://demo.lifterlms.com/lesson/who-lifterlms-is-for/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Who LifterLMS is For\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/0fzc5vkyhd#\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_wp_old_slug\":[\"new-lesson\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_8e90d298064263c4eae4c2af0aff4fbe\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/0fzc5vkyhd?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_8e90d298064263c4eae4c2af0aff4fbe\":[\"1527711415\"],\"_oembed_9c7903575c94f1288f7541216509067f\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/0fzc5vkyhd?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_9c7903575c94f1288f7541216509067f\":[\"1527711420\"],\"_oembed_c360a8539c42981791dd0a2af35394d8\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/0fzc5vkyhd?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_c360a8539c42981791dd0a2af35394d8\":[\"1527711420\"],\"_oembed_2e8db2a5c463c7c316b0fa3ec3f0c8a8\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/0fzc5vkyhd?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_2e8db2a5c463c7c316b0fa3ec3f0c8a8\":[\"1527711422\"]},\"date\":\"2018-05-30 19:28:48\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19149,\"menu_order\":0,\"modified\":\"2018-05-30 21:20:44\",\"name\":\"what-is-lifterlms-4\",\"order\":3,\"parent_course\":19145,\"parent_section\":19146,\"permalink\":\"https://demo.lifterlms.com/lesson/what-is-lifterlms-4/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"What is LifterLMS\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/vz13l2v62a\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_ebf56e381d503b7de8e916f60a7f1342\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vz13l2v62a?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_ebf56e381d503b7de8e916f60a7f1342\":[\"1527711470\"],\"_oembed_27f7d01c9421f7fd044145cf9925d322\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vz13l2v62a?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_27f7d01c9421f7fd044145cf9925d322\":[\"1527711474\"],\"_oembed_7ed6047d662890cda52612ec48394737\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vz13l2v62a?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_7ed6047d662890cda52612ec48394737\":[\"1527711474\"],\"_oembed_58be520e55afee3b7cf7b82357098375\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vz13l2v62a?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_58be520e55afee3b7cf7b82357098375\":[\"1527711481\"]},\"date\":\"2018-05-30 19:29:48\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19150,\"menu_order\":0,\"modified\":\"2018-05-30 21:20:37\",\"name\":\"goal-for-this-course\",\"order\":4,\"parent_course\":19145,\"parent_section\":19146,\"permalink\":\"https://demo.lifterlms.com/lesson/goal-for-this-course/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Goal For This Course\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/vj1m9wdh8u\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_e379f6ab69747dc43df76a66401f287e\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vj1m9wdh8u?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_e379f6ab69747dc43df76a66401f287e\":[\"1527711562\"],\"_oembed_90155347f51f18fc7458182f0177921b\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vj1m9wdh8u?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_90155347f51f18fc7458182f0177921b\":[\"1527711566\"],\"_oembed_d1307d6b07849595c947f6a6e010dbb8\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vj1m9wdh8u?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_d1307d6b07849595c947f6a6e010dbb8\":[\"1527711567\"],\"_oembed_d3aff4dc0ae1c3469295e8d54700081c\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/vj1m9wdh8u?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_d3aff4dc0ae1c3469295e8d54700081c\":[\"1527714939\"]},\"date\":\"2018-05-30 19:30:03\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19151,\"menu_order\":0,\"modified\":\"2018-05-30 21:20:29\",\"name\":\"install-lifterlms\",\"order\":5,\"parent_course\":19145,\"parent_section\":19146,\"permalink\":\"https://demo.lifterlms.com/lesson/install-lifterlms/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Install LifterLMS\",\"type\":\"lesson\",\"video_embed\":\"\"}],\"menu_order\":0,\"modified\":\"2018-05-30 19:28:33\",\"name\":\"19146\",\"order\":1,\"parent_course\":19145,\"status\":\"publish\",\"title\":\"Getting Started\",\"type\":\"section\"},{\"content\":\"\",\"custom\":[],\"date\":\"2018-05-30 19:30:18\",\"excerpt\":\"\",\"id\":19152,\"lessons\":[{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/mkw34nl1nh\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_wp_old_slug\":[\"new-lesson\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_2c7d2a6d0f183b7cd0c1000487266324\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/mkw34nl1nh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_2c7d2a6d0f183b7cd0c1000487266324\":[\"1527711584\"],\"_oembed_723e2f15d4ff136af3352aae1de5f674\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/mkw34nl1nh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_723e2f15d4ff136af3352aae1de5f674\":[\"1527711588\"],\"_oembed_624fe81250288f35a7f32133ed298b7f\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/mkw34nl1nh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_624fe81250288f35a7f32133ed298b7f\":[\"1527711588\"],\"_oembed_cd641591ffbcb81c6a926c58e2e2aa6b\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/mkw34nl1nh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_cd641591ffbcb81c6a926c58e2e2aa6b\":[\"1527714942\"]},\"date\":\"2018-05-30 19:30:18\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19153,\"menu_order\":0,\"modified\":\"2018-05-30 21:40:45\",\"name\":\"create-your-course-structure\",\"order\":1,\"parent_course\":19145,\"parent_section\":19152,\"permalink\":\"https://demo.lifterlms.com/lesson/create-your-course-structure/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Create Your Course Structure\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/1qbwsplmil\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_c9afd0aab5b1cfd811205e9f17e6a6aa\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/1qbwsplmil?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_c9afd0aab5b1cfd811205e9f17e6a6aa\":[\"1527711609\"],\"_oembed_6e979f8ec0251a8e542feafcd75607ad\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/1qbwsplmil?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_6e979f8ec0251a8e542feafcd75607ad\":[\"1527711612\"],\"_oembed_fe5a9ab2ce2e95b23fc044ea45f32ed9\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/1qbwsplmil?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_fe5a9ab2ce2e95b23fc044ea45f32ed9\":[\"1527711612\"],\"_oembed_2c5dc29bef7f4485c95219ece97e4eb4\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/1qbwsplmil?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_2c5dc29bef7f4485c95219ece97e4eb4\":[\"1527714943\"]},\"date\":\"2018-05-30 19:31:18\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19154,\"menu_order\":0,\"modified\":\"2018-05-30 21:20:15\",\"name\":\"add-course-and-lesson-sidebars\",\"order\":2,\"parent_course\":19145,\"parent_section\":19152,\"permalink\":\"https://demo.lifterlms.com/lesson/add-course-and-lesson-sidebars/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Add Course and Lesson Sidebars\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/a7dxs4vxqm\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_wp_old_slug\":[\"new-lesson\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_b7f1a477df6ec69889c6db094ae1fd16\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/a7dxs4vxqm?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_b7f1a477df6ec69889c6db094ae1fd16\":[\"1527711670\"],\"_oembed_75a1a7b2443dd24ad51727d667ad54cc\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/a7dxs4vxqm?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_75a1a7b2443dd24ad51727d667ad54cc\":[\"1527711678\"],\"_oembed_f36725c7aa064082e0dc5b1bd7aa19fc\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/a7dxs4vxqm?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_f36725c7aa064082e0dc5b1bd7aa19fc\":[\"1527711678\"],\"_oembed_ee8c6c9dc0b305c1244691be81877036\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/a7dxs4vxqm?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_ee8c6c9dc0b305c1244691be81877036\":[\"1527714944\"]},\"date\":\"2018-05-30 19:31:18\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19155,\"menu_order\":0,\"modified\":\"2018-05-30 21:20:07\",\"name\":\"add-course-description-and-image\",\"order\":3,\"parent_course\":19145,\"parent_section\":19152,\"permalink\":\"https://demo.lifterlms.com/lesson/add-course-description-and-image/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Add Course Description And Image\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/kmsip8qy39\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_wp_old_slug\":[\"new-lesson\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_22667054f8d3f709aabe961c883fd9ad\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/kmsip8qy39?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_22667054f8d3f709aabe961c883fd9ad\":[\"1527711713\"],\"_oembed_dbe1090e49e9264d29390509f367bae1\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/kmsip8qy39?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_dbe1090e49e9264d29390509f367bae1\":[\"1527711716\"],\"_oembed_ac55334956683808b00ce641fe6f0949\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/kmsip8qy39?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_ac55334956683808b00ce641fe6f0949\":[\"1527711717\"],\"_oembed_aad61fa4a2036687c9fd998538037cd8\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/kmsip8qy39?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_aad61fa4a2036687c9fd998538037cd8\":[\"1527714944\"]},\"date\":\"2018-05-30 19:31:48\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19156,\"menu_order\":0,\"modified\":\"2018-05-30 21:20:01\",\"name\":\"add-lesson-content\",\"order\":4,\"parent_course\":19145,\"parent_section\":19152,\"permalink\":\"https://demo.lifterlms.com/lesson/add-lesson-content/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Add Lesson Content\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/g05543magh\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_e252d7359ff881ccac9c346b3e7f2f25\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/g05543magh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_e252d7359ff881ccac9c346b3e7f2f25\":[\"1527711741\"],\"_oembed_6973164d6b023c58b47d85193359ad6a\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/g05543magh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_6973164d6b023c58b47d85193359ad6a\":[\"1527711745\"],\"_oembed_1c39b98323e6e9f302639e2b798a0ce4\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/g05543magh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_1c39b98323e6e9f302639e2b798a0ce4\":[\"1527711746\"],\"_oembed_46dbc37a804232054748e20f370136b2\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/g05543magh?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_46dbc37a804232054748e20f370136b2\":[\"1527714945\"]},\"date\":\"2018-05-30 19:32:48\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19157,\"menu_order\":0,\"modified\":\"2018-05-30 21:19:54\",\"name\":\"set-up-course-pricing-and-access\",\"order\":5,\"parent_course\":19145,\"parent_section\":19152,\"permalink\":\"https://demo.lifterlms.com/lesson/set-up-course-pricing-and-access/\",\"prerequisite\":0,\"quiz\":{\"allowed_attempts\":5,\"content\":\"<p>Hold up! I just wanted to give you a simple demonstration of the LifterLMS quiz system.</p>\",\"custom\":{\"launchpad_default_layout\":[\"content\"],\"launchpad_hide_page_title\":[\"yes\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"]},\"date\":\"2018-05-31 15:05:07\",\"excerpt\":\"\",\"id\":19193,\"lesson_id\":19157,\"limit_attempts\":\"no\",\"limit_time\":\"no\",\"menu_order\":0,\"modified\":\"2018-05-31 15:05:07\",\"name\":\"set-up-course-pricing-and-access-quiz\",\"passing_percent\":0,\"permalink\":\"https://demo.lifterlms.com/quiz/set-up-course-pricing-and-access-quiz/\",\"questions\":[{\"choices\":[{\"id\":\"5b100d235d9f9\",\"choice\":\"LifterLMS\",\"choice_type\":\"text\",\"correct\":true,\"marker\":\"A\",\"question_id\":\"19194\",\"type\":\"choice\"},{\"id\":\"5b100d235f271\",\"choice\":\"Something else\",\"choice_type\":\"text\",\"correct\":false,\"marker\":\"B\",\"question_id\":\"19194\",\"type\":\"choice\"}],\"clarifications\":\"<p>Really? </p>\",\"clarifications_enabled\":\"yes\",\"content\":\"\",\"description_enabled\":\"no\",\"id\":19194,\"image\":[],\"menu_order\":1,\"multi_choices\":\"no\",\"name\":\"19194\",\"parent_id\":19193,\"permalink\":\"https://demo.lifterlms.com/llms_question/19194/\",\"points\":1,\"question\":\"\",\"question_type\":\"choice\",\"title\":\"What is the best WordPress LMS?\",\"type\":\"llms_question\",\"video_enabled\":\"no\",\"video_src\":\"\"}],\"random_questions\":\"no\",\"show_correct_answer\":\"no\",\"status\":\"publish\",\"time_limit\":30,\"title\":\"Set Up Course Pricing And Access Quiz\",\"type\":\"llms_quiz\"},\"quiz_enabled\":\"yes\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Set Up Course Pricing And Access\",\"type\":\"lesson\",\"video_embed\":\"\"}],\"menu_order\":0,\"modified\":\"2018-05-30 19:30:18\",\"name\":\"19152\",\"order\":2,\"parent_course\":19145,\"status\":\"publish\",\"title\":\"Create Your Course\",\"type\":\"section\"},{\"content\":\"\",\"custom\":[],\"date\":\"2018-05-30 19:32:48\",\"excerpt\":\"\",\"id\":19158,\"lessons\":[{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/d474d8qnyg\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_wp_old_slug\":[\"new-lesson\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_8873a1e75439bc8f0a22e932159892c2\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/d474d8qnyg?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_8873a1e75439bc8f0a22e932159892c2\":[\"1527711835\"],\"_oembed_2e6fc0b6f77f51af4eb0ab8f4a436407\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/d474d8qnyg?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_2e6fc0b6f77f51af4eb0ab8f4a436407\":[\"1527711838\"],\"_oembed_8bf9501dd79793cef3a3e17f738e50bc\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/d474d8qnyg?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_8bf9501dd79793cef3a3e17f738e50bc\":[\"1527711839\"],\"_oembed_538fd91ada8e59b3c6fe118929e768fa\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/d474d8qnyg?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_538fd91ada8e59b3c6fe118929e768fa\":[\"1527714948\"]},\"date\":\"2018-05-30 19:32:48\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19159,\"menu_order\":0,\"modified\":\"2018-05-30 21:19:47\",\"name\":\"admire-your-course\",\"order\":1,\"parent_course\":19145,\"parent_section\":19158,\"permalink\":\"https://demo.lifterlms.com/lesson/admire-your-course/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Admire Your Course\",\"type\":\"lesson\",\"video_embed\":\"\"},{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/9otjp9ms8q\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_wp_old_slug\":[\"new-lesson\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_181175ed9e433432f008b3fb4d4adf50\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/9otjp9ms8q?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_181175ed9e433432f008b3fb4d4adf50\":[\"1527711911\"],\"_oembed_ee8e9d6ffe195ab2fe36f8fb16a2ad39\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/9otjp9ms8q?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_ee8e9d6ffe195ab2fe36f8fb16a2ad39\":[\"1527711915\"],\"_oembed_76e3892eef1f48a774a85c64b720f6ce\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/9otjp9ms8q?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_76e3892eef1f48a774a85c64b720f6ce\":[\"1527711915\"],\"_oembed_eb6d9f448075e3e78fae7a6a72f7d134\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/9otjp9ms8q?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_eb6d9f448075e3e78fae7a6a72f7d134\":[\"1527714949\"]},\"date\":\"2018-05-30 19:33:03\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19160,\"menu_order\":0,\"modified\":\"2018-05-30 21:19:40\",\"name\":\"start-course-marketing\",\"order\":2,\"parent_course\":19145,\"parent_section\":19158,\"permalink\":\"https://demo.lifterlms.com/lesson/start-course-marketing/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Start Course Marketing\",\"type\":\"lesson\",\"video_embed\":\"\"}],\"menu_order\":0,\"modified\":\"2018-05-30 19:32:48\",\"name\":\"19158\",\"order\":3,\"parent_course\":19145,\"status\":\"publish\",\"title\":\"Launch Your Course\",\"type\":\"section\"},{\"content\":\"\",\"custom\":[],\"date\":\"2018-05-30 19:33:52\",\"excerpt\":\"\",\"id\":19162,\"lessons\":[{\"audio_embed\":\"\",\"content\":\"https://gocodebox.wistia.com/medias/2kbpbs1ao0\\r\\n\\r\\n<h3 style=\\\"text-align: center;\\\">Lesson Resources</h3>\\r\\n<a href=\\\"https://lifterlms.com\\\">LifterLMS Website</a>\\r\\n<a href=\\\"https://lifterlms.com/store/\\\">LifterLMS Add-ons</a>\\r\\n<a href=\\\"https://lifterlms.com/product-category/bundles/\\\">Upgrade to a Bundle</a>\\r\\n<a href=\\\"https://lifterlms.com/try/\\\">Test Premium Add-ons Before You Buy with Your Very Own Demo Site</a>\\r\\n<a href=\\\"https://lifterlms.com/docs/\\\">LifterLMS Documentation</a>\\r\\n<a href=\\\"https://lifterlms.com/help/\\\">Contact LifterLMS</a>\",\"custom\":{\"_oembed_10fe58e242759c79b27dcb1e9c945161\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/2kbpbs1ao0?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"913\\\" height=\\\"514\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_10fe58e242759c79b27dcb1e9c945161\":[\"1527710928\"],\"_llms_reviews_enabled\":[\"\"],\"_llms_display_reviews\":[\"\"],\"_llms_num_reviews\":[\"0\"],\"_llms_multiple_reviews_disabled\":[\"\"],\"wpf-settings\":[\"a:4:{s:8:\\\"redirect\\\";s:0:\\\"\\\";s:12:\\\"redirect_url\\\";s:0:\\\"\\\";s:11:\\\"apply_delay\\\";s:1:\\\"0\\\";s:12:\\\"lock_content\\\";i:0;}\"],\"course_options\":[\"\"],\"launchpad_course_excerpt\":[\"\"],\"layout_options\":[\"\"],\"launchpad_page_menu\":[\"default\"],\"launchpad_hide_page_title\":[\"no\"],\"launchpad_hide_page_header\":[\"no\"],\"launchpad_hide_page_footer_widgets\":[\"no\"],\"launchpad_default_layout\":[\"content_sidebar\"],\"launchpad_header_content\":[\"\"],\"_oembed_a9069eeaff35f53a88e4af282ebbb267\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/2kbpbs1ao0?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"500\\\" height=\\\"281\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_a9069eeaff35f53a88e4af282ebbb267\":[\"1527710940\"],\"_oembed_b76ce23312a45eb8dbabb80d00b22d3a\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/2kbpbs1ao0?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"917\\\" height=\\\"516\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_b76ce23312a45eb8dbabb80d00b22d3a\":[\"1527710940\"],\"_oembed_605997120f6d4de872900b8373973d9f\":[\"<iframe src=\\\"https://fast.wistia.net/embed/iframe/2kbpbs1ao0?dnt=1\\\" title=\\\"Wistia video player\\\" allowtransparency=\\\"true\\\" frameborder=\\\"0\\\" scrolling=\\\"no\\\" class=\\\"wistia_embed\\\" name=\\\"wistia_embed\\\" allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen width=\\\"960\\\" height=\\\"540\\\"></iframe><script src=\\\"https://fast.wistia.net/assets/external/E-v1.js\\\" async></script>\"],\"_oembed_time_605997120f6d4de872900b8373973d9f\":[\"1527710942\"]},\"date\":\"2018-05-30 19:34:05\",\"date_available\":\"\",\"days_before_available\":0,\"drip_method\":\"\",\"excerpt\":\"\",\"featured_image\":\"\",\"free_lesson\":\"no\",\"has_prerequisite\":\"no\",\"id\":19163,\"menu_order\":0,\"modified\":\"2018-05-30 21:19:31\",\"name\":\"where-to-go-from-here-2\",\"order\":1,\"parent_course\":19145,\"parent_section\":19162,\"permalink\":\"https://demo.lifterlms.com/lesson/where-to-go-from-here-2/\",\"prerequisite\":0,\"quiz\":0,\"quiz_enabled\":\"no\",\"require_assignment_passing_grade\":\"yes\",\"require_passing_grade\":\"yes\",\"status\":\"publish\",\"time_available\":\"\",\"title\":\"Where To Go From Here\",\"type\":\"lesson\",\"video_embed\":\"\"}],\"menu_order\":0,\"modified\":\"2018-05-30 19:33:52\",\"name\":\"19162\",\"order\":4,\"parent_course\":19145,\"status\":\"publish\",\"title\":\"Next Steps\",\"type\":\"section\"}],\"start_date\":\"\",\"status\":\"publish\",\"tags\":[],\"temp_calc_data\":{\"students\":100,\"progress\":3123.03,\"quizzes\":21,\"grade\":2100},\"tile_featured_video\":\"no\",\"time_period\":\"no\",\"title\":\"The Official Quickstart Course for LifterLMS\",\"tracks\":[],\"type\":\"course\",\"video_embed\":\"\"}\n"
  },
  {
    "path": "src/blocks/access-plan-button/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/access-plan-button\",\n  \"title\": \"Access Plan Button\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Outputs a button which links to the purchase page for a LifterLMS access plan. Useful if you’re creating custom sales pages for courses or memberships.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"classes\": {\n      \"type\": \"string\"\n    },\n    \"id\": {\n      \"type\": \"string\"\n    },\n    \"size\": {\n      \"type\": \"string\",\n      \"default\": \"default\"\n    },\n    \"type\": {\n      \"type\": \"string\",\n      \"default\": \"primary\"\n    },\n    \"text\": {\n      \"type\": \"string\",\n      \"default\": \"Buy Now\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/access-plan-button/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome rectangle-wide regular.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\">\n\t\t<Path\n\t\t\td=\"M592 416H48c-26.5 0-48-21.5-48-48V144c0-26.5 21.5-48 48-48h544c26.5 0 48 21.5 48 48v224c0 26.5-21.5 48-48 48z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/access-plan-button/index.jsx",
    "content": "import { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tButtonGroup,\n\tFlex,\n\tDisabled,\n\tSelectControl,\n\tButton,\n\tBaseControl,\n\tSpinner,\n\tTextControl,\n\tComboboxControl,\n} from '@wordpress/components';\nimport {\n\tInspectorControls,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport apiFetch from '@wordpress/api-fetch';\nimport { useEffect, useState, useMemo, useRef } from '@wordpress/element';\n\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst [ accessPlans, setAccessPlans ] = useState( [] );\n\tconst [ isLoading, setIsLoading ] = useState( false );\n\tconst [ searchTerm, setSearchTerm ] = useState( '' );\n\n\t// Fetch plans from API based on search term.\n\tconst fetchPlans = ( term, value = '' ) => {\n\t\tsetIsLoading( true );\n\n\t\tapiFetch( {\n\t\t\tpath: `/llms/v1/access-plans?per_page=10&search=${ encodeURIComponent( term ) }`,\n\t\t} )\n\t\t\t.then( ( plans ) => {\n\t\t\t\tconst planOptions = plans.map( ( plan ) => {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tlabel: plan.title.rendered,\n\t\t\t\t\t\tvalue: plan.id.toString(),\n\t\t\t\t\t};\n\t\t\t\t} );\n\t\t\t\tsetAccessPlans( planOptions );\n\n\t\t\t\tif ( value && !planOptions.some( o => o.value === value ) ) {\n\t\t\t\t\tapiFetch( { path: `/llms/v1/access-plans?include=${ value }` } )\n\t\t\t\t\t\t.then( ( plans ) => {\n\t\t\t\t\t\t\tsetAccessPlans( [\n\t\t\t\t\t\t\t\t...planOptions,\n\t\t\t\t\t\t\t\t...plans.map( ( plan ) => {\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tlabel: plan.title.rendered,\n\t\t\t\t\t\t\t\t\tvalue: plan.id.toString(),\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t} ) ] );\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(() => {});\n\t\t\t\t}\n\t\t\t} )\n\t\t\t.catch( ( err ) => {\n\t\t\t\tif ( err.name !== 'AbortError' ) {\n\t\t\t\t\tsetAccessPlans( [] );\n\t\t\t\t}\n\t\t\t} )\n\t\t\t.finally( () => {\n\t\t\t\tsetIsLoading( false );\n\t\t\t} );\n\t};\n\n\tuseEffect( () => {\n\n\t\tconst timeout = setTimeout( () => fetchPlans( searchTerm, attributes.id ), 300 );\n\t\treturn () => clearTimeout( timeout );\n\n\t}, [ searchTerm, attributes ] );\n\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\tlet emptyPlaceholder = __( 'No Access Plans found matching your selection. This block will not be displayed.', 'lifterlms' );\n\n\t\tif ( ! attributes.id && accessPlans.length > 0 ) {\n\t\t\temptyPlaceholder = __( 'No Access Plan selected. Please choose an Access Plan from the block sidebar panel.', 'lifterlms' );\n\t\t}\n\n\t\treturn (\n\t\t\t<ServerSideRender\n\t\t\t\tblock={ blockJson.name }\n\t\t\t\tattributes={ attributes }\n\t\t\t\tLoadingResponsePlaceholder={ () => <Spinner /> }\n\t\t\t\tErrorResponsePlaceholder={ () => (\n\t\t\t\t\t<p className={ 'llms-block-error' }>\n\t\t\t\t\t\t{ __(\n\t\t\t\t\t\t\t'Error loading content. Please check block settings are valid. This block will not be displayed.',\n\t\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t\t) }\n\t\t\t\t\t</p>\n\t\t\t\t) }\n\t\t\t\tEmptyResponsePlaceholder={ () => (\n\t\t\t\t\t<p className={ 'llms-block-empty' }>\n\t\t\t\t\t\t{ emptyPlaceholder }\n\t\t\t\t\t</p>\n\t\t\t\t) }\n\t\t\t/>\n\t\t);\n\t}, [ attributes ] );\n\n\treturn (\n\t\t<>\n\t\t\t<InspectorControls>\n\t\t\t\t<PanelBody title={ __( 'Access Plan Button Settings', 'lifterlms' ) }>\n\t\t\t\t\t<PanelRow>\n\t\t\t\t\t\t<ComboboxControl\n\t\t\t\t\t\t\tlabel={ __( 'Access Plan', 'lifterlms' ) }\n\t\t\t\t\t\t\tvalue={ attributes.id ?? '' }\n\t\t\t\t\t\t\toptions={ accessPlans }\n\t\t\t\t\t\t\tonChange={ ( id ) => setAttributes( { id } ) }\n\t\t\t\t\t\t\tisLoading={ isLoading }\n\t\t\t\t\t\t\tonFilterValueChange={ setSearchTerm }\n\t\t\t\t\t\t\tallowReset={ true }\n\t\t\t\t\t\t\thelp={ __(\n\t\t\t\t\t\t\t\t'Select the access plan to display a button for.',\n\t\t\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t\t\t) }\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PanelRow>\n\t\t\t\t\t<PanelRow>\n\t\t\t\t\t\t<BaseControl\n\t\t\t\t\t\t\thelp={ __(\n\t\t\t\t\t\t\t\t'Controls the size of the button.',\n\t\t\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t\t\t) }\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Flex direction={ 'column' }>\n\t\t\t\t\t\t\t\t<BaseControl.VisualLabel>\n\t\t\t\t\t\t\t\t\t{ __( 'Size', 'lifterlms' ) }\n\t\t\t\t\t\t\t\t</BaseControl.VisualLabel>\n\t\t\t\t\t\t\t\t<ButtonGroup>\n\t\t\t\t\t\t\t\t\t{ [ 'Default', 'Large', 'Small' ].map( ( size ) => {\n\t\t\t\t\t\t\t\t\t\tconst value = size.toLowerCase();\n\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\tkey={ value }\n\t\t\t\t\t\t\t\t\t\t\t\tisPrimary={\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue === attributes.size\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={ () =>\n\t\t\t\t\t\t\t\t\t\t\t\t\tsetAttributes( {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize: value,\n\t\t\t\t\t\t\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{ size }\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t} ) }\n\t\t\t\t\t\t\t\t</ButtonGroup>\n\t\t\t\t\t\t\t</Flex>\n\t\t\t\t\t\t</BaseControl>\n\t\t\t\t\t</PanelRow>\n\t\t\t\t\t<PanelRow>\n\t\t\t\t\t\t<SelectControl\n\t\t\t\t\t\t\tlabel={ __( 'Type', 'lifterlms' ) }\n\t\t\t\t\t\t\thelp={ __(\n\t\t\t\t\t\t\t\t'Controls the style of the button. Your theme and/or custom CSS may alter the colors defined by these styles.',\n\t\t\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t\t\t) }\n\t\t\t\t\t\t\tvalue={ attributes.type }\n\t\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: __( 'Primary', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\tvalue: 'primary',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: __( 'Secondary', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\tvalue: 'secondary',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: __( 'Action', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\tvalue: 'action',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: __( 'Danger', 'lifterlms' ),\n\t\t\t\t\t\t\t\t\tvalue: 'danger',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t] }\n\t\t\t\t\t\t\tonChange={ ( type ) => setAttributes( { type } ) }\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PanelRow>\n\t\t\t\t\t<PanelRow>\n\t\t\t\t\t\t<TextControl\n\t\t\t\t\t\t\tlabel={ __( 'Text', 'lifterlms' ) }\n\t\t\t\t\t\t\thelp={ __(\n\t\t\t\t\t\t\t\t'The text to display on the button.',\n\t\t\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t\t\t) }\n\t\t\t\t\t\t\tvalue={ attributes.text }\n\t\t\t\t\t\t\tonChange={ ( text ) => setAttributes( { text } ) }\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PanelRow>\n\t\t\t\t</PanelBody>\n\t\t\t</InspectorControls>\n\t\t\t<div { ...blockProps }>\n\t\t\t\t<Disabled>{ memoizedServerSideRender }</Disabled>\n\t\t\t</div>\n\t\t</>\n\t);\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/certificate-title/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/certificate-title\",\n  \"title\": \"Certificate Title\",\n  \"category\": \"text\",\n  \"description\": \"Displays the title of a certificate.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"textAlign\": {\n      \"type\": \"string\",\n      \"default\": \"center\"\n    },\n    \"content\": {\n      \"type\": \"string\",\n      \"source\": \"html\",\n      \"selector\": \"h1,h2,h3,h4,h5,h6\",\n      \"default\": \"\",\n      \"__experimentalRole\": \"content\"\n    },\n    \"level\": {\n      \"type\": \"number\",\n      \"default\": 1\n    },\n    \"placeholder\": {\n      \"type\": \"string\"\n    },\n    \"fontFamily\": {\n      \"type\": \"string\",\n      \"default\": \"default\"\n    }\n  },\n  \"supports\": {\n    \"align\": [ \"wide\", \"full\" ],\n    \"anchor\": true,\n    \"className\": false,\n    \"color\": {\n      \"link\": true\n    },\n    \"spacing\": {\n      \"margin\": true\n    },\n    \"typography\": {\n      \"fontSize\": true,\n      \"lineHeight\": true,\n      \"__experimentalFontStyle\": true,\n      \"__experimentalFontFamily\": true,\n      \"__experimentalFontWeight\": true,\n      \"__experimentalLetterSpacing\": true,\n      \"__experimentalTextTransform\": true,\n      \"__experimentalDefaultControls\": {\n        \"fontSize\": true,\n        \"fontAppearance\": true,\n        \"textTransform\": true\n      }\n    },\n    \"__experimentalSelector\": \"h1,h2,h3,h4,h5,h6\",\n    \"__unstablePasteTextInline\": true,\n    \"__experimentalSlashInserter\": true,\n    \"multiple\": false,\n    \"llms_visibility\": false\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/certificate-title/edit.jsx",
    "content": "// WordPress dependencies.\nimport { __ } from '@wordpress/i18n';\nimport { useSelect } from '@wordpress/data';\nimport { store as blocksStore } from '@wordpress/blocks';\nimport { store as editorStore } from '@wordpress/editor';\n\n// Internal dependencies.\nimport { editCertificateTitle } from '../../js/util';\n\n/**\n * Block edit component.\n *\n * @since 6.0.0\n *\n * @param {Object}   args               Component arguments.\n * @param {Object}   args.attributes    Block attributes object.\n * @param {Function} args.setAttributes Function used to update the block's attributes.\n * @param {Function} args.mergeBlocks   Function called when merging the block with another block.\n * @param {Function} args.onReplace     Function called when replacing the block with another block.\n * @param {Object}   args.style         Block style attributes.\n * @param {string}   args.clientId      Block client ID.\n * @return {WPElement} The edit component.\n */\nexport default function Edit( {\n\tattributes,\n\tsetAttributes: origSetAttributes,\n\tmergeBlocks,\n\tonReplace,\n\tstyle,\n\tclientId,\n} ) {\n\tconst { getBlockType } = useSelect( blocksStore ),\n\t\t{ getEditedPostAttribute, getCurrentPostType } = useSelect( editorStore ),\n\t\t{ edit: HeadingEdit } = getBlockType( 'core/heading' ),\n\t\ttitleAttribute = 'llms_certificate' === getCurrentPostType() ? 'certificate_title' : 'title';\n\n\tattributes.placeholder = attributes.placeholder || __( 'Certificate of Achievement', 'lifterlms' );\n\tattributes.content = attributes.content || getEditedPostAttribute( titleAttribute );\n\n\tconst setAttributes = ( attrs ) => {\n\t\tconst { content } = attrs;\n\n\t\tif ( undefined !== content ) {\n\t\t\teditCertificateTitle( content );\n\t\t}\n\n\t\treturn origSetAttributes( attrs );\n\t};\n\n\tconst context = [];\n\n\treturn (\n\t\t<>\n\t\t\t<HeadingEdit { ...{\n\t\t\t\tattributes,\n\t\t\t\tsetAttributes,\n\t\t\t\tmergeBlocks,\n\t\t\t\tonReplace,\n\t\t\t\tstyle,\n\t\t\t\tclientId,\n\t\t\t\tcontext\n\t\t\t} } />\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/blocks/certificate-title/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome award solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\">\n\t\t<Path\n\t\t\td=\"M173.8 5.5c11-7.3 25.4-7.3 36.4 0L228 17.2c6 3.9 13 5.8 20.1 5.4l21.3-1.3c13.2-.8 25.6 6.4 31.5 18.2l9.6 19.1c3.2 6.4 8.4 11.5 14.7 14.7L344.5 83c11.8 5.9 19 18.3 18.2 31.5l-1.3 21.3c-.4 7.1 1.5 14.2 5.4 20.1l11.8 17.8c7.3 11 7.3 25.4 0 36.4L366.8 228c-3.9 6-5.8 13-5.4 20.1l1.3 21.3c.8 13.2-6.4 25.6-18.2 31.5l-19.1 9.6c-6.4 3.2-11.5 8.4-14.7 14.7L301 344.5c-5.9 11.8-18.3 19-31.5 18.2l-21.3-1.3c-7.1-.4-14.2 1.5-20.1 5.4l-17.8 11.8c-11 7.3-25.4 7.3-36.4 0L156 366.8c-6-3.9-13-5.8-20.1-5.4l-21.3 1.3c-13.2 .8-25.6-6.4-31.5-18.2l-9.6-19.1c-3.2-6.4-8.4-11.5-14.7-14.7L39.5 301c-11.8-5.9-19-18.3-18.2-31.5l1.3-21.3c.4-7.1-1.5-14.2-5.4-20.1L5.5 210.2c-7.3-11-7.3-25.4 0-36.4L17.2 156c3.9-6 5.8-13 5.4-20.1l-1.3-21.3c-.8-13.2 6.4-25.6 18.2-31.5l19.1-9.6C65 70.2 70.2 65 73.4 58.6L83 39.5c5.9-11.8 18.3-19 31.5-18.2l21.3 1.3c7.1 .4 14.2-1.5 20.1-5.4L173.8 5.5zM272 192a80 80 0 1 0 -160 0 80 80 0 1 0 160 0zM1.3 441.8L44.4 339.3c.2 .1 .3 .2 .4 .4l9.6 19.1c11.7 23.2 36 37.3 62 35.8l21.3-1.3c.2 0 .5 0 .7 .2l17.8 11.8c5.1 3.3 10.5 5.9 16.1 7.7l-37.6 89.3c-2.3 5.5-7.4 9.2-13.3 9.7s-11.6-2.2-14.8-7.2L74.4 455.5l-56.1 8.3c-5.7 .8-11.4-1.5-15-6s-4.3-10.7-2.1-16zm248 60.4L211.7 413c5.6-1.8 11-4.3 16.1-7.7l17.8-11.8c.2-.1 .4-.2 .7-.2l21.3 1.3c26 1.5 50.3-12.6 62-35.8l9.6-19.1c.1-.2 .2-.3 .4-.4l43.2 102.5c2.2 5.3 1.4 11.4-2.1 16s-9.3 6.9-15 6l-56.1-8.3-32.2 49.2c-3.2 5-8.9 7.7-14.8 7.2s-11-4.3-13.3-9.7z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/certificate-title/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Edit from './edit.jsx';\nimport Save from './save.jsx';\nimport Icon from './icon.jsx';\n\n/**\n * Register the Certificate Title block.\n *\n * @since 6.0.0\n */\nregisterBlockType(\n\tblockJson,\n\t{\n\t\ticon: Icon,\n\t\tedit: Edit,\n\t\tsave: Save,\n\t}\n);\n"
  },
  {
    "path": "src/blocks/certificate-title/save.jsx",
    "content": "// External dependencies.\nimport classnames from 'classnames';\n\n// WordPress dependencies.\nimport { RichText, useBlockProps } from '@wordpress/block-editor';\n\n/**\n * Save the block content.\n *\n * @since 6.0.0\n *\n * @param {Object} args            Save arguments.\n * @param {Object} args.attributes Block attributes.\n * @return {WPElement} Block HTML fragment.\n */\nexport default function save( { attributes } ) {\n\tconst { textAlign, content, level } = attributes,\n\t\tTagName = 'h' + level,\n\t\tclassName = classnames( {\n\t\t\t[ `has-text-align-${ textAlign }` ]: textAlign,\n\t\t} );\n\n\treturn (\n\t\t<TagName { ...useBlockProps.save( { className } ) }>\n\t\t\t<RichText.Content value={ content } />\n\t\t</TagName>\n\t);\n}\n"
  },
  {
    "path": "src/blocks/checkout/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/checkout\",\n  \"title\": \"Checkout\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Outputs the checkout page for purchasing courses and memberships in LifterLMS.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"cols\": {\n      \"type\": \"integer\",\n      \"default\": 1\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/checkout/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome cart-shopping solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\">\n\t\t<Path\n\t\t\td=\"M0 24C0 10.7 10.7 0 24 0H69.5c22 0 41.5 12.8 50.6 32h411c26.3 0 45.5 25 38.6 50.4l-41 152.3c-8.5 31.4-37 53.3-69.5 53.3H170.7l5.4 28.5c2.2 11.3 12.1 19.5 23.6 19.5H488c13.3 0 24 10.7 24 24s-10.7 24-24 24H199.7c-34.6 0-64.3-24.6-70.7-58.5L77.4 54.5c-.7-3.8-4-6.5-7.9-6.5H24C10.7 48 0 37.3 0 24zM128 464a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zm336-48a48 48 0 1 1 0 96 48 48 0 1 1 0-96z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/checkout/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tDisabled,\n\tFlex,\n\tBaseControl,\n\tButtonGroup,\n\tButton,\n\tSpinner,\n} from '@wordpress/components';\nimport {\n\tInspectorControls,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst VisualLabel = BaseControl.VisualLabel ?? <></>;\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\n\tconst columns = {\n\t\t1: __( 'One', 'lifterlms' ),\n\t\t2: __( 'Two', 'lifterlms' ),\n\t};\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>\n\t\t\t\t\t{ __( 'There was an error loading the content. This block will not be displayed.', 'lifterlms' ) }\n\t\t\t\t</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>\n\t\t\t\t\t{ __( 'Checkout not available. This block will not be displayed.', 'lifterlms' ) }\n\t\t\t\t</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'Checkout Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<BaseControl\n\t\t\t\t\t\thelp={ __( 'Determines the number of columns on the checkout screen. 1 or 2 are the only acceptable values.', 'lifterlms' ) }\n\t\t\t\t\t>\n\t\t\t\t\t\t<Flex\n\t\t\t\t\t\t\tdirection={ 'column' }\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<VisualLabel>\n\t\t\t\t\t\t\t\t{ __( 'Number of Columns', 'lifterlms' ) }\n\t\t\t\t\t\t\t</VisualLabel>\n\t\t\t\t\t\t\t<ButtonGroup>\n\t\t\t\t\t\t\t\t{ Object.keys( columns ).map( ( column ) => {\n\t\t\t\t\t\t\t\t\treturn <Button\n\t\t\t\t\t\t\t\t\t\tkey={ column }\n\t\t\t\t\t\t\t\t\t\tisPrimary={ column === attributes.cols }\n\t\t\t\t\t\t\t\t\t\tonClick={ () => setAttributes( {\n\t\t\t\t\t\t\t\t\t\t\tcols: column,\n\t\t\t\t\t\t\t\t\t\t} ) }\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{ columns[ column ] }\n\t\t\t\t\t\t\t\t\t</Button>;\n\t\t\t\t\t\t\t\t} ) }\n\t\t\t\t\t\t\t</ButtonGroup>\n\t\t\t\t\t\t</Flex>\n\t\t\t\t\t</BaseControl>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/course-author/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/course-author\",\n  \"title\": \"Course Author\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Display the Course Author’s name, avatar, and (optionally) biography for a specific course.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"avatar_size\": {\n      \"type\": \"integer\",\n      \"default\": 48\n    },\n    \"bio\": {\n      \"type\": \"string\",\n      \"default\": \"yes\"\n    },\n    \"course_id\": {\n      \"type\": \"integer\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/course-author/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome user solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\">\n\t\t<Path\n\t\t\td=\"M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/course-author/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tDisabled,\n\tRangeControl,\n\tToggleControl,\n\tSpinner,\n} from '@wordpress/components';\nimport {\n\tInspectorControls,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport { usePostOptions, PostSelect } from '../../../packages/components/src/post-select';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\tlet emptyPlaceholder = __( 'Author not found. This block will not be displayed.', 'lifterlms' );\n\n\t\tif ( ! attributes.course_id && courseOptions.length > 0 ) {\n\t\t\temptyPlaceholder = __( 'No course selected. Please choose a Course from the block sidebar panel.', 'lifterlms' );\n\t\t}\n\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ emptyPlaceholder }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'Course Author Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<RangeControl\n\t\t\t\t\t\tlabel={ __( 'Avatar Size', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ __( 'The size of the avatar in pixels.', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.avatar_size }\n\t\t\t\t\t\tonChange={ ( size ) => setAttributes( {\n\t\t\t\t\t\t\tavatar_size: parseInt( size ),\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t\tmin={ 0 }\n\t\t\t\t\t\tmax={ 300 }\n\t\t\t\t\t\tallowReset={ true }\n\t\t\t\t\t\tresetFallbackValue={ blockJson.attributes.avatar_size.default }\n\t\t\t\t\t\tdefault={ blockJson.attributes.avatar_size.default }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Display Bio', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ attributes?.bio ? __( 'Author bio is displayed.', 'lifterlms' ) : __( 'Author bio is hidden.', 'lifterlms' ) }\n\t\t\t\t\t\tchecked={ attributes.bio === 'yes' }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tbio: value ? 'yes' : 'no',\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PostSelect { ...props } />\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/course-continue/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/course-continue\",\n  \"title\": \"Course Progress with Continue Button\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Display a progress bar with continue button for a specific course. Renders only for enrolled students.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"course_id\": {\n      \"type\": \"integer\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/course-continue/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome bars-progress solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\n\t\t<Path\n\t\t\td=\"M448 160H320V128H448v32zM48 64C21.5 64 0 85.5 0 112v64c0 26.5 21.5 48 48 48H464c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zM448 352v32H192V352H448zM48 288c-26.5 0-48 21.5-48 48v64c0 26.5 21.5 48 48 48H464c26.5 0 48-21.5 48-48V336c0-26.5-21.5-48-48-48H48z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/course-continue/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tDisabled,\n\tSpinner,\n} from '@wordpress/components';\nimport {\n\tInspectorControls,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport { usePostOptions, PostSelect } from '../../../packages/components/src/post-select';\n\nconst Edit = ( props ) => {\n\tconst { attributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\tlet emptyPlaceholder = __( 'No progress data found for this course. This block will not be displayed.', 'lifterlms' );\n\n\t\tif ( ! attributes.course_id && courseOptions.length > 0 ) {\n\t\t\temptyPlaceholder = __( 'No course selected. Please choose a Course from the block sidebar panel.', 'lifterlms' );\n\t\t}\n\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ emptyPlaceholder }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn (\n\t\t<>\n\t\t\t<InspectorControls>\n\t\t\t\t<PanelBody\n\t\t\t\t\ttitle={ __( 'Course Continue Settings', 'lifterlms' ) }\n\t\t\t\t>\n\t\t\t\t\t<PostSelect { ...props } />\n\t\t\t\t</PanelBody>\n\t\t\t</InspectorControls>\n\t\t\t<div { ...blockProps }>\n\t\t\t\t<Disabled>\n\t\t\t\t\t{ memoizedServerSideRender }\n\t\t\t\t</Disabled>\n\t\t\t</div>\n\t\t</>\n\t);\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/course-meta-info/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/course-meta-info\",\n  \"title\": \"Course Meta Information\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Display all meta information for a course.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"course_id\": {\n      \"type\": \"integer\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/course-meta-info/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome circle-info solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\n\t\t<Path\n\t\t\td=\"M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/course-meta-info/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tDisabled,\n\tSpinner,\n} from '@wordpress/components';\nimport {\n\tInspectorControls,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport { usePostOptions, PostSelect } from '../../../packages/components/src/post-select';\n\nconst Edit = ( props ) => {\n\tconst { attributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\tlet emptyPlaceholder = __( 'No meta information available for this course. This block will not be displayed.', 'lifterlms' );\n\n\t\tif ( ! attributes.course_id && courseOptions.length > 0 ) {\n\t\t\temptyPlaceholder = __( 'No course selected. Please choose a Course from the block sidebar panel.', 'lifterlms' );\n\t\t}\n\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ emptyPlaceholder }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody\n\t\t\t\ttitle={ __( 'Course Meta Info Settings', 'lifterlms' ) }\n\t\t\t>\n\t\t\t\t<PostSelect { ...props } />\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/course-outline/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/course-outline\",\n  \"title\": \"Course Outline\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Outputs the course outline as displayed by the widget of the same name. Can show full course outline or just the current section outline. Setting the Outline Type to Current Sections refers to the section that contains the next uncompleted lesson for current student. If the student is not enrolled then the first section in the course will be displayed.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"collapse\": {\n      \"type\": \"boolean\",\n      \"default\": false\n    },\n    \"course_id\": {\n      \"type\": \"integer\"\n    },\n    \"outline_type\": {\n      \"type\": \"string\",\n      \"default\": \"full\"\n    },\n    \"toggles\": {\n      \"type\": \"boolean\",\n      \"default\": false\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/course-outline/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome folder-tree solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\">\n\t\t<Path\n\t\t\td=\"M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32v96V384c0 35.3 28.7 64 64 64H256V384H64V160H256V96H64V32zM288 192c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V192zm0 288c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V480z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/course-outline/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tDisabled,\n\tToggleControl,\n\tSelectControl,\n\tSpinner,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport { usePostOptions, useLlmsPostType, PostSelect } from '../../../packages/components/src/post-select';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst isLlmsPostType = useLlmsPostType();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ __( 'No outline information available for this course. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\tif ( ! attributes.course_id && ! isLlmsPostType ) {\n\t\tsetAttributes( {\n\t\t\tcourse_id: courseOptions?.[ 0 ]?.value,\n\t\t} );\n\t}\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'Course Outline Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Collapse', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ __( 'If true, will make the outline sections collapsible via click events.', 'lifterlms' ) }\n\t\t\t\t\t\tchecked={ attributes.collapse }\n\t\t\t\t\t\tonChange={ ( collapse ) => setAttributes( {\n\t\t\t\t\t\t\tcollapse,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t{ attributes.collapse &&\n\t\t\t\t\t<PanelRow>\n\t\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\t\tlabel={ __( 'Toggles', 'lifterlms' ) }\n\t\t\t\t\t\t\thelp={ __( 'If true, will display \"Collapse All\" and \"Expand All\" toggles at the bottom of the outline. Only functions if \"collapse\" is true.', 'lifterlms' ) }\n\t\t\t\t\t\t\tchecked={ attributes.toggles }\n\t\t\t\t\t\t\tonChange={ ( toggles ) => setAttributes( {\n\t\t\t\t\t\t\t\ttoggles,\n\t\t\t\t\t\t\t} ) }\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PanelRow>\n\t\t\t\t}\n\t\t\t\t<PostSelect { ...props } />\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Outline Type', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ __( 'Select the type of outline to display.', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.outline_type }\n\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tlabel: __( 'Full', 'lifterlms' ),\n\t\t\t\t\t\t\t\tvalue: 'full',\n\t\t\t\t\t\t\t\tisDefault: true,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tlabel: __( 'Current Section', 'lifterlms' ),\n\t\t\t\t\t\t\t\tvalue: 'current_section',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t] }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\toutline_type: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/course-prerequisites/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/course-prerequisites\",\n  \"title\": \"Course Prerequisites\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Display a notice describing unfulfilled prerequisites for a course.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"course_id\": {\n      \"type\": \"integer\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/course-prerequisites/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome list-check solid.\nconst Icon = () => (\n\t<SVG\n\t\tclassName=\"llms-block-icon\"\n\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\tviewBox=\"0 0 512 512\"\n\t>\n\t\t<Path d=\"M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/course-prerequisites/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tDisabled,\n\tSpinner,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport { PostSelect, usePostOptions } from '../../../packages/components/src/post-select';\n\nconst Edit = ( props ) => {\n\tconst { attributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\tlet emptyPlaceholder = __( 'No prerequisites available for this course. This block will not be displayed.', 'lifterlms' );\n\n\t\tif ( ! attributes.course_id && courseOptions.length > 0 ) {\n\t\t\temptyPlaceholder = __( 'No course selected. Please choose a Course from the block sidebar panel.', 'lifterlms' );\n\t\t}\n\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ emptyPlaceholder }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody\n\t\t\t\ttitle={ __( 'Course Prerequisites Settings', 'lifterlms' ) }\n\t\t\t>\n\t\t\t\t<PostSelect { ...props } />\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/course-reviews/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/course-reviews\",\n  \"title\": \"Course Reviews\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Display reviews and review form for a LifterLMS Course.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"course_id\": {\n      \"type\": \"integer\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/course-reviews/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome star-half-stroke solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\">\n\t\t<Path\n\t\t\td=\"M320 376.4l.1-.1 26.4 14.1 85.2 45.5-16.5-97.6-4.8-28.7 20.7-20.5 70.1-69.3-96.1-14.2-29.3-4.3-12.9-26.6L320.1 86.9l-.1 .3V376.4zm175.1 98.3c2 12-3 24.2-12.9 31.3s-23 8-33.8 2.3L320.1 439.8 191.8 508.3C181 514 167.9 513.1 158 506s-14.9-19.3-12.9-31.3L169.8 329 65.6 225.9c-8.6-8.5-11.7-21.2-7.9-32.7s13.7-19.9 25.7-21.7L227 150.3 291.4 18c5.4-11 16.5-18 28.8-18s23.4 7 28.8 18l64.3 132.3 143.6 21.2c12 1.8 22 10.2 25.7 21.7s.7 24.2-7.9 32.7L470.5 329l24.6 145.7z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/course-reviews/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tDisabled,\n\tSpinner,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport { PostSelect, usePostOptions } from '../../../packages/components/src/post-select';\n\nconst Edit = ( props ) => {\n\tconst { attributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\tlet emptyPlaceholder = __( 'No reviews found for this course. This block will not be displayed.', 'lifterlms' );\n\n\t\tif ( ! attributes.course_id && courseOptions.length > 0 ) {\n\t\t\temptyPlaceholder = __( 'No course selected. Please choose a Course from the block sidebar panel.', 'lifterlms' );\n\t\t}\n\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ emptyPlaceholder }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody\n\t\t\t\ttitle={ __( 'Course Reviews Settings', 'lifterlms' ) }\n\t\t\t>\n\t\t\t\t<PostSelect { ...props } />\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/course-syllabus/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/course-syllabus\",\n  \"title\": \"Course Syllabus\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Display the Course Syllabus for a specific course.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"course_id\": {\n      \"type\": \"integer\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/course-syllabus/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome table-list solid.\nconst Icon = () => (\n\t<SVG\n\t\tclassName=\"llms-block-icon\"\n\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\tviewBox=\"0 0 512 512\">\n\t\t<Path\n\t\t\td=\"M152.1 38.2c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 113C-2.3 103.6-2.3 88.4 7 79s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zm0 160c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 273c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zM224 96c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zm0 160c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zM160 416c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-17.7 0-32-14.3-32-32zM48 368a48 48 0 1 1 0 96 48 48 0 1 1 0-96z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/course-syllabus/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tDisabled,\n\tSpinner,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport { PostSelect, usePostOptions, useLlmsPostType } from '../../../packages/components/src/post-select';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst isLlmsPostType = useLlmsPostType();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ {\n\t\t\t\tcourse_id: attributes.course_id ?? courseOptions?.[ 0 ]?.value,\n\t\t\t} }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ __( 'No syllabus found for this course. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\tif ( ! attributes.course_id && ! isLlmsPostType ) {\n\t\tsetAttributes( {\n\t\t\tcourse_id: courseOptions?.[ 0 ]?.value,\n\t\t} );\n\t}\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody\n\t\t\t\ttitle={ __( 'Course Syllabus Settings', 'lifterlms' ) }\n\t\t\t>\n\t\t\t\t<PostSelect\n\t\t\t\t\tattributes={ attributes }\n\t\t\t\t\tsetAttributes={ setAttributes }\n\t\t\t\t/>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/courses/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/courses\",\n  \"title\": \"Courses\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Displays a loop of LifterLMS Course \\\"Tiles\\\" as displayed on the default \\\"Courses\\\" page.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"category\": {\n      \"type\": \"string\"\n    },\n    \"hidden\": {\n      \"type\": \"boolean\",\n      \"default\": true\n    },\n    \"id\": {\n      \"type\": \"string\"\n    },\n    \"mine\": {\n      \"type\": \"string\"\n    },\n    \"order\": {\n      \"type\": \"string\",\n      \"default\": \"ASC\"\n    },\n    \"orderby\": {\n      \"type\": \"string\",\n      \"default\": \"title\"\n    },\n    \"posts_per_page\": {\n      \"type\": \"integer\",\n      \"default\": -1\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/courses/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome graduation-cap solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\">\n\t\t<Path\n\t\t\td=\"M320 32c-8.1 0-16.1 1.4-23.7 4.1L15.8 137.4C6.3 140.9 0 149.9 0 160s6.3 19.1 15.8 22.6l57.9 20.9C57.3 229.3 48 259.8 48 291.9v28.1c0 28.4-10.8 57.7-22.3 80.8c-6.5 13-13.9 25.8-22.5 37.6C0 442.7-.9 448.3 .9 453.4s6 8.9 11.2 10.2l64 16c4.2 1.1 8.7 .3 12.4-2s6.3-6.1 7.1-10.4c8.6-42.8 4.3-81.2-2.1-108.7C90.3 344.3 86 329.8 80 316.5V291.9c0-30.2 10.2-58.7 27.9-81.5c12.9-15.5 29.6-28 49.2-35.7l157-61.7c8.2-3.2 17.5 .8 20.7 9s-.8 17.5-9 20.7l-157 61.7c-12.4 4.9-23.3 12.4-32.2 21.6l159.6 57.6c7.6 2.7 15.6 4.1 23.7 4.1s16.1-1.4 23.7-4.1L624.2 182.6c9.5-3.4 15.8-12.5 15.8-22.6s-6.3-19.1-15.8-22.6L343.7 36.1C336.1 33.4 328.1 32 320 32zM128 408c0 35.3 86 72 192 72s192-36.7 192-72L496.7 262.6 354.5 314c-11.1 4-22.8 6-34.5 6s-23.5-2-34.5-6L143.3 262.6 128 408z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/courses/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\t// eslint-disable-next-line @wordpress/no-unsafe-wp-apis\n\t__experimentalNumberControl as NumberControl,\n\tBaseControl,\n\tDisabled,\n\tFormTokenField,\n\tPanelBody,\n\tPanelRow,\n\tSelectControl,\n\tSpinner,\n\tToggleControl,\n\tComboboxControl,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport { useState, useMemo, useEffect, useRef } from '@wordpress/element';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport apiFetch from '@wordpress/api-fetch';\n\n// Internal dependencies.\nimport { usePostOptions } from '../../../packages/components/src/post-select';\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst [ courseTitles, setCourseTitles ] = useState( [] );\n\tconst [ categoryOptions, setCategoryOptions ] = useState( [] );\n\tconst [ isLoading, setIsLoading ] = useState( false );\n\tconst [ searchTerm, setSearchTerm ] = useState( '' );\n\n\t// Fetch categories from API based on search term.\n\tconst fetchCategories = ( term, value = '' ) => {\n\t\tsetIsLoading( true );\n\n\t\tconst searchParam = term ? `&search=${ encodeURIComponent( term ) }` : '';\n\t\tapiFetch( {\n\t\t\tpath: `/wp/v2/course_cat?per_page=10${ searchParam }`,\n\t\t} )\n\t\t\t.then( ( categories ) => {\n\t\t\t\tconst options = categories.map( ( category ) => {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalue: category.slug,\n\t\t\t\t\t\tlabel: category.name,\n\t\t\t\t\t};\n\t\t\t\t} );\n\n\t\t\t\toptions.unshift( {\n\t\t\t\t\tvalue: '',\n\t\t\t\t\tlabel: __( '- All -', 'lifterlms' ),\n\t\t\t\t} );\n\n\t\t\t\t// If there's a selected value and it's not in the results, fetch it separately.\n\t\t\t\tif ( value && !options.some( o => o.value === value ) ) {\n\t\t\t\t\tapiFetch( { path: `/wp/v2/course_cat?slug=${ encodeURIComponent( value ) }` } )\n\t\t\t\t\t\t.then( ( categories ) => {\n\t\t\t\t\t\t\tif ( categories && categories.length > 0 ) {\n\t\t\t\t\t\t\t\tsetCategoryOptions( [\n\t\t\t\t\t\t\t\t\t...options,\n\t\t\t\t\t\t\t\t\t...categories.map( ( category ) => {\n\t\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\t\tvalue: category.slug,\n\t\t\t\t\t\t\t\t\t\t\tlabel: category.name,\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t} ),\n\t\t\t\t\t\t\t\t] );\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tsetCategoryOptions( options );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} )\n\t\t\t\t\t\t.catch( () => {\n\t\t\t\t\t\t\tsetCategoryOptions( options );\n\t\t\t\t\t\t} );\n\t\t\t\t} else {\n\t\t\t\t\tsetCategoryOptions( options );\n\t\t\t\t}\n\t\t\t} )\n\t\t\t.catch( () => {\n\t\t\t\tsetCategoryOptions( [\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue: '',\n\t\t\t\t\t\tlabel: __( '- All -', 'lifterlms' ),\n\t\t\t\t\t},\n\t\t\t\t] );\n\t\t\t} )\n\t\t\t.finally( () => {\n\t\t\t\tsetIsLoading( false );\n\t\t\t} );\n\t};\n\n\tuseEffect( () => {\n\t\tconst timeout = setTimeout( () => fetchCategories( searchTerm, attributes?.category ), 300 );\n\t\treturn () => clearTimeout( timeout );\n\t}, [ searchTerm, attributes?.category ] );\n\n\tconst fetchedOptions = usePostOptions();\n\tconst courseOptions = {};\n\n\tfetchedOptions?.forEach( ( { value, label } ) => {\n\t\tcourseOptions[ value ] = label;\n\t} );\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>\n\t\t\t\t\t{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }\n\t\t\t\t</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>\n\t\t\t\t\t{ __( 'No courses found matching your selection. This block will not be displayed.', 'lifterlms' ) }\n\t\t\t\t</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'Courses Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<ComboboxControl\n\t\t\t\t\t\tlabel={ __( 'Category', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ __( 'Display courses from a specific Course Category only.', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes?.category || '' }\n\t\t\t\t\t\toptions={ categoryOptions }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tcategory: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t\tonFilterValueChange={ setSearchTerm }\n\t\t\t\t\t\tisLoading={ isLoading }\n\t\t\t\t\t\tallowReset={ true }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Show hidden courses?', 'lifterlms' ) }\n\t\t\t\t\t\tchecked={ attributes.hidden }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { hidden: value } ) }\n\t\t\t\t\t\thelp={ __( 'Whether or not courses with a \"hidden\" visibility should be included. Defaults to \"yes\" (hidden courses displayed). Switch to \"no\" to exclude hidden courses.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<BaseControl\n\t\t\t\t\t\thelp={ __( 'Display only specific course(s). You can select multiple courses.', 'lifterlms' ) }\n\t\t\t\t\t>\n\t\t\t\t\t\t<FormTokenField\n\t\t\t\t\t\t\tlabel={ __( 'Courses', 'lifterlms' ) }\n\t\t\t\t\t\t\tplaceholder={ __( 'Search available courses', 'lifterlms' ) }\n\t\t\t\t\t\t\tsuggestions={ Object.values( courseOptions ) }\n\t\t\t\t\t\t\tvalue={ courseTitles }\n\t\t\t\t\t\t\tonChange={ ( value ) => {\n\t\t\t\t\t\t\t\tsetCourseTitles( value );\n\t\t\t\t\t\t\t\tsetAttributes( {\n\t\t\t\t\t\t\t\t\tid: value.map( ( title ) => {\n\t\t\t\t\t\t\t\t\t\treturn Object.keys( courseOptions ).find( ( key ) => courseOptions[ key ] === title );\n\t\t\t\t\t\t\t\t\t} ).join( ',' ),\n\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t} }\n\t\t\t\t\t\t\t__experimentalShowHowTo={ false }\n\t\t\t\t\t\t/>\n\t\t\t\t\t</BaseControl>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Show only my courses', 'lifterlms' ) }\n\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t{ value: 'no', label: __( 'No', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'any', label: __( 'Any', 'lifterlms' ) },\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'enrolled',\n\t\t\t\t\t\t\t\tlabel: __( 'Enrolled', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'expired',\n\t\t\t\t\t\t\t\tlabel: __( 'Expired', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'cancelled',\n\t\t\t\t\t\t\t\tlabel: __( 'Cancelled', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t] }\n\t\t\t\t\t\tchecked={ attributes.mine }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { mine: value } ) }\n\t\t\t\t\t\thelp={ __( 'Show only courses the current student is enrolled in. By default (\"no\") shows courses regardless of enrollment.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Order', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.order }\n\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'ASC',\n\t\t\t\t\t\t\t\tlabel: __( 'Ascending', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'DESC',\n\t\t\t\t\t\t\t\tlabel: __( 'Descending', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t] }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { order: value } ) }\n\t\t\t\t\t\thelp={ __( 'Display courses in ascending or descending order.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Order by', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.orderby }\n\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t{ value: 'id', label: __( 'ID', 'lifterlms' ) },\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'author',\n\t\t\t\t\t\t\t\tlabel: __( 'Author', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'title',\n\t\t\t\t\t\t\t\tlabel: __( 'Title', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ value: 'name', label: __( 'Name', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'date', label: __( 'Date', 'lifterlms' ) },\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'modified',\n\t\t\t\t\t\t\t\tlabel: __( 'Date modified', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'rand',\n\t\t\t\t\t\t\t\tlabel: __( 'Random', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvalue: 'menu_order',\n\t\t\t\t\t\t\t\tlabel: __( 'Menu Order', 'lifterlms' ),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t] }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { orderby: value } ) }\n\t\t\t\t\t\thelp={ __( 'Determines which field is used to order courses in the courses list.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<NumberControl\n\t\t\t\t\t\tlabel={ __( 'Per Page', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.posts_per_page }\n\t\t\t\t\t\tmin={ -1 }\n\t\t\t\t\t\tmax={ 100 }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { posts_per_page: value ?? -1 } ) }\n\t\t\t\t\t\thelp={ __( ' Determines the number of results to display. Default returns all available courses.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/login/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/login\",\n  \"title\": \"LifterLMS Login\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Displays the LifterLMS login form. If a user is already logged in, nothing is displayed.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"layout\": {\n      \"type\": \"string\",\n      \"default\": \"columns\"\n    },\n    \"redirect\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/login/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome right-to-bracket solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\n\t\t<Path\n\t\t\td=\"M217.9 105.9L340.7 228.7c7.2 7.2 11.3 17.1 11.3 27.3s-4.1 20.1-11.3 27.3L217.9 406.1c-6.4 6.4-15 9.9-24 9.9c-18.7 0-33.9-15.2-33.9-33.9l0-62.1L32 320c-17.7 0-32-14.3-32-32l0-64c0-17.7 14.3-32 32-32l128 0 0-62.1c0-18.7 15.2-33.9 33.9-33.9c9 0 17.6 3.6 24 9.9zM352 416l64 0c17.7 0 32-14.3 32-32l0-256c0-17.7-14.3-32-32-32l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64 0c53 0 96 43 96 96l0 256c0 53-43 96-96 96l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/login/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tBaseControl, Button, ButtonGroup,\n\tDisabled, Flex, PanelBody, PanelRow, Spinner, TextControl,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst Edit = ( { attributes, setAttributes } ) => {\n\tconst blockProps = useBlockProps();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ __( 'Displays LifterLMS register form. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'Login Form Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<BaseControl\n\t\t\t\t\t\thelp={ __( 'Controls the size of the button.', 'lifterlms' ) }\n\t\t\t\t\t>\n\t\t\t\t\t\t<Flex\n\t\t\t\t\t\t\tdirection={ 'column' }\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<BaseControl.VisualLabel>\n\t\t\t\t\t\t\t\t{ __( 'Size', 'lifterlms' ) }\n\t\t\t\t\t\t\t</BaseControl.VisualLabel>\n\t\t\t\t\t\t\t<ButtonGroup>\n\t\t\t\t\t\t\t\t{ [ 'Columns', 'Stacked' ].map( ( layout ) => {\n\t\t\t\t\t\t\t\t\tconst value = layout?.toLowerCase();\n\n\t\t\t\t\t\t\t\t\treturn <Button\n\t\t\t\t\t\t\t\t\t\tkey={ value }\n\t\t\t\t\t\t\t\t\t\tisPrimary={ value === attributes.layout }\n\t\t\t\t\t\t\t\t\t\tonClick={ () => setAttributes( {\n\t\t\t\t\t\t\t\t\t\t\tlayout: value,\n\t\t\t\t\t\t\t\t\t\t} ) }\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{ layout }\n\t\t\t\t\t\t\t\t\t</Button>;\n\t\t\t\t\t\t\t\t} ) }\n\t\t\t\t\t\t\t</ButtonGroup>\n\t\t\t\t\t\t</Flex>\n\t\t\t\t\t</BaseControl>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Login redirect URL', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.redirect }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tredirect: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div\n\t\t\t{ ...blockProps }\n\t\t>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/memberships/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/memberships\",\n  \"title\": \"Memberships\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Display a loop of LifterLMS Membership \\\"Tiles\\\" as displayed on the default \\\"Memberships\\\" page.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"category\": {\n      \"type\": \"string\"\n    },\n    \"hidden\": {\n      \"type\": \"boolean\",\n      \"default\": true\n    },\n    \"id\": {\n      \"type\": \"string\"\n    },\n    \"order\": {\n      \"type\": \"string\",\n      \"default\": \"ASC\"\n    },\n    \"orderby\": {\n      \"type\": \"string\",\n      \"default\": \"title\"\n    },\n    \"posts_per_page\": {\n      \"type\": \"integer\",\n      \"default\": -1\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/memberships/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome users solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\">\n\t\t<Path\n\t\t\td=\"M144 0a80 80 0 1 1 0 160A80 80 0 1 1 144 0zM512 0a80 80 0 1 1 0 160A80 80 0 1 1 512 0zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM224 224a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/memberships/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tSelectControl,\n\t// eslint-disable-next-line @wordpress/no-unsafe-wp-apis\n\t__experimentalNumberControl as NumberControl,\n\tTextControl,\n\tDisabled,\n\tSpinner,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport { useSelect } from '@wordpress/data';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\n\tconst { categories, memberships } = useSelect( ( select ) => {\n\t\treturn {\n\t\t\tcategories: select( 'core' )?.getEntityRecords( 'taxonomy', 'membership_cat' ),\n\t\t\tmemberships: select( 'core' )?.getEntityRecords( 'postType', 'membership' ),\n\t\t};\n\t}, [] );\n\n\tconst categoryOptions = categories?.map( ( category ) => {\n\t\treturn {\n\t\t\tvalue: category.slug,\n\t\t\tlabel: category.name,\n\t\t};\n\t} );\n\n\tconst membershipOptions = {};\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ __( 'No memberships found matching this criteria.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\tcategoryOptions?.unshift( {\n\t\tvalue: '',\n\t\tlabel: __( '- All -', 'lifterlms' ),\n\t} );\n\n\tmemberships?.map( ( membership ) => {\n\t\tmembershipOptions[ membership.id ] = membership.title.rendered;\n\n\t\treturn membership;\n\t} );\n\n\treturn <>\n\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'Memberships Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Category', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.category }\n\t\t\t\t\t\toptions={ categoryOptions }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { category: value } ) }\n\t\t\t\t\t\thelp={ __( 'Display memberships from a specific Membership Category only.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Membership ID', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.id }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { id: value } ) }\n\t\t\t\t\t\thelp={ __( 'Display only a specific membership. Use the memberships’s post ID. If using this option, all other options are rendered irrelevant.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Order', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.order }\n\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t{ value: 'ASC', label: __( 'Ascending', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'DESC', label: __( 'Descending', 'lifterlms' ) },\n\t\t\t\t\t\t] }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { order: value } ) }\n\t\t\t\t\t\thelp={ __( 'Display memberships in ascending or descending order.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Order by', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes?.orderby }\n\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t{ value: 'id', label: __( 'ID', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'author', label: __( 'Author', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'title', label: __( 'Title', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'name', label: __( 'Name', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'date', label: __( 'Date', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'modified', label: __( 'Date modified', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'rand', label: __( 'Random', 'lifterlms' ) },\n\t\t\t\t\t\t\t{ value: 'menu_order', label: __( 'Menu Order', 'lifterlms' ) },\n\t\t\t\t\t\t] }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\torderby: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t\thelp={ __( 'Determines which field is used to order memberships in the memberships list. This block will not be displayed.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<NumberControl\n\t\t\t\t\t\tlabel={ __( 'Per Page', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.posts_per_page }\n\t\t\t\t\t\tmin={ -1 }\n\t\t\t\t\t\tmax={ 100 }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( { posts_per_page: value ?? -1 } ) }\n\t\t\t\t\t\thelp={ __( ' Determines the number of results to display. Default returns all available memberships. This block will not be displayed.', 'lifterlms' ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/my-account/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/my-account\",\n  \"title\": \"My Account\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Outputs the login, registration, dashboard, profile and reset password templates.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"layout\": {\n      \"type\": \"string\"\n    },\n    \"login_redirect\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/my-account/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome user-gear solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\">\n\t\t<Path\n\t\t\td=\"M224 0a128 128 0 1 1 0 256A128 128 0 1 1 224 0zM178.3 304h91.4c11.8 0 23.4 1.2 34.5 3.3c-2.1 18.5 7.4 35.6 21.8 44.8c-16.6 10.6-26.7 31.6-20 53.3c4 12.9 9.4 25.5 16.4 37.6s15.2 23.1 24.4 33c15.7 16.9 39.6 18.4 57.2 8.7v.9c0 9.2 2.7 18.5 7.9 26.3H29.7C13.3 512 0 498.7 0 482.3C0 383.8 79.8 304 178.3 304zM436 218.2c0-7 4.5-13.3 11.3-14.8c10.5-2.4 21.5-3.7 32.7-3.7s22.2 1.3 32.7 3.7c6.8 1.5 11.3 7.8 11.3 14.8v30.6c7.9 3.4 15.4 7.7 22.3 12.8l24.9-14.3c6.1-3.5 13.7-2.7 18.5 2.4c7.6 8.1 14.3 17.2 20.1 27.2s10.3 20.4 13.5 31c2.1 6.7-1.1 13.7-7.2 17.2l-25 14.4c.4 4 .7 8.1 .7 12.3s-.2 8.2-.7 12.3l25 14.4c6.1 3.5 9.2 10.5 7.2 17.2c-3.3 10.6-7.8 21-13.5 31s-12.5 19.1-20.1 27.2c-4.8 5.1-12.5 5.9-18.5 2.4l-24.9-14.3c-6.9 5.1-14.3 9.4-22.3 12.8l0 30.6c0 7-4.5 13.3-11.3 14.8c-10.5 2.4-21.5 3.7-32.7 3.7s-22.2-1.3-32.7-3.7c-6.8-1.5-11.3-7.8-11.3-14.8V454.8c-8-3.4-15.6-7.7-22.5-12.9l-24.7 14.3c-6.1 3.5-13.7 2.7-18.5-2.4c-7.6-8.1-14.3-17.2-20.1-27.2s-10.3-20.4-13.5-31c-2.1-6.7 1.1-13.7 7.2-17.2l24.8-14.3c-.4-4.1-.7-8.2-.7-12.4s.2-8.3 .7-12.4L343.8 325c-6.1-3.5-9.2-10.5-7.2-17.2c3.3-10.6 7.7-21 13.5-31s12.5-19.1 20.1-27.2c4.8-5.1 12.4-5.9 18.5-2.4l24.8 14.3c6.9-5.1 14.5-9.4 22.5-12.9V218.2zm92.1 133.5a48.1 48.1 0 1 0 -96.1 0 48.1 48.1 0 1 0 96.1 0z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/my-account/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tTextControl,\n\tSelectControl,\n\tDisabled, Spinner,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ __( 'Account preview not available. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'My Account Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Layout', 'lifterlms' ) }\n\t\t\t\t\t\toptions={ [\n\t\t\t\t\t\t\t{ label: __( 'Columns', 'lifterlms' ), value: 'columns' },\n\t\t\t\t\t\t\t{ label: __( 'Stacked', 'lifterlms' ), value: 'stacked' },\n\t\t\t\t\t\t] }\n\t\t\t\t\t\tvalue={ attributes.layout }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tlayout: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Login redirect URL', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.login_redirect }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tlogin_redirect: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/my-achievements/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/my-achievements\",\n  \"title\": \"My Achievements\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Outputs achievements using the achievement loop templates.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"count\": {\n      \"type\": \"integer\"\n    },\n    \"columns\": {\n      \"type\": \"integer\",\n      \"default\": 5\n    },\n    \"user_id\": {\n      \"type\": \"integer\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/my-achievements/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome trophy solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\">\n\t\t<Path\n\t\t\td=\"M400 0H176c-26.5 0-48.1 21.8-47.1 48.2c.2 5.3 .4 10.6 .7 15.8H24C10.7 64 0 74.7 0 88c0 92.6 33.5 157 78.5 200.7c44.3 43.1 98.3 64.8 138.1 75.8c23.4 6.5 39.4 26 39.4 45.6c0 20.9-17 37.9-37.9 37.9H192c-17.7 0-32 14.3-32 32s14.3 32 32 32H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H357.9C337 448 320 431 320 410.1c0-19.6 15.9-39.2 39.4-45.6c39.9-11 93.9-32.7 138.2-75.8C542.5 245 576 180.6 576 88c0-13.3-10.7-24-24-24H446.4c.3-5.2 .5-10.4 .7-15.8C448.1 21.8 426.5 0 400 0zM48.9 112h84.4c9.1 90.1 29.2 150.3 51.9 190.6c-24.9-11-50.8-26.5-73.2-48.3c-32-31.1-58-76-63-142.3zM464.1 254.3c-22.4 21.8-48.3 37.3-73.2 48.3c22.7-40.3 42.8-100.5 51.9-190.6h84.4c-5.1 66.3-31.1 111.2-63 142.3z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/my-achievements/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tDisabled,\n\t// eslint-disable-next-line @wordpress/no-unsafe-wp-apis\n\t__experimentalNumberControl as NumberControl,\n\tRangeControl,\n\tSelectControl, Spinner,\n} from '@wordpress/components';\nimport { InspectorControls, useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport apiFetch from '@wordpress/api-fetch';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useState, useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\n\tconst [ users, setUsers ] = useState( [] );\n\n\tconst userOptions = [\n\t\t{\n\t\t\tlabel: __( 'Current user', 'lifterlms' ),\n\t\t\tvalue: 0,\n\t\t},\n\t];\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ __( 'No achievements found matching your selection. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\tapiFetch( { path: '/wp/v2/users' } )\n\t\t.then( ( userData ) => {\n\t\t\tsetUsers( userData );\n\t\t} );\n\n\tusers.forEach( ( user ) => {\n\t\tuserOptions.push( {\n\t\t\tlabel: user.name,\n\t\t\tvalue: user.id,\n\t\t} );\n\t} );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'My Account Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<NumberControl\n\t\t\t\t\t\tlabel={ __( 'Count', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ __( 'Number of achievements to display. Leave empty to display all achievements for user.', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.count }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tcount: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<RangeControl\n\t\t\t\t\t\tlabel={ __( 'Columns', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ __( 'Number of columns to display.', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.columns }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tcolumns: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t\tmin={ 1 }\n\t\t\t\t\t\tmax={ 12 }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'User', 'lifterlms' ) }\n\t\t\t\t\t\thelp={ __( 'Select a user to display achievements for. Leave empty to display achievements for the current user.', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.user_id }\n\t\t\t\t\t\toptions={ userOptions }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tuser_id: value,\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/navigation-link/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/navigation-link\",\n  \"title\": \"LifterLMS Link\",\n  \"category\": \"llms-blocks\",\n  \"parent\": [\n    \"core/navigation\"\n  ],\n  \"description\": \"Add dynamic LifterLMS links to navigation menus.\",\n  \"keywords\": [\n    \"LifterLMS\",\n    \"Dashboard\",\n    \"My Courses\",\n    \"My Grades\",\n    \"My Memberships\",\n    \"My Achievements\",\n    \"My Certificates\",\n    \"Notifications\",\n    \"Edit Account\",\n    \"Redeem a Voucher\",\n    \"Order History\",\n    \"Sign In\",\n    \"Sign Out\"\n  ],\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"label\": {\n      \"type\": \"string\",\n      \"default\": \"Dashboard\"\n    },\n    \"page\": {\n      \"type\": \"string\",\n      \"default\": \"dashboard\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"typography\": {\n      \"fontSize\": true,\n      \"fontFamily\": true,\n      \"fontWeight\": true,\n      \"lineHeight\": true,\n      \"textDecoration\": true,\n      \"textTransform\": true,\n      \"letterSpacing\": true\n    },\n    \"spacing\": {\n      \"width\": true\n    }\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/navigation-link/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome arrow-up-right-from-square solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\n\t\t<Path\n\t\t\td=\"M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/navigation-link/index.jsx",
    "content": "// WordPress dependencies.\nimport { __ } from '@wordpress/i18n';\nimport { registerBlockType } from '@wordpress/blocks';\nimport { InspectorControls, RichText, useBlockProps } from '@wordpress/block-editor';\nimport { PanelBody, PanelRow, TextControl, SelectControl } from '@wordpress/components';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst links = window?.llmsNavMenuItems || [];\n\nconst linkOptions = Object.keys( links ).map( ( key ) => ( {\n\tlabel: links[ key ],\n\tvalue: key,\n} ) );\n\nconst Edit = ( { attributes, setAttributes } ) => {\n\tconst blockProps = useBlockProps();\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody\n\t\t\t\ttitle={ __( 'LifterLMS Link Settings', 'lifterlms' ) }\n\t\t\t\tclassName={ 'llms-navigation-link-settings' }\n\t\t\t>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Label', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.label ?? links?.dashboard ?? '' }\n\t\t\t\t\t\tonChange={ ( label ) => setAttributes( { label } ) }\n\t\t\t\t\t\tplaceholder={ links?.[ attributes?.page ] ?? links?.dashboard ?? 'LifterLMS' }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'URL', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.page }\n\t\t\t\t\t\toptions={ linkOptions }\n\t\t\t\t\t\tonChange={ ( value ) => setAttributes( {\n\t\t\t\t\t\t\tpage: value,\n\t\t\t\t\t\t\tlabel: links[ value ],\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div\n\t\t\t{ ...blockProps }\n\t\t>\n\t\t\t<RichText\n\t\t\t\ttagName={ 'div' }\n\t\t\t\tvalue={ attributes.label }\n\t\t\t\tonChange={ ( label ) => setAttributes( { label } ) }\n\t\t\t\tplaceholder={ links?.[ attributes?.page ] ?? links?.dashboard ?? 'LifterLMS' }\n\t\t\t/>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/pricing-table/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/pricing-table\",\n  \"title\": \"Pricing Table\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Outputs a LifterLMS Pricing table (like those found on a course or membership page) for a course or membership outside of a course or membership. Useful if you want to create custom sales pages.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"product\": {\n      \"type\": \"integer\"\n    },\n    \"postType\": {\n      \"type\": \"string\",\n      \"default\": \"course\"\n    },\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/pricing-table/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\nexport default (\n\t<SVG\n\t\tclassName=\"llms-block-icon\"\n\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\tviewBox=\"0 0 576 512\"\n\t>\n\t\t<Path d=\"M64 64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64H64zM272 192H496c8.8 0 16 7.2 16 16s-7.2 16-16 16H272c-8.8 0-16-7.2-16-16s7.2-16 16-16zM256 304c0-8.8 7.2-16 16-16H496c8.8 0 16 7.2 16 16s-7.2 16-16 16H272c-8.8 0-16-7.2-16-16zM164 152v13.9c7.5 1.2 14.6 2.9 21.1 4.7c10.7 2.8 17 13.8 14.2 24.5s-13.8 17-24.5 14.2c-11-2.9-21.6-5-31.2-5.2c-7.9-.1-16 1.8-21.5 5c-4.8 2.8-6.2 5.6-6.2 9.3c0 1.8 .1 3.5 5.3 6.7c6.3 3.8 15.5 6.7 28.3 10.5l.7 .2c11.2 3.4 25.6 7.7 37.1 15c12.9 8.1 24.3 21.3 24.6 41.6c.3 20.9-10.5 36.1-24.8 45c-7.2 4.5-15.2 7.3-23.2 9V360c0 11-9 20-20 20s-20-9-20-20V345.4c-10.3-2.2-20-5.5-28.2-8.4l0 0 0 0c-2.1-.7-4.1-1.4-6.1-2.1c-10.5-3.5-16.1-14.8-12.6-25.3s14.8-16.1 25.3-12.6c2.5 .8 4.9 1.7 7.2 2.4c13.6 4.6 24 8.1 35.1 8.5c8.6 .3 16.5-1.6 21.4-4.7c4.1-2.5 6-5.5 5.9-10.5c0-2.9-.8-5-5.9-8.2c-6.3-4-15.4-6.9-28-10.7l-1.7-.5c-10.9-3.3-24.6-7.4-35.6-14c-12.7-7.7-24.6-20.5-24.7-40.7c-.1-21.1 11.8-35.7 25.8-43.9c6.9-4.1 14.5-6.8 22.2-8.5V152c0-11 9-20 20-20s20 9 20 20z\" />\n\t</SVG>\n);\n"
  },
  {
    "path": "src/blocks/pricing-table/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport {\n\tPanelBody,\n\tPanelRow,\n\tDisabled,\n\tSelectControl,\n\tSpinner,\n} from '@wordpress/components';\nimport {\n\tInspectorControls,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\nimport {\n\tusePostOptions,\n\tPostSelect,\n} from '../../../packages/components/src/post-select';\n\nconst postTypeOptions = [\n\t{ label: __( 'Course', 'lifterlms' ), value: 'course' },\n\t{ label: __( 'Membership', 'lifterlms' ), value: 'llms_membership' },\n];\n\nconst Edit = ( props ) => {\n\tconst { attributes, setAttributes } = props;\n\tconst blockProps = useBlockProps();\n\tconst courseOptions = usePostOptions();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\tlet emptyPlaceholder = __( 'Author not found. This block will not be displayed.', 'lifterlms' );\n\n\t\tif ( ! attributes.product && courseOptions.length > 0 ) {\n\t\t\temptyPlaceholder = __( 'No course selected. Please choose a Course from the block sidebar panel.', 'lifterlms' );\n\t\t}\n\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ emptyPlaceholder }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<InspectorControls>\n\t\t\t<PanelBody title={ __( 'Pricing Table Settings', 'lifterlms' ) }>\n\t\t\t\t<PanelRow>\n\t\t\t\t\t<SelectControl\n\t\t\t\t\t\tlabel={ __( 'Post Type', 'lifterlms' ) }\n\t\t\t\t\t\tvalue={ attributes.postType }\n\t\t\t\t\t\toptions={ postTypeOptions }\n\t\t\t\t\t\tonChange={ ( postType ) => setAttributes( {\n\t\t\t\t\t\t\tpostType,\n\t\t\t\t\t\t\tproduct: '',\n\t\t\t\t\t\t} ) }\n\t\t\t\t\t/>\n\t\t\t\t</PanelRow>\n\t\t\t\t<PostSelect\n\t\t\t\t\t{ ...{\n\t\t\t\t\t\t...props,\n\t\t\t\t\t\tpostType: attributes?.postType ?? 'course',\n\t\t\t\t\t\tattribute: 'product',\n\t\t\t\t\t} }\n\t\t\t\t/>\n\t\t\t</PanelBody>\n\t\t</InspectorControls>\n\t\t<div { ...blockProps }>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/blocks/registration/block.json",
    "content": "{\n  \"$schema\": \"https://schemas.wp.org/trunk/block.json\",\n  \"apiVersion\": 2,\n  \"name\": \"llms/registration\",\n  \"title\": \"LifterLMS Register\",\n  \"category\": \"llms-blocks\",\n  \"description\": \"Displays the LifterLMS registration form. If a user is already logged in, nothing is displayed.\",\n  \"textdomain\": \"lifterlms\",\n  \"attributes\": {\n    \"llms_visibility\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_in\": {\n      \"type\": \"string\"\n    },\n    \"llms_visibility_posts\": {\n      \"type\": \"string\"\n    }\n  },\n  \"supports\": {\n    \"align\": [\n      \"wide\",\n      \"full\"\n    ]\n  },\n  \"editorScript\": \"file:./index.js\"\n}\n"
  },
  {
    "path": "src/blocks/registration/icon.jsx",
    "content": "// WordPress dependencies.\nimport { SVG, Path } from '@wordpress/primitives';\n\n// FontAwesome user-plus solid.\nconst Icon = () => (\n\t<SVG className=\"llms-block-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 512\">\n\t\t<Path d=\"M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3zM504 312V248H440c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V136c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H552v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z\"\n\t\t/>\n\t</SVG>\n);\n\nexport default Icon;\n"
  },
  {
    "path": "src/blocks/registration/index.jsx",
    "content": "// WordPress dependencies.\nimport { registerBlockType } from '@wordpress/blocks';\nimport { Disabled, Spinner } from '@wordpress/components';\nimport { useBlockProps } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport ServerSideRender from '@wordpress/server-side-render';\nimport { useMemo } from '@wordpress/element';\n\n// Internal dependencies.\nimport blockJson from './block.json';\nimport Icon from './icon.jsx';\n\nconst Edit = ( { attributes } ) => {\n\tconst blockProps = useBlockProps();\n\n\tconst memoizedServerSideRender = useMemo( () => {\n\t\treturn <ServerSideRender\n\t\t\tblock={ blockJson.name }\n\t\t\tattributes={ attributes }\n\t\t\tLoadingResponsePlaceholder={ () =>\n\t\t\t\t<Spinner />\n\t\t\t}\n\t\t\tErrorResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-error' }>{ __( 'Error loading content. Please check block settings are valid. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t\tEmptyResponsePlaceholder={ () =>\n\t\t\t\t<p className={ 'llms-block-empty' }>{ __( 'Registration form preview not available. This block will not be displayed.', 'lifterlms' ) }</p>\n\t\t\t}\n\t\t/>;\n\t}, [ attributes ] );\n\n\treturn <>\n\t\t<div\n\t\t\t{ ...blockProps }\n\t\t>\n\t\t\t<Disabled>\n\t\t\t\t{ memoizedServerSideRender }\n\t\t\t</Disabled>\n\t\t</div>\n\t</>;\n};\n\nregisterBlockType( blockJson, {\n\ticon: Icon,\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/js/.eslintrc.js",
    "content": "module.exports = {\n\trules: {\n\t\t// This conflicts with PHPCS sniff for the fileheader comment: @parckage in JSDoc should be empty.\n\t\t'jsdoc/empty-tags': 'off',\n\t},\n};\n"
  },
  {
    "path": "src/js/admin-addons.js",
    "content": "/**\n * UI & UX for the Admin add-ons management screen\n *\n * @package LifterLMS/Scripts/Admin\n *\n * @since 3.22.0\n * @version 5.5.0\n */\n\nimport { _n, sprintf } from '@wordpress/i18n';\nimport $ from 'jquery';\nimport '../scss/admin-addons.scss';\n\n( function() {\n\t/**\n\t * Tracks current # of each bulk action to be run upon form submission\n\t *\n\t * @type {Object}\n\t */\n\tconst actions = {\n\t\tupdate: 0,\n\t\tinstall: 0,\n\t\tactivate: 0,\n\t\tdeactivate: 0,\n\t};\n\n\t/**\n\t * When the bulk action modal is closed, clear all existing staged actions\n\t *\n\t * @since 3.22.0\n\t */\n\t$( '.llms-bulk-close' ).on( 'click', function( e ) {\n\t\te.preventDefault();\n\t\t$( 'input.llms-bulk-check' ).filter( ':checked' ).prop( 'checked', false ).trigger( 'change' );\n\t} );\n\n\t/**\n\t * Update the UI and counters when a checkbox action is changed\n\t *\n\t * @since 3.22.0\n\t */\n\t$( 'input.llms-bulk-check' ).on( 'change', function() {\n\t\tconst action = $( this ).attr( 'data-action' );\n\n\t\tif ( $( this ).is( ':checked' ) ) {\n\t\t\tactions[ action ]++;\n\t\t} else {\n\t\t\tactions[ action ]--;\n\t\t}\n\n\t\tupdateUserInterface();\n\t} );\n\n\t/**\n\t * Updates the UI when bulk actions are changed.\n\t *\n\t * Shows # of each action to be applied & shows the form submission / cancel buttons\n\t *\n\t * @since 3.22.0\n\t * @since 5.5.0 Use `wp.i18n` functions in favor of `LLMS.l10n` and use `$.text()` in favor of `$.html()`.\n\t *               Renamed from `update_ui()` to match coding standards.\n\t *\n\t * @return {void}\n\t */\n\tfunction updateUserInterface() {\n\t\tconst $el = $( '#llms-addons-bulk-actions' );\n\t\tif ( actions.update || actions.install || actions.activate || actions.deactivate ) {\n\t\t\t$el.addClass( 'active' );\n\t\t} else {\n\t\t\t$el.removeClass( 'active' );\n\t\t}\n\n\t\t$.each( actions, function( key, count ) {\n\t\t\tconst $desc = $el.find( '.llms-bulk-desc.' + key );\n\n\t\t\tlet text = '';\n\n\t\t\tif ( count ) {\n\t\t\t\t// Translators: %d = Number of add-ons to perform the specified action against.\n\t\t\t\ttext = sprintf( _n( '%d add-on', '%d add-ons', count, 'lifterlms' ), count );\n\t\t\t\t$desc.show();\n\t\t\t} else {\n\t\t\t\t$desc.hide();\n\t\t\t}\n\t\t\t$desc.find( 'span' ).text( text );\n\t\t} );\n\t}\n\n\t/**\n\t * Show the keys management dropdown on click of the \"My License Keys\" button\n\t *\n\t * @since 3.22.0\n\t */\n\t$( '#llms-active-keys-toggle' ).on( 'click', function() {\n\t\t$( '#llms-key-field-form' ).toggle();\n\t} );\n}() );\n"
  },
  {
    "path": "src/js/admin-award-certificate.js",
    "content": "// WP deps.\nimport { render } from '@wordpress/element';\nimport { getQueryArg } from '@wordpress/url';\n\n// Internal deps.\nimport { AwardCertificateButton } from './util';\n\nconst WRAPPER_ID = 'llms-award-certificate-wrapper',\n\trenderNodeStyles = {},\n\tbuttonArgs = {},\n\tdefaultBtn = getDefaultButton();\n\n/**\n * Retrieves the DOM Element selector for the default button to be replaced by the component.\n *\n * @since 6.0.0\n *\n * @return {?Element} DOM element for the default button or null if no button found.\n */\nfunction getDefaultButton() {\n\t// Certificates post table page.\n\tlet btn = document.querySelector( '.page-title-action' );\n\tif ( btn ) {\n\t\trenderNodeStyles.top = '-3px';\n\t\trenderNodeStyles.position = 'relative';\n\t}\n\n\t// Student certificates reporting page.\n\tif ( ! btn ) {\n\t\tbtn = document.getElementById( 'llms-new-award-button' );\n\n\t\trenderNodeStyles.marginBottom = '20px';\n\t\trenderNodeStyles.display = 'inline-block';\n\n\t\tbuttonArgs.studentId = parseInt( getQueryArg( window.location, 'student_id' ) );\n\t\tbuttonArgs.selectStudent = false;\n\t}\n\n\t// Reuse the original buttons label.\n\tif ( btn ) {\n\t\tbuttonArgs.buttonLabel = btn.textContent;\n\t}\n\n\treturn btn;\n}\n\n/**\n * Inserts the wrapper element node where the new component button will be rendered.\n *\n * @since 6.0.0\n *\n * @return {boolean} Returns `false` if no default button is found, otherwise returns true.\n */\nfunction insertRenderNode() {\n\tif ( ! defaultBtn ) {\n\t\treturn false;\n\t}\n\n\tconst renderNode = document.createElement( 'span' );\n\trenderNode.id = WRAPPER_ID;\n\n\tObject.entries( renderNodeStyles ).forEach( ( [ rule, style ] ) => {\n\t\trenderNode.style[ rule ] = style;\n\t} );\n\n\tdefaultBtn.style.display = 'none';\n\tdefaultBtn.after( renderNode );\n\n\treturn true;\n}\n\n// Render the component.\nif ( insertRenderNode() ) {\n\trender( <AwardCertificateButton { ...buttonArgs } />, document.getElementById( WRAPPER_ID ) );\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/document-settings.js",
    "content": "import { __ } from '@wordpress/i18n';\nimport { compose } from '@wordpress/compose';\nimport { withSelect } from '@wordpress/data';\nimport { PluginDocumentSettingPanel } from '@wordpress/edit-post';\nimport { store as editorStore } from '@wordpress/editor';\n\nimport BackgroundControl from './plugin/background-control';\nimport MarginsControl from './plugin/margins-control';\nimport OrientationControl from './plugin/orientation-control';\nimport SequentialIdControl from './plugin/sequential-id-control';\nimport SizeControl from './plugin/size-control';\nimport TitleControl, { Check as TitleControlCheck } from './plugin/title-control';\n\n/**\n * Render the certificate settings editor panel.\n *\n * @since 6.0.0\n *\n * @param {Object}   args              Component arguments.\n * @param {string}   args.type         Current post type.\n * @param {string}   args.title        Current certificate title.\n * @param {string}   args.sequentialId Next certificate sequential ID.\n * @param {string}   args.background   Current background color setting.\n * @param {number}   args.height       Current height setting.\n * @param {number[]} args.margins      Current margins setting.\n * @param {string}   args.orientation  Current orientation setting.\n * @param {string}   args.size         Current size setting.\n * @param {string}   args.unit         Current unit setting.\n * @param {number}   args.width        Current wfidth setting.\n * @return {PluginDocumentSettingPanel} The component.\n */\nfunction CertificateDocumentSettings( { type, title, sequentialId, background, height, margins, orientation, size, unit, width } ) {\n\treturn (\n\n\t\t<PluginDocumentSettingPanel\n\t\t\tclassName=\"llms-certificate-doc-settings\"\n\t\t\tname=\"llms-certificate-doc-settings\"\n\t\t\ttitle={ __( 'Settings', 'lifterlms' ) }\n\t\t\topened={ true }\n\t\t>\n\n\t\t\t<TitleControlCheck>\n\t\t\t\t<TitleControl { ...{ title } } />\n\t\t\t\t<br />\n\t\t\t</TitleControlCheck>\n\t\t\t<SizeControl { ...{ size, width, height, unit } } />\n\t\t\t<br />\n\t\t\t<OrientationControl { ...{ orientation } } />\n\t\t\t<br />\n\t\t\t<MarginsControl { ...{ margins, unit } } />\n\t\t\t<br />\n\t\t\t<BackgroundControl { ...{ background } } />\n\t\t\t{ 'llms_certificate' === type && (\n\t\t\t\t<>\n\t\t\t\t\t<br />\n\t\t\t\t\t<SequentialIdControl { ...{ sequentialId } } />\n\t\t\t\t</>\n\t\t\t) }\n\n\t\t</PluginDocumentSettingPanel>\n\n\t);\n}\n\nconst applyWithSelect = withSelect( ( select ) => {\n\tconst { getEditedPostAttribute } = select( editorStore );\n\n\treturn {\n\t\ttype: getEditedPostAttribute( 'type' ),\n\t\ttitle: getEditedPostAttribute( 'certificate_title' ),\n\t\tsequentialId: getEditedPostAttribute( 'certificate_sequential_id' ),\n\t\tbackground: getEditedPostAttribute( 'certificate_background' ),\n\t\theight: getEditedPostAttribute( 'certificate_height' ),\n\t\tmargins: getEditedPostAttribute( 'certificate_margins' ),\n\t\torientation: getEditedPostAttribute( 'certificate_orientation' ),\n\t\tsize: getEditedPostAttribute( 'certificate_size' ),\n\t\tunit: getEditedPostAttribute( 'certificate_unit' ),\n\t\twidth: getEditedPostAttribute( 'certificate_width' ),\n\t};\n} );\n\nexport default compose( [ applyWithSelect ] )(\n\tCertificateDocumentSettings\n);\n"
  },
  {
    "path": "src/js/admin-certificate-editor/edit-certificate.js",
    "content": "import { dispatch } from '@wordpress/data';\nimport { store as editorStore } from '@wordpress/editor';\n\n/**\n * Make changes to a custom field of a certificate post.\n *\n * @since 6.0.0\n *\n * @param {string} key Unprefixed field key. For example, to edit \"certificate_size\", pass \"size\".\n * @param {*}      val Field value.\n * @return {void}\n */\nexport default function editCertificate( key, val ) {\n\tconst { editPost } = dispatch( editorStore ),\n\t\tedits = {};\n\tedits[ `certificate_${ key }` ] = val;\n\teditPost( edits );\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/editor.js",
    "content": "import { store as coreStore } from '@wordpress/core-data';\nimport { dispatch, select, subscribe } from '@wordpress/data';\nimport { rawHandler } from '@wordpress/blocks';\nimport domReady from '@wordpress/dom-ready';\nimport { store as editorStore } from '@wordpress/editor';\nimport { store as blockEditorStore } from '@wordpress/block-editor';\n\n/**\n * Flag used by maybeRefreshContent() to determine of the current post has been saved.\n *\n * @type {boolean}\n */\nlet hasSaved = false;\n\n/**\n * Retrieves the current media object for the certificate's featured image.\n *\n * @since 6.0.0\n *\n * @return {Object} The featured image object or an empty object if no featured image is set.\n */\nfunction getFeaturedMedia() {\n\tconst { getEditedPostAttribute } = select( editorStore ),\n\t\t{ getMedia } = select( coreStore ),\n\t\timageId = getEditedPostAttribute( 'featured_media' );\n\n\treturn imageId ? getMedia( imageId ) : {};\n}\n\n/**\n * Retrieve the source url for the certificate background image.\n *\n * Utilizes the current featured image if set otherwise falls back to the global\n * default certificate background image.\n *\n * @since 6.0.0\n *\n * @return {string} The background image source url.\n */\nfunction getBackgroundImage() {\n\tconst mediaRes = getFeaturedMedia(),\n\t\t{ default_image: defaultSrc } = window.llms.certificates;\n\n\t// Wait until the API is ready.\n\tif ( undefined === mediaRes ) {\n\t\treturn null;\n\t}\n\n\tconst { source_url: src } = mediaRes;\n\n\treturn src ? src : defaultSrc;\n}\n\n/**\n * Add inline styles to fix visual issues for certificate building.\n *\n * Forces blocks to take up the actual full-width of the certificate \"canvas\" and\n * removes margins and spacing between blocks so that what is displayed in the editor\n * more closely resembles what will be displayed on the frontend.\n *\n * @since 6.0.0\n *\n * @return {void}\n */\nfunction applyBlockVisualFixes() {\n\tconst style = document.createElement( 'style' );\n\tstyle.type = 'text/css';\n\t// Force editor style to show blocks as full width.\n\tstyle.appendChild( document.createTextNode( '.editor-styles-wrapper .wp-block { max-width: 100% !important; }' ) );\n\t// Force editor block spacing to more closely resemble rendering on the frontend.\n\tstyle.appendChild( document.createTextNode( '.editor-styles-wrapper [data-block], .wp-block { margin-top: 0 !important; margin-bottom: 0 !important }' ) );\n\tdocument.head.appendChild( style );\n}\n\n/**\n * Determines whether or not the current post has a certificate title block.\n *\n * @since 6.0.0\n *\n * @see {@link https://github.com/WordPress/gutenberg/issues/37540}\n *\n * @return {boolean} Returns `true` if the post has the block and `false` if it doesn't.\n */\nfunction hasCertificateTitle() {\n\tconst { getInserterItems } = select( blockEditorStore );\n\n\t// Using this method in favor of `canInsertBlockType()` due to this: https://github.com/WordPress/gutenberg/issues/37540.\n\tconst { isDisabled } = getInserterItems().find( ( { name } ) => 'llms/certificate-title' === name );\n\treturn isDisabled;\n}\n\n/**\n * Sync saved content with displayed content.\n *\n * We process merge codes, shortcodes and reusable blocks server-side when a published `llms_my_certificate` is updated.\n * When the content is returned from the server, it is not updated in the block editor (it's assumed that\n * the content doesn't change).\n *\n * This function waits until a save has been processed and then determines if the editor's content should be updated\n * by looking for merge codes, shortcodes or reusable blocks in the editor's content. If any are found in the editor's\n * content and none are found in the post's content as returned from the server it will update the editor's\n * content with the content returned from the server.\n *\n * @since 6.0.0\n * @since 6.4.0 Added refresh if the edited content contains a WordPress reusable block.\n *\n * @see {@link https://github.com/WordPress/gutenberg/issues/26763}\n *\n * @param {string} content       Post content as returned from the server.\n * @param {string} editedContent Post content currently found in the editor.\n * @return {void}\n */\nfunction maybeRefreshContent( content, editedContent ) {\n\tconst { isSavingPost } = select( editorStore ),\n\t\tisSaving = isSavingPost();\n\n\t// @see {@link https://github.com/WordPress/gutenberg/issues/17632#issuecomment-583772895}\n\tif ( isSaving ) {\n\t\thasSaved = true;\n\t} else if ( ! isSaving && hasSaved ) {\n\t\thasSaved = false;\n\n\t\tconst REGEX = /(\\{[A-Za-z_].*\\})|(\\[llms-user .+]|(<!-- wp:block .+? \\/-->))/g,\n\t\t\tactualMatch = content.match( REGEX ),\n\t\t\teditedMatch = editedContent.match( REGEX );\n\n\t\tif ( editedMatch?.length && ! actualMatch?.length ) {\n\t\t\trefreshContent( content );\n\t\t}\n\t}\n}\n\n/**\n * Replace the content in the editor with the specified content.\n *\n * @since 6.0.0\n *\n * @param {string} content HTML/Block markup string.\n * @return {void}\n */\nfunction refreshContent( content ) {\n\tconst { replaceBlocks } = dispatch( blockEditorStore ),\n\t\t{ savePost } = dispatch( editorStore ),\n\t\t{ getBlocks } = select( blockEditorStore );\n\n\treplaceBlocks(\n\t\tgetBlocks().map( ( { clientId } ) => clientId ),\n\t\trawHandler( { HTML: content } )\n\t);\n\n\tsavePost();\n}\n\n/**\n * Updates to the the editor \"canvas\" to reflect certificate settings.\n *\n * Sets the width, margins, background image, color, etc...\n *\n * @since 6.0.0\n *\n * @return {void}\n */\nfunction updateDOM() {\n\tconst { getCurrentPostAttribute, getEditedPostAttribute, getCurrentPostType } = select( editorStore ),\n\t\tbg = getEditedPostAttribute( 'certificate_background' ),\n\t\tmargins = getEditedPostAttribute( 'certificate_margins' ),\n\t\twidth = getEditedPostAttribute( 'certificate_width' ),\n\t\theight = getEditedPostAttribute( 'certificate_height' ),\n\t\tunit = getEditedPostAttribute( 'certificate_unit' ),\n\t\torientation = getEditedPostAttribute( 'certificate_orientation' ),\n\t\tcontent = getCurrentPostAttribute( 'content' ),\n\t\teditedContent = getEditedPostAttribute( 'content' );\n\n\tconst list = document.querySelector( '.block-editor-block-list__layout.is-root-container' );\n\tif ( list ) {\n\t\tconst displayWidth = 'portrait' === orientation ? width : height,\n\t\t\tdisplayHeight = 'portrait' === orientation ? height : width,\n\t\t\tpadding = margins.map( ( margin ) => `${ margin }%` ).join( ' ' );\n\n\t\tlist.style.backgroundImage = `url( '${ getBackgroundImage() }' )`;\n\t\tlist.style.backgroundSize = `${ displayWidth }${ unit } ${ displayHeight }${ unit }`;\n\t\tlist.style.backgroundRepeat = 'no-repeat';\n\t\tlist.style.marginLeft = 'auto';\n\t\tlist.style.marginRight = 'auto';\n\t\tlist.style.padding = padding;\n\t\tlist.style.width = `${ displayWidth }${ unit }`;\n\t\tlist.style.minHeight = `${ displayHeight }${ unit }`;\n\t\tlist.style.boxSizing = 'border-box';\n\t}\n\n\tconst styles = document.querySelector( '.editor-styles-wrapper' );\n\tif ( styles ) {\n\t\tstyles.style.backgroundColor = bg;\n\t}\n\n\tif ( 'llms_my_certificate' === getCurrentPostType() ) {\n\t\tmaybeRefreshContent( content, editedContent );\n\n\t\t// Visually hide the post title element based on the presence of the cert title block.\n\t\tconst title = document.querySelector( '.edit-post-visual-editor__post-title-wrapper' );\n\t\tif ( title ) {\n\t\t\ttitle.style.display = hasCertificateTitle() ? 'none' : 'initial';\n\t\t}\n\t}\n}\n\ndomReady( () => {\n\tapplyBlockVisualFixes();\n\n\tsubscribe( updateDOM );\n} );\n"
  },
  {
    "path": "src/js/admin-certificate-editor/i18n.js",
    "content": "import { addFilter } from '@wordpress/hooks';\nimport { __ } from '@wordpress/i18n';\nimport { subscribe, select } from '@wordpress/data';\nimport { store as editorStore } from '@wordpress/editor';\n\n/**\n * Wait for the current post type to load and unsubscribe as soon as we have a post type.\n *\n * This subscription \"translates\" the \"Move to trash\" button to \"Delete permanently\".\n *\n * @since 6.0.0\n */\nconst unsubscribe = subscribe( () => {\n\tconst { getCurrentPostType } = select( editorStore ),\n\t\tpostType = getCurrentPostType();\n\n\tif ( null !== postType ) {\n\t\tdoUnsubscribe( 'llms_my_certificate' === postType );\n\t}\n} );\n\n/**\n * Performs unsubscribe on the subscription and optionally applies the desired translation.\n *\n * @since 6.0.0\n *\n * @param {boolean} withFilter Whether or not the addFilter call should be applied.\n * @return {void}\n */\nfunction doUnsubscribe( withFilter ) {\n\tunsubscribe();\n\n\tif ( ! withFilter ) {\n\t\treturn;\n\t}\n\n\taddFilter( 'i18n.gettext_default', 'llms/certificates', function( text ) {\n\t\tif ( 'Move to trash' === text ) {\n\t\t\treturn __( 'Delete permanently', 'lifterlms' );\n\t\t}\n\n\t\treturn text;\n\t} );\n}\n\n"
  },
  {
    "path": "src/js/admin-certificate-editor/index.js",
    "content": "// WP deps.\nimport { registerPlugin } from '@wordpress/plugins';\n\n// Internal deps.\nimport './editor';\nimport './i18n';\nimport './merge-codes';\nimport './migrate';\nimport './modify-blocks';\nimport './notices';\nimport CertificateDocumentSettings from './document-settings';\nimport CertificatePostStatusInfo from './post-status-info';\nimport CertificateUserSettings from './user-settings';\n\n/**\n * Register the document settings plugin with the block editor.\n *\n * @since 6.0.0\n */\nregisterPlugin(\n\t'llms-certificate-doc-settings',\n\t{\n\t\trender: CertificateDocumentSettings,\n\t\ticon: '',\n\t}\n);\n\n/**\n * Registers the awarded certificate user selection / display control.\n *\n * @since 6.0.0\n */\nregisterPlugin(\n\t'llms-certificate-user',\n\t{\n\t\trender: CertificateUserSettings,\n\t}\n);\n\n/**\n * Registers the certificate default template reset button.\n *\n * @since 6.0.0\n */\nregisterPlugin(\n\t'llms-certificate-post-status-info',\n\t{\n\t\trender: CertificatePostStatusInfo,\n\t}\n);\n"
  },
  {
    "path": "src/js/admin-certificate-editor/merge-codes.js",
    "content": "// WP deps.\nimport { __ } from '@wordpress/i18n';\nimport { RichTextToolbarButton } from '@wordpress/block-editor';\nimport { Button, Modal } from '@wordpress/components';\nimport { registerFormatType, insert } from '@wordpress/rich-text';\nimport { useState } from '@wordpress/element';\n\nimport { CopyButton } from '@lifterlms/components';\nimport { Icon, lifterlms } from '@lifterlms/icons';\n\n/**\n * Displays a single <tr> for a merge code.\n *\n * @since 6.0.0\n *\n * @param {Object}   args            Component arguments.\n * @param {Function} args.closeModal Function to close the modal.\n * @param {Function} args.onChange   RichText change callback function, used to insert\n *                                   the merge code into the current RichText area in the editor.\n * @param {Object}   args.value      Current value object of the RichText element.\n * @return {WPElement} The table row component.\n */\nfunction MergeCodeTableRow( { closeModal, onChange, value } ) {\n\tconst { merge_codes: list } = window.llms.certificates;\n\n\treturn Object.entries( list ).map( ( [ code, name ], index ) => {\n\t\treturn (\n\t\t\t<tr key={ index }>\n\t\t\t\t<td style={ { textAlign: 'left' } }>{ name }</td>\n\t\t\t\t<td>\n\t\t\t\t\t<CopyButton\n\t\t\t\t\t\tbuttonText={ code }\n\t\t\t\t\t\tcopyText={ code }\n\t\t\t\t\t\tonCopy={ closeModal }\n\t\t\t\t\t\tisLink\n\t\t\t\t\t/>\n\t\t\t\t</td>\n\t\t\t\t<td>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tisSecondary\n\t\t\t\t\t\tisSmall\n\t\t\t\t\t\tonClick={ () => {\n\t\t\t\t\t\t\tcloseModal();\n\t\t\t\t\t\t\tonChange( insert( value, code ) );\n\t\t\t\t\t\t} }\n\t\t\t\t\t>\n\t\t\t\t\t\t{ __( 'Insert', 'lifterlms' ) }\n\t\t\t\t\t</Button>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t);\n\t} );\n}\n\n/**\n * RichText format edit component.\n *\n * @since 6.0.0\n *\n * @param {Object} props Component properties.\n * @return {WPElement} The component.\n */\nfunction Edit( props ) {\n\tconst [ isOpen, setOpen ] = useState( false ),\n\t\topenModal = () => setOpen( true ),\n\t\tcloseModal = () => setOpen( false ),\n\t\t{ value, onChange } = props;\n\n\treturn (\n\t\t<>\n\t\t\t<RichTextToolbarButton\n\t\t\t\ticon={ <Icon icon={ lifterlms } /> }\n\t\t\t\ttitle={ __( 'Merge Codes', 'lifterlms' ) }\n\t\t\t\tonClick={ openModal }\n\t\t\t/>\n\n\t\t\t{ isOpen && (\n\t\t\t\t<Modal\n\t\t\t\t\tclassName=\"llms-certificate-merge-codes-modal\"\n\t\t\t\t\ttitle={ __(\n\t\t\t\t\t\t'LifterLMS Certificate Merge Codes',\n\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t) }\n\t\t\t\t\tonRequestClose={ closeModal }\n\t\t\t\t>\n\t\t\t\t\t<div className=\"llms-certificate-merge-codes-modal--main\">\n\t\t\t\t\t\t<table className=\"llms-table zebra\" style={ { width: '480px' } }>\n\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t<th style={ { textAlign: 'left' } }>{ __( 'Name', 'lifterlms' ) }</th>\n\t\t\t\t\t\t\t\t\t<th>{ __( 'Merge code', 'lifterlms' ) }</th>\n\t\t\t\t\t\t\t\t\t<th>{ __( 'Insert', 'lifterlms' ) }</th>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t<MergeCodeTableRow { ...{ closeModal, onChange, value } } />\n\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</div>\n\t\t\t\t</Modal>\n\t\t\t) }\n\t\t</>\n\t);\n}\n\n/**\n * Register a RichText format with the block editor.\n *\n * @since 6.0.0\n */\nregisterFormatType( 'llms/certificate-merge-codes', {\n\ttitle: __( 'LifterLMS Certificate Merge Codes', 'lifterlms' ),\n\ttagName: 'span',\n\tclassName: 'llms-cert-mc-wrap',\n\tedit: Edit,\n} );\n"
  },
  {
    "path": "src/js/admin-certificate-editor/migrate.js",
    "content": "import { subscribe, select, dispatch } from '@wordpress/data';\nimport { rawHandler, serialize } from '@wordpress/blocks';\nimport { store as editorStore } from '@wordpress/editor';\nimport { store as blockEditorStore } from '@wordpress/block-editor';\n\n/**\n * Wait for the current post type to load and unsubscribe as soon as we have a post type.\n *\n * This subscription \"translates\" the \"Move to trash\" button to \"Delete permanently\".\n *\n * @since 6.0.0\n */\nconst unsubscribe = subscribe( () => {\n\tconst search = new URLSearchParams( window.location.search ),\n\t\tdoMigration = 1 === parseInt( search.get( 'llms-migrate-legacy-certificate' ) );\n\n\tif ( ! doMigration ) {\n\t\treturn doUnsubscribe( false );\n\t}\n\n\tconst blocks = getAllBlocks();\n\n\tif ( 0 !== blocks.length ) {\n\t\tdoUnsubscribe( true );\n\t}\n} );\n\n/**\n * Performs unsubscribe on the subscription and optionally migrates classic editor blocks.\n *\n * @since 6.0.0\n *\n * @param {boolean} withMigration Whether or not to perform the classic editor migration.\n * @return {void}\n */\nfunction doUnsubscribe( withMigration ) {\n\tunsubscribe();\n\n\tif ( ! withMigration ) {\n\t\treturn;\n\t}\n\n\tmigrateClassicBlock();\n}\n\n/**\n * Helper to retrieve a list of all blocks.\n *\n * @since 6.0.0\n *\n * @return {WPBlock[]} Array of blocks.\n */\nfunction getAllBlocks() {\n\tconst { getBlocks } = select( blockEditorStore );\n\treturn getBlocks();\n}\n\n/**\n * Performs a migration on all classic editor blocks.\n *\n * This performs logic largely similar to the classic blocks \"Convert to Blocks\"\n * button. Also forces a post update after migrating.\n *\n * @since 6.0.0\n *\n * @see {@link https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/freeform/convert-to-blocks-button.js}\n *\n * @return {void}\n */\nfunction migrateClassicBlock() {\n\tconst classics = getAllBlocks().filter( ( { name } ) => 'core/freeform' === name );\n\n\tif ( 0 === classics.length ) {\n\t\treturn;\n\t}\n\n\tconst { replaceBlocks } = dispatch( blockEditorStore ),\n\t\t{ savePost } = dispatch( editorStore );\n\n\tclassics.forEach( ( block ) => {\n\t\treplaceBlocks(\n\t\t\tblock.clientId,\n\t\t\trawHandler( { HTML: serialize( block ) } )\n\t\t);\n\t} );\n\n\tsavePost();\n}\n\n"
  },
  {
    "path": "src/js/admin-certificate-editor/modify-blocks.js",
    "content": "// WP Deps.\nimport { addFilter } from '@wordpress/hooks';\n\n/**\n * Modifies the registration of the core/columns block.\n *\n * I cannot find a way to disable the toggle in the block's inspector panel, nor\n * can I determine the proper way to simply define the block's default attribute value\n * as `false`. By setting the variations to all have the default value it will\n * ensure that any new columns added to a certificate will have mobile stacking disabled.\n * Users will still be able to enable this via the admin UI but since there's no way\n * to disable the toggle we'll have to accept that. Realistically it won't have much impact\n * anyway but it would be good to be able to disable it.\n *\n * @since 6.0.0\n *\n * @see {@link https://github.com/gocodebox/lifterlms/issues/1972}\n *\n * @param {Object} settings  Block registration settings.\n * @param {string} blockName The block's name.\n * @return {Object} Block registration settings.\n */\nfunction modifyColumnsBlock( settings, blockName ) {\n\tif ( 'core/columns' === blockName ) {\n\t\t// Force all the existing columns block variation to have mobile stacking disabled by default.\n\t\tsettings.variations = settings.variations.map( ( variation ) => {\n\t\t\tconst { attributes = {} } = variation;\n\t\t\tvariation.attributes = {\n\t\t\t\t...attributes,\n\t\t\t\tisStackedOnMobile: false,\n\t\t\t};\n\t\t\treturn variation;\n\t\t} );\n\t}\n\n\treturn settings;\n}\n\naddFilter(\n\t'blocks.registerBlockType',\n\t'llms/certificate-editor/columns-block',\n\tmodifyColumnsBlock,\n);\n"
  },
  {
    "path": "src/js/admin-certificate-editor/notices.js",
    "content": "import { __ } from '@wordpress/i18n';\nimport { dispatch } from '@wordpress/data';\nimport domReady from '@wordpress/dom-ready';\nimport { store as noticeStore } from '@wordpress/notices';\nimport { getQueryArg } from '@wordpress/url';\n\ndomReady( () => {\n\tif ( '1' !== getQueryArg( window.location.href, 'newAwardMsg' ) ) {\n\t\treturn;\n\t}\n\n\tconst { createSuccessNotice } = dispatch( noticeStore );\n\n\tcreateSuccessNotice( __( 'The certificate award has been created as a draft.', 'lifterlms' ) );\n} );\n"
  },
  {
    "path": "src/js/admin-certificate-editor/plugin/background-control.js",
    "content": "import { useSetting } from '@wordpress/block-editor';\nimport { BaseControl, ColorPalette } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\nimport { __ } from '@wordpress/i18n';\n\nimport editCertificate from '../edit-certificate';\n\n/**\n * Retrieves a color palette for use in the background control component.\n *\n * Attempts to use the theme's color palette (if available) and falls back\n * to the color palette provided by the LifterLMS plugin.\n *\n * Additionally converts all hexcodes to lowercase to enforce consistency\n * across themes which may store hexcodes in upper or lower case.\n *\n * @see {@link https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/#block-color-palettes}\n *\n * @since 6.0.0\n *\n * @return {Object[]} Array of color palette objects.\n */\nfunction usePalette() {\n\tlet palette = useSetting( 'color.palette' );\n\n\t// Use default LifterLMS colors if there's none specified by the theme.\n\tif ( ! palette.length ) {\n\t\tpalette = window.llms.certificates.colors;\n\t}\n\n\treturn palette.map( ( item ) => {\n\t\tconst { color } = item;\n\t\treturn {\n\t\t\t...item,\n\t\t\tcolor: color.startsWith( '#' ) ? color.toLowerCase() : color,\n\t\t};\n\t} );\n}\n\n/**\n * Certificate background color control.\n *\n * @since 6.0.0\n *\n * @param {Object} args            Function arguments object.\n * @param {string} args.background Value of the background color.\n * @return {BaseControl} The background control component.\n */\nexport default function BackgroundControl( { background } ) {\n\tconst [ color, setColor ] = useState( background );\n\treturn (\n\t\t<BaseControl\n\t\t\tlabel={ __( 'Background Color', 'lifterlms' ) }\n\t\t\tid=\"llms-certificate-control--background-color\"\n\t\t>\n\t\t\t<ColorPalette\n\t\t\t\tcolors={ usePalette() }\n\t\t\t\tonChange={ ( val ) => {\n\t\t\t\t\tsetColor( val );\n\t\t\t\t\teditCertificate( 'background', val );\n\t\t\t\t} }\n\t\t\t\tvalue={ color }\n\t\t\t\tclearable={ false }\n\t\t\t/>\n\t\t</BaseControl>\n\t);\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/plugin/margins-control.js",
    "content": "// External dependencies.\nimport styled from '@emotion/styled';\n\n// WordPress dependencies.\nimport { BaseControl, TextControl } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\nimport { __ } from '@wordpress/i18n';\n\n// Internal dependencies.\nimport editCertificate from '../edit-certificate';\n\n/**\n * Retrieve a description for the margin based on it's index in the margins array.\n *\n * @since 6.0.0\n *\n * @param {number} index Index of the margin.\n * @return {string} Margin description.\n */\nfunction getDesc( index ) {\n\tconst vals = [\n\t\t__( 'Top', 'lifterlms' ),\n\t\t__( 'Right', 'lifterlms' ),\n\t\t__( 'Bottom', 'lifterlms' ),\n\t\t__( 'Left', 'lifterlms' ),\n\t];\n\n\treturn vals[ index ];\n}\n\n/**\n * Styled Text Control\n *\n * @since 6.0.0\n */\nconst StyledTextControl = styled( TextControl )`\n\t& .components-base-control__field {\n\t\tposition: relative;\n\n\t\t&:hover:after,\n\t\t&:focus-within:after {\n\t\t    right: 25px;\n\t\t}\n\n\t\t&:after {\n\t\t\tcontent: '%';\n\t\t\tfont-size: 85%;\n\t\t\tpointer-events: none;\n\t\t\tposition: absolute;\n\t\t\tright: 6px;\n\t\t\ttop: 6px;\n\t\t\ttransition: right 0.05s ease-in-out;\n\t\t}\n\t}\n`;\n\n/**\n * Single margin control component.\n *\n * @since 6.0.0\n *\n * @param {Object}   args             Component arguments.\n * @param {number}   args.margin      Current value of the margin.\n * @param {number}   args.index       Index of the margin.\n * @param {Function} args.editMargins Function used to update the margins.\n * @return {WPElement} Component.\n */\nfunction MarginControl( { margin, index, editMargins } ) {\n\tconst [ currMargin, setMargin ] = useState( margin ),\n\t\tmarginId = [ 'top', 'right', 'bottom', 'left' ][ index ];\n\n\treturn (\n\t\t<div style={ { flex: 1 } }>\n\t\t\t<StyledTextControl\n\t\t\t\tid={ `llms-certificate-control--margin--${ marginId }` }\n\t\t\t\tvalue={ currMargin }\n\t\t\t\ttype=\"number\"\n\t\t\t\tonChange={ ( val ) => {\n\t\t\t\t\teditMargins( val, index, setMargin );\n\t\t\t\t} }\n\t\t\t/>\n\t\t\t<em style={ { display: 'block', marginLeft: '4px', marginTop: '-8px' } }>{ getDesc( index ) }</em>\n\t\t</div>\n\t);\n}\n\n/**\n * Certificate margins control.\n *\n * @since 6.0.0\n *\n * @param {Object}   args         Function arguments object.\n * @param {number[]} args.margins Array of numbers representing the certificate's margins.\n * @return {BaseControl} The background control component.\n */\nexport default function MarginsControl( { margins } ) {\n\tconst editMargins = ( val, index, setState ) => {\n\t\tconst newMargins = [ ...margins ];\n\t\tnewMargins[ index ] = val;\n\n\t\tsetState( val );\n\t\teditCertificate( 'margins', newMargins );\n\t};\n\n\treturn (\n\t\t<BaseControl\n\t\t\tlabel={ __( 'Inner Margins', 'lifterlms' ) }\n\t\t\tid=\"llms-certificate-margins-control\"\n\t\t>\n\t\t\t<div style={ { display: 'flex' } }>\n\t\t\t\t{ margins.map( ( margin, index ) => ( <MarginControl key={ index } { ...{ margin, index, editMargins } } /> ) ) }\n\t\t\t</div>\n\t\t</BaseControl>\n\t);\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/plugin/orientation-control.js",
    "content": "import { __ } from '@wordpress/i18n';\n\nimport { ButtonGroupControl } from '@lifterlms/components';\n\nimport editCertificate from '../edit-certificate';\n\n/**\n * Certificates orientation control component.\n *\n * @since 6.0.0\n *\n * @param {Object} args             Component arguments.\n * @param {string} args.orientation Currently selected orientation value.\n * @return {ButtonGroupControl} Control component.\n */\nexport default function OrientationControl( { orientation } ) {\n\tconst { orientations } = window.llms.certificates,\n\t\toptions = Object.entries( orientations ).map( ( [ value, label ] ) => ( { value, label } ) );\n\n\treturn (\n\t\t<ButtonGroupControl\n\t\t\tid=\"llms-certificate-orientation-control\"\n\t\t\tlabel={ __( 'Orientation', 'lifterlms' ) }\n\t\t\tselected={ orientation }\n\t\t\toptions={ options }\n\t\t\tonClick={ ( val ) => editCertificate( 'orientation', val ) }\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/plugin/sequential-id-control.js",
    "content": "import { __ } from '@wordpress/i18n';\nimport { TextControl } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\n\nimport editCertificate from '../edit-certificate';\n\n/**\n * Certificates next sequential id control component.\n *\n * @since 6.0.0\n *\n * @param {Object} args              Component arguments.\n * @param {string} args.sequentialId Current sequential ID.\n * @return {TextControl} Control component.\n */\nexport default function SequentialIdControl( { sequentialId } ) {\n\tconst [ currId, setId ] = useState( sequentialId );\n\n\tlet { minSequentialId } = window.llms.certificates;\n\n\tif ( ! minSequentialId ) {\n\t\tminSequentialId = sequentialId;\n\t\twindow.llms.certificates.minSequentialId = minSequentialId;\n\t}\n\n\treturn (\n\t\t<TextControl\n\t\t\tid=\"llms-certificate-title-control\"\n\t\t\tlabel={ __( 'Next Sequential ID', 'lifterlms' ) }\n\t\t\tvalue={ currId }\n\t\t\ttype=\"number\"\n\t\t\tstep=\"1\"\n\t\t\tmin={ minSequentialId }\n\t\t\tonChange={ ( val ) => {\n\t\t\t\tsetId( val );\n\t\t\t\teditCertificate( 'sequential_id', val );\n\t\t\t} }\n\t\t\thelp={ __( 'Used for the {sequential_id} merge code when generating a certificate from this template.', 'lifterlms' ) }\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/plugin/size-control.js",
    "content": "import { __, _x, sprintf } from '@wordpress/i18n';\nimport { SelectControl, TextControl } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\n\nimport editCertificate from '../edit-certificate';\n\n/**\n * Format the label for a size in the SizeControl.\n *\n * @since 6.0.0\n *\n * @param {Object} args        Component args.\n * @param {string} args.name   Name of the size.\n * @param {number} args.width  Size width.\n * @param {number} args.height Size height.\n * @param {string} args.unit   Size unit ID.\n * @return {string} Label for the size.\n */\nfunction formatSizeLabel( { name, width, height, unit } ) {\n\tconst { units } = window.llms.certificates,\n\t\t{ symbol } = units[ unit ] || {};\n\treturn sprintf( '%1$s (%2$s%4$s x %3$s%4$s)', name, width, height, symbol );\n}\n\n/**\n * Control component group for defining a custom size.\n *\n * @since 6.0.0\n *\n * @param {Object} args        Component args.\n * @param {number} args.width  Current width.\n * @param {number} args.height Current height.\n * @param {string} args.unit   Current unit ID.\n * @return {WPElement} The component.\n */\nfunction CustomSizeControl( { width, height, unit } ) {\n\tconst [ currWidth, setWidth ] = useState( width ),\n\t\t[ currHeight, setHeight ] = useState( height ),\n\t\t[ currUnit, setUnit ] = useState( unit );\n\n\treturn (\n\t\t<div style={ { display: 'flex' } }>\n\t\t\t<div style={ { flex: 1 } }>\n\t\t\t\t<TextControl\n\t\t\t\t\tid=\"llms-certificate-control--size--custom-width\"\n\t\t\t\t\tlabel={ __( 'Custom Size Width', 'lifterlms' ) }\n\t\t\t\t\tplaceholder={ __( 'Width', 'lifterlms' ) }\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tvalue={ currWidth }\n\t\t\t\t\thideLabelFromVision\n\t\t\t\t\tonChange={ ( val ) => {\n\t\t\t\t\t\tsetWidth( val );\n\t\t\t\t\t\teditCertificate( 'width', val );\n\t\t\t\t\t} }\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<div style={ { flex: 1 } }>\n\t\t\t\t<TextControl\n\t\t\t\t\tid=\"llms-certificate-control--size--custom-height\"\n\t\t\t\t\tlabel={ __( 'Custom Size Height', 'lifterlms' ) }\n\t\t\t\t\tplaceholder={ __( 'Height', 'lifterlms' ) }\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tvalue={ currHeight }\n\t\t\t\t\thideLabelFromVision\n\t\t\t\t\tonChange={ ( val ) => {\n\t\t\t\t\t\tsetHeight( val );\n\t\t\t\t\t\teditCertificate( 'height', val );\n\t\t\t\t\t} }\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<div style={ { flex: 2 } }>\n\t\t\t\t<SelectControl\n\t\t\t\t\tid=\"llms-certificate-control--size--custom-unit\"\n\t\t\t\t\tlabel={ __( 'Custom Size Dimension', 'lifterlms' ) }\n\t\t\t\t\thideLabelFromVision\n\t\t\t\t\tvalue={ currUnit }\n\t\t\t\t\tonChange={ ( val ) => {\n\t\t\t\t\t\tsetUnit( val );\n\t\t\t\t\t\teditCertificate( 'unit', val );\n\t\t\t\t\t} }\n\t\t\t\t\toptions={ [\n\t\t\t\t\t\t{ value: 'in', label: __( 'in (Inches)', 'lifterlms' ) },\n\t\t\t\t\t\t{ value: 'mm', label: __( 'mm (Millimeters)', 'lifterlms' ) },\n\t\t\t\t\t] }\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\n/**\n * Size control selector.\n *\n * @since 6.0.0\n *\n * @param {Object} args        Component args.\n * @param {string} args.size   Selected size ID.\n * @param {number} args.width  Current width.\n * @param {number} args.height Current height.\n * @param {string} args.unit   Current unit.\n * @return {WPElement} Component.\n */\nexport default function SizeControl( { size: selected, width, height, unit } ) {\n\tconst { sizes } = window.llms.certificates,\n\t\toptions = Object.entries( sizes ).map( ( [ value, sizeData ] ) => ( { value, label: formatSizeLabel( sizeData ) } ) ),\n\t\t[ size, setSize ] = useState( selected );\n\n\toptions.push( {\n\t\tvalue: 'CUSTOM',\n\t\tlabel: _x( 'Custom', 'certificate sizing option', 'lifterlms' ),\n\t} );\n\n\treturn (\n\t\t<>\n\t\t\t<SelectControl\n\t\t\t\tid=\"llms-certificate-control--size\"\n\t\t\t\tlabel={ __( 'Size', 'lifterlms' ) }\n\t\t\t\tvalue={ size }\n\t\t\t\toptions={ options }\n\t\t\t\tonChange={ ( val ) => {\n\t\t\t\t\tsetSize( val );\n\t\t\t\t\teditCertificate( 'size', val );\n\n\t\t\t\t\t// Update other fields so that when switching to custom it always shows the previously selected size data.\n\t\t\t\t\tif ( 'CUSTOM' !== val ) {\n\t\t\t\t\t\tconst newSize = sizes[ val ];\n\t\t\t\t\t\teditCertificate( 'unit', newSize.unit );\n\t\t\t\t\t\teditCertificate( 'width', newSize.width );\n\t\t\t\t\t\teditCertificate( 'height', newSize.height );\n\t\t\t\t\t}\n\t\t\t\t} }\n\t\t\t/>\n\n\t\t\t{ 'CUSTOM' === size && ( <CustomSizeControl { ...{ editCertificate, width, height, unit } } /> ) }\n\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/plugin/title-control.js",
    "content": "import { __ } from '@wordpress/i18n';\nimport { TextControl } from '@wordpress/components';\nimport { useSelect } from '@wordpress/data';\nimport { store as editorStore } from '@wordpress/editor';\nimport { store as blockEditorStore } from '@wordpress/block-editor';\n\nimport editCertificate from '../edit-certificate';\n\n/**\n * Determine if the TitleControl should be displayed.\n *\n * The control is not available for `llms_my_certificate` post types at all and is not used\n * when a `lifterlms/certificate-title` block is found in the content of an `llms_certificate`\n * post type.\n *\n * @since 6.0.0\n *\n * @param {Object}      props          Component properties.\n * @param {WPElement[]} props.children Child components.\n * @return {?WPElement[]} Child components list or `null` if the components shouldn't display.\n */\nexport function Check( { children } ) {\n\tconst { getCurrentPostType } = useSelect( editorStore ),\n\t\t{ getInserterItems } = useSelect( blockEditorStore );\n\n\tif ( 'llms_certificate' !== getCurrentPostType() ) {\n\t\treturn null;\n\t}\n\n\t// Using this method in favor of `canInsertBlockType()` due to this: https://github.com/WordPress/gutenberg/issues/37540.\n\tconst { isDisabled } = getInserterItems().find( ( { name } ) => 'llms/certificate-title' === name );\n\tif ( isDisabled ) {\n\t\treturn null;\n\t}\n\n\treturn children;\n}\n\n/**\n * Certificates title control component.\n *\n * @since 6.0.0\n *\n * @param {Object} args       Component arguments.\n * @param {string} args.title Currently selected title value.\n * @return {TextControl} Control component.\n */\nexport default function TitleControl( { title } ) {\n\treturn (\n\t\t<TextControl\n\t\t\tid=\"llms-certificate-title-control\"\n\t\t\tlabel={ __( 'Title', 'lifterlms' ) }\n\t\t\tvalue={ title }\n\t\t\tonChange={ ( val ) => editCertificate( 'title', val ) }\n\t\t\thelp={ __( 'Used as the title for certificates generated from this template.', 'lifterlms' ) }\n\t\t/>\n\t);\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/post-status-info/award-button.js",
    "content": "import { AwardCheck } from './award-check';\nimport { AwardCertificateButton } from '../../util';\n\n/**\n * Renders a button / modal interface used to generate awarded certificates.\n *\n * @since 6.0.0\n *\n * @param {Object}  params             Component options.\n * @param {number}  params.postId      WP_Post ID of the template.\n * @param {string}  params.postType    Current post type where the button is being displayed.\n * @param {boolean} params.isSaving    Whether or not the editor is currently saving.\n * @param {boolean} params.isPublished Whether or not the current post is published.\n * @return {AwardCheck} Check component which conditionally renders the <AwardCertificateButton> component.\n */\nexport default function( { postId, postType, isSaving, isPublished } ) {\n\treturn (\n\t\t<AwardCheck { ...{ postType } }>\n\t\t\t<AwardCertificateButton\n\t\t\t\tenableScratch={ false }\n\t\t\t\tselectTemplate={ false }\n\t\t\t\ttemplateId={ postId }\n\t\t\t\tisDisabled={ isSaving || ! isPublished }\n\t\t\t/>\n\t\t</AwardCheck>\n\t);\n}\n\n"
  },
  {
    "path": "src/js/admin-certificate-editor/post-status-info/award-check.js",
    "content": "/**\n * Conditional wrapper component used to determine if the AwardFromTemplate component should be rendered.\n *\n * The component is rendered for certificate templates.\n *\n * @since 6.0.0\n *\n * @param {Object}   params          Component parameters.\n * @param {Object[]} params.postType Current post type.\n * @param {Object[]} params.children Child components.\n * @return {?Object[]} Returns the children or `null` if the check fails.\n */\nexport function AwardCheck( { postType, children } ) {\n\tif ( postType && 'llms_certificate' === postType ) {\n\t\treturn children;\n\t}\n\treturn null;\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/post-status-info/index.js",
    "content": "import { compose } from '@wordpress/compose';\nimport { withSelect } from '@wordpress/data';\nimport { PluginPostStatusInfo } from '@wordpress/edit-post';\nimport { store as editorStore } from '@wordpress/editor';\n\nimport AwardButton from './award-button';\nimport ResetButton from './reset-template-button';\n\n/**\n * Renders a button / modal interface used to generate awarded certificates.\n *\n * @since 6.0.0\n *\n * @param {Object}  params             Component options.\n * @param {boolean} params.isPublished Whether or not the current post is published.\n * @param {boolean} params.isSaving    Whether or not the editor is currently saving.\n * @param {number}  params.postId      WP_Post ID of the template.\n * @param {string}  params.postType    Current post type where the button is being displayed.\n * @return {PluginPostStatusInfo} The status info component.\n */\nexport function PluginStatusButtons( { isPublished, isSaving, postId, postType } ) {\n\treturn (\n\t\t<PluginPostStatusInfo>\n\t\t\t<div>\n\t\t\t\t<AwardButton { ...{ postId, postType, isPublished, isSaving } } />\n\t\t\t\t&nbsp;\n\t\t\t\t<ResetButton { ...{ isPublished, isSaving, postType } } />\n\t\t\t</div>\n\t\t</PluginPostStatusInfo>\n\t);\n}\n\nexport default compose( [\n\twithSelect( ( wpSelect ) => {\n\t\tconst {\n\t\t\tisSavingPost,\n\t\t\tisCurrentPostPublished,\n\t\t\tgetCurrentPostId,\n\t\t\tgetCurrentPostType,\n\t\t} = wpSelect( editorStore );\n\t\treturn {\n\t\t\tisPublished: isCurrentPostPublished(),\n\t\t\tisSaving: isSavingPost(),\n\t\t\tpostId: getCurrentPostId(),\n\t\t\tpostType: getCurrentPostType(),\n\t\t};\n\t} ),\n] )( PluginStatusButtons );\n"
  },
  {
    "path": "src/js/admin-certificate-editor/post-status-info/reset-template-button.js",
    "content": "import { __ } from '@wordpress/i18n';\nimport { Button, Modal } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\nimport { dispatch, select } from '@wordpress/data';\nimport { store as blockEditorStore } from '@wordpress/block-editor';\nimport { store as editorStore } from '@wordpress/editor';\nimport { synchronizeBlocksWithTemplate } from '@wordpress/blocks';\nimport { doAction } from '@wordpress/hooks';\n\nimport { ResetTemplateCheck } from './reset-template-check';\nimport { editCertificateTitle } from '../../util';\n\n/**\n * Resets the post's default post type template and then triggers a save action.\n *\n * @since 6.0.0\n * @param {Function} onComplete  Callback function invoked when the reset and save actions are completed.\n * @param {boolean}  isPublished Determines if the current post is published.\n * @return {void}\n */\nfunction resetTemplate( onComplete, isPublished ) {\n\tconst { getBlocks, getTemplate } = select( blockEditorStore ),\n\t\t{ replaceBlocks, insertBlocks } = dispatch( blockEditorStore ),\n\t\t{ editPost, savePost } = dispatch( editorStore ),\n\t\tclientIds = getBlocks().map( ( { clientId } ) => clientId ),\n\t\ttemplate = synchronizeBlocksWithTemplate( [], getTemplate() );\n\n\teditCertificateTitle( '' );\n\tif ( isPublished ) {\n\t\teditPost( { status: 'draft' } );\n\t}\n\n\t/**\n\t * Action run before the default certificate post type template is reset.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param {Array} template Block template array.\n\t */\n\tdoAction( 'llms.resetCertificateTemplate.before', template );\n\n\tif ( clientIds.length ) {\n\t\treplaceBlocks( clientIds, template );\n\t} else {\n\t\tinsertBlocks( template );\n\t}\n\n\t/**\n\t * Action run after the default certificate post type template is reset.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param {Array} template Block template array.\n\t */\n\tdoAction( 'llms.resetCertificateTemplate.after', template );\n\n\tsavePost();\n\tonComplete();\n}\n\n/**\n * Resets a certificate to the default block template defined during post type registration.\n *\n * Renders a \"Reset template\" button near the \"Move to trash\" button in the post status\n * area of the editor document settings sidebar.\n *\n * @since 6.0.0\n *\n * @param {Object}  props             Component properties.\n * @param {boolean} props.isSaving    Whether or not the post is currently being saved. The main button is disabled during saves.\n * @param {boolean} props.isPublished Whether or not the post is currently published. If the post is published, it will be switched to a draft during the reset.\n *\n * @return {?ResetTemplateCheck} Returns the child components to render or `null` if the button should not be displayed.\n */\nexport default function( { isSaving, isPublished } ) {\n\tconst [ isOpen, setIsOpen ] = useState( false ),\n\t\tcloseModal = () => setIsOpen( false ),\n\t\topenModal = () => setIsOpen( true );\n\n\tlet msg = __( 'Are you sure you wish to replace the certificate content with the original default layout? This action cannot be undone!', 'lifterlms' );\n\tif ( isPublished ) {\n\t\tmsg = __( \"Are you sure you wish to unpublish the certificate and replace it's content with the original default layout? This action cannot be undone!\", 'lifterlms' );\n\t}\n\n\treturn (\n\t\t<ResetTemplateCheck>\n\t\t\t{ isOpen && (\n\t\t\t\t<Modal\n\t\t\t\t\ttitle={ __( 'Confirm template reset', 'lifterlms' ) }\n\t\t\t\t\tstyle={ { maxWidth: '360px' } }\n\t\t\t\t\tonRequestClose={ closeModal }\n\t\t\t\t>\n\t\t\t\t\t<p>{ msg }</p>\n\t\t\t\t\t<div style={ { textAlign: 'right' } }>\n\t\t\t\t\t\t<Button variant=\"tertiary\" onClick={ closeModal }>\n\t\t\t\t\t\t\t{ __( 'Cancel', 'lifterlms' ) }\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t&nbsp;\n\t\t\t\t\t\t<Button variant=\"primary\" onClick={ () => resetTemplate( closeModal, isPublished ) }>\n\t\t\t\t\t\t\t{ __( 'Reset template', 'lifterlms' ) }\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</Modal>\n\t\t\t) }\n\t\t\t<Button onClick={ openModal } disabled={ isSaving } isDestructive>{ __( 'Reset template', 'lifterlms' ) }</Button>\n\t\t</ResetTemplateCheck>\n\t);\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/post-status-info/reset-template-check.js",
    "content": "import { useSelect } from '@wordpress/data';\nimport { store as editorStore } from '@wordpress/editor';\n\n/**\n * Conditional wrapper component used to determine if the ResetTemplate component should be rendered.\n *\n * The component is rendered for certificate templates and awarded certificates so long as the template\n * is *not* connected to a template.\n *\n * @since 6.0.0\n *\n * @param {Object}   params          Component parameters.\n * @param {Object[]} params.children Child components.\n * @return {?Object[]} Returns the children or `null` if the check fails.\n */\nexport function ResetTemplateCheck( { children } ) {\n\tconst { getCurrentPost } = useSelect( editorStore ),\n\t\tpost = getCurrentPost(),\n\t\t{ type, certificate_template: template } = post;\n\n\tif ( type && ( 'llms_certificate' === type || ( 'llms_my_certificate' === type && 0 === template ) ) ) {\n\t\treturn children;\n\t}\n\n\treturn null;\n}\n"
  },
  {
    "path": "src/js/admin-certificate-editor/user-settings.js",
    "content": "// External deps.\nimport styled from '@emotion/styled';\n\n// WP deps.\nimport { __ } from '@wordpress/i18n';\nimport { PanelRow, ExternalLink } from '@wordpress/components';\nimport { compose } from '@wordpress/compose';\nimport { withSelect, dispatch, useSelect } from '@wordpress/data';\nimport { PluginPostStatusInfo } from '@wordpress/edit-post';\nimport { store as editorStore } from '@wordpress/editor';\nimport { store as coreStore } from '@wordpress/core-data';\nimport { addQueryArgs, getQueryArg } from '@wordpress/url';\n\n// Internal deps.\nimport { UserSearchControl } from '@lifterlms/components';\n\n/**\n * Force the PanelRow to full-width.\n *\n * @since 6.0.0\n */\nexport const StyledPanelRow = styled( PanelRow )`\n\twidth: 100%;\n`;\n\n/**\n * Outputs information about the selected user.\n *\n * This component is displayed for published certificates in place of the <UserSearchControl>.\n *\n * @since 6.0.0\n *\n * @param {Object} args        Component arguments.\n * @param {Object} args.userId WP_User ID of the selected user.\n * @return {WPElement|ExternalLink} Returns a generic element with loading text or an ExternalLink component linking to the\n *                                  the WP admin user edit screen for the selected user.\n */\nfunction SelectedUser( { userId } ) {\n\tconst name = useSelect(\n\t\t( select ) => {\n\t\t\tconst { getEntityRecord } = select( coreStore ),\n\t\t\t\tuser = getEntityRecord( 'root', 'user', userId );\n\t\t\treturn user?.name;\n\t\t},\n\t\t[ userId ]\n\t);\n\n\tif ( ! name ) {\n\t\treturn (\n\t\t\t<span>{ __( 'Loading…', 'lifterlms' ) }</span>\n\t\t);\n\t}\n\n\treturn ( <ExternalLink href={ addQueryArgs(\n\t\t'admin.php',\n\t\t{\n\t\t\tpage: 'llms-reporting',\n\t\t\ttab: 'students',\n\t\t\tstab: 'certificates',\n\t\t\tstudent_id: userId,\n\t\t}\n\t) }>{ name }</ExternalLink> );\n}\n\n/**\n * Render the certificate settings editor panel.\n *\n * @since 6.0.0\n *\n * @param {Object}  args        Component arguments.\n * @param {string}  args.type   Current post type.\n * @param {number}  args.userId WP User Id.\n * @param {boolean} args.isNew  Whether the current post has never been saved.\n * @return {?PluginPostStatusInfo} The component or `null` when rendered in an invalid context.\n */\nfunction CertificateUserSettings( { type, userId, isNew } ) {\n\t// Only load for the right post type.\n\tif ( 'llms_my_certificate' !== type ) {\n\t\treturn null;\n\t}\n\n\t// Retrieve the user ID from the URL.\n\tconst forceId = getQueryArg( window.location.href, 'sid' );\n\tuserId = forceId ? forceId : userId;\n\n\treturn (\n\n\t\t<PluginPostStatusInfo>\n\t\t\t<StyledPanelRow>\n\t\t\t\t<span style={ { display: 'block', width: '45%' } }>{ __( 'Student', 'lifterlms' ) }</span>\n\n\t\t\t\t{ ( ! isNew || forceId ) && (\n\t\t\t\t\t<SelectedUser userId={ userId } />\n\t\t\t\t) }\n\t\t\t\t{ ( isNew && ! forceId ) && (\n\t\t\t\t\t<UserSearchControl\n\t\t\t\t\t\tselectedValue={ userId }\n\t\t\t\t\t\tonUpdate={ ( { id } ) => {\n\t\t\t\t\t\t\tconst { editPost } = dispatch( editorStore );\n\t\t\t\t\t\t\teditPost(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tauthor: id, // Update the post author.\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} }\n\t\t\t\t\t/>\n\t\t\t\t) }\n\t\t\t</StyledPanelRow>\n\t\t</PluginPostStatusInfo>\n\n\t);\n}\n\nconst applyWithSelect = withSelect( ( select ) => {\n\tconst { getEditedPostAttribute, isEditedPostNew } = select( editorStore );\n\treturn {\n\t\tisNew: isEditedPostNew(),\n\t\ttype: getEditedPostAttribute( 'type' ),\n\t\tuserId: getEditedPostAttribute( 'author' ),\n\t};\n} );\n\nexport default compose( [ applyWithSelect ] )(\n\tCertificateUserSettings\n);\n"
  },
  {
    "path": "src/js/admin-certificate-editor.js",
    "content": "import './admin-certificate-editor/index';\n"
  },
  {
    "path": "src/js/admin-elementor-editor.js",
    "content": "jQuery(document).ready(function($) {\n\tif (typeof elementor === 'undefined') {\n\t\treturn;\n\t}\n\n\telementor.modules.layouts.panel.pages.menu.Menu.addItem({\n\t\tname:'Course Builder',\n\t\ttitle:'Launch Course Builder',\n\t\ticon: 'wp-menu-image dashicons-before dashicons-welcome-learn-more',\n\t\tcallback: function callback() {\n\t\t\twindow.location.href = llms_elementor.builder_url;\n\t\t}}, 'navigate_from_page', 'finder');\n});\n"
  },
  {
    "path": "src/js/admin-media-protection-block-protect.js",
    "content": "( function( wp ) {\n\tconst { addFilter } = wp.hooks;\n\tconst { createHigherOrderComponent } = wp.compose;\n\tconst { Fragment, useState, useEffect, useRef, createInterpolateElement } = wp.element;\n\tconst { ToolbarButton, Modal, Button, Flex, FlexItem, Notice, ExternalLink } = wp.components;\n\tconst { BlockControls } = wp.blockEditor;\n\tconst { apiFetch } = wp;\n\n\tconst supportedMediaBlocks = [\n\t\t'core/image',\n\t\t'core/video',\n\t\t'core/audio',\n\t\t'core/file',\n\t];\n\n\tconst getUrlAttr = ( blockName ) => {\n\t\tswitch ( blockName ) {\n\t\t\tcase 'core/image':\n\t\t\t\treturn 'url';\n\t\t\tcase 'core/audio':\n\t\t\t\treturn 'src';\n\t\t\tcase 'core/video':\n\t\t\t\treturn 'src';\n\t\t\tcase 'core/file':\n\t\t\t\treturn 'href';\n\t\t\tdefault:\n\t\t\t\treturn 'url';\n\t\t}\n\t};\n\n\tconst withProtectImageToolbar = createHigherOrderComponent( ( BlockEdit ) => {\n\t\treturn ( props ) => {\n\t\t\tconst warningText = createInterpolateElement(\n\t\t\t\tLLMS.l10n.translate(\n\t\t\t\t\t'This media is not protected. If you select a product here, the media will be moved to the protected uploads directory and existing links to the media will no longer work. <link>Learn More</link>'\n\t\t\t\t),\n\t\t\t\t{\n\t\t\t\t\tlink: (\n\t\t\t\t\t\t<ExternalLink\n\t\t\t\t\t\t\thref=\"https://lifterlms.com/docs/how-protected-media-files-work/?utm_source=LifterLMS%20Plugin&utm_medium=Media&utm_campaign=Backend%20Help%20Page\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t);\n\n\t\t\t// We don't have a media ID if \"insert from URL\" is used.\n\t\t\tif ( ! props.attributes || ! props.attributes.id ) {\n\t\t\t\treturn <BlockEdit { ...props } />;\n\t\t\t}\n\n\t\t\tif ( ! supportedMediaBlocks.includes( props.name ) ) {\n\t\t\t\treturn <BlockEdit { ...props } />;\n\t\t\t}\n\n\t\t\tconst [ isModalOpen, setModalOpen ] = useState( false );\n\t\t\tconst [ productTitle, setProductTitle ] = useState( null );\n\t\t\tconst selectRef = useRef( null );\n\n\t\t\tuseEffect( () => {\n\t\t\t\tif ( isModalOpen && selectRef.current ) {\n\t\t\t\t\tjQuery( selectRef.current ).llmsPostsSelect2();\n\t\t\t\t}\n\t\t\t}, [ isModalOpen ] );\n\n\t\t\tconst handleProtectImage = () => {\n\t\t\t\tconst selectedId = jQuery( selectRef.current ).val();\n\n\t\t\t\tapiFetch( {\n\t\t\t\t\tpath: `/wp/v2/media/${ props.attributes.id }`,\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tdata: {\n\t\t\t\t\t\t_llms_media_protection_product_id: selectedId\n\t\t\t\t\t}\n\t\t\t\t} ).then( ( updatedMedia ) => {\n\t\t\t\t\tconst urlAttr = getUrlAttr( props.name );\n\t\t\t\t\tprops.setAttributes( { [ urlAttr ]: updatedMedia.source_url } );\n\t\t\t\t\tsetProductTitle( null );\n\t\t\t\t} ).catch( ( err ) => {\n\t\t\t\t\tconsole.error( 'Error updating media meta:', err );\n\t\t\t\t} );\n\t\t\t\tsetModalOpen( false );\n\t\t\t};\n\n\t\t\tuseEffect( () => {\n\t\t\t\tif ( ! isModalOpen ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tapiFetch( {\n\t\t\t\t\tpath: `/wp/v2/media/${ props.attributes.id }?context=edit`,\n\t\t\t\t} )\n\t\t\t\t\t.then( ( media ) => {\n\t\t\t\t\t\tconst productId = media._llms_media_protection_product_id;\n\t\t\t\t\t\tif ( ! productId ) {\n\t\t\t\t\t\t\tsetProductTitle( null );      // media isn’t protected yet\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst tryEndpoints = [ 'courses', 'memberships' ];\n\t\t\t\t\t\t( async () => {\n\t\t\t\t\t\t\tfor ( const type of tryEndpoints ) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst product = await apiFetch( {\n\t\t\t\t\t\t\t\t\t\tpath: `/llms/v1/${ type }/${ productId }?context=edit`,\n\t\t\t\t\t\t\t\t\t} );\n\t\t\t\t\t\t\t\t\tsetProductTitle( product.title.raw );\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t} catch ( er ) {\n\t\t\t\t\t\t\t\t\t// keep trying\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsetProductTitle( '(unknown)' );   // couldn’t load it\n\t\t\t\t\t\t} )();\n\t\t\t\t\t} )\n\t\t\t\t\t.catch( ( er ) => {\n\t\t\t\t\t\tconsole.error( 'Unable to read media meta', er );\n\t\t\t\t\t\tsetProductTitle( null );\n\t\t\t\t\t} );\n\t\t\t}, [ isModalOpen, props.attributes.id ] );\n\n\t\t\treturn (\n\t\t\t\t<Fragment>\n\t\t\t\t\t<BlockEdit { ...props } />\n\t\t\t\t\t<BlockControls group=\"inline\">\n\t\t\t\t\t\t<ToolbarButton\n\t\t\t\t\t\t\ticon=\"lock\"\n\t\t\t\t\t\t\tlabel=\"Protect Image\"\n\t\t\t\t\t\t\tonClick={ () => setModalOpen( true ) }\n\t\t\t\t\t\t/>\n\t\t\t\t\t</BlockControls>\n\n\t\t\t\t\t{ isModalOpen && (\n\t\t\t\t\t\t<Modal\n\t\t\t\t\t\t\ttitle={ LLMS.l10n.translate( 'Select Course or Membership' ) }\n\t\t\t\t\t\t\tonRequestClose={() => setModalOpen(false)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Flex direction=\"column\" gap={4}>\n\t\t\t\t\t\t\t\t<FlexItem>\n\t\t\t\t\t\t\t\t\t<label htmlFor=\"llms-protect-image-select\">{ LLMS.l10n.translate( 'Select a Course or Membership to protect this image:' ) }</label>\n\t\t\t\t\t\t\t\t</FlexItem>\n\n\t\t\t\t\t\t\t\t<FlexItem>\n\t\t\t\t\t\t\t\t\t{ ! props.attributes[ getUrlAttr( props.name ) ].includes( 'llms_media_id' ) &&\n\t\t\t\t\t\t\t\t\t\t<Notice status=\"warning\" isDismissible={false}>\n\t\t\t\t\t\t\t\t\t\t\t{ warningText }\n\t\t\t\t\t\t\t\t\t\t</Notice>\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\tid=\"llms-protect-image-select\"\n\t\t\t\t\t\t\t\t\tref={ selectRef }\n\t\t\t\t\t\t\t\t\tclassName='llms-block-protect llms-posts-select2'\n\t\t\t\t\t\t\t\t\tdata-no-view-button='true'\n\t\t\t\t\t\t\t\t\tdata-allow_clear='false'\n\t\t\t\t\t\t\t\t\tdata-post-type='course,llms_membership'\n\t\t\t\t\t\t\t\t\t></select>\n\t\t\t\t\t\t\t\t</FlexItem>\n\t\t\t\t\t\t\t\t{ productTitle && (\n\t\t\t\t\t\t\t\t\t<FlexItem>\n\t\t\t\t\t\t\t\t\t\t{ LLMS.l10n.translate( 'Currently protected by:' ) }&nbsp;\n\t\t\t\t\t\t\t\t\t\t<strong>{ productTitle }</strong>\n\t\t\t\t\t\t\t\t\t</FlexItem>\n\t\t\t\t\t\t\t\t) }\n\t\t\t\t\t\t\t\t<FlexItem>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tisPrimary\n\t\t\t\t\t\t\t\t\t\tonClick={ () => {\n\t\t\t\t\t\t\t\t\t\t\thandleProtectImage();\n\t\t\t\t\t\t\t\t\t\t} }\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tProtect Image\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</FlexItem>\n\t\t\t\t\t\t\t</Flex>\n\t\t\t\t\t\t</Modal>\n\t\t\t\t\t)}\n\t\t\t\t</Fragment>\n\t\t\t);\n\t\t};\n\t}, 'withProtectImageToolbar');\n\n\taddFilter(\n\t\t'editor.BlockEdit',\n\t\t'my-plugin/with-protect-image-toolbar',\n\t\twithProtectImageToolbar\n\t);\n})(window.wp);\n"
  },
  {
    "path": "src/js/components.js",
    "content": "import * as Components from '../../packages/components/src';\n\nwindow.llms = window.llms || {};\n\n// Preserve components from `lifterlms-blocks`.\nconst { components = {} } = window.llms;\n\n/**\n * Expose @lifterlms/components via the global `window.llms` object.\n *\n * @since 6.0.0\n */\nwindow.llms.components = {\n\t...components,\n\t...Components,\n};\n"
  },
  {
    "path": "src/js/icons.js",
    "content": "import * as icons from '../../packages/icons/src';\n\n/**\n * Expose @lifterlms/icons via the global `window.llms` object.\n *\n * @since 6.0.0\n */\nwindow.llms = window.llms || {};\nwindow.llms.icons = icons;\n"
  },
  {
    "path": "src/js/quill-wordcount.js",
    "content": "import quillWordcountModule from '../../packages/quill-wordcount-module/src';\n\n/**\n * Registers the wordcount module with Quill.\n *\n * @since 6.10.0\n */\n( function() {\n\tquillWordcountModule();\n}() );\n"
  },
  {
    "path": "src/js/spinner.js",
    "content": "// Loading the Spinner component file(s) directly so that we don't load other unneeded dependencies (like WPElement/React).\nimport * as Spinner from '../../packages/components/src/spinner/';\n\n/**\n * Expose the components Spinner module via the global `window.LLMS.Spinner` object for backwards compatibility.\n *\n * This is automatically included in the `llms.js` script file so you likely do not need to include this script directly.\n *\n * @since 7.0.0\n */\nwindow.LLMS = window.LLMS || {};\nwindow.LLMS.Spinner = Spinner;\n"
  },
  {
    "path": "src/js/util/README.md",
    "content": "LifterLMS Internal Javascript Utilities\n=======================================\n\nThe functions contained within this directory are general-purpose utilities shared by various blocks and components. The exports found within this packages are intentionally not exposed. They cannot be imported from outside the core plugin and are therefore considered to be part of the _private_ API.\n\nAs such, semantic versioning will not be upheld for functions and files found within this directory.\n"
  },
  {
    "path": "src/js/util/award-certificate-button/create.js",
    "content": "// WP deps.\nimport { store as coreStore } from '@wordpress/core-data';\nimport { dispatch } from '@wordpress/data';\n\n/**\n * Creates a new awarded certificate post for a given student with a specified parent template.\n *\n * @since 6.0.0\n *\n * @param {number} studentId  WP_User ID.\n * @param {number} templateId WP_Post ID.\n * @return {Promise<Object>} A promise that resolves to the WP_Post object api response on success.\n */\nexport default function( studentId, templateId ) {\n\tconst { saveEntityRecord } = dispatch( coreStore );\n\n\treturn saveEntityRecord(\n\t\t'postType',\n\t\t'llms_my_certificate',\n\t\t{\n\t\t\tauthor: studentId,\n\t\t\tcertificate_template: templateId,\n\t\t\tstatus: 'draft',\n\t\t}\n\t);\n}\n"
  },
  {
    "path": "src/js/util/award-certificate-button/index.js",
    "content": "// WP Deps.\nimport { __ } from '@wordpress/i18n';\nimport { Button, Modal } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\n\n// LLMS Deps.\nimport { PostSearchControl, UserSearchControl } from '@lifterlms/components';\n\n// Internal Deps.\nimport createAward from './create';\nimport getMessage from './message';\nimport { getRedirectUrl, getScratchUrl } from './urls';\n\n/**\n * Button and modal interface for creating a draft certificate from a specified template for a specific student.\n *\n * @since 6.0.0\n *\n * @param {Object}  params                Component configuration object.\n * @param {string}  params.modalTitle     Title of the modal.\n * @param {string}  params.buttonLabel    Text of the button.\n * @param {boolean} params.isDisabled     Whether or not the button is disabled.\n * @param {boolean} params.enableScratch  When `true`, displays a \"Start from Scratch\" button that links to the awarded certificate editor page.\n * @param {boolean} params.selectStudent  When `true`, displays a searchable select element to allow user selection of the user.\n * @param {boolean} params.selectTemplate When `true`, displays a searchable select element to allow user selection of the template.\n * @param {?number} params.studentId      WP_User ID of the student to award the certificate to.\n * @param {?number} params.templateId     WP_Post ID of the certificate template post to create the certificate from.\n * @return {WPElement} The component.\n */\nexport default function( {\n\tmodalTitle = __( 'Award a New Certificate', 'lifterlms' ),\n\tbuttonLabel = __( 'Award', 'lifterlms' ),\n\tisDisabled = false,\n\tenableScratch = true,\n\tselectStudent = true,\n\tselectTemplate = true,\n\tstudentId = null,\n\ttemplateId = null,\n} ) {\n\tconst [ isOpen, setIsOpen ] = useState( false ),\n\t\t[ isBusy, setIsBusy ] = useState( false ),\n\t\t[ currStudentId, setStudentId ] = useState( studentId ),\n\t\t[ currTemplateId, setTemplateId ] = useState( templateId ),\n\t\tcloseModal = () => setIsOpen( false ),\n\t\topenModal = () => setIsOpen( true ),\n\t\tisReady = currStudentId && currTemplateId,\n\t\tonClick = () => {\n\t\t\tsetIsBusy( true );\n\t\t\tcreateAward( currStudentId, currTemplateId ).then( ( { id } ) => {\n\t\t\t\twindow.location = getRedirectUrl( id );\n\t\t\t} );\n\t\t};\n\n\treturn (\n\t\t<>\n\t\t\t{ isOpen && (\n\t\t\t\t<Modal\n\t\t\t\t\ttitle={ modalTitle }\n\t\t\t\t\tstyle={ { maxWidth: '420px' } }\n\t\t\t\t\tonRequestClose={ closeModal }\n\t\t\t\t>\n\n\t\t\t\t\t<p>{ getMessage( selectStudent, selectTemplate ) }</p>\n\n\t\t\t\t\t{ selectStudent && (\n\t\t\t\t\t\t<UserSearchControl\n\t\t\t\t\t\t\tisClearable\n\t\t\t\t\t\t\tlabel={ __( 'Award to', 'lifterlms' ) }\n\t\t\t\t\t\t\tselectedValue={ studentId ? [ studentId ] : [] }\n\t\t\t\t\t\t\tonUpdate={ ( obj ) => {\n\t\t\t\t\t\t\t\tconst id = obj?.id || null;\n\t\t\t\t\t\t\t\tsetStudentId( id );\n\t\t\t\t\t\t\t} }\n\t\t\t\t\t\t/>\n\t\t\t\t\t) }\n\n\t\t\t\t\t{ selectTemplate && (\n\t\t\t\t\t\t<PostSearchControl\n\t\t\t\t\t\t\tisClearable\n\t\t\t\t\t\t\tpostType=\"llms_certificate\"\n\t\t\t\t\t\t\tlabel={ __( 'Template', 'lifterlms' ) }\n\t\t\t\t\t\t\tplaceholder={ __( 'Search for a certificate template…', 'lifterlms' ) }\n\t\t\t\t\t\t\tselectedValue={ templateId ? [ templateId ] : [] }\n\t\t\t\t\t\t\tonUpdate={ ( obj ) => {\n\t\t\t\t\t\t\t\tconst id = obj?.id || null;\n\t\t\t\t\t\t\t\tsetTemplateId( id );\n\t\t\t\t\t\t\t} }\n\t\t\t\t\t\t/>\n\t\t\t\t\t) }\n\n\t\t\t\t\t<div style={ { textAlign: 'right', padding: '24px 32px 0', margin: '24px -32px 0', borderTop: '1px solid #ddd' } }>\n\n\t\t\t\t\t\t<Button style={ { marginRight: '5px' } } disabled={ ! isReady } isBusy={ isBusy } variant=\"primary\" onClick={ onClick }>\n\t\t\t\t\t\t\t{ __( 'Create Draft' ) }\n\t\t\t\t\t\t</Button>\n\n\t\t\t\t\t\t{ enableScratch && (\n\t\t\t\t\t\t\t<Button style={ { marginRight: '5px' } } variant=\"secondary\" href={ getScratchUrl( currStudentId ) }>\n\t\t\t\t\t\t\t\t{ __( 'Start from Scratch', 'lifterlms' ) }\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t) }\n\n\t\t\t\t\t\t<Button variant=\"tertiary\" onClick={ closeModal }>\n\t\t\t\t\t\t\t{ __( 'Cancel', 'lifterlms' ) }\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</Modal>\n\t\t\t) }\n\t\t\t<Button\n\t\t\t\tdisabled={ isDisabled }\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\tonClick={ openModal }\n\t\t\t>\n\t\t\t\t{ buttonLabel }\n\t\t\t</Button>\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/js/util/award-certificate-button/message.js",
    "content": "import { __ } from '@wordpress/i18n';\n\n/**\n * Retrieves the message based on the selectors present in the modal.\n *\n * @since 6.0.0\n *\n * @param {boolean} selectStudent  Whether or not the student selector is present.\n * @param {boolean} selectTemplate Whether or not the template selector is present.\n * @return {string} A message string.\n */\nexport default function( selectStudent, selectTemplate ) {\n\tlet msg = '';\n\tif ( selectStudent && selectTemplate ) {\n\t\tmsg = __( 'Create a new certificate award from the selected template for the selected student.', 'lifterlms' );\n\t} else if ( selectStudent && ! selectTemplate ) {\n\t\tmsg = __( 'Create a new certificate award from this template for the selected student.', 'lifterlms' );\n\t} else if ( ! selectStudent && selectTemplate ) {\n\t\tmsg = __( 'Create a new certificate award from the selected template for this student.', 'lifterlms' );\n\t}\n\treturn msg;\n}\n"
  },
  {
    "path": "src/js/util/award-certificate-button/test/__snapshots__/index.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`AwardCertificateButton getMessage No student and no template 1`] = `\"\"`;\n\nexports[`AwardCertificateButton getMessage Student and no template 1`] = `\"Create a new certificate award from this template for the selected student.\"`;\n\nexports[`AwardCertificateButton getMessage Template and no student 1`] = `\"Create a new certificate award from the selected template for this student.\"`;\n\nexports[`AwardCertificateButton getMessage Template and student 1`] = `\"Create a new certificate award from the selected template for the selected student.\"`;\n\nexports[`AwardCertificateButton urls getRedirectUrl Returns the URL 1`] = `\"/wp-admin/post.php?post=123&action=edit&newAwardMsg=1\"`;\n\nexports[`AwardCertificateButton urls getScratchUrl With student ID 1`] = `\"/wp-admin/post-new.php?post_type=llms_my_certificate&sid=123\"`;\n\nexports[`AwardCertificateButton urls getScratchUrl Without student ID 1`] = `\"/wp-admin/post-new.php?post_type=llms_my_certificate\"`;\n"
  },
  {
    "path": "src/js/util/award-certificate-button/test/index.js",
    "content": "import getMessage from '../message';\nimport { getScratchUrl, getRedirectUrl } from '../urls';\n\ndescribe( 'AwardCertificateButton', () => {\n\tdescribe( 'getMessage', () => {\n\t\tconst testData = [\n\t\t\t[ 'No student and no template', null, null ],\n\t\t\t[ 'Student and no template', 123, null ],\n\t\t\t[ 'Template and no student', null, 456 ],\n\t\t\t[ 'Template and student', 1, 2 ],\n\t\t];\n\t\ttest.each( testData )( '%s', ( name, studentId, templateId ) => {\n\t\t\texpect( getMessage( studentId, templateId ) ).toMatchSnapshot();\n\t\t} );\n\t} );\n\n\tdescribe( 'urls', () => {\n\t\tdescribe( 'getRedirectUrl', () => {\n\t\t\ttest( 'Returns the URL', () => {\n\t\t\t\texpect( getRedirectUrl( 123 ) ).toMatchSnapshot();\n\t\t\t} );\n\t\t} );\n\n\t\tdescribe( 'getScratchUrl', () => {\n\t\t\tconst testData = [\n\t\t\t\t[ 'With student ID', 123 ],\n\t\t\t\t[ 'Without student ID', null ],\n\t\t\t];\n\t\t\ttest.each( testData )( '%s', ( name, studentId ) => {\n\t\t\t\texpect( getScratchUrl( studentId ) ).toMatchSnapshot();\n\t\t\t} );\n\t\t} );\n\t} );\n} );\n"
  },
  {
    "path": "src/js/util/award-certificate-button/urls.js",
    "content": "// WP deps.\nimport { addQueryArgs } from '@wordpress/url';\n\n// LLMS deps.\nimport { getAdminUrl } from '@lifterlms/utils';\n\n/**\n * Retrieves the redirect URL for the newly created certificate.\n *\n * @since 6.0.0\n *\n * @param {number} post WP_Post ID.\n * @return {string} The edit post URL for awarded certificate draft.\n */\nexport function getRedirectUrl( post ) {\n\treturn addQueryArgs(\n\t\t`${ getAdminUrl() }/post.php`,\n\t\t{\n\t\t\tpost,\n\t\t\taction: 'edit',\n\t\t\tnewAwardMsg: 1,\n\t\t}\n\t);\n}\n\n/**\n * Retrieves the url for the \"Start from Scratch\" button.\n *\n * @since 6.0.0\n *\n * @param {?number} sid WP_User ID of the selected student.\n * @return {string} The URL.\n */\nexport function getScratchUrl( sid = null ) {\n\tconst args = {\n\t\tpost_type: 'llms_my_certificate',\n\t};\n\n\tif ( sid ) {\n\t\targs.sid = sid;\n\t}\n\n\treturn addQueryArgs(\n\t\t`${ getAdminUrl() }/post-new.php`,\n\t\targs,\n\t);\n}\n"
  },
  {
    "path": "src/js/util/edit-certificate-title.js",
    "content": "import { dispatch, select } from '@wordpress/data';\nimport { store as editorStore } from '@wordpress/editor';\n\n/**\n * Edits the title of the current certificate post type in the block editor.\n *\n * This utility is used to allow sharing functionality between `llms_certificate` and `llms_my_certificate`\n * post types. Depending on the post type, the certificate title is stored in a different post field.\n *\n * To edit the actual post title of a `llms_certificate` post (not the awarded certificate's title), use\n * `wp.data.dispatch( 'core/editor' ).editPost()` directly.\n *\n * @since 6.0.0\n *\n * @param {string} title    The desired certificate title.\n * @param {string} postType The current post type, automatically reads it from the current post if omitted.\n * @return {Promise} Promise that resolves when the title edits have been made to the current post.\n */\nexport function editCertificateTitle( title, postType = null ) {\n\tif ( ! postType ) {\n\t\tconst { getCurrentPostType } = select( editorStore );\n\t\tpostType = getCurrentPostType();\n\t}\n\n\tconst { editPost } = dispatch( editorStore ),\n\t\tedits = {};\n\n\tif ( 'llms_certificate' === postType ) {\n\t\tedits.certificate_title = title;\n\t} else if ( 'llms_my_certificate' === postType ) {\n\t\tedits.title = title;\n\t}\n\n\treturn editPost( edits );\n}\n"
  },
  {
    "path": "src/js/util/index.js",
    "content": "export { editCertificateTitle } from './edit-certificate-title';\nexport { default as AwardCertificateButton } from './award-certificate-button';\n"
  },
  {
    "path": "src/js/utils.js",
    "content": "import * as utils from '../../packages/utils/src';\n\n/**\n * Expose @lifterlms/utils via the global `window.llms` object.\n *\n * @since 6.0.0\n */\nwindow.llms = window.llms || {};\nwindow.llms.utils = utils;\n"
  },
  {
    "path": "src/scss/admin-addons.scss",
    "content": "@import '@lifterlms/brand/sass/colors';\n@import \"../../assets/scss/_includes/mixins\";\n\n.wrap.lifterlms-addons {\n\n\t.llms-subheader {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t}\n\n}\n\n.llms-addons-bulk-actions {\n\tbackground-color: #FFF;\n\tborder: 1px solid #dedede;\n\tborder-radius: 12px;\n\tbox-shadow: 0px 0px 1px rgba(48, 49, 51, 0.05), 0px 2px 4px rgba(48, 49, 51, 0.1);\n\tleft: 0;\n\tmargin-right: auto;\n\tmargin-left: auto;\n\tpadding: 40px 60px;\n\tposition: fixed;\n\tright: 0;\n\ttext-align: center;\n\ttransition: top 0.2s ease;\n\ttop: -100%;\n\twidth: 240px;\n\tz-index: 1;\n\t&.active {\n\t\ttop: 80px;\n\t}\n\n\t.llms-bulk-close {\n\t\tbackground: #fff;\n\t\tborder: 1px solid #ddd;\n\t\tborder-bottom-width: 0;\n\t\tborder-left-width: 0;\n\t\tborder-radius: 50%;\n\t\tcolor: llms-color( wp-red-50 );\n\t\tfont-size: 25px;\n\t\theight: 25px;\n\t\tpadding: 5px;\n\t\tposition: absolute;\n\t\tright: -10px;\n\t\ttop: -10px;\n\t\twidth: 25px;\n\t}\n\n\t.llms-bulk-desc {\n\t\tfont-size: 18px;\n\t\tmargin-bottom: 20px;\n\t\t.fa {\n\t\t\tcolor: llms-color( llms-blue );\n\t\t\tdisplay: block;\n\t\t\tfont-size: 30px;\n\t\t\tmargin-bottom: 10px;\n\t\t}\n\t\t&.deactivate .fa {\n\t\t\tcolor: #777;\n\t\t}\n\t}\n\n}\n\n.llms-addons-wrap {\n\tdisplay: grid;\n\tgrid-gap: 30px;\n\tgrid-template-columns: 1fr;\n\tjustify-content: space-between;\n\n\t@media only screen and ( min-width: 782px ) {\n\t\tgrid-template-columns: 1fr 1fr 1fr;\n\t}\n\n\t.llms-add-on-item {\n\t\tbackground-color: #FFF;\n\t\tborder: 1px solid #dedede;\n\t\tborder-radius: 12px;\n\t\tbox-shadow: 0px 0px 1px rgba(48, 49, 51, 0.05), 0px 2px 4px rgba(48, 49, 51, 0.1);\n\t\tlist-style: none;\n\t\tmargin: 0;\n\t\toverflow: hidden;\n\n\t\t@media only screen and ( min-width: 680px ) {\n\n\t\t}\n\t}\n\n\t.llms-add-on {\n\t\tdisplay: flex;\n\t\tflex-flow: column no-wrap;\n\t\theight: 100%;\n\n\t\t.llms-add-on-link {\n\t\t\tcolor: #444;\n\t\t\tdisplay: block;\n\t\t\ttext-decoration: none;\n\t\t}\n\n\t\theader {\n\t\t\tmargin-bottom: 0;\n\t\t\th4 {\n\t\t\t\tcolor: #1d2327;\n\t\t\t\tfont-size: 20px;\n\t\t\t\tline-height: 1.5;\n\t\t\t\tmargin: 30px 20px 20px 20px;\n\t\t\t}\n\t\t\timg {\n\t\t\t\taspect-ratio: 16 / 9;\n\t\t\t\tdisplay: block;\n\t\t\t\theight: auto;\n\t\t\t\twidth: 100%;\n\t\t\t}\n\t\t}\n\n\t\tsection {\n\t\t\tpadding: 0 20px;\n\t\t\tp {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tline-height: 1.5;\n\t\t\t\tmargin: 0 0 15px;\n\t\t\t\ttext-align: left;\n\t\t\t}\n\t\t\tul, li {\n\t\t\t\tfont-size: 15px;\n\t\t\t\tline-height: 1.5;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 0;\n\n\t\t\t\t:first-child {\n\t\t\t\t\tfont-weight: 700;\n\t\t\t\t}\n\t\t\t}\n\t\t\timg {\n\t\t\t\tborder-radius: 50%;\n\t\t\t\tdisplay: inline-block;\n\t\t\t\theight: 20px;\n\t\t\t\twidth: 20px;\n\t\t\t\tvertical-align: text-bottom;\n\t\t\t}\n\t\t}\n\n\t\tfooter.llms-actions {\n\t\t\tmargin-top: auto;\n\t\t\tpadding: 20px;\n\n\t\t\ta.open-plugin-details-modal {\n\t\t\t\tfont-size: 18px;\n\t\t\t\tpadding: 5px;\n\t\t\t\tvertical-align: middle;\n\t\t\t}\n\n\t\t\t.llms-status-icon {\n\t\t\t\tbackground-color: #e1e1e1;\n\t\t\t\tborder: none;\n\t\t\t\tborder-radius: 8px;\n\t\t\t\tcolor: #414141;\n\t\t\t\tcursor: pointer;\n\t\t\t\tdisplay: block;\n\t\t\t\tfont-size: 16px;\n\t\t\t\tfont-weight: 700;\n\t\t\t\ttext-decoration: none;\n\t\t\t\ttext-shadow: none;\n\t\t\t\tline-height: 1;\n\t\t\t\tmargin: 10px 0;\n\t\t\t\tmax-width: 100%;\n\t\t\t\tpadding: 8px 14px;\n\t\t\t\t-webkit-transition: all .5s ease;\n\t\t\t\ttransition: all .5s ease;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tvertical-align: middle;\n\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground-color: #cdcdcd;\n\t\t\t\t\t.fa.show-on-hover { display: inline-block; }\n\t\t\t\t\t.fa.hide-on-hover { display: none; }\n\t\t\t\t}\n\n\t\t\t\t.fa {\n\t\t\t\t\tcolor: #414141;\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\theight: 16px;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tmargin-right: 2px;\n\t\t\t\t\twidth: 16px;\n\t\t\t\t}\n\n\t\t\t\t.fa.show-on-hover { display: none; }\n\t\t\t\t.fa.hide-on-hover { display: inline-block; }\n\n\t\t\t\tinput,\n\t\t\t\tinput + .fa {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t\tcolor: llms-color( llms-blue ) !important;\n\t\t\t\t}\n\n\t\t\t\tinput:checked + .fa {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\t& + .fa {\n\t\t\t\t\t\tdisplay: none;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.llms-status-text {\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t}\n\n\n\t\t\t\t&.status--installed,\n\t\t\t\t&.status--license_active {\n\t\t\t\t\t.fa {\n\t\t\t\t\t\tcolor: llms-color( wp-green-50 );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// &.status--uninstalled,\n\t\t\t\t&.status--active,\n\t\t\t\t&.status--license_inactive {\n\t\t\t\t\t.fa {\n\t\t\t\t\t\tcolor: llms-color( wp-red-50 );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t&.external.status--none,\n\t\t\t\t&.external.status--license_active, // fixes xapi\n\t\t\t\t&.external.status--license_inactive { // fixes xapi\n\t\t\t\t\t.fa {\n\t\t\t\t\t\tcolor: llms-color( llms-blue );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t.llms-button-secondary {\n\t\t\t\tborder: 1px solid #b7b7b7;\n\t\t\t\tborder-radius: 4px;\n\t\t\t\tfloat: right;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: #f0f0f0;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.llms-addon-actions {\n\t\t\t\tbackground: #f0f0f0;\n\t\t\t\tborder: 1px solid #b7b7b7;\n\t\t\t\tbox-shadow: inset 0 1px 0 rgba(255,255,255,.2), inset 0 -1px 0 rgba(0,0,0,.1);\n\t\t\t\tdisplay: none;\n\t\t\t\tleft: 16px;\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 16px;\n\t\t\t\tposition: absolute;\n\t\t\t\tright: 16px;\n\t\t\t\tz-index: 1;\n\t\t\t\t&:before, &:after {\n\t\t\t\t\tcontent: '';\n\t\t\t\t\tposition: absolute;\n\t\t\t\t}\n\t\t\t\t&:before {\n\t\t\t\t\tborder: 10px solid transparent;\n\t\t\t\t\tborder-bottom-color: #b7b7b7;\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: -20px;\n\t\t\t\t\tright: 34px;\n\t\t\t\t}\n\t\t\t\t&:after {\n\t\t\t\t\tborder: 8px solid transparent;\n\t\t\t\t\tborder-bottom-color: #f0f0f0;\n\t\t\t\t\ttop: -16px;\n\t\t\t\t\tright: 36px;\n\t\t\t\t}\n\n\t\t\t\tli {\n\t\t\t\t\tmargin-bottom: 8px;\n\t\t\t\t\t&:last-child {\n\t\t\t\t\t\tmargin-bottom: 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n\n@media only screen and (min-width: 782px) {\n\t.wrap.lifterlms-addons {\n\n\t\t.llms-subheader {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row;\n\t\t\theight: 40px;\n\t\t\tposition: sticky;\n\t\t\ttop: 32px;\n\n\t\t\th1 {\n\t\t\t\tmargin-bottom: 0;\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t.llms-addons-wrap {\n\n\t\t.llms-add-on {\n\n\t\t\tfooter.llms-actions {\n\n\t\t\t\t.llms-status-icon {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tmargin: 10px 5px 0 0;\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "src/scss/fontawesome.scss",
    "content": "@import '@lifterlms/fontawesome/src/fontawesome';\n"
  },
  {
    "path": "templates/achievements/loop.php",
    "content": "<?php\n/**\n * Achievements Loop\n *\n * @package LifterLMS/Templates/Achievements\n *\n * @since 3.14.0\n * @version 6.0.0\n *\n * @param LLMS_User_Achievements[] $achievements List of achievements to display.\n * @param int                      $cols         Number of columns to display.\n * @param false|array             $pagination   Pagination arguments to pass to {@see llms_paginate_links()} or `false`\n *                                              when pagination is disabled.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php\n\t/**\n\t * Action run prior to the output of an achievement list.\n\t *\n\t * @since 3.14.0\n\t */\n\tdo_action( 'llms_before_achievement_loop' );\n?>\n\n<?php if ( $achievements ) : ?>\n\n\t<ul class=\"llms-achievements-loop listing-achievements <?php echo esc_attr( sprintf( 'loop-cols-%d', $cols ) ); ?>\">\n\n\t\t<?php foreach ( $achievements as $achievement ) : ?>\n\n\t\t\t<li class=\"llms-achievement-loop-item achievement-item\">\n\t\t\t\t<?php\n\t\t\t\t\t/**\n\t\t\t\t\t * Action run to display a single achievement.\n\t\t\t\t\t *\n\t\t\t\t\t * @since 3.14.0\n\t\t\t\t\t *\n\t\t\t\t\t * @see llms_the_achievement() Hooked at priority 10.\n\t\t\t\t\t *\n\t\t\t\t\t * @param LLMS_User_Achievement $achievement Achievement object to display.\n\t\t\t\t\t */\n\t\t\t\t\tdo_action( 'llms_achievement_content', $achievement );\n\t\t\t\t?>\n\t\t\t</li>\n\n\t\t<?php endforeach; ?>\n\n\t</ul>\n\n<?php else : ?>\n\n\t<p>\n\t<?php\n\t\t/**\n\t\t * Filters the message displayed when no achievements have been earned by the student.\n\t\t *\n\t\t * @since 3.14.0\n\t\t *\n\t\t * @param string $message Message text..\n\t\t */\n\t\techo wp_kses_post( apply_filters( 'lifterlms_no_achievements_text', __( 'You do not have any achievements yet. Enroll in a course to get started!', 'lifterlms' ) ) );\n\t?>\n\t</p>\n\n<?php endif; ?>\n\n<?php if ( $pagination ) : ?>\n\t<?php\n\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\techo llms_paginate_links( $pagination );\n\t?>\n<?php endif; ?>\n\n<?php\n\t/**\n\t * Action run after to the output of an achievement list.\n\t *\n\t * @since 3.14.0\n\t */\n\tdo_action( 'llms_after_achievement_loop' );\n?>\n"
  },
  {
    "path": "templates/achievements/template.php",
    "content": "<?php\n/**\n * Single Achievement Template\n *\n * @package LifterLMS/Templates/Achievements\n *\n * @since 1.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n\n<a class=\"llms-achievement\" data-id=\"<?php echo esc_attr( $achievement->get( 'id' ) ); ?>\" href=\"#<?php printf( 'achievement-%d', intval( $achievement->get( 'id' ) ) ); ?>\" id=\"<?php printf( 'llms-achievement-%d', intval( $achievement->get( 'id' ) ) ); ?>\">\n\n\t<?php do_action( 'lifterlms_before_achievement', $achievement ); ?>\n\n\t<div class=\"llms-achievement-image\"><?php echo $achievement->get_image_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in method. ?></div>\n\n\t<h4 class=\"llms-achievement-title\"><?php echo esc_html( $achievement->get( 'title' ) ); ?></h4>\n\n\t<div class=\"llms-achievement-info\">\n\t\t<div class=\"llms-achievement-content\"><?php echo wp_kses_post( $achievement->get( 'content' ) ); ?></div>\n\t\t<div class=\"llms-achievement-date\"><?php printf( esc_html_x( 'Awarded on %s', 'achievement earned date', 'lifterlms' ), esc_html( $achievement->get_earned_date() ) ); ?></div>\n\t</div>\n\n\t<?php do_action( 'lifterlms_after_achievement', $achievement ); ?>\n\n</a>\n\n"
  },
  {
    "path": "templates/admin/analytics/analytics.php",
    "content": "<?php\n/**\n * Analytics\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since    3.0.0\n * @version  3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n?>\n\n<div class=\"wrap lifterlms llms-analytics-wrap\">\n\n\t<form action=\"<?php echo esc_url( admin_url( 'admin.php' ) ); ?>\" class=\"llms-analytics-nav\" id=\"llms-analytics-filters-form\" method=\"GET\">\n\n\t\t<nav class=\"llms-nav-tab-wrapper\">\n\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t<?php foreach ( $tabs as $name => $label ) : ?>\n\n\t\t\t\t<?php $current_tab_class = ( $current_tab == $name ) ? ' llms-active' : ''; ?>\n\t\t\t\t<li class=\"llms-nav-item<?php echo esc_attr( $current_tab_class ); ?>\"><a class=\"llms-nav-link\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-analytics&tab=' . $name ) ); ?>\"><?php echo esc_html( $label ); ?></a>\n\n\t\t\t<?php endforeach; ?>\n\t\t\t</ul>\n\n\t\t</nav>\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\" id=\"llms-date-quick-filters\">\n\n\t\t\t<ul class=\"llms-nav-items\">\n\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( 'this-year' == $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" data-range=\"this-year\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-analytics&tab=' . $current_tab . '&range=this-year' ) ); ?>\"><?php esc_html_e( 'This Year', 'lifterlms' ); ?></a>\n\t\t\t\t</li>\n\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( 'last-month' == $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" data-range=\"last-month\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-analytics&tab=' . $current_tab . '&range=last-month' ) ); ?>\"><?php esc_html_e( 'Last Month', 'lifterlms' ); ?></a>\n\t\t\t\t</li>\n\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( 'this-month' == $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" data-range=\"this-month\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-analytics&tab=' . $current_tab . '&range=this-month' ) ); ?>\"><?php esc_html_e( 'This Month', 'lifterlms' ); ?></a>\n\t\t\t\t</li>\n\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( 'last-7-days' == $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" data-range=\"last-7-days\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-analytics&tab=' . $current_tab . '&range=last-7-days' ) ); ?>\"><?php esc_html_e( 'Last 7 Days', 'lifterlms' ); ?></a>\n\t\t\t\t</li>\n\n\n\t\t\t\t<li class=\"llms-nav-item llms-analytics-form<?php echo ( 'custom' == $current_range ) ? ' llms-active' : ''; ?>\">\n\n\t\t\t\t\t<label><?php esc_html_e( 'Custom', 'lifterlms' ); ?></label>\n\t\t\t\t\t<input type=\"text\" name=\"date_start\" class=\"llms-datepicker\" placeholder=\"yyyy-mm-dd\" value=\"<?php echo esc_attr( $date_start ); ?>\"> -\n\t\t\t\t\t<input type=\"text\" name=\"date_end\" class=\"llms-datepicker\" placeholder=\"yyyy-mm-dd\" value=\"<?php echo esc_attr( $date_end ); ?>\">\n\n\t\t\t\t\t<button class=\"button small\" id=\"llms-custom-date-submit\" type=\"submit\"><?php esc_html_e( 'Go', 'lifterlms' ); ?></a>\n\t\t\t\t</li>\n\n\t\t\t\t<li class=\"llms-nav-item llms-nav-item-right\">\n\t\t\t\t\t<a class=\"llms-nav-link\" href=\"#llms-toggle-filters\"><span class=\"dashicons dashicons-filter\"></span><?php esc_html_e( 'Toggle Filters', 'lifterlms' ); ?></a>\n\t\t\t\t</li>\n\n\t\t\t</ul>\n\n\t\t</nav>\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary llms-analytics-filters\"<?php echo ( $current_students || $current_courses || $current_memberships ) ? ' style=\"display:block;\"' : ''; ?>>\n\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t\t<li class=\"llms-nav-item llms-analytics-form\">\n\n\t\t\t\t\t<label><?php esc_html_e( 'Students', 'lifterlms' ); ?></label>\n\n\t\t\t\t\t<select id=\"llms-students-ids-filter\" name=\"student_ids[]\" multiple=\"multiple\">\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * todo: do a better job on this loop for scalability...\n\t\t\t\t\t\t */\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<?php foreach ( $current_students as $id ) : ?>\n\t\t\t\t\t\t\t<?php $s = get_user_by( 'id', $id ); ?>\n\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $id ); ?>\" selected=\"selected\"><?php echo esc_html( $s->display_name ); ?> &lt;<?php echo esc_html( $s->user_email ); ?>&gt;</option>\n\t\t\t\t\t\t<?php endforeach; ?>\n\n\t\t\t\t\t</select>\n\n\t\t\t\t</li>\n\n\t\t\t\t<li class=\"llms-nav-item llms-analytics-form\">\n\n\t\t\t\t\t<label><?php esc_html_e( 'Courses', 'lifterlms' ); ?></label>\n\n\t\t\t\t\t<select class=\"llms-select2-post\" data-placeholder=\"<?php esc_attr_e( 'Filter by Course(s)', 'lifterlms' ); ?>\" data-post-type=\"course\" id=\"llms-course-ids-filter\" name=\"course_ids[]\" multiple=\"multiple\">\n\t\t\t\t\t\t<?php foreach ( $current_courses as $course_id ) : ?>\n\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $course_id ); ?>\" selected><?php echo esc_html( get_the_title( $course_id ) ); ?> <?php printf( esc_html__( '(ID# %d)', 'lifterlms' ), esc_html( $course_id ) ); ?></option>\n\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t</select>\n\n\t\t\t\t</li>\n\n\t\t\t\t<li class=\"llms-nav-item llms-analytics-form\">\n\n\t\t\t\t\t<label><?php esc_html_e( 'Memberships', 'lifterlms' ); ?></label>\n\n\t\t\t\t\t<select class=\"llms-select2-post\" data-placeholder=\"<?php esc_attr_e( 'Filter by Memberships(s)', 'lifterlms' ); ?>\" data-post-type=\"llms_membership\" id=\"llms-membership-ids-filter\" name=\"membership_ids[]\" multiple=\"multiple\">\n\t\t\t\t\t\t<?php foreach ( $current_memberships as $membership_id ) : ?>\n\t\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $membership_id ); ?>\" selected><?php echo esc_html( get_the_title( $membership_id ) ); ?> <?php printf( esc_html__( '(ID# %d)', 'lifterlms' ), esc_html( $membership_id ) ); ?></option>\n\t\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t</select>\n\n\t\t\t\t</li>\n\n\t\t\t\t<li class=\"llms-nav-item llms-analytics-form\">\n\t\t\t\t\t<button class=\"button\" type=\"submit\"><?php esc_html_e( 'Apply Filters', 'lifterlms' ); ?></a>\n\t\t\t\t</li>\n\n\t\t\t</ul>\n\t\t</nav>\n\n\t\t<input type=\"hidden\" name=\"range\" value=\"<?php echo esc_attr( $current_range ); ?>\">\n\t\t<input type=\"hidden\" name=\"tab\" value=\"<?php echo esc_attr( $current_tab ); ?>\">\n\t\t<input type=\"hidden\" name=\"page\" value=\"llms-analytics\">\n\n\t</form>\n\n\t<h1 style=\"display:none;\"></h1><!-- find a home for admin notices -->\n\n\t<div class=\"llms-options-page-contents\">\n\n\t\t<?php foreach ( $widget_data as $row => $widgets ) : ?>\n\t\t\t<div class=\"llms-widget-row llms-widget-row-<?php esc_attr( $row ); ?>\">\n\t\t\t<?php foreach ( $widgets as $id => $opts ) : ?>\n\n\t\t\t\t<div class=\"llms-widget-<?php echo esc_attr( $opts['cols'] ); ?>\">\n\t\t\t\t\t<div class=\"llms-widget is-loading\" data-method=\"<?php echo esc_attr( $id ); ?>\" id=\"llms-widget-<?php echo esc_attr( $id ); ?>\">\n\n\t\t\t\t\t\t<p class=\"llms-label\"><?php echo esc_html( $opts['title'] ); ?></p>\n\t\t\t\t\t\t<h1><?php echo esc_html( $opts['content'] ); ?></h1>\n\n\t\t\t\t\t\t<span class=\"spinner\"></span>\n\n\t\t\t\t\t\t<i class=\"fa fa-info-circle llms-widget-info-toggle\"></i>\n\t\t\t\t\t\t<div class=\"llms-widget-info\">\n\t\t\t\t\t\t\t<p><?php echo esc_html( $opts['info'] ); ?></p>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t<?php endforeach; ?>\n\t\t\t</div>\n\t\t<?php endforeach; ?>\n\n\t\t<div class=\"llms-charts-wrapper\" id=\"llms-charts-wrapper\"></div>\n\n\t</div>\n\n\t<div id=\"llms-analytics-json\" style=\"display:none;\"><?php echo esc_html( $json ); ?></div>\n\n</div>\n"
  },
  {
    "path": "templates/admin/notices/staging.php",
    "content": "<?php\n/**\n * Staging Site Recurring Payment Notice\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.0.2\n * @version 3.0.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n?>\n\n<p><strong><?php echo esc_html__( 'It looks like you may have installed LifterLMS on a staging site!', 'lifterlms' ); ?></strong></p>\n\n<p><?php esc_html_e( 'LifterLMS watches for potential signs of a staging site and disables automatic payments so that your students do not receive duplicate charges.', 'lifterlms' ); ?></p>\n\n<p>\n<?php\nprintf(\n\tesc_html__( 'You can choose to enable automatic recurring payments using the buttons below. If you\\'re not sure what to do, you can learn more %1$shere%2$s. You can always change your mind later by clicking \"Reset Automatic Payments\" on the LifterLMS General Settings screen under Tools and Utilities.', 'lifterlms' ),\n\t'<a href=\"https://lifterlms.com/docs/staging-sites-and-lifterlms-recurring-payments\" target=\"_blank\">',\n\t'</a>'\n);\n?>\n</p>\n\n<p>\n\t<a class=\"button-primary\" href=\"<?php echo esc_url( wp_nonce_url( add_query_arg( 'llms-staging-status', 'disable', admin_url( 'admin.php?page=llms-settings' ) ), 'llms_staging_status', '_llms_staging_nonce' ) ); ?>\"><?php echo esc_html__( 'Leave Automatic Payments Disabled', 'lifterlms' ); ?></a>\n\t&nbsp;&nbsp;\n\t<a class=\"button\" href=\"<?php echo esc_url( wp_nonce_url( add_query_arg( 'llms-staging-status', 'enable', admin_url( 'admin.php?page=llms-settings' ) ), 'llms_staging_status', '_llms_staging_nonce' ) ); ?>\"><?php echo esc_html__( 'Enable Automatic Payments', 'lifterlms' ); ?></a>\n</p>\n"
  },
  {
    "path": "templates/admin/post-types/order-transactions.php",
    "content": "<?php\n/**\n * Transactions Table Metabox for Orders\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.5.0\n * @since 3.26.1 Unknown.\n * @since 4.21.2 Don't localize the price \"step\" html attribute.\n * @version 4.21.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n// Create a \"step\" attribute for price fields according to LLMS settings.\n$price_step = number_format( 0.01, get_lifterlms_decimals() );\n\n?>\n<table class=\"llms-table\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th><?php esc_html_e( 'ID', 'lifterlms' ); ?></th>\n\t\t\t<th><?php esc_html_e( 'Status', 'lifterlms' ); ?></th>\n\t\t\t<th><?php esc_html_e( 'Date', 'lifterlms' ); ?></th>\n\t\t\t<th><?php esc_html_e( 'Amount', 'lifterlms' ); ?></th>\n\t\t\t<th><?php esc_html_e( 'Refunded', 'lifterlms' ); ?></th>\n\t\t\t<th class=\"expandable closed\"><?php esc_html_e( 'Type', 'lifterlms' ); ?></th>\n\t\t\t<th class=\"expandable closed\"><?php esc_html_e( 'Gateway', 'lifterlms' ); ?></th>\n\t\t\t<th class=\"expandable closed\"><?php esc_html_e( 'Source', 'lifterlms' ); ?></th>\n\t\t\t<th class=\"expandable closed\"><?php esc_html_e( 'Transaction ID', 'lifterlms' ); ?></th>\n\t\t\t<th class=\"expandable\"><?php esc_html_e( 'Actions', 'lifterlms' ); ?></th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t<?php if ( $transactions['transactions'] ) : ?>\n\t\t\t<?php foreach ( $transactions['transactions'] as $txn ) : ?>\n\t\t\t\t<?php $gateway = $txn->get_gateway(); ?>\n\t\t\t\t<?php $refund_amount = $txn->get_price( 'refund_amount', array(), 'float' ); ?>\n\t\t\t\t<tr class=\"<?php echo esc_attr( $txn->get( 'status' ) ); ?>\" data-transaction-id=\"<?php echo esc_attr( $txn->get( 'id' ) ); ?>\">\n\t\t\t\t\t<td><?php echo esc_html( $txn->get( 'id' ) ); ?></td>\n\t\t\t\t\t<td><?php echo esc_html( $txn->get_status_name() ); ?></td>\n\t\t\t\t\t<td><?php echo esc_html( $txn->get_date( 'date', 'm/d/Y h:ia' ) ); ?></td>\n\t\t\t\t\t<td>\n\n\t\t\t\t\t\t<?php if ( $refund_amount ) : ?>\n\t\t\t\t\t\t\t<del><?php echo wp_kses( $txn->get_price( 'amount' ), LLMS_ALLOWED_HTML_PRICES ); ?></del>\n\t\t\t\t\t\t\t<?php echo wp_kses( $txn->get_net_amount(), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t<?php echo wp_kses( $txn->get_price( 'amount' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t</td>\n\t\t\t\t\t<td><?php echo wp_kses( llms_price( $refund_amount * -1 ), LLMS_ALLOWED_HTML_PRICES ); ?></td>\n\t\t\t\t\t<td class=\"expandable closed\"><?php echo esc_html( $txn->translate( 'payment_type' ) ); ?></td>\n\t\t\t\t\t<td class=\"expandable closed\"><?php echo $gateway ? esc_html( $gateway->get_admin_title() ) : esc_html( $txn->get( 'payment_gateway' ) ); ?></td>\n\t\t\t\t\t<td class=\"expandable closed\">\n\t\t\t\t\t\t<?php echo esc_html( $txn->get( 'gateway_source_description' ) ); ?>\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t$source_id = $txn->get( 'gateway_source_id' );\n\t\t\t\t\t\tif ( $source_id ) :\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t<?php $source = $gateway ? $gateway->get_source_url( $source_id ) : false; ?>\n\t\t\t\t\t\t\t<?php if ( false === filter_var( $source, FILTER_VALIDATE_URL ) ) : ?>\n\t\t\t\t\t\t\t\t(<?php echo esc_html( $source ); ?>)\n\t\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t\t(<a href=\"<?php echo esc_url( $source ); ?>\" target=\"_blank\"><?php echo esc_html( $source_id ); ?></a>)\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"expandable closed\">\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t$txn_id = $txn->get( 'gateway_transaction_id' );\n\t\t\t\t\t\tif ( $txn_id ) :\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t\t<?php $txn_url = $gateway ? $gateway->get_transaction_url( $txn_id, $txn->get( 'api_mode' ) ) : false; ?>\n\t\t\t\t\t\t\t<?php if ( false === filter_var( $txn_url, FILTER_VALIDATE_URL ) ) : ?>\n\t\t\t\t\t\t\t\t<?php echo esc_html( $txn_id ); ?>\n\t\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t\t<a href=\"<?php echo esc_url( $txn_url ); ?>\" target=\"_blank\"><?php echo esc_html( $txn_id ); ?></a>\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td class=\"expandable\">\n\t\t\t\t\t\t<?php if ( $txn->can_be_refunded() ) : ?>\n\t\t\t\t\t\t\t<button class=\"button\" data-gateway=\"<?php echo $gateway ? esc_attr( $gateway->get_admin_title() ) : ''; ?>\" data-gateway-supports=\"<?php echo esc_attr( $gateway && $gateway->supports( 'refunds' ) ); ?>\" data-refundable=\"<?php echo wp_kses( $txn->get_refundable_amount(), LLMS_ALLOWED_HTML_PRICES ); ?>\" name=\"llms-refund-toggle\" type=\"button\"><?php esc_html_e( 'Refund', 'lifterlms' ); ?></button>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<button class=\"button\" name=\"llms_resend_receipt\" type=\"submit\" value=\"<?php echo esc_attr( $txn->get( 'id' ) ); ?>\"><?php esc_html_e( 'Resend Receipt', 'lifterlms' ); ?></button>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t<?php endforeach; ?>\n\t\t<?php endif; ?>\n\t</tbody>\n\t<tfoot>\n\t\t<tr>\n\t\t\t<th colspan=\"10\">\n\n\t\t\t\t<?php if ( ! empty( $prev_url ) ) : ?>\n\t\t\t\t\t<a class=\"button\" href=\"<?php echo esc_url( $prev_url ); ?>\"><?php printf( esc_html__( '%s Newer', 'lifterlms' ), '&laquo;' ); ?></a>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( ! empty( $next_url ) ) : ?>\n\t\t\t\t\t<a class=\"button\" href=\"<?php echo esc_url( $next_url ); ?>\"><?php printf( esc_html__( 'Older %s', 'lifterlms' ), '&raquo;' ); ?></a>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( ! empty( $all_url ) ) : ?>\n\t\t\t\t\t<a class=\"button\" href=\"<?php echo esc_url( $all_url ); ?>\"><?php printf( esc_html__( 'View all', 'lifterlms' ), '&raquo;' ); ?></a>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<button class=\"button\" name=\"llms-manual-txn-toggle\" type=\"button\"><?php esc_html_e( 'Record a Manual Payment', 'lifterlms' ); ?></button>\n\t\t\t\t<button class=\"button\" data-text=\"<?php esc_attr_e( 'Show Less Info', 'lifterlms' ); ?>\" name=\"llms-expand-table\" type=\"button\"><?php esc_html_e( 'Show More Info', 'lifterlms' ); ?></button>\n\t\t\t</th>\n\t\t</tr>\n\t</tfoot>\n</table>\n\n<table id=\"llms-txn-refund-model\" style=\"display:none;\">\n\t<tr class=\"llms-txn-refund-form\"><td colspan=\"10\">\n\t<div class=\"llms-metabox\">\n\n\t\t<div class=\"llms-metabox-section\">\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Refund Amount:', 'lifterlms' ); ?></label>\n\t\t\t\t<input disabled=\"disabled\" name=\"llms_refund_amount\" min=\"0\" step=\"<?php echo esc_attr( $price_step ); ?>\" type=\"number\">\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Refund Note (optional):', 'lifterlms' ); ?></label>\n\t\t\t\t<input disabled=\"disabled\" name=\"llms_refund_note\" type=\"text\">\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<button class=\"button button-primary tooltip\" data-gateway=\"manual\" name=\"llms_process_refund\" title=\"<?php esc_attr_e( 'The refund will be recorded and you will need to manually issue a refund', 'lifterlms' ); ?>\" value=\"manual\"><?php esc_html_e( 'Refund Manually', 'lifterlms' ); ?></button>\n\t\t\t\t<button class=\"button button-primary gateway-btn\" data-gateway=\"0\" name=\"llms_process_refund\" style=\"display:none;\" value=\"gateway\"><?php printf( esc_html_x( 'Refund via %s', 'refund via payment gateway', 'lifterlms' ), '<span class=\"llms-gateway-title\"></span>' ); ?></button>\n\t\t\t</div>\n\n\t\t\t<input disabled=\"disabled\" type=\"hidden\" name=\"llms_refund_txn_id\">\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t</div>\n\t</td></tr>\n</table>\n\n\n<table id=\"llms-manual-txn-model\" style=\"display:none;\">\n\t<tr class=\"llms-manual-txn-form\"><td colspan=\"10\">\n\t<div class=\"llms-metabox\">\n\n\t\t<div class=\"llms-metabox-section\">\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Payment Amount:', 'lifterlms' ); ?></label>\n\t\t\t\t<input disabled=\"disabled\" name=\"llms_txn_amount\" min=\"0\" step=\"<?php echo esc_attr( $price_step ); ?>\" type=\"number\">\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Payment Source (optional):', 'lifterlms' ); ?></label>\n\t\t\t\t<input disabled=\"disabled\" name=\"llms_txn_source\" type=\"text\">\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Payment Transaction ID (optional):', 'lifterlms' ); ?></label>\n\t\t\t\t<input disabled=\"disabled\" name=\"llms_txn_id\" type=\"text\">\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<label><?php esc_html_e( 'Payment Note (optional):', 'lifterlms' ); ?></label>\n\t\t\t\t<input disabled=\"disabled\" name=\"llms_txn_note\" type=\"text\">\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field\">\n\t\t\t\t<button class=\"button button-primary\" name=\"llms_record_txn\" value=\"llms_record_txn\"><?php esc_html_e( 'Record Payment', 'lifterlms' ); ?></button>\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\n\t\t</div>\n\n\t</div>\n\t</td></tr>\n</table>\n"
  },
  {
    "path": "templates/admin/post-types/students.php",
    "content": "<?php\n/**\n * Students Metabox on admin panel\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.0.0\n * @version 3.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$table = new LLMS_Table_StudentManagement();\n$table->get_results();\n?>\n<div class=\"llms-metabox llms-metabox-students\">\n\n\t<?php do_action( 'lifterlms_before_students_metabox' ); ?>\n\n\t<div class=\"llms-metabox-section llms-metabox-students-enrollments no-top-margin\">\n\t\t<?php $table->output_table_html(); ?>\n\t</div>\n\n\t<?php if ( current_user_can( 'enroll' ) ) : ?>\n\n\t\t<div class=\"llms-metabox-section llms-metabox-students-add-new\">\n\t\t\t<h2><?php echo esc_html__( 'Enroll New Students', 'lifterlms' ); ?></h2>\n\n\t\t\t<div class=\"llms-metabox-field d-all\">\n\t\t\t\t<select id=\"llms-add-student-select\" multiple=\"multiple\" name=\"_llms_add_student\"></select>\n\t\t\t</div>\n\n\t\t\t<div class=\"llms-metabox-field d-all d-right\">\n\t\t\t\t<button class=\"llms-button-primary\" id=\"llms-enroll-students\" type=\"button\"><?php esc_html_e( 'Enroll Students', 'lifterlms' ); ?></button>\n\t\t\t</div>\n\n\t\t\t<div class=\"clear\"></div>\n\t\t</div>\n\n\t<?php endif; ?>\n\n\t<?php do_action( 'lifterlms_after_students_metabox' ); ?>\n\n</div>\n"
  },
  {
    "path": "templates/admin/reporting/nav-filters.php",
    "content": "<?php\n/**\n * Additional Filters used by various reporting screens\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @since 7.6.2 Added `data-post-statuses` attribute to the course and membership filters.\n * @version 7.6.2\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n?>\n\n<nav class=\"llms-nav-tab-wrapper llms-nav-style-filters\" id=\"llms-date-quick-filters\">\n\t<div class=\"llms-inside-wrap\">\n\t\t<ul class=\"llms-nav-items\">\n\n\t\t\t<li class=\"llms-nav-item<?php echo ( 'this-year' === $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t<a class=\"llms-nav-link\" data-range=\"this-year\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=' . $current_tab . '&range=this-year' ) ); ?>\"><?php esc_html_e( 'This Year', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-nav-item<?php echo ( 'last-month' === $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t<a class=\"llms-nav-link\" data-range=\"last-month\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=' . $current_tab . '&range=last-month' ) ); ?>\"><?php esc_html_e( 'Last Month', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-nav-item<?php echo ( 'this-month' === $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t<a class=\"llms-nav-link\" data-range=\"this-month\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=' . $current_tab . '&range=this-month' ) ); ?>\"><?php esc_html_e( 'This Month', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-nav-item<?php echo ( 'last-7-days' === $current_range ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t<a class=\"llms-nav-link\" data-range=\"last-7-days\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=' . $current_tab . '&range=last-7-days' ) ); ?>\"><?php esc_html_e( 'Last 7 Days', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\n\t\t\t<li class=\"llms-nav-item llms-analytics-form<?php echo ( 'custom' === $current_range ) ? ' llms-active' : ''; ?>\">\n\n\t\t\t\t<label><?php esc_html_e( 'Custom', 'lifterlms' ); ?></label>\n\t\t\t\t<input type=\"text\" name=\"date_start\" class=\"llms-datepicker\" placeholder=\"yyyy-mm-dd\" value=\"<?php echo esc_html( $date_start ); ?>\"> -\n\t\t\t\t<input type=\"text\" name=\"date_end\" class=\"llms-datepicker\" placeholder=\"yyyy-mm-dd\" value=\"<?php echo esc_html( $date_end ); ?>\">\n\n\t\t\t\t<button class=\"llms-button-action small\" id=\"llms-custom-date-submit\" type=\"submit\"><?php esc_html_e( 'Go', 'lifterlms' ); ?></button>\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-nav-item llms-nav-item-right\">\n\t\t\t\t<a class=\"llms-nav-link\" href=\"#llms-toggle-filters\"><span class=\"dashicons dashicons-filter\"></span><?php esc_html_e( 'Toggle Filters', 'lifterlms' ); ?></a>\n\t\t\t</li>\n\n\t\t</ul>\n\n\t</div>\n\n</nav>\n\n<nav class=\"llms-analytics-filters\"<?php echo ( $current_students || $current_courses || $current_memberships ) ? ' style=\"display:block;\"' : ''; ?>>\n\t<div class=\"llms-inside-wrap\">\n\t\t<ul class=\"llms-nav-items\">\n\t\t\t<li class=\"llms-nav-item llms-analytics-form\">\n\n\t\t\t\t<label><?php esc_html_e( 'Students', 'lifterlms' ); ?></label>\n\n\t\t\t\t<select id=\"llms-students-ids-filter\" name=\"student_ids[]\" multiple=\"multiple\">\n\t\t\t\t\t<?php\n\t\t\t\t\t/**\n\t\t\t\t\t * todo: do a better job on this loop for scalability...\n\t\t\t\t\t */\n\t\t\t\t\t?>\n\t\t\t\t\t<?php foreach ( $current_students as $id ) : ?>\n\t\t\t\t\t\t<?php $s = get_user_by( 'id', $id ); ?>\n\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $id ); ?>\" selected=\"selected\"><?php echo esc_html( $s->display_name ); ?> &lt;<?php echo esc_html( $s->user_email ); ?>&gt;</option>\n\t\t\t\t\t<?php endforeach; ?>\n\n\t\t\t\t</select>\n\n\t\t\t</li>\n\t\t\t<li class=\"llms-nav-item llms-analytics-form\">\n\t\t\t\t<label><?php esc_html_e( 'Courses', 'lifterlms' ); ?></label>\n\t\t\t\t<select data-post-statuses=\"<?php echo esc_attr( implode( ',', array_keys( get_post_statuses() ) ) . ',future' ); ?>\" class=\"llms-select2-post\" data-placeholder=\"<?php esc_html_e( 'Filter by Course(s)', 'lifterlms' ); ?>\" data-post-type=\"course\" id=\"llms-course-ids-filter\" name=\"course_ids[]\" multiple=\"multiple\">\n\t\t\t\t\t<?php foreach ( $current_courses as $course_id ) : ?>\n\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $course_id ); ?>\" selected><?php echo esc_html( get_the_title( $course_id ) ); ?>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\tprintf(\n\t\t\t\t\t\t\t\t// Translators: %d = Course ID.\n\t\t\t\t\t\t\t\tesc_html__( '(ID# %d)', 'lifterlms' ),\n\t\t\t\t\t\t\t\tesc_html( $course_id )\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</option>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</select>\n\n\t\t\t</li>\n\n\t\t\t<li class=\"llms-nav-item llms-analytics-form\">\n\n\t\t\t\t<label><?php esc_html_e( 'Memberships', 'lifterlms' ); ?></label>\n\n\t\t\t\t<select data-post-statuses=\"<?php echo esc_attr( implode( ',', array_keys( get_post_statuses() ) ) . ',future' ); ?>\" class=\"llms-select2-post\" data-placeholder=\"<?php esc_html_e( 'Filter by Memberships(s)', 'lifterlms' ); ?>\" data-post-type=\"llms_membership\" id=\"llms-membership-ids-filter\" name=\"membership_ids[]\" multiple=\"multiple\">\n\t\t\t\t\t<?php foreach ( $current_memberships as $membership_id ) : ?>\n\t\t\t\t\t\t<option value=\"<?php echo esc_attr( $membership_id ); ?>\" selected><?php echo esc_html( get_the_title( $membership_id ) ); ?>\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\tprintf(\n\t\t\t\t\t\t\t\t// Translators: %d = Membership ID.\n\t\t\t\t\t\t\t\tesc_html__( '(ID# %d)', 'lifterlms' ),\n\t\t\t\t\t\t\t\tesc_html( $membership_id )\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</option>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</select>\n\n\t\t\t</li>\n\t\t</ul>\n\t\t<p><button class=\"llms-button-primary small\" type=\"submit\"><?php esc_html_e( 'Apply Filters', 'lifterlms' ); ?></button></p>\n\t</div>\n</nav>\n\n<input type=\"hidden\" name=\"range\" value=\"<?php echo esc_attr( $current_range ); ?>\">\n<input type=\"hidden\" name=\"tab\" value=\"<?php echo esc_attr( $current_tab ); ?>\">\n<input type=\"hidden\" name=\"page\" value=\"llms-reporting\">\n"
  },
  {
    "path": "templates/admin/reporting/reporting.php",
    "content": "<?php\n/**\n * Reporting Screen Main Template\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @version 3.29.0\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n?>\n<div class=\"wrap lifterlms llms-reporting tab--<?php echo esc_attr( $current_tab ); ?>\">\n\t<form action=\"<?php echo esc_url( admin_url( 'admin.php' ) ); ?>\" class=\"llms-reporting-nav\" method=\"GET\">\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\n\t\t\t<div class=\"llms-inside-wrap\">\n\t\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t\t<?php foreach ( $tabs as $name => $label ) : ?>\n\n\t\t\t\t\t<?php $current_tab_class = ( $current_tab === $name ) ? ' llms-active' : ''; ?>\n\t\t\t\t\t<li class=\"llms-nav-item<?php echo esc_attr( $current_tab_class ); ?>\"><a class=\"llms-nav-link\" href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=' . $name ) ); ?>\"><?php echo esc_html( $label ); ?></a>\n\n\t\t\t\t<?php endforeach; ?>\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t</nav>\n\n\t\t<?php do_action( 'llms_reporting_after_nav', $current_tab ); ?>\n\n\t</form>\n\n\t<h1 style=\"display:none;\"></h1><!-- find a home for admin notices -->\n\n\t<div class=\"llms-options-page-contents\">\n\n\t\t<?php do_action( 'llms_reporting_before_content', $current_tab ); ?>\n\n\t\t<?php do_action( 'llms_reporting_content_' . $current_tab ); ?>\n\n\t\t<?php do_action( 'llms_reporting_after_content', $current_tab ); ?>\n\n\n\t</div>\n\n</div>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/courses/course.php",
    "content": "<?php\n/**\n * Single Course View\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n$img = $course->get_image( array( 64, 64 ) );\n?>\n<section class=\"llms-reporting-tab llms-reporting-course\">\n\n\t<header class=\"llms-reporting-breadcrumbs\">\n\t\t<a href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=courses' ) ); ?>\"><?php esc_html_e( 'Courses', 'lifterlms' ); ?></a>\n\t\t<?php do_action( 'llms_reporting_course_tab_breadcrumbs' ); ?>\n\t</header>\n\n\t<div class=\"llms-reporting-body\">\n\n\t\t<header class=\"llms-reporting-header\">\n\n\t\t\t<?php if ( $img ) : ?>\n\t\t\t\t<div class=\"llms-reporting-header-img\">\n\t\t\t\t\t<img src=\"<?php echo esc_url( $img ); ?>\">\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\t\t\t<div class=\"llms-reporting-header-info\">\n\t\t\t\t<h2><a href=\"<?php echo esc_url( get_edit_post_link( $course->get( 'id' ) ) ); ?>\"><?php echo esc_html( $course->get( 'title' ) ); ?></a></h2>\n\t\t\t</div>\n\n\t\t</header>\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t<?php foreach ( $tabs as $name => $label ) : ?>\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( $current_tab === $name ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" href=\"<?php echo esc_url( LLMS_Admin_Reporting::get_stab_url( $name ) ); ?>\">\n\t\t\t\t\t\t<?php echo wp_kses_post( $label ); ?>\n\t\t\t\t\t</a></li>\n\t\t\t<?php endforeach; ?>\n\t\t\t</ul>\n\t\t</nav>\n\n\t\t<section class=\"llms-gb-tab\">\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/courses/' . $current_tab . '.php',\n\t\t\t\tarray(\n\t\t\t\t\t'course' => $course,\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t</section>\n\n\t</div>\n\n</section>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/courses/overview.php",
    "content": "<?php\n/**\n * Single Course Tab: Overview Subtab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.15.0\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING` and validate the period exists before attempting to use it.\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n\n$data   = new LLMS_Course_Data( $course->get( 'id' ) );\n$period = $data->parse_period();\n$data->set_period( $period );\n$period_text = strtolower( LLMS_Admin_Reporting::get_period_filters()[ $period ] );\n$now         = current_time( 'timestamp' );\n?>\n\n<div class=\"llms-reporting-tab-content\">\n\n\t<section class=\"llms-reporting-tab-main llms-reporting-widgets\">\n\n\t\t<header>\n\n\t\t\t<?php\n\t\t\tLLMS_Admin_Reporting::output_widget_range_filter(\n\t\t\t\t$period,\n\t\t\t\t'courses',\n\t\t\t\tarray(\n\t\t\t\t\t'course_id' => $course->get( 'id' ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t\t<h3><?php esc_html_e( 'Course Overview', 'lifterlms' ); ?></h3>\n\n\t\t</header>\n\t\t<?php\n\n\t\tdo_action( 'llms_reporting_single_course_overview_before_widgets', $course );\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols' => 'd-1of3',\n\t\t\t\t'icon' => 'users',\n\t\t\t\t'id'   => 'llms-reporting-course-total-enrollments',\n\t\t\t\t'data' => $course->get_student_count(),\n\t\t\t\t'text' => __( 'Currently enrolled students', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of3',\n\t\t\t\t'icon'      => 'line-chart',\n\t\t\t\t'id'        => 'llms-reporting-course-avg-progress',\n\t\t\t\t'data'      => $course->get( 'average_progress' ),\n\t\t\t\t'data_type' => 'percentage',\n\t\t\t\t'text'      => __( 'Current average progress', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of3',\n\t\t\t\t'icon'      => 'graduation-cap',\n\t\t\t\t'id'        => 'llms-reporting-course-avg-grade',\n\t\t\t\t'data'      => $course->get( 'average_grade' ),\n\t\t\t\t'data_type' => 'percentage',\n\t\t\t\t'text'      => __( 'Current average grade', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'shopping-cart',\n\t\t\t\t'id'           => 'llms-reporting-course-orders',\n\t\t\t\t'data'         => $data->get_orders( 'current' ),\n\t\t\t\t'data_compare' => $data->get_orders( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'New orders %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'money',\n\t\t\t\t'id'           => 'llms-reporting-course-revenue',\n\t\t\t\t'data'         => $data->get_revenue( 'current' ),\n\t\t\t\t'data_compare' => $data->get_revenue( 'previous' ),\n\t\t\t\t'data_type'    => 'monetary',\n\t\t\t\t'text'         => sprintf( __( 'Total sales %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'smile-o',\n\t\t\t\t'id'           => 'llms-reporting-course-enrollments',\n\t\t\t\t'data'         => $data->get_enrollments( 'current' ),\n\t\t\t\t'data_compare' => $data->get_enrollments( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'New enrollments %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'frown-o',\n\t\t\t\t'id'           => 'llms-reporting-course-unenrollments',\n\t\t\t\t'data'         => $data->get_unenrollments( 'current' ),\n\t\t\t\t'data_compare' => $data->get_unenrollments( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Unenrollments %s', 'lifterlms' ), $period_text ),\n\t\t\t\t'impact'       => 'negative',\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'check-circle',\n\t\t\t\t'id'           => 'llms-reporting-course-lessons-completed',\n\t\t\t\t'data'         => $data->get_lesson_completions( 'current' ),\n\t\t\t\t'data_compare' => $data->get_lesson_completions( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Lessons completed %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'flag-checkered',\n\t\t\t\t'id'           => 'llms-reporting-course-course-completions',\n\t\t\t\t'data'         => $data->get_completions( 'current' ),\n\t\t\t\t'data_compare' => $data->get_completions( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Course completions %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'trophy',\n\t\t\t\t'id'           => 'llms-reporting-course-achievements',\n\t\t\t\t'data'         => $data->get_engagements( 'achievement_earned', 'current' ),\n\t\t\t\t'data_compare' => $data->get_engagements( 'achievement_earned', 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Achievements earned %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'certificate',\n\t\t\t\t'id'           => 'llms-reporting-course-certificates',\n\t\t\t\t'data'         => $data->get_engagements( 'certificate_earned', 'current' ),\n\t\t\t\t'data_compare' => $data->get_engagements( 'certificate_earned', 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Certificates earned %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'envelope',\n\t\t\t\t'id'           => 'llms-reporting-course-email',\n\t\t\t\t'data'         => $data->get_engagements( 'email_sent', 'current' ),\n\t\t\t\t'data_compare' => $data->get_engagements( 'email_sent', 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Emails sent %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tdo_action( 'llms_reporting_single_course_overview_after_widgets', $course );\n\t\t?>\n\n\t</section>\n\n\t<aside class=\"llms-reporting-tab-side\">\n\n\t\t<h3><i class=\"fa fa-bolt\" aria-hidden=\"true\"></i> <?php esc_html_e( 'Recent events', 'lifterlms' ); ?></h3>\n\n\t\t<?php foreach ( $data->recent_events() as $event ) : ?>\n\t\t\t<?php LLMS_Admin_Reporting::output_event( $event, 'course' ); ?>\n\t\t<?php endforeach; ?>\n\n\t</aside>\n\n</div>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/courses/students.php",
    "content": "<?php\n/**\n * Single Course Tab: Students Subtab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.15.0\n * @version 3.15.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n\n$table = new LLMS_Table_Course_Students();\n$table->get_results();\n$table->output_table_html();\n"
  },
  {
    "path": "templates/admin/reporting/tabs/memberships/membership.php",
    "content": "<?php\n/**\n * Single Membership View.\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since    3.32.0\n * @version  3.32.0\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n\n$img = $membership->get_image( array( 64, 64 ) );\n?>\n<section class=\"llms-reporting-tab llms-reporting-membership\">\n\n\t<header class=\"llms-reporting-breadcrumbs\">\n\t\t<a href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=memberships' ) ); ?>\"><?php esc_html_e( 'Memberships', 'lifterlms' ); ?></a>\n\t\t<?php do_action( 'llms_reporting_membership_tab_breadcrumbs' ); ?>\n\t</header>\n\n\t<div class=\"llms-reporting-body\">\n\n\t\t<header class=\"llms-reporting-header\">\n\n\t\t\t<?php if ( $img ) : ?>\n\t\t\t\t<div class=\"llms-reporting-header-img\">\n\t\t\t\t\t<img src=\"<?php echo esc_url( $img ); ?>\">\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\t\t\t<div class=\"llms-reporting-header-info\">\n\t\t\t\t<h2><a href=\"<?php echo esc_url( get_edit_post_link( $membership->get( 'id' ) ) ); ?>\"><?php echo esc_html( $membership->get( 'title' ) ); ?></a></h2>\n\t\t\t</div>\n\n\t\t</header>\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t<?php foreach ( $tabs as $name => $label ) : ?>\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( $current_tab === $name ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" href=\"<?php echo esc_url( LLMS_Admin_Reporting::get_stab_url( $name ) ); ?>\">\n\t\t\t\t\t\t<?php echo wp_kses_post( $label ); ?>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t<?php endforeach; ?>\n\t\t\t</ul>\n\t\t</nav>\n\n\t\t<section class=\"llms-gb-tab\">\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/memberships/' . $current_tab . '.php',\n\t\t\t\tarray(\n\t\t\t\t\t'membership' => $membership,\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t</section>\n\t</div>\n\n</section>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/memberships/overview.php",
    "content": "<?php\n/**\n * Single Membership Tab: Overview Subtab.\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.32.0\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING` and validate the period exists before attempting to use it.\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n\n$data   = new LLMS_Membership_Data( $membership->get( 'id' ) );\n$period = $data->parse_period();\n$data->set_period( $period );\n$period_text = strtolower( LLMS_Admin_Reporting::get_period_filters()[ $period ] );\n$now         = current_time( 'timestamp' );\n?>\n\n<div class=\"llms-reporting-tab-content\">\n\n\t<section class=\"llms-reporting-tab-main llms-reporting-widgets\">\n\n\t\t<header>\n\n\t\t\t<?php\n\t\t\tLLMS_Admin_Reporting::output_widget_range_filter(\n\t\t\t\t$period,\n\t\t\t\t'memberships',\n\t\t\t\tarray(\n\t\t\t\t\t'membership_id' => $membership->get( 'id' ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t\t<h3><?php esc_html_e( 'Membership Overview', 'lifterlms' ); ?></h3>\n\n\t\t</header>\n\t\t<?php\n\n\t\tdo_action( 'llms_reporting_single_membership_overview_before_widgets', $membership );\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols' => 'd-1of3',\n\t\t\t\t'icon' => 'users',\n\t\t\t\t'id'   => 'llms-reporting-membership-total-enrollments',\n\t\t\t\t'data' => $membership->get_student_count(),\n\t\t\t\t'text' => __( 'Currently enrolled students', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'shopping-cart',\n\t\t\t\t'id'           => 'llms-reporting-membership-orders',\n\t\t\t\t'data'         => $data->get_orders( 'current' ),\n\t\t\t\t'data_compare' => $data->get_orders( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'New orders %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'money',\n\t\t\t\t'id'           => 'llms-reporting-membership-revenue',\n\t\t\t\t'data'         => $data->get_revenue( 'current' ),\n\t\t\t\t'data_compare' => $data->get_revenue( 'previous' ),\n\t\t\t\t'data_type'    => 'monetary',\n\t\t\t\t'text'         => sprintf( __( 'Total sales %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'smile-o',\n\t\t\t\t'id'           => 'llms-reporting-membership-enrollments',\n\t\t\t\t'data'         => $data->get_enrollments( 'current' ),\n\t\t\t\t'data_compare' => $data->get_enrollments( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'New enrollments %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'frown-o',\n\t\t\t\t'id'           => 'llms-reporting-membership-unenrollments',\n\t\t\t\t'data'         => $data->get_unenrollments( 'current' ),\n\t\t\t\t'data_compare' => $data->get_unenrollments( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Unenrollments %s', 'lifterlms' ), $period_text ),\n\t\t\t\t'impact'       => 'negative',\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'trophy',\n\t\t\t\t'id'           => 'llms-reporting-membership-achievements',\n\t\t\t\t'data'         => $data->get_engagements( 'achievement_earned', 'current' ),\n\t\t\t\t'data_compare' => $data->get_engagements( 'achievement_earned', 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Achievements earned %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'certificate',\n\t\t\t\t'id'           => 'llms-reporting-membership-certificates',\n\t\t\t\t'data'         => $data->get_engagements( 'certificate_earned', 'current' ),\n\t\t\t\t'data_compare' => $data->get_engagements( 'certificate_earned', 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Certificates earned %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of3',\n\t\t\t\t'icon'         => 'envelope',\n\t\t\t\t'id'           => 'llms-reporting-membership-email',\n\t\t\t\t'data'         => $data->get_engagements( 'email_sent', 'current' ),\n\t\t\t\t'data_compare' => $data->get_engagements( 'email_sent', 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Emails sent %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tdo_action( 'llms_reporting_single_membership_overview_after_widgets', $membership );\n\t\t?>\n\n\t</section>\n\n\t<aside class=\"llms-reporting-tab-side\">\n\n\t\t<h3><i class=\"fa fa-bolt\" aria-hidden=\"true\"></i> <?php esc_html_e( 'Recent events', 'lifterlms' ); ?></h3>\n\n\t\t<?php foreach ( $data->recent_events() as $event ) : ?>\n\t\t\t<?php LLMS_Admin_Reporting::output_event( $event, 'membership' ); ?>\n\t\t<?php endforeach; ?>\n\n\t</aside>\n\n</div>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/memberships/students.php",
    "content": "<?php\n/**\n * Single Membership Tab: Students Subtab.\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.32.0\n * @version 3.32.0\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n\n$table = new LLMS_Table_Membership_Students();\n$table->get_results();\n$table->output_table_html();\n"
  },
  {
    "path": "templates/admin/reporting/tabs/quizzes/attempt.php",
    "content": "<?php\n/**\n * Single Quiz Tab: Single Attempt Subtab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.16.0\n * @since 3.17.3 Unknown.\n * @since 5.3.0 Do not show the \"Start a review\" button, if there are no existing questions to review.\n * @since 7.8.0 Add information on whether the attempt can be resumed or not and disable resume attempt button.\n *\n * @param LLMS_Quiz_Attempt $attempt Quiz attempt object.\n */\n\ndefined( 'ABSPATH' ) || exit;\nif ( ! is_admin() ) {\n\texit;\n}\n\n$student  = $attempt->get_student();\n$siblings = array();\nif ( $student ) {\n\t$siblings = $student->quizzes()->get_attempts_by_quiz(\n\t\t$attempt->get( 'quiz_id' ),\n\t\tarray(\n\t\t\t'per_page' => 10,\n\t\t)\n\t);\n}\n?>\n\n<div class=\"llms-reporting-tab-content\">\n\n\t<section class=\"llms-reporting-tab-main llms-reporting-widgets\">\n\n\t\t<header>\n\t\t\t<h3><?php echo wp_kses_post( $attempt->get_title() ); ?></h3>\n\t\t</header>\n\t\t<?php\n\n\t\tdo_action( 'llms_reporting_single_quiz_attempt_before_widgets', $attempt );\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of4',\n\t\t\t\t'icon'      => 'graduation-cap',\n\t\t\t\t'id'        => 'llms-reporting-quiz-attempt-grade',\n\t\t\t\t'data'      => $attempt->get( 'grade' ),\n\t\t\t\t'data_type' => 'percentage',\n\t\t\t\t'text'      => __( 'Grade', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of4',\n\t\t\t\t'icon'      => 'check-circle',\n\t\t\t\t'id'        => 'llms-reporting-quiz-attempt-correct',\n\t\t\t\t'data'      => sprintf( '%1$d / %2$d', $attempt->get_count( 'correct_answers' ), $attempt->get_count( 'questions' ) ),\n\t\t\t\t'data_type' => 'numeric',\n\t\t\t\t'text'      => __( 'Correct answers', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of4',\n\t\t\t\t'icon'      => 'percent',\n\t\t\t\t'id'        => 'llms-reporting-quiz-attempt-points',\n\t\t\t\t'data'      => sprintf( '%1$d / %2$d', $attempt->get_count( 'points' ), $attempt->get_count( 'available_points' ) ),\n\t\t\t\t'data_type' => 'numeric',\n\t\t\t\t'text'      => __( 'Points earned', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tswitch ( $attempt->get( 'status' ) ) {\n\t\t\tcase 'pass':\n\t\t\t\t$icon = 'star';\n\t\t\t\tbreak;\n\t\t\tcase 'incomplete':\n\t\t\tcase 'fail':\n\t\t\t\t$icon = 'times-circle';\n\t\t\t\tbreak;\n\t\t\tcase 'pending':\n\t\t\t\t$icon = 'clock-o';\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t$icon = 'question-circle';\n\t\t}\n\n\t\tif ( $attempt->can_be_resumed() && $attempt->is_last_attempt() ) {\n\t\t\t$additional = ' - <i>' . esc_html__( 'Can be resumed', 'lifterlms' ) . '</i>';\n\t\t}\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of4',\n\t\t\t\t'icon'      => $icon,\n\t\t\t\t'id'        => 'llms-reporting-quiz-attempt-status',\n\t\t\t\t'data'      => $attempt->l10n( 'status' ) . ( $additional ?? '' ),\n\t\t\t\t'data_type' => 'text',\n\t\t\t\t'text'      => __( 'Status', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of3',\n\t\t\t\t'icon'      => 'sign-in',\n\t\t\t\t'id'        => 'llms-reporting-quiz-attempt-start-date',\n\t\t\t\t'data'      => $attempt->get_date( 'start' ),\n\t\t\t\t'data_type' => 'date',\n\t\t\t\t'text'      => __( 'Start Date', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of3',\n\t\t\t\t'icon'      => 'sign-out',\n\t\t\t\t'id'        => 'llms-reporting-quiz-attempt-end-date',\n\t\t\t\t'data'      => ( 'incomplete' !== $attempt->get( 'status' ) ) ? $attempt->get_date( 'end' ) : '&ndash;',\n\t\t\t\t'data_type' => 'date',\n\t\t\t\t'text'      => __( 'End Date', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols' => 'd-1of3',\n\t\t\t\t'icon' => 'clock-o',\n\t\t\t\t'id'   => 'llms-reporting-quiz-attempt-time',\n\t\t\t\t'data' => ( 'incomplete' !== $attempt->get( 'status' ) ) ? $attempt->get_time() : '&ndash;',\n\t\t\t\t'text' => __( 'Time Elapsed', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tdo_action( 'llms_reporting_single_quiz_attempt_after_widgets', $attempt );\n\t\t?>\n\n\t\t<div class=\"clear\"></div>\n\n\t\t<h3><?php esc_html_e( 'Answers', 'lifterlms' ); ?></h3>\n\n\t\t<form action=\"\" method=\"POST\">\n\n\t\t\t<?php lifterlms_template_quiz_attempt_results_questions_list( $attempt ); ?>\n\n\t\t\t<br><br><br>\n\t\t\t<?php if ( $attempt->get_question_objects( true, true ) ) : // Show the start review button only if there are existing questions to review. ?>\n\t\t\t\t<button class=\"llms-button-primary large\" name=\"llms_quiz_attempt_action\" type=\"submit\" value=\"llms_attempt_grade\">\n\t\t\t\t\t<span class=\"default\">\n\t\t\t\t\t\t<i class=\"fa fa-check-square-o\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t<?php esc_html_e( 'Start a Review', 'lifterlms' ); ?>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"save\">\n\t\t\t\t\t\t<i class=\"fa fa-floppy-o\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t<?php esc_html_e( 'Save Review', 'lifterlms' ); ?>\n\t\t\t\t\t</span>\n\t\t\t</button>\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( $attempt->can_be_resumed() ) : // Show the clear resume attempt button only if quiz can be resumed. ?>\n\t\t\t<button class=\"llms-button-secondary large\" name=\"llms_quiz_attempt_action\" type=\"submit\" value=\"llms_disable_resume_attempt\">\n\t\t\t\t<i class=\"fa fa-ban\" aria-hidden=\"true\"></i>\n\t\t\t\t<?php esc_html_e( 'Disable Resume Attempt', 'lifterlms' ); ?>\n\t\t\t</button>\n\t\t\t<?php endif; ?>\n\t\t\t<button class=\"llms-button-danger large\" name=\"llms_quiz_attempt_action\" type=\"submit\" value=\"llms_attempt_delete\">\n\t\t\t\t<i class=\"fa fa-trash-o\" aria-hidden=\"true\"></i>\n\t\t\t\t<?php esc_html_e( 'Delete Attempt', 'lifterlms' ); ?>\n\t\t\t</button>\n\n\t\t\t<input type=\"hidden\" name=\"llms_attempt_id\" value=\"<?php echo esc_attr( $attempt->get( 'id' ) ); ?>\">\n\n\t\t\t<?php wp_nonce_field( 'llms_quiz_attempt_actions', '_llms_quiz_attempt_nonce' ); ?>\n\n\t\t</form>\n\n\n\t</section>\n\n\t<aside class=\"llms-reporting-tab-side\">\n\n\t\t<h3><i class=\"fa fa-history\" aria-hidden=\"true\"></i> <?php esc_html_e( 'Additional Attempts', 'lifterlms' ); ?></h3>\n\n\t\t<?php foreach ( $siblings as $attempt ) : ?>\n\t\t\t<div class=\"llms-reporting-event quiz_attempt\">\n\n\t\t\t\t<a href=\"\n\t\t\t\t<?php\n\t\t\t\techo esc_url(\n\t\t\t\t\tLLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'attempt_id' => $attempt->get( 'id' ),\n\t\t\t\t\t\t\t'quiz_id'    => $attempt->get( 'quiz_id' ),\n\t\t\t\t\t\t\t'stab'       => 'attempts',\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\t\t\t\t\">\n\n\t\t\t\t\t<?php echo esc_html( sprintf( 'Attempt #%1$s - %2$s', $attempt->get( 'attempt' ), $attempt->get( 'grade' ) . '%' ) ); ?>\n\t\t\t\t\t<br>\n\t\t\t\t\t<time datetime=\"<?php echo esc_attr( $attempt->get( 'update_date' ) ); ?>\"><?php echo esc_html( llms_get_date_diff( current_time( 'timestamp' ), $attempt->get( 'update_date' ), 1 ) ); ?></time>\n\n\t\t\t\t</a>\n\n\t\t\t</div>\n\t\t<?php endforeach; ?>\n\n\t</aside>\n\n</div>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/quizzes/attempts.php",
    "content": "<?php\n/**\n * Single Quiz Tab: Attempts Subtab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.16.0\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @version  3.16.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\nif ( isset( $_GET['attempt_id'] ) ) {\n\n\tllms_get_template(\n\t\t'admin/reporting/tabs/quizzes/attempt.php',\n\t\tarray(\n\t\t\t'attempt' => new LLMS_Quiz_Attempt( llms_filter_input( INPUT_GET, 'attempt_id', FILTER_SANITIZE_NUMBER_INT ) ),\n\t\t)\n\t);\n\n} else {\n\n\t$table = new LLMS_Table_Quiz_Attempts();\n\t$table->get_results(\n\t\tarray(\n\t\t\t'quiz_id' => llms_filter_input( INPUT_GET, 'quiz_id', FILTER_SANITIZE_NUMBER_INT ),\n\t\t)\n\t);\n\t$table->output_table_html();\n\n}\n"
  },
  {
    "path": "templates/admin/reporting/tabs/quizzes/non-attempts.php",
    "content": "<?php\n/**\n * Single Quiz Tab: Non-Attempts Subtab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 9.1.0\n * @version 9.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$table = new LLMS_Table_Quiz_Non_Attempts();\n$table->get_results(\n\tarray(\n\t\t'quiz_id' => llms_filter_input( INPUT_GET, 'quiz_id', FILTER_SANITIZE_NUMBER_INT ),\n\t)\n);\n$table->output_table_html();"
  },
  {
    "path": "templates/admin/reporting/tabs/quizzes/overview.php",
    "content": "<?php\n/**\n * Single Quiz Tab: Overview Subtab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.16.0\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @since 4.10.1 Remove unneded require of the file LLMS_PLUGIN_DIR . 'includes/class.llms.quiz.data.php', the autoloader will do the job.\n * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING` and validate the period exists before attempting to use it.\n * @version 5.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n\n$data   = new LLMS_Quiz_Data( $quiz->get( 'id' ) );\n$period = $data->parse_period();\n$data->set_period( $period );\n$period_text = strtolower( LLMS_Admin_Reporting::get_period_filters()[ $period ] );\n$now         = current_time( 'timestamp' );\n?>\n\n<div class=\"llms-reporting-tab-content\">\n\n\t<section class=\"llms-reporting-tab-main llms-reporting-widgets\">\n\n\t\t<header>\n\n\t\t\t<?php\n\t\t\tLLMS_Admin_Reporting::output_widget_range_filter(\n\t\t\t\t$period,\n\t\t\t\t'quizzes',\n\t\t\t\tarray(\n\t\t\t\t\t'quiz_id' => $quiz->get( 'id' ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t\t<h3><?php esc_html_e( 'Quiz Overview', 'lifterlms' ); ?></h3>\n\n\t\t</header>\n\t\t<?php\n\n\t\tdo_action( 'llms_reporting_single_quiz_overview_before_widgets', $quiz );\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of2',\n\t\t\t\t'icon'         => 'users',\n\t\t\t\t'id'           => 'llms-reporting-quiz-total-attempts',\n\t\t\t\t'data'         => $data->get_attempt_count( 'current' ),\n\t\t\t\t'data_compare' => $data->get_attempt_count( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Attempts %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'         => 'd-1of2',\n\t\t\t\t'icon'         => 'graduation-cap',\n\t\t\t\t'id'           => 'llms-reporting-quiz-avg-grade',\n\t\t\t\t'data'         => $data->get_average_grade( 'current' ),\n\t\t\t\t'data_compare' => $data->get_average_grade( 'previous' ),\n\t\t\t\t'data_type'    => 'percentage',\n\t\t\t\t'text'         => sprintf( __( 'Average grade %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'check-circle',\n\t\t\t\t'id'           => 'llms-reporting-quiz-passes',\n\t\t\t\t'data'         => $data->get_pass_count( 'current' ),\n\t\t\t\t'data_compare' => $data->get_pass_count( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Passed attempts %s', 'lifterlms' ), $period_text ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'icon'         => 'times-circle',\n\t\t\t\t'id'           => 'llms-reporting-quiz-fails',\n\t\t\t\t'data'         => $data->get_fail_count( 'current' ),\n\t\t\t\t'data_compare' => $data->get_fail_count( 'previous' ),\n\t\t\t\t'text'         => sprintf( __( 'Failed attempts %s', 'lifterlms' ), $period_text ),\n\t\t\t\t'impact'       => 'negative',\n\t\t\t)\n\t\t);\n\n\t\tdo_action( 'llms_reporting_single_quiz_overview_after_widgets', $quiz );\n\t\t?>\n\n\t</section>\n\n\t<aside class=\"llms-reporting-tab-side\">\n\n\t\t<h3><i class=\"fa fa-bolt\" aria-hidden=\"true\"></i> <?php esc_html_e( 'Recent events', 'lifterlms' ); ?></h3>\n\n\t\t<em><?php esc_html_e( 'Quiz events coming soon...', 'lifterlms' ); ?></em>\n\n\t</aside>\n\n</div>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/quizzes/quiz.php",
    "content": "<?php\n/**\n * Single Quiz View\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.16.0\n * @version 3.16.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n?>\n<section class=\"llms-reporting-tab llms-reporting-quiz\">\n\n\t<header class=\"llms-reporting-breadcrumbs\">\n\t\t<a href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting&tab=quizzes' ) ); ?>\"><?php esc_html_e( 'Quizzes', 'lifterlms' ); ?></a>\n\t\t<?php do_action( 'llms_reporting_quiz_tab_breadcrumbs' ); ?>\n\t</header>\n\n\t<div class=\"llms-reporting-body\">\n\n\t\t<header class=\"llms-reporting-header\">\n\n\t\t\t<div class=\"llms-reporting-header-info\">\n\t\t\t\t<h2><a href=\"<?php echo esc_url( get_edit_post_link( $quiz->get( 'id' ) ) ); ?>\"><?php echo esc_html( $quiz->get( 'title' ) ); ?></a></h2>\n\t\t\t</div>\n\n\t\t</header>\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t<?php foreach ( $tabs as $name => $label ) : ?>\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( $current_tab === $name ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" href=\"<?php echo esc_url( LLMS_Admin_Reporting::get_stab_url( $name ) ); ?>\">\n\t\t\t\t\t\t<?php echo wp_kses_post( $label ); ?>\n\t\t\t\t\t</a></li>\n\t\t\t<?php endforeach; ?>\n\t\t\t</ul>\n\t\t</nav>\n\n\t\t<section class=\"llms-gb-tab\">\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/quizzes/' . $current_tab . '.php',\n\t\t\t\tarray(\n\t\t\t\t\t'quiz' => $quiz,\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t</section>\n\n\t</div>\n\n</section>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/achievements.php",
    "content": "<?php\n/**\n * Single Student View: Achievements Tab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$table = new LLMS_Table_Achievements();\n$table->get_results(\n\tarray(\n\t\t'student' => $student,\n\t)\n);\n$table->output_table_html();\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/certificates.php",
    "content": "<?php\n/**\n * Single Student View: Certificates Tab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$table = new LLMS_Table_Student_Certificates();\n$table->get_results(\n\tarray(\n\t\t'student' => $student,\n\t)\n);\n$table->output_table_html();\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/courses-course.php",
    "content": "<?php\n/**\n * Single Student View: Courses Tab: Single Course View\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @since 3.36.2 Upgrade UI to utilize reporting widgets.\n *               Add edit link tooltip and update icon.\n *               Add a link to view full course reporting screen.\n * @since 6.0.0 Provide existing hooks with more information and add a new hook.\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\nis_admin() || exit;\n\n$course_id = llms_filter_input( INPUT_GET, 'course_id', FILTER_SANITIZE_NUMBER_INT );\n$course    = llms_get_post( $course_id );\n$table     = new LLMS_Table_Student_Course();\n$table->get_results(\n\tarray(\n\t\t'course_id' => $course_id,\n\t\t'student'   => $student,\n\t)\n);\n\n/**\n * Action run prior to content on the student course reporting screen.\n *\n * @since Unknown\n * @since 6.0.0 Added the `$student` and `$course` parameters.\n *\n * @param LLMS_Student $student Current student.\n * @param LLMS_course  $course  Current course.\n */\ndo_action( 'llms_reporting_student_single_course_before_content', $student, $course );\n?>\n<div class=\"llms-reporting-tab-content\">\n\n\t<section class=\"llms-reporting-tab-main llms-reporting-widgets\">\n\n\t\t<header>\n\t\t\t<h3>\n\t\t\t\t<?php\n\t\t\t\t\t// Translators: %s = Course title.\n\t\t\t\t\techo esc_html( sprintf( __( 'Course: %s', 'lifterlms' ), $course->get( 'title' ) ) );\n\t\t\t\t?>\n\t\t\t\t<?php\n\t\t\t\t\techo wp_kses_post(\n\t\t\t\t\t\t$table->get_post_link(\n\t\t\t\t\t\t\t$course->get( 'id' ),\n\t\t\t\t\t\t\t'<span class=\"tip--top-right\" data-tip=\"' . esc_attr__( 'Edit course', 'lifterlms' ) . '\"><i class=\"fa fa-pencil\" aria-hidden=\"true\"></i></span>'\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\t\t\t\t<a href=\"\n\t\t\t\t<?php\n\t\t\t\techo esc_url(\n\t\t\t\t\tLLMS_Admin_Reporting::get_current_tab_url(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'tab'       => 'courses',\n\t\t\t\t\t\t\t'course_id' => $course_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\t\t\t\t\">\n\t\t\t\t\t<span class=\"tip--top-right\" data-tip=\"<?php esc_attr_e( 'View course reports', 'lifterlms' ); ?>\">\n\t\t\t\t\t\t<i class=\"fa fa-pie-chart\" aria-hidden=\"true\"></i>\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t\t<?php\n\t\t\t\t\t/**\n\t\t\t\t\t * Action run after default action buttons on the student course reporting screen.\n\t\t\t\t\t *\n\t\t\t\t\t * @since 6.0.0\n\t\t\t\t\t *\n\t\t\t\t\t * @param LLMS_Student $student Current student.\n\t\t\t\t\t * @param LLMS_course  $course  Current course.\n\t\t\t\t\t */\n\t\t\t\t\tdo_action( 'llms_reporting_single_student_course_actions', $student, $course );\n\t\t\t\t?>\n\t\t\t</h3>\n\n\t\t</header>\n\t\t<?php\n\t\t/**\n\t\t * Action run before the default widgets on the student course reporting screen.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 6.0.0 Added the `$course` parameter.\n\t\t *\n\t\t * @param LLMS_Student $student Current student.\n\t\t * @param LLMS_course  $course  Current course.\n\t\t */\n\t\tdo_action( 'llms_reporting_single_student_course_before_widgets', $student, $course );\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of5',\n\t\t\t\t'icon'      => 'calendar',\n\t\t\t\t'id'        => 'llms-reporting-student-course-enrollment-date',\n\t\t\t\t'data'      => $student->get_enrollment_date( $course_id, 'enrolled' ),\n\t\t\t\t'data_type' => 'date',\n\t\t\t\t'text'      => __( 'Enrollment Date', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\t$enrollment_status = $student->get_enrollment_status( $course_id );\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of5',\n\t\t\t\t'icon'      => 'enrolled' === $enrollment_status ? 'check-circle' : 'exclamation-triangle',\n\t\t\t\t'id'        => 'llms-reporting-student-course-enrollment-status',\n\t\t\t\t'data'      => llms_get_enrollment_status_name( $enrollment_status ),\n\t\t\t\t'data_type' => 'text',\n\t\t\t\t'text'      => __( 'Enrollment Status', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\t$is_complete = $student->is_complete( $course_id, 'course' );\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of5',\n\t\t\t\t'icon'      => 'calendar',\n\t\t\t\t'id'        => 'llms-reporting-student-course-completed-date',\n\t\t\t\t'data'      => $is_complete ? $student->get_completion_date( $course_id ) : $student->get_enrollment_date( $course_id, 'updated' ),\n\t\t\t\t'data_type' => 'date',\n\t\t\t\t'text'      => $is_complete ? __( 'Completed Date', 'lifterlms' ) : __( 'Enrollment Updated Date', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of5',\n\t\t\t\t'icon'      => 'line-chart',\n\t\t\t\t'id'        => 'llms-reporting-student-course-progress',\n\t\t\t\t'data'      => $student->get_progress( $course_id, 'course' ),\n\t\t\t\t'data_type' => 'percentage',\n\t\t\t\t'text'      => __( 'Progress', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\t$grade = $student->get_grade( $course_id );\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of5',\n\t\t\t\t'icon'      => 'graduation-cap',\n\t\t\t\t'id'        => 'llms-reporting-student-course-grade',\n\t\t\t\t'data'      => $grade,\n\t\t\t\t'data_type' => is_numeric( $grade ) ? 'percentage' : 'text',\n\t\t\t\t'text'      => __( 'Grade', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\t/**\n\t\t * Action run after the default widgets on the student course reporting screen.\n\t\t *\n\t\t * @since Unknown\n\t\t * @since 6.0.0 Added the `$course` parameter.\n\t\t *\n\t\t * @param LLMS_Student $student Current student.\n\t\t * @param LLMS_course  $course  Current course.\n\t\t */\n\t\tdo_action( 'llms_reporting_single_student_course_after_widgets', $student, $course );\n\t\t?>\n\n\t\t<?php $table->output_table_html(); ?>\n\n\t</section>\n\n</div>\n\n<?php\n/**\n * Action run after the content on the student course reporting screen.\n *\n * @since Unknown\n * @since 6.0.0 Added the `$student` and `$course` parameters.\n *\n * @param LLMS_Student $student Current student.\n * @param LLMS_course  $course  Current course.\n */\ndo_action( 'llms_reporting_student_single_course_after_content', $student, $course );\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/courses.php",
    "content": "<?php\n/**\n * Single Student View: Courses Tab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.2.0\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$course_id = llms_filter_input( INPUT_GET, 'course_id', FILTER_SANITIZE_NUMBER_INT );\n\nif ( empty( $course_id ) ) {\n\n\t$table = new LLMS_Table_Student_Courses();\n\t$table->get_results(\n\t\tarray(\n\t\t\t'student' => $student,\n\t\t)\n\t);\n\t$table->output_table_html();\n\n} else {\n\n\t$quiz_id   = llms_filter_input( INPUT_GET, 'quiz_id', FILTER_SANITIZE_NUMBER_INT );\n\t$lesson_id = llms_filter_input( INPUT_GET, 'lesson_id', FILTER_SANITIZE_NUMBER_INT );\n\n\tif ( $quiz_id && $lesson_id ) {\n\n\t\t$table = new LLMS_Table_Quiz_Attempts();\n\t\t$table->get_results(\n\t\t\tarray(\n\t\t\t\t'quiz_id'    => $quiz_id,\n\t\t\t\t'student_id' => llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT ),\n\t\t\t)\n\t\t);\n\t\t$table->output_table_html();\n\n\t} else {\n\n\t\tif ( ! current_user_can( 'edit_post', $course_id ) ) {\n\t\t\twp_die( esc_html__( 'You do not have permission to access this content.', 'lifterlms' ) );\n\t\t}\n\n\t\tllms_get_template(\n\t\t\t'admin/reporting/tabs/students/courses-course.php',\n\t\t\tarray(\n\t\t\t\t'student' => $student,\n\t\t\t)\n\t\t);\n\n\t}\n}\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/information.php",
    "content": "<?php\n/**\n * Single Student View: Information Tab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.2.0\n * @since 6.0.0 Use `LLMS_Student::get_awards_count()`.\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n?>\n\n<?php do_action( 'llms_reporting_student_tab_info_stab_before_content' ); ?>\n\n<div class=\"llms-reporting-tab-content\">\n\n\t<section class=\"llms-reporting-tab-main llms-reporting-widgets\">\n\n\t\t<header>\n\t\t\t<h3><?php esc_html_e( 'Student Information', 'lifterlms' ); ?></h3>\n\t\t</header>\n\t\t<?php\n\n\t\tdo_action( 'llms_reporting_single_student_overview_before_widgets', $student );\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of3',\n\t\t\t\t'icon'      => 'calendar',\n\t\t\t\t'id'        => 'llms-reporting-student-registered',\n\t\t\t\t'data'      => $student->get_registration_date(),\n\t\t\t\t'data_type' => 'date',\n\t\t\t\t'text'      => __( 'Registered', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of3',\n\t\t\t\t'icon'      => 'line-chart',\n\t\t\t\t'id'        => 'llms-reporting-student-registered',\n\t\t\t\t'data'      => $student->get_overall_progress(),\n\t\t\t\t'data_type' => 'percentage',\n\t\t\t\t'text'      => __( 'Overall Progress', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of3',\n\t\t\t\t'icon'      => 'graduation-cap',\n\t\t\t\t'id'        => 'llms-reporting-student-registered',\n\t\t\t\t'data'      => $student->get_overall_grade(),\n\t\t\t\t'data_type' => 'percentage',\n\t\t\t\t'text'      => __( 'Overall Grade', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols' => 'd-1of2',\n\t\t\t\t'icon' => 'trophy',\n\t\t\t\t'id'   => 'llms-reporting-student-achievements',\n\t\t\t\t'data' => $student->get_awards_count( 'achievement' ),\n\t\t\t\t'text' => __( 'Achievements earned', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols' => 'd-1of2',\n\t\t\t\t'icon' => 'certificate',\n\t\t\t\t'id'   => 'llms-reporting-student-certificates',\n\t\t\t\t'data' => $student->get_awards_count( 'certificate' ),\n\t\t\t\t'text' => __( 'Certificates earned', 'lifterlms' ),\n\t\t\t)\n\t\t);\n\n\t\t$address = $student->get( 'billing_address_1' );\n\t\tif ( $student->get( 'billing_address_2' ) ) {\n\t\t\t$address .= ' ' . $student->get( 'billing_address_2' );\n\t\t}\n\t\t$address .= '<br>' . $student->get( 'billing_city' ) . ', ' . $student->get( 'billing_state' ) . ' ' . $student->get( 'billing_zip' );\n\t\t$address .= ' ' . $student->get( 'billing_country' );\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of2',\n\t\t\t\t'icon'      => 'map-marker',\n\t\t\t\t'id'        => 'llms-reporting-student-address',\n\t\t\t\t'data'      => trim( $address ),\n\t\t\t\t'data_type' => 'text',\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Admin_Reporting::output_widget(\n\t\t\tarray(\n\t\t\t\t'cols'      => 'd-1of2',\n\t\t\t\t'icon'      => 'phone',\n\t\t\t\t'id'        => 'llms-reporting-student-address',\n\t\t\t\t'data'      => $student->get( 'phone' ),\n\t\t\t\t'data_type' => 'text',\n\t\t\t)\n\t\t);\n\n\t\tdo_action( 'llms_reporting_single_student_overview_after_widgets', $student );\n\t\t?>\n\n\t</section>\n\n\t<aside class=\"llms-reporting-tab-side\">\n\n\t\t<h3><i class=\"fa fa-bolt\" aria-hidden=\"true\"></i> <?php esc_html_e( 'Recent events', 'lifterlms' ); ?></h3>\n\n\t\t<?php foreach ( $student->get_events() as $event ) : ?>\n\t\t\t<?php LLMS_Admin_Reporting::output_event( $event, 'student' ); ?>\n\t\t<?php endforeach; ?>\n\n\t</aside>\n\n</div>\n\n<?php do_action( 'llms_reporting_student_tab_info_stab_after_content' ); ?>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/memberships.php",
    "content": "<?php\n/**\n * Single Student View: Memberships Tab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$table = new LLMS_Table_Student_Memberships();\n$table->get_results(\n\tarray(\n\t\t'student' => $student,\n\t)\n);\n$table->output_table_html();\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/quiz_attempts.php",
    "content": "<?php\n/**\n * Single Student View: Quiz Attempts Tab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 9.1.0\n * @version 9.1.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$table = new LLMS_Table_Student_Quiz_Attempts();\n$table->get_results(\n\tarray(\n\t\t'student_id' => $student->get_id(),\n\t)\n);\n$table->output_table_html();"
  },
  {
    "path": "templates/admin/reporting/tabs/students/student.php",
    "content": "<?php\n/**\n * Single Student View\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 3.2.0\n * @version 3.15.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n?>\n<section class=\"llms-reporting-tab llms-reporting-student\">\n\n\t<header class=\"llms-reporting-breadcrumbs\">\n\t\t<a href=\"<?php echo esc_url( admin_url( 'admin.php?page=llms-reporting' ) ); ?>\"><?php esc_html_e( 'Students', 'lifterlms' ); ?></a>\n\t\t<?php do_action( 'llms_reporting_student_tab_breadcrumbs' ); ?>\n\t</header>\n\n\t<div class=\"llms-reporting-body\">\n\n\t\t<header class=\"llms-reporting-header\">\n\n\t\t\t<div class=\"llms-reporting-header-img\">\n\t\t\t\t<?php echo wp_kses_post( $student->get_avatar( 64 ) ); ?>\n\t\t\t</div>\n\t\t\t<div class=\"llms-reporting-header-info\">\n\t\t\t\t<h2><a href=\"<?php echo esc_url( get_edit_user_link( $student->get_id() ) ); ?>\"><?php echo esc_html( $student->get_name() ); ?></a></h2>\n\t\t\t\t<h5><a href=\"<?php echo esc_url( 'mailto:' . $student->get( 'user_email' ) ); ?>\"><?php echo esc_html( $student->get( 'user_email' ) ); ?></a></h5>\n\t\t\t</div>\n\n\t\t</header>\n\n\t\t<nav class=\"llms-nav-tab-wrapper llms-nav-secondary\">\n\t\t\t<ul class=\"llms-nav-items\">\n\t\t\t<?php foreach ( $tabs as $name => $label ) : ?>\n\t\t\t\t<li class=\"llms-nav-item<?php echo ( $current_tab === $name ) ? ' llms-active' : ''; ?>\">\n\t\t\t\t\t<a class=\"llms-nav-link\" href=\"<?php echo esc_url( LLMS_Admin_Reporting::get_stab_url( $name ) ); ?>\">\n\t\t\t\t\t\t<?php echo wp_kses_post( $label ); ?>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t<?php endforeach; ?>\n\t\t\t</ul>\n\t\t</nav>\n\n\t\t<section class=\"llms-reporting-stab\">\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'admin/reporting/tabs/students/' . $current_tab . '.php',\n\t\t\t\tarray(\n\t\t\t\t\t'student' => $student,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t/**\n\t\t\t * Allow add-ons to provide content for student tabs.\n\t\t\t *\n\t\t\t * @since 9.1.0\n\t\t\t *\n\t\t\t * @param LLMS_Student $student LLMS_Student instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_reporting_student_tab_' . $current_tab . '_content', $student );\n\t\t\t?>\n\t\t</section>\n\n\t</div>\n\n</section>\n"
  },
  {
    "path": "templates/admin/reporting/tabs/students/students.php",
    "content": "<?php\n/**\n * Students Table\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! is_admin() ) {\n\texit;\n}\n\n$table = new LLMS_Table_Students();\n$table->get_results();\n$table->output_table_html();\n"
  },
  {
    "path": "templates/admin/reporting/tabs/widgets.php",
    "content": "<?php\n/**\n * Reporting Sales Tab\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since Unknown\n * @since 7.2.0 Add content tag param to widget options.\n * @since 7.3.0 Escape output.\n * @version 7.3.0\n *\n * @param array $widget_data Array of widget data to display.\n */\n\ndefined( 'ABSPATH' ) || exit;\nif ( ! is_admin() ) {\n\texit;\n}\n\n?>\n\n<?php foreach ( $widget_data as $row => $widgets ) : ?>\n\t<div class=\"llms-widget-row llms-widget-row-<?php echo esc_attr( $row ); ?>\">\n\t<?php foreach ( $widgets as $id => $opts ) : ?>\n\n\t\t<div class=\"llms-widget-<?php echo esc_attr( $opts['cols'] ); ?>\">\n\t\t\t<div class=\"llms-widget is-loading\" data-method=\"<?php echo esc_attr( $id ); ?>\" id=\"llms-widget-<?php echo esc_attr( $id ); ?>\">\n\n\t\t\t\t<p class=\"llms-label\"><?php echo esc_html( $opts['title'] ); ?></p>\n\n\t\t\t\t<?php if ( ! empty( $opts['link'] ) ) { ?>\n\t\t\t\t\t<a href=\"<?php echo esc_url( $opts['link'] ); ?>\">\n\t\t\t\t<?php } ?>\n\n\t\t\t\t<?php\n\t\t\t\tprintf(\n\t\t\t\t\t'<%s class=\"llms-widget-content\">%s</%s>',\n\t\t\t\t\tesc_html( $opts['content_tag'] ?? 'h3' ),\n\t\t\t\t\tesc_html( $opts['content'] ?? '' ),\n\t\t\t\t\tesc_html( $opts['content_tag'] ?? 'h3' )\n\t\t\t\t);\n\t\t\t\t?>\n\n\t\t\t\t<?php if ( ! empty( $opts['link'] ) ) { ?>\n\t\t\t\t\t</a>\n\t\t\t\t<?php } ?>\n\n\t\t\t\t<span class=\"spinner\"></span>\n\n\t\t\t\t<i class=\"fa fa-info-circle llms-widget-info-toggle\"></i>\n\t\t\t\t<div class=\"llms-widget-info\">\n\t\t\t\t\t<p><?php echo esc_html( $opts['info'] ); ?></p>\n\t\t\t\t</div>\n\n\t\t\t</div>\n\t\t</div>\n\n\t<?php endforeach; ?>\n\t</div>\n<?php endforeach; ?>\n\n<div class=\"llms-charts-wrapper\" id=\"llms-charts-wrapper\"></div>\n\n<div id=\"llms-analytics-json\" style=\"display:none;\"><?php echo esc_html( $json ); ?></div>\n"
  },
  {
    "path": "templates/admin/user-edit.php",
    "content": "<?php\n/**\n * Add LifterLMS fields to the user-edit screen on the WordPress admin Panel\n *\n * @package LifterLMS/Templates/Admin\n *\n * @since 2.7.0\n * @version 3.13.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<h2><?php echo esc_html( $section_title ); ?></h2>\n\n<table class=\"form-table\">\n\t<tbody>\n\n\t\t<?php foreach ( $fields as $field => $data ) : ?>\n\n\t\t\t<tr class=\"user-<?php echo esc_attr( $field ); ?>-wrap\">\n\t\t\t\t<th>\n\t\t\t\t\t<label for=\"<?php echo esc_attr( $field ); ?>\">\n\t\t\t\t\t\t<?php echo esc_html( $data['label'] ); ?>\n\t\t\t\t\t\t<?php echo ( $data['required'] ) ? '<span class=\"description\">(' . esc_html__( 'required', 'lifterlms' ) . ')</span>' : ''; ?>\n\t\t\t\t\t</label>\n\t\t\t\t</th>\n\t\t\t\t<td>\n\t\t\t\t\t<input type=\"<?php echo esc_attr( $data['type'] ); ?>\" name=\"<?php echo esc_attr( $field ); ?>\" id=\"<?php echo esc_attr( $field ); ?>\" value=\"<?php echo esc_attr( $data['value'] ); ?>\" class=\"regular-text\">\n\t\t\t\t\t<?php echo ( $data['description'] ) ? '<span class=\"description\">' . wp_kses_post( $data['description'] ) . '</span>' : ''; ?>\n\t\t\t\t</td>\n\t\t\t</tr>\n\n\t\t<?php endforeach; ?>\n\n\t</tbody>\n</table>\n"
  },
  {
    "path": "templates/archive-course.php",
    "content": "<?php\n/**\n * Template for displaying course archives\n *\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n * @since       1.0.0\n * @version     3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'loop.php' );\n"
  },
  {
    "path": "templates/archive-llms_membership.php",
    "content": "<?php\n/**\n * Template for displaying membership archives\n *\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n * @since       1.0.0\n * @version     3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'loop.php' );\n"
  },
  {
    "path": "templates/block-templates/archive-course.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"archive-course\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/archive-llms_membership.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"archive-llms_membership\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/single-no-access.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"single-no-access\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/taxonomy-course_cat.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"taxonomy-course_cat\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/taxonomy-course_difficulty.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"taxonomy-course_difficulty\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/taxonomy-course_tag.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"taxonomy-course_tag\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/taxonomy-course_track.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"taxonomy-course_track\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/taxonomy-membership_cat.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"taxonomy-membership_cat\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/block-templates/taxonomy-membership_tag.html",
    "content": "<!-- wp:template-part {\"slug\":\"header\",\"tagName\":\"header\"} /-->\n<!-- wp:group {\"layout\":{\"inherit\":true}} -->\n<div class=\"wp-block-group\"><!-- wp:llms/php-template {\"template\":\"taxonomy-membership_cat\"} /--></div>\n<!-- /wp:group -->\n<!-- wp:template-part {\"slug\":\"footer\",\"tagName\":\"footer\"} /-->\n"
  },
  {
    "path": "templates/certificates/actions.php",
    "content": "<?php\n/**\n * Single certificate actions.\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 6.0.0\n * @version 6.0.0\n *\n * @param LLMS_User_Certificate $certificate       Certificate object.\n * @param string                $back_link         URL for the back link.\n * @param string                $back_text         Text for the back link anchor.\n * @param boolean               $is_shaing_enabled Whether or not sharing is enabled for the certificate.\n * @param boolean               $is_template       Whether or not a certificate template is being displayed.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"llms-print-certificate no-print\" id=\"llms-print-certificate\">\n\n\t<?php if ( ! $is_template ) : ?>\n\t\t<a class=\"llms-cert-return-link\" href=\"<?php echo esc_url( $back_link ); ?>\">&larr; <?php echo esc_html( $back_text ); ?></a>\n\t<?php endif; ?>\n\n\t<button class=\"llms-button-secondary\" onClick=\"window.print()\" type=\"button\">\n\t\t<?php esc_html_e( 'Print', 'lifterlms' ); ?>\n\t\t<i class=\"fa fa-print\" aria-hidden=\"true\"></i>\n\t</button>\n\n\t<form action=\"\" method=\"POST\">\n\n\t\t<button class=\"llms-button-secondary\" type=\"submit\" name=\"llms_generate_cert\">\n\t\t\t<?php esc_html_e( 'Download', 'lifterlms' ); ?>\n\t\t\t<i class=\"fa fa-cloud-download\" aria-hidden=\"true\"></i>\n\t\t</button>\n\n\t\t<?php if ( ! $is_template ) : ?>\n\t\t\t<button class=\"llms-button-secondary\" type=\"submit\" name=\"llms_enable_cert_sharing\" value=\"<?php echo esc_attr( ! $is_sharing_enabled ); ?>\">\n\t\t\t<?php echo ( $is_sharing_enabled ? esc_html__( 'Disable sharing', 'lifterlms' ) : esc_html__( 'Enable sharing', 'lifterlms' ) ); ?>\n\t\t\t\t<i class=\"fa fa-share-alt\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\t\t<?php endif; ?>\n    \n    <?php if ( $is_sharing_enabled ) : ?>\n\t\t\t<button id=\"llms-copy-to-clipboard\" class=\"llms-button-secondary\" type=\"button\" aria-disabled=\"false\">\n\t\t\t\t<?php echo esc_html__( 'Copy Shareable Link', 'lifterlms' ); ?>\n\t\t\t\t<i class=\"fa fa-clipboard\" aria-hidden=\"true\"></i>\n\t\t\t</button> <span id=\"llms-copy-to-clipboard-success\" class=\"fa fa-check\" role=\"alert\" aria-live=\"polite\" style=\"display: none;\"><span class=\"sr-only\"><?php echo esc_html__( 'Copied', 'lifterlms' ); ?></span></span>\n\t\t<?php endif; ?>\n\n\t\t<input type=\"hidden\" name=\"certificate_id\" value=\"<?php echo esc_attr( get_the_ID() ); ?>\">\n\n\t\t<?php wp_nonce_field( 'llms-cert-actions', '_llms_cert_actions_nonce' ); ?>\n\n\t</form>\n\n</div>\n\n<style>\n\t#llms-copy-to-clipboard-success {\n\t\tdisplay: none;\n\t}\n\n\t.sr-only {\n\t\tclip: rect(1px, 1px, 1px, 1px);\n\t\tclip-path: inset(50%);\n\t\theight: 1px;\n\t\toverflow: hidden;\n\t\tposition: absolute;\n\t\twhite-space: nowrap;\n\t\twidth: 1px;\n\t\tmargin: -1px;\n\t}\n</style>\n\n<script type=\"text/javascript\">\n\tdocument.getElementById( 'llms-copy-to-clipboard' ).addEventListener( 'click', function() {\n\t\tif (navigator.clipboard && navigator.clipboard.writeText) {\n\t\t\tnavigator.clipboard.writeText( window.location.href );\n\t\t\tdocument.getElementById( 'llms-copy-to-clipboard-success' ).style.display = 'inline-block';\n\t\t\tsetTimeout( function() {\n\t\t\t\tdocument.getElementById( 'llms-copy-to-clipboard-success' ).style.display = 'none';\n\t\t\t}, 2000 );\n\t\t} else {\n\t\t\talert( <?php echo wp_json_encode( __( 'Copy to clipboard is not supported or not available. You can copy and share the URL of this page from the address bar.', 'lifterlms' ) ); ?> );\n\t\t}\n\t} );\n</script>\n\n"
  },
  {
    "path": "templates/certificates/content-legacy.php",
    "content": "<?php\n/**\n * Single certificate main content.\n *\n * This is the legacy template for certificates built prior to version 6.\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 6.0.0\n * @version 6.0.0\n *\n * @param LLMS_User_Certificate $certificate Certificate object.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$image = llms_get_certificate_image( $certificate->get( 'id' ) );\n?>\n<div class=\"llms-certificate-container\" style=\"width:<?php echo esc_attr( $image['width'] ); ?>px; height:<?php echo esc_attr( $image['height'] ); ?>px;\">\n\t<img src=\"<?php echo esc_url( $image['src'] ); ?>\" style=\"margin-bottom:-<?php echo esc_attr( $image['height'] ); ?>px;\" alt=\"<?php esc_html_e( 'Certificate Background', 'lifterlms' ); ?>\" class=\"certificate-background\">\n\t<div id=\"certificate-<?php echo esc_attr( $certificate->get( 'id' ) ); ?>\" <?php post_class(); ?>>\n\n\t\t<div class=\"llms-summary\">\n\n\t\t\t<?php llms_print_notices(); ?>\n\n\t\t\t<?php\n\t\t\t\t/**\n\t\t\t\t * Output content prior to the main content of a single certificate.\n\t\t\t\t *\n\t\t\t\t * @since Unknown.\n\t\t\t\t * @since 6.0.0 Added the `$certificate` parameter.\n\t\t\t\t *\n\t\t\t\t * @param LLMS_User_Certificate $certificate Certificate object.\n\t\t\t\t */\n\t\t\t\tdo_action( 'before_lifterlms_certificate_main_content', $certificate );\n\t\t\t?>\n\n\t\t\t<h1><?php echo esc_html( llms_get_certificate_title() ); ?></h1>\n\t\t\t<?php echo wp_kses_post( llms_get_certificate_content() ); ?>\n\n\t\t\t<?php\n\t\t\t\t/**\n\t\t\t\t * Output content after to the main content of a single certificate.\n\t\t\t\t *\n\t\t\t\t * @since Unknown.\n\t\t\t\t * @since 6.0.0 Added the `$certificate` parameter.\n\t\t\t\t *\n\t\t\t\t * @param LLMS_User_Certificate $certificate Certificate object.\n\t\t\t\t */\n\t\t\t\tdo_action( 'after_lifterlms_certificate_main_content', $certificate );\n\t\t\t?>\n\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/certificates/content.php",
    "content": "<?php\n/**\n * Single certificate main content.\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 6.0.0\n * @version 6.0.0\n *\n * @param LLMS_User_Certificate $certificate Certificate object.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<div class=\"llms-certificate-wrapper\">\n\t<div id=\"certificate-<?php echo esc_attr( $certificate->get( 'id' ) ); ?>\" <?php post_class( array( 'llms-certificate-container', 'cert-template-v2' ) ); ?>>\n\n\t\t<?php llms_print_notices(); ?>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Output content prior to the main content of a single certificate.\n\t\t\t *\n\t\t\t * @since Unknown.\n\t\t\t * @since 6.0.0 Added the `$certificate` parameter.\n\t\t\t *\n\t\t\t * @param LLMS_User_Certificate $certificate Certificate object.\n\t\t\t */\n\t\t\tdo_action( 'before_lifterlms_certificate_main_content', $certificate );\n\t\t?>\n\n\t\t<?php echo wp_kses_post( llms_get_certificate_content() ); ?>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Output content after to the main content of a single certificate.\n\t\t\t *\n\t\t\t * @since Unknown.\n\t\t\t * @since 6.0.0 Added the `$certificate` parameter.\n\t\t\t *\n\t\t\t * @param LLMS_User_Certificate $certificate Certificate object.\n\t\t\t */\n\t\t\tdo_action( 'after_lifterlms_certificate_main_content', $certificate );\n\t\t?>\n\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/certificates/dynamic-styles.php",
    "content": "<?php\n/**\n * Single certificate dynamic styles.\n *\n * Output in the header, via the `wp_head` action.\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 6.0.0\n * @version 6.0.0\n *\n * @param LLMS_User_Certificate $certificate      Certificate object.\n * @param string                $width            Width (with unit) accounting for the orientation value.\n * @param string                $height           Height (with unit) accounting for the orientation value.\n * @param string                $background_color Background color value.\n * @param string                $background_img   Image source URL for the background image.\n * @param string                $padding          Internal margin value with units, ready to be used in CSS.\n * @param array[]               $fonts            Array of custom certificate fonts used by the certificate.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$gfonts_preconnet = false;\n?>\n\n<!-- Certificates Dynamic Styles -->\n<?php foreach ( $fonts as $font ) : ?>\n\t<?php if ( ! empty( $font['href'] ) ) : ?>\n\t\t<?php\n\t\tif ( ! $gfonts_preconnet && false !== strpos( $font['href'], 'fonts.googleapis.com' ) ) :\n\t\t\t$gfonts_preconnet = true;\n\t\t\t?>\n\t\t\t<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n\t\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n\t\t<?php endif; ?>\n\t<link id=\"llms-font--<?php echo esc_attr( $font['id'] ); ?>\" href=\"<?php echo esc_url( $font['href'] ); ?>\" rel=\"stylesheet\">\n\t<?php endif; ?>\n<?php endforeach; ?>\n<style type=\"text/css\">\n\thtml, body {\n\t\tbackground-color: <?php echo esc_html( $background_color ); ?> !important;\n\t}\n\t.llms-certificate-wrapper {\n\t\theight: <?php echo esc_html( $height ); ?>;\n\t\twidth: <?php echo esc_html( $width ); ?>;\n\t}\n\t.llms-certificate-container {\n\t\tbackground-image: <?php echo \"url( \" . esc_url( $background_img ) . \" )\"; ?> !important;\n\t\tpadding: <?php echo esc_html( $padding ); ?>;\n\t}\n\t<?php foreach ( $fonts as $font ) : ?>\n\t.has-<?php echo esc_html( $font['id'] ); ?>-font-family {\n\t\tfont-family: <?php echo esc_html( $font['fontFamily'] ); ?>;\n\t}\n\t<?php endforeach; ?>\n</style>\n<style type=\"text/css\" media=\"print\">\n\t@page {\n\t\tsize: <?php echo esc_html( $width ); ?> <?php echo esc_html( $height ); ?>;\n\t\tmargin: 0;\n\t}\n</style>\n<!-- End Certificates Dynamic Styles -->\n"
  },
  {
    "path": "templates/certificates/footer.php",
    "content": "<?php\n/**\n * Single certificate footer file.\n *\n * This is used in favor of the theme's footer.php file in order to reduce theme compatibility issues\n * which arise on certificate templates.\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nwp_footer(); ?>\n</body>\n</html>\n"
  },
  {
    "path": "templates/certificates/header.php",
    "content": "<?php\n/**\n * Single certificate header file.\n *\n * This is used in favor of the theme's header.php file in order to reduce theme compatibility issues\n * which arise on certificate templates.\n *\n * Certificates are meant to be minimal and should not display navigation, headers, logos, sidebars,\n * footers, and so on. Certificates are print-first with a limited on-screen user interface containing\n * actions related to the currently-viewed certificate (such as printing, exporting, etc...).\n *\n * Note: the viewport declaration commonly found in the <head> element is intentionally excluded since\n * certificates are print-first and non-responsive to device width.\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 6.0.0\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?><!DOCTYPE html>\n<html <?php language_attributes(); ?>>\n<head>\n<link rel=\"profile\" href=\"https://gmpg.org/xfn/11\" />\n<meta http-equiv=\"Content-Type\" content=\"<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>\" />\n<title><?php echo esc_html( wp_get_document_title() ); ?></title>\n<?php wp_head(); ?>\n</head>\n<body <?php body_class(); ?>>\n<?php\nwp_body_open();\n"
  },
  {
    "path": "templates/certificates/loop.php",
    "content": "<?php\n/**\n * Certificates Loop\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 3.14.0\n * @since 6.0.0 Add pagination.\n * @version 3.14.0\n *\n * @param LLMS_User_Certificate[] $certificates Array of certificates to display.\n * @param int                     $cols         Number of columns.\n * @param false|array             $pagination   Pagination arguments to pass to {@see llms_paginate_links()} or `false`\n *                                              when pagination is disabled.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php\n\t/**\n\t * Action run prior to the certificate loop template.\n\t *\n\t * @since 3.14.0\n\t */\n\tdo_action( 'llms_before_certificate_loop' );\n?>\n\n<?php if ( $certificates ) : ?>\n\n\t<ul class=\"llms-certificates-loop listing-certificates <?php printf( 'loop-cols-%d', esc_attr( $cols ) ); ?>\">\n\n\t\t<?php foreach ( $certificates as $certificate ) : ?>\n\n\t\t\t<li class=\"llms-certificate-loop-item certificate-item\">\n\t\t\t\t<?php\n\t\t\t\t\t/**\n\t\t\t\t\t * Action run to display the preview for a single certificate.\n\t\t\t\t\t *\n\t\t\t\t\t * @since 3.14.0\n\t\t\t\t\t *\n\t\t\t\t\t * @param LLMS_User_Certificate $certificate Certificate object being displayed.\n\t\t\t\t\t */\n\t\t\t\t\tdo_action( 'llms_certificate_preview', $certificate );\n\t\t\t\t?>\n\t\t\t</li>\n\n\t\t<?php endforeach; ?>\n\n\t</ul>\n\n<?php else : ?>\n\n\t<p>\n\t<?php\n\t\t/**\n\t\t * Filters the message displayed when the student hasn't earned any certificates.\n\t\t *\n\t\t * @since 3.14.0\n\t\t *\n\t\t * @param string $message The message text.\n\t\t */\n\t\techo wp_kses_post( apply_filters( 'lifterlms_no_certificates_text', esc_html__( 'You do not have any certificates yet.', 'lifterlms' ) ) );\n\t?>\n\t</p>\n\n<?php endif; ?>\n\n<?php if ( $pagination ) : ?>\n\t<?php\n\t\t// HTML output is escaped in the function.\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo llms_paginate_links( $pagination );\n\t?>\n<?php endif; ?>\n\n<?php\n\t/**\n\t * Action run after to the certificate loop template.\n\t *\n\t * @since 3.14.0\n\t */\n\tdo_action( 'llms_after_certificate_loop' );\n?>\n"
  },
  {
    "path": "templates/certificates/preview.php",
    "content": "<?php\n/**\n * Single Certificate Preview Template\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 3.14.0\n * @version 6.0.0\n *\n * @param LLMS_User_Certificate $certificate Certificate object being displayed.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<a class=\"llms-certificate\" data-id=\"<?php echo esc_attr( $certificate->get( 'id' ) ); ?>\" href=\"<?php echo esc_url( get_permalink( $certificate->get( 'id' ) ) ); ?>\" id=\"<?php printf( 'llms-certificate-%d', esc_attr( $certificate->get( 'id' ) ) ); ?>\">\n\n\t<?php do_action( 'lifterlms_before_certificate_preview', $certificate ); ?>\n\n\t<h4 class=\"llms-certificate-title\"><?php echo esc_html( $certificate->get( 'title' ) ); ?></h4>\n\t<div class=\"llms-certificate-date\"><?php echo esc_html( $certificate->get_earned_date() ); ?></div>\n\n\t<?php do_action( 'lifterlms_after_certificate_preview', $certificate ); ?>\n\n</a>\n\n"
  },
  {
    "path": "templates/certificates/template.php",
    "content": "<?php\n/**\n * Legacy template used for generating the stored contend of an earned certificate.\n *\n * This file is loaded via `LLMS_User_Certificate::merge_content` when the deprecated\n * filter `llms_certificate_use_legacy_template` is used.\n *\n * Historically this template was (likely mistakenly) copied from the email engagement\n * functionality where an HTML email is constructed (and mailed). With certificates\n * the content of the saved certificate is much simpler and adding custom HTML can be\n * done using the certificate template editor, rendering the usage of a template\n * superfluous.\n *\n * The template is retained until the `llms_certificate_use_legacy_template` is removed\n * in the next major release, at which point this template will also be removed.\n *\n * @package LifterLMS/Templates/Certificates\n *\n * @since 1.0.0\n * @version 6.0.0\n *\n * @deprecated 6.0.0\n */\ndefined( 'ABSPATH' ) || exit; ?>\n\n<p><?php echo wp_kses_post( $email_message ); ?></p>\n"
  },
  {
    "path": "templates/checkout/form-checkout.php",
    "content": "<?php\n/**\n * Checkout Form\n *\n * @package LifterLMS/Templates/Checkout\n *\n * @since 1.0.0\n * @since 5.0.0 Moved all variable declarations to the checkout shortcode controller.\n *               Updated to utilize fields from LLMS_Forms class.\n * @version 5.0.0\n *\n * @var int $cols Number of columns to use for the form layout.\n * @var LLMS_Payment_Gateway[] $gateways Array of enabled payment gateway instances.\n * @var string $selected_gateway ID of the currently selected/default payment gateway.\n * @var string $order_key Current order key. Empty string for new orders.\n * @var LLMS_Coupon|false $coupon Coupon currently applied to the session or `false` when none found.\n * @var LLMS_Access_Plan $plan Access plan object.\n * @var LLMS_Product $product Product object.\n * @var bool $is_free Whether or not the access plan is a free plan.\n * @var string $form_location Form location id.\n * @var string $form_title Form title.\n * @var array $form_fields Array of LifterLMS Form Fields.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php do_action( 'lifterlms_pre_checkout_form' ); ?>\n\n<form action=\"\" class=\"llms-checkout llms-checkout-cols-<?php echo esc_attr( $cols ); ?>\" method=\"POST\" id=\"llms-product-purchase-form\">\n\n\t<?php do_action( 'lifterlms_before_checkout_form' ); ?>\n\n\t<?php if ( $form_fields ) : ?>\n\t\t<div class=\"llms-checkout-col llms-col-1\">\n\n\t\t\t<section class=\"llms-checkout-section billing-information\">\n\n\t\t\t\t<?php if ( $form_title ) : ?>\n\t\t\t\t\t<h4 class=\"llms-form-heading\"><?php echo esc_html( $form_title ); ?></h4>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<div class=\"llms-checkout-section-content llms-form-fields\">\n\t\t\t\t\t<?php do_action( 'lifterlms_checkout_before_billing_fields' ); ?>\n\t\t\t\t\t<?php\n\t\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\t\t\techo $form_fields;\n\t\t\t\t\t?>\n\t\t\t\t\t<?php do_action( 'lifterlms_checkout_after_billing_fields' ); ?>\n\t\t\t\t</div>\n\n\t\t\t</section>\n\n\t\t</div>\n\t<?php endif; ?>\n\n\t<div class=\"llms-checkout-col llms-col-2\">\n\n\t\t<section class=\"llms-checkout-section order-summary\">\n\n\t\t\t<h4 class=\"llms-form-heading\"><?php esc_html_e( 'Order Summary', 'lifterlms' ); ?></h4>\n\n\t\t\t<div class=\"llms-checkout-section-content\">\n\n\t\t\t\t<?php\n\t\t\t\tllms_get_template(\n\t\t\t\t\t'checkout/form-summary.php',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'coupon'  => $coupon,\n\t\t\t\t\t\t'plan'    => $plan,\n\t\t\t\t\t\t'product' => $product,\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\n\t\t\t\t<?php\n\t\t\t\tllms_get_template(\n\t\t\t\t\t'checkout/form-coupon.php',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'coupon' => $coupon,\n\t\t\t\t\t\t'plan'   => $plan,\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\n\t\t\t</div>\n\n\t\t</section>\n\n\t\t<section class=\"llms-checkout-section payment-details\">\n\n\t\t\t<h4 class=\"llms-form-heading\">\n\t\t\t\t<?php if ( ! $is_free ) : ?>\n\t\t\t\t\t<?php esc_html_e( 'Payment Details', 'lifterlms' ); ?>\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<?php esc_html_e( 'Enrollment Confirmation', 'lifterlms' ); ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t</h4>\n\n\n\t\t\t<div class=\"llms-checkout-section-content llms-form-fields\">\n\n\t\t\t\t<?php\n\t\t\t\tllms_get_template(\n\t\t\t\t\t'checkout/form-gateways.php',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'coupon'           => $coupon,\n\t\t\t\t\t\t'gateways'         => $gateways,\n\t\t\t\t\t\t'selected_gateway' => $selected_gateway,\n\t\t\t\t\t\t'plan'             => $plan,\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\n\t\t\t\t<footer class=\"llms-checkout-confirm llms-form-fields flush\">\n\n\t\t\t\t\t<?php do_action( 'llms_checkout_footer_before' ); ?>\n\n\t\t\t\t\t<?php\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Hook: llms_registration_privacy\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @hooked llms_privacy_policy_form_field - 10\n\t\t\t\t\t\t * @hooked llms_agree_to_terms_form_field - 20\n\t\t\t\t\t\t */\n\t\t\t\t\t\tdo_action( 'llms_registration_privacy' );\n\t\t\t\t\t?>\n\n\t\t\t\t\t<?php\n\t\t\t\t\tllms_form_field(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'classes' => 'llms-button-action',\n\t\t\t\t\t\t\t'id'      => 'llms_create_pending_order',\n\t\t\t\t\t\t\t'value'   => apply_filters( 'lifterlms_checkout_buy_button_text', ! $is_free ? __( 'Buy Now', 'lifterlms' ) : __( 'Enroll Now', 'lifterlms' ) ),\n\t\t\t\t\t\t\t'type'    => 'submit',\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\n\t\t\t\t\t<?php do_action( 'llms_checkout_footer_after' ); ?>\n\n\t\t\t\t</footer>\n\n\t\t\t</div>\n\n\t\t</section>\n\n\t</div>\n\n\t<?php wp_nonce_field( 'create_pending_order', '_llms_checkout_nonce' ); ?>\n\t<input name=\"action\" type=\"hidden\" value=\"create_pending_order\">\n\t<input id=\"llms-plan-id\" name=\"llms_plan_id\" type=\"hidden\" value=\"<?php echo esc_attr( $plan->get( 'id' ) ); ?>\">\n\t<input id=\"llms-order-key\" name=\"llms_order_key\" type=\"hidden\" value=\"<?php echo esc_attr( $order_key ); ?>\">\n\n\t<?php do_action( 'lifterlms_after_checkout_form' ); ?>\n\n</form>\n\n<?php do_action( 'lifterlms_post_checkout_form' ); ?>\n"
  },
  {
    "path": "templates/checkout/form-confirm-payment.php",
    "content": "<?php\n/**\n * Payment gateways area of the checkout form\n *\n * @package LifterLMS/Templates/Checkout\n *\n * @since Unknown\n * @since 5.0.0 Update form field to utilize \"checked\" attribute of \"selected\" and removed superfluous values.\n * @since 7.0.0 Disable data-source loading for gateway radio fields.\n * @version 7.0.0\n *\n * @param LLMS_Payment_Gateway[] $gateways         Array of enabled payment gateway instances.\n * @param string                 $selected_gateway ID of the currently selected/default payment gateway.\n * @param LLMS_Coupon|false      $coupon           Coupon currently applied to the session or `false` when none found.\n * @param LLMS_Access_Plan       $plan             Access plan object.\n */\ndefined( 'ABSPATH' ) || exit;\n\n$order_key  = llms_filter_input_sanitize_string( INPUT_GET, 'order' );\n$order      = llms_get_order_by_key( $order_key );\n$gateway_id = $selected_gateway->get_id();\n$fields     = LLMS_Forms::instance()->get_form_fields( 'checkout', array( 'plan' => $plan ) );\n?>\n\n<?php if ( ! apply_filters( 'llms_order_can_be_confirmed', ( 'llms-pending' === $order->get( 'status' ) ), $order, $gateway_id ) ) : ?>\n\n\t<?php\n\tllms_print_notice(\n\t\tsprintf(\n\t\t\t// Translators: %1$s = opening anchor tag; %2$s = closing anchor tag.\n\t\t\t__( 'Only pending orders can be confirmed. View your %1$sorder history%2$s for more information', 'lifterlms' ),\n\t\t\t'<a href=\"' . esc_url( llms_get_endpoint_url( 'orders', '', llms_get_page_url( 'myaccount' ) ) ) . '\">',\n\t\t\t'</a>'\n\t\t),\n\t\t'error'\n\t);\n\t?>\n\n<?php else : ?>\n\n\t<form action=\"\" class=\"llms-checkout llms-confirm llms-checkout-cols-<?php echo esc_attr( $cols ); ?>\" method=\"POST\" id=\"llms-product-purchase-confirm-form\">\n\n\t\t<?php do_action( 'lifterlms_before_checkout_confirm_form' ); ?>\n\n\t\t<div class=\"llms-checkout-col llms-col-1\">\n\n\t\t\t<section class=\"llms-checkout-section\">\n\n\t\t\t\t<h4 class=\"llms-form-heading\"><?php echo esc_html( llms_get_form_title( 'checkout', array( 'plan' => $plan ) ) ); ?></h4>\n\n\t\t\t\t<div class=\"llms-checkout-section-content llms-form-fields\">\n\t\t\t\t\t<?php do_action( 'lifterlms_checkout_confirm_before_billing_info' ); ?>\n\t\t\t\t\t<?php foreach ( $fields as $field ) : ?>\n\t\t\t\t\t\t\t<?php if ( ! empty( $field['value'] ) && ! empty( $field['label'] ) ) : ?>\n\t\t\t\t\t\t\t\t<div class=\"llms-form-field llms-field-display <?php echo esc_attr( $field['id'] ); ?>\">\n\t\t\t\t\t\t\t\t\t<strong><?php echo esc_html( $field['label'] ); ?></strong>: <?php echo esc_html( $field['value'] ); ?>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php endforeach; ?>\n\t\t\t\t\t<?php do_action( 'lifterlms_checkout_confirm_after_billing_info' ); ?>\n\t\t\t\t</div>\n\n\t\t\t</section>\n\n\t\t</div>\n\n\t\t<div class=\"llms-checkout-col llms-col-2\">\n\n\t\t\t<section class=\"llms-checkout-section\">\n\n\t\t\t\t<h4 class=\"llms-form-heading\"><?php esc_html_e( 'Order Summary', 'lifterlms' ); ?></h4>\n\n\t\t\t\t<div class=\"llms-checkout-section-content\">\n\n\t\t\t\t\t<?php\n\t\t\t\t\tllms_get_template(\n\t\t\t\t\t\t'checkout/form-summary.php',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'coupon'  => $coupon,\n\t\t\t\t\t\t\t'plan'    => $plan,\n\t\t\t\t\t\t\t'product' => $product,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\n\t\t\t\t</div>\n\n\t\t\t</section>\n\n\t\t\t<section class=\"llms-checkout-section\">\n\n\t\t\t\t<h4 class=\"llms-form-heading\"><?php esc_html_e( 'Payment Details', 'lifterlms' ); ?></h4>\n\t\t\t\t<div class=\"llms-checkout-section-content llms-form-fields\">\n\n\t\t\t\t\t<div class=\"llms-payment-method\">\n\t\t\t\t\t\t<?php do_action( 'lifterlms_checkout_confirm_before_payment_method', $gateway_id ); ?>\n\t\t\t\t\t\t<span class=\"llms-gateway-title\"><span class=\"llms-label\"><?php esc_html_e( 'Payment Method:', 'lifterlms' ); ?></span> <?php echo esc_html( $selected_gateway->get_title() ); ?></span>\n\t\t\t\t\t\t<?php if ( $selected_gateway->get_icon() ) : ?>\n\t\t\t\t\t\t\t<span class=\"llms-gateway-icon\"><?php echo wp_kses_post( $selected_gateway->get_icon() ); ?></span>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php if ( $selected_gateway->get_description() ) : ?>\n\t\t\t\t\t\t\t<div class=\"llms-gateway-description\"><?php echo wp_kses_post( wpautop( wptexturize( $selected_gateway->get_description() ) ) ); ?></div>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t<?php do_action( 'lifterlms_checkout_confirm_after_payment_method', $gateway_id ); ?>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<footer class=\"llms-checkout-confirm llms-form-fields flush\">\n\n\t\t\t\t\t\t<?php if ( apply_filters( 'llms_gateway_' . $gateway_id . '_show_confirm_order_button', true ) ) : ?>\n\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\tllms_form_field(\n\t\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t\t\t\t\t'classes'     => 'llms-button-action',\n\t\t\t\t\t\t\t\t\t'id'          => 'llms_confirm_pending_order',\n\t\t\t\t\t\t\t\t\t'value'       => apply_filters( 'lifterlms_checkout_confirm_button_text', __( 'Confirm Payment', 'lifterlms' ) ),\n\t\t\t\t\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t\t\t\t\t'required'    => false,\n\t\t\t\t\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t?>\n\n\t\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t\t<input id=\"llms-payment-gateway\" type=\"hidden\" readonly=\"readonly\" value=\"<?php echo esc_attr( $gateway_id ); ?>\">\n\n\t\t\t\t\t</footer>\n\n\t\t\t\t</div>\n\n\t\t\t</section>\n\n\t\t</div>\n\n\t\t<?php wp_nonce_field( 'confirm_pending_order' ); ?>\n\t\t<input name=\"action\" type=\"hidden\" value=\"confirm_pending_order\">\n\t\t<input name=\"llms_order_key\" type=\"hidden\" value=\"<?php echo esc_attr( $order_key ); ?>\">\n\n\t\t<?php do_action( 'lifterlms_after_checkout_confirm_form' ); ?>\n\n\t</form>\n<?php endif; ?>\n"
  },
  {
    "path": "templates/checkout/form-coupon.php",
    "content": "<?php\n/**\n * Coupon area of the checkout form\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.2\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// don't display if the plan is marked as free.\nif ( isset( $plan ) && $plan->is_free() ) {\n\treturn;\n}\n?>\n<div class=\"llms-coupon-wrapper\">\n\n\t<?php if ( empty( $coupon ) ) : ?>\n\n\t\t<?php esc_html_e( 'Have a coupon?', 'lifterlms' ); ?>\n\t\t<a href=\"#llms-coupon-toggle\"><?php esc_html_e( 'Click here to enter your code', 'lifterlms' ); ?></a>\n\n\t\t<div class=\"llms-coupon-entry llms-form-fields flush\">\n\n\t\t\t<div class=\"llms-coupon-messages\"></div>\n\n\t\t\t<?php\n\t\t\tllms_form_field(\n\t\t\t\tarray(\n\n\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t'id'          => 'llms_coupon_code',\n\t\t\t\t\t'placeholder' => __( 'Coupon Code', 'lifterlms' ),\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'text',\n\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t\t<?php\n\t\t\tllms_form_field(\n\t\t\t\tarray(\n\n\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t'classes'     => 'llms-button-secondary',\n\t\t\t\t\t'id'          => 'llms-apply-coupon',\n\t\t\t\t\t'value'       => __( 'Apply Coupon', 'lifterlms' ),\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'button',\n\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t</div>\n\n\t<?php else : ?>\n\n\t\t<?php\n\t\t// Translators: %s = coupon code.\n\t\tllms_print_notice( sprintf( __( 'Coupon code \"%s\" has been applied to your order.', 'lifterlms' ), $coupon->get( 'title' ) ), 'success' );\n\t\t?>\n\n\t\t<div class=\"llms-form-fields flush\">\n\t\t\t<?php\n\t\t\tllms_form_field(\n\t\t\t\tarray(\n\n\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t'classes'     => 'llms-button-secondary',\n\t\t\t\t\t'id'          => 'llms-remove-coupon',\n\t\t\t\t\t'value'       => __( 'Remove Coupon', 'lifterlms' ),\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'button',\n\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\n\t\t</div>\n\n\t\t<input name=\"llms_coupon_code\" type=\"hidden\" value=\"<?php echo esc_attr( $coupon->get( 'title' ) ); ?>\">\n\n\t<?php endif; ?>\n\n</div>\n"
  },
  {
    "path": "templates/checkout/form-gateways.php",
    "content": "<?php\n/**\n * Coupon area of the checkout form\n *\n * @package LifterLMS/Templates/Checkout\n *\n * @since Unknown\n * @since 5.0.0 Update form field to utilize \"checked\" attribute of \"selected\" and removed superfluous values.\n * @since 7.0.0 Disable data-source loading for gateway radio fields.\n * @since 7.5.0 Added check on whether a gateway can or cannot process a plan, or an order's plan (source switching).\n *              Escaped localized strings.\n * @version 7.5.0\n *\n * @param LLMS_Payment_Gateway[] $gateways         Array of enabled payment gateway instances.\n * @param string                 $selected_gateway ID of the currently selected/default payment gateway.\n * @param LLMS_Coupon|false      $coupon           Coupon currently applied to the session or `false` when none found.\n * @param LLMS_Access_Plan|null  $plan             Access plan object.\n * @param LLMS_Order|null        $order            Order object.\n */\ndefined( 'ABSPATH' ) || exit;\n\n$coupon        = isset( $coupon ) ? $coupon : false;\n$show_gateways = true;\n\n// Don't display for free plans or plans which do not require any payment.\nif ( isset( $plan ) && ( $plan->is_free() || ! $plan->requires_payment( $coupon ) ) ) {\n\t$show_gateways = false;\n} elseif ( isset( $plan ) ) {\n\t$supports = $plan->is_recurring() ? 'recurring_payments' : 'single_payments';\n} elseif ( isset( $order ) ) { // Switching source.\n\t$supports = $order->is_recurring() ? 'recurring_payments' : 'single_payments';\n}\n\n$supporting_gateways = 0;\n$gateways_array      = array_values( $gateways );\n?>\n<ul class=\"llms-payment-gateways\">\n\t<?php if ( $show_gateways ) : ?>\n\t\t<?php if ( ! $gateways ) : ?>\n\t\t\t<li class=\"llms-payment-gateway-error\"><?php esc_html_e( 'Payment processing is currently disabled.', 'lifterlms' ); ?></li>\n\t\t<?php else : ?>\n\t\t\t<?php foreach ( $gateways_array as $index => $gateway ) : ?>\n\t\t\t\t<?php if ( $gateway->supports( $supports ) ) : ?>\n\t\t\t\t\t<?php\n\t\t\t\t\tif ( ! $gateway->can_process_access_plan( $plan ?? null, $order ?? null ) ) {\n\t\t\t\t\t\t$selected_gateway = ( $gateway->get_id() === $selected_gateway && isset( $gateways_array[ $index + 1 ] ) ) ?\n\t\t\t\t\t\t\t$gateways_array[ $index + 1 ]->get_id() : $selected_gateway;\n\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Fired when a gateway cannot process a specific plan.\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @since 7.5.0\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @param LLMS_Payment_Gateway  $gateway Payment gateway instance.\n\t\t\t\t\t\t * @param LLMS_Access_Plan|null $plan    Access plan object.\n\t\t\t\t\t\t * @param LLMS_Order|null       $order   Order object.\n\t\t\t\t\t\t */\n\t\t\t\t\t\tdo_action( 'llms_checkout_form_gateway_cant_process_plan', $gateway, $plan ?? null, $order ?? null );\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t?>\n\t\t\t\t\t<li class=\"llms-payment-gateway <?php echo esc_attr( $gateway->get_id() ); ?><?php echo ( $selected_gateway === $gateway->get_id() ) ? ' is-selected' : ''; ?>\">\n\t\t\t\t\t<?php\n\t\t\t\t\tllms_form_field(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'description'     => $gateway->get_icon(),\n\t\t\t\t\t\t\t'id'              => 'llms_payment_gateway_' . $gateway->get_id(),\n\t\t\t\t\t\t\t'label'           => $gateway->get_title(),\n\t\t\t\t\t\t\t'name'            => 'llms_payment_gateway',\n\t\t\t\t\t\t\t'checked'         => ( $selected_gateway === $gateway->get_id() ),\n\t\t\t\t\t\t\t'type'            => 'radio',\n\t\t\t\t\t\t\t'value'           => $gateway->get_id(),\n\t\t\t\t\t\t\t'wrapper_classes' => 'llms-payment-gateway-option',\n\t\t\t\t\t\t\t'data_store'      => false,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\t\t\t\t\t<?php if ( $gateway->get_description() ) : ?>\n\t\t\t\t\t\t<div class=\"llms-gateway-description\"><?php echo wp_kses_post( wpautop( wptexturize( $gateway->get_description() ) ) ); ?></div>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php if ( $gateway->supports( 'checkout_fields' ) ) : ?>\n\t\t\t\t\t\t<div class=\"llms-gateway-fields\"><?php\n\t\t\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\t\t\t\techo $gateway->get_fields();\n\t\t\t\t\t\t?></div>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</li>\n\t\t\t\t\t<?php $supporting_gateways++; ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t<?php endforeach; ?>\n\n\t\t\t<?php if ( ! $supporting_gateways ) : ?>\n\t\t\t\t<li class=\"llms-payment-gateway-error\"><?php esc_html_e( 'There are no gateways enabled which can support the necessary transaction type for this access plan.', 'lifterlms' ); ?></li>\n\t\t\t<?php endif; ?>\n\t\t<?php endif; ?>\n\t<?php endif; ?>\n</ul>\n"
  },
  {
    "path": "templates/checkout/form-summary.php",
    "content": "<?php\n/**\n * Order Summary area of the checkout form\n *\n * @since     2.4.2\n * @version   3.21.1\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n<ul class=\"llms-order-summary<?php echo $plan->is_on_sale() ? ' on-sale' : ''; ?><?php echo $coupon ? ' has-coupon' : ''; ?>\">\n\t<li><span class=\"llms-label\"><?php echo esc_html( $product->get_post_type_label( 'singular_name' ) ); ?>:</span> <?php echo esc_html( $product->get( 'title' ) ); ?></li>\n\t<li><span class=\"llms-label\"><?php esc_html_e( 'Access Plan', 'lifterlms' ); ?>:</span> <?php echo esc_html( $plan->get( 'title' ) ); ?></li>\n\t<?php if ( $plan->has_trial() ) : ?>\n\t\t<li class=\"llms-pricing llms-pricing-trial<?php echo ( $coupon && $coupon->has_trial_discount() ) ? ' has-coupon' : ''; ?>\">\n\t\t\t<span class=\"llms-label\"><?php esc_html_e( 'Trial', 'lifterlms' ); ?>:</span>\n\t\t\t<span class=\"price-regular price-trial\"><?php echo wp_kses( $plan->get_price( 'trial_price' ), LLMS_ALLOWED_HTML_PRICES ); ?></span>\n\t\t\t<?php if ( $coupon && $coupon->has_trial_discount() ) : ?>\n\t\t\t\t<span class=\"price-coupon\"><?php echo wp_kses( $plan->get_price_with_coupon( 'trial_price', $coupon ), LLMS_ALLOWED_HTML_PRICES ); ?></span>\n\t\t\t<?php endif; ?>\n\t\t\t<?php echo esc_html( $plan->get_trial_details() ); ?>\n\t\t</li>\n\t<?php endif; ?>\n\t<li class=\"llms-pricing llms-pricing-main<?php echo $plan->is_on_sale() ? ' on-sale' : ''; ?><?php echo ( $coupon && $coupon->has_main_discount() ) ? ' has-coupon' : ''; ?>\">\n\t\t<span class=\"llms-label\"><?php esc_html_e( 'Terms', 'lifterlms' ); ?>:</span>\n\t\t<span class=\"price-regular\"><?php echo wp_kses( $plan->get_price( 'price' ), LLMS_ALLOWED_HTML_PRICES ); ?></span>\n\t\t<?php if ( $coupon && $coupon->has_main_discount() ) : ?>\n\t\t\t<span class=\"price-coupon\"><?php echo wp_kses( $plan->get_price_with_coupon( $plan->is_on_sale() ? 'sale_price' : 'price', $coupon ), LLMS_ALLOWED_HTML_PRICES ); ?></span>\n\t\t<?php else : ?>\n\t\t\t<?php if ( $plan->is_on_sale() ) : ?>\n\t\t\t\t<span class=\"price-sale\"><?php echo wp_kses( $plan->get_price( 'sale_price' ), LLMS_ALLOWED_HTML_PRICES ); ?></span>\n\t\t\t<?php endif; ?>\n\t\t<?php endif; ?>\n\t\t<?php\n\t\t$schedule = $plan->get_schedule_details();\n\t\tif ( $schedule ) :\n\t\t\t?>\n\t\t\t<?php echo esc_html( $schedule ); ?>\n\t\t<?php endif; ?>\n\t</li>\n\t<?php\n\t$expires = $plan->get_expiration_details();\n\tif ( $expires ) :\n\t\t?>\n\t\t<li><span class=\"llms-label\"><?php esc_html_e( 'Access', 'lifterlms' ); ?>:</span> <?php echo esc_html( $expires ); ?></li>\n\t<?php endif; ?>\n\t<?php\n\t\t/**\n\t\t * Action hook fired at the end of the checkout Order Summary area.\n\t\t */\n\t\tdo_action( 'llms_checkout_order_summary_end', $plan, $product, $coupon );\n\t?>\n</ul>\n"
  },
  {
    "path": "templates/checkout/form-switch-source.php",
    "content": "<?php\n/**\n * User form used to switch the payment source for recurring payment orders.\n *\n * Included on single order view pages via Student Dashboard.\n *\n * @package LifterLMS/Templates\n *\n * @since 3.10.0\n * @since 7.0.0 Use {@see LLMS_Order::get_switch_source_action()} to determine the switch source action input value.\n * @since 7.5.0 Pass the `LLMS_Order` instance to the form-gateways template.\n * @version 7.0.0\n *\n * @var string     $confirm The ID of the payment gateway when confirming a switch.\n * @var LLMS_Order $order   The order object.\n */\ndefined( 'ABSPATH' ) || exit;\n\n$status        = $order->get( 'status' );\n$gateway       = llms()->payment_gateways()->get_gateway_by_id( $confirm );\n$order_gateway = llms()->payment_gateways()->get_gateway_by_id( $order->get( 'payment_gateway' ) );\n$plan          = llms_get_post( $order->get( 'plan_id' ) );\nif ( ! $plan ) {\n\treturn;\n}\nif ( 'llms-active' === $status ) {\n\tif ( $order_gateway && $order_gateway->is_external_payment_entry() ) {\n\t\t$submit_text = __( 'Save and Continue', 'lifterlms' );\n\t} else {\n\t\t$submit_text = __( 'Save Payment Method', 'lifterlms' );\n\t}\n} elseif ( 'llms-pending-cancel' === $status ) {\n\t$submit_text = __( 'Reactivate Subscription', 'lifterlms' );\n} elseif ( $order_gateway && $order_gateway->is_external_payment_entry() ) {\n\t$submit_text = __( 'Save and Continue', 'lifterlms' );\n} else {\n\t$submit_text = __( 'Save and Pay Now', 'lifterlms' );\n}\n?>\n\n<form action=\"\" class=\"llms-switch-payment-source llms-checkout-wrapper\" id=\"llms-product-purchase-form\" method=\"POST\">\n\n\t<?php\n\tllms_form_field(\n\t\tarray(\n\t\t\t'columns'     => 12,\n\t\t\t'classes'     => 'llms-button-primary',\n\t\t\t'id'          => 'llms_update_payment_method',\n\t\t\t'value'       => 'llms-pending-cancel' === $status ? __( 'Reactivate Subscription', 'lifterlms' ) : __( 'Update Payment Method', 'lifterlms' ),\n\t\t\t'last_column' => true,\n\t\t\t'required'    => false,\n\t\t\t'type'        => 'button',\n\t\t)\n\t);\n\t?>\n\n\t<div class=\"llms-switch-payment-source-main llms-checkout-section\"<?php echo $confirm ? ' style=\"display:block;\"' : ''; ?>>\n\n\t\t<?php if ( ! $confirm ) : ?>\n\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'checkout/form-gateways.php',\n\t\t\t\tarray(\n\t\t\t\t\t'gateways'         => llms()->payment_gateways()->get_enabled_payment_gateways(),\n\t\t\t\t\t'selected_gateway' => $order->get( 'payment_gateway' ),\n\t\t\t\t\t'plan'             => null,\n\t\t\t\t\t'order'            => $order,\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\n\t\t\t<?php if ( ! in_array( $status, array( 'llms-active', 'llms-pending-cancel' ) ) ) : ?>\n\t\t\t\t<ul class=\"llms-order-summary\">\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t// Translators: %s = formatted price / amount due.\n\t\t\t\t\t\techo wp_kses( sprintf( __( 'Due Now: %s', 'lifterlms' ), '<span class=\"price-regular\">' . $order->get_price( 'total' ) . '</span>' ), LLMS_ALLOWED_HTML_PRICES );\n\t\t\t\t\t\t?>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t<?php endif; ?>\n\n\t\t<?php elseif ( $confirm && $gateway ) : ?>\n\n\t\t\t<div class=\"llms-payment-method llms-payment-gateway <?php echo esc_attr( $confirm ); ?>\">\n\t\t\t\t<?php do_action( 'lifterlms_checkout_confirm_before_payment_method', $gateway->get_id(), 'switch' ); ?>\n\t\t\t\t<span class=\"llms-gateway-title\"><span class=\"llms-label\"><?php esc_html_e( 'Payment Method:', 'lifterlms' ); ?></span> <?php echo esc_html( $gateway->get_title() ); ?></span>\n\t\t\t\t<?php if ( $gateway->get_icon() ) : ?>\n\t\t\t\t\t<span class=\"llms-gateway-icon llms-description\"><?php echo wp_kses_post( $gateway->get_icon() ); ?></span>\n\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php if ( $gateway->get_description() ) : ?>\n\t\t\t\t\t<div class=\"llms-gateway-description\"><?php echo wp_kses_post( wpautop( wptexturize( $gateway->get_description() ) ) ); ?></div>\n\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php do_action( 'lifterlms_checkout_confirm_after_payment_method', $gateway->get_id(), 'switch' ); ?>\n\t\t\t</div>\n\n\t\t\t<input name=\"llms_payment_gateway\" type=\"hidden\" value=\"<?php echo esc_attr( $gateway->get_id() ); ?>\">\n\n\t\t<?php endif; ?>\n\n\t\t<?php wp_nonce_field( 'llms_switch_order_source', '_switch_source_nonce' ); ?>\n\t\t<input name=\"order_id\" type=\"hidden\" value=\"<?php echo esc_attr( $order->get( 'id' ) ); ?>\">\n\t\t<input name=\"llms_switch_action\" type=\"hidden\" value=\"<?php echo esc_attr( $order->get_switch_source_action() ); ?>\">\n\n\t\t<?php\n\t\tif ( apply_filters( 'llms_show_switch_save_payment_method_button', true, $order ) ) :\n\n\t\t\tllms_form_field(\n\t\t\t\tarray(\n\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t'classes'     => 'llms-button-primary',\n\t\t\t\t\t'id'          => 'llms_save_payment_method',\n\t\t\t\t\t'value'       => $submit_text,\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t)\n\t\t\t);\n\n\t\tendif;\n\t\t?>\n\n\t</div>\n\n</form>\n"
  },
  {
    "path": "templates/content-certificate.php",
    "content": "<?php\n/**\n * Single certificate content.\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 4.21.0 Make certificate background alt localizable.\n * @since 6.0.0 Moved HTML content to `templates/certificates/content.php` and `templates/certificates/actions.php`.\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$certificate = llms_get_certificate( get_the_ID(), true );\n\n/**\n * Action triggered to display a single certificate.\n *\n * @since 6.0.0\n *\n * @hooked llms_certificate_content - 10.\n * @hooked llms_certificate_actions - 20.\n *\n * @param LLMS_User_Certificate $certificate The certificate object.\n */\ndo_action( 'llms_display_certificate', $certificate );\n"
  },
  {
    "path": "templates/content-no-access-after.php",
    "content": "<?php\n/**\n * Template displayed after content on a restricted page\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 4.17.0 Removed redundant notice output call and replaced duplicated hook with a new hook.\n * @version 4.17.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Action triggered after the main content of a restricted page is rendered\n *\n * @since 4.17.0\n */\ndo_action( 'lifterlms_no_access_after' );\n"
  },
  {
    "path": "templates/content-no-access-before.php",
    "content": "<?php\n/**\n * Template displayed before the main content on a restricted page\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 4.17.0 Removed redundant notice output call and replaced duplicated hook with a new hook.\n * @version 4.17.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_print_notices();\n\n/**\n * Action triggered before the main content of a restricted page is rendered\n *\n * @since 4.17.0\n */\ndo_action( 'lifterlms_no_access_main_content' );\n"
  },
  {
    "path": "templates/content-no-access.php",
    "content": "<?php\n/**\n * The Template for displaying all single courses.\n *\n * @author      codeBOX\n * @package     lifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\ndo_action( 'lifterlms_before_main_content' );\n?>\n\t<div id=\"post-<?php the_ID(); ?>\" <?php post_class(); ?>>\n\n\n\t\t<div class=\"llms-summary entry-content\">\n\t\t<?php llms_print_notices(); ?>\n\n\t\t\t<?php\n\t\t\t\tdo_action( 'before_lifterlms_no_access_main_content' );\n\n\t\t\t\tdo_action( 'lifterlms_no_access_main_content' );\n\n\t\t\t\tdo_action( 'after_lifterlms_no_access_main_content' );\n\t\t\t?>\n\n\t\t</div>\n\n\t</div>\n<?php do_action( 'lifterlms_after_main_content' ); ?>\n"
  },
  {
    "path": "templates/content-single-course-after.php",
    "content": "<?php\n/**\n * Single Course After\n *\n * @author   LifterLMS\n * @package  LifterLMS/Templates\n * @since    1.0.0\n * @version  3.0.3\n */\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * @hooked - lifterlms_template_single_meta_wrapper_start - 5\n * @hooked - lifterlms_template_single_length - 10\n * @hooked - lifterlms_template_single_difficulty - 20\n * @hooked - lifterlms_template_single_course_tracks - 25\n * @hooked - lifterlms_template_single_course_categories - 30\n * @hooked - lifterlms_template_single_course_tags - 35\n * @hooked - lifterlms_template_course_author - 40\n * @hooked - lifterlms_template_single_meta_wrapper_end - 50\n * @hooked - lifterlms_template_single_prerequisites - 55\n * @hooked - lifterlms_template_pricing_table - 60\n * @hooked - lifterlms_template_single_course_progress - 60\n * @hooked - lifterlms_template_single_syllabus - 90\n */\ndo_action( 'lifterlms_single_course_after_summary' );\n"
  },
  {
    "path": "templates/content-single-course-before.php",
    "content": "<?php\n/**\n * The Template for displaying all single courses.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * @todo Move these notices somewhere else.\n */\n$course = new LLMS_Course( get_the_ID() );\n\nif ( 'yes' === $course->get( 'time_period' ) ) {\n\t// If the start date hasn't passed yet.\n\tif ( $course->get( 'start_date' ) && ! $course->has_date_passed( 'start_date' ) ) {\n\n\t\tllms_add_notice( $course->get( 'course_opens_message' ), 'notice' );\n\n\t} elseif ( $course->has_date_passed( 'end_date' ) ) {\n\n\t\tllms_add_notice( $course->get( 'course_closed_message' ), 'error' );\n\n\t}\n}\n\nllms_print_notices();\ndo_action( 'lifterlms_single_course_before_summary' );\n"
  },
  {
    "path": "templates/content-single-lesson-after.php",
    "content": "<?php\n/**\n * Display content after lesson content\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * Hook: lifterlms_single_lesson_after_summary\n *\n * @hooked - lifterlms_template_complete_lesson_link - 10\n * @hooked - lifterlms_template_lesson_navigation    - 20\n */\ndo_action( 'lifterlms_single_lesson_after_summary' );\n"
  },
  {
    "path": "templates/content-single-lesson-before.php",
    "content": "<?php\n/**\n * Display content before lessons\n *\n * @since   1.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_print_notices();\n\n/**\n * @hooked - lifterlms_template_single_parent_course - 10\n * @hooked - llms_template_favorite - 10\n * @hooked - lifterlms_template_single_lesson_video  -  20\n * @hooked - lifterlms_template_single_lesson_audio  -  20\n */\ndo_action( 'lifterlms_single_lesson_before_summary' );\n"
  },
  {
    "path": "templates/content-single-membership-after.php",
    "content": "<?php\n/**\n * The Template for displaying all single membership.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\ndo_action( 'lifterlms_single_membership_after_summary' );\n"
  },
  {
    "path": "templates/content-single-membership-before.php",
    "content": "<?php\n/**\n * The Template for displaying all single membership.\n *\n * @author      codeBOX\n * @package     lifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_print_notices();\ndo_action( 'lifterlms_single_membership_before_summary' );\n\n\n"
  },
  {
    "path": "templates/content-single-question.php",
    "content": "<?php\n/**\n * Single Question Template\n *\n * @since 1.0.0\n * @since 3.16.0 Unknown.\n * @since 7.8.0 Pass the `$attempt` object when retrieving the question content via `$question->get_question();`\n * @version 7.8.0\n *\n * @arg  $attempt  (obj)  LLMS_Quiz_Attempt instance\n * @arg  $question (obj)  LLMS_Question instance\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * lifterlms_single_question_before_summary\n *\n * @hooked lifterlms_template_question_wrapper_start - 10\n */\ndo_action( 'lifterlms_single_question_before_summary', $args ); ?>\n\n\t<h3 class=\"llms-question-text\">\n\t<?php\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in the question templates.\n\t\t\techo $question->get_question( 'html', $attempt );\n\t?>\n\t</h3>\n\n\t<?php\n\t\t/**\n\t\t * lifterlms_single_question_content\n\t\t *\n\t\t * @hooked lifterlms_template_question_description - 10\n\t\t * @hooked lifterlms_template_question_image - 20\n\t\t * @hooked lifterlms_template_question_video - 30\n\t\t * @hooked lifterlms_template_question_content - 40\n\t\t */\n\t\tdo_action( 'lifterlms_single_question_content', $args );\n\t?>\n\n<?php\n/**\n * lifterlms_single_question_after_summary\n *\n * @hooked lifterlms_template_question_wrapper_end - 10\n */\ndo_action( 'lifterlms_single_question_after_summary', $args );\n"
  },
  {
    "path": "templates/content-single-quiz-after.php",
    "content": "<?php\n/**\n * LifterLMS Single Quiz After\n *\n * @since   1.0.0\n * @version 3.16.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\ndo_action( 'lifterlms_single_quiz_after_summary' );\n"
  },
  {
    "path": "templates/content-single-quiz-before.php",
    "content": "<?php\n/**\n * LifterLMS Single Quiz Before\n *\n * @since   1.0.0\n * @version 3.16.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_print_notices();\n\ndo_action( 'lifterlms_single_quiz_before_summary' );\n"
  },
  {
    "path": "templates/course/audio.php",
    "content": "<?php\n/**\n * @author      codeBOX\n * @package     lifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$course = new LLMS_Course( $post );\n\nif ( ! $course->get_audio() ) {\n\treturn; }\n?>\n\n<div class=\"llms-audio-wrapper\">\n\t<div class=\"center-audio\">\n\t\t<?php\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo $course->get_audio();\n\t\t?>\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/course/author.php",
    "content": "<?php\n/**\n * LifterLMS Course Instructors Info\n *\n * @package LifterLMS/Templates/Course\n *\n * @since 3.0.0\n * @since 4.11.0 Use `llms_template_instructors()`.\n * @version 4.11.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_template_instructors();\n"
  },
  {
    "path": "templates/course/categories.php",
    "content": "<?php\n/**\n * Course categories template\n *\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n// Return if the course doesn't have a course_cat.\nif ( ! has_term( '', 'course_cat', $post->ID ) ) {\n\treturn;\n}\n\n?>\n\n<div class=\"llms-meta llms-categories\">\n\t<p><?php echo get_the_term_list( $post->ID, 'course_cat', __( 'Categories: ', 'lifterlms' ), ', ', '' ); ?></p>\n</div>\n"
  },
  {
    "path": "templates/course/complete-lesson-link.php",
    "content": "<?php\n/**\n * Lesson Progression actions\n * Mark Complete & Mark Incomplete buttons\n * Take Quiz Button when quiz attached\n *\n * @since 1.0.0\n * @since 3.33.0 Only render on lesson post types.\n * @version 3.33.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$lesson = llms_get_post( $post );\nif ( ! $lesson || ! is_a( $lesson, 'LLMS_Lesson' ) ) {\n\treturn;\n}\n\nif ( ! llms_is_user_enrolled( get_current_user_id(), $lesson->get( 'parent_course' ) ) && ! current_user_can( 'edit_post', $lesson->get( 'id' ) ) ) {\n\treturn;\n}\n\n$student = llms_get_student( get_current_user_id() );\n?>\n\n<div class=\"clear\"></div>\n<div class=\"llms-lesson-button-wrapper\">\n\n\t<?php do_action( 'llms_before_lesson_buttons', $lesson, $student ); ?>\n\n\t<?php if ( $student->is_complete( $lesson->get( 'id' ), 'lesson' ) ) : ?>\n\n\t\t<?php if ( llms_show_mark_complete_button( $lesson ) ) : ?>\n\n\t\t\t<?php echo wp_kses_post( apply_filters( 'llms_lesson_complete_text', esc_html__( 'Lesson Complete', 'lifterlms' ) ) ); ?>\n\t\t\t<?php do_action( 'llms_after_lesson_complete_text', $lesson ); ?>\n\n\t\t\t<?php if ( 'yes' === get_option( 'lifterlms_retake_lessons', 'no' ) || apply_filters( 'lifterlms_retake_lesson_' . $lesson->get( 'parent_course' ), false ) ) : ?>\n\n\t\t\t\t<form action=\"\" class=\"llms-incomplete-lesson-form\" method=\"POST\" name=\"mark_incomplete\">\n\n\t\t\t\t\t<?php do_action( 'lifterlms_before_mark_incomplete_lesson' ); ?>\n\n\t\t\t\t\t<input type=\"hidden\" name=\"mark-incomplete\" value=\"<?php echo esc_attr( $lesson->get( 'id' ) ); ?>\" />\n\t\t\t\t\t<input type=\"hidden\" name=\"action\" value=\"mark_incomplete\" />\n\t\t\t\t\t<?php wp_nonce_field( 'mark_incomplete' ); ?>\n\n\t\t\t\t\t<?php\n\t\t\t\t\tllms_form_field(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t\t\t'classes'     => 'llms-button-secondary auto button',\n\t\t\t\t\t\t\t'id'          => 'llms_mark_incomplete',\n\t\t\t\t\t\t\t'value'       => apply_filters( 'lifterlms_mark_lesson_incomplete_button_text', __( 'Mark Incomplete', 'lifterlms' ), $lesson ),\n\t\t\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t\t\t'name'        => 'mark_incomplete',\n\t\t\t\t\t\t\t'required'    => false,\n\t\t\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\n\t\t\t\t\t<?php do_action( 'lifterlms_after_mark_incomplete_lesson' ); ?>\n\n\t\t\t\t</form>\n\n\t\t\t<?php endif; ?>\n\n\t\t<?php endif; ?>\n\n\t<?php else : ?>\n\n\t\t<?php if ( llms_show_mark_complete_button( $lesson ) ) : ?>\n\n\t\t\t<form action=\"\" class=\"llms-complete-lesson-form\" method=\"POST\" name=\"mark_complete\">\n\n\t\t\t\t<?php do_action( 'lifterlms_before_mark_complete_lesson' ); ?>\n\n\t\t\t\t<input type=\"hidden\" name=\"mark-complete\" value=\"<?php echo esc_attr( $lesson->get( 'id' ) ); ?>\" />\n\t\t\t\t<input type=\"hidden\" name=\"action\" value=\"mark_complete\" />\n\t\t\t\t<?php wp_nonce_field( 'mark_complete' ); ?>\n\n\t\t\t\t<?php\n\t\t\t\tllms_form_field(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t\t'classes'     => 'llms-button-primary auto button',\n\t\t\t\t\t\t'id'          => 'llms_mark_complete',\n\t\t\t\t\t\t'value'       => apply_filters( 'lifterlms_mark_lesson_complete_button_text', __( 'Mark Complete', 'lifterlms' ), $lesson ),\n\t\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t\t'name'        => 'mark_complete',\n\t\t\t\t\t\t'required'    => false,\n\t\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\n\t\t\t\t<?php do_action( 'lifterlms_after_mark_complete_lesson' ); ?>\n\n\t\t\t</form>\n\n\t\t<?php endif; ?>\n\n\t<?php endif; ?>\n\n\t<?php if ( llms_show_take_quiz_button( $lesson ) ) : ?>\n\n\t\t<?php do_action( 'llms_before_start_quiz_button' ); ?>\n\n\t\t<a class=\"llms-button-action auto button\" id=\"llms_start_quiz\" href=\"<?php echo esc_url( get_permalink( $lesson->get( 'quiz' ) ) ); ?>\">\n\t\t\t<?php echo wp_kses_post( apply_filters( 'lifterlms_start_quiz_button_text', esc_html__( 'Take Quiz', 'lifterlms' ), $lesson->get( 'quiz' ), $lesson ) ); ?>\n\t\t</a>\n\n\t\t<?php do_action( 'llms_after_start_quiz_button' ); ?>\n\n\t<?php endif; ?>\n\n\t<?php do_action( 'llms_after_lesson_buttons', $lesson, $student ); ?>\n\n</div>\n"
  },
  {
    "path": "templates/course/difficulty.php",
    "content": "<?php\n/**\n * Course difficulty template\n *\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$course = new LLMS_Course( $post );\n\nif ( ! $course->get_difficulty() ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-meta llms-difficulty\">\n\t<p><?php echo wp_kses_post( sprintf( __( 'Difficulty: <span class=\"difficulty\">%s</span>', 'lifterlms' ), $course->get_difficulty() ) ); ?></p>\n</div>\n"
  },
  {
    "path": "templates/course/favorite.php",
    "content": "<?php\n/**\n * Template for favorite button.\n *\n * @author LifterLMS\n * @package LifterLMS/Templates\n *\n * @since 7.5.0\n *\n * @param int    $object_id   WP Post ID of the object to mark/unmark as favorite.\n * @param string $object_type The object type, currently only 'lesson'.\n */\ndefined( 'ABSPATH' ) || exit;\n\n$lesson = null;\nif ( 'lesson' === $object_type ) {\n\tglobal $post;\n\t$lesson = llms_get_post( empty( $object_id ) ? $post : $object_id );\n}\n\nif ( ! $lesson || ! is_a( $lesson, 'LLMS_Lesson' ) ) {\n\treturn;\n}\n\n$total_favorites   = llms_get_object_total_favorites( $lesson->get( 'id' ) );\n$student           = llms_get_student( get_current_user_id() );\n$is_favorite       = $student && $student->is_favorite( $lesson->get( 'id' ), 'lesson' );\n$can_mark_favorite = $lesson && ( ( $student && $student->is_enrolled( $lesson->get( 'id' ) ) ) || $lesson->is_free() );\n?>\n\n<div class=\"llms-favorite-wrapper\">\n\n\t<?php\n\t/**\n\t * Action fired before Favorite Button Hook.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param LLMS_Lesson  $lesson  Lesson object.\n\t * @param LLMS_Student $student Student object.\n\t */\n\tdo_action( 'llms_before_favorite_button', $lesson, $student );\n\t?>\n\n\t<?php if ( ! is_user_logged_in() || ( ! $can_mark_favorite && ! $is_favorite ) ) { ?>\n\n\t\t<i class=\"fa fa-heart-o llms-favorite-btn llms-heart-btn\"></i>\n\n\t<?php } else { ?>\n\n\t\t<?php if ( $is_favorite ) : ?>\n\n\t\t\t<?php /* Translators: %s: lesson title */ ?>\n\t\t\t<button aria-label=\"<?php echo esc_attr( sprintf( __( 'Toggle favorite for lesson %s', 'lifterlms' ), get_the_title( $lesson->get( 'id' ) ) ) ); ?>\" data-action=\"unfavorite\" aria-pressed=\"true\" data-type=\"lesson\" data-id=\"<?php echo esc_attr( $lesson->get( 'id' ) ); ?>\" class=\"llms-unfavorite-btn\">\n\t\t\t\t<i class=\"fa fa-heart llms-heart-btn\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t<?php else : ?>\n\n\t\t\t<?php /* Translators: %s: lesson title */ ?>\n\t\t\t<button aria-label=\"<?php echo esc_attr( sprintf( __( 'Toggle favorite for lesson %s', 'lifterlms' ), get_the_title( $lesson->get( 'id' ) ) ) ); ?>\" data-action=\"favorite\" aria-pressed=\"false\" data-type=\"lesson\" data-id=\"<?php echo esc_attr( $lesson->get( 'id' ) ); ?>\" class=\"llms-favorite-btn\">\n\t\t\t\t<i class=\"fa fa-heart-o llms-heart-btn\" aria-hidden=\"true\"></i>\n\t\t\t</button>\n\n\t\t<?php endif; ?>\n\n\t<?php } ?>\n\n\t<span class=\"llms-favorites-count\" aria-label=\"<?php esc_attr_e( 'Total favorites for this lesson', 'lifterlms' ); ?>\">\n\t\t<?php echo esc_html( $total_favorites ); ?>\n\t</span>\n\n\t<?php\n\t/**\n\t * Action fired after Favorite Button Hook.\n\t *\n\t * @since 7.5.0\n\t *\n\t * @param LLMS_Lesson  $lesson  Lesson object.\n\t * @param LLMS_Student $student Student object.\n\t */\n\tdo_action( 'llms_after_favorite_button', $lesson, $student );\n\t?>\n\n</div>\n"
  },
  {
    "path": "templates/course/full-description.php",
    "content": "<?php\n/**\n * @author      codeBOX\n * @package     lifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n?>\n<div class=\"llms-full-description\">\n\t<?php echo wp_kses_post( apply_filters( 'lifterlms_full_description', do_shortcode( $post->post_content ) ) ); ?>\n</div>\n<div class=\"clear\"></div>\n"
  },
  {
    "path": "templates/course/length.php",
    "content": "<?php\n/**\n * LifterLMS Course Length Meta Info\n *\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$course = new LLMS_Course( $post );\n\nif ( ! $course->get( 'length' ) ) {\n\treturn; }\n?>\n\n<div class=\"llms-meta llms-course-length\">\n\t<p><?php echo wp_kses_post( sprintf( __( 'Estimated Time: <span class=\"length\">%s</span>', 'lifterlms' ), $course->get( 'length' ) ) ); ?></p>\n</div>\n"
  },
  {
    "path": "templates/course/lesson-count.php",
    "content": "<?php\n/**\n * Template: Lessons count in a Course.\n *\n * @package LifterLMS/Templates/Course\n *\n * @since 7.5.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$course        = new LLMS_Course( $post );\n$lessons_count = $course->get_lessons_count();\n?>\n\n<div class=\"llms-meta llms-lessons-count\">\n\t<p>\n\t\t<?php\n\t\t\t// Translators: %1$s = Lessons Count.\n\t\t\techo wp_kses_post( sprintf( esc_html__( 'Number of lessons: %1$s', 'lifterlms' ), '<span class=\"lessons-count\">' . $lessons_count . '</span>' ) );\n\t\t?>\n\t</p>\n</div>\n"
  },
  {
    "path": "templates/course/lesson-navigation.php",
    "content": "<?php\n/**\n * Lesson navigation template\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown Introduced.\n * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n * @version 5.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$lesson = new LLMS_Lesson( $post->ID );\n\n$prev_id = $lesson->get_previous_lesson();\n$next_id = $lesson->get_next_lesson();\n?>\n\n<nav class=\"llms-course-navigation\">\n\n\t<?php if ( $prev_id ) : ?>\n\n\t\t<div class=\"llms-course-nav llms-prev-lesson\">\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'course/lesson-preview.php',\n\t\t\t\tarray(\n\t\t\t\t\t'lesson'   => new LLMS_Lesson( $prev_id ),\n\t\t\t\t\t'pre_text' => __( 'Previous Lesson', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t</div>\n\n\t<?php endif; ?>\n\n\t<?php if ( ! $prev_id || ! $next_id ) : ?>\n\t\t<div class=\"llms-course-nav llms-back-to-course\">\n\t\t\t<div class=\"llms-lesson-preview\">\n\t\t\t\t<a class=\"llms-lesson-link\" href=\"<?php echo esc_url( get_permalink( $lesson->get( 'parent_course' ) ) ); ?>\">\n\t\t\t\t\t<section class=\"llms-main\">\n\t\t\t\t\t\t<div class=\"llms-pre-text\"><?php esc_html_e( 'Back to Course', 'lifterlms' ); ?></div>\n\t\t\t\t\t\t<div class=\"llms-lesson-title\"><?php echo esc_html( get_the_title( $lesson->get( 'parent_course' ) ) ); ?></div>\n\t\t\t\t\t</section>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t</div>\n\t<?php endif; ?>\n\n\t<?php if ( $next_id ) : ?>\n\n\t\t<div class=\"llms-course-nav llms-next-lesson\">\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'course/lesson-preview.php',\n\t\t\t\tarray(\n\t\t\t\t\t'lesson'   => new LLMS_Lesson( $next_id ),\n\t\t\t\t\t'pre_text' => __( 'Next Lesson', 'lifterlms' ),\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\t\t</div>\n\n\t<?php endif; ?>\n\n\n\n</nav>\n<div class=\"clear\"></div>\n"
  },
  {
    "path": "templates/course/lesson-preview.php",
    "content": "<?php\n/**\n * Template for a lesson preview element\n *\n * @author LifterLMS\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 3.19.2 Unknown.\n * @since 4.4.0 Use the passed `$order` param if available, in favor of retrieving the lesson's order post meta.\n * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_order()` method with `LLMS_Lesson::get( 'order' )`.\n * @since 7.5.0 Added `llms_lesson_preview_before_title` and `llms_lesson_preview_after_title` action hooks.\n * @version 7.5.0\n *\n * @var LLMS_Lesson $lesson        The lesson object.\n * @var string      $pre_text      The text to display before the lesson.\n * @var int         $total_lessons The number of lessons in the section.\n */\ndefined( 'ABSPATH' ) || exit;\n\n$restrictions = llms_page_restricted( $lesson->get( 'id' ), get_current_user_id() );\n$data_msg     = $restrictions['is_restricted'] ? ' data-tooltip-msg=\"' . esc_html( wp_strip_all_tags( llms_get_restriction_message( $restrictions ) ) ) . '\"' : '';\n\n// Get the section name for this lesson.\n$section       = $lesson->get_parent_section() ? llms_get_post( $lesson->get_parent_section() ) : false;\n$section_title = $section ? $section->post->post_title : '';\nif ( isset( $total_lessons ) && $total_lessons ) {\n\t$lesson_screen_reader_msg = sprintf(\n\t/* translators: 1: lesson order, 2: total lessons, 3: section title */\n\t\t__( 'Lesson %1$d of %2$d within section %3$s.', 'lifterlms' ),\n\t\tisset( $order ) ? $order : $lesson->get( 'order' ),\n\t\t$total_lessons,\n\t\t$section_title\n\t);\n} else {\n\t$lesson_screen_reader_msg = sprintf(\n\t/* translators: 1: lesson order, 2: section title */\n\t\t__( 'Lesson %1$d within section %2$s.', 'lifterlms' ),\n\t\tisset( $order ) ? $order : $lesson->get( 'order' ),\n\t\t$section_title\n\t);\n}\n?>\n\n<div class=\"llms-lesson-preview<?php echo esc_attr( $lesson->get_preview_classes() ); ?>\">\n\t<section\n\t<?php if ( $restrictions['is_restricted'] ) : ?>\n\t\tclass=\"llms-lesson-locked\"\n\t\tdata-tooltip-msg=\"<?php echo esc_attr( wp_strip_all_tags( llms_get_restriction_message( $restrictions ) ) ); ?>\"\n\t<?php endif; ?>\n\t>\n\t\t<?php if ( $restrictions['is_restricted'] ) : ?>\n\t\t\t<div class=\"llms-lesson-link\">\n\t\t<?php else : ?>\n\t\t\t<a class=\"llms-lesson-link\" href=\"<?php echo esc_url( get_permalink( $lesson->get( 'id' ) ) ); ?>\" aria-label=\"<?php echo esc_attr( get_the_title( $lesson->get( 'id' ) ) . ' ' . $lesson_screen_reader_msg ); ?>\">\n\t\t<?php endif; ?>\n\n\t\t\t<?php if ( 'course' === get_post_type( get_the_ID() ) ) : ?>\n\n\t\t\t\t<?php if ( apply_filters( 'llms_display_outline_thumbnails', true ) ) : ?>\n\t\t\t\t\t<?php if ( has_post_thumbnail( $lesson->get( 'id' ) ) ) : ?>\n\t\t\t\t\t\t<div class=\"llms-lesson-thumbnail\">\n\t\t\t\t\t\t\t<?php echo wp_kses_post( get_the_post_thumbnail( $lesson->get( 'id' ) ) ); ?>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t<?php endif; ?>\n\n\t\t\t<div class=\"llms-lesson-preview-row\">\n\n\t\t\t\t<?php if ( 'course' === get_post_type( get_the_ID() ) ) : ?>\n\n\t\t\t\t\t<aside class=\"llms-extra\">\n\t\t\t\t\t\t<span class=\"llms-lesson-counter\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t\t// Translators: %1$d: Lesson number, %2$d: total number of lessons in section.\n\t\t\t\t\t\t\t\techo esc_html( sprintf( _x( '%1$d of %2$d', 'lesson order within section', 'lifterlms' ), isset( $order ) ? $order : $lesson->get( 'order' ), $total_lessons ) );\n\t\t\t\t\t\t\t?>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<?php echo wp_kses_post( $lesson->get_preview_icon_html() ); ?>\n\t\t\t\t\t</aside>\n\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<div class=\"llms-main\">\n\t\t\t\t\t<?php if ( 'lesson' === get_post_type( get_the_ID() ) ) : ?>\n\t\t\t\t\t\t<div class=\"llms-pre-text\"><?php echo wp_kses_post( $pre_text ); ?></div>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php\n\t\t\t\t\t/**\n\t\t\t\t\t * Action fired before the lesson title in the lesson preview template.\n\t\t\t\t\t *\n\t\t\t\t\t * @since 7.5.0\n\t\t\t\t\t *\n\t\t\t\t\t * @param LLMS_Lesson $lesson The lesson's instance.\n\t\t\t\t\t */\n\t\t\t\t\tdo_action( 'llms_lesson_preview_before_title', $lesson )\n\t\t\t\t\t?>\n\t\t\t\t\t<div class=\"llms-lesson-title\"><?php echo esc_html( get_the_title( $lesson->get( 'id' ) ) ); ?></div>\n\n\t\t\t\t\t<?php if ( apply_filters( 'llms_show_preview_excerpt', true ) && llms_get_excerpt( $lesson->get( 'id' ) ) ) : ?>\n\t\t\t\t\t\t<div class=\"llms-lesson-excerpt\"><?php echo wp_kses_post( llms_get_excerpt( $lesson->get( 'id' ) ) ); ?></div>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</div>\n\n\t\t\t\t<span class=\"screen-reader-text\"><?php echo esc_attr( $lesson_screen_reader_msg ); ?></span>\n\n\t\t\t\t<?php if ( $restrictions['is_restricted'] ) : ?>\n\t\t\t\t\t<span class=\"screen-reader-text\"><?php echo esc_html( wp_strip_all_tags( llms_get_restriction_message( $restrictions ) ) ); ?></span>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t</div>\n\n\t\t<?php echo $restrictions['is_restricted'] ? '</div>' : '</a>'; ?>\n\n\t\t<div class=\"llms-lesson-meta\">\n\t\t\t<?php\n\t\t\t/**\n\t\t\t * Action fired after the lesson title in the lesson preview template.\n\t\t\t *\n\t\t\t * @since 7.5.0\n\t\t\t *\n\t\t\t * @param LLMS_Lesson $lesson The lesson's instance.\n\t\t\t */\n\t\t\tdo_action( 'llms_lesson_preview_after_title', $lesson )\n\t\t\t?>\n\n\t\t\t<?php\n\t\t\tif ( 'lesson' !== get_post_type( get_the_ID() ) ) :\n\t\t\t\t?>\n\t\t\t\t<?php\n\t\t\t\tif ( $lesson->is_quiz_enabled() ) :\n\t\t\t\t\t?>\n\t\t\t\t\t<span class=\"llms-lesson-has-quiz\">\n\t\t\t\t\t\t<i class=\"fa fa-question-circle\"></i>\n\t\t\t\t\t<?php esc_html_e( 'Has Quiz', 'lifterlms' ); ?>\n\t\t\t\t\t</span>\n\t\t\t\t\t<?php\n\t\t\t\tendif;\n\t\t\t\t?>\n\n\t\t\t\t<?php\n\t\t\t\tif ( function_exists( 'llms_lesson_has_assignment' ) && llms_lesson_has_assignment( $lesson->get( 'id' ) ) ) :\n\t\t\t\t\t?>\n\t\t\t\t\t<span class=\"llms-lesson-has-assignment\">\n\t\t\t\t\t\t<i class=\"fa fa-pencil-square\"></i>\n\t\t\t\t\t<?php esc_html_e( 'Has Assignment', 'lifterlms' ); ?>\n\t\t\t\t\t</span>\n\t\t\t\t\t<?php\n\t\t\t\tendif;\n\t\t\t\t?>\n\t\t\t<?php endif; ?>\n\t\t</div>\n\t</section>\n</div>\n"
  },
  {
    "path": "templates/course/meta-wrapper-end.php",
    "content": "<?php\n/**\n * LifterLMS Course Meta Information Wrapper End.\n *\n * @package  LifterLMS/Templates\n * @since    3.0.0\n * @version  3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n</div><!-- .llms-meta-info -->\n"
  },
  {
    "path": "templates/course/meta-wrapper-start.php",
    "content": "<?php\n/**\n * LifterLMS Course Meta Information Wrapper Start.\n *\n * @package  LifterLMS/Templates\n * @since    3.0.0\n * @version  3.25.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$title_tag = apply_filters( 'llms_course_meta_info_title_size', 'h3' );\nif ( ! in_array( $title_tag, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ) ) ) {\n\t$title_tag = 'h3';\n}\n?>\n\n<div class=\"llms-meta-info\">\n\t<<?php echo esc_html( $title_tag ); ?> class=\"llms-meta-title\">\n\t\t<?php echo wp_kses_post( apply_filters( 'llms_course_meta_info_title', esc_html_x( 'Course Information', 'course meta info title', 'lifterlms' ) ) ); ?>\n\t</<?php echo esc_html( $title_tag ); ?>>\n"
  },
  {
    "path": "templates/course/outline-list-small.php",
    "content": "<?php\n/**\n * Course Outline Small List\n * Used for lifterlms_course_outline Shortcode & Course Syllabus Widget\n *\n * @property  $collapse         bool   whether or not sections are collapsible via user interaction\n * @property  $course           obj    instance of the LLMS_Course for the current course\n * @property  $current_section  int    WP Post ID of the current section, this determines which section is open when the outline is collapsible\n * @property  $current_lesson   int    WP Post ID of the lesson being currently viewed, will be null if used outside of a lesson\n * @property  $sections         array  array of LLMS_Sections\n * @property  $student          obj    Instance of the LLMS_Student for the current user\n * @property  $toggles          bool   whether or not open/close all toggles should display in the outline footer. Only works when $collapse is also true\n * @since     1.0.0\n * @version   3.19.2\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"llms-widget-syllabus<?php echo ( $collapse ) ? ' llms-widget-syllabus--collapsible' : ''; ?>\">\n\n\t<?php do_action( 'lifterlms_outline_before' ); ?>\n\n\t<ul class=\"llms-course-outline\">\n\n\t\t<?php foreach ( $sections as $section ) : ?>\n\n\t\t\t<li class=\"llms-section<?php echo ( $collapse ) ? ( ( $section->get( 'id' ) == $current_section ) ? ' llms-section--opened' : ' llms-section--closed' ) : ''; ?>\">\n\n\t\t\t\t<div class=\"section-header\">\n\n\t\t\t\t\t<?php do_action( 'lifterlms_outline_before_header' ); ?>\n\n\t\t\t\t\t<?php if ( $collapse ) : ?>\n\n\t\t\t\t\t\t<span class=\"llms-collapse-caret\">\n\t\t\t\t\t\t\t<i class=\"fa fa-caret-down\"></i>\n\t\t\t\t\t\t\t<i class=\"fa fa-caret-right\"></i>\n\t\t\t\t\t\t</span>\n\n\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t<span class=\"section-title\"><?php echo wp_kses_post( apply_filters( 'llms_widget_syllabus_section_title', $section->get( 'title' ), $section ) ); ?></span>\n\n\t\t\t\t\t<?php do_action( 'lifterlms_outline_after_header' ); ?>\n\n\t\t\t\t</div>\n\n\t\t\t\t<?php\n\t\t\t\tforeach ( $section->get_lessons() as $lesson ) :\n\t\t\t\t\t$current     = ( $current_lesson == $lesson->get( 'id' ) );\n\t\t\t\t\t$is_complete = $student ? $student->is_complete( $lesson->get( 'id' ), 'lesson' ) : false;\n\t\t\t\t\t$restricted  = llms_page_restricted( $lesson->get( 'id' ) );\n\t\t\t\t\t?>\n\n\t\t\t\t\t<ul class=\"llms-lesson<?php echo $current ? ' current-lesson' : ''; ?><?php echo $restricted['is_restricted'] ? ' llms-lesson-locked' : ''; ?>\"<?php echo $restricted['is_restricted'] ? ' data-tooltip-msg=\"' . esc_html( wp_strip_all_tags( llms_get_restriction_message( $restricted ) ) ) . '\"' : ''; ?>>\n\n\t\t\t\t\t\t<li>\n\n\t\t\t\t\t\t\t<span class=\"llms-lesson-complete <?php echo ( $is_complete ? 'done' : '' ); ?>\">\n\t\t\t\t\t\t\t\t<i class=\"fa fa-check-circle\"></i>\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<?php do_action( 'lifterlms_outline_before_lesson_title', $lesson ); ?>\n\n\t\t\t\t\t\t\t<span class=\"lesson-title <?php echo ( $is_complete ? 'done' : '' ); ?>\">\n\n\t\t\t\t\t\t\t\t<?php if ( $lesson->is_free() || ( $student && ! $restricted['is_restricted'] ) ) : ?>\n\n\t\t\t\t\t\t\t\t\t<a href=\"<?php echo esc_url( get_permalink( $lesson->get( 'id' ) ) ); ?>\">\n\t\t\t\t\t\t\t\t\t\t<?php echo wp_kses_post( apply_filters( 'llms_widget_syllabus_section_title', $lesson->get( 'title' ) ) ); ?>\n\t\t\t\t\t\t\t\t\t</a>\n\n\t\t\t\t\t\t\t\t<?php else : ?>\n\n\t\t\t\t\t\t\t\t\t<?php echo wp_kses_post( apply_filters( 'llms_widget_syllabus_section_title', $lesson->get( 'title' ) ) ); ?>\n\n\t\t\t\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<?php do_action( 'lifterlms_outline_after_lesson_title', $lesson ); ?>\n\n\t\t\t\t\t\t</li>\n\n\t\t\t\t\t</ul>\n\n\t\t\t\t<?php endforeach; ?>\n\n\t\t\t</li>\n\n\t\t<?php endforeach; ?>\n\n\t\t<?php if ( $collapse && $toggles ) : ?>\n\n\t\t\t<li class=\"llms-section llms-syllabus-footer\">\n\n\t\t\t\t<?php do_action( 'lifterlms_outline_before_footer' ); ?>\n\n\t\t\t\t<a class=\"llms-button-text llms-collapse-toggle\" data-action=\"open\" href=\"#\"><?php esc_html_e( 'Open All', 'lifterlms' ); ?></a>\n\t\t\t\t<span>&middot;</span>\n\t\t\t\t<a class=\"llms-button-text llms-collapse-toggle\" data-action=\"close\" href=\"#\"><?php esc_html_e( 'Close All', 'lifterlms' ); ?></a>\n\n\t\t\t\t<?php do_action( 'lifterlms_outline_after_footer' ); ?>\n\n\t\t\t</li>\n\n\t\t<?php endif; ?>\n\n\t</ul>\n\n\t<?php do_action( 'lifterlms_outline_after' ); ?>\n\n</div>\n"
  },
  {
    "path": "templates/course/parent-course.php",
    "content": "<?php\n/**\n * Back to Course Template\n *\n * @package LifterLMS/Templates\n *\n * @since  1.0.0\n * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n * @version 5.7.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$lesson = new LLMS_Lesson( $post );\n\necho wp_kses_post( sprintf( __( '<p class=\"llms-parent-course-link\">Back to: <a class=\"llms-lesson-link\" href=\"%1$s\">%2$s</a></p>', 'lifterlms' ), get_permalink( $lesson->get( 'parent_course' ) ), get_the_title( $lesson->get( 'parent_course' ) ) ) );\n"
  },
  {
    "path": "templates/course/prerequisites.php",
    "content": "<?php\n/**\n * LifterLMS Prerequisite Display\n *\n * @since    3.0.0\n * @version  3.7.5\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$course = new LLMS_Course( $post );\n\n?>\n\n<?php\nif ( $course->has_prerequisite( 'course' ) && ! $course->is_prerequisite_complete( 'course' ) ) :\n\t$prereq_id = $course->get_prerequisite_id( 'course' );\n\t?>\n\n\t<?php llms_print_notice( sprintf( __( 'Before starting this course you must complete the required prerequisite course: %s', 'lifterlms' ), '<a href=\"' . get_permalink( $prereq_id ) . '\">' . get_the_title( $prereq_id ) . '</a>' ), 'error' ); ?>\n\n<?php endif; ?>\n\n<?php\nif ( $course->has_prerequisite( 'course_track' ) && ! $course->is_prerequisite_complete( 'course_track' ) ) :\n\t$track = new LLMS_Track( $course->get_prerequisite_id( 'course_track' ) );\n\t?>\n\n\t<?php llms_print_notice( sprintf( __( 'Before starting this course you must complete the required prerequisite track: %s', 'lifterlms' ), '<a href=\"' . $track->get_permalink() . '\">' . $track->term->name . '</a>' ), 'error' ); ?>\n\n\t<?php\nendif;\n"
  },
  {
    "path": "templates/course/progress.php",
    "content": "<?php\n/**\n * Display a course progress bar and\n * a button for the next incomplete lesson in the course\n *\n * @since    1.0.0\n * @version  3.11.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\nif ( ! llms_is_user_enrolled( get_current_user_id(), $post->ID ) ) {\n\treturn;\n}\n\n$student  = new LLMS_Student();\n$progress = $student->get_progress( $post->ID, 'course' );\n?>\n\n<div class=\"llms-course-progress\">\n\n\t<?php if ( apply_filters( 'lifterlms_display_course_progress_bar', true ) ) : ?>\n\n\t\t<?php lifterlms_course_progress_bar( $progress, false, false ); ?>\n\n\t<?php endif; ?>\n\n\t<?php lifterlms_course_continue_button( $post->ID, $student, $progress ); ?>\n\n</div>\n"
  },
  {
    "path": "templates/course/short-description.php",
    "content": "<?php\n/**\n * @author      codeBOX\n * @package     lifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\necho the_excerpt();\n"
  },
  {
    "path": "templates/course/syllabus.php",
    "content": "<?php\n/**\n * Template for the Course Syllabus Displayed on individual course pages\n *\n * @author LifterLMS\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 3.24.0 Unknown.\n * @since 4.4.0 Pass the progressive lesson order value to the lesson-preview template.\n * @since 7.1.3 Add paragraph tag to wrap message when sections or lessons are empty.\n * @version 7.1.3\n */\ndefined( 'ABSPATH' ) || exit;\nglobal $post;\n$course   = new LLMS_Course( $post );\n$sections = $course->get_sections();\n?>\n\n<div class=\"clear\"></div>\n\n<div class=\"llms-syllabus-wrapper\">\n\n\t<?php if ( ! $sections ) : ?>\n\n\t\t<p><?php esc_html_e( 'This course does not have any sections.', 'lifterlms' ); ?></p>\n\n\t<?php else : ?>\n\n\t\t<?php foreach ( $sections as $section ) : ?>\n\n\t\t\t<?php $lesson_order = 0; ?>\n\n\t\t\t<?php if ( apply_filters( 'llms_display_outline_section_titles', true ) ) : ?>\n\t\t\t\t<h3 class=\"llms-h3 llms-section-title\"><?php echo esc_html( get_the_title( $section->get( 'id' ) ) ); ?></h3>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php $lessons = $section->get_lessons(); ?>\n\t\t\t<?php if ( $lessons ) : ?>\n\n\t\t\t\t<?php foreach ( $lessons as $lesson ) : ?>\n\n\t\t\t\t\t<?php\n\t\t\t\t\tllms_get_template(\n\t\t\t\t\t\t'course/lesson-preview.php',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'lesson'        => $lesson,\n\t\t\t\t\t\t\t'total_lessons' => count( $lessons ),\n\t\t\t\t\t\t\t'order'         => ++$lesson_order,\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\n\t\t\t\t<?php endforeach; ?>\n\n\t\t\t<?php else : ?>\n\n\t\t\t\t<p><?php esc_html_e( 'This section does not have any lessons.', 'lifterlms' ); ?></p>\n\n\t\t\t<?php endif; ?>\n\n\t\t<?php endforeach; ?>\n\n\t<?php endif; ?>\n\n\t<div class=\"clear\"></div>\n\n</div>\n"
  },
  {
    "path": "templates/course/tags.php",
    "content": "<?php\n/**\n * Course tags template\n *\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n// Return if the course doesn't have a course_tag.\nif ( ! has_term( '', 'course_tag', $post->ID ) ) {\n\treturn;\n}\n\n?>\n\n<div class=\"llms-meta llms-tags\">\n\t<p><?php echo get_the_term_list( $post->ID, 'course_tag', __( 'Tags: ', 'lifterlms' ), ', ', '' ); ?></p>\n</div>\n"
  },
  {
    "path": "templates/course/title.php",
    "content": "<?php\n/**\n * @author      codeBOX\n * @package     lifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<h1 class=\"entry-title hentry-title llms-h1 llms-title\"><?php the_title(); ?></h1>\n"
  },
  {
    "path": "templates/course/tracks.php",
    "content": "<?php\n/**\n * Course tracks template\n *\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n// Return if the course doesn't have a track.\nif ( ! has_term( '', 'course_track', $post->ID ) ) {\n\treturn;\n}\n\n?>\n\n<div class=\"llms-meta llms-tracks\">\n\t<p><?php echo get_the_term_list( $post->ID, 'course_track', __( 'Tracks: ', 'lifterlms' ), ', ', '' ); ?></p>\n</div>\n"
  },
  {
    "path": "templates/course/video.php",
    "content": "<?php\n/**\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$course = new LLMS_Course( $post );\n\nif ( ! $course->get_video() ) {\n\treturn; }\n\n?>\n\n<div class=\"llms-video-wrapper\">\n\t<div class=\"center-video\">\n\t\t<?php\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo $course->get_video();\n\t\t?>\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/emails/footer.php",
    "content": "<?php\n/**\n * LifterLMS Emails Footer Template\n *\n * @since    1.0.0\n * @version  3.16.15\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$mailer = llms()->mailer();\n\n$terms = false;\nif ( 'yes' === get_option( 'lifterlms_registration_require_agree_to_terms', 'no' ) ) {\n\t$terms = get_option( 'lifterlms_terms_page_id', false );\n\t// Unset terms if the page is not published.\n\tif ( $terms && 'publish' !== get_post_status( $terms ) ) {\n\t\t$terms = false;\n\t}\n}\n?>\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<!-- END MAIN CONTENT AREA -->\n\t\t\t</table>\n\t\t\t<!-- START FOOTER -->\n\t\t\t<div class=\"footer\" style=\"clear:both;padding-top:10px;text-align:center;width:100%;\">\n\t\t\t\t<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;width:100%;\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"content-block\" style=\"font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size-small' ); ?>;vertical-align:top;color:#999999;text-align:center;\">\n\t\t\t\t\t\t\t<?php echo wp_kses_post( wpautop( wptexturize( apply_filters( 'lifterlms_email_footer_text', get_option( 'lifterlms_email_footer_text' ) ) ) ) ); ?>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"content-block powered-by\" style=\"font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size-small' ); ?>;vertical-align:top;color:#999999;text-align:center;\">\n\t\t\t\t\t\t\t<a href=\"<?php echo esc_url( get_bloginfo( 'url', 'display' ) ); ?>\" style=\"text-decoration:underline;color:<?php $mailer->get_css( 'main-color' ); ?>;\"><?php echo wp_kses_post( get_bloginfo( 'name', 'display' ) ); ?></a>\n\t\t\t\t\t\t\t<?php if ( $terms ) : ?>\n\t\t\t\t\t\t\t\t| <a alt=\"<?php echo esc_attr( get_the_title( $terms ) ); ?>\" href=\"<?php echo esc_url( get_permalink( $terms ) ); ?>\" style=\"text-decoration:underline;color:<?php $mailer->get_css( 'main-color' ); ?>;\"><?php echo esc_html( get_the_title( $terms ) ); ?></a>\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t\t<!-- END FOOTER -->\n\t\t</div>\n\t\t<!-- END CENTERED WHITE CONTAINER -->\n\t</td>\n\t<td style=\"font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size-small' ); ?>;vertical-align:top;\">&nbsp;</td>\n</tr>\n</table>\n</body>\n</html>\n"
  },
  {
    "path": "templates/emails/header.php",
    "content": "<?php\n/**\n * LifterLMS Email Header Template\n *\n * @since    1.0.0\n * @version  3.16.15\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$mailer       = llms()->mailer();\n$header_image = $mailer->get_header_image_src();\n?><!DOCTYPE html>\n<html <?php language_attributes(); ?>>\n<head>\n\t<meta name=\"viewport\" content=\"width=device-width\" />\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=<?php bloginfo( 'charset' ); ?>\" />\n\t<title><?php echo wp_kses_post( get_bloginfo( 'name', 'display' ) ); ?></title>\n\t<style>\n\t@media all {\n\t\ta {\n\t\t\tcolor: <?php $mailer->get_css( 'main-color' ); ?> !important; }\n\t\tp {\n\t\t\tMargin-bottom: 15px !important;\n\t\t\tMargin-top: 0 !important; }\n\t\ttd[class=main-content] *:first-child {\n\t\t\tMargin-top: 0 !important; }\n\t\ttd[class=main-content] *:last-child {\n\t\t\tMargin-bottom: 0 !important; }\n\t\ttable[class=body] img.alignright {\n\t\t\tfloat:right; }\n\t\ttable[class=body] img.alignleft {\n\t\t\tfloat:left; }\n\t\ttable[class=body] img {\n\t\t\tdisplay: block;\n\t\t\theight: auto !important;\n\t\t\tMargin: 0 auto;\n\t\t\tmax-width: 100% !important;\n\t\t\twidth: auto !important; }\n\t\t.ExternalClass {\n\t\t\twidth: 100%; }\n\t\t.ExternalClass,\n\t\t.ExternalClass p,\n\t\t.ExternalClass span,\n\t\t.ExternalClass font,\n\t\t.ExternalClass td,\n\t\t.ExternalClass div {\n\t\t\tline-height: 100%; }\n\t@media only screen and (max-width: 620px) {\n\t\ttable[class=body] p,\n\t\ttable[class=body] ul,\n\t\ttable[class=body] ol,\n\t\ttable[class=body] td,\n\t\ttable[class=body] span,\n\t\ttable[class=body] a {\n\t\t\tfont-size: 16px !important; }\n\t\ttable[class=body] .wrapper,\n\t\ttable[class=body] .article {\n\t\t\tpadding: 10px !important; }\n\t\ttable[class=body] .content {\n\t\t\tpadding: 0 !important; }\n\t\ttable[class=body] .container {\n\t\t\tpadding: 0 !important;\n\t\t\twidth: 100% !important; }\n\t\ttable[class=body] .main {\n\t\t\tborder-left-width: 0 !important;\n\t\t\tborder-radius: 0 !important;\n\t\t\tborder-right-width: 0 !important; } }\n\t<?php do_action( 'llms_email_after_css' ); ?>\n\t</style>\n</head>\n<body class=\"\" style=\"background-color:<?php $mailer->get_css( 'background-color' ); ?>;color:<?php $mailer->get_css( 'font-color' ); ?>;font-family:<?php $mailer->get_css( 'font-family' ); ?>;-webkit-font-smoothing:antialiased;font-size:<?php $mailer->get_css( 'font-size' ); ?>;line-height:1.4;margin:0;padding:0;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;\">\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"body\" style=\"border-collapse:collapse;color:<?php $mailer->get_css( 'font-color' ); ?>;mso-table-lspace:0pt;mso-table-rspace:0pt;background-color:<?php $mailer->get_css( 'background-color' ); ?>;width:100%;\">\n<tr>\n\t<td style=\"font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size' ); ?>;vertical-align:top;\">&nbsp;</td>\n\t<td class=\"container\" style=\"font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size' ); ?>;vertical-align:top;display:block;max-width:<?php $mailer->get_css( 'max-width' ); ?>;padding:10px;width:<?php $mailer->get_css( 'max-width' ); ?>;Margin:0 auto !important;\">\n\n\t\t<?php if ( $header_image ) : ?>\n\t\t<div class=\"content\" style=\"box-sizing:border-box;display:block;Margin:0 auto;max-width:<?php $mailer->get_css( 'max-width' ); ?>;padding:10px;\">\n\t\t\t<img alt=\"<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>\" src=\"<?php echo esc_url( $header_image ); ?>\" style=\"display:block;height:auto;Margin:0 auto;max-width:100%;\" />\n\t\t</div>\n\t\t<?php endif; ?>\n\n\t\t<!-- START CENTERED WHITE CONTAINER -->\n\t\t<div class=\"content\" style=\"box-sizing:border-box;color:<?php $mailer->get_css( 'font-color' ); ?>;display:block;Margin:0 auto;max-width:<?php $mailer->get_css( 'max-width' ); ?>;padding:10px;\">\n\n\t\t\t<?php if ( ! empty( $email_heading ) ) : ?>\n\t\t\t<!-- START HEADING AREA -->\n\t\t\t<table class=\"main\" style=\"background:<?php $mailer->get_css( 'heading-background-color' ); ?>;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;border-radius:<?php echo esc_attr( sprintf( '%1$s %1$s 0 0', $mailer->get_css( 'border-radius' ) ) ); ?>;width:100%;\">\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"wrapper\" style=\"font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size' ); ?>;vertical-align:top;box-sizing:border-box;padding:0;\">\n\t\t\t\t\t\t<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;width:100%;\">\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td style=\"color:<?php $mailer->get_css( 'heading-font-color' ); ?>;font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:28px;vertical-align:top;\">\n\t\t\t\t\t\t\t\t\t<h1 style=\"color:<?php $mailer->get_css( 'heading-font-color' ); ?>;font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:28px;font-weight:400;Margin:0;padding:20px;\">\n\t\t\t\t\t\t\t\t\t\t<?php echo wp_kses_post( $email_heading ); ?>\n\t\t\t\t\t\t\t\t\t</h1>\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t</table>\n\t\t\t<!-- END HEADING AREA -->\n\t\t\t<?php endif; ?>\n\n\t\t\t<table class=\"main\" style=\"border-collapse:collapse;color:<?php $mailer->get_css( 'font-color' ); ?>;mso-table-lspace:0pt;mso-table-rspace:0pt;background:#fff;border-radius:<?php if ( ! empty( $email_heading ) ) { echo esc_attr( sprintf( '0 0 %1$s %1$s', $mailer->get_css( 'border-radius' ) ) ); } else { $mailer->get_css( 'border-radius' ); } ?>;width:100%;\">\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"wrapper\" style=\"color:<?php $mailer->get_css( 'font-color' ); ?>;font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size' ); ?>;vertical-align:top;box-sizing:border-box;padding:20px;\">\n\t\t\t\t\t\t<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:collapse;color:<?php $mailer->get_css( 'font-color' ); ?>;mso-table-lspace:0pt;mso-table-rspace:0pt;width:100%;\">\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td class=\"main-content\" style=\"color:<?php $mailer->get_css( 'font-color' ); ?>;font-family:<?php $mailer->get_css( 'font-family' ); ?>;font-size:<?php $mailer->get_css( 'font-size' ); ?>;vertical-align:top;\">\n"
  },
  {
    "path": "templates/emails/reset-password.php",
    "content": "<?php\n/**\n * LifterLMS Reset Password Email Body Content\n *\n * @since    1.0.0\n * @version  3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit; ?>\n\n<p><?php echo wp_kses_post( sprintf( __( 'Someone recently requested that the password be reset for %s.', 'lifterlms' ), '<strong>{user_login}</strong>' ) ); ?></p>\n\n<p><?php esc_html_e( 'To reset your password, click on the button below:', 'lifterlms' ); ?></p>\n\n<p><a href=\"<?php echo esc_url( $url ); ?>\" style=\"{button_style}\"><?php esc_html_e( 'Reset Password', 'lifterlms' ); ?></a></p>\n\n<p><?php esc_html_e( 'If this was a mistake you can ignore this email and your password will not be changed.', 'lifterlms' ); ?></p>\n\n{divider}\n\n<p><small><?php esc_html_e( 'Trouble clicking? Copy and paste this URL into your browser:', 'lifterlms' ); ?><br><a href=\"<?php echo esc_url( $url ); ?>\"><?php echo esc_url( $url ); ?></a></small></p>\n"
  },
  {
    "path": "templates/emails/template.php",
    "content": "<?php\n/**\n * LifterLMS Emails Template\n *\n * @since    1.0.0\n * @version  3.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n/**\n * lifterlms_email_header hook\n *\n * @hooked llms_email_header - 10\n */\ndo_action( 'lifterlms_email_header', $email_heading );\n\n/**\n * lifterlms_email_body hook\n *\n * @hooked llms_email_body\n */\ndo_action( 'lifterlms_email_body', $email_message );\n\n/**\n * lifterlms_email_footer hook\n *\n * @hooked llms_email_footer - 10\n */\ndo_action( 'lifterlms_email_footer' );\n"
  },
  {
    "path": "templates/global/form-login.php",
    "content": "<?php\n/**\n * LifterLMS Login Form\n *\n * @package LifterLMS/Templates\n *\n * @since 3.0.0\n * @since 5.0.0 Moved setup logic for passed arguments to the function llms_get_login_form().\n * @version 5.0.0\n *\n * @param string $message (Optional) Messages to display before login form.\n * @param string $redirect (Optional) URL to redirect to after login.\n * @param string $layout (Optional) Form layout [columns|stacked].\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php llms_print_notices(); ?>\n\n<?php\n\t/**\n\t * Fire an action prior to the output of the login form.\n\t *\n\t * @since Unknown\n\t */\n\tdo_action( 'llms_before_person_login_form' );\n?>\n\n<div class=\"llms-person-login-form-wrapper\">\n\n\t<form action=\"\" class=\"llms-login\" method=\"POST\">\n\n\t\t<h2 class=\"llms-form-heading\"><?php esc_html_e( 'Login', 'lifterlms' ); ?></h2>\n\n\t\t<div class=\"llms-form-fields\">\n\n\t\t\t<?php\n\t\t\t\t/**\n\t\t\t\t * Fire an action prior to the output of the login form fields.\n\t\t\t\t *\n\t\t\t\t * @since Unknown\n\t\t\t\t */\n\t\t\t\tdo_action( 'lifterlms_login_form_start' );\n\t\t\t?>\n\n\t\t\t<?php foreach ( LLMS_Person_Handler::get_login_fields( $layout ) as $field ) : ?>\n\t\t\t\t<?php llms_form_field( $field ); ?>\n\t\t\t<?php endforeach; ?>\n\n\t\t\t<?php wp_nonce_field( 'llms_login_user', '_llms_login_user_nonce' ); ?>\n\t\t\t<input type=\"hidden\" name=\"redirect\" value=\"<?php echo esc_url( $redirect ); ?>\" />\n\t\t\t<input type=\"hidden\" name=\"action\" value=\"llms_login_user\" />\n\n\t\t\t<?php\n\t\t\t\t/**\n\t\t\t\t * Fire an action after the output of the login form fields.\n\t\t\t\t *\n\t\t\t\t * @since Unknown\n\t\t\t\t */\n\t\t\t\tdo_action( 'lifterlms_login_form_end' );\n\t\t\t?>\n\n\t\t</div>\n\n\t</form>\n\n</div>\n\n<?php\n\t/**\n\t * Fire an action after the output of the login form.\n\t *\n\t * @since Unknown\n\t */\n\tdo_action( 'llms_after_person_login_form' );\n?>\n"
  },
  {
    "path": "templates/global/form-registration.php",
    "content": "<?php\n/**\n * Registration Form\n *\n * @package LifterLMS/Templates/Global\n *\n * @since 3.0.0\n * @since 5.0.0 Utilize fields from LLMS_Forms.\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$form_title  = llms_get_form_title( 'registration' );\n$form_fields = llms_get_form_html( 'registration' );\n\n/**\n * Filters whether or not the registration form should be displayed\n *\n * By default, the registration form is hidden from logged-in users and\n * displayed to logged out users.\n *\n * @since 4.16.0\n *\n * @param boolean $hide_form Whether or not to hide the form. If `true`, the form is hidden, otherwise it is displayed.\n */\nif ( apply_filters( 'llms_hide_registration_form', is_user_logged_in() ) ) {\n\treturn;\n}\n?>\n\n<?php llms_print_notices(); ?>\n\n<?php do_action( 'lifterlms_before_person_register_form' ); ?>\n\n<div class=\"llms-new-person-form-wrapper\">\n\n\t<?php if ( $form_title ) : ?>\n\t\t<h2 class=\"llms-form-heading\"><?php echo esc_html( $form_title ); ?></h2>\n\t<?php endif; ?>\n\n\t<form method=\"post\" class=\"llms-new-person-form register\">\n\n\t\t<?php do_action( 'lifterlms_register_form_start' ); ?>\n\n\n\t\t<div class=\"llms-form-fields\">\n\n\t\t\t<?php do_action( 'lifterlms_before_registration_fields' ); ?>\n\n\t\t\t<?php\n\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\techo $form_fields;\n\t\t\t?>\n\n\t\t\t<?php\n\t\t\t\t/**\n\t\t\t\t * Hook: llms_registration_privacy\n\t\t\t\t *\n\t\t\t\t * @hooked llms_privacy_policy_form_field - 10\n\t\t\t\t * @hooked llms_agree_to_terms_form_field - 20\n\t\t\t\t */\n\t\t\t\tdo_action( 'llms_registration_privacy' );\n\t\t\t?>\n\n\t\t\t<?php do_action( 'lifterlms_after_registration_fields' ); ?>\n\n\t\t</div>\n\n\t\t<footer class=\"llms-form-fields\">\n\n\t\t\t<?php do_action( 'lifterlms_before_registration_button' ); ?>\n\n\t\t\t<?php\n\t\t\tllms_form_field(\n\t\t\t\tarray(\n\t\t\t\t\t'columns'     => 3,\n\t\t\t\t\t'classes'     => 'llms-button-action',\n\t\t\t\t\t'id'          => 'llms_register_person',\n\t\t\t\t\t'value'       => apply_filters( 'lifterlms_registration_button_text', __( 'Register', 'lifterlms' ) ),\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\n\t\t\t<?php do_action( 'lifterlms_after_registration_button' ); ?>\n\t\t\t<?php wp_nonce_field( 'llms_register_person', '_llms_register_person_nonce' ); ?>\n\t\t\t<input name=\"action\" type=\"hidden\" value=\"llms_register_person\">\n\n\t\t</footer>\n\n\t\t<?php do_action( 'lifterlms_register_form_end' ); ?>\n\n\t</form>\n\n</div>\n\n<?php do_action( 'lifterlms_after_person_register_form' ); ?>\n"
  },
  {
    "path": "templates/global/sidebar.php",
    "content": "<?php\n/**\n * Retrieve sidebar\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @since 7.3.0 Don't include WordPress default sidebar.php template when using a block theme.\n * @version 7.3.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$core_fallback     = ABSPATH . WPINC . '/theme-compat/sidebar.php';\n$sidebar_templates = array( 'sidebar-llms_shop.php', 'sidebar.php' );\n\n// Return early if using block theme with no sidebar template.\nif ( wp_is_block_theme() && locate_template( $sidebar_templates ) === $core_fallback ) {\n\treturn;\n}\n\nget_sidebar( 'llms_shop' );\n"
  },
  {
    "path": "templates/global/wrapper-end.php",
    "content": "<?php\n/**\n * Content Wrapper: End\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$template = get_option( 'template' );\n\nswitch ( $template ) {\n\tcase 'twentyeleven':\n\t\techo '</div>';\n\t\tget_sidebar( 'shop' );\n\t\techo '</div>';\n\t\tbreak;\n\tcase 'twentytwelve':\n\t\techo '</div></div>';\n\t\tbreak;\n\tcase 'twentythirteen':\n\t\techo '</div></div>';\n\t\tbreak;\n\tcase 'twentyfourteen':\n\t\techo '</div></div></div>';\n\t\tget_sidebar( 'content' );\n\t\tbreak;\n\tcase 'twentyfifteen':\n\t\techo '</div></div>';\n\t\tbreak;\n\tcase 'twentysixteen':\n\t\techo '</main></div>';\n\t\tbreak;\n\tcase 'twentyseventeen':\n\t\techo '</div>';\n\t\tbreak;\n\tdefault:\n\t\techo '</div></div>';\n\t\tbreak;\n}\n"
  },
  {
    "path": "templates/global/wrapper-start.php",
    "content": "<?php\n/**\n * Content Wrapper: Start\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$template = get_option( 'template' );\n\nswitch ( $template ) {\n\n\tcase 'twentyeleven':\n\t\techo '<div id=\"primary\"><div id=\"content\" role=\"main\" class=\"twentyeleven\">';\n\t\tbreak;\n\n\tcase 'twentytwelve':\n\t\techo '<div id=\"primary\" class=\"site-content\"><div id=\"content\" role=\"main\" class=\"twentytwelve\">';\n\t\tbreak;\n\n\tcase 'twentythirteen':\n\t\techo '<div id=\"primary\" class=\"site-content\"><div id=\"content\" role=\"main\" class=\"entry-content twentythirteen\">';\n\t\tbreak;\n\n\tcase 'twentyfourteen':\n\t\techo '<div id=\"primary\" class=\"content-area\"><div id=\"content\" role=\"main\" class=\"site-content twentyfourteen\"><div class=\"tfwc\">';\n\t\tbreak;\n\n\tcase 'twentyfifteen':\n\t\techo '<div id=\"primary\" role=\"main\" class=\"content-area twentyfifteen\"><div id=\"main\" class=\"site-main t15wc\">';\n\t\tbreak;\n\n\tcase 'twentysixteen':\n\t\techo '<div id=\"primary\" class=\"content-area twentysixteen\"><main id=\"main\" class=\"site-main\" role=\"main\">';\n\t\tbreak;\n\n\tcase 'twentyseventeen':\n\t\techo '<div class=\"wrap\">';\n\t\tbreak;\n\n\tdefault:\n\t\techo '<div id=\"container\"><div id=\"content\" role=\"main\">';\n\t\tbreak;\n\n}\n"
  },
  {
    "path": "templates/lesson/audio.php",
    "content": "<?php\n/**\n * Lesson Audio embed\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @version 3.1.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$lesson = new LLMS_Lesson( $post );\n\nif ( ! $lesson->get( 'audio_embed' ) ) {\n\treturn; }\n?>\n\n<div class=\"llms-audio-wrapper\">\n\t<div class=\"center-audio\">\n\t\t<?php\n\t\t\t// Calls wp_oembed_get(); can't be escaped.\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\techo $lesson->get_audio();\n\t\t?>\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/lesson/video.php",
    "content": "<?php\n/**\n * Lesson Video embed\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @version 3.1.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$lesson = new LLMS_Lesson( $post );\n\nif ( ! $lesson->get( 'video_embed' ) ) {\n\treturn; }\n?>\n\n<div class=\"llms-video-wrapper\">\n\t<div class=\"center-video\">\n\t\t<?php\n\t\t\t// Calls wp_oembed_get(); can't be escaped.\n\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\techo $lesson->get_video();\n\t\t?>\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/loop/author.php",
    "content": "<?php\n/**\n * LifterLMS Loop Author Info\n *\n * @package LifterLMS/Templates\n *\n * @since   3.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// Generated HTML is escaped inside the function.\n// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\necho llms_get_author(\n\tarray(\n\t\t'avatar_size' => 28,\n\t)\n);\n\n"
  },
  {
    "path": "templates/loop/content.php",
    "content": "<?php\n/**\n * The Template for displaying all single courses.\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @version 3.14.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<li <?php post_class( 'llms-loop-item' ); ?>>\n\t<div class=\"llms-loop-item-content\">\n\n\t<?php\n\t\t/**\n\t\t * Hook: lifterlms_before_loop_item\n\t\t *\n\t\t * @hooked lifterlms_loop_featured_video - 8\n\t\t * @hooked lifterlms_loop_link_start - 10\n\t\t */\n\t\tdo_action( 'lifterlms_before_loop_item' );\n\t?>\n\n\t<?php\n\t\t/**\n\t\t * Hook: lifterlms_before_loop_item_title\n\t\t *\n\t\t * @hooked lifterlms_template_loop_thumbnail - 10\n\t\t * @hooked lifterlms_template_loop_progress - 15\n\t\t */\n\t\tdo_action( 'lifterlms_before_loop_item_title' );\n\t?>\n\n\t<h4 class=\"llms-loop-title\"><?php the_title(); ?></h4>\n\n\t<footer class=\"llms-loop-item-footer\">\n\t\t<?php\n\t\t\t/**\n\t\t\t * Hook: lifterlms_after_loop_item_title\n\t\t\t *\n\t\t\t * @hooked lifterlms_template_loop_author - 10\n\t\t\t * @hooked lifterlms_template_loop_length - 15\n\t\t\t * @hooked lifterlms_template_loop_difficulty - 20\n\t\t\t * @hooked lifterlms_template_loop_lesson_count - 22\n\t\t\t *\n\t\t\t * On Student Dashboard & \"Mine\" Courses Shortcode\n\t\t\t * @hooked lifterlms_template_loop_enroll_status - 25\n\t\t\t * @hooked lifterlms_template_loop_enroll_date - 30\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_after_loop_item_title' );\n\t\t?>\n\t</footer>\n\n\t<?php\n\t\t/**\n\t\t * Hook: lifterlms_after_loop_item\n\t\t *\n\t\t * @hooked lifterlms_loop_link_end - 5\n\t\t */\n\t\tdo_action( 'lifterlms_after_loop_item' );\n\t?>\n\n\t</div><!-- .llms-loop-item-content -->\n</li><!-- .llms-loop-item -->\n"
  },
  {
    "path": "templates/loop/enroll-date.php",
    "content": "<?php\n/**\n * LifterLMS Loop Enrollment Date\n *\n * @package LifterLMS/Templates\n *\n * @since   3.14.0\n * @version 3.14.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$student = llms_get_student();\nif ( ! $student ) {\n\treturn;\n}\n\n?>\n<div class=\"llms-meta llms-enroll-date\">\n\t<p>\n\t<?php\n\tprintf(\n\t\t// Translators: %s = Enrollment date.\n\t\tesc_html__( 'Enrolled: %s', 'lifterlms' ),\n\t\tesc_html( $student->get_enrollment_date( get_the_ID() ) )\n\t);\n\t?>\n\t</p>\n</div>\n"
  },
  {
    "path": "templates/loop/enroll-status.php",
    "content": "<?php\n/**\n * LifterLMS Loop Enrollment Status\n *\n * @package LifterLMS/Templates\n *\n * @since   3.14.0\n * @version 3.14.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$student = llms_get_student();\nif ( ! $student ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-meta llms-enroll-status\">\n\t<p>\n\t<?php\n\tprintf(\n\t\t// Translators: %s = enrollment status.\n\t\tesc_html__( 'Status: %s', 'lifterlms' ),\n\t\tesc_html( llms_get_enrollment_status_name( $student->get_enrollment_status( get_the_ID() ) ) )\n\t);\n\t?>\n\t</p>\n</div>\n"
  },
  {
    "path": "templates/loop/featured-image.php",
    "content": "<?php\n/**\n * Display a Featured Image on the Loop Tile\n *\n * @package LifterLMS/Templates\n *\n * @since  Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n// Short circuit if the featured video tile option is enabled for a course or membership.\nif ( 'course' === $post->post_type || 'llms_membership' === $post->post_type ) {\n\t$product = llms_get_post( $post );\n\tif ( 'yes' === $product->get( 'tile_featured_video' ) && $product->get( 'video_embed' ) ) {\n\t\treturn;\n\t}\n}\n\nif ( has_post_thumbnail( $post->ID ) ) {\n\t// Generated HTML is escaped inside the function.\n\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\techo llms_featured_img( $post->ID, 'full' );\n} elseif ( llms_placeholder_img_src() ) {\n\t// Generated HTML is escaped inside the function.\n\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\techo llms_placeholder_img();\n}\n"
  },
  {
    "path": "templates/loop/featured-pricing.php",
    "content": "<?php\n/**\n * LifterLMS Loop End Wrapper\n *\n * @package LifterLMS/Templates\n *\n * @since   1.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$model = llms_get_post( $post );\n\nif ( ! $model->get( 'featured_pricing' ) ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-featured-pricing\">\n\t<p>\n\t\t<?php\n\t\techo wp_kses( $model->get( 'featured_pricing' ), LLMS_ALLOWED_HTML_PRICES );\n\t\t?>\n\t</p>\n</div>\n"
  },
  {
    "path": "templates/loop/loop-end.php",
    "content": "<?php\n/**\n * LifterLMS Loop End Wrapper\n *\n * @package LifterLMS/Templates\n *\n * @since   1.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\t</ul>\n</div><!-- .llms-loop -->\n"
  },
  {
    "path": "templates/loop/loop-start.php",
    "content": "<?php\n/**\n * LifterLMS Loop Start Wrapper\n *\n * @package LifterLMS/Templates\n *\n * @since   1.0.0\n * @version 3.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"llms-loop\">\n\t<ul class=\"llms-loop-list<?php echo esc_attr( llms_get_loop_list_classes() ); ?>\">\n"
  },
  {
    "path": "templates/loop/none-found.php",
    "content": "<?php\n/**\n * Template: No items found.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<p class=\"lifterlms-info\"><?php esc_html_e( 'No products were found matching your selection.', 'lifterlms' ); ?></p>\n"
  },
  {
    "path": "templates/loop/pagination.php",
    "content": "<?php\n/**\n * LLMS Pagination Template\n *\n * @package LifterLMS/Templates/Loop\n *\n * @since 1.0.0\n * @version 4.10.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $wp_query;\nif ( $wp_query->max_num_pages < 2 ) {\n\treturn;\n}\n\n/**\n * Filter the list of CSS classes on the pagination wrapper element.\n *\n * @since 4.10.0\n *\n * @param string[] $classes Array of CSS classes.\n */\n$classes = apply_filters( 'llms_get_pagination_wrapper_classes', array( 'llms-pagination' ) );\n?>\n\n<nav class=\"<?php echo esc_attr( implode( ' ', $classes ) ); ?>\">\n<?php\n// Generated HTML is escaped inside the function.\n// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\necho paginate_links(\n\tarray(\n\t\t'base'      => str_replace( 999999, '%#%', get_pagenum_link( 999999, false ) ),\n\t\t'format'    => '?page=%#%',\n\t\t'total'     => $wp_query->max_num_pages,\n\t\t'current'   => max( 1, get_query_var( 'paged' ) ),\n\t\t'prev_next' => true,\n\t\t'prev_text' => '« ' . __( 'Previous', 'lifterlms' ),\n\t\t'next_text' => __( 'Next', 'lifterlms' ) . ' »',\n\t\t'type'      => 'list',\n\t)\n);\n?>\n</nav>\n"
  },
  {
    "path": "templates/loop-main.php",
    "content": "<?php\n/**\n * LifterLMS Loop main template\n *\n * @since 5.8.0\n * @version 5.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php do_action( 'lifterlms_before_main_content' ); ?>\n\n<?php if ( apply_filters( 'lifterlms_show_page_title', true ) ) : ?>\n\n<h1 class=\"page-title\"><?php lifterlms_page_title(); ?></h1>\n\n<?php endif; ?>\n\n<?php do_action( 'lifterlms_archive_description' ); ?>\n\n<?php\n/**\n * Hook: lifterlms_loop\n *\n * @hooked lifterlms_loop - 10\n */\ndo_action( 'lifterlms_loop' );\n?>\n\n<?php do_action( 'lifterlms_after_main_content' ); ?>\n\n<?php do_action( 'lifterlms_sidebar' ); ?>\n"
  },
  {
    "path": "templates/loop.php",
    "content": "<?php\n/**\n * Generic loop template\n *\n * Utilized by both courses and memberships.\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 5.8.0 Moved the main part in loop-main.php.\n * @version 5.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<?php get_header( 'llms_loop' ); ?>\n\n<?php llms_get_template_part( 'loop', 'main' ); ?>\n\n<?php\nget_footer();\n"
  },
  {
    "path": "templates/membership/audio.php",
    "content": "<?php\n/**\n * @author      codeBOX\n * @package     lifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$membership = new LLMS_Membership( $post );\n\nif ( ! $membership->get_audio() ) {\n\treturn; }\n?>\n\n<div class=\"llms-audio-wrapper\">\n\t<div class=\"center-audio\">\n\t\t<?php\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo $membership->get_audio();\n\t\t?>\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/membership/full-description.php",
    "content": "<?php\n/**\n * Membership description template.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 4.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n?>\n<div class=\"llms-full-description\">\n\n\t<?php\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo apply_filters( 'the_content', apply_filters( 'lifterlms_full_description', do_shortcode( $post->post_content ) ) );\n\t?>\n\n</div>\n"
  },
  {
    "path": "templates/membership/instructors.php",
    "content": "<?php\n/**\n * LifterLMS Membership instructor information\n *\n * @package LifterLMS/Templates/Membership\n *\n * @since 4.11.0\n * @version 4.11.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_template_instructors();\n"
  },
  {
    "path": "templates/membership/price.php",
    "content": "<?php\n/**\n * Membership price template\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version  3.24.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$llms_product = new LLMS_Product( $post->ID );\n?>\n\n<?php if ( ! llms_is_user_enrolled( get_current_user_id(), $post->ID ) ) : ?>\n\n\t<div class=\"llms-price-wrapper\">\n\n\t\t<?php foreach ( $llms_product->get_payment_options() as $option ) : ?>\n\n\t\t\t<?php if ( 'single' === $option || 'free' === $option ) : ?>\n\n\t\t\t\t<h4 class=\"llms-price\"><span><?php echo wp_kses( apply_filters( 'lifterlms_single_payment_text', $llms_product->get_single_price_html(), $llms_product ), LLMS_ALLOWED_HTML_PRICES ); ?></span></h4>\n\n\t\t\t<?php elseif ( 'recurring' === $option ) : ?>\n\n\t\t\t\t<?php foreach ( $llms_product->get_subscriptions() as $sub ) : ?>\n\n\t\t\t\t\t<?php if ( count( $llms_product->get_payment_options() ) > 1 ) : ?>\n\n\t\t\t\t\t\t<span class=\"llms-price-option-separator\"><?php echo esc_html( apply_filters( 'lifterlms_price_option_separator', __( 'or', 'lifterlms' ), $llms_product ) ); ?></span>\n\n\t\t\t\t\t<?php endif; ?>\n\n\t\t\t\t\t<h4 class=\"llms-price\"><span><?php echo wp_kses( $llms_product->get_subscription_price_html( $sub ), LLMS_ALLOWED_HTML_PRICES ); ?></span></h4>\n\n\t\t\t\t<?php endforeach; ?>\n\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php\n\t\t\t/**\n\t\t\t * Allow addons / plugins / themes to define custom payment options\n\t\t\t * This action will be called to allow them to output some custom html for the payment options\n\t\t\t */\n\t\t\t?>\n\t\t\t<?php do_action( 'lifterlms_product_payment_option_' . $option, $llms_product ); ?>\n\n\t\t<?php endforeach; ?>\n\n\t</div>\n\n<?php endif; ?>\n"
  },
  {
    "path": "templates/membership/title.php",
    "content": "<?php\n/**\n * Membership Title\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version Unknown\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n\n<h1 class=\"entry-title hentry-title llms-h1 llms-title\"><?php the_title(); ?></h1>\n"
  },
  {
    "path": "templates/membership/video.php",
    "content": "<?php\n/**\n * @author      LifterLMS\n * @package     LifterLMS/Templates\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$membership = new LLMS_Membership( $post );\n\nif ( ! $membership->get_video() ) {\n\treturn; }\n\n?>\n\n<div class=\"llms-video-wrapper\">\n\t<div class=\"center-video\">\n\t\t<?php\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo $membership->get_video();\n\t\t?>\n\t</div>\n</div>\n"
  },
  {
    "path": "templates/myaccount/dashboard-section.php",
    "content": "<?php\n/**\n * Section template for dashboard index\n *\n * @since 3.14.0\n * @since 3.30.1 Added dynamic filter on the `$more` var to allow customization of the URL and text on the \"More\" button.\n * @version  3.30.1\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$more = apply_filters( 'llms_' . $action . '_more', $more );\n?>\n\n<section class=\"llms-sd-section <?php echo esc_attr( $slug ); ?>\">\n\n\t<?php if ( $title ) : ?>\n\t\t<h3 class=\"llms-sd-section-title\">\n\t\t\t<?php echo wp_kses_post( apply_filters( 'lifterlms_' . $action . '_title', $title ) ); ?>\n\t\t</h3>\n\t<?php endif; ?>\n\n\t<?php do_action( 'lifterlms_before_' . $action ); ?>\n\n\t<?php echo wp_kses( $content, LLMS_ALLOWED_HTML_FORM_FIELDS ); ?>\n\n\t<?php if ( $more ) : ?>\n\t\t<footer class=\"llms-sd-section-footer\">\n\t\t\t<a class=\"llms-button-secondary\" href=\"<?php echo esc_url( $more['url'] ); ?>\"><?php echo esc_html( $more['text'] ); ?></a>\n\t\t</footer>\n\t<?php endif; ?>\n\n\t<?php do_action( 'lifterlms_after_' . $action ); ?>\n\n</section>\n"
  },
  {
    "path": "templates/myaccount/dashboard.php",
    "content": "<?php\n/**\n * My Account page.\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 7.5.0 Hooked my_favorites function.\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_print_notices();\n?>\n\n<div class=\"llms-sd-tab dashboard\">\n\n\t<?php\n\n\t\tdo_action( 'lifterlms_before_student_dashboard_tab' );\n\n\t\t/**\n\t\t * lifterlms_student_dashboard_index\n\t\t *\n\t\t * @hooked lifterlms_template_student_dashboard_my_courses - 10\n\t\t * @hooked lifterlms_template_student_dashboard_my_achievements - 20\n\t\t * @hooked lifterlms_template_student_dashboard_my_certificates - 30\n\t\t * @hooked lifterlms_template_student_dashboard_my_memberships - 40\n\t\t * @hooked llms_template_student_dashboard_my_favorites - 50\n\t\t */\n\t\tdo_action( 'lifterlms_student_dashboard_index', true );\n\n\t\tdo_action( 'lifterlms_after_student_dashboard_tab' );\n\n\t?>\n\n</div>\n"
  },
  {
    "path": "templates/myaccount/form-edit-account.php",
    "content": "<?php\n/**\n * Account Edit Template / Form\n *\n * @since 1.0.0\n * @since 5.0.0 Utilize fields from LLMS_Forms.\n * @version 5.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$form_title  = llms_get_form_title( 'account' );\n$form_fields = llms_get_form_html( 'account' );\n?>\n\n<?php llms_print_notices(); ?>\n\n<?php do_action( 'lifterlms_my_account_navigation' ); ?>\n\n<?php do_action( 'lifterlms_before_person_edit_account_form' ); ?>\n\n<div class=\"llms-person-form-wrapper\">\n\n\t<?php if ( $form_title ) : ?>\n\t\t<h4 class=\"llms-form-heading\"><?php echo wp_kses_post( $form_title ); ?></h4>\n\t<?php endif; ?>\n\n\t<form method=\"post\" class=\"llms-person-form edit-account\">\n\n\t\t<?php do_action( 'lifterlms_edit_account_start' ); ?>\n\n\t\t<div class=\"llms-form-fields\">\n\n\t\t\t<?php do_action( 'lifterlms_before_update_fields' ); ?>\n\n\t\t\t<?php echo wp_kses( $form_fields, LLMS_ALLOWED_HTML_FORM_FIELDS ); ?>\n\n\t\t\t<?php do_action( 'lifterlms_after_update_fields' ); ?>\n\n\t\t</div>\n\n\t\t<footer class=\"llms-form-fields\">\n\n\t\t\t<?php do_action( 'lifterlms_before_update_button' ); ?>\n\n\t\t\t<?php\n\t\t\tllms_form_field(\n\t\t\t\tarray(\n\t\t\t\t\t'columns'     => 6,\n\t\t\t\t\t'classes'     => 'llms-button-action',\n\t\t\t\t\t'id'          => 'llms_update_person',\n\t\t\t\t\t'value'       => apply_filters( 'lifterlms_update_button_text', __( 'Save', 'lifterlms' ) ),\n\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t'required'    => false,\n\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\n\t\t\t<?php do_action( 'lifterlms_after_update_button' ); ?>\n\n\t\t\t<?php wp_nonce_field( 'llms_update_person', '_llms_update_person_nonce' ); ?>\n\n\t\t\t<input name=\"action\" type=\"hidden\" value=\"llms_update_person\">\n\n\t\t</footer>\n\n\t\t<?php do_action( 'lifterlms_edit_account_form_end' ); ?>\n\n\t</form>\n\n</div>\n\n<?php\ndo_action( 'lifterlms_after_person_edit_account_form' );\n"
  },
  {
    "path": "templates/myaccount/form-lost-password.php",
    "content": "<?php\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php llms_print_notices(); ?>\n\n<form action=\"\" class=\"llms-lost-password-form\" method=\"POST\">\n\n\t<?php foreach ( $fields as $field ) : ?>\n\t\t<?php llms_form_field( $field ); ?>\n\t<?php endforeach; ?>\n\n\t<?php wp_nonce_field( 'llms_' . $form, '_' . $form . '_nonce' ); ?>\n\n</form>\n"
  },
  {
    "path": "templates/myaccount/form-redeem-voucher.php",
    "content": "<?php\n/**\n * Redeem vouchers\n *\n * @package LifterLMS/Templates\n *\n * @since 2.0.0\n * @since 4.12.0 Updated the label `for` attribute and added an `id` to the input element.\n * @version 4.12.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<?php llms_print_notices(); ?>\n\n<?php do_action( 'lifterlms_my_account_navigation' ); ?>\n\n<?php do_action( 'lifterlms_before_redeem_voucher' ); ?>\n\n<form action=\"\" method=\"POST\" class=\"llms-voucher-form\">\n\n\t<div class=\"llms-form-fields\">\n\t\t<div class=\"form-row form-row-first llms-form-field type-text llms-cols-12 llms-is-required\">\n\t\t\t<label for=\"llms-voucher-code\"><?php esc_html_e( 'Voucher Code', 'lifterlms' ); ?></label>\n\t\t\t<input id=\"llms-voucher-code\" type=\"text\" placeholder=\"<?php esc_attr_e( 'Voucher Code', 'lifterlms' ); ?>\" name=\"llms_voucher_code\" class=\"llms-field-input\" required=\"required\">\n\t\t</div>\n\t</div>\n\n\t<footer class=\"llms-form-fields\">\n\t\t<div class=\"llms-form-field type-submit llms-cols-6 llms-cols-last\"\">\n\t\t\t<button id=\"llms-redeem-voucher-submit\" type=\"submit\" class=\"llms-field-button llms-button-action\"><?php echo esc_html_x( 'Submit', 'Voucher Code', 'lifterlms' ); ?></button>\n\t\t</div>\n\t\t<?php wp_nonce_field( 'lifterlms_voucher_check', 'lifterlms_voucher_nonce' ); ?>\n\t</footer>\n\n</form>\n\n<?php do_action( 'lifterlms_after_redeem_voucher' ); ?>\n"
  },
  {
    "path": "templates/myaccount/header.php",
    "content": "<?php\n/**\n * Student Dashboard Header\n *\n * @package LifterLMS/Templates\n *\n * @since    3.14.0\n * @since    7.8.0\n * @version  3.14.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<header class=\"llms-sd-header\">\n\n\t<?php\n\t/**\n\t * @hooked lifterlms_template_student_dashboard_title - 10\n\t */\n\tdo_action( 'lifterlms_student_dashboard_header' );\n\t?>\n\n</header>\n"
  },
  {
    "path": "templates/myaccount/my-favorites.php",
    "content": "<?php\n/**\n * My Favorites\n *\n * @package LifterLMS/Templates/MyAccount\n *\n * @since 7.5.0\n * @version 7.5.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-sd-section llms-my-favorites\">\n\t<?php\n\t// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in templates.\n\techo $content;\n\t?>\n</div>\n\n"
  },
  {
    "path": "templates/myaccount/my-grades-single-table.php",
    "content": "<?php\n/**\n * My Grades Single Course Table Template\n *\n * @since 3.24.0\n * @since 6.0.0 Wrap each section in a <tbody> element.\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<table class=\"llms-table llms-single-course-grades\">\n<?php foreach ( $course->get_sections() as $section ) : ?>\n\t<tbody>\n\t\t<tr class=\"llms-section\">\n\t\t\t<th class=\"llms-section_title\" colspan=\"2\">\n\t\t\t\t<?php echo esc_html( sprintf( __( 'Section %1$d: %2$s', 'lifterlms' ), $section->get( 'order' ), $section->get( 'title' ) ) ); ?>\n\t\t\t</th>\n\t\t\t<?php foreach ( $section_headings as $id => $content ) : ?>\n\t\t\t\t<th class=\"llms-<?php echo esc_attr( $id ); ?>\">\n\t\t\t\t\t<?php echo wp_kses_post( $content ); ?>\n\t\t\t\t</th>\n\t\t\t<?php endforeach; ?>\n\t\t</tr>\n\n\t\t<?php\n\t\tforeach ( $section->get_lessons() as $lesson ) :\n\t\t\t$restricted = llms_page_restricted( $lesson->get( 'id' ) );\n\t\t\t$title      = $lesson->get( 'title' );\n\t\t\t$url        = $restricted['is_restricted'] ? '#' : get_permalink( $lesson->get( 'id' ) );\n\t\t\t$title      = sprintf( '<a href=\"%1$s\">%2$s</a>', $url, $title );\n\t\t\t?>\n\t\t\t<tr>\n\t\t\t\t<td class=\"llms-lesson_title\" colspan=\"2\">\n\t\t\t\t\t<?php echo wp_kses_post( sprintf( __( 'Lesson %1$d: %2$s', 'lifterlms' ), $lesson->get( 'order' ), $title ) ); ?>\n\t\t\t\t\t<?php if ( $restricted['is_restricted'] ) : ?>\n\t\t\t\t\t\t<a data-tooltip-msg=\"<?php echo esc_attr( wp_strip_all_tags( llms_get_restriction_message( $restricted ) ) ); ?>\" href=\"#llms-lesson-locked\">\n\t\t\t\t\t\t\t<i class=\"fa fa-lock\" aria-hidden=\"true\"></i>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</td>\n\n\t\t\t\t<?php foreach ( $section_headings as $id => $data ) : ?>\n\t\t\t\t\t<td class=\"llms-<?php echo esc_attr( $id ); ?>\">\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in template.\n\t\t\t\t\t\techo llms_sd_my_grades_table_content( $id, $lesson, $student, $restricted );\n\t\t\t\t\t\t?>\n\t\t\t\t\t</td>\n\t\t\t\t<?php endforeach; ?>\n\n\t\t\t</tr>\n\t\t<?php endforeach; ?>\n\t</tbody>\n<?php endforeach; ?>\n</table>\n"
  },
  {
    "path": "templates/myaccount/my-grades-single.php",
    "content": "<?php\n/**\n * My Grades Single Course Template\n *\n * @since    3.24.0\n * @version  3.24.0\n */\ndefined( 'ABSPATH' ) || exit;\nllms_print_notices();\n?>\n\n<?php if ( $course ) : ?>\n\n\t<div class=\"llms-sd-section llms-sd-grades\">\n\n\t\t<?php do_action( 'llms_before_my_grades_content', $course, $student ); ?>\n\n\t\t<section class=\"llms-sd-widgets\">\n\n\t\t\t<?php\n\t\t\tdo_action( 'llms_before_my_grades_widgets', $course, $student );\n\n\t\t\tllms_sd_dashboard_donut_widget(\n\t\t\t\t__( 'Progress', 'lifterlms' ),\n\t\t\t\t$student->get_progress( $course->get( 'id' ) ),\n\t\t\t\t__( 'Complete', 'lifterlms' )\n\t\t\t);\n\t\t\tllms_sd_dashboard_donut_widget(\n\t\t\t\t__( 'Grade', 'lifterlms' ),\n\t\t\t\t$student->get_grade( $course->get( 'id' ) ),\n\t\t\t\t__( 'Overall Grade', 'lifterlms' )\n\t\t\t);\n\t\t\tllms_sd_dashboard_date_widget(\n\t\t\t\t__( 'Enrollment Date', 'lifterlms' ),\n\t\t\t\t$student->get_enrollment_date( $course->get( 'id' ), 'enrolled', 'U' )\n\t\t\t);\n\t\t\tllms_sd_dashboard_widget(\n\t\t\t\t__( 'Latest Achievement', 'lifterlms' ),\n\t\t\t\t$latest_achievement ? llms_get_achievement( $latest_achievement ) : '',\n\t\t\t\t__( 'No achievements', 'lifterlms' )\n\t\t\t);\n\t\t\tllms_sd_dashboard_date_widget(\n\t\t\t\t__( 'Last Activity', 'lifterlms' ),\n\t\t\t\t$last_activity,\n\t\t\t\t__( 'No activity', 'lifterlms' )\n\t\t\t);\n\n\t\t\tdo_action( 'llms_after_my_grades_widgets', $course, $student );\n\t\t\t?>\n\n\t\t</section>\n\n\t\t<div class=\"llms-sd-section\">\n\t\t\t<?php\n\t\t\t/**\n\t\t\t * Hook: llms_my_grades_course_table.\n\t\t\t *\n\t\t\t * @hooked lifterlms_template_student_dashboard_my_grades_table - 10\n\t\t\t */\n\t\t\tdo_action( 'llms_my_grades_course_table', $course, $student );\n\t\t\t?>\n\t\t</div>\n\t</div>\n\t<?php else : ?>\n\n\t\t<p><?php esc_html_e( 'Invalid course.', 'lifterlms' ); ?></p>\n\n\t<?php endif; ?>\n"
  },
  {
    "path": "templates/myaccount/my-grades.php",
    "content": "<?php\n/**\n * My Grades Template\n *\n * @since    3.24.0\n * @version  3.24.0\n */\ndefined( 'ABSPATH' ) || exit;\nllms_print_notices();\n?>\n\n<div class=\"llms-sd-section llms-sd-grades\">\n\n\t<?php do_action( 'llms_student_dashboard_before_my_grades' ); ?>\n\n\t<table class=\"llms-table\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Course', 'lifterlms' ); ?></a></th>\n\t\t\t\t<th><?php esc_html_e( 'Enrollment Date', 'lifterlms' ); ?></a></th>\n\t\t\t\t<th><?php esc_html_e( 'Progress', 'lifterlms' ); ?></th>\n\t\t\t\t<th><?php esc_html_e( 'Grade', 'lifterlms' ); ?></th>\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t<?php foreach ( $courses as $course ) : ?>\n\t\t\t<tr>\n\t\t\t\t<td><a href=\"<?php echo esc_url( llms_get_endpoint_url( 'my-grades', $course->get( 'name' ) ) ); ?>\"><?php echo esc_html( $course->get( 'title' ) ); ?></a></td>\n\t\t\t\t<td><?php echo esc_html( $student->get_enrollment_date( $course->get( 'id' ) ) ); ?></td>\n\t\t\t\t<td><?php echo wp_kses_post( llms_get_progress_bar_html( $student->get_progress( $course->get( 'id' ) ) ) ); ?></td>\n\t\t\t\t<td>\n\t\t\t\t<?php\n\t\t\t\t\t$grade = $student->get_grade( $course->get( 'id' ) );\n\t\t\t\t\techo is_numeric( $grade ) ? wp_kses_post( llms_get_donut( $grade, '', 'mini' ) ) : '&ndash;';\n\t\t\t\t?>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t<?php endforeach; ?>\n\t\t</tbody>\n\n\t\t<tfoot>\n\t\t\t<tr>\n\t\t\t\t<td class=\"llms-table-navigation\" colspan=\"2\">\n\t\t\t\t\t<?php if ( 1 !== $pagination['current'] || $pagination['max'] !== $pagination['current'] ) : ?>\n\t\t\t\t\t<nav class=\"llms-pagination\">\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\t\t\techo paginate_links(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'base'      => str_replace( 999999, '%#%', esc_url( get_pagenum_link( 999999 ) ) ),\n\t\t\t\t\t\t\t\t'format'    => '?page=%#%',\n\t\t\t\t\t\t\t\t'total'     => $pagination['max'],\n\t\t\t\t\t\t\t\t'current'   => $pagination['current'],\n\t\t\t\t\t\t\t\t'prev_next' => true,\n\t\t\t\t\t\t\t\t'prev_text' => '« ' . __( 'Previous', 'lifterlms' ),\n\t\t\t\t\t\t\t\t'next_text' => __( 'Next', 'lifterlms' ) . ' »',\n\t\t\t\t\t\t\t\t'type'      => 'list',\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t\t?>\n\t\t\t\t\t</nav>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"llms-table-sort\" colspan=\"2\">\n\t\t\t\t\t<form action=\"<?php echo esc_url( llms_get_endpoint_url( 'my-grades' ) ); ?>\" method=\"GET\">\n\t\t\t\t\t\t<label for=\"llms-sd-table-sort\"><?php esc_html_e( 'Sort: ', 'lifterlms' ); ?></label>\n\t\t\t\t\t\t<select name=\"sort\" id=\"llms-sd-table-sort\">\n\t\t\t\t\t\t\t<option value=\"date_desc\" <?php selected( 'date_desc', $sort ); ?>><?php esc_attr_e( 'Enrollment Date (Most Recent)', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<option value=\"date_asc\" <?php selected( 'date_asc', $sort ); ?>><?php esc_attr_e( 'Enrollment Date (Oldest)', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<option value=\"title_asc\" <?php selected( 'title_asc', $sort ); ?>><?php esc_attr_e( 'Course Title (A-Z)', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t\t<option value=\"title_desc\" <?php selected( 'title_desc', $sort ); ?>><?php esc_attr_e( 'Course Title (Z-A)', 'lifterlms' ); ?></option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<button class=\"llms-button-secondary small\" type=\"submit\"><?php esc_html_e( 'Update', 'lifterlms' ); ?></button>\n\t\t\t\t\t</form>\n\t\t\t\t</td>\n\t\t</tfoot>\n\n\t</table>\n\n\t<?php do_action( 'llms_student_dashboard_after_my_grades' ); ?>\n\n</div>\n"
  },
  {
    "path": "templates/myaccount/my-notifications.php",
    "content": "<?php\n/**\n * Student Dashboard: Notifications Tab\n *\n * @since 3.8.0\n * @version 3.30.3\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-sd-notification-center\">\n\n\t<?php if ( isset( $notifications ) ) : ?>\n\n\t\t<?php if ( ! $notifications ) : ?>\n\t\t\t<p><?php esc_html_e( 'You have no notifications.', 'lifterlms' ); ?></p>\n\t\t<?php else : ?>\n\t\t\t<ol class=\"llms-notification-list\">\n\t\t\t<?php foreach ( $notifications as $noti ) : ?>\n\t\t\t\t<li class=\"llms-notification-list-item\">\n\t\t\t\t\t<?php echo wp_kses_post( $noti->get_html() ); ?>\n\t\t\t\t</li>\n\t\t\t<?php endforeach; ?>\n\t\t\t</ol>\n\t\t<?php endif; ?>\n\n\t\t<footer class=\"llms-sd-pagination llms-my-notifications-pagination\">\n\t\t\t<nav class=\"llms-pagination\">\n\t\t\t<?php\n\t\t\t$pagination = paginate_links(\n\t\t\t\tarray(\n\t\t\t\t\t'base'      => str_replace( 999999, '%#%', esc_url( get_pagenum_link( 999999 ) ) ),\n\t\t\t\t\t'format'    => '?page=%#%',\n\t\t\t\t\t'total'     => $pagination['max'],\n\t\t\t\t\t'current'   => $pagination['current'],\n\t\t\t\t\t'prev_next' => true,\n\t\t\t\t\t'prev_text' => '« ' . __( 'Previous', 'lifterlms' ),\n\t\t\t\t\t'next_text' => __( 'Next', 'lifterlms' ) . ' »',\n\t\t\t\t\t'type'      => 'list',\n\t\t\t\t)\n\t\t\t);\n\t\t\tif ( ! empty( $pagination ) ) {\n\t\t\t\techo wp_kses_post( $pagination );\n\t\t\t}\n\t\t\t?>\n\t\t\t</nav>\n\t\t</footer>\n\n\t<?php elseif ( isset( $settings ) ) : ?>\n\n\t\t<?php foreach ( $settings as $type => $triggers ) : ?>\n\n\t\t\t<h4><?php echo esc_html( apply_filters( 'llms_notification_' . $type . '_title', $type ) ); ?></h4>\n\t\t\t<p><?php echo esc_html( apply_filters( 'llms_notification_' . $type . '_desc', '' ) ); ?></p>\n\t\t\t<?php foreach ( $triggers as $id => $data ) : ?>\n\t\t\t\t<?php\n\t\t\t\tllms_form_field(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'description' => '',\n\t\t\t\t\t\t'id'          => $id,\n\t\t\t\t\t\t'label'       => $data['name'],\n\t\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t\t'name'        => 'llms_notification_pref[' . $type . '][' . $id . ']',\n\t\t\t\t\t\t'selected'    => ( 'yes' === $data['value'] ),\n\t\t\t\t\t\t'type'        => 'checkbox',\n\t\t\t\t\t\t'value'       => 'yes',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\t\t\t<?php endforeach; ?>\n\n\t\t<?php endforeach; ?>\n\n\t<?php endif; ?>\n\n</div>\n"
  },
  {
    "path": "templates/myaccount/my-orders.php",
    "content": "<?php\n/**\n * Order History List\n *\n * @package LifterLMS/Templates\n *\n * @since    3.0.0\n * @version  7.6.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"llms-sd-section llms-my-orders\">\n\n\t<?php if ( ! $orders || ! $orders['orders'] ) : ?>\n\t\t<p><?php esc_html_e( 'No orders found.', 'lifterlms' ); ?></p>\n\t<?php else : ?>\n\n\t\t<table class=\"orders-table\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><?php esc_html_e( 'Order', 'lifterlms' ); ?></td>\n\t\t\t\t\t<td><?php esc_html_e( 'Date', 'lifterlms' ); ?></td>\n\t\t\t\t\t<td><?php esc_html_e( 'Expires', 'lifterlms' ); ?></td>\n\t\t\t\t\t<td><?php esc_html_e( 'Next Payment', 'lifterlms' ); ?></td>\n\t\t\t\t\t<td></td>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t<?php foreach ( $orders['orders'] as $order ) : ?>\n\t\t\t\t<tr class=\"llms-order-item <?php echo esc_attr( $order->get( 'status' ) ); ?>\" id=\"llms-order-<?php echo esc_attr( $order->get( 'id' ) ); ?>\">\n\t\t\t\t\t<td data-label=\"<?php esc_attr_e( 'Order', 'lifterlms' ); ?>: \">\n\t\t\t\t\t\t<a href=\"<?php echo esc_url( $order->get_view_link() ); ?>\">#<?php echo esc_html( $order->get( 'id' ) ); ?></a>\n\t\t\t\t\t\t<span class=\"llms-status <?php echo esc_attr( $order->get( 'status' ) ); ?>\"><?php echo esc_html( $order->get_status_name() ); ?></span>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td data-label=\"<?php esc_attr_e( 'Date', 'lifterlms' ); ?>: \"><?php echo esc_html( $order->get_date( 'date', 'F j, Y' ) ); ?></td>\n\t\t\t\t\t<td data-label=\"<?php esc_attr_e( 'Expires', 'lifterlms' ); ?>: \">\n\t\t\t\t\t\t<?php if ( $order->is_recurring() && 'lifetime' === $order->get( 'access_expiration' ) ) : ?>\n\t\t\t\t\t\t\t&ndash;\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t<?php echo esc_html( $order->get_access_expiration_date( 'F j, Y' ) ); ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td data-label=\"<?php esc_attr_e( 'Next Payment', 'lifterlms' ); ?>: \">\n\t\t\t\t\t\t<?php if ( $order->has_scheduled_payment() ) : ?>\n\t\t\t\t\t\t\t<?php echo esc_html( $order->get_next_payment_due_date( 'F j, Y' ) ); ?>\n\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t&ndash;\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<a class=\"llms-button-primary small\" href=\"<?php echo esc_url( $order->get_view_link() ); ?>\"><?php esc_html_e( 'View', 'lifterlms' ); ?></a>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t<?php endforeach; ?>\n\t\t\t</tbody>\n\t\t</table>\n\n\t\t<?php if ( $orders['orders'] ) : ?>\n\t\t\t<footer class=\"llms-sd-pagination llms-my-orders-pagination\">\n\t\t\t\t<?php if ( $orders['page'] > 1 ) : ?>\n\t\t\t\t\t<a href=\"\n\t\t\t\t\t<?php\n\t\t\t\t\techo esc_url(\n\t\t\t\t\t\tadd_query_arg(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'opage' => $orders['page'] - 1,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\t\t\t\t\t\"><?php esc_html_e( 'Back', 'lifterlms' ); ?></a>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( $orders['page'] < $orders['pages'] ) : ?>\n\t\t\t\t\t<a href=\"\n\t\t\t\t\t<?php\n\t\t\t\t\techo esc_url(\n\t\t\t\t\t\tadd_query_arg(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'opage' => $orders['page'] + 1,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\t?>\n\t\t\t\t\t\"><?php esc_html_e( 'Next', 'lifterlms' ); ?></a>\n\t\t\t\t<?php endif; ?>\n\t\t\t</footer>\n\t\t<?php endif; ?>\n\n\t<?php endif; ?>\n</div>\n"
  },
  {
    "path": "templates/myaccount/navigation.php",
    "content": "<?php\n/**\n * My Account Navigation Links\n *\n * @since    2.?.?\n * @version  3.17.5\n */\n\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n$sep     = apply_filters( 'lifterlms_my_account_navigation_link_separator', '&bull;' );\n$current = LLMS_Student_Dashboard::get_current_tab( 'slug' );\n?>\n<nav class=\"llms-sd-nav\">\n\n\t<?php do_action( 'lifterlms_before_my_account_navigation' ); ?>\n\n\t<ul class=\"llms-sd-items\">\n\t\t<?php foreach ( LLMS_Student_Dashboard::get_tabs_for_nav() as $var => $data ) : ?>\n\t\t\t<li class=\"llms-sd-item <?php echo esc_attr( sprintf( '%1$s %2$s', $var, ( $var === $current ) ? ' current' : '' ) ); ?>\">\n\t\t\t\t<a class=\"llms-sd-link\" href=\"<?php echo esc_url( $data['url'] ); ?>\"><?php echo esc_html( $data['title'] ); ?></a>\n\t\t\t\t<span class=\"llms-sep\"><?php echo wp_kses_post( $sep ); ?></span>\n\t\t\t</li>\n\t\t<?php endforeach; ?>\n\t</ul>\n\n\n\t<?php lifterlms_template_student_dashboard_select_mobile_navigation( $current ); ?>\n\n\t<?php do_action( 'lifterlms_after_my_account_navigation' ); ?>\n\n</nav>\n"
  },
  {
    "path": "templates/myaccount/view-order-actions.php",
    "content": "<?php\n/**\n * Order information template part.\n *\n * @package LifterLMS/Templates\n *\n * @since 6.0.0\n * @since 7.0.0 Use {@see LLMS_Order::can_switch_source()} to determine if the order's source can be switched.\n * @version 7.0.0\n *\n * @var LLMS_Order $order The order object.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<aside class=\"order-secondary\">\n\n\t<?php\n\t\t/**\n\t\t * Action executed after opening the secondary order element.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param LLMS_Order $order The current order object.\n\t\t */\n\t\tdo_action( 'llms_view_order_before_secondary', $order );\n\t?>\n\n\t<?php if ( $order->is_recurring() ) : ?>\n\n\t\t<?php if ( isset( $_GET['confirm-switch'] ) || $order->can_switch_source() ) : ?>\n\n\t\t\t<?php\n\t\t\tllms_get_template(\n\t\t\t\t'checkout/form-switch-source.php',\n\t\t\t\tarray(\n\t\t\t\t\t'confirm' => llms_filter_input_sanitize_string( INPUT_GET, 'confirm-switch' ),\n\t\t\t\t\t'order'   => $order,\n\t\t\t\t)\n\t\t\t);\n\t\t\t?>\n\n\t\t<?php endif; ?>\n\n\t\t<?php if ( apply_filters( 'llms_allow_subscription_cancellation', true, $order ) && in_array( $order->get( 'status' ), array( 'llms-active', 'llms-on-hold' ), true ) ) : ?>\n\n\t\t\t<form action=\"\" id=\"llms-cancel-subscription-form\" method=\"POST\">\n\n\t\t\t\t<?php\n\t\t\t\tllms_form_field(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'columns'     => 12,\n\t\t\t\t\t\t'classes'     => 'llms-button-secondary',\n\t\t\t\t\t\t'id'          => 'llms_cancel_subscription',\n\t\t\t\t\t\t'value'       => __( 'Cancel Subscription', 'lifterlms' ),\n\t\t\t\t\t\t'last_column' => true,\n\t\t\t\t\t\t'required'    => false,\n\t\t\t\t\t\t'type'        => 'submit',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\n\t\t\t\t<?php wp_nonce_field( 'llms_cancel_subscription', '_cancel_sub_nonce' ); ?>\n\t\t\t\t<input name=\"order_id\" type=\"hidden\" value=\"<?php echo esc_attr( $order->get( 'id' ) ); ?>\">\n\n\t\t\t</form>\n\n\t\t<?php endif; ?>\n\n\t<?php endif; ?>\n\n\t<?php\n\t\t/**\n\t\t * Action executed before closing the secondary order element.\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param LLMS_Order $order The current order object.\n\t\t */\n\t\tdo_action( 'llms_view_order_after_secondary', $order );\n\t?>\n\n</aside>\n"
  },
  {
    "path": "templates/myaccount/view-order-information.php",
    "content": "<?php\n/**\n * Order information template part.\n *\n * @package LifterLMS/Templates\n *\n * @since 6.0.0\n * @version 6.0.0\n *\n * @var LLMS_Order                    $order   Current order object.\n * @var LLMS_Payment_Gateway|WP_Error $gateway Instance of the LLMS_Payment_Gateway extending class used for the payment.\n *                                             WP_Error if the gateway cannot be located, e.g. because it's no longer enabled.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<section class=\"order-primary\">\n\n\t<table class=\"orders-table\">\n\t\t<tbody>\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Status', 'lifterlms' ); ?></th>\n\t\t\t\t<td><?php echo esc_html( $order->get_status_name() ); ?></td>\n\t\t\t</tr>\n\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Access Plan', 'lifterlms' ); ?></th>\n\t\t\t\t<td><?php echo esc_html( $order->get( 'plan_title' ) ); ?></td>\n\t\t\t</tr>\n\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Product', 'lifterlms' ); ?></th>\n\t\t\t\t<td>\n\t\t\t\t<?php if ( llms_get_post( $order->get( 'product_id' ) ) ) : ?>\n\t\t\t\t\t<a href=\"<?php echo esc_url( get_permalink( $order->get( 'product_id' ) ) ); ?>\"><?php echo wp_kses_post( $order->get( 'product_title' ) ); ?></a>\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<?php echo esc_html__( '[DELETED]', 'lifterlms' ) . ' ' . wp_kses_post( $order->get( 'product_title' ) ); ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t\t<?php if ( $order->has_trial() ) : ?>\n\t\t\t\t<?php if ( $order->has_coupon() && $order->get( 'coupon_amount_trial' ) ) : ?>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th><?php esc_html_e( 'Original Total', 'lifterlms' ); ?></th>\n\t\t\t\t\t\t<td><?php echo wp_kses( $order->get_price( 'trial_original_total' ), LLMS_ALLOWED_HTML_PRICES ); ?></td>\n\t\t\t\t\t</tr>\n\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th><?php esc_html_e( 'Coupon Discount', 'lifterlms' ); ?></th>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<?php echo wp_kses( $order->get_coupon_amount( 'trial' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t\t\t(<?php echo wp_kses( llms_price( $order->get_price( 'coupon_value_trial', array(), 'float' ) * - 1 ), LLMS_ALLOWED_HTML_PRICES ); ?>)\n\t\t\t\t\t\t\t[<code><?php echo esc_html( $order->get( 'coupon_code' ) ); ?></code>]\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<tr>\n\t\t\t\t\t<th><?php esc_html_e( 'Trial Total', 'lifterlms' ); ?></th>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<?php echo wp_kses( $order->get_price( 'trial_total' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t\t<?php echo esc_html( sprintf( _n( 'for %1$d %2$s', 'for %1$d %2$ss', $order->get( 'trial_length' ), 'lifterlms' ), $order->get( 'trial_length' ), $order->get( 'trial_period' ) ) ); ?>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php if ( $order->has_discount() ) : ?>\n\t\t\t\t<tr>\n\t\t\t\t\t<th><?php esc_html_e( 'Original Total', 'lifterlms' ); ?></th>\n\t\t\t\t\t<td><?php echo wp_kses( $order->get_price( 'original_total' ), LLMS_ALLOWED_HTML_PRICES ); ?></td>\n\t\t\t\t</tr>\n\n\t\t\t\t<?php if ( $order->has_sale() ) : ?>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th><?php esc_html_e( 'Sale Discount', 'lifterlms' ); ?></th>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<?php echo wp_kses( $order->get_price( 'sale_price' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t\t\t(<?php echo wp_kses( llms_price( $order->get_price( 'sale_value', array(), 'float' ) * -1 ), LLMS_ALLOWED_HTML_PRICES ); ?>)\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( $order->has_coupon() ) : ?>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th><?php esc_html_e( 'Coupon Discount', 'lifterlms' ); ?></th>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<?php echo wp_kses( $order->get_coupon_amount( 'regular' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t\t\t(<?php echo wp_kses( llms_price( $order->get_price( 'coupon_value', array(), 'float' ) * - 1 ), LLMS_ALLOWED_HTML_PRICES ); ?>)\n\t\t\t\t\t\t\t[<code><?php echo esc_html( $order->get( 'coupon_code' ) ); ?></code>]\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t<?php endif; ?>\n\t\t\t<?php endif; ?>\n\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Total', 'lifterlms' ); ?></th>\n\t\t\t\t<td>\n\t\t\t\t\t<?php echo wp_kses( $order->get_price( 'total' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t\t<?php if ( $order->is_recurring() ) : ?>\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\techo esc_html(\n\t\t\t\t\t\t\tsprintf(\n\t\t\t\t\t\t\t// Translators: %1$d = the billing frequency; %2$s = the billing period.\n\t\t\t\t\t\t\t\t_n( // phpcs:ignore: WordPress.WP.I18n.MismatchedPlaceholders -- It's not an error.\n\t\t\t\t\t\t\t\t\t'Every %2$s', // phpcs:ignore: WordPress.WP.I18n.MissingSingularPlaceholder -- It works as expected despite the CS warning.\n\t\t\t\t\t\t\t\t\t'Every %1$d %2$ss',\n\t\t\t\t\t\t\t\t\t$order->get( 'billing_frequency' ),\n\t\t\t\t\t\t\t\t\t'lifterlms'\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t$order->get( 'billing_frequency' ),\n\t\t\t\t\t\t\t\t$order->get( 'billing_period' )\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t\t?>\n\t\t\t\t\t\t<?php if ( $order->get( 'billing_cycle' ) > 0 ) : ?>\n\t\t\t\t\t\t\t<?php echo esc_html( sprintf( _n( 'for %1$d %2$s', 'for %1$d %2$ss', $order->get( 'billing_cycle' ), 'lifterlms' ), $order->get( 'billing_cycle' ), $order->get( 'billing_period' ) ) ); ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t<?php esc_html_e( 'One-time', 'lifterlms' ); ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</td>\n\t\t\t</tr>\n\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Payment Method', 'lifterlms' ); ?></th>\n\t\t\t\t<td>\n\t\t\t\t\t<?php if ( is_wp_error( $gateway ) ) : ?>\n\t\t\t\t\t\t<?php echo esc_html( $order->get( 'payment_gateway' ) ); ?>\n\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t<?php echo esc_html( $gateway->get_title() ); ?>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Action run immediately after the payment method is output within the view order information template.\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @since Unknown\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @param LLMS_Order $order Order object.\n\t\t\t\t\t\t */\n\t\t\t\t\t\tdo_action( 'lifterlms_view_order_after_payment_method', $order );\n\t\t\t\t\t?>\n\t\t\t\t</td>\n\t\t\t</tr>\n\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Start Date', 'lifterlms' ); ?></th>\n\t\t\t\t<td><?php echo esc_html( $order->get_date( 'date', 'F j, Y' ) ); ?></td>\n\t\t\t</tr>\n\t\t\t<?php if ( $order->is_recurring() ) : ?>\n\t\t\t\t<tr>\n\t\t\t\t\t<th><?php esc_html_e( 'Last Payment Date', 'lifterlms' ); ?></th>\n\t\t\t\t\t<td><?php echo esc_html( $order->get_last_transaction_date( 'llms-txn-succeeded', 'any', 'F j, Y' ) ); ?></td>\n\t\t\t\t</tr>\n\n\t\t\t\t<?php if ( 'llms-pending-cancel' !== $order->get( 'status' ) ) : ?>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th><?php esc_html_e( 'Next Payment Date', 'lifterlms' ); ?></th>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<?php if ( $order->has_scheduled_payment() ) : ?>\n\t\t\t\t\t\t\t\t<?php echo esc_html( $order->get_next_payment_due_date( 'F j, Y' ) ); ?>\n\t\t\t\t\t\t\t<?php else : ?>\n\t\t\t\t\t\t\t\t&ndash;\n\t\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t<?php endif; ?>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php if ( ! $order->is_recurring() || 'lifetime' !== $order->get( 'access_expiration' ) || 'llms-pending-cancel' === $order->get( 'status' ) ) : ?>\n\t\t\t<tr>\n\t\t\t\t<th><?php esc_html_e( 'Expiration Date', 'lifterlms' ); ?></th>\n\t\t\t\t<td><?php echo esc_html( $order->get_access_expiration_date( 'F j, Y' ) ); ?></td>\n\t\t\t</tr>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php\n\t\t\t\t/**\n\t\t\t\t * Action run before the closing of the `<tbody>` element on the view orders information table.\n\t\t\t\t *\n\t\t\t\t * @since Unknown\n\t\t\t\t *\n\t\t\t\t * @param LLMS_Order $order Order object.\n\t\t\t\t */\n\t\t\t\tdo_action( 'lifterlms_view_order_table_body', $order );\n\t\t\t?>\n\t\t</tbody>\n\t</table>\n</section>\n"
  },
  {
    "path": "templates/myaccount/view-order-transactions.php",
    "content": "<?php\n/**\n * Single order transactions table.\n *\n * @package LifterLMS/Templates\n *\n * @since 3.10.0\n * @since 6.0.0 Logic to return empty when no transactions present has been moved to the template function.\n * @version 6.0.0\n *\n * @var array $transactions Result array from {@see LLMS_Order::get_transactions()}.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<table class=\"orders-table transactions\" id=\"llms-txns\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th><?php esc_html_e( 'Transaction', 'lifterlms' ); ?></th>\n\t\t\t<th><?php esc_html_e( 'Date', 'lifterlms' ); ?></th>\n\t\t\t<th><?php esc_html_e( 'Amount', 'lifterlms' ); ?></th>\n\t\t\t<th><?php esc_html_e( 'Method', 'lifterlms' ); ?></th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t<?php foreach ( $transactions['transactions'] as $txn ) : ?>\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t#<?php echo esc_html( $txn->get( 'id' ) ); ?>\n\t\t\t\t<span class=\"llms-status <?php echo esc_attr( $txn->get( 'status' ) ); ?>\"><?php echo esc_html( $txn->get_status_name() ); ?></span>\n\t\t\t</td>\n\t\t\t<td><?php echo esc_html( $txn->get_date( 'date' ) ); ?></td>\n\t\t\t<td>\n\t\t\t\t<?php $refund_amount = $txn->get_price( 'refund_amount', array(), 'float' ); ?>\n\t\t\t\t<?php if ( $refund_amount ) : ?>\n\t\t\t\t\t<del><?php echo wp_kses( $txn->get_price( 'amount' ), LLMS_ALLOWED_HTML_PRICES ); ?></del>\n\t\t\t\t\t<?php echo wp_kses( $txn->get_net_amount(), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<?php echo wp_kses( $txn->get_price( 'amount' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t\t\t<?php endif; ?>\n\t\t\t</td>\n\t\t\t<td><?php echo wp_kses_post( $txn->get( 'gateway_source_description' ) ); ?></td>\n\t\t</tr>\n\t<?php endforeach; ?>\n\t</tbody>\n\t<?php if ( $transactions['pages'] > 1 ) : ?>\n\t\t<tfoot>\n\t\t\t<tr>\n\t\t\t\t<td colspan=\"5\">\n\t\t\t\t\t<?php if ( $transactions['page'] > 1 ) : ?>\n\t\t\t\t\t\t<a class=\"llms-button-secondary small\" href=\"<?php echo esc_url( add_query_arg( 'txnpage', $transactions['page'] - 1 ) ); ?>#llms-txns\"><?php esc_html_e( 'Back', 'lifterlms' ); ?></a>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<?php if ( $transactions['page'] < $transactions['pages'] ) : ?>\n\t\t\t\t\t\t<a class=\"llms-button-secondary small\" href=\"<?php echo esc_url( add_query_arg( 'txnpage', $transactions['page'] + 1 ) ); ?>#llms-txns\"><?php esc_html_e( 'Next', 'lifterlms' ); ?></a>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t</tfoot>\n\t<?php endif; ?>\n</table>\n"
  },
  {
    "path": "templates/myaccount/view-order.php",
    "content": "<?php\n/**\n * View an Order.\n *\n * @package LifterLMS/Templates\n *\n * @since 3.0.0\n * @since 3.33.0 Pass the current order object instance as param for all the actions and filters, plus redundant check on order existence removed.\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @since 5.4.0 Inform about deleted products.\n * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n * @since 6.0.0 Load sub-templates using hooks and template functions.\n * @version 6.0.0\n *\n * @var LLMS_Order $order        Current order object.\n * @var array      $transactions Result array from {@see LLMS_Order::get_transactions()}.\n * @var string     $layout_class The view's layout classname. Either `llms-stack-cols` or an empty string for the default side-by-side layout.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$classes = array_filter(\n\tarray_map(\n\t\t'esc_attr',\n\t\tarray( 'llms-sd-section', 'llms-view-order', $layout_class )\n\t)\n);\n\nllms_print_notices();\n?>\n\n<div class=\"<?php echo esc_attr( implode( ' ', $classes ) ); ?>\">\n\n\t<h2 class=\"order-title\">\n\t\t<?php echo esc_html( sprintf( __( 'Order #%d', 'lifterlms' ), $order->get( 'id' ) ) ); ?>\n\t\t<span class=\"llms-status <?php echo esc_attr( $order->get( 'status' ) ); ?>\"><?php echo wp_kses_post( $order->get_status_name() ); ?></span>\n\t</h2>\n\n\t<?php\n\t\t/**\n\t\t * Action run prior to the display of order information.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param LLMS_Order $order The order being displayed.\n\t\t */\n\t\tdo_action( 'lifterlms_before_view_order_table', $order );\n\n\t\t/**\n\t\t * Displays information about the order.\n\t\t *\n\t\t * @hooked llms_template_view_order_information 10\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param LLMS_Order $order The order being displayed.\n\t\t */\n\t\tdo_action( 'llms_view_order_information', $order );\n\n\t\t/**\n\t\t * Displays user actions for the order.\n\t\t *\n\t\t * @hooked llms_template_view_order_information 10\n\t\t *\n\t\t * @since 6.0.0\n\t\t *\n\t\t * @param LLMS_Order $order The order being displayed.\n\t\t */\n\t\tdo_action( 'llms_view_order_actions', $order );\n\t?>\n\n\t<div class=\"clear\"></div>\n\n\t<?php\n\t\t/**\n\t\t * Displays order transactions.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param LLMS_Order $order        The order being displayed.\n\t\t * @param array      $transactions Result array from {@see LLMS_Order::get_transactions()}.\n\t\t */\n\t\tdo_action( 'llms_view_order_transactions', $order, $transactions );\n\n\t\t/**\n\t\t * Action run after the display of order information.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param LLMS_Order $order The order being displayed.\n\t\t */\n\t\tdo_action( 'lifterlms_after_view_order_table', $order );\n\t?>\n\n</div>\n"
  },
  {
    "path": "templates/notices/debug.php",
    "content": "<?php\n/**\n * Show debug notices\n *\n * @package     Lifterlms/Templates\n *\n * @since       1.0.0\n * @version     1.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $messages ) {\n\treturn;\n}\n?>\n<?php do_action( 'lifterlms_before_debug_notices' ); ?>\n<?php foreach ( $messages as $message ) : ?>\n\t<div class=\"llms-notice llms-debug\"><?php print_r( $message ); ?></div>\n<?php endforeach; ?>\n<?php do_action( 'lifterlms_after_debug_notices' ); ?>\n"
  },
  {
    "path": "templates/notices/error.php",
    "content": "<?php\n/**\n * Show error notices\n *\n * @package     Lifterlms/Templates\n *\n * @since       1.0.0\n * @version     1.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $messages ) {\n\treturn;\n}\n?>\n<?php do_action( 'lifterlms_before_error_notices' ); ?>\n<ul class=\"llms-notice llms-error\">\n\t<?php foreach ( $messages as $message ) : ?>\n\t\t<li><?php echo wp_kses_post( $message ); ?></li>\n\t<?php endforeach; ?>\n</ul>\n<?php do_action( 'lifterlms_after_error_notices' ); ?>\n"
  },
  {
    "path": "templates/notices/notice.php",
    "content": "<?php\n/**\n * Show regular (info) notices\n *\n * @package     Lifterlms/Templates\n *\n * @since       1.0.0\n * @version     1.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $messages ) {\n\treturn;\n}\n?>\n<?php do_action( 'lifterlms_before_notices' ); ?>\n<?php foreach ( $messages as $message ) : ?>\n\t<div class=\"llms-notice\"><?php print_r( $message ); ?></div>\n<?php endforeach; ?>\n<?php do_action( 'lifterlms_after_notices' ); ?>\n"
  },
  {
    "path": "templates/notices/success.php",
    "content": "<?php\n/**\n * Show success notices\n *\n * @package     Lifterlms/Templates\n *\n * @since       1.0.0\n * @version     1.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $messages ) {\n\treturn;\n}\n?>\n<?php do_action( 'lifterlms_before_success_notices' ); ?>\n<?php foreach ( $messages as $message ) : ?>\n\t<div class=\"llms-notice llms-success\"><?php print_r( $message ); ?></div>\n<?php endforeach; ?>\n<?php do_action( 'lifterlms_after_success_notices' ); ?>\n"
  },
  {
    "path": "templates/notifications/basic.php",
    "content": "<?php\n/**\n * Basic Notification Template\n *\n * @package LifterLMS/Templates\n *\n * @since 3.8.0\n * @version 3.29.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"<?php echo esc_attr( $classes ); ?>\"\n\t<?php foreach ( (array) $attributes as $att => $val ) : ?>\n\t\t\t<?php echo esc_attr( 'data-' . $att ); ?>=\"<?php echo esc_attr( $val ); ?>\"\n\t<?php endforeach; ?>\n\tid=\"<?php echo esc_attr( 'llms-notification-' . $id ); ?>\">\n\n\t<?php do_action( 'llms_before_basic_notification', $id ); ?>\n\n\t<i class=\"llms-notification-dismiss fa fa-times-circle\" aria-hidden=\"true\"></i>\n\n\t<section class=\"llms-notification-content\">\n\t\t<div class=\"llms-notification-main\">\n\t\t\t<div class=\"llms-notification-title\"><?php echo esc_html( $title ); ?></div>\n\t\t\t<div class=\"llms-notification-body\"><?php echo wp_kses_post( $body ); ?></div>\n\t\t</div>\n\n\t\t<?php if ( $icon ) : ?>\n\t\t\t<aside class=\"llms-notification-aside\">\n\t\t\t\t<img class=\"llms-notification-icon\" alt=\"<?php echo esc_attr( $title ); ?>\" src=\"<?php echo esc_url( $icon ); ?>\">\n\t\t\t</aside>\n\t\t<?php endif; ?>\n\t</section>\n\n\t<?php if ( is_string( $footer ) && ! empty( $footer ) ) : ?>\n\t\t<footer class=\"llms-notification-footer\">\n\t\t\t<?php echo wp_kses_post( $footer ); ?>\n\t\t\t<?php if ( 'new' !== $status ) : ?>\n\t\t\t\t<span class=\"llms-notification-date\"><?php echo esc_html( $date ); ?></span>\n\t\t\t<?php endif; ?>\n\t\t</footer>\n\t<?php endif; ?>\n\n\t<?php do_action( 'llms_after_basic_notification', $id ); ?>\n\n</div>\n"
  },
  {
    "path": "templates/product/access-plan-button.php",
    "content": "<?php\n/**\n * Single Access Plan Button.\n *\n * @property LLMS_Access_Plan $plan Instance of the LLMS_Access_Plan.\n * @author LifterLMS\n * @package LifterLMS/Templates\n *\n * @since 3.23.0\n * @since 4.2.0 Added `llms_display_free_enroll_form` filter hook.\n * @version 4.2.0\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n<?php\n/**\n * Filter the displaying of the free enroll form.\n *\n * @since 4.2.0\n *\n * @param boolean          $display Whether or not displaying the free enroll form.\n * @param LLMS_Access_Plan $plan    Instance of the LLMS_Access_Plan.\n */\nif ( apply_filters( 'llms_display_free_enroll_form', get_current_user_id() && $plan->has_free_checkout() && $plan->is_available_to_user(), $plan ) ) :\n\t?>\n\t<?php llms_get_template( 'product/free-enroll-form.php', compact( 'plan' ) ); ?>\n<?php else : ?>\n\t<a class=\"llms-button-action button\" href=\"<?php echo esc_url( $plan->get_checkout_url() ); ?>\" aria-label=\"<?php echo esc_attr( $plan->get_enroll_text( true ) ); ?>\"><?php echo esc_html( $plan->get_enroll_text() ); ?></a>\n<?php endif; ?>\n"
  },
  {
    "path": "templates/product/access-plan-description.php",
    "content": "<?php\n/**\n * Single Access Plan Description\n *\n * @property  obj  $plan  Instance of the LLMS_Access_Plan\n * @author    LifterLMS\n * @package   LifterLMS/Templates\n * @since     3.23.0\n * @version   3.23.0\n */\ndefined( 'ABSPATH' ) || exit;\n\n// If the plan has no content, don't display anything.\nif ( ! $plan->get( 'content' ) ) {\n\treturn;\n}\n\n?>\n<div class=\"llms-access-plan-description\"><?php echo wp_kses_post( $plan->get( 'content' ) ); ?></div>\n"
  },
  {
    "path": "templates/product/access-plan-feature.php",
    "content": "<?php\n/**\n * Single Access Plan Featured Tab\n *\n * @property  obj  $plan  Instance of the LLMS_Access_Plan\n * @author    LifterLMS\n * @package   LifterLMS/Templates\n * @since     3.23.0\n * @version   3.23.0\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"llms-access-plan-featured\">\n\t<?php if ( $plan->is_featured() ) : ?>\n\t\t<?php echo wp_kses_post( apply_filters( 'lifterlms_featured_access_plan_text', __( 'FEATURED', 'lifterlms' ), $plan ) ); ?>\n\t<?php else : ?>\n\t\t&nbsp;\n\t<?php endif; ?>\n</div>\n"
  },
  {
    "path": "templates/product/access-plan-pricing.php",
    "content": "<?php\n/**\n * Single Access Plan Pricing\n *\n * @property  obj  $plan  Instance of the LLMS_Access_Plan\n * @author    LifterLMS\n * @package   LifterLMS/Templates\n * @since     3.23.0\n * @version   3.29.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$schedule = $plan->get_schedule_details();\n$expires  = $plan->get_expiration_details();\n?>\n<div class=\"llms-access-plan-pricing regular\">\n\n\t<div class=\"llms-access-plan-price\">\n\n\t\t<?php if ( $plan->is_on_sale() ) : ?>\n\t\t\t<em class=\"stamp\"><?php esc_html_e( 'SALE', 'lifterlms' ); ?></em>\n\t\t<?php endif; ?>\n\n\t\t<span class=\"price-regular\"><?php echo wp_kses( $plan->get_price( 'price' ), LLMS_ALLOWED_HTML_PRICES ); ?></span>\n\n\t\t<?php if ( $plan->is_on_sale() ) : ?>\n\t\t\t<span class=\"price-sale\"><?php echo wp_kses( $plan->get_price( 'sale_price' ), LLMS_ALLOWED_HTML_PRICES ); ?></span>\n\t\t<?php endif; ?>\n\n\t</div>\n\n\t<?php if ( $schedule ) : ?>\n\t\t<div class=\"llms-access-plan-schedule\"><?php echo esc_html( $schedule ); ?></div>\n\t<?php endif; ?>\n\n\t<?php if ( $expires ) : ?>\n\t\t<div class=\"llms-access-plan-expiration\"><?php echo esc_html( $expires ); ?></div>\n\t<?php endif; ?>\n\n\t<?php if ( $plan->is_on_sale() && $plan->get( 'sale_end' ) ) : ?>\n\t\t<div class=\"llms-access-plan-sale-end\"><?php echo esc_html( sprintf( __( 'sale ends %s', 'lifterlms' ), $plan->get_date( 'sale_end', get_option( 'date_format' ) ) ) ); ?></div>\n\t<?php endif; ?>\n\n</div>\n"
  },
  {
    "path": "templates/product/access-plan-restrictions.php",
    "content": "<?php\n/**\n * Single Access Plan Restrictions\n *\n * @author    LifterLMS\n * @package   LifterLMS/Templates\n *\n * @property  LLMS_Access_Plan $plan Instance of the plan object.\n *\n * @since     3.23.0\n * @since     3.30.0 Added redirect parameter to `$membership_link`\n * @version   3.30.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<?php if ( $plan->has_availability_restrictions() ) : ?>\n\t<div class=\"llms-access-plan-restrictions\">\n\t\t<em class=\"stamp\"><?php esc_html_e( 'MEMBER PRICING', 'lifterlms' ); ?></em>\n\t\t<ul>\n\t\t\t<?php\n\t\t\tforeach ( $plan->get_array( 'availability_restrictions' ) as $mid ) :\n\t\t\t\t$membership_link = get_permalink( $mid );\n\t\t\t\t$redirection     = $plan->get_redirection_url();\n\t\t\t\tif ( ! empty( $redirection ) ) {\n\t\t\t\t\t$membership_link = add_query_arg(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'redirect' => $redirection,\n\t\t\t\t\t\t),\n\t\t\t\t\t\t$membership_link\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t?>\n\t\t\t\t<li><a href=\"<?php echo esc_url( $membership_link ); ?>\"><?php echo esc_html( get_the_title( $mid ) ); ?></a></li>\n\t\t\t<?php endforeach; ?>\n\t\t</ul>\n\t</div>\n<?php endif; ?>\n"
  },
  {
    "path": "templates/product/access-plan-title.php",
    "content": "<?php\n/**\n * Single Access Plan Title\n *\n * @property  obj  $plan  Instance of the LLMS_Access_Plan\n * @author    LifterLMS\n * @package   LifterLMS/Templates\n * @since     3.23.0\n * @version   3.23.0\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n<h4 class=\"llms-access-plan-title\"><?php echo esc_html( $plan->get( 'title' ) ); ?></h4>\n"
  },
  {
    "path": "templates/product/access-plan-trial.php",
    "content": "<?php\n/**\n * Single Access Plan Trial\n *\n * @property  obj  $plan  Instance of the LLMS_Access_Plan\n * @author    LifterLMS\n * @package   LifterLMS/Templates\n * @since     3.23.0\n * @version   3.23.0\n */\ndefined( 'ABSPATH' ) || exit;\n\n// If the plan has no trial, don't display anything.\nif ( ! $plan->has_trial() ) {\n\treturn;\n}\n\n?>\n<div class=\"llms-access-plan-pricing trial\">\n\t<?php if ( $plan->has_trial() ) : ?>\n\t\t<div class=\"llms-access-plan-price\">\n\t\t\t<em class=\"stamp\"><?php esc_html_e( 'TRIAL', 'lifterlms' ); ?></em>\n\t\t\t<?php echo wp_kses( $plan->get_price( 'trial_price' ), LLMS_ALLOWED_HTML_PRICES ); ?>\n\t\t</div>\n\t\t<div class=\"llms-access-plan-trial\"><?php echo esc_html( $plan->get_trial_details() ); ?></div>\n\t<?php else : ?>\n\t\t&nbsp;\n\t<?php endif; ?>\n</div>\n"
  },
  {
    "path": "templates/product/access-plan.php",
    "content": "<?php\n/**\n * Single Access Plan Template\n *\n * @property  obj  $plan  Instance of the LLMS_Access_Plan\n * @author    LifterLMS\n * @package   LifterLMS/Templates\n * @since     3.23.0\n * @version   3.23.0\n */\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<div class=\"<?php echo esc_attr( llms_get_access_plan_classes( $plan ) ); ?>\" id=\"llms-access-plan-<?php echo esc_attr( $plan->get( 'id' ) ); ?>\">\n\n\t<?php\n\t\t/**\n\t\t * llms_before_access_plan\n\t\t *\n\t\t * @hooked llms_template_access_plan_feature - 10\n\t\t */\n\t\tdo_action( 'llms_before_access_plan', $plan );\n\t?>\n\n\t<div class=\"llms-access-plan-content\">\n\t\t<?php\n\t\t\t/**\n\t\t\t * llms_acces_plan_content\n\t\t\t *\n\t\t\t * @hooked llms_template_access_plan_title - 10\n\t\t\t * @hooked llms_template_access_plan_pricing - 20\n\t\t\t * @hooked llms_template_access_plan_restrictions - 30\n\t\t\t * @hooked llms_template_access_plan_description - 40\n\t\t\t */\n\t\t\tdo_action( 'llms_acces_plan_content', $plan );\n\t\t?>\n\t</div>\n\n\t<div class=\"llms-access-plan-footer\">\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * llms_acces_plan_footer\n\t\t\t *\n\t\t\t * @hooked llms_template_access_plan_trial - 10\n\t\t\t * @hooked llms_template_access_plan_button - 20\n\t\t\t */\n\t\t\tdo_action( 'llms_acces_plan_footer', $plan );\n\t\t?>\n\n\t</div>\n\n\n\t<?php\n\t\t/**\n\t\t * llms_after_access_plan\n\t\t */\n\t\tdo_action( 'llms_after_access_plan', $plan );\n\t?>\n\n</div>\n"
  },
  {
    "path": "templates/product/free-enroll-form.php",
    "content": "<?php\n/**\n * Template for the free enrollment form\n *\n * Displays to logged in users on pricing tables for free access plans if free checkout is not disabled via filter\n *\n * @package LifterLMS/Templates\n *\n * @since 3.4.0\n * @since 3.30.0 Added redirect field.\n * @since 5.0.0 Use `LLMS_Forms::get_free_enroll_form_html()` in favor of deprecated `LLMS_Person_Handler::get_available_fields()`.\n * @version 5.0.0\n *\n * @property LLMS_Access_Plan $plan Instance of the plan object.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$uid = get_current_user_id();\nif ( ! $uid || empty( $plan ) || ! $plan->has_free_checkout() ) {\n\treturn;\n}\n?>\n\n<form action=\"\" class=\"llms-free-enroll-form\" method=\"POST\">\n\n\t<?php\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo LLMS_Forms::instance()->get_free_enroll_form_html( $plan );\n\t?>\n\n\t<?php wp_nonce_field( 'create_pending_order', '_llms_checkout_nonce' ); ?>\n\n\t<input name=\"action\" type=\"hidden\" value=\"create_pending_order\">\n\t<input name=\"form\" type=\"hidden\" value=\"free_enroll\">\n\t<input name=\"llms_agree_to_terms\" type=\"hidden\" value=\"yes\">\n\n\t<?php do_action( 'lifterlms_after_free_enroll_fields' ); ?>\n\n\t<button class=\"llms-button-action button\" type=\"submit\" aria-label=\"<?php echo esc_attr( $plan->get_enroll_text( true ) ); ?>\"><?php echo esc_html( $plan->get_enroll_text() ); ?></button>\n\n</form>\n"
  },
  {
    "path": "templates/product/not-purchasable.php",
    "content": "<?php\n/**\n * Output errors when a product is not purchasable\n *\n * @package LifterLMS/Templates/Product\n *\n * @since 3.38.0\n * @version 3.38.0\n *\n * @property LLMS_Product $product Product object of the course or membership.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( 'course' === $product->get( 'type' ) ) :\n\t$course = new LLMS_Course( $product->post ); ?>\n\n\t<?php if ( 'yes' === $course->get( 'enrollment_period' ) ) : ?>\n\n\t\t<?php if ( $course->get( 'enrollment_start_date' ) && ! $course->has_date_passed( 'enrollment_start_date' ) ) : ?>\n\t\t\t<?php llms_print_notice( $course->get( 'enrollment_opens_message' ), 'error' ); ?>\n\t\t<?php elseif ( $course->has_date_passed( 'enrollment_end_date' ) ) : ?>\n\t\t\t<?php llms_print_notice( $course->get( 'enrollment_closed_message' ), 'error' ); ?>\n\t\t<?php endif; ?>\n\n\t<?php endif; ?>\n\n\t<?php if ( ! $course->has_capacity() ) : ?>\n\t\t<?php llms_print_notice( $course->get( 'capacity_message' ), 'error' ); ?>\n\t<?php endif; ?>\n\n\t<?php\nendif;\n"
  },
  {
    "path": "templates/product/pricing-table.php",
    "content": "<?php\n/**\n * Product (Course & Membership) Pricing Table Template\n *\n * @package LifterLMS/Templates/Product\n *\n * @since 3.0.0\n * @since 6.0.0 Removed the deprecated and misspelled `$purchaseable` global variable.\n * @version 6.0.0\n *\n * @var LLMS_Product $product          Product object of the course or membership.\n * @var bool         $is_enrolled      Determines if current viewer is enrolled in $product.\n * @var bool         $purchasable      Determines if current product is purchasable.\n * @var bool         $has_free         Determines if any free access plans are available for the product.\n * @var bool         $has_restrictions Determines if any free access plans are available for the product.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$free_only = ( $has_free && ! $purchasable );\n?>\n\n<?php if ( ! $is_enrolled && ! $has_restrictions && ( $purchasable || $has_free ) ) : ?>\n\n\t<?php\n\t\t/**\n\t\t * Run prior to output of a course or membership pricing table.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param int $id WP_Post ID of the course or membership.\n\t\t */\n\t\tdo_action( 'lifterlms_before_access_plans', $product->get( 'id' ) );\n\t?>\n\n\t<section class=\"llms-access-plans cols-<?php echo esc_attr( $product->get_pricing_table_columns_count( $free_only ) ); ?>\">\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Run prior to listing access plans.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int $id WP_Post ID of the course or membership.\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_before_access_plans_loop', $product->get( 'id' ) );\n\t\t?>\n\n\t\t<?php foreach ( $product->get_access_plans( $free_only ) as $plan ) : ?>\n\n\t\t\t<?php\n\t\t\t\t/**\n\t\t\t\t * Outputs a single access plan\n\t\t\t\t *\n\t\t\t\t * Hooked: llms_template_access_plan - 10\n\t\t\t\t *\n\t\t\t\t * @since Unknown\n\t\t\t\t *\n\t\t\t\t * @param LLMS_Access_Plan $plan Access plan object\n\t\t\t\t */\n\t\t\t\tdo_action( 'llms_access_plan', $plan );\n\t\t\t?>\n\n\t\t<?php endforeach; ?>\n\n\t\t<?php\n\t\t\t/**\n\t\t\t * Run prior to listing access plans.\n\t\t\t *\n\t\t\t * @since Unknown\n\t\t\t *\n\t\t\t * @param int $id WP_Post ID of the course or membership.\n\t\t\t */\n\t\t\tdo_action( 'lifterlms_after_access_plans_loop', $product->get( 'id' ) );\n\t\t?>\n\n\t</section>\n\n\t<?php\n\t\t/**\n\t\t * Run after output of a course or membership pricing table.\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param int $id WP_Post ID of the course or membership.\n\t\t */\n\t\tdo_action( 'lifterlms_after_access_plans', $product->get( 'id' ) );\n\t?>\n\n<?php elseif ( ! $is_enrolled ) : ?>\n\n\t<?php\n\t\t/**\n\t\t * Pricing table output when the user is not enrolled but the product is not purchasable.\n\t\t *\n\t\t * Hooked: llms_template_product_not_purchasable - 10\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param int $id WP_Post ID of the course or membership.\n\t\t */\n\t\tdo_action( 'lifterlms_product_not_purchasable', $product->get( 'id' ) );\n\t?>\n\n<?php endif; ?>\n"
  },
  {
    "path": "templates/quiz/meta-information.php",
    "content": "<?php\n/**\n * Single Quiz: Meta Information\n *\n * @package LifterLMS/Templates\n *\n * @since 3.9.0\n * @since 4.0.0 Unknown.\n * @since 4.17.0 Return early if accessed without a logged in user or the quiz can't be loaded from the `$post` global.\n * @since 7.4.0 Used `LLMS_Quiz::get_questions_count()` method for showing count and escaped labels.\n * @version 7.4.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$student = llms_get_student();\nif ( ! $student ) {\n\treturn;\n}\n\n$quiz = llms_get_post( $post );\nif ( ! $quiz ) {\n\treturn;\n}\n$passing_percent = $quiz->get( 'passing_percent' );\n?>\n\n<h2 class=\"llms-quiz-meta-title\"><?php esc_html_e( 'Quiz Information', 'lifterlms' ); ?></h2>\n<ul class=\"llms-quiz-meta-info\">\n\t<?php if ( $passing_percent ) : ?>\n\t<li class=\"llms-quiz-meta-item llms-passing-percent\">\n\t\t<?php printf( esc_html__( 'Minimum Passing Grade: %s', 'lifterlms' ), '<span class=\"llms-pass-perc\">' . esc_html( $passing_percent ) . '%</span>' ); ?>\n\t</li>\n\t<?php endif; ?>\n\n\t<li class=\"llms-quiz-meta-item llms-attempts\">\n\t\t<?php printf( esc_html__( 'Remaining Attempts: %s', 'lifterlms' ), '<span class=\"llms-attempts\">' . esc_html( $student->quizzes()->get_attempts_remaining_for_quiz( $quiz->get( 'id' ) ) ) . '</span>' ); ?>\n\t</li>\n\n\t<li class=\"llms-quiz-meta-item llms-question-count\">\n\t\t<?php printf( esc_html__( 'Questions: %s', 'lifterlms' ), '<span class=\"llms-question-count\">' . esc_html( $quiz->get_questions_count() ) . '</span>' ); ?>\n\t</li>\n\n\t<?php if ( $quiz->has_time_limit() && ! $student->has_unlimited_quiz_time() ) : ?>\n\t<li class=\"llms-quiz-meta-item llms-time-limit\">\n\t\t<?php printf( esc_html__( 'Time Limit: %s', 'lifterlms' ), '<span class=\"llms-time-limit\">' . esc_html( $quiz->get_time_limit_string() ) . '</span>' ); ?>\n\t</li>\n\t<?php endif; ?>\n</ul>\n"
  },
  {
    "path": "templates/quiz/questions/content-choice.php",
    "content": "<?php\n/**\n * Choice Question Template\n *\n * @package LifterLMS/Templates\n *\n * @since 3.16.0\n * @since 7.8.0 Account for question answers.\n * @version 7.8.0\n *\n * @param $attempt  LLMS_Quiz_Attempt LLMS_Quiz_Attempt instance.\n * @param $question LLMS_Question     LLMS_Question instance.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$input_type      = ( 'yes' === $question->get( 'multi_choices' ) ) ? 'checkbox' : 'radio';\n$question_answer = isset( $attempt ) && $attempt ? $attempt->get_question_answer( $question->get( 'id' ) ) : array();\n?>\n\n\t<fieldset class=\"llms-question-choices\">\n\t\t<legend class=\"sr-only\">\n\t\t\t<?php\n\t\t\t\techo esc_html( wp_strip_all_tags( $question->get_question( 'html', $attempt ) ) );\n\t\t\t?>\n\t\t</legend>\n\t\t<?php foreach ( $question->get_choices() as $choice ) : ?>\n\t\t\t<?php\n\t\t\t$answer = is_array( $question_answer ) ? in_array( $choice->get( 'id' ), $question_answer, true ) ? $choice->get( 'id' ) : null : null;\n\t\t\t?>\n\t\t\t<div class=\"llms-choice type--text\" id=\"choice-wrapper-<?php echo esc_attr( $choice->get( 'id' ) ); ?>\">\n\t\t\t\t<input id=\"choice-<?php echo esc_attr( $choice->get( 'id' ) ); ?>\" name=\"question_<?php echo esc_attr( $question->get( 'id' ) ); ?>[]\" type=\"<?php echo esc_attr( $input_type ); ?>\" value=\"<?php echo esc_attr( $choice->get( 'id' ) ); ?>\" <?php checked( $answer, $choice->get( 'id' ) ); ?>>\n\t\t\t\t<label for=\"choice-<?php echo esc_attr( $choice->get( 'id' ) ); ?>\" data-marker=\"<?php echo esc_attr( $choice->get( 'marker' ) ); ?>\">\n\t\t\t\t\t<p class=\"llms-choice-text\"><?php echo esc_html( $choice->get( 'choice' ) ); ?></p>\n\t\t\t\t</label>\n\t\t\t</div>\n\t\t<?php endforeach; ?>\n\t</fieldset>\n"
  },
  {
    "path": "templates/quiz/questions/content-picture_choice.php",
    "content": "<?php\n/**\n * Picture choice question template.\n *\n * @package LifterLMS/Templates\n *\n * @since 3.16.0\n * @since 5.9.0 Use `llms-flex-cols` in favor of `llms-cols` for arranging choices in columns.\n * @since 7.8.0 Account for question answers.\n * @version 7.8.0\n *\n * @var LLMS_Quiz_Attempt $attempt  Current quiz attempt object.\n * @var LLMS_Question     $question Question object.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$input_type      = ( 'yes' === $question->get( 'multi_choices' ) ) ? 'checkbox' : 'radio';\n$choices         = $question->get_choices();\n$cols            = llms_get_picture_choice_question_cols( count( $choices ) );\n$question_answer = isset( $attempt ) && $attempt ? $attempt->get_question_answer( $question->get( 'id' ) ) : array();\n?>\n\n<fieldset class=\"llms-question-choices type--picture\">\n\t<legend class=\"sr-only\">\n\t\t<?php\n\t\techo esc_html( wp_strip_all_tags( $question->get_question( 'html', $attempt ) ) );\n\t\t?>\n\t</legend>\n\t<?php foreach ( $choices as $choice ) : ?>\n\t\t<?php\n\t\t$answer = is_array( $question_answer ) ? in_array( $choice->get( 'id' ), $question_answer, true ) ? $choice->get( 'id' ) : null : null;\n\t\t?>\n\t\t<div class=\"llms-choice type--picture llms-col-<?php echo absint( $cols ); ?>\" id=\"choice-wrapper-<?php echo esc_attr( $choice->get( 'id' ) ); ?>\">\n\t\t\t<input id=\"choice-<?php echo esc_attr( $choice->get( 'id' ) ); ?>\" name=\"question_<?php echo esc_attr( $question->get( 'id' ) ); ?>[]\" type=\"<?php echo esc_attr( $input_type ); ?>\" value=\"<?php echo esc_attr( $choice->get( 'id' ) ); ?>\" <?php checked( $answer, $choice->get( 'id' ) ); ?>>\n\t\t\t<label for=\"choice-<?php echo esc_attr( $choice->get( 'id' ) ); ?>\" data-marker=\"<?php echo esc_attr( $choice->get( 'marker' ) ); ?>\">\n\t\t\t\t<div class=\"llms-choice-image\"><?php echo wp_kses_post( $choice->get_image() ); ?></div>\n\t\t\t</label>\n\t\t</div>\n\t<?php endforeach; ?>\n</fieldset>\n"
  },
  {
    "path": "templates/quiz/questions/content-true_false.php",
    "content": "<?php\n/**\n * True / False question template\n *\n * @package LifterLMS/Templates\n *\n * @since    3.16.0\n * @version  3.16.0\n *\n * @arg  $attempt  (obj)  LLMS_Quiz_Attempt instance\n * @arg  $question (obj)  LLMS_Question instance\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'quiz/questions/content-choice.php', $args );\n"
  },
  {
    "path": "templates/quiz/questions/description.php",
    "content": "<?php\n/**\n * Single Question description template\n *\n * @package LifterLMS/Templates\n *\n * @since    3.16.0\n * @version  3.16.6\n *\n * @arg  $attempt  (obj)  LLMS_Quiz_Attempt instance\n * @arg  $question (obj)  LLMS_Question instance\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $question->has_description() ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-question-description\"><?php echo wp_kses( $question->get_description(), LLMS_ALLOWED_HTML_FORM_FIELDS ); ?></div>\n"
  },
  {
    "path": "templates/quiz/questions/image.php",
    "content": "<?php\n/**\n * Single Question featured image template\n *\n * @package LifterLMS/Templates\n *\n * @since    3.16.0\n * @version  3.16.0\n *\n * @arg  $attempt  (obj)  LLMS_Quiz_Attempt instance\n * @arg  $question (obj)  LLMS_Question instance\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $question->has_image() ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-question-image\">\n\t<img alt=\"<?php echo esc_attr( $question->get( 'title' ) ); ?>\" src=\"<?php echo esc_url( $question->get_image( 'size' ) ); ?>\">\n</div>\n"
  },
  {
    "path": "templates/quiz/questions/video.php",
    "content": "<?php\n/**\n * Single Question featured video template\n *\n * @package LifterLMS/Templates\n *\n * @since    3.16.0\n * @version  3.16.0\n *\n * @arg  $attempt  (obj)  LLMS_Quiz_Attempt instance\n * @arg  $question (obj)  LLMS_Question instance\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $question->has_video() ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-question-video llms-video-wrapper\">\n\t<div class=\"center-video\"><?php\n\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\techo $question->get_video();\n\t?></div>\n</div>\n"
  },
  {
    "path": "templates/quiz/questions/wrapper-end.php",
    "content": "<?php\n/**\n * Single Question: Wrapper end\n *\n * @package LifterLMS/Templates\n *\n * @since    1.0.0\n * @version  3.16.0\n *\n * @arg  $attempt  (obj)  LLMS_Quiz_Attempt instance\n * @arg  $question (obj)  LLMS_Question instance\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n</div>\n"
  },
  {
    "path": "templates/quiz/questions/wrapper-start.php",
    "content": "<?php\n/**\n * Single Question: Wrapper Start\n *\n * @package LifterLMS/Templates\n *\n * @since    1.0.0\n * @version  3.16.0\n *\n * @arg  $attempt  (obj)  LLMS_Quiz_Attempt instance\n * @arg  $question (obj)  LLMS_Question instance\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n<div class=\"llms-question-wrapper type--<?php echo esc_attr( $question->get( 'question_type' ) ); ?>\" data-id=\"<?php echo esc_attr( $question->get( 'id' ) ); ?>\" data-type=\"<?php echo esc_attr( $question->get( 'question_type' ) ); ?>\" id=\"llms-question-<?php echo esc_attr( $question->get( 'id' ) ); ?>\">\n"
  },
  {
    "path": "templates/quiz/quiz-wrapper-end.php",
    "content": "<?php\n/**\n * Quiz Wrapper: Close\n *\n * @package LifterLMS/Templates\n *\n * @since    1.0.0\n * @version  3.16.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n</div><!--end #llms-quiz-wrapper -->\n"
  },
  {
    "path": "templates/quiz/quiz-wrapper-start.php",
    "content": "<?php\n/**\n * Quiz Wrapper: Open\n *\n * @package LifterLMS/Templates\n *\n * @since    1.0.0\n * @version  3.9.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n<div class=\"llms-quiz-wrapper\" id=\"llms-quiz-wrapper\">\n"
  },
  {
    "path": "templates/quiz/results-attempt-questions-list.php",
    "content": "<?php\n/**\n * List of attempt questions/answers for a single attempt\n *\n * @param LLMS_Quiz_Attempt $attempt LLMS_Quiz_Attempt instance.\n *\n * @since 3.17.8 Unknown.\n * @since 5.3.0 Display removed questions too.\n * @since 7.3.0 Script moved into the main llms.js.\n * @since 7.8.0 Hide answers if resumable attempt is incomplete.\n * @version 7.3.0\n *\n * @since 3.16.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n?>\n\n<ol class=\"llms-quiz-attempt-results\">\n<?php if ( ! $attempt->can_be_resumed() ) : ?>\n\t<?php\n\tforeach ( $attempt->get_question_objects() as $attempt_question ) :\n\t\t$quiz_question = $attempt_question->get_question();\n\t\tif ( ! $quiz_question ) { // Question missing/deleted.\n\t\t\t?>\n\t\t<li class=\"llms-quiz-attempt-question type--deleted status--<?php echo esc_attr( $attempt_question->get_status() ); ?> <?php echo $attempt_question->is_correct() ? 'correct' : 'incorrect'; ?>\"\n\t\t\tdata-question-id=\"<?php echo esc_attr( $attempt_question->get( 'id' ) ); ?>\"\n\t\t\tdata-grading-manual=\"<?php echo $attempt_question->can_be_manually_graded() ? 'yes' : 'no'; ?>\"\n\t\t\tdata-points=\"<?php echo esc_attr( $attempt_question->get( 'points' ) ); ?>\"\n\t\t\tdata-points-curr=\"<?php echo esc_attr( $attempt_question->get( 'earned' ) ); ?>\">\n\t\t\t<header class=\"llms-quiz-attempt-question-header\">\n\t\t\t\t<span class=\"toggle-answer\">\n\t\t\t\t\t<h3 class=\"llms-question-title\"><?php esc_html_e( 'This question has been deleted', 'lifterlms' ); ?></h3>\n\t\t\t\t\t<span class=\"llms-points\">\n\t\t\t\t\t\t<?php if ( $attempt_question->get( 'points' ) ) : ?>\n\t\t\t\t\t\t\t<?php echo esc_html( sprintf( __( '%1$d / %2$d points', 'lifterlms' ), $attempt_question->get( 'earned' ), $attempt_question->get( 'points' ) ) ); ?>\n\t\t\t\t\t\t<?php endif; ?>\n\t\t\t\t\t</span>\n\t\t\t\t\t<?php echo wp_kses_post( $attempt_question->get_status_icon() ); ?>\n\t\t\t\t</span>\n\t\t\t</header>\n\t\t</li>\n\t\t\t<?php\n\t\t\tcontinue;\n\t\t}\n\t\t?>\n\n\t<li class=\"llms-quiz-attempt-question type--<?php echo esc_attr( $quiz_question->get( 'question_type' ) ); ?> status--<?php echo esc_attr( $attempt_question->get_status() ); ?> <?php echo $attempt_question->is_correct() ? 'correct' : 'incorrect'; ?>\"\n\t\tdata-question-id=\"<?php echo esc_attr( $quiz_question->get( 'id' ) ); ?>\"\n\t\tdata-grading-manual=\"<?php echo $attempt_question->can_be_manually_graded() ? 'yes' : 'no'; ?>\"\n\t\tdata-points=\"<?php echo esc_attr( $attempt_question->get( 'points' ) ); ?>\"\n\t\tdata-points-curr=\"<?php echo esc_attr( $attempt_question->get( 'earned' ) ); ?>\">\n\t\t<header class=\"llms-quiz-attempt-question-header\">\n\t\t\t<a class=\"toggle-answer\" href=\"#\">\n\n\t\t\t\t<h3 class=\"llms-question-title\"><?php echo wp_kses_post( $quiz_question->get_question( 'plain' ) ); ?></h3>\n\n\t\t\t\t<?php if ( $quiz_question->get( 'points' ) ) : ?>\n\t\t\t\t\t<span class=\"llms-points\">\n\t\t\t\t\t\t<?php echo esc_html( sprintf( __( '%1$d / %2$d points', 'lifterlms' ), $attempt_question->get( 'earned' ), $attempt_question->get( 'points' ) ) ); ?>\n\t\t\t\t\t</span>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php echo wp_kses_post( $attempt_question->get_status_icon() ); ?>\n\n\t\t\t</a>\n\t\t</header>\n\n\t\t<section class=\"llms-quiz-attempt-question-main\">\n\n\t\t\t<?php if ( apply_filters( 'llms_quiz_show_question_description', true, $attempt, $attempt_question, $quiz_question ) && $quiz_question->has_description() ) : ?>\n\t\t\t\t<div class=\"llms-quiz-attempt-answer-section llms-question-description\">\n\t\t\t\t\t<?php echo wp_kses_post( $quiz_question->get_description() ); ?>\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php if ( $attempt_question->get( 'answer' ) ) : ?>\n\t\t\t\t<div class=\"llms-quiz-attempt-answer-section llms-student-answer\">\n\t\t\t\t\t<p class=\"llms-quiz-results-label student-answer\"><?php esc_html_e( 'Selected answer: ', 'lifterlms' ); ?></p>\n\t\t\t\t\t<?php echo wp_kses_post( $attempt_question->get_answer() ); ?>\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t\t<?php if ( ! $attempt_question->is_correct() ) : ?>\n\t\t\t\t<?php if ( llms_parse_bool( $quiz_question->get_quiz()->get( 'show_correct_answer' ) ) ) : ?>\n\t\t\t\t\t<?php if ( in_array( $quiz_question->get_auto_grade_type(), array( 'choices', 'conditional' ) ) ) : ?>\n\t\t\t\t\t\t<div class=\"llms-quiz-attempt-answer-section llms-correct-answer\">\n\t\t\t\t\t\t\t<p class=\"llms-quiz-results-label correct-answer\"><?php esc_html_e( 'Correct answer: ', 'lifterlms' ); ?></p>\n\t\t\t\t\t\t\t<?php echo wp_kses_post( $attempt_question->get_correct_answer() ); ?>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t<?php endif; ?>\n\t\t\t\t<?php endif; ?>\n\n\t\t\t\t<?php if ( llms_parse_bool( $quiz_question->get( 'clarifications_enabled' ) ) ) : ?>\n\t\t\t\t\t<div class=\"llms-quiz-attempt-answer-section llms-clarifications\">\n\t\t\t\t\t\t<p class=\"llms-quiz-results-label clarification\"><?php esc_html_e( 'Clarification: ', 'lifterlms' ); ?></p>\n\t\t\t\t\t\t<?php echo wp_kses_post( $quiz_question->get( 'clarifications' ) ); ?>\n\t\t\t\t\t</div>\n\t\t\t\t<?php endif; ?>\n\t\t\t<?php endif; ?>\n\n\n\t\t\t<?php if ( $attempt_question->has_remarks() ) : ?>\n\t\t\t\t<div class=\"llms-quiz-attempt-answer-section llms-remarks\">\n\t\t\t\t\t<p class=\"llms-quiz-results-label remarks\"><?php esc_html_e( 'Instructor remarks: ', 'lifterlms' ); ?></p>\n\t\t\t\t\t<div class=\"llms-remarks\"><?php echo wp_kses_post( wpautop( $attempt_question->get( 'remarks' ) ) ); ?></div>\n\t\t\t\t</div>\n\t\t\t<?php endif; ?>\n\n\t\t</section>\n\n\t</li>\n\t<?php endforeach; ?>\n<?php else : ?>\n\t<?php if ( is_admin() ) : ?>\n\t\t<p><?php esc_html_e( 'The quiz is still ongoing.', 'lifterlms' ); ?></p>\n\t<?php else : ?>\n\t\t<p><?php esc_html_e( \"The quiz is still ongoing. You can resume your attempt by clicking the 'Resume Quiz' button.\", 'lifterlms' ); ?></p>\n\t<?php endif; ?>\n<?php endif; ?>\n</ol>\n"
  },
  {
    "path": "templates/quiz/results-attempt.php",
    "content": "<?php\n/**\n * Quiz Single Attempt Results\n *\n * @param LLMS_Quiz_Attempt $attempt LLMS_Quiz_Attempt instance.\n *\n * @version  3.27.0\n * @since 7.8.0 Hide answers if resumable attempt is incomplete.\n *\n * @since    3.16.0\n */\ndefined( 'ABSPATH' ) || exit;\n\nif ( ! $attempt ) {\n\treturn;\n}\n$donut_class = $attempt->get( 'status' );\nif ( in_array( $donut_class, array( 'pass', 'fail' ) ) ) {\n\t$donut_class .= 'ing';\n}\n?>\n\n<h2 class=\"llms-quiz-results-title\"><?php echo esc_html( sprintf( __( 'Attempt #%d Results', 'lifterlms' ), $attempt->get( 'attempt' ) ) ); ?></h2>\n\n<?php if ( ! $attempt->can_be_resumed() ) : ?>\n\t<aside class=\"llms-quiz-results-aside\">\n\t\t<?php if ( $attempt->get_count( 'available_points' ) ) : ?>\n\t\t\t<?php echo wp_kses_post( llms_get_donut( $attempt->get( 'grade' ), $attempt->l10n( 'status' ), 'default', array( $donut_class ) ) ); ?>\n\t\t<?php endif; ?>\n\t\t<ul class=\"llms-quiz-meta-info\">\n\t\t\t<?php if ( $attempt->get_count( 'gradeable_questions' ) ) : ?>\n\t\t\t\t<li class=\"llms-quiz-meta-item\"><?php echo esc_html( sprintf( __( 'Correct Answers: %1$d / %2$d', 'lifterlms' ), $attempt->get_count( 'correct_answers' ), $attempt->get_count( 'gradeable_questions' ) ) ); ?></li>\n\t\t\t<?php endif; ?>\n\t\t\t<li class=\"llms-quiz-meta-item\"><?php echo esc_html( sprintf( __( 'Completed: %s', 'lifterlms' ), $attempt->get_date( 'start' ) ) ); ?></li>\n\t\t\t<li class=\"llms-quiz-meta-item\"><?php echo esc_html( sprintf( __( 'Total time: %s', 'lifterlms' ), $attempt->get_time() ) ); ?></li>\n\t\t</ul>\n\t</aside>\n<?php endif; ?>\n\n<section class=\"llms-quiz-results-main\">\n\n\t<?php\n\t\t/**\n\t\t * llms_single_quiz_attempt_results_main\n\t\t *\n\t\t * @hooked lifterlms_template_quiz_attempt_results_questions_list - 10\n\t\t */\n\t\tdo_action( 'llms_single_quiz_attempt_results_main', $attempt );\n\t?>\n\n</section>\n\n"
  },
  {
    "path": "templates/quiz/results.php",
    "content": "<?php\n/**\n * Quiz Results Template\n *\n * @package LifterLMS/Templates\n *\n * @since 1.0.0\n * @since 3.35.0 Access `$_GET` data via `llms_filter_input()`.\n * @since 4.17.0 Return early if accessed without a logged in user.\n * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.\n * @since 7.8.0 Don't try to round nulls.\n * @version 7.8.0\n *\n * @property LLMS_Quiz_Attempt $attempt Attempt object.\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n$quiz = llms_get_post( $post );\nif ( ! $quiz ) {\n\treturn;\n}\n\n$student = llms_get_student();\nif ( ! $student ) {\n\treturn;\n}\n\n$attempts = $student->quizzes()->get_attempts_by_quiz(\n\t$quiz->get( 'id' ),\n\tarray(\n\t\t'per_page' => 25,\n\t\t'sort'     => array(\n\t\t\t'attempt' => 'DESC',\n\t\t),\n\t)\n);\n\n$key     = llms_filter_input_sanitize_string( INPUT_GET, 'attempt_key' );\n$attempt = $key ? $student->quizzes()->get_attempt_by_key( $key ) : false;\n\nif ( ! $attempt && ! $attempts ) {\n\treturn;\n}\n?>\n\n<div class=\"clear\"></div>\n<div class=\"llms-quiz-results\">\n\n\t<?php\n\t\t/**\n\t\t * llms_single_quiz_attempt_results\n\t\t *\n\t\t * @hooked lifterlms_template_quiz_attempt_results - 10\n\t\t */\n\n\t\t/**\n\t\t * Action fired prior to the output of LifterLMS Quiz Results HTML\n\t\t *\n\t\t * @since Unknown\n\t\t *\n\t\t * @param LLMS_Quiz_Attempt $attempt Attempt object.\n\t\t */\n\t\tdo_action( 'llms_single_quiz_attempt_results', $attempt );\n\t?>\n\n\t<?php if ( $attempts ) : ?>\n\t\t<section class=\"llms-quiz-results-history\">\n\t\t\t<h2 class=\"llms-quiz-results-title\"><?php esc_html_e( 'View Previous Attempts', 'lifterlms' ); ?></h2>\n\t\t\t<label for=\"llms-quiz-attempt-select\" class=\"sr-only\"><?php esc_html_e( 'Select an Attempt', 'lifterlms' ); ?></label>\n\t\t\t<select id=\"llms-quiz-attempt-select\">\n\t\t\t\t<option value=\"\">-- <?php esc_html_e( 'Select an Attempt', 'lifterlms' ); ?> --</option>\n\t\t\t\t<?php foreach ( $attempts as $attempt ) : ?>\n\t\t\t\t\t<option value=\"<?php echo esc_url( $attempt->get_permalink() ); ?>\">\n\t\t\t\t\t\t<?php // Translators: %1$d = Attempt number; %2$s = Grade percentage; %3$s = Pass/fail text. ?>\n\t\t\t\t\t\t<?php echo esc_html( sprintf( __( 'Attempt #%1$d - %2$s (%3$s)', 'lifterlms' ), $attempt->get( 'attempt' ), round( $attempt->get( 'grade' ) ?? 0, 2 ) . '%', $attempt->l10n( 'status' ) ) ); ?>\n\t\t\t\t\t</option>\n\t\t\t\t<?php endforeach; ?>\n\t\t\t</select>\n\t\t</section>\n\t<?php endif; ?>\n\n</div>\n"
  },
  {
    "path": "templates/quiz/return-to-lesson.php",
    "content": "<?php\n/**\n * Single Quiz: Return to Lesson Link\n *\n * @package LifterLMS/Templates\n *\n * @since    1.0.0\n * @version  3.16.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n\n$quiz = llms_get_post( $post );\nif ( ! $quiz ) {\n\treturn;\n}\n?>\n\n<div class=\"clear\"></div>\n<div class=\"llms-return\">\n\t<a href=\"<?php echo esc_url( get_permalink( $quiz->get( 'lesson_id' ) ) ); ?>\"><?php esc_html_e( 'Return to Lesson', 'lifterlms' ); ?></a>\n</div>\n"
  },
  {
    "path": "templates/quiz/start-button.php",
    "content": "<?php\n/**\n * Quiz Start & Next lesson buttons\n *\n * @since 1.0.0\n * @since 3.25.0 Unknown.\n * @since 4.17.0 Early bail on orphan quiz.\n * @since 7.8.0 Added support for quiz resume.\n * @version 7.8.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\n$quiz   = llms_get_post( $post );\n$lesson = $quiz->get_lesson();\n\nif ( ! $lesson || ! is_a( $lesson, 'LLMS_Lesson' ) ) {\n\treturn;\n}\n?>\n\n<div class=\"llms-quiz-buttons llms-button-wrapper\" id=\"quiz-start-button\">\n\n\t<?php\n\t\t/**\n\t\t * Fired before the start quiz button\n\t\t *\n\t\t * @since Unknown\n\t\t */\n\t\tdo_action( 'lifterlms_before_start_quiz' );\n\t?>\n\n\t<?php if ( $quiz ) : ?>\n\n\t\t<?php if ( $quiz->is_open() ) : ?>\n\t\t\t<form method=\"POST\" action=\"\" name=\"llms_start_quiz\" enctype=\"multipart/form-data\">\n\n\t\t\t\t<input id=\"llms-lesson-id\" name=\"llms_lesson_id\" type=\"hidden\" value=\"<?php echo esc_attr( $lesson->get( 'id' ) ); ?>\"/>\n\t\t\t\t<input id=\"llms-quiz-id\" name=\"llms_quiz_id\" type=\"hidden\" value=\"<?php echo esc_attr( $quiz->get( 'id' ) ); ?>\"/>\n\n\t\t\t\t<input type=\"hidden\" name=\"action\" value=\"llms_start_quiz\" />\n\n\t\t\t\t<?php wp_nonce_field( 'llms_start_quiz' ); ?>\n\n\t\t\t\t<?php if ( $quiz->can_be_resumed_by_student() ) : ?>\n\t\t\t\t\t<?php\n\t\t\t\t\t\t$message  = esc_html__( 'You have a partially completed attempt for this quiz. You can continue where you left off by clicking the Resume Quiz button below.', 'lifterlms' );\n\t\t\t\t\t\t$message .= '<div><button class=\"llms-start-quiz-button llms-button-secondary button\" id=\"llms_start_quiz\" name=\"llms_start_quiz\" type=\"submit\">';\n\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Filters the restart quiz button text\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @since Unknown\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @param string      $button_text The start quiz button text.\n\t\t\t\t\t\t * @param LLMS_Quiz   $quiz        The current quiz instance.\n\t\t\t\t\t\t * @param LLMS_Lesson $lesson      The parent lesson instance.\n\t\t\t\t\t\t */\n\t\t\t\t\t\t$message .= wp_kses_post( apply_filters( 'lifterlms_restart_quiz_button_text', __( 'Restart Quiz Instead', 'lifterlms' ), $quiz, $lesson ) );\n\n\t\t\t\t\t\t$message .= '</button></div>';\n\t\t\t\t\t?>\n\t\t\t\t\t<?php llms_print_notice( $message, 'notice' ); ?>\n\t\t\t\t<?php else : ?>\n\n\t\t\t\t\t<button class=\"llms-start-quiz-button llms-button-action button\" id=\"llms_start_quiz\" name=\"llms_start_quiz\" type=\"submit\">\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t * Filters the quiz button text\n\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t * @since Unknown\n\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t * @param string      $button_text The start quiz button text.\n\t\t\t\t\t\t\t * @param LLMS_Quiz   $quiz        The current quiz instance.\n\t\t\t\t\t\t\t * @param LLMS_Lesson $lesson      The parent lesson instance.\n\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\techo wp_kses_post( apply_filters( 'lifterlms_begin_quiz_button_text', __( 'Start Quiz', 'lifterlms' ), $quiz, $lesson ) );\n\t\t\t\t\t\t?>\n\t\t\t\t\t</button>\n\t\t\t\t<?php endif; ?>\n\t\t\t</form>\n\n\t\t<?php else : ?>\n\t\t\t<p><?php esc_html_e( 'You are not able to take this quiz', 'lifterlms' ); ?></p>\n\t\t<?php endif; ?>\n\n\t\t<?php if ( $quiz->can_be_resumed_by_student() ) : ?>\n\t\t\t<form method=\"POST\" action=\"\" name=\"llms_resume_quiz\" enctype=\"multipart/form-data\">\n\n\t\t\t\t<input id=\"llms-attempt-key\" name=\"llms_attempt_key\" type=\"hidden\" value=\"<?php echo esc_attr( $quiz->get_student_last_attempt_key() ); ?>\"/>\n\n\t\t\t\t<input type=\"hidden\" name=\"action\" value=\"llms_resume_quiz\" />\n\n\t\t\t\t<?php wp_nonce_field( 'llms_resume_quiz' ); ?>\n\n\t\t\t\t<button class=\"llms-resume-quiz-button llms-button-action button\" id=\"llms_resume_quiz\" name=\"llms_resume_quiz\" type=\"submit\">\n\t\t\t\t\t<?php\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Filters the quiz resume button text.\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @since 7.8.0\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @param string      $button_text The resume quiz button text.\n\t\t\t\t\t\t * @param LLMS_Quiz   $quiz        The current quiz instance.\n\t\t\t\t\t\t * @param LLMS_Lesson $lesson      The parent lesson instance.\n\t\t\t\t\t\t */\n\t\t\t\t\t\techo esc_html( apply_filters( 'lifterlms_resume_quiz_button_text', esc_html__( 'Resume Quiz', 'lifterlms' ), $quiz, $lesson ) );\n\t\t\t\t\t?>\n\t\t\t\t</button>\n\n\t\t\t</form>\n\t\t<?php endif; ?>\n\n\t\t<?php if ( $lesson->get_next_lesson() && llms_is_complete( get_current_user_id(), $lesson->get( 'id' ), 'lesson' ) ) : ?>\n\t\t\t<a href=\"<?php echo esc_url( get_permalink( $lesson->get_next_lesson() ) ); ?>\" class=\"button llms-button-secondary llms-next-lesson\"><?php esc_html_e( 'Next Lesson', 'lifterlms' ); ?></a>\n\t\t<?php endif; ?>\n\n\t<?php else : ?>\n\n\t\t<p><?php esc_html_e( 'You are not able to take this quiz', 'lifterlms' ); ?></p>\n\n\t<?php endif; ?>\n\n\t<?php\n\t\t/**\n\t\t * Fired after the start quiz button\n\t\t *\n\t\t * @since Unknown\n\t\t */\n\t\tdo_action( 'lifterlms_after_start_quiz' );\n\t?>\n\n</div>\n"
  },
  {
    "path": "templates/shared/instructors.php",
    "content": "<?php\n/**\n * Course & Membership Instructors Block\n *\n * @package LifterLMS/Templates/Shared\n *\n * @since 4.11.0\n * @version 4.11.0\n *\n * @param LLMS_Post_Model $llms_post   Instance of the LLMS_Post_Model for the current screen.\n * @param array[]         $instructors Array of instructor data from the post's `get_instructors()` method.\n * @param int             $count       Number of instructors found in the `$instructors` array.\n */\n\ndefined( 'ABSPATH' ) || exit;\n?>\n\n<section class=\"llms-instructor-info\">\n\t<h3 class=\"llms-meta-title\">\n\t\t<?php\n\t\t/**\n\t\t * Filters the displayed title of the Instructors block\n\t\t *\n\t\t * @since 4.11.0\n\t\t *\n\t\t * @param string          $title     The block's title.\n\t\t * @param LLMS_Post_Model $llms_post The post model object.\n\t\t * @param int             $count     Number of instructors found, used to pluralize the title.\n\t\t */\n\t\techo esc_html( apply_filters(\n\t\t\t'llms_instructors_info_title',\n\t\t\t// Translators: %s = The singular name of the post type, eg: \"Course\".\n\t\t\tsprintf( _n( '%s Instructor', '%s Instructors', $count, 'lifterlms' ), $llms_post->get_post_type_label() ),\n\t\t\t$llms_post,\n\t\t\t$count\n\t\t) );\n\t\t?>\n\t</h3>\n\t<div class=\"llms-instructors llms-cols\">\n\t\t<?php foreach ( $instructors as $instructor ) : ?>\n\t\t\t<div class=\"llms-col-<?php echo esc_attr( $count <= 4 ? $count : 4 ); ?>\">\n\t\t\t\t<?php\n\t\t\t\t// HTML output is escaped in the `llms_get_author()` function.\n\t\t\t\t// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped\n\t\t\t\techo llms_get_author(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'avatar_size' => 100,\n\t\t\t\t\t\t'bio'         => true,\n\t\t\t\t\t\t'label'       => $instructor['label'],\n\t\t\t\t\t\t'user_id'     => $instructor['id'],\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t?>\n\t\t\t</div>\n\t\t<?php endforeach; ?>\n\t</div>\n</section>\n"
  },
  {
    "path": "templates/single-certificate.php",
    "content": "<?php\n/**\n * Display a single certificate.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @since 6.0.0 Use custom header and footer templates in favor of the templates provided by the current theme.\n * @version 6.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'certificates/header.php' );\n\nwhile ( have_posts() ) :\n\n\tthe_post();\n\tllms_get_template_part( 'content', 'certificate' );\n\nendwhile;\n\nllms_get_template( 'certificates/footer.php' );\n"
  },
  {
    "path": "templates/single-lesson-focus.php",
    "content": "<?php\n/**\n * The Template for displaying single lesson, quiz, or other focus-mode content (e.g. assignments via add-on).\n *\n * @package LifterLMS/Templates\n *\n * @since 10.0.0\n * @version 10.0.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n$course    = llms_get_post_parent_course( get_the_ID() );\n$course_id = $course ? $course->get( 'id' ) : 0;\n$student   = llms_get_student();\n$progress  = ( $student && $course_id ) ? $student->get_progress( $course_id, 'course' ) : 0;\n\n$post_type = get_post_type();\n$lesson    = null;\nif ( 'lesson' === $post_type ) {\n\t$lesson = llms_get_post( get_the_ID() );\n} else {\n\t$current_post = llms_get_post( get_the_ID() );\n\tif ( $current_post && is_callable( array( $current_post, 'get' ) ) ) {\n\t\t$lesson_id = $current_post->get( 'lesson_id' );\n\t\tif ( $lesson_id ) {\n\t\t\t$lesson = llms_get_post( $lesson_id );\n\t\t}\n\t}\n}\n$prev_id = ( $lesson && is_callable( array( $lesson, 'get_previous_lesson' ) ) ) ? $lesson->get_previous_lesson() : false;\n$next_id = ( $lesson && is_callable( array( $lesson, 'get_next_lesson' ) ) ) ? $lesson->get_next_lesson() : false;\n\n$prev_restricted = $prev_id ? llms_page_restricted( $prev_id, get_current_user_id() ) : array( 'is_restricted' => false );\n$next_restricted = $next_id ? llms_page_restricted( $next_id, get_current_user_id() ) : array( 'is_restricted' => false );\n?>\n<!DOCTYPE html>\n<html <?php language_attributes(); ?>>\n<head>\n\t<meta charset=\"<?php bloginfo( 'charset' ); ?>\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<link rel=\"profile\" href=\"https://gmpg.org/xfn/11\">\n\t<?php wp_head(); ?>\n</head>\n\n<body <?php body_class( 'llms-focus-mode' ); ?>>\n\n<?php wp_body_open(); ?>\n\n<div class=\"llms-focus-mode-wrapper\">\n\n\t<header class=\"llms-focus-mode-header\">\n\t\t<div class=\"llms-focus-mode-header-left\">\n\t\t\t<?php if ( 'lesson' === get_post_type() ) : ?>\n\t\t\t\t<div class=\"llms-parent-course-link\">\n\t\t\t\t\t<a class=\"llms-lesson-link\" href=\"<?php echo esc_url( get_permalink( $course_id ) ); ?>\"><?php echo esc_html__( 'Back to Course', 'lifterlms' ); ?></a>\n\t\t\t\t</div>\n\t\t\t<?php elseif ( ( $current = llms_get_post( get_the_ID() ) ) && method_exists( $current, 'get' ) && $current->get( 'lesson_id' ) ) : ?>\n\t\t\t\t<?php lifterlms_template_quiz_return_link(); ?>\n\t\t\t<?php endif; ?>\n\t\t</div>\n\t\t<div class=\"llms-focus-mode-header-center\">\n\t\t\t<?php echo lifterlms_course_progress_bar( $progress, false, false, false ); ?>\n\t\t</div>\n\t\t<div class=\"llms-focus-mode-header-nav\">\n\t\t\t<?php if ( $prev_id ) : ?>\n\t\t\t\t<?php if ( $prev_restricted['is_restricted'] ) : ?>\n\t\t\t\t\t<span class=\"llms-focus-mode-nav-btn llms-focus-mode-nav-prev llms-lesson-locked\" data-tooltip-msg=\"<?php echo esc_attr( wp_strip_all_tags( llms_get_restriction_message( $prev_restricted ) ) ); ?>\" aria-label=\"<?php esc_attr_e( 'Previous Lesson', 'lifterlms' ); ?>\">\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<a href=\"<?php echo esc_url( get_permalink( $prev_id ) ); ?>\" class=\"llms-focus-mode-nav-btn llms-focus-mode-nav-prev\" aria-label=\"<?php esc_attr_e( 'Previous Lesson', 'lifterlms' ); ?>\">\n\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<svg class=\"llms-focus-mode-nav-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 512\"><path d=\"M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z\"/></svg>\n\t\t\t\t\t<span class=\"llms-focus-mode-nav-label\"><?php esc_html_e( 'Previous Lesson', 'lifterlms' ); ?></span>\n\t\t\t\t<?php echo $prev_restricted['is_restricted'] ? '</span>' : '</a>'; ?>\n\t\t\t<?php endif; ?>\n\t\t\t<?php if ( $next_id ) : ?>\n\t\t\t\t<?php if ( $next_restricted['is_restricted'] ) : ?>\n\t\t\t\t\t<span class=\"llms-focus-mode-nav-btn llms-focus-mode-nav-next llms-lesson-locked\" data-tooltip-msg=\"<?php echo esc_attr( wp_strip_all_tags( llms_get_restriction_message( $next_restricted ) ) ); ?>\" aria-label=\"<?php esc_attr_e( 'Next Lesson', 'lifterlms' ); ?>\">\n\t\t\t\t<?php else : ?>\n\t\t\t\t\t<a href=\"<?php echo esc_url( get_permalink( $next_id ) ); ?>\" class=\"llms-focus-mode-nav-btn llms-focus-mode-nav-next\" aria-label=\"<?php esc_attr_e( 'Next Lesson', 'lifterlms' ); ?>\">\n\t\t\t\t<?php endif; ?>\n\t\t\t\t\t<span class=\"llms-focus-mode-nav-label\"><?php esc_html_e( 'Next Lesson', 'lifterlms' ); ?></span>\n\t\t\t\t\t<svg class=\"llms-focus-mode-nav-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 512\"><path d=\"M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z\"/></svg>\n\t\t\t\t<?php echo $next_restricted['is_restricted'] ? '</span>' : '</a>'; ?>\n\t\t\t<?php endif; ?>\n\t\t</div>\n\t</header>\n\n\t<div class=\"llms-focus-mode-body\">\n\n\t\t<aside class=\"llms-focus-mode-sidebar\">\n\t\t\t<div class=\"llms-focus-mode-sidebar-header\">\n\t\t\t\t<h3><?php esc_html_e( 'Lessons', 'lifterlms' ); ?></h3>\n\t\t\t</div>\n\t\t\t<div class=\"llms-focus-mode-sidebar-content\">\n\t\t\t\t<?php\n\t\t\t\tif ( $course_id ) {\n\t\t\t\t\techo do_shortcode( '[lifterlms_course_outline collapse=\"true\" toggles=\"true\" course_id=\"' . intval( $course_id ) . '\"]' );\n\t\t\t\t}\n\t\t\t\t?>\n\t\t\t</div>\n\t\t\t<button class=\"llms-focus-mode-sidebar-toggle\" type=\"button\" aria-label=\"<?php esc_attr_e( 'Toggle sidebar', 'lifterlms' ); ?>\">\n\t\t\t\t<svg class=\"llms-chevron-left\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 512\"><path d=\"M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z\"/></svg>\n\t\t\t\t<svg class=\"llms-chevron-right\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 512\"><path d=\"M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z\"/></svg>\n\t\t\t</button>\n\t\t</aside>\n\n\t\t<div class=\"llms-focus-mode-main\">\n\t\t\t<?php\n\t\t\t\t$content_classes = array( 'llms-focus-mode-content' );\n\t\t\t\t$content_classes = apply_filters( 'llms_focus_mode_content_classes', $content_classes );\n\t\t\t?>\n\t\t\t<main class=\"<?php echo esc_attr( implode( ' ', array_filter( $content_classes ) ) ); ?>\">\n\t\t\t\t<?php\n\t\t\t\twhile ( have_posts() ) :\n\t\t\t\t\tthe_post();\n\t\t\t\t\t$lesson_content_classes = array( 'llms-lesson-content', 'entry-content', 'is-layout-constrained' );\n\t\t\t\t\t$lesson_content_classes = apply_filters( 'llms_focus_mode_lesson_content_classes', $lesson_content_classes );\n\t\t\t\t\t?>\n\t\t\t\t\t<h1 class=\"llms-focus-mode-title\"><?php the_title(); ?></h1>\n\t\t\t\t\t<div class=\"<?php echo esc_attr( implode( ' ', array_filter( $lesson_content_classes ) ) ); ?>\">\n\t\t\t\t\t\t<?php\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Renders the post content in focus mode.\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @since 10.0.0\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @see llms_focus_mode_render_content() Default handler.\n\t\t\t\t\t\t */\n\t\t\t\t\t\tdo_action( 'llms_focus_mode_the_content' );\n\t\t\t\t\t\t?>\n\t\t\t\t\t</div>\n\t\t\t\t\t<?php\n\t\t\t\tendwhile;\n\t\t\t\t?>\n\t\t\t</main>\n\n\t\t\t<?php if ( 'lesson' === $post_type && $lesson ) : ?>\n\t\t\t\t<footer class=\"llms-focus-mode-footer\">\n\t\t\t\t\t<?php lifterlms_template_complete_lesson_link(); ?>\n\t\t\t\t</footer>\n\t\t\t<?php endif; ?>\n\n\t\t</div>\n\n\t</div>\n\n\t<?php if ( 'lesson' === $post_type && $lesson ) : ?>\n\t\t<footer class=\"llms-focus-mode-footer llms-focus-mode-footer--mobile\">\n\t\t\t<?php lifterlms_template_complete_lesson_link(); ?>\n\t\t</footer>\n\t<?php endif; ?>\n\n</div>\n\n<?php wp_footer(); ?>\n\n</body>\n</html>\n"
  },
  {
    "path": "templates/single-no-access.php",
    "content": "<?php\n/**\n * Template: Single post no access\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nglobal $post;\nget_header();\n\nwhile ( have_posts() ) :\n\n\tthe_post();\n\n\tllms_get_template_part( 'content', 'no-access' );\n\nendwhile;\n\nget_footer();\n"
  },
  {
    "path": "templates/taxonomy-course_cat.php",
    "content": "<?php\n/**\n * Template: Archive for the Course Category taxonomy.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'archive-course.php' );\n"
  },
  {
    "path": "templates/taxonomy-course_difficulty.php",
    "content": "<?php\n/**\n * Template: Archive for the Course Difficulty taxonomy.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'archive-course.php' );\n"
  },
  {
    "path": "templates/taxonomy-course_tag.php",
    "content": "<?php\n/**\n * Template: Archive for the Course Tags taxonomy.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'archive-course.php' );\n"
  },
  {
    "path": "templates/taxonomy-course_track.php",
    "content": "<?php\n/**\n * Template: Archive for the Course Tracks taxonomy.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'archive-course.php' );\n"
  },
  {
    "path": "templates/taxonomy-membership_cat.php",
    "content": "<?php\n/**\n * Template: Archive for the Membership Category taxonomy.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'archive-llms_membership.php' );\n"
  },
  {
    "path": "templates/taxonomy-membership_tag.php",
    "content": "<?php\n/**\n * Template: Archive for the Membership Tags taxonomy.\n *\n * @package LifterLMS/Templates\n *\n * @since Unknown\n * @version 3.35.0\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nllms_get_template( 'archive-llms_membership.php' );\n"
  },
  {
    "path": "tests/assets/example-style-1.css",
    "content": ".llms-sample-selector-for-tests-only-1 {\n  color: white;\n}\n\n"
  },
  {
    "path": "tests/assets/example-style-2.css",
    "content": ".llms-sample-selector-for-tests-only-2 {\n  color: black;\n}\n"
  },
  {
    "path": "tests/assets/example-style.css",
    "content": ".llms-sample-selector-for-tests-only {\n  color: red;\n}\n\n"
  },
  {
    "path": "tests/assets/import-error.json",
    "content": "{\"_generator\": \"LifterLMS/SingleCourseExporter\"}"
  },
  {
    "path": "tests/assets/import-fake-generator.json",
    "content": "{\"_generator\": \"LifterLMS/FakeGenerator\"}\n"
  },
  {
    "path": "tests/assets/import-with-prerequisites.json",
    "content": "{\n  \"_generator\": \"LifterLMS/BulkCourseExporter\",\n  \"_source\": \"https://llms.test\",\n  \"_version\": \"4.5.1\",\n  \"courses\": [\n    {\n      \"access_plans\": [],\n      \"audio_embed\": \"\",\n      \"author\": 1,\n      \"average_grade\": 0,\n      \"average_progress\": 0,\n      \"capacity\": 0,\n      \"capacity_message\": \"\",\n      \"categories\": [],\n      \"comment_status\": \"open\",\n      \"content\": \"\\r\\r<!-- wp:llms/course-information /-->\\n\\n<!-- wp:llms/instructors /-->\\n\\n<!-- wp:llms/pricing-table /-->\\n\\n<!-- wp:llms/course-progress /-->\\n\\t\\t\\t\\n<!-- wp:llms/course-continue-button -->\\n<div class=\\\"wp-block-llms-course-continue-button\\\" style=\\\"text-align:center\\\">[lifterlms_course_continue_button]</div>\\n<!-- /wp:llms/course-continue-button -->\\n\\n<!-- wp:llms/course-syllabus /-->\\n\\t\\t\\t\",\n      \"content_restricted_message\": \"\",\n      \"course_closed_message\": \"\",\n      \"course_opens_message\": \"\",\n      \"custom\": {\n        \"_llms_blocks_migrated\": [\n          \"yes\"\n        ]\n      },\n      \"date\": \"2020-10-14 22:40:28\",\n      \"date_gmt\": \"2020-10-14 22:40:28\",\n      \"difficulty\": \"\",\n      \"enable_capacity\": \"no\",\n      \"end_date\": \"\",\n      \"enrollment_closed_message\": \"\",\n      \"enrollment_end_date\": \"\",\n      \"enrollment_opens_message\": \"\",\n      \"enrollment_period\": \"no\",\n      \"enrollment_start_date\": \"\",\n      \"excerpt\": \"\",\n      \"featured_image\": \"\",\n      \"has_prerequisite\": \"yes\",\n      \"id\": 1104,\n      \"length\": \"\",\n      \"menu_order\": 0,\n      \"modified\": \"2020-10-14 22:40:28\",\n      \"modified_gmt\": \"2020-10-14 22:40:28\",\n      \"name\": \"course-with-prerequisites\",\n      \"password\": \"\",\n      \"permalink\": \"https://llms.test/course/course-with-prerequisites/\",\n      \"ping_status\": \"closed\",\n      \"prerequisite\": 1102,\n      \"prerequisite_track\": 0,\n      \"sales_page_content_page_id\": 0,\n      \"sales_page_content_type\": \"\",\n      \"sales_page_content_url\": \"\",\n      \"sections\": [\n        {\n          \"comment_status\": \"closed\",\n          \"content\": \"\",\n          \"custom\": [],\n          \"date\": \"2020-10-14 22:40:52\",\n          \"date_gmt\": \"2020-10-14 22:40:52\",\n          \"excerpt\": \"\",\n          \"id\": 1106,\n          \"lessons\": [\n            {\n              \"assignment\": 0,\n              \"assignment_enabled\": \"no\",\n              \"audio_embed\": \"\",\n              \"comment_status\": \"closed\",\n              \"content\": \"\",\n              \"custom\": [],\n              \"date\": \"2020-10-14 22:40:52\",\n              \"date_available\": \"\",\n              \"date_gmt\": \"2020-10-14 22:40:52\",\n              \"days_before_available\": 0,\n              \"drip_method\": \"\",\n              \"excerpt\": \"\",\n              \"featured_image\": \"\",\n              \"free_lesson\": \"no\",\n              \"has_prerequisite\": \"no\",\n              \"id\": 1107,\n              \"menu_order\": 0,\n              \"modified\": \"2020-10-14 22:40:53\",\n              \"modified_gmt\": \"2020-10-14 22:40:53\",\n              \"name\": \"new-lesson-9\",\n              \"order\": 1,\n              \"parent_course\": 1104,\n              \"parent_section\": 1106,\n              \"password\": \"\",\n              \"permalink\": \"https://llms.test/lesson/new-lesson-9/\",\n              \"ping_status\": \"closed\",\n              \"points\": 1,\n              \"prerequisite\": 0,\n              \"quiz\": 0,\n              \"quiz_enabled\": \"no\",\n              \"require_assignment_passing_grade\": \"yes\",\n              \"require_passing_grade\": \"yes\",\n              \"status\": \"publish\",\n              \"time_available\": \"\",\n              \"title\": \"New Lesson\",\n              \"type\": \"lesson\",\n              \"video_embed\": \"\"\n            },\n            {\n              \"assignment\": 0,\n              \"assignment_enabled\": \"no\",\n              \"audio_embed\": \"\",\n              \"comment_status\": \"closed\",\n              \"content\": \"\",\n              \"custom\": [],\n              \"date\": \"2020-10-14 22:41:09\",\n              \"date_available\": \"\",\n              \"date_gmt\": \"2020-10-14 22:41:09\",\n              \"days_before_available\": 0,\n              \"drip_method\": \"\",\n              \"excerpt\": \"\",\n              \"featured_image\": \"\",\n              \"free_lesson\": \"no\",\n              \"has_prerequisite\": \"yes\",\n              \"id\": 1111,\n              \"menu_order\": 0,\n              \"modified\": \"2020-10-14 22:41:10\",\n              \"modified_gmt\": \"2020-10-14 22:41:10\",\n              \"name\": \"new-lesson-11\",\n              \"order\": 2,\n              \"parent_course\": 1104,\n              \"parent_section\": 1106,\n              \"password\": \"\",\n              \"permalink\": \"https://llms.test/lesson/new-lesson-11/\",\n              \"ping_status\": \"closed\",\n              \"points\": 1,\n              \"prerequisite\": 1107,\n              \"quiz\": 0,\n              \"quiz_enabled\": \"no\",\n              \"require_assignment_passing_grade\": \"yes\",\n              \"require_passing_grade\": \"yes\",\n              \"status\": \"publish\",\n              \"time_available\": \"\",\n              \"title\": \"New Lesson\",\n              \"type\": \"lesson\",\n              \"video_embed\": \"\"\n            }\n          ],\n          \"menu_order\": 0,\n          \"modified\": \"2020-10-14 22:40:52\",\n          \"modified_gmt\": \"2020-10-14 22:40:52\",\n          \"name\": \"1106\",\n          \"order\": 1,\n          \"parent_course\": 1104,\n          \"password\": \"\",\n          \"ping_status\": \"closed\",\n          \"status\": \"publish\",\n          \"title\": \"New Section\",\n          \"type\": \"section\"\n        }\n      ],\n      \"start_date\": \"\",\n      \"status\": \"publish\",\n      \"tags\": [],\n      \"temp_calc_data\": [],\n      \"tile_featured_video\": \"no\",\n      \"time_period\": \"no\",\n      \"title\": \"Course with Prerequisites\",\n      \"tracks\": [],\n      \"type\": \"course\",\n      \"video_embed\": \"\"\n    },\n    {\n      \"access_plans\": [],\n      \"audio_embed\": \"\",\n      \"author\": 1,\n      \"average_grade\": 0,\n      \"average_progress\": 0,\n      \"capacity\": 0,\n      \"capacity_message\": \"\",\n      \"categories\": [],\n      \"comment_status\": \"open\",\n      \"content\": \"\\r\\r<!-- wp:llms/course-information /-->\\n\\n<!-- wp:llms/instructors /-->\\n\\n<!-- wp:llms/pricing-table /-->\\n\\n<!-- wp:llms/course-progress /-->\\n\\t\\t\\t\\n<!-- wp:llms/course-continue-button -->\\n<div class=\\\"wp-block-llms-course-continue-button\\\" style=\\\"text-align:center\\\">[lifterlms_course_continue_button]</div>\\n<!-- /wp:llms/course-continue-button -->\\n\\n<!-- wp:llms/course-syllabus /-->\\n\\t\\t\\t\",\n      \"content_restricted_message\": \"\",\n      \"course_closed_message\": \"\",\n      \"course_opens_message\": \"\",\n      \"custom\": {\n        \"_llms_blocks_migrated\": [\n          \"yes\"\n        ]\n      },\n      \"date\": \"2020-10-14 22:39:39\",\n      \"date_gmt\": \"2020-10-14 22:39:39\",\n      \"difficulty\": \"\",\n      \"enable_capacity\": \"no\",\n      \"end_date\": \"\",\n      \"enrollment_closed_message\": \"\",\n      \"enrollment_end_date\": \"\",\n      \"enrollment_opens_message\": \"\",\n      \"enrollment_period\": \"no\",\n      \"enrollment_start_date\": \"\",\n      \"excerpt\": \"\",\n      \"featured_image\": \"\",\n      \"has_prerequisite\": \"no\",\n      \"id\": 1102,\n      \"length\": \"\",\n      \"menu_order\": 0,\n      \"modified\": \"2020-10-14 22:39:39\",\n      \"modified_gmt\": \"2020-10-14 22:39:39\",\n      \"name\": \"a-prerequisite-course\",\n      \"password\": \"\",\n      \"permalink\": \"https://llms.test/course/a-prerequisite-course/\",\n      \"ping_status\": \"closed\",\n      \"prerequisite\": 0,\n      \"prerequisite_track\": 0,\n      \"sales_page_content_page_id\": 0,\n      \"sales_page_content_type\": \"\",\n      \"sales_page_content_url\": \"\",\n      \"sections\": [],\n      \"start_date\": \"\",\n      \"status\": \"publish\",\n      \"tags\": [],\n      \"temp_calc_data\": [],\n      \"tile_featured_video\": \"no\",\n      \"time_period\": \"no\",\n      \"title\": \"A Prerequisite Course\",\n      \"tracks\": [],\n      \"type\": \"course\",\n      \"video_embed\": \"\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/assets/import-with-quiz.json",
    "content": "{\n  \"_generator\": \"LifterLMS/SingleCourseExporter\",\n  \"_source\": \"http://localhost:8080\",\n  \"_version\": \"4.3.0\",\n  \"access_plans\": [\n    {\n      \"access_expiration\": \"lifetime\",\n      \"access_expires\": \"\",\n      \"access_length\": 0,\n      \"access_period\": \"\",\n      \"availability\": \"open\",\n      \"availability_restrictions\": [],\n      \"content\": \"\",\n      \"date\": \"2018-05-30 20:26:38\",\n      \"enroll_text\": \"\",\n      \"excerpt\": \"\",\n      \"frequency\": 0,\n      \"id\": 19164,\n      \"is_free\": \"yes\",\n      \"length\": 0,\n      \"menu_order\": 1,\n      \"modified\": \"2018-05-31 15:29:30\",\n      \"name\": \"start-today\",\n      \"on_sale\": \"no\",\n      \"period\": \"\",\n      \"price\": 0,\n      \"product_id\": 19145,\n      \"sale_end\": \"\",\n      \"sale_price\": 0,\n      \"sale_start\": \"\",\n      \"sku\": \"\",\n      \"status\": \"publish\",\n      \"title\": \"Start Today!\",\n      \"trial_length\": 0,\n      \"trial_offer\": \"no\",\n      \"trial_period\": \"\",\n      \"trial_price\": 0,\n      \"type\": \"llms_access_plan\"\n    }\n  ],\n  \"audio_embed\": \"\",\n  \"author\": 1,\n  \"average_grade\": 0,\n  \"average_progress\": 0,\n  \"capacity\": 25,\n  \"capacity_message\": \"Enrollment has closed because the maximum number of allowed students has been reached.\",\n  \"categories\": [],\n  \"comment_status\": \"open\",\n  \"content\": \"<!-- wp:paragraph -->\\n<p></p>\\n<!-- /wp:paragraph -->\\n\\n<!-- wp:llms/course-information /-->\\n\\n<!-- wp:llms/instructors /-->\\n\\n<!-- wp:llms/pricing-table /-->\\n\\n<!-- wp:llms/course-progress /-->\\n\\n<!-- wp:llms/course-continue-button -->\\n<div class=\\\"wp-block-llms-course-continue-button\\\" style=\\\"text-align:center\\\">[lifterlms_course_continue_button]</div>\\n<!-- /wp:llms/course-continue-button -->\\n\\n<!-- wp:llms/course-syllabus /-->\",\n  \"content_restricted_message\": \"You must enroll in this course to access course content.\",\n  \"course_closed_message\": \"This course closed on [lifterlms_course_info id='98' key=\\\"end_date\\\"].\",\n  \"course_opens_message\": \"This course opens on [lifterlms_course_info id=\\\"98\\\" key=\\\"start_date\\\"].\",\n  \"custom\": {\n    \"_llms_blocks_migrated\": [\n      \"yes\"\n    ],\n    \"_custom_key\": [\n      \"custom value\",\n      \"second val\"\n    ]\n  },\n  \"date\": \"2020-08-04 23:09:49\",\n  \"date_gmt\": \"2020-08-04 23:09:49\",\n  \"difficulty\": \"Hardmode\",\n  \"enable_capacity\": \"yes\",\n  \"end_date\": \"\",\n  \"enrollment_closed_message\": \"Enrollment in this course closed on [lifterlms_course_info id=\\\"98\\\" key=\\\"enrollment_end_date\\\"].\",\n  \"enrollment_end_date\": \"\",\n  \"enrollment_opens_message\": \"Enrollment in this course opens on [lifterlms_course_info id='98' key=\\\"enrollment_start_date\\\"].\",\n  \"enrollment_period\": \"no\",\n  \"enrollment_start_date\": \"\",\n  \"excerpt\": \"\",\n  \"featured_image\": \"\",\n  \"has_prerequisite\": \"no\",\n  \"id\": 98,\n  \"instructors\": [\n    {\n      \"label\": \"Author\",\n      \"visibility\": \"visible\",\n      \"id\": 1,\n      \"description\": \"\",\n      \"email\": \"admin@wpllms.test\",\n      \"first_name\": \"\",\n      \"last_name\": \"\"\n    }\n  ],\n  \"length\": \"\",\n  \"menu_order\": 0,\n  \"modified\": \"2020-08-04 23:09:49\",\n  \"modified_gmt\": \"2020-08-04 23:09:49\",\n  \"name\": \"simple-course-with-quiz\",\n  \"password\": \"\",\n  \"permalink\": \"http://localhost:8080/course/simple-course-with-quiz/\",\n  \"ping_status\": \"closed\",\n  \"prerequisite\": 0,\n  \"prerequisite_track\": 0,\n  \"sales_page_content_page_id\": 0,\n  \"sales_page_content_type\": \"none\",\n  \"sales_page_content_url\": \"\",\n  \"sections\": [\n    {\n      \"author\": {\n        \"description\": \"\",\n        \"email\": \"admin@wpllms.test\",\n        \"first_name\": \"\",\n        \"id\": 1,\n        \"last_name\": \"\"\n      },\n      \"comment_status\": \"closed\",\n      \"content\": \"\",\n      \"custom\": [],\n      \"date\": \"2020-08-04 23:10:08\",\n      \"date_gmt\": \"2020-08-04 23:10:08\",\n      \"excerpt\": \"\",\n      \"id\": 100,\n      \"lessons\": [\n        {\n          \"audio_embed\": \"\",\n          \"author\": {\n            \"description\": \"\",\n            \"email\": \"admin@wpllms.test\",\n            \"first_name\": \"\",\n            \"id\": 1,\n            \"last_name\": \"\"\n          },\n          \"comment_status\": \"closed\",\n          \"content\": \"This is the content of the lesson.\",\n          \"custom\": {\n            \"_custom_key\": [\n              \"custom value\",\n              \"second val\"\n            ]\n          },\n          \"date\": \"2020-08-04 23:10:08\",\n          \"date_available\": \"\",\n          \"date_gmt\": \"2020-08-04 23:10:08\",\n          \"days_before_available\": 0,\n          \"drip_method\": \"\",\n          \"excerpt\": \"\",\n          \"featured_image\": \"\",\n          \"free_lesson\": \"yes\",\n          \"has_prerequisite\": \"no\",\n          \"id\": 101,\n          \"menu_order\": 0,\n          \"modified\": \"2020-08-04 23:10:08\",\n          \"modified_gmt\": \"2020-08-04 23:10:08\",\n          \"name\": \"new-lesson\",\n          \"order\": 1,\n          \"parent_course\": 98,\n          \"parent_section\": 100,\n          \"password\": \"\",\n          \"permalink\": \"http://localhost:8080/lesson/new-lesson/\",\n          \"ping_status\": \"closed\",\n          \"points\": 25,\n          \"prerequisite\": 0,\n          \"quiz\": {\n            \"allowed_attempts\": 202,\n            \"author\": {\n              \"description\": \"\",\n              \"email\": \"admin@wpllms.test\",\n              \"first_name\": \"\",\n              \"id\": 1,\n              \"last_name\": \"\"\n            },\n            \"comment_status\": \"closed\",\n            \"content\": \"Take the quiz!\",\n            \"custom\": {\n              \"_custom_key\": [\n                \"custom value\",\n                \"second val\"\n              ]\n            },\n            \"date\": \"2020-08-04 23:10:23\",\n            \"date_gmt\": \"2020-08-04 23:10:23\",\n            \"excerpt\": \"\",\n            \"id\": 103,\n            \"lesson_id\": 101,\n            \"limit_attempts\": \"yes\",\n            \"limit_time\": \"no\",\n            \"menu_order\": 0,\n            \"modified\": \"2020-08-04 23:10:23\",\n            \"modified_gmt\": \"2020-08-04 23:10:23\",\n            \"name\": \"new-lesson-quiz\",\n            \"passing_percent\": 85,\n            \"can_be_resumed\": \"yes\",\n            \"password\": \"\",\n            \"permalink\": \"http://localhost:8080/quiz/new-lesson-quiz/\",\n            \"ping_status\": \"closed\",\n            \"questions\": [\n              {\n                \"choices\": [\n                  {\n                    \"id\": \"5f29eadfb0312\",\n                    \"choice\": \"Blue\",\n                    \"choice_type\": \"text\",\n                    \"correct\": false,\n                    \"marker\": \"A\",\n                    \"question_id\": \"104\",\n                    \"type\": \"choice\"\n                  },\n                  {\n                    \"id\": \"5f29eaeb91ebe\",\n                    \"choice\": \"Red\",\n                    \"choice_type\": \"text\",\n                    \"correct\": false,\n                    \"marker\": \"B\",\n                    \"question_id\": \"104\",\n                    \"type\": \"choice\"\n                  },\n                  {\n                    \"id\": \"5f29eaf54a910\",\n                    \"choice\": \"Green\",\n                    \"choice_type\": \"text\",\n                    \"correct\": true,\n                    \"marker\": \"C\",\n                    \"question_id\": \"104\",\n                    \"type\": \"choice\"\n                  }\n                ],\n                \"clarifications\": \"Enhance!\",\n                \"clarifications_enabled\": \"yes\",\n                \"comment_status\": \"closed\",\n                \"content\": \"<p>There are more colors than this, of course.</p>\\n\",\n                \"date_gmt\": \"2020-08-04 23:10:23\",\n                \"description_enabled\": \"yes\",\n                \"id\": 104,\n                \"image\": [],\n                \"menu_order\": 1,\n                \"modified_gmt\": \"2020-08-04 23:10:23\",\n                \"multi_choices\": \"no\",\n                \"name\": \"what-is-your-favorite-color\",\n                \"parent_id\": 103,\n                \"password\": \"\",\n                \"ping_status\": \"closed\",\n                \"points\": 125,\n                \"question\": \"\",\n                \"question_type\": \"choice\",\n                \"title\": \"What is your Favorite Color?\",\n                \"type\": \"llms_question\",\n                \"video_enabled\": \"no\",\n                \"video_src\": \"\"\n              }\n            ],\n            \"random_questions\": \"yes\",\n            \"show_correct_answer\": \"no\",\n            \"status\": \"publish\",\n            \"time_limit\": 300,\n            \"title\": \"New Lesson Quiz\",\n            \"type\": \"llms_quiz\",\n            \"disable_retake\": \"yes\"\n          },\n          \"quiz_enabled\": \"yes\",\n          \"require_assignment_passing_grade\": \"yes\",\n          \"require_passing_grade\": \"yes\",\n          \"status\": \"publish\",\n          \"time_available\": \"\",\n          \"title\": \"New Lesson\",\n          \"type\": \"lesson\",\n          \"video_embed\": \"\"\n        }\n      ],\n      \"menu_order\": 0,\n      \"modified\": \"2020-08-04 23:10:08\",\n      \"modified_gmt\": \"2020-08-04 23:10:08\",\n      \"name\": \"100\",\n      \"order\": 1,\n      \"parent_course\": 98,\n      \"password\": \"\",\n      \"ping_status\": \"closed\",\n      \"status\": \"publish\",\n      \"title\": \"New Section\",\n      \"type\": \"section\"\n    }\n  ],\n  \"start_date\": \"\",\n  \"status\": \"publish\",\n  \"tags\": [],\n  \"temp_calc_data\": [],\n  \"tile_featured_video\": \"no\",\n  \"time_period\": \"no\",\n  \"title\": \"Simple Course with Quiz\",\n  \"tracks\": [],\n  \"type\": \"course\",\n  \"video_embed\": \"\"\n}\n"
  },
  {
    "path": "tests/assets/import-with-restrictions.json",
    "content": "{\n  \"_generator\": \"LifterLMS/SingleCourseExporter\",\n  \"_source\": \"https://llms.test\",\n  \"_version\": \"4.3.0\",\n  \"access_plans\": [],\n  \"audio_embed\": \"\",\n  \"author\": 1,\n  \"av_prog_auto_advance\": \"global\",\n  \"av_prog_auto_play\": \"global\",\n  \"av_prog_require_completion\": \"global\",\n  \"av_vimeo_player_disable_controls\": \"global\",\n  \"av_vimeo_player_disable_speed\": \"global\",\n  \"average_grade\": 0,\n  \"average_progress\": 0,\n  \"capacity\": 0,\n  \"capacity_message\": \"Enrollment has closed because the maximum number of allowed students has been reached.\",\n  \"categories\": [],\n  \"comment_status\": \"open\",\n  \"content\": \"<!-- wp:html -->\\n<strong id=\\\"enrolled-user-content\\\">Enrolled user content.</strong>\\n<!-- /wp:html -->\\n<!-- wp:llms/course-syllabus /-->\",\n  \"content_restricted_message\": \"You must enroll in this course to access course content.\",\n  \"course_closed_message\": \"This course closed on [lifterlms_course_info id=\\\"85\\\" key=\\\"end_date\\\"].\",\n  \"course_opens_message\": \"This course opens on [lifterlms_course_info id=\\\"85\\\" key=\\\"start_date\\\"].\",\n  \"custom\": {\n    \"_llms_generated_from_id\": [\n      \"85\"\n    ],\n    \"_llms_blocks_migrated\": [\n      \"yes\"\n    ],\n    \"_llms_reviews_enabled\": [\n      \"\"\n    ],\n    \"_llms_display_reviews\": [\n      \"\"\n    ],\n    \"_llms_num_reviews\": [\n      \"\"\n    ],\n    \"_llms_multiple_reviews_disabled\": [\n      \"\"\n    ]\n  },\n  \"date\": \"2020-08-05 18:34:15\",\n  \"date_gmt\": \"2020-08-05 18:34:15\",\n  \"difficulty\": \"\",\n  \"enable_capacity\": \"no\",\n  \"end_date\": \"\",\n  \"enrollment_closed_message\": \"Enrollment in this course closed on [lifterlms_course_info id=\\\"85\\\" key=\\\"enrollment_end_date\\\"].\",\n  \"enrollment_end_date\": \"\",\n  \"enrollment_opens_message\": \"Enrollment in this course opens on [lifterlms_course_info id=\\\"85\\\" key=\\\"enrollment_start_date\\\"].\",\n  \"enrollment_period\": \"no\",\n  \"enrollment_start_date\": \"\",\n  \"excerpt\": \"<strong id=\\\"non-enrolled-user-content\\\">Non-enrolled user content.</strong>\",\n  \"featured_image\": \"\",\n  \"has_prerequisite\": \"no\",\n  \"id\": 245,\n  \"instructors\": [\n    {\n      \"label\": \"Author\",\n      \"visibility\": \"visible\",\n      \"id\": 1,\n      \"description\": \"\",\n      \"email\": \"admin@llms.test\",\n      \"first_name\": \"arst\\\" onfocus=\\\"alert(1)\\\"\",\n      \"last_name\": \"arst\"\n    }\n  ],\n  \"length\": \"\",\n  \"menu_order\": 0,\n  \"modified\": \"2020-08-05 19:40:11\",\n  \"modified_gmt\": \"2020-08-05 19:40:11\",\n  \"name\": \"restrictions-testing\",\n  \"password\": \"\",\n  \"permalink\": \"https://llms.test/course/restrictions-testing/\",\n  \"ping_status\": \"closed\",\n  \"prerequisite\": 0,\n  \"prerequisite_track\": 0,\n  \"sales_page_content_page_id\": 0,\n  \"sales_page_content_type\": \"content\",\n  \"sales_page_content_url\": \"\",\n  \"sections\": [\n    {\n      \"author\": {\n        \"description\": \"\",\n        \"email\": \"admin@wpllms.test\",\n        \"first_name\": \"\",\n        \"id\": 55,\n        \"last_name\": \"\"\n      },\n      \"comment_status\": \"closed\",\n      \"content\": \"\",\n      \"custom\": [],\n      \"date\": \"2020-08-04 23:07:01\",\n      \"date_gmt\": \"2020-08-04 23:07:01\",\n      \"excerpt\": \"\",\n      \"id\": 247,\n      \"lessons\": [\n        {\n          \"assignment\": 0,\n          \"assignment_enabled\": \"no\",\n          \"audio_embed\": \"\",\n          \"author\": {\n            \"description\": \"\",\n            \"email\": \"admin@wpllms.test\",\n            \"first_name\": \"\",\n            \"id\": 55,\n            \"last_name\": \"\"\n          },\n          \"av_prog_auto_advance\": \"course\",\n          \"av_prog_auto_play\": \"course\",\n          \"av_prog_require_completion\": \"course\",\n          \"av_vimeo_player_disable_controls\": \"course\",\n          \"av_vimeo_player_disable_speed\": \"course\",\n          \"comment_status\": \"closed\",\n          \"content\": \"\",\n          \"custom\": {\n            \"_llms_generated_from_id\": [\n              \"88\"\n            ],\n            \"_wp_old_slug\": [\n              \"new-lesson\"\n            ]\n          },\n          \"date\": \"2020-08-04 23:07:01\",\n          \"date_available\": \"\",\n          \"date_gmt\": \"2020-08-04 23:07:01\",\n          \"days_before_available\": 0,\n          \"drip_method\": \"\",\n          \"excerpt\": \"\",\n          \"featured_image\": \"\",\n          \"free_lesson\": \"no\",\n          \"has_prerequisite\": \"no\",\n          \"id\": 248,\n          \"menu_order\": 0,\n          \"modified\": \"2020-08-05 18:34:15\",\n          \"modified_gmt\": \"2020-08-05 18:34:15\",\n          \"name\": \"regular\",\n          \"order\": 1,\n          \"parent_course\": 245,\n          \"parent_section\": 247,\n          \"password\": \"\",\n          \"permalink\": \"https://llms.test/lesson/regular/\",\n          \"ping_status\": \"closed\",\n          \"points\": 1,\n          \"prerequisite\": 0,\n          \"quiz\": 0,\n          \"quiz_enabled\": \"no\",\n          \"require_assignment_passing_grade\": \"yes\",\n          \"require_passing_grade\": \"yes\",\n          \"status\": \"publish\",\n          \"time_available\": \"\",\n          \"title\": \"Regular\",\n          \"type\": \"lesson\",\n          \"video_embed\": \"\"\n        },\n        {\n          \"assignment\": 0,\n          \"assignment_enabled\": \"no\",\n          \"audio_embed\": \"\",\n          \"author\": {\n            \"description\": \"\",\n            \"email\": \"admin@wpllms.test\",\n            \"first_name\": \"\",\n            \"id\": 55,\n            \"last_name\": \"\"\n          },\n          \"av_prog_auto_advance\": \"course\",\n          \"av_prog_auto_play\": \"course\",\n          \"av_prog_require_completion\": \"course\",\n          \"av_vimeo_player_disable_controls\": \"course\",\n          \"av_vimeo_player_disable_speed\": \"course\",\n          \"comment_status\": \"closed\",\n          \"content\": \"\",\n          \"custom\": {\n            \"_llms_generated_from_id\": [\n              \"90\"\n            ],\n            \"_wp_old_slug\": [\n              \"new-lesson-2\"\n            ]\n          },\n          \"date\": \"2020-08-04 23:07:01\",\n          \"date_available\": \"\",\n          \"date_gmt\": \"2020-08-04 23:07:01\",\n          \"days_before_available\": 0,\n          \"drip_method\": \"\",\n          \"excerpt\": \"\",\n          \"featured_image\": \"\",\n          \"free_lesson\": \"no\",\n          \"has_prerequisite\": \"yes\",\n          \"id\": 250,\n          \"menu_order\": 0,\n          \"modified\": \"2020-08-05 18:34:15\",\n          \"modified_gmt\": \"2020-08-05 18:34:15\",\n          \"name\": \"has-prereq\",\n          \"order\": 2,\n          \"parent_course\": 245,\n          \"parent_section\": 247,\n          \"password\": \"\",\n          \"permalink\": \"https://llms.test/lesson/has-prereq/\",\n          \"ping_status\": \"closed\",\n          \"points\": 1,\n          \"prerequisite\": 248,\n          \"quiz\": 0,\n          \"quiz_enabled\": \"no\",\n          \"require_assignment_passing_grade\": \"yes\",\n          \"require_passing_grade\": \"yes\",\n          \"status\": \"publish\",\n          \"time_available\": \"\",\n          \"title\": \"Has Prereq\",\n          \"type\": \"lesson\",\n          \"video_embed\": \"\"\n        },\n        {\n          \"assignment\": 0,\n          \"assignment_enabled\": \"no\",\n          \"audio_embed\": \"\",\n          \"author\": {\n            \"description\": \"\",\n            \"email\": \"admin@wpllms.test\",\n            \"first_name\": \"\",\n            \"id\": 55,\n            \"last_name\": \"\"\n          },\n          \"av_prog_auto_advance\": \"course\",\n          \"av_prog_auto_play\": \"course\",\n          \"av_prog_require_completion\": \"course\",\n          \"av_vimeo_player_disable_controls\": \"course\",\n          \"av_vimeo_player_disable_speed\": \"course\",\n          \"comment_status\": \"closed\",\n          \"content\": \"\",\n          \"custom\": {\n            \"_llms_generated_from_id\": [\n              \"93\"\n            ]\n          },\n          \"date\": \"2020-08-04 23:07:48\",\n          \"date_available\": \"\",\n          \"date_gmt\": \"2020-08-04 23:07:48\",\n          \"days_before_available\": 5,\n          \"drip_method\": \"enrollment\",\n          \"excerpt\": \"\",\n          \"featured_image\": \"\",\n          \"free_lesson\": \"no\",\n          \"has_prerequisite\": \"no\",\n          \"id\": 252,\n          \"menu_order\": 0,\n          \"modified\": \"2020-08-05 18:34:15\",\n          \"modified_gmt\": \"2020-08-05 18:34:15\",\n          \"name\": \"has-drip\",\n          \"order\": 3,\n          \"parent_course\": 245,\n          \"parent_section\": 247,\n          \"password\": \"\",\n          \"permalink\": \"https://llms.test/lesson/has-drip/\",\n          \"ping_status\": \"closed\",\n          \"points\": 1,\n          \"prerequisite\": 0,\n          \"quiz\": 0,\n          \"quiz_enabled\": \"no\",\n          \"require_assignment_passing_grade\": \"yes\",\n          \"require_passing_grade\": \"yes\",\n          \"status\": \"publish\",\n          \"time_available\": \"\",\n          \"title\": \"Has Drip\",\n          \"type\": \"lesson\",\n          \"video_embed\": \"\"\n        },\n        {\n          \"assignment\": 0,\n          \"assignment_enabled\": \"no\",\n          \"audio_embed\": \"\",\n          \"author\": {\n            \"description\": \"\",\n            \"email\": \"admin@wpllms.test\",\n            \"first_name\": \"\",\n            \"id\": 55,\n            \"last_name\": \"\"\n          },\n          \"av_prog_auto_advance\": \"course\",\n          \"av_prog_auto_play\": \"course\",\n          \"av_prog_require_completion\": \"course\",\n          \"av_vimeo_player_disable_controls\": \"course\",\n          \"av_vimeo_player_disable_speed\": \"course\",\n          \"comment_status\": \"closed\",\n          \"content\": \"<!-- wp:html -->\\n<strong id=\\\"free-lesson-content\\\">Free lesson content.</strong>\\n<!-- /wp:html -->\",\n          \"custom\": {\n            \"_llms_generated_from_id\": [\n              \"95\"\n            ]\n          },\n          \"date\": \"2020-08-04 23:07:48\",\n          \"date_available\": \"\",\n          \"date_gmt\": \"2020-08-04 23:07:48\",\n          \"days_before_available\": 0,\n          \"drip_method\": \"\",\n          \"excerpt\": \"\",\n          \"featured_image\": \"\",\n          \"free_lesson\": \"yes\",\n          \"has_prerequisite\": \"no\",\n          \"id\": 254,\n          \"menu_order\": 0,\n          \"modified\": \"2020-08-05 18:34:15\",\n          \"modified_gmt\": \"2020-08-05 18:34:15\",\n          \"name\": \"is-free\",\n          \"order\": 4,\n          \"parent_course\": 245,\n          \"parent_section\": 247,\n          \"password\": \"\",\n          \"permalink\": \"https://llms.test/lesson/is-free/\",\n          \"ping_status\": \"closed\",\n          \"points\": 1,\n          \"prerequisite\": 0,\n          \"quiz\": 0,\n          \"quiz_enabled\": \"no\",\n          \"require_assignment_passing_grade\": \"yes\",\n          \"require_passing_grade\": \"yes\",\n          \"status\": \"publish\",\n          \"time_available\": \"\",\n          \"title\": \"Is Free\",\n          \"type\": \"lesson\",\n          \"video_embed\": \"\"\n        },\n        {\n          \"assignment\": 0,\n          \"assignment_enabled\": \"no\",\n          \"audio_embed\": \"\",\n          \"author\": {\n            \"description\": \"\",\n            \"email\": \"admin@llms.test\",\n            \"first_name\": \"arst\\\" onfocus=\\\"alert(1)\\\"\",\n            \"id\": 1,\n            \"last_name\": \"arst\"\n          },\n          \"av_prog_auto_advance\": \"course\",\n          \"av_prog_auto_play\": \"course\",\n          \"av_prog_require_completion\": \"course\",\n          \"av_vimeo_player_disable_controls\": \"course\",\n          \"av_vimeo_player_disable_speed\": \"course\",\n          \"comment_status\": \"closed\",\n          \"content\": \"\",\n          \"custom\": {\n            \"_wp_old_slug\": [\n              \"new-lesson-5\"\n            ]\n          },\n          \"date\": \"2020-08-05 19:30:37\",\n          \"date_available\": \"\",\n          \"date_gmt\": \"2020-08-05 19:30:37\",\n          \"days_before_available\": 0,\n          \"drip_method\": \"\",\n          \"excerpt\": \"\",\n          \"featured_image\": \"\",\n          \"free_lesson\": \"no\",\n          \"has_prerequisite\": \"no\",\n          \"id\": 311,\n          \"menu_order\": 0,\n          \"modified\": \"2020-08-05 19:32:54\",\n          \"modified_gmt\": \"2020-08-05 19:32:54\",\n          \"name\": \"has-quiz\",\n          \"order\": 5,\n          \"parent_course\": 245,\n          \"parent_section\": 247,\n          \"password\": \"\",\n          \"permalink\": \"https://llms.test/lesson/has-quiz/\",\n          \"ping_status\": \"closed\",\n          \"points\": 1,\n          \"prerequisite\": 0,\n          \"quiz\": {\n            \"allowed_attempts\": 5,\n            \"author\": {\n              \"description\": \"\",\n              \"email\": \"admin@llms.test\",\n              \"first_name\": \"arst\\\" onfocus=\\\"alert(1)\\\"\",\n              \"id\": 1,\n              \"last_name\": \"arst\"\n            },\n            \"comment_status\": \"closed\",\n            \"content\": \"\",\n            \"custom\": [],\n            \"date\": \"2020-08-05 19:30:38\",\n            \"date_gmt\": \"2020-08-05 19:30:38\",\n            \"excerpt\": \"\",\n            \"id\": 313,\n            \"lesson_id\": 311,\n            \"limit_attempts\": \"no\",\n            \"limit_time\": \"no\",\n            \"menu_order\": 0,\n            \"modified\": \"2020-08-05 19:30:38\",\n            \"modified_gmt\": \"2020-08-05 19:30:38\",\n            \"name\": \"313\",\n            \"passing_percent\": 65,\n            \"password\": \"\",\n            \"permalink\": \"https://llms.test/quiz/313/\",\n            \"ping_status\": \"closed\",\n            \"points\": 1,\n            \"questions\": [\n              {\n                \"choices\": [\n                  {\n                    \"id\": \"5f2b08de35a8e\",\n                    \"choice\": \"11\",\n                    \"choice_type\": \"text\",\n                    \"correct\": false,\n                    \"marker\": \"A\",\n                    \"question_id\": \"314\",\n                    \"type\": \"choice\"\n                  },\n                  {\n                    \"id\": \"5f2b08de398ee\",\n                    \"choice\": \"2\",\n                    \"choice_type\": \"text\",\n                    \"correct\": true,\n                    \"marker\": \"B\",\n                    \"question_id\": \"314\",\n                    \"type\": \"choice\"\n                  }\n                ],\n                \"clarifications\": \"\",\n                \"clarifications_enabled\": \"no\",\n                \"comment_status\": \"closed\",\n                \"content\": \"\",\n                \"date_gmt\": \"2020-08-05 19:30:38\",\n                \"description_enabled\": \"no\",\n                \"id\": 314,\n                \"image\": [],\n                \"menu_order\": 1,\n                \"modified_gmt\": \"2020-08-05 19:31:04\",\n                \"multi_choices\": \"no\",\n                \"name\": \"314\",\n                \"parent_id\": 313,\n                \"password\": \"\",\n                \"ping_status\": \"closed\",\n                \"points\": 1,\n                \"question\": \"\",\n                \"question_type\": \"choice\",\n                \"title\": \"1 + 1 = ?\",\n                \"type\": \"llms_question\",\n                \"video_enabled\": \"no\",\n                \"video_src\": \"\"\n              }\n            ],\n            \"random_questions\": \"no\",\n            \"show_correct_answer\": \"no\",\n            \"status\": \"publish\",\n            \"time_limit\": 30,\n            \"title\": \"New Lesson Quiz\",\n            \"type\": \"llms_quiz\"\n          },\n          \"quiz_enabled\": \"yes\",\n          \"require_assignment_passing_grade\": \"yes\",\n          \"require_passing_grade\": \"yes\",\n          \"status\": \"publish\",\n          \"time_available\": \"\",\n          \"title\": \"Has Quiz\",\n          \"type\": \"lesson\",\n          \"video_embed\": \"\"\n        }\n      ],\n      \"menu_order\": 0,\n      \"modified\": \"2020-08-04 23:07:01\",\n      \"modified_gmt\": \"2020-08-04 23:07:01\",\n      \"name\": \"new-section-2\",\n      \"order\": 1,\n      \"parent_course\": 245,\n      \"password\": \"\",\n      \"ping_status\": \"closed\",\n      \"status\": \"publish\",\n      \"title\": \"New Section\",\n      \"type\": \"section\"\n    }\n  ],\n  \"start_date\": \"\",\n  \"status\": \"publish\",\n  \"tags\": [],\n  \"temp_calc_data\": [],\n  \"tile_featured_video\": \"no\",\n  \"time_period\": \"no\",\n  \"title\": \"Restrictions Testing\",\n  \"tracks\": [],\n  \"type\": \"course\",\n  \"video_embed\": \"\"\n}\n"
  },
  {
    "path": "tests/assets/lifterlms-en_US-cd71ad734c92669051f6fd28eb90dfd4.json",
    "content": "{\n  \"translation-revision-date\": \"2020-11-20T23:21:55.271Z\",\n  \"generator\": \"llms-dev 1.0.0\\n\",\n  \"source\": \"assets/js/llms-test-messages.js\",\n  \"domain\": \"messages\",\n  \"locale_data\": {\n    \"messages\": {\n      \"\": {\n        \"domain\": \"messages\",\n        \"lang\": \"en\",\n        \"plural-forms\": \"nplurals=2; plural=(n != 1);\"\n      },\n      \"LifterLMS\": [\n        \"MyLMS\"\n      ],\n      \"Course\": [\n        \"Module\"\n      ],\n    }\n  }\n}\n"
  },
  {
    "path": "tests/assets/lifterlms-en_US.po",
    "content": "# Copyright (C) 2018 lifterlms\n# This file is distributed under the same license as the lifterlms package.\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: lifterlms\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Language-Team: LifterLMS <help@lifterlms.com>\\n\"\n\"Report-Msgid-Bugs-To: https://github.com/gocodebox/lifterlms/issues\\n\"\n\"X-Poedit-Basepath: ..\\n\"\n\"X-Poedit-KeywordsList: __;_e;_ex:1,2c;_n:1,2;_n_noop:1,2;_nx:1,2,4c;\"\n\"_nx_noop:1,2,3c;_x:1,2c;esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;\"\n\"esc_html_e;esc_html_x:1,2c\\n\"\n\"X-Poedit-SourceCharset: UTF-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"POT-Creation-Date: \\n\"\n\"PO-Revision-Date: \\n\"\n\"X-Generator: Poedit 1.8.7\\n\"\n\"Last-Translator: \\n\"\n\"Language: en_US\\n\"\n\"X-Poedit-SearchPath-0: .\\n\"\n\"X-Poedit-SearchPathExcluded-0: *.js\\n\"\n\n#: lifterlms.php:497, includes/admin/class.llms.admin.menus.php:131\nmsgid \"Settings\"\nmsgstr \"Options\"\n\n#: includes/class.llms.ajax.handler.php:22,\n#: includes/class.llms.ajax.handler.php:41,\n#: includes/class.llms.ajax.handler.php:778\nmsgid \"Missing required parameters\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:28\nmsgid \"Members are being enrolled in the background. You may leave this page.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:69\nmsgid \"There was a problem deleting your access plan, please try again.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:97\nmsgid \"The export is being generated and will be emailed to %s when complete.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:155\nmsgid \"Missing required parameters\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:260\nmsgid \"Missing required parameters.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:266\nmsgid \"There was an error removing the course, please try again.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:491,\n#: includes/class.llms.ajax.handler.php:549,\n#: includes/class.llms.ajax.handler.php:615,\n#: includes/class.llms.template.loader.php:345\nmsgid \"You must be logged in to take quizzes.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:503\nmsgid \"\"\n\"There was an error starting the quiz. Please return to the lesson and begin \"\n\"again.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:513\nmsgid \"Unable to start quiz because the quiz does not contain any questions.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:556,\n#: includes/class.llms.ajax.handler.php:620\nmsgid \"Missing required parameters. Could not proceed.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:568\nmsgid \"\"\n\"There was an error recording your answer the quiz. Please return to the \"\n\"lesson and begin again.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:738,\n#: includes/llms.functions.core.php:841,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:48,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:54\nmsgid \"ID#\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:782\nmsgid \"Invalid status\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:806\nmsgid \"Please enter a coupon code.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:811\nmsgid \"Please enter a plan ID.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.handler.php:820,\n#: includes/controllers/class.llms.controller.orders.php:211\nmsgid \"Coupon code \\\"%s\\\" not found.\"\nmsgstr \"\"\n\n#: includes/class.llms.ajax.php:630, includes/class.llms.ajax.php:759,\n#: includes/class.llms.lesson.handler.php:31,\n#: includes/class.llms.post.handler.php:195\nmsgid \"unassigned\"\nmsgstr \"\"\n\n#: includes/class.llms.certificates.php:85\nmsgid \"Unable to open export file (HTML certificate) for writing.\"\nmsgstr \"\"\n\n#: includes/class.llms.certificates.php:89\nmsgid \"Unable to write to export file (HTML certificate).\"\nmsgstr \"\"\n\n#. translators: %1$s = url-safe certificate title, %2$s = random alpha-numeric characters for filename obscurity\n#: includes/class.llms.certificates.php:118\nmsgctxt \"certificate download filename\"\nmsgid \"certificate-%1$s-%2$s\"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:158\nmsgctxt \"Localized Order DateTime\"\nmsgid \"%1$b %2$d, %3$Y @ %4$I:%5$M %6$p\"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:178, includes/llms.functions.core.php:260\nmsgid \"hours\"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:180, includes/llms.functions.core.php:252\nmsgid \"hour\"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:183\nmsgid \"%1$d %2$s \"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:187, includes/llms.functions.core.php:262\nmsgid \"seconds\"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:189, includes/llms.functions.core.php:254\nmsgid \"second\"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:200, includes/llms.functions.core.php:261\nmsgid \"minutes\"\nmsgstr \"\"\n\n#: includes/class.llms.date.php:202, includes/llms.functions.core.php:253\nmsgid \"minute\"\nmsgstr \"\"\n\n#: includes/class.llms.gateway.manual.php:20\nmsgid \"\"\n\"Collect manual or offline payments. Also handles any free orders during \"\n\"checkout.\"\nmsgstr \"\"\n\n#: includes/class.llms.gateway.manual.php:21,\n#: includes/class.llms.gateway.manual.php:22\nmsgid \"Manual\"\nmsgstr \"\"\n\n#: includes/class.llms.gateway.manual.php:23\nmsgid \"Pay manually via check\"\nmsgstr \"\"\n\n#: includes/class.llms.gateway.manual.php:71,\n#: includes/class.llms.gateway.manual.php:95\nmsgid \"Payment Instructions\"\nmsgstr \"\"\n\n#: includes/class.llms.gateway.manual.php:94\nmsgid \"\"\n\"Displayed to the user when this gateway is selected during checkout. Add \"\n\"information here instructing the student on how to send payment.\"\nmsgstr \"\"\n\n#: includes/class.llms.gateway.manual.php:128\nmsgid \"Payment method switched from \\\"%1$s\\\" to \\\"%2$s\\\"\"\nmsgstr \"\"\n\n#: includes/class.llms.gateway.manual.php:162,\n#: includes/functions/llms.functions.updates.php:214\nmsgid \"Free\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:192\nmsgid \"No generator supplied.\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:208, includes/class.llms.l10n.js.php:196,\n#: includes/class.llms.l10n.js.php:305,\n#: includes/admin/post-types/class.llms.post.tables.php:44\nmsgid \"Clone\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:250\nmsgid \"Missing required \\\"courses\\\" array\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:252\nmsgid \"\\\"courses\\\" must be an array\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:341\nmsgid \"Error creating course\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:427\nmsgid \"Error creating lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:501\nmsgid \"Error creating quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:547\nmsgid \"Error creating question\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:599\nmsgid \"Error creating section\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:844\nmsgid \"Error creating new term \\\"%s\\\"\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:1029\nmsgid \"The supplied file cannot be processed by the importer.\"\nmsgstr \"\"\n\n#: includes/class.llms.generator.php:1042\nmsgid \"Invalid generator supplied\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:224,\n#: includes/admin/class.llms.admin.setup.wizard.php:322,\n#: includes/admin/settings/class.llms.settings.courses.php:84\nmsgid \"Course Catalog\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:230,\n#: includes/admin/class.llms.admin.setup.wizard.php:326\nmsgid \"Membership Catalog\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:236\nmsgid \"Purchase\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:242,\n#: includes/class.llms.student.dashboard.php:135,\n#: includes/functions/llms.functions.templates.dashboard.php:257\nmsgid \"My Courses\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:343\nmsgctxt \"course difficulty name\"\nmsgid \"Beginner\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:344\nmsgctxt \"course difficulty name\"\nmsgid \"Intermediate\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:345\nmsgctxt \"course difficulty name\"\nmsgid \"Advanced\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:553, includes/class.llms.install.php:573,\n#: includes/admin/class.llms.admin.notices.core.php:67,\n#: includes/admin/class.llms.admin.notices.php:195,\n#: includes/admin/class.llms.admin.page.status.php:21,\n#: includes/admin/class.llms.admin.page.status.php:184\nmsgid \"Action failed. Please refresh the page and retry.\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:557, includes/class.llms.install.php:577,\n#: includes/admin/class.llms.admin.notices.core.php:70,\n#: includes/admin/class.llms.admin.notices.php:198\nmsgid \"Cheatin&#8217; huh?\"\nmsgstr \"\"\n\n#: includes/class.llms.install.php:629\nmsgid \"The LifterLMS database update is complete.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:41, includes/class.llms.l10n.js.php:378\nmsgid \"This is a %2$s %1$s String\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:48, includes/class.llms.l10n.js.php:379,\n#: includes/functions/llms.functions.access.php:177\nmsgid \"You do not have permission to access this content\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:55, includes/class.llms.l10n.js.php:380\nmsgid \"There is an issue with your chosen password.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:56, includes/class.llms.l10n.js.php:381\nmsgid \"Too Short\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:57, includes/class.llms.l10n.js.php:382\nmsgid \"Very Weak\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:58, includes/class.llms.l10n.js.php:383\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:59, includes/class.llms.l10n.js.php:384\nmsgid \"Medium\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:60, includes/class.llms.l10n.js.php:385\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:61, includes/class.llms.l10n.js.php:386\nmsgid \"Mismatch\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:68, includes/class.llms.l10n.js.php:387\nmsgid \"Members Only Pricing\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:75, includes/class.llms.l10n.js.php:388\nmsgid \"Are you sure you want to cancel your subscription?\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:82, includes/class.llms.l10n.js.php:287,\n#: includes/class.llms.post-types.php:316,\n#: includes/admin/class.llms.admin.builder.php:773,\n#: includes/models/model.llms.lesson.php:347,\n#: includes/admin/views/builder/elements.php:21\nmsgid \"New Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:83, includes/class.llms.l10n.js.php:288\nmsgid \"lessons\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:84, includes/class.llms.l10n.js.php:289\nmsgid \"lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:85, includes/class.llms.l10n.js.php:290,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:140\nmsgid \"Section %1$d: %2$s\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:86, includes/class.llms.l10n.js.php:291\nmsgid \"Lesson %1$d: %2$s\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:87, includes/class.llms.l10n.js.php:292\nmsgid \"%1$s Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:94, includes/class.llms.l10n.js.php:269,\n#: includes/class.llms.post-types.php:353\nmsgid \"New Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:95, includes/class.llms.l10n.js.php:270\nmsgid \"quizzes\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:96, includes/class.llms.l10n.js.php:271,\n#: includes/models/model.llms.user.postmeta.php:82\nmsgid \"quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:103, includes/class.llms.l10n.js.php:293,\n#: includes/class.llms.post-types.php:284\nmsgid \"New Section\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:104, includes/class.llms.l10n.js.php:294\nmsgid \"sections\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:105, includes/class.llms.l10n.js.php:295\nmsgid \"section\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:112, includes/class.llms.l10n.js.php:134,\n#: includes/class.llms.l10n.js.php:257,\n#: includes/admin/settings/class.llms.settings.general.php:109\nmsgid \"General Settings\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:113, includes/class.llms.l10n.js.php:272\nmsgid \"Video Embed URL\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:114, includes/class.llms.l10n.js.php:273\nmsgid \"Audio Embed URL\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:115, includes/class.llms.l10n.js.php:274,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:82,\n#: includes/admin/views/builder/lesson.php:127\nmsgid \"Free Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:116, includes/class.llms.l10n.js.php:275\nmsgid \"Require Passing Grade on Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:117, includes/class.llms.l10n.js.php:276\nmsgid \"Require Passing Grade on Assignment\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:118, includes/class.llms.l10n.js.php:277,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:42\nmsgid \"Prerequisite\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:119, includes/class.llms.l10n.js.php:278\nmsgid \"Drip Method\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:120, includes/class.llms.l10n.js.php:279,\n#: includes/admin/class.llms.admin.settings.php:631\nmsgid \"None\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:121, includes/class.llms.l10n.js.php:280,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:44\nmsgid \"On a specific date\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:122, includes/class.llms.l10n.js.php:281\nmsgid \"# of days after course enrollment\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:123, includes/class.llms.l10n.js.php:282\nmsgid \"# of days after course start date\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:124, includes/class.llms.l10n.js.php:283\nmsgid \"# of days after prerequisite lesson completion\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:125, includes/class.llms.l10n.js.php:284\nmsgid \"# of days\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:126, includes/class.llms.l10n.js.php:250,\n#: includes/class.llms.l10n.js.php:285, templates/myaccount/my-orders.php:20,\n#: templates/myaccount/my-orders.php:33,\n#: templates/myaccount/view-order-transactions.php:18,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:39,\n#: templates/admin/post-types/order-transactions.php:19,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:44,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:47\nmsgid \"Date\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:127, includes/class.llms.l10n.js.php:286\nmsgid \"Time\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:135, includes/class.llms.l10n.js.php:258,\n#: includes/abstracts/abstract.llms.payment.gateway.php:278,\n#: includes/privacy/class-llms-privacy-exporters.php:102,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:200,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:204,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:35,\n#: includes/admin/views/builder/question.php:58\nmsgid \"Description\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:136, includes/class.llms.l10n.js.php:259\nmsgid \"Passing Percentage\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:137, includes/class.llms.l10n.js.php:260\nmsgid \"Minimum percentage of total points required to pass the quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:138, includes/class.llms.l10n.js.php:261\nmsgid \"Limit Attempts\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:139, includes/class.llms.l10n.js.php:262\nmsgid \"Limit the maximum number of times a student can take this quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:140, includes/class.llms.l10n.js.php:263\nmsgid \"Time Limit\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:141, includes/class.llms.l10n.js.php:264\nmsgid \"Enforce a maximum number of minutes a student can spend on each attempt\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:142, includes/class.llms.l10n.js.php:265\nmsgid \"Show Correct Answers\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:143, includes/class.llms.l10n.js.php:266\nmsgid \"\"\n\"When enabled, students will be shown the correct answer to any question they \"\n\"answered incorrectly.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:144, includes/class.llms.l10n.js.php:267\nmsgid \"Randomize Question Order\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:145, includes/class.llms.l10n.js.php:268\nmsgid \"\"\n\"Display questions in a random order for each attempt. Content questions are \"\n\"locked into their defined positions.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:152, includes/class.llms.l10n.js.php:296\nmsgid \"Are you sure you want to detach this %s?\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:159, includes/class.llms.l10n.js.php:297\nmsgid \"Select an image\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:160, includes/class.llms.l10n.js.php:298\nmsgid \"Use this image\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:167, includes/class.llms.l10n.js.php:299\nmsgid \"Are you sure you want to move this %s to the trash?\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:174, includes/class.llms.l10n.js.php:312\nmsgid \"%1$s Assignment\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:175, includes/class.llms.l10n.js.php:313,\n#: includes/admin/views/builder/assignment.php:25\nmsgid \"Add Existing Assignment\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:176, includes/class.llms.l10n.js.php:314\nmsgid \"Search for existing assignments...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:177, includes/class.llms.l10n.js.php:315\nmsgid \"Get Your Students Taking Action\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:178, includes/class.llms.l10n.js.php:316\nmsgid \"Get Assignments Now!\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:179, includes/class.llms.l10n.js.php:317\nmsgid \"Unlock LifterLMS Assignments\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:186, includes/class.llms.l10n.js.php:318\nmsgid \"Add Existing Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:187, includes/class.llms.l10n.js.php:319\nmsgid \"Search for existing lessons...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:194, includes/class.llms.l10n.js.php:303\nmsgid \"Searching...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:195, includes/class.llms.l10n.js.php:304\nmsgid \"Attach\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:197, includes/class.llms.l10n.js.php:306,\n#: templates/admin/post-types/order-transactions.php:17,\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:347,\n#: includes/admin/reporting/tables/llms.table.achievements.php:151,\n#: includes/admin/reporting/tables/llms.table.certificates.php:155,\n#: includes/admin/reporting/tables/llms.table.course.students.php:368,\n#: includes/admin/reporting/tables/llms.table.courses.php:271,\n#: includes/admin/reporting/tables/llms.table.questions.php:89,\n#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:247,\n#: includes/admin/reporting/tables/llms.table.quizzes.php:295,\n#: includes/admin/reporting/tables/llms.table.student.course.php:182,\n#: includes/admin/reporting/tables/llms.table.student.courses.php:196,\n#: includes/admin/reporting/tables/llms.table.student.memberships.php:104,\n#: includes/admin/reporting/tables/llms.table.students.php:395\nmsgid \"ID\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:204, includes/class.llms.l10n.js.php:307\nmsgid \"Are you sure you want to delete this question?\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:211, includes/class.llms.l10n.js.php:308\nmsgid \"\"\n\"An error occurred while trying to load the questions. Please refresh the \"\n\"page and try again.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:212, includes/class.llms.l10n.js.php:309,\n#: includes/admin/views/builder/quiz.php:24\nmsgid \"Add Existing Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:213, includes/class.llms.l10n.js.php:310\nmsgid \"Search for existing quizzes...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:214, includes/class.llms.l10n.js.php:311\nmsgid \"Add a Question\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:221, includes/class.llms.l10n.js.php:300\nmsgid \"Use SoundCloud or Spotify audio URLS.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:222, includes/class.llms.l10n.js.php:301\nmsgid \"Permalink\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:223, includes/class.llms.l10n.js.php:302,\n#: includes/admin/views/builder/question.php:112\nmsgid \"Use YouTube, Vimeo, or Wistia video URLS.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:230\nmsgid \"Select an Image\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:231\nmsgid \"Select Image\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:238\nmsgid \"Select a Course/Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:239\nmsgid \"Select a student\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:246\nmsgid \"Filter by Student(s)\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:247\nmsgid \"Error\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:248\nmsgid \"Request timed out\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:249\nmsgid \"Retry\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:326\nmsgid \"There was an error loading the necessary resources. Please try again.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:333\nmsgid \"Please select a student to enroll\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:340, includes/class.llms.l10n.js.php:395\nmsgid \"Are you sure you want to delete this row? This cannot be undone.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:341, includes/class.llms.l10n.js.php:402\nmsgid \"\"\n\"Click okay to enroll all active members into the selected course. Enrollment \"\n\"will take place in the background and you may leave your site after \"\n\"confirmation. This action cannot be undone!\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:342, includes/class.llms.l10n.js.php:403,\n#: includes/class.llms.person.handler.php:379,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:62\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:343, includes/class.llms.l10n.js.php:404,\n#: templates/admin/post-types/order-transactions.php:76\nmsgid \"Refund\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:344, includes/class.llms.l10n.js.php:405,\n#: templates/admin/post-types/order-transactions.php:100\nmsgid \"Record a Manual Payment\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:345, includes/class.llms.l10n.js.php:406\nmsgid \"Copy this code and paste it into the desired area\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:346, includes/class.llms.l10n.js.php:407,\n#: templates/myaccount/my-orders.php:49,\n#: includes/admin/reporting/tables/llms.table.certificates.php:35\nmsgid \"View\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:353\nmsgid \"Remarks to Student\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:354,\n#: includes/admin/views/builder/question.php:43\nmsgid \"points\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:361\nmsgid \"Are you sure you wish to quit this quiz attempt?\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:362\nmsgid \"Grading Quiz...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:363\nmsgid \"Loading Question...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:364\nmsgid \"An unknown error occurred. Please try again.\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:365\nmsgid \"Loading Quiz...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:366\nmsgid \"Time Remaining\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:367\nmsgid \"Next Question\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:368\nmsgid \"Complete Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:369\nmsgid \"Previous Question\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:370\nmsgid \"Loading...\"\nmsgstr \"\"\n\n#: includes/class.llms.l10n.js.php:371\nmsgid \"You must select an answer to continue.\"\nmsgstr \"\"\n\n#: includes/class.llms.nav.menus.php:41,\n#: includes/privacy/class-llms-privacy.php:19\nmsgid \"LifterLMS\"\nmsgstr \"MyLMS\"\n\n#: includes/class.llms.nav.menus.php:68\nmsgid \"Custom Link\"\nmsgstr \"\"\n\n#: includes/class.llms.nav.menus.php:87\nmsgctxt \"customizer menu section title\"\nmsgid \"LifterLMS\"\nmsgstr \"\"\n\n#: includes/class.llms.nav.menus.php:158, includes/class.llms.nav.menus.php:159\nmsgid \"Sign In\"\nmsgstr \"\"\n\n#: includes/class.llms.nav.menus.php:163,\n#: includes/class.llms.nav.menus.php:164,\n#: includes/class.llms.student.dashboard.php:181\nmsgid \"Sign Out\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:84\nmsgid \"Username\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:98,\n#: includes/class.llms.person.handler.php:265,\n#: includes/class.llms.person.handler.php:419\nmsgid \"Email Address\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:108\nmsgid \"Confirm Email Address\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:127,\n#: includes/admin/reporting/tables/llms.table.course.students.php:382,\n#: includes/admin/reporting/tables/llms.table.students.php:414\nmsgid \"First Name\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:135,\n#: includes/admin/reporting/tables/llms.table.course.students.php:377,\n#: includes/admin/reporting/tables/llms.table.students.php:409\nmsgid \"Last Name\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:148\nmsgid \"Street Address\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:158\nmsgid \"Apartment, suite, or unit\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:165\nmsgid \"City\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:173\nmsgid \"State\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:181\nmsgid \"Zip Code\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:190,\n#: includes/admin/settings/class.llms.settings.checkout.php:230\nmsgid \"Country\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:203,\n#: includes/admin/settings/class.llms.settings.accounts.php:355,\n#: includes/admin/settings/class.llms.settings.accounts.php:400,\n#: includes/admin/settings/class.llms.settings.accounts.php:449\nmsgid \"Phone Number\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:205\nmsgctxt \"Phone Number Placeholder\"\nmsgid \"(123) 456 - 7890\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:215\nmsgid \"Have a voucher?\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:226,\n#: templates/myaccount/form-redeem-voucher.php:18,\n#: templates/myaccount/form-redeem-voucher.php:19\nmsgid \"Voucher Code\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:265,\n#: includes/class.llms.person.handler.php:419\nmsgid \"Username or Email Address\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:273,\n#: includes/class.llms.person.handler.php:336\nmsgid \"Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:282,\n#: templates/global/form-login.php:34\nmsgid \"Login\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:290\nmsgid \"Remember me\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:299\nmsgid \"Lost your password?\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:324\nmsgid \"Current Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:336\nmsgid \"New Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:347\nmsgid \"Confirm New Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:347\nmsgid \"Confirm Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:358\nmsgid \"A %s password is required.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:360\nmsgid \"A minimum password strength of %s is required.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:366\nmsgid \"\"\n\"The password must be at least 6 characters in length. Consider adding \"\n\"letters, numbers, and symbols to increase the password strength.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:379\nmsgid \"Change Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:403\nmsgid \"\"\n\"Lost your password? Enter your email address and we will send you a link to \"\n\"reset it.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:405\nmsgid \"\"\n\"Lost your password? Enter your username or email address and we will send \"\n\"you a link to reset it.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:428,\n#: templates/emails/reset-password.php:14\nmsgid \"Reset Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:443\nmsgid \"Update Password\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:649\nmsgid \"\"\n\"Could not find an account with the supplied email address and password \"\n\"combination.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:820\nmsgid \"No user ID specified.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:919\nmsgid \"%s is a required field\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:938\nmsgid \"An account with the email address \\\"%s\\\" already exists.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:948\nmsgid \"The username \\\"%s\\\" is invalid, please try a different username.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:952\nmsgid \"An account with the username \\\"%s\\\" already exists.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:965\nmsgid \"The submitted %s was incorrect.\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:978\nmsgid \"\\\"%1$s\\\" is an invalid option for %2$s\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:990\nmsgid \"%s must be numeric\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:998\nmsgid \"%s must be a valid email address\"\nmsgstr \"\"\n\n#: includes/class.llms.person.handler.php:1018\nmsgid \"%1$s must match %2$s\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:83\nmsgctxt \"Order status\"\nmsgid \"Completed\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:84\nmsgid \"Completed <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Completed <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:89\nmsgctxt \"Order status\"\nmsgid \"Active\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:90\nmsgid \"Active <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Active <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:93\nmsgctxt \"Order status\"\nmsgid \"Expired\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:94\nmsgid \"Expired <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Expired <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:97\nmsgctxt \"Order status\"\nmsgid \"On Hold\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:98\nmsgid \"On Hold <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"On Hold <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:101\nmsgctxt \"Order status\"\nmsgid \"Pending Cancellation\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:102\nmsgid \"Pending Cancellation <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Pending Cancellation <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:107\nmsgctxt \"Order status\"\nmsgid \"Pending Payment\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:108\nmsgid \"Pending Payment <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Pending Payment <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:111\nmsgctxt \"Order status\"\nmsgid \"Cancelled\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:112\nmsgid \"Cancelled <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Cancelled <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:115\nmsgctxt \"Order status\"\nmsgid \"Refunded\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:116,\n#: includes/class.llms.post-types.php:947\nmsgid \"Refunded <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Refunded <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:119\nmsgctxt \"Order status\"\nmsgid \"Failed\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:120,\n#: includes/class.llms.post-types.php:931\nmsgid \"Failed <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Failed <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:239,\n#: includes/admin/class.llms.admin.builder.php:38,\n#: includes/admin/class.llms.admin.import.php:74,\n#: includes/integrations/class.llms.integration.buddypress.php:52,\n#: includes/integrations/class.llms.integration.buddypress.php:65,\n#: includes/admin/analytics/class.llms.analytics.courses.php:20,\n#: includes/admin/analytics/class.llms.analytics.sales.php:103,\n#: includes/admin/reporting/class.llms.admin.reporting.php:250,\n#: includes/admin/settings/class.llms.settings.courses.php:19,\n#: templates/admin/analytics/analytics.php:90,\n#: templates/admin/reporting/nav-filters.php:72,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:152,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:51,\n#: includes/admin/reporting/tables/llms.table.courses.php:157,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:83,\n#: templates/admin/reporting/tabs/courses/course.php:12\nmsgid \"Courses\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:240,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:40,\n#: includes/admin/reporting/tables/llms.table.quizzes.php:305,\n#: includes/admin/views/builder/editor.php:15\nmsgid \"Course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:241\nmsgctxt \"Admin menu name\"\nmsgid \"Courses\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:242\nmsgid \"Add Course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:243\nmsgid \"Add New Course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:244,\n#: includes/class.llms.post-types.php:282,\n#: includes/class.llms.post-types.php:314,\n#: includes/class.llms.post-types.php:351,\n#: includes/class.llms.post-types.php:388,\n#: includes/class.llms.post-types.php:427,\n#: includes/class.llms.post-types.php:470,\n#: includes/class.llms.post-types.php:511,\n#: includes/class.llms.post-types.php:554,\n#: includes/class.llms.post-types.php:596,\n#: includes/class.llms.post-types.php:635,\n#: includes/class.llms.post-types.php:677,\n#: includes/class.llms.post-types.php:719,\n#: includes/class.llms.post-types.php:758,\n#: includes/class.llms.post-types.php:797,\n#: includes/class.llms.post-types.php:837,\n#: includes/class.llms.post-types.php:875,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:51\nmsgid \"Edit\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:245\nmsgid \"Edit Course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:246\nmsgid \"New Course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:247,\n#: includes/class.llms.post-types.php:248, templates/loop/view-link.php:20\nmsgid \"View Course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:249\nmsgid \"Search Courses\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:250\nmsgid \"No Courses found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:251\nmsgid \"No Courses found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:252\nmsgid \"Parent Course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:254\nmsgid \"This is where you can add new courses.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:264\nmsgctxt \"course url slug\"\nmsgid \"course\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:278,\n#: includes/admin/class.llms.admin.import.php:78\nmsgid \"Sections\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:279,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:41,\n#: includes/admin/views/builder/elements.php:15\nmsgid \"Section\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:280\nmsgid \"Add Section\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:281\nmsgid \"Add New Section\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:283\nmsgid \"Edit Section\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:285,\n#: includes/class.llms.post-types.php:286\nmsgid \"View Section\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:287\nmsgid \"Search Sections\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:288\nmsgid \"No Sections found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:289\nmsgid \"No Sections found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:290\nmsgid \"Parent Sections\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:291\nmsgctxt \"Admin menu name\"\nmsgid \"Sections\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:293\nmsgid \"This is where sections are stored.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:310,\n#: includes/admin/class.llms.admin.import.php:82\nmsgid \"Lessons\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:311,\n#: includes/admin/reporting/tables/llms.table.quizzes.php:310,\n#: includes/admin/views/builder/editor.php:19\nmsgid \"Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:312\nmsgid \"Add Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:313\nmsgid \"Add New Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:315\nmsgid \"Edit Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:317,\n#: includes/class.llms.post-types.php:318\nmsgid \"View Lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:319\nmsgid \"Search Lessons\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:320\nmsgid \"No Lessons found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:321\nmsgid \"No Lessons found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:322\nmsgid \"Parent Lessons\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:323\nmsgctxt \"Admin menu name\"\nmsgid \"Lessons\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:325\nmsgid \"This is where you can view all of the lessons.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:335\nmsgctxt \"lesson url slug\"\nmsgid \"lesson\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:347,\n#: includes/admin/class.llms.admin.import.php:90,\n#: includes/admin/reporting/class.llms.admin.reporting.php:251,\n#: includes/admin/reporting/tables/llms.table.quizzes.php:189,\n#: templates/admin/reporting/tabs/quizzes/quiz.php:13\nmsgid \"Quizzes\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:348,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:159,\n#: includes/admin/reporting/tables/llms.table.student.course.php:188,\n#: includes/admin/views/builder/editor.php:27\nmsgid \"Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:349\nmsgid \"Add Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:350\nmsgid \"Add New Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:352\nmsgid \"Edit Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:354,\n#: includes/class.llms.post-types.php:355\nmsgid \"View Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:356\nmsgid \"Search Quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:357\nmsgid \"No Quizzes found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:358\nmsgid \"No Quizzes found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:359\nmsgid \"Parent Quizzes\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:360\nmsgctxt \"Admin menu name\"\nmsgid \"Quizzes\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:362\nmsgid \"This is where you can view all of the quizzes.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:372\nmsgctxt \"quiz url slug\"\nmsgid \"quiz\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:384,\n#: includes/admin/class.llms.admin.import.php:94\nmsgid \"Questions\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:385,\n#: includes/class.llms.question.types.php:52,\n#: includes/admin/reporting/tables/llms.table.questions.php:91\nmsgid \"Question\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:386,\n#: includes/admin/views/builder/quiz.php:87\nmsgid \"Add Question\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:387\nmsgid \"Add New Question\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:389\nmsgid \"Edit Question\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:390\nmsgid \"New Question\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:391,\n#: includes/class.llms.post-types.php:392\nmsgid \"View Question\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:393\nmsgid \"Search Questions\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:394\nmsgid \"No Questions found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:395\nmsgid \"No Questions found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:396\nmsgid \"Parent Questions\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:397\nmsgctxt \"Admin menu name\"\nmsgid \"Quiz Questions\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:399\nmsgid \"This is where you can view all of the Quiz Questions.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:409\nmsgctxt \"quiz question url slug\"\nmsgid \"llms_question\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:422,\n#: includes/class.llms.post-types.php:423,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:165,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:63\nmsgid \"Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:424\nmsgctxt \"Admin menu name\"\nmsgid \"Memberships\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:425\nmsgid \"Add Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:426\nmsgid \"Add New Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:428\nmsgid \"Edit Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:429\nmsgid \"New Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:430,\n#: includes/class.llms.post-types.php:431\nmsgid \"View Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:432\nmsgid \"Search Memberships\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:433\nmsgid \"No Memberships found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:434\nmsgid \"No Memberships found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:435\nmsgid \"Parent Membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:437\nmsgid \"This is where you can add new Membership levels.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:448\nmsgctxt \"slug\"\nmsgid \"membership\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:466,\n#: includes/admin/settings/class.llms.settings.engagements.php:20\nmsgid \"Engagements\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:467\nmsgid \"Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:468\nmsgid \"Add Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:469\nmsgid \"Add New Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:471\nmsgid \"Edit Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:472\nmsgid \"New Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:473,\n#: includes/class.llms.post-types.php:474\nmsgid \"View Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:475\nmsgid \"Search Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:476\nmsgid \"No Engagement found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:477\nmsgid \"No Engagement found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:478\nmsgid \"Parent Engagement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:479\nmsgctxt \"Admin menu name\"\nmsgid \"Engagements\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:481\nmsgid \"This is where engagements are stored.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:507,\n#: includes/privacy/class-llms-privacy-exporters.php:477\nmsgid \"Orders\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:508,\n#: templates/myaccount/my-orders.php:19, templates/myaccount/my-orders.php:29,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:41\nmsgid \"Order\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:509\nmsgid \"Add Order\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:510\nmsgid \"Add New Order\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:512\nmsgid \"Edit Order\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:513\nmsgid \"New Order\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:514,\n#: includes/class.llms.post-types.php:515\nmsgid \"View Order\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:516\nmsgid \"Search Orders\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:517\nmsgid \"No Orders found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:518\nmsgid \"No Orders found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:519\nmsgid \"Parent Orders\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:520,\n#: includes/class.llms.post-types.php:563\nmsgctxt \"Admin menu name\"\nmsgid \"Orders\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:522\nmsgid \"This is where orders are managed\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:550,\n#: includes/privacy/class-llms-privacy-exporters.php:248,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:22\nmsgid \"Transactions\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:551,\n#: templates/myaccount/view-order-transactions.php:17\nmsgid \"Transaction\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:552\nmsgid \"Add Transaction\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:553\nmsgid \"Add New Transaction\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:555\nmsgid \"Edit Transaction\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:556\nmsgid \"New Transaction\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:557,\n#: includes/class.llms.post-types.php:558\nmsgid \"View Transaction\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:559\nmsgid \"Search Transactions\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:560\nmsgid \"No Transactions found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:561\nmsgid \"No Transactions found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:562\nmsgid \"Parent Transactions\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:565\nmsgid \"This is where single and recurring order transactions are stored\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:592,\n#: includes/integrations/class.llms.integration.buddypress.php:83,\n#: includes/privacy/class-llms-privacy-exporters.php:31,\n#: includes/admin/reporting/tables/llms.table.students.php:445,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:85\nmsgid \"Achievements\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:593\nmsgid \"Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:594\nmsgid \"Add Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:595\nmsgid \"Add New Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:597\nmsgid \"Edit Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:598\nmsgid \"New Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:599,\n#: includes/class.llms.post-types.php:600\nmsgid \"View Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:601\nmsgid \"Search Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:602\nmsgid \"No Achievement found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:603\nmsgid \"No Achievement found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:604\nmsgid \"Parent Achievement\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:605\nmsgctxt \"Admin menu name\"\nmsgid \"Achievements\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:607\nmsgid \"This is where achievements are stored.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:631,\n#: includes/integrations/class.llms.integration.buddypress.php:92,\n#: includes/privacy/class-llms-privacy-exporters.php:68,\n#: includes/admin/reporting/tables/llms.table.students.php:441,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:86\nmsgid \"Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:632\nmsgid \"Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:633\nmsgid \"Add Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:634\nmsgid \"Add New Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:636\nmsgid \"Edit Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:637\nmsgid \"New Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:638,\n#: includes/class.llms.post-types.php:639\nmsgid \"View Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:640\nmsgid \"Search Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:641\nmsgid \"No Certificates found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:642\nmsgid \"No Certificates found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:643\nmsgid \"Parent Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:644\nmsgctxt \"Admin menu name\"\nmsgid \"Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:646,\n#: includes/class.llms.post-types.php:688\nmsgid \"This is where you can view all of the certificates.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:655\nmsgctxt \"slug\"\nmsgid \"certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:673,\n#: includes/class.llms.student.dashboard.php:153,\n#: includes/functions/llms.functions.templates.dashboard.php:221\nmsgid \"My Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:674\nmsgid \"My Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:675\nmsgid \"Add My Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:676\nmsgid \"Add New My Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:678\nmsgid \"Edit My Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:679\nmsgid \"New My Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:680,\n#: includes/class.llms.post-types.php:681\nmsgid \"View My Certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:682\nmsgid \"Search My Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:683\nmsgid \"No My Certificates found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:684\nmsgid \"No My Certificates found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:685\nmsgid \"Parent My Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:686\nmsgctxt \"Admin menu name\"\nmsgid \"My Certificates\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:697\nmsgctxt \"slug\"\nmsgid \"my_certificate\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:715\nmsgid \"Emails\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:716,\n#: includes/abstracts/llms.abstract.notification.controller.php:420,\n#: includes/notifications/controllers/class.llms.notification.controller.manual.payment.due.php:96,\n#: includes/notifications/controllers/class.llms.notification.controller.payment.retry.php:96,\n#: includes/notifications/controllers/class.llms.notification.controller.purchase.receipt.php:98,\n#: includes/notifications/controllers/class.llms.notification.controller.student.welcome.php:83,\n#: includes/notifications/controllers/class.llms.notification.controller.subscription.cancelled.php:125,\n#: includes/admin/reporting/tables/llms.table.course.students.php:387,\n#: includes/admin/reporting/tables/llms.table.students.php:400\nmsgid \"Email\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:717\nmsgid \"Add Email\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:718\nmsgid \"Add New Email\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:720\nmsgid \"Edit Email\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:721\nmsgid \"New Email\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:722,\n#: includes/class.llms.post-types.php:723\nmsgid \"View Email\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:724\nmsgid \"Search Emails\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:725\nmsgid \"No Emails found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:726\nmsgid \"No Emails found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:727\nmsgid \"Parent Emails\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:728\nmsgctxt \"Admin menu name\"\nmsgid \"Emails\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:730\nmsgid \"This is where emails are stored.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:754\nmsgid \"Coupons\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:755,\n#: includes/admin/class.llms.admin.setup.wizard.php:190\nmsgid \"Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:756\nmsgid \"Add Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:757\nmsgid \"Add New Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:759\nmsgid \"Edit Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:760\nmsgid \"New Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:761,\n#: includes/class.llms.post-types.php:762\nmsgid \"View Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:763\nmsgid \"Search Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:764\nmsgid \"No Coupon found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:765\nmsgid \"No Coupon found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:766\nmsgid \"Parent Coupon\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:767\nmsgctxt \"Admin menu name\"\nmsgid \"Coupons\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:769\nmsgid \"This is where coupons are stored.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:793\nmsgid \"Vouchers\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:794,\n#: includes/admin/settings/class.llms.settings.accounts.php:419\nmsgid \"Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:795\nmsgid \"Add Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:796\nmsgid \"Add New Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:798\nmsgid \"Edit Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:799\nmsgid \"New Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:800,\n#: includes/class.llms.post-types.php:801\nmsgid \"View Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:802\nmsgid \"Search Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:803\nmsgid \"No Voucher found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:804\nmsgid \"No Voucher found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:805\nmsgid \"Parent Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:806\nmsgctxt \"Admin menu name\"\nmsgid \"Vouchers\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:808\nmsgid \"This is where voucher are stored.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:832,\n#: includes/admin/class.llms.admin.reviews.php:152\nmsgid \"Reviews\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:833\nmsgid \"Review\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:834\nmsgctxt \"Admin menu name\"\nmsgid \"Reviews\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:835\nmsgid \"Add Review\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:836\nmsgid \"Add New Review\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:838\nmsgid \"Edit Review\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:839\nmsgid \"New Review\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:840,\n#: includes/class.llms.post-types.php:841\nmsgid \"View Review\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:842\nmsgid \"Search Reviews\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:843\nmsgid \"No Reviews found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:844\nmsgid \"No Reviews found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:845\nmsgid \"Parent Review\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:847\nmsgid \"This is where you can add new reviews.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:871\nmsgid \"Access Plans\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:872,\n#: templates/checkout/form-summary.php:11,\n#: templates/myaccount/view-order.php:42\nmsgid \"Access Plan\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:873,\n#: templates/admin/post-types/product.php:24\nmsgid \"Add Access Plan\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:874\nmsgid \"Add New Access Plan\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:876\nmsgid \"Edit Access Plan\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:877,\n#: templates/admin/post-types/product-access-plan.php:45,\n#: templates/admin/post-types/product-access-plan.php:45\nmsgid \"New Access Plan\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:878,\n#: includes/class.llms.post-types.php:879\nmsgid \"View Access Plan\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:880\nmsgid \"Search Access Plans\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:881\nmsgid \"No Access Plans found\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:882\nmsgid \"No Access Plans found in trash\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:883\nmsgid \"Parent Access Plans\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:884\nmsgctxt \"Admin menu name\"\nmsgid \"Access Plans\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:886\nmsgid \"This is where access plans are stored.\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:926\nmsgctxt \"Transaction status\"\nmsgid \"Failed\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:934\nmsgctxt \"Transaction status\"\nmsgid \"Pending\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:939\nmsgid \"Pending <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Pending <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:942\nmsgctxt \"Transaction status\"\nmsgid \"Refunded\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:950\nmsgctxt \"Transaction status\"\nmsgid \"Succeeded\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:955\nmsgid \"Succeeded <span class=\\\"count\\\">(%s)</span>\"\nmsgid_plural \"Succeeded <span class=\\\"count\\\">(%s)</span>\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/class.llms.post-types.php:1000,\n#: includes/class.llms.post-types.php:1002\nmsgid \"Course Categories\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1003\nmsgid \"Course Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1004,\n#: includes/class.llms.post-types.php:1116\nmsgctxt \"Admin menu name\"\nmsgid \"Categories\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1005\nmsgid \"Search Course Categories\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1006\nmsgid \"All Course Categories\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1007\nmsgid \"Parent Course Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1008\nmsgid \"Parent Course Category:\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1009\nmsgid \"Edit Course Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1010\nmsgid \"Update Course Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1011\nmsgid \"Add New Course Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1012\nmsgid \"New Course Category Name\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1020\nmsgctxt \"slug\"\nmsgid \"course-category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1028,\n#: includes/class.llms.post-types.php:1030\nmsgid \"Course Difficulties\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1031\nmsgid \"Course Difficulty\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1032\nmsgctxt \"Admin menu name\"\nmsgid \"Difficulties\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1033\nmsgid \"Search Course Difficulties\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1034\nmsgid \"All Course Difficulties\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1035\nmsgid \"Parent Course Difficulty\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1036\nmsgid \"Parent Course Difficulty:\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1037\nmsgid \"Edit Course Difficulty\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1038\nmsgid \"Update Course Difficulty\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1039\nmsgid \"Add New Course Difficulty\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1040\nmsgid \"New Course Difficulty Name\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1048\nmsgctxt \"slug\"\nmsgid \"course-difficulty\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1056,\n#: includes/class.llms.post-types.php:1058\nmsgid \"Course Tags\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1059\nmsgid \"Course Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1060,\n#: includes/class.llms.post-types.php:1145\nmsgctxt \"Admin menu name\"\nmsgid \"Tags\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1061\nmsgid \"Search Course Tags\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1062\nmsgid \"All Course Tags\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1063\nmsgid \"Parent Course Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1064\nmsgid \"Parent Course Tag:\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1065\nmsgid \"Edit Course Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1066\nmsgid \"Update Course Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1067\nmsgid \"Add New Course Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1068\nmsgid \"New Course Tag Name\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1076\nmsgctxt \"slug\"\nmsgid \"course-tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1083,\n#: includes/class.llms.post-types.php:1086\nmsgid \"Course Track\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1085\nmsgid \"Course Tracks\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1087\nmsgctxt \"Admin menu name\"\nmsgid \"Tracks\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1088\nmsgid \"Search Course Tracks\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1089\nmsgid \"All Course Tracks\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1090\nmsgid \"Parent Course Track\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1091\nmsgid \"Parent Course Track:\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1092\nmsgid \"Edit Course Track\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1093\nmsgid \"Update Course Track\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1094\nmsgid \"Add New Course Track\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1095\nmsgid \"New Course Track Name\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1103\nmsgctxt \"slug\"\nmsgid \"course-track\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1112,\n#: includes/class.llms.post-types.php:1114\nmsgid \"Membership Categories\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1115\nmsgid \"Membership Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1117\nmsgid \"Search Membership Categories\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1118\nmsgid \"All Membership Categories\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1119\nmsgid \"Parent Membership Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1120\nmsgid \"Parent Membership Category:\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1121\nmsgid \"Edit Membership Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1122\nmsgid \"Update Membership Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1123\nmsgid \"Add New Membership Category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1124\nmsgid \"New Membership Category Name\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1132\nmsgctxt \"slug\"\nmsgid \"membership-category\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1141,\n#: includes/class.llms.post-types.php:1143\nmsgid \"Membership Tags\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1144\nmsgid \"Membership Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1146\nmsgid \"Search Membership Tags\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1147\nmsgid \"All Membership Tags\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1148\nmsgid \"Parent Membership Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1149\nmsgid \"Parent Membership Tag:\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1150\nmsgid \"Edit Membership Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1151\nmsgid \"Update Membership Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1152\nmsgid \"Add New Membership Tag\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1153\nmsgid \"New Membership Tag Name\"\nmsgstr \"\"\n\n#: includes/class.llms.post-types.php:1161\nmsgctxt \"slug\"\nmsgid \"membership-tag\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:47\nmsgid \"Other\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:53\nmsgid \"Enter your question...\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:77,\n#: includes/class.llms.question.types.php:90,\n#: includes/class.llms.question.types.php:116\nmsgid \"Basic Questions\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:81\nmsgid \"Multiple Choice\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:94\nmsgid \"Picture Choice\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:105\nmsgid \"True\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:110\nmsgid \"False\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:120\nmsgid \"True or False\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:129\nmsgid \"Content\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:130\nmsgid \"Enter your content title...\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:153,\n#: includes/class.llms.question.types.php:165,\n#: includes/class.llms.question.types.php:177,\n#: includes/class.llms.question.types.php:189,\n#: includes/class.llms.question.types.php:201,\n#: includes/class.llms.question.types.php:213,\n#: includes/class.llms.question.types.php:225,\n#: includes/class.llms.question.types.php:237\nmsgid \"Advanced Questions\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:157\nmsgid \"Fill in the Blank\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:169\nmsgid \"Reorder Items\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:181\nmsgid \"Reorder Pictures\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:193\nmsgid \"Short Answer\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:205\nmsgid \"Long Answer\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:217\nmsgid \"File Upload\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:229,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:33\nmsgid \"Code\"\nmsgstr \"\"\n\n#: includes/class.llms.question.types.php:241\nmsgid \"Scale\"\nmsgstr \"\"\n\n#: includes/class.llms.quiz.legacy.php:411,\n#: includes/models/model.llms.student.quizzes.php:129\nmsgctxt \"quiz attempts remaining\"\nmsgid \"Unlimited\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:42\nmsgid \"What Others Have Said\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:70\nmsgid \"By: %s\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:106, includes/class.llms.review.php:125\nmsgid \"Thank you for your review!\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:112\nmsgid \"Write a Review\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:114,\n#: includes/admin/class.llms.admin.reviews.php:48\nmsgid \"Review Title\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:115\nmsgid \"Review Title is required.\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:116\nmsgid \"Review Text\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:117\nmsgid \"Review Text is required.\"\nmsgstr \"\"\n\n#: includes/class.llms.review.php:121\nmsgid \"Leave Review\"\nmsgstr \"\"\n\n#: includes/class.llms.roles.php:280,\n#: tests/unit-tests/class.llms.test.roles.php:22\nmsgid \"LMS Manager\"\nmsgstr \"\"\n\n#: includes/class.llms.roles.php:281,\n#: tests/unit-tests/class.llms.test.roles.php:20,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:65\nmsgid \"Instructor\"\nmsgstr \"\"\n\n#: includes/class.llms.roles.php:282,\n#: tests/unit-tests/class.llms.test.roles.php:21\nmsgid \"Instructor's Assistant\"\nmsgstr \"\"\n\n#: includes/class.llms.roles.php:283, includes/class.llms.view.manager.php:188,\n#: includes/abstracts/llms.abstract.notification.controller.php:256,\n#: includes/functions/llms.functions.updates.php:717,\n#: tests/unit-tests/class.llms.test.roles.php:23,\n#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:257\nmsgid \"Student\"\nmsgstr \"\"\n\n#: includes/class.llms.roles.php:306\nmsgid \"Administrator\"\nmsgstr \"\"\n\n#: includes/class.llms.sidebars.php:171\nmsgid \"Widgets in this area will be shown on LifterLMS courses.\"\nmsgstr \"\"\n\n#: includes/class.llms.sidebars.php:172\nmsgid \"Course Sidebar\"\nmsgstr \"\"\n\n#: includes/class.llms.sidebars.php:175\nmsgid \"Widgets in this area will be shown on LifterLMS lessons.\"\nmsgstr \"\"\n\n#: includes/class.llms.sidebars.php:177\nmsgid \"Lesson Sidebar\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:128\nmsgid \"Dashboard\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:141,\n#: includes/functions/llms.functions.templates.dashboard.php:293\nmsgid \"My Memberships\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:147,\n#: includes/functions/llms.functions.templates.dashboard.php:185\nmsgid \"My Achievements\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:159,\n#: includes/admin/settings/class.llms.settings.accounts.php:145,\n#: includes/admin/settings/class.llms.settings.notifications.php:17\nmsgid \"Notifications\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:165,\n#: includes/admin/settings/class.llms.settings.accounts.php:154\nmsgid \"Edit Account\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:171\nmsgid \"Redeem a Voucher\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:177\nmsgid \"Order History\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:293\nmsgid \"View Notifications\"\nmsgstr \"\"\n\n#: includes/class.llms.student.dashboard.php:297\nmsgid \"Manage Preferences\"\nmsgstr \"\"\n\n#: includes/class.llms.template.loader.php:335\nmsgid \"You must be enrolled in the course to access this quiz.\"\nmsgstr \"\"\n\n#: includes/class.llms.view.manager.php:90\nmsgid \"Viewing as %s\"\nmsgstr \"\"\n\n#: includes/class.llms.view.manager.php:108\nmsgid \"View as %s\"\nmsgstr \"\"\n\n#: includes/class.llms.view.manager.php:186\nmsgid \"Myself\"\nmsgstr \"\"\n\n#: includes/class.llms.view.manager.php:187\nmsgid \"Visitor\"\nmsgstr \"\"\n\n#: includes/class.llms.voucher.php:260\nmsgid \"Voucher code \\\"%s\\\" could not be found.\"\nmsgstr \"\"\n\n#: includes/class.llms.voucher.php:264\nmsgid \"\"\n\"Voucher code \\\"%s\\\" has already been redeemed the maximum number of times.\"\nmsgstr \"\"\n\n#: includes/class.llms.voucher.php:268\nmsgid \"Voucher code \\\"%s\\\" is no longer valid.\"\nmsgstr \"\"\n\n#: includes/class.llms.voucher.php:296\nmsgid \"You have already redeemed this voucher.\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:126\nmsgid \"\"\n\"%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:128\nmsgid \"%1$s is <strong>deprecated</strong> since version %2$s!\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:186,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:84\nmsgid \"Visible\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:187, includes/llms.functions.core.php:404,\n#: includes/admin/settings/class.llms.settings.accounts.php:44,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:85\nmsgid \"Hidden\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:188\nmsgid \"Featured\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:249,\n#: templates/admin/post-types/product-access-plan.php:121\nmsgid \"year\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:250,\n#: templates/admin/post-types/product-access-plan.php:122\nmsgid \"month\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:251,\n#: templates/admin/post-types/product-access-plan.php:124\nmsgid \"day\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:257\nmsgid \"years\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:258\nmsgid \"months\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:259\nmsgid \"days\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:337\nmsgid \"Student creates a new account\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:338\nmsgid \"Student Purchases an Access Plan\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:339\nmsgid \"Student enrolls in a course\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:340\nmsgid \"Student purchases a course\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:341\nmsgid \"Student completes a course\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:343\nmsgid \"Student completes a lesson\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:344\nmsgid \"Student completes a quiz\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:345\nmsgid \"Student passes a quiz\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:346\nmsgid \"Student fails a quiz\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:347\nmsgid \"Student completes a section\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:348\nmsgid \"Student completes a course track\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:349\nmsgid \"Student enrolls in a membership\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:350\nmsgid \"Student purchases a membership\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:362\nmsgid \"Award an Achievement\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:363\nmsgid \"Award a Certificate\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:401\nmsgid \"Catalog &amp; Search\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:402\nmsgid \"Catalog only\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:403\nmsgid \"Search only\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:624\nmsgid \"Cancelled\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:625,\n#: includes/admin/reporting/tables/llms.table.student.memberships.php:113\nmsgid \"Enrolled\"\nmsgstr \"\"\n\n#: includes/llms.functions.core.php:626\nmsgid \"Expired\"\nmsgstr \"\"\n\n#: includes/llms.template.functions.php:768\nmsgid \"Search Results: &ldquo;%s&rdquo;\"\nmsgstr \"\"\n\n#: includes/llms.template.functions.php:771\nmsgid \"&nbsp;&ndash; Page %s\"\nmsgstr \"\"\n\n#: includes/llms.template.functions.php:823\nmsgid \"%s\"\nmsgstr \"\"\n\n#: includes/llms.template.functions.php:830,\n#: includes/llms.template.functions.php:902\nmsgid \"Continue\"\nmsgstr \"\"\n\n#: includes/llms.template.functions.php:887,\n#: includes/notifications/controllers/class.llms.notification.controller.course.complete.php:85\nmsgid \"Course Complete\"\nmsgstr \"\"\n\n#: includes/llms.template.functions.php:898\nmsgid \"Get Started\"\nmsgstr \"\"\n\n#: templates/content-certificate.php:14\nmsgid \"Certificate not found.\"\nmsgstr \"\"\n\n#: templates/content-certificate.php:40\nmsgid \"Print\"\nmsgstr \"\"\n\n#: templates/content-certificate.php:46,\n#: templates/myaccount/form-edit-account.php:44\nmsgid \"Save\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:381\nmsgid \"Any\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:383,\n#: includes/abstracts/abstract.llms.admin.table.php:385\nmsgid \"Any %s\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:578\nmsgid \"Search\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:645,\n#: includes/admin/post-types/class.llms.post.tables.php:53,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.courses.php:109\nmsgid \"Export\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:656\nmsgctxt \"pagination\"\nmsgid \"%d of %d\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:660\nmsgid \"First\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:662,\n#: templates/myaccount/my-notifications.php:37,\n#: templates/myaccount/my-orders.php:61,\n#: templates/myaccount/view-order-transactions.php:49\nmsgid \"Back\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:665,\n#: includes/shortcodes/class.llms.shortcodes.php:290,\n#: templates/loop/pagination.php:24,\n#: templates/myaccount/my-notifications.php:41,\n#: templates/myaccount/my-orders.php:67,\n#: templates/myaccount/view-order-transactions.php:52\nmsgid \"Next\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:667\nmsgid \"Last\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.admin.table.php:881\nmsgid \"No results were found.\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:220\nmsgid \"Customer ID\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:225\nmsgid \"Source ID\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:230\nmsgid \"Subscription ID\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:259\nmsgctxt \"Payment gateway title\"\nmsgid \"Enable %s\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:260\nmsgid \"Checking this box will allow users to use this payment gateway.\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:262,\n#: includes/abstracts/llms.abstract.integration.php:115,\n#: includes/admin/settings/class.llms.settings.accounts.php:245,\n#: includes/admin/settings/class.llms.settings.accounts.php:377\nmsgid \"Enable / Disable\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:268\nmsgid \"The title the user sees during checkout.\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:270,\n#: includes/abstracts/llms.abstract.notification.view.php:318,\n#: includes/privacy/class-llms-privacy-exporters.php:97,\n#: includes/privacy/class-llms-privacy-exporters.php:141,\n#: includes/privacy/class-llms-privacy-exporters.php:270,\n#: includes/notifications/views/class.llms.notification.view.enrollment.php:73,\n#: includes/admin/reporting/tables/llms.table.courses.php:276,\n#: includes/admin/reporting/tables/llms.table.quizzes.php:300,\n#: includes/admin/views/builder/lesson-settings.php:13,\n#: includes/admin/views/builder/quiz.php:35\nmsgid \"Title\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:276\nmsgid \"The description the user sees during checkout.\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:284\nmsgid \"\"\n\"This determines the order gateways are displayed on the checkout page. \"\n\"Lowest number will display first.\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:286\nmsgid \"Display Order\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:294\nmsgctxt \"Payment gateway test mode title\"\nmsgid \"Enable %s\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:305\nmsgid \"Enable debug logging\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:306\nmsgid \"When enabled, debugging information will be logged to \\\"%s\\\"\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:307\nmsgid \"Debug Log\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.payment.gateway.php:558\nmsgid \"\"\n\"The selected payment Gateway \\\"%s\\\" does not support payment method \"\n\"switching.\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.post.model.php:332\nmsgid \"An unknown error occurred during post cloning. Please try again.\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.update.php:260\nmsgid \"LifterLMS Database Upgrade %s Progress Report\"\nmsgstr \"\"\n\n#: includes/abstracts/abstract.llms.update.php:262\nmsgid \"\"\n\"This completion percentage is an estimate, please be patient and %sclick here\"\n\"%s for more information.\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.integration.php:111\nmsgid \"Check to enable this integration.\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.controller.php:253,\n#: includes/models/model.llms.post.instructors.php:55,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:43\nmsgid \"Author\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.controller.php:259\nmsgid \"Lesson Author\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.controller.php:262\nmsgid \"Course Author\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.controller.php:265\nmsgid \"\"\n\"Enter additional email addresses which will receive this notification. \"\n\"Separate multiple addresses with commas.\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.controller.php:266\nmsgid \"Additional Recipients\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.controller.php:419,\n#: includes/notifications/controllers/class.llms.notification.controller.achievement.earned.php:118,\n#: includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php:118,\n#: includes/notifications/controllers/class.llms.notification.controller.manual.payment.due.php:95,\n#: includes/notifications/controllers/class.llms.notification.controller.payment.retry.php:95\nmsgid \"Basic\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:253\nmsgctxt \"relative date display\"\nmsgid \"About %s ago\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:308\nmsgid \"Subject\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:318\nmsgid \"Heading\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:330\nmsgid \"Body\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:340\nmsgid \"Icon\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:346\nmsgid \"\"\n\"When checked the icon will not be displayed when showing this notification.\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:348\nmsgid \"Disable Icon\"\nmsgstr \"\"\n\n#: includes/abstracts/llms.abstract.notification.view.php:468\nmsgid \"Divider Line\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:87\nmsgid \"Advanced\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:91\nmsgid \"Affiliates\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:95\nmsgid \"All\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:99\nmsgid \"Bundles\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:103,\n#: includes/admin/settings/class.llms.settings.checkout.php:310\nmsgid \"Payment Gateways\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:107\nmsgid \"E-Mail & Marketing\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:111\nmsgid \"Themes & Design\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:115,\n#: includes/admin/class.llms.admin.page.status.php:128\nmsgid \"Tools & Utilities\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:119\nmsgid \"Resources\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:123\nmsgid \"Services\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:140,\n#: includes/admin/class.llms.admin.addons.php:225\nmsgid \"There was an error retrieving add-ons. Please try again.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:146\nmsgid \"LifterLMS Add-Ons, Services, and Resources\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.addons.php:173\nmsgid \"Created by:\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.analytics.php:79,\n#: includes/admin/class.llms.admin.settings.php:70\nmsgid \"Whoa! something went wrong there!. Please refresh the page and retry.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.analytics.php:86\nmsgid \"Your analytics have been saved.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.analytics.php:182,\n#: includes/admin/class.llms.admin.analytics.php:251,\n#: includes/admin/class.llms.admin.analytics.php:333\nmsgid \"You must choose a product option.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.analytics.php:185,\n#: includes/admin/class.llms.admin.analytics.php:254,\n#: includes/admin/class.llms.admin.analytics.php:336\nmsgid \"You must choose a date filter.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.analytics.php:188,\n#: includes/admin/class.llms.admin.analytics.php:257,\n#: includes/admin/class.llms.admin.analytics.php:339\nmsgid \"You must enter a start and end date.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:84\nmsgid \"%s Theme Settings\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:176\nmsgid \"Lesson: %1$s (#%2$d)\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:179\nmsgid \"Course: %1$s (#%2$d)\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:367\nmsgid \"Success\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:374\nmsgid \"Error: Invalid or missing course ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:379\nmsgid \"Error: You do not have permission to edit this course.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:456\nmsgid \"Invalid course ID\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:465\nmsgid \"You cannot edit this course!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:547\nmsgid \"Unable to detach \\\"%s\\\". Invalid ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:602\nmsgid \"Unable to delete \\\"%s\\\". Invalid ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:651\nmsgid \"Error deleting %1$s \\\"%s\\\".\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:793\nmsgid \"Unable to update lesson \\\"%s\\\". Invalid lesson ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:882\nmsgid \"Unable to update question \\\"%s\\\". Invalid question ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:909\nmsgid \"Unable to update choice \\\"%s\\\". Invalid choice ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:964\nmsgid \"Unable to update quiz \\\"%s\\\". Invalid quiz ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.builder.php:1032\nmsgid \"Unable to update section \\\"%s\\\". Invalid section ID.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:60\nmsgid \"Import Successful\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:70\nmsgid \"Authors\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:86\nmsgid \"Plans\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:98,\n#: templates/checkout/form-summary.php:23\nmsgid \"Terms\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:128\nmsgid \"The uploaded file exceeds the upload_max_filesize directive in php.ini.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:132\nmsgid \"\"\n\"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in \"\n\"the HTML form.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:136\nmsgid \"The uploaded file was only partially uploaded.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:140\nmsgid \"No file was uploaded.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:144\nmsgid \"Missing a temporary folder.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:148\nmsgid \"Failed to write file to disk.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:152\nmsgid \"File upload stopped by extension.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:156\nmsgid \"Unknown upload error.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.import.php:163\nmsgid \"Only valid JSON files can be imported.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:114,\n#: includes/admin/class.llms.admin.menus.php:139,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:21\nmsgid \"Course Builder\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:131\nmsgid \"LifterLMS Settings\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:133\nmsgid \"LifterLMS Reporting\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:133\nmsgid \"Reporting\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:135\nmsgid \"LifterLMS Import\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:135,\n#: templates/admin/import/import.php:26\nmsgid \"Import\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:137\nmsgid \"LifterLMS Status\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:137,\n#: includes/privacy/class-llms-privacy-exporters.php:286,\n#: templates/myaccount/view-order.php:37,\n#: templates/admin/post-types/order-transactions.php:18,\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:356,\n#: includes/admin/reporting/tables/llms.table.course.students.php:393,\n#: includes/admin/reporting/tables/llms.table.student.courses.php:203,\n#: includes/admin/reporting/tables/llms.table.student.memberships.php:110,\n#: templates/admin/reporting/tabs/quizzes/attempt.php:76\nmsgid \"Status\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:139\nmsgid \"LifterLMS Course Builder\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:158\nmsgid \"LifterLMS Add-ons\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:158\nmsgid \"Add-ons\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.menus.php:232,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.courses.php:86,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.quizzes.php:67,\n#: templates/admin/reporting/tabs/students/courses.php:33\nmsgid \"You do not have permission to access this content.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.notices.core.php:114\nmsgid \"\"\n\"No LifterLMS Payment Gateways are currently enabled. Students will only be \"\n\"able to enroll in courses or memberships with free access plans.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.notices.core.php:115\nmsgid \"\"\n\"For starters you can configure manual payments on the %1$sCheckout Settings \"\n\"tab%2$s. Be sure to check out all the available %3$sLifterLMS Payment \"\n\"Gateways%4$s and install one later so that you can start selling your \"\n\"courses and memberships.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.notices.core.php:163\nmsgid \"\"\n\"<strong>The current theme, %1$s, does not declare support for LifterLMS \"\n\"Sidebars.</strong> Course and Lesson sidebars may not work as expected. \"\n\"Please see our %2$sintegration guide%3$s or check out our %4$sLaunchPad%5$s \"\n\"theme which is designed specifically for use with LifterLMS.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.notices.php:237\nmsgid \"Dismiss\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.notices.php:252\nmsgid \"Remind me later\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:127\nmsgid \"System Report\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:129\nmsgid \"Logs\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:237\nmsgid \"View Log\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:241\nmsgid \"Viewing: %s\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:244\nmsgid \"Delete Log\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:252\nmsgid \"There are currently no logs to view.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:268\nmsgid \"\"\n\"Allows you to choose to enable or disable automatic recurring payments which \"\n\"may be disabled on a staging site.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:269\nmsgid \"Automatic Payments\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:270\nmsgid \"Reset Automatic Payments\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:274\nmsgid \"\"\n\"Manage User Sessions. LifterLMS creates custom user sessions to manage, \"\n\"payment processing, quizzes and user registration. If you are experiencing \"\n\"issues or incorrect error messages are displaying. Clearing out all of the \"\n\"user session data may help.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:275\nmsgid \"User Sessions\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:276\nmsgid \"Clear All Session Data\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:280\nmsgid \"\"\n\"If you opted into LifterLMS Tracking and no longer wish to participate, you \"\n\"may opt out here.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:281,\n#: includes/admin/class.llms.admin.page.status.php:282\nmsgid \"Reset Tracking Settings\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:286\nmsgid \"\"\n\"Clears the cached data displayed on various reporting screens. This does not \"\n\"affect actual student progress, it only clears cached progress data. This \"\n\"data will be regenerated the next time it is accessed.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:287\nmsgid \"Student Progress Cache\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:288\nmsgid \"Clear cache\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:292\nmsgid \"\"\n\"If you want to run the LifterLMS Setup Wizard again or skipped it and want \"\n\"to return now, click below.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:293\nmsgid \"Setup Wizard\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.page.status.php:294\nmsgid \"Return to Setup Wizard\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.post-types.php:80\nmsgid \"Custom field updated.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.post-types.php:81\nmsgid \"Custom field deleted.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.post-types.php:88\nmsgid \"M j, Y @ G:i\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:49\nmsgid \"Course Reviewed\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:50\nmsgid \"Review Author\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:51\nmsgid \"Review Date\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:106\nmsgid \"Enable Reviews\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:107\nmsgid \"Select to enable reviews.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:116\nmsgid \"Display Reviews\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:117\nmsgid \"Select to display reviews on the page.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:127\nmsgid \"Number of Reviews\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:128\nmsgid \"Number of reviews to display on the page.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:137\nmsgid \"Prevent Multiple Reviews\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.reviews.php:138\nmsgid \"Select to prevent a user from submitting more than one review.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.settings.php:77\nmsgid \"Your settings have been saved.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.settings.php:566\nmsgid \"Upload\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.settings.php:605\nmsgid \"Select a page&hellip;\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:140\nmsgid \"Allow\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:142\nmsgid \"Install a Sample Course\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:144\nmsgid \"Save & Continue\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:157\nmsgid \"No thanks\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:159\nmsgid \"Skip this step\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:187\nmsgid \"Welcome!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:188,\n#: includes/admin/class.llms.admin.setup.wizard.php:316\nmsgid \"Page Setup\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:189,\n#: includes/admin/class.llms.admin.setup.wizard.php:350\nmsgid \"Payments\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:191\nmsgid \"Finish!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:236\nmsgid \"Skip setup\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:237\nmsgid \"Get Started Now\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:241\nmsgid \"Go back\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:249\nmsgid \"Start from Scratch\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:262\nmsgid \"Return to the WordPress Dashboard\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:285\nmsgid \"Help Improve LifterLMS & Get a Coupon\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:286\nmsgid \"\"\n\"By allowing us to collect non-sensitive usage information and diagnostic \"\n\"data, you'll be providing us with information we can use to make the future \"\n\"of LifterLMS stronger and more powerful with every update!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:287\nmsgid \"Click \\\"Allow\\\" to and we'll send you a coupon immediately.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:288\nmsgid \"Find out more information\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:294\nmsgid \"Setup Complete!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:295\nmsgid \"Here's some resources to help you get familiar with LifterLMS:\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:297\nmsgid \"Watch the LifterLMS video tutorials\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:298\nmsgid \"Read the LifterLMS Getting Started Guide\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:301\nmsgid \"Get started with your first course\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:307\nmsgid \"Welcome to LifterLMS!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:309\nmsgid \"\"\n\"Thanks for choosing LifterLMS to power your online courses! This short setup \"\n\"wizard will guide you through the basic settings and configure LifterLMS so \"\n\"you can get started creating courses faster!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:310\nmsgid \"\"\n\"It will only take a few minutes and it is completely optional. If you don't \"\n\"have the time now, come back later.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:318\nmsgid \"\"\n\"LifterLMS has a few essential pages. The following will be created \"\n\"automatically if they don't already exist.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:323\nmsgid \"\"\n\"This page is where your visitors will find a list of all your available \"\n\"courses.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:327,\n#: includes/admin/settings/class.llms.settings.memberships.php:89\nmsgid \"\"\n\"This page is where your visitors will find a list of all your available \"\n\"memberships.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:330,\n#: includes/admin/settings/class.llms.settings.checkout.php:30\nmsgid \"Checkout\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:331\nmsgid \"\"\n\"This is the page where visitors will be directed in order to pay for courses \"\n\"and memberships.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:334,\n#: includes/admin/settings/class.llms.settings.accounts.php:57\nmsgid \"Student Dashboard\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:335,\n#: includes/admin/settings/class.llms.settings.accounts.php:63\nmsgid \"\"\n\"Page where students can view and manage their current enrollments, earned \"\n\"certificates and achievements, account information, and purchase history.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:339\nmsgid \"\"\n\"After setup, you can manage these pages from the admin dashboard on the \"\n\"%1$sPages screen%2$s and you can control which pages display on your menu(s) \"\n\"via %3$sAppearance > Menus%4$s.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:355\nmsgid \"Which country should be used as the default for student registrations?\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:367\nmsgid \"Which currency should be used for payment processing?\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:374\nmsgid \"If you currency is not listed you can %1$sadd it later%2$s.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:380\nmsgid \"\"\n\"With LifterLMS you can accept both online and offline payments. Be sure to \"\n\"install a %1$spayment gateway%2$s to accept online payments.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:381\nmsgid \"Enable Offline Payments\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.setup.wizard.php:482\nmsgid \"There was an error saving your data, please try again.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.system-report.php:47\nmsgid \"Support\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.system-report.php:52\nmsgid \"Copy for Support\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.system-report.php:53\nmsgid \"Get Help\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:93,\n#: includes/privacy/class-llms-privacy.php:102,\n#: includes/privacy/class-llms-privacy.php:201,\n#: includes/admin/reporting/tables/llms.table.students.php:454\nmsgid \"Billing Address 1\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:101,\n#: includes/privacy/class-llms-privacy.php:103,\n#: includes/privacy/class-llms-privacy.php:202,\n#: includes/admin/reporting/tables/llms.table.students.php:459\nmsgid \"Billing Address 2\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:109,\n#: includes/privacy/class-llms-privacy.php:104,\n#: includes/privacy/class-llms-privacy.php:203,\n#: includes/admin/reporting/tables/llms.table.students.php:464\nmsgid \"Billing City\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:117,\n#: includes/privacy/class-llms-privacy.php:105,\n#: includes/privacy/class-llms-privacy.php:204,\n#: includes/admin/reporting/tables/llms.table.students.php:469\nmsgid \"Billing State\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:125,\n#: includes/privacy/class-llms-privacy.php:106,\n#: includes/privacy/class-llms-privacy.php:205\nmsgid \"Billing Zip Code\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:133,\n#: includes/privacy/class-llms-privacy.php:107,\n#: includes/privacy/class-llms-privacy.php:206,\n#: includes/admin/reporting/tables/llms.table.students.php:479\nmsgid \"Billing Country\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:141,\n#: includes/privacy/class-llms-privacy.php:108,\n#: includes/privacy/class-llms-privacy.php:207,\n#: includes/admin/reporting/tables/llms.table.students.php:484\nmsgid \"Phone\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:229\nmsgid \"Parent Instructor(s)\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.admin.user.custom.fields.php:330\nmsgid \"Required field \\\"%s\\\" is missing.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.student.bulk.enroll.php:72\nmsgid \"Choose Course/Membership\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.student.bulk.enroll.php:76,\n#: includes/functions/llms.functions.updates.php:110,\n#: includes/models/model.llms.access.plan.php:326\nmsgid \"Enroll\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.student.bulk.enroll.php:102\nmsgid \"Please select a Course or Membership to enroll users into!\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.student.bulk.enroll.php:114\nmsgid \"Please select users to enroll into <em>%s</em>.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.student.bulk.enroll.php:166\nmsgid \"No such users found. Cannot enroll into <em>%s</em>.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.student.bulk.enroll.php:221\nmsgid \"Failed to enroll <em>%1s</em> into <em>%2s</em>.\"\nmsgstr \"\"\n\n#: includes/admin/class.llms.student.bulk.enroll.php:221\nmsgid \"Successfully enrolled <em>%1s</em> into <em>%2s</em>.\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:109,\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:68\nmsgid \"Site Title\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:110\nmsgid \"Site URL\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:111,\n#: includes/privacy/class-llms-privacy-exporters.php:107,\n#: includes/privacy/class-llms-privacy-exporters.php:146,\n#: includes/admin/reporting/tables/llms.table.achievements.php:155,\n#: includes/admin/reporting/tables/llms.table.certificates.php:158\nmsgid \"Earned Date\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:112,\n#: includes/admin/llms.functions.admin.php:128\nmsgid \"Student First Name\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:113,\n#: includes/admin/llms.functions.admin.php:129\nmsgid \"Student Last Name\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:114,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:40\nmsgid \"Student Email\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:115\nmsgid \"Student User ID\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:116,\n#: includes/admin/llms.functions.admin.php:127\nmsgid \"Student Username\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:124\nmsgid \"Website Title\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:125\nmsgid \"Website URL\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:126\nmsgid \"Student Email Address\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:130\nmsgid \"Current Date\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:153\nmsgid \"Merge Codes\"\nmsgstr \"\"\n\n#: includes/admin/llms.functions.admin.php:163\nmsgid \"No merge codes found.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.lesson.progression.php:73,\n#: includes/controllers/class.llms.controller.lesson.progression.php:115\nmsgid \"An error occurred, please try again.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.lesson.progression.php:124\nmsgid \"%s is now incomplete.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:85,\n#: includes/controllers/class.llms.controller.orders.php:91,\n#: includes/shortcodes/class.llms.shortcode.checkout.php:171\nmsgid \"Could not locate an order to confirm.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:96\nmsgid \"Only pending orders can be confirmed.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:189\nmsgid \"You must agree to the %s to continue.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:195\nmsgid \"Missing an Access Plan ID.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:199\nmsgid \"Invalid Access Plan ID.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:231\nmsgid \"No payment method selected.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:262\nmsgid \"\"\n\"An unknown error occurred when attempting to create an account, please try \"\n\"again.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:267\nmsgid \"\"\n\"You already have access to this %2$s! Visit your dashboard <a href=\\\"%s\"\n\"\\\">here.</a>\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:298\nmsgid \"There was an error creating your order, please try again.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:363\nmsgid \"\"\n\"Student unenrolled at the end of access period due to subscription \"\n\"cancellation.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:370\nmsgid \"Student unenrolled due to automatic access plan expiration\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:457\nmsgid \"Missing order information.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:462,\n#: templates/myaccount/view-order.php:10\nmsgid \"Invalid Order.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:464\nmsgid \"Missing gateway information.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:580\nmsgid \"Order status changed from %1$s to %2$s\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:611\nmsgid \"The selected payment gateway is not currently enabled.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:616\nmsgid \"\"\n\"%s does not support recurring payments and cannot process this transaction.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:621\nmsgid \"\"\n\"%s does not support single payments and cannot process this transaction.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.orders.php:626\nmsgid \"An invalid payment method was selected.\"\nmsgstr \"\"\n\n#: includes/controllers/class.llms.controller.quizzes.php:34\nmsgid \"Could not proceed to the quiz because required information was missing.\"\nmsgstr \"\"\n\n#: includes/emails/class.llms.email.reset.password.php:26\nmsgid \"Password Reset for {site_title}\"\nmsgstr \"\"\n\n#: includes/emails/class.llms.email.reset.password.php:27\nmsgid \"Reset Your Password\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.access.php:185\nmsgctxt \"restricted by course prerequisite message\"\nmsgid \"\"\n\"The lesson \\\"%1$s\\\" cannot be accessed until the required prerequisite \"\n\"course \\\"%2$s\\\" is completed.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.access.php:192\nmsgctxt \"restricted by course track prerequisite message\"\nmsgid \"\"\n\"The lesson \\\"%1$s\\\" cannot be accessed until the required prerequisite track \"\n\"\\\"%2$s\\\" is completed.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.access.php:214\nmsgctxt \"lesson restricted by drip settings message\"\nmsgid \"The lesson \\\"%1$s\\\" will be available on %2$s\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.access.php:221\nmsgctxt \"lesson restricted by prerequisite message\"\nmsgid \"\"\n\"The lesson \\\"%1$s\\\" cannot be accessed until the required prerequisite \\\"%2$s\"\n\"\\\" is completed.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:48\nmsgid \"Afghanistan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:49\nmsgid \"Albania\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:50\nmsgid \"Algeria\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:51\nmsgid \"American Samoa\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:52\nmsgid \"Andorra\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:53\nmsgid \"Angola\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:54\nmsgid \"Anguilla\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:55\nmsgid \"Antarctica\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:56\nmsgid \"Antigua And Barbuda\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:57\nmsgid \"Argentina\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:58\nmsgid \"Armenia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:59\nmsgid \"Aruba\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:60\nmsgid \"Australia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:61\nmsgid \"Austria\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:62\nmsgid \"Azerbaijan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:63\nmsgid \"Bahamas\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:64\nmsgid \"Bahrain\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:65\nmsgid \"Bangladesh\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:66\nmsgid \"Barbados\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:67\nmsgid \"Belarus\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:68\nmsgid \"Belgium\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:69\nmsgid \"Belize\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:70\nmsgid \"Benin\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:71\nmsgid \"Bermuda\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:72\nmsgid \"Bhutan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:73\nmsgid \"Bolivia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:74\nmsgid \"Bosnia And Herzegowina\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:75\nmsgid \"Botswana\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:76\nmsgid \"Bouvet Island\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:77\nmsgid \"Brazil\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:78\nmsgid \"British Indian Ocean Territory\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:79\nmsgid \"Brunei Darussalam\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:80\nmsgid \"Bulgaria\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:81\nmsgid \"Burkina Faso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:82\nmsgid \"Burundi\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:83\nmsgid \"Cambodia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:84\nmsgid \"Cameroon\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:85\nmsgid \"Canada\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:86\nmsgid \"Cape Verde\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:87\nmsgid \"Cayman Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:88\nmsgid \"Central African Republic\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:89\nmsgid \"Chad\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:90\nmsgid \"Chile\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:91\nmsgid \"China\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:92\nmsgid \"Christmas Island\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:93\nmsgid \"Cocos (Keeling) Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:94\nmsgid \"Colombia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:95\nmsgid \"Comoros\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:96\nmsgid \"Congo\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:97\nmsgid \"Congo, The Democratic Republic Of The\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:98\nmsgid \"Cook Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:99\nmsgid \"Costa Rica\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:100\nmsgid \"Cote D'Ivoire\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:101\nmsgid \"Croatia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:102\nmsgid \"Cuba\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:103\nmsgid \"Cyprus\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:104\nmsgid \"Czech Republic\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:105\nmsgid \"Denmark\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:106\nmsgid \"Djibouti\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:107\nmsgid \"Dominica\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:108\nmsgid \"Dominican Republic\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:109\nmsgid \"East Timor\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:110\nmsgid \"Ecuador\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:111\nmsgid \"Egypt\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:112\nmsgid \"El Salvador\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:113\nmsgid \"Equatorial Guinea\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:114\nmsgid \"Eritrea\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:115\nmsgid \"Estonia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:116\nmsgid \"Ethiopia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:117\nmsgid \"Falkland Islands (Malvinas)\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:118\nmsgid \"Faroe Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:119\nmsgid \"Fiji\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:120\nmsgid \"Finland\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:121\nmsgid \"France\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:122\nmsgid \"France, Metropolitan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:123\nmsgid \"French Guiana\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:124\nmsgid \"French Polynesia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:125\nmsgid \"French Southern Territories\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:126\nmsgid \"Gabon\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:127\nmsgid \"Gambia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:128\nmsgid \"Georgia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:129\nmsgid \"Germany\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:130\nmsgid \"Ghana\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:131\nmsgid \"Gibraltar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:132\nmsgid \"Greece\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:133\nmsgid \"Greenland\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:134\nmsgid \"Grenada\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:135\nmsgid \"Guadeloupe\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:136\nmsgid \"Guam\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:137\nmsgid \"Guatemala\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:138\nmsgid \"Guinea\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:139\nmsgid \"Guinea-Bissau\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:140\nmsgid \"Guyana\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:141\nmsgid \"Haiti\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:142\nmsgid \"Heard And Mc Donald Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:143\nmsgid \"Holy See (Vatican City State)\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:144\nmsgid \"Honduras\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:145\nmsgid \"Hong Kong\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:146\nmsgid \"Hungary\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:147\nmsgid \"Iceland\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:148\nmsgid \"India\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:149\nmsgid \"Indonesia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:150\nmsgid \"Iran (Islamic Republic Of)\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:151\nmsgid \"Iraq\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:152\nmsgid \"Ireland\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:153\nmsgid \"Israel\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:154\nmsgid \"Italy\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:155\nmsgid \"Jamaica\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:156\nmsgid \"Japan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:157\nmsgid \"Jordan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:158\nmsgid \"Kazakhstan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:159\nmsgid \"Kenya\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:160\nmsgid \"Kiribati\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:161\nmsgid \"Korea, Democratic People's Republic Of\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:162\nmsgid \"Korea, Republic Of\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:163\nmsgid \"Kuwait\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:164\nmsgid \"Kyrgyzstan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:165\nmsgid \"Lao People's Democratic Republic\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:166\nmsgid \"Latvia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:167\nmsgid \"Lebanon\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:168\nmsgid \"Lesotho\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:169\nmsgid \"Liberia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:170\nmsgid \"Libyan Arab Jamahiriya\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:171\nmsgid \"Liechtenstein\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:172\nmsgid \"Lithuania\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:173\nmsgid \"Luxembourg\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:174\nmsgid \"Macau\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:175\nmsgid \"Macedonia, Former Yugoslav Republic Of\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:176\nmsgid \"Madagascar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:177\nmsgid \"Malawi\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:178\nmsgid \"Malaysia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:179\nmsgid \"Maldives\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:180\nmsgid \"Mali\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:181\nmsgid \"Malta\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:182\nmsgid \"Marshall Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:183\nmsgid \"Martinique\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:184\nmsgid \"Mauritania\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:185\nmsgid \"Mauritius\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:186\nmsgid \"Mayotte\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:187\nmsgid \"Mexico\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:188\nmsgid \"Micronesia, Federated States Of\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:189\nmsgid \"Moldova, Republic Of\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:190\nmsgid \"Monaco\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:191\nmsgid \"Mongolia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:192\nmsgid \"Montserrat\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:193\nmsgid \"Morocco\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:194\nmsgid \"Mozambique\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:195\nmsgid \"Myanmar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:196\nmsgid \"Namibia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:197\nmsgid \"Nauru\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:198\nmsgid \"Nepal\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:199\nmsgid \"Netherlands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:200\nmsgid \"Netherlands Antilles\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:201\nmsgid \"New Caledonia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:202\nmsgid \"New Zealand\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:203\nmsgid \"Nicaragua\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:204\nmsgid \"Niger\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:205\nmsgid \"Nigeria\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:206\nmsgid \"Niue\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:207\nmsgid \"Norfolk Island\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:208\nmsgid \"Northern Mariana Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:209\nmsgid \"Norway\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:210\nmsgid \"Oman\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:211\nmsgid \"Pakistan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:212\nmsgid \"Palau\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:213\nmsgid \"Panama\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:214\nmsgid \"Papua New Guinea\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:215\nmsgid \"Paraguay\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:216\nmsgid \"Peru\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:217\nmsgid \"Philippines\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:218\nmsgid \"Pitcairn\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:219\nmsgid \"Poland\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:220\nmsgid \"Portugal\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:221\nmsgid \"Puerto Rico\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:222\nmsgid \"Qatar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:223\nmsgid \"Reunion\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:224\nmsgid \"Romania\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:225\nmsgid \"Russian Federation\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:226\nmsgid \"Rwanda\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:227\nmsgid \"Saint Kitts And Nevis\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:228\nmsgid \"Saint Lucia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:229\nmsgid \"Saint Vincent And The Grenadines\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:230\nmsgid \"Samoa\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:231\nmsgid \"San Marino\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:232\nmsgid \"Sao Tome And Principe\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:233\nmsgid \"Saudi Arabia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:234\nmsgid \"Senegal\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:235\nmsgid \"Seychelles\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:236\nmsgid \"Sierra Leone\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:237\nmsgid \"Singapore\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:238\nmsgid \"Slovakia (Slovak Republic)\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:239\nmsgid \"Slovenia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:240\nmsgid \"Solomon Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:241\nmsgid \"Somalia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:242\nmsgid \"South Africa\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:243\nmsgid \"South Georgia, South Sandwich Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:244\nmsgid \"Spain\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:245\nmsgid \"Sri Lanka\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:246\nmsgid \"St. Helena\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:247\nmsgid \"St. Pierre And Miquelon\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:248\nmsgid \"Sudan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:249\nmsgid \"Suriname\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:250\nmsgid \"Svalbard And Jan Mayen Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:251\nmsgid \"Swaziland\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:252\nmsgid \"Sweden\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:253\nmsgid \"Switzerland\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:254\nmsgid \"Syrian Arab Republic\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:255\nmsgid \"Taiwan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:256\nmsgid \"Tajikistan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:257\nmsgid \"Tanzania, United Republic Of\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:258\nmsgid \"Thailand\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:259\nmsgid \"Togo\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:260\nmsgid \"Tokelau\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:261\nmsgid \"Tonga\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:262\nmsgid \"Trinidad And Tobago\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:263\nmsgid \"Tunisia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:264\nmsgid \"Turkey\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:265\nmsgid \"Turkmenistan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:266\nmsgid \"Turks And Caicos Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:267\nmsgid \"Tuvalu\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:268\nmsgid \"Uganda\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:269\nmsgid \"Ukraine\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:270\nmsgid \"United Arab Emirates\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:271\nmsgid \"United Kingdom\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:272\nmsgid \"United States\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:273\nmsgid \"United States Minor Outlying Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:274\nmsgid \"Uruguay\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:275\nmsgid \"Uzbekistan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:276\nmsgid \"Vanuatu\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:277\nmsgid \"Venezuela\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:278\nmsgid \"Viet Nam\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:279\nmsgid \"Virgin Islands (British)\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:280\nmsgid \"Virgin Islands (U.S.)\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:281\nmsgid \"Wallis And Futuna Islands\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:282\nmsgid \"Western Sahara\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:283\nmsgid \"Yemen\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:284\nmsgid \"Yugoslavia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:285\nmsgid \"Zambia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:286\nmsgid \"Zimbabwe\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:341\nmsgid \"United Arab Emirates dirham\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:342\nmsgid \"Afghan afghani\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:343\nmsgid \"Albanian lek\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:344\nmsgid \"Armenian dram\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:345\nmsgid \"Netherlands Antillean guilder\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:346\nmsgid \"Angolan kwanza\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:347\nmsgid \"Argentine peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:348\nmsgid \"Australian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:349\nmsgid \"Aruban florin\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:350\nmsgid \"Azerbaijani manat\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:351\nmsgid \"Bosnia and Herzegovina convertible mark\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:352\nmsgid \"Barbadian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:353\nmsgid \"Bangladeshi taka\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:354\nmsgid \"Bulgarian lev\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:355\nmsgid \"Bahraini dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:356\nmsgid \"Burundian franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:357\nmsgid \"Bermudian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:358\nmsgid \"Brunei dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:359\nmsgid \"Bolivian boliviano\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:360\nmsgid \"Brazilian real\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:361\nmsgid \"Bahamian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:362\nmsgid \"Bitcoin\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:363\nmsgid \"Bhutanese ngultrum\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:364\nmsgid \"Botswana pula\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:365\nmsgid \"Belarusian ruble\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:366\nmsgid \"Belize dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:367\nmsgid \"Canadian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:368\nmsgid \"Congolese franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:369\nmsgid \"Swiss franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:370\nmsgid \"Chilean peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:371\nmsgid \"Chinese yuan\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:372\nmsgid \"Colombian peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:373\nmsgid \"Costa Rican col&oacute;n\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:374\nmsgid \"Cuban convertible peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:375\nmsgid \"Cuban peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:376\nmsgid \"Cape Verdean escudo\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:377\nmsgid \"Czech koruna\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:378\nmsgid \"Djiboutian franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:379\nmsgid \"Danish krone\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:380\nmsgid \"Dominican peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:381\nmsgid \"Algerian dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:382\nmsgid \"Egyptian pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:383\nmsgid \"Eritrean nakfa\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:384\nmsgid \"Ethiopian birr\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:385\nmsgid \"Euro\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:386\nmsgid \"Fijian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:387\nmsgid \"Falkland Islands pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:388\nmsgid \"Pound sterling\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:389\nmsgid \"Georgian lari\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:390\nmsgid \"Guernsey pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:391\nmsgid \"Ghana cedi\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:392\nmsgid \"Gibraltar pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:393\nmsgid \"Gambian dalasi\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:394\nmsgid \"Guinean franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:395\nmsgid \"Guatemalan quetzal\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:396\nmsgid \"Guyanese dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:397\nmsgid \"Hong Kong dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:398\nmsgid \"Honduran lempira\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:399\nmsgid \"Croatian kuna\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:400\nmsgid \"Haitian gourde\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:401\nmsgid \"Hungarian forint\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:402\nmsgid \"Indonesian rupiah\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:403\nmsgid \"Israeli new shekel\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:404\nmsgid \"Manx pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:405\nmsgid \"Indian rupee\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:406\nmsgid \"Iraqi dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:407\nmsgid \"Iranian rial\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:408\nmsgid \"Icelandic kr&oacute;na\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:409\nmsgid \"Jersey pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:410\nmsgid \"Jamaican dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:411\nmsgid \"Jordanian dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:412\nmsgid \"Japanese yen\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:413\nmsgid \"Kenyan shilling\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:414\nmsgid \"Kyrgyzstani som\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:415\nmsgid \"Cambodian riel\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:416\nmsgid \"Comorian franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:417\nmsgid \"North Korean won\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:418\nmsgid \"South Korean won\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:419\nmsgid \"Kuwaiti dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:420\nmsgid \"Cayman Islands dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:421\nmsgid \"Kazakhstani tenge\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:422\nmsgid \"Lao kip\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:423\nmsgid \"Lebanese pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:424\nmsgid \"Sri Lankan rupee\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:425\nmsgid \"Liberian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:426\nmsgid \"Lesotho loti\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:427\nmsgid \"Libyan dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:428\nmsgid \"Moroccan dirham\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:429\nmsgid \"Moldovan leu\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:430\nmsgid \"Malagasy ariary\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:431\nmsgid \"Macedonian denar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:432\nmsgid \"Burmese kyat\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:433\nmsgid \"Mongolian t&ouml;gr&ouml;g\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:434\nmsgid \"Macanese pataca\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:435\nmsgid \"Mauritanian ouguiya\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:436\nmsgid \"Mauritian rupee\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:437\nmsgid \"Maldivian rufiyaa\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:438\nmsgid \"Malawian kwacha\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:439\nmsgid \"Mexican peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:440\nmsgid \"Malaysian ringgit\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:441\nmsgid \"Mozambican metical\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:442\nmsgid \"Namibian dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:443\nmsgid \"Nigerian naira\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:444\nmsgid \"Nicaraguan c&oacute;rdoba\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:445\nmsgid \"Norwegian krone\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:446\nmsgid \"Nepalese rupee\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:447\nmsgid \"New Zealand dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:448\nmsgid \"Omani rial\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:449\nmsgid \"Panamanian balboa\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:450\nmsgid \"Peruvian nuevo sol\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:451\nmsgid \"Papua New Guinean kina\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:452\nmsgid \"Philippine peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:453\nmsgid \"Pakistani rupee\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:454\nmsgid \"Polish z&#x142;oty\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:455\nmsgid \"Transnistrian ruble\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:456\nmsgid \"Paraguayan guaran&iacute;\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:457\nmsgid \"Qatari riyal\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:458\nmsgid \"Romanian leu\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:459\nmsgid \"Serbian dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:460\nmsgid \"Russian ruble\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:461\nmsgid \"Rwandan franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:462\nmsgid \"Saudi riyal\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:463\nmsgid \"Solomon Islands dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:464\nmsgid \"Seychellois rupee\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:465\nmsgid \"Sudanese pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:466\nmsgid \"Swedish krona\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:467\nmsgid \"Singapore dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:468\nmsgid \"Saint Helena pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:469\nmsgid \"Sierra Leonean leone\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:470\nmsgid \"Somali shilling\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:471\nmsgid \"Surinamese dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:472\nmsgid \"South Sudanese pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:473\nmsgid \"S&atilde;o Tom&eacute; and Pr&iacute;ncipe dobra\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:474\nmsgid \"Syrian pound\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:475\nmsgid \"Swazi lilangeni\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:476\nmsgid \"Thai baht\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:477\nmsgid \"Tajikistani somoni\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:478\nmsgid \"Turkmenistan manat\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:479\nmsgid \"Tunisian dinar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:480\nmsgid \"Tongan pa&#x2bb;anga\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:481\nmsgid \"Turkish lira\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:482\nmsgid \"Trinidad and Tobago dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:483\nmsgid \"New Taiwan dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:484\nmsgid \"Tanzanian shilling\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:485\nmsgid \"Ukrainian hryvnia\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:486\nmsgid \"Ugandan shilling\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:487\nmsgid \"United States dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:488\nmsgid \"Uruguayan peso\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:489\nmsgid \"Uzbekistani som\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:490\nmsgid \"Venezuelan bol&iacute;var\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:491\nmsgid \"Vietnamese &#x111;&#x1ed3;ng\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:492\nmsgid \"Vanuatu vatu\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:493\nmsgid \"Samoan t&#x101;l&#x101;\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:494\nmsgid \"Central African CFA franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:495\nmsgid \"East Caribbean dollar\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:496\nmsgid \"West African CFA franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:497\nmsgid \"CFP franc\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:498\nmsgid \"Yemeni rial\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:499\nmsgid \"South African rand\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.currency.php:500\nmsgid \"Zambian kwacha\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:203\nmsgid \"strong\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:207\nmsgid \"medium\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:211\nmsgid \"weak\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:215\nmsgid \"very weak\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:439\nmsgid \"Last Login\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:440,\n#: includes/integrations/class.llms.integration.buddypress.php:74,\n#: includes/admin/analytics/class.llms.analytics.memberships.php:19,\n#: includes/admin/analytics/class.llms.analytics.sales.php:114,\n#: includes/admin/settings/class.llms.settings.memberships.php:19,\n#: templates/admin/analytics/analytics.php:102,\n#: templates/admin/post-types/product-access-plan.php:217,\n#: templates/admin/reporting/nav-filters.php:84,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:67,\n#: includes/admin/reporting/tables/llms.table.students.php:449,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:84\nmsgid \"Memberships\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:467\nmsgid \"Never\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.person.php:507\nmsgid \"No memberships\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.privacy.php:39\nmsgid \"\"\n\"Your personal data will be used to process your enrollment, support your \"\n\"experience on this website, and for other purposes described in our \"\n\"{{policy}}.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.privacy.php:86\nmsgid \"I have read and agree to the {{terms}}.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.quiz.php:95\nmsgid \"Incomplete\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.quiz.php:96\nmsgid \"Pending Review\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.quiz.php:97\nmsgid \"Fail\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.quiz.php:98\nmsgid \"Pass\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.quiz.php:115\nmsgid \"Layout\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.templates.dashboard.php:31\nmsgid \"You are not enrolled in any courses.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.templates.dashboard.php:102\nmsgid \"You are not enrolled in any memberships.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.templates.dashboard.php:173\nmsgid \"View All My Achievements\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.templates.dashboard.php:211\nmsgid \"View All My Certificates\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.templates.dashboard.php:247\nmsgid \"View All My Courses\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.templates.dashboard.php:283\nmsgid \"View All My Memberships\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:110,\n#: includes/models/model.llms.access.plan.php:330\nmsgid \"Join\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:172\nmsgid \"Members Only\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:197\nmsgid \"One-Time Payment\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:241\nmsgid \"Subscription\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:458,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:259\nmsgid \"\"\n\"This course opens on [lifterlms_course_info id=\\\"%d\\\" key=\\\"start_date\\\"].\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:459,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:269\nmsgid \"\"\n\"This course closed on [lifterlms_course_info id=\\\"%d\\\" key=\\\"end_date\\\"].\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:472,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:336\nmsgid \"\"\n\"Enrollment has closed because the maximum number of allowed students has \"\n\"been reached.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:673\nmsgid \"\"\n\"This order was migrated to the LifterLMS 3.0 data structure. %1$sLearn more\"\n\"%2$s.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:978\nmsgid \"\"\n\"Welcome to LifterLMS 3.13.0! We've packed a ton of features into this \"\n\"release: Take a moment to get familiar with the all new %1$scourse builder\"\n\"%3$s and our new %2$suser roles%3$s.\"\nmsgstr \"\"\n\n#: includes/functions/llms.functions.updates.php:1613\nmsgid \"\"\n\"Welcome to LifterLMS 3.16.0! This update adds significant improvements to \"\n\"the quiz-building experience. Notice quizzes and questions are no longer \"\n\"found under \\\"Courses\\\" on the sidebar? Your quizzes have not been deleted \"\n\"but they have been moved! Read more about the all new %1$squiz builder%2$s.\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:33,\n#: includes/integrations/class.llms.integration.bbpress.php:104\nmsgid \"bbPress\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:34\nmsgid \"\"\n\"Restrict forums and topics to memberships, add forums to courses, and \"\n\"%1$smore%2$s.\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:111\nmsgid \"Select forums\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:113\nmsgid \"\"\n\"Add forums which will only be available to students currently enrolled in \"\n\"this course.\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:117\nmsgid \"Private Course Forums\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:167\nmsgid \"You must be enrolled in this course to access the course forum\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:347\nmsgid \"Student creates a new forum topic\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.bbpress.php:348\nmsgid \"Student creates a new forum reply\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.buddypress.php:29\nmsgid \"BuddyPress\"\nmsgstr \"\"\n\n#: includes/integrations/class.llms.integration.buddypress.php:30\nmsgid \"\"\n\"Add LifterLMS information to user profiles and enable membership \"\n\"restrictions for activity, group, and member directories. %1$sLearn More%2$s.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:95\nmsgctxt \"Access plan period\"\nmsgid \"year\"\nmsgid_plural \"years\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/models/model.llms.access.plan.php:99\nmsgctxt \"Access plan period\"\nmsgid \"month\"\nmsgid_plural \"months\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/models/model.llms.access.plan.php:103\nmsgctxt \"Access plan period\"\nmsgid \"week\"\nmsgid_plural \"weeks\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/models/model.llms.access.plan.php:107\nmsgctxt \"Access plan period\"\nmsgid \"day\"\nmsgid_plural \"days\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: includes/models/model.llms.access.plan.php:175,\n#: includes/models/model.llms.lesson.php:273\nmsgid \"FREE\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:334\nmsgid \"Buy\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:354\nmsgctxt \"Access expiration date\"\nmsgid \"access until %s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:360\nmsgctxt \"Access period description\"\nmsgid \"%1$d %2$s of access\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:390\nmsgctxt \"subscription schedule\"\nmsgid \"per %s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:398\nmsgctxt \"subscription schedule\"\nmsgid \"every %1$d %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:407\nmsgctxt \"subscription # of payments\"\nmsgid \"for %1$d total payments\"\nmsgstr \"\"\n\n#: includes/models/model.llms.access.plan.php:430\nmsgctxt \"trial offer description\"\nmsgid \"for %1$d %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.coupon.php:111,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:82\nmsgid \"Percentage Discount\"\nmsgstr \"\"\n\n#: includes/models/model.llms.coupon.php:164\nmsgctxt \"Remaining coupon uses\"\nmsgid \"Unlimited\"\nmsgstr \"\"\n\n#: includes/models/model.llms.coupon.php:251\nmsgid \"This coupon has reached its usage limit and can no longer be used.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.coupon.php:255\nmsgid \"This coupon expired on %s and can no longer be used.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.coupon.php:259,\n#: includes/models/model.llms.coupon.php:263\nmsgid \"This coupon cannot be used to purchase \\\"%s\\\".\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:165\nmsgctxt \"default order note author\"\nmsgid \"LifterLMS\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:414,\n#: templates/admin/post-types/product-access-plan.php:176\nmsgid \"Lifetime Access\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:426\nmsgid \"To be Determined\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:539\nmsgid \"Anonymous\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:555\nmsgid \"Order &ndash; %s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:555\nmsgctxt \"Order date parsed by strftime\"\nmsgid \"%b %d, %Y @ %I:%M %p\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:583,\n#: includes/models/model.llms.transaction.php:121\nmsgid \"Payment gateway %s could not be located or is no longer enabled\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:687\nmsgid \"Order is not recurring\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:689\nmsgid \"Invalid order status\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:699\nmsgid \"No more payments due\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:1264\nmsgid \"Order payment plan completed.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:1311\nmsgid \"Automatic retry attempt scheduled for %s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.order.php:1322\nmsgid \"Maximum retry attempts reached.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.quiz.attempt.php:491\nmsgid \"Quiz Attempt #%1$d by %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.quiz.attempt.php:509\nmsgid \"You must be logged in to take a quiz!\"\nmsgstr \"\"\n\n#: includes/models/model.llms.quiz.attempt.question.php:211\nmsgid \"Correct answer\"\nmsgstr \"\"\n\n#: includes/models/model.llms.quiz.attempt.question.php:214\nmsgid \"Incorrect answer\"\nmsgstr \"\"\n\n#: includes/models/model.llms.quiz.attempt.question.php:219\nmsgid \"Awaiting review\"\nmsgstr \"\"\n\n#: includes/models/model.llms.student.php:656\nmsgctxt \"course grade when no quizzes taken or in course\"\nmsgid \"N/A\"\nmsgstr \"\"\n\n#: includes/models/model.llms.student.php:677\nmsgctxt \"lesson grade when lesson has no quiz\"\nmsgid \"N/A\"\nmsgstr \"\"\n\n#: includes/models/model.llms.student.php:794\nmsgctxt \"overall grade when no quizzes\"\nmsgid \"N/A\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:71\nmsgid \"Transaction for Order #%1$d &ndash; %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:71\nmsgctxt \"Transaction date parsed by strftime\"\nmsgid \"%1$b %2$d, %Y @ %I:%M %p\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:184\nmsgid \"The selected transaction is not eligible for a refund.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:192\nmsgid \"\"\n\"Requested refund amount was %1$s, the maximum possible refund for this \"\n\"transaction is %2$s.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:201\nmsgid \"manual refund\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:209\nmsgid \"Selected gateway \\\"%s\\\" is inactive or invalid.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:212\nmsgid \"Selected gateway \\\"%s\\\" does not support refunds.\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:243\nmsgid \"Refunded %1$s for transaction #%2$d via %3$s [Refund ID: %4$s]\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:247\nmsgid \"Refund Notes: \"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:277\nmsgid \"An unknown error occurred during refund processing\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:298\nmsgid \"Single\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:300,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:141\nmsgid \"Recurring\"\nmsgstr \"\"\n\n#: includes/models/model.llms.transaction.php:302,\n#: templates/checkout/form-summary.php:14\nmsgid \"Trial\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:79,\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:163\nmsgid \"[Deleted]\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:91\nmsgid \"%1$s earned the achievement \\\"%2$s\\\"\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:97\nmsgid \"%1$s earned the certificate \\\"%2$s\\\"\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:103\nmsgid \"Email \\\"%1$s\\\" was sent to %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:109\nmsgid \"%1$s purchased the %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:116\nmsgid \"%1$s enrolled into the %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:118\nmsgid \"%1$s unenrolled from the %2$s\"\nmsgstr \"\"\n\n#: includes/models/model.llms.user.postmeta.php:125\nmsgid \"%1$s completed the %2$s\"\nmsgstr \"\"\n\n#. translators: %d = number of notifications\n#: includes/privacy/class-llms-privacy-erasers.php:128\nmsgid \"Removed %d notifications.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-erasers.php:148\nmsgid \"Order cancelled during personal data erasure.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-erasers.php:160\nmsgid \"Personal data removed during personal data erasure.\"\nmsgstr \"\"\n\n#. translators: %s Prop name.\n#: includes/privacy/class-llms-privacy-erasers.php:190\nmsgid \"Removed student \\\"%s\\\"\"\nmsgstr \"\"\n\n#. translators: %d Order number.\n#: includes/privacy/class-llms-privacy-erasers.php:248\nmsgid \"Removed personal data from order #%d.\"\nmsgstr \"\"\n\n#. translators: %d Order number.\n#: includes/privacy/class-llms-privacy-erasers.php:254\nmsgid \"Personal data within order #%d has been retained.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-erasers.php:294\nmsgid \"Removed all student course and membership enrollment and activity data.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-erasers.php:299\nmsgid \"\"\n\"Retained all student course and membership enrollment and activity data.\"\nmsgstr \"\"\n\n#. translators: %d quiz attempt id.\n#: includes/privacy/class-llms-privacy-erasers.php:333\nmsgid \"Quiz attempt #%d removed.\"\nmsgstr \"\"\n\n#. translators: %d quiz attempt id.\n#: includes/privacy/class-llms-privacy-erasers.php:341\nmsgid \"Quiz attempt #%d retained.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:112,\n#: includes/admin/reporting/tables/llms.table.achievements.php:154,\n#: includes/admin/views/builder/question.php:78\nmsgid \"Image\"\nmsgstr \"\"\n\n#. translators: %s = post type singular name label (Course or Membership)\n#: includes/privacy/class-llms-privacy-exporters.php:169\nmsgid \"%s Title\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:174\nmsgid \"Enrollment Status\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:179\nmsgid \"Enrollment Date\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:186,\n#: templates/admin/reporting/tabs/students/courses-course.php:59\nmsgid \"Last Activity\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:195,\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:364,\n#: includes/admin/reporting/tables/llms.table.course.students.php:408,\n#: includes/admin/reporting/tables/llms.table.student.courses.php:209,\n#: includes/admin/reporting/tables/llms.table.students.php:424,\n#: templates/admin/reporting/tabs/students/courses-course.php:33\nmsgid \"Progress\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:204,\n#: includes/privacy/class-llms-privacy-exporters.php:292,\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:80,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:80,\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:368,\n#: includes/admin/reporting/tables/llms.table.course.students.php:413,\n#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:263,\n#: includes/admin/reporting/tables/llms.table.student.course.php:191,\n#: includes/admin/reporting/tables/llms.table.student.courses.php:206,\n#: includes/admin/reporting/tables/llms.table.students.php:429,\n#: templates/admin/reporting/tabs/quizzes/attempt.php:34\nmsgid \"Grade\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:276\nmsgid \"Attempt ID\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:281\nmsgid \"Attempt Number\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:512\nmsgid \"Personal Information\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy-exporters.php:542,\n#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:164\nmsgid \"Quiz Attempts\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:30,\n#: includes/privacy/class-llms-privacy.php:41\nmsgid \"Student Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:31\nmsgid \"Course Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:32,\n#: includes/privacy/class-llms-privacy.php:42\nmsgid \"Quiz Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:33\nmsgid \"Membership Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:34,\n#: includes/privacy/class-llms-privacy.php:43,\n#: includes/privacy/class-llms-privacy.php:45\nmsgid \"Order Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:35,\n#: includes/privacy/class-llms-privacy.php:44\nmsgid \"Achievement Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:36\nmsgid \"Certificate Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:46\nmsgid \"Notification Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:48\nmsgid \"Postmeta Data\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:87\nmsgid \"Order Number\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:88,\n#: includes/admin/views/metaboxes/view-order-submit.php:31\nmsgid \"Order Date\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:89,\n#: templates/myaccount/view-order.php:47,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:44\nmsgid \"Product\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:90,\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:80,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:80,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:41\nmsgid \"Plan\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:94\nmsgid \"User ID\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:99\nmsgid \"Billing First Name\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:100\nmsgid \"Billing Last Name\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:101\nmsgid \"Billing Email\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:109,\n#: includes/privacy/class-llms-privacy.php:208\nmsgid \"IP Address\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:127\nmsgid \"\"\n\"This sample language includes the basics around what personal data your \"\n\"learning platform may be collecting, storing and sharing, as well as who may \"\n\"have access to that data. Depending on what settings are enabled and which \"\n\"additional add-ons are used, the specific information shared by your site \"\n\"will vary. We recommend consulting with a lawyer when deciding what \"\n\"information to disclose on your privacy policy.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:130\nmsgid \"\"\n\"We collect information about you during the registration, enrollment, and \"\n\"checkout processes on our site.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:131\nmsgid \"What we collect and store\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:132\nmsgid \"\"\n\"When you register an account with us, we’ll ask you to provide information \"\n\"including your name, billing address, email address, phone number, credit \"\n\"card/payment details and optional account information like username and \"\n\"password. We’ll use this information for purposes, such as, to:\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:134\nmsgid \"\"\n\"Send you information about your account, orders, courses, and memberships\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:135\nmsgid \"\"\n\"Communicate with you about courses and memberships that you’re enrolled in\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:136\nmsgid \"Respond to your requests, including refunds and complaints\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:137\nmsgid \"Process payments and prevent fraud\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:138\nmsgid \"Set up your account for our site\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:139\nmsgid \"Comply with any legal obligations we have\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:140\nmsgid \"Improve our site’s offerings\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:141\nmsgid \"Send you marketing messages, if you choose to receive them\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:143\nmsgid \"\"\n\"When you create an account, we will store your name, address, email and \"\n\"phone number, which will be used to populate the enrollment and checkout for \"\n\"future purchases and enrollments.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:144\nmsgid \"\"\n\"We generally store information about you for as long as we need the \"\n\"information for the purposes for which we collect and use it, and we are not \"\n\"legally required to continue to keep it. For example, we will store order \"\n\"information for XXX years for tax and accounting purposes. This includes \"\n\"your name, email address and billing address.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:145\nmsgid \"We will also store comments or reviews, if you chose to leave them.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:146\nmsgid \"Who on our team has access\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:147\nmsgid \"\"\n\"Members of our team have access to the information you provide us. For \"\n\"example, both Administrators and Site Managers can access:\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:149\nmsgid \"\"\n\"Order information like what was purchased, when it was purchased and where \"\n\"it should be sent, and\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:150\nmsgid \"\"\n\"Customer information like your name, email address, and billing information.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:152\nmsgid \"\"\n\"Course and membership instructors can access your course progress and \"\n\"activities including:\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:154\nmsgid \"Enrollment dates for their courses and memberships\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:155\nmsgid \"Course progress and status information for their courses\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:156\nmsgid \"Quiz and assignments answers and grades for their courses\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:157\nmsgid \"Comments and reviews made on their memberships and courses\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:159\nmsgid \"\"\n\"Our team members have access to this information to help fulfill orders, \"\n\"process refunds, and support you.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:160\nmsgid \"What we share with others\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:162\nmsgid \"\"\n\"In this section you should list who you’re sharing data with, and for what \"\n\"purpose. This could include, but may not be limited to, analytics, \"\n\"marketing, payment gateways, and third party embeds.\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:164\nmsgid \"\"\n\"We share information with third parties who help us provide our orders and \"\n\"store services to you; for example --\"\nmsgstr \"\"\n\n#: includes/privacy/class-llms-privacy.php:209\nmsgid \"Last Login Date\"\nmsgstr \"\"\n\n#: includes/processors/class.llms.processor.table.to.csv.php:210\nmsgid \"Your %1$s export file from %2$s\"\nmsgstr \"\"\n\n#: includes/processors/class.llms.processor.table.to.csv.php:211\nmsgid \"Please find the attached CSV file.\"\nmsgstr \"\"\n\n#: includes/shortcodes/class.llms.shortcode.checkout.php:44\nmsgid \"\"\n\"You are currently logged in as <em>%1$s</em>. <a href=\\\"%2$s\\\">Click here to \"\n\"logout</a>\"\nmsgstr \"\"\n\n#: includes/shortcodes/class.llms.shortcode.checkout.php:46\nmsgid \"Already have an account? <a href=\\\"%s\\\">Click here to login</a>\"\nmsgstr \"\"\n\n#: includes/shortcodes/class.llms.shortcode.checkout.php:162\nmsgid \"Invalid access plan.\"\nmsgstr \"\"\n\n#: includes/shortcodes/class.llms.shortcode.checkout.php:191\nmsgid \"\"\n\"Your cart is currently empty. Click <a href=\\\"%s\\\">here</a> to get started.\"\nmsgstr \"\"\n\n#: includes/shortcodes/class.llms.shortcode.courses.php:164\nmsgid \"\"\n\"You must be logged in to view this information. Click %1$shere%2$s to login.\"\nmsgstr \"\"\n\n#: includes/shortcodes/class.llms.shortcodes.php:289,\n#: templates/loop/pagination.php:23\nmsgid \"Previous\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.bbp.widget.course.forums.list.php:22\nmsgid \"Displays a list of bbPress forums associated with the course.\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.bbp.widget.course.forums.list.php:25\nmsgid \"LifterLMS Course Forums List\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.bbp.widget.course.forums.list.php:75\nmsgid \"Course Forums\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.progress.php:18\nmsgid \"Course Progress\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.progress.php:20\nmsgid \"Displays Course Progress on Course or Lesson\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.syllabus.php:18\nmsgid \"Course Syllabus\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.syllabus.php:20\nmsgid \"Displays All Course lessons on Course or Lesson page\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.syllabus.php:45\nmsgid \"Make outline collapsible?\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.syllabus.php:46\nmsgid \"\"\n\"Allow students to hide lessons within a section by clicking the section \"\n\"titles.\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.syllabus.php:53\nmsgid \"Display open and close all toggles\"\nmsgstr \"\"\n\n#: includes/widgets/class.llms.widget.course.syllabus.php:54\nmsgid \"\"\n\"Display \\\"Open All\\\" and \\\"Close All\\\" toggles at the bottom of the outline.\"\nmsgstr \"\"\n\n#: templates/achievements/loop.php:30\nmsgid \"\"\n\"You do not have any achievements yet. Enroll in a course to get started!\"\nmsgstr \"\"\n\n#: templates/achievements/template.php:22\nmsgctxt \"achievement earned date\"\nmsgid \"Awarded on %s\"\nmsgstr \"\"\n\n#: templates/admin/user-edit.php:22\nmsgid \"required\"\nmsgstr \"\"\n\n#: templates/certificates/loop.php:30\nmsgid \"You do not have any certificates yet.\"\nmsgstr \"\"\n\n#: templates/checkout/form-checkout.php:26,\n#: templates/checkout/form-confirm-payment.php:19\nmsgid \"Billing Information\"\nmsgstr \"\"\n\n#: templates/checkout/form-checkout.php:28,\n#: templates/admin/reporting/tabs/students/information.php:18\nmsgid \"Student Information\"\nmsgstr \"\"\n\n#: templates/checkout/form-checkout.php:50,\n#: templates/checkout/form-confirm-payment.php:37\nmsgid \"Order Summary\"\nmsgstr \"\"\n\n#: templates/checkout/form-checkout.php:74,\n#: templates/checkout/form-confirm-payment.php:53\nmsgid \"Payment Details\"\nmsgstr \"\"\n\n#: templates/checkout/form-checkout.php:76\nmsgid \"Enrollment Confirmation\"\nmsgstr \"\"\n\n#: templates/checkout/form-checkout.php:107\nmsgid \"Buy Now\"\nmsgstr \"\"\n\n#: templates/checkout/form-checkout.php:107\nmsgid \"Enroll Now\"\nmsgstr \"\"\n\n#: templates/checkout/form-confirm-payment.php:58,\n#: templates/checkout/form-switch-source.php:59\nmsgid \"Payment Method:\"\nmsgstr \"\"\n\n#: templates/checkout/form-confirm-payment.php:74,\n#: includes/admin/settings/class.llms.settings.checkout.php:184\nmsgid \"Confirm Payment\"\nmsgstr \"\"\n\n#: templates/checkout/form-confirm-payment.php:131\nmsgid \"The order for this transaction could not be located.\"\nmsgstr \"\"\n\n#: templates/checkout/form-confirm-payment.php:144,\n#: templates/checkout/form-confirm-payment.php:187\nmsgid \"Confirm Purchase\"\nmsgstr \"\"\n\n#: templates/checkout/form-confirm-payment.php:159\nmsgid \"Payment Terms:\"\nmsgstr \"\"\n\n#: templates/checkout/form-confirm-payment.php:164\nmsgid \"Price:\"\nmsgstr \"\"\n\n#: templates/checkout/form-confirm-payment.php:177,\n#: templates/myaccount/view-order.php:121,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:43\nmsgid \"Payment Method\"\nmsgstr \"\"\n\n#: templates/checkout/form-coupon.php:19\nmsgid \"Have a coupon?\"\nmsgstr \"\"\n\n#: templates/checkout/form-coupon.php:20\nmsgid \"Click here to enter your code\"\nmsgstr \"\"\n\n#: templates/checkout/form-coupon.php:30\nmsgid \"Coupon Code\"\nmsgstr \"\"\n\n#: templates/checkout/form-coupon.php:41\nmsgid \"Apply Coupon\"\nmsgstr \"\"\n\n#: templates/checkout/form-coupon.php:51\nmsgid \"Coupon code \\\"%s\\\" has been applied to your order.\"\nmsgstr \"\"\n\n#: templates/checkout/form-coupon.php:59\nmsgid \"Remove Coupon\"\nmsgstr \"\"\n\n#: templates/checkout/form-gateways.php:28\nmsgid \"Payment processing is currently disabled.\"\nmsgstr \"\"\n\n#: templates/checkout/form-gateways.php:62\nmsgid \"\"\n\"There are no gateways enabled which can support the necessary transaction \"\n\"type for this access plan.\"\nmsgstr \"\"\n\n#: templates/checkout/form-summary.php:39\nmsgid \"Access\"\nmsgstr \"\"\n\n#: templates/checkout/form-switch-source.php:17\nmsgid \"Save Payment Method\"\nmsgstr \"\"\n\n#: templates/checkout/form-switch-source.php:19,\n#: templates/checkout/form-switch-source.php:31\nmsgid \"Reactivate Subscription\"\nmsgstr \"\"\n\n#: templates/checkout/form-switch-source.php:21\nmsgid \"Save and Pay Now\"\nmsgstr \"\"\n\n#: templates/checkout/form-switch-source.php:31,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:97,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:110\nmsgid \"Update Payment Method\"\nmsgstr \"\"\n\n#: templates/checkout/form-switch-source.php:50\nmsgid \"Due Now: %s\"\nmsgstr \"\"\n\n#: templates/course/author.php:18\nmsgid \"Course Instructor\"\nmsgid_plural \"Course Instructors\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: templates/course/categories.php:14\nmsgid \"Categories: \"\nmsgstr \"\"\n\n#: templates/course/complete-lesson-link.php:42,\n#: includes/notifications/controllers/class.llms.notification.controller.lesson.complete.php:90\nmsgid \"Lesson Complete\"\nmsgstr \"\"\n\n#: templates/course/complete-lesson-link.php:59\nmsgid \"Mark Incomplete\"\nmsgstr \"\"\n\n#: templates/course/complete-lesson-link.php:90\nmsgid \"Mark Complete\"\nmsgstr \"\"\n\n#: templates/course/complete-lesson-link.php:110\nmsgid \"Take Quiz\"\nmsgstr \"\"\n\n#: templates/course/difficulty.php:20\nmsgid \"Difficulty: <span class=\\\"difficulty\\\">%s</span>\"\nmsgstr \"\"\n\n#: templates/course/length.php:18\nmsgid \"Estimated Time: <span class=\\\"length\\\">%s</span>\"\nmsgstr \"\"\n\n#: templates/course/lesson-navigation.php:24,\n#: templates/course/lesson-navigation.php:68,\n#: templates/course/lesson-navigation.php:69\nmsgid \"Previous Lesson\"\nmsgstr \"\"\n\n#: templates/course/lesson-navigation.php:35,\n#: templates/course/lesson-navigation.php:84,\n#: templates/course/lesson-navigation.php:85\nmsgid \"Back to Course\"\nmsgstr \"\"\n\n#: templates/course/lesson-navigation.php:48,\n#: templates/course/lesson-navigation.php:97,\n#: templates/course/lesson-navigation.php:98,\n#: templates/quiz/start-button.php:41\nmsgid \"Next Lesson\"\nmsgstr \"\"\n\n#: templates/course/lesson-preview.php:29\nmsgctxt \"lesson order within section\"\nmsgid \"%1$d of %2$d\"\nmsgstr \"\"\n\n#: templates/course/meta-wrapper-start.php:12\nmsgctxt \"course meta info title\"\nmsgid \"Course Information\"\nmsgstr \"\"\n\n#: templates/course/outline-list-small.php:95\nmsgid \"Open All\"\nmsgstr \"\"\n\n#: templates/course/outline-list-small.php:97\nmsgid \"Close All\"\nmsgstr \"\"\n\n#: templates/course/parent-course.php:14\nmsgid \"\"\n\"<p class=\\\"llms-parent-course-link\\\">Back to: <a class=\\\"llms-lesson-link\\\" \"\n\"href=\\\"%1$s\\\">%2$s</a></p>\"\nmsgstr \"\"\n\n#: templates/course/prerequisites.php:19\nmsgid \"\"\n\"Before starting this course you must complete the required prerequisite \"\n\"course: %s\"\nmsgstr \"\"\n\n#: templates/course/prerequisites.php:26\nmsgid \"\"\n\"Before starting this course you must complete the required prerequisite \"\n\"track: %s\"\nmsgstr \"\"\n\n#: templates/course/syllabus.php:27\nmsgid \"This course does not have any sections.\"\nmsgstr \"\"\n\n#: templates/course/syllabus.php:52\nmsgid \"This section does not have any lessons.\"\nmsgstr \"\"\n\n#: templates/course/tags.php:14\nmsgid \"Tags: \"\nmsgstr \"\"\n\n#: templates/course/tracks.php:14\nmsgid \"Tracks: \"\nmsgstr \"\"\n\n#: templates/emails/reset-password.php:10\nmsgid \"Someone recently requested that the password be reset for %s.\"\nmsgstr \"\"\n\n#: templates/emails/reset-password.php:12\nmsgid \"To reset your password, click on the button below:\"\nmsgstr \"\"\n\n#: templates/emails/reset-password.php:16\nmsgid \"\"\n\"If this was a mistake you can ignore this email and your password will not \"\n\"be changed.\"\nmsgstr \"\"\n\n#: templates/emails/reset-password.php:20\nmsgid \"Trouble clicking? Copy and paste this URL into your browser:\"\nmsgstr \"\"\n\n#: templates/global/form-registration.php:23,\n#: templates/global/form-registration.php:58\nmsgid \"Register\"\nmsgstr \"\"\n\n#: templates/loop/enroll-date.php:18\nmsgid \"Enrolled: %s\"\nmsgstr \"\"\n\n#: templates/loop/enroll-status.php:18\nmsgid \"Status: %s\"\nmsgstr \"\"\n\n#: templates/loop/none-found.php:4\nmsgid \"No products were found matching your selection.\"\nmsgstr \"\"\n\n#: templates/loop/view-link.php:35\nmsgid \"Learn More\"\nmsgstr \"\"\n\n#: templates/membership/price.php:25\nmsgid \"or\"\nmsgstr \"\"\n\n#: templates/myaccount/form-redeem-voucher.php:22\nmsgctxt \"Voucher Code\"\nmsgid \"Submit\"\nmsgstr \"\"\n\n#: templates/myaccount/my-notifications.php:24\nmsgid \"You have no notifications.\"\nmsgstr \"\"\n\n#: templates/myaccount/my-orders.php:13\nmsgid \"No orders found.\"\nmsgstr \"\"\n\n#: templates/myaccount/my-orders.php:21, templates/myaccount/my-orders.php:34\nmsgid \"Expires\"\nmsgstr \"\"\n\n#: templates/myaccount/my-orders.php:22, templates/myaccount/my-orders.php:41\nmsgid \"Next Payment\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order-transactions.php:19,\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:81,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:81,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:42,\n#: templates/admin/post-types/order-transactions.php:20\nmsgid \"Amount\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order-transactions.php:20,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:125\nmsgid \"Method\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:22\nmsgid \"Invalid Order\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:26,\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:159\nmsgid \"Order #%d\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:54, templates/myaccount/view-order.php:79\nmsgid \"Original Total\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:59, templates/myaccount/view-order.php:95\nmsgid \"Coupon Discount\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:69\nmsgid \"Trial Total\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:72,\n#: templates/myaccount/view-order.php:112,\n#: templates/admin/post-types/order-details.php:109,\n#: templates/admin/post-types/order-details.php:146\nmsgid \"for %1$d %2$s\"\nmsgid_plural \"for %1$d %2$ss\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: templates/myaccount/view-order.php:85\nmsgid \"Sale Discount\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:106\nmsgid \"Total\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:110,\n#: templates/admin/post-types/order-details.php:144\nmsgid \"Every %2$s\"\nmsgid_plural \"Every %1$d %2$ss\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: templates/myaccount/view-order.php:115,\n#: templates/admin/post-types/order-details.php:149,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:143\nmsgid \"One-time\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:133,\n#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:268,\n#: templates/admin/reporting/tabs/quizzes/attempt.php:85\nmsgid \"Start Date\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:138\nmsgid \"Last Payment Date\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:144,\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:134,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:134,\n#: includes/admin/views/metaboxes/view-order-submit.php:57\nmsgid \"Next Payment Date\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:158,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:37\nmsgid \"Expiration Date\"\nmsgstr \"\"\n\n#: templates/myaccount/view-order.php:189\nmsgid \"Cancel Subscription\"\nmsgstr \"\"\n\n#: templates/product/pricing-table.php:32\nmsgid \"FEATURED\"\nmsgstr \"\"\n\n#: templates/product/pricing-table.php:47\nmsgid \"SALE\"\nmsgstr \"\"\n\n#: templates/product/pricing-table.php:68\nmsgid \"sale ends %s\"\nmsgstr \"\"\n\n#: templates/product/pricing-table.php:75\nmsgid \"MEMBER PRICING\"\nmsgstr \"\"\n\n#: templates/product/pricing-table.php:93\nmsgid \"TRIAL\"\nmsgstr \"\"\n\n#: templates/quiz/meta-information.php:17\nmsgid \"Quiz Information\"\nmsgstr \"\"\n\n#: templates/quiz/meta-information.php:21\nmsgid \"Minimum Passing Grade: %s\"\nmsgstr \"\"\n\n#: templates/quiz/meta-information.php:26\nmsgid \"Remaining Attempts: %s\"\nmsgstr \"\"\n\n#: templates/quiz/meta-information.php:30\nmsgid \"Questions: %s\"\nmsgstr \"\"\n\n#: templates/quiz/meta-information.php:35\nmsgid \"Time Limit: %s\"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt-questions-list.php:34\nmsgid \"%1$d / %2$d points\"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt-questions-list.php:53\nmsgid \"Selected answer: \"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt-questions-list.php:62\nmsgid \"Correct answer: \"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt-questions-list.php:70\nmsgid \"Clarification: \"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt-questions-list.php:79\nmsgid \"Instructor remarks: \"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt.php:20\nmsgid \"Attempt #%d Results\"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt.php:25\nmsgid \"Correct Answers: %1$d / %2$d\"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt.php:26,\n#: templates/admin/reporting/tabs/students/courses-course.php:72\nmsgid \"Completed: %s\"\nmsgstr \"\"\n\n#: templates/quiz/results-attempt.php:27\nmsgid \"Total time: %s\"\nmsgstr \"\"\n\n#: templates/quiz/results.php:46\nmsgid \"View Previous Attempts\"\nmsgstr \"\"\n\n#: templates/quiz/results.php:48\nmsgid \"Select an Attempt\"\nmsgstr \"\"\n\n#. translators: 1: attempt number; 2: grade percentage; 3: pass/fail text\n#: templates/quiz/results.php:52\nmsgid \"Attempt #%1$d - %2$s (%3$s)\"\nmsgstr \"\"\n\n#: templates/quiz/return-to-lesson.php:19\nmsgid \"Return to Lesson\"\nmsgstr \"\"\n\n#: templates/quiz/start-button.php:33\nmsgid \"Start Quiz\"\nmsgstr \"\"\n\n#: templates/quiz/start-button.php:37, templates/quiz/start-button.php:47\nmsgid \"You are not able take this quiz\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:39\nmsgid \"Course Analytics\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:95,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:63\nmsgid \"Select a Course\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:99,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:38\nmsgid \"All Courses\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:118,\n#: includes/admin/analytics/class.llms.analytics.memberships.php:115,\n#: includes/admin/analytics/class.llms.analytics.sales.php:132\nmsgid \"Filter Date Range\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:134,\n#: includes/admin/analytics/class.llms.analytics.memberships.php:131,\n#: includes/admin/analytics/class.llms.analytics.sales.php:148\nmsgid \"Start date\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:140,\n#: includes/admin/analytics/class.llms.analytics.memberships.php:137,\n#: includes/admin/analytics/class.llms.analytics.sales.php:154\nmsgid \"End date\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:180,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:22\nmsgid \"Student Enrollment\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:204,\n#: includes/admin/analytics/class.llms.analytics.memberships.php:201\nmsgid \"Lesson Completion Percentage\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:222\nmsgid \"Enrolled Students\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:240\nmsgid \"All Students\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:253\nmsgid \"Current Students\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:266\nmsgid \"Completion %\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.courses.php:279\nmsgid \"Certificates Issued\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:36\nmsgid \"Membership Analytics\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:93,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:86\nmsgid \"Select a Membership\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:97\nmsgid \"All Memberships\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:183\nmsgid \"Membership Enrollment by Day\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:219\nmsgid \"Members\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:237\nmsgid \"All Members\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:250\nmsgid \"Current Members\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:263\nmsgid \"Retention %\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.memberships.php:277\nmsgid \"Expired Members\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:19,\n#: includes/admin/reporting/class.llms.admin.reporting.php:252\nmsgid \"Sales\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:37\nmsgid \"Sales Analytics\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:93\nmsgid \"Select a product\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:97\nmsgid \"All Products\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:206\nmsgid \"Sales Volume\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:222\nmsgid \"Total Sold\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:235\nmsgid \"Total Sales\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:248\nmsgid \"Coupons Used\"\nmsgstr \"\"\n\n#: includes/admin/analytics/class.llms.analytics.sales.php:261\nmsgid \"Total Coupons\"\nmsgstr \"\"\n\n#: includes/admin/post-types/class.llms.meta.boxes.php:144\nmsgid \"Export CSV\"\nmsgstr \"\"\n\n#: includes/admin/post-types/class.llms.post.tables.php:77\nmsgid \"Missing post ID.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/class.llms.post.tables.php:83\nmsgid \"Invalid post ID.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/class.llms.post.tables.php:87\nmsgid \"Action cannot be executed on the current post.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/class.llms.post.tables.php:127,\n#: includes/admin/post-types/class.llms.post.tables.php:129\nmsgid \"Filter by %s\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:194\nmsgid \"Today\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:195\nmsgid \"Yesterday\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:196\nmsgid \"This Week\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:197\nmsgid \"Last Week\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:198,\n#: templates/admin/analytics/analytics.php:41,\n#: templates/admin/reporting/nav-filters.php:23\nmsgid \"This Month\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:199,\n#: templates/admin/analytics/analytics.php:37,\n#: templates/admin/reporting/nav-filters.php:19\nmsgid \"Last Month\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:200,\n#: templates/admin/analytics/analytics.php:33,\n#: templates/admin/reporting/nav-filters.php:15\nmsgid \"This Year\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:201\nmsgid \"Last Year\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:202\nmsgid \"All Time\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:249,\n#: templates/admin/analytics/analytics.php:71,\n#: templates/admin/reporting/nav-filters.php:53,\n#: includes/admin/reporting/tables/llms.table.course.students.php:252,\n#: includes/admin/reporting/tables/llms.table.courses.php:286,\n#: includes/admin/reporting/tables/llms.table.students.php:259,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.courses.php:91,\n#: templates/admin/reporting/tabs/students/student.php:13\nmsgid \"Students\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:253,\n#: includes/admin/settings/class.llms.settings.general.php:174,\n#: includes/admin/reporting/tables/llms.table.students.php:433,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:60\nmsgid \"Enrollments\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:331\nmsgid \"You don't have permission to do that\"\nmsgstr \"\"\n\n#: includes/admin/reporting/class.llms.admin.reporting.php:434\nmsgid \"Previously %s\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:26\nmsgid \"Accounts\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:42\nmsgid \"Required\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:43\nmsgid \"Optional\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:62\nmsgid \"Dashboard Page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:76\nmsgid \"Courses Sorting\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:78\nmsgid \"\"\n\"Determines the order of the courses in-progress listed on the student \"\n\"dashboard.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:82\nmsgid \"Course Title (A to Z)\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:83\nmsgid \"Course Title (Z to A)\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:84\nmsgid \"Enrollment Date (Most Recent to Least Recent)\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:85,\n#: includes/admin/settings/class.llms.settings.courses.php:101,\n#: includes/admin/settings/class.llms.settings.memberships.php:110\nmsgid \"Order (Low to High)\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:86\nmsgid \"Order (High to Low)\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:103\nmsgid \"Student Dashboard Endpoints\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:104\nmsgid \"\"\n\"Each endpoint allows students to view more information or manage parts of \"\n\"their account. Each endpoint should be unique, URL-safe, and can be left \"\n\"blank to disable the endpoint completely.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:109\nmsgid \"View Courses\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:110\nmsgid \"List of all the student's courses\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:118\nmsgid \"View Memberships\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:119\nmsgid \"List of all the student's memberships\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:127\nmsgid \"View Achievements\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:128\nmsgid \"List of all the student's achievements\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:136\nmsgid \"View Certificates\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:137\nmsgid \"List of all the student's certificates\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:146\nmsgid \"View Notifications and adjust notification settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:155\nmsgid \"Edit Account page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:163\nmsgid \"Lost Password\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:164\nmsgid \"Lost Password page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:172\nmsgid \"Redeem Vouchers\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:173\nmsgid \"Redeem vouchers page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:181\nmsgid \"Orders History\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:182\nmsgid \"Students can review order history on this page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:200\nmsgid \"User Information Options\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:206\nmsgid \"These settings apply to all user information screens.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:207\nmsgid \"General Information Field Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:211\nmsgid \"Disable Usernames\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:212\nmsgid \"\"\n\"Automatically generate student usernames and enable email address login.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:218\nmsgid \"Password Strength\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:219\nmsgid \"Add a password strength meter\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:226\nmsgid \"\"\n\"Select the minimum password strength allowed when students create a new \"\n\"password.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:231\nmsgctxt \"password strength meter\"\nmsgid \"Weak\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:232\nmsgctxt \"password strength meter\"\nmsgid \"Medium\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:233\nmsgctxt \"password strength meter\"\nmsgid \"Strong\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:237\nmsgid \"Terms and Conditions\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:244\nmsgid \"\"\n\"When enabled users must agree to your site's Terms and Conditions to \"\n\"register for an account.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:254\nmsgid \"Select a page where your site's Terms and Conditions are described.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:259\nmsgid \"Terms and Conditions Page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:263,\n#: includes/admin/settings/class.llms.settings.accounts.php:294,\n#: includes/admin/settings/class.llms.settings.courses.php:79,\n#: includes/admin/settings/class.llms.settings.memberships.php:86,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:95,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:123\nmsgid \"Select a page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:271\nmsgid \"\"\n\"Customize the text used to display the Terms and Conditions checkbox that \"\n\"students must accept.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:272\nmsgid \"Terms and Conditions Notice\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:278\nmsgid \"Privacy Policy\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:284\nmsgid \"\"\n\"Select a page where your site's Privacy Policy is described. See \"\n\"%1$sWordPress Privacy Settings%2$s for more information\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:290\nmsgid \"Privacy Policy Page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:302\nmsgid \"\"\n\"Optionally display a privacy policy notice during registration and checkout.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:303\nmsgid \"Privacy Policy Notice\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:308\nmsgid \"Account Erasure Requests\"\nmsgstr \"\"\n\n#. translators: %$1s = opening anchor to account erasure screen; %2$s closing anchor\n#: includes/admin/settings/class.llms.settings.accounts.php:310\nmsgid \"Customize data retention during %1$saccount erasure requests%2$s.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:317\nmsgid \"When enabled orders will be anonymized during a personal data erasure.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:318\nmsgid \"Remove Order Data\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:325\nmsgid \"\"\n\"When enabled all student data related to course and membership activities \"\n\"will be removed.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:326\nmsgid \"Remove Student LMS Data\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:331\nmsgid \"Customize the user information fields available on the checkout screen.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:332\nmsgid \"Checkout Fields\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:339,\n#: includes/admin/settings/class.llms.settings.accounts.php:384,\n#: includes/admin/settings/class.llms.settings.accounts.php:433\nmsgid \"First & Last Name\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:347,\n#: includes/admin/settings/class.llms.settings.accounts.php:392,\n#: includes/admin/settings/class.llms.settings.accounts.php:441\nmsgid \"Address\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:362,\n#: includes/admin/settings/class.llms.settings.accounts.php:407,\n#: includes/admin/settings/class.llms.settings.accounts.php:456\nmsgid \"Add an email confirmation field\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:364,\n#: includes/admin/settings/class.llms.settings.accounts.php:409,\n#: includes/admin/settings/class.llms.settings.accounts.php:458\nmsgid \"Email Confirmation\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:369\nmsgid \"\"\n\"Customize the user information fields available on the open registration \"\n\"screen.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:370\nmsgid \"Open Registration Fields\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:375\nmsgid \"\"\n\"Allow registration on the Account Access Page without enrolling in a course \"\n\"or membership.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:415\nmsgid \"If required, users can only use open registration with a voucher.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:416\nmsgid \"If optional, users may redeem a voucher during open registration.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:417\nmsgid \"If hidden, users can only redeem vouchers on their dashboard.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:425\nmsgid \"\"\n\"Customize the user information fields available on the account update screen.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.accounts.php:426\nmsgid \"Account Update Fields\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:58,\n#: templates/admin/post-types/order-transactions.php:23\nmsgid \"Gateway\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:59\nmsgid \"Gateway ID\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:60,\n#: includes/admin/settings/class.llms.settings.checkout.php:74,\n#: includes/admin/settings/class.llms.settings.checkout.php:75,\n#: includes/admin/settings/class.llms.settings.general.php:93,\n#: includes/admin/settings/class.llms.settings.integrations.php:123,\n#: includes/admin/settings/class.llms.settings.integrations.php:147,\n#: includes/admin/settings/class.llms.settings.integrations.php:148\nmsgid \"Enabled\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:111,\n#: includes/admin/settings/class.llms.settings.checkout.php:166\nmsgid \"Checkout Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:171\nmsgid \"Checkout Page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:172\nmsgid \"Page used for displaying the checkout form.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:185\nmsgid \"Payment confirmation endpoint slug\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:194\nmsgid \"Force SSL\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:195\nmsgid \"Force secure checkout via SSL (https) on the checkout page(s).\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:196\nmsgid \"Requires an SSL certificate. %1$sLearn More%2$s\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:204\nmsgid \"Enable automatic retry of failed recurring payments.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:205\nmsgid \"\"\n\"Recover lost revenue from temporarily declined payment methods. %1$sLearn \"\n\"more%2$s.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:207\nmsgid \"Retry Failed Payments\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:222\nmsgid \"Currency Options\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:224\nmsgid \"The following options affect how prices are displayed on the frontend.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:231\nmsgid \"\"\n\"Select the country LifterLMS should use as the default during transactions \"\n\"and registrations.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:241\nmsgid \"Currency\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:242\nmsgid \"\"\n\"Select the currency LifterLMS should use to display prices and process \"\n\"transactions.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:251\nmsgid \"Currency Position\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:252\nmsgid \"\"\n\"Customize the position and formatting of the currency symbol for displayed \"\n\"prices.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:265\nmsgid \"Thousand Separator\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:267\nmsgid \"\"\n\"Choose the character to display as the thousand's place separator for \"\n\"displayed prices.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:274\nmsgid \"Decimal Separator\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:276\nmsgid \"\"\n\"Choose the character to display as the decimal separator for displayed \"\n\"prices.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:283\nmsgid \"Decimal Places\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:285\nmsgid \"Customize the number of decimal places for prices.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:292\nmsgid \"Hide Zero Decimals\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:293\nmsgid \"Automatically remove zero decimals from the end of displayed prices.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:351\nmsgid \"Payment Gateway Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:357\nmsgid \"Error: \\\"%s\\\" is not a valid payment gateway\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.checkout.php:365\nmsgid \"%s Payment Gateway Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:45\nmsgid \"Course Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:50\nmsgid \"\"\n\"Enabling this setting allows students to mark a lesson as \\\"incomplete\\\" \"\n\"after they have completed a lesson.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:53\nmsgid \"Retake Lessons\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:70\nmsgid \"Course Catalog Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:81\nmsgid \"\"\n\"This page is where your visitors will find a list of all your available \"\n\"courses. %1$sMore information%2$s.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:90\nmsgid \"To show all courses on one page, enter -1\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:92\nmsgid \"Courses per page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:98\nmsgid \"Determines the display order for courses on the courses page.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:102,\n#: includes/admin/settings/class.llms.settings.memberships.php:111\nmsgid \"Title (A - Z)\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:103,\n#: includes/admin/settings/class.llms.settings.memberships.php:112\nmsgid \"Title (Z - A)\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:104,\n#: includes/admin/settings/class.llms.settings.memberships.php:113\nmsgid \"Most Recent\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.courses.php:106\nmsgid \"Catalog Sorting\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:44,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:21\nmsgid \"Email Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:46\nmsgid \"\"\n\"Settings for all emails sent by LifterLMS. Notification and engagement \"\n\"emails will adhere to these settings.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:50\nmsgid \"Sender Name\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:51\nmsgid \"Name to be displayed in From field\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:58\nmsgid \"Sender Email\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:59\nmsgid \"Email Address displayed in the From field\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:66\nmsgid \"Header Image\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:73\nmsgid \"Email Footer Text\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:74\nmsgid \"Text you would like displayed in the footer of all emails.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:93\nmsgid \"Certificates Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:101\nmsgid \"Background Image Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:102\nmsgid \"\"\n\"Use these sizes to determine the dimensions of certificate background \"\n\"images. After changing these settings, you may need to <a href=\\\"http://\"\n\"wordpress.org/extend/plugins/regenerate-thumbnails/\\\" target=\\\"_blank\"\n\"\\\">regenerate your thumbnails</a>.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:107\nmsgid \"Image Width\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:108,\n#: includes/admin/settings/class.llms.settings.engagements.php:118\nmsgid \"in pixels\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:116\nmsgid \"Image Height\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:125\nmsgid \"Legacy compatibility\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:126\nmsgid \"Use legacy certificate image sizes.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.engagements.php:127\nmsgid \"\"\n\"Enabling this will override the above dimension settings and set the image \"\n\"dimensions to match the dimensions of the uploaded image.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:20,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:118,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:58,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:44\nmsgid \"General\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:59\nmsgid \"Quick Links\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:64\nmsgid \"Version: %s\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:65\nmsgid \"Need help? Get support on the %1$sforums%2$s\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:66\nmsgid \"\"\n\"Looking for a quickstart guide, shortcodes, or developer documentation? Get \"\n\"started at %s\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:67\nmsgid \"Get LifterLMS news, updates, and more on our %1$sblog%2$s\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:85\nmsgid \"Features\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:92\nmsgid \"Automatic Recurring Payments: <strong>%s</strong>\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:93\nmsgid \"Disabled\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:122\nmsgid \"Select user roles\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:125\nmsgid \"\"\n\"Users with the selected roles will bypass enrollment, drip, and prerequisite \"\n\"restrictions for courses and memberships.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:128\nmsgid \"Unrestricted Preview Access\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:157\nmsgid \"Activity This Week\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:176,\n#: includes/admin/settings/class.llms.settings.general.php:182,\n#: includes/admin/settings/class.llms.settings.general.php:188,\n#: includes/admin/settings/class.llms.settings.general.php:194,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:56,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:62,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:68,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:74,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:56,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:62,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:68,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:74,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:88,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:94\nmsgid \"loading...\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:177,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:63\nmsgid \"Number of total enrollments during the selected period\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:180,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:54\nmsgid \"Registrations\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:183,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:57\nmsgid \"Number of total user registrations during the selected period\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:186,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:60,\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.sold.php:24\nmsgid \"Net Sales\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:189,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:63\nmsgid \"Total of all successful transactions during this period\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:192,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:72\nmsgid \"Lessons Completed\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:195,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:75\nmsgid \"Number of total lessons completed during the selected period\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:219\nmsgid \"Most Popular Add-ons, Services, and Resources\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.general.php:220\nmsgid \"View More &rarr;\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.integrations.php:20,\n#: includes/admin/settings/class.llms.settings.integrations.php:43,\n#: includes/admin/settings/class.llms.settings.integrations.php:78\nmsgid \"Integrations\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.integrations.php:120\nmsgid \"Integration\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.integrations.php:121\nmsgid \"Integration ID\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.integrations.php:122,\n#: includes/admin/settings/class.llms.settings.integrations.php:137,\n#: includes/admin/settings/class.llms.settings.integrations.php:138\nmsgid \"Installed\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:45,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:21\nmsgid \"Membership Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:54\nmsgid \"Select a membership\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:57\nmsgid \"\"\n\"Only allow access to site to users with a specific membership level. Users \"\n\"will be able to view and purchase membership level.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:60\nmsgid \"Restrict site by membership level\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:77\nmsgid \"Memberships Catalog\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:92\nmsgid \"Memberships Page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:97\nmsgid \"Memberships per page\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:98\nmsgid \"To show all memberships on one page, enter -1\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:107\nmsgid \"Determines the display order for items on the memberships page.\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.memberships.php:115\nmsgid \"Memberships Sorting\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.notifications.php:36\nmsgid \"All Notifications\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.notifications.php:81\nmsgid \"Subscribers\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.notifications.php:121\nmsgid \"Notification Settings\"\nmsgstr \"\"\n\n#: includes/admin/settings/class.llms.settings.notifications.php:139\nmsgid \"Invalid notification\"\nmsgstr \"\"\n\n#: includes/admin/views/html.admin.settings.php:36\nmsgid \"Save Changes\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:38,\n#: includes/forms/controllers/class.llms.controller.account.php:45\nmsgid \"Something went wrong. Please try again.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:48\nmsgid \"Subscription cancelled by student from account page.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:54\nmsgid \"Enrollment will be cancelled at the end of the prepaid period.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:83\nmsgid \"Please log in and try again.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:101\nmsgid \"Your account information has been saved.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:126,\n#: includes/forms/frontend/class.llms.frontend.password.php:26\nmsgid \"Enter a username or e-mail address.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:150\nmsgid \"Invalid username or e-mail address.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:160\nmsgid \"Password reset is not allowed for this user\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:181\nmsgid \"Check your e-mail for the confirmation link.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:185\nmsgid \"Unable to reset password due to an unknown error. Please try again.\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:216,\n#: includes/forms/controllers/class.llms.controller.account.php:223\nmsgid \"Invalid Key\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.account.php:232\nmsgid \"Your password has been updated. %1$sClick here to login%2$s\"\nmsgstr \"\"\n\n#: includes/forms/controllers/class.llms.controller.registration.php:67\nmsgid \"Already logged in! Please log out and try again.\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.forms.php:64\nmsgid \"Please enter your password.\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.forms.php:71\nmsgid \"Passwords do not match.\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.forms.php:125\nmsgid \"Voucher redeemed successfully!\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.password.php:34\nmsgid \"The email address entered is not associated with an account.\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.password.php:51\nmsgid \"Invalid username or e-mail.\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.password.php:65\nmsgid \"Could not reset password.\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.password.php:97\nmsgid \"Check your e-mail for the account confirmation link.\"\nmsgstr \"\"\n\n#: includes/forms/frontend/class.llms.frontend.password.php:113,\n#: includes/forms/frontend/class.llms.frontend.password.php:120,\n#: includes/forms/frontend/class.llms.frontend.password.php:129\nmsgid \"Invalid key\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.achievement.earned.php:82\nmsgid \"Achievement Earned\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php:82\nmsgid \"Certificate Earned\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.course.track.complete.php:81\nmsgid \"Course Track Complete\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.enrollment.php:88\nmsgid \"Enrollment\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.manual.payment.due.php:108\nmsgid \"Gateway: Manual - Payment Due\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.payment.retry.php:108\nmsgid \"Payment Retry Scheduled\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.purchase.receipt.php:110\nmsgid \"Purchase Receipt\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.quiz.failed.php:93\nmsgid \"Quiz Failed\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.quiz.passed.php:93\nmsgid \"Quiz Passed\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.section.complete.php:86\nmsgid \"Section Complete\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.student.welcome.php:95\nmsgid \"Student Welcome\"\nmsgstr \"\"\n\n#: includes/notifications/controllers/class.llms.notification.controller.subscription.cancelled.php:88,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:131\nmsgid \"Subscription Cancellation Notice\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:78\nmsgid \"Achievement Content\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:79\nmsgid \"Achievement Image\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:80\nmsgid \"Achievement Image URL\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:81\nmsgid \"Achievement Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:82,\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:110,\n#: includes/notifications/views/class.llms.notification.view.course.complete.php:77,\n#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:77,\n#: includes/notifications/views/class.llms.notification.view.enrollment.php:75,\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:81,\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:84,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:84,\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:79,\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:69\nmsgid \"Student Name\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:106\nmsgctxt \"Achievement icon alt text\"\nmsgid \"%s Icon\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:121,\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:145,\n#: includes/notifications/views/class.llms.notification.view.course.complete.php:97,\n#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:98,\n#: includes/notifications/views/class.llms.notification.view.enrollment.php:99,\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:115,\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:135,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:135,\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:113\nmsgid \"you\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.achievement.earned.php:147\nmsgid \"You've been awarded an achievement!\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:86\nmsgid \"View Full Certificate\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:107\nmsgid \"Certificate Content\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:108,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:45,\n#: includes/admin/reporting/tables/llms.table.certificates.php:157\nmsgid \"Certificate Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:109\nmsgid \"Certificate URL\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:111\nmsgid \"Mini Certificate\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.certificate.earned.php:171\nmsgid \"You've earned a certificate!\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.course.complete.php:43,\n#: includes/notifications/views/class.llms.notification.view.course.complete.php:113,\n#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:43,\n#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:114,\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:43,\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:131,\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:43,\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:129\nmsgid \"Congratulations! %1$s completed %2$s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.course.complete.php:45,\n#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:45,\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:45,\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:45\nmsgid \"Congratulations! You finished %s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.course.complete.php:76,\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:79,\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:79,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:79,\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:77\nmsgid \"Course Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.course.complete.php:123\nmsgid \"%s Completed a Course\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:76\nmsgid \"Track Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.course.track.complete.php:124\nmsgid \"%s Completed a Track\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.enrollment.php:42\nmsgid \"Congratulations! %1$s enrolled in %2$s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.enrollment.php:74\nmsgid \"Type (Course or Membership)\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.enrollment.php:115\nmsgid \"%1$s enrolled in %2$s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.enrollment.php:125\nmsgid \"%1$s enrollment success!\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:78,\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:78,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:78,\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:76\nmsgid \"Course Progress Bar\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:80,\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:82,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:82,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.lessons.php:39\nmsgid \"Lesson Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.lesson.complete.php:141\nmsgid \"%s Completed a Lesson\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:57\nmsgid \"Head over to your dashboard for payment instructions.\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:78,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:78\nmsgid \"Payment Due Date\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:85,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:85,\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:28\nmsgid \"Hello %s,\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:86\nmsgid \"A payment for your subscription to %1$s is due.\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:87\nmsgid \"Sign in to your account and %1$spay now%2$s.\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:88,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:88,\n#: templates/admin/post-types/order-details.php:29\nmsgid \"Order #%s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:97\nmsgid \"Pay Invoice\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:110\nmsgid \"Pay Now\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:131,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:131,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:90\nmsgid \"Customer Address\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:132,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:132,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:91,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:63\nmsgid \"Customer Name\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:133,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:133,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:92\nmsgid \"Customer Phone\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:135,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:135,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:93,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:64\nmsgid \"Order ID\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:136,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:136,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:94\nmsgid \"Order URL\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:137,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:137\nmsgid \"Payment Amount\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:138,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:138,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:95,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:65,\n#: templates/admin/post-types/product-access-plan.php:61\nmsgid \"Plan Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:139,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:139,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:96,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:66\nmsgid \"Product Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:140,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:140,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:97,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:67\nmsgid \"Product Type\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:141,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:141,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:98,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:68\nmsgid \"Product Title (Link)\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:219,\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:219,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:175,\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:114\nmsgctxt \"generic product type description\"\nmsgid \"Item\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:238,\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:251\nmsgid \"A payment is due for your subscription to %s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.manual.payment.due.php:249\nmsgid \"Payment Due for Order #%s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:57\nmsgid \"\"\n\"Head over to the order to see what went wrong and update your payment method \"\n\"to reactivate your subscription.\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:86\nmsgid \"\"\n\"The automatic payment for your subscription to %1$s has failed. We'll \"\n\"automatically retry this charge on %2$s.\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:87\nmsgid \"\"\n\"To reactivate your subscription you can login to your account and %1$spay now\"\n\"%2$s.\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:238\nmsgid \"Automatic payment for %1$s failed, retry scheduled for %2$s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:249\nmsgid \"Automatic payment failed for order #%s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.payment.retry.php:251\nmsgid \"An automatic payment failed for your subscription to %s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:44,\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:101,\n#: templates/admin/post-types/order-transactions.php:25\nmsgid \"Transaction ID\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:56\nmsgid \"View Order Details\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:99\nmsgid \"Transaction Amount\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:100\nmsgid \"Transaction Date\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:102\nmsgid \"Transaction Source\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:208\nmsgid \"Purchase Receipt for %s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.purchase.receipt.php:218\nmsgid \"Purchase Receipt for Order #%s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:43,\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:151\nmsgid \"%1$s failed %2$s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:45\nmsgid \"You failed %s!\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:81,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:81\nmsgid \"Grade Bar\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:83,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:83\nmsgid \"Quiz Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.failed.php:161\nmsgid \"%s failed a quiz\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:43,\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:151\nmsgid \"Congratulations! %1$s passed %2$s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:45\nmsgid \"Congratulations! You passed %s!\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.quiz.passed.php:161\nmsgid \"%s passed a quiz\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:78\nmsgid \"Section Title\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.section.complete.php:139\nmsgid \"%s Completed a Section\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:29\nmsgid \"Here's some helpful information to help you get started at %s.\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:30\nmsgid \"Your Login\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:31\nmsgid \"Your Dashboard\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:32\nmsgid \"\"\n\"If you forgot or don't have a password you can reset it now so you can login \"\n\"and get started:\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:66\nmsgid \"Dashboard URL\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:67\nmsgid \"Password Reset URL\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:70\nmsgid \"Student Login\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:119\nmsgid \"Welcome to %s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.student.welcome.php:129\nmsgid \"Let's get started %s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:26\nmsgid \"%1$s has cancelled their subscription (#%2$s) to the %3$s %4$s\"\nmsgstr \"\"\n\n#: includes/notifications/views/class.llms.notification.view.subscription.cancelled.php:141\nmsgid \"%1$s subscription cancellation\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:45,\n#: templates/admin/reporting/nav-filters.php:27\nmsgid \"Last 7 Days\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:51,\n#: templates/admin/reporting/nav-filters.php:33\nmsgid \"Custom\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:55,\n#: templates/admin/reporting/nav-filters.php:37\nmsgid \"Go\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:59,\n#: templates/admin/reporting/nav-filters.php:41\nmsgid \"Toggle Filters\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:92,\n#: templates/admin/reporting/nav-filters.php:74\nmsgid \"Filter by Course(s)\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:94,\n#: templates/admin/analytics/analytics.php:106,\n#: templates/admin/reporting/nav-filters.php:76,\n#: templates/admin/reporting/nav-filters.php:88\nmsgid \"(ID# %d)\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:104,\n#: templates/admin/reporting/nav-filters.php:86\nmsgid \"Filter by Memberships(s)\"\nmsgstr \"\"\n\n#: templates/admin/analytics/analytics.php:113,\n#: templates/admin/reporting/nav-filters.php:95\nmsgid \"Apply Filters\"\nmsgstr \"\"\n\n#: templates/admin/import/import.php:12\nmsgid \"LifterLMS Importer\"\nmsgstr \"\"\n\n#: templates/admin/import/import.php:21\nmsgid \"Import Course(s)\"\nmsgstr \"\"\n\n#: templates/admin/import/import.php:24\nmsgid \"Upload export files generated by LifterLMS. Must be a \\\".json\\\" file.\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-update.php:11\nmsgid \"The LifterLMS database needs to be updated to the latest version.\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-update.php:12\nmsgid \"\"\n\"The update will only take a few minutes and it will run in the background. A \"\n\"notice like this will let you know when it's finished.\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-update.php:13\nmsgid \"\"\n\"See the %1$sdatabase update log%2$s for a complete list of changes scheduled \"\n\"for each upgrade.\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-update.php:14\nmsgid \"Run the Updater\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-update.php:18\nmsgid \"\"\n\"We strongly recommended that you backup your database before proceeding. Are \"\n\"you sure you wish to run the updater now?\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-updating.php:13\nmsgid \"LifterLMS database update\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-updating.php:13\nmsgid \"Your database is being upgraded in the background.\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-updating.php:14\nmsgid \"Click here for database update FAQs\"\nmsgstr \"\"\n\n#: templates/admin/notices/db-updating.php:16\nmsgid \"Taking too long? Click here to run the update now.\"\nmsgstr \"\"\n\n#: templates/admin/notices/staging.php:12\nmsgid \"It looks like you may have installed LifterLMS on a staging site!\"\nmsgstr \"\"\n\n#: templates/admin/notices/staging.php:14\nmsgid \"\"\n\"LifterLMS watches for potential signs of a staging site and disables \"\n\"automatic payments so that your students do not receive duplicate charges.\"\nmsgstr \"\"\n\n#: templates/admin/notices/staging.php:17\nmsgid \"\"\n\"You can choose to enable automatic recurring payments using the buttons \"\n\"below. If you're not sure what to do, you can learn more %1$shere%2$s. You \"\n\"can always change your mind later by clicking \\\"Reset Automatic Payments\\\" \"\n\"on the LifterLMS General Settings screen under Tools and Utilities.\"\nmsgstr \"\"\n\n#: templates/admin/notices/staging.php:22\nmsgid \"Leave Automatic Payments Disabled\"\nmsgstr \"\"\n\n#: templates/admin/notices/staging.php:24\nmsgid \"Enable Automatic Payments\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:23\nmsgid \"This order was processed in the gateway's testing mode\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:30\nmsgid \"Processed by %s\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:48\nmsgid \"Access Plan Information\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:51,\n#: templates/admin/post-types/order-details.php:70,\n#: templates/admin/post-types/order-details.php:220\nmsgid \"Name:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:57,\n#: templates/admin/post-types/order-details.php:76\nmsgid \"SKU:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:67\nmsgid \"Product Information\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:90\nmsgid \"Trial Information\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:94,\n#: templates/admin/post-types/order-details.php:118\nmsgid \"Original Total:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:99,\n#: templates/admin/post-types/order-details.php:132\nmsgid \"Coupon Discount:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:107,\n#: templates/admin/post-types/order-details.php:141\nmsgid \"Total:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:114\nmsgid \"Payment Information\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:124\nmsgid \"Sale Discount:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:161\nmsgid \"Customer Information\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:164\nmsgid \"Buyer Name:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:173\nmsgid \"Buyer Email:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:179\nmsgid \"Buyer Address:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:193\nmsgid \"Buyer Phone:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:200\nmsgid \"Buyer IP Address:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-details.php:217\nmsgid \"Gateway Information\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:21\nmsgid \"Refunded\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:22,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:38\nmsgid \"Type\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:24\nmsgid \"Source\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:26\nmsgid \"Actions\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:78\nmsgid \"Resend Receipt\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:89,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:85\nmsgid \"%s Newer\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:93,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:89\nmsgid \"Older %s\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:97\nmsgid \"View all\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:101\nmsgid \"Show Less Info\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:101\nmsgid \"Show More Info\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:114\nmsgid \"Refund Amount:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:119\nmsgid \"Refund Note (optional):\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:124\nmsgid \"\"\n\"The refund will be recorded and you will need to manually issue a refund\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:125\nmsgctxt \"refund via payment gateway\"\nmsgid \"Refund via %s\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:146\nmsgid \"Payment Amount:\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:151\nmsgid \"Payment Source (optional):\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:156\nmsgid \"Payment Transaction ID (optional):\"\nmsgstr \"\"\n\n#: templates/admin/post-types/order-transactions.php:161\nmsgid \"Payment Note (optional):\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:41\nmsgid \"Unnamed Access Plan\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:42\nmsgctxt \"Product Access Plan ID\"\nmsgid \"ID# %s\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:43\nmsgid \"Purchase Link\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:66\nmsgid \"Plan SKU\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:71\nmsgid \"Enroll Text\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:76,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:82\nmsgid \"Visibility\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:85\nmsgid \"Is Free\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:87\nmsgid \"No payment required\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:97\nmsgid \"Price\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:102\nmsgid \"Frequency\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:104\nmsgid \"one-time payment\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:105\nmsgid \"every\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:106\nmsgid \"every 2nd\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:107\nmsgid \"every 3rd\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:108\nmsgid \"every 4th\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:109\nmsgid \"every 5th\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:110\nmsgid \"every 6th\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:119\nmsgid \"Plan Period\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:123\nmsgid \"week\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:129\nmsgid \"Plan Length\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:131,\n#: templates/admin/post-types/product-access-plan.php:139,\n#: templates/admin/post-types/product-access-plan.php:147,\n#: templates/admin/post-types/product-access-plan.php:155\nmsgid \"for all time\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:133\nmsgid \"for %s year\"\nmsgid_plural \"for %s years\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: templates/admin/post-types/product-access-plan.php:141\nmsgid \"for %s month\"\nmsgid_plural \"for %s months\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: templates/admin/post-types/product-access-plan.php:149\nmsgid \"for %s week\"\nmsgid_plural \"for %s weeks\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: templates/admin/post-types/product-access-plan.php:157\nmsgid \"for %s day\"\nmsgid_plural \"for %s days\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n#: templates/admin/post-types/product-access-plan.php:174,\n#: includes/admin/views/metaboxes/view-order-submit.php:80\nmsgid \"Access Expiration\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:177\nmsgid \"Expires after\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:178\nmsgid \"Expires on\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:195,\n#: templates/admin/post-types/product-access-plan.php:257\nmsgid \"year(s)\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:196,\n#: templates/admin/post-types/product-access-plan.php:258\nmsgid \"month(s)\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:197,\n#: templates/admin/post-types/product-access-plan.php:259\nmsgid \"week(s)\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:198,\n#: templates/admin/post-types/product-access-plan.php:260\nmsgid \"day(s)\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:209\nmsgid \"Plan Availability\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:211\nmsgid \"Anyone\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:212\nmsgid \"Members only\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:221\nmsgid \"ID# %d\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:237\nmsgid \"Trial Offer\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:239\nmsgid \"No trial offer\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:240\nmsgid \"Enable trial\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:245\nmsgid \"Trial Price\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:250\nmsgid \"Trial Length\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:271\nmsgid \"Sale Pricing\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:273\nmsgid \"Not on sale\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:274\nmsgid \"On Sale\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:279\nmsgid \"Sale Price\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:284\nmsgid \"Sale Start Date\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:289\nmsgid \"Sale End Date\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product-access-plan.php:299\nmsgid \"Plan Description\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:18\nmsgid \"%s Access Plans\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:19\nmsgid \"\"\n\"Access plans define the payment options available for this %s during checkout\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:25,\n#: templates/admin/post-types/product.php:44\nmsgid \"You cannot create more than %d access plans for each product.\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:33\nmsgid \"No access plans exist for your %s.\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:43\nmsgid \"Save Access Plans\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:54\nmsgid \"Confirm Your Action\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:57\nmsgid \"\"\n\"After deleting this access plan, any students subscribed to this plan will \"\n\"still have access and will continue to make recurring payments according to \"\n\"the access plan's settings. If you wish to terminate their plans you must do \"\n\"so manually.\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:58\nmsgid \"This action cannot be reversed. \"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:59\nmsgid \"Press the \\\"Delete\\\" button to permanently remove this plan.\"\nmsgstr \"\"\n\n#: templates/admin/post-types/product.php:60,\n#: includes/admin/reporting/tables/llms.table.achievements.php:37,\n#: includes/admin/reporting/tables/llms.table.certificates.php:47\nmsgid \"Delete\"\nmsgstr \"\"\n\n#: templates/admin/post-types/students.php:26\nmsgid \"Enroll New Students\"\nmsgstr \"\"\n\n#: templates/admin/post-types/students.php:33\nmsgid \"Enroll Students\"\nmsgstr \"\"\n\n#: templates/admin/reporting/reporting.php:43\nmsgid \"LifterLMS Reporting Beta\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:25,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:50\nmsgid \"Membership Access\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:56\nmsgctxt \"apply membership restriction to post type\"\nmsgid \"Restrict this %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.access.php:65\nmsgid \"Visitors must belong to one of these memberships to access this %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:25\nmsgid \"Achievement Settings\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:50,\n#: includes/admin/reporting/tables/llms.table.achievements.php:153\nmsgid \"Achievement Title\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:51\nmsgid \"Enter a title for your achievement. IE: Achievement of Completion\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:62\nmsgid \"Achievement Content\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:63\nmsgid \"Enter any information you would like to display on the achievement.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:74,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:56\nmsgid \"Background Image\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.achievement.php:75\nmsgid \"Select an Image to use for the achievement.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:21\nmsgid \"Certificate Settings\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:46\nmsgid \"Enter a title for your certificate. EG: Certificate of Completion\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.certificate.php:57\nmsgid \"Select an Image to use for the certificate.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:20\nmsgid \"Coupon Settings\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:74\nmsgid \"Select a dollar or percentage discount.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:77\nmsgid \"Discount Type\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:86\nmsgid \"%s Discount\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:92\nmsgid \"Access Plan Types\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:93\nmsgid \"Select which type of access plans this coupon can be used with.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:99\nmsgid \"Any Access Plan\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:103\nmsgid \"Only One-time Payment Access Plans\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:107\nmsgid \"Only Recurring Access Plans\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:118\nmsgid \"Discount Amount\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:119\nmsgid \"\"\n\"The amount to be subtracted from the \\\"Price\\\" of an applicable access plan. \"\n\"Do not include symbols such as %1$s.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:136\nmsgid \"Trial Discount Amount\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:137\nmsgid \"\"\n\"The amount to be subtracted from the \\\"Trial Price\\\" of an applicable access \"\n\"plan. Do not include symbols such as %1$s.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:148,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:166,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:147\nmsgid \"Restrictions\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:153\nmsgid \"Limit coupon to the following courses.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:166\nmsgid \"Limit coupon to the following memberships.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:179\nmsgid \"\"\n\"Coupon will no longer be usable after this date. Leave blank for no \"\n\"expiration.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:188\nmsgid \"Usage Limit\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:189\nmsgid \"\"\n\"The amount of times this coupon can be used. Leave empty or enter 0 for \"\n\"unlimited uses.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:205\nmsgid \"\"\n\"Optional description for internal notes. This is never displayed to your \"\n\"students.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:232\nmsgid \"\"\n\"Coupon code already exists. Customers will use the most recently created \"\n\"coupon with this code.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.coupon.php:241\nmsgid \"\"\n\"A Trial Discount Amount was not supplied. Trial Pricing Discount has \"\n\"automatically been disabled. Please re-enable Trial Pricing Discount and \"\n\"enter a Trial Discount Amount, then save this coupon again.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:74\nmsgid \"This lesson is not attached to a course.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:93\nmsgid \"Course: %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.builder.php:103\nmsgid \"Launch Course Builder\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:19\nmsgid \"Course Options\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:63,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:91\nmsgid \"Sales Page\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:68\nmsgid \"\"\n\"Customize the content displayed to visitors and students who are not \"\n\"enrolled in the course.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:73,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:101\nmsgid \"Sales Page Content\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:76\nmsgid \"Display default course content\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:77,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:105\nmsgid \"Show custom content\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:78,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:106\nmsgid \"Redirect to WordPress Page\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:79,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:107\nmsgid \"Redirect to custom URL\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:85\nmsgid \"\"\n\"This content will only be shown to visitors who are not enrolled in this \"\n\"course.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:87,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:115\nmsgid \"Sales Page Custom Content\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:100,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:128\nmsgid \"Select a Page\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:107,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:135\nmsgid \"Sales Page Redirect URL\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:122\nmsgid \"Course Length\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:123\nmsgid \"Enter a description of the estimated length. IE: 3 days\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:133\nmsgid \"\"\n\"Choose a course difficulty level. New difficulties can be added via \"\n\"%1$sCourses -> Difficulties%2$s.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:136\nmsgid \"Course Difficulty Category\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:143\nmsgid \"Featured Video\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:144,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:62\nmsgid \"\"\n\"Paste the url for a Wistia, Vimeo or Youtube video or a hosted video file. \"\n\"For a full list of supported providers see %s.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:149\nmsgid \"\"\n\"When enabled, the featured video will be displayed on the course tile in \"\n\"addition to the course page.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:152\nmsgid \"Display Featured Video on Course Tile\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:158\nmsgid \"Featured Audio\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:159,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:70\nmsgid \"\"\n\"Paste the url for a SoundCloud or Spotify song or a hosted audio file. For a \"\n\"full list of supported providers see %s.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:171\nmsgid \"You must enroll in this course to access course content.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:172\nmsgid \"\"\n\"This message will be displayed when non-enrolled visitors attempt to access \"\n\"course content directly without enrolling first\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:174\nmsgid \"Content Restricted Message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:180\nmsgid \"Enable Enrollment Period\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:181\nmsgid \"Set registration start and end dates for this course\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:191\nmsgid \"Registration opens on this date.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:194\nmsgid \"Enrollment Start Date\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:201\nmsgid \"Registration closes on this date.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:204\nmsgid \"Enrollment End Date\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:211\nmsgid \"\"\n\"Enrollment in this course opens on [lifterlms_course_info id=\\\"%d\\\" key=\"\n\"\\\"enrollment_start_date\\\"].\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:212\nmsgid \"\"\n\"This message will be displayed to non-enrolled visitors before the \"\n\"Enrollment Start Date. You may use shortcodes like [lifterlms_course_info id=\"\n\"\\\"%d\\\" key=\\\"enrollment_start_date\\\"] in this message.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:214\nmsgid \"Enrollment Opens Message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:221\nmsgid \"\"\n\"Enrollment in this course closed on [lifterlms_course_info id=\\\"%d\\\" key=\"\n\"\\\"enrollment_end_date\\\"].\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:222\nmsgid \"\"\n\"This message will be displayed to non-enrolled visitors once the Enrollment \"\n\"End Date has passed. You may use shortcodes like [lifterlms_course_info id=\"\n\"\\\"%d\\\" key=\\\"enrollment_end_date\\\"] in this message.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:224\nmsgid \"Enrollment Closed Message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:230\nmsgid \"Enable Course Time Period\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:231\nmsgid \"\"\n\"Set start and end dates for this course. Content can only be viewed and \"\n\"completed within the selected range.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:243\nmsgid \"Course Start Date\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:252\nmsgid \"Course End Date\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:260\nmsgid \"\"\n\"This message will be displayed to non-enrolled visitors before the Course \"\n\"Start Date. You may use shortcodes like [lifterlms_course_info id=\\\"%d\\\" key=\"\n\"\\\"start_date\\\"] in this message.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:262\nmsgid \"Course Opens Message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:270\nmsgid \"\"\n\"This message will be displayed to non-enrolled visitors once the Course End \"\n\"Date has passed. You may use shortcodes like [lifterlms_course_info id=\\\"%d\"\n\"\\\" key=\\\"end_date\\\"] in this message.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:272\nmsgid \"Course Closed Message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:279,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:97\nmsgid \"Enable Prerequisite\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:280\nmsgid \"Enable to choose a prerequisite course or course track\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:292\nmsgid \"Select a course\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:295\nmsgid \"\"\n\"Select a prerequisite course. Students must have completed the selected \"\n\"course before they can view or complete content in this course.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:298\nmsgid \"Choose Prerequisite Course\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:305\nmsgid \"\"\n\"Select the prerequisite course track. Students must have completed the \"\n\"select track before they can view or complete content in this course.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:309\nmsgid \"Choose Prerequisite Course Track\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:316\nmsgid \"Enable Course Capacity\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:317\nmsgid \"Limit the number of users that can enroll in this course.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:330\nmsgid \"Course Capacity\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:337\nmsgid \"\"\n\"This message will be displayed to non-enrolled visitors once the Course \"\n\"Capacity has been reached. \"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php:339\nmsgid \"Capacity Reached Message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:41\nmsgid \"Admin Email\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:50\nmsgid \"Email Subject\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:51\nmsgid \"This will be used for the subject line of your email.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:60\nmsgid \"Email Heading\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:61\nmsgid \"This is the heading for your email. It will display above the content.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:70\nmsgid \"Email To:\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:71,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:82,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:91\nmsgid \"Separate multiple address with a comma.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:81\nmsgid \"Email CC:\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.email.settings.php:90\nmsgid \"Email BCC:\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:21\nmsgid \"Engagement Options\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:46\nmsgid \"\"\n\"This engagement will be triggered when a student completes the selected \"\n\"action\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:50\nmsgid \"Triggering Event\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:69\nmsgid \"Select a Lesson\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:77\nmsgid \"Select an Access Plan\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:96\nmsgid \"Select a Quiz\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:102\nmsgid \"Select a Section\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:153,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:156\nmsgid \"Select a Course Track\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:165\nmsgid \"Determines the type of engagement\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:168\nmsgid \"Engagement Type\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:181,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:185\nmsgid \"Select an Engagement\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:193\nmsgid \"\"\n\"Enter the number of days to wait before triggering this engagement. Enter 0 \"\n\"or leave blank to trigger immediately.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:195\nmsgid \"Engagement Delay\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.engagement.php:202\nmsgid \"Engagement Settings\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.expiration.php:67\nmsgid \"Please select an option...\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:22,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:41,\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.instructors.php:49,\n#: includes/admin/reporting/tables/llms.table.courses.php:282\nmsgid \"Instructors\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:45\nmsgid \"Add Instructor\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:49\nmsgid \"New Instructor\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:58\nmsgid \"Select an Instructor\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.instructors.php:73\nmsgid \"Label\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:23\nmsgid \"Lesson Settings\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:45\nmsgid \"After course enrollment\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:46\nmsgid \"After course start date\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:53\nmsgid \"After prerequisite completion\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:65\nmsgid \"Video Embed Url\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:74\nmsgid \"Audio Embed Url\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:78\nmsgid \"\"\n\"Checking this box will allow guests to view the content of this lesson \"\n\"without registering or signing up for the course.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:89\nmsgid \"Prerequisites\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:94\nmsgid \"Enable to choose a prerequisite Lesson\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:105\nmsgid \"Select a Prerequisite Lesson\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:108\nmsgid \"Select the prerequisite lesson\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:111\nmsgid \"Choose Prerequisite\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:118\nmsgid \"Drip Settings\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:134\nmsgid \"Delay (in days) \"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:144\nmsgid \"Date Available\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:151\nmsgid \"\"\n\"Optionally enter a time when the lesson should become available. If no time \"\n\"supplied, lesson will be available at 12:00 AM. Format must be HH:MM AM\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:153\nmsgid \"Time Available\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:164\nmsgid \"\"\n\"Checking this box will require students to get a passing score on the above \"\n\"quiz to complete the lesson.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php:167\nmsgid \"Require Passing Grade\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:49\nmsgid \"Remove from Auto-enrollment\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:50\nmsgid \"Enroll All Members\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:96\nmsgid \"\"\n\"Customize the content displayed to visitors and students who are not \"\n\"enrolled in the membership.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:104\nmsgid \"Display default membership content\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:113\nmsgid \"\"\n\"This content will only be shown to visitors who are not enrolled in this \"\n\"membership.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:152\nmsgid \"\"\n\"When a non-member attempts to access content restricted to this membership\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:156\nmsgid \"Restricted Access Redirect\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:160\nmsgid \"Stay on page\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:164\nmsgid \"Redirect to this membership page\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:168\nmsgid \"Redirect to a WordPress page\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:172\nmsgid \"Redirect to a Custom URL\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:184\nmsgid \"Select a WordPress Page\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:194\nmsgid \"Enter a Custom URL\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:202\nmsgid \"\"\n\"Check this box to output a message after redirecting. If no redirect is \"\n\"selected this message will replace the normal content that would be \"\n\"displayed.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:205\nmsgid \"Display a Message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:211\nmsgid \"Shortcodes like %s can be used in this message\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:212\nmsgid \"You must belong to the %s membership to access this content.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:214\nmsgid \"Restricted Content Notice\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:221,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:224\nmsgid \"Auto Enrollment\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:225\nmsgid \"\"\n\"When a student joins this membership they will be automatically enrolled in \"\n\"these courses. Click %1$shere%2$s for more information.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:227\nmsgid \"Course Name\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:228\nmsgid \"No auto-enrollment courses found.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:237\nmsgid \"Select course(s)\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:240\nmsgid \"\"\n\"When a member is enrolled in this membership they will be automatically \"\n\"enrolled into any courses in the auto-enrollment list\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.membership.php:242\nmsgid \"Add Auto-enrollment Course(s)\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.details.php:22\nmsgid \"Order Details\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:51\nmsgid \"Cannot manage enrollment status for anonymized orders.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:63\nmsgid \"Select\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:70\nmsgctxt \"enrollment status\"\nmsgid \"Status: %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:75\nmsgctxt \"enrollment trigger\"\nmsgid \"Enrolled: %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:78\nmsgctxt \"enrollment trigger\"\nmsgid \"Updated: %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:81\nmsgctxt \"enrollment trigger\"\nmsgid \"Trigger: %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:87\nmsgid \"Update Enrollment Status\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.enrollment.php:109\nmsgid \"Student enrollment status changed from %1$s to %2$s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:19\nmsgid \"Order Notes\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:68\nmsgctxt \"order note author\"\nmsgid \"by %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:69\nmsgctxt \"order note date\"\nmsgid \"on %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.notes.php:93\nmsgid \"No order notes found.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.submit.php:20\nmsgid \"Order Information\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.submit.php:67\nmsgid \"The status of a Legacy order cannot be changed.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:127\nmsgid \"Refund Error: Missing a transaction ID\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:129\nmsgid \"Refund Error: Missing or invalid refund amount\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:137,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:170\nmsgctxt \"admin error message\"\nmsgid \"Refund Error: %s\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.order.transactions.php:151\nmsgid \"Refund Error: Missing or invalid payment amount\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:21\nmsgid \"Product Options\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:126\nmsgid \"Access Plan data was posted in an invalid format\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:134\nmsgid \"Access Plan title is required\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:138\nmsgid \"Access Plan price is required\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:142\nmsgid \"Sale price is required if the plan is on sale\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.product.php:146\nmsgid \"Trial price is required if the plan has a trial\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.students.php:29\nmsgid \"Student Management\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.students.php:60\nmsgid \"You must publish this post before you can manage students.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:36\nmsgid \"Video Embed Code\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:44\nmsgid \"Paste the url for your Wistia, Vimeo or Youtube videos.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:54\nmsgid \"Audio Embed Code\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.video.php:62\nmsgid \"Paste the embed code for your externally hosted audio.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:49\nmsgid \"Catalog visibility:\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:55\nmsgid \"\"\n\"Choose the visibility of the %s in your catalog. It will always be available \"\n\"directly.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.visibility.php:61\nmsgid \"OK\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:25\nmsgid \"You need to publish this post before you can generate a CSV.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:34\nmsgid \"Vouchers only\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:35\nmsgid \"Generates a CSV of voucher codes, uses, and remaining uses.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:40\nmsgid \"Redeemed codes\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:41\nmsgid \"Generated a CSV of student emails, redemption date, and used code.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:48\nmsgid \"Email CSV\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:50\nmsgid \"Send to multiple emails by separating emails addresses with commas.\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.export.php:53\nmsgid \"Generate Export\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:18\nmsgid \"Voucher Settings\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:72\nmsgid \"Codes\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:80,\n#: includes/admin/post-types/meta-boxes/class.llms.meta.box.voucher.php:84\nmsgid \"Redemptions\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:34\nmsgid \"Coupon Amount\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:36\nmsgid \"Usage / Limit\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:59\nmsgid \"Discount: \"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.coupons.php:64\nmsgid \"Trial Discount: \"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.courses.php:45\nmsgid \"Builder\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.courses.php:84\nmsgid \"courses export\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:37\nmsgid \"Trigger\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:39\nmsgid \"Delay\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.engagements.php:114\nmsgid \"%d days\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:42\nmsgid \"Payment Status\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:43\nmsgid \"Access Status\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:45\nmsgid \"Revenue\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:71\nmsgctxt \"order number display\"\nmsgid \"#%d\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:74\nmsgid \"by\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:102\nmsgctxt \"access plan expiration\"\nmsgid \"Expired:\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.orders.php:104\nmsgctxt \"access plan expiration\"\nmsgid \"Expires:\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:25\nmsgid \"LifterLMS Checkout\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:26\nmsgid \"LifterLMS Course Catalog\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:27\nmsgid \"LifterLMS Memberships Catalog\"\nmsgstr \"\"\n\n#: includes/admin/post-types/post-tables/class.llms.admin.post.table.pages.php:28\nmsgid \"LifterLMS Student Dashboard\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:88\nmsgid \"Visit the triggering order to manage this student's enrollment\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:91\nmsgid \"Cancel Enrollment\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:96\nmsgid \"Reactivate Enrollment\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:164\nmsgid \"Admin: %1$s (#%2$d)\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:220,\n#: includes/admin/reporting/tables/llms.table.course.students.php:240,\n#: includes/admin/reporting/tables/llms.table.students.php:247\nmsgid \"Search students by name or email...\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:232\nmsgid \"Manage Existing Enrollments\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:351,\n#: includes/admin/reporting/tables/llms.table.course.students.php:372,\n#: includes/admin/reporting/tables/llms.table.student.course.php:185,\n#: includes/admin/reporting/tables/llms.table.student.courses.php:199,\n#: includes/admin/reporting/tables/llms.table.student.memberships.php:107,\n#: includes/admin/reporting/tables/llms.table.students.php:404\nmsgid \"Name\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:360,\n#: includes/admin/reporting/tables/llms.table.course.students.php:398\nmsgid \"Enrollment Updated\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:372,\n#: includes/admin/reporting/tables/llms.table.course.students.php:417\nmsgid \"Last Lesson\"\nmsgstr \"\"\n\n#: includes/admin/post-types/tables/class.llms.table.student.management.php:376\nmsgid \"Enrollment Trigger\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.achievements.php:47\nmsgid \"\"\n\"Are you sure you want to delete this achievement? This action cannot be \"\n\"undone!\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.achievements.php:152,\n#: includes/admin/reporting/tables/llms.table.certificates.php:156\nmsgid \"Template ID\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.achievements.php:156,\n#: includes/admin/reporting/tables/llms.table.certificates.php:159\nmsgid \"Related Post\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.achievements.php:168\nmsgid \"This student has not yet earned any achievements.\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.certificates.php:42\nmsgid \"Download\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.certificates.php:57\nmsgid \"\"\n\"Are you sure you want to delete this certificate? This action cannot be \"\n\"undone!\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.certificates.php:171\nmsgid \"This student has not yet earned any certificates.\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.course.students.php:403,\n#: includes/admin/reporting/tables/llms.table.student.course.php:194,\n#: includes/admin/reporting/tables/llms.table.student.courses.php:216\nmsgid \"Completed\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.courses.php:248\nmsgid \"Search courses...\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.courses.php:290\nmsgid \"Average Progress\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.courses.php:295,\n#: includes/admin/reporting/tables/llms.table.quizzes.php:320\nmsgid \"Average Grade\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.questions.php:90\nmsgid \"Points\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.questions.php:92\nmsgid \"Selected Answer\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.questions.php:93\nmsgid \"Correct Answer\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:252\nmsgid \"Attempt #\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.quiz.attempts.php:273,\n#: templates/admin/reporting/tabs/quizzes/attempt.php:94\nmsgid \"End Date\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.quizzes.php:272\nmsgid \"Search quizzes...\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.quizzes.php:315\nmsgid \"Total Attempts\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.student.course.php:143\nmsgctxt \"section title\"\nmsgid \"Section: %s\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.student.courses.php:212\nmsgid \"Updated\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.student.courses.php:228\nmsgid \"This student is not enrolled in any courses.\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.student.memberships.php:125\nmsgid \"This student is not enrolled in any memberships.\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:419\nmsgid \"Registration Date\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:437\nmsgid \"Completions\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:474\nmsgid \"Billing Zip\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:489\nmsgid \"Courses (Enrolled)\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:494\nmsgid \"Courses (Cancelled)\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:499\nmsgid \"Courses (Expired)\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:504\nmsgid \"Memberships (Enrolled)\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:509\nmsgid \"Memberships (Cancelled)\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tables/llms.table.students.php:514\nmsgid \"Memberships (Expired)\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.courses.php:90,\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.quizzes.php:71\nmsgid \"Overview\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:66\nmsgid \"Courses Completed\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.enrollments.php:69\nmsgid \"Number of total courses completed during the selected period\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.quizzes.php:72\nmsgid \"Attempts\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:54,\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.sales.php:23\nmsgid \"# of Sales\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:57\nmsgid \"Number of new active or completed orders placed within this period\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:66,\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.refunds.php:25\nmsgid \"# of Refunds\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:69\nmsgid \"Number of orders refunded during this period\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:72,\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.refunded.php:24\nmsgid \"Amount Refunded\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:75\nmsgid \"Total of all transactions refunded during this period\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:86,\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.coupons.php:26\nmsgid \"# of Coupons Used\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:89\nmsgid \"Number of orders completed using coupons during this period\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:92\nmsgid \"Amount of Coupons\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.sales.php:95\nmsgid \"Total amount of coupons used during this period\"\nmsgstr \"\"\n\n#: includes/admin/reporting/tabs/class.llms.admin.reporting.tab.students.php:82\nmsgid \"Information\"\nmsgstr \"\"\n\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.coursecompletions.php:19\nmsgid \"# of Courses Completed\"\nmsgstr \"\"\n\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.enrollments.php:20\nmsgid \"# of Enrollments\"\nmsgstr \"\"\n\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.lessoncompletions.php:19\nmsgid \"# of Lessons Completed\"\nmsgstr \"\"\n\n#: includes/admin/reporting/widgets/class.llms.analytics.widget.registrations.php:19\nmsgid \"# of Registrations\"\nmsgstr \"\"\n\n#: includes/admin/settings/tables/class.llms.table.notification.settings.php:102\nmsgid \"Notification\"\nmsgstr \"\"\n\n#: includes/admin/settings/tables/class.llms.table.notification.settings.php:103\nmsgid \"Configure\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/assignment.php:15\nmsgid \"There's no assignment associated with this lesson.\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/assignment.php:18\nmsgid \"Create New Assignment\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/course.php:18,\n#: includes/admin/views/builder/course.php:20\nmsgid \"Open WordPress course editor\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/course.php:23\nmsgid \"View course\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/editor.php:23\nmsgid \"Assignment\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/editor.php:32\nmsgid \"Close\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/elements.php:10\nmsgid \"Add Elements\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/elements.php:27\nmsgid \"Existing Lesson\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson-settings.php:17,\n#: includes/admin/views/builder/quiz.php:43\nmsgid \"Published\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson-settings.php:25,\n#: includes/admin/views/builder/lesson-settings.php:27,\n#: includes/admin/views/builder/lesson.php:21\nmsgid \"Open WordPress lesson editor\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson-settings.php:30,\n#: includes/admin/views/builder/lesson-settings.php:32,\n#: includes/admin/views/builder/lesson.php:49\nmsgid \"Detach Lesson\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson-settings.php:36,\n#: includes/admin/views/builder/lesson-settings.php:38\nmsgid \"Delete Lesson\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:27\nmsgid \"View lesson\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:32,\n#: includes/admin/views/builder/section.php:33\nmsgid \"Shift up\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:36,\n#: includes/admin/views/builder/section.php:37\nmsgid \"Shift down\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:40\nmsgid \"Move to previous section\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:44\nmsgid \"Move to next section\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:55\nmsgid \"Trash Lesson\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:72\nmsgid \"Edit Lesson settings\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:81\nmsgid \"Add an assignment\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:82\nmsgid \"Edit Assignment: %s\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:90\nmsgid \"Add a quiz\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:91\nmsgid \"Edit Quiz: %s\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:99\nmsgid \"No content\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:100\nmsgid \"Has content\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:108\nmsgid \"No video\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:109\nmsgid \"Has video\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:117\nmsgid \"No audio\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:118\nmsgid \"Has audio\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:126\nmsgid \"Enrolled students only\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:135\nmsgid \"No prerequisite\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:136\nmsgid \"Prerequisite Enabled\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:144\nmsgid \"Drip disabled\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/lesson.php:145\nmsgid \"Drip Enabled\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question-choice.php:22\nmsgid \"Enter a choice...\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question-choice.php:28,\n#: includes/admin/views/builder/question-choice.php:30,\n#: includes/admin/views/builder/question.php:87,\n#: includes/admin/views/builder/question.php:89\nmsgid \"Remove image\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question-choice.php:32,\n#: includes/admin/views/builder/question.php:91\nmsgid \"image preview\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question-choice.php:36,\n#: includes/admin/views/builder/question.php:95\nmsgid \"Add Image\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question-choice.php:44,\n#: includes/admin/views/builder/question-choice.php:46\nmsgid \"Add Choice\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question-choice.php:49,\n#: includes/admin/views/builder/question-choice.php:51\nmsgid \"Delete Choice\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question-type.php:11\nmsgid \"\"\n\"Install the LifterLMS Advanced Quizzes add-on to enable this question type\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:24\nmsgid \"Expand question\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:28\nmsgid \"Collapse question\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:33\nmsgid \"Clone question\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:36\nmsgid \"Delete question\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:106\nmsgid \"Video\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:113\nmsgid \"https://\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:129\nmsgid \"Choices\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:133\nmsgid \"Multiple Correct Choices\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:144\nmsgid \"Drag a question here to add it to the group.\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/question.php:153\nmsgid \"Result Clarifications\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:14\nmsgid \"There's no quiz associated with this lesson.\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:17\nmsgid \"Create New Quiz\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:39\nmsgid \"Total Points\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:51,\n#: includes/admin/views/builder/quiz.php:53\nmsgid \"Detach Quiz\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:57,\n#: includes/admin/views/builder/quiz.php:59\nmsgid \"Delete Quiz\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:72\nmsgid \"Click \\\"Add Question\\\" below to start building your quiz!\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:77,\n#: includes/admin/views/builder/utilities.php:20\nmsgid \"Collapse All\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:82,\n#: includes/admin/views/builder/utilities.php:14\nmsgid \"Expand All\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/quiz.php:98\nmsgid \"Filter\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/section.php:22\nmsgid \"Expand section\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/section.php:28\nmsgid \"Collapse section\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/section.php:42\nmsgid \"Delete Section\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/sidebar.php:20\nmsgid \"Saved\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/sidebar.php:21\nmsgid \"Save changes\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/sidebar.php:22\nmsgid \"Saving changes...\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/sidebar.php:23\nmsgid \"Error saving changes...\"\nmsgstr \"\"\n\n#: includes/admin/views/builder/sidebar.php:26\nmsgid \"Exit\"\nmsgstr \"\"\n\n#: includes/admin/views/metaboxes/view-order-submit.php:22\nmsgid \"Update Order Status:\"\nmsgstr \"\"\n\n#: includes/admin/views/metaboxes/view-order-submit.php:41\nmsgid \"Trial End Date\"\nmsgstr \"\"\n\n#: includes/admin/views/metaboxes/view-order-submit.php:99\nmsgid \"Update Order\"\nmsgstr \"\"\n\n#: includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.repeater.php:27\nmsgid \"Add New\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:27\nmsgid \"Course Overview\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:38\nmsgid \"Currently enrolled students\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:47\nmsgid \"Current average progress\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:56\nmsgid \"Current average grade\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:64\nmsgid \"New orders %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:73\nmsgid \"Total sales %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:82\nmsgid \"New enrollments %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:90\nmsgid \"Unenrollments %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:99\nmsgid \"Lessons completed %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:107\nmsgid \"Course completions %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:116\nmsgid \"Achievements earned %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:125\nmsgid \"Certificates earned %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:134\nmsgid \"Emails sent %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/courses/overview.php:143,\n#: templates/admin/reporting/tabs/quizzes/overview.php:77,\n#: templates/admin/reporting/tabs/students/information.php:96\nmsgid \"Recent events\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:43\nmsgid \"Correct answers\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:52\nmsgid \"Points earned\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:102\nmsgid \"Time Elapsed\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:109\nmsgid \"Answers\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:120\nmsgid \"Start a Review\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:124\nmsgid \"Save Review\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:130\nmsgid \"Delete Attempt\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/attempt.php:144\nmsgid \"Additional Attempts\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/overview.php:29\nmsgid \"Quiz Overview\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/overview.php:41\nmsgid \"Attempts %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/overview.php:51\nmsgid \"Average grade %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/overview.php:59\nmsgid \"Passed attempts %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/overview.php:67\nmsgid \"Failed attempts %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/quizzes/overview.php:79\nmsgid \"Quiz events coming soon...\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/courses-course.php:26\nmsgid \"Enrollment Status\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/courses-course.php:40\nmsgid \"Current Grade\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/courses-course.php:48\nmsgid \"Enrollment Date\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/courses-course.php:56\nmsgid \"Completion Date\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/courses-course.php:70\nmsgid \"Progress: %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/courses-course.php:71\nmsgid \"Grade: %s\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/information.php:30\nmsgid \"Registered\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/information.php:39\nmsgid \"Overall Progress\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/information.php:48\nmsgid \"Overall Grade\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/information.php:56\nmsgid \"Achievements earned\"\nmsgstr \"\"\n\n#: templates/admin/reporting/tabs/students/information.php:64\nmsgid \"Certificates earned\"\nmsgstr \"\"\n"
  },
  {
    "path": "tests/assets/lifterlms-mock-addon.php",
    "content": "<?php\n/**\n * Plugin Name: LifterLMS Mock Add-on\n * Plugin URI: https://lifterlms.com/\n * Description: Mock add-on plugin for phpunit integration tests related to add-ons.\n * Version: 1.0.0\n * Author: LifterLMS\n * Author URI: https://lifterlms.com/\n * License: GPLv3\n * License URI: https://www.gnu.org/licenses/gpl-3.0.html\n */\n\ndefined( 'ABSPATH' ) || exit;\n\ndefine( 'LLMS_MOCK_PLUGIN_LOADED', true );\n"
  },
  {
    "path": "tests/bin/setup-e2e.sh",
    "content": "#!/bin/bash\n\nllmsenv=vendor/bin/llms-env\n\n# 1. Activate LifterLMS plugin\n##############################\n$llmsenv wp plugin activate lifterlms\n\n\n# 2. Bootstrap user accounts\n############################\n\n# Give the primary admin user a name.\n$llmsenv wp user meta update 1 first_name Chad\n$llmsenv wp user meta update 1 last_name Feldheimer\n\n# StudentDashboard/RedeemVoucher\n$llmsenv wp user create voucher voucher@email.tld --role=student --user_pass=password\n\n# StudentDashboardLogin -> should allow a user with valid credentials to login\n# Settings/CopyPrevention -> StudentUser\n$llmsenv wp user create validcreds validcreds@email.tld --role=student --user_pass=password\n\n# CourseRestrictions -> Enrolled Users\n$llmsenv wp user create restrictionstester restrictions@email.tld --role=student --user_pass=password\n\n# Engagements/Certificates -> Legacy\nHAS_A_CERT_UID=$( $llmsenv wp user create hasacert hasacert@email.tld --role=student --user_pass=password --porcelain )\n\n\n# 3. Set options.\n#################\n\n$llmsenv wp option update can_compress_scripts 1\n\n\n# 4. Bootstrap posts\n####################\n\n# Settings/CopyPrevention\nCOPY_TEST_ID=$( $llmsenv wp post create --post_type=page --post_title=\"Integrity-Test\" --post_status=publish --porcelain )\n$llmsenv wp media import https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg --post_id=$COPY_TEST_ID --featured_image\n\n# Engagements/Certificates -> Legacy\n$llmsenv wp post create --post_type=llms_my_certificate --post_author=$HAS_A_CERT_UID --post_title=\"Template-V1\" --post_status=publish --post_content=\\\"Legacy Template\\\"\n"
  },
  {
    "path": "tests/e2e/README.md",
    "content": "LifterLMS E2E (End-to-End) Tests\n================================\n\n## Requirements\n\nThe E2E test suite requires [Node](https://nodejs.org/en/download/) to run tests via the terminal of your choosing.\n\n[Docker](https://docs.docker.com/install/) is not required, but it is recommended. You could configure any WordPress site (local or publicly accessible) to be used for testing.\n\n**If you choose to run tests on an environment other than Docker, the setup and configuration will differ from what is outlined here and you will also risk polluting your site with unwanted test content and data.**\n\n\n## Installation\n\nTo install the test suite:\n\n+ `npm install`: Install Node dependencies.\n+ `composer install`: Install all required PHP dependencies.\n+ `composer run env up`: Build and install the local environment.\n+ `composer run env:setup`: Setup the local environment.\n\nAfter installation a WordPress site should be accessible at [http://localhost:8080](http://localhost:8080) using the username `admin` and password `password`.\n\n\n## Running Tests\n\nTo run tests:\n\n+ `npm run test`: Runs all tests in a headless browser.\n+ `npm run test:dev`: Runs tests in an interactive browser with \"slow\" motion enabled. This mode is helpful when writing tests so you can see what's going on.\n+ `npm run test -- -t SuiteName`: Run a single test suite by name. \"SuiteName\" will be the name of a test file `describe()`. For  example \"SetupWizard\".\n+ `npm run test -- -t \"test expect description\"`: Run a single test by its \"should\" description block. For example \"should load and run the entire setup wizard.\".\n\n\n## Managing Docker Containers\n\nThe local environment is powered by docker containers which can be managed with the following commands:\n\n```\nconfig:  Creates configuration override files\ndown:    Stop and remove containers and volumes\nup:      Start containers\nps:      List containers\nreset:   Destroy and recreate containers and volumes\nrestart: Restart containers\nrm:      Remove containers and volumes\nssh:     Open an interactive bash session with the PHP service container\nstop:    Stop containers without removing them\nwp:      Execute a wp-cli command inside the PHP service container\n```\n\nTo run these commands, run `composer run env <command>` where `<command>` is the name of the command you wish to run.\n\nFor additionally information and options for each command run, the command with the `-h` or `--help` flag to view usage information.\n\n\n## Test Organization\n\nAll tests are stored in the [tests/e2e/tests](./tests) directory.\n\nTests should organized into subdirectories by group and each file should function as a secondary level of organization for grouping tests.\n\n\n## Credits\n\nTools and libraries used:\n\n+ [Puppeteer](https://github.com/GoogleChrome/puppeteer): a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol.\n+ [Jest](https://github.com/facebook/jest): A comprehensive JavaScript testing solution.\n+ [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer): A test runner to run tests using Jest & Puppeteer.\n+ [expect-puppeteer](https://github.com/smooth-code/jest-puppeteer/tree/master/packages/expect-puppeteer): Assertion library for Puppeteer.\n\nThe following utility packages are used to help facilitate e2e tests in WordPress and LifterLMS:\n\n+ [@wordpress/scripts](https://github.com/WordPress/gutenberg/tree/master/packages/scripts): A collection of reusable scripts tailored for WordPress development.\n+ [@wordpress/e2e-test-utils](https://github.com/WordPress/gutenberg/tree/master/packages/e2e-test-utils): End-To-End (E2E) test utils for WordPress.\n+ [llms-e2e-test-utils](https://github.com/gocodebox/lifterlms/tree/trunk/packages/llms-e2e-test-utils): End-To-End (E2E) test utils for LifterLMS.\n\nA debt of gratitude is owed to [WP React Starter by devowl.io](https://github.com/devowlio/wp-react-starter), without the open-source code found in this repository our lead developer would surely have descended into eventual madness trying to figure out how to mount a working directory into a Docker container. I know you're saying it sounds simple and in retrospect he agrees with you but you know how things go sometimes...\n"
  },
  {
    "path": "tests/e2e/tests/activate/bootstrap.test.js",
    "content": "/**\n * Bootstraps E2E Tests.\n *\n * @since 3.37.8\n * @since 3.37.14 Fix package references.\n * @since 4.0.0-rc.1 Use `runSetupWizard()`.\n * @since 6.0.0 Add theme activation based on current WP version.\n */\n\nimport { activateTheme, visitPage, runSetupWizard } from '@lifterlms/llms-e2e-test-utils';\n\n\ndescribe( 'Bootstrap', () => {\n\n\t/**\n\t * This first test will intermittently fail with the fetch_error \"You are probably offline\".\n\t *\n\t * I suspect this error comes from the dashboard's widgets when loading WP meetup events since\n\t * it makes an async request to pull the data when none yet exists but I can't exactly recreate it\n\t * and narrow it down or figure out how to turn that off with WP-CLI or something.\n\t *\n\t * I've never been able to reproduce the error locally. It only intermittently happens in the CI.\n\t *\n\t * @link https://github.com/WordPress/gutenberg/discussions/34856\n\t * @link https://github.com/WordPress/gutenberg/issues/39862\n\t */\n\tjest.retryTimes( 2 );\n\tit ( 'should configure the correct theme based on the tested WP version.', async () => {\n\t\tawait activateTheme();\n\t} );\n\n\tit ( 'should load and run the entire setup wizard.', async () => {\n\t\tawait runSetupWizard();\n\t} );\n\n} );\n"
  },
  {
    "path": "tests/e2e/tests/checkout/coupon.test.js",
    "content": "import {\n\tclick,\n\tcreateAccessPlan,\n\tcreateCoupon,\n\tcreateCourse,\n\tfillField,\n\tlogoutUser,\n} from '@lifterlms/llms-e2e-test-utils';\n\nlet courseId = null,\n\tcoupon   = null,\n\tplanUrl  = null;\n\n/**\n * Setup the test\n *\n * @since 3.39.0\n *\n * @return {Void}\n */\nasync function setupTest() {\n\n\tif ( ! courseId ) {\n\t\tcourseId = await createCourse( 'Test Coupons' );\n\t}\n\n\tif ( ! planUrl ) {\n\t\tplanUrl = await createAccessPlan( {\n\t\t\tpostId: courseId,\n\t\t\tprice: 9.99,\n\t\t\ttitle: 'Test Plan ' + parseInt( Math.random() * 100000 ),\n\t\t} );\n\n\t}\n\n\tif ( ! coupon ) {\n\t\tcoupon = await createCoupon( {} );\n\t}\n\n\tawait logoutUser();\n\n}\n\n/**\n * Apply a coupon\n *\n * @since 3.39.0\n *\n * @param {String} code Coupon code.\n * @return {Void}\n */\nasync function applyCoupon( code ) {\n\n\tawait page.goto( planUrl );\n\n\tawait click( '.llms-coupon-wrapper a[href=\"#llms-coupon-toggle\"]' );\n\n\tawait page.waitForSelector( '#llms_coupon_code' );\n\n\tawait fillField( '#llms_coupon_code', code );\n\n\tawait click( '#llms-apply-coupon' );\n\n}\n\ndescribe( 'Checkout/Coupons', () => {\n\n\tbeforeEach( async () => {\n\t\tawait setupTest();\n\t} );\n\n\t// Randomly failing during the create access plan step, investigate.\n\txit ( 'should respond with an error for an unknown coupon', async () => {\n\n\t\tconst codeNotFound = 'notfound';\n\n\t\tawait applyCoupon( codeNotFound );\n\n\t\tawait page.waitForSelector( '.llms-coupon-messages' );\n\t\t// Wait for animation.\n\t\tawait page.waitForTimeout( 700 );\n\n\t\texpect( await page.$eval( '.llms-coupon-messages .llms-notice.llms-error li:first-child', el => el.textContent ) ).toBe( `Coupon code \"${ codeNotFound }\" not found.` );\n\n\t} );\n\n\t// Randomly failing during the create access plan step, investigate.\n\txit ( 'should accept an existing coupon, save it to session data, and allow it to be removed', async () => {\n\n\t\t// Add a valid coupon.\n\t\tawait applyCoupon( coupon );\n\t\tawait page.waitForSelector( '.llms-coupon-wrapper .llms-notice.llms-success' );\n\t\texpect( await page.$eval( '.llms-coupon-wrapper .llms-notice.llms-success', el => el.textContent ) ).toBe( `Coupon code \"${ coupon }\" has been applied to your order.` );\n\n\t\t// Navigate away.\n\t\tawait page.goto( process.env.WP_BASE_URL );\n\n\t\t// Return and it still found due to it being saved in session data.\n\t\tawait page.goto( planUrl );\n\n\t\texpect( await page.$eval( '.llms-coupon-wrapper .llms-notice.llms-success', el => el.textContent ) ).toMatchStringWithQuotes( `Coupon code \"${ coupon }\" has been applied to your order.` );\n\n\t\t// Remove it.\n\t\tawait click( '#llms-remove-coupon' );\n\t\tawait page.waitForSelector( '.llms-coupon-wrapper a[href=\"#llms-coupon-toggle\"]' );\n\t\texpect( await page.$eval( '.llms-coupon-wrapper a[href=\"#llms-coupon-toggle\"]', el => el.textContent ) ).toBe( 'Click here to enter your code' );\n\n\t} );\n\n} );\n"
  },
  {
    "path": "tests/e2e/tests/page-restrictions/course.test.js",
    "content": "import {\n\tclickAndWait,\n\tcreateUser,\n\tenrollStudent,\n\tgetPostTitleTextContent,\n\timportCourse,\n\tlogoutUser,\n} from '@lifterlms/llms-e2e-test-utils';\n\nimport {\n\tcreateURL,\n\tloginUser,\n} from '@wordpress/e2e-test-utils';\n\ndescribe( 'CourseRestrictions', () => {\n\n\tlet course = {},\n\t\tlessons = [];\n\n\tbeforeAll( async () => {\n\n\t\tawait importCourse( 'import-with-restrictions.json' );\n\t\tawait clickAndWait( '.llms-builder-launcher a.llms-button-primary' );\n\n\t\tcourse = await page.evaluate( () => window.llms_builder.course );\n\t\tlessons = course.sections[0].lessons;\n\n\t} );\n\n\tdescribe( 'Enrolled users', () => {\n\n\t\tbeforeAll( async () => {\n\t\t\tawait enrollStudent( course.id, 4 );\n\t\t\tawait logoutUser();\n\t\t\tawait loginUser( 'restrictions@email.tld', 'password' );\n\t\t} );\n\n\t\tit ( 'should see enrolled user content on the course page', async () => {\n\n\t\t\tawait page.goto( course.permalink );\n\t\t\texpect( await page.$eval( '.entry-content #enrolled-user-content', el => el.textContent ) ).toBe( 'Enrolled user content.' );\n\n\t\t} );\n\n\t\tit ( 'should be able to view a lesson with no restrictions', async () => {\n\n\t\t\tawait page.goto( lessons[0].permalink ); // Lesson: \"Regular\".\n\n\t\t\t// On the right page.\n\t\t\texpect( await getPostTitleTextContent() ).toBe( 'Regular' );\n\n\t\t\t// Mark complete is visible.\n\t\t\texpect( await page.$eval( '#llms_mark_complete', el => el.textContent ) ).toBe( 'Mark Complete' );\n\n\t\t} );\n\n\t\tit ( 'should be redirected when accessing a lesson with unmet prerequisites', async () => {\n\n\t\t\tawait page.goto( lessons[1].permalink ); // Lesson: \"Has Prereq\".\n\n\t\t\t// Redirected to the prerequisite lesson.\n\t\t\texpect( page.url() ).toBe( lessons[0].permalink );\n\n\t\t\t// Shown an error message.\n\t\t\texpect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent ) ).toMatchStringWithQuotes( 'The lesson \"Has Prereq\" cannot be accessed until the required prerequisite \"Regular\" is completed.' );\n\n\t\t} );\n\n\t\tit ( 'should be able to access lessons with prerequisites when the prerequisite is complete', async () => {\n\n\t\t\tawait page.goto( lessons[0].permalink ); // Lesson: \"Regular\".\n\n\t\t\tawait clickAndWait( '#llms_mark_complete' );\n\n\t\t\t// Redirected to the next lesson (the one with the prereq).\n\t\t\texpect( await page.url() ).toBe( lessons[1].permalink );\n\n\t\t\t// On the right page.\n\t\t\texpect( await getPostTitleTextContent() ).toBe( 'Has Prereq' );\n\n\t\t\t// Mark complete is visible.\n\t\t\texpect( await page.$eval( '#llms_mark_complete', el => el.textContent ) ).toMatchStringWithQuotes( 'Mark Complete' );\n\n\t\t} );\n\n\t\tit ( 'should be redirected when accessing a lesson that is not available because of a drip delay', async () => {\n\n\t\t\tawait page.goto( lessons[2].permalink ); // Lesson: \"Has Drip\".\n\n\t\t\t// Redirected to the course.\n\t\t\texpect( await page.url() ).toBe( course.permalink );\n\n\t\t\t// Shown an error message.\n\t\t\texpect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent.replace( /[“”‘’]/g, '\"' ).includes( 'The lesson \"Has Drip\" will be available on ' ) ) ).toBe( true );\n\n\t\t} );\n\n\t\tit ( 'should be able to view free lessons', async () => {\n\n\t\t\tawait page.goto( lessons[3].permalink ); // Lesson: \"Is Free\".\n\t\t\texpect( await page.$eval( '.entry-content #free-lesson-content', el => el.textContent ) ).toMatchStringWithQuotes( 'Free lesson content.' );\n\n\t\t} );\n\n\t\tit ( 'should be able to access and take a quiz', async () => {\n\n\t\t\tawait page.goto( lessons[4].permalink ); // Lesson: \"Has Quiz\"\n\n\t\t\t// On the right page.\n\t\t\texpect( await getPostTitleTextContent() ).toBe( 'Has Quiz' );\n\n\t\t\t// Take quiz button is visible.\n\t\t\texpect( await page.$eval( '#llms_start_quiz', el => el.textContent.trim() ) ).toBe( 'Take Quiz' );\n\n\t\t\tawait clickAndWait( '#llms_start_quiz' );\n\n\t\t\t// On the quiz page.\n\t\t\texpect( await page.url() ).toBe( lessons[4].quiz.permalink );\n\n\t\t\t// Start button visible.\n\t\t\texpect( await page.$eval( '#llms_start_quiz', el => el.textContent.trim() ) ).toBe( 'Start Quiz' );\n\n\t\t} );\n\n\t} );\n\n\tdescribe( 'Non-enrolled users', () => {\n\n\t\tbeforeAll( async () => {\n\t\t\tawait logoutUser();\n\t\t} );\n\n\t\tit ( 'should see sales page content on the course page', async () => {\n\n\t\t\tawait page.goto( course.permalink );\n\t\t\texpect( await page.$eval( '.entry-content #non-enrolled-user-content', el => el.textContent ) ).toBe( 'Non-enrolled user content.' );\n\n\t\t} );\n\n\t\tit ( 'should not be able to click syllabus links or view lesson URLs', async () => {\n\n\t\t\tawait page.goto( course.permalink );\n\n\t\t\t// Get all href values from the matching elements\n\t\t\tconst linkHrefs = await page.$$eval(\n\t\t\t\t'.llms-syllabus-wrapper .llms-lesson-preview a',\n\t\t\t\t( links ) => links.map( ( link ) => link.href )\n\t\t\t);\n\n\t\t\t// Verify each link ends with '/lesson/is-free/'\n\t\t\tlinkHrefs.forEach( ( href ) => {\n\t\t\t\texpect( href.endsWith( '/lesson/is-free/' ) ).toBe( true );\n\t\t\t} );\n\n\t\t} );\n\n\t\tit ( 'should be redirected to the course when accessing a lesson', async () => {\n\n\t\t\tawait page.goto( lessons[0].permalink ); // Lesson: \"Regular\".\n\n\t\t\t// Redirected to the course.\n\t\t\texpect( await page.url() ).toBe( course.permalink );\n\n\t\t\t// Shown an error message.\n\t\t\texpect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent ) ).toBe( 'You must enroll in this course to access course content.' );\n\n\t\t} );\n\n\t\tit ( 'should be able to view free lessons', async () => {\n\n\t\t\tawait page.goto( lessons[3].permalink ); // Lesson: \"Is Free\".\n\t\t\texpect( await page.$eval( '.entry-content #free-lesson-content', el => el.textContent ) ).toBe( 'Free lesson content.' );\n\n\t\t} );\n\n\t\tit ( 'should not be able to access quizzes', async () => {\n\n\t\t\tawait page.goto( lessons[4].quiz.permalink );\n\n\t\t\t// Redirected to dashboard.\n\t\t\texpect( await page.url() ).toBe( createURL( '/dashboard/my-courses/' ) );\n\n\t\t\t// Shown an error message.\n\t\t\texpect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent ) ).toBe( 'You must be logged in to take quizzes.' );\n\n\t\t} );\n\n\n\t} );\n\n\n} );\n"
  },
  {
    "path": "tests/e2e/tests/page-restrictions/sitewide-membership.test.js",
    "content": "/**\n * Test restrictions when a sitewide membership is enabled.\n *\n * @since 4.3.1\n */\n\nimport {\n\tclickAndWait,\n\tcreateMembership,\n\tsetSelect2Option,\n\tvisitSettingsPage,\n} from '@lifterlms/llms-e2e-test-utils';\n\nimport {\n\tloginUser,\n\tvisitAdminPage\n} from '@wordpress/e2e-test-utils';\n\ndescribe( 'SitewideMembershipRestrictions', () => {\n\n\t// beforeAll( async () => {\n\t// \tconst membership_id = await createMembership( 'Sitewide Membership' );\n\t// \tawait visitSettingsPage( { tab: 'memberships' } );\n\t// \tawait setSelect2Option( '#lifterlms_membership_required', membership_id );\n\t// \tawait clickAndWait( '.llms-save .llms-button-primary' );\n\t// } );\n\n\tit ( 'should not allow logged out users to view the homepage', async () => {\n\t} );\n\n} );\n"
  },
  {
    "path": "tests/e2e/tests/settings/copy-prevention.test.js",
    "content": "/**\n * Test the Setup Wizard\n *\n * @since 3.37.8\n * @since 3.37.14 Fix package references.\n * @since 4.5.0 Use package functions.\n * @since 4.12.0 Added registration test with a voucher.\n * @since 5.0.0 Added tests for form field localization (country, state, etc...).\n * @since 5.5.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n */\n\nimport {\n\tclickAndWait,\n\tgetPostTitleSelector,\n\thighlightNode,\n\tlogoutUser,\n\tloginStudent,\n\tsetCheckboxSetting,\n\tvisitPage,\n\tvisitSettingsPage,\n} from '@lifterlms/llms-e2e-test-utils';\n\nimport { switchUserToAdmin } from '@wordpress/e2e-test-utils';\n\nconst context = browser.defaultBrowserContext();\ncontext.overridePermissions( process.env.WP_BASE_URL, [ 'clipboard-read' ] );\n\n/**\n * Watch for an event to run.\n *\n * @since 5.6.0\n *\n * @param {string} eventName The event name.\n * @return {void}\n */\nasync function watchForEvent( eventName ) {\n\treturn await page.evaluate( ( _eventName ) => {\n\t\tdocument.addEventListener( _eventName, ( event ) => window.watchCopyPreventionEvents( { eventName: _eventName, event } ) );\n\t}, eventName );\n}\n\ndescribe( 'Setting/CopyPrevention', () => {\n\n\tlet caughtEvents = [];\n\n\tbeforeAll( async () => {\n\t\tawait visitSettingsPage();\n\t\tawait setCheckboxSetting( '#lifterlms_content_protection', true );\n\t\tawait page.exposeFunction( 'watchCopyPreventionEvents', ( event ) => {\n\t\t\tcaughtEvents.push( event );\n\t\t} );\n\t} );\n\n\tafterAll( async () => {\n\t\tawait visitSettingsPage();\n\t\tawait setCheckboxSetting( '#lifterlms_content_protection', false );\n\t\tawait logoutUser();\n\t} );\n\n\tbeforeEach( async () => {\n\t\tawait visitPage( 'integrity-test' );\n\t} );\n\n\tafterEach( () => {\n\t\tcaughtEvents = [];\n\t} );\n\n\tdescribe( 'AdminUser', () => {\n\n\t\tbeforeAll( async() => {\n\t\t\tawait switchUserToAdmin();\n\t\t} );\n\n\t\tit ( 'is allowed to copy content', async () => {\n\n\t\t\twatchForEvent( 'llms-copy-prevented' );\n\t\t\texpect( await highlightNode( getPostTitleSelector(), true ) ).toBe( 'Integrity-Test' );\n\t\t\texpect( caughtEvents.length ).toStrictEqual( 0 );\n\n\t\t} );\n\n\t} );\n\n\tdescribe( 'StudentUser', () => {\n\n\t\tbeforeAll( async() => {\n\n\t\t\tawait logoutUser();\n\t\t\tawait loginStudent( 'validcreds@email.tld', 'password' );\n\n\t\t} );\n\n\t\tit ( 'is not allowed to copy content', async () => {\n\n\t\t\twatchForEvent( 'llms-copy-prevented' );\n\t\t\texpect( await highlightNode( getPostTitleSelector(), true ) ).toBe( 'Copying is not allowed.' );\n\t\t\texpect( caughtEvents[0].eventName ).toBe( 'llms-copy-prevented' );\n\n\t\t} );\n\n\t} );\n\n\tdescribe( 'LoggedOutUser', () => {\n\n\t\tbeforeAll( async() => {\n\n\t\t\tawait logoutUser();\n\t\t\tawait visitPage( 'integrity-test' );\n\n\t\t} );\n\n\t\tit ( 'is not allowed to copy content', async () => {\n\n\t\t\tawait visitPage( 'integrity-test' );\n\t\t\twatchForEvent( 'llms-copy-prevented' );\n\t\t\texpect( await highlightNode( getPostTitleSelector(), true ) ).toBe( 'Copying is not allowed.' );\n\t\t\texpect( caughtEvents[0].eventName ).toBe( 'llms-copy-prevented' );\n\n\t\t} );\n\n\t} );\n} );\n"
  },
  {
    "path": "tests/e2e/tests/student/__snapshots__/open-registration.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`OpenRegistration Registration should register a new user with a voucher. 1`] = `\" <div class=\\\\\"llms-notification-title\\\\\">Course enrollment success!</div> <div class=\\\\\"llms-notification-body\\\\\"><p>Congratulations! You enrolled in LifterLMS Quickstart Course</p> </div> \"`;\n"
  },
  {
    "path": "tests/e2e/tests/student/__snapshots__/voucher.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`StudentDashboard/RedeemVoucher Should redeem a valid voucher 1`] = `\"Voucher redeemed successfully!\"`;\n\nexports[`StudentDashboard/RedeemVoucher Should redeem a valid voucher 2`] = `\" <h4 class=\\\\\"llms-notification-title\\\\\">Course enrollment success!</h4> <div class=\\\\\"llms-notification-body\\\\\"><p>Congratulations! You enrolled in LifterLMS Quickstart Course</p> </div> \"`;\n"
  },
  {
    "path": "tests/e2e/tests/student/login.test.js",
    "content": "/**\n * Test the Setup Wizard\n *\n * @since 3.37.8\n * @since 3.37.14 Fix package references.\n * @since 5.5.0 Use user created via setup-e2e.sh in favor of `createUser()`.\n */\n\nimport {\n\tloginStudent,\n\tlogoutUser,\n\tvisitPage,\n} from '@lifterlms/llms-e2e-test-utils';\n\nimport {\n\tloginUser,\n\tvisitAdminPage\n} from '@wordpress/e2e-test-utils';\n\ndescribe( 'StudentDashboardLogin', () => {\n\n\tafterEach( async () => {\n\t\tawait logoutUser();\n\t} );\n\n\tit ( 'should not allow a user to login if they are already logged in.', async () => {\n\n\t\tawait loginUser();\n\t\tawait visitPage( 'dashboard' );\n\t\tawait expect( await page.$( '.llms-new-person-login-wrapper > h4.llms-form-heading' ) ).toBeNull();\n\n\t} );\n\n\tit ( 'should display an error message when invalid credentials are used.', async () => {\n\n\t\tawait loginStudent( 'fake@fake.tld', 'fake' );\n\t\tawait expect( await page.$eval( '.llms-notice.llms-error li', el => el.textContent ) ).toBe( 'Could not find an account with the supplied email address and password combination.' );\n\n\t} );\n\n\tit ( 'should allow a user with valid credentials to login.', async () => {\n\n\t\tawait loginStudent( 'validcreds@email.tld', 'password' );\n\t\texpect( await page.$eval( 'h2.llms-sd-title', el => el.textContent ) ).toBe( 'Dashboard' );\n\n\t} );\n\n} );\n"
  },
  {
    "path": "tests/e2e/tests/student/open-registration.test.js",
    "content": "/**\n * Test the Setup Wizard\n *\n * @since 3.37.8\n * @since 3.37.14 Fix package references.\n * @since 4.5.0 Use package functions.\n * @since 4.12.0 Added registration test with a voucher.\n * @since 5.0.0 Added tests for form field localization (country, state, etc...).\n * @since 5.5.0 Use `waitForTimeout()` in favor of deprecated `waitFor()`.\n */\n\nimport {\n\tclickAndWait,\n\tcreateVoucher,\n\tfillField,\n\tlogoutUser,\n\tregisterStudent,\n\tselect2Select,\n\ttoggleOpenRegistration,\n\tvisitPage,\n} from '@lifterlms/llms-e2e-test-utils';\n\nimport { visitAdminPage } from '@wordpress/e2e-test-utils';\n\nlet openRegStatus = null;\n\n/**\n * Toggles the open registration setting on or off\n *\n * @since 3.37.8\n * @since 4.5.0 Use toggleOpenRegistration function from utils pacakage.\n *\n * @param  {Boolean} status Whether to toggle on (`true`) or off (`false`).\n * @return {void}\n */\nconst toggleOpenReg = async function( status ) {\n\n\tif ( openRegStatus === status ) {\n\t\treturn;\n\t}\n\tawait toggleOpenRegistration( status );\n\n}\n\ndescribe( 'OpenRegistration', () => {\n\n\tafterEach( async () => {\n\t\tawait logoutUser();\n\t} );\n\n\tdescribe( 'Registration', () => {\n\n\t\tit ( 'should not allow registration because user is already logged in.', async () => {\n\n\t\t\tawait toggleOpenReg( true );\n\t\t\tawait visitPage( 'dashboard' );\n\t\t\tawait expect( await page.$( '.llms-new-person-form-wrapper > h4.llms-form-heading' ) ).toBeNull();\n\n\t\t} );\n\n\t\tit ( 'should allow registration.', async () => {\n\n\t\t\tawait toggleOpenReg( true );\n\t\t\tawait logoutUser();\n\t\t\tawait visitPage( 'dashboard' );\n\t\t\texpect( await page.$eval( '.llms-new-person-form-wrapper > .llms-form-heading', el => el.textContent ) ).toBe( 'Register' );\n\n\t\t} );\n\n\t\tit ( 'should register a new user.', async () => {\n\n\t\t\tawait toggleOpenReg( true );\n\t\t\tawait registerStudent();\n\t\t\texpect( await page.$eval( 'h2.llms-sd-title', el => el.textContent ) ).toBe( 'Dashboard' );\n\n\t\t} );\n\n\t\tit ( 'should register a new user with a voucher.', async() => {\n\n\t\t\tawait toggleOpenReg( true );\n\t\t\tconst codes = await createVoucher( { codes: 1, uses: 1 } );\n\t\t\tawait registerStudent( { voucher: codes[0] } );\n\n\t\t\texpect( await page.$eval( 'h2.llms-sd-title', el => el.textContent ) ).toBe( 'Dashboard' );\n\n\t\t\tawait page.waitForSelector( '.llms-notification .llms-notification-main' );\n\t\t\texpect( await page.$eval( '.llms-notification .llms-notification-main', el => el.innerHTML ) ).toMatchSnapshot();\n\n\t\t} );\n\n\t\tit ( 'should not allow registration because open registration is disabled.', async () => {\n\n\t\t\tawait toggleOpenReg( false );\n\t\t\tawait logoutUser();\n\t\t\tawait visitPage( 'dashboard' );\n\t\t\tawait expect( await page.$( '.llms-new-person-form-wrapper > h4.llms-form-heading' ) ).toBeNull();\n\n\t\t} );\n\n\t} );\n\n\tdescribe( 'Localization', () => {\n\n\t\tit ( 'should localize city, state, and postcode fields when changing the selected country', async () => {\n\n\t\t\tconst selectCountry = async ( country ) => {\n\t\t\t\tawait select2Select( '#llms_billing_country', country );\n\t\t\t};\n\n\t\t\tconst getStatesList = async () => {\n\t\t\t\tconst list = await page.$$eval( '#llms_billing_state option', els =>\n\t\t\t\t\tels.map( ( { value, textContent } ) => ( [ value, textContent ] ) ) );\n\n\t\t\t\treturn Object.fromEntries( list );\n\t\t\t};\n\n\t\t\tawait toggleOpenReg( true );\n\t\t\tawait logoutUser();\n\t\t\tawait visitPage( 'dashboard' );\n\n\t\t\t// China.\n\t\t\tawait selectCountry( 'China' );\n\t\t\texpect( await page.$eval( 'label[for=\"llms_billing_state\"]', el => el.textContent ) ).toBe( 'Province*' );\n\t\t\tlet statesList = await getStatesList();\n\t\t\texpect( Object.values( statesList ).some( state => state.includes( 'Zhejiang' ) ) ).toBe( true );\n\n\t\t\tawait page.waitForTimeout( 1000 );\n\n\t\t\t// Peru changes name of the State & City fields.\n\t\t\tawait selectCountry( 'Peru' );\n\t\t\tstatesList = await getStatesList();\n\t\t\texpect( Object.values( statesList ).some( state => state.includes( 'Lima' ) ) ).toBe( true );\n\t\t\texpect( await page.$eval( 'label[for=\"llms_billing_city\"]', el => el.textContent ) ).toBe( 'District*' );\n\t\t\texpect( await page.$eval( 'label[for=\"llms_billing_zip\"]', el => el.textContent ) ).toBe( 'Postal code*' );\n\n\t\t\tawait page.waitForTimeout( 1000 );\n\n\t\t\t// United States.\n\t\t\tawait selectCountry( 'United States' );\n\t\t\tstatesList = await getStatesList();\n\t\t\texpect( Object.values( statesList ).some( state => state.includes( 'California' ) ) ).toBe( true );\n\t\t\texpect( await page.$eval( 'label[for=\"llms_billing_state\"]', el => el.textContent ) ).toBe( 'State*' );\n\t\t\texpect( await page.$eval( 'label[for=\"llms_billing_city\"]', el => el.textContent ) ).toBe( 'City*' );\n\t\t\texpect( await page.$eval( 'label[for=\"llms_billing_zip\"]', el => el.textContent ) ).toBe( 'ZIP code*' );\n\n\t\t} );\n\n\t} );\n\n} );\n"
  },
  {
    "path": "tests/e2e/tests/student/voucher.test.js",
    "content": "import {\n\tclickAndWait,\n\tcreateVoucher,\n\tfillField,\n\tloginStudent,\n\tlogoutUser,\n\tvisitPage,\n} from '@lifterlms/llms-e2e-test-utils';\n\ndescribe( 'StudentDashboard/RedeemVoucher', () => {\n\n\tafterEach( async () => {\n\t\tawait logoutUser();\n\t} );\n\n\t// Randomly failing, investigate to fix later.\n\txit ( 'Should redeem a valid voucher', async () => {\n\n\t\t// Setup.\n\t\tconst [ code ] = await createVoucher( { codes: 1, uses: 1 } );\n\n\t\tawait logoutUser();\n\n\t\t// Use the voucher.\n\t\tawait loginStudent( 'voucher@email.tld', 'password' );\n\t\tawait visitPage( 'dashboard/redeem-voucher' );\n\t\tawait fillField( '#llms-voucher-code', code );\n\t\tawait clickAndWait( '#llms-redeem-voucher-submit' );\n\n\t\t// Success.\n\t\texpect( await page.$eval( '.llms-notice.llms-success', el => el.textContent ) ).toMatchSnapshot();\n\n\t\tawait page.waitForSelector( '.llms-notification .llms-notification-main' );\n\t\texpect( await page.$eval( '.llms-notification .llms-notification-main', el => el.innerHTML ) ).toMatchSnapshot();\n\n\t} );\n\n\tit ( 'Should display an error for an invalid voucher', async() => {\n\n\t\tconst code = 'fakecode';\n\n\t\tawait logoutUser();\n\n\t\tawait loginStudent( 'voucher@email.tld', 'password' );\n\t\tawait visitPage( 'dashboard/redeem-voucher' );\n\t\tawait fillField( '#llms-voucher-code', 'fakecode' );\n\t\tawait clickAndWait( '#llms-redeem-voucher-submit' );\n\n\t\t// Error message.\n\t\texpect( await page.$eval( '.llms-notice.llms-error', el => el.textContent.trim() ) ).toMatchStringWithQuotes( `Voucher code \"${ code }\" could not be found.` );\n\n\t} );\n\n} );\n"
  },
  {
    "path": "tests/e2e/tests/view-manager/__snapshots__/view-manager.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`ViewManager Checkout should show an already enrolled notice when viewing as a student. 1`] = `\"You already have access to this Course! Visit your dashboard <a href=\\\\\"http://localhost:8080/dashboard/\\\\\">here.</a>\"`;\n"
  },
  {
    "path": "tests/e2e/tests/view-manager/view-manager.test.js",
    "content": "/**\n * Test the LifterLMS View Manager\n *\n * @since 4.16.0\n */\n\nimport {\n\tclickAndWait,\n\tcreateAccessPlan,\n\tcreateCourse,\n\tlogoutUser,\n\ttoggleOpenRegistration,\n\tvisitPage,\n} from '@lifterlms/llms-e2e-test-utils';\n\nimport {\n\tvisitAdminPage\n} from '@wordpress/e2e-test-utils';\n\n\n/**\n * Select a view from the view manager menu in the WP Admin bar.\n *\n * @since 4.16.0\n *\n * @param {String} view View name to select. Accepts \"self\", \"visitor\", or \"student\".\n * @return {Void}\n */\nasync function selectView( view ) {\n\n\tconst\n\t\ttopLevelSelector = '#wp-admin-bar-llms-view-as-menu',\n\t\tviewSelector     = `#wp-admin-bar-llms-view-as--${ view }`;\n\n\tawait page.waitForSelector( topLevelSelector );\n\tawait page.hover( topLevelSelector );\n\n\tawait page.waitForSelector( viewSelector );\n\tawait clickAndWait( `${ viewSelector } a.ab-item` );\n\n}\n\ndescribe( 'ViewManager', () => {\n\n\tbeforeAll( async () => {\n\t\t// Ensure we're a logged in admin that can use the view manager.\n\t\tawait visitAdminPage( '/' );\n\t} );\n\n\tafterAll( async () => {\n\t\tawait logoutUser();\n\t} );\n\n\tdescribe( 'Dashboard', () => {\n\n\t\tbeforeAll( async () => {\n\t\t\tawait toggleOpenRegistration( true );\n\t\t} );\n\t\tafterAll( async () => {\n\t\t\tawait toggleOpenRegistration( false );\n\t\t} );\n\n\t\tbeforeEach( async () => {\n\t\t\tawait visitPage( 'dashboard' );\n\t\t} );\n\n\t\tit ( 'should show forms when viewing as a visitor.', async () => {\n\n\t\t\tawait selectView( 'visitor' );\n\n\t\t\t// Login and registration forms should exist.\n\t\t\tawait page.waitForSelector( '.llms-person-login-form-wrapper' );\n\t\t\tawait page.waitForSelector( '.llms-new-person-form-wrapper' );\n\n\t\t\t// Dashboard header should not exist.\n\t\t\texpect( await page.$( '.llms-sd-header' ) ).toBeNull();\n\n\t\t} );\n\n\t\tit ( 'should show the dashboard when viewing as a student.', async () => {\n\n\t\t\tawait selectView( 'student' );\n\n\t\t\t// Login and registration forms should not exist.\n\t\t\texpect( await page.$( '.llms-person-login-form-wrapper' ) ).toBeNull();\n\t\t\texpect( await page.$( '.llms-new-person-form-wrapper' ) ).toBeNull();\n\n\t\t\t// Dashboard header should exist.\n\t\t\tawait page.waitForSelector( '.llms-sd-header' );\n\n\t\t} );\n\n\t} );\n\n\tdescribe( 'Checkout', () => {\n\n\t\tbeforeAll( async () => {\n\n\t\t\tconst\n\t\t\t\tcourseId = await createCourse( 'View Manager Test' ),\n\t\t\t\tplanUrl = await createAccessPlan( {\n\t\t\t\t\tpostId: courseId,\n\t\t\t\t\tprice: 5.00,\n\t\t\t\t\ttitle: 'Test VM Plan ' + parseInt( Math.random() * 100000 ),\n\t\t\t\t} );\n\n\t\t\tawait page.goto( planUrl );\n\n\t\t} );\n\n\t\t// Randomly failing during the create access plan step, investigate.\n\t\txit ( 'should show the checkout form when viewing as a visitor.', async () => {\n\n\t\t\tawait selectView( 'visitor' );\n\n\t\t\t// Should show the checkout form.\n\t\t\tawait page.waitForSelector( '#llms-product-purchase-form' );\n\n\t\t} );\n\n\t\t// Randomly failing during the create access plan step, investigate.\n\t\txit ( 'should show an already enrolled notice when viewing as a student.', async () => {\n\n\t\t\tawait selectView( 'student' );\n\n\t\t\t// Should show a notice.\n\t\t\texpect( await page.$eval( '.llms-checkout-wrapper .llms-notice', el => el.innerHTML ) ).toMatchSnapshot();\n\n\t\t\t// Should not show the checkout form.\n\t\t\texpect( await page.$( '#llms-product-purchase-form' ) ).toBeNull();\n\n\t\t} );\n\n\t} );\n\n} );\n"
  },
  {
    "path": "tests/phpunit/README.md",
    "content": "LifterLMS Tests\n===============\n\n## Running Tests Locally\n\nTo install tests locally you'll first need a local MySQL server (5.6 or later) and PHP 7.1. Xdebug is required to generate code coverage reports.\n\n### Installing\n\n1. Install all development dependencies via `composer install`\n2. Install the testing database and environment: `composer run-script tests-install`\n\n### Running Tests\n\n+ Run tests: `composer run-script tests-run`\n+ Run tests by group `composer run-script tests-run -- --group LLMS_Post_Model`\n+ Run a specific tests `composer run-script tests-run -- --filter test_my_test_method`\n+ Run tests and generate code coverage in HTML format: `composer run-script tests-run -- --coverage-html tmp/coverage`\n+ Run tests and generate text code coverage: `composer run-script tests-run -- --coverage-text`\n\n## Automated Testing\n\nTests are run automatically on commits and pull requests via [CircleCI](https://circleci.com/gh/gocodebox/lifterlms/tree/master).\n\n## Code Coverage\n\nCode coverage is available on [Code Climate](https://codeclimate.com/github/gocodebox/lifterlms/code?sort=-test_coverage) and updated automatically after each CircleCI build.\n\n## Writing Tests\n\n+ Each test file should roughly correspond to an associated source file, e.g. the `functions/class-llms-test-functions-access-plans.php` test file covers code in the `functions/llms-functions-access-plans.php` file.\n+ Each test method should cover a single method or function with one or more assertions\n+ A single method or function can have multiple associated test methods if it's a large or complex method\n+ Use coverage reports to examine which lines your tests are covering and aim for 100% coverage\n+ In addition to covering each line of a method/function, make sure to test common input and edge cases.\n+ Remember that only methods prefixed with test will be run so use helper methods liberally to keep test methods small and reduce code duplication.\n+ If there is a common helper method used in multiple test files, consider adding it to the `LLMS_UnitTestCase` class so it can be shared by all test cases.\n+ The test suite uses the `lifterlms-tests` library which is aimed to provide shared utilities for testing the LifterLMS core, as well as LifterLMS add-ons. Many methods and utilities are available and documented in the libraries GitHub repo: https://github.com/gocodebox/lifterlms-tests\n+ Filters, options, and actions persist between test cases so be sure to remove or reset them in your test method or in the `tear_down()` method.\n"
  },
  {
    "path": "tests/phpunit/bootstrap.php",
    "content": "<?php\n/**\n * LifterLMS Add-On Testing Bootstrap\n *\n * @package LifterLMS/Tests\n *\n * @since 3.3.1\n * @since 3.28.0 Unknown\n * @since 3.37.8 Added class variable to access the tests assets directory.\n */\n\nrequire_once './vendor/lifterlms/lifterlms-tests/bootstrap.php';\n\nclass LLMS_Unit_Tests_Bootstrap extends LLMS_Tests_Bootstrap {\n\n\t/**\n\t * __FILE__ reference, should be defined in the extending class\n\t *\n\t * @var [type]\n\t */\n\tpublic $file = __FILE__;\n\n\t/**\n\t * Name of the testing suite\n\t *\n\t * @var string\n\t */\n\tpublic $suite_name = 'LifterLMS';\n\n\t/**\n\t * Main PHP File for the plugin\n\t *\n\t * @var string\n\t */\n\tpublic $plugin_main = 'lifterlms.php';\n\n\t/**\n\t * Location of testing assets.\n\t *\n\t * @var string\n\t */\n\tpublic $assets_dir = '';\n\n\t/**\n\t * Determines if the LifterLMS core should be loaded\n\t *\n\t * @var bool\n\t */\n\tpublic $use_core = false;\n\n\t/**\n\t * Install the plugin\n\t *\n\t * @return   void\n\t * @since    3.28.0\n\t * @version  3.28.0\n\t */\n\tpublic function install() {\n\n\t\tparent::install();\n\n\t\t// install LLMS\n\t\tLLMS_Install::install();\n\n\t\t// Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374\n\t\tif ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) {\n\t\t\t$GLOBALS['wp_roles']->reinit();\n\t\t} else {\n\t\t\t$GLOBALS['wp_roles'] = null;\n\t\t\twp_roles();\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Load the plugin\n\t *\n\t * @since 3.28.0\n\t * @since 3.37.8 Use $this->assets_dir.\n\t *\n\t * @return void\n\t */\n\tpublic function load() {\n\n\t\t// Assets are shared between phpunit and e2e tests.\n\t\t$this->assets_dir = dirname( $this->tests_dir ) . '/assets/';\n\n\t\t// override this constant otherwise a bunch of includes will fail when running tests\n\t\t// define( 'LLMS_PLUGIN_DIR', trailingslashit( $this->plugin_dir ) );\n\n\t\tparent::load();\n\n\t}\n\n\t/**\n\t * Uninstall the plugin.\n\t *\n\t * @return  void\n\t * @since   3.28.0\n\t * @version 3.28.0\n\t */\n\tpublic function uninstall() {\n\n\t\tparent::uninstall();\n\n\t\t// Clean existing install first.\n\t\tdefine( 'LLMS_REMOVE_ALL_DATA', true );\n\t\tinclude( $this->plugin_dir . '/uninstall.php' );\n\n\t}\n\n}\n\nglobal $lifterlms_tests;\n$lifterlms_tests = new LLMS_Unit_Tests_Bootstrap();\nreturn $lifterlms_tests;\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-admin-tool-test-case.php",
    "content": "<?php\n/**\n * Admin Tool base test case\n *\n * @package LifterLMS/Tests/Framework\n *\n * @since 5.3.0\n */\n\nrequire_once 'class-llms-unit-test-case.php';\n\nclass LLMS_Admin_Tool_Test_Case extends LLMS_UnitTestCase {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * This must be added to extending classes.\n\t *\n\t * @var sting\n\t */\n\t// const CLASS_NAME = 'LLMS_Admin_Tool_Class_Name';\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include abstract required classes.\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\n\t\t// Abstract tool.\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/abstracts/llms-abstract-admin-tool.php';\n\n\t\t// Include the tool itself.\n\t\t$filename = 'class-' . str_replace( '_', '-', strtolower( static::CLASS_NAME ) ) . '.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/tools/' . $filename;\n\n\t}\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$classname = static::CLASS_NAME;\n\t\t$this->main = new $classname();\n\n\t}\n\n\t/**\n\t * Test get_description()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_description() {\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_description' );\n\t\t$this->assertTrue( ! empty( $res ) );\n\t\t$this->assertTrue( is_string( $res ) );\n\n\t}\n\n\t/**\n\t * Test get_label()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_label() {\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_label' );\n\t\t$this->assertTrue( ! empty( $res ) );\n\t\t$this->assertTrue( is_string( $res ) );\n\n\t}\n\n\t/**\n\t * Test get_text()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_text() {\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_text' );\n\t\t$this->assertTrue( ! empty( $res ) );\n\t\t$this->assertTrue( is_string( $res ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-notification-test-case.php",
    "content": "<?php\nrequire_once 'class-llms-unit-test-case.php';\n\n/**\n * Unit Test Case with tests and utilities specific to testing LifterLMS Notification Classes\n *\n * @since 3.8.0\n */\nabstract class LLMS_NotificationTestCase extends LLMS_UnitTestCase {\n\n\t/**\n\t * The ID of the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $notification_id = '';\n\n\t/**\n\t * The name of the controller class for the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $controller_class = '';\n\n\t/**\n\t * The name of the view class for the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $view_class = '';\n\n\t/**\n\t * Function used to setup arguments passed to a notification controller's `action_callback()` function.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tabstract protected function setup_args();\n\n\t/**\n\t * Retrieve a notification controller for the tested notification.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return object A child class of an LLMS_Abstract_Notification_Controller.\n\t */\n\tprotected function get_controller() {\n\n\t\t$main = llms()->notifications();\n\t\treturn $main->get_controller( $this->notification_id );\n\n\t}\n\n\t/**\n\t * Retrieve a notification object for the tested notification.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return LLMS_Notification\n\t */\n\tprotected function get_notification() {\n\n\t\t$this->last_setup_args = $this->setup_args();\n\n\t\t// Create a notification.\n\t\t$this->get_controller()->action_callback( ...$this->last_setup_args );\n\n\t\tglobal $wpdb;\n\t\treturn new LLMS_Notification( $wpdb->insert_id );\n\n\t}\n\n\t/**\n\t * Retrieve a notification view for the tested notification.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return object A child class of an LLMS_Abstract_Notification_View.\n\t */\n\tprotected function get_view() {\n\n\t\t$main = llms()->notifications();\n\t\treturn $main->get_view( $this->get_notification() );\n\n\t}\n\n\t/**\n\t * Test notification view and controller are registered.\n\t *\n\t * @since 3.8.0\n\t * @since 6.0.0 Test the notification view exists.\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_registered() {\n\n\t\t$controller = $this->get_controller();\n\t\t$this->assertTrue( is_a( $controller, 'LLMS_Abstract_Notification_Controller' ) );\n\t\t$this->assertTrue( is_a( $controller, $this->controller_class ) );\n\t\t$this->assertEquals( $this->notification_id, $controller->id );\n\n\t\t$view = $this->get_view();\n\n\t\t$this->assertTrue( is_a( $view, 'LLMS_Abstract_Notification_View' ) );\n\t\t$this->assertTrue( is_a( $view, $this->view_class ) );\n\t\t$this->assertEquals( $this->notification_id, $view->trigger_id );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-payment-gateway-mock.php",
    "content": "<?php\n/**\n * Mock payment gateway for testing.\n *\n * @since 3.37.6\n */\nclass LLMS_Payment_Gateway_Mock extends LLMS_Payment_Gateway {\n\n\t/**\n\t * Constructor\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return void\n\t */\n\tpublic function __construct() {\n\n\t\t$this->id                = 'mock';\n\t\t$this->admin_description = __( 'Mock payment gateway used for unit testing', 'lifterlms' );\n\t\t$this->admin_title       = __( 'Mock', 'lifterlms' );\n\t\t$this->title             = __( 'Mock', 'lifterlms' );\n\t\t$this->description       = __( 'Make mock payments', 'lifterlms' );\n\n\t\t$this->supports = array(\n\t\t\t'checkout_fields'    => false,\n\t\t\t'refunds'            => true,\n\t\t\t'single_payments'    => true,\n\t\t\t'recurring_payments' => true,\n\t\t\t'test_mode'          => false,\n\t\t);\n\n\t}\n\n\t/**\n\t * Handle a Pending Order\n\t *\n\t * @since 3.37.6\n\t *\n\t * @param LLMS_Order        $order  Order object.\n\t * @param LLMS_AccessPlan   $plan   Access plan object.\n\t * @param LLMS_Student      $person Student object.\n\t * @param false|LLMS_Coupon $coupon Coupon object, or false if none is being used.\n\t * @return void\n\t */\n\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {\n\n\t\t$payment_type = 'single';\n\n\t\tif ( $order->is_recurring() ) {\n\t\t\t$payment_type = $order->has_trial() ? 'trial' : 'recurring';\n\t\t}\n\n\t\t$order->record_transaction(\n\t\t\tarray(\n\t\t\t\t'amount'             => $order->get_initial_price( array(), 'float' ),\n\t\t\t\t'source_description' => 'Mock Payment',\n\t\t\t\t'transaction_id'     => uniqid( 'mock-' ),\n\t\t\t\t'status'             => 'llms-txn-succeeded',\n\t\t\t\t'payment_gateway'    => $this->id,\n\t\t\t\t'payment_type'       => $payment_type,\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Called by scheduled actions to charge an order for a scheduled recurring transaction\n\t *\n\t * This function must be defined by gateways which support recurring transactions\n\t *\n\t * @since 3.37.6\n\t *\n\t * @param LLMS_Order $order Instance LLMS_Order for the order being processed.\n\t * @return void\n\t */\n\tpublic function handle_recurring_transaction( $order ) {\n\n\t\t$order->record_transaction(\n\t\t\tarray(\n\t\t\t\t'amount'             => $order->get_price( 'total', array(), 'float' ),\n\t\t\t\t'source_description' => 'Mock Payment',\n\t\t\t\t'transaction_id'     => uniqid( 'mock-' ),\n\t\t\t\t'status'             => 'llms-txn-succeeded',\n\t\t\t\t'payment_gateway'    => $this->id,\n\t\t\t\t'payment_type'       => 'recurring',\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Determine if the gateway is enabled according to admin settings checkbox.\n\t *\n\t * The mock gateway is always enabled.\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return boolean\n\t */\n\tpublic function is_enabled() {\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-post-model-unit-test-case.php",
    "content": "<?php\n/**\n * Unit Test Case with tests and utilities specific to testing classes\n * which extend the LLMS_Post_Model\n *\n * @since 3.4.0\n * @since 3.34.0 Add tests for new `set_bulk()` method and other recently added properties.\n */\n\nrequire_once 'class-llms-unit-test-case.php';\n\nclass LLMS_PostModelUnitTestCase extends LLMS_UnitTestCase {\n\n\t/**\n\t * class name for the model being tested by the class\n\t * @var  string\n\t */\n\tprotected $class_name = '';\n\n\t/**\n\t * db post type of the model being tested\n\t * @var  string\n\t */\n\tprotected $post_type = '';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t *\n\t * This should match, exactly, the object's $properties array\n\t *\n\t * @since 3.4.0\n\t * @since 4.5.0 Use unit test utils to retrieve `properties` array automatically.\n\t *\n\t * @return array\n\t */\n\tprotected function get_properties() {\n\t\treturn LLMS_Unit_Test_Util::get_private_property_value( new $this->class_name( 'new' ), 'properties' );\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t * This is used by test_getters_setters\n\t * @return   array\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tprotected function get_data() {\n\t\treturn array();\n\t}\n\n\t/*\n\t\t             /$$     /$$ /$$\n\t\t            | $$    |__/| $$\n\t\t /$$   /$$ /$$$$$$   /$$| $$  /$$$$$$$\n\t\t| $$  | $$|_  $$_/  | $$| $$ /$$_____/\n\t\t| $$  | $$  | $$    | $$| $$|  $$$$$$\n\t\t| $$  | $$  | $$ /$$| $$| $$ \\____  $$\n\t\t|  $$$$$$/  |  $$$$/| $$| $$ /$$$$$$$/\n\t\t \\______/    \\___/  |__/|__/|_______/\n\t*/\n\n\t/**\n\t * Will hold an instance of the model being tested by the class.\n\t *\n\t * @var LLMS_Post_Model\n\t */\n\tprotected $obj;\n\n\n\t/**\n\t * Create a post that can be tested\n\t * @param    string|array  $args  string for post title or array of arguments to use when creating the post\n\t * @return   void\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tprotected function create( $args = 'test title' ) {\n\n\t\t$this->obj = new $this->class_name( 'new', $args );\n\n\t}\n\n\t/*\n\t\t   /$$                           /$$\n\t\t  | $$                          | $$\n\t\t /$$$$$$    /$$$$$$   /$$$$$$$ /$$$$$$   /$$$$$$$\n\t\t|_  $$_/   /$$__  $$ /$$_____/|_  $$_/  /$$_____/\n\t\t  | $$    | $$$$$$$$|  $$$$$$   | $$   |  $$$$$$\n\t\t  | $$ /$$| $$_____/ \\____  $$  | $$ /$$\\____  $$\n\t\t  |  $$$$/|  $$$$$$$ /$$$$$$$/  |  $$$$//$$$$$$$/\n\t\t   \\___/   \\_______/|_______/    \\___/ |_______/\n\t*/\n\n\t/**\n\t * Test creation of the model\n\t * @return   void\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function test_create_model() {\n\n\t\t$this->create( 'test title' );\n\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t$test = llms_get_post( $id );\n\n\t\t$this->assertEquals( $id, $test->get( 'id' ) );\n\t\t$this->assertEquals( $this->post_type, $test->get( 'type' ) );\n\t\t$this->assertEquals( 'test title', $test->get( 'title' ) );\n\n\t}\n\n\t/**\n\t * Test getters and setters\n\t *\n\t * @return   void\n\t * @since    3.4.0\n\t * @version  3.28.0\n\t */\n\tpublic function test_getters_setters() {\n\n\t\t$this->create( 'test title' );\n\t\t$props = $this->get_properties();\n\t\t$data = $this->get_data();\n\n\t\tif ( ! $data ) {\n\t\t\t$this->markTestSkipped( 'No properties to test.' );\n\t\t}\n\n\t\tforeach ( $props as $prop => $type ) {\n\n\t\t\t// set should return true\n\t\t\t$this->assertTrue( $this->obj->set( $prop, $data[ $prop ] ) );\n\n\t\t\t// make sure gotten value equals set val\n\t\t\t$this->assertEquals( $data[ $prop ], $this->obj->get( $prop ) );\n\n\t\t\t// check type\n\t\t\tswitch ( $type ) {\n\n\t\t\t\tcase 'absint':\n\t\t\t\t\t// should be numeric\n\t\t\t\t\t$this->assertTrue( is_numeric( $this->obj->get( $prop ) ) );\n\t\t\t\t\t// strings should return 0\n\t\t\t\t\t$this->obj->set( $prop, 'string' );\n\t\t\t\t\t$this->assertEquals( 0, $this->obj->get( $prop ) );\n\t\t\t\t\t// floats should drop the decimal\n\t\t\t\t\t$this->obj->set( $prop, 12.3 );\n\t\t\t\t\t$this->assertEquals( 12, $this->obj->get( $prop ) );\n\t\t\t\t\t// negative should return positive\n\t\t\t\t\t$this->obj->set( $prop, -45 );\n\t\t\t\t\t$this->assertEquals( 45, $this->obj->get( $prop ) );\n\t\t\t\t\t// numeric string should return int\n\t\t\t\t\t$this->obj->set( $prop, '6' );\n\t\t\t\t\t$this->assertEquals( '6', $this->obj->get( $prop ) );\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'array':\n\t\t\t\t\t// should be an array\n\t\t\t\t\t$this->assertTrue( is_array( $this->obj->get( $prop ) ) );\n\t\t\t\t\t// strings should return an array with the string as the first item in the array\n\t\t\t\t\t$this->obj->set( $prop, 'string' );\n\t\t\t\t\t$this->assertEquals( array( 'string' ), $this->obj->get( $prop ) );\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'float':\n\t\t\t\t\t// should be a float\n\t\t\t\t\t$this->assertTrue( is_float( $this->obj->get( $prop ) ) );\n\t\t\t\t\t// string should return 0\n\t\t\t\t\t$this->obj->set( $prop, 'string' );\n\t\t\t\t\t$this->assertEquals( 0, $this->obj->get( $prop ) );\n\t\t\t\t\t// decimals shouldn't be lost\n\t\t\t\t\t$this->obj->set( $prop, 123.456 );\n\t\t\t\t\t$this->assertEquals( 123.456, $this->obj->get( $prop ) );\n\t\t\t\t\t// whole numbers should still be whole numbers\n\t\t\t\t\t$this->obj->set( $prop, 789 );\n\t\t\t\t\t$this->assertEquals( 789, $this->obj->get( $prop ) );\n\t\t\t\t\t// check super big numbers\n\t\t\t\t\t$this->obj->set( $prop, 1234567.89 );\n\t\t\t\t\t$this->assertEquals( 1234567.89, $this->obj->get( $prop ) );\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'text':\n\t\t\t\t\t$this->assertTrue( is_string( $this->obj->get( $prop ) ) );\n\t\t\t\tbreak;\n\n\t\t\t\tcase 'yesno':\n\t\t\t\t\t// yes returns yes\n\t\t\t\t\t$this->obj->set( $prop, 'yes' );\n\t\t\t\t\t$this->assertEquals( 'yes', $this->obj->get( $prop ) );\n\t\t\t\t\t// no returns no\n\t\t\t\t\t$this->obj->set( $prop, 'no' );\n\t\t\t\t\t$this->assertEquals( 'no', $this->obj->get( $prop ) );\n\t\t\t\t\t// anything else returns no\n\t\t\t\t\t$this->obj->set( $prop, 'string' );\n\t\t\t\t\t$this->assertEquals( 'no', $this->obj->get( $prop ) );\n\t\t\t\t\t$this->obj->set( $prop, '' );\n\t\t\t\t\t$this->assertEquals( 'no', $this->obj->get( $prop ) );\n\t\t\t\t\t$this->obj->set( $prop, 123456 );\n\t\t\t\t\t$this->assertEquals( 'no', $this->obj->get( $prop ) );\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\t}\n\n\t/**\n\t * Test creation date and status relationship on updating.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_date_status_relationship_update() {\n\n\t\tif ( ! $this->get_data() ) {\n\t\t\t$this->markTestSkipped( 'No properties to test.' );\n\t\t}\n\n\t\t// Check we can update drafts creation date.\n\t\t$this->create( 'test title date status relationship' );\n\n\t\t// Check that when setting the creation date to the future, the post status changes accordingly.\n\t\t$this->obj->set( 'status', 'publish' );\n\t\t$this->obj->set( 'date_gmt', date( 'Y-m-d H:i:s', strtotime( '+1 year', current_time( 'timestamp' ) ) ) );\n\t\t$this->assertEquals( 'future', $this->obj->get( 'status' ) );\n\n\t}\n\n\t/**\n\t * Test edit_date post proerty.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_edit_date() {\n\n\t\tif ( ! $this->get_data() ) {\n\t\t\t$this->markTestSkipped( 'No properties to test.' );\n\t\t}\n\n\t\t// Check we can update drafts creation date.\n\t\t$this->create( 'test title draft' );\n\n\t\t// Makes sense only for drafts.\n\t\tif ( 'draft' !== $this->obj->get( 'status' ) ) {\n\t\t\t$this->markTestSkipped( 'No properties to test.' );\n\t\t}\n\n\t\t$new_date = date( 'Y-m-d H:i:s', strtotime( '-1 year', current_time( 'timestamp' ) ) );\n\t\t$this->obj->set_bulk( array(\n\t\t\t'date_gmt'  => $new_date,\n\t\t\t'edit_date' => true,\n\t\t) );\n\t\t$this->assertEquals( $new_date, $this->obj->get( 'date_gmt' ) );\n\n\t\t// Check we cannot update drafts creation dates without passing edit_date.\n\t\t$this->create( 'test title draft two' );\n\n\t\t$this->obj->set_bulk( array(\n\t\t\t'date_gmt' => $new_date,\n\t\t) );\n\t\t$this->assertNotEquals( $new_date, $this->obj->get( 'date_gmt' ) );\n\t\t$this->assertEquals( '0000-00-00 00:00:00', $this->obj->get( 'date_gmt' ) );\n\n\t}\n\n\n\t/**\n\t * Test set_bulk()\n\t *\n\t * @since 3.34.0\n\t * @return void\n\t */\n\tpublic function test_set_bulk() {\n\n\t\t$this->create( 'another creative test title' );\n\t\t$props = $this->get_properties();\n\t\t$data = $this->get_data();\n\n\t\tif ( ! $data ) {\n\t\t\t$this->markTestSkipped( 'No properties to test.' );\n\t\t}\n\n\t\t// update should return true\n\t\t$this->assertTrue( $this->obj->set_bulk( $data ) );\n\n\t\t// Check each property has been set as expected.\n\t\tforeach ( $props as $prop => $type ) {\n\t\t\t// make sure gotten value equals set val\n\t\t\t$this->assertEquals( $data[ $prop ], $this->obj->get( $prop ) );\n\t\t}\n\n\t\t// update should return false, the DB values are the same.\n\t\t$this->assertFalse( $this->obj->set_bulk( $data ) );\n\n\t}\n\n\t/**\n\t * Test set_bulk() when passing $wp_error param as true.\n\t *\n\t * @since 3.34.0\n\t * @return void\n\t */\n\tpublic function test_set_bulk_wp_error() {\n\n\t\t$this->create( 'a creative test title take one' );\n\t\t$props = $this->get_properties();\n\t\t$data = $this->get_data();\n\n\t\tif ( ! $data ) {\n\t\t\t$this->markTestSkipped( 'No properties to test.' );\n\t\t}\n\n\t\t// update should return true\n\t\t$this->assertTrue( $this->obj->set_bulk( $data, $wp_error = true ) );\n\n\t\t// Let's add some post data\n\t\t$data['content'] = 'Special creative content';\n\n\t\t// We're updating an llms post with exactly the same set of metas\n\t\t// this will produce a wp_error object with the error code 'invalid_meta'.\n\t\t$result = $this->obj->set_bulk( $data, $wp_error = true );\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'invalid_meta', $result );\n\n\t\t// let's force a wp_post_update (wp_insert_post) failure, by forcing the 'wp_insert_post_empty_content' filter\n\t\t// see wp-includes/post.php:wp_insert_post()\n\t\tadd_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t\t// update should a wp_error object which contains both the 'invalid_meta' error code\n\t\t// and the 'empty_content' one.\n\t\t$result = $this->obj->set_bulk( $data, true );\n\t\t$this->assertArrayHasKey( 'invalid_meta', $result->errors );\n\t\t$this->assertArrayHasKey( 'empty_content', $result->errors );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-post-type-metabox-test-case.php",
    "content": "<?php\n// Require main test case.\nrequire_once 'class-llms-unit-test-case.php';\n\n/**\n * Unit Test Case with tests and utilities specific to testing LifterLMS post type Metabox classes.\n *\n * @since 3.33.0\n*/\nclass LLMS_PostTypeMetaboxTestCase extends LLMS_UnitTestCase {\n\n\t/**\n\t * Require all necessary files.\n\t *\n\t * @since 3.33.0\n\t * @since 3.36.1 Conditionally require LLMS_Admin_Meta_Boxes.\n\t * @since 3.37.12 Call parent method.\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\n\t\t// Manually include required files.\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.post-types.php';\n\t\tif ( ! class_exists( 'LLMS_Admin_Meta_Boxes' ) ) {\n\t\t\t( new LLMS_Admin_Post_Types() )->include_post_type_metabox_class();\n\t\t}\n\n\t}\n\n\t/**\n\t * Metabox utility function to add the metabox nonce field to an array of data.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @param array $data Data array.\n\t * @param bool  $real If true, uses a real nonce. Otherwise uses a fake nonce (useful for testing negative cases).\n\t * @return array\n\t */\n\tprotected function add_nonce_to_array( $data = array(), $real = true ) {\n\n\t\t$nonce_string = $real ? wp_create_nonce( 'lifterlms_save_data' ) : wp_create_nonce( 'fake' );\n\n\t\treturn wp_parse_args( $data, array(\n\t\t\t'lifterlms_meta_nonce' => $nonce_string,\n\t\t) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-settings-page-test-case.php",
    "content": "<?php\n/**\n * LifterLMS Unit Test Case Base class\n *\n * @since 3.37.3\n */\nclass LLMS_Settings_Page_Test_Case extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.3\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->page = new $this->classname();\n\n\t}\n\n\t/**\n\t * Stub to be overridden in extending classes.\n\t *\n\t * This function should return an array of arrays.\n\t *\n\t * The array key is the option id and the value is an array of possible values to store.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return array[]\n\t */\n\tprotected function get_mock_settings() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Retrieve an indexed array of ids for the page's registered settings.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @param bool $save_only If `true`, only return fields that can be saved to the database.\n\t * @return string[]\n\t */\n\tprotected function get_settings_ids( $save_only = true ) {\n\n\t\t$saveable = array( 'checkbox', 'textarea', 'wpeditor', 'password', 'text', 'email', 'number', 'select', 'single_select_page', 'single_select_membership', 'radio', 'hidden', 'image', 'multiselect' );\n\n\t\t$ids = array();\n\t\tforeach ( $this->page->get_settings() as $setting ) {\n\t\t\tif ( empty( $setting['id'] ) || ( $save_only && ! in_array( $setting['type'], $saveable, true ) ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$ids[] = $setting['id'];\n\t\t}\n\t\treturn $ids;\n\n\t}\n\n\t/**\n\t * Test the settings page ID matches the expected ID.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_id() {\n\t\t$this->assertEquals( $this->class_id, $this->page->id );\n\t}\n\n\t/**\n\t * Test the settings page label matches the expected label.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_label() {\n\t\t$this->assertEquals( $this->class_label, $this->page->label );\n\t}\n\n\t/**\n\t * Ensure all editable settings exist in the settings array.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings() {\n\n\t\t$settings = $this->get_mock_settings();\n\n\t\tif ( ! $settings ) {\n\t\t\t$this->markTestSkipped( 'No mock setting registered to test.' );\n\t\t}\n\n\t\t$mock   = array_keys( $settings );\n\t\t$actual = $this->get_settings_ids();\n\t\t$this->assertEquals( $mock, $actual );\n\n\t}\n\n\t/**\n\t * Ensure no duplicate values exist in the settings array.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings_dupcheck() {\n\n\t\t$actual   = $this->get_settings_ids( false );\n\t\t$no_dupes = array_unique( $actual );\n\t\t$this->assertEquals( $no_dupes, $actual );\n\n\t}\n\n\t/**\n\t * Test the save() method.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_save() {\n\n\t\t$settings = $this->get_mock_settings();\n\n\t\tif ( ! $settings ) {\n\t\t\t$this->markTestSkipped( 'No mock setting registered to test.' );\n\t\t}\n\n\t\t$post = array();\n\t\tforeach ( $settings as $key => $vals ) {\n\t\t\t$post[ $key ] = $vals[0];\n\n\t\t\tforeach ( $vals as $val ) {\n\n\t\t\t\t$this->mockPostRequest( array(\n\t\t\t\t\t$key => $val,\n\t\t\t\t) );\n\t\t\t\t$this->page->save();\n\t\t\t\t$this->assertEquals( $val, get_option( $key ), $key );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// Bulk save all of them at once.\n\t\t$this->mockPostRequest( $post );\n\t\t$this->page->save();\n\t\tforeach ( $post as $key => $val ) {\n\t\t\t$this->assertEquals( $val, get_option( $key ), $key );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the set_label() method.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_label() {\n\t\t$this->assertEquals( $this->class_label, LLMS_Unit_Test_Util::call_method( $this->page, 'set_label' ) );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-shortcode-test-case.php",
    "content": "<?php\n/**\n * Unit Test Case with tests and utilities specific to testing LifterLMS Shortcodes\n *\n * @since 3.24.1\n * @since 5.0.0 Add helper method `get_class()`.\n * @version 5.0.0\n */\n\nrequire_once 'class-llms-unit-test-case.php';\n\nclass LLMS_ShortcodeTestCase extends LLMS_UnitTestCase {\n\n\t/**\n\t * Class name of the Shortcode Class\n\t *\n\t * @var string\n\t */\n\tpublic $class_name = '';\n\n\t/**\n\t * Retrieve an instance of the shortcode generator class.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return obj\n\t */\n\tprotected function get_class() {\n\n\t\treturn call_user_func( array( $this->class_name, 'instance' ) );\n\n\t}\n\n\t/**\n\t * Assertion to expect the output of a given shortcode string.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $expect Expected shortcode output.\n\t * @param string $shortcode Shortcode string (to be wrapped in `do_shortcode()`).\n\t * @return void\n\t */\n\tprotected function assertShortcodeOutputEquals( $expect, $shortcode ) {\n\n\t\tob_start();\n\t\techo do_shortcode( $shortcode );\n\t\t$actual = ob_get_clean();\n\n\t\treturn $this->assertEquals( $expect, $actual );\n\n\t}\n\n\t/**\n\t * Test shortcode registration\n\t *\n\t * @since 3.24.1\n\t * @since 3.24.3 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_registration() {\n\n\t\t$obj = $this->get_class();\n\t\t$this->assertTrue( shortcode_exists( $obj->tag ) );\n\t\t$this->assertTrue( is_a( $obj, 'LLMS_Shortcode' ) );\n\t\t$this->assertTrue( ! empty( $obj->tag ) );\n\t\t$this->assertTrue( is_string( $obj->output() ) );\n\t\t$this->assertTrue( is_array( $obj->get_attributes() ) );\n\t\t$this->assertTrue( is_string( $obj->get_content() ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/framework/class-llms-unit-test-case.php",
    "content": "<?php\n/**\n * LifterLMS Unit Test Case Base class\n *\n * @since 3.3.1\n * @since 3.33.0 Marked `setup_get()` and `setup_post()` as deprecated and removed private `setup_request()`. Use methods from lifterlms/lifterlms_tests.\n * @since 3.37.4 Add certificate template mock generation and earning methods.\n * @since 3.37.8 Changed return of `take_quiz` method from `void` to an `LLMS_Quiz_Attempt` object\n * @since 3.37.17 Added voucher creation method.\n * @since 3.38.0 Added `setManualGatewayStatus()` method.\n * @since 4.0.0 Added create_mock_session-data() class.\n * @since 4.7.0 Disabled image sideloading during mock course generation.\n * @since 5.0.0 Automatically clear notices on teardown.\n *              Add a method to generate mock vouchers.\n * @since 6.0.0 Removed deprecated items.\n *              - `LLMS_UnitTestCase::setup_get()` method\n *              - `LLMS_UnitTestCase::setup_post()` method\n */\nclass LLMS_UnitTestCase extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Cached access plan object used by mock orders if the plan isn't specified.\n\t *\n\t * @var LLMS_Access_Plan|null\n\t */\n\tprotected ?LLMS_Access_Plan $saved_mock_plan = null;\n\n\t/**\n\t * Setup tests\n\t * Automatically called before each test\n\t *\n\t * @since 3.17.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t * @since 6.0.0 Replaced use of the deprecated `llms_reset_current_time()` function with\n\t *              `llms_tests_reset_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\tllms_tests_reset_current_time();\n\t}\n\n\t/**\n\t * Loads a payment gateway class.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param LLMS_Payment_Gateway $gateway Gateway class instance.\n\t * @param boolean              $enabled Whether or not to enable the gateway.\n\t * @return void\n\t */\n\tprotected function load_payment_gateway( $gateway, $enabled = true ) {\n\t\t$gateway->set_option( 'enabled', $enabled ? 'yes' : 'no' );\n\t\tllms()->payment_gateways()->payment_gateways[] = $gateway;\n\t}\n\n\t/**\n\t * Unload a payment gateway.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param string $id Gateway ID.\n\t * @return void\n\t */\n\tprotected function unload_payment_gateway( $id ) {\n\n\t\tforeach ( llms()->payment_gateways()->payment_gateways as $i => $gateway ) {\n\t\t\tif ( $id !== $gateway->id ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tunset( llms()->payment_gateways()->payment_gateways[ $i ] );\n\t\t}\n\n\t}\n\n\t/**\n\t * Create mock user session data.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param integer $count   Number of session to create.\n\t * @param boolean $expired Whether or not the sessions are expired.\n\t * @return int[]\n\t */\n\tprotected function create_mock_session_data( $count = 5, $expired = false ) {\n\n\t\t$sessions = array();\n\n\t\tglobal $wpdb;\n\t\t$i = 1;\n\t\twhile ( $i <= $count ) {\n\t\t\t$wpdb->insert( $wpdb->prefix . 'lifterlms_sessions', array(\n\t\t\t\t'session_key' => LLMS_Unit_Test_Util::call_method( llms()->session, 'generate_id' ),\n\t\t\t\t'data'        => serialize( array( microtime() ) ),\n\t\t\t\t'expires'     => $expired ? time() - DAY_IN_SECONDS : time() + DAY_IN_SECONDS,\n\t\t\t), array( '%s', '%s', '%d' ) );\n\n\t\t\t$sessions[] = $wpdb->insert_id;\n\n\t\t\t++$i;\n\n\t\t}\n\n\t\treturn $sessions;\n\n\t}\n\n\t/**\n\t * Automatically complete a percentage of courses for a student\n\t * @param    integer    $student_id  WP User ID of a student\n\t * @param    array      $course_ids  array of WP Post IDs for the courses\n\t * @param    integer    $perc        percentage of each course complete\n\t *                                   percentage is based off the total number of lessons in the course\n\t *                                   fractions will be rounded up\n\t * @return   void\n\t * @since    3.7.3\n\t * @version  3.24.0\n\t */\n\tprotected function complete_courses_for_student( $student_id = 0, $course_ids = array(), $perc = 100 ) {\n\n\t\tif ( ! $student_id ) {\n\t\t\t$student = $this->get_mock_student();\n\t\t} else {\n\t\t\t$student = llms_get_student( $student_id );\n\t\t}\n\n\t\tif ( ! is_array( $course_ids ) ) {\n\t\t\t$course_ids = array( $course_ids );\n\t\t}\n\n\t\tforeach ( $course_ids as $course_id ) {\n\n\t\t\t$course = llms_get_post( $course_id );\n\n\t\t\t// enroll the student if not already enrolled\n\t\t\tif ( ! $student->is_enrolled( $course_id ) ) {\n\t\t\t\t$student->enroll( $course_id );\n\t\t\t}\n\n\t\t\t$lessons = $course->get_lessons( 'ids' );\n\t\t\t$num_lessons = count( $lessons );\n\t\t\t$stop = 100 === $perc ? $num_lessons : round( ( $perc / 100 ) * $num_lessons );\n\n\t\t\tforeach ( $lessons as $i => $lid ) {\n\n\t\t\t\t// stop once we reach the stopping point\n\t\t\t\tif ( $i + 1 > $stop ) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t$lesson = llms_get_post( $lid );\n\t\t\t\tif ( $lesson->has_quiz() ) {\n\n\t\t\t\t\t$this->take_quiz( $lesson->get( 'quiz' ), $student->get_id() );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t$student->mark_complete( $lid, 'lesson' );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Create a voucher.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @param int   $codes    Number of codes to generate for the voucher.\n\t * @param int   $uses     Number of uses per code.\n\t * @param int[] $products List of course/membership ids.\n\t * @return LLMS_Voucher\n\t */\n\tprotected function create_voucher( $codes = 5, $uses = 5, $products = array() ) {\n\n\t\t// Create the Voucher Post.\n\t\t$post_id = $this->factory->post->create( array( 'post_type' => 'llms_voucher' ) );\n\t\t$voucher = new LLMS_Voucher( $post_id );\n\n\t\t// Generate voucher codes.\n\t\t$i = 0;\n\t\twhile( $i < $codes ) {\n\t\t\t$voucher->save_voucher_code( array(\n\t\t\t\t'code'             => substr( bin2hex( random_bytes( 12 ) ), 0, 12 ),\n\t\t\t\t'redemption_count' => $uses,\n\t\t\t) );\n\t\t\t++$i;\n\t\t}\n\n\t\t// Add a mock course if no products are specified.\n\t\tif ( ! $products ) {\n\t\t\t$products[] = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t}\n\n\t\t// Save the products.\n\t\tforeach ( $products as $product ) {\n\t\t\t$voucher->save_product( $product );\n\t\t}\n\n\t\treturn $voucher;\n\n\t}\n\n\t/**\n\t * Take a quiz for a student and get a desired grade\n\t *\n\t * @since 3.24.0\n\t * @since 3.37.8 Change return from `void` to an `LLMS_Quiz_Attempt` object\n\t *\n\t * @param int $quiz_id    WP Post ID of the Quiz.\n\t * @param int $student_id WP Used ID of the student.\n\t * @param int $grade      Desired grade. Do the math in the test, this can't make the grade happen if it's not possible\n\t *                        for example a quiz with 5 questions CANNOT get a 75%!\n\t *\n\t * @return LLMS_Quiz_Attempt\n\t */\n\tpublic function take_quiz( $quiz_id, $student_id, $grade = 100 ) {\n\n\t\t$quiz = llms_get_post( $quiz_id );\n\t\t$student = llms_get_student( $student_id );\n\n\t\t$attempt = LLMS_Quiz_Attempt::init( $quiz_id, $quiz->get( 'lesson_id' ), $student_id )->start();\n\n\t\t$questions_count = $attempt->get_count( 'gradeable_questions' );\n\t\t$points_per_question = ( 100 / $questions_count );\n\t\t$to_be_correct = $grade / $points_per_question;\n\n\t\t$i = 1;\n\t\twhile ( $attempt->get_next_question() ) {\n\n\t\t\t$question_id = $attempt->get_next_question();\n\n\t\t\t$question = llms_get_post( $question_id );\n\t\t\t$correct = $question->get_correct_choice();\n\t\t\t// select the correct answer\n\t\t\tif ( $i <= $to_be_correct ) {\n\n\t\t\t\t$selected = $correct;\n\n\t\t\t// select a random incorrect answer\n\t\t\t} else {\n\n\t\t\t\t// filter all correct choices out of the array of choices\n\t\t\t\t$options = array_filter( $question->get_choices(), function( $choice ) {\n\t\t\t\t\treturn ( ! $choice->is_correct() );\n\t\t\t\t} );\n\n\t\t\t\t// rekey\n\t\t\t\t$options = array_values( $options );\n\n\t\t\t\t// select a random incorrect answer\n\t\t\t\t$selected = array( $options[ rand( 0, count( $options ) - 1 ) ]->get( 'id' ) );\n\n\t\t\t}\n\n\t\t\t$attempt->answer_question( $question_id, $selected );\n\n\t\t\t$i++;\n\n\t\t}\n\n\t\t$attempt->end();\n\n\t\treturn $attempt;\n\n\t}\n\n\t/**\n\t * Generates a set of mock courses\n\t *\n\t * @since 3.7.3\n\t * @since 4.7.0 Disabled image sideloading during mock course generation.\n\t *\n\t * @param    integer    $num_courses   number of courses to generate\n\t * @param    integer    $num_sections  number of sections to generate for each course\n\t * @param    integer    $num_lessons   number of lessons to generate for each section\n\t * @param    integer    $num_quizzes   number of quizzes to generate for each section\n\t *                                     quizzes will be attached to the last lessons ie each section\n\t *                                     if you generate 3 lessons / section and 1 quiz / section the quiz\n\t *                                     will always be the 3rd lesson\n\t * @return   array \t\t\t\t\t   indexed array of course ids\n\t */\n\tprotected function generate_mock_courses( $num_courses = 1, $num_sections = 5, $num_lessons = 5, $num_quizzes = 1, $num_questions = 5 ) {\n\n\t\t$courses = array();\n\t\t$i = 1;\n\t\twhile ( $i <= $num_courses ) {\n\t\t\t$courses[] = $this->get_mock_course_array( $i, $num_sections, $num_lessons, $num_quizzes, $num_questions );\n\t\t\t$i++;\n\t\t}\n\n\t\tadd_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\n\t\t$gen = new LLMS_Generator( array( 'courses' => $courses ) );\n\t\t$gen->set_generator( 'LifterLMS/BulkCourseGenerator' );\n\t\t$gen->set_default_post_status( 'publish' );\n\t\t$gen->generate();\n\n\t\tremove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\t\tif ( ! $gen->is_error() ) {\n\t\t\treturn $gen->get_generated_courses();\n\t\t}\n\n\t}\n\n\t/**\n\t * Retrieves an array of data as would be found in `$_POST` from a checkout form submission.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @param LLMS_Access_Plan|null $plan An access plan or `null` to automatically create one with default values from `get_mock_plan()`.\n\t * @return array\n\t */\n\tprotected function get_mock_checkout_data_array( $plan = null ) {\n\n\t\t$plan = $plan ? $plan : $this->get_mock_plan();\n\n\t\t$data = array(\n\t\t\t'llms_plan_id'         => $plan->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'manual',\n\t\t);\n\t\t$user_data = $this->get_mock_user_data_array();\n\n\t\treturn array_merge( $data, $user_data );\n\n\t}\n\n\t/**\n\t * Retrieves an array user data as would be found in `$_POST` from a checkout or registration form submission.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_mock_user_data_array() {\n\n\t\t$email = wp_generate_password( 5, false ) . '@' . wp_generate_password( 5, false ) . '.tld';\n\n\t\treturn array(\n\t\t\t'email_address'          => $email,\n\t\t\t'email_address_confirm'  => $email,\n\t\t\t'password'               => '12345678',\n\t\t\t'password_confirm'       => '12345678',\n\t\t\t'first_name'             => 'Fred',\n\t\t\t'last_name'              => 'Stevens',\n\t\t\t'llms_phone'             => '1234567890',\n\t\t\t'llms_billing_address_1' => '123 A Street',\n\t\t\t'llms_billing_address_2' => '#456',\n\t\t\t'llms_billing_city'      => 'City',\n\t\t\t'llms_billing_state'     => 'State',\n\t\t\t'llms_billing_zip'       => '12345',\n\t\t\t'llms_billing_country'   => 'CA',\n\t\t);\n\n\t}\n\n\t/**\n\t * Generates an array of course data which can be passed to a Generator\n\t * @param    int     $iterator      number for use as course number\n\t * @param    int     $num_sections  number of sections to generate for the course\n\t * @param    int     $num_lessons   number of lessons for each section in the course\n\t * @param    int     $num_quizzes   number of quizzes for each section in the course\n\t * @return   array\n\t * @since    3.7.3\n\t * @version  3.16.12\n\t */\n\tprotected function get_mock_course_array( $iterator = 1, $num_sections = 3, $num_lessons = 5, $num_quizzes = 1, $num_questions = 5 ) {\n\n\t\t$mock = array(\n\t\t\t'title' => sprintf( 'mock course %d', $iterator ),\n\t\t);\n\n\t\t$sections = array();\n\t\t$sections_i = 1;\n\t\twhile ( $sections_i <= $num_sections ) {\n\n\t\t\t$section = array(\n\t\t\t\t'title' => sprintf( 'mock section %d', $sections_i ),\n\t\t\t\t'lessons' => array(),\n\t\t\t);\n\n\t\t\t$lessons_i = 1;\n\n\t\t\t$quizzes_start_i = $num_lessons - $num_quizzes + 1;\n\n\t\t\twhile ( $lessons_i <= $num_lessons ) {\n\n\t\t\t\t$lesson = array(\n\t\t\t\t\t'title' => sprintf( 'mock lesson %d', $lessons_i ),\n\t\t\t\t);\n\n\t\t\t\tif ( $lessons_i >= $quizzes_start_i ) {\n\n\t\t\t\t\t$lesson['quiz_enabled'] = 'yes';\n\n\t\t\t\t\t$lesson['quiz'] = array(\n\t\t\t\t\t\t'title' => sprintf( 'mock quiz %d', $lessons_i ),\n\t\t\t\t\t);\n\n\t\t\t\t\t$questions = array();\n\t\t\t\t\t$questions_i = 1;\n\t\t\t\t\twhile ( $questions_i <= $num_questions ) {\n\n\t\t\t\t\t\t$options_i = 1;\n\t\t\t\t\t\t$total_options = rand( 2, 5 );\n\t\t\t\t\t\t$correct_option = rand( $options_i, $total_options );\n\t\t\t\t\t\t$choices = array();\n\t\t\t\t\t\twhile( $options_i <= $total_options ) {\n\t\t\t\t\t\t\t$choices[] = array(\n\t\t\t\t\t\t\t\t'choice' => sprintf( 'choice %d', $options_i ),\n\t\t\t\t\t\t\t\t'choice_type' => 'text',\n\t\t\t\t\t\t\t\t'correct' => ( $options_i === $correct_option ),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t$options_i++;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t$questions[] = array(\n\t\t\t\t\t\t\t'title' => sprintf( 'question %d', $questions_i ),\n\t\t\t\t\t\t\t'question_type' => 'choice',\n\t\t\t\t\t\t\t'choices' => $choices,\n\t\t\t\t\t\t\t'points' => 1,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t$questions_i++;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t$lesson['quiz']['questions'] = $questions;\n\n\t\t\t\t}\n\n\t\t\t\tarray_push( $section['lessons'], $lesson );\n\t\t\t\t$lessons_i++;\n\t\t\t}\n\n\t\t\tarray_push( $sections, $section );\n\n\t\t\t$sections_i++;\n\n\t\t}\n\n\t\t$mock['sections'] = $sections;\n\n\t\treturn $mock;\n\n\t}\n\n\t/**\n\t * Retrieves a mock order object.\n\t *\n\t * @since Unknown\n\t * @since 7.0.0 Added `$student` parameter to allow supplying the student for the order.\n\t *\n\t * @param LLMS_Access_Plan $plan    An access plan object. If not supplied will create one.\n\t * @param boolean          $coupon  If `true` will create (and apply) a coupon to the order.\n\t * @param LLMS_Student     $student Student object. If not supplied will create one.\n\t * @return void\n\t */\n\tprotected function get_mock_order( $plan = null, $coupon = false, $student = null ) {\n\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\tupdate_option( $gateway->get_option_name( 'enabled' ), 'yes' );\n\n\t\tif ( ! $plan ) {\n\t\t\tif ( ! $this->saved_mock_plan ) {\n\t\t\t\t$plan = $this->get_mock_plan();\n\t\t\t\t$this->saved_mock_plan = $plan;\n\t\t\t} else {\n\t\t\t\t$plan = $this->saved_mock_plan;\n\t\t\t}\n\t\t}\n\n\t\tif ( $coupon ) {\n\t\t\t$coupon = new LLMS_Coupon( 'new', 'couponcode' );\n\t\t\t$coupon_data = array(\n\t\t\t\t'coupon_amount' => 10,\n\t\t\t\t'discount_type' => 'percent',\n\t\t\t\t'plan_type' => 'any',\n\t\t\t);\n\t\t\tforeach ( $coupon_data as $key => $val ) {\n\t\t\t\t$coupon->set( $key, $val );\n\t\t\t}\n\t\t}\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\treturn $order->init( $student ? $student : $this->get_mock_student(), $plan, $gateway, $coupon );\n\n\t}\n\n\t/**\n\t * Retrieve a mock access plan\n\t *\n\t * Automatically generates a course associated with the plan.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @param float   $price      Plan price.\n\t * @param integer $frequency  Recurring frequency.\n\t * @param string  $expiration Plan expiration.\n\t * @param boolean $on_sale    Whether or not the plan is on sale.\n\t * @param boolean $trial      whether or not the plan has a trial.\n\t * @return LLMS_Access_Plan\n\t */\n\tprotected function get_mock_plan( $price = 25.99, $frequency = 1, $expiration = 'lifetime', $on_sale = false, $trial = false ) {\n\n\t\t$course = $this->generate_mock_courses( 1, 0 );\n\t\t$course_id = $course[0];\n\n\t\t$plan = new LLMS_Access_Plan( 'new', 'Test Access Plan' );\n\t\t$plan_data = array(\n\t\t\t'access_expiration' => $expiration,\n\t\t\t'access_expires' => ( 'limited-date' === $expiration ) ? date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) : '',\n\t\t\t'access_length' => '1',\n\t\t\t'access_period' => 'year',\n\t\t\t'frequency' => $frequency,\n\t\t\t'is_free' => $price > 0 ? 'no' : 'yes',\n\t\t\t'length' => 0,\n\t\t\t'on_sale' => $on_sale ? 'yes' : 'no',\n\t\t\t'period' => 'day',\n\t\t\t'price' => $price,\n\t\t\t'product_id' => $course_id,\n\t\t\t'sale_price' => round( $price - ( $price * .1 ), 2 ),\n\t\t\t'sku' => 'accessplansku',\n\t\t\t'trial_length' => 1,\n\t\t\t'trial_offer' => $trial ? 'yes' : 'no',\n\t\t\t'trial_period' => 'week',\n\t\t\t'trial_price' => 1.00,\n\t\t);\n\n\t\tforeach ( $plan_data as $key => $val ) {\n\t\t\t$plan->set( $key, $val );\n\t\t}\n\n\t\treturn $plan;\n\n\t}\n\n\t/**\n\t * Generate a mock voucher.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param int $codes Number of codes to create for the voucher.\n\t * @param int $uses Number of uses for each code.\n\t * @param array $products Array of WP_Post IDs to associate with voucher.\n\t * @return LLMS_Voucher\n\t */\n\tprotected function get_mock_voucher( $codes = 5, $uses = 1, $products = array() ) {\n\n\t\t$voucher_id = $this->factory->post->create( array( 'post_type' => 'llms_voucher' ) );\n\t\t$voucher = new LLMS_Voucher( $voucher_id );\n\n\t\tif ( ! $products ) {\n\t\t\t$products = array( $this->factory->course->create( array( 'sections' => 0 ) ) );\n\t\t}\n\n\t\tarray_map( array( $voucher, 'save_product' ), $products );\n\n\t\t$i = 1;\n\t\twhile( $i <= $codes ) {\n\t\t\t$voucher->save_voucher_code( array(\n\t\t\t\t'code'             => wp_generate_password( 12, false ),\n\t\t\t\t'redemption_count' => $uses,\n\t\t\t) );\n\t\t\t++$i;\n\t\t}\n\n\t\treturn $voucher;\n\n\t}\n\n\tprotected function get_mock_student( $login = false ) {\n\t\t$student_id = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\tif ( $login ) {\n\t\t\twp_set_current_user( $student_id );\n\t\t}\n\t\treturn llms_get_student( $student_id );\n\t}\n\n\n\t/**\n\t * Create an achievement template post.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $title   Achievement title.\n\t * @param string $content Achievement content.\n\t * @param string $image   Achievement image path.\n\t * @return int\n\t */\n\tprotected function create_achievement_template( $title = 'Mock Achievement Title', $content = 'You did it!', $image = '' ) {\n\n\t\t$template_id = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type'    => 'llms_achievement',\n\t\t\t\t'post_content' => $content,\n\t\t\t)\n\t\t);\n\t\tupdate_post_meta( $template_id, '_llms_achievement_title', $title );\n\t\tset_post_thumbnail( $template_id, $image );\n\n\t\treturn $template_id;\n\t}\n\n\t/**\n\t * Create a certificate template post.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @param string $title   Certificate title.\n\t * @param string $content Certificate content.\n\t * @param int    $image   Certificate background image ID.\n\t * @return int\n\t */\n\tprotected function create_certificate_template( $title = 'Mock Certificate Title', $content = '', $image = '' ) {\n\n\t\t$template_id = $this->factory->post->create( array(\n\t\t\t'post_type'    => 'llms_certificate',\n\t\t\t'post_content' => $content ? $content : '{site_title}, {current_date}',\n\t\t) );\n\t\tupdate_post_meta( $template_id, '_llms_certificate_title', $title );\n\t\tset_post_thumbnail( $template_id, $image );\n\n\t\treturn $template_id;\n\t}\n\n\tprotected function create_email_template( $subject = 'Mock Email Title' ) {\n\n\t\treturn $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_email',\n\t\t\t'meta_input' => array(\n\t\t\t\t'_llms_email_subject' => $subject,\n\t\t\t),\n\t\t) );\n\t}\n\n\t/**\n\t * Earn an achievement for a user.\n\t *\n\t * @since 3.37.3\n\t * @since 3.37.4 Moved to `LLMS_UnitTestCase`.\n\t * @since 6.0.0 Add `$engagement` param & use `LLMS_Engagement_Handler::handle_certificate()` in favor of deprecated `LLMS_Certificates::trigger_engagement()`.\n\t *\n\t * @param int      $user       WP_User ID.\n\t * @param int      $template   WP_Post ID of the `llms_certificate` template.\n\t * @param int      $related    WP_Post ID of the related post.\n\t * @param int|null $engagement WP_Post ID of the engagement post.\n\t * @return int[] {\n\t *     Indexed array containing information about the earned certificate.\n\t *\n\t *     int $0 WP_User ID.\n\t *     int $1 WP_Post ID of the earned cert (`llms_my_achievement`).\n\t *     int $2 WP_Post ID of the related post.\n\t *     int $3 WP_Post ID of the triggering engagement.\n\t * }\n\t */\n\tprotected function earn_achievement( $user_id, $template_id, $related_id, $engagement_id = null ) {\n\n\t\tllms_enroll_student( $user_id, $related_id );\n\n\t\t$earned = LLMS_Engagement_Handler::handle_achievement( array( $user_id, $template_id, $related_id, $engagement_id ) );\n\n\t\treturn array(\n\t\t\t$user_id,\n\t\t\t$earned->get( 'id' ),\n\t\t\t$related_id,\n\t\t\t$engagement_id,\n\t\t);\n\n\t}\n\n\t/**\n\t * Earn a certificate for a user.\n\t *\n\t * @since 3.37.3\n\t * @since 3.37.4 Moved to `LLMS_UnitTestCase`.\n\t * @since 6.0.0 Add `$engagement` param & use `LLMS_Engagement_Handler::handle_certificate()` in favor of deprecated `LLMS_Certificates::trigger_engagement()`.\n\t *\n\t * @param int      $user       WP_User ID.\n\t * @param int      $template   WP_Post ID of the `llms_certificate` template.\n\t * @param int      $related    WP_Post ID of the related post.\n\t * @param int|null $engagement WP_Post ID of the engagement post.\n\t * @return int[] {\n\t *     Indexed array containing information about the earned certificate.\n\t *\n\t *     int $0 WP_User ID.\n\t *     int $1 WP_Post ID of the earned cert (`llms_my_certificate`).\n\t *     int $2 WP_Post ID of the related post.\n\t *     int $3 WP_Post ID of the triggering engagement.\n\t * }\n\t */\n\tprotected function earn_certificate( $user_id, $template_id, $related_id, $engagement_id = null ) {\n\n\t\tllms_enroll_student( $user_id, $related_id );\n\n\t\t$earned = LLMS_Engagement_Handler::handle_certificate( array( $user_id, $template_id, $related_id, $engagement_id ) );\n\n\t\treturn array(\n\t\t\t$user_id,\n\t\t\t$earned->get( 'id' ),\n\t\t\t$related_id,\n\t\t\t$engagement_id,\n\t\t);\n\n\t}\n\n\t/**\n\t * Toggle the status of the manual payment gateway.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @param string $enabled Status of the gateway, \"yes\" for enabled and \"no\" for disabled.\n\t */\n\tprotected function setManualGatewayStatus( $enabled = 'yes' ) {\n\n\t\t$manual = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), $enabled );\n\n\t}\n\n\t/**\n\t * Create an engagement post and template post\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string  $trigger_type    Type of trigger (see list below).\n\t * @param string  $engagement_type Type of engagement to be awarded (email, achievement, certificate).\n\t * @param integer $delay           Sending delay for the created engagement trigger.\n\t * @return WP_Post Post object for the created `llms_engagement` post type.\n\t */\n\tpublic function create_mock_engagement( $trigger_type, $engagement_type, $delay = 0, $trigger_post = null, $engagement_post = null ) {\n\n\t\tif ( ! $trigger_post ) {\n\n\t\t\t/**\n\t\t\t * Trigger Types\n\t\t\t *\n\t\t\t * user_registration\n\t\t\t *\n\t\t\t * course_completed\n\t\t\t * lesson_completed\n\t\t\t * section_completed\n\t\t\t *\n\t\t\t * course_track_completed\n\t\t\t *\n\t\t\t * quiz_completed\n\t\t\t * quiz_passed\n\t\t\t * quiz_failed\n\t\t\t *\n\t\t\t * course_enrollment\n\t\t\t * membership_enrollment\n\t\t\t *\n\t\t\t * access_plan_purchased\n\t\t\t * course_purchased\n\t\t\t * membership_purchased\n\t\t\t */\n\t\t\tswitch ( $trigger_type ) {\n\t\t\t\tcase 'user_registration':\n\t\t\t\t\t$trigger_post = 0;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase 'course_completed':\n\t\t\t\tcase 'lesson_completed':\n\t\t\t\tcase 'section_completed':\n\t\t\t\tcase 'quiz_completed':\n\t\t\t\tcase 'quiz_passed':\n\t\t\t\tcase 'quiz_failed':\n\t\t\t\tcase 'course_enrollment':\n\t\t\t\tcase 'membership_enrollment':\n\t\t\t\tcase 'access_plan_purchased':\n\t\t\t\tcase 'course_purchased':\n\t\t\t\tcase 'membership_purchased':\n\t\t\t\t\t$post_type    = str_replace( array( '_completed', '_enrollment', '_passed', '_failed', '_purchased' ), '', $trigger_type );\n\t\t\t\t\t$post_type    = in_array( $post_type, array( 'access_plan', 'membership', 'quiz' ), true ) ? 'llms_' . $post_type : $post_type;\n\t\t\t\t\t$trigger_post = $this->factory->post->create( compact( 'post_type' ) );\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t}\n\n\t\tif ( ! $engagement_post ) {\n\n\t\t\t$engagement_create_func = \"create_{$engagement_type}_template\";\n\t\t\t$engagement_post        = $this->$engagement_create_func();\n\n\t\t}\n\n\t\treturn $this->factory->post->create_and_get( array(\n\t\t\t'post_type'  => 'llms_engagement',\n\t\t\t'meta_input' => array(\n\t\t\t\t'_llms_trigger_type'            => $trigger_type,\n\t\t\t\t'_llms_engagement_trigger_post' => $trigger_post,\n\t\t\t\t'_llms_engagement_type'         => $engagement_type,\n\t\t\t\t'_llms_engagement'              => $engagement_post,\n\t\t\t\t'_llms_engagement_delay'        => $delay,\n\t\t\t)\n\t\t) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-metabox.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Metabox class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group metaboxes\n * @group metabox_abstract\n *\n * @since 3.37.12\n */\nclass LLMS_Test_Admin_Metabox extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Retrieve an mocked abstract.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return LLMS_Admin_Metabox\n\t */\n\tprivate function get_stub() {\n\n\t\t$stub = $this->getMockForAbstractClass( 'LLMS_Admin_Metabox' );\n\n\t\t$stub->title = 'Mock Metabox';\n\t\t$stub->id    = 'mocker';\n\n\t\treturn $stub;\n\n\t}\n\n\t/**\n\t * Mock the get_fields() method for an LLMS_Admin_Metabox stub.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @param LLMS_Admin_Metabox $stub Metabox stub.\n\t * @return array Array of metabox field data.\n\t */\n\tprivate function add_fields_to_stub( $stub ) {\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'title'  => 'Tab Title',\n\t\t\t\t'fields' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'label' => 'Field Title.',\n\t\t\t\t\t\t'desc'  => 'Field Description',\n\t\t\t\t\t\t'id'    => $stub->prefix . 'mock_field',\n\t\t\t\t\t\t'type'  => 'text',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'label' => 'Field Title.',\n\t\t\t\t\t\t'desc'  => 'Field Description',\n\t\t\t\t\t\t'id'    => $stub->prefix . 'mock_field_2',\n\t\t\t\t\t\t'type'  => 'text',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'label'    => 'Allow quotes Field Title.',\n\t\t\t\t\t\t'desc'     => 'Field Description',\n\t\t\t\t\t\t'id'       => $stub->prefix . 'mock_field_with_quotes',\n\t\t\t\t\t\t'type'     => 'text',\n\t\t\t\t\t\t'sanitize' => 'shortcode',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'label'    => 'Allow quotes Field Title.',\n\t\t\t\t\t\t'desc'     => 'Field Description',\n\t\t\t\t\t\t'id'       => $stub->prefix . 'mock_field_with_quotes_2',\n\t\t\t\t\t\t'type'     => 'text',\n\t\t\t\t\t\t'sanitize' => 'no_encode_quotes',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'label' => 'Multi Select Title.',\n\t\t\t\t\t\t'desc'  => 'Field Description',\n\t\t\t\t\t\t'id'    => $stub->prefix . 'mock_field_multi_select',\n\t\t\t\t\t\t'type'  => 'select',\n\t\t\t\t\t\t'multi' => true,\n\t\t\t\t\t\t'value' => array(\n\t\t\t\t\t\t\t'key_1' => 'Value 1',\n\t\t\t\t\t\t\t'key_2' => 'Value 2',\n\t\t\t\t\t\t\t'key_3' => 'Value 3',\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$stub->method( 'get_fields' )->will( $this->returnValue( $fields ) );\n\n\t\treturn $fields;\n\n\t}\n\n\t/**\n\t * Test add_error(), get_errors(), has_errors(), and save_errors().\n\t *\n\t * @since 3.37.12\n\t * @since 6.0.0 Add WP_Error test.\n\t *\n\t * @return void.\n\t */\n\tpublic function test_errors_get_set_save() {\n\n\t\t$stub   = $this->get_stub();\n\t\t$errors = array(\n\t\t\t1 => 'Error message.',\n\t\t\t2 => 'Second message.',\n\t\t\t3 => new WP_Error( 'brown', 'Third Message' ),\n\t\t);\n\n\t\t// No messages.\n\t\t$this->assertEquals( array(), $stub->get_errors() );\n\t\t$this->assertEquals( false, $stub->has_errors() );\n\n\t\t// Has a specific number of messages.\n\t\tforeach ( $errors as $error_number => $error ) {\n\t\t\t$stub->add_error( $error );\n\t\t\t$this->assertEquals( true, $stub->has_errors() );\n\t\t\t$stub->save_errors();\n\t\t\t$this->assertEquals( array_slice( $errors, 0, $error_number ), $stub->get_errors() );\n\t\t}\n\t}\n\n\t/**\n\t * Test get_screens() method.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screens() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t// As string.\n\t\t$stub->screens = 'post';\n\t\t$this->assertEquals( array( 'post' ), LLMS_Unit_Test_Util::call_method( $stub, 'get_screens' ) );\n\n\t\t// Array.\n\t\t$stub->screens = array( 'post' );\n\t\t$this->assertEquals( array( 'post' ), LLMS_Unit_Test_Util::call_method( $stub, 'get_screens' ) );\n\n\t\t// Array with multiple post types.\n\t\t$stub->screens[] = 'page';\n\t\t$this->assertEquals( array( 'post', 'page' ), LLMS_Unit_Test_Util::call_method( $stub, 'get_screens' ) );\n\n\t}\n\n\t/**\n\t * Test output_errors().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_errors() {\n\n\t\t$stub   = $this->get_stub();\n\t\t$errors = array(\n\t\t\t'string error'   => 'string error',\n\t\t\t'WP_Error error' => new WP_Error( 'blue', 'WP_Error error' ),\n\t\t);\n\n\t\tforeach ( $errors as $contains => $error ) {\n\t\t\t$stub->add_error( $error );\n\t\t\t$stub->save_errors();\n\t\t\t$this->assertOutputContains( $contains, array( $stub, 'output_errors' ) );\n\t\t}\n\t}\n\n\t/**\n\t * Test save(): no nonce supplied.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_no_nonce() {\n\n\t\t$stub = $this->get_stub();\n\t\t$post = $this->factory->post->create();\n\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Test save(): invalid nonce supplied.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_invalid_nonce() {\n\n\t\t$stub = $this->get_stub();\n\t\t$post = $this->factory->post->create();\n\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(), false ) );\n\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Test save(): missing required capabilites.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_no_cap() {\n\n\t\t$stub = $this->get_stub();\n\t\t$post = $this->factory->post->create();\n\n\t\t$this->mockPostRequest( $this->add_nonce_to_array() );\n\n\t\t// Logged out.\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t\t// Invalid cap.\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Test save(): during a quick edit (inline save).\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_inline_save() {\n\n\t\t$stub = $this->get_stub();\n\t\t$post = $this->factory->post->create();\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(\n\t\t\t'action' => 'inline-save',\n\t\t) ) );\n\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Test save(): for a metabox with no fields.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_no_fields() {\n\n\t\t$stub = $this->get_stub();\n\t\t$post = $this->factory->post->create();\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array() ) );\n\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Test save(): when it all works.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_success() {\n\n\t\t$stub = $this->get_stub();\n\t\t$this->add_fields_to_stub( $stub );\n\t\t$post = $this->factory->post->create();\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Save.\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(\n\t\t\t$stub->prefix . 'mock_field'   => 'mock_val_1',\n\t\t\t$stub->prefix . 'mock_field_2' => 'mock_val_2',\n\t\t) ) );\n\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t\t$this->assertEquals( 'mock_val_1', get_post_meta( $post, $stub->prefix . 'mock_field', true ) );\n\t\t$this->assertEquals( 'mock_val_2', get_post_meta( $post, $stub->prefix . 'mock_field_2', true ) );\n\n\t\t// Unset values that aren't posted.\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(\n\t\t\t$stub->prefix . 'mock_field'   => 'mock_val_1',\n\t\t) ) );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t\t$this->assertEquals( 'mock_val_1', get_post_meta( $post, $stub->prefix . 'mock_field', true ) );\n\t\t$this->assertEquals( '', get_post_meta( $post, $stub->prefix . 'mock_field_2', true ) );\n\n\t\t// Unset a value, update another.\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(\n\t\t\t$stub->prefix . 'mock_field'   => '',\n\t\t\t$stub->prefix . 'mock_field_2'   => 'new_Val',\n\t\t) ) );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $stub, 'save', array( $post ) ) );\n\n\t\t$this->assertEquals( '', get_post_meta( $post, $stub->prefix . 'mock_field', true ) );\n\t\t$this->assertEquals( 'new_Val', get_post_meta( $post, $stub->prefix . 'mock_field_2', true ) );\n\n\t}\n\n\t/**\n\t * Test save_field() for a standard field (text)\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_field_standard() {\n\n\t\t$stub  = $this->get_stub();\n\t\t$field = $this->add_fields_to_stub( $stub )[0]['fields'][0];\n\t\t$post  = $this->factory->post->create();\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$field['id'] => 'Saved \"Field\" Value.',\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) );\n\t\t$this->assertEquals( 'Saved &#34;Field&#34; Value.', get_post_meta( $post, $field['id'], true ) );\n\n\t\t// Unset the value.\n\t\t$this->mockPostRequest( array() );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) );\n\t\t$this->assertEquals( '', get_post_meta( $post, $field['id'], true ) );\n\n\n\t}\n\n\t/**\n\t * Test save_field() for \"shortcode\" sanitization.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_field_allow_quotes() {\n\n\t\t$stub   = $this->get_stub();\n\t\t$fields = $this->add_fields_to_stub( $stub );\n\t\t$post   = $this->factory->post->create();\n\n\t\tforeach ( array( 2, 3 ) as $index ) {\n\n\t\t\t$field = $fields[0]['fields'][ $index ];\n\n\t\t\t$this->mockPostRequest( array(\n\t\t\t\t$field['id'] => 'Saved \"Field\" Value.',\n\t\t\t) );\n\n\t\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) );\n\t\t\t$this->assertEquals( 'Saved \"Field\" Value.', get_post_meta( $post, $field['id'], true ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test save_field() for a multi-select\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_field_multi_select() {\n\n\t\t$stub  = $this->get_stub();\n\t\t$field = $this->add_fields_to_stub( $stub )[0]['fields'][4];\n\t\t$post  = $this->factory->post->create();\n\n\t\t// Array not submitted.\n\t\t$this->mockPostRequest( array(\n\t\t\t$field['id'] => 'key_1',\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) );\n\t\t$this->assertEquals( '', get_post_meta( $post, $field['id'], true ) );\n\n\t\t// Single value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$field['id'] => array( 'key_1' ),\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) );\n\t\t$this->assertEquals( array( 'key_1' ), get_post_meta( $post, $field['id'], true ) );\n\n\t\t// Multi values.\n\t\t$this->mockPostRequest( array(\n\t\t\t$field['id'] => array( 'key_1', 'key_2' ),\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) );\n\t\t$this->assertEquals( array( 'key_1', 'key_2' ), get_post_meta( $post, $field['id'], true ) );\n\n\n\t\t// Unset.\n\t\t$this->mockPostRequest( array(\n\t\t\t$field['id'] => array(),\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $stub, 'save_field', array( $post, $field ) ) );\n\t\t$this->assertEquals( array(), get_post_meta( $post, $field['id'], true ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-admin-tool.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Admin_Tool class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group admin\n * @group admin_tools\n *\n * @since 3.37.19\n */\nclass LLMS_Test_Abstract_Admin_Tool extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include abstract class.\n\t *\n\t * @since 3.37.19\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/abstracts/llms-abstract-admin-tool.php';\n\n\t}\n\n\t/**\n\t * Retrieve a mock for the abstract class.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return LLMS_Abstract_Admin_Tool\n\t */\n\tprivate function get_abstract_mock() {\n\n\t\t$mock = $this->getMockForAbstractClass( 'LLMS_Abstract_Admin_Tool' );\n\t\tLLMS_Unit_Test_Util::set_private_property( $mock, 'id', 'mock' );\n\n\t\tremove_filter( 'llms_status_tools', array( $mock, 'register' ) );\n\t\tremove_action( 'llms_status_tool', array( $mock, 'maybe_handle' ) );\n\n\t\treturn $mock;\n\n\t}\n\n\t/**\n\t * Retrieve a \"concrete\" mock with the abstract methods defined.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @param boolean $load The mock return of `should_load()`.\n\t * @return LLMS_Abstract_Admin_Tool\n\t */\n\tprivate function get_concrete_mock( $load = true, $handle = true ) {\n\n\t\t// Gross.\n\t\tglobal $llms_mock_temp_load;\n\t\t$llms_mock_temp_load = $load;\n\n\t\t$mock = new class extends LLMS_Abstract_Admin_Tool {\n\t\t\tprotected $id = 'mock';\n\t\t\tpublic function should_load() {\n\t\t\t\t// Disgusting.\n\t\t\t\tglobal $llms_mock_temp_load;\n\t\t\t\treturn $llms_mock_temp_load;\n\t\t\t}\n\t\t\tprotected function handle() { return true; }\n\t\t\tprotected function get_description() { return 'Description'; }\n\t\t\tprotected function get_label() { return 'Label'; }\n\t\t\tprotected function get_text() { return 'Text'; }\n\t\t};\n\n\t\t// Ehck.\n\t\tunset( $llms_mock_temp_load );\n\n\t\treturn $mock;\n\n\t}\n\n\t/**\n\t * Test the constructor when the tool should load.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor_should_load() {\n\n\t\t$tool = $this->get_abstract_mock();\n\t\t$tool->__construct();\n\n\t\t$this->assertEquals( 10, has_filter( 'llms_status_tools', array( $tool, 'register' ) ) );\n\t\t$this->assertEquals( 10, has_action( 'llms_status_tool', array( $tool, 'maybe_handle' ) ) );\n\n\t}\n\n\t/**\n\t * Test maybe_handle() should_load() condition\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_check_should_load() {\n\n\t\t$tool = $this->get_concrete_mock( true );\n\t\t$this->assertTrue( $tool->maybe_handle( 'mock' ) );\n\n\t\t$tool = $this->get_concrete_mock( false );\n\t\t$this->assertFalse( $tool->maybe_handle( 'mock' ) );\n\n\t}\n\n\t/**\n\t * Test maybe_handle() ensure the id matches.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_check_ids() {\n\n\t\t$tool = $this->get_concrete_mock();\n\n\t\t$this->assertFalse( $tool->maybe_handle( 'fake' ) );\n\t\t$this->assertTrue( $tool->maybe_handle( 'mock' ) );\n\n\t}\n\n\t/**\n\t * Test register() when the tool should load.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_register() {\n\n\t\t$tool = $this->get_concrete_mock();\n\t\t$this->assertEquals( array(\n\t\t\t'mock' => array(\n\t\t\t\t'description' => 'Description',\n\t\t\t\t'label'       => 'Label',\n\t\t\t\t'text'        => 'Text',\n\t\t\t),\n\t\t), $tool->register( array() ) );\n\n\t}\n\n\t/**\n\t * Test register() when the tool should not load.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_no_load() {\n\n\t\t$tool = $this->get_concrete_mock( false );\n\n\t\t$this->assertEquals( array(), $tool->register( array() ) );\n\n\t}\n\n\t/**\n\t * Test should_load() stub.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load() {\n\n\t\t$tool = $this->get_abstract_mock();\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $tool, 'should_load' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-database-query.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Database_Query class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group query\n * @group dbquery\n *\n * @since 4.5.1\n */\nclass LLMS_Test_Database_Query extends LLMS_UnitTestCase {\n\n\tprivate $_arguments_original;\n\n\t/**\n\t * Cleanup on tear_down\n\t *\n\t * @since 4.5.1\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t * @since 7.0.0 Add call to `parent::tear_down()`.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE {$wpdb->posts}\" );\n\t}\n\n\t/**\n\t * Retrieve a stub for the abstract.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return LLMS_Database_Query\n\t */\n\tpublic function get_stub() {\n\n\t\treturn new class() extends LLMS_Database_Query {\n\t\t\tprotected function parse_args() {}\n\t\t\tprotected function prepare_query() {\n\t\t\t\treturn '';\n\t\t\t}\n\t\t};\n\n\t}\n\n\t/**\n\t * Test usage of deprecated preprare_query() when the method is defined\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_deprecated_preprare_query_defined() {\n\n\t\t$stub = new class() extends LLMS_Database_Query {\n\t\t\tpublic function __construct() {}\n\t\t\tprotected function parse_args() {}\n\t\t\tprotected function preprare_query() {\n\t\t\t\tglobal $wpdb;\n\t\t\t\treturn \"SELECT * FROM {$wpdb->posts} LIMIT 0, 0\";\n\t\t\t}\n\t\t};\n\n\t\t$class = get_class( $stub );\n\n\t\t// Deprecation notice thrown to identify that the method should be removed.\n\t\t$this->expected_deprecated = array_merge( $this->expected_deprecated, array( \"{$class}::preprare_query()\" ) );\n\n\t\tglobal $wpdb;\n\t\t$this->assertEquals( \"SELECT * FROM {$wpdb->posts} LIMIT 0, 0\", LLMS_Unit_Test_Util::call_method( $stub, 'prepare_query' ) );\n\n\t}\n\n\t/**\n\t * Test usage of deprecated preprare_query() when the method is not defined and `prepare_query()` doesn't overload the default method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @expectedIncorrectUsage LLMS_Database_Query::prepare_query\n\t *\n\t * @return void\n\t */\n\tpublic function test_deprecated_preprare_query_not_defined() {\n\n\t\t$stub = new class() extends LLMS_Database_Query {\n\t\t\tpublic function __construct() {}\n\t\t\tprotected function parse_args() {}\n\n\t\t};\n\n\t\tLLMS_Unit_Test_Util::call_method( $stub, 'prepare_query' );\n\n\t}\n\n\t/**\n\t * Test usage of deprecated preprare_query() when the method is not defined (if it was removed, for example).\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_deprecated_preprare_query_called_directly_but_not_defined() {\n\n\t\t$stub = new class() extends LLMS_Database_Query {\n\t\t\tpublic function __construct() {}\n\t\t\tprotected function parse_args() {}\n\t\t\tprotected function prepare_query() {\n\t\t\t\tglobal $wpdb;\n\t\t\t\treturn \"SELECT * FROM {$wpdb->posts} LIMIT 0, 0\";\n\t\t\t}\n\t\t};\n\n\t\t$class = get_class( $stub );\n\n\t\t// Deprecation notice thrown to identify that the method should be removed.\n\t\t$this->expected_deprecated = array_merge( $this->expected_deprecated, array( \"{$class}::preprare_query()\" ) );\n\n\t\tglobal $wpdb;\n\t\t$this->assertEquals( \"SELECT * FROM {$wpdb->posts} LIMIT 0, 0\", $stub->preprare_query() );\n\n\t}\n\n\t/**\n\t * Test __get() and __set() for deprecated properties.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_set_deprecated_public_properties() {\n\n\t\t$query = $this->get_stub();\n\t\t$class = get_class( $query );\n\n\t\t$expected_deprecated = array();\n\n\t\t$props = array(\n\t\t\t'found_results'  => 'get_found_results',\n\t\t\t'max_pages'      => 'get_max_pages',\n\t\t\t'number_results' => 'get_number_results',\n\t\t\t'query_vars'     => null,\n\t\t\t'results'        => 'get_results',\n\t\t);\n\t\tforeach ( $props as $prop => $func ) {\n\n\t\t\t$val = \"{$prop}_fake\";\n\n\t\t\t$query->$prop = $val;\n\t\t\t$this->assertEquals( $val, $query->$prop );\n\n\t\t\t// Replacement funciton if it exists.\n\t\t\tif ( ! is_null( $func ) ) {\n\t\t\t\t$this->assertEquals( $val, $query->$func() );\n\t\t\t}\n\n\t\t\t$expected_deprecated[] = \"Public access to property {$class}::{$prop}\";\n\n\t\t}\n\n\t\t// Removed sql prop.\n\t\t$query->sql = 'test';\n\t\t$this->assertEquals( 'test', $query->sql );\n\t\t$this->assertEquals( 'test', $query->get_query() );\n\t\t$expected_deprecated[] = \"Property {$class}::sql\";\n\n\t\t$this->expected_deprecated = array_merge( $this->expected_deprecated, $expected_deprecated );\n\t}\n\n\t/**\n\t * Test default_arguments()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_default_arguments() {\n\n\t\t$query = $this->get_stub();\n\n\t\t$defaults = LLMS_Unit_Test_Util::call_method( $query, 'default_arguments' );\n\n\t\t$this->assertEquals( 1, $defaults['page'] );\n\t\t$this->assertEquals( 25, $defaults['per_page'] );\n\t\t$this->assertEquals( array( 'id' => 'ASC' ), $defaults['sort'] );\n\n\t}\n\n\t/**\n\t * Test that by default the query args has no_found_rows set to false\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_default_args_no_found_rows_false() {\n\t\t$query = $this->query();\n\t\t$args  = LLMS_Unit_Test_Util::call_method( $query, 'get_default_args' );\n\t\t$this->assertEquals( false, $args['no_found_rows'] );\n\t}\n\n\t/**\n\t * Test that by default the query found_results and max_pages are not empty (when there are results)\n\t *\n\t * This is because no_found_rows by default is false.\n\t *\n\t * @since 4.5.1\n\t * @since 6.0.0 Use getters instead of direct property access.\n\t *\n\t * @return void\n\t */\n\tpublic function test_found_rows_max_pages_not_empty() {\n\t\t// Create some posts to have some element in our test table.\n\t\t$this->factory->post->create_many(8);\n\n\t\t$query = $this->query();\n\t\t$this->assertEquals( 8, $query->get_found_results() );\n\t\t$this->assertEquals( 1, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * Test when found rows and max pages are not set\n\t *\n\t * @since 4.5.1\n\t * @since 6.0.0 Use getters instead of direct property access.\n\t *\n\t * @return void\n\t */\n\tpublic function test_found_rows_max_pages_empty() {\n\t\t// No results, no found_results no max_pages are set.\n\t\t$query = $this->query();\n\t\t$this->assertFalse( $query->has_results() );\n\t\t$this->assertEquals( 0, $query->get_found_results() );\n\t\t$this->assertEquals( 0, $query->get_max_pages() );\n\n\t\t// Create some posts to have some element in our test table.\n\t\t$this->factory->post->create_many(8);\n\n\t\t// Query but avoiding calculating found rows.\n\t\t$query = $this->query(\n\t\t\tarray(\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t// We have results but no found_results no max_pages are set.\n\t\t$this->assertTrue( $query->has_results() );\n\t\t$this->assertEquals( 0, $query->get_found_results() );\n\t\t$this->assertEquals( 0, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * Test get_skip()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_skip() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t$tests = array(\n\t\t\t// Page, per page, expected.\n\t\t\tarray( 1, 2, 0 ),\n\t\t\tarray( 1, 25, 0 ),\n\t\t\tarray( 2, 25, 25 ),\n\t\t\tarray( 2, 300, 300 ),\n\t\t\tarray( 2, 300, 300 ),\n\t\t\tarray( 10, 5, 45 ),\n\t\t\tarray( 8, 10, 70 ),\n\t\t);\n\n\t\tforeach ( $tests as $i => $test ) {\n\n\t\t\tlist( $page, $per_page, $expect ) = $test;\n\n\t\t\t$stub->set( 'page', $page );\n\t\t\t$stub->set( 'per_page', $per_page );\n\n\t\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $stub, 'get_skip' ), $i );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test sql_select_columns() method\n\t *\n\t * SQL_CALC_FOUND_ROWS is no longer prepended.\n\t *\n\t * @since 4.5.1\n\t * @since 10.0.0 Updated: SQL_CALC_FOUND_ROWS is no longer added.\n\t *\n\t * @return void\n\t */\n\tpublic function test_sql_select_columns() {\n\n\t\t$query = $this->query();\n\t\t$this->assertEquals( '*', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns' ) );\n\t\t$this->assertEquals( 'column', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns', array( 'column' ) ) );\n\n\t\t// Query but avoiding calculating found rows.\n\t\t$query = $this->query(\n\t\t\tarray(\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals( '*', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns' ) );\n\t\t$this->assertEquals( 'column', LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns', array( 'column' ) ) );\n\t}\n\n\t/**\n\t * Pagination: found_results, max_pages, and per-page counts.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_pagination_found_results_and_max_pages() {\n\t\t$this->factory->post->create_many( 30 );\n\n\t\t$query = $this->query(\n\t\t\tarray(\n\t\t\t\t'per_page' => 10,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 30, $query->get_found_results() );\n\t\t$this->assertSame( 3, $query->get_max_pages() );\n\t\t$this->assertSame( 10, $query->get_number_results() );\n\n\t\t$query_page_3 = $this->query(\n\t\t\tarray(\n\t\t\t\t'per_page' => 10,\n\t\t\t\t'page'     => 3,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 10, $query_page_3->get_number_results() );\n\t}\n\n\t/**\n\t * When `no_found_rows` is true, the count query is skipped.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_no_found_rows_skips_count() {\n\t\t$this->factory->post->create_many( 5 );\n\n\t\t$query = $this->query(\n\t\t\tarray(\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertTrue( $query->has_results() );\n\t\t$this->assertSame( 0, $query->get_found_results() );\n\t\t$this->assertSame( 0, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * With no matching posts, found_results and max_pages are zero.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_empty_results_found_results_zero() {\n\t\t$query = $this->query();\n\n\t\t$this->assertFalse( $query->has_results() );\n\t\t$this->assertSame( 0, $query->get_found_results() );\n\t\t$this->assertSame( 0, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * The main query string must not use SQL_CALC_FOUND_ROWS.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sql_calc_found_rows_not_in_query_output() {\n\t\t$this->factory->post->create_many( 3 );\n\n\t\t$query = $this->query();\n\n\t\t$this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $query->get_query() );\n\t}\n\n\t/**\n\t * sql_limit() returns an empty string when `count_only` is true.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sql_limit_returns_empty_for_count_only() {\n\t\t$stub = $this->get_stub();\n\t\t$stub->set( 'count_only', true );\n\n\t\t$this->assertSame( '', LLMS_Unit_Test_Util::call_method( $stub, 'sql_limit' ) );\n\t}\n\n\t/**\n\t * A subclass that does not set count_query triggers _doing_it_wrong.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_missing_count_query_triggers_doing_it_wrong() {\n\n\t\t$this->factory->post->create_many( 3 );\n\n\t\t$stub = new class() extends LLMS_Database_Query {\n\t\t\tprotected $id = 'test_no_count';\n\t\t\tprotected function parse_args() {}\n\t\t\tprotected function prepare_query() {\n\t\t\t\tglobal $wpdb;\n\t\t\t\treturn \"SELECT ID FROM {$wpdb->posts} LIMIT 10\";\n\t\t\t}\n\t\t};\n\n\t\t$this->setExpectedIncorrectUsage( get_class( $stub ) . '::prepare_query' );\n\t}\n\n\t/**\n\t * Filters that reintroduce SQL_CALC_FOUND_ROWS trigger a deprecation and FOUND_ROWS() fallback.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_filter_reintroducing_sql_calc_found_rows() {\n\t\t$this->expected_deprecated = array_merge(\n\t\t\t$this->expected_deprecated,\n\t\t\tarray( 'llms_database_query_prepare_query' )\n\t\t);\n\n\t\tadd_filter( 'llms_database_query_prepare_query', array( $this, '_filter_prepend_sql_calc_found_rows' ), 20, 2 );\n\n\t\ttry {\n\t\t\t$this->factory->post->create_many( 4 );\n\n\t\t\t$query = $this->query();\n\n\t\t\t$this->assertGreaterThan( 0, $query->get_found_results() );\n\t\t} finally {\n\t\t\tremove_filter( 'llms_database_query_prepare_query', array( $this, '_filter_prepend_sql_calc_found_rows' ), 20 );\n\t\t}\n\t}\n\n\t/**\n\t * Filter callback: prepend SQL_CALC_FOUND_ROWS to the SELECT for {@see test_filter_reintroducing_sql_calc_found_rows()}.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @param string              $sql   Query SQL.\n\t * @param LLMS_Database_Query $query Query instance.\n\t * @return string\n\t */\n\tpublic function _filter_prepend_sql_calc_found_rows( $sql, $query ) {\n\t\treturn preg_replace( '/SELECT /', 'SELECT SQL_CALC_FOUND_ROWS ', $sql, 1 );\n\t}\n\n\t/**\n\t * Build query\n\t *\n\t * @since 4.5.1\n\t *\n\t * @param array $args Optional. Query arguments. Default empty array.\n\t *                    When not provided the default arguments will be used.\n\t * @return void\n\t */\n\tprivate function query( $args = array() ) {\n\n\t\tadd_filter( 'llms_database_query_prepare_query', array( $this, '_prepare_query' ), 10, 2 );\n\t\tif ( ! empty( $args ) ) {\n\t\t\t$this->_arguments_original = $args;\n\t\t\tadd_filter( 'llms_database_query_parse_args', array( $this, '_parse_args' ), 10, 2 );\n\t\t}\n\n\t\t$query = $this->get_stub();\n\n\t\tadd_filter( 'llms_database_query_prepare_query', array( $this, '_prepare_query' ), 10, 2 );\n\t\tif ( ! empty( $args ) ) {\n\t\t\tremove_filter( 'llms_database_query_parse_args', array( $this, '_parse_args' ), 10, 2 );\n\t\t\tunset($this->_arguments_original);\n\t\t}\n\n\t\treturn $query;\n\t}\n\n\t/**\n\t * Prepare query to build a testable SQL\n\t *\n\t * @since 4.5.1\n\t * @since 10.0.0 Set count_query for found_results() support.\n\t *\n\t * @return string\n\t */\n\tpublic function _prepare_query( $sql, $query ) {\n\t\tglobal $wpdb;\n\t\t$select  = LLMS_Unit_Test_Util::call_method( $query, 'sql_select_columns' );\n\t\t$orderby = LLMS_Unit_Test_Util::call_method( $query, 'sql_orderby' );\n\t\t$limit   = LLMS_Unit_Test_Util::call_method( $query, 'sql_limit' );\n\n\t\tif ( ! $query->get( 'no_found_rows' ) ) {\n\t\t\tLLMS_Unit_Test_Util::set_private_property( $query, 'count_query', \"SELECT COUNT(*) FROM {$wpdb->posts}\" );\n\t\t}\n\n\t\treturn \"\n\t\t\tSELECT {$select}\n\t\t\tFROM {$wpdb->posts}\n\t\t\t{$orderby}\n\t\t\t{$limit};\n\t\t\";\n\t}\n\n\t/**\n\t * Parse args\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return string\n\t */\n\tpublic function _parse_args( $args, $query ) {\n\t\treturn wp_parse_args( $this->_arguments_original, $args );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-exportable-abmin-table.php",
    "content": "<?php\n/**\n * Tests {@see LLMS_Abstract_Exportable_Admin_Table}.\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group admin_tables\n *\n * @since 7.0.1\n */\nclass LLMS_Test_Abstract_Exportable_Admin_Table extends LLMS_UnitTestCase {\n\n\t/**\n\t * Retrieves a mock for the abstract class.\n\t *\n\t * @since 7.0.1\n\t *\n\t * @return LLMS_Abstract_Exportable_Admin_Table\n\t */\n\tprivate function get_mock( $id = 'mock', $title = 'Mock Title' ) {\n\t\t$mock = $this->getMockForAbstractClass(\n\t\t\tLLMS_Abstract_Exportable_Admin_Table::class,\n\t\t\tarray(),\n\t\t\t'',\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t\tarray( 'get_title' )\n\t\t);\n\t\tLLMS_Unit_Test_Util::set_private_property( $mock, 'id', $id );\n\n\t\t$mock->method( 'get_title' )->willReturn( $title );\n\n\t\treturn $mock;\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Abstract_Exportable_Admin_Table::get_export_file_name}\n\t *\n\t * @since 7.0.1\n\t */\n\tpublic function test_get_export_file_name() {\n\n\t\t$pass = function( $pass ) {\n\t\t\treturn 'ABCD1234';\n\t\t};\n\t\tadd_filter( 'random_password', $pass );\n\n\t\t$now  = time();\n\t\t$date = date( 'Y-m-d', $now );\n\t\tllms_tests_mock_current_time( $now );\n\n\t\t$this->assertEquals(\n\t\t\t\"mock-title_export_{$date}_ABCD1234\",\n\t\t\t$this->get_mock()->get_export_file_name()\n\t\t);\n\n\t\tremove_filter( 'random_password', $pass );\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Abstract_Exportable_Admin_Table::get_export_file_name}\n\t * when the table's title contains special characters.\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1540\n\t *\n\t * @since 7.0.1\n\t */\n\tpublic function test_get_export_file_name_special_chars() {\n\n\t\t$pass = function( $pass ) {\n\t\t\treturn 'ABCD1234';\n\t\t};\n\t\tadd_filter( 'random_password', $pass );\n\n\t\t$now  = time();\n\t\t$date = date( 'Y-m-d', $now );\n\t\tllms_tests_mock_current_time( $now );\n\n\t\t$this->assertEquals(\n\t\t\t\"الطلاب_export_{$date}_ABCD1234\",\n\t\t\t$this->get_mock( 'mock', 'الطلاب' )->get_export_file_name()\n\t\t);\n\n\t\tremove_filter( 'random_password', $pass );\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Abstract_Exportable_Admin_Table::get_title} stub.\n\t *\n\t * @since 7.0.1\n\t */\n\tpublic function test_get_title() {\n\n\t\t$mock = $this->getMockForAbstractClass(\n\t\t\tLLMS_Abstract_Exportable_Admin_Table::class\n\t\t);\n\t\tLLMS_Unit_Test_Util::set_private_property( $mock, 'id', 'mock' );\n\n\t\t$this->setExpectedIncorrectUsage(\n\t\t\t'LLMS_Abstract_Exportable_Admin_Table::get_title'\n\t\t);\n\t\t$this->assertEquals( 'mock', $mock->get_title() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-generator-posts.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Generator_Posts class\n *\n * @group abstracts\n * @group generator\n * @group generator_posts\n *\n * @since 4.7.0\n */\nclass LLMS_Test_Abstract_Generator_Posts extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.7.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->stub = $this->get_stub();\n\n\t}\n\n\t/**\n\t * Retrieve the abstract class mock stub\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return LLMS_Abstract_Generator_Posts\n\t */\n\tprivate function get_stub( $raw = array() ) {\n\t\treturn $this->getMockForAbstractClass( 'LLMS_Abstract_Generator_Posts', array( $raw ) );\n\t}\n\n\t/**\n\t * Test add_custom_values()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_custom_values() {\n\n\t\t$post_id = $this->factory->post->create();\n\n\t\t$raw = array(\n\t\t\t'custom' => array(\n\t\t\t\t'_mock_multi'      => array( 1, 2, 3, ),\n\t\t\t\t'_mock_single'     => array( 'value', ),\n\t\t\t\t'_mock_empty'      => array( '', ),\n\t\t\t\t'_mock_serialized' => array( serialize( array( 'data' => true ) ) ),\n\t\t\t\t'_mock_json'       => array( '{\"data\":\"string\"}' ),\n\t\t\t),\n\t\t);\n\n\t\t$this->stub->add_custom_values( $post_id, $raw );\n\n\t\t$this->assertEquals( array( 1, 2, 3 ), get_post_meta( $post_id, '_mock_multi' ) );\n\t\t$this->assertEquals( 'value', get_post_meta( $post_id, '_mock_single', true ) );\n\t\t$this->assertEquals( '', get_post_meta( $post_id, '_mock_empty', true ) );\n\t\t$this->assertEquals( array( 'data' => true ), get_post_meta( $post_id, '_mock_serialized', true ) );\n\t\t$this->assertEquals( '{\"data\":\"string\"}', get_post_meta( $post_id, '_mock_json', true ) );\n\t}\n\n\t/**\n\t * Test create_post() success\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_post() {\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'create_post', array( 'course', array( 'title' => 'test' ) ) );\n\t\t$this->assertInstanceOf( 'LLMS_Course', $res );\n\t\t$this->assertEquals( 'test', $res->get( 'title' ) );\n\n\t}\n\n\t/**\n\t * Test create_post() for invalid post type classes\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_post_invalid_type() {\n\n\t\t$this->setExpectedException( Exception::class, esc_html( 'The class \"LLMS_Fake_Type\" does not exist.' ), 1100 );\n\t\tLLMS_Unit_Test_Util::call_method( $this->stub, 'create_post', array( 'fake_type' ) );\n\n\t}\n\n\t/**\n\t * Test create_post() when an error is encountered during creation\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_post_error() {\n\n\t\t// Force post creation to fail.\n\t\t$handler = function( $args ) {\n\t\t\treturn array();\n\t\t};\n\t\tadd_filter( 'llms_new_course', $handler );\n\n\t\t$this->setExpectedException( Exception::class, 'Error creating the course post object.', 1000 );\n\t\tLLMS_Unit_Test_Util::call_method( $this->stub, 'create_post', array( 'course', array( 'title' => '' ) ) );\n\n\t\tremove_filter( 'llms_new_course', $handler );\n\n\t}\n\n\t/**\n\t * Test create_reusable_block() when the block already exists\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_reusable_block_already_exists() {\n\n\t\t$title   = 'Dupcheck reuse block';\n\t\t$content = 'Block content';\n\n\t\t$dup = $this->factory->post->create( array(\n\t\t\t'post_type' => 'wp_block',\n\t\t\t'post_title' => $title,\n\t\t\t'post_content' => $content,\n\t\t) );\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'create_reusable_block', array( $dup, compact( 'title', 'content' ) ) ) );\n\n\t}\n\n\t/**\n\t * Test create_reusable_block() when there's an error creating the block\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_reusable_block_error() {\n\n\t\t// Force an error response.\n\t\tadd_filter( 'wp_insert_post_empty_content', '__return_true' );\n\t\t$block_post_id = $this->factory->post->create();\n\n\t\t$this->assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->stub, 'create_reusable_block',\n\t\t\tarray(\n\t\t\t\tis_wp_error( $block_post_id ) ? 0 : $block_post_id,\n\t\t\t\tarray(\n\t\t\t\t\t'title' => '',\n\t\t\t\t\t'content' => '',\n\t\t\t\t)\n\t\t\t)\n\t\t) );\n\n\t\tremove_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test create_reusable_block() for success\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_reusable_block_success() {\n\n\t\t$orig_id = $this->factory->post->create();\n\n\t\t$title   = 'Reusable block title';\n\t\t$content = 'Reusable block content';\n\n\t\t$id = LLMS_Unit_Test_Util::call_method( $this->stub, 'create_reusable_block', array( $orig_id,  compact( 'title', 'content' ) ) );\n\t\t$post = get_post( $id );\n\n\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t$this->assertEquals( 'wp_block', $post->post_type );\n\t\t$this->assertEquals( $title, $post->post_title );\n\t\t$this->assertEquals( $content, $post->post_content );\n\n\t\t$blocks = LLMS_Unit_Test_Util::get_private_property_value( $this->stub, 'reusable_blocks' );\n\t\t$this->assertEquals( $id, $blocks[ $orig_id ] );\n\n\t}\n\n\t/**\n\t * Test format_date()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_format_date() {\n\n\t\t// No date supplied, use current time.\n\t\t$expect = '2020-03-25 09:54:12';\n\t\tllms_tests_mock_current_time( $expect );\n\n\t\t$this->assertEquals( $expect, $this->stub->format_date() );\n\n\t\tllms_tests_reset_current_time();\n\n\t\t// Format is okay.\n\t\t$this->assertEquals( '2015-03-02 23:12:32', $this->stub->format_date( '2015-03-02 23:12:32' ) );\n\n\t\t// Missing time.\n\t\t$this->assertEquals( '2019-01-01 00:00:00', $this->stub->format_date( '2019-01-01' ) );\n\n\t\t// Valid format.\n\t\t$this->assertEquals( '2019-01-01 00:00:00', $this->stub->format_date( 'January 1, 2019' ) );\n\n\t}\n\n\tpublic function test_get_author_id_no_id_or_email() {\n\n\t\t$uid = $this->factory->user->create();\n\t\twp_set_current_user( $uid );\n\n\t\t$this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array() ) ) );\n\n\t}\n\n\tpublic function test_get_author_id() {\n\n\t\t$email = 'mockauthor@test.tld';\n\t\t$uid   = $this->factory->user->create( array( 'user_email' => $email ) );\n\n\t\t// Only email.\n\t\t$this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array(\n\t\t\t'email' => $email,\n\t\t) ) ) );\n\n\t\t// Only ID.\n\t\t$this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array(\n\t\t\t'id'    => $uid,\n\t\t) ) ) );\n\n\t\t// ID & EMail and the email matches the existing user.\n\t\t$this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array(\n\t\t\t'id'    => $uid,\n\t\t\t'email' => $email,\n\t\t) ) ) );\n\n\t\t// ID & email and the email does not match the existing user.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array(\n\t\t\t'id'    => $uid,\n\t\t\t'email' => 'adifferentemail@test.tld',\n\t\t) ) );\n\t\t$this->assertEquals( 'adifferentemail@test.tld', get_user_by( 'ID', $res )->user_email );\n\n\t\t// User doesn't exist, create a new one.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array(\n\t\t\t'id'    => $res + 1,\n\t\t\t'email' => 'anotheremail@test.tld',\n\t\t) ) );\n\t\t$this->assertEquals( 'anotheremail@test.tld', get_user_by( 'ID', $res )->user_email );\n\n\t\t// Email only, create a new user.\n\t\t$raw = array(\n\t\t\t'email'       => 'el_duderino@earthlink.net',\n\t\t\t'first_name'  => 'Jeffrey',\n\t\t\t'last_name'   => 'Lebowski',\n\t\t\t'description' => \"Nobody calls me Lebowski. You got the wrong guy. I'm the Dude, man.\",\n\t\t);\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( $raw ) );\n\t\t$user = get_user_by( 'ID', $res );\n\t\t$this->assertEquals( $raw['email'], $user->user_email );\n\t\t$this->assertEquals( $raw['first_name'] . ' ' . $raw['last_name'], $user->display_name );\n\t\t$this->assertEquals( $raw['first_name'], $user->first_name );\n\t\t$this->assertEquals( $raw['last_name'], $user->last_name );\n\t\t$this->assertEquals( $raw['description'], $user->description );\n\t\t$this->assertTrue( $user->has_cap( 'administrator' ) ); // Default role.\n\n\t\t// Pass in a role.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array(\n\t\t\t'email' => 'instructoruser@test.tld',\n\t\t\t'role'  => 'instructor',\n\t\t) ) );\n\t\t$this->assertTrue( get_user_by( 'ID', $res )->has_cap( 'instructor' ) );\n\n\t}\n\n\t/**\n\t * Test get_author_id() when an error creating the user is encountered.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_author_id_error() {\n\n\t\t// Error during creation.\n\t\t$handler = function( $data ) {\n\t\t\t$data['user_login'] = '';\n\t\t\treturn $data;\n\t\t};\n\t\tadd_filter( 'llms_generator_new_author_data', $handler );\n\t\t$this->setExpectedException( Exception::class, 'Cannot create a user with an empty login name.', 1002 );\n\t\tLLMS_Unit_Test_Util::call_method( $this->stub, 'get_author_id', array( array( 'email' => 'fake@test.tld' ) ) );\n\t\tremove_filter( 'llms_generator_new_author_data', $handler );\n\n\t}\n\n\t/**\n\t * Test get_author_id_from_raw()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_author_id_from_raw() {\n\n\t\t$user = $this->factory->user->create();\n\n\t\t// Retrievable from raw.\n\t\t$this->assertEquals( $user, $this->stub->get_author_id_from_raw( array( 'author' => array( 'id' => $user ) ) ) );\n\n\t\t// No raw submitted & no fallback, use current user.\n\t\twp_set_current_user( $user );\n\t\t$this->assertEquals( $user, $this->stub->get_author_id_from_raw( array() ) );\n\n\t\t// Use fallback id.\n\t\t$this->assertEquals( 832, $this->stub->get_author_id_from_raw( array(), 832 ) );\n\n\t}\n\n\t/**\n\t * Test default post status getter & setter\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_set_default_post_status() {\n\n\t\t// Default.\n\t\t$this->assertEquals( 'draft', $this->stub->get_default_post_status() );\n\n\t\t// Modify.\n\t\t$this->stub->set_default_post_status( 'publish' );\n\t\t$this->assertEquals( 'publish', $this->stub->get_default_post_status() );\n\n\t}\n\n\t/**\n\t * Test get_term_id()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_term_id() {\n\n\t\t$name = 'mock generator term';\n\t\t$tax  = 'course_cat';\n\n\t\t// Create a term that doesn't already exist.\n\t\t$id = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_term_id', array( $name, $tax ) );\n\n\t\t$term = get_term_by( 'id', $id, $tax );\n\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t$this->assertEquals( $name, $term->name );\n\n\t\t// Already exists.\n\t\t$this->assertEquals( $id, LLMS_Unit_Test_Util::call_method( $this->stub, 'get_term_id', array( $name, $tax ) ) );\n\n\t}\n\n\t/**\n\t * Test get_term_id() when an error is encountered during creation of a new term\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_term_id_error() {\n\n\t\t$handler = function( $term ) {\n\t\t\treturn new WP_Error( 'mock-term-insert-err', 'Error' );\n\t\t};\n\t\tadd_filter( 'pre_insert_term', $handler );\n\t\t$this->setExpectedException( Exception::class, esc_html( 'Error creating new term \"mock gen term\".' ), 1001 );\n\t\tLLMS_Unit_Test_Util::call_method( $this->stub, 'get_term_id', array( 'mock gen term', 'course_cat' ) );\n\t\tremove_filter( 'pre_insert_term', $handler );\n\n\t}\n\n\t/**\n\t * Test handle_reusable_blocks() when importing is disabled\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_reusable_blocks_disabled() {\n\n\t\tadd_filter( 'llms_generator_is_reusable_block_importing_enabled', '__return_false' );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, 2 ) ) );\n\t\tremove_filter( 'llms_generator_is_reusable_block_importing_enabled', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test handle_reusable_blocks() when no blocks to import\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_reusable_blocks_none() {\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, array() ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, array( '_extras' => array() ) ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( 1, array( '_extras' => array( 'blocks' => array() ) ) ) ) );\n\n\t}\n\n\t/**\n\t * Test handle_reusable_blocks() when no blocks to import\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_reusable_blocks() {\n\n\t\t$html = serialize_blocks( array(\n\t\t\tarray(\n\t\t\t\t'blockName' => 'core/block',\n\t\t\t\t'innerContent' => array( '' ),\n\t\t\t\t'attrs' => array(\n\t\t\t\t\t'ref' => 123,\n\t\t\t\t)\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'blockName'    => 'core/paragraph',\n\t\t\t\t'innerContent' => array( 'Lorem ipsum dolor sit.' ),\n\t\t\t\t'attrs'        => array(),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'blockName' => 'core/block',\n\t\t\t\t'innerContent' => array( '' ),\n\t\t\t\t'attrs' => array(\n\t\t\t\t\t'ref' => 456,\n\t\t\t\t)\n\t\t\t),\n\t\t) );\n\n\t\t$course_id = $this->factory->post->create( array(\n\t\t\t'post_content' => $html,\n\t\t\t'post_type'    => 'course',\n\t\t) );\n\t\t$course    = llms_get_post( $course_id );\n\n\t\t$raw = array(\n\t\t\t'_extras' => array(\n\t\t\t\t'blocks' => array(\n\t\t\t\t\t'123' => array(\n\t\t\t\t\t\t'title'  => 'Mock Block 1',\n\t\t\t\t\t\t'content' => 'mock content 1'\n\t\t\t\t\t),\n\t\t\t\t\t'456' => array(\n\t\t\t\t\t\t'title'  => 'Mock Block 2',\n\t\t\t\t\t\t'content' => 'mock content 2'\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'handle_reusable_blocks', array( $course, $raw ) );\n\n\t\t// Proper return.\n\t\t$this->assertTrue( $res );\n\n\t\t// Post content updated with newly created blocks.\n\t\t$block = parse_blocks( llms_get_post( $course_id )->get( 'content', true ) );\n\n\t\t$this->assertEquals( 'core/block', $block[0]['blockName'] );\n\t\t$this->assertNotEquals( 123, $block[0]['attrs']['ref'] );\n\t\t$block1 = get_post( $block[0]['attrs']['ref'] );\n\t\t$this->assertEquals( 'Mock Block 1', $block1->post_title );\n\t\t$this->assertEquals( 'mock content 1', $block1->post_content );\n\n\t\t$this->assertEquals( 'core/paragraph', $block[1]['blockName'] );\n\n\t\t$this->assertEquals( 'core/block', $block[2]['blockName'] );\n\t\t$this->assertNotEquals( 456, $block[2]['attrs']['ref'] );\n\t\t$block2 = get_post( $block[2]['attrs']['ref'] );\n\t\t$this->assertEquals( 'Mock Block 2', $block2->post_title );\n\t\t$this->assertEquals( 'mock content 2', $block2->post_content );\n\n\t}\n\n\t/**\n\t * Test is_image_sideloading_enabled()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_image_sideloading_enabled() {\n\t\t$this->assertTrue( $this->stub->is_image_sideloading_enabled() );\n\t}\n\n\t/**\n\t * Test is_reusable_block_importing_enabled()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_reusable_block_importing_enabled() {\n\t\t$this->assertTrue( $this->stub->is_reusable_block_importing_enabled() );\n\t}\n\n\t/**\n\t * Test set_featured_image()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_featured_image() {\n\n\t\t$tests = array(\n\t\t\t// String.\n\t\t\t'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg',\n\t\t\t// Parse from raw.\n\t\t\tarray( 'featured_image' => 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg' ),\n\t\t);\n\n\t\tforeach ( $tests as $arg ) {\n\n\t\t\t$post_id = $this->factory->post->create();\n\t\t\t$id      = LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( $arg, $post_id ) );\n\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t\t$this->assertEquals( $id, get_post_thumbnail_id( $post_id ) );\n\n\t\t}\n\n\t\t// No image.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( array(), $post_id ) ) );\n\n\t\t// Error.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( 'fake', $post_id ) ) );\n\n\t\t// Disabled.\n\t\tadd_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->stub, 'set_featured_image', array( 'fake', $post_id ) ) );\n\t\tremove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test sideload_image()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sideload_image() {\n\n\t\t$post = $this->factory->post->create();\n\t\t$url  = 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg';\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url ) );\n\n\t\t$this->assertStringNotContains( 'raw.githubusercontent', $res );\n\t\t$this->assertStringContains( 'christian-fregnan-unsplash', $res );\n\n\t\t// Image already sideloaded so it's not sideloaded again.\n\t\t$res2 = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url ) );\n\t\t$this->assertEquals( $res, $res2 );\n\n\t\t// Test ID return.\n\t\t$id = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url, 'id' ) );\n\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t$this->assertEquals( $res2, wp_get_attachment_url( $id ) );\n\n\t}\n\n\t/**\n\t * Test sideload_image() error\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sideload_image_error() {\n\n\t\t$post = $this->factory->post->create();\n\t\t$url  = 'fake.jpg';\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_image', array( $post, $url ) );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'http_request_failed', $res );\n\n\t}\n\n\t/**\n\t * Test sideload_images()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sideload_images() {\n\n\t\t$course = llms_get_post( $this->factory->post->create( array(\n\t\t\t'post_type'    => 'course',\n\t\t\t'post_content' => '<!-- wp:image {\"id\":552,\"sizeSlug\":\"large\"} -->\n<figure class=\"wp-block-image size-large\"><img src=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg\" alt=\"\" class=\"wp-image-552\"/></figure>\n<!-- /wp:image -->\n\n<!-- wp:gallery {\"ids\":[552,11]} -->\n<figure class=\"wp-block-gallery columns-2 is-cropped\"><ul class=\"blocks-gallery-grid\">\n<li class=\"blocks-gallery-item\"><figure><img src=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg\" alt=\"\" data-id=\"552\" data-full-url=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg\" data-link=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg\" class=\"wp-image-552\"/></figure></li>\n<li class=\"blocks-gallery-item\"><figure><img src=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/richard-i49WGMPd5aA-unsplash.jpg\" alt=\"\" data-id=\"11\" data-full-url=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/richard-i49WGMPd5aA-unsplash.jpg\" data-link=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/richard-i49WGMPd5aA-unsplash.jpg\" class=\"wp-image-11\"/></figure></li></ul></figure>\n<!-- /wp:gallery -->\n\n<img src=\"https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg\" alt=\"\" class=\"wp-image-552\"/>'\n\t\t) ) );\n\n\t\t$raw = array(\n\t\t\t'_extras' => array(\n\t\t\t\t'images' => array(\n\t\t\t\t\t'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg',\n\t\t\t\t\t'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/richard-i49WGMPd5aA-unsplash.jpg',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, $raw ) ) );\n\t\t$this->assertStringNotContains( 'raw.githubusercontent', $course->post->post_content );\n\n\t}\n\n\t/**\n\t * Test sideload_images(): skip sideloading of images from the same site.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sideload_images_from_same_site() {\n\n\t\t$course = llms_get_post( $this->factory->post->create( array(\n\t\t\t'post_type'    => 'course',\n\t\t\t'post_content' => '<img src=\"https://example.org/fake-image.png\" />',\n\t\t) ) );\n\n\t\t$raw = array(\n\t\t\t'_extras' => array(\n\t\t\t\t'images' => array(\n\t\t\t\t\t'https://example.org/fake-image.png',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, $raw ) ) );\n\t\t$this->assertEquals( '<img src=\"https://example.org/fake-image.png\" />', $course->post->post_content );\n\n\n\t}\n\n\t/**\n\t * Test sideload_images() with no images in post content\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sideload_images_none() {\n\n\t\t$course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array() ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array( '_extras' => array() ) ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array( '_extras' => array( 'images' => array() ) ) ) ) );\n\n\t}\n\n\t/**\n\t * Test sideload_images() with sideloading disabled\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sideload_images_disabled() {\n\n\t\t$course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\n\t\tadd_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->stub, 'sideload_images', array( $course, array() ) ) );\n\t\tremove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-integration.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Integration class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group integrations\n *\n * @since 3.19.0\n */\nclass LLMS_Test_Abstract_Integration extends LLMS_UnitTestCase {\n\n\t/**\n\t * Retrieve the abstract class mock stub\n\t *\n\t * @since 3.19.0\n\t * @since 4.21.0 Use an anonymous class in favor of a mock abstract.\n\t *\n\t * @return LLMS_Abstract_Integration\n\t */\n\tprivate function get_stub() {\n\n\t\treturn new class() extends LLMS_Abstract_Integration {\n\n\t\t\tprotected function configure() {\n\t\t\t\t$this->id          = 'mocker';\n\t\t\t\t$this->title       = 'Mock Integration';\n\t\t\t\t$this->description = 'this is a mock description of the integration';\n\t\t\t\tdo_action( 'llms_tests_mock_integration_configured' );\n\t\t\t}\n\n\t\t\tpublic $__is_installed = true;\n\t\t\tpublic function is_installed() {\n\t\t\t\treturn $this->__is_installed;\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\t/**\n\t * Test the constructor.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t$configure_action = did_action( 'llms_tests_mock_integration_configured' );\n\t\t$init_action      = did_action( 'llms_integration_mocker_init' );\n\n\t\tremove_filter( 'lifterlms_integrations_settings_mocker', array( $stub, 'add_settings' ), 20 );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'plugin_basename', 'mockerpluginbasename' );\n\n\t\t$stub->__construct();\n\n\t\t// Actions ran.\n\t\t$this->assertEquals( ++$configure_action, did_action( 'llms_tests_mock_integration_configured' ) );\n\t\t$this->assertEquals( ++$init_action, did_action( 'llms_integration_mocker_init' ) );\n\n\t\t// Filter added.\n\t\t$this->assertEquals( 20, has_filter( 'lifterlms_integrations_settings_mocker', array( $stub, 'add_settings' ) ) );\n\n\t\t// Plugin actions link added.\n\t\t$this->assertEquals( 100, has_action( 'plugin_action_links_mockerpluginbasename', array( $stub, 'plugin_action_links' ) ) );\n\n\n\t}\n\n\t/**\n\t * Test add_settings() method\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_settings() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t// Must be an array.\n\t\t$this->assertTrue( is_array( $stub->add_settings( array() ) ) );\n\n\t\t// Only the default integration settings.\n\t\t$this->assertEquals( 4, count( $stub->add_settings( array() ) ) );\n\n\t\t// Mimic other settings from other integrations.\n\t\t$this->assertEquals( 10, count( $stub->add_settings( array( 1, 2, 3, 4, 5, 6 ) ) ) );\n\n\t}\n\n\t/**\n\t * Test the get_option() method v1 behavior\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option_v1() {\n\n\t\t$stub = $this->get_stub();\n\t\t$this->assertEquals( '', $stub->get_option( 'enabled' ) );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) );\n\t\t$this->assertEquals( 'no', $stub->get_option( 'enabled', 'no' ) );\n\t\t$this->assertEquals( 'fake', $stub->get_option( 'enabled', 'fake' ) );\n\n\t\t$stub->set_option( 'enabled', 'yes' );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled' ) );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'no' ) );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test the get_option() method v2 behavior\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option_v2() {\n\n\t\t$stub = $this->get_stub();\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'version', 2 );\n\n\t\t$this->assertEquals( 'no', $stub->get_option( 'enabled' ) );\n\n\t\t// Don't autoload the default value when a default value is passed.\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) );\n\t\t$this->assertEquals( 'no', $stub->get_option( 'enabled', 'no' ) );\n\t\t$this->assertEquals( 'fake', $stub->get_option( 'enabled', 'fake' ) );\n\n\t\t$stub->set_option( 'enabled', 'yes' );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled' ) );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'yes' ) );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'no' ) );\n\t\t$this->assertEquals( 'yes', $stub->get_option( 'enabled', 'fake' ) );\n\n\t}\n\n\t/**\n\t * Directly test the get_option_default_value() method.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option_default_value() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t$this->assertEquals( 'no', $stub->get_option_default_value( '', $stub->get_option_name( 'enabled' ), false ) );\n\t\t$this->assertEquals( 'no', $stub->get_option_default_value( 'yes', $stub->get_option_name( 'enabled' ), false ) );\n\n\t\t// Default value explicitly passed.\n\t\t$this->assertEquals( 'yes', $stub->get_option_default_value( 'yes', $stub->get_option_name( 'enabled' ), true ) );\n\n\t}\n\n\n\t/**\n\t * Test the get_priority() method\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_priority() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t// Default.\n\t\t$this->assertEquals( 20, $stub->get_priority() );\n\n\t\t// Redefined.\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'priority', 50 );\n\t\t$this->assertEquals( 50, $stub->get_priority() );\n\n\t}\n\n\t/**\n\t * Test get_settings()\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t$settings = LLMS_Unit_Test_Util::call_method( $stub, 'get_settings' );\n\n\t\t$expected = array(\n\t\t\t'llms_integration_mocker_start',\n\t\t\t'llms_integration_mocker_title',\n\t\t\t'llms_integration_mocker_enabled',\n\t\t\t'llms_integration_mocker_end',\n\t\t);\n\t\t$this->assertEquals( $expected, wp_list_pluck( $settings, 'id' ) );\n\n\t}\n\n\t/**\n\t * Test get_settings() when missing requirements.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings_not_installed() {\n\n\t\t$stub = $this->get_stub();\n\t\t$stub->__is_installed = false;\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'description_missing', 'Missing requirements' );\n\n\t\t$settings = LLMS_Unit_Test_Util::call_method( $stub, 'get_settings' );\n\n\t\t$expected = array(\n\t\t\t'llms_integration_mocker_start',\n\t\t\t'llms_integration_mocker_title',\n\t\t\t'llms_integration_mocker_enabled',\n\t\t\t'llms_integration_mocker_missing_requirements_desc',\n\t\t\t'llms_integration_mocker_end',\n\t\t);\n\t\t$this->assertEquals( $expected, wp_list_pluck( $settings, 'id' ) );\n\n\t}\n\n\t/**\n\t * Test is_available() method\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_available() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t// By default it is not available.\n\t\t$this->assertFalse( $stub->is_available() );\n\n\t\t// Enable it.\n\t\t$stub->set_option( 'enabled', 'yes' );\n\t\t$this->assertTrue( $stub->is_available() );\n\n\t\t// Explicitly disable it.\n\t\t$stub->set_option( 'enabled', 'no' );\n\t\t$this->assertFalse( $stub->is_available() );\n\n\t}\n\n\t/**\n\t * Test is_enabled() method\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_enabled() {\n\n\t\t$stub = $this->get_stub();\n\n\t\t// Disabled by default (no option found).\n\t\t$this->assertFalse( $stub->is_enabled() );\n\n\t\t// Enable it.\n\t\t$stub->set_option( 'enabled', 'yes' );\n\t\t$this->assertTrue( $stub->is_enabled() );\n\n\t\t// Explicitly disable it.\n\t\t$stub->set_option( 'enabled', 'no' );\n\t\t$this->assertFalse( $stub->is_enabled() );\n\n\t}\n\n\t/**\n\t * Test is_installed() method\n\t *\n\t * By default this just returns true, extending classes override it\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_installed() {\n\n\t\t$this->assertTrue( $this->get_stub()->is_installed() );\n\n\t}\n\n\t/**\n\t * Test plugin_action_links()\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_plugin_action_links() {\n\n\t\t$mock_links    = array( '<a href=\"#\">FAKE</a>' );\n\t\t$expected_link = array( '<a href=\"http://example.org/wp-admin/admin.php?page=llms-settings&#038;tab=integrations&#038;section=mocker\">Settings</a>' );\n\t\t$stub          = $this->get_stub();\n\n\t\t$this->assertEquals( array_merge( $mock_links, $expected_link ), $stub->plugin_action_links( $mock_links, 'mock', array(), 'all' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-notification-view.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Notification_View.\n *\n * @group abstracts\n * @group notifications\n *\n * @since 6.4.0\n */\nclass LLMS_Test_Abstract_Notification_View extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\tglobal $wpdb;\n\n\t\t$this->main = $this->get_stub(\n\t\t\tnew LLMS_Notification( $wpdb->insert_id )\n\t\t);\n\n\t}\n\n\t/**\n\t * Retrieve the abstract class mock stub.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return LLMS_Abstract_Notification_View\n\t */\n\tprivate function get_stub( $notification ) {\n\n\t\treturn new class( $notification ) extends LLMS_Abstract_Notification_View {\n\n\t\t\t/**\n\t\t\t * Replace merge codes with actual values.\n\t\t\t *\n\t\t\t * @param string $code The merge code to get merged data for.\n\t\t\t * @return string\n\t\t\t */\n\t\t\tprotected function set_merge_data( $code ) {\n\n\t\t\t\tswitch ( $code ) {\n\t\t\t\t\tcase '{{MG_1}}' :\n\t\t\t\t\t\treturn 'Merge Code expanded 1';\n\n\t\t\t\t\tcase '{{MG_2}}' :\n\t\t\t\t\t\treturn 'Merge Code expanded 2';\n\n\t\t\t\t\tcase '{{MG_3}}' :\n\t\t\t\t\t\treturn 'Merge Code expanded 3';\n\n\t\t\t\t\tcase '{{MG_4}}' :\n\t\t\t\t\t\treturn 'Merge Code expanded 4';\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Setup body content for output.\n\t\t\t *\n\t\t\t * @return string\n\t\t\t */\n\t\t\tprotected function set_body() {\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Setup footer content for output.\n\t\t\t *\n\t\t\t * @return string\n\t\t\t */\n\t\t\tprotected function set_footer() {\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Setup notification icon for output.\n\t\t\t *\n\t\t\t * @return string\n\t\t\t */\n\t\t\tprotected function set_icon() {\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Setup merge codes that can be used with the notification\n\t\t\t *\n\t\t\t * @return array\n\t\t\t */\n\t\t\tprotected function set_merge_codes() {\n\n\t\t\t\treturn array(\n\t\t\t\t\t'{{MG_1}}' => 'Merge code 1',\n\t\t\t\t\t'{{MG_2}}' => 'Merge code 2',\n\t\t\t\t\t'{{MG_3}}' => 'Merge code 3',\n\t\t\t\t\t'{{MG_4}}' => 'Merge code 4',\n\t\t\t\t);\n\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Setup notification subject line for output.\n\t\t\t *\n\t\t\t * @return string\n\t\t\t */\n\t\t\tprotected function set_subject() {\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Setup notification title for output\n\t\t\t *\n\t\t\t * On an email the title acts as the \"heading\" element.\n\t\t\t *\n\t\t\t * @return string\n\t\t\t */\n\t\t\tprotected function set_title() {\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\t/**\n\t * Test get_used_merge_codes().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_used_merge_codes() {\n\n\t\t$string_expected = array(\n\t\t\t''                                                         => array(),\n\t\t\t'Something in the way she moves... reminds me of {{MG_6}}' => array(),\n\t\t\t'{{MG_1}} and {{MG_4}}'                                    => array(\n\t\t\t\t'{{MG_1}}',\n\t\t\t\t'{{MG_4}}',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $string_expected as $string => $expected ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t$expected,\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'get_used_merge_codes',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$string,\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$string\n\t\t\t);\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test get_merged_string().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_merged_string() {\n\n\t\t$string_expected = array(\n\t\t\t''                                                                           => '',\n\t\t\t'Something'                                                                  => 'Something',\n\t\t\t'Useless Merge Code {{MG_5}}'                                                => 'Useless Merge Code {{MG_5}}',\n\t\t\t'{{MG_1}} and {{MG_4}}|{{MG_3}}|{{MG_2}} but not {{MG_5}}; {{MG_2}}_reprise' => 'Merge Code expanded 1 and Merge Code expanded 4|Merge Code expanded 3|Merge Code expanded 2 but not {{MG_5}}; Merge Code expanded 2_reprise',\n\t\t);\n\n\t\tforeach ( $string_expected as $string => $expected ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t$expected,\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'get_merged_string',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$string,\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$string\n\t\t\t);\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-options-data.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Integration class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group options\n * @group settings\n *\n * @since 3.19.0\n * @since 4.21.0 Replaced the `get_stub()` method with `$this->main`, initialized in `set_up()`.\n */\nclass LLMS_Test_Abstract_Options_Data extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 4.21.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->main = $this->getMockForAbstractClass( 'LLMS_Abstract_Options_Data' );\n\t}\n\n\t/**\n\t * Test get_option(): version 1 behavior.\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option() {\n\n\t\t// Default value.\n\t\t$this->assertEquals( '', $this->main->get_option( 'mock_option' ) );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) );\n\n\t\tupdate_option( 'llms_mock_option', 'mockvalue' );\n\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option' ) );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'anothermockvalue' ) );\n\n\t}\n\n\t/**\n\t * Test get_option() when there's an empty string value explicitly saved in the database\n\t *\n\t * This test illustrates what's actually a bug but exists as expected behavior. Fixing this bug\n\t * might result in unexpected consequences throughout add-ons utilizing the existing behavior as\n\t * if it were intended and not a bug.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option_v1_expected_bug() {\n\n\t\t// An empty string value is expected here but due to the bug the supplied default value is supplied instead.\n\t\tupdate_option( 'llms_mock_option', '' );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) );\n\n\t\t// Option Does not exist so we should get the default value either way.\n\t\tdelete_option( 'llms_mock_option' );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) );\n\n\t}\n\n\t/**\n\t * Test get_option(): v2 behavior\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option_v2_behavior() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'version', 2 );\n\n\t\t// No default passed.\n\t\t$this->assertEquals( '', $this->main->get_option( 'mock_option' ) );\n\n\t\t// Default value passed.\n\t\t$this->assertEquals( '', $this->main->get_option( 'mock_option', '' ) );\n\t\t$this->assertEquals( false, $this->main->get_option( 'mock_option', false ) );\n\t\t$this->assertEquals( array(), $this->main->get_option( 'mock_option', array() ) );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) );\n\n\t\tupdate_option( 'llms_mock_option', 'mockvalue' );\n\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option' ) );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', '' ) );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'anothermockvalue' ) );\n\n\t}\n\n\t/**\n\t * Run test_get_option_v1_expected_bug() on v2 to see the bug fixed.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option_v2_expected_bug_fixed() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'version', 2 );\n\n\t\t// This fails on v1, see `test_get_option_v1_expected_bug()`.\n\t\tupdate_option( 'llms_mock_option', '' );\n\t\t$this->assertEquals( '', $this->main->get_option( 'mock_option', 'mockvalue' ) );\n\n\t\t// Option Does not exist so we should get the default value.\n\t\tdelete_option( 'llms_mock_option' );\n\t\t$this->assertEquals( 'mockvalue', $this->main->get_option( 'mock_option', 'mockvalue' ) );\n\n\t}\n\n\t/**\n\t * test get_option_name() method\n\t *\n\t * @since 3.19.0\n\t * @since 4.21.0 Use unit test utils to update private property value.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_option_name() {\n\n\t\t$this->assertEquals( 'llms_mock_option', $this->main->get_option_name( 'mock_option' ) );\n\n\t\t// Change the option prefix as an extending class might via overriding the `get_option_prefix()` method\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'option_prefix', 'llms_extended_' );\n\n\t\t$this->assertEquals( 'llms_extended_mock_option', $this->main->get_option_name( 'mock_option' ) );\n\n\t}\n\n\t/**\n\t * test set_option() method\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_option() {\n\n\t\tdelete_option( 'llms_mock_option' );\n\t\t$this->assertEquals( true, $this->main->set_option( 'mock_option', 'mockvalue' ) );\n\t\t$this->assertEquals( 'mockvalue', get_option( 'llms_mock_option', 'mockvalue' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-payment-gateway.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Payment_Gateway abstract\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group payment_gateway\n *\n * @since 5.3.0\n */\nclass LLMS_Test_Payment_Gateway extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = $this->getMockForAbstractClass( 'LLMS_Payment_Gateway' );\n\t\t$this->main->id = 'cash-now';\n\n\t\t// Clean logs.\n\t\tforeach ( glob( LLMS_LOG_DIR . 'cash-now-*.log*' ) as $file ) {\n\t\t\tunlink( $file );\n\t\t}\n\n\t}\n\n\t/**\n\t * Mock callback method used to add a secure option to the mock gateway's settings.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @param array[] $settings Existing settings array\n\t * @return array[]\n\t */\n\tpublic function add_admin_settings( $settings ) {\n\n\t\t$settings[] = array(\n\t\t\t'id'            => $this->main->get_option_name( 'secure_key' ),\n\t\t\t'secure_option' => 'LLMS_CASH_NOW_SECURE_KEY',\n\t\t);\n\n\t\treturn $settings;\n\t}\n\n\t/**\n\t * Test get_secure_strings().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_get_and_retrieve_secure_strings() {\n\n\t\t$strings = array( 'abcdefg' );\n\n\t\t// No secure options defined.\n\t\t$this->assertEquals( $strings, $this->main->get_secure_strings( $strings, 'cash-now' ) );\n\n\t\tadd_filter( 'llms_get_gateway_settings_fields', array( $this, 'add_admin_settings' ), 10 );\n\n\t\t// Has an option but it isn't defined.\n\t\t$this->assertEquals( $strings, $this->main->get_secure_strings( $strings, 'cash-now' ) );\n\n\t\t// Has a defined option.\n\t\t$this->main->set_option( 'secure_key', 'MY-KEY' );\n\t\t$this->assertEquals(\n\t\t\tarray( 'abcdefg', 'MY-KEY' ),\n\t\t\t$this->main->get_secure_strings( $strings, 'cash-now' )\n\t\t);\n\n\t\t// Has strings loaded at runtime.\n\t\t$this->main->add_secure_string( 'Another String' );\n\t\t$this->assertEquals( array( 'Another String', 'MY-KEY' ), $this->main->retrieve_secure_strings() );\n\n\t\t// Dupchecked.\n\t\t$this->main->add_secure_string( 'Another String' );\n\t\t$this->assertEquals( array( 'Another String', 'MY-KEY' ), $this->main->retrieve_secure_strings() );\n\n\t\t// Another log file.\n\t\t$this->assertEquals( $strings, $this->main->get_secure_strings( $strings, 'llms' ) );\n\n\t\tremove_filter( 'llms_get_gateway_settings_fields', array( $this, 'add_admin_settings' ), 10 );\n\n\t}\n\n\t/**\n\t * Test complete_transaction_ajax().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_complete_transaction_ajax() {\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$res = $this->main->complete_transaction_ajax( $order );\n\n\t\t$this->assertArrayHasKey( 'redirect', $res );\n\t\t$this->assertEquals( 'SUCCESS', $res['status'] );\n\n\t\t// With extra data.\n\t\t$res = $this->main->complete_transaction_ajax( $order, array( 'yes' => 1 ) );\n\t\t$this->assertArrayHasKey( 'redirect', $res );\n\t\t$this->assertEquals( 'SUCCESS', $res['status'] );\n\t\t$this->assertSame( 1, $res['yes'] );\n\n\t\t// Overwrite defaults.\n\t\t$res = $this->main->complete_transaction_ajax( $order, array( 'status' => 'ERROR' ) );\n\t\t$this->assertArrayHasKey( 'redirect', $res );\n\t\t$this->assertEquals( 'ERROR', $res['status'] );\n\n\t}\n\n\t/**\n\t * Test get_option_name()\n\t *\n\t * Tests options-related methods:\n\t *   + get_option()\n\t *   + get_option_default_value()\n\t *   + get_option_prefix()\n\t *   + get_option_name()\n\t *   + and set_option()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_option_methods() {\n\n\t\t$expected_name = 'llms_gateway_cash-now_title';\n\t\t$secure_key    = 'LLMS_GATEWAY_CASH_NOW_TITLE';\n\t\t$expected_val  = 'Cash Now';\n\t\t$this->assertEquals( $expected_name, $this->main->get_option_name( 'title' ) );\n\n\t\t// Empty.\n\t\t$this->assertEquals( '', $this->main->get_option( 'title') );\n\n\t\t// Default value.\n\t\t$this->main->title = 'Currency Immediately';\n\t\t$this->assertEquals( 'Currency Immediately', $this->main->get_option( 'title') );\n\n\t\t// Set the title via WP core methods.\n\t\tupdate_option( $expected_name, $expected_val );\n\n\t\t$this->assertEquals( $expected_val, $this->main->get_option( 'title' ) );\n\n\t\t// Secure not defined, fallsback with the default value.\n\t\t$this->assertEquals( $expected_val, $this->main->get_option( 'title', $secure_key ) );\n\n\t\t// Change the value via setter.\n\t\t$this->main->set_option( 'title', 'Money Later' );\n\t\t$this->assertEquals( 'Money Later', $this->main->get_option( 'title' ) );\n\n\t\t// Secure value defined.\n\t\tdefine( $secure_key, 'Bucks Yesterday' );\n\t\t$this->assertEquals( 'Bucks Yesterday', $this->main->get_option( 'title', $secure_key ) );\n\n\t}\n\n\t/**\n\t * Test log().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_log() {\n\n\t\t$log_path = llms_get_log_path( 'cash-now' );\n\n\t\t// Disabled.\n\t\t$this->main->set_option( 'logging_enabled', 'no' );\n\t\t$this->main->log( 'Test log' );\n\n\t\t// Nothing logged because it's disabled.\n\t\t$this->assertFalse( file_exists( $log_path ) );\n\n\t\t// Logging enabled.\n\t\t$this->main->set_option( 'logging_enabled', 'yes' );\n\t\t$this->main->log( 'Test log' );\n\n\t\t$logs = explode( ' - ', file_get_contents( llms_get_log_path( 'cash-now' ) ) );\n\t\t$this->assertTrue( date_create( $logs[0] ) instanceof DateTime );\n\t\t$this->assertEquals( 'Test log', trim( $logs[1] ) );\n\n\t}\n\n\t/**\n\t * Test log() masks secure strings.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_log_secure_strings() {\n\n\t\t$this->main->set_option( 'logging_enabled', 'yes' );\n\n\t\tadd_filter( 'llms_get_gateway_settings_fields', array( $this, 'add_admin_settings' ), 10 );\n\n\t\t$key = 'F@K3-$3CUR3-K3Y!';\n\t\t$this->main->set_option( 'secure_key', $key );\n\n\t\t$this->main->log( array(\n\t\t\t'headers' => array(\n\t\t\t\t'Authorization' => \"Basic {$key}:password\",\n\t\t\t),\n\t\t) );\n\n\t\t$logs = explode( ' - ', file_get_contents( llms_get_log_path( 'cash-now' ) ) );\n\n\t\t$this->assertTrue( date_create( $logs[0] ) instanceof DateTime );\n\t\t$this->assertEquals( 'Array\n(\n    [headers] => Array\n        (\n            [Authorization] => Basic F@************Y!:password\n        )\n\n)', trim( $logs[1] ) );\n\n\t\tremove_filter( 'llms_get_gateway_settings_fields', array( $this, 'add_admin_settings' ), 10 );\n\n\t}\n\n\t/**\n\t * Test get_supported_features() method, regarding to the `modify_recurring_payments` feature.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_supported_features_modify_recurring_payments() {\n\n\t\t$original_supports = $this->main->supports;\n\n\t\t/**\n\t\t * By default the mock gateway doesn't specify to NOT support 'recurring_payments',\n\t\t * So it will inherit the default `false` value.\n\t\t * It doesn't specify whether or not it supports 'modify_recurring_payments', then\n\t\t * this feature will follow the `recurring_payments` one (true or false)\n\t\t */\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'checkout_fields'           => false,\n\t\t\t\t'cc_save'                   => false,\n\t\t\t\t'refunds'                   => false,\n\t\t\t\t'single_payments'           => false,\n\t\t\t\t'recurring_payments'        => false,\n\t\t\t\t'recurring_retry'           => false,\n\t\t\t\t'test_mode'                 => false,\n\t\t\t\t'modify_recurring_payments' => false,\n\t\t\t),\n\t\t\t$this->main->get_supported_features()\n\t\t);\n\n\t\t// Turn the `recurring_payments` feature to `true`, reset `modify_recurring_payments`, it will follow.\n\t\t$this->main->supports = array_merge(\n\t\t\t$this->main->supports,\n\t\t\tarray(\n\t\t\t\t'recurring_payments'        => true,\n\t\t\t\t'modify_recurring_payments' => null,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'checkout_fields'           => false,\n\t\t\t\t'cc_save'                   => false,\n\t\t\t\t'refunds'                   => false,\n\t\t\t\t'single_payments'           => false,\n\t\t\t\t'recurring_payments'        => true,\n\t\t\t\t'recurring_retry'           => false,\n\t\t\t\t'test_mode'                 => false,\n\t\t\t\t'modify_recurring_payments' => true,\n\t\t\t),\n\t\t\t$this->main->get_supported_features()\n\t\t);\n\n\t\tunset( $this->main->supports['modify_recurring_payments'] );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'checkout_fields'           => false,\n\t\t\t\t'cc_save'                   => false,\n\t\t\t\t'refunds'                   => false,\n\t\t\t\t'single_payments'           => false,\n\t\t\t\t'recurring_payments'        => true,\n\t\t\t\t'recurring_retry'           => false,\n\t\t\t\t'test_mode'                 => false,\n\t\t\t\t'modify_recurring_payments' => true,\n\t\t\t),\n\t\t\t$this->main->get_supported_features()\n\t\t);\n\n\t\t// Turn the  `modify_recurring_payments` feature to `false`.\n\t\t$this->main->supports['modify_recurring_payments'] = false;\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'checkout_fields'           => false,\n\t\t\t\t'cc_save'                   => false,\n\t\t\t\t'refunds'                   => false,\n\t\t\t\t'single_payments'           => false,\n\t\t\t\t'recurring_payments'        => true,\n\t\t\t\t'recurring_retry'           => false,\n\t\t\t\t'test_mode'                 => false,\n\t\t\t\t'modify_recurring_payments' => false,\n\t\t\t),\n\t\t\t$this->main->get_supported_features()\n\t\t);\n\n\t\t// Turn the  `recurring_payments` feature to `false`, `modify_recurring_payments` is not going to follow.\n\t\t$this->main->supports = array_merge(\n\t\t\t$this->main->supports,\n\t\t\tarray(\n\t\t\t\t'recurring_payments'        => false,\n\t\t\t\t'modify_recurring_payments' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'checkout_fields'           => false,\n\t\t\t\t'cc_save'                   => false,\n\t\t\t\t'refunds'                   => false,\n\t\t\t\t'single_payments'           => false,\n\t\t\t\t'recurring_payments'        => false,\n\t\t\t\t'recurring_retry'           => false,\n\t\t\t\t'test_mode'                 => false,\n\t\t\t\t'modify_recurring_payments' => true,\n\t\t\t),\n\t\t\t$this->main->get_supported_features()\n\t\t);\n\n\t\t$this->main->supports['modify_recurring_payments'] = false;\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'checkout_fields'           => false,\n\t\t\t\t'cc_save'                   => false,\n\t\t\t\t'refunds'                   => false,\n\t\t\t\t'single_payments'           => false,\n\t\t\t\t'recurring_payments'        => false,\n\t\t\t\t'recurring_retry'           => false,\n\t\t\t\t'test_mode'                 => false,\n\t\t\t\t'modify_recurring_payments' => false,\n\t\t\t),\n\t\t\t$this->main->get_supported_features()\n\t\t);\n\n\t\t$this->main->supports = $original_supports;\n\n\t}\n\n\t/**\n\t * Test get_complete_transaction_redirect_url() method.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_complete_transaction_redirect_url() {\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$this->assertEquals(\n\t\t\t'?order-complete=' . $order->get( 'order_key' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'get_complete_transaction_redirect_url', array( $order ) )\n\t\t);\n\n\t\t// Force INPUT_GET redirect.\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'redirect' => 'https://example-redirect-get.com',\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals(\n\t\t\t'https://example-redirect-get.com?order-complete=' . $order->get( 'order_key' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'get_complete_transaction_redirect_url', array( $order ) )\n\t\t);\n\n\t\t// Force INPUT_POST redirect, the INPUT_GET will win.\n\t\t$this->mockPostRequest(\n\t\t\tarray(\n\t\t\t\t'redirect' => 'https://example-redirect-post.com',\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals(\n\t\t\t'https://example-redirect-get.com?order-complete=' . $order->get( 'order_key' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'get_complete_transaction_redirect_url', array( $order ) )\n\t\t);\n\n\t\t// Reset INPUT_GET, INPUT_POST will win.\n\t\t$this->mockGetRequest( array() );\n\t\t$this->assertEquals(\n\t\t\t'https://example-redirect-post.com?order-complete=' . $order->get( 'order_key' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'get_complete_transaction_redirect_url', array( $order ) )\n\t\t);\n\n\t\t// Free enroll and no user logged in, INPUT_POST will win.\n\t\t$this->mockPostRequest(\n\t\t\tarray(\n\t\t\t\t'redirect'               => 'https://example-redirect-post.com',\n\t\t\t\t'form'                   => 'free_enroll',\n\t\t\t\t'free_checkout_redirect' => 'https://free-checkout-redirect.com',\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals(\n\t\t\t'https://example-redirect-post.com?order-complete=' . $order->get( 'order_key' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'get_complete_transaction_redirect_url', array( $order ) )\n\t\t);\n\n\t\t// Free enroll user logged in, INPUT_POST will win.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'student' ) ) );\n\t\t$this->mockPostRequest(\n\t\t\tarray(\n\t\t\t\t'redirect'               => 'https://example-redirect-post.com',\n\t\t\t\t'form'                   => 'free_enroll',\n\t\t\t\t'free_checkout_redirect' => 'https://free-checkout-redirect.com',\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals(\n\t\t\t'https://free-checkout-redirect.com',\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'get_complete_transaction_redirect_url', array( $order ) )\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-post-model.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Post_Model abstract\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group post_model_abstract\n * @group post_models\n *\n * @since 4.10.0\n * @since 6.5.0 Added various tests on set()/set_bulk() methods.\n */\nclass LLMS_Test_Abstract_Post_Model extends LLMS_UnitTestCase {\n\n\tprivate $post_type = 'mock_post_type';\n\n\t/**\n\t * @since 4.10.0\n\t * @var LLMS_Post_Model\n\t */\n\tprotected $stub;\n\n\t/**\n\t * Setup before class.\n\t *\n\t * @since 4.10.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tregister_post_type( 'mock_post_type' );\n\n\t}\n\n\t/**\n\t * Tear down after class.\n\t *\n\t * @since 4.10.0\n\t * @since 5.3.3 Renamed from `tearDownAfterClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function tear_down_after_class() {\n\n\t\tparent::tear_down_after_class();\n\t\tunregister_post_type( 'mock_post_type' );\n\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.10.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->stub = $this->get_stub();\n\n\t}\n\n\t/**\n\t * Retrieve the abstract class mock stub\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return LLMS_Post_Model\n\t */\n\tprivate function get_stub() {\n\n\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => $this->post_type ) );\n\t\t$stub = $this->getMockForAbstractClass( 'LLMS_Post_Model', array( $post ) );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'db_post_type', $this->post_type );\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'model_post_type', $this->post_type );\n\n\t\treturn $stub;\n\n\t}\n\n\t/**\n\t * Test get() to ensure properties that should not be scrubbed are not scrubbed.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_skipped_no_scrub_properties() {\n\n\t\t$tests = array(\n\t\t\t'content' => \"<p>has html</p>\\n\",\n\t\t\t'name'    => 'اسم-آخر', // See https://github.com/gocodebox/lifterlms/pull/1408.\n\t\t);\n\n\t\t// Filters should\n\t\tforeach ( $tests as $key => $val ) {\n\n\t\t\t$this->stub->set( $key, $val );\n\n\t\t\t// The scrub filter should not run when getting the value.\n\t\t\t$actions = did_action( \"llms_scrub_{$this->post_type}_field_{$key}\" );\n\n\t\t\t// Characters should not be scrubbed.\n\t\t\t$this->assertEquals( 'name' === $key ? utf8_uri_encode( $val ) : $val, $this->stub->get( $key ) );\n\n\t\t\t$this->assertSame( $actions, did_action( \"llms_scrub_{$this->post_type}_field_{$key}\" ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test scrub_field().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_scrub_field() {\n\n\t\t$types = array(\n\t\t\t'absint' => array(\n\t\t\t\tarray( 1, 1 ),\n\t\t\t\tarray( 0, 0 ),\n\t\t\t\tarray( -1, 1 ),\n\t\t\t\tarray( 1.5, 1 ),\n\t\t\t\tarray( 2910, 2910 ),\n\t\t\t\tarray( '932', 932 ),\n\t\t\t\tarray( '34920.23', 34920 ),\n\t\t\t\tarray( 'string', 0 ),\n\t\t\t\tarray( '', 0 ),\n\t\t\t\tarray( null, 0 ),\n\t\t\t),\n\t\t\t'array' => array(\n\t\t\t\tarray( '', array() ),\n\t\t\t\tarray( 1, array( 1 ) ),\n\t\t\t\tarray( array( 1, 2, 3 ), array( 1, 2, 3 ) ),\n\t\t\t\tarray( array( 'test' ), array( 'test' ) ),\n\t\t\t),\n\t\t\t'boolean' => array(\n\t\t\t\tarray( true, true ),\n\t\t\t\tarray( false, false ),\n\t\t\t\tarray( 1, true ),\n\t\t\t\tarray( 0, false ),\n\t\t\t\tarray( null, false ),\n\t\t\t),\n\t\t\t'float' => array(\n\t\t\t\tarray( 1.0, 1.0 ),\n\t\t\t\tarray( 1, 1.0 ),\n\t\t\t\tarray( 0.234, 0.234 ),\n\t\t\t\tarray( 0, 0.0 ),\n\t\t\t\tarray( '2.230', 2.23 ),\n\t\t\t\tarray( null, 0.0 ),\n\t\t\t),\n\t\t\t'int' => array(\n\t\t\t\tarray( 1, 1 ),\n\t\t\t\tarray( 0, 0 ),\n\t\t\t\tarray( -1, -1 ),\n\t\t\t\tarray( 1.5, 1 ),\n\t\t\t\tarray( 2910, 2910 ),\n\t\t\t\tarray( '-932', -932 ),\n\t\t\t\tarray( '34920.23', 34920 ),\n\t\t\t\tarray( 'string', 0 ),\n\t\t\t\tarray( '', 0 ),\n\t\t\t\tarray( null, 0 ),\n\t\t\t),\n\t\t\t'yesno' => array(\n\t\t\t\tarray( 'yes', 'yes' ),\n\t\t\t\tarray( 'no', 'no' ),\n\t\t\t\tarray( 0, 'no' ),\n\t\t\t\tarray( 999, 'no' ),\n\t\t\t\tarray( false, 'no' ),\n\t\t\t\tarray( true, 'no' ),\n\t\t\t\tarray( null, 'no' ),\n\t\t\t),\n\t\t\t'text' => array(\n\t\t\t\tarray( 'yes', 'yes' ),\n\t\t\t\tarray( 'a text string.', 'a text string.' ),\n\t\t\t\tarray( 'no <b>tags</b>', 'no tags' ),\n\t\t\t\tarray( '', '' ),\n\t\t\t\tarray( null, '' ),\n\t\t\t),\n\t\t\t'html' => array(\n\t\t\t\tarray( 'yes', 'yes' ),\n\t\t\t\tarray( 'a text string.', 'a text string.' ),\n\t\t\t\tarray( 'Tags <b>are (mostly) okay</b>.', 'Tags <b>are (mostly) okay</b>.' ),\n\t\t\t\tarray( '', '' ),\n\t\t\t\tarray( null, '' ),\n\t\t\t),\n\t\t);\n\n\t\t$types['bool'] = $types['boolean'];\n\t\t$types['string'] = $types['text'];\n\n\t\tforeach ( $types as $type => $tests ) {\n\n\t\t\tforeach ( $tests as $test ) {\n\n\t\t\t\tlist( $input, $expected ) = $test;\n\t\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $this->stub, 'scrub_field', array( $input, $type ) ) );\n\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test (bulk) setting meta with the same values as the stored ones, default behavior: not allowed.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_meta_same_value_unallowed() {\n\n\t\t$meta = $this->_stage_meta_test();\n\n\t\t// Set all the meta except the last one.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_column(\n\t\t\t\tarray_slice( $meta, 0, count( $meta ) - 1, true ),\n\t\t\t\t'value',\n\t\t\t\t'key'\n\t\t\t),\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertTrue( $result );\n\n\t\t// Update with the same values, plus a new one (the latest).\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_column(\n\t\t\t\t$meta,\n\t\t\t\t'value',\n\t\t\t\t'key'\n\t\t\t),\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'invalid_meta', $result );\n\n\t\tforeach ( $result->get_error_messages( 'invalid_meta' ) as $i => $message ) {\n\t\t\t$this->assertEquals(\n\t\t\t\tsprintf( 'Cannot insert/update the meta_%1$s meta', $i + 1 ),\n\t\t\t\t$message,\n\t\t\t\t$message\n\t\t\t);\n\t\t}\n\n\t\t// Last meta updated.\n\t\t$this->assertEquals( end( $meta )['value'], $this->stub->get( end( $meta )['key'] ) );\n\n\t\t$this->_unstage_meta_test();\n\n\t}\n\n\t/**\n\t * Test setting meta with the same values as the stored ones, default behavior: not allowed.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_meta_same_value_unallowed() {\n\n\t\t$meta = $this->_stage_meta_test();\n\n\t\t// Set all the meta except the last one.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_column(\n\t\t\t\tarray_slice( $meta, 0, count( $meta ) - 1, true ),\n\t\t\t\t'value',\n\t\t\t\t'key'\n\t\t\t),\n\t\t);\n\n\t\t$this->assertTrue( $result );\n\n\t\t// Update with the same values, plus a new one (the latest).\n\t\tforeach ( $meta as $i => $mdata ) {\n\t\t\tif ( $i + 1 < count( $meta ) ) {\n\t\t\t\t$this->assertFalse(\n\t\t\t\t\t$this->stub->set(\n\t\t\t\t\t\t$mdata['key'],\n\t\t\t\t\t\t$mdata['value']\n\t\t\t\t\t),\n\t\t\t\t\t$mdata['key']\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t$this->assertTrue(\n\t\t\t\t\t$this->stub->set(\n\t\t\t\t\t\t$mdata['key'],\n\t\t\t\t\t\t$mdata['value']\n\t\t\t\t\t),\n\t\t\t\t\t$mdata['key']\n\t\t\t\t);\n\t\t\t}\n\n\t\t}\n\n\t\t// Last meta updated.\n\t\t$this->assertEquals( end( $meta )['value'], $this->stub->get( end( $meta )['key'] ) );\n\n\t\t$this->_unstage_meta_test();\n\n\t}\n\n\t/**\n\t * Test (bulk) setting meta with the same values as the stored ones, allowed.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_meta_same_value_allowed() {\n\n\t\t$meta = $this->_stage_meta_test();\n\n\t\t// Set all the meta.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_column(\n\t\t\t\t$meta,\n\t\t\t\t'value',\n\t\t\t\t'key'\n\t\t\t),\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertTrue( $result );\n\n\t\t// Update with the same value.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_column(\n\t\t\t\t$meta,\n\t\t\t\t'value',\n\t\t\t\t'key'\n\t\t\t),\n\t\t\ttrue,\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertTrue( $result );\n\n\t\t// Meta updated.\n\t\tforeach ( $meta as $m ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t$this->stub->get( $m['key'] ),\n\t\t\t\t$m['value'],\n\t\t\t\t$m['key']\n\t\t\t);\n\t\t}\n\n\t\t// Update meta with different values.\n\t\t$values = array_combine(\n\t\t\tarray_column(\n\t\t\t\t$meta,\n\t\t\t\t'key'\n\t\t\t),\n\t\t\tarray_values(\n\t\t\t\t$this->get_all_types_fields( true )\n\t\t\t)\n\t\t);\n\n\t\t$result = $this->stub->set_bulk(\n\t\t\t$values,\n\t\t\ttrue,\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertTrue( $result );\n\n\t\t// Meta updated.\n\t\tforeach ( $values as $key => $value ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t$this->stub->get( $key ),\n\t\t\t\t$value,\n\t\t\t\t$key\n\t\t\t);\n\t\t}\n\n\t\t$this->_unstage_meta_test();\n\n\t}\n\n\t/**\n\t * Test (bulk) setting meta with the same values as the stored ones, allowed.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_meta_same_value_allowed() {\n\n\t\t$meta = $this->_stage_meta_test();\n\n\t\t// Set all the meta.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_column(\n\t\t\t\t$meta,\n\t\t\t\t'value',\n\t\t\t\t'key'\n\t\t\t),\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertTrue( $result );\n\n\t\t// Update with the same value.\n\t\tforeach ( $meta as $mdata ) {\n\t\t\t$result = $this->stub->set(\n\t\t\t\t$mdata['key'],\n\t\t\t\t$mdata['value'],\n\t\t\t\ttrue\n\t\t\t);\n\n\t\t\t$this->assertTrue( $result, $mdata['key'] );\n\n\t\t\t// Meta updated.\n\t\t\t$this->assertEquals(\n\t\t\t\t$this->stub->get( $mdata['key'] ),\n\t\t\t\t$mdata['value'],\n\t\t\t\t$mdata['key']\n\t\t\t);\n\n\t\t}\n\n\t\t// Update meta with different values.\n\t\t$values = array_combine(\n\t\t\tarray_column(\n\t\t\t\t$meta,\n\t\t\t\t'key'\n\t\t\t),\n\t\t\tarray_values(\n\t\t\t\t$this->get_all_types_fields( true )\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $meta as $mdata ) {\n\t\t\t$result = $this->stub->set(\n\t\t\t\t$mdata['key'],\n\t\t\t\t$mdata['value'],\n\t\t\t\ttrue\n\t\t\t);\n\n\t\t\t$this->assertTrue( $result, $mdata['key'] );\n\n\t\t\t// Meta updated.\n\t\t\t$this->assertEquals(\n\t\t\t\t$this->stub->get( $mdata['key'] ),\n\t\t\t\t$mdata['value'],\n\t\t\t\t$mdata['key']\n\t\t\t);\n\n\t\t}\n\n\t\t$this->_unstage_meta_test();\n\n\t}\n\n\t/**\n\t * Test set_bulk() method passing empty data array.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_empty_data() {\n\n\t\t// Return WP_Error, don't allow same meta value.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray(),\n\t\t\ttrue,\n\t\t\tfalse\n\t\t);\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'empty_data', $result );\n\n\t\t// Return WP_Error, allow same meta value.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray(),\n\t\t\ttrue,\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'empty_data', $result );\n\n\t\t// Don't return WP_Error, don't allow same meta value.\n\t\t$this->assertFalse(\n\t\t\t$this->stub->set_bulk(\n\t\t\t\tarray(),\n\t\t\t\tfalse,\n\t\t\t\ttrue\n\t\t\t)\n\t\t);\n\n\t\t// Don't return WP_Error, allow same meta value.\n\t\t$this->assertFalse(\n\t\t\t$this->stub->set_bulk(\n\t\t\t\tarray(),\n\t\t\t\tfalse,\n\t\t\t\tfalse\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test set_bulk() method passing invalid data.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_invalid_data() {\n\n\t\t// Setting only unsettable properties produces invalid data error.\n\t\t$unsettable_properties = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_unsettable_properties' );\n\n\t\t// Return WP_Error, don't allow same meta value.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_flip( $unsettable_properties ),\n\t\t\ttrue,\n\t\t\tfalse\n\t\t);\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'invalid_data', $result );\n\n\t\t// Return WP_Error, allow same meta value.\n\t\t$result = $this->stub->set_bulk(\n\t\t\tarray_flip( $unsettable_properties ),\n\t\t\ttrue,\n\t\t\tfalse\n\t\t);\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'invalid_data', $result );\n\n\t\t// Don't return WP_Error, don't allow same meta value.\n\t\t$this->assertFalse(\n\t\t\t$this->stub->set_bulk(\n\t\t\t\tarray_flip( $unsettable_properties ),\n\t\t\t\tfalse,\n\t\t\t\tfalse\n\t\t\t)\n\t\t);\n\n\t\t// Don't return WP_Error, allow same meta value.\n\t\t$this->assertFalse(\n\t\t\t$this->stub->set_bulk(\n\t\t\t\tarray_flip( $unsettable_properties ),\n\t\t\t\tfalse,\n\t\t\t\ttrue\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test setting (bulk) post property that would generate an error.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_post_properties_with_error() {\n\n\t\t// Simulate empty content error.\n\t\tadd_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t\t$properties = array(\n\t\t\t'content' => '',\n\t\t\t'title'   => ''\n\t\t);\n\n\t\t// Returning WP_Error, don't allow same meta.\n\t\t$result = $this->stub->set_bulk(\n\t\t\t$properties,\n\t\t\ttrue,\n\t\t\tfalse\n\t\t);\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'empty_content', $result );\n\n\t\t// Returning WP_Error, allow same meta.\n\t\t$result = $this->stub->set_bulk(\n\t\t\t$properties,\n\t\t\ttrue,\n\t\t\ttrue\n\t\t);\n\n\t\t$this->assertWPError( $result );\n\t\t$this->assertWPErrorCodeEquals( 'empty_content', $result );\n\n\t\t// Not returning WP_Error, do not allow same meta.\n\t\t$this->assertFalse(\n\t\t\t$result = $this->stub->set_bulk(\n\t\t\t\t$properties,\n\t\t\t\tfalse,\n\t\t\t\tfalse\n\t\t\t)\n\t\t);\n\n\t\t// Not returning WP_Error, allow same meta.\n\t\t$this->assertFalse(\n\t\t\t$result = $this->stub->set_bulk(\n\t\t\t\t$properties,\n\t\t\t\tfalse,\n\t\t\t\ttrue\n\t\t\t)\n\t\t);\n\n\t\t// Simulate empty content error.\n\t\tremove_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t}\n\n\n\t/**\n\t * Test setting post property that would generate an error.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_post_properties_with_error() {\n\n\t\t// Simulate empty content error.\n\t\tadd_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t\t$properties = array(\n\t\t\t'content' => '',\n\t\t\t'title'   => '',\n\t\t);\n\n\t\tforeach ( $properties as $key => $val ) {\n\n\t\t\t$this->assertFalse(\n\t\t\t\t// Allow same meta.\n\t\t\t\t$this->stub->set(\n\t\t\t\t\t$key,\n\t\t\t\t\t$val,\n\t\t\t\t\ttrue,\n\t\t\t\t),\n\t\t\t\t$key\n\t\t\t);\n\n\t\t\t$this->assertFalse(\n\t\t\t\t// Don't allow same meta.\n\t\t\t\t$this->stub->set(\n\t\t\t\t\t$key,\n\t\t\t\t\t$val,\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t\t$key\n\t\t\t);\n\t\t}\n\n\t\t// Simulate empty content error.\n\t\tremove_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test set_bulk() with post properties.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_post_properties_with_no_errors() {\n\n\t\t// Array of the type 'property' => 'type'.\n\t\t$post_properties = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_post_properties' );\n\t\t$values          = array(\n\t\t\t'author'         => $this->factory->user->create(),\n\t\t\t'content'        => '<div> Some html </div>',\n\t\t\t'date'           => date( 'Y-m-d H:i:s' ),\n\t\t\t'date_gmt'       => gmdate( 'Y-m-d H:i:s' ),\n\t\t\t'excerpt'        => '<div> Some excerpt </div>',\n\t\t\t'password'       => 'pw',\n\t\t\t'parent'         => $this->factory->post->create(),\n\t\t\t'menu_order'     => 1,\n\t\t\t'modified'       => date( 'Y-m-d H:i:s' ),\n\t\t\t'modified_gmt'   => gmdate( 'Y-m-d H:i:s' ),\n\t\t\t'name'           => sanitize_title( 'Some slug' ),\n\t\t\t'status'         => 'publish',\n\t\t\t'title'          => 'Title',\n\t\t\t'type'           => $this->stub->get( 'type' ),\n\t\t\t'comment_status' => 'closed',\n\t\t\t'ping_status'    => 'closed',\n\t\t);\n\n\t\t$post_properties = array_merge(\n\t\t\t$post_properties,\n\t\t\t$values\n\t\t);\n\n\t\t// Allow returning WP Error.\n\t\t$this->assertTrue(\n\t\t\t$this->stub->set_bulk(\n\t\t\t\t$post_properties,\n\t\t\t\ttrue\n\t\t\t)\n\t\t);\n\n\t\t// Don't allow returning WP Error.\n\t\t$this->assertTrue(\n\t\t\t$this->stub->set_bulk(\n\t\t\t\t$post_properties\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Test set_bulk() with post properties.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_post_properties_with_no_errors() {\n\n\t\t// Array of the type 'property' => 'type'.\n\t\t$post_properties = LLMS_Unit_Test_Util::call_method( $this->stub, 'get_post_properties' );\n\t\t$values          = array(\n\t\t\t'author'         => $this->factory->user->create(),\n\t\t\t'content'        => '<div> Some html </div>',\n\t\t\t'date'           => date( 'Y-m-d H:i:s' ),\n\t\t\t'date_gmt'       => gmdate( 'Y-m-d H:i:s' ),\n\t\t\t'excerpt'        => '<div> Some excerpt </div>',\n\t\t\t'password'       => 'pw',\n\t\t\t'parent'         => $this->factory->post->create(),\n\t\t\t'menu_order'     => 1,\n\t\t\t'modified'       => date( 'Y-m-d H:i:s' ),\n\t\t\t'modified_gmt'   => gmdate( 'Y-m-d H:i:s' ),\n\t\t\t'name'           => sanitize_title( 'Some slug' ),\n\t\t\t'status'         => 'publish',\n\t\t\t'title'          => 'Title',\n\t\t\t'type'           => $this->stub->get( 'type' ),\n\t\t\t'comment_status' => 'closed',\n\t\t\t'ping_status'    => 'closed',\n\t\t);\n\n\t\t$post_properties = array_merge(\n\t\t\t$post_properties,\n\t\t\t$values\n\t\t);\n\n\t\tforeach ( $post_properties as $property => $value ) {\n\t\t\t$this->assertTrue(\n\t\t\t\t$this->stub->set(\n\t\t\t\t\t$property,\n\t\t\t\t\t$value\n\t\t\t\t),\n\t\t\t\t$property\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Test `set_bulk()` to ensure single quotes and double quotes are correctly slashed.\n\t *\n\t * @since 5.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_quotes() {\n\n\t\t$content = 'Content with \"Double\" Quotes and \\'Single\\' Quotes';\n\t\t$excerpt = 'Excerpt with \"Double\" Quotes and \\'Single\\' Quotes';\n\t\t$title   = 'Title with \"Double\" Quotes and \\'Single\\' Quotes';\n\n\t\t# Test with KSES filters\n\t\t$this->stub->set_bulk( array(\n\t\t\t'content' => $content,\n\t\t\t'excerpt' => $excerpt,\n\t\t\t'title'   => $title,\n\t\t) );\n\t\t$saved_post = get_post( $this->stub->get( 'id' ) );\n\t\t$this->assertEquals( $content, $saved_post->post_content );\n\t\t$this->assertEquals( $excerpt, $saved_post->post_excerpt );\n\t\t$this->assertEquals( $title, $saved_post->post_title );\n\n\t\t# Test without KSES filters\n\t\tkses_remove_filters();\n\t\t$this->stub->set_bulk( array(\n\t\t\t'content' => $content,\n\t\t\t'excerpt' => $excerpt,\n\t\t\t'title'   => $title,\n\t\t) );\n\t\t$saved_post = get_post( $this->stub->get( 'id' ) );\n\t\t$this->assertEquals( $content, $saved_post->post_content );\n\t\t$this->assertEquals( $excerpt, $saved_post->post_excerpt );\n\t\t$this->assertEquals( $title, $saved_post->post_title );\n\n\t}\n\n\t/**\n\t * Test `set()` to ensure single quotes and double quotes are correctly slashed.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_quotes() {\n\n\t\t$properties = array(\n\t\t\t'content' => 'Content with \"Double\" Quotes and \\'Single\\' Quotes',\n\t\t\t'excerpt' => 'Excerpt with \"Double\" Quotes and \\'Single\\' Quotes',\n\t\t\t'title'   => 'Title with \"Double\" Quotes and \\'Single\\' Quotes',\n\t\t);\n\n\t\t# Test with KSES filters.\n\t\tforeach ( $properties as $key => $value ) {\n\t\t\t$this->stub->set(\n\t\t\t\t$key,\n\t\t\t\t$value\n\t\t\t);\n\n\t\t\t$saved_post = get_post( $this->stub->get( 'id' ) );\n\t\t\t$this->assertEquals( $value, $saved_post->{\"post_{$key}\"}, $key );\n\t\t}\n\n\t\t# Test without KSES filters.\n\t\tkses_remove_filters();\n\t\tforeach ( $properties as $key => $value ) {\n\t\t\t$this->stub->set(\n\t\t\t\t$key,\n\t\t\t\t$value\n\t\t\t);\n\n\t\t\t$saved_post = get_post( $this->stub->get( 'id' ) );\n\t\t\t$this->assertEquals( $value, $saved_post->{\"post_{$key}\"}, $key );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test toArray() method.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_toArray() {\n\n\t\t// Add custom meta data.\n\t\tupdate_post_meta( $this->stub->get( 'id' ), '_custom_meta', 'meta_value' );\n\n\t\t// Generate the array.\n\t\t$array = $this->stub->toArray();\n\n\t\t// Make sure all expected properties are returned.\n\t\t$this->assertEqualSets( array_merge( array_keys( $this->stub->get_properties() ), array( 'custom', 'id' ) ), array_keys( $array ) );\n\n\t\t// Values in the array should match the values retrieved by the object getters.\n\t\tforeach ( $array as $key => $val ) {\n\n\t\t\tif ( 'custom' === $key ) {\n\t\t\t\t$expect = array(\n\t\t\t\t\t'_custom_meta' => array(\n\t\t\t\t\t\t'meta_value',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t} elseif ( in_array( $key, array( 'content', 'excerpt', 'title' ), true ) ) {\n\t\t\t\t$key = \"post_{$key}\";\n\t\t\t\t$expect = $this->stub->post->$key;\n\t\t\t} else {\n\t\t\t\t$expect = $this->stub->get( $key );\n\t\t\t}\n\n\t\t\t$this->assertEquals( $expect, $val, $key );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test toArray() method when the author is expanded.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_toArray_expanded_author() {\n\n\t\t$data = array(\n\t\t\t'role'        => 'editor',\n\t\t\t'first_name'  => 'Jeffrey',\n\t\t\t'last_name'   => 'Lebowski',\n\t\t\t'description' => \"Let me explain something to you. Um, I am not \\\"Mr. Lebowski\\\". You're Mr. Lebowski. I'm the Dude. So that's what you call me.\",\n\t\t);\n\t\t$user = $this->factory->user->create_and_get( $data );\n\t\t$this->stub->set( 'author', $user->ID );\n\n\t\tunset( $data['role'] );\n\t\t$data['id'] = $user->ID;\n\t\t$data['email'] = $user->user_email;\n\n\t\t// Generate the array.\n\t\t$array = $this->stub->toArray();\n\t\t$this->assertEquals( $data, $array['author'] );\n\n\t}\n\n\t/**\n\t * Stages tests on meta fields and returns an array of meta to tests.\n\t *\n\t * @return void\n\t */\n\tprivate function _stage_meta_test() {\n\n\t\t$types = $this->get_all_types_fields();\n\n\t\t$meta = array();\n\t\t$i = 1;\n\n\t\t// Build meta.\n\t\tforeach ( $types as $type => $value ) {\n\t\t\t$meta[] = array(\n\t\t\t\t'key'   => 'meta_' . $i++,\n\t\t\t\t'type'  => $type,\n\t\t\t\t'value' => $value,\n\t\t\t);\n\t\t}\n\n\t\t$declare_property_types = function( $props ) use ( $meta ) {\n\t\t\treturn array_merge(\n\t\t\t\t$props,\n\t\t\t\tarray_column( $meta, 'type', 'key' )\n\t\t\t);\n\t\t};\n\n\t\t$model_post_type = LLMS_Unit_Test_Util::get_private_property_value( $this->stub, 'model_post_type' );\n\t\tadd_filter( \"llms_get_{$model_post_type}_properties\", $declare_property_types );\n\n\t\t$on_unstage = function() use ( $declare_property_types, $model_post_type ) {\n\t\t\tremove_filter( \"llms_get_{$model_post_type}_properties\", $declare_property_types );\n\t\t};\n\n\t\tif ( ! has_action( 'llms_test_meta_test_unstage', $on_unstage ) ) {\n\t\t\tadd_action( 'llms_test_meta_test_unstage', $on_unstage );\n\t\t}\n\n\t\treturn $meta;\n\n\t}\n\n\t/**\n\t * Unstage meta test.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tprivate function _unstage_meta_test() {\n\t\tdo_action( 'llms_test_meta_test_unstage' );\n\t}\n\n\t/**\n\t * All types fields with values.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @param bool $alt Alternative values.\n\t * @return array\n\t */\n\tprivate function get_all_types_fields( $alt = false ) {\n\n\t\t$types = ! $alt\n\t\t\t?\n\t\t\tarray(\n\t\t\t\t'absint'  => 1,\n\t\t\t\t'array'   => array( 1 ),\n\t\t\t\t'boolean' => true,\n\t\t\t\t'float'   => 1.0,\n\t\t\t\t'int'     => -1,\n\t\t\t\t'yesno'   => 'yes',\n\t\t\t\t'text'    => 'a text string.',\n\t\t\t\t'html'    => 'Tags <b>are (mostly) okay</b>.',\n\t\t\t)\n\t\t\t:\n\t\t\tarray(\n\t\t\t\t'absint'  => 2,\n\t\t\t\t'array'   => array( 2 ),\n\t\t\t\t'boolean' => false,\n\t\t\t\t'float'   => 2.0,\n\t\t\t\t'int'     => -2,\n\t\t\t\t'yesno'   => 'no',\n\t\t\t\t'text'    => 'a different text string.',\n\t\t\t\t'html'    => 'Different Tags <b>are (mostly) okay</b>.',\n\t\t\t);\n\n\t\t$types['bool']   = $types['boolean'];\n\t\t$types['string'] = $types['text'];\n\n\t\treturn $types;\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-posts-query.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Posts_Query class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group query\n * @group abstract_posts_query\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Abstract_Posts_Query extends LLMS_UnitTestCase {\n\n\t/**\n\t * Set up the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = $this->get_stub();\n\n\t}\n\n\t/**\n\t * Retrieve a mocked abstract.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return LLMS_Abstract_Posts_Query\n\t */\n\tprivate function get_stub() {\n\n\t\t$stub = $this->getMockForAbstractClass( 'LLMS_Abstract_Posts_Query' );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'id', 'mock' );\n\n\t\treturn $stub;\n\n\t}\n\n\t/**\n\t * Test count_results(), get_number_results(), get_found_results(), get_max_pages(), and has_results().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_count_results() {\n\n\t\t// No results.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'wp_query', (object) array(\n\t\t\t'post_count'    => 0,\n\t\t\t'max_num_pages' => 0,\n\t\t\t'found_posts'   => 0,\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'count_results' );\n\n\t\t$this->assertEquals( 0, $this->main->get_number_results() );\n\t\t$this->assertEquals( 0, $this->main->get_found_results() );\n\t\t$this->assertEquals( 0, $this->main->get_max_pages() );\n\t\t$this->assertFalse( $this->main->has_results() );\n\n\t\t// 52 Results.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'wp_query', (object) array(\n\t\t\t'post_count'    => 10,\n\t\t\t'max_num_pages' => 3,\n\t\t\t'found_posts'   => 25,\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'count_results' );\n\n\t\t$this->assertEquals( 10, $this->main->get_number_results() );\n\t\t$this->assertEquals( 25, $this->main->get_found_results() );\n\t\t$this->assertEquals( 3, $this->main->get_max_pages() );\n\t\t$this->assertTrue( $this->main->has_results() );\n\n\t}\n\n\t/**\n\t * Test default_arguments()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_default_arguments() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'allowed_post_types', array( 'post' ) );\n\n\t\t$defaults = LLMS_Unit_Test_Util::call_method( $this->main, 'default_arguments' );\n\n\t\t$this->assertEquals( 1, $defaults['page'] );\n\t\t$this->assertEquals( 10, $defaults['per_page'] );\n\t\t$this->assertEquals( 'all', $defaults['fields'] );\n\t\t$this->assertEquals( 'publish', $defaults['status'] );\n\t\t$this->assertEquals( array( 'post' ), $defaults['post_types'] );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'date' => 'DESC',\n\t\t\t\t'ID'   => 'DESC',\n\t\t\t),\n\t\t\t$defaults['sort']\n\t\t);\n\n\t}\n\n\t/**\n\t * Test prepare_query()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_query() {\n\n\t\t$defaults = LLMS_Unit_Test_Util::call_method( $this->main, 'default_arguments' );\n\t\t$query    = $this->main->get_query();\n\n\t\t$this->assertEquals( $defaults['page'], $query['paged'] );\n\t\t$this->assertEquals( $defaults['per_page'], $query['posts_per_page'] );\n\t\t$this->assertEquals( $defaults['post_types'], $query['post_type'] );\n\t\t$this->assertEquals( $defaults['search'], $query['s'] );\n\t\t$this->assertEquals( $defaults['sort'], $query['orderby'] );\n\t\t$this->assertEquals( $defaults['status'], $query['post_status'] );\n\n\t}\n\n\t/**\n\t * Test get() and set() and additionally test sanitize_post_types().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_set() {\n\n\t\t// Make sure post type sanitization works.\n\t\t$post_types = array( 'post' );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'allowed_post_types', $post_types );\n\n\t\t$this->main->set( 'post_types', array( 'fake', 'post' ) );\n\t\t$this->assertEquals( $post_types, $this->main->get( 'post_types' ) );\n\n\t\t$this->main->set( 'post_types', 'fake' );\n\t\t$this->assertEquals( array(), $this->main->get( 'post_types' ) );\n\n\t\t// Test something else/\n\t\t$this->main->set( 'status', 'draft' );\n\t\t$this->assertEquals( 'draft', $this->main->get( 'status' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-query.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Query class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group query\n * @group abstract_query\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Abstract_Query extends LLMS_UnitTestCase {\n\n\t/**\n\t * Set up the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = $this->get_stub();\n\n\t}\n\n\t/**\n\t * Retrieve a mocked abstract.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return LLMS_Abstract_Query\n\t */\n\tprivate function get_stub() {\n\n\t\t$stub = $this->getMockForAbstractClass( 'LLMS_Abstract_Query' );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $stub, 'id', 'mock' );\n\n\t\treturn $stub;\n\n\t}\n\n\t/**\n\t * Test count_results(), get_number_results(), get_found_results(), get_max_pages(), and has_results().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_count_results() {\n\n\t\t// No results.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'results', array() );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'count_results' );\n\n\t\t$this->assertEquals( 0, $this->main->get_number_results() );\n\t\t$this->assertEquals( 0, $this->main->get_found_results() );\n\t\t$this->assertEquals( 0, $this->main->get_max_pages() );\n\t\t$this->assertFalse( $this->main->has_results() );\n\n\t\t// 52 Results.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'results', array_fill( 0, 10, 'a' ) );\n\t\t$this->main->method( 'found_results' )->will( $this->returnValue( 52 ) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'count_results' );\n\n\t\t$this->assertEquals( 10, $this->main->get_number_results() );\n\t\t$this->assertEquals( 52, $this->main->get_found_results() );\n\t\t$this->assertEquals( 6, $this->main->get_max_pages() );\n\t\t$this->assertTrue( $this->main->has_results() );\n\n\t}\n\n\t/**\n\t * Test get() and set()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_and_set() {\n\n\t\t// Default values for unset vals.\n\t\t$this->assertEquals( '', $this->main->get( 'fake', '' ) );\n\t\t$this->assertEquals( 'default', $this->main->get( 'fake', 'default' ) );\n\n\t\t// Set val.\n\t\t$this->main->set( 'fake', 'abc' );\n\t\t$this->assertEquals( 'abc', $this->main->get( 'fake', 'default' ) );\n\n\t\t// We can set falsies.\n\t\t$this->main->set( 'fake', false );\n\t\t$this->assertFalse( $this->main->get( 'fake', 'default' ) );\n\n\t}\n\n\t/**\n\t * Test get_allowed_sort_fields()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_allowed_sort_fields() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'allowed_sort_fields', array( 'id' ) );\n\n\t\t$handler = function( $fields ) {\n\t\t\t$fields[] = 'added';\n\t\t\treturn $fields;\n\t\t};\n\t\tadd_filter( 'llms_mock_query_allowed_sort_fields', $handler );\n\n\t\t// Filtered field is added.\n\t\t$this->assertEquals( array( 'id', 'added' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_allowed_sort_fields' ) );\n\n\t\t// Filter is suppressed.\n\t\t$this->main->set( 'suppress_filters', true );\n\t\t$this->assertEquals( array( 'id' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_allowed_sort_fields' ) );\n\n\t\tremove_filter( 'llms_mock_query_allowed_sort_fields', $handler );\n\n\t}\n\n\t/**\n\t * Test get_default_args()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_default_args() {\n\n\t\t$defaults = LLMS_Unit_Test_Util::call_method( $this->main, 'default_arguments' );\n\n\t\t$handler = function( $val ) {\n\t\t\t$val['added'] = 1;\n\t\t\treturn $val;\n\t\t};\n\t\tadd_filter( 'llms_mock_query_get_default_args', $handler );\n\n\t\t// Filtered field is added.\n\t\t$this->assertEquals( array_merge( $defaults, array( 'added' => 1 ) ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_default_args' ) );\n\n\t\t// Filter is suppressed.\n\t\t$this->main->set( 'suppress_filters', true );\n\t\t$this->assertEquals( $defaults, LLMS_Unit_Test_Util::call_method( $this->main, 'get_default_args' ) );\n\n\t\tremove_filter( 'llms_mock_query_get_default_args', $handler );\n\n\t}\n\n\t/**\n\t * Test get_results()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_results() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'results', array( 1 ) );\n\n\t\t$handler = function( $fields ) {\n\t\t\treturn array( 1, 2, 3 );\n\t\t};\n\t\tadd_filter( 'llms_mock_query_get_results', $handler );\n\n\t\t// Filtered field is added.\n\t\t$this->assertEquals( array( 1, 2, 3 ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_results' ) );\n\n\t\t// Filter is suppressed.\n\t\t$this->main->set( 'suppress_filters', true );\n\t\t$this->assertEquals( array( 1 ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_results' ) );\n\n\t\tremove_filter( 'llms_mock_query_get_results', $handler );\n\n\t}\n\n\t/**\n\t * Test is_first_page()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_first_page() {\n\n\t\t$this->assertTrue( $this->main->is_first_page() );\n\n\t\t$this->main->set( 'page', 2 );\n\t\t$this->assertFalse( $this->main->is_first_page() );\n\n\t}\n\n\t/**\n\t * Test is_last_page()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_last_page() {\n\n\t\t$this->assertTrue( $this->main->is_last_page() );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'number_results', 1 );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'max_pages', 1 );\n\t\t$this->assertTrue( $this->main->is_last_page() );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'number_results', 1 );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'max_pages', 2 );\n\t\t$this->assertFalse( $this->main->is_last_page() );\n\n\t\t$this->main->set( 'page', 2 );\n\t\t$this->assertTrue( $this->main->is_last_page() );\n\n\t}\n\n\t/**\n\t * Test sanitize_id_array()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_id_array() {\n\n\t\t// Test arrays: $0 = input, $1 = expected output.\n\t\t$tests = array(\n\t\t\t// Empty values.\n\t\t\tarray( 0, array() ),\n\t\t\tarray( false, array() ),\n\t\t\tarray( array(), array() ),\n\t\t\t// Numeric input.\n\t\t\tarray( 1, array( 1 ) ),\n\t\t\tarray( \"1\", array( 1 ) ),\n\t\t\tarray( -1, array( 1 ) ),\n\t\t\tarray( \"-1\", array( 1 ) ),\n\t\t\tarray( 20190, array( 20190 ) ),\n\t\t\tarray( \"923\", array( 923 ) ),\n\t\t\t// Arrays of numbers\n\t\t\tarray( array( 1, 2, \"5\" ), array( 1, 2, 5 ) ),\n\t\t\tarray( array( \"2342\", 999009, \"1\" ), array( 2342, 999009, 1 ) ),\n\t\t\t// Non numeric data.\n\t\t\tarray( \"abc\", array() ),\n\t\t\tarray( array( \"abc\" ), array() ),\n\t\t\t// Mixed data.\n\t\t\tarray( array( 0, \"abc\", 1, \"202\", \"\", false, 5 ), array( 1, 202, 5 ) ),\n\t\t\t// Comma strings get weird.\n\t\t\tarray( array( '1,2,3' ), array( 1 ) ),\n\t\t\tarray( array( 'abc,1' ), array() ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\t\t\tlist( $input, $expected ) = $test;\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $this->main, 'sanitize_id_array', array( $input ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test sanitize_sort()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_sort() {\n\n\t\t// No `$allowed_sort` defined for the query.\n\t\t$this->assertEquals( array( 'whatever' => 'fake' ), LLMS_Unit_Test_Util::call_method( $this->main, 'sanitize_sort', array( array( 'whatever' => 'fake' ) ) ) );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'allowed_sort_fields', array( 'whatever', 'fake' ) );\n\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\tarray( 'whatever' => 'fake' ),\n\t\t\t\tarray(),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'whatever' => 'ASC', 'fake' => 'fake' ),\n\t\t\t\tarray( 'whatever' => 'ASC' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'whatever' => 'ASC', 'fake' => 'DESC' ),\n\t\t\t\tarray( 'whatever' => 'ASC', 'fake' => 'DESC' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'fake' => 'ASC' ),\n\t\t\t\tarray( 'fake' => 'ASC' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'id' => 'ASC', 'fake' => 'DESC' ),\n\t\t\t\tarray( 'fake' => 'DESC' ),\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\t\t\tlist( $input, $expected ) = $test;\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $this->main, 'sanitize_sort', array( $input ) ) );\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-data.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Session_Data class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group sessions\n * @group session_data\n *\n * @since 4.0.0\n */\nclass LLMS_Test_Abstract_Session_Data extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 4.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = $this->getMockForAbstractClass( 'LLMS_Abstract_Session_Data' );\n\n\t}\n\n\t/**\n\t * Test get, set, and magic methods.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_set_isset_unset() {\n\n\t\t$vals = array(\n\t\t\t1, true, 'yes', 'true', 'on',\n\t\t\tfalse, 0, 'no', 'off',\n\t\t\tarray(), array( 'yes' ), array( 'yes' => 'okay' ),\n\t\t\t1234.56, '1234.56',\n\t\t\t25, '20389'\n\t\t);\n\n\t\tforeach ( $vals as $val ) {\n\n\t\t\t$key = sprintf( '%s_%s', uniqid(), microtime() );\n\n\t\t\t// Var not set.\n\t\t\t$this->assertFalse( isset( $this->main->$key ) );\n\n\t\t\t// Default value get when var is not set.\n\t\t\t$this->assertEquals( $val, $this->main->get( $key, $val ) );\n\n\t\t\t// Set.\n\t\t\t$this->assertEquals( $val, $this->main->set( $key, $val ) );\n\t\t\t$this->assertFalse( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) );\n\n\t\t\t// Var is set.\n\t\t\t$this->assertTrue( isset( $this->main->$key ) );\n\n\t\t\t// Reset.\n\t\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true );\n\t\t\tunset( $this->main->$key );\n\n\t\t\t// Magic set.\n\t\t\t$this->assertEquals( $val, $this->main->set( $key, $val ) );\n\t\t\t$this->assertFalse( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) );\n\n\t\t\t// Var is set.\n\t\t\t$this->assertTrue( isset( $this->main->$key ) );\n\n\t\t\t// Get.\n\t\t\t$this->assertEquals( $val, $this->main->get( $key ) );\n\n\t\t\t// Magic Get.\n\t\t\t$this->assertEquals( $val, $this->main->$key );\n\n\t\t\t// Reset.\n\t\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true );\n\n\t\t\t// Unset.\n\t\t\tunset( $this->main->$key );\n\t\t\t$this->assertFalse( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) );\n\n\t\t\t// Gone, should return the default value.\n\t\t\t$this->assertEquals( 'deleted', $this->main->get( $key, 'deleted' ) );\n\n\t\t}\n\n\t}\n\n\t// public function\n\n\t/**\n\t * Test get_id()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_id() {\n\n\t\t// Already set.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'id', 'fakeid' );\n\t\t$this->assertEquals( 'fakeid', $this->main->get_id() );\n\n\t\t// Generate a new id.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'id', '' );\n\t\t$id = $this->main->get_id();\n\t\t$this->assertTrue( is_string( $this->main->get_id() ) );\n\t\t$this->assertEquals( 32, strlen( $this->main->get_id() ) );\n\n\t}\n\n\t/**\n\t * Test get_id() for logged in users.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_id_logged_in() {\n\n\t\t$uid = $this->factory->user->create();\n\t\twp_set_current_user( $uid );\n\n\t\t$this->assertEquals( $uid, $this->main->get_id() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/abstracts/class-llms-test-abstract-session-database-handler.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Abstract_Session_Database_Handler class\n *\n * @package LifterLMS/Tests/Abstracts\n *\n * @group abstracts\n * @group sessions\n * @group session_database_handler\n *\n * @since 4.0.0\n */\nclass LLMS_Test_Abstract_Session_Database_Handler extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 4.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_sessions\" );\n\n\t\t$this->main = $this->getMockForAbstractClass( 'LLMS_Abstract_Session_Database_Handler' );\n\n\t}\n\n\t/**\n\t * Test clean() when deleting only expired sessions.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_clean_expired_only() {\n\n\t\t$prefix = LLMS_Cache_Helper::get_prefix( 'llms_session_id' );\n\n\t\t$active  = $this->create_mock_session_data( 2 );\n\t\t$expired = $this->create_mock_session_data( 2, true );\n\n\t\t// Return 2 deletions.\n\t\t$this->assertEquals( 2, $this->main->clean() );\n\n\t\t// Active sessions were not removed.\n\t\tglobal $wpdb;\n\t\t$remaining = array_map( 'absint', $wpdb->get_col( \"SELECT id FROM {$wpdb->prefix}lifterlms_sessions\" ) );\n\t\t$this->assertEqualSets( $active, $remaining );\n\n\t\t// New prefix because the old one is invalidated.\n\t\t$this->assertNotEquals( $prefix, LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) );\n\n\t}\n\n\t/**\n\t * Test clean() when deleting all sessions.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_clean_all() {\n\n\t\t$active  = $this->create_mock_session_data( 2 );\n\t\t$expired = $this->create_mock_session_data( 2, true );\n\n\t\t// Return 4 deletions.\n\t\t$this->assertEquals( 4, $this->main->clean( false ) );\n\n\t\t// No sessions remain.\n\t\tglobal $wpdb;\n\t\t$remaining = $wpdb->get_col( \"SELECT id FROM {$wpdb->prefix}lifterlms_sessions\" );\n\t\t$this->assertEquals( array(), $remaining );\n\n\t}\n\n\t/**\n\t * Test delete()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete() {\n\n\t\t$id = $this->create_mock_session_data( 1 )[0];\n\n\t\tglobal $wpdb;\n\t\t$session_id = $wpdb->get_col( \"SELECT session_key FROM {$wpdb->prefix}lifterlms_sessions WHERE id = {$id};\" )[0];\n\n\t\t// Mock cached data data.\n\t\twp_cache_set( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . $session_id, 'mock_data', 'llms_session_id' );\n\n\t\t$this->assertTrue( $this->main->delete( $session_id ) );\n\n\t\t$this->assertFalse( wp_cache_get( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . $session_id, 'llms_session_id' ) );\n\n\n\n\t}\n\n\t/**\n\t * Test save() when there's not data to be saved\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_is_clean() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true );\n\t\t$this->assertFalse( $this->main->save( time() + HOUR_IN_SECONDS ) );\n\n\t}\n\n\t/**\n\t * Test save()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save() {\n\n\t\t$this->main->set( 'item', 'yes' );\n\n\t\t// Saved to DB.\n\t\t$this->assertTrue( $this->main->save( time() + HOUR_IN_SECONDS ) );\n\n\t\t// Cache set.\n\t\t$data = wp_cache_get( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . $this->main->get_id(), 'llms_session_id' );\n\t\t$this->assertEquals( array( 'item' => 'yes' ), $data );\n\n\t}\n\n\t/**\n\t * Test read() when there's no saved data so it returns a default value\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_read_default() {\n\n\t\t$this->assertEquals( 'defaultvalue', $this->main->read( 'fake', 'defaultvalue' ) );\n\n\t}\n\n\t/**\n\t * Test read() when there's a cache hit\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_read_cache_hit() {\n\n\t\twp_cache_set( LLMS_Cache_Helper::get_prefix( 'llms_session_id' ) . 'fake_session', 'mock_data', 'llms_session_id' );\n\t\t$this->assertEquals( 'mock_data', $this->main->read( 'fake_session' ) );\n\n\t}\n\n\n\t/**\n\t * Test read() when there's a cache miss\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_read_cache_miss() {\n\n\t\t$this->main->set( 'something', 'is_set' );\n\t\t$this->main->save( time() + HOUR_IN_SECONDS );\n\n\t\tLLMS_Cache_Helper::invalidate_group( 'llms_session_id' );\n\n\t\t$this->assertEquals( array( 'something' => 'is_set' ), $this->main->read( $this->main->get_id() ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-assets.php",
    "content": "<?php\n/**\n * Test Admin Assets Class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group admin_assets\n * @group assets\n *\n * @since 4.3.3\n * @since 7.1.0 Turn `test_maybe_enqueue_reporting_general_settings_assumed()` into ` test_maybe_enqueue_reporting_dashboard_assumed()`.\n *              Removed outdated `test_maybe_enqueue_reporting_general_settings_explicit()`.\n */\nclass LLMS_Test_Admin_Assets extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.3.3\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Assets();\n\n\t}\n\n\t/**\n\t * Tear down test case\n\t *\n\t * Dequeue & Deregister all assets that may have been enqueued during tests.\n\t *\n\t * @since 4.3.3\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\n\t\t/**\n\t\t * List of asset handles that may have been enqueued or registered during the test\n\t\t *\n\t\t * We do not care if they actually were registered or enqueued, we'll remove them\n\t\t * anyway since the functions will fail silently for assets that were not\n\t\t * previously enqueued or registered.\n\t\t */\n\t\t$handles = array(\n\t\t\t'llms-google-charts',\n\t\t\t'llms-analytics'\n\t\t);\n\n\t\tforeach ( $handles as $handle ) {\n\t\t\twp_dequeue_script( $handle );\n\t\t\twp_deregister_script( $handle );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test block_editor_assets()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_editor_assets_for_certificates() {\n\n\t\tif ( ! llms_is_block_editor_supported_for_certificates() ) {\n\t\t\t$this->markTestSkipped( 'Block editor is not supported for certificates on this version of WordPress.' );\n\t\t}\n\n\t\t$handle    = 'llms-admin-certificate-editor';\n\t\t$inline_id = 'llms-admin-certificate-settings';\n\n\t\t$reset = function() use ( $handle ) {\n\t\t\tLLMS_Unit_Test_Util::set_private_property( llms()->assets, 'inline', array() );\n\t\t\twp_dequeue_script( $handle );\n\t\t};\n\t\t$reset();\n\n\t\t// Wrong screen.\n\t\tset_current_screen( 'fake' );\n\t\t$this->main->block_editor_assets();\n\t\t$this->assertAssetNotEnqueued( 'script', $handle );\n\t\t$this->assertArrayNotHasKey(\n\t\t\t$inline_id,\n\t\t\tLLMS_Unit_Test_Util::get_private_property_value( llms()->assets, 'inline' ) );\n\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$reset();\n\n\t\t\tset_current_screen( $post_type );\n\t\t\tglobal $current_screen;\n\t\t\t$current_screen->is_block_editor = true;\n\n\t\t\t$this->main->block_editor_assets();\n\n\t\t\t$this->assertAssetIsEnqueued( 'script', $handle );\n\n\t\t\t$this->assertArrayHasKey(\n\t\t\t\t$inline_id,\n\t\t\t\tLLMS_Unit_Test_Util::get_private_property_value( llms()->assets, 'inline' )\n\t\t\t);\n\n\t\t}\n\n\t\tllms_tests_reset_current_screen();\n\t\t$current_screen->is_block_editor = false;\n\n\t}\n\n\t/**\n\t * Test get_analytics_options()\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_analytics_options() {\n\n\t\t$this->assertEquals( array( 'currency_format' => '$#,##0.00' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_analytics_options' ) );\n\n\t\t// Simulate comma decimal separator that's forced back to decimals.\n\t\tadd_filter( 'lifterlms_thousand_separator', function( $sep ) { return '.'; } );\n\t\tadd_filter( 'lifterlms_decimal_separator', function( $sep ) { return ','; } );\n\n\t\t$this->assertEquals( array( 'currency_format' => '$#,##0.00' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_analytics_options' ) );\n\n\t\tremove_all_filters( 'lifterlms_thousand_separator' );\n\t\tremove_all_filters( 'lifterlms_decimal_separator' );\n\n\t\t// Simulate non US symbol on the right with a space.\n\t\tadd_filter( 'lifterlms_currency_symbol', function( $sym ) { return 'A'; } );\n\t\tadd_filter( 'lifterlms_price_format', function( $format ) { return '%2$s %1$s'; } );\n\n\t\t$this->assertEquals( array( 'currency_format' => '#,##0.00 A' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_analytics_options' ) );\n\n\t\tremove_all_filters( 'lifterlms_currency_symbol' );\n\t\tremove_all_filters( 'lifterlms_price_format' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on a screen where it shouldn't be registered.\n\t *\n\t * @since 4.3.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_wrong_screen() {\n\n\t\t$screen = (object) array( 'base' => 'fake' );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetNotRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetNotRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetNotEnqueued( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetNotEnqueued( 'script', 'llms-analytics' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on the dashboard page where analytics are required for the data widgets\n\t *\n\t * This test tests the default \"assumed\" tab when there's no `tab` set in the $_GET array.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_dashboard_assumed() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-dashboard' );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetIsEnqueued( 'script', 'llms-analytics' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on the dashboard page where analytics are required for the data widgets\n\t *\n\t * This test is the same as test_maybe_enqueue_reporting_dashboard_assumed() except this one explicitly\n\t * tests for the presence of the `tab=general` in the $_GET array.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_dashbord_explicit() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-dashboard' );\n\t\t$this->mockGetRequest( array( 'tab' => 'general' ) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetIsEnqueued( 'script', 'llms-analytics' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on settings tabs other than general, scripts will not be registered.\n\t *\n\t * @since 4.3.3\n\t * @since 7.1.0 Updated to reflect that `llms-google-charts` and `llms-analytics` are not registered on settings screens.\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_other_tabs() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-settings' );\n\t\t$this->mockGetRequest( array( 'tab' => 'fake' ) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetNotRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetNotRegistered( 'script', 'llms-analytics' );\n\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on reporting screens where the scripts aren't needed.\n\t *\n\t * @since 4.3.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_invalid_reporting_screens() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetNotEnqueued( 'script', 'llms-analytics' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on the enrollments reporting screen\n\t *\n\t * @since 4.3.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_enrollments_reporting_screens() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' );\n\t\t$this->mockGetRequest( array( 'tab' => 'enrollments' ) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetIsEnqueued( 'script', 'llms-analytics' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on the sales reporting screen\n\t *\n\t * @since 4.3.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_sales_reporting_screens() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' );\n\t\t$this->mockGetRequest( array( 'tab' => 'sales' ) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetIsEnqueued( 'script', 'llms-analytics' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on the main quizzes reporting screen\n\t *\n\t * @since 4.3.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_quiz_main_reporting_screens() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' );\n\t\t$this->mockGetRequest( array( 'tab' => 'quizzes' ) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetNotEnqueued( 'script', 'llms-analytics' );\n\t\t$this->assertAssetNotEnqueued( 'script', 'llms-quiz-attempt-review' );\n\n\t}\n\n\t/**\n\t * Test maybe_enqueue_reporting() on the quiz attempts reporting screen\n\t *\n\t * @since 4.3.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_enqueue_reporting_quiz_attempts_reporting_screens() {\n\n\t\t$screen = (object) array( 'base' => 'lifterlms_page_llms-reporting' );\n\t\t$this->mockGetRequest( array( 'tab' => 'quizzes', 'stab' => 'attempts' ) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'maybe_enqueue_reporting', array( $screen ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-google-charts' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-analytics' );\n\n\t\t$this->assertAssetNotEnqueued( 'script', 'llms-analytics' );\n\t\t$this->assertAssetIsEnqueued( 'script', 'llms-quiz-attempt-review' );\n\n\t}\n\n\t/**\n\t * Tets {@see LLMS_Admin_Assets:register_quill}.\n\t *\n\t * @since 6.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_quill() {\n\n\t\twp_deregister_script( 'llms-quill' );\n\t\twp_deregister_script( 'llms-quill-wordcount' );\n\t\twp_deregister_style( 'llms-quill-bubble' );\n\n\t\tLLMS_Admin_Assets::register_quill( array( 'wordcount' ) );\n\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-quill' );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-quill-wordcount' );\n\t\t$this->assertAssetIsRegistered( 'style', 'llms-quill-bubble' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-builder.php",
    "content": "<?php\n/**\n * Test Admin Builder API\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group builder\n *\n * @since 3.37.12\n * @since 4.14.0 Added tests on the autosave option.\n * @since 4.16.0 Added tests on 'the_title' and 'the_content' filters not affecting the save.\n * @since 5.1.3 Added tests on lesson moved into a brand new section.\n */\nclass LLMS_Test_Admin_Builder extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 3.37.12\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->main = 'LLMS_Admin_Builder';\n\t}\n\n\t/**\n\t * Test get_autosave_states()\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_autosave_status() {\n\n\t\t// Defaults to yes.\n\t\t$this->assertEquals( 'no', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) );\n\n\t\t// User has no value set.\n\t\t$user = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $user );\n\t\t$this->assertEquals( 'no', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) );\n\n\t\t// Explicit yes.\n\t\tupdate_user_meta( $user, 'llms_builder_autosave','yes' );\n\t\t$this->assertEquals( 'yes', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) );\n\n\t\t// Explicit no.\n\t\tupdate_user_meta( $user, 'llms_builder_autosave','no' );\n\t\t$this->assertEquals( 'no', LLMS_Unit_Test_Util::call_method( $this->main, 'get_autosave_status' ) );\n\n\t}\n\n\t/**\n\t * Test LLMS_Admin_Builder::get_existing_posts() with a lesson created by users of different roles.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1849\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_get_existing_lesson_by_role() {\n\n\t\t$all_lesson_ids        = array();\n\t\t$instructor_lesson_ids = array();\n\t\t$users                 = array();\n\t\t$roles                 = array(\n\t\t\t'administrator',\n\t\t\t'lms_manager',\n\t\t\t'instructor',\n\t\t\t'instructors_assistant',\n\t\t\t'student',\n\t\t);\n\n\t\t// Create multiple users for each role.\n\t\tforeach ( $roles as $role ) {\n\n\t\t\tfor ( $user_counter = 0; $user_counter < 2; $user_counter ++ ) {\n\n\t\t\t\t$user               = $this->factory->user->create_and_get( array( 'role' => $role ) );\n\t\t\t\t$users[ $user->ID ] = $user;\n\n\t\t\t\t// Create multiple courses that are authored by this instructor.\n\t\t\t\tif ( 'instructor' === $role ) {\n\t\t\t\t\twp_set_current_user( $user->ID );\n\n\t\t\t\t\tif ( ! isset( $instructor_lesson_ids[ $user->ID ] ) ) {\n\t\t\t\t\t\t$instructor_lesson_ids[ $user->ID ] = array();\n\t\t\t\t\t}\n\n\t\t\t\t\tfor ( $course_counter = 0; $course_counter < 2; $course_counter ++ ) {\n\n\t\t\t\t\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 2 ) );\n\t\t\t\t\t\tforeach ( $course->get_lessons( 'ids' ) as $lesson_id ) {\n\t\t\t\t\t\t\t$all_lesson_ids[]                     = $lesson_id;\n\t\t\t\t\t\t\t$instructor_lesson_ids[ $user->ID ][] = $lesson_id;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Create an instructor assistant for this instructor.\n\t\t\t\t\t$assistant = $this->factory->instructor->create_and_get( array( 'role' => 'instructors_assistant' ) );\n\t\t\t\t\t$assistant->add_parent( $user->ID );\n\t\t\t\t\t$users[ $assistant->get_id() ] = $assistant->get_user();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Test each user's capability to build courses with lessons.\n\t\tforeach ( $users as $user_id => $user ) {\n\n\t\t\twp_set_current_user( $user_id );\n\t\t\t$role = reset( $user->roles ); // We created users with only one role.\n\n\t\t\t// Get lessons that the user can access.\n\t\t\t$lesson_search    = LLMS_Unit_Test_Util::call_method( $this->main, 'get_existing_posts', array( 'lesson' ) );\n\t\t\t$found_lesson_ids = array();\n\t\t\tforeach ( $lesson_search['results'] as $result ) {\n\t\t\t\t$found_lesson_ids[] = $result['id'];\n\t\t\t}\n\n\t\t\tswitch ( $role ) {\n\t\t\t\tcase 'administrator':\n\t\t\t\tcase 'lms_manager':\n\t\t\t\t\t$message = \"$role can build courses with all lessons.\";\n\t\t\t\t\t$this->assertEqualSets( $all_lesson_ids, $found_lesson_ids, $message );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'instructor':\n\t\t\t\t\t$message = 'Instructors can build courses with lessons that they have authored.';\n\t\t\t\t\t$this->assertEqualSets( $instructor_lesson_ids[ $user_id ], $found_lesson_ids, $message );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'instructors_assistant':\n\t\t\t\t\t$assistant           = llms_get_instructor( $user_id );\n\t\t\t\t\t$instructor_ids      = (array) $assistant->get( 'parent_instructors' );\n\t\t\t\t\t$expected_lesson_ids = $instructor_lesson_ids[ reset( $instructor_ids ) ] ?? array();\n\t\t\t\t\t$message             = 'Instructor\\'s assistants can build courses with lessons that their ' .\n\t\t\t\t\t\t'parent instructors have authored.';\n\t\t\t\t\t$this->assertEqualSets( $expected_lesson_ids, $found_lesson_ids, $message );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'student':\n\t\t\t\t\t$this->assertEmpty( $found_lesson_ids, 'Students can not build courses with any lessons.' );\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Filter callback for `llms_builder_trash_custom_item` used to mock a custom item deletion.\n\t *\n\t * @since  3.37.12\n\t *\n\t * @param null|array $trash_response Denotes the trash response. See description above for details.\n\t * @param array      $res            The initial default error response which can be modified for your needs and then returned.\n\t * @param mixed      $id             The ID of the course element. Usually a WP_Post id.\n\t * @return array\n\t */\n\tpublic function filter_llms_builder_trash_custom_item( $ret, $res, $id ) {\n\t\treturn compact( 'id' );\n\t}\n\n\t/**\n\t * Test process_trash() for an invalid post id (one that doesn't exist).\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_trash_invalid_post_id() {\n\n\t\t$data = array(\n\t\t\t'trash' => array( $this->factory->post->create() + 1 ),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) );\n\n\t\t$this->assertEquals( $data['trash'][0], $res[0]['id'] );\n\t\t$this->assertStringContains( 'Invalid ID.', $res[0]['error'] );\n\n\t}\n\n\t/**\n\t * Test process_trash() for a custom / 3rd party item.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_trash_custom_item() {\n\n\t\tadd_filter( 'llms_builder_trash_custom_item', array( $this, 'filter_llms_builder_trash_custom_item' ), 10, 3 );\n\n\t\t$data = array(\n\t\t\t'trash' => array( $this->factory->post->create() + 1 ),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) );\n\n\t\t$this->assertEquals( array( 'id' => $data['trash'][0] ), $res[0] );\n\n\t\tremove_filter( 'llms_builder_trash_custom_item', array( $this, 'filter_llms_builder_trash_custom_item' ));\n\n\t}\n\n\t/**\n\t * Test process_trash() for an invalid post type.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_trash_invalid_post_type() {\n\n\t\t$data = array(\n\t\t\t'trash' => array( $this->factory->post->create() ),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) );\n\n\t\t$this->assertEquals( $data['trash'][0], $res[0]['id'] );\n\t\t$this->assertEquals( 'Posts cannot be deleted via the Course Builder.', $res[0]['error'] );\n\n\t}\n\n\t/**\n\t * Test process_trash() for success when the post is force-deleted.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_trash_force_delete_success() {\n\n\t\t$types = array( 'section', 'llms_question', 'llms_quiz' );\n\t\tforeach ( $types as $type ) {\n\n\t\t\t$post_id = $this->factory->post->create( array( 'post_type' => $type ) );\n\n\t\t\t$data = array(\n\t\t\t\t'trash' => array( $post_id ),\n\t\t\t);\n\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) );\n\n\t\t\t// Proper return.\n\t\t\t$this->assertEquals( array( 'id' => $post_id ), $res[0] );\n\n\t\t\t// Post has been force deleted.\n\t\t\t$this->assertNull( get_post( $post_id ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test process_trash() when an error is encountered deleting the post.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_trash_deletion_error() {\n\n\t\t// Mock the return of `wp_delete_post()` to simulate an error.\n\t\tadd_filter( 'pre_delete_post', '__return_false' );\n\n\t\t$post_id = $this->factory->post->create( array( 'post_type' => 'section' ) );\n\n\t\t$data = array(\n\t\t\t'trash' => array( $post_id ),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) );\n\n\t\t$this->assertEquals( $post_id, $res[0]['id'] );\n\t\t$this->assertStringContains( 'Error deleting the Section', $res[0]['error'] );\n\n\t\tremove_filter( 'pre_delete_post', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test process_trash() success when moving an item to the trash.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_trash_move_to_trash() {\n\n\t\t$post_id = $this->factory->post->create( array( 'post_type' => 'lesson' ) );\n\n\t\t$data = array(\n\t\t\t'trash' => array( $post_id ),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) );\n\n\t\t// Proper return.\n\t\t$this->assertEquals( array( 'id' => $post_id ), $res[0] );\n\n\t\t// Post has been trashed\n\t\t$this->assertEquals( 'trash', get_post_status( $post_id ) );\n\n\t}\n\n\t/**\n\t * Test process_trash() when deleting a question choice.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_trash_question_choice() {\n\n\t\t$course    = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) );\n\t\t$quiz      = $course->get_lessons()[0]->get_quiz();\n\t\t$question  = $quiz->get_questions()[0];\n\t\t$choice    = $question->get_choices()[0];\n\t\t$choice_id = $choice->get( 'id' );\n\n\t\t$id = sprintf( '%1$d:%2$s', $question->get( 'id' ), $choice_id );\n\n\t\t$data = array(\n\t\t\t'trash' => array( $id ),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'process_trash', array( $data ) );\n\n\t\t// Proper return.\n\t\t$this->assertEquals( array( 'id' => $id ), $res[0] );\n\n\t\t// Choice has been deleted.\n\t\t$this->assertFalse( $question->get_choice( $choice_id ) );\n\n\t}\n\n\t/**\n\t * Test the ajax save an possible filters applied to the title and the content\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_ajax_save_unfiltered_title_content() {\n\n\t\t// Handle wp die ajax and simulate ajax call.\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, '_wp_die_handler' ), 1 );\n\t\tadd_filter( 'wp_doing_ajax', '__return_true' );\n\n\t\t$user = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $user );\n\n\t\t// Add title and content filters.\n\t\tforeach ( array( 'the_title', 'the_content' ) as $filter_hook ) {\n\t\t\tadd_filter( $filter_hook, array( $this, '__return_filtered' ), 999999 );\n\t\t}\n\t\t// Create a valid course.\n\t\t$course = $this->factory->course->create( array( 0,0,0,0 ) );\n\n\t\t$request = array(\n\t\t\t'action_type'  => 'ajax_save',\n\t\t\t'course_id'    => $course,\n\t\t\t'llms_builder' => array(\n\t\t\t),\n\t\t);\n\n\t\t$to_save = array(\n\t\t\t'updates' => array(\n\t\t\t\t'id'       => $course,\n\t\t\t\t'sections' => array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'id'            => 'temp_28',\n\t\t\t\t\t\t'parent_course' => $course,\n\t\t\t\t\t\t'title'         => 'New Section',\n\t\t\t\t\t\t'type'          => 'section',\n\t\t\t\t\t\t'lessons'       => array(\n\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t'id'             => 'temp_40',\n\t\t\t\t\t\t\t\t'title'          => 'New Lesson',\n\t\t\t\t\t\t\t\t'content'        => '<p>Content</p>',\n\t\t\t\t\t\t\t\t'video_embed'    => 'https://somevideo',\n\t\t\t\t\t\t\t\t'parent_course'  => $course,\n\t\t\t\t\t\t\t\t'parent_section' => 'temp_28',\n\t\t\t\t\t\t\t\t'type'           => 'lesson',\n\t\t\t\t\t\t\t\t'quiz'           => array(\n\t\t\t\t\t\t\t\t\t'id'        => 'temp_123',\n\t\t\t\t\t\t\t\t\t'title'     => 'New Quiz',\n\t\t\t\t\t\t\t\t\t'type'      => 'llms_quiz',\n\t\t\t\t\t\t\t\t\t'lesson_id' => 'temp_40',\n\t\t\t\t\t\t\t\t\t'content'   => '<p>Quiz description</p>',\n\t\t\t\t\t\t\t\t\t'questions' => array(\n\t\t\t\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t\t\t\t'id'            => 'temp_155',\n\t\t\t\t\t\t\t\t\t\t\t'content'       => '<p>Question description 1</p>',\n\t\t\t\t\t\t\t\t\t\t\t'title'         => 'Question title 1',\n\t\t\t\t\t\t\t\t\t\t\t'parent_id'     => 'temp_123',\n\t\t\t\t\t\t\t\t\t\t\t'type'          => 'llms_question',\n\t\t\t\t\t\t\t\t\t\t\t'question_type' => 'choice',\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t\t\t\t\t'id'            => 'temp_156',\n\t\t\t\t\t\t\t\t\t\t\t'content'       => '<p>Question description 2</p>',\n\t\t\t\t\t\t\t\t\t\t\t'title'         => 'Question title 2',\n\t\t\t\t\t\t\t\t\t\t\t'parent_id'     => 'temp_123',\n\t\t\t\t\t\t\t\t\t\t\t'type'          => 'llms_question',\n\t\t\t\t\t\t\t\t\t\t\t'question_type' => 'choice',\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t\t'id'      => $course,\n\t\t);\n\n\t\t$request['llms_builder'] = wp_json_encode( $to_save );\n\n\t\t// Simulate the ajax save request.\n\t\tob_start();\n\t\ttry {\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'handle_ajax', array( $request ) );\n\t\t} catch ( WPAjaxDieContinueException $e ) {}\n\t\t$res = json_decode( $this->last_response, true );\n\n\t\t// Check the request went through.\n\t\t$this->assertEquals( 'success', $res['llms_builder']['status'] );\n\n\t\t// Check the raw title and content have not been affected by the filters.\n\t\t$this->check_title_content_filtering_on_save( $res, $to_save );\n\n\t\t/* Check the raw title and content have not been affected by the filters. */\n\n\t\t// Following the instructions contained in the handle_ajax method that actually perform the update,\n\t\t// but without removing any filters on the_title, the_content.\n\t\t$req = $request;\n\t\t$req['llms_builder'] = stripslashes( $request['llms_builder'] );\n\t\t$res = LLMS_Unit_Test_Util::call_method(\n\t\t\t$this->main,\n\t\t\t'heartbeat_received',\n\t\t\tarray(\n\t\t\t\tarray(),\n\t\t\t\t$req,\n\t\t\t)\n\t\t);\n\n\t\t// Check the request went through.\n\t\t$this->assertEquals( 'success', $res['llms_builder']['status'] );\n\n\t\t// Check the raw title and content have not been affected by the filters.\n\t\t$this->check_title_content_filtering_on_save( $res, $to_save );\n\n\t\t// Reset.\n\t\tforeach ( array( 'the_title', 'the_content' ) as $filter_hook ) {\n\t\t\tremove_filter( $filter_hook, array( $this, '__return_filtered' ), 999999 );\n\t\t}\n\t\tremove_filter( 'wp_die_handler', array( $this, '_wp_die_handler' ), 1 );\n\t\tremove_filter( 'wp_doing_ajax', '__return_true' );\n\t}\n\n\t/**\n\t * Helper that always returns the string '{filtered}'\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return string\n\t */\n\tprivate function __return_filtered() {\n\t\treturn '{filtered}';\n\t}\n\n\t/**\n\t * Helper to check whether the title and content props are filtered on save.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @param array $res  Associative array containing the response from the save ajax method.\n\t * @param array $sent Associative array containing the data sent for the update.\n\t * @return void\n\t */\n\tprivate function check_title_content_filtering_on_save( $res, $sent ) {\n\n\t\t$li = 0;\n\n\t\tforeach ( $res['llms_builder']['updates']['sections'][0]['lessons'] as $lesson ) {\n\t\t\t$lq = 0;\n\t\t\tforeach ( array( 'title', 'content' ) as $prop ) {\n\t\t\t\t// Check lesson's title and content.\n\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t$sent['updates']['sections'][0]['lessons'][$li][$prop],\n\t\t\t\t\tllms_get_post( $lesson['id'] )->get( $prop, true ),\n\t\t\t\t\t$prop\n\t\t\t\t);\n\t\t\t\t$this->assertStringNotContainsString(\n\t\t\t\t\t$this->__return_filtered(),\n\t\t\t\t\tllms_get_post( $lesson['id'] )->get( $prop, true ),\n\t\t\t\t\t$prop\n\t\t\t\t);\n\n\t\t\t\t// Check quiz title and content.\n\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t$sent['updates']['sections'][0]['lessons'][$li]['quiz'][$prop],\n\t\t\t\t\tllms_get_post( $lesson['quiz']['id'] )->get( $prop, true ),\n\t\t\t\t\t$prop\n\t\t\t\t);\n\t\t\t\t$this->assertStringNotContainsString(\n\t\t\t\t\t$this->__return_filtered(),\n\t\t\t\t\tllms_get_post( $lesson['quiz']['id'] )->get( $prop, true ),\n\t\t\t\t\t$prop\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tforeach ( $lesson['quiz']['questions'] as $question ) {\n\t\t\t\tforeach ( array( 'title', 'content' ) as $prop ) {\n\t\t\t\t\t// Check question title and content.\n\t\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t\t$sent['updates']['sections'][0]['lessons'][$li]['quiz']['questions'][$lq][$prop],\n\t\t\t\t\t\tllms_get_post( $question['id'] )->get( $prop, true ),\n\t\t\t\t\t\t$prop\n\t\t\t\t\t);\n\t\t\t\t\t$this->assertStringNotContainsString(\n\t\t\t\t\t\t$this->__return_filtered(),\n\t\t\t\t\t\tllms_get_post( $question['id'] )->get( $prop, true ),\n\t\t\t\t\t\t$prop\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t$lq++;\n\t\t\t}\n\t\t\t$li++;\n\t\t}\n\t}\n\n\t/**\n\t * Test a lesson is correctly \"moved\" into a brand new section :)\n\t *\n\t * @since 5.1.3\n\t * @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.\n\t *              Replaced the call to the deprecated `LLMS_Lesson::set_parent_course()` method with `LLMS_Lesson::set( 'parent_course', $course_id )`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_move_lesson_in_a_brand_new_section() {\n\n\t\t// Create a Course with a Lesson.\n\t\t$course = $this->factory->course->create_and_get( array(\n\t\t\t'sections' => 1,\n\t\t\t'lessons'  => 1,\n\t\t\t'quizzes'  => 0,\n\t\t) );\n\t\t$lesson = $course->get_lessons()[0];\n\n\t\t// Create a section.\n\t\t$section_id = $this->factory->post->create( array( 'post_type' => 'section' ) );\n\t\t$section    = llms_get_post( $section_id );\n\t\t// Add the section to the course above.\n\t\t$section->set( 'parent_course', $course->get( 'id' ) );\n\n\t\t// Simulate the course lesson moved from its section to the brand new one.\n\t\t// Build builder data.\n\t\t$lessons_data_from_builder = array(\n\t\t\tarray(\n            \t'parent_section' => 'temp_108', // temp parent section.\n\t\t\t\t'id'             => $lesson->get( 'id' ),\n\t\t\t),\n\t\t);\n\n\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t$this->main,\n\t\t\t'update_lessons',\n\t\t\tarray(\n\t\t\t\t$lessons_data_from_builder,\n\t\t\t\t$section // The just created section parent.\n\t\t\t)\n\t\t);\n\n\t\t// Check lesson parents.\n\t\t$this->assertEquals( $course->get( 'id' ), $lesson->get( 'parent_course' ) );\n\t\t$this->assertEquals( $section->get( 'id' ), $lesson->get_parent_section() );\n\n\t}\n\n\t/**\n\t * Catch wp_die() called by ajax methods & store the output buffer contents for use later.\n\t *\n\t * The same method is used in LLMS_Test_AJAX_Handler.\n\t * @since 4.16.0\n\t *\n\t * @param string $msg Die msg.\n\t * @return void\n\t */\n\tpublic function _wp_die_handler( $msg ) {\n\t\t$this->last_response = ob_get_clean();\n\t\tthrow new WPAjaxDieContinueException( $msg );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-import.php",
    "content": "<?php\n/**\n * Tests for LLMS_Admin_Review class\n *\n * @package LifterLMS_Tests/Admin\n *\n * @group admin\n * @group admin_import\n *\n * @since 3.35.0\n * @since 3.37.8 Update path to assets directory.\n * @since 4.7.0 Test success message generation.\n * @since 4.8.0 Move includes to `setUpBeforeClass()` method.\n */\nclass LLMS_Test_Admin_Import extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class.\n\t *\n\t * @since 4.8.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.import.php';\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-export-api.php';\n\n\t}\n\n\t/**\n\t * Setup test case.\n\t *\n\t * @since 3.35.0\n\t * @since 4.8.0 Move includes to `set_up_before_class()` method.\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->import = new LLMS_Admin_Import();\n\n\t}\n\n\t/**\n\t * Tear down test case.\n\t *\n\t * @since 3.35.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tunset( $_FILES['llms_import'] );\n\n\t}\n\n\t/**\n\t * Mock a file upload for some test data.\n\t *\n\t * @since 3.35.0\n\t *\n\t * @param int $err Mock a PHP file upload error code, see https://www.php.net/manual/en/features.file-upload.errors.php.\n\t * @param string $import Filename to use for the import, see `import-*.json` files in the `tests/assets` directory.\n\t * @return void\n\t */\n\tprivate function mock_file_upload( $err = 0, $import = null ) {\n\n\t\t$file = is_null( $import ) ? LLMS_PLUGIN_DIR . 'sample-data/sample-course.json' : $import;\n\n\t\t$_FILES['llms_import'] = array(\n\t\t\t'name' => basename( $file ),\n\t\t\t'tmp_name' => $file,\n\t\t\t'type' => 'application/json',\n\t\t\t'error' => $err,\n\t\t\t'size' => filesize( $file ),\n\t\t);\n\n\t}\n\n\t/**\n\t * Test the add_help_tabs() method.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_help_tabs() {\n\n\t\t// Not on the right screen.\n\t\t$this->assertFalse( $this->import->add_help_tabs() );\n\n\t\t// On the right screen.\n\t\tllms_tests_mock_current_screen( 'lifterlms_page_llms-import' );\n\n\t\t$screen = $this->import->add_help_tabs();\n\n\t\t// Tab has been added.\n\t\t$tab_id = 'llms_import_overview';\n\t\t$tab = $screen->get_help_tab( $tab_id );\n\n\t\t$this->assertEquals( $tab_id, $tab['id'] );\n\n\t\t// Has sidebar content.\n\t\t$this->assertStringContains( 'Import Documentation', $screen->get_help_sidebar() );\n\n\t\tllms_tests_reset_current_screen();\n\n\t}\n\n\t/**\n\t * Test cloud_import() errors from nonce\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cloud_import_error_nonce() {\n\n\t\t// No nonce.\n\t\t$this->assertFalse( $this->import->cloud_import() );\n\n\t\t// Invalid nonce.\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_cloud_importer_nonce' => 'fake',\n\t\t) );\n\t\t$this->assertFalse( $this->import->cloud_import() );\n\n\t}\n\n\t/**\n\t * Test cloud_import() user permission errors\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cloud_import_error_permissions() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ),\n\t\t) );\n\t\t$this->assertFalse( $this->import->cloud_import() );\n\n\t}\n\n\t/**\n\t * Test cloud_import() missing necessary data\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cloud_import_error_no_course_id() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_cloud_importer_nonce' => wp_create_nonce( 'llms-cloud-importer' ),\n\t\t) );\n\t\t$res = $this->import->cloud_import();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms-cloud-import-missing-id', $res );\n\n\t}\n\n\t/**\n\t * Test cloud_import() with an api errors\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cloud_import_error_api() {\n\n\t\t$handler = function( $preempt ) {\n\t\t\treturn new WP_Error( 'mocked', 'Mocked error.' );\n\t\t};\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_cloud_import_course_id' => 1,\n\t\t\t'llms_cloud_importer_nonce'   => wp_create_nonce( 'llms-cloud-importer' ),\n\t\t) );\n\n\t\tadd_filter( 'pre_http_request', $handler );\n\n\t\t$res = $this->import->cloud_import();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'mocked', $res );\n\n\t\tremove_filter( 'pre_http_request', $handler );\n\n\t}\n\n\t/**\n\t * Test cloud_import() with a real API error from submitting invalid ids\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cloud_import_error_api_real() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_cloud_import_course_id' => 1,\n\t\t\t'llms_cloud_importer_nonce'   => wp_create_nonce( 'llms-cloud-importer' ),\n\t\t) );\n\n\t\t$res = $this->import->cloud_import();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'not-found', $res );\n\n\t}\n\n\t/**\n\t * Test cloud_import() with a generator error\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cloud_import_error_generator() {\n\n\t\t$handler = function( $preempt ) {\n\t\t\treturn array( 'fake api response' );\n\t\t};\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_cloud_import_course_id' => 1,\n\t\t\t'llms_cloud_importer_nonce'   => wp_create_nonce( 'llms-cloud-importer' ),\n\t\t) );\n\n\t\tadd_filter( 'pre_http_request', $handler );\n\n\t\t$res = $this->import->cloud_import();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'missing-generator', $res );\n\n\t\tremove_filter( 'pre_http_request', $handler );\n\n\t}\n\n\t/**\n\t * Test cloud_import() success\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cloud_import_success() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_cloud_import_course_id' => 33579, // Free Course Lead Magnet Template.\n\t\t\t'llms_cloud_importer_nonce'   => wp_create_nonce( 'llms-cloud-importer' ),\n\t\t) );\n\n\t\t$this->assertTrue( $this->import->cloud_import() );\n\n\t}\n\n\t/**\n\t * Test enqueue() method\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue() {\n\n\t\t$slug = 'llms-admin-importer';\n\n\t\t$this->assertNull( $this->import->enqueue() );\n\t\t$this->assertAssetNotRegistered( 'style', $slug );\n\t\t$this->assertAssetNotEnqueued( 'style', $slug );\n\n\t\tllms_tests_mock_current_screen( 'lifterlms_page_llms-import' );\n\n\t\t$this->assertTrue( $this->import->enqueue() );\n\n\t\t$this->assertAssetIsRegistered( 'style', $slug );\n\t\t$this->assertAssetIsEnqueued( 'style', $slug );\n\n\t\tllms_tests_reset_current_screen();\n\n\t}\n\n\t/**\n\t * Test get_screen()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screen() {\n\n\t\t// Wrong screen.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->import, 'get_screen' ) );\n\n\t\tllms_tests_mock_current_screen( 'admin.php' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->import, 'get_screen' ) );\n\n\t\t// Right screen.\n\t\tllms_tests_mock_current_screen( 'lifterlms_page_llms-import' );\n\t\t$screen = LLMS_Unit_Test_Util::call_method( $this->import, 'get_screen' );\n\t\t$this->assertTrue( $screen instanceof WP_Screen );\n\t\t$this->assertEquals( 'lifterlms_page_llms-import', $screen->id );\n\n\t}\n\n\t/**\n\t * Test get_success_message()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_success_message() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$generator = new LLMS_Generator( array() );\n\t\t$course = $this->factory->post->create_many( 2, array( 'post_type' => 'course' ) );\n\t\t$user = $this->factory->user->create_many( 1 );\n\t\tLLMS_Unit_Test_Util::set_private_property( $generator, 'generated', compact( 'course', 'user' ) );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->import, 'get_success_message', array( $generator ) );\n\n\t\t$this->assertStringContains( 'Import Successful!', $res );\n\n\t\tforeach( $course as $id ) {\n\t\t\t$this->assertStringContains( esc_url( get_edit_post_link( $id ) ), $res );\n\t\t\t$this->assertStringContains( get_the_title( $id ), $res );\n\t\t}\n\n\t\t$user = new WP_User( $user[0] );\n\t\t$this->assertStringContains( esc_url( get_edit_user_link( $user->ID ) ), $res );\n\t\t$this->assertStringContains( $user->display_name, $res );\n\n\t}\n\n\t/**\n\t * Upload form not submitted.\n\t *\n\t * @since 3.35.0\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_import_not_submitted() {\n\n\t\t$this->assertFalse( $this->import->upload_import() );\n\n\t}\n\n\t/**\n\t * Submitted with an invalid nonce.\n\t *\n\t * @since 3.35.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_upload_import_invalid_nonce() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_importer_nonce' => 'fake',\n\t\t) );\n\t\t$this->assertFalse( $this->import->upload_import() );\n\n\t}\n\n\t/**\n\t * Submitted without files.\n\t *\n\t * @since 3.35.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_upload_import_missing_files() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ),\n\t\t) );\n\t\t$this->assertFalse( $this->import->upload_import() );\n\n\t}\n\n\t/**\n\t * Submitted by a user without proper permissions.\n\t *\n\t * @since 3.35.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_upload_import_invalid_permissions() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ),\n\t\t) );\n\t\t$this->mock_file_upload();\n\t\t$this->assertFalse( $this->import->upload_import() );\n\n\n\t}\n\n\t/**\n\t * File encountered validation errors.\n\t *\n\t * @since 3.35.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_upload_import_validation_issues() {\n\n\t\twp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ),\n\t\t) );\n\n\t\t// Test all the possible PHP file errors.\n\t\t$errs = array(\n\t\t\t1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',\n\t\t\t2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',\n\t\t\t3 => 'The uploaded file was only partially uploaded.',\n\t\t\t4 => 'No file was uploaded.',\n\t\t\t6 => 'Missing a temporary folder.',\n\t\t\t7 => 'Failed to write file to disk.',\n\t\t\t8 => 'File upload stopped by extension.',\n\t\t\t9 => 'Unknown upload error.',\n\t\t);\n\t\tforeach ( $errs as $i => $msg ) {\n\n\t\t\t$this->mock_file_upload( $i );\n\t\t\t$err = $this->import->upload_import();\n\t\t\t$this->assertIsWPError( $err );\n\t\t\t$this->assertWPErrorMessageEquals( $msg, $err );\n\n\t\t}\n\n\t\t// invalid filetype.\n\t\t$this->mock_file_upload();\n\t\t$_FILES['llms_import']['name'] = 'mock.txt';\n\n\t\t$err = $this->import->upload_import();\n\t\t$this->assertIsWPError( $err );\n\t\t$this->assertWPErrorMessageEquals( 'Only valid JSON files can be imported.', $err );\n\n\t}\n\n\t/**\n\t * Generator encountered an issues when setting the generator method.\n\t *\n\t * @since 3.35.0\n\t * @since 3.37.8 Update path to assets directory.\n\t *\n\t * @return void\n\t */\n\tpublic function test_upload_import_invalid_generator_error() {\n\n\t\twp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ),\n\t\t) );\n\n\t\tglobal $lifterlms_tests;\n\t\t$this->mock_file_upload( 0, $lifterlms_tests->assets_dir . 'import-fake-generator.json' );\n\n\t\t$err = $this->import->upload_import();\n\t\t$this->assertIsWPError( $err );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-generator', $err );\n\n\t}\n\n\t/**\n\t * Error during generation (missing required data)\n\t *\n\t * @since 3.35.0\n\t * @since 3.37.8 Update path to assets directory.\n\t * @since 4.9.0 PHP8 upgrades from notice to warning.\n\t *\n\t * @return void\n\t */\n\tpublic function test_upload_import_generation_error() {\n\n\t\twp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ),\n\t\t) );\n\n\t\tglobal $lifterlms_tests;\n\t\t$this->mock_file_upload( 0, $lifterlms_tests->assets_dir . 'import-error.json' );\n\n\t\t$err = $this->import->upload_import();\n\t\t$this->assertIsWPError( $err );\n\n\t\t$expected_code = 8 === PHP_MAJOR_VERSION ? 'E_WARNING' : 'E_NOTICE';\n\t\t$this->assertWPErrorCodeEquals( $expected_code, $err );\n\n\t}\n\n\t/**\n\t * Success.\n\t *\n\t * @since 3.35.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_upload_import_success() {\n\n\t\twp_set_current_user( $this->factory->student->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_importer_nonce' => wp_create_nonce( 'llms-importer' ),\n\t\t) );\n\t\t$this->mock_file_upload();\n\n\t\t$this->assertTrue( $this->import->upload_import() );\n\n\t}\n\n\t/**\n\t * Test output() method.\n\t *\n\t * @since 4.7.0\n\t * @since 7.1.0 Mark-up update.\n\t *\n\t * @return void\n\t */\n\tpublic function test_output() {\n\n\t\t$this->assertOutputContains( '<div class=\"wrap lifterlms lifterlms-settings llms-import-export\">', array( $this->import, 'output' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-menus.php",
    "content": "<?php\n/**\n * Test Admin Menus Class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group admin_menus\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Admin_Menus extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * @var LLMS_Admin_Menus\n\t */\n\tprivate $main;\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 4.7.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t * @since 6.0.0 Removed loading the LLMS_Admin_Reporting class file that is now handled by the autoloader.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.menus.php';\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 4.7.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Menus();\n\n\t}\n\n\t/**\n\t * Retrieves a mock admin menu array.\n\t *\n\t * @since 7.0.1\n\t *\n\t * @return array[]\n\t */\n\tprivate function get_mock_admin_menu() {\n\n\t\t$menu = array();\n\t\t$menu[2]  = array( __( 'Dashboard' ), 'read', 'index.php', '', 'menu-top menu-top-first menu-icon-dashboard', 'menu-dashboard', 'dashicons-dashboard' );\n\t\t$menu[4]  = array( '', 'read', 'separator1', '', 'wp-menu-separator' );\n\t\t$menu[5] = array( 'Posts', 'edit_posts', 'edit.php', '', 'menu-top menu-icon-post open-if-no-js', 'menu-posts', 'dashicons-admin-post' );\n\t\t$menu[7] = array( '', 'read', 'separator2', '', 'wp-menu-separator' );\n\n\t\treturn $menu;\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Admin_Menus::instructor:menu_hack}.\n\t *\n\t * @since 7.0.1\n\t */\n\tpublic function test_instructor_menu_hack() {\n\n\t\tglobal $menu;\n\n\t\t$tests = array(\n\t\t\t'administrator'         => array( 2, 4, 5, 7 ),\n\t\t\t'lms_manager'           => array( 2, 4, 5, 7 ),\n\t\t\t'author'                => array( 2, 4, 5, 7 ),\n\t\t\t'instructor'            => array( 2, 4, 7 ),\n\t\t\t'instructors_assistant' => array( 2, 4, 7 ),\n\t\t);\n\n\t\tforeach ( $tests as $role => $expected ) {\n\t\t\t$menu = $this->get_mock_admin_menu();\n\t\t\twp_set_current_user( $this->factory->user->create( compact( 'role' ) ) );\n\t\t\t$this->main->instructor_menu_hack();\n\t\t\t$this->assertEquals( $expected, array_keys( $menu ), $role );\n\t\t}\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Admin_Menus::instructor:menu_hack} when an instructor is\n\t * explicitly allowed to edit posts.\n\t *\n\t * @since 7.0.1\n\t */\n\tpublic function test_instructor_menu_hack_removed() {\n\n\t\tglobal $menu;\n\t\t$menu = $this->get_mock_admin_menu();\n\n\t\t// Allow instructor to edit.\n\t\t$handler = function( $roles ) {\n\t\t\treturn array( 'instructors_assistant' );\n\t\t};\n\t\tadd_filter( 'llms_instructor_menu_hack_roles', $handler );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'instructor' ) ) );\n\n\t\t$this->main->instructor_menu_hack();\n\t\t$this->assertEquals( array( 2, 4, 5, 7 ), array_keys( $menu ) );\n\n\t\tremove_filter( 'llms_instructor_menu_hack_roles', $handler );\n\t\tunset( $menu );\n\n\t}\n\n\t/**\n\t * Test reporting_page_init() when there's permission issues.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reporting_page_init_permissions_error() {\n\n\t\t$this->mockGetRequest( array( 'student_id' => $this->factory->student->create() ) );\n\n\t\t$this->setExpectedException( 'WPDieException', 'You do not have permission to access this content.' );\n\n\t\t$this->main->reporting_page_init();\n\n\t}\n\n\t/**\n\t * Test reporting_page_init() when there's no permission issues\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reporting_page_init_permission_success() {\n\n\t\tset_current_screen( 'admin' );\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockGetRequest( array( 'student_id' => $this->factory->student->create() ) );\n\n\t\t$this->assertOutputContains( '<div class=\"wrap lifterlms llms-reporting tab--students\">', array( $this->main, 'reporting_page_init' ) );\n\n\t\tset_current_screen( 'front' );\n\t}\n\n\t/**\n\t * Test reporting_page_init() when there's no permission issues\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reporting_page_init_no_permissions() {\n\n\t\tset_current_screen( 'admin' );\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->assertOutputContains( '<div class=\"wrap lifterlms llms-reporting tab--students\">', array( $this->main, 'reporting_page_init' ) );\n\n\t\tset_current_screen( 'front' );\n\t}\n\n\t/**\n\t * Test status_page_includes()\n\t *\n\t * @since 4.12.0\n\t * @since 6.0.0 Updated for autoloader changes. Stopped autoloading classes when checking if they exist.\n\t *              Stopped checking for the LLMS_Admin_Page_Status class because status_page_includes() no longer loads it.\n\t *\n\t * @return void\n\t */\n\tpublic function test_status_page_includes() {\n\n\t\t$classes = array(\n\t\t\t'LLMS_Admin_Tool_Batch_Eraser',\n\t\t\t'LLMS_Admin_Tool_Clear_Sessions',\n\t\t\t'LLMS_Admin_Tool_Recurring_Payment_Rescheduler',\n\t\t);\n\n\t\t$actions = did_action( 'llms_load_admin_tools' );\n\n\t\tforeach ( $classes as $class ) {\n\t\t\t$this->assertFalse( class_exists( $class, false ), $class );\n\t\t}\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'status_page_includes' );\n\n\t\t// Classes included.\n\t\tforeach ( $classes as $class ) {\n\t\t\t$this->assertTrue( class_exists( $class, false ), $class );\n\t\t}\n\n\t\t// Action ran.\n\t\t$this->assertSame( ++$actions, did_action( 'llms_load_admin_tools' ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-notices.php",
    "content": "<?php\n/**\n * Test Admin Notices Class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group admin_notices\n *\n * @since 4.10.0\n */\nclass LLMS_Test_Admin_Notices extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 4.10.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\t}\n\n\t/**\n\t * Test add_output_actions().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_output_actions() {\n\n\t\tremove_action( 'admin_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) );\n\n\t\t// Any screen.\n\t\tLLMS_Admin_Notices::add_output_actions();\n\t\t$this->assertEquals( 10, has_action( 'admin_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) ) );\n\n\t\tremove_action( 'admin_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) );\n\n\t\t// LLMS settings screen.\n\t\tset_current_screen( 'lifterlms_page_llms-settings' );\n\n\t\tLLMS_Admin_Notices::add_output_actions();\n\t\t$this->assertEquals( 10, has_action( 'lifterlms_settings_notices', array( 'LLMS_Admin_Notices', 'output_notices' ) ) );\n\n\t\tset_current_screen( 'front' );\n\n\t}\n\n\t/**\n\t * Test init() properly initializes the `$notices` class variable\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_notices_var() {\n\n\t\t$expect = array( 'fake' );\n\t\tupdate_option( 'llms_admin_notices', $expect );\n\n\t\tLLMS_Admin_Notices::init();\n\n\t\t$this->assertEquals( $expect, LLMS_Admin_Notices::get_notices() );\n\n\t}\n\n\t/**\n\t * Test init() properly adds action hooks\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_add_actions() {\n\n\t\tremove_action( 'wp_loaded', array( 'LLMS_Admin_Notices', 'hide_notices' ) );\n\t\tremove_action( 'current_screen', array( 'LLMS_Admin_Notices', 'add_output_actions' ) );\n\t\tremove_action( 'shutdown', array( 'LLMS_Admin_Notices', 'save_notices' ) );\n\n\t\tLLMS_Admin_Notices::init();\n\n\t\t$this->assertEquals( 10, has_action( 'wp_loaded', array( 'LLMS_Admin_Notices', 'hide_notices' ) ) );\n\t\t$this->assertEquals( 10, has_action( 'current_screen', array( 'LLMS_Admin_Notices', 'add_output_actions' ) ) );\n\t\t$this->assertEquals( 10, has_action( 'shutdown', array( 'LLMS_Admin_Notices', 'save_notices' ) ) );\n\n\t}\n\n\t/**\n\t * Test add_notice() for a notice that has been previously dismissed\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_notice_already_dismissed() {\n\n\t\tset_transient( 'llms_admin_notice_test-dismissal_delay', 'yes', 60 );\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-dismissal' );\n\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'test-dismissal' ) );\n\n\t}\n\n\t/**\n\t * Test add_notice() with HTML and defaults\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_notice_with_defaults() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-add-notice', '<p>HTML CONTENT</p>' );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-add-notice' ) );\n\n\t\t$this->assertEquals( array(\n\t\t\t'dismissible'      => true,\n\t\t\t'dismiss_for_days' => 7,\n\t\t\t'flash'            => false,\n\t\t\t'html'             => '<p>HTML CONTENT</p>',\n\t\t\t'remind_in_days'   => 7,\n\t\t\t'remindable'       => false,\n\t\t\t'type'             => 'info',\n\t\t\t'template'         => false,\n\t\t\t'template_path'    => '',\n\t\t\t'default_path'     => '',\n\t\t), LLMS_Admin_Notices::get_notice( 'test-add-notice' ) );\n\n\t}\n\n\t/**\n\t * Test add_notice() with HTML and defaults\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_notice_with_options() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-add-notice-2', array( 'template' => 'path/to/template.php' ) );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-add-notice-2' ) );\n\n\t\t$this->assertEquals( array(\n\t\t\t'dismissible'      => true,\n\t\t\t'dismiss_for_days' => 7,\n\t\t\t'flash'            => false,\n\t\t\t'html'             => '',\n\t\t\t'remind_in_days'   => 7,\n\t\t\t'remindable'       => false,\n\t\t\t'type'             => 'info',\n\t\t\t'template'         => 'path/to/template.php',\n\t\t\t'template_path'    => '',\n\t\t\t'default_path'     => '',\n\t\t), LLMS_Admin_Notices::get_notice( 'test-add-notice-2' ) );\n\n\t}\n\n\t/**\n\t * Test delete_notice()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_notice() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-delete' );\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-delete' ) );\n\n\t\tLLMS_Admin_Notices::delete_notice( 'test-delete' );\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-delete' ) );\n\n\t\t$this->assertSame( 1, did_action( 'lifterlms_delete_test-delete_notice' ) );\n\t\t$this->assertFalse( get_transient( 'llms_admin_notice_test-delete_delay' ) );\n\n\t}\n\n\t/**\n\t * Test delete_notice() when \"reminding\" for a notice that is not remindable\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_notice_remind_not_remindable() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-delete-not-remindable' );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-delete-not-remindable' ) );\n\n\t\tLLMS_Admin_Notices::delete_notice( 'test-delete-not-remindable', 'remind' );\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-delete-not-remindable' ) );\n\t\t$this->assertFalse( get_transient( 'llms_admin_notice_test-delete-not-remindable_delay' ) );\n\t\t$this->assertSame( 1, did_action( 'lifterlms_remind_test-delete-not-remindable_notice' ) );\n\n\t}\n\n\t/**\n\t * Test delete_notice() for a remindable notice\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_notice_remind() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-remind', array( 'remindable' => true ) );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-remind' ) );\n\n\t\tLLMS_Admin_Notices::delete_notice( 'test-remind', 'remind' );\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-remind' ) );\n\t\t$this->assertTrue( is_numeric( get_option( 'llms_admin_notice_test-remind_delay' ) ) && time() < get_option( 'llms_admin_notice_test-remind_delay' ) );\n\t\t$this->assertSame( 1, did_action( 'lifterlms_remind_test-remind_notice' ) );\n\n\n\t}\n\n\t/**\n\t * Test delete_notice() for dismissing a not dismissible notice\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_notice_remind_not_dismissable() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-delete-not-dismissible', array( 'dismissible' => false ) );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-delete-not-dismissible' ) );\n\n\t\tLLMS_Admin_Notices::delete_notice( 'test-delete-not-dismissible', 'hide' );\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-delete-not-dismissible' ) );\n\t\t$this->assertFalse( get_transient( 'llms_admin_notice_test-delete-not-dismissible_delay' ) );\n\t\t$this->assertSame( 1, did_action( 'lifterlms_hide_test-delete-not-dismissible_notice' ) );\n\n\t}\n\n\t/**\n\t * Test delete_notice() for a dismissible notice\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_notice_dismiss() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-dismiss' );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'test-dismiss' ) );\n\n\t\tLLMS_Admin_Notices::delete_notice( 'test-dismiss', 'hide' );\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-dismiss' ) );\n\t\t$this->assertTrue( is_numeric( get_option( 'llms_admin_notice_test-dismiss_delay' ) ) && time() < get_option( 'llms_admin_notice_test-dismiss_delay' ) );\n\t\t$this->assertSame( 1, did_action( 'lifterlms_hide_test-dismiss_notice' ) );\n\n\t}\n\n\t/**\n\t * Test flash_notice()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_flash_notice() {\n\n\t\tLLMS_Admin_Notices::flash_notice( '<p>FLASH NOTICE</p>', 'error' );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'llms-flash-notice-0' ) );\n\t\t$this->assertEquals( array(\n\t\t\t'dismissible'      => false,\n\t\t\t'dismiss_for_days' => 7,\n\t\t\t'flash'            => true,\n\t\t\t'html'             => '<p>FLASH NOTICE</p>',\n\t\t\t'remind_in_days'   => 7,\n\t\t\t'remindable'       => false,\n\t\t\t'type'             => 'error',\n\t\t\t'template'         => '',\n\t\t\t'template_path'    => '',\n\t\t\t'default_path'     => '',\n\t\t), LLMS_Admin_Notices::get_notice( 'llms-flash-notice-0' ) );\n\n\t\t// Test incrementor.\n\t\tLLMS_Admin_Notices::flash_notice( '<p>FLASH NOTICE 2</p>', 'success' );\n\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'llms-flash-notice-1' ) );\n\t\t$this->assertEquals( array(\n\t\t\t'dismissible'      => false,\n\t\t\t'dismiss_for_days' => 7,\n\t\t\t'flash'            => true,\n\t\t\t'html'             => '<p>FLASH NOTICE 2</p>',\n\t\t\t'remind_in_days'   => 7,\n\t\t\t'remindable'       => false,\n\t\t\t'type'             => 'success',\n\t\t\t'template'         => '',\n\t\t\t'template_path'    => '',\n\t\t\t'default_path'     => '',\n\t\t), LLMS_Admin_Notices::get_notice( 'llms-flash-notice-1' ) );\n\n\n\t}\n\n\t/**\n\t * Test get_notice()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notice() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-get' );\n\n\t\t$this->assertEquals( array(\n\t\t\t'dismissible'      => true,\n\t\t\t'dismiss_for_days' => 7,\n\t\t\t'flash'            => false,\n\t\t\t'html'             => '',\n\t\t\t'remind_in_days'   => 7,\n\t\t\t'remindable'       => false,\n\t\t\t'type'             => 'info',\n\t\t\t'template'         => false,\n\t\t\t'template_path'    => '',\n\t\t\t'default_path'     => '',\n\t\t), LLMS_Admin_Notices::get_notice( 'test-get' ) );\n\n\t}\n\n\tpublic function test_get_notice_not_found() {\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notice( 'test-get-not-found' ) );\n\n\t}\n\n\t/**\n\t * Test get_notices()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notices() {\n\n\t\t// Reset the array from previous tests.\n\t\tLLMS_Admin_Notices::init();\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-get-all' );\n\t\tLLMS_Admin_Notices::add_notice( 'test-get-all-2' );\n\t\t$this->assertEquals( array( 'test-get-all', 'test-get-all-2' ), LLMS_Admin_Notices::get_notices() );\n\n\t}\n\n\t/**\n\t * Test get_notices() when no notices record exists in the DB\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notices_no_db_option() {\n\n\t\tdelete_option( 'llms_admin_notices' );\n\n\t\t// Reset the array from previous tests.\n\t\tLLMS_Admin_Notices::init();\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notices() );\n\n\t}\n\n\t/**\n\t * Test get_notices() when an empty string is stored in the DB option\n\t *\n\t * @since 4.13.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1443\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notices_empty_string_db_option() {\n\n\t\tupdate_option( 'llms_admin_notices', '' );\n\n\t\t// Reset the array from previous tests.\n\t\tLLMS_Admin_Notices::init();\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notices() );\n\n\t}\n\n\t/**\n\t * Test get_notices() when malformed or invalid data is stored in the DB.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notices_invalid_db_option() {\n\n\t\tupdate_option( 'llms_admin_notices', array( array(), 1, null, new stdClass() ) );\n\n\t\t// Reset the array from previous tests.\n\t\tLLMS_Admin_Notices::init();\n\n\t\t$this->assertEquals( array(), LLMS_Admin_Notices::get_notices() );\n\n\t}\n\n\t/**\n\t * Test has_notice()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_notice() {\n\n\t\t$id = 'test-has';\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( $id ) );\n\n\t\tLLMS_Admin_Notices::add_notice( $id );\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( $id ) );\n\n\t}\n\n\t/**\n\t * Test output_notice().\n\t *\n\t * @since 5.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_notice() {\n\n\t\tLLMS_Admin_Notices::init();\n\n\t\t# Create a normal notice.\n\t\t$notice_html = 'Have you heard of the band 999 MB? They haven\\'t got a gig yet.';\n\t\t$notice_id   = 'test-output-notice-normal';\n\t\tLLMS_Admin_Notices::add_notice( $notice_id, $notice_html );\n\t\tLLMS_Admin_Notices::save_notices();\n\n\t\t# Test where current user does not have the 'manage_options' capability.\n\t\t$this->assertOutputEmpty( array( 'LLMS_Admin_Notices', 'output_notice' ), array( $notice_id ) );\n\n\t\t# Test where current user does have the 'manage_options' capability.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertOutputContains( $notice_html, array( 'LLMS_Admin_Notices', 'output_notice' ), array( $notice_id ) );\n\n\t\t# Test where the notice does not exist.\n\t\t$this->assertOutputEmpty( array( 'LLMS_Admin_Notices', 'output_notice' ), array( 'notice-does-not-exist' ) );\n\n\t\t# Test where the notice html is empty.\n\t\t$notice_id = 'test-output-notice-empty-html-empty-template';\n\t\tLLMS_Admin_Notices::add_notice( $notice_id, '' );\n\t\tLLMS_Admin_Notices::save_notices();\n\t\t$this->assertOutputEmpty( array( 'LLMS_Admin_Notices', 'output_notice' ), array( $notice_id ) );\n\n\t}\n\n\t/**\n\t * Test save_notices()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_notices() {\n\n\t\t// Reset the array from previous tests.\n\t\tLLMS_Admin_Notices::init();\n\n\t\tLLMS_Admin_Notices::add_notice( 'test-save-1' );\n\t\tLLMS_Admin_Notices::add_notice( 'test-save-2' );\n\n\t\tLLMS_Admin_Notices::save_notices();\n\n\t\t$this->assertEquals( array( 'test-save-1', 'test-save-2' ), get_option( 'llms_admin_notices' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-page-status.php",
    "content": "<?php\n/**\n * Test Admin Status page\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group status\n *\n * @since 3.37.14\n * @since 4.0.0 Removed clear sessions tests in favor of tests in the `LLMS_Test_Admin_Tool_Clear_Sessions` test class.\n */\nclass LLMS_Test_Admin_Page_Status extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Set up before class\n\t *\n\t * @since Unknown\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.page.status.php';\n\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 3.37.14\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = 'LLMS_Admin_Page_Status';\n\n\t}\n\n\t/**\n\t * Test do_tool() when no nonce is submitted.\n\t *\n\t * @since 3.37.14\n\t * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation.\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_tool_no_nonce() {\n\n\t\t$this->expectException( 'WPDieException' );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' );\n\n\t}\n\n\t/**\n\t * Test do_tool() when invalid nonce is submitted.\n\t *\n\t * @since 3.37.14\n\t * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation.\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_tool_invalid_nonce() {\n\n\t\t$this->expectException( 'WPDieException' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => 'fake',\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' );\n\n\t}\n\n\t/**\n\t * Test do_tool() when no user permissions\n\t *\n\t * @since 3.37.14\n\t * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation.\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_tool_no_user_caps() {\n\n\t\t$this->expectException( 'WPDieException' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => wp_create_nonce( 'llms_tool' ),\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' );\n\n\t}\n\n\t/**\n\t * Test do_tool() valid.\n\t *\n\t * @since 3.37.14\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_tool_valid_user() {\n\n\t\t$actions = did_action( 'llms_status_tool' );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'  => wp_create_nonce( 'llms_tool' ),\n\t\t\t'llms_tool' => 'custom',\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' );\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_status_tool' ) );\n\n\t}\n\n\t/**\n\t * Test the overall progress cache clear tool.\n\t *\n\t * @since 3.37.14\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_tool_clear_cache() {\n\n\t\t// Add mock data.\n\t\tforeach ( $this->factory->student->create_many( 3 ) as $uid ) {\n\t\t\tupdate_user_meta( $uid, 'llms_overall_progress', 'mock' );\n\t\t\tupdate_user_meta( $uid, 'llms_overall_grade', 'mock' );\n\t\t}\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'  => wp_create_nonce( 'llms_tool' ),\n\t\t\t'llms_tool' => 'clear-cache',\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' );\n\n\t\tglobal $wpdb;\n\t\t$res = $wpdb->get_results( \"SELECT * FROM {$wpdb->usermeta} WHERE meta_key = 'llms_overall_progress' OR meta_key = 'llms_overall_grade';\" );\n\n\t\t$this->assertEquals( array(), $res );\n\n\t}\n\n\t/**\n\t * Test the tracking reset tool.\n\t *\n\t * @since 3.37.14\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_tool_reset_tracking() {\n\n\t\tupdate_option( 'llms_allow_tracking', 'yes' );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'  => wp_create_nonce( 'llms_tool' ),\n\t\t\t'llms_tool' => 'reset-tracking',\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' );\n\n\t\t$this->assertEquals( 'no', get_option( 'llms_allow_tracking' ) );\n\n\t}\n\n\t/**\n\t * Test the setup wizard redirect tool.\n\t *\n\t * @since 3.37.14\n\t * @since 4.13.0 Fix expected redirect URL.\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_tool_setup_wizard() {\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( sprintf( '%s [302] YES', admin_url( 'admin.php?page=llms-setup') ) );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'  => wp_create_nonce( 'llms_tool' ),\n\t\t\t'llms_tool' => 'setup-wizard',\n\t\t) );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'do_tool' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-post-types.php",
    "content": "<?php\n/**\n * Test LLMS_Admin_Post_Types\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group admin_post_types\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Admin_Post_Types extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Set Up Before Class\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.post-types.php';\n\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Post_types();\n\n\t}\n\n\t/**\n\t * Test use_block_editor_for_post().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_use_block_editor_for_post() {\n\n\t\t$force_version_2 = function( $ver ) {\n\t\t\treturn 2;\n\t\t};\n\n\t\t$this->assertTrue( $this->main->use_block_editor_for_post( true, $this->factory->post->create_and_get() ) );\n\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => $post_type, 'post_content' => 'Not a block.' ) );\n\n\t\t\t// V1 Template.\n\t\t\t$this->assertFalse( $this->main->use_block_editor_for_post( true, $post ) );\n\n\t\t\tadd_filter( 'llms_certificate_template_version', $force_version_2 );\n\t\t\t$this->assertTrue( $this->main->use_block_editor_for_post( true, $post ) );\n\t\t\tremove_filter( 'llms_certificate_template_version', $force_version_2 );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-profile.php",
    "content": "<?php\n/**\n * Test Admin Profile Class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group admin_profile\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Admin_Profile extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Set Up Before Class\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-profile.php';\n\n\t}\n\n\t/**\n\t * Set-Up\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Profile();\n\n\t}\n\n\t/**\n\t * Tear down\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\twp_set_current_user( null );\n\n\t}\n\n\t/**\n\t * Test current_user_can_edit_admin_custom_fields() method\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_current_user_can_edit_admin_custom_fields() {\n\n\t\t$func = LLMS_Unit_Test_Util::get_private_method( $this->main, 'current_user_can_edit_admin_custom_fields' );\n\n\t\t// No user logged in.\n\t\t$this->assertFalse(\n\t\t\t$func->invokeArgs( $this->main, array( null ) ) // No user passed.\n\t\t);\n\n\t\t$user = $this->factory->user->create();\n\n\t\t$this->assertFalse(\n\t\t\t$func->invokeArgs( $this->main, array( $user ) )\n\t\t);\n\n\t\t// Create a subscriber.\n\t\t$subscriber = $this->factory->user->create( array( 'role' => 'subscriber' ) );\n\t\t// Log-in.\n\t\twp_set_current_user( $subscriber );\n\n\t\t// Still cannot manage the other user custom fields.\n\t\t$this->assertFalse(\n\t\t\t$func->invokeArgs( $this->main, array( $user ) )\n\t\t);\n\n\t\t// Create an admin.\n\t\t$admin = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\t// Log-in.\n\t\twp_set_current_user( $admin );\n\n\t\t$this->assertTrue(\n\t\t\t$func->invokeArgs( $this->main, array( $user ) )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test add_user_meta_fields()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_user_meta_fields() {\n\n\t\t$user_id = $this->factory->user->create();\n\n\t\t$user = get_user( $user_id );\n\n\t\t// No logged-in user.\n\t\t$this->assertFalse(\n\t\t\t$this->main->add_user_meta_fields( $user )\n\t\t);\n\n\t\t// Create an admin.\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\t$admin = get_user( $admin_id );\n\t\t// Log-in.\n\t\twp_set_current_user( $admin_id );\n\n\t\t// Admin user logged-in.\n\t\tob_start(); // ob_start/ob_end_clean wrapper to avoid the view printing (via `include_once`).\n\t\t$this->assertTrue(\n\t\t\t$this->main->add_user_meta_fields( $user )\n\t\t);\n\t\t$this->assertTrue(\n\t\t\t$this->main->add_user_meta_fields( $admin )\n\t\t);\n\t\tob_end_clean();\n\n\t\t// Simple user logged-in: no required caps.\n\t\twp_set_current_user( $user_id );\n\t\t$this->assertFalse(\n\t\t\t$this->main->add_user_meta_fields( $user )\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->main->add_user_meta_fields( $admin )\n\t\t);\n\n\t\t// Admin user logged-in but empty custom fields.\n\t\twp_set_current_user( $admin_id );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'fields', null );\n\n\t\tadd_filter( 'llms_admin_profile_fields', '__return_empty_array' );\n\n\t\t$this->assertFalse(\n\t\t\t$this->main->add_user_meta_fields( $user )\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->main->add_user_meta_fields( $admin )\n\t\t);\n\n\t\tremove_filter( 'llms_admin_profile_fields', '__return_empty_array' );\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-review.php",
    "content": "<?php\n/**\n * Tests for LLMS_Admin_Review class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group admin_reviews\n *\n * @since 3.24.0\n * @version 7.1.0\n */\nclass LLMS_Test_Admin_Review extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup test class\n\t *\n\t * @since 4.14.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-review.php';\n\n\t}\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 3.24.0\n\t * @since 4.14.0 Move file include into `setUpBeforeClass()`.\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Review();\n\n\t}\n\n\t/**\n\t * Test admin_footer() when it's not supposed to display\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_admin_footer_screen_not_set() {\n\t\t$this->assertEquals( 'fake', $this->main->admin_footer( 'fake' ) );\n\t}\n\n\t/**\n\t * Test admin_footer() when it's supposed to display.\n\t *\n\t * @since 4.14.0\n\t * @since 7.1.0 Updated expected text.\n\t *\n\t * @return void\n\t */\n\tpublic function test_admin_footer_screen_on_lifterlms_screen() {\n\n\t\tset_current_screen( 'lifterlms' );\n\t\t$this->assertEquals( 'Please rate <strong>LifterLMS</strong> <a class=\"llms-rating-stars\" href=\"https://wordpress.org/support/plugin/lifterlms/reviews/#new-post\" target=\"_blank\" rel=\"noopener noreferrer\">&#9733;&#9733;&#9733;&#9733;&#9733;</a> on <a href=\"https://wordpress.org/support/plugin/lifterlms/reviews/#new-post\" target=\"_blank\" rel=\"noopener\">WordPress.org</a> to help us spread the word. Thank you from the LifterLMS team!', $this->main->admin_footer( 'fake' ) );\n\t\tset_current_screen( 'front' );\n\n\t}\n\n\t/**\n\t * Test dismiss() for a logged out user with no nonce\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dismiss_permissions_logged_out_no_nonce() {\n\n\t\ttry {\n\t\t\t$this->main->dismiss();\n\t\t} catch ( WPDieException $e ) {\n\t\t\t$this->assertSame( '', get_option( 'llms_review', '' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test dismiss() for a valid user with no nonce\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dismiss_permissions_logged_in_invalid_nonce() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'success' => 'yes',\n\t\t\t'nonce'   => 'fake',\n\t\t) );\n\n\t\ttry {\n\t\t\t$this->main->dismiss();\n\t\t} catch ( WPDieException $e ) {\n\t\t\t$this->assertSame( '', get_option( 'llms_review', '' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test dismiss() when the user goes to wp.org\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dismiss_success() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'success' => 'yes',\n\t\t\t'nonce'   => wp_create_nonce( 'llms-admin-review-request-dismiss' ),\n\t\t) );\n\n\t\ttry {\n\t\t\t$this->main->dismiss();\n\t\t} catch ( WPDieException $e ) {\n\n\t\t\t$this->assertEquals( array(\n\t\t\t\t'time'      => time(),\n\t\t\t\t'dismissed' => true,\n\t\t\t\t'success'   => 'yes',\n\t\t\t), get_option( 'llms_review' ) );\n\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test dismiss() when the user ignores/dismissed\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dismiss_nope() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'success' => 'no',\n\t\t\t'nonce'   => wp_create_nonce( 'llms-admin-review-request-dismiss' ),\n\t\t) );\n\n\t\ttry {\n\t\t\t$this->main->dismiss();\n\t\t} catch ( WPDieException $e ) {\n\t\t\t$review = get_option( 'llms_review' );\n\n\t\t\t$this->assertTrue( $review['dismissed'] );\n\t\t\t$this->assertEquals( 'no', $review['success'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test maybe_show_notice() when logged out.\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_show_notice_no_user() {\n\t\t$this->assertNull( $this->main->maybe_show_notice() );\n\t}\n\n\t/**\n\t * Test maybe_show_notice() on its first run\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_show_notice_first_run() {\n\n\t\tdelete_option( 'llms_review' );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertFalse( $this->main->maybe_show_notice() );\n\n\t\t$this->assertEquals( array(\n\t\t\t'time'      => time(),\n\t\t\t'dismissed' => false,\n\t\t), get_option( 'llms_review' ) );\n\n\t}\n\n\t/**\n\t * Test maybe_show_notice()\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_show() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->factory->student->create_and_enroll_many( 30, $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\n\t\t// Already Dismissed.\n\t\tupdate_option( 'llms_review', array(\n\t\t\t'time'      => time() - YEAR_IN_SECONDS,\n\t\t\t'dismissed' => true,\n\t\t) );\n\t\t$this->assertFalse( $this->main->maybe_show_notice() );\n\n\t\t// Too soon.\n\t\tupdate_option( 'llms_review', array(\n\t\t\t'time'      => time() - HOUR_IN_SECONDS,\n\t\t\t'dismissed' => false,\n\t\t) );\n\t\t$this->assertFalse( $this->main->maybe_show_notice() );\n\n\t\t// Okay.\n\t\tupdate_option( 'llms_review', array(\n\t\t\t'time'      => time() - YEAR_IN_SECONDS,\n\t\t\t'dismissed' => false,\n\t\t) );\n\n\t\t$output = $this->get_output( array( $this->main, 'maybe_show_notice' ) );\n\n\t\t$this->assertStringContains( '<div class=\"notice notice-info is-dismissible llms-admin-notice llms-review-notice\">', $output );\n\n\t}\n\n\t/**\n\t * Test maybe_show_notice() when the notice would display (assuming there were enough enrollments)\n\t *\n\t * @since 4.14.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_show_too_few_enrollments() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Okay.\n\t\tupdate_option( 'llms_review', array(\n\t\t\t'time'      => time() - YEAR_IN_SECONDS,\n\t\t\t'dismissed' => false,\n\t\t) );\n\n\t\t$this->assertFalse( $this->main->maybe_show_notice() );\n\n\t}\n\n\t/**\n\t * Test round_down().\n\t *\n\t * @since 3.24.0\n\t * @since 4.14.0 Use a loop.\n\t *\n\t * @return void\n\t */\n\tpublic function test_round_down() {\n\n\t\t$tests = array(\n\t\t\t// Expected, Input.\n\t\t\tarray( 1, 1 ),\n\t\t\tarray( 5, 5 ),\n\t\t\tarray( 9, 9 ),\n\t\t\tarray( 10, 11 ),\n\t\t\tarray( 20, 25 ),\n\t\t\tarray( 30, 37 ),\n\t\t\tarray( 40, 40 ),\n\t\t\tarray( 50, 58 ),\n\t\t\tarray( 60, 63 ),\n\t\t\tarray( 70, 72 ),\n\t\t\tarray( 80, 88 ),\n\t\t\tarray( 90, 99 ),\n\t\t\tarray( 100, 105 ),\n\t\t\tarray( 200, 293 ),\n\t\t\tarray( 300, 392 ),\n\t\t\tarray( 500, 532 ),\n\t\t\tarray( 700, 781 ),\n\t\t\tarray( 800, 850 ),\n\t\t\tarray( 900, 900 ),\n\t\t\tarray( 1000, 1000 ),\n\t\t\tarray( 1000, 1101 ),\n\t\t\tarray( 1000, 1500 ),\n\t\t\tarray( 2000, 2205 ),\n\t\t\tarray( 5000, 5878 ),\n\t\t\tarray( 9000, 9999 ),\n\t\t\tarray( 10000, 10000 ),\n\t\t\tarray( 10000, 10001 ),\n\t\t\tarray( 10000, 10299 ),\n\t\t\tarray( 10000, 50099 ),\n\t\t);\n\n\t\tforeach ( $tests as $vals ) {\n\t\t\t$this->assertEquals( $vals[0], LLMS_Admin_Review::round_down( $vals[1] ) );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-settings.php",
    "content": "<?php\n/**\n * Tests for LLMS_Admin_Settings class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group admin_settings\n *\n * @since 7.0.0\n */\nclass LLMS_Test_Admin_Settings extends LLMS_UnitTestCase {\n\n\t/**\n\t * Tests set_field_defaults().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_field_defaults() {\n\n\t\t$expected = array(\n\t\t\t'id'           => '',\n\t\t\t'title'        => '',\n\t\t\t'class'        => '',\n\t\t\t'css'          => '',\n\t\t\t'default'      => '',\n\t\t\t'desc'         => '',\n\t\t\t'desc_tooltip' => '',\n\t\t\t'after_html'   => '',\n\t\t);\n\n\t\t$this->assertEquals( $expected, LLMS_Admin_Settings::set_field_defaults( array() ) );\n\n\t\t// Test all default values will be set.\n\t\tforeach ( array_keys( $expected ) as $key ) {\n\t\t\t$expected[ $key ] = 'abc';\n\t\t\t$this->assertEquals( $expected, LLMS_Admin_Settings::set_field_defaults( array( $key => 'abc' ) ) );\n\t\t\t$expected[ $key ] = '';\n\t\t}\n\n\t\t// Title fallback to name.\n\t\t$expected['name']  = 'abc';\n\t\t$expected['title'] = 'abc';\n\t\t$this->assertEquals( $expected, LLMS_Admin_Settings::set_field_defaults( array( 'name' => 'abc' ) ) );\n\n\t}\n\n\t/**\n\t * Test save_fields() with a single checkbox type field.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_fields_checkbox() {\n\n\t\t$id     = 'mock_checkbox_field';\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'checkbox',\n\t\t\t\t'id'   => $id,\n\t\t\t)\n\t\t);\n\n\t\t// Previous value should be overwritten.\n\t\tupdate_option( $id, 'previous val' );\n\n\t\t// Post a new value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => 'doensntmatter',\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals( 'yes', get_option( $id ) );\n\n\t\t// The element wasn't posted.\n\t\t$this->mockPostRequest( array(\n\t\t\t'mock' => '1',\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertSame( 'no', get_option( $id ) );\n\n\t}\n\n\t/**\n\t * Test save_fields() with an checkbox group (array) field.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_fields_checkboxes() {\n\n\t\t$id     = 'mock_checkbox_field';\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'checkbox',\n\t\t\t\t'id'   => $id . '[one]',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type' => 'checkbox',\n\t\t\t\t'id'   => $id . '[two]',\n\t\t\t)\n\t\t);\n\n\t\t// Previous value should be overwritten.\n\t\tupdate_option( $id, 'previous val' );\n\n\t\t// Post a new value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => array(\n\t\t\t\t'one' => 'doesntmatter',\n\t\t\t),\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals( array( 'one' => 'yes', 'two' => 'no' ), get_option( $id ) );\n\n\t\t// The element wasn't posted.\n\t\t$this->mockPostRequest( array(\n\t\t\t'mock' => '1',\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertSame( array( 'one' => 'no', 'two' => 'no' ), get_option( $id ) );\n\n\t}\n\n\t/**\n\t * Tests save_fields() with regular fields.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_fields_basic() {\n\n\t\t$types = array(\n\t\t\t'password',\n\t\t\t'text',\n\t\t\t'email',\n\t\t\t'number',\n\t\t\t'select',\n\t\t\t'single_select_page',\n\t\t\t'single_select_membership',\n\t\t\t'radio',\n\t\t\t'hidden',\n\t\t\t'image',\n\t\t);\n\n\t\tforeach ( $types as $type ) {\n\n\t\t\t$id     = \"mock_{$type}_field\";\n\t\t\t$val    = (string) time();\n\t\t\t$fields = array(\n\t\t\t\tarray(\n\t\t\t\t\t'type' => 'text',\n\t\t\t\t\t'id'   => $id,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Previous value should be overwritten.\n\t\t\tupdate_option( $id, 'previous val' );\n\n\t\t\t// Post a new value.\n\t\t\t$this->mockPostRequest( array(\n\t\t\t\t$id => $val,\n\t\t\t) );\n\t\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t\t$this->assertEquals( $val, get_option( $id ) );\n\n\t\t\t// The element wasn't posted.\n\t\t\t$this->mockPostRequest( array(\n\t\t\t\t'mock' => '1',\n\t\t\t) );\n\t\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t\t$this->assertSame( '', get_option( $id ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test save_fields() with array type fields.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_fields_array() {\n\n\t\t$id     = 'mock_text_arr_field';\n\t\t$val    = (string) time();\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'text',\n\t\t\t\t'id'   => $id . '[one]',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type' => 'text',\n\t\t\t\t'id'   => $id . '[two]',\n\t\t\t)\n\t\t);\n\n\t\t// Post only one value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => array(\n\t\t\t\t'one' => $val\n\t\t\t),\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'one' => $val,\n\t\t\t\t'two' => '',\n\t\t\t),\n\t\t\tget_option( $id )\n\t\t);\n\n\t\t// Post only one value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => array(\n\t\t\t\t'two' => $val\n\t\t\t),\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'one' => '',\n\t\t\t\t'two' => $val,\n\t\t\t),\n\t\t\tget_option( $id )\n\t\t);\n\n\t\t// Post both values.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => array(\n\t\t\t\t'one' => \"{$val}_1\",\n\t\t\t\t'two' => \"{$val}_2\",\n\t\t\t),\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'one' => \"{$val}_1\",\n\t\t\t\t'two' => \"{$val}_2\",\n\t\t\t),\n\t\t\tget_option( $id )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test save_fields() with a secure option.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_fields_secure_option() {\n\n\t\t$id        = 'mock_secure_field_' . time();\n\t\t$secure_id = strtoupper( $id );\n\t\t$val       = (string) time();\n\t\t$fields    = array(\n\t\t\tarray(\n\t\t\t\t'type'          => 'text',\n\t\t\t\t'id'            => $id,\n\t\t\t\t'secure_option' => $secure_id,\n\t\t\t),\n\t\t);\n\n\t\tupdate_option( $id, 'db-value' );\n\n\t\t// A constant/env var isn't defined so save the value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => $val,\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals( $val, get_option( $id ) );\n\n\t\t// The secure value is defined so the DB value will be deleted.\n\t\tputenv( \"{$secure_id}=SECURE-VAL\" );\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => $val,\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals( 'NOT-FOUND', get_option( $id, 'NOT-FOUND' ) );\n\n\t}\n\n\t/**\n\t * Test save_fields() for a setting field with no ID.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_fields_no_id() {\n\n\t\t$actions = did_action( 'lifterlms_update_option' );\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'text',\n\t\t\t),\n\t\t);\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'mock' => '1',\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertSame( $actions, did_action( 'lifterlms_update_option' ) );\n\n\t}\n\n\n\t/**\n\t * Test save_fields() on fields with maxlength attribute.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_field_with_maxlength() {\n\n\t\t// Checking on an array of fields.\n\t\t$id     = 'mock_text_arr_field';\n\t\t$val    = '123456789101112';\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'type' => 'text',\n\t\t\t\t'id'   => $id . '[one]',\n\t\t\t\t'custom_attributes' => array(\n\t\t\t\t\t'maxlength' => 9,\n\t\t\t\t)\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type' => 'text',\n\t\t\t\t'id'   => $id . '[two]',\n\t\t\t)\n\t\t);\n\n\t\t// Post only one value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => array(\n\t\t\t\t'one' => $val\n\t\t\t),\n\t\t) );\n\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'one' => '123456789',\n\t\t\t\t'two' => '',\n\t\t\t),\n\t\t\tget_option( $id )\n\t\t);\n\n\t\t// Post only one value.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => array(\n\t\t\t\t'two' => $val\n\t\t\t),\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'one' => '',\n\t\t\t\t'two' => $val,\n\t\t\t),\n\t\t\tget_option( $id )\n\t\t);\n\n\t\t// Post both values.\n\t\t$this->mockPostRequest( array(\n\t\t\t$id => array(\n\t\t\t\t'one' => $val,\n\t\t\t\t'two' => $val,\n\t\t\t),\n\t\t) );\n\t\t$res = LLMS_Admin_Settings::save_fields( $fields );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'one' => '123456789',\n\t\t\t\t'two' => $val,\n\t\t\t),\n\t\t\tget_option( $id )\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-setup-wizard.php",
    "content": "<?php\n/**\n * Test Setup Wizard\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group setup_wizard\n *\n * @since 7.4.0\n * @version 7.4.0\n */\nclass LLMS_Test_Admin_Setup_Wizard extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup Before Class\n\t *\n\t * Include required class files\n\t *\n\t * @since 4.8.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-export-api.php';\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.setup.wizard.php';\n\n\t}\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 4.8.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Setup_Wizard();\n\n\t}\n\n\t/**\n\t * Test constructor\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\tforeach ( array( '__return_true' => 10, '__return_false' => false ) as $func => $expect ) {\n\n\t\t\tadd_filter( 'llms_enable_setup_wizard', $func );\n\n\t\t\tremove_action( 'admin_enqueue_scripts', array( $this->main, 'enqueue' ) );\n\t\t\tremove_action( 'admin_menu', array( $this->main, 'admin_menu' ) );\n\t\t\tremove_action( 'admin_init', array( $this->main, 'save' ) );\n\n\t\t\t$this->assertEquals( false, has_action( 'admin_enqueue_scripts', array( $this->main, 'enqueue' ) ) );\n\t\t\t$this->assertEquals( false, has_action( 'admin_menu', array( $this->main, 'admin_menu' ) ) );\n\t\t\t$this->assertEquals( false, has_action( 'admin_init', array( $this->main, 'save' ) ) );\n\n\t\t\t$this->main = new LLMS_Admin_Setup_Wizard();\n\n\t\t\t$this->assertEquals( $expect, has_action( 'admin_enqueue_scripts', array( $this->main, 'enqueue' ) ) );\n\t\t\t$this->assertEquals( $expect, has_action( 'admin_menu', array( $this->main, 'admin_menu' ) ) );\n\t\t\t$this->assertEquals( $expect, has_action( 'admin_init', array( $this->main, 'save' ) ) );\n\n\t\t\tremove_filter( 'llms_enable_setup_wizard', $func );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test admin_menu()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_admin_menu() {\n\n\t\t// No user.\n\t\t$this->assertFalse( $this->main->admin_menu() );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertEquals( 'admin_page_llms-setup', $this->main->admin_menu() );\n\n\t\t$this->assertEquals( 'yes', get_option( 'lifterlms_first_time_setup' ) );\n\n\t\t// Clean up.\n\t\tdelete_option( 'lifterlms_first_time_setup' );\n\t\twp_set_current_user( null );\n\n\t}\n\n\t/**\n\t * Test enqueue()\n\t *\n\t * @since 4.8.0\n\t * @since 7.4.0 Added mock request to test for `llms-setup` page.\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue() {\n\n\t\t$this->mockGetRequest( array( 'page' => 'llms-setup' ) );\n\t\t$this->assertTrue( $this->main->enqueue() );\n\n\t}\n\n\t/**\n\t * Test get_completed_url().\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_completed_url() {\n\n\t\t$ids = $this->factory->course->create_many( 3, array( 'sections' => 0 ) );\n\n\t\t// More than one course redirects to the course post table.\n\t\t$this->assertEquals( 'http://example.org/wp-admin/edit.php?post_type=course&orderby=date&order=desc', LLMS_Unit_Test_Util::call_method( $this->main, 'get_completed_url', array( $ids ) ) );\n\t\tunset( $ids[2] );\n\t\t$this->assertEquals( 'http://example.org/wp-admin/edit.php?post_type=course&orderby=date&order=desc', LLMS_Unit_Test_Util::call_method( $this->main, 'get_completed_url', array( $ids ) ) );\n\n\t\t// One course goes to the the course's edit page.\n\t\tunset( $ids[1] );\n\t\t$this->assertEquals( get_edit_post_link( $ids[0], 'not-display' ), LLMS_Unit_Test_Util::call_method( $this->main, 'get_completed_url', array( $ids ) ) );\n\n\t}\n\n\t/**\n\t * Test get_current_step()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_current_step() {\n\n\t\t$this->assertEquals( 'intro', $this->main->get_current_step() );\n\n\t\t$this->mockGetRequest( array( 'step' => 'mock' ) );\n\t\t$this->assertEquals( 'mock', $this->main->get_current_step() );\n\n\t}\n\n\t/**\n\t * Test get_next_step()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_next_step() {\n\n\t\t// Not found.\n\t\t$this->assertFalse( $this->main->get_next_step( 'fake' ) );\n\n\t\t// No next step.\n\t\t$this->assertFalse( $this->main->get_next_step( 'finish' ) );\n\n\t\t$this->assertEquals( 'pages', $this->main->get_next_step( 'intro' ) );\n\n\t\t$this->mockGetRequest( array( 'step' => 'intro' ) );\n\t\t$this->assertEquals( 'pages', $this->main->get_next_step() );\n\n\t}\n\n\n\t/**\n\t * Test get_prev_step()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_prev_step() {\n\n\t\t// Not found.\n\t\t$this->assertFalse( $this->main->get_prev_step( 'fake' ) );\n\n\t\t// No previous step.\n\t\t$this->assertFalse( $this->main->get_prev_step( 'intro' ) );\n\n\t\t$this->assertEquals( 'coupon', $this->main->get_prev_step( 'finish' ) );\n\n\t\t$this->mockGetRequest( array( 'step' => 'finish' ) );\n\t\t$this->assertEquals( 'coupon', $this->main->get_prev_step() );\n\n\t}\n\n\t/**\n\t * Test get_save_text()\n\t *\n\t * @since 4.8.0\n\t * @since 7.4.0 Escaped 'Save & Continue' text.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_save_text() {\n\n\t\t$this->assertEquals( 'Allow', LLMS_Unit_Test_Util::call_method( $this->main, 'get_save_text', array( 'coupon' ) ) );\n\t\t$this->assertEquals( 'Import Courses', LLMS_Unit_Test_Util::call_method( $this->main, 'get_save_text', array( 'finish' ) ) );\n\n\t\t$this->assertEquals( 'Save &amp; Continue', LLMS_Unit_Test_Util::call_method( $this->main, 'get_save_text', array( 'anything-else' )  ));\n\n\t}\n\n\t/**\n\t * Test get_save_text()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_skip_text() {\n\n\t\t$this->assertEquals( 'No thanks', LLMS_Unit_Test_Util::call_method( $this->main, 'get_skip_text', array( 'coupon' ) ) );\n\t\t$this->assertEquals( 'Skip this step', LLMS_Unit_Test_Util::call_method( $this->main, 'get_skip_text', array( 'anything-else' )  ));\n\n\t}\n\n\t/**\n\t * Test get_step_url()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_step_url() {\n\n\t\t$this->assertEquals( 'http://example.org/wp-admin/?page=llms-setup&step=mock', LLMS_Unit_Test_Util::call_method( $this->main, 'get_step_url', array( 'mock' ) ) );\n\n\t}\n\n\t/**\n\t * Test get_steps()\n\t *\n\t * @since 4.8.0\n\t * @since 7.4.0 Updated step value to array and check for title.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_steps() {\n\n\t\t$steps = $this->main->get_steps();\n\t\t$this->assertTrue( is_array( $steps ) );\n\n\t\tforeach ( $steps as $step => $args ) {\n\t\t\t$this->assertTrue( ! empty( $step ) );\n\t\t\t$this->assertTrue( is_array( $args ) );\n\t\t\t$this->assertArrayHasKey( 'title', $args );\n\t\t\t$this->assertTrue( is_string( $step ) );\n\t\t\t$this->assertTrue( is_string( $args['title'] ?? '' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test output()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output() {\n\n\t\t$output = $this->get_output( array( $this->main, 'output' ), array( 'intro' ) );\n\n\t\t$this->assertStringContains( '<div id=\"llms-setup-wizard\">', $output );\n\t\t$this->assertStringContains( '<h1 id=\"llms-logo\">', $output );\n\t\t$this->assertStringContains( '<ul class=\"llms-setup-progress\">', $output );\n\n\t}\n\n\t/**\n\t * Test save() when there are nonce or user permission issues\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_permissions_issues() {\n\n\t\t// No nonce.\n\t\t$this->assertNull( $this->main->save() );\n\n\t\t// Invalid nonce.\n\t\t$data = array(\n\t\t\t'llms_setup_nonce' => 'fake',\n\t\t);\n\t\t$this->mockPostRequest( $data );\n\t\t$this->assertNull( $this->main->save() );\n\n\t\t// Missing user.\n\t\t$data = array(\n\t\t\t'llms_setup_nonce' => wp_create_nonce( 'llms_setup_save' ),\n\t\t);\n\t\t$this->mockPostRequest( $data );\n\t\t$this->assertNull( $this->main->save() );\n\n\t}\n\n\t/**\n\t * Test save() for an invalid step\n\t *\n\t * This test also covers an error response from any valid step.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_invalid_step() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_setup_nonce' => wp_create_nonce( 'llms_setup_save' ),\n\t\t\t'llms_setup_save'  => 'fake-step',\n \t\t) );\n\n\t\t$res = $this->main->save();\n\n\t\t$this->assertIsWpError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms-setup-save-invalid', $res );\n\n\t\t$this->assertEquals( $res, $this->main->error );\n\n\t}\n\n\t/**\n\t * Test save() for success (and redirection)\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_success() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'step' => 'pages',\n\t\t) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_setup_nonce' => wp_create_nonce( 'llms_setup_save' ),\n\t\t\t'llms_setup_save'  => 'pages',\n \t\t) );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( 'http://example.org/wp-admin/?page=llms-setup&step=payments [302] YES' );\n\n\t\t$this->main->save();\n\n\t}\n\n\t/**\n\t * Test save_coupon() when an http error is encountered\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_coupon_http_error() {\n\n\t\t$handler = function( $preempt, $args, $url ) {\n\t\t\tif ( 'https://lifterlms.com/llms-api/tracking' === $url ) {\n\t\t\t\treturn new WP_Error( 'mock-err', 'Error' );\n\t\t\t}\n\t\t\treturn $preempt;\n\t\t};\n\n\t\tadd_filter( 'pre_http_request', $handler, 10, 3 );\n\n\t\t$ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' );\n\n\t\t$this->assertIsWpError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'mock-err', $ret );\n\n\t\tremove_filter( 'pre_http_request', $handler, 10 );\n\n\t}\n\n\t/**\n\t * Test save_coupon() when the tracking data api returns an error\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_coupon_api_error() {\n\n\t\t$handler = function( $preempt, $args, $url ) {\n\t\t\tif ( 'https://lifterlms.com/llms-api/tracking' === $url ) {\n\t\t\t\treturn array( 'body' => json_encode( array( 'success' => false, 'message' => 'Server error' ) ) );\n\t\t\t}\n\t\t\treturn $preempt;\n\t\t};\n\n\t\tadd_filter( 'pre_http_request', $handler, 10, 3 );\n\n\t\t$ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' );\n\n\t\t$this->assertIsWpError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-setup-coupon-save-tracking-api', $ret );\n\n\t\tremove_filter( 'pre_http_request', $handler, 10 );\n\n\t}\n\n\t/**\n\t * Test save_coupon() when the tracking data api returns data in an unexpected format\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_coupon_unknown_error() {\n\n\t\t$handler = function( $preempt, $args, $url ) {\n\t\t\tif ( 'https://lifterlms.com/llms-api/tracking' === $url ) {\n\t\t\t\treturn array( 'body' => json_encode( array() ) );\n\t\t\t}\n\t\t\treturn $preempt;\n\t\t};\n\n\t\tadd_filter( 'pre_http_request', $handler, 10, 3 );\n\n\t\t$ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' );\n\n\t\t$this->assertIsWpError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-setup-coupon-save-unknown', $ret );\n\n\t\tremove_filter( 'pre_http_request', $handler, 10 );\n\n\t}\n\n\t/**\n\t * Test save_coupon() success\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_coupon_success() {\n\n\t\tdelete_option( 'llms_allow_tracking' );\n\t\t$handler = function( $preempt, $args, $url ) {\n\t\t\tif ( 'https://lifterlms.com/llms-api/tracking' === $url ) {\n\t\t\t\treturn array( 'body' => json_encode( array( 'success' => true, 'message' => '' ) ) );\n\t\t\t}\n\t\t\treturn $preempt;\n\t\t};\n\n\t\tadd_filter( 'pre_http_request', $handler, 10, 3 );\n\n\t\t$ret = LLMS_Unit_Test_Util::call_method( $this->main, 'save_coupon' );\n\n\t\t$this->assertTrue( $ret );\n\t\t$this->assertEquals( 'yes', get_option( 'llms_allow_tracking' ) );\n\n\t\tremove_filter( 'pre_http_request', $handler, 10 );\n\n\t}\n\n\t/**\n\t * Test save_finish() when no import ids are provided\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_finish_error_no_ids() {\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' ) );\n\n\t}\n\n\t/**\n\t * Test save_finish() when an export api error occurs\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_finish_error_api() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_setup_course_import_ids' => array( 1 ),\n\t\t) );\n\n\t\t$handler = function( $res ) {\n\t\t\treturn new WP_Error( 'mock', 'Mocked API response.' );\n\t\t};\n\t\tadd_filter( 'pre_http_request', $handler );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' );\n\t\t$this->assertIsWpError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'mock', $res );\n\n\t\tremove_filter( 'pre_http_request', $handler );\n\n\t}\n\n\t/**\n\t * Test save_finish() when an error is encountered during generation\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_finish_error_generator() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_setup_course_import_ids' => array( 1 ),\n\t\t) );\n\n\t\t$handler = function( $res ) {\n\t\t\treturn array();\n\t\t};\n\t\tadd_filter( 'pre_http_request', $handler );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' );\n\t\t$this->assertIsWpError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'missing-generator', $res );\n\n\t\tremove_filter( 'pre_http_request', $handler );\n\n\t}\n\n\t/**\n\t * Test save_finish() for success\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_finish_success() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_setup_course_import_ids' => array( 33579 ), // Free course template.\n\t\t) );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'save_finish' );\n\n\t\tforeach ( $res as $id ) {\n\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t\t$this->assertEquals( 'course', get_post_type( $id ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test save_pages()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_pages() {\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'save_pages' ) );\n\n\t}\n\n\t/**\n\t * Test save_payments()\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_payments() {\n\n\t\t// With values submitted.\n\t\t$this->mockPostRequest( array(\n\t\t\t'country'         => 'MOCK',\n\t\t\t'currency'        => 'CURR',\n\t\t\t'manual_payments' => 'yes'\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'save_payments' ) );\n\n\t\t$this->assertEquals( 'MOCK', get_option( 'lifterlms_country' ) );\n\t\t$this->assertEquals( 'CURR', get_option( 'lifterlms_currency' ) );\n\t\t$this->assertEquals( 'yes', get_option( 'llms_gateway_manual_enabled' ) );\n\n\t\tdelete_option( 'lifterlms_country' );\n\t\tdelete_option( 'lifterlms_currency' );\n\t\tdelete_option( 'llms_gateway_manual_enabled' );\n\n\t\t// No values, use the defaults.\n\t\t$this->mockPostRequest( array() );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'save_payments' ) );\n\n\t\t$this->assertEquals( 'US', get_option( 'lifterlms_country' ) );\n\t\t$this->assertEquals( 'USD', get_option( 'lifterlms_currency' ) );\n\t\t$this->assertEquals( 'no', get_option( 'llms_gateway_manual_enabled' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-admin-users-table.php",
    "content": "<?php\n/**\n * Test LLMS_Admin_Users_Table class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group users_table\n *\n * @since 4.0.0\n */\nclass LLMS_Test_Admin_Users_table extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 4.0.0\n\t * @since 4.7.0 Add `LLMS_Admin_Reporting` class.\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/class.llms.admin.reporting.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-users-table.php';\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\tset_current_screen( 'users.php' );\n\t\t$this->main = new LLMS_Admin_Users_Table();\n\n\t}\n\n\n\t/**\n\t * Teardown the test case\n\t *\n\t * @since 4.0.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\n\t\t/**\n\t\t * Reset current screen\n\t\t *\n\t\t * I can't find anything officially documenting the proper way to do this but this line seems to indicate\n\t\t * you can reset it by using `front` as the current screen:\n\t\t *\n\t\t * https://core.trac.wordpress.org/browser/tags/5.4/src/wp-admin/includes/class-wp-screen.php#L277\n\t\t *\n\t\t * Without this, tests following theses tests these tests which use function that have `is_admin()` calls in them\n\t\t * may fail because `is_admin()` would otherwise return `true` on PHP 7.3 and lower and WP 5.2 or lower.\n\t\t */\n\t\tset_current_screen( 'front' );\n\n\t}\n\n\t/**\n\t * Test add_actions() method\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_actions() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\t\t$res  = $this->main->add_actions( array(), $user );\n\n\t\t$this->assertArrayHasKey( 'llms-reporting', $res );\n\n\t\t$this->assertStringContains( 'page=llms-reporting', $res['llms-reporting'] );\n\t\t$this->assertStringContains( 'tab=students', $res['llms-reporting'] );\n\t\t$this->assertStringContains( 'student_id=' . $user->ID, $res['llms-reporting'] );\n\n\t}\n\n\t/**\n\t * Test add_cols()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_cols() {\n\n\t\t$this->assertEquals( array(\n\t\t\t'llms-last-login'  => 'Last Login',\n\t\t\t'llms-enrollments' => 'Enrollments',\n\t\t), $this->main->add_cols( array() ) );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-export-api.php",
    "content": "<?php\n/**\n * Test export api class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group export_api\n *\n * @since 4.8.0\n */\nclass LLMS_Test_Export_API extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup before class.\n\t *\n\t * @since 4.8.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-export-api.php';\n\t}\n\n\t/**\n\t * Test get() when a request error is encountered.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_conn_error() {\n\n\t\t$handler = function( $res ) {\n\t\t\treturn new WP_Error( 'mocked', 'Mocked error' );\n\t\t};\n\n\t\tadd_filter( 'pre_http_request', $handler );\n\n\t\t$res = LLMS_Export_API::get( array( 1 ) );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'mocked', $res );\n\n\t\tremove_filter( 'pre_http_request', $handler );\n\n\t}\n\n\t/**\n\t * Test get() when an API error is encountered (404)\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_api_error() {\n\n\t\t$res = LLMS_Export_API::get( array( 1 ) );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'not-found', $res );\n\n\t}\n\n\t/**\n\t * Test get() for success response\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_success() {\n\n\t\t$res = LLMS_Export_API::get( array( 33579 ) ); // Free course lead magnet template.\n\n\t\t$this->assertEquals( 'LifterLMS/BulkCourseExporter', $res['_generator'] );\n\t\t$this->assertArrayHasKey( 33579, $res['courses'] );\n\n\t}\n\n\t/**\n\t * Test list() when a request error is encountered.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_list_conn_error() {\n\n\t\t$handler = function( $res ) {\n\t\t\treturn new WP_Error( 'mocked', 'Mocked error' );\n\t\t};\n\n\t\tadd_filter( 'pre_http_request', $handler );\n\n\t\t$res = LLMS_Export_API::list();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'mocked', $res );\n\n\t\tremove_filter( 'pre_http_request', $handler );\n\n\t}\n\n\t/**\n\t * Test list() for success response\n\t *\n\t * @since 4.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_list_success() {\n\n\t\t$list = LLMS_Export_API::list();\n\n\t\t$this->assertTrue( is_array( $list ) );\n\n\t\tforeach ( $list as $res ) {\n\t\t\t$this->assertEquals( array( 'id', 'description', 'image', 'title' ), array_keys( $res ) );\n\t\t}\n\n\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/class-llms-test-sendwp.php",
    "content": "<?php\n/**\n * Test SendWP Connector\n *\n * @package LifterLMS/Tests\n *\n * @group sendwp\n *\n * @since 3.36.1\n * @since 3.37.0 Add testing for nonce verifications.\n * @since 3.40.0 Added additional coverage.\n */\nclass LLMS_Test_SendWP extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * @var LLMS_SendWP\n\t */\n\tprotected $sendwp;\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 3.40.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/abstracts/llms-abstract-email-provider.php';\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-sendwp.php';\n\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.36.1\n\t * @since 3.40.0 Include class file via `set_up_before_class()`.\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->sendwp = new LLMS_SendWP();\n\n\t}\n\n\t/**\n\t * Tear down the testcase.\n\t *\n\t * @since 3.36.1\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tdelete_plugins( array( 'sendwp/sendwp.php' ) );\n\n\t}\n\n\t/**\n\t * Test the add_settings() method.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_settings() {\n\n\t\t// No settings for anyone without the `install_plugins` cap.\n\t\t$this->assertEquals( array(), $this->sendwp->add_settings( array() ) );\n\n\t\t// Admin can see the settings.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$res = $this->sendwp->add_settings( array() );\n\t\t$this->assertEquals( array( 'sendwp_title', 'sendwp_connect' ), wp_list_pluck( $res, 'id' ) );\n\n\t}\n\n\t/**\n\t * Test do_remote_install() error with no nonce submitted.\n\t *\n\t * @since 3.37.0\n\t * @since 6.0.0 Changed {@see LLMS_SendWP::do_remote_install()} access from public to protected.\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_do_remote_install_no_nonce() {\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->sendwp, 'do_remote_install' );\n\n\t\t$this->assertArrayHasKey( 'message', $res );\n\t\t$this->assertEquals( 'llms_sendwp_install_nonce_failure', $res['code'] );\n\t\t$this->assertEquals( 401, $res['status'] );\n\n\t}\n\n\t/**\n\t * Test do_remote_install() error for no user.\n\t *\n\t * @since 3.36.1\n\t * @since 3.37.0 Add mock nonce to test.\n\t * @since 6.0.0 Changed {@see LLMS_SendWP::do_remote_install()} access from public to protected.\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_do_remote_install_no_user() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_sendwp_nonce' => wp_create_nonce( 'llms-sendwp-install' ),\n\t\t) );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->sendwp, 'do_remote_install' );\n\n\t\t$this->assertArrayHasKey( 'message', $res );\n\t\t$this->assertEquals( 'llms_sendwp_install_unauthorized', $res['code'] );\n\t\t$this->assertEquals( 403, $res['status'] );\n\n\t}\n\n\t/**\n\t * Test do_remote_install() error with plugins api.\n\t *\n\t * @since 3.36.1\n\t * @since 3.37.0 Add mock nonce to test.\n\t * @since 6.0.0 Changed {@see LLMS_SendWP::do_remote_install()} access from public to protected.\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_do_remote_install_plugins_api_error() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_sendwp_nonce' => wp_create_nonce( 'llms-sendwp-install' ),\n\t\t) );\n\n\t\t$handler = function( $ret, $action, $args ) {\n\t\t\treturn new WP_Error( 'plugins_api_failed', 'Error' );\n\t\t};\n\t\tadd_filter( 'plugins_api', $handler, 10, 3 );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->sendwp, 'do_remote_install' );\n\t\tremove_filter( 'plugins_api', $handler, 10 );\n\n\t\t$this->assertArrayHasKey( 'message', $res );\n\t\t$this->assertEquals( 'plugins_api_failed', $res['code'] );\n\t\t$this->assertEquals( 400, $res['status'] );\n\n\t}\n\n\t/**\n\t * Test do remote install success.\n\t *\n\t * @since 3.36.1\n\t * @since 3.37.0 Add mock nonce to test.\n\t * @since 6.0.0 Changed {@see LLMS_SendWP::do_remote_install()} access from public to protected.\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_do_remote_install_success() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_sendwp_nonce' => wp_create_nonce( 'llms-sendwp-install' ),\n\t\t) );\n\n\t\t// Install.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->sendwp, 'do_remote_install' );\n\t\t$this->assertEquals( array( 'success', ), array_keys( $res ) );\n\n\t\t// Already installed, activate.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->sendwp, 'do_remote_install_verify' );\n\t\t$this->assertEquals( array( 'partner_id', 'register_url', 'client_name', 'client_secret', 'client_redirect', ), array_keys( $res ) );\n\t\t$this->assertEquals( 2007, $res['partner_id'] );\n\n\t}\n\n\t/**\n\t * Test get_connect_setting()\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_get_connect_setting() {\n\n\t\t// Not connected.\n\t\t$this->assertStringContains( 'id=\"llms-sendwp-connect\"', LLMS_Unit_Test_Util::call_method( $this->sendwp, 'get_connect_setting' ) );\n\n\t\t// Connected and forwarding.\n\t\tupdate_option( 'sendwp_client_connected', '1' );\n\t\t$this->assertStringContains( 'Manage your account', LLMS_Unit_Test_Util::call_method( $this->sendwp, 'get_connect_setting' ) );\n\n\t\t// Connected and not forwarding.\n\t\tupdate_option( 'sendwp_forwarding_enabled', '0' );\n\t\t$this->assertStringContains( 'Email sending is currently disabled', LLMS_Unit_Test_Util::call_method( $this->sendwp, 'get_connect_setting' ) );\n\n\t}\n\n\t/**\n\t * Test should_output_inline() method.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_should_output_inline() {\n\n\t\t// No user.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Wrong screen.\n\t\tset_current_screen( 'admin' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) );\n\n\t\t// Mock screen.\n\t\tset_current_screen( 'lifterlms_page_llms-settings' );\n\n\t\t// Right screen, wrong tab.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) );\n\n\t\t// Right screen, right tab, is connected.\n\t\tupdate_option( 'sendwp_client_connected', '1' );\n\t\t$this->mockGetRequest( array( 'tab' => 'engagements' ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) );\n\n\t\t// Right screen, right tab, not connected.\n\t\tupdate_option( 'sendwp_client_connected', '0' );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->sendwp, 'should_output_inline' ) );\n\n\t\tset_current_screen( 'front' );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/class-llms-test-llms-admin-meta-boxes.php",
    "content": "<?php\n/**\n * Test Admin Notices Class\n *\n * @package LifterLMS/Tests/Admin\n *\n * @group admin\n * @group metaboxes\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Admin_Meta_Boxes extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/class.llms.meta.boxes.php';\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Meta_Boxes();\n\n\t}\n\n\t/**\n\t * Test maybe_modify_post_thumbnail_html().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_modify_post_thumbnail_html() {\n\n\t\t$types = array(\n\t\t\t'post'             => false,\n\t\t\t'llms_achievement' => true,\n\t\t\t'llms_certificate' => true,\n\t\t);\n\n\t\tforeach ( $types as $post_type => $modified ) {\n\n\t\t\t$post = $this->factory->post->create( compact( 'post_type' ) );\n\n\t\t\t// Without image.\n\t\t\t$res = $this->main->maybe_modify_post_thumbnail_html( 'Content', $post, '' );\n\n\t\t\tif ( $modified ) {\n\t\t\t\t$this->assertStringContainsString( 'Using the global default.', $res );\n\t\t\t\t$this->assertStringContainsString( '<img ', $res );\n\t\t\t\t$this->assertStringContainsString( 'Content', $res );\n\t\t\t} else {\n\t\t\t\t$this->assertEquals( 'Content', $res );\n\t\t\t}\n\n\t\t\t// With an image.\n\t\t\t$res = $this->main->maybe_modify_post_thumbnail_html( 'Content', $post, 123 );\n\t\t\t$this->assertEquals( 'Content', $res );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test maybe_modify_title_placeholder().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_modify_title_placeholder() {\n\n\t\t$types = array(\n\t\t\t'post'             => 'Default Placeholder',\n\t\t\t'llms_achievement' => 'Default Placeholder (for internal use only)',\n\t\t\t'llms_certificate' => 'Default Placeholder (for internal use only)',\n\t\t);\n\n\t\tforeach ( $types as $post_type => $expect ) {\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\t\t\t$this->assertEquals( $expect, $this->main->maybe_modify_title_placeholder( 'Default Placeholder', $post ) );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-access.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Order Metabox\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_access\n * @group admin\n * @group metaboxes\n * @group metaboxes_post_type\n *\n * @since 3.36.1\n * @version 3.36.1\n */\nclass LLMS_Test_Meta_Box_Access extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test\n\t *\n\t * @since 3.36.1\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Access();\n\n\t}\n\n\t/**\n\t * Test the get_screens() method.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screens() {\n\n\t\t$this->assertEquals( array( 'post', 'page' ), $this->metabox->get_screens() );\n\n\t}\n\n\t/**\n\t * Save with no user should fail.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_save_no_user() {\n\n\t\t$post = $this->factory->post->create();\n\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Save with no nonce should fail.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_save_no_nonce() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$post = $this->factory->post->create();\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Save with invalid nonce will fail.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_save_invalid_nonce() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$post = $this->factory->post->create();\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(), false ) );\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\n\t}\n\n\t/**\n\t * Test save method.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_save() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$post = $this->factory->post->create();\n\t\t$post_data = $this->add_nonce_to_array( array() );\n\n\t\t// Nothing saved, value is reset.\n\t\t$this->mockPostRequest( $post_data );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\t\t$this->assertEquals( '', get_post_meta( $post, '_llms_is_restricted', true ) );\n\n\t\t// Toggle restrictions on.\n\t\t$post_data['_llms_is_restricted'] = 'yes';\n\t\t$this->mockPostRequest( $post_data );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\t\t$this->assertEquals( 'yes', get_post_meta( $post, '_llms_is_restricted', true ) );\n\n\t\t// Restrict to a single membership.\n\t\t$post_data['_llms_restricted_levels'] = array( 1 );\n\t\t$this->mockPostRequest( $post_data );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\t\t$this->assertEquals( 'yes', get_post_meta( $post, '_llms_is_restricted', true ) );\n\t\t$this->assertEquals( array( 1 ), get_post_meta( $post, '_llms_restricted_levels', true ) );\n\n\t\t// Multiple memberships.\n\t\t$post_data['_llms_restricted_levels'] = array( 2, 3 );\n\t\t$this->mockPostRequest( $post_data );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\t\t$this->assertEquals( 'yes', get_post_meta( $post, '_llms_is_restricted', true ) );\n\t\t$this->assertEquals( array( 2, 3 ), get_post_meta( $post, '_llms_restricted_levels', true ) );\n\n\t\t// Disable restrictions.\n\t\tunset( $post_data['_llms_is_restricted'] );\n\t\t$this->mockPostRequest( $post_data );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post ) ) );\n\t\t$this->assertEquals( '', get_post_meta( $post, '_llms_is_restricted', true ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-achievement-sync.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Achievement Sync Meta Box.\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_achievement_sync\n * @group admin\n * @group metaboxes\n * @group metaboxes_post_type\n *\n * @since 6.0.0\n * @version 6.0.0\n */\nclass LLMS_Test_Meta_Box_Achievement_Sync extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * @var LLMS_Meta_Box_Achievement_Sync\n\t */\n\tprivate $metabox;\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Achievement_Sync();\n\t}\n\n\t/**\n\t * Tear down test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\t// Reset current screen.\n\t\tllms_tests_reset_current_screen();\n\t}\n\n\n\t/**\n\t * Test the get_screens() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screens() {\n\n\t\t$this->assertEquals(\n\t\t\tarray( 'llms_achievement', 'llms_my_achievement' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'get_screens' )\n\t\t);\n\t}\n\n\t/**\n\t * Test sync awarded achievement action.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_action_achievement() {\n\n\t\t$action = 'action=sync_awarded_achievement';\n\n\t\t$post                = $this->factory->post->create_and_get();\n\t\t$this->metabox->post = $post;\n\t\t$this->metabox->configure();\n\n\t\t// Not llms_my_achievement post type.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t$my_achievement      = $this->factory->post->create_and_get( array( 'post_type' => 'llms_my_achievement' ) );\n\t\t$this->metabox->post = $my_achievement;\n\t\t$this->metabox->configure();\n\n\t\t// llms_my_achievement post type but no achievement template parent.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Set a template which is not an `llms_achievement`.\n\t\t$template = $this->factory->post->create_and_get();\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'          => $my_achievement->ID,\n\t\t\t\t'post_parent' => $template->ID,\n\t\t\t)\n\t\t);\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Set a template which is a `llms_achievement`.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'        => $template->ID,\n\t\t\t\t'post_type' => 'llms_achievement',\n\t\t\t)\n\t\t);\n\t\t$this->assertStringContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Delete created posts.\n\t\tforeach ( array( $post, $my_achievement, $template ) as $to_delete ) {\n\t\t\twp_delete_post( $to_delete->ID );\n\t\t}\n\t}\n\n\t/**\n\t * Test sync awarded achievements action.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_action_achievements() {\n\n\t\t$action = 'action=sync_awarded_achievements';\n\n\t\t$post                = $this->factory->post->create_and_get();\n\t\t$this->metabox->post = $post;\n\t\t$this->metabox->configure();\n\n\t\t// Not llms_achievement post type.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\t\twp_delete_post( $post->ID, true );\n\n\t\t$post                = $this->factory->post->create_and_get( array( 'post_type' => 'llms_achievement' ) );\n\t\t$this->metabox->post = $post;\n\t\t$this->metabox->configure();\n\n\t\t// llms_achievement post type but no awarded achievements.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t$awarded_achievements = array();\n\n\t\t// Create various awarded achievements but with a different template.\n\t\tforeach ( get_available_post_statuses( 'llms_my_achievement' ) as $status ) {\n\t\t\t$awarded_achievements[] = $this->factory->post->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t\t'post_parent' => 999,\n\t\t\t\t\t'post_status' => $status,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Create various awarded achievements: only 3 of them have the required post_status (draft, future and publish).\n\t\tforeach ( get_available_post_statuses( 'llms_my_achievement' ) as $status ) {\n\t\t\t$awarded_achievements[] = $this->factory->post->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t\t'post_parent' => $post->ID,\n\t\t\t\t\t'post_status' => $status,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t$this->assertStringContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t$this->assertStringContainsString(\n\t\t\t'3 awarded achievements',\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Delete created posts.\n\t\tforeach ( array_merge( $awarded_achievements, array( $post->ID ) ) as $to_delete ) {\n\t\t\twp_delete_post( $to_delete );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-achievement.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Achievement Metabox.\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_achievement\n * @group admin\n * @group metaboxes\n * @group metaboxes_post_type\n *\n * @since 6.0.0\n * @version 6.0.0\n */\nclass LLMS_Test_Meta_Box_Achievement extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Achievement();\n\n\t}\n\n\t/**\n\t * Test the get_screens() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screens() {\n\n\t\t$this->assertEquals( array( 'llms_achievement', 'llms_my_achievement' ), LLMS_Unit_Test_Util::call_method( $this->metabox, 'get_screens' ) );\n\n\t}\n\n\t/**\n\t * Test get fields.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_fields_template() {\n\n\t\t$this->metabox->post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_achievement' ) );\n\n\t\t$this->assertEqualSets(\n\t\t\tarray(\n\t\t\t\t'_llms_achievement_title',\n\t\t\t\t'_llms_achievement_content',\n\t\t\t),\n\t\t\tarray_column(\n\t\t\t\t$this->metabox->get_fields()[0]['fields'],\n\t\t\t\t'id'\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test get fields.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_fields_award() {\n\n\t\t$this->metabox->post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_my_achievement' ) );\n\n\t\t$this->assertEqualSets(\n\t\t\tarray(\n\t\t\t\t'_llms_achievement_content',\n\t\t\t),\n\t\t\tarray_column(\n\t\t\t\t$this->metabox->get_fields()[0]['fields'],\n\t\t\t\t'id'\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test save achievement content in the post_content.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_field_db() {\n\n\t\t// Set-up global post.\n\t\tglobal $post;\n\t\t$original_post = $post;\n\n\t\t// Set current user to an admin.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\tforeach ( LLMS_Unit_Test_Util::call_method( $this->metabox, 'get_screens' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\n\t\t\t$this->metabox->post = $post;\n\n\t\t\t$content = 'Some content to save';\n\n\t\t\t// Simulate saving with the achievement_content field set.\n\t\t\t$updates = array(\n\t\t\t\t$this->metabox->prefix . 'achievement_content' => $content,\n\t\t\t);\n\t\t\t$this->mockPostRequest( $this->add_nonce_to_array( $updates ) );\n\n\t\t\t// Save.\n\t\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->metabox, 'save', array( $post->ID ) ) );\n\t\t\t// Refresh post.\n\t\t\t$post = get_post( $post->ID );\n\n\t\t\t// Skip backwards compat function so we can ensure the postmeta is truly not saved.\n\t\t\tremove_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\t\t\t$this->assertEquals( '', get_post_meta( $post->ID, $this->metabox->prefix . 'achievement_content', true ) );\n\t\t\tadd_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\n\t\t\t$this->assertEquals( $content, $post->post_content );\n\n\t\t}\n\n\t\t// Reset global post.\n\t\t$post = $original_post;\n\t\t// Reset current user.\n\t\twp_set_current_user( 0 );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-award-engagement-submit.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Award Engagement Submit Meta Box.\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_award_engagement\n * @group admin\n * @group metaboxes\n * @group metaboxes_post_type\n *\n * @since 6.0.0\n * @version 6.0.0\n */\nclass LLMS_Test_Meta_Box_Award_Engagement_Submit extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Award_Engagement_Submit();\n\n\t}\n\n\t/**\n\t * Tear down test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\t// Reset current screen.\n\t\tllms_tests_reset_current_screen();\n\t}\n\n\t/**\n\t * Test the get_screens() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screens() {\n\n\t\t$this->assertEquals(\n\t\t\tarray( 'llms_my_achievement', 'llms_my_certificate' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'get_screens' )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test get fields.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_fields() {\n\n\t\t$this->assertEquals(\n\t\t\tarray(),\n\t\t\t$this->metabox->get_fields()\n\t\t);\n\n\t}\n\n\t/**\n\t * Test current_student_id() method on creation passing no params.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_current_student_id_no_param_on_new_post() {\n\n\t\tforeach ( $this->metabox->screens as $post_type ) {\n\t\t\t$this->metabox->post = $this->factory->post->create_and_get(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => $post_type,\n\t\t\t\t\t'post_author' =>  2, // Student.\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Set current screen to new post.\n\t\t\tset_current_screen( 'post-new.php' );\n\t\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->metabox, 'current_student_id' ), $post_type );\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test current_student_id() method passing `true` as `$creating` param.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_current_student_creating_param() {\n\n\t\tforeach ( $this->metabox->screens as $post_type ) {\n\t\t\t$this->metabox->post = $this->factory->post->create_and_get(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => $post_type,\n\t\t\t\t\t'post_author' =>  2, // Student.\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Set current screen to edit post.\n\t\t\tset_current_screen( 'edit.php' );\n\n\t\t\t// Pass creating=true.\n\t\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->metabox, 'current_student_id', array( true ) ), $post_type );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test current_student_id() when editing an awarded engagement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_current_student_edit_awarded_engagement() {\n\n\n\t\tforeach ( $this->metabox->screens as $post_type ) {\n\t\t\t$this->metabox->post = $this->factory->post->create_and_get(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => $post_type,\n\t\t\t\t\t'post_author' =>  2, // Student.\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Set current screen to edit post.\n\t\t\tset_current_screen( 'edit.php' );\n\n\t\t\t// Edit a certificate with assigned student id.\n\t\t\t$this->assertEquals( 2, LLMS_Unit_Test_Util::call_method( $this->metabox, 'current_student_id' ), $post_type );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test current_student_id() when creating an awarded engagement passing the student id via GET.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_current_student_create_awarded_engagement_passing_student_id_via_get() {\n\n\t\tforeach ( $this->metabox->screens as $post_type ) {\n\t\t\t$this->metabox->post = $this->factory->post->create_and_get(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => $post_type,\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Pass the ID of the student who's awarding the engagement.\n\t\t\t$this->mockGetRequest(\n\t\t\t\tarray(\n\t\t\t\t\t'sid' => 12\n\t\t\t\t),\n\t\t\t);\n\n\t\t\t// Set current screen to create.\n\t\t\tset_current_screen( 'post-new.php' );\n\t\t\t$this->assertEquals( 12, LLMS_Unit_Test_Util::call_method( $this->metabox, 'current_student_id' ), $post_type );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test current_student_id() when editing an already awarded engagement passing the student id via GET.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_current_student_edit_awarded_engagement_passing_student_id_via_get() {\n\n\t\tforeach ( $this->metabox->screens as $post_type ) {\n\t\t\t$this->metabox->post = $this->factory->post->create_and_get(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => $post_type,\n\t\t\t\t\t'post_author' => 23\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Pass the ID of the student who's awarding the engagement.\n\t\t\t$this->mockGetRequest(\n\t\t\t\tarray(\n\t\t\t\t\t'sid' => 12\n\t\t\t\t),\n\t\t\t);\n\n\t\t\t// Set current screen to edit post.\n\t\t\tset_current_screen( 'edit.php' );\n\n\t\t\t// Edit a certificate with assigned student id.\n\t\t\t$this->assertEquals( 23, LLMS_Unit_Test_Util::call_method( $this->metabox, 'current_student_id' ), $post_type );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-certificate-sync.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Certificate Sync Meta Box.\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_certificate_sync\n * @group admin\n * @group metaboxes\n * @group metaboxes_post_type\n *\n * @since 6.0.0\n * @version 6.0.0\n */\nclass LLMS_Test_Meta_Box_Certificate_Sync extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * @var LLMS_Meta_Box_Certificate_Sync\n\t */\n\tprivate $metabox;\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Certificate_Sync();\n\t}\n\n\t/**\n\t * Tear down test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\t// Reset current screen.\n\t\tllms_tests_reset_current_screen();\n\t}\n\n\n\t/**\n\t * Test the get_screens() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screens() {\n\n\t\t$this->assertEquals(\n\t\t\tarray( 'llms_certificate', 'llms_my_certificate' ),\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'get_screens' )\n\t\t);\n\t}\n\n\t/**\n\t * Test sync awarded certificate action.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_action_certificate() {\n\n\t\t$action = 'action=sync_awarded_certificate';\n\n\t\t$post                = $this->factory->post->create_and_get();\n\t\t$this->metabox->post = $post;\n\t\t$this->metabox->configure();\n\n\t\t// Not llms_my_certificate post type.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t$my_certificate      = $this->factory->post->create_and_get( array( 'post_type' => 'llms_my_certificate' ) );\n\t\t$this->metabox->post = $my_certificate;\n\t\t$this->metabox->configure();\n\n\t\t// llms_my_certificate post type but no certificate template parent.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Set a template which is not an `llms_certificate`.\n\t\t$template = $this->factory->post->create_and_get();\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'          => $my_certificate->ID,\n\t\t\t\t'post_parent' => $template->ID,\n\t\t\t)\n\t\t);\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Set a template which is a `llms_certificate`.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'        => $template->ID,\n\t\t\t\t'post_type' => 'llms_certificate',\n\t\t\t)\n\t\t);\n\t\t$this->assertStringContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Delete created posts.\n\t\tforeach ( array( $post, $my_certificate, $template ) as $to_delete ) {\n\t\t\twp_delete_post( $to_delete->ID );\n\t\t}\n\t}\n\n\t/**\n\t * Test sync awarded certificates action.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_action_certificates() {\n\n\t\t$action = 'action=sync_awarded_certificates';\n\n\t\t$post                = $this->factory->post->create_and_get();\n\t\t$this->metabox->post = $post;\n\t\t$this->metabox->configure();\n\n\t\t// Not llms_certificate post type.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\t\twp_delete_post( $post->ID, true );\n\n\t\t$post                = $this->factory->post->create_and_get( array( 'post_type' => 'llms_certificate' ) );\n\t\t$this->metabox->post = $post;\n\t\t$this->metabox->configure();\n\n\t\t// llms_certificate post type but no awarded certificates.\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t$awarded_certificates = array();\n\n\t\t// Create various awarded certificates but with a different template.\n\t\tforeach ( get_available_post_statuses( 'llms_my_certificate' ) as $status ) {\n\t\t\t$awarded_certificates[] = $this->factory->post->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t\t'post_parent' => 999,\n\t\t\t\t\t'post_status' => $status,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\t$this->assertStringNotContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Create various awarded certificates: only 3 of them have the required post_status (draft, future and publish).\n\t\tforeach ( get_available_post_statuses( 'llms_my_certificate' ) as $status ) {\n\t\t\t$awarded_certificates[] = $this->factory->post->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t\t'post_parent' => $post->ID,\n\t\t\t\t\t'post_status' => $status,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t$this->assertStringContainsString(\n\t\t\t$action,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t$this->assertStringContainsString(\n\t\t\t'3 awarded certificates',\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->metabox, 'sync_action' )\n\t\t);\n\n\t\t// Delete created posts.\n\t\tforeach ( array_merge( $awarded_certificates, array( $post->ID ) ) as $to_delete ) {\n\t\t\twp_delete_post( $to_delete );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-certificate.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Certificate Metabox.\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_certificate\n * @group admin\n * @group metaboxes\n * @group metaboxes_post_type\n *\n * @since 6.0.0\n * @version 6.0.0\n */\nclass LLMS_Test_Meta_Box_Certificate extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Certificate();\n\n\t}\n\n\t/**\n\t * Test the get_screens() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_screens() {\n\n\t\t$this->assertEquals( array( 'llms_certificate' ), LLMS_Unit_Test_Util::call_method( $this->metabox, 'get_screens' ) );\n\n\t}\n\n\t/**\n\t * Test get fields.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_fields() {\n\n\t\t$this->metabox->post = $this->factory->post->create_and_get(\n\t\t\tarray( 'post_type' => 'llms_certificate' )\n\t\t);\n\n\t\t$this->assertEqualSets(\n\t\t\tarray(\n\t\t\t\t'_llms_certificate_title',\n\t\t\t\t'_llms_sequential_id',\n\t\t\t),\n\t\t\tarray_column(\n\t\t\t\t$this->metabox->get_fields()[0]['fields'],\n\t\t\t\t'id'\n\t\t\t)\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Order Metabox\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_lesson\n * @group admin\n * @group metaboxes\n * @group metaboxes_post_type\n *\n * @since 3.36.2\n * @version 3.36.2\n */\nclass LLMS_Test_Meta_Box_Lesson extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test\n\t *\n\t * @since 3.36.2\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Lesson();\n\n\t}\n\n\t/**\n\t * Test get fields.\n\t *\n\t * @since 3.36.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_fields() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] );\n\t\t$lesson = llms_get_post( $course->get_lessons( 'ids' )[0] );\n\t\t$post   = $lesson->get( 'post' );\n\t\t$this->metabox->post = $post;\n\n\t\t// check the lessons Drip Settings methods list does not cointain 'start',\n\t\t// as the course has no start date set.\n\t\tforeach ( $this->metabox->get_fields() as $index => $f ) {\n\t\t\tif ( 'Drip Settings' === $f['title'] ) {\n\t\t\t\t$this->assertFalse( array_key_exists( 'start', $f['fields'][1]['value'] ) );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// set a course start date.\n\t\t$course->set( 'start_date', current_time( 'm/d/Y' ) );\n\t\t// check the lessons Drip Settings methods list contains 'start',\n\t\t// as the course now has a start date set.\n\t\tforeach ( $this->metabox->get_fields() as $index => $f ) {\n\t\t\tif ( 'Drip Settings' === $f['title'] ) {\n\t\t\t\t$this->assertTrue( array_key_exists( 'start', $f['fields'][1]['value'] ) );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-details.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Order Metabox\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group metaboxes\n * @group order_details\n * @group metaboxes_post_type\n *\n * @since 5.3.0\n */\nclass LLMS_Test_Meta_Box_Order_Details extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Meta_Box_Order_Details();\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t}\n\n\t/**\n\t * Test save() nonce-related errors\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_errs_nonce() {\n\n\t\t// No nonce.\n\t\t$this->assertEquals( -1, $this->main->save( 123 ) );\n\n\t\t// Invalid nonce.\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(), false ) );\n\t\t$this->assertEquals( -1, $this->main->save( 123 ) );\n\n\t}\n\n\t/**\n\t * Test save() with an invalid order.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_order_err() {\n\n\t\t$post_id = $this->factory->post->create();\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array() ) );\n\n\t\t// Not an order post type.\n\t\t$this->assertEquals( 0, $this->main->save( $post_id ) );\n\n\t\t// Non-existent post id.\n\t\t$this->assertEquals( 0, $this->main->save( ++$post_id ) );\n\n\t}\n\n\t/**\n\t * Test save() gateway data.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_success_payment_gateway_data() {\n\n\t\t$updates = array(\n\t\t\t'payment_gateway'         => 'mock_gateway',\n\t\t\t'gateway_customer_id'     => 'cust_12345',\n\t\t\t'gateway_subscription_id' => 'sub_678',\n\t\t\t'gateway_source_id'       => 'source_1011',\n\t\t);\n\n\t\t$post_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) );\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( $updates ) );\n\n\t\t$this->assertEquals( 1, $this->main->save( $post_id ) );\n\n\t\t$order = llms_get_post( $post_id );\n\t\tforeach ( $updates as $key => $val ) {\n\t\t\t$this->assertEquals( $val, $order->get( $key ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test save() new order total.\n\t *\n\t * @since 9.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_success_payment_total() {\n\t\t$order = $this->get_mock_order();\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 5 );\n\t\t$order->set( 'billing_period', 'day' );\n\n\t\t$updates = array(\n\t\t\t'total'         => round( rand(1000, 9999) / 100, 2 ),\n\t\t);\n\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( $updates ) );\n\n\t\t$this->assertEquals( 1, $this->main->save( $order->get( 'id' ) ) );\n\n\t\t// Refresh the order data.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\t\tforeach ( $updates as $key => $val ) {\n\t\t\t$this->assertEquals( $val, $order->get( $key ) );\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test save() when remaining payment data is updated.\n\t *\n\t * @since 5.3.0\n\t * @since 7.0.0 Create the order via `$this->get_mock_order()` which also sets a valid gateway.\n\t * @since 7.0.1 Compare all order notes in favor of testing for the expected order.\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_success_remaining_payment_data() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 5 );\n\t\t$order->set( 'billing_period', 'day' );\n\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(\n\t\t\t'_llms_remaining_payments' => 3,\n\t\t\t'_llms_remaining_note'    => 'Mock note',\n\t\t) ) );\n\n\t\t$this->main->save( $order->get( 'id' ) );\n\n\t\t// Data.\n\t\t$this->assertEquals( 3, $order->get( 'billing_length' ) );\n\t\t$this->assertEquals( 3, $order->get_remaining_payments() );\n\n\t\t// Notes.\n\t\tremove_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\t\t$notes = $order->get_notes();\n\t\tadd_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\n\t\t$this->assertEqualSets(\n\t\t\tarray(\n\t\t\t\t'Order status changed from new to Pending Payment',\n\t\t\t\t'The billing length of the order has been modified from 5 days to 3 days.',\n\t\t\t\t'Mock note',\n\t\t\t),\n\t\t\twp_list_pluck( $notes, 'comment_content' )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test save() when remaining payment data is updated but the order doesn't support recurring payment modifications.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_unsuccess_remaining_payment_data_when_order_does_not_support_recurring_payment_modifications() {\n\n\t\t// The order's gateway is not set, so the order does not supports modifying recurring payments.\n\t\t$order_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) );\n\t\t$order    = llms_get_post( $order_id );\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 5 );\n\t\t$order->set( 'billing_period', 'day' );\n\n\n\t\t$this->mockPostRequest( $this->add_nonce_to_array( array(\n\t\t\t'_llms_remaining_payments' => 3,\n\t\t\t'_llms_remaining_note'    => 'Mock note',\n\t\t) ) );\n\n\t\t$this->main->save( $order->get( 'id' ) );\n\n\t\t// Data.\n\t\t$this->assertEquals( 5, $order->get( 'billing_length' ) );\n\t\t$this->assertEquals( 5, $order->get_remaining_payments() );\n\n\t\t// Notes.\n\t\tremove_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\t\t$notes = $order->get_notes();\n\t\tadd_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\n\t\t$user_note  = array_pop( $notes );\n\n\t\t$this->assertNotEquals( 'Mock note', $user_note->comment_content );\n\t\t$this->assertNotEquals( 'The billing length of the order has been modified from 5 days to 3 days.', $user_note->comment_content );\n\t\t$this->assertEmpty( $notes );\n\n\t}\n\n\t/**\n\t * Test save_remaining_payments() when no changes should occur.\n\t *\n\t * @since 5.3.0\n\t * @since 7.0.0 Create the order via `$this->get_mock_order()` which also sets a valid gateway.\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_remaining_payments_no_changes() {\n\n\t\t$order = $this->get_mock_order();\n\n\t\t// Single order.\n\t\t$order->set( 'order_type', 'single' );\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t\t// Recurring without expiration.\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 0 );\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t\t// Nothing to save: no update submitted.\n\t\t$order->set( 'billing_length', 3 );\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t\t// Update submitted with no change.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => $order->get_remaining_payments(),\n\t\t) );\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t\t// Can't end a plan via an adjustment.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => 0,\n\t\t) );\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t}\n\n\t/**\n\t * Test save_remaining_payments() when changes are made.\n\t *\n\t * @since 5.3.0\n\t * @since 7.0.0 Create the order via `$this->get_mock_order()` which also sets a valid gateway.\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_remaining_payments_success() {\n\n\t\t$order = $this->get_mock_order();\n\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 5 );\n\n\t\t// Has one payment.\n\t\t$order->record_transaction( array(\n\t\t\t'payment_type' => 'recurring',\n\t\t\t'status'       => 'llms-txn-succeeded',\n\t\t) );\n\n\t\t// Reduce to one remaining payment.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => 1,\n\t\t) );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t\t$this->assertEquals( 2, $order->get( 'billing_length' ) );\n\t\t$this->assertEquals( 1, $order->get_remaining_payments() );\n\n\t\t// Increase to 7 remaining.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => 7,\n\t\t) );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t\t$this->assertEquals( 8, $order->get( 'billing_length' ) );\n\t\t$this->assertEquals( 7, $order->get_remaining_payments() );\n\n\t\t// Record another payment.\n\t\t$order->record_transaction( array(\n\t\t\t'payment_type' => 'recurring',\n\t\t\t'status'       => 'llms-txn-succeeded',\n\t\t) );\n\n\t\t// Decrease to 3 remaining.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => 3,\n\t\t) );\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\n\t\t$this->assertEquals( 5, $order->get( 'billing_length' ) );\n\t\t$this->assertEquals( 3, $order->get_remaining_payments() );\n\n\t}\n\n\t/**\n\t * Test save_remaining_payments() when changes are made but the order doesn't support recurring payment modifications.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_remaining_payments_unsuccess_when_order_does_not_support_recurring_payment_modifications() {\n\n\t\t// The order's gateway is not set, so the order does not supports modifying recurring payments.\n\t\t$order_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) );\n\t\t$order    = llms_get_post( $order_id );\n\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 5 );\n\n\t\t// Has one payment.\n\t\t$order->record_transaction( array(\n\t\t\t'payment_type' => 'recurring',\n\t\t\t'status'       => 'llms-txn-succeeded',\n\t\t) );\n\n\t\t// Try to reduce to one remaining payment.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => 1,\n\t\t) );\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\t\t// Billing length unchanged.\n\t\t$this->assertEquals( 5, $order->get( 'billing_length' ) );\n\t\t// Remaining payments are 4 because a payment has been mande.\n\t\t$this->assertEquals( 4, $order->get_remaining_payments() );\n\n\t\t// Try to increase to 7 remaining.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => 7,\n\t\t) );\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\t\t// Billing length unchanged.\n\t\t$this->assertEquals( 5, $order->get( 'billing_length' ) );\n\t\t// Remaining payments are still 4 because only one payment has been mande.\n\t\t$this->assertEquals( 4, $order->get_remaining_payments() );\n\n\t\t// Record another payment.\n\t\t$order->record_transaction( array(\n\t\t\t'payment_type' => 'recurring',\n\t\t\t'status'       => 'llms-txn-succeeded',\n\t\t) );\n\n\t\t// Decrease to 2 remaining.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_remaining_payments' => 2,\n\t\t) );\n\t\t$this->assertEquals( -1, LLMS_Unit_Test_Util::call_method( $this->main, 'save_remaining_payments', array( $order ) ) );\n\t\t// Billing length unchanged.\n\t\t$this->assertEquals( 5, $order->get( 'billing_length' ) );\n\t\t// Remaining payments are 3 because two payments have been mande.\n\t\t$this->assertEquals( 3, $order->get_remaining_payments() );\n\n\t}\n\n\t/**\n\t * Test meta box view contains editable recurring remaining payments.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_remaining_payments_editable() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 5 );\n\n\t\t// Setup the metabox post.\n\t\t$_post = $this->main->post;\n\t\t$this->main->post = get_post( $order->get( 'id' ) );\n\n\t\t$this->assertTrue( $order->supports_modify_recurring_payments() );\n\n\t\t$metabox_view = $this->get_output( array( $this->main, 'output' ) );\n\n\t\t$finds = array(\n\t\t\t'<span id=\"llms-remaining-payments-view\">5</span>'      => true,\n\t\t\t'<input type=\"number\" id=\"llms-num-remaining-payments\"' => true,\n\t\t);\n\n\t\t// The above editable fields are present.\n\t\tforeach ( $finds as $find => $bool ) {\n\t\t\t$func = $bool ? 'assertStringContainsString' : 'assertStringNotContainsString';\n\t\t\t$this->{$func}( $find, $metabox_view, \"{$func}: {$find}\" );\n\t\t}\n\n\t\t// Reset the metabox post.\n\t\t$this->main->post = $_post;\n\n\t}\n\n\t/**\n\t * Test meta box view doesn't contain editable recurring remaining payments.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_remaining_payments_not_editable() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$order->set( 'billing_length', 5 );\n\n\t\t// The order's gateway is set to something which does not supports modifying recurring payments.\n\t\t$order->set( 'payment_gateway', 'garbage' );\n\n\t\t// Setup the metabox post.\n\t\t$_post = $this->main->post;\n\t\t$this->main->post = get_post( $order->get( 'id' ) );\n\n\t\t$this->assertFalse( $order->supports_modify_recurring_payments() );\n\n\t\t$metabox_view = $this->get_output( array( $this->main, 'output' ) );\n\n\t\t$finds = array(\n\t\t\t'<span id=\"llms-remaining-payments-view\">5</span>'      => true,\n\t\t\t'<input type=\"number\" id=\"llms-num-remaining-payments\"' => false,\n\t\t);\n\n\t\t// The above editable fields are present.\n\t\tforeach ( $finds as $find => $bool ) {\n\t\t\t$func = $bool ? 'assertStringContainsString' : 'assertStringNotContainsString';\n\t\t\t$this->{$func}( $find, $metabox_view, \"{$func}: {$find}\" );\n\t\t}\n\n\t\t// Reset the metabox post.\n\t\t$this->main->post = $_post;\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-enrollment.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Order Metabox\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group metaboxes\n * @group LLMS_Meta_Box_Order_Enrollment\n * @group metaboxes_post_type\n *\n * @since 3.33.0\n * @since 4.18.0 Added some tests on the output method.\n */\nclass LLMS_Test_Meta_Box_Order_Enrollment extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test\n\t *\n\t * @since 3.33.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->metabox = new LLMS_Meta_Box_Order_Enrollment();\n\n\t}\n\n\t/**\n\t * Test the LLMS_Meta_Box_Order_Enrollment save method\n\t *\n\t * @since 3.33.0\n\t * @since 6.0.0 Replaced use of deprecated items.\n\t *              - `LLMS_UnitTestCase::setup_post()` method with `LLMS_Unit_Test_Mock_Requests::mockPostRequest()`\n\t *\n\t * @return void\n\t */\n\tpublic function test_save() {\n\n\t\t// Create a real order.\n\t\t$order = $this->get_mock_order();\n\n\t\t$order_id   = $order->get( 'id' );\n\t\t$product_id = $order->get( 'product_id' );\n\t\t$student_id = $order->get( 'user_id' );\n\n\t\t// Check enroll.\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_update_enrollment_status'      => 'Update',\n\t\t\t'llms_student_old_enrollment_status' => '',\n\t\t\t'llms_student_new_enrollment_status' => 'enrolled',\n\t\t) );\n\n\t\t$this->metabox->save( $order_id );\n\t\t$this->assertTrue( llms_is_user_enrolled( $student_id, $product_id ) );\n\n\t\t// Check unenroll.\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_update_enrollment_status'      => 'Update',\n\t\t\t'llms_student_old_enrollment_status' => 'enrolled',\n\t\t\t'llms_student_new_enrollment_status' => 'expired',\n\t\t) );\n\n\t\t$this->metabox->save( $order_id );\n\t\t$this->assertFalse( llms_is_user_enrolled( $student_id, $product_id ) );\n\n\t\t// Check enrollment deleted => no enrollment records + order status set to cancelled.\n\t\t$this->mockPostRequest( array(\n\t\t\t'llms_delete_enrollment_status'      => 'Delete',\n\t\t\t'llms_student_old_enrollment_status' => 'expired',\n\t\t\t'llms_student_new_enrollment_status' => 'deleted',\n\t\t) );\n\n\t\t$this->metabox->save( $order_id );\n\t\t$this->assertFalse( llms_is_user_enrolled( $student_id, $product_id ) );\n\t\t$this->assertEquals( array(), llms_get_user_postmeta( $student_id, $product_id ) );\n\t\t$this->assertSame( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) );\n\n\t}\n\n\n\t/**\n\t * Test the LLMS_Meta_Box_Order_Enrollment output method for anonymized orders\n\t *\n\t * @since 4.18.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_anonymized_order() {\n\n\t\t// Create a real order.\n\t\t$order = $this->get_mock_order();\n\n\t\t$order_id   = $order->get( 'id' );\n\t\t$product_id = $order->get( 'product_id' );\n\t\t$student_id = $order->get( 'user_id' );\n\n\t\t$order->set( 'anonymized', 'yes' );\n\n\t\t$this->assertOutputEquals( 'Cannot manage enrollment status for anonymized orders.', array( $this->metabox, 'output' ) );\n\n\t}\n\n\t/**\n\t * Test the LLMS_Meta_Box_Order_Enrollment output method for orders with no user\n\t *\n\t * @since 4.18.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_order_with_no_user() {\n\n\t\t// Create a real order.\n\t\t$order = $this->get_mock_order();\n\n\t\t$order_id   = $order->get( 'id' );\n\t\t$product_id = $order->get( 'product_id' );\n\n\t\t$order->set( 'user_id', '' );\n\t\t$this->assertOutputEmpty( array( $this->metabox, 'output' ) );\n\n\t}\n\n\n\t/**\n\t * Test the LLMS_Meta_Box_Order_Enrollment output method for orders of deleted students\n\t *\n\t * @since 4.18.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_order_with_deleted_student() {\n\n\t\t// Create a real order.\n\t\t$order = $this->get_mock_order();\n\n\t\t$order_id   = $order->get( 'id' );\n\t\t$product_id = $order->get( 'product_id' );\n\t\t$student_id = $order->get( 'user_id' );\n\n\t\twp_delete_user( $student_id );\n\n\t\t$this->assertOutputEquals( \"The student who placed the order doesn&#039;t exist anymore.\", array( $this->metabox, 'output' ) );\n\n\t}\n\n\t/**\n\t * Test the LLMS_Meta_Box_Order_Enrollment output method for orders with student\n\t *\n\t * @since 4.18.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_order_with_student() {\n\n\t\t// Create a real order.\n\t\t$order = $this->get_mock_order();\n\n\t\t$order_id   = $order->get( 'id' );\n\t\t$product_id = $order->get( 'product_id' );\n\t\t$student_id = $order->get( 'user_id' );\n\n\t\t$output = $this->get_output( array( $this->metabox, 'output' ) );\n\n\t\t// There's a status selecter.\n\t\t$this->assertStringContainsString( '<select name=\"llms_student_new_enrollment_status\">', $output );\n\n\t\t// The student is not enrolled yet.\n\t\t// No selected option, as well as the old (current) enrollment status.\n\t\t$this->assertStringNotContainsString( \"selected='selected'>\", $output );\n\t\t$this->assertStringContainsString( '<input name=\"llms_student_old_enrollment_status\" type=\"hidden\" value=\"\">', $output );\n\t\t// The delete enrollment button doesn't exist.\n\t\t$this->assertStringNotContainsString( '<input name=\"llms_delete_enrollment_status\" ', $output );\n\n\t\t// Enroll the student.\n\t\tllms_enroll_student( $student_id, $product_id );\n\n\t\t$output = $this->get_output( array( $this->metabox, 'output' ) );\n\n\t\t// The selected option is 'enrolled', as well as the old (current) enrollment status.\n\t\t$this->assertStringContainsString( \"<option value=\\\"enrolled\\\" selected='selected'>\", $output );\n\t\t$this->assertStringContainsString( '<input name=\"llms_student_old_enrollment_status\" type=\"hidden\" value=\"enrolled\">', $output );\n\t\t// The delete enrollment button does not exist.\n\t\t$this->assertStringNotContainsString( '<input name=\"llms_delete_enrollment_status\" ', $output );\n\n\t\t// Unenroll the student (cancelled status).\n\t\tllms_unenroll_student( $student_id, $product_id, 'cancelled', 'any' );\n\n\t\t$output = $this->get_output( array( $this->metabox, 'output' ) );\n\n\t\t// The selected option is 'cancelled', as well as the old (current) enrollment status.\n\t\t$this->assertStringContainsString( \"<option value=\\\"cancelled\\\" selected='selected'>\", $output );\n\t\t$this->assertStringContainsString( '<input name=\"llms_student_old_enrollment_status\" type=\"hidden\" value=\"cancelled\">', $output );\n\t\t// The delete enrollment button exists.\n\t\t$this->assertStringContainsString( '<input name=\"llms_delete_enrollment_status\" ', $output );\n\n\t\t// Unenroll the student (expired status).\n\t\tllms_enroll_student( $student_id, $product_id );\n\t\tllms_unenroll_student( $student_id, $product_id, 'expired', 'any' );\n\n\t\t$output = $this->get_output( array( $this->metabox, 'output' ) );\n\n\t\t// The selected option is 'expired', as well as the old (current) enrollment status.\n\t\t$this->assertStringContainsString( \"<option value=\\\"expired\\\" selected='selected'>\", $output );\n\t\t$this->assertStringContainsString( '<input name=\"llms_student_old_enrollment_status\" type=\"hidden\" value=\"expired\">', $output );\n\t\t// The delete enrollment button exists.\n\t\t$this->assertStringContainsString( '<input name=\"llms_delete_enrollment_status\" ', $output );\n\n\t\t// Delete enrollment.\n\t\tllms_delete_student_enrollment( $student_id, $product_id, 'any' );\n\n\t\t$output = $this->get_output( array( $this->metabox, 'output' ) );\n\n\t\t// No selected option, as well as the old (current) enrollment status.\n\t\t$this->assertStringNotContainsString( \"selected='selected'>\", $output );\n\t\t$this->assertStringContainsString( '<input name=\"llms_student_old_enrollment_status\" type=\"hidden\" value=\"\">', $output );\n\t\t// The delete enrollment button doesn't exist.\n\t\t$this->assertStringNotContainsString( '<input name=\"llms_delete_enrollment_status\" ', $output );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-order-submit.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Order Metabox\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group metaboxes\n * @group order_submit\n * @group metaboxes_post_type\n *\n * @since 7.0.0\n */\nclass LLMS_Test_Meta_Box_Order_Submit extends LLMS_PostTypeMetaboxTestCase {\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Meta_Box_Order_Submit();\n\n\t}\n\n\t/**\n\t * Test save() method checking all the editable date fields can be saved.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_all_editable_dates_success() {\n\n\t\t$order = $this->get_mock_order();\n\n\t\t$originals = array(\n\t\t\t'_llms_date_trial_end'      => $order->get( 'date_trial_end' ),\n\t\t\t'_llms_date_next_payment'   => $order->get( 'date_next_payment' ),\n\t\t\t'_llms_date_access_expires' => $order->get( 'date_access_expires' ),\n\t\t);\n\n\t\t$post_values = array(\n\t\t\t'_llms_date_trial_end'      => array(\n\t\t\t\t'date'   => '2022-06-20',\n\t\t\t\t'hour'   => '10',\n\t\t\t\t'minute' => '00',\n\t\t\t),\n\t\t\t'_llms_date_next_payment'   => array(\n\t\t\t\t'date'   => '2022-06-21',\n\t\t\t\t'hour'   => '10',\n\t\t\t\t'minute' => '00',\n\t\t\t),\n\t\t\t'_llms_date_access_expires' => array(\n\t\t\t\t'date'   => '2022-06-22',\n\t\t\t\t'hour'   => '10',\n\t\t\t\t'minute' => '00',\n\t\t\t),\n\t\t);\n\n\t\t$this->mockPostRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\t$post_values\n\t\t\t)\n\t\t);\n\n\t\t$this->main->save( $order->get( 'id' ) );\n\n\t\tforeach ( $originals as $key => $value ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t$post_values[ $key ]['date'] . ' ' . sprintf( '%02d', $post_values[ $key ]['hour'] ) . ':' . sprintf( '%02d', $post_values[ $key ]['minute'] ) . ':00',\n\t\t\t\t$order->get( str_replace( '_llms_', '', $key ) ),\n\t\t\t\t$key\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Test save() method checking all the editable date fields can be saved, except recurring payment related.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_all_editable_dates_success_except_recurring_payment_related() {\n\n\t\t// The order's gateway is not set, so the order does not supports modifying recurring payments.\n\t\t$order_id = $this->factory->post->create( array( 'post_type' => 'llms_order' ) );\n\t\t$order    = llms_get_post( $order_id );\n\n\t\t$originals = array(\n\t\t\t'_llms_date_trial_end'      => $order->get( 'date_trial_end' ),\n\t\t\t'_llms_date_next_payment'   => $order->get( 'date_next_payment' ),\n\t\t\t'_llms_date_access_expires' => $order->get( 'date_access_expires' ),\n\t\t);\n\n\t\t$post_values = array(\n\t\t\t'_llms_date_trial_end'      => array(\n\t\t\t\t'date'   => '2022-06-20',\n\t\t\t\t'hour'   => '10',\n\t\t\t\t'minute' => '00',\n\t\t\t),\n\t\t\t'_llms_date_next_payment'   => array(\n\t\t\t\t'date'   => '2022-06-21',\n\t\t\t\t'hour'   => '10',\n\t\t\t\t'minute' => '00',\n\t\t\t),\n\t\t\t'_llms_date_access_expires' => array(\n\t\t\t\t'date'   => '2022-06-22',\n\t\t\t\t'hour'   => '10',\n\t\t\t\t'minute' => '00',\n\t\t\t),\n\t\t);\n\n\t\t$this->mockPostRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\t$post_values\n\t\t\t)\n\t\t);\n\n\t\t$this->main->save( $order->get( 'id' ) );\n\n\t\tforeach ( $originals as $key => $value ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t( '_llms_date_access_expires' === $key )\n\t\t\t\t\t?\n\t\t\t\t\t$post_values[ $key ]['date'] . ' ' . sprintf( '%02d', $post_values[ $key ]['hour'] ) . ':' . sprintf( '%02d', $post_values[ $key ]['minute'] ) . ':00'\n\t\t\t\t\t:\n\t\t\t\t\t$value, // Dates which are not `_llms_date_access_expires` are not being saved.\n\t\t\t\t$order->get( str_replace( '_llms_', '', $key ) ),\n\t\t\t\t$key\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Test meta box view contains editable recurring payment date fields.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_payments_dates_are_editable() {\n\n\t\t$plan  = $this->get_mock_plan( 25.99, 3, 'lifetime', false, true );\n\t\t// This order supports `modify_recurring_payments`.\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Setup the metabox post.\n\t\t$_post = $this->main->post;\n\t\t$this->main->post = get_post( $order->get( 'id' ) );\n\n\t\t$this->assertTrue( $order->supports_modify_recurring_payments() );\n\n\t\t$metabox_view = $this->get_output( array( $this->main, 'output' ) );\n\n\t\t$finds = array(\n\t\t\t'data-llms-editable=\"_llms_date_next_payment\"',\n\t\t\t'data-llms-editable=\"_llms_date_trial_end\"',\n\t\t);\n\n\t\t// The above editable fields are present.\n\t\tforeach ( $finds as $find ) {\n\t\t\t$this->assertStringContainsString( $find, $metabox_view, $find );\n\t\t}\n\n\t\t// Reset the metabox post.\n\t\t$this->main->post = $_post;\n\n\t}\n\n\t/**\n\t * Test meta box view doesn't contain editable recurring payment date fields, for orders which do not support recurring payments.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_payments_dates_are_not_editable() {\n\n\t\t$plan  = $this->get_mock_plan( 25.99, 3, 'lifetime', false, true );\n\t\t$order = $this->get_mock_order( $plan );\n\t\t// The order's gateway is set to something which does not supports modifying recurring payments.\n\t\t$order->set( 'payment_gateway', 'garbage' );\n\n\t\t// Setup the metabox post.\n\t\t$_post = $this->main->post;\n\t\t$this->main->post = get_post( $order->get( 'id' ) );\n\n\t\t$this->assertFalse( $order->supports_modify_recurring_payments() );\n\n\t\t$metabox_view = $this->get_output( array( $this->main, 'output' ) );\n\n\t\t$finds = array(\n\t\t\t'data-llms-editable=\"_llms_date_next_payment\"',\n\t\t\t'data-llms-editable=\"_llms_date_trial_end\"',\n\t\t);\n\n\t\t// The above editable fields are not present.\n\t\tforeach ( $finds as $find ) {\n\t\t\t$this->assertStringNotContainsString( $find, $metabox_view, $find );\n\t\t}\n\n\t\t// Reset the metabox post.\n\t\t$this->main->post = $_post;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/meta-boxes/fields/class-llms-test-meta-box-textarea-tags.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Order Metabox.\n *\n * @package LifterLMS/Tests\n *\n * @group metabox_textarea_w_tags\n * @group admin\n * @group metaboxes\n * @group metaboxes_fields\n *\n * @since 6.0.0\n * @version 6.0.0\n */\nclass LLMS_Test_Metabox_Textarea_W_Tags_Field extends LLMS_Unit_Test_Case {\n\n\n\t/**\n\t * Setup before class.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/meta-boxes/fields/llms.interface.meta.box.field.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.fields.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/meta-boxes/fields/llms.class.meta.box.textarea.tags.php';\n\n\t}\n\n\t/**\n\t * Test output when not passing a custom value.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_without_custom_value() {\n\n\t\t// Set-up global post.\n\t\tglobal $post;\n\t\t$original_post = $post;\n\n\t\t$post = $this->factory->post->create_and_get();\n\t\tupdate_post_meta( $post->ID, 'without_custom_value', 'This should show' );\n\n\t\t$field = new LLMS_Metabox_Textarea_W_Tags_Field(\n\t\t\tarray(\n\t\t\t\t'type'  => 'textarea_w_tags',\n\t\t\t\t'label' => __( 'Test', 'lifterlms' ),\n\t\t\t\t'id'    => 'without_custom_value',\n\t\t\t\t'class' => 'code input-full',\n\t\t\t\t'value' => '',\n\t\t\t),\n\t\t);\n\n\t\t$this->assertOutputContains(\n\t\t\t'>This should show</textarea>',\n\t\t\tarray(\n\t\t\t\t$field,\n\t\t\t\t'output'\n\t\t\t)\n\t\t);\n\n\t\tdelete_post_meta( $post->ID, 'without_custom_value' );\n\n\t\t$field = new LLMS_Metabox_Textarea_W_Tags_Field(\n\t\t\tarray(\n\t\t\t\t'type'  => 'textarea_w_tags',\n\t\t\t\t'label' => __( 'Test', 'lifterlms' ),\n\t\t\t\t'id'    => 'without_custom_value',\n\t\t\t\t'class' => 'code input-full',\n\t\t\t\t'value' => '',\n\t\t\t),\n\t\t);\n\t\t$this->assertOutputContains(\n\t\t\t'></textarea>',\n\t\t\tarray(\n\t\t\t\t$field,\n\t\t\t\t'output'\n\t\t\t)\n\t\t);\n\n\t\t// Reset global post.\n\t\t$post = $original_post;\n\n\t}\n\n\t/**\n\t * Test output when passing a custom value.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_with_custom_value() {\n\n\t\t// Set-up global post.\n\t\tglobal $post;\n\t\t$original_post = $post;\n\n\t\t$post = $this->factory->post->create_and_get();\n\t\tupdate_post_meta( $post->ID, 'with_custom_value', 'This should not show' );\n\n\t\t$field = new LLMS_Metabox_Textarea_W_Tags_Field(\n\t\t\tarray(\n\t\t\t\t'type'  => 'textarea_w_tags',\n\t\t\t\t'label' => __( 'Test', 'lifterlms' ),\n\t\t\t\t'id'    => 'with_custom_value',\n\t\t\t\t'class' => 'code input-full',\n\t\t\t\t'value' => 'Custom Value',\n\t\t\t),\n\t\t);\n\n\t\t$this->assertOutputContains(\n\t\t\t'>Custom Value</textarea>',\n\t\t\tarray(\n\t\t\t\t$field,\n\t\t\t\t'output'\n\t\t\t)\n\t\t);\n\n\t\t// Reset global post.\n\t\t$post = $original_post;\n\n\t}\n\n\n\t/**\n\t * Test output when forcing a meta.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_with_meta_forced() {\n\n\t\t// Set-up global post.\n\t\tglobal $post;\n\t\t$original_post = $post;\n\n\t\t$post = $this->factory->post->create_and_get();\n\t\tupdate_post_meta( $post->ID, 'with_custom_value', 'This should not show' );\n\n\t\t$field = new LLMS_Metabox_Textarea_W_Tags_Field(\n\t\t\tarray(\n\t\t\t\t'type'  => 'textarea_w_tags',\n\t\t\t\t'label' => __( 'Test', 'lifterlms' ),\n\t\t\t\t'id'    => 'with_custom_value',\n\t\t\t\t'class' => 'code input-full',\n\t\t\t\t'meta'  => 'Custom Value',\n\t\t\t),\n\t\t);\n\n\t\t$this->assertOutputContains(\n\t\t\t'>Custom Value</textarea>',\n\t\t\tarray(\n\t\t\t\t$field,\n\t\t\t\t'output'\n\t\t\t)\n\t\t);\n\n\t\t// Reset global post.\n\t\t$post = $original_post;\n\n\t}\n\n\n\t/**\n\t * Test output with rows and columns.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_rows_and_cols() {\n\n\t\t// Set-up global post.\n\t\tglobal $post;\n\t\t$original_post = $post;\n\t\t$post = $this->factory->post->create_and_get();\n\n\t\t$args = array(\n\t\t\t'type'  => 'textarea_w_tags',\n\t\t\t'label' => __( 'Test', 'lifterlms' ),\n\t\t\t'id'    => 'without_custom_value',\n\t\t\t'class' => 'code input-full',\n\t\t);\n\n\t\t// Use defaults.\n\t\t$field = new LLMS_Metabox_Textarea_W_Tags_Field( $args );\n\t\t$this->assertOutputContains( 'cols=\"60\"', array( $field, 'output' ) );\n\t\t$this->assertOutputContains( 'rows=\"4\"', array( $field, 'output' ) );\n\n\t\t// Custom values.\n\t\t$args['cols'] = 5;\n\t\t$args['rows'] = 20;\n\n\t\t$field = new LLMS_Metabox_Textarea_W_Tags_Field( $args );\n\t\t$this->assertOutputContains( 'cols=\"5\"', array( $field, 'output' ) );\n\t\t$this->assertOutputContains( 'rows=\"20\"', array( $field, 'output' ) );\n\n\t\t// Reset global post.\n\t\t$post = $original_post;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/post-types/post-tables/class-llms-admin-post-table-certificates.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Post_Table_Certificates class.\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group post_tables\n * @group post_table_certificates\n *\n * @since 6.0.0\n * @since 6.2.0 Added tests for `add_states()` method on posts that aren't certificates.\n */\nclass LLMS_Test_Admin_Post_Table_Certificates extends LLMS_UnitTestCase {\n\n\t/**\n\t * Set up before class.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/post-tables/class-llms-admin-post-table-certificates.php';\n\n\t}\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Post_Table_Certificates();\n\n\t}\n\n\t/**\n\t * Test __construct().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\tif ( ! llms_is_block_editor_supported_for_certificates() ) {\n\t\t\t$this->markTestSkipped( 'No actions are registered for this version of WordPress.' );\n\t\t}\n\n\t\t// Always added.\n\t\t$this->assertEquals( 10, has_filter( 'manage_llms_my_certificate_posts_columns', array( $this->main, 'mod_cols' ) ) );\n\n\t\t// Wrong screen.\n\t\t$this->assertFalse( has_filter( 'display_post_states', array( $this->main, 'add_states' ) ) );\n\t\t$this->assertFalse( has_filter( 'post_row_actions', array( $this->main, 'add_actions' ) ) );\n\t\t$this->assertFalse( has_filter( 'llms_certificate_template_version', array( $this->main, 'upgrade_template' ) ) );\n\n\t\t// Right screen.\n\t\t$this->mockGetRequest( array( 'post_type' => 'llms_certificate' ) );\n\t\t$this->main = new LLMS_Admin_Post_Table_Certificates();\n\n\t\t$this->assertEquals( 20, has_filter( 'display_post_states', array( $this->main, 'add_states' ) ) );\n\t\t$this->assertEquals( 20, has_filter( 'post_row_actions', array( $this->main, 'add_actions' ) ) );\n\n\t\t// Action set but invalid.\n\t\t$this->mockGetRequest( array( $this->main::MIGRATE_ACTION => 'yes' ) );\n\t\t$this->main = new LLMS_Admin_Post_Table_Certificates();\n\t\t$this->assertFalse( has_filter( 'llms_certificate_template_version', array( $this->main, 'upgrade_template' ) ) );\n\n\t\t// Valid action set.\n\t\t$this->mockGetRequest( array( $this->main::MIGRATE_ACTION => 1 ) );\n\t\t$this->main = new LLMS_Admin_Post_Table_Certificates();\n\t\t$this->assertEquals( 10, has_filter( 'llms_certificate_template_version', array( $this->main, 'upgrade_template' ) ) );\n\n\t}\n\n\t/**\n\t * Test add_actions().\n\t *\n\t * @since 6.0.0\n\t * @since 7.1.0 Log in as administrator so that the certificates have a post edit link set.\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_actions() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Is legacy.\n\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_certificate' ) );\n\t\t$this->assertArrayHasKey(\n\t\t\t'llms-migrate-legacy-certificate',\n\t\t\t$this->main->add_actions( array(), $post )\n\t\t);\n\n\t\t// Use block editor.\n\t\t$post->post_content = '';\n\t\t$this->assertEquals( array(), $this->main->add_actions( array(), $post ) );\n\n\t}\n\n\t/**\n\t * Test add_states().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_states() {\n\n\t\t// Is legacy.\n\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_certificate' ) );\n\t\t$this->assertArrayHasKey(\n\t\t\t'llms-legacy-template',\n\t\t\t$this->main->add_states( array(), $post )\n\t\t);\n\n\t\t// Use block editor.\n\t\t$post->post_content = '';\n\t\t$this->assertEquals( array(), $this->main->add_states( array(), $post ) );\n\n\t}\n\n\t/**\n\t * Test add_states() on posts which are not certificates.\n\t *\n\t * @since 6.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_states_no_certificate_post() {\n\n\t\t$post = $this->factory->post->create_and_get();\n\t\t$this->assertEquals( array(), $this->main->add_states( array(), $post ) );\n\n\t\t$post = null;\n\t\t$this->assertEquals( array(), $this->main->add_states( array(), $post ) );\n\n\t}\n\n\t/**\n\t * Test mod_cols().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_mod_cols() {\n\n\t\t$cols = array(\n\t\t\t'col1'   => 'retain',\n\t\t\t'author' => 'remove',\n\t\t\t'col3'   => 'retain',\n\t\t);\n\n\t\t$this->assertEquals( array( 'col1', 'col3' ), array_keys( $this->main->mod_cols( $cols ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-a.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-a\" id=\"test-a\">\n\t\t\t\t\t<i class=\"fa fa-mocking-bird\" aria-hidden=\"true\"></i>\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong>123</strong>\n\t\t\t\t\t\t\t<small class=\"compare tooltip negative\" title=\"Previously 456\">\n\t\t\t\t\t-270.73%\n\t\t\t\t</small>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter A</small>\n\t</div>\n</div>"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-b.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-b\" id=\"test-b\">\n\t\t\t\t\t<i class=\"fa fa-mocking-bird\" aria-hidden=\"true\"></i>\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong>1.24<sup>%</sup></strong>\n\t\t\t\t\t\t\t<small class=\"compare tooltip negative\" title=\"Previously 5.92%\">\n\t\t\t\t\t-379.35%\n\t\t\t\t</small>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter B</small>\n\t</div>\n</div>\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-c.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-c\" id=\"test-c\">\n\t\t\t\t\t<i class=\"fa fa-mocking-bird\" aria-hidden=\"true\"></i>\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong>9328320.952</strong>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter C</small>\n\t</div>\n</div>"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-d.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-d\" id=\"test-d\">\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong>Lorem ipsum dolor sit.</strong>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter D</small>\n\t</div>\n</div>"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-e.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-e\" id=\"test-e\">\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong><span class=\"lifterlms-price\"><span class=\"llms-price-currency-symbol\">&#036;</span>45.90</span></strong>\n\t\t\t\t\t\t\t<small class=\"compare tooltip positive\" title=\"Previously 200.32\">\n\t\t\t\t\t-336.43%\n\t\t\t\t</small>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter E</small>\n\t</div>\n</div>"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-f.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-f\" id=\"test-f\">\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong>January 1, 2022</strong>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter F</small>\n\t</div>\n</div>"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-g.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-g\" id=\"test-g\">\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong>January 1, 2022</strong>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter G</small>\n\t</div>\n</div>"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/__snapshots__/admin-reporting-output_widget-test-h.txt",
    "content": "<div class=\"d-1of2\">\n\t<div class=\"llms-reporting-widget test-h\" id=\"test-h\">\n\t\t\t\t<div class=\"llms-reporting-widget-data\">\n\t\t\t<strong>0.000</strong>\n\t\t\t\t\t</div>\n\t\t<small>Test Letter H</small>\n\t</div>\n</div>"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/reporting/class-llms-test-admin-reporting.php",
    "content": "<?php\n/**\n * Tests Admin Reporting Class\n *\n * @package LifterLMS/Tests/Admin/Reporting\n *\n * @group admin\n * @group admin_reporting\n *\n * @since 6.11.0\n */\nclass LLMS_Test_Admin_Reporting extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.11.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Admin_Reporting();\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Admin_Reporting::output_widget}\n\t *\n\t * @since 6.11.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_widget() {\n\n\t\t$tests = array(\n\t\t\t// Simple numeric data with comparison.\n\t\t\tarray(\n\t\t\t\t'id'           => 'a',\n\t\t\t\t'data'         => 123,\n\t\t\t\t'data_compare' => 456,\n\t\t\t\t'icon'         => 'mocking-bird',\n\t\t\t),\n\t\t\t// Percentage data.\n\t\t\tarray(\n\t\t\t\t'id'           => 'b',\n\t\t\t\t'data'         => '1.235',\n\t\t\t\t'data_compare' => '5.920',\n\t\t\t\t'data_type'    => 'percentage',\n\t\t\t\t'icon'         => 'mocking-bird',\n\t\t\t),\n\t\t\t// Numeric with no compare.\n\t\t\tarray(\n\t\t\t\t'id'   => 'c',\n\t\t\t\t'data' => 9328320.952,\n\t\t\t\t'icon' => 'mocking-bird',\n\t\t\t),\n\t\t\t// Text data with no icon.\n\t\t\tarray(\n\t\t\t\t'id'   => 'd',\n\t\t\t\t'data' => 'Lorem ipsum dolor sit.',\n\t\t\t),\n\t\t\t// Monetary data with negative impact.\n\t\t\tarray(\n\t\t\t\t'id'           => 'e',\n\t\t\t\t'data'         => '45.90',\n\t\t\t\t'data_compare' => '200.32',\n\t\t\t\t'data_type'    => 'monetary',\n\t\t\t\t'impact'       => 'negative',\n\t\t\t),\n\t\t\t// Date data.\n\t\t\tarray(\n\t\t\t\t'id'           => 'f',\n\t\t\t\t'data'         => 'January 1, 2022',\n\t\t\t\t'data_type'    => 'date',\n\t\t\t),\n\t\t\t// Date with comparison (invalid but shouldn't error).\n\t\t\tarray(\n\t\t\t\t'id'           => 'g',\n\t\t\t\t'data'         => 'January 1, 2022',\n\t\t\t\t'data_compare' => '200.32',\n\t\t\t\t'data_type'    => 'date',\n\t\t\t),\n\t\t\t/**\n\t\t\t * Numeric divide by zero comparison.\n\t\t\t *\n\t\t\t * @link https://github.com/gocodebox/lifterlms/issues/2270\n\t\t\t */\n\t\t\tarray(\n\t\t\t\t'id'           => 'h',\n\t\t\t\t'data'         => '0.000',\n\t\t\t\t'data_compare' => '0.000',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\t$test['text'] = 'Test Letter ' . strtoupper( $test['id'] );\n\t\t\t$test['id']   = \"test-{$test['id']}\";\n\n\t\t\t$output = trim(\n\t\t\t\t$this->get_output( \n\t\t\t\t\tarray( 'LLMS_Admin_Reporting', 'output_widget' ),\n\t\t\t\t\tarray( $test )\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$snap_path = __DIR__ . \"/__snapshots__/admin-reporting-output_widget-{$test['id']}.txt\";\n\n\t\t\t// Quick snapshot generator, reenable if we add more tests to save copy/pasting.\n\t\t\t// if ( ! file_exists( $snap_path ) ) {\n\t\t\t// \t$fh = fopen( $snap_path, 'w' );\n\t\t\t// \tfwrite( $fh, $output );\n\t\t\t// \tfclose( $fh );\n\t\t\t// \t$this->markTestIncomplete( \"Snapshot written for {$test['id']}.\" );\n\t\t\t// }\n\n\t\t\t$snap = file_get_contents( $snap_path );\n\n\t\t\t$this->assertEquals( trim( $snap ), trim( $output ) );\n\n\t\t}\n\n\t}\n\n}"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-accounts.php",
    "content": "<?php\n/**\n * Test LLMS_Settings_Accounts\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group settings_page\n * @group settings_page_accounts\n *\n * @since 3.37.3\n * @since 3.37.4 The ID is \"account\" not \"accounts\".\n */\nclass LLMS_Test_Settings_Accounts extends LLMS_Settings_Page_Test_Case {\n\n\t/**\n\t * Classname.\n\t *\n\t * @var string\n\t */\n\tprotected $classname = 'LLMS_Settings_Accounts';\n\n\t/**\n\t * Expected class $id property.\n\t *\n\t * @var string\n\t */\n\tprotected $class_id = 'account';\n\n\t/**\n\t * Expected class $label property.\n\t *\n\t * @var string\n\t */\n\tprotected $class_label = 'Accounts';\n\n\t/**\n\t * Return an array of mock settings and possible values.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tprotected function get_mock_settings() {\n\n\t\t$pages = array(\n\t\t\t$this->factory->post->create( array( 'post_type' => 'page' ) ),\n\t\t\t$this->factory->post->create( array( 'post_type' => 'page' ) ),\n\t\t);\n\n\n\t\t$settings = array(\n\t\t\t'lifterlms_myaccount_page_id' => $pages,\n\t\t\t'lifterlms_myaccount_courses_in_progress_sorting' => array(\n\t\t\t\t'title,ASC',\n\t\t\t\t'title,DESC',\n\t\t\t\t'date,DESC',\n\t\t\t\t'order,ASC',\n\t\t\t\t'order,DESC',\n\t\t\t),\n\t\t\t'lifterlms_enable_myaccount_registration' => array(\n\t\t\t\t'yes',\n\t\t\t),\n\t\t\t'lifterlms_prevent_concurrent_logins' => array(\n\t\t\t\t'yes',\n\t\t\t),\n\t\t\t'lifterlms_prevent_concurrent_logins_roles' => array(\n\t\t\t\tarray( '' ),\n\t\t\t\tarray( 'student' ),\n\t\t\t),\n\t\t\t'lifterlms_myaccount_grades_endpoint' => array(\n\t\t\t\t'my-grades',\n\t\t\t\t'custom-endpoint-grades',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_courses_endpoint' => array(\n\t\t\t\t'my-courses',\n\t\t\t\t'custom-endpoint-courses',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_memberships_endpoint' => array(\n\t\t\t\t'my-memberships',\n\t\t\t\t'custom-endpoint-memberships',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_achievements_endpoint' => array(\n\t\t\t\t'my-achievements',\n\t\t\t\t'custom-endpoint-achievements',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_certificates_endpoint' => array(\n\t\t\t\t'my-certificates',\n\t\t\t\t'custom-endpoint-certificates',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_favorites_endpoint' => array(\n\t\t\t\t'my-favorites',\n\t\t\t\t'custom-endpoint-favorites',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_notifications_endpoint' => array(\n\t\t\t\t'notifications',\n\t\t\t\t'custom-endpoint-notifications',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_edit_account_endpoint' => array(\n\t\t\t\t'edit-account',\n\t\t\t\t'custom-endpoint-account',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_lost_password_endpoint' => array(\n\t\t\t\t'lost-password',\n\t\t\t\t'custom-endpoint-reset-pass',\n\t\t\t),\n\t\t\t'lifterlms_myaccount_redeem_vouchers_endpoint' => array(\n\t\t\t\t'redeem-voucher',\n\t\t\t\t'custom-redemption-code'\n\t\t\t),\n\t\t\t'lifterlms_myaccount_orders_endpoint' => array(\n\t\t\t\t'orders',\n\t\t\t\t'custom-order-history',\n\t\t\t),\n\t\t\t'lifterlms_registration_require_agree_to_terms' => array(\n\t\t\t\t'yes',\n\t\t\t),\n\t\t\t'lifterlms_terms_page_id' => $pages,\n\t\t\t'llms_terms_notice' => array(\n\t\t\t\tllms_get_terms_notice(),\n\t\t\t\t'mock terms notice',\n\t\t\t),\n\t\t\t'wp_page_for_privacy_policy' => $pages,\n\t\t\t'llms_privacy_notice' => array(\n\t\t\t\tllms_get_privacy_notice(),\n\t\t\t\t'mock privacy notice',\n\t\t\t),\n\t\t\t'llms_erasure_request_removes_order_data' => array(\n\t\t\t\t'yes',\n\t\t\t),\n\t\t\t'llms_erasure_request_removes_lms_data' => array(\n\t\t\t\t'yes',\n\t\t\t),\n\t\t);\n\n\t\tif ( ! llms_is_favorites_enabled() ) {\n\t\t\tunset( $settings['lifterlms_myaccount_favorites_endpoint'] );\n\t\t}\n\n\t\treturn $settings;\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-engagements.php",
    "content": "<?php\n/**\n * Test LLMS_Settings_Engagements\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group settings_page\n * @group settings_page_engagements\n *\n * @since 3.37.3\n * @since 3.40.0 Add tests for `get_settings_group_email_delivery()`.\n */\nclass LLMS_Test_Settings_Engagements extends LLMS_Settings_Page_Test_Case {\n\n\t/**\n\t * Classname.\n\t *\n\t * @var string\n\t */\n\tprotected $classname = 'LLMS_Settings_Engagements';\n\n\t/**\n\t * Expected class $id property.\n\t *\n\t * @var string\n\t */\n\tprotected $class_id = 'engagements';\n\n\t/**\n\t * Expected class $label property.\n\t *\n\t * @var string\n\t */\n\tprotected $class_label = 'Engagements';\n\n\t/**\n\t * Determines whether or not legacy setting should be added to the mock settings array.\n\t *\n\t * @var boolean\n\t */\n\tprivate $expect_legacy_opts = false;\n\n\t/**\n\t * Return an array of mock settings and possible values.\n\t *\n\t * @since 3.37.3\n\t * @since 6.0.0 Update settings.\n\t *\n\t * @return void\n\t */\n\tprotected function get_mock_settings() {\n\n\t\t$settings = array(\n\t\t\t'lifterlms_email_from_name' => array(\n\t\t\t\tesc_attr( get_bloginfo( 'title' ) ),\n\t\t\t\t'mock from name',\n\t\t\t),\n\t\t\t'lifterlms_email_from_address' => array(\n\t\t\t\tget_option( 'admin_email' ),\n\t\t\t\t'mock@mock.com',\n\t\t\t),\n\t\t\t'lifterlms_email_header_image' => array(\n\t\t\t\t'fake.png',\n\t\t\t),\n\t\t\t'lifterlms_email_footer_text' => array(\n\t\t\t\t'footer text content',\n\t\t\t),\n\t\t\t'lifterlms_achievement_default_img' => array(\n\t\t\t\t0,\n\t\t\t\t25,\n\t\t\t),\n\t\t\t'lifterlms_certificate_default_img' => array(\n\t\t\t\t0,\n\t\t\t\t32,\n\t\t\t),\n\t\t\t'lifterlms_certificate_default_size' => array(\n\t\t\t\t'LETTER',\n\t\t\t\t'A4'\n\t\t\t),\n\t\t\t'lifterlms_certificate_default_user_defined_width' => array(\n\t\t\t\t8.5,\n\t\t\t\t120,\n\t\t\t),\n\t\t\t'lifterlms_certificate_default_user_defined_height' => array(\n\t\t\t\t11,\n\t\t\t\t200\n\t\t\t),\n\t\t\t'lifterlms_certificate_default_user_defined_unit' => array(\n\t\t\t\t'in',\n\t\t\t\t'mm'\n\t\t\t),\n\t\t);\n\n\t\tif ( $this->expect_legacy_opts ) {\n\t\t\t$settings = array_merge(\n\t\t\t\t$settings,\n\t\t\t\tarray(\n\t\t\t\t\t'lifterlms_certificate_bg_img_width' => array(\n\t\t\t\t\t\t800,\n\t\t\t\t\t\t1024,\n\t\t\t\t\t),\n\t\t\t\t\t'lifterlms_certificate_bg_img_height' => array(\n\t\t\t\t\t\t616,\n\t\t\t\t\t\t1200,\n\t\t\t\t\t),\n\t\t\t\t\t'lifterlms_certificate_legacy_image_size' => array(\n\t\t\t\t\t\t'yes',\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\treturn $settings;\n\n\t}\n\n\t/**\n\t * Ensure all editable settings exist in the settings array when the legacy option is set.\n\t *\n\t * Calls the parent test method `test_get_setting()` after setting up data to enable legacy opts.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings_with_legacy() {\n\n\t\tupdate_option( 'llms_has_certificates_with_legacy_default_image', 'yes' );\n\t\t$this->expect_legacy_opts = true;\n\t\tparent::test_get_settings();\n\t\t$this->expect_legacy_opts = false;\n\n\t}\n\n\t/**\n\t * Retrieve mock email provider settings used to test the get_settings_group_email_delivery() method.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return array[]\n\t */\n\tpublic function get_mock_email_provider_settings() {\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'id' => 'mock_email_provider_title',\n\t\t\t\t'type' => 'subtitle',\n\t\t\t\t'title' => 'Email sender',\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Return an array of mock settings and possible values.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings_group_email_delivery_no_providers() {\n\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->page, 'get_settings_group_email_delivery' ) );\n\n\t}\n\n\t/**\n\t * Return an array of mock settings and possible values.\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings_group_email_delivery_with_providers() {\n\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->page, 'get_settings_group_email_delivery' ) );\n\n\t\tadd_filter( 'llms_email_delivery_services', array( $this, 'get_mock_email_provider_settings' ) );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->page, 'get_settings_group_email_delivery' );\n\n\t\t$this->assertEquals( array( 'email_delivery', 'email_delivery_title', 'mock_email_provider_title', 'email_delivery_end' ), wp_list_pluck( $res, 'id' ) );\n\n\t\tremove_filter( 'llms_email_delivery_services', array( $this, 'get_mock_email_provider' ) );\n\n\t}\n\n\t/**\n\t * Test the save() method with legacy options enabled.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_with_legacy_opts() {\n\n\t\tupdate_option( 'llms_has_certificates_with_legacy_default_image', 'yes' );\n\t\t$this->expect_legacy_opts = true;\n\t\tparent::test_save();\n\t\t$this->expect_legacy_opts = false;\n\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/settings/class-llms-test-settings-page.php",
    "content": "<?php\n/**\n * Test LLMS_Settings_Page\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group settings_page\n *\n * @since 3.37.3\n */\nclass LLMS_Test_Settings_Page extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.3\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\tinclude_once LLMS_PLUGIN_DIR . 'includes/admin/settings/class.llms.settings.page.php';\n\n\t\t// Setup a mock settings page.\n\t\t$this->page = new class() extends LLMS_Settings_Page {\n\t\t\tpublic $id = 'mock';\n\t\t\tprotected function set_label() {\n\t\t\t\treturn 'Mock';\n\t\t\t}\n\t\t};\n\n\t}\n\n\t/**\n\t * Test constructor\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\t$this->assertEquals( 'Mock', $this->page->label );\n\n\t\t// Tab should be registered.\n\t\t$this->assertEquals( $this->page->tab_priority, has_action( 'lifterlms_settings_tabs_array', array( $this->page, 'add_settings_page' ) ) );\n\n\t\t// Output action.\n\t\t$this->assertEquals( 10, has_action( 'lifterlms_settings_mock', array( $this->page, 'output' ) ) );\n\n\t\t// Save action.\n\t\t$this->assertEquals( 10, has_action( 'lifterlms_settings_save_mock', array( $this->page, 'save' ) ) );\n\n\t}\n\n\t/**\n\t * Test add_settings_page() method.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_settings_page() {\n\n\t\t// No other pages exist.\n\t\t$this->assertEquals( array(\n\t\t\t'mock' => 'Mock',\n\t\t), $this->page->add_settings_page( array() ) );\n\n\t\t// Another page exists, add it to the end.\n\t\t$this->assertEquals( array(\n\t\t\t'fake' => 'Fake',\n\t\t\t'mock' => 'Mock',\n\t\t), $this->page->add_settings_page( array(\n\t\t\t'fake' => 'Fake',\n\t\t) ) );\n\n\t}\n\n\t/**\n\t * Test generation of a settings group with no settings added.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_settings() {\n\n\t\t// No description.\n\t\t$args = array(\n\t\t\t'mock_options',\n\t\t\t'Mock Settings',\n\t\t);\n\t\t$settings = LLMS_Unit_Test_Util::call_method( $this->page, 'generate_settings_group', $args );\n\n\t\t$this->assertEquals( 'mock_options', $settings[0]['id'] );\n\t\t$this->assertEquals( 'sectionstart', $settings[0]['type'] );\n\n\t\t$this->assertEquals( 'mock_options_title', $settings[1]['id'] );\n\t\t$this->assertEquals( 'title', $settings[1]['type'] );\n\t\t$this->assertEquals( 'Mock Settings', $settings[1]['title'] );\n\t\t$this->assertEquals( '', $settings[1]['desc'] );\n\n\t\t$this->assertEquals( 'mock_options_end', $settings[2]['id'] );\n\t\t$this->assertEquals( 'sectionend', $settings[2]['type'] );\n\n\t\t// Has a description.\n\t\t$args = array(\n\t\t\t'mock_options',\n\t\t\t'Mock Settings',\n\t\t\t'Mock Settings Description',\n\t\t);\n\t\t$settings = LLMS_Unit_Test_Util::call_method( $this->page, 'generate_settings_group', $args );\n\t\t$this->assertEquals( 'Mock Settings Description', $settings[1]['desc'] );\n\n\t\t// Has a description.\n\t\t$args = array(\n\t\t\t'mock_options',\n\t\t\t'Mock Settings',\n\t\t\t'Mock Settings Description',\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'id'   => 'mock_setting',\n\t\t\t\t\t'type' => 'text',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\t\t$settings = LLMS_Unit_Test_Util::call_method( $this->page, 'generate_settings_group', $args );\n\t\t$this->assertEquals( 'mock_setting', $settings[2]['id'] );\n\t\t$this->assertEquals( 'text', $settings[2]['type'] );\n\n\t}\n\n\n\t/**\n\t * Test set_label() stub when no ID exists for the class.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_label_stub_no_id() {\n\n\t\t// Empty string because no ID defined.\n\t\t$page = new LLMS_Settings_Page();\n\t\t$this->assertEquals( '', LLMS_Unit_Test_Util::call_method( $page, 'set_label' ) );\n\n\t}\n\n\t/**\n\t * Test set_label() stub when an ID is set.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_label_stub_with_id() {\n\n\t\t// Return ID because the method isn't overriden.\n\t\t$page = new class() extends LLMS_Settings_Page {\n\t\t\tpublic $id = 'mock';\n\t\t};\n\t\t$this->assertEquals( 'mock', LLMS_Unit_Test_Util::call_method( $page, 'set_label' ) );\n\n\t}\n\n\t/**\n\t * Test the get_sections() stub.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_sections() {\n\t\t$this->assertEquals( array(), $this->page->get_sections() );\n\t}\n\n\t/**\n\t * Test the get_settings() stub.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_settings_stub() {\n\t\t$this->assertEquals( array(), $this->page->get_settings() );\n\t}\n\n\t/**\n\t * Test the output() stub.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_output() {\n\t\t$this->assertOutputEmpty( array( $this->page, 'output' ) );\n\t}\n\n\t/**\n\t * Test the output_sections_nav() stub when no sections exist.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_sections_nav_empty() {\n\t\t$this->assertOutputEmpty( array( $this->page, 'output_sections_nav' ) );\n\t}\n\n\t/**\n\t * Test the output_sections_nav() stub when sections do exist.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_sections_nav() {\n\n\t\t$page = new class() extends LLMS_Settings_Page {\n\t\t\tpublic $id = 'mock';\n\t\t\tpublic function get_sections() {\n\t\t\t\treturn array(\n\t\t\t\t\t'section_1' => 'Section 1',\n\t\t\t\t\t'section_2' => 'Section 2',\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\n\t\t$method = array( $page, 'output_sections_nav' );\n\n\t\t$this->assertOutputContains( '<nav class=\"llms-nav-tab-wrapper llms-nav-text\">', $method );\n\t\t$this->assertOutputContains( '<ul class=\"llms-nav-items\">', $method );\n\n\t\t$this->assertOutputContains( 'section=section_1\">Section 1</a>', $method );\n\t\t$this->assertOutputContains( 'section=section_2\">Section 2</a>', $method );\n\n\t\t$this->assertOutputContains( '</ul>', $method );\n\t\t$this->assertOutputContains( '</nav>', $method );\n\n\t}\n\n\t/**\n\t * Test the save() method.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_save() {\n\n\t\t$page = new class() extends LLMS_Settings_Page {\n\t\t\tpublic $id = 'mock';\n\t\t\tpublic function get_settings() {\n\t\t\t\treturn array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'id'   => 'mock_setting_id',\n\t\t\t\t\t\t'type' => 'text',\n\t\t\t\t\t),\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'id'   => 'mock_setting_id_2',\n\t\t\t\t\t\t'type' => 'text',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\n\t\t// No data posted.\n\t\t$page->save();\n\t\t$this->assertEmpty( get_option( 'mock_setting_id' ) );\n\t\t$this->assertEmpty( get_option( 'mock_setting_id_2' ) );\n\n\t\t// Some Data posted.\n\t\t$this->mockPostRequest( array( 'mock_setting_id' => 'mock_setting_val' ) );\n\t\t$page->save();\n\t\t$this->assertEquals( 'mock_setting_val', get_option( 'mock_setting_id' ) );\n\t\t$this->assertEmpty( get_option( 'mock_setting_id_2' ) );\n\n\t\t// All Data posted.\n\t\t$this->mockPostRequest( array(\n\t\t\t'mock_setting_id'   => 'mock_setting_val',\n\t\t\t'mock_setting_id_2' => 'mock_setting_val',\n\t\t) );\n\t\t$page->save();\n\t\t$this->assertEquals( 'mock_setting_val', get_option( 'mock_setting_id' ) );\n\t\t$this->assertEquals( 'mock_setting_val', get_option( 'mock_setting_id_2' ) );\n\n\t}\n\n\t/**\n\t * Ensure unregistered (fake) options aren't stored during save events.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_fake_option() {\n\n\t\t// Fake option.\n\t\t$this->mockPostRequest( array(\n\t\t\t'mock_setting_id_3' => 'mock_setting_val',\n\t\t) );\n\t\t$this->page->save();\n\t\t$this->assertEmpty( get_option( 'mock_setting_id_3' ) );\n\n\t}\n\n\t/**\n\t * Test the save() method when the $flush prop is true.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_flush_disabled() {\n\n\t\t$page = new class() extends LLMS_Settings_Page {\n\t\t\tpublic $id = 'mock';\n\t\t};\n\n\t\t$this->assertFalse( has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) );\n\t\t$page->save();\n\t\t$this->assertFalse( has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) );\n\n\t}\n\n\t/**\n\t * Test the save() method when the $flush prop is true.\n\t *\n\t * @since 3.37.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_flush_enabled() {\n\n\t\t$page = new class() extends LLMS_Settings_Page {\n\t\t\tpublic $id = 'mock';\n\t\t\tprotected $flush = true;\n\t\t};\n\n\t\t$this->assertFalse( has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) );\n\t\t$page->save();\n\t\t$this->assertEquals( 10, has_action( 'shutdown', array( $page, 'flush_rewrite_rules' ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-batch-eraser.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Tool_Batch_Eraser class\n *\n * @package LifterLMS/Tests/Admins/Tools\n *\n * @group admin\n * @group admin_tools\n * @group batch_eraser\n *\n * @since 3.37.19\n * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests.\n */\nclass LLMS_Test_Admin_Tool_Batch_Eraser extends LLMS_Admin_Tool_Test_Case {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * @var sting\n\t */\n\tconst CLASS_NAME = 'LLMS_Admin_Tool_Batch_Eraser';\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 3.37.19\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\t$this->clear_cache();\n\n\t}\n\n\t/**\n\t * Clear cached batch count data.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tprivate function clear_cache() {\n\t\twp_cache_delete( 'batch-eraser', 'llms_tool_data' );\n\t}\n\n\t/**\n\t * Test get_pending_batches()\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_pending_batches() {\n\n\t\t$key = 'llms_background_processor_course_data_batch_ast9a0st';\n\t\tadd_option( $key, array( 'data' ) );\n\t\t$this->clear_cache();\n\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) );\n\n\t\tdelete_option( $key );\n\n\t}\n\n\t/**\n\t * Test get_pending_batches(): no batches found.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_pending_batches_none_found() {\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) );\n\t}\n\n\t/**\n\t * Test get_pending_batches(): when there's a cache hit.\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_pending_batches_cache_hit() {\n\n\t\twp_cache_set( 'batch-eraser', 25, 'llms_tool_data' );\n\t\t$this->assertEquals( 25, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) );\n\n\t}\n\n\t/**\n\t * Test handle()\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle() {\n\n\t\t$key = 'llms_background_processor_course_data_batch_ast9a0st';\n\t\tadd_option( $key, array( 'data' ) );\n\t\t$key = 'wp_llms_notification_processor_email_batch_ast9a0st';\n\t\tadd_option( $key, array( 1, 2, 3 ) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) );\n\n\t\t$this->clear_cache();\n\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'get_pending_batches' ) );\n\n\t}\n\n\t/**\n\t * Test should_load()\n\t *\n\t * @since 3.37.19\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load() {\n\n\t\t$this->clear_cache();\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\n\t\twp_cache_set( 'batch-eraser', 25, 'llms_tool_data' );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-clear-sessions.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Tool_Clear_Sessions class\n *\n * @package LifterLMS/Tests/Admins/Tools\n *\n * @group admin\n * @group admin_tools\n * @group clear_sessions\n *\n * @since 4.0.0\n * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests.\n */\nclass LLMS_Test_Admin_Tool_Clear_Sessions extends LLMS_Admin_Tool_Test_Case {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * @var sting\n\t */\n\tconst CLASS_NAME = 'LLMS_Admin_Tool_Clear_Sessions';\n\n\t/**\n\t * Test handle()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle() {\n\n\t\t$this->create_mock_session_data();\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) );\n\n\t\tglobal $wpdb;\n\t\t$this->assertEquals( 0, $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_sessions\" ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-install-forms.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Tool_Install_Forms class\n *\n * @package LifterLMS/Tests/Admins/Tools\n *\n * @group admin\n * @group admin_tools\n * @group install_forms\n *\n * @since 5.0.0\n * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests.\n */\nclass LLMS_Test_Admin_Tool_Install_Forms extends LLMS_Admin_Tool_Test_Case {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * @var sting\n\t */\n\tconst CLASS_NAME = 'LLMS_Admin_Tool_Install_Forms';\n\n\t/**\n\t * Retrieve a list of core reusable block post ids\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return int[]\n\t */\n\tprivate function get_block_posts() {\n\n\t\t$blocks = new WP_Query( array(\n\t\t\t'post_type'    => 'wp_block',\n\t\t\t'meta_key'     => '_llms_field_id',\n\t\t\t'meta_compare' => 'EXISTS',\n\t\t) );\n\t\treturn wp_list_pluck( $blocks->posts, 'ID' );\n\n\t}\n\n\t/**\n\t * Retrieve a list of LLMS Form post objects\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return WP_Post[]\n\t */\n\tprivate function get_form_posts() {\n\n\t\t$forms = new WP_Query( array( 'post_type' => 'llms_form' ) );\n\t\treturn $forms->posts;\n\n\t}\n\n\t/**\n\t * Test get_reusable_blcoks()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_reusable_blocks() {\n\n\t\tLLMS_Forms::instance()->install();\n\n\t\t$list = $this->main->get_reusable_blocks();\n\n\t\tforeach ( $list as $id ) {\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t\t$block = get_post( $id );\n\t\t\t$this->assertEquals( 'wp_block', $block->post_type );\n\t\t\t$this->assertStringContains( '(Reusable)', $block->post_title );\n\t\t\t$this->assertNotEmpty( get_post_meta( $id, '_llms_field_id', true ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test handle()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle() {\n\n\t\tLLMS_Forms::instance()->install();\n\n\t\tforeach ( $this->get_form_posts() as $form ) {\n\t\t\twp_update_post( array(\n\t\t\t\t'ID'           => $form->ID,\n\t\t\t\t'post_content' => 'overwritten',\n\t\t\t) );\n\t\t}\n\n\t\t$original_blocks = $this->get_block_posts();\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) );\n\n\t\tforeach ( $this->get_form_posts() as $form ) {\n\t\t\t$this->assertNotEquals( 'overwritten', $form->post_content );\n\t\t}\n\n\t\t$new_blocks = $this->get_block_posts();\n\t\t$this->assertNotEmpty( $new_blocks );\n\t\tforeach ( $original_blocks as $id ) {\n\t\t\t$this->assertFalse( in_array( $id, $new_blocks, true ) );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-limited-billing-order-locator.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Tool_Limited_Billing_Order_Locator class.\n *\n * @package LifterLMS/Tests/Admins/Tools\n *\n * @group admin\n * @group admin_tools\n * @group limited_billing\n *\n * @since 5.3.0\n * @version 5.4.0\n */\nclass LLMS_Test_Admin_Tool_Limited_Billing_Order_Locator extends LLMS_Admin_Tool_Test_Case {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * @var sting\n\t */\n\tconst CLASS_NAME = 'LLMS_Admin_Tool_Limited_Billing_Order_Locator';\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\t$this->clear_cache();\n\t}\n\n\t/**\n\t * Clear cached tool data.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function clear_cache() {\n\t\twp_cache_delete( 'limited-billing-order-locator', 'llms_tool_data' );\n\t}\n\n\t/**\n\t * Create mock orders.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param int   $count Number of orders.\n\t * @param array $meta  Order meta data.\n\t * @param array $args  Additional args.\n\t * @return int[]\n\t */\n\tprivate function create_mock_orders( $count, $meta = array(), $args = array() ) {\n\n\t\treturn $this->factory->post->create_many( $count, wp_parse_args( $args, array(\n\t\t\t'post_type'   => 'llms_order',\n\t\t\t'post_status' => 'llms-active',\n\t\t\t'meta_input'  => $meta,\n\t\t) ) );\n\n\t}\n\n\t/**\n\t * Test generate_csv().\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_csv() {\n\n\t\t$this->assertEquals( 0, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) );\n\n\t\t// Not qualifying.\n\t\t$this->create_mock_orders( 1 );\n\t\t$this->assertEquals( 0, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) );\n\n\t\t// Has length but wrong status.\n\t\t$this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05' ), array( 'post_status' => 'llms-cancelled' ) );\n\t\t$this->assertEquals( 0, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) );\n\n\t\t// Qualifying.\n\t\t$this->create_mock_orders( 2, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05', '_llms_plan_ended' => 'yes' ) );\n\t\t$this->assertEquals( 2, count( LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' ) ) );\n\n\t}\n\n\t/**\n\t * Test get_order_csv(): doesn't quality because the order hasn't ended and there's no refunds.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_order_csv_not_ended_no_refunds() {\n\n\t\t$order = llms_get_post( $this->create_mock_orders( 1, array( '_llms_billing_length' => 2 ) )[0] );\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) );\n\n\t}\n\n\t/**\n\t * Test get_order_csv(): Doesn't qualify because it has ended but has the expected number of payments.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_order_right_number_of_payments() {\n\n\t\t$order = llms_get_post( $this->create_mock_orders( 1, array( '_llms_billing_length' => 2 ) )[0] );\n\t\t$order->record_transaction();\n\t\t$order->record_transaction();\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) );\n\n\t}\n\n\t/**\n\t * Test get_order_csv(): Qualifies because it has ended and is missing a payment.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_order_missing_payments() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tLLMS_Post_Types::register_post_types();\n\n\t\t$order_id = $this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) )[0];\n\t\t$order = llms_get_post( $order_id );\n\t\t$expect = array( $order_id, 2, 1, 1, 0, get_edit_post_link( $order_id, 'raw' ) );\n\t\t$order->record_transaction();\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) );\n\n\t}\n\n\t/**\n\t * Test get_order_csv(): Qualifies because it has a refund.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_order_has_refund() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tLLMS_Post_Types::register_post_types();\n\n\t\t$order_id = $this->create_mock_orders( 1, array( '_llms_billing_length' => 5 ) )[0];\n\t\t$order = llms_get_post( $order_id );\n\t\t$expect = array( $order_id, 5, 1, 0, 1, get_edit_post_link( $order_id, 'raw' ) );\n\t\t$order->record_transaction( array( 'status' => 'llms-txn-refunded' ) );\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_order_csv', array( $order ) ) );\n\n\t}\n\n\t/**\n\t * Test get_csv() when there's nothing cached.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_csv_cache_miss() {\n\n\t\t$this->clear_cache();\n\n\t\t$this->create_mock_orders( 2, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) );\n\t\t$expect = LLMS_Unit_Test_Util::call_method( $this->main, 'generate_csv' );\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_csv' ) );\n\n\t\t// Should be cached.\n\t\t$this->assertEquals( $expect, wp_cache_get( 'limited-billing-order-locator', 'llms_tool_data' ) );\n\n\t}\n\n\t/**\n\t * Test get_csv() when there's cached results.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_csv_cache_hit() {\n\n\t\twp_cache_set( 'limited-billing-order-locator', 'fake', 'llms_tool_data' );\n\t\t$this->assertEquals( 'fake', LLMS_Unit_Test_Util::call_method( $this->main, 'get_csv' ) );\n\n\t}\n\n\t/**\n\t * Test handle().\n\t *\n\t * @since 5.3.0\n\t * @since 5.4.0 Made sure to compare the lists of orders with the same ordering.\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tLLMS_Post_Types::register_post_types();\n\n\t\t// Included.\n\t\t$orders = $this->create_mock_orders( 3, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05', '_llms_plan_ended' => 'yes' ) );\n\n\t\t// Not included bc it was created after the migration..\n\t\t$this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) );\n\n\t\ttry {\n\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'handle' );\n\n\t\t} catch ( LLMS_Unit_Test_Exception_Exit $exception ) {\n\n\t\t\t$csv = $exception->get_status();\n\n\t\t\t$this->assertTrue( is_string( $csv ) );\n\n\t\t\t$lines = explode( \"\\n\", $csv );\n\t\t\t$this->assertEquals( '\"Order ID\",\"Expected Payments\",\"Total Payments\",\"Successful Payments\",\"Refunded Payments\",\"Edit Link\"', $lines[0] );\n\t\t\tarray_shift( $lines );\n\t\t\t$orders = array_reverse( $orders ); // Orders affected by the change ($lines) are ordered by their `ID` `DESC`.\n\n\t\t\tforeach ( $lines as $i => $line ) {\n\t\t\t\t// Empty line at the end of the file.\n\t\t\t\tif ( 3 === $i ) {\n\t\t\t\t\t$this->assertEmpty( $line );\n\t\t\t\t} else {\n\t\t\t\t\t$link = get_edit_post_link( $orders[ $i ], 'raw' );\n\t\t\t\t\t$this->assertEquals( \"{$orders[ $i ]},2,0,0,0,{$link}\", $line );\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test should_load()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load() {\n\n\t\t// Shouldn't load.\n\t\t$this->create_mock_orders( 1 );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\n\t\t// Created after upgrade, shouldn't load.\n\t\t$this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_plan_ended' => 'yes' ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\n\t\t// Should load.\n\t\t$this->create_mock_orders( 1, array( '_llms_billing_length' => 2, '_llms_date_billing_end' => '2021-05-05', '_llms_plan_ended' => 'yes' ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-recurring-payment-rescheduler.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Tool_Recurring_Payment_Rescheduler class\n *\n * @package LifterLMS/Tests/Admins/Tools\n *\n * @group admin\n * @group admin_tools\n * @group recurring_rescheduler\n *\n * @since 4.6.0\n * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests.\n */\nclass LLMS_Test_Admin_Tool_Recurring_Payment_Rescheduler extends LLMS_Admin_Tool_Test_Case {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * @var sting\n\t */\n\tconst CLASS_NAME = 'LLMS_Admin_Tool_Recurring_Payment_Rescheduler';\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 4.6.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\t$this->clear_cache();\n\n\t}\n\n\t/**\n\t * Create N number of orders in the DB\n\t *\n\t * @since 4.6.0\n\t *\n\t * @param integer $count         Number of orders to create.\n\t * @param boolean $remove_action Whether or not to remove a scheduled payment action.\n\t *                               If `true`, creates orders that would be handled by the tool, otherwise creates orders\n\t *                               that should be missed by the tool's queries.\n\t * @return int[] An array of WP_Post IDs for the created orders.\n\t */\n\tprivate function create_orders_to_handle( $count = 3, $remove_action = true ) {\n\n\t\t$orders = array();\n\n\t\t$i = 1;\n\t\twhile ( $i <= $count ) {\n\n\t\t\t$order = $this->get_mock_order();\n\t\t\t$order->set_status( 'llms-active' );\n\t\t\t$order->maybe_schedule_payment();\n\n\t\t\tif ( $remove_action ) {\n\t\t\t\t$order->unschedule_recurring_payment();\n\t\t\t}\n\n\t\t\t$orders[] = $order->get( 'id' );\n\n\t\t\t++$i;\n\t\t}\n\n\t\treturn $orders;\n\n\t}\n\n\t/**\n\t * Clear cached batch count data.\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return void\n\t */\n\tprivate function clear_cache() {\n\t\twp_cache_delete( 'recurring-payment-rescheduler', 'llms_tool_data' );\n\t\twp_cache_delete( 'recurring-payment-rescheduler-total-results', 'llms_tool_data' );\n\t}\n\n\t/**\n\t * Test get_orders() during a cache hit\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_orders_cache_hit() {\n\n\t\twp_cache_set( 'recurring-payment-rescheduler', 'mock cache', 'llms_tool_data' );\n\t\t$this->assertEquals( 'mock cache',  LLMS_Unit_Test_Util::call_method( $this->main, 'get_orders' ) );\n\n\t}\n\n\t/**\n\t * Test get_orders() during a cache miss\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_orders_cache_miss() {\n\n\t\t$orders = $this->create_orders_to_handle();\n\n\t\t// Order IDs returned.\n\t\t$this->assertEqualSets( $orders, LLMS_Unit_Test_Util::call_method( $this->main, 'get_orders' ) );\n\n\t\t// Cache is set.\n\t\t$this->assertEqualSets( $orders, wp_cache_get( 'recurring-payment-rescheduler', 'llms_tool_data' ) );\n\n\t}\n\n\t/**\n\t * Test handle()\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle() {\n\n\t\t$orders = $this->create_orders_to_handle();\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'handle' );\n\n\t\t// All expected orders were handled.\n\t\t$this->assertEqualSets( $orders, $res );\n\n\t\t// Cache erased.\n\t\t$this->assertFalse( wp_cache_get( 'recurring-payment-rescheduler', 'llms_tool_data' ) );\n\n\t\tforeach ( $res as $id ) {\n\n\t\t\t$order = llms_get_post( $id );\n\n\t\t\t// Action is rescheduled.\n\t\t\t$this->assertEquals( $order->get_next_payment_due_date( 'U' ), $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test handle() properly handles \"legacy\" orders that don't have `plan_ended()` meta data.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_orders_with_no_meta() {\n\n\t\t// Force a WP_Error to be returned by LLMS_Order::get_next_payment_due_date().\n\t\tadd_filter( 'llms_order_calculate_next_payment_date', '__return_empty_string' );\n\n\t\t$orders = $this->create_orders_to_handle( 1 );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'handle' );\n\n\t\t// No orders handled.\n\t\t$this->assertEquals( array(), $res );\n\n\t\t// The missing metadata has been added by the tool.\n\t\t$this->assertEquals( 'yes', llms_get_post( $orders[0] )->get( 'plan_ended' ) );\n\n\t\tremove_filter( 'llms_order_calculate_next_payment_date', '__return_empty_string' );\n\n\t}\n\n\t/**\n\t * Test query_orders()\n\t *\n\t * @since 4.6.0\n\t * @since 4.7.0 Add an order with `plan_ended` meta that should be ignored and add tests for `FOUND_ROWS()` cached data.\n\t * @since 7.0.1 Remove reference to undefined property.\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_orders() {\n\n\t\t// No orders.\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'query_orders' ) );\n\n\t\t// Should be found.\n\t\t$to_handle = $this->create_orders_to_handle();\n\n\t\t// This order should not be in the returned array.\n\t\t$to_ignore = $this->create_orders_to_handle( 1, false );\n\n\t\t// Ignored because of `plan_ended` meta data.\n\t\t$to_ignore_2 = $this->create_orders_to_handle( 1 );\n\t\tllms_get_post( $to_ignore_2[0] )->set( 'plan_ended', 'yes' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'query_orders' );\n\n\t\t$this->assertEqualSets( $to_handle, wp_list_pluck( $res, 'ID' ) );\n\n\t\t// Test FOUND_ROWS() cache data.\n\t\t$this->assertEquals( 3, wp_cache_get( 'recurring-payment-rescheduler-total-results', 'llms_tool_data' ) );\n\n\t}\n\n\t/**\n\t * Test should_load()\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load() {\n\n\t\t// No orders to handle.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\n\t\t// Orders to handle.\n\t\t$this->create_orders_to_handle( 1 );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-reset-automatic-payments.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Tool_Reset_Automatic_Payments class\n *\n * @package LifterLMS/Tests/Admins/Tools\n *\n * @group admin\n * @group admin_tools\n * @group reset_payments\n *\n * @since 4.13.0\n * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests.\n */\nclass LLMS_Test_Admin_Tool_Reset_Automatic_Payments extends LLMS_Admin_Tool_Test_Case {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * @var sting\n\t */\n\tconst CLASS_NAME = 'LLMS_Admin_Tool_Reset_Automatic_Payments';\n\n\t/**\n\t * Test handle()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle() {\n\n\t\t$actions = did_action( 'llms_site_clone_detected' );\n\n\t\t// Get the original values of options to be cleared.\n\t\t$orig_url    = get_option( 'llms_site_url' );\n\t\t$orig_ignore = get_option( 'llms_site_url_ignore' );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( sprintf( '%s [302] YES', admin_url( 'admin.php?page=llms-status&tab=tools') ) );\n\n\t\ttry {\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'handle' );\n\t\t} catch( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\t$this->assertEquals( '', get_option( 'llms_site_url' ) );\n\t\t\t$this->assertEquals( 'no', get_option( 'llms_site_url_ignore' ) );\n\t\t\t$this->assertEquals( ++$actions, did_action( 'llms_site_clone_detected' ) );\n\n\t\t\t// Reset to the orig values.\n\t\t\tupdate_option( 'llms_site_url', $orig_url );\n\t\t\tupdate_option( 'llms_site_url_ignore', $orig_ignore );\n\n\t\t\tthrow $exception;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test should_load() with no constants set.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load() {\n\t\t$this->assertTrue( true, LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\t}\n\n\t/**\n\t * Test should_load() with LLMS_SITE_IS_CLONE constant set.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load_with_site_clone_constant_set() {\n\t\tdefine( 'LLMS_SITE_IS_CLONE', false );\n\t\t$this->assertTrue( true, LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\t}\n\n\t/**\n\t * Test should_load() with LLMS_SITE_FEATURE_RECURRING_PAYMENTS constant set.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load_with_recurring_payments_constant_set() {\n\t\tdefine( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS', false );\n\t\t$this->assertTrue( true, LLMS_Unit_Test_Util::call_method( $this->main, 'should_load' ) );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/admin/tools/class-llms-test-admin-tool-wipe-legacy-account-options.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Admin_Tool_Wipe_Legacy_Account_Options class\n *\n * @package LifterLMS/Tests/Admins/Tools\n *\n * @group admin\n * @group admin_tools\n * @group legacy_opts\n *\n * @since 5.0.0\n * @since 5.3.0 Use `LLMS_Admin_Tool_Test_Case` and remove redundant methods/tests.\n */\nclass LLMS_Test_Admin_Tool_Wipe_Legacy_Account_Options extends LLMS_Admin_Tool_Test_Case {\n\n\t/**\n\t * Name of the class being tested.\n\t *\n\t * @var sting\n\t */\n\tconst CLASS_NAME = 'LLMS_Admin_Tool_Wipe_Legacy_Account_Options';\n\n\tconst LEGACY_OPTIONS = array(\n\t\t'lifterlms_registration_generate_username',\n\t\t'lifterlms_registration_password_strength',\n\t\t'lifterlms_registration_password_min_strength',\n\t\t'lifterlms_user_info_field_names_checkout_visibility',\n\t\t'lifterlms_user_info_field_address_checkout_visibility',\n\t\t'lifterlms_user_info_field_phone_checkout_visibility',\n\t\t'lifterlms_user_info_field_email_confirmation_checkout_visibility',\n\t\t'lifterlms_user_info_field_names_registration_visibility',\n\t\t'lifterlms_user_info_field_address_registration_visibility',\n\t\t'lifterlms_user_info_field_phone_registration_visibility',\n\t\t'lifterlms_user_info_field_email_confirmation_registration_visibility',\n\t\t'lifterlms_voucher_field_registration_visibility',\n\t\t'lifterlms_user_info_field_names_account_visibility',\n\t\t'lifterlms_user_info_field_address_account_visibility',\n\t\t'lifterlms_user_info_field_phone_account_visibility',\n\t\t'lifterlms_user_info_field_email_confirmation_account_visibility',\n\t);\n\n\t/**\n\t * Tear Down\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\t$this->delete_legacy_options();\n\n\t}\n\n\t/**\n\t * Test handle()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle() {\n\n\t\tglobal $wpdb;\n\n\t\t$sql = \"\n\t\tSELECT COUNT(*) FROM {$wpdb->options}\n\t\tWHERE option_name IN (\" . implode( ', ', array_fill( 0, count( self::LEGACY_OPTIONS ), '%s' ) ) . ')';\n\n\t\t$query = $wpdb->prepare(\n\t\t\t$sql,\n\t\t\tself::LEGACY_OPTIONS\n\t\t);\n\n\t\t$this->assertEquals( 0, $wpdb->get_var( $query ) );\n\n\t\t$this->add_legacy_options();\n\n\t\t$this->assertEquals( count( self::LEGACY_OPTIONS ), $wpdb->get_var( $query ) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'handle' ) );\n\n\t\t$this->assertEquals( 0, $wpdb->get_var( $query ) );\n\n\t}\n\n\t/**\n\t * Test can_load()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_load() {\n\t\t$this->assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'should_load' )\n\t\t);\n\n\t\t$this->add_legacy_options();\n\n\t\t$this->assertTrue(\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'should_load' )\n\t\t);\n\n\t\t// Check that the tool doesn't load after it has been handled.\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'handle' );\n\n\t\t$this->assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'should_load' )\n\t\t);\n\n\t}\n\n\t/**\n\t * Add legacy options to the WP options table\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function add_legacy_options() {\n\n\t\tarray_map( 'add_option', self::LEGACY_OPTIONS, array_fill( 0, count( self::LEGACY_OPTIONS ), 'yes' ) );\n\n\t}\n\n\n\t/**\n\t * Remove legacy options to the WP options table\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function delete_legacy_options() {\n\n\t\tarray_map( 'delete_option', self::LEGACY_OPTIONS, array_fill( 0, count( self::LEGACY_OPTIONS ), 'yes' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-coupons.php",
    "content": "<?php\n/**\n * Test Coupon-related methods in the LLMS_AJAX_Handler class.\n *\n * @package LifterLMS/Tests/AJAX\n *\n * @group ajax_coupons\n * @group ajax\n * @group coupons\n *\n * @since 3.39.0\n */\nclass LLMS_Test_AJAX_Handler_Coupons extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test remove_coupon_code()\n\t *\n\t * @since 3.39.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_remove_coupon_code() {\n\n\t\tllms()->session->set( 'llms_coupon', 'this-will-be-cleared' );\n\n\t\t$res = LLMS_AJAX_Handler::remove_coupon_code( array(\n\t\t\t'plan_id' => $this->get_mock_plan(),\n\t\t) );\n\n\t\t// HTML returned.\n\t\t$this->assertEquals( array( 'coupon_html', 'gateways_html', 'summary_html' ), array_keys( $res ) );\n\n\t\t$this->assertFalse( llms()->session->get( 'llms_coupon' ) );\n\n\t}\n\n\t/**\n\t * Test validate_coupon_code(): no coupon data supplied\n\t *\n\t * @since 3.39.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_code_none_supplied() {\n\n\t\t$request = array();\n\t\t$res = LLMS_AJAX_Handler::validate_coupon_code( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'error', $res );\n\t\t$this->assertWPErrorMessageEquals( 'Please enter a coupon code.', $res );\n\n\t}\n\n\t/**\n\t * Test validate_coupon_code(): no access plan supplied\n\t *\n\t * @since 3.39.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_code_no_plan() {\n\n\t\t$request = array(\n\t\t\t'code' => 123,\n\t\t);\n\t\t$res = LLMS_AJAX_Handler::validate_coupon_code( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'error', $res );\n\t\t$this->assertWPErrorMessageEquals( 'Please enter a plan ID.', $res );\n\n\t}\n\n\t/**\n\t * Test validate_coupon_code(): coupon not found\n\t *\n\t * @since 3.39.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_code_not_found() {\n\n\t\t$request = array(\n\t\t\t'code' => 123,\n\t\t\t'plan_id' => 456,\n\t\t);\n\t\t$res = LLMS_AJAX_Handler::validate_coupon_code( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'error', $res );\n\t\t$this->assertWPErrorMessageEquals( 'Coupon code \"123\" not found.', $res );\n\n\t}\n\n\t/**\n\t * Test validate_coupon_code(): coupon invalid for the given plan\n\t *\n\t * @since 3.39.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_code_invalid() {\n\n\t\t$coupon = new LLMS_Coupon( 'new', 'couponname' );\n\t\t$coupon->set( 'status', 'publish' );\n\t\t$coupon->set( 'coupon_courses', array( $this->factory->post->create() ) );\n\n\t\t$request = array(\n\t\t\t'code' => 'couponname',\n\t\t\t'plan_id' => $this->get_mock_plan(),\n\t\t);\n\t\t$res = LLMS_AJAX_Handler::validate_coupon_code( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'error', $res );\n\t\t$this->assertStringContains( 'This coupon cannot be used to purchase', $res->get_error_message() );\n\n\t}\n\n\t/**\n\t * Test validate_coupon_code(): coupon code is valid\n\t *\n\t * @since 3.39.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_code_valid() {\n\n\t\t$coupon = new LLMS_Coupon( 'new', 'couponname' );\n\t\t$coupon->set( 'status', 'publish' );\n\n\t\t$request = array(\n\t\t\t'code' => 'couponname',\n\t\t\t'plan_id' => $this->get_mock_plan(),\n\t\t);\n\n\t\t$res = LLMS_AJAX_Handler::validate_coupon_code( $request );\n\n\t\t// HTML returned.\n\t\t$this->assertEquals( array( 'code', 'coupon_html', 'gateways_html', 'summary_html' ), array_keys( $res ) );\n\n\t\t// Session data set.\n\t\t$expect = array(\n\t\t\t'plan_id'   => $request['plan_id'],\n\t\t\t'coupon_id' => $coupon->get( 'id' ),\n\t\t);\n\t\t$this->assertEquals( $expect, llms()->session->get( 'llms_coupon' ) );\n\n\t}\n\n\t/**\n\t * Test validate_coupon_code(): prevent reflected xss\n\t *\n\t * Input is only a tag that will be stripped resulting in an empty response.\n\t *\n\t * @since 4.21.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_code_sanitization_empty_result() {\n\n\t\t$request = array(\n\t\t\t'code' => '<img src=\"#\">',\n\t\t);\n\t\t$res = LLMS_AJAX_Handler::validate_coupon_code( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'error', $res );\n\t\t$this->assertWPErrorMessageEquals( 'Please enter a coupon code.', $res );\n\n\t}\n\n\t/**\n\t * Test validate_coupon_code(): prevent reflected xss\n\t *\n\t * Input is text mixed with a a tag that will be stripped resulting in a not found error.\n\t *\n\t * @since 4.21.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_code_sanitization_mixed_result() {\n\n\t\t$request = array(\n\t\t\t'code'    => 'FAKE_CODE<script>alert(1);</script>_WITH_TAGS',\n\t\t\t'plan_id' => 123,\n\t\t);\n\t\t$res = LLMS_AJAX_Handler::validate_coupon_code( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'error', $res );\n\t\t$this->assertWPErrorMessageEquals( 'Coupon code \"FAKE_CODE_WITH_TAGS\" not found.', $res );\n\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/ajax/class-llms-test-ajax-handler-quizzes.php",
    "content": "<?php\n/**\n * Test Quizzes-related methods in the LLMS_AJAX_Handler class.\n *\n * @package LifterLMS/Tests/AJAX\n *\n * @group ajax_quizzes\n * @group ajax\n * @group quizzes\n *\n * @since 6.4.0\n */\nclass LLMS_Test_AJAX_Handler_Quizzes extends LLMS_UnitTestCase {\n\n\t/**\n\t * Student instance.\n\t *\n\t * @var LLMS_Student\n\t */\n\tprotected $student;\n\n\t/**\n\t * Quiz's lesson instance.\n\t *\n\t * @var LLMS_Lesson\n\t */\n\tprotected $lesson;\n\n\t/**\n\t * Quiz instance.\n\t *\n\t * @var LLMS_Quiz\n\t */\n\tprotected $quiz;\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$this->student = $this->get_mock_student();\n\t\t// Create new course with quiz.\n\t\t$courses      = $this->generate_mock_courses( 1, 1, 1, 1, 1 );\n\t\t$course       = llms_get_post( $courses[0] );\n\t\t$this->lesson = $course->get_lessons()[0];\n\t\t$this->quiz   = $this->lesson->get_quiz();\n\n\t}\n\n\t/**\n\t * Test quiz_start() when no student logged in.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_quiz_start_no_student() {\n\n\t\twp_set_current_user( 0 );\n\n\t\t$res = LLMS_AJAX_Handler::quiz_start(\n\t\t\tarray()\n\t\t);\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 400, $res );\n\t\t$this->assertWPErrorMessageEquals( 'You must be logged in to take quizzes.', $res );\n\n\t}\n\n\t/**\n\t * Test attempts limit check in quiz_start().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_quiz_start_test_attempts_limit() {\n\n\t\twp_set_current_user( $this->student->get( 'id' ) );\n\n\t\t// Limit quiz's attempts.\n\t\t$this->quiz->set( 'limit_attempts', 'yes' );\n\t\t$this->quiz->set( 'allowed_attempts', 1 );\n\n\t\t// Record an attempt so to reach the limit.\n\t\t$attempt = LLMS_Quiz_Attempt::init(\n\t\t\t$this->quiz->get( 'id' ),\n\t\t\t$this->lesson->get( 'id' ),\n\t\t\t$this->student->get( 'id' )\n\t\t);\n\t\t$attempt->save();\n\n\t\t$res = LLMS_AJAX_Handler::quiz_start(\n\t\t\tarray(\n\t\t\t\t'quiz_id' => $this->quiz->get( 'id' ),\n\t\t\t)\n\t\t);\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 400, $res );\n\t\t$this->assertWPErrorMessageEquals( \"You've reached the maximum number of attempts for this quiz.\", $res );\n\n\t\t// Increase the limit.\n\t\t$this->quiz->set( 'allowed_attempts', 2 );\n\t\t$res = LLMS_AJAX_Handler::quiz_start(\n\t\t\tarray(\n\t\t\t\t'quiz_id'   => $this->quiz->get( 'id' ),\n\t\t\t\t'lesson_id' => $this->lesson->get( 'id' ),\n\t\t\t)\n\t\t);\n\t\t// Attempt recorded.\n\t\t$this->assertArrayHasKey(\n\t\t\t'attempt_key',\n\t\t\t$res\n\t\t);\n\n\t\t// Reset.\n\t\t$this->quiz->set( 'limit_attempts', 'no' );\n\t\twp_set_current_user( 0 );\n\n\t}\n\n\t/**\n\t * Test attempts limit check in quiz_answer_question().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_quiz_answer_question_test_attempts_limit() {\n\n\t\twp_set_current_user( $this->student->get( 'id' ) );\n\n\t\t// Start a quiz first.\n\t\t$res = LLMS_AJAX_Handler::quiz_start(\n\t\t\tarray(\n\t\t\t\t'quiz_id'   => $this->quiz->get( 'id' ),\n\t\t\t\t'lesson_id' => $this->lesson->get( 'id' ),\n\t\t\t)\n\t\t);\n\n\t\t$attempt_key = $res['attempt_key'];\n\t\t$student_quizzes = $this->student->quizzes();\n\n\t\t// Limit quiz's attempts so that this attempt is the last possible.\n\t\t$this->quiz->set( 'limit_attempts', 'yes' );\n\t\t$this->quiz->set( 'allowed_attempts', $student_quizzes->count_attempts_by_quiz( $this->quiz->get( 'id' ) ) );\n\n\t\t$question = $this->quiz->get_questions()[0];\n\n\t\t$res = LLMS_AJAX_Handler::quiz_answer_question(\n\t\t\tarray(\n\t\t\t\t'question_id'   => $this->quiz->get( 'id' ),\n\t\t\t\t'attempt_key'   => $attempt_key,\n\t\t\t\t'question_id'   => $question->get( 'id' ),\n\t\t\t\t'question_type' => $question->get( 'type' ),\n\t\t\t)\n\t\t);\n\t\t// Quiz completed, expect an array with 'redirect' key.\n\t\t$this->assertArrayHasKey(\n\t\t\t'redirect',\n\t\t\t$res\n\t\t);\n\n\t\t// Now increase the limit.\n\t\t$this->quiz->set( 'allowed_attempts', $this->quiz->get( 'allowed_attempts' ) + 1 );\n\t\t// Start the quiz again.\n\t\t$res = LLMS_AJAX_Handler::quiz_start(\n\t\t\tarray(\n\t\t\t\t'quiz_id'   => $this->quiz->get( 'id' ),\n\t\t\t\t'lesson_id' => $this->lesson->get( 'id' ),\n\t\t\t)\n\t\t);\n\t\t// Decrease the limit.\n\t\t$this->quiz->set( 'allowed_attempts', $this->quiz->get( 'allowed_attempts' ) - 1 );\n\n\t\t$res = LLMS_AJAX_Handler::quiz_answer_question(\n\t\t\tarray(\n\t\t\t\t'question_id'   => $this->quiz->get( 'id' ),\n\t\t\t\t'attempt_key'   => $attempt_key,\n\t\t\t\t'question_id'   => $question->get( 'id' ),\n\t\t\t\t'question_type' => $question->get( 'type' ),\n\t\t\t)\n\t\t);\n\n\t\t// The status is \"pass\" so it won't be possible to answer the question again anyway (500 vs 400).\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 500, $res );\n\t\t$this->assertWPErrorMessageEquals( \"There was an error recording your answer. Please return to the lesson and begin again.\", $res );\n\n\t\t// Reset.\n\t\t$this->quiz->set( 'limit_attempts', 'no' );\n\t\twp_set_current_user( 0 );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-admin-media-protection-attachment-settings.php",
    "content": "<?php\n/**\n * LLMS_Admin_Media_Protection_Attachment_Settings tests.\n *\n * @package LifterLMS/Tests\n *\n * @group admin\n * @group media_protection\n *\n * @since 10.0.0\n */\nclass LLMS_Test_Admin_Media_Protection_Attachment_Settings extends LLMS_UnitTestCase {\n\n\t/**\n\t * Load the class file.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class-llms-admin-media-protection-attachment-settings.php';\n\n\t}\n\n\t/**\n\t * Test the attachment field can be filtered.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_attachment_fields_to_edit_allows_media_protection_field_filter() {\n\n\t\t$attachment = get_post(\n\t\t\t$this->factory->post->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => 'attachment',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$filter = function( $field, $post, $protector ) use ( $attachment ) {\n\t\t\t$this->assertEquals( $attachment->ID, $post->ID );\n\t\t\t$this->assertInstanceOf( 'LLMS_Media_Protector', $protector );\n\n\t\t\t$field['html']  = '<p>Filtered media protection output.</p>';\n\t\t\t$field['helps'] = 'Filtered help text.';\n\n\t\t\treturn $field;\n\t\t};\n\n\t\tadd_filter( 'llms_media_protection_attachment_field', $filter, 10, 3 );\n\n\t\t$settings = new LLMS_Admin_Media_Protection_Attachment_Settings();\n\t\t$fields   = $settings->attachment_fields_to_edit( array(), $attachment );\n\n\t\tremove_filter( 'llms_media_protection_attachment_field', $filter, 10 );\n\n\t\t$this->assertEquals( '<p>Filtered media protection output.</p>', $fields['llms_media_protection_post']['html'] );\n\t\t$this->assertEquals( 'Filtered help text.', $fields['llms_media_protection_post']['helps'] );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-ajax-handler.php",
    "content": "<?php\n/**\n * Test AJAX Handler\n *\n * @package LifterLMS/Tests\n *\n * @group AJAX\n *\n * @since 3.32.0\n * @since 3.37.2 Added tests on querying courses/memberships filtererd by instructors.\n * @since 3.37.14 Added tests on persisting tracking events.\n * @since 3.37.15 Added tests for admin table methods.\n * @since 5.5.0 Added tests on select2_query_posts when searching terms with quotes.\n */\nclass LLMS_Test_AJAX_Handler extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 4.7.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/class.llms.admin.reporting.php';\n\t}\n\n\t/**\n\t * Setup the test\n\t *\n\t * @since 3.32.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\tadd_filter( 'wp_die_handler', array( $this, '_wp_die_handler' ), 1 );\n\t}\n\n\t/**\n\t * Teardown the test\n\t *\n\t * @since 3.32.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tremove_filter( 'wp_die_handler', array( $this, '_wp_die_handler' ), 1 );\n\t}\n\n\t/**\n\t * Call a method for the LLMS_AJAX_Handler class that calls wp_die()\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $function Method name.\n\t * @param array  $args     $_REQUEST args.\n\t * @return array\n\t */\n\tprotected function do_ajax( $function, $args = array() ) {\n\n\t\tob_start();\n\t\t$this->mockPostRequest( $args );\n\t\ttry {\n\t\t\tcall_user_func( array( 'LLMS_AJAX_Handler', $function ), $args );\n\t\t} catch ( WPAjaxDieContinueException $e ) {}\n\t\treturn json_decode( $this->last_response, true );\n\n\t}\n\n\t/**\n\t * Test export_admin_table()\n\t *\n\t * @since 3.37.15\n\t *\n\t * @return void\n\t */\n\tpublic function test_export_admin_table() {\n\n\t\t$expected_keys = array( 'filename', 'progress', 'url' );\n\t\tforeach( array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ) as $role ) {\n\t\t\twp_set_current_user( $this->factory->user->create( array( 'role' => $role ) ) );\n\t\t\t$res = LLMS_AJAX_Handler::export_admin_table( array( 'handler' => 'Students' ) );\n\t\t\t$this->assertEquals( $expected_keys, array_keys( $res ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test export_admin_table() with invalid handlers\n\t *\n\t * @since 3.37.15\n\t *\n\t * @return void\n\t */\n\tpublic function test_export_admin_table_invalid_handler() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// No handler.\n\t\t$this->assertNull( $this->do_ajax( 'export_admin_table', array() ) );\n\n\t\t// Invalid handler.\n\t\t$this->assertNull( $this->do_ajax( 'export_admin_table', array( 'handler' => 'fake' ) ) );\n\n\t}\n\n\t/**\n\t * Test export_admin_table() ensuring only users with proper permissions can access.\n\t *\n\t * @since  3.37.15\n\t *\n\t * @return void\n\t */\n\tpublic function test_export_admin_table_invalid_permissions() {\n\n\t\t// No user.\n\t\t$this->assertNull( $this->do_ajax( 'export_admin_table', array( 'handler' => 'Students' ) ) );\n\n\t\t// Student.\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\t$this->assertNull( $this->do_ajax( 'export_admin_table', array( 'handler' => 'Students' ) ) );\n\n\t}\n\n\t/**\n\t * Test get_admin_table_data()\n\t *\n\t * @since 3.37.15\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_admin_table_data() {\n\n\t\t$expected_keys = array( 'args', 'thead', 'tbody', 'tfoot' );\n\n\t\tforeach( array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ) as $role ) {\n\n\t\t\twp_set_current_user( $this->factory->user->create( array( 'role' => $role ) ) );\n\t\t\t$res = LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'Students' ) );\n\t\t\t$this->assertEquals( $expected_keys, array_keys( $res ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_admin_table_data() when invalid handlers are submitted.\n\t *\n\t * @since  3.37.15\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_admin_table_data_invalid_handler() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// No handler.\n\t\t$this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array() ) );\n\n\t\t// Invalid handler.\n\t\t$this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'fake' ) ) );\n\n\t}\n\n\t/**\n\t * Test get_admin_table_data() ensuring only users with proper permissions can access.\n\t *\n\t * @since  3.37.15\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_admin_table_data_invalid_permissions() {\n\n\t\t// No user.\n\t\t$this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'Students' ) ) );\n\n\t\t// Student.\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\t$this->assertFalse( LLMS_AJAX_Handler::get_admin_table_data( array( 'handler' => 'Students' ) ) );\n\n\t}\n\n\t/**\n\t * Test the select2_query_posts() ajax method.\n\t *\n\t * @since 3.32.0\n\t * @since 3.37.2 Added tests on querying courses/memberships filtererd by instructors.\n\t *\n\t * @return void\n\t */\n\tpublic function test_select2_query_posts() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\t\t$args = array(\n\t\t\t'post_type' => 'course',\n\t\t);\n\n\t\t// No results.\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 0, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertFalse( $res['more'] );\n\n\t\t$this->factory->post->create_many( 50, array(\n\t\t\t'post_type' => 'course',\n\t\t) );\n\n\t\t// Full result list.\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 30, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertTrue( $res['more'] );\n\n\t\t// Second page.\n\t\t$args['page'] = 1;\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 20, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertFalse( $res['more'] );\n\n\t\t// Term not found.\n\t\tunset( $args['page'] );\n\t\t$args['term'] = 'arstarstarst';\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 0, count( $res['items'] ) );\n\n\t\t// Term found.\n\t\t$args['term'] = 'title';\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertTrue( count( $res['items'] ) >= 1 );\n\n\t\t$this->factory->post->create_many( 5, array(\n\t\t\t'post_title' => 'search title',\n\t\t) );\n\t\t$this->factory->post->create_many( 5, array(\n\t\t\t'post_type'  => 'course',\n\t\t\t'post_title' => 'search title',\n\t\t) );\n\n\t\t// multiple post types.\n\t\t$args['post_type'] .= ',post';\n\t\t$args['term'] = 'search title';\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertTrue( array_key_exists( 'post', $res['items'] ) );\n\t\t$this->assertSame( 'Posts', $res['items']['post']['label'] );\n\t\t$this->assertTrue( array_key_exists( 'items', $res['items']['post'] ) );\n\t\t$this->assertTrue( array_key_exists( 'course', $res['items'] ) );\n\t\t$this->assertSame( 'Courses', $res['items']['course']['label'] );\n\t\t$this->assertTrue( array_key_exists( 'items', $res['items']['course'] ) );\n\n\t\t// No results, when querying for 'future' posts.\n\t\t$args = array(\n\t\t\t'post_type'     => 'course',\n\t\t\t'post_statuses' => 'future',\n\t\t);\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 0, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertFalse( $res['more'] );\n\n\t\t// create 4 courses in draft.\n\t\t$this->factory->post->create_many( 4, array(\n\t\t\t'post_type'   => 'course',\n\t\t\t'post_status' => 'draft',\n\t\t));\n\n\t\t// 4 results when querying for Courses in 'draft'.\n\t\t$args['post_statuses'] = 'draft';\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 4, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertFalse( $res['more'] );\n\n\t\t// Full result list querying for 'draft' and 'publish' Course statuses.\n\t\t$args['post_statuses'] .= ',publish';\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 30, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertTrue( $res['more'] );\n\n\t\t// Second page querying for 'draft' and 'publish' Course statuses.\n\t\t$args['page'] = 1;\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 29, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertFalse( $res['more'] );\n\n\t\t$this->factory->post->create_many( 1, array(\n\t\t\t'post_title'  => 'search title again',\n\t\t\t'post_status' => 'draft',\n\t\t));\n\t\t$this->factory->post->create_many( 5, array(\n\t\t\t'post_type'  => 'course',\n\t\t\t'post_title' => 'search title again',\n\t\t));\n\n\t\t// Search for multiple post types and multiple status.\n\t\t// Only 1 post in 'draft' and 5 courses 'publish' must be found matching the 'term'.\n\t\tunset( $args['page'] );\n\n\t\t$args['post_type'] .= ',post';\n\t\t$args['term']       = 'search title again';\n\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertTrue( array_key_exists( 'post', $res['items'] ) );\n\t\t$this->assertSame( 'Posts', $res['items']['post']['label'] );\n\t\t$this->assertTrue( array_key_exists( 'items', $res['items']['post'] ) );\n\t\t$this->assertSame( 1, count( $res['items']['post']['items'] ) );\n\t\t$this->assertTrue( array_key_exists( 'course', $res['items'] ) );\n\t\t$this->assertSame( 'Courses', $res['items']['course']['label'] );\n\t\t$this->assertTrue( array_key_exists( 'items', $res['items']['course'] ) );\n\t\t$this->assertSame( 5, count( $res['items']['course']['items'] ) );\n\n\t\t// Search for multiple post types only for the 'draft' status.\n\t\t// Only 1 post in 'draft' and no courses must be found matching the 'term'.\n\t\t$args['post_statuses'] = 'draft';\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertTrue( array_key_exists( 'post', $res['items'] ) );\n\t\t$this->assertSame( 'Posts', $res['items']['post']['label'] );\n\t\t$this->assertTrue( array_key_exists( 'items', $res['items']['post'] ) );\n\t\t$this->assertSame( 1, count( $res['items']['post']['items'] ) );\n\n\t\t// 2 Courses and 2 Memberships when querying for multiple post types limited to a specific instructor id.\n\t\t// create and setup an instructor for the just created 2 Courses and 2 Memberships.\n\t\t$instructor_id = $this->factory->instructor->create();\n\t\tforeach ( array( 'course', 'llms_membership' ) as $post_type ) {\n\t\t\t$ids = $this->factory->post->create_many( 2, array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t));\n\t\t\tforeach ( $ids as $id ) {\n\t\t\t\tllms_get_post( $id )->instructors()->set_instructors( array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'id' => $instructor_id,\n\t\t\t\t\t),\n\t\t\t\t));\n\t\t\t}\n\t\t}\n\n\t\t$args = array(\n\t\t\t'post_type'     => 'course,llms_membership',\n\t\t\t'post_statuses' => 'publish',\n\t\t\t'instructor_id' => $instructor_id,\n\t\t);\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertTrue( array_key_exists( 'course', $res['items'] ) );\n\t\t$this->assertSame( 'Courses', $res['items']['course']['label'] );\n\t\t$this->assertTrue( array_key_exists( 'items', $res['items']['course'] ) );\n\t\t$this->assertSame( 2, count( $res['items']['course']['items'] ) );\n\t\t$this->assertTrue( array_key_exists( 'llms_membership', $res['items'] ) );\n\t\t$this->assertSame( 'Memberships', $res['items']['llms_membership']['label'] );\n\t\t$this->assertTrue( array_key_exists( 'items', $res['items']['llms_membership'] ) );\n\t\t$this->assertSame( 2, count( $res['items']['llms_membership']['items'] ) );\n\n\t}\n\n\t/**\n\t * Test the select2_query_posts() ajax method with search term and quotes.\n\t *\n\t * @since 5.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_select2_query_posts_search_term_quote() {\n\n\t\t$course = $this->factory->post->create( array(\n\t\t\t'post_title'  => 'search title with this quotes:\\'\" - :)',\n\t\t\t'post_type'   => 'course',\n\t\t\t'post_stauts' => 'publish',\n\t\t));\n\n\t\t$args = array(\n\t\t\t'post_type'   => 'course',\n\t\t\t'term'        => 'search title with this quotes:\\'',\n\t\t);\n\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertNull( $res, 'Should not return if no logged in user' );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$res = $this->do_ajax( 'select2_query_posts', $args );\n\t\t$this->assertSame( 1, count( $res['items'] ) );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertSame( $course, (int) $res['items'][0]['id'] );\n\t\t$this->assertSame( 'search title with this quotes:\\'\" - :)' .  \" (ID# $course)\", $res['items'][0]['name'] );\n\n\t}\n\n\t/**\n\t * Test the errors returned by the LLMS_AJAX_Handler::update_student_enrollment() method.\n\t *\n\t * @since 3.33.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_student_enrollment_errors() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$request = array(\n\t\t\t'post_id' => 1,\n\t\t\t'status'  => 'add',\n\t\t);\n\t\t// Missing student_id.\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Missing required parameters', $res->get_error_message() );\n\n\t\t$request = array(\n\t\t\t'student_id' => 1,\n\t\t\t'status'     => 'add',\n\t\t);\n\t\t// Missing post_id.\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Missing required parameters', $res->get_error_message() );\n\n\t\t$request = array(\n\t\t\t'student_id' => 1,\n\t\t\t'post_id'    => 1,\n\t\t);\n\t\t// Missing status.\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Missing required parameters', $res->get_error_message() );\n\n\t\t$request = array(\n\t\t\t'status'     => 'add',\n\t\t\t'student_id' => 1,\n\t\t\t'post_id'    => '',\n\t\t);\n\t\t// Empty post_id ( or student_id, or status) value.\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Missing required parameters', $res->get_error_message() );\n\n\t\t$request = array(\n\t\t\t'status'     => 'enjoy',\n\t\t\t'student_id' => 1,\n\t\t\t'post_id'    => 2,\n\t\t);\n\t\t// status not in ('add', 'remove', 'delete').\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Invalid status', $res->get_error_message() );\n\n\t\t// create a student.\n\t\t$student    = $this->get_mock_student();\n\t\t$student_id = $student->get( 'id' );\n\n\t\t// create a course.\n\t\t$course_id  = $this->generate_mock_courses( 1, 1, 3 )[0];\n\n\t\t$request = array(\n\t\t\t'status'     => 'add',\n\t\t\t'student_id' => $student_id,\n\t\t\t'post_id'    => $course_id + 1,\n\t\t);\n\t\t// 'add' failure: no course.\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Action \"add\" failed. Please try again', $res->get_error_message() );\n\n\t\t// 'remove' failure: student not enrolled in a Course with ID as $course_id.\n\t\t$request['status']  = 'remove';\n\t\t$request['post_id'] = $course_id;\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Action \"remove\" failed. Please try again', $res->get_error_message() );\n\n\t\t// 'delete' failure: student not enrolled in a Course with ID as $course_id.\n\t\t$request['status']  = 'delete';\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( '400', $res );\n\t\t$this->assertSame( 'Action \"delete\" failed. Please try again', $res->get_error_message() );\n\n\t}\n\n\t/**\n\t * Test the update_student_enrollment() method can perform user's enrollment\n\t *\n\t * @since 3.33.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_student_enrollment_enroll() {\n\n\t\t// create a student.\n\t\t$student    = $this->get_mock_student();\n\t\t$student_id = $student->get( 'id' );\n\n\t\t// create a course.\n\t\t$course_id  = $this->generate_mock_courses( 1, 1, 3 )[0];\n\n\t\t$request = array(\n\t\t\t'status'     => 'add',\n\t\t\t'student_id' => $student_id,\n\t\t\t'post_id'    => $course_id,\n\t\t);\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertTrue( $student->is_enrolled( $course_id ) );\n\n\t}\n\n\t/**\n\t * Test the update_student_enrollment() method can perform user's unenrollment\n\t *\n\t * @since 3.33.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_student_enrollment_unenroll() {\n\n\t\t// create a student.\n\t\t$student    = $this->get_mock_student();\n\t\t$student_id = $student->get( 'id' );\n\n\t\t// create a course.\n\t\t$course_id  = $this->generate_mock_courses( 1, 1, 3 )[0];\n\n\t\t// enroll the student in the course.\n\t\t$student->enroll( $course_id );\n\n\t\t$request = array(\n\t\t\t'status'     => 'remove',\n\t\t\t'student_id' => $student_id,\n\t\t\t'post_id'    => $course_id,\n\t\t);\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertFalse( $student->is_enrolled( $course_id ) );\n\n\t}\n\n\t/**\n\t * Test the update_student_enrollment() method can perform user's enrollment deletion\n\t *\n\t * @since 3.33.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_student_enrollment_delete() {\n\n\t\t// create a student.\n\t\t$student    = $this->get_mock_student();\n\t\t$student_id = $student->get( 'id' );\n\n\t\t// create a course.\n\t\t$course_id  = $this->generate_mock_courses( 1, 1, 3 )[0];\n\n\t\t// enroll the student in the course.\n\t\t$student->enroll( $course_id );\n\n\t\t$request = array(\n\t\t\t'status'     => 'delete',\n\t\t\t'student_id' => $student_id,\n\t\t\t'post_id'    => $course_id,\n\t\t);\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$res = LLMS_AJAX_Handler::update_student_enrollment( $request );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$this->assertEquals( array(), llms_get_user_postmeta( $student_id, $course_id ) );\n\n\t}\n\n\t/**\n\t * Test `persist_tracking_events()` ajax callback.\n\t *\n\t * @since 3.37.14\n\t *\n\t * @return void\n\t */\n\tpublic function test_persist_tracking_events() {\n\n\t\t$request = array(\n\t\t\t'something' => 'what'\n\t\t);\n\n\t\t// missing tracking data.\n\t\t$res = LLMS_AJAX_Handler::persist_tracking_events( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'error', $res );\n\t\t$this->assertSame( 'Missing tracking data.', $res->get_error_message() );\n\n\n\t\t// unauthorized, missing tracking nonce or user not logged in.\n\n\t\t// create nonce.\n\t\t// check user not logged in.\n\t\t$request = array(\n\t\t\t'llms-tracking' => json_encode(\n\t\t\t\tarray(\n\t\t\t\t\t'events' => array(),\n\t\t\t\t\t'nonce'  => wp_create_nonce( 'llms-tracking' ),\n\t\t\t\t)\n\t\t\t),\n\t\t);\n\n\t\t$res = LLMS_AJAX_Handler::persist_tracking_events( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_events_tracking_unauthorized', $res );\n\t\t$this->assertSame( 'You\\'re not allowed to store tracking events', $res->get_error_message() );\n\n\t\t// log-in. check missing nonce.\n\t\twp_set_current_user(1);\n\t\t$request = array(\n\t\t\t'llms-tracking' => json_encode(\n\t\t\t\tarray(\n\t\t\t\t\t'events' => array(),\n\t\t\t\t)\n\t\t\t),\n\t\t);\n\n\t\t$res = LLMS_AJAX_Handler::persist_tracking_events( $request );\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_events_tracking_unauthorized', $res );\n\t\t$this->assertSame( 'You\\'re not allowed to store tracking events', $res->get_error_message() );\n\n\t\t// persist events.\n\t\t$request = array(\n\t\t\t'llms-tracking' => json_encode(\n\t\t\t\tarray(\n\t\t\t\t\t'events' => array(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'object_type' => 'user',\n\t\t\t\t\t\t\t'object_id' => 1,\n\t\t\t\t\t\t\t'event' => 'account.signon',\n\t\t\t\t\t\t),\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'object_type' => 'user',\n\t\t\t\t\t\t\t'object_id' => 1,\n\t\t\t\t\t\t\t'event' => 'account.signoff',\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\t'nonce'  => wp_create_nonce( 'llms-tracking' ),\n\t\t\t\t)\n\t\t\t),\n\t\t);\n\n\t\t$res = LLMS_AJAX_Handler::persist_tracking_events( $request );\n\t\t$this->assertTrue( $res['success'] );\n\t\t$events = ( new LLMS_Events_Query( array(\n\t\t\t'actor' => array(1)\n\t\t) ) )->get_events();\n\n\t\t$this->assertEquals( 2, count( $events ) );\n\n\t}\n\n\t/**\n\t * Catch wp_die() called by ajax methods & store the output buffer contents for use later.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param string $msg Die msg.\n\t * @return void\n\t */\n\tpublic function _wp_die_handler( $msg ) {\n\t\t$this->last_response = ob_get_clean();\n\t\tthrow new WPAjaxDieContinueException( $msg );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-assets.php",
    "content": "<?php\n/**\n * Test LLMS_Assets\n *\n * @package LifterLMS/Tests\n *\n * @group assets\n *\n * @since 4.4.0\n * @version 7.2.0\n */\nclass LLMS_Test_Assets extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 4.4.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' );\n\n\t}\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * Dequeue and deregister all assets that may have been registered/enqueued during the test.\n\t *\n\t * @since 4.4.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'scripts' ) ) as $handle ) {\n\t\t\twp_dequeue_script( $handle );\n\t\t\twp_deregister_script( $handle );\n\t\t}\n\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'styles' ) ) as $handle ) {\n\t\t\twp_dequeue_style( $handle );\n\t\t\twp_deregister_style( $handle );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test merging of defaults during construction\n\t *\n\t * @since 4.9.0\n\t * @since 5.5.0 Add `asset_file`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_default_merge() {\n\n\t\t$defaults = array(\n\t\t\t// Base defaults shared by all asset types.\n\t\t\t'base'   => array(\n\t\t\t\t'base_file' => 'Some/Custom/Plugin/File.php',\n\t\t\t\t'base_url'  => 'https://mock.tld/wp-content/plugins/custom-plugin',\n\t\t\t\t'version'   => '93.29.107',\n\t\t\t\t'suffix'    => '.custom',\n\t\t\t),\n\t\t\t// Script specific defaults.\n\t\t\t'script' => array(\n\t\t\t\t'translate' => true, // All scripts in this plugin are translated.\n\t\t\t),\n\t\t);\n\n\t\t$expected = array(\n\t\t\t'base' => array(\n\t\t\t\t'base_file' => 'Some/Custom/Plugin/File.php',\n\t\t\t\t'base_url' => 'https://mock.tld/wp-content/plugins/custom-plugin',\n\t\t\t\t'suffix' => '.custom',\n\t\t\t\t'dependencies' => array(),\n\t\t\t\t'version' => '93.29.107',\n\t\t\t),\n\t\t\t'script' => array(\n\t\t\t\t'path'       => 'assets/js',\n\t\t\t\t'extension'  => '.js',\n\t\t\t\t'in_footer'  => true,\n\t\t\t\t'translate'  => true,\n\t\t\t\t'asset_file' => false,\n\t\t\t),\n\t\t\t'style' => array(\n\t\t\t\t'path' => 'assets/css',\n\t\t\t\t'extension' => '.css',\n\t\t\t\t'media' => 'all',\n\t\t\t\t'rtl' => true,\n\t\t\t),\n\t\t);\n\n\n\t\t$assets = new LLMS_Assets( 'mock-package-id', $defaults );\n\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::get_private_property_value( $assets, 'defaults' ) );\n\n\t}\n\n\t/**\n\t * Test define() with script assets.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_define_scripts() {\n\n\t\t$scripts = array(\n\t\t\t'llms' => array( 'src' => 'mock' ), // Overwrite an existing script.\n\t\t\t'mock' => array( 'src' => 'mock' ), // Define a new one.\n\t\t);\n\n\t\t$res = $this->main->define( 'scripts', $scripts );\n\n\t\t$this->assertEquals( $scripts['llms'], $res['llms'] );\n\t\t$this->assertEquals( $scripts['mock'], $res['mock'] );\n\n\t}\n\n\t/**\n\t * Test define() with style assets.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_define_styles() {\n\n\t\t$styles = array(\n\t\t\t'lifterlms' => array( 'src' => 'mock' ), // Overwrite an existing style.\n\t\t\t'mock' => array( 'src' => 'mock' ),      // Define a new one.\n\t\t);\n\n\t\t$res = $this->main->define( 'styles', $styles );\n\n\t\t$this->assertEquals( $styles['lifterlms'], $res['lifterlms'] );\n\t\t$this->assertEquals( $styles['mock'], $res['mock'] );\n\n\t}\n\n\t/**\n\t * Test define() with an invalid type.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_define_invalid_type() {\n\n\t\t$this->assertFalse( $this->main->define( 'fake', array() ) );\n\n\t}\n\n\t/**\n\t * Test enqueue_inline()\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_inline() {\n\n\t\t$this->assertEquals( 10, $this->main->enqueue_inline( 'mock-foot', 'console.log( 1 );', 'footer' ) );\n\n\t\t// Already enqueued.\n\t\t$this->assertEquals( 10, $this->main->enqueue_inline( 'mock-foot', 'console.log( 1 );', 'footer' ) );\n\n\t\t// Priority automatically incremented.\n\t\t$this->assertEquals( 10.01, $this->main->enqueue_inline( 'mock-foot-two', 'console.log( 1 );', 'footer' ) );\n\n\t\t// Explicit priority.\n\t\t$this->assertEquals( 25, $this->main->enqueue_inline( 'mock-head', 'console.log( 1 );', 'header', 25 ) );\n\n\t\t$inline = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'inline' );\n\t\t$this->assertEquals( array( 'mock-foot', 'mock-foot-two', 'mock-head' ), array_keys( $inline ) );\n\n\t\tforeach ( $inline as $def ) {\n\t\t\t$this->assertEquals( array( 'handle', 'asset', 'location', 'priority' ), array_keys( $def ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test enqueue_script() for a defined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_script_defined() {\n\n\t\t$this->assertAssetNotRegistered( 'script', 'llms' );\n\n\t\t// Register and enqueue.\n\t\t$this->assertTrue( $this->main->enqueue_script( 'llms' ) );\n\n\t\t// Already registered.\n\t\t$this->assertTrue( $this->main->enqueue_script( 'llms' ) );\n\n\t}\n\n\t/**\n\t * Test enqueue_script() for an undefined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_script_undefined() {\n\n\t\t$this->assertFalse( $this->main->enqueue_script( 'fake-script' ) );\n\n\t}\n\n\t/**\n\t * Test enqueue_style() for a defined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_style_defined() {\n\n\t\t$this->assertAssetNotRegistered( 'style', 'lifterlms-styles' );\n\n\t\t// Register and enqueue.\n\t\t$this->assertTrue( $this->main->enqueue_style( 'lifterlms-styles' ) );\n\n\t\t// Already registered.\n\t\t$this->assertTrue( $this->main->enqueue_style( 'lifterlms-styles' ) );\n\n\t}\n\n\t/**\n\t * Test enqueue_style() for an undefined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_style_undefined() {\n\n\t\t$this->assertFalse( $this->main->enqueue_style( 'fake-style' ) );\n\n\t}\n\n\t/**\n\t * Test get() method.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get() {\n\n\t\t$asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'llms' ) );\n\n\t\t// Add the handle to the data array.\n\t\t$this->assertEquals( 'llms', $asset['handle'] );\n\t\t$this->assertArrayHasKey( 'src', $asset );\n\t\t$this->assertEquals( 'llms-core', $asset['package_id'] );\n\n\t}\n\n\t/**\n\t * Test get() method for an asset with an asset.php file\n\t *\n\t * @since 5.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_with_asset_file() {\n\n\t\t$definition = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'scripts' )['llms-addons'];\n\t\t$asset_file = include LLMS_PLUGIN_DIR . 'assets/js/llms-admin-addons.asset.php';\n\n\t\t$asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'llms-addons' ) );\n\n\t\t$this->assertArrayHasKey( 'src', $asset );\n\n\t\t$this->assertEquals( $asset_file['version'], $asset['version'] );\n\t\t$this->assertEqualSets( $asset_file['dependencies'], $asset['dependencies'] );\n\n\t}\n\n\t/**\n\t * Test get() method for an undefined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_undefined() {\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'style', 'undefined-style' ) ) );\n\n\t}\n\n\t/**\n\t * Test get() method for an asset which is defined with an empty array signifying that all asset values should be defaults.\n\t *\n\t * @since 4.4.1\n\t *\n\t * @see https://github.com/gocodebox/lifterlms/issues/1313\n\t *\n\t * @return version\n\t */\n\tpublic function test_get_all_default_values() {\n\n\t\tadd_filter( 'llms_get_style_asset_before_prep', function( $asset, $handle ) {\n\n\t\t\tif ( 'mock-style-with-all-defaults' === $handle ) {\n\t\t\t\t$asset = array();\n\t\t\t}\n\n\t\t\treturn $asset;\n\n\t\t}, 10, 2 );\n\n\t\t$asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'style', 'mock-style-with-all-defaults' ) );\n\n\t\t$this->assertTrue( is_array( $asset ) );\n\t\t$this->assertEquals( 'mock-style-with-all-defaults', $asset['handle'] );\n\n\t}\n\n\t/**\n\t * Test that adding an asset with a custom src will use the custom src instead of a generated one\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_custom_src() {\n\n\t\tadd_filter( 'llms_get_script_asset_before_prep', function( $asset, $handle ) {\n\n\t\t\tif ( 'mock-script-custom-src' === $handle ) {\n\t\t\t\t$asset = array(\n\t\t\t\t\t'file_slug' => 'mock',\n\t\t\t\t\t'src'       => 'custom-src',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn $asset;\n\n\t\t}, 10, 2 );\n\n\t\t$asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'mock-script-custom-src' ) );\n\n\t\t$this->assertEquals( 'custom-src', $asset['src'] );\n\n\t}\n\n\t/**\n\t * Test that adding an asset with an empty suffix will not add the default suffix.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_no_suffix() {\n\n\t\tadd_filter( 'llms_get_script_asset_before_prep', function( $asset, $handle ) {\n\n\t\t\tif ( 'mock-style-no-suffix' === $handle ) {\n\t\t\t\t$asset = array(\n\t\t\t\t\t'file_slug' => 'mock',\n\t\t\t\t\t'suffix'    => '',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn $asset;\n\n\t\t}, 10, 2 );\n\n\t\t$asset = LLMS_Unit_Test_Util::call_method( $this->main, 'get', array( 'script', 'mock-style-no-suffix' ) );\n\n\t\t$this->assertEquals( '', $asset['suffix'] );\n\n\n\n\t}\n\n\t/**\n\t * Test get_scripts()\n\t *\n\t * @since 4.4.0\n\t * @since 5.5.0 Add `asset_file`.\n\t * @since 7.2.0 Use `LLMS_ASSETS_VERSION` for asset versions.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_defaults_for_scripts() {\n\n\t\t$expect = array(\n\t\t\t'base_file'    => LLMS_PLUGIN_FILE,\n\t\t\t'base_url'     => LLMS_PLUGIN_URL,\n\t\t\t'suffix'       => LLMS_ASSETS_SUFFIX,\n\t\t\t'dependencies' => array(),\n\t\t\t'version'      => LLMS_ASSETS_VERSION,\n\t\t\t'extension'    => '.js',\n\t\t\t'in_footer'    => true,\n\t\t\t'path'         => 'assets/js',\n\t\t\t'translate'    => false,\n\t\t\t'asset_file'   => false,\n\t\t);\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_defaults', array( 'script' ) ) );\n\n\t}\n\n\t/**\n\t * Test get_styles()\n\t *\n\t * @since 4.4.0\n\t * @since 7.2.0 Use `LLMS_ASSETS_VERSION` for asset versions.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_defaults_for_styles() {\n\n\t\t$expect = array(\n\t\t\t'base_file'    => LLMS_PLUGIN_FILE,\n\t\t\t'base_url'     => LLMS_PLUGIN_URL,\n\t\t\t'suffix'       => LLMS_ASSETS_SUFFIX,\n\t\t\t'dependencies' => array(),\n\t\t\t'version'      => LLMS_ASSETS_VERSION,\n\t\t\t'extension'    => '.css',\n\t\t\t'media'        => 'all',\n\t\t\t'path'         => 'assets/css',\n\t\t\t'rtl'          => true,\n\t\t);\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $this->main, 'get_defaults', array( 'style' ) ) );\n\n\t}\n\n\t/**\n\t * Test get_definitions()\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_definitions() {\n\n\t\t// Definitions returned.\n\t\t$this->assertFalse( empty( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions', array( 'script' ) ) ) );\n\t\t$this->assertFalse( empty( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions', array( 'style' ) ) ) );\n\n\t\t// Not a real asset type.\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions', array( 'fake' ) ) );\n\n\t}\n\n\t/**\n\t * Test get_definitions_inline()\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_definitions_inline() {\n\n\t\t// No assets.\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'header' ) ) );\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'footer' ) ) );\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'style' ) ) );\n\n\t\t// Fake.\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'fake' ) ) );\n\n\t\t$this->main->enqueue_inline( 'in-header', '', 'header' );\n\t\t$this->main->enqueue_inline( 'in-footer', '', 'footer' );\n\t\t$this->main->enqueue_inline( 'in-style', '', 'style' );\n\n\t\t// Reduces to scripts by location.\n\t\t$this->assertEquals( array( 'in-header'), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'header' ) ) ) );\n\t\t$this->assertEquals( array( 'in-footer' ), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'footer' ) ) ) );\n\t\t$this->assertEquals( array( 'in-style' ), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'style' ) ) ) );\n\n\t\t$this->main->enqueue_inline( 'in-header-first', '', 'header', 5 );\n\n\t\t// Sorted by priority.\n\t\t$this->assertEquals( array( 'in-header-first', 'in-header' ), array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_definitions_inline', array( 'header' ) ) ) );\n\n\t}\n\n\t/**\n\t * Test get_inline_priority()\n\t *\n\t * @since 4.4.0\n\t * @since 7.0.0 Round mock priorities to nearest 2 decimals.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_inline_priority() {\n\n\t\t$existing_priorties = array();\n\n\t\t$i = (float) 5;\n\t\twhile ( $i <= 5.05 ) {\n\n\t\t\t$this->assertEquals( $i, LLMS_Unit_Test_Util::call_method( $this->main, 'get_inline_priority', array( 5, $existing_priorties ) ) );\n\n\t\t\t$existing_priorties[] = array( 'priority' => $i );\n\t\t\t$i = round( $i + 0.01, 2 );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test is_inline_enqueued()\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_inline_enqueued() {\n\n\t\t// Not enqueued.\n\t\t$this->assertFalse( $this->main->is_inline_enqueued( 'is-inline-enqueued' ) );\n\n\t\t// Enqueue.\n\t\t$this->main->enqueue_inline( 'is-inline-enqueued', 'console.log( 1 );', 'footer' );\n\n\t\t// Is enqueued.\n\t\t$this->assertTrue( $this->main->is_inline_enqueued( 'is-inline-enqueued' ) );\n\n\t}\n\n\t/**\n\t * Test merge_asset_file() when `asset_file` is `false`.\n\t *\n\t * @since 5.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_merge_asset_file_disabled() {\n\n\t\t$asset = array(\n\t\t\t'base_file'    => 'fake.php',\n\t\t\t'asset_file'   => false,\n\t\t\t'dependencies' => array(),\n\t\t);\n\n\t\t$this->assertEquals( $asset, LLMS_Unit_Test_Util::call_method( $this->main, 'merge_asset_file', array( $asset ) ) );\n\n\t}\n\n\t/**\n\t * Test output_inline()\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_output_inline() {\n\n\t\tadd_filter( 'llms_assets_debug', '__return_false' );\n\t\t$this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' );\n\n\t\t$this->main->enqueue_inline( 'in-header', 'console.log(1);', 'header' );\n\t\t$this->main->enqueue_inline( 'in-header-2', 'console.log(2);', 'header' );\n\t\t$this->main->enqueue_inline( 'in-footer', 'console.log(1);', 'footer' );\n\t\t$this->main->enqueue_inline( 'in-footer-2', 'console.log(2);', 'footer' );\n\t\t$this->main->enqueue_inline( 'in-style', 'body{background:red;}', 'style' );\n\t\t$this->main->enqueue_inline( 'in-style-2', 'body{color:black;}', 'style' );\n\n\t\t$this->assertOutputEquals( '<script id=\"llms-inline-header-scripts\" type=\"text/javascript\">console.log(1);console.log(2);</script>', array( $this->main, 'output_inline' ), array( 'header' ) );\n\t\t$this->assertOutputEquals( '<script id=\"llms-inline-footer-scripts\" type=\"text/javascript\">console.log(1);console.log(2);</script>', array( $this->main, 'output_inline' ), array( 'footer' ) );\n\n\t\t$this->assertOutputEquals( '<style id=\"llms-inline-styles\" type=\"text/css\">body{background:red;}body{color:black;}</style>', array( $this->main, 'output_inline' ), array( 'style' ) );\n\n\t\tremove_filter( 'llms_assets_debug', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test prepare_inline_asset_for_output(): not in debug mode, scripts & styles work the same.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_inline_asset_for_output() {\n\n\t\t$asset = array(\n\t\t\t'handle' => 'fake-handle',\n\t\t\t'asset'  => 'console.log(1);',\n\t\t);\n\n\t\tadd_filter( 'llms_assets_debug', '__return_false' );\n\t\t$this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' );\n\n\t\t$this->assertEquals( $asset['asset'], LLMS_Unit_Test_Util::call_method( $this->main, 'prepare_inline_asset_for_output', array( $asset, 'header' ) ) );\n\n\t\tremove_filter( 'llms_assets_debug', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test prepare_inline_asset_for_output(): for scripts.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_inline_asset_for_output_scripts_debug_on() {\n\n\t\t$asset = array(\n\t\t\t'handle' => 'fake-handle',\n\t\t\t'asset'  => 'console.log(1);',\n\t\t);\n\n\t\tadd_filter( 'llms_assets_debug', '__return_true' );\n\t\t$this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' );\n\n\t\t$this->assertEquals( \"// fake-handle.\\nconsole.log(1);\\n\", LLMS_Unit_Test_Util::call_method( $this->main, 'prepare_inline_asset_for_output', array( $asset, 'header' ) ) );\n\n\t\tremove_filter( 'llms_assets_debug', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test prepare_inline_asset_for_output(): for styles.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_inline_asset_for_output_styles_debug_on() {\n\n\t\t$asset = array(\n\t\t\t'handle' => 'fake-handle',\n\t\t\t'asset'  => 'body{background:red;}',\n\t\t);\n\n\t\tadd_filter( 'llms_assets_debug', '__return_true' );\n\t\t$this->main = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' );\n\n\t\t$this->assertEquals( \"/* fake-handle. */\\nbody{background:red;}\\n\", LLMS_Unit_Test_Util::call_method( $this->main, 'prepare_inline_asset_for_output', array( $asset, 'style' ) ) );\n\n\t\tremove_filter( 'llms_assets_debug', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test register_script() for a custom asset (added via a filter)\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_script_custom() {\n\n\t\tadd_filter( 'llms_get_script_asset_definitions', function( $defs ) {\n\t\t\t$defs['mock-script'] = array(\n\t\t\t\t'file_slug' => 'mock-script',\n\t\t\t);\n\t\t\treturn $defs;\n\t\t} );\n\n\t\t$this->assertTrue( $this->main->register_script( 'mock-script' ) );\n\t\t$this->assertAssetIsRegistered( 'script', 'mock-script' );\n\n\t}\n\n\t/**\n\t * Test register_script() for a defined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_script_defined() {\n\n\t\t$this->assertTrue( $this->main->register_script( 'llms' ) );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms' );\n\n\t}\n\n\t/**\n\t * Test register_script() for a defined asset with defined dependencies.\n\t *\n\t * @since 5.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_script_defined_with_deps() {\n\n\t\t// Dependency is not registered.\n\t\t$this->assertAssetNotRegistered( 'script', 'llms' );\n\n\t\t$this->assertTrue( $this->main->register_script( 'llms-quiz' ) );\n\t\t$this->assertAssetIsRegistered( 'script', 'llms-quiz' );\n\n\t\t// Dependency was automatically registered.\n\t\t$this->assertAssetIsRegistered( 'script', 'llms' );\n\n\t}\n\n\t/**\n\t * Test register_script() for an undefined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_script_undefined() {\n\n\t\t$this->assertFalse( $this->main->register_script( 'fake-script' ) );\n\t\t$this->assertAssetNotRegistered( 'script', 'fake-script' );\n\n\t}\n\n\t/**\n\t * Test register_style() for a custom asset (added via a filter)\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_style_custom() {\n\n\t\tadd_filter( 'llms_get_style_asset_definitions', function( $defs ) {\n\t\t\t$defs['mock-style'] = array(\n\t\t\t\t'file_slug' => 'mock-style',\n\t\t\t\t'rtl'       => false,\n\t\t\t);\n\t\t\treturn $defs;\n\t\t} );\n\n\t\t$this->assertTrue( $this->main->register_style( 'mock-style' ) );\n\t\t$this->assertAssetIsRegistered( 'style', 'mock-style' );\n\n\t\t// No RTL is added.\n\t\tglobal $wp_styles;\n\t\t$this->assertEquals( array(), $wp_styles->registered['mock-style']->extra );\n\n\t}\n\n\n\t/**\n\t * Test register_style() for a defined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_style_defined() {\n\n\t\t$this->assertTrue( $this->main->register_style( 'lifterlms-styles' ) );\n\t\t$this->assertAssetIsRegistered( 'style', 'lifterlms-styles' );\n\n\t\t// Ensure RTL is added.\n\t\tglobal $wp_styles;\n\t\t$expect = array(\n\t\t\t'rtl'    => 'replace',\n\t\t\t'suffix' => LLMS_ASSETS_SUFFIX,\n\t\t);\n\t\t$this->assertEquals( $expect, $wp_styles->registered['lifterlms-styles']->extra );\n\n\t}\n\n\t/**\n\t * Test register_style() for an asset with defined dependencies\n\t *\n\t * @since 5.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_style_with_deps() {\n\n\t\t$this->markTestIncomplete( 'Need to rework this test when a qualifying asset is defined.' );\n\n\t\t// Deps are not registered.\n\t\t$deps = array( 'llms-datetimepicker', 'llms-quill-bubble', 'webui-popover' );\n\t\tforeach ( $deps as $dep ) {\n\t\t\t$this->assertAssetNotRegistered( 'style', $dep );\n\t\t}\n\n\t\t$this->assertTrue( $this->main->register_style( 'llms-builder-styles' ) );\n\t\t$this->assertAssetIsRegistered( 'style', 'llms-builder-styles' );\n\n\t\t// Deps are registered.\n\t\tforeach ( $deps as $dep ) {\n\t\t\t$this->assertAssetIsRegistered( 'style', $dep );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test register_style() for an undefined asset.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_style_undefined() {\n\n\t\t$this->assertFalse( $this->main->register_style( 'fake-style' ) );\n\t\t$this->assertAssetNotRegistered( 'style', 'fake-style' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-awards-query.php",
    "content": "<?php\n/**\n * Test LLMS_Awards_Query\n *\n * @package LifterLMS/Tests\n *\n * @group awards_query\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Awards_Query extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test get_number_results(), get_found_results(), get_max_pages(), has_results(), and get_results().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_query() {\n\n\t\t$tests = array(\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_my_certificate',\n\t\t);\n\n\t\t$expected_num_results = array(\n\t\t\t1 => 10,\n\t\t\t2 => 10,\n\t\t\t3 => 5,\n\t\t\t4 => 0,\n\t\t);\n\n\t\tforeach ( $tests as $post_type ) {\n\n\t\t\t$awards = $this->factory->post->create_many( 25, compact( 'post_type' ) );\n\n\t\t\t$page = 1;\n\t\t\twhile ( $page <= 4 ) {\n\n\t\t\t\t$query = new LLMS_Awards_Query( array( 'page' => $page, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\n\t\t\t\t$this->assertEquals( $expected_num_results[ $page ], $query->get_number_results() );\n\t\t\t\t$this->assertEquals( 4 !== $page ? 25 : 0, $query->get_found_results() );\n\t\t\t\t$this->assertEquals( 4 !== $page ? 3 : 0, $query->get_max_pages() );\n\t\t\t\t$this->assertEquals( 4 !== $page, $query->has_results() );\n\n\t\t\t\tforeach ( $query->get_awards() as $result ) {\n\t\t\t\t\t$this->assertTrue( in_array( $result->get( 'id' ), $awards, true ) );\n\t\t\t\t\t$this->assertInstanceOf( strtoupper( str_replace( '_my_', '_user_', $post_type ) ), $result );\n\t\t\t\t}\n\n\t\t\t\tforeach ( $query->get_results() as $result ) {\n\t\t\t\t\t$this->assertTrue( in_array( $result->ID, $awards, true ) );\n\t\t\t\t\t$this->assertInstanceOf( 'WP_Post', $result );\n\n\t\t\t\t\t$index = array_search( $result->ID, $awards, true );\n\t\t\t\t\tunset( $awards[ $index ] );\n\n\t\t\t\t}\n\n\t\t\t\t$page++;\n\t\t\t}\n\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Run a query for awards earned or not earned by a specific user\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_by_user() {\n\n\t\t$tests = array(\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_my_certificate',\n\t\t);\n\n\t\t$post_author = $this->factory->user->create();\n\t\tforeach ( $tests as $post_type ) {\n\n\t\t\t$included = $this->factory->post->create_many( 3, compact( 'post_type', 'post_author' ) );\n\t\t\t$excluded = $this->factory->post->create_many( 2, compact( 'post_type' ) );\n\n\t\t\t// Included for a specified user.\n\t\t\t$users_query = new LLMS_Awards_Query( array( 'users' => $post_author, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\t\t\t$this->assertEquals( 3, $users_query->get_number_results() );\n\t\t\tforeach ( $users_query->get_results() as $post ) {\n\t\t\t\t$this->assertEquals( $post_author, $post->post_author );\n\t\t\t\t$this->assertTrue( in_array( $post->ID, $included, true ) );\n\t\t\t}\n\n\t\t\t// Excluded for a specified user.\n\t\t\t$users_exclude_query = new LLMS_Awards_Query( array( 'users__exclude' => $post_author, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\t\t\t$this->assertEquals( 2, $users_exclude_query->get_number_results() );\n\t\t\tforeach ( $users_exclude_query->get_results() as $post ) {\n\t\t\t\t$this->assertNotEquals( $post_author, $post->post_author );\n\t\t\t\t$this->assertTrue( in_array( $post->ID, $excluded, true ) );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test query by related_posts or engagements\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_by_relationships() {\n\n\t\t$tests = array(\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_my_certificate',\n\t\t);\n\n\t\tforeach ( $tests as $post_type ) {\n\n\t\t\t$metas = array(\n\t\t\t\t'related_posts' => '_llms_related',\n\t\t\t\t'engagements'   => '_llms_engagement',\n\t\t\t);\n\n\t\t\tforeach ( $metas as $arg => $meta_key ) {\n\n\t\t\t\t$meta_input = array(\n\t\t\t\t\t$meta_key => 123,\n\t\t\t\t);\n\n\t\t\t\t$included = $this->factory->post->create_many( 3, compact( 'post_type', 'meta_input' ) );\n\t\t\t\t$excluded = $this->factory->post->create_many( 2, compact( 'post_type' ) );\n\t\t\t\t$excluded[] = $this->factory->post->create( array(\n\t\t\t\t\t'post_type' => $post_type,\n\t\t\t\t\t'meta_input' => array( $meta_key => 456 ),\n\t\t\t\t) );\n\n\t\t\t\t// Included.\n\t\t\t\t$include_query = new LLMS_Awards_Query( array( $arg => 123, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\t\t\t\t$this->assertEquals( 3, $include_query->get_number_results() );\n\t\t\t\tforeach ( $include_query->get_results() as $post ) {\n\t\t\t\t\t$this->assertEquals( 123, get_post_meta( $post->ID, $meta_key, true ) );\n\t\t\t\t\t$this->assertTrue( in_array( $post->ID, $included, true ) );\n\t\t\t\t}\n\n\t\t\t\t// Excluded.\n\t\t\t\t$exclude_query = new LLMS_Awards_Query( array( \"{$arg}__exclude\" => 123, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\t\t\t\tforeach ( $exclude_query->get_results() as $post ) {\n\t\t\t\t\t$this->assertNotEquals( 123, get_post_meta( $post->ID, $meta_key, true ) );\n\t\t\t\t\t$this->assertTrue( ! in_array( $post->ID, $included, true ) );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test query() for parent template relationships\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_template() {\n\n\t\t$tests = array(\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_my_certificate',\n\t\t);\n\n\t\tforeach ( $tests as $post_type ) {\n\n\t\t\t$post_parent = $this->factory->post->create();\n\n\t\t\t$included = $this->factory->post->create_many( 3, compact( 'post_type', 'post_parent' ) );\n\t\t\t$excluded = $this->factory->post->create_many( 2, compact( 'post_type' ) );\n\t\t\t$excluded[] = $this->factory->post->create( array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t\t'post_parent' => $this->factory->post->create(),\n\t\t\t) );\n\n\t\t\t// Included.\n\t\t\t$include_query = new LLMS_Awards_Query( array( 'templates' => $post_parent, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\t\t\t$this->assertEquals( 3, $include_query->get_number_results() );\n\t\t\tforeach ( $include_query->get_results() as $post ) {\n\t\t\t\t$this->assertEquals( $post_parent, $post->post_parent );\n\t\t\t\t$this->assertTrue( in_array( $post->ID, $included, true ) );\n\t\t\t}\n\n\t\t\t// Excluded.\n\t\t\t$exclude_query = new LLMS_Awards_Query( array( \"templates__exclude\" => $post_parent, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\t\t\tforeach ( $exclude_query->get_results() as $post ) {\n\t\t\t\t$this->assertNotEquals( $post_parent, $post->post_parent );\n\t\t\t\t$this->assertTrue( ! in_array( $post->ID, $included, true ) );\n\t\t\t}\n\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Run a query for manual awards only\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_manual() {\n\n\t\t$tests = array(\n\t\t\t'llms_my_achievement',\n\t\t\t'llms_my_certificate',\n\t\t);\n\t\tforeach ( $tests as $post_type ) {\n\n\t\t\t$excluded = $this->factory->post->create_many( 1, array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t\t'meta_input' => array( '_llms_engagement' => 456 ),\n\t\t\t) );\n\n\t\t\t$included = $this->factory->post->create_many( 1, array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t) );\n\t\t\t$included[] = $this->factory->post->create( array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t\t'meta_input' => array( '_llms_engagement' => 0 ),\n\t\t\t) );\n\t\t\t$included[] = $this->factory->post->create( array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t\t'meta_input' => array( '_llms_engagement' => '' ),\n\t\t\t) );\n\n\t\t\t$include_query = new LLMS_Awards_Query( array( 'manual_only' => true, 'type' => str_replace( 'llms_my_', '', $post_type ) ) );\n\t\t\t$this->assertEquals( 3, $include_query->get_number_results() );\n\t\t\tforeach ( $include_query->get_results() as $post ) {\n\t\t\t\t$this->assertEmpty( get_post_meta( $post->ID, '_llms_engagement', true ) );\n\t\t\t\t$this->assertTrue( in_array( $post->ID, $included, true ) );\n\t\t\t}\n\n\t\t}\n\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-block-library.php",
    "content": "<?php\n/**\n * Test LLMS_Block_Library\n *\n * @package LifterLMS/Tests\n *\n * @group blocks\n * @group block_library\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Block_Library extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tif ( ! llms_is_block_editor_supported_for_certificates() ) {\n\t\t\t$this->markTestSkipped( 'No blocks supported on this version of WordPress.' );\n\t\t}\n\n\t\tparent::set_up();\n\t\t$this->main     = new LLMS_Block_Library();\n\t\t$this->registry = WP_Block_Type_Registry::get_instance();\n\n\t\t$this->deregister_blocks();\n\n\t}\n\n\t/**\n\t * Deregister all LifterLMS blocks.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function deregister_blocks() {\n\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_blocks' ) ) as $id ) {\n\t\t\t$id = 'llms/' . $id;\n\t\t\tif ( $this->registry->is_registered( $id ) ) {\n\t\t\t\t$this->registry->unregister( $id );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test modify_editor_settings()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_editor_settings() {\n\n\t\tif ( ! class_exists( 'WP_Block_Editor_Context' ) ) {\n\t\t\t$this->markTestSkipped( 'Test not required on this version of WordPress.' );\n\t\t}\n\n\t\t$input = array( 'settings' => '123' );\n\n\t\t// Like widgets or site editor.\n\t\t$this->assertEquals(\n\t\t\t$input,\n\t\t\t$this->main->modify_editor_settings( $input, new WP_Block_Editor_Context() )\n\t\t);\n\n\t\t// Post editor but for the wrong post type.\n\t\t$post = $this->factory->post->create_and_get();\n\t\t$this->assertEquals(\n\t\t\t$input,\n\t\t\t$this->main->modify_editor_settings( $input, new WP_Block_Editor_Context( compact( 'post' ) ) )\n\t\t);\n\n\t\t// Settings already has theme fonts.\n\t\t$input_with_theme = $input;\n\t\t// Add a theme font.\n\t\t_wp_array_set(\n\t\t\t$input_with_theme,\n\t\t\tarray( '__experimentalFeatures', 'typography', 'fontFamilies', 'theme' ),\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'fontFamily' => '\"Awesome Sans\"',\n\t\t\t\t\t'name'       => 'Awesome Sans',\n\t\t\t\t\t'slug'       => 'awesome-sans',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Certificate context!\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\n\t\t\t$res = $this->main->modify_editor_settings( $input, new WP_Block_Editor_Context( compact( 'post' ) ) );\n\n\t\t\t// Still has initial settings.\n\t\t\t$this->assertEquals( '123', $res['settings'] );\n\n\t\t\t// Fonts have been injected.\n\t\t\t$fonts = _wp_array_get( $res, array(\n\t\t\t\t'__experimentalFeatures',\n\t\t\t\t'blocks',\n\t\t\t\t'llms/certificate-title',\n\t\t\t\t'typography',\n\t\t\t\t'fontFamilies',\n\t\t\t\t'custom',\n\t\t\t) );\n\t\t\t$this->assertEquals( array_keys( llms_get_certificate_fonts() ), wp_list_pluck( $fonts, 'slug' ) );\n\n\n\t\t\t// Theme fonts are preserved.\n\t\t\t$res = $this->main->modify_editor_settings( $input_with_theme, new WP_Block_Editor_Context( compact( 'post' ) ) );\n\t\t\t// Fonts have been injected.\n\t\t\t$fonts = _wp_array_get( $res, array(\n\t\t\t\t'__experimentalFeatures',\n\t\t\t\t'blocks',\n\t\t\t\t'llms/certificate-title',\n\t\t\t\t'typography',\n\t\t\t\t'fontFamilies',\n\t\t\t\t'custom',\n\t\t\t) );\n\t\t\t$this->assertEquals( array_merge( array( 'awesome-sans' ), array_keys( llms_get_certificate_fonts() ) ), wp_list_pluck( $fonts, 'slug' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test register().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_wrong_context() {\n\n\t\t$this->main->register();\n\t\t$this->assertFalse( $this->registry->is_registered( 'llms/certificate-title' ) );\n\n\t}\n\n\t/**\n\t * Test register() when in the block editor for an existing post.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_dot_php() {\n\n\t\tglobal $pagenow;\n\t\t$original_pagenow = $pagenow;\n\t\t$pagenow = 'post.php';\n\n\t\t$block_id = 'llms/certificate-title';\n\n\t\t// No post ID set so post type can't be found.\n\t\t$this->main->register();\n\t\t$this->assertFalse( $this->registry->is_registered( $block_id ) );\n\n\t\t// Regular post, nothing to register.\n\t\t$this->mockGetRequest( array( 'post' => $this->factory->post->create() ) );\n\t\t$this->main->register();\n\t\t$this->assertFalse( $this->registry->is_registered( $block_id ) );\n\n\t\t// Valid post.\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$this->mockGetRequest( array( 'post' => $this->factory->post->create( compact( 'post_type' ) ) ) );\n\n\t\t\t$this->main->register();\n\t\t\t$this->assertTrue( $this->registry->is_registered( 'llms/certificate-title' ) );\n\n\t\t\t// Ensure _doing_it_wrong() isn't thrown when registering a block that's already registered.\n\t\t\t$this->main->register();\n\n\t\t\t$this->deregister_blocks();\n\t\t}\n\n\t\t$pagenow = $original_pagenow;\n\n\t}\n\n\t/**\n\t * Test register() when in the block editor for a new post.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_new_dot_php() {\n\n\t\tglobal $pagenow;\n\t\t$original_pagenow = $pagenow;\n\t\t$pagenow = 'post-new.php';\n\n\t\t$block_id = 'llms/certificate-title';\n\n\t\t// No post type set so the post type is assumed to be a post.\n\t\t$this->main->register();\n\t\t$this->assertFalse( $this->registry->is_registered( $block_id ) );\n\n\t\t// Valid post.\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$this->mockGetRequest( compact( 'post_type' ) );\n\n\t\t\t$this->main->register();\n\t\t\t$this->assertTrue( $this->registry->is_registered( 'llms/certificate-title' ) );\n\n\t\t\t// Ensure _doing_it_wrong() isn't thrown when registering a block that's already registered.\n\t\t\t$this->main->register();\n\n\t\t\t$this->deregister_blocks();\n\t\t}\n\n\t\t$pagenow = $original_pagenow;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-block-templates.php",
    "content": "<?php\n/**\n * Test LLMS_Block_Templates\n *\n * @package LifterLMS/Tests\n *\n * @group block_templates\n *\n * @since 5.8.0\n * @version 7.2.0\n */\nclass LLMS_Test_Block_Templates extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\t$this->main = LLMS_Block_Templates::instance();\n\t\tparent::set_up();\n\n\t}\n\n\t/**\n\t * Test __construct().\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\t// Reset data.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'block_templates_config', null );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) );\n\n\t\tremove_filter( 'get_block_templates', array( $this->main, 'add_llms_block_templates' ), 10 );\n\t\tremove_filter( 'pre_get_block_file_template', array( $this->main, 'maybe_return_blocks_template' ), 10 );\n\t\tremove_action( 'admin_enqueue_scripts', array( $this->main, 'localize_blocks' ), 9999 );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, '__construct' );\n\n\t\t// Configuration runs.\n\t\t$this->assertNotNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) );\n\n\t\t// Hooks added.\n\t\t$this->assertEquals( 10, has_filter( 'get_block_templates', array( $this->main, 'add_llms_block_templates' ) ) );\n\t\t$this->assertEquals( 10, has_filter( 'pre_get_block_file_template', array( $this->main, 'maybe_return_blocks_template' ) ) );\n\t\t$this->assertEquals( 9999, has_action( 'admin_enqueue_scripts', array( $this->main, 'localize_blocks' ) ) );\n\n\t}\n\n\t/**\n\t * Test configure_block_templates().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_configure_block_templates() {\n\n\t\t// Reset data.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'block_templates_config', null );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) );\n\n\t\t// Run configurator.\n\t\t$this->main->configure_block_templates();\n\n\t\t$block_templates_config = array(\n\t\t\tllms()->plugin_path() . '/templates/' . $this->main::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME => array(\n\t\t\t\t'slug_prefix'       => $this->main::LLMS_BLOCK_TEMPLATES_PREFIX,\n\t\t\t\t'namespace'         => $this->main::LLMS_BLOCK_TEMPLATES_NAMESPACE,\n\t\t\t\t'blocks_dir'        => $this->main::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME, // Relative to the plugin's templates directory.\n\t\t\t\t'admin_blocks_l10n' => LLMS_Unit_Test_Util::call_method( $this->main, 'block_editor_l10n' ),\n\t\t\t\t'template_titles'   => LLMS_Unit_Test_Util::call_method( $this->main, 'template_titles' ),\n\t\t\t),\n\t\t);\n\n\t\t$this->assertNotNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) );\n\t\t$this->assertEquals(\n\t\t\t$block_templates_config,\n\t\t\tLLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' )\n\t\t);\n\n\t\t// Check that the configuration can be extended through a filter\n\t\t$additional_configuration = array(\n\t\t\t'/some/path/to' => array(\n\t\t\t\t'slug_prefix'       => 'some-slug-prefix_',\n\t\t\t\t'namespace'         => 'some/namespace',\n\t\t\t\t'blocks_dir'        => 'blocks-dir', // Relative to the plugin's templates directory.\n\t\t\t\t'admin_blocks_l10n' => array( 'string-1' => 'String 1' ),\n\t\t\t\t'template_titles'   => array( 'some-archive' => 'Some Archive title' ),\n\t\t\t),\n\t\t);\n\t\t$add_configuration_cb = function( $config ) use ( $additional_configuration ) {\n\t\t\treturn array_merge( $config, $additional_configuration );\n\t\t};\n\t\tadd_filter( 'llms_block_templates_config', $add_configuration_cb );\n\n\t\t// Run configurator again.\n\t\t$this->main->configure_block_templates();\n\t\tremove_filter( 'llms_block_templates_config', $add_configuration_cb );\n\n\t\t$this->assertNotNull( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' ) );\n\t\t$this->assertEquals(\n\t\t\tarray_merge( $block_templates_config, $additional_configuration ),\n\t\t\tLLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' )\n\t\t);\n\n\t\t// Run configurator again, without the filter, to reinit the configuration.\n\t\t$this->main->configure_block_templates();\n\t\t$this->assertEquals(\n\t\t\t$block_templates_config,\n\t\t\tLLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test generate_template_slug_from_path().\n\t *\n\t * @since 5.9.0\n\t * @since 7.2.0 Remove unneded tests after switching `generate_template_slug_from_path()` logic to use `basename()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_template_slug_from_path() {\n\n\t\t$block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' );\n\t\t$this->assertNotNull( $block_templates_config );\n\t\t$this->assertNotEmpty( reset( $block_templates_config )['slug_prefix'] );\n\n\t\t// Expecting the slug to be the template name, without the extension, plus the configured slug prefix.\n\t\t$this->assertEquals(\n\t\t\treset( $block_templates_config )['slug_prefix'] . 'template',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'generate_template_slug_from_path',\n\t\t\t\tarray(\n\t\t\t\t\tkey( $block_templates_config ) . '/template.html',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test generate_template_namespace_from_path().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_template_namespace_from_path() {\n\n\t\t$block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' );\n\t\t$this->assertNotNull( $block_templates_config );\n\t\t$this->assertNotEmpty( reset( $block_templates_config )['namespace'] );\n\n\t\t// Expecting the namespace to be the class constant LLMS_BLOCK_TEMPLATES_NAMESPACE.\n\t\t$this->assertEquals(\n\t\t\t$this->main::LLMS_BLOCK_TEMPLATES_NAMESPACE,\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'generate_template_namespace_from_path',\n\t\t\t\tarray(\n\t\t\t\t\tkey( $block_templates_config ) . '/template.html',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// If you pass a path which is not in the configuration I expect an empty namespace.\n\t\t$this->assertEquals(\n\t\t\t'',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'generate_template_namespace_from_path',\n\t\t\t\tarray(\n\t\t\t\t\t'/whateverpath/template.html',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test generate_template_prefix_from_path().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_template_prefix_from_path() {\n\n\t\t$block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' );\n\t\t$this->assertNotNull( $block_templates_config );\n\t\t$this->assertNotEmpty( reset( $block_templates_config )['slug_prefix'] );\n\n\t\t// Expecting the prefix to be the class constant LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME.\n\t\t$this->assertEquals(\n\t\t\t$this->main::LLMS_BLOCK_TEMPLATES_DIRECTORY_NAME ,\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'generate_template_blocks_dir_from_path',\n\t\t\t\tarray(\n\t\t\t\t\tkey( $block_templates_config ) . '/template.html',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// If you pass a path which is not in the configuration I expect an empty blocks directory.\n\t\t$this->assertEquals(\n\t\t\t'',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'generate_template_blocks_dir_from_path',\n\t\t\t\tarray(\n\t\t\t\t\t'/whateverpath/template.html',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test generate_template_prefix_from_path().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_blocks_dir_from_path() {\n\n\t\t$block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' );\n\t\t$this->assertNotNull( $block_templates_config );\n\t\t$this->assertNotEmpty( reset( $block_templates_config )['blocks_dir'] );\n\n\t\t// Expecting the prefix to be the class constant LLMS_BLOCK_TEMPLATES_PREFIX\n\t\t$this->assertEquals(\n\t\t\t$this->main::LLMS_BLOCK_TEMPLATES_PREFIX ,\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'generate_template_prefix_from_path',\n\t\t\t\tarray(\n\t\t\t\t\tkey( $block_templates_config ) . '/template.html',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// If you pass a path which is not in the configuration I expect an empty prefix.\n\t\t$this->assertEquals(\n\t\t\t'',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'generate_template_prefix_from_path',\n\t\t\t\tarray(\n\t\t\t\t\t'/whateverpath/template.html',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test block_template_config_property_from_path().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_template_config_property_from_path() {\n\n\t\t$block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' );\n\t\t$this->assertNotNull( $block_templates_config );\n\n\t\t// Non-existent property for an existent path => empty string.\n\t\t$this->assertEquals(\n\t\t\t'',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'block_template_config_property_from_path',\n\t\t\t\tarray(\n\t\t\t\t\tkey( $block_templates_config ) . '/some/block/template.html',\n\t\t\t\t\t'this-property-does-not-exist'\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Non-existent property for a non-existent path => empty string.\n\t\t$this->assertEquals(\n\t\t\t'',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'block_template_config_property_from_path',\n\t\t\t\tarray(\n\t\t\t\t\t'/some/block/template.html',\n\t\t\t\t\t'this-property-does-not-exist'\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Existent property for non-existent path => empty string.\n\t\t$this->assertEquals(\n\t\t\t'',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'block_template_config_property_from_path',\n\t\t\t\tarray(\n\t\t\t\t\t'/some/block/template.html',\n\t\t\t\t\t'slug_prefix'\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Existent property for existent path => property value.\n\t\t$this->assertEquals(\n\t\t\treset( $block_templates_config )['slug_prefix'],\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'block_template_config_property_from_path',\n\t\t\t\tarray(\n\t\t\t\t\tkey( $block_templates_config ) . '/some/block/template.html',\n\t\t\t\t\t'slug_prefix'\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test convert_slug_to_title().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return string Human friendly title converted from the slug.\n\t */\n\tpublic function test_convert_slug_to_title() {\n\n\t\t$block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' );\n\t\t$this->assertNotNull( $block_templates_config );\n\n\t\t// Existent slugs.\n\t\t$titles = reset( $block_templates_config )['template_titles'];\n\t\tforeach ( $titles as $slug => $title ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t$title,\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'convert_slug_to_title',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$slug,\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$slug\n\t\t\t);\n\t\t}\n\n\t\t// Non-existent slug.\n\t\t$this->assertEquals(\n\t\t\t\"This Slug Does Not Exist\",\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'convert_slug_to_title',\n\t\t\t\tarray(\n\t\t\t\t\t'this-slug-does-not-exist',\n\t\t\t\t)\n\t\t\t),\n\t\t\t$slug\n\t\t);\n\n\t}\n\n\t/**\n\t * Test localize_blocks().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_localize_blocks() {\n\t\tglobal $wp_scripts;\n\n\t\t$block_templates_config = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'block_templates_config' );\n\t\t$this->assertNotNull( $block_templates_config );\n\n\t\t// Add a fake llms-blocks-editor script.\n\t\t$wp_scripts->registered['llms-blocks-editor'] = new _WP_Dependency(\n\t\t\t'llms-blocks-editor',\n\t\t\t'/fake/',\n\t\t\tarray(),\n\t\t\t'ver',\n\t\t\tarray()\n\t\t);\n\n\t\t// Check localization went through.\n\t\t$this->assertTrue( $this->main->localize_blocks() );\n\n\t\t// Check localization is what we expect.\n\t\t$this->assertEquals(\n\t\t\tsprintf(\n\t\t\t\t'var %1$s = %2$s;',\n\t\t\t\t'llmsBlockTemplatesL10n',\n\t\t\t\twp_json_encode(\n\t\t\t\t\tarray_map(\n\t\t\t\t\t\tfunction( $value ) {\n\t\t\t\t\t\t\treturn html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8' );\n\t\t\t\t\t\t},\n\t\t\t\t\t\tarray_merge( ...array_column( $block_templates_config, 'admin_blocks_l10n' ) )\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t),\n\n\t\t\t$wp_scripts->get_data( 'llms-blocks-editor', 'data' ),\n\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-blocks.php",
    "content": "<?php\n/**\n * Test inclusion and initialization of the blocks library.\n *\n * @package LifterLMS/Tests\n *\n * @group blocks\n * @group packages\n *\n * @since 3.36.3\n * @version 3.36.3\n */\nclass LLMS_Test_Blocks extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test blocks lib exists and is loaded.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_blocks_lib_exists() {\n\t\t$this->assertTrue( class_exists( 'LLMS_Blocks' ) );\n\t\t$this->assertTrue( defined( 'LLMS_BLOCKS_VERSION' ) );\n\t\t$this->assertNotNull( LLMS_BLOCKS_VERSION );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-cache-helper.php",
    "content": "<?php\n/**\n * Test LLMS_Cache_Helper\n *\n * @package LifterLMS/Tests\n *\n * @group cache\n * @group cache_helper\n *\n * @since 4.0.0\n */\nclass LLMS_Test_Cache_Helper extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Sets the WP Engine \"live\" status and defines the is_wpe() function, if not already defined.\n\t *\n\t * @see https://wpengine.com/support/determining-wp-engine-environment/\n\t *\n\t * @since 6.6.0\n\t *\n\t * @param bool $is_live \"Live\" means a production, development or staging environment, but not a legacy staging site.\n\t * @return void\n\t */\n\tprivate function set_wpe_status( bool $is_live ) {\n\n\t\tputenv( 'IS_WPE=' . (int) $is_live );\n\n\t\tif ( function_exists( 'is_wpe' ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tfunction is_wpe() {\n\t\t\treturn getenv('IS_WPE');\n\t\t}\n\t}\n\n\t/**\n\t * Tests the exclude_page_from_wpe_server_cache() method.\n\t *\n\t * @see LLMS_Cache_Helper::exclude_page_from_wpe_server_cache()\n\t *\n\t * @since 6.6.0\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_exclude_page_from_wpe_server_cache() {\n\n\t\t// Setup.\n\t\t$helper  = new LLMS_Cache_Helper();\n\t\t$cookies = LLMS_Tests_Cookies::instance();\n\t\t$cookies->unset( 'wordpress_wpe_no_cache' );\n\t\tLLMS_Install::create_pages();\n\t\t$this->set_permalink_structure( '/%postname%/' );\n\t\t$dashboard_id    = llms_get_page_id( 'myaccount' );\n\t\t$dashboard_path  = parse_url( get_permalink( $dashboard_id ), PHP_URL_PATH );\n\t\t$expected_cookie = array(\n\t\t\t'value'    => '1',\n\t\t\t'expires'  => 0,\n\t\t\t'path'     => '',\n\t\t\t'domain'   => COOKIE_DOMAIN,\n\t\t\t'secure'   => is_ssl(),\n\t\t\t'httponly' => true,\n\t\t);\n\n\t\t// is_wpe() function is not defined.\n\t\tLLMS_Unit_Test_Util::call_method( $helper, 'exclude_page_from_wpe_server_cache' );\n\t\t$this->assertNull( $cookies->get( 'wordpress_wpe_no_cache' ) );\n\n\t\t// is_wpe() returns 0.\n\t\t$this->set_wpe_status( false );\n\t\tLLMS_Unit_Test_Util::call_method( $helper, 'exclude_page_from_wpe_server_cache' );\n\t\t$this->assertNull( $cookies->get( 'wordpress_wpe_no_cache' ) );\n\n\t\t// is_wpe() returns 1, $post not given and global $post is not set.\n\t\t$this->set_wpe_status( true );\n\t\tunset( $GLOBALS['post'] );\n\t\tLLMS_Unit_Test_Util::call_method( $helper, 'exclude_page_from_wpe_server_cache' );\n\t\t$this->assertEquals( $expected_cookie, $cookies->get( 'wordpress_wpe_no_cache' ) );\n\n\t\t// is_wpe() returns 1, $post parameter is not given, global $post is for a LifterLMS dashboard page.\n\t\t$GLOBALS['post']         = get_post( $dashboard_id );\n\t\t$expected_cookie['path'] = $dashboard_path;\n\t\tLLMS_Unit_Test_Util::call_method( $helper, 'exclude_page_from_wpe_server_cache' );\n\t\t$this->assertEquals( $expected_cookie, $cookies->get( 'wordpress_wpe_no_cache' ) );\n\n\t\t// is_wpe() returns 1, $post parameter is for a LifterLMS dashboard page.\n\t\tLLMS_Unit_Test_Util::call_method( $helper, 'exclude_page_from_wpe_server_cache', array( $dashboard_id ) );\n\t\t$this->assertEquals( $expected_cookie, $cookies->get( 'wordpress_wpe_no_cache' ) );\n\n\t\t// URI contains a LifterLMS dashboard endpoint.\n\t\t$_SERVER['REQUEST_URI'] = '/dashboard/my-courses/';\n\t\tLLMS_Unit_Test_Util::call_method( $helper, 'exclude_page_from_wpe_server_cache' );\n\t\t$this->assertEquals( $expected_cookie, $cookies->get( 'wordpress_wpe_no_cache' ) );\n\n\t\t// Set permalink structure to plain.\n\t\t$this->set_permalink_structure( '' );\n\t\t$cookies->unset( 'wordpress_wpe_no_cache' );\n\t\tLLMS_Unit_Test_Util::call_method( $helper, 'exclude_page_from_wpe_server_cache' );\n\t\t$this->assertNull( $cookies->get( 'wordpress_wpe_no_cache' ) );\n\t}\n\n\t/**\n\t * Test get_prefix() method.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_prefix() {\n\n\t\t$group = 'mock_prefix';\n\n\t\t// Cache miss.\n\t\twp_cache_delete( 'llms_mock_cache_prefix', $group );\n\n\t\t$prefix = LLMS_Cache_Helper::get_prefix( $group );\n\n\t\t// Looks right.\n\t\t$this->assertEquals( 1, preg_match( '/llms_cache_0.[0-9]{8} [0-9]{10}_/', $prefix ) );\n\n\t\t// Cache hit.\n\t\t$this->assertEquals( $prefix, LLMS_Cache_Helper::get_prefix( $group ) );\n\n\t}\n\n\t/**\n\t * Test invalidate_group() method.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_invalidate_group() {\n\n\t\t$group = 'mock_invalidate';\n\n\t\t$prefix = LLMS_Cache_Helper::get_prefix( $group );\n\n\t\t// Cache an item with the prefix.\n\t\twp_cache_set( sprintf( 'fake_%s', $prefix ), 'mock_val', $group );\n\n\t\t$prefix = LLMS_Cache_Helper::invalidate_group( $group );\n\n\t\t// New prefix should not match the original prefix.\n\t\t$this->assertNotEquals( $prefix, LLMS_Cache_Helper::get_prefix( $group ) );\n\n\t\t// Cached item is gone.\n\t\t$this->assertFalse( wp_cache_get( sprintf( 'fake_%s', $prefix ), $group ) );\n\n\t}\n\n\t/**\n\t * Test additional_nocache_headers() method.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_additional_nocache_headers() {\n\n\t\t$headers = array();\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'Cache-Control' => 'no-cache, must-revalidate, max-age=0, no-store',\n\t\t\t),\n\t\t\tLLMS_Cache_Helper::additional_nocache_headers( $headers )\n\t\t);\n\n\t\t$headers = array(\n\t\t\t'Cache-Control' => '',\n\t\t);\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'Cache-Control' => 'no-cache, must-revalidate, max-age=0, no-store',\n\t\t\t),\n\t\t\tLLMS_Cache_Helper::additional_nocache_headers( $headers )\n\t\t);\n\n\t\t$headers = array(\n\t\t\t'Cache-Control' => 'no-cache, something, no-store, something-else',\n\t\t);\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'Cache-Control' => 'no-cache, must-revalidate, max-age=0, no-store, something, something-else',\n\t\t\t),\n\t\t\tLLMS_Cache_Helper::additional_nocache_headers( $headers )\n\t\t);\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-certificates.php",
    "content": "<?php\n/**\n * Test LLMS_Certificates\n *\n * @package LifterLMS/Tests\n *\n * @group certificates\n *\n * @since 3.37.3\n */\nclass LLMS_Test_Certificates extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test trigger_engagement() method.\n\t *\n\t * @since 3.37.3\n\t * @since 3.37.4 Use `$this->create_certificate_template()` from test case base.\n\t * @since 6.0.0 Expect deprecated warning and actually call the method instead of using the abstract method `earn_certificate()`.\n\t *\n\t * @expectedDeprecated LLMS_Certificates::trigger_engagement()\n\t *\n\t * @return void\n\t */\n\tpublic function test_trigger_engagement() {\n\n\t\t$user = $this->factory->user->create();\n\t\t$template = $this->create_certificate_template();\n\t\t$related = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\tllms_enroll_student( $user, $related );\n\n\t\tllms()->certificates()->trigger_engagement( $user, $template, $related );\n\n\t\t$student = llms_get_student( $user );\n\n\t\t$earned = $student->get_certificates( array() );\n\n\t\t// Related ID matches.\n\t\t$this->assertEquals( $related, $earned->get_awards()[0]->get( 'related' ) );\n\n\t}\n\n\t/**\n\t * Retrieve a certificate export, bypassing the cache.\n\t *\n\t * @since 3.37.3\n\t * @since 3.37.4 Use `$this->create_certificate_template()` from test case base.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_export_no_cache() {\n\n\t\t$user = $this->factory->user->create();\n\t\t$template = $this->create_certificate_template();\n\t\t$related = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t$earned = $this->earn_certificate( $user, $template, $related );\n\n\t\t$cert_id = $earned[1];\n\n\t\t$path = llms()->certificates()->get_export( $cert_id );\n\t\t$this->assertTrue( false !== strpos( $path, '/uploads/llms-tmp/certificate-mock-certificate-title' ) );\n\t\t$this->assertTrue( false !== strpos( $path, '.html' ) );\n\n\t}\n\n\t/**\n\t * Retrieve a certificate export using caching.\n\t *\n\t * @since 3.37.3\n\t * @since 3.37.4 Use `$this->create_certificate_template()` from test case base.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_export_with_cache() {\n\n\t\t$user = $this->factory->user->create();\n\t\t$template = $this->create_certificate_template();\n\t\t$related = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t$earned = $this->earn_certificate( $user, $template, $related );\n\n\t\t$cert_id = $earned[1];\n\n\t\t// Generate a new cert when item not found in the cache.\n\t\t$orig_path = llms()->certificates()->get_export( $cert_id, true );\n\t\t$this->assertTrue( false !== strpos( $orig_path, '/uploads/llms-tmp/certificate-mock-certificate-title' ) );\n\n\t\t// Store the filepath for future use.\n\t\t$this->assertEquals( $orig_path, get_post_meta( $cert_id, '_llms_export_filepath', true ) );\n\n\t\t// Get it again, should return the original path from the cache.\n\t\t$cached_path = llms()->certificates()->get_export( $cert_id, true );\n\t\t$this->assertEquals( $orig_path, $cached_path );\n\n\t\t// Delete the file (simulate LLMS_TMP_DIR file expiration).\n\t\tunlink( $orig_path );\n\n\t\t// Should regen since the file saved in meta data doesn't exist anymore.\n\t\t$new_path = llms()->certificates()->get_export( $cert_id, true );\n\t\t$this->assertTrue( $orig_path !== $new_path );\n\n\t}\n\n\t/**\n\t * Test get_unique_slug()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_unique_slug() {\n\n\t\t$slugs = array();\n\n\t\t$i = 0;\n\t\twhile ( $i < 250 ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( array(\n\t\t\t\t'post_type' => 'llms_my_certificate',\n\t\t\t\t'post_name' => llms()->certificates()->get_unique_slug( 'A Title' ),\n\t\t\t) );\n\n\t\t\t$slugs[] = $post->post_name;\n\n\t\t\t$i++;\n\n\t\t}\n\n\t\t$this->assertEquals( 250, count( array_unique( $slugs ) ) );\n\n\t}\n\n\t/**\n\t * Test get_unique_slug() will increase the suffix string length after encountering collisions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_unique_slug_length_increase() {\n\n\t\t$handler = function( $password, $len ) {\n\t\t\treturn 3 === $len ? 'aaa' : $password;\n\t\t};\n\t\tadd_filter( 'random_password', $handler, 10, 2 );\n\n\t\t// Create a post with the '-aaa' suffix so we can have a real fake collision.\n\t\t$post = $this->factory->post->create_and_get( array(\n\t\t\t'post_type' => 'llms_my_certificate',\n\t\t\t'post_name' => llms()->certificates()->get_unique_slug( 'A Title' ),\n\t\t) );\n\t\t$this->assertEquals( 11, strlen( $post->post_name ) );\n\t\t$this->assertEquals( 'a-title-aaa', $post->post_name );\n\n\t\t// This request will result find '-aaa' as a collision and then try 4 more times and then increase to 4 characters.\n\t\t$this->assertEquals( 12, strlen( llms()->certificates()->get_unique_slug( 'A Title' ) ) );\n\n\t}\n\n\t/**\n\t * Test modify_dom_links()\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_dom_links() {\n\n\t\t// Copy test CSSs to the local website for testing purpose.\n\t\tLLMS_Unit_Test_Files::copy_asset( 'example-style-1.css', WP_CONTENT_DIR );\n\t\tLLMS_Unit_Test_Files::copy_asset( 'example-style-2.css', WP_CONTENT_DIR );\n\n\t\t$stylesheet_hrefs = array(\n\t\t\tget_site_url() . '/wp-content/example-style-1.css'                                                                                                                      => true, // Local.\n\t\t\tget_home_url() . '/wp-content/example-style-2.css'                                                                                                                      => true, // Local.\n\t\t\t'https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&#038;subset=latin,latin-ext&#038;display=swap' => false, // Blocked host.\n\t\t\t'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/example-style.css'                                                                            => true,\n\t\t\t'https://unreacha.ble/style.css'                                                                                                                                        => false,\n\t\t);\n\n\t\t$dom = $this->_get_certificate_dom(\n\t\t\tarray(\n\t\t\t\t'head' => array_reduce(\n\t\t\t\t\tarray_keys( $stylesheet_hrefs ),\n\t\t\t\t\tfunction( $carry, $stylesheet_href ) {\n\t\t\t\t\t\treturn sprintf(\n\t\t\t\t\t\t\t'%1$s<link rel=\"stylesheet\" href=\"%2$s\" type=\"test/css\" media=\"all\">',\n\t\t\t\t\t\t\t$carry,\n\t\t\t\t\t\t\t$stylesheet_href\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\tllms()->certificates(),\n\t\t\t'modify_dom_links',\n\t\t\tarray( $dom )\n\t\t);\n\n\t\t$dom->saveHTML();\n\n\t\t// Test there are no survived link tags (stylesheets are inlined).\n\t\t$this->assertEmpty( $dom->getElementsByTagName( 'link' )->length );\n\n\t\t$head = $dom->getElementsByTagName( 'head' )->item(0)->nodeValue;\n\n\t\tforeach ( $stylesheet_hrefs as $stylesheet_href => $contained ) {\n\n\t\t\t$stylesheet_raw = LLMS_Unit_Test_Util::call_method(\n\t\t\t\tllms()->certificates(),\n\t\t\t\t'get_stylesheet_raw',\n\t\t\t\tarray( $stylesheet_href, false )\n\t\t\t);\n\n\t\t\tif ( ! $stylesheet_raw ) {\n\t\t\t\t$this->assertFalse( $contained, $stylesheet_href );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( $contained ) {\n\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t$stylesheet_raw,\n\t\t\t\t\t$head,\n\t\t\t\t\t$stylesheet_href\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t$this->assertStringNotContainsString(\n\t\t\t\t\t$stylesheet_raw,\n\t\t\t\t\t$head,\n\t\t\t\t\t$stylesheet_href\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Delete copied assets.\n\t\tLLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/example-style-1.css' );\n\t\tLLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/example-style-2.css' );\n\n\t}\n\n\t/**\n\t * Test modify_dom_images()\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_dom_images() {\n\n\t\t// Copy test images to the local website for testing purpose.\n\t\tLLMS_Unit_Test_Files::copy_asset( 'klim-musalimov-rDMacl1FDjw-unsplash.jpeg', WP_CONTENT_DIR );\n\t\tLLMS_Unit_Test_Files::copy_asset( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg', WP_CONTENT_DIR );\n\n\t\t$image_srcs = array(\n\t\t\tget_site_url() . '/wp-content/klim-musalimov-rDMacl1FDjw-unsplash.jpeg'                                   => true, // Local.\n\t\t\tget_home_url() . '/wp-content/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg'                                  => true, // Local.\n\t\t\t'https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg'                                         => false, // Blocked host.\n\t\t\t'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg' => true,\n\t\t\t'https://unreach.able/christian-fregnan-unsplash.jpg'                                                     => false,\n\t\t);\n\n\t\t$dom = $this->_get_certificate_dom(\n\t\t\tarray(\n\t\t\t\t'certificate' => array_reduce(\n\t\t\t\t\tarray_keys( $image_srcs ),\n\t\t\t\t\tfunction( $carry, $image_src ) {\n\t\t\t\t\t\treturn sprintf(\n\t\t\t\t\t\t\t'%1$s<img src=\"%2$s\" loading=\"lazy\" srcset=\"%2$s 320w\" sizes=\"(max-width: 320px) 280px\">',\n\t\t\t\t\t\t\t$carry,\n\t\t\t\t\t\t\t$image_src\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Block wikimedia host.\n\t\tadd_filter(\n\t\t\t'llms_certificate_export_blocked_image_hosts',\n\t\t\tfunction () {\n\t\t\t\treturn array(\n\t\t\t\t\t'upload.wikimedia.org'\n\t\t\t\t);\n\t\t\t}\n\t\t);\n\n\t\t// Re-init certificates to apply the filter above.\n\t\tllms()->certificates()->init();\n\n\t\t// Modify DOM images.\n\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\tllms()->certificates(),\n\t\t\t'modify_dom_images',\n\t\t\tarray( $dom )\n\t\t);\n\n\t\t$html = $dom->saveHTML();\n\n\t\tforeach ( $image_srcs as $image_src => $contained ) {\n\n\t\t\t// Test the image src URLS are removed.\n\t\t\t$this->assertStringNotContainsString(\n\t\t\t\t$image_src,\n\t\t\t\t$html,\n\t\t\t\t$image_src\n\t\t\t);\n\n\t\t\t$image_data_type = LLMS_Unit_Test_Util::call_method(\n\t\t\t\tllms()->certificates(),\n\t\t\t\t'get_image_data_and_type',\n\t\t\t\tarray( $image_src, false )\n\t\t\t);\n\n\t\t\tif ( empty( $image_data_type['data'] ) || empty( $image_data_type['type'] ) ) {\n\t\t\t\t$this->assertFalse( $contained, $image_src );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$image_data = base64_encode( $image_data_type['data'] );\n\n\t\t\tif ( $contained ) {\n\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t$image_data,\n\t\t\t\t\t$html,\n\t\t\t\t\t$image_src\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\t$this->assertStringNotContainsString(\n\t\t\t\t\t$image_data,\n\t\t\t\t\t$html,\n\t\t\t\t\t$image_src\n\t\t\t\t);\n\t\t\t}\n\n\t\t}\n\n\t\t// Get images do not have loading, sizes, and srcset attributes.\n\t\tforeach ( $dom->getElementsByTagName( 'img' ) as $img ) {\n\t\t\t$this->assertEmpty( $img->getAttribute( 'srcset' ) );\n\t\t\t$this->assertEmpty( $img->getAttribute( 'sizes' ) );\n\t\t\t$this->assertEmpty( $img->getAttribute( 'loading' ) );\n\t\t}\n\n\t\t// Clean added filters.\n\t\tremove_all_filters( 'llms_certificate_export_blocked_image_hosts' );\n\n\t\t// Delete copied images.\n\t\tLLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/klim-musalimov-rDMacl1FDjw-unsplash.jpeg' );\n\t\tLLMS_Unit_Test_Files::remove( WP_CONTENT_DIR . '/yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\n\t}\n\n\t/**\n\t * Util to build a DOMDocument similar to the scraped certificate\n\t *\n\t * @since 4.21.0\n\t *\n\t * @param array $dom_sections Sections of the page.\n\t * @return DOMDocument|WP_Error\n\t */\n\tprivate function _get_certificate_dom( $dom_sections ) {\n\t\t$sections = array(\n\t\t\t'head'         => '',\n\t\t\t'certificate'  => '',\n\t\t\t'footer'       => '',\n\t\t);\n\n\t\t$sections = wp_parse_args( $dom_sections, $sections );\n\n\t\t$html = '\n\t\t<!DOCTYPE html>\n<html lang=\"en-US\">\n\t<head>\n\t\t<meta charset=\"UTF-8\">\n\t\t'\n\t\t. $sections['head']  .\n\t\t'\n\t</head>\n\t<body>\n\t\t<div class=\"llms-certificate-container\" style=\"width:800px; height:616px;\">\n\t\t\t<div id=\"certificate-243\" class=\"post-243 llms_certificate type-llms_certificate status-publish hentry\">\n\t\t\t\t<div class=\"llms-summary\">'\n\t\t\t\t. $sections['certificate'] .\n\t\t\t\t'</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<footer>'\n\t\t. $sections['footer'] .\n\t\t'</footer>\n\t</body>\n</html>';\n\n\t\t$dom = llms_get_dom_document( $html );\n\t\tif ( is_wp_error( $dom ) ) {\n\t\t\treturn $dom;\n\t\t}\n\n\t\t// Don't throw or log warnings.\n\t\t$libxml_state = libxml_use_internal_errors( true );\n\n\t\treturn $dom;\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-cli.php",
    "content": "<?php\n/**\n * Test inclusion and initialization of the LLMS-CLI library\n *\n * @package LifterLMS/Tests\n *\n * @group cli\n * @group packages\n *\n * @since 5.5.0\n * @version 5.5.0\n */\nclass LLMS_Test_CLI extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test rest package exists and is loaded.\n\t *\n\t * @since 5.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cli_package_exists() {\n\t\t$this->assertTrue( function_exists( 'llms_cli' ) );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-comments.php",
    "content": "<?php\n/**\n * Test LLMS_Comments\n *\n * @package LifterLMS/Tests\n *\n * @group comments\n *\n * @since 3.37.12\n */\nclass LLMS_Test_Comments extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Conditionally skips a test related to the `wp_count_comments()` method.\n\t *\n\t * @since 6.6.0\n\t *\n\t * @param boolean $should_count Whether the current version of WP should count comments.\n\t * @return void\n\t */\n\tpublic function maybe_skip_count_test( $should_count = false ) {\n\n\t\tif ( $should_count === LLMS_Unit_Test_Util::call_method( 'LLMS_Comments', 'should_modify_comment_counts' ) ) {\n\t\t\tglobal $wp_version;\n\t\t\t$this->markTestSkipped( \"Test skipped on WP Version: {$wp_version}.\" );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test wp_count_comments() throws `_doing_it_wrong()` and returns early when called on WP 6.0 or later.\n\t *\n\t * @since 6.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_wp_count_comments_wp_6_dot_0_or_later() {\n\n\t\t$this->maybe_skip_count_test( true );\n\n\t\t$this->setExpectedIncorrectUsage( 'LLMS_Comments::wp_count_comments' );\n\n\t\t$expect = array( 123 );\n\t\t$this->assertEquals( $expect, LLMS_Comments::wp_count_comments( $expect, 123 ) );\n\n\t}\n\n\t/**\n\t * Test wp_count_comments() when passing in a specific post id.\n\t *\n\t * @since 3.37.12\n\t * @since 6.6.0 Added mock input data to guard against false-positives.\n\t *               Skip test on WP 6.0 or later.\n\t *\n\t * @return void\n\t */\n\tpublic function test_wp_count_comments_specific_post() {\n\n\t\t$this->maybe_skip_count_test();\n\n\t\t$expect = array( 123 );\n\t\t$this->assertEquals( $expect, LLMS_Comments::wp_count_comments( $expect, 123 ) );\n\n\t}\n\n\t/**\n\t * Test wp_count_comments() when the transient already exists.\n\t *\n\t * @since 3.37.12\n\t * @since 6.6.0 Skip test on WP 6.0 or later.\n\t *\n\t * @return void\n\t */\n\tpublic function test_wp_count_comments_transient_exists() {\n\n\t\t$this->maybe_skip_count_test();\n\n\t\t$expect = array( 1 );\n\t\tset_transient( 'llms_count_comments', $expect, 10 );\n\n\t\t$this->assertEquals( $expect, LLMS_Comments::wp_count_comments( $expect, 0 ) );\n\n\t}\n\n\t/**\n\t * Test wp_count_comments() when a new stats object should be generated\n\t *\n\t * @since 3.37.12\n\t * @since 6.6.0 Skip test on WP 6.0 or later.\n\t *\n\t * @return void\n\t */\n\tpublic function test_wp_count_comments_new() {\n\n\t\t$this->maybe_skip_count_test();\n\n\t\t// Insert 5 regular comments.\n\t\t$this->factory->comment->create_many( 5 );\n\n\t\t// Insert 5 other custom comment types (we don't want to mess with other plugins).\n\t\t$this->factory->comment->create_many( 5, array( 'comment_type' => 'custom_type' ) );\n\n\t\t// Insert 5 order notes, these will be excluded.\n\t\t$this->factory->comment->create_many( 5, array( 'comment_type' => 'llms_order_note' ) );\n\n\t\t$res = LLMS_Comments::wp_count_comments( array(), 0 );\n\n\t\t// Ensure the function creates the stats object in the correct format.\n\t\t$keys = array( 'approved', 'moderated', 'spam', 'trash', 'post-trashed', 'total_comments', 'all' );\n\t\t$this->assertEqualSets( $keys, array_keys( get_object_vars( $res ) ) );\n\n\t\t// Order notes should be excluded.\n\t\t$this->assertEquals( 10, $res->total_comments );\n\t\t$this->assertEquals( 10, $res->all );\n\t\t$this->assertEquals( 10, $res->approved );\n\n\t\t// All of these are default 0.\n\t\t$this->assertEquals( 0, $res->moderated );\n\t\t$this->assertEquals( 0, $res->spam );\n\t\t$this->assertEquals( 0, $res->trash );\n\t\t$this->assertEquals( 0, $res->{'post-trashed'} );\n\n\t}\n\n\t/**\n\t * Test wp_count_comments() when another plugin has already created a stats object we want to modify\n\t *\n\t * @since 3.37.12\n\t * @since 6.6.0 Skip test on WP 6.0 or later.\n\t *\n\t * @return void\n\t */\n\tpublic function test_wp_count_comments_modify_existing() {\n\n\t\t$this->maybe_skip_count_test();\n\n\t\t// Insert 5 regular comments.\n\t\t$this->factory->comment->create_many( 5 );\n\n\t\t// Insert 5 other custom comment types (we don't want to mess with other plugins).\n\t\t$this->factory->comment->create_many( 5, array( 'comment_type' => 'custom_type' ) );\n\n\t\t// Insert 5 order notes, these will be excluded.\n\t\t$this->factory->comment->create_many( 5, array( 'comment_type' => 'llms_order_note' ) );\n\n\t\tremove_filter( 'wp_count_comments', array( 'LLMS_Comments', 'wp_count_comments' ), 999 );\n\t\t$stats = wp_count_comments();\n\t\tadd_filter( 'wp_count_comments', array( 'LLMS_Comments', 'wp_count_comments' ), 999, 2 );\n\n\t\t$res = LLMS_Comments::wp_count_comments( $stats, 0 );\n\n\t\t// Ensure the function creates the stats object in the correct format.\n\t\t$keys = array( 'approved', 'moderated', 'spam', 'trash', 'post-trashed', 'total_comments', 'all' );\n\t\t$this->assertEqualSets( $keys, array_keys( get_object_vars( $res ) ) );\n\n\t\t// Order notes should be excluded.\n\t\t$this->assertEquals( 10, $res->total_comments );\n\t\t$this->assertEquals( 10, $res->all );\n\t\t$this->assertEquals( 10, $res->approved );\n\n\t\t// All of these are default 0.\n\t\t$this->assertEquals( 0, $res->moderated );\n\t\t$this->assertEquals( 0, $res->spam );\n\t\t$this->assertEquals( 0, $res->trash );\n\t\t$this->assertEquals( 0, $res->{'post-trashed'} );\n\n\t}\n\n\t/**\n\t * Test should_modify_comment_counts().\n\t *\n\t * @since 6.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_modify_comment_counts() {\n\n\t\tglobal $wp_version;\n\n\t\t$orig_version = $wp_version;\n\n\t\t$tests = array(\n\t\t\t'6.0'     => false,\n\t\t\t'6.0-RC3' => false,\n\t\t\t'6.0-src' => false,\n\t\t\t'6.0.1'   => false,\n\t\t\t'5.9.3'   => true,\n\t\t\t'5.9.1'   => true,\n\t\t\t'5.9'     => true,\n\t\t\t'5.8.1'   => true,\n\t\t\t'5.8'     => true,\n\t\t\t'5.7'     => true,\n\t\t);\n\n\t\tforeach ( $tests as $wp_version => $expected ) {\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Comments', 'should_modify_comment_counts' ) );\n\t\t}\n\n\t\t$wp_version = $orig_version;\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-db-upgrader.php",
    "content": "<?php\n/**\n * Test AJAX Handler\n *\n * @package LifterLMS/Tests\n *\n * @group upgrader\n *\n * @since 5.2.0\n */\nclass LLMS_Test_DB_Upgrader extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test can_auto_update()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_auto_update() {\n\n\t\t// All manual.\n\t\t$updates = array(\n\t\t\t'1.0.0' => array(\n\t\t\t\t'type' => 'manual',\n\t\t\t),\n\t\t\t'2.0.0' => array(\n\t\t\t\t'type' => 'manual',\n\t\t\t),\n\t\t);\n\t\t$upgrader = new LLMS_DB_Upgrader( '0.0.1', $updates );\n\t\t$this->assertFalse( $upgrader->can_auto_update() );\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.5.0', $updates );\n\t\t$this->assertFalse( $upgrader->can_auto_update() );\n\n\t\t// As one auto but it's still manual.\n\t\t$updates['2.0.0']['type'] = 'auto';\n\t\t$upgrader = new LLMS_DB_Upgrader( '0.1.0', $updates );\n\t\t$this->assertFalse( $upgrader->can_auto_update() );\n\n\t\t// Only auto so it's okay.\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.9.999', $updates );\n\t\t$this->assertTrue( $upgrader->can_auto_update() );\n\n\t}\n\n\t/**\n\t * Test constructor and get_updates()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor_and_get_updates() {\n\n\t\t// No update schema passed, use the core included file.\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.2.3' );\n\n\t\t$expect = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-db-updates.php';\n\t\t$this->assertEquals( $expect, $upgrader->get_updates() );\n\n\t\t// Pass in a schema.\n\t\t$schema = array(\n\t\t\t'1.2.3' => array(\n\t\t\t\t'type' => 'manual',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'fake_callback',\n\t\t\t\t\t'fake_callback_2',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'2.0.0' => array(\n\t\t\t\t'type' => 'auto',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'fake_callback',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.2.3', $schema );\n\t\t$this->assertEquals( $schema, $upgrader->get_updates() );\n\n\t}\n\n\t/**\n\t * Test get_callback_prefix()\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_callback_prefix() {\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.2.3' );\n\n\t\t$tests = array(\n\t\t\tarray( false, '5.0.0', '', ),\n\t\t\tarray( null, '5.0.0', '', ),\n\t\t\tarray( true, '5.0.0', 'LLMS\\Updates\\Version_5_0_0\\\\', ),\n\t\t\tarray( true, '5.0.0-beta.1', 'LLMS\\Updates\\Version_5_0_0\\\\', ),\n\t\t\tarray( true, '5.0.0-alpha.1', 'LLMS\\Updates\\Version_5_0_0\\\\', ),\n\t\t\tarray( 'Custom\\String\\Provided', '1.0.0', 'Custom\\String\\Provided\\Version_1_0_0\\\\', ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\tlist( $namespace, $version, $expected ) = $test;\n\n\t\t\t$info = compact( 'namespace' );\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $upgrader, 'get_callback_prefix', array( $info, $version ) ) );\n\n\t\t}\n\n\t\t// When `$namespace` not provided in the $info object.\n\t\t$this->assertEquals( '', LLMS_Unit_Test_Util::call_method( $upgrader, 'get_callback_prefix', array( array(), $version ) ) );\n\n\t}\n\n\t/**\n\t * Test enuqeue_updates() when auto updating\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_updates_auto() {\n\n\t\t$schema = array(\n\t\t\t'1.5.0' => array(\n\t\t\t\t'type' => 'auto',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'update_auto',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.2.3', $schema );\n\t\t$upgrader->enqueue_updates();\n\n\t\t$updater = LLMS_Unit_Test_Util::get_private_property_value( $upgrader, 'updater' );\n\t\t$batch   = LLMS_Unit_Test_Util::call_method( $updater, 'get_batch' )->data;\n\n\t\t$this->assertEquals( array( 'update_auto' ), $batch );\n\n\t\t// Reinit the updater for future tests.\n\t\tLLMS_Install::init_background_updater();\n\n\t}\n\n\t/**\n\t * Test enuqeue_updates() when manual updating is required\n\t *\n\t * @since 5.2.0\n\t * @since 5.6.0 Add tests for automatic namespacing.\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_updates_manual() {\n\n\t\t$schema = array(\n\t\t\t'1.5.0' => array(\n\t\t\t\t'type' => 'manual',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'update_150_1',\n\t\t\t\t\t'update_150_2',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'2.0.0' => array(\n\t\t\t\t'type' => 'auto',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'update_200',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'3.5.1' => array(\n\t\t\t\t'type'      => 'manual',\n\t\t\t\t'namespace' => true,\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'update_something',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'3.9.9' => array(\n\t\t\t\t'type'      => 'manual',\n\t\t\t\t'namespace' => 'Custom\\Namespace',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'update_something',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.2.3', $schema );\n\n\t\t$upgrader->enqueue_updates();\n\n\t\t// Check logs.\n\t\t$expected_logs = array(\n\t\t\t'Queuing 1.5.0 - update_150_1',\n\t\t\t'Queuing 1.5.0 - update_150_2',\n\t\t\t'Queuing 2.0.0 - update_200',\n\t\t\t'Queuing 3.5.1 - LLMS\\Updates\\Version_3_5_1\\update_something',\n\t\t\t'Queuing 3.9.9 - Custom\\Namespace\\Version_3_9_9\\update_something',\n\t\t);\n\t\t$this->assertEquals( $expected_logs, $this->logs->get( 'updater' ) );\n\n\t\t// Callbacks loaded into queue properly.\n\t\t$expected_batch = array(\n\t\t\t'update_150_1',\n\t\t\t'update_150_2',\n\t\t\t'update_200',\n\t\t\t'LLMS\\Updates\\Version_3_5_1\\update_something',\n\t\t\t'Custom\\Namespace\\Version_3_9_9\\update_something',\n\t\t);\n\n\t\t$updater = LLMS_Unit_Test_Util::get_private_property_value( $upgrader, 'updater' );\n\t\t$batch   = LLMS_Unit_Test_Util::call_method( $updater, 'get_batch' )->data;\n\n\t\t// Show completion message.\n\t\t$complete = array_pop( $batch );\n\t\t$this->assertInstanceOf( 'LLMS_DB_Upgrader', $complete[0] );\n\t\t$this->assertEquals( 'show_notice_complete', $complete[1] );\n\n\t\t// Rest of the callbacks.\n\t\t$this->assertEquals( $expected_batch, $batch );\n\n\t\t// Reinit the updater for future tests.\n\t\tLLMS_Install::init_background_updater();\n\n\t}\n\n\t/**\n\t * Test get_required_updates() and has_required_updates()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_required_updates_and_has_required_updates() {\n\n\t\t// Mock updates.\n\t\t$updates = array(\n\t\t\t'1.2.3' => array(),\n\t\t\t'2.0.0' => array(),\n\t\t\t'3.0.5' => array(),\n\t\t\t'4.5.6' => array(),\n\t\t);\n\n\t\tforeach ( array( '0.1.1', '1.0.0', '1.2.2' ) as $version ) {\n\t\t\t$upgrader = new LLMS_DB_Upgrader( $version, $updates );\n\t\t\t$this->assertEquals( $updates, $upgrader->get_required_updates() );\n\t\t\t$this->assertTrue( $upgrader->has_required_updates() );\n\t\t}\n\n\t\tunset( $updates['1.2.3'] );\n\t\tforeach ( array( '1.2.3', '1.5.0', '1.99.999' ) as $version ) {\n\t\t\t$upgrader = new LLMS_DB_Upgrader( $version, $updates );\n\t\t\t$this->assertEquals( $updates, $upgrader->get_required_updates() );\n\t\t\t$this->assertTrue( $upgrader->has_required_updates() );\n\t\t}\n\n\t\tunset( $updates['2.0.0'] );\n\t\t$upgrader = new LLMS_DB_Upgrader( '2.0.0', $updates );\n\t\t$this->assertEquals( $updates, $upgrader->get_required_updates() );\n\t\t$this->assertTrue( $upgrader->has_required_updates() );\n\n\t\tunset( $updates['3.0.5'] );\n\t\t$upgrader = new LLMS_DB_Upgrader( '4.1.2', $updates );\n\t\t$this->assertEquals( $updates, $upgrader->get_required_updates() );\n\t\t$this->assertTrue( $upgrader->has_required_updates() );\n\n\t\t// No updates.\n\t\tforeach ( array( '4.5.6', '5.0.0', '10.5.9' ) as $version ) {\n\t\t\t$upgrader = new LLMS_DB_Upgrader( $version, $updates );\n\t\t\t$this->assertEquals( array(), $upgrader->get_required_updates() );\n\t\t\t$this->assertFalse( $upgrader->has_required_updates() );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test show_notice_complete()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_show_notice_complete() {\n\n\t\tLLMS_Admin_Notices::add_notice( 'bg-db-update-started', 'notice' );\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.2.3' );\n\t\tLLMS_Unit_Test_Util::call_method( $upgrader, 'show_notice_complete' );\n\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'bg-db-update-started' ) );\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'bg-db-update-complete' ) );\n\n\t}\n\n\t/**\n\t * Test show_notice_pending()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_show_notice_pending() {\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.2.3' );\n\n\t\t// Add a fake notice so we can make sure it's deleted.\n\t\tLLMS_Admin_Notices::add_notice( 'bg-db-update', 'deleted' );\n\t\tLLMS_Unit_Test_Util::call_method( $upgrader, 'show_notice_pending' );\n\n\t\t// Has notice.\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'bg-db-update' ) );\n\n\t}\n\n\t/**\n\t * Test update() when no updates are required.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_no_required() {\n\n\t\t$upgrader = new LLMS_DB_Upgrader( '5.0.0', array( '1.0.0' => array() ) );\n\t\t$this->assertFalse( $upgrader->update() );\n\n\t}\n\n\t/**\n\t * Test update() when updates are required.\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_required() {\n\n\t\tLLMS_Admin_Notices::delete_notice( 'bg-db-update' );\n\n\t\t$schema = array(\n\t\t\t'1.5.0' => array(\n\t\t\t\t'type' => 'manual',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'update_150_1',\n\t\t\t\t\t'update_150_2',\n\t\t\t\t),\n\t\t\t),\n\t\t\t'2.0.0' => array(\n\t\t\t\t'type' => 'auto',\n\t\t\t\t'updates' => array(\n\t\t\t\t\t'update_200',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t// Manual update.\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.0.0', $schema );\n\t\t$this->assertTrue( $upgrader->update() );\n\n\t\t// Notice displayed.\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'bg-db-update' ) );\n\t\tLLMS_Admin_Notices::delete_notice( 'bg-db-update' );\n\n\n\t\t// Auto update.\n\t\t$upgrader = new LLMS_DB_Upgrader( '1.9.1', $schema );\n\t\t$this->assertTrue( $upgrader->update() );\n\n\t\t// No notice displayed.\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'bg-db-update' ) );\n\t\t// Updates queued.\n\t\t$this->assertEquals( array( 'Queuing 2.0.0 - update_200' ), $this->logs->get( 'updater' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-engagement-handler.php",
    "content": "<?php\n/**\n * Tests for LLMS_Engagement_Handler class\n *\n * @package LifterLMS/Tests\n *\n * @group engagements\n * @group engagement_handler\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Engagement_Handler extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test can_process()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_process() {\n\n\t\t$related = $this->factory->course->create( array( 'sections' => 0 ) );\n\n\t\t$user_not_enrolled = $this->factory->user->create();\n\n\t\t$user_enrolled = $this->factory->user->create();\n\t\tllms_enroll_student( $user_enrolled, $related );\n\n\t\tforeach ( array( 'achievement', 'certificate', 'email' ) as $type ) {\n\n\t\t\t$engagement = $this->create_mock_engagement( 'course_enrollment', $type, 0, $related );\n\t\t\t$template   = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\n\t\t\t// User doesn't exist.\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'can_process', array(\n\t\t\t\t$type,\n\t\t\t\t$user_enrolled + 1,\n\t\t\t\t$template,\n\t\t\t\t$related,\n\t\t\t\t$engagement->ID,\n\t\t\t) );\n\n\t\t\t$this->assertEquals( 1, count( $res ) );\n\t\t\t$this->assertIsWpError( $res[0] );\n\t\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-check-user--not-found', $res[0] );\n\n\t\t\t// Template post problem.\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'can_process', array(\n\t\t\t\t$type,\n\t\t\t\t$user_enrolled,\n\t\t\t\t$template + 1,\n\t\t\t\t$related,\n\t\t\t\t$engagement->ID,\n\t\t\t) );\n\n\t\t\t$this->assertEquals( 1, count( $res ) );\n\t\t\t$this->assertIsWpError( $res[0] );\n\t\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-post--type', $res[0] );\n\n\t\t\t// Engagement post problem.\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'can_process', array(\n\t\t\t\t$type,\n\t\t\t\t$user_enrolled,\n\t\t\t\t$template,\n\t\t\t\t$related,\n\t\t\t\t$engagement->ID + 1,\n\t\t\t) );\n\n\t\t\t$this->assertEquals( 1, count( $res ) );\n\t\t\t$this->assertIsWpError( $res[0] );\n\t\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-post--not-found', $res[0] );\n\n\t\t\t// Not enrolled.\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'can_process', array(\n\t\t\t\t$type,\n\t\t\t\t$user_not_enrolled,\n\t\t\t\t$template,\n\t\t\t\t$related,\n\t\t\t\t$engagement->ID,\n\t\t\t) );\n\n\t\t\t$this->assertEquals( 1, count( $res ) );\n\t\t\t$this->assertIsWpError( $res[0] );\n\t\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-check-post--enrollment', $res[0] );\n\n\t\t\t// All Good.\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'can_process', array(\n\t\t\t\t$type,\n\t\t\t\t$user_enrolled,\n\t\t\t\t$template,\n\t\t\t\t$related,\n\t\t\t\t$engagement->ID,\n\t\t\t) );\n\t\t\t$this->assertTrue( $res );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test handle_achievement()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_achievement() {\n\n\t\t$actions = did_action( 'llms_user_earned_achievement' );\n\n\t\t$user_id     = $this->factory->student->create();\n\t\t$engagement  = $this->create_mock_engagement( 'course_completed', 'achievement' );\n\t\t$related_id  = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\t\t$template_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\n\t\t$handle_args = array( $user_id, $template_id, $related_id, $engagement->ID );\n\n\t\t// Add a thumbnail to the template.\n\t\t$attachment_id = $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\tset_post_thumbnail( $template_id, $attachment_id );\n\n\t\t$earned = LLMS_Engagement_Handler::handle_achievement( $handle_args );\n\n\t\t// Enrollment error.\n\t\t$this->assertIsWPError( $earned[0] );\n\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-check-post--enrollment', $earned[0] );\n\n\t\tllms_enroll_student( $user_id, $related_id );\n\n\t\t// No errors.\n\t\t$earned = LLMS_Engagement_Handler::handle_achievement( $handle_args );\n\n\t\t// Proper object returned.\n\t\t$this->assertInstanceOf( 'LLMS_User_Achievement', $earned );\n\n\t\t// Relationships saved as meta.\n\t\t$this->assertEquals( $related_id, $earned->get( 'related' ) );\n\t\t$this->assertEquals( $engagement->ID, $earned->get( 'engagement' ) );\n\t\t$this->assertEquals( $template_id, $earned->get( 'parent' ) );\n\n\t\t// Content and Title.\n\t\t$this->assertEquals( get_post_meta( $template_id, '_llms_achievement_title', true ), $earned->get( 'title' ) );\n\t\t$this->assertEquals( get_the_content( null, false, $template_id ), $earned->get( 'content', true ) );\n\n\t\t// Author.\n\t\t$this->assertEquals( $user_id, $earned->get( 'author' ) );\n\n\t\t// Featured Image.\n\t\t$this->assertEquals( $attachment_id, get_post_thumbnail_id( $earned->get( 'id' ) ) );\n\n\t\t// Ran action.\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_user_earned_achievement' ) );\n\n\t\t// Added user postmeta.\n\t\t$this->assertEquals( $earned->get( 'id' ), llms_get_user_postmeta( $user_id, $related_id, '_achievement_earned', true ) );\n\n\t\t// Try it again, we should get a dupcheck.\n\t\t$earned = LLMS_Engagement_Handler::handle_achievement( $handle_args );\n\t\t$this->assertIsWPError( $earned[0] );\n\t\t$this->assertWPErrorCodeEquals( 'llms-engagement--is-duplicate', $earned[0] );\n\n\t}\n\n\t/**\n\t * Test handle_certificate().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_certificate() {\n\n\t\t$actions = did_action( 'llms_user_earned_certificate' );\n\n\t\t$user_id     = $this->factory->student->create();\n\t\t$engagement  = $this->create_mock_engagement( 'course_completed', 'certificate' );\n\t\t$related_id  = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\t\t$template_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\n\t\t$handle_args = array( $user_id, $template_id, $related_id, $engagement->ID );\n\n\t\t// Add a thumbnail to the template.\n\t\t$attachment_id = $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\tset_post_thumbnail( $template_id, $attachment_id );\n\n\t\t$expected_date = date( get_option( 'date_format' ) );\n\n\t\t$earned = LLMS_Engagement_Handler::handle_certificate( $handle_args );\n\n\t\t// Enrollment error.\n\t\t$this->assertIsWPError( $earned[0] );\n\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-check-post--enrollment', $earned[0] );\n\n\t\tllms_enroll_student( $user_id, $related_id );\n\n\t\t// No errors.\n\t\t$earned = LLMS_Engagement_Handler::handle_certificate( $handle_args );\n\n\t\t// Proper object returned.\n\t\t$this->assertInstanceOf( 'LLMS_User_Certificate', $earned );\n\n\t\t// Relationships saved as meta.\n\t\t$this->assertEquals( $related_id, $earned->get( 'related' ) );\n\t\t$this->assertEquals( $engagement->ID, $earned->get( 'engagement' ) );\n\t\t$this->assertEquals( $template_id, $earned->get( 'parent' ) );\n\n\t\t// Content and Title.\n\t\t$this->assertEquals( get_post_meta( $template_id, '_llms_certificate_title', true ), $earned->get( 'title' ) );\n\t\t$this->assertEquals( \"Test Blog, {$expected_date}\", $earned->get( 'content', true ) );\n\n\t\t// Author.\n\t\t$this->assertEquals( $user_id, $earned->get( 'author' ) );\n\n\t\t// Featured Image.\n\t\t$this->assertEquals( $attachment_id, get_post_thumbnail_id( $earned->get( 'id' ) ) );\n\n\t\t// Ran action.\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_user_earned_certificate' ) );\n\n\t\t// Added user postmeta.\n\t\t$this->assertEquals( $earned->get( 'id' ), llms_get_user_postmeta( $user_id, $related_id, '_certificate_earned', true ) );\n\n\t\t// Try it again, we should get a dupcheck.\n\t\t$earned = LLMS_Engagement_Handler::handle_certificate( $handle_args );\n\t\t$this->assertIsWPError( $earned[0] );\n\t\t$this->assertWPErrorCodeEquals( 'llms-engagement--is-duplicate', $earned[0] );\n\n\t}\n\n\t/**\n\t * Test do_deprecated_creation_filters()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @expectedDeprecated lifterlms_new_achievement\n\t * @expectedDeprecated lifterlms_new_page\n\t *\n\t * @return void\n\t */\n\tpublic function test_do_deprecated_creation_filters() {\n\n\t\t$callback = function( $args ) {\n\t\t\treturn 'changed';\n\t\t};\n\n\t\t$tests = array(\n\t\t\t'achievement' => 'lifterlms_new_achievement',\n\t\t\t'certificate' => 'lifterlms_new_page',\n\t\t);\n\n\t\tforeach ( $tests as $type => $deprecated_hook ) {\n\n\t\t\t// Nothing attached.\n\t\t\t$this->assertEquals( 'args', LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'do_deprecated_creation_filters', array( 'args', $type ) ) );\n\n\t\t\t// Attached.\n\t\t\tadd_filter( $deprecated_hook, $callback );\n\t\t\t$this->assertEquals( 'changed', LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'do_deprecated_creation_filters', array( 'args', $type ) ) );\n\n\t\t\t// Invalid type.\n\t\t\t$this->assertEquals( 'args', LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'do_deprecated_creation_filters', array( 'args', 'fake' ) ) );\n\t\t\tremove_filter( $deprecated_hook, $callback );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-engagements.php",
    "content": "<?php\n/**\n * Tests for LLMS_Engagements class\n *\n * @package LifterLMS/Tests\n *\n * @group engagements\n * @group engagements_main\n *\n * @since 4.4.1\n * @since 4.4.3 Test different emails triggered by the same post are correctly sent.\n */\nclass LLMS_Test_Engagements extends LLMS_UnitTestCase {\n\n\t/**\n\t * Returns a mock 3rd party engagements class.\n\t *\n\t * @since 6.6.0\n\t *\n\t * @return object\n\t */\n\tprivate function instantiate_mock_engagements() {\n\n\t\t$mock_engagements = new class {\n\n\t\t\tpublic $engagement_action = 'llms_mock_curriculum_completed';\n\n\t\t\tpublic $event_type = 'diploma';\n\n\t\t\tpublic $handler_action = 'lifterlms_engagement_ship_diploma';\n\n\t\t\tpublic $post_type = 'llms_mock_diploma';\n\n\t\t\tpublic function __construct() {\n\t\t\t\tregister_post_type( 'llms_mock_diploma' );\n\t\t\t\tadd_filter( 'lifterlms_engagement_types', array( $this, 'register_engagement_types' ), 10, 1 );\n\t\t\t\tadd_filter( 'lifterlms_engagement_actions', array( $this, 'register_engagement_actions' ), 10, 1 );\n\n\t\t\t\tadd_filter(\n\t\t\t\t\t'lifterlms_external_engagement_handler_arguments',\n\t\t\t\t\tarray( $this, 'filter_engagement_handler_arguments' ),\n\t\t\t\t\t10,\n\t\t\t\t\t5\n\t\t\t\t);\n\n\t\t\t\tadd_filter(\n\t\t\t\t\t'lifterlms_external_engagement_query_arguments',\n\t\t\t\t\tarray( $this, 'filter_engagement_query_arguments' ),\n\t\t\t\t\t10,\n\t\t\t\t\t3\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tpublic function __destruct() {\n\t\t\t\tunregister_post_type( 'llms_mock_diploma' );\n\t\t\t}\n\n\t\t\tpublic function filter_engagement_handler_arguments( $parsed, $engagement, $user_id, $related_post_id, $event_type ) {\n\t\t\t\tif ( $this->event_type !== $event_type ) {\n\t\t\t\t\treturn $parsed;\n\t\t\t\t}\n\t\t\t\t$parsed['handler_action'] = $this->handler_action;\n\t\t\t\t$parsed['handler_args']   = array( $user_id, $engagement->engagement_id, $related_post_id, $engagement->trigger_id );\n\n\t\t\t\treturn $parsed;\n\t\t\t}\n\n\t\t\tpublic function filter_engagement_query_arguments( $parsed, $action, $args ) {\n\t\t\t\tif ( $this->engagement_action !== $action ) {\n\t\t\t\t\treturn $parsed;\n\t\t\t\t}\n\t\t\t\t$parsed['trigger_type']    = $action;\n\t\t\t\t$parsed['user_id']         = $args[0];\n\t\t\t\t$parsed['related_post_id'] = $args[1];\n\n\t\t\t\treturn $parsed;\n\t\t\t}\n\n\t\t\tpublic function register_engagement_types( $engagement_types ) {\n\t\t\t\t$engagement_types[ $this->event_type ] = __( 'Print and mail a diploma', 'lifterlms' );\n\n\t\t\t\treturn $engagement_types;\n\t\t\t}\n\n\t\t\tpublic function register_engagement_actions( $engagement_actions ) {\n\t\t\t\t$engagement_actions[] = $this->engagement_action;\n\n\t\t\t\treturn $engagement_actions;\n\t\t\t}\n\t\t};\n\n\t\treturn new $mock_engagements;\n\t}\n\n\t/**\n\t * Set up before class.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tllms()->certificates();\n\n\t}\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 4.4.1\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->main = llms()->engagements();\n\t\treset_phpmailer_instance();\n\t}\n\n\t/**\n\t * Teardown test case\n\t *\n\t * @since 4.4.1\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\treset_phpmailer_instance();\n\n\t}\n\n\t/**\n\t * Test delayed triggers are unscheduled when the triggering engagement post is trashed/deleted.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/290\n\t *\n\t * @expectedDeprecated LLMS_Engagements::handle_email\n\t *\n\t * @return void\n\t */\n\tpublic function test_delayed_engagement_deleted() {\n\n\t\t$users = $this->factory->user->create_many( 5 );\n\n\t\t$delay              = 1;\n\t\t$engagement         = $this->create_mock_engagement( 'course_completed', 'email', $delay );\n\t\t$engagement_post_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\t\t$related_post_id    = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\n\t\t$trigger_filter     = 'lifterlms_course_completed';\n\t\t$expected_action    = 'lifterlms_engagement_send_email';\n\n\t\tforeach ( $users as $user ) {\n\n\t\t\tllms_enroll_student( $user, $related_post_id );\n\n\t\t\t$trigger_args  = array( $user, $related_post_id );\n\t\t\t$expected_args = array( array( $user, $engagement_post_id, absint( $related_post_id ), $engagement->ID ) );\n\n\t\t\t// Record the number of run actions so we can ensure it was properly incremented.\n\t\t\t$start_actions = did_action( $expected_action );\n\n\t\t\t// Mock the `current_filter()` return.\n\t\t\tglobal $wp_current_filter;\n\t\t\t$wp_current_filter = array( $trigger_filter );\n\n\t\t\t// Simulate trigger callback.\n\t\t\t$this->main->maybe_trigger_engagement( ...$trigger_args );\n\n\t\t\t// Event scheduled.\n\t\t\t$this->assertTrue( as_has_scheduled_action( $expected_action, $expected_args, sprintf( 'llms_engagement_%d', $engagement->ID ) ) );\n\n\t\t}\n\n\t\t// Trash the engagement.\n\t\twp_trash_post( $engagement->ID );\n\n\t\tforeach ( $users as $user ) {\n\t\t\t$expected_args = array( array( $user, $engagement_post_id, absint( $related_post_id ), $engagement->ID ) );\n\n\t\t\t// Item is still scheduled.\n\t\t\t$this->assertTrue( as_has_scheduled_action( $expected_action, $expected_args, sprintf( 'llms_engagement_%d', $engagement->ID ) ) );\n\n\t\t\t// Will not fire when it's triggered.\n\t\t\t$errs = $this->main->handle_email( $expected_args[0] );\n\t\t\t$this->assertIsWPError( $errs );\n\t\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-post--status', $errs );\n\n\t\t}\n\n\t\t// Delete the engagement.\n\t\twp_delete_post( $engagement->ID );\n\n\t\t// The whole group is unscheduled.\n\t\tforeach ( $users as $user ) {\n\t\t\t$expected_args = array( array( $user, $engagement_post_id, absint( $related_post_id ), $engagement->ID ) );\n\t\t\t$this->assertFalse( as_has_scheduled_action( $expected_action, $expected_args, sprintf( 'llms_engagement_%d', $engagement->ID ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test handle_email() as triggered by a related post type that's enrollable.\n\t *\n\t * @since 4.4.1\n\t * @since 6.0.0 Update test against new error codes and expect deprecated warning.\n\t *\n\t * @expectedDeprecated LLMS_Engagements::handle_email\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_email_with_course_posts() {\n\n\t\t$mailer = tests_retrieve_phpmailer_instance();\n\n\t\t$user  = $this->factory->user->create_and_get();\n\t\t$email = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_email',\n\t\t\t'meta_input' => array(\n\t\t\t\t'_llms_email_subject' => 'Engagement Email',\n\t\t\t),\n\t\t) );\n\t\t$course = $this->factory->course->create_and_get( array(\n\t\t\t'sections' => 1,\n\t\t\t'lessons'  => 1,\n\t\t\t'quizzes'  => 0,\n\t\t) );\n\n\t\t// Shouldn't send because of enrollment.\n\t\t$send = $this->main->handle_email( array( $user->ID, $email, $course->get( 'id' ) ) );\n\t\t$this->assertIsWPError( $send );\n\t\t$this->assertWPErrorCodeEquals( 'llms-engagement-check-post--enrollment', $send );\n\t\t$this->assertFalse( $mailer->get_sent() );\n\n\t\tllms_enroll_student( $user->ID, $course->get( 'id' ) );\n\n\t\t// Try from course, section, and lesson.\n\t\t$send_ids = array( $course->get( 'id' ), $course->get_sections( 'ids' )[0], $course->get_lessons( 'ids' )[0] );\n\t\tforeach ( $send_ids as $post_id ) {\n\n\t\t\t// Send the email.\n\t\t\t$this->assertTrue( $this->main->handle_email( array( $user->ID, $email, $post_id ) ) );\n\n\t\t\t// Email sent.\n\t\t\t$sent = $mailer->get_sent();\n\t\t\t$this->assertEquals( $user->user_email, $sent->to[0][0] );\n\t\t\t$this->assertEquals( 'Engagement Email', $sent->subject );\n\n\t\t\t// User meta recorded.\n\t\t\t$this->assertEquals( $email, llms_get_user_postmeta( $user->ID, $post_id, '_email_sent' ) );\n\n\t\t\t// Reset the mailer.\n\t\t\treset_phpmailer_instance();\n\t\t\t$mailer = tests_retrieve_phpmailer_instance();\n\n\t\t\t// Shouldn't send again because of dupcheck.\n\t\t\t$send = $this->main->handle_email( array( $user->ID, $email, $post_id ) );\n\t\t\t$this->assertIsWPError( $send );\n\t\t\t$this->assertWPErrorCodeEquals( 'llms_engagement_email_not_sent_dupcheck', $send );\n\t\t\t$this->assertFalse( $mailer->get_sent() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test handle_email() as triggered by the same related post type with different emails.\n\t *\n\t * @since 4.4.3\n\t * @since 6.0.0 Expect deprecated warning.\n\t *\n\t * @expectedDeprecated LLMS_Engagements::handle_email\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_different_emails_same_trigger() {\n\n\t\t$mailer = tests_retrieve_phpmailer_instance();\n\n\t\t$user  = $this->factory->user->create_and_get();\n\n\t\t$emails = $this->factory->post->create_many(\n\t\t\t2,\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_email',\n\t\t\t\t'meta_input' => array(\n\t\t\t\t\t'_llms_email_subject' => 'Engagement Email',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\t$course = $this->factory->course->create( array(\n\t\t\t'sections' => 0,\n\t\t\t'lessons'  => 0,\n\t\t\t'quizzes'  => 0,\n\t\t) );\n\n\t\tllms_enroll_student( $user->ID, $course );\n\n\t\t// Send the email.\n\t\t$this->assertTrue( $this->main->handle_email( array( $user->ID, $emails[0], $course ) ) );\n\n\t\t// Email sent.\n\t\t$sent = $mailer->get_sent();\n\t\t$this->assertEquals( $user->user_email, $sent->to[0][0] );\n\t\t$this->assertEquals( 'Engagement Email', $sent->subject );\n\n\t\t// User meta recorded.\n\t\t$this->assertEquals( $emails[0], llms_get_user_postmeta( $user->ID, $course, '_email_sent' ) );\n\n\t\t// Reset the mailer.\n\t\treset_phpmailer_instance();\n\t\t$mailer = tests_retrieve_phpmailer_instance();\n\n\t\t// Should send the new mail.\n\t\t$this->assertTrue( $this->main->handle_email( array( $user->ID, $emails[1], $course ) ) );\n\n\t\t// Email sent.\n\t\t$sent = $mailer->get_sent();\n\t\t$this->assertEquals( $user->user_email, $sent->to[0][0] );\n\t\t$this->assertEquals( 'Engagement Email', $sent->subject );\n\n\t\t// User meta recorded.\n\t\t$this->assertEquals( $emails[1], llms_get_user_postmeta( $user->ID, $course, '_email_sent' ) );\n\n\t}\n\n\t/**\n\t * Test handle_email() with no related post (as found during registration)\n\t *\n\t * @since 4.4.1\n\t * @since 6.0.0 Expect deprecated warning.\n\t *\n\t * @expectedDeprecated LLMS_Engagements::handle_email\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_email_with_registration() {\n\n\t\t$mailer = tests_retrieve_phpmailer_instance();\n\n\t\t$user  = $this->factory->user->create_and_get();\n\t\t$email = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_email',\n\t\t\t'meta_input' => array(\n\t\t\t\t'_llms_email_subject' => 'Engagement Email',\n\t\t\t),\n\t\t) );\n\n\t\t$this->assertTrue( $this->main->handle_email( array( $user->ID, $email, '' ) ) );\n\t\t$sent = $mailer->get_sent();\n\t\t$this->assertEquals( $user->user_email, $sent->to[0][0] );\n\t\t$this->assertEquals( 'Engagement Email', $sent->subject );\n\n\t}\n\n\t/**\n\t * Test maybe_trigger_engagement() for the user registration trigger\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_trigger_engagement_user_registration() {\n\n\t\t$this->run_engagement_tests( function( $engagement_type, $expected_action, $delay ) {\n\n\t\t\t$engagement        = $this->create_mock_engagement( 'user_registration', $engagement_type, $delay );\n\t\t\t$engagement_post_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\n\t\t\t$user = $this->factory->user->create();\n\n\t\t\t$this->assertEngagementTriggered(\n\t\t\t\t'lifterlms_user_registered', // Trigger hook.\n\t\t\t\tarray( $user ), // Args passed to trigger hook.\n\t\t\t\t$expected_action,\n\t\t\t\tarray( $user, $engagement_post_id, 'certificate' === $engagement_type ? $engagement_post_id : '', $engagement->ID ), // Expected args passed to the expected action's callback.\n\t\t\t\t$delay\n\t\t\t);\n\n\t\t} );\n\n\t}\n\n\t/**\n\t * Test maybe_trigger_engagement() for the completion hooks (course, section, lesson)\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_trigger_engagement_content_completed() {\n\n\t\tforeach ( array( 'course', 'section', 'lesson', 'quiz' ) as $post_type ) {\n\n\t\t\t$this->run_engagement_tests( function( $engagement_type, $expected_action, $delay ) use ( $post_type ) {\n\n\t\t\t\t$engagement        = $this->create_mock_engagement( $post_type . '_completed', $engagement_type, $delay );\n\t\t\t\t$engagement_post_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\t\t\t\t$related_post_id    = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\n\t\t\t\t$user = $this->factory->user->create();\n\n\t\t\t\t$this->assertEngagementTriggered(\n\t\t\t\t\t'lifterlms_' . $post_type . '_completed', // Trigger hook.\n\t\t\t\t\tarray( $user, $related_post_id ), // Args passed to trigger hook.\n\t\t\t\t\t$expected_action,\n\t\t\t\t\tarray( $user, $engagement_post_id, absint( $related_post_id ), $engagement->ID ), // Expected args passed to the expected action's callback.\n\t\t\t\t\t$delay\n\t\t\t\t);\n\n\t\t\t} );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test maybe_trigger_engagement() for the enrollment hooks\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_trigger_engagement_enrollment() {\n\n\t\t$tests = array(\n\t\t\t'llms_user_enrolled_in_course'        => 'course',\n\t\t\t'llms_user_added_to_membership_level' => 'membership',\n\t\t);\n\n\t\tforeach ( $tests as $trigger_hook => $post_type ) {\n\n\t\t\t$this->run_engagement_tests( function( $engagement_type, $expected_action, $delay ) use ( $trigger_hook, $post_type ) {\n\n\t\t\t\t$engagement        = $this->create_mock_engagement( $post_type . '_enrollment', $engagement_type, $delay );\n\t\t\t\t$engagement_post_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\t\t\t\t$related_post_id    = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\n\t\t\t\t$user = $this->factory->user->create();\n\n\t\t\t\t$this->assertEngagementTriggered(\n\t\t\t\t\t$trigger_hook,\n\t\t\t\t\tarray( $user, $related_post_id ), // Args passed to trigger hook.\n\t\t\t\t\t$expected_action,\n\t\t\t\t\tarray( $user, $engagement_post_id, absint( $related_post_id ), $engagement->ID ), // Expected args passed to the expected action's callback.\n\t\t\t\t\t$delay\n\t\t\t\t);\n\n\t\t\t} );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test maybe_trigger_engagement() for the purchase hooks\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_trigger_engagement_purchase() {\n\n\t\t$tests = array(\n\t\t\t'lifterlms_access_plan_purchased' => 'access_plan',\n\t\t\t'lifterlms_product_purchased'     => 'course',\n\t\t\t'lifterlms_product_purchased'     => 'membership',\n\t\t);\n\n\t\tforeach ( $tests as $trigger_hook => $post_type ) {\n\n\t\t\t$this->run_engagement_tests( function( $engagement_type, $expected_action, $delay ) use ( $trigger_hook, $post_type ) {\n\n\t\t\t\t$engagement        = $this->create_mock_engagement( $post_type . '_purchased', $engagement_type, $delay );\n\t\t\t\t$engagement_post_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\t\t\t\t$related_post_id    = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\n\t\t\t\t$user = $this->factory->user->create();\n\n\t\t\t\t$this->assertEngagementTriggered(\n\t\t\t\t\t$trigger_hook,\n\t\t\t\t\tarray( $user, $related_post_id ), // Args passed to trigger hook.\n\t\t\t\t\t$expected_action,\n\t\t\t\t\tarray( $user, $engagement_post_id, absint( $related_post_id ), $engagement->ID ), // Expected args passed to the expected action's callback.\n\t\t\t\t\t$delay\n\t\t\t\t);\n\n\t\t\t} );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test parse_engagement().\n\t *\n\t * @since 6.6.0\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_parse_engagement() {\n\n\t\t$mock_engagements = $this->instantiate_mock_engagements();\n\t\t$engagements      = llms()->engagements();\n\n\t\t// Set up course, engagement to be triggered, trigger settings, and student.\n\t\t$course_id       = $this->factory->course->create();\n\t\t$engagement_id   = $this->factory->post->create( array( 'post_type' => $mock_engagements->post_type ) );\n\t\t$mock_engagement = $this->create_mock_engagement(\n\t\t\t'course_track_completed',\n\t\t\t'certificate',\n\t\t\t0,\n\t\t\t$course_id,\n\t\t\t$engagement_id\n\t\t);\n\t\t$trigger_id      = $mock_engagement->ID;\n\t\t$student_id      = $this->factory->student->create();\n\n\t\t// Set up parse_engagement() arguments.\n\t\t$engagement                = new stdClass();\n\t\t$engagement->engagement_id = $engagement_id;\n\t\t$engagement->trigger_id    = $trigger_id;\n\t\t$engagement->trigger_event = 'course_completed';\n\t\t$engagement->event_type    = 'email';\n\t\t$engagement->delay         = 0;\n\n\t\t$parse_args = array(\n\t\t\t$engagement,\n\t\t\tarray(\n\t\t\t\t'trigger_type'    => 'course_enrollment',\n\t\t\t\t'user_id'         => $student_id,\n\t\t\t\t'related_post_id' => $course_id,\n\t\t\t)\n\t\t);\n\n\t\t$expected_handler_args = array(\n\t\t\t$student_id,\n\t\t\t$engagement_id,\n\t\t\t$course_id, // Related Post ID.\n\t\t\t$trigger_id,\n\t\t);\n\n\t\t// Test a core email engagement event type.\n\t\t$expected_handler_action = 'lifterlms_engagement_send_email';\n\t\t$handler                 = LLMS_Unit_Test_Util::call_method( $engagements, 'parse_engagement', $parse_args );\n\t\t$this->assertEquals( $expected_handler_action, $handler['handler_action'] );\n\t\t$this->assertEquals( $expected_handler_args, $handler['handler_args'] );\n\n\t\t// Test a core certificate engagement event type.\n\t\t$engagement->event_type  = 'certificate';\n\t\t$expected_handler_action = 'lifterlms_engagement_award_certificate';\n\t\t$handler                 = LLMS_Unit_Test_Util::call_method( $engagements, 'parse_engagement', $parse_args );\n\t\t$this->assertEquals( $expected_handler_action, $handler['handler_action'] );\n\t\t$this->assertEquals( $expected_handler_args, $handler['handler_args'] );\n\n\t\t// Test an unknown engagement event type.\n\t\t$engagement->event_type = 'unknown_action';\n\t\t$handler                = LLMS_Unit_Test_Util::call_method( $engagements, 'parse_engagement', $parse_args );\n\t\t$this->assertNull( $handler['handler_action'] );\n\t\t$this->assertNull( $handler['handler_args'] );\n\n\t\t// Test a non-core engagement event type.\n\t\t$engagement->event_type  = $mock_engagements->event_type;\n\t\t$expected_handler_action = $mock_engagements->handler_action;\n\t\t$handler                 = LLMS_Unit_Test_Util::call_method( $engagements, 'parse_engagement', $parse_args );\n\t\t$this->assertEquals( $expected_handler_action, $handler['handler_action'] );\n\t\t$this->assertEquals( $expected_handler_args, $handler['handler_args'] );\n\t}\n\n\t/**\n\t * Test parse_hook().\n\t *\n\t * @since 6.6.0\n\t *\n\t * @return void\n\t * @throws ReflectionException\n\t */\n\tpublic function test_parse_hook() {\n\n\t\t$mock_engagements = $this->instantiate_mock_engagements();\n\t\t$engagements      = llms()->engagements();\n\n\t\t// Set up course and student.\n\t\t$related_post_id = $this->factory->course->create();\n\t\t$user_id         = $this->factory->student->create();\n\n\t\t// Set up parse_hook() arguments.\n\t\t$parse_args    = array(\n\t\t\t&$action,\n\t\t\tarray(\n\t\t\t\t$user_id,\n\t\t\t\t$related_post_id,\n\t\t\t),\n\t\t);\n\t\t$expected_hook = array(\n\t\t\t'user_id'         => $user_id,\n\t\t\t'trigger_type'    => &$trigger_type,\n\t\t\t'related_post_id' => $related_post_id\n\t\t);\n\n\t\t// Test a core hook.\n\t\t$action       = 'llms_user_enrolled_in_course';\n\t\t$trigger_type = 'course_enrollment';\n\t\t$actual_hook  = LLMS_Unit_Test_Util::call_method( $engagements, 'parse_hook', $parse_args );\n\t\t$this->assertEqualSetsWithIndex( $expected_hook, $actual_hook );\n\n\t\t// Test an unknown action.\n\t\t$action                = 'unknown';\n\t\t$expected_unknown_hook = array(\n\t\t\t'user_id'         => null,\n\t\t\t'trigger_type'    => null,\n\t\t\t'related_post_id' => null\n\t\t);\n\t\t$actual_hook           = LLMS_Unit_Test_Util::call_method( $engagements, 'parse_hook', $parse_args );\n\t\t$this->assertEqualSetsWithIndex( $expected_unknown_hook, $actual_hook );\n\n\t\t// Test a non-core hook.\n\t\t$action       = $mock_engagements->engagement_action; // Input to parse_hook().\n\t\t$trigger_type = $mock_engagements->engagement_action; // Output from parse_hook().\n\t\t$actual_hook  = LLMS_Unit_Test_Util::call_method( $engagements, 'parse_hook', $parse_args );\n\t\t$this->assertEqualSetsWithIndex( $expected_hook, $actual_hook );\n\t}\n\n\t/**\n\t * Test unschedule_delayed_engagements()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_unschedule_delayed_engagements() {\n\n\t\t$unscheduled = did_action( 'action_scheduler_canceled_action' );\n\t\t$post_id     = $this->factory->post->create();\n\n\t\t// Not an engagement.\n\t\t$this->main->unschedule_delayed_engagements( $post_id, get_post( $post_id ) );\n\t\t$this->assertEquals( $unscheduled, did_action( 'action_scheduler_canceled_action' ) );\n\n\t\t// Deleted.\n\t\t$engagement_id = $this->factory->post->create( array(\n\t\t\t'post_type'  => 'llms_engagement',\n\t\t) );\n\n\t\tas_schedule_single_action( time() + HOUR_IN_SECONDS, 'doesntmatter', array( array( 0, 1, 'two' ) ), sprintf( 'llms_engagement_%d', $engagement_id ) );\n\n\t\t// Deleted.\n\t\t$this->main->unschedule_delayed_engagements( $engagement_id, get_post( $engagement_id ) );\n\t\t$this->assertEquals( ++$unscheduled, did_action( 'action_scheduler_canceled_action' ) );\n\n\t}\n\n\t/**\n\t * Runs tests for all engagements types\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param Closure $callback A callback function that will be passed the engagement type, expected action, and delay.\n\t * @return void\n\t */\n\tprivate function run_engagement_tests( $callback ) {\n\n\t\t$tests = array(\n\t\t\t'achievement' => 'lifterlms_engagement_award_achievement',\n\t\t\t'certificate' => 'lifterlms_engagement_award_certificate',\n\t\t\t'email'       => 'lifterlms_engagement_send_email',\n\t\t);\n\n\t\tforeach ( $tests as $engagement_type => $expected_action ) {\n\n\t\t\t$delay = 0;\n\t\t\twhile ( $delay <= 1 ) {\n\t\t\t\t$callback( $engagement_type, $expected_action, $delay );\n\t\t\t\t$delay++;\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Simulates triggering of an engagement and asserts that it ran the expected action\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $trigger_filter  The action hook used to trigger the engagement.\n\t * @param array  $trigger_args    Arguments passed to the hook, eg: lifterlms_access_plan_purchased.\n\t * @param string $expected_action Action expected to be triggered, eg: lifterlms_engagement_award_achievement.\n\t * @param array  $expected_args   Arguments expected to be passed  to the $expected_action callback function.\n\t * @param int    $delay           Delay in days. If `0` the action should be triggered immediately, otherwise the trigger should be scheduled this number of days in the future.\n\t * @return void\n\t */\n\tprivate function assertEngagementTriggered( $trigger_filter, $trigger_args, $expected_action, $expected_args, $delay = 0 ) {\n\n\t\t// Record the number of run actions so we can ensure it was properly incremented.\n\t\t$start_actions = did_action( $expected_action );\n\n\t\t// Mock the `current_filter()` return.\n\t\tglobal $wp_current_filter;\n\t\t$wp_current_filter = array( $trigger_filter );\n\n\t\tif ( ! $delay ) {\n\n\t\t\t// Add an action to assert the expected arguments.\n\t\t\t$callback = function( $args ) use ( $expected_args ) {\n\t\t\t\t$this->assertEquals( $expected_args, $args );\n\t\t\t};\n\t\t\tadd_action( $expected_action, $callback, 15 );\n\n\t\t}\n\n\t\t// Simulate trigger callback.\n\t\t$this->main->maybe_trigger_engagement( ...$trigger_args );\n\n\t\tif ( ! $delay ) {\n\n\t\t\t// Assert the action ran.\n\t\t\t$this->assertEquals( ++$start_actions, did_action( $expected_action ), $expected_action );\n\n\t\t\t// Remove our assertion action.\n\t\t\tremove_action( $expected_action, $callback, 15 );\n\n\t\t} else {\n\n\t\t\t$next = as_next_scheduled_action( $expected_action, array( $expected_args ), sprintf( 'llms_engagement_%d', $expected_args[3] ) );\n\t\t\t$this->assertEqualsWithDelta( time() + ( DAY_IN_SECONDS * $delay ), $next, 5, $expected_action );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-events-core.php",
    "content": "<?php\n/**\n * Test core events\n *\n * @package LifterLMS_Tests/Classes\n *\n * @group events\n * @group events_core\n *\n * @since 3.36.0\n * @version 3.36.0\n */\nclass LLMS_Test_Events_Core extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.36.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->events = new LLMS_Events_Core();\n\t}\n\n\t/**\n\t * Test on_signon() method\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_signon() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t$event = $this->events->on_signon( $user->user_login, $user );\n\n\t\t$this->assertTrue( is_a( $event, 'LLMS_Event' ) );\n\t\t$this->assertEquals( $user->ID, $event->get( 'actor_id' ) );\n\t\t$this->assertEquals( $user->ID, $event->get( 'object_id' ) );\n\n\t\t$this->assertEquals( 'user', $event->get( 'object_type' ) );\n\t\t$this->assertEquals( 'account', $event->get( 'event_type' ) );\n\t\t$this->assertEquals( 'signon', $event->get( 'event_action' ) );\n\n\t}\n\n\t/**\n\t * Test on_signout() method\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Added test on the method returning `false` when no user was logged in.\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_signout() {\n\n\t\t// No user logged, no event created.\n\t\t$this->assertFalse( $this->events->on_signout() );\n\n\t\t$user = $this->factory->user->create();\n\t\twp_set_current_user( $user );\n\n\t\t$event = $this->events->on_signout();\n\n\t\t$this->assertTrue( is_a( $event, 'LLMS_Event' ) );\n\t\t$this->assertEquals( $user, $event->get( 'actor_id' ) );\n\t\t$this->assertEquals( $user, $event->get( 'object_id' ) );\n\n\t\t$this->assertEquals( 'user', $event->get( 'object_type' ) );\n\t\t$this->assertEquals( 'account', $event->get( 'event_type' ) );\n\t\t$this->assertEquals( 'signout', $event->get( 'event_action' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-events-query.php",
    "content": "<?php\n/**\n * Test events query\n *\n * @package LifterLMS/Tests\n *\n * @group events\n * @group query\n * @group dbquery\n *\n * @since 4.7.0\n */\nclass LLMS_Test_Events_Query extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 3.36.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t}\n\n\t/**\n\t * Teardown the test case\n\t *\n\t * @since 4.7.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_events\" );\n\t}\n\n\n\t/**\n\t * Test that the events query, using default args, sets up a count_query\n\t * and does not use SQL_CALC_FOUND_ROWS.\n\t *\n\t * @since 4.7.0\n\t * @since 6.0.0 Don't call deprecated `preprare_query()`.\n\t * @since 10.0.0 Updated: SQL_CALC_FOUND_ROWS replaced with count_query.\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_with_default_args_sets_count_query() {\n\t\t$query = new LLMS_Events_Query();\n\t\t$sql = LLMS_Unit_Test_Util::call_method( $query, 'prepare_query' );\n\t\t$this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $sql );\n\n\t\t$count_query = LLMS_Unit_Test_Util::get_private_property_value( $query, 'count_query' );\n\t\t$this->assertStringStartsWith( 'SELECT COUNT(*)', $count_query );\n\t}\n\n\t/**\n\t * Test found_results and max_pages with real events data.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_found_results_with_pagination() {\n\n\t\t$user_id = $this->factory->user->create();\n\n\t\tfor ( $i = 0; $i < 7; $i++ ) {\n\t\t\t$event = new LLMS_Event();\n\t\t\t$event->setUp(\n\t\t\t\tarray(\n\t\t\t\t\t'actor_id'     => $user_id,\n\t\t\t\t\t'object_type'  => 'post',\n\t\t\t\t\t'object_id'    => 1,\n\t\t\t\t\t'event_type'   => 'page',\n\t\t\t\t\t'event_action' => 'load',\n\t\t\t\t)\n\t\t\t);\n\t\t\t$event->save();\n\t\t}\n\n\t\t$query = new LLMS_Events_Query(\n\t\t\tarray(\n\t\t\t\t'actor'    => $user_id,\n\t\t\t\t'per_page' => 3,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 7, $query->get_found_results() );\n\t\t$this->assertSame( 3, $query->get_max_pages() );\n\t\t$this->assertSame( 3, $query->get_number_results() );\n\t}\n\n\t/**\n\t * Test that no_found_rows skips counting with real data.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_no_found_rows_skips_count() {\n\n\t\t$user_id = $this->factory->user->create();\n\n\t\t$event = new LLMS_Event();\n\t\t$event->setUp(\n\t\t\tarray(\n\t\t\t\t'actor_id'     => $user_id,\n\t\t\t\t'object_type'  => 'post',\n\t\t\t\t'object_id'    => 1,\n\t\t\t\t'event_type'   => 'page',\n\t\t\t\t'event_action' => 'load',\n\t\t\t)\n\t\t);\n\t\t$event->save();\n\n\t\t$query = new LLMS_Events_Query(\n\t\t\tarray(\n\t\t\t\t'actor'         => $user_id,\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertTrue( $query->has_results() );\n\t\t$this->assertSame( 0, $query->get_found_results() );\n\t\t$this->assertSame( 0, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * Test that the events query, passing no_found_rows as true, does not set count_query.\n\t *\n\t * @since 4.7.0\n\t * @since 6.0.0 Don't call deprecated `preprare_query()`.\n\t * @since 10.0.0 Updated: SQL_CALC_FOUND_ROWS replaced with count_query.\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_correctly_doesnt_set_count_query() {\n\t\t$query = new LLMS_Events_Query(\n\t\t\tarray(\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\t\t$sql = LLMS_Unit_Test_Util::call_method( $query, 'prepare_query' );\n\t\t$this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $sql );\n\n\t\t$count_query = LLMS_Unit_Test_Util::get_private_property_value( $query, 'count_query' );\n\t\t$this->assertEmpty( $count_query );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-events.php",
    "content": "<?php\n/**\n * Test events\n *\n * @package LifterLMS/Tests\n *\n * @group events\n *\n * @since 3.36.0\n * @version 4.5.0\n */\nclass LLMS_Test_Events extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.36.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->events = llms()->events();\n\t}\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 3.36.0\n\t * @since 4.5.0 Truncate open sessions table.\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_events\" );\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_events_open_sessions\" );\n\t}\n\n\t/**\n\t * Test missing fields error when recording\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_record_missing_fields() {\n\n\t\t$event = array();\n\n\t\t$ret = $this->events->record( $event );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret );\n\t\t$this->assertEquals( 5, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) );\n\n\t\t$event['actor_id'] = 1;\n\t\t$ret = $this->events->record( $event );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret );\n\t\t$this->assertEquals( 4, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) );\n\n\t\t$event['object_type'] = 'user';\n\t\t$ret = $this->events->record( $event );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret );\n\t\t$this->assertEquals( 3, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) );\n\n\t\t$event['object_id'] = 1;\n\t\t$ret = $this->events->record( $event );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret );\n\t\t$this->assertEquals( 2, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) );\n\n\t\t$event['event_type'] = 'account';\n\t\t$ret = $this->events->record( $event );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms_event_record_missing_field', $ret );\n\t\t$this->assertEquals( 1, count( $ret->get_error_messages( 'llms_event_record_missing_field' ) ) );\n\n\t}\n\n\t/**\n\t * Test recording an invalid event\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_record_invalid_event() {\n\n\t\t$args = array(\n\t\t\t'actor_id' => 1,\n\t\t\t'object_type' => 'user',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'fake',\n\t\t\t'event_action' => 'mock',\n\t\t);\n\t\t$ret = $this->events->record( $args );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms_event_record_invalid_event', $ret );\n\n\t}\n\n\t/**\n\t * Test success recording event\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_record_success() {\n\n\t\t$args = array(\n\t\t\t'actor_id' => 1,\n\t\t\t'object_type' => 'user',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'account',\n\t\t\t'event_action' => 'signon',\n\t\t);\n\t\t$ret = $this->events->record( $args );\n\n\t\t$this->assertTrue( is_a( $ret, 'LLMS_Event' ) );\n\t\tforeach ( $args as $key => $expect ) {\n\t\t\t$this->assertEquals( $expect, $ret->get( $key ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test success recording event with meta\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_record_success_with_metas() {\n\n\t\t$args = array(\n\t\t\t'actor_id' => 1,\n\t\t\t'object_type' => 'user',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'account',\n\t\t\t'event_action' => 'signon',\n\t\t\t'meta' => array(\n\t\t\t\t'meta_key' => 'meta_val',\n\t\t\t),\n\t\t);\n\t\t$ret = $this->events->record( $args );\n\n\t\t$this->assertTrue( is_a( $ret, 'LLMS_Event' ) );\n\t\tforeach ( $args as $key => $expect ) {\n\n\t\t\tif ( 'meta' === $key ) {\n\t\t\t\t$this->assertEquals( $expect, $ret->get_meta() );\n\t\t\t} else {\n\t\t\t\t$this->assertEquals( $expect, $ret->get( $key ) );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test errors when recording many events\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_record_many_with_errors() {\n\n\t\t// All errors.\n\t\t$events = array(\n\t\t\tarray(),\n\t\t\tarray(),\n\t\t);\n\t\t$ret = $this->events->record_many( $events );\n\n\t\t$this->assertIsWPError( $ret );\n\t\t$errors = $ret->get_error_data( 'llms_events_record_many_errors' );\n\t\t$this->assertEquals( 2, count( $errors ) );\n\t\tforeach ( $errors as $stat ) {\n\t\t\t$this->assertIsWPError( $stat );\n\t\t}\n\n\t\t$events = array(\n\t\t\tarray(\n\t\t\t\t'actor_id' => 1,\n\t\t\t\t'object_type' => 'user',\n\t\t\t\t'object_id' => 1,\n\t\t\t\t'event_type' => 'account',\n\t\t\t\t'event_action' => 'signon',\n\t\t\t),\n\t\t\tarray(),\n\t\t);\n\n\t\t// One error with one success.\n\t\t$ret = $this->events->record_many( $events );\n\t\t$this->assertIsWPError( $ret );\n\t\t$errors = $ret->get_error_data( 'llms_events_record_many_errors' );\n\t\t$this->assertEquals( 1, count( $errors ) );\n\t\tforeach ( $errors as $stat ) {\n\t\t\t$this->assertIsWPError( $stat );\n\t\t}\n\n\t\t// Query rolled back.\n\t\tglobal $wpdb;\n\t\t$this->assertEquals( 0, $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_events\" ) );\n\n\t}\n\n\t/**\n\t * Test success recording many events\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_record_many_success() {\n\n\t\t$events = array(\n\t\t\tarray(\n\t\t\t\t'actor_id' => 1,\n\t\t\t\t'object_type' => 'user',\n\t\t\t\t'object_id' => 1,\n\t\t\t\t'event_type' => 'account',\n\t\t\t\t'event_action' => 'signon',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'actor_id' => 1,\n\t\t\t\t'object_type' => 'user',\n\t\t\t\t'object_id' => 1,\n\t\t\t\t'event_type' => 'account',\n\t\t\t\t'event_action' => 'signon',\n\t\t\t),\n\t\t);\n\n\t\t$ret = $this->events->record_many( $events );\n\n\t\tforeach ( $ret as $event ) {\n\t\t\t$this->assertTrue( is_a( $event, 'LLMS_Event' ) );\n\t\t}\n\n\t\t// Query committed.\n\t\tglobal $wpdb;\n\t\t// 3 = the two events created above plus 1 for the session opened.\n\t\t$this->assertEquals( 3, $wpdb->get_var( \"SELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_events\" ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-frontend-assets.php",
    "content": "<?php\n/**\n * LLMS Frontend Assets Tests\n *\n * @package LifterLMS/Tests\n *\n * @group assets\n * @group frontend_assets\n *\n * @since 4.4.0\n * @since 6.0.0 Removed testing of removed items.\n *              - `LLMS_Frontend_Assets::enqueue_inline_script()` method\n *              - `LLMS_Frontend_Assets::is_inline_enqueued()` method\n */\nclass LLMS_Test_Frontend_Assets extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->clear_inline_scripts();\n\n\t}\n\n\t/**\n\t * Retrieves a list of enqueued inline scripts from the LLMS_Assets instance.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_inline_scripts() {\n\t\treturn LLMS_Unit_Test_Util::get_private_property_value( llms()->assets, 'inline' );\n\t}\n\n\t/**\n\t * Clears enqueued inline scripts.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tprivate function clear_inline_scripts() {\n\t\tLLMS_Unit_Test_Util::set_private_property( llms()->assets, 'inline', array() );\t\t\n\t}\n\n\t/**\n\t * Test enqueue_content_protection().\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_content_protection() {\n\n\t\t// Content protection off & user is logged out: no scripts loaded.\n\t\tupdate_option( 'lifterlms_content_protection', 'no' );\n\t\tLLMS_Frontend_Assets::enqueue_content_protection();\n\t\t$this->assertEquals( array(), $this->get_inline_scripts() );\n\n\t\t// Content protection is on and user is logged out: scripts are loaded.\n\t\tupdate_option( 'lifterlms_content_protection', 'yes' );\n\t\tLLMS_Frontend_Assets::enqueue_content_protection();\n\t\t$this->assertArrayHasKey( 'llms-integrity', $this->get_inline_scripts() );\n\n\t\t$this->clear_inline_scripts();\n\n\t\t// Admin can bypass restrictions, script is not loaded.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tLLMS_Frontend_Assets::enqueue_content_protection();\n\t\t$this->assertEquals( array(), $this->get_inline_scripts() );\n\n\t\t$this->clear_inline_scripts();\n\n\t\t// Student can't copy content.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'student' ) ) );\n\t\tLLMS_Frontend_Assets::enqueue_content_protection();\n\t\t$this->assertArrayHasKey( 'llms-integrity', $this->get_inline_scripts() );\n\n\t\t$this->clear_inline_scripts();\n\n\t}\n\n\t/**\n\t * Test enqueue_inline_scripts().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enqueue_inline_scripts() {\n\n\t\t// Any page.\n\t\tLLMS_Unit_Test_Util::call_method( 'LLMS_Frontend_Assets', 'enqueue_inline_scripts' );\n\n\t\t$expected = array(\n\t\t\t'llms-obj'               => 5.0,\n\t\t\t'llms-ajaxurl'           => 10.0,\n\t\t\t'llms-ajax-nonce'        => 10.01,\n\t\t\t'llms-tracking-settings' => 10.02,\n\t\t\t'llms-LLMS-obj'          => 10.03,\n\t\t\t'llms-l10n'              => 10.04,\n\t\t);\n\t\t$this->assertEquals( $expected, wp_list_pluck( $this->get_inline_scripts(), 'priority' ) );\n\n\t\t// On checkout page.\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'checkout' ) );\n\n\t\tLLMS_Unit_Test_Util::call_method( 'LLMS_Frontend_Assets', 'enqueue_inline_scripts' );\n\n\t\t$expected['llms-checkout-urls'] = 10.05;\n\t\t$this->assertEquals( $expected, wp_list_pluck( $this->get_inline_scripts(), 'priority' ) );\n\n\t}\n\n\t/**\n\t * Test get_checkout_urls().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_checkout_urls() {\n\n\t\t// Regular page.\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( 'LLMS_Frontend_Assets', 'get_checkout_urls' ) );\n\n\t\t// Checkout.\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'checkout' ) );\n\t\t$this->assertEquals( \n\t\t\tarray( 'createPendingOrder', 'confirmPendingOrder' ), \n\t\t\tarray_keys( LLMS_Unit_Test_Util::call_method( 'LLMS_Frontend_Assets', 'get_checkout_urls' ) )\n\t\t);\n\n\t\t// Dashboard.\n\t\t$this->go_to( llms_get_endpoint_url( 'orders', 123, llms_get_page_url( 'myaccount' ) ) );\n\t\t$this->assertEquals( \n\t\t\tarray( 'switchPaymentSource' ), \n\t\t\tarray_keys( LLMS_Unit_Test_Util::call_method( 'LLMS_Frontend_Assets', 'get_checkout_urls' ) )\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-functions-access.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Access Functions.\n *\n * @group access\n *\n * @since 3.7.3\n * @since 3.16.0 Unknown.\n * @since 3.37.10 Added tests on sitewide membership restriction.\n * @since 6.5.0 Added tests for drip restrictions on completed lessons.\n */\nclass LLMS_Test_Functions_Access extends LLMS_UnitTestCase {\n\n\t/**\n\t * Get a formatted date for setting time period related restrictions.\n\t *\n\t * @param    string     $offset  adjust day via strtotime\n\t * @param    string     $format  desired returned format, passed to date()\n\t * @return   string\n\t * @since    3.7.3\n\t * @version  3.7.3\n\t */\n\tprivate function get_date( $offset = '+7 days', $format = 'm/d/y' ) {\n\t\treturn date( $format, strtotime( $offset, current_time( 'timestamp' ) ) );\n\t}\n\n\t/**\n\t * Test drip restrictions.\n\t *\n\t * @since 3.16.0\n\t * @since 6.0.0 Replaced use of deprecated items.\n\t *              - `llms_reset_current_time()` with `llms_tests_reset_current_time()` from the `lifterlms-tests` project\n\t *              - `llms_mock_current_time()` with `llms_tests_mock_current_time()` from the `lifterlms-tests` project\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_post_restricted_by_drip_settings() {\n\n\t\t$course_id = $this->generate_mock_courses( 1, 1, 2, 0 )[0];\n\t\t$course = llms_get_post( $course_id );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$second_lesson = $course->get_lessons()[1];\n\t\t$lesson_id = $lesson->get( 'id' );\n\t\t$student = $this->get_mock_student();\n\t\twp_set_current_user( $student->get_id() );\n\t\t$student->enroll( $course_id );\n\n\t\t// no drip settings, lesson is currently available.\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// date in past so the lesson is available.\n\t\t$lesson = llms_get_post( $lesson_id );\n\t\t$lesson->set( 'drip_method', 'date' );\n\t\t$lesson->set( 'date_available', '12/12/2012' );\n\t\t$lesson->set( 'time_available', '12:12 AM' );\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// date in future so lesson not available.\n\t\t$lesson->set( 'date_available', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) );\n\t\t$this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// available 3 days after enrollment.\n\t\t$lesson->set( 'drip_method', 'enrollment' );\n\t\t$lesson->set( 'days_before_available', '3' );\n\t\t$this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// now available.\n\t\tllms_tests_mock_current_time( '+4 days' );\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\tllms_tests_reset_current_time();\n\t\t$lesson->set( 'drip_method', 'start' );\n\t\t$course->set( 'start_date', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) );\n\n\t\t// not available until 3 days after course start date.\n\t\t$this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// now available.\n\t\tllms_tests_mock_current_time( '+4 days' );\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// Test course-level drip settings.\n\n\t\t// Ensure first lesson is not available due to lesson-level drip settings.\n\t\tllms_tests_reset_current_time();\n\t\t$this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// Set individual drip settings on the second lesson and ensure it is available.\n\t\t$second_lesson->set( 'drip_method', 'date' );\n\t\t$second_lesson->set( 'date_available', date( 'm/d/Y', current_time( 'timestamp' ) - DAY_IN_SECONDS ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $second_lesson->get( 'id' ) ) );\n\n\t\t// Now set course-level drip settings and ensure the first lesson is available.\n\t\t$course->set( 'drip_method', 'start' );\n\t\t$course->set( 'lesson_drip', 'yes' );\n\t\t$course->set( 'days_before_available', 10 );\n\t\t$course->set( 'ignore_lessons', 1 );\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\t\t// second not available until 10 days from now.\n\t\t$this->assertEquals( $second_lesson->get( 'id' ), llms_is_post_restricted_by_drip_settings( $second_lesson->get( 'id' ) ) );\n\n\t\t// If lesson drip turned off the rest of the course drip settings should be ignored.\n\t\t$course->set( 'lesson_drip', '' );\n\t\t$this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $second_lesson->get( 'id' ) ) );\n\n\t}\n\n\t/**\n\t * Test drip restriction for already completed lesson.\n\t *\n\t * @since 6.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_post_restricted_by_drip_settings_completed_lesson() {\n\n\t\t$course    = $this->factory->course->create_and_get();\n\t\t$lesson    = $course->get_lessons()[0];\n\t\t$lesson_id = $lesson->get( 'id' );\n\t\t$student   = $this->get_mock_student();\n\t\twp_set_current_user( $student->get( 'id' ) );\n\t\t$student->enroll( $course );\n\n\t\t// No drip settings, lesson is currently available.\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// Add any drip settings, lesson available in the future.\n\t\t$lesson->set( 'drip_method', 'date' );\n\t\t$lesson->set( 'date_available', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) );\n\t\t$this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// Mark the lesson complete.\n\t\t$student->mark_complete( $lesson_id, 'lesson' );\n\t\t// We expect the lesson to be not restricted by drip settings anymore.\n\t\t$this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\n\t\t// Turn off the drip bypass filter: lesson drip.\n\t\tadd_filter( 'llms_lesson_drip_bypass_if_completed', '__return_false', 10 );\n\t\t$this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) );\n\t\tremove_filter( 'llms_lesson_drip_bypass_if_completed', '__return_false', 10 );\n\n\t}\n\n\t/**\n\t * Test restricted by membership.\n\t *\n\t * @since Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_post_restricted_by_membership() {\n\n\t\t$memberships = $this->factory->post->create_many( 2, array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t) );\n\t\t$post_id = $this->factory->post->create();\n\t\t$student = $this->get_mock_student();\n\t\t$uid = $student->get_id();\n\n\n\t\t$this->assertFalse( llms_is_post_restricted_by_membership( $post_id ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_membership( $post_id, $uid ) );\n\n\t\tupdate_post_meta( $post_id, '_llms_restricted_levels', $memberships );\n\t\tupdate_post_meta( $post_id, '_llms_is_restricted', 'yes' );\n\n\t\t$this->assertEquals( $memberships[0], llms_is_post_restricted_by_membership( $post_id ) );\n\t\t$this->assertEquals( $memberships[0], llms_is_post_restricted_by_membership( $post_id, $uid ) );\n\n\t\t$out = llms_is_post_restricted_by_membership( $post_id );\n\t\t$in = llms_is_post_restricted_by_membership( $post_id, $uid );\n\n\t\t$student->enroll( $memberships[1] );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_membership( $post_id, $uid ) );\n\n\t}\n\n\t/**\n\t * Test restriction by membership sitewide.\n\t *\n\t * @since Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_post_restricted_by_sitewide_membership() {\n\n\t\t$memberships = $this->factory->post->create_many( 2, array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t) );\n\t\t$post_id     = $this->factory->post->create();\n\n\t\t// create a page where redirect to.\n\t\t$redirect_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'page'\n\t\t) );\n\n\t\t/**\n\t\t * Create pages that must be always accessible.\n\t\t */\n\n\t\t// create and set privacy policy page.\n\t\tupdate_option( 'wp_page_for_privacy_policy', $this->factory->post->create( array(\n\t\t\t'post_type' => 'page'\n\t\t) ) );\n\n\t\t// create and set terms page.\n\t\tupdate_option( 'lifterlms_terms_page_id', $this->factory->post->create( array(\n\t\t\t'post_type' => 'page'\n\t\t) ) );\n\n\t\t// create and set memberships catalog page.\n\t\tupdate_option( 'lifterlms_memberships_page_id', $this->factory->post->create( array(\n\t\t\t'post_type' => 'page'\n\t\t) ) );\n\n\t\t// create and set myaccount page.\n\t\tupdate_option( 'lifterlms_myaccount_page_id', $this->factory->post->create( array(\n\t\t\t'post_type' => 'page'\n\t\t) ) );\n\n\t\t// create and set checkout page.\n\t\tupdate_option( 'lifterlms_checkout_page_id', $this->factory->post->create( array(\n\t\t\t'post_type' => 'page'\n\t\t) ) );\n\n\t\t// require membership sitewide.\n\t\tupdate_option( 'lifterlms_membership_required', $memberships[1] );\n\n\t\t// set membership's restriction redirection.\n\t\tupdate_post_meta( $memberships[1], '_llms_redirect_page_id', $redirect_id );\n\t\tupdate_post_meta( $memberships[1], '_llms_restriction_redirect_type', 'page' );\n\n\t\t// I expect a post or another membership to be restricted.\n\t\t// While I expect the redirection page and the membership set as requirement to be accessible.\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $post_id ) );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $redirect_id ) );\n\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) );\n\n\t\t// unset the redirection page.\n\t\t// I expect a post, the former redirection page or another membership to be restricted.\n\t\t// While I expect membership set as requirement to be accessible.\n\t\tupdate_post_meta( $memberships[1], '_llms_redirect_page_id', '' );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $post_id ) );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $redirect_id ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) );\n\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) );\n\n\t\t// re-set the redirection page, but set the restriction redirect type as 'custom'.\n\t\t// I expect a post, the former redirection page or another membership to be restricted.\n\t\t// While I expect membership set as requirement to be accessible.\n\t\tupdate_post_meta( $memberships[1], '_llms_redirect_page_id', $redirect_id );\n\t\tupdate_post_meta( $memberships[1], '_llms_restriction_redirect_type', 'custom' );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $post_id ) );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) );\n\t\t$this->assertEquals( $memberships[1], llms_is_post_restricted_by_sitewide_membership( $redirect_id ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) );\n\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) );\n\n\t\t// unset the membership enrollment requirement.\n\t\t// I expect 'everything' to be not restricted.\n\t\tupdate_option( 'lifterlms_membership_required', '' );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $post_id ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[0] ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $memberships[1] ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( $redirect_id ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'lifterlms_terms_page_id' ) ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'memberships' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'myaccount' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( llms_get_page_id( 'checkout' ) ) );\n\t\t$this->assertFalse( llms_is_post_restricted_by_sitewide_membership( absint( get_option( 'wp_page_for_privacy_policy' ) ) ) );\n\t}\n\n\t/**\n\t * Test the llms_is_post_restricted_by_prerequisite() function.\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_post_restricted_by_prerequisite() {\n\n\t\t$courses = $this->generate_mock_courses( 3, 2, 1, 1 );\n\n\t\t$prereq_course_id = $courses[0];\n\n\t\t$course_id = $courses[1];\n\t\t$course = llms_get_post( $course_id );\n\n\t\t$track = wp_insert_term( 'mock track', 'course_track' );\n\t\t$track_id = $track['term_id'];\n\t\t$course_in_track_id = $courses[2];\n\t\twp_set_post_terms( $course_in_track_id, $track_id, 'course_track' );\n\n\t\t$lessons = $course->get_lessons( 'ids' );\n\n\t\t$lesson_2 = llms_get_post( $lessons[1] );\n\t\t$lesson_2->set( 'has_prerequisite', 'yes' );\n\t\t$lesson_2->set( 'prerequisite', $lessons[0] );\n\n\t\t$test_ids = array_merge( $lessons, $course->get_quizzes() );\n\n\t\t$this->prereq_tests( $test_ids, $course, $prereq_course_id, $track_id );\n\n\t\t$student_id = $this->factory->user->create( array( 'role' => 'student' ) );\n\n\t\t// results should all be the same with the student b/c nothing completed.\n\t\t$this->prereq_tests( $test_ids, $course, $prereq_course_id, $track_id, $student_id );\n\n\t\t// results differ once student completes courses.\n\t\t$this->complete_courses_for_student( $student_id, $courses );\n\n\t\t$this->prereq_tests( $test_ids, $course, $prereq_course_id, $track_id, $student_id );\n\n\t}\n\n\t/**\n\t * test_llms_is_post_restricted_by_prerequisite() runs this series of assertions several times.\n\t *\n\t * @since 3.7.3\n\t * @since 3.12.0 Unknown.\n\t * @since 4.9.0 Remove default value of `$test_ids` parameter for php8 compatibility.\n\t *\n\t * @param array $test_ids         Array of post ids to test the llms_is_post_restricted_by_prerequisite() against.\n\t * @param obj   $course           Course object.\n\t * @param int   $prereq_course_id Post id of the prereq course.\n\t * @param int   $track_id         Term id of the prereq track.\n\t * @param int   $user_id          Wp user id of a student.\n\t * @return void\n\t */\n\tprivate function prereq_tests( $test_ids, $course, $prereq_course_id, $track_id, $user_id = null ) {\n\n\t\t$student = $user_id ? new LLMS_Student( $user_id ) : null;\n\n\t\tforeach ( $test_ids as $test_id ) {\n\n\t\t\t$course->set( 'has_prerequisite', 'no' );\n\t\t\t$course->set( 'prerequisite', '' );\n\t\t\t$course->set( 'prerequisite_track', '' );\n\n\t\t\t$post = llms_get_post( $test_id );\n\n\t\t\tif ( 'lesson' === get_post_type( $test_id ) && $post->has_prerequisite() ) {\n\n\t\t\t\t$lesson_prereq_id = $post->get( 'prerequisite' );\n\t\t\t\t$lesson_res = $student && $student->is_complete( $lesson_prereq_id, 'lesson' ) ? false : array(\n\t\t\t\t\t'type' => 'lesson',\n\t\t\t\t\t'id' => $lesson_prereq_id,\n\t\t\t\t);\n\t\t\t\t$this->assertEquals( $lesson_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) );\n\n\t\t\t}\n\n\t\t\t// set a course prereq.\n\t\t\t$course->set( 'has_prerequisite', 'yes' );\n\t\t\t$course->set( 'prerequisite', $prereq_course_id );\n\t\t\t$prereq_course_res = $student && $student->is_complete( $prereq_course_id, 'course' ) ? false : array(\n\t\t\t\t'type' => 'course',\n\t\t\t\t'id' => $prereq_course_id,\n\t\t\t);\n\t\t\t$this->assertEquals( $prereq_course_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) );\n\n\t\t\t// set a track prereq.\n\t\t\t$course->set( 'prerequisite_track', $track_id );\n\n\t\t\t// checks course prereq first and only returns one.\n\t\t\t$this->assertEquals( $prereq_course_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) );\n\n\t\t\t// no course prereq, returns track id.\n\t\t\t$course->set( 'prerequisite', '' );\n\t\t\t$prereq_track_res = $student && $student->is_complete( $track_id, 'course_track' ) ? false : array(\n\t\t\t\t'type' => 'course_track',\n\t\t\t\t'id' => $track_id,\n\t\t\t);\n\t\t\t$this->assertEquals( $prereq_track_res, llms_is_post_restricted_by_prerequisite( $test_id, $user_id ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the llms_is_post_restricted_by_time_period() function.\n\t *\n\t * @since 3.7.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_post_restricted_by_time_period() {\n\n\t\t$courses = $this->generate_mock_courses( 1, 1, 1, 1 );\n\t\t$course_id = $courses[0];\n\t\t$course = llms_get_post( $course_id );\n\n\t\t$test_ids = array_merge( array( $course_id ), $course->get_lessons( 'ids' ), $course->get_quizzes() );\n\n\t\tforeach ( $test_ids as $test_post_id ) {\n\n\t\t\t$course->set( 'time_period', 'no' );\n\n\t\t\t// no time period.\n\t\t\t$this->assertFalse( llms_is_post_restricted_by_time_period( $test_post_id ) );\n\n\t\t\t// enable the restriction.\n\t\t\t$course->set( 'time_period', 'yes' );\n\n\t\t\t// no dates set the course is closed without dates.\n\t\t\t$this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) );\n\n\t\t\t// start date in the future.\n\t\t\t$course->set( 'start_date', $this->get_date( '+7 days' ) );\n\t\t\t$this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) );\n\n\t\t\t// start date in past.\n\t\t\t$course->set( 'start_date', $this->get_date( '-7 days' ) );\n\t\t\t$this->assertFalse( llms_is_post_restricted_by_time_period( $test_post_id ) );\n\n\t\t\t// start date in past and end date in past.\n\t\t\t$course->set( 'end_date', $this->get_date( '-5 days' ) );\n\t\t\t$this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) );\n\n\t\t\t// no start date, end date in past.\n\t\t\t$course->set( 'start_date', '' );\n\t\t\t$this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) );\n\n\t\t\t// no start date end in future.\n\t\t\t$course->set( 'end_date', $this->get_date( '+7 days' ) );\n\t\t\t$this->assertEquals( $course_id, llms_is_post_restricted_by_time_period( $test_post_id ) );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-functions-privacy.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Privacy Functions\n * @group    functions\n * @group    functions_privacy\n * @group    privacy\n * @since    3.19.0\n * @version  3.19.0\n */\nclass LLMS_Test_Functions_Privacy extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_are_terms_and_conditions_required()\n\t * @return   void\n\t * @since    3.3.1\n\t * @version  3.3.1\n\t */\n\tpublic function test_llms_are_terms_and_conditions_required() {\n\n\t\t// terms true & page id numeric\n\t\tupdate_option( 'lifterlms_registration_require_agree_to_terms', 'yes' );\n\t\tupdate_option( 'lifterlms_terms_page_id', '1' );\n\t\t$this->assertTrue( llms_are_terms_and_conditions_required() );\n\n\t\t// terms true & page id non-numeric\n\t\tupdate_option( 'lifterlms_registration_require_agree_to_terms', 'yes' );\n\t\tupdate_option( 'lifterlms_terms_page_id', 'brick' );\n\t\t$this->assertFalse( llms_are_terms_and_conditions_required() );\n\n\t\t// terms true & no page id\n\t\tupdate_option( 'lifterlms_terms_page_id', '' );\n\t\t$this->assertFalse( llms_are_terms_and_conditions_required() );\n\n\t\t// terms true & page id 0\n\t\tupdate_option( 'lifterlms_terms_page_id', '0' );\n\t\t$this->assertFalse( llms_are_terms_and_conditions_required() );\n\n\t\t// terms false and page id good\n\t\tupdate_option( 'lifterlms_registration_require_agree_to_terms', 'no' );\n\t\tupdate_option( 'lifterlms_terms_page_id', '1' );\n\t\t$this->assertFalse( llms_are_terms_and_conditions_required() );\n\n\t\tupdate_option( 'lifterlms_registration_require_agree_to_terms', 'no' );\n\t\tupdate_option( 'lifterlms_terms_page_id', 'brick' );\n\t\t$this->assertFalse( llms_are_terms_and_conditions_required() );\n\n\t}\n\n\tfunction test_llms_get_privacy_notice() {\n\n\t\t$this->assertEquals( 'Your personal data will be used to process your enrollment, support your experience on this website, and for other purposes described in our {{policy}}.', llms_get_privacy_notice() );\n\n\t\tupdate_option( 'llms_privacy_notice', 'The {{policy}} says things' );\n\n\t\t$this->assertEquals( 'The {{policy}} says things', llms_get_privacy_notice() );\n\n\t\t// empty b/c no page set\n\t\t$this->assertEmpty( llms_get_terms_notice( true ) );\n\n\t\t// set a page\n\t\t$page_id = $this->factory->post->create( array(\n\t\t\t'post_title' => 'The Page Title',\n\t\t\t'post_type' => 'page',\n\t\t) );\n\t\tupdate_option( 'wp_page_for_privacy_policy', $page_id );\n\n\t\t// merging works\n\t\t$this->assertEquals( 'The ' . llms_get_option_page_anchor( 'wp_page_for_privacy_policy' ) . ' says things', llms_get_privacy_notice( true ) );\n\n\t\t// empty the option\n\t\tupdate_option( 'llms_privacy_notice', '' );\n\n\t\t// empty b/c there's no option anymore\n\t\t$this->assertEmpty( llms_get_terms_notice( true ) );\n\n\t}\n\n\t/**\n\t * test llms_get_terms_notice()\n\t * @return   void\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tfunction test_llms_get_terms_notice() {\n\n\t\t// default\n\t\t$this->assertEquals( 'I have read and agree to the {{terms}}.', llms_get_terms_notice() );\n\n\t\tupdate_option( 'llms_terms_notice', 'I agree to {{terms}}' );\n\n\t\t$this->assertEquals( 'I agree to {{terms}}', llms_get_terms_notice() );\n\n\t\t// returns empty string when no page set\n\t\t$this->assertEmpty( llms_get_terms_notice( true ) );\n\n\t\t// set the page\n\t\t$page_id = $this->factory->post->create( array(\n\t\t\t'post_title' => 'The Page Title',\n\t\t\t'post_type' => 'page',\n\t\t) );\n\t\tupdate_option( 'lifterlms_terms_page_id', $page_id );\n\n\t\t// test the merged get\n\t\t$this->assertEquals( 'I agree to ' . llms_get_option_page_anchor( 'lifterlms_terms_page_id' ), llms_get_terms_notice( true ) );\n\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-functions-quiz.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Core Functions\n * @group    functions\n * @group    functions_quiz\n * @group    quizzes\n * @group    quiz\n * @since    3.16.0\n * @version  3.16.12\n */\nclass LLMS_Test_Functions_Quiz extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test picture choice columns\n\t * @return   void\n\t * @since    3.16.0\n\t * @version  3.16.0\n\t */\n\tpublic function test_llms_get_picture_choice_question_cols() {\n\n\t\t$combos = array(\n\t\t\t1 => 1,\n\t\t\t2 => 2,\n\t\t\t3 => 3,\n\t\t\t4 => 4,\n\t\t\t5 => 3,\n\t\t\t6 => 3,\n\t\t\t7 => 4,\n\t\t\t8 => 4,\n\t\t\t9 => 3,\n\t\t\t10 => 5,\n\t\t\t11 => 4,\n\t\t\t12 => 4,\n\t\t\t13 => 5,\n\t\t\t14 => 5,\n\t\t\t15 => 5,\n\t\t\t16 => 4,\n\t\t\t17 => 3,\n\t\t\t18 => 3,\n\t\t\t19 => 5,\n\t\t\t20 => 5,\n\t\t\t21 => 3,\n\t\t\t22 => 4,\n\t\t\t23 => 4,\n\t\t\t24 => 4,\n\t\t\t25 => 5,\n\t\t\t26 => 5,\n\t\t\t27 => 5,\n\t\t\t45 => 5,\n\t\t\t999 => 5,\n\t\t\t9999 => 5,\n\t\t);\n\n\t\tforeach ( $combos as $choices => $expected_cols ) {\n\n\t\t\t$this->assertEquals( $expected_cols, llms_get_picture_choice_question_cols( $choices ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_shuffle_choices\n\t * @return   void\n\t * @since    3.16.12\n\t * @version  3.16.12\n\t */\n\tpublic function test_llms_shuffle_choices() {\n\n\t\t// 0 & 1 elements can't really be shuffled...\n\t\t$choices = array();\n\t\t$this->assertEquals( $choices, llms_shuffle_choices( $choices ) );\n\n\t\t$choices = array( 1 );\n\t\t$this->assertEquals( $choices, llms_shuffle_choices( $choices ) );\n\n\t\t// 2 or more items will never match the original after shuffling\n\t\t$i = 2;\n\t\twhile( $i <= 26 ) {\n\n\t\t\t$choices = range( 0, $i );\n\t\t\t$this->assertNotEquals( $choices, llms_shuffle_choices( $choices ) );\n\t\t\t$i++;\n\n\t\t}\n\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-gateway-manual.php",
    "content": "<?php\n/**\n * Test LLMS_Payment_Gateway_Manual class.\n *\n * @group checkout\n * @group gateways\n * @group gateway_manual\n *\n * @since 6.4.0\n */\nclass LLMS_Test_Gateway_Manual extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\n\t}\n\n\t/**\n\t * Test handle_pending_order() for a paid order.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_pending_order() {\n\n\t\t$actions = array(\n\t\t\tdid_action( 'llms_manual_payment_due' ),\n\t\t\tdid_action( 'lifterlms_handle_pending_order_complete' ),\n\t\t);\n\n\t\t$plan  = $this->get_mock_plan();\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$view_link = $order->get_view_link();\n\n\t\ttry {\n\n\t\t\t$this->main->handle_pending_order( $order, $plan, null );\n\n\t\t} catch( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\t$this->assertEquals( \"{$view_link} [302] YES\", $exception->getMessage() );\n\n\t\t\t$this->assertEquals( ++$actions[0], did_action( 'llms_manual_payment_due' ) );\n\t\t\t$this->assertEquals( ++$actions[1], did_action( 'lifterlms_handle_pending_order_complete' ) );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-generator-courses.php",
    "content": "<?php\n/**\n * LLMS_Generator_Courses Tests\n *\n * @package LifterLMS/Tests\n *\n * @group generator\n * @group generator_courses\n *\n * @since 4.7.0\n */\nclass LLMS_Test_Generator_Courses extends LLMS_UnitTestCase {\n\n\t/**\n\t * Load required class\n\t *\n\t * @since 4.7.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/class-llms-generator-courses.php';\n\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.7.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Generator_Courses();\n\n\t}\n\n\t/**\n\t * Get raw data as an array\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tprotected function get_raw( $file = 'import-with-quiz.json' ) {\n\n\t\tglobal $lifterlms_tests;\n\t\treturn json_decode( file_get_contents( $lifterlms_tests->assets_dir . $file ), true );\n\n\t}\n\n\t/**\n\t * Test add_course_terms()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_course_terms() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$raw       = array(\n\t\t\t'categories' => array( 'cat term' ),\n\t\t\t'difficulty' => array( 'difficulty term', '' ),\n\t\t\t'tags' => array( 'tags term', 'another tag' ),\n\t\t\t'tracks' => array( 'tracks term' ),\n\t\t);\n\n\t\t$actions = did_action( 'llms_generator_new_term' );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'add_course_terms', array( $course_id, $raw ) );\n\n\t\t// 5 terms created.\n\t\t$this->assertEquals( 5, did_action( 'llms_generator_new_term' ) );\n\n\t\t// Match.\n\t\t$this->assertEqualSets( $raw['categories'], wp_get_post_terms( $course_id, 'course_cat', array( 'fields' => 'names' ) ) );\n\t\t$this->assertEqualSets( array( $raw['difficulty'][0] ), wp_get_post_terms( $course_id, 'course_difficulty', array( 'fields' => 'names' ) ) );\n\t\t$this->assertEqualSets( $raw['tags'], wp_get_post_terms( $course_id, 'course_tag', array( 'fields' => 'names' ) ) );\n\t\t$this->assertEqualSets( $raw['tracks'], wp_get_post_terms( $course_id, 'course_track', array( 'fields' => 'names' ) ) );\n\n\n\t}\n\n\t/**\n\t * Test clone_course()\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_clone_course() {\n\n\t\t$raw = array(\n\t\t\t'title'   => 'Sample Course',\n\t\t\t'content' => 'Content',\n\t\t);\n\n\t\t$id = $this->main->clone_course( $raw );\n\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t$post = get_post( $id );\n\t\t$this->assertEquals( 'course', $post->post_type );\n\t\t$this->assertEquals( 'Sample Course (Clone)', $post->post_title );\n\t\t$this->assertEquals( 'Content', $post->post_content );\n\t\t$this->assertEquals( 'draft', $post->post_status );\n\n\t}\n\n\t/**\n\t * Test clone_lesson()\n\t *\n\t * @since 4.7.0\n\t * @since 4.13.0 Add check against post status.\n\t *\n\t * @return void\n\t */\n\tpublic function test_clone_lesson() {\n\n\t\t$raw = array(\n\t\t\t'title'   => 'Sample Lesson',\n\t\t\t'content' => 'Content',\n\t\t);\n\n\t\t$id = $this->main->clone_lesson( $raw );\n\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t$post = get_post( $id );\n\t\t$this->assertEquals( 'lesson', $post->post_type );\n\t\t$this->assertEquals( 'Sample Lesson (Clone)', $post->post_title );\n\t\t$this->assertEquals( 'Content', $post->post_content );\n\t\t$this->assertEquals( 'draft', $post->post_status );\n\n\t}\n\n\t/**\n\t * Test generate_course()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_course() {\n\n\t\t$raw = $this->get_raw();\n\t\t$res = $this->main->generate_course( $raw );\n\t\t$this->assertTrue( is_numeric( $res ) );\n\t\t$this->assertEquals( 'course', get_post_type( $res ) );\n\n\t}\n\n\t/**\n\t * Test generate_courses() when with missing raw course data\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_courses_missing_courses() {\n\n\t\t$this->setExpectedException( Exception::class, 'Raw data is missing the required &quot;courses&quot; array.', 2000 );\n\t\t$this->main->generate_courses( array() );\n\n\t}\n\n\t/**\n\t * Test generate_courses() when with invalid raw course data\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_courses_invalid_courses() {\n\n\t\t$this->setExpectedException( Exception::class, 'The raw &quot;courses&quot; item must be an array.', 2001 );\n\t\t$this->main->generate_courses( array( 'courses' => 'invalid' ) );\n\n\t}\n\n\t/**\n\t * Test generate_courses()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_courses() {\n\n\t\t$res = $this->main->generate_courses( array(\n\t\t\t'courses' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'title' => 'Course 0',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'title' => 'Course 1',\n\t\t\t\t),\n\t\t\t),\n\t\t) );\n\n\t\tforeach ( $res as $i => $id ) {\n\t\t\t$this->assertEquals( 'course', get_post_type( $id ) );\n\t\t\t$this->assertEquals( sprintf( 'Course %d', $i ), get_the_title( $id ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test create_access_plan()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_access_plan() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$raw       = array(\n\t\t\t'id'      => 987,\n\t\t\t'author'  => array(\n\t\t\t\t'id' => $this->factory->user->create(),\n\t\t\t),\n\t\t\t'title'             => 'Generated Access Plan',\n\t\t\t'content'           => 'Content',\n\t\t\t'status'            => 'publish',\n\t\t\t'access_expiration' => 'lifetime',\n\t\t\t'availability'      => 'open',\n\t\t\t'enroll_text'       => 'Join',\n\t\t\t'is_free'           => 'yes',\n\t\t);\n\n\t\t$id   = LLMS_Unit_Test_Util::call_method( $this->main, 'create_access_plan', array( $raw, $course_id ) );\n\t\t$plan = llms_get_post( $id );\n\n\t\t$this->assertTrue( $plan instanceof LLMS_Access_Plan );\n\t\t$this->assertEquals( 'llms_access_plan', get_post_type( $id ) );\n\n\t\t$this->assertEquals( $raw['author']['id'], $plan->get( 'author' ) );\n\t\t$this->assertEquals( $raw['title'], $plan->get( 'title' ) );\n\t\t$this->assertEquals( $raw['content'], $plan->get( 'content', true ) );\n\t\t$this->assertEquals( $raw['status'], $plan->get( 'status' ) );\n\t\t$this->assertEquals( $raw['access_expiration'], $plan->get( 'access_expiration' ) );\n\t\t$this->assertEquals( $raw['availability'], $plan->get( 'availability' ) );\n\t\t$this->assertEquals( $raw['enroll_text'], $plan->get( 'enroll_text' ) );\n\t\t$this->assertEquals( $raw['is_free'], $plan->get( 'is_free' ) );\n\t\t$this->assertEquals( $raw['id'], $plan->get( 'generated_from_id' ) );\n\n\t}\n\n\t/**\n\t * Test create_course().\n\t *\n\t * @since 4.7.0\n\t * @since 4.12.0 Only test properties that exist on the raw data arrays.\n\t * @since 7.1.0 Check that properties on the generated course correctly\n\t *               refer to the course id rather than the raw data id.\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_course() {\n\n\t\t$course_actions   = did_action( 'llms_generator_new_course' );\n\t\t$plan_actions     = did_action( 'llms_generator_new_access_plan' );\n\t\t$section_actions  = did_action( 'llms_generator_new_section' );\n\t\t$lesson_actions   = did_action( 'llms_generator_new_lesson' );\n\t\t$quiz_actions     = did_action( 'llms_generator_new_quiz' );\n\t\t$question_actions = did_action( 'llms_generator_new_question' );\n\n\t\t$raw    = $this->get_raw();\n\t\t$id     = LLMS_Unit_Test_Util::call_method( $this->main, 'create_course', array( $raw ) );\n\t\t$course = llms_get_post( $id );\n\n\t\t$this->assertTrue( $course instanceof LLMS_Course );\n\n\t\t// Default post properties.\n\t\t$this->assertEquals( $raw['title'], $course->get( 'title' ) );\n\t\t$this->assertEquals( $raw['content'], $course->get( 'content', true ) );\n\n\t\t// Store the original ID.\n\t\t$this->assertEquals( $raw['id'], $course->get( 'generated_from_id' ) );\n\n\t\t/**\n\t\t * Properties which refer to the new id rather than the\n\t\t * `$generated_from_id`.\n\t\t */\n\t\t$replace_id_props = array(\n\t\t\t'course_closed_message',\n\t\t\t'course_opens_message',\n\t\t\t'enrollment_closed_message',\n\t\t\t'enrollment_opens_message',\n\t\t);\n\n\t\t$find    = '#(.*id=[\"\\'])' . $raw['id'] . '([\"\\'].*)#';\n\t\t$replace = '${1}' . $course->get( 'id' ) . '${2}';\n\n\t\t// Test meta props are set.\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $course, 'properties' ) ) as $prop ) {\n\t\t\tif ( isset( $raw[ $prop ] ) ) {\n\t\t\t\tif ( in_array( $prop, $replace_id_props, true ) ) {\n\t\t\t\t\t$this->assertEquals( preg_replace( $find, $replace, $raw[$prop] ), $course->get( $prop ) );\n\t\t\t\t} else {\n\t\t\t\t\t$this->assertEquals( $raw[ $prop ], $course->get( $prop ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Test custom values.\n\t\tforeach ( $raw['custom'] as $key => $vals ) {\n\t\t\t$this->assertEquals( $vals, get_post_meta( $course->get( 'id' ), $key ) );\n\t\t}\n\n\t\t// Check taxonomies.\n\t\t$this->assertEquals( $raw['difficulty'], $course->get_difficulty() );\n\t\t$this->assertEquals( $raw['categories'], $course->get_categories() );\n\t\t$this->assertEquals( $raw['tags'], $course->get_tags() );\n\t\t$this->assertEquals( $raw['tracks'], $course->get_tracks() );\n\n\t\t// Calls actions (noting that children have been created).\n\t\t$this->assertEquals( ++$course_actions, did_action( 'llms_generator_new_course' ) );\n\t\t$this->assertEquals( ++$plan_actions, did_action( 'llms_generator_new_access_plan' ) );\n\t\t$this->assertEquals( ++$section_actions, did_action( 'llms_generator_new_section' ) );\n\t\t$this->assertEquals( ++$lesson_actions, did_action( 'llms_generator_new_lesson' ) );\n\t\t$this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) );\n\t\t$this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) );\n\n\t\t// Test course structure of generated course is preserved.\n\t\tforeach ( $course->get_sections() as $section ) {\n\n\t\t\t$this->assertEquals( $id, $section->get( 'parent_course' ) );\n\n\t\t\tforeach ( $section->get_lessons() as $lesson ) {\n\n\t\t\t\t$this->assertEquals( $id, $lesson->get( 'parent_course' ) );\n\t\t\t\t$this->assertEquals( $section->get( 'id' ), $lesson->get( 'parent_section' ) );\n\n\t\t\t\t$quiz = $lesson->get_quiz();\n\t\t\t\t$this->assertEquals( $lesson->get( 'id' ), $quiz->get( 'lesson_id' ) );\n\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test create_course() error.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_course_error() {\n\n\t\t// Force post creation to fail.\n\t\t$handler = function( $args ) {\n\t\t\treturn array();\n\t\t};\n\t\tadd_filter( 'llms_new_course', $handler );\n\n\t\t$this->setExpectedException( Exception::class, 'Error creating the course post object.', 1000 );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'create_course', array( array( 'title' => '' ) ) );\n\n\t\tremove_filter( 'llms_new_course', $handler );\n\n\t}\n\n\t/**\n\t * Test create_lesson()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_lesson() {\n\n\t\t$lesson_actions   = did_action( 'llms_generator_new_lesson' );\n\t\t$quiz_actions     = did_action( 'llms_generator_new_quiz' );\n\t\t$question_actions = did_action( 'llms_generator_new_question' );\n\n\t\t$raw     = $this->get_raw()['sections'][0]['lessons'][0];\n\t\t$order   = 3;\n\t\t$course  = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 0 ) );\n\t\t$section = $course->get_sections()[0];\n\t\t$id      = LLMS_Unit_Test_Util::call_method( $this->main, 'create_lesson', array( $raw, $order, $section->get( 'id' ), $course->get( 'id' ) ) );\n\t\t$lesson  = llms_get_post( $id );\n\n\t\t$this->assertTrue( $lesson instanceof LLMS_Lesson );\n\n\t\t// Default post properties.\n\t\t$this->assertEquals( $raw['title'], $lesson->get( 'title' ) );\n\t\t$this->assertEquals( $raw['content'], $lesson->get( 'content', true ) );\n\n\t\t// Test meta props are set.\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $lesson, 'properties' ) ) as $prop ) {\n\t\t\t// This data is not based off raw.\n\t\t\tif ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz', 'content_added_in_builder' ), true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$this->assertEquals( $raw[ $prop ], $lesson->get( $prop ), $prop );\n\t\t}\n\n\t\t// Test custom values.\n\t\tforeach ( $raw['custom'] as $key => $vals ) {\n\t\t\t$this->assertEquals( $vals, get_post_meta( $lesson->get( 'id' ), $key ) );\n\t\t}\n\n\t\t// Order.\n\t\t$this->assertEquals( $order, $lesson->get( 'order' ) );\n\n\t\t// Store the original ID.\n\t\t$this->assertEquals( $raw['id'], $lesson->get( 'generated_from_id' ) );\n\n\t\t// Calls actions (noting that children have been created).\n\t\t$this->assertEquals( ++$lesson_actions, did_action( 'llms_generator_new_lesson' ) );\n\t\t$this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) );\n\t\t$this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) );\n\n\t\t// Relationships.\n\t\t$this->assertEquals( $course->get( 'id' ), $lesson->get( 'parent_course' ) );\n\t\t$this->assertEquals( $section->get( 'id' ), $lesson->get( 'parent_section' ) );\n\n\t\t$quiz = $lesson->get_quiz();\n\t\t$this->assertEquals( $id, $quiz->get( 'lesson_id' ) );\n\n\t}\n\n\tpublic function test_create_quiz() {\n\n\t\t$quiz_actions     = did_action( 'llms_generator_new_quiz' );\n\t\t$question_actions = did_action( 'llms_generator_new_question' );\n\n\t\t$raw              = $this->get_raw()['sections'][0]['lessons'][0]['quiz'];\n\t\t$lesson_id        = $this->factory->post->create( array( 'post_type' => 'lesson' ) );\n\t\t$raw['lesson_id'] = $lesson_id;\n\n\t\t$id   = LLMS_Unit_Test_Util::call_method( $this->main, 'create_quiz', array( $raw ) );\n\t\t$quiz = llms_get_post( $id );\n\n\t\t$this->assertTrue( $quiz instanceof LLMS_Quiz );\n\n\t\t// Default post properties.\n\t\t$this->assertEquals( $raw['title'], $quiz->get( 'title' ) );\n\t\t$this->assertEquals( $raw['content'], $quiz->get( 'content', true ) );\n\n\t\t// Test meta props are set.\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $quiz, 'properties' ) ) as $prop ) {\n\t\t\t// This data is not based off raw.\n\t\t\tif ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz' ), true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$this->assertEquals( $raw[ $prop ], $quiz->get( $prop ), $prop );\n\t\t}\n\n\t\t// Test custom values.\n\t\tforeach ( $raw['custom'] as $key => $vals ) {\n\t\t\t$this->assertEquals( $vals, get_post_meta( $quiz->get( 'id' ), $key ) );\n\t\t}\n\n\t\t// Store the original ID.\n\t\t$this->assertEquals( $raw['id'], $quiz->get( 'generated_from_id' ) );\n\n\t\t// Calls actions (noting that children have been created).\n\t\t$this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) );\n\t\t$this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) );\n\n\t\t// Relationships.\n\t\t$this->assertEquals( $lesson_id, $quiz->get( 'lesson_id' ) );\n\n\t}\n\n\t/**\n\t * Test create_question()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_question() {\n\n\t\t$question_actions = did_action( 'llms_generator_new_question' );\n\n\t\t$raw     = $this->get_raw()['sections'][0]['lessons'][0]['quiz']['questions'][0];\n\t\t$quiz_id = $this->factory->post->create( array( 'post_type' => 'llms_quiz' ) );\n\t\t$quiz    = llms_get_post( $quiz_id );\n\n\t\t$id       = LLMS_Unit_Test_Util::call_method( $this->main, 'create_question', array( $raw, $quiz->questions(), $this->factory->user->create() ) );\n\t\t$question = llms_get_post( $id );\n\n\t\t$this->assertTrue( $question instanceof LLMS_Question );\n\n\t\t// Default post properties.\n\t\t$this->assertEquals( $raw['title'], $question->get( 'title' ) );\n\t\t$this->assertEquals( $raw['content'], $question->get( 'content', true ) );\n\n\t\t// Test meta props are set.\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $question, 'properties' ) ) as $prop ) {\n\t\t\t// This data is not based off raw.\n\t\t\tif ( in_array( $prop, array( 'parent_id' ), true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$this->assertEquals( $raw[ $prop ], $question->get( $prop ), $prop );\n\t\t}\n\n\t\t// Store the original ID.\n\t\t$this->assertEquals( $raw['id'], $question->get( 'generated_from_id' ) );\n\n\t\t// Calls actions (noting that children have been created).\n\t\t$this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) );\n\n\t\t// Relationships.\n\t\t$this->assertEquals( $quiz_id, $question->get( 'parent_id' ) );\n\n\t\t// Check choices.\n\t\tforeach ( $question->get_choices() as $i => $choice ) {\n\n\t\t\t$this->assertEquals( $id, $choice->get_question_id() );\n\n\t\t\t$this->assertEquals( $raw['choices'][ $i ]['choice'], $choice->get_choice() );\n\t\t\t$this->assertEquals( $raw['choices'][ $i ]['choice_type'], $choice->get( 'choice_type' ) );\n\t\t\t$this->assertEquals( $raw['choices'][ $i ]['correct'], $choice->get( 'correct' ) );\n\t\t\t$this->assertEquals( $raw['choices'][ $i ]['marker'], $choice->get( 'marker' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test create_question() during a post creation error\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_question_error() {\n\n\t\t$quiz_id = $this->factory->post->create( array( 'post_type' => 'llms_quiz' ) );\n\t\t$quiz    = llms_get_post( $quiz_id );\n\n\t\t// Force post creation to fail.\n\t\t$handler = function( $args ) {\n\t\t\treturn array();\n\t\t};\n\t\tadd_filter( 'llms_new_question', $handler );\n\n\t\t$this->setExpectedException( Exception::class, 'Error creating the question post object.', 1000 );\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'create_question', array( array( 'title' => '' ), $quiz->questions(), $this->factory->user->create() ) );\n\n\t\tremove_filter( 'llms_new_question', $handler );\n\n\t}\n\n\t/**\n\t * Test create_section()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_section() {\n\n\t\t$section_actions  = did_action( 'llms_generator_new_section' );\n\t\t$lesson_actions   = did_action( 'llms_generator_new_lesson' );\n\t\t$quiz_actions     = did_action( 'llms_generator_new_quiz' );\n\t\t$question_actions = did_action( 'llms_generator_new_question' );\n\n\t\t$raw     = $this->get_raw()['sections'][0];\n\t\t$order   = 20;\n\t\t$course  = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$id      = LLMS_Unit_Test_Util::call_method( $this->main, 'create_section', array( $raw, $order, $course ) );\n\t\t$section = llms_get_post( $id );\n\n\t\t$this->assertTrue( $section instanceof LLMS_Section );\n\n\t\t// Default post properties.\n\t\t$this->assertEquals( $raw['title'], $section->get( 'title' ) );\n\n\t\t// These are the only important pieces of meta data.\n\t\t$this->assertEquals( $course, $section->get( 'parent_course' ) );\n\t\t$this->assertEquals( $order, $section->get( 'order' ) );\n\n\t\t// Store the original ID.\n\t\t$this->assertEquals( $raw['id'], $section->get( 'generated_from_id' ) );\n\n\t\t// Calls actions (noting that children have been created).\n\t\t$this->assertEquals( ++$section_actions, did_action( 'llms_generator_new_section' ) );\n\t\t$this->assertEquals( ++$lesson_actions, did_action( 'llms_generator_new_lesson' ) );\n\t\t$this->assertEquals( ++$quiz_actions, did_action( 'llms_generator_new_quiz' ) );\n\t\t$this->assertEquals( ++$question_actions, did_action( 'llms_generator_new_question' ) );\n\n\t\t// Test course structure of generated course is preserved.\n\t\tforeach ( $section->get_lessons() as $lesson ) {\n\n\t\t\t$this->assertEquals( $course, $lesson->get( 'parent_course' ) );\n\t\t\t$this->assertEquals( $section->get( 'id' ), $lesson->get( 'parent_section' ) );\n\n\t\t\t$quiz = $lesson->get_quiz();\n\t\t\t$this->assertEquals( $lesson->get( 'id' ), $quiz->get( 'lesson_id' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test handle_prerequisites()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_prerequisites() {\n\n\t\t$raw = $this->get_raw( 'import-with-prerequisites.json' );\n\t\t$courses = $this->main->generate_courses( $raw );\n\n\t\t$course = llms_get_post( $courses[0] );\n\n\t\t$this->assertTrue( $course->has_prerequisite( 'course' ) );\n\t\t$this->assertEquals( $courses[1], $course->get_prerequisite_id( 'course' ) );\n\n\t\t// Tracks aren't preserved.\n\t\t$this->assertFalse( $course->has_prerequisite( 'course_track' ) );\n\n\t\t$lessons = $course->get_lessons();\n\t\t$this->assertTrue( $lessons[1]->has_prerequisite() );\n\t\t$this->assertEquals( $lessons[0]->get( 'id' ), $lessons[1]->get_prerequisite() );\n\n\t}\n\n\n\t/**\n\t * Test maybe_sideload_choice_image() for various conditions where the choice can't be sideloaded.\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_sideload_choice_image_disabled() {\n\n\t\t$choice = array(\n\t\t\t'id'     => 'mock',\n\t\t\t'choice' => 'string',\n\t\t);\n\n\t\t// The 'choice_type' prop is missing.\n\t\t$this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) );\n\n\t\t$choice['choice_type'] = 'text';\n\n\t\t// The 'choice_type' prop is not \"image\".\n\t\t$this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) );\n\n\t\t// Sideloading is disabled.\n\t\tadd_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\t\t$this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) );\n\t\tremove_filter( 'llms_generator_is_image_sideloading_enabled', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test maybe_sideload_choice_image()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_sideload_choice_image() {\n\n\t\t$choice = array(\n\t\t\t'id'          => 'mock',\n\t\t\t'choice_type' => 'image',\n\t\t\t'choice'      => array(\n\t\t\t\t'id'  => 123,\n\t\t\t\t'src' => 'https://raw.githubusercontent.com/gocodebox/lifterlms/trunk/tests/assets/christian-fregnan-unsplash.jpg',\n\t\t\t),\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) );\n\n\t\t$this->assertTrue( 123 !== $res['choice']['id'] );\n\t\t$this->assertTrue( $choice['choice']['src'] !== $res['choice']['src'] );\n\t\t$this->assertEquals( wp_get_attachment_url( $res['choice']['id'] ),  $res['choice']['src'] );\n\n\t}\n\n\n\t/**\n\t * Test maybe_sideload_choice_image() when an error is encountered during sideloading\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_sideload_choice_image_error() {\n\n\t\t$choice = array(\n\t\t\t'id'          => 'mock',\n\t\t\t'choice_type' => 'image',\n\t\t\t'choice'      => array(\n\t\t\t\t'id'  => 123,\n\t\t\t\t'src' => 'fake.jpg',\n\t\t\t),\n\t\t);\n\n\t\t$this->assertEquals( $choice, LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_sideload_choice_image', array( $choice, 123 ) ) );\n\n\t}\n\n\t/**\n\t * Test store_temp_id()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_store_temp_id() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$course    = llms_get_post( $course_id );\n\n\t\t$raw = array(\n\t\t\t'id' => 128,\n\t\t);\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'store_temp_id', array( $raw, $course ) );\n\n\t\t$this->assertEquals( 128, $res );\n\t\t$this->assertEquals( 128, $course->get( 'generated_from_id' ) );\n\n\t\t$this->assertEquals( array( 128 => $course_id ), LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'tempids' )['course'] );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-generator.php",
    "content": "<?php\n/**\n * LLMS Generator Tests\n *\n * @group generator\n *\n * @since Unknown\n * @since 3.36.3 Add tests for `is_generator_valid()` and `set_generator()` methods.\n *              Split `is_error()` method tests into multiple tests.\n * @since 3.37.4 Don't test against core metadata.\n * @since 4.7.0 Add tests for image sideloading methods.\n * @since 6.0.0 Removed testing of the removed `LLMS_Generator::get_generated_posts()` method.\n */\nclass LLMS_Test_Generator extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test generate method.\n\t *\n\t * @since Unknown.\n\t * @since 3.37.4 Don't test against core metadata.\n\t * @since 4.7.0 Update to accommodate changes in results data (and test to maintain backwards compat).\n\t * @since 5.0.0 Ignore core custom field data for custom data assertions.\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate() {\n\n\t\t$course = $this->get_mock_course_array( 1, 3, 5, 1, 5 );\n\n\t\t$course['author'] = array(\n\t\t\t'email' => 'test@test.tld',\n\t\t\t'id' => 12345,\n\t\t);\n\t\t$course['categories'] = array( 'cat' );\n\t\t$course['tags'] = array( 'tag1', 'tag2' );\n\t\t$course['tracks'] = array( 'track' );\n\t\t$course['difficulty'] = 'hard';\n\t\t$course['access_plans'] = array(\n\t\t\tarray(\n\t\t\t\t'title' => 'plan1'\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'title' => 'plan2'\n\t\t\t),\n\t\t);\n\n\t\t$course['custom'] = array(\n\t\t\t'customdata' => array( 'yes' ),\n\t\t\t'customdata2' => array( 'no', 'yes', 'maybe' ),\n\t\t\t'customdata3' => array( serialize( array( 'no', 'yes', 'maybe' ) ) ),\n\t\t);\n\n\t\t$gen = new LLMS_Generator( $course );\n\t\t$gen->set_generator( 'LifterLMS/SingleCourseGenerator' );\n\t\t$gen->set_default_post_status( 'publish' );\n\t\t$gen->generate();\n\n\t\t$results = $gen->get_results();\n\n\t\t// Backwards compat keys.\n\t\t$this->assertEquals( 1, $results['authors'] );\n\t\t$this->assertEquals( 1, $results['courses'] );\n\t\t$this->assertEquals( 3, $results['sections'] );\n\t\t$this->assertEquals( 15, $results['lessons'] );\n\t\t$this->assertEquals( 3, $results['quizzes'] );\n\t\t$this->assertEquals( 15, $results['questions'] );\n\t\t$this->assertEquals( 5, $results['terms'] );\n\t\t$this->assertEquals( 2, $results['plans'] );\n\n\t\t// Everything else.\n\t\t$this->assertEquals( 1, $results['user'] );\n\t\t$this->assertEquals( 1, $results['course'] );\n\t\t$this->assertEquals( 3, $results['section'] );\n\t\t$this->assertEquals( 15, $results['lesson'] );\n\t\t$this->assertEquals( 3, $results['quiz'] );\n\t\t$this->assertEquals( 15, $results['question'] );\n\t\t$this->assertEquals( 5, $results['term'] );\n\t\t$this->assertEquals( 2, $results['access_plan'] );\n\t\t$this->assertEquals( 1, $results['user'] );\n\n\t\t// Ensure custom data is properly added\n\t\t$courses = $gen->get_generated_courses();\n\t\t$custom = get_post_custom( $courses[0] );\n\t\tunset( $custom['_llms_instructors'] ); // Ignore core custom data.\n\t\t$this->assertEquals( $course['custom'], $custom );\n\n\t}\n\n\t/**\n\t * Test get_error_code().\n\t *\n\t * @since 4.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_error_code() {\n\n\t\t$gen   = new LLMS_Generator( array() );\n\t\t$class = new LLMS_Generator_Courses();\n\n\t\t$errors = array(\n\n\t\t\t// Native errors.\n\t\t\tE_ERROR         => 'E_ERROR',\n\t\t\tE_COMPILE_ERROR => 'E_COMPILE_ERROR',\n\n\t\t\t// From Courses generator class.\n\t\t\t2000 => 'ERROR_GEN_MISSING_REQUIRED',\n\t\t\t2001 => 'ERROR_GEN_INVALID_FORMAT',\n\n\t\t\t// From posts generator abstract.\n\t\t\t1000 => 'ERROR_CREATE_POST',\n\t\t\t1001 => 'ERROR_CREATE_TERM',\n\t\t\t1002 => 'ERROR_CREATE_USER',\n\t\t\t1100 => 'ERROR_INVALID_POST',\n\n\t\t\t// Undefined error.\n\t\t\t9999 => 'ERROR_UNKNOWN',\n\n\t\t);\n\n\t\tforeach ( $errors as $in => $out ) {\n\t\t\t$this->assertEquals( $out, LLMS_Unit_Test_Util::call_method( $gen, 'get_error_code', array( $in, $class ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_results()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_results() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$expect = array(\n\t\t  'courses'   => 0,\n\t\t  'sections'  => 0,\n\t\t  'lessons'   => 0,\n\t\t  'plans'     => 0,\n\t\t  'quizzes'   => 0,\n\t\t  'questions' => 0,\n\t\t  'terms'     => 0,\n\t\t  'authors'   => 0,\n\t\t);\n\t\t$this->assertEquals( $expect, $gen->get_results() );\n\n\t}\n\n\t/**\n\t * Test get_results() when an error is encountered\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_results_error() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$gen->generate();\n\t\t$res = $gen->get_results();\n\t\t$this->assertIsWPError($res );\n\t\t$this->assertWPErrorCodeEquals( 'missing-generator', $res );\n\n\t}\n\n\t/**\n\t * Test get_generated_content()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_generated_content() {\n\n\t\t$expect = array( 'mock' => array( 1 ) );\n\t\t$gen    = new LLMS_Generator( array() );\n\t\tLLMS_Unit_Test_Util::set_private_property( $gen, 'generated', $expect );\n\n\t\t$this->assertEquals( $expect, $gen->get_generated_content() );\n\n\t}\n\n\t/**\n\t * Test get_generated_courses()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_generated_courses() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\n\t\t// No courses.\n\t\t$this->assertEquals( array(), $gen->get_generated_courses() );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $gen, 'generated', array( 'course' => array( 123 ) ) );\n\t\t$this->assertEquals( array( 123 ), $gen->get_generated_courses() );\n\n\t}\n\n\t/**\n\t * Test is_error() method: no generator supplied.\n\t *\n\t * @since 3.36.3\n\t * @since 4.7.0 Added assertion for error code.\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_error_no_generator() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$gen->generate();\n\t\t$this->assertTrue( $gen->is_error() );\n\t\t$this->assertWPErrorCodeEquals( 'missing-generator', $gen->error );\n\n\t}\n\n\t/**\n\t * Test is_error() method: valid generator but no data to generate.\n\t *\n\t * @since 3.36.3\n\t * @since 4.7.0 Added assertion for error code.\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_error_no_data() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$gen->set_generator( 'LifterLMS/BulkCourseGenerator' );\n\t\t$gen->generate();\n\t\t$this->assertTrue( $gen->is_error() );\n\t\t$this->assertWPErrorCodeEquals( 'ERROR_GEN_MISSING_REQUIRED', $gen->error );\n\n\t}\n\n\t/**\n\t * Test is_error() method: valid generator but data formatted improperly.\n\t *\n\t * @since 3.36.3\n\t * @since 4.7.0 Added assertion for error code.\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_error_invalid_data_format() {\n\n\t\t$gen = new LLMS_Generator( array( 'title' => 'course title' ) );\n\t\t$gen->set_generator( 'LifterLMS/BulkCourseGenerator' );\n\t\t$gen->generate();\n\t\t$this->assertTrue( $gen->is_error() );\n\t\t$this->assertWPErrorCodeEquals( 'ERROR_GEN_MISSING_REQUIRED', $gen->error );\n\n\t}\n\n\t/**\n\t * Test is_error() method: not an error\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_error_not_an_error() {\n\n\t\t$gen = new LLMS_Generator( array( 'title' => 'course title' ) );\n\t\t$gen->set_generator( 'LifterLMS/SingleCourseExporter' );\n\t\t$gen->generate();\n\t\t$this->assertFalse( $gen->is_error() );\n\n\t}\n\n\t/**\n\t * Test is_generator_valid() method: valid generators.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_generator_valid_valid_generators() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$list = array_keys( LLMS_Unit_Test_Util::call_method( $gen, 'get_generators' ) );\n\t\tforeach ( $list as $name ) {\n\t\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'is_generator_valid', array( $name ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test is_generator_valid() method: invalid generators.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_generator_valid_invalid() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $gen, 'is_generator_valid', array( 'fake' ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $gen, 'is_generator_valid', array( 'LifterLMS/SingleFakeExporter' ) ) );\n\n\t}\n\n\t/**\n\t * Test parse_raw() when passing in an array\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_parse_raw_array() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$this->assertEquals( array( 'test' ), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( array( 'test' ) ) ) );\n\n\t}\n\n\t/**\n\t * Test parse_raw() when passing in a JSON string\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_parse_raw_json() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$this->assertEquals( array( 'test' ), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( wp_json_encode( array( 'test' ) ) ) ) );\n\n\t}\n\n\t/**\n\t * Test parse_raw() when passing in an object\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_parse_raw_object() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$obj = new stdClass();\n\t\t$obj->test = 1;\n\t\t$this->assertEquals( array( 'test' => 1 ), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( wp_json_encode( $obj ) ) ) );\n\n\t}\n\n\t/**\n\t * Test parse_raw() when passing in invalid data\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_parse_raw_invalid() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $gen, 'parse_raw', array( 'not json string' ) ) );\n\n\t}\n\n\t/**\n\t * Test set_generator(): interpret from raw missing generator.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_generator_interpret_from_raw_missing() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$err = $gen->set_generator();\n\t\t$this->assertIsWPError( $err );\n\t\t$this->assertWPErrorCodeEquals( 'missing-generator', $err );\n\n\t}\n\n\t/**\n\t * Test set_generator(): interpret from raw invalid generator.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_generator_interpret_from_raw_invalid() {\n\n\t\t$gen = new LLMS_Generator( array(\n\t\t\t'_generator' => 'Fake/Generator',\n\t\t) );\n\t\t$err = $gen->set_generator();\n\t\t$this->assertIsWPError( $err );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-generator', $err );\n\n\t}\n\n\t/**\n\t * Test set_generator(): interpret from raw success.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_generator_interpret_from_raw_success() {\n\n\t\t$gen = new LLMS_Generator( array(\n\t\t\t'_generator' => 'LifterLMS/SingleCourseExporter',\n\t\t) );\n\t\t$this->assertEquals( 'LifterLMS/SingleCourseExporter', $gen->set_generator() );\n\n\t}\n\n\t/**\n\t * Test set_generator(): explicitly supplied invalid.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_generator_explicit_invalid() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$err = $gen->set_generator( 'Fake/Generator' );\n\t\t$this->assertIsWPError( $err );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-generator', $err );\n\n\t}\n\n\t/**\n\t * Test set_generator(): explicitly supplied success.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_generator_explicit_success() {\n\n\t\t$gen = new LLMS_Generator( array() );\n\t\t$this->assertEquals( 'LifterLMS/SingleCourseExporter', $gen->set_generator( 'LifterLMS/SingleCourseExporter' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-grades.php",
    "content": "<?php\n/**\n * Tests for Grading methods\n * @group    grades\n * @since    3.24.0\n */\nclass LLMS_Test_Grades extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test the `instance()` method.\n\t *\n\t * @since 3.24.0\n\t * @since 5.3.0 Rename `_instance` property to `instance`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_instance() {\n\n\t\t$this->assertTrue( is_a( LLMS_Grades::instance(), 'LLMS_Grades' ) );\n\t\t$this->assertClassHasStaticAttribute( 'instance', 'LLMS_Grades' );\n\n\t}\n\n\t/**\n\t * test calculate_grade() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_calculate_grade() {\n\n\t\t$grader = llms()->grades();\n\n\t\t$student = $this->get_mock_student();\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 5, 10 )[0] );\n\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// no grade yet\n\t\t$this->assertNull( $grader->calculate_grade( $course, $student ) );\n\n\t\t$possible_grades = array( 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 );\n\t\t$lesson_points = array();\n\t\t$lesson_grades = array();\n\n\t\tforeach ( $course->get_lessons() as $i => $lesson ) {\n\n\t\t\t// calculate the ongoing grade as quizzes are completed\n\t\t\tif ( 0 !== $i ) {\n\t\t\t\t$total_points = array_sum( $lesson_points );\n\t\t\t\t$course_grade = 0;\n\t\t\t\tforeach ( $lesson_grades as $i => $grade ) {\n\t\t\t\t\tif ( $lesson_points[ $i ] ) {\n\t\t\t\t\t\t$course_grade += $grade * ( $lesson_points[ $i ] / $total_points );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$this->assertEquals( round( $course_grade, 2 ), $grader->calculate_grade( $course, $student ) );\n\t\t\t}\n\n\t\t\t$points = rand( 0, 5 );\n\t\t\t$lesson->set( 'points', $points );\n\t\t\t$lesson_points[] = $points;\n\n\t\t\t// no grade on the lesson yet\n\t\t\t$this->assertNull( $grader->calculate_grade( $lesson, $student ) );\n\n\t\t\t$quiz_id = $lesson->get( 'quiz' );\n\t\t\tif ( ! $quiz_id ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$grade = $possible_grades[ rand( 0, count( $possible_grades ) - 1 ) ];\n\t\t\t$this->take_quiz( $quiz_id, $student->get( 'id' ), $grade );\n\t\t\t$this->assertEquals( $grade, $grader->calculate_grade( $lesson, $student ) );\n\t\t\t$lesson_grades[] = $grade;\n\n\t\t}\n\n\t\t$total_points = array_sum( $lesson_points );\n\t\t$course_grade = 0;\n\t\tforeach ( $lesson_grades as $i => $grade ) {\n\t\t\tif ( $lesson_points[ $i ] ) {\n\t\t\t\t$course_grade += $grade * ( $lesson_points[ $i ] / $total_points );\n\t\t\t}\n\t\t}\n\n\t\t// checkout overall course grade once completed\n\t\t$this->assertEquals( round( $course_grade, 2 ), $grader->calculate_grade( $course, $student ) );\n\n\t}\n\n\t/**\n\t * test get_grade() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_get_grade() {\n\n\t\t$grader = llms()->grades();\n\n\t\t$student = $this->get_mock_student();\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 5, 10 )[0] );\n\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// no grade yet\n\t\t$this->assertNull( $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ) ) );\n\n\t\t$possible_grades = array( 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 );\n\t\t$lesson_points = array();\n\t\t$lesson_grades = array();\n\n\t\tforeach ( $course->get_lessons() as $i => $lesson ) {\n\n\t\t\t// calculate the ongoing grade as quizzes are completed\n\t\t\tif ( 0 !== $i ) {\n\t\t\t\t$total_points = array_sum( $lesson_points );\n\t\t\t\t$course_grade = 0;\n\t\t\t\tforeach ( $lesson_grades as $i => $grade ) {\n\t\t\t\t\tif ( $lesson_points[ $i ] ) {\n\t\t\t\t\t\t$course_grade += $grade * ( $lesson_points[ $i ] / $total_points );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ), false ) );\n\t\t\t\t$this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ) ) );\n\t\t\t}\n\n\t\t\t$points = rand( 0, 5 );\n\t\t\t$lesson->set( 'points', $points );\n\t\t\t$lesson_points[] = $points;\n\n\t\t\t// no grade on the lesson yet\n\t\t\t$this->assertNull( $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ) ) );\n\n\t\t\t$quiz_id = $lesson->get( 'quiz' );\n\t\t\tif ( ! $quiz_id ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$grade = $possible_grades[ rand( 0, count( $possible_grades ) - 1 ) ];\n\t\t\t$this->take_quiz( $quiz_id, $student->get( 'id' ), $grade );\n\t\t\t$this->assertNull( $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ) ) ); // cached\n\t\t\t$this->assertEquals( $grade, $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ), false ) ); // no cache\n\t\t\t$this->assertEquals( $grade, $grader->get_grade( $lesson->get( 'id' ), $student->get( 'id' ) ) ); // cached\n\t\t\t$lesson_grades[] = $grade;\n\n\t\t}\n\n\t\t$total_points = array_sum( $lesson_points );\n\t\t$course_grade = 0;\n\t\tforeach ( $lesson_grades as $i => $grade ) {\n\t\t\tif ( $lesson_points[ $i ] ) {\n\t\t\t\t$course_grade += $grade * ( $lesson_points[ $i ] / $total_points );\n\t\t\t}\n\t\t}\n\n\t\t// checkout overall course grade once completed\n\t\t$this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ), false ) );\n\t\t$this->assertEquals( round( $course_grade, 2 ), $grader->get_grade( $course->get( 'id' ), $student->get( 'id' ) ) );\n\n\n\t}\n\n\t/**\n\t * test round() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_round() {\n\n\t\t$this->assertEquals( 0, llms()->grades()->round( 0 ) );\n\t\t$this->assertEquals( 1.5, llms()->grades()->round( 1.5 ) );\n\t\t$this->assertEquals( 25, llms()->grades()->round( 25 ) );\n\t\t$this->assertEquals( 25.0, llms()->grades()->round( 25.0 ) );\n\t\t$this->assertEquals( 1.67, llms()->grades()->round( 1.666 ) );\n\t\t$this->assertEquals( 251.67, llms()->grades()->round( 251.666 ) );\n\t\t$this->assertEquals( 82.12, llms()->grades()->round( 82.123 ) );\n\t\t$this->assertEquals( 98.13, llms()->grades()->round( 98.125 ) );\n\t\t$this->assertEquals( 75.12, llms()->grades()->round( 75.12 ) );\n\t\t$this->assertEquals( 0.02, llms()->grades()->round( 0.015559 ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-hasher.php",
    "content": "<?php\n/**\n * Tests for LLMS_Hasher\n *\n * @group hasher\n *\n * @version 3.16.10\n */\nclass LLMS_Test_Hasher extends LLMS_UnitTestCase {\n\n\tprivate $ids = array();\n\tprivate $hashes = array();\n\n\tprivate function get_random_id( $max = 99999999 ) {\n\n\t\t$id = rand( 1, $max );\n\t\twhile ( ! in_array( $id, $this->ids ) ) {\n\t\t\tarray_push( $this->ids, $id );\n\t\t\treturn $id;\n\t\t}\n\t\treturn $max + 1;\n\n\t}\n\n\t/**\n\t * Test the hashing/unhashing functions\n\t *\n\t * @since 3.16.10\n\t *\n\t * @return void\n\t */\n\tpublic function test_hash_unhash() {\n\n\t\tforeach ( range( 1, 10000 ) as $i ) {\n\t\t\t$id = $this->get_random_id();\n\t\t\t$hash = LLMS_Hasher::hash( $id );\n\t\t\t$this->assertFalse( in_array( $hash, $this->hashes ) );\n\t\t\t$this->assertEquals( $id, LLMS_Hasher::unhash( $hash ) );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-helper.php",
    "content": "<?php\n/**\n * Test inclusion and initialization of the helper library.\n *\n * @package LifterLMS/Tests\n *\n * @group helper\n * @group packages\n *\n * @since 5.0.0\n * @version 5.0.0\n */\nclass LLMS_Test_Helper extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test helper lib exists and is loaded.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_helper_lib_exists() {\n\t\t$this->assertTrue( class_exists( 'LifterLMS_Helper' ) );\n\t\t$this->assertTrue( defined( 'LLMS_HELPER_VERSION' ) );\n\t\t$this->assertNotNull( LLMS_HELPER_VERSION );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-https.php",
    "content": "<?php\n/**\n * Test LLMS_HTTPS class\n *\n * @package LifterLMS/Tests\n *\n * @group https\n *\n * @since 3.35.1\n * @version 3.35.1\n */\nclass LLMS_Test_HTTPS extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup testcase.\n\t *\n\t * @since 3.35.1\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->https = new LLMS_HTTPS();\n\t\t$this->original_server = $_SERVER;\n\n\t}\n\n\t/**\n\t * Setup testcase.\n\t *\n\t * @since 3.35.1\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\t$_SERVER = $this->original_server;\n\n\t}\n\n\t/**\n\t * Test force url getter\n\t *\n\t * @since 3.35.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_force_redirect_url() {\n\n\t\t// No REQUEST_URI or HTTP_X_FORWARDED_HOST.\n\t\t$this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) );\n\t\t$this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) );\n\n\t\t// No REQUEST_URI.\n\t\t$_SERVER['HTTP_X_FORWARDED_HOST'] = 'example.org';\n\t\t$this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) );\n\t\t$this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) );\n\n\t\t$_SERVER['REQUEST_URI'] = 'http://example.org';\n\t\t$this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) );\n\n\t\t$_SERVER['REQUEST_URI'] = 'https://example.org';\n\t\t$this->assertEquals( 'https://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( true ) ) );\n\n\t\t$_SERVER['REQUEST_URI'] = 'https://example.org';\n\t\t$this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) );\n\n\t\t$_SERVER['REQUEST_URI'] = 'http://example.org';\n\t\t$this->assertEquals( 'http://example.org', LLMS_Unit_Test_Util::call_method( $this->https, 'get_force_redirect_url', array( false ) ) );\n\n\t}\n\n\t/**\n\t * Test force redirect\n\t *\n\t * @since 3.35.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_force_https_redirect() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$_SERVER['HTTPS'] = 1;\n\t\t$this->assertNull( $this->https->force_https_redirect() );\n\n\t\tunset( $_SERVER['HTTPS'] );\n\t\t$url = llms_get_page_url( 'checkout' );\n\t\t$this->go_to( $url );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Exit::class );\n\t\t$this->expectExceptionMessage( sprintf( '%s [301] YES', preg_replace( '|^http://|', 'https://', $url ) ) );\n\n\t\t$this->https->force_https_redirect();\n\n\t}\n\n\t/**\n\t * Test unforce redirect\n\t *\n\t * @since 3.35.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_unforce_https_redirect() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'checkout' ) );\n\n\t\t$home = get_option( 'home' );\n\t\tupdate_option( 'home', preg_replace( '|^https://|', 'http://', $home ) );\n\t\t$this->assertNull( $this->https->unforce_https_redirect() );\n\t\tupdate_option( 'home', $home );\n\n\t\t$_SERVER['HTTPS'] = 1;\n\t\t$this->assertNull( $this->https->unforce_https_redirect() );\n\n\t\t$this->go_to( '/' );\n\t\tunset( $_SERVER['HTTPS'] );\n\t\t$this->assertNull( $this->https->unforce_https_redirect() );\n\n\t\t$_SERVER['HTTPS'] = 1;\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Exit::class );\n\t\t$this->expectExceptionMessage(  'http://example.org/ [301] YES' );\n\n\t\t$this->assertNull( $this->https->unforce_https_redirect() );\n\n\t}\n\n}\n\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-install.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Install Class\n *\n * @package LifterLMS/Tests\n *\n * @group install\n *\n * @since 3.3.1\n * @since 3.37.8 Fix directory path to uninstall.php.\n * @since 4.0.0 Test creation of all tables; fix caching issue when testing full install; add new cron test.\n * @since 4.5.0 Test log backup cron.\n * @since 5.0.0 Added tests for the get_can_install_user_id() method.\n */\nclass LLMS_Test_Install extends LLMS_UnitTestCase {\n\n\t/**\n\t * Tests for check_version()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_check_version() {\n\n\t\t// Ensure the database update runs.\n\t\tupdate_option( 'lifterlms_current_version', (float) llms()->version - 1 );\n\t\tupdate_option( 'lifterlms_db_version', llms()->version );\n\t\tLLMS_Install::check_version();\n\t\t$this->assertTrue( did_action( 'lifterlms_updated' ) === 1 );\n\n\t\t// Ensure that if both are equal the database doesn't run again.\n\t\tupdate_option( 'lifterlms_current_version', llms()->version );\n\t\tupdate_option( 'lifterlms_db_version', llms()->version );\n\t\tLLMS_Install::check_version();\n\t\t$this->assertTrue( did_action( 'lifterlms_updated' ) === 1 );\n\n\t}\n\n\t/**\n\t * Tests for create_cron_jobs()\n\t *\n\t * @since 3.3.1\n\t * @since 3.28.0 Unknown.\n\t * @since 4.0.0 Test session cleanup cron.\n\t * @since 4.5.0 Test log backup cron.\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_cron_jobs() {\n\n\t\t$crons = array(\n\t\t\t'llms_cleanup_tmp',\n\t\t\t'llms_backup_logs',\n\t\t\t'llms_send_tracking_data',\n\t\t\t'llms_delete_expired_session_data',\n\t\t);\n\n\t\t// Clear.\n\t\tforeach ( $crons as $cron ) {\n\t\t\twp_clear_scheduled_hook( $cron );\n\t\t\t$this->assertFalse( wp_next_scheduled( $cron ) );\n\t\t}\n\n\t\tLLMS_Install::create_cron_jobs();\n\n\t\t// Scheduled.\n\t\tforeach ( $crons as $cron ) {\n\t\t\t$this->assertTrue( is_numeric( wp_next_scheduled( $cron ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Tests for create_difficulties() & remove_difficulties()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_difficulties_crud() {\n\n\t\t// Terms may or may not exist and should exist after creation.\n\t\tLLMS_Install::create_difficulties();\n\t\tforeach( LLMS_Install::get_difficulties() as $name ) {\n\t\t\t$this->assertInstanceOf( 'WP_Term', get_term_by( 'name', $name, 'course_difficulty' ) );\n\t\t}\n\n\t\t// Terms should not exist after deleting terms.\n\t\tLLMS_Install::remove_difficulties();\n\t\tforeach( LLMS_Install::get_difficulties() as $name ) {\n\t\t\t$this->assertFalse( get_term_by( 'name', $name, 'course_difficulty' ) );\n\t\t}\n\n\t\t// Terms should exist after creating difficulties.\n\t\tLLMS_Install::create_difficulties();\n\t\tforeach( LLMS_Install::get_difficulties() as $name ) {\n\t\t\t$this->assertInstanceOf( 'WP_Term', get_term_by( 'name', $name, 'course_difficulty' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test create_files()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_files() {\n\n\t\tLLMS_Install::create_files();\n\t\t$this->assertTrue( file_exists( LLMS_LOG_DIR ) );\n\t\t$this->assertTrue( file_exists( LLMS_LOG_DIR . '.htaccess' ) );\n\t\t$this->assertTrue( file_exists( LLMS_LOG_DIR . 'index.html' ) );\n\t\t$this->assertFalse( file_exists( LLMS_LOG_DIR . 'fail.txt' ) );\n\n\t}\n\n\t/**\n\t * Tests for create_options()\n\t *\n\t * @since 3.3.1\n\t * @since 3.5.1 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_options() {\n\n\t\t// Clear options.\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"DELETE FROM $wpdb->options WHERE option_name LIKE 'lifterlms\\_%';\" );\n\n\t\t// Install options.\n\t\tLLMS_Install::create_options();\n\n\t\t// Check they exist.\n\t\t$settings = LLMS_Admin_Settings::get_settings_tabs();\n\n\t\tforeach ( $settings as $section ) {\n\t\t\t// Skip general settings since this screen doesn't actually have any settings on it.\n\t\t\tif ( 'general' === $section->id ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tforeach ( $section->get_settings() as $value ) {\n\t\t\t\tif ( isset( $value['default'] ) && isset( $value['id'] ) ) {\n\t\t\t\t\t$this->assertEquals( $value['default'], get_option( $value['id'] ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Tests for create_pages()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pages() {\n\n\t\t// Clear options.\n\t\tdelete_option( 'lifterlms_shop_page_id' );\n\t\tdelete_option( 'lifterlms_memberships_page_id' );\n\t\tdelete_option( 'lifterlms_checkout_page_id' );\n\t\tdelete_option( 'lifterlms_myaccount_page_id' );\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_shop_page_id' ) );\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_memberships_page_id' ) );\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_checkout_page_id' ) );\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_myaccount_page_id' ) );\n\n\t\t// Delete pages.\n\t\twp_delete_post( get_option( 'lifterlms_shop_page_id' ), true );\n\t\twp_delete_post( get_option( 'lifterlms_memberships_page_id' ), true );\n\t\twp_delete_post( get_option( 'lifterlms_checkout_page_id' ), true );\n\t\twp_delete_post( get_option( 'lifterlms_myaccount_page_id' ), true );\n\n\t\t// Clear options.\n\t\tdelete_option( 'lifterlms_shop_page_id' );\n\t\tdelete_option( 'lifterlms_memberships_page_id' );\n\t\tdelete_option( 'lifterlms_checkout_page_id' );\n\t\tdelete_option( 'lifterlms_myaccount_page_id' );\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_shop_page_id' ) );\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_memberships_page_id' ) );\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_checkout_page_id' ) );\n\t\t$this->assertGreaterThan( 0, get_option( 'lifterlms_myaccount_page_id' ) );\n\n\t}\n\n\t/**\n\t * Tests for create_tables()\n\t *\n\t * @since 3.3.1\n\t * @since 4.0.0 Add missing tables.\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_tables() {\n\n\t\tglobal $wpdb;\n\n\t\t$tables = array(\n\t\t\t\"{$wpdb->prefix}lifterlms_user_postmeta\",\n\t\t\t\"{$wpdb->prefix}lifterlms_quiz_attempts\",\n\t\t\t\"{$wpdb->prefix}lifterlms_product_to_voucher\",\n\t\t\t\"{$wpdb->prefix}lifterlms_voucher_code_redemptions\",\n\t\t\t\"{$wpdb->prefix}lifterlms_vouchers_codes\",\n\t\t\t\"{$wpdb->prefix}lifterlms_notifications\",\n\t\t\t\"{$wpdb->prefix}lifterlms_events\",\n\t\t\t\"{$wpdb->prefix}lifterlms_sessions\",\n\t\t);\n\n\t\t// Clear tables.\n\t\t// $list = implode( ', ', $tables );\n\t\t// $wpdb->query( \"DROP TABLE IF EXISTS $list;\" );\n\n\t\t// Install tables.\n\t\tLLMS_Install::create_tables();\n\n\t\tforeach ( $tables as $table ) {\n\t\t\t$this->assertEquals( $table, $wpdb->get_var( \"SHOW TABLES LIKE '{$table}'\" ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test create_visibilities()\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_visibilities() {\n\n\t\t// Terms may or may not exist and should exist after creation.\n\t\tLLMS_Install::create_visibilities();\n\t\tforeach( array_keys( llms_get_product_visibility_options() ) as $name ) {\n\t\t\t$this->assertInstanceOf( 'WP_Term', get_term_by( 'name', $name, 'llms_product_visibility' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_difficulties()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_difficulties() {\n\n\t\t$this->assertTrue( ! empty( LLMS_Install::get_difficulties() ) );\n\t\t$this->assertTrue( is_array( LLMS_Install::get_difficulties() ) );\n\n\t}\n\n\t/**\n\t * Test update_db_version()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_db_version() {\n\n\t\tLLMS_Install::update_db_version( '1' );\n\t\t$this->assertEquals( '1', get_option( 'lifterlms_db_version' ) );\n\n\t\tLLMS_Install::update_db_version();\n\t\t$this->assertEquals( llms()->version, get_option( 'lifterlms_db_version' ) );\n\n\t\tLLMS_Install::update_db_version( '1.2.3' );\n\t\t$this->assertEquals( '1.2.3', get_option( 'lifterlms_db_version' ) );\n\n\t}\n\n\t/**\n\t * Test update_llms_version()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_llms_version() {\n\n\t\tLLMS_Install::update_llms_version( '1' );\n\t\t$this->assertEquals( '1', get_option( 'lifterlms_current_version' ) );\n\n\t\tLLMS_Install::update_llms_version();\n\t\t$this->assertEquals( llms()->version, get_option( 'lifterlms_current_version' ) );\n\n\t\tLLMS_Install::update_llms_version( '1.2.3' );\n\t\t$this->assertEquals( '1.2.3', get_option( 'lifterlms_current_version' ) );\n\n\t}\n\n\t/**\n\t * Tests for install() function\n\t *\n\t * @since 3.3.1\n\t * @since 3.37.8 Fix directory path to uninstall.php\n\t * @since 4.0.0 Flush cache after uninstall is run.\n\t *\n\t * @return void\n\t */\n\tpublic function test_install() {\n\n\t\t// Clean existing install first.\n\t\tif ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {\n\t\t\tdefine( 'WP_UNINSTALL_PLUGIN', true );\n\t\t\tdefine( 'LLMS_REMOVE_ALL_DATA', true );\n\t\t}\n\n\t\tinclude( dirname( __FILE__, 4 ) . '/uninstall.php' );\n\n\t\twp_cache_flush();\n\n\t\tLLMS_Install::install();\n\t\t$this->assertEquals( llms()->version, get_option( 'lifterlms_current_version' ) );\n\n\t}\n\n\t/**\n\t * Test get_can_install_user_id() method\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_can_install_user_id() {\n\n\t\t// Clean user* tables.\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE $wpdb->users\" );\n\t\t$wpdb->query( \"TRUNCATE TABLE $wpdb->usermeta\" );\n\n\t\t// No users, expect 0.\n\t\t$this->assertEquals( 0, LLMS_Install::get_can_install_user_id() );\n\n\t\t// Create a subscriber.\n\t\t$subscriber = $this->factory->user->create( array( 'role' => 'subscriber' ) );\n\n\t\t// No admin users, expect 0.\n\t\t$this->assertEquals( 0, LLMS_Install::get_can_install_user_id() );\n\n\t\t// Create two admins.\n\t\t$admins = $this->factory->user->create_many( 2, array( 'role' => 'administrator' ) );\n\n\t\t// Expect the first admin to be returned.\n\t\t$this->assertEquals( $admins[0], LLMS_Install::get_can_install_user_id() );\n\n\t\t// Log in as subscriber.\n\t\twp_set_current_user( $subscriber );\n\n\t\t// Expect the first admin to be returned.\n\t\t$this->assertEquals( $admins[0], LLMS_Install::get_can_install_user_id() );\n\n\t\t// Log in as first admin.\n\t\twp_set_current_user( $admins[0] );\n\n\t\t// Expect the first admin to be returned.\n\t\t$this->assertEquals( $admins[0], LLMS_Install::get_can_install_user_id() );\n\n\t\t// Log in as second admin.\n\t\twp_set_current_user( $admins[1] );\n\n\t\t// Expect the second admin to be returned.\n\t\t$this->assertEquals( $admins[1], LLMS_Install::get_can_install_user_id() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-integrations.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Integrations class\n * @group    integrations\n * @since    3.19.0\n * @version  3.19.0\n */\nclass LLMS_Test_Integrations extends LLMS_UnitTestCase {\n\n\t/**\n\t * test instance() method\n\t * @return   void\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tpublic function test_instance() {\n\n\t\t$this->assertTrue( is_a( llms()->integrations(), 'LLMS_Integrations' ) );\n\n\t}\n\n\t// public function test_get_integration() {}\n\n\t/**\n\t * test init() method\n\t * @return   void\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tpublic function test_init() {\n\n\t\t$instance = llms()->integrations();\n\t\t$this->assertEquals( 2, count( $instance->integrations() ) );\n\n\t}\n\n\t/**\n\t * Test get available integrations\n\t * @return   void\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tpublic function test_get_available_integrations() {\n\n\t\t$instance = llms()->integrations();\n\t\t$this->assertEquals( array(), $instance->get_available_integrations() );\n\n\t\t// enable an integration\n\t\tupdate_option( 'llms_integration_bbpress_enabled', 'yes' );\n\n\t\t// option is enabled but it's not available b/c deps. don't exist\n\t\t$this->assertEquals( 0, count( $instance->get_available_integrations() ) );\n\n\t\t// deps exist now\n\t\t$stub = $this->getMockBuilder( 'bbPress' )->getMock();\n\t\t$this->assertEquals( 1, count( $instance->get_available_integrations() ) );\n\n\t}\n\n\t/**\n\t * Test integrations() method\n\t * @return   [type]\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tpublic function test_integrations() {\n\n\t\t$instance = llms()->integrations();\n\t\t$this->assertEquals( 2, count( $instance->integrations() ) );\n\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-llms-dom-document.php",
    "content": "<?php\n/**\n * Test LLMS_DOM_Document\n *\n * @package LifterLMS/Tests\n *\n * @group llms_dom_document\n *\n * @since 4.13.0\n */\nclass LLMS_Test_LLMS_DOM_Document extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test DOMDocument library missing\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dom_document_missing_error() {\n\t\t$llms_dom = new LLMS_DOM_Document( 'some string to load' );\n\n\t\t// Simulate that the DOMDocument library is not available.\n\t\tLLMS_Unit_Test_Util::set_private_property(\n\t\t\t$llms_dom,\n\t\t\t'error',\n\t\t\tnew WP_Error( 'llms-dom-document-missing', __( 'DOMDocument not available.', 'lifterlms' ) )\n\t\t);\n\t\t$load = $llms_dom->load();\n\n\t\t$this->assertWPError( $load );\n\t\t$this->assertWPErrorCodeEquals( 'llms-dom-document-missing', $load );\n\t}\n\n\t/**\n\t * Test loading string success\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_loading_success() {\n\n\t\t$llms_dom = new LLMS_DOM_Document( 'some string to load' );\n\t\t$this->assertTrue( $llms_dom->load() );\n\t}\n\n\t/**\n\t * Test loading method switch\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_loading_method_switch() {\n\n\t\t// Check that by default the loading method is 'load_with_mb_convert_encoding'.\n\t\t$llms_dom = new LLMS_DOM_Document( 'some string to load' );\n\n\t\t$load_method = LLMS_Unit_Test_Util::get_private_property_value(\n\t\t\t$llms_dom,\n\t\t\t'load_method'\n\t\t);\n\n\t\t$this->assertEquals( 'load_with_mb_convert_encoding', $load_method );\n\n\t\t// Force using utf fixer.\n\t\tadd_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false', 999 );\n\n\t\t$llms_dom = new LLMS_DOM_Document( 'some other string to load' );\n\n\t\t$load_method = LLMS_Unit_Test_Util::get_private_property_value(\n\t\t\t$llms_dom,\n\t\t\t'load_method'\n\t\t);\n\n\t\tremove_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false', 999 );\n\n\t\t$this->assertEquals( 'load_with_meta_utf_fixer', $load_method );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-main-class.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Main Class\n *\n * @package LifterLMS/Tests\n *\n * @group main_class\n *\n * @since 3.3.1\n * @since 3.21.1 Add localization tests.\n * @since 4.0.0 Add tests for `init_session()` method.\n *               Remove tests against removed LLMS_SVG_DIR constant.\n * @since 4.4.0 Add tests for `init_assets()` method.\n */\nclass LLMS_Test_Main_Class extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup function\n\t *\n\t * @since 3.3.1\n\t * @since 5.3.3 Use `llms()` in favor of `LLMS()` and renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->llms = llms();\n\t}\n\n\t/**\n\t * Test the `instance` property.\n\t *\n\t * @since 3.3.1\n\t * @since 5.3.0 Rename `_instance` property to `instance`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_instance() {\n\n\t\t$this->assertClassHasStaticAttribute( 'instance', 'LifterLMS' );\n\n\t}\n\n\t/**\n\t * Test class constants\n\t *\n\t * @since 3.3.1\n\t * @since 4.0.0 Remove tests against removed LLMS_SVG_DIR constant.\n\t *\n\t * @return void\n\t */\n\tpublic function test_constants() {\n\n\t\t$this->assertEquals( $this->llms->version, LLMS_VERSION );\n\t\t$this->assertNotEquals( LLMS_LOG_DIR, '' );\n\t\t$this->assertNotEquals( LLMS_PLUGIN_DIR, '' );\n\t\t$this->assertNotEquals( LLMS_PLUGIN_FILE, '' );\n\t\t$this->assertNotEquals( LLMS_TEMPLATE_PATH, '' );\n\n\t}\n\n\t/**\n\t * Test main instances\n\t *\n\t * @since 3.3.1\n\t * @since 5.8.0 Added tests for additional instances.\n\t *\n\t * @return void\n\t */\n\tpublic function test_instances() {\n\n\t\t$tests = array(\n\t\t\tarray( 'LLMS_Achievements', 'achievements' ),\n\t\t\tarray( 'LLMS_Block_Templates', 'block_templates' ),\n\t\t\tarray( 'LLMS_Certificates', 'certificates' ),\n\t\t\tarray( 'LLMS_Engagements', 'engagements' ),\n\t\t\tarray( 'LLMS_Events', 'events' ),\n\t\t\tarray( 'LLMS_Grades', 'grades' ),\n\t\t\tarray( 'LLMS_Integrations', 'integrations' ),\n\t\t\tarray( 'LLMS_Emails', 'mailer' ),\n\t\t\tarray( 'LLMS_Notifications', 'notifications' ),\n\t\t\tarray( 'LLMS_Payment_Gateways', 'payment_gateways' ),\n\t\t\tarray( 'LLMS_Processors', 'processors' ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\tlist( $expected_class, $func ) = $test;\n\t\t\t$this->assertInstanceOf( $expected_class, $this->llms->$func() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the init_assets() method.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_assets() {\n\n\t\t$assets = LLMS_Unit_Test_Util::call_method( llms(), 'init_assets' );\n\n\t\t$this->assertEquals( $assets, llms()->assets );\n\n\t\t$this->assertEquals( require LLMS_PLUGIN_DIR . 'includes/assets/llms-assets-scripts.php', LLMS_Unit_Test_Util::get_private_property_value( $assets, 'scripts' ) );\n\t\t$this->assertEquals( require LLMS_PLUGIN_DIR . 'includes/assets/llms-assets-styles.php', LLMS_Unit_Test_Util::get_private_property_value( $assets, 'styles' ) );\n\n\t}\n\n\t/**\n\t * Test the init_session() method\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_session() {\n\n\t\t// Clear the session.\n\t\tllms()->session = null;\n\n\t\t// Initializes a new session.\n\t\t$session = llms()->init_session();\n\t\t$this->assertTrue( is_a( $session, 'LLMS_Session' ) );\n\t\t$session->set( 'test', 'mock' );\n\n\t\t// Call it again, should respond with the same session as before.\n\t\t$this->assertEquals( $session->get_id(), llms()->init_session()->get_id() );\n\t\t$this->assertEquals( 'mock', llms()->init_session()->get( 'test' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-mime-type-extractor.php",
    "content": "<?php\n/**\n * Test LLMS_Mime_Type_Extractor\n *\n * @package LifterLMS/Tests\n *\n * @group mime_type_extractor\n *\n * @since 3.38.1\n * @version 3.38.1\n */\nclass LLMS_Test_Mime_Type_Extractor extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test files.\n\t *\n\t * @var array\n\t */\n\tprotected $files = array(\n\t\t'po'  => 'lifterlms-en_US.po',\n\t\t'jpg' => 'christian-fregnan-unsplash.jpg',\n\t);\n\n\t/**\n\t * Test from_file_path() for a file with a mime-type that exists\n\t *\n\t * @since 3.38.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_mime_type_in_list() {\n\n\t\tglobal $lifterlms_tests;\n\t\t$this->assertEquals(\n\t\t\t'image/jpeg',\n\t\t\tLLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . $this->files['jpg'] )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test from_file_path() for a mime-type not found in our list\n\t *\n\t * @since 3.38.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_mime_type_not_in_list() {\n\n\t\tglobal $lifterlms_tests;\n\n\t\t// I expect po files to be recognized by one of the fallback functions as 'text/plain' or 'text/x-po'.\n\t\tif ( function_exists( 'finfo_file' ) || function_exists( 'mime_content_type' ) ) {\n\t\t\t$this->assertContains(\n\t\t\t\tLLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . $this->files['po'] ),\n\t\t\t\tarray( \n\t\t\t\t\t'text/plain', \n\t\t\t\t\t'text/x-po' \n\t\t\t\t)\n\t\t\t);\n\t\t} else {\n\t\t\t$this->assertEquals(\n\t\t\t\tLLMS_Mime_Type_Extractor::DEFAULT_MIME_TYPE,\n\t\t\t\tLLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . $this->files['po'] )\n\t\t\t);\n\t\t}\n\n\n\t}\n\n\t/**\n\t * Test from_file_path() for a file that does not exist\n\t *\n\t * @since 3.38.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_mime_type_not_existent() {\n\n\t\tglobal $lifterlms_tests;\n\t\t$this->assertEquals(\n\t\t\tLLMS_Mime_Type_Extractor::DEFAULT_MIME_TYPE,\n\t\t\tLLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir . 'SomeoneJoinsSomethingTheyDoNotBelongTo.jpg' )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test from_file_path() when checking a directory\n\t *\n\t * @since 3.38.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_mime_type_of_a_dir() {\n\n\t\tglobal $lifterlms_tests;\n\t\t$this->assertEquals(\n\t\t\tLLMS_Mime_Type_Extractor::DEFAULT_MIME_TYPE,\n\t\t\tLLMS_Mime_Type_Extractor::from_file_path( $lifterlms_tests->assets_dir )\n\t\t);\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-order-generator.php",
    "content": "<?php\n/**\n * Test LLMS_Order_Generator\n *\n * @package LifterLMS/Tests\n *\n * @group orders\n * @group order_generator\n *\n * @since 7.0.0\n * @version 7.0.0\n */\nclass LLMS_Test_Order_Generator extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test confirm() when validation errors are encountered.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_validation_errors() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$res = $gen->confirm();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_ORDER_NOT_FOUND, $res );\n\n\t}\n\n\t/**\n\t * Test confirm() when the gateway encounters an error.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_gateway_errors() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-confirm-err';\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t\tpublic function confirm_pending_order( $order ) {\n\t\t\t\treturn new WP_Error( 'gateway-err', 'Message' );\n\t\t\t}\n\t\t};\n\n\t\t$gateway->supports['recurring_payments'] = true;\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order->set( 'payment_gateway', 'fake-confirm-err' );\n\n\t\t$data                         = $this->get_mock_checkout_data_array();\n\t\t$data['llms_order_key']       = $order->get( 'order_key' );\n\t\t$data['llms_payment_gateway'] = 'fake-confirm-err';\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\tLLMS_Unit_Test_Util::set_private_property( $gen, 'gateway', $gateway );\n\n\t\t$res = $gen->confirm();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-err', $res );\n\n\t\t$this->unload_payment_gateway( 'fake-confirm-err' );\n\n\t}\n\n\t/**\n\t * Test confirm() success\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_success() {\n\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-confirm-success';\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t\tpublic function confirm_pending_order( $order ) {\n\t\t\t\treturn array(\n\t\t\t\t\t'txn_id' => 12345,\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\n\t\t$gateway->supports['recurring_payments'] = true;\n\t\t$gateway->set_option( 'enabled', 'yes' );\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order->set( 'payment_gateway', 'fake-confirm-success' );\n\n\t\t$data                         = $this->get_mock_checkout_data_array();\n\t\t$data['llms_order_key']       = $order->get( 'order_key' );\n\t\t$data['llms_payment_gateway'] = 'fake-confirm-success';\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\tLLMS_Unit_Test_Util::set_private_property( $gen, 'gateway', $gateway );\n\n\t\t$this->assertEquals( array( 'txn_id' => 12345 ), $gen->confirm() );\n\n\t\t// User data should have been stored.\n\t\t$this->assertEquals( $data['email_address'], $order->get( 'billing_email' ) );\n\n\t\t$this->unload_payment_gateway( 'fake-confirm-success' );\n\n\t}\n\n\t/**\n\t * Test create() when an error is encountered creating the order post.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_error() {\n\n\t\t// Forces an error to be encountered during order creation.\n\t\tadd_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'create' );\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_CREATE_ORDER, $res );\n\n\t\tremove_filter( 'wp_insert_post_empty_content', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test commit_user().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_commit_user() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$data = $this->get_mock_user_data_array();\n\n\t\t// Register a new user.\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'commit_user' );\n\n\t\t$this->assertTrue( is_numeric( $res ) );\n\t\t$student = llms_get_student( $res );\n\t\t$this->assertEquals( $data['email_address'], $student->get( 'user_email' ) );\n\t\t$this->assertEquals( $data['first_name'], $student->get( 'first_name' ) );\n\n\t\t// Update the user.\n\t\twp_set_current_user( $res );\n\t\t$data['first_name'] = 'Albert';\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'commit_user' );\n\n\t\t$this->assertEquals( $student->get( 'id' ), $res );\n\t\t$this->assertEquals( $data['email_address'], $student->get( 'user_email' ) );\n\t\t$this->assertEquals( $data['first_name'], $student->get( 'first_name' ) );\n\n\t}\n\n\t/**\n\t * Test error().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_error() {\n\n\t\t$data = array( 'input' => 1 );\n\t\t$gen  = new LLMS_Order_Generator( $data );\n\n\t\tforeach ( array( 'coupon', 'gateway', 'plan', 'student', 'order' ) as $var ) {\n\t\t\tLLMS_Unit_Test_Util::set_private_property( $gen, $var, \"{$var}_value\" );\n\t\t}\n\n\n\t\t$res = LLMS_Unit_Test_Util::call_method(\n\t\t\t$gen,\n\t\t\t'error',\n\t\t\tarray( 'mock-code', 'Mock Message', array( 'extra' => 1 ) )\n\t\t);\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'mock-code', $res );\n\t\t$this->assertWPErrorMessageEquals( 'Mock Message', $res );\n\t\t$this->assertWPErrorDataEquals(\n\t\t\tarray(\n\t\t\t\t'coupon'  => 'coupon_value',\n\t\t\t\t'data'    => $data,\n\t\t\t\t'gateway' => 'gateway_value',\n\t\t\t\t'plan'    => 'plan_value',\n\t\t\t\t'student' => 'student_value',\n\t\t\t\t'extra'   => 1,\n\t\t\t\t'order'   => 'order_value',\n\t\t\t),\n\t\t\t$res\n\t\t);\n\n\t}\n\n\t/**\n\t * Test generate() with validation errors.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_validation_errors() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$res = $gen->generate();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_PLAN_REQUIRED, $res );\n\n\t}\n\n\t/**\n\t * Test generate() when an error is encountered during the user commit step.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_user_commit_errors() {\n\n\t\t$data = $this->get_mock_checkout_data_array();\n\n\t\t// After validation passes, register the user so the commit will fail.\n\t\t$handler = function( $valid ) use ( $data ) {\n\t\t\t$this->factory->user->create( array(\n\t\t\t\t'user_email' => $data['email_address'],\n\t\t\t) );\n\t\t\treturn $valid;\n\t\t};\n\t\tadd_filter( 'llms_after_generate_order_validation', $handler );\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\t$res = $gen->generate();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'existing_user_email', $res );\n\n\t\tremove_filter( 'llms_after_generate_order_validation', $handler );\n\n\t}\n\n\t/**\n\t * Test generate() with commit success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_success_user_commit() {\n\n\t\t$data = $this->get_mock_checkout_data_array();\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\t$res = $gen->generate();\n\n\t\t$this->assertTrue( is_a( $res, 'LLMS_Order' ) );\n\n\t\t$this->assertEquals( $data['llms_plan_id'], $res->get( 'plan_id' ) );\n\t\t$this->assertEquals( $data['email_address'], $res->get( 'billing_email' ) );\n\n\t\t$this->assertEquals(\n\t\t\tllms_get_student( get_user_by( 'email', $data['email_address'] ) ),\n\t\t\t$gen->get_student()\n\t\t);\n\n\t}\n\n\t/**\n\t * Test generate() with user validation only success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_success_user_validate() {\n\n\t\t$data = $this->get_mock_checkout_data_array();\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\t$res = $gen->generate( $gen::UA_VALIDATE );\n\n\t\t$this->assertTrue( is_a( $res, 'LLMS_Order' ) );\n\n\t\t$this->assertEquals( $data['llms_plan_id'], $res->get( 'plan_id' ) );\n\t\t$this->assertEquals( $data['email_address'], $res->get( 'billing_email' ) );\n\n\t\t$this->assertNull( $gen->get_student() );\n\n\t}\n\n\n\t/**\n\t * Test the protected property getter methods.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_getters() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\tforeach ( array( 'coupon', 'gateway', 'plan', 'student', 'order' ) as $var ) {\n\t\t\t$val = \"{$var}_value\";\n\t\t\tLLMS_Unit_Test_Util::set_private_property( $gen, $var, $val );\n\t\t\t$this->assertEquals( $val, $gen->{\"get_{$var}\"}() );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_order_id().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_order_id_with_order_key() {\n\n\t\t// Not submitted: create a new order.\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$this->assertEquals( 'new', LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t\t// Invalid key submitted.\n\t\t$gen = new LLMS_Order_Generator( array( 'llms_order_key' => 'fake' ) );\n\t\t$this->assertEquals( 'new', LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t\t// Actual key submitted.\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$gen = new LLMS_Order_Generator( array( 'llms_order_key' => $order->get( 'order_key' ) ) );\n\t\t$this->assertEquals( $order->get( 'id' ), LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t\t// Not a pending order: create a new one.\n\t\t$order->set_status( 'active' );\n\t\t$gen = new LLMS_Order_Generator( array( 'llms_order_key' => $order->get( 'order_key' ) ) );\n\t\t$this->assertEquals( 'new', LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t}\n\n\t/**\n\t * Test get_order_id() lookup by user/email & plan.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_order_id_by_user_and_plan() {\n\n\t\t$user_id = $this->factory->user->create( array( 'user_email' => 'email@test.tld' ) );\n\t\t$plan_id = $this->factory->post->create( array( 'post_type' => 'llms_access_plan' ) );\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order->set_bulk( compact( 'user_id', 'plan_id' ) );\n\n\t\t// Lookup by email of an existing user & plan.\n\t\t$gen = new LLMS_Order_Generator( array( \n\t\t\t'llms_plan_id'  => $plan_id,\n\t\t\t'email_address' => 'email@test.tld',\n\t\t) );\n\t\t$this->assertEquals( $order->get( 'id' ), LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t\t// Lookup using current user and plan.\n\t\twp_set_current_user( $user_id );\n\t\t$gen = new LLMS_Order_Generator( array( \n\t\t\t'llms_plan_id'  => $plan_id,\n\t\t) );\n\t\t$this->assertEquals( $order->get( 'id' ), LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t\t// Not a pending order: create a new one.\n\t\t$order->set_status( 'active' );\n\t\t$gen = new LLMS_Order_Generator( array( 'llms_order_key' => $order->get( 'order_key' ) ) );\n\t\t$this->assertEquals( 'new', LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t}\n\n\t/**\n\t * Test get_order_id() lookup by email (for a non-existent user) & plan.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_order_id_by_email_and_plan() {\n\n\t\t$billing_email = 'notstored@email.tld';\n\t\t$plan_id       = $this->factory->post->create( array( 'post_type' => 'llms_access_plan' ) );\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order->set_bulk( compact( 'billing_email', 'plan_id' ) );\n\n\t\t// Lookup by email of an existing user & plan.\n\t\t$gen = new LLMS_Order_Generator( array( \n\t\t\t'llms_plan_id'  => $plan_id,\n\t\t\t'email_address' => $billing_email,\n\t\t) );\n\t\t$this->assertEquals( $order->get( 'id' ), LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\n\t\t// Not a pending order: create a new one.\n\t\t$order->set_status( 'active' );\n\t\t$gen = new LLMS_Order_Generator( array( 'llms_order_key' => $order->get( 'order_key' ) ) );\n\t\t$this->assertEquals( 'new', LLMS_Unit_Test_Util::call_method( $gen, 'get_order_id' ) );\n\n\t}\n\n\t/**\n\t * Test get_user_data().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_user_data() {\n\n\t\t$data     = $this->get_mock_user_data_array();\n\t\t$gen      = new LLMS_Order_Generator( $data );\n\t\t$expected = array(\n\t\t\t'billing_email'      => $data['email_address'],\n\t\t\t'billing_first_name' => 'Fred',\n\t\t\t'billing_last_name'  => 'Stevens',\n\t\t\t'billing_address_1'  => '123 A Street',\n\t\t\t'billing_address_2'  => '#456',\n\t\t\t'billing_city'       => 'City',\n\t\t\t'billing_state'      => 'State',\n\t\t\t'billing_zip'        => '12345',\n\t\t\t'billing_country'    => 'CA',\n\t\t\t'billing_phone'      => '1234567890',\n\t\t\t'user_id'            => '',\n\t\t);\n\t\t$this->assertEquals( $expected, $gen->get_user_data() );\n\n\t\t// With Student.\n\t\t$student = $this->factory->student->create_and_get();\n\t\tLLMS_Unit_Test_Util::set_private_property( $gen, 'student', $student );\n\n\t\t$expected['user_id'] = $student->get( 'id' );\n\t\t$this->assertEquals( $expected, $gen->get_user_data() );\n\n\t}\n\n\t/**\n\t * Test get_user_data() when incomplete data is submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_user_data_incomplete() {\n\n\t\t// Incomplete data.\n\t\t$gen = new LLMS_Order_Generator( array( 'email_address' => 'a@b.tld' ) );\n\n\t\t$expected = array(\n\t\t\t'billing_email'      => 'a@b.tld',\n\t\t\t'billing_first_name' => '',\n\t\t\t'billing_last_name'  => '',\n\t\t\t'billing_address_1'  => '',\n\t\t\t'billing_address_2'  => '',\n\t\t\t'billing_city'       => '',\n\t\t\t'billing_state'      => '',\n\t\t\t'billing_zip'        => '',\n\t\t\t'billing_country'    => '',\n\t\t\t'billing_phone'      => '',\n\t\t\t'user_id'            => '',\n\t\t);\n\t\t$this->assertEquals( $expected, $gen->get_user_data() );\n\n\t}\n\n\t/**\n\t * Test validate() during an early return from a 3rd party.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_err_before() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\n\t\t$handler = function() {\n\t\t\treturn new WP_Error( 'mock', 'Message' );\n\t\t};\n\t\tadd_filter( 'llms_before_generate_order_validation', $handler );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'mock', $res );\n\t\t$this->assertWPErrorMessageEquals( 'Message', $res );\n\n\t\tremove_filter( 'llms_before_generate_order_validation', $handler );\n\n\t}\n\n\t/**\n\t * Test validate() when a validation error is encountered().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_error() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_PLAN_REQUIRED, $res );\n\n\t}\n\n\t/**\n\t * Test validate() when a validation error is encountered() with order data.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_error_order() {\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_order_key' => 'fake',\n\t\t) );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate', array( true ) );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_ORDER_NOT_FOUND, $res );\n\n\t}\n\n\n\t/**\n\t * Test validate() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_success() {\n\n\t\t$data = $this->get_mock_checkout_data_array();\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate' ) );\n\n\t\t// Order not validated.\n\t\t$this->assertNull( $gen->get_order() );\n\n\t}\n\n\t/**\n\t * Test validate() success with order validation.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_success_with_order() {\n\n\t\t$order = new LLMS_Order( 'new' );\n\n\t\t$data                   = $this->get_mock_checkout_data_array();\n\t\t$data['llms_order_key'] = $order->get( 'order_key' );\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate', array( true ) ) );\n\n\t\t// Order was validated\n\t\t$this->assertEquals( $order, $gen->get_order() );\n\n\t}\n\n\t/**\n\t * Test validate_coupon() when no coupon is submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_not_submitted() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_coupon' ) );\n\n\t}\n\n\t/**\n\t * Test validate_coupon() when the supplied coupon can't be found.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_not_found() {\n\n\t\t$gen = new LLMS_Order_Generator( array( 'llms_coupon_code' => 'fake' ) );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_coupon' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_COUPON_NOT_FOUND, $res );\n\n\t}\n\n\t/**\n\t * Test validate_coupon() when the supplied coupon isn't valid.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_not_valid() {\n\n\t\t$coupon = new LLMS_Coupon( 'new', 'expiredcoupon' );\n\t\t$coupon->set( 'status', 'publish' );\n\t\t$coupon->set( 'expiration_date', '01/01/2015' );\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id'     => $this->get_mock_plan()->get( 'id' ),\n\t\t\t'llms_coupon_code' => 'expiredcoupon'\n\t\t) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_coupon' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_COUPON_INVALID, $res );\n\n\t}\n\n\t/**\n\t * Test validate_coupon() with a valid coupon.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_coupon_valid() {\n\n\t\t$coupon = new LLMS_Coupon( 'new', 'validcoupon' );\n\t\t$coupon->set( 'status', 'publish' );\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id'     => $this->get_mock_plan()->get( 'id' ),\n\t\t\t'llms_coupon_code' => 'validcoupon'\n\t\t) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_coupon' ) );\n\t\t$this->assertEquals( $coupon, $gen->get_coupon() );\n\n\t}\n\n\t/**\n\t * Test validate_gateway() when no gateway is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_gateway_no_gateway() {\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id' => $this->get_mock_plan()->get( 'id' ),\n\t\t) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_gateway' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_GATEWAY_REQUIRED, $res );\n\n\t}\n\n\t/**\n\t * Test validate_gateway() when no gateway is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_gateway_invalid() {\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id'         => $this->get_mock_plan()->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'fake',\n\t\t) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_gateway' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-invalid', $res );\n\n\t}\n\n\t/**\n\t * Test validate_gateway() when no gateway is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_gateway_manual_for_free() {\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id' => $this->get_mock_plan( 0 )->get( 'id' ),\n\t\t) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_gateway' ) );\n\t\t$this->assertEquals( llms()->payment_gateways()->get_gateway_by_id( 'manual' ), $gen->get_gateway() );\n\n\t}\n\n\t/**\n\t * Test validate_gateway() when no gateway is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_gateway_success() {\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id'         => $this->get_mock_plan()->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'manual',\n\t\t) );\n\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_gateway' ) );\n\t\t$this->assertEquals( llms()->payment_gateways()->get_gateway_by_id( 'manual' ), $gen->get_gateway() );\n\n\t}\n\n\t/**\n\t * Test validate_order() when the order can't be found.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_order_not_found() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_order' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_ORDER_NOT_FOUND, $res );\n\n\n\t}\n\n\t/**\n\t * Test validate_order() when the order can't be confirmed.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_order_not_confirmable() {\n\n\t\t$order = new LLMS_Order( 'new' );\n\n\t\tadd_filter( 'llms_order_can_be_confirmed', '__return_false' );\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_order_key' => $order->get( 'order_key' ),\n\t\t) );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_order' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_ORDER_NOT_CONFIRMABLE, $res );\n\n\t\tremove_filter( 'llms_order_can_be_confirmed', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test validate_order() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_order_success() {\n\n\n\t\t$order = new LLMS_Order( 'new' );\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_order_key' => $order->get( 'order_key' ),\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_order' ) );\n\t\t$this->assertEquals( $order, $gen->get_order() );\n\n\t}\n\n\t/**\n\t * Test validate_plan() when no plan is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_plan_missing() {\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_PLAN_REQUIRED, $res );\n\n\t}\n\n\t/**\n\t * Test validate_plan() when an invalid or non-existent plan is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_plan_not_found() {\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id' => $this->factory->post->create(),\n\t\t) );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_PLAN_NOT_FOUND, $res );\n\n\t}\n\n\t/**\n\t * Test validate_plan() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_plan_success() {\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_plan_id' => $plan->get( 'id' ),\n\t\t) );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' ) );\n\t\t$this->assertEquals( $plan, $gen->get_plan() );\n\n\t}\n\n\t/**\n\t * Test validate_terms() when terms aren't required.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_terms_not_required() {\n\n\t\tupdate_option( 'lifterlms_registration_require_agree_to_terms', 'no' );\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_terms' ) );\n\n\t\tdelete_option( 'lifterlms_registration_require_agree_to_terms' );\n\n\t}\n\n\t/**\n\t * Test validate_terms() when terms aren't required.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_terms_required() {\n\n\t\tupdate_option( 'lifterlms_registration_require_agree_to_terms', 'yes' );\n\t\tupdate_option( 'lifterlms_terms_page_id', $this->factory->post->create( array( 'post_type' => 'page' ) ) );\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_terms' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_SITE_TERMS, $res );\n\n\t\t$gen = new LLMS_Order_Generator( array(\n\t\t\t'llms_agree_to_terms' => 'yes',\n\t\t) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_terms' ) );\n\n\t\tdelete_option( 'lifterlms_registration_require_agree_to_terms' );\n\t\tdelete_option( 'lifterlms_terms_page_id' );\n\n\t}\n\n\t/**\n\t * Test validate_user() with validation errors.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_user_error() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$gen = new LLMS_Order_Generator( array() );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_user' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $res );\n\n\t}\n\n\t/**\n\t * Test validate_user() extra enrollment validations.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_user_enrollment() {\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$data = $this->get_mock_user_data_array();\n\n\t\t$data['llms_plan_id'] = $plan->get( 'id' );\n\n\t\t$uid = $this->factory->user->create( array(\n\t\t\t'user_email' => $data['email_address'],\n\t\t) );\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\n\t\t// Existing user not enrolled.\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_user' ) );\n\n\t\t// User is enrolled.\n\t\tllms_enroll_student( $uid, $plan->get( 'product_id' ) );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $gen, 'validate_user' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( $gen::E_USER_ENROLLED, $res );\n\n\t}\n\n\t/**\n\t * Test validate_user() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_user_success() {\n\n\t\t$data = $this->get_mock_user_data_array();\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$gen = new LLMS_Order_Generator( $data );\n\t\tLLMS_Unit_Test_Util::call_method( $gen, 'validate_plan' );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $gen, 'validate_user' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-payment-gateway-integrations.php",
    "content": "<?php\n/**\n * Run round-trip payment tests using the mock testing gateway.\n *\n * @group payments\n *\n * @since 3.37.6\n * @since 3.37.12 Added additional assertion message information to assist in debug chaos-related failures.\n * @since 3.37.14 Reduce number of tests run for monthly and yearly chaotic simulations.\n * @since 4.3.1 Increased delta for `test_recurring_lifecycle_for_month_plan_with_chaos_and_frequency()` and `test_recurring_lifecycle_for_month_plan_with_chaos()`.\n * @since 5.3.1 Declare the `$gateway` property.\n */\nclass LLMS_Test_Payment_Gateway_Integrations extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var LLMS_Payment_Gateway|false\n\t */\n\tprotected $gateway;\n\n\t/**\n\t * Before the class runs, register the mock gateway.\n\t *\n\t * @since 3.37.6\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()` and renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tadd_filter( 'lifterlms_payment_gateways', array( __CLASS__, 'add_mock_gateway' ) );\n\n\t\t// We shouldn't be able to do this but currently we can so whatever.\n\t\tllms()->payment_gateways()->__construct();\n\n\t}\n\n\t/**\n\t * After the class runs, remove the mock gateway.\n\t *\n\t * @since 3.37.6\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()` and renamed from `tearDownAfterClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function tear_down_after_class() {\n\n\t\tremove_filter( 'lifterlms_payment_gateways', array( __CLASS__, 'add_mock_gateway' ) );\n\n\t\t// The gateways class is a bit messed up and loads gateways weird.\n\t\t// we need to remove the gateway manually so other tests don't break.\n\t\tforeach ( llms()->payment_gateways()->payment_gateways as $i => $gateway ) {\n\t\t\tif ( 'mock' === $gateway->id ) {\n\t\t\t\tunset( llms()->payment_gateways()->payment_gateways[ $i ] );\n\t\t\t}\n\t\t}\n\t\tparent::tear_down_after_class();\n\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.6\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->gateway = llms()->payment_gateways()->get_gateway_by_id( 'mock' );\n\t}\n\n\t/**\n\t * Register mock gateway\n\t *\n\t * @since 3.37.6\n\t *\n\t * @param string[] $gateways Array of gateway class names\n\t *\n\t * @return string[]\n\t */\n\tpublic static function add_mock_gateway( $gateways ) {\n\t\t$gateways[] = 'LLMS_Payment_Gateway_Mock';\n\t\treturn $gateways;\n\t}\n\n\t/**\n\t * Sets up a mock order for use with tests.\n\t *\n\t * @since 3.37.6\n\t *\n\t * @param string $period Access plan period value.\n\t * @param int $frequency Access plan frequency value.\n\t * @return LLMS_Order\n\t */\n\tprivate function setup_order( $period, $frequency = 1 ) {\n\n\t\t// Setup the objects.\n\t\t$student = $this->factory->student->create_and_get();\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$plan->set( 'period', $period );\n\t\t$plan->set( 'frequency', $frequency );\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order   = $order->init( $student, $plan, $this->gateway );\n\n\t\t// Process the order.\n\t\t$this->gateway->handle_pending_order( $order, $plan, $student );\n\n\t\treturn $order;\n\n\t}\n\n\t/**\n\t * Run some tests on the initial setup of the order and the first payment.\n\t *\n\t * @since 3.37.6\n\t * @since 5.3.3 Use assertEqualsWithDelta() in favor of 4th parameter supplied to assertEquals().\n\t *\n\t * @param LLMS_Order $order The order.\n\t * @return void\n\t */\n\tprivate function do_order_setup_tests( $order ) {\n\n\t\t$plan      = llms_get_post( $order->get( 'plan_id' ) );\n\t\t$period    = $plan->get( 'period' );\n\t\t$frequency = $plan->get( 'frequency' );\n\n\t\t// Order should be active.\n\t\t$this->assertEquals( 'llms-active', $order->get( 'status' ) );\n\n\t\t// Check there's only 1 transaction.\n\t\t$txns = $order->get_transactions();\n\t\t$this->assertEquals( 1, $txns['count'] );\n\n\t\t// Transaction succeeded.\n\t\t$last = array_pop( $txns['transactions'] );\n\t\t$this->assertEquals( 'llms-txn-succeeded', $last->get( 'status' ) );\n\n\t\t// Next payment date.\n\t\t$next_payment_time = $order->get_date( 'date_next_payment', 'U' );\n\t\t$this->assertEqualsWithDelta( strtotime( \"+{$frequency} {$period}\", $order->get_date( 'date', 'U' ) ), $next_payment_time, 5, $period ); // 5 seconds tolerance.\n\n\t}\n\n\t/**\n\t * Runs N charges on a recurring order with optionally included \"chaos\".\n\t *\n\t * \"Chaos\" will run the recurring payment randomly between $chaos_hours before and $chaos_hours after the scheduled payment time.\n\t *\n\t * @since 3.37.6\n\t * @since 3.37.12 Added additional assertion message information to assist in debug chaos-related failures.\n\t * @since 5.3.1 If the chaos >= 0, calculate the expected next payment time based on the scheduled payment time.\n\t * @since 5.3.3 Use assertEqualsWithDelta() in favor of 4th parameter supplied to assertEquals().\n\t *\n\t * @param LLMS_Order $order Initialized order to run charges against.\n\t * @param int $num Number of charges to run.\n\t * @param int $chaos_hours Number of hours of chaos to introduce.\n\t * @param int $delta_hours Number of hours of tolerance to allow as the \"delta\" for date comparison assertions.\n\t * @return void\n\t */\n\tprivate function do_n_charges_for_order( $order, $num, $chaos_hours = 0, $delta_hours = 0 ) {\n\n\t\t$plan      = llms_get_post( $order->get( 'plan_id' ) );\n\t\t$period    = $plan->get( 'period' );\n\t\t$frequency = $plan->get( 'frequency' );\n\n\t\t$start   = microtime( true );\n\t\t$limit   = 2.5;\n\t\t$elapsed = 0;\n\t\t$i       = 2;\n\t\twhile ( $i <= $num + 1 && $elapsed <= $limit ) {\n\n\t\t\t$scheduled_payment_time = (int) $order->get_date( 'date_next_payment', 'U' );\n\n\t\t\t// Run the recurring payment randomly between 12 hours before and 12 hours after the scheduled payment time.\n\t\t\t$chaos = rand( 0, HOUR_IN_SECONDS * $chaos_hours ) * ( rand( 0, 1 ) ? -1 : 1 );\n\n\t\t\t// Time travel.\n\t\t\tllms_tests_mock_current_time( $scheduled_payment_time + $chaos );\n\n\t\t\t// Run the transaction.\n\t\t\t$this->gateway->handle_recurring_transaction( $order );\n\n\t\t\t$txns = $order->get_transactions();\n\t\t\t$last_txn = array_shift( $txns['transactions'] );\n\t\t\t$last_txn_time = $last_txn->get_date( 'date', 'U' );\n\n\t\t\t// Should have transactions equal to the current loop interval.\n\t\t\t$this->assertEquals( $i, $txns['total'] );\n\n\t\t\t// Last transaction date should equal the chaos time, this way we can be sure it was the payment we thought it was.\n\t\t\t$this->assertEquals( $last_txn->get_date( 'date', 'U' ), $scheduled_payment_time + $chaos );\n\n\t\t\t$next_payment_time = $order->get_date( 'date_next_payment', 'U' );\n\n\t\t\tif ( $chaos < 0 ) {\n\t\t\t\t$expect = strtotime( \"+{$frequency} {$period}\", $last_txn_time );\n\t\t\t} else {\n\t\t\t\t$expect = strtotime( \"+{$frequency} {$period}\", $scheduled_payment_time );\n\t\t\t}\n\t\t\t$msg = sprintf(\n\t\t\t\t'%1$s Payment #%2$d: Got %3$s and expected %4$s ( $chaos_hours = %5$d | $chaos = %6$s )',\n\t\t\t\tucfirst( $period ),\n\t\t\t\t$i,\n\t\t\t\tdate( 'Y-m-d H:i:s', $next_payment_time ),\n\t\t\t\tdate( 'Y-m-d H:i:s', $expect ),\n\t\t\t\t$chaos_hours,\n\t\t\t\t$chaos\n\t\t\t);\n\n\t\t\t// Ensure that the calculated next payment time is 1 period +/- 23:59:59 from the previous transaction.\n\t\t\t$this->assertEqualsWithDelta( $expect, $next_payment_time, $delta_hours ? $delta_hours * HOUR_IN_SECONDS - 1 : 0, $msg );\n\n\t\t\t++$i;\n\t\t\t$elapsed = microtime( true ) - $start;\n\n\t\t}\n\n\t\t// if ( $elapsed > $limit ) {\n\n\t\t// \t$trace = debug_backtrace();\n\t\t// \t$caller = $trace[1];\n\n\t\t// \t$this->markTestSkipped( \"{$caller['class']}::{$caller['function']}: {$i}\" );\n\n\t\t// }\n\n\t}\n\n\t/**\n\t * Run tests for a for a daily plan\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_day_plan() {\n\n\t\t$order = $this->setup_order( 'day' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a daily plan with irregular frequency\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_day_plan_with_frequency() {\n\n\t\t$order = $this->setup_order( 'day', 3 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 10 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a daily plan_with_chaos\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_day_plan_with_chaos() {\n\n\t\t$order = $this->setup_order( 'day' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99, 6, 12 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a daily plan with chaos and irregular frequency\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_day_plan_with_chaos_and_frequency() {\n\n\t\t$order = $this->setup_order( 'day', 3 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 25, 6, 12 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a weekly plan\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_week_plan() {\n\n\t\t$order = $this->setup_order( 'week' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a weekly plan with irregular frequency\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_week_plan_with_frequency() {\n\n\t\t$order = $this->setup_order( 'week', 8 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 10 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a weekly plan_with_chaos\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_week_plan_with_chaos() {\n\n\t\t$order = $this->setup_order( 'week' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99, 12, 24 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a weekly plan with chaos and irregular frequency\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_week_plan_with_chaos_and_frequency() {\n\n\t\t$order = $this->setup_order( 'week', 2 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99, 12, 24 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a monthly plan\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_month_plan() {\n\n\t\t$order = $this->setup_order( 'month' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a monthly plan with irregular frequency\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_month_plan_with_frequency() {\n\n\t\t$order = $this->setup_order( 'month', 2 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a monthly plan_with_chaos\n\t *\n\t * @since 3.37.6\n\t * @since 3.37.14 Reduce number of tests run.\n\t * @since 4.3.1 Increased delta from 24 to 48 hours.\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_month_plan_with_chaos() {\n\n\t\t$order = $this->setup_order( 'month' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 50, 12, 48 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a monthly plan with chaos and irregular frequency\n\t *\n\t * @since 3.37.6\n\t * @since 4.3.1 Increased delta from 24 to 48 hours.\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_month_plan_with_chaos_and_frequency() {\n\n\t\t$order = $this->setup_order( 'month', 3 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 31, 12, 48 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a yearly plan\n\t *\n\t * @since 3.37.6\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_year_plan() {\n\n\t\t$order = $this->setup_order( 'year' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 99 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a yearly plan with irregular frequency\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_year_plan_with_frequency() {\n\n\t\t$order = $this->setup_order( 'year', 5 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 20 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a yearly plan_with_chaos\n\t *\n\t * @since 3.37.6\n\t * @since 3.37.14 Reduce number of tests run.\n\t *\n\t * @medium\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_year_plan_with_chaos() {\n\n\t\t$order = $this->setup_order( 'year' );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 50, 12, 24 );\n\n\t}\n\n\t/**\n\t * Run tests for a for a yearly plan with chaos and irregular frequency\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_lifecycle_for_year_plan_with_chaos_and_frequency() {\n\n\t\t$order = $this->setup_order( 'year', 2 );\n\n\t\t// Reinitialize the order for assertions.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Test setup data.\n\t\t$this->do_order_setup_tests( $order );\n\n\t\t// Run recurring charges for the order.\n\t\t$this->do_n_charges_for_order( $order, 9, 12, 24 );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-payment-gateways.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Payment_Gateways class\n *\n * @group payment_gateways\n *\n * @since 3.10.0\n */\nclass LLMS_Test_Payment_Gateways extends LLMS_UnitTestCase {\n\n\t/**\n\t * Enable or disable a payment gateway by ID\n\t *\n\t * @since 3.10.0\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`.\n\t *\n\t * @param string $id      Gateway id.\n\t * @param string $enabled Whether the gateway should be enabled or disabled. Accepts on or off.\n\t * @return void\n\t */\n\tprivate function toggle_gateway( $id, $enabled = 'on' ) {\n\n\t\t$enabled = 'on' === $enabled ? 'yes' : 'no';\n\n\t\t$manual = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), $enabled );\n\n\t}\n\n\t/**\n\t * Test get_enabled_payment_gateways function\n\t *\n\t * @since 3.10.0\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_enabled_payment_gateways() {\n\n\t\t$gways = llms()->payment_gateways();\n\n\t\t$this->toggle_gateway( 'manual', 'off' );\n\n\t\t$this->assertEquals( array(), $gways->get_enabled_payment_gateways() );\n\n\t\t// enable the manual gateway\n\t\t$this->toggle_gateway( 'manual', 'on' );\n\n\t\t// gateway should exist in the array\n\t\t$this->assertTrue( is_array( $gways->get_enabled_payment_gateways() ) );\n\t\t$this->assertTrue( array_key_exists( 'manual', $gways->get_enabled_payment_gateways() ) );\n\t\t$this->assertEquals( 1, count( $gways->get_enabled_payment_gateways() ) );\n\n\t}\n\n\t/**\n\t * Test get_default_gateway() function\n\t *\n\t * @since 3.10.0\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_default_gateway() {\n\n\t\t// enable the manual gateway\n\t\t$this->toggle_gateway( 'manual', 'on' );\n\t\t$this->assertEquals( 'manual', llms()->payment_gateways()->get_default_gateway() );\n\n\t}\n\n\t/**\n\t * Test get_payment_gateways() method\n\t *\n\t * @since 3.10.0\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_payment_gateways() {\n\n\t\t$gways = llms()->payment_gateways();\n\n\t\t$this->assertTrue( is_array( $gways->get_payment_gateways() ) );\n\t\t$this->assertTrue( array_key_exists( 'manual', $gways->get_payment_gateways() ) );\n\t\t$this->assertEquals( 1, count( $gways->get_payment_gateways() ) );\n\n\t}\n\n\t/**\n\t * Test has_gateways() method\n\t *\n\t * @since 3.10.0\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_gateways() {\n\n\t\t$gways = llms()->payment_gateways();\n\n\t\t// check all gateways (default)\n\t\t$this->assertTrue( $gways->has_gateways() );\n\t\t// check all gateways passing false\n\t\t$this->assertTrue( $gways->has_gateways( false ) );\n\n\t\t// check enabled\n\t\t$this->toggle_gateway( 'manual', 'off' );\n\t\t$this->assertFalse( $gways->has_gateways( true ) );\n\n\t\t$this->toggle_gateway( 'manual', 'on' );\n\t\t$this->assertTrue( $gways->has_gateways( true ) );\n\n\t}\n\n\t/**\n\t * Test get_gateway_by_id()\n\t *\n\t * @since 3.10.0\n\t * @since 5.3.3 Use `llms()` in favor of deprecated `LLMS()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_gateway_by_id() {\n\n\t\t$gways = llms()->payment_gateways();\n\t\t$manual = $gways->get_gateway_by_id( 'manual' );\n\t\t$this->assertTrue( is_a( $manual, 'LLMS_Payment_Gateway' ) );\n\t\t$this->assertEquals( 'manual', $manual->get_id() );\n\n\t\t$this->assertFalse( $gways->get_gateway_by_id( 'fake_gway' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-playnice.php",
    "content": "<?php\n/**\n * Tests for the LLMS_PlayNice Class\n * @since    3.19.6\n * @version  3.19.6\n */\nclass LLMS_Test_PlayNice extends LLMS_UnitTestCase {\n\n\t/**\n\t * Tests for wp_optimizepress_live_editor()\n\t * @return   void\n\t * @since    3.19.6\n\t * @version  3.19.6\n\t */\n\tpublic function test_wp_optimizepress_live_editor() {\n\n\t\t$play = new LLMS_PlayNice();\n\t\t$this->assertNull( $play->wp_optimizepress_live_editor() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-post-instructors.php",
    "content": "<?php\n/**\n * Tests for LLMS_Post_Instructors model & functions\n *\n * @package LifterLMS/Tests\n *\n * @group LLMS_Post_Instructors\n * @group LLMS_Course\n * @group LLMS_Membership\n *\n * @since 3.13.0\n */\nclass LLMS_Test_Post_Instructors extends LLMS_UnitTestCase {\n\n\tprivate $post_types = array( 'course', 'llms_membership' );\n\n\tpublic function test_interface() {\n\n\t\tforeach ( $this->post_types as $post_type ) {\n\n\t\t\t$post_id = $this->factory->post->create( array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t) );\n\n\t\t\t$post = llms_get_post( $post_id );\n\n\t\t\t$this->assertTrue( method_exists( $post, 'instructors' ) );\n\t\t\t$this->assertTrue( method_exists( $post, 'get_instructors' ) );\n\t\t\t$this->assertTrue( method_exists( $post, 'set_instructors' ) );\n\n\t\t\t$this->assertTrue( is_a( $post->instructors(), 'LLMS_Post_Instructors' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get and set methods.\n\t *\n\t * @since Unknown\n\t * @since 4.2.0 Added check to ensure `name` is set when no instructor data is set.\n\t *\n\t * @return void\n\t */\n\tpublic function test_getters_setters() {\n\n\t\t$user_ids = $this->factory->user->create_many( 3 );\n\n\t\tforeach ( $this->post_types as $post_type ) {\n\n\t\t\t$post_id = $this->factory->post->create( array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t\t'post_author' => $user_ids[0],\n\t\t\t) );\n\n\t\t\t$post = llms_get_post( $post_id );\n\n\t\t\t$defaults = llms_get_instructors_defaults();\n\n\t\t\t$this->assertTrue( is_array( $post->get_instructors() ) );\n\n\t\t\t$post->set_instructors( array(\n\t\t\t\tarray( 'id' => $user_ids[0] ),\n\t\t\t\tarray( 'id' => $user_ids[1] ),\n\t\t\t\tarray( 'id' => $user_ids[2] ),\n\t\t\t) );\n\n\t\t\tforeach ( $post->get_instructors() as $instructor ) {\n\n\t\t\t\t$this->assertTrue( in_array( $instructor['id'], $user_ids ) );\n\t\t\t\t$this->assertEquals( $defaults['label'], $instructor['label'] );\n\t\t\t\t$this->assertEquals( $defaults['visibility'], $instructor['visibility'] );\n\n\t\t\t}\n\n\t\t\t$this->assertEquals( $post->get( 'author' ), $user_ids[0] );\n\n\t\t\t$update = array(\n\t\t\t\tarray(\n\t\t\t\t\t'id' => $user_ids[1],\n\t\t\t\t\t'label' => 'mock label',\n\t\t\t\t\t'visibility' => 'visible',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'id' => $user_ids[0],\n\t\t\t\t\t'label' => 'mock label',\n\t\t\t\t\t'visibility' => 'hidden',\n\t\t\t\t),\n\t\t\t);\n\t\t\t$post->set_instructors( $update );\n\t\t\t$this->assertEquals( $update, $post->get_instructors() );\n\n\t\t\t// Check exclude hidden works right.\n\t\t\tunset( $update[1] );\n\t\t\t$this->assertEquals( $update, $post->get_instructors( true ) );\n\n\n\t\t\t// Clear instructors, should respond with a default of the post_author.\n\t\t\t$post->set_instructors();\n\t\t\t$expect = $defaults;\n\t\t\t$expect['id'] = $user_ids[1];\n\t\t\t$author = get_userdata( $user_ids[1] );\n\t\t\t$expect['name'] = $author->display_name;\n\t\t\t$this->assertEquals( array( $expect ), $post->get_instructors() );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-post-relationships.php",
    "content": "<?php\n/**\n * Tests for LLMS_Post_Instructors model & functions\n *\n * @group post_relationships\n *\n * @since 3.16.12\n * @since 3.37.8 Added tests to remove quiz attempts upon quiz deletion.\n * @since 4.15.0 Added tests on access plans deletion upon quiz deletion.\n * @since 5.4.0 Added tests for static methods delete_product_with_active_subscriptions_error_message() and maybe_prevent_product_deletion().\n */\nclass LLMS_Test_Post_Relationships extends LLMS_UnitTestCase {\n\n\t/**\n\t * When deleting lessons\n\t *\n\t * \t\tA) Any lesson which has this lesson as a prereq should have that prereq removed\n\t * \t\t   And the has_prereq metavalue should be unset returning \"no\"\n\t * \t\tB) Any quiz attached to this lesson should be detached (making it an orphan)\n\t *\n\t * @since 3.16.12\n\t * @return void\n\t */\n\tprivate function delete_lesson() {\n\n\t\t$courses = $this->generate_mock_courses( 1, 1, 4, 3, 1 );\n\t\t$lessons = llms_get_post( $courses[0] )->get_lessons();\n\n\t\t// add prereqs to all the lessons except the first.\n\t\tforeach ( $lessons as $i => $lesson ) {\n\n\t\t\tif ( 0 === $i ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$prev = $lessons[ $i - 1 ];\n\n\t\t\t$lesson->set( 'has_prerequisite', 'yes' );\n\t\t\t$lesson->set( 'prerequisite', $prev->get( 'id' ) );\n\n\t\t}\n\n\t\t// Delete posts and run tests.\n\t\tforeach ( $lessons as $i => $lesson ) {\n\n\t\t\t$quiz = $lesson->get_quiz();\n\n\t\t\twp_delete_post( $lesson->get( 'id' ) );\n\n\t\t\t// Quizzes attached to the lesson should now be orphaned.\n\t\t\tif ( $quiz ) {\n\t\t\t\t$this->assertTrue( $quiz->is_orphan() );\n\t\t\t}\n\n\t\t\tif ( $i === count( $lessons ) - 1 ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t$next = $lessons[ $i + 1 ];\n\n\t\t\t// Prereqs should be removed.\n\t\t\t$this->assertEquals( 'no', $next->get( 'has_prerequisite' ) );\n\t\t\t$this->assertEquals( 0, $next->get( 'prerequisite' ) );\n\t\t\t$this->assertFalse( $next->has_prerequisite() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * When a quiz is deleted, all the child questions should be deleted too\n\t *\n\t * Lesson should switch quiz_enabled to \"no\".\n\t *\n\t * All student attempts for the quiz should be deleted.\n\t *\n\t * @since 3.16.12\n\t * @since 3.37.8 Add tests to remove quiz attempts upon quiz deletion.\n\t * @since 6.0.0 Don't access `LLMS_Query_Quiz_Attempt` properties directly.\n\t *\n\t * @return void\n\t */\n\tprivate function delete_quiz() {\n\n\t\t$courses = $this->generate_mock_courses( 1, 1, 1, 1, 20 );\n\t\t$lesson = llms_get_post( llms_get_post( $courses[0] )->get_lessons( 'ids' )[0] );\n\t\t$quiz = $lesson->get_quiz();\n\t\t$quiz_id = $quiz->get( 'id' );\n\n\t\t$student_1 = $this->factory->student->create();\n\t\t$attempt_1 = $this->take_quiz( $quiz_id, $student_1 );\n\t\t$student_2 = $this->factory->student->create();\n\t\t$attempt_2 = $this->take_quiz( $quiz_id, $student_2, 50 );\n\n\t\t$questions = $quiz->get_questions( 'ids' );\n\n\t\twp_delete_post( $quiz->get( 'id' ), true );\n\n\t\t// All question posts should be deleted.\n\t\tforeach ( $questions as $question_id ) {\n\t\t\t$this->assertNull( get_post( $question_id ) );\n\t\t}\n\n\t\t// The quiz will be disabled on the lesson because metadata is unset.\n\t\t$this->assertFalse( $lesson->is_quiz_enabled() );\n\n\t\t// Quiz attempts should be deleted.\n\t\t$this->assertFalse( $attempt_1->exists() );\n\t\t$this->assertFalse( $attempt_2->exists() );\n\n\t\t// Query for quiz attempts should return nothing.\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'quiz_id'  => $quiz_id,\n\t\t\t\t'per_page' => 1,\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals( 0, $query->get_found_results() );\n\n\t}\n\n\t/**\n\t * When a product is deleted all the related access plans should be deleted\n\t *\n\t * @since 4.15.0\n\t *\n\t * @return void\n\t */\n\tprivate function delete_product() {\n\n\t\t$product_types = array(\n\t\t\t'course',\n\t\t\t'llms_membership',\n\t\t);\n\n\t\tforeach ( $product_types as $product_type ) {\n\n\t\t\t// Create product.\n\t\t\t$product_id  = $this->factory->post->create( array( 'post_type' => $product_type ) );\n\t\t\t$title       = sprintf( 'Access plan for %1$s', $product_id );\n\n\t\t\t// Create access plan and assign the related product to it.\n\t\t\t$access_plan    = llms_insert_access_plan( compact( 'product_id', 'title' ) );\n\t\t\t$access_plan_id = $access_plan->get( 'id' );\n\n\t\t\t// Get access plan properties (meta) to test.\n\t\t\tif ( ! isset( $access_plan_metas ) ) {\n\t\t\t\t$access_plan_metas = array_map(\n\t\t\t\t\tfunction( $prop ) use ( $access_plan ) {\n\t\t\t\t\t\treturn LLMS_Unit_Test_Util::get_private_property_value( $access_plan, 'meta_prefix' ) . $prop;\n\t\t\t\t\t},\n\t\t\t\t\tarray_keys(\n\t\t\t\t\t\tarray_diff_key(\n\t\t\t\t\t\t\t$access_plan->get( 'properties' ),\n\t\t\t\t\t\t\tLLMS_Unit_Test_Util::call_method( $access_plan, 'get_post_properties' )\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Trash product => do not remove access plans.\n\t\t\twp_trash_post( $product_id );\n\t\t\t$this->assertNotNull( get_post( $product_id ), $product_type );\n\n\t\t\t// Delete the product (no trash, force deletion is true by default for non built-in post types).\n\t\t\twp_delete_post( $product_id );\n\n\t\t\t// Check the access plan has been deleted.\n\t\t\t$this->assertNull( get_post( $product_id ), $product_type );\n\n\t\t\t// Check access plan's meta deletion\n\t\t\tforeach ( $access_plan_metas as $access_plan_meta ) {\n\t\t\t\t$this->assertFalse(\n\t\t\t\t\tmetadata_exists( 'post', $access_plan_id, $access_plan_meta ),\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t'Test failing for meta %1$s of access plan with ID %2$s on %3$s deletion',\n\t\t\t\t\t\t$access_plan_meta,\n\t\t\t\t\t\t$access_plan_id,\n\t\t\t\t\t\t$product_type\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test all relationships based on post types\n\t *\n\t * @since 3.16.12\n\t * @since 4.15.0 Added tests on course on membership deletion.\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_update_relationships() {\n\n\t\t$funcs = array(\n\t\t\t'delete_quiz',\n\t\t\t'delete_lesson',\n\t\t\t'delete_product',\n\t\t);\n\t\tforeach ( $funcs as $func ) {\n\t\t\t$this->{$func}();\n\t\t}\n\n\t}\n\n\t/**\n\t * Test delete_product_with_active_subscriptions_error_message().\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_product_with_active_subscriptions_error_message() {\n\n\t\t$post_types = array(\n\t\t\t'post',\n\t\t\t'course',\n\t\t\t'llms_membership',\n\t\t);\n\n\t\tforeach ( $post_types as $post_type ) {\n\n\t\t\t// Create post/product.\n\t\t\t$post_id  = $this->factory->post->create( array( 'post_type' => $post_type ) );\n\n\t\t\t$post_type_object = get_post_type_object( $post_type );\n\t\t\t$post_type_name   = $post_type_object->labels->name;\n\n\t\t\t$this->assertEquals(\n\t\t\t\t'post' !== $post_type ?\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t'Sorry, you are not allowed to delete %s with active subscriptions.',\n\t\t\t\t\t\t$post_type_name\n\t\t\t\t\t):\n\t\t\t\t\t''\n\t\t\t\t,\n\t\t\t\tLLMS_Post_Relationships::delete_product_with_active_subscriptions_error_message( $post_id )\n\t\t\t);\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test maybe_prevent_product_deletion()\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_prevent_product_deletion() {\n\n\t\t$post_types = array(\n\t\t\t'post',\n\t\t\t'course',\n\t\t\t'llms_membership',\n\t\t);\n\n\t\tforeach ( $post_types as $post_type ) {\n\n\t\t\t// Create post/product.\n\t\t\t$post_id  = $this->factory->post->create( array( 'post_type' => $post_type ) );\n\n\t\t\twp_delete_post( $post_id, true );\n\n\t\t\t$this->assertEmpty(\n\t\t\t\tget_post( $post_id ),\n\t\t\t\t$post_type\n\t\t\t);\n\n\t\t}\n\n\t\tunset( $post_types[0] );\n\n\t\t// Courses and Memberships are deletable if associated to a single-payment order.\n\t\tforeach ( $post_types as $post_type ) {\n\n\t\t\t// Create product.\n\t\t\t$post_id  = $this->factory->post->create( array( 'post_type' => $post_type ) );\n\n\t\t\t// Create an active subscription per product.\n\t\t\t$order   = $this->get_mock_order();\n\t\t\t$order->set( 'product_id', $post_id );\n\t\t\t$order->set( 'order_type', 'single' );\n\n\t\t\twp_delete_post( $post_id );\n\n\t\t\t$this->assertEmpty(\n\t\t\t\tget_post( $post_id ),\n\t\t\t\t$post_type\n\t\t\t);\n\n\t\t}\n\n\t\t// Courses and Memberships are deletable if associated to a recurring payment order depending on whether there are active subscriptions.\n\t\tforeach ( array_keys( llms_get_order_statuses( 'recurring' ) ) as $status ) {\n\t\t\tforeach ( $post_types as $post_type ) {\n\n\t\t\t\t// Create product.\n\t\t\t\t$post_id  = $this->factory->post->create( array( 'post_type' => $post_type ) );\n\n\t\t\t\t// Create an active subscription per product.\n\t\t\t\t$order   = $this->get_mock_order();\n\t\t\t\t$order->set( 'product_id', $post_id );\n\t\t\t\t$order->set( 'order_type', 'recurring' );\n\t\t\t\t$order->set( 'status', $status );\n\n\t\t\t\t$expected_error_message = LLMS_Post_Relationships::delete_product_with_active_subscriptions_error_message( $post_id );\n\n\t\t\t\ttry {\n\t\t\t\t\twp_delete_post( $post_id );\n\t\t\t\t} catch( WPDieException $e ) {\n\t\t\t\t\t$this->assertEquals(\n\t\t\t\t\t\t$expected_error_message,\n\t\t\t\t\t\t$e->getMessage()\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t// Test if subscription active no deletion occurred.\n\t\t\t\t$_test = in_array( $status, array( 'llms-active', 'llms-pending-cancel', 'llms-on-hold' ), true ) ? 'assertNotEmpty' : 'assertEmpty';\n\t\t\t\t$this->$_test(\n\t\t\t\t\tget_post( $post_id ),\n\t\t\t\t\t\"{$post_type} : {$status}\"\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test maybe_prevent_product_deletion() via REST API.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_prevent_product_deletion_rest_api() {\n\n\t\t$post_types = array(\n\t\t\t'course'          => 'courses',\n\t\t\t'llms_membership' => 'memberships',\n\t\t);\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Force llms_is_rest.\n\t\tadd_filter( 'llms_is_rest', '__return_true' );\n\n\t\t// Courses and Memberships are deletable if associated to a recurring payment order depending on whether there are active subscriptions.\n\t\tforeach ( array_keys( llms_get_order_statuses( 'recurring' ) ) as $status ) {\n\n\t\t\tforeach ( $post_types as $post_type => $endpoint ) {\n\n\t\t\t\t// Create product.\n\t\t\t\t$post_id  = $this->factory->post->create( array( 'post_type' => $post_type ) );\n\n\t\t\t\t// Create an active subscription per product.\n\t\t\t\t$order = $this->get_mock_order();\n\n\t\t\t\t$order->set( 'product_id', $post_id );\n\t\t\t\t$order->set( 'order_type', 'recurring' );\n\t\t\t\t$order->set( 'status', $status );\n\n\t\t\t\t$expected_error_message = LLMS_Post_Relationships::delete_product_with_active_subscriptions_error_message( $post_id );\n\n\t\t\t\t$request = new WP_REST_Request(\n\t\t\t\t\t'DELETE',\n\t\t\t\t\t\"/llms/v1/{$endpoint}/{$post_id}\"\n\t\t\t\t);\n\n\t\t\t\t$request->set_param( 'force', 'true' );\n\t\t\t\t$res = rest_get_server()->dispatch( $request );\n\n\t\t\t\tif ( in_array( $status, array( 'llms-active', 'llms-pending-cancel', 'llms-on-hold' ), true ) ) {\n\t\t\t\t\t// Not deleted.\n\t\t\t\t\t$this->assertNotEmpty(\n\t\t\t\t\t\tget_post( $post_id ),\n\t\t\t\t\t\t\"{$post_type} : {$status}\"\n\t\t\t\t\t);\n\n\t\t\t\t\t$this->assertEquals( 500, $res->get_status(), \"{$post_type} : {$status}\" );\n\t\t\t\t\t$this->assertEquals( $expected_error_message, $res->get_data()['message'], \"{$post_type} : {$status}\" );\n\t\t\t\t} else {\n\t\t\t\t\t// Deleted.\n\t\t\t\t\t$this->assertEmpty(\n\t\t\t\t\t\tget_post( $post_id ),\n\t\t\t\t\t\t\"{$post_type} : {$status}\"\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tremove_filter( 'llms_is_rest', '__return_true' );\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-post-types.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Custom Post Types.\n *\n * @group LLMS_Post_Types\n *\n * @since 3.13.0\n * @since 5.5.0 Addedd tests for deprecated filters of the type \"lifterlms_register_post_type_${prefixed_post_type_name}\".\n * @version 7.5.0\n */\nclass LLMS_Test_Post_Types extends LLMS_UnitTestCase {\n\n\t/**\n\t * LifterLMS Custom Post Types.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprivate $post_types = array(\n\t\t'course',\n\t\t'section',\n\t\t'lesson',\n\t\t'llms_membership',\n\t\t'llms_engagement',\n\t\t'llms_order',\n\t\t'llms_transaction',\n\t\t'llms_achievement',\n\t\t'llms_my_achievement',\n\t\t'llms_certificate',\n\t\t'llms_my_certificate',\n\t\t'llms_email',\n\t\t'llms_quiz',\n\t\t'llms_question',\n\t\t'llms_coupon',\n\t\t'llms_voucher',\n\t\t'llms_review',\n\t\t'llms_access_plan',\n\t\t'llms_form'\n\t);\n\n\t/**\n\t * LifterLMS Custom Post Types for earned engagements.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprivate $earned_engagements_post_types = array(\n\t\t'llms_my_achievement',\n\t\t'llms_my_certificate',\n\t);\n\n\t/**\n\t * LifterLMS Custom Taxonomies.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprivate $taxonomies = array(\n\t\t'course_cat',\n\t\t'course_difficulty',\n\t\t'course_tag',\n\t\t'course_track',\n\t\t'membership_cat',\n\t\t'membership_tag',\n\t\t'llms_product_visibility',\n\t\t'llms_access_plan_visibility',\n\t);\n\n\t/**\n\t * LifterLMS Custom Post Statuses.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @var string\n\t */\n\tprivate $post_statuses = array(\n\t\t'llms-completed',\n\t\t'llms-active',\n\t\t'llms-expired',\n\t\t'llms-on-hold',\n\t\t'llms-pending',\n\t\t'llms-cancelled',\n\t\t'llms-refunded',\n\t\t'llms-failed',\n\t\t'llms-txn-failed',\n\t\t'llms-txn-pending',\n\t\t'llms-txn-refunded',\n\t\t'llms-txn-succeeded',\n\t);\n\n\t/**\n\t * Test LLMS_Post_Types::deregister_sitemap_post_types( $mock ).\n\t *\n\t * @since 4.3.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_deregister_sitemap_post_types() {\n\n\t\t$mock = array(\n\t\t\t'post' => true,\n\t\t\t'page' => true,\n\t\t\t'course' => true,\n\t\t\t'lesson' => true,\n\t\t\t'llms_quiz' => true,\n\t\t\t'llms_certificate' => true,\n\t\t\t'llms_my_certificate' => true\n\t\t);\n\n\t\t$expect = array(\n\t\t\t'post' => true,\n\t\t\t'page' => true,\n\t\t\t'course' => true,\n\t\t);\n\n\t\t$this->assertEquals( $expect, LLMS_Post_Types::deregister_sitemap_post_types( $mock ) );\n\n\t}\n\n\t/**\n\t * Test get_template().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( 'LLMS_Post_Types', 'templates', array() );\n\n\t\t// No template exists.\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( 'LLMS_Post_Types', 'get_template', array( 'fake' ) ) );\n\n\t\t// Templates are loaded.\n\t\t$this->assertNotEmpty( LLMS_Unit_Test_Util::get_private_property_value( 'LLMS_Post_Types', 'templates' ) );\n\n\t\t// Template is returned.\n\t\t$this->assertNotEmpty( LLMS_Unit_Test_Util::call_method( 'LLMS_Post_Types', 'get_template', array( 'llms_my_certificate' ) ) );\n\n\t}\n\n\t/**\n\t * Test register taxonomies.\n\t *\n\t * @since 3.13.0\n\t * @since 6.0.0 Use `$this->taxonomies` member.\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_taxonomies() {\n\n\t\tLLMS_Post_Types::register_taxonomies();\n\n\t\tforeach ( $this->taxonomies as $name ) {\n\t\t\t// var_dump( sprintf( '%s: %s', $name, taxonomy_exists( $name ) ) );\n\t\t\t$this->assertTrue( taxonomy_exists( $name ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test register_post_type().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_type() {\n\n\t\t$slug = 'a_fake_post_type';\n\t\t$post_type = LLMS_Post_Types::register_post_type( $slug, array() );\n\t\t$this->assertInstanceOf( 'WP_Post_Type', $post_type );\n\t\t$this->assertEquals( $slug, $post_type->name );\n\t\tunregister_post_type( $slug );\n\n\t}\n\n\t/**\n\t * Test register_post_type() for a post type that's already been registered.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_type_already_registered() {\n\n\t\t$this->assertTrue( post_type_exists( 'course' ) );\n\t\t$post_type = LLMS_Post_Types::register_post_type( 'course', array( 'menu_postion' => 10 ) );\n\t\t$this->assertInstanceOf( 'WP_Post_Type', $post_type );\n\t\t$this->assertEquals( 52, $post_type->menu_position );\n\n\t}\n\n\t/**\n\t * Test register_post_type() for a post that has a defined block template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_type_with_template() {\n\n\t\tunregister_post_type( 'llms_certificate' );\n\t\t$post_type = LLMS_Post_Types::register_post_type( 'llms_certificate', array() );\n\t\t$this->assertInstanceOf( 'WP_Post_Type', $post_type );\n\t\t$this->assertNotEmpty( $post_type->template );\n\t\tunregister_post_type( 'llms_certificate' );\n\n\t\tLLMS_Post_Types::register_post_types();\n\n\t}\n\n\t/**\n\t * Test register post types.\n\t *\n\t * @since 3.13.0\n\t * @since 6.0.0 Use `$this->post_types` member.\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_types() {\n\n\t\tLLMS_Post_Types::register_post_types();\n\n\t\tforeach ( $this->post_types as $name ) {\n\t\t\t$this->assertTrue( post_type_exists( $name ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test register post statusess.\n\t *\n\t * @since 3.13.0\n\t * @since 6.0.0 Use `$this->post_statuses` member.\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_statuses() {\n\n\t\tLLMS_Post_Types::register_post_statuses();\n\n\t\tforeach ( $this->post_statuses as $name ) {\n\t\t\t$this->assertTrue( ! is_null( get_post_status_object( $name ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test deprecated filters of the type \"lifterlms_register_post_type_${prefixed_post_type_name}\".\n\t *\n\t * @expectedDeprecated lifterlms_register_post_type_llms_membership\n\t * @expectedDeprecated lifterlms_register_post_type_llms_engagement\n\t * @expectedDeprecated lifterlms_register_post_type_llms_order\n\t * @expectedDeprecated lifterlms_register_post_type_llms_transaction\n\t * @expectedDeprecated lifterlms_register_post_type_llms_achievement\n\t * @expectedDeprecated lifterlms_register_post_type_llms_certificate\n\t * @expectedDeprecated lifterlms_register_post_type_llms_my_certificate\n\t * @expectedDeprecated lifterlms_register_post_type_llms_my_achievement\n\t * @expectedDeprecated lifterlms_register_post_type_llms_email\n\t * @expectedDeprecated lifterlms_register_post_type_llms_quiz\n\t * @expectedDeprecated lifterlms_register_post_type_llms_question\n\t * @expectedDeprecated lifterlms_register_post_type_llms_coupon\n\t * @expectedDeprecated lifterlms_register_post_type_llms_voucher\n\t * @expectedDeprecated lifterlms_register_post_type_llms_review\n\t * @expectedDeprecated lifterlms_register_post_type_llms_access_plan\n\t * @expectedDeprecated lifterlms_register_post_type_llms_form\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @since 5.5.0\n\t * @since 7.5.0 Fixed use of deprecated `${var}`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_deprecated_filters() {\n\n\t\tforeach ( $this->post_types as $post_type ) {\n\n\t\t\tunregister_post_type( $post_type );\n\t\t\tadd_filter( \"lifterlms_register_post_type_{$post_type}\", '__return_empty_array' );\n\t\t\tLLMS_Post_Types::register_post_type( $post_type, array() );\n\t\t\tremove_filter( \"lifterlms_register_post_type_{$post_type}\", '__return_empty_array' );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test LLMS_Post_Types::get_post_type_caps() when argument is an array.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_post_type_caps_argument_as_array() {\n\n\t\tforeach ( array_diff( $this->post_types, $this->earned_engagements_post_types ) as $post_type ) {\n\t\t\t$post_type = str_replace( 'llms_', '', $post_type );\n\t\t\t$singular  = $post_type;\n\t\t\t$plural    = $post_type . '_plural';\n\n\t\t\t$post_type = array(\n\t\t\t\t$singular,\n\t\t\t\t$plural,\n\t\t\t);\n\n\t\t\t$this->assertEquals(\n\t\t\t\tLLMS_Post_Types::get_post_type_caps( $post_type ),\n\t\t\t\tarray(\n\t\t\t\t\t'read_post'              => sprintf( 'read_%s', $singular ),\n\t\t\t\t\t'read_private_posts'     => sprintf( 'read_private_%s', $plural ),\n\n\t\t\t\t\t'edit_post'              => sprintf( 'edit_%s', $singular ),\n\t\t\t\t\t'edit_posts'             => sprintf( 'edit_%s', $plural ),\n\t\t\t\t\t'edit_others_posts'      => sprintf( 'edit_others_%s', $plural ),\n\t\t\t\t\t'edit_private_posts'     => sprintf( 'edit_private_%s', $plural ),\n\t\t\t\t\t'edit_published_posts'   => sprintf( 'edit_published_%s', $plural ),\n\n\t\t\t\t\t'publish_posts'          => sprintf( 'publish_%s', $plural ),\n\n\t\t\t\t\t'delete_post'            => sprintf( 'delete_%s', $singular ),\n\t\t\t\t\t'delete_posts'           => sprintf( 'delete_%s', $plural ), // This is the core bug issue here.\n\t\t\t\t\t'delete_private_posts'   => sprintf( 'delete_private_%s', $plural ),\n\t\t\t\t\t'delete_published_posts' => sprintf( 'delete_published_%s', $plural ),\n\t\t\t\t\t'delete_others_posts'    => sprintf( 'delete_others_%s', $plural ),\n\n\t\t\t\t\t'create_posts'           => sprintf( 'create_%s', $plural ),\n\t\t\t\t),\n\t\t\t\t$post_type[0]\n\t\t\t);\n\t\t}\n\n\t\tforeach ( $this->earned_engagements_post_types as $post_type ) {\n\n\t\t\t$post_type = str_replace( 'llms_', '', $post_type );\n\n\t\t\t$post_type = array(\n\t\t\t\t$post_type,\n\t\t\t\t$post_type . '_plural',\n\t\t\t);\n\n\t\t\t$this->assertEquals(\n\t\t\t\tLLMS_Post_Types::get_post_type_caps( $post_type ),\n\t\t\t\tLLMS_Post_Types::get_earned_engagements_post_type_caps(),\n\t\t\t);\n\n\t\t}\n\t}\n\n\n\t/**\n\t * Test LLMS_Post_Types::get_post_type_caps() when argument is a string.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_post_type_caps_argument_as_string() {\n\n\t\tforeach ( array_diff( $this->post_types, $this->earned_engagements_post_types ) as $post_type ) {\n\t\t\t$post_type = str_replace( 'llms_', '', $post_type );\n\t\t\t$singular  = $post_type;\n\t\t\t$plural    = $post_type . 's';\n\n\t\t\t$this->assertEquals(\n\t\t\t\tLLMS_Post_Types::get_post_type_caps( $post_type ),\n\t\t\t\tarray(\n\t\t\t\t\t'read_post'              => sprintf( 'read_%s', $singular ),\n\t\t\t\t\t'read_private_posts'     => sprintf( 'read_private_%s', $plural ),\n\n\t\t\t\t\t'edit_post'              => sprintf( 'edit_%s', $singular ),\n\t\t\t\t\t'edit_posts'             => sprintf( 'edit_%s', $plural ),\n\t\t\t\t\t'edit_others_posts'      => sprintf( 'edit_others_%s', $plural ),\n\t\t\t\t\t'edit_private_posts'     => sprintf( 'edit_private_%s', $plural ),\n\t\t\t\t\t'edit_published_posts'   => sprintf( 'edit_published_%s', $plural ),\n\n\t\t\t\t\t'publish_posts'          => sprintf( 'publish_%s', $plural ),\n\n\t\t\t\t\t'delete_post'            => sprintf( 'delete_%s', $singular ),\n\t\t\t\t\t'delete_posts'           => sprintf( 'delete_%s', $plural ), // This is the core bug issue here.\n\t\t\t\t\t'delete_private_posts'   => sprintf( 'delete_private_%s', $plural ),\n\t\t\t\t\t'delete_published_posts' => sprintf( 'delete_published_%s', $plural ),\n\t\t\t\t\t'delete_others_posts'    => sprintf( 'delete_others_%s', $plural ),\n\n\t\t\t\t\t'create_posts'           => sprintf( 'create_%s', $plural ),\n\t\t\t\t),\n\t\t\t\t$post_type[0]\n\t\t\t);\n\t\t}\n\n\t\tforeach ( $this->earned_engagements_post_types as $post_type ) {\n\n\t\t\t$post_type = str_replace( 'llms_', '', $post_type );\n\n\t\t\t$this->assertEquals(\n\t\t\t\tLLMS_Post_Types::get_post_type_caps( $post_type ),\n\t\t\t\tLLMS_Post_Types::get_earned_engagements_post_type_caps(),\n\t\t\t);\n\n\t\t}\n\t}\n\n\t/**\n\t * Check actual post types capabilities.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_post_type_capabilities() {\n\t\tforeach (\n\t\t\t\tarray(\n\t\t\t\t\t'course'              => 'course',\n\t\t\t\t\t'lesson'              => 'lesson',\n\t\t\t\t\t'llms_membership'     => 'membership',\n\t\t\t\t\t'llms_quiz'           => array( 'quiz', 'quizzes' ),\n\t\t\t\t\t'llms_question'       => 'question',\n\t\t\t\t\t'llms_my_achievement' => 'my_achievement',\n\t\t\t\t\t'llms_my_certificate' => 'my_certificate',\n\t\t\t\t) as $post_type => $post_type_name_for_caps ) {\n\n\t\t\t$post_type_object = get_post_type_object( $post_type );\n\t\t\t$caps = (array) $post_type_object->cap;\n\t\t\tunset( $caps['read'] );\n\n\t\t\t$this->assertEquals(\n\t\t\t\t$caps,\n\t\t\t\tLLMS_Post_Types::get_post_type_caps( $post_type_name_for_caps ),\n\t\t\t);\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-prevent-concurrent-logins.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Prevent Concurrent Logins class\n *\n * @group LLMS_Prevent_Concurrent_Logins\n *\n * @since 5.6.0\n */\nclass LLMS_Test_Prevent_Concurrent_Logins extends  LLMS_UnitTestCase {\n\n\t/**\n\t * Test maybe_prevent_concurrent_logins().\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_prevent_concurrent_logins() {\n\n \t\t// By default the 'student' role is not allowed to log-in multiple times at once.\n\t\t$user_id = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\t$session_1 = $this->_log_in( $user_id, time() + DAY_IN_SECONDS );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// First login, nothing to prevent.\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\tfalse,\n\t\t\tLLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins()\n\t\t);\n\n\t\t// Another login.\n\t\t$session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ) );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t\t$session_2,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Second login, the first session should be destroyed.\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\ttrue,\n\t\t\tLLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins()\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray( $session_2 ),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t}\n\n\t/**\n\t * Test maybe_prevent_concurrent_logins().\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_prevent_concurrent_logins_allow_roles() {\n\t\t$prevent_option = get_option( 'lifterlms_prevent_concurrent_logins' );\n\t\t$roles_option   = get_option( 'lifterlms_prevent_concurrent_logins_roles' );\n\n\t\t// By default the 'student' role is not allowed to log-in multiple times at once.\n\t\t$user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\t$session_1 = $this->_log_in( $user_id, time() + DAY_IN_SECONDS );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// First login, nothing to prevent.\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\tfalse,\n\t\t\tLLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins()\n\t\t);\n\n\t\t// Another login.\n\t\t$session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ) );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t\t$session_2,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Second login, since we're an allowed role, there's nothing to prevent.\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\tfalse,\n\t\t\tLLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins()\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t\t$session_2,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Change the allowed role option to disallow administrators.\n\t\tupdate_option( 'lifterlms_prevent_concurrent_logins_roles', array( 'administrator' ) );\n\n\t\t// Another login.\n\t\t$session_3 = $this->_log_in( $user_id, time() + ( 3 * DAY_IN_SECONDS ) );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t\t$session_2,\n\t\t\t\t$session_3,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\ttrue,\n\t\t\tLLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins()\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_3,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Allow current user to login multiple times via a filter.\n\t\t$allow_current_user = function( $allow, $uid ) use ( $user_id ) {\n\t\t\treturn $uid === $user_id ? true : $allow;\n\t\t};\n\t\tadd_filter( 'llms_allow_user_concurrent_logins', $allow_current_user, 10, 2 );\n\n\t\t// Another login.\n\t\t$session_4 = $this->_log_in( $user_id, time() + ( 4 * DAY_IN_SECONDS ) );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_3,\n\t\t\t\t$session_4,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\tfalse,\n\t\t\tLLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins()\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_3,\n\t\t\t\t$session_4,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\tremove_filter( 'llms_allow_user_concurrent_logins', $allow_current_user, 10, 2 );\n\n\t\t// Change the allowed role option to an empty array.\n\t\tupdate_option( 'lifterlms_prevent_concurrent_logins_roles', array() );\n\t\t// Another login.\n\t\t$session_5 = $this->_log_in( $user_id, time() + ( 5 * DAY_IN_SECONDS ) );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_3,\n\t\t\t\t$session_4,\n\t\t\t\t$session_5,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\tfalse,\n\t\t\tLLMS_Prevent_Concurrent_Logins::instance()->maybe_prevent_concurrent_logins()\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_3,\n\t\t\t\t$session_4,\n\t\t\t\t$session_5,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Reset.\n\t\tupdate_option( 'lifterlms_prevent_concurrent_logins', $prevent_option );\n\t\tupdate_option( 'lifterlms_prevent_concurrent_logins_roles', $roles_option );\n\n\t}\n\n\t/**\n\t * Test maybe_prevent_concurrent_logins().\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_destroy_all_sessions_but_newest() {\n\n\t\t// First login, it's also the newest, I expect it to be kept: 1.\n\t\t$user_id   = $this->factory->user->create();\n\t\t$session_1 = $this->_log_in( $user_id, time() + ( 1 * DAY_IN_SECONDS ), $first_token );\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\t1,\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\tLLMS_Prevent_Concurrent_Logins::instance(),\n\t\t\t\t'destroy_all_sessions_but_newest'\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Another login.\n\t\t$session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ) );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t\t$session_2,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Second login, it's also the newest, I expect it to be kept: 1.\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t$this->assertEquals(\n\t\t\t1,\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\tLLMS_Prevent_Concurrent_Logins::instance(),\n\t\t\t\t'destroy_all_sessions_but_newest'\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_2,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t\t// Now simulate the current session is the oldest.\n\t\twp_destroy_all_sessions( $user_id );\n\t\t$session_1 = $this->_log_in( $user_id, time() + ( 1 * DAY_IN_SECONDS ), $first_token );\n\t\t$session_2 = $this->_log_in( $user_id, time() + ( 2 * DAY_IN_SECONDS ), $second_token );\n\n\t\t// Make the session 2 the oldest:\n\t\t$session_2['login'] = time() - ( 2 * DAY_IN_SECONDS );\n\t\tWP_Session_Tokens::get_instance( $user_id )->update(\n\t\t\t$second_token,\n\t\t\t$session_2\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t\t$session_2,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\t\tLLMS_Prevent_Concurrent_Logins::instance()->init();\n\t\t// I expect the first session (promoted as newest) to be kept.\n\t\t$this->assertEquals(\n\t\t\t0,\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\tLLMS_Prevent_Concurrent_Logins::instance(),\n\t\t\t\t'destroy_all_sessions_but_newest'\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t$session_1,\n\t\t\t),\n\t\t\twp_get_all_sessions()\n\t\t);\n\n\t}\n\n\t/**\n\t * Simulate a log in.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @param int    $user_id   WP_User ID.\n\t * @param int    $epiration Expiration time.\n\t * @param string $token     Passed by reference, the created session token.\n\t * @return array Login session.\n\t */\n\tprivate function _log_in( $user_id, $expiration, &$token = '' ) {\n\n\t\t$manager    = WP_Session_Tokens::get_instance( $user_id );\n\t\t$token      = $manager->create( $expiration );\n\t\twp_set_current_user( $user_id );\n\t\t$logged_in_cookie = wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $token );\n\t\t$this->cookies->set( LOGGED_IN_COOKIE, $logged_in_cookie, $expiration + ( 12 * HOUR_IN_SECONDS ), SITECOOKIEPATH, COOKIE_DOMAIN, false, true );\n\t\treturn $manager->get( $token );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-query.php",
    "content": "<?php\n/**\n * Tests for LLMS_Query class\n *\n * @package LifterLMS/Tests\n *\n * @group query\n *\n * @since 4.5.0\n */\nclass LLMS_Test_Query extends LLMS_UnitTestCase {\n\n\t/**\n\t * Set up test case\n\t *\n\t * @since 4.5.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Query();\n\n\t}\n\n\t/**\n\t * Assertion helper to ensure that the `$wp_query->query` variables equal an expected array\n\t *\n\t * @since 5.0.2\n\t *\n\t * @param array  $expected Expected query array.\n\t * @param string $message  Error message.\n\t * @return void\n\t */\n\tprivate function assertQueryVarsEqual( $expected, $message = null ) {\n\n\t\tglobal $wp_query;\n\t\t$this->assertEquals( $expected, $wp_query->query, urldecode( $message ) );\n\n\t}\n\n\t/**\n\t * Tests the add_endpoints() method\n\t *\n\t * This is a large \"integration\" test that ensures that student dashboard\n\t * endpoints are properly added to the `$wp_rewrite` list.\n\t *\n\t * It does \"real\" tests by simulating a visit to the URL and testing that the expected\n\t * `$wp_query->query` variables are set.\n\t *\n\t * It runs tests with the default values of the endpoints, with custom translated values in various\n\t * languages (to ensure non-latin characters work in customized slugs) and finally adds a random string\n\t * of alphanumeric latin chars for testing.\n\t *\n\t * It additionally tests pagination for endpoints that utilize pagination work regardless of the customized\n\t * slug.\n\t *\n\t * @since 5.0.2\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1639\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_endpoints() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t// Setup.\n\t\t$temp = get_option( 'permalink_structure' );\n\t\tupdate_option( 'permalink_structure', '/%postname%/' );\n\n\t\tglobal $wp_rewrite;\n\t\t$wp_rewrite->init();\n\n\t\t$account_url = llms_get_page_url( 'myaccount' );\n\t\t$options     = array(\n\t\t\t'view-courses'      => 'lifterlms_myaccount_courses_endpoint',\n\t\t\t'my-grades'         => 'lifterlms_myaccount_grades_endpoint',\n\t\t\t'view-memberships'  => 'lifterlms_myaccount_memberships_endpoint',\n\t\t\t'view-achievements' => 'lifterlms_myaccount_achievements_endpoint',\n\t\t\t'view-certificates' => 'lifterlms_myaccount_certificates_endpoint',\n\t\t\t'view-favorites'    => 'lifterlms_myaccount_favorites_endpoint',\n\t\t\t'notifications'     => 'lifterlms_myaccount_notifications_endpoint',\n\t\t\t'edit-account'      => 'lifterlms_myaccount_edit_account_endpoint',\n\t\t\t'redeem-voucher'    => 'lifterlms_myaccount_redeem_vouchers_endpoint',\n\t\t\t'orders'            => 'lifterlms_myaccount_orders_endpoint',\n\t\t);\n\n\t\t$non_latin = array(\n\t\t\t'view-courses'      => 'ビューコース', // Japanese.\n\t\t\t'my-grades'         => 'мои-оценки', // Russian.\n\t\t\t'view-memberships'  => 'ਵੇਖੋ-ਸਦੱਸਤਾ', // Punjabi.\n\t\t\t'view-achievements' => 'nailiyyətlər', // Azerbaijani.\n\t\t\t'view-certificates' => 'ເບິ່ງໃບຢັ້ງຢືນ', // Lao.\n\t\t\t'view-favorites'    => '즐겨찾기', // Korean.\n\t\t\t'notifications'     => '通知', // Chinese (Simplified).\n\t\t\t'edit-account'      => 'חשבון-עריכה', // Hebrew.\n\t\t\t'redeem-voucher'    => 'چھڑانا', // Urdu.\n\t\t\t'orders'            => 'आदेश', // Hindi.\n\t\t);\n\n\t\tforeach ( LLMS_Student_Dashboard::get_tabs() as $id => $tab ) {\n\n\t\t\tif ( empty( $tab['endpoint'] ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$tests = array(\n\t\t\t\t$tab['endpoint'],\n\t\t\t\twp_generate_password( 6, false, false ),\n\t\t\t\t$non_latin[ $id ],\n\t\t\t);\n\n\t\t\tforeach ( $tests as $option ) {\n\n\t\t\t\tupdate_option( $options[ $id ], urlencode( $option ) );\n\t\t\t\tnew LLMS_Student_Dashboard();\n\t\t\t\t$this->main->add_endpoints();\n\t\t\t\tflush_rewrite_rules();\n\n\t\t\t\t$url = isset( $tab['url'] ) ? $tab['url'] : llms_get_endpoint_url( $id, null, $account_url );\n\n\t\t\t\t$this->go_to( $url );\n\n\t\t\t\t$expect = array(\n\t\t\t\t\t'pagename' => 'dashboard',\n\t\t\t\t);\n\n\t\t\t\tif ( 'dashboard' === $id ) {\n\t\t\t\t\t$expect['page'] = '';\n\t\t\t\t} else {\n\t\t\t\t\t$expect[ $id ] = '';\n\t\t\t\t}\n\n\t\t\t\t$this->assertQueryVarsEqual( $expect, $id . ' - ' . $url );\n\n\t\t\t\tif ( ! empty( $tab['paginate'] ) ) {\n\t\t\t\t\t$url .= 'page/1';\n\t\t\t\t\t$this->go_to( $url );\n\t\t\t\t\t$expect['paged'] = 1;\n\t\t\t\t\t$this->assertQueryVarsEqual( $expect, $url );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t$this->go_to( '' );\n\n\t\t// Teardown.\n\t\tupdate_option( 'permalink_structure', $temp );\n\t\t$wp_rewrite->init();\n\n\t}\n\n\t/**\n\t * Test maybe_404_certificate()\n\t *\n\t * This test runs in a separate process because something before it is making it hard\n\t * to mock the `$wp_query` and `$post` globals.\n\t *\n\t * @since 4.5.0\n\t * @since 6.0.0 Ensure a post author exists for tested posts.\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_404_certificate() {\n\n\t\tglobal $post, $wp_query;\n\t\t$temp = $post;\n\n\t\t$admin       = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\t$post_author = $this->factory->user->create();\n\n\t\t// Not set.\n\t\t$post = null;\n\t\t$this->main->maybe_404_certificate();\n\t\t$this->assertFalse( $wp_query->is_404() );\n\n\t\t$tests = array(\n\t\t\t'llms_my_certificate' => true,\n\t\t\t'post'                => false,\n\t\t\t'page'                => false,\n\t\t\t'course'              => false,\n\t\t);\n\n\t\tforeach ( $tests as $post_type => $expect ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type', 'post_author' ) );\n\t\t\t$wp_query->init();\n\n\t\t\t// Logged out user.\n\t\t\t$this->main->maybe_404_certificate();\n\t\t\t$this->assertEquals( $expect, $wp_query->is_404(), $post_type );\n\n\t\t\t// Logged in admin can always see.\n\t\t\t$wp_query->init();\n\t\t\twp_set_current_user( $admin );\n\t\t\t$this->main->maybe_404_certificate();\n\t\t\t$this->assertFalse( $wp_query->is_404(), $post_type );\n\n\t\t\twp_set_current_user( null );\n\n\t\t}\n\n\t\t$post = $temp;\n\n\t}\n\n\t/**\n\t * Test maybe_redirect_certificates() when a redirect is not expected.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_redirect_certificates_caught() {\n\n\t\tglobal $wp, $wp_query;\n\n\t\t$cert = $this->factory->post->create_and_get( array( 'post_type' => 'llms_my_certificate' ) );\n\n\t\t$wp->request = '/my_certificate/' . $cert->post_name;\n\t\t$wp_query->is_404 = true;\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( sprintf( '%1$s [302] YES', get_permalink( $cert->ID ) ) );\n\n\t\t$this->main->maybe_redirect_certificate();\n\n\t}\n\n\t/**\n\t * Test maybe_redirect_certificates() in scenarios where a redirect is not expected.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_redirect_certificates_not_caught() {\n\n\t\t$this->set_permalink_structure( '/%postname%/' );\n\n\t\t// Assert that the LLMS_Unit_Test_Exception_Redirect is not thrown.\n\t\t$this->expectNotToPerformAssertions();\n\n\t\tglobal $wp, $wp_query;\n\n\t\t// Not a 404.\n\t\t$wp->request = '/fake/url';\n\t\t$wp_query->is_404 = false;\n\t\t$this->main->maybe_redirect_certificate();\n\n\t\t// Is a 404 but url doesn't contain \"/my_certificate/\".\n\t\t$wp_query->is_404 = true;\n\t\t$this->main->maybe_redirect_certificate();\n\n\t\t// Does contain \"/my_certificate/\" but isn't a 404.\n\t\t$wp->request = '/my_certificate/slug';\n\t\t$wp_query->is_404 = false;\n\t\t$this->main->maybe_redirect_certificate();\n\n\t\t// Doesn't redirect because the certificate doesn't exist.\n\t\t$wp->request = '/my_certificate/fake-slug-doesnt-exist';\n\t\t$wp_query->is_404 = true;\n\t\t$this->main->maybe_redirect_certificate();\n\n\t\t// A real post that contains \"/my_certificate/\" but isn't an `llms_my_certificate` post type.\n\t\t// This is something of a dumb test because in this scenario the page would be loaded and not 404 but just in case...\n\t\t$parent = $this->factory->post->create( array( 'post_type' => 'page', 'post_name' => 'my_certificate' ) );\n\t\t$wp->request = wp_parse_url( get_permalink( $parent ), PHP_URL_PATH );\n\t\t$wp_query->is_404 = true;\n\t\t$this->main->maybe_redirect_certificate();\n\n\t\t// The child post.\n\t\t$child = $this->factory->post->create( array( 'post_type' => 'page', 'post_parent' => $parent ) );\n\t\t$wp->request = wp_parse_url( get_permalink( $child ), PHP_URL_PATH );\n\t\t$this->main->maybe_redirect_certificate();\n\n\t\t// Create this scenario: https://github.com/gocodebox/lifterlms/pull/1855#pullrequestreview-804521213\n\t\t$parent_parent = $this->factory->post->create( array( 'post_type' => 'page' ) );\n\t\twp_update_post( array( 'ID' => $parent, 'post_parent' => $parent_parent ) );\n\n\t\t$child_post  = get_post( $child );\n\t\t$parent_post = get_post( $parent_parent );\n\n\t\t$cert = $this->factory->post->create( array( 'post_type' => 'llms_my_certificate', 'post_name' => \"{$parent_post->post_name}{$child_post->post_name}\" ) );\n\n\t\t$wp->request = wp_parse_url( get_permalink( $child ), PHP_URL_PATH );\n\t\t$this->main->maybe_redirect_certificate();\n\n\t\t$this->set_permalink_structure( false );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-quiz-attempt-query.php",
    "content": "<?php\n/**\n * Tests for LLMS_Query_Quiz_Attempt found_results / count_only behavior.\n *\n * @package LifterLMS/Tests\n *\n * @group quizzes\n * @group query\n * @group dbquery\n *\n * @since 10.0.0\n */\nclass LLMS_Test_Quiz_Attempt_Query extends LLMS_UnitTestCase {\n\n\t/**\n\t * Teardown.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_quiz_attempts\" );\n\t}\n\n\t/**\n\t * Create mock quiz attempts for a student.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @param int $count Number of attempts.\n\t * @return array { quiz_id, lesson_id, student_id }\n\t */\n\tprivate function create_attempts( $count = 1 ) {\n\n\t\t$uid     = $this->factory->user->create();\n\t\t$courses = $this->generate_mock_courses( 1, 1, 1, 1, 1 );\n\t\t$course  = llms_get_post( $courses[0] );\n\t\t$lesson  = $course->get_lessons()[0];\n\t\t$lid     = $lesson->get( 'id' );\n\t\t$qid     = $lesson->get( 'quiz' );\n\n\t\tfor ( $i = 0; $i < $count; $i++ ) {\n\t\t\t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid );\n\t\t\t$attempt->save();\n\t\t}\n\n\t\treturn array(\n\t\t\t'quiz_id'    => $qid,\n\t\t\t'lesson_id'  => $lid,\n\t\t\t'student_id' => $uid,\n\t\t);\n\t}\n\n\t/**\n\t * Test found_results and max_pages with pagination.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_found_results_with_pagination() {\n\n\t\t$data = $this->create_attempts( 7 );\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'student_id' => $data['student_id'],\n\t\t\t\t'quiz_id'    => $data['quiz_id'],\n\t\t\t\t'per_page'   => 3,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 7, $query->get_found_results() );\n\t\t$this->assertSame( 3, $query->get_max_pages() );\n\t\t$this->assertSame( 3, $query->get_number_results() );\n\t}\n\n\t/**\n\t * Test no_found_rows skips counting.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_no_found_rows_skips_count() {\n\n\t\t$data = $this->create_attempts( 3 );\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'student_id'    => $data['student_id'],\n\t\t\t\t'quiz_id'       => $data['quiz_id'],\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertTrue( $query->has_results() );\n\t\t$this->assertSame( 0, $query->get_found_results() );\n\t\t$this->assertSame( 0, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * Test count_only returns accurate count.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_count_only() {\n\n\t\t$data = $this->create_attempts( 5 );\n\n\t\t$query = new LLMS_Query_Quiz_Attempt(\n\t\t\tarray(\n\t\t\t\t'student_id' => $data['student_id'],\n\t\t\t\t'quiz_id'    => $data['quiz_id'],\n\t\t\t\t'count_only' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 5, $query->get_count_only_result() );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-rest-fields.php",
    "content": "<?php\n/**\n * Tests for LLMS_REST_Fields class\n *\n * @package LifterLMS/Tests\n *\n * @group rest\n * @group rest_fields\n *\n * @since 6.0.0\n */\nclass LLMS_Test_REST_Fields extends LLMS_REST_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tif ( ! llms_is_block_editor_supported_for_certificates() ) {\n\t\t\t$this->markTestSkipped( 'REST endpoints are not supported for certificates on this version of WordPress.' );\n\t\t}\n\n\t\tparent::set_up();\n\n\t\tarray_map( 'unregister_post_type', array( 'llms_certificate', 'llms_my_certificate' ) );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\tLLMS_Post_Types::register_post_types();\n\n\t\tdo_action( 'rest_api_init' );\n\n\t}\n\n\t/**\n\t * Retrieves a rest route for a given post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param string $post_type A WP_Post_Type name.\n\t * @return string\n\t */\n\tprivate function get_route( $post_type ) {\n\t\treturn \"/wp/v2/{$post_type}\";\n\t}\n\n\t/**\n\t * Test certificate rest fields\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_fields_for_certificates() {\n\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$route = $this->get_route( $post_type );\n\n\t\t\t// Test schema registration.\n\t\t\t$opts  = $this->perform_mock_request( 'OPTIONS', $route )->get_data();\n\t\t\t$props = $opts['schema']['properties'];\n\t\t\t$fields = LLMS_Unit_Test_Util::call_method( new LLMS_REST_Fields(), 'get_fields_for_certificates' );\n\t\t\tforeach ( $fields as $key => $schema ) {\n\t\t\t\t$this->assertArrayHasKey( \"certificate_{$key}\", $props );\n\t\t\t\t$schema['context'] = array( 'view', 'edit' );\n\t\t\t\t$this->assertEquals( $schema, $props[\"certificate_{$key}\"] );\n\t\t\t}\n\n\t\t\t// Create a new certificate with field data: tests the update callback.\n\t\t\t$create = $this->perform_mock_request( 'POST', $route, array(\n\t\t\t\t'certificate_size'        => 'A3',\n\t\t\t\t'certificate_orientation' => 'landscape',\n\t\t\t\t'certificate_background'  => '#000000',\n\t\t\t\t'certificate_margins'     => array( 15, 25, 0, 5.501 ),\n\t\t\t) );\n\t\t\t$this->assertResponseStatusEquals( 201, $create );\n\n\t\t\t// Retrieve the field: tests the get callback.\n\t\t\t$get = $this->perform_mock_request( 'GET', $route . '/' . $create->get_data()['id'] );\n\t\t\t$this->assertResponseStatusEquals( 200, $get );\n\n\t\t\t$data = $get->get_data();\n\t\t\t$this->assertEquals( 'A3', $data['certificate_size'] );\n\t\t\t$this->assertEquals( 297, $data['certificate_width'] );\n\t\t\t$this->assertEquals( 420, $data['certificate_height'] );\n\t\t\t$this->assertEquals( 'mm', $data['certificate_unit'] );\n\t\t\t$this->assertEquals( 'landscape', $data['certificate_orientation'] );\n\t\t\t$this->assertEquals( array( 15, 25, 0, 5.501 ), $data['certificate_margins'] );\n\t\t\t$this->assertEquals( '#000000', $data['certificate_background'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test register_fields_for_certificate_awards()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_fields_for_certificate_awards() {\n\n\t\t$template_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) );\n\n\t\t$route = $this->get_route( 'llms_my_certificate' );\n\n\t\t// Test field registration.\n\t\t$opts  = $this->perform_mock_request( 'OPTIONS', $route )->get_data();\n\t\t$props = $opts['schema']['properties'];\n\n\t\t$this->assertArrayHasKey( 'certificate_template', $props );\n\n\t\t// Create a new certificate with field data: tests the update callback.\n\t\t$create = $this->perform_mock_request( 'POST', $route, array(\n\t\t\t'certificate_template' => $template_id,\n\t\t) );\n\t\t$this->assertResponseStatusEquals( 201, $create );\n\t\t$this->assertEquals( $template_id, $create->get_data()['certificate_template'] );\n\n\t\t$created_route = $route . '/' . $create->get_data()['id'];\n\n\t\t// Validation error.\n\t\t$create_with_error = $this->perform_mock_request( 'POST', $created_route, array(\n\t\t\t'certificate_template' => $this->factory->post->create(),\n\t\t) );\n\t\t$this->assertResponseStatusEquals( 400, $create_with_error );\n\t\t$this->assertResponseCodeEquals( 'rest_invalid_param', $create_with_error );\n\t\t$this->assertArrayHasKey( 'certificate_template', $create_with_error->get_data()['data']['params'] );\n\n\t\t// Remove the template (no error).\n\t\t$update = $this->perform_mock_request( 'POST', $created_route, array(\n\t\t\t'certificate_template' => 0,\n\t\t) );\n\t\t$this->assertResponseStatusEquals( 200, $update );\n\n\t\t// Retrieve the field: tests the get callback.\n\t\t$get = $this->perform_mock_request( 'GET', $created_route );\n\t\t$this->assertResponseStatusEquals( 200, $get );\n\t\t$this->assertEquals( 0, $get->get_data()['certificate_template'] );\n\n\t}\n\n\t/**\n\t * Test register_fields_for_certificate_templates()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_fields_for_certificate_templates() {\n\n\t\t$route = $this->get_route( 'llms_certificate' );\n\n\t\t// Test field registration.\n\t\t$opts  = $this->perform_mock_request( 'OPTIONS', $route )->get_data();\n\t\t$props = $opts['schema']['properties'];\n\n\t\t$this->assertArrayHasKey( 'certificate_title', $props );\n\t\t$this->assertArrayHasKey( 'certificate_sequential_id', $props );\n\n\t\t// Create a new certificate with field data: tests the update callback.\n\t\t$create = $this->perform_mock_request( 'POST', $route, array(\n\t\t\t'certificate_title'         => 'Title',\n\t\t\t'certificate_sequential_id' => 25,\n\t\t) );\n\t\t$this->assertResponseStatusEquals( 201, $create );\n\n\t\t$created_route = $route . '/' . $create->get_data()['id'];\n\n\t\t// Validation error on sequential id.\n\t\t$create_with_error = $this->perform_mock_request( 'POST', $created_route, array(\n\t\t\t'certificate_sequential_id' => 10,\n\t\t) );\n\t\t$this->assertResponseStatusEquals( 400, $create_with_error );\n\t\t$this->assertResponseCodeEquals( 'rest_invalid_param', $create_with_error );\n\t\t$this->assertArrayHasKey( 'certificate_sequential_id', $create_with_error->get_data()['data']['params'] );\n\n\t\t// Retrieve the field: tests the get callback.\n\t\t$get = $this->perform_mock_request( 'GET', $created_route );\n\t\t$this->assertResponseStatusEquals( 200, $get );\n\n\t\t$data = $get->get_data();\n\t\t$this->assertEquals( 'Title', $data['certificate_title'] );\n\t\t$this->assertEquals( 25, $data['certificate_sequential_id'] );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-rest.php",
    "content": "<?php\n/**\n * Test inclusion and initialization of the rest api bundle\n *\n * @package LifterLMS/Tests\n *\n * @group rest\n * @group packages\n *\n * @since 3.36.3\n * @version 3.36.3\n */\nclass LLMS_Test_REST extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test rest package exists and is loaded.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_rest_package_exists() {\n\t\t$this->assertTrue( function_exists( 'LLMS_REST_API' ) );\n\t\t$this->assertTrue( defined( 'LLMS_REST_API_VERSION' ) );\n\t\t$this->assertNotNull( LLMS_REST_API_VERSION );\n\t}\n\n\t/**\n\t * Ensure the REST API initializes.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_api_init() {\n\n\t\t$res = llms_rest_get_api_endpoint_data( '/llms/v1' );\n\t\t$this->assertEquals( 'llms/v1', $res['namespace'] );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-review.php",
    "content": "<?php\n/**\n * Test LLMS_Review\n *\n * @package LifterLMS/Tests\n *\n * @group review\n *\n * @since 7.5.3\n */\nclass LLMS_Test_Review extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test processing a review.\n\t *\n\t * @since 7.5.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_review() {\n\t\t$student = $this->factory->student->create();\n\t\t$course = $this->factory->course->create();\n\n\t\t// enable reviews for course\n\t\tupdate_post_meta( $course, '_llms_reviews_enabled', 'yes' );\n\n\t\tllms_enroll_student( $student, $course );\n\n\t\twp_set_current_user( $student );\n\n\t\t$review_content = array(\n\t\t\t'pageID' => $course,\n\t\t\t'review_text' => 'This is a review',\n\t\t\t'review_title' => 'Great course',\n\t\t\t'llms_review_nonce' => wp_create_nonce( 'llms-review' ),\n\t\t);\n\n\t\t$this->mockPostRequest( $review_content );\n\n\t\t$llms_reviews = new LLMS_Reviews();\n\t\t$llms_reviews->process_review();\n\n\t\t$reviews = get_posts( array(\n\t\t\t'post_type' => 'llms_review',\n\t\t\t'post_status' => 'publish',\n\t\t\t'post_content' => $review_content['review_text'],\n\t\t\t'post_title' => $review_content['review_title'],\n\t\t) );\n\n\t\t$this->assertCount( 1, $reviews );\n\t}\n\n\n\t/**\n\t * Test processing a review with invalid nonce.\n\t *\n\t * @since 7.5.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_review_fails_with_invalid_nonce() {\n\t\t$student = $this->factory->student->create();\n\t\t$course = $this->factory->course->create();\n\n\t\t// enable reviews for course\n\t\tupdate_post_meta( $course, '_llms_reviews_enabled', 'yes' );\n\n\t\tllms_enroll_student( $student, $course );\n\n\t\twp_set_current_user( $student );\n\n\t\t$review_content = array(\n\t\t\t'pageID' => $course,\n\t\t\t'review_text' => 'This is a review',\n\t\t\t'review_title' => 'Great course',\n\t\t\t'llms_review_nonce' => wp_create_nonce( 'fake' ),\n\t\t);\n\n\t\t$this->mockPostRequest( $review_content );\n\n\t\t$llms_reviews = new LLMS_Reviews();\n\t\t$llms_reviews->process_review();\n\n\t\t$reviews = get_posts( array(\n\t\t\t'post_type' => 'llms_review',\n\t\t\t'post_status' => 'publish',\n\t\t\t'post_content' => $review_content['review_text'],\n\t\t\t'post_title' => $review_content['review_title'],\n\t\t) );\n\n\t\t$this->assertCount( 0, $reviews );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-roles.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Custom Post Types\n *\n * @group LLMS_Roles\n *\n * @since 3.13.0\n * @version 4.5.1\n */\nclass LLMS_Test_Roles extends LLMS_UnitTestCase {\n\n\t/**\n\t * Tear down\n\t *\n\t * @since 3.28.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\t$wp_roles = wp_roles();\n\t\tLLMS_Roles::install();\n\t}\n\n\t/**\n\t * test get_all_core_caps() method\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_all_core_caps() {\n\n\t\t$this->assertTrue( is_array( LLMS_Roles::get_all_core_caps() ) );\n\t\t$this->assertTrue( ! empty( LLMS_Roles::get_all_core_caps() ) );\n\n\t}\n\n\t/**\n\t * Test get_roles() method.\n\t *\n\t * @since 3.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_roles() {\n\n\t\t$expect = array(\n\t\t\t'instructor' => __( 'Instructor', 'lifterlms' ),\n\t\t\t'instructors_assistant' => __( 'Instructor\\'s Assistant', 'lifterlms' ),\n\t\t\t'lms_manager' => __( 'LMS Manager', 'lifterlms' ),\n\t\t\t'student' => __( 'Student', 'lifterlms' ),\n\t\t);\n\t\t$this->assertEquals( $expect, LLMS_Roles::get_roles() );\n\n\t}\n\n\t/**\n\t * Test install_roles() method.\n\t *\n\t * @since 3.13.0\n\t * @since 3.34.0 Test for \"view_students\" on instructors.\n\t *\n\t * @return  void\n\t */\n\tpublic function test_install() {\n\n\t\t$wp_roles = wp_roles();\n\n\t\t// Remove first.\n\t\tLLMS_Roles::remove_roles();\n\n\t\t// Install them.\n\t\tLLMS_Roles::install();\n\n\t\t// Ensure all the roles were installed.\n\t\tforeach ( array_keys( LLMS_Roles::get_roles() ) as $role ) {\n\t\t\t$this->assertTrue( $wp_roles->is_role( $role ) );\n\t\t}\n\n\t\t// Test admin caps were installed.\n\t\t$admin = $wp_roles->get_role( 'administrator' );\n\n\t\tforeach ( LLMS_Roles::get_all_core_caps() as $cap ) {\n\t\t\t$this->assertTrue( $admin->has_cap( $cap ) );\n\t\t}\n\n\t\t// Test instructor caps.\n\t\t$instructor = $wp_roles->get_role( 'instructor' );\n\t\tforeach ( LLMS_Roles::get_all_core_caps() as $cap ) {\n\t\t\t$has = $instructor->has_cap( $cap );\n\t\t\tif ( in_array( $cap, array( 'view_lifterlms_reports', 'lifterlms_instructor', 'view_students' ) ) ) {\n\t\t\t\t$this->assertTrue( $has );\n\t\t\t} else {\n\t\t\t\t$this->assertFalse( $has );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test remove_roles() method.\n\t *\n\t * @since 3.13.0\n\t * @since 3.28.0 Unknown.\n\t * @since 4.5.1 Make sure only custom roles are removed from the 'adminitrator' role.\n\t *\n\t * @return void\n\t */\n\tpublic function test_remove_roles() {\n\n\t\t$wp_roles = wp_roles();\n\n\t\t// Remove them.\n\t\tLLMS_Roles::remove_roles();\n\n\t\t// Make sure roles are gone.\n\t\tforeach ( array_keys( LLMS_Roles::get_roles() ) as $role ) {\n\t\t\t$this->assertFalse( $wp_roles->is_role( $role ) );\n\t\t}\n\n\t\t// Test admin custom caps were removed.\n\t\t$admin = $wp_roles->get_role( 'administrator' );\n\t\t$admin_caps = LLMS_Unit_Test_Util::call_method( 'LLMS_Roles', 'get_all_caps', array( 'administrator') );\n\t\t$wp_caps = $admin_caps['wp'];\n\n\t\tforeach ( $admin_caps as $group => $caps ) {\n\t\t\tforeach ( array_keys( $caps ) as $cap ) {\n\t\t\t\tif ( 'wp' === $group  ) {\n\t\t\t\t\t$this->assertTrue( $admin->has_cap( $cap ) );\n\t\t\t\t} else {\n\t\t\t\t\t$this->assertFalse( $admin->has_cap( $cap ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_all_role_names() method.\n\t *\n\t * @since 5.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_all_role_names() {\n\n\t\t$wp_roles = array(\n\t\t\t'administrator'         => 'Administrator',\n\t\t\t'editor'                => 'Editor',\n\t\t\t'author'                => 'Author',\n\t\t\t'contributor'           => 'Contributor',\n\t\t\t'subscriber'            => 'Subscriber',\n\t\t);\n\t\t$llms_roles = array(\n\t\t\t'lms_manager'           => 'LMS Manager',\n\t\t\t'instructor'            => 'Instructor',\n\t\t\t'instructors_assistant' => 'Instructor\\'s Assistant',\n\t\t\t'student'               => 'Student',\n\t\t);\n\n\t\t$expect = array_merge( $wp_roles, $llms_roles );\n\n\t\t$translated_roles = array_combine(\n\t\t\tarray_keys( $expect ),\n\t\t\tarray_map(\n\t\t\t\tfunction( $role_name ) {\n\t\t\t\t\treturn \"Translated {$role_name}\";\n\t\t\t\t},\n\t\t\t\t$expect\n\t\t\t)\n\t\t);\n\n\t\t$translations = array_combine(\n\t\t\tarray_values( $expect ),\n\t\t\tarray_values( $translated_roles )\n\t\t);\n\n\t\t$this->assertEquals( $expect, LLMS_Roles::get_all_role_names() );\n\n\t\t// Simulate a different language.\n\t\t// For wp roles.\n\t\t$gettext_with_context = function( $translation, $text, $context, $domain ) use ( $wp_roles, $translations ) {\n\t\t\tif ( 'User role' === $context && 'default' === $domain && in_array( $text, $wp_roles, true ) ) {\n\t\t\t\treturn $translations[ $text ];\n\t\t\t}\n\t\t\treturn $translation;\n\t\t};\n\t\t// For our roles.\n\t\t$gettext = function( $translation, $text, $domain ) use ( $llms_roles, $translations ) {\n\t\t\tif ( 'lifterlms' === $domain && in_array( $text, $llms_roles, true ) ) {\n\t\t\t\treturn $translations[ $text ];\n\t\t\t}\n\t\t\treturn $translation;\n\t\t};\n\n\t\tadd_filter( 'gettext_with_context', $gettext_with_context, 10, 4 );\n\t\tadd_filter( 'gettext', $gettext, 10, 3 );\n\t\t$this->assertEquals( $translated_roles , LLMS_Roles::get_all_role_names() );\n\t\tremove_filter( 'gettext_with_context', $gettext_with_context, 10, 4 );\n\t\tremove_filter( 'gettext', $gettext, 10, 3 );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-session.php",
    "content": "<?php\n/**\n * Test session class\n *\n * @package LifterLMS/Tests\n *\n * @group session\n * @group sessions\n *\n * @since 4.0.0\n */\nclass LLMS_Test_Session extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup test\n\t *\n\t * @since 4.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Session();\n\n\t}\n\n\t/**\n\t * Retrieve the name of the cookie\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return string\n\t */\n\tprotected function get_cookie_name() {\n\t\treturn LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cookie' );\n\t}\n\n\t/**\n\t * Retrieve the raw cookie value.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_raw_cookie() {\n\t\treturn $this->cookies->get( $this->get_cookie_name() );\n\t}\n\n\t/**\n\t * Test constructor\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_construct_should_init() {\n\n\t\tremove_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) );\n\t\tremove_action( 'wp_logout', array( $this->main, 'destroy' ) );\n\t\tremove_action( 'shutdown', array( $this->main, 'maybe_save_data' ), 20 );\n\n\t\t$this->main = new LLMS_Session();\n\n\t\t$this->assertEquals( 10, has_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) ) );\n\t\t$this->assertEquals( 10, has_action( 'wp_logout', array( $this->main, 'destroy' ) ) );\n\t\t$this->assertEquals( 20, has_action( 'shutdown', array( $this->main, 'maybe_save_data' ) ) );\n\n\t\t$this->assertEquals( sprintf( 'wp_llms_session_%s', COOKIEHASH ), $this->get_cookie_name() );\n\n\t}\n\n\t/**\n\t * Test constructor when we should not initialize.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_construct_should_not_init() {\n\n\t\tremove_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) );\n\t\tremove_action( 'wp_logout', array( $this->main, 'destroy' ) );\n\t\tremove_action( 'shutdown', array( $this->main, 'maybe_save_data' ), 20 );\n\n\t\tadd_filter( 'llms_session_should_init', '__return_false' );\n\t\t$this->main = new LLMS_Session();\n\t\tremove_filter( 'llms_session_should_init', '__return_false' );\n\n\t\t$this->assertEquals( 10, has_action( 'llms_delete_expired_session_data', array( $this->main, 'clean' ) ) );\n\t\t$this->assertFalse( has_action( 'wp_logout', array( $this->main, 'destroy' ) ) );\n\t\t$this->assertFalse( has_action( 'shutdown', array( $this->main, 'maybe_save_data' ) ) );\n\n\t\t$this->assertEquals( sprintf( 'wp_llms_session_%s', COOKIEHASH ), LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cookie' ) );\n\n\t}\n\n\t/**\n\t * Test destroy()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_destroy() {\n\n\t\t$this->main->set( 'somedata', 'isset' );\n\t\t$this->assertTrue( $this->main->save( time() + HOUR_IN_SECONDS ) );\n\n\t\t// Destroyed.\n\t\t$this->assertTrue( $this->main->destroy() );\n\n\t\t// Class properties reset.\n\t\t$this->assertEquals( '', LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'id' ) );\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'data' ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'is_clean' ) );\n\n\t\t// Cookie should be emptied and set to expire.\n\t\t$cookie = $this->get_raw_cookie();\n\n\t\t$this->assertEquals( '', $cookie['value'] );\n\t\t$this->assertTrue( $cookie['expires'] < time() );\n\n\t}\n\n\t/**\n\t * Test get_cookie() when there's no cookie set.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_cookie_not_set() {\n\n\t\t$this->cookies->unset_all();\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) );\n\n\t}\n\n\t/**\n\t * Test get_cookie() when it returns something unexpected\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_cookie_not_string() {\n\n\t\t$this->cookies->set( $this->get_cookie_name(), 1234 );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) );\n\n\t}\n\n\t/**\n\t * Test get_cookie() when it's missing required parts.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_cookie_missing_parts() {\n\n\t\t$this->cookies->set( $this->get_cookie_name(), 'part1' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) );\n\n\t\t$this->cookies->set( $this->get_cookie_name(), 'part1||part2||part3||' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) );\n\n\t}\n\n\t/**\n\t * Test get_cookie() when the hash is invalid\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_cookie_invalid() {\n\n\t\t$this->cookies->set( $this->get_cookie_name(), 'part1||part2||part3||part4|1234' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) );\n\n\t}\n\n\t/**\n\t * Test get_cookie() for a success return\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_cookie() {\n\n\t\t$parts = LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' );\n\n\t\t$this->assertEquals( $this->main->get_id(), $parts[0] );\n\t\t$this->assertEquals( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expires' ), $parts[1] );\n\t\t$this->assertEquals( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expiring' ), $parts[2] );\n\t\t$this->assertTrue( is_string( $parts[3] ) );\n\n\t}\n\n\t/**\n\t * Test init_cookie() when the cookie exists\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_cookie_from_existing() {\n\n\t\t$data = $this->main->set( 'something', 123 );\n\t\t$this->main->save( time() + HOUR_IN_SECONDS );\n\t\t$parts = LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' );\n\n\t\t// Reset everything.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'id', '' );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'expires', 0 );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'expiring', 0 );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'data', array() );\n\n\t\t// Reinit.\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' );\n\n\t\t$this->assertEquals( $parts, LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' ) );\n\t\t$this->assertEquals( $parts[0], $this->main->get_id() );\n\t\t$this->assertEquals( $parts[1], LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expires' ) );\n\t\t$this->assertEquals( $parts[2], LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expiring' ) );\n\t\t$this->assertEquals( 123, $this->main->get( 'something' ) );\n\n\t}\n\n\t/**\n\t * Test init_cookie() when the cookie is expiring\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_cookie_from_existing_expiring() {\n\n\t\t// Expiring is in the past.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'expiring', 0 );\n\n\t\t// Reinit.\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' );\n\n\t\t// Expiring reset to the future.\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'expiring' ) > time() );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' )[2] > time() );\n\n\t}\n\n\t/**\n\t * Test init_cookie() when the user id is to change\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_cookie_from_existing_user_logged_in() {\n\n\t\t$id  = $this->main->get_id();\n\t\t$uid = $this->factory->user->create();\n\n\t\twp_set_current_user( $uid );\n\n\t\t// Reinit.\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' );\n\n\t\t$this->assertEquals( $uid, $this->main->get_id() );\n\t\t$this->assertEquals( $uid, LLMS_Unit_Test_Util::call_method( $this->main, 'get_cookie' )[0] );\n\n\t}\n\n\t/**\n\t * Test init_cookie() when a new cookie is created\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_cookie_new() {\n\n\t\t$original = $this->get_raw_cookie();\n\t\t$this->cookies->unset_all();\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'init_cookie' );\n\t\t$this->assertNotEquals( $original, $this->get_raw_cookie() );\n\n\n\t}\n\n\t/**\n\t * Test\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_save_data_is_clean() {\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'is_clean', true );\n\t\t$this->assertFalse( $this->main->maybe_save_data() );\n\n\t}\n\n\t/**\n\t * Test\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_save_data_is_not_clean() {\n\n\t\t$this->main->set( 'test', 'data' );\n\t\t$this->assertTrue( $this->main->maybe_save_data() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-sessions.php",
    "content": "<?php\n/**\n * Test sessions class\n *\n * @package LifterLMS/Tests\n *\n * @group sessions\n *\n * @since 3.36.0\n * @version 4.5.0\n */\nclass LLMS_Test_Sessions extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.36.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->sessions = LLMS_Sessions::instance();\n\n\n\t}\n\n\t/**\n\t * Test get_open_sessions()\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_open_sessions() {\n\n\t\t$time = time();\n\n\t\t$i = 0;\n\t\twhile ( $i < 5 ) {\n\n\t\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t\t$time += MINUTE_IN_SECONDS;\n\t\t\tllms_tests_mock_current_time( $time );\n\t\t\t$this->sessions->start();\n\t\t\t$time += MINUTE_IN_SECONDS;\n\t\t\tllms_tests_mock_current_time( $time );\n\t\t\t$this->sessions->end_current();\n\n\t\t\t$time += MINUTE_IN_SECONDS;\n\t\t\tllms_tests_mock_current_time( $time );\n\n\t\t\t$this->sessions->start();\n\n\t\t\t$i++;\n\n\t\t}\n\n\t\t$sessions = LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_open_sessions' );\n\t\t$this->assertEquals( 5, count( $sessions ) );\n\n\t\tforeach ( $sessions as $session ) {\n\t\t\t$this->assertEquals( 2, $session->get( 'object_id' ) );\n\t\t\t$this->assertTrue( $this->sessions->is_session_open( $session ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Setup end_idle_sessions()\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_end_idle_sessions() {\n\n\t\t$time = time();\n\n\t\t$started = array();\n\n\t\t$i = 0;\n\t\twhile ( $i < 5 ) {\n\n\t\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t\t$time += MINUTE_IN_SECONDS;\n\t\t\tllms_tests_mock_current_time( $time );\n\t\t\t$started[] = $this->sessions->start();\n\n\t\t\t$i++;\n\n\t\t}\n\n\t\t// It hasn't been long enough.\n\t\t$this->sessions->end_idle_sessions();\n\t\tforeach ( $started as $i => $session ) {\n\t\t\t$this->assertTrue( $this->sessions->is_session_open( $session ) );\n\t\t}\n\n\t\t$this->assertEquals( 4, $i );\n\n\t\t$time += HOUR_IN_SECONDS;\n\t\tllms_tests_mock_current_time( $time );\n\t\t$this->sessions->end_idle_sessions();\n\t\tforeach ( $started as $i => $session ) {\n\t\t\t$this->assertFalse( $this->sessions->is_session_open( $session ) );\n\t\t}\n\n\t\t$this->assertEquals( 4, $i );\n\n\t}\n\n\t/**\n\t * Test end_current()\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_end_current() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$start = $this->sessions->start();\n\n\t\t$end = $this->sessions->end_current();\n\n\t\t$this->assertTrue( is_a( $end, 'LLMS_Event' ) );\n\t\t$this->assertEquals( $start->get( 'actor_id' ), $end->get( 'actor_id' ) );\n\t\t$this->assertEquals( 'session', $end->get( 'event_type' ) );\n\t\t$this->assertEquals( 'end', $end->get( 'event_action' ) );\n\t\t$this->assertEquals( 'session', $end->get( 'object_type' ) );\n\t\t$this->assertEquals( $start->get( 'object_id' ), $end->get( 'object_id' ) );\n\n\t}\n\n\t/**\n\t * Test get_new_session_id()\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_new_session_id() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t$this->assertEquals( 1, LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_new_id' ) );\n\t\t$this->sessions->start();\n\n\t\t$this->assertEquals( 2, LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_new_id' ) );\n\n\t}\n\n\t/**\n\t * Test get_current() when there's no logged in user\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_current_no_user() {\n\n\t\t$this->assertFalse( $this->sessions->get_current() );\n\n\t}\n\n\t/**\n\t * Test get_current() when user has no previous sessions\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_current_no_previous_sessions() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$this->assertFalse( $this->sessions->get_current() );\n\n\t}\n\n\t/**\n\t * Test get_current() when there's an open session\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_current_is_open() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$event = $this->sessions->start();\n\n\t\t$current = $this->sessions->get_current();\n\t\t$this->assertEquals( $event->get( 'id' ), $current->get( 'id' ) );\n\n\t}\n\n\t/**\n\t * Test get_current() when the most recent session is closed\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_current_last_is_closed() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$this->sessions->start();\n\t\t$this->sessions->end_current();\n\n\t\t$this->assertFalse( $this->sessions->get_current() );\n\n\t}\n\n\t/**\n\t * Test get_session_end() when there's no end event for the session\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_session_end_no_end() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$start = $this->sessions->start();\n\n\t\t$this->assertNull( $this->sessions->get_session_end( $start ) );\n\n\t}\n\n\t/**\n\t * Test get_session_end()\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_session_end() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$start = $this->sessions->start();\n\t\t$end = $this->sessions->end_current();\n\n\t\t$test_end = $this->sessions->get_session_end( $start );\n\n\t\t$this->assertTrue( is_a( $test_end, 'LLMS_Event' ) );\n\t\t$this->assertEquals( $end->get( 'id' ), $test_end->get( 'id' ) );\n\n\t}\n\n\t/**\n\t * Test get_session_events()\n\t *\n\t * @since 3.36.0\n\t * @since 3.37.15 Updated to take into account the page.* events removal.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_session_events() {\n\n\t\tadd_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) );\n\t\tllms()->events()->register_events();\n\n\t\t$start_time = time() - HOUR_IN_SECONDS;\n\t\tllms_tests_mock_current_time( $start_time );\n\n\t\t$user = $this->factory->user->create();\n\t\twp_set_current_user( $user );\n\n\t\t// Start session.\n\t\t$start = $this->sessions->start();\n\n\t\tllms_tests_mock_current_time( $start_time + MINUTE_IN_SECONDS );\n\n\t\t// Create events.\n\t\tllms()->events()->record( array(\n\t\t\t'actor_id' => $user,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'load',\n\t\t) );\n\n\t\tllms_tests_mock_current_time( $start_time + ( MINUTE_IN_SECONDS * 2 ) );\n\n\t\tllms()->events()->record( array(\n\t\t\t'actor_id' => $user,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'exit',\n\t\t) );\n\n\t\t// Return those events during an open session.\n\t\t$sessions = $this->sessions->get_session_events( $start );\n\t\t$this->assertEquals( 2, count( $sessions ) );\n\n\t\tforeach ( $sessions as $event ) {\n\t\t\t$this->assertTrue( is_a( $event, 'LLMS_Event' ) );\n\t\t\t$this->assertEquals( $user, $event->get( 'actor_id' ) );\n\t\t\t$this->assertEquals( 1, $event->get( 'object_id' ) );\n\t\t\t$this->assertEquals( 'page', $event->get( 'event_type' ) );\n\t\t\t$this->assertEquals( 'post', $event->get( 'object_type' ) );\n\t\t}\n\n\t\tllms_tests_mock_current_time( $start_time + ( MINUTE_IN_SECONDS * 3 ) );\n\n\t\t// End the session.\n\t\t$this->sessions->end_current();\n\n\t\t// Add a new event (new session)\n\t\tllms()->events()->record( array(\n\t\t\t'actor_id' => $user,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'exit',\n\t\t) );\n\n\t\t// Original session should still only return 2 events.\n\t\t$sessions = $this->sessions->get_session_events( $start );\n\t\t$this->assertEquals( 2, count( $sessions ) );\n\n\t\tremove_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) );\n\n\t}\n\n\t/**\n\t * Test is_session_idle() on an already closed session\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_session_idle_already_closed() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t// Start session.\n\t\t$start = $this->sessions->start();\n\t\t$this->sessions->end_current();\n\n\t\t// This session has already ended.\n\t\t$this->assertFalse( $this->sessions->is_session_idle( $start ) );\n\n\t}\n\n\t/**\n\t * Test is_session_idle() on a session that started less than 30 minutes ago\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_session_idle_started_within_window() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t// Start session.\n\t\t$start = $this->sessions->start();\n\n\t\t$this->assertFalse( $this->sessions->is_session_idle( $start ) );\n\n\t\tllms_tests_mock_current_time( time() + ( 29 * MINUTE_IN_SECONDS ) );\n\t\t$this->assertFalse( $this->sessions->is_session_idle( $start ) );\n\n\t}\n\n\t/**\n\t * Test is_session_idle() for a session that started more than 30 minutes ago and has no events\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_session_idle_old_with_no_events() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t// Start session.\n\t\t$start = $this->sessions->start();\n\n\t\t// Session is older than 30 minutes or older & has no events.\n\t\tllms_tests_mock_current_time( time() + ( 31 * MINUTE_IN_SECONDS ) );\n\t\t$this->assertTrue( $this->sessions->is_session_idle( $start ) );\n\n\t}\n\n\n\t/**\n\t * Test is_session_idle() for a session that started more than 30 minutes ago\n\t * and has at least one active event that's less than 30 minutes old\n\t *\n\t * @since 3.36.0\n\t * @since 3.37.15 Updated to take into account the page.* events removal.\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_session_idle_old_with_events_within_window() {\n\n\t\tadd_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) );\n\t\tllms()->events()->register_events();\n\n\t\t$user = $this->factory->user->create();\n\t\twp_set_current_user( $user );\n\n\t\t// Start session.\n\t\t$start = $this->sessions->start();\n\n\t\tllms_tests_mock_current_time( time() + ( 10 * MINUTE_IN_SECONDS ) );\n\n\t\t// Add a new\n\t\tllms()->events()->record( array(\n\t\t\t'actor_id' => $user,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'exit',\n\t\t) );\n\n\t\tllms_tests_mock_current_time( time() + ( 31 * MINUTE_IN_SECONDS ) );\n\t\t$this->assertFalse( $this->sessions->is_session_idle( $start ) );\n\n\t\tremove_filter( 'llms_get_registered_events', array( $this, 'allow_page_events_for_testing' ) );\n\t}\n\n\n\t/**\n\t * Test is_session_idle() for a session that started more than 30 minutes ago with it's most recent event more than 30 minutes old.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_session_idle_old_with_events_outside_window() {\n\n\t\t$user = $this->factory->user->create();\n\t\twp_set_current_user( $user );\n\n\t\t// Start session.\n\t\t$start = $this->sessions->start();\n\n\t\tllms_tests_mock_current_time( time() + ( 15 * MINUTE_IN_SECONDS ) );\n\n\t\t// Add a new\n\t\tllms()->events()->record( array(\n\t\t\t'actor_id' => $user,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'exit',\n\t\t) );\n\n\t\t// Session is older than 30 minutes and last event within the session is older than 30 mins.\n\t\tllms_tests_mock_current_time( time() + ( 46 * MINUTE_IN_SECONDS ) );\n\t\t$this->assertTrue( $this->sessions->is_session_idle( $start ) );\n\n\t}\n\n\t/**\n\t * Test start() when no user\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_start_no_user() {\n\n\t\t$this->assertFalse( $this->sessions->start() );\n\n\t}\n\n\t/**\n\t * Test is_session_open()\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_session_open() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$start = $this->sessions->start();\n\n\t\t$this->assertTrue( $this->sessions->is_session_open( $start ) );\n\t\t$this->sessions->end_current();\n\n\t\t$this->assertFalse( $this->sessions->is_session_open( $start ) );\n\n\t}\n\n\t/**\n\t * Test start()\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_start() {\n\n\t\t$user =  $this->factory->user->create();\n\t\twp_set_current_user( $user );\n\n\t\t$event = $this->sessions->start();\n\n\t\t$this->assertTrue( is_a( $event, 'LLMS_Event' ) );\n\t\t$this->assertEquals( $user, $event->get( 'actor_id' ) );\n\t\t$this->assertEquals( 'session', $event->get( 'event_type' ) );\n\t\t$this->assertEquals( 'start', $event->get( 'event_action' ) );\n\t\t$this->assertEquals( 'session', $event->get( 'object_type' ) );\n\t\t$this->assertTrue( is_numeric( $event->get( 'object_id' ) ) );\n\n\t}\n\n\t/**\n\t * Test session starts on user login\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_wp_login_action() {\n\n\t\t$user = $this->factory->user->create_and_get(\n\t\t\tarray(\n\t\t\t\t'user_pass' => 'user_pass',\n\t\t\t)\n\t\t);\n\t\t$wp_login_count = did_action( 'wp_login' );\n\n\t\t// Test there's no current session.\n\t\t$this->assertFalse( $this->sessions->get_current() );\n\n\t\t// Simulate wp login that will trigger the `wp_login` action without setting the current user though.\n\t\twp_signon(\n\t\t\tarray(\n\t\t\t\t'user_login'    => $user->user_login,\n\t\t\t\t'user_password' => 'user_pass',\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals( $wp_login_count + 1, did_action( 'wp_login' ) );\n\n\t\t// Set the current user.\n\t\twp_set_current_user( $user->ID );\n\n\t\t$start_session = $this->sessions->get_current();\n\n\t\t// A new session has been created.\n\t\t$this->assertTrue( is_a( $start_session, 'LLMS_Event' ) );\n\n\t\t// And it's the correct one.\n\t\t$this->assertEquals( $user->ID, $start_session->get( 'actor_id' ) );\n\t\t$this->assertEquals( 'session', $start_session->get( 'object_type' ) );\n\t\t$this->assertEquals( 'session', $start_session->get( 'event_type' ) );\n\t\t$this->assertEquals( 'start', $start_session->get( 'event_action' ) );\n\n\t\t// Clean the opened session.\n\t\t$this->sessions->end_current();\n\t}\n\n\t/**\n\t * Test session ends on user logout\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_signout() {\n\n\t\t$user = $this->factory->user->create_and_get(\n\t\t\tarray(\n\t\t\t\t'user_pass' => 'user_pass',\n\t\t\t)\n\t\t);\n\n\t\twp_set_current_user( $user->ID );\n\t\t$start_session = $this->sessions->start();\n\t\t// A new session has been created and it's the current one.\n\t\t$this->assertTrue( is_a( $start_session, 'LLMS_Event' ) );\n\t\t$this->assertTrue( $this->sessions->is_session_open( $start_session ) );\n\n\t\t$current_session = $this->sessions->get_current();\n\t\t$this->assertEquals( $current_session->get( 'id' ), $start_session->get( 'id' ) );\n\n\t\t// Simulate sign out.\n\t\tdo_action( 'clear_auth_cookie' );\n\t\t// No current session.\n\t\t$current_session = $this->sessions->get_current();\n\t\t$this->assertFalse( $current_session );\n\t\t// Previously started session correctly ended.\n\t\t$this->assertFalse( $this->sessions->is_session_open( $start_session ) );\n\n\t}\n\n\t/**\n\t * Allow page events for testing purposes.\n\t *\n\t * @since 3.37.15\n\t *\n\t * @param array $allowed_events Array of allowed events\n\t * @return array\n\t */\n\tpublic function allow_page_events_for_testing( $allowed_events ) {\n\n\t\treturn array_merge(\n\t\t\t$allowed_events,\n\t\t\tarray(\n\t\t\t\t'page.load'  => true,\n\t\t\t\t'page.exit'  => true,\n\t\t\t\t'page.focus' => true,\n\t\t\t\t'page.blur'  => true,\n\t\t\t)\n\t\t);\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-shortcodes.php",
    "content": "<?php\n/**\n * Test LifterLMS Shortcodes\n *\n * @package LifterLMS/Tests\n *\n * @group shortcodes\n *\n * @since 3.4.3\n * @since 3.24.1 Unknown.\n * @since 4.0.0 Add tests for `get_course_id()` method.\n * @since 5.0.0 Don't need to test for password strength enqueue anymore.\n */\nclass LLMS_Test_Shortcodes extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test the private get_course_id() method used by various legacy shortcodes.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_course_id() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'questions' => 1 ) );\n\n\t\t$course_id = $course->get( 'id' );\n\n\t\t// On course.\n\t\t$this->go_to( get_permalink( $course_id ) );\n\t\t$this->assertTrue( is_course() );\n\t\t$this->assertEquals( $course_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Shortcodes', 'get_course_id' ) );\n\n\t\t// On lesson.\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$this->go_to( get_permalink( $lesson->get( 'id' ) ) );\n\t\t$this->assertTrue( is_lesson() );\n\t\t$this->assertEquals( $course_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Shortcodes', 'get_course_id' ) );\n\n\t\t// On quiz.\n\t\t$this->go_to( get_permalink( $lesson->get( 'quiz' ) ) );\n\t\t$this->assertTrue( is_quiz() );\n\t\t$this->assertEquals( $course_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Shortcodes', 'get_course_id' ) );\n\n\t}\n\n\t/**\n\t * Generic tests and a few tests on the abstract\n\t * @return   void\n\t * @since    3.4.3\n\t * @version  3.24.1\n\t */\n\tpublic function test_shortcodes() {\n\n\t\t$shortcodes = array(\n\t\t\t'LLMS_Shortcode_Course_Author',\n\t\t\t'LLMS_Shortcode_Course_Continue',\n\t\t\t'LLMS_Shortcode_Course_Meta_Info',\n\t\t\t'LLMS_Shortcode_Course_Outline',\n\t\t\t'LLMS_Shortcode_Course_Prerequisites',\n\t\t\t'LLMS_Shortcode_Course_Reviews',\n\t\t\t'LLMS_Shortcode_Course_Syllabus',\n\t\t\t'LLMS_Shortcode_Membership_Link',\n\t\t\t'LLMS_Shortcode_Registration',\n\t\t);\n\n\t\tforeach ( $shortcodes as $class ) {\n\n\t\t\t$obj = $class::instance();\n\t\t\t$this->assertTrue( shortcode_exists( $obj->tag ) );\n\t\t\t$this->assertTrue( is_a( $obj, 'LLMS_Shortcode' ) );\n\t\t\t$this->assertTrue( ! empty( $obj->tag ) );\n\t\t\t$this->assertTrue( is_string( $obj->output() ) );\n\t\t\t$this->assertTrue( is_array( $obj->get_attributes() ) );\n\t\t\t$this->assertTrue( is_string( $obj->get_content() ) );\n\n\t\t}\n\n\t\t$this->assertClassHasStaticAttribute( '_instances', 'LLMS_Shortcode' );\n\n\t}\n\n\t/**\n\t * Test the registration shortcode\n\t *\n\t * @since 3.4.3\n\t * @since 4.4.0 Use `LLMS_Assets::is_inline_enqueued()` in favor of deprecated `LLMS_Frontend_Assets::is_inline_script_enqueued()`.\n\t * @since 5.0.0 Don't need to test for password strength enqueue anymore.\n\t * @since 5.3.3 Use `assertStringContains()` in favor of `assertContains()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_registration() {\n\n\t\t// our output should enqueue this\n\t\twp_dequeue_script( 'password-strength-meter' );\n\n\t\t$obj = LLMS_Shortcode_Registration::instance();\n\n\t\t// when logged out, there should be html content\n\t\t$this->assertStringContains( 'llms-new-person-form-wrapper', $obj->output() );\n\n\t\t// no html when logged in\n\t\t$user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $user_id );\n\t\t$this->assertEmpty( $obj->output() );\n\n\t}\n\n\t/**\n\t * Test lifterlms_membership_link shortcode\n\t *\n\t * @since 3.4.3\n\t * @since 5.3.3 Use `assertStringContains()` in favor of `assertContains()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_membership_link() {\n\n\t\t// create a membership that we can use for linking\n\t\t$mid = $this->factory->post->create( array(\n\t\t\t'post_title' => 'Test Membership',\n\t\t\t'post_type' => 'llms_membership',\n\t\t) );\n\n\t\t$obj = LLMS_Shortcode_Membership_Link::instance();\n\n\t\t// test default settings\n\t\t$this->assertStringContains( get_permalink( $mid ), $obj->output( array( 'id' => $mid ) ) );\n\t\t$this->assertStringContains( get_the_title( $mid ), $obj->output( array( 'id' => $mid ) ) );\n\n\t\t$this->assertEquals( $mid, $obj->get_attribute( 'id' ) );\n\n\t\t// check non default content\n\t\t$this->assertStringContains( 'Alternate Text', $obj->output( array( 'id' => $mid ), 'Alternate Text' ) );\n\t\t$this->assertEquals( 'Alternate Text', $obj->get_content( 'Alternate Text' ) );\n\n\t}\n\n\t/**\n\t * Tests that the shortcodes are initialized before the WordPress 'init' action hook calls any other LifterLMS callbacks.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_shortcodes_initialized_early() {\n\n\t\tglobal $wp_filter;\n\n\t\t$shortcodes_init_is_found = false;\n\t\tforeach ( $wp_filter['init'][10] as $idx => $callback ) {\n\n\t\t\t// $idx is a unique ID that for static methods will be class_name . '::' . method_name.\n\t\t\tif ( 'LLMS_Shortcodes::init' === $idx ) {\n\t\t\t\t$shortcodes_init_is_found = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( is_array( $callback['function'] ) ) {\n\t\t\t\t$class = is_object( $callback['function'][0] ) ? get_class( $callback['function'][0] ) : $callback['function'][0];\n\t\t\t\t$function = \"{$class}::{$callback['function'][1]}\";\n\t\t\t} else {\n\t\t\t\t$function = $callback['function'];\n\t\t\t}\n\n\t\t\tif ( 0 === strpos( $function, 'LLMS_' ) ) {\n\t\t\t\t$this->assertTrue(\n\t\t\t\t\t$shortcodes_init_is_found,\n\t\t\t\t\t\"Should find LLMS_Shortcodes::init callback before $function.\"\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-site.php",
    "content": "<?php\n/**\n * Tests for LLMS_Site\n *\n * @package LifterLMS/Tests\n *\n * @group site\n *\n * @since 3.7.4\n */\nclass LLMS_Test_Site extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test clear_lock_url() function\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_clear_lock_url() {\n\n\t\tupdate_option( 'llms_site_url', 'http://mockurl.tld/' );\n\t\tLLMS_Site::clear_lock_url();\n\t\t$this->assertEquals( '', get_option( 'llms_site_url' ) );\n\n\t}\n\n\t/**\n\t * Test check_status() method\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_check_status() {\n\n\t\t$actions  = did_action( 'llms_site_clone_detected' );\n\t\t$original = get_site_url();\n\n\t\t// Not a clone.\n\t\t$this->assertFalse( LLMS_Site::check_status() );\n\n\t\t// Simulate the site being cloned.\n\t\tupdate_option( 'siteurl', 'http://fakeurl.tld' );\n\n\t\t$this->assertTrue( LLMS_Site::check_status() );\n\t\t$this->assertSame( ++$actions, did_action( 'llms_site_clone_detected' ) );\n\n\t\t// Site has been ignored.\n\t\tupdate_option( 'llms_site_url_ignore', 'yes' );\n\t\t$this->assertFalse( LLMS_Site::check_status() );\n\n\t\t// Restore URL.\n\t\tupdate_option( 'siteurl', $original );\n\n\t}\n\n\t/**\n\t * Test lock url getter and setter functions\n\t *\n\t * @since 3.8.0\n\t * @since 4.12.0 Added urls with \"www\".\n\t * @since 5.9.0 Pass an explicit integer to `substr_replace()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_set_lock_url() {\n\n\t\t$urls = array(\n\t\t\t'https://whatever.com',\n\t\t\t'http://whatever.com',\n\t\t\t'https://www.whatever.com',\n\t\t\t'http://www.whatever.com',\n\t\t\t'https://w.com',\n\t\t\t'https://whatever-with-a-dash.net',\n\t\t\t'http://wh.at',\n\t\t\t'http://wah.tld',\n\t\t\t'http://waht.tld',\n\t\t);\n\n\t\tforeach ( $urls as $url ) {\n\n\t\t\tupdate_option( 'siteurl', $url );\n\n\t\t\t$site_url = get_site_url();\n\n\t\t\t// This is what the lock url should be.\n\t\t\t$lock_url = substr_replace( $site_url, LLMS_Site::$lock_string, intval( strlen( $site_url ) / 2 ), 0 );\n\n\t\t\t// Make sure they match.\n\t\t\t$this->assertEquals( $lock_url, LLMS_Site::get_lock_url() );\n\n\t\t\t// Save it.\n\t\t\tLLMS_Site::set_lock_url();\n\n\t\t\t// Make sure it saves the right option.\n\t\t\t$this->assertEquals( $lock_url, get_option( 'llms_site_url' ) );\n\n\t\t\t// This should match the original URL.\n\t\t\t$this->assertEquals( $site_url, LLMS_Site::get_url() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test feature getter and setter functions\n\t *\n\t * @since 3.8.0\n\t * @since 4.12.0 Test against feature constants.\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_set_features() {\n\n\t\t// Should return an array of defaults even when option doesnt exist.\n\t\tdelete_option( 'llms_site_get_features' );\n\t\t$this->assertTrue( is_array( LLMS_Site::get_features() ) );\n\n\t\t// Fake feature always returns false.\n\t\t$this->assertFalse( LLMS_Site::get_feature( 'mock_feature' ) );\n\n\t\t// Test getters/setters with a real feature.\n\t\tLLMS_Site::update_feature( 'recurring_payments', true );\n\t\t$this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) );\n\n\t\tLLMS_Site::update_feature( 'recurring_payments', false );\n\t\t$this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) );\n\n\t\t// Constant not set.\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( 'LLMS_Site', 'get_feature_constant', array( 'recurring_payments' ) ) );\n\t\t$this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) );\n\n\t\t// Constant is set.\n\t\tllms_maybe_define_constant( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS', true );\n\t\t$this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) );\n\n\t}\n\n\n\t/**\n\t * Test is_clone() function\n\t *\n\t * @since 3.7.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_clone() {\n\n\t\t$original = get_site_url();\n\n\t\t// Not a clone because the url is the lock url.\n\t\t$this->assertFalse( LLMS_Site::is_clone() );\n\n\t\t// The url has changed.\n\t\tupdate_option( 'siteurl', 'http://fakeurl.tld' );\n\t\t$this->assertTrue( LLMS_Site::is_clone() );\n\n\t\t// Change it back to the original.\n\t\tupdate_option( 'siteurl', $original );\n\t\t$this->assertFalse( LLMS_Site::is_clone() );\n\n\t\t// Change the schema (should not be identified as a clone).\n\t\tupdate_option( 'siteurl', set_url_scheme( $original, 'https' ) );\n\t\t$this->assertFalse( LLMS_Site::is_clone() );\n\n\t}\n\n\t/**\n\t * Test is_clone() when using a constant set to `true`.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_clone_constant_true() {\n\n\t\t// Not a clone.\n\t\t$this->assertFalse( LLMS_Site::is_clone() );\n\n\t\tdefine( 'LLMS_SITE_IS_CLONE', true );\n\t\t$this->assertTrue( LLMS_Site::is_clone() );\n\n\t}\n\n\t/**\n\t * Test is_clone() when using a constant set to `false`.\n\t *\n\t * @since 4.13.0\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_clone_constant_false() {\n\n\t\t$original = get_site_url();\n\n\t\t// Is a clone.\n\t\tupdate_option( 'siteurl', 'http://fakeurl.tld' );\n\t\t$this->assertTrue( LLMS_Site::is_clone() );\n\n\t\tdefine( 'LLMS_SITE_IS_CLONE', false );\n\t\t$this->assertFalse( LLMS_Site::is_clone() );\n\n\t\tupdate_option( 'siteurl', $original );\n\n\t}\n\n\t/**\n\t * Test is_clone_ignored() function\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_clone_ignored() {\n\n\t\t$this->assertFalse( LLMS_Site::is_clone_ignored() );\n\n\t\tupdate_option( 'llms_site_url_ignore', 'yes' );\n\t\t$this->assertTrue( LLMS_Site::is_clone_ignored() );\n\n\t\tupdate_option( 'llms_site_url_ignore', 'no' );\n\t\t$this->assertFalse( LLMS_Site::is_clone_ignored() );\n\n\t\tupdate_option( 'llms_site_url_ignore', 'mock' );\n\t\t$this->assertFalse( LLMS_Site::is_clone_ignored() ) ;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-staging.php",
    "content": "<?php\n/**\n * Test LLMS_Staging class\n *\n * @package LifterLMS/Tests\n *\n * @group staging\n *\n * @since 4.12.0\n */\nclass LLMS_Test_Staging extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\t}\n\n\t/**\n\t * Removes actions added by the `init()` method (so that we can test the `init()` method)\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tprivate function remove_init_actions() {\n\t\tremove_action( 'llms_site_clone_detected', array( 'LLMS_Staging', 'clone_detected' ), 10 );\n\t\tremove_action( 'admin_init', array( 'LLMS_Staging', 'handle_staging_notice_actions' ), 10 );\n\t\tremove_action( 'admin_menu', array( 'LLMS_Staging', 'menu_warning' ), 10 );\n\t}\n\n\t/**\n\t * Test init() actions when no recurring feature constant is set\n\t *\n\t * @since 4.13.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init() {\n\n\t\t$this->remove_init_actions();\n\n\t\tLLMS_Staging::init();\n\n\t\t$this->assertEquals( 10, has_action( 'llms_site_clone_detected', array( 'LLMS_Staging', 'clone_detected' ) ) );\n\t\t$this->assertEquals( 10, has_action( 'admin_init', array( 'LLMS_Staging', 'handle_staging_notice_actions' ) ) );\n\n\t\t$this->assertEquals( 10, has_action( 'admin_menu', array( 'LLMS_Staging', 'menu_warning' ) ) );\n\n\t}\n\n\t/**\n\t * Test init() actions when a recurring feature constant is set\n\t *\n\t * @since 4.13.0\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_with_constant() {\n\n\t\t$this->remove_init_actions();\n\n\t\tdefine( 'LLMS_SITE_FEATURE_RECURRING_PAYMENTS', true );\n\n\t\tLLMS_Staging::init();\n\n\t\t$this->assertFalse( has_action( 'llms_site_clone_detected', array( 'LLMS_Staging', 'clone_detected' ) ) );\n\t\t$this->assertFalse( has_action( 'admin_init', array( 'LLMS_Staging', 'handle_staging_notice_actions' ) ) );\n\n\t\t$this->assertEquals( 10, has_action( 'admin_menu', array( 'LLMS_Staging', 'menu_warning' ) ) );\n\n\t}\n\n\t/**\n\t * Test init() actions when a recurring feature constant is set\n\t *\n\t * @since 4.13.0\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_clone_site_feature_cascade() {\n\n\t\tdefine( 'LLMS_SITE_IS_CLONE', true );\n\n\t\tLLMS_Staging::init();\n\n\t\t$this->assertFalse( LLMS_SITE_FEATURE_RECURRING_PAYMENTS );\n\n\t}\n\n\t/**\n\t * Test clone_detected()\n\t *\n\t * @since 4.12.0\n\t * @since 4.13.0 Add tests for all potential conditions.\n\t *\n\t * @return void\n\t */\n\tpublic function test_clone_detected() {\n\n\t\tLLMS_Site::update_feature( 'recurring_payments', true );\n\n\t\t// Not admin panel.\n\t\tLLMS_Staging::clone_detected();\n\t\t$this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) );\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) );\n\n\t\t// Admin panel but not admin.\n\t\tset_current_screen( 'admin.php' );\n\t\tLLMS_Staging::clone_detected();\n\t\t$this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) );\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) );\n\n\t\t// Admin panel and admin but doing an ajax request.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tadd_filter( 'wp_doing_ajax', '__return_true' );\n\t\tLLMS_Staging::clone_detected();\n\t\t$this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) );\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) );\n\n\t\t// All good.\n\t\tremove_filter( 'wp_doing_ajax', '__return_true' );\n\t\tLLMS_Staging::clone_detected();\n\t\t$this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) );\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) );\n\t\tLLMS_Admin_Notices::delete_notice( 'maybe-staging' );\n\n\t\t// Return to front.\n\t\tset_current_screen( 'front' );\n\n\t}\n\n\t/**\n\t * Test handle_staging_notice_actions() when the method isn't called\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_staging_notice_actions_not_called() {\n\n\t\t$this->assertNull( LLMS_Staging::handle_staging_notice_actions() );\n\n\t}\n\n\t/**\n\t * Test handle_staging_notice_actions() with an invalid nonce.\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation.\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_staging_notice_actions_invalid_nonce() {\n\n\t\t$this->expectException( 'WPDieException' );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'llms-staging-status' => 'enable',\n\t\t\t'_llms_staging_nonce' => 'fake',\n\t\t) );\n\n\t\tLLMS_Staging::handle_staging_notice_actions();\n\n\t}\n\n\t/**\n\t * Test handle_staging_notice_actions() with an invalid user.\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Use `expectException()` in favor of deprecated `@expectedException` annotation.\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_staging_notice_actions_invalid_user() {\n\n\t\t$this->expectException( 'WPDieException' );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'llms-staging-status' => 'enable',\n\t\t\t'_llms_staging_nonce' => wp_create_nonce( 'llms_staging_status' ),\n\t\t) );\n\n\t\tLLMS_Staging::handle_staging_notice_actions();\n\n\t}\n\n\t/**\n\t * Test handle_staging_notice_actions() when enabling recurring payments\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_staging_notice_actions_enable() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$_SERVER['HTTP_REFERER'] = 'http://example.tld/wp-admin/?page=whatever';\n\t\t$original = get_site_url();\n\t\tupdate_option( 'siteurl', 'http://fakeurl.tld' );\n\t\tLLMS_Site::update_feature( 'recurring_payments', false );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'llms-staging-status' => 'enable',\n\t\t\t'_llms_staging_nonce' => wp_create_nonce( 'llms_staging_status' ),\n\t\t) );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( $_SERVER['HTTP_REFERER'] . ' [302] YES' );\n\n\t\ttry {\n\n\t\t\tLLMS_Staging::handle_staging_notice_actions();\n\n\t\t} catch( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\t$this->assertEquals( get_option( 'llms_site_url' ), LLMS_Site::get_lock_url() );\n\t\t\t$this->assertTrue( LLMS_Site::get_feature( 'recurring_payments' ) );\n\t\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) );\n\n\t\t\tupdate_option( 'siteurl', $original );\n\t\t\tunset( $_SERVER['HTTP_REFERER'] );\n\n\t\t\tthrow $exception;\n\t\t}\n\n\t}\n\n\t/**\n\t * Test handle_staging_notice_actions() when enabling recurring payments\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_staging_notice_actions_disable() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$_SERVER['HTTP_REFERER'] = 'http://example.tld/wp-admin/?page=whatever';\n\t\t$original = get_site_url();\n\t\tupdate_option( 'siteurl', 'http://fakeurl.tld' );\n\t\tLLMS_Site::update_feature( 'recurring_payments', true );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'llms-staging-status' => 'disable',\n\t\t\t'_llms_staging_nonce' => wp_create_nonce( 'llms_staging_status' ),\n\t\t) );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( $_SERVER['HTTP_REFERER'] . ' [302] YES' );\n\n\t\ttry {\n\n\t\t\tLLMS_Staging::handle_staging_notice_actions();\n\n\t\t} catch( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\t$this->assertEquals( '', get_option( 'llms_site_url' ) );\n\t\t\t$this->assertTrue( LLMS_Site::is_clone_ignored() );\n\t\t\t$this->assertFalse( LLMS_Site::get_feature( 'recurring_payments' ) );\n\t\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) );\n\n\t\t\tupdate_option( 'siteurl', $original );\n\t\t\tunset( $_SERVER['HTTP_REFERER'] );\n\n\t\t\tthrow $exception;\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the menu_warning() method\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_menu_warning() {\n\n\t\t$mock_menu = array(\n\t\t\tarray(\n\t\t\t\t'Dashboard',\n\t\t\t\t'read',\n\t\t\t\t'index.php',\n\t\t\t\t'',\n\t\t\t\t'menu-top menu-top-first menu-icon-dashboard',\n\t\t\t\t'menu-dashboard',\n\t\t\t\t'dashicons-dashboard',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'Orders',\n\t\t\t\t'edit_posts',\n\t\t\t\t'edit.php?post_type=llms_order',\n\t\t\t\t'',\n\t\t\t\t'menu-top menu-icon-llms_order',\n\t\t\t\t'menu-posts-llms_order',\n\t\t\t\t'dashicons-cart',\n\t\t\t),\n\t\t);\n\n\t\tglobal $menu;\n\t\t$menu = $mock_menu;\n\n\t\tLLMS_Site::update_feature( 'recurring_payments', true );\n\t\tLLMS_Staging::menu_warning();\n\t\t$this->assertSame( $mock_menu, $menu );\n\n\n\t\tLLMS_Site::update_feature( 'recurring_payments', false );\n\t\tLLMS_Staging::menu_warning();\n\t\t$this->assertSame( $mock_menu[0], $menu[0] );\n\n\t\t$mock_menu[1][0] .= LLMS_Unit_Test_Util::call_method( 'LLMS_Staging', 'get_menu_warning_bubble' );\n\t\t$this->assertSame( $mock_menu[1], $menu[1] );\n\n\t}\n\n\t/**\n\t * Test notice() method\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_notice() {\n\n\t\tLLMS_Admin_Notices::delete_notice( 'maybe-staging' );\n\t\tLLMS_Staging::notice();\n\t\t$this->assertTrue( LLMS_Admin_Notices::has_notice( 'maybe-staging' ) );\n\t\tLLMS_Admin_Notices::delete_notice( 'maybe-staging' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-student-query.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Install Class\n *\n * @group student_query\n *\n * @since 3.3.1\n */\nclass LLMS_Test_Student_Query extends LLMS_UnitTestCase {\n\n\t/**\n\t * Create a new query for use in these tests\n\t * @param    array      $args  args to pass to the query\n\t * @return   obj\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tprivate function query( $args = array() ) {\n\t\treturn new LLMS_Student_Query( $args );\n\t}\n\n\t/**\n\t * Test get() and set() functions\n\t *\n\t * @since 3.4.0\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @return void\n\t */\n\tpublic function test_getters_setters() {\n\n\t\t$args = array(\n\t\t\t'page' => 2,\n\t\t\t'per_page' => 25,\n\t\t\t'post_id' => 1234,\n\t\t\t'search' => 'a search string',\n\t\t\t'sort' => array(\n\t\t\t\t'id' => 'ASC',\n\t\t\t),\n\t\t\t'suppress_filters' => true,\n\t\t\t'statuses' => array(\n\t\t\t\t'enrolled', 'expired'\n\t\t\t),\n\t\t);\n\n\t\t$query = $this->query();\n\n\t\tforeach ( $args as $key => $val ) {\n\n\t\t\t$query->set( $key, $val );\n\t\t\t$this->assertEquals( $args[ $key ], $query->get( $key ) );\n\n\t\t\t// Test defaults.\n\t\t\tLLMS_Unit_Test_Util::set_private_property( $query, 'query_vars', array() );\n\t\t\t$this->assertEquals( 'default_val', $query->get( $key, 'default_val' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test some real queries\n\t *\n\t * @since 3.13.0\n\t * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_students() {\n\n\t\t$course_id = $this->generate_mock_courses( 1, 1, 1, 0 )[0];\n\n\t\t$students = $this->factory->user->create_many( 25, array( 'role' => 'student' ) );\n\t\tforeach ( $students as $sid ) {\n\t\t\tllms_enroll_student( $sid, $course_id, 'testing' );\n\t\t}\n\n\t\t// 25 students enrolled\n\t\t$query = $this->query( array(\n\t\t\t'post_id' => $course_id,\n\t\t\t'per_page' => 10,\n\t\t) );\n\n\t\t$this->assertEquals( 25, $query->get_found_results() );\n\t\t$this->assertEquals( 10, $query->get_number_results() );\n\t\t$this->assertEquals( 3, $query->get_max_pages() );\n\n\t\tsleep( 1 ); // sleep because timestamps can't be the same for the next queries to work correctly\n\n\t\t// unenroll 10 students & results should stay the same\n\t\tforeach ( $query->get_students() as $student ) {\n\t\t\t$student->unenroll( $course_id, 'testing' );\n\t\t}\n\n\t\t// check for expired from any courses\n\t\t$query = $this->query( array(\n\t\t\t'per_page' => 10,\n\t\t\t'statuses' => 'expired',\n\t\t) );\n\t\t$this->assertEquals( 10, $query->get_found_results() );\n\n\t\t// check for any status again\n\t\t$query = $this->query( array(\n\t\t\t'post_id' => $course_id,\n\t\t\t'per_page' => 10,\n\t\t) );\n\t\t$this->assertEquals( 25, $query->get_found_results() );\n\t\t$this->assertEquals( 10, $query->get_number_results() );\n\t\t$this->assertEquals( 3, $query->get_max_pages() );\n\n\t\t// check for enrolled only\n\t\t$query = $this->query( array(\n\t\t\t'post_id' => $course_id,\n\t\t\t'per_page' => 10,\n\t\t\t'statuses' => 'enrolled',\n\t\t) );\n\t\t$this->assertEquals( 15, $query->get_found_results() );\n\t\t$this->assertEquals( 10, $query->get_number_results() );\n\t\t$this->assertEquals( 2, $query->get_max_pages() );\n\n\n\t\t// second course\n\t\t$course_id2 = $this->generate_mock_courses( 1, 1, 1, 0 )[0];\n\t\t$students2 = $this->factory->user->create_many( 25, array( 'role' => 'student' ) );\n\t\tforeach ( array_merge( $students, $students2 ) as $sid ) {\n\t\t\tllms_enroll_student( $sid, $course_id2, 'testing' );\n\t\t}\n\n\t\t// check for enrolled only\n\t\t$query = $this->query( array(\n\t\t\t'post_id' => array( $course_id, $course_id2 ),\n\t\t\t'per_page' => 10,\n\t\t\t// 'statuses' => 'enrolled',\n\t\t) );\n\t\t$this->assertEquals( 50, $query->get_found_results() );\n\t\t$this->assertEquals( 10, $query->get_number_results() );\n\t\t$this->assertEquals( 5, $query->get_max_pages() );\n\n\t\t// more students who aren't enrolled\n\t\t$students3 = $this->factory->user->create_many( 25, array( 'role' => 'student' ) );\n\n\t\t// anything in any course\n\t\t$query = $this->query( array(\n\t\t\t'per_page' => 10,\n\t\t) );\n\t\t$this->assertEquals( 50, $query->get_found_results() );\n\n\t\t// cancelled in any course (shouldn't have anything here)\n\t\t$query = $this->query( array(\n\t\t\t'per_page' => 10,\n\t\t\t'statuses' => 'cancelled',\n\t\t) );\n\t\t$this->assertEquals( 0, $query->get_found_results() );\n\n\n\t\t// test some searches\n\t\t$query = $this->query( array(\n\t\t\t'search' => 'No Results Found Plz'\n\t\t) );\n\t\t$this->assertEquals( 0, $query->get_found_results() );\n\n\t\t// should hit all the mock users\n\t\t$query = $this->query( array(\n\t\t\t'search' => 'user_'\n\t\t) );\n\t\t$this->assertEquals( 50, $query->get_found_results() );\n\n\n\t\tupdate_user_meta( $students2[5], 'first_name', 'testymcname' );\n\t\t$query = $this->query( array(\n\t\t\t'search' => 'testymcname'\n\t\t) );\n\t\t$this->assertEquals( 1, $query->get_found_results() );\n\t\t$this->assertEquals( $students2[5], $query->get_students()[0]->get_id() );\n\n\t}\n\n\t/**\n\t * Test the parse_setup_args() function\n\t * @return   void\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function test_parse_setup_args() {\n\n\t\t$query = $this->query();\n\t\t$this->assertEquals( array_keys( llms_get_enrollment_statuses() ), $query->get( 'statuses' ) );\n\n\t\t// ensure valid string is converted to array\n\t\t$query = $this->query( array( 'statuses' => 'enrolled' ) );\n\t\t$this->assertEquals( array( 'enrolled' ), $query->get( 'statuses' ) );\n\n\t\t// ensure invalid status is removed\n\t\t$query = $this->query( array( 'statuses' => array( 'ooboi', 'enrolled' ) ) );\n\t\t$this->assertFalse( in_array( 'ooboi', $query->get( 'statuses' ) ) );\n\n\t\t// ensure at least one status is returned\n\t\t$query = $this->query( array( 'statuses' => array( 'ooboi', 'fake' ) ) );\n\t\t$this->assertGreaterThanOrEqual( 1, count( $query->get( 'statuses' ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-template-functions.php",
    "content": "<?php\n/**\n * Tests for template functions\n *\n * @group functions_templates\n *\n * @since 3.15.0\n * @version 3.37.0\n */\nclass LLMS_Functions_Templates extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test lifterlms_course_continue_button() func\n\t *\n\t * @since 3.15.0\n\t *\n\t * @return   void\n\t */\n\tpublic function test_lifterlms_course_continue_button() {\n\n\t\tglobal $post;\n\t\t$func = 'lifterlms_course_continue_button';\n\n\t\t// student to use\n\t\t$student = $this->get_mock_student();\n\n\t\t// course to use\n\t\t$course_id = $this->generate_mock_courses()[0];\n\t\t$course = llms_get_post( $course_id );\n\n\t\t// blog post to test globals against\n\t\t$post_id = $this->factory->post->create( array(\n\t\t\t'post_title' => 'Test Post',\n\t\t) );\n\n\n\t\t// call function with no parameters (using only defaults)\n\t\t// no student and no post set right now\n\t\t$this->assertEmpty( $this->get_output( $func ) );\n\n\t\t// set the global post to be a blog post\n\t\t$post = get_post( $post_id );\n\n\t\t// call function with no parameters (using only defaults)\n\t\t// post is a blog post & no student\n\t\t$this->assertEmpty( $this->get_output( $func ) );\n\n\t\t// set global to be a course but still no student\n\t\t$post = get_post( $course_id );\n\t\t$this->assertEmpty( $this->get_output( $func ) );\n\n\t\t// set the current student (should display a continue button)\n\t\twp_set_current_user( $student->get_id() );\n\n\t\t// student setup but no enrollment\n\t\t$this->assertEmpty( $this->get_output( $func ) );\n\n\t\t// enroll student\n\t\tllms_enroll_student( $student->get_id(), $course_id );\n\n\t\t// 0 progress, \"Get Started\" text displays in button\n\t\t$this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Get Started' ) ) );\n\n\t\t// Progress > 0, \"Continue\" text displays in button\n\t\t$this->complete_courses_for_student( $student->get_id(), array( $course_id ), 85 );\n\t\t$this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Continue' ) ) );\n\n\t\t// 100% progress, \"Course Complete\" text displays\n\t\t$this->complete_courses_for_student( $student->get_id(), array( $course_id ), 100 );\n\t\t$this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Course Complete' ) ) );\n\n\t\t// use a lesson, same result as last\n\t\t$post = get_post( $course->get_lessons( 'ids' )[0] );\n\t\t$this->assertTrue( ( false !== strpos( $this->get_output( $func ), 'Course Complete' ) ) );\n\n\t\t// reset global\n\t\t$post = null;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-template-loader.php",
    "content": "<?php\n/**\n * Test LLMS_Template_Loader\n *\n * @package LifterLMS/Tests\n *\n * @group template_loader\n *\n * @since 3.41.1\n * @since 6.0.0 Added tests for the block loader.\n * @since 6.4.0 Updated tests on single restricted content template loading.\n */\nclass LLMS_Test_Template_Loader extends LLMS_UnitTestCase {\n\n\t/**\n\t * Mock restriction id when calling `mock_page_restricted()`.\n\t *\n\t * @var integer\n\t */\n\tprivate $mock_page_restricted_id = 987;\n\n\t/**\n\t * Setup test case.\n\t *\n\t * @since 3.41.1\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Template_Loader();\n\n\t}\n\n\t/**\n\t * Callback for testing custom restrictions applied through a filter.\n\t *\n\t * @since 3.41.1\n\t * @since 4.10.1 Use `$this->mock_page_restricted_id` for the restriction_id to allow easy customization of the mocked data.\n\t *\n\t * @param array $restrictions Restriction data array from `llms_page_restricted()`.\n\t * @param int   $post_id      WP_Post ID.\n\t * @return array\n\t */\n\tpublic function mock_page_restricted( $restrictions, $post_id  ) {\n\n\t\t$restrictions['is_restricted']  = true;\n\t\t$restrictions['reason']         = 'mock';\n\t\t$restrictions['restriction_id'] = $this->mock_page_restricted_id;\n\n\t\treturn $restrictions;\n\n\t}\n\n\t/**\n\t * Retrieve a WP_Post object for restriction-related tests.\n\t *\n\t * @since 3.41.1\n\t *\n\t * @param string $post_type Post type to be created.\n\t * @return WP_Post\n\t */\n\tprotected function get_post_for_restrictions( $post_type = 'post' ) {\n\t\treturn $this->factory->post->create_and_get( array(\n\t\t\t'post_type'    => $post_type,\n\t\t\t'post_content' => 'content',\n\t\t\t'post_excerpt' => 'excerpt',\n\t\t) );\n\t}\n\n\t/**\n\t * Assertion helper for restriction-related tests.\n\t *\n\t * @since 3.41.1\n\t *\n\t * @param WP_Post $post    Post object\n\t * @param string  $content Expected post_content string.\n\t * @param string  $excerpt Expected post_excerpt string.\n\t * @return void\n\t */\n\tprotected function assertContentEquals( $post, $content = 'content', $excerpt = 'excerpt' ) {\n\n\t\t$this->assertEquals( $content, $post->post_content );\n\t\t$this->assertEquals( $excerpt, $post->post_excerpt );\n\n\t}\n\n\t/**\n\t * Test maybe_restrict_post_content(): for a skipped post type.\n\t *\n\t * @since 3.41.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_restrict_post_content_skipped_post_type() {\n\n\t\tglobal $wp_query;\n\n\t\t$post = $this->get_post_for_restrictions( 'course' );\n\n\t\t$this->main->maybe_restrict_post_content( $post, $wp_query );\n\n\t\t$this->assertContentEquals( $post );\n\n\t}\n\n\t/**\n\t * Test maybe_restrict_post_content(): for a valid post type that's not restricted\n\t *\n\t * @since 3.41.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_restrict_post_content_not_restricted() {\n\n\t\tglobal $wp_query, $post;\n\n\t\t$post = $this->get_post_for_restrictions();\n\n\t\t$this->main->maybe_restrict_post_content( $post, $wp_query );\n\n\t\t$this->assertContentEquals( $post );\n\n\t}\n\n\t/**\n\t * Test maybe_restrict_post_content(): for a post restricted by a membership (when not accessible by the user)\n\t *\n\t * @since 3.41.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_restrict_post_content_restricted_by_membership_not_accessible() {\n\n\t\tglobal $wp_query, $post;\n\n\t\t$membership = llms_get_post( $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t) ) );\n\n\t\t$membership->set( 'restriction_add_notice', 'yes' );\n\t\t$membership->set( 'restriction_notice', 'no access.' );\n\n\t\t$post = $this->get_post_for_restrictions();\n\n\t\tupdate_post_meta( $post->ID, '_llms_restricted_levels', array( $membership->get( 'id' ) ) );\n\t\tupdate_post_meta( $post->ID, '_llms_is_restricted', 'yes' );\n\n\t\t$this->main->maybe_restrict_post_content( $post, $wp_query );\n\n\t\t$this->assertContentEquals( $post, 'no access.', 'no access.' );\n\n\t}\n\n\t/**\n\t * Test maybe_restrict_post_content(): for a post restricted by a membership that is accessible by the user\n\t *\n\t * @since 3.41.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_restrict_post_content_restricted_by_membership_is_accessible() {\n\n\t\tglobal $wp_query, $post;\n\n\t\t$membership = llms_get_post( $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t) ) );\n\n\t\t$membership->set( 'restriction_add_notice', 'yes' );\n\t\t$membership->set( 'restriction_notice', 'no access.' );\n\n\t\t$post = $this->get_post_for_restrictions();\n\n\t\tupdate_post_meta( $post->ID, '_llms_restricted_levels', array( $membership->get( 'id' ) ) );\n\t\tupdate_post_meta( $post->ID, '_llms_is_restricted', 'yes' );\n\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $membership->get( 'id' ) );\n\t\twp_set_current_user( $student->get( 'id' ) );\n\n\t\t$this->main->maybe_restrict_post_content( $post, $wp_query );\n\n\t\t$this->assertContentEquals( $post );\n\n\t}\n\n\t/**\n\t * Test maybe_restrict_post_content(): for a custom restriction applied via filter by a 3rd party.\n\t *\n\t * @since 3.41.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_restrict_post_content_restricted_by_other() {\n\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10, 2 );\n\n\t\tglobal $wp_query, $post;\n\n\t\t$post = $this->get_post_for_restrictions();\n\n\t\t$this->main->maybe_restrict_post_content( $post, $wp_query );\n\n\t\t$this->assertContentEquals( $post, 'This content is restricted', 'This content is restricted' );\n\n\t\tremove_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10 );\n\n\t}\n\n\t/**\n\t * Test template_loader() with a screen we don't care about modifying\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_default() {\n\n\t\t$this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) );\n\n\t}\n\n\t/**\n\t * Test template_loader() on the blog (home) page.\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_is_home() {\n\n\t\t// Mock `llms_page_restricted()` to have a sitewide membership restriction.\n\t\t$handler = function( $results ) {\n\t\t\t$results['reason'] = 'sitewide_membership';\n\t\t\t$results['is_restricted'] = true;\n\t\t\treturn $results;\n\t\t};\n\n\t\t// Mock `is_home()` so it looks like we're on the blog post page.\n\t\tglobal $wp_query;\n\t\t$temp = $wp_query->is_home;\n\t\t$wp_query->is_home = true;\n\n\t\t// No restrictions.\n\t\t$this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) );\n\t\t$this->assertSame( 0, did_action( 'lifterlms_content_restricted' ) );\n\t\t$this->assertSame( 0, did_action( 'llms_content_restricted_by_sitewide_membership' ) );\n\t\t$this->assertFalse( has_action( 'loop_start', 'llms_print_notices' ) );\n\n\t\t// Has restrictions.\n\t\tadd_filter( 'llms_page_restricted', $handler );\n\t\t$this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) );\n\t\t$this->assertSame( 1, did_action( 'lifterlms_content_restricted' ) );\n\t\t$this->assertSame( 1, did_action( 'llms_content_restricted_by_sitewide_membership' ) );\n\t\t$this->assertEquals( 5, has_action( 'loop_start', 'llms_print_notices' ) );\n\n\t\t// Reset.\n\t\t$wp_query->is_home = $temp;\n\t\tremove_filter( 'llms_page_restricted', $handler );\n\n\t}\n\n\t/**\n\t * Test template_loader() for restricted pages.\n\t *\n\t * @since 4.10.1\n\t * @since 6.4.0 Updated to reflect changes in the `template_loader()` method for single restricted content template.\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_page_is_restricted() {\n\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10, 2 );\n\n\t\t// Modify the template & fire actions.\n\t\t// `template_loader()` returns the original template for single restricted content.\n\t\t$this->assertEquals( 'mock.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) );\n\t\t// And defers the single restricted content template redirect at `template_include|100`.\n\t\t$this->assertSame( 100, has_filter( 'template_include', array( $this->main, 'maybe_force_php_template' ) ) );\n\t\t// `maybe_force_php_template()` returns the single restricted content template.\n\t\t$this->assertEquals( 'single-no-access.php', basename( $this->main->maybe_force_php_template( '/html/wp-content/theme/atheme/mock.php' ) ) );\n\t\t$this->assertSame( 1, did_action( 'lifterlms_content_restricted' ) );\n\t\t$this->assertSame( 1, did_action( 'llms_content_restricted_by_mock' ) );\n\n\t\t// Courses and memberships return the original template (but still fire actions).\n\t\tglobal $post;\n\t\tforeach ( array( 'course', 'llms_membership' ) as $i => $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\t\t\t$this->mock_page_restricted_id = $post->ID;\n\n\t\t\t$this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) );\n\t\t\t$this->assertSame( $i + 2, did_action( 'lifterlms_content_restricted' ) );\n\t\t\t$this->assertSame( $i + 2, did_action( 'llms_content_restricted_by_mock' ) );\n\n\t\t}\n\n\t\tremove_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10 );\n\n\t}\n\n\t/**\n\t * Test block_template_loader() for restricted pages.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_template_loader_page_is_restricted() {\n\n\t\t! function_exists( 'wp_is_block_theme' ) && $this->markTestSkipped( 'FSE not available.' );\n\n\t\tadd_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'mock_page_restricted' ), 10, 2 );\n\n\t\t// Modify the template & fire actions.\n\t\t// Check we're going to load the expected block template.\n\t\t$this->assertEquals(\n\t\t\tllms()->block_templates()->add_llms_block_templates(\n\t\t\t\tarray(),\n\t\t\t\tarray( 'slug__in' => array( LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . 'single-no-access' ) )\n\t\t\t),\n\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' )\n\t\t);\n\t\t// Check we're going to prevent the loading of the PHP template.\n\t\t$this->assertTrue( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t$this->assertEquals( 'template-canvas.php', $this->main->template_loader( 'template-canvas.php' ) );\n\t\tremove_filter( 'llms_force_php_template_loading', '__return_false' );\n\n\t\t$this->assertSame( 1, did_action( 'lifterlms_content_restricted' ) );\n\t\t$this->assertSame( 1, did_action( 'llms_content_restricted_by_mock' ) );\n\n\t\t// Courses and memberships return the original template (but still fire actions).\n\t\tglobal $post;\n\t\tforeach ( array( 'course', 'llms_membership' ) as $i => $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\t\t\t$this->mock_page_restricted_id = $post->ID;\n\n\t\t\t// Check we're not to going a specific lifterlms block template.\n\t\t\t$this->assertEquals(\n\t\t\t\tarray(),\n\t\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' ),\n\t\t\t\t$post_type\n\t\t\t);\n\n\t\t\t// Check we're not going to prevent the loading of the PHP template.\n\t\t\t$this->assertFalse( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ), $post_type );\n\t\t\t// But still the default template is loaded by the template loader.\n\t\t\t$this->assertEquals( 'template-canvas.php', $this->main->template_loader( 'template-canvas.php' ), $post_type );\n\n\t\t\t$this->assertSame( $i + 2, did_action( 'lifterlms_content_restricted' ), $post_type );\n\t\t\t$this->assertSame( $i + 2, did_action( 'llms_content_restricted_by_mock' ), $post_type );\n\n\t\t}\n\n\t\tremove_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t}\n\n\t/**\n\t * Test template_loader() with the course catalog.\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_courses() {\n\n\t\t// Post type archive.\n\t\t$this->go_to( get_post_type_archive_link( 'course' ) );\n\t\t$this->assertEquals( 'archive-course.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) );\n\n\t\t// Check the course catalog page.\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'courses' ) ) );\n\t\t$this->assertEquals( 'archive-course.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) );\n\n\t}\n\n\t/**\n\t * Test block_template_loader() on courses archives with a block theme.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_template_loader_for_courses() {\n\n\t\t! function_exists( 'wp_is_block_theme' ) && $this->markTestSkipped( 'FSE not available.' );\n\n\t\tadd_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t\t// Post type archive.\n\t\t$this->go_to( get_post_type_archive_link( 'course' ) );\n\t\t// Check we're going to load the expected block template.\n\t\t$this->assertEquals(\n\t\t\tllms()->block_templates()->add_llms_block_templates(\n\t\t\t\tarray(),\n\t\t\t\tarray( 'slug__in' => array( LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . 'archive-course' ) )\n\t\t\t),\n\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' )\n\t\t);\n\t\t// Check we're going to prevent the loading of the PHP template.\n\t\t$this->assertTrue( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t$this->assertEquals( 'template-canvas.php', $this->main->template_loader( 'template-canvas.php' ) );\n\t\tremove_filter( 'llms_force_php_template_loading', '__return_false' );\n\n\t\t// Check the course catalog page.\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'courses' ) ) );\n\t\t// Check we're going to load the expected block template.\n\t\t$this->assertEquals(\n\t\t\tllms()->block_templates()->add_llms_block_templates(\n\t\t\t\tarray(),\n\t\t\t\tarray( 'slug__in' => array( LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . 'archive-course' ) )\n\t\t\t),\n\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' )\n\t\t);\n\t\t// Check we're going to prevent the loading of the PHP template.\n\t\t$this->assertTrue( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t$this->assertEquals( 'template-canvas.php', $this->main->template_loader( 'template-canvas.php' ) );\n\t\tremove_filter( 'llms_force_php_template_loading', '__return_false' );\n\n\t\tremove_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t}\n\n\t/**\n\t * Test template_loader() with the membership catalog.\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_memberships() {\n\n\t\t// Post type archive.\n\t\t$this->go_to( get_post_type_archive_link( 'llms_membership' ) );\n\t\t$this->assertEquals( 'archive-llms_membership.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) );\n\n\t\t// Check the membership catalog page.\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'memberships' ) ) );\n\t\t$this->assertEquals( 'archive-llms_membership.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) );\n\n\t}\n\n\t/**\n\t * Test block_template_loader() on membership archives with a block theme.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_template_loader_for_memberships() {\n\n\t\t! function_exists( 'wp_is_block_theme' ) && $this->markTestSkipped( 'FSE not available.' );\n\n\t\tadd_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t\t// Post type archive.\n\t\t$this->go_to( get_post_type_archive_link( 'llms_membership' ) );\n\t\t// Check we're going to load the expected block template.\n\t\t$this->assertEquals(\n\t\t\tllms()->block_templates()->add_llms_block_templates(\n\t\t\t\tarray(),\n\t\t\t\tarray( 'slug__in' => array( LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . 'archive-llms_membership' ) )\n\t\t\t),\n\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' )\n\t\t);\n\t\t// Check we're going to prevent the loading of the PHP template.\n\t\t$this->assertTrue( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t$this->assertEquals( 'template-canvas.php', $this->main->template_loader( 'template-canvas.php' ) );\n\t\tremove_filter( 'llms_force_php_template_loading', '__return_false' );\n\n\t\t// Check the membership catalog page.\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'memberships' ) ) );\n\t\t// Check we're going to load the expected block template.\n\t\t$this->assertEquals(\n\t\t\tllms()->block_templates()->add_llms_block_templates(\n\t\t\t\tarray(),\n\t\t\t\tarray( 'slug__in' => array( LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . 'archive-llms_membership' ) )\n\t\t\t),\n\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' )\n\t\t);\n\t\t// Check we're going to prevent the loading of the PHP template.\n\t\t$this->assertTrue( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t$this->assertEquals( 'template-canvas.php', $this->main->template_loader( 'template-canvas.php' ) );\n\t\tremove_filter( 'llms_force_php_template_loading', '__return_false' );\n\n\t\tremove_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t}\n\n\t/**\n\t * Test template_loader() on custom taxonomy archives.\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_for_taxonomies() {\n\n\t\tforeach ( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) as $tax ) {\n\n\t\t\t$term = wp_create_term( 'mock-' . $tax, $tax );\n\t\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t\t// $this->assertTrue( is_course_taxonomy() );\n\t\t\t$this->assertEquals( sprintf( 'taxonomy-%s.php', $tax ), basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test block_template_loader() on custom taxonomy archives with a block theme.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_template_loader_for_taxonomies() {\n\n\t\t! function_exists( 'wp_is_block_theme' ) && $this->markTestSkipped( 'FSE not available.' );\n\n\t\tadd_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t\tforeach ( array( 'course_cat', 'course_tag', 'course_difficulty', 'course_track', 'membership_tag', 'membership_cat' ) as $tax ) {\n\n\t\t\t$this->assertFalse( has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t\t$term = wp_create_term( 'mock-' . $tax, $tax );\n\t\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t\t// Check we're going to load the expected block template.\n\t\t\t$this->assertEquals(\n\t\t\t\tllms()->block_templates()->add_llms_block_templates(\n\t\t\t\t\tarray(),\n\t\t\t\t\tarray( 'slug__in' => array( LLMS_Block_Templates::LLMS_BLOCK_TEMPLATES_PREFIX . 'taxonomy-' . $tax ) )\n\t\t\t\t),\n\t\t\t\t$this->main->block_template_loader( array(), array( 'archive' ), 'wp_template' ),\n\t\t\t\t$tax\n\t\t\t);\n\t\t\t// Check we're going to prevent the loading of the PHP template.\n\t\t\t$this->assertTrue( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t\t$this->assertEquals( 'template-canvas.php', $this->main->template_loader( 'template-canvas.php' ) );\n\t\t\tremove_filter( 'llms_force_php_template_loading', '__return_false' );\n\n\t\t}\n\n\t\tremove_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t}\n\n\t/**\n\t * Test template_loader() on certificate pages.\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_certificates() {\n\n\t\tglobal $post, $wp_query;\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\t\t\t$wp_query->queried_object = $post;\n\t\t\t$wp_query->is_singular    = true;\n\t\t\t$this->assertEquals( 'single-certificate.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ), $post_type );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test block_template_loader() on certificate pages with block theme.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_template_loader_for_certificates() {\n\n\t\t! function_exists( 'wp_is_block_theme' ) && $this->markTestSkipped( 'FSE not available.' );\n\n\t\tadd_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t\tglobal $wp_query, $post;\n\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\t\t\t$wp_query->queried_object = $post;\n\t\t\t$wp_query->is_singular    = true;\n\n\t\t\t$this->assertFalse( has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\n\t\t\t// Check we're not going to load a certificate block template, it doesn't exist.\n\t\t\t$this->assertEquals(\n\t\t\t\tarray(),\n\t\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' ),\n\t\t\t\t$post_type\n\t\t\t);\n\n\t\t\t// Check we're not going to prevent the loading of the PHP template.\n\t\t\t$this->assertFalse( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\t\t\t// The PHP template is loaded instead.\n\t\t\t$this->assertEquals( 'single-certificate.php', basename( $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) ), $post_type );\n\n\t\t}\n\n\t\tremove_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t}\n\n\t/**\n\t * Test template_loader() with a default unrestricted post type\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_template_loader_default_post_type() {\n\n\t\tglobal $post;\n\t\t$post = $this->factory->post->create_and_get();\n\n\t\t// Not touched.\n\t\t$this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) );\n\t\t$this->assertSame( 0, did_action( 'lifterlms_content_restricted' ) );\n\t\t$this->assertSame( 0, did_action( 'llms_content_restricted_by_sitewide_membership' ) );\n\t\t$this->assertFalse( has_action( 'loop_start', 'llms_print_notices' ) );\n\n\t}\n\n\t/**\n\t * Test block_template_loader() with a default unrestricted post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_template_loader_default_post_type() {\n\n\t\t! function_exists( 'wp_is_block_theme' ) && $this->markTestSkipped( 'FSE not available.' );\n\n\t\tadd_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t\tglobal $post;\n\t\t$post = $this->factory->post->create_and_get();\n\n\t\t// Not touched.\n\n\t\t$this->assertFalse( has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\n\t\t// Check we're not going to load a certificate block template, it doesn't exist.\n\t\t$this->assertEquals(\n\t\t\tarray(),\n\t\t\t$this->main->block_template_loader( array(), array(), 'wp_template' )\n\t\t);\n\n\t\t// Check we're not going to prevent the loading of the PHP template.\n\t\t$this->assertFalse( (bool) has_filter( 'llms_force_php_template_loading', '__return_false' ) );\n\n\t\t$this->assertEquals( '/html/wp-content/theme/atheme/mock.php', $this->main->template_loader( '/html/wp-content/theme/atheme/mock.php' ) );\n\t\t$this->assertSame( 0, did_action( 'lifterlms_content_restricted' ) );\n\t\t$this->assertSame( 0, did_action( 'llms_content_restricted_by_sitewide_membership' ) );\n\t\t$this->assertFalse( has_action( 'loop_start', 'llms_print_notices' ) );\n\n\t\tremove_filter( 'llms_is_block_theme', '__return_true', 11 );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-user-postmeta-query.php",
    "content": "<?php\n/**\n * Tests for LLMS_Query_User_Postmeta found_results / count_only behavior.\n *\n * @package LifterLMS/Tests\n *\n * @group query\n * @group dbquery\n *\n * @since 10.0.0\n */\nclass LLMS_Test_User_Postmeta_Query extends LLMS_UnitTestCase {\n\n\t/**\n\t * Teardown.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_user_postmeta\" );\n\t}\n\n\t/**\n\t * Insert mock user postmeta rows.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @param int $count   Number of rows.\n\t * @param int $user_id User ID.\n\t * @param int $post_id Post ID.\n\t * @return void\n\t */\n\tprivate function insert_rows( $count, $user_id, $post_id ) {\n\n\t\tglobal $wpdb;\n\n\t\tfor ( $i = 0; $i < $count; $i++ ) {\n\t\t\t$wpdb->insert(\n\t\t\t\t\"{$wpdb->prefix}lifterlms_user_postmeta\",\n\t\t\t\tarray(\n\t\t\t\t\t'user_id'      => $user_id,\n\t\t\t\t\t'post_id'      => $post_id,\n\t\t\t\t\t'meta_key'     => '_status',\n\t\t\t\t\t'meta_value'   => 'enrolled',\n\t\t\t\t\t'updated_date' => current_time( 'mysql' ),\n\t\t\t\t),\n\t\t\t\tarray( '%d', '%d', '%s', '%s', '%s' )\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Test found_results and max_pages with pagination.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_found_results_with_pagination() {\n\n\t\t$uid = $this->factory->user->create();\n\t\t$pid = $this->factory->post->create();\n\t\t$this->insert_rows( 9, $uid, $pid );\n\n\t\t$query = new LLMS_Query_User_Postmeta(\n\t\t\tarray(\n\t\t\t\t'user_id'  => $uid,\n\t\t\t\t'post_id'  => $pid,\n\t\t\t\t'per_page' => 4,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 9, $query->get_found_results() );\n\t\t$this->assertSame( 3, $query->get_max_pages() );\n\t\t$this->assertSame( 4, $query->get_number_results() );\n\t}\n\n\t/**\n\t * Test no_found_rows skips counting.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_no_found_rows_skips_count() {\n\n\t\t$uid = $this->factory->user->create();\n\t\t$pid = $this->factory->post->create();\n\t\t$this->insert_rows( 3, $uid, $pid );\n\n\t\t$query = new LLMS_Query_User_Postmeta(\n\t\t\tarray(\n\t\t\t\t'user_id'       => $uid,\n\t\t\t\t'post_id'       => $pid,\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertTrue( $query->has_results() );\n\t\t$this->assertSame( 0, $query->get_found_results() );\n\t\t$this->assertSame( 0, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * Test count_only returns accurate count.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_count_only() {\n\n\t\t$uid = $this->factory->user->create();\n\t\t$pid = $this->factory->post->create();\n\t\t$this->insert_rows( 6, $uid, $pid );\n\n\t\t$query = new LLMS_Query_User_Postmeta(\n\t\t\tarray(\n\t\t\t\t'user_id'    => $uid,\n\t\t\t\t'post_id'    => $pid,\n\t\t\t\t'count_only' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 6, $query->get_count_only_result() );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/class-llms-test-view-manager.php",
    "content": "<?php\n/**\n * Test view manager\n *\n * @package LifterLMS/Tests\n *\n * @group view_manager\n *\n * @since 4.5.1\n */\nclass LLMS_Test_View_Manager extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 4.5.1\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_View_Manager();\n\n\t}\n\n\t/**\n\t * Initiate (and retrieve) an instance of WP_Admin_Bar\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return WP_Admin_Bar\n\t */\n\tprivate function get_admin_bar() {\n\n\t\tadd_filter( 'show_admin_bar', '__return_true' );\n\t\t_wp_admin_bar_init();\n\n\t\tglobal $wp_admin_bar;\n\n\t\tremove_filter( 'show_admin_bar', '__return_true' );\n\n\t\treturn $wp_admin_bar;\n\n\t}\n\n\t/**\n\t * Mock `$_GET` data to control the return of `get_view()`.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @param string $role Requested view role.\n\t * @return void\n\t */\n\tpublic function mock_view_data( $role ) {\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'view_nonce'   => wp_create_nonce( 'llms-view-as' ),\n\t\t\t'llms-view-as' => $role,\n\t\t) );\n\n\t}\n\n\t/**\n\t * Test constructor\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test__construct() {\n\n\t\t// Remove existing action.\n\t\tremove_action( 'init', array( $this->main, 'add_actions' ) );\n\t\t$this->assertFalse( has_action( 'init', array( $this->main, 'add_actions' ) ) );\n\n\t\t// Reinit.\n\t\t$this->main = new LLMS_View_Manager();\n\t\t$this->assertEquals( 10, has_action( 'init', array( $this->main, 'add_actions' ) ) );\n\n\t}\n\n\t/**\n\t * Test constructor when a pending order is being created.\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test__construct_pending_order() {\n\n\t\t// Remove existing action.\n\t\tremove_action( 'init', array( $this->main, 'add_actions' ) );\n\t\t$this->assertFalse( has_action( 'init', array( $this->main, 'add_actions' ) ) );\n\n\t\t// Reinit.\n\t\t$this->mockPostRequest( array(\n\t\t\t'action' => 'create_pending_order',\n\t\t) );\n\t\t$this->main = new LLMS_View_Manager();\n\t\t$this->assertFalse( has_action( 'init', array( $this->main, 'add_actions' ) ) );\n\t}\n\n\t/**\n\t * Test add_menu_items() when the display manager shouldn't be displayed.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_menu_items_no_display() {\n\n\t\t$bar = $this->get_admin_bar();\n\n\t\t$this->main->add_menu_items( $bar );\n\n\t\t$this->assertNull( $bar->get_nodes() );\n\n\t}\n\n\t/**\n\t * Test add_menu_items()\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_menu_items() {\n\n\t\t$bar = $this->get_admin_bar();\n\n\t\tadd_filter( 'llms_view_manager_should_display', '__return_true' );\n\n\t\t$this->main->add_menu_items( $bar );\n\n\t\t$this->assertEquals( array( 'llms-view-as-menu', 'llms-view-as--visitor', 'llms-view-as--student' ), array_keys( $bar->get_nodes() ) );\n\n\t\tremove_filter( 'llms_view_manager_should_display', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test get_url() with a supplied URL and additional QS args.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_url_with_url() {\n\n\t\t$url = parse_url( LLMS_View_Manager::get_url(  'visitor', 'https://mock.tld/test?whatever=0', array( 'more' => 'yes' ) ) );\n\n\t\t// Make sure URL is preserved properly.\n\t\t$this->assertEquals( 'https', $url['scheme'] );\n\t\t$this->assertEquals( 'mock.tld', $url['host'] );\n\t\t$this->assertEquals( '/test', $url['path'] );\n\n\t\t// Check query vars.\n\t\tparse_str( $url['query'], $qs );\n\t\t$this->assertEquals( '0', $qs['whatever'] );\n\t\t$this->assertEquals( 'yes', $qs['more'] );\n\t\t$this->assertEquals( 'visitor', $qs['llms-view-as'] );\n\n\t\t// Ensure generated nonce is valid.\n\t\t$this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) );\n\n\t}\n\n\t/**\n\t * Test get_url() with a supplied URL and additional QS args.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_url_without_url() {\n\n\t\t$_SERVER['REQUEST_URI'] = 'https://fake.tld';\n\n\t\t$url = parse_url( LLMS_View_Manager::get_url(  'student' ) );\n\n\t\t// Make sure URL is preserved properly.\n\t\t$this->assertEquals( 'https', $url['scheme'] );\n\t\t$this->assertEquals( 'fake.tld', $url['host'] );\n\n\t\t// Check query vars.\n\t\tparse_str( $url['query'], $qs );\n\t\t$this->assertEquals( 'student', $qs['llms-view-as'] );\n\n\t\t// Ensure generated nonce is valid.\n\t\t$this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) );\n\n\t\t$_SERVER['REQUEST_URI'] = '';\n\n\t}\n\n\t/**\n\t * Test get_view() when there's nonce errors.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_view_nonce_error() {\n\n\t\t// Nothing set.\n\t\t$this->assertEquals( 'self', LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) );\n\n\t\t// Invalid nonce.\n\t\t$this->mockGetRequest( array(\n\t\t\t'view_nonce'   => 'fake',\n\t\t\t'llms-view-as' => 'student',\n\t\t) );\n\t\t$this->assertEquals( 'self', LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) );\n\n\t}\n\n\t/**\n\t * Test get_view() with an invalid view.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_view_invalid_view() {\n\n\t\t$this->mock_view_data( 'fake' );\n\t\t$this->assertEquals( 'self', LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) );\n\n\t}\n\n\t/**\n\t * Test get_view() with valid data.\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_view() {\n\n\t\tforeach ( array_keys( LLMS_Unit_Test_Util::call_method( $this->main, 'get_views' ) ) as $view ) {\n\n\t\t\t$this->mock_view_data( $view );\n\t\t\t$this->assertEquals( $view, LLMS_Unit_Test_Util::call_method( $this->main, 'get_view' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test modify_dashboard()\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_dashboard() {\n\n\t\t// Unchanged when viewing as self.\n\t\t$this->assertNull( $this->main->modify_dashboard( null ) );\n\n\t\t// Visitors can't load the dashboard (they see forms).\n\t\t$this->mock_view_data( 'visitor' );\n\t\t$this->assertFalse( $this->main->modify_dashboard( null ) );\n\n\t\t// Students see the dashboard.\n\t\t$this->mock_view_data( 'student' );\n\t\t$this->assertTrue( $this->main->modify_dashboard( null ) );\n\n\t}\n\n\t/**\n\t * Test modify_course_open().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_course_open() {\n\n\t\t$course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\n\t\t$this->mock_view_data( 'visitor' );\n\t\t$this->assertFalse( $this->main->modify_course_open( false, $course ) );\n\n\t\t// Logged out user.\n\t\t$this->mock_view_data( 'self' );\n\t\t$this->assertFalse( $this->main->modify_course_open( false, $course ) );\n\n\t\t// Admin can do it.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertTrue( $this->main->modify_course_open( false, $course ) );\n\n\t\t// Instructor can't.\n\t\t$instructor = $this->factory->user->create( array( 'role' => 'instructor' ) );\n\t\twp_set_current_user( $instructor );\n\t\t$this->assertFalse( $this->main->modify_course_open( false, $course ) );\n\n\t\t$course->set_instructors( array(\n\t\t\tarray(\n\t\t\t\t'id' => $instructor,\n\t\t\t)\n\t\t) );\n\t\t$this->assertTrue( $this->main->modify_course_open( false, $course ) );\n\n\t}\n\n\t/**\n\t * Test modify_restrictions().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_restrictions() {\n\n\t\t$course = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$mock_restriction = array(\n\t\t\t'content_id'     => $course,\n\t\t\t'is_restricted'  => true,\n\t\t\t'reason'         => 'enrollment_course',\n\t\t\t'restriction_id' => $course,\n\t\t);\n\n\t\t$expected_success = wp_parse_args( array(\n\t\t\t'is_restricted' => false,\n\t\t\t'reason'        => 'role-access',\n\t\t), $mock_restriction );\n\n\t\t$this->mock_view_data( 'visitor' );\n\t\t$this->assertEquals( $mock_restriction, $this->main->modify_restrictions( $mock_restriction ) );\n\n\t\t// No user.\n\t\t$this->mock_view_data( 'self' );\n\t\t$this->assertEquals( $mock_restriction, $this->main->modify_restrictions( $mock_restriction ) );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertEquals( $expected_success, $this->main->modify_restrictions( $mock_restriction ) );\n\n\t\t$instructor = $this->factory->user->create( array( 'role' => 'instructor' ) );\n\t\twp_set_current_user( $instructor );\n\t\t$this->assertEquals( $mock_restriction, $this->main->modify_restrictions( $mock_restriction ) );\n\n\t\tllms_get_post( $course )->set_instructors( array(\n\t\t\tarray(\n\t\t\t\t'id' => $instructor,\n\t\t\t)\n\t\t) );\n\n\t\t$this->assertEquals( $expected_success, $this->main->modify_restrictions( $mock_restriction ) );\n\n\t}\n\n\t/**\n\t * Test should_display() when viewing valid post types with a valid user\n\t *\n\t * @since 4.5.1\n\t * @since 5.9.0 Add tests for instructors.\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_on_valid_post_types() {\n\n\t\tglobal $post;\n\n\t\t$admin     = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\t$instructor = $this->factory->user->create( array( 'role' => 'instructor' ) );\n\n\t\tforeach ( array( 'course', 'lesson', 'llms_membership', 'llms_quiz' ) as $post_type ) {\n\n\t\t\twp_set_current_user( $admin );\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\t\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t\t\twp_set_current_user( $instructor );\n\t\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t\t\tif ( in_array( $post_type, array( 'course', 'llms_membership' ), true ) ) {\n\t\t\t\tllms_get_post( $post )->set_instructors( array(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'id' => $instructor,\n\t\t\t\t\t)\n\t\t\t\t) );\n\t\t\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test should_display() when viewing checkout valid with a valid user\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_on_checkout() {\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'checkout' ) );\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t}\n\n\t/**\n\t * Test should_display() when viewing the student dashboard with a valid user\n\t *\n\t * @since 4.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_on_dashboard() {\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'myaccount' ) );\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\t}\n\n\t/**\n\t * Test should_display() when no user is present\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_no_user() {\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\t}\n\n\t/**\n\t * Test should_display() when an invalid user is logged in\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_invalid_user() {\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'student' ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\t}\n\n\t/**\n\t * Test should_display() on the admin panel\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_in_admin() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tset_current_screen( 'users.php' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\t\tset_current_screen( 'front' ); // Reset for later tests.\n\n\t}\n\n\t/**\n\t * Test should_display() on a post type archive\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_post_type_archive() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->go_to( get_post_type_archive_link( 'course' ) );\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t}\n\n\t/**\n\t * Test should_display() when a valid using is viewing an invalid post type\n\t *\n\t * @since 4.5.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display_on_invalid_post_types() {\n\n\t\tglobal $post;\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\tforeach ( array( 'post', 'page' ) as $post_type ) {\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\t\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-conroller-quizzes.php",
    "content": "<?php\n/**\n * Test LLMS_Controller_Quizzes\n *\n * @package LifterLMS/Tests/Controllers\n *\n * @group controllers\n * @group quizzes\n * @group controller_quizzes\n *\n * @since 3.37.8\n */\nclass LLMS_Test_Controller_Quizzes extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.8\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->controller = new LLMS_Controller_Quizzes();\n\n\t}\n\n\t/**\n\t * Test maybe_handle_reporting_actions(): form not submitted\n\t *\n\t * @since 3.37.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_reporting_actions_not_submitted() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->assertNull( $this->controller->maybe_handle_reporting_actions() );\n\n\t}\n\n\t/**\n\t * Test maybe_handle_reporting_actions(): invalid nonce\n\t *\n\t * @since 3.37.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_reporting_actions_invalid_nonce() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_quiz_actions_nonce' => 'fake',\n\t\t) );\n\n\t\t$this->assertNull( $this->controller->maybe_handle_reporting_actions() );\n\n\t}\n\n\t/**\n\t * Test maybe_handle_reporting_actions(): there's no quiz id passed via to the button form element.\n\t *\n\t * @since 3.37.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_reporting_actions_no_button() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t// Button not set.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ),\n\t\t) );\n\n\t\t$this->assertFalse( $this->controller->maybe_handle_reporting_actions() );\n\n\t\t// Button empty\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ),\n\t\t\t'llms_del_quiz' => '',\n\t\t) );\n\n\t\t$this->assertFalse( $this->controller->maybe_handle_reporting_actions() );\n\n\t}\n\n\t/**\n\t * Test maybe_handle_reporting_actions(): submitted WP Post ID isn't a quiz id.\n\t *\n\t * @since 3.37.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_reporting_actions_not_a_quiz() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ),\n\t\t\t'llms_del_quiz' => $this->factory->post->create(),\n\t\t) );\n\n\t\t$this->assertFalse( $this->controller->maybe_handle_reporting_actions() );\n\n\t}\n\n\t/**\n\t * Test maybe_handle_reporting_actions(): the quiz isn't an orphan.\n\t *\n\t * @since 3.37.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_reporting_actions_not_an_orphan() {\n\n\t\t$courses = $this->generate_mock_courses( 1, 1, 1, 1, 1 );\n\t\t$lesson  = llms_get_post( llms_get_post( $courses[0] )->get_lessons( 'ids' )[0] );\n\t\t$quiz    = $lesson->get_quiz();\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ),\n\t\t\t'llms_del_quiz' => $quiz->get( 'id' ),\n\t\t) );\n\n\t\t$this->assertFalse( $this->controller->maybe_handle_reporting_actions() );\n\n\t}\n\n\t/**\n\t * Test maybe_handle_reporting_actions() success: the quiz is an orphan and can be deleted.\n\t *\n\t * @since 3.37.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_reporting_actions_is_orphan() {\n\n\t\t$quiz = $this->factory->post->create_and_get( array( 'post_type' => 'llms_quiz' ) );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ),\n\t\t\t'llms_del_quiz' => $quiz->ID,\n\t\t) );\n\n\t\t$this->assertEquals( $quiz, $this->controller->maybe_handle_reporting_actions() );\n\n\t}\n\n\t/**\n\t * Test maybe_handle_reporting_actions() success: the quiz's parent course doesn't exist anymore and the quiz can be deleted.\n\t *\n\t * @since 3.37.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_reporting_actions_no_course() {\n\n\t\t$courses = $this->generate_mock_courses( 1, 1, 1, 1, 1 );\n\t\t$lesson  = llms_get_post( llms_get_post( $courses[0] )->get_lessons( 'ids' )[0] );\n\t\t$quiz    = $lesson->get_quiz();\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_quiz_actions_nonce' => wp_create_nonce( 'llms-quiz-actions' ),\n\t\t\t'llms_del_quiz' => $quiz->get( 'id' ),\n\t\t) );\n\n\t\t// Now it's attached to an orphaned lesson, we should still be able to delete it.\n\t\t$lesson->set( 'parent_course', '' );\n\n\t\t$this->assertEquals( $quiz->post, $this->controller->maybe_handle_reporting_actions() );\n\n\t}\n\n\n\n\n\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-account.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Controller_Account class\n *\n * @group controllers\n * @group controller_account\n *\n * @since 3.19.0\n * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib.\n * @since 3.37.17 Added tests for the `lost_password()` and `reset_password()` methods.\n * @since 4.12.0 Added tests for `redeem_voucher()` method.\n */\nclass LLMS_Test_Controller_Account extends LLMS_UnitTestCase {\n\n\t// Consider dates equal within 60 seconds.\n\tprivate $date_delta = 60;\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.17\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Controller_Account();\n\n\t}\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * Clears LifterLMS Notices.\n\t *\n\t * @since 3.37.17\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tllms_clear_notices();\n\n\t}\n\n\t/**\n\t * Mock wp_mail() arguments to ensure we fail when we want to test a wp_mail() failure.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @param  array $args Associative array of arguments passed to wp_mail()\n\t * @return array\n\t */\n\tpublic function fail_wp_mail( $args ) {\n\n\t\t$args['to'] = 'fail';\n\t\treturn $args;\n\n\t}\n\n\t/**\n\t * Test order completion actions\n\t *\n\t * @since 3.19.0\n\t * @since 3.37.17 Use `$this->main->cancel_subscription()` instead of `do_action( 'init' )`.\n\t * @since 6.0.0 Replaced use of deprecated items.\n\t *              - `LLMS_UnitTestCase::setup_post()` method with `LLMS_Unit_Test_Mock_Requests::mockPostRequest()`\n\t *\n\t * @return void\n\t */\n\tpublic function test_cancel_subscription() {\n\n\t\t// form not submitted\n\t\t$this->mockPostRequest( array() );\n\t\t$this->main->cancel_subscription();\n\t\t$this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) );\n\n\t\t// form submitted but missing required fields\n\t\t$this->mockPostRequest( array(\n\t\t\t'_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ),\n\t\t) );\n\t\t$this->main->cancel_subscription();\n\t\t$this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) );\n\t\t$this->assertEquals( 1, llms_notice_count( 'error' ) );\n\n\t\tllms_clear_notices();\n\n\t\t// form submitted but invalid order id or the order id is invalid\n\t\t$this->mockPostRequest( array(\n\t\t\t'_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ),\n\t\t\t'order_id' => 123,\n\t\t) );\n\t\t$this->main->cancel_subscription();\n\t\t$this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) );\n\t\t$this->assertEquals( 1, llms_notice_count( 'error' ) );\n\n\t\tllms_clear_notices();\n\n\t\t// create a real order\n\t\t$order = $this->get_mock_order();\n\n\t\t// form submitted but invalid order id or the order doesn't belong to the current user\n\t\t$this->mockPostRequest( array(\n\t\t\t'_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ),\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t) );\n\t\t$this->main->cancel_subscription();\n\t\t$this->assertEquals( 0, did_action( 'llms_subscription_cancelled_by_student' ) );\n\t\t$this->assertEquals( 1, llms_notice_count( 'error' ) );\n\n\t\tllms_clear_notices();\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\tforeach ( array_keys( llms_get_order_statuses( 'recurring' ) ) as $status ) {\n\n\t\t\t// active order moves to pending cancel\n\t\t\t$order->set_status( $status );\n\n\t\t\t$this->mockPostRequest( array(\n\t\t\t\t'_cancel_sub_nonce' => wp_create_nonce( 'llms_cancel_subscription' ),\n\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t) );\n\t\t\t$this->main->cancel_subscription();\n\n\t\t\t$expected = 'llms-active' === $status ? 'llms-pending-cancel' : 'llms-cancelled';\n\t\t\t$this->assertEquals( $expected, get_post_status( $order->get( 'id' ) ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test lost_password() when form not submitted.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_not_submitted() {\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->assertNull( $this->main->lost_password() );\n\t\t$this->assertEquals( $actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t}\n\n\t/**\n\t * Test lost_password() when an invalid nonce is submitted.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_invalid_nonce() {\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => 'fake',\n\t\t) );\n\n\t\t$this->assertNull( $this->main->lost_password() );\n\t\t$this->assertEquals( $actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t}\n\n\t/**\n\t * Test the lost password form returns an error if missing a required field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_missing_required() {\n\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t) );\n\t\t$res = $controller->lost_password();\n\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_pass_reset_missing_login', $res );\n\n\t\t$this->assertEquals( 1, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t\t$this->assertStringContains( 'Enter a username or e-mail address.', llms_get_notices() );\n\n\t}\n\n\t/**\n\t * Test lost_password() error: login not submitted.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_missing_login() {\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t) );\n\n\t\t$res = $this->main->lost_password();\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_pass_reset_missing_login', $res );\n\n\t\t$this->assertHasNotice( 'Enter a username or e-mail address.', 'error' );\n\n\t}\n\n\t/**\n\t * Test lost_password() error: user not found.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_user_not_found_email() {\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t'llms_login'           => 'fake',\n\t\t) );\n\n\t\t$res = $this->main->lost_password();\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_pass_reset_invalid_login', $res );\n\n\t\t$this->assertHasNotice( 'Invalid username or e-mail address.', 'error' );\n\n\t}\n\n\t/**\n\t * Test lost_password() returns errors for an invalid username.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return vod\n\t */\n\tpublic function test_lost_password_user_not_found_email_username() {\n\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t'llms_login'           => 'thisisafakeusername',\n\t\t) );\n\n\t\t$res = $controller->lost_password();\n\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_pass_reset_invalid_login', $res );\n\n\t\t$this->assertStringContains( 'Invalid username or e-mail address.', llms_get_notices() );\n\n\t}\n\n\t/**\n\t * Test lost_password() when WP core get_password_reset_key() returns an error or password reset is disabled via filters.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_key_error() {\n\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t$user = $this->factory->user->create_and_get();\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t'llms_login'           => $user->user_login,\n\t\t) );\n\n\t\t// Mock an error.\n\t\tadd_filter( 'allow_password_reset', '__return_false' );\n\n\t\t$res = $controller->lost_password();\n\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'no_password_reset', $res );\n\n\t\t$this->assertStringContains( 'Password reset is not allowed for this user', llms_get_notices() );\n\n\t\tremove_filter( 'allow_password_reset', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test lost_password() success.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_email_success() {\n\n\t\t// Something prior to this test triggers a password changed email to be sent and causes this test to fail as a result.\n\t\t// Adding a reset here is faster than tracking down the test that causes that email to be sent.\n\t\treset_phpmailer_instance();\n\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t// Test with user-submitted email & username.\n\t\tforeach ( array( 'user_email', 'user_login' ) as $field ) {\n\n\t\t\t$this->mockPostRequest( array(\n\t\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t\t'llms_login'           => $user->$field,\n\t\t\t) );\n\n\t\t\t$this->assertTrue( $controller->lost_password() );\n\n\t\t\t$this->assertStringContains( 'Check your e-mail for the confirmation link.', llms_get_notices() );\n\n\t\t\t// Test the email sent.\n\t\t\t$sent = tests_retrieve_phpmailer_instance()->get_sent();\n\t\t\t$this->assertEquals( $user->user_email, $sent->to[0][0] );\n\t\t\t$this->assertEquals( 'Password Reset for Test Blog', $sent->subject );\n\n\t\t}\n\n\t\treset_phpmailer_instance();\n\n\t}\n\n\t/**\n\t * Test lost_password() when password reset is disabled by the `allow_password_reset` WP core filter.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_reset_disabled() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t'llms_login'           => $user->user_email,\n\t\t) );\n\n\t\tadd_filter( 'allow_password_reset', '__return_false' );\n\n\t\t$res = $this->main->lost_password();\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'no_password_reset', $res );\n\n\t\t$this->assertHasNotice( 'Password reset is not allowed for this user', 'error' );\n\n\t\tremove_filter( 'allow_password_reset', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test lost_password() when a wp_mail() error is encountered.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_email_error() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t'llms_login'           => $user->user_email,\n\t\t) );\n\n\t\tadd_filter( 'wp_mail', array( $this, 'fail_wp_mail' ) );\n\n\t\t$res = $this->main->lost_password();\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_pass_reset_email_failure', $res );\n\n\t\t$this->assertHasNotice( 'Unable to reset password due to an unknown error. Please try again.', 'error' );\n\n\t\tremove_filter( 'wp_mail', array( $this, 'fail_wp_mail' ) );\n\n\t}\n\n\t/**\n\t * Test lost_password() success with an email address.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_with_email_success() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t'llms_login'           => $user->user_email,\n\t\t) );\n\n\t\t$res = $this->main->lost_password();\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t\t$this->assertTrue( $res );\n\n\t\t$this->assertHasNotice( 'Check your e-mail for the confirmation link.', 'success' );\n\n\t}\n\n\t/**\n\t * Test lost_password() success with username.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_lost_password_with_login_success() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t// Baseline actions count.\n\t\t$actions = did_action( 'llms_before_lost_password_form_submit' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_lost_password_nonce' => wp_create_nonce( 'llms_lost_password' ),\n\t\t\t'llms_login'           => $user->user_login,\n\t\t) );\n\n\t\t$res = $this->main->lost_password();\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_before_lost_password_form_submit' ) );\n\n\t\t$this->assertTrue( $res );\n\n\t\t$this->assertHasNotice( 'Check your e-mail for the confirmation link.', 'success' );\n\n\t}\n\n\t/**\n\t * Test redeem_voucher() when the form isn't submitted\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_redeem_voucher_not_submitted() {\n\t\t$this->assertNull( $this->main->redeem_voucher() );\n\t}\n\n\t/**\n\t * Test redeem_voucher() when there's an invalid nonce\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_redeem_voucher_invalid_nonce() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'lifterlms_voucher_nonce' => 'fake',\n\t\t) );\n\n\t\t$this->assertNull( $this->main->redeem_voucher() );\n\n\t}\n\n\t/**\n\t * Test redeem_voucher() when no voucher code is submitted\n\t *\n\t * Note: the error message doesn't really make sense but in real world scenarios\n\t * and end user will never encounter this error as HTML5 validation prevents\n\t * the form from being submitted without a voucher.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_redeem_voucher_missing_voucher() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$this->mockPostRequest( array(\n\t\t\t'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ),\n\t\t) );\n\n\t\t$res = $this->main->redeem_voucher();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'not-found', $res );\n\t\t$this->assertHasNotice( 'Voucher code \"\" could not be found.', 'error' );\n\n\t}\n\n\t/**\n\t * Test redeem_voucher() when there's no user\n\t *\n\t * This shouldn't ever really happen but we'll test it just in case.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_redeem_voucher_missing_user() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ),\n\t\t) );\n\n\t\t$this->assertNull( $this->main->redeem_voucher() );\n\n\t}\n\n\t/**\n\t * Test redeem_voucher() when an error is encountered during the voucher redemption\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_redeem_voucher_error() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$this->mockPostRequest( array(\n\t\t\t'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ),\n\t\t\t'llms_voucher_code'       => 'fakevouchercode1',\n\t\t) );\n\n\t\t$res = $this->main->redeem_voucher();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'not-found', $res );\n\t\t$this->assertHasNotice( 'Voucher code \"fakevouchercode1\" could not be found.', 'error' );\n\n\t}\n\n\t/**\n\t * Test redeem_voucher() success\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_redeem_voucher_success() {\n\n\t\t$voucher = $this->create_voucher( 1, 1 );\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$this->mockPostRequest( array(\n\t\t\t'lifterlms_voucher_nonce' => wp_create_nonce( 'lifterlms_voucher_check' ),\n\t\t\t'llms_voucher_code'       => $voucher->get_voucher_codes()[0]->code,\n\t\t) );\n\n\t\t$this->assertTrue( $this->main->redeem_voucher() );\n\t\t$this->assertHasNotice( 'Voucher redeemed successfully!', 'success' );\n\n\t}\n\n\t/**\n\t * Test reset_password(): form not submitted.\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_not_submitted() {\n\n\t\t$this->assertNull( $this->main->reset_password() );\n\n\t}\n\n\t/**\n\t * Test reset_password(): invalid nonce\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_invalid_nonce() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_reset_password_nonce' => 'fake',\n\t\t) );\n\n\t\t$this->assertNull( $this->main->reset_password() );\n\n\t}\n\n\t/**\n\t * Test reset_password(): form validation errors (missing required fields)\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_form_validation_error() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ),\n\t\t) );\n\n\t\t$res = $this->main->reset_password();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertEquals( 1, count( $res->errors ) );\n\n\t\t$errors = array(\n\t\t\t'Password is a required field',\n\t\t\t'Confirm Password is a required field',\n\t\t);\n\n\t\t$notices = llms_get_notices();\n\n\t\tforeach ( $errors as $error ) {\n\t\t\t$this->assertStringContains( $error, $notices );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test reset_password(): password reset key errors\n\t *\n\t * @since 3.37.17\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_reset_key_errors() {\n\n\t\t$pass = wp_generate_password( 12 );\n\n\t\t// Fake user and key.\n\t\t$post = array(\n\t\t\t'_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ),\n\t\t\t'password'         => $pass,\n\t\t\t'password_confirm' => $pass,\n\t\t\t'llms_reset_key'   => 'fake',\n\t\t\t'llms_reset_login' => 'fake',\n\t\t);\n\t\t$this->mockPostRequest( $post );\n\n\t\t$res = $this->main->reset_password();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res );\n\t\t$this->assertStringContains( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', llms_get_notices() );\n\n\t\t// Real user fake key.\n\t\t$user = $this->factory->user->create_and_get();\n\t\t$data['llms_reset_login'] = $user->user_login;\n\t\t$this->mockPostRequest( $post );\n\n\t\t$res = $this->main->reset_password();\n\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res );\n\t\t$this->assertStringContains( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', llms_get_notices() );\n\n\t}\n\n\t/**\n\t * Test reset_password() submitted passwords don't match.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_no_match() {\n\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t$this->mockPostRequest( array(\n \t\t\t'_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ),\n \t\t\t'password' => 'fake',\n \t\t\t'password_confirm' => 'fake2',\n\t\t) );\n\n\t\t$res = $controller->reset_password();\n\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms-passwords-must-match', $res );\n\n\t\t$notices = llms_get_notices();\n\t\t$this->assertStringContains( 'The submitted passwords do must match.', $notices );\n\n\t}\n\n\t/**\n\t * Test reset_password() with an expired password reset key.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_expired_key() {\n\n\t\tadd_filter( 'password_reset_expiration', '__return_zero' );\n\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t$user = $this->factory->user->create_and_get();\n\t\t$key  = get_password_reset_key( $user );\n\n\t\tllms_set_password_reset_cookie( sprintf( '%1$d:%2$s', $user->ID, $key ) );\n\n\t\t$this->mockPostRequest( array(\n \t\t\t'_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ),\n \t\t\t'password' => 'fake',\n \t\t\t'password_confirm' => 'fake',\n \t\t\t'llms_reset_login' => $user->user_login,\n \t\t\t'llms_reset_key' => $key,\n\t\t) );\n\n\t\t$res = $controller->reset_password();\n\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_expired_key', $res );\n\t\t$this->assertStringContains( 'This password reset key is invalid or has already been used. Please reset your password again if needed.', llms_get_notices() );\n\n\t\tremove_filter( 'password_reset_expiration', '__return_zero' );\n\n\t}\n\n\t/**\n\t * Test reset_password() success\n\t *\n\t * @since 3.37.17\n\t * @since 4.21.0 Added more assertions for testing with special character passwords.\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_success() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t$passwords = array(\n\t\t\t// See notes on spaces below.\n\t\t\t' with leading space',\n\t\t\t'with trailing space ',\n\n\t\t\t// Some simple characters.\n\t\t\t'123456arst!',\n\t\t\t'\\slashy \\ passwordy',\n\t\t\t'<such>()-!=***. special!y320',\n\n\t\t\t// These passwords were failing before we improved the tests.\n\t\t\t' AxwVr=@D`z5SXh&Cj/z{#8xta>rvx Nr!5Ur48rtI[ykmc8k~Uj&HO>)/$4:z98',\n\t\t\t'G!*EODpN[!rw] Z|tW|L4.2]@Iok1b1ws(kF3~BP0B%_}./{?)5y$Y`ODn|#-!x ',\n\t\t\t' w,~5E3`=RiPZq&.q0P>-R]1t|t7Qxev',\n\t\t\t'_rsBES.Icg~T)c( -UPh?;Dhu>Up{|b!woR{_hynn7$0(*e1mI1Q3t9(h.V1h]v ',\n\n\t\t\t// Generate some random passwords to test.\n\t\t\twp_generate_password( 12 ),\n\t\t\twp_generate_password( 32, true, true ),\n\t\t\twp_generate_password( 64, true, true ),\n\t\t);\n\n\t\tforeach ( $passwords as $pass ) {\n\n\t\t\twp_set_current_user( null );\n\n\t\t\t$user = $this->factory->user->create_and_get();\n\n\t\t\t// Fake user and key.\n\t\t\t$post = array(\n\t\t\t\t'_reset_password_nonce' => wp_create_nonce( 'llms_reset_password' ),\n\t\t\t\t'password'         => $pass,\n\t\t\t\t'password_confirm' => $pass,\n\t\t\t\t'llms_reset_key'   => get_password_reset_key( $user ),\n\t\t\t\t'llms_reset_login' => $user->user_login,\n\t\t\t);\n\n\t\t\t$this->mockPostRequest( $post );\n\n\t\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'reset_password_handler' ) );\n\n\t\t\t$user = get_user_by( 'id', $user->ID );\n\n\t\t\t/**\n\t\t\t * Because of `wp_magic_quotes()`, slashes will be automatically added when a user actually tries to login.\n\t\t\t *\n\t\t\t * We also will trim the password because WP runs `trim()` on passwords when logging in / creating accounts\n\t\t\t * but it doesn't run it when using `wp_check_password()` itself: https://core.trac.wordpress.org/ticket/34889.\n\t\t\t *\n\t\t\t * We'll add these to `wp_check_password()` here to make sure that a user can actually login with their newly updated\n\t\t\t * password.\n\t\t\t */\n\t\t\t$this->assertTrue( wp_check_password( addslashes( trim( $pass ) ), $user->user_pass ), $pass );\n\n\t\t\t// $this->assertHasNotices( 'success' );\n\t\t\t// $this->assertStringContains( 'Your password has been updated.', llms_get_notices() );\n\n\t\t\t// User should be able to login using our login functionality / forms.\n\t\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t\t'llms_login'    => $user->user_email,\n\t\t\t\t/**\n\t\t\t\t * Here we add slashes to simulate a physical $_POST with `wp_magic_quotes()` but we don't need to `trim()` because\n\t\t\t\t * that's handled in `wp_authenticate()` (called by `wp_signon()` used by our handler).\n\t\t\t\t */\n\t\t\t\t'llms_password' => addslashes( $pass ),\n\t\t\t) );\n\n\t\t\t$this->assertEquals( $user->ID, $login, $pass );\n\n\t\t\twp_set_current_user( null );\n\n\t\t\t// Authenticate via the WP core method directly (redundant but...).\n\t\t\t$auth = wp_authenticate( $user->user_login, addslashes( $pass ) );\n\t\t\t$this->assertEquals( $auth, $user, $pass );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test reset_password_link_redirect(): no redirect when not on the account page.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_link_redirect_not_account_page() {\n\n\t\t$controller = new LLMS_Controller_Account();\n\t\t$this->go_to( home_url() );\n\n\t\t$controller->reset_password_link_redirect();\n\t\t$this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) );\n\n\t}\n\n\t/**\n\t * Test reset_password_link_redirect(): no redirect when missing key and/or login params.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_link_redirect_no_vars() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'myaccount' ) );\n\n\t\t$controller = new LLMS_Controller_Account();\n\n\t\t// No vars.\n\t\t$controller->reset_password_link_redirect();\n\t\t$this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) );\n\n\t\t// No login.\n\t\t$this->mockGetRequest( array(\n\t\t\t'key' => 'fake-key',\n\t\t) );\n\t\t$controller->reset_password_link_redirect();\n\t\t$this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) );\n\n\t\t// No key.\n\t\t$this->mockGetRequest( array(\n\t\t\t'login' => 'fake-login',\n\t\t) );\n\t\t$controller->reset_password_link_redirect();\n\t\t$this->assertNull( $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) ) );\n\n\t}\n\n\t/**\n\t * Test reset_password_link_redirect(): redirect & set the cookie (even if it's an invalid user.)\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_link_redirect_success_fake_user() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'myaccount' ) );\n\n\t\t$controller = new LLMS_Controller_Account();\n\t\t$this->mockGetRequest( array(\n\t\t\t'key'   => 'fake-key',\n\t\t\t'login' => 'fake-login',\n\t\t) );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( add_query_arg( 'reset-pass', 1, wp_lostpassword_url() ) . ' [302] YES' );\n\n\t\ttry {\n\n\t\t\t$controller->reset_password_link_redirect();\n\n\t\t} catch( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\t$cookie = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) );\n\t\t\t$this->assertEquals( '0:fake-key', $cookie['value'] );\n\t\t\tthrow $exception;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test reset_password_link_redirect(): redirect & set the cookie with a valid user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_reset_password_link_redirect_success_real_user() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( llms_get_page_url( 'myaccount' ) );\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t$controller = new LLMS_Controller_Account();\n\t\t$this->mockGetRequest( array(\n\t\t\t'key'   => 'fake-key',\n\t\t\t'login' => $user->user_login,\n\t\t) );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( add_query_arg( 'reset-pass', 1, wp_lostpassword_url() ) . ' [302] YES' );\n\n\t\ttry {\n\n\t\t\t$controller->reset_password_link_redirect();\n\n\t\t} catch( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\t$cookie = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) );\n\t\t\t$this->assertEquals( sprintf( '%d:fake-key', $user->ID ), $cookie['value'] );\n\t\t\tthrow $exception;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test account update form submission handler when form is not submitted\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_not_submitted() {\n\n\t\t$this->mockPostRequest( array() );\n\t\t$this->main->update();\n\t\t$this->assertEquals( 0, did_action( 'llms_before_user_account_update_submit' ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_updated' ) );\n\n\t}\n\n\t/**\n\t * Test account update form submission handler when user is not logged in\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_no_user() {\n\n\t\t// form submitted but user isn't logged in\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_update_person_nonce' => wp_create_nonce( 'llms_update_person' ),\n\t\t) );\n\t\t$this->main->update();\n\t\t$this->assertEquals( 1, did_action( 'llms_before_user_account_update_submit' ) );\n\t\t$this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_updated' ) );\n\t\tllms_clear_notices();\n\n\t}\n\n\t/**\n\t * Test account update form submission handler when missing required fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_missing_fields() {\n\n\t\tLLMS_Install::create_pages();\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t// create a user\n\t\t$uid = $this->factory->user->create();\n\t\t// sign the user in\n\t\twp_set_current_user( $uid );\n\n\t\t// form submitted but missing fields\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_update_person_nonce' => wp_create_nonce( 'llms_update_person' ),\n\t\t) );\n\t\t$this->main->update();\n\t\t$this->assertEquals( 1, did_action( 'llms_before_user_account_update_submit' ) );\n\t\t$this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_updated' ) );\n\t\tllms_clear_notices();\n\n\t}\n\n\t/**\n\t * Test account update form submission handler\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_success() {\n\n\t\tLLMS_Install::create_pages();\n\t\tLLMS_Forms::instance()->install();\n\n\t\t// I can't figure out why the action in the constructor isn't added when this test is run.\n\t\tLLMS_Unit_Test_Util::call_method( LLMS_Form_Handler::instance(), '__construct' );\n\n\t\t// create a user\n\t\t$uid = $this->factory->user->create();\n\t\t// sign the user in\n\t\twp_set_current_user( $uid );\n\n\t\t// update something\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_update_person_nonce' => wp_create_nonce( 'llms_update_person' ),\n\t\t\t'email_address' => 'help+23568@lifterlms.com',\n\t\t\t'email_address_confirm' => 'help+23568@lifterlms.com',\n\t\t\t'display_name' => 'Marshall P.',\n\t\t\t'first_name' => 'Marshall',\n\t\t\t'last_name' => 'Pate',\n\t\t\t'llms_billing_address_1' => 'Voluptatem',\n\t\t\t'llms_billing_address_2' => '#12345',\n\t\t\t'llms_billing_city' => 'Harum est dolorum sed vel perspiciatis consequatur dignissimos possimus delectus quos optio omnis error quas rem dicta et consectetur odio',\n\t\t\t'llms_billing_state' => 'Esse ea est dolore sed sunt ipsum a ut nemo dolorem aut aliquam cillum asperiores minim culpa',\n\t\t\t'llms_billing_zip' => '72995',\n\t\t\t'llms_billing_country' => 'US',\n\t\t) );\n\n\t\t// exceptions thrown in testing env instead of exit()\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Exit::class );\n\t\t$this->expectExceptionMessage( sprintf( '%s [302] YES', llms_get_endpoint_url( 'edit-account', '', llms_get_page_url( 'myaccount' ) ) ) );\n\n\t\t// run these assertions within actions because the exit() at the end of the redirect will halt program execution\n\t\t// and then we'll never get to these assertions!\n\t\tadd_action( 'llms_before_user_account_update_submit', function() {\n\t\t\t$this->assertEquals( 1, did_action( 'llms_before_user_account_update_submit' ) );\n\t\t\t$this->assertEquals( 0, llms_notice_count( 'error' ) );\n\t\t} );\n\t\tadd_action( 'lifterlms_user_updated', function() {\n\t\t\t$this->assertEquals( 1, did_action( 'lifterlms_user_updated' ) );\n\t\t} );\n\n\t\t$this->main->update();\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-achievements.php",
    "content": "<?php\n/**\n * Test LLMS_Controller_Achievements.\n *\n * @package LifterLMS/Tests/Controllers\n *\n * @group controllers\n * @group achievements\n * @group controller_achievements\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Controller_Achievements extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var LLMS_Controller_Achievements\n\t */\n\tprivate $instance;\n\n\t/**\n\t * Add nonce to array.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $data Data array.\n\t * @param bool  $real If true, uses a real nonce. Otherwise uses a fake nonce (useful for testing negative cases).\n\t * @return array\n\t */\n\tprotected function add_nonce_to_array( $data = array(), $real = true ) {\n\t\t$nonce_string = $real ? wp_create_nonce( 'llms-achievement-sync-actions' ) : wp_create_nonce( 'fake' );\n\n\t\treturn wp_parse_args( $data, array(\n\t\t\t'_llms_achievement_sync_actions_nonce' => $nonce_string,\n\t\t) );\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->instance = new LLMS_Controller_Achievements();\n\t}\n\n\t/**\n\t * Test maybe_handle_awarded_engagement_sync_actions() when not supplying an achievement/template ID.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_awarded_achievements_sync_actions_missing_achievement_or_template_id() {\n\n\t\t// Not supplying an achievement ID.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_achievement',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-missing-awarded-achievement-id',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\n\t\t// Not supplying an achievement template ID.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_achievements',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-missing-achievement-template-id',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\t}\n\n\t/**\n\t * Test maybe_handle_awarded_engagement_sync_actions() when not supplying an action or supplying an invalid action.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_awarded_achievements_sync_actions_missing_invalid_action() {\n\n\t\t// Not supplying an action.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray()\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-achievements-missing-action',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\n\t\t// Supplying an invalid nonce.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_achievement_wrong',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-achievements-invalid-action',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\t}\n\n\t/**\n\t * Test maybe_handle_awarded_engagement_sync_actions() when not supplying a nonce or supplying an invalid nonce.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_awarded_achievements_sync_actions_missing_invalid_nonce() {\n\n\t\t// Not supplying a nonce.\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'action' => 'sync_awarded_achievement',\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-achievements-invalid-nonce',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\n\t\t// Supplying an invalid nonce.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_achievement',\n\t\t\t\t),\n\t\t\t\tfalse\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-achievements-invalid-nonce',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\t}\n\n\t/**\n\t * Test sync_awarded_engagement() handling.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_awarded_achievement_handling() {\n\n\t\t// Create an achievement template.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Unregister the llms_my_achievement post type then re-register it so that the post type property _edit_link\n\t\t// is populated (admin can edit the post type).\n\t\tunregister_post_type( 'llms_my_achievement' );\n\t\tLLMS_Post_Types::register_post_types();\n\t\t$achievement_template_id = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_achievement',\n\t\t\t)\n\t\t);\n\t\t$awarded_achievement_id  = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t'post_parent' => $achievement_template_id,\n\t\t\t)\n\t\t);\n\n\t\t// Current user cannot edit 'llms_my_achievement'.\n\t\twp_set_current_user( 0 );\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-achievement-insufficient-permissions',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->instance,\n\t\t\t\t'sync_awarded_engagement',\n\t\t\t\tarray( $awarded_achievement_id )\n\t\t\t)\n\t\t);\n\n\t\t// Current user can edit 'llms_my_achievement'.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_achievement',\n\t\t\t\t\t'post'   => $awarded_achievement_id,\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( get_edit_post_link( $awarded_achievement_id, 'raw' ) . '&message=1 [302] YES' ); // Update success.\n\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions();\n\t}\n\n\t/**\n\t * Test sync_awarded_achievement() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_awarded_achievement_method_invalid_template() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Unregister the llms_my_achievement post type then re-register it so that the post type property _edit_link\n\t\t// is populated (admin can edit the post type).\n\t\tunregister_post_type( 'llms_my_achievement' );\n\t\tLLMS_Post_Types::register_post_types();\n\n\t\t// Invalid achievement template.\n\t\t$achievement_template_id = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'post',\n\t\t\t)\n\t\t);\n\t\t$awarded_achievement_id  = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t'post_parent' => $achievement_template_id,\n\t\t\t)\n\t\t);\n\n\t\t// Current user can edit 'llms_my_achievement'.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-achievement-invalid-template',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->instance,\n\t\t\t\t'sync_awarded_engagement',\n\t\t\t\tarray( $awarded_achievement_id )\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Test sync_awarded_engagements handling.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_awarded_achievements_handling() {\n\n\t\t// Create an achievement template.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Unregister the llms_achievement post type then re-register it so that the post type property _edit_link\n\t\t// is populated (admin can edit the post type).\n\t\tunregister_post_type( 'llms_achievement' );\n\t\tLLMS_Post_Types::register_post_types();\n\t\t$achievement_template_id = $this->factory->post->create( array( 'post_type' => 'llms_achievement' ) );\n\n\t\t// Current user cannot edit 'llms_my_achievement' post type.\n\t\twp_set_current_user( 0 );\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-achievements-insufficient-permissions',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->instance,\n\t\t\t\t'sync_awarded_engagements',\n\t\t\t\tarray( $achievement_template_id )\n\t\t\t)\n\t\t);\n\n\t\t// Current user can edit 'llms_my_achievement' post type.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_achievements',\n\t\t\t\t\t'post'   => $achievement_template_id,\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( get_edit_post_link( $achievement_template_id, 'raw' ) . ' [302] YES' );\n\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions();\n\t\t$this->assertEquals( 1, did_action( 'llms_do_awarded_achievements_bulk_sync' ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-awards.php",
    "content": "<?php\n/**\n * Test LLMS_Controller_Quizzes\n *\n * @package LifterLMS/Tests/Controllers\n *\n * @group controllers\n * @group controller_awards\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Controller_Awards extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = 'LLMS_Controller_Awards';\n\n\t}\n\n\t/**\n\t * Test __construct()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\t$actions = array(\n\t\t\t// Hook, Callback, Priority.\n\t\t\tarray( 'llms_user_earned_certificate', 'on_earn', 20 ),\n\t\t\tarray( 'llms_user_earned_achievement', 'on_earn', 20 ),\n\t\t\tarray( 'save_post_llms_my_certificate', 'on_save', 20 ),\n\t\t\tarray( 'save_post_llms_my_achievement', 'on_save', 20 ),\n\t\t\tarray( 'rest_after_insert_llms_my_certificate', 'on_rest_insert', 20 ),\n\t\t);\n\n\t\tforeach ( $actions as $data ) {\n\t\t\tremove_action( $data[0], array( $this->main, $data[1] ), $data[2] );\n\t\t\t$this->assertFalse( has_action( $data[0], array( $this->main, $data[1] ) ) );\n\t\t}\n\n\t\t// Reinstantiate.\n\t\t$this->main::init();\n\n\t\tforeach ( $actions as $data ) {\n\t\t\t$this->assertEquals( $data[2], has_action( $data[0], array( $this->main, $data[1] ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test on_earn() when an invalid post type is passed in.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_earn_invalid_post_type() {\n\n\t\t$post = $this->factory->post->create();\n\t\t$this->assertFalse( $this->main::on_earn( 1, $post ) );\n\n\t}\n\n\t/**\n\t * Test on_earn()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_earn() {\n\n\t\tforeach ( array( 'llms_my_achievement', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create( compact( 'post_type' ) );\n\n\t\t\t$ts = '2021-12-23 11:45:55';\n\t\t\tllms_tests_mock_current_time( $ts );\n\n\t\t\t$this->assertEquals( $ts, $this->main::on_earn( 1, $post ) );\n\t\t\t$this->assertEquals( $ts, get_post_meta( $post, '_llms_awarded', true ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test on_rest_insert() during a rest update.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_rest_insert_update() {\n\t\t$this->assertSame( 0, $this->main::on_rest_insert( null, null, false ) );\n\t}\n\n\t/**\n\t * Test on_rest_insert() for a cert with no parent template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_rest_insert_no_parent() {\n\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_my_certificate' ) );\n\t\t$this->assertSame( 1, $this->main::on_rest_insert( $post, null, true ) );\n\t}\n\n\t/**\n\t * Test on_rest_insert() during an insertion with a parent.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_rest_insert() {\n\n\t\t$actions = did_action( 'llms_certificate_synchronized' );\n\n\t\t$template_id = $this->create_certificate_template();\n\t\t$post        = $this->factory->post->create_and_get( array(\n\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t'post_parent' => $template_id,\n\t\t) );\n\n\t\t$this->assertSame( 2, $this->main::on_rest_insert( $post, null, true ) );\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_certificate_synchronized' ) );\n\n\t\t$cert = llms_get_certificate( $post->ID );\n\t\t$this->assertNotEquals( $post->post_name, $cert->get( 'name' ) );\n\n\t}\n\n\t/**\n\t * Test on_save() with an invalid post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_save_award_invalid_post_type() {\n\n\t\t$post = $this->factory->post->create();\n\t\t$this->assertFalse( $this->main::on_save( $post ) );\n\n\t}\n\n\t/**\n\t * Test on_save() for a achievement.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_save_achievement() {\n\n\t\t$content = 'post content';\n\n\t\t$actions = did_action( 'llms_user_earned_achievement' );\n\n\t\t$achievement_id = $this->factory->post->create( array(\n\t\t\t'post_type'    => 'llms_my_achievement',\n\t\t\t'post_content' => $content,\n\t\t) );\n\n\t\t$this->assertTrue( $this->main::on_save( $achievement_id ) );\n\n\t\t$cert = new LLMS_User_Achievement( $achievement_id );\n\t\t$this->assertEquals( $content, $cert->get( 'content', true ) );\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_user_earned_achievement' ) );\n\n\t\t// Action shouldn't run again.\n\t\t$this->assertTrue( $this->main::on_save( $achievement_id ) );\n\t\t$this->assertEquals( $actions, did_action( 'llms_user_earned_achievement' ) );\n\n\t}\n\n\t/**\n\t * Test on_save() for a certificate.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_save_certificate() {\n\n\t\t$first_name = 'Sarah';\n\n\t\t$actions = did_action( 'llms_user_earned_certificate' );\n\n\t\t$cert_id = $this->factory->post->create( array(\n\t\t\t'post_type'    => 'llms_my_certificate',\n\t\t\t'post_content' => '{first_name}',\n\t\t\t'post_author'  => $this->factory->user->create( compact( 'first_name' ) ),\n\t\t) );\n\n\t\t$this->assertTrue( $this->main::on_save( $cert_id ) );\n\n\t\t$cert = llms_get_certificate( $cert_id );\n\t\t$this->assertEquals( $first_name, $cert->get( 'content', true ) );\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_user_earned_certificate' ) );\n\n\t\t// Action shouldn't run again.\n\t\t$this->assertTrue( $this->main::on_save( $cert_id ) );\n\t\t$this->assertEquals( $actions, did_action( 'llms_user_earned_certificate' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-certificates.php",
    "content": "<?php\n/**\n * Test LLMS_Controller_Certificates.\n *\n * @package LifterLMS/Tests/Controllers\n *\n * @group controllers\n * @group certificates\n * @group controller_certificates\n *\n * @since 3.37.4\n * @since 4.5.0 Added tests for managing certificate sharing settings.\n * @since 6.0.0 Added tests for handling awarded certificates sync actions.\n */\nclass LLMS_Test_Controller_Certificates extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var LLMS_Controller_Certificates\n\t */\n\tprivate $instance;\n\n\t/**\n\t * Add nonce to array.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $data Data array.\n\t * @param bool  $real If true, uses a real nonce. Otherwise uses a fake nonce (useful for testing negative cases).\n\t * @return array\n\t */\n\tprotected function add_nonce_to_array( $data = array(), $real = true ) {\n\t\t$nonce_string = $real ? wp_create_nonce( 'llms-certificate-sync-actions' ) : wp_create_nonce( 'fake' );\n\n\t\treturn wp_parse_args( $data, array(\n\t\t\t'_llms_certificate_sync_actions_nonce' => $nonce_string,\n\t\t) );\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.4\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->instance = new LLMS_Controller_Certificates();\n\n\t}\n\n\t/**\n\t * Test maybe_allow_public_query(): no authorization data in query string.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_allow_public_query_no_auth() {\n\t\t$this->assertEquals( array(), $this->instance->maybe_allow_public_query( array() ) );\n\t}\n\n\t/**\n\t * Test maybe_allow_public_query(): authorization present but invalid.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_allow_public_query_invalid_auth() {\n\n\t\t// Doesn't exist.\n\t\t$args = array(\n\t\t\t'publicly_queryable' => false,\n\t\t);\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'_llms_cert_auth' => 'fake',\n\t\t) );\n\n\t\t$this->assertEquals( $args, $this->instance->maybe_allow_public_query( $args ) );\n\n\t\t// Post exists but submitted nonce is incorrect.\n\t\t$post_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) );\n\t\tupdate_post_meta( $post_id, '_llms_auth_nonce', 'mock-nonce' );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'_llms_cert_auth' => 'incorrect-nonce',\n\t\t) );\n\t\t$this->assertEquals( $args, $this->instance->maybe_allow_public_query( $args ) );\n\n\t}\n\n\t/**\n\t * Test maybe_allow_public_query(): authorization present and exists but on an invalid post type.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_allow_public_query_invalid_post_type() {\n\n\t\t$post_id = $this->factory->post->create();\n\t\tupdate_post_meta( $post_id, '_llms_auth_nonce', 'mock-nonce' );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'_llms_cert_auth' => 'mock-nonce',\n\t\t) );\n\n\t\t$args = array(\n\t\t\t'publicly_queryable' => false,\n\t\t);\n\n\t\t$this->assertEquals( $args, $this->instance->maybe_allow_public_query( $args ) );\n\n\t}\n\n\t/**\n\t * Test maybe_allow_public_query(): valid auth and post type.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_allow_public_query_update() {\n\n\t\t$post_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) );\n\t\tupdate_post_meta( $post_id, '_llms_auth_nonce', 'mock-nonce' );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'_llms_cert_auth' => 'mock-nonce',\n\t\t) );\n\n\t\t$args = array(\n\t\t\t'publicly_queryable' => false,\n\t\t);\n\t\t$expect = array(\n\t\t\t'publicly_queryable' => true,\n\t\t);\n\n\t\t$this->assertEquals( $expect, $this->instance->maybe_allow_public_query( $args ) );\n\n\t}\n\n\t/**\n\t * Test maybe_authenticate_export_generation() when no authorization data is passed.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_authenticate_export_generation_no_auth() {\n\n\t\t$this->instance->maybe_authenticate_export_generation();\n\t\t$this->assertEquals( 0, get_current_user_id() );\n\n\t}\n\n\t/**\n\t * Test maybe_authenticate_export_generation() when no authorization data is passed.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_authenticate_export_generation_invalid_post_type() {\n\n\t\tglobal $post;\n\t\t$temp = $post;\n\t\t$post = $this->factory->post->create_and_get();\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'_llms_cert_auth' => 'fake',\n\t\t) );\n\n\t\t$this->instance->maybe_authenticate_export_generation();\n\t\t$this->assertEquals( 0, get_current_user_id() );\n\n\t\t// Reset post.\n\t\t$post = $temp;\n\n\t}\n\n\t/**\n\t * Test maybe_authenticate_export_generation() when no authorization data is passed.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_authenticate_export_generation_invalid_nonce() {\n\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\tglobal $post;\n\t\t\t$temp = $post;\n\t\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => $post_type ) );\n\n\t\t\tupdate_post_meta( $post->ID, '_llms_auth_nonce', 'mock-nonce' );\n\n\t\t\t$this->mockGetRequest( array(\n\t\t\t\t'_llms_cert_auth' => 'fake',\n\t\t\t) );\n\n\t\t\t$this->instance->maybe_authenticate_export_generation();\n\t\t\t$this->assertEquals( 0, get_current_user_id() );\n\n\t\t\t// Reset post.\n\t\t\t$post = $temp;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test maybe_authenticate_export_generation() for a certificate template.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_authenticate_export_generation_for_template() {\n\n\t\t$uid = $this->factory->user->create( array( 'role' => 'lms_manager' ) );\n\n\t\t$template = $this->create_certificate_template();\n\t\tupdate_post_meta( $template, '_llms_auth_nonce', 'mock-nonce' );\n\t\twp_update_post( array(\n\t\t\t'ID' => $template,\n\t\t\t'post_author' => $uid,\n\t\t) );\n\n\t\tglobal $post;\n\t\t$temp = $post;\n\t\t$post = get_post( $template );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'_llms_cert_auth' => 'mock-nonce',\n\t\t) );\n\n\t\t$this->instance->maybe_authenticate_export_generation();\n\t\t$this->assertEquals( $uid, get_current_user_id() );\n\n\t\t// Reset post.\n\t\t$post = $temp;\n\n\t}\n\n\t/**\n\t * Test maybe_authenticate_export_generation() for an earned certificate.\n\t *\n\t * @since 3.37.4\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_authenticate_export_generation_for_earned_cert() {\n\n\t\t$uid = $this->factory->student->create();\n\n\t\t$template = $this->create_certificate_template();\n\n\t\t$earned = $this->earn_certificate( $uid, $template, $this->factory->post->create() );\n\n\t\tglobal $post;\n\t\t$temp = $post;\n\t\t$post = get_post( $earned[1] );\n\t\tupdate_post_meta( $post->ID, '_llms_auth_nonce', 'mock-nonce' );\n\n\t\t$this->mockGetRequest( array(\n\t\t\t'_llms_cert_auth' => 'mock-nonce',\n\t\t) );\n\n\t\t$this->instance->maybe_authenticate_export_generation();\n\t\t$this->assertEquals( $uid, get_current_user_id() );\n\n\t\t// Reset post.\n\t\t$post = $temp;\n\n\t}\n\n\t/**\n\t * Test change_sharing_settings() when user has insufficient permissions\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_change_sharing_settings_invalid_permissions() {\n\n\t\t$earned = $this->earn_certificate( $this->factory->student->create(), $this->create_certificate_template(), $this->factory->post->create() );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $earned[1], true ) );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'insufficient-permissions', $res );\n\n\t}\n\n\t/**\n\t * Test change_sharing_settings()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_change_sharing_settings() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $this->factory->post->create() );\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\twp_set_current_user( $uid );\n\n\t\t// Enable Sharing\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $cert_id, true ) ) );\n\t\t$this->assertEquals( 'yes', $cert->get( 'allow_sharing' ) );\n\n\t\t// Already enabled.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $cert_id, true ) ) );\n\t\t$this->assertEquals( 'yes', $cert->get( 'allow_sharing' ) );\n\n\t\t// Disable sharing.\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->instance, 'change_sharing_settings', array( $cert_id, false ) ) );\n\t\t$this->assertEquals( 'no', $cert->get( 'allow_sharing' ) );\n\n\t}\n\n\t/**\n\t * Test maybe_handle_awarded_engagement_sync_actions() when not supplying a certificate/template id.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_awarded_certificates_sync_actions_missing_certificate_or_template_id() {\n\n\t\t// Not supplying a certificate id.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_certificate',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-missing-awarded-certificate-id',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\n\t\t// Not supplying a certificate template id.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_certificates',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-missing-certificate-template-id',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\t}\n\n\t/**\n\t * Test maybe_handle_awarded_engagement_sync_actions() when not supplying an action or supplying an invalid action.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_awarded_certificates_sync_actions_missing_invalid_action() {\n\n\t\t// Not supplying an action.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray()\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-certificates-missing-action',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\n\t\t// Supplying an invalid nonce.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_certificate_wrong',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-certificates-invalid-action',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\t}\n\n\t/**\n\t * Test maybe_handle_awarded_engagement_sync_actions() when not supplying a nonce or supplying an invalid nonce.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_handle_awarded_certificates_sync_actions_missing_invalid_nonce() {\n\n\t\t// Not supplying a nonce.\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'action' => 'sync_awarded_certificate',\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-certificates-invalid-nonce',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\n\t\t// Supplying an invalid nonce.\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_certificate',\n\t\t\t\t),\n\t\t\t\tfalse\n\t\t\t)\n\t\t);\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-certificates-invalid-nonce',\n\t\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions()\n\t\t);\n\t}\n\n\t/**\n\t * Test sync_awarded_engagement() handling.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_awarded_certificate_handling() {\n\n\t\t// Create a certificate template.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Unregister the llms_my_certificate post type then re-register it so that the post type property _edit_link\n\t\t// is populated (admin can edit the post type).\n\t\tunregister_post_type( 'llms_my_certificate' );\n\t\tLLMS_Post_Types::register_post_types();\n\t\t$certificate_template_id = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_certificate',\n\t\t\t)\n\t\t);\n\t\t$awarded_certificate_id  = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t'post_parent' => $certificate_template_id,\n\t\t\t)\n\t\t);\n\n\t\t// Current user cannot edit 'llms_my_certificate'.\n\t\twp_set_current_user( 0 );\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-certificate-insufficient-permissions',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->instance,\n\t\t\t\t'sync_awarded_engagement',\n\t\t\t\tarray( $awarded_certificate_id )\n\t\t\t)\n\t\t);\n\n\t\t// Current user can edit 'llms_my_certificate'.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_certificate',\n\t\t\t\t\t'post'   => $awarded_certificate_id,\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( get_edit_post_link( $awarded_certificate_id, 'raw' ) . '&message=1 [302] YES' ); // Update success.\n\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions();\n\t}\n\n\t/**\n\t * Test sync_awarded_certificate method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_awarded_certificate_method_invalid_template() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Unregister the llms_my_certificate post type then re-register it so that the post type property _edit_link\n\t\t// is populated (admin can edit the post type).\n\t\tunregister_post_type( 'llms_my_certificate' );\n\t\tLLMS_Post_Types::register_post_types();\n\n\t\t// Invalid certificate template.\n\t\t$certificate_template_id = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'post',\n\t\t\t)\n\t\t);\n\t\t$awarded_certificate_id  = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t'post_parent' => $certificate_template_id,\n\t\t\t)\n\t\t);\n\n\t\t// Current user can edit 'llms_my_certificate'.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-certificate-invalid-template',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->instance,\n\t\t\t\t'sync_awarded_engagement',\n\t\t\t\tarray( $awarded_certificate_id )\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Test sync_awarded_engagements handling.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_awarded_certificates_handling() {\n\n\t\t// Create a certificate template.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Unregister the llms_certificate post type then re-register it so that the post type property _edit_link\n\t\t// is populated (admin can edit the post type).\n\t\tunregister_post_type( 'llms_certificate' );\n\t\tLLMS_Post_Types::register_post_types();\n\t\t$certificate_template_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) );\n\n\t\t// Current user cannot edit 'llms_my_certificate' post type.\n\t\twp_set_current_user( 0 );\n\t\t$this->assertWPErrorCodeEquals(\n\t\t\t'llms-sync-awarded-certificates-insufficient-permissions',\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->instance,\n\t\t\t\t'sync_awarded_engagements',\n\t\t\t\tarray( $certificate_template_id )\n\t\t\t)\n\t\t);\n\n\t\t// Current user can edit 'llms_my_certificate' post type.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\t\t$this->mockGetRequest(\n\t\t\t$this->add_nonce_to_array(\n\t\t\t\tarray(\n\t\t\t\t\t'action' => 'sync_awarded_certificates',\n\t\t\t\t\t'post'   => $certificate_template_id,\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( get_edit_post_link( $certificate_template_id, 'raw' ) . ' [302] YES' );\n\t\t$this->instance->maybe_handle_awarded_engagement_sync_actions();\n\t\t$this->assertEquals( 1, did_action( 'llms_do_awarded_achievements_bulk_sync' ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-checkout.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Controller_Checkout class.\n *\n * @package LifterLMS/Tests\n *\n * @group orders\n * @group controllers\n * @group checkout\n * @group controller_checkout\n *\n * @since 7.0.0\n */\nclass LLMS_Test_Controller_Checkout extends LLMS_UnitTestCase {\n\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = LLMS_Controller_Checkout::instance();\n\n\t}\n\n\t/**\n\t * Test constructor().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\t$actions = array(\n\t\t\t'create_pending_order_ajax'  => 5,\n\t\t\t'create_pending_order'       => 10,\n\t\t\t'confirm_pending_order_ajax' => 5,\n\t\t\t'confirm_pending_order'      => 10,\n\t\t\t'switch_payment_source_ajax' => 5,\n\t\t\t'switch_payment_source'      => 10,\n\t\t);\n\n\t\tforeach ( $actions as $hook => $priority ) {\n\t\t\tremove_action( 'init', array( $this->main, $hook ), $priority );\n\t\t}\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, '__construct' );\n\n\t\tforeach ( $actions as $hook => $priority ) {\n\t\t\t$this->assertEquals( $priority, has_action( 'init', array( $this->main, $hook ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test confirm_pending_order() when the form hasn't been submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_not_submitted() {\n\t\t$this->assertNull( $this->main->confirm_pending_order() );\n\t}\n\n\t/**\n\t * Test confirm_pending_order() when a nonce error is encountered.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_nonce_error() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => 'fake',\n\t\t) );\n\t\t$this->assertNull( $this->main->confirm_pending_order() );\n\t}\n\n\t/**\n\t * Test confirm_pending_order() when an missing action.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_missing_action() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'       => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'llms_order_key' => 'NOT-A-REAL-ORDER-KEY',\n\t\t) );\n\t\t$this->assertFalse( $this->main->confirm_pending_order() );\n\n\t}\n\n\t/**\n\t * Test confirm_pending_order() when an invalid order key is submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_invalid_action() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'       => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'action'         => 'FAKE',\n\t\t) );\n\t\t$this->assertFalse( $this->main->confirm_pending_order() );\n\n\t}\n\n\t/**\n\t * Test confirm_pending_order() when an missing order key is submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_missing_order_key() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'       => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'action'         => $this->main::ACTION_CONFIRM_PENDING_ORDER,\n\t\t) );\n\t\t$this->assertNull( $this->main->confirm_pending_order() );\n\t\t$this->assertHasNotice( 'Could not locate an order to confirm.', 'error' );\n\n\t}\n\n\t/**\n\t * Test confirm_pending_order() when an invalid order key is submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_invalid_order_key() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'       => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'action'         => $this->main::ACTION_CONFIRM_PENDING_ORDER,\n\t\t\t'llms_order_key' => 'NOT-A-REAL-ORDER-KEY',\n\t\t) );\n\t\t$this->assertNull( $this->main->confirm_pending_order() );\n\t\t$this->assertHasNotice( 'Could not locate an order to confirm.', 'error' );\n\n\t}\n\n\t/**\n\t * Test confirm_pending_order() when an the order cannot be confirmed.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_order_cannot_be_confirmed() {\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order->set_status( 'llms-failed' );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'       => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'action'         => $this->main::ACTION_CONFIRM_PENDING_ORDER,\n\t\t\t'llms_order_key' => $order->get( 'order_key' ),\n\t\t) );\n\t\t$this->assertNull( $this->main->confirm_pending_order() );\n\t\t$this->assertHasNotice( 'Only pending orders can be confirmed.', 'error' );\n\n\t}\n\n\t/**\n\t * Test confirm_pending_order() is passed to the specified gateway and runs the gateway's method.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_success() {\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-confirm-pending-success';\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t\tpublic function confirm_pending_order( $order ) {\n\t\t\t\tdo_action( 'llms_gateway_fake_confirm_pending_success' );\n\t\t\t}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t// Setup the order.\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order->set( 'payment_gateway', 'fake-confirm-pending-success' );\n\n\t\t// Mock the request.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce'       => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'action'         => $this->main::ACTION_CONFIRM_PENDING_ORDER,\n\t\t\t'llms_order_key' => $order->get( 'order_key' ),\n\t\t) );\n\t\t$this->assertNull( $this->main->confirm_pending_order() );\n\n\t\t// The fake gateway's confirm method should have run.\n\t\t$this->assertSame( 1, did_action( 'llms_gateway_fake_confirm_pending_success' ) );\n\n\t\t$this->unload_payment_gateway( 'fake-confirm-pending-success' );\n\n\t}\n\n\t/**\n\t * Test confirm_pending_order_ajax() when the form was not submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_ajax_not_submitted() {\n\t\t$this->assertNull( $this->main->confirm_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test confirm_pending_order_ajax() when the nonce is invalid.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_ajax_invalid_nonce() {\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => 'fake',\n\t\t) );\n\t\t$this->assertNull( $this->main->confirm_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test confirm_pending_order_ajax() when the action is missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_ajax_missing_action() {\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t) );\n\t\t$this->assertFalse( $this->main->confirm_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test confirm_pending_order_ajax() when the action is invalid.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_ajax_invalid_action() {\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'action'                 => 'invalid',\n\t\t) );\n\t\t$this->assertFalse( $this->main->confirm_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test confirm_pending_order_ajax() successfully passes to the order generator.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order_ajax_success() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CONFIRM_PENDING_ORDER ),\n\t\t\t'action'                 => $this->main::ACTION_CONFIRM_PENDING_ORDER,\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->confirm_pending_order_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'llms-order-gen-order-not-found', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test create_pending_order() when the form is not submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_not_submitted() {\n\t\t$this->assertNull( $this->main->create_pending_order() );\n\t}\n\n\t/**\n\t * Test create_pending_order() when a nonce error is encountered.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_nonce_error() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_checkout_nonce' => 'fake',\n\t\t) );\n\t\t$this->assertNull( $this->main->create_pending_order() );\n\t}\n\n\t/**\n\t * Test create_pending_order() when the action is not submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_missing_action() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_checkout_nonce' => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t) );\n\t\t$this->assertFalse( $this->main->create_pending_order() );\n\t}\n\n\t/**\n\t * Test create_pending_order() when the submitted action is invalid.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_invalid_action() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_checkout_nonce' => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t'action'               => 'fake',\n\t\t) );\n\t\t$this->assertFalse( $this->main->create_pending_order() );\n\t}\n\n\t/**\n\t * Test create_pending_order() when custom validation errors are encountered.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_custom_validation() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_checkout_nonce' => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t'action'               => $this->main::ACTION_CREATE_PENDING_ORDER,\n\t\t) );\n\t\tadd_filter( 'llms_before_checkout_validation', '__return_true' );\n\t\t$this->assertFalse( $this->main->create_pending_order() );\n\t\tremove_filter( 'llms_before_checkout_validation', '__return_true' );\n\t}\n\n\t/**\n\t * Test create_pending_order() when an error is encountered running `llms_setup_pending_order()`.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_setup_error() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_checkout_nonce' => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t'action'               => $this->main::ACTION_CREATE_PENDING_ORDER,\n\t\t) );\n\n\t\t$res = $this->main->create_pending_order();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'missing-plan-id', $res );\n\n\t\t$this->assertHasNotice( 'Missing an Access Plan ID.', 'error' );\n\n\t}\n\n\t/**\n\t * Test create_pending_order() when a validation error is encountered via the free checkout / enrollment form.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_setup_error_free_enroll_form() {\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$plan->set( array(\n\t\t\t'price'   => 0.00,\n\t\t\t'is_free' => 'yes',\n\t\t) );\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$user = $this->factory->student->create();\n\t\twp_set_current_user( $user );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_checkout_nonce' => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t'action'               => $this->main::ACTION_CREATE_PENDING_ORDER,\n\t\t\t'llms_plan_id'         => $plan->get( 'id' ),\n\t\t\t'form'                 => 'free_enroll',\n\t\t) );\n\n\t\ttry {\n\t\t\t$this->main->create_pending_order();\n\t\t} catch ( LLMS_Unit_Test_Exception_Exit $exception ) {\n\t\t\t$this->assertHasNotice( 'First Name is a required field.', 'error' );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test create_pending_order() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_success() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$plan = $this->get_mock_plan();\n\n\t\t$post_data = array(\n\t\t\t'_llms_checkout_nonce'   => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t'action'                 => $this->main::ACTION_CREATE_PENDING_ORDER,\n\n\t\t\t'llms_plan_id'           => $plan->get( 'id' ),\n\n\t\t\t'llms_payment_gateway'   => 'manual',\n\n\t\t\t'user_login'             => 'creatependingorder',\n\t\t\t'email_address'          => 'creatependingorder@example.tld',\n\t\t\t'email_address_confirm'  => 'creatependingorder@example.tld',\n\t\t\t'password'               => '12345678',\n\t\t\t'password_confirm'       => '12345678',\n\t\t\t'first_name'             => 'Test',\n\t\t\t'last_name'              => 'Person',\n\t\t\t'llms_billing_address_1' => '123',\n\t\t\t'llms_billing_address_2' => '123',\n\t\t\t'llms_billing_city'      => 'City',\n\t\t\t'llms_billing_state'     => 'CA',\n\t\t\t'llms_billing_zip'       => '91231',\n\t\t\t'llms_billing_country'   => 'US',\n\t\t\t'llms_phone'             => '1234567890',\n\t\t);\n\n\t\t$this->mockPostRequest( $post_data );\n\n\t\t// Assert some things using an action hook from the manual payment gateway.\n\t\t$handler = function( $order ) use ( $plan, $post_data ) {\n\n\t\t\t$this->assertEquals( $plan->get( 'id' ), $order->get( 'plan_id' ) );\n\n\t\t\t$student = llms_get_student( $order->get( 'user_id' ) );\n\n\t\t\t$this->assertEquals( $order->get( 'user_id' ), get_user_by( 'email', $post_data['email_address'] )->ID );\n\t\t\t$this->assertEquals( $order->get( 'billing_email' ), $post_data['email_address'] );\n\n\n\t\t};\n\t\tadd_action( 'llms_manual_payment_due', $handler );\n\n\t\ttry {\n\t\t\t$this->main->create_pending_order();\n\t\t} catch ( LLMS_Unit_Test_Exception_Exit $exception ) {\n\t\t\tremove_action( 'llms_manual_payment_due', $handler );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test create_pending_order_ajax() when the form was not submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_ajax_not_submitted() {\n\t\t$this->assertNull( $this->main->create_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test create_pending_order_ajax() when the nonce is invalid.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_ajax_invalid_nonce() {\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => 'fake',\n\t\t) );\n\t\t$this->assertNull( $this->main->create_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test create_pending_order_ajax() when the action is missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_ajax_missing_action() {\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t) );\n\t\t$this->assertFalse( $this->main->create_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test create_pending_order_ajax() when the action is invalid.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_ajax_invalid_action() {\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t'action'                 => 'invalid',\n\t\t) );\n\t\t$this->assertFalse( $this->main->create_pending_order_ajax() );\n\t}\n\n\t/**\n\t * Test create_pending_order_ajax() when an order validation error is encountered.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_ajax_order_validation_error() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t'action'                 => $this->main::ACTION_CREATE_PENDING_ORDER,\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->create_pending_order_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'llms-order-gen-plan-required', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test create_pending_order_ajax() when a gateway error is encountered.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_ajax_order_gateway_error() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-create-pending-order-ajax-gateway-err';\n\t\t\tpublic $supports = array(\n\t\t\t\t'recurring_payments' => true,\n\t\t\t);\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {\n\t\t\t\treturn new WP_Error( 'gateway-err' );\n\t\t\t}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$this->mockPostRequest(\n\t\t\tarray_merge(\n\t\t\t\t$this->get_mock_checkout_data_array(),\n\t\t\t\tarray(\n\t\t\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t\t\t'action'                 => $this->main::ACTION_CREATE_PENDING_ORDER,\n\t\t\t\t\t'llms_payment_gateway'   => 'fake-create-pending-order-ajax-gateway-err',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->create_pending_order_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'gateway-err', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$this->unload_payment_gateway( 'fake-create-pending-order-ajax-gateway-err' );\n\n\t}\n\n\t/**\n\t * Test create_pending_order_ajax() when an order validation error is encountered.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order_ajax_order_success() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-create-pending-order-ajax-success';\n\t\t\tpublic $supports = array(\n\t\t\t\t'recurring_payments' => true,\n\t\t\t);\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {\n\t\t\t\treturn array( 'success' => 'yes' );\n\t\t\t}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$this->mockPostRequest(\n\t\t\tarray_merge(\n\t\t\t\t$this->get_mock_checkout_data_array(),\n\t\t\t\tarray(\n\t\t\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_CREATE_PENDING_ORDER ),\n\t\t\t\t\t'action'                 => $this->main::ACTION_CREATE_PENDING_ORDER,\n\t\t\t\t\t'llms_payment_gateway'   => 'fake-create-pending-order-ajax-success',\n\t\t\t\t),\n\t\t\t)\n\t\t);\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->create_pending_order_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertEquals( 'yes', $res['success'] );\n\t\t$this->assertArrayHasKey( 'order_key', $res );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$this->unload_payment_gateway( 'fake-create-pending-order-ajax-success' );\n\n\t}\n\n\t/**\n\t * Test get_url().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_url() {\n\t\t$nonce = wp_create_nonce( 'action' );\n\t\t$this->assertEquals( \"http://example.org?llms-checkout={$nonce}\", $this->main->get_url( 'action' ) );\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Controller_Checkout::maybe_redirect_from_free_enroll_form} when called from an invalid context.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_redirect_from_free_enroll_form_invalid_context() {\n\n\t\t// Everything is wrong.\n\t\t$this->assertNull(\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'maybe_redirect_from_free_enroll_form',\n\t\t\t\tarray( 0, 'fake' )\n\t\t\t)\n\t\t);\n\n\t\t// User is logged in, a plan ID is submitted, but the form is invalid.\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\t$this->assertNull(\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'maybe_redirect_from_free_enroll_form',\n\t\t\t\tarray( 123, 'fake' )\n\t\t\t)\n\t\t);\n\t\t$this->assertNull(\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'maybe_redirect_from_free_enroll_form',\n\t\t\t\tarray( 123, '' )\n\t\t\t)\n\t\t);\n\n\t\t// User is logged in and correct form but no plan ID.\n\t\t$this->assertNull(\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'maybe_redirect_from_free_enroll_form',\n\t\t\t\tarray( 0, 'free_enroll' )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Controller_Checkout::maybe_redirect_from_free_enroll_form} when called with an invalid plan ID.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_redirect_from_free_enroll_form_invalid_plan() {\n\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\t$this->assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'maybe_redirect_from_free_enroll_form',\n\t\t\t\tarray( $this->factory->post->create(), 'free_enroll' )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Controller_Checkout::maybe_redirect_from_free_enroll_form} when a redirect is executed.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_redirect_from_free_enroll_form_success() {\n\n\t\t$plan    = $this->get_mock_plan();\n\t\t$plan_id = $plan->get( 'id' );\n\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\ttry {\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'maybe_redirect_from_free_enroll_form',\n\t\t\t\tarray( $plan_id, 'free_enroll' )\n\t\t\t);\n\t\t} catch ( LLMS_Unit_Test_Exception_Exit $exception ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t\" [302] YES\",\n\t\t\t\t$exception->getMessage()\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Test switch_payment_source() when the form isn't submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_not_submitted() {\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertNoticeCountEquals( 0, 'error' );\n\t}\n\n\t/**\n\t * Test switch_payment_source() with an invalid nonce.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_invalid_nonce() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => 'fake',\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertNoticeCountEquals( 0, 'error' );\n\t}\n\n\t/**\n\t * Test switch_payment_source() when the order_id is missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_missing_order_id() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertHasNotice( 'Missing order information.', 'error' );\n\t}\n\n\t/**\n\t * Test switch_payment_source() when an invalid order ID is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_invalid_order_id() {\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => 'NaN',\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertHasNotice( 'Missing order information.', 'error' );\n\t}\n\n\t/**\n\t * Test switch_payment_source() when the order ID isn't an order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_not_an_order() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $this->factory->post->create(),\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertHasNotice( 'Invalid order.', 'error' );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source() when there's no logged in user.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_mismatched_order() {\n\n\t\t$order = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_order' ) ) );\n\t\t$order->set( 'user_id', 123 );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertHasNotice( 'Invalid order.', 'error' );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source() when the gateway is missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_missing_gateway() {\n\n\t\t$user_id = $this->factory->user->create();\n\t\twp_set_current_user( $user_id );\n\n\t\t$order = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_order' ) ) );\n\t\t$order->set( 'user_id', $user_id );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertHasNotice( 'Missing gateway information.', 'error' );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source() when there's a gateway validation error.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_gateway_validation_error() {\n\n\t\t$user_id = $this->factory->user->create();\n\t\twp_set_current_user( $user_id );\n\n\t\t$order = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_order' ) ) );\n\t\t$order->set( 'user_id', $user_id );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'FAKE',\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertHasNotice( 'The selected payment gateway is not valid.', 'error' );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source() when the switch action is invalid, fake, or missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_invalid_switch_action() {\n\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\t$gateway->set_option( 'enabled', 'yes' );\n\n\t\t$actions = did_action( 'llms_order_payment_source_switched' );\n\n\t\t$plan = llms_insert_access_plan( array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price'      => 25.00,\n\t\t\t'frequency'  => 1,\n\t\t) );\n\t\t$order = $this->factory->order->create_and_get( array( 'plan_id' => $plan->get( 'id' ) ) );\n\t\t$order->set( 'payment_gateway', 'not-manual' );\n\t\t$order->set_status( 'pending-cancel' );\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\tforeach ( array( null, false, 'pay', 'fake ' ) as $action ) {\n\n\t\t\tllms_clear_notices();\n\t\t\t$this->mockPostRequest( array(\n\t\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t\t\t'llms_payment_gateway' => 'manual',\n\t\t\t\t'llms_switch_action'   => $action,\n\t\t\t) );\n\n\t\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t\t$this->assertHasNotice( 'Invalid action.', 'error' );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test switch_payment_source() when the gateway handler returns an error.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_gateway_error() {\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'mock-switch-source-err';\n\t\t\tpublic $supports = array(\n\t\t\t\t'recurring_payments' => true,\n\t\t\t);\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t\tpublic function handle_payment_source_switch( $order, $data = array() ) {\n\t\t\t\treturn llms_add_notice( 'Mock gateway error.', 'error' );\n\t\t\t}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$plan = llms_insert_access_plan( array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price'      => 25.00,\n\t\t\t'frequency'  => 1,\n\t\t) );\n\t\t$order = $this->factory->order->create_and_get( array( 'plan_id' => $plan->get( 'id' ) ) );\n\t\t$order->set( 'payment_gateway', 'manual' );\n\t\t$order->set_status( 'on-hold' );\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'mock-switch-source-err',\n\t\t\t'llms_switch_action'   => 'pay'\n\t\t) );\n\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\t\t$this->assertHasNotice( 'Mock gateway error.', 'error' );\n\n\t\t// Re-initialized the order object.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t$this->assertSame( 0, did_action( 'llms_order_payment_source_switched' ) );\n\t\t$this->assertEquals( 'manual', $order->get( 'payment_gateway' ) );\n\t\t$this->assertEquals( 'llms-on-hold', $order->get( 'status' ) );\n\n\t\t$this->unload_payment_gateway( 'mock-switch-source-err' );\n\n\n\t}\n\n\t/**\n\t * Test switch_payment_source() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_success() {\n\n\t\t$temp_id_handler = function( $order ) {\n\n\t\t\t$this->assertEquals(\n\t\t\t\tarray(\n\t\t\t\t\t'customer'     => 'CUS-123',\n\t\t\t\t\t'source'       => 'SRC-456',\n\t\t\t\t\t'subscription' => 'SUB-789',\n\t\t\t\t),\n\t\t\t\t$order->get_array( 'temp_gateway_ids' )\n\t\t\t);\n\n\t\t};\n\t\tadd_action( 'llms_order_payment_source_switched', $temp_id_handler );\n\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\t$gateway->set_option( 'enabled', 'yes' );\n\n\t\t$actions = did_action( 'llms_order_payment_source_switched' );\n\n\t\t$plan = llms_insert_access_plan( array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price'      => 25.00,\n\t\t\t'frequency'  => 1,\n\t\t) );\n\t\t$order = $this->factory->order->create_and_get( array( 'plan_id' => $plan->get( 'id' ) ) );\n\t\t$order->set( array(\n\t\t\t'payment_gateway'         => 'not-manual',\n\t\t\t'gateway_customer_id'     => 'CUS-123',\n\t\t\t'gateway_source_id'       => 'SRC-456',\n\t\t\t'gateway_subscription_id' => 'SUB-789',\n\t\t) );\n\t\t$order->set_status( 'pending-cancel' );\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_switch_source_nonce' => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'manual',\n\t\t\t'llms_switch_action'   => 'switch',\n\t\t) );\n\n\t\t$this->assertNull( $this->main->switch_payment_source() );\n\n\t\t// Re-initialized the order object.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_order_payment_source_switched' ) );\n\t\t$this->assertEquals( 'manual', $order->get( 'payment_gateway' ) );\n\t\t$this->assertEquals( 'llms-active', $order->get( 'status' ) );\n\n\t\t$gateway->set_option( 'enabled', 'no' );\n\n\t\t$this->assertEmpty( $order->get( 'temp_gateway_ids' ) );\n\n\t\tremove_action( 'llms_order_payment_source_switched', $temp_id_handler );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when the form isn't submitted.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_not_submitted() {\n\t\t$this->assertNull( $this->main->switch_payment_source_ajax() );\n\t\t$this->assertNoticeCountEquals( 0, 'error' );\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() with an invalid nonce.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_invalid_nonce() {\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => 'fake',\n\t\t) );\n\t\t$this->assertNull( $this->main->switch_payment_source_ajax() );\n\t\t$this->assertNoticeCountEquals( 0, 'error' );\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when the order_id is missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_missing_order_id() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'switch-source-order-missing', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when an invalid order ID is supplied.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_invalid_order_id() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'               => 'NaN',\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'switch-source-order-missing', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when the order ID isn't an order.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_not_an_order() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'               => $this->factory->post->create(),\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'switch-source-order-invalid', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when there's no logged in user.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_mismatched_order() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$order = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_order' ) ) );\n\t\t$order->set( 'user_id', 123 );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'               => $order->get( 'id' ),\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'switch-source-order-invalid', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when the gateway is missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_missing_gateway() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$user_id = $this->factory->user->create();\n\t\twp_set_current_user( $user_id );\n\n\t\t$order = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_order' ) ) );\n\t\t$order->set( 'user_id', $user_id );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'               => $order->get( 'id' ),\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'switch-source-gateway-missing', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when there's a gateway validation error.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_gateway_validation_error() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$user_id = $this->factory->user->create();\n\t\twp_set_current_user( $user_id );\n\n\t\t$order = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_order' ) ) );\n\t\t$order->set( 'user_id', $user_id );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'               => $order->get( 'id' ),\n\t\t\t'llms_payment_gateway'   => 'FAKE',\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'gateway-invalid', $res['errors'] );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when the switch action is invalid, fake, or missing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_invalid_switch_action() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\t$gateway->set_option( 'enabled', 'yes' );\n\n\t\t$actions = did_action( 'llms_order_payment_source_switched' );\n\n\t\t$plan = llms_insert_access_plan( array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price'      => 25.00,\n\t\t\t'frequency'  => 1,\n\t\t) );\n\t\t$order = $this->factory->order->create_and_get( array( 'plan_id' => $plan->get( 'id' ) ) );\n\t\t$order->set( 'payment_gateway', 'not-manual' );\n\t\t$order->set_status( 'pending-cancel' );\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'               => $order->get( 'id' ),\n\t\t\t'llms_payment_gateway'   => 'manual',\n\t\t\t'llms_switch_action'     => 'fake',\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'switch-source-action-invalid', $res['errors'] );\n\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() when the gateway handler returns an error.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_gateway_error() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'mock-switch-source-err';\n\t\t\tpublic $supports = array(\n\t\t\t\t'recurring_payments' => true,\n\t\t\t);\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t\tpublic function handle_payment_source_switch( $order, $data = array() ) {\n\t\t\t\treturn new WP_Error( 'mock-gateway-error', 'ERR' );\n\t\t\t}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$plan = llms_insert_access_plan( array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price'      => 25.00,\n\t\t\t'frequency'  => 1,\n\t\t) );\n\t\t$order = $this->factory->order->create_and_get( array( 'plan_id' => $plan->get( 'id' ) ) );\n\t\t$order->set( 'payment_gateway', 'manual' );\n\t\t$order->set_status( 'on-hold' );\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'mock-switch-source-err',\n\t\t\t'llms_switch_action'   => 'pay'\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertArrayHasKey( 'mock-gateway-error', $res['errors'] );\n\n\t\t// Re-initialized the order object.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t$this->assertSame( 0, did_action( 'llms_order_payment_source_switched' ) );\n\t\t$this->assertEquals( 'manual', $order->get( 'payment_gateway' ) );\n\t\t$this->assertEquals( 'llms-on-hold', $order->get( 'status' ) );\n\n\t\t$this->unload_payment_gateway( 'mock-switch-source-err' );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n\t/**\n\t * Test switch_payment_source_ajax() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_switch_payment_source_ajax_success() {\n\n\t\tadd_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'mock-switch-source-success';\n\t\t\tpublic $supports = array(\n\t\t\t\t'recurring_payments' => true,\n\t\t\t);\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t\tpublic function handle_payment_source_switch( $order, $data = array() ) {\n\t\t\t\treturn array( 'yes' => true );\n\t\t\t}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$actions = did_action( 'llms_order_payment_source_switched' );\n\n\t\t$plan = llms_insert_access_plan( array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price'      => 25.00,\n\t\t\t'frequency'  => 1,\n\t\t) );\n\t\t$order = $this->factory->order->create_and_get( array( 'plan_id' => $plan->get( 'id' ) ) );\n\t\t$order->set( 'payment_gateway', 'not-manual' );\n\t\t$order->set_status( 'pending-cancel' );\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t$this->main::AJAX_QS_VAR => wp_create_nonce( $this->main::ACTION_SWITCH_PAYMENT_SOURCE ),\n\t\t\t'order_id'             => $order->get( 'id' ),\n\t\t\t'llms_payment_gateway' => 'mock-switch-source-success',\n\t\t\t'llms_switch_action'   => 'switch',\n\t\t) );\n\n\t\ttry {\n\t\t\tob_start();\n\t\t\t$this->main->switch_payment_source_ajax();\n\t\t} catch ( WPDieException $e ) {}\n\n\t\t$res = json_decode( ob_get_clean(), true );\n\t\t$this->assertEquals( array( 'yes' => true ), $res );\n\n\t\t// Re-initialized the order object.\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t// Ensure order note is recorded.\n\t\tremove_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\t\t$has_note = false;\n\t\tforeach ( $order->get_notes() as $note ) {\n\t\t\tif ( 'Payment source updated by customer. Payment gateway changed from \"not-manual\" to \"mock-switch-source-success\".' === $note->comment_content ) {\n\t\t\t\t$has_note = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tadd_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\t\t$this->assertTrue( $has_note );\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_order_payment_source_switched' ) );\n\t\t$this->assertEquals( 'mock-switch-source-success', $order->get( 'payment_gateway' ) );\n\t\t$this->assertEquals( 'llms-active', $order->get( 'status' ) );\n\n\t\t$gateway->set_option( 'enabled', 'no' );\n\n\t\t$this->unload_payment_gateway( 'mock-switch-source-success' );\n\n\t\tremove_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler') );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-lesson-progression.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Lesson Progression Forms & Functions\n *\n * @group controllers\n * @group lessons\n *\n * @since 3.17.1\n */\nclass LLMS_Test_Controller_Lesson_Progression extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup tests.\n\t *\n\t * @since 3.17.1\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tllms_clear_notices();\n\t\tparent::set_up();\n\t}\n\n\t/**\n\t * Test the handle_admin_managment_forms() method.\n\t *\n\t * @since 3.29.0\n\t * @since 6.10.0 Added tests on user caps.\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_admin_managment_forms() {\n\n\t\t$data = array();\n\n\t\t$class = new LLMS_Controller_Lesson_Progression();\n \t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 2, 'quizzes' => 0 ) );\n \t\t$student_id = $this->factory->student->create_and_enroll( $course->get( 'id' ) );\n\n\t\t// Form not submitted.\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_complete' ) );\n\n\t\t// Form submitted but missing required fields.\n\t\t$data['llms-admin-progression-nonce'] = wp_create_nonce( 'llms-admin-lesson-progression' );\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_complete' ) );\n\n\t\t$data['lesson_id'] = $course->get_lessons( 'ids' )[0];\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_complete' ) );\n\n\t\t$data['student_id'] = $student_id;\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_complete' ) );\n\n\t\t// All data but invalid action...\n\t\t$data['llms-lesson-action'] = 'fake';\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_complete' ) );\n\n\t\t// Mark lessons complete/incomplete as users with no adequate caps, or no user.\n\t\twp_set_current_user( 0 );\n\n\t\t// Mark the lesson complete...\n\t\t$data['llms-lesson-action'] = 'complete';\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_complete' ) );\n\n\t\t// Mark it incomplete...\n\t\t$data['llms-lesson-action'] = 'incomplete';\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_complete' ) );\n\n\t\t// Mark lessons complete/incomplete as users with adequate caps.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'lms_manager' ) ) );\n\t\t$data['llms-admin-progression-nonce'] = wp_create_nonce( 'llms-admin-lesson-progression' );\n\n\t\t// Mark the lesson complete...\n\t\t$data['llms-lesson-action'] = 'complete';\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_mark_complete' ) );\n\n\t\t// Mark it incomplete...\n\t\t$data['llms-lesson-action'] = 'incomplete';\n\t\t$this->mockPostRequest( $data );\n\t\t$class->handle_admin_managment_forms();\n\t\t$this->assertEquals( 3, did_action( 'llms_mark_incomplete' ) ); // @note the mark_incomplete method cascades up and marks parents incomplete even if they're already incomplete, this is possibly a bug..\n\t\t$this->assertEquals( 1, did_action( 'llms_mark_complete' ) );\n\n\t}\n\n\t/**\n\t * Test the submission of the mark lesson complete form\n\t *\n\t * @since 3.17.1\n\t * @since 3.29.0 Unknown.\n\t * @since 6.10.0 Call the tested method directly instead of indirectly via `do_action( 'init' )`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_complete_form() {\n\n\t\t$main = new LLMS_Controller_Lesson_Progression();\n\n\t\t// Form not submitted.\n\t\t$this->mockPostRequest( array() );\n\t\t$main->handle_complete_form();\n\t\t$this->assertEquals( 0, did_action( 'llms_trigger_lesson_completion' ) );\n\n\t\t// Form submitted but missing required fields.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => wp_create_nonce( 'mark_complete' ),\n\t\t) );\n\t\t$main->handle_complete_form();\n\t\t$this->assertEquals( 0, did_action( 'llms_trigger_lesson_completion' ) );\n\n\t\t// Form submitted but invalid lesson id.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => wp_create_nonce( 'mark_complete' ),\n\t\t\t'mark-complete' => 'wut', // Lesson id.\n\t\t\t'mark_complete' => '', // Button.\n\t\t) );\n\t\t$main->handle_complete_form();\n\t\t$this->assertEquals( 0, did_action( 'llms_trigger_lesson_completion' ) );\n\t\t$this->assertEquals( 1, llms_notice_count( 'error' ) );\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] );\n\t\t$lesson_id = $course->get_lessons( 'ids' )[0];\n\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $course->get( 'id' ) );\n\t\twp_set_current_user( $student->get_id() );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => wp_create_nonce( 'mark_complete' ),\n\t\t\t'mark-complete' => $lesson_id, // Lesson id.\n\t\t\t'mark_complete' => '', // Button.\n\t\t) );\n\t\t$main->handle_complete_form();\n\t\t$this->assertEquals( 1, did_action( 'llms_trigger_lesson_completion' ) );\n\t\t$this->assertTrue( $student->is_complete( $lesson_id, 'lesson' ) );\n\n\t}\n\n\t/**\n\t * Test the submission of the mark lesson incomplete form\n\t *\n\t * @since 3.17.1\n\t * @since 3.29.0 Unknown.\n\t * @since 6.10.0 Call the tested method directly instead of indirectly via `do_action( 'init' )`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_incomplete_form() {\n\n\t\t$main = new LLMS_Controller_Lesson_Progression();\n\n\t\t// Form not submitted.\n\t\t$this->mockPostRequest( array() );\n\t\t$main->handle_incomplete_form();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\n\t\t// Form submitted but missing required fields.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => wp_create_nonce( 'mark_incomplete' ),\n\t\t) );\n\t\t$main->handle_incomplete_form();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\n\t\t// Form submitted but invalid lesson id.\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => wp_create_nonce( 'mark_incomplete' ),\n\t\t\t'mark-incomplete' => 'wut', // Lesson id.\n\t\t\t'mark_incomplete' => '', // Button.\n\t\t) );\n\t\t$main->handle_incomplete_form();\n\t\t$this->assertEquals( 0, did_action( 'llms_mark_incomplete' ) );\n\t\t$this->assertEquals( 1, llms_notice_count( 'error' ) );\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] );\n\t\t$lesson_id = $course->get_lessons( 'ids' )[0];\n\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $course->get( 'id' ) );\n\t\t$student->mark_complete( $lesson_id, 'lesson' );\n\t\twp_set_current_user( $student->get_id() );\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'_wpnonce' => wp_create_nonce( 'mark_incomplete' ),\n\t\t\t'mark-incomplete' => $lesson_id, // Lesson id.\n\t\t\t'mark_incomplete' => '', // Button.\n\t\t) );\n\t\t$main->handle_incomplete_form();\n\t\t$this->assertFalse( $student->is_complete( $lesson_id, 'lesson' ) );\n\n\t}\n\n\t/**\n\t * Test the Mark Complete function as triggered by the `llms_trigger_lesson_completion` action\n\t *\n\t * @since 3.17.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_mark_complete() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] );\n\t\t$lesson_id = $course->get_lessons( 'ids' )[0];\n\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\tdo_action( 'llms_trigger_lesson_completion', $student->get( 'id' ), $lesson_id );\n\t\t$this->assertTrue( $student->is_complete( $lesson_id, 'lesson' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-login.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Controller_Registration class\n *\n * @group controllers\n * @group registration\n *\n * @since 3.19.4\n * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib.\n */\nclass LLMS_Test_Controller_Login extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test order completion actions\n\t *\n\t * @since 3.19.4\n\t * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib.\n\t * @since 6.0.0 Replaced use of deprecated items.\n\t *              - `LLMS_UnitTestCase::setup_get()` method with `LLMS_Unit_Test_Mock_Requests::mockGetRequest()`\n\t *              - `LLMS_UnitTestCase::setup_post()` method with `LLMS_Unit_Test_Mock_Requests::mockPostRequest()`\n\t * @since 6.10.0 Call the tested method directly instead of indirectly via `do_action( 'init' )`.\n\t * \n\t * @return void\n\t */\n\tpublic function test_login() {\n\n\t\t$main = new LLMS_Controller_Login();\n\n\t\tLLMS_Install::create_pages();\n\n\t\t// form not submitted\n\t\t$this->mockPostRequest( array() );\n\t\t$main->login();\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_before_user_login' ) );\n\t\t$this->assertEquals( 0, did_action( 'wp_login' ) );\n\n\t\t// not submitted\n\t\t$this->mockGetRequest( array() );\n\t\t$main->login();\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_before_user_login' ) );\n\t\t$this->assertEquals( 0, did_action( 'wp_login' ) );\n\n\t\t// form submitted but missing things\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_login_user_nonce' => wp_create_nonce( 'llms_login_user' ),\n\t\t) );\n\t\t$main->login();\n\t\t$this->assertEquals( 1, did_action( 'lifterlms_before_user_login' ) );\n\t\t$this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) );\n\t\t$this->assertEquals( 0, did_action( 'wp_login' ) );\n\t\tllms_clear_notices();\n\n\t\t// incomplete form\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_login_user_nonce' => wp_create_nonce( 'llms_login_user' ),\n\t\t\t'email_address' => 'fake@mock.org',\n\t\t) );\n\t\t$main->login();\n\t\t$this->assertEquals( 2, did_action( 'lifterlms_before_user_login' ) );\n\t\t$this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) );\n\t\t$this->assertEquals( 0, did_action( 'wp_login' ) );\n\t\tllms_clear_notices();\n\n\t\t$uid = $this->factory->user->create( array(\n\t\t\t'user_email' => 'test@arstarst.com',\n\t\t\t'user_pass' => '123456789',\n\t\t) );\n\n\t\t// this should login a user\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_login_user_nonce' => wp_create_nonce( 'llms_login_user' ),\n\t\t\t'llms_login' => 'test@arstarst.com',\n\t\t\t'llms_password' => '123456789',\n\t\t) );\n\n\t\t// exceptions thrown in testing env instead of exit()\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Exit::class );\n\t\t$this->expectExceptionMessage( sprintf( '%s [302] YES', llms_get_page_url( 'myaccount' ) ) );\n\n\t\t// run these assertions within actions because the exit() at the end of the redirect will halt program execution\n\t\t// and then we'll never get to these assertions!\n\t\tadd_action( 'lifterlms_before_user_login', function() {\n\t\t\t$this->assertEquals( 3, did_action( 'lifterlms_before_user_login' ) );\n\t\t\t$this->assertEquals( 0, llms_notice_count( 'error' ) );\n\t\t} );\n\t\tadd_action( 'wp_login', function( $login, $user ) use ( $uid ) {\n\t\t\t$this->assertEquals( $uid, $user->ID );\n\t\t\t$this->assertEquals( 1, did_action( 'wp_login' ) );\n\t\t\twp_logout();\n\t\t}, 10, 2 );\n\n\t\t$main->login();\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-orders.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Controller_Orders class.\n *\n * @package LifterLMS/Tests\n *\n * @group orders\n *\n * @since 3.19.0\n * @since 3.32.0 Update to use latest action-scheduler functions.\n * @since 3.33.0 Add test for the `on_delete_order` method.\n * @since 3.36.1 When testing deleting/erroring orders make sure to schedule a recurring payment when setting an order as active so that,\n *               when subsequently we error/delete the order, checking the recurring payment is unscheduled makes sense.\n *               Also add tests on recurrint payments not processed when order or user deleted.\n * @since 4.2.0 Added `test_on_user_enrollment_deleted()`.\n * @since 5.4.0 Added test on recurring_charge attempts on orders when related product manually removed.\n */\nclass LLMS_Test_Controller_Orders extends LLMS_UnitTestCase {\n\n\t// Consider dates equal within 60 seconds.\n\tprivate $date_delta = 60;\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since Unknown\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\tLLMS_Site::update_feature( 'recurring_payments', true );\n\n\t}\n\n\t/**\n\t * Disable manual gateway recurring payments for mocking error conditions.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @param array  $supports   Gateway features array.\n\t * @param string $gateway_id Gateway ID.\n\t * @return array\n\t */\n\tpublic function mod_gateway_features( $supports, $gateway_id ) {\n\n\t\tif ( 'manual' === $gateway_id ) {\n\t\t\t$supports['recurring_payments'] = false;\n\t\t}\n\n\t\treturn $supports;\n\n\t}\n\n\t/**\n\t * Test order completion actions.\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of `assertEquals()` with 4th parameter.\n\t *\n\t * @return void\n\t */\n\tpublic function test_complete_order() {\n\n\t\t/**\n\t\t * Tests for one-time payment with no access expiration\n\t\t */\n\t\t$plan = $this->get_mock_plan( '25.99', 0 );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Student not yet enrolled.\n\t\t$this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t\t// Complete the order.\n\t\t$order->set( 'status', 'llms-completed' );\n\n\t\t// Student gets enrolled.\n\t\t$this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t\t// Student now has lifetime access.\n\t\t$this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() );\n\n\t\t// No next payment date.\n\t\t$this->assertTrue( is_a( $order->get_next_payment_due_date(), 'WP_Error' ) );\n\n\t\t// Actions were run.\n\t\t$this->assertEquals( 1, did_action( 'lifterlms_product_purchased' ) );\n\t\t$this->assertEquals( 1, did_action( 'lifterlms_access_plan_purchased' ) );\n\n\t\t/**\n\t\t * Tests for one-time payment with access expiration\n\t\t */\n\t\t$plan = $this->get_mock_plan( '25.99', 0, 'limited-date' );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Student not yet enrolled.\n\t\t$this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t\t// Complete the order.\n\t\t$order->set( 'status', 'llms-completed' );\n\n\t\t// Student gets enrolled.\n\t\t$this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t\t// Student will expire based on expiration settings.\n\t\t$this->assertEquals( date( 'Y-m-d', current_time( 'timestamp' ) + DAY_IN_SECONDS ), $order->get_access_expiration_date() );\n\n\t\t// No next payment date.\n\t\t$this->assertTrue( is_a( $order->get_next_payment_due_date(), 'WP_Error' ) );\n\n\t\t// Actions were run.\n\t\t$this->assertEquals( 2, did_action( 'lifterlms_product_purchased' ) );\n\t\t$this->assertEquals( 2, did_action( 'lifterlms_access_plan_purchased' ) );\n\n\t\t/**\n\t\t * Tests for recurring payment\n\t\t */\n\t\t$plan = $this->get_mock_plan( '25.99', 1 );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Student not yet enrolled.\n\t\t$this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t\t// Complete the order.\n\t\t$order->set( 'status', 'llms-active' );\n\n\t\t// Student gets enrolled.\n\t\t$this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t\t// Student now has lifetime access.\n\t\t$this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() );\n\n\t\t// Next payment date.\n\t\t$this->assertEqualsWithDelta( (float) date( 'U', current_time( 'timestamp' ) + DAY_IN_SECONDS ), (float) $order->get_next_payment_due_date( 'U' ), $this->date_delta );\n\n\t\t// Actions were run.\n\t\t$this->assertEquals( 3, did_action( 'lifterlms_product_purchased' ) );\n\t\t$this->assertEquals( 3, did_action( 'lifterlms_access_plan_purchased' ) );\n\n\t\t// Cancel the order to test reactivation.\n\t\t$this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() );\n\t\t$order->set( 'status', 'llms-pending-cancel' );\n\t\t$order->set( 'status', 'llms-active' );\n\n\t\t// Should still have lifetime access after reactivation.\n\t\t$this->assertEquals( 'Lifetime Access', $order->get_access_expiration_date() );\n\n\t\t// Expiration event should be cleared.\n\t\t$this->assertFalse( as_next_scheduled_action( 'llms_access_plan_expiration', array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t) ) );\n\n\t\t// Test a limited date order for reactivation events.\n\t\t$plan = $this->get_mock_plan( '25.99', 1, 'limited-date' );\n\t\t$order = $this->get_mock_order( $plan );\n\t\t$order->set( 'status', 'llms-pending-cancel' );\n\t\t$order->set( 'status', 'llms-active' );\n\t\t$this->assertEquals( date( 'Y-m-d', current_time( 'timestamp' ) + DAY_IN_SECONDS ), $order->get_access_expiration_date( 'Y-m-d' ) );\n\n\t\t// Expiration event should be reset.\n\t\t$this->assertEqualsWithDelta(\n\t\t\t(float) $order->get_access_expiration_date( 'U' ),\n\t\t\t(float) as_next_scheduled_action(\n\t\t\t\t'llms_access_plan_expiration',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t), $this->date_delta );\n\n\t}\n\n\t/**\n\t * Test deprecated create_pending_order().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @expectedDeprecated LLMS_Controller_Orders::create_pending_order\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_pending_order() {\n\t\t$controller = new LLMS_Controller_Orders();\n\t\t$res = $controller->create_pending_order();\n\t}\n\n\t/**\n\t * Test deprecated confirm_pending_order().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @expectedDeprecated LLMS_Controller_Orders::confirm_pending_order\n\t *\n\t * @return void\n\t */\n\tpublic function test_confirm_pending_order() {\n\t\t$controller = new LLMS_Controller_Orders();\n\t\t$controller->confirm_pending_order();\n\t}\n\n\t/**\n\t * Test order error statuses.\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 3.36.1 Make sure to schedule a recurring payment when setting an order as active so that,\n\t *               when subsequently we error the order, checking the recurring payment is unscheduled makes sense.\n\t * @since 5.2.0 Test upcoming payment reminder.\n\t *\n\t * @return void\n\t */\n\tpublic function test_error_order() {\n\n\t\t$err_statuses = array(\n\t\t\t'llms-refunded' => 'cancelled',\n\t\t\t'llms-cancelled' => 'cancelled',\n\t\t\t'llms-expired' => 'expired',\n\t\t\t'llms-failed' => 'expired',\n\t\t\t'llms-on-hold' => 'cancelled',\n\t\t\t'llms-trash' => 'cancelled',\n\t\t);\n\n\t\tforeach ( $err_statuses as $status => $enrollment_status ) {\n\n\t\t\t$order = $this->get_mock_order();\n\n\t\t\t$student = llms_get_student( $order->get( 'user_id' ) );\n\n\t\t\t// Schedule payments & enroll the student.\n\t\t\t$order->set( 'status', 'llms-active' );\n\n\t\t\t$order->maybe_schedule_payment();\n\n\t\t\t// Recurring payment is scheduled.\n\t\t\t$this->assertEquals(\n\t\t\t\t$order->get_next_payment_due_date( 'U' ),\n\t\t\t\tas_next_scheduled_action(\n\t\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Error the order.\n\t\t\t$order->set( 'status', $status );\n\n\t\t\t// Student should be removed.\n\t\t\t$this->assertFalse( $student->is_enrolled( $order->get( 'product_id' ) ) );\n\n\t\t\t// Status should be changed.\n\t\t\t$this->assertEquals( $enrollment_status, $student->get_enrollment_status( $order->get( 'product_id' ) ) );\n\n\t\t\t// Recurring payment is unscheduled.\n\t\t\t$this->assertFalse(\n\t\t\t\tas_next_scheduled_action(\n\t\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Upcoming payment reminder is unscheduled.\n\t\t\t$this->assertFalse(\n\t\t\t\tas_next_scheduled_action(\n\t\t\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test delete order.\n\t *\n\t * @since 3.33.0\n\t * @since 3.36.1 Check recurring payment is unscheduled.\n\t * @since 5.2.0 Test upcoming payment reminder.\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_delete_order() {\n\n\t\t$order   = $this->get_mock_order();\n\t\t$student = llms_get_student( $order->get( 'user_id' ) );\n\n\t\t$order_product_id = $order->get( 'product_id' );\n\n\t\t// Schedule payments & enroll the student.\n\t\t$order->set( 'status', 'llms-active' );\n\n\t\t$order->maybe_schedule_payment();\n\n\t\t// Recurring payment is scheduled.\n\t\t$this->assertEquals(\n\t\t\t$order->get_next_payment_due_date( 'U' ),\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Delete order.\n\t\twp_delete_post( $order->get( 'id' ), false );\n\n\t\t// Student should be removed.\n\t\t$this->assertFalse( $student->is_enrolled( $order_product_id ) );\n\n\t\t// More in depth checks.\n\t\t// Enrollment status must be false.\n\t\t$this->assertFalse( $student->get_enrollment_status( $order_product_id ) );\n\n\t\t// Enrollment trigger must be false.\n\t\t$this->assertFalse( $student->get_enrollment_trigger( $order_product_id ) );\n\n\t\t// Enrollment date must be false.\n\t\t$this->assertFalse( $student->get_enrollment_date( $order_product_id ) );\n\n\t\t// Recurring payment is unscheduled.\n\t\t$this->assertFalse(\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Upcoming payment reminder is unscheduled.\n\t\t$this->assertFalse(\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Test on user enrollment deleted.\n\t *\n\t * The controller's `on_user_enrollment_deleted()` method is reponsible of changing the order status to `cancelled`\n\t * in reaction to the deletion of an enrollment with the same order as trigger.\n\t *\n\t * @since 4.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_on_user_enrollment_deleted() {\n\n\t\t$order            = $this->get_mock_order();\n\t\t$student_id       = $order->get( 'user_id' );\n\t\t$order_product_id = $order->get( 'product_id' );\n\t\t$order_id         = $order->get( 'id' );\n\n\t\t// Enroll the student.\n\t\t$order->set( 'status', 'llms-active' );\n\n\t\t$order_cancelled_actions = did_action( 'lifterlms_order_status_cancelled' );\n\n\t\t$fake_order_id = $order_id + 999;\n\n\t\t// Delete user enrollment passing a fake order as trigger.\n\t\tllms_delete_student_enrollment( $student_id, $order_product_id, \"order_{$fake_order_id}\" );\n\t\t$this->assertEquals( $order_cancelled_actions, did_action( 'lifterlms_order_status_cancelled' ) );\n\t\t// Check order status.\n\t\t$this->assertEquals( 'llms-active', llms_get_post( $order_id )->get( 'status' ) );\n\n\t\t// Delete user enrollment.\n\t\tllms_delete_student_enrollment( $student_id, $order_product_id, \"order_{$order_id}\" );\n\t\t$this->assertEquals( $order_cancelled_actions + 1, did_action( 'lifterlms_order_status_cancelled' ) );\n\t\t// Check order status.\n\t\t$this->assertEquals( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) );\n\n\t\t$order_cancelled_actions = did_action( 'lifterlms_order_status_cancelled' );\n\n\t\t// Check that trying to delete it again doesn't trigger the action again.\n\t\tllms_delete_student_enrollment( $student_id, $order_product_id, \"order_{$order_id}\" );\n\t\t$this->assertEquals( $order_cancelled_actions, did_action( 'lifterlms_order_status_cancelled' ) );\n\t\t// Check order status.\n\t\t$this->assertEquals( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) );\n\n\t\t// Enroll the student again on the same course with a different trigger.\n\t\t$student = llms_get_student( $student_id );\n\t\tllms_enroll_student( $student_id, $order_product_id );\n\n\t\tllms_delete_student_enrollment( $student_id, $order_product_id, \"order_{$order_id}\" );\n\t\t$this->assertEquals( $order_cancelled_actions, did_action( 'lifterlms_order_status_cancelled' ) );\n\t\t// Check order status.\n\t\t$this->assertEquals( 'llms-cancelled', llms_get_post( $order_id )->get( 'status' ) );\n\n\t}\n\n\t/**\n\t * Test expire access function.\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 5.2.0 Test upcoming payment reminder.\n\t *\n\t * @return void\n\t */\n\tpublic function test_expire_access() {\n\n\t\t// Recurring -> expire via access settings.\n\t\t$plan = $this->get_mock_plan( '25.99', 1, 'limited-date' );\n\t\t$order = $this->get_mock_order( $plan );\n\t\t$order->set_status( 'active' );\n\t\t$student = llms_get_student( $order->get( 'user_id' ) );\n\n\t\tdo_action( 'llms_access_plan_expiration', $order->get( 'id' ) );\n\n\t\t$this->assertFalse( $student->is_enrolled( $order->get( 'product_id' ) ) );\n\n\t\t// Recurring payment is not scheduled.\n\t\t$this->assertFalse(\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Upcoming payment reminder is not scheduled.\n\t\t$this->assertFalse(\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertEquals( 'expired', $student->get_enrollment_status( $order->get( 'product_id' ) ) );\n\t\t$this->assertEquals( 'llms-active', $order->get( 'status' ) );\n\n\t\t// Simulate a pending-cancel -> cancel.\n\t\t$plan = $this->get_mock_plan( '25.99', 1, 'limited-date' );\n\t\t$order = $this->get_mock_order( $plan );\n\t\t$order->set_status( 'active' );\n\t\t$order->set_status( 'pending-cancel' );\n\t\t$student = llms_get_student( $order->get( 'user_id' ) );\n\n\t\tdo_action( 'llms_access_plan_expiration', $order->get( 'id' ) );\n\n\t\t$this->assertFalse( $student->is_enrolled( $order->get( 'product_id' ) ) );\n\n\t\t// Recurring payment is not scheduled.\n\t\t$this->assertFalse(\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Upcoming payment reminder is not scheduled.\n\t\t$this->assertFalse(\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertEquals( 'cancelled', $student->get_enrollment_status( $order->get( 'product_id' ) ) );\n\t\t$this->assertEquals( 'llms-cancelled', get_post_status( $order->get( 'id' ) ) );\n\n\t}\n\n\t/**\n\t * Test set_untrash_status().r\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_untrash_status() {\n\n\t\t$controller = new LLMS_Controller_Orders();\n\n\t\t$tests = array(\n\t\t\t'post'       => 'draft',\n\t\t\t'page'       => 'draft',\n\t\t\t'course'     => 'draft',\n\t\t\t'llms_order' => 'llms-pending',\n\t\t);\n\t\tforeach ( $tests as $post_type => $expected ) {\n\t\t\t$post_id = $this->factory->post->create( compact( 'post_type' ) );\n\n\t\t\t$this->assertEquals(\n\t\t\t\t$expected,\n\t\t\t\t$controller->set_untrash_status( 'draft', $post_id, 'publish' )\n\t\t\t);\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test recurring_charge attempts on orders manually removed from the database.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_charge_on_manually_deleted_order() {\n\n\t\t$plan     = $this->get_mock_plan( '200.00', 1 );\n\t\t$order    = $this->get_mock_order( $plan );\n\t\t$order_id = $order->get( 'id' );\n\n\t\t// Starting action numbers.\n\t\t$note_actions      = did_action( 'llms_new_order_note_added' );\n\t\t$err_gw_actions    = did_action( 'llms_order_recurring_charge_gateway_error' );\n\t\t$pdue_actions      = did_action( 'llms_manual_payment_due' );\n\t\t$err_order_actions = did_action( 'llms_order_recurring_charge_gateway_error' );\n\t\t$err_user_actions  = did_action( 'llms_order_recurring_charge_user_error' );\n\n\t\t// Emulate a manul order deletion from the db.\n\t\tglobal $wpdb;\n\t\t$wpdb->delete( $wpdb->prefix . 'posts', array( 'id' => $order_id ) );\n\t\tclean_post_cache( $order_id );\n\n\t\t// Trigger recurring payment.\n\t\tdo_action( 'llms_charge_recurring_payment', $order_id );\n\n\t\t$this->assertSame( $pdue_actions, did_action( 'llms_manual_payment_due' ) );\n\t\t$this->assertSame( $note_actions, did_action( 'llms_new_order_note_added' ) );\n\t\t$this->assertSame( $err_gw_actions, did_action( 'llms_order_recurring_charge_gateway_error' ) );\n\t\t$this->assertSame( $err_order_actions + 1, did_action( 'llms_order_recurring_charge_order_error' ) );\n\t\t$this->assertSame( $err_user_actions, did_action( 'llms_order_recurring_charge_user_error' ) );\n\n\t}\n\n\n\t/**\n\t * Test recurring_charge attempts on orders whose user has been deleted.\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_charge_on_deleted_user() {\n\n\t\t$plan     = $this->get_mock_plan( '200.00', 1 );\n\t\t$order    = $this->get_mock_order( $plan );\n\t\t$order_id = $order->get( 'id' );\n\n\t\t// Starting action numbers.\n\t\t$note_actions      = did_action( 'llms_new_order_note_added' );\n\t\t$err_gw_actions    = did_action( 'llms_order_recurring_charge_gateway_error' );\n\t\t$pdue_actions      = did_action( 'llms_manual_payment_due' );\n\t\t$err_order_actions = did_action( 'llms_order_recurring_charge_gateway_error' );\n\t\t$err_user_actions  = did_action( 'llms_order_recurring_charge_user_error' );\n\n\t\t// Emulate an user deletion.\n\t\twp_delete_user( $order->get( 'user_id' ) );\n\n\t\t// Trigger recurring payment.\n\t\tdo_action( 'llms_charge_recurring_payment', $order_id );\n\n\t\t$this->assertSame( $pdue_actions, did_action( 'llms_manual_payment_due' ) );\n\t\t$this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) );\n\t\t$this->assertSame( $err_gw_actions, did_action( 'llms_order_recurring_charge_gateway_error' ) );\n\t\t$this->assertSame( $err_order_actions, did_action( 'llms_order_recurring_charge_order_error' ) );\n\t\t$this->assertSame( $err_user_actions + 1, did_action( 'llms_order_recurring_charge_user_error' ) );\n\n\t}\n\n\t/**\n\t * Test gateway-related errors encountered during a recurring_charge attempt.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_charge_gateway_errors() {\n\n\t\t$plan = $this->get_mock_plan( '200.00', 1 );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$order->set( 'payment_gateway', 'fake-gateway' );\n\n\t\t// Starting action numbers.\n\t\t$note_actions = did_action( 'llms_new_order_note_added' );\n\t\t$err_actions = did_action( 'llms_order_recurring_charge_gateway_error' );\n\n\t\t// Trigger recurring payment.\n\t\tdo_action( 'llms_charge_recurring_payment', $order->get( 'id' ) );\n\n\t\t$this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) );\n\t\t$this->assertSame( $err_actions + 1, did_action( 'llms_order_recurring_charge_gateway_error' ) );\n\n\t}\n\n\t/**\n\t * Test a recurring payment processed when recurring payments are disabled on the site.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_charge_staging_mode() {\n\n\t\t// Disable recurring payments.\n\t\tLLMS_Site::update_feature( 'recurring_payments', false );\n\n\t\t$plan = $this->get_mock_plan( '200.00', 1 );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Starting action numbers.\n\t\t$skip_actions = did_action( 'llms_order_recurring_charge_skipped' );\n\t\t$note_actions = did_action( 'llms_new_order_note_added' );\n\n\t\t// Trigger recurring payment.\n\t\tdo_action( 'llms_charge_recurring_payment', $order->get( 'id' ) );\n\n\t\t$this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) );\n\t\t$this->assertSame( $skip_actions + 1, did_action( 'llms_order_recurring_charge_skipped' ) );\n\n\t}\n\n\t/**\n\t * Test gateway-related errors encountered during a recurring_charge attempt.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_charge_gateway_support_disabled() {\n\n\t\t$plan = $this->get_mock_plan( '200.00', 1 );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Disable recurring payments.\n\t\tadd_filter( 'llms_get_gateway_supported_features', array( $this, 'mod_gateway_features' ), 10, 2 );\n\n\t\t// Starting action numbers.\n\t\t$err_actions = did_action( 'llms_order_recurring_charge_gateway_payments_disabled' );\n\t\t$note_actions = did_action( 'llms_new_order_note_added' );\n\n\t\t// Trigger recurring payment.\n\t\tdo_action( 'llms_charge_recurring_payment', $order->get( 'id' ) );\n\n\t\t$this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) );\n\t\t$this->assertSame( $err_actions + 1, did_action( 'llms_order_recurring_charge_gateway_payments_disabled' ) );\n\n\t\t// Re-enable recurring payments.\n\t\tremove_filter( 'llms_get_gateway_supported_features', array( $this, 'mod_gateway_features' ), 10, 2 );\n\n\t}\n\n\t/**\n\t * Test recurring_charge attempts on orders when related product manually removed.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_charge_on_manually_deleted_product() {\n\n\t\t$plan = $this->get_mock_plan( '200.00', 1 );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$student = llms_get_student( $order->get( 'user_id' ) );\n\n\t\t// Schedule payments & enroll the student.\n\t\t$order->set( 'status', 'llms-active' );\n\n\t\t// Recurring payment is scheduled.\n\t\t$this->assertEquals(\n\t\t\t$order->get_next_payment_due_date( 'U' ),\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Student gets enrolled.\n\t\t$this->assertTrue( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t\t// Manually remove product.\n\t\tglobal $wpdb;\n\t\t$wpdb->delete(\n\t\t\t$wpdb->posts,\n\t\t\tarray(\n\t\t\t\t'ID' => $order->get( 'product_id' ),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'%d',\n\t\t\t)\n\t\t);\n\t\tclean_post_cache( $order->get( 'product_id' ) );\n\n\t\t// Starting action numbers.\n\t\t$err_actions  = did_action( 'llms_order_recurring_charge_aborted_product_deleted' );\n\t\t$note_actions = did_action( 'llms_new_order_note_added' );\n\n\t\t// Trigger recurring payment.\n\t\tdo_action( 'llms_charge_recurring_payment', $order->get( 'id' ) );\n\n\t\t$this->assertSame( $note_actions + 1, did_action( 'llms_new_order_note_added' ) );\n\t\t$this->assertSame( $err_actions + 1, did_action( 'llms_order_recurring_charge_aborted_product_deleted' ) );\n\n\t\t// Recurring payment is unscheduled.\n\t\t$this->assertFalse(\n\t\t\tas_next_scheduled_action(\n\t\t\t\t'llms_charge_recurring_payment',\n\t\t\t\tarray(\n\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Student unenrolled.\n\t\t$this->assertFalse( llms_is_user_enrolled( $order->get( 'user_id' ), $order->get( 'product_id' ) ) );\n\n\t}\n\n\t/**\n\t * Test gateway-related errors encountered during a recurring_charge attempt.\n\t *\n\t * @since 3.32.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_recurring_charge_success() {\n\n\t\t$plan = $this->get_mock_plan( '200.00', 1 );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Starting action numbers.\n\t\t$actions = did_action( 'llms_manual_payment_due' );\n\n\t\t// Trigger recurring payment.\n\t\tdo_action( 'llms_charge_recurring_payment', $order->get( 'id' ) );\n\n\t\t$this->assertSame( $actions + 1, did_action( 'llms_manual_payment_due' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/controllers/class-llms-test-controller-registration.php",
    "content": "<?php\n/**\n * Tests for the LLMS_Controller_Registration class\n *\n * @group controllers\n * @group registration\n * @group controller_registration\n *\n * @since 3.19.4\n * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib.\n * @since 5.0.0 Install forms during setup.\n */\nclass LLMS_Test_Controller_Registration extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test registration form submission.\n\t *\n\t * @since 3.19.4\n\t * @since 3.34.0 Use `LLMS_Unit_Test_Exception_Exit` from tests lib.\n\t * @since 5.0.0 Install forms during setup.\n\t * @since 6.0.0 Replaced use of deprecated items.\n\t *              - `LLMS_UnitTestCase::setup_get()` method with `LLMS_Unit_Test_Mock_Requests::mockGetRequest()`\n\t *              - `LLMS_UnitTestCase::setup_post()` method with `LLMS_Unit_Test_Mock_Requests::mockPostRequest()`\n\t * @since 6.10.0 Call the tested method directly instead of indirectly via `do_action( 'init' )`.\n\t * \n\t * @return void\n\t */\n\tpublic function test_register() {\n\n\t\t$main = new LLMS_Controller_Registration();\n\n\t\tLLMS_Install::create_pages();\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t// form not submitted\n\t\t$this->mockPostRequest( array() );\n\t\t$main->register();\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_before_new_user_registration' ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) );\n\n\t\t// not submitted\n\t\t$this->mockGetRequest( array() );\n\t\t$main->register();\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_before_new_user_registration' ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) );\n\n\t\t// form submitted but missing things\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ),\n\t\t) );\n\t\t$main->register();\n\t\t$this->assertEquals( 1, did_action( 'lifterlms_before_new_user_registration' ) );\n\t\t$this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) );\n\t\tllms_clear_notices();\n\n\t\t// user already logged in\n\t\t$uid = $this->factory->user->create();\n\t\twp_set_current_user( $uid );\n\t\t// form submitted but missing things\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ),\n\t\t) );\n\t\t$main->register();\n\t\t$this->assertEquals( 2, did_action( 'lifterlms_before_new_user_registration' ) );\n\t\t$this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) );\n\t\tllms_clear_notices();\n\n\t\t// log that user out\n\t\twp_set_current_user( null );\n\n\t\t// incomplete form\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ),\n\t\t\t'user_login' => '',\n\t\t\t'email_address' => 'fake@mock.org',\n\t\t\t'password' => 'owb2g1pICH82',\n\t\t) );\n\t\t$main->register();\n\t\t$this->assertEquals( 3, did_action( 'lifterlms_before_new_user_registration' ) );\n\t\t$this->assertTrue( ( llms_notice_count( 'error' ) >= 1 ) );\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_user_registered' ) );\n\t\tllms_clear_notices();\n\n\t\t// this should register a user\n\t\t$this->mockPostRequest( array(\n\t\t\t'_llms_register_person_nonce' => wp_create_nonce( 'llms_register_person' ),\n\t\t\t'user_login' => '',\n\t\t\t'email_address' => 'fake@mock.org',\n\t\t\t'email_address_confirm' => 'fake@mock.org',\n\t\t\t'password' => 'owb2g1pICH82',\n\t\t\t'password_confirm' => 'owb2g1pICH82',\n\t\t\t'first_name' => 'David',\n\t\t\t'last_name' => 'Stevens',\n\t\t\t'llms_billing_address_1' => 'Voluptatem',\n\t\t\t'llms_billing_address_2' => '#12345',\n\t\t\t'llms_billing_city' => 'Harum est dolorum sed vel perspiciatis consequatur dignissimos possimus delectus quos optio omnis error quas rem dicta et consectetur odio',\n\t\t\t'llms_billing_state' => 'Esse ea est dolore sed sunt ipsum a ut nemo dolorem aut aliquam cillum asperiores minim culpa',\n\t\t\t'llms_billing_zip' => '72995',\n\t\t\t'llms_billing_country' => 'US',\n\t\t\t'llms_voucher' => '',\n\t\t\t'llms_mc_consent' => 'yes',\n\t\t\t'llms_agree_to_terms' => 'yes',\n\t\t) );\n\n\t\t// exceptions thrown in testing env instead of exit()\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Exit::class );\n\t\t$this->expectExceptionMessage( sprintf( '%s [302] YES', llms_get_page_url( 'myaccount' ) ) );\n\n\t\t// run these assertions within actions because the exit() at the end of the redirect will halt program execution\n\t\t// and then we'll never get to these assertions!\n\t\tadd_action( 'lifterlms_before_new_user_registration', function() {\n\t\t\t$this->assertEquals( 4, did_action( 'lifterlms_before_new_user_registration' ) );\n\t\t\t$this->assertEquals( 0, llms_notice_count( 'error' ) );\n\t\t} );\n\t\tadd_action( 'lifterlms_user_registered', function() {\n\t\t\t$this->assertEquals( 1, did_action( 'lifterlms_user_registered' ) );\n\t\t} );\n\n\t\t$main->register();\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-form-field.php",
    "content": "<?php\n/**\n * Test LLMS_Form_Field class\n *\n * @package LifterLMS/Tests\n *\n * @group form_field\n *\n * @since 5.0.0\n * @since 5.10.0 Update tests on password strength meter enqueueing.\n * @version 5.10.0\n */\nclass LLMS_Test_Form_Field extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Returns an array of attributes for a 'llms/form-field-checkboxes' block to be serialized into\n\t * a form's `post_content`.\n\t *\n\t * @since 6.2.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_checkboxes_attributes() {\n\n\t\t$checkboxes = array(\n\t\t\t'datastore' => 'user_meta',\n\t\t\t'type'      => 'checkbox',\n\t\t\t'id'        => 'matrix-choices-1',\n\t\t\t'name'      => 'matrix_choices_1',\n\t\t\t'label'     => 'Matrix Choices',\n\t\t\t'options'   => array(\n\t\t\t\tarray(\n\t\t\t\t\t'key'  => 'blue_pill',\n\t\t\t\t\t'text' => 'Believe whatever you want to believe.',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'key'  => 'red_pill',\n\t\t\t\t\t'text' => 'I show you how deep the rabbit hole goes',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\treturn $checkboxes;\n\t}\n\n\t/**\n\t * Retrieve a new user with specified user meta data.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $meta_key Meta key name.\n\t * @param string $meta_val Meta value (optional).\n\t * @return int WP_User ID.\n\t */\n\tprivate function get_user_with_meta( $meta_key, $meta_val = '' ) {\n\n\t\t$uid = $this->factory->user->create();\n\t\tupdate_user_meta( $uid, $meta_key, $meta_val );\n\n\t\twp_set_current_user( $uid );\n\n\t\treturn $uid;\n\n\t}\n\n\t/**\n\t * teardown the test case.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\twp_set_current_user( null );\n\n\t}\n\n\t/**\n\t * Test the 'explode_options_to_fields()' method.\n\t *\n\t * @since 6.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_explode_options_to_fields() {\n\n\t\t$checkboxes = $this->get_checkboxes_attributes();\n\n\t\t// The user has checked the 2nd option.\n\t\t$user_id = $this->factory->user->create();\n\t\twp_set_current_user( $user_id );\n\t\tadd_user_meta( $user_id, $checkboxes['name'], array( $checkboxes['options'][1]['key'] ) );\n\n\t\t$field_with_options = new LLMS_Form_Field( $checkboxes );\n\n\t\t// Test on a form that is not hidden.\n\t\t$fields = $field_with_options->explode_options_to_fields( false );\n\t\t$this->assertCount( count( $checkboxes['options'] ), $fields );\n\n\t\tforeach ( $fields as $index => $field ) {\n\t\t\t$expected_key = $checkboxes['options'][ $index ]['key'];\n\t\t\t$settings     = $field->get_settings();\n\n\t\t\t$this->assertEquals( ( 1 === $index ), $settings['checked'] );\n\t\t\t$this->assertFalse( $settings['data_store'] );\n\n\t\t\t$equals = array(\n\t\t\t\t\"{$checkboxes['id']}--{$expected_key}\"   => $settings['id'],\n\t\t\t\t\"{$checkboxes['name']}[]\"                => $settings['name'],\n\t\t\t\t$checkboxes['options'][ $index ]['text'] => $settings['label'],\n\t\t\t\t'checkbox'                               => $settings['type'],\n\t\t\t\t$expected_key                            => $settings['value'],\n\t\t\t);\n\t\t\t$this->assertEquals( array_keys( $equals ), array_values( $equals ) );\n\t\t}\n\n\t\t// Test on a form that is hidden.\n\t\t$fields = $field_with_options->explode_options_to_fields( true );\n\t\t$this->assertCount( 1, $fields );\n\n\t\tforeach ( $fields as $field ) {\n\t\t\t$expected_key = $checkboxes['options'][1]['key'];\n\t\t\t$settings     = $field->get_settings();\n\n\t\t\t$this->assertEquals( true, $settings['checked'] );\n\t\t\t$this->assertFalse( $settings['data_store'] );\n\n\t\t\t$equals = array(\n\t\t\t\t\"{$checkboxes['id']}--{$expected_key}\" => $settings['id'],\n\t\t\t\t\"{$checkboxes['name']}[]\"              => $settings['name'],\n\t\t\t\t$checkboxes['options'][1]['text']      => $settings['label'],\n\t\t\t\t'hidden'                               => $settings['type'],\n\t\t\t\t$expected_key                          => $settings['value'],\n\t\t\t);\n\t\t\t$this->assertEquals( array_keys( $equals ), array_values( $equals ) );\n\t\t}\n\t}\n\n\t/**\n\t * Test output of a hidden input field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_hidden() {\n\n\t\t$this->assertEquals( '<input class=\"llms-field-input\" id=\"mock-id\" name=\"mock-id\" type=\"hidden\" value=\"1\" />', llms_form_field( array( 'type' => 'hidden', 'id' => 'mock-id', 'value' => '1' ), false ) );\n\n\t}\n\n\t/**\n\t * Test output of a select field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_select() {\n\n\t\t$opts = array(\n\t\t\t'type' => 'select',\n\t\t\t'options' => array(\n\t\t\t\t'mock' => 'MOCK',\n\t\t\t\t'fake' => 'FAKE',\n\t\t\t),\n\t\t);\n\n\t\t$html = llms_form_field( $opts, false );\n\n\t\t$this->assertStringContains( '<select class=\"llms-field-select', $html );\n\t\t$this->assertStringContains( '<option value=\"mock\">MOCK</option>', $html );\n\t\t$this->assertStringContains( '<option value=\"fake\">FAKE</option>', $html );\n\n\t\t// With selected value.\n\t\t$opts['selected'] = 'fake';\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( '<option value=\"fake\" selected=\"selected\">FAKE</option>', $html );\n\n\t\tunset( $opts['selected'] );\n\n\t\t// With default value.\n\t\t$opts['default'] = 'fake';\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( '<option value=\"fake\" selected=\"selected\">FAKE</option>', $html );\n\n\t}\n\n\t/**\n\t * Test select field with user data.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_select_with_user_data() {\n\n\t\t$opts = array(\n\t\t\t'type'           => 'select',\n\t\t\t'data_store_key' => 'select_data',\n\t\t\t'selected'       => 'mock',\n\t\t\t'options'        => array(\n\t\t\t\t'mock' => 'MOCK',\n\t\t\t\t'fake' => 'FAKE',\n\t\t\t),\n\t\t);\n\n\t\t// Uses default value.\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( '<option value=\"mock\" selected=\"selected\">MOCK</option>', $html );\n\t\t$this->assertStringNotContains( '<option value=\"fake\" selected=\"selected\">FAKE</option>', $html );\n\n\t\t// No meta saved for user, uses default.\n\t\t$this->get_user_with_meta( 'other', '' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( '<option value=\"mock\" selected=\"selected\">MOCK</option>', $html );\n\t\t$this->assertStringNotContains( '<option value=\"fake\" selected=\"selected\">FAKE</option>', $html );\n\n\t\t// Use user's value.\n\t\t$this->get_user_with_meta( 'select_data', 'fake' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringNotContains( '<option value=\"mock\" selected=\"selected\">MOCK</option>', $html );\n\t\t$this->assertStringContains( '<option value=\"fake\" selected=\"selected\">FAKE</option>', $html );\n\n\t}\n\n\t/**\n\t * Test select field with an option group.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_select_opt_group() {\n\n\t\t$opts = array(\n\t\t\t'type'           => 'select',\n\t\t\t'data_store_key' => 'select_data',\n\t\t\t'options'        => array(\n\t\t\t\tarray(\n\t\t\t\t\t'label' => __( 'Group 1', 'lifterlms' ),\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'opt1' => __( 'Option 1', 'lifterlms' ),\n\t\t\t\t\t\t'opt2' => __( 'Option 2', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'label' => __( 'Group 2', 'lifterlms' ),\n\t\t\t\t\t'options' => array(\n\t\t\t\t\t\t'opt3' => __( 'Option 3', 'lifterlms' ),\n\t\t\t\t\t\t'opt4' => __( 'Option 4', 'lifterlms' ),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\t$html = llms_form_field( $opts, false );\n\n\t\t$this->assertStringContains( '<optgroup label=\"Group 1\" data-key=\"0\">', $html );\n\t\t$this->assertStringContains( '<optgroup label=\"Group 2\" data-key=\"1\">', $html );\n\n\t\tfor ( $i = 1; $i <= 4; $i++ ) {\n\t\t\t$this->assertStringContains( sprintf( '<option value=\"opt%1$d\">Option %1$d</option>', $i ), $html );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test radio field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_radio() {\n\n\t\t$opts = array(\n\t\t\t'type'  => 'radio',\n\t\t\t'value' => 'mock_val',\n\t\t);\n\n\t\t$html = llms_form_field( $opts, false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-radio', $html );\n\t\t$this->assertStringContains( '<input class=\"llms-field-radio\"', $html );\n\t\t$this->assertStringContains( 'type=\"radio\"', $html );\n\t\t$this->assertStringContains( 'value=\"mock_val\"', $html );\n\t\t$this->assertStringNotContains( 'checked=\"checked\"', $html );\n\n\t\t// checked.\n\t\t$opts['checked'] = true;\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( 'checked=\"checked\"', $html );\n\n\t}\n\n\t/**\n\t * Test radio field with a user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_radio_with_user() {\n\n\t\t$opts = array(\n\t\t\t'id'    => 'radio_store',\n\t\t\t'type'  => 'radio',\n\t\t\t'value' => 'mock_val',\n\t\t);\n\n\t\t// User doesn't have value stored.\n\t\t$this->get_user_with_meta( 'radio_store' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringNotContains( 'checked=\"checked\"', $html );\n\n\t\t$this->get_user_with_meta( 'radio_store', 'mock_val' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( 'checked=\"checked\"', $html );\n\n\t}\n\n\t/**\n\t * Test a radio group field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_radio_group() {\n\n\t\t$opts = array(\n\t\t\t'id'      => 'radio-id',\n\t\t\t'label'   => 'Radio Label',\n\t\t\t'type'    => 'radio',\n\t\t\t'options' => array(\n\t\t\t\t'opt1' => 'Option1',\n\t\t\t\t'opt2' => 'Option2',\n\t\t\t),\n\t\t);\n\n\t\t$html = llms_form_field( $opts, false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-radio is-group', $html );\n\t\t$this->assertStringContains( '<label for=\"radio-id\">Radio Label</label><div class=\"llms-field-radio llms-input-group\"', $html );\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-radio llms-cols-12 llms-cols-last\"><input class=\"llms-field-radio\" id=\"radio-id--opt1\" name=\"radio-id\" type=\"radio\" value=\"opt1\" /><label for=\"radio-id--opt1\">Option1</label></div>', $html );\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-radio llms-cols-12 llms-cols-last\"><input class=\"llms-field-radio\" id=\"radio-id--opt2\" name=\"radio-id\" type=\"radio\" value=\"opt2\" /><label for=\"radio-id--opt2\">Option2</label></div>', $html );\n\n\t\t// default value.\n\t\t$opts['default'] = 'opt1';\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( '<input checked=\"checked\" class=\"llms-field-radio\" id=\"radio-id--opt1\" name=\"radio-id\" type=\"radio\" value=\"opt1\" /><label for=\"radio-id--opt1\">Option1</label>', $html );\n\n\t\t// user has saved data.\n\t\t$this->get_user_with_meta( 'radio-id', 'opt2' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( '<input checked=\"checked\" class=\"llms-field-radio\" id=\"radio-id--opt2\" name=\"radio-id\" type=\"radio\" value=\"opt2\" /><label for=\"radio-id--opt2\">Option2</label>', $html );\n\t\t$this->assertStringNotContains( '<input checked=\"checked\" class=\"llms-field-radio\" id=\"radio-id--opt1\" name=\"radio-id\" type=\"radio\" value=\"opt1\" /><label for=\"radio-id--opt1\">Option1</label>', $html );\n\n\t}\n\n\t/**\n\t * Test a checkbox field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_checkbox() {\n\n\t\t$opts = array(\n\t\t\t'type'  => 'checkbox',\n\t\t\t'value' => 'mock_val',\n\t\t);\n\n\t\t$html = llms_form_field( $opts, false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-checkbox', $html );\n\t\t$this->assertStringContains( '<input class=\"llms-field-checkbox\"', $html );\n\t\t$this->assertStringContains( 'type=\"checkbox\"', $html );\n\t\t$this->assertStringContains( 'value=\"mock_val\"', $html );\n\t\t$this->assertStringNotContains( 'checked=\"checked\"', $html );\n\n\t\t// Checked.\n\t\t$opts['checked'] = true;\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( 'checked=\"checked\"', $html );\n\n\t}\n\n\t/**\n\t * Test checkbox with a user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_checkbox_with_user() {\n\n\t\t$opts = array(\n\t\t\t'id'    => 'checkbox_store',\n\t\t\t'type'  => 'checkbox',\n\t\t\t'value' => 'mock_val',\n\t\t);\n\n\t\t// User doesn't have value stored.\n\t\t$this->get_user_with_meta( 'checkbox_store' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringNotContains( 'checked=\"checked\"', $html );\n\n\t\t$this->get_user_with_meta( 'checkbox_store', 'mock_val' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( 'checked=\"checked\"', $html );\n\n\t}\n\n\t/**\n\t * Test checkbox group.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_checkbox_group() {\n\n\t\t$opts = array(\n\t\t\t'id'      => 'checkbox-id',\n\t\t\t'label'   => 'Checkbox Label',\n\t\t\t'type'    => 'checkbox',\n\t\t\t'options' => array(\n\t\t\t\t'opt1' => 'Option1',\n\t\t\t\t'opt2' => 'Option2',\n\t\t\t),\n\t\t);\n\n\t\t$html = llms_form_field( $opts, false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-checkbox is-group', $html );\n\t\t$this->assertStringContains( '<label for=\"checkbox-id\">Checkbox Label</label><div class=\"llms-field-checkbox llms-input-group\"', $html );\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-checkbox llms-cols-12 llms-cols-last\"><input class=\"llms-field-checkbox\" id=\"checkbox-id--opt1\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt1\" /><label for=\"checkbox-id--opt1\">Option1</label></div>', $html );\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-checkbox llms-cols-12 llms-cols-last\"><input class=\"llms-field-checkbox\" id=\"checkbox-id--opt2\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt2\" /><label for=\"checkbox-id--opt2\">Option2</label></div>', $html );\n\n\t\t// Default value.\n\t\t$opts['default'] = 'opt1';\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains(\n\t\t\t'<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox-id--opt1\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt1\" /><label for=\"checkbox-id--opt1\">Option1</label>',\n\t\t\t$html\n\t\t);\n\t\t$this->assertStringNotContains(\n\t\t\t'<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox-id--opt2\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt2\" /><label for=\"checkbox-id--opt2\">Option2</label>',\n\t\t\t$html\n\t\t);\n\n\t\t// Test multiple defaults.\n\t\t$opts['default'] = array( 'opt1', 'opt2' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains(\n\t\t\t'<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox-id--opt1\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt1\" /><label for=\"checkbox-id--opt1\">Option1</label>',\n\t\t\t$html\n\t\t);\n\t\t$this->assertStringContains(\n\t\t\t'<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox-id--opt2\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt2\" /><label for=\"checkbox-id--opt2\">Option2</label>',\n\t\t\t$html\n\t\t);\n\n\t\t// User has saved data.\n\t\t$this->get_user_with_meta( 'checkbox-id', 'opt2' );\n\t\t$html = llms_form_field( $opts, false );\n\t\t$this->assertStringContains( '<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox-id--opt2\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt2\" /><label for=\"checkbox-id--opt2\">Option2</label>', $html );\n\t\t$this->assertStringNotContains( '<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox-id--opt1\" name=\"checkbox-id[]\" type=\"checkbox\" value=\"opt1\" /><label for=\"checkbox-id--opt1\">Option1</label>', $html );\n\n\t}\n\n\t/**\n\t * Test button field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_button() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type'  => 'button',\n\t\t\t'value' => 'Button Text',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-button', $html );\n\t\t$this->assertStringContains( '<button class=\"llms-field-button\"', $html );\n\t\t$this->assertStringContains( 'type=\"button\"', $html );\n\t\t$this->assertStringContains( '>Button Text</button>', $html );\n\n\t}\n\n\t/**\n\t * Test submit button field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_submit() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type'  => 'submit',\n\t\t\t'value' => 'Button Text',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-submit', $html );\n\t\t$this->assertStringContains( '<button class=\"llms-field-button\"', $html );\n\t\t$this->assertStringContains( 'type=\"submit\"', $html );\n\t\t$this->assertStringContains( '>Button Text</button>', $html );\n\n\t}\n\n\t/**\n\t * Test reset button field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_reset() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type'  => 'reset',\n\t\t\t'value' => 'Button Text',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-reset', $html );\n\t\t$this->assertStringContains( '<button class=\"llms-field-button\"', $html );\n\t\t$this->assertStringContains( 'type=\"reset\"', $html );\n\t\t$this->assertStringContains( '>Button Text</button>', $html );\n\t}\n\n\t/**\n\t * Test output of a text input field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_text() {\n\n\t\t$html = llms_form_field( array(), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-text', $html );\n\t\t$this->assertStringContains( '<input ', $html );\n\t\t$this->assertStringContains( 'type=\"text\"', $html );\n\n\t}\n\n\t/**\n\t * Test email field type.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_email() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type' => 'email',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-email', $html );\n\t\t$this->assertStringContains( '<input ', $html );\n\t\t$this->assertStringContains( 'type=\"email\"', $html );\n\n\t}\n\n\t/**\n\t * Test tel field type.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_tel() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type' => 'tel',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-tel', $html );\n\t\t$this->assertStringContains( '<input ', $html );\n\t\t$this->assertStringContains( 'type=\"tel\"', $html );\n\n\t}\n\n\t/**\n\t * Test number field type.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_number() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type' => 'number',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-number', $html );\n\t\t$this->assertStringContains( '<input ', $html );\n\t\t$this->assertStringContains( 'type=\"number\"', $html );\n\n\t}\n\n\t/**\n\t * Test textarea field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_textarea() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type' => 'textarea',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-textarea', $html );\n\t\t$this->assertStringContains( '<textarea class=\"llms-field-textarea\"', $html );\n\t\t$this->assertStringContains( '></textarea>', $html );\n\n\t}\n\n\t/**\n\t * Test textarea field with user data.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_textarea_with_user_data() {\n\n\t\t$this->get_user_with_meta( 'textarea-id', 'Lorem ipsum dolor sit.' );\n\n\t\t$html = llms_form_field( array(\n\t\t\t'id'   => 'textarea-id',\n\t\t\t'type' => 'textarea',\n\t\t), false );\n\n\t\t$this->assertStringContains( '>Lorem ipsum dolor sit.</textarea>', $html );\n\n\t}\n\n\t/**\n\t * Test custom html field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_type_html() {\n\n\t\t$html = llms_form_field( array(\n\t\t\t'type' => 'html',\n\t\t\t'value' => '<h2>HTML Content.</h2>',\n\t\t), false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-form-field type-html', $html );\n\t\t$this->assertStringContains( '<div class=\"llms-field-html\"', $html );\n\t\t$this->assertStringContains( '><h2>HTML Content.</h2></div>', $html );\n\n\t}\n\n\t/**\n\t * Test attributes setting.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_attributes() {\n\n\t\t$this->assertStringContains( 'data-custom=\"whatever', llms_form_field( array( 'attributes' => array( 'data-custom' => 'whatever' ) ), false ) );\n\n\t\t$multi = llms_form_field( array( 'attributes' => array( 'data-custom' => 'whatever', 'maxlength' => 5 ) ), false );\n\t\t$this->assertStringContains( 'maxlength=\"5\"', $multi );\n\t\t$this->assertStringContains( 'data-custom=\"whatever', $multi );\n\n\t}\n\n\t/**\n\t * Test columns setting.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_columns() {\n\n\t\t// Default.\n\t\t$this->assertStringContains( 'llms-cols-12 llms-cols-last', llms_form_field( array(), false ) );\n\t\t$this->assertStringContains( '<div class=\"clear\"></div>', llms_form_field( array(), false ) );\n\n\t\t// Set cols.\n\t\t$this->assertStringContains( 'llms-cols-5 llms-cols-last', llms_form_field( array( 'columns' => 5 ), false ) );\n\t\t$this->assertStringContains( 'llms-cols-8 llms-cols-last', llms_form_field( array( 'columns' => 8 ), false ) );\n\n\t\t// Not last.\n\t\t$this->assertStringNotContains( 'llms-cols-last', llms_form_field( array( 'last_column' => false ), false ) );\n\t\t$this->assertStringNotContains( '<div class=\"clear\"></div>', llms_form_field( array( 'last_column' => false ), false ) );\n\n\t}\n\n\t/**\n\t * Test id setting.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_id() {\n\n\t\t$this->assertStringContains( 'id=\"', llms_form_field( array(), false ) );\n\t\t$this->assertStringContains( 'id=\"mock\"', llms_form_field( array( 'id' => 'mock' ), false ) );\n\n\t}\n\n\t/**\n\t * Test wrapper classes setting.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_wrapper_classes() {\n\n\t\t// Strings.\n\t\t$this->assertStringContains( 'mock-wrapper-class\">', llms_form_field( array( 'wrapper_classes' => 'mock-wrapper-class' ), false ) );\n\t\t$this->assertStringContains( 'mock-wrapper-class alt-class\">', llms_form_field( array( 'wrapper_classes' => 'mock-wrapper-class alt-class' ), false ) );\n\n\t\t// Arrays.\n\t\t$this->assertStringContains( 'mock-wrapper-class\">', llms_form_field( array( 'wrapper_classes' => array( 'mock-wrapper-class' ) ), false ) );\n\t\t$this->assertStringContains( 'mock-wrapper-class alt-class\">', llms_form_field( array( 'wrapper_classes' => array( 'mock-wrapper-class', 'alt-class' ) ), false ) );\n\n\t}\n\n\t/**\n\t * Test field `value` attribute.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_value() {\n\n\t\t// No specified value.\n\t\t$this->assertStringNotContains( 'value=\"', llms_form_field( array(), false ) );\n\n\t\t// Value is specified.\n\t\t$this->assertStringContains( 'value=\"mock\"', llms_form_field( array( 'value' => 'mock' ), false ) );\n\n\t\t// Default value specified.\n\t\t$this->assertStringContains( 'value=\"mock\"', llms_form_field( array( 'default' => 'mock' ), false ) );\n\n\t\t// Default value not added if a value is specified.\n\t\t$this->assertStringContains( 'value=\"mock\"', llms_form_field( array( 'value' => 'mock', 'default' => 'fake' ), false ) );\n\t\t$this->assertStringNotContains( 'value=\"fake\"', llms_form_field( array( 'value' => 'mock', 'default' => 'fake' ), false ) );\n\n\t}\n\n\t/**\n\t * Test field `name` attribute.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_name() {\n\n\t\t// No name specified, fallback to the field id.\n\t\t$this->assertStringContains( 'name=\"mock\"', llms_form_field( array( 'id' => 'mock' ), false ) );\n\n\t\t// Name specified.\n\t\t$this->assertStringContains( 'name=\"mock\"', llms_form_field( array( 'name' => 'mock', 'id' => 'fake' ), false ) );\n\n\t\t// Name explicitly disabled.\n\t\t$this->assertStringNotContains( 'name=\"', llms_form_field( array( 'name' => false ), false ) );\n\n\t}\n\n\t/**\n\t * Test field `placeholder` attribute.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_placeholder() {\n\n\t\t$this->assertStringContains( 'placeholder=\"test\"', llms_form_field( array( 'placeholder' => 'test' ), false ) );\n\n\t}\n\n\t/**\n\t * Test field `style` attribute.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_deprecated_attributes() {\n\n\t\t// No style.\n\t\t$this->assertStringNotContains( 'style=\"', llms_form_field( array(), false ) );\n\n\t\t// Has style.\n\t\t$this->assertStringContains( 'style=\"test\"', llms_form_field( array( 'style' => 'test' ), false ) );\n\n\t\t$this->assertStringContains( 'maxlength=\"1\"', llms_form_field( array( 'max_length' => '1' ), false ) );\n\t\t$this->assertStringContains( 'minlength=\"25\"', llms_form_field( array( 'min_length' => '25' ), false ) );\n\n\t}\n\n\t/**\n\t * Test field description.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_description() {\n\n\t\t// No description.\n\t\t$this->assertStringNotContains( '<span class=\"llms-description\">', llms_form_field( array(), false ) );\n\n\t\t// Has Description.\n\t\t$this->assertStringContains( '<span class=\"llms-description\">Test Description</span>', llms_form_field( array( 'description' => 'Test Description' ), false ) );\n\n\t}\n\n\t/**\n\t * Test field `required` attribute.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_required() {\n\n\t\t// Not required.\n\t\t$this->assertStringNotContains( '<span class=\"llms-required\">*</span>', llms_form_field( array( 'label' => 'mock' ), false ) );\n\t\t$this->assertStringNotContains( 'required=\"required\"', llms_form_field( array(), false ) );\n\n\t\t// Is required.\n\t\t$this->assertStringContains( '<span class=\"llms-required\">*</span>', llms_form_field( array( 'required' => true, 'label' => 'mock' ), false ) );\n\t\t$this->assertStringContains( 'required=\"required\"', llms_form_field( array( 'required' => true ), false ) );\n\n\t\t// Required but no label.\n\t\t$this->assertStringNotContains( '<span class=\"llms-required\">*</span>', llms_form_field( array( 'required' => true ), false ) );\n\n\t}\n\n\t/**\n\t * Test field `label` attribute.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_label() {\n\n\t\t$this->assertStringContains( '<label for=\"fake\">mock</label>', llms_form_field( array( 'id' => 'fake', 'label' => 'mock' ), false ) );\n\t\t$this->assertStringContains( '<label for=\"fake\">mock<span class=\"llms-required\">*</span></label>', llms_form_field( array( 'id' => 'fake', 'label' => 'mock', 'required' => true ), false ) );\n\n\t}\n\n\t/**\n\t * No label element output when label is empty.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_label_empty() {\n\n\t\t$this->assertStringNotContains( '<label', llms_form_field( array( 'id' => 'fake' ), false ) );\n\n\t}\n\n\t/**\n\t * Output an empty label element if `label_show_empty` is true and `label` is empty.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_label_show_empty() {\n\n\t\t$this->assertStringContains( '<label for=\"fake\"></label>', llms_form_field( array( 'id' => 'fake', 'label_show_empty' => true ), false ) );\n\n\t}\n\n\t/**\n\t * Test field `disabled` attribute.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_disabled() {\n\n\t\t// No disabled.\n\t\t$this->assertStringNotContains( 'disabled=\"disabled\"', llms_form_field( array(), false ) );\n\n\t\t// Has disabled.\n\t\t$this->assertStringContains( 'disabled=\"disabled\"', llms_form_field( array( 'disabled' => true ), false ) );\n\n\t}\n\n\tpublic function test_prepare_value_for_button_and_html() {\n\n\t\t$types =  array(\n\t\t\t// Always have an explicit value.\n\t\t\t'button', 'reset', 'submit', 'html',\n\t\t\t// May or may not have an explicit value.\n\t\t\t'text',\n\t\t);\n\n\t\tforeach ( $types as $type ) {\n\n\t\t\t$args = array(\n\t\t\t\t'type'  => $type,\n\t\t\t\t'name'  => 'test_field',\n\t\t\t\t'value' => 'A Value',\n\t\t\t);\n\n\t\t\t$field = new LLMS_Form_Field( $args );\n\n\t\t\t$settings = $field->get_settings();\n\n\t\t\t$this->assertEquals( $args['value'], $settings['value'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test prepare_password_strength_meter() with default values.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_password_strength_meter_default_values() {\n\n\t\t$field = new LLMS_Form_Field();\n\n\t\t$handler = function( $args ) {\n\t\t\t$this->assertEquals( array(), $args['blocklist'] );\n\t\t\t$this->assertEquals( 6, $args['min_length'] );\n\t\t\t$this->assertEquals( 'weak', $args['min_strength'] );\n\t\t\treturn $args;\n\t\t};\n\t\tadd_filter( 'llms_password_strength_meter_settings', $handler );\n\n\t\tLLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() );\n\n\t\tremove_filter( 'llms_password_strength_meter_settings', $handler );\n\t}\n\n\t/**\n\t * Test prepare_password_strength_meter with custom values\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_password_strength_meter_custom_values() {\n\n\t\t$field = new LLMS_Form_Field( array(\n\t\t\t'min_strength' => 'weak',\n\t\t\t'min_length'   => 10,\n\t\t) );\n\n\t\t$handler = function( $args ) {\n\t\t\t$this->assertEquals( 10, $args['min_length'] );\n\t\t\t$this->assertEquals( 'weak', $args['min_strength'] );\n\t\t\treturn $args;\n\t\t};\n\t\tadd_filter( 'llms_password_strength_meter_settings', $handler );\n\n\t\tLLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() );\n\t\t$this->assertFalse( isset( $field->get_settings()['min_length'] ) );\n\n\t\tremove_filter( 'llms_password_strength_meter_settings', $handler );\n\t}\n\n\t/**\n\t * Test prepare_password_strength_meter() to ensure the minimum accepted value is 6\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_password_strength_meter_min_length() {\n\n\t\t$field = new LLMS_Form_Field( array(\n\t\t\t'min_length' => 2,\n\t\t) );\n\n\t\t$handler = function( $args ) {\n\t\t\t$this->assertEquals( 6, $args['min_length'] );\n\t\t\treturn $args;\n\t\t};\n\t\tadd_filter( 'llms_password_strength_meter_settings', $handler );\n\n\t\tLLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() );\n\t\t$this->assertFalse( isset( $field->get_settings()['min_length'] ) );\n\n\t\tremove_filter( 'llms_password_strength_meter_settings', $handler );\n\t}\n\n\t/**\n\t * Test prepare_password_strength_meter() for script enqueue: not enqueued case.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_password_strength_meter_assets_not_enqueued() {\n\n\t\t$field = new LLMS_Form_Field();\n\n\t\t// Not enqueued.\n\t\tLLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() );\n\t\t$this->assertAssetNotEnqueued( 'script', 'password-strength-meter' );\n\t\t$this->assertFalse( llms()->assets->is_inline_enqueued( 'llms-pw-strength-settings' ) );\n\n\t}\n\n\t/**\n\t * Test prepare_password_strength_meter() for script enqueue: enqueued deferred on `wp_enqueue_scripts` hook firing.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_password_strength_meter_assets_enqueued_deferred() {\n\n\t\t$field = new LLMS_Form_Field();\n\n\t\t// Enqueued.\n\t\tLLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() );\n\n\t\tdo_action( 'wp_enqueue_scripts' );\n\n\t\t$this->assertAssetIsEnqueued( 'script', 'password-strength-meter' );\n\t\t$this->assertTrue( llms()->assets->is_inline_enqueued( 'llms-pw-strength-settings' ) );\n\n\t\t// Pretend wp_enqueue_scripts was never called, for further tests.\n\t\tglobal $wp_actions;\n\t\tunset( $wp_actions[ 'wp_enqueue_scripts' ] );\n\n\t}\n\n\t/**\n\t * Test prepare_password_strength_meter() for script enqueue: enqueued right away b/c `wp_enqueue_scripts` already fired.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_password_strength_meter_assets_enqueued_right_away() {\n\n\t\t$field = new LLMS_Form_Field();\n\n\t\tdo_action( 'wp_enqueue_scripts' );\n\n\t\t// Enqueued.\n\t\tLLMS_Unit_Test_Util::call_method( $field, 'prepare_password_strength_meter', array() );\n\n\t\t$this->assertAssetIsEnqueued( 'script', 'password-strength-meter' );\n\t\t$this->assertTrue( llms()->assets->is_inline_enqueued( 'llms-pw-strength-settings' ) );\n\n\t}\n\n\t/**\n\t * Test prepare_value() for a password field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_value_for_password() {\n\n\t\t$field = new LLMS_Form_Field( array(\n\t\t\t'type'  => 'password',\n\t\t\t'name'  => 'test_field',\n\t\t) );\n\n\t\t$settings = $field->get_settings();\n\n\t\t$this->assertEmpty( $settings['value'] );\n\n\t}\n\n\t/**\n\t * Test prepare_value() with user-posted data\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_value_with_posted_data() {\n\n\t\t$this->mockPostRequest( array(\n\t\t\t'test_field' => 'submitted value',\n\t\t) );\n\n\t\t$field = new LLMS_Form_Field( array(\n\t\t\t'name'  => 'test_field',\n\t\t) );\n\n\t\t$settings = $field->get_settings();\n\n\t\t$this->assertEquals( 'submitted value', $settings['value'] );\n\n\t}\n\n\t/**\n\t * Test field html generated on submision when value is an array\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_array_value_only_post_request() {\n\n\t\t$checkbox_options = array(\n\t\t\t'yes_key' => 'Yes',\n\t\t\t'no_key'  => 'No',\n\t\t);\n\n\t\t$form_field_conf = array(\n\t\t\t'name'    => 'array_type',\n\t\t\t'type'    => 'checkbox',\n\t\t\t'options' => $checkbox_options,\n\t\t);\n\n\t\t// Simulate a field submission where both the checkboxes are checked.\n\t\t$this->mockPostRequest(\n\t\t\tarray(\n\t\t\t\t'array_type' => array_keys( $checkbox_options ),\n\t\t\t)\n\t\t);\n\n\t\t// Create a form field.\n\t\t$form_field = llms_form_field(\n\t\t\t$form_field_conf,\n\t\t\tfalse\n\t\t);\n\n\t\t// Expect the html has 2 \"checked\" checkboxes.\n\t\t$this->assertEquals(\n\t\t\t2,\n\t\t\tsubstr_count(\n\t\t\t\t$form_field,\n\t\t\t\t'\"checked\"'\n\t\t\t)\n\t\t);\n\n\t\t// Simulate a field submission where only one checkbox is checked.\n\t\t$this->mockPostRequest(\n\t\t\tarray(\n\t\t\t\t'array_type' => array_keys( $checkbox_options )[1],\n\t\t\t)\n\t\t);\n\n\t\t// Create a form field.\n\t\t$form_field = llms_form_field(\n\t\t\t$form_field_conf,\n\t\t\tfalse\n\t\t);\n\n\t\t// Expect the html has 1 \"checked\" checkbox.\n\t\t$this->assertEquals(\n\t\t\t1,\n\t\t\tsubstr_count(\n\t\t\t\t$form_field,\n\t\t\t\t'\"checked\"'\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test llms_form_field when passing an user\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_with_user_as_data_source() {\n\n\t\t$opts = array(\n\t\t\t'id'         => 'checkbox_store',\n\t\t\t'type'       => 'checkbox',\n\t\t\t'options'    => array(\n\t\t\t\t'mock_val'  => 'Mock val',\n\t\t\t\t'mock_val2' => 'Mock val 2',\n\t\t\t),\n\t\t\t'data_store' => 'usermeta',\n\t\t);\n\n\t\t// User doesn't have value stored.\n\t\t$this->get_user_with_meta( 'checkbox_store' );\n\t\t$user_id = get_current_user_id();\n\t\t// Log-out.\n\t\twp_set_current_user( null );\n\n\t\t$html = llms_form_field( $opts, false, $user_id );\n\t\t$this->assertStringNotContains( 'checked=\"checked\"', $html );\n\n\t\t// User has value stored.\n\t\t$this->get_user_with_meta( 'checkbox_store', array( 'mock_val2' ) );\n\t\t$user_id = get_current_user_id();\n\t\t// Log-out.\n\t\twp_set_current_user( null );\n\n\t\t$html = llms_form_field( $opts, false, $user_id );\n\t\t$this->assertStringContains(\n\t\t\t'<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox_store--mock_val2\" name=\"checkbox_store[]\" type=\"checkbox\" value=\"mock_val2\" />',\n\t\t\t$html\n\t\t);\n\n\t\t// Log in the last user.\n\t\twp_set_current_user( $user_id );\n\t\t$html = llms_form_field( $opts, false, $user_id );\n\t\t$this->assertStringContains(\n\t\t\t'<input checked=\"checked\" class=\"llms-field-checkbox\" id=\"checkbox_store--mock_val2\" name=\"checkbox_store[]\" type=\"checkbox\" value=\"mock_val2\" />',\n\t\t\t$html\n\t\t);\n\n\t\t// Log in the last user.\n\t\twp_set_current_user( $user_id );\n\t\t// Pass a non existing user.\n\t\t$html = llms_form_field( $opts, false, $user_id + 1 );\n\t\t$this->assertStringNotContains( 'checked=\"checked\"', $html );\n\n\t}\n\n\t/**\n\t * Test LLMS_Form_Field data_source and data_source_type props setting\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_field_data_source_and_type() {\n\t\t$opts = array(\n\t\t\t'type'       => 'button',\n\t\t\t'value'      => 'Button Text',\n\t\t\t'data_store' => 'usermeta',\n\t\t);\n\n\t\t// Pass a WP Post in place of a user.\n\t\t$post  = $this->factory->post->create_and_get();\n\t\t$field = new LLMS_Form_Field( $opts, $post );\n\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) );\n\n\t\t// Pass a WP User.\n\t\t$user  = $this->factory->user->create_and_get();\n\t\t$field = new LLMS_Form_Field( $opts, $user );\n\n\t\t$this->assertEquals( $user, LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) );\n\t\t$this->assertEquals( 'wp_user', LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) );\n\n\t\t// Pass a WP User ID.\n\t\t$opts['data_store'] = 'users'; // Test it works with the users table as store too.\n\t\t$field = new LLMS_Form_Field( $opts, $user->ID );\n\n\t\t$this->assertEquals( $user, LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) );\n\t\t$this->assertEquals( 'wp_user', LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) );\n\n\t\t// Pass a non existing WP User ID.\n\t\t$field = new LLMS_Form_Field( $opts, $user->ID + 1 );\n\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) );\n\n\t\t// Pass an existing WP User ID but change the data_store to something different from 'usermeta' or 'users'.\n\t\t$opts['data_store'] = 'whatever';\n\t\t$field = new LLMS_Form_Field( $opts, $user->ID );\n\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source' ) );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::get_private_property_value( $field, 'data_source_type' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-form-handler.php",
    "content": "<?php\n/**\n * Test Form Handler class\n *\n * @package LifterLMS/Tests\n *\n * @group forms\n * @group form_handler\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Form_Handler extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->handler = LLMS_Form_Handler::instance();\n\n\t\t// Actions aren't firing on unit tests without explicitly calling the constructor to add them. Not sure why.\n\t\tLLMS_Unit_Test_Util::call_method( $this->handler, '__construct' );\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t}\n\n\t/**\n\t * Teardown the test.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\n\t\tglobal $wpdb;\n\t\t$wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) );\n\n\t}\n\n\tpublic function make_address_required( $settings ) {\n\n\t\tif ( 0 === strpos( $settings['name'], 'llms_billing_' ) && 'llms_billing_address_2' !== $settings['name'] ) {\n\t\t\t$settings['required'] = true;\n\t\t}\n\n\t\treturn $settings;\n\t}\n\n\tprotected function get_data_for_form_submit( $args = array() ) {\n\n\t\t$email = uniqid( 'fake-' ) . '@mock.tld';\n\n\t\treturn wp_parse_args( $args, array(\n\t\t\t'email_address'          => $email,\n\t\t\t'email_address_confirm'  => $email,\n\t\t\t'password'               => '12345678',\n\t\t\t'password_confirm'       => '12345678',\n\t\t\t'first_name'             => 'Jeffrey',\n\t\t\t'last_name'              => 'Lebowski',\n\t\t\t'llms_billing_address_1' => '123 Any Street',\n\t\t\t'llms_billing_city'      => 'Reseda',\n\t\t\t'llms_billing_state'     => 'CA',\n\t\t\t'llms_billing_zip'       => '91234',\n\t\t\t'llms_billing_country'   => 'US',\n\t\t) );\n\n\t}\n\n\t/**\n\t * Test submit() for the account form when there's no logged in user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_account_no_user() {\n\n\t\t$ret = $this->handler->submit( array(), 'account' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-no-user', $ret );\n\n\t}\n\n\t/**\n\t * Test submit() for the account for when there is a logged in user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_account_with_user() {\n\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\t$ret = $this->handler->submit( array(), 'account' );\n\n\t\t// We're still going to get an error but it won't be the \"llms-form-no-user\" error.\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret );\n\n\t}\n\n\t/**\n\t * Test submit on an invalid form location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_invalid() {\n\n\t\t$ret = $this->handler->submit( array(), 'fake' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-invalid-location', $ret );\n\n\t}\n\n\t/**\n\t * Test submit with missing required fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_missing_required() {\n\n\t\t$ret = $this->handler->submit( array(), 'checkout' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret );\n\n\t}\n\n\t/**\n\t * Test custom fields added the legacy way are correctly parsed\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_custom_field_legacy() {\n\n\t\t$custom_fields = array(\n\t\t\tarray(\n\t\t\t\t'columns'      => 12,\n\t\t\t\t'id'           => 'llms_company_name',\n\t\t\t\t'label'        => 'Company name',\n\t\t\t\t'last_column'  => false,\n\t\t\t\t'required'     => true,\n\t\t\t\t'type'         => 'text',\n\t\t\t),\n\t\t);\n\n\t\tadd_filter(\n\t\t\t'lifterlms_get_person_fields',\n\t\t\tfunction( $fields, $screen ) use ( $custom_fields ) {\n\t\t\t\tarray_push( $fields, ...$custom_fields );\n\t\t\t\treturn $fields;\n\t\t\t},\n\t\t\t10,\n\t\t\t2\n\t\t);\n\n\t\t$args = $this->get_data_for_form_submit();\n\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret );\n\n\t\t$args[ 'llms_company_name' ] = 'something';\n\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\n\t\t$this->assertTrue( is_int( $ret ) );\n\t\t$this->assertEquals( 'something', get_user_meta( $ret, 'llms_company_name', true ) );\n\n\t\tremove_all_filters( 'lifterlms_get_person_fields' );\n\n\t}\n\n\t/**\n\t * Test submission matching errors.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_matching_errors() {\n\n\t\t$args = array(\n\t\t\t'email_address' => 'fake@mock.com',\n\t\t\t'email_address_confirm' => 'mismatch@mock.com',\n\t\t\t'password' => '12345678',\n\t\t\t'password_confirm' => 'mistmatch',\n\t\t\t'first_name' => 'Jeffrey',\n\t\t\t'last_name' => 'Lebowski',\n\t\t\t'llms_billing_address_1' => '123 Any Street',\n\t\t\t'llms_billing_city' => 'Reseda',\n\t\t\t'llms_billing_state' => 'CA',\n\t\t\t'llms_billing_zip' => '91234',\n\t\t\t'llms_billing_country' => 'US',\n\t\t);\n\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-not-matched', $ret );\n\n\t}\n\n\t/**\n\t * Test registration form submissions with an invalid voucher code.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_registration_voucher_err_not_found() {\n\n\t\t$ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => 'invalid-code' ) ), 'registration' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret );\n\t\t$this->assertWPErrorMessageEquals( 'Voucher code \"invalid-code\" could not be found.', $ret );\n\n\t}\n\n\t/**\n\t * Test registration form submissions with a deleted voucher code.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_registration_voucher_err_deleted() {\n\n\t\t$voucher = $this->create_voucher( 1, 1 );\n\t\t$code    = $voucher->get_voucher_codes()[0];\n\t\t$voucher->delete_voucher_code( $code->id );\n\n\t\t$ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => $code->code ) ), 'registration' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret );\n\t\t$this->assertWPErrorMessageEquals( sprintf( 'Voucher code \"%s\" could not be found.', $code->code ), $ret );\n\n\t}\n\n\t/**\n\t * Test registration form submissions when a voucher code's parent post is deleted (or not published).\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_registration_voucher_err_post_deleted() {\n\n\t\t$voucher = $this->create_voucher( 1, 1 );\n\t\t$code    = $voucher->get_voucher_codes()[0];\n\t\twp_delete_post( $code->voucher_id, true );\n\n\t\t$ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => $code->code ) ) , 'registration' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret );\n\t\t$this->assertWPErrorMessageEquals( sprintf( 'Voucher code \"%s\" could not be found.', $code->code ), $ret );\n\n\t}\n\n\t/**\n\t * Test registration form submissions when a voucher code has been redeemed the maximum number of times allowed\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_registration_voucher_err_max() {\n\n\t\t$voucher = $this->create_voucher( 1, 1 );\n\t\t$code    = $voucher->get_voucher_codes()[0];\n\t\t$voucher->use_voucher( $code->code, $this->factory->user->create() );\n\n\t\t$ret = $this->handler->submit( $this->get_data_for_form_submit( array( 'llms_voucher' => $code->code ) ), 'registration' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret );\n\t\t$this->assertWPErrorMessageEquals( sprintf( 'Voucher code \"%s\" has already been redeemed the maximum number of times.', $code->code ), $ret );\n\n\t}\n\n\t/**\n\t * Test submit() with the validate_only flag and validation errors.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_validate_only_error() {\n\n\t\t$ret = $this->handler->submit( array(), 'checkout', array( 'validate_only' => true ) );\n\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret );\n\n\t}\n\n\t/**\n\t * Test submit() with the validate_only flag and no validation errors.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_validate_only_success() {\n\n\t\t$this->assertTrue(\n\t\t\t$this->handler->submit(\n\t\t\t\t$this->get_data_for_form_submit(),\n\t\t\t\t'checkout',\n\t\t\t\tarray( 'validate_only' => true )\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test successful submission for a new users.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Provide `password_current` when updating the `password`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_success() {\n\n\t\t$args = $this->get_data_for_form_submit();\n\n\t\t// Register.\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\n\t\t$this->assertTrue( is_int( $ret ) );\n\t\t$user = new WP_User( $ret );\n\n\t\t$this->assertEquals( $args['email_address'], $user->user_email );\n\t\t$this->assertEquals( $args['first_name'], $user->first_name );\n\t\t$this->assertEquals( $args['last_name'], $user->last_name );\n\n\t\t$this->assertEquals( $args['llms_billing_address_1'], $user->llms_billing_address_1 );\n\t\t$this->assertEquals( $args['llms_billing_city'], $user->llms_billing_city );\n\t\t$this->assertEquals( $args['llms_billing_state'], $user->llms_billing_state );\n\t\t$this->assertEquals( $args['llms_billing_zip'], $user->llms_billing_zip );\n\t\t$this->assertEquals( $args['llms_billing_country'], $user->llms_billing_country );\n\n\t\t$this->assertTrue( wp_check_password( $args['password'], $user->user_pass, $user->ID ) );\n\n\t\t// Update.\n\t\twp_set_current_user( $ret );\n\t\t$args['first_name'] = 'Maude';\n\t\t$args['display_name'] = $user->display_name;\n\t\t// Current password is required when updating the password.\n\t\t$args['password_current'] = $args['password'];\n\t\t$this->assertSame( $ret, $this->handler->submit( $args, 'account' ) );\n\t\t$this->assertEquals( $args['first_name'], $user->first_name );\n\n\t}\n\n\n\t/**\n\t * Test submitting account update without password update\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_password_update_wrong_current_password() {\n\n\t\t$args = $this->get_data_for_form_submit(\n\t\t\tarray(\n\t\t\t\t'display_name' => 'Disp', // Required on update.\n\t\t\t)\n\t\t);\n\n\t\t// Register.\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\n\t\t$this->assertTrue( is_int( $ret ) );\n\t\t$user = new WP_User( $ret );\n\n\t\t// Update.\n\t\twp_set_current_user( $ret );\n\t\t$args['first_name'] = 'Maude';\n\t\tunset($args['password']);\n\t\tunset($args['password_confirm']);\n\n\t\t$this->assertSame( $ret, $this->handler->submit( $args, 'account' ) );\n\t\t$this->assertEquals( $args['first_name'], $user->first_name );\n\n\t}\n\n\t/**\n\t * Test submit password change without providing, or with wrong current password\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_account_update_no_password() {\n\n\t\t$args = $this->get_data_for_form_submit(\n\t\t\tarray(\n\t\t\t\t'display_name' => 'Disp', // Required on update.\n\t\t\t)\n\t\t);\n\n\t\t// Register.\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\n\t\t$this->assertTrue( is_int( $ret ) );\n\t\t$user = new WP_User( $ret );\n\n\t\t// Update.\n\t\twp_set_current_user( $ret );\n\n\t\t// No current password provided.\n\t\t$ret = $this->handler->submit( $args, 'account' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret );\n\t\t$this->assertWPErrorMessageEquals( 'Current Password is a required field.', $ret );\n\n\t\t// Provide a wrong current password.\n\t\t$args['password_current'] = $args['password'] . \"-wrong\";\n\t\t$ret = $this->handler->submit( $args, 'account' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret );\n\t\t$this->assertWPErrorMessageEquals( 'The submitted password was not correct.', $ret );\n\n\t}\n\n\t/**\n\t * Test submit() with a country that doesn't require states.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_address_no_zip() {\n\n\t\tadd_filter( 'llms_field_settings', array( $this, 'make_address_required' ) );\n\n\t\t$args = $this->get_data_for_form_submit( array(\n\t\t\t'llms_billing_state' => 'C',\n\t\t\t'llms_billing_country' => 'UG', // Uganda.\n\t\t) );\n\t\tunset( $args['llms_billing_zip'] );\n\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\t\t$this->assertTrue( is_numeric( $ret ) );\n\n\t\tremove_filter( 'llms_field_settings', array( $this, 'make_address_required' ), 10 );\n\n\t}\n\n\t/**\n\t * Test submit() with a country that doesn't require zip codes.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_address_no_states() {\n\n\t\tadd_filter( 'llms_field_settings', array( $this, 'make_address_required' ) );\n\n\t\t$args = $this->get_data_for_form_submit( array(\n\t\t\t'llms_billing_zip' => '23424',\n\t\t\t'llms_billing_country' => 'AS', // America Samoa.\n\t\t) );\n\t\tunset( $args['llms_billing_state'] );\n\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\t\t$this->assertTrue( is_numeric( $ret ) );\n\n\t\tremove_filter( 'llms_field_settings', array( $this, 'make_address_required' ), 10 );\n\n\t}\n\n\t/**\n\t * Test submit() with a country that doesn't require states or zip codes.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_address_no_city_or_zip() {\n\n\t\tadd_filter( 'llms_field_settings', array( $this, 'make_address_required' ) );\n\n\t\t$args = $this->get_data_for_form_submit( array(\n\t\t\t'llms_billing_country' => 'NR',\n\t\t\t'llms_billing_state' => '08',\n\t\t) );\n\t\tunset( $args['llms_billing_city'] );\n\t\tunset( $args['llms_billing_zip'] );\n\n\t\t$ret = $this->handler->submit( $args, 'checkout' );\n\t\t$this->assertTrue( is_numeric( $ret ) );\n\n\t\tremove_filter( 'llms_field_settings', array( $this, 'make_address_required' ), 10 );\n\n\t}\n\n\t/**\n\t * Test successful submission for a new users.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_success_with_voucher() {\n\n\t\t$voucher  = $this->get_mock_voucher( 1 );\n\t\t$products = $voucher->get_products();\n\t\t$code     = $voucher->get_voucher_codes()[0]->code;\n\n\t\t$args = $this->get_data_for_form_submit( array( 'llms_voucher' => $code ) );\n\n\t\t$ret = $this->handler->submit( $args, 'registration' );\n\n\t\t$this->assertTrue( is_int( $ret ) );\n\t\t$user = new WP_User( $ret );\n\n\t\t// Ensure voucher was redeemed successfully.\n\t\tforeach ( $products as $product_id ) {\n\t\t\tllms_is_user_enrolled( $user->ID, $product_id, 'all', false );\n\t\t}\n\n\t}\n\n\t/**\n\t * Tests that submitting a invalid email address produces an error.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_submit_registration_with_invalid_email_error() {\n\n\t\t// Disable user generation.\n\t\tadd_filter( 'pre_option_lifterlms_registration_generate_username', '__return_empty_string' );\n\t\tLLMS_Forms::instance()->create( 'registration', true );\n\n\t\t$args = $this->get_data_for_form_submit(\n\t\t\tarray(\n\t\t\t\t'user_login'             => 'the_dude',\n\t\t\t\t'email_address'          => 'fake@wrong',\n\t\t\t\t'email_address_confirm'  => 'fake@wrong',\n\t\t\t)\n\t\t);\n\n\n\t\t$ret = $this->handler->submit( $args, 'registration' );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $ret );\n\t\t$this->assertWPErrorMessageEquals( 'The email address \"fake@wrong\" is not valid.', $ret );\n\n\t\t// Re-enable user generation.\n\t\tremove_filter( 'pre_option_lifterlms_registration_generate_username', '__return_empty_string' );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-form-post-type.php",
    "content": "<?php\n/**\n * Test LLMS_Form_Post_Type class\n *\n * @package LifterLMS/Tests\n *\n * @group forms\n *\n * @since 5.0.0\n * @version 6.4.0\n */\nclass LLMS_Test_Form_Post_Type extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Form_Post_Type( LLMS_Forms::instance() );\n\n\t}\n\n\t/**\n\t * Test class properties.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_properties() {\n\n\t\t$this->assertEquals( 'llms_form', $this->main->post_type );\n\t\t$this->assertEquals( 'manage_lifterlms', $this->main->capability );\n\n\t}\n\n\t/**\n\t * Test enabled_post_type_visibility() when skipping the override\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_enable_post_type_visibility() {\n\n\t\t$res_data  = array( 'viewable' => false );\n\t\t$res       = rest_ensure_response( $res_data );\n\t\t$post_type = get_post_type_object( 'llms_form' );\n\n\t\t// Not admin.\n\t\t$this->assertEquals( $res_data, $this->main->enable_post_type_visibility( $res, $post_type )->get_data() );\n\n\t\t// Is admin.\n\t\tset_current_screen( 'admin.php' );\n\n\t\t// Wrong post type.\n\t\t$this->assertEquals( $res_data, $this->main->enable_post_type_visibility( $res, get_post_type_object( 'course' ) )->get_data() );\n\n\t\t// Okay.\n\t\t$this->assertEquals( array( 'viewable' => true ), $this->main->enable_post_type_visibility( $res, $post_type )->get_data() );\n\n\t\tset_current_screen( 'front' ); // Reset.\n\n\t}\n\n\t/**\n\t * Test permalink retrieval for account updates.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_permalink_for_account() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'account' ) ) );\n\t\tparse_str( $url['query'], $qs );\n\n\t\t$this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] );\n\t\t$this->assertEquals( get_option( 'lifterlms_myaccount_page_id' ), $qs['page_id'] );\n\t\t$this->assertArrayHasKey( 'edit-account', $qs );\n\n\t}\n\n\t/**\n\t * Test permalink retrieval for checkout when no access plans exist.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_permalink_for_checkout_no_plans() {\n\n\t\tglobal $wpdb;\n\t\t$wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_access_plan' ) );\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'checkout' ) ) );\n\t\tparse_str( $url['query'], $qs );\n\n\t\t$this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] );\n\t\t$this->assertEquals( get_option( 'lifterlms_checkout_page_id' ), $qs['page_id'] );\n\t\t$this->assertEquals( 'visitor', $qs['llms-view-as'] );\n\n\t\t$this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) );\n\n\t}\n\n\t/**\n\t * Test permalink retrieval for checkout with access plans.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_permalink_for_checkout_with_plans() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$plan = $this->get_mock_plan();\n\n\t\t$url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'checkout' ) ) );\n\t\tparse_str( $url['query'], $qs );\n\n\t\t$this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] );\n\t\t$this->assertEquals( get_option( 'lifterlms_checkout_page_id' ), $qs['page_id'] );\n\t\t$this->assertEquals( 'visitor', $qs['llms-view-as'] );\n\t\t$this->assertEquals( $plan->get( 'id' ), $qs['plan'] );\n\n\t\t$this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) );\n\n\t}\n\n\t/**\n\t * Test permalink retrieval for registration form when open registration is not enabled.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_permalink_for_registration_not_enabled() {\n\n\t\t$form = get_post( LLMS_Forms::instance()->create( 'registration' ) );\n\t\tupdate_option( 'lifterlms_enable_myaccount_registration', 'no' );\n\t\t$this->assertFalse( get_permalink( LLMS_Forms::instance()->create( 'registration' ) ) );\n\n\t}\n\n\t/**\n\t * Test permalink retrieval for registration form when open registration is enabled.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_permalink_for_registration_enabled() {\n\n\t\tLLMS_Install::create_pages();\n\t\tupdate_option( 'lifterlms_enable_myaccount_registration', 'yes' );\n\n\t\t$url = parse_url( get_permalink( LLMS_Forms::instance()->create( 'registration' ) ) );\n\t\tparse_str( $url['query'], $qs );\n\n\t\t$this->assertEquals( parse_url( get_site_url(), PHP_URL_HOST ), $url['host'] );\n\t\t$this->assertEquals( get_option( 'lifterlms_myaccount_page_id' ), $qs['page_id'] );\n\t\t$this->assertEquals( 'visitor', $qs['llms-view-as'] );\n\n\t\t$this->assertEquals( 1, wp_verify_nonce( $qs['view_nonce'], 'llms-view-as' ) );\n\n\t}\n\n\t/**\n\t * Test maybe_prevent_deletion() for other post types\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_prevent_deletion_wrong_post_type() {\n\t\t$post = $this->factory->post->create_and_get();\n\t\t$this->assertNull( $this->main->maybe_prevent_deletion( null, $post ) );\n\t}\n\n\t/**\n\t * Test maybe_prevent_deletion() for non-core forms.\n\t *\n\t * @since 5.0.0\n\t * @since 6.4.0 Updated to test that core form duplicates can be deleted.\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_prevent_deletion_not_core() {\n\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => 'llms_form' ) );\n\t\t$this->assertNull( $this->main->maybe_prevent_deletion( null, $post ) );\n\t\tupdate_post_meta( $post->ID, '_llms_form_is_core', 'yes' );\n\t\t$this->assertNull( $this->main->maybe_prevent_deletion( null, $post ) );\n\t}\n\n\t/**\n\t * Test maybe_prevent_deletion() for core forms that cannot be deleted.\n\t *\n\t * @since 5.0.0\n\t * @since 6.4.0 Updated to test that only real core forms cannot be deleted.\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_prevent_deletion() {\n\t\t$forms = array();\n\t\tforeach ( array_keys( LLMS_Forms::instance()->get_locations() ) as $location ) {\n\t\t\t$forms[] = get_post( LLMS_Forms::instance()->create( $location ) );\n\t\t}\n\t\tforeach ( $forms as $form ) {\n\t\t\t$this->assertFalse( $this->main->maybe_prevent_deletion( null, $form ), $location );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test meta_auth_callback()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_meta_auth_callback() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$form = get_post( LLMS_Forms::instance()->create( 'registration' ) );\n\n\t\t$roles = array(\n\t\t\t'administrator' => true,\n\t\t\t'lms_manager'   => true,\n\t\t\t'instructor'    => false,\n\t\t\t'student'       => false,\n\t\t\t'editor'        => false,\n\t\t\t'subscriber'    => false,\n\t\t);\n\n\t\t// Logged out user can't do stuff.\n\t\t$this->assertFalse( $this->main->meta_auth_callback( false, 'does_not_matter', $form->ID, null, 'does_not_matter', array() ) );\n\n\t\t// Test various roes.\n\t\tforeach ( $roles as $role => $expect ) {\n\t\t\t$user = $this->factory->user->create_and_get( array( 'role' => $role ) );\n\t\t\t$this->assertSame( $expect, $this->main->meta_auth_callback( false, 'does_not_matter', $form->ID, $user->ID, 'does_not_matter', $user->caps ) );\n\t\t}\n\n\n\t}\n\n\t/**\n\t * Test post type registration.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_post_type() {\n\n\t\t// Remove it so we can ensure we register it.\n\t\tunregister_post_type( 'llms_form' );\n\n\t\t// Make sure the filter runs.\n\t\tglobal $form_post_type_registrion_runs;\n\t\t$filter_ran = 0;\n\t\t$handler = function( $name ) {\n\t\t\tglobal $form_post_type_registrion_runs;\n\t\t\t++$form_post_type_registrion_runs;\n\t\t\treturn $name;\n\t\t};\n\t\tadd_filter( 'lifterlms_register_post_type_form', $handler );\n\n\t\t$this->main->register_post_type();\n\n\t\t// Post type has been registered.\n\t\t$this->assertTrue( post_type_exists( 'llms_form' ) );\n\n\t\t// Filter ran.\n\t\t$this->assertEquals( 1, $form_post_type_registrion_runs );\n\n\t\tremove_filter( 'lifterlms_register_post_type_form', $handler );\n\n\t}\n\n\t/**\n\t * Test custom meta prop registration.\n\t *\n\t * @since 5.0.0\n\t * @since 5.10.0 Added expected meta prop '_llms_form_title_free_access_plans'.\n\t * @since 6.10.0 Call the tested method directly instead of indirectly via `do_action( 'init' )`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_meta() {\n\n\t\t$this->main->register_meta();\n\n\t\tglobal $wp_meta_keys;\n\t\t$this->assertArrayHasKey( 'post', $wp_meta_keys );\n\t\t$this->assertArrayHasKey( 'llms_form', $wp_meta_keys['post'] );\n\n\t\t// Expected meta props.\n\t\t$props = array(\n\t\t\t'_llms_form_location',\n\t\t\t'_llms_form_show_title',\n\t\t\t'_llms_form_is_core',\n\t\t\t'_llms_form_title_free_access_plans',\n\t\t);\n\n\t\tforeach ( $props as $meta ) {\n\t\t\t$this->assertArrayHasKey( $meta, $wp_meta_keys['post']['llms_form'] );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-form-templates.php",
    "content": "<?php\n/**\n * Test LLMS_Form_Templates class\n *\n * @package LifterLMS/Tests\n *\n * @group form_templates\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Form_Templates extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Ensures the generated block content of a reusable block matches the stored \"snapshot\"\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.1 Remove duplicate references to last-name block.\n\t *\n\t * @param string $id     Field ID.\n\t * @param string $actual Actual generated content to compare to the expected \"snapshot\"\n\t * @return void\n\t */\n\tpublic function assertReusableBlockContentMatchesSnapshot( $id, $actual ) {\n\n\t\t$snapshots = array(\n\t\t\t'username'     => '<!-- wp:llms/form-field-user-login {\"field\":\"text\",\"required\":true,\"label\":\"Username\",\"name\":\"user_login\",\"id\":\"user_login\",\"data_store\":\"users\",\"data_store_key\":\"user_login\",\"llms_visibility\":\"logged_out\"} /-->',\n\t\t\t'email'        => '<!-- wp:llms/form-field-confirm-group {\"fieldLayout\":\"columns\",\"llms_visibility\":\"logged_out\"} --><!-- wp:llms/form-field-user-email {\"field\":\"email\",\"required\":true,\"label\":\"Email Address\",\"name\":\"email_address\",\"id\":\"email_address\",\"data_store\":\"users\",\"data_store_key\":\"user_email\",\"llms_visibility\":\"off\",\"columns\":6,\"last_column\":false,\"isConfirmationControlField\":true,\"match\":\"email_address_confirm\"} /--><!-- wp:llms/form-field-text {\"field\":\"email\",\"required\":true,\"label\":\"Confirm Email Address\",\"name\":\"email_address_confirm\",\"id\":\"email_address_confirm\",\"data_store\":false,\"data_store_key\":false,\"llms_visibility\":\"off\",\"columns\":6,\"last_column\":true,\"isConfirmationField\":true,\"match\":\"email_address\"} /--><!-- /wp:llms/form-field-confirm-group -->',\n\t\t\t'password'     => '<!-- wp:llms/form-field-confirm-group {\"fieldLayout\":\"columns\",\"llms_visibility\":\"logged_out\"} --><!-- wp:llms/form-field-user-password {\"field\":\"password\",\"required\":true,\"label\":\"Password\",\"name\":\"password\",\"id\":\"password\",\"data_store\":\"users\",\"data_store_key\":\"user_pass\",\"llms_visibility\":\"off\",\"meter\":true,\"min_strength\":\"weak\",\"html_attrs\":{\"minlength\":8},\"meter_description\":\"A weak password is required with at least 8 characters. To make it stronger, use both upper and lower case letters, numbers, and symbols.\",\"columns\":6,\"last_column\":false,\"isConfirmationControlField\":true,\"match\":\"password_confirm\"} /--><!-- wp:llms/form-field-text {\"field\":\"password\",\"required\":true,\"label\":\"Confirm Password\",\"name\":\"password_confirm\",\"id\":\"password_confirm\",\"data_store\":false,\"data_store_key\":false,\"llms_visibility\":\"off\",\"meter\":true,\"min_strength\":\"weak\",\"html_attrs\":{\"minlength\":8},\"meter_description\":\"A weak password is required with at least 8 characters. To make it stronger, use both upper and lower case letters, numbers, and symbols.\",\"columns\":6,\"last_column\":true,\"isConfirmationField\":true,\"match\":\"password\"} /--><!-- /wp:llms/form-field-confirm-group -->',\n\t\t\t'name'         => '<!-- wp:llms/form-field-user-name --><!-- wp:llms/form-field-user-first-name {\"field\":\"text\",\"label\":\"First Name\",\"name\":\"first_name\",\"id\":\"first_name\",\"data_store\":\"usermeta\",\"data_store_key\":\"first_name\",\"columns\":6,\"last_column\":false,\"required\":true} /--><!-- wp:llms/form-field-user-last-name {\"field\":\"text\",\"label\":\"Last Name\",\"name\":\"last_name\",\"id\":\"last_name\",\"data_store\":\"usermeta\",\"data_store_key\":\"last_name\",\"columns\":6,\"last_column\":true,\"required\":true} /--><!-- /wp:llms/form-field-user-name -->',\n\t\t\t'display_name' => '<!-- wp:llms/form-field-user-display-name {\"field\":\"text\",\"required\":true,\"label\":\"Display Name\",\"name\":\"display_name\",\"id\":\"display_name\",\"data_store\":\"users\",\"data_store_key\":\"display_name\"} /-->',\n\t\t\t'address'      => '<!-- wp:llms/form-field-user-address --><!-- wp:llms/form-field-user-address-street --><!-- wp:llms/form-field-user-address-street-primary {\"field\":\"text\",\"label\":\"Address\",\"name\":\"llms_billing_address_1\",\"id\":\"llms_billing_address_1\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_address_1\",\"columns\":8,\"last_column\":false,\"required\":true} /--><!-- wp:llms/form-field-user-address-street-secondary {\"field\":\"text\",\"label\":\"\",\"label_show_empty\":true,\"placeholder\":\"Apartment, suite, etc...\",\"name\":\"llms_billing_address_2\",\"id\":\"llms_billing_address_2\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_address_2\",\"columns\":4,\"last_column\":true,\"required\":false} /--><!-- /wp:llms/form-field-user-address-street --><!-- wp:llms/form-field-user-address-city {\"field\":\"text\",\"label\":\"City\",\"name\":\"llms_billing_city\",\"id\":\"llms_billing_city\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_city\",\"required\":true} /--><!-- wp:llms/form-field-user-address-country {\"field\":\"select\",\"label\":\"Country\",\"name\":\"llms_billing_country\",\"id\":\"llms_billing_country\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_country\",\"required\":true,\"options_preset\":\"countries\",\"placeholder\":\"Select a Country\",\"className\":\"llms-select2\"} /--><!-- wp:llms/form-field-user-address-region --><!-- wp:llms/form-field-user-address-state {\"field\":\"select\",\"label\":\"State \\/ Region\",\"options_preset\":\"states\",\"placeholder\":\"Select a State \\/ Region\",\"name\":\"llms_billing_state\",\"id\":\"llms_billing_state\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_state\",\"columns\":6,\"last_column\":false,\"required\":true,\"className\":\"llms-select2\"} /--><!-- wp:llms/form-field-user-address-postal-code {\"field\":\"text\",\"label\":\"Postal \\/ Zip Code\",\"name\":\"llms_billing_zip\",\"id\":\"llms_billing_zip\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_zip\",\"columns\":6,\"last_column\":true,\"required\":true} /--><!-- /wp:llms/form-field-user-address-region --><!-- /wp:llms/form-field-user-address -->',\n\t\t\t'phone'        => '<!-- wp:llms/form-field-user-phone {\"field\":\"tel\",\"label\":\"Phone Number\",\"name\":\"llms_phone\",\"id\":\"llms_phone\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_phone\",\"required\":false} /-->',\n\t\t);\n\n\t\t// Parse blocks for comparison, mostly because we don't care about the order of attributes.\n\t\t$expected = parse_blocks( $snapshots[ $id ] );\n\t\t$actual   = parse_blocks( $actual );\n\n\t\t$this->assertEquals( $expected, $actual, $id );\n\n\t}\n\n\t/**\n\t * Retrieve a list of field ids as they are to be stored on a template at a given location\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param string $location A form location ID.\n\t * @return string[]\n\t */\n\tprivate function get_template_field_id_list( $location ) {\n\n\t\t$blocks = $this->get_template( $location );\n\t\t$list   = array();\n\n\t\tforeach ( $blocks as $block ) {\n\n\t\t\tif ( 'core/block' === $block['blockName'] ) {\n\t\t\t\t$list[] = get_post_meta( $block['attrs']['ref'], '_llms_field_id', true );\n\t\t\t} elseif ( 'llms/form-field-redeem-voucher' === $block['blockName'] ) {\n\t\t\t\t$list[] = 'voucher';\n\t\t\t} else {\n\t\t\t\t$list[] = $block['blockName'];\n\t\t\t}\n\n\t\t}\n\n\t\treturn $list;\n\n\t}\n\n\tprivate function get_template( $location ) {\n\n\t\t$res = LLMS_Form_Templates::get_template( $location );\n\t\treturn parse_blocks( $res );\n\n\t}\n\n\tprivate function get_block_from_template( $location, $name ) {\n\t\t$blocks = $this->get_template( $location );\n\n\t\tforeach ( $blocks as $block ) {\n\t\t\tif ( $name === $block['blockName'] ) {\n\t\t\t\treturn $block;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\t/**\n\t * Test create_reusable_block()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_and_get_reusable_block() {\n\n\t\t$list = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-reusable-blocks.php';\n\n\t\tforeach ( $list as $field_id => $def ) {\n\n\t\t\t$post_id = LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'create_reusable_block', array( $field_id ) );\n\t\t\t$this->assertTrue( ! empty( $post_id ) && is_int( $post_id ) );\n\n\t\t\t$post = get_post( $post_id );\n\n\t\t\t// Title stored.\n\t\t\t$this->assertEquals( $def['title'], $post->post_title );\n\n\t\t\t// Block(s) inserted correctly.\n\t\t\t$this->assertReusableBlockContentMatchesSnapshot( $field_id, $post->post_content );\n\n\t\t\t// Meta data is stored.\n\t\t\t$this->assertEquals( 'yes', get_post_meta( $post_id, '_is_llms_field', true ) );\n\t\t\t$this->assertEquals( $field_id, get_post_meta( $post_id, '_llms_field_id', true ) );\n\n\t\t\t// If we try to create it again the existing post will be used (in favor of creating a new one).\n\t\t\t$this->assertEquals( $post_id, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'create_reusable_block', array( $field_id ) ) );\n\n\t\t\t// Retrieve the core/block array.\n\t\t\t$expected = array(\n\t\t\t\t'blockName'    => 'core/block',\n\t\t\t\t'attrs'        => array(\n\t\t\t\t\t'ref' => $post_id,\n\t\t\t\t),\n\t\t\t\t'innerContent' => array(),\n\t\t\t);\n\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_reusable_block', array( $field_id ) ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_template() for the account location during a clean installation.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template_account_clean() {\n\n\t\tadd_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' );\n\n\t\t$expected = array(\n\t\t\t'name',\n\t\t\t'display_name',\n\t\t\t'address',\n\t\t\t'phone',\n\t\t\t'email',\n\t\t\t'password',\n\t\t);\n\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'account' ) );\n\n\t\tremove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test get_template() for the account location during an upgrade.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template_account_update() {\n\n\t\tadd_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' );\n\n\t\t$opts = array(\n\t\t\t'lifterlms_user_info_field_email_confirmation_account_visibility' => 'no',\n\t\t\t'lifterlms_user_info_field_address_account_visibility'            => 'hidden',\n\t\t\t'lifterlms_user_info_field_names_account_visibility'              => 'optional',\n\t\t\t'lifterlms_user_info_field_phone_account_visibility'              => 'required',\n\t\t);\n\n\t\tforeach ( $opts as $key => $val ) {\n\t\t\tupdate_option( $key, $val );\n\t\t}\n\n\t\t// Expected List.\n\t\t$expected = array(\n\t\t\t'llms/form-field-user-name',\n\t\t\t'llms/form-field-user-display-name',\n\t\t\t'llms/form-field-user-phone',\n\t\t\t'llms/form-field-user-email',\n\t\t\t'llms/form-field-confirm-group',\n\t\t);\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'account' ) );\n\n\t\t// Password has confirm group.\n\t\t$pass = $this->get_block_from_template( 'account', 'llms/form-field-confirm-group' );\n\t\t$this->assertEquals( 'llms/form-field-user-password', $pass['innerBlocks'][0]['blockName'] );\n\t\t$this->assertEquals( 'llms/form-field-text', $pass['innerBlocks'][1]['blockName'] );\n\n\t\t// No confirm field on email.\n\t\t$email = $this->get_block_from_template( 'account', 'llms/form-field-user-email' );\n\t\t$this->assertEquals( array(), $email['innerBlocks'] );\n\n\t\t// Names are optional.\n\t\t$name = $this->get_block_from_template( 'account', 'llms/form-field-user-name' );\n\t\t$this->assertFalse( $name['innerBlocks'][0]['attrs']['required'] );\n\t\t$this->assertFalse( $name['innerBlocks'][1]['attrs']['required'] );\n\n\t\t// Phone is required.\n\t\t$phone = $this->get_block_from_template( 'account', 'llms/form-field-user-phone' );\n\t\t$this->assertTrue( $phone['attrs']['required'] );\n\n\t\tremove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test get_template() for the checkout location during a clean installation.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template_checkout_clean() {\n\n\t\tadd_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' );\n\n\t\t$expected = array(\n\t\t\t'email',\n\t\t\t'password',\n\t\t\t'name',\n\t\t\t'address',\n\t\t\t'phone',\n\t\t);\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'checkout' ) );\n\n\t\t// With username.\n\t\tupdate_option( 'lifterlms_registration_generate_username', 'no' );\n\t\tarray_unshift( $expected, 'username' );\n\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'checkout' ) );\n\n\t\tdelete_option( 'lifterlms_registration_generate_username' );\n\n\t\tremove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test get_template() for the checkout location during an upgrade.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template_checkout_update() {\n\n\t\tadd_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' );\n\n\t\t$opts = array(\n\t\t\t'lifterlms_user_info_field_email_confirmation_checkout_visibility' => 'no',\n\t\t\t'lifterlms_user_info_field_address_checkout_visibility'            => 'hidden',\n\t\t\t'lifterlms_user_info_field_names_checkout_visibility'              => 'optional',\n\t\t\t'lifterlms_user_info_field_phone_checkout_visibility'              => 'required',\n\t\t);\n\n\t\tforeach ( $opts as $key => $val ) {\n\t\t\tupdate_option( $key, $val );\n\t\t}\n\n\t\t// Expected List.\n\t\t$expected = array(\n\t\t\t'llms/form-field-user-email',\n\t\t\t'llms/form-field-confirm-group',\n\t\t\t'llms/form-field-user-name',\n\t\t\t'llms/form-field-user-phone',\n\t\t);\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'checkout' ) );\n\n\t\t// Password has confirm group.\n\t\t$pass = $this->get_block_from_template( 'account', 'llms/form-field-confirm-group' );\n\t\t$this->assertEquals( 'llms/form-field-user-password', $pass['innerBlocks'][0]['blockName'] );\n\t\t$this->assertEquals( 'llms/form-field-text', $pass['innerBlocks'][1]['blockName'] );\n\n\t\t// No confirm field on email.\n\t\t$email = $this->get_block_from_template( 'checkout', 'llms/form-field-user-email' );\n\t\t$this->assertEquals( array(), $email['innerBlocks'] );\n\n\t\t// Names are optional.\n\t\t$name = $this->get_block_from_template( 'checkout', 'llms/form-field-user-name' );\n\t\t$this->assertFalse( $name['innerBlocks'][0]['attrs']['required'] );\n\t\t$this->assertFalse( $name['innerBlocks'][1]['attrs']['required'] );\n\n\t\t// Phone is required.\n\t\t$phone = $this->get_block_from_template( 'checkout', 'llms/form-field-user-phone' );\n\t\t$this->assertTrue( $phone['attrs']['required'] );\n\n\t\tremove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test get_template() for the registration location during a clean installation.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template_registration_clean() {\n\n\t\tadd_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' );\n\n\t\t$expected = array(\n\t\t\t'email',\n\t\t\t'password',\n\t\t\t'name',\n\t\t\t'address',\n\t\t\t'phone',\n\t\t);\n\n\t\t// No voucher.\n\t\tupdate_option( 'lifterlms_voucher_field_registration_visibility', 'hidden' );\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) );\n\n\t\t// With username.\n\t\tupdate_option( 'lifterlms_registration_generate_username', 'no' );\n\t\tarray_unshift( $expected, 'username' );\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) );\n\n\t\t// With voucher.\n\t\tdelete_option( 'lifterlms_voucher_field_registration_visibility' );\n\t\t$expected[] = 'voucher';\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) );\n\n\t\tdelete_option( 'lifterlms_registration_generate_username' );\n\n\t\tremove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test get_template() for the registration location during an upgrade.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template_registration_update() {\n\n\t\tadd_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' );\n\n\t\t$opts = array(\n\t\t\t'lifterlms_user_info_field_email_confirmation_registration_visibility' => 'yes',\n\t\t\t'lifterlms_user_info_field_address_registration_visibility'            => 'required',\n\t\t\t'lifterlms_user_info_field_names_registration_visibility'              => 'hidden',\n\t\t\t'lifterlms_user_info_field_phone_registration_visibility'              => 'hidden',\n\t\t);\n\n\t\tforeach ( $opts as $key => $val ) {\n\t\t\tupdate_option( $key, $val );\n\t\t}\n\n\t\t// Expected List.\n\t\t$expected = array(\n\t\t\t'llms/form-field-confirm-group', // Email\n\t\t\t'llms/form-field-confirm-group', // Password\n\t\t\t'llms/form-field-user-address',\n\t\t\t'voucher',\n\t\t);\n\t\t$this->assertEquals( $expected, $this->get_template_field_id_list( 'registration' ) );\n\n\t\t$blocks = $this->get_template( 'registration' );\n\n\t\t// Confirm field on email.\n\t\t$email = $blocks[0];\n\t\t$this->assertEquals( 'llms/form-field-user-email', $email['innerBlocks'][0]['blockName'] );\n\t\t$this->assertEquals( 'llms/form-field-text', $email['innerBlocks'][1]['blockName'] );\n\n\t\t// Password has confirm group.\n\t\t$pass = $blocks[1];\n\t\t$this->assertEquals( 'llms/form-field-user-password', $pass['innerBlocks'][0]['blockName'] );\n\t\t$this->assertEquals( 'llms/form-field-text', $pass['innerBlocks'][1]['blockName'] );\n\n\t\t// Address is required are optional.\n\t\t$address = $this->get_block_from_template( 'registration', 'llms/form-field-user-address' );\n\t\t$this->assertTrue( $address['innerBlocks'][0]['innerBlocks'][0]['attrs']['required'] ); // Line 1.\n\t\t$this->assertFalse( $address['innerBlocks'][0]['innerBlocks'][1]['attrs']['required'] ); // Line 2.\n\t\t$this->assertTrue( $address['innerBlocks'][1]['attrs']['required'] ); // City.\n\t\t$this->assertTrue( $address['innerBlocks'][2]['attrs']['required'] ); // Country.\n\t\t$this->assertTrue( $address['innerBlocks'][3]['innerBlocks'][0]['attrs']['required'] ); // State.\n\t\t$this->assertTrue( $address['innerBlocks'][3]['innerBlocks'][1]['attrs']['required'] ); // Postal code.\n\n\t\tremove_filter( 'llms_blocks_template_use_reusable_blocks', '__return_false' );\n\n\t}\n\n\n\t/**\n\t * Test get_voucher_block() when the voucher field is disabled\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_voucher_block_disabled() {\n\n\t\tupdate_option( 'lifterlms_voucher_field_registration_visibility', 'hidden' );\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_voucher_block' ) );\n\n\t\tdelete_option( 'lifterlms_voucher_field_registration_visibility' );\n\n\t}\n\n\t/**\n\t * Test get_voucher_block() when voucher submission is optional.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_voucher_block_optional() {\n\n\t\tupdate_option( 'lifterlms_voucher_field_registration_visibility', 'optional' );\n\n\t\t$expected = array(\n\t\t\t'blockName'    => 'llms/form-field-redeem-voucher',\n\t\t\t'attrs'        => array(\n\t\t\t\t'id'             => 'llms_voucher',\n\t\t\t\t'label'          => __( 'Have a voucher?', 'lifterlms' ),\n\t\t\t\t'placeholder'    => __( 'Voucher Code', 'lifterlms' ),\n\t\t\t\t'required'       => false,\n\t\t\t\t'toggleable'     => true,\n\t\t\t\t'data_store'     => false,\n\t\t\t\t'data_store_key' => false,\n\t\t\t),\n\t\t\t'innerContent' => array(),\n\t\t);\n\n\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_voucher_block' ) );\n\n\t\tdelete_option( 'lifterlms_voucher_field_registration_visibility' );\n\n\t}\n\n\t/**\n\t * Test get_voucher_block() when voucher submission is required.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_voucher_block_required() {\n\n\t\tupdate_option( 'lifterlms_voucher_field_registration_visibility', 'required' );\n\n\t\t$expected = array(\n\t\t\t'blockName'    => 'llms/form-field-redeem-voucher',\n\t\t\t'attrs'        => array(\n\t\t\t\t'id'             => 'llms_voucher',\n\t\t\t\t'label'          => __( 'Have a voucher?', 'lifterlms' ),\n\t\t\t\t'placeholder'    => __( 'Voucher Code', 'lifterlms' ),\n\t\t\t\t'required'       => true,\n\t\t\t\t'toggleable'     => true,\n\t\t\t\t'data_store'     => false,\n\t\t\t\t'data_store_key' => false,\n\t\t\t),\n\t\t\t'innerContent' => array(),\n\t\t);\n\n\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_voucher_block' ) );\n\n\t\tdelete_option( 'lifterlms_voucher_field_registration_visibility' );\n\n\t}\n\n\t/**\n\t * Test prepare_blocks(): Missing properties automatically added.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_blocks_without_props() {\n\n\t\t$input = array(\n\t\t\tarray(),\n\t\t);\n\t\t$expected = array(\n\t\t\tarray(\n\t\t\t\t'attrs'        => array(),\n\t\t\t\t'innerBlocks'  => array(),\n\t\t\t\t'innerContent' => array(),\n\t\t\t),\n\t\t);\n\n\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'prepare_blocks', array( $input ) ) );\n\n\t}\n\n\t/**\n\t * Test prepare_blocks(): Existing props not overwritten.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_blocks_with_props() {\n\n\t\t$input = array(\n\t\t\tarray(\n\t\t\t\t'attrs'       => 'fake',\n\t\t\t\t'innerBlocks' => array(),\n\t\t\t),\n\t\t);\n\t\t$expected = array(\n\t\t\tarray(\n\t\t\t\t'attrs'        => 'fake',\n\t\t\t\t'innerBlocks'  => array(),\n\t\t\t\t'innerContent' => array(),\n\t\t\t),\n\t\t);\n\n\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'prepare_blocks', array( $input ) ) );\n\n\t}\n\n\t/**\n\t * Test prepare_blocks(): Works recursively on inner blocks and fills innerContent.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_prepare_blocks_recursive() {\n\n\t\t$input = array(\n\t\t\tarray(\n\t\t\t\t'attrs'       => array(),\n\t\t\t\t'innerBlocks' => array( array(), array() ),\n\t\t\t),\n\t\t);\n\t\t$expected = array(\n\t\t\tarray(\n\t\t\t\t'attrs'        => array(),\n\t\t\t\t'innerBlocks'  => array_fill( 0, 2, array(\n\t\t\t\t\t'attrs'        => array(),\n\t\t\t\t\t'innerBlocks'  => array(),\n\t\t\t\t\t'innerContent' => array(),\n\t\t\t\t) ),\n\t\t\t\t'innerContent' => array( null, null ),\n\t\t\t),\n\t\t);\n\n\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'prepare_blocks', array( $input ) ) );\n\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-form-validator.php",
    "content": "<?php\n/**\n * Test Form Handler class\n *\n * @package LifterLMS/Tests\n *\n * @group forms\n * @group form_validator\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Form_Validator extends LLMS_UnitTestCase {\n\n\t/**\n\t * Data for testing text and textarea field sanitization\n\t *\n\t * Each array is a numeric array with the first item being the \"dirty\" data and the second item\n\t * being the expected result.\n\t *\n\t * @link https://github.com/WordPress/wordpress-develop/blob/master/tests/phpunit/tests/formatting/SanitizeTextField.php Most data adapted from WP Core tests.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return array\n\t */\n\tprotected function data_for_text_fields() {\n\n\t\treturn array(\n\t\t\tarray(\n\t\t\t\t'оРангутанг',\n\t\t\t\t'оРангутанг',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'one is < two',\n\t\t\t\t'one is &lt; two',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'no <span>tags</span> <em>allowed</em> here <br>',\n\t\t\t\t'no tags allowed here',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t' trimmed ' ,\n\t\t\t\t'trimmed',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'No %AB octets %ab',\n\t\t\t\t'No octets',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'emails@are.okay',\n\t\t\t\t'emails@are.okay',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray(),\n\t\t\t\tarray(),\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tllms(),\n\t\t\t\t'',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tfalse,\n\t\t\t\t'',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\ttrue,\n\t\t\t\t'1',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'text 1',\n\t\t\t\t\t'text 2',\n\t\t\t\t\tfalse,\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'text 1',\n\t\t\t\t\t'text 2',\n\t\t\t\t\t'',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t}\n\n\tprotected function get_field_arr( $type, $args = array() ) {\n\t\treturn wp_parse_args( $args, array(\n\t\t\t'id'   => \"field-{$type}-id\",\n\t\t\t'name' => \"field-{$type}-name\",\n\t\t\t'type' => $type,\n\t\t) );\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Form_Validator();\n\n\t}\n\n\t/**\n\t * Test reducing a fields array down to only the required fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_required_fields() {\n\n\t\t$required = array(\n\t\t\tarray(\n\t\t\t\t'name'     => 'good',\n\t\t\t\t'required' => true,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'name'     => 'good2',\n\t\t\t\t'required' => true,\n\t\t\t),\n\t\t);\n\n\t\t$optional = array(\n\t\t\tarray(\n\t\t\t\t'name'     => 'bad',\n\t\t\t\t'required' => false,\n\t\t\t),\n\t\t);\n\n\t\t// Only has required fields in the array.\n\t\t$this->assertEquals( $required, $this->main->get_required_fields( $required ) );\n\n\t\t// Only has optional fields.\n\t\t$this->assertEquals( array(), $this->main->get_required_fields( $optional ) );\n\n\t\t// Has both required and optional.\n\t\t$this->assertEquals( $required, $this->main->get_required_fields( array_merge( $optional, $required ) ) );\n\n\t}\n\n\t/**\n\t * Test validate_fields() when no user input is supplied.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Added test when the form field is not empty.\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_fields_empty_input() {\n\n\t\t// Empty input - empty form => validates.\n\t\t$res = $this->main->validate_fields( array(), array() );\n\t\t$this->assertTrue( $res );\n\n\t\t// Empty input - not empty form => doesn't validate.\n\t\t$res = $this->main->validate_fields( array(), array( $this->get_field_arr( 'text' ) ) );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-no-input', $res );\n\n\t}\n\n\t/**\n\t * Test sanitize_field() for text fields / default case.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_field_for_default() {\n\n\t\t$tests = $this->data_for_text_fields();\n\n\t\t$tests[] = array(\n\t\t\t\"no \\nnewlines\",\n\t\t\t'no newlines',\n\t\t);\n\t\t$tests[] = array(\n\t\t\t\"no \\ttabs\",\n\t\t\t'no tabs',\n\t\t);\n\t\t$tests[] = array(\n\t\t\t\"internal  whitespace   removed\",\n\t\t\t'internal whitespace removed',\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\t\t\t$this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'text' ) ) );\n\t\t\t$this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'custom-field-type' ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test sanitize_field() for fields whose values are arrays.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\t/*public function test_sanitize_field_arra)y() {\n\n\t\t$tests = $this->data_for_text_fields(;\n\t\t$t\n\t}*/\n\n\t/**\n\t * Sanitize email fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_field_for_email() {\n\n\t\t$emails = array(\n\t\t\t\"hello'hi@hello.com\"                 => \"hello'hi@hello.com\",\n\t\t\t'hello@hello.com'                    => 'hello@hello.com',\n\t\t\t'admin@hello.net'                    => '    admin@hello.net',\n\t\t\t'admin+hi@hello.edu'                 => 'admin+hi@hello.edu',\n\t\t\t'hello@so.many.subdomains.org'       => 'hello@so.many.subdomains.org',\n\t\t\t'ip@204.32.111.32'                   => 'ip@204.32.111.32',\n\t\t\t'l@l.ms'                             => 'l@l.ms',\n\t\t\t''                                   => 'fake',\n\t\t);\n\n\t\tforeach ( $emails as $clean => $dirty ) {\n\t\t\t$this->assertEquals( $clean, $this->main->sanitize_field( $dirty, $this->get_field_arr( 'email' ) ) );\n\t\t}\n\n\t\t$this->assertEquals(\n\t\t\tarray( 'hello@hello.com', 'l@l.ms', '' ),\n\t\t\t$this->main->sanitize_field( array( 'hello@hello.com', 'l@l.ms', 'j' ), $this->get_field_arr( 'email' ) )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test email validation.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_email() {\n\n\t\t$emails = array(\n\t\t\tarray( true, \"hello'hi@hello.com\" ),\n\t\t\tarray( true, 'hello@hello.com' ),\n\t\t\tarray( true, 'admin@hello.net' ),\n\t\t\tarray( true, 'admin+hi@hello.edu' ),\n\t\t\tarray( true, 'hello@so.many.subdomains.org' ),\n\t\t\tarray( true, 'ip@204.32.111.32' ),\n\t\t\tarray( true, 'l@l.ms' ),\n\t\t\tarray( false, 'fake' ),\n\t\t\tarray( false, 'f@k.e' ),\n\t\t\tarray( false, ' f' ),\n\t\t\tarray( false, 'fake.mock.com' ),\n\t\t\tarray( true, array( 'ip@204.32.111.32', 'hello@hello.com' ) ),\n\t\t\tarray( false, array( 'ip@204.32.111.32', 'hello@hello.com', ' f' ) ),\n\t\t);\n\n\t\tforeach ( $emails as $test ) {\n\n\t\t\t$valid = $this->main->validate_field( $test[1], $this->get_field_arr( 'email' ) );\n\n\t\t\tif ( $test[0] ) {\n\t\t\t\t$this->assertTrue( $valid );\n\t\t\t} else {\n\t\t\t\t$this->assertIsWPError( $valid );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test validate_field_attribute_minlength()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_attribute_minlength() {\n\n\t\t$length = 1;\n\t\twhile ( $length <= 25 ) {\n\n\t\t\t$field = $this->get_field_arr( 'password', array(\n\t\t\t\t'attributes' => array(\n\t\t\t\t\t'minlength' => $length,\n\t\t\t\t),\n\t\t\t) );\n\n\t\t\t// Too short.\n\t\t\t$value = str_repeat( 'A', $length - 1 );\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'validate_field_attribute_minlength', array( $value, $length, $field ) );\n\t\t\t$this->assertIsWPError( $res );\n\n\t\t\t// Equal\n\t\t\t$value .= 'A';\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'validate_field_attribute_minlength', array( $value, $length, $field ) );\n\t\t\t$this->assertTrue( $res );\n\n\t\t\t// Longer\n\t\t\t$value .= 'AAAA';\n\t\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'validate_field_attribute_minlength', array( $value, $length, $field ) );\n\t\t\t$this->assertTrue( $res );\n\n\t\t\t++$length;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test validate_field() for a field with an html minlength attribute\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_minlength_attribute() {\n\n\t\t$field = $this->get_field_arr( 'password', array(\n\t\t\t'attributes' => array(\n\t\t\t\t'minlength' => 6,\n\t\t\t),\n\t\t) );\n\n\t\t$tests = array(\n\t\t\tarray( false, 'short' ),\n\t\t\tarray( false, array( 'short' ) ),\n\t\t\tarray( false, array( 'short', 'corto' ) ),\n\t\t\tarray( true, 'it is good' ),\n\t\t\tarray( true, array( 'it is good' ) ),\n\t\t\tarray( true, array( 'it is good', 'è buono' ) ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\t\t\t$res = $this->main->validate_field( $test[1], $field );\n\t\t\tif ( $test[0] ) {\n\t\t\t\t$this->assertTrue( $res );\n\t\t\t} else {\n\t\t\t\t$this->assertIsWPError( $res );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test special validation for the current password field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_password_current() {\n\n\t\t// Not logged in.\n\t\t$no_user = $this->main->validate_field( 'password', $this->get_field_arr( 'password', array( 'id' => 'password_current' ) ) );\n\t\t$this->assertIsWPError( $no_user );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid-no-user', $no_user );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'user_pass' => 'password' ) ) );\n\n\t\t// Invalid password.\n\t\t$invalid = $this->main->validate_field( 'fake', $this->get_field_arr( 'password', array( 'id' => 'password_current' ) ) );\n\t\t$this->assertIsWPError( $invalid );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $invalid );\n\n\t\t// Valid.\n\t\t$valid = $this->main->validate_field( 'password', $this->get_field_arr( 'password', array( 'id' => 'password_current' ) ) );\n\t\t$this->assertTrue( $valid );\n\n\t}\n\n\t/**\n\t * Test special validation for user emails. They must be unique.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_user_email() {\n\n\t\t$email = sprintf( 'mock+%s@mock.tld', uniqid() );\n\n\t\t// Valid.\n\t\t$valid = $this->main->validate_field( $email, $this->get_field_arr( 'email', array( 'id' => 'user_email' ) ) );\n\t\t$this->assertTrue( $valid );\n\n\t\t// Not unique.\n\t\t$this->factory->user->create( array( 'user_email' => $email ) );\n\t\t$exists = $this->main->validate_field( $email, $this->get_field_arr( 'email', array( 'id' => 'user_email' ) ) );\n\t\t$this->assertIsWPError( $exists );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-not-unique', $exists );\n\n\t}\n\n\t/**\n\t * Test special validation for the username field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_user_login() {\n\n\t\t// Banned.\n\t\t$banned = $this->main->validate_field( 'admin', $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) );\n\t\t$this->assertIsWPError( $banned );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $banned );\n\n\t\t// Not valid.\n\t\t$invalid = $this->main->validate_field( '   +-!', $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) );\n\t\t$this->assertIsWPError( $invalid );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-invalid', $invalid );\n\n\t\t$login = sprintf( 'mock-%s', uniqid() );\n\n\t\t// Valid.\n\t\t$valid = $this->main->validate_field( $login, $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) );\n\t\t$this->assertTrue( $valid );\n\n\t\t// Not unique.\n\t\t$this->factory->user->create( array( 'user_login' => $login ) );\n\t\t$exists = $this->main->validate_field( $login, $this->get_field_arr( 'text', array( 'id' => 'user_login' ) ) );\n\t\t$this->assertIsWPError( $exists );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-not-unique', $exists );\n\n\t}\n\n\t/**\n\t * Sanitize telephone fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_field_for_tel() {\n\n\t\t$tels = array(\n\t\t\t'+00 000 000 0000'   => '+00 000 000 0000',\n\t\t\t'+00-000-000-0000'   => '+00-000-000-0000',\n\t\t\t'(000) 000 0000'     => '(000) 000 0000',\n\t\t\t'+00.000.000.0000'   => '+00.000.000.0000',\n\t\t\t'+00 (000) 000 0000' => '+00 (000) 000 0000',\n\t\t\t'000 000 0000 #000'  => '000 000 0000 #000',\n\t\t\t'+00'                => '+00 aaa bbb cccc',\n\t\t\t''                   => 'fake',\n\t\t);\n\n\t\tforeach ( $tels as $clean => $dirty ) {\n\t\t\t$this->assertEquals( $clean, $this->main->sanitize_field( $dirty, $this->get_field_arr( 'tel' ) ) );\n\t\t}\n\n\t\t$this->assertEquals(\n\t\t\tarray( '000 000 0000 #000', '+00' ),\n\t\t\t$this->main->sanitize_field( array( '000 000 0000 #000', '+00 aaa bbb cccc' ), $this->get_field_arr( 'tel' ) )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test telephone validation.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_tel() {\n\n\t\t$emails = array(\n\t\t\tarray( true, '+00 000 000 0000' ),\n\t\t\tarray( true, '+00-000-000-0000' ),\n\t\t\tarray( true, '(000) 000 0000' ),\n\t\t\tarray( true, '+00.000.000.0000' ),\n\t\t\tarray( true, '+00 (000) 000 0000' ),\n\t\t\tarray( true, '000 000 0000 #000' ),\n\t\t\tarray( false, '+00 aaa bbb cccc' ),\n\t\t\tarray( false, 'fake' ),\n\t\t\tarray( true, array( '000 000 0000 #000', '(000) 000 0000' ) ),\n\t\t\tarray( false, array( '000 000 0000 #000', '+00 aaa bbb cccc' ) ),\n\t\t);\n\n\t\tforeach ( $emails as $test ) {\n\n\t\t\t$valid = $this->main->validate_field( $test[1], $this->get_field_arr( 'tel' ) );\n\n\t\t\tif ( $test[0] ) {\n\t\t\t\t$this->assertTrue( $valid );\n\t\t\t} else {\n\t\t\t\t$this->assertIsWPError( $valid );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test sanitize_field() for textareas\n\t *\n\t * We don't need super thorough tests here as we're using a WP Core function.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_field_for_textarea() {\n\n\t\t$tests = $this->data_for_text_fields();\n\n\t\t$tests[] = array(\n\t\t\t\"newlines \\nokay\",\n\t\t\t\"newlines \\nokay\",\n\t\t);\n\t\t$tests[] = array(\n\t\t\t\"tabs \\nokay too\",\n\t\t\t\"tabs \\nokay too\",\n\t\t);\n\t\t$tests[] = array(\n\t\t\t\"internal  whitespace   okay\",\n\t\t\t\"internal  whitespace   okay\",\n\t\t);\n\t\t$tests[] = array(\n\t\t\tarray(\n\t\t\t\t\"internal  whitespace   okay\",\n\t\t\t\t\"tabs \\nokay too\",\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t\"internal  whitespace   okay\",\n\t\t\t\t\"tabs \\nokay too\",\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\t\t\t$this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'textarea' ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test sanitize_field() for URL fields.\n\t *\n\t * We don't need super thorough tests here as we're using a WP Core function.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_field_for_url() {\n\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\t'https://example.tld/',\n\t\t\t\t'https://example.tld/',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'http://www.example.tld',\n\t\t\t\t'http://www.example.tld',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'https://example.tld/path/to/something',\n\t\t\t\t'https://example.tld/path/to/something',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'https://example.tld?qs=yes&more=1',\n\t\t\t\t'https://example.tld?qs=yes&more=1',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'https://example.tld/with space',\n\t\t\t\t'https://example.tld/with%20space',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D',\n\t\t\t\t'',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D',\n\t\t\t\t'',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'https://example.tld/?qs=whatever+<script>alert(1)</script>',\n\t\t\t\t'https://example.tld/?qs=whatever+scriptalert(1)/script',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray(\n\t\t\t\t\t'http://www.example.tld',\n\t\t\t\t\t'https://example.tld/?qs=whatever+<script>alert(1)</script>',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'http://www.example.tld',\n\t\t\t\t\t'https://example.tld/?qs=whatever+scriptalert(1)/script',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\t\t\t$this->assertEquals( $data[1], $this->main->sanitize_field( $data[0], $this->get_field_arr( 'url' ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test validate_field() for a URL field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_url() {\n\n\t\t$tests = array(\n\t\t\tarray( false, 'notaurl' ),\n\t\t\tarray( false, 'test.php' ),\n\t\t\tarray( false, 'example.tld' ),\n\t\t\tarray( true, 'https://example.tld' ),\n\t\t\tarray( true, 'https://example.tld' ),\n\t\t\tarray( true, array( 'https://example.tld', 'https://another-example.ltd' ) ),\n\t\t\tarray( false, array( 'https://example.tld', 'another-example.ltd' ) ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\t$valid = $this->main->validate_field( $test[1], $this->get_field_arr( 'url' ) );\n\n\t\t\tif ( $test[0] ) {\n\t\t\t\t$this->assertTrue( $valid );\n\t\t\t} else {\n\t\t\t\t$this->assertIsWPError( $valid );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Sanitize number fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sanitize_field_for_number() {\n\n\t\t$numbers = array(\n\t\t\t'1'        => '1',\n\t\t\t'100'      => '100',\n\t\t\t'1.00'     => '1.00',\n\t\t\t'1,00'     => '1,00',\n\t\t\t'1,000'    => '1,000',\n\t\t\t'1.000'    => '1.000',\n\t\t\t'1,000.00' => '1,000.00',\n\t\t\t'1.000,00' => '1.000,00',\n\t\t\t'2'        => ' fake 2 mock',\n\t\t);\n\n\t\tforeach ( $numbers as $clean => $dirty ) {\n\t\t\t$this->assertEquals( $clean, $this->main->sanitize_field( $dirty, $this->get_field_arr( 'number' ) ) );\n\t\t}\n\n\t\t$this->assertEquals(\n\t\t\tarray( '1', '100' ),\n\t\t\t$this->main->sanitize_field( array( '1', '100' ), $this->get_field_arr( 'number' ) )\n\t\t);\n\n\t\t$this->assertEquals(\n\t\t\tarray( '1', '100', '2' ),\n\t\t\t$this->main->sanitize_field( array( '1', '100', ' fake 2 mock' ), $this->get_field_arr( 'number' ) )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test number field validation.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_number() {\n\n\t\t$field = $this->get_field_arr( 'number', array( 'name' => 'number_field' ) );\n\n\t\t$tests = array(\n\t\t\tarray( true, '1' ),\n\t\t\tarray( true, '-1' ),\n\t\t\tarray( true, '+1' ),\n\t\t\tarray( true, '100' ),\n\t\t\tarray( true, '1.00' ),\n\t\t\tarray( true, '1,00' ),\n\t\t\tarray( true, '1,000' ),\n\t\t\tarray( true, '1.000' ),\n\t\t\tarray( true, '1,000.00' ),\n\t\t\tarray( true, '1.000,00' ),\n\t\t\tarray( false, ' fake 2 mock' ),\n\t\t\tarray( true, array( '1.000,00', '1' ) ),\n\t\t\tarray( false, array( '1.000,00', '1', ' fake 2 mock' ) ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\t$valid = $this->main->validate_field( $test[1], $field );\n\n\t\t\tif ( $test[0] ) {\n\t\t\t\t$this->assertTrue( $valid );\n\t\t\t} else {\n\t\t\t\t$this->assertIsWPError( $valid );\n\t\t\t}\n\n\t\t}\n\n\t\t$field['attributes']['min'] = '25';\n\t\t$field['attributes']['max'] = '500';\n\n\t\t$tests = array(\n\t\t\tarray( 'greater', '10' ),\n\t\t\tarray( 'greater', '10.50' ),\n\t\t\tarray( 'greater', '24.99' ),\n\t\t\tarray( 'greater', '-500' ),\n\t\t\tarray( 'less', '500.01' ),\n\t\t\tarray( 'less', '1,000' ),\n\t\t\tarray( 'less', '+99999' ),\n\t\t\tarray( 'less', '909090' ),\n\t\t);\n\t\tforeach ( $tests as $test ) {\n\n\t\t\t$res = $this->main->validate_field( $test[1], $field );\n\t\t\t$this->assertIsWPError( $res );\n\t\t\t$this->assertStringContains( $test[0], $res->get_error_message() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test number field validation with empty limits\n\t *\n\t * When min|max attributes are set but empty (like empty string): default.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_number_with_empty_limits() {\n\n\t\t$field = $this->get_field_arr( 'number', array( 'name' => 'number_field' ) );\n\n\t\t$field['attributes']['min'] = '';\n\t\t$field['attributes']['max'] = '';\n\n\t\t$this->assertTrue( $this->main->validate_field( '1', $field ) );\n\t\t$this->assertIsWPError( $this->main->validate_field( ' fake 2 mock', $field ) );\n\n\t\t$field['attributes']['min'] = '0';\n\t\t$field['attributes']['max'] = '';\n\n\t\t$this->assertTrue( $this->main->validate_field( '1', $field ) );\n\t\t$this->assertIsWPError( $this->main->validate_field( ' fake 2 mock', $field ) );\n\t\t$this->assertIsWPError( $this->main->validate_field( '-1', $field ) );\n\t\t$this->assertStringContains( 'greater', $this->main->validate_field( '-1', $field )->get_error_message() );\n\n\t\t$field['attributes']['min'] = '';\n\t\t$field['attributes']['max'] = '5';\n\n\t\t$this->assertTrue( $this->main->validate_field( '1', $field ) );\n\t\t$this->assertIsWPError( $this->main->validate_field( ' fake 2 mock', $field ) );\n\t\t$this->assertIsWPError( $this->main->validate_field( '6', $field ) );\n\t\t$this->assertStringContains( 'less', $this->main->validate_field( '6', $field )->get_error_message() );\n\n\t}\n\n\t/**\n\t * Test special voucher field validation.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_field_for_voucher() {\n\n\t\t$field = $this->get_field_arr( 'text', array( 'id' => 'llms_voucher' ) );\n\n\t\t// Invalid code.\n\t\t$res = $this->main->validate_field( 'invalid-code', $field );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorMessageEquals( 'Voucher code \"invalid-code\" could not be found.', $res );\n\n\t\t// Valid code.\n\t\t$voucher = $this->get_mock_voucher( 1 );\n\t\t$code    = $voucher->get_voucher_codes()[0]->code;\n\t\t$res = $this->main->validate_field( $code, $field );\n\t\t$this->assertTrue( $res );\n\n\t\t// Use the voucher.\n\t\t$voucher->use_voucher( $code, 123 );\n\n\t\t// Valid code without any remaining redemptions.\n\t\t$res = $this->main->validate_field( $code, $field );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorMessageEquals( sprintf( 'Voucher code \"%s\" has already been redeemed the maximum number of times.', $code ), $res );\n\n\t}\n\n\t/**\n\t * Test checking matching fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_matching_fields() {\n\n\t\t$posted = array(\n\t\t\t'email' => 'l@l.ms',\n\t\t\t'email_confirm' => 'l@l.ms',\n\t\t);\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'id'    => 'email',\n\t\t\t\t'name'  => 'email',\n\t\t\t\t'match' => 'email_confirm',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'    => 'email_confirm',\n\t\t\t\t'name'  => 'email_confirm',\n\t\t\t\t'match' => 'email',\n\t\t\t),\n\t\t);\n\n\t\t$this->assertTrue( $this->main->validate_matching_fields( $posted, $fields ) );\n\n\t}\n\n\t/**\n\t * Test checking matching fields when the matching field doesn't exist in the form.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_matching_fields_err_missing_match_definition() {\n\n\t\t$posted = array(\n\t\t\t'email' => 'l@l.ms',\n\t\t);\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'id'    => 'email',\n\t\t\t\t'name'  => 'email',\n\t\t\t\t'match' => 'email_confirm',\n\t\t\t),\n\t\t);\n\n\t\t$this->assertTrue( $this->main->validate_matching_fields( $posted, $fields ) );\n\n\t}\n\n\t/**\n\t * Test checking matchind fields when user data is mismatched.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_matching_fields_err_missing_match() {\n\n\t\t$posted = array(\n\t\t\t'email' => 'l@l.ms',\n\t\t);\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'id'    => 'email',\n\t\t\t\t'name'  => 'email',\n\t\t\t\t'match' => 'email_confirm',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'id'    => 'email_confirm',\n\t\t\t\t'name'  => 'email_confirm',\n\t\t\t\t'match' => 'email',\n\t\t\t),\n\t\t);\n\n\t\t$valid = $this->main->validate_matching_fields( $posted, $fields );\n\t\t$this->assertIsWPError( $valid );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-field-not-matched', $valid );\n\n\t\t// Should only have a single error message, not one error for each message.\n\t\t$this->assertEquals( 1, count( $valid->get_error_messages( 'llms-form-field-not-matched' ) ) );\n\n\t}\n\n\t/**\n\t * Test validate_required_fields()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_required_fields_exist() {\n\n\t\t$posted = array(\n\t\t\t'email'    => 'fake@mock.com',\n\t\t\t'password' => '1234',\n\t\t);\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'label'    => __( 'Email', 'lifterlms' ),\n\t\t\t\t'name'     => 'email',\n\t\t\t\t'required' => true,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'label'    => __( 'Password', 'lifterlms' ),\n\t\t\t\t'name'     => 'password',\n\t\t\t\t'required' => true,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'label'    => __( 'Name', 'lifterlms' ),\n\t\t\t\t'name'     => 'name',\n\t\t\t\t'required' => false,\n\t\t\t),\n\t\t);\n\n\t\t$this->assertTrue( $this->main->validate_required_fields( $posted, $fields ) );\n\n\t}\n\n\t/**\n\t * Test validity of form based on presence of all required fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_validate_required_fields_missing_fields() {\n\n\t\t$posted = array();\n\n\t\t$fields = array(\n\t\t\tarray(\n\t\t\t\t'label'    => __( 'Email', 'lifterlms' ),\n\t\t\t\t'name'     => 'email',\n\t\t\t\t'required' => true,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'label'    => __( 'Password', 'lifterlms' ),\n\t\t\t\t'name'     => 'password',\n\t\t\t\t'required' => true,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'label'    => __( 'Name', 'lifterlms' ),\n\t\t\t\t'name'     => 'name',\n\t\t\t\t'required' => false,\n\t\t\t),\n\t\t);\n\n\t\t// Missing both required fields,\n\t\t$valid = $this->main->validate_required_fields( $posted, $fields );\n\t\t$this->assertIsWPError( $valid );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $valid );\n\t\t$this->assertEquals( 2, count( $valid->errors['llms-form-missing-required'] ) );\n\t\t$this->assertEquals( 'Email is a required field.', $valid->errors['llms-form-missing-required'][0] );\n\t\t$this->assertEquals( 'Password is a required field.', $valid->errors['llms-form-missing-required'][1] );\n\n\t\t// Only missing password.\n\t\t$posted['email'] = 'fake@mock.com';\n\t\t$valid = $this->main->validate_required_fields( $posted, $fields );\n\t\t$this->assertIsWPError( $valid );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $valid );\n\t\t$this->assertEquals( 1, count( $valid->errors['llms-form-missing-required'] ) );\n\t\t$this->assertEquals( 'Password is a required field.', $valid->errors['llms-form-missing-required'][0] );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-forms-admin-bar.php",
    "content": "<?php\n/**\n * Test LLMS_Forms_Admin_Bar class\n *\n * @package LifterLMS/Tests\n *\n * @group forms\n * @group forms_admin_bar\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Forms_Admin_Bar extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Forms_Admin_Bar();\n\n\t}\n\n\t/**\n\t * Initiate (and retrieve) an instance of WP_Admin_Bar\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return WP_Admin_Bar\n\t */\n\tprivate function get_admin_bar() {\n\n\t\tadd_filter( 'show_admin_bar', '__return_true' );\n\t\t_wp_admin_bar_init();\n\n\t\tglobal $wp_admin_bar;\n\n\t\tremove_filter( 'show_admin_bar', '__return_true' );\n\n\t\treturn $wp_admin_bar;\n\n\t}\n\n\t/**\n\t * Test add_menu_items() when nothing should be added\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_menu_items_no_display() {\n\n\t\t$bar = $this->get_admin_bar();\n\n\t\t$this->main->add_menu_items( $bar );\n\n\t\t$this->assertNull( $bar->get_nodes() );\n\n\t}\n\n\t/**\n\t * Test add_menu_items()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_menu_items() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tLLMS_Forms::instance()->install();\n\t\tLLMS_Install::create_pages();\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) );\n\n\t\t$bar = $this->get_admin_bar();\n\n\t\tadd_filter( 'llms_view_manager_should_display', '__return_true' );\n\n\t\t$this->main->add_menu_items( $bar );\n\n\t\t$this->assertEquals( array( 'llms-edit-form' ), array_keys( $bar->get_nodes() ) );\n\n\t\tremove_filter( 'llms_view_manager_should_display', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test get_current_location()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_current_location() {\n\n\t\t// Invalid screen.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tLLMS_Install::create_pages();\n\n\t\t// Checkout.\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) );\n\t\t$this->assertEquals( 'checkout', LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) );\n\n\t\t// Edit Account.\n\t\t$this->go_to( llms_person_edit_account_url() );\n\t\t$this->assertEquals( 'account', LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) );\n\n\t\t// Open Reg.\n\t\tupdate_option( 'lifterlms_enable_myaccount_registration', 'yes' );\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'myaccount' ) ) );\n\t\t$this->mockGetRequest( array( 'llms-view-as' => 'visitor' ) );\n\t\t$this->assertEquals( 'registration', LLMS_Unit_Test_Util::call_method( $this->main, 'get_current_location' ) );\n\n\n\t}\n\n\t/**\n\t * Test should_display() on checkout page\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_should_display() {\n\n\t\t// No user.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Invalid screen.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t\tLLMS_Install::create_pages();\n\n\t\t// Checkout.\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t\t// Edit Account.\n\t\t$this->go_to( llms_person_edit_account_url() );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t\t// Open Reg.\n\t\tupdate_option( 'lifterlms_enable_myaccount_registration', 'yes' );\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'myaccount' ) ) );\n\t\t$this->mockGetRequest( array( 'llms-view-as' => 'visitor' ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'should_display' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-forms-classic-editor.php",
    "content": "<?php\n/**\n * Test LLMS_Forms_Classic_Editor class\n *\n * @package LifterLMS/Tests\n *\n * @group forms\n * @group forms_classic\n *\n * @since 5.0.0\n * @version 5.0.0\n */\nclass LLMS_Test_Forms_Classic_Editor extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test init()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init() {\n\n\t\tremove_filter( 'use_block_editor_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'force_block_editor' ), 200 );\n\t\tremove_filter( 'classic_editor_enabled_editors_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'disable_classic_editor' ), 20 );\n\n\t\tLLMS_Forms_Classic_Editor::init();\n\n\t\t$this->assertEquals( 200, has_filter( 'use_block_editor_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'force_block_editor' ) ) );\n\t\t$this->assertEquals( 20, has_filter( 'classic_editor_enabled_editors_for_post_type', array( 'LLMS_Forms_Classic_Editor', 'disable_classic_editor' ) ) );\n\n\t}\n\n\t/**\n\t * Test force_block_editor()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_force_block_editor() {\n\n\t\t$tests = array(\n\t\t\t'post' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => true,\n\t\t\t\t\t'output' => true\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => false,\n\t\t\t\t\t'output' => false,\n\t\t\t\t)\n\t\t\t),\n\t\t\t'page' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => true,\n\t\t\t\t\t'output' => true\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => false,\n\t\t\t\t\t'output' => false,\n\t\t\t\t)\n\t\t\t),\n\t\t\t'course' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => true,\n\t\t\t\t\t'output' => true\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => false,\n\t\t\t\t\t'output' => false,\n\t\t\t\t)\n\t\t\t),\n\t\t\t'llms_form' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => true,\n\t\t\t\t\t'output' => true\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'input'  => false,\n\t\t\t\t\t'output' => true,\n\t\t\t\t)\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $post_type => $groups ) {\n\t\t\tforeach ( $groups as $data ) {\n\t\t\t\t$this->assertSame( $data['output'], LLMS_Forms_Classic_Editor::force_block_editor( $data['input'], $post_type ) );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test disable_classic_editor()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_disable_classic_editor() {\n\n\t\t$expected = array(\n\t\t\t'classic_editor' => true,\n\t\t\t'block_editor'   => true,\n\t\t);\n\t\tforeach ( array( 'post', 'page', 'course' ) as $post_type ) {\n\t\t\t$this->assertEquals( $expected, LLMS_Forms_Classic_Editor::disable_classic_editor( $expected, $post_type ) );\n\t\t}\n\n\t\t$expected['classic_editor'] = false;\n\t\t$this->assertEquals( $expected, LLMS_Forms_Classic_Editor::disable_classic_editor( $expected, 'llms_form' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-forms-data.php",
    "content": "<?php\n/**\n * Test LLMS_Forms Singleton\n *\n * @package LifterLMS/Tests\n *\n * @group forms_data\n *\n * @since 5.0.0\n * @version 5.0.0\n */\nclass LLMS_Test_Forms_Data extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main  = new LLMS_Forms_Data();\n\t\t$this->forms = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'forms' );\n\n\t}\n\n\tpublic function test_constructor() {\n\n\t\tremove_action( 'save_post_llms_form', array( $this->main, 'save_username_locations' ), 10 );\n\n\t\t$this->main = new LLMS_Forms_Data();\n\n\t\t$this->assertEquals( 10, has_action( 'save_post_llms_form', array( $this->main, 'save_username_locations' ) ) );\n\n\t\t$this->assertTrue( is_a( LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'forms' ), 'LLMS_Forms' ) );\n\n\t}\n\n\tpublic function test_save_username_location_with_username() {\n\n\t\tupdate_option( 'lifterlms_registration_generate_username', 'no' );\n\t\t$reg_form_id = $this->forms->create( 'registration', true );\n\t\t$expect      = array( $reg_form_id );\n\n\t\t// Clear data.\n\t\tdelete_option( 'llms_forms_username_locations' );\n\n\t\t$res = $this->main->save_username_locations( $reg_form_id, get_post( $reg_form_id ) );\n\t\t$this->assertEquals( $expect, $res );\n\n\t\t// Add another form.\n\t\t$form_id = $this->forms->create( 'checkout', true );\n\t\t$expect[] = $form_id;\n\t\t$res = $this->main->save_username_locations( $form_id, get_post( $form_id ) );\n\t\t$this->assertEquals( $expect, $res );\n\n\t\t// Recreate the form without a username field, this should remove the form id.\n\t\tdelete_option( 'lifterlms_registration_generate_username', 'no' );\n\t\t$form_id = $this->forms->create( 'checkout', true );\n\t\t$res = $this->main->save_username_locations( $form_id, get_post( $form_id ) );\n\t\t$this->assertEquals( array( $reg_form_id ), $res );\n\n\t}\n\n}\n\n\n\n\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-forms-dynamic-fields.php",
    "content": "<?php\n/**\n * Test LLMS_Forms_Dynamic_Fields Singleton\n *\n * @package LifterLMS/Tests\n *\n * @group forms\n * @group forms_dynamic_fields\n *\n * @since 5.0.0\n * @version 5.4.1\n */\nclass LLMS_Test_Forms_Dynamic_fields extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = new LLMS_Forms_Dynamic_fields();\n\t\t$this->forms = LLMS_Forms::instance();\n\t}\n\n\t/**\n\t * Test add_password_strength_meter() when no password field found\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_password_strength_meter_no_password() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-text {\"id\":\"block-one\"} /-->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) );\n\t\t$this->assertEquals( $blocks, $res );\n\t}\n\n\t/**\n\t * Test add_password_strength_meter() when the password meter attr is not present\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_password_strength_meter_meter_attr_not_present() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {\"id\":\"password\"} /-->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) );\n\t\t$this->assertEquals( $blocks, $res );\n\n\t}\n\n\t/**\n\t * Test add_password_strength_meter() when the meter is explicitly disabled\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_password_strength_meter_meter_disabled() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {\"id\":\"password\",\"meter\":\"no\"} /-->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) );\n\t\t$this->assertEquals( $blocks, $res );\n\n\t}\n\n\t/**\n\t * Test add_password_strength_meter() when meter is enabled\n\t *\n\t * @since 5.0.0\n\t * @since 5.0.1 Add aria attribute to expected response.\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_password_strength_meter_meter_enabled() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {\"id\":\"password\",\"meter\":\"yes\",\"meter_description\":\"test\"} /--><!-- wp:llms/form-field-text {\"id\":\"block-one\"} /-->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'checkout' ) );\n\n\t\t// Password block is unaffected.\n\t\t$this->assertEquals( $blocks[0], $res[0] );\n\n\t\t$this->assertEquals( '<div class=\"llms-form-field type-html llms-cols-12 llms-cols-last\"><div aria-live=\"polite\" class=\"llms-field-html llms-password-strength-meter\" id=\"llms-password-strength-meter\"></div><span class=\"llms-description\">test</span></div><div class=\"clear\"></div>', trim( $res[1]['innerHTML'] ) );\n\n\t\t// Block after password is in the new last position, unaffected.\n\t\t$this->assertEquals( $blocks[1], $res[2] );\n\n\t}\n\n\n\t/**\n\t * Test add_password_strength_meter() when meter is enabled on the account edit screen\n\t *\n\t * @since 5.0.0\n\t * @since 5.0.1 Add aria attribute to expected response.\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_password_strength_meter_meter_enabled_account() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-user-password {\"id\":\"password\",\"meter\":\"yes\",\"meter_description\":\"test\"} /--><!-- wp:llms/form-field-text {\"id\":\"block-one\"} /-->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'add_password_strength_meter', array( $blocks, 'account' ) );\n\n\t\t// Password block is unaffected.\n\t\t$this->assertEquals( $blocks[0], $res[0] );\n\n\t\t// Differs from above test because of the `llms-visually-hidden-field` class.\n\t\t$this->assertEquals( '<div class=\"llms-form-field type-html llms-cols-12 llms-cols-last llms-visually-hidden-field\"><div aria-live=\"polite\" class=\"llms-field-html llms-password-strength-meter\" id=\"llms-password-strength-meter\"></div><span class=\"llms-description\">test</span></div><div class=\"clear\"></div>', trim( $res[1]['innerHTML'] ) );\n\n\t\t// Block after password is in the new last position, unaffected.\n\t\t$this->assertEquals( $blocks[1], $res[2] );\n\n\t}\n\n\t/**\n\t * Test find_block() when no password confirmation is present\n\t *\n\t * This also tests that the password field isn't the first field in the form to ensure the index returns properly.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_find_block_not_nested() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-text {\"id\":\"block-one\"} /--><!-- wp:llms/form-field-user-password {\"id\":\"password\"} /--><!-- wp:llms/form-field-text {\"id\":\"block-two\"} /-->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) );\n\n\t\t$this->assertEquals( array( 1, $blocks[1] ), $res );\n\n\t}\n\n\t/**\n\t * Test find_block() when no password block is present\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_find_block_no_field() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-text {\"id\":\"block-one\"} /-->' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) ) );\n\n\t}\n\n\t/**\n\t * Test find_block() when a password confirm field is used\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_find_block_with_confirm() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-confirm-group -->\n<!-- wp:llms/form-field-user-password {\"id\":\"password\"} /-->\n<!-- wp:llms/form-field-text {\"id\":\"password-confirm\"} /-->\n<!-- /wp:llms/form-field-confirm-group -->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) );\n\n\t\t$this->assertEquals( array( 0, $blocks[0]['innerBlocks'][0] ), $res );\n\n\t}\n\n\t/**\n\t * Test find_block() when a password confirm field is used and the block is nested inside another block (in this case a wp core group block)\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_find_block_nested() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:group -->\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container\"><!-- wp:llms/form-field-confirm-group -->\n<!-- wp:llms/form-field-user-password {\"id\":\"password\"} /-->\n<!-- wp:llms/form-field-text {\"id\":\"password-confirm\"} /-->\n<!-- /wp:llms/form-field-confirm-group --></div></div>\n<!-- /wp:group -->' );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'find_block', array( 'password', $blocks ) );\n\n\t\t$this->assertEquals( array( 0, $blocks[0]['innerBlocks'][0]['innerBlocks'][0] ), $res );\n\n\t}\n\n\tpublic function test_get_toggle_button_html() {\n\n\t\t$expect = '<a class=\"llms-toggle-fields\" data-fields=\"#mock\" data-change-text=\"Change Label\" data-cancel-text=\"Cancel\" href=\"#\">Change Label</a>';\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_toggle_button_html', array( '#mock', 'Label' ) );\n\n\t\t$this->assertEquals( $expect, $res );\n\n\t}\n\n\tpublic function test_modify_account_form_wrong_form() {\n\n\t\t$input = 'fake';\n\t\t$this->assertEquals( $input, $this->main->modify_account_form( $input, 'checkout' ) );\n\n\t}\n\n\tpublic function test_modify_account_form() {\n\n\t\t$fields = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( parse_blocks( LLMS_Form_Templates::get_template( 'account' ) ) ) );\n\n\n\t\t$res = $this->main->modify_account_form( $fields, 'account' );\n\n\t\t// @todo.\n\t\t$this->markTestIncomplete();\n\n\t}\n\n\t/**\n\t * Test required fields block added to form blocks\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_add_required_block_fields() {\n\n\t\t// Make sure no user is logged in.\n\t\twp_set_current_user( null );\n\n\t\t// Email and pw fields not added to forms which are not checkout or registration.\n\t\t$this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'what', array() ) );\n\t\t$this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'account', array() ) );\n\n\t\t// Email and pw fields added to checkout form.\n\t\t$checkout_blocks = $this->main->maybe_add_required_block_fields( array(), 'checkout', array() );\n\t\tforeach ( array( 'email_address', 'password' ) as $id ) {\n\t\t\t$this->assertNotEmpty(\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'find_block',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$id,\n\t\t\t\t\t\t$checkout_blocks\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$id\n\t\t\t);\n\t\t}\n\n\t\t// Email and pw fields added to registration form.\n\t\t$registration_blocks = $this->main->maybe_add_required_block_fields( array(), 'registration', array() );\n\t\tforeach ( array( 'email_address', 'password' ) as $id ) {\n\t\t\t$this->assertNotEmpty(\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'find_block',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$id,\n\t\t\t\t\t\t$registration_blocks\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$id\n\t\t\t);\n\t\t}\n\n\t\t// Log in.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t// Email and pw field not added to any forms except account for logged in users.\n\t\t$this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'what', array() ) );\n\t\t$this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'checkout', array() ) );\n\t\t$this->assertEmpty( $this->main->maybe_add_required_block_fields( array(), 'registration', array() ) );\n\n\t\t$account_blocks = $this->main->maybe_add_required_block_fields( array(), 'account', array() );\n\t\tforeach ( array( 'email_address', 'password' ) as $id ) {\n\t\t\t$this->assertNotEmpty(\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'find_block',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$id,\n\t\t\t\t\t\t$account_blocks\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$id\n\t\t\t);\n\t\t}\n\n\t\t// Make sure no user is logged in.\n\t\twp_set_current_user( null );\n\n\t}\n\n\t/**\n\t * Test required fields made visible if they were not\n\t *\n\t * This additionally covers `LLMS_Forms_Dynamic_Fields::make_block_visible()` and `LLMS_Forms_Dynamic_Fields::get_confirm_group()`.\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_add_required_block_fields_not_visible_fields() {\n\n\t\t// Make sure no user is logged in.\n\t\twp_set_current_user( null );\n\n\t\t// Only visible to logged in users, for testing purposes.\n\t\t$email_confirm_original_block = array( // Use a confirm block to cover `LLMS_Forms_Dynamic_Fields::get_confirm_group()`.\n\t\t\t'blockName'    => 'llms/form-field-confirm-group',\n\t\t\t'attrs'        => array(\n\t\t\t\t'fieldLayout'     => 'columns',\n\t\t\t\t'llms_visibility' => 'logged_in',\n\t\t\t),\n\t\t\t'innerBlocks'  => array(\n\t\t\t\tarray(\n\t\t\t\t\t'blockName'    => 'llms/form-field-user-email',\n\t\t\t\t\t'attrs'        => array(\n\t\t\t\t\t\t'required'                  => true,\n\t\t\t\t\t\t'llms_visibility'           => 'logged_in',\n\t\t\t\t\t\t'id'                        => 'email_address',\n\t\t\t\t\t\t'name'                      => 'email_address',\n\t\t\t\t\t\t'label'                     => 'Email Address',\n\t\t\t\t\t\t'data_store'                => 'users',\n\t\t\t\t\t\t'data_store_key'            => 'user_email',\n\t\t\t\t\t\t'field'                     => 'email',\n\t\t\t\t\t\t'isConfimationControlField' => true,\n\t\t\t\t\t\t'match'                     => 'email_address_confirm',\n\t\t\t\t\t\t'isOriginal'                => true, // For testing purposes.\n\t\t\t\t\t),\n\t\t\t\t\t'innerBlocks'  => array(),\n\t\t\t\t\t'innerHTML'    => '',\n\t\t\t\t\t'innerContent' => array(),\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'blockName'    => 'llms/form-field-text',\n\t\t\t\t\t'attrs'        => array(\n\t\t\t\t\t\t'required'                  => true,\n\t\t\t\t\t\t'id'                        => 'email_address_confirm',\n\t\t\t\t\t\t'name'                      => 'email_address_confirm',\n\t\t\t\t\t\t'label'                     => 'Confirm Email Address',\n\t\t\t\t\t\t'data_store'                => '',\n\t\t\t\t\t\t'data_store_key'            => '',\n\t\t\t\t\t\t'field'                     => 'email',\n\t\t\t\t\t\t'isConfimationControlField' => true,\n\t\t\t\t\t\t'match'                     => 'email_address_confirm',\n\t\t\t\t\t),\n\t\t\t\t\t'innerBlocks'  => array(),\n\t\t\t\t\t'innerHTML'    => '',\n\t\t\t\t\t'innerContent' => array(),\n\t\t\t\t),\n\t\t\t),\n\t\t\t'innerHTML'    => '',\n\t\t\t'innerContent' => array(\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t),\n\t\t);\n\n\t\t// Only visible to logged in users, for testing purposes.\n\t\t$password_original_block = array(\n\t\t\t'blockName'    => 'llms/form-field-user-password',\n\t\t\t'attrs'        => array(\n\t\t\t\t'required'                  => true,\n\t\t\t\t'llms_visibility'           => 'logged_in',\n\t\t\t\t'id'                        => 'password',\n\t\t\t\t'name'                      => 'password',\n\t\t\t\t'label'                     => 'Password',\n\t\t\t\t'data_store'                => 'users',\n\t\t\t\t'data_store_key'            => 'user_pass',\n\t\t\t\t'field'                     => 'password',\n\t\t\t\t'isOriginal'                => true, // For testing purposes.\n\t\t\t),\n\t\t\t'innerBlocks'  => array(),\n\t\t\t'innerHTML'    => '',\n\t\t\t'innerContent' => array(),\n\t\t);\n\n\t\t$blocks = $this->main->maybe_add_required_block_fields(\n\t\t\tarray(\n\t\t\t\t$email_confirm_original_block,\n\t\t\t\t$password_original_block\n\t\t\t),\n\t\t\t'checkout',\n\t\t\tarray()\n\t\t);\n\n\n\t\t// Check the email block is visible.\n\t\t$email_block = LLMS_Unit_Test_Util::call_method(\n\t\t\t$this->main,\n\t\t\t'find_block',\n\t\t\tarray(\n\t\t\t\t'email_address',\n\t\t\t\t$blocks\n\t\t\t)\n\t\t);\n\t\t$this->assertNotEmpty( $email_block );\n\t\t$this->assertTrue( $this->forms->is_block_visible_in_list( $email_block[1], $blocks ) );\n\n\t\t// Check the password is visible.\n\t\t$password_block = LLMS_Unit_Test_Util::call_method(\n\t\t\t$this->main,\n\t\t\t'find_block',\n\t\t\tarray(\n\t\t\t\t'password',\n\t\t\t\t$blocks\n\t\t\t)\n\t\t);\n\t\t$this->assertNotEmpty( $password_block );\n\t\t$this->assertTrue( $this->forms->is_block_visible_in_list( $password_block[1], $blocks ) );\n\n\t\t// Check both email and password block are the original ones (not replaced, only made visibile).\n\t\t$this->assertTrue( $email_block[1]['attrs']['isOriginal'] );\n\t\t$this->assertTrue( $password_block[1]['attrs']['isOriginal'] );\n\n\t\t// Move the password block into a group block, so to test it's correctly extrapolated from its parent.\n\t\t$blocks = $this->main->maybe_add_required_block_fields(\n\t\t\tarray(\n\t\t\t\t$email_confirm_original_block,\n\t\t\t\tarray(\n\t\t\t\t\t'blockName'    => 'core/group',\n\t\t\t\t\t'attrs'        => array(\n\t\t\t\t\t\t'isPasswordParent' => true,\n\t\t\t\t\t),\n\t\t\t\t\t'innerBlocks'  => array(\n\t\t\t\t\t\t$password_original_block\n\t\t\t\t\t),\n\t\t\t\t\t'innerHTML'    => '',\n\t\t\t\t\t'innerContent' => array(\n\t\t\t\t\t\tnull\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t),\n\t\t\t'checkout',\n\t\t\tarray()\n\t\t);\n\n\t\t// Check the password is visible.\n\t\t$password_block = LLMS_Unit_Test_Util::call_method(\n\t\t\t$this->main,\n\t\t\t'find_block',\n\t\t\tarray(\n\t\t\t\t'password',\n\t\t\t\t$blocks\n\t\t\t)\n\t\t);\n\t\t$this->assertNotEmpty( $password_block );\n\t\t$this->assertTrue( $this->forms->is_block_visible_in_list( $password_block[1], $blocks ) );\n\t\t// It's the original one.\n\t\t$this->assertTrue( $password_block[1]['attrs']['isOriginal'] );\n\t\t// Check it has no parents anymore.\n\t\t$this->assertTrue( $this->forms->is_block_visible_in_list( $password_block[1], $blocks ) );\n\t\t// Check its former parent is now empty.\n\t\tforeach ( $blocks as $block ) {\n\t\t\tif ( 'core/group' === $block['blockName'] && ! empty( $block['attrs']['isPasswordParent'] ) ) {\n\t\t\t\t$this->assertEmpty( $block['innerBlocks'] );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test required fields blocks not added to form blocks if they already have them.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_add_required_block_fields_check_no_dupes() {\n\n\t\tforeach ( array( 'checkout', 'registration', 'account' ) as $location ) {\n\t\t\tif ( 'account' === $location ) {\n\t\t\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t\t}\n\n\t\t\t$this->forms->create( $location, true );\n\t\t\t$blocks = $this->forms->get_form_blocks( $location );\n\n\t\t\tforeach ( array( 'email_address', 'password' ) as $id ) {\n\n\t\t\t\t$block  = LLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'find_block',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$id,\n\t\t\t\t\t\t$blocks\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$this->assertNotEmpty(\n\t\t\t\t\t$block,\n\t\t\t\t\t\"{$location}:{$id}\"\n\t\t\t\t);\n\n\t\t\t\t// Check again for dupes.\n\t\t\t\tarray_splice( $blocks, $block[0], 1); // Remove just found block.\n\n\t\t\t\t$this->assertEmpty(\n\t\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t\t$this->main,\n\t\t\t\t\t\t'find_block',\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t$id,\n\t\t\t\t\t\t\t$blocks\n\t\t\t\t\t\t)\n\t\t\t\t\t),\n\t\t\t\t\t\"{$location}:{$id}\"\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif ( 'account' === $location ) {\n\t\t\t\twp_set_current_user( null );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test remove_block\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_remove_block() {\n\n\t\t$this->forms->create( 'checkout', true );\n\t\t$blocks = $this->forms->get_form_blocks( 'checkout' );\n\n\t\t// Remove a field block, e.g. the email one.\n\t\t$email_field_block = LLMS_Unit_Test_Util::call_method(\n\t\t\t$this->main,\n\t\t\t'find_block',\n\t\t\tarray(\n\t\t\t\t'email_address',\n\t\t\t\t$blocks\n\t\t\t)\n\t\t)[1];\n\n\t\t$removed = LLMS_Unit_Test_Util::call_method(\n\t\t\t$this->main,\n\t\t\t'remove_block',\n\t\t\tarray(\n\t\t\t\t$email_field_block,\n\t\t\t\t&$blocks\n\t\t\t)\n\t\t);\n\n\t\t$this->assertTrue( $removed );\n\n\t\t$this->assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'find_block',\n\t\t\t\tarray(\n\t\t\t\t\t'email_address',\n\t\t\t\t\t$blocks\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t$this->assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t$this->main,\n\t\t\t\t'remove_block',\n\t\t\t\tarray(\n\t\t\t\t\t$email_field_block,\n\t\t\t\t\t&$blocks\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test required fields are still added if their reusable blocks exist but do not contain them.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_required_fields_added_when_reusable_empty() {\n\n\t\tforeach ( array( 'checkout', 'registration' ) as $location ) {\n\t\t\tif ( 'account' === $location ) {\n\t\t\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t\t}\n\n\t\t\t// Get reusable blocks.\n\t\t\tforeach ( array( 'email', 'password' ) as $block_name ) {\n\t\t\t\t$reusable_block = LLMS_Form_Templates::get_block( $block_name, $location, true );\n\n\t\t\t\t// Turn reusable block contents into text (we remove the fields from them).\n\t\t\t\tif ( ! empty( $reusable_block['attrs']['ref'] ) ) {\n\t\t\t\t\twp_update_post(\n\t\t\t\t\t\tarray(\n\t\t\t\t\t\t\t'ID'      => $reusable_block['attrs']['ref'],\n\t\t\t\t\t\t\t'post_content' => '<p>Nothing special</p>',\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$this->forms->create( $location, true );\n\t\t\t// Here's where the required fields are added back.\n\t\t\t$blocks = $this->forms->get_form_blocks( $location );\n\n\t\t\tforeach ( array( 'email_address', 'password' ) as $id ) {\n\n\t\t\t\t$block  = LLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->main,\n\t\t\t\t\t'find_block',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t$id,\n\t\t\t\t\t\t$blocks\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\t$this->assertNotEmpty(\n\t\t\t\t\t$block,\n\t\t\t\t\t\"{$location}:{$id}\"\n\t\t\t\t);\n\n\t\t\t}\n\n\t\t\tif ( 'account' === $location ) {\n\t\t\t\twp_set_current_user( null );\n\t\t\t}\n\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-forms-unsupported-versions.php",
    "content": "<?php\n/**\n * Test LLMS_Forms_Unsupported_Versions\n *\n * @package LifterLMS/Tests/Forms\n *\n * @group forms\n * @group forms_unsupported_versions\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Forms_Unsupported_Versions extends LLMS_UnitTestCase {\n\n\t/**\n\t * Set up before class\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/forms/class-llms-forms-unsupported-versions.php';\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->init_main();\n\n\t}\n\n\t/**\n\t * Construct a new main class for testing.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function init_main() {\n\t\t$this->main = new LLMS_Forms_Unsupported_Versions();\n\t}\n\n\t/**\n\t * Test constructor\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\tglobal $wp_version;\n\t\t$temp = $wp_version;\n\n\t\t$versions = array(\n\t\t\t'5.8.0' => false,\n\t\t\t'5.7.2' => false,\n\t\t\t'5.7.0' => false,\n\t\t\t'5.6.2' => 10,\n\t\t\t'5.6.0' => 10,\n\t\t\t'5.5.0' => 10,\n\t\t);\n\n\t\tforeach ( $versions as $wp_version => $expect ) {\n\t\t\t$this->init_main();\n\t\t\t$this->assertEquals( $expect, has_action( 'current_screen', array( $this->main, 'init' ) ) );\n\t\t\tremove_action( 'current_screen', array( $this->main, 'init' ) );\n\t\t}\n\n\t\t$wp_version = $temp;\n\n\t}\n\n\t/**\n\t * Test init() when nothing should happen\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_for_other() {\n\n\t\tset_current_screen( 'admin.php' );\n\n\t\t$this->main->init();\n\n\t\t$this->assertFalse( has_action( 'admin_print_styles', array( $this->main, 'print_styles' ) ) );\n\t\t$this->assertFalse( has_action( 'admin_notices', array( $this->main, 'output_notice' ) ) );\n\n\t\tset_current_screen( 'front' );\n\n\t}\n\n\t/**\n\t * Test init() for the forms post table list\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_for_post_table() {\n\n\t\tset_current_screen( 'edit-llms_form' );\n\n\t\t$this->main->init();\n\n\t\t$this->assertEquals( 10, has_action( 'admin_print_styles', array( $this->main, 'print_styles' ) ) );\n\t\t$this->assertEquals( 10, has_action( 'admin_notices', array( $this->main, 'output_notice' ) ) );\n\n\t\tset_current_screen( 'front' );\n\n\t}\n\n\t/**\n\t * Test init() when accessing a form block editor directly\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_for_form_post() {\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( 'http://example.org/wp-admin/edit.php?post_type=llms_form [302] YES' );\n\n\t\ttry {\n\n\t\t\tset_current_screen( 'llms_form' );\n\t\t\t$this->main->init();\n\n\t\t} catch ( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\tset_current_screen( 'front' );\n\t\t\tthrow $exception;\n\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/forms/class-llms-test-forms.php",
    "content": "<?php\n/**\n * Test LLMS_Forms Singleton\n *\n * @package LifterLMS/Tests\n *\n * @group forms\n *\n * @since 5.0.0\n * @version 6.4.0\n */\nclass LLMS_Test_Forms extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var LLMS_Forms\n\t */\n\tprivate LLMS_Forms $forms;\n\n\t/**\n\t * Serializes checkboxes attributes and appends a 'llms/form-field-checkboxes' block markup to the form.\n\t *\n\t * @since 6.2.0\n\t *\n\t * @param int   $form_id    WP post ID of the form to append to.\n\t * @param array $checkboxes Attributes, {@see LLMS_Test_Forms::get_checkboxes_attributes()}.\n\t * @return void\n\t */\n\tprivate function append_checkboxes_to_form( $form_id, $checkboxes ) {\n\n\t\t$form_post               = get_post( $form_id );\n\t\t$form_post->post_content .= '<!-- wp:llms/form-field-checkboxes ' . wp_json_encode( $checkboxes ) . ' /-->';\n\t\twp_update_post( $form_post );\n\t}\n\n\t/**\n\t * Setup the test\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->forms = LLMS_Forms::instance();\n\n\t}\n\n\t/**\n\t * Teardown the test.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\n\t\tglobal $wpdb;\n\t\t$wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) );\n\n\t}\n\n\t/**\n\t * Returns an array of attributes for a 'llms/form-field-checkboxes' block to be serialized into\n\t * a form's `post_content`.\n\t *\n\t * @since 6.2.0\n\t *\n\t * @param int $form_id WP post ID of the form.\n\t * @return array\n\t */\n\tprivate function get_checkboxes_attributes( $form_id ) {\n\n\t\t$checkboxes = array(\n\t\t\t'field' => 'checkbox',\n\t\t\t'id'    => \"checkbox-{$form_id}-1\",\n\t\t\t'label' => 'Do you like coffee?',\n\t\t\t'options' => array(\n\t\t\t\tarray(\n\t\t\t\t\t'text'    => 'Yes',\n\t\t\t\t\t'key'     => 'like_coffee_yes',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'text'    => 'No',\n\t\t\t\t\t'key'     => 'like_coffee_no',\n\t\t\t\t),\n\t\t\t\tarray(\n\t\t\t\t\t'text'    => 'No, but I like the smell of it.',\n\t\t\t\t\t'key'     => 'like_coffee_smell',\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\n\t\treturn $checkboxes;\n\t}\n\n\t/**\n\t * Retrieve an array of form locations to run tests against.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return string[]\n\t */\n\tprivate function get_form_locs() {\n\n\t\treturn array( 'checkout', 'registration', 'account' );\n\n\t}\n\n\t/**\n\t * Assert that an array looks like a WordPress block array.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $block Block settings array.\n\t * @return void\n\t */\n\tprotected function assertIsABlock( $block ) {\n\n\t\tforeach ( array( 'blockName', 'attrs', 'innerBlocks', 'innerHTML', 'innerContent' ) as $prop ) {\n\t\t\t$this->assertTrue( array_key_exists( $prop, $block ), \"Block is missing property {$prop}.\" );\n\t\t}\n\n\t\tif ( ! empty( $block['innerBlocks'] ) ) {\n\t\t\tforeach ( $block['innerBlocks'] as $innerBlock ) {\n\t\t\t\t$this->assertIsABlock( $innerBlock );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Assert that an array looks like a LifterLMS Form Field settings array.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @param array $field Field settings array.\n\t * @return void\n\t */\n\tprotected function assertIsAField( $field ) {\n\n\t\tforeach ( array( 'id', 'name', 'type' ) as $prop ) {\n\t\t\t$this->assertTrue( array_key_exists( $prop, $field ), \"Field is missing property {$prop}.\" );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test singleton instance.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_instance() {\n\n\t\t$this->assertClassHasStaticAttribute( 'instance', 'LLMS_Forms' );\n\n\t}\n\n\t/**\n\t * Test are_requirements_met()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_are_requirements_met() {\n\n\t\tglobal $wp_version;\n\t\t$temp = $wp_version;\n\n\t\t$versions = array(\n\t\t\t'5.3.1' => false,\n\t\t\t'5.6.0' => false,\n\t\t\t'5.6.5' => false,\n\t\t\t'5.7.0' => true,\n\t\t\t'5.7.2' => true,\n\t\t\t'5.8.0' => true,\n\t\t);\n\n\t\tforeach ( $versions as $wp_version => $expect ) {\n\n\t\t\t$this->assertEquals( $expect, LLMS_Forms::instance()->are_requirements_met(), $wp_version );\n\n\t\t}\n\n\t\t// Restore the version.\n\t\t$wp_version = $temp;\n\n\t}\n\n\t/**\n\t * Test are_usernames_enabled() when at least one form with a username block exists.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_are_usernames_enabled_one_form_with_usernames() {\n\n\t\tupdate_option( 'lifterlms_registration_generate_username', 'no' );\n\t\t$this->forms->create( 'registration', true );\n\n\t\t$this->assertTrue( $this->forms->are_usernames_enabled() );\n\n\t\t// Explicitly disabled by the filter.\n\t\tadd_filter( 'llms_are_usernames_enabled', '__return_false' );\n\t\t$this->assertFalse( $this->forms->are_usernames_enabled() );\n\t\tremove_filter( 'llms_are_usernames_enabled', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test are_usernames_enabled() when no forms with usernames exist.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_are_usernames_enabled_no_forms_with_usernames() {\n\n\t\tupdate_option( 'lifterlms_registration_generate_username', 'yes' );\n\t\t$this->forms->create( 'registration', true );\n\t\t$this->forms->create( 'checkout', true );\n\n\t\t$this->assertFalse( $this->forms->are_usernames_enabled() );\n\n\t\t// Explicitly enabled by the filter.\n\t\tadd_filter( 'llms_are_usernames_enabled', '__return_true' );\n\t\t$this->assertTrue( $this->forms->are_usernames_enabled() );\n\t\tremove_filter( 'llms_are_usernames_enabled', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test are_usernames_enabled() when there's a mixture of forms with and without usernames.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_are_usernames_enabled_some_forms_with_usernames() {\n\n\t\t// Has username.\n\t\tupdate_option( 'lifterlms_registration_generate_username', 'no' );\n\t\t$this->forms->create( 'checkout', true );\n\n\t\t// Doesn't have username.\n\t\tupdate_option( 'lifterlms_registration_generate_username', 'yes' );\n\t\t$this->forms->create( 'registration', true );\n\n\t\t$this->assertTrue( $this->forms->are_usernames_enabled() );\n\n\t}\n\n\t/**\n\t * Test block_to_field_settings(): ensure keys are renamed properly.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_to_field_settings() {\n\n\t\t$attrs = array(\n\t\t\t'id'        => 'field_id',\n\t\t\t'className' => 'mock fake class-name',\n\t\t\t'field'     => 'text',\n\t\t\t'extra'     => 'remains',\n\t\t);\n\t\t$html   = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) );\n\t\t$blocks = parse_blocks( $html );\n\n\t\t$parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) );\n\t\t$expect = array(\n\t\t\t'id'      => 'field_id',\n\t\t\t'classes' => 'mock fake class-name',\n\t\t\t'type'    => 'text',\n\t\t\t'extra'   => 'remains',\n\t\t);\n\t\t$this->assertEquals( $expect, $parsed );\n\n\t}\n\n\t/**\n\t * Test block_to_field_settings(): no keys to rename so attributes don't change.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_to_field_settings_no_updates() {\n\n\t\t$attrs  = array(\n\t\t\t'id'    => 'field_id',\n\t\t\t'extra' => 'remains',\n\t\t);\n\t\t$html   = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) );\n\t\t$blocks = parse_blocks( $html );\n\n\t\t$parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) );\n\t\t$this->assertEquals( $attrs, $parsed );\n\n\t}\n\n\t/**\n\t * Test block_to_field_settings(): has visibility but the field isn't required so we don't do anything.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_to_field_settings_with_visiblity_no_required() {\n\n\t\t$attrs = array(\n\t\t\t'id'              => 'field_id',\n\t\t\t'llms_visibility' => 'logged_in',\n\t\t\t'extra'           => 'remains',\n\t\t);\n\t\t$html   = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) );\n\t\t$blocks = parse_blocks( $html );\n\n\t\t$parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) );\n\t\t$this->assertEquals( $attrs, $parsed );\n\n\t}\n\n\t/**\n\t * Test block_to_field_settings(): has visibility and field is required so the required should be switched to optional.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_block_to_field_settings_with_visiblity_is_required() {\n\n\t\t$attrs = array(\n\t\t\t'id'              => 'field_id',\n\t\t\t'llms_visibility' => 'logged_in',\n\t\t\t'extra'           => 'remains',\n\t\t\t'required'        => true,\n\t\t);\n\t\t$html   = sprintf( '<!-- wp:llms/form-field-text %s /-->', wp_json_encode( $attrs ) );\n\t\t$blocks = parse_blocks( $html );\n\n\t\t$parsed = LLMS_Unit_Test_Util::call_method( $this->forms, 'block_to_field_settings', array( $blocks[0] ) );\n\t\t$expect = $attrs;\n\t\t$expect['required'] = false;\n\t\t$this->assertEquals( $expect, $parsed );\n\n\t}\n\n\t/**\n\t * Test cascade_visibility_attrs() for blocks with no innerBlocks.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cascade_visibility_attrs_no_inner_blocks() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:paragraph --><p>mock</p><!-- /wp:paragraph --><!-- wp:paragraph {\"llms_visibility\":\"logged_out\"} --><p>mock</p><!-- /wp:paragraph -->' );\n\n\t\t// No changes to make.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks ) );\n\t\t$this->assertEquals( $blocks, $res );\n\n\t\t// Add the visibility setting.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks, 'logged_in' ) );\n\n\t\t// Changed.\n\t\t$this->assertEquals( 'logged_in', $res[0]['attrs']['llms_visibility'] );\n\n\t\t// Unchanged.\n\t\t$this->assertEquals( 'logged_out', $res[1]['attrs']['llms_visibility'] );\n\n\t}\n\n\t/**\n\t * Test cascade_visibility_attrs() for blocks with innerBlocks.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_cascade_visibility_attrs_with_inner_blocks() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:columns {\"className\":\"has-2-columns\"} -->\n\t\t\t<div class=\"wp-block-columns has-2-columns\"><!-- wp:column -->\n\t\t\t<div class=\"wp-block-column\"><!-- wp:paragraph --><p>mock</p><!-- /wp:paragraph --></div>\n\t\t\t<!-- /wp:column -->\n\n\t\t\t<!-- wp:column -->\n\t\t\t<div class=\"wp-block-column\"><!-- wp:paragraph {\"llms_visibility\":\"logged_out\"} --><p>mock</p><!-- /wp:paragraph --></div>\n\t\t\t<!-- /wp:column --></div>\n\t\t\t<!-- /wp:columns -->' );\n\n\t\t// No changes to make.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks ) );\n\t\t$this->assertEquals( $blocks, $res );\n\n\t\t// Add the visibility setting.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->forms, 'cascade_visibility_attrs', array( $blocks, 'logged_in' ) );\n\n\t\t// Changed.\n\t\t$this->assertEquals( 'logged_in', $res[0]['attrs']['llms_visibility'] );\n\t\t$this->assertEquals( 'logged_in', $res[0]['innerBlocks'][0]['attrs']['llms_visibility'] );\n\t\t$this->assertEquals( 'logged_in', $res[0]['innerBlocks'][0]['innerBlocks'][0]['attrs']['llms_visibility'] );\n\n\t\t$this->assertEquals( 'logged_in', $res[0]['innerBlocks'][1]['attrs']['llms_visibility'] );\n\n\t\t// Already had visibility so this one doesn't change.\n\t\t$this->assertEquals( 'logged_out', $res[0]['innerBlocks'][1]['innerBlocks'][0]['attrs']['llms_visibility'] );\n\n\t}\n\n\t/**\n\t * Test creation for an invalid location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_invalid_location() {\n\n\t\t$this->assertFalse( $this->forms->create( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test convert_settings_to_block_attrs()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_convert_settings_to_block_attrs() {\n\n\t\t$in = array(\n\t\t\t'id' => 'mock',\n\t\t\t'type' => 'text',\n\t\t\t'classes' => 'test',\n\t\t\t'attributes' => array(),\n\t\t);\n\n\t\t$out = array(\n\t\t\t'id' => 'mock',\n\t\t\t'field' => 'text',\n\t\t\t'className' => 'test',\n\t\t\t'html_attrs' => array(),\n\t\t);\n\n\t\t$this->assertEquals( $out, $this->forms->convert_settings_to_block_attrs( $in ) );\n\n\t}\n\n\t/**\n\t * Test convert_settings_format() for a block -> field transformation\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_convert_settings_format_to_field() {\n\n\t\t$in = array(\n\t\t\t'id' => 'mock',\n\t\t\t'field' => 'text',\n\t\t\t'className' => 'test',\n\t\t\t'html_attrs' => array(),\n\t\t);\n\n\t\t$out = array(\n\t\t\t'id' => 'mock',\n\t\t\t'type' => 'text',\n\t\t\t'classes' => 'test',\n\t\t\t'attributes' => array(),\n\t\t);\n\n\t\t$this->assertEquals( $out, LLMS_Unit_Test_Util::call_method( $this->forms, 'convert_settings_format', array( $in, 'block' ) ) );\n\n\t}\n\n\t/**\n\t * Test creating/updating forms.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create() {\n\n\t\t$locs = $this->forms->get_locations();\n\t\t$created = array();\n\n\t\t// Create new forms.\n\t\tforeach ( $locs as $loc => $data ) {\n\t\t\t$id = $this->forms->create( $loc );\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t\t$post = get_post( $id );\n\t\t\t$this->assertEquals( 'llms_form', $post->post_type );\n\t\t\t$this->assertEquals( $loc, get_post_meta( $post->ID, '_llms_form_location', true ) );\n\n\t\t\tforeach ( $data['meta'] as $key => $val ) {\n\t\t\t\t$this->assertEquals( $val, get_post_meta( $post->ID, $key, true ) );\n\t\t\t}\n\n\t\t\t$created[ $loc ] = $id;\n\n\t\t}\n\n\t\t// Locs already exist.\n\t\tforeach ( array_keys( $locs ) as $loc ) {\n\t\t\t$this->assertFalse( $this->forms->create( $loc ) );\n\t\t}\n\n\t\t// Locs already exist and we want to update them.\n\t\tforeach ( array_keys( $locs ) as $loc ) {\n\t\t\t$this->assertEquals( $created[ $loc ], $this->forms->create( $loc, true ), $loc );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test forms author on install\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_forms_author_on_install() {\n\n\t\t// Clean user* tables.\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE $wpdb->users\" );\n\t\t$wpdb->query( \"TRUNCATE TABLE $wpdb->usermeta\" );\n\n\t\t// Create a subscriber.\n\t\t$subscriber = $this->factory->user->create( array( 'role' => 'subscriber' ) );\n\n\t\t$locs = $this->forms->get_locations();\n\n\t\t// Install forms\n\t\t$installed = $this->forms->install();\n\n\t\tforeach ( $installed as $loc => $id ) {\n\t\t\t// No admin users, expect 0.\n\t\t\t$this->assertEquals( 0, get_post( $id )->post_author, $id );\n\t\t}\n\n\t\t// Delete forms.\n\t\t$wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) );\n\n\t\t// Create two admins.\n\t\t$admins = $this->factory->user->create_many( 2, array( 'role' => 'administrator' ) );\n\n\t\t// Install forms.\n\t\t$installed = $this->forms->install();\n\n\t\tforeach ( $installed as $loc => $id ) {\n\t\t\t// Expect the first admin to be the forms author.\n\t\t\t$this->assertEquals( $admins[0], get_post( $id )->post_author, $id );\n\t\t}\n\n\t\t// Delete forms.\n\t\t$wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) );\n\n\t\t// Log in as subscriber.\n\t\twp_set_current_user( $subscriber );\n\n\t\t// Install forms.\n\t\t$installed = $this->forms->install();\n\n\t\tforeach ( $installed as $loc => $id ) {\n\t\t\t// Expect the first admin to be the forms author.\n\t\t\t$this->assertEquals( $admins[0], get_post( $id )->post_author, $id );\n\t\t}\n\n\t\t// Delete forms.\n\t\t$wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) );\n\n\t\t// Log in as first admin.\n\t\twp_set_current_user( $admins[0] );\n\n\t\t// Install forms.\n\t\t$installed = $this->forms->install();\n\n\t\tforeach ( $installed as $loc => $id ) {\n\t\t\t// Expect the first admin to be the forms author.\n\t\t\t$this->assertEquals( $admins[0], get_post( $id )->post_author, $id );\n\t\t}\n\n\t\t// Delete forms.\n\t\t$wpdb->delete( $wpdb->posts, array( 'post_type' => 'llms_form' ) );\n\n\t\t// Log in as second admin.\n\t\twp_set_current_user( $admins[1] );\n\n\t\t// Install forms.\n\t\t$installed = $this->forms->install();\n\n\t\tforeach ( $installed as $loc => $id ) {\n\t\t\t// Expect the first admin to be the forms author.\n\t\t\t$this->assertEquals( $admins[1], get_post( $id )->post_author, $id );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the get_capability() method\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_capability() {\n\t\t$this->assertEquals( 'manage_lifterlms', $this->forms->get_capability() );\n\t}\n\n\n\t/**\n\t * Test the get_fields_settings_from_blocks() method.\n\t *\n\t * @since 5.0.0\n\t * @since 6.2.0 Added checkboxes.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_fields_settings_from_blocks() {\n\n\t\t$form_id    = $this->forms->create( 'checkout', true );\n\t\t$checkboxes = $this->get_checkboxes_attributes( $form_id );\n\t\t$this->append_checkboxes_to_form( $form_id, $checkboxes );\n\n\t\t$blocks = $this->forms->get_form_blocks( 'checkout' );\n\n\t\t$fields = $this->forms->get_fields_settings_from_blocks( $blocks );\n\n\t\tforeach ( $fields as $field ) {\n \t\t\t$this->assertIsArray( $field );\n\t\t\t$this->assertTrue( ! empty( $field ) );\n\t\t}\n\n\t\t$expect = array(\n\t\t\t'email_address',\n\t\t\t'email_address_confirm',\n\t\t\t'password',\n\t\t\t'password_confirm',\n\t\t\t'llms-password-strength-meter',\n\t\t\t'first_name',\n\t\t\t'last_name',\n\t\t\t'llms_billing_address_1',\n\t\t\t'llms_billing_address_2',\n\t\t\t'llms_billing_city',\n\t\t\t'llms_billing_country',\n\t\t\t'llms_billing_state',\n\t\t\t'llms_billing_zip',\n\t\t\t'llms_phone',\n\t\t\t$checkboxes['id'],\n\t\t);\n\t\t$this->assertEquals( $expect, wp_list_pluck( $fields, 'name' ) );\n\n\t}\n\n\t/**\n\t * Test get_free_enroll_form_fields().\n\t *\n\t * @since 5.0.0\n\t * @since 6.2.0 Added checkboxes.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_free_enroll_form_fields() {\n\n\t\t$plan = $this->get_mock_plan();\n\n\t\t$form_id = $this->forms->create( 'checkout', true );\n\n\t\t// Add a checkboxes block to the form.\n\t\t$checkboxes = $this->get_checkboxes_attributes( $form_id );\n\t\t$this->append_checkboxes_to_form( $form_id, $checkboxes );\n\t\t$checkboxes_id    = $checkboxes['id'];\n\t\t$checkboxes_key_2 = $checkboxes['options'][2]['key'];\n\n\t\t// The user has checked the 2nd checkbox.\n\t\t$user_id = $this->factory->user->create();\n\t\twp_set_current_user( $user_id );\n\t\tadd_user_meta( $user_id, $checkboxes_id, array( $checkboxes_key_2 ) );\n\n\t\t// Expected field IDs and names.\n\t\t$expected_fields = array(\n\t\t\tarray( 'id' => 'first_name', 'name' => 'first_name' ),\n\t\t\tarray( 'id' => 'last_name', 'name' => 'last_name' ),\n\t\t\tarray( 'id' => 'llms_billing_address_1', 'name' => 'llms_billing_address_1' ),\n\t\t\tarray( 'id' => 'llms_billing_address_2', 'name' => 'llms_billing_address_2' ),\n\t\t\tarray( 'id' => 'llms_billing_city', 'name' => 'llms_billing_city' ),\n\t\t\tarray( 'id' => 'llms_billing_country', 'name' => 'llms_billing_country' ),\n\t\t\tarray( 'id' => 'llms_billing_state', 'name' => 'llms_billing_state' ),\n\t\t\tarray( 'id' => 'llms_billing_zip', 'name' => 'llms_billing_zip' ),\n\t\t\tarray( 'id' => 'llms_phone', 'name' => 'llms_phone' ),\n\t\t\tarray( 'id' => \"{$checkboxes_id}--{$checkboxes_key_2}\", 'name' => \"{$checkboxes_id}[]\" ),\n\t\t\tarray( 'id' => null, 'name' => 'free_checkout_redirect' ),\n\t\t\tarray( 'id' => 'llms-plan-id', 'name' => 'llms_plan_id' ),\n\t\t);\n\n\t\t$fields = $this->forms->get_free_enroll_form_fields( $plan );\n\t\t$this->assertCount( count( $expected_fields ), $fields );\n\n\t\tforeach ( $fields as $index => $field ) {\n\t\t\t$actual = array( 'id' => $field['id'] ?? null, 'name' => $field['name'] );\n\t\t\t$this->assertEquals( $expected_fields[ $index ], $actual );\n\t\t}\n\n\t\t// Only hidden fields.\n\t\t$this->assertEquals( array( 'hidden' ), array_unique( wp_list_pluck( $fields, 'type' ) ) );\n\n\t}\n\n\t/**\n\t * Can't retrieve blocks for an invalid location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_blocks_invalid_location() {\n\n\t\t$this->assertFalse( $this->forms->get_form_blocks( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Can't retrieve blocks for a location that hasn't been installed yet.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_blocks_not_installed() {\n\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$this->assertFalse( $this->forms->get_form_blocks( $loc ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_form_blocks() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_blocks() {\n\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\n\t\t\t$this->forms->create( $loc );\n\t\t\t$blocks = $this->forms->get_form_blocks( $loc );\n\n\t\t\tforeach ( $blocks as $block ) {\n\t\t\t\t$this->assertIsABlock( $block );\n\t\t\t}\n\n\t\t}\n\n\n\t}\n\n\t/**\n\t * Can't retrieve fields for an invalid location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_fields_invalid_loc() {\n\t\t$this->assertFalse( $this->forms->get_form_fields( 'fake' ) );\n\t}\n\n\t/**\n\t * Can't retrieve fields for a location that hasn't been installed yet.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_fields_not_installed() {\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$this->assertFalse( $this->forms->get_form_fields( $loc ) );\n\t\t}\n\t}\n\n\t/**\n\t * Test get_form_fields() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_fields() {\n\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$this->forms->create( $loc );\n\t\t\t$fields = $this->forms->get_form_fields( $loc );\n\n\t\t\tforeach ( $fields as $field ) {\n\t\t\t\t$this->assertIsAField( $field );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Can't get form html for an invalid form.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_html_invalid() {\n\n\t\t$this->assertEquals( '', $this->forms->get_form_html( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Can't get form html for a form that hasn't been installed.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_html_not_installed() {\n\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$this->assertEquals( '', $this->forms->get_form_html( $loc ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_form_html() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @todo  this test can assert a lot more and should.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_html() {\n\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$this->forms->create( $loc );\n\t\t\t$html = $this->forms->get_form_html( $loc );\n\n\t\t\t$this->assertStringContains( '<div class=\"llms-form-field type-email', $html );\n\t\t}\n\n\t}\n\n\t/**\n\t * Can't retrieve a post for an invalid location.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_post_invalid() {\n\n\t\t$this->assertFalse( $this->forms->get_form_post( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test get_form_post() for forms when they're not installed.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_post_not_installed() {\n\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$this->assertFalse( $this->forms->get_form_post( $loc ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_form_post()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_form_post() {\n\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$id = $this->forms->create( $loc );\n\t\t\t$this->assertEquals( get_post( $id ), $this->forms->get_form_post( $loc ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_locations() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @see {Reference}\n\t * @link {URL}\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_locations() {\n\n\t\t$locs = $this->forms->get_locations();\n\t\tforeach ( $this->get_form_locs() as $loc ) {\n\t\t\t$this->assertArrayHasKey( $loc, $locs );\n\t\t\t$this->assertArrayHasKey( 'name', $locs[ $loc ] );\n\t\t\t$this->assertArrayHasKey( 'description', $locs[ $loc ] );\n\t\t\t$this->assertArrayHasKey( 'title', $locs[ $loc ] );\n\t\t\t$this->assertArrayHasKey( 'meta', $locs[ $loc ] );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the get_post_type() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_post_type() {\n\t\t$this->assertEquals( 'llms_form', $this->forms->get_post_type() );\n\t}\n\n\t/**\n\t * test the install() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_install() {\n\n\t\t$installed = $this->forms->install();\n\t\t$this->assertEquals( 3, count( $installed ) );\n\n\t\tforeach( $installed as $id ) {\n\t\t\t$post = get_post( $id );\n\t\t\t$this->assertTrue( is_a( $post, 'WP_Post' ) );\n\t\t\t$this->assertEquals( 'llms_form', $post->post_type );\n\t\t}\n\n\t\t// Already installed.\n\t\t$installed = $this->forms->install();\n\t\tforeach ( $installed as $id ) {\n\t\t\t$this->assertFalse( $id );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test is_block_visible() when no visibility settings exist on the block.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_block_visible_no_visibility() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:paragraph --><p>Fake paragraph content</p><!-- /wp:paragraph -->' );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->forms, 'is_block_visible', array( $blocks[0] ) ) );\n\n\t}\n\n\t/**\n\t * Test is_block_visible() when there are visibility settings which would affect the visibility of the block.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_block_visible_with_visibility() {\n\n\t\t// Logged out users only.\n\t\t$blocks = parse_blocks( '<!-- wp:paragraph {\"llms_visibility\":\"logged_out\"} --><p>Fake paragraph content</p><!-- /wp:paragraph -->' );\n\n\t\t// No user, show the block.\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->forms, 'is_block_visible', array( $blocks[0] ) ) );\n\n\t\t// Has a user, don't show.\n\t\twp_set_current_user( $this->factory->student->create() );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->forms, 'is_block_visible', array( $blocks[0] ) ) );\n\n\t}\n\n\t/**\n\t * Test is_block_visible_in_list()\n\t *\n\t * This additionally covers conditions in get_block_path().\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_block_visible_in_list() {\n\n\t\t$hidden_json = '{\"llms_visibility\":\"logged_in\",\"llms_visibility_in\":\"any_course\"}';\n\n\t\t$visible = '<!-- wp:paragraph -->\\n<p>Test</p>\\n<!-- /wp:paragraph -->';\n\t\t$hidden  = sprintf( '<!-- wp:paragraph %s -->\\n<p>Test</p>\\n<!-- /wp:paragraph -->', $hidden_json );\n\n\t\t/**\n\t\t * List of tests to run\n\t\t *\n\t\t * @param array[] {\n\t\t *     @type string $0 Test description / message. Passed to the assertion for debugging failed tests.\n\t\t *     @type string $1 Block markup for the block being tested.\n\t\t *     @type string $2 List of blocks for use as second parameter. The HTML from $1 must be found in this list!\n\t\t *     @type bool   $3 The expected result of `is_block_visible_in_list()`.\n\t\t * }\n\t\t */\n\t\t$tests = array(\n\n\t\t\tarray(\n\t\t\t\t'Block not found in the list',\n\t\t\t\t$visible,\n\t\t\t\t$hidden,\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Empty list falls back to `is_block_visible()`: is visible',\n\t\t\t\t$visible,\n\t\t\t\t'',\n\t\t\t\ttrue,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Empty list falls back to `is_block_visible()`: not visible',\n\t\t\t\t$hidden,\n\t\t\t\t'',\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Flat list: is visible',\n\t\t\t\t$visible,\n\t\t\t\t$hidden . $visible,\n\t\t\t\ttrue,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Flat list: not visible',\n\t\t\t\t$hidden,\n\t\t\t\t$visible . $hidden,\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Visible in a group',\n\t\t\t\t$visible,\n\t\t\t\tsprintf( '<!-- wp:group -->\\n<div class=\"wp-block-group\">%s</div>\\n<!-- /wp:group -->', $visible ),\n\t\t\t\ttrue,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Hidden in a group',\n\t\t\t\t$hidden,\n\t\t\t\tsprintf( '<!-- wp:group -->\\n<div class=\"wp-block-group\">%s</div>\\n<!-- /wp:group -->', $hidden ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Visible in a hidden group',\n\t\t\t\t$visible,\n\t\t\t\tsprintf( '<!-- wp:group %1$s -->\\n<div class=\"wp-block-group\">%2$s</div>\\n<!-- /wp:group -->', $hidden_json, $visible ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Hidden in a hidden group',\n\t\t\t\t$hidden,\n\t\t\t\tsprintf( '<!-- wp:group %1$s -->\\n<div class=\"wp-block-group\">%2$s</div>\\n<!-- /wp:group -->', $hidden_json, $hidden ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: visible -> visible -> visible',\n\t\t\t\t$visible,\n\t\t\t\tsprintf( '<!-- wp:columns -->\\n<div class=\"wp-block-columns\"><!-- wp:column -->\\n<div class=\"wp-block-column\">%s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $visible ),\n\t\t\t\ttrue,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: hidden -> hidden -> hidden',\n\t\t\t\t$hidden,\n\t\t\t\tsprintf( '<!-- wp:columns %2$s -->\\n<div class=\"wp-block-columns\"><!-- wp:column %2$s -->\\n<div class=\"wp-block-column\">%1$s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $hidden, $hidden_json ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: visible -> visible -> hidden',\n\t\t\t\t$hidden,\n\t\t\t\tsprintf( '<!-- wp:columns -->\\n<div class=\"wp-block-columns\"><!-- wp:column -->\\n<div class=\"wp-block-column\">%s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $hidden ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: visible -> hidden -> hidden',\n\t\t\t\t$hidden,\n\t\t\t\tsprintf( '<!-- wp:columns -->\\n<div class=\"wp-block-columns\"><!-- wp:column %2$s -->\\n<div class=\"wp-block-column\">%1$s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $hidden, $hidden_json ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: hidden -> hidden -> visible',\n\t\t\t\t$visible,\n\t\t\t\tsprintf( '<!-- wp:columns %2$s -->\\n<div class=\"wp-block-columns\"><!-- wp:column %2$s -->\\n<div class=\"wp-block-column\">%1$s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $visible, $hidden_json ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: hidden -> visible -> visible',\n\t\t\t\t$visible,\n\t\t\t\tsprintf( '<!-- wp:columns %2$s -->\\n<div class=\"wp-block-columns\"><!-- wp:column -->\\n<div class=\"wp-block-column\">%1$s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $visible, $hidden_json ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: hidden -> visible -> hidden',\n\t\t\t\t$hidden,\n\t\t\t\tsprintf( '<!-- wp:columns %2$s -->\\n<div class=\"wp-block-columns\"><!-- wp:column -->\\n<div class=\"wp-block-column\">%1$s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $hidden, $hidden_json ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Multiple parents: visible -> hidden -> visible',\n\t\t\t\t$visible,\n\t\t\t\tsprintf( '<!-- wp:columns -->\\n<div class=\"wp-block-columns\"><!-- wp:column %2$s -->\\n<div class=\"wp-block-column\">%1$s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $visible, $hidden_json ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Break Stuff',\n\t\t\t\t$visible,\n\t\t\t\tsprintf( '<!-- wp:columns -->\\n<div class=\"wp-block-columns\"><!-- wp:column %2$s -->\\n<div class=\"wp-block-column\">%1$s</div>\\n<!-- /wp:column --></div>\\n<!-- /wp:columns -->', $visible, $hidden_json ),\n\t\t\t\tfalse,\n\t\t\t),\n\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\n\t\t\t$msg    = $data[0];\n\t\t\t$block  = parse_blocks( $data[1] )[0];\n\t\t\t$list   = parse_blocks( $data[2] );\n\t\t\t$expect = $data[3];\n\n\t\t\t$this->assertEquals( $expect, $this->forms->is_block_visible_in_list( $block, $list ), $msg );\n\n\t\t}\n\n\n\t}\n\n\t/**\n\t * Test get_block_tree()\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_block_tree() {\n\n\t\t$test_block_json  = '<!-- wp:paragraph -->\\n<p>Test</p>\\n<!-- /wp:paragraph -->';\n\t\t$group_block_json = '<!-- wp:group -->\\n<div class=\"wp-block-group\">%1$s</div>\\n<!-- /wp:group -->';\n\n\t\t$test_block_as_parent_json  = sprintf( $group_block_json, $test_block_json );\n\n\t\t/**\n\t\t * List of tests to run\n\t\t *\n\t\t * @param array[] {\n\t\t *     @type string $0 Test description / message. Passed to the assertion for debugging failed tests.\n\t\t *     @type string $1 Block markup for the block being tested.\n\t\t *     @type string $2 List of blocks for use as second parameter. The HTML from $1 must be found in this list!\n\t\t *     @type bool   $3 The expected result of `get_block_tree()`.\n\t\t * }\n\t\t */\n\t\t$tests = array(\n\n\t\t\tarray(\n\t\t\t\t'Block in a tree with two levels, with the leaf\\'s parent branch having one sibling',\n\t\t\t\t$test_block_json,\n\t\t\t\tsprintf(\n\t\t\t\t\t$group_block_json,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t$test_block_json\n\t\t\t\t\t) .\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t'Suppressed'\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t$group_block_json,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t$test_block_json\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Block in a tree with two levels, with the leaf\\'s gran parent\\'s branch having one sibling',\n\t\t\t\t$test_block_json,\n\t\t\t\tsprintf(\n\t\t\t\t\t$group_block_json,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t'Suppressed'\n\t\t\t\t\t) .\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\tsprintf(\n\t\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t\t$test_block_json\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t$group_block_json,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\tsprintf(\n\t\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t\t$test_block_json\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'No block found',\n\t\t\t\t$test_block_json,\n\t\t\t\tsprintf(\n\t\t\t\t\t$group_block_json,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t'Something'\n\t\t\t\t\t) .\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\tsprintf(\n\t\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t\t'Something Else'\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t''\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Block as first of the list',\n\t\t\t\t$test_block_json,\n\t\t\t\t$test_block_json,\n\t\t\t\t$test_block_json\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'Block\\'s children preserved',\n\t\t\t\t$test_block_as_parent_json,\n\t\t\t\tsprintf(\n\t\t\t\t\t$group_block_json,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t$test_block_as_parent_json\n\t\t\t\t\t) .\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t'Suppressed'\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t$group_block_json,\n\t\t\t\t\tsprintf(\n\t\t\t\t\t\t$group_block_json,\n\t\t\t\t\t\t$test_block_as_parent_json\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t),\n\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\n\t\t\t$msg    = $data[0];\n\t\t\t$block  = parse_blocks( $data[1] )[0];\n\t\t\t$list   = parse_blocks( $data[2] );\n\t\t\t$expect = parse_blocks( $data[3] );\n\n\t\t\t$this->assertEquals(\n\t\t\t\t$expect,\n\t\t\t\tLLMS_Unit_Test_Util::call_method( $this->forms, 'get_block_tree', array( $block, $list ), $msg )\n\t\t\t);\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test is_location_valid()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_location_valid() {\n\n\t\tforeach ( array_keys( $this->forms->get_locations() ) as $loc ) {\n\t\t\t$this->assertTrue( $this->forms->is_location_valid( $loc ) );\n\t\t}\n\n\t\t$this->assertFalse( $this->forms->is_location_valid( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test load_reusable_blocks() default successful behavior.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_load_reusable_blocks() {\n\n\t\t$blocks = array(\n\t\t\tLLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_reusable_block', array( 'email' ) ),\n\t\t\tLLMS_Unit_Test_Util::call_method( 'LLMS_Form_Templates', 'get_reusable_block', array( 'address' ) ),\n\t\t);\n\n\t\t$load = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( $blocks ) );\n\n\t\t// Make sure the loaded blocks match the following snapshot when serialized.\n\t\t$expected = '<!-- wp:llms/form-field-confirm-group {\"fieldLayout\":\"columns\",\"llms_visibility\":\"logged_out\"} --><!-- wp:llms/form-field-user-email {\"required\":true,\"id\":\"email_address\",\"llms_visibility\":\"logged_out\",\"name\":\"email_address\",\"label\":\"Email Address\",\"data_store\":\"users\",\"data_store_key\":\"user_email\",\"field\":\"email\",\"columns\":6,\"last_column\":false,\"isConfirmationControlField\":true,\"match\":\"email_address_confirm\"} /--><!-- wp:llms/form-field-text {\"required\":true,\"id\":\"email_address_confirm\",\"llms_visibility\":\"logged_out\",\"name\":\"email_address_confirm\",\"label\":\"Confirm Email Address\",\"data_store\":false,\"data_store_key\":false,\"field\":\"email\",\"columns\":6,\"last_column\":true,\"isConfirmationField\":true,\"match\":\"email_address\"} /--><!-- /wp:llms/form-field-confirm-group --><!-- wp:llms/form-field-user-address --><!-- wp:llms/form-field-user-address-street --><!-- wp:llms/form-field-user-address-street-primary {\"id\":\"llms_billing_address_1\",\"required\":true,\"columns\":8,\"last_column\":false,\"name\":\"llms_billing_address_1\",\"label\":\"Address\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_address_1\",\"field\":\"text\"} /--><!-- wp:llms/form-field-user-address-street-secondary {\"id\":\"llms_billing_address_2\",\"required\":false,\"columns\":4,\"last_column\":true,\"name\":\"llms_billing_address_2\",\"label\":\"\",\"label_show_empty\":true,\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_address_2\",\"placeholder\":\"Apartment, suite, etc...\",\"field\":\"text\"} /--><!-- /wp:llms/form-field-user-address-street --><!-- wp:llms/form-field-user-address-city {\"id\":\"llms_billing_city\",\"required\":true,\"name\":\"llms_billing_city\",\"label\":\"City\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_city\",\"field\":\"text\"} /--><!-- wp:llms/form-field-user-address-country {\"id\":\"llms_billing_country\",\"required\":true,\"name\":\"llms_billing_country\",\"label\":\"Country\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_country\",\"options_preset\":\"countries\",\"placeholder\":\"Select a Country\",\"field\":\"select\",\"className\":\"llms-select2\"} /--><!-- wp:llms/form-field-user-address-region --><!-- wp:llms/form-field-user-address-state {\"id\":\"llms_billing_state\",\"required\":true,\"columns\":6,\"last_column\":false,\"name\":\"llms_billing_state\",\"label\":\"State \\/ Region\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_state\",\"options_preset\":\"states\",\"placeholder\":\"Select a State \\/ Region\",\"field\":\"select\",\"className\":\"llms-select2\"} /--><!-- wp:llms/form-field-user-address-postal-code {\"id\":\"llms_billing_zip\",\"required\":true,\"columns\":6,\"last_column\":true,\"name\":\"llms_billing_zip\",\"label\":\"Postal \\/ Zip Code\",\"data_store\":\"usermeta\",\"data_store_key\":\"llms_billing_zip\",\"field\":\"text\"} /--><!-- /wp:llms/form-field-user-address-region --><!-- /wp:llms/form-field-user-address -->';\n\t\t$this->assertEquals( parse_blocks( $expected ), parse_blocks( serialize_blocks( $load ) ) );\n\n\t}\n\n\t/**\n\t * Test load_reusable_blocks(): a non-existent block is passed in\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_load_reusable_blocks_fake() {\n\n\t\t$blocks = array(\n\t\t\tarray(\n\t\t\t\t'blockName'    => 'core/block',\n\t\t\t\t'attrs'        => array( 'ref' => $this->factory->post->create() + 1 ),\n\t\t\t\t'innerContent' => array(),\n\t\t\t),\n\t\t);\n\n\t\t$load = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( $blocks ) );\n\t\t$this->assertEquals( array(), $load );\n\n\t}\n\n\t/**\n\t * Test load_reusable_blocks(): when the reusable block is not published.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_load_reusable_blocks_draft() {\n\n\t\t$blocks = array(\n\t\t\tarray(\n\t\t\t\t'blockName'    => 'core/block',\n\t\t\t\t'attrs'        => array( 'ref' => $this->factory->post->create( array( 'post_status' => 'draft' ) ) ),\n\t\t\t\t'innerContent' => array(),\n\t\t\t),\n\t\t);\n\n\t\t$load = LLMS_Unit_Test_Util::call_method( $this->forms, 'load_reusable_blocks', array( $blocks ) );\n\t\t$this->assertEquals( array(), $load );\n\n\t}\n\n\t/**\n\t * Test maybe_load_preview() when no post is found\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_load_preview_no_post() {\n\t\t$this->assertFalse( $this->forms->maybe_load_preview( false ) );\n\t}\n\n\t/**\n\t * Test maybe_load_preview() when not previewing\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_load_preview_not_preview() {\n\t\t$post = $this->factory->post->create_and_get();\n\t\t$this->assertEquals( $post, $this->forms->maybe_load_preview( $post ) );\n\t}\n\n\t/**\n\t * Test maybe_load_preview() when current user can't preview\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_load_preview_user_cant_preview() {\n\t\tglobal $wp_query;\n\t\t$post = $this->factory->post->create_and_get();\n\t\t$save = (array) $post;\n\t\t$save['post_ID'] = $save['ID'];\n\t\t$save['post_content'] = 'autosave content';\n\t\twp_create_post_autosave( $save );\n\t\t$wp_query->is_preview();\n\t\t$this->assertEquals( $post, $this->forms->maybe_load_preview( $post ) );\n\t}\n\n\t/**\n\t * Test maybe_load_preview() when there is a preview\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_load_preview_user_can_preview() {\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\tglobal $wp_query;\n\t\t$post = $this->factory->post->create_and_get();\n\t\t$save = (array) $post;\n\t\t$save['post_ID'] = $save['ID'];\n\t\t$save['post_content'] = 'autosave content';\n\t\twp_create_post_autosave( $save );\n\t\t$wp_query->is_preview();\n\t\t$this->assertEquals( $post, $this->forms->maybe_load_preview( $post ) );\n\t}\n\n\t/**\n\t * Test block field render function for non-field blocks.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_render_field_block_non_field_block() {\n\n\t\t$html = '<p>Fake paragraph content</p>';\n\t\t$blocks = parse_blocks( '<!-- wp:paragraph -->' . $html . '<!-- /wp:paragraph -->' );\n\t\t$this->assertEquals( $html, $this->forms->render_field_block( $html, $blocks[0] ) );\n\n\t}\n\n\t/**\n\t * Test rendering a field block as a field.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_render_field_block() {\n\n\t\t$atts = array(\n\t\t\t'id' => 'field_id',\n\t\t);\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-text {\"id\":\"field_id\"} /-->' );\n\n\t\t$this->assertEquals( llms_form_field( $atts, false ), $this->forms->render_field_block( '', $blocks[0] ) );\n\n\t}\n\n\t/**\n\t * Test rendering a field block which contains fields in the inner blocks\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_render_field_block_with_inner() {\n\n\t\t$blocks = parse_blocks( '<!-- wp:llms/form-field-confirm-group -->\n<!-- wp:llms/form-field-user-email {\"id\":\"one\"} /-->\n\n<!-- wp:llms/form-field-text {\"id\":\"two\"} /-->\n<!-- /wp:llms/form-field-confirm-group -->' );\n\n\t\tob_start();\n\t\tllms_form_field( array( 'id' => 'one' ) );\n\t\techo \"\\n\";\n\t\tllms_form_field( array( 'id' => 'two' ) );\n\t\t$expected = ob_get_clean();\n\n\t\t$this->assertEquals( $expected, $this->forms->render_field_block( '', $blocks[0] ) );\n\n\t}\n\n\t/**\n\t * Test is_a_core_form() method passing something which is not a form.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_a_core_form_with_no_form() {\n\n\t\t$fake_form = new stdClass();\n\t\t$this->assertFalse( $this->forms->is_a_core_form( $fake_form ) );\n\n\t\t$fake_form_id = 939393;\n\t\t$this->assertFalse( $this->forms->is_a_core_form( $fake_form_id ) );\n\n\t\t$fake_form_id = $this->factory->post->create();\n\t\t$this->assertFalse( $this->forms->is_a_core_form( $fake_form_id ) );\n\n\t}\n\n\t/**\n\t * Test is_a_core_form() method passing something which is a core form.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_a_core_form() {\n\n\t\t$locations = $this->forms->get_locations();\n\n\t\t$created_ids = array();\n\n\t\t// Create new forms.\n\t\tforeach ( $locations as $location_id => $data ) {\n\t\t\t$created_ids[] = $this->forms->create( $location_id );\n\t\t}\n\t\tforeach ( $created_ids as $created_id ) {\n\t\t\t$this->assertTrue( $this->forms->is_a_core_form( $created_id ) );\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test is_a_core_form() method passing something which is a form but NOT a core form.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_a_core_form_not_core_form() {\n\n\t\t$locations = $this->forms->get_locations();\n\n\t\t$core      = array();\n\t\t$core_dups = array();\n\t\t$new       = array();\n\n\t\t// Create new forms.\n\t\tforeach ( $locations as $location_id => $data ) {\n\t\t\t$core[] = $this->forms->create( $location_id );\n\t\t\t// Duplicate core forms.\n\t\t\t$core_dups[] = $this->factory->post->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => 'llms_form',\n\t\t\t\t)\n\t\t\t);\n\t\t\tupdate_post_meta( end( $core_dups ), '_llms_form_location', $location_id );\n\t\t\tupdate_post_meta( end( $core_dups ), '_llms_form_is_core', 'yes' );\n\t\t}\n\t\t// Create 3 additional billing forms.\n\t\tfor ( $i = 0; $i < 3; $i++ ) {\n\t\t\t$core_dups[] = $this->factory->post->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => 'llms_form',\n\t\t\t\t)\n\t\t\t);\n\t\t\tupdate_post_meta( end( $core_dups ), '_llms_form_location', 'checkout' );\n\t\t}\n\n\t\tforeach ( $core as $c ) {\n\t\t\t$this->assertTrue( $this->forms->is_a_core_form( $c ), $c );\n\t\t}\n\t\tforeach ( $core_dups as $cd ) {\n\t\t\t$this->assertFalse( $this->forms->is_a_core_form( $cd ), $cd );\n\t\t}\n\t\tforeach ( $new as $n ) {\n\t\t\t$this->assertFalse( $this->forms->is_a_core_form( $n ), $n );\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-access-plans.php",
    "content": "<?php\n/**\n * Test Order Functions\n *\n * @package LifterLMS/Tests\n *\n * @group LLMS_Access_Plan\n *\n * @since 3.29.0\n * @since 3.32.0 Add delta to date assertions for `test_llms_insert_access_plan_update()`.\n * @since 3.34.0 Add gmt date to list of date fields that should be assersted with a delta.\n */\nclass LLMS_Test_Functions_Access_Plans extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test the llms_get_access_plan_period_options() method\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_access_plan_period_options() {\n\n\t\t$options = llms_get_access_plan_period_options();\n\t\t$this->assertEquals(  array( 'year', 'month', 'week', 'day' ), array_keys( $options ) );\n\n\t}\n\n\t/**\n\t * Test the llms_get_access_plan_visibility_options() method\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_access_plan_visibility_options() {\n\n\t\t$options = llms_get_access_plan_visibility_options();\n\t\t$this->assertEquals( array( 'visible', 'hidden', 'featured' ), array_keys( $options ) );\n\n\t}\n\n\t/**\n\t * Test default props for llms_insert_access_plan() function.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_default() {\n\n\t\t$props = array();\n\t\t$props['product_id'] = $this->factory->course->create( array( 'sections' => 0 ) );\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Default properties.\n\t\t$this->assertEquals( 0, $plan->get( 'price' ) );\n\t\t$this->assertEquals( 'yes', $plan->get( 'is_free' ) );\n\t\t$this->assertEquals( 'no', $plan->get( 'on_sale' ) );\n\t\t$this->assertEquals( 0, $plan->get( 'frequency' ) );\n\t\t$this->assertEquals( 'Access Plan', $plan->get( 'title' ) );\n\n\t\t// No possible trial.\n\t\t$this->assertEquals( 'no', $plan->get( 'trial_offer' ) );\n\t\t$this->assertEmpty( $plan->get( 'trial_price' ) );\n\t\t$this->assertEmpty( $plan->get( 'trial_length' ) );\n\t\t$this->assertEmpty( $plan->get( 'trial_period' ) );\n\n\t\t// Expiration.\n\t\t$this->assertEquals( 'lifetime', $plan->get( 'access_expiration' ) );\n\n\t}\n\n\t/**\n\t * Test the default parameters that will be automatically \"fixed\" or overridden for the llms_insert_access_plan() function.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_free_default_overrides() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price' => 0,\n\t\t\t'is_free' => 'no',\n\t\t\t'frequency' => 0,\n\t\t\t'on_sale' => 'yes',\n\t\t\t'trial_offer' => 'yes',\n\t\t);\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t$this->assertEquals( 0, $plan->get( 'price' ) );\n\t\t$this->assertEquals( 'yes', $plan->get( 'is_free' ) );\n\t\t$this->assertEquals( 'no', $plan->get( 'on_sale' ) );\n\t\t$this->assertEquals( 'no', $plan->get( 'trial_offer' ) );\n\t\t$this->assertEquals( 0, $plan->get( 'frequency' ) );\n\n\t}\n\n\t/**\n\t * Test recurring payment props for llms_insert_access_plan() function.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_payment_recurring() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price' => 5,\n\t\t\t'frequency' => 1,\n\t\t\t'length' => 1,\n\t\t\t'period' => 'week',\n\t\t);\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Props.\n\t\t$this->assertEquals( 5, $plan->get( 'price' ) );\n\t\t$this->assertEquals( 1, $plan->get( 'frequency' ) );\n\t\t$this->assertEquals( 1, $plan->get( 'length' ) );\n\t\t$this->assertEquals( 'week', $plan->get( 'period' ) );\n\n\t}\n\n\t/**\n\t * Test one-time payment props for llms_insert_access_plan() function.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_payment_single() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price' => 5,\n\t\t\t'frequency' => 0,\n\t\t\t'length' => 1,\n\t\t\t'period' => 'week',\n\t\t);\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Props.\n\t\t$this->assertEquals( 5, $plan->get( 'price' ) );\n\t\t$this->assertEquals( 0, $plan->get( 'frequency' ) );\n\t\t$this->assertEmpty( $plan->get( 'length' ) );\n\t\t$this->assertEmpty( $plan->get( 'period' ) );\n\n\t}\n\n\t/**\n\t * Test sale-related props on the llms_insert_access_plan() function\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_props_sale() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'on_sale' => 'yes',\n\t\t\t'price' => 50,\n\t\t);\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Default sale.\n\t\t$this->assertEquals( 'yes', $plan->get( 'on_sale' ) );\n\t\t$this->assertEquals( 0, $plan->get( 'sale_price' ) );\n\n\t\t// Other props.\n\t\t$props['sale_price'] = 25;\n\t\t$props['on_sale'] = 'yes';\n\t\t$props['sale_end'] = '2019-05-05';\n\t\t$props['sale_start'] = '2019-05-05';\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Test props.\n\t\t$this->assertEquals( $props['sale_price'], $plan->get( 'sale_price' ) );\n\t\t$this->assertEquals( $props['on_sale'], $plan->get( 'on_sale' ) );\n\t\t$this->assertEquals( $props['sale_end'], $plan->get( 'sale_end' ) );\n\t\t$this->assertEquals( $props['sale_start'], $plan->get( 'sale_start' ) );\n\n\t}\n\n\t/**\n\t * Test expiration-related props on the llms_insert_access_plan() function\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_props_expiration() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'access_expiration' => 'lifetime',\n\t\t);\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Props.\n\t\t$this->assertEquals( 'lifetime', $plan->get( 'access_expiration' ) );\n\t\t$this->assertEmpty( $plan->get( 'access_expires' ) );\n\t\t$this->assertEmpty( $plan->get( 'access_length' ) );\n\t\t$this->assertEmpty( $plan->get( 'access_period' ) );\n\n\t\t// Limited Date.\n\t\t$props['access_expiration'] = 'limited-date';\n\t\t$props['access_expires'] = '2019-02-14'; // naw.... so much <3.\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Props.\n\t\t$this->assertEquals( 'limited-date', $plan->get( 'access_expiration' ) );\n\t\t$this->assertEquals( $props['access_expires'], $plan->get( 'access_expires' ) );\n\t\t$this->assertEmpty( $plan->get( 'access_length' ) );\n\t\t$this->assertEmpty( $plan->get( 'access_period' ) );\n\n\t\t// Limited Period.\n\t\t$props['access_expiration'] = 'limited-period';\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// Props.\n\t\t$this->assertEquals( 'limited-period', $plan->get( 'access_expiration' ) );\n\t\t$this->assertEquals( 1, $plan->get( 'access_length' ) );\n\t\t$this->assertEquals( 'year', $plan->get( 'access_period' ) );\n\t\t$this->assertEmpty( $plan->get( 'access_expires' ) );\n\n\t}\n\n\t/**\n\t * Test trial-related props on the llms_insert_access_plan() function\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_props_trial() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'trial_offer' => 'yes',\n\t\t\t'trial_length' => 1,\n\t\t\t'trial_period' => 'year',\n\t\t);\n\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// No trial on a free plan.\n\t\t$this->assertEquals( 'no', $plan->get( 'trial_offer' ) ) ;\n\n\t\t$props['price'] = 1;\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t// No trial for one-time payments.\n\t\t$this->assertEquals( 'no', $plan->get( 'trial_offer' ) ) ;\n\n\t\t$props['frequency'] = 1;\n\t\t$plan = llms_insert_access_plan( $props );\n\t\t// Creation success.\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t$this->assertEquals( 'yes', $plan->get( 'trial_offer' ) ) ;\n\t\t$this->assertEquals( 0, $plan->get( 'trial_price' ) );\n\t\t$this->assertEquals( 1, $plan->get( 'trial_length' ) );\n\t\t$this->assertEquals( 'year', $plan->get( 'trial_period' ) );\n\n\t}\n\n\n\t/**\n\t * Test updating existing llms_insert_access_plan() function.\n\t *\n\t * @since 3.29.0\n\t * @since 3.32.0 Add delta to date assertions.\n\t * @since 3.34.0 Add gmt date to list of date fields that should be assersted with a delta.\n\t * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of 4th parameter to `assertEquals()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_update() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price' => 1,\n\t\t);\n\n\t\t// Create.\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_access_plan_after_create' ) );\n\t\t$this->assertEquals( 1, $plan->get( 'price' ) );\n\n\t\t// Update with a a fake ID.\n\t\t$props['id'] = 'fake';\n\t\t$this->assertIsWPError( llms_insert_access_plan( $props ) );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-plan', llms_insert_access_plan( $props ) );\n\n\t\t// Update with a valid post ID (but not the access plan post type).\n\t\t$props['id'] = $this->factory->post->create();\n\t\t$this->assertIsWPError( llms_insert_access_plan( $props ) );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-plan', llms_insert_access_plan( $props ) );\n\n\t\t// plan before props.\n\t\t$plan_before = $plan->toArray();\n\n\t\t// Real plan.\n\t\t$props['id'] = $plan->get( 'id' );\n\t\t$props['price'] = 2;\n\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_access_plan_after_update' ) );\n\t\t$this->assertEquals( 2, $plan->get( 'price' ) );\n\n\t\t// Price is the only property that should have changed.\n\t\tforeach ( $plan->toArray() as $key => $val ) {\n\t\t\tif ( 'price' === $key ) {\n\t\t\t\t$this->assertFalse( $plan_before[ $key ] === $val );\n\t\t\t} elseif ( in_array( $key, array( 'date', 'date_gmt', 'modified', 'modified_gmt' ), true ) ) {\n\t\t\t\t$this->assertEqualsWithDelta( strtotime( $plan_before[ $key ] ), strtotime( $val ), 5, $key );\n\t\t\t} else {\n\t\t\t\t$this->assertEquals( $plan_before[ $key ], $val, $key );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test period field validators for the llms_insert_access_plan_validation() function.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_validation_period() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t\t'price' => 1,\n\t\t\t'frequency' => 1,\n\t\t\t'trial_offer' => 'yes',\n\t\t\t'access_expiration' => 'limited-period',\n\t\t);\n\n\t\tforeach ( array( 'period', 'access_period', 'trial_period' ) as $period_prop ) {\n\n\t\t\tforeach ( array( 'year', 'month', 'week', 'day' ) as $period ) {\n\n\t\t\t\t$props[ $period_prop ] = $period;\n\n\t\t\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t\t\t// Success.\n\t\t\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t\t\t// Getter matches set value.\n\t\t\t\t$this->assertEquals( $period, $plan->get( $period_prop ) );\n\n\t\t\t}\n\n\t\t\t// Doesn't work with an invalid visibility.\n\t\t\t$props[ $period_prop ] = 'fake';\n\t\t\t$plan = llms_insert_access_plan( $props );\n\t\t\t$this->assertIsWPError( $plan );\n\t\t\t$this->assertWPErrorCodeEquals( 'invalid-' . $period_prop, $plan );\n\t\t\tunset( $props[ $period_prop ] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test product related conditions for llms_insert_access_plan() function.\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_validation_product() {\n\n\t\t$props = array();\n\n\t\t// Missing Product ID.\n\t\t$this->assertIsWPError( llms_insert_access_plan( $props ) );\n\t\t$this->assertWPErrorCodeEquals( 'missing-product-id', llms_insert_access_plan( $props ) );\n\n\t\t// Set but empty.\n\t\t$props['product_id'] = '';\n\t\t$this->assertIsWPError( llms_insert_access_plan( $props ) );\n\t\t$this->assertWPErrorCodeEquals( 'missing-product-id', llms_insert_access_plan( $props ) );\n\n\t\t// Not an ID.\n\t\t$props['product_id'] = 'fake';\n\t\t$this->assertIsWPError( llms_insert_access_plan( $props ) );\n\t\t$this->assertWPErrorCodeEquals( 'missing-product-id', llms_insert_access_plan( $props ) );\n\n\t\t// Real Product.\n\t\t$props['product_id'] = $this->factory->course->create( array( 'sections' => 0 ) );\n\t\t$this->assertTrue( is_a( llms_insert_access_plan( $props ), 'LLMS_Access_Plan' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_access_plan_after_create' ) );\n\n\t}\n\n\t/**\n\t * Test plan visibility validation for the llms_insert_access_plan() function\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_insert_access_plan_validation_visibility() {\n\n\t\t$props = array(\n\t\t\t'product_id' => $this->factory->course->create( array( 'sections' => 0 ) ),\n\t\t);\n\n\t\t// Invalid visibility.\n\t\t$props['visibility'] = 'fake';\n\t\t$this->assertIsWPError( llms_insert_access_plan( $props ) );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-visibility', llms_insert_access_plan( $props ) );\n\n\t\t// Valid visibilities.\n\t\tforeach ( array_keys( llms_get_access_plan_visibility_options() ) as $visibility ) {\n\n\t\t\t$props['visibility'] = $visibility;\n\t\t\t$plan = llms_insert_access_plan( $props );\n\n\t\t\t// Success.\n\t\t\t$this->assertTrue( is_a( $plan, 'LLMS_Access_Plan' ) );\n\n\t\t\t// Getter.\n\t\t\t$this->assertEquals( $visibility, $plan->get_visibility() );\n\n\t\t}\n\n\t}\n\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-admin.php",
    "content": "<?php\n/**\n * Tests for LifterLMS User Postmeta functions\n *\n * @group functions\n * @group admin_functions\n * @group admin\n *\n * @since 3.23.0\n */\nclass LLMS_Test_Functions_Admin extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test: llms_get_add_ons()\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_add_ons() {\n\n\t\t$res = llms_get_add_ons();\n\n\t\t// Return looks right.\n\t\t$this->assertEquals( array( 'categories', 'items' ), array_keys( $res ) );\n\n\t\t// Transient set for caching.\n\t\t$this->assertEquals( $res, get_transient( 'llms_products_api_result' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_add_ons() when an error is encountered.\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_add_ons_error() {\n\n\t\t$err = new WP_Error( 'mocked-err', 'Mocked Message', array( 'data' => 'mocked' ) );\n\t\t$this->mock_http_request( 'https://lifterlms.com/wp-json/llms/v3/products', $err );\n\n\t\t$res = llms_get_add_ons();\n\n\t\t// Expect mocked error message.\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'api_connection', $res );\n\t\t$this->assertWPErrorDataEquals( $err, $res );\n\n\t\t// No transient data.\n\t\t$this->assertFalse( get_transient( 'llms_products_api_result' ) );\n\n\t}\n\n\t/**\n\t * Test: llms_get_add_ons() caching mechanisms\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_add_ons_with_caching() {\n\n\t\t$mock = array( 'mock' );\n\t\tset_transient( 'llms_products_api_result', $mock, DAY_IN_SECONDS );\n\t\t$this->assertEquals( $mock, llms_get_add_ons() );\n\n\t\t// Skip cache.\n\t\t$this->assertNotEquals( $mock, llms_get_add_ons( false ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_add_on()\n\t *\n\t * @since 4.21.3\n\t * @since 5.0.0 Stop testing against Helper_Add_on.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_add_on() {\n\n\t\t// Fake add-on still works.\n\t\t$this->assertTrue( llms_get_add_on( array( 'id' => 'test' ) ) instanceof LLMS_Add_On );\n\n\t\t// Lookup a real add-on via a string.\n\t\t$res = llms_get_add_on( 'lifterlms-com-lifterlms', 'id' );\n\t\t$this->assertEquals( 'lifterlms-com-lifterlms', $res->get( 'id' ) );\n\n\t\t// // Pass in the whole add-on array.\n\t\t// $res = llms_get_add_on( LLMS_Unit_Test_Util::get_private_property_value( $res, 'data' ) );\n\t\t// $this->assertEquals( 'lifterlms-com-lifterlms', $res->get( 'id' ) );\n\n\t\t// // Should load the Helper's if found subclass.\n\t\t// global $lifterlms_tests;\n\t\t// require_once $lifterlms_tests->tests_dir . '/mocks/class-llms-helper-add-on.php';\n\n\t\t// $this->assertTrue( llms_get_add_on( array( 'id' => 'test' ) ) instanceof LLMS_Helper_Add_On );\n\n\t}\n\n\t/**\n\t * Test llms_get_dashicon_link().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_dashicon_link() {\n\n\t\t$url = 'https://mock.tld/fake';\n\n\t\t// Defaults.\n\t\t$this->assertEquals(\n\t\t\t'<a href=\"https://mock.tld/fake\" style=\"text-decoration:none;\" target=\"_blank\" rel=\"noreferrer\" title=\"More information\"><span class=\"dashicons dashicons-external\" style=\"font-size:18px;width:18px;height:18px\"></span></a>',\n\t\t\tllms_get_dashicon_link( $url )\n\t\t);\n\n\t\t// Custom args.\n\t\t$this->assertEquals(\n\t\t\t'<a href=\"https://mock.tld/fake\" style=\"text-decoration:none;\" target=\"_blank\" rel=\"noreferrer\" title=\"Something...\"><span class=\"dashicons dashicons-fake\" style=\"font-size:22px;width:22px;height:22px\"></span></a>',\n\t\t\tllms_get_dashicon_link( \n\t\t\t\t$url,\n\t\t\t\tarray(\n\t\t\t\t\t'size'   => 22,\n\t\t\t\t\t'title'  => 'Something...',\n\t\t\t\t\t'icon'   => 'fake',\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * test the llms_get_sales_page_types() function\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_sales_page_types() {\n\n\t\t$this->assertEquals( array(\n\t\t\t'none'    => 'Display default course content',\n\t\t\t'content' => 'Show custom content',\n\t\t\t'page'    => 'Redirect to WordPress Page',\n\t\t\t'url'     => 'Redirect to custom URL',\n\t\t), llms_get_sales_page_types() );\n\n\t}\n\n\t/**\n\t * Test lms_merge_code_button() when no merge codes are passed or exist in the current context.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_merge_code_button_no_codes() {\n\n\t\t$ret = llms_merge_code_button( 'content', false );\n\t\t$this->assertEquals( '', $ret );\n\n\t}\n\n\t/**\n\t * Test lms_merge_code_button() when custom merge codes are passed.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_merge_code_button_custom_codes() {\n\n\t\t$ret = llms_merge_code_button( 'content', false, array( '{test}' => 'Merge Code' ) );\n\t\t$this->assertStringContains( '<div class=\"llms-merge-code-wrapper\">', $ret );\n\t\t$this->assertStringContains( '<div class=\"llms-merge-codes\" data-target=\"content\">', $ret );\n\t\t$this->assertStringContains( '<li data-code=\"{test}\">Merge Code</li>', $ret );\n\t\t$this->assertStringContains( '</div><!-- .llms-merge-code-wrapper -->', $ret );\n\n\t}\n\n\t/**\n\t * Test lms_merge_code_button() on the `llms_certificate` screen.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_merge_code_button_certs() {\n\n\t\tllms_tests_mock_current_screen( 'llms_certificate' );\n\n\t\t$ret = llms_merge_code_button( 'content', false );\n\n\t\t$this->assertStringContains( '<div class=\"llms-merge-code-wrapper\">', $ret );\n\t\t$this->assertStringContains( '<div class=\"llms-merge-codes\" data-target=\"content\">', $ret );\n\n\t\tforeach ( llms_get_certificate_merge_codes() as $code => $desc ) {\n\n\t\t\t$this->assertStringContains( '<li data-code=\"' . $code . '\">' . $desc . '</li>', $ret );\n\n\t\t}\n\t\t$this->assertStringContains( '</div><!-- .llms-merge-code-wrapper -->', $ret );\n\n\t\tllms_tests_reset_current_screen();\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-certificates.php",
    "content": "<?php\n/**\n * Tests for LifterLMS User Postmeta functions\n *\n * @package LifterLMS/Tests\n *\n * @group functions\n * @group functions_certificates\n * @group certificates\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Functions_Certificates extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_get_certificate().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificate() {\n\n\t\t$post = $this->factory->post->create();\n\n\t\t// Invalid post type.\n\t\t$this->assertFalse( llms_get_certificate( $post ) );\n\n\t\t// Non-existent post.\n\t\t$this->assertFalse( llms_get_certificate( $post + 1 ) );\n\n\t\t// Template post without preview flag.\n\t\t$template_post = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) );\n\t\t$this->assertFalse( llms_get_certificate( $template_post ) );\n\n\t\t// Template post with preview flag.\n\t\t$preview = llms_get_certificate( $template_post, true );\n\t\t$this->assertInstanceOf( 'LLMS_User_Certificate', $preview );\n\t\t$this->assertEquals( $template_post, $preview->get( 'id' ) );\n\n\t\t// Earned cert.\n\t\t$earned_post = $this->factory->post->create( array( 'post_type' => 'llms_my_certificate' ) );\n\t\t$earned = llms_get_certificate( $earned_post );\n\t\t$this->assertInstanceOf( 'LLMS_User_Certificate', $earned );\n\t\t$this->assertEquals( $earned_post, $earned->get( 'id' ) );\n\n\t\t// From global.\n\t\tglobal $post;\n\t\t$post = get_post( $earned_post );\n\t\t$global = llms_get_certificate();\n\t\t$this->assertInstanceOf( 'LLMS_User_Certificate', $global );\n\t\t$this->assertEquals( $earned_post, $global->get( 'id' ) );\n\n\t\t$post = null;\n\n\t}\n\n\t/**\n\t * Test llms_get_certificate_content().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificate_content() {\n\n\t\t// Invalid post.\n\t\t$post = $this->factory->post->create();\n\t\t$this->assertEquals( '', llms_get_certificate_content( $post ) );\n\n\t\t// Template: merge the stored content.\n\t\t$template_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_certificate',\n\t\t\t'post_content' => 'Cert ID: {certificate_id}',\n\t\t) );\n\t\t$this->assertEquals( \"<p>Cert ID: {$template_id}</p>\\n\", llms_get_certificate_content( $template_id ) );\n\n\t\t// Earned cert: return the stored content.\n\t\t$earned_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_certificate',\n\t\t\t'post_content' => 'Just some content.',\n\t\t) );\n\t\t$this->assertEquals( \"<p>Just some content.</p>\\n\", llms_get_certificate_content( $earned_id ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_certificate_content() with reusable blocks and merge codes.\n\t *\n\t * @since 6.4.0\n\t * @since 6.6.0 Remove layout filters for easier snapshots across WP versions.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificate_content_reusable_blocks() {\n\n\t\t/**\n\t\t * Remove layout support for the purposes of this test.\n\t\t *\n\t\t * It's complicated to conditionally match the classes added to column wrappers (added via this filter)\n\t\t * so we'll just disable them.\n\t\t *\n\t\t * @todo When the minimum supported WP version is 6.0 we can remove this an update the snapshots.\n\t\t */\n\t\tremove_filter( 'render_block', 'wp_render_layout_support_flag', 10 );\n\n\t\t// Define reusable blocks.\n\t\t$reusable_blocks = array(\n\t\t\t// Multiple blocks.\n\t\t\t1 => \"<!-- wp:paragraph --><p>reusable block 1 paragraph 1</p><!-- /wp:paragraph -->\\n\" .\n\t\t\t     \"<!-- wp:paragraph --><p>reusable block 1 paragraph 2</p><!-- /wp:paragraph -->\",\n\n\t\t\t// Multiple blocks containing different LifterLMS merge codes.\n\t\t\t2 => \"<!-- wp:paragraph --><p>reusable block 2 Certificate ID: {certificate_id}</p><!-- /wp:paragraph -->\\n\" .\n\t\t\t     \"<!-- wp:paragraph --><p>reusable block 2 Sequential ID: {sequential_id}</p><!-- /wp:paragraph -->\",\n\n\t\t\t// One block with two levels of inner blocks.\n\t\t\t3 => \"<!-- wp:columns --><div class=\\\"wp-block-columns\\\">\\n\" .\n\t\t\t     \"    <!-- wp:column --><div class=\\\"wp-block-column\\\">\\n\" .\n\t\t\t     \"        <!-- wp:paragraph --><p>reusable block 3 column 1</p><!-- /wp:paragraph -->\\n\" .\n\t\t\t     \"    <!-- /wp:column --></div>\\n\" .\n\t\t\t     \"    <!-- wp:column --><div class=\\\"wp-block-column\\\">\\n\" .\n\t\t\t     \"        <!-- wp:paragraph --><p>reusable block 3 column 2</p><!-- /wp:paragraph -->\\n\" .\n\t\t\t     \"    <!-- /wp:column --></div>\\n\" .\n\t\t\t     \"<!-- /wp:columns -->\",\n\n\t\t\t// One block and multiple references to different reusable blocks.\n\t\t\t4 => \"<!-- wp:paragraph --><p>reusable block 4 paragraph 1</p><!-- /wp:paragraph -->\\n\" .\n\t\t\t     \"<!-- wp:block {\\\"ref\\\":{REUSABLE_BLOCK_1}} /-->\\n\" .\n\t\t\t     \"<!-- wp:block {\\\"ref\\\":{REUSABLE_BLOCK_2}} /-->\",\n\n\t\t\t// One block with two levels of inner blocks that reference different reusable blocks.\n\t\t\t5 => \"<!-- wp:columns --><div class=\\\"wp-block-columns\\\">\\n\" .\n\t\t\t     \"    <!-- wp:column --><div class=\\\"wp-block-column\\\">\\n\" .\n\t\t\t     \"        <!-- wp:block {\\\"ref\\\":{REUSABLE_BLOCK_1}} /-->\\n\" .\n\t\t\t     \"    <!-- /wp:column --></div>\\n\" .\n\t\t\t     \"    <!-- wp:column --><div class=\\\"wp-block-column\\\">\\n\" .\n\t\t\t     \"        <!-- wp:block {\\\"ref\\\":{REUSABLE_BLOCK_4}} /-->\\n\" .\n\t\t\t     \"    <!-- /wp:column --></div>\\n\" .\n\t\t\t     \"<!-- /wp:columns -->\",\n\t\t);\n\n\t\t$reusable_posts   = array();\n\t\t$template_blocks  = array();\n\t\t$template_posts   = array();\n\t\t$reusable_pattern = '/{REUSABLE_BLOCK_(\\d+?)}/';\n\n\t\tforeach ( $reusable_blocks as $key => $reusable_block ) {\n\n\t\t\t// Replace reusable block merge codes with their reusable post ID.\n\t\t\twhile ( preg_match_all( $reusable_pattern, $reusable_block, $matches, PREG_SET_ORDER ) ) {\n\t\t\t\tforeach ( $matches as $match ) {\n\t\t\t\t\t$reusable_block = str_replace( $match[0], $reusable_posts[ $match[1] ]->ID, $reusable_block );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Create reusable block.\n\t\t\t$reusable_posts[ $key ] = $this->factory->post->create_and_get( array(\n\t\t\t\t'post_type'    => 'wp_block',\n\t\t\t\t'post_content' => $reusable_block,\n\t\t\t) );\n\n\t\t\t// Create template with only the reference to a reusable block.\n\t\t\t$template_blocks[ $key ] = null;\n\t\t\t$template_posts[ $key ]  = $this->factory->post->create_and_get( array(\n\t\t\t\t'post_type'    => 'llms_certificate',\n\t\t\t\t'post_content' => \"<!-- wp:block {\\\"ref\\\":{$reusable_posts[ $key ]->ID}} /-->\",\n\t\t\t) );\n\n\t\t\t// Create template with a paragraph block and a reference to a reusable block.\n\t\t\t$template_blocks[ $key + 100 ] = \"<!-- wp:paragraph --><p>template 10$key reusable block $key</p>\" .\n\t\t\t                                 \"<!-- /wp:paragraph -->\";\n\t\t\t$template_posts[ $key + 100 ]  = $this->factory->post->create_and_get( array(\n\t\t\t\t'post_type'    => 'llms_certificate',\n\t\t\t\t'post_content' => $template_blocks[ $key + 100 ] .\n\t\t\t\t                  \"<!-- wp:block {\\\"ref\\\":{$reusable_posts[ $key ]->ID}} /-->\",\n\t\t\t) );\n\t\t}\n\n\t\t$reusable_pattern = '/<!-- wp:block {\"ref\":{REUSABLE_BLOCK_(\\d+?)}} \\/-->/';\n\n\t\tforeach ( $template_posts as $key => $template_post ) {\n\n\t\t\t$reusable_key = $key < 100 ? $key : $key - 100;\n\t\t\t$expected     = $template_blocks[ $key ] . $reusable_blocks[ $reusable_key ];\n\n\t\t\t// Replace reusable block merge codes with their reusable block content.\n\t\t\twhile ( preg_match_all( $reusable_pattern, $expected, $matches, PREG_SET_ORDER ) ) {\n\t\t\t\tforeach ( $matches as $match ) {\n\t\t\t\t\t$expected = str_replace( $match[0], $reusable_blocks[ $match[1] ], $expected );\n\t\t\t\t}\n\t\t\t}\n\t\t\t$expected = preg_replace( '/<!--.+?-->/', '', $expected );\n\n\t\t\t$sequence_id = llms_get_certificate_sequential_id( $template_post->ID, false );\n\t\t\t$sequence_id = str_pad( $sequence_id, 6, '0', STR_PAD_LEFT );\n\t\t\t$expected    = str_replace( '{certificate_id}', $template_post->ID, $expected );\n\t\t\t$expected    = str_replace( '{sequential_id}', $sequence_id, $expected );\n\n\t\t\t$this->assertEquals(\n\t\t\t\t$expected,\n\t\t\t\tllms_get_certificate_content( $template_post->ID ),\n\t\t\t\t\"template #$key, reusable block #$reusable_key\"\n\t\t\t);\n\t\t}\n\t\t\n\t\t// Restore filter.\n\t\tadd_filter( 'render_block', 'wp_render_layout_support_flag', 10, 2 );\n\n\t}\n\n\t/**\n\t * Test llms_get_certificate_fonts().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificate_fonts() {\n\n\t\t$this->assertIsArray( llms_get_certificate_fonts() );\n\n\t}\n\n\tpublic function test_llms_get_certificate_image() {\n\n\t\t$this->markTestIncomplete();\n\n\t}\n\n\t/**\n\t * Test llms_get_certificate_merge_codes()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificate_merge_codes() {\n\n\t\t$ret = llms_get_certificate_merge_codes();\n\t\t$this->assertIsArray( $ret );\n\t\tforeach ( $ret as $code => $desc ) {\n\n\t\t\t$this->assertEquals( '{', $code[0] );\n\t\t\t$this->assertEquals( '}', $code[strlen( $code ) - 1] );\n\t\t\t$this->assertIsString( $desc );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_get_certificate_sequential_id()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificate_sequential_id() {\n\n\t\t$template_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate' ) );\n\n\t\t$id = 1;\n\t\twhile ( $id <= 100 ) {\n\n\t\t\t// Just get the next ID, don't increment.\n\t\t\t$this->assertEquals( $id, llms_get_certificate_sequential_id( $template_id, false ) );\n\t\t\t$this->assertEquals( $id, absint( get_post_meta( $template_id, '_llms_sequential_id', true ) ) );\n\n\t\t\t// Increment the ID.\n\t\t\t$this->assertEquals( $id, llms_get_certificate_sequential_id( $template_id, true ) );\n\t\t\t$this->assertEquals( $id + 1, absint( get_post_meta( $template_id, '_llms_sequential_id', true ) ) );\n\n\t\t\t$id++;\n\n\t\t}\n\n\t\t// Big numbers.\n\t\t$id = 923409;\n\t\tupdate_post_meta( $template_id, '_llms_sequential_id', $id );\n\t\twhile ( $id <= 923512 ) {\n\n\t\t\t// Just get the next ID, don't increment.\n\t\t\t$this->assertEquals( $id, llms_get_certificate_sequential_id( $template_id, false ) );\n\t\t\t$this->assertEquals( $id, absint( get_post_meta( $template_id, '_llms_sequential_id', true ) ) );\n\n\t\t\t// Increment the ID.\n\t\t\t$this->assertEquals( $id, llms_get_certificate_sequential_id( $template_id, true ) );\n\t\t\t$this->assertEquals( $id + 1, absint( get_post_meta( $template_id, '_llms_sequential_id', true ) ) );\n\n\t\t\t$id++;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_get_certificate_title().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificate_title() {\n\n\t\t// Invalid post type.\n\t\t$post = $this->factory->post->create();\n\t\t$this->assertEquals( '', llms_get_certificate_title( $post ) );\n\n\t\t$template_id = $this->factory->post->create( array( 'post_type' => 'llms_certificate', 'post_title' => 'Not Returned' ) );\n\t\tupdate_post_meta( $template_id, '_llms_certificate_title', 'Cert Title!' );\n\n\t\t$this->assertEquals( 'Cert Title!', llms_get_certificate_title( $template_id ) );\n\n\t\t$earned_id = $this->factory->post->create( array( 'post_type' => 'llms_my_certificate', 'post_title' => 'A Title' ) );\n\t\t$this->assertEquals( 'A Title', llms_get_certificate_title( $earned_id ) );\n\n\t}\n\n\t/**\n\t * Test llms_is_block_editor_supported_for_certificates()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_block_editor_supported_for_certificates() {\n\n\t\tglobal $wp_version;\n\t\t$orig = $wp_version;\n\n\t\t$tests = array(\n\t\t\t// Unsupported versions.\n\t\t\tarray( '5.5.0', false ),\n\t\t\tarray( '5.6.0', false ),\n\t\t\tarray( '5.6.1', false ),\n\t\t\tarray( '5.7', false ),\n\t\t\tarray( '5.7.0', false ),\n\t\t\tarray( '5.7.5', false ),\n\t\t\t// Supported versions.\n\t\t\tarray( '5.8', true ),\n\t\t\tarray( '5.8.0', true ),\n\t\t\tarray( '5.8.1', true ),\n\t\t\tarray( '5.8.3', true ),\n\t\t\tarray( '5.9', true ),\n\t\t\tarray( '5.9-alpha', true ),\n\t\t\tarray( '5.9-RC1', true ),\n\t\t\t// Future versions?\n\t\t\tarray( '6.0', true ),\n\t\t\tarray( '6.0.1', true ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\t\t\tlist( $wp_version, $expect ) = $test;\n\t\t\t$this->assertEquals( $expect, llms_is_block_editor_supported_for_certificates(), $wp_version );\n\n\t\t\t// Test \"-src\" version.\n\t\t\t$wp_version .= '-src';\n\t\t\t$this->assertEquals( $expect, llms_is_block_editor_supported_for_certificates(), $wp_version);\n\t\t}\n\n\t\t$wp_version = $orig;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-conditional-tags.php",
    "content": "<?php\n/**\n * Test Order Functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_conditional_tags\n *\n * @since 3.37.0\n * @since 3.37.12 Fix tests failing due to incorrect post type (It's 'llms_membership' not 'membership').\n */\nclass LLMS_Test_Functions_Conditional_Tags extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test the is_course() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_course() {\n\n\t\t$this->assertFalse( is_course() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_course() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create() ) );\n\t\t$this->assertFalse( is_course() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) );\n\t\t$this->assertTrue( is_course() );\n\n\t}\n\n\t/**\n\t * Test is_course_category() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_course_category() {\n\n\t\t$this->assertFalse( is_course_category() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_course_category() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) );\n\t\t$this->assertFalse( is_course_category() );\n\n\t\t$term = wp_create_tag( 'mock-tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertFalse( is_course_category() );\n\n\t\t// Cat not specified.\n\t\t$term = wp_create_term( 'mock-cat', 'course_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_course_category() );\n\t\t$this->assertTrue( is_course_category( $term['term_id'] ) );\n\t\t$this->assertTrue( is_course_category( array( $term['term_id'] ) ) );\n\n\t\t// Another term.\n\t\t$term_2 = wp_create_term( 'mock-cat-2', 'course_cat' );\n\t\t$this->go_to( get_term_link( $term_2['term_id'] ) );\n\t\t$this->assertTrue( is_course_category() );\n\n\t\t// We're on the other term's page.\n\t\t$this->assertFalse( is_course_category( $term['term_id'] ) );\n\n\t\t// One of passed terms.\n\t\t$this->assertTrue( is_course_category( array( $term['term_id'], $term_2['term_id'] ) ) );\n\n\t}\n\n\t/**\n\t * Test is_course_tag() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_course_tag() {\n\n\t\t$this->assertFalse( is_course_tag() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_course_tag() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) );\n\t\t$this->assertFalse( is_course_tag() );\n\n\t\t$term = wp_create_tag( 'mock-tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertFalse( is_course_tag() );\n\n\t\t// Cat not specified.\n\t\t$term = wp_create_term( 'mock-cat', 'course_tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_course_tag() );\n\t\t$this->assertTrue( is_course_tag( $term['term_id'] ) );\n\t\t$this->assertTrue( is_course_tag( array( $term['term_id'] ) ) );\n\n\t\t// Another term.\n\t\t$term_2 = wp_create_term( 'mock-cat-2', 'course_tag' );\n\t\t$this->go_to( get_term_link( $term_2['term_id'] ) );\n\t\t$this->assertTrue( is_course_tag() );\n\n\t\t// We're on the other term's page.\n\t\t$this->assertFalse( is_course_tag( $term['term_id'] ) );\n\n\t\t// One of passed terms.\n\t\t$this->assertTrue( is_course_tag( array( $term['term_id'], $term_2['term_id'] ) ) );\n\n\t}\n\n\t/**\n\t * Test is_course_tag() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_course_taxonomy() {\n\n\t\t$this->assertFalse( is_course_taxonomy() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_course_taxonomy() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'course' ) ) ) );\n\t\t$this->assertFalse( is_course_taxonomy() );\n\n\t\t$term = wp_create_tag( 'mock-tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertFalse( is_course_taxonomy() );\n\n\t\t// Cat.\n\t\t$term = wp_create_term( 'mock-cat', 'course_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_course_taxonomy() );\n\n\t\t// Tag.\n\t\t$term = wp_create_term( 'mock-tag', 'course_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_course_taxonomy() );\n\n\t}\n\n\t/**\n\t * Test is_courses()\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_courses() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$this->assertFalse( is_courses() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_courses() );\n\n\t\t$this->go_to( get_post_type_archive_link( 'llms_membership' ) );\n\t\t$this->assertFalse( is_courses() );\n\n\t\t$this->go_to( get_post_type_archive_link( 'course' ) );\n\t\t$this->assertTrue( is_courses() );\n\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'courses' ) ) );\n\t\t$this->assertTrue( is_courses() );\n\n\t}\n\n\t/**\n\t * Test the is_lesson() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_lesson() {\n\n\t\t$this->assertFalse( is_lesson() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_lesson() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create() ) );\n\t\t$this->assertFalse( is_lesson() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'lesson' ) ) ) );\n\t\t$this->assertTrue( is_lesson() );\n\n\t}\n\n\t/**\n\t * Test is_lifterlms() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_lifterlms() {\n\n\t\t$this->assertFalse( is_lifterlms() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_lifterlms() );\n\n\t\t$post_types = array(\n\t\t\t'post' => false,\n\t\t\t'course' => true,\n\t\t\t'lesson' => true,\n\t\t\t'llms_quiz' => true,\n\t\t\t'llms_membership' => true,\n\t\t);\n\t\tforeach( $post_types as $post_type => $expect ) {\n\n\t\t\t// Single post type.\n\t\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => $post_type ) ) ) );\n\t\t\t$this->assertEquals( $expect, is_lifterlms() );\n\n\t\t\tif ( ! in_array( $post_type, array( 'lesson', 'llms_quiz' ), true ) ) {\n\n\t\t\t\t// Archive page.\n\t\t\t\t$this->go_to( get_post_type_archive_link( $post_type ) );\n\t\t\t\t$this->assertEquals( $expect, is_lifterlms(), $post_type );\n\n\t\t\t}\n\n\t\t}\n\n\t\t$term = wp_create_term( 'mock-cat', 'course_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_lifterlms() );\n\n\t\t$term = wp_create_term( 'mock-cat', 'membership_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_lifterlms() );\n\n\t\t$term = wp_create_tag( 'mock-tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertFalse( is_lifterlms() );\n\n\t}\n\n\t/**\n\t * Test the is_llms_account_page() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_llms_account_page() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$this->assertFalse( is_llms_account_page() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_llms_account_page() );\n\n\t\tadd_filter( 'lifterlms_is_account_page', '__return_true' );\n\t\t$this->assertTrue( is_llms_account_page() );\n\t\tremove_filter( 'lifterlms_is_account_page', '__return_true' );\n\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'myaccount' ) ) );\n\t\t$this->assertTrue( is_llms_account_page() );\n\n\t}\n\n\t/**\n\t * Test the is_llms_checkout() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_llms_checkout() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$this->assertFalse( is_llms_checkout() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_llms_checkout() );\n\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'checkout' ) ) );\n\t\t$this->assertTrue( is_llms_checkout() );\n\n\t}\n\n\t/**\n\t * Test the is_membership() function.\n\t *\n\t * @since 3.37.0\n\t * @since 3.37.12 Fix tests failing due to incorrect post type.\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_membership() {\n\n\t\t$this->assertFalse( is_membership() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_membership() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create() ) );\n\t\t$this->assertFalse( is_membership() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) );\n\t\t$this->assertTrue( is_membership() );\n\n\t}\n\n\t/**\n\t * Test is_membership_category() function.\n\t *\n\t * @since 3.37.0\n\t * @since 3.37.12 Fix tests failing due to incorrect post type.\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_is_membership_category() {\n\n\t\t$this->assertFalse( is_membership_category() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_membership_category() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) );\n\t\t$this->assertFalse( is_membership_category() );\n\n\t\t$term = wp_create_tag( 'mock-tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertFalse( is_membership_category() );\n\n\t\t// Cat not specified.\n\t\t$term = wp_create_term( 'mock-cat', 'membership_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_membership_category() );\n\t\t$this->assertTrue( is_membership_category( $term['term_id'] ) );\n\t\t$this->assertTrue( is_membership_category( array( $term['term_id'] ) ) );\n\n\t\t// Another term.\n\t\t$term_2 = wp_create_term( 'mock-cat-2', 'membership_cat' );\n\t\t$this->go_to( get_term_link( $term_2['term_id'] ) );\n\t\t$this->assertTrue( is_membership_category() );\n\n\t\t// We're on the other term's page.\n\t\t$this->assertFalse( is_membership_category( $term['term_id'] ) );\n\n\t\t// One of passed terms.\n\t\t$this->assertTrue( is_membership_category( array( $term['term_id'], $term_2['term_id'] ) ) );\n\n\t}\n\n\t/**\n\t * Test is_membership_tag() function.\n\t *\n\t * @since 3.37.0\n\t * @since 3.37.12 Fix tests failing due to incorrect post type.\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_is_membership_tag() {\n\n\t\t$this->assertFalse( is_membership_tag() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_membership_tag() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) );\n\t\t$this->assertFalse( is_membership_tag() );\n\n\t\t$term = wp_create_tag( 'mock-tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertFalse( is_membership_tag() );\n\n\t\t// Cat not specified.\n\t\t$term = wp_create_term( 'mock-cat', 'membership_tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_membership_tag() );\n\t\t$this->assertTrue( is_membership_tag( $term['term_id'] ) );\n\t\t$this->assertTrue( is_membership_tag( array( $term['term_id'] ) ) );\n\n\t\t// Another term.\n\t\t$term_2 = wp_create_term( 'mock-cat-2', 'membership_tag' );\n\t\t$this->go_to( get_term_link( $term_2['term_id'] ) );\n\t\t$this->assertTrue( is_membership_tag() );\n\n\t\t// We're on the other term's page.\n\t\t$this->assertFalse( is_membership_tag( $term['term_id'] ) );\n\n\t\t// One of passed terms.\n\t\t$this->assertTrue( is_membership_tag( array( $term['term_id'], $term_2['term_id'] ) ) );\n\n\t}\n\n\t/**\n\t * Test is_membership_tag() function.\n\t *\n\t * @since 3.37.0\n\t * @since 3.37.12 Fix tests failing due to incorrect post type.\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_is_membership_taxonomy() {\n\n\t\t$this->assertFalse( is_membership_taxonomy() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_membership_taxonomy() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_membership' ) ) ) );\n\t\t$this->assertFalse( is_membership_taxonomy() );\n\n\t\t$term = wp_create_tag( 'mock-tag' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertFalse( is_membership_taxonomy() );\n\n\t\t// Cat.\n\t\t$term = wp_create_term( 'mock-cat', 'membership_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_membership_taxonomy() );\n\n\t\t// Tag.\n\t\t$term = wp_create_term( 'mock-tag', 'membership_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertTrue( is_membership_taxonomy() );\n\n\t}\n\n\t/**\n\t * Test is_memberships()\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_memberships() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$this->assertFalse( is_memberships() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_memberships() );\n\n\t\t$this->go_to( get_post_type_archive_link( 'course' ) );\n\t\t$this->assertFalse( is_memberships() );\n\n\t\t$this->go_to( get_post_type_archive_link( 'llms_membership' ) );\n\t\t$this->assertTrue( is_memberships() );\n\n\t\t$this->go_to( get_permalink( llms_get_page_id( 'memberships' ) ) );\n\t\t$this->assertTrue( is_memberships() );\n\n\t}\n\n\t/**\n\t * Test the is_quiz() function.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_quiz() {\n\n\t\t$this->assertFalse( is_quiz() );\n\n\t\t$this->go_to( home_url() );\n\t\t$this->assertFalse( is_quiz() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create() ) );\n\t\t$this->assertFalse( is_quiz() );\n\n\t\t$this->go_to( get_permalink( $this->factory->post->create( array( 'post_type' => 'llms_quiz' ) ) ) );\n\t\t$this->assertTrue( is_quiz() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-content.php",
    "content": "<?php\n/**\n * Tests for LifterLMS User Postmeta functions\n *\n * @package LifterLMS/Tests\n *\n * @group functions\n * @group content_functions\n *\n * @since 3.25.1\n */\nclass LLMS_Test_Functions_Content extends LLMS_UnitTestCase {\n\n\t/**\n\t * Helper to retrieve filtered post content for a given post\n\t *\n\t * @since 4.17.0\n\t *\n\t * @param WP_Post $post Post object\n\t * @return string\n\t */\n\tprivate function get_post_content( $post ) {\n\t\treturn trim( apply_filters( 'the_content', $post->post_content ) );\n\t}\n\n\t/**\n\t * Retrieve a mock post of a give type with expected content and excerpts.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @param WP_Post $post Post object\n\t * @return WP_Post\n\t */\n\tprivate function get_mock_post( $post_type ) {\n\n\t\tglobal $post;\n\t\t$post = $this->factory->post->create_and_get( array(\n\t\t\t'post_type'    => $post_type,\n\t\t\t'post_content' => '<p>Post Content</p>',\n\t\t\t'post_excerpt' => '<p>Post Excerpt</p>',\n\t\t) );\n\n\t\treturn $post;\n\n\t}\n\n\t/**\n\t * Callback for `llms_page_restricted` filter to force a page to look restricted\n\t *\n\t * @since 4.17.0\n\t *\n\t * @param array $restrictions Restriction data array from llms_page_restricted().\n\t * @return array\n\t */\n\tpublic function make_restricted( $restrictions ) {\n\t\t$restrictions['is_restricted'] = true;\n\t\treturn $restrictions;\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for various post types\n\t *\n\t * This test was never a very good one but it's retained as it does ensure WP core post types\n\t * are not affected by our functions.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content() {\n\n\t\tllms_post_content_init();\n\n\t\t$content = '<p>Lorem ipsum dolor sit amet.</p>';\n\t\t$post_types = array( 'llms_membership', 'course', 'lesson', 'llms_quiz', 'post', 'page' );\n\t\tforeach ( $post_types as $post_type ) {\n\n\t\t\tglobal $post;\n\t\t\t$post = $this->factory->post->create_and_get( array(\n\t\t\t\t'post_type'    => $post_type,\n\t\t\t\t'post_content' => $content,\n\t\t\t) );\n\n\t\t\tif ( in_array( $post_type, array( 'post', 'page', 'llms_membership' ), true ) ) {\n\t\t\t\t$this->assertEquals( $content, $this->get_post_content( $post ) );\n\t\t\t} else {\n\t\t\t\t$this->assertNotEquals( $content, $this->get_post_content( $post ) );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for the course post type.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_course_restricted_no_sales_page() {\n\n\t\t$before = did_action( 'lifterlms_single_course_before_summary' );\n\t\t$after  = did_action( 'lifterlms_single_course_after_summary' );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'course' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t// Starts with the default post content.\n\t\t$this->assertSame( 0, strpos( $res, '<p>Post Content</p>' ) );\n\n\t\t// Additions added to the end.\n\t\t$additions = array(\n\t\t\t'<div class=\"llms-meta-info\">',\n\t\t\t'<section class=\"llms-instructor-info\">',\n\t\t\t'<div class=\"llms-syllabus-wrapper\">',\n\t\t);\n\t\tforeach ( $additions as $add ) {\n\t\t\t$this->assertStringContains( $add, $res );\n\t\t}\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_single_course_before_summary' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_single_course_after_summary' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for the course post type with restrictions and a salse page.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_course_restricted_with_sales_page() {\n\n\t\t$before = did_action( 'lifterlms_single_course_before_summary' );\n\t\t$after  = did_action( 'lifterlms_single_course_after_summary' );\n\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'course' );\n\n\t\tupdate_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t// Starts with the post's excerpt post content.\n\t\t$this->assertSame( 0, strpos( $res, '<p>Post Excerpt</p>' ) );\n\n\t\t// Post's content should not be found.\n\t\t$this->assertSame( false, strpos( $res, '<p>Post Content</p>' ) );\n\n\t\t// Additions added to the end.\n\t\t$additions = array(\n\t\t\t'<div class=\"llms-meta-info\">',\n\t\t\t'<section class=\"llms-instructor-info\">',\n\t\t\t'<div class=\"llms-syllabus-wrapper\">',\n\t\t);\n\t\tforeach ( $additions as $add ) {\n\t\t\t$this->assertStringContains( $add, $res );\n\t\t}\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_single_course_before_summary' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_single_course_after_summary' ) );\n\n\t\tremove_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for the membership post type.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_membership_restricted_no_sales_page() {\n\n\t\t$before = did_action( 'lifterlms_single_membership_before_summary' );\n\t\t$after  = did_action( 'lifterlms_single_membership_after_summary' );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'llms_membership' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t// No additions to the post content.\n\t\t$this->assertEquals( '<p>Post Content</p>', $res );\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_single_membership_before_summary' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_single_membership_after_summary' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for the membership post type with restrictions and a salse page.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_membership_restricted_with_sales_page() {\n\n\t\t$before = did_action( 'lifterlms_single_membership_before_summary' );\n\t\t$after  = did_action( 'lifterlms_single_membership_after_summary' );\n\n\t\t$handler = function( $restrictions ) {\n\t\t\t$restrictions['is_restricted'] = true;\n\t\t\treturn $restrictions;\n\t\t};\n\t\tadd_filter( 'llms_page_restricted', $handler );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'llms_membership' );\n\n\t\tupdate_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t// Just the excerpt.\n\t\t$this->assertEquals( '<p>Post Excerpt</p>', $res );\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_single_membership_before_summary' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_single_membership_after_summary' ) );\n\n\t\tremove_filter( 'llms_page_restricted', $handler );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for the lesson post type.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_lesson() {\n\n\t\t$before = did_action( 'lifterlms_single_lesson_before_summary' );\n\t\t$after  = did_action( 'lifterlms_single_lesson_after_summary' );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'lesson' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t// Starts with the back to course link.\n\t\t$this->assertSame( 0, strpos( $res, '<p class=\"llms-parent-course-link\">' ) );\n\n\t\t$additions = array(\n\t\t\t'<p>Post Content</p>', // Default content.\n\t\t\t'<nav class=\"llms-course-navigation\">',\n\t\t);\n\t\tforeach ( $additions as $add ) {\n\t\t\t$this->assertStringContains( $add, $res );\n\t\t}\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_single_lesson_before_summary' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_single_lesson_after_summary' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for a restricted lesson post type.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_lesson_restricted() {\n\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) );\n\n\t\t$before = did_action( 'lifterlms_no_access_main_content' );\n\t\t$after  = did_action( 'lifterlms_no_access_after' );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'lesson' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t$this->assertSame( '', $res );\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_no_access_main_content' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_no_access_after' ) );\n\n\t\tremove_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for the quiz post type.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_quiz() {\n\n\t\t$before = did_action( 'lifterlms_single_quiz_before_summary' );\n\t\t$after  = did_action( 'lifterlms_single_quiz_after_summary' );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'llms_quiz' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t// Starts with a wrapper.\n\t\t$this->assertSame( 0, strpos( $res, '<div class=\"llms-quiz-wrapper\" id=\"llms-quiz-wrapper\">' ) );\n\n\t\t$additions = array(\n\t\t\t'<div class=\"llms-return\">',\n\t\t\t'<p>Post Content</p>', // Default content.\n\t\t\t'</div><!--end #llms-quiz-wrapper -->',\n\t\t);\n\t\tforeach ( $additions as $add ) {\n\t\t\t$this->assertStringContains( $add, $res );\n\t\t}\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_single_quiz_before_summary' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_single_quiz_after_summary' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_content() for a restricted quiz post type.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_quiz_restricted() {\n\n\t\tadd_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) );\n\n\t\t$before = did_action( 'lifterlms_no_access_main_content' );\n\t\t$after  = did_action( 'lifterlms_no_access_after' );\n\n\t\tllms_post_content_init();\n\t\t$post = $this->get_mock_post( 'llms_quiz' );\n\n\t\t$res = $this->get_post_content( $post );\n\n\t\t$this->assertSame( '', $res );\n\n\t\t$this->assertEquals( ++$before, did_action( 'lifterlms_no_access_main_content' ) );\n\t\t$this->assertEquals( ++$after, did_action( 'lifterlms_no_access_after' ) );\n\n\t\tremove_filter( 'llms_page_restricted', array( $this, 'make_restricted' ) );\n\n\t}\n\n\t/**\n\t * Test that llms_get_post_content() will return early if the `$post` global is not set.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_content_no_global() {\n\n\t\tllms_post_content_init();\n\n\t\t$input = 'whatever';\n\t\t$this->assertEquals( $input, llms_get_post_content( $input ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post_sales_page_content() for an unsupported post type.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_sales_page_content_unsupported() {\n\t\t$this->assertEquals( 'default content', llms_get_post_sales_page_content( $this->factory->post->create_and_get(), 'default content' ) );\n\t}\n\n\t/**\n\t * Test llms_get_post_sales_page_content() for supported post types.\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_sales_page_content_supported() {\n\n\t\t$post_excerpt = 'excerpt content';\n\n\t\tforeach ( array( 'course', 'llms_membership' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type', 'post_excerpt' ) );\n\t\t\tupdate_post_meta( $post->ID, '_llms_sales_page_content_type', 'redirect' );\n\t\t\t$this->assertEquals( 'default content', llms_get_post_sales_page_content( $post, 'default content' ) );\n\n\t\t\tupdate_post_meta( $post->ID, '_llms_sales_page_content_type', 'content' );\n\n\t\t\t$this->assertEquals( \"<p>excerpt content</p>\\n\", llms_get_post_sales_page_content( $post, 'default content' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_post_content_init() when filters should be applied\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_post_content_init() {\n\n\t\tremove_filter( 'the_content', 'llms_get_post_content' );\n\n\t\t$this->assertTrue( llms_post_content_init() );\n\t\t$this->assertEquals( 10, has_filter( 'the_content', 'llms_get_post_content' ) );\n\n\t}\n\n\t/**\n\t * Test llms_post_content_init() when on the admin panel\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_post_content_init_is_admin() {\n\n\t\tremove_filter( 'the_content', 'llms_get_post_content' );\n\n\t\tset_current_screen( 'admin.php' );\n\n\t\t$this->assertFalse( llms_post_content_init() );\n\t\t$this->assertFalse( has_filter( 'the_content', 'llms_get_post_content' ) );\n\n\t\tset_current_screen( 'front' ); // Reset.\n\n\t}\n\n\t/**\n\t * Test llms_post_content_init() when filters should be applied\n\t *\n\t * @since 4.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_post_content_custom() {\n\n\t\t$this->assertTrue( llms_post_content_init( 'a_fake_callback', 85 ) );\n\t\t$this->assertEquals( 85, has_filter( 'the_content', 'a_fake_callback' ) );\n\n\t\tremove_filter( 'the_content', 'a_fake_callback' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-core.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Core Functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_core\n *\n * @since 3.3.1\n * @since 3.35.0 Test ipv6 addresses.\n * @since 3.36.1 Use exception from lifterlms-tests lib.\n * @since 3.37.12 Fix errors thrown due to usage of `llms_section` instead of `section`.\n * @since 3.37.14 When testing `llms_get_post_parent_course()` added tests on other LLMS post types which are not instance of `LLMS_Post_Model`.\n * @since 4.2.0 Add tests for llms_get_completable_post_types() & llms_get_completable_taxonomies().\n * @since 4.4.0 Add tests for `llms_deprecated_function()`.\n * @since 4.4.1 Add tests for `llms_get_enrollable_post_types()` and `llms_get_enrollable_status_check_post_types()`.\n * @since 4.7.0 Add test for `llms_get_dom_document()`.\n * @since 4.10.1 Add test for possible 3rd party cpts conflicts using `llms_get_post()`.\n * @since 4.13.0 Test `llms_get_dom_document()` relying on `mb_convert_encoding()` and not.\n */\nclass LLMS_Test_Functions_Core extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_anonymize_string().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_anonymize_string() {\n\n\t\t$tests = array(\n\t\t\tarray( 'A', '*' ),\n\t\t\tarray( 'ABCD', '***D' ),\n\t\t\tarray( 'ABCDEF', '*****F' ),\n\t\t\tarray( 'ABCDEFG', '*****FG' ),\n\t\t\tarray( 'ABCDEFGHIJ', '********IJ' ),\n\t\t\tarray( 'ABCDEFGHIJK', 'AB*******JK' ),\n\t\t\tarray( 'ABCDE!FGHIJK.com', 'AB************om' ),\n\t\t\tarray( 'hello@lifterlms.com', '****o@li*********om' ),\n\t\t\tarray( '^|5M{Qx0Bq@)U*yPrgAc+({MBwSQUumW6', '^|*****************************W6' ),\n\t\t);\n\n\t\tforeach ( $tests as $i => $test ) {\n\t\t\tlist( $input, $expected ) = $test;\n\t\t\t$this->assertEquals( $expected, llms_anonymize_string( $input ), \"{$i}: {$input}\" );\n\t\t}\n\n\t\t$this->assertEquals( 'TE%%%%%%%%%%%%%%%%%%%%AR', llms_anonymize_string( 'TEST WITH ALTERNATE CHAR', '%' ) );\n\t\t$this->assertEquals( 'TEXXXXXXXXXXXXXXXXXXXXAR', llms_anonymize_string( 'TEST WITH ALTERNATE CHAR', 'X' ) );\n\n\t}\n\n\t/**\n\t * Test the llms_assoc_array_insert\n\t *\n\t * @since 3.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_assoc_array_insert() {\n\n\t\t// base array.\n\t\t$array = array(\n\t\t\t'test' => 'asrt',\n\t\t\t'tester' => 'asrtarst',\n\t\t\t'moretest_key' => 'arst',\n\t\t\t'another' => 'arst',\n\t\t);\n\n\t\t// after first item.\n\t\t$expect = array(\n\t\t\t'test' => 'asrt',\n\t\t\t'new_key' => 'item',\n\t\t\t'tester' => 'asrtarst',\n\t\t\t'moretest_key' => 'arst',\n\t\t\t'another' => 'arst',\n\t\t);\n\t\t$this->assertEquals( $expect, llms_assoc_array_insert( $array, 'test', 'new_key', 'item' ) );\n\n\t\t// add in the middle.\n\t\t$expect = array(\n\t\t\t'test'         => 'asrt',\n\t\t\t'tester'       => 'asrtarst',\n\t\t\t'new_key'      => 'item',\n\t\t\t'moretest_key' => 'arst',\n\t\t\t'another'      => 'arst',\n\t\t);\n\t\t$this->assertEquals( $expect, llms_assoc_array_insert( $array, 'tester', 'new_key', 'item' ) );\n\n\t\t// requested key doesn't exist so it'll be added to the end.\n\t\t$expect = array(\n\t\t\t'test'         => 'asrt',\n\t\t\t'tester'       => 'asrtarst',\n\t\t\t'moretest_key' => 'arst',\n\t\t\t'another'      => 'arst',\n\t\t\t'new_key'      => 'item',\n\t\t);\n\t\t$this->assertEquals( $expect, llms_assoc_array_insert( $array, 'noexist', 'new_key', 'item' ) );\n\n\t\t// after last item.\n\t\t$expect = array(\n\t\t\t'test'         => 'asrt',\n\t\t\t'new_key'      => 'item',\n\t\t\t'tester'       => 'asrtarst',\n\t\t\t'moretest_key' => 'arst',\n\t\t\t'another'      => 'arst',\n\t\t);\n\t\t$this->assertEquals( $expect, llms_assoc_array_insert( $array, 'another', 'new_key', 'item' ) );\n\n\t}\n\n\t/**\n\t * Test llms_deprecated_function()\n\t *\n\t * @since 4.4.0\n\t *\n\t * @expectedDeprecated DEPRECATED\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_deprecated_function() {\n\n\t\t// Add an action where we'll test that all our deprecation data is properly passed.\n\t\tadd_action( 'deprecated_function_run', array( $this, 'deprecated_function_run_assertions' ), 10, 3 );\n\n\t\tllms_deprecated_function( 'DEPRECATED', '999.999.999', 'REPLACEMENT' );\n\n\t\tremove_action( 'deprecated_function_run', array( $this, 'deprecated_function_run_assertions' ) );\n\n\t}\n\n\t/**\n\t * Callback method used to test `llms_deprecated_function()`.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @param string $function    Deprecated function name.\n\t * @param string $replacement Deprecated function replacement.\n\t * @param string $version     Deprecated version number.\n\t * @return void\n\t */\n\tpublic function deprecated_function_run_assertions( $function, $replacement, $version ) {\n\n\t\t// Our deprecation data should be passed to the core.\n\t\t$this->assertEquals( 'DEPRECATED', $function );\n\t\t$this->assertEquals( 'REPLACEMENT', $replacement );\n\t\t$this->assertEquals( '999.999.999', $version );\n\n\t}\n\n\t/**\n\t * Test llms_esc_and_quote_str()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_esc_and_quote_str() {\n\n\t\t$tests = array(\n\t\t\tarray( 'test', \"'test'\" ),\n\t\t\tarray( '1', \"'1'\" ),\n\t\t\tarray( 1, \"'1'\" ),\n\t\t\tarray( 0, \"'0'\" ),\n\t\t\tarray( false, \"''\" ),\n\t\t\tarray( \"\", \"''\" ),\n\t\t);\n\t\tforeach ( $tests as $test ) {\n\t\t\tlist( $input, $expected ) = $test;\n\t\t\t$this->assertEquals( $expected, llms_esc_and_quote_str( $input ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_get_completable_post_types()\n\t *\n\t * @since 4.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_completable_post_types() {\n\t\t$this->assertEquals( array( 'course', 'section', 'lesson' ), llms_get_completable_post_types() );\n\t}\n\n\t/**\n\t * Test llms_get_completable_taxonomies()\n\t *\n\t * @since 4.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_completable_taxonomies() {\n\t\t$this->assertEquals( array( 'course_track' ), llms_get_completable_taxonomies() );\n\t}\n\n\n\t/**\n\t * Test llms_get_core_supported_themes()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_core_supported_themes() {\n\n\t\t$this->assertFalse( empty( llms_get_core_supported_themes() ) );\n\t\t$this->assertTrue( is_array( llms_get_core_supported_themes() ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_date_diff()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_date_diff() {\n\n\t\t$this->assertEquals( '18 days', llms_get_date_diff( '2016-05-12', '2016-05-30' ) );\n\t\t$this->assertEquals( '1 year, 2 months', llms_get_date_diff( '2016-01-01', '2017-03-25 23:32:32' ) );\n\t\t$this->assertEquals( '10 months, 14 days', llms_get_date_diff( '2016-01-01', '2016-11-15' ) );\n\t\t$this->assertEquals( '4 years, 24 days', llms_get_date_diff( '2013-03-01', '2017-03-25' ) );\n\t\t$this->assertEquals( '3 years, 10 months', llms_get_date_diff( '2013-03-01', '2017-01-25' ) );\n\t\t$this->assertEquals( '24 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:01:25' ) );\n\t\t$this->assertEquals( '1 second', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:01:02' ) );\n\t\t$this->assertEquals( '59 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:02:00' ) );\n\t\t$this->assertEquals( '1 minute, 44 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:02:45' ) );\n\t\t$this->assertEquals( '1 minute, 14 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:02:15' ) );\n\t\t$this->assertEquals( '3 minutes, 59 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:05:00' ) );\n\t\t$this->assertEquals( '44 minutes, 33 seconds', llms_get_date_diff( '2016-05-12 01:01:01', '2016-05-12 01:45:34' ) );\n\t\t$this->assertEquals( '44 minutes, 33 seconds', llms_get_date_diff( '2016-05-12 01:45:34', '2016-05-12 01:01:01' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_dom_document()\n\t *\n\t * @since 4.7.0\n\t * @since 4.8.0 Test against HTML strings, HTML documents, strings with character entities, and strings with non-utf8 characters.\n\t * @since 4.13.0 Test `llms_get_dom_document()` relying on `mb_convert_encoding()` and not.\n\t *               Also, use `$this->assertStringContainsString()` in place of `$this->assertStringContainsString()` to get a better erro message on failures.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_dom_document() {\n\n\t\t/**\n\t\t * Array of test strings\n\t\t *\n\t\t * First value is the input string & the second value is the expected output string.\n\t\t *\n\t\t * @var array[]\n\t\t */\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\t'simple text string',\n\t\t\t\t'<p>simple text string</p>',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'<h1>html text string</h1><br><div class=\"test\"><em>wow!</em></div>',\n\t\t\t\t'<h1>html text string</h1><br><div class=\"test\"><em>wow!</em></div>',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'Ḷ𝝄𝔯𝚎ɱ ĭ𝓹ᵴǘɱ ժөḻ𝝈ɍ 𝘀𝗂ᴛ.',\n\t\t\t\t'<p>&#7734;&#120644;&#120111;&#120462;&#625; &#301;&#120057;&#7540;&#472;&#625; &#1386;&#1257;&#7739;&#120648;&#589; &#120320;&#120258;&#7451;.</p>',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'Contains &mdash; Char Codes and special – !',\n\t\t\t\t'<p>Contains &mdash; Char Codes and special &ndash; !</p>',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'<!DOCTYPE html><html lang=\"en-US\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width\" /></head><body>And &gt;>&gt; a <b>full</b> HTML docum𝞔nt!</body></html>',\n\t\t\t\t'And &gt;&gt;&gt; a <b>full</b> HTML docum&#120724;nt!',\n\t\t\t),\n\t\t);\n\n\t\t// Using `mb_convert_econding()`.\n\t\tforeach ( $tests as $test ) {\n\n\t\t\t$dom = llms_get_dom_document( $test[0] );\n\t\t\t$this->assertTrue( $dom instanceof DOMDocument, $test[1] );\n\t\t\t$this->assertStringContainsString( sprintf( '<body>%s</body></html>', $test[1] ), $dom->saveHTML() );\n\n\t\t}\n\n\t\t// Repeat the same test using \"the meta fixer\".\n\t\tadd_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false' );\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\t$dom = llms_get_dom_document( $test[0] );\n\t\t\t$this->assertTrue( $dom instanceof DOMDocument, $test[1] );\n\t\t\t$this->assertStringContainsString( sprintf( '<body>%s</body></html>', $test[1] ), $dom->saveHTML() );\n\n\t\t}\n\n\t\tremove_filter( 'llms_dom_document_use_mb_convert_encoding', '__return_false' );\n\t}\n\n\t/**\n\t * Test llms_get_engagement_triggers()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_engagement_triggers() {\n\t\t$this->assertFalse( empty( llms_get_engagement_triggers() ) );\n\t\t$this->assertTrue( is_array( llms_get_engagement_triggers() ) );\n\t}\n\n\t/**\n\t * Test llms_get_engagement_types()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_engagement_types() {\n\t\t$this->assertFalse( empty( llms_get_engagement_types() ) );\n\t\t$this->assertTrue( is_array( llms_get_engagement_types() ) );\n\t}\n\n\t/**\n\t * Test llms_get_enrollable_post_types()\n\t *\n\t * @since 4.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_enrollable_post_types() {\n\t\tforeach ( llms_get_enrollable_post_types() as $post_type ) {\n\t\t\t$this->assertTrue( is_string( $post_type ) );\n\t\t\t$this->assertTrue( post_type_exists( $post_type ) );\n\t\t}\n\t}\n\n\t/**\n\t * Test llms_get_enrollable_status_check_post_types()\n\t *\n\t * @since 4.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_enrollable_status_check_post_types() {\n\t\tforeach ( llms_get_enrollable_status_check_post_types() as $post_type ) {\n\t\t\t$this->assertTrue( is_string( $post_type ) );\n\t\t\t$this->assertTrue( post_type_exists( $post_type ) );\n\t\t}\n\t}\n\n\t/**\n\t * Test llms_get_open_registration_status()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_open_registration_status() {\n\n\t\t// No value, defaults to no.\n\t\tdelete_option( 'lifterlms_enable_myaccount_registration' );\n\t\t$this->assertEquals( 'no', llms_get_open_registration_status() );\n\n\t\t// Explicitly no.\n\t\tupdate_option( 'lifterlms_enable_myaccount_registration', 'no' );\n\t\t$this->assertEquals( 'no', llms_get_open_registration_status() );\n\n\t\t// Explicitly yes.\n\t\tupdate_option( 'lifterlms_enable_myaccount_registration', 'yes' );\n\t\t$this->assertEquals( 'yes', llms_get_open_registration_status() );\n\n\t\t// Explicitly yes but filtered off.\n\t\t$handler = function( $val ) {\n\t\t\treturn 'no';\n\t\t};\n\t\tadd_filter( 'llms_enable_open_registration', $handler );\n\t\t$this->assertEquals( 'no', llms_get_open_registration_status() );\n\t\tremove_filter( 'llms_enable_open_registration', $handler );\n\n\t}\n\n\t/**\n\t * Test the llms_get_option_page_anchor() function\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_option_page_anchor() {\n\n\t\t$id = $this->factory->post->create( array(\n\t\t\t'post_title' => 'The Page Title',\n\t\t\t'post_type'  => 'page',\n\t\t) );\n\n\t\t$option_name = 'llms_test_page_anchor';\n\n\t\t// returns empty if option isn't set.\n\t\t$this->assertEmpty( llms_get_option_page_anchor( $option_name ) );\n\n\t\tupdate_option( $option_name, $id );\n\n\t\t// title found in string.\n\t\t$this->assertTrue( false !== strpos( llms_get_option_page_anchor( $option_name ), get_the_title( $id ) ) );\n\n\t\t// URL found.\n\t\t$this->assertTrue( false !== strpos( llms_get_option_page_anchor( $option_name ), get_the_permalink( $id ) ) );\n\n\t\t// no target found.\n\t\t$this->assertTrue( false === strpos( llms_get_option_page_anchor( $option_name, false ), 'target=\"_blank\"' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_product_visibility_options()\n\t *\n\t * @since 3.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_product_visibility_options() {\n\t\t$this->assertFalse( empty( llms_get_product_visibility_options() ) );\n\t\t$this->assertTrue( is_array( llms_get_product_visibility_options() ) );\n\t}\n\n\t/**\n\t * Test llms_filter_input_sanitize_string() when the input var isn't set.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_filter_input_sanitize_string_var_not_set() {\n\n\t\t$this->assertNull( llms_filter_input_sanitize_string( INPUT_POST, uniqid( 'notset_' ) ) );\n\t\t$this->assertNull( llms_filter_input_sanitize_string( INPUT_POST, uniqid( 'notset_' ), array( FILTER_REQUIRE_ARRAY ) ) );\n\n\t}\n\n\n\t/**\n\t * Test llms_filter_input_sanitize_string() when the input var is \"empty\".\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_filter_input_sanitize_string_var_empty() {\n\n\t\t$tests = array(\n\n\t\t\tarray(\n\t\t\t\t'',\n\t\t\t\t'',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'0',\n\t\t\t\t'0',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\t\t\tlist( $input, $output ) = $test;\n\t\t\t$this->mockPostRequest( compact( 'input' ) );\n\t\t\t$this->assertEquals( $output, llms_filter_input_sanitize_string( INPUT_POST, 'input' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_filter_input_sanitize_string().\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_filter_input_sanitize_string() {\n\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\t'simple text input', // Input.\n\t\t\t\t'simple text input', // Output with quotes encoded.\n\t\t\t\t'simple text input', // Output without quotes encoded.\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'input \"with\" double quotes.',\n\t\t\t\t'input &#34;with&#34; double quotes.',\n\t\t\t\t'input \"with\" double quotes.',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t\"input 'with' single quotes.\",\n\t\t\t\t\"input &#39;with&#39; single quotes.\",\n\t\t\t\t\"input 'with' single quotes.\",\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'<a href=\"#\">Solo Tag</a>',\n\t\t\t\t'Solo Tag',\n\t\t\t\t'Solo Tag',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'Text and <a href=\"#\">a tag</a> and more text',\n\t\t\t\t'Text and a tag and more text',\n\t\t\t\t'Text and a tag and more text',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'Text and <a href=\"#\">a tag</a> and <b>more tags</b> and \"quotes\".',\n\t\t\t\t'Text and a tag and more tags and &#34;quotes&#34;.',\n\t\t\t\t'Text and a tag and more tags and \"quotes\".',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t1,\n\t\t\t\t'1',\n\t\t\t\t'1',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\ttrue,\n\t\t\t\t'1',\n\t\t\t\t'1',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'234234',\n\t\t\t\t'234234',\n\t\t\t\t'234234',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'true',\n\t\t\t\t'true',\n\t\t\t\t'true',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'false',\n\t\t\t\t'false',\n\t\t\t\t'false',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'null',\n\t\t\t\t'null',\n\t\t\t\t'null',\n\t\t\t),\n\t\t);\n\n\t\t$types = array(\n\t\t\tINPUT_GET  => 'mockGetRequest',\n\t\t\tINPUT_POST => 'mockPostRequest',\n\t\t);\n\t\tforeach ( $types as $type => $mock_func ) {\n\n\t\t\t// Setup FILTER_REQUIRE_ARRAY vars.\n\t\t\t$arr_input            = array();\n\t\t\t$arr_output           = array();\n\t\t\t$arr_output_no_encode = array();\n\n\t\t\tforeach ( $tests as $test ) {\n\n\t\t\t\tlist( $input, $output, $output_no_encode ) = $test;\n\t\t\t\t$this->$mock_func( compact( 'input' ) );\n\n\t\t\t\t// Test input with quotes encoded.\n\t\t\t\t$this->assertEquals( $output, llms_filter_input_sanitize_string( $type, 'input' ), \"Input string: {$input}\" );\n\n\t\t\t\t// Quotes not encoded.\n\t\t\t\t$this->assertEquals( $output_no_encode, llms_filter_input_sanitize_string( $type, 'input', array( FILTER_FLAG_NO_ENCODE_QUOTES ) ), \"Input string: {$input}\" );\n\n\t\t\t\t// Requesting array when no array submitted results in the filter failing.\n\t\t\t\t$this->assertFalse( llms_filter_input_sanitize_string( $type, 'input', array( FILTER_REQUIRE_ARRAY ) ), \"Input string: {$input}\" );\n\n\t\t\t\t// Add to FILTER_REQUIRE_ARRAY vars.\n\t\t\t\t$arr_input[]            = $input;\n\t\t\t\t$arr_output[]           = $output;\n\t\t\t\t$arr_output_no_encode[] = $output_no_encode;\n\n\t\t\t}\n\n\t\t\t// Test array-related input.\n\t\t\t$this->$mock_func( compact( 'arr_input' ) );\n\n\t\t\t// Array submitted but FILTER_REQUIRE_ARRAY not passed as an option.\n\t\t\t$this->assertEquals( '', llms_filter_input_sanitize_string( $type, 'arr_input' ) );\n\n\t\t\t// Array requested.\n\t\t\t$this->assertEquals( $arr_output, llms_filter_input_sanitize_string( $type, 'arr_input', array( FILTER_REQUIRE_ARRAY ) ) );\n\t\t\t$this->assertEquals( $arr_output_no_encode, llms_filter_input_sanitize_string( $type, 'arr_input', array( FILTER_REQUIRE_ARRAY, FILTER_FLAG_NO_ENCODE_QUOTES ) ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_find_coupon()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_find_coupon() {\n\n\t\t// create a coupon.\n\t\t$id = $this->factory->post->create( array(\n\t\t\t'post_title' => 'coopond',\n\t\t\t'post_type'  => 'llms_coupon',\n\t\t) );\n\t\t$this->assertEquals( $id, llms_find_coupon( 'coopond' ) );\n\n\t\t// create a dup.\n\t\t$dup = $this->factory->post->create( array(\n\t\t\t'post_title' => 'coopond',\n\t\t\t'post_type'  => 'llms_coupon',\n\t\t) );\n\t\t$this->assertEquals( $dup, llms_find_coupon( 'coopond' ) );\n\n\t\t// test dupcheck.\n\t\t$this->assertEquals( $id, llms_find_coupon( 'coopond', $dup ) );\n\n\t\t// delete the coupon.\n\t\twp_delete_post( $id );\n\t\twp_delete_post( $dup );\n\t\t$this->assertEmpty( llms_find_coupon( 'coopond' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_enrolled_students()\n\t *\n\t * @since 3.6.0\n\t * @return void\n\t */\n\tfunction test_llms_get_enrolled_students() {\n\n\t\t$course_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'course',\n\t\t) );\n\n\t\t$students = $this->factory->user->create_many( 25, array( 'role' => 'student' ) );\n\t\t$students_copy = $students;\n\t\tforeach ( $students as $student_id ) {\n\t\t\t$student = new LLMS_Student( $student_id );\n\t\t\t$student->enroll( $course_id );\n\t\t}\n\n\t\t// test basic enrollment query passing in a string.\n\t\t$this->assertEquals( $students, llms_get_enrolled_students( $course_id, 'enrolled', 50, 0 ) );\n\t\t// test basic enrollment query passing in an array.\n\t\t$this->assertEquals( $students, llms_get_enrolled_students( $course_id, array( 'enrolled' ), 50, 0 ) );\n\n\t\t// test pagination.\n\t\t$this->assertEquals( array_splice( $students, 0, 10 ), llms_get_enrolled_students( $course_id, 'enrolled', 10, 0 ) );\n\t\t$this->assertEquals( array_splice( $students, 0, 10 ), llms_get_enrolled_students( $course_id, 'enrolled', 10, 10 ) );\n\t\t$this->assertEquals( $students, llms_get_enrolled_students( $course_id, 'enrolled', 10, 20 ) );\n\n\t\t// should be no one expired.\n\t\t$this->assertEquals( array(), llms_get_enrolled_students( $course_id, 'expired', 10, 0 ) );\n\n\t\t// sleeping makes unenrollment tests work.\n\t\tsleep( 1 );\n\n\t\t$i = 0;\n\t\t$expired = array();\n\t\twhile ( $i < 5 ) {\n\t\t\t$student = new LLMS_Student( $students_copy[ $i ] );\n\t\t\t$student->unenroll( $course_id, 'any', 'expired' );\n\t\t\t$expired[] = $students_copy[ $i ];\n\t\t\t$i++;\n\t\t}\n\n\t\t// test expired alone.\n\t\t$this->assertEquals( $expired, llms_get_enrolled_students( $course_id, 'expired', 10, 0 ) );\n\n\t\t// test multiple statuses.\n\t\t$this->assertEquals( $students_copy, llms_get_enrolled_students( $course_id, array( 'enrolled', 'expired' ), 50, 0 ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_enrollment_statuses()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_enrollment_statuses() {\n\t\t$this->assertFalse( empty( llms_get_enrollment_statuses() ) );\n\t\t$this->assertTrue( is_array( llms_get_enrollment_statuses() ) );\n\t}\n\n\t/**\n\t * Test llms_get_enrollment_status_name()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_enrollment_status_name() {\n\t\t$this->assertNotEquals( 'asrt', llms_get_enrollment_status_name( 'cancelled' ) );\n\t\t$this->assertNotEquals( 'cancelled', llms_get_enrollment_status_name( 'Cancelled' ) );\n\t\t$this->assertEquals( 'Cancelled', llms_get_enrollment_status_name( 'cancelled' ) );\n\t\t$this->assertEquals( 'Cancelled', llms_get_enrollment_status_name( 'Cancelled' ) );\n\t\t$this->assertEquals( 'wut', llms_get_enrollment_status_name( 'wut' ) );\n\t}\n\n\t/**\n\t * Test llms_get_ip_address()\n\t *\n\t * @since 3.6.0\n\t * @since 3.35.0 Test sanitization and ipv6 addresses.\n\t * @since 7.0.0 Reset the original `$_SERVER['REMOTE_ADDR']` value on test completion.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_ip_address() {\n\n\t\t$orig_ip = $_SERVER['REMOTE_ADDR'];\t\n\n\t\t$_SERVER['REMOTE_ADDR'] = '127.0.0.1';\n\t\t$this->assertEquals( '127.0.0.1', llms_get_ip_address() );\n\n\t\t$_SERVER['REMOTE_ADDR'] = '::1';\n\t\t$this->assertEquals( '::1', llms_get_ip_address() );\n\t\tunset( $_SERVER['REMOTE_ADDR'] );\n\n\t\t$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1, 192.168.1.1, 192.168.1.5';\n\t\t$this->assertEquals( '127.0.0.1', llms_get_ip_address() );\n\n\t\t$_SERVER['HTTP_X_FORWARDED_FOR'] = '::1, ::2';\n\t\t$this->assertEquals( '::1', llms_get_ip_address() );\n\t\tunset( $_SERVER['HTTP_X_FORWARDED_FOR'] );\n\n\t\t$_SERVER['HTTP_X_REAL_IP'] = '127.0.0.1';\n\t\t$this->assertEquals( '127.0.0.1', llms_get_ip_address() );\n\n\t\t$_SERVER['HTTP_X_REAL_IP'] = '::1';\n\t\t$this->assertEquals( '::1', llms_get_ip_address() );\n\t\tunset( $_SERVER['HTTP_X_REAL_IP'] );\n\n\t\t$this->assertEquals( '', llms_get_ip_address() );\n\n\t\t$_SERVER['REMOTE_ADDR'] = '127\\.0.0.1';\n\t\t$this->assertEquals( '127.0.0.1', llms_get_ip_address() );\n\n\t\t$_SERVER['REMOTE_ADDR'] = '127\\\\/\\/\\/\\.0.0.1';\n\t\t$this->assertEquals( '', llms_get_ip_address() );\n\n\t\t// Reset.\n\t\t$_SERVER['REMOTE_ADDR'] = $orig_ip;\n\n\t}\n\n\t/**\n\t * Test llms_get_post()\n\t *\n\t * @since 3.3.1\n\t * @since 3.16.11 Unknown.\n\t * @since 3.37.12 Fix errors thrown due to usage of `llms_section` instead of `section`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post() {\n\n\t\t$types = array(\n\t\t\t'LLMS_Access_Plan' => 'llms_access_plan',\n\t\t\t'LLMS_Coupon'      => 'llms_coupon',\n\t\t\t'LLMS_Course'      => 'course',\n\t\t\t'LLMS_Lesson'      => 'lesson',\n\t\t\t'LLMS_Membership'  => 'llms_membership',\n\t\t\t'LLMS_Order'       => 'llms_order',\n\t\t\t'LLMS_Quiz'        => 'llms_quiz',\n\t\t\t'LLMS_Question'    => 'llms_question',\n\t\t\t'LLMS_Section'     => 'section',\n\t\t\t'LLMS_Transaction' => 'llms_transaction',\n\t\t);\n\n\t\tforeach ( $types as $class => $type ) {\n\n\t\t\t$id = $this->factory->post->create( array(\n\t\t\t\t'post_type' => $type,\n\t\t\t) );\n\t\t\t$this->assertInstanceOf( $class, llms_get_post( $id ) );\n\n\t\t}\n\n\t\t$this->assertInstanceOf( 'WP_Post', llms_get_post( $this->factory->post->create(), 'post' ) );\n\t\t$this->assertNull( llms_get_post( 'fail' ) );\n\t\t$this->assertNull( llms_get_post( 0 ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_post() with post types which don't have to be confused with LifterLMS post types\n\t *\n\t * @since 4.10.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_no_conflicts() {\n\n\t\t$types = array(\n\t\t\t'LLMS_Events'      => 'events',\n\t\t\t'LLMS_Certificate' => 'certificate',\n\t\t\t'LLMS_Transaction' => 'transaction',\n\t\t);\n\n\t\tforeach ( $types as $class => $type ) {\n\t\t\tregister_post_type( $type );\n\t\t\t$id = $this->factory->post->create( array(\n\t\t\t\t'post_type' => $type,\n\t\t\t) );\n\n\t\t\t$this->assertNotInstanceOf( $class, llms_get_post( $id ) );\n\t\t\tunregister_post_type( $type );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test `llms_get_post_parent_course()`\n\t *\n\t * @since 3.6.0\n\t * @since 3.37.14 Added tests on other LLMS post types which are not instance of `LLMS_Post_Model`.\n\t * @since 6.0.0 Replaced use of the deprecated `LLMS_Certificate` class, an LLMS post type class that is NOT\n\t *              extended from `LLMS_Post_Model`, with `LLMS_Email`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_post_parent_course() {\n\n\t\t$course = new LLMS_Course( 'new', 'title' );\n\t\t$section = new LLMS_Section( 'new', array(\n\t\t\t'post_title' => 'section',\n\t\t\t'meta_input' => array(\n\t\t\t\t'_llms_parent_course' => $course->get( 'id' )\n\t\t\t),\n\t\t) );\n\t\t$lesson = new LLMS_Lesson( 'new', array(\n\t\t\t'post_title' => 'lesson',\n\t\t\t'meta_input' => array(\n\t\t\t\t'_llms_parent_course' => $course->get( 'id' ),\n\t\t\t\t'_llms_parent_section' => $section->get( 'id' ),\n\t\t\t),\n\t\t) );\n\n\t\tforeach ( array( $section, $lesson ) as $obj ) {\n\n\t\t\t$post = get_post( $obj->get( 'id' ) );\n\n\t\t\t// pass in post id.\n\t\t\t$this->assertEquals( $course, llms_get_post_parent_course( $post->ID ) );\n\n\t\t\t// pass in an object.\n\t\t\t$this->assertEquals( $course, llms_get_post_parent_course( $post ) );\n\n\t\t}\n\n\t\t// other non lms post types don't have a parent course.\n\t\t$reg_post = $this->factory->post->create();\n\t\t$this->assertNull( llms_get_post_parent_course( $reg_post ) );\n\n\t\t// Make sure an LLMS post type, which is not an instance of `LLMS_Post_Model` doesn't have a parent course.\n\t\t// and no fatal errors are produced.\n\t\t$non_model_post = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_email',\n\t\t\t)\n\t\t);\n\t\t$this->assertNull( llms_get_post_parent_course( $non_model_post ) );\n\t}\n\n\n\t/**\n\t * Test llms_get_transaction_statuses()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_transaction_statuses() {\n\t\t$this->assertFalse( empty( llms_get_transaction_statuses() ) );\n\t\t$this->assertTrue( is_array( llms_get_transaction_statuses() ) );\n\t}\n\n\t/**\n\t * Test llms_is_site_https()\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_site_https() {\n\t\tupdate_option( 'home', 'https://is.ssl' );\n\t\t$this->assertTrue( llms_is_site_https() );\n\n\t\tupdate_option( 'home', 'http://is.ssl' );\n\t\t$this->assertFalse( llms_is_site_https() );\n\t}\n\n\t/**\n\t * Test the llms_parse_bool function\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_parse_bool() {\n\n\t\t$true = array( 'yes', 'on', true, 1, 'true', '1' );\n\n\t\tforeach ( $true as $val ) {\n\t\t\t$this->assertTrue( llms_parse_bool( $val ) );\n\t\t}\n\n\t\t$false = array( 'no', 'off', false, 0, 'false', 'something', '', null, '0', array(), array( 'ast' ), array( true ) );\n\n\t\tforeach ( $false as $val ) {\n\t\t\t$this->assertFalse( llms_parse_bool( $val ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_php_error_constant_to_code()\n\t *\n\t * @since 4.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_php_error_constant_to_code() {\n\n\t\t$errors = array(\n\t\t\tE_ERROR             => 'E_ERROR',\n\t\t\tE_WARNING           => 'E_WARNING',\n\t\t\tE_PARSE             => 'E_PARSE',\n\t\t\tE_NOTICE            => 'E_NOTICE',\n\t\t\tE_CORE_ERROR        => 'E_CORE_ERROR',\n\t\t\tE_CORE_WARNING      => 'E_CORE_WARNING',\n\t\t\tE_COMPILE_ERROR     => 'E_COMPILE_ERROR',\n\t\t\tE_COMPILE_WARNING   => 'E_COMPILE_WARNING',\n\t\t\tE_USER_ERROR        => 'E_USER_ERROR',\n\t\t\tE_USER_WARNING      => 'E_USER_WARNING',\n\t\t\tE_USER_NOTICE       => 'E_USER_NOTICE',\n\t\t\tE_STRICT            => 'E_STRICT',\n\t\t\tE_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',\n\t\t\tE_DEPRECATED        => 'E_DEPRECATED',\n\t\t\tE_USER_DEPRECATED   => 'E_USER_DEPRECATED',\n\t\t\t9999                => 9999,\n\t\t);\n\n\t\tforeach ( $errors as $in => $out ) {\n\t\t\t$this->assertEquals( $out, llms_php_error_constant_to_code( $in ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_redirect_and_exit() func with safe on\n\t *\n\t * @since 3.19.4\n\t * @since 3.34.0 Use exception from lifterlms-tests lib.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_redirect_and_exit_safe_on() {\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( 'https://lifterlms.com [302] YES' );\n\t\tllms_redirect_and_exit( 'https://lifterlms.com' );\n\n\t}\n\n\t/**\n\t * Test llms_redirect_and_exit() func with safe on\n\t *\n\t * @since 3.36.1 Use exception from lifterlms-tests lib.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_redirect_and_exit_safe_off() {\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( 'https://lifterlms.com [302] NO' );\n\t\tllms_redirect_and_exit( 'https://lifterlms.com', array( 'safe' => false ) );\n\n\t}\n\n\t/**\n\t * Test llms_redirect_and_exit() func with safe custom status\n\t *\n\t * @since 3.36.1 Use exception from lifterlms-tests lib.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_redirect_and_exit_safe_status() {\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( 'https://lifterlms.com [301] YES' );\n\t\tllms_redirect_and_exit( 'https://lifterlms.com', array( 'status' => 301 ) );\n\n\t}\n\n\t/**\n\t * Test llms_strip_prefixes().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_strip_prefixes() {\n\n\t\t$tests = array(\n\t\t\t// Input string, prefixes list, expected output string.\n\n\t\t\t// Default behaviors.\n\t\t\tarray( 'llms_test', null, 'test' ),\n\t\t\tarray( 'llms_test', array(), 'test' ),\n\t\t\tarray( 'lifterlms_test', null, 'test' ),\n\t\t\tarray( 'lifterlms_test', array(), 'test' ),\n\t\t\tarray( 'llms-test', null, 'test' ),\n\t\t\tarray( 'llms-test', array(), 'test' ),\n\t\t\tarray( 'lifterlms-test', null, 'test' ),\n\t\t\tarray( 'lifterlms-test', array(), 'test' ),\n\n\n\t\t\t// Only strip from the start of the string.\n\t\t\tarray( 'test_llms_test', null, 'test_llms_test' ),\n\t\t\tarray( 'test_lifterlms_test', null, 'test_lifterlms_test' ),\n\t\t\tarray( 'test_llms_', null, 'test_llms_' ),\n\t\t\tarray( 'test_lifterlms_', null, 'test_lifterlms_' ),\n\t\t\tarray( 'test_llms-', null, 'test_llms-' ),\n\t\t\tarray( 'test_lifterlms-', null, 'test_lifterlms-' ),\n\n\t\t\t// Don't strip multiple prefixes.\n\t\t\tarray( 'llms_lifterlms_test', null, 'lifterlms_test' ),\n\t\t\tarray( 'lifterlms_llms_test', null, 'llms_test' ),\n\t\t\tarray( 'llms-lifterlms-test', null, 'lifterlms-test' ),\n\t\t\tarray( 'lifterlms-llms-test', null, 'llms-test' ),\n\n\t\t\t// Custom prefix.\n\t\t\tarray( 'test_llms_test', array( 'test_' ), 'llms_test' ),\n\t\t\tarray( 'test_llms_test', array( 'test_', 'llms_' ), 'llms_test' ),\n\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\t\t\tlist( $input, $prefixes, $expect ) = $data;\n\t\t\t$this->assertEquals( $expect, llms_strip_prefixes( $input, $prefixes ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_trim_string()\n\t *\n\t * @since 3.3.1\n\t * @since 3.6.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_trim_string() {\n\n\t\t$this->assertEquals( 'yasssss', llms_trim_string( 'yasssss' ) );\n\t\t$this->assertEquals( 'y...',    llms_trim_string( 'yasssss', 4 ) );\n\t\t$this->assertEquals( 'ya.',     llms_trim_string( 'yasssss', 3, '.' ) );\n\t\t$this->assertEquals( 'yassss$', llms_trim_string( 'yassss$s', 7, '' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-currency.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Currency functions\n *\n * @group functions\n * @group currency\n *\n * @since 3.24.1\n * @since 5.0.0 Moved country-related function tests to locale functions test file.\n * @since 6.0.0 Removed testing of the removed `llms_format_decimal()` function.\n */\nclass LLMS_Test_Functions_Currency extends LLMS_UnitTestCase {\n\n\t/**\n\t * test the get_lifterlms_currency() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_currency() {\n\n\t\t// test default\n\t\t$this->assertEquals( 'USD', get_lifterlms_currency() );\n\n\t\t// test lifterlms_country option\n\t\tupdate_option( 'lifterlms_currency', 'GBP' );\n\t\t$this->assertEquals( 'GBP', get_lifterlms_currency() );\n\n\t\t// test that the lifterlms_currency filter is applied\n\t\tadd_filter( 'lifterlms_currency', function() {\n\t\t\treturn 'EUR';\n\t\t} );\n\t\t$this->assertEquals( 'EUR', get_lifterlms_currency() );\n\t}\n\n\t/**\n\t * test the get_lifterlms_currency() function\n\t *\n\t * @since 3.24.1\n\t * @since 5.0.0 Update language.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_currency_name() {\n\n\t\t// test default\n\t\t$this->assertEquals( 'United States Dollar', get_lifterlms_currency_name() );\n\n\t\t// test $currency argument\n\t\t$this->assertEquals( 'British Pound', get_lifterlms_currency_name( 'GBP' ) );\n\n\t\t// test that the lifterlms_currency_name filter is applied\n\t\tadd_filter( 'lifterlms_currency_name', function( $name, $currency ) {\n\t\t\treturn sprintf( '%s (%s)', $name, $currency );\n\t\t}, 10, 2 );\n\t\t$this->assertEquals( 'United States Dollar (USD)', get_lifterlms_currency_name() );\n\t}\n\n\t/**\n\t * test the get_lifterlms_currencies() function\n\t *\n\t * @since 3.24.1\n\t * @since 5.0.0 Update test to ensure result matches source data array.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_currencies() {\n\n\t\t$expected = include LLMS_PLUGIN_DIR . 'languages/currencies.php';\n\t\t$this->assertEquals( $expected, get_lifterlms_currencies() );\n\n\t}\n\n\t/**\n\t * test the get_lifterlms_currency_symbol() function\n\t *\n\t * @since 3.24.1\n\t * @since 5.0.0 Update character entity used for the pound.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_currency_symbol() {\n\n\t\t// test default\n\t\t$this->assertEquals( '&#36;', get_lifterlms_currency_symbol() );\n\n\t\t// test $currency argument\n\t\t$this->assertEquals( '&#163;', get_lifterlms_currency_symbol( 'GBP' ) );\n\n\t\t// test that the lifterlms_currency_symbol filter is applied\n\t\tadd_filter( 'lifterlms_currency_symbol', function( $currency_symbol, $currency ) {\n\t\t\treturn sprintf( '%s (%s)', $currency_symbol, $currency );\n\t\t}, 10, 2 );\n\t\t$this->assertEquals( '&#36; (USD)', get_lifterlms_currency_symbol() );\n\t}\n\n\t/**\n\t * test the get_lifterlms_currency_symbol() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_decimals() {\n\n\t\t// test default\n\t\t$this->assertEquals( 2, get_lifterlms_decimals() );\n\n\t\t// test lifterlms_decimals option\n\t\tupdate_option( 'lifterlms_decimals', 3 );\n\t\t$this->assertEquals( 3, get_lifterlms_decimals() );\n\n\t\t// test that the lifterlms_decimals filter is applied\n\t\tadd_filter( 'lifterlms_decimals', function() {\n\t\t\treturn 4;\n\t\t} );\n\t\t$this->assertEquals( 4, get_lifterlms_decimals() );\n\t}\n\n\t/**\n\t * test the get_lifterlms_decimal_separator() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_decimal_separator() {\n\n\t\t// test default\n\t\t$this->assertEquals( '.', get_lifterlms_decimal_separator() );\n\n\t\t// test lifterlms_decimal_separator option\n\t\tupdate_option( 'lifterlms_decimal_separator', ',' );\n\t\t$this->assertEquals( ',', get_lifterlms_decimal_separator() );\n\n\t\t// test that the lifterlms_decimal_separator filter is applied\n\t\tadd_filter( 'lifterlms_decimal_separator', function() {\n\t\t\treturn ':';\n\t\t} );\n\t\t$this->assertEquals( ':', get_lifterlms_decimal_separator() );\n\t}\n\n\t/**\n\t * test the get_lifterlms_trim_zero_decimals() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_trim_zero_decimals() {\n\n\t\t// test default\n\t\t$this->assertEquals( 'no', get_lifterlms_trim_zero_decimals() );\n\n\t\t// test lifterlms_trim_zero_decimals option\n\t\tupdate_option( 'lifterlms_trim_zero_decimals', 'yes' );\n\t\t$this->assertEquals( 'yes', get_lifterlms_trim_zero_decimals() );\n\n\t\t// test that the lifterlms_trim_zero_decimals filter is applied\n\t\tadd_filter( 'lifterlms_trim_zero_decimals', function() {\n\t\t\treturn 'no';\n\t\t} );\n\t\t$this->assertEquals( 'no', get_lifterlms_trim_zero_decimals() );\n\t}\n\n\t/**\n\t * test the get_lifterlms_price_format() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_price_format() {\n\n\t\t// test default\n\t\t$this->assertEquals( '%1$s%2$s', get_lifterlms_price_format() );\n\n\t\t// test right option\n\t\tupdate_option( 'lifterlms_currency_position', 'right' );\n\t\t$this->assertEquals( '%2$s%1$s', get_lifterlms_price_format() );\n\n\t\t// test left_space option\n\t\tupdate_option( 'lifterlms_currency_position', 'left_space' );\n\t\t$this->assertEquals( '%1$s&nbsp;%2$s', get_lifterlms_price_format() );\n\n\t\t// test right_space option\n\t\tupdate_option( 'lifterlms_currency_position', 'right_space' );\n\t\t$this->assertEquals( '%2$s&nbsp;%1$s', get_lifterlms_price_format() );\n\n\t\t// test that the lifterlms_price_format filter is applied\n\t\tadd_filter( 'lifterlms_price_format', function( $format, $pos ) {\n\t\t\treturn sprintf( '%s (%s)', $format, $pos );\n\t\t}, 10, 2 );\n\t\t$this->assertEquals( '%2$s&nbsp;%1$s (right_space)', get_lifterlms_price_format() );\n\t}\n\n\t/**\n\t * test the get_lifterlms_thousand_separator() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_thousand_separator() {\n\n\t\t// test default\n\t\t$this->assertEquals( ',', get_lifterlms_thousand_separator() );\n\n\t\t// test lifterlms_thousand_separator option\n\t\tupdate_option( 'lifterlms_thousand_separator', '.' );\n\t\t$this->assertEquals( '.', get_lifterlms_thousand_separator() );\n\n\t\t// test that the lifterlms_thousand_separator filter is applied\n\t\tadd_filter( 'lifterlms_thousand_separator', function() {\n\t\t\treturn ':';\n\t\t} );\n\t\t$this->assertEquals( ':', get_lifterlms_thousand_separator() );\n\t}\n\n\tpublic function test_llms_get_currency_symbols() {\n\n\t\t$expected = include LLMS_PLUGIN_DIR . 'languages/currency-symbols.php';\n\t\t$res      = llms_get_currency_symbols();\n\t\t$this->assertEquals( $expected, $res );\n\n\t\t// Make sure entities decode to what's expected.\n\t\t$this->assertEquals( '$', html_entity_decode( $res['USD'] ) );\n\t\t$this->assertEquals( '£', html_entity_decode( $res['GBP'] ) );\n\t\t$this->assertEquals( '€', html_entity_decode( $res['EUR'] ) );\n\n\t\t// Text symbols.\n\t\t$this->assertEquals( 'P', $res['BWP'] );\n\t\t$this->assertEquals( 'CHF', $res['CHF'] );\n\n\t}\n\n\t/**\n\t * test the llms_price() function\n\t *\n\t * @since 3.24.1\n\t * @since 5.0.0 Update currency symbol entities.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_price() {\n\n\t\t// test default positive price\n\t\t$this->assertEquals( '<span class=\"lifterlms-price\"><span class=\"llms-price-currency-symbol\">&#36;</span>2.99</span>', llms_price( 2.99 ) );\n\n\t\t// test default negative price\n\t\t$this->assertEquals( '<span class=\"lifterlms-price\">-<span class=\"llms-price-currency-symbol\">&#36;</span>2.99</span>', llms_price( -2.99 ) );\n\n\t\t// test that raw_lifterlms_price filter is applied\n\t\tadd_filter( 'raw_lifterlms_price', function( $price ) {\n\t\t\treturn $price * 10;\n\t\t} );\n\t\t$this->assertEquals( '<span class=\"lifterlms-price\"><span class=\"llms-price-currency-symbol\">&#36;</span>29.90</span>', llms_price( 2.99 ) );\n\t\tremove_all_filters( 'raw_lifterlms_price' );\n\n\t\t// test that formatted_lifterlms_price filter is applied\n\t\tadd_filter( 'formatted_lifterlms_price', function( $formatted_price, $price, $decimals, $decimal_separator, $thousand_separator ) {\n\t\t\t$price = number_format( $price, $decimals, $decimal_separator, $thousand_separator );\n\t\t\treturn round( $formatted_price );\n\t\t}, 10, 5 );\n\t\t$this->assertEquals( '<span class=\"lifterlms-price\"><span class=\"llms-price-currency-symbol\">&#36;</span>3</span>', llms_price( 2.99 ) );\n\t\tremove_all_filters( 'formatted_lifterlms_price' );\n\n\t\t// test that llms_price filter is applied\n\t\tadd_filter( 'llms_price', function( $r, $price, $args ) {\n\t\t\treturn $price;\n\t\t}, 10, 3 );\n\t\t$this->assertEquals( '2.99', llms_price( 2.99 ) );\n\t\tremove_all_filters( 'llms_price' );\n\n\t\t// test with custom options\n\t\tupdate_option( 'lifterlms_decimal_separator', ',' );\n\t\tupdate_option( 'lifterlms_decimals', 3 );\n\t\tupdate_option( 'lifterlms_currency_position', 'left_space' );\n\t\tupdate_option( 'lifterlms_thousand_separator', '.' );\n\t\tupdate_option( 'lifterlms_trim_zero_decimals', 'yes' );\n\t\t$this->assertEquals( '<span class=\"lifterlms-price\"><span class=\"llms-price-currency-symbol\">&#36;</span>&nbsp;1.002</span>', llms_price( 1002.00 ) );\n\n\t\t// test with custom options via $args argument\n\t\t$args = array(\n\t\t\t'currency' => 'GBP',\n\t\t\t'decimal_separator' => '.',\n\t\t\t'decimals' => 2,\n\t\t\t'format' => '<div>%s</div>%s',\n\t\t\t'thousand_separator' => ',',\n\t\t\t'trim_zeros' => 'no',\n\t\t);\n\t\t$this->assertEquals( '<span class=\"lifterlms-price\"><div><span class=\"llms-price-currency-symbol\">&#163;</span></div>1,003.00</span>', llms_price( '1002.999', $args ) );\n\n\t\t// test with custom arguments via llms_price_args filter\n\t\tadd_filter( 'llms_price_args', function() {\n\t\t\treturn array(\n\t\t\t\t'currency' => 'EUR',\n\t\t\t\t'decimal_separator' => ':',\n\t\t\t\t'decimals' => 1,\n\t\t\t\t'format' => '%s - %s',\n\t\t\t\t'thousand_separator' => '.',\n\t\t\t\t'trim_zeros' => 'no',\n\t\t\t);\n\t\t} );\n\t\t$this->assertEquals( '<span class=\"lifterlms-price\"><span class=\"llms-price-currency-symbol\">&#8364;</span> - 1.003:0</span>', llms_price( '1002.999', $args ) );\n\t}\n\n\t/**\n\t * test the llms_price_raw() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_price_raw() {\n\n\t\t// test default case\n\t\t$this->assertEquals( '$2.99', llms_price_raw( 2.99 ) );\n\n\t\t// test with $args\n\t\t$args = array(\n\t\t\t'currency' => 'GBP',\n\t\t\t'decimal_separator' => '.',\n\t\t\t'decimals' => 2,\n\t\t\t'format' => '<div>%s</div>%s',\n\t\t\t'thousand_separator' => ',',\n\t\t\t'trim_zeros' => 'no',\n\t\t);\n\t\t$this->assertEquals( '£1,003.00', llms_price_raw( 1002.999, $args ) );\n\t}\n\n\t/**\n\t * test the llms_trim_zeros() function\n\t *\n\t * @since 3.24.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_trim_zeros() {\n\n\t\t$this->assertEquals( '2', llms_trim_zeros( '2.00' ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-deprecated.php",
    "content": "<?php\n/**\n * Tests for deprecated functions\n *\n * @group functions\n * @group deprecated\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Functions_Deprecated extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test earned engagement deprecated keys against an invalid post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_engagement_handle_deprecated_meta_keys_invalid_post() {\n\n\t\t$attachment   = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$post_title   = 'Actual Title';\n\t\t$post_content = wpautop( 'Actual Content' );\n\t\t$meta_input = array(\n\t\t\t'_llms_achievement_content' => 'Not the Content',\n\t\t\t'_llms_certificate_content' => 'Not the Content\t',\n\t\t\t'_llms_achievement_image' => 123,\n\t\t\t'_llms_certificate_image' => 123,\n\t\t\t'_llms_achievement_title' => 'Not the Title',\n\t\t\t'_llms_certificate_title' => 'Not the Title\t',\n\t\t\t'_llms_achievement_template' => 456,\n\t\t\t'_llms_certificate_template' => 456,\n\t\t);\n\n\t\t$post = $this->factory->post->create( compact( 'meta_input', 'post_title', 'post_content' ) );\n\t\tset_post_thumbnail( $post, $attachment );\n\n\t\tforeach ( $meta_input as $key => $value ) {\n\t\t\t$this->assertEquals( $value, get_post_meta( $post, $key, true ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_my_certificate deprecated keys\n\t *\n\t * @since 6.0.0\n\t *\n\t * @expectedDeprecated LLMS_User_Certificate meta key &#039;certificate_content&#039;\n\t * @expectedDeprecated LLMS_User_Certificate meta key &#039;certificate_image&#039;\n\t * @expectedDeprecated LLMS_User_Certificate meta key &#039;certificate_title&#039;\n\t * @expectedDeprecated LLMS_User_Certificate meta key &#039;certificate_template&#039;\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_engagement_handle_deprecated_meta_keys_certificate_post() {\n\n\t\t$attachment   = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$post_type    = 'llms_my_certificate';\n\t\t$post_title   = 'Actual Title';\n\t\t$post_content = wpautop( 'Actual Content' );\n\t\t$post_parent  = $this->factory->post->create();\n\t\t$meta_input   = array(\n\t\t\t'_llms_certificate_content'  => 'Not the Content',\n\t\t\t'_llms_certificate_image'    => $attachment + 1,\n\t\t\t'_llms_certificate_title'    => 'Not the Title',\n\t\t\t'_llms_certificate_template' => $post_parent + 1,\n\t\t);\n\n\t\t$post = $this->factory->post->create( compact( 'meta_input', 'post_title', 'post_type', 'post_parent', 'post_content' ) );\n\t\tset_post_thumbnail( $post, $attachment );\n\n\t\t// Via `get_post_meta()`.\n\t\t$this->assertEquals( $post_content, get_post_meta( $post, '_llms_certificate_content', true ) );\n\t\t$this->assertEquals( $post_title, get_post_meta( $post, '_llms_certificate_title', true ) );\n\t\t$this->assertEquals( $attachment, get_post_meta( $post, '_llms_certificate_image', true ) );\n\t\t$this->assertEquals( $post_parent, get_post_meta( $post, '_llms_certificate_template', true ) );\n\n\t\t// Via LLMS_User_Certificate->get().\n\t\t$obj = new LLMS_User_Certificate( $post );\n\t\t$this->assertEquals( $post_content, $obj->get( 'content', true ) );\n\t\t$this->assertEquals( $post_title, $obj->get( 'certificate_title' ) );\n\t\t$this->assertEquals( $attachment, $obj->get( 'certificate_image' ) );\n\t\t$this->assertEquals( $post_parent, $obj->get( 'certificate_template' ) );\n\n\t}\n\n\t/**\n\t * Test llms_my_achievement deprecated keys\n\t *\n\t * @since 6.0.0\n\t *\n\t * @expectedDeprecated LLMS_User_Achievement meta key &#039;achievement_content&#039;\n\t * @expectedDeprecated LLMS_User_Achievement meta key &#039;achievement_image&#039;\n\t * @expectedDeprecated LLMS_User_Achievement meta key &#039;achievement_title&#039;\n\t * @expectedDeprecated LLMS_User_Achievement meta key &#039;achievement_template&#039;\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_engagement_handle_deprecated_meta_keys_achievement_post() {\n\n\t\t$attachment   = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$post_type    = 'llms_my_achievement';\n\t\t$post_title   = 'Actual Title';\n\t\t$post_content = wpautop( 'Actual Content' );\n\t\t$post_parent  = $this->factory->post->create();\n\t\t$meta_input   = array(\n\t\t\t'_llms_achievement_content'  => 'Not the Content',\n\t\t\t'_llms_achievement_image'    => $attachment + 1,\n\t\t\t'_llms_achievement_title'    => 'Not the Title',\n\t\t\t'_llms_achievement_template' => $post_parent + 1,\n\t\t);\n\n\t\t$post = $this->factory->post->create( compact( 'meta_input', 'post_title', 'post_type', 'post_parent', 'post_content' ) );\n\t\tset_post_thumbnail( $post, $attachment );\n\n\t\t// Via `get_post_meta()`.\n\t\t$this->assertEquals( $post_content, get_post_meta( $post, '_llms_achievement_content', true ) );\n\t\t$this->assertEquals( $post_title, get_post_meta( $post, '_llms_achievement_title', true ) );\n\t\t$this->assertEquals( $attachment, get_post_meta( $post, '_llms_achievement_image', true ) );\n\t\t$this->assertEquals( $post_parent, get_post_meta( $post, '_llms_achievement_template', true ) );\n\n\t\t// Via LLMS_User_Certificate->get().\n\t\t$obj = new LLMS_User_Certificate( $post );\n\t\t$this->assertEquals( $post_content, $obj->get( 'content', true ) );\n\t\t$this->assertEquals( $post_title, $obj->get( 'achievement_title' ) );\n\t\t$this->assertEquals( $attachment, $obj->get( 'achievement_image' ) );\n\t\t$this->assertEquals( $post_parent, $obj->get( 'achievement_template' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-forms.php",
    "content": "<?php\n/**\n * Test form-related functions\n *\n * @package LifterLMS/Tests\n *\n * @group form_functions\n * @group forms\n * @group functions\n *\n * @since 5.0.0\n * @since 5.10.0 Added tests for form title in free access plans checkout.\n * @version 5.10.0\n */\nclass LLMS_Test_Functions_Forms extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_get_form() function.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_form() {\n\n\t\t$this->assertFalse( llms_get_form( 'fake' ) );\n\t\t$this->assertFalse( llms_get_form( 'checkout' ) );\n\n\t\tLLMS_Forms::instance()->create( 'checkout' );\n\t\t$this->assertTrue( is_a( llms_get_form( 'checkout' ), 'WP_Post' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_form_html() function.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_form_html() {\n\n\t\t$this->assertEquals( '', llms_get_form_html( 'fake' ) );\n\t\t$this->assertEquals( '', llms_get_form_html( 'checkout' ) );\n\n\t\tLLMS_Forms::instance()->create( 'checkout' );\n\t\t$this->assertTrue( '' !== llms_get_form_html( 'checkout' ) );\n\n\t}\n\n\t/**\n\t * test llms_get_form_title() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_form_title() {\n\n\t\t$this->assertEquals( '', llms_get_form_title( 'fake' ) );\n\t\t$this->assertEquals( '', llms_get_form_title( 'checkout' ) );\n\n\t\t// Title enabled.\n\t\tLLMS_Forms::instance()->create( 'checkout' );\n\t\t$this->assertEquals( 'Billing Information', llms_get_form_title( 'checkout' ) );\n\n\t\t// Title disabled.\n\t\tLLMS_Forms::instance()->create( 'account' );\n\t\t$this->assertEquals( '', llms_get_form_title( 'account' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_form_title() method with free access plans.\n\t *\n\t * This runs in a separate process b/c otherwise, when running among the other tests, e.g. of the forms group,\n\t * the fallback to the default meta value doesn't work.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_form_title_free_access_plan() {\n\n\t\t$free_ap = $this->get_mock_plan( $price = 0 );\n\n\t\t// Expect default.\n\t\t$form_id = LLMS_Forms::instance()->create( 'checkout', true );\n\t\t$this->assertEquals( 'Student Information', llms_get_form_title( 'checkout', array( 'plan' => $free_ap ) ) );\n\n\t\t// Disable title.\n\t\tupdate_post_meta( $form_id, '_llms_form_show_title', 'no' );\n\t\t$this->assertEquals( '', llms_get_form_title( 'checkout', array( 'plan' => $free_ap ) ) );\n\t\tupdate_post_meta( $form_id, '_llms_form_show_title', 'yes' );\n\n\t\t// Change specific title.\n\t\tupdate_post_meta( $form_id, '_llms_form_title_free_access_plans', 'New title' );\n\t\t$this->assertEquals( 'New title', llms_get_form_title( 'checkout', array( 'plan' => $free_ap ) ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_login_form() for a logged out user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_login_form_logged_out_user() {\n\n\t\t$res = $this->get_output( 'llms_get_login_form' );\n\t\t$this->assertStringContains( '<div class=\"llms-person-login-form-wrapper\">', $res );\n\t\t$this->assertStringContains( '<form action=\"\" class=\"llms-login\" method=\"POST\">', $res );\n\n\t}\n\n\t/**\n\t * Test llms_get_login_form() for a logged in user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_login_form_logged_in_user() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$this->assertOutputEmpty( 'llms_get_login_form' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-l10n.php",
    "content": "<?php\n/**\n * Test Localization functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_l10n\n *\n * @since 4.9.0\n */\nclass LLMS_Test_Functions_L10n extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_get_locale()\n\t *\n\t * @since 4.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_locale() {\n\t\t$this->assertEquals( 'en_US', llms_get_locale() );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-locale.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Locale functiosn\n *\n * @group functions\n * @group locale\n *\n * @since 5.0.0\n * @version 5.0.0\n */\nclass LLMS_Test_Functions_Locale extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test the get_lifterlms_countries() method.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_countries() {\n\n\t\t$countries = get_lifterlms_countries();\n\t\t$this->assertTrue( is_array( get_lifterlms_countries() ) );\n\n\t\t// Spot check presence of countries.\n\t\t$this->assertEquals( 'United States (US)', $countries['US'] );\n\t\t$this->assertEquals( 'United Kingdom (UK)', $countries['GB'] );\n\t\t$this->assertEquals( 'Australia', $countries['AU'] );\n\t\t$this->assertEquals( 'China', $countries['CN'] );\n\t\t$this->assertEquals( 'Afghanistan', $countries['AF'] );\n\t\t$this->assertEquals( 'Haiti', $countries['HT'] );\n\t\t$this->assertEquals( 'Nigeria', $countries['NG'] );\n\t\t$this->assertEquals( 'Slovakia', $countries['SK'] );\n\t\t$this->assertEquals( 'Uzbekistan', $countries['UZ'] );\n\t\t$this->assertEquals( 'Zimbabwe', $countries['ZW'] );\n\n\t}\n\n\t/**\n\t * test the get_lifterlms_country() function\n\t *\n\t * @since 3.24.1\n\t * @since 5.0.0 Moved from currency tests file.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_country() {\n\n\t\t// test default\n\t\t$this->assertEquals( 'US', get_lifterlms_country() );\n\n\t\t// test lifterlms_country option\n\t\tupdate_option( 'lifterlms_country', 'GB' );\n\t\t$this->assertEquals( 'GB', get_lifterlms_country() );\n\n\t\t// test that the lifterlms_country filter is applied\n\t\tadd_filter( 'lifterlms_country', function() {\n\t\t\treturn 'FR';\n\t\t} );\n\t\t$this->assertEquals( 'FR', get_lifterlms_country() );\n\t}\n\n\t/**\n\t * Test the llms_get_country_locale() function\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_country_address_info() {\n\n\t\t$this->assertEquals( array(\n\t\t\t'city'     => 'City',\n\t\t\t'state'    => 'State',\n\t\t\t'postcode' => 'ZIP code',\n\t\t), llms_get_country_address_info( 'US' ) );\n\n\t\t$this->assertEquals( array(), llms_get_country_address_info( 'FAKE' ) );\n\n\t}\n\n\t/**\n\t * test the llms_get_country_name() function\n\t *\n\t * @since 3.24.1\n\t * @since 3.28.2 Unknown.\n\t * @since 5.0.0 Moved from currency tests file.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_country_name() {\n\n\t\t// test existing country definition\n\t\t$this->assertEquals( 'United States (US)', llms_get_country_name( 'US' ) );\n\n\t\t// test non-existing country definition\n\t\t$this->assertEquals( 'XX', llms_get_country_name( 'XX' ) );\n\t}\n\n\t/**\n\t * Test llms_get_time_period_l10n()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_time_period_l10n() {\n\n\t\t/**\n\t\t * List of tests to run\n\t\t *\n\t\t * Each array contains two items:\n\t\t * 1) An array of arguments to pass to the function\n\t\t * 2) the expected string output.\n\t\t */\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\tarray( 'day' ),\n\t\t\t\t'day',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'dAy' ),\n\t\t\t\t'day',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'day', 1 ),\n\t\t\t\t'day',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'day', 2 ),\n\t\t\t\t'days',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'day', 100 ),\n\t\t\t\t'days',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'week' ),\n\t\t\t\t'week',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'WEEK' ),\n\t\t\t\t'week',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'week', 1 ),\n\t\t\t\t'week',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'week', 2 ),\n\t\t\t\t'weeks',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'week', 25 ),\n\t\t\t\t'weeks',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'month' ),\n\t\t\t\t'month',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'Month' ),\n\t\t\t\t'month',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'month', 1 ),\n\t\t\t\t'month',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'month', 2 ),\n\t\t\t\t'months',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'month', 17 ),\n\t\t\t\t'months',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'year' ),\n\t\t\t\t'year',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'yeAR' ),\n\t\t\t\t'year',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'year', 1 ),\n\t\t\t\t'year',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'year', 2 ),\n\t\t\t\t'years',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'year', 999 ),\n\t\t\t\t'years',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'UNSUPPORTED' ),\n\t\t\t\t'UNSUPPORTED',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\t\t\tlist( $args, $expect ) = $test;\n\t\t\t$this->assertEquals( $expect, llms_get_time_period_l10n( ...$args ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * test the get_lifterlms_countries() function\n\t *\n\t * @since 3.24.1\n\t * @since 5.0.0 Updated name when adding test for the base function\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lifterlms_countries_filter_and_unique() {\n\n\t\t// test unique and lifterlms_countries filters are applied\n\t\tadd_filter( 'lifterlms_countries', function() {\n\t\t\treturn array(\n\t\t\t\t'AF' => 'Afghanistan',\n\t\t\t\t'AL' => 'Albania',\n\t\t\t\t'DZ' => 'Algeria',\n\t\t\t\t'AS' => 'American Samoa',\n\t\t\t\t'AD' => 'Andorra',\n\t\t\t\t'AN' => 'Andorra',\n\t\t\t);\n\t\t} );\n\n\t\t$test = array(\n\t\t\t'AF' => 'Afghanistan',\n\t\t\t'AL' => 'Albania',\n\t\t\t'DZ' => 'Algeria',\n\t\t\t'AS' => 'American Samoa',\n\t\t\t'AD' => 'Andorra',\n\t\t);\n\n\t\t$this->assertEquals( $test, get_lifterlms_countries() );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-logs.php",
    "content": "<?php\n/**\n * Test Logging Functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_logs\n *\n * @since 4.5.0\n */\nclass LLMS_Test_Functions_Logs extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.5.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\tadd_filter( 'llms_log_max_filesize', array( $this, 'shrink_max_log_size' ) );\n\t}\n\n\t/**\n\t * Teardown\n\t *\n\t * Clean log files from the log directory.\n\t *\n\t * This isn't strictly necessary when running tests in a CI but if you run tests\n\t * locally without regular manual cleanup you'll see a lot of trash logs generated as a result\n\t * and this teardown prevents that.\n\t *\n\t * @since 4.5.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tforeach ( glob( LLMS_LOG_DIR . '*.log*' ) as $file ) {\n\t\t\tunlink( $file );\n\t\t}\n\n\t\tremove_filter( 'llms_log_max_filesize', array( $this, 'shrink_max_log_size' ) );\n\n\t}\n\n\t/**\n\t * Create a mock log file with a target size\n\t *\n\t * @since 4.5.0\n\t *\n\t * @param string  $handle      Log file's handle.\n\t * @param integer $target_size Target logfile size (in MB). The created file will be at least this big and more than likely a little bigger.\n\t * @return void\n\t */\n\tprotected function create_mock_log_file( $handle, $target_size = 1 ) {\n\n\t\t// Convert target size to MB.\n\t\t$target_size = 1 * 1000 * 1000;\n\t\t$file        = llms_get_log_path( $handle );\n\n\t\t$size = 0;\n\t\twhile ( $size < $target_size ) {\n\n\t\t\t$i = 0;\n\t\t\twhile ( $i <= 20 ) {\n\t\t\t\tllms_log( str_repeat( '01', 999 ), $handle );\n\t\t\t\t++$i;\n\t\t\t}\n\n\t\t\tclearstatcache( true, $file );\n\t\t\t$size = filesize( $file );\n\t\t}\n\n\t}\n\n\t/**\n\t * Mock the max allowed file size to be 1MB (instead of default 5MB)\n\t *\n\t * @since 4.5.0\n\t *\n\t * @param int $size Default max file size.\n\t * @return int\n\t */\n\tpublic function shrink_max_log_size( $size ) {\n\t\treturn 1;\n\t}\n\n\t/**\n\t * Test llms_get_callable_name()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_callable_name() {\n\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\t'llms',\n\t\t\t\t'llms',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'LLMS_Install::install',\n\t\t\t\t'LLMS_Install::install',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( llms(), 'init' ),\n\t\t\t\t'LifterLMS->init',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray( 'LLMS_Install', 'install' ),\n\t\t\t\t'LLMS_Install::install',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tllms(),\n\t\t\t\t'LifterLMS',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tfunction() {},\n\t\t\t\t'Closure'\n\t\t\t),\n\t\t\tarray(\n\t\t\t\tarray(),\n\t\t\t\t'Unknown',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\t$callable = $test[0];\n\t\t\t$expected = $test[1];\n\n\t\t\t$this->assertEquals( $expected, llms_get_callable_name( $callable ), $expected );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_get_log_path()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_log_path() {\n\n\t\t$handle = 'testhandle';\n\t\t$expected_hash = wp_hash( $handle );\n\n\t\t$expected_file = sprintf( '%1$s-%2$s.log', $handle, $expected_hash );\n\n\t\t$path = llms_get_log_path( $handle );\n\n\t\t$this->assertEquals( $expected_file, basename( $path ) );\n\t\t$this->assertEquals( untrailingslashit( LLMS_LOG_DIR ), dirname( $path ) );\n\n\t\t$this->assertEquals( LLMS_LOG_DIR . $expected_file, $path );\n\n\t}\n\n\t/**\n\t * Test llms_log() when logging a string\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_log_string() {\n\n\t\t$this->assertTrue( llms_log( 'Test message', 'teststringlog' ) );\n\n\t\t$logs = explode( ' - ', file_get_contents( llms_get_log_path( 'teststringlog' ) ) );\n\n\t\t$this->assertTrue( date_create( $logs[0] ) instanceof DateTime );\n\n\t\t$this->assertEquals( \"Test message\\n\", $logs[1] );\n\n\t}\n\n\t/**\n\t * Test llms_log() when logging an array\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_log_array() {\n\n\t\t$this->assertTrue( llms_log( array( 'Test message' ), 'testarrlog' ) );\n\n\t\t$logs = explode( ' - ', file_get_contents( llms_get_log_path( 'testarrlog' ) ) );\n\n\t\t$this->assertTrue( date_create( $logs[0] ) instanceof DateTime );\n\n\t\t$this->assertEquals( \"Array\n(\n    [0] => Test message\n)\n\n\", $logs[1] );\n\n\t}\n\n\t/**\n\t * Test llms_log() when logging an object\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_log_object() {\n\n\t\t$this->assertTrue( llms_log( (object) array( 'Test' => 1 ), 'testobjlog' ) );\n\n\t\t$logs = explode( ' - ', file_get_contents( llms_get_log_path( 'testobjlog' ) ) );\n\n\t\t$this->assertTrue( date_create( $logs[0] ) instanceof DateTime );\n\n\t\t$this->assertEquals( \"stdClass Object\n(\n    [Test] => 1\n)\n\n\", $logs[1] );\n\n\t}\n\n\t/**\n\t * Test llms_backup_log\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_backup_log() {\n\n\t\t$actions = did_action( 'llms_log_file_backup_created' );\n\n\t\t$handle = 'logtobackup';\n\t\t$file   = llms_get_log_path( $handle );\n\n\t\t// File doesn't exist, no need to backup.\n\t\t$this->assertNull( llms_backup_log( $handle ) );\n\n\t\tllms_log( str_repeat( '01', 999 ), $handle );\n\n\t\t// File does exist but doesn't need to be backup yet.\n\t\t$this->assertNull( llms_backup_log( $handle ) );\n\n\t\t$this->create_mock_log_file( $handle );\n\n\t\t// Get the contents of the original to compare later.\n\t\t$original = file_get_contents( $file );\n\n\t\t// Split the file.\n\t\t$copy = llms_backup_log( $handle );\n\n\t\t// We made a copy.\n\t\t$this->assertTrue( false !== $copy );\n\n\t\t// Return should be different than than the original.\n\t\t$this->assertNotEquals( $copy, $file );\n\n\t\t// Copy exists.\n\t\t$this->assertTrue( file_exists( $copy ) );\n\n\t\t// Original has been removed.\n\t\t$this->assertFalse( file_exists( $file ) );\n\n\t\t// Compare copy contents to the original.\n\t\t$this->assertEquals( $original, file_get_contents( $copy ) );\n\n\t\t// Action ran.\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_log_file_backup_created' ) );\n\n\t}\n\n\t/**\n\t * Test llms_backup_logs()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_backup_logs() {\n\n\t\t$actions = did_action( 'llms_log_file_backup_created' );\n\n\t\t// Make sure the created files are the right ones.\n\t\t$handler = function( $copy, $file, $handle ) {\n\t\t\t$this->assertTrue( in_array( $handle, array( 'tobackup1', 'tobackup2', 'tobackup-withonehyphen', 'tobackup-with-mutli-hyphens' ), true ) );\n\t\t};\n\t\tadd_action( 'llms_log_file_backup_created', $handler, 10, 3 );\n\n\t\tllms_log( 'message', 'notbackedup1' );\n\t\tllms_log( 'message', 'notbackedup2' );\n\n\t\t$this->create_mock_log_file( 'tobackup1' );\n\t\t$this->create_mock_log_file( 'tobackup2' );\n\t\t$this->create_mock_log_file( 'tobackup-withonehyphen' );\n\t\t$this->create_mock_log_file( 'tobackup-with-mutli-hyphens' );\n\n\t\tllms_backup_logs();\n\n\t\t$this->assertEquals( $actions + 4, did_action( 'llms_log_file_backup_created' ) );\n\n\t\tremove_action( 'llms_log_file_backup_created', $handler, 10 );\n\n\t}\n\n\t/**\n\t * Test _llms_secure_log_messages() when no secure strings are registered.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test__llms_secure_log_messages_no_strings_registered() {\n\t\t$this->assertEquals( 'log', _llms_secure_log_messages( 'log', 'llms' ) );\n\t}\n\n\n\t/**\n\t * Test _llms_secure_log_messages() when no secure strings are registered.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test__llms_secure_log_messages() {\n\n\n\t\t$handler = function( $strings ) {\n\n\t\t\t$strings[] = 'abcd';\n\t\t\t$strings[] = '1234567890';\n\t\t\t$strings[] = 'xyz'; // Not found.\n\n\t\t\treturn $strings;\n\t\t};\n\n\t\tadd_filter( 'llms_secure_strings', $handler );\n\n\t\t$input_log = array(\n\t\t\t'abcd',\n\t\t\t'other stuff',\n\t\t\t'1234567890',\n\t\t\t'more logs',\n\t\t); \n\n\t\t$this->assertEquals( \n\t\t\t'[\"***d\",\"other stuff\",\"********90\",\"more logs\"]',\n\t\t\t_llms_secure_log_messages( json_encode( $input_log ), 'llms' )\n\t\t);\n\n\t\tremove_filter( 'llms_secure_strings', $handler );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-options.php",
    "content": "<?php\n/**\n * Test Option functions\n *\n * @package  LifterLMS/Tests/Functions\n * @since    3.29.0\n * @version  3.29.0\n */\nclass LLMS_Test_Functions_Options extends LLMS_UnitTestCase {\n\n\t/**\n\t * test the get_secure_var method\n\t *\n\t * @return  void\n\t * @since   3.29.0\n\t * @version 3.29.0\n\t */\n\tpublic function test_llms_get_secure_option() {\n\n\t\t$val = 'F4K3_ApI-K3Y$!';\n\n\t\t// nothing set.\n\t\t$this->assertFalse( llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR' ) );\n\t\t// fallback to something else.\n\t\t$this->assertEquals( '', llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', '' ) );\n\t\t// fallback to actual val.\n\t\t$this->assertEquals( $val, llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', $val ) );\n\t\t// fallback with db call.\n\t\t$this->assertEquals( $val, llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', $val, 'llms_mock_secure_option' ) );\n\t\t// no fallback with db call.\n\t\t$this->assertFalse( llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) );\n\n\t\t// add the option.\n\t\tupdate_option( 'llms_mock_secure_option', $val );\n\t\t$this->assertEquals( $val, llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) );\n\n\t\t// use constant variable.\n\t\tdefine( 'LLMS_MOCK_SECURE_VAR', 'arstarstarst' );\n\t\t$this->assertEquals( 'arstarstarst', llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) );\n\n\t\t// use environment var.\n\t\tputenv( 'LLMS_MOCK_SECURE_VAR=a90rst0-98arst' );\n\t\t$this->assertEquals( 'a90rst0-98arst', llms_get_secure_option( 'LLMS_MOCK_SECURE_VAR', false, 'llms_mock_secure_option' ) );\n\n\t}\n\n\t/**\n\t * Tests the llms_is_option_secure() function.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_is_option_secure() {\n\n\t\t// Empty name.\n\t\t$this->assertFalse( llms_is_option_secure( '' ) );\n\n\t\t// Environment variable.\n\t\tputenv( 'FOO=BAR' );\n\t\t$this->assertTrue( llms_is_option_secure( 'FOO' ) );\n\n\t\t// Unset environment variable.\n\t\tputenv( 'FOO' );\n\t\t$this->assertFalse( llms_is_option_secure( 'FOO' ) );\n\n\t\t// Undefined constant.\n\t\t$this->assertFalse( llms_is_option_secure( 'F00D' ) );\n\n\t\t// Defined constant.\n\t\tdefine( 'F00D', 'BAD' );\n\t\t$this->assertTrue( llms_is_option_secure( 'F00D' ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-order.php",
    "content": "<?php\n/**\n * Test Order Functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group orders\n *\n * @group orders\n * @group functions\n * @group functions_orders\n *\n * @since 3.27.0\n * @since 5.0.0 Updated for form handler error codes & install forms on setup.\n * @since 5.4.0 Added tests for `llms_get_possible_order_statuses()`.\n */\nclass LLMS_Test_Functions_Order extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_can_gateway_be_used_for_plan (and llms_can_gateway_be_used_for_plan_or_order() for a plan).\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_can_gateway_be_used_for_plan() {\n\n\t\t$plan    = llms_insert_access_plan( array(\n\t\t\t'price'      => 15.99,\n\t\t\t'product_id' => $this->factory->post->create( array( 'post_type' => 'course' ) ),\n\t\t) );\n\t\t$plan_id = $plan->get( 'id' );\n\n\t\t$gateway           = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\t$orig_supports     = $gateway->supports;\n\t\t$gateway->supports = array(\n\t\t\t'single_payments'    => false,\n\t\t\t'recurring_payments' => false,\n\t\t);\n\n\t\t// Invalid gateway.\n\t\t$this->assertWPErrorCodeEquals( 'gateway-invalid', llms_can_gateway_be_used_for_plan( 'fake', $plan ) );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-invalid', llms_can_gateway_be_used_for_plan_or_order( 'fake', $plan, true ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'fake', $plan_id ) );\n\n\t\t// Manual not available.\n\t\t$this->assertWPErrorCodeEquals( 'gateway-disabled', llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan, true ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan_id ) );\n\n\t\t// Recurring plan without support.\n\t\t$plan->set( 'frequency', 1 );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-support-recurring', llms_can_gateway_be_used_for_plan( 'manual', $plan ) );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-support-recurring', llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan, true, false ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan_id ) );\n\t\t\t\n\t\t// One-time plan without support.\n\t\t$plan->set( 'frequency', 0 );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-support-single', llms_can_gateway_be_used_for_plan( 'manual', $plan ) );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-support-single', llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan, true, false ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan_id ) );\n\n\t\t// Reset support.\n\t\t$gateway->supports = $orig_supports;\n\n\t\t// Recurring okay.\n\t\t$plan->set( 'frequency', 1 );\n\t\t$this->assertTrue( llms_can_gateway_be_used_for_plan( 'manual', $plan ) );\n\t\t$this->asserttrue( llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan_id, false, false ) );\n\n\t\t// Single okay.\n\t\t$plan->set( 'frequency', 0 );\n\t\t$this->assertTrue( llms_can_gateway_be_used_for_plan( 'manual', $plan ) );\n\t\t$this->asserttrue( llms_can_gateway_be_used_for_plan_or_order( 'manual', $plan_id, false, false ) );\n\n\t}\n\n\t/**\n\t * Test llms_can_gateway_be_used_for_plan() for a disabled gateway.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_can_gateway_be_used_for_plan_disabled_gateway() {\n\n\t\t$plan = llms_insert_access_plan( array(\n\t\t\t'price'      => 15.99,\n\t\t\t'product_id' => $this->factory->post->create( array( 'post_type' => 'course' ) ),\n\t\t) );\n\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-not-enabled';\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway, false );\n\n\t\t$this->assertWPErrorCodeEquals( 'gateway-disabled', llms_can_gateway_be_used_for_plan( 'fake-not-enabled', $plan ) );\n\n\t\t$this->unload_payment_gateway( 'fake-not-enabled' );\n\n\t}\n\n\t/**\n\t * Test llms_can_gateway_be_used_for_plan_or_order() for an order. \n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_can_gateway_be_used_for_plan_or_order() {\n\n\t\t$order    = $this->factory->order->create_and_get();\n\t\t$order_id = $order->get( 'id' );\n\n\t\t$gateway           = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\t$orig_supports     = $gateway->supports;\n\t\t$gateway->supports = array(\n\t\t\t'single_payments'    => false,\n\t\t\t'recurring_payments' => false,\n\t\t);\n\n\t\t// Invalid gateway.\n\t\t$this->assertWPErrorCodeEquals( 'gateway-invalid', llms_can_gateway_be_used_for_plan_or_order( 'fake', $order, true ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'fake', $order_id ) );\n\n\t\t// Manual not available.\n\t\t$this->assertWPErrorCodeEquals( 'gateway-disabled', llms_can_gateway_be_used_for_plan_or_order( 'manual', $order, true ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'manual', $order_id ) );\n\n\t\t// Recurring plan without support.\n\t\t$order->set( 'order_type', 'recurring' );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-support-recurring', llms_can_gateway_be_used_for_plan_or_order( 'manual', $order, true, false ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'manual', $order_id ) );\n\t\t\t\n\t\t// One-time plan without support.\n\t\t$order->set( 'order_type', 'single' );\n\t\t$this->assertWPErrorCodeEquals( 'gateway-support-single', llms_can_gateway_be_used_for_plan_or_order( 'manual', $order, true, false ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'manual', $order_id ) );\n\n\t\t// Reset support.\n\t\t$gateway->supports = $orig_supports;\n\n\t\t// Recurring okay.\n\t\t$order->set( 'frequency', 1 );\n\t\t$this->asserttrue( llms_can_gateway_be_used_for_plan_or_order( 'manual', $order_id, false, false ) );\n\n\t\t// Single okay.\n\t\t$order->set( 'frequency', 0 );\n\t\t$this->asserttrue( llms_can_gateway_be_used_for_plan_or_order( 'manual', $order_id, false, false ) );\n\n\t}\n\n\t/**\n\t * Test llms_can_gateway_be_used_for_plan_or_order() with an invalid post input.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_can_gateway_be_used_for_plan_or_order_invalid_post() {\n\n\t\t$post_id = $this->factory->post->create();\n\t\t$this->assertWPErrorCodeEquals( 'post-invalid', llms_can_gateway_be_used_for_plan_or_order( 'manual', $post_id, true ) );\n\t\t$this->assertFalse( llms_can_gateway_be_used_for_plan_or_order( 'manual', $post_id ) );\n\n\t}\n\n\t/**\n\t * Test the llms_get_order_by_key() method.\n\t *\n\t * @since 3.30.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_order_by_key() {\n\n\t\t// Errors.\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 'arst' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 'arst', 'order' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 'arst', 'id' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 'arst', 'fake' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '1' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '1', 'order' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '1', 'id' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '1', 'fake' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 12345 ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 12345, 'order' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 12345, 'id' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( 12345, 'fake' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '', 'order' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '', 'id' ) ) );\n\t\t$this->assertTrue( is_null( llms_get_order_by_key( '', 'fake' ) ) );\n\n\t\t// Success.\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$this->assertEquals( $order, llms_get_order_by_key( $order->get( 'order_key' ) ) ); // Default.\n\t\t$this->assertEquals( $order, llms_get_order_by_key( $order->get( 'order_key' ), 'order' ) ); // Explicit.\n\t\t$this->assertEquals( $order->get( 'id' ), llms_get_order_by_key( $order->get( 'order_key' ), 'id' ) ); // Id.\n\t\t$this->assertEquals( $order->get( 'id' ), llms_get_order_by_key( $order->get( 'order_key' ), 'somethingelse' ) ); // Fake.\n\n\t}\n\n\t/**\n\t * Test llms_get_order_status_name().\n\t *\n\t * @since 3.3.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_order_status_name() {\n\t\t$this->assertNotEmpty( llms_get_order_status_name( 'llms-active' ) );\n\t\t$this->assertEquals( 'Active', llms_get_order_status_name( 'llms-active' ) );\n\t\t$this->assertEquals( 'wut', llms_get_order_status_name( 'wut' ) );\n\t}\n\n\t/**\n\t * Test llms_get_order_statuses().\n\t *\n\t * @since 3.3.1\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_order_statuses() {\n\n\t\t$this->assertTrue( is_array( llms_get_order_statuses() ) );\n\t\t$this->assertFalse( empty( llms_get_order_statuses() ) );\n\t\t$this->assertEquals( array(\n\t\t\t'llms-completed',\n\t\t\t'llms-active',\n\t\t\t'llms-expired',\n\t\t\t'llms-on-hold',\n\t\t\t'llms-pending-cancel',\n\t\t\t'llms-pending',\n\t\t\t'llms-cancelled',\n\t\t\t'llms-refunded',\n\t\t\t'llms-failed',\n\t\t), array_keys( llms_get_order_statuses() ) );\n\n\t\t$this->assertTrue( is_array( llms_get_order_statuses( 'recurring' ) ) );\n\t\t$this->assertFalse( empty( llms_get_order_statuses( 'recurring' ) ) );\n\t\t$this->assertEquals( array(\n\t\t\t'llms-active',\n\t\t\t'llms-expired',\n\t\t\t'llms-on-hold',\n\t\t\t'llms-pending-cancel',\n\t\t\t'llms-pending',\n\t\t\t'llms-cancelled',\n\t\t\t'llms-refunded',\n\t\t\t'llms-failed',\n\t\t), array_keys( llms_get_order_statuses( 'recurring' ) ) );\n\n\t\t$this->assertTrue( is_array( llms_get_order_statuses( 'single' ) ) );\n\t\t$this->assertFalse( empty( llms_get_order_statuses( 'single' ) ) );\n\t\t$this->assertEquals( array(\n\t\t\t'llms-completed',\n\t\t\t'llms-pending',\n\t\t\t'llms-cancelled',\n\t\t\t'llms-refunded',\n\t\t\t'llms-failed',\n\t\t), array_keys( llms_get_order_statuses( 'single' ) ) );\n\n\t}\n\n\t/**\n\t * Test llms_locate_order_for_email_and_plan().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_locate_order_for_email_and_plan() {\n\n\t\t$order   = new LLMS_Order( 'new' );\n\t\t$email   = 'locate_order_for_email_and_plan@fake.tld';\n\t\t$plan_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_access_plan',\n\t\t) );\n\n\t\t$order->set( 'plan_id', $plan_id );\n\t\t$order->set_status( 'llms-pending' );\n\n\t\t// Invalid email & plan.\n\t\t$this->assertNull( llms_locate_order_for_email_and_plan( $email, $plan_id + 1 ) );\n\n\t\t// Invalid email & valid plan.\n\t\t$this->assertNull( llms_locate_order_for_email_and_plan( $email, $plan_id ) );\n\n\t\t$order->set( 'billing_email', $email );\n\t\t\n\t\t// Valid email & invalid plan.\n\t\t$this->assertNull( llms_locate_order_for_email_and_plan( $email, $plan_id + 1 ) );\n\n\t\t// Valid email & valid plan.\n\t\t$this->assertEquals( $order->get( 'id' ), llms_locate_order_for_email_and_plan( $email, $plan_id ) );\n\n\t\t// Only locates pending orders.\n\t\t$order->set_status( 'llms-failed' ); \n\t\t$this->assertNull( llms_locate_order_for_email_and_plan( $email, $plan_id ) );\n\n\t}\n\n\t/**\n\t * Test llms_locate_order_for_user_and_plan() method.\n\t *\n\t * @since 3.30.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_locate_order_for_user_and_plan() {\n\n\t\t$order = new LLMS_Order( 'new' );\n\n\t\t$uid = $this->factory->student->create();\n\t\t$pid = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_access_plan',\n\t\t) );\n\n\t\t// Fake student & fake plan\n\t\t$this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid + 1, $pid + 1 ) ) );\n\n\t\t// Real student & fake plan\n\t\t$this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid, $pid + 1 ) ) );\n\n\t\t// Fake student & real plan\n\t\t$this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid + 1, $pid ) ) );\n\n\t\t// Real student & real plan & no order exists.\n\t\t$this->assertTrue( is_null( llms_locate_order_for_user_and_plan( $uid + 1, $pid ) ) );\n\n\t\t// Real student & real plan & order exists.\n\t\t$order->set( 'user_id', $uid );\n\t\t$order->set( 'plan_id', $pid );\n\t\t$this->assertSame( $order->get( 'id' ), llms_locate_order_for_user_and_plan( $uid, $pid ) );\n\n\t}\n\n\t/**\n\t * Test llms_setup_pending_order()\n\t *\n\t * @since 3.27.0\n\t * @since 5.0.0 Install forms & Updated expected error code.\n\t *              Only logged in users can edit themselves.\n\t * @return void\n\t */\n\tpublic function test_llms_setup_pending_order() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t// Enable t&c.\n\t\tupdate_option( 'lifterlms_registration_require_agree_to_terms', 'yes' );\n\t\tupdate_option( 'lifterlms_terms_page_id', 123456789 );\n\n\t\t// Order data to pass to tests.\n\t\t// Will be built upon as we go through tests below.\n\t\t$order_data = array(\n\t\t\t'plan_id' => '',\n\t\t\t'agree_to_terms' => '',\n\t\t\t'payment_gateway' => '',\n\t\t\t'coupon_code' => '',\n\t\t\t'customer' => array(),\n\t\t);\n\n\t\t// Didn't agree to t&c.\n\t\t$this->setup_pending_order_fail( $order_data, 'terms-violation' );\n\n\t\t// Agree to t&c for all future tests.\n\t\t$order_data['agree_to_terms'] = 'yes';\n\n\t\t// Missing plan id.\n\t\t$this->setup_pending_order_fail( $order_data, 'missing-plan-id' );\n\n\t\t// Add a fake plan id.\n\t\t$order_data['plan_id'] = 123;\n\t\t$this->setup_pending_order_fail( $order_data, 'invalid-plan-id' );\n\n\t\t// Create a real plan and add it to the order data.\n\t\t$order_data['plan_id'] = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t'post_title' => 'plan name',\n\t\t) );\n\t\tupdate_post_meta( $order_data['plan_id'], '_llms_price', '25.00' );\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\tupdate_post_meta( $order_data['plan_id'], '_llms_product_id', $course_id );\n\n\t\t// Fake coupon code.\n\t\t$order_data['coupon_code'] = 'coupon';\n\t\t$this->setup_pending_order_fail( $order_data, 'coupon-not-found' );\n\n\t\t// Create a real coupon.\n\t\t$coupon_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_coupon',\n\t\t\t'post_title' => 'coupon',\n\t\t) );\n\t\t// But make it unusable.\n\t\tupdate_post_meta( $coupon_id, '_llms_expiration_date', date( 'm/d/Y', strtotime( '-1 year' ) ) );\n\t\t$this->setup_pending_order_fail( $order_data, 'invalid-coupon' );\n\n\t\t// Make the coupon usable.\n\t\tupdate_post_meta( $coupon_id, '_llms_expiration_date', date( 'm/d/Y', strtotime( '+5 years' ) ) );\n\n\t\t// Missing payment gateway.\n\t\t$this->setup_pending_order_fail( $order_data, 'missing-gateway-id' );\n\n\t\t// Fake payment gateway.\n\t\t$order_data['payment_gateway'] = 'fakeway';\n\t\t$this->setup_pending_order_fail( $order_data, 'gateway-invalid' );\n\n\t\t// Real payment gateway.\n\t\t$order_data['payment_gateway'] = 'manual';\n\n\t\t// No customer data.\n\t\t$this->setup_pending_order_fail( $order_data, 'missing-customer' );\n\n\t\t// Most customer data but missing required email confirm field.\n\t\t$order_data['customer'] = array(\n\t\t\t'user_login' => 'arstehnarst',\n\t\t\t'email_address' => 'arstinhasrteinharst@test.net',\n\t\t\t'password' => '12345678',\n\t\t\t'password_confirm' => '12345678',\n\t\t\t'first_name' => 'Test',\n\t\t\t'last_name' => 'Person',\n\t\t\t'llms_billing_address_1' => '123',\n\t\t\t'llms_billing_address_2' => '123',\n\t\t\t'llms_billing_city' => 'City',\n\t\t\t'llms_billing_state' => 'CA',\n\t\t\t'llms_billing_zip' => '91231',\n\t\t\t'llms_billing_country' => 'US',\n\t\t\t'llms_phone' => '1234567890',\n\t\t);\n\n\t\t// Missing required field.\n\t\t$this->setup_pending_order_fail( $order_data, 'llms-form-missing-required' );\n\n\t\t// Existing user who's already enrolled.\n\t\t$uid = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\twp_set_current_user( $uid );\n\t\t$order_data['customer']['email_address_confirm'] = 'arstinhasrteinharst@test.net';\n\t\t$order_data['customer']['user_id'] = $uid;\n\t\tllms_enroll_student( $uid, $course_id );\n\t\t$this->setup_pending_order_fail( $order_data, 'already-enrolled' );\n\n\t\t// This should return an array of details we need to create a new order!\n\t\tunset( $order_data['customer']['user_id'] );\n\t\twp_set_current_user( null );\n\t\t$order_data['customer']['email_address'] = 'arstarst@ats.net';\n\t\t$order_data['customer']['email_address_confirm'] = 'arstarst@ats.net';\n\t\t$setup = llms_setup_pending_order( $order_data );\n\t\t$this->assertEquals( array( 'person', 'plan', 'gateway', 'coupon' ), array_keys( $setup ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_possible_order_statuses() function for a recurring order.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_possible_recurring_order_statuses() {\n\t\t$order = $this->get_mock_order();\n\t\t$this->assertTrue( $order->is_recurring() );\n\t\t$this->assertEquals(\n\t\t\tllms_get_order_statuses( 'recurring' ),\n\t\t\tllms_get_possible_order_statuses( $order )\n\t\t);\n\t}\n\n\t/**\n\t * Test llms_get_possible_order_statuses() function for a single order.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_possible_single_order_statuses() {\n\t\t$order = $this->get_mock_order();\n\t\t$order->set( 'order_type', 'single' );\n\t\t$this->assertFalse( $order->is_recurring() );\n\t\t$this->assertEquals(\n\t\t\tllms_get_order_statuses( 'single' ),\n\t\t\tllms_get_possible_order_statuses( $order )\n\t\t);\n\t}\n\n\t/**\n\t * Test llms_get_possible_order_statuses() function for a recurring order with deleted product.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_possible_recurring_order_statuses_deleted_product() {\n\n\t\t$order = $this->get_mock_order();\n\n\t\t// Delete product.\n\t\twp_delete_post( $order->get( 'product_id' ) );\n\n\t\t$this->assertTrue( $order->is_recurring() );\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'llms-expired',\n\t\t\t\t'llms-cancelled',\n\t\t\t\t'llms-refunded',\n\t\t\t\t'llms-failed',\n\t\t\t),\n\t\t\tarray_keys( llms_get_possible_order_statuses( $order ) )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test llms_get_possible_order_statuses() function for a single with deleted product.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_possible_single_order_statuses_deleted_product() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$order->set( 'order_type', 'single' );\n\n\t\t// Delete product.\n\t\twp_delete_post( $order->get( 'product_id' ) );\n\n\t\t$this->assertFalse( $order->is_recurring() );\n\t\t$this->assertEquals(\n\t\t\tllms_get_order_statuses( 'single' ),\n\t\t\tllms_get_possible_order_statuses( $order )\n\t\t);\n\n\t}\n\n\n\t/**\n\t * Test llms_setup_pending_order() failure\n\t *\n\t * @since 3.27.0\n\t * @since 4.9.0 Remove default optional value from `$order_data` arg for php8 compat.\n\t *\n\t * @param array  $order_data    Array of order data to pass to `llms_setup_pending_order()`.\n\t * @param string $expected_code Expected error code.\n\t * @return void\n\t */\n\tprivate function setup_pending_order_fail( $order_data, $expected_code ) {\n\n\t\t$setup = llms_setup_pending_order( $order_data );\n\t\t$this->assertTrue( is_wp_error( $setup ) );\n\t\t$this->assertEquals( $expected_code, $setup->get_error_code() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-page.php",
    "content": "<?php\n/**\n * Test page functions.\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_page\n *\n * @since 3.38.0\n * @since 6.3.0 Added tests for llms_get_paged_query_var(), llms_get_endpoint_url(), _llms_normalize_endpoint_base_url().\n */\nclass LLMS_Test_Functions_Fage extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test the llms_confirm_payment_url() function.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_confirm_payment_url() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$base = get_permalink( llms_get_page_id( 'checkout' ) ) . '&confirm-payment';\n\n\t\t// No additional args provided.\n\t\t$this->assertEquals( $base, llms_confirm_payment_url() );\n\n\t\t// Has order key.\n\t\t$this->assertEquals( $base . '&order=fake', llms_confirm_payment_url( 'fake' ) );\n\n\t\t// Has redirect.\n\t\t$this->mockGetRequest( array(\n\t\t\t'redirect' => get_site_url(),\n\t\t) );\n\t\t$this->assertEquals( $base . '&redirect=' . urlencode( get_site_url() ), llms_confirm_payment_url() );\n\n\t\t// Has both.\n\t\t$this->assertEquals( $base . '&order=fake&redirect=' . urlencode( get_site_url() ), llms_confirm_payment_url( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_endpoint_url() when pretty permalinks are disabled.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_endpoint_url_no_pretty_permalinks() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$permalink = get_permalink( llms_get_page_id( 'myaccount' ) );\n\t\t$this->go_to( $permalink );\n\n\t\tforeach ( llms()->query->get_query_vars() as $var => $slug ) {\n\n\t\t\t$this->assertEquals( \"{$permalink}&{$slug}\", llms_get_endpoint_url( $var ) );\n\t\t\t$this->assertEquals( \"{$permalink}&{$slug}=test\", llms_get_endpoint_url( $var, 'test' ) );\n\t\t\t$this->assertEquals( \"https://fake.tld/?{$slug}=1\", llms_get_endpoint_url( $var, 1, 'https://fake.tld/' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_get_endpoint_url() when pretty permalinks are enabled with a trailing slash.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_endpoint_url_with_trailing_slash_pretty_permalink() {\n\n\t\tglobal $wp_rewrite;\n\n\t\t$orig_permastruct = get_option( 'permalink_structure' );\n\n\t\tLLMS_Install::create_pages();\n\n\t\tupdate_option( 'permalink_structure', '/%postname%/' );\n\t\t$wp_rewrite->init();\n\n\n\t\t$permalink = get_permalink( llms_get_page_id( 'myaccount' ) );\n\t\t$this->go_to( $permalink );\n\n\t\tforeach ( llms()->query->get_query_vars() as $var => $slug ) {\n\n\t\t\t$this->assertEquals( \"{$permalink}{$slug}/\", llms_get_endpoint_url( $var ) );\n\t\t\t$this->assertEquals( \"{$permalink}{$slug}/test/\", llms_get_endpoint_url( $var, 'test' ) );\n\t\t\t$this->assertEquals( \"https://fake.tld/{$slug}/1/\", llms_get_endpoint_url( $var, 1, 'https://fake.tld/' ) );\n\t\t\t$this->assertEquals( \"https://fake.tld/{$slug}/1/?whatever=yes\", llms_get_endpoint_url( $var, 1, 'https://fake.tld/?whatever=yes' ) );\n\n\t\t}\n\n\t\t$this->go_to( '' );\n\n\t\tupdate_option( 'permalink_structure', $orig_permastruct );\n\t\t$wp_rewrite->init();\n\n\t}\n\n\t/**\n\t * Test llms_get_endpoint_url() when pretty permalinks are enabled with a trailing slash.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1983\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_endpoint_url_without_trailing_slash_pretty_permalink() {\n\n\t\tglobal $wp_rewrite;\n\n\t\t$orig_permastruct = get_option( 'permalink_structure' );\n\n\t\tLLMS_Install::create_pages();\n\n\t\tupdate_option( 'permalink_structure', '/%postname%' );\n\t\t$wp_rewrite->init();\n\n\n\t\t$permalink = get_permalink( llms_get_page_id( 'myaccount' ) );\n\t\t$this->go_to( $permalink );\n\n\t\tforeach ( llms()->query->get_query_vars() as $var => $slug ) {\n\n\t\t\t$this->assertEquals( \"{$permalink}/{$slug}\", llms_get_endpoint_url( $var ) );\n\t\t\t$this->assertEquals( \"{$permalink}/{$slug}/test\", llms_get_endpoint_url( $var, 'test' ) );\n\t\t\t$this->assertEquals( \"https://fake.tld/{$slug}/1\", llms_get_endpoint_url( $var, 1, 'https://fake.tld/' ) );\n\t\t\t$this->assertEquals( \"https://fake.tld/{$slug}/1?whatever=yes\", llms_get_endpoint_url( $var, 1, 'https://fake.tld/?whatever=yes' ) );\n\n\t\t}\n\n\t\t$this->go_to( '' );\n\n\t\tupdate_option( 'permalink_structure', $orig_permastruct );\n\t\t$wp_rewrite->init();\n\n\t}\n\n\t/**\n\t * Test the llms_get_page_id() function.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_page_id() {\n\n\t\t$pages = array(\n\t\t\t'checkout' => 'checkout',\n\t\t\t'courses' => 'shop',\n\t\t\t'myaccount' => 'myaccount',\n\t\t\t'memberships' => 'memberships',\n\t\t);\n\n\t\t// Clear options maybe installed by other tests.\n\t\tforeach ( array_values( $pages ) as $option ) {\n\t\t\tdelete_option( 'lifterlms_' . $option . '_page_id' );\n\t\t}\n\n\t\t// Options don't exist.\n\n\t\t// Backwards compat.\n\t\t$this->assertEquals( -1, llms_get_page_id( 'shop' ) );\n\n\t\tforeach ( array_keys( $pages ) as $slug ) {\n\t\t\t$this->assertEquals( -1, llms_get_page_id( $slug ) );\n\t\t}\n\n\t\t// Options do exist.\n\t\tLLMS_Install::create_pages();\n\n\t\t// Backwards compat.\n\t\t$this->assertEquals( get_option( 'lifterlms_shop_page_id' ), llms_get_page_id( 'shop' ) );\n\n\t\tforeach ( $pages as $slug => $option ) {\n\n\t\t\t$id = llms_get_page_id( $slug );\n\n\t\t\t// Number.\n\t\t\t$this->assertTrue( is_int( $id ) );\n\n\t\t\t// Equals expected option value.\n\t\t\t$this->assertEquals( get_option( 'lifterlms_' . $option . '_page_id' ), $id );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the llms_get_paged_query_var() function.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_paged_query_var() {\n\n\t\t// No `paged` or `page` query var set.\n\t\t$this->assertEquals(\n\t\t\t1,\n\t\t\tllms_get_paged_query_var()\n\t\t);\n\n\t\t// `page` query var set.\n\t\tset_query_var( 'page', 2 );\n\t\t$this->assertEquals(\n\t\t\t2,\n\t\t\tllms_get_paged_query_var()\n\t\t);\n\n\t\t// `paged` query var set - it'll win over `page`.\n\t\tset_query_var( 'paged', 4 );\n\t\t$this->assertEquals(\n\t\t\t4,\n\t\t\tllms_get_paged_query_var()\n\t\t);\n\n\t\t// `paged` query var set to falsy - `page` query var value will be returned.\n\t\tset_query_var( 'paged', 0 );\n\t\t$this->assertEquals(\n\t\t\t2,\n\t\t\tllms_get_paged_query_var()\n\t\t);\n\n\t}\n\n\t/**\n\t * Test the _llms_normalize_endpoint_base_url() function.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tfunction test__llms_normalize_endpoint_base_url() {\n\n\t\t$temp = get_option( 'permalink_structure' );\n\t\tupdate_option( 'permalink_structure', '/%postname%/' );\n\n\t\tglobal $wp_rewrite;\n\t\t$wp_rewrite->init();\n\n\t\t$url      = 'https://example.com/members/admin/courses/my-courses/page/2/';\n\t\t$endpoint = 'something';\n\t\tset_query_var( 'page', 2 );\n\n\t\t// Provided endpoint not found in the url: nothing to do.\n\t\t$this->assertEquals(\n\t\t\t$url,\n\t\t\t_llms_normalize_endpoint_base_url( $url, $endpoint )\n\t\t);\n\n\t\t$endpoint = 'members';\n\t\t// Provided endpoint found in the url, but not as last part (except for the paging info): nothing to do.\n\t\t$this->assertEquals(\n\t\t\t$url,\n\t\t\t_llms_normalize_endpoint_base_url( $url, $endpoint )\n\t\t);\n\n\t\t$endpoint = 'my-courses';\n\n\t\t// Provided endpoint found in the url, as last part (except for the paging info).\n\t\t$this->assertEquals(\n\t\t\t'https://example.com/members/admin/courses/',\n\t\t\t_llms_normalize_endpoint_base_url( $url, $endpoint )\n\t\t);\n\n\t\t// Teardown.\n\t\tupdate_option( 'permalink_structure', $temp );\n\t\t$wp_rewrite->init();\n\n\t}\n\n\t/**\n\t * Test the llms_get_endpoint_url() function.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tfunction test_llms_get_endpoint_url() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t// Get the courses endpoint url when on the dashboard's my grades tab.\n\t\t$account_url = llms_get_page_url( 'myaccount' );\n\t\t$this->go_to( $account_url . '&my-grades' );\n\n\t\t// Ugly permalinks.\n\t\t$endpoint = 'view-courses';\n\t\t$this->assertEquals(\n\t\t\t$account_url . '&my-courses',\n\t\t\tllms_get_endpoint_url( $endpoint )\n\t\t);\n\n\t\t// Pretty permalinks.\n\t\t$temp = get_option( 'permalink_structure' );\n\t\tupdate_option( 'permalink_structure', '/%postname%/' );\n\n\t\tglobal $wp_rewrite;\n\t\t$wp_rewrite->init();\n\n\t\t$account_url = llms_get_page_url( 'myaccount' );\n\t\t$this->go_to( $account_url );\n\n\t\t$endpoint = 'view-courses';\n\t\t$this->assertEquals(\n\t\t\t$account_url . 'my-courses/',\n\t\t\tllms_get_endpoint_url( $endpoint ),\n\t\t);\n\n\t\t/**\n\t\t * Simulate we're on a location for which there is no permalink.\n\t\t * Since llms_get_endpoint_url's third parameter `$permalink` is not passed,\n\t\t * the endpoint url is based off the current URL.\n\t\t */\n\t\t$buddypress_lifterlms_base_url = home_url( '/members/admin/courses/' );\n\t\t// e.g. on a BuddyPress courses profile base url.\n\t\t$this->go_to( $buddypress_lifterlms_base_url );\n\t\t$this->assertEquals(\n\t\t\t$buddypress_lifterlms_base_url . 'my-courses/',\n\t\t\tllms_get_endpoint_url( $endpoint ),\n\t\t);\n\n\t\t// Now on a BuddyPress courses profile page (page 2).\n\t\t$this->go_to( $buddypress_lifterlms_base_url . 'my-courses/page/2/' );\n\t\tset_query_var( 'page', 2 );\n\t\t$this->assertEquals(\n\t\t\t$buddypress_lifterlms_base_url . 'my-courses/',\n\t\t\tllms_get_endpoint_url( $endpoint ),\n\t\t);\n\n\t\t// Now on a BuddyPress my-courses profile page (page 1).\n\t\t$this->go_to( $buddypress_lifterlms_base_url . 'my-courses/' );\n\t\tset_query_var( 'page', 1 );\n\t\t$this->assertEquals(\n\t\t\t$buddypress_lifterlms_base_url . 'my-courses/',\n\t\t\tllms_get_endpoint_url( $endpoint ),\n\t\t);\n\n\t\t// Teardown.\n\t\tupdate_option( 'permalink_structure', $temp );\n\t\t$wp_rewrite->init();\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-person.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Core Functions\n *\n * @group functions_person\n * @group functions\n * @group LLMS_Student\n *\n * @since 3.8.0\n * @since 3.9.0 Add tests for `llms_get_student()`.\n * @since 3.9.0 Add tests for `llms_get_usernames_blacklist()`.\n * @since 5.0.0 Add tests for `llms_set_password_reset_cookie()` and `llms_parse_password_reset_cookie()`.\n * @since 6.0.0 Removed testing of the removed `llms_set_person_auth_cookie()` function.\n */\nclass LLMS_Test_Functions_Person extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_can_user_bypass_restrictions()\n\t *\n\t * @since 3.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_can_user_bypass_restrictions() {\n\n\t\t// Allow admins to bypass.\n\t\tupdate_option( 'llms_grant_site_access', array( 'administrator' ) );\n\n\t\t$admin      = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\t$instructor = $this->factory->user->create( array( 'role' => 'instructor' ) );\n\t\t$student    = $this->factory->user->create( array( 'role' => 'student' ) );\n\n\t\t$this->assertTrue( llms_can_user_bypass_restrictions( $admin ) );\n\t\t$this->assertFalse( llms_can_user_bypass_restrictions( $student ) );\n\n\t\t$this->assertFalse( llms_can_user_bypass_restrictions( 'fake' ) );\n\n\t\t// Pass in a student.\n\t\t$this->assertTrue( llms_can_user_bypass_restrictions( $admin ) );\n\n\t\t// Should still work with two roles.\n\t\tupdate_option( 'llms_grant_site_access', array( 'administrator', 'editor' ) );\n\t\t$this->assertTrue( llms_can_user_bypass_restrictions( $admin ) );\n\n\t\t// Test restrictions against a post.\n\t\tupdate_option( 'llms_grant_site_access', array( 'administrator', 'editor', 'instructor' ) );\n\t\t$course_id = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) );\n\t\t$course    = llms_get_post( $course_id );\n\t\t$lesson    = $course->get_lessons()[0];\n\t\t$quiz      = $lesson->get_quiz();\n\t\t$tests     = array( $course_id, $lesson->get( 'id' ), $quiz->get( 'id' ) );\n\n\t\tforeach ( $tests as $post_id ) {\n\n\t\t\t$this->assertTrue( llms_can_user_bypass_restrictions( $admin, $post_id ) );\n\t\t\t$this->assertFalse( llms_can_user_bypass_restrictions( $instructor, $post_id ) );\n\t\t\t$this->assertFalse( llms_can_user_bypass_restrictions( $student, $post_id ) );\n\n\t\t}\n\n\t\t$course->set_instructors( array(\n\t\t\tarray(\n\t\t\t\t'id' => $instructor,\n\t\t\t)\n\t\t) );\n\n\t\tforeach ( $tests as $post_id ) {\n\n\t\t\t$this->assertTrue( llms_can_user_bypass_restrictions( $admin, $post_id ) );\n\t\t\t$this->assertTrue( llms_can_user_bypass_restrictions( $instructor, $post_id ) );\n\t\t\t$this->assertFalse( llms_can_user_bypass_restrictions( $student, $post_id ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_get_minimum_password_strength_name().\n\t *\n\t * @since Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_minimum_password_strength_name() {\n\n\t\t// Default value.\n\t\t$this->assertEquals( 'strong', llms_get_minimum_password_strength_name() );\n\n\t\t// Existing options.\n\t\t$this->assertEquals( 'strong', llms_get_minimum_password_strength_name( 'strong' ) );\n\t\t$this->assertEquals( 'medium', llms_get_minimum_password_strength_name( 'medium' ) );\n\t\t$this->assertEquals( 'weak', llms_get_minimum_password_strength_name( 'weak' ) );\n\t\t$this->assertEquals( 'very weak', llms_get_minimum_password_strength_name( 'very-weak' ) );\n\n\t\t// Custom option.\n\t\t$this->assertEquals( 'fake', llms_get_minimum_password_strength_name( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_student\n\t *\n\t * @since 3.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_student() {\n\n\t\t$uid = $this->factory->user->create();\n\n\t\t$this->assertTrue( is_a( llms_get_student( $uid ), 'LLMS_Student' ) );\n\t\t$this->assertTrue( is_a( llms_get_student( new WP_User( $uid ) ), 'LLMS_Student' ) );\n\t\t$this->assertTrue( is_a( llms_get_student( new LLMS_Student( $uid ) ), 'LLMS_Student' ) );\n\n\t\t$this->assertFalse( is_a( llms_get_student( $uid + 1 ), 'LLMS_Student' ) );\n\t\t$this->assertFalse( is_a( llms_get_student( 'string' ), 'LLMS_Student' ) );\n\n\t}\n\n\t/**\n\t * Test llms_get_student autoload.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_student_autoload() {\n\n\t\t$uid = $this->factory->user->create();\n\n\t\t$this->assertFalse( llms_get_student( 0 ) );\n\n\t\t// Log in with uid.\n\t\twp_set_current_user( $uid );\n\t\t$this->assertTrue( is_a( llms_get_student( new WP_User( $uid ) ), 'LLMS_Student' ) );\n\t\t$this->assertFalse( llms_get_student( 0, false ) );\n\n\t\t// Log out.\n\t\twp_set_current_user( 0 );\n\n\t}\n\n\t/**\n\t * Test llms_get_usernames_blocklist() function.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_usernames_blocklist() {\n\n\t\t$this->assertTrue( is_array( llms_get_usernames_blocklist() ) );\n\t\t$this->assertTrue( in_array( 'admin', llms_get_usernames_blocklist(), true ) );\n\n\t}\n\n\t/**\n\t * Test llms_parse_password_reset_cookie() when no cookie is set.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_parse_password_reset_cookie_no_cookie() {\n\n\t\t$this->cookies->unset( sprintf( 'wp-resetpass-%s', COOKIEHASH ) );\n\n\t\t$res = llms_parse_password_reset_cookie();\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_no_cookie', $res );\n\n\t}\n\n\t/**\n\t * Test llms_parse_password_reset_cookie() when the cookie is malformed.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_parse_password_reset_cookie_bad_cookie() {\n\n\t\tllms_set_password_reset_cookie( 'fake' );\n\n\t\t$res = llms_parse_password_reset_cookie();\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_cookie', $res );\n\n\t}\n\n\t/**\n\t * Test llms_parse_password_reset_cookie() when the user doesn't exist.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_parse_password_reset_cookie_bad_user() {\n\n\t\t$uid = $this->factory->user->create() + 1; // Fake user.\n\n\t\tllms_set_password_reset_cookie( sprintf( '%d:fake', $uid ) );\n\n\t\t$res = llms_parse_password_reset_cookie();\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res );\n\n\t}\n\n\t/**\n\t * Test llms_parse_password_reset_cookie() when the key is invalid.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_parse_password_reset_cookie_bad_key() {\n\n\t\t$uid = $this->factory->user->create();\n\n\t\tllms_set_password_reset_cookie( sprintf( '%d:fake', $uid ) );\n\n\t\t$res = llms_parse_password_reset_cookie();\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_invalid_key', $res );\n\n\t}\n\n\t/**\n\t * Test llms_parse_password_reset_cookie() when the key is expired.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_parse_password_reset_cookie_expired_key() {\n\n\t\tadd_filter( 'password_reset_expiration', '__return_zero' );\n\n\t\t$user = $this->factory->user->create_and_get();\n\t\t$key  = get_password_reset_key( $user );\n\n\t\tllms_set_password_reset_cookie( sprintf( '%1$d:%2$s', $user->ID, $key ) );\n\n\t\t$res = llms_parse_password_reset_cookie();\n\n\t\t$this->assertWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password_reset_expired_key', $res );\n\n\t\tremove_filter( 'password_reset_expiration', '__return_zero' );\n\n\t}\n\n\t/**\n\t * Test llms_parse_password_reset_cookie() success.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_parse_password_reset_cookie_success() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\t\t$key  = get_password_reset_key( $user );\n\n\t\tllms_set_password_reset_cookie( sprintf( '%1$d:%2$s', $user->ID, $key ) );\n\n\t\t$res = llms_parse_password_reset_cookie();\n\n\t\t$this->assertEquals( $user->user_login, $res['login'] );\n\t\t$this->assertEquals( $key, $res['key'] );\n\n\t}\n\n\t/**\n\t * Test llms_set_password_reset_cookie() under default circumstances\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_set_password_reset_cookie() {\n\n\t\t$name = sprintf( 'wp-resetpass-%s', COOKIEHASH );\n\t\t$this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) );\n\n\t\t$this->assertArrayHasKey( $name, $this->cookies->get_all() );\n\t\t$this->assertEquals( array(\n\t\t\t'value'    => 'reset_pass',\n\t\t\t'expires'  => 0,\n\t\t\t'path'     => '',\n\t\t\t'domain'   => COOKIE_DOMAIN,\n\t\t\t'secure'   => false,\n\t\t\t'httponly' => true,\n\t\t), $this->cookies->get( $name ) );\n\n\t}\n\n\t/**\n\t * Test that the llms_set_password_reset_cookie fails.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_set_password_reset_cookie_fail() {\n\n\t\t// Mock failure.\n\t\t$this->cookies->expect_error();\n\n\t\t$this->assertFalse( llms_set_password_reset_cookie( 'cookieval' ) );\n\n\t}\n\n\t/**\n\t * Test llms_set_password_reset_cookie() when no value is set (expires the cookie).\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_set_password_reset_cookie_no_val() {\n\n\t\t$this->assertTrue( llms_set_password_reset_cookie() );\n\t\t$data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) );\n\t\t$this->assertEmpty( $data['value'] );\n\t\t$this->assertTrue( time() > $data['expires'] );\n\n\t}\n\n\t/**\n\t * Test llms_set_password_reset_cookie() sets the cookie path properly.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_set_password_reset_cookie_path() {\n\n\t\t// Regular URL path.\n\t\t$_SERVER['REQUEST_URI'] = '/dashboard/lost-password';\n\n\t\t$this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) );\n\t\t$data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) );\n\t\t$this->assertEquals( '/dashboard/lost-password', $data['path'] );\n\n\t\t// With query string.\n\t\t$_SERVER['REQUEST_URI'] = '/dashboard/lost-password?var1=1&var2=2';\n\n\t\t$this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) );\n\t\t$data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) );\n\t\t$this->assertEquals( '/dashboard/lost-password', $data['path'] );\n\n\t\t// Reset.\n\t\t$_SERVER['REQUEST_URI'] = '';\n\n\t}\n\n\t/**\n\t * Test llms_set_password_reset_cookie() sets a secure cookie when SSL is enabled on the site.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_set_password_reset_cookie_ssl() {\n\n\t\t// Mock is_ssl() to return `true`.\n\t\t$_SERVER['HTTPS'] = 'ON';\n\n\t\t$this->assertTrue( llms_set_password_reset_cookie( 'reset_pass' ) );\n\t\t$data = $this->cookies->get( sprintf( 'wp-resetpass-%s', COOKIEHASH ) );\n\t\t$this->assertTrue( $data['secure'] );\n\n\t\t// Reset.\n\t\tunset( $_SERVER['HTTPS'] );\n\n\t}\n\n\t/**\n\t * Test llms_set_user_login_time()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_set_user_login_time() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t$date = '2020-03-21 10:32:48';\n\t\tllms_tests_mock_current_time( $date );\n\n\t\tllms_set_user_login_time( $user->user_login, $user );\n\n\t\t$this->assertEquals( $date, get_user_meta( $user->ID, 'llms_last_login', true ) );\n\n\t}\n\n\t/**\n\t * Test llms_validate_user() with validation errors.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_validate_user_errors() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$ret = llms_validate_user( array() );\n\t\t$this->assertIsWPError( $ret );\n\t\t$this->assertWPErrorCodeEquals( 'llms-form-missing-required', $ret );\n\n\t}\n\n\t/**\n\t * Test llms_validate_user() success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_validate_user_success() {\n\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\t$data = $this->get_mock_user_data_array();\n\t\t$user = $this->factory->user->create_and_get( array(\n\t\t\t'first_name' => 'Kyle',\n\t\t\t'user_email' => $data['email_address'],\n\t\t) );\n\n\t\twp_set_current_user( $user->ID );\n\n\t\t$data['first_name'] = 'NotKyle';\n\n\t\t$this->assertTrue( llms_validate_user( $data , 'checkout' ) );\n\n\t\t$user = get_user_by( 'id', $user->ID );\n\t\t$this->assertEquals( 'Kyle', $user->first_name );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-progression.php",
    "content": "<?php\n/**\n * Test course and lesson progression functions.\n *\n * @group functions\n * @group progression_functions\n * @package  LifterLMS/Tests/Functions\n * @since    3.29.0\n * @version  3.29.0\n */\nclass LLMS_Test_Functions_Progression extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test the llms_allow_lesson_completion() method.\n\t *\n\t * @return  void\n\t * @since   3.29.0\n\t * @version 3.29.0\n\t */\n\tpublic function test_llms_allow_lesson_completion() {\n\n\t\t$student = $this->factory->student->create_and_get();\n\t\t$course = $this->factory->course->create_and_get();\n\t\t$lesson_id = $course->get_lessons( 'ids' )[0];\n\n\t\t// progression is okay with no intervention.\n\t\t$this->assertTrue( llms_allow_lesson_completion( $student->get( 'id' ), $lesson_id ) );\n\n\t\t// something somewhere prevents progression.\n\t\tadd_filter( 'llms_allow_lesson_completion', '__return_false' );\n\t\t$this->assertFalse( llms_allow_lesson_completion( $student->get( 'id' ), $lesson_id ) );\n\n\t\t// remove the filter so we don't potentially break other tests.\n\t\tremove_filter( 'llms_allow_lesson_completion', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test the llms_show_mark_complete_button() method.\n\t *\n\t * @return  void\n\t * @since   3.29.0\n\t * @version 3.29.0\n\t */\n\tpublic function test_llms_show_mark_complete_button() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 3, 'quizzes' => 2 ) );\n\t\t$no_quiz = $course->get_lessons()[0];\n\t\t$has_quiz = $course->get_lessons()[1];\n\n\t\t$has_unpublished_quiz = $course->get_lessons()[2];\n\t\t$has_unpublished_quiz->get_quiz()->set( 'status', 'draft' );\n\n\t\t$this->assertTrue( llms_show_mark_complete_button( $no_quiz ) );\n\t\t$this->assertFalse( llms_show_mark_complete_button( $has_quiz ) );\n\t\t$this->assertTrue( llms_show_mark_complete_button( $has_unpublished_quiz ) );\n\n\t}\n\n\t/**\n\t * Test llms_show_mark_complete_button() when quiz has not been attempted.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_show_mark_complete_button_quiz_not_attempted() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$student = $this->factory->student->create_and_get();\n\n\t\twp_set_current_user( $student->get( 'id' ) );\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// Quiz exists but no attempts - should not show mark complete.\n\t\t$this->assertFalse( llms_show_mark_complete_button( $lesson ) );\n\n\t}\n\n\t/**\n\t * Test llms_show_mark_complete_button() when quiz has been passed.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_show_mark_complete_button_quiz_passed() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$quiz   = $lesson->get_quiz();\n\t\t$student = $this->factory->student->create_and_get();\n\n\t\twp_set_current_user( $student->get( 'id' ) );\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// Set passing grade requirement.\n\t\t$lesson->set( 'require_passing_grade', 'yes' );\n\t\t$quiz->set( 'passing_percent', 50 );\n\n\t\t// Simulate a passing quiz attempt.\n\t\t$attempt = LLMS_Quiz_Attempt::init( $quiz->get( 'id' ), $lesson->get( 'id' ), $student->get( 'id' ) );\n\t\t$attempt->start();\n\n\t\t// Get all questions and answer them correctly (100% score).\n\t\t$questions = $attempt->get_questions();\n\t\tforeach ( $questions as $key => $question ) {\n\t\t\t$questions[ $key ]['answer'] = 'correct_answer';\n\t\t\t$questions[ $key ]['earned'] = $questions[ $key ]['points'];\n\t\t}\n\t\t$attempt->set_questions( $questions, true );\n\t\t$attempt->end();\n\n\t\t// Quiz passed - should show mark complete button.\n\t\t$this->assertTrue( llms_show_mark_complete_button( $lesson ) );\n\n\t}\n\n\t/**\n\t * Test llms_show_mark_complete_button() when quiz failed and passing is required.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_show_mark_complete_button_quiz_failed_passing_required() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$quiz   = $lesson->get_quiz();\n\t\t$student = $this->factory->student->create_and_get();\n\n\t\twp_set_current_user( $student->get( 'id' ) );\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// Set passing grade requirement.\n\t\t$lesson->set( 'require_passing_grade', 'yes' );\n\t\t$quiz->set( 'passing_percent', 80 );\n\n\t\t// Simulate a failing quiz attempt (0% score).\n\t\t$attempt = LLMS_Quiz_Attempt::init( $quiz->get( 'id' ), $lesson->get( 'id' ), $student->get( 'id' ) );\n\t\t$attempt->start();\n\n\t\t// Get all questions and answer them incorrectly (0% score).\n\t\t$questions = $attempt->get_questions();\n\t\tforeach ( $questions as $key => $question ) {\n\t\t\t$questions[ $key ]['answer'] = 'wrong_answer';\n\t\t\t$questions[ $key ]['earned'] = 0;\n\t\t}\n\t\t$attempt->set_questions( $questions, true );\n\t\t$attempt->end();\n\n\t\t// Quiz failed and passing required - should NOT show mark complete button.\n\t\t$this->assertFalse( llms_show_mark_complete_button( $lesson ) );\n\n\t}\n\n\t/**\n\t * Test llms_show_mark_complete_button() when quiz failed but passing is NOT required.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_show_mark_complete_button_quiz_failed_passing_not_required() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1, 'quizzes' => 1 ) );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$quiz   = $lesson->get_quiz();\n\t\t$student = $this->factory->student->create_and_get();\n\n\t\twp_set_current_user( $student->get( 'id' ) );\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// Passing grade is NOT required.\n\t\t$lesson->set( 'require_passing_grade', 'no' );\n\t\t$quiz->set( 'passing_percent', 80 );\n\n\t\t// Simulate a failing quiz attempt (0% score).\n\t\t$attempt = LLMS_Quiz_Attempt::init( $quiz->get( 'id' ), $lesson->get( 'id' ), $student->get( 'id' ) );\n\t\t$attempt->start();\n\n\t\t// Get all questions and answer them incorrectly (0% score).\n\t\t$questions = $attempt->get_questions();\n\t\tforeach ( $questions as $key => $question ) {\n\t\t\t$questions[ $key ]['answer'] = 'wrong_answer';\n\t\t\t$questions[ $key ]['earned'] = 0;\n\t\t}\n\t\t$attempt->set_questions( $questions, true );\n\t\t$attempt->end();\n\n\t\t// Quiz failed but passing NOT required - should show mark complete button.\n\t\t$this->assertTrue( llms_show_mark_complete_button( $lesson ) );\n\n\t}\n\n\t/**\n\t * Test the llms_show_take_quiz_button()\n\t * @return  void\n\t * @since   3.29.0\n\t * @version 3.29.0\n\t */\n\tpublic function test_llms_show_take_quiz_button() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 3, 'quizzes' => 2 ) );\n\t\t$no_quiz = $course->get_lessons()[0];\n\t\t$has_quiz = $course->get_lessons()[1];\n\n\t\t$has_unpublished_quiz = $course->get_lessons()[2];\n\t\t$has_unpublished_quiz->get_quiz()->set( 'status', 'draft' );\n\n\t\t$this->assertFalse( llms_show_take_quiz_button( $no_quiz ) );\n\t\t$this->assertTrue( llms_show_take_quiz_button( $has_quiz ) );\n\t\t$this->assertFalse( llms_show_take_quiz_button( $has_unpublished_quiz ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-template.php",
    "content": "<?php\n/**\n * Test functions template\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_template\n *\n * @since 4.8.0\n * @version 7.5.0\n */\nclass LLMS_Test_Functions_Template extends LLMS_UnitTestCase {\n\n\tprivate $themes = array(\n\t\t'fake_parent',\n\t\t'fake_child',\n\t);\n\n\t/**\n\t * Setup test cases.\n\t *\n\t * @since 4.8.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t * @since 5.9.0 Clean theme overrides directories cache.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\tforeach ( $this->themes as $theme ) {\n\t\t\t$this->_delete_theme_override_directory( $theme );\n\t\t}\n\t\twp_cache_delete( 'theme-override-directories', 'llms_template_functions' );\n\t}\n\n\t/**\n\t * Test `llms_get_template_override_directories()` when only parent theme override dir is present.\n\t *\n\t * @since 4.8.0\n\t * @since 7.5.0 Clear cache globals `$wp_template_path` since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_template_override_directories_only_parent_theme() {\n\n\t\tglobal $wp_template_path;\n\t\t$original_template = get_option( 'template' );\n\n\t\tupdate_option( 'template', 'fake_parent' );\n\t\t$wp_template_path = null;\n\n\t\t$this->_create_theme_override_directory( 'fake_parent' );\n\t\t$template_directories = llms_get_template_override_directories();\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tget_theme_root() . '/fake_parent/lifterlms'\n\t\t\t),\n\t\t\tarray_values( $template_directories )\n\t\t);\n\n\t\t$this->_delete_theme_override_directory( 'fake_parent' );\n\t\tupdate_option( 'template', $original_template );\n\t\t$wp_template_path = null;\n\t}\n\n\t/**\n\t * Test llms_get_template_override_directories() when parent and child theme override dir are present.\n\t *\n\t * @since 4.8.0\n\t * @since 7.5.0 Clear cache globals `$wp_template_path`, `$wp_stylesheet_path' since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_template_override_directories_parent_and_child_theme() {\n\n\t\tglobal $wp_template_path, $wp_stylesheet_path;\n\n\t\t$original_template   = get_option( 'template', '' );\n\t\t$original_stylesheet = get_option( 'stylesheet', '' );\n\t\tupdate_option( 'template', 'fake_parent' );\n\t\tupdate_option( 'stylesheet', 'fake_child' );\n\t\t$this->_create_theme_override_directory( 'fake_parent' );\n\t\t$this->_create_theme_override_directory( 'fake_child' );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t\t$template_directories = llms_get_template_override_directories();\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tget_theme_root() . '/fake_child/lifterlms',\n\t\t\t\tget_theme_root() . '/fake_parent/lifterlms'\n\t\t\t),\n\t\t\t$template_directories\n\t\t);\n\n\t\tupdate_option( 'template', $original_template );\n\t\tupdate_option( 'stylesheet', $original_stylesheet );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t}\n\n\t/**\n\t * Test llms_get_template_override_directories() when parent and child theme are present but only parent overrides.\n\t *\n\t * @since 4.8.0\n\t * @since 7.5.0 Clear cache globals `$wp_template_path`, `$wp_stylesheet_path' since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_template_override_directories_parent_and_child_theme_parent_overrides() {\n\n\t\tglobal $wp_template_path, $wp_stylesheet_path;\n\n\t\t$original_template   = get_option( 'template', '' );\n\t\t$original_stylesheet = get_option( 'stylesheet', '' );\n\t\tupdate_option( 'template', 'fake_parent' );\n\t\tupdate_option( 'stylesheet', 'fake_child' );\n\t\t$this->_create_theme_override_directory( 'fake_parent' );\n\t\t$this->_create_theme_override_directory( 'fake_child' );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t\trmdir(  get_theme_root() . '/fake_child/lifterlms' );\n\n\t\t$template_directories = llms_get_template_override_directories();\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tget_theme_root() . '/fake_parent/lifterlms'\n\t\t\t),\n\t\t\tarray_values( $template_directories )\n\t\t);\n\n\t\tupdate_option( 'template', $original_template );\n\t\tupdate_option( 'stylesheet', $original_stylesheet );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t}\n\n\t/**\n\t * Test llms_get_template_override_directories() when parent and child theme are present but only child overrides.\n\t *\n\t * @since 4.8.0\n\t * @since 7.5.0 Clear cache globals `$wp_template_path`, `$wp_stylesheet_path' since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_template_override_directories_parent_and_child_theme_child_overrides() {\n\n\t\tglobal $wp_template_path, $wp_stylesheet_path;\n\n\t\t$original_template   = get_option( 'template', '' );\n\t\t$original_stylesheet = get_option( 'stylesheet', '' );\n\t\tupdate_option( 'template', 'fake_parent' );\n\t\tupdate_option( 'stylesheet', 'fake_child' );\n\t\t$this->_create_theme_override_directory( 'fake_parent' );\n\t\t$this->_create_theme_override_directory( 'fake_child' );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t\trmdir(  get_theme_root() . '/fake_parent/lifterlms' );\n\n\t\t$template_directories = llms_get_template_override_directories();\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tget_theme_root() . '/fake_child/lifterlms'\n\t\t\t),\n\t\t\tarray_values( $template_directories )\n\t\t);\n\n\t\tupdate_option( 'template', $original_template );\n\t\tupdate_option( 'stylesheet', $original_stylesheet );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t}\n\n\t/**\n\t * Test llms_get_template_override_directories() when parent and child theme are present but none of them overrides.\n\t *\n\t * @since 4.8.0\n\t * @since 7.5.0 Clear cache globals `$wp_template_path`, `$wp_stylesheet_path' since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_template_override_directories_parent_and_child_theme_no_override() {\n\n\t\tglobal $wp_template_path, $wp_stylesheet_path;\n\n\t\t$original_template   = get_option( 'template', '' );\n\t\t$original_stylesheet = get_option( 'stylesheet', '' );\n\t\tupdate_option( 'template', 'fake_parent' );\n\t\tupdate_option( 'stylesheet', 'fake_child' );\n\t\t$this->_create_theme_override_directory( 'fake_parent' );\n\t\t$this->_create_theme_override_directory( 'fake_child' );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t\trmdir(  get_theme_root() . '/fake_parent/lifterlms' );\n\t\trmdir(  get_theme_root() . '/fake_child/lifterlms' );\n\n\t\t$template_directories = llms_get_template_override_directories();\n\n\t\t$this->assertEmpty( $template_directories );\n\n\t\t$this->_delete_theme_override_directory( 'fake_child' );\n\t\t$this->_delete_theme_override_directory( 'fake_parent' );\n\n\t\tupdate_option( 'template', $original_template );\n\t\tupdate_option( 'stylesheet', $original_stylesheet );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t}\n\n\t/**\n\t * Test llms_template_file_path() passing an empty template file.\n\t *\n\t * @since 5.9.0\n\t * @since 7.5.0 Clear cache globals `$wp_template_path`, `$wp_stylesheet_path' since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_file_path_empty_template_file_passed() {\n\n\t\tglobal $wp_template_path, $wp_stylesheet_path;\n\n\t\t$this->assertEquals(\n\t\t\tllms()->plugin_path() . '/templates/',\n\t\t\tllms_template_file_path( '' )\n\t\t);\n\n\t\t/**\n\t\t * Simulate the activation of a theme with the templates directory overridden.\n\t\t */\n\t\t$original_template   = get_option( 'template', '' );\n\t\t$original_stylesheet = get_option( 'stylesheet', '' );\n\t\twp_cache_delete( 'theme-override-directories', 'llms_template_functions' );\n\t\tupdate_option( 'template', 'fake' );\n\t\tupdate_option( 'stylesheet', 'fake' );\n\t\t$this->_create_theme_override_directory( 'fake' );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t\t$this->assertEquals(\n\t\t\tget_theme_root() . '/fake/lifterlms/',\n\t\t\tllms_template_file_path( '' )\n\t\t);\n\n\t\t$this->_delete_theme_override_directory( 'fake' );\n\n\t\tupdate_option( 'template', $original_template );\n\t\tupdate_option( 'stylesheet', $original_stylesheet );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t}\n\n\t/**\n\t * Test llms_template_file_path() passing a template file that doesn't exist in the theme.\n\t *\n\t * @since 5.9.0\n\t * @since 7.5.0 Clear cache globals `$wp_template_path`, `$wp_stylesheet_path' since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_file_path_template_file_not_in_theme() {\n\n\t\tglobal $wp_template_path, $wp_stylesheet_path;\n\n\t\t/**\n\t\t * Simulate the activation of a theme with the templates directory overridden.\n\t\t */\n\t\t$original_template   = get_option( 'template', '' );\n\t\t$original_stylesheet = get_option( 'stylesheet', '' );\n\t\tupdate_option( 'template', 'fake' );\n\t\tupdate_option( 'stylesheet', 'fake' );\n\t\t$this->_create_theme_override_directory( 'fake' );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t\t$this->assertEquals(\n\t\t\tllms()->plugin_path() . '/templates/single-certificate.php',\n\t\t\tllms_template_file_path( 'single-certificate.php' )\n\t\t);\n\n\t\t$this->_delete_theme_override_directory( 'fake' );\n\n\t\tupdate_option( 'template', $original_template );\n\t\tupdate_option( 'stylesheet', $original_stylesheet );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t}\n\n\t/**\n\t * Test llms_template_file_path() when passing an absolute template directory (not relative to the plugin dir).\n\t *\n\t * @since 5.9.0\n\t * @since 7.2.0 Stop expecting leading slash added to absolute paths.\n\t * @since 7.5.0 Clear cache globals `$wp_template_path`, `$wp_stylesheet_path' since WP 6.4.\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_file_path_template_directory_absolute() {\n\n\t\tglobal $wp_template_path, $wp_stylesheet_path;\n\n\t\t$this->_delete_theme_override_directory( 'fake' );\n\n\t\t$this->assertEquals(\n\t\t\t'/path/to/absolute/single-certificate.php',\n\t\t\tllms_template_file_path( 'single-certificate.php', '/path/to/absolute', true )\n\t\t);\n\n\t\t/**\n\t\t * Simulate the activation of a theme with the templates directory overridden.\n\t\t */\n\t\t$original_template   = get_option( 'template', '' );\n\t\t$original_stylesheet = get_option( 'stylesheet', '' );\n\t\tupdate_option( 'template', 'fake' );\n\t\tupdate_option( 'stylesheet', 'fake' );\n\t\t$this->_create_theme_override_directory( 'fake' );\n\t\twp_cache_delete( 'theme-override-directories', 'llms_template_functions' );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t\t$this->assertEquals(\n\t\t\tget_theme_root() . '/fake/lifterlms/',\n\t\t\tllms_template_file_path( '', 'path/to/absolute', true )\n\t\t);\n\n\t\t$this->_delete_theme_override_directory( 'fake' );\n\n\t\tupdate_option( 'template', $original_template );\n\t\tupdate_option( 'stylesheet', $original_stylesheet );\n\t\t$wp_template_path = null;\n\t\t$wp_stylesheet_path = null;\n\n\t}\n\n\t/**\n\t * Creates a theme and override lifterlms template directory.\n\t *\n\t * @since 4.8.0\n\t * @since 5.9.0 Always remove the theme directory if it already exists.\n\t *\n\t * @param string $theme_dir_name Theme directory name.\n\t * @return void\n\t */\n\tprivate function _create_theme_override_directory( $theme_dir_name ) {\n\t\t$theme_root = get_theme_root();\n\t\t$this->_delete_theme_override_directory( 'fake' );\n\t\tmkdir( \"{$theme_root}/{$theme_dir_name}/lifterlms\", 0777, true );\n\t}\n\n\t/**\n\t * Deletes a theme and override lifterlms template directory.\n\t *\n\t * @since 4.8.0\n\t *\n\t * @param string $theme_dir_name Theme directory name.\n\t * @return void\n\t */\n\tprivate function _delete_theme_override_directory( $theme_dir_name ) {\n\t\t$theme_root = get_theme_root();\n\t\tif ( is_dir( \"{$theme_root}/{$theme_dir_name}/lifterlms\" ) ) {\n\t\t\trmdir( \"{$theme_root}/{$theme_dir_name}/lifterlms\" );\n\t\t}\n\t\tif ( is_dir( \"{$theme_root}/{$theme_dir_name}\" ) ) {\n\t\t\trmdir(  \"{$theme_root}/{$theme_dir_name}\" );\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-certificates.php",
    "content": "<?php\n/**\n * Test certificate template functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_template\n * @group functions_template_certificates\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Functions_Templates_Certificates extends LLMS_UnitTestCase {\n\n\t/**\n\t * Retrieve a certificate for testing.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param array $args     Certificate creation arguments.\n\t * @parak bool  $template If `true` retrieves a certificate template object in favor of an awarded certificate.\n\t * @return LLMS_User_Certificate\n\t */\n\tprivate function get_cert( $args = array(), $template = false ) {\n\t\t$post_type = $template ? 'llms_certificate' : 'llms_my_certificate';\n\t\treturn llms_get_certificate( $this->factory->post->create( wp_parse_args( $args, compact( 'post_type' ) ) ), $template );\n\t}\n\n\t/**\n\t * Test llms_certificate_content() with a v1 certificate template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_content_v1() {\n\n\t\t$cert = $this->get_cert();\n\n\t\t$output = $this->get_output( 'llms_certificate_content', array( $cert ) );\n\n\t\t$this->assertStringContains( '<div class=\"llms-certificate-container\" style=\"width:800px; height:616px;\">', $output );\n\t\t$this->assertStringContains( sprintf( '<div id=\"certificate-%d\" class=\"\">', $cert->get( 'id' ) ), $output );\n\n\t}\n\n\t/**\n\t * Test llms_certificate_content() with a v2 certificate template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_content_v2() {\n\n\t\t$cert = $this->get_cert( array( 'post_content' => '' ) );\n\n\t\t$this->assertOutputContains(\n\t\t\tsprintf( '<div id=\"certificate-%d\" class=\"llms-certificate-container cert-template-v2\">', $cert->get( 'id' ) ),\n\t\t\t'llms_certificate_content',\n\t\t\tarray( $cert )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test llms_certificate_styles() with an invalid post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_styles_not_a_cert() {\n\n\t\tglobal $post;\n\t\t$post = $this->factory->post->create_and_get();\n\n\t\t$this->assertOutputEmpty( 'llms_certificate_styles' );\n\n\t\t$post = null;\n\n\t}\n\n\t/**\n\t * Test llms_certificate_styles() with an v1 template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_styles_v1() {\n\n\t\tglobal $post;\n\t\t$post =  $this->factory->post->create_and_get( array( 'post_type' => 'llms_my_certificate' ) );\n\n\t\t$this->assertOutputEmpty( 'llms_certificate_styles' );\n\n\t\t$post = null;\n\n\t}\n\n\t/**\n\t * Test llms_certificate_styles() with an v2 template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_styles_v2() {\n\n\t\tglobal $post;\n\t\t$post =  $this->factory->post->create_and_get( array( 'post_type' => 'llms_my_certificate', 'post_content' => '' ) );\n\n\t\t$output = $this->get_output( 'llms_certificate_styles' );\n\t\t$this->assertStringContains( '<style type=\"text/css\">', $output );\n\t\t$this->assertStringContains( '<style type=\"text/css\" media=\"print\">', $output );\n\n\t\t$post = null;\n\n\t}\n\n\t/**\n\t * Test llms_certificate_actions().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_actions() {\n\n\t\t$cert = $this->get_cert();\n\n\t\t// Cannot manage.\n\t\twp_set_current_user( null );\n\t\t$this->assertOutputEmpty( 'llms_certificate_actions', array( $cert ) );\n\n\t\t// Can manage.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertOutputContains(\n\t\t\t'<div class=\"llms-print-certificate no-print\" id=\"llms-print-certificate\">',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\n\t\t// Back link.\n\t\t$this->assertOutputContains(\n\t\t\t'<a class=\"llms-cert-return-link\" ',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\t// Print button.\n\t\t$this->assertOutputContains(\n\t\t\t'<button class=\"llms-button-secondary\" type=\"submit\" name=\"llms_generate_cert\">',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\t// Sharing button.\n\t\t$this->assertOutputContains(\n\t\t\t'<button class=\"llms-button-secondary\" type=\"submit\" name=\"llms_enable_cert_sharing\"',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test llms_certificate_actions().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_actions_template() {\n\n\t\t$cert = $this->get_cert( array(), true );\n\n\t\t// Cannot manage.\n\t\twp_set_current_user( null );\n\t\t$this->assertOutputEmpty( 'llms_certificate_actions', array( $cert ) );\n\n\t\t// Can manage.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertOutputContains(\n\t\t\t'<div class=\"llms-print-certificate no-print\" id=\"llms-print-certificate\">',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\t// No backlink.\n\t\t$this->assertOutputNotContains(\n\t\t\t'<a class=\"llms-cert-return-link\" ',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\t// Print button.\n\t\t$this->assertOutputContains(\n\t\t\t'<button class=\"llms-button-secondary\" type=\"submit\" name=\"llms_generate_cert\">',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\t// Sharing button.\n\t\t$this->assertOutputNotContains(\n\t\t\t'<button class=\"llms-button-secondary\" type=\"submit\" name=\"llms_enable_cert_sharing\"',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test llms_certificate_actions().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificate_actions_backlink() {\n\n\t\t$cert = $this->get_cert();\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$this->assertOutputContains(\n\t\t\t'<a class=\"llms-cert-return-link\" ',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\t$this->assertOutputContains(\n\t\t\t'All certificates</a>',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\tupdate_option( 'lifterlms_myaccount_certificates_endpoint', '' );\n\n\t\t$this->assertOutputContains(\n\t\t\t'<a class=\"llms-cert-return-link\" ',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\t$this->assertOutputContains(\n\t\t\t'Dashboard</a>',\n\t\t\t'llms_certificate_actions', array( $cert )\n\t\t);\n\n\t\tupdate_option( 'lifterlms_myaccount_certificates_endpoint', 'my-certificates' );\n\n\t}\n\n\t// public function test_llms_get_certificate_preview() {}\n\t// public function test_llms_the_certificate_preview() {}\n\n\t/**\n\t * Test llms_get_certificates_loop_columns().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_certificates_loop_columns() {\n\t\t$this->assertTrue( is_int( llms_get_certificates_loop_columns() ) );\n\t}\n\n\t/**\n\t * Test lifterlms_template_certificates_loop() when there's no logged in student.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_certificates_loop_no_student() {\n\n\t\t$this->assertOutputEmpty( 'lifterlms_template_certificates_loop' );\n\n\t}\n\n\t/**\n\t * Test llms_certificates_remove_print_styles() on invalid post type.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificates_remove_print_styles_wrong_post_type() {\n\n\t\tglobal $post;\n\n\t\t$post = $this->factory->post->create_and_get();\n\n\t\t$this->assertFalse( llms_certificates_remove_print_styles() );\n\t\t$post = null;\n\n\t}\n\n\t/**\n\t * Test llms_certificates_remove_print_styles().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_certificates_remove_print_styles() {\n\n\t\tglobal $post;\n\t\t$callback = function( $list ) {\n\t\t\t$list[] = 'fake-print-style-safe';\n\t\t\treturn $list;\n\t\t};\n\t\tadd_filter( 'llms_certificate_print_styles_safelist', $callback );\n\n\t\tforeach ( array( 'llms_certificate', 'llms_my_certificate' ) as $post_type ) {\n\n\t\t\t$post = $this->factory->post->create_and_get( compact( 'post_type' ) );\n\n\t\t\twp_enqueue_style( 'fake-print-style', 'https://fake.tld/print.css', array(), '1.0.0', 'print' );\n\t\t\twp_enqueue_style( 'fake-print-style-safe', 'https://fake.tld/print-safe.css', array(), '1.0.0', 'print' );\n\t\t\twp_enqueue_style( 'fake-style', 'https://fake.tld/style.css', array(), '1.0.0' );\n\n\t\t\t$this->assertTrue( llms_certificates_remove_print_styles() );\n\n\t\t\t// Print style is removed.\n\t\t\t$this->assertFalse( wp_style_is( 'fake-print-style' ) );\n\n\t\t\t// Safelisted print style is not removed.\n\t\t\t$this->assertTrue( wp_style_is( 'fake-print-style-safe' ) );\n\n\t\t\t// Non-print style is not removed.\n\t\t\t$this->assertTrue( wp_style_is( 'fake-style' ) );\n\n\t\t}\n\n\t\tremove_filter( 'llms_certificate_print_styles_safelist', $callback );\n\t\t$post = null;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-dasbhoard.php",
    "content": "<?php\n/**\n * Test dashboard template functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_template\n * @group functions_template_dashboard\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Functions_Templates_Dashboard extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_achievements() with no student.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_achievements_no_student() {\n\n\t\twp_set_current_user( null );\n\n\t\t$this->assertOutputEmpty( 'lifterlms_template_student_dashboard_my_achievements' );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_achievements() when the endpoint is disabled.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_achievements_disabled() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\tupdate_option( 'lifterlms_myaccount_achievements_endpoint', '' );\n\n\t\t$this->assertOutputEmpty( 'lifterlms_template_student_dashboard_my_achievements' );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_achievements() when showing a preview.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_achievements_preview() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t$output = $this->get_output( 'lifterlms_template_student_dashboard_my_achievements', array( true ) );\n\n\t\t$this->assertStringContainsString( '<section class=\"llms-sd-section llms-my-achievements\">', $output );\n\t\t$this->assertStringContainsString( '<h3 class=\"llms-sd-section-title\">', $output );\n\t\t$this->assertStringContainsString( '<a class=\"llms-button-secondary\" href=\"?my-achievements\">View All My Achievements</a>', $output );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_achievements() when showing all.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_achievements_all() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t$output = $this->get_output( 'lifterlms_template_student_dashboard_my_achievements' );\n\n\t\t$this->assertStringContainsString( '<section class=\"llms-sd-section llms-my-achievements\">', $output );\n\n\t\t$this->assertStringNotContainsString( '<h3 class=\"llms-sd-section-title\">', $output );\n\t\t$this->assertStringNotContainsString( '<a class=\"llms-button-secondary\" href=\"?my-achievements\">View All My Achievements</a>', $output );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_certificates() with no student.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_certificates_no_student() {\n\n\t\twp_set_current_user( null );\n\n\t\t$this->assertOutputEmpty( 'lifterlms_template_student_dashboard_my_certificates' );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_certificates() when the endpoint is disabled.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_certificates_disabled() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\tupdate_option( 'lifterlms_myaccount_certificates_endpoint', '' );\n\n\t\t$this->assertOutputEmpty( 'lifterlms_template_student_dashboard_my_certificates' );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_certificates() when showing a preview.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_certificates_preview() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t$output = $this->get_output( 'lifterlms_template_student_dashboard_my_certificates', array( true ) );\n\n\t\t$this->assertStringContainsString( '<section class=\"llms-sd-section llms-my-certificates\">', $output );\n\t\t$this->assertStringContainsString( '<h3 class=\"llms-sd-section-title\">', $output );\n\t\t$this->assertStringContainsString( '<a class=\"llms-button-secondary\" href=\"?my-certificates\">View All My Certificates</a>', $output );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_student_dashboard_my_certificates() when showing all.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_student_dashboard_my_certificates_all() {\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t$output = $this->get_output( 'lifterlms_template_student_dashboard_my_certificates' );\n\n\t\t$this->assertStringContainsString( '<section class=\"llms-sd-section llms-my-certificates\">', $output );\n\n\t\t$this->assertStringNotContainsString( '<h3 class=\"llms-sd-section-title\">', $output );\n\t\t$this->assertStringNotContainsString( '<a class=\"llms-button-secondary\" href=\"?my-certificates\">View All My Certificates</a>', $output );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-loop.php",
    "content": "<?php\n/**\n * Test page functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_loop\n *\n * @since 3.38.0\n */\nclass LLMS_Test_Functions_Loop extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test lifterlms_get_archive_description() and lifterlms_archive_description() on course and course taxonomy catalogs.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_get_archive_description_courses() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t// On courses page with no description.\n\t\t$this->go_to( get_post_type_archive_link( 'course' ) );\n\t\t$this->assertEquals( '', lifterlms_get_archive_description() );\n\t\t$this->assertEquals( '', $this->get_output( 'lifterlms_archive_description' ) );\n\n\t\t// On courses page with a description.\n\t\twp_update_post( array(\n\t\t\t'ID'           => llms_get_page_id( 'courses' ),\n\t\t\t'post_content' => 'Archive Description',\n\t\t) );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) );\n\n\t\t// On a tax archive page with no tax description.\n\t\t$term = wp_insert_term( 'mock-cat', 'course_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) );\n\n\t\t// On a tax archive page with a tax description.\n\t\t$term = wp_insert_term( 'mock-cat-with-desc', 'course_cat', array( 'description' => 'Term desc.' ) );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertEquals( llms_content( 'Term desc.' ), lifterlms_get_archive_description() );\n\t\t$this->assertEquals( llms_content( 'Term desc.' ), $this->get_output( 'lifterlms_archive_description' ) );\n\n\t}\n\n\t/**\n\t * Test lifterlms_get_archive_description() and lifterlms_archive_description() on membership and membership taxonomy catalogs.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_get_archive_description_memberships() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t// On courses page with no description.\n\t\t$this->go_to( get_post_type_archive_link( 'llms_membership' ) );\n\t\t$this->assertEquals( '', lifterlms_get_archive_description() );\n\t\t$this->assertEquals( '', $this->get_output( 'lifterlms_archive_description' ) );\n\n\t\t// On courses page with a description.\n\t\twp_update_post( array(\n\t\t\t'ID'           => llms_get_page_id( 'memberships' ),\n\t\t\t'post_content' => 'Archive Description',\n\t\t) );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) );\n\n\t\t// On a tax archive page with no tax description.\n\t\t$term = wp_insert_term( 'mock-cat', 'membership_cat' );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), lifterlms_get_archive_description() );\n\t\t$this->assertEquals( llms_content( 'Archive Description' ), $this->get_output( 'lifterlms_archive_description' ) );\n\n\t\t// On a tax archive page with a tax description.\n\t\t$term = wp_insert_term( 'mock-cat-with-desc', 'membership_cat', array( 'description' => 'Term desc.' ) );\n\t\t$this->go_to( get_term_link( $term['term_id'] ) );\n\t\t$this->assertEquals( llms_content( 'Term desc.' ), lifterlms_get_archive_description() );\n\t\t$this->assertEquals( llms_content( 'Term desc.' ), $this->get_output( 'lifterlms_archive_description' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-pricing-table.php",
    "content": "<?php\n/**\n * Test product pricing table template functions\n *\n * @package  LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_template\n * @group functions_template_product\n *\n * @since 3.38.0\n */\nclass LLMS_Test_Functions_Templates_Pricing_Table extends LLMS_UnitTestCase {\n\n\t// Uncovered methods.\n\n\t// public function test_llms_get_access_plan_classes() {}\n\t// public function test_llms_template_access_plan() {}\n\t// public function test_llms_template_access_plan_button() {}\n\t// public function test_llms_template_access_plan_description() {}\n\t// public function test_llms_template_access_plan_feature() {}\n\t// public function test_llms_template_access_plan_pricing() {}\n\t// public function test_llms_template_access_plan_restrictions() {}\n\t// public function test_llms_template_access_plan_title() {}\n\t// public function test_llms_template_access_plan_trial() {}\n\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): gateways disabled so we should show only free plans.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_free_only() {\n\n\t\t$this->setManualGatewayStatus( 'no' );\n\n\t\t$plan = $this->get_mock_plan( 0 );\n\n\t\t$actions = did_action( 'llms_access_plan' );\n\n\t\t$output = $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) );\n\n\t\t// Check HTML output.\n\t\t$this->assertStringContains( sprintf( 'id=\"llms-access-plan-%d\"', $plan->get( 'id' ) ), $output );\n\t\t$this->assertStringContains( sprintf( '<h4 class=\"llms-access-plan-title\">%s</h4>', $plan->get( 'title' ) ), $output );\n\t\t$this->assertStringContains( 'FREE', $output );\n\n\t\t// Action ran.\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_access_plan' ) );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): paid plan with gateways enabled.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_purchasable() {\n\n\t\t$this->setManualGatewayStatus( 'yes' );\n\n\t\t$plan = $this->get_mock_plan();\n\n\t\t$actions = did_action( 'llms_access_plan' );\n\n\t\t$output = $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) );\n\n\t\t// Check HTML output.\n\t\t$this->assertStringContains( sprintf( 'id=\"llms-access-plan-%d\"', $plan->get( 'id' ) ), $output );\n\t\t$this->assertStringContains( sprintf( '<h4 class=\"llms-access-plan-title\">%s</h4>', $plan->get( 'title' ) ), $output );\n\t\t$this->assertStringContains( (string) $plan->get( 'price' ), $output );\n\n\t\t// Action ran.\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_access_plan' ) );\n\n\t\t$this->setManualGatewayStatus( 'no' );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): paid plan with no enabled gateways.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_not_purchasable() {\n\n\t\t$this->setManualGatewayStatus( 'no' );\n\n\t\t$plan = $this->get_mock_plan();\n\n\t\t$actions = did_action( 'lifterlms_product_not_purchasable' );\n\n\t\t// Empty output (just a bunch of new lines, actually).\n\t\t$output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) );\n\n\t\t// Action ran.\n\t\t$this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) );\n\n\t\t$this->assertEquals( '', $output );\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): course enrollment start is in future.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_err_enrollment_period_starts_in_future() {\n\n\t\t$plans = array(\n\t\t\t$this->get_mock_plan(),\n\t\t\t$this->get_mock_plan( 0 ),\n\t\t);\n\n\t\tforeach ( $plans as $plan ) {\n\n\t\t\t$course = llms_get_post( $plan->get( 'product_id' ) );\n\t\t\t$course->set( 'enrollment_period', 'yes' );\n\t\t\t$course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) );\n\t\t\t$course->set( 'enrollment_opens_message', 'Enrollment closed.' );\n\n\t\t\t$actions = did_action( 'lifterlms_product_not_purchasable' );\n\n\t\t\t$output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) );\n\n\t\t\t// Test HTML output.\n\t\t\t$this->assertStringContains( 'class=\"llms-notice llms-error\"', $output );\n\t\t\t$this->assertStringContains( 'Enrollment closed.', $output );\n\n\t\t\t// Action ran.\n\t\t\t$this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): course enrollment start is in past.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_err_enrollment_period_starts_in_past() {\n\n\t\t$plans = array(\n\t\t\t$this->get_mock_plan(),\n\t\t\t$this->get_mock_plan( 0 ),\n\t\t);\n\n\t\tforeach ( $plans as $plan ) {\n\n\t\t\t$course = llms_get_post( $plan->get( 'product_id' ) );\n\t\t\t$course->set( 'enrollment_period', 'yes' );\n\t\t\t$course->set( 'enrollment_end_date', date( 'Y-m-d h:i:s', strtotime( '-1 day' ) ) );\n\t\t\t$course->set( 'enrollment_closed_message', 'Enrollment closed.' );\n\n\t\t\t$actions = did_action( 'lifterlms_product_not_purchasable' );\n\n\t\t\t$output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) );\n\n\t\t\t// Test HTML output.\n\t\t\t$this->assertStringContains( 'class=\"llms-notice llms-error\"', $output );\n\t\t\t$this->assertStringContains( 'Enrollment closed.', $output );\n\n\t\t\t// Action ran.\n\t\t\t$this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): course capacity maxed error\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_err_capacity() {\n\n\t\t$plans = array(\n\t\t\t$this->get_mock_plan(),\n\t\t\t$this->get_mock_plan( 0 ),\n\t\t);\n\n\t\tforeach ( $plans as $plan ) {\n\t\t\t$course = llms_get_post( $plan->get( 'product_id' ) );\n\t\t\t$course->set( 'enable_capacity', 'yes' );\n\t\t\t$course->set( 'capacity', 1 );\n\t\t\t$course->set( 'capacity_message', 'No more room.' );\n\n\t\t\t$student = $this->get_mock_student();\n\t\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t\t$actions = did_action( 'lifterlms_product_not_purchasable' );\n\n\t\t\t$output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) );\n\n\t\t\t// Test HTML output.\n\t\t\t$this->assertStringContains( 'class=\"llms-notice llms-error\"', $output );\n\t\t\t$this->assertStringContains( 'No more room.', $output );\n\n\t\t\t// Action ran.\n\t\t\t$this->assertEquals( ++$actions, did_action( 'lifterlms_product_not_purchasable' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): user already enrolled in course\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_enrolled_course() {\n\n\t\t$plan    = $this->get_mock_plan();\n\t\t$course  = llms_get_post( $plan->get( 'product_id' ) );\n\t\t$student = $this->get_mock_student();\n\n\t\t$student->enroll( $course->get( 'id' ) );\n\t\twp_set_current_user( $student->get( 'id' ) );\n\n\t\t// Empty output (just a bunch of new lines, actually).\n\t\t$output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) );\n\n\t\t// No actions ran.\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_product_not_purchasable' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_access_plan' ) );\n\n\t\t$this->assertEquals( '', $output );\n\n\n\t}\n\n\t/**\n\t * Test lifterlms_template_pricing_table(): user already enrolled in membership\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_pricing_table_enrolled_membership() {\n\n\t\t$plan          = $this->get_mock_plan();\n\t\t$membership_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) );\n\t\t$student       = $this->get_mock_student();\n\n\t\t$plan->set( 'product_id', $membership_id );\n\t\t$student->enroll( $membership_id );\n\t\twp_set_current_user( $student->get( 'id' ) );\n\n\t\t// Empty output (just a bunch of new lines, actually).\n\t\t$output = trim( $this->get_output( 'lifterlms_template_pricing_table', array( $plan->get( 'product_id' ) ) ) );\n\n\t\t// No actions ran.\n\t\t$this->assertEquals( 0, did_action( 'lifterlms_product_not_purchasable' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_access_plan' ) );\n\n\t\t$this->assertEquals( '', $output );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-templates-vier-order.php",
    "content": "<?php\n/**\n * Test certificate template functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group functions_template\n * @group functions_template_view_order\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Functions_Templates_View_Order extends LLMS_UnitTestCase {\n\n\t/**\n\t * Utility used to get the number of times a set of actions has run.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return array\n\t */\n\tprivate function get_action_counts( $func ) {\n\n\t\tswitch ( $func ) {\n\n\t\t\tcase 'llms_template_view_order':\n\t\t\t\t$list = array(\n\t\t\t\t\t'lifterlms_before_view_order_table',\n\t\t\t\t\t'llms_view_order_information',\n\t\t\t\t\t'llms_view_order_actions',\n\t\t\t\t\t'llms_view_order_transactions',\n\t\t\t\t\t'lifterlms_after_view_order_table',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_template_view_order_actions':\n\t\t\t\t$list = array(\n\t\t\t\t\t'llms_view_order_before_secondary',\n\t\t\t\t\t'llms_view_order_after_secondary',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t\tcase 'llms_template_view_order_information':\n\t\t\t\t$list = array(\n\t\t\t\t\t'lifterlms_view_order_table_body',\n\t\t\t\t);\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\t$actions = array_fill_keys( $list, 0 );\n\t\tforeach ( $actions as $action => &$count ) {\n\t\t\t$count = did_action( $action );\n\t\t}\n\n\t\treturn $actions;\n\n\t}\n\n\t/**\n\t * Test llms_template_view_order() with invalid input.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_view_order_not_an_order() {\n\n\t\t$post  = $this->factory->post->create_and_get( array( 'post_type' => 'course' ) );\n\t\t$tests = array(\n\t\t\t$post,\n\t\t\tllms_get_post( $post ),\n\t\t\t$post->ID,\n\t\t\tfalse,\n\t\t\tnew stdClass(),\n\t\t);\n\t\tforeach ( $tests as $input ) {\n\t\t\t$this->assertOutputEquals( 'Invalid Order.', 'llms_template_view_order', array( $input ) );\n\t\t}\n\n\t\tforeach ( $this->get_action_counts( 'llms_template_view_order' ) as $action => $count ) {\n\t\t\t$this->assertSame( 0, $count, $action );\n\t\t}\n\n\n\t}\n\n\t/**\n\t * Test llms_template_view_order() when accessed by another user.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_view_order_wrong_user() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$this->assertOutputEquals( 'Invalid Order.', 'llms_template_view_order', array( $order ) );\n\n\t\tforeach ( $this->get_action_counts( 'llms_template_view_order' ) as $action => $count ) {\n\t\t\t$this->assertSame( 0, $count, $action );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_template_view_order().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_view_order() {\n\n\t\t$actions = $this->get_action_counts( 'llms_template_view_order' );\n\n\t\t$order = $this->get_mock_order();\n\t\twp_set_current_user( $order->get( 'user_id' ) );\n\n\t\t$res = $this->get_output( 'llms_template_view_order', array( $order ) );\n\n\t\t$this->assertStringContainsString( '<div class=\"llms-sd-section llms-view-order\">', $res );\n\t\t$this->assertStringContainsString( sprintf( 'Order #%d', $order->get( 'id' ) ), $res );\n\n\t\tforeach ( $this->get_action_counts( 'llms_template_view_order' ) as $action => $count ) {\n\t\t\t$this->assertEquals( $actions[ $action] + 1, $count, $action );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_template_view_order_actions().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_view_order_actions() {\n\n\t\t$actions = $this->get_action_counts( 'llms_template_view_order_actions' );\n\n\t\t$order = $this->get_mock_order();\n\n\t\t$res = $this->get_output( 'llms_template_view_order_actions', array( $order ) );\n\n\t\t$this->assertStringContainsString( '<aside class=\"order-secondary\">', $res );\n\n\t\tforeach ( $this->get_action_counts( 'llms_template_view_order_actions' ) as $action => $count ) {\n\t\t\t$this->assertEquals( $actions[ $action] + 1, $count, $action );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_template_view_order_information().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_view_order_information() {\n\n\t\t$actions = $this->get_action_counts( 'llms_template_view_order_information' );\n\n\t\t$order = $this->get_mock_order();\n\n\t\t$res = $this->get_output( 'llms_template_view_order_information', array( $order ) );\n\n\t\t$this->assertStringContainsString( '<section class=\"order-primary\">', $res );\n\n\t\tforeach ( $this->get_action_counts( 'llms_template_view_order_information' ) as $action => $count ) {\n\t\t\t$this->assertEquals( $actions[ $action] + 1, $count, $action );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_template_view_order_transactions() for an order with no transactions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_view_order_transactions_no_txns() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$this->assertOutputEmpty( 'llms_template_view_order_transactions', array( $order ) );\n\n\t}\n\n\t/**\n\t * Test llms_template_view_order_transactions() for an order with no transactions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_view_order_transactions() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$order->record_transaction();\n\n\t\t$res = $this->get_output( 'llms_template_view_order_transactions', array( $order ) );\n\n\t\t$this->assertStringContainsString( '<table class=\"orders-table transactions\" id=\"llms-txns\">', $res );\n\n\t\t// No pagination.\n\t\t$this->assertStringNotContainsString( '<tfoot>', $res );\n\n\t\t// Test pagination.\n\t\t$order->record_transaction();\n\t\t$order->record_transaction();\n\n\t\t$filter = function( $count ) {\n\t\t\treturn 2;\n\t\t};\n\t\tadd_filter( 'llms_student_dashboard_transactions_per_page', $filter );\n\n\t\t$res = $this->get_output( 'llms_template_view_order_transactions', array( $order ) );\n\n\t\t$this->assertStringContainsString( '<tfoot>', $res );\n\t\t$this->assertStringContainsString( '<a class=\"llms-button-secondary small\" href=\"?txnpage=2#llms-txns\">Next</a>', $res );\n\n\t\t// Two transactions in the table.\n\t\t$dom = llms_get_dom_document( $res );\n\t\t$this->assertEquals( 2, $dom->getElementsByTagName( 'tbody' )[0]->getElementsByTagName( 'tr' )->length );\n\n\t\t// Go to page 2.\n\t\t$this->mockGetRequest( array( 'txnpage' => 2 ) );\n\t\t$res = $this->get_output( 'llms_template_view_order_transactions', array( $order ) );\n\n\t\t$this->assertStringContainsString( '<tfoot>', $res );\n\t\t$this->assertStringContainsString( '<a class=\"llms-button-secondary small\" href=\"?txnpage=1#llms-txns\">Back</a>', $res );\n\n\t\t// One transaction in the table.\n\t\t$dom = llms_get_dom_document( $res );\n\t\t$this->assertEquals( 1, $dom->getElementsByTagName( 'tbody' )[0]->getElementsByTagName( 'tr' )->length );\n\n\t\tremove_filter( 'llms_student_dashboard_transactions_per_page', $filter );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-updates.php",
    "content": "<?php\n/**\n* Test update utility functions\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Functions_Updates extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/llms.functions.updates.php';\n\t}\n\n\t/**\n\t * Test llms_update_util_get_items_per_page()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_update_util_get_items_per_page() {\n\t\t$ret = llms_update_util_get_items_per_page();\n\t\t$this->assertTrue( is_int( $ret ) );\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-user-information-fields.php",
    "content": "<?php\n/**\n * Test user information field functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group user_info_fields\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Functions_User_Info_fields extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test llms_get_user_information_field()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_user_information_field() {\n\n\t\t// Does not exist.\n\t\t$this->assertFalse( llms_get_user_information_field( 'fake' ) );\n\n\t\t// Does exist.\n\t\t$field = llms_get_user_information_field( 'email_address' );\n\n\t\t$this->assertEquals( array( 'id', 'name', 'type', 'label', 'data_store', 'data_store_key' ), array_keys( $field ) );\n\n\t\t$this->assertEquals( 'user_email', $field['data_store_key'] );\n\t\t$this->assertEquals( 'users', $field['data_store'] );\n\t\t$this->assertEquals( 'email_address', $field['name'] );\n\t\t$this->assertEquals( 'email_address', $field['id'] );\n\n\t}\n\n\t/**\n\t * Test llms_get_user_information_fields()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_get_user_information_fields() {\n\n\t\t$list = llms_get_user_information_fields();\n\n\t\t$ids = array();\n\n\t\tforeach ( $list as $field ) {\n\n\t\t\t$this->assertArrayHasKey( 'id', $field );\n\t\t\t$this->assertArrayHasKey( 'data_store', $field );\n\t\t\t$this->assertArrayHasKey( 'data_store_key', $field );\n\n\t\t\t$ids[] = $field['id'];\n\n\t\t}\n\n\t\t$expected_ids = array(\n\t\t\t'user_login',\n\t\t\t'email_address',\n\t\t\t'password',\n\t\t\t'first_name',\n\t\t\t'last_name',\n\t\t\t'display_name',\n\t\t\t'llms_billing_address_1',\n\t\t\t'llms_billing_address_2',\n\t\t\t'llms_billing_city',\n\t\t\t'llms_billing_country',\n\t\t\t'llms_billing_state',\n\t\t\t'llms_billing_zip',\n\t\t\t'llms_phone',\n\t\t);\n\t\t$this->assertEquals( $expected_ids, $ids );\n\t}\n\n\t/**\n\t * Test _llms_add_user_info_to_merge_buttons()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tfunction test__llms_add_user_info_to_merge_buttons() {\n\n\t\t$input  = array( '{code}' => 'Desc' );\n\t\t$screen = get_current_screen();\n\n\t\t$this->assertEquals( $input, _llms_add_user_info_to_merge_buttons( $input, $screen ) );\n\n\t\tforeach ( array( 'llms_certificate', 'llms_email' ) as $post_type ) {\n\n\t\t\tllms_tests_mock_current_screen( $post_type );\n\n\t\t\t$screen = get_current_screen();\n\t\t\t$res    = _llms_add_user_info_to_merge_buttons( $input, $screen );\n\n\t\t\t$this->assertArrayHasKey( array_keys( $input )[0], $res );\n\n\t\t\t$this->assertEquals( 'Email Address', $res['[llms-user user_email]'] );\n\t\t\t$this->assertEquals( 'Address Line 2', $res['[llms-user llms_billing_address_2]'] );\n\t\t\t$this->assertEquals( 'Phone Number', $res['[llms-user llms_phone]'] );\n\n\t\t\t$this->assertFalse( array_key_exists( '[llms-user user_pass]', $res ) );\n\n\t\t\tllms_tests_reset_current_screen();\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-functions-user-postmeta.php",
    "content": "<?php\n/**\n * Tests for LifterLMS User Postmeta functions\n *\n * @package LifterLMS/Tests\n *\n * @group functions\n * @group user_postmeta\n *\n * @since 3.21.0\n * @since 3.33.0 Add test for the `llms_bulk_delete_user_postmeta` function.\n * @since 4.5.1 Fix failing `test_delete_user_postmeta()` which was comparing based on array order when that doesn't strictly matter.\n * @version 5.4.1\n */\nclass LLMS_Test_Functions_User_Postmeta extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since Unknown\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$this->student = $this->get_mock_student();\n\t\t$this->student_id = $this->student->get( 'id' );\n\t\t$this->course_id = $this->generate_mock_courses( 1, 1, 3 )[0];\n\t\t$this->student->enroll( $this->course_id );\n\n\t}\n\n\tpublic function test__llms_query_user_postmeta() {\n\n\t\t// fake user, fake post, fake key\n\t\t$this->assertEquals( array(), _llms_query_user_postmeta( 123, 456, '_fake_val' ) );\n\n\t\t// real user, fake post, fake key\n\t\t$this->assertEquals( array(), _llms_query_user_postmeta( $this->student_id, 123, '_fake_val' ) );\n\n\t\t// fake user, real post, fake key\n\t\t$this->assertEquals( array(), _llms_query_user_postmeta( 123, $this->course_id, '_fake_val' ) );\n\n\t\t// fake user, fake post, real key\n\t\t$this->assertEquals( array(), _llms_query_user_postmeta( 123, 456, '_status' ) );\n\n\t\t// has a result\n\t\t$this->assertEquals( 1, count( _llms_query_user_postmeta( $this->student_id, $this->course_id, '_status' ) ) );\n\n\t\t// find a specific value\n\t\t$this->assertEquals( 1, count( _llms_query_user_postmeta( $this->student_id, $this->course_id, '_status', 'enrolled' ) ) );\n\n\t\t// no key has more results\n\t\t$this->assertEquals( 3, count( _llms_query_user_postmeta( $this->student_id, $this->course_id ) ) );\n\n\t}\n\n\t/**\n\t * Test llms_delete_user_postmeta()\n\t *\n\t * @since Unknown\n\t * @since 4.5.1 Compare data as equal sets in favor of strict order comparison.\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_user_postmeta() {\n\n\t\t// with a value\n\t\tllms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' );\n\t\t$this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' ) );\n\t\t$this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) );\n\n\t\t// without a value\n\t\tllms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' );\n\t\t$this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) );\n\t\t$this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) );\n\n\t\t// delete all non-unique vals\n\t\t$i = 1;\n\t\twhile( $i <= 3 ) {\n\t\t\tllms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' . $i, false );\n\t\t\t$i++;\n\t\t}\n\t\t$this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) );\n\t\t$this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' ) );\n\n\t\t// delete only a specific non unique value\n\t\t$i = 1;\n\t\twhile( $i <= 3 ) {\n\t\t\tllms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme' . $i, false );\n\t\t\t$i++;\n\t\t}\n\t\t$this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', 'eraseme3' ) );\n\t\t$this->assertEqualSets( array( 'eraseme1', 'eraseme2' ), llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase', false ) );\n\n\t\t// delete all user post meta for student & post\n\t\t$i = 1;\n\t\twhile( $i <= 3 ) {\n\t\t\tllms_update_user_postmeta( $this->student_id, $this->course_id, '_test_data_to_erase' . $i, 'eraseme', false );\n\t\t\t$i++;\n\t\t}\n\t\t$this->assertTrue( llms_delete_user_postmeta( $this->student_id, $this->course_id ) );\n\t\t$this->assertEquals( array(), llms_get_user_postmeta( $this->student_id, $this->course_id ) );\n\n\t}\n\n\t/**\n\t * Test the bulk_delete_user_postmeta() method.\n\t *\n\t * @since 3.33.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_bulk_delete_user_postmeta() {\n\n\t\t// delete the (key,value) matching pairs\n\t\t$data = array(\n\t\t\t'_bulk_test_data_to_erase1'         => 'bulk_eraseme_1',\n\t\t\t'_bulk_test_data_to_erase2'         => 'bulk_eraseme_2',\n\t\t\t'_bulk_test_data_to_preserve'       => 'bulk_saveme_1',\n\t\t\t'_bulk_test_data_to_preserve_value' => 'bulk_savemy_value_1',\n\t\t);\n\t\tllms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data );\n\n\t\t$data_to_erase = array(\n\t\t\t'_bulk_test_data_to_erase1'         => 'bulk_eraseme_1',\n\t\t\t'_bulk_test_data_to_erase2'         => 'bulk_eraseme_2',\n\t\t\t'_bulk_test_data_to_preserve_value' => 'bulk_eraseme_3',\n\t\t);\n\n\t\t$this->assertEquals( array(\n\t\t\t'_bulk_test_data_to_erase1'         => true,\n\t\t\t'_bulk_test_data_to_erase2'         => true,\n\t\t\t'_bulk_test_data_to_preserve_value' => false,\n\t\t), llms_bulk_delete_user_postmeta( $this->student_id, $this->course_id, $data_to_erase ) );\n\t\t$this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_bulk_test_data_to_erase1' ) );\n\t\t$this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_bulk_test_data_to_erase2' ) );\n\t\t$this->assertEquals( $data['_bulk_test_data_to_preserve'], llms_get_user_postmeta( $this->student_id, $this->course_id, '_bulk_test_data_to_preserve' ) );\n\t\t$this->assertEquals( $data['_bulk_test_data_to_preserve_value'], llms_get_user_postmeta( $this->student_id, $this->course_id, '_bulk_test_data_to_preserve_value' ) );\n\n\t\t// delete all the metas for a student and course\n\t\t$data = array(\n\t\t\t'_bulk_test_data_to_erase1' => 'bulk_eraseme_1',\n\t\t\t'_bulk_test_data_to_erase2' => 'bulk_eraseme_2',\n\t\t\t'_bulk_test_data_to_erase3' => 'bulk_eraseme_3',\n\t\t\t'_bulk_test_data_to_erase4' => 'bulk_eraseme_4',\n\t\t);\n\t\tllms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data );\n\n\t\t$this->assertTrue( llms_bulk_delete_user_postmeta( $this->student_id, $this->course_id ) );\n\t\t$this->assertEquals( array(), llms_get_user_postmeta( $this->student_id, $this->course_id ) );\n\n\t\t// delete all the metas with the matching keys\n\t\t$data = array(\n\t\t\t'_bulk_test_data_to_erase1'   => 'bulk_eraseme_1',\n\t\t\t'_bulk_test_data_to_erase2'   => 'bulk_eraseme_2',\n\t\t\t'_bulk_test_data_to_erase3'   => 'bulk_eraseme_3',\n\t\t\t'_bulk_test_data_to_preserve' => 'bulk_saveme_1',\n\t\t);\n\t\tllms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data );\n\n\t\t$data_to_erase = array(\n\t\t\t'_bulk_test_data_to_erase1' => null,\n\t\t\t'_bulk_test_data_to_erase2' => null,\n\t\t\t'_bulk_test_data_to_erase3' => null,\n\t\t);\n\t\t$this->assertTrue( llms_bulk_delete_user_postmeta( $this->student_id, $this->course_id, $data_to_erase ) );\n\t\t$this->assertArrayHasKey( '_bulk_test_data_to_preserve', llms_get_user_postmeta( $this->student_id, $this->course_id ) );\n\n\t}\n\n\n\t/**\n\t * Test llms_get_user_postmeta().\n\t *\n\t * @since 3.21.0\n\t * @since 5.3.2 Add delta when comparing enrollment date with updated date.\n\t * @since 5.4.1 Compare dates using UNIX timestamps.\n\t */\n\tpublic function test_llms_get_user_postmeta() {\n\n\t\t$this->assertEquals( 'enrolled', llms_get_user_postmeta( $this->student_id, $this->course_id, '_status' ) );\n\t\t$this->assertEquals( '', llms_get_user_postmeta( $this->student_id, $this->course_id, '_fake' ) );\n\t\t$this->assertEquals( 3, count( llms_get_user_postmeta( $this->student_id, $this->course_id ) ) );\n\n\t\t// Test serialized values.\n\t\t$data = range( 1, 5 );\n\t\tllms_update_user_postmeta( $this->student_id, $this->course_id, '_test_serialized_data', $data );\n\t\t$this->assertEquals( $data, llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_serialized_data' ) );\n\n\t\t// Test updated date.\n\t\t$enrollment_date = $this->student->get_enrollment_date( $this->course_id, 'enrolled', 'U' );\n\t\t$updated_date    = llms_get_user_postmeta( $this->student_id, $this->course_id, '_status', true, 'updated_date' );\n\t\t$this->assertEqualsWithDelta( $enrollment_date, strtotime( $updated_date ), 2 );\n\n\t}\n\n\tpublic function test_llms_update_user_postmeta() {\n\n\t\t// simple set and get\n\t\t$this->assertTrue( llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_key', 'testval' ) );\n\t\t$this->assertEquals( 'testval', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key' ) );\n\n\t\t// update that same val\n\t\t$this->assertTrue( llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_key', 'testval2' ) );\n\t\t$this->assertEquals( 'testval2', llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key' ) );\n\n\t\t// should only have one for the key\n\t\t$this->assertEquals( 1, count( llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key', false ) ) );\n\n\t\t// add another but non unique\n\t\t$this->assertTrue( llms_update_user_postmeta( $this->student_id, $this->course_id, '_test_key', 'testval2-1', false ) );\n\n\t\t// should be 2 now\n\t\t$this->assertEquals( 2, count( llms_get_user_postmeta( $this->student_id, $this->course_id, '_test_key', false ) ) );\n\n\t}\n\n\tpublic function test_llms_bulk_update_user_postmeta() {\n\n\t\t$data = array(\n\t\t\t'bulk_key1' => 'bulk_val1',\n\t\t\t'bulk_key2' => 'bulk_val2',\n\t\t);\n\n\t\t$this->assertTrue( llms_bulk_update_user_postmeta( $this->student_id, $this->course_id, $data ) );\n\t\tforeach ( $data as $key => $val ) {\n\t\t\t$this->assertEquals( $val, llms_get_user_postmeta( $this->student_id, $this->course_id, $key ) );\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/class-llms-test-template-functions.php",
    "content": "<?php\n/**\n * Test template functions\n *\n * @package LifterLMS/Tests/Functions\n *\n * @group functions\n * @group template_functions\n *\n * @since 4.8.0\n * @version 7.5.0\n */\nclass LLMS_Test_Template_Functions extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test `lifterlms_template_single_reviews()` outputs the Write a Review content.\n\t *\n\t * @since 7.5.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_single_reviews() {\n\t\tglobal $post;\n\n\t\t// create student\n\t\t$student = $this->factory->student->create();\n\t\twp_set_current_user( $student );\n\n\t\t$post = $this->factory->course->create();\n\n\t\t// enable reviews\n\t\tupdate_post_meta( $post, '_llms_reviews_enabled', 'yes' );\n\n\t\t// run the template function\n\t\tob_start();\n\t\tlifterlms_template_single_reviews();\n\t\t$output = ob_get_clean();\n\n\t\t$this->assertStringContainsString( __( 'Write a Review', 'lifterlms' ), $output );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-400.php",
    "content": "<?php\n/**\n * Test update to 4.0.0 functions\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_400\n *\n * @since 4.0.0\n */\nclass LLMS_Test_Functions_Updates_400 extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 4.0.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-400.php';\n\t}\n\n\t/**\n\t * Test llms_update_400_remove_session_options()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_remove_session_options() {\n\n\t\t$i = 0;\n\t\twhile ( $i < 20 ) {\n\t\t\t$string = uniqid();\n\t\t\tadd_option( '_wp_session_' . $string, array( 'data' ) );\n\t\t\tadd_option( '_wp_session_expires' . $string, time() + HOUR_IN_SECONDS );\n\t\t\t++$i;\n\t\t}\n\n\t\tglobal $wpdb;\n\t\t$sql = \"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_wp_session_%';\";\n\n\t\t$this->assertEquals( 40, $wpdb->get_var( $sql ) );\n\n\t\tllms_update_400_remove_session_options();\n\n\t\t$this->assertEquals( 0, $wpdb->get_var( $sql ) );\n\n\t}\n\n\t/**\n\t * Test llms_update_400_clear_session_cron()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_clear_session_cron() {\n\n\t\twp_schedule_event( time(), 'daily', 'wp_session_garbage_collection' );\n\n\t\tllms_update_400_clear_session_cron();\n\n\t\t$this->assertFalse( wp_next_scheduled( 'wp_session_garbage_collection' ) );\n\n\t}\n\n\t/**\n\t * Test llms_update_400_update_db_version()\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\tllms_update_400_update_db_version();\n\n\t\t$this->assertEquals( '4.0.0', get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-4150.php",
    "content": "<?php\n/**\n* Test updates functions when updating to 4.15.0\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_4150\n *\n * @since 4.15.0\n * @version 4.15.0\n */\nclass LLMS_Test_Functions_Updates_4150 extends LLMS_UnitTestCase {\n\n\tprivate $sessions;\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 4.15.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes and move teardown functions into here.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-4150.php';\n\n\t\t// Clean posts and postmeta tables.\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->postmeta}\" );\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->posts}\" );\n\t\t// Delete transients.\n\t\tdelete_transient( 'llms_update_4150_remove_orphan_access_plans' );\n\n\t}\n\n\t/**\n\t * Test llms_update_4150_remove_orphan_access_plans\n\t *\n\t * @since 4.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_4150_remove_orphan_access_plans() {\n\n\t\t// Create orphan access plans.\n\t\t$access_plan_ids = $this->factory->post->create_many(\n\t\t\t10,\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t)\n\t\t);\n\t\tforeach ( $access_plan_ids as $access_plan_id ) {\n\t\t\tupdate_post_meta( $access_plan_id, '_llms_product_id', end( $access_plan_ids ) + 1 );\n\t\t}\n\n\t\t$this->assertEquals(\n\t\t\t10,\n\t\t\tcount(\n\t\t\t\tget_posts(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'include'   => $access_plan_ids,\n\t\t\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Fire the update.\n\t\tllms_update_4150_remove_orphan_access_plans();\n\n\t\t// Expect no orphan access plans.\n\t\t$this->assertEquals(\n\t\t\t0,\n\t\t\tcount(\n\t\t\t\tget_posts(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'include'   => $access_plan_ids,\n\t\t\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test llms_update_4150_remove_orphan_access_plans\n\t *\n\t * @since 4.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_4150_remove_orphan_access_plans_keep_linked() {\n\n\t\t// Create linked access plans.\n\t\t$access_plan_ids = $this->factory->post->create_many(\n\t\t\t11,\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t)\n\t\t);\n\n\t\t$course = $this->factory->post->create();\n\t\tforeach ( $access_plan_ids as $access_plan_id ) {\n\t\t\tupdate_post_meta( $access_plan_id, '_llms_product_id', $course );\n\t\t}\n\n\t\t// Create orphan access plans.\n\t\t$orphan_access_plan_ids = $this->factory->post->create_many(\n\t\t\t10,\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $orphan_access_plan_ids as $access_plan_id ) {\n\t\t\tupdate_post_meta( $access_plan_id, '_llms_product_id', end( $orphan_access_plan_ids ) + 1 );\n\t\t}\n\n\t\tllms_update_4150_remove_orphan_access_plans();\n\n\t\t// Expect no orphan access plans.\n\t\t$this->assertEquals(\n\t\t\t0,\n\t\t\tcount(\n\t\t\t\tget_posts(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'include'   => $orphan_access_plan_ids,\n\t\t\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Expect linked access plans are still there.\n\t\t$this->assertEquals(\n\t\t\tcount( $access_plan_ids ),\n\t\t\tcount(\n\t\t\t\tget_posts(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'include'   => $access_plan_ids,\n\t\t\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\t/**\n\t * Test \"pagination\" in llms_update_4150_remove_orphan_access_plans()\n\t *\n\t * @since 4.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_4150_remove_orphan_access_plans_pagination() {\n\n\t\t// Create orphan access plans.\n\t\t$orphan_access_plan_ids = $this->factory->post->create_many(\n\t\t\t110, // Each page is of 50 orphan access plans.\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_access_plan',\n\t\t\t)\n\t\t);\n\n\t\tforeach ( $orphan_access_plan_ids as $access_plan_id ) {\n\t\t\tupdate_post_meta( $access_plan_id, '_llms_product_id', end( $orphan_access_plan_ids ) + 1 );\n\t\t}\n\n\t\t$loops = 0;\n\t\t// Check how many times the update function needs to run.\n\t\t// Internally we fetch 50 orphan access plans at time, we expect it to run the following number of times:\n\t\t$expected_loops = 3;\n\t\twhile ( llms_update_4150_remove_orphan_access_plans() ) {\n\t\t\t$loops++;\n\t\t}\n\n\t\t$this->assertEquals( $expected_loops, $loops );\n\t\t$this->assertEquals( get_transient( 'llms_update_4150_remove_orphan_access_plans' ), 'complete' );\n\n\t\t// Expect no orphan access plans.\n\t\t$this->assertEquals(\n\t\t\t0,\n\t\t\tcount(\n\t\t\t\tget_posts(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'include'     => $orphan_access_plan_ids,\n\t\t\t\t\t\t'post_type'   => 'llms_access_plan',\n\t\t\t\t\t\t'numberposts' => 200\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t}\n\n\n\t/**\n\t * Test llms_update_4150_update_db_version()\n\t *\n\t * @since 4.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\t// Remove existing db version.\n\t\tdelete_option( 'lifterlms_db_version' );\n\n\t\tllms_update_4150_update_db_version();\n\n\t\t$this->assertNotEquals( '4.15.0', get_option( 'lifterlms_db_version' ) );\n\n\t\t// Unlock the db version update.\n\t\tset_transient( 'llms_update_4150_remove_orphan_access_plans', 'complete', DAY_IN_SECONDS );\n\n\t\tllms_update_4150_update_db_version();\n\n\t\t$this->assertEquals( '4.15.0', get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-450.php",
    "content": "<?php\n/**\n* Test updates functions when updating to 4.5.0\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_450\n *\n * @since 4.5.0\n * @version 4.15.0\n */\nclass LLMS_Test_Functions_Updates_450 extends LLMS_UnitTestCase {\n\n\tprivate $sessions;\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 4.5.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-450.php';\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.5.0\n\t * @since 5.3.3 Renamed setUp() to set_up() and moved teardown functions into here.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->sessions = LLMS_Sessions::instance();\n\n\t\t// Clean open sessions table.\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_events_open_sessions\" );\n\t\t// Delete transients.\n\t\tdelete_transient( 'llms_update_450_migrate_events_open_sessions' );\n\t\tdelete_transient( 'llms_450_skipper_events_open_sessions' );\n\t}\n\n\t/**\n\t * Test llms_update_450_migrate_events_open_sessions()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_migrate_events_open_sessions() {\n\t\t// Create open session events.\n\t\t$open_session_ids = $this->create_open_session_events( 10, 3 );\n\n\t\t// Fire the update.\n\t\tllms_update_450_migrate_events_open_sessions();\n\n\t\t// Get the migrated open sessions.\n\t\t$open_sessions = LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_open_sessions' );\n\n\t\t// Expect 7 open sessions.\n\t\t$this->assertEquals( 7, count( $open_sessions ) );\n\t\t$this->assertEquals( count( $open_session_ids ), count( $open_sessions ) );\n\n\t\t// Expect their ids match the not closed ones.\n\t\tforeach ( $open_sessions as $os ) {\n\t\t\t$this->assertContains( $os->get('id'), $open_session_ids );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test \"pagination\" in llms_update_450_migrate_events_open_sessions()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_migrate_events_open_sessions_pagination() {\n\n\t\t// Create open session events.\n\t\t$num_open_sessions = 250;\n\t\t$open_session_ids  = $this->create_open_session_events( $num_open_sessions );\n\n\t\t$loops = 1;\n\t\t// Check how many times the update function needs to run.\n\t\t// Internally we fetch 200 sessions at time, we expect it to run the following number of times:\n\t\t$expected_loops = 3;\n\t\twhile ( llms_update_450_migrate_events_open_sessions() ) {\n\t\t\t$loops++;\n\t\t}\n\t\t$this->assertEquals( $expected_loops, $loops );\n\t\t$this->assertEquals( get_transient( 'llms_450_skipper_events_open_sessions' ), $expected_loops * 200 );\n\t\t$this->assertEquals( get_transient( 'llms_update_450_migrate_events_open_sessions' ), 'complete' );\n\n\t\t// Get the migrated open sessions.\n\t\tglobal $wpdb;\n\t\t$open_sessions = $wpdb->get_col( // db call ok; no-cache ok.\n\t\t\t$wpdb->prepare(\n\t\t\t\t\"\n\t\t\t   SELECT event_id\n\t\t\t   FROM {$wpdb->prefix}lifterlms_events_open_sessions\n\t\t\t   ORDER BY event_id ASC\n\t\t\t   LIMIT %d, %d\n\t\t\",\n\t\t\t\t0,\n\t\t\t\t300\n\t\t\t)\n\t\t);\n\n\t\t// Expect all of them have been correctly migrated.\n\t\t$this->assertEquals( $num_open_sessions, count( $open_sessions ) );\n\n\t}\n\n\t/**\n\t * Test llms_update_450_update_db_version()\n\t *\n\t * @since 4.5.0\n\t * @since 4.15.0 Get original db_version before removing it.\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\t// Remove existing db version.\n\t\tdelete_option( 'lifterlms_db_version' );\n\n\t\tllms_update_450_update_db_version();\n\n\t\t$this->assertNotEquals( '4.5.0', get_option( 'lifterlms_db_version' ) );\n\n\t\t// Unlock the db version update.\n\t\tset_transient( 'llms_update_450_migrate_events_open_sessions', 'complete', DAY_IN_SECONDS );\n\n\t\tllms_update_450_update_db_version();\n\n\t\t$this->assertEquals( '4.5.0', get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n\t/**\n\t * Util to create open sessions in the lifterlms_events table\n\t *\n\t * @since 4.5.0\n\t *\n\t * @param int $num_open_sessions   Number of sessions to open.\n\t * @param int $num_closed_sessions Optional. Number of sessions to close. Default 0.\n\t * @return int[] An array with the events ids of the still open sessions.\n\t */\n\tprivate function create_open_session_events( $num_open_sessions, $num_closed_sessions = 0 ) {\n\t\t$time = time();\n\n\t\t$i = 1;\n\t\t$open_session_ids = array();\n\n\t\twhile ( $i <= $num_open_sessions ) {\n\t\t\t$user = $this->factory->user->create();\n\t\t\twp_set_current_user( $user );\n\n\t\t\t$time += MINUTE_IN_SECONDS;\n\t\t\tllms_tests_mock_current_time( $time );\n\n\t\t\t$object_id = LLMS_Unit_Test_Util::call_method( $this->sessions, 'get_new_id', array( $user ) );\n\t\t\t// Record session start.\n\t\t\t$session_start = llms()->events()->record(\n\t\t\t\tarray(\n\t\t\t\t\t'actor_id'     => $user,\n\t\t\t\t\t'object_type'  => 'session',\n\t\t\t\t\t'object_id'    => $object_id,\n\t\t\t\t\t'event_type'   => 'session',\n\t\t\t\t\t'event_action' => 'start',\n\t\t\t\t)\n\t\t\t);\n\t\t\t$open_session_ids[] = $session_start->get( 'id' );\n\n\t\t\t// Close N sessions.\n\t\t\tif ( $num_closed_sessions\n\t\t\t\t\t&& $num_closed_sessions <= $num_open_sessions\n\t\t\t\t\t&& $i > ( $num_open_sessions - $num_closed_sessions ) ) {\n\n\t\t\t\t$time += MINUTE_IN_SECONDS;\n\t\t\t\tllms_tests_mock_current_time( $time );\n\t\t\t\t// Record session end.\n\t\t\t\tllms()->events()->record(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'actor_id'     => $user,\n\t\t\t\t\t\t'object_type'  => 'session',\n\t\t\t\t\t\t'object_id'    => $object_id,\n\t\t\t\t\t\t'event_type'   => 'session',\n\t\t\t\t\t\t'event_action' => 'end',\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\tarray_pop( $open_session_ids );\n\t\t\t}\n\n\t\t\t$i++;\n\t\t}\n\n\t\treturn $open_session_ids;\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-500.php",
    "content": "<?php\n/**\n* Test updates functions when updating to 5.0.0\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_500\n *\n * @since 5.0.0\n * @since 5.2.0 Removed tearDown override, we don't need to remove any transient related to this update as we don't create it.\n */\nclass LLMS_Test_Functions_Updates_500 extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-500.php';\n\t}\n\n\t/**\n\t * Test llms_update_500_update_db_version()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\t// Remove existing db version.\n\t\tdelete_option( 'lifterlms_db_version' );\n\n\t\tllms_update_500_update_db_version();\n\n\t\t$this->assertEquals( '5.0.0', get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n\t/**\n\t * Test llms_update_500_add_admin_notice()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_500_add_admin_notice() {\n\n\t\t$notice = 'v500-welcome-msg';\n\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( $notice ) );\n\n\t\tllms_update_500_add_admin_notice();\n\n\t\t$this->assertTrue( true, LLMS_Admin_Notices::has_notice( $notice ) );\n\n\t\t// Cleanup.\n\t\tLLMS_Admin_Notices::delete_notice( $notice );\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-520.php",
    "content": "<?php\n/**\n* Test updates functions when updating to 5.2.0\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_520\n *\n * @since 5.2.0\n */\nclass LLMS_Test_Functions_Updates_520 extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 5.2.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-520.php';\n\t}\n\n\t/**\n\t * Test llms_update_520_upcoming_reminder_notification_backward_compat() method\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_update_520_upcoming_reminder_notification_backward_compat() {\n\n\t\t$subscribers_for_type = array(\n\t\t\t'email' => array(\n\t\t\t\t'student',\n\t\t\t),\n\t\t\t'basic' => array(\n\t\t\t\t'student',\n\t\t\t\t'author',\n\t\t\t\t'custom',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $subscribers_for_type as $type => $subscribers ) {\n\t\t\t$this->assertEquals(\n\t\t\t\tarray(),\n\t\t\t\tget_option( \"llms_notification_upcoming_payment_reminder_{$type}_subscribers\", array() )\n\t\t\t);\n\t\t}\n\n\t\t// Run the update.\n\t\tllms_update_520_upcoming_reminder_notification_backward_compat();\n\n\t\tforeach ( $subscribers_for_type as $type => $subscribers ) {\n\t\t\t$this->assertEquals(\n\t\t\t\tarray_fill_keys( $subscribers, 'no' ),\n\t\t\t\tget_option( \"llms_notification_upcoming_payment_reminder_{$type}_subscribers\", array() )\n\t\t\t);\n\t\t}\n\n\t\t// Create the option and check it's not overridden.\n\t\tforeach ( $subscribers_for_type as $type => $subscribers ) {\n\t\t\tupdate_option( \"llms_notification_upcoming_payment_reminder_{$type}_subscribers\", array_fill_keys( $subscribers, 'yes' ) );\n\n\t\t\t$this->assertNotEquals(\n\t\t\t\tarray_fill_keys( $subscribers, 'no' ),\n\t\t\t\tget_option( \"llms_notification_upcoming_payment_reminder_{$type}_subscribers\", array() )\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Test llms_update_520_update_db_version()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_520_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\t// Remove existing db version.\n\t\tdelete_option( 'lifterlms_db_version' );\n\n\t\tllms_update_520_update_db_version();\n\n\t\t$this->assertEquals( '5.2.0', get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-600.php",
    "content": "<?php\n/**\n* Test updates functions when updating to 6.0.0\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_600\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Functions_Updates_600 extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/llms.functions.updates.php';\n\t}\n\n\t/**\n\t * Setup the test\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\tadd_filter( 'llms_update_items_per_page', array( $this, 'per_page' ) );\n\t\tdelete_option( 'llms_has_achievements_with_legacy_default_image' );\n\t\tdelete_option( 'llms_has_certificates_with_legacy_default_image' );\n\n\t}\n\n\t/**\n\t * Tear down the test\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tremove_filter( 'llms_update_items_per_page', array( $this, 'per_page' ) );\n\t\tdelete_option( 'llms_has_achievements_with_legacy_default_image' );\n\t\tdelete_option( 'llms_has_certificates_with_legacy_default_image' );\n\n\t}\n\n\t/**\n\t * Callback function to reduce items per page for testing.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int\n\t */\n\tpublic function per_page() {\n\t\treturn 5;\n\t}\n\n\t/**\n\t * Calls a namespaced function with the specified arguments.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param [type] $func [description]\n\t * @param array $args [description]\n\t * @return [type] [description]\n\t */\n\tprivate function call_ns_func( $func, $args = array() ) {\n\t\treturn call_user_func( \"LLMS\\Updates\\Version_6_0_0\\\\{$func}\", ...$args );\n\t}\n\n\t/**\n\t * Creates one or more awards of a given type using the pre-migration data structure.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int    $count             Number of award posts to create.\n\t * @param string $type              Type of award, either \"achievement\" or \"certificate\".\n\t * @param bool   $use_default_image If `true`, then the award will not use a custom image.\n\t * @return array[] {\n\t *     Array of data arrays describing the generated award.\n\t *\n\t *     @type int    $template_id WP_Post id of the template post.\n\t *     @type int    $user_id     WP_User id of the user who earned the award.\n\t *     @type int    $image_id    WP_Post id of the attachment post for the award's image.\n\t *     @type int    $post_id     WP_Post id of the award post.\n\t *     @type string $title       Title of the award.\n\t * }\n\t */\n\tprivate function create_legacy_awards( $count, $type, $use_default_image ) {\n\n\t\tremove_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\t\tremove_action( \"save_post_llms_my_{$type}\", array( 'LLMS_Controller_Awards', 'on_save' ), 20 );\n\n\t\t$res = array();\n\t\t$i = 0;\n\t\twhile ( $i < $count ) {\n\t\t\t$post_type   = \"llms_my_{$type}\";\n\t\t\t$image_id    = $use_default_image ? 0 : $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\t\t$template_id = $this->factory->post->create( array( 'post_type' => \"llms_{$type}\" ) );\n\t\t\t$user_id     = $this->factory->user->create();\n\t\t\t$title       = sprintf( '%1$s Title %2$s', ucwords( $type ), wp_generate_password( 4, false ) );\n\t\t\t$meta_input  = array(\n\t\t\t\t\"_llms_{$type}_template\" => $template_id,\n\t\t\t\t\"_llms_{$type}_image\"    => $image_id,\n\t\t\t\t\"_llms_{$type}_title\"    => $title,\n\t\t\t);\n\t\t\tif ( 'achievement' === $type ) {\n\t\t\t\t$meta_input['_llms_achievement_content'] = 'Some content.';\n\t\t\t}\n\t\t\t$post_id     = $this->factory->post->create( array(\n\t\t\t\t'post_type'  => $post_type,\n\t\t\t\t'meta_input' => $meta_input,\n\t\t\t) );\n\n\t\t\tllms_update_user_postmeta(\n\t\t\t\t$user_id,\n\t\t\t\t$this->factory->post->create(),\n\t\t\t\t\"_{$type}_earned\",\n\t\t\t\t$post_id\n\t\t\t);\n\n\t\t\t$res[] = compact( 'template_id', 'user_id', 'image_id', 'post_id', 'title' );\n\n\t\t\t$i++;\n\t\t}\n\n\t\tadd_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\t\tadd_action( \"save_post_llms_my_{$type}\", array( 'LLMS_Controller_Awards', 'on_save' ), 20 );\n\n\t\treturn $res;\n\n\t}\n\n\t/**\n\t * Creates one or more award templates of a given type using the pre-migration data structure.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @param int  $count             Number of award posts to create.\n\t * @param bool $use_default_image If `true`, then the award will not use a custom image.\n\t * @return array[] {\n\t *     Array of data arrays describing the generated template.\n\t *\n\t *     @type int    $image_id    WP_Post id of the attachment post for the award's image.\n\t *     @type int    $post_id     WP_Post id of the award post.\n\t * }\n\t */\n\tprivate function create_legacy_templates( $count, $use_default_image ) {\n\n\t\t$res = array();\n\t\t$i = 0;\n\t\twhile ( $i < $count ) {\n\t\t\t$post_type   = array( 'llms_achievement', 'llms_certificate' );\n\t\t\tshuffle( $post_type );\n\t\t\t$post_type   = $post_type[0];\n\t\t\t$type        = str_replace( 'llms_', '', $post_type );\n\t\t\t$image_id    = $use_default_image ? 0 : $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\t\t$meta_input  = array(\n\t\t\t\t\"_llms_{$type}_image\"    => $image_id,\n\t\t\t);\n\t\t\tif ( 'llms_achievement' === $post_type ) {\n\t\t\t\t$meta_input['_llms_achievement_content'] = 'Some content.';\n\t\t\t}\n\t\t\t$post_id     = $this->factory->post->create( array(\n\t\t\t\t'post_type'  => $post_type,\n\t\t\t\t'meta_input' => $meta_input,\n\t\t\t) );\n\n\t\t\t$res[] = compact( 'image_id', 'post_id', 'type' );\n\n\t\t\t$i++;\n\t\t}\n\n\t\treturn $res;\n\n\t}\n\n\t/**\n\t * Test update_db_version()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\t// Remove existing db version.\n\t\tdelete_option( 'lifterlms_db_version' );\n\n\t\t$this->call_ns_func( 'update_db_version' );\n\n\t\t$this->assertEquals( '6.0.0', get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n\t/**\n\t * Test the migrate_achievements() and migrate_certificates() functions.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_award_metas() {\n\n\t\t$per_page = function() {\n\t\t\treturn 5;\n\t\t};\n\t\tadd_filter( 'llms_update_items_per_page', $per_page );\n\n\t\t$tests = array(\n\t\t\tarray( 'achievement', false ),\n\t\t\tarray( 'achievement', true ),\n\t\t\tarray( 'certificate', false ),\n\t\t\tarray( 'certificate', true ),\n\t\t);\n\n\t\tforeach ( $tests as $test ) {\n\n\t\t\tlist( $type, $use_default_image ) = $test;\n\n\t\t\tdelete_option( 'llms_has_achievements_with_legacy_default_image' );\n\t\t\tdelete_option( 'llms_has_certificates_with_legacy_default_image' );\n\n\t\t\t$awards = $this->create_legacy_awards( 12, $type, $use_default_image );\n\n\t\t\t// Should run 3 times, the 3rd has fewer than max results so we're complete.\n\t\t\t$i = 1;\n\t\t\twhile ( $i <= 3 ) {\n\t\t\t\t$this->assertEquals( $i !== 3, $this->call_ns_func( \"migrate_{$type}s\", array( $type ) ) );\n\t\t\t\t$i++;\n\t\t\t}\n\n\t\t\tforeach ( $awards as $i => $award ) {\n\n\t\t\t\t$post = get_post( $award['post_id'] );\n\n\t\t\t\t// Everything is migrated.\n\t\t\t\t$this->assertEquals( $award['user_id'], $post->post_author );\n\t\t\t\t$this->assertEquals( $award['template_id'], $post->post_parent );\n\t\t\t\t$this->assertEquals( $award['title'], $post->post_title );\n\t\t\t\t$this->assertEquals( $award['image_id'], get_post_thumbnail_id( $post ) );\n\t\t\t\tif ( 'achievement' === $type ) {\n\t\t\t\t\t$this->assertEquals( 'Some content.', $post->post_content );\n\t\t\t\t}\n\n\t\t\t\t// Metadata is deleted.\n\t\t\t\tremove_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\t\t\t\t$this->assertFalse( metadata_exists( 'post', $post->ID, \"_llms_achievement_content\" ) );\n\t\t\t\t$this->assertFalse( metadata_exists( 'post', $post->ID, \"_llms_{$type}_title\" ) );\n\t\t\t\t$this->assertFalse( metadata_exists( 'post', $post->ID, \"_llms_{$type}_template\" ) );\n\t\t\t\t$this->assertFalse( metadata_exists( 'post', $post->ID, \"_llms_{$type}_image\" ) );\n\t\t\t\tadd_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\n\t\t\t}\n\n\t\t\t// Test both legacy options because sometimes paranoia helps sanity.\n\t\t\t$this->assertEquals(\n\t\t\t\t'achievement' === $type && $use_default_image ? 'yes' : 'no',\n\t\t\t\tget_option( \"llms_has_achievements_with_legacy_default_image\", 'no' ),\n\t\t\t\t\"\\$type = $type, \\$use_default_image = \" . ( $use_default_image ? 'true' : 'false' )\n\t\t\t);\n\t\t\t$this->assertEquals(\n\t\t\t\t'certificate' === $type && $use_default_image ? 'yes' : 'no',\n\t\t\t\tget_option( 'llms_has_certificates_with_legacy_default_image', 'no' ),\n\t\t\t\t\"\\$type = $type, \\$use_default_image = \" . ( $use_default_image ? 'true' : 'false' )\n\t\t\t);\n\n\t\t}\n\n\t\tremove_filter( 'llms_update_600_per_page', $per_page );\n\n\t}\n\n\t/**\n\t * Test the migrate_achievements() and migrate_certificates() functions when none exist to migrate.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_award_metas_none_found() {\n\n\t\t$tests = array(\n\t\t\t'achievement',\n\t\t\t'certificate',\n\t\t);\n\n\t\tforeach ( $tests as $type ) {\n\n\t\t\t$this->assertFalse( $this->call_ns_func( \"migrate_{$type}s\", array( $type ) ) );\n\t\t\t$this->assertEquals( 'no', get_option( 'llms_has_achievements_with_legacy_default_image', 'no' ) );\n\t\t\t$this->assertEquals( 'no', get_option( 'llms_has_certificates_with_legacy_default_image', 'no' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test migrate_award_templates().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_migrate_award_templates() {\n\n\t\tforeach ( array( false, true ) as $use_default_image ) {\n\n\t\t\tdelete_option( 'llms_has_achievements_with_legacy_default_image' );\n\t\t\tdelete_option( 'llms_has_certificates_with_legacy_default_image' );\n\n\t\t\t$templates = $this->create_legacy_templates( 20, $use_default_image );\n\n\t\t\t// Should run 5 times, the 5th has no results and the migration is complete.\n\t\t\t$i = 1;\n\t\t\twhile ( $i <= 5 ) {\n\t\t\t\t$this->assertEquals( $i !== 5, $this->call_ns_func( 'migrate_award_templates' ) );\n\t\t\t\t$i++;\n\t\t\t}\n\n\t\t\tforeach ( $templates as $template ) {\n\t\t\t\t$this->assertEquals( $template['image_id'], get_post_thumbnail_id( $template['post_id'] ) );\n\t\t\t\t$this->assertFalse( metadata_exists( 'post', $template['post_id'], \"_llms_{$template['type']}_image\" ) );\n\n\t\t\t\tif ( 'achievement' === $template['type'] ) {\n\t\t\t\t\t$post = get_post( $template['post_id'] );\n\t\t\t\t\t$this->assertEquals( 'Some content.', $post->post_content );\n\t\t\t\t\tremove_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\t\t\t\t\t$this->assertFalse( metadata_exists( 'post', $template['post_id'], '_llms_achievement_content' ) );\n\t\t\t\t\tadd_filter( 'get_post_metadata', 'llms_engagement_handle_deprecated_meta_keys', 20, 3 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$this->assertEquals(\n\t\t\t\t$use_default_image ? 'yes' : 'no',\n\t\t\t\tget_option( 'llms_has_achievements_with_legacy_default_image', 'no' )\n\t\t\t);\n\t\t\t$this->assertEquals(\n\t\t\t\t$use_default_image ? 'yes' : 'no',\n\t\t\t\tget_option( 'llms_has_certificates_with_legacy_default_image', 'no' )\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Test show_notice()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_show_notice() {\n\n\t\t$notice = 'v600alpha1-welcome-msg';\n\n\t\t// require_once LLMS_PLUGIN_DIR . 'includes/admin/class.llms.admin.notices.php';\n\n\t\t$this->assertFalse( LLMS_Admin_Notices::has_notice( $notice ) );\n\n\t\t$this->call_ns_func( 'show_notice' );\n\n\t\t$this->assertTrue( true, LLMS_Admin_Notices::has_notice( $notice ) );\n\n\t\t// Cleanup.\n\t\tLLMS_Admin_Notices::delete_notice( $notice );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-6100.php",
    "content": "<?php\n/**\n* Test updates functions when updating to 6.10.0.\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_6100\n *\n * @since 6.10.0\n */\nclass LLMS_Test_Functions_Updates_6100 extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class.\n\t *\n\t * Include update functions file.\n\t *\n \t * @since 6.10.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-6100.php';\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/llms.functions.updates.php';\n\t}\n\n\t/**\n\t * Test migrate_spanish_users().\n\t *\n\t * @since 6.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_migrate_spanish_users() {\n\n\t\t// Create one spanish user with a spanish province that should be migrated to a new one.\n\t\t$student_spanish_prov_migrate = $this->factory->user->create();\n\t\tupdate_user_meta( $student_spanish_prov_migrate, 'llms_billing_country', 'ES' );\n\t\tupdate_user_meta( $student_spanish_prov_migrate, 'llms_billing_state', 'AS' ); // 'AS' turned into 'O'.\n\n\t\t// Create a spanish user with a spanish province which doesn't exist anymore.\n\t\t$student_spanish_prov_migrate_2 = $this->factory->user->create();\n\t\tupdate_user_meta( $student_spanish_prov_migrate_2, 'llms_billing_country', 'ES' );\n\t\tupdate_user_meta( $student_spanish_prov_migrate_2, 'llms_billing_state', 'EX' );\n\n\t\t// Create another two spanish users with a spanish province that should not be migrated to a new one.\n\t\t$student_spanish_prov_migrate_not = $this->factory->user->create();\n\t\tupdate_user_meta( $student_spanish_prov_migrate_not, 'llms_billing_country', 'ES' );\n\t\tupdate_user_meta( $student_spanish_prov_migrate_not, 'llms_billing_state', 'CE' ); // Didn't change.\n\n\t\t$student_spanish_prov_migrate_not_2 = $this->factory->user->create();\n\t\tupdate_user_meta( $student_spanish_prov_migrate_not_2, 'llms_billing_country', 'ES' );\n\t\tupdate_user_meta( $student_spanish_prov_migrate_not_2, 'llms_billing_state', 'B' ); // Wasn't there before.\n\n\t\t// Create an US user with a state that looks like a spanish province that should be migrated.\n\t\t$student_spanish_prov_migrate_not_3 = $this->factory->user->create();\n\t\tupdate_user_meta( $student_spanish_prov_migrate_not_3, 'llms_billing_country', 'US' );\n\t\tupdate_user_meta( $student_spanish_prov_migrate_not_3, 'llms_billing_state', 'AS' ); // 'AS' turned into 'O'.\n\n\n\t\t\\LLMS\\Updates\\Version_6_10_0\\migrate_spanish_users();\n\n\t\t// Migrated 'AS' to 'O'.\n\t\t$this->assertEquals(\n\t\t\t'O',\n\t\t\tget_user_meta( $student_spanish_prov_migrate, 'llms_billing_state', true )\n\t\t);\n\t\t// Migrated 'S' to ''.\n\t\t$this->assertEquals(\n\t\t\t'',\n\t\t\tget_user_meta( $student_spanish_prov_migrate_2, 'llms_billing_state', true )\n\t\t);\n\t\t// Unmigrated 'CE'.\n\t\t$this->assertEquals(\n\t\t\t'CE',\n\t\t\tget_user_meta( $student_spanish_prov_migrate_not, 'llms_billing_state', true )\n\t\t);\n\t\t// Unmigrated 'AN'.\n\t\t$this->assertEquals(\n\t\t\t'B',\n\t\t\tget_user_meta( $student_spanish_prov_migrate_not_2, 'llms_billing_state', true )\n\t\t);\n\t\t// Unmigrated american user with 'AS' as state/province.\n\t\t$this->assertEquals(\n\t\t\t'AS',\n\t\t\tget_user_meta( $student_spanish_prov_migrate_not_3, 'llms_billing_state', true )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test update_db_version().\n\t *\n\t * @since 6.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\t// Remove existing db version.\n\t\tdelete_option( 'lifterlms_db_version' );\n\n\t\t\\LLMS\\Updates\\Version_6_10_0\\update_db_version();\n\n\t\t$this->assertEquals( \\LLMS\\Updates\\Version_6_10_0\\_get_db_version(), get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions/updates/class-llms-test-functions-updates-630.php",
    "content": "<?php\n/**\n* Test updates functions when updating to 6.3.0.\n *\n * @package LifterLMS/Tests/Functions/Updates\n *\n * @group functions\n * @group updates\n * @group updates_630\n *\n * @since 6.3.0\n */\nclass LLMS_Test_Functions_Updates_630 extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class.\n\t *\n\t * Include update functions file.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/functions/updates/llms-functions-updates-630.php';\n\t}\n\n\t/**\n\t * Test buddypress_profile_endpoints_bc() method.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_buddypress_profile_endpoints_bc() {\n\n\t\t$this->assertEquals(\n\t\t\tarray(),\n\t\t\tget_option( 'llms_integration_buddypress_profile_endpoints', array() )\n\t\t);\n\n\t\t// Run the update.\n\t\t\\LLMS\\Updates\\Version_6_3_0\\buddypress_profile_endpoints_bc();\n\n\t\t$this->assertEquals(\n\t\t\tarray(),\n\t\t\tget_option( 'llms_integration_buddypress_profile_endpoints', array() )\n\t\t);\n\n\t\t// Enable the integration.\n\t\tupdate_option(  'llms_integration_buddypress_enabled', 'yes' );\n\n\t\t// Run the update.\n\t\t\\LLMS\\Updates\\Version_6_3_0\\buddypress_profile_endpoints_bc();\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'view-courses',\n\t\t\t\t'view-memberships',\n\t\t\t\t'view-achievements',\n\t\t\t\t'view-certificates',\n\t\t\t),\n\t\t\tget_option( 'llms_integration_buddypress_profile_endpoints', array() )\n\t\t);\n\n\t\t// Turn it off.\n\t\tupdate_option(  'llms_integration_buddypress_enabled', 'no' );\n\n\t}\n\n\t/**\n\t * Test update_db_version().\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_update_db_version() {\n\n\t\t$orig = get_option( 'lifterlms_db_version' );\n\n\t\t// Remove existing db version.\n\t\tdelete_option( 'lifterlms_db_version' );\n\n\t\t\\LLMS\\Updates\\Version_6_3_0\\update_db_version();\n\n\t\t$this->assertEquals( '6.3.0', get_option( 'lifterlms_db_version' ) );\n\n\t\tupdate_option( 'lifterlms_db_version', $orig );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-courses.php",
    "content": "<?php\n/**\n * Course template function tests\n *\n * @group functions\n * @group template_functions_courses\n * @group template_functions\n *\n * @since 4.11.0\n */\nclass LLMS_Test_Functions_Templates_Courses extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test lifterlms_template_course_author()\n\t *\n\t * @since 4.11.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_lifterlms_template_course_author() {\n\n\t\t$user  = $this->factory->user->create_and_get( array(\n\t\t\t'first_name'  => 'Jimothy',\n\t\t\t'last_name'   => 'Halpert',\n\t\t\t'description' => 'Paper salesman at Dunder Mifflin Scranton.'\n\t\t) );\n\t\t$user2 = $this->factory->user->create_and_get( array(\n\t\t\t'first_name'  => 'Dwight',\n\t\t\t'last_name'   => 'Schrute',\n\t\t\t'description' => 'Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.'\n\t\t) );\n\n\t\tglobal $post;\n\t\t$post   = $this->factory->post->create_and_get( array(\n\t\t\t'post_type'   => 'course',\n\t\t\t'post_author' => $user->ID,\n\t\t) );\n\t\t$course = llms_get_post( $post );\n\n\t\t// One user (default post author).\n\t\t$course->instructors()->set_instructors( array() );\n\n\t\t$template = $this->get_output( 'lifterlms_template_course_author' );\n\n\t\t$this->assertStringContains( 'Course Instructor', $template );\n\t\t$this->assertStringContains( '<div class=\"llms-col-1\">', $template );\n\t\t$this->assertStringContains( '<span class=\"llms-author-info name\">Jimothy Halpert</span>', $template );\n\t\t$this->assertStringContains( '<span class=\"llms-author-info label\">Author</span>', $template );\n\t\t$this->assertStringContains( '<p class=\"llms-author-info bio\">Paper salesman at Dunder Mifflin Scranton.</p>', $template );\n\n\t\t// Two Instructors.\n\t\t$course->instructors()->set_instructors( array( array( 'id' => $user->ID ), array( 'id' => $user2->ID ) ) );\n\n\t\t$template = $this->get_output( 'lifterlms_template_course_author' );\n\n\t\t$this->assertStringContains( 'Course Instructors', $template );\n\t\t$this->assertStringContains( '<div class=\"llms-col-2\">', $template );\n\n\t\t$this->assertStringContains( '<span class=\"llms-author-info name\">Jimothy Halpert</span>', $template );\n\t\t$this->assertStringContains( '<p class=\"llms-author-info bio\">Paper salesman at Dunder Mifflin Scranton.</p>', $template );\n\n\t\t$this->assertStringContains( '<span class=\"llms-author-info name\">Dwight Schrute</span>', $template );\n\t\t$this->assertStringContains( '<p class=\"llms-author-info bio\">Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.</p>', $template );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-memberships.php",
    "content": "<?php\n/**\n * Membership template function tests\n *\n * @group functions\n * @group template_functions_memberships\n * @group template_functions\n *\n * @since 4.11.0\n */\nclass LLMS_Test_Functions_Templates_Memberships extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test llms_template_membership_instructors()\n\t *\n\t * @since 4.11.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_llms_template_membership_auinstructors() {\n\n\t\t$user  = $this->factory->user->create_and_get( array(\n\t\t\t'first_name'  => 'Jimothy',\n\t\t\t'last_name'   => 'Halpert',\n\t\t\t'description' => 'Paper salesman at Dunder Mifflin Scranton.'\n\t\t) );\n\t\t$user2 = $this->factory->user->create_and_get( array(\n\t\t\t'first_name'  => 'Dwight',\n\t\t\t'last_name'   => 'Schrute',\n\t\t\t'description' => 'Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.'\n\t\t) );\n\n\t\tglobal $post;\n\t\t$post   = $this->factory->post->create_and_get( array(\n\t\t\t'post_type'   => 'llms_membership',\n\t\t\t'post_author' => $user->ID,\n\t\t) );\n\t\t$membership = llms_get_post( $post );\n\n\t\t// One user (default post author).\n\t\t$membership->instructors()->set_instructors( array() );\n\n\t\t$template = $this->get_output( 'llms_template_membership_instructors' );\n\n\t\t$this->assertStringContains( 'Membership Instructor', $template );\n\t\t$this->assertStringContains( '<div class=\"llms-col-1\">', $template );\n\t\t$this->assertStringContains( '<span class=\"llms-author-info name\">Jimothy Halpert</span>', $template );\n\t\t$this->assertStringContains( '<span class=\"llms-author-info label\">Author</span>', $template );\n\t\t$this->assertStringContains( '<p class=\"llms-author-info bio\">Paper salesman at Dunder Mifflin Scranton.</p>', $template );\n\n\t\t// Two Instructors.\n\t\t$membership->instructors()->set_instructors( array( array( 'id' => $user->ID ), array( 'id' => $user2->ID ) ) );\n\n\t\t$template = $this->get_output( 'llms_template_membership_instructors' );\n\n\t\t$this->assertStringContains( 'Membership Instructors', $template );\n\t\t$this->assertStringContains( '<div class=\"llms-col-2\">', $template );\n\n\t\t$this->assertStringContains( '<span class=\"llms-author-info name\">Jimothy Halpert</span>', $template );\n\t\t$this->assertStringContains( '<p class=\"llms-author-info bio\">Paper salesman at Dunder Mifflin Scranton.</p>', $template );\n\n\t\t$this->assertStringContains( '<span class=\"llms-author-info name\">Dwight Schrute</span>', $template );\n\t\t$this->assertStringContains( '<p class=\"llms-author-info bio\">Assistant <em>to</em> the Regional Manager at Dunder Mifflin Scranton.</p>', $template );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/functions-templates/class-llms-test-functions-templates-pricing-table.php",
    "content": "<?php\n/**\n * Tests for LifterLMS User Postmeta functions\n * @group    functions\n * @group    template_functions\n * @group    pricing_tables\n * @since    3.23.0\n * @version  3.23.0\n */\nclass LLMS_Test_Functions_Templates_Pricing_Tables extends LLMS_UnitTestCase {\n\n\t/**\n\t * Retrieve output buffer for a given template function and access plan\n\t * @param    string     $func       template function name\n\t * @param    array      $plan_args  plan arguments, passed to $this->get_mock_plan()\n\t * @param    obj        $plan       optionally pass a plan (ignores $plan_args)\n\t * @return   array\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tprivate function get_ob( $func, $plan_args = array(), $plan = null ) {\n\n\t\tif ( is_null( $plan ) ) {\n\t\t\t$plan = call_user_func_array( array( $this, 'get_mock_plan' ), $plan_args );\n\t\t}\n\n\t\tob_start();\n\t\tcall_user_func( $func, $plan );\n\t\treturn array(\n\t\t\t'plan' => $plan,\n\t\t\t'html' => trim( ob_get_clean() ),\n\t\t);\n\n\t}\n\n\t/**\n\t * test the llms_get_access_plan_classes method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_get_access_plan_classes() {\n\n\t\t$expect = 'llms-access-plan llms-access-plan-%d';\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$this->assertEquals( sprintf( $expect, $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) );\n\n\t\t// on sale\n\t\t$plan = $this->get_mock_plan( 1, 1, 'liftetime', true );\n\t\t$this->assertEquals( sprintf( $expect . ' on-sale', $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) );\n\n\t\t// featured\n\t\t$plan = $this->get_mock_plan();\n\t\t$plan->set_visibility( 'featured' );\n\t\t$this->assertEquals( sprintf( $expect . ' featured', $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) );\n\n\t\t// featured & on sale\n\t\t$plan = $this->get_mock_plan( 1, 1, 'liftetime', true );\n\t\t$plan->set_visibility( 'featured' );\n\t\t$this->assertEquals( sprintf( $expect . ' featured on-sale', $plan->get( 'id' ) ), llms_get_access_plan_classes( $plan ) );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan() {\n\n\t\t$ob = $this->get_ob( 'llms_template_access_plan' );\n\n\t\t$this->assertTrue( 0 === strpos( $ob['html'], '<div class=\"llms-access-plan' ) );\n\t\t$this->assertTrue( strlen( $ob['html'] ) - 6 === strrpos( $ob['html'], '</div>' ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], sprintf( 'id=\"llms-access-plan-%d\"', $ob['plan']->get( 'id' ) ) ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_before_access_plan' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_acces_plan_content' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_acces_plan_footer' ) );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan_button method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan_button() {\n\n\t\tLLMS_Install::create_pages();\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_button', array( 0 ) );\n\n\t\t// purchase button link\n\t\t$this->assertTrue( false !== strpos( $ob['html'], '<a class=\"llms-button-action button\"' ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], sprintf( 'href=\"%s\"', esc_url( $ob['plan']->get_checkout_url() ) ) ) );\n\n\t\t// check free enroll form\n\t\t$student = $this->get_mock_student();\n\t\twp_set_current_user( $student->get_id() );\n\t\t$ob['plan']->set( 'is_free', 'yes' );\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_button', array(), $ob['plan'] );\n\t\t$this->assertTrue( 0 === strpos( $ob['html'], '<form' ) );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan_description method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan_description() {\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$plan->set( 'content', '<p>mock description</p>' );\n\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_description', array(), $plan );\n\n\t\t$this->assertTrue( 0 === strpos( $ob['html'], '<div class=\"llms-access-plan-description\">' ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], '<p>mock description</p>' ) );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan_feature method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan_feature() {\n\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_feature', array() );\n\n\t\t// not featured\n\t\t$this->assertTrue( 0 === strpos( $ob['html'], '<div class=\"llms-access-plan-featured\">' ) );\n\t\t$this->assertTrue( false === strpos( $ob['html'], 'FEATURED' ) );\n\n\t\t// featured\n\t\t$ob['plan']->set_visibility( 'featured' );\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_feature', array(), $ob['plan'] );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], 'FEATURED' ) );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan_pricing method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan_pricing() {\n\n\t\t// single, not on sale, no expiration, no recurring\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_pricing', array( 1, 0 ) );\n\t\t$this->assertTrue( 0 === strpos( $ob['html'], '<div class=\"llms-access-plan-pricing regular\">' ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], wp_kses( llms_price( 1 ), LLMS_ALLOWED_HTML_PRICES ) ) );\n\t\t$this->assertTrue( false === strpos( $ob['html'], 'SALE' ) );\n\t\t$this->assertTrue( false === strpos( $ob['html'], 'class=\"llms-access-plan-schedule\"' ) );\n\t\t$this->assertTrue( false === strpos( $ob['html'], 'class=\"llms-access-plan-expiration\"' ) );\n\n\t\t// on sale\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_pricing', array( 1, 0, 'liftetime', true ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], 'SALE' ) );\n\n\t\t// expires\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_pricing', array( 1, 0, 'limited-date' ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], 'class=\"llms-access-plan-expiration\"' ) );\n\n\t\t// recurring\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_pricing' );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], 'class=\"llms-access-plan-schedule\"' ) );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan_restrictions method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan_restrictions() {\n\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_restrictions' );\n\t\t$this->assertEmpty( $ob['html'] );\n\n\t\t// has restriction\n\t\t$mid = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t) );\n\t\t$ob['plan']->set( 'availability', 'members' );\n\t\t$ob['plan']->set( 'availability_restrictions', array( $mid ) );\n\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_restrictions', array(), $ob['plan'] );\n\t\t$this->assertTrue( 0 === strpos( $ob['html'], '<div class=\"llms-access-plan-restrictions\">' ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], get_the_title( $mid ) ) );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan_title method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan_title() {\n\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_title' );\n\t\t$this->assertEquals( sprintf( '<h4 class=\"llms-access-plan-title\">%s</h4>', $ob['plan']->get( 'title' ) ), $ob['html'] );\n\n\t}\n\n\t/**\n\t * test the llms_template_access_plan_trial method\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_llms_template_access_plan_trial() {\n\n\t\t// no trial\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_trial' );\n\t\t$this->assertTrue( false === strpos( $ob['html'], '<div class=\"llms-access-plan-pricing trial\">' ) );\n\t\t$this->assertTrue( false === strpos( $ob['html'], 'TRIAL' ) );\n\n\t\t// has trial\n\t\t$ob = $this->get_ob( 'llms_template_access_plan_trial', array( 1, 1, 'lifetime', false, true ) );\n\t\t$this->assertTrue( false !== strpos( $ob['html'], 'TRIAL' ) );\n\n\t}\n\n\t/**\n\t * test test_lifterlms_template_pricing_table method\n\t * @todo     add tests to test logic in template\n\t * @return   void\n\t * @since    3.23.0\n\t * @version  3.23.0\n\t */\n\tpublic function test_lifterlms_template_pricing_table() {\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$course = $plan->get_product();\n\n\t\t$plan = $this->get_mock_plan( 15 );\n\t\t$plan->set( 'product_id', $course->get( 'id' ) );\n\n\t\t$plan = $this->get_mock_plan( 1 );\n\t\t$plan->set( 'product_id', $course->get( 'id' ) );\n\n\t\t$manual = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), 'no' );\n\n\t\t// no gateways available\n\t\tob_start();\n\t\tlifterlms_template_pricing_table( $course->get( 'id' ) );\n\t\t$html = trim( ob_get_clean() );\n\n\t\t$this->assertEmpty( $html );\n\n\t\t// gateways available\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), 'yes' );\n\n\t\tob_start();\n\t\tlifterlms_template_pricing_table( $course->get( 'id' ) );\n\t\t$html = trim( ob_get_clean() );\n\n\t\t$this->assertTrue( 0 === strpos( $html, '<section class=\"llms-access-plans cols-3\">' ) );\n\t\t$this->assertEquals( 3, did_action( 'llms_access_plan' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/integrations/class-llms-test-integration-bbpress.php",
    "content": "<?php\n/**\n * Tests for LLMS_Integration_BBPress class\n *\n * @package LifterLMS/Tests/Integrations\n *\n * @group integrations\n * @group integration_bbpress\n *\n * @since 3.37.11\n * @since 3.38.1 Added test on forum values saved as array of strings.\n */\nclass LLMS_Test_Integration_BBPress extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Instance of a mock bbPress class.\n\t *\n\t * @var bbPress\n\t */\n\tprotected $mock_bbPress = null;\n\n\t/**\n\t * Instance of the bbPress integration class.\n\t *\n\t * @var LLMS_Integration_BBPress\n\t */\n\tprotected $main = null;\n\n\t/**\n\t * Array of hooks added by the integration.\n\t *\n\t * @var array\n\t */\n\tprotected $hooks = array();\n\n\t/**\n\t * Add or remove hooks based on hooks defined in the $this->hooks array.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @param string $action Either \"add\" or \"remove\".\n\t * @return void\n\t */\n\tprivate function update_hooks( $action = 'add' ) {\n\n\t\tforeach ( $this->hooks as $hook ) {\n\n\t\t\t$function = sprintf( '%1$s_%2$s', $action, $hook['type'] );\n\t\t\t$function( $hook['hook'], $hook['method'], $hook['priority'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Run assertions for all hooks in the $this->hooks array.\n\t *\n\t * @since  3.37.11\n\t *\n\t * @param  mixed $equals If `null`, asserts that the priority matches the configured priority. Otherwise all hooks equal this value.\n\t * @return void\n\t */\n\tprivate function assertHooks( $equals = null ) {\n\n\t\tforeach( $this->hooks as $hook ) {\n\n\t\t\t$function = sprintf( 'has_%s', $hook['type'] );\n\t\t\t$this->assertEquals( is_null( $equals ) ? $hook['priority'] : $equals, $function( $hook['hook'], $hook['method'] ), $hook['hook'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Setup all the hooks defined in the configuration method.\n\t *\n\t * @since  3.37.11\n\t *\n\t * @return void\n\t */\n\tprivate function setup_hooks() {\n\n\t\t$this->hooks = array(\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'lifterlms_engagement_triggers',\n\t\t\t\t'method'   => array( $this->main, 'register_engagement_triggers' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'lifterlms_external_engagement_query_arguments',\n\t\t\t\t'method'   => array( $this->main, 'engagement_query_args' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'llms_load_shortcodes',\n\t\t\t\t'method'   => array( $this->main, 'register_shortcodes' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'llms_membership_restricted_post_types',\n\t\t\t\t'method'   => array( $this->main, 'add_membership_restrictions' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'llms_page_restricted_before_check_access',\n\t\t\t\t'method'   => array( $this->main, 'restriction_checks_memberships' ),\n\t\t\t\t'priority' => 40,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'llms_page_restricted_before_check_access',\n\t\t\t\t'method'   => array( $this->main, 'restriction_checks_courses' ),\n\t\t\t\t'priority' => 50,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'llms_metabox_fields_lifterlms_course_options',\n\t\t\t\t'method'   => array( $this->main, 'course_settings_fields' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'llms_get_course_properties',\n\t\t\t\t'method'   => array( $this->main, 'add_course_props' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\n\t\t\tarray(\n\t\t\t\t'type'     => 'action',\n\t\t\t\t'hook'     => 'bbp_new_topic',\n\t\t\t\t'method'   => array( llms()->engagements(), 'maybe_trigger_engagement' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'action',\n\t\t\t\t'hook'     => 'bbp_new_reply',\n\t\t\t\t'method'   => array( llms()->engagements(), 'maybe_trigger_engagement' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'action',\n\t\t\t\t'hook'     => 'llms_metabox_after_save_lifterlms-course-options',\n\t\t\t\t'method'   => array( $this->main, 'save_course_settings' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'action',\n\t\t\t\t'hook'     => 'llms_content_restricted_by_bbp_course_forum',\n\t\t\t\t'method'   => array( $this->main, 'handle_course_forum_restriction' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Setup the mock bbPress class and functions.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tprotected function setup_mock_bbPress() {\n\n\t\t// Mock functions.\n\t\tif ( ! function_exists( 'bbp_get_forum_post_type' ) ) {\n\t\t\tfunction bbp_get_forum_post_type() {\n\t\t\t\treturn 'mock_forum_post_type';\n\t\t\t}\n\t\t}\n\n\t\t// Create the mock bbPress class.\n\t\t$this->mock_bbPress = $this->getMockBuilder( 'bbPress' )\n\t\t\t->getMock();\n\n\t\t// Enable the integration.\n\t\tupdate_option( 'llms_integration_bbpress_enabled', 'yes' );\n\n\t\t// Refresh cached available integrations list.\n\t\tllms()->integrations()->get_available_integrations();\n\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.11\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t// Load mock.\n\t\tif ( ! $this->mock_bbPress ) {\n\t\t\t$this->setup_mock_bbPress();\n\t\t}\n\n\t\t$this->main = llms()->integrations()->get_integration( 'bbpress' );\n\n\t\tif ( ! $this->hooks ) {\n\t\t\t$this->setup_hooks();\n\t\t}\n\n\t}\n\n\t/**\n\t * Test that attributes are setup properly.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_attributes() {\n\n\t\t$this->assertEquals( 'bbPress', $this->main->title );\n\t\t$this->assertTrue( ! empty( $this->main->description ) );\n\n\t}\n\n\t/**\n\t * Test configure()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_configure() {\n\n\t\t// Disable the integration.\n\t\tupdate_option( 'llms_integration_bbpress_enabled', 'no' );\n\n\t\t// Remove all the set hooks.\n\t\t$this->update_hooks( 'remove' );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'configure' );\n\n\t\t// All hooks should be false when calling has_action()/has_filter().\n\t\t$this->assertHooks( false );\n\n\n\t\t// Re-enable the integration.\n\t\t$this->setup_mock_bbPress();\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'configure' );\n\n\t\t// All hooks should be configured.\n\t\t$this->assertHooks();\n\n\t}\n\n\t/**\n\t * Test add_course_props()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_course_props() {\n\n\t\t$this->assertEquals( array( 'bbp_forum_ids' => 'array' ), $this->main->add_course_props( array(), 'mock' ) );\n\n\t}\n\n\t/**\n\t * Test add_membership_restrictions()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_membership_restrictions() {\n\n\t\t$this->assertEquals( array( 'mock_forum_post_type' ), $this->main->add_membership_restrictions( array() ) );\n\n\t}\n\n\t/**\n\t * Test course_settings_field()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_course_settings_fields() {\n\n\t\t$res = $this->main->course_settings_fields( array() )[0];\n\n\t\t$this->assertEquals( 'bbPress', $res['title'] );\n\t\t$this->assertEquals( 1, count( $res['fields'] ) );\n\n\t\t$this->assertequals( '_llms_bbp_forum_ids', $res['fields'][0]['id'] );\n\n\t}\n\n\t/**\n\t * Test engagement_query_args() for non bbPress hooks.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_engagement_query_args_not_supported() {\n\n\t\t$expect = array(\n\t\t\t'trigger_type'    => 'mock',\n\t\t\t'related_post_id' => 123,\n\t\t\t'user_id'         => 456,\n\t\t);\n\t\t$this->assertEquals( $expect, $this->main->engagement_query_args( $expect, 'mock_action', array() ) );\n\n\t}\n\n\t/**\n\t * Test engagement_query_args() for a new reply hook.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_engagement_query_args_new_reply() {\n\n\t\t$args = array(\n\t\t\t'trigger_type'    => 'mock',\n\t\t\t'related_post_id' => 123,\n\t\t\t'user_id'         => 456,\n\t\t);\n\n\t\t$expect = array(\n\t\t\t'trigger_type'    => 'bbp_new_reply',\n\t\t\t'related_post_id' => '',\n\t\t\t'user_id'         => 4,\n\t\t);\n\n\t\t$this->assertEquals( $expect, $this->main->engagement_query_args( $args, 'bbp_new_reply', array( 0, 1, 2, 3, 4, ) ) );\n\n\t}\n\n\t/**\n\t * Test engagement_query_args() for a new topic\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_engagement_query_args_new_topic() {\n\n\t\t$args = array(\n\t\t\t'trigger_type'    => 'mock',\n\t\t\t'related_post_id' => 123,\n\t\t\t'user_id'         => 456,\n\t\t);\n\n\t\t$expect = array(\n\t\t\t'trigger_type'    => 'bbp_new_topic',\n\t\t\t'related_post_id' => '',\n\t\t\t'user_id'         => 3,\n\t\t);\n\n\t\t$this->assertEquals( $expect, $this->main->engagement_query_args( $args, 'bbp_new_topic', array( 0, 1, 2, 3 ) ) );\n\n\t}\n\n\t/**\n\t * Test handle_course_forum_restriction()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_course_forum_restriction() {\n\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t$this->expectException( LLMS_Unit_Test_Exception_Redirect::class );\n\t\t$this->expectExceptionMessage( sprintf( '%s [302] YES', get_permalink( $id ) ) );\n\n\t\ttry {\n\n\t\t\t$this->main->handle_course_forum_restriction( array( 'restriction_id' => $id ) );\n\n\t\t} catch( LLMS_Unit_Test_Exception_Redirect $exception ) {\n\n\t\t\t$notices = llms_get_notices();\n\n\t\t\t$this->assertStringContains( 'llms-error', $notices );\n\t\t\t$this->assertStringContains( 'You must be enrolled in this course to access the course forum.', $notices );\n\n\t\t\tthrow $exception;\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_course_forum_ids()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_course_forum_ids() {\n\n\t\t// Ensure property filter is applied.\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'configure' );\n\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t// Nothing set.\n\t\t$this->assertEquals( array(), $this->main->get_course_forum_ids( $id ) );\n\n\t\t// Empty string.\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', '' );\n\t\t$this->assertEquals( array(), $this->main->get_course_forum_ids( $id ) );\n\n\t\t// Empty array.\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array() );\n\t\t$this->assertEquals( array(), $this->main->get_course_forum_ids( $id ) );\n\n\t\t// Has forums.\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array( 1, 2, 3 ) );\n\t\t$this->assertEquals( array( 1, 2, 3 ), $this->main->get_course_forum_ids( $id ) );\n\n\t}\n\n\t/**\n\t * Test get_forum_course_restrictions()\n\t *\n\t * @since 3.37.11\n\t * @since 3.38.1 Made sure it's able to match forum ids either saved as strings or integers.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_forum_course_restrictions() {\n\n\t\t// No restrictions for a fake forum.\n\t\t$this->assertEquals( array(), $this->main->get_forum_course_restrictions( 3452 ) );\n\n\t\t// Restricted to one course.\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array( 9239 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 9239 ) );\n\n\t\t// Restricted to two courses.\n\t\t$id2 = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\tupdate_post_meta( $id2, '_llms_bbp_forum_ids', array( 9239 ) );\n\t\t$this->assertEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 9239 ) );\n\n\t\t// Restricted to two courses, second course forum ids saved as strings.\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array( 9239, 1008 ) );\n\t\tupdate_post_meta( $id2, '_llms_bbp_forum_ids', array( '9239', '1008', '1007' ) );\n\n\t\t// Make sure we don't match a forum id which is part of one of the saved values.\n\t\t$this->assertNotEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 923 ) );\n\n\t\t$this->assertEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 9239 ) );\n\t\t$this->assertEquals( array( $id, $id2 ), $this->main->get_forum_course_restrictions( 1008 ) );\n\t\t$this->assertEquals( array( $id2 ), $this->main->get_forum_course_restrictions( 1007 ) );\n\n\t\tupdate_post_meta( $id2, '_llms_bbp_forum_ids', array( '1' ) );\n\t\t$this->assertEquals( array( $id2 ), $this->main->get_forum_course_restrictions( 1 ) );\n\n\t\t/**\n\t\t * Edge case check:\n\t\t * We save the values as a serialized array, and before 3.37.11 we used to save them as integers.\n\t\t * Our SQL query to retrieve the courses linked to a certain forum uses a REGEXP to match the forum id in it.\n\t\t * This REGEXP is able to match either ids saved as strings or integers.\n\t\t * We want also to be sure that if we have a value of the type\n\t\t * a:3:{i:0;i:2299;i:1;i:3333;i:2:i:7777;}\n\t\t * and a forum id to check against equal to 1, that value above doesn't match.\n\t\t * This would mean that our query is able differentiate between serialized array item values and indexes.\n\t\t */\n\t\t// Case saved as integers.\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array( 2299, 3333, 7777, 9999, 29999, 109999 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 2299 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 3333 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 7777 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 9999 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 29999 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 109999 ) );\n\n\n\t\t// Make sure we don't match the array indexes.\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 0 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 1 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 2 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 3 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 4 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 5 ) );\n\n\t\t// Case saved as strings.\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array( '12299', '13333', '17777', '19999', '129999', '1109999' ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 12299 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 13333 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 17777 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 19999 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 129999 ) );\n\t\t$this->assertEquals( array( $id ), $this->main->get_forum_course_restrictions( 1109999 ) );\n\n\t\t// Make sure we don't match the array indexes\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 0 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 1 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 2 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 3 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 4 ) );\n\t\t$this->assertNotEquals( array( $id ), $this->main->get_forum_course_restrictions( 5 ) );\n\t}\n\n\t/**\n\t * Test is_installed()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_installed() {\n\n\t\t$this->assertTrue( $this->main->is_installed() );\n\n\t}\n\n\t/**\n\t * Test register_shortcodes()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_shortcodes() {\n\n\t\t$this->assertEquals( array( 'LLMS_BBP_Shortcode_Course_Forums_List' ), $this->main->register_shortcodes( array() ) );\n\n\t}\n\n\t// @todo these tests should be written but I'm tired.\n\t// public function test_restriction_checks_courses() {}\n\t// public function test_restriction_checks_memberships() {}\n\n\t/**\n\t * Test register_engagement_triggers()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_register_engagement_triggers() {\n\n\t\t$this->assertEquals( array( 'bbp_new_topic', 'bbp_new_reply' ), array_keys( $this->main->register_engagement_triggers( array() ) ) );\n\n\t}\n\n\t/**\n\t * Test save_course_settings() when a quick edit is performed on a course.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_course_settings_quick_edit() {\n\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$expect = array( 1, 2, 3 );\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', $expect );\n\n\t\t$this->mockPostRequest( array( 'action' => 'inline-save' ) );\n\t\t$this->assertNull( $this->main->save_course_settings( $id ) );\n\n\t\t$this->assertEquals( $expect, get_post_meta( $id, '_llms_bbp_forum_ids', true ) );\n\n\t}\n\n\t/**\n\t * Test save_course_settings() correctly saving strings\n\t *\n\t * @since 3.38.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_course_settings_save_strings() {\n\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$expect = array( 1, 2, 3 );\n\n\t\t$this->mockPostRequest( array( 'action' => '', '_llms_bbp_forum_ids' => $expect ) );\n\t\t$this->main->save_course_settings( $id );\n\n\t\t$this->assertEquals( $expect, array_filter( get_post_meta( $id, '_llms_bbp_forum_ids', true ), 'is_string' ) );\n\n\t}\n\n\t/**\n\t * Test save_course_settings() when there's no new ids passed to the form.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_course_settings_not_set() {\n\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array( 1 ) );\n\n\t\t$this->assertEquals( array(), $this->main->save_course_settings( $id ) );\n\t\t$this->assertEquals( array(), get_post_meta( $id, '_llms_bbp_forum_ids', true ) );\n\n\t}\n\n\t/**\n\t * Test save_course_settiongs()\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_course_settings() {\n\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$expect = array( 1, 2, 3 );\n\t\t$this->mockPostRequest( array( '_llms_bbp_forum_ids' => $expect ) );\n\n\t\t$this->assertEquals( $expect, $this->main->save_course_settings( $id ) );\n\t\t$this->assertEquals( $expect, get_post_meta( $id, '_llms_bbp_forum_ids', true ) );\n\n\t}\n\n\t/**\n\t * Test save_course_settings() to delete existing courses.\n\t *\n\t * @since 3.37.11\n\t *\n\t * @return void\n\t */\n\tpublic function test_save_course_settings_delete() {\n\n\t\t$id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\tupdate_post_meta( $id, '_llms_bbp_forum_ids', array( 1 ) );\n\t\t$this->mockPostRequest( array( '_llms_bbp_forum_ids' => array() ) );\n\n\t\t$this->assertEquals( array(), $this->main->save_course_settings( $id ) );\n\t\t$this->assertEquals( array(), get_post_meta( $id, '_llms_bbp_forum_ids', true ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/integrations/class-llms-test-integration-buddypress.php",
    "content": "<?php\n/**\n * Tests for LLMS_Integration_Byddypress class\n *\n * @package LifterLMS/Tests/Integrations\n *\n * @group integrations\n * @group integration_buddypress\n *\n * @since 6.3.0\n */\nclass LLMS_Test_Integration_Buddypress extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Instance of a mock BuddyPress class.\n\t *\n\t * @var BuddyPress\n\t */\n\tprivate $mock_buddypress = null;\n\n\t/**\n\t * Instance of the BuddyPress integration class.\n\t *\n\t * @var LLMS_Integration_Buddypress\n\t */\n\tprivate $main = null;\n\n\t/**\n\t * Array of hooks added by the integration.\n\t *\n\t * @var array\n\t */\n\tprivate $hooks = array();\n\n\t/**\n\t * Logged in user slug to be used to build the profile domain.\n\t *\n\t * @var string\n\t */\n\tprivate $loggedin_user_slug;\n\n\t/**\n\t * Displayed user slug to be used to build the profile domain.\n\t *\n\t * @var string\n\t */\n\tprivate $displayed_user_slug;\n\n\n\t/**\n\t * Displayed user id to be used to build the profile nav.\n\t *\n\t * @var string\n\t */\n\tprivate $displayed_user_id;\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t// Load mock.\n\t\tif ( ! $this->mock_buddypress ) {\n\t\t\t$this->setup_mock_buddypress();\n\t\t}\n\n\t\t$this->main = llms()->integrations()->get_integration( 'buddypress' );\n\n\t\tif ( ! $this->hooks ) {\n\t\t\t$this->setup_hooks();\n\t\t}\n\n\t}\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\n\t\t// Reset private property endpoints.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'endpoints', null );\n\t\tunset( $GLOBALS['bp'] );\n\n\t}\n\n\t/**\n\t * Test that attributes are setup properly.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_attributes() {\n\n\t\t$this->assertEquals( 'BuddyPress', $this->main->title );\n\t\t$this->assertTrue( ! empty( $this->main->description ) );\n\n\t}\n\n\t/**\n\t * Test configure().\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_configure() {\n\n\t\t// Disable the integration.\n\t\tupdate_option( 'llms_integration_buddypress_enabled', 'no' );\n\n\t\t// Remove all the set hooks.\n\t\t$this->update_hooks( 'remove' );\n\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'configure' );\n\n\t\t// All hooks should be false when calling has_action()/has_filter().\n\t\t$this->assertHooks( false );\n\n\t\t// Re-enable the integration.\n\t\t$this->setup_mock_buddypress();\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'configure' );\n\n\t\t// All hooks should be configured.\n\t\t$this->assertHooks();\n\n\t}\n\n\t/**\n\t * Test is_installed().\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_installed() {\n\n\t\t$this->assertTrue( $this->main->is_installed() );\n\n\t}\n\n\t/**\n\t * Test populate_profile_endpoints().\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_populate_profile_endpoints() {\n\n\t\t// Populate all possible endpoints.\n\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'populate_profile_endpoints' );\n\n\t\t// By default they're all the default LLMS_Student_Dashboard endpoints except 'dashboard' and 'signout'.\n\t\t$dashboard_endpoints = LLMS_Student_Dashboard::get_tabs();\n\t\t$endpoints           = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'endpoints' );\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\t'dashboard',\n\t\t\t\t'signout',\n\t\t\t),\n\t\t\tarray_values(\n\t\t\t\tarray_diff(\n\t\t\t\t\tarray_keys( $dashboard_endpoints ),\n\t\t\t\t\tarray_keys( $endpoints )\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\t// Check the endpoints do not have the fields 'nav_item', 'url', 'paginate'.\n\t\tforeach ( array( 'nav_item', 'url', 'paginate' ) as $field ) {\n\t\t\t$this->assertEmpty( array_column( $endpoints, $field ), $field );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_profile_endpoints().\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_profile_endpoints() {\n\n\t\t// Get all possible endpoints ($active_only=false).\n\t\t$profile_endpoints = LLMS_Unit_Test_Util::call_method( $this->main, 'get_profile_endpoints', array( false ) );\n\t\t$endpoints         = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'endpoints' );\n\n\t\t// They're equal to the populated endpoints.\n\t\t$this->assertEquals(\n\t\t\t$endpoints,\n\t\t\t$profile_endpoints\n\t\t);\n\n\t\t// Set the profile endpoints option as only the first of all the available endpoints.\n\t\t$this->main->set_option( 'profile_endpoints', array( key( $endpoints ) ) );\n\t\t// Get active endpoints ($active_only=false).\n\t\t$profile_endpoints = LLMS_Unit_Test_Util::call_method( $this->main, 'get_profile_endpoints', array( false ) );\n\t\t// They're equal to the populated endpoints.\n\t\t$this->assertEquals(\n\t\t\t$endpoints,\n\t\t\t$profile_endpoints\n\t\t);\n\n\t\t// Get active only.\n\t\t$profile_endpoints = LLMS_Unit_Test_Util::call_method( $this->main, 'get_profile_endpoints' );\n\t\t// They only contains the first possible endpoint.\n\t\t$this->assertEquals(\n\t\t\tarray( key( $endpoints ) => reset( $endpoints ) ),\n\t\t\t$profile_endpoints\n\t\t);\n\n\t\t// Set the profile endpoints option as empty string.\n\t\t$this->main->set_option( 'profile_endpoints', array( '' ) );\n\t\t// Get active only.\n\t\t$profile_endpoints = LLMS_Unit_Test_Util::call_method( $this->main, 'get_profile_endpoints' );\n\t\t// Empty array.\n\t\t$this->assertEmpty( $profile_endpoints );\n\n\t}\n\n\t/**\n\t * Test get_profile_endpoints_options().\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_profile_endpoints_options() {\n\n\t\t$endpoints_options = LLMS_Unit_Test_Util::call_method( $this->main, 'get_profile_endpoints_options' );\n\t\t// Must be an array having as keys all the possible endpoints, and values the endpoints titles.\n\t\t$endpoints = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'endpoints' );\n\n\t\t$this->assertEquals(\n\t\t\tarray_combine(\n\t\t\t\tarray_keys( $endpoints ),\n\t\t\t\tarray_column( $endpoints, 'title' )\n\t\t\t),\n\t\t\t$endpoints_options\n\t\t);\n\n\t}\n\n\t/**\n\t * Test add_profile_nav_items().\n\t *\n\t * @since 6.3.0\n\t * @since 6.8.0 Main nav item always shown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_profile_nav_items() {\n\n\t\t$bp = buddypress();\n\n\t\t// User not logged in.\n\t\t$this->_setup_members_nav();\n\t\t$this->_set_bp_is_my_profile_false();\n\n\t\t$this->main->add_profile_nav_items();\n\t\t$endpoints = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'endpoints' );\n\n\t\t$this->assertNotEmpty(\n\t\t\t$bp->members->nav->nav[0][ 'courses' ]\n\t\t);\n\n\t\tforeach ( $endpoints as $key => $endpoint ) {\n\t\t\t$this->assertNotEmpty(\n\t\t\t\t$bp->members->nav->nav[0][ 'courses/' . $endpoint['endpoint'] ]\n\t\t\t);\n\t\t\t$this->assertEquals(\n\t\t\t\t$endpoint['title'],\n\t\t\t\t$bp->members->nav->nav[0][ 'courses/' . $endpoint['endpoint'] ]->name\n\t\t\t);\n\t\t\t// No access to this.\n\t\t\t$this->assertFalse(\n\t\t\t\t$bp->members->nav->nav[0][ 'courses/' . $endpoint['endpoint'] ]->user_has_access\n\t\t\t);\n\t\t}\n\n\t\t// Log in as admin.\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $admin_id );\n\t\t$this->loggedin_user_slug = 'admin';\n\t\t$this->_set_loggedin_user_domain();\n\n\t\t// Visit someone else profile: cannot see other's courses related endpoints.\n\t\t$this->_set_bp_is_my_profile_false();\n\t\t$this->displayed_user_id = $admin_id + 1;\n\t\t$this->_set_displayed_user_id();\n\t\t$this->_setup_members_nav();\n\t\t$this->main->add_profile_nav_items();\n\t\tforeach ( $endpoints as $key => $endpoint ) {\n\t\t\t$this->assertNotEmpty(\n\t\t\t\t$bp->members->nav->nav[ $this->displayed_user_id ][ 'courses/' . $endpoint['endpoint'] ]\n\t\t\t);\n\t\t\t$this->assertEquals(\n\t\t\t\t$endpoint['title'],\n\t\t\t\t$bp->members->nav->nav[ $this->displayed_user_id ][ 'courses/' . $endpoint['endpoint'] ]->name\n\t\t\t);\n\t\t\t// No access to the endpoint's content.\n\t\t\t$this->assertFalse(\n\t\t\t\t$bp->members->nav->nav[ $this->displayed_user_id ][ 'courses/' . $endpoint['endpoint'] ]->user_has_access\n\t\t\t);\n\t\t}\n\n\t\t// 'admin' visiting 'admin' profile.\n\t\t$this->_set_bp_is_my_profile_true();\n\t\t$this->displayed_user_id = $admin_id;\n\t\t$this->_set_displayed_user_id();\n\t\t$this->_setup_members_nav();\n\t\t$this->main->add_profile_nav_items();\n\t\t$this->assertNotEmpty( $bp->members->nav->nav[ $this->displayed_user_id ] );\n\n\t\t$this->assertNotEmpty(\n\t\t\t$bp->members->nav->nav[ $this->displayed_user_id ][ 'courses' ]\n\t\t);\n\n\t\t// Check all the endpoints are registered as subnav items of 'course'.\n\t\tforeach ( $endpoints as $key => $endpoint ) {\n\t\t\t$this->assertNotEmpty(\n\t\t\t\t$bp->members->nav->nav[ $this->displayed_user_id ][ 'courses/' . $endpoint['endpoint'] ]\n\t\t\t);\n\t\t\t$this->assertEquals(\n\t\t\t\t$endpoint['title'],\n\t\t\t\t$bp->members->nav->nav[ $this->displayed_user_id ][ 'courses/' . $endpoint['endpoint'] ]->name\n\t\t\t);\n\t\t\t// Can see the endpoint content.\n\t\t\t$this->assertTrue(\n\t\t\t\t$bp->members->nav->nav[ $this->displayed_user_id ][ 'courses/' . $endpoint['endpoint'] ]->user_has_access\n\t\t\t);\n\t\t}\n\n\t\t// Reset.\n\t\t$this->_clear_bp_is_my_profile();\n\t\t$this->_clear_displayed_user_domain();\n\t\t$this->_clear_loggedin_user_domain();\n\t\t$this->_clear_displayed_user_id();\n\n\t}\n\n\t/**\n\t * Test endpoint_content() method.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_endpoint_content() {\n\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $admin_id );\n\n\t\t$profile_endpoints = LLMS_Unit_Test_Util::call_method( $this->main, 'get_profile_endpoints' );\n\t\tforeach ( $profile_endpoints as $key => $endpoint ) {\n\n\t\t\t$content = $this->get_output( array( $this->main, 'endpoint_content' ), array( $key, $endpoint['content'] ) );\n\t\t\t// Current endpoint set.\n\t\t\t$this->assertEquals(\n\t\t\t\t$key,\n\t\t\t\tLLMS_Unit_Test_Util::get_private_property_value( $this->main, 'current_endpoint_key' ),\n\t\t\t\t$key\n\t\t\t);\n\n\t\t\t// BuddyPress tab content is the $endpoint['content'].\n\t\t\t$this->assertEquals(\n\t\t\t\t$this->get_output( $endpoint['content'] ),\n\t\t\t\t$content,\n\t\t\t\t$key\n\t\t\t);\n\n\t\t\t// Remove the current endpoint callback, for the next loop.\n\t\t\tremove_action( 'bp_template_content', $endpoint['content'] );\n\n\t\t}\n\t}\n\n\t/**\n\t * Test modify_paginate_links() method.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_paginate_links() {\n\n\t\tglobal $wp_rewrite;\n\n\t\t$original_permalink_structure = get_option( 'permalink_structure' );\n\t\t// Ugly permalinks, bail.\n\t\tif ( empty( $origianl_permalink_structure ) ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t'whatever',\n\t\t\t\t$this->main->modify_paginate_links(\n\t\t\t\t\t'whatever'\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Pretty permalinks.\n\t\tupdate_option( 'permalink_structure', '/%postname%/' );\n\t\t$wp_rewrite->init();\n\n\t\t// Enroll to many courses.\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $admin_id );\n\t\t$this->loggedin_user_slug = 'admin';\n\t\t$this->_set_loggedin_user_domain();\n\t\t//$this->factory->student->create_and_enroll_many( 30, $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\n\t\t// 'admin' visiting 'admin' profile.\n\t\t$this->_set_bp_is_my_profile_true();\n\t\t$this->displayed_user_id = $admin_id;\n\t\t$this->_set_displayed_user_id();\n\t\t$this->_setup_members_nav();\n\t\t$this->main->add_profile_nav_items();\n\n\t\t// Go to my-grades tab, not the first subnav.\n\t\t$my_grades = home_url() . '/members/' . $this->loggedin_user_slug . '/courses/my-grades/';\n\t\t$this->go_to( $my_grades );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'current_endpoint_key', 'my-grades' );\n\n\t\t// Link to page 1: page/1/ stripped.\n\t\t$this->assertEquals(\n\t\t\t$my_grades,\n\t\t\t$this->main->modify_paginate_links(\n\t\t\t\t$my_grades . 'page/1/'\n\t\t\t)\n\t\t);\n\n\t\t// Link to page 2: nothing to do.\n\t\t$this->assertEquals(\n\t\t\t$my_grades . 'page/2/',\n\t\t\t$this->main->modify_paginate_links(\n\t\t\t\t$my_grades . 'page/2/'\n\t\t\t)\n\t\t);\n\n\t\t// Link to page 1 but with query args.\n\t\t$this->assertEquals(\n\t\t\t$my_grades . '?query=arg_1',\n\t\t\t$this->main->modify_paginate_links(\n\t\t\t\t$my_grades . 'page/1/?query=arg_1'\n\t\t\t)\n\t\t);\n\n\t\t// Test first subnav.\n\t\t$fist_subnav_first_page = home_url() . '/members/' . $this->loggedin_user_slug . '/courses/';\n\t\t$profile_endpoints      = LLMS_Unit_Test_Util::call_method( $this->main, 'get_profile_endpoints' );\n\t\t$first_endpoint_slug    = reset( $profile_endpoints )['endpoint'];\n\t\t$first_endpoint_key     = key( $profile_endpoints );\n\t\t// Go to first subnav.\n\t\t$this->go_to( $fist_subnav_first_page );\n\t\tset_query_var( 'page', 1 );\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'current_endpoint_key', $first_endpoint_key );\n\n\t\t// Link to page 2.\n\t\t$this->assertEquals(\n\t\t\t$fist_subnav_first_page . $first_endpoint_slug  . '/page/2/',\n\t\t\t$this->main->modify_paginate_links(\n\t\t\t\t$fist_subnav_first_page . 'page/2/'\n\t\t\t)\n\t\t);\n\n\t\t// Go to page 2.\n\t\t$this->go_to( $fist_subnav_first_page . $first_endpoint_slug  . '/page/2/' );\n\t\tset_query_var( 'page', 2 );\n\n\t\t// Link to page 1: expect link to the first subnav first page === defaults to the parent nav URL.\n\t\t$this->assertEquals(\n\t\t\t$fist_subnav_first_page,\n\t\t\t$this->main->modify_paginate_links(\n\t\t\t\t$fist_subnav_first_page . $first_endpoint_slug  . '/page/1/'\n\t\t\t)\n\t\t);\n\n\t\t// Reset.\n\t\t$this->_clear_bp_is_my_profile();\n\t\t$this->_clear_displayed_user_domain();\n\t\t$this->_clear_loggedin_user_domain();\n\t\t$this->_clear_displayed_user_id();\n\t\tupdate_option( 'permalink_structure', $original_permalink_structure );\n\t\t$wp_rewrite->init();\n\t\tset_query_var( 'page', 1 );\n\n\t}\n\n\t/**\n\t * Add or remove hooks based on hooks defined in the $this->hooks array.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @param string $action Either \"add\" or \"remove\".\n\t * @return void\n\t */\n\tprivate function update_hooks( $action = 'add' ) {\n\n\t\tforeach ( $this->hooks as $hook ) {\n\n\t\t\t$function = sprintf( '%1$s_%2$s', $action, $hook['type'] );\n\t\t\t$function( $hook['hook'], $hook['method'], $hook['priority'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Run assertions for all hooks in the $this->hooks array.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @param mixed $equals If `null`, asserts that the priority matches the configured priority. Otherwise all hooks equal this value.\n\t * @return void\n\t */\n\tprivate function assertHooks( $equals = null ) {\n\n\t\tforeach( $this->hooks as $hook ) {\n\n\t\t\t$function = sprintf( 'has_%s', $hook['type'] );\n\t\t\t$this->assertEquals( is_null( $equals ) ? $hook['priority'] : $equals, $function( $hook['hook'], $hook['method'] ), $hook['hook'] );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Setup all the hooks defined in the configuration method.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function setup_hooks() {\n\n\t\t$this->hooks = array(\n\t\t\tarray(\n\t\t\t\t'type'     => 'action',\n\t\t\t\t'hook'     => 'bp_setup_nav',\n\t\t\t\t'method'   => array( $this->main, 'add_profile_nav_items' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'llms_page_restricted_before_check_access',\n\t\t\t\t'method'   => array( $this->main, 'restriction_checks' ),\n\t\t\t\t'priority' => 40,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'type'     => 'filter',\n\t\t\t\t'hook'     => 'lifterlms_update_account_redirect',\n\t\t\t\t'method'   => array( $this->main, 'maybe_alter_update_account_redirect' ),\n\t\t\t\t'priority' => 10,\n\t\t\t),\n\t\t);\n\n\t}\n\n\t/**\n\t * Setup the mock BuddyPress class and functions.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function setup_mock_buddypress() {\n\n\t\t// Mock functions.\n\t\tif ( ! function_exists( 'bp_loggedin_user_domain' ) ) {\n\t\t\tfunction bp_loggedin_user_domain() {\n\t\t\t\t/**\n\t\t\t\t * BuddyPress filter.\n\t\t\t\t *\n\t\t\t\t * @since BuddyPress 1.0.0\n\t\t\t\t */\n\t\t\t\treturn apply_filters( 'bp_loggedin_user_domain', '' );\n\t\t\t}\n\t\t}\n\t\t// Mock functions.\n\t\tif ( ! function_exists( 'bp_displayed_user_id' ) ) {\n\t\t\tfunction bp_displayed_user_id() {\n\t\t\t\t/**\n\t\t\t\t * BuddyPress filter.\n\t\t\t\t *\n\t\t\t\t * @since BuddyPress 1.0.0\n\t\t\t\t */\n\t\t\t\treturn apply_filters( 'bp_displayed_user_id', 0 );\n\t\t\t}\n\t\t}\n\t\tif ( ! function_exists( 'bp_is_my_profile' ) ) {\n\t\t\tfunction bp_is_my_profile() {\n\t\t\t\t/**\n\t\t\t\t * BuddyPress filter.\n\t\t\t\t *\n\t\t\t\t * @since BuddyPress 1.2.4\n\t\t\t\t */\n\t\t\t\treturn apply_filters( 'bp_is_my_profile', false );\n\t\t\t}\n\t\t}\n\t\tif ( ! function_exists( 'bp_core_new_nav_item' ) ) {\n\t\t\tfunction bp_core_new_nav_item( $args ) {\n\n\t\t\t\tif ( empty( $args['slug'] ) ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t$bp                           = buddypress();\n\t\t\t\t$args['primary']              = true;\n\t\t\t\t$args['link']                 = trailingslashit( bp_loggedin_user_domain() . $args['slug'] );\n\t\t\t\t$args['default_subnav_slug '] = $args['default_subnav_slug'];\n\n\t\t\t\t$bp->members->nav->nav[ $bp->members->nav->object_id ][$args['slug']] = new ArrayObject(\n\t\t\t\t\t$args, ArrayObject::ARRAY_AS_PROPS\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\t\tif ( ! function_exists( 'bp_core_new_subnav_item' ) ) {\n\t\t\tfunction bp_core_new_subnav_item( $args ) {\n\n\t\t\t\tif ( empty( $args['slug'] ) || empty( $args['parent_slug'] ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t$bp = buddypress();\n\t\t\t\t$args['secondary'] = true;\n\t\t\t\t$args['link']      = trailingslashit( $args['parent_url'] . $args['slug'] );\n\n\t\t\t\t$parent_nav = $bp->members->nav->get_primary(\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'slug' => $args['parent_slug'],\n\t\t\t\t\t),\n\t\t\t\t\tfalse\n\t\t\t\t);\n\n\t\t\t\t// If this sub item is the default for its parent, skip the slug.\n\t\t\t\tif ( $parent_nav ) {\n\t\t\t\t\t$parent_nav_item = reset( $parent_nav );\n\t\t\t\t\tif ( ! empty( $parent_nav_item->default_subnav_slug ) && $args['slug'] === $parent_nav_item->default_subnav_slug ) {\n\t\t\t\t\t\t$args['link'] = trailingslashit( $args['parent_url'] );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t$bp->members->nav->nav[ $bp->members->nav->object_id ][$args['parent_slug'] . '/' . $args['slug']] = new ArrayObject(\n\t\t\t\t\t$args, ArrayObject::ARRAY_AS_PROPS\n\t\t\t\t);\n\n\t\t\t}\n\t\t}\n\t\tif ( ! function_exists( 'bp_core_load_template' ) ) {\n\t\t\tfunction bp_core_load_template( $default_template = '' ) {\n\t\t\t\tdo_action( 'bp_template_content' );\n\t\t\t}\n\t\t}\n\t\tif ( ! function_exists( 'buddypress' ) ) {\n\t\t\tfunction buddypress() {\n\t\t\t\tglobal $bp;\n\t\t\t\treturn $bp;\n\t\t\t}\n\t\t}\n\n\t\t// Create the mock BuddyPress class.\n\t\t$this->mock_buddypress = $this->getMockBuilder( 'BuddyPress' )\n\t\t\t->disableOriginalConstructor()\n\t\t\t->getMock();\n\n\t\t$GLOBALS['bp'] = $this->mock_buddypress;\n\n\t\t// Mock user properties.\n\t\t$this->mock_buddypress->displayed_user = new stdClass();\n\t\t$this->mock_buddypress->loggedin_user  = new stdClass();\n\t\t// Mock members and members->nav properties, so to be able to mock members->nav->get_secondary method.\n\t\t$this->mock_buddypress->members      = new stdClass();\n\t\t$this->mock_buddypress->members->nav = $this->getMockBuilder( 'BP_Core_Nav' )\n\t\t\t->disableOriginalConstructor()\n\t\t\t->setMethods( array( 'get_secondary', 'get_primary' ) )\n\t\t\t->getMock();\n\n\t\t// Mock get_primary and get_secondary method for the members->nav.\n\t\t$this->mock_buddypress->members->nav->\n\t\t\tmethod( 'get_primary' )->will(\n\t\t\t\t$this->returnCallback(\n\t\t\t\t\tfunction ( $args ) {\n\n\t\t\t\t\t\t$params = wp_parse_args( $args, array( 'primary' => true ) );\n\n\t\t\t\t\t\t// This parameter is not overridable.\n\t\t\t\t\t\tif ( empty( $params['primary'] ) ) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$bp = buddypress();\n\t\t\t\t\t\t$primary_nav =wp_list_filter( $bp->members->nav->nav[ $bp->members->nav->object_id ], $params );\n\n\t\t\t\t\t\tif ( ! $primary_nav ) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn $primary_nav;\n\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t);\n\t\t$this->mock_buddypress->members->nav->\n\t\t\tmethod( 'get_secondary' )->will(\n\t\t\t\t$this->returnCallback(\n\t\t\t\t\tfunction ( $args ) {\n\n\t\t\t\t\t\t$params = wp_parse_args( $args, array( 'parent_slug' => '' ) );\n\n\t\t\t\t\t\t// No need to search children if the parent is not set.\n\t\t\t\t\t\tif ( empty( $params['parent_slug'] ) && empty( $params['secondary'] ) ) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t$bp = buddypress();\n\t\t\t\t\t\t$secondary_nav = wp_list_filter( $bp->members->nav->nav[ $bp->members->nav->object_id ], $params );\n\n\t\t\t\t\t\tif ( ! $secondary_nav ) {\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn $secondary_nav;\n\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t);\n\n\t\t$this->mock_buddypress->members->nav->nav = array();\n\t\t$this->mock_buddypress->members->nav->object_id = 0;\n\n\t\t// Enable the integration.\n\t\tupdate_option( 'llms_integration_buddypress_enabled', 'yes' );\n\n\t\t// Refresh cached available integrations list.\n\t\tllms()->integrations()->get_available_integrations();\n\n\t}\n\n\n\t/**\n\t * Utility to set the logged in user domain.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _set_loggedin_user_domain() {\n\t\tadd_filter( 'bp_loggedin_user_domain', array( $this, 'loggedin_user_domain_filter_cb' ) );\n\t}\n\n\t/**\n\t * Utility to clear the logged in user domain.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _clear_loggedin_user_domain() {\n\t\tremove_filter( 'bp_loggedin_user_domain', array( $this, 'loggedin_user_domain_filter_cb' ) );\n\t}\n\n\t/**\n\t * Call back that sets the logged in user domain.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return string\n\t */\n\tpublic function loggedin_user_domain_filter_cb() {\n\t\treturn home_url() . '/members/' . $this->loggedin_user_slug . '/';\n\t}\n\n\t/**\n\t * Utility to set the displayed user id.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _set_displayed_user_id() {\n\t\tadd_filter( 'bp_displayed_user_id', array( $this, 'displayed_user_id_filter_cb' ) );\n\t}\n\n\t/**\n\t * Utility to clear the displayed user id.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _clear_displayed_user_id() {\n\t\tremove_filter( 'bp_displayed_user_id', array( $this, 'displayed_user_id_filter_cb' ) );\n\t}\n\n\t/**\n\t * Call back that sets the displayed user id.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return string\n\t */\n\tpublic function displayed_user_id_filter_cb() {\n\t\treturn $this->displayed_user_id;\n\t}\n\n\n\t/**\n\t * Utility to set the displayed user domain.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _set_displayed_user_domain() {\n\t\tadd_filter( 'bp_displayed_user_domain', array( $this, 'displayed_user_domain_filter_cb' ) );\n\t}\n\n\t/**\n\t * Utility to clear the displayed user domain.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _clear_displayed_user_domain() {\n\t\tremove_filter( 'bp_displayed_user_domain', array( $this, 'displayed_user_domain_filter_cb' ) );\n\t}\n\n\t/**\n\t * Call back that sets the displayed user domain.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return string\n\t */\n\tprivate function displayed_user_domain_filter_cb() {\n\t\treturn home_url() . '/members/' . $this->displayed_user_slug . '/';\n\t}\n\n\t/**\n\t * Utility to simulate we're on the BuddyPress my profile.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _set_bp_is_my_profile_true() {\n\t\tadd_filter( 'bp_is_my_profile', '__return_true' );\n\t}\n\n\t/**\n\t * Utility to simulate we're NOT on the BuddyPress my profile.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _set_bp_is_my_profile_false() {\n\t\tremove_filter( 'bp_is_my_profile', '__return_false' );\n\t}\n\n\t/**\n\t * Utility to clear simulations.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tprivate function _clear_bp_is_my_profile() {\n\t\tremove_filter( 'bp_is_my_profile', '__return_true' );\n\t\tremove_filter( 'bp_is_my_profile', '__return_false' );\n\t}\n\n\t/**\n\t * Utility to setup members nav.\n\t *\n\t * This is done in the BP_Core_Nav constructor.\n\t *\n\t * @since 6.3.0\n\t * @since 6.8.0 Setup nav also for not logged in users.\n\t *\n\t * @return void\n\t */\n\tprivate function _setup_members_nav() {\n\n\t\t$displayed_user_id = (int) bp_displayed_user_id();\n\n\t\t$bp = buddypress();\n\n\t\t$bp->members->nav->object_id = $displayed_user_id;\n\t\t$bp->members->nav->nav[ $displayed_user_id ] = array();\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-event.php",
    "content": "<?php\n/**\n * Test INsturctor model\n *\n * @package  LifterLMS_Tests/Models\n *\n * @group LLMS_Event\n * @group events\n *\n * @since 3.36.0\n * @since 4.3.0 Add assertions to test against hooks and deprecated hooks.\n * @since 5.3.3 Removed empty `setUp()` method.\n */\nclass LLMS_Test_Event extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 3.36.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_events\" );\n\t}\n\n\t/**\n\t * Test CRUD.\n\t *\n\t * @since 3.36.0\n\t * @since 4.3.0 Add update & deletion & added assertions against expected hooks.\n\t *\n\t * @return void\n\t */\n\tpublic function test_crud() {\n\n\t\t$actions = did_action( 'llms_event_created' );\n\n\t\t$expected_time = current_time( 'timestamp' ) - DAY_IN_SECONDS;\n\t\tllms_tests_mock_current_time( $expected_time );\n\n\t\t$args = array(\n\t\t\t'actor_id' => 1,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'load',\n\t\t);\n\n\t\t// Create.\n\t\t$event = new LLMS_Event();\n\t\t$event->setUp( $args );\n\t\t$this->assertTrue( $event->save() );\n\t\t$id = $event->get( 'id' );\n\t\t$this->assertTrue( is_numeric( $id ) );\n\n\t\tllms_tests_reset_current_time();\n\n\t\t$event = new LLMS_Event( $id );\n\n\t\t$this->assertEquals( $expected_time, strtotime( $event->get( 'date' ) ) );\n\t\tforeach( $args as $key => $expected ) {\n\t\t\t$this->assertEquals( $expected, $event->get( $key ) );\n\t\t}\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_event_created' ) );\n\n\t\t// Update.\n\t\t$actions = did_action( 'llms_event_updated' );\n\t\t$event->set( 'actor_id', 2, true );\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_event_updated' ) );\n\n\t\t$event = new LLMS_Event( $id );\n\t\t$this->assertEquals( 2, $event->get( 'actor_id' ) );\n\n\t\t// Delete.\n\t\t$actions = did_action( 'llms_event_deleted' );\n\t\t$this->assertTrue( $event->delete() );\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_event_deleted' ) );\n\n\t}\n\n\t/**\n\t * Test metadata getters, setters, unsetters.\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_meta() {\n\n\t\t$args = array(\n\t\t\t'actor_id' => 1,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'load',\n\t\t);\n\n\t\t$meta = array(\n\t\t\t'meta_key' => 'meta_val',\n\t\t\t'another' => 1,\n\t\t);\n\n\t\t$event = new LLMS_Event();\n\n\t\t// Set multiple metas.\n\t\t$event->setUp( $args )->set_metas( $meta );\n\n\t\t// Get all metas.\n\t\t$this->assertEquals( $meta, $event->get_meta() );\n\n\t\t// Get individual metas.\n\t\tforeach ( $meta as $key => $expect ) {\n\n\t\t\t$this->assertEquals( $expect, $event->get_meta( $key ) );\n\n\t\t}\n\n\t\t// Update a single meta value.\n\t\t$event->set_meta( 'meta_key', 'new_val' );\n\t\t$this->assertEquals( 'new_val', $event->get_meta( 'meta_key' ) );\n\n\t\t// Create a new meta item.\n\t\t$event->set_meta( 'new_key', true );\n\t\t$this->assertTrue( $event->get_meta( 'new_key' ) );\n\n\t\t// Delete a single meta item.\n\t\t$event->delete_meta( 'new_key' );\n\t\t$this->assertNull( $event->get_meta( 'new_key' ) );\n\n\t\t// Delete all meta items.\n\t\t$event->delete_meta();\n\t\t$this->assertEquals( array(), $event->get_meta() );\n\n\t}\n\n\t/**\n\t * Test meta getters/setters when the data is saved (ensure db serialization is working properly).\n\t *\n\t * @since 3.36.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_meta_store() {\n\n\t\t$args = array(\n\t\t\t'actor_id' => 1,\n\t\t\t'object_type' => 'post',\n\t\t\t'object_id' => 1,\n\t\t\t'event_type' => 'page',\n\t\t\t'event_action' => 'load',\n\t\t);\n\n\t\t$meta = array(\n\t\t\t'meta_key' => 'meta_val',\n\t\t\t'another' => 1,\n\t\t);\n\n\t\t$event = new LLMS_Event();\n\t\t$event->setUp( $args )->save();\n\n\t\t$event->set_metas( $meta, true );\n\n\t\t$event = new LLMS_Event( $event->get( 'id' ), true );\n\t\t$this->assertEquals( wp_json_encode( $meta ), $event->get( 'meta' ) );\n\t\t$this->assertEquals( $meta, $event->get_meta() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-instructor.php",
    "content": "<?php\n/**\n * Test INsturctor model\n *\n * @package LifterLMS_Tests/Models\n *\n * @group instructor\n *\n * @since 3.34.0\n */\nclass LLMS_Test_Instructor extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test has_student()\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_student() {\n\n\t\t$instructor = $this->factory->instructor->create_and_get();\n\t\t$student    = $this->factory->student->create_and_get();\n\n\t\t$this->assertFalse( $instructor->has_student( $student ) );\n\n\t\t$course_1 = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\t\t$course_1->instructors()->set_instructors( array( array( 'id' => $instructor->get( 'id' ) ) ) );\n\n\t\t$course_2 = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\t\t$course_2->instructors()->set_instructors( array( array( 'id' => $instructor->get( 'id' ) ) ) );\n\n\t\t$this->assertFalse( $instructor->has_student( 'fake' ) );\n\t\t$this->assertFalse( $instructor->has_student( $student ) );\n\t\t$this->assertFalse( $instructor->has_student( $student->get( 'id' ) ) );\n\t\t$this->assertFalse( $instructor->has_student( llms_get_student( $student ) ) );\n\n\t\t$student->enroll( $course_2->get( 'id' ) );\n\n\t\t$this->assertTrue( $instructor->has_student( $student ) );\n\n\t\t$student->enroll( $course_1->get( 'id' ) );\n\n\t\t$this->assertTrue( $instructor->has_student( $student ) );\n\n\t\t$student->unenroll( $course_1->get( 'id' ) );\n\t\t$student->unenroll( $course_2->get( 'id' ) );\n\n\t\t$this->assertFalse( $instructor->has_student( $student ) );\n\n\t}\n\n\t/**\n\t * Test get_students().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_students() {\n\n\t\t$this->markTestSkipped( 'This tests fails randomly.' );\n\n\t\t$instructor = $this->factory->instructor->create_and_get();\n\t\t$student_1  = $this->factory->student->create_and_get();\n\t\t$student_2  = $this->factory->student->create_and_get();\n\t\t$student_3  = $this->factory->student->create_and_get();\n\n\t\t$course_1 = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\t\t$course_1->instructors()->set_instructors( array( array( 'id' => $instructor->get( 'id' ) ) ) );\n\n\t\t$course_2 = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\t\t$course_2->instructors()->set_instructors( array( array( 'id' => $instructor->get( 'id' ) ) ) );\n\n\t\t// Instructor doesn't have access to this.\n\t\t$course_3 = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\n\t\t$student_1->enroll( $course_1->get( 'id' ) );\n\t\t$student_2->enroll( $course_2->get( 'id' ) );\n\t\t$student_3->enroll( $course_3->get( 'id' ) );\n\n\t\t// All students.\n\t\t$query = $instructor->get_students();\n\t\t$this->assertEquals( array( $student_1, $student_2 ), $query->get_students() );\n\n\t\t// Course 1 only.\n\t\t$query = $instructor->get_students( array( 'post_id' => $course_1->get( 'id' ) ) );\n\t\t$this->assertEquals( array( $student_1 ), $query->get_students() );\n\n\t\t// Course 2 only.\n\t\t$query = $instructor->get_students( array( 'post_id' => $course_2->get( 'id' ) ) );\n\t\t$this->assertEquals( array( $student_2 ), $query->get_students() );\n\n\t\t// Course 3 (no results).\n\t\t$query = $instructor->get_students( array( 'post_id' => $course_3->get( 'id' ) ) );\n\t\t$this->assertEquals( array(), $query->get_students() );\n\n\t\t// Mix courses the instructor has and doesn't have, only returns results from course 2.\n\t\t$query = $instructor->get_students( array( 'post_id' => array( $course_2->get( 'id' ), $course_3->get( 'id' ) ) ) );\n\t\t$this->assertEquals( array( $student_2 ), $query->get_students() );\n\n\t}\n\n\t/**\n\t * Test get_students() for an instructor with no courses available to access\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_students_no_post_access() {\n\n\t\t$instructor = $this->factory->instructor->create_and_get();\n\t\t$student    = $this->factory->student->create_and_get();\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t$tests = array(\n\t\t\t// Query all instructor's posts (which are none).\n\t\t\tarray(),\n\t\t\t// Query a post the instructor doesn't own.\n\t\t\tarray(\n\t\t\t\t'post_id' => $course->get( 'id' ),\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $args ) {\n\n\t\t\t$query = $instructor->get_students( $args );\n\t\t\t$this->assertEquals( array(), $query->get_results() );\n\n\t\t\t$this->assertEquals( 0, $query->get_found_results() );\n\t\t\t$this->assertEquals( 0, $query->get_max_pages() );\n\t\t\t$this->assertEquals( 0, $query->get_number_results() );\n\n\t\t}\n\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-access-plan.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Coupon Model\n *\n * @package  LifterLMS_Tests/Models\n *\n * @group access_plan\n *\n * @since 3.23.0\n */\nclass LLMS_Test_LLMS_Access_Plan extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Class name for the model being tested by the class\n\t *\n\t * @var string\n\t */\n\tprotected $class_name = 'LLMS_Access_Plan';\n\n\t/**\n\t * DB post type of the model being tested\n\t *\n\t * @var string\n\t */\n\tprotected $post_type = 'llms_access_plan';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t *\n\t * This should match, exactly, the object's $properties array\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\t\t\t'access_expiration' => 'string',\n\t\t\t'access_expires' => 'string',\n\t\t\t'access_length' => 'absint',\n\t\t\t'access_period' => 'string',\n\t\t\t'availability' => 'string',\n\t\t\t'availability_restrictions' => 'array',\n\t\t\t'enroll_text' => 'string',\n\t\t\t'frequency' => 'absint',\n\t\t\t'is_free' => 'yesno',\n\t\t\t'length' => 'absint',\n\t\t\t'menu_order' => 'absint',\n\t\t\t'on_sale' => 'yesno',\n\t\t\t'period' => 'string',\n\t\t\t'price' => 'float',\n\t\t\t'product_id' => 'absint',\n\t\t\t'sale_end' => 'string',\n\t\t\t'sale_start' => 'string',\n\t\t\t'sale_price' => 'float',\n\t\t\t'sku' => 'string',\n\t\t\t'title' => 'string',\n\t\t\t'trial_length' => 'absint',\n\t\t\t'trial_offer' => 'yesno',\n\t\t\t'trial_period' => 'string',\n\t\t\t'trial_price' => 'float',\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'access_expiration' => 'lifetime',\n\t\t\t'access_expires' => '01/01/2018',\n\t\t\t'access_length' => 2,\n\t\t\t'access_period' => 'year',\n\t\t\t'availability' => 'open', // members\n\t\t\t'availability_restrictions' => array(),\n\t\t\t'enroll_text' => 'Enroll Now',\n\t\t\t'frequency' => 0,\n\t\t\t'is_free' => 'no',\n\t\t\t'length' => 0,\n\t\t\t'menu_order' => 0,\n\t\t\t'on_sale' => 'no',\n\t\t\t'period' => 'year',\n\t\t\t'price' => 25.99,\n\t\t\t'product_id' => $this->factory->post->create( array(\n\t\t\t\t'post_type' => 'course',\n\t\t\t) ),\n\t\t\t'sale_end' => '2018-02-03',\n\t\t\t'sale_start' => '2018-01-15',\n\t\t\t'sale_price' => 5.99,\n\t\t\t'sku' => 'testsku',\n\t\t\t'title' => 'Access Plan Title',\n\t\t\t'trial_length' => 1,\n\t\t\t'trial_offer' => 'no',\n\t\t\t'trial_period' => 'week',\n\t\t\t'trial_price' => 1.99,\n\t\t);\n\t}\n\n\t/**\n\t * Setup plan product association.\n\t *\n\t * @since 3.23.0\n\t *\n\t * @param string $type Associated post type.\n\t *\n\t * @return void\n\t */\n\tprotected function set_obj_product( $type = 'course' ) {\n\t\t$this->obj->set( 'product_id', $this->factory->post->create( array(\n\t\t\t'post_type' => $type,\n\t\t) ) );\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 3.23.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->create();\n\t}\n\n\t/**\n\t * Test can_expire()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_expire() {\n\n\t\t$opts = array(\n\t\t\t'' => true, // @todo empty values should return false\n\t\t\t'fake' => true, // @todo fake values should return false\n\t\t\t'lifetime' => false,\n\t\t\t'limited-period' => true,\n\t\t\t'limited-date' => true,\n\t\t);\n\n\t\tforeach ( $opts as $val => $expect ) {\n\t\t\t$this->obj->set( 'access_expiration', $val );\n\t\t\t$this->assertEquals( $expect, $this->obj->can_expire() );\n\t\t}\n\n\t}\n\n\t/**\n\t * Override to prevent output of skipped test since the test doesn't matter for this class.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_edit_date() {\n\t\t$this->assertTrue( true );\n\t}\n\n\t/**\n\t * Test get_access_period_name()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_access_period_name() {\n\n\t\t//  Use values from the plan.\n\t\t$this->obj->set( 'access_period', 'week' );\n\t\t$this->obj->set( 'access_length', 2 );\n\t\t$this->assertEquals( 'weeks', $this->obj->get_access_period_name() );\n\n\t\t// Pass in values.\n\t\t$this->assertEquals( 'day', $this->obj->get_access_period_name( 'day', 1 ) );\n\t\t$this->assertEquals( 'month', $this->obj->get_access_period_name( 'month', 1 ) );\n\t\t$this->assertEquals( 'years', $this->obj->get_access_period_name( 'years', 25 ) );\n\n\t}\n\n\t/**\n\t * Test get_checkout_url()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_checkout_url() {\n\n\t\t$this->set_obj_product();\n\t\tLLMS_Install::create_pages();\n\n\t\t// no restrictions\n\t\t$url = add_query_arg( 'plan', $this->obj->get( 'id' ), get_permalink( get_option( 'lifterlms_checkout_page_id' ) ) );\n\t\t$this->assertEquals( $url, $this->obj->get_checkout_url() );\n\n\t\t// 1 restriction returns link to that membership\n\t\t$membership_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t) );\n\t\t$this->obj->set( 'availability', 'members' );\n\t\t$this->obj->set( 'availability_restrictions', array( $membership_id ) );\n\t\t$this->assertEquals( get_permalink( $membership_id ), $this->obj->get_checkout_url() );\n\n\t\t// multiple returns the hash for popover display\n\t\t$this->obj->set( 'availability_restrictions', array( $membership_id, 1234 ) );\n\t\t$this->assertEquals( '#llms-plan-locked', $this->obj->get_checkout_url() );\n\n\t\t// bypass availability checks\n\t\t$this->assertEquals( $url, $this->obj->get_checkout_url( false ) );\n\n\t}\n\n\t/**\n\t * Test get_checkout_url() with redirection.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_checkout_url_with_redirection() {\n\n\t\t$this->set_obj_product();\n\t\tLLMS_Install::create_pages();\n\n\t\t// No restrictions.\n\t\t$url = add_query_arg( 'plan', $this->obj->get( 'id' ), get_permalink( get_option( 'lifterlms_checkout_page_id' ) ) );\n\t\t$this->assertEquals( $url, $this->obj->get_checkout_url() );\n\n\t\t// Add redirect.\n\t\t$this->obj->set( 'checkout_redirect_type', 'url' );\n\t\t$this->obj->set( 'checkout_redirect_url', 'https://example.com' );\n\t\t// No redirect query arg added to the checkout url, the redirect will be added to the checkout form as hidden input field.\n\t\t$this->assertEquals( $url, $this->obj->get_checkout_url() );\n\n\t\t// 1 restriction returns link to that membership.\n\t\t$membership_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t) );\n\t\t$this->obj->set( 'availability', 'members' );\n\t\t$this->obj->set( 'availability_restrictions', array( $membership_id ) );\n\t\t// No redirect query arg added to the checkout url, the redirect will be added to the checkout form as hidden input field.\n\t\t$this->assertEquals( get_permalink( $membership_id ), $this->obj->get_checkout_url() );\n\n\t\t// Force the redirect via INPUT_GET\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'redirect' => 'https://example-redirect-get.com',\n\t\t\t)\n\t\t);\n\t\t// Expect the redirect URL via INPUT_GET to be added to the membership's permalink.\n\t\t$this->assertEquals(\n\t\t\tadd_query_arg(\n\t\t\t\t'redirect',\n\t\t\t\turlencode( 'https://example-redirect-get.com' ),\n\t\t\t\tget_permalink( $membership_id )\n\t\t\t),\n\t\t\t$this->obj->get_checkout_url()\n\t\t);\n\n\t\t// Enable the option that forces the access plan redirection settings to take over the membership redirections.\n\t\t$this->obj->set( 'checkout_redirect_forced', 'yes' );\n\t\t// The INPUT_GET will win.\n\t\t// Expect the redirect URL to be added to the membership's permalink.\n\t\t$this->assertEquals(\n\t\t\tadd_query_arg(\n\t\t\t\t'redirect',\n\t\t\t\turlencode( 'https://example-redirect-get.com' ),\n\t\t\t\tget_permalink( $membership_id )\n\t\t\t),\n\t\t\t$this->obj->get_checkout_url()\n\t\t);\n\n\t\t// Reset the INPUT_GET\n\t\t$this->mockGetRequest( array() );\n\t\t// Expect the redirect URL to be added to the membership's permalink.\n\t\t$this->assertEquals(\n\t\t\tadd_query_arg(\n\t\t\t\t'redirect',\n\t\t\t\turlencode( $this->obj->get( 'checkout_redirect_url' ) ),\n\t\t\t\tget_permalink( $membership_id )\n\t\t\t),\n\t\t\t$this->obj->get_checkout_url()\n\t\t);\n\n\t\t// Multiple returns the hash for popover display.\n\t\t$this->obj->set( 'availability_restrictions', array( $membership_id, 1234 ) );\n\t\t$this->assertEquals( '#llms-plan-locked', $this->obj->get_checkout_url() );\n\n\t\t// Bypass availability checks.\n\t\t$this->assertEquals( $url, $this->obj->get_checkout_url( false ) );\n\n\t}\n\n\t/**\n\t * Test get_free_pricing_text()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_free_pricing_text() {\n\n\t\t$text = '<span class=\"lifterlms-price\">FREE</span>';\n\t\t$this->assertEquals( $text, $this->obj->get_free_pricing_text() );\n\t\t$this->assertEquals( $text, $this->obj->get_free_pricing_text( 'html' ) );\n\t\t$this->assertEquals( 0.00, $this->obj->get_free_pricing_text( 'float' ) );\n\n\t}\n\n\t/**\n\t * Test the get_initial_price() method.\n\t *\n\t * @since 3.30.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_initial_price() {\n\n\t\t// trial w/ no price\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price() );\n\n\t\t// free trial.\n\t\t$this->obj->set( 'trial_price', 0 );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price() );\n\n\t\t// paid trial.\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->assertSame( 1.00, $this->obj->get_initial_price() );\n\n\n\t\t// disable the trial.\n\t\t$this->obj->set( 'trial_offer', 'no' );\n\n\n\t\t// No sale price set.\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price() );\n\n\t\t// on sale for free.\n\t\t$this->obj->set( 'sale_price', 0 );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price() );\n\n\t\t// paid sale.\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->assertSame( 1.00, $this->obj->get_initial_price() );\n\n\n\t\t// disable the sale.\n\t\t$this->obj->set( 'on_sale', 'no' );\n\n\n\t\t// free.\n\t\t$this->obj->set( 'price', 0 );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price() );\n\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->assertSame( 2.00, $this->obj->get_initial_price() );\n\n\t}\n\n\t/**\n\t * Test the get_initial_price() method when using coupons.\n\t *\n\t * @since 3.30.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_initial_price_with_coupon() {\n\n\t\t$coupon_id = $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) );\n\t\t$coupon = llms_get_post( $coupon_id );\n\t\t$coupon->set( 'coupon_amount', 100 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\t\t$coupon->set( 'enable_trial_discount', 'yes' );\n\t\t$coupon->set( 'trial_amount', 100 );\n\n\n\t\t// Trial 100% discount.\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price( array(), $coupon_id ) );\n\n\t\t// Trial 50% discount.\n\t\t$coupon->set( 'trial_amount', 50 );\n\t\t$this->assertSame( 0.50, $this->obj->get_initial_price( array(), $coupon_id ) );\n\n\t\t// No trial offer.\n\t\t$this->obj->set( 'trial_offer', 'no' );\n\n\t\t// Free with coupon.\n\t\t$this->obj->set( 'price', 10 );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price( array(), $coupon_id ) );\n\n\t\t// 50% off coupon.\n\t\t$coupon->set( 'coupon_amount', 50 );\n\t\t$this->assertSame( 5.00, $this->obj->get_initial_price( array(), $coupon_id ) );\n\n\t\t// free with coupon.\n\t\t$this->obj->set( 'is_free', 'yes' );\n\t\t$this->assertSame( 0.00, $this->obj->get_initial_price( array(), $coupon_id) );\n\n\t}\n\n\t/**\n\t * Test get_price()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_price() {\n\n\t\t$prices = array(\n\t\t\t'price',\n\t\t\t'trial_price',\n\t\t\t'sale_price',\n\t\t);\n\n\t\tforeach ( $prices as $key ) {\n\n\t\t\t$this->obj->set( $key, 1.00 );\n\t\t\t$this->assertEquals( llms_price( 1.00 ), $this->obj->get_price( $key ) );\n\t\t\t$this->assertEquals( 1.00, $this->obj->get_price( $key, array(), 'float' ) );\n\n\t\t\t$this->obj->set( $key, 0.00 );\n\t\t\t$this->assertEquals( $this->obj->get_free_pricing_text(), $this->obj->get_price( $key ) );\n\t\t\t$this->assertEquals( 0.00, $this->obj->get_price( $key, array(), 'float' ) );\n\n\t\t}\n\n\t}\n\n\t// public function test_get_price_with_coupon() {}\n\n\t/**\n\t * Test get_product()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_product() {\n\n\t\t$this->set_obj_product();\n\t\t$this->assertTrue( is_a( $this->obj->get_product(), 'LLMS_Product' ) );\n\n\t}\n\n\t/**\n\t * Test get_product_type()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_product_type() {\n\n\t\t$this->set_obj_product();\n\t\t$this->assertEquals( 'course', $this->obj->get_product_type() );\n\n\t\t$this->set_obj_product( 'llms_membership' );\n\t\t$this->assertEquals( 'membership', $this->obj->get_product_type() );\n\n\t}\n\n\t/**\n\t * Test get_enroll_text()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_enroll_text() {\n\n\t\t// course\n\t\t$this->set_obj_product();\n\t\t$this->assertEquals( 'Enroll', $this->obj->get_enroll_text() );\n\n\t\t// membership\n\t\t$this->set_obj_product( 'llms_membership' );\n\t\t$this->assertEquals( 'Join', $this->obj->get_enroll_text() );\n\n\t\t// custom\n\t\t$this->obj->set( 'enroll_text', 'DO SOMETHING!' );\n\t\t$this->assertEquals( 'DO SOMETHING!', $this->obj->get_enroll_text() );\n\n\t}\n\n\t/**\n\t * Test get_expiration_details()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_expiration_details() {\n\n\t\t$this->assertEquals( '', $this->obj->get_expiration_details() );\n\n\t\t$this->obj->set( 'access_expiration', 'limited-date' );\n\t\t$this->assertTrue( 0 === strpos( $this->obj->get_expiration_details(), 'access until' ) );\n\n\t\t$this->obj->set( 'access_expiration', 'limited-period' );\n\t\t$this->assertTrue( false !== strpos( $this->obj->get_expiration_details(), 'of access' ) );\n\n\t}\n\n\t/**\n\t * Test get_schedule_details()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_schedule_details() {\n\n\t\t$this->assertEquals( '', $this->obj->get_schedule_details() );\n\n \t\t$this->obj->set( 'period', 'week' );\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'length', 0 );\n\n\t\t$this->assertEquals( 'per week', $this->obj->get_schedule_details() );\n\t\t$this->assertTrue( false === strpos( $this->obj->get_schedule_details(), 'total payments' ) );\n\n\t\t$this->obj->set( 'frequency', 2 );\n\t\t$this->assertEquals( 'every 2 weeks', $this->obj->get_schedule_details() );\n\n\n\t\t$this->obj->set( 'length', 3 );\n\t\t$this->assertEquals( 'every 2 weeks for 3 total payments', $this->obj->get_schedule_details() );\n\n\t}\n\n\t/**\n\t * Test get_trial_details()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_trial_details() {\n\n\t\t$this->assertEquals( '', $this->obj->get_trial_details() );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\t\t$this->obj->set( 'trial_length', 1 );\n\t\t$this->obj->set( 'trial_period', 'year' );\n\n\t\t$this->assertEquals( 'for 1 year', $this->obj->get_trial_details() );\n\n\t}\n\n\t/**\n\t * Test visibility getters / setters\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_visibility() {\n\n\t\t$this->assertEquals( 'visible', $this->obj->get_visibility() );\n\n\t\t$opts = array(\n\t\t\t'visible' => array(\n\t\t\t\t'is_featured' => false,\n\t\t\t\t'is_visible' => true,\n\t\t\t),\n\t\t\t'hidden'  => array(\n\t\t\t\t'is_featured' => false,\n\t\t\t\t'is_visible' => false,\n\t\t\t),\n\t\t\t'featured' => array(\n\t\t\t\t'is_featured' => true,\n\t\t\t\t'is_visible' => true,\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $opts as $opt => $tests ) {\n\t\t\t$this->obj->set_visibility( $opt );\n\t\t\t$this->assertEquals( $opt, $this->obj->get_visibility() );\n\t\t\tforeach ( $tests as $func => $expect ) {\n\t\t\t\t$this->assertEquals( $expect, call_user_func( array( $this->obj, $func ) ) );\n\t\t\t}\n\t\t}\n\n\t}\n\n\t/**\n\t * Test has_availability_restrictions()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_availability_restrictions() {\n\n\t\t$this->set_obj_product( 'llms_membership' );\n\t\t$this->assertFalse( $this->obj->has_availability_restrictions() );\n\n\t\t$this->set_obj_product();\n\t\t$this->assertFalse( $this->obj->has_availability_restrictions() );\n\n\t\t$this->obj->set( 'availability', 'members' );\n\t\t$this->assertFalse( $this->obj->has_availability_restrictions() );\n\n\t\t$this->obj->set( 'availability_restrictions', array( 12345 ) );\n\t\t$this->assertTrue( $this->obj->has_availability_restrictions() );\n\n\t}\n\n\t/**\n\t * Test has_free_checkout()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_free_checkout() {\n\n\t\t$this->assertFalse( $this->obj->has_free_checkout() );\n\n\t\t$this->obj->set( 'is_free', 'no' );\n\t\t$this->assertFalse( $this->obj->has_free_checkout() );\n\n\t\t$this->obj->set( 'is_free', 'fake' );\n\t\t$this->assertFalse( $this->obj->has_free_checkout() );\n\n\t\t$this->obj->set( 'is_free', '' );\n\t\t$this->assertFalse( $this->obj->has_free_checkout() );\n\n\t\t$this->obj->set( 'is_free', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_free_checkout() );\n\n\t}\n\n\t/**\n\t * Test has_trial()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_trial() {\n\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'frequency', 0 );\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'trial_offer', 'no' );\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_trial() );\n\n\t}\n\n\t/**\n\t * Test is_available_to_user()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_available_to_user() {\n\n\t\t$this->set_obj_product();\n\t\t$this->assertTrue( $this->obj->is_available_to_user() );\n\n\t\t$mid = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) );\n\n\t\t$this->obj->set( 'availability', 'members' );\n\t\t$this->obj->set( 'availability_restrictions', array( $mid ) );\n\n\t\t$this->assertFalse( $this->obj->is_available_to_user() );\n\n\t\t// enroll the student\n\t\t$uid = $this->factory->user->create();\n\t\tllms_enroll_student( $uid, $mid );\n\t\t$this->assertTrue( $this->obj->is_available_to_user( $uid ) );\n\n\t}\n\n\t/**\n\t * Test is_free()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_free() {\n\n\t\t$this->assertFalse( $this->obj->is_free() );\n\n\t\t$this->obj->set( 'is_free', 'no' );\n\t\t$this->assertFalse( $this->obj->is_free() );\n\n\t\t$this->obj->set( 'is_free', 'fake' );\n\t\t$this->assertFalse( $this->obj->is_free() );\n\n\t\t$this->obj->set( 'is_free', '' );\n\t\t$this->assertFalse( $this->obj->is_free() );\n\n\t\t$this->obj->set( 'is_free', 'yes' );\n\t\t$this->assertTrue( $this->obj->is_free() );\n\n\t}\n\n\t/**\n\t * Test is_on_sale()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_on_sale() {\n\n\t\t// no vals, not on sale\n\t\t$this->assertFalse( $this->obj->is_on_sale() );\n\n\t\t$now = current_time( 'timestamp' );\n\t\t$future = date( 'Y-m-d', strtotime( '+1 year', $now ) );\n\t\t$past = date( 'Y-m-d', strtotime( '-1 year', $now ) );\n\t\t$now = date( 'Y-m-d', $now );\n\n\t\t// on sale, no dates\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->assertTrue( $this->obj->is_on_sale() );\n\n\t\t// start & end\n\t\t$this->obj->set( 'sale_start', $past );\n\t\t$this->obj->set( 'sale_end', $future );\n\t\t$this->assertTrue( $this->obj->is_on_sale() );\n\n\t\t// no start & has end\n\t\t$this->obj->set( 'sale_start', '' );\n\t\t$this->assertTrue( $this->obj->is_on_sale() );\n\n\t\t// has start & no end\n\t\t$this->obj->set( 'sale_start', $past );\n\t\t$this->obj->set( 'sale_end', '' );\n\t\t$this->assertTrue( $this->obj->is_on_sale() );\n\n\t\t// not on sale\n\t\t$this->obj->set( 'on_sale', 'no' );\n\t\t$this->assertFalse( $this->obj->is_on_sale() );\n\n\t\t// start in future\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->obj->set( 'sale_start', $future );\n\t\t$this->obj->set( 'sale_end', '' );\n\t\t$this->assertFalse( $this->obj->is_on_sale() );\n\n\t\t// end in past\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->obj->set( 'sale_start', '' );\n\t\t$this->obj->set( 'sale_end', $past );\n\t\t$this->assertFalse( $this->obj->is_on_sale() );\n\n\t\t// test on sale end at 00:00 of $future day plus 1\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->obj->set( 'sale_end', $future );\n\n\t\t// set current current time as last second of $future day\n\t\tllms_tests_mock_current_time( strtotime( $future . ' 23:59:59' ) );\n\t\t$this->assertTrue( $this->obj->is_on_sale() );\n\n\t\t// set current current time as first second of $future day plus 1\n\t\tllms_tests_mock_current_time( strtotime( '+1 day', strtotime( $future . ' 00:00:00' ) ) );\n\t\t$this->assertFalse( $this->obj->is_on_sale() );\n\n\t}\n\n\t/**\n\t * Test is_recurring()\n\t *\n\t * @since 3.23.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_recurring() {\n\n\t\t$this->assertFalse( $this->obj->is_recurring() );\n\n\t\t$this->obj->set( 'frequency', 0 );\n\t\t$this->assertFalse( $this->obj->is_recurring() );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->assertTrue( $this->obj->is_recurring() );\n\n\t\t$this->obj->set( 'frequency', 3 );\n\t\t$this->assertTrue( $this->obj->is_recurring() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): free plan\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_free() {\n\n\t\t$this->obj->set( 'is_free', 'yes' );\n\t\t$this->assertFalse( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): one-time payment\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_one_time() {\n\n\t\t$this->obj->set( 'price', 1 );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): one-time payment with a paid sale\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_one_time_sale() {\n\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): one-time payment with a free sale\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_one_time_sale_free() {\n\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 0 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertFalse( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): one-time payment with a sale and a coupon\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_one_time_sale_coupon() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 50 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): one-time payment with a sale and a coupon that discounts price to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_one_time_sale_coupon_free() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 100 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertFalse( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): one-time payment with a coupon\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_one_time_coupon() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 50 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'price', 2 );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): one-time payment with a coupon that discounts the price to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_one_time_coupon_free() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 100 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'price', 2 );\n\n\t\t$this->assertFalse( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 1 );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with sale\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_sale() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with sale reducing price to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_sale_free() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 0 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertFalse( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with sale and coupon\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_sale_coupon() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 50 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with sale and coupon reducing price to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_sale_coupon_free() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 100 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\n\t\t$this->assertFalse( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with paid trial\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_free() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 0 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial and a coupon\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_coupon() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 50 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial and a coupon discounting recurring price to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_coupon_free() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 100 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_coupon_trial_coupon_discount() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 50 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\t\t$coupon->set( 'enable_trial_discount', 'yes' );\n\t\t$coupon->set( 'trial_amount', 50 );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_coupon_free_trial_coupon_discount() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 100 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\t\t$coupon->set( 'enable_trial_discount', 'yes' );\n\t\t$coupon->set( 'trial_amount', 50 );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_coupon_trial_coupon_free() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 50 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\t\t$coupon->set( 'enable_trial_discount', 'yes' );\n\t\t$coupon->set( 'trial_amount', 100 );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial and a coupon that discounts both recurring and trial payments to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_coupon_free_trial_coupon_free() {\n\n\t\t$coupon = llms_get_post( $this->factory->post->create( array( 'post_type' => 'llms_coupon' ) ) );\n\t\t$coupon->set( 'coupon_amount', 100 );\n\t\t$coupon->set( 'discount_type', 'percent' );\n\t\t$coupon->set( 'enable_trial_discount', 'yes' );\n\t\t$coupon->set( 'trial_amount', 100 );\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertFalse( $this->obj->requires_payment( $coupon ) );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with paid trial and sale\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_sale() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 1 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with paid trial and sale reducing recurring payment to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_sale_free() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 0 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->obj->set( 'trial_price', 1 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertTrue( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test requires_payment(): recurring payment with free trial and sale reducing recurring payment to free\n\t *\n\t * @since 3.40.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_requires_payment_recurring_trial_free_sale_free() {\n\n\t\t$this->obj->set( 'frequency', 1 );\n\t\t$this->obj->set( 'price', 2 );\n\t\t$this->obj->set( 'sale_price', 0 );\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->obj->set( 'trial_price', 0 );\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\n\t\t$this->assertFalse( $this->obj->requires_payment() );\n\n\t}\n\n\t/**\n\t * Test method `get_redirection_url()` when there's no 'redirect' $_GET variable.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_redirection_url_no_input_get() {\n\n\t\t$this->set_obj_product( 'course' );\n\t\t$this->obj->set( 'checkout_redirect_type', 'url' );\n\t\t$this->obj->set( 'checkout_redirect_url', 'https://example.com' );\n\n\t\t// Expect the encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url(), urlencode( $this->obj->get( 'checkout_redirect_url' ) ) );\n\t\t// Require only querystring, expect empty string.\n\t\t$this->assertEmpty( $this->obj->get_redirection_url( true, true ) );\n\n\t\t// Expect the not-encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url( false ), $this->obj->get( 'checkout_redirect_url' ) );\n\t\t// Require only querystring, expect empty string.\n\t\t$this->assertEmpty( $this->obj->get_redirection_url( false, true ) );\n\n\n\t\t$this->obj->set( 'checkout_redirect_type', 'self' ); // Default.\n\t\t$this->obj->set( 'checkout_redirect_url', '' );\n\n\t\t// Expect empty string.\n\t\t$this->assertEmpty( $this->obj->get_redirection_url() );\n\t\t$this->assertEmpty( $this->obj->get_redirection_url( true, false ) );\n\t}\n\n\t/**\n\t * Test method `get_redirection_url()` when there's a 'redirect' $_GET variable.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_redirection_url_with_input_get() {\n\n\t\t$this->set_obj_product( 'course' );\n\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'redirect' => '',\n\t\t\t)\n\t\t);\n\t\t// Expect empty string.\n\t\t$this->assertEmpty( $this->obj->get_redirection_url() );\n\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'redirect' => 'https://example-redirect-get.com',\n\t\t\t)\n\t\t);\n\t\t// Expect the encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url(), urlencode( 'https://example-redirect-get.com' ) );\n\t\t// Require only querystring, expect the encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url( true, true ), urlencode( 'https://example-redirect-get.com' ) );\n\n\t\t// Expect the not-encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url( false ), 'https://example-redirect-get.com' );\n\t\t// Require only querystring, expect the not-encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url( false, true ), 'https://example-redirect-get.com' );\n\n\t\t// Set access plan redirect options, still the $_GET variable will win.\n\t\t$this->obj->set( 'checkout_redirect_type', 'url' );\n\t\t$this->obj->set( 'checkout_redirect_url', 'https://example.com' );\n\n\t\t// Expect the encoded url.\n\t\t$this->assertEquals( $this->obj->get_redirection_url(), urlencode( 'https://example-redirect-get.com' ) );\n\t\t// Require only querystring, expect the encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url( true, true ), urlencode( 'https://example-redirect-get.com' ) );\n\n\t\t// Expect the not-encoded url.\n\t\t$this->assertEquals( $this->obj->get_redirection_url( false ), 'https://example-redirect-get.com' );\n\t\t// Require only querystring, expect the not-encoded URL.\n\t\t$this->assertEquals( $this->obj->get_redirection_url( false, true ), 'https://example-redirect-get.com' );\n\n\t\t$this->obj->set( 'checkout_redirect_type', 'self' ); // Default.\n\t\t$this->obj->set( 'checkout_redirect_url', '' );\n\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'redirect' => '',\n\t\t\t)\n\t\t);\n\n\t\t// Expect empty string.\n\t\t$this->assertEmpty( $this->obj->get_redirection_url() );\n\t\t$this->assertEmpty( $this->obj->get_redirection_url( true, false ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-add-on.php",
    "content": "<?php\n/**\n * Test Add On model\n *\n * @package LifterLMS_Tests/Models\n *\n * @group LLMS_Add_On\n * @group add_ons\n *\n * @since 4.21.3\n */\nclass LLMS_Test_Add_On extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Retrieve a mock plugin add-on for testing\n\t *\n\t * @since 5.1.1\n\t *\n\t * @param boolean $install  If true, calls `install_mock_addon()` to physically install the mock plugin.\n\t * @param boolean $activate If true and `$install` is also true, activates the mock plugin following installation.\n\t * @return LLMS_Add_On\n\t */\n\tprivate function get_mock_addon( $install = false, $activate = false ) {\n\n\t\t$asset = 'lifterlms-mock-addon.php';\n\t\t$dir   = 'lifterlms-mock-addon/';\n\t\t$file  = $dir . $asset;\n\t\tif ( $install ) {\n\t\t\tLLMS_Unit_Test_Files::copy_asset( $asset, trailingslashit( WP_PLUGIN_DIR  ). $dir );\n\t\t\tif ( $activate ) {\n\t\t\t\tactivate_plugin( $file );\n\t\t\t}\n\t\t}\n\n\t\treturn new LLMS_Add_On( array(\n\t\t\t'title'       => 'LLMS Mock Add-on',\n\t\t\t'update_file' => $file,\n\t\t\t'id'          => 'lifterlms-com-mock-addon',\n\t\t\t'type'        => 'plugin',\n\t\t) );\n\n\t}\n\n\t/**\n\t * Retrieves the first `twenty*` theme that's installed on the site.\n\t *\n\t * We used to hardcode the theme but with WP 5.9 the core included themes list (in the test environment) includes\n\t * only twentytwenty and later. Doing it this way is safer because we don't actually care what theme were using,\n\t * we just need one that *is installed*.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tprivate function get_wp_included_default_theme() {\n\n\t\tforeach ( array_keys( wp_get_themes() ) as $slug ) {\n\n\t\t\tif ( 0 === strpos( $slug, 'twenty' ) ) {\n\t\t\t\treturn $slug;\n\t\t\t}\n\n\t\t}\n\n\t\treturn 'default';\n\n\t}\n\n\t/**\n\t * Test constructor with an addon array passed in.\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor_with_addon() {\n\n\t\t$mock = array(\n\t\t\t'id'  => 'test',\n\t\t\t'key' => 'val',\n\t\t);\n\t\t$addon = new LLMS_Add_On( $mock );\n\n\t\t$this->assertEquals( $mock, LLMS_Unit_Test_Util::get_private_property_value( $addon, 'data' ) );\n\t\t$this->assertEquals( 'test', LLMS_Unit_Test_Util::get_private_property_value( $addon, 'id' ) );\n\n\t}\n\n\t/**\n\t * Test constructor with a lookup\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor_with_lookup() {\n\n\t\t$addon = new LLMS_Add_On( 'lifterlms-com-lifterlms', 'id' );\n\n\t\t$this->assertEquals( 'lifterlms-com-lifterlms', $addon->get( 'id' ) );\n\t\t$this->assertEquals( 'lifterlms-com-lifterlms', LLMS_Unit_Test_Util::get_private_property_value( $addon, 'id' ) );\n\n\t}\n\n\tpublic function test_get() {\n\n\t\t$addon = new LLMS_Add_On( 'lifterlms-com-lifterlms', 'id' );\n\n\t\t// Non-existent prop.\n\t\t$this->assertSame( '', $addon->get( 'fake' ) );\n\n\t\t// Real prop.\n\t\t$this->assertSame( 'LifterLMS', $addon->get( 'title' ) );\n\n\t}\n\n\t/**\n\t * Test plugin activation and deactivation\n\t *\n\t * Also tests the `is_active()` and partially the `get_status()` methods.\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_activate_deactivate_plugin() {\n\n\t\t$addon    = new LLMS_Add_On( array( 'title' => 'Akismet', 'type' => 'plugin', 'update_file' => 'akismet/akismet.php' ) );\n\t\t$activate = $addon->activate();\n\t\t$this->assertEquals( $activate, 'Akismet was successfully activated.' );\n\n\t\t$this->assertTrue( $addon->is_active() );\n\t\t$this->assertEquals( 'active', $addon->get_status() );\n\t\t$this->assertEquals( 'Active', $addon->get_status( true ) );\n\n\t\t$deactivate = $addon->deactivate();\n\t\t$this->assertEquals( $deactivate, 'Akismet was successfully deactivated.' );\n\n\t\t$this->assertFalse( $addon->is_active() );\n\t\t$this->assertEquals( 'inactive', $addon->get_status() );\n\t\t$this->assertEquals( 'Inactive', $addon->get_status( true ) );\n\n\t}\n\n\t/**\n\t * Test theme activation\n\t *\n\t * Also tests the `is_active()` and partially the `get_status()` methods.\n\t *\n\t * @since 4.21.3\n\t * @since 5.10.0 Make sure the theme is installed before testing that it's inactivate.\n\t *\n\t * @return void\n\t */\n\tpublic function test_activate_theme_success() {\n\n\t\t$addon = new LLMS_Add_On( array( 'title' => 'Default Theme', 'type' => 'theme', 'update_file' => $this->get_wp_included_default_theme() ) );\n\n\t\t$this->assertFalse( $addon->is_active() );\n\t\t$this->assertEquals( 'inactive', $addon->get_status() );\n\t\t$this->assertEquals( 'Inactive', $addon->get_status( true ) );\n\n\t\t$res = $addon->activate();\n\t\t$this->assertEquals( $res, 'Default Theme was successfully activated.' );\n\n\t\t$this->assertTrue( $addon->is_active() );\n\t\t$this->assertEquals( 'active', $addon->get_status() );\n\t\t$this->assertEquals( 'Active', $addon->get_status( true ) );\n\n\t}\n\n\t/**\n\t * Test activate() error for a plugin\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_activate_error() {\n\n\t\t$addon    = new LLMS_Add_On( array( 'title' => 'fake', 'type' => 'plugin' ) );\n\t\t$activate = $addon->activate();\n\n\t\t$this->assertIsWPError( $activate);\n\t\t$this->assertWPErrorCodeEquals( 'activation', $activate );\n\n\t}\n\n\t/**\n\t * Test deactivate() error for a plugin\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_deactivate_error() {\n\n\t\t$addon      = new LLMS_Add_On( array( 'title' => 'fake' ) );\n\t\t$deactivate = $addon->deactivate();\n\t\t$this->assertIsWPError( $deactivate );\n\t\t$this->assertWPErrorCodeEquals( 'deactivation', $deactivate );\n\n\t}\n\n\t/**\n\t * Test get_channel_subscription()\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_channel_subscription() {\n\n\t\t$addon = new LLMS_Add_On();\n\t\t$this->assertEquals( 'stable', $addon->get_channel_subscription() );\n\n\t}\n\n\t/**\n\t * Test get_type()\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_type() {\n\n\t\t$tests = array(\n\t\t\t'theme'    => array( 'type' => 'theme' ),\n\t\t\t'plugin'   => array( 'type' => 'plugin' ),\n\t\t\t'fake'     => array( 'type' => 'fake' ),\n\t\t\t'bundle'   => array( 'categories' => array( 'bundles' => 'Bundles' ) ),\n\t\t\t'external' => array( 'categories' => array( 'third-party' => 'Third Party' ) ),\n\t\t\t'support'  => array( 'categories' => array() ),\n\t\t);\n\n\t\tforeach ( $tests as $expected => $data ) {\n\t\t\t$addon = new LLMS_Add_On( $data );\n\t\t\t$this->assertEquals( $expected, $addon->get_type(), $expected );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_permalink()\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_permalink() {\n\n\t\t$addon = new LLMS_Add_On( 'lifterlms-com-lifterlms', 'id' );\n\t\t$expect = 'https://lifterlms.com/product/lifterlms/?utm_source=LifterLMS%20Plugin&utm_campaign=Plugin%20to%20Sale&utm_medium=Add-Ons%20Screen&utm_content=LifterLMS%20Ad%20' . llms()->version;\n\t\t$this->assertEquals( $expect, $addon->get_permalink() );\n\n\t}\n\n\t/**\n\t * Test get_install_status() and is_installed()\n\t *\n\t * @since 4.21.3\n\t * @since 5.10.0 Make sure the theme is installed before checking it's install status.\n\t *\n\t * @return void\n\t */\n\tpublic function test_install_status() {\n\n\t\t// Invalid.\n\t\t$addon = new LLMS_Add_On();\n\t\t$this->assertEquals( 'none', $addon->get_install_status() );\n\t\t$this->assertEquals( 'N/A', $addon->get_install_status( true ) );\n\n\t\t// Plugin installed.\n\t\t$addon = new LLMS_Add_On( array( 'type' => 'plugin', 'update_file' => 'akismet/akismet.php' ) );\n\t\t$this->assertEquals( 'installed', $addon->get_install_status() );\n\t\t$this->assertEquals( 'Installed', $addon->get_install_status( true ) );\n\n\t\t// Plugin not installed.\n\t\t$addon = new LLMS_Add_On( array( 'type' => 'plugin', 'update_file' => 'mock/mock.php' ) );\n\t\t$this->assertEquals( 'uninstalled', $addon->get_install_status() );\n\t\t$this->assertEquals( 'Not Installed', $addon->get_install_status( true ) );\n\n\t\t// Theme installed.\n\t\t$addon = new LLMS_Add_On( array( 'type' => 'theme', 'update_file' => $this->get_wp_included_default_theme() ) );\n\t\t$this->assertEquals( 'installed', $addon->get_install_status() );\n\t\t$this->assertEquals( 'Installed', $addon->get_install_status( true ) );\n\n\t\t// Theme not installed.\n\t\t$addon = new LLMS_Add_On( array( 'type' => 'theme', 'update_file' => 'fake' ) );\n\t\t$this->assertEquals( 'uninstalled', $addon->get_install_status() );\n\t\t$this->assertEquals( 'Not Installed', $addon->get_install_status( true ) );\n\n\t}\n\n\t/**\n\t * Test lookup_add_on() when errors are encountered.\n\t *\n\t * @since 4.21.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_lookup_errors() {\n\n\t\t$addon = new LLMS_Add_On();\n\n\t\t// Mock the HTTP request to find addons for an error.\n\t\t$err = new WP_Error( 'mocked-err', 'Mocked Message', array( 'data' => 'mocked' ) );\n\t\t$this->mock_http_request( 'https://lifterlms.com/wp-json/llms/v3/products', $err );\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $addon, 'lookup_add_on', array( 'mock', 'mock' ) ) );\n\n\t\t// Mock the HTTP request to return an empty array for some reason..\n\t\t$ret = array( 'items' => array() );\n\t\t$this->mock_http_request( 'https://lifterlms.com/wp-json/llms/v3/products', $ret );\n\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $addon, 'lookup_add_on', array( 'mock', 'mock' ) ) );\n\n\t}\n\n\t/**\n\t * Test uninstall() for an add-on that isn't installed\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_uninstall_error_addon_not_installed() {\n\n\t\t$addon = llms_get_add_on( 'lifterlms-groups', 'slug' );\n\t\t$res   = $addon->uninstall();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'not-installed', $res );\n\n\t}\n\n\t/**\n\t * Test uninstall() error for an active add-on.\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_uninstall_error_is_activate() {\n\n\t\t$addon = $this->get_mock_addon( true, true );\n\t\t$res   = $addon->uninstall();\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'uninstall-active', $res );\n\n\t}\n\n\t/**\n\t * Test uninstall() error for an invalid add-on type.\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_uninstall_real_error_invalid_type() {\n\n\t\t$addon = new LLMS_Add_On( array( 'type' => 'fake' ) );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $addon, 'uninstall_real' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'uninstall-invalid-type', $res );\n\n\t}\n\n\t/**\n\t * Test uninstall() success for a plugin add-on\n\t *\n\t * @since 5.1.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_uninstall_plugin_real_success() {\n\n\t\t$addon = $this->get_mock_addon( true, false );\n\t\t$res   = LLMS_Unit_Test_Util::call_method( $addon, 'uninstall_real' );\n\t\t$this->assertEquals( 'LLMS Mock Add-on was successfully uninstalled.', $res );\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-coupon.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Coupon Model\n * @group    coupons\n * @since    3.4.0\n * @version  3.19.0\n */\nclass LLMS_Test_LLMS_Coupon extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * class name for the model being tested by the class\n\t * @var  string\n\t */\n\tprotected $class_name = 'LLMS_Coupon';\n\n\t/**\n\t * db post type of the model being tested\n\t * @var  string\n\t */\n\tprotected $post_type = 'llms_coupon';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t * This should match, exactly, the object's $properties array\n\t * @return   array\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\t\t\t'coupon_amount' => 'float',\n\t\t\t'coupon_courses' => 'array',\n\t\t\t'coupon_membership' => 'array',\n\t\t\t'description' => 'string',\n\t\t\t'discount_type' => 'string',\n\t\t\t'enable_trial_discount' => 'yesno',\n\t\t\t'expiration_date' => 'string',\n\t\t\t'plan_type' => 'string',\n\t\t\t'trial_amount' => 'float',\n\t\t\t'usage_limit' => 'absint',\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t * This is used by test_getters_setters\n\t * @return   array\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'coupon_amount' => 50,\n\t\t\t'coupon_courses' => array(),\n\t\t\t'coupon_membership' => array(),\n\t\t\t'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',\n\t\t\t'discount_type' => 'percent',\n\t\t\t'enable_trial_discount' => 'no',\n\t\t\t'expiration_date' => '02/17/2017',\n\t\t\t'plan_type' => 'any',\n\t\t\t'trial_amount' => 5,\n\t\t\t'usage_limit' => 25,\n\t\t);\n\t}\n\n\n\t/*\n\t\t   /$$                           /$$\n\t\t  | $$                          | $$\n\t\t /$$$$$$    /$$$$$$   /$$$$$$$ /$$$$$$   /$$$$$$$\n\t\t|_  $$_/   /$$__  $$ /$$_____/|_  $$_/  /$$_____/\n\t\t  | $$    | $$$$$$$$|  $$$$$$   | $$   |  $$$$$$\n\t\t  | $$ /$$| $$_____/ \\____  $$  | $$ /$$\\____  $$\n\t\t  |  $$$$/|  $$$$$$$ /$$$$$$$/  |  $$$$//$$$$$$$/\n\t\t   \\___/   \\_______/|_______/    \\___/ |_______/\n\t*/\n\n\t/**\n\t * Test the get expiration time function\n\t * @return   void\n\t * @since    3.19.0\n\t * @version  3.19.0\n\t */\n\tpublic function test_get_expiration_time() {\n\n\t\t$this->create();\n\n\t\t// no expiration date\n\t\t$this->obj->set( 'expiration_date', '' );\n\t\t$this->assertFalse( $this->obj->get_expiration_time() );\n\n\t\t$dates = array(\n\t\t\t'02/28/2018',\n\t\t\t'01/31/2015',\n\t\t\t'12/31/2016',\n\t\t\t'05/05/2015',\n\t\t);\n\n\t\tforeach ( $dates as $date ) {\n\t\t\t$this->obj->set( 'expiration_date', $date );\n\t\t\t$this->assertEquals( ( strtotime( $date ) + DAY_IN_SECONDS - 1 ), $this->obj->get_expiration_time() );\n\t\t}\n\n\n\t}\n\n\t/**\n\t * Test get_products() function\n\t * @return   void\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function test_get_products() {\n\t\t$this->create();\n\t\t$this->obj->set( 'coupon_courses', array( 1, 2, 3 ) );\n\t\t$this->obj->set( 'coupon_membership', array( 4, 5, 6 ) );\n\t\t$this->assertEquals( array( 1, 2, 3, 4, 5, 6 ), $this->obj->get_products() );\n\t}\n\n\t/**\n\t * test the has_main_discount() method\n\t * @return   void\n\t * @since    3.21.1\n\t * @version  3.21.1\n\t */\n\tpublic function test_has_main_discount() {\n\n\t\t$this->create();\n\n\t\t// not set\n\t\t$this->assertFalse( $this->obj->has_main_discount() );\n\n\t\t// set to various positive numbers\n\t\t$amounts = array(\n\t\t\t'1', 1, '1.00', 1.00, 200, 2934234, 234.32, 0.50, '0.99'\n\t\t);\n\t\tforeach ( $amounts as $amount ) {\n\t\t\t$this->obj->set( 'coupon_amount', $amount );\n\t\t\t$this->assertTrue( $this->obj->has_main_discount() );\n\t\t}\n\n\t\t// 0 amounts\n\t\t$amounts = array(\n\t\t\t0, false, '', '0', '0.00', null, 0.00, '.00', 'no', 'arst',\n\t\t);\n\t\tforeach ( $amounts as $amount ) {\n\t\t\t$this->obj->set( 'coupon_amount', $amount );\n\t\t\t$this->assertFalse( $this->obj->has_main_discount() );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test has_trial_discount() function\n\t * @return   void\n\t * @since    3.4.0\n\t * @version  3.4.0\n\t */\n\tpublic function test_has_trial_discount() {\n\n\t\t$this->create();\n\n\t\t// trial discount enabled\n\t\t$this->obj->set( 'enable_trial_discount', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_trial_discount() );\n\n\t\t// trial discount not enabled\n\t\t$this->obj->set( 'enable_trial_discount', 'no' );\n\t\t$this->assertFalse( $this->obj->has_trial_discount() );\n\t\t$this->obj->set( 'enable_trial_discount', '' );\n\t\t$this->assertFalse( $this->obj->has_trial_discount() );\n\t\t$this->obj->set( 'enable_trial_discount', 'string' );\n\t\t$this->assertFalse( $this->obj->has_trial_discount() );\n\n\t}\n\n\t/**\n\t * Test the is_expired() function.\n\t *\n\t * @since    3.2.2\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return   void\n\t */\n\tpublic function test_is_expired() {\n\n\t\t$this->create();\n\n\t\t// no date set so it's not expired\n\t\t$this->assertFalse( $this->obj->is_expired() );\n\n\t\t// date empty, not expired\n\t\t$this->obj->set( 'expiration_date', '' );\n\t\t$this->assertFalse( $this->obj->is_expired() );\n\n\t\t// should be expired\n\t\tllms_tests_mock_current_time( '2016-01-02' );\n\t\t$this->obj->set( 'expiration_date', '01/01/2016' );\n\t\t$this->assertTrue( $this->obj->is_expired() );\n\n\t\t// should not be expired\n\t\tllms_tests_mock_current_time( '2015-01-01' );\n\t\t$this->obj->set( 'expiration_date', '01/01/2016' );\n\t\t$this->assertFalse( $this->obj->is_expired() );\n\n\t\t// should expire end of day on expiration date\n\t\tllms_tests_mock_current_time( '2016-01-01 12:00pm' );\n\t\t$this->obj->set( 'expiration_date', '01/01/2016' );\n\t\t$this->assertFalse( $this->obj->is_expired() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-course.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Course Model\n *\n * @group    LLMS_Course\n * @group    LLMS_Post_Model\n *\n * @since 3.4.0\n * @since 3.24.0 Add tests for the `get_available_points()` method.\n * @since 4.7.0 Add tests for `to_array_extra_blocks()` and `to_array_extra_images()`.\n * @since 5.2.1 Add checks for empty URL and page ID in `test_has_sales_page_redirect()`.\n */\nclass LLMS_Test_LLMS_Course extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * class name for the model being tested by the class\n\t * @var  string\n\t */\n\tprotected $class_name = 'LLMS_Course';\n\n\t/**\n\t * db post type of the model being tested\n\t * @var  string\n\t */\n\tprotected $post_type = 'course';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t * This should match, exactly, the object's $properties array\n\t *\n\t * @since 3.4.0\n\t * @since 3.20.0 Unknown.\n\t * @since 4.12.0 Added missing values.\n\t *\n\t * @return array\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\t\t\t// Public.\n\t\t\t'audio_embed'                => 'text',\n\t\t\t'average_grade'              => 'float',\n\t\t\t'average_progress'           => 'float',\n\t\t\t'capacity'                   => 'absint',\n\t\t\t'capacity_message'           => 'text',\n\t\t\t'course_closed_message'      => 'text',\n\t\t\t'course_opens_message'       => 'text',\n\t\t\t'content_restricted_message' => 'text',\n\t\t\t'enable_capacity'            => 'yesno',\n\t\t\t'end_date'                   => 'text',\n\t\t\t'enrolled_students'          => 'absint',\n\t\t\t'enrollment_closed_message'  => 'text',\n\t\t\t'enrollment_end_date'        => 'text',\n\t\t\t'enrollment_opens_message'   => 'text',\n\t\t\t'enrollment_period'          => 'yesno',\n\t\t\t'enrollment_start_date'      => 'text',\n\t\t\t'has_prerequisite'           => 'yesno',\n\t\t\t'length'                     => 'text',\n\t\t\t'prerequisite'               => 'absint',\n\t\t\t'prerequisite_track'         => 'absint',\n\t\t\t'sales_page_content_page_id' => 'absint',\n\t\t\t'sales_page_content_type'    => 'string',\n\t\t\t'sales_page_content_url'     => 'string',\n\t\t\t'tile_featured_video'        => 'yesno',\n\t\t\t'time_period'                => 'yesno',\n\t\t\t'start_date'                 => 'text',\n\t\t\t'video_embed'                => 'text',\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t * This is used by test_getters_setters\n\t * @return   array\n\t * @since    3.4.0\n\t * @version  3.20.0\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'audio_embed' => 'http://example.tld/audio_embed',\n\t\t\t'average_grade' => 25.55,\n\t\t\t'average_progress' => 99.32,\n\t\t\t'capacity' => 25,\n\t\t\t'capacity_message' => 'Capacity Reached',\n\t\t\t'course_closed_message' => 'Course has closed',\n\t\t\t'course_opens_message' => 'Course is not yet open',\n\t\t\t'content_restricted_message' => 'You cannot access this content',\n\t\t\t'enable_capacity' => 'yes',\n\t\t\t'end_date' => '2017-05-05',\n\t\t\t'enrolled_students' => 25,\n\t\t\t'enrollment_closed_message' => 'Enrollment is closed',\n\t\t\t'enrollment_end_date' => '2017-05-05',\n\t\t\t'enrollment_opens_message' => 'Enrollment opens later',\n\t\t\t'enrollment_period' => 'yes',\n\t\t\t'enrollment_start_date' => '2017-05-01',\n\t\t\t'has_prerequisite' => 'no',\n\t\t\t'length' => '1 year',\n\t\t\t'prerequisite' => 0,\n\t\t\t'prerequisite_track' => 0,\n\t\t\t'tile_featured_video' => 'yes',\n\t\t\t'time_period' => 'yes',\n\t\t\t'sales_page_content_page_id' => 0,\n\t\t\t'sales_page_content_type' => 'none',\n\t\t\t'sales_page_content_url' => 'https://lifterlms.com',\n\t\t\t'start_date' => '2017-05-01',\n\t\t\t'video_embed' => 'http://example.tld/video_embed',\n\t\t);\n\t}\n\n\t/**\n\t * Test the get_available_points() method\n\t * @return   [type]\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_get_available_points() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 0, 0 )[0] );\n\n\t\t// default setup is 1 point per lesson\n\t\t$this->assertEquals( 10, $course->get_available_points() );\n\n\t\t// change them all up\n\t\t$points = 0;\n\t\tforeach ( $course->get_lessons() as $lesson ) {\n\t\t\t$lesson_points = rand( 0, 3 );\n\t\t\t$lesson->set( 'points', $lesson_points );\n\t\t\t$points += $lesson_points;\n\t\t}\n\t\t$this->assertEquals( $points, $course->get_available_points() );\n\n\t}\n\n\t/**\n\t * Test Audio and Video Embeds\n\t *\n\t * @since 3.4.0\n\t * @since 4.10.0 Fix faulty tests, use assertSame in favor of assertEquals.\n\t * @since 6.0.0 Mock oembed results to prevent rate limiting issues causing tests to fail.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_embeds() {\n\n\t\t$iframe = '<iframe src=\"%s\"></iframe>';\n\n\t\t$handler = function( $html, $url ) use ( $iframe ) {\n\t\t\treturn sprintf( $iframe, $url );\n\t\t};\n\n\t\tadd_filter( 'pre_oembed_result', $handler, 10, 2 );\n\n\t\t$course = new LLMS_Course( 'new', 'Course With Embeds' );\n\n\t\t$audio_url = 'http://example.tld/audio_embed';\n\t\t$video_url = 'http://example.tld/video_embed';\n\n\t\t// Empty string when none set.\n\t\t$this->assertEmpty( $course->get_audio() );\n\t\t$this->assertEmpty( $course->get_video() );\n\n\t\t$course->set( 'audio_embed', $audio_url );\n\t\t$course->set( 'video_embed', $video_url );\n\n\t\t$audio_embed = $course->get_audio();\n\t\t$video_embed = $course->get_video();\n\n\t\t// Should be an iframe for valid embeds.\n\t\t$this->assertEquals( sprintf( $iframe, $audio_url ),$audio_embed );\n\t\t$this->assertEquals( sprintf( $iframe, $video_url ),$video_embed );\n\n\t\tremove_filter( 'pre_oembed_result', $handler, 10, 2 );\n\n\t\t// Fallbacks should be a link to the URL.\n\t\t$not_embeddable_url = 'http://lifterlms.com/not/embeddable';\n\n\t\t$course->set( 'audio_embed', $not_embeddable_url );\n\t\t$course->set( 'video_embed', $not_embeddable_url );\n\t\t$audio_embed = $course->get_audio();\n\t\t$video_embed = $course->get_video();\n\n\t\t$this->assertSame( 0, strpos( $audio_embed, '<a' ) );\n\t\t$this->assertSame( 0, strpos( $video_embed, '<a' ) );\n\n\t\t$this->assertStringContains( sprintf( 'href=\"%s\"', $not_embeddable_url ), $audio_embed );\n\t\t$this->assertStringContains( sprintf( 'href=\"%s\"', $not_embeddable_url ), $video_embed );\n\n\t\t// Ensure special characters in URL works.\n\t\t$audio_url_special_char = home_url( '/a①b.mp3' );\n\t\t$video_url_special_char = home_url( '/a①b.mp4' );\n\n\t\t$course->set( 'audio_embed', $audio_url_special_char );\n\t\t$course->set( 'video_embed', $video_url_special_char );\n\n\t\t$this->assertStringContains( '<audio', $course->get_audio() );\n\t\t$this->assertStringContains( '<source', $course->get_video() );\n\t\t$this->assertStringContains( $audio_url_special_char, $course->get_audio() );\n\t\t$this->assertStringContains( $video_url_special_char, $course->get_video() );\n\n\t}\n\n\t/**\n\t * Test get percent complete from course\n\t * @return   void\n\t * @since    3.17.2\n\t * @version  3.17.2\n\t */\n\tpublic function test_get_percent_complete() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 4, 4, 0, 0 )[0] );\n\t\t$student = $this->get_mock_student();\n\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// get student by ID\n\t\t$this->assertEquals( 0, $course->get_percent_complete( $student->get( 'id' ) ) );\n\n\t\t// get from current user\n\t\t$this->assertEquals( 0, $course->get_percent_complete() );\n\n\t\t// complete some courses\n\t\t$this->complete_courses_for_student( $student->get_id(), $course->get( 'id' ), 75 );\n\n\t\t// get by id\n\t\t$this->assertEquals( 75, $course->get_percent_complete( $student->get( 'id' ) ) );\n\n\t\t// get from current user\n\t\t$this->assertEquals( 0, $course->get_percent_complete() );\n\n\t\t// log the user in\n\t\twp_set_current_user( $student->get_id() );\n\n\t\t// get from current user\n\t\t$this->assertEquals( 75, $course->get_percent_complete() );\n\n\n\t}\n\n\t/**\n\t * Test prerequisite functions related to courses\n\t * @return   void\n\t * @since    3.4.0\n\t * @version  3.7.3\n\t */\n\tpublic function test_get_prerequisites() {\n\n\t\t$course = new LLMS_Course( 'new', 'Course Name' );\n\t\t$prereq_course = new LLMS_Course( 'new', 'Course Prereq' );\n\t\t$prereq_track = wp_create_term( 'test track', 'course_track' );\n\n\t\t// no prereqs\n\t\t$this->assertFalse( $course->has_prerequisite( 'any' ) );\n\t\t$this->assertFalse( $course->has_prerequisite( 'course' ) );\n\t\t$this->assertFalse( $course->has_prerequisite( 'course_track' ) );\n\t\t$this->assertFalse( $course->get_prerequisite_id( 'course' ) );\n\t\t$this->assertFalse( $course->get_prerequisite_id( 'course_track' ) );\n\n\t\t$course->set( 'prerequisite', $prereq_course->get( 'id' ) );\n\t\t$course->set( 'prerequisite_track', $prereq_track['term_id'] );\n\n\t\t// still no prereqs\n\t\t$this->assertFalse( $course->has_prerequisite( 'any' ) );\n\t\t$this->assertFalse( $course->has_prerequisite( 'course' ) );\n\t\t$this->assertFalse( $course->has_prerequisite( 'course_track' ) );\n\t\t$this->assertFalse( $course->get_prerequisite_id( 'course' ) );\n\t\t$this->assertFalse( $course->get_prerequisite_id( 'course_track' ) );\n\n\t\t$course->set( 'has_prerequisite', 'yes' );\n\n\t\t// have prereqs\n\t\t$this->assertTrue( $course->has_prerequisite( 'any' ) );\n\t\t$this->assertTrue( $course->has_prerequisite( 'course' ) );\n\t\t$this->assertTrue( $course->has_prerequisite( 'course_track' ) );\n\t\t$this->assertEquals( $prereq_course->get( 'id' ), $course->get_prerequisite_id( 'course' ) );\n\t\t$this->assertEquals( $prereq_track['term_id'], $course->get_prerequisite_id( 'course_track' ) );\n\n\t\t$course->set( 'prerequisite', 0 );\n\n\t\t$this->assertTrue( $course->has_prerequisite( 'any' ) );\n\t\t$this->assertFalse( $course->has_prerequisite( 'course' ) );\n\t\t$this->assertTrue( $course->has_prerequisite( 'course_track' ) );\n\t\t$this->assertEquals( 0, $course->get_prerequisite_id( 'course' ) );\n\n\t\t$course->set( 'prerequisite', 'string' );\n\t\t$this->assertFalse( $course->has_prerequisite( 'course' ) );\n\t\t$this->assertEquals( 0, $course->get_prerequisite_id( 'course' ) );\n\n\t}\n\n\t/**\n\t * Test the get lessons function\n\t * @return   void\n\t * @since    3.12.0\n\t * @version  3.12.0\n\t */\n\tpublic function test_get_lessons() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 2, 2, 0, 0 )[0] );\n\n\t\t// get just ids\n\t\t$lessons = $course->get_lessons( 'ids' );\n\t\t$this->assertEquals( 4, count( $lessons ) );\n\t\tarray_map( function( $id ) {\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t}, $lessons );\n\n\t\t// wp post objects\n\t\t$lessons = $course->get_lessons( 'posts' );\n\t\t$this->assertEquals( 4, count( $lessons ) );\n\t\tarray_map( function( $post ) {\n\t\t\t$this->assertTrue( is_a( $post, 'WP_Post' ) );\n\t\t}, $lessons );\n\n\t\t// lesson objects\n\t\t$lessons = $course->get_lessons( 'lessons' );\n\t\t$this->assertEquals( 4, count( $lessons ) );\n\t\tarray_map( function( $lesson ) {\n\t\t\t$this->assertTrue( is_a( $lesson, 'LLMS_Lesson' ) );\n\t\t}, $lessons );\n\n\t}\n\n\t/**\n\t * Test the get quizzes function\n\t * @return   void\n\t * @since    3.12.0\n\t * @version  3.12.0\n\t */\n\tpublic function test_get_quizzes() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 5, 3, 1 )[0] );\n\n\t\t$quizzes = $course->get_quizzes();\n\t\t$this->assertEquals( 3, count( $quizzes ) );\n\t\tarray_map( function( $id ) {\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t}, $quizzes );\n\n\t}\n\n\t/**\n\t * Test get_sales_page_url method\n\t * @return   void\n\t * @since    3.20.0\n\t * @version  3.20.0\n\t */\n\tpublic function test_get_sales_page_url() {\n\n\t\t$course = new LLMS_Course( 'new', 'Course Name' );\n\n\t\t$this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'none' );\n\t\t$this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'content' );\n\t\t$this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'url' );\n\t\t$course->set( 'sales_page_content_url', 'https://lifterlms.com' );\n\t\t$this->assertEquals( 'https://lifterlms.com', $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'page' );\n\t\t$page = $this->factory->post->create();\n\t\t$course->set( 'sales_page_content_page_id', $page );\n\t\t$this->assertEquals( get_permalink( $page ), $course->get_sales_page_url() );\n\n\t}\n\n\t/**\n\t * Test the get sections function\n\t * @return   void\n\t * @since    3.12.0\n\t * @version  3.12.0\n\t */\n\tpublic function test_get_sections() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 4, 0, 0, 0 )[0] );\n\n\t\t// get just ids\n\t\t$sections = $course->get_sections( 'ids' );\n\t\t$this->assertEquals( 4, count( $sections ) );\n\t\tarray_map( function( $id ) {\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t}, $sections );\n\n\t\t// wp post objects\n\t\t$sections = $course->get_sections( 'posts' );\n\t\t$this->assertEquals( 4, count( $sections ) );\n\t\tarray_map( function( $post ) {\n\t\t\t$this->assertTrue( is_a( $post, 'WP_Post' ) );\n\t\t}, $sections );\n\n\t\t// section objects\n\t\t$sections = $course->get_sections( 'sections' );\n\t\t$this->assertEquals( 4, count( $sections ) );\n\t\tarray_map( function( $section ) {\n\t\t\t$this->assertTrue( is_a( $section, 'LLMS_Section' ) );\n\t\t}, $sections );\n\n\t}\n\n\t/**\n\t * Test get_student_count()\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_student_count() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$course    = llms_get_post( $course_id );\n\n\t\t// No value, uses default from course default property value (instead of using an empty string).\n\t\t$this->assertSame( 0, $course->get_student_count() );\n\n\t\t// Cached 0.\n\t\t$this->assertSame( 0, $course->get_student_count() );\n\n\t\t// Fake cache hit.\n\t\t$course->set( 'enrolled_students', 52 );\n\t\t$this->assertSame( 52, $course->get_student_count() );\n\n\t\t// Use real data.\n\t\t$this->factory->student->create_and_enroll_many( 2, $course_id );\n\n\t\t// Skip cache.\n\t\t$this->assertSame( 2, $course->get_student_count( true ) );\n\n\t\t// Cached.\n\t\t$this->assertSame( 2, $course->get_student_count() );\n\n\t}\n\n\t/**\n\t * Test the get students function\n\t * @return   void\n\t * @since    3.12.0\n\t * @version  3.12.0\n\t */\n\tpublic function test_get_students() {\n\n\t\t$this->create();\n\n\t\t$students = $this->factory->user->create_many( 10, array( 'role' => 'student' ) );\n\t\tforeach ( $students as $sid ) {\n\t\t\tllms_enroll_student( $sid, $this->obj->get( 'id' ), 'testing' );\n\t\t}\n\n\t\t$this->assertEquals( 5, count( $this->obj->get_students( array( 'enrolled' ), 5 ) ) );\n\t\t$this->assertEquals( 10, count( $this->obj->get_students() ) );\n\n\t}\n\n\t/**\n\t * Test the has_capacity function\n\t * @return   void\n\t * @since    3.12.0\n\t * @version  3.12.0\n\t */\n\tpublic function test_has_capacity() {\n\n\t\t$this->create();\n\t\t// has capacity when nothing set\n\t\t$this->assertTrue( $this->obj->has_capacity() );\n\n\t\t$students = $this->factory->user->create_many( 10, array( 'role' => 'student' ) );\n\t\tforeach ( $students as $sid ) {\n\t\t\tllms_enroll_student( $sid, $this->obj->get( 'id' ), 'testing' );\n\t\t}\n\n\t\t// has capacity when students enrolled and nothing set\n\t\t$this->assertTrue( $this->obj->has_capacity() );\n\n\t\t// enabled capacity\n\t\t$this->obj->set( 'enable_capacity', 'yes' );\n\t\t$this->obj->set( 'capacity', 25 );\n\n\t\t// still open\n\t\t$this->assertTrue( $this->obj->has_capacity() );\n\n\t\t// over capacity\n\t\t$this->obj->set( 'capacity', 5 );\n\t\t$this->assertFalse( $this->obj->has_capacity() );\n\n\t\t// disable capacity\n\t\t$this->obj->set( 'enable_capacity', 'no' );\n\t\t$this->assertTrue( $this->obj->has_capacity() );\n\n\t}\n\n\t/**\n\t * Test the `has_sales_page_redirect` method.\n\t *\n\t * @since 3.20.0\n\t * @since 5.2.1 Add checks for empty URL and page ID.\n\t */\n\tpublic function test_has_sales_page_redirect() {\n\n\t\t$course = new LLMS_Course( 'new', 'Course Name' );\n\n\t\t$this->assertEquals( false, $course->has_sales_page_redirect() );\n\n\t\t$course->set( 'sales_page_content_type', 'none' );\n\t\t$this->assertEquals( false, $course->has_sales_page_redirect() );\n\n\t\t$course->set( 'sales_page_content_type', 'content' );\n\t\t$this->assertEquals( false, $course->has_sales_page_redirect() );\n\n\t\t$course->set( 'sales_page_content_type', 'url' );\n\t\t$this->assertEquals( false, $course->has_sales_page_redirect() );\n\n\t\t$course->set( 'sales_page_content_url', 'https://lifterlms.com' );\n\t\t$this->assertEquals( true, $course->has_sales_page_redirect() );\n\n\t\t$course->set( 'sales_page_content_type', 'page' );\n\t\t$this->assertEquals( false, $course->has_sales_page_redirect() );\n\n\t\t$page_id = $this->factory()->post->create( array( 'post_type' => 'page' ) );\n\t\t$course->set( 'sales_page_content_page_id', $page_id );\n\t\t$this->assertEquals( true, $course->has_sales_page_redirect() );\n\n\t}\n\n\t/**\n\t * Test to_array_extra_blocks()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_to_array_extra_blocks() {\n\n\t\t// Mock reusable block.\n\t\t$block_title   = 'Reusable block title';\n\t\t$block_content = '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->';\n\t\t$block         = $this->factory->post->create( array(\n\t\t\t'post_content' => $block_content,\n\t\t\t'post_title'   => $block_title,\n\t\t\t'post_type'    => 'wp_block',\n\t\t) );\n\n\t\t// Get the HTML of the reusable block to use in our mock course content..\n\t\t$html  = serialize_block( array(\n\t\t\t'blockName' => 'core/block',\n\t\t\t'innerContent' => array( '' ),\n\t\t\t'attrs' => array(\n\t\t\t\t'ref' => $block,\n\t\t\t)\n\t\t) );\n\t\t$html .= serialize_block( array(\n\t\t\t'blockName'    => 'core/paragraph',\n\t\t\t'innerContent' => array( 'Lorem ipsum dolor sit.' ),\n\t\t\t'attrs'        => array(),\n\t\t) );\n\n\t\t// Mock course.\n\t\t$post   = $this->factory->post->create_and_get( array(\n\t\t\t'post_type'    => 'course',\n\t\t\t'post_content' => $html,\n\t\t) );\n\t\t$course = llms_get_post( $post );\n\n\t\t$expect = array(\n\t\t\t$block => array(\n\t\t\t\t'title'   => $block_title,\n\t\t\t\t'content' => $block_content,\n\t\t\t),\n\t\t);\n\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $course, 'to_array_extra_blocks', array( $post->post_content ) ) );\n\n\t}\n\n\t/**\n\t * Test to_array_extra_images()\n\t *\n\t * @since 4.7.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_to_array_extra_images() {\n\n\t\t$post = $this->factory->post->create_and_get( array(\n\t\t\t'post_type'    => 'course',\n\t\t\t'post_content' => '<!-- wp:image {\"id\":552,\"sizeSlug\":\"large\"} -->\n<figure class=\"wp-block-image size-large\"><img src=\"http://example.org/wp-content/uploads/2020/09/image1.png\" alt=\"\" class=\"wp-image-1\" /></figure>\n<!-- /wp:image -->\n<!-- wp:gallery {\"ids\":[1,2]} -->\n<figure class=\"wp-block-gallery columns-2 is-cropped\"><ul class=\"blocks-gallery-grid\">\n<li class=\"blocks-gallery-item\"><figure><img src=\"http://example.org/wp-content/uploads/2020/09/image1.png\" alt=\"\" data-id=\"1\" data-full-url=\"http://example.org/wp-content/uploads/2020/09/image1.png\" data-link=\"http://example.org/wp-content/uploads/2020/09/image1.png\" class=\"wp-image-1\" /></figure></li>\n<li class=\"blocks-gallery-item\"><figure><img src=\"http://example.org/wp-content/uploads/2020/09/image2.jpg\" alt=\"\" data-id=\"2\" data-full-url=\"http://example.org/wp-content/uploads/2020/09/image2.jpg\" data-link=\"http://example.org/wp-content/uploads/2020/09/image2.jpg\" class=\"wp-image-2\" /></figure></li></ul></figure>\n<!-- /wp:gallery -->\n<img src=\"http://example.org/wp-content/uploads/2020/09/image1.png\" alt=\"\" class=\"wp-image-1\"  />\n<img src=\"http://cdn.tld/image3.png\"  />'\n\t\t) );\n\n\t\t$expect = array(\n\t\t\t'http://example.org/wp-content/uploads/2020/09/image1.png',\n\t\t\t'http://example.org/wp-content/uploads/2020/09/image2.jpg',\n\t\t);\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( llms_get_post( $post ), 'to_array_extra_images', array( $post->post_content ) ) );\n\n\t}\n\n\t/**\n\t * Test summary get_to_array_excluded_properties()\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_to_array_excluded_properties() {\n\n\t\t// Default behavior\n\t\t$course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\t\t$expect = array(\n\t\t\t'average_grade',\n\t\t\t'average_progress',\n\t\t\t'enrolled_students',\n\t\t\t'last_data_calc_run',\n\t\t\t'temp_calc_data'\n\t\t);\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::call_method( $course, 'get_to_array_excluded_properties' ) );\n\n\t\t// Disabled via filter.\n\t\tadd_filter( 'llms_course_to_array_disable_prop_exclusion', '__return_true' );\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $course, 'get_to_array_excluded_properties' ) );\n\t\tremove_filter( 'llms_course_to_array_disable_prop_exclusion', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test toArray to ensure no excluded properties are included.\n\t *\n\t * @since 5.4.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_toArray_exclusion() {\n\n\t\t$course = llms_get_post( $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\n\t\t$arr = $course->toArray();\n\n\t\t$excluded = array(\n\t\t\t'average_grade',\n\t\t\t'average_progress',\n\t\t\t'enrolled_students',\n\t\t\t'last_data_calc_run',\n\t\t\t'temp_calc_data'\n\t\t);\n\n\t\t// Shouldn't contain any excluded props.\n\t\t$this->assertEquals( array(), array_intersect( $excluded, array_keys( $arr ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Lesson Model\n *\n * @group post_models\n * @group lessons\n *\n * @since 3.14.8\n * @since 3.29.0 Unknown.\n * @since 3.36.2 Added tests on lesson's availability with drip method set as 3 days after\n *               the course start date and empty course start date.\n *               Also added `$date_delta` property to be used to test dates against current time.\n * @since 4.4.0 Added tests on next/previous lessons retrieval.\n * @since 4.4.2 Added additional navigation testing scenarios.\n * @since 4.11.0 Addeed additional tests when retrieving next/prev lesson with empty sibling sections.\n * @since 6.3.0 Added tests on comment_status reflecting default settings on lesson creation.\n */\nclass LLMS_Test_LLMS_Lesson extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Class name for the model being tested by the class\n\t *\n\t * @var string\n\t */\n\tprotected $class_name = 'LLMS_Lesson';\n\n\t/**\n\t * Db post type of the model being tested\n\t *\n\t * @var string\n\t */\n\tprotected $post_type = 'lesson';\n\n\t/**\n\t * Consider dates equal for +/- 1 min\n\t *\n\t * @var integer\n\t */\n\tprivate $date_delta = 60;\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t *\n\t * This should match, exactly, the object's $properties array.\n\t *\n\t * @since 3.14.8\n\t * @since 3.16.11 Unknown.\n\t * @return array\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\n\t\t\t'order'                 => 'absint',\n\n\t\t\t// Drippable.\n\t\t\t'days_before_available' => 'absint',\n\t\t\t'date_available'        => 'text',\n\t\t\t'drip_method'           => 'text',\n\t\t\t'time_available'        => 'text',\n\n\t\t\t// Parent element.\n\t\t\t'parent_course'         => 'absint',\n\t\t\t'parent_section'        => 'absint',\n\n\t\t\t'audio_embed'           => 'text',\n\t\t\t'free_lesson'           => 'yesno',\n\t\t\t'has_prerequisite'      => 'yesno',\n\t\t\t'prerequisite'          => 'absint',\n\t\t\t'require_passing_grade' => 'yesno',\n\t\t\t'video_embed'           => 'text',\n\n\t\t\t// Quizzes.\n\t\t\t'quiz'                  => 'absint',\n\t\t\t'quiz_enabled'          => 'yesno',\n\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters.\n\t *\n\t * @since 3.14.8\n\t * @since 3.16.11 Unknown.\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'audio_embed'           => 'http://example.tld/audio_embed',\n\t\t\t'date_available'        => '11/21/2018',\n\t\t\t'days_before_available' => '24',\n\t\t\t'drip_method'           => 'date',\n\t\t\t'free_lesson'           => 'no',\n\t\t\t'has_prerequisite'      => 'yes',\n\t\t\t'order'                 => 1,\n\t\t\t'parent_course'         => 85,\n\t\t\t'parent_section'        => 32,\n\t\t\t'prerequisite'          => 344,\n\t\t\t'quiz'                  => 123,\n\t\t\t'quiz_enabled'          => 'yes',\n\t\t\t'require_passing_grade' => 'yes',\n\t\t\t'time_available'        => '12:34 PM',\n\t\t\t'video_embed'           => 'http://example.tld/video_embed',\n\t\t);\n\t}\n\n\n\t/*\n\t\t   /$$                           /$$\n\t\t  | $$                          | $$\n\t\t /$$$$$$    /$$$$$$   /$$$$$$$ /$$$$$$   /$$$$$$$\n\t\t|_  $$_/   /$$__  $$ /$$_____/|_  $$_/  /$$_____/\n\t\t  | $$    | $$$$$$$$|  $$$$$$   | $$   |  $$$$$$\n\t\t  | $$ /$$| $$_____/ \\____  $$  | $$ /$$\\____  $$\n\t\t  |  $$$$/|  $$$$$$$ /$$$$$$$/  |  $$$$//$$$$$$$/\n\t\t   \\___/   \\_______/|_______/    \\___/ |_______/\n\t*/\n\n\t/**\n\t * Test get available date.\n\t *\n\t * @since Unknown.\n\t * @since 3.36.2 Added tests on lesson's availability with drip method set as 3 days after\n\t *               the course start date and empty course start date.\n\t * @since 5.3.3 Use `assertEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_available_date() {\n\n\t\t$format = 'Y-m-d';\n\n\t\t$course_id = $this->generate_mock_courses( 1, 1, 2, 0 )[0];\n\t\t$course    = llms_get_post( $course_id );\n\t\t$lesson    = $course->get_lessons()[0];\n\t\t$lesson_id = $lesson->get( 'id' );\n\t\t$student   = $this->get_mock_student();\n\t\twp_set_current_user( $student->get_id() );\n\t\t$student->enroll( $course_id );\n\n\t\t// No drip settings, lesson is currently available.\n\t\t$this->assertEquals( current_time( $format ), $lesson->get_available_date( $format ) );\n\n\t\t$lesson->set( 'drip_method', 'date' );\n\t\t$lesson->set( 'date_available', '12/12/2012' );\n\t\t$lesson->set( 'time_available', '12:12 AM' );\n\t\t$this->assertEquals( date( $format, strtotime( '12/12/2012' ) ), $lesson->get_available_date( $format ) );\n\t\t$this->assertEquals( date( 'U', strtotime( '12/12/2012 12:12 AM' ) ), $lesson->get_available_date( 'U' ) );\n\n\t\t$lesson->set( 'drip_method', 'enrollment' );\n\t\t$lesson->set( 'days_before_available', '3' );\n\t\t$this->assertEquals( $student->get_enrollment_date( $course_id, 'enrolled', 'U' ) + ( DAY_IN_SECONDS * 3 ), $lesson->get_available_date( 'U' ) );\n\n\t\t$lesson->set( 'drip_method', 'start' );\n\t\t$start = current_time( 'm/d/Y' );\n\t\t$course->set( 'start_date', $start );\n\t\t$this->assertEquals( strtotime( $start ) + ( DAY_IN_SECONDS * 3 ), $lesson->get_available_date( 'U' ) );\n\n\t\t$prereq_id = $lesson_id;\n\t\t$student->mark_complete( $lesson_id, 'lesson' );\n\n\t\t$lesson = $course->get_lessons()[1];\n\n\t\t$lesson->set( 'has_prerequisite', 'yes' );\n\t\t$lesson->set( 'prerequisite', $lesson_id );\n\n\t\t$lesson->set( 'drip_method', 'prerequisite' );\n\t\t$lesson->set( 'days_before_available', '3' );\n\t\t$this->assertEquals( $student->get_completion_date( $prereq_id, 'U' ) + ( DAY_IN_SECONDS * 3 ), $lesson->get_available_date( 'U' ) );\n\n\t\t// Check lesson immediately available if set to be available after 3 days ofter a course start date which is empty.\n\t\t$lesson->set( 'drip_method', 'start' );\n\t\t$lesson->set( 'days_before_available', '3' );\n\t\t$course->set( 'start_date', '' );\n\t\t$this->assertEqualsWithDelta( current_time( 'timestamp' ), $lesson->get_available_date( 'U' ), $this->date_delta );\n\n\t}\n\n\t/**\n\t * Test get available date when the course has \"After course starts\" delay in days set.\n\t *\n\t * @since 7.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_available_date_with_course_drip_settings() {\n\n\t\t$format = 'Y-m-d';\n\n\t\t$course_id = $this->generate_mock_courses( 1, 3, 2, 0 )[0];\n\n\t\t$course = llms_get_post( $course_id );\n\t\t$course->set( 'lesson_drip', 'yes' );\n\t\t$course->set( 'drip_method', 'start' );\n\t\t$course->set( 'days_before_available', '7' );\n\t\t$course->set( 'ignore_lessons', '1' );\n\n\t\t$student = $this->get_mock_student();\n\t\twp_set_current_user( $student->get_id() );\n\t\t$student->enroll( $course_id );\n\n\t\t$now = new DateTimeImmutable();\n\n\t\t$this->assertEquals( $now->format( $format ), $course->get_lessons()[0]->get_available_date( $format ) );\n\t\t$this->assertEquals( $now->add( DateInterval::createFromDateString( '7 days') )->format( $format ), $course->get_lessons()[1]->get_available_date( $format ) );\n\t\t$this->assertEquals( $now->add( DateInterval::createFromDateString( '14 days') )->format( $format ), $course->get_lessons()[2]->get_available_date( $format ) );\n\t\t$this->assertEquals( $now->add( DateInterval::createFromDateString( '21 days') )->format( $format ), $course->get_lessons()[3]->get_available_date( $format ) );\n\n\t}\n\n\t/**\n\t * Test get available date when the course has \"After course starts\" delay in days set and\n\t * the course has a fixed start date.\n\t *\n\t * @since 7.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_available_date_with_course_drip_settings_with_course_start_date() {\n\n\t\t$format = 'Y-m-d';\n\n\t\t$course_id = $this->generate_mock_courses( 1, 3, 2, 0 )[0];\n\n\t\t$course = llms_get_post( $course_id );\n\t\t$course->set( 'lesson_drip', 'yes' );\n\t\t$course->set( 'drip_method', 'start' );\n\t\t$course->set( 'days_before_available', '7' );\n\t\t$course->set( 'ignore_lessons', '1' );\n\t\t$course_start = new DateTimeImmutable( '-1 week' );\n\t\t$course->set( 'start_date', $course_start->format( 'm/d/Y' ) );\n\n\t\t$student = $this->get_mock_student();\n\t\twp_set_current_user( $student->get_id() );\n\t\t$student->enroll( $course_id );\n\n\t\t$now = new DateTimeImmutable();\n\n\t\t$this->assertEquals( $now->format( $format ), $course->get_lessons()[0]->get_available_date( $format ) );\n\t\t$this->assertEquals( $course_start->add( DateInterval::createFromDateString( '7 days') )->format( $format ), $course->get_lessons()[1]->get_available_date( $format ) );\n\t\t$this->assertEquals( $course_start->add( DateInterval::createFromDateString( '14 days') )->format( $format ), $course->get_lessons()[2]->get_available_date( $format ) );\n\t\t$this->assertEquals( $course_start->add( DateInterval::createFromDateString( '21 days') )->format( $format ), $course->get_lessons()[3]->get_available_date( $format ) );\n\n\t}\n\n\t/**\n\t * Test get course\n\t *\n\t * @since unknown\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_course() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] );\n\t\t$lesson = llms_get_post( $course->get_lessons( 'ids' )[0] );\n\n\t\t// Returns a course when everything's okay.\n\t\t$this->assertTrue( is_a( $lesson->get_course(), 'LLMS_Course' ) );\n\n\t\t// Course trashed / doesn't exist, returns null.\n\t\twp_delete_post( $course->get( 'id' ), true );\n\t\t$this->assertNull( $lesson->get_course() );\n\n\t}\n\n\t/**\n\t * Test Audio and Video Embeds\n\t *\n\t * @since 3.14.8\n\t * @since 4.10.0 Fix faulty tests, use assertSame in favor of assertEquals.\n\t * @since 6.0.0 Mock oembed results to prevent rate limiting issues causing tests to fail.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_embeds() {\n\n\t\t$iframe = '<iframe src=\"%s\"></iframe>';\n\n\t\t$handler = function( $html, $url ) use ( $iframe ) {\n\t\t\treturn sprintf( $iframe, $url );\n\t\t};\n\n\t\tadd_filter( 'pre_oembed_result', $handler, 10, 2 );\n\n\t\t$lesson = new LLMS_Lesson( 'new', 'Lesson With Embeds' );\n\n\t\t$audio_url = 'http://example.tld/audio_embed';\n\t\t$video_url = 'http://example.tld/video_embed';\n\n\t\t// Empty string when none set.\n\t\t$this->assertEmpty( $lesson->get_audio() );\n\t\t$this->assertEmpty( $lesson->get_video() );\n\n\t\t$lesson->set( 'audio_embed', $audio_url );\n\t\t$lesson->set( 'video_embed', $video_url );\n\n\t\t$audio_embed = $lesson->get_audio();\n\t\t$video_embed = $lesson->get_video();\n\n\t\t// Should be an iframe for valid embeds.\n\t\t$this->assertEquals( sprintf( $iframe, $audio_url ),$audio_embed );\n\t\t$this->assertEquals( sprintf( $iframe, $video_url ),$video_embed );\n\n\t\tremove_filter( 'pre_oembed_result', $handler, 10, 2 );\n\n\t\t// Fallbacks should be a link to the URL.\n\t\t$not_embeddable_url = 'http://lifterlms.com/not/embeddable';\n\n\t\t$lesson->set( 'audio_embed', $not_embeddable_url );\n\t\t$lesson->set( 'video_embed', $not_embeddable_url );\n\t\t$audio_embed = $lesson->get_audio();\n\t\t$video_embed = $lesson->get_video();\n\n\t\t$this->assertSame( 0, strpos( $audio_embed, '<a' ) );\n\t\t$this->assertSame( 0, strpos( $video_embed, '<a' ) );\n\n\t\t$this->assertStringContains( sprintf( 'href=\"%s\"', $not_embeddable_url ), $audio_embed );\n\t\t$this->assertStringContains( sprintf( 'href=\"%s\"', $not_embeddable_url ), $video_embed );\n\n\t}\n\n\t/**\n\t * Test getting parent section\n\t *\n\t * @since unknown\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_section() {\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] );\n\t\t$lesson = llms_get_post( $course->get_lessons( 'ids' )[0] );\n\n\t\t// Returns a course when everything's okay.\n\t\t$this->assertTrue( is_a( $lesson->get_section(), 'LLMS_Section' ) );\n\n\t\t// Section trashed / doesn't exist, returns null.\n\t\twp_delete_post( $lesson->get( 'parent_section' ), true );\n\t\t$this->assertNull( $lesson->get_section() );\n\n\t}\n\n\t/**\n\t * Test has_modified_slug function\n\t *\n\t * @since 3.14.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_modified_slug() {\n\n\t\t$lesson = new LLMS_Lesson( 'new', 'New Lesson' );\n\n\t\t// Default unmodified slug.\n\t\t$this->assertFalse( $lesson->has_modified_slug() );\n\n\t\t// Default unmodified slug with a unique int at the end.\n\t\t$lesson->set( 'name', 'new-lesson-123' );\n\n\t\t$this->assertFalse( $lesson->has_modified_slug() );\n\n\t\t// Renamed slug.\n\t\t$lesson->set( 'name', 'modified-slug' );\n\n\t\t$this->assertTrue( $lesson->has_modified_slug() );\n\n\t}\n\n\t/**\n\t * Test the has_quiz() method\n\t *\n\t * @since 3.29.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_quiz() {\n\n\t\t$lesson = new LLMS_Lesson( 'new', 'New Lesson' );\n\n\t\t$this->assertFalse( $lesson->has_quiz() );\n\t\t$lesson->set( 'quiz', 123 );\n\t\t$this->assertTrue( $lesson->has_quiz() );\n\n\t}\n\n\t/**\n\t * Test the is_available() method\n\t *\n\t * @since unknown\n\t * @since 6.0.0 Replaced use of deprecated items.\n\t *              - `llms_reset_current_time()` with `llms_tests_reset_current_time()` from the `lifterlms-tests` project\n\t *              - `llms_mock_current_time()` with `llms_tests_mock_current_time()` from the `lifterlms-tests` project\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_available() {\n\n\t\t$course_id = $this->generate_mock_courses( 1, 1, 2, 0 )[0];\n\t\t$course    = llms_get_post( $course_id );\n\t\t$lesson    = $course->get_lessons()[0];\n\t\t$lesson_id = $lesson->get( 'id' );\n\t\t$student   = $this->get_mock_student();\n\t\twp_set_current_user( $student->get_id() );\n\t\t$student->enroll( $course_id );\n\n\t\t// No drip settings, lesson is currently available.\n\t\t$this->assertTrue( $lesson->is_available() );\n\n\t\t// Date in past so the lesson is available.\n\t\t$lesson = llms_get_post( $lesson_id );\n\t\t$lesson->set( 'drip_method', 'date' );\n\t\t$lesson->set( 'date_available', '12/12/2012' );\n\t\t$lesson->set( 'time_available', '12:12 AM' );\n\t\t$this->assertTrue( $lesson->is_available() );\n\n\t\t// Date in future so lesson not available.\n\t\t$lesson->set( 'date_available', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) );\n\t\t$this->assertFalse( $lesson->is_available() );\n\n\t\t// Available 3 days after enrollment.\n\t\t$lesson->set( 'drip_method', 'enrollment' );\n\t\t$lesson->set( 'days_before_available', '3' );\n\t\t$this->assertFalse( $lesson->is_available() );\n\n\t\t// Now available.\n\t\tllms_tests_mock_current_time( '+4 days' );\n\t\t$this->assertTrue( $lesson->is_available() );\n\n\t\tllms_tests_reset_current_time();\n\t\t$lesson->set( 'drip_method', 'start' );\n\t\t$course->set( 'start_date', date( 'm/d/Y', current_time( 'timestamp' ) + DAY_IN_SECONDS ) );\n\n\t\t// Not available until 3 days after course start date.\n\t\t$this->assertFalse( $lesson->is_available() );\n\n\t\t// Now available.\n\t\tllms_tests_mock_current_time( '+4 days' );\n\t\t$this->assertTrue( $lesson->is_available() );\n\t\tllms_tests_reset_current_time();\n\n\t\t$prereq_id = $lesson_id;\n\t\t$student->mark_complete( $lesson_id, 'lesson' );\n\n\t\t// Second lesson not available until 3 days after lesson 1 is complete.\n\t\t$lesson = $course->get_lessons()[1];\n\n\t\t$lesson->set( 'has_prerequisite', 'yes' );\n\t\t$lesson->set( 'prerequisite', $lesson_id );\n\n\t\t$lesson->set( 'drip_method', 'prerequisite' );\n\t\t$lesson->set( 'days_before_available', '3' );\n\n\t\t$this->assertFalse( $lesson->is_available() );\n\n\t\tllms_tests_mock_current_time( '+4 days' );\n\t\t$this->assertTrue( $lesson->is_available() );\n\n\t}\n\n\t/**\n\t * Test the is_orphan() method\n\t *\n\t * @since 3.14.8\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_orphan() {\n\n\t\t$course  = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 0, 0 )[0] );\n\t\t$section = llms_get_post( $course->get_sections( 'ids' )[0] );\n\t\t$lesson  = llms_get_post( $course->get_lessons( 'ids' )[0] );\n\n\t\t// Not an orphan.\n\t\t$this->assertFalse( $lesson->is_orphan() );\n\n\t\t$test_statuses = get_post_stati( array( '_builtin' => true ) );\n\t\tforeach ( array_keys( $test_statuses ) as $status ) {\n\n\t\t\t$assert = in_array( $status, array( 'publish', 'future', 'draft', 'pending', 'private', 'auto-draft' ), true ) ? 'assertFalse' : 'assertTrue';\n\n\t\t\t// Check parent course.\n\t\t\twp_update_post(\n\t\t\t\tarray(\n\t\t\t\t\t'ID'          => $course->get( 'id' ),\n\t\t\t\t\t'post_status' => $status,\n\t\t\t\t)\n\t\t\t);\n\t\t\t$this->$assert( $lesson->is_orphan() );\n\t\t\twp_update_post(\n\t\t\t\tarray(\n\t\t\t\t\t'ID'          => $course->get( 'id' ),\n\t\t\t\t\t'post_status' => 'publish',\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Check parent section.\n\t\t\twp_update_post(\n\t\t\t\tarray(\n\t\t\t\t\t'ID'          => $section->get( 'id' ),\n\t\t\t\t\t'post_status' => $status,\n\t\t\t\t)\n\t\t\t);\n\t\t\t$this->$assert( $lesson->is_orphan() );\n\t\t\twp_update_post(\n\t\t\t\tarray(\n\t\t\t\t\t'ID'          => $section->get( 'id' ),\n\t\t\t\t\t'post_status' => 'publish',\n\t\t\t\t)\n\t\t\t);\n\n\t\t}\n\n\t\t// Parent course doesn't exist.\n\t\t$lesson->set( 'parent_course', 123456789 );\n\t\t$this->assertTrue( $lesson->is_orphan() );\n\t\t$lesson->set( 'parent_course', $course->get( 'id' ) );\n\n\t\t// Parent section doesn't exist.\n\t\t$lesson->set( 'parent_section', 123456789 );\n\t\t$this->assertTrue( $lesson->is_orphan() );\n\t\t$lesson->set( 'parent_section', $section->get( 'id' ) );\n\n\t\t// Parent course isn't set.\n\t\t$lesson->set( 'parent_course', '' );\n\t\t$this->assertTrue( $lesson->is_orphan() );\n\t\t$lesson->set( 'parent_course', $course->get( 'id' ) );\n\n\t\t// Parent section isn't set.\n\t\t$lesson->set( 'parent_section', '' );\n\t\t$this->assertTrue( $lesson->is_orphan() );\n\t\t$lesson->set( 'parent_section', $section->get( 'id' ) );\n\n\t\t// Metakey for parent course doesn't exist.\n\t\tdelete_post_meta( $lesson->get( 'id' ), '_llms_parent_course' );\n\t\t$this->assertTrue( $lesson->is_orphan() );\n\t\t$lesson->set( 'parent_course', $course->get( 'id' ) );\n\n\t\t// Metakey for parent section doesn't exist.\n\t\tdelete_post_meta( $lesson->get( 'id' ), '_llms_parent_section' );\n\t\t$this->assertTrue( $lesson->is_orphan() );\n\t\t$lesson->set( 'parent_section', $section->get( 'id' ) );\n\n\t\t// Not an orphan.\n\t\t$this->assertFalse( $lesson->is_orphan() );\n\n\t}\n\n\t/**\n\t * Test next lesson\n\t *\n\t * @since 4.4.0\n\t */\n\tpublic function test_get_next_lesson() {\n\n\t\t// Generate a course with 2 sections and 3 lessons for each of them.\n\t\t$course_id   = $this->generate_mock_courses( 1, 2, 3, 0 )[0];\n\t\t$section_ids = llms_get_post( $course_id )->get_sections( 'ids' );\n\t\t$section_one = llms_get_post( $section_ids[0] );\n\t\t$section_two = llms_get_post( $section_ids[1] );\n\n\t\t$sec_lessons = array(\n\t\t\t$section_one->get_lessons( 'ids' ),\n\t\t\t$section_two->get_lessons( 'ids' ),\n\t\t);\n\n\t\t// Test next lesson of s1 l2 is s1 l3.\n\t\t$this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[0][1] )->get_next_lesson() );\n\n\t\t// Test next lesson of s1 l3 is s2 l1.\n\t\t$this->assertEquals( $sec_lessons[1][0], llms_get_post( $sec_lessons[0][2] )->get_next_lesson() );\n\n\t\t// Swap s1 l2 and s1 l3 orders.\n\t\tllms_get_post( $sec_lessons[0][1] )->set( 'order', 3 );\n\t\tllms_get_post( $sec_lessons[0][2] )->set( 'order', 2 );\n\n\t\t// Test next lesson of s1 l1 is the original s1 l3.\n\t\t$this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[0][0] )->get_next_lesson() );\n\t\t// \"Persist\" the new order in our sec_lessons array.\n\t\tlist( $sec_lessons[0][2], $sec_lessons[0][1] ) = array( $sec_lessons[0][1], $sec_lessons[0][2] );\n\n\t\t// Test s2 l3 has no next lesson.\n\t\t$this->assertFalse( llms_get_post( $sec_lessons[1][2] )->get_next_lesson() );\n\n\t\t// Unpublish s1 l2, test next lesson of s1 l1 is s1 l3.\n\t\tllms_get_post( $sec_lessons[0][1] )->set( 'status', 'draft' );\n\t\t$this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[0][0] )->get_next_lesson() );\n\n\t\t// Unpublish s2 l3, test next lesson of s2 l2 is false.\n\t\tllms_get_post( $sec_lessons[1][2] )->set( 'status', 'draft' );\n\t\t$this->assertFalse( llms_get_post( $sec_lessons[1][1] )->get_next_lesson() );\n\t}\n\n\n\t/**\n\t * Test previous lesson\n\t *\n\t * @since 4.4.0\n\t */\n\tpublic function test_get_previous_lesson() {\n\n\t\t// Generate a course with 2 sections and 3 lessons for each of them.\n\t\t$course_id   = $this->generate_mock_courses( 1, 2, 3, 0 )[0];\n\t\t$section_ids = llms_get_post( $course_id )->get_sections( 'ids' );\n\t\t$section_one = llms_get_post( $section_ids[0] );\n\t\t$section_two = llms_get_post( $section_ids[1] );\n\n\t\t$sec_lessons = array(\n\t\t\t$section_one->get_lessons( 'ids' ),\n\t\t\t$section_two->get_lessons( 'ids' ),\n\t\t);\n\n\t\t// Test previous lesson of s1 l3 is s1 l2.\n\t\t$this->assertEquals( $sec_lessons[0][1], llms_get_post( $sec_lessons[0][2] )->get_previous_lesson() );\n\n\t\t// Test previous lesson of s2 l1 is s1 l3.\n\t\t$this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[1][0] )->get_previous_lesson() );\n\n\t\t// Swap s1 l1 and s1 l2 orders.\n\t\tllms_get_post( $sec_lessons[0][0] )->set( 'order', 2 );\n\t\tllms_get_post( $sec_lessons[0][1] )->set( 'order', 1 );\n\n\t\t// Test previous lesson of s1 l3 is the original s1 l1.\n\t\t$this->assertEquals( $sec_lessons[0][0], llms_get_post( $sec_lessons[0][2] )->get_previous_lesson() );\n\t\t// \"Persist\" the new order in our sec_lessons array.\n\t\tlist( $sec_lessons[0][0], $sec_lessons[0][1] ) = array( $sec_lessons[0][1], $sec_lessons[0][0] );\n\n\t\t// Test s1 l1 has no previous lesson.\n\t\t$this->assertFalse( llms_get_post( $sec_lessons[0][0] )->get_previous_lesson() );\n\n\t\t// Unpublish s2 l2, test previous lesson of s2 l3 is s2 l1.\n\t\tllms_get_post( $sec_lessons[1][1] )->set( 'status', 'draft' );\n\t\t$this->assertEquals( $sec_lessons[1][0], llms_get_post( $sec_lessons[1][2] )->get_previous_lesson() );\n\n\t\t// Unpublish s2 l1, test previous lesson of s2 l2 is s1 l3.\n\t\tllms_get_post( $sec_lessons[1][0] )->set( 'status', 'draft' );\n\t\t$this->assertEquals( $sec_lessons[0][2], llms_get_post( $sec_lessons[1][1] )->get_previous_lesson() );\n\n\t\t// Unpublish s1 l1, test previous lesson of s1 l2 is false.\n\t\tllms_get_post( $sec_lessons[0][0] )->set( 'status', 'draft' );\n\t\t$this->assertFalse( llms_get_post( $sec_lessons[0][1] )->get_previous_lesson() );\n\n\t}\n\n\t/**\n\t * Test navigation with sections that have more than 10 lessons\n\t *\n\t * This scenario exposes an issue that causes string comparisons to fail, the lesson order will be returned\n\t * incorrectly.\n\t *\n\t * @since 4.4.2\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1316\n\t *\n\t * @return void\n\t */\n\tpublic function test_navigation_large_sections() {\n\n\t\t$course  = $this->factory->course->create_and_get( array( 'sections' => 2, 'lessons' => 10, 'quizzes' => 0 ) );\n\t\t$lessons = $course->get_lessons();\n\n\t\t$i = 0;\n\t\twhile ( $i < count( $lessons ) ) {\n\n\t\t\t$lesson = $lessons[ $i ];\n\n\t\t\t$next = 19 === $i ? false : $lessons[ $i + 1 ]->get( 'id' );\n\t\t\t$prev = 0 === $i ? false : $lessons[ $i - 1 ]->get( 'id' );\n\n\t\t\t$this->assertEquals( $next, $lesson->get_next_lesson(), $i );\n\t\t\t$this->assertEquals( $prev, $lesson->get_previous_lesson(), $i );\n\n\t\t\t++$i;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test comment status on creation.\n\t *\n\t * @since 6.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_comment_status_on_creation() {\n\n\t\t$original_comments_status = get_default_comment_status( $this->post_type );\n\t\tupdate_option( 'default_comment_status', 'open' );\n\t\t$lesson = new LLMS_Lesson( 'new', 'Lesson with open comments' );\n\t\t$this->assertEquals(\n\t\t\t'open',\n\t\t\t$lesson->get( 'comment_status' )\n\t\t);\n\t\tupdate_option( 'default_comment_status', 'closed' );\n\t\t$lesson = new LLMS_Lesson( 'new', 'Lesson with closed comments' );\n\t\t$this->assertEquals(\n\t\t\t'closed',\n\t\t\t$lesson->get( 'comment_status' )\n\t\t);\n\t\tupdate_option( 'default_comment_status', $original_comments_status );\n\n\t}\n\n\t/**\n\t * Test next/prev lesson with empty sibling sections\n\t *\n\t * @since 4.11.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_navigation_empty_sibling_section() {\n\n\t\t$course = $this->factory->course->create_and_get(\n\t\t\tarray(\n\t\t\t\t'sections' => 2,\n\t\t\t\t'lessons' => 1,\n\t\t\t\t'quizzes' => 0\n\t\t\t)\n\t\t);\n\n\t\t$lessons = $course->get_lessons();\n\n\t\t// Detach the second lesson from the second section.\n\t\t$second_section = $lessons[1]->get_parent_section();\n\t\t$lessons[1]->set_parent_section('');\n\t\t// Check the next lesson of the first one is false.\n\t\t$this->assertEquals( false, $lessons[0]->get_next_lesson() );\n\n\t\t// Re-attach the second lesson to the second section.\n\t\t$lessons[1]->set_parent_section( $second_section );\n\n\t\t// Detach the first lesson from the first section.\n\t\t$lessons[0]->set_parent_section('');\n\t\t// Check the previous lesson of the second one is false.\n\t\t$this->assertEquals( false, $lessons[1]->get_previous_lesson() );\n\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-membership.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Membership Model.\n *\n * @group LLMS_Membership\n * @group LLMS_Post_Model\n *\n * @since 3.20.0\n * @since 3.36.3 Remove redundant test method `test_get_sections()`,\n *               @see tests/unit-tests/models/class-llms-test-model-llms-course.php.\n * @since 5.2.1 Add checks for empty URL and page ID in `test_has_sales_page_redirect()`.\n */\nclass LLMS_Test_LLMS_Membership extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * class name for the model being tested by the class.\n\t * @var string\n\t */\n\tprotected $class_name = 'LLMS_Membership';\n\n\t/**\n\t * db post type of the model being tested.\n\t * @var string\n\t */\n\tprotected $post_type = 'llms_membership';\n\n\t/**\n\t * Get properties, used by test_getters_setters.\n\t * This should match, exactly, the object's $properties array.\n\t *\n\t * @since 3.20.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\t\t\t'auto_enroll' => 'array',\n\t\t\t'redirect_page_id' => 'absint',\n\t\t\t'restriction_add_notice' => 'yesno',\n\t\t\t'restriction_notice' => 'html',\n\t\t\t'restriction_redirect_type' => 'text',\n\t\t\t'redirect_custom_url' => 'text',\n\t\t\t'sales_page_content_page_id' => 'absint',\n\t\t\t'sales_page_content_type' => 'string',\n\t\t\t'sales_page_content_url' => 'string',\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with.\n\t * This is used by test_getters_setters.\n\t *\n\t * @since 3.20.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'auto_enroll' => array(),\n\t\t\t'redirect_page_id' => '1',\n\t\t\t'restriction_add_notice' => 'yes',\n\t\t\t'restriction_notice' => '<p>test</p>',\n\t\t\t'restriction_redirect_type' => 'none',\n\t\t\t'redirect_custom_url' => 'https://lifterlms.com',\n\t\t\t'sales_page_content_page_id' => 1,\n\t\t\t'sales_page_content_type' => 'none',\n\t\t\t'sales_page_content_url' => 'https://lifterlms.com',\n\t\t);\n\t}\n\n\t/**\n\t * Test CRUD functions for auto enroll courses.\n\t *\n\t * Tests the following three methods:\n\t *\n\t *   + add_auto_enroll_courses()\n\t *   + get_auto_enroll_courses()\n\t *   + remoe_auto_enroll_course()\n\t *\n\t * @since 4.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_crud_auto_enroll() {\n\n\t\t$membership = $this->factory->membership->create_and_get();\n\n\t\t// No posts.\n\t\t$this->assertSame( array(), $membership->get_auto_enroll_courses() );\n\n\t\t// Add a single course (not an array).\n\t\t$course = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$this->assertTrue( $membership->add_auto_enroll_courses( $course ) );\n\t\t$this->assertSame( array( $course ), $membership->get_auto_enroll_courses() );\n\n\t\t// Add multiple courses (as an array).\n\t\t$courses = $this->factory->post->create_many( 2, array( 'post_type' => 'course' ) );\n\t\t$this->assertTrue( $membership->add_auto_enroll_courses( $courses ) );\n\t\t$this->assertEqualSets( array_merge( array( $course ), $courses ), $membership->get_auto_enroll_courses() );\n\n\t\t// Remove a course.\n\t\t$this->assertTrue( $membership->remove_auto_enroll_course( $course ) );\n\t\t$this->assertEqualSets( $courses, $membership->get_auto_enroll_courses() );\n\n\t\t// Add a course that already exists (should remove duplicates).\n\t\t$this->assertTrue( $membership->add_auto_enroll_courses( $courses[1] ) );\n\t\t$this->assertEqualSets( $courses, $membership->get_auto_enroll_courses() );\n\n\t\t// Add & replace.\n\t\t$this->assertTrue( $membership->add_auto_enroll_courses( $course, true ) );\n\t\t$this->assertEquals( array( $course ), $membership->get_auto_enroll_courses() );\n\n\t}\n\n\t/**\n\t * Ensure only published courses\n\t *\n\t * @since 4.15.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms-groups/issues/135\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_auto_enroll_courses_published_only() {\n\n\t\t$membership = $this->factory->membership->create_and_get();\n\t\t$draft      = $this->factory->post->create( array( 'post_type' => 'course', 'post_status' => 'draft' ) );\n\t\t$private    = $this->factory->post->create( array( 'post_type' => 'course', 'post_status' => 'private' ) );\n\t\t$published  = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t$this->assertTrue( $membership->add_auto_enroll_courses( array( $draft, $private, $published ) ) );\n\t\t$this->assertEqualSets( array( $published ), $membership->get_auto_enroll_courses() );\n\n\t}\n\n\t/**\n\t * Test get_associated_posts() when none exist for the membership.\n\t *\n\t * @since 3.38.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_associated_posts_none_found() {\n\n\t\t$membership = $this->factory->membership->create_and_get();\n\n\t\t$this->assertEquals( array(), $membership->get_associated_posts() );\n\n\t\t$this->assertEquals( array(), $membership->get_associated_posts( 'course' ) );\n\t\t$this->assertEquals( array(), $membership->get_associated_posts( 'page' ) );\n\t\t$this->assertEquals( array(), $membership->get_associated_posts( 'post' ) );\n\t\t$this->assertEquals( array(), $membership->get_associated_posts( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test get_associated_posts() when associations do exist.\n\t *\n\t * @since 3.38.1\n\t * @since 4.15.0 Test equal sets instead of strict equals because we don't really care about the returned order.\n\t *               Added tests to check when querying for a single post type.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_associated_posts_has_associations() {\n\n\t\t$membership = $this->factory->membership->create_and_get();\n\n\t\t// Add a post.\n\t\t$post = $this->factory->post->create();\n\t\tupdate_post_meta( $post, '_llms_is_restricted', 'yes' );\n\t\tupdate_post_meta( $post, '_llms_restricted_levels', array( $membership->get( 'id' ), 1, 1008, '183' ) );\n\n\t\t// Add pages.\n\t\t$page1 = $this->factory->post->create( array( 'post_type' => 'page' ) );\n\t\tupdate_post_meta( $page1, '_llms_is_restricted', 'yes' );\n\t\tupdate_post_meta( $page1, '_llms_restricted_levels', array( (string) $membership->get( 'id' ) . '00', $membership->get( 'id' ), 1234, 2 ) );\n\n\t\t$page2 = $this->factory->post->create( array( 'post_type' => 'page' ) );\n\t\tupdate_post_meta( $page2, '_llms_is_restricted', 'yes' );\n\t\tupdate_post_meta( $page2, '_llms_restricted_levels', array( $membership->get( 'id' ) ) );\n\n\t\t// Add a course with a plan.\n\t\t$plan = $this->get_mock_plan();\n\t\t$plan->set( 'availability', 'members' );\n\t\t$plan->set( 'availability_restrictions', array( 1, $membership->get( 'id' ) ) );\n\n\t\t// Add an autoenrollment course.\n\t\t$course = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$membership->set( 'auto_enroll', array( $course, $plan->get( 'product_id' ) ) );\n\n\t\t// Get all associations.\n\t\t$res = $membership->get_associated_posts();\n\n\t\t$this->assertEquals( array( $post ), $res['post'] );\n\t\t$this->assertEqualSets( array( $page1, $page2 ), $res['page'] );\n\t\t$this->assertEqualSets( array( $plan->get( 'product_id' ), $course ), $res['course'] );\n\n\t\t// Get only course associations.\n\t\t$res = $membership->get_associated_posts( 'course' );\n\t\t$this->assertEqualSets( array( $plan->get( 'product_id' ), $course ), $res );\n\n\t\t// Get posts.\n\t\t$res = $membership->get_associated_posts( 'post' );\n\t\t$this->assertEquals( array( $post ), $res );\n\n\t\t// Get pages.\n\t\t$res = $membership->get_associated_posts( 'page' );\n\t\t$this->assertEqualSets( array( $page1, $page2 ), $res );\n\n\t\t// Fake post type.\n\t\t$res = $membership->get_associated_posts( 'fake' );\n\t\t$this->assertEqualSets( array(), $res );\n\n\n\t}\n\n\t/**\n\t * Test LLMS_Membership->get_categories() method.\n\t *\n\t * @since 3.36.3\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_categories() {\n\t\t// create new membership\n\t\t$membership_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) );\n\t\t$membership    = new LLMS_Membership( $membership_id );\n\n\t\t// create new categories\n\t\t$taxonomy = 'membership_cat';\n\t\t$created_term_ids = array();\n\t\tfor ( $i = 1; $i <= 3; $i ++ ) {\n\t\t\t$new_term_ids = wp_create_term( \"mock-membership-category-$i\", $taxonomy );\n\t\t\t$this->assertNotWPError( $new_term_ids );\n\t\t\t$created_term_ids[ $i ] = $new_term_ids['term_id'];\n\t\t}\n\n\t\t// set categories in membership\n\t\t$term_taxonomy_ids = wp_set_post_terms( $membership_id, $created_term_ids, $taxonomy );\n\t\t$this->assertNotWPError( $term_taxonomy_ids );\n\t\t$this->assertNotFalse( $term_taxonomy_ids );\n\n\t\t// get categories from membership\n\t\t$membership_terms = $membership->get_categories();\n\t\t$membership_term_ids = array();\n\t\t/** @var WP_Term $membership_term */\n\t\tforeach ( $membership_terms as $membership_term ) {\n\t\t\t$membership_term_ids[] = $membership_term->term_id;\n\t\t}\n\n\t\t// compare array values while ignoring keys and order\n\t\t$this->assertEqualSets( $created_term_ids, $membership_term_ids );\n\t}\n\n\t/**\n\t * Test get_product()\n\t *\n\t * @since 4.15.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_product() {\n\n\t\t$membership = $this->factory->membership->create_and_get();\n\n\t\t$this->assertInstanceOf( 'LLMS_Product', $membership->get_product() );\n\n\t}\n\n\t/**\n\t * Test LLMS_Membership->get_tags() method.\n\t *\n\t * @since 3.36.3\n\t * @return void\n\t */\n\tpublic function test_get_tags() {\n\t\t// create new membership\n\t\t$membership_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) );\n\t\t$membership    = new LLMS_Membership( $membership_id );\n\n\t\t// create new tags\n\t\t$taxonomy = 'membership_tag';\n\t\t$created_term_ids = array();\n\t\tfor ( $i = 1; $i <= 3; $i ++ ) {\n\t\t\t$new_term_ids = wp_create_term( \"mock-membership-tag-$i\", $taxonomy );\n\t\t\t$this->assertNotWPError( $new_term_ids );\n\t\t\t$created_term_ids[ $i ] = $new_term_ids['term_id'];\n\t\t}\n\n\t\t// set tags in membership\n\t\t$term_taxonomy_ids = wp_set_post_terms( $membership_id, $created_term_ids, $taxonomy );\n\t\t$this->assertNotWPError( $term_taxonomy_ids );\n\t\t$this->assertNotFalse( $term_taxonomy_ids );\n\n\t\t// get tags from membership\n\t\t$membership_terms = $membership->get_tags();\n\t\t$membership_term_ids = array();\n\t\t/** @var WP_Term $membership_term */\n\t\tforeach ( $membership_terms as $membership_term ) {\n\t\t\t$membership_term_ids[] = $membership_term->term_id;\n\t\t}\n\n\t\t// compare array values while ignoring keys and order\n\t\t$this->assertEqualSets( $created_term_ids, $membership_term_ids );\n\t}\n\n\t/**\n\t * Test get_sales_page_url method.\n\t *\n\t * @since 3.20.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_sales_page_url() {\n\n\t\t$course = new LLMS_Membership( 'new', 'Membership Title' );\n\n\t\t$this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'none' );\n\t\t$this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'content' );\n\t\t$this->assertEquals( get_permalink( $course->get( 'id' ) ), $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'url' );\n\t\t$course->set( 'sales_page_content_url', 'https://lifterlms.com' );\n\t\t$this->assertEquals( 'https://lifterlms.com', $course->get_sales_page_url() );\n\n\t\t$course->set( 'sales_page_content_type', 'page' );\n\t\t$page = $this->factory->post->create();\n\t\t$course->set( 'sales_page_content_page_id', $page );\n\t\t$this->assertEquals( get_permalink( $page ), $course->get_sales_page_url() );\n\n\t}\n\n\t/**\n\t * Test the get students function.\n\t *\n\t * @since 3.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_students() {\n\n\t\t$this->create();\n\n\t\t$students = $this->factory->user->create_many( 10, array( 'role' => 'student' ) );\n\t\tforeach ( $students as $sid ) {\n\t\t\tllms_enroll_student( $sid, $this->obj->get( 'id' ), 'testing' );\n\t\t}\n\n\t\t$this->assertEquals( 5, count( $this->obj->get_students( array( 'enrolled' ), 5 ) ) );\n\t\t$this->assertEquals( 10, count( $this->obj->get_students() ) );\n\n\t}\n\n\t/**\n\t * Test the `has_sales_page_redirect` method.\n\t *\n\t * @since 3.20.0\n\t * @since 5.2.1 Add checks for empty URL and page ID.\n\t */\n\tpublic function test_has_sales_page_redirect() {\n\n\t\t$membership = new LLMS_Membership( 'new', 'Membership Title' );\n\n\t\t$this->assertEquals( false, $membership->has_sales_page_redirect() );\n\n\t\t$membership->set( 'sales_page_content_type', 'none' );\n\t\t$this->assertEquals( false, $membership->has_sales_page_redirect() );\n\n\t\t$membership->set( 'sales_page_content_type', 'content' );\n\t\t$this->assertEquals( false, $membership->has_sales_page_redirect() );\n\n\t\t$membership->set( 'sales_page_content_type', 'url' );\n\t\t$this->assertEquals( false, $membership->has_sales_page_redirect() );\n\n\t\t$membership->set( 'sales_page_content_url', 'https://lifterlms.com' );\n\t\t$this->assertEquals( true, $membership->has_sales_page_redirect() );\n\n\t\t$membership->set( 'sales_page_content_type', 'page' );\n\t\t$this->assertEquals( false, $membership->has_sales_page_redirect() );\n\n\t\t$page_id = $this->factory()->post->create( array( 'post_type' => 'page' ) );\n\t\t$membership->set( 'sales_page_content_page_id', $page_id );\n\t\t$this->assertEquals( true, $membership->has_sales_page_redirect() );\n\n\t}\n\n\t/**\n\t * Test query_associated_courses() to ensure only plan associations from published courses are returned.\n\t *\n\t * @since 4.15.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms-groups/issues/135\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_associated_courses_published_only() {\n\n\t\t$membership = $this->factory->membership->create_and_get();\n\n\t\t$plan = $this->get_mock_plan();\n\t\t$plan->set( 'availability', 'members' );\n\t\t$plan->set( 'availability_restrictions', array( 1, $membership->get( 'id' ) ) );\n\n\t\t$course = llms_get_post( $plan->get( 'product_id' ) );\n\t\t$course->set( 'status', 'draft' );\n\n\t\t$this->assertEquals( array(), LLMS_Unit_Test_Util::call_method( $membership, 'query_associated_courses' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-order.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Course Model\n *\n * @package LifterLMS/Tests\n *\n * @group orders\n * @group LLMS_Order\n * @group LLMS_Post_Model\n *\n * @since 3.10.0\n * @since 3.32.0 Update to use latest action-scheduler functions.\n * @since 3.37.2 Add additional recurring payment tests.\n * @since 3.37.6 Adjusted date delta for recurring payment next date assertions.\n *               Added default test override for test_edit_date() test to prevent output\n *               of skipped test that doesn't apply to the order model.\n * @since 4.6.0 Add coverage for `get_next_scheduled_action_time()`, `unschedule_expiration()`, and `unschedule_recurring_payment()`.\n * @since 4.7.0 Update tests to handle new meta property `plan_ended`.\n */\nclass LLMS_Test_LLMS_Order extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Consider dates equal for +/- 2 mins\n\t * @var  integer\n\t */\n\tprivate $date_delta = 120;\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since Unknown\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->create();\n\t}\n\n\t/**\n\t * Add support for a payment gateway feature.\n\t *\n\t * @since Unknown\n\t *\n\t * @param string $feature Feature name\n\t * @return void\n\t */\n\tprivate function mock_gateway_support( $feature ) {\n\n\t\tglobal $llms_mock_gateway_feature;\n\t\t$llms_mock_gateway_feature = $feature;\n\n\t\tadd_filter( 'llms_get_gateway_supported_features', function( $features ) {\n\t\t\tglobal $llms_mock_gateway_feature;\n\t\t\t$features[ $llms_mock_gateway_feature ] = true;\n\t\t\treturn $features;\n\t\t} );\n\n\t}\n\n\tprivate function get_plan( $price = 25.99, $frequency = 1, $expiration = 'lifetime', $on_sale = false, $trial = false ) {\n\n\t\treturn $this->get_mock_plan( $price, $frequency, $expiration, $on_sale, $trial );\n\n\t}\n\n\tprivate function get_order( $plan = null, $coupon = false ) {\n\n\t\treturn $this->get_mock_order( $plan, $coupon );\n\n\t}\n\n\t/**\n\t * Class name for the model being tested by the class\n\t *\n\t * @var  string\n\t */\n\tprotected $class_name = 'LLMS_Order';\n\n\t/**\n\t * Db post type of the model being tested\n\t *\n\t * @var  string\n\t */\n\tprotected $post_type = 'llms_order';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t *\n\t * This should match, exactly, the object's $properties array\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return string[]\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\n\t\t\t'coupon_amount' => 'float',\n\t\t\t'coupon_amout_trial' => 'float',\n\t\t\t'coupon_value' => 'float',\n\t\t\t'coupon_value_trial' => 'float',\n\t\t\t'original_total' => 'float',\n\t\t\t'sale_price' => 'float',\n\t\t\t'sale_value' => 'float',\n\t\t\t'total' => 'float',\n\t\t\t'trial_original_total' => 'float',\n\t\t\t'trial_total' => 'float',\n\n\t\t\t'access_length' => 'absint',\n\t\t\t'billing_frequency' => 'absint',\n\t\t\t'billing_length' => 'absint',\n\t\t\t'coupon_id' => 'absint',\n\t\t\t'plan_id' => 'absint',\n\t\t\t'product_id' => 'absint',\n\t\t\t'trial_length' => 'absint',\n\t\t\t'user_id' => 'absint',\n\n\t\t\t'access_expiration' => 'text',\n\t\t\t'access_expires' => 'text',\n\t\t\t'access_period' => 'text',\n\t\t\t'billing_address_1' => 'text',\n\t\t\t'billing_address_2' => 'text',\n\t\t\t'billing_city' => 'text',\n\t\t\t'billing_country' => 'text',\n\t\t\t'billing_email' => 'text',\n\t\t\t'billing_first_name' => 'text',\n\t\t\t'billing_last_name' => 'text',\n\t\t\t'billing_state' => 'text',\n\t\t\t'billing_zip' => 'text',\n\t\t\t'billing_period' => 'text',\n\t\t\t'coupon_code' => 'text',\n\t\t\t'coupon_type' => 'text',\n\t\t\t'coupon_used' => 'text',\n\t\t\t'currency' => 'text',\n\t\t\t'on_sale' => 'text',\n\t\t\t'order_key' => 'text',\n\t\t\t'order_type' => 'text',\n\t\t\t'payment_gateway' => 'text',\n\t\t\t'plan_sku' => 'text',\n\t\t\t'plan_title' => 'text',\n\t\t\t'product_sku' => 'text',\n\t\t\t'product_type' => 'text',\n\t\t\t'title' => 'text',\n\t\t\t'gateway_api_mode' => 'text',\n\t\t\t'gateway_customer_id' => 'text',\n\t\t\t'trial_offer' => 'text',\n\t\t\t'trial_period' => 'text',\n\t\t\t'user_ip_address' => 'text',\n\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters.\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\n\t\t\t'coupon_amount' => 1.00,\n\t\t\t'coupon_amout_trial' => 0.50,\n\t\t\t'coupon_value' => 1.00,\n\t\t\t'coupon_value_trial' => 1234234.00,\n\t\t\t'original_total' => 25.93,\n\t\t\t'sale_price' => 25.23,\n\t\t\t'sale_value' => 2325.00,\n\t\t\t'total' => 12325.00,\n\t\t\t'trial_original_total' => 25.00,\n\t\t\t'trial_total' => 123.43,\n\n\t\t\t'access_length' => 1,\n\t\t\t'billing_frequency' => 1,\n\t\t\t'billing_length' => 1,\n\t\t\t'coupon_id' => 1,\n\t\t\t'plan_id' => 1,\n\t\t\t'product_id' => 1,\n\t\t\t'trial_length' => 1,\n\t\t\t'user_id' => 1,\n\n\t\t\t'access_expiration' => 'text',\n\t\t\t'access_expires' => 'text',\n\t\t\t'access_period' => 'text',\n\t\t\t'billing_address_1' => 'text',\n\t\t\t'billing_address_2' => 'text',\n\t\t\t'billing_city' => 'text',\n\t\t\t'billing_country' => 'text',\n\t\t\t'billing_email' => 'text',\n\t\t\t'billing_first_name' => 'text',\n\t\t\t'billing_last_name' => 'text',\n\t\t\t'billing_state' => 'text',\n\t\t\t'billing_zip' => 'text',\n\t\t\t'billing_period' => 'text',\n\t\t\t'coupon_code' => 'text',\n\t\t\t'coupon_type' => 'text',\n\t\t\t'coupon_used' => 'text',\n\t\t\t'currency' => 'text',\n\t\t\t'on_sale' => 'text',\n\t\t\t'order_key' => 'text',\n\t\t\t'order_type' => 'single',\n\t\t\t'payment_gateway' => 'text',\n\t\t\t'plan_sku' => 'text',\n\t\t\t'plan_title' => 'text',\n\t\t\t'product_sku' => 'text',\n\t\t\t'product_type' => 'text',\n\t\t\t'title' => 'test title',\n\t\t\t'gateway_api_mode' => 'text',\n\t\t\t'gateway_customer_id' => 'text',\n\t\t\t'trial_offer' => 'text',\n\t\t\t'trial_period' => 'text',\n\t\t\t'user_ip_address' => 'text',\n\n\t\t);\n\t}\n\n\t/**\n\t * Test the add_note() method\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_note() {\n\n\t\t// Don't create empty notes.\n\t\t$this->assertNull( $this->obj->add_note( '' ) );\n\n\t\t$note_text = 'This is an order note';\n\t\t$id = $this->obj->add_note( $note_text );\n\n\t\t// Should return the comment id.\n\t\t$this->assertTrue( is_numeric( $id ) );\n\n\t\t$note = get_comment( $id );\n\n\t\t// Should be a comment.\n\t\t$this->assertTrue( is_a( $note, 'WP_Comment' ) );\n\n\t\t// Comment content should be our original note.\n\t\t$this->assertEquals( $note->comment_content, $note_text );\n\t\t// Author should be the system (LifterLMS).\n\t\t$this->assertEquals( $note->comment_author, 'LifterLMS' );\n\n\t\t// Create a new note by a user.\n\t\t$id = $this->obj->add_note( $note_text, true );\n\t\t$note = get_comment( $id );\n\t\t$this->assertEquals( get_current_user_id(), $note->user_id );\n\n\t\t// 1 for original creation note, 2 for our test notes.\n\t\t$this->assertEquals( 3, did_action( 'llms_new_order_note_added' ) );\n\n\t}\n\n\t/**\n\t * Test can_be_confirmed().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_be_confirmed() {\n\n\t\t$statuses = array(\n\t\t\t// Can be confirmed.\n\t\t\t'llms-pending'        => true,\n\n\t\t\t// Cannot be confirmed.\n\t\t\t'llms-completed'      => false,\n\t\t\t'llms-active'         => false,\n\t\t\t'llms-expired'        => false,\n\t\t\t'llms-on-hold'        => false,\n\t\t\t'llms-pending-cancel' => false,\n\t\t\t'llms-cancelled'      => false,\n\t\t\t'llms-refunded'       => false,\n\t\t\t'llms-failed'         => false,\n\t\t);\n\n\t\tforeach ( $statuses as $status => $expected ) {\n\t\t\t$this->obj->set_status( $status );\n\t\t\t$this->assertEquals( $expected, $this->obj->can_be_confirmed() );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the can_be_retried() method.\n\t *\n\t * @since Unknown.\n\t * @since 5.2.1 Add assertions for checking against single payment orders and\n\t *              when the recurring retry feature option is disabled.\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_be_retried() {\n\n\t\t$order = $this->get_order();\n\n\t\t// Pending order can't be retried.\n\t\t$this->assertFalse( $order->can_be_retried() );\n\n\t\t// Active can be retried.\n\t\t$order->set_status( 'llms-active' );\n\n\t\t// Gateway doesn't support retries.\n\t\t$this->assertFalse( $order->can_be_retried() );\n\n\t\t// Allow the gateway to support retries.\n\t\t$this->mock_gateway_support( 'recurring_retry' );\n\n\t\t// Can be retried now.\n\t\t$this->assertTrue( $order->can_be_retried() );\n\n\t\t// On hold can be retried.\n\t\t$order->set_status( 'llms-on-hold' );\n\t\t$this->assertTrue( $order->can_be_retried() );\n\n\t\t// Retry disabled.\n\t\tupdate_option( 'lifterlms_recurring_payment_retry', 'no' );\n\t\t$this->assertFalse( $order->can_be_retried() );\n\t\tupdate_option( 'lifterlms_recurring_payment_retry', 'yes' );\n\n\t\t// Single payment cannot be retried.\n\t\t$order->set( 'order_type', 'single' );\n\t\t$this->assertFalse( $order->can_be_retried() );\n\n\t}\n\n\t/**\n\t * Test the can_resubscribe() method\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_resubscribe() {\n\n\t\t$statuses = array(\n\t\t\t'llms-completed'      => false,\n\t\t\t'llms-active'         => false,\n\t\t\t'llms-expired'        => false,\n\t\t\t'llms-on-hold'        => true,\n\t\t\t'llms-pending-cancel' => true,\n\t\t\t'llms-pending'        => true,\n\t\t\t'llms-cancelled'      => false,\n\t\t\t'llms-refunded'       => false,\n\t\t\t'llms-failed'         => false,\n\t\t);\n\n\t\tforeach ( $statuses as $status => $expect ) {\n\n\t\t\t$this->obj->set( 'order_type', 'single' );\n\t\t\t$this->obj->set_status( $status );\n\t\t\t$this->assertEquals( false, $this->obj->can_resubscribe() );\n\n\t\t\t$this->obj->set( 'order_type', 'recurring' );\n\t\t\t$this->obj->set_status( $status );\n\t\t\t$this->assertEquals( $expect, $this->obj->can_resubscribe() );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test can_switch_source().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_switch_source() {\n\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\n\t\t$statuses = array(\n\t\t\t'llms-completed'      => false,\n\t\t\t'llms-active'         => true,\n\t\t\t'llms-expired'        => false,\n\t\t\t'llms-on-hold'        => true,\n\t\t\t'llms-pending-cancel' => true,\n\t\t\t'llms-pending'        => true,\n\t\t\t'llms-cancelled'      => false,\n\t\t\t'llms-refunded'       => false,\n\t\t\t'llms-failed'         => false,\n\t\t);\n\n\t\tforeach ( $statuses as $status => $expect ) {\n\n\t\t\t$this->obj->set( 'status', $status );\n\t\t\t$this->assertEquals( $expect, $this->obj->can_switch_source(), $status );\n\t\t}\n\n\n\t}\n\n\t/**\n\t * Test creation of the model.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_model() {\n\n\t\t$date = '2021-04-22 14:34:00';\n\t\tllms_tests_mock_current_time( $date );\n\n\t\t$this->create( '' );\n\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t$test = llms_get_post( $id );\n\n\t\t$this->assertEquals( $id, $test->get( 'id' ) );\n\t\t$this->assertEquals( 'llms_order', $test->get( 'type' ) );\n\t\t$this->assertEquals( 'Order &ndash; Apr 22, 2021 @ 02:34 PM', $test->get( 'title' ) );\n\n\t\t$this->assertEquals( $date, $test->get( 'date' ) );\n\t\t$this->assertEquals( 'llms-pending', $test->get( 'status' ) );\n\n\t\t$this->assertTrue( post_password_required( $id ) );\n\n\t}\n\n\n\t/**\n\t * Overrides test from the abstract\n\t *\n\t * Since this test isn't essential for this class we'll skip the test with a fake assertion\n\t * in order to reduce the number of skipped tests warnings which are output.\n\t *\n\t * @since 3.37.6\n\t *\n\t * @return void\n\t */\n\tpublic function test_edit_date() {\n\t\t$this->assertTrue( true );\n\t}\n\n\t/**\n\t * Test the generate_order_key() method\n\t *\n\t * @since 3.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_order_key() {\n\n\t\t$this->assertTrue( is_string( $this->obj->generate_order_key() ) );\n\t\t$this->assertEquals( 0, strpos( $this->obj->generate_order_key(), 'order-' ) );\n\n\t}\n\n\t/**\n\t * Test the get_access_expiration_date() method\n\t *\n\t * @since 3.10.0\n\t * @since 3.19.0 Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_access_expiration_date() {\n\n\t\t// Lifetime responds with a string not a date.\n\t\t$this->obj->set( 'access_expiration', 'lifetime' );\n\t\t$this->assertEquals( 'Lifetime Access', $this->obj->get_access_expiration_date() );\n\n\t\t// Expires on a specific date.\n\t\t$this->obj->set( 'access_expiration', 'limited-date' );\n\t\t$this->obj->set( 'access_expires', '12/01/2020' ); // m/d/Y format (from datepicker).\n\t\t$this->assertEquals( '2020-12-01', $this->obj->get_access_expiration_date() );\n\n\t\t// Expires after a period of time.\n\t\t$this->obj->set( 'access_expiration', 'limited-period' );\n\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\t'start' => '05/25/2015',\n\t\t\t\t'length' => '1',\n\t\t\t\t'period' => 'week',\n\t\t\t\t'expect' => '06/01/2015',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'start' => '12/21/2017',\n\t\t\t\t'length' => '1',\n\t\t\t\t'period' => 'day',\n\t\t\t\t'expect' => '12/22/2017',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'start' => '02/05/2017',\n\t\t\t\t'length' => '1',\n\t\t\t\t'period' => 'year',\n\t\t\t\t'expect' => '02/05/2018',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'start' => '12/31/2017',\n\t\t\t\t'length' => '1',\n\t\t\t\t'period' => 'day',\n\t\t\t\t'expect' => '01/01/2018',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'start' => '05/01/2017',\n\t\t\t\t'length' => '2',\n\t\t\t\t'period' => 'month',\n\t\t\t\t'expect' => '07/01/2017',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\n\t\t\t$this->obj->set( 'start_date', $data['start'] );\n\t\t\t$this->obj->set( 'access_length', $data['length'] );\n\t\t\t$this->obj->set( 'access_period', $data['period'] );\n\t\t\t$this->assertEquals( date_i18n( 'Y-m-d', strtotime( $data['expect'] ) ), $this->obj->get_access_expiration_date() );\n\n\t\t}\n\n\t\t// Recurring pending cancel has access until the next payment due date.\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\t\t$this->obj->set( 'status', 'llms-pending-cancel' );\n\t\t$this->assertEquals( $this->obj->get_next_payment_due_date( 'U' ), $this->obj->get_access_expiration_date( 'U' ) );\n\n\t}\n\n\t/**\n\t * Test get access status function\n\t *\n\t * @since 3.10.0\n\t * @since 3.19.0 Unknown.\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_access_status() {\n\n\t\t$this->assertEquals( 'inactive', $this->obj->get_access_status() );\n\n\t\t$this->obj->set( 'order_type', 'single' );\n\t\t$this->obj->set( 'status', 'llms-active' );\n\t\t$this->assertEquals( 'active', $this->obj->get_access_status() );\n\n\t\t$this->obj->set( 'status', 'llms-completed' );\n\t\t$this->obj->set( 'access_expiration', 'lifetime' );\n\t\t$this->assertEquals( 'active', $this->obj->get_access_status() );\n\n\t\t// Past should still grant access.\n\t\tllms_tests_mock_current_time( '2010-05-05' );\n\t\t$this->assertEquals( 'active', $this->obj->get_access_status() );\n\n\t\t// Future should still grant access.\n\t\tllms_tests_mock_current_time( '2525-05-05' );\n\t\t$this->assertEquals( 'active', $this->obj->get_access_status() );\n\n\t\t// Check limited access by date.\n\t\t$this->obj->set( 'access_expiration', 'limited-date' );\n\t\t$tests = array(\n\t\t\tarray(\n\t\t\t\t'now' => '2010-05-05',\n\t\t\t\t'expires' => '05/06/2010', // m/d/Y from datepicker.\n\t\t\t\t'expect' => 'active',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'now' => '2015-05-05',\n\t\t\t\t'expires' => '05/06/2010', // m/d/Y from datepicker.\n\t\t\t\t'expect' => 'expired',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'now' => '2010-05-05',\n\t\t\t\t'expires' => '05/05/2010', // m/d/Y from datepicker.\n\t\t\t\t'expect' => 'active',\n\t\t\t),\n\t\tarray(\n\t\t\t\t'now' => '2010-05-06',\n\t\t\t\t'expires' => '05/05/2010', // m/d/Y from datepicker.\n\t\t\t\t'expect' => 'expired',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $data ) {\n\t\t\tllms_tests_mock_current_time( $data['now'] );\n\t\t\t$this->obj->set( 'access_expires', $data['expires'] );\n\t\t\t$this->assertEquals( $data['expect'], $this->obj->get_access_status() );\n\t\t\tif ( 'active' === $data['expect'] ) {\n\t\t\t\t$this->assertTrue( $this->obj->has_access() );\n\t\t\t} else {\n\t\t\t\t$this->assertFalse( $this->obj->has_access() );\n\t\t\t}\n\t\t}\n\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\t\t$this->obj->set( 'status', 'llms-pending-cancel' );\n\t\t$this->assertEquals( 'active', $this->obj->get_access_status() );\n\n\t}\n\n\t/**\n\t * Test the get_customer_name() method.\n\t *\n\t * @since Unknown\n\t * @since 5.2.1 Add assertion for anonymized order.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_customer_name() {\n\t\t$first = 'Jeffrey';\n\t\t$last = 'Lebowski';\n\t\t$this->obj->set( 'billing_first_name', $first );\n\t\t$this->obj->set( 'billing_last_name', $last );\n\t\t$this->assertEquals( $first . ' ' . $last,  $this->obj->get_customer_name() );\n\n\t\t$this->obj->set( 'anonymized', 'yes' );\n\t\t$this->assertEquals( 'Anonymous', $this->obj->get_customer_name() );\n\n\t}\n\n\t/**\n\t * Test the get_gateway() method.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_gateway() {\n\n\t\t// Gateway doesn't exist.\n\t\t$this->obj->set( 'payment_gateway', 'garbage' );\n\t\t$this->assertTrue( is_a( $this->obj->get_gateway(), 'WP_Error' ) );\n\n\t\t$manual = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\t$this->obj->set( 'payment_gateway', 'manual' );\n\n\t\t// Real gateway that's not enabled.\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), 'no' );\n\t\t$this->assertTrue( is_a( $this->obj->get_gateway(), 'WP_Error' ) );\n\n\t\t// Enabled gateway responds with the gateway instance.\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), 'yes' );\n\t\t$this->assertTrue( is_a( $this->obj->get_gateway(), 'LLMS_Payment_Gateway_Manual' ) );\n\n\t}\n\n\t/**\n\t * Test get_initial_price() method\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_initial_price() {\n\n\t\t// No trial.\n\t\t$order = $this->get_order();\n\t\t$this->assertEquals( 25.99, $order->get_initial_price( array(), 'float' ) );\n\n\t\t// With trial.\n\t\t$trial_plan = $this->get_plan( 25.99, 1, 'lifetime', false, true );\n\t\t$order = $this->get_order( $trial_plan );\n\t\t$this->assertEquals( 1.00, $order->get_initial_price( array(), 'float' ) );\n\n\t}\n\n\t/**\n\t * Test get_notes() method\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notes() {\n\n\t\t$i = 1;\n\t\twhile( $i <= 10 ) {\n\n\t\t\t$this->obj->add_note( sprintf( 'note %d', $i ) );\n\t\t\t$i++;\n\n\t\t}\n\n\t\t// Remove filter so we can test order notes.\n\t\tremove_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\n\t\t$notes = $this->obj->get_notes( 1, 1 );\n\n\t\t$this->assertCount( 1, $notes );\n\t\t$this->assertTrue( is_a( $notes[0], 'WP_Comment' ) );\n\n\t\t$notes_p_1 = $this->obj->get_notes( 5, 1 );\n\t\t$notes_p_2 = $this->obj->get_notes( 5, 2 );\n\t\t$this->assertCount( 5, $notes_p_1 );\n\t\t$this->assertCount( 5, $notes_p_2 );\n\t\t$this->assertTrue( $notes_p_2 !== $notes_p_1 );\n\n\t\tadd_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\n\t}\n\n\t/**\n\t * Test get_product() method\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_product() {\n\n\t\t$course = new LLMS_Course( 'new', 'test' );\n\t\t$this->obj->set( 'product_id', $course->get( 'id' ) );\n\t\t$this->assertTrue( is_a( $this->obj->get_product(), 'LLMS_Course' ) );\n\n\t}\n\n/**\n\t * Test get_last_transaction() on a one-time payment.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_last_transaction_single() {\n\n\t\t$this->assertFalse( $this->obj->get_last_transaction() );\n\n\t\t// Record a transaction.\n\t\t$txn = $this->obj->record_transaction( array(\n\t\t\t'amount'         => 25.99,\n\t\t\t'completed_date' => current_time( 'Y-m-d H:i:s' ),\n\t\t\t'status'         => 'llms-txn-succeeded',\n\t\t\t'payment_type'   => 'single',\n\t\t) );\n\n\t\t$this->assertEquals( $this->obj->get_last_transaction(), $txn );\n\t\t$this->assertEquals(\n\t\t\t$this->obj->get_last_transaction( 'llms-txn-succeeded', 'single'),\n\t\t\t$txn\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->obj->get_last_transaction( 'llms-txn-failed', 'single'),\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->obj->get_last_transaction( 'any', 'recurring' )\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->obj->get_last_transaction( 'any', 'trial' )\n\t\t);\n\t}\n\n\t/**\n\t * Test get_last_transaction() on a recurring payments order.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_last_transaction_recurring() {\n\n\t\t$this->assertFalse( $this->obj->get_last_transaction() );\n\n\t\t// Record a transaction.\n\t\t$txn_time = date_i18n( 'Y-m-d H:i:s', strtotime( '-1 day' ) );\n\t\t$txn      = $this->obj->record_transaction( array(\n\t\t\t'amount'         => 25.99,\n\t\t\t'completed_date' => $txn_time,\n\t\t\t'status'         => 'llms-txn-succeeded',\n\t\t\t'payment_type'   => 'recurring',\n\t\t) );\n\n\t\t// Change published date to reflect the completed date.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'        => $txn->get( 'id' ),\n\t\t\t\t'post_date' => $txn_time,\n\t\t\t)\n\t\t);\n\n\t\t// Hydrate.\n\t\t$txn = llms_get_post( $txn->get( 'id' ) );\n\t\t$this->assertEquals( $this->obj->get_last_transaction(), $txn );\n\t\t$this->assertEquals(\n\t\t\t$this->obj->get_last_transaction( 'llms-txn-succeeded', 'recurring'),\n\t\t\t$txn\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->obj->get_last_transaction( 'llms-txn-failed', 'recurring'),\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->obj->get_last_transaction( 'any', 'single' )\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$this->obj->get_last_transaction( 'any', 'trial' )\n\t\t);\n\n\t\t// Record a new transaction.\n\t\t$txn = $this->obj->record_transaction( array(\n\t\t\t'amount'         => 25.99,\n\t\t\t'completed_date' => current_time( 'Y-m-d H:i:s' ),\n\t\t\t'status'         => 'llms-txn-succeeded',\n\t\t\t'payment_type'   => 'recurring',\n\t\t) );\n\t\t$this->assertEquals( $txn, $this->obj->get_last_transaction() );\n\n\t}\n\n\t/**\n\t * Test get_last_transaction() on a recurring payment order with trial.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_last_transaction_recurring_with_trial() {\n\n\t\t$trial_plan = $this->get_plan( 25.99, 1, 'lifetime', false, true );\n\t\t$order      = $this->get_order( $trial_plan );\n\n\t\t$this->assertFalse( $order->get_last_transaction() );\n\n\t\t// Record a transaction.\n\t\t$txn_time = current_time( 'Y-m-d H:i:s' );\n\t\t$txn_rec  = $order->record_transaction( array(\n\t\t\t'amount'         => 25.99,\n\t\t\t'completed_date' => $txn_time,\n\t\t\t'status'         => 'llms-txn-succeeded',\n\t\t\t'payment_type'   => 'recurring',\n\t\t) );\n\n\t\t// Change published date to reflect the completed date.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'        => $txn_rec->get( 'id' ),\n\t\t\t\t'post_date' => $txn_time,\n\t\t\t)\n\t\t);\n\n\t\t// Hydrate.\n\t\t$txn_rec = llms_get_post( $txn_rec->get( 'id' ) );\n\t\t$this->assertEquals( $order->get_last_transaction(), $txn_rec );\n\t\t$this->assertEquals(\n\t\t\t$order->get_last_transaction( 'llms-txn-succeeded', 'recurring'),\n\t\t\t$txn_rec\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$order->get_last_transaction( 'llms-txn-failed', 'recurring'),\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$order->get_last_transaction( 'any', 'single' )\n\t\t);\n\t\t$this->assertFalse(\n\t\t\t$order->get_last_transaction( 'any', 'trial' )\n\t\t);\n\n\t\t// Record a new transaction, mimicking a trial paid yestarday.\n\t\t$txn_time = date_i18n( 'Y-m-d H:i:s', strtotime( '-1 day' ) );\n\t\t$txn = $order->record_transaction( array(\n\t\t\t'amount'         => 25.99,\n\t\t\t'completed_date' => $txn_time,\n\t\t\t'status'         => 'llms-txn-succeeded',\n\t\t\t'payment_type'   => 'trial',\n\t\t) );\n\n\t\t// Change published date to reflect the completed date.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'        => $txn->get( 'id' ),\n\t\t\t\t'post_date' => $txn_time,\n\t\t\t)\n\t\t);\n\n\t\t// Hydrate.\n\t\t$txn = llms_get_post( $txn->get( 'id' ) );\n\t\t// The last transaction is still the recurring one, being newer.\n\t\t$this->assertEquals( $txn_rec, $order->get_last_transaction() );\n\n\t}\n\n\t// public function test_get_last_transaction_date() {}\n\n\t/**\n\t * Test get_next_payment_due_date() for a one-time payment\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_next_payment_due_date_single() {\n\n\t\t$plan = $this->get_plan( 25.99, 0 );\n\t\t$order = $this->get_order( $plan );\n\t\t$this->assertTrue( is_a( $order->get_next_payment_due_date(), 'WP_Error' ) );\n\n\t}\n\n\t/**\n\t * Test get_next_payment_due_date() for recurring payments\n\t *\n\t * @since Unknown.\n\t * @since 3.37.6 Adjusted delta on date comparison to allow 2 hours difference when calculating recurring payment dates.\n\t * @since 5.3.0 Don't rely on the date_billing_end property for ending a payment plan.\n\t * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of 4th parameter provided to `assertEquals()`.\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_next_payment_due_date_recurring() {\n\n\t\t// Set time to early in the day since we're scheduling three of them, and it'll start to go beyond a single day.\n\t\t$original_time = current_time( 'Y-m-d ' ) . ' 01:00:00';\n\n\t\t$plan = $this->get_plan();\n\t\tforeach ( array( 'day', 'week', 'month', 'year' ) as $period ) {\n\n\t\t\tllms_tests_mock_current_time( $original_time );\n\n\t\t\t$plan->set( 'period', $period );\n\n\t\t\t// Test due date with a trial.\n\t\t\t$plan->set( 'trial_offer', 'yes' );\n\t\t\t$order = $this->get_order( $plan );\n\t\t\t$this->assertEqualsWithDelta( strtotime( $order->get_trial_end_date() ), strtotime( $order->get_next_payment_due_date() ), $this->date_delta );\n\t\t\t$plan->set( 'trial_offer', 'no' );\n\n\t\t\t// Perform calculation tests against different frequencies.\n\t\t\t$i = 1;\n\t\t\twhile ( $i <= 3 ) {\n\n\t\t\t\t$plan->set( 'frequency', $i );\n\n\t\t\t\t$order = $this->get_order( $plan );\n\n\t\t\t\t$expect = strtotime( \"+{$i} {$period}\", $order->get_date( 'date', 'U' ) );\n\t\t\t\t$this->assertEquals( $expect, $order->get_next_payment_due_date( 'U') );\n\n\t\t\t\t// Time travel a bit and recalculate the time.\n\t\t\t\tllms_tests_mock_current_time( date( 'Y-m-d H:i:s', $expect + HOUR_IN_SECONDS * 2 ) );\n\t\t\t\t$future_expect = strtotime( \"+{$i} {$period}\", $expect );\n\n\t\t\t\t// This will calculate the next payment date based off of the saved next payment date (which is now in the past).\n\t\t\t\t$order->maybe_schedule_payment( true );\n\t\t\t\t$this->assertEquals( $future_expect, $order->get_next_payment_due_date( 'U' ) );\n\n\t\t\t\t// Recalculate off a transaction -- this is the fallback for pre 3.10 orders.\n\t\t\t\t// Occurs only when no date_next_payment is set.\n\t\t\t\t$order->set( 'date_next_payment', '' );\n\t\t\t\t$order->record_transaction( array(\n\t\t\t\t\t'amount' => 25.99,\n\t\t\t\t\t'completed_date' => $original_time,\n\t\t\t\t\t'status' => 'llms-txn-succeeded',\n\t\t\t\t\t'payment_type' => 'recurring',\n\t\t\t\t) );\n\t\t\t\t$order->maybe_schedule_payment( true );\n\n\t\t\t\t$this->assertEqualsWithDelta( strtotime( date( 'Y-m-d H:i:s', $future_expect ) ), strtotime( $order->get_next_payment_due_date( 'Y-m-d H:i:s' ) ), HOUR_IN_SECONDS * 2 );\n\n\t\t\t\t// Plan ended so func should return a WP_Error.\n\t\t\t\t$order->set( 'billing_length', 1 );\n\t\t\t\t$order->maybe_schedule_payment( true );\n\t\t\t\t$date = $order->get_next_payment_due_date();\n\t\t\t\t$this->assertIsWPError( $date );\n\t\t\t\t$this->assertWPErrorCodeEquals( 'plan-ended', $date );\n\t\t\t\t$this->assertEquals( 'yes', $order->get( 'plan_ended' ) );\n\t\t\t\t$order->set( 'billing_length', 0 );\n\n\t\t\t\t$i++;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_next_payment_due_date() method for a payment plan\n\t *\n\t * Additionally tests calculate_next_payment_date() via action hooks.\n\t *\n\t * @since Unknown\n\t * @since 5.3.0 Updated to rely on number of successful transactions in favor of the current date.\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_next_payment_due_date_payment_plan() {\n\n\t\t$original_time = current_time( 'Y-m-d H:i:s' );\n\n\t\tllms_tests_mock_current_time( $original_time );\n\n\t\t// This should run 3 total payments over the course of 9 weeks.\n\t\t$plan = $this->get_plan();\n\t\t$plan->set( 'frequency', 3 ); // Every 3rd.\n\t\t$plan->set( 'period', 'week' ); // Week.\n\t\t$plan->set( 'length', 3 ); // For 3 payments.\n\n\t\t// Create the order.\n\t\t$order = $this->get_order( $plan );\n\n\t\t// 3 total payments due.\n\t\t$this->assertEquals( 3, $order->get_remaining_payments() );\n\n\t\t// Make the initial payment.\n\t\t$order->record_transaction( array(\n\t\t\t'payment_type' => 'recurring',\n\t\t\t'status'       => 'llms-txn-succeeded',\n\t\t) );\n\n\t\t// Two payments remaining.\n\t\t$this->assertEquals( 2, $order->get_remaining_payments() );\n\n\t\t// Payment two is scheduled properly.\n\t\t$expect = strtotime( \"+3 weeks\", $order->get_date( 'date', 'U' ) );\n\t\t$this->assertEquals( $expect, $order->get_next_payment_due_date( 'U' ) );\n\n\t\t// Time travel to when the second payment is due.\n\t\tllms_tests_mock_current_time( date( 'Y-m-d H:i:s', $expect ) );\n\n\t\t// Record the second payment.\n\t\t$order->record_transaction( array(\n\t\t\t'payment_type' => 'recurring',\n\t\t\t'status'       => 'llms-txn-succeeded',\n\t\t) );\n\n\t\t// Only one payment remaining.\n\t\t$this->assertEquals( 1, $order->get_remaining_payments() );\n\n\t\t// Payment 3 is scheduled properly.\n\t\t$expect += WEEK_IN_SECONDS * 3;\n\t\t$this->assertEquals( $expect, $order->get_next_payment_due_date( 'U' ) );\n\n\t\t// Time travel to when the 3rd payment is due.\n\t\tllms_tests_mock_current_time( date( 'Y-m-d H:i:s', $expect ) );\n\n\t\t// Make the 3rd payment.\n\t\t$order->record_transaction( array(\n\t\t\t'payment_type' => 'recurring',\n\t\t\t'status'       => 'llms-txn-succeeded',\n\t\t) );\n\n\t\t// No more payments due.\n\t\t$this->assertTrue( is_a( $order->get_next_payment_due_date( 'U' ), 'WP_Error' ) );\n\t\t$this->assertEquals( 0, $order->get_remaining_payments() );\n\n\t}\n\n\t/**\n\t * Test get_remaining_payments()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_remaining_payments() {\n\n\t\t// Not recurring.\n\t\t$this->assertFalse( $this->obj->get_remaining_payments() );\n\n\t\t// No length.\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\t\t$this->assertFalse( $this->obj->get_remaining_payments() );\n\n\t\t// Has length.\n\t\t$this->obj->set( 'billing_length', 5 );\n\t\t$this->assertEquals( 5, $this->obj->get_remaining_payments() );\n\n\t\t// These statuses don't count.\n\t\tforeach ( array( 'failed', 'pending' ) as $status ) {\n\t\t\t$this->obj->record_transaction( array(\n\t\t\t\t'status'       => \"llms-txn-{$status}\",\n\t\t\t\t'payment_type' => 'recurring',\n\t\t\t) );\n\t\t\t$this->assertEquals( 5, $this->obj->get_remaining_payments() );\n\t\t}\n\n\t\t// Record a few successes.\n\t\t$i = 1;\n\t\twhile ( $i <= 4 ) {\n\t\t\t$this->obj->record_transaction( array(\n\t\t\t\t'payment_type' => 'recurring',\n\t\t\t) );\n\t\t\t$this->assertEquals( 5 - $i, $this->obj->get_remaining_payments(), $i );\n\t\t\t++$i;\n\t\t}\n\n\t\t// Refunds count?\n\t\t$this->obj->record_transaction( array(\n\t\t\t'status'       => 'llms-txn-refunded',\n\t\t\t'payment_type' => 'recurring',\n\t\t) );\n\t\t$this->assertEquals( 0, $this->obj->get_remaining_payments() );\n\n\t}\n\n\t/**\n\t * Test get_switch_source_action().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_switch_source_action() {\n\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\n\t\t$tests = array(\n\t\t\t'llms-completed'      => null,\n\t\t\t'llms-active'         => 'switch',\n\t\t\t'llms-expired'        => null,\n\t\t\t'llms-on-hold'        => 'pay',\n\t\t\t'llms-pending-cancel' => 'switch',\n\t\t\t'llms-pending'        => 'pay',\n\t\t\t'llms-cancelled'      => null,\n\t\t\t'llms-refunded'       => null,\n\t\t\t'llms-failed'         => null,\n\t\t);\n\n\t\tforeach ( $tests as $status => $expected ) {\n\n\t\t\t$this->obj->set( 'status', $status );\n\t\t\t$this->assertEquals( $expected, $this->obj->get_switch_source_action(), $status );\n\n\t\t}\n\n\t}\n\n\t// public function test_get_transaction_total() {}\n\n/**\n\t * Test get_start_date() mathod.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_start_date() {\n\n\t\t$trial_plan = $this->get_plan( 25.99, 1, 'lifetime', false, true );\n\t\t$order      = $this->get_order( $trial_plan );\n\n\t\t$this->assertEquals(\n\t\t\t$order->get_date( 'date', 'Y-m-d H:i:s' ),\n\t\t\t$order->get_start_date()\n\t\t);\n\n\t\t// Record a transaction.\n\t\t$txn_time = date_i18n( 'Y-m-d H:i:s', strtotime( '+1 hour' ) );\n\t\t$txn      = $order->record_transaction( array(\n\t\t\t'amount'         => 25.99,\n\t\t\t'completed_date' => $txn_time,\n\t\t\t'status'         => 'llms-txn-succeeded',\n\t\t\t'payment_type'   => 'recurring',\n\t\t) );\n\t\t// Change published date to reflect the completed date.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'        => $txn->get( 'id' ),\n\t\t\t\t'post_date' => $txn_time,\n\t\t\t)\n\t\t);\n\n\t\t// Hydrate.\n\t\t$txn = llms_get_post( $txn->get( 'id' ) );\n\t\t$this->assertNotEquals(\n\t\t\t$order->get_start_date(),\n\t\t\t$order->get_date( 'date', 'Y-m-d H:i:s' )\n\t\t);\n\t\t$this->assertEquals( $order->get_start_date(), $txn_time );\n\n\t\t// Record a new transaction, mimicking a trial paid yestarday.\n\t\t$txn_time = date_i18n( 'Y-m-d H:i:s', strtotime( '-1 day' ) );\n\t\t$txn = $order->record_transaction( array(\n\t\t\t'amount'         => 25.99,\n\t\t\t'completed_date' => $txn_time,\n\t\t\t'status'         => 'llms-txn-succeeded',\n\t\t\t'payment_type'   => 'trial',\n\t\t) );\n\n\t\t// Change published date to reflect the completed date.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'        => $txn->get( 'id' ),\n\t\t\t\t'post_date' => $txn_time,\n\t\t\t)\n\t\t);\n\t\t// Hydrate.\n\t\t$txn = llms_get_post( $txn->get( 'id' ) );\n\t\t$this->assertEquals( $order->get_start_date(), $txn_time );\n\n\t}\n\n\t// public function test_get_transactions() {}\n\n\t/**\n\t * Test the get_trial_end_date() method.\n\t *\n\t * @since 3.10.0\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_trial_end_date() {\n\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\n\t\t// No trial so false for end date.\n\t\t$this->assertEmpty( $this->obj->get_trial_end_date() );\n\n\t\t// Enable trial.\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\t\t$start = $this->obj->get_start_date( 'U' );\n\n\t\t// When the date is saved the getter shouldn't calculate a new date and should return the saved date.\n\t\t$set = '2017-05-05 13:42:19';\n\t\t$this->obj->set( 'date_trial_end', $set );\n\t\t$this->assertEquals( $set, $this->obj->get_trial_end_date() );\n\t\t$this->obj->set( 'date_trial_end', '' );\n\n\t\t// Run a bunch of tests testing the dynamic calculations for various periods and whatever.\n\t\tforeach ( array( 'day', 'week', 'month', 'year' ) as $period ) {\n\n\t\t\t$this->obj->set( 'trial_period', $period );\n\t\t\t$i = 1;\n\t\t\twhile ( $i <= 3 ) {\n\n\t\t\t\tllms_tests_mock_current_time( date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) );\n\n\t\t\t\t$this->obj->set( 'trial_length', $i );\n\t\t\t\t$expect = strtotime( '+' . $i . ' ' . $period, $start );\n\t\t\t\t$this->assertEquals( $expect, $this->obj->get_trial_end_date( 'U' ) );\n\n\t\t\t\t// Trial is not over.\n\t\t\t\t$this->assertFalse( $this->obj->has_trial_ended() );\n\n\t\t\t\t// Change date to future.\n\t\t\t\tllms_tests_mock_current_time( date( 'Y-m-d H:i:s', $this->obj->get_trial_end_date( 'U' ) + HOUR_IN_SECONDS ) );\n\t\t\t\t$this->assertTrue( $this->obj->has_trial_ended() );\n\n\t\t\t\t$i++;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t// public function test_get_revenue() {}\n\n\t/**\n\t * Test has_coupon() method\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_coupon() {\n\n\t\t$this->obj->set( 'coupon_used', 'whatarst' );\n\t\t$this->assertFalse( $this->obj->has_coupon() );\n\n\t\t$this->obj->set( 'coupon_used', 'no' );\n\t\t$this->assertFalse( $this->obj->has_coupon() );\n\n\t\t$this->obj->set( 'coupon_used', '' );\n\t\t$this->assertFalse( $this->obj->has_coupon() );\n\n\t\t$this->obj->set( 'coupon_used', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_coupon() );\n\n\t}\n\n\t/**\n\t * Test had_discount() method\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_discount() {\n\n\t\t$this->obj->set( 'coupon_used', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_discount() );\n\n\t\t$this->obj->set( 'coupon_used', 'no' );\n\t\t$this->assertFalse( $this->obj->has_discount() );\n\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_discount() );\n\n\t\t$this->obj->set( 'on_sale', 'no' );\n\t\t$this->assertFalse( $this->obj->has_discount() );\n\n\t}\n\n\t/**\n\t * Test has_plan_expiration()\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_plan_expiration() {\n\n\t\t// Single payment.\n\t\t$this->assertFalse( $this->obj->has_plan_expiration() );\n\n\t\t// Recurring with no length.\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\t\t$this->assertFalse( $this->obj->has_plan_expiration() );\n\n\t\t// Has length.\n\t\t$this->obj->set( 'billing_length', 1 );\n\t\t$this->assertTrue( $this->obj->has_plan_expiration() );\n\n\t}\n\n\t/**\n\t * Test has_sale() method\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_sale() {\n\n\t\t$this->obj->set( 'on_sale', 'whatarst' );\n\t\t$this->assertFalse( $this->obj->has_sale() );\n\n\t\t$this->obj->set( 'on_sale', 'no' );\n\t\t$this->assertFalse( $this->obj->has_sale() );\n\n\t\t$this->obj->set( 'on_sale', '' );\n\t\t$this->assertFalse( $this->obj->has_sale() );\n\n\t\t$this->obj->set( 'on_sale', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_sale() );\n\n\t}\n\n\t// public function test_has_scheduled_payment() {}\n\n\t/**\n\t * Test has_trial() method\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_trial() {\n\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\n\t\t$this->obj->set( 'trial_offer', 'whatarst' );\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'trial_offer', 'no' );\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'trial_offer', '' );\n\t\t$this->assertFalse( $this->obj->has_trial() );\n\n\t\t$this->obj->set( 'trial_offer', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_trial() );\n\n\t}\n\n\t/**\n\t * Test init().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init() {\n\n\t\t$user_data = array(\n\t\t\t'user_email' => 'orderinit@mock.tld',\n\t\t\t'first_name' => 'Adam',\n\t\t\t'last_name'  => 'Phillips',\n\t\t);\n\n\t\t$user_extra = array(\n\t\t\t'billing_address_1'    => 'add1',\n\t\t\t'billing_address_2'    => 'add2',\n\t\t\t'billing_city'         => 'City',\n\t\t\t'billing_state'        => 'AB',\n\t\t\t'billing_zip'          => '12345',\n\t\t\t'billing_country'      => 'ZZ',\n\t\t);\n\n\t\t$student = $this->factory->student->create_and_get( $user_data );\n\t\t$plan    = $this->get_mock_plan();\n\n\t\t$expected_data = array(\n\t\t\t'billing_email'        => $user_data['user_email'],\n\t\t\t'billing_first_name'   => $user_data['first_name'],\n\t\t\t'billing_last_name'    => $user_data['last_name'],\n\t\t\t'billing_phone'        => '(123) 456-7890',\n\t\t\t'user_ip_address'      => '127.0.0.1',\n\t\t\t'plan_id'              => $plan->get( 'id' ),\n\t\t\t'plan_title'           => $plan->get( 'title' ),\n\t\t\t'plan_sku'             => $plan->get( 'sku' ),\n\t\t\t'product_id'           => $plan->get( 'product_id' ),\n\t\t\t'product_title'        => get_the_title( $plan->get( 'product_id' ) ),\n\t\t\t'product_sku'          => '',\n\t\t\t'product_type'         => 'course',\n\t\t\t'payment_gateway'      => 'manual',\n\t\t\t'gateway_api_mode'     => 'live',\n\t\t\t'trial_offer'          => 'no',\n\t\t\t'trial_length'         => 0,\n\t\t\t'trial_period'         => '',\n\t\t\t'trial_original_total' => 0.0,\n\t\t\t'trial_total'          => 0.0,\n\t\t\t'date_trial_end'       => '',\n\t\t\t'currency'             => 'USD',\n\t\t\t'on_sale'              => 'no',\n\t\t\t'sale_price'           => 0.0,\n\t\t\t'sale_value'           => 0,\n\t\t\t'original_total'       => 25.99,\n\t\t\t'total'                => 25.99,\n\t\t\t'coupon_id'            => 0,\n\t\t\t'coupon_amount'        => 0,\n\t\t\t'coupon_code'          => '',\n\t\t\t'coupon_type'          => '',\n\t\t\t'coupon_used'          => 'no',\n\t\t\t'coupon_value'         => 0.0,\n\t\t\t'coupon_amount_trial'  => '',\n\t\t\t'coupon_value_trial'   => 0,\n\t\t\t'billing_frequency'    => 1,\n\t\t\t'billing_length'       => 0,\n\t\t\t'billing_period'       => 'day',\n\t\t\t'order_type'           => 'recurring',\n\t\t\t'date_next_payment'    => '2022-03-03 12:22:19',\n\t\t\t'access_expiration'    => 'lifetime',\n\t\t\t'access_expires'       => '',\n\t\t\t'access_length'        => 0,\n\t\t\t'access_period'        => '',\n\t\t);\n\n\t\tforeach ( $user_extra as $key => $val ) {\n\t\t\t$student->set( $key, $val );\n\t\t\t$expected_data[ $key ] = $val;\n\t\t}\n\t\t$student->set( 'phone', '(123) 456-7890' );\n\n\t\t$mock_next_payment_date = function() use ( $expected_data ) {\n\t\t\treturn $expected_data['date_next_payment'];\n\t\t};\n\t\tadd_filter( 'llms_order_calculate_next_payment_date', $mock_next_payment_date );\n\n\t\t$order = $this->get_mock_order( $plan, null, $student );\n\n\t\tremove_filter( 'llms_order_calculate_next_payment_date', $mock_next_payment_date );\n\n\t\tforeach ( $expected_data as $key => $expected ) {\n\t\t\t$this->assertEquals( $expected, $order->get( $key ), $key );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test init() with a user data array.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_with_user_data_array() {\n\n\t\t// Should pass an empty student object to the hook.\n\t\t$handler = function( $order, $student, $user_data ) {\n\t\t\t$this->assertFalse( $student->exists() );\n\t\t};\n\t\tadd_action( 'lifterlms_new_pending_order', $handler, 10, 3 );\n\n\t\t$order = new LLMS_Order( 'new' );\n\n\t\t// Use a user data array.\n\t\t$order->init(\n\t\t\tarray(\n\t\t\t\t'billing_email' => 'email@email.tld',\n\t\t\t),\n\t\t\t$this->get_mock_plan(),\n\t\t\tllms()->payment_gateways()->get_gateway_by_id( 'manual' )\n\t\t);\n\n\t\t$this->assertEquals( 'email@email.tld', $order->get( 'billing_email' ) );\n\t\t$this->assertEquals( 0, $order->get( 'user_id' ) );\n\n\t\tremove_action( 'lifterlms_new_pending_order', $handler, 10 );\n\n\t}\n\n\t/**\n\t * Test init() with a plan that has a trial.\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function test_init_with_trial() {\n\n\t\t// Test initialization of a trial.\n\t\t$plan = $this->get_plan( 25.99, 1, 'lifetime', false, true );\n\t\t$order = $this->get_order( $plan );\n\n\t\t$this->assertTrue( $order->has_trial() );\n\t\t$this->assertNotEmpty( $order->get( 'date_trial_end' ) );\n\n\t}\n\n\t/**\n\t * Test the is_recurring() method.\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_recurring() {\n\n\t\t$this->assertFalse( $this->obj->is_recurring() );\n\t\t$this->obj->set( 'order_type', 'recurring' );\n\t\t$this->assertTrue( $this->obj->is_recurring() );\n\n\t}\n\n\t/**\n\t * Test the schedule expiration function\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 4.6.0 Add coverage for `get_next_scheduled_action_time()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_schedule_expiration() {\n\n\t\t// Recurring order with lifetime access won't schedule expiration.\n\t\t$order = $this->get_mock_order();\n\n\t\t$order->set_status( 'llms-active' );\n\t\t$order->maybe_schedule_expiration();\n\n\t\t$this->assertFalse( as_next_scheduled_action( 'llms_access_plan_expiration', array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t) ) );\n\t\t$this->assertFalse( $order->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) );\n\n\t\t// Limited access will schedule expiration.\n\t\t$plan = $this->get_mock_plan( '25.99', 0, 'limited-date' );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$order->set_status( 'llms-active' );\n\t\t$order->maybe_schedule_expiration();\n\n\t\t$action_time = as_next_scheduled_action( 'llms_access_plan_expiration', array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t) );\n\t\t$this->assertEquals( $order->get_access_expiration_date( 'U' ), $action_time );\n\t\t$this->assertEquals( $action_time, $order->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) );\n\n\t}\n\n\t/**\n\t * Test recurring payment scheduling for a one-time order\n\t *\n\t * @since 4.7.0 Split from test_maybe_schedule_payment_recurring()\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_schedule_payment_one_time() {\n\n\t\t// Does nothing for a one-time order.\n\t\t$plan = $this->get_mock_plan( '25.99', 0 );\n\t\t$order = $this->get_mock_order( $plan );\n\t\t$order->maybe_schedule_payment();\n\t\t$this->assertEmpty( $order->get( 'date_next_payment' ) );\n\n\t}\n\n\t/**\n\t * Test recurring payment scheduling for a recurring order\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 4.6.0 Add coverage for `get_next_scheduled_action_time()`.\n\t * @since 4.7.0 Split into its own method to prevent variable clashes.\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_schedule_payment_recurring() {\n\n\t\t$order = $this->get_mock_order();\n\n\t\t$this->assertFalse( as_next_scheduled_action( 'llms_charge_recurring_payment', array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t) ) );\n\t\t$this->assertFalse( $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) );\n\n\t\t$order->maybe_schedule_payment();\n\t\t$this->assertTrue( ! empty( $order->get( 'date_next_payment' ) ) );\n\n\t\t$action_time = as_next_scheduled_action( 'llms_charge_recurring_payment', array( 'order_id' => $order->get( 'id' ) ) );\n\t\t$this->assertEquals( $order->get_next_payment_due_date( 'U' ), $action_time );\n\t\t$this->assertEquals( $action_time, $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) );\n\n\t}\n\n\t/**\n\t * Test maybe_schedule_retry() method.\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_schedule_retry() {\n\n\t\t$this->mock_gateway_support( 'recurring_retry' );\n\n\t\t$order = $this->get_order();\n\t\t$order->set_status( 'on-hold' );\n\n\t\t$i = 1;\n\t\twhile ( $i <= 5 ) {\n\n\t\t\t$original_next_date = $order->get_next_payment_due_date( 'U' );\n\n\t\t\t$txn = $order->record_transaction( array(\n\t\t\t\t'amount' => 25.99,\n\t\t\t\t'status' => 'llms-txn-pending',\n\t\t\t\t'payment_type' => 'recurring',\n\t\t\t) );\n\t\t\t$txn->set( 'status', 'llms-txn-failed' );\n\n\t\t\t$order = llms_get_post( $order->get( 'id' ) );\n\n\t\t\tif ( $i <= 4 ) {\n\n\t\t\t\t$this->assertEquals( $i, did_action( 'llms_automatic_payment_retry_scheduled' ) );\n\t\t\t\t$this->assertEquals( $i - 1, $order->get( 'last_retry_rule' ) );\n\t\t\t\t$this->assertNotEquals( $original_next_date, $order->get_next_payment_due_date( 'U' ) );\n\n\t\t\t} else {\n\n\t\t\t\t$this->assertEquals( 1, did_action( 'llms_automatic_payment_maximum_retries_reached' ) );\n\t\t\t\t$this->assertEquals( '', $order->get( 'last_retry_rule' ) );\n\t\t\t\t$this->assertEquals( 'llms-failed', $order->get( 'status' ) );\n\n\t\t\t}\n\n\n\t\t\t$i++;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test record_transaction() method\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function test_record_transaction() {\n\n\t\t$order = $this->get_order();\n\t\t$txn = $order->record_transaction( array(\n\t\t\t'amount' => 25.99,\n\t\t\t'status' => 'llms-txn-succeeded',\n\t\t\t'payment_type' => 'recurring',\n\t\t) );\n\t\t$this->assertTrue( is_a( $txn, 'LLMS_Transaction' ) );\n\t\t$order = llms_get_post( $order->get( 'id' ) );\n\t\t$this->assertEquals( 'llms-active', $order->get( 'status' ) );\n\t\t$this->assertEquals( 1, did_action( 'lifterlms_transaction_status_succeeded' ) );\n\t\t$this->assertEquals( 1, did_action( 'lifterlms_order_status_active' ) );\n\n\t}\n\n\t/**\n\t * Test the set_date() method\n\t *\n\t * @since 3.19.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_date() {\n\n\t\t$dates = array(\n\t\t\t'next_payment',\n\t\t\t'trial_end',\n\t\t\t'access_expires',\n\t\t);\n\n\t\tforeach ( $dates as $key ) {\n\n\t\t\t// Set via date string.\n\t\t\t$date = current_time( 'mysql' );\n\t\t\t$this->obj->set_date( $key, $date );\n\t\t\t$this->assertEquals( $date, $this->obj->get( 'date_' . $key ) );\n\n\t\t\t// Set via timestamp.\n\t\t\t$timestamp = current_time( 'timestamp' );\n\t\t\t$this->obj->set_date( $key, $timestamp );\n\t\t\t$this->assertEquals( date_i18n( 'Y-m-d H:i:s', $timestamp ), $this->obj->get( 'date_' . $key ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test set_status() method\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_status() {\n\n\t\t$this->obj->set_status( 'fakestatus' );\n\t\t$this->assertNotEquals( 'fakestatus', $this->obj->get( 'status' ) );\n\n\t\t$this->obj->set( 'order_type', 'single' );\n\t\tforeach ( array_keys( llms_get_order_statuses( 'single' ) ) as $status ) {\n\n\t\t\t$this->obj->set_status( $status );\n\t\t\t$this->assertEquals( $status, $this->obj->get( 'status' ) );\n\n\t\t\t$unprefixed = str_replace( 'llms-', '', $status );\n\t\t\t$this->obj->set_status( $unprefixed );\n\t\t\t$this->assertEquals( $status, $this->obj->get( 'status' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test set_user_data().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_user_data() {\n\n\t\t$expected = array(\n\t\t\t'billing_email'      => 'maude@thelittlelebowskiurbanachievers.org',\n\t\t\t'billing_first_name' => 'Maude',\n\t\t\t'billing_last_name'  => 'Lebowski',\n\t\t\t'billing_address_1'  => '123 Ant Street',\n\t\t\t'billing_address_2'  => 'Suite Z',\n\t\t\t'billing_city'       => 'Someplace',\n\t\t\t'billing_state'      => 'OK',\n\t\t\t'billing_zip'        => '32921-2342',\n\t\t\t'billing_country'    => 'US',\n\t\t\t'billing_phone'      => '(123) 456-7890',\n\t\t\t'user_ip_address'    => '127.0.0.1',\n\t\t);\n\n\t\t$user_id = $this->factory->student->create( array(\n\t\t\t'user_email' => $expected['billing_email'],\n\t\t\t'first_name' => $expected['billing_first_name'],\n\t\t\t'last_name'  => $expected['billing_last_name'],\n\t\t) );\n\n\t\t$student = llms_get_student( $user_id );\n\t\t$student->set( 'billing_address_1', $expected['billing_address_1'] );\n\t\t$student->set( 'billing_address_2', $expected['billing_address_2'] );\n\t\t$student->set( 'billing_city', $expected['billing_city'] );\n\t\t$student->set( 'billing_state', $expected['billing_state'] );\n\t\t$student->set( 'billing_zip', $expected['billing_zip'] );\n\t\t$student->set( 'billing_country', $expected['billing_country'] );\n\t\t$student->set( 'phone', $expected['billing_phone'] );\n\n\t\t$expected['user_id'] = $user_id;\n\n\t\t$tests = array(\n\t\t\t'User ID'      => $user_id,\n\t\t\t'LLMS_Student' => $student,\n\t\t\t'WP_User'      => get_user_by( 'id', $user_id ),\n\t\t\t'Raw Array'    => $expected,\n\t\t);\n\t\tforeach ( $tests as $msg => $input ) {\n\n\t\t\t$this->create(); // Reset the data from the previous run.\n \t\t\t$this->assertUserDataSet( $expected, $this->obj->set_user_data( $input ), \"From {$msg}\" );\n\n\t\t}\n\n\t\t$this->create();\n\n\t\t// Test raw array with a \"forced\" ip address.\n\t\t$expected['user_ip_address'] = '192.168.1.45';\n\t\t$this->assertUserDataSet( $expected, $this->obj->set_user_data( $expected ), \"From raw array with forced IP address\" );\n\n\t\t// Extra fields are excluded.\n\t\t$this->assertUserDataSet( $expected, $this->obj->set_user_data( array_merge( $expected, array( 'extra_field' => 'excluded' ) ) ), \"From raw array with extra data\" );\n\n\t}\n\n\tprivate function assertUserDataSet( $expected, $received, $message = '' ) {\n\n\t\t// Response is correct.\n\t\t$this->assertEquals( $expected, $received, $message );\n\n\t\t// Persisted to the order.\n\t\tforeach ( $expected as $key => $val ) {\n\t\t\t$this->assertEquals( $val, $this->obj->get( $key ), \"{$message}: {$key}\" );\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test the start access method\n\t *\n\t * @since 3.19.0\n\t * @since 3.32.0 Update to use latest action-scheduler functions.\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function test_start_access() {\n\n\t\t$plan = $this->get_mock_plan( '25.99', 0, 'limited-date' );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Freeze time.\n\t\t$time = current_time( 'mysql' );\n\t\tllms_tests_mock_current_time( $time );\n\n\t\t// Prior to starting access there should be no access start date.\n\t\t$this->assertEmpty( $order->get( 'start_date' ) );\n\n\t\t// Start the access.\n\t\t$order->start_access();\n\n\t\t// Time should be our mocked time.\n\t\t$this->assertEquals( $time, $order->get( 'start_date' ) );\n\n\t\t// An expiration event should be scheduled to match the expiration date.\n\t\t$event_time = as_next_scheduled_action( 'llms_access_plan_expiration', array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t) );\n\t\t$this->assertEquals( $order->get_access_expiration_date( 'U' ), $event_time );\n\n\t}\n\n\t/**\n\t * Test unschedule_expiration() method\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_unschedule_expiration() {\n\n\t\t$plan = $this->get_mock_plan( '25.99', 0, 'limited-date' );\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$order->set_status( 'llms-active' );\n\t\t$order->maybe_schedule_expiration();\n\n\t\t$order->unschedule_expiration();\n\n\t\t$this->assertFalse( $order->get_next_scheduled_action_time( 'llms_access_plan_expiration' ) );\n\n\t}\n\n\t/**\n\t * Test unschedule_recurring_payment() method\n\t *\n\t * @since 4.6.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_unschedule_recurring_payment() {\n\n\t\t$order = $this->get_mock_order();\n\t\t$order->maybe_schedule_payment();\n\n\t\t$order->unschedule_recurring_payment();\n\n\t\t$this->assertFalse( $order->get_next_scheduled_action_time( 'llms_charge_recurring_payment' ) );\n\n\t}\n\n\t/**\n\t * Test get_customer_full_address() method\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_customer_full_address() {\n\n\t\t$customer_details = array(\n\t\t\t'billing_address_1' => 'Rue Jennifer 7',\n\t\t\t'billing_address_2' => 'c/o Juniper',\n\t\t\t'billing_city'      => 'Pasadena',\n\t\t\t'billing_state'     => 'CA',\n\t\t\t'billing_zip'       => '28282',\n\t\t\t'billing_country'   => 'US'\n\t\t);\n\n\t\t$this->obj->set_bulk( $customer_details );\n\n\t\t$this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena CA, 28282, United States (US)', $this->obj->get_customer_full_address() );\n\n\t\t// Remove city.\n\t\t$this->obj->set( 'billing_city', '' );\n\t\t$this->assertEquals( 'Rue Jennifer 7 c/o Juniper, CA, 28282, United States (US)', $this->obj->get_customer_full_address() );\n\n\t\t// Remove state.\n\t\t$this->obj->set( 'billing_state', '' );\n\t\t$this->assertEquals( 'Rue Jennifer 7 c/o Juniper, 28282, United States (US)', $this->obj->get_customer_full_address() );\n\n\t\t// Add back city.\n\t\t$this->obj->set( 'billing_city', $customer_details['billing_city'] );\n\t\t$this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena, 28282, United States (US)', $this->obj->get_customer_full_address() );\n\n\t\t// Remove zip code.\n\t\t$this->obj->set( 'billing_zip', '' );\n\t\t$this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena, United States (US)', $this->obj->get_customer_full_address() );\n\n\t\t// Remove country.\n\t\t$this->obj->set( 'billing_country', '' );\n\t\t$this->assertEquals( 'Rue Jennifer 7 c/o Juniper, Pasadena', $this->obj->get_customer_full_address() );\n\n\t\t// Remove secondary address.\n\t\t$this->obj->set( 'billing_address_2', '' );\n\t\t$this->assertEquals( 'Rue Jennifer 7, Pasadena', $this->obj->get_customer_full_address() );\n\n\t\t// Remove main billing address. We expect that nothing is returned.\n\t\t$this->obj->set( 'billing_address_1', '' );\n\t\t$this->assertEquals( '', $this->obj->get_customer_full_address() );\n\n\t}\n\n\n\t/**\n\t * Test get_recurring_payment_due_date_for_scheduler() method\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_recurring_payment_due_date_for_scheduler() {\n\n\t\t$order = $this->get_mock_order();\n\n\t\t$now = current_time( 'timestamp' );\n\t\tllms_tests_mock_current_time( $now );\n\n\t\t// One time payment plan.\n\t\t$plan = $this->get_plan( '25.99', 0 );\n\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$this->assertWPErrorCodeEquals( 'not-recurring', $order->get_recurring_payment_due_date_for_scheduler() );\n\n\t\t// Check order with invalid status.\n\t\t$plan = $this->get_plan();\n\n\t\t$plan->set( 'frequency', 1 ); // Every.\n\t\t$plan->set( 'period', 'month' ); // Month.\n\t\t$plan->set( 'length', 3 ); // for 3 total payments.\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$original_status = $order->get( 'status' );\n\t\t$order->set( 'status', 'some-invalid' );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-status', $order->get_recurring_payment_due_date_for_scheduler() );\n\t\t$order->set( 'status', $original_status);\n\n\t\t// Check providing a (boolean) false date.\n\t\t$this->assertWPErrorCodeEquals( 'invalid-recurring-payment-date', $order->get_recurring_payment_due_date_for_scheduler( 0 ) );\n\n\t\t// Check the returning timestamp is the order next payment due date converted to UTC.\n\t\t$this->assertEquals(\n\t\t\tget_gmt_from_date( $order->get_next_payment_due_date(), 'U' ),\n\t\t\t$order->get_recurring_payment_due_date_for_scheduler()\n\t\t);\n\n\t\t// Pretend we the next payment due date was UTC.\n\t\t$this->assertEquals(\n\t\t\tdate_format( date_create( $order->get_next_payment_due_date() ), 'U' ),\n\t\t\t$order->get_recurring_payment_due_date_for_scheduler( false, true )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test supports_modify_recurring_payments() method.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_support_modify_recurring_payments() {\n\n\t\t// Default gateway: manual - supports recurring payments.\n\t\t$order = $this->get_mock_order();\n\t\t$this->assertTrue( $order->supports_modify_recurring_payments() );\n\n\t\t// Set gateway to something that, by default doesn't support recurring payments.\n\t\t$order->set( 'payment_gateway', 'garbage' );\n\t\t$this->assertFalse( $order->supports_modify_recurring_payments() );\n\n\t\t// Set the gateway to support 'modify_recurring_payments'.\n\t\t$order->set( 'payment_gateway', 'manual' );\n\t\t$gateway = $order->get_gateway();\n\t\t$gw_original_supports = $gateway->supports;\n\t\t$gateway->supports['recurring_payments'] = false;\n\t\t$gateway->supports['modify_recurring_payments'] = true;\n\t\t$this->assertTrue($order->get_gateway()->supports( 'modify_recurring_payments' ) );\n\t\t$this->assertTrue( $order->supports_modify_recurring_payments() );\n\n\t\t// Set the gateway to not support 'modify_recurring_payments'.\n\t\t$order->get_gateway()->supports['modify_recurring_payments'] = false;\n\t\t$this->assertFalse( $order->supports_modify_recurring_payments() );\n\n\t\t$gateway->supports = $gw_original_supports;\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-product.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Product Model\n *\n * @package LifterLMS/Tests/Models\n *\n * @group LLMS_Product\n * @group LLMS_Post_Model\n *\n * @since 3.25.2\n * @since 3.37.12 Create a stub for the test_create_method() since this class doesn't need to test that.\n * @since 3.38.0 Add tests for the get_restrictions() and has_restrictions() methods.\n *               Override unnecessary parent tests so they're not marked as skipped.\n * @since 5.4.0 Added tests for `has_active_subscriptions` method.\n */\nclass LLMS_Test_LLMS_Product extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Class name for the model being tested by the class.\n\t *\n\t * @var string\n\t */\n\tprotected $class_name = 'LLMS_Product';\n\n\t/**\n\t * DB post type of the model being tested.\n\t *\n\t * @var string\n\t */\n\tprotected $post_type = 'product';\n\n\t/**\n\t * Get properties, used by test_getters_setters.\n\t *\n\t * This should match, exactly, the object's $properties array.\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_properties() {\n\t\treturn array();\n\t}\n\n\t/**\n\t * Get data used to create the mock post.\n\t *\n\t * This is used by test_getters_setters().\n\t *\n\t * @since 3.24.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array();\n\t}\n\n\tprivate function add_plan( $product, $data = array() ) {\n\n\t\t$data = wp_parse_args( $data, array(\n\t\t\t'title' => 'mock plan',\n\t\t\t'is_free' => 'no',\n\t\t\t'price' => 100.00,\n\t\t\t'frequency' => 0,\n\t\t\t'visibility' => 'visible',\n\t\t) );\n\n\t\t$plan = new LLMS_Access_Plan( 'new', $data['title'] );\n\n\t\t$plan->set( 'product_id', $product->get( 'id' ) );\n\t\t$plan->set_visibility( $data['visibility'] );\n\n\t\tunset( $data['title'] );\n\t\tunset( $data['visibility'] );\n\n\t\tforeach ( $plan->get_properties() as $prop => $type ) {\n\n\t\t\tif ( array_key_exists( $prop, $data ) ) {\n\t\t\t\t$plan->set( $prop, $data[ $prop ] );\n\t\t\t} elseif ( 'yesno' === $type ) {\n\t\t\t\t$plan->set( $prop, 'no' );\n\t\t\t}\n\t\t}\n\n\t\treturn $plan;\n\n\t}\n\n\tprivate function get_product() {\n\n\t\t$product = new LLMS_Product( $this->factory->post->create( array( 'post_type' => 'course' ) ) );\n\t\treturn $product;\n\n\t}\n\n\t/**\n\t * Override parent test.\n\t *\n\t * This model has no properties of it's own so we can safely skip this test\n\t * without outputting a warning.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_getters_setters() {\n\t\t$this->assertTrue( true );\n\t}\n\n\n\t/**\n\t * Overwrite parent class method that tests model creation.\n\t *\n\t * This model shouldn't be created, instead the `LLMS_Course` or `LLMS_Membership` classes are used to create products.\n\t *\n\t * @since 3.37.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_model() {\n\t\t$this->assertTrue( true );\n\t}\n\n\n\t/**\n\t * Overwrite unnecessary parent test.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_date_status_relationship_update() {\n\t\t$this->assertTrue( true );\n\t}\n\n\t/**\n\t * Overwrite unnecessary parent test.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_edit_date() {\n\t\t$this->assertTrue( true );\n\t}\n\n\t/**\n\t * Overwrite unnecessary parent test.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk() {\n\t\t$this->assertTrue( true );\n\t}\n\n\t/**\n\t * Overwrite unnecessary parent test.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_bulk_wp_error() {\n\t\t$this->assertTrue( true );\n\t}\n\n\n\t/**\n\t * Test get_access_plan_limit() method.\n\t *\n\t * @since 3.25.2\n\t * @since 5.4.0 Remove 'llms_get_product_access_plan_limit' filter callback after use.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_access_plan_limit() {\n\n\t\t$product = $this->get_product();\n\n\t\t$this->assertTrue( is_int( $product->get_access_plan_limit() ) );\n\t\t$this->assertEquals( 6, $product->get_access_plan_limit() );\n\n\t\t// Test the filter.\n\t\t$return_three = function() {\n\t\t\treturn 3;\n\t\t};\n\n\t\tadd_filter( 'llms_get_product_access_plan_limit', $return_three );\n\t\t$this->assertEquals( 3, $product->get_access_plan_limit() );\n\t\tremove_filter( 'llms_get_product_access_plan_limit', $return_three );\n\n\t}\n\n\t/**\n\t * Test get_access_plan_limit() method.\n\t *\n\t * @since Unknown.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_access_plans() {\n\n\t\t$product = $this->get_product();\n\n\t\t// No plans.\n\t\t$this->assertEquals( 0, count( $product->get_access_plans() ) );\n\n\t\t// Add a plan.\n\t\t$this->add_plan( $product );\n\t\t// One plan returned.\n\t\t$this->assertEquals( 1, count( $product->get_access_plans() ) );\n\n\t\t// Add a free plan.\n\t\t$this->add_plan( $product, array( 'is_free' => 'yes' ) );\n\t\t// Two plans returned.\n\t\t$this->assertEquals( 2, count( $product->get_access_plans() ) );\n\t\t// Exclude free.\n\t\t$this->assertEquals( 1, count( $product->get_access_plans( true ) ) );\n\n\t\t// Add a hidden plan.\n\t\t$this->add_plan( $product, array( 'visibility' => 'hidden' ) );\n\t\t// Show all plans except hidden.\n\t\t$this->assertEquals( 2, count( $product->get_access_plans() ) );\n\t\t// Only show free & visible plans.\n\t\t$this->assertEquals( 1, count( $product->get_access_plans( true ) ) );\n\t\t// Show all plans.\n\t\t$this->assertEquals( 3, count( $product->get_access_plans( false, false ) ) );\n\t\t// Only show free (allow hidden plans).\n\t\t$this->assertEquals( 1, count( $product->get_access_plans( true, false ) ) );\n\n\t}\n\n\t/**\n\t * Test get_restrictions(): no restrictions on product.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_restrictions_none() {\n\n\t\t$product = $this->get_product();\n\t\t$course = llms_get_post( $product->get( 'id' ) );\n\n\t\t$this->assertEquals( array(), $product->get_restrictions() );\n\n\t}\n\n\t/**\n\t * Test get_restrictions(): enrollment period.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_restrictions_period() {\n\n\t\t$product = $this->get_product();\n\t\t$course = llms_get_post( $product->get( 'id' ) );\n\n\t\t$course->set( 'enrollment_period', 'yes' );\n\t\t$course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) );\n\t\t$this->assertEquals( array( 'enrollment_period' ), $product->get_restrictions() );\n\n\t}\n\n\t/**\n\t * Test get_restrictions(): max capacity.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_restrictions_capacity() {\n\n\t\t$product = $this->get_product();\n\t\t$course = llms_get_post( $product->get( 'id' ) );\n\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $course->get( 'id' ) );\n\t\t$course->set( 'enable_capacity', 'yes' );\n\t\t$course->set( 'capacity', 1 );\n\n\t\t$this->assertEquals( array( 'student_capacity' ), $product->get_restrictions() );\n\n\t}\n\n\t/**\n\t * Test get_restrictions(): multiple restrictions.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_restriction_multiple() {\n\n\t\t$product = $this->get_product();\n\t\t$course = llms_get_post( $product->get( 'id' ) );\n\n\t\t// Enrollment period.\n\t\t$course->set( 'enrollment_period', 'yes' );\n\t\t$course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) );\n\t\t$this->assertEquals( array( 'enrollment_period' ), $product->get_restrictions() );\n\n\t\t// No capacity.\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $course->get( 'id' ) );\n\t\t$course->set( 'enable_capacity', 'yes' );\n\t\t$course->set( 'capacity', 1 );\n\n\t\t$this->assertEquals( array( 'enrollment_period', 'student_capacity' ), $product->get_restrictions() );\n\t}\n\n\t/**\n\t * Test has_free_access_plan() method.\n\t *\n\t * @since 3.25.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_free_access_plan() {\n\n\t\t$product = $this->get_product();\n\n\t\t// Has no plans.\n\t\t$this->assertFalse( $product->has_free_access_plan() );\n\n\t\t// Has paid plan.\n\t\t$this->add_plan( $product );\n\t\t$this->assertFalse( $product->has_free_access_plan() );\n\n\t\t$this->add_plan( $product, array( 'is_free' => 'yes' ) );\n\t\t$this->assertTrue( $product->has_free_access_plan() );\n\n\t}\n\n\t/**\n\t * Test the has_restrictions() method.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_restrictions() {\n\n\t\t$product = $this->get_product();\n\t\t$course = llms_get_post( $product->get( 'id' ) );\n\n\t\t// No restrictions.\n\t\t$this->assertFalse( $product->has_restrictions() );\n\n\t\t// Add enrollment period restrictions.\n\t\t$course->set( 'enrollment_period', 'yes' );\n\t\t$course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) );\n\t\t$this->assertEquals( array( 'enrollment_period' ), $product->get_restrictions() );\n\n\t\t$this->assertTrue( $product->has_restrictions() );\n\n\t}\n\n\t/**\n\t * Test the is_purchasable() method.\n\t *\n\t * @since 3.25.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_purchasable() {\n\n\t\t$manual = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), 'no' );\n\n\t\t$product = $this->get_product();\n\t\t$course = llms_get_post( $product->get( 'id' ) );\n\n\t\t// Enrollment is closed.\n\t\t$course->set( 'enrollment_period', 'yes' );\n\t\t$course->set( 'enrollment_start_date', date( 'Y-m-d h:i:s', strtotime( '+1 day' ) ) );\n\t\t$this->assertFalse( $product->is_purchasable() );\n\t\t$course->set( 'enrollment_period', 'no' );\n\n\t\t// No capacity.\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $course->get( 'id' ) );\n\t\t$course->set( 'enable_capacity', 'yes' );\n\t\t$course->set( 'capacity', 1 );\n\t\t$this->assertFalse( $product->is_purchasable() );\n\n\t\t// Enrollment closed & no capacity.\n\t\t$course->set( 'enrollment_period', 'yes' );\n\t\t$this->assertFalse( $product->is_purchasable() );\n\t\t$course->set( 'enable_capacity', 'no' );\n\t\t$course->set( 'enrollment_period', 'no' );\n\n\t\t// No plans & no gateways.\n\t\t$this->assertFalse( $product->is_purchasable() );\n\n\t\t// Has a plan but no gateway.\n\t\t$plan = $this->add_plan( $product );\n\t\t$this->assertFalse( $product->is_purchasable() );\n\n\t\t// Has a plan and a gateway.\n\t\tupdate_option( $manual->get_option_name( 'enabled' ), 'yes' );\n\t\t$this->assertTrue( $product->is_purchasable() );\n\n\t\t// Only free plans.\n\t\t$plan->set( 'is_free', 'yes' );\n\t\t$this->assertTrue( $product->is_purchasable() );\n\n\t\t// No plans but has a gateway.\n\t\twp_delete_post( $plan->get( 'id' ), true );\n\t\t$this->assertFalse( $product->is_purchasable() );\n\n\t}\n\n\t/**\n\t * Test `has_active_subscriptions()` on a product only related to a single order.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_active_subscriptions_single_order() {\n\n\t\t$order   = $this->get_mock_order();\n\t\t$product = $this->get_product();\n\t\t$order->set( 'product_id', $product->get('id') );\n\t\t$order->set( 'order_type', 'single' );\n\n\t\t$this->assertFalse( $product->has_active_subscriptions( false ) );\n\n\t\t$order->set( 'status', 'llms-active' );\n\n\t\t$this->assertFalse( $product->has_active_subscriptions( false ) );\n\n\t}\n\n\t/**\n\t * Test `has_active_subscriptions()` on a product related to a single order and to an active subscription.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_active_subscriptions_single_and_recurring_order() {\n\n\t\t$order   = $this->get_mock_order();\n\t\t$product = $this->get_product();\n\n\t\t// Single order.\n\t\t$order->set( 'product_id', $product->get('id') );\n\t\t$order->set( 'order_type', 'single' );\n\n\t\t// Recurring order.\n\t\t$order_recurring = $this->get_mock_order();\n\t\t$order_recurring->set( 'product_id', $product->get('id') );\n\n\t\tforeach ( array( 'active', 'pending-cancel', 'on-hold' ) as $status ) {\n\t\t\t$order_recurring->set( 'status', \"llms-{$status}\" );\n\t\t\t$this->assertTrue( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test `has_active_subscriptions()` on a product related to a single order and to a not active subscription.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_active_subscriptions_single_and_recurring_order_inactive_status() {\n\n\t\t$order   = $this->get_mock_order();\n\t\t$product = $this->get_product();\n\n\t\t// Single order.\n\t\t$order->set( 'product_id', $product->get('id') );\n\t\t$order->set( 'order_type', 'single' );\n\n\t\t// Recurring order.\n\t\t$order_recurring = $this->get_mock_order();\n\t\t$order_recurring->set( 'product_id', $product->get('id') );\n\n\t\tforeach ( array_keys( llms_get_order_statuses() ) as $status ) {\n\n\t\t\tif ( in_array( $status, array( 'llms-active', 'llms-pending-cancel', 'llms-on-hold' ), true ) ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$order_recurring->set( 'status', $status );\n\t\t\t$this->assertFalse( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test `has_active_subscriptions()` using cache mechanism.\n\t *\n\t * @since 5.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_active_subscriptions_use_cache() {\n\n\t\t$order   = $this->get_mock_order();\n\t\t$product = $this->get_product();\n\n\t\t// Recurring order.\n\t\t$order_recurring = $this->get_mock_order();\n\t\t$order_recurring->set( 'product_id', $product->get('id') );\n\t\t$order_recurring->set( 'status', 'llms-active' );\n\n\t\t$this->assertTrue( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) );\n\n\t\t// Cancel subscription.\n\t\t$order_recurring->set( 'status', 'llms-cancelled' );\n\t\t// Use cache, I expect an active subscription.\n\t\t$this->assertTrue( $product->has_active_subscriptions( true ), $order_recurring->get( 'status' ) );\n\t\t// Do not use cache, I expect no active subscriptions.\n\t\t$this->assertFalse( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) );\n\n\t\t// Activate it again.\n\t\t$order_recurring->set( 'status', 'llms-active' );\n\t\t// Use cache, I expect no active subscriptions.\n\t\t$this->assertFalse( $product->has_active_subscriptions( true ), $order_recurring->get( 'status' ) );\n\t\t// Do not use cache, I expect an active subscription.\n\t\t$this->assertTrue( $product->has_active_subscriptions( false ), $order_recurring->get( 'status' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-question.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Quiz Model\n *\n * @package LifterLMS_Tests/Models\n *\n * @group post_models\n * @group quizzes\n * @group questions\n *\n * @since 3.16.12\n * @since 3.30.1 Added more tests for `get_next_choice_marker()` and `get_choices()`\n * @since 4.4.0 Add tests for the `grade()` method.\n */\nclass LLMS_Test_LLMS_Question extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Class name for the model being tested by the class\n\t *\n\t * @var string\n\t */\n\tprotected $class_name = 'LLMS_Question';\n\n\t/**\n\t * DB post type of the model being tested\n\t *\n\t * @var  string\n\t */\n\tprotected $post_type = 'llms_question';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t *\n\t * This should match, exactly, the object's $properties array\n\t *\n\t * @since 3.16.12\n\t *\n\t * @return array\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\t\t\t'clarifications' => 'html',\n\t\t\t'clarifications_enabled' => 'yesno',\n\t\t\t'description_enabled' => 'yesno',\n\t\t\t// 'image' => 'array',\n\t\t\t'multi_choices' => 'yesno',\n\t\t\t'parent_id' => 'absint',\n\t\t\t'points' => 'absint',\n\t\t\t'question_type' => 'string',\n\t\t\t'title' => 'html',\n\t\t\t'video_enabled' => 'yesno',\n\t\t\t'video_src' => 'string',\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters\n\t *\n\t * @since 3.16.12\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'clarifications' => '<p>this is <b>a</b> clarification</p>',\n\t\t\t'clarifications_enabled' => 'yes',\n\t\t\t'description_enabled' => 'yes',\n\t\t\t// 'image' => 'array',\n\t\t\t'multi_choices' => 'no',\n\t\t\t'parent_id' => 123,\n\t\t\t'points' => 3,\n\t\t\t'question_type' => 'choice',\n\t\t\t'title' => 'this <b>is</b> <i>a</i> question',\n\t\t\t'video_enabled' => 'yes',\n\t\t\t'video_src' => 'http://example.tld/video_embed',\n\t\t);\n\t}\n\n\t/**\n\t * Overwrite unnecessary parent test.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_edit_date() {\n\t\t$this->assertTrue( true );\n\t}\n\n\t/**\n\t * Test the has_description() method.\n\t *\n\t * @since 3.16.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_description() {\n\n\t\t$this->create( 'title' );\n\t\t$this->assertFalse( $this->obj->has_description() );\n\n\t\t$this->obj->set( 'content', 'arstarst' );\n\t\t$this->assertFalse( $this->obj->has_description() );\n\n\t\t$this->obj->set( 'description_enabled', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_description() );\n\n\t\t$this->obj->set( 'content', '' );\n\t\t$this->assertFalse( $this->obj->has_description() );\n\n\t}\n\n\t/**\n\t * Test the has_video() method.\n\t *\n\t * @since 3.16.12\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_video() {\n\n\t\t$this->create( 'title' );\n\t\t$this->assertFalse( $this->obj->has_video() );\n\n\t\t$this->obj->set( 'video_src', 'http://example.tld/video_embed' );\n\t\t$this->assertFalse( $this->obj->has_video() );\n\n\t\t$this->obj->set( 'video_enabled', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_video() );\n\n\t\t$this->obj->set( 'video_src', '' );\n\t\t$this->assertFalse( $this->obj->has_video() );\n\n\t}\n\n\t/**\n\t * Test the get_next_choice_marker() method.\n\t *\n\t * @since 3.30.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_next_choice_marker() {\n\n\t\t$this->create();\n\t\t$this->obj->set( 'question_type', 'choice' );\n\t\tforeach( range( 'A', 'Z' ) as $expected ) {\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $this->obj, 'get_next_choice_marker' ) );\n\t\t\t$choice = $this->obj->get_choice( $this->obj->create_choice( array() ) );\n\t\t\t$this->assertEquals( $expected, $choice->get( 'marker' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the get_choices() method.\n\t *\n\t * @since 3.30.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_choices() {\n\n\t\tforeach ( array( range( 'A', 'Z' ), range( 1, 26 ) ) as $i => $markers ) {\n\n\t\t\t$last_marker = false;\n\n\t\t\tif ( 1 === $i ) {\n\t\t\t\t// Filter marker for testing numeric values.\n\t\t\t\tadd_filter( 'llms_get_question_type', function( $data, $type ) {\n\t\t\t\t\tif ( 'choice' === $type ) {\n\t\t\t\t\t\t$data['choices']['markers'] = range( 1, 26 );\n\t\t\t\t\t}\n\t\t\t\t\treturn $data;\n\t\t\t\t}, 923, 2 );\n\t\t\t}\n\n\t\t\t$this->create();\n\t\t\t$this->obj->set( 'question_type', 'choice' );\n\n\t\t\t// No choices.\n\t\t\t$this->assertSame( array(), $this->obj->get_choices() );\n\n\t\t\t$expected_ids = array();\n\t\t\tforeach ( $markers as $marker ) {\n\t\t\t\t$expected_ids[] = $this->obj->create_choice( array( 'marker' => $marker ) );\n\t\t\t}\n\n\t\t\t$choices = $this->obj->get_choices( 'choices' );\n\t\t\t$this->assertEquals( 26, count( $choices ) );\n\t\t\tforeach ( $choices as $choice ) {\n\n\t\t\t\t// Ensure the correct order.\n\t\t\t\tif ( ! empty( $last_marker ) ) {\n\t\t\t\t\t$this->assertEquals( -1, strnatcmp( $last_marker, $choice->get( 'marker' ) ), $last_marker . ':' . $choice->get( 'marker' ) );\n\t\t\t\t}\n\t\t\t\t$last_marker = $choice->get( 'marker' );\n\n\t\t\t\t// Must be a choice.\n\t\t\t\t$this->assertTrue( is_a( $choice, 'LLMS_Question_Choice' ) );\n\n\t\t\t\t// Make sure the ID exists in the array.\n\t\t\t\t$this->assertTrue( in_array( $choice->get( 'id' ), $expected_ids, true ) );\n\n\t\t\t}\n\n\t\t\t// Only ids.\n\t\t\t$ids = $this->obj->get_choices( 'ids' );\n\t\t\t$this->assertSame( 26, count( $ids ) );\n\t\t\tforeach( $expected_ids as $id ) {\n\t\t\t\t$this->assertTrue( in_array( '_llms_choice_' . $id, $ids, true ) );\n\t\t\t}\n\n\t\t}\n\n\t\t// Remove marker filter.\n\t\tremove_all_filters( 'llms_get_question_type', 923 );\n\n\t}\n\n\t/**\n\t * Test grade() for a question with no points.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_grade_no_points() {\n\n\t\t$question = new LLMS_Question( 'new' );\n\t\t$this->assertNull( $question->grade( array( 1 ) ) );\n\n\t}\n\n\t/**\n\t * Test grade() when grading is handled by a filter from a 3rd party\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_grade_custom() {\n\n\t\t$question = new LLMS_Question( 'new' );\n\t\t$question->set( 'question_type', 'custom_grading_test_type' );\n\n\t\tadd_filter( 'llms_custom_grading_test_type_question_pre_grade', function( $grade ) {\n\t\t\treturn 'yes';\n\t\t}, 529 );\n\n\t\t$this->assertEquals( 'yes', $question->grade( array( 1 ) ) );\n\n\t\tremove_all_filters( 'llms_custom_grading_test_type_question_pre_grade', 529 );\n\n\t}\n\n\t/**\n\t * Test grade() for a multiple choice question with multiple correct answers.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_grade_choices_multi() {\n\n\t\t$question = new LLMS_Question( 'new' );\n\t\t$question->set( 'question_type', 'choice' );\n\t\t$question->set( 'multi_choices', 'yes' );\n\t\t$question->set( 'points', 1 );\n\n\t\t$choices = array(\n\t\t\t$question->create_choice( array( 'choice' => 'A', 'correct' => true ) ),\n\t\t\t$question->create_choice( array( 'choice' => 'B' ) ),\n\t\t\t$question->create_choice( array( 'choice' => 'C', 'correct' => true ) ),\n\t\t);\n\n\t\t// Correct answer.\n\t\t$this->assertEquals( 'yes', $question->grade( array( $choices[0], $choices[2] ) ) );\n\n\t\t// Order doesn't matter.\n\t\t$this->assertEquals( 'yes', $question->grade( array( $choices[2], $choices[0] ) ) );\n\n\t\t// Various potential incorrect answers.\n\t\t$this->assertEquals( 'no', $question->grade( array( $choices[0] ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( $choices[1] ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( $choices[2] ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( $choices[0], $choices[1] ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( $choices[1], $choices[2] ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( $choices[0], $choices[1], $choices[2] ) ) );\n\n\t}\n\n\t/**\n\t * Test grade() for a multiple choice with a single correct answer and true_false questions.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_grade_choices_single() {\n\n\t\tforeach ( array( 'choice', 'true_false' ) as $question_type ) {\n\n\t\t\t$question = new LLMS_Question( 'new' );\n\t\t\t$question->set( 'question_type', $question_type );\n\t\t\t$question->set( 'points', 1 );\n\n\t\t\t$choices = array(\n\t\t\t\t'correct'   => $question->create_choice( array( 'choice' => 'A', 'correct' => true ) ),\n\t\t\t\t'incorrect' => $question->create_choice( array( 'choice' => 'B' ) ),\n\t\t\t);\n\n\t\t\t$this->assertEquals( 'yes', $question->grade( array( $choices['correct'] ) ) );\n\t\t\t$this->assertEquals( 'no', $question->grade( array( $choices['incorrect'] ) ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test grade() for a conditionally auto-graded question.\n\t *\n\t * @since 4.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_grade_conditional() {\n\n\t\t$question = new LLMS_Question( 'new' );\n\t\t$question->set( 'question_type', 'fake_conditional_type' );\n\t\t$question->set( 'points', 1 );\n\t\t$question->set( 'auto_grade', 'yes' );\n\t\t$question->set( 'correct_value', 'This is correct.' );\n\n\t\t// Mock Conditional grading enabled.\n\t\tadd_filter( 'llms_fake_conditional_type_question_supports', function( $ret, $feature, $option ) {\n\t\t\treturn ( 'grading' === $feature && 'conditional' === $option );\n\t\t}, 329, 3 );\n\n\t\t// Correct answers, case sensitivity doesn't matter.\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'This is correct.' ) ) );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'THIS IS CORRECT.' ) ) );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'this is correct.' ) ) );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'tHiS is coRrECt.' ) ) );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'this IS correct.' ) ) );\n\n\t\t// Incorrect.\n\t\t$this->assertEquals( 'no', $question->grade( array( 'This is not correct.' ) ) );\n\n\t\t// Case matters now.\n\t\tadd_filter( 'llms_quiz_grading_case_sensitive', '__return_true' );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'This is correct.' ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( 'this is correct.' ) ) );\n\t\tremove_filter( 'llms_quiz_grading_case_sensitive', '__return_true' );\n\n\t\t// Add an additional value.\n\t\t$question->set( 'correct_value', 'one|TWO' );\n\n\t\t// Correct.\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'one', 'TWO' ) ) );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'ONE', 'two' ) ) );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'OnE', 'Two' ) ) );\n\n\t\t// Incorrect.\n\t\t$this->assertEquals( 'no', $question->grade( array( 'TWO' ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( 'one' ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( 'fake' ) ) );\n\n\t\t// Incorrect, order matters.\n\t\t$this->assertEquals( 'no', $question->grade( array( 'TWO', 'one' ) ) );\n\n\t\t// Make case matter.\n\t\tadd_filter( 'llms_quiz_grading_case_sensitive', '__return_true' );\n\t\t$this->assertEquals( 'yes', $question->grade( array( 'one', 'TWO' ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( 'One', 'Two' ) ) );\n\t\t$this->assertEquals( 'no', $question->grade( array( 'Two' ) ) );\n\t\tremove_filter( 'llms_quiz_grading_case_sensitive', '__return_true' );\n\n\t\t// Unmock.\n\t\tremove_all_filters( 'llms_blank_question_supports', 329 );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz-attempt.php",
    "content": "<?php\n/**\n * Tests LLMS_Quiz_Attempt model.\n *\n * @group quizzes\n * @group quiz_attempt\n *\n * @since 3.9.0\n * @since 3.17.4 Unknown.\n * @since 4.0.0 Add tests for the answer_question() method.\n * @since 4.2.0 Added tests for the get_siblings() method.\n *              Added tests on lesson completion status when deleting attempts.\n * @since 5.3.0 Added tests on get_question_objects() when filtering out the removed questions.\n */\nclass LLMS_Test_Model_Quiz_Attempt extends LLMS_UnitTestCase {\n\n\t/**\n\t * Teardown the test case\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\t\tparent::tear_down();\n\t\tglobal $wpdb;\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_quiz_attempts\" );\n\t}\n\n\t/**\n\t * Get an initialized mock attempt\n\t *\n\t * @since 3.9.2\n\t * @since 3.16.11 Unknown.\n\t * @since 4.2.0 Added uid and courses parameter.\n\t *\n\t * @param integer $num_questions Optional. Number of questions to add to the quiz. Default 5.\n\t * @param integer $uid           Optional. WordPress user id, if not passed a new user will be created. Default `null`.\n\t * @param int[]   $course        Optional. Course id, if not passed a new mock course will be created. Default `null`.\n\t * @return obj\n\t */\n\tprivate function get_mock_attempt( $num_questions = 5, $uid = null, $course = null ) {\n\n\t\t$uid     = $uid ? $uid : $this->factory->user->create();\n\t\t$courses = ! empty( $course ) ? array( $course ) : $this->generate_mock_courses( 1, 1, 1, 1, $num_questions );\n\n\t\t$course = llms_get_post( $courses[0] );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$lid = $lesson->get( 'id' );\n\t\t$qid = $lesson->get( 'quiz' );\n\n\t\t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid );\n\t\t$attempt->save();\n\t\treturn $attempt;\n\n\t}\n\n\t/**\n\t * Get a series of initialized mock sibling attempts\n\t *\n\t * @since 4.2.0\n\t *\n\t * @param integer $num           Optional. Number of sibling attmpts. Default 5.\n\t * @param integer $num_questions Optional. Number of questions to add to the quiz. Default 5.\n\t * @param integer $uid           Optional. WordPress user id, if not passed a new user will be created. Default `null`.\n\t * @param int[]   $course        Optional. Course id, if not passed a new mock course will be created. Default `null`.\n\t * @return obj[]\n\t */\n\tprivate function get_mock_sibling_attempts( $num = 5, $num_questions = 5, $uid = null, $course = null ) {\n\n\t\t$uid     = $uid ? $uid : $this->factory->user->create();\n\t\t$course = ! empty( $course ) ? $course : $this->generate_mock_courses( 1, 1, 1, 1, $num_questions )[0];\n\n\t\t// Create attempts.\n\t\t$attempts = array();\n\t\tfor ( $i = 0; $i < $num; $i++ ) {\n\t\t\t$attempts[] = $this->get_mock_attempt( $num_questions, $uid, $course );\n\t\t}\n\n\t\treturn $attempts;\n\t}\n\n\t/**\n\t * Retrieve the first incorrect choice for a given question.\n\t *\n\t * @since 4.0.0\n\t *\n\t * @param LLMS_Question|WP_Post|int $question Question object, WP_Post object for a question post, or WP_Post ID of the question.\n\t * @return LLMS_Question_Choice\n\t */\n\tprivate function get_incorrect_choice( $question ) {\n\n\t\t$question = is_a( $question, 'LLMS_Question' ) ? $question : llms_get_post( $question );\n\n\t\tforeach ( $question->get_choices() as $choice ) {\n\n\t\t\tif ( $choice->is_correct() ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\treturn array(\n\t\t\t\t$choice->get( 'id' ),\n\t\t\t);\n\n\t\t}\n\n\t}\n\n\t/**\n\t * [take_a_quiz description]\n\t * @param    [type]     $desired_grade    grade for the attempt\n\t * @param    [type]     $passing_percent  required passing percentage\n\t * @param    integer    $num_questions    number of questions in the quiz\n\t * @param    string     $rand             whether to randomize question order\n\t * @param    string     $passing_required whether passing grade is required to complete the associated lesson\n\t * @return   [type]                       [description]\n\t * @since    3.9.2\n\t * @version  3.17.1\n\t */\n\tprivate function take_a_quiz( $desired_grade, $passing_percent, $num_questions = 15, $attempt = null, $rand = 'no', $passing_required = 'no' ) {\n\n\t\tif ( ! $attempt ) {\n\t\t\t$attempt = $this->get_mock_attempt( $num_questions );\n\t\t}\n\n\t\tupdate_post_meta( $attempt->get( 'lesson_id' ), '_llms_require_passing_grade', $passing_required );\n\n\t\tupdate_post_meta( $attempt->get( 'quiz_id' ), '_llms_random_questions', $rand );\n\t\tupdate_post_meta( $attempt->get( 'quiz_id' ), '_llms_passing_percent', $passing_percent );\n\t\t$to_answer_correctly = 0 === $desired_grade ? 0 : $desired_grade / 100 * $num_questions;\n\n\t\t$attempt->start();\n\n\t\t$current_question = 1;\n\t\twhile ( $attempt->get_next_question() ) {\n\n\t\t\t$question_id = $attempt->get_next_question();\n\t\t\t$question = llms_get_post( $question_id );\n\n\t\t\t$answer_type = ( $current_question <= $to_answer_correctly );\n\n\t\t\t// Answer correctly until we don't have to anymore.\n\t\t\tforeach( $question->get_choices() as $key => $choice ) {\n\t\t\t\tif ( $answer_type === $choice->is_correct() ) {\n\t\t\t\t\t$attempt->answer_question( $question_id, array( $choice->get( 'id' ) ) );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$current_question++;\n\n\t\t}\n\n\t\t$attempt->end();\n\n\t\treturn $attempt;\n\n\t}\n\n\tpublic function test_answer_question_correctly() {\n\n\t\t$attempt   = $this->get_mock_attempt();\n\t\t$questions = wp_list_pluck( $attempt->get_questions(), 'id' );\n\t\t$question  = llms_get_post( $questions[0] );\n\t\t$correct   = $question->get_correct_choice();\n\n\t\t// Answer question.\n\t\t$attempt = $attempt->answer_question( $questions[0], $correct );\n\n\t\t$this->assertTrue( is_a( $attempt, 'LLMS_Quiz_Attempt' ) );\n\n\t\t$res = $attempt->get_questions()[0];\n\n\t\t$this->assertEquals( $res['points'], $res['earned'] );\n\t\t$this->assertEquals( 'yes', $res['correct'] );\n\t\t$this->assertEquals( $correct, $res['answer'] );\n\n\n\t\t/**\n\t\t * Answer the question again to simulate a user going back to change their answer.\n\t\t *\n\t\t * @see https://github.com/gocodebox/lifterlms/issues/1211\n\t\t */\n\t\t$incorrect = $this->get_incorrect_choice( $question );\n\t\t$attempt->answer_question( $questions[0], $incorrect );\n\n\t\t$res = $attempt->get_questions()[0];\n\n\t\t$this->assertEquals( 0, $res['earned'] );\n\t\t$this->assertEquals( 'no', $res['correct'] );\n\t\t$this->assertEquals( $incorrect, $res['answer'] );\n\n\t}\n\n\t/**\n\t * Test answer_question() when supplying a correct answer\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_answer_question_incorrectly() {\n\n\t\t$attempt   = $this->get_mock_attempt();\n\t\t$questions = wp_list_pluck( $attempt->get_questions(), 'id' );\n\t\t$question  = llms_get_post( $questions[0] );\n\t\t$correct   = $question->get_correct_choice();\n\n\t\t// Answer question.\n\t\t$incorrect = $this->get_incorrect_choice( $question );\n\t\t$attempt = $attempt->answer_question( $questions[0], $incorrect );\n\n\t\t$res = $attempt->get_questions()[0];\n\n\t\t$this->assertEquals( 0, $res['earned'] );\n\t\t$this->assertEquals( 'no', $res['correct'] );\n\t\t$this->assertEquals( $incorrect, $res['answer'] );\n\n\t\t/**\n\t\t * Answer the question again to simulate a user going back to change their answer.\n\t\t *\n\t\t * @see https://github.com/gocodebox/lifterlms/issues/1211\n\t\t */\n\t\t$attempt->answer_question( $questions[0], $correct );\n\n\t\t$this->assertTrue( is_a( $attempt, 'LLMS_Quiz_Attempt' ) );\n\n\t\t$res = $attempt->get_questions()[0];\n\n\t\t$this->assertEquals( $res['points'], $res['earned'] );\n\t\t$this->assertEquals( 'yes', $res['correct'] );\n\t\t$this->assertEquals( $correct, $res['answer'] );\n\n\t}\n\n\t/**\n\t * Test answer_question() when supplying an incorrect answer\n\t *\n\t * @since 4.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_grading_with_floats() {\n\n\t\t$attempt = $this->get_mock_attempt( 6 );\n\n\t\t$questions = $attempt->get_questions();\n\n\t\tforeach ( $questions as $key => &$data ) {\n\t\t\t$data['points'] = 3.3333;\n\t\t}\n\n\t\t$attempt->set_questions( $questions, true );\n\n\t\t$attempt = $this->take_a_quiz( 67, 65, 6, $attempt );\n\t\t$this->assertEquals( 66.67, $attempt->get( 'grade' ) );\n\n\t}\n\n\n\t/**\n\t * Test counter functions\n\t * @return   void\n\t * @since    3.9.2\n\t * @version  3.16.11\n\t */\n\tpublic function test_get_count() {\n\n\t\t$i = 1;\n\t\twhile ( $i <= 10 ) {\n\n\t\t\t$attempt = $this->get_mock_attempt( $i );\n\n\t\t\t// num of questions and num available points will both be the same given the default mock quiz data\n\t\t\tforeach ( array( 'available_points', 'questions' ) as $key ) {\n\t\t\t\t$this->assertEquals( $i, $attempt->get_count( $key ) );\n\t\t\t}\n\n\t\t\t// update each question to have a random number of points and ensure the available points from getter is correct\n\t\t\t$questions = $attempt->get_questions();\n\t\t\t$total_points = 0;\n\t\t\tforeach( $questions as $key => $question ) {\n\t\t\t\t$add = rand( 1, 100 );\n\t\t\t\t$questions[ $key ]['points'] = $add;\n\t\t\t\t$total_points += $add;\n\t\t\t}\n\n\t\t\t$attempt->set_questions( $questions, true );\n\n\t\t\t$this->assertEquals( $total_points, $attempt->get_count( 'available_points' ) );\n\n\t\t\t$i++;\n\n\t\t}\n\n\n\t}\n\n\n\t/**\n\t * test get student function\n\t * @return   void\n\t * @since    3.9.0\n\t * @version  3.9.0\n\t */\n\tpublic function test_get_student() {\n\n\t\t$uid = $this->factory->user->create();\n\t\t$courses = $this->generate_mock_courses( 1, 1, 1, 1 );\n\n\t\t$course = llms_get_post( $courses[0] );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$lid = $lesson->get( 'id' );\n\t\t$qid = $lesson->get( 'quiz' );\n\n\t\t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid );\n\n\t\t$this->assertTrue( is_a( $attempt->get_student(), 'LLMS_Student' ) );\n\t\t$this->assertEquals( $uid, $attempt->get_student()->get_id() );\n\n\t}\n\n\t// /**\n\t//  * test getters and setters and save method\n\t//  * @return   void\n\t//  * @since    3.9.0\n\t//  * @version  3.9.2\n\t//  */\n\t// public function test_getters_setters_and_save() {\n\n\t// \t$uid = $this->factory->user->create();\n\t// \t$courses = $this->generate_mock_courses( 1, 1, 1, 1 );\n\n\t// \t$course = llms_get_post( $courses[0] );\n\t// \t$lesson = $course->get_lessons()[0];\n\t// \t$lid = $lesson->get( 'id' );\n\t// \t$qid = $lesson->get( 'quiz' );\n\n\t// \t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid );\n\n\t// \t$data = array(\n\t// \t\t'attempt' => 5,\n\t// \t\t'current' => false,\n\t// \t\t'end_date' => current_time( 'mysql' ),\n\t// \t\t'grade' => 85.35,\n\t// \t\t'passed' => true,\n\t// \t\t'start_date' => current_time( 'mysql' ),\n\t// \t);\n\n\t// \tforeach ( $data as $key => $val ) {\n\n\t// \t\t$attempt->set( $key, $val );\n\t// \t\t$this->assertEquals( $data[ $key ], $attempt->get( $key ) );\n\n\t// \t}\n\n\t// \tforeach ( $attempt->get( 'questions' ) as $key => $question ) {\n\n\t// \t\t$this->assertEquals( $key + 1, $attempt->get_question_order( $question['id'] ) );\n\n\t// \t}\n\n\t// \t// save the attempt again and ensure persistence works\n\t// \t$attempt->save();\n\n\t// \t$student = llms_get_student( $uid );\n\t// \t$attempt = $student->quizzes()->get_attempt( $qid, $lid, $data['attempt'] );\n\t// \tforeach ( $data as $key => $val ) {\n\t// \t\t$this->assertEquals( $data[ $key ], $attempt->get( $key ) );\n\t// \t}\n\n\t// }\n\n\t// /**\n\t//  * test static init function\n\t//  * @return   void\n\t//  * @since    3.9.0\n\t//  * @version  3.9.0\n\t//  */\n\t// public function test_init() {\n\n\t// \t$uid = $this->factory->user->create();\n\t// \t$courses = $this->generate_mock_courses( 1, 1, 1, 1 );\n\n\t// \t$course = llms_get_post( $courses[0] );\n\t// \t$lesson = $course->get_lessons()[0];\n\t// \t$lid = $lesson->get( 'id' );\n\t// \t$qid = $lesson->get( 'assigned_quiz' );\n\n\t// \t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, $uid )->save();\n\n\t// \t$att_num = $attempt->get( 'attempt' );\n\t// \t$student = llms_get_student( $uid );\n\n\t// \t// attempt saved successfully\n\t// \t$this->assertEquals( $student->quizzes()->get_attempt( $qid, $lid, $att_num ), $attempt );\n\n\t// \t// no user, attempt throws exception\n\t// \ttry {\n\t// \t\t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, null )->save();\n\t// \t} catch ( Exception $exception ) {\n\t// \t\t$this->assertTrue( is_a( $exception, 'Exception' ) );\n\t// \t}\n\n\t// \t// no user but a current user exists\n\t// \twp_set_current_user( $uid );\n\t// \t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, null )->save();\n\t// \t$att_num = $attempt->get( 'attempt' );\n\t// \t$this->assertEquals( 1, $att_num ); // should not increment because the attempt already exists\n\t// \t$this->assertEquals( $student->quizzes()->get_attempt( $qid, $lid, $att_num ), $attempt );\n\n\t// \t// mark the new attempt as not-current\n\t// \t$attempt->set( 'current', false )->save();\n\t// \t$attempt = LLMS_Quiz_Attempt::init( $qid, $lid, null )->save();\n\t// \t// new attempt should be #2\n\t// \t$this->assertEquals( 2, $attempt->get( 'attempt' ) );\n\n\t// }\n\n\t// /**\n\t//  * test quiz start\n\t//  * @return   void\n\t//  * @since    3.9.0\n\t//  * @version  3.9.0\n\t//  */\n\t// public function test_start() {\n\n\t// \t$attempt = $this->get_mock_attempt( 2 );\n\t// \t$attempt->start();\n\n\t// \t$this->assertTrue( ! empty( $attempt->get( 'start_date' ) ) );\n\n\t// }\n\n\t/**\n\t * Take a bunch of quizzes\n\t * quiz taking / ending functions\n\t * Tests grade / point calculations\n\t * pass/fail/complete actions\n\t * @return   void\n\t * @since    3.9.2\n\t * @version  3.17.4\n\t */\n\tpublic function test_take_some_quizzes( ) {\n\n\t\t$i = 0;\n\t\t$num_tests = 0;\n\t\t$num_pass = 0;\n\t\t$num_fail = 0;\n\t\twhile ( $i <= 100 ) {\n\n\t\t\t$rand = rand( 0, 1 ) ? 'yes' : 'no';\n\t\t\t$passing = $rand = rand( 0, 1 ) ? 'yes' : 'no';\n\t\t\t$attempt = $this->take_a_quiz( $i, 65, 25, null, $rand, $passing );\n\n\t\t\tif ( 0 === $i ) {\n\t\t\t\t$grade = 0;\n\t\t\t} else {\n\t\t\t\t$weight = ( 100 / $attempt->get_count( 'available_points' ) );\n\t\t\t\t$grade = floor( $i / 100 * 25 ) * $weight;\n\t\t\t}\n\n\t\t\t$this->assertEquals( $grade, $attempt->get( 'grade' ) );\n\t\t\t$this->assertTrue( ! is_null( $attempt->get( 'end_date' ) ) );\n\n\t\t\tif ( $grade < 65 ) {\n\t\t\t\t$num_fail++;\n\t\t\t\t$this->assertFalse( $attempt->is_passing() );\n\t\t\t\t$is_complete = llms_parse_bool( $passing ) ? false : true;\n\t\t\t} else {\n\t\t\t\t$num_pass++;\n\t\t\t\t$this->assertTrue( $attempt->is_passing() );\n\t\t\t\t$is_complete = true;\n\t\t\t}\n\n\t\t\t$this->assertEquals( $is_complete, llms_is_complete( $attempt->get( 'student_id' ), $attempt->get( 'lesson_id' ), 'lesson' ) );\n\n\t\t\t$num_tests++;\n\n\t\t\t$i = $i + ( 5 * rand( 1, 20 ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get siblings\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_siblings() {\n\n\t\t$attempts = $this->get_mock_sibling_attempts( 5, 1 );\n\t\t$attempt_ids = array_map(\n\t\t\tfunction( $attempt ) {\n\t\t\t\treturn $attempt->get( 'id' );\n\t\t\t},\n\t\t\t$attempts\n\t\t);\n\n\t\t// Test get siblings of the first attempt equals to the created array of attempts (id).\n\t\t$this->assertEquals(\n\t\t\tarray_reverse( $attempt_ids ),\n\t\t\t$attempts[0]->get_siblings( array(), 'ids' )\n\t\t);\n\n\t\t// Test exclude.\n\t\t$this->assertEquals(\n\t\t\tarray_reverse( array_slice( $attempt_ids, 1, count( $attempt_ids ) ) ),\n\t\t\t$attempts[0]->get_siblings(\n\t\t\t\tarray(\n\t\t\t\t\t'exclude' => array( $attempt_ids[0] ),\n\t\t\t\t),\n\t\t\t\t'ids'\n\t\t\t)\n\t\t);\n\n\t\t// Test per page, get only 4 attempts out of 5.\n\t\t$this->assertEquals(\n\t\t\tarray_slice( array_reverse( $attempt_ids ), 0, count( $attempt_ids ) - 1 ),\n\t\t\t$attempts[0]->get_siblings(\n\t\t\t\tarray(\n\t\t\t\t\t'per_page' => count( $attempt_ids ) - 1,\n\t\t\t\t),\n\t\t\t\t'ids'\n\t\t\t)\n\t\t);\n\n\t\t// Test return as attempt.\n\t\t$is_attempt = $attempts[0]->get_siblings(\n\t\t\tarray(\n\t\t\t\t'per_page' => 1,\n\t\t\t),\n\t\t\t'attempts'\n\t\t)[0] instanceof LLMS_Quiz_Attempt;\n\t\t$is_attempt_two = $attempts[0]->get_siblings(\n\t\t\tarray(\n\t\t\t\t'per_page' => 1,\n\t\t\t),\n\t\t\t'whatever'\n\t\t)[0] instanceof LLMS_Quiz_Attempt;\n\t\t$this->assertTrue( $is_attempt );\n\n\t}\n\n\t/**\n\t * Test lesson completion on delete attempts for a lesson not requiring a passing grade\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_not_requiring_passing_grade_lesson() {\n\n\t\t// Create 3 attempts (for a quiz with 1 question), for a given lesson.\n\t\t$attempts   = $this->get_mock_sibling_attempts( 3, 1 );\n\t\t$lesson_id  = $attempts[0]->get( 'lesson_id' );\n\t\t$student_id = $attempts[0]->get( 'student_id' );\n\n\t\t// Take a quiz (no passing). This will mark the lesson as complete.\n\t\t$this->take_a_quiz( 0, 65, 1, $attempts[0] );\n\n\t\t// Only the last deletion will mark the lesson as incomplete.\n\t\tforeach ( $attempts as $attempt ) {\n\t\t\t$this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\t\t\t$attempt->delete();\n\t\t}\n\t\t$this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\n\t\t// Create 3 attempts (for a quiz with 1 question), for a given lesson.\n\t\t$attempts   = $this->get_mock_sibling_attempts( 3, 1 );\n\t\t$lesson_id  = $attempts[0]->get( 'lesson_id' );\n\t\t$student_id = $attempts[0]->get( 'student_id' );\n\n\t\t// Take a quiz (passing). This will mark the lesson as complete.\n\t\t$this->take_a_quiz( 100, 65, 1, $attempts[0] );\n\n\t\t// We have 1 passing attempt, still only the last deletion will mark the lesson as incomplete.\n\t\tforeach ( $attempts as $attempt ) {\n\t\t\t$this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\t\t\t$attempt->delete();\n\t\t}\n\t\t$this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\n\t}\n\n\t/**\n\t * Test lesson completion on delete attempts for a lesson requiring a passing grade\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_requiring_passing_grade_lesson() {\n\n\t\t// Create 3 attempts (for a quiz with 1 question), for a given lesson.\n\t\t$attempts   = $this->get_mock_sibling_attempts( 3, 1 );\n\t\t$lesson_id  = $attempts[0]->get( 'lesson_id' );\n\t\t$student_id = $attempts[0]->get( 'student_id' );\n\n\t\t// Take a quiz (no passing). This will NOT mark the lesson as complete.\n\t\t$this->take_a_quiz( 0, 65, 1, $attempts[0], 'no', 'yes' );\n\n\t\tforeach ( $attempts as $attempt ) {\n\t\t\t$this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\t\t\t$attempt->delete();\n\t\t}\n\t\t$this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\n\t\t// Create 3 attempts (for a quiz with 1 question), for a given lesson.\n\t\t$attempts   = $this->get_mock_sibling_attempts( 3, 1 );\n\t\t$lesson_id  = $attempts[0]->get( 'lesson_id' );\n\t\t$student_id = $attempts[0]->get( 'student_id' );\n\n\t\t// Take a quiz (passing). This will mark the lesson as complete.\n\t\t$this->take_a_quiz( 100, 65, 1, $attempts[0], 'no', 'yes' );\n\n\t\t// We have 1 passing attempt (the first), deleting all the others (see the reverse order) will not mark the lesson as incomplete.\n\t\tforeach ( array_reverse( $attempts ) as $attempt ) {\n\t\t\t$this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\t\t\t$attempt->delete();\n\t\t}\n\t\t$this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\n\t\t// Create 1 attempts (for a quiz with 1 question), for a given lesson.\n\t\t$attempts   = $this->get_mock_sibling_attempts( 3, 1 );\n\t\t$lesson_id  = $attempts[0]->get( 'lesson_id' );\n\t\t$student_id = $attempts[0]->get( 'student_id' );\n\n\t\t// Take a quiz (passing). This will mark the lesson as complete.\n\t\t$this->take_a_quiz( 100, 65, 1, $attempts[0], 'no', 'yes' );\n\n\t\t// We have 1 passing attempt (the first), deleting it will mark the lesson as incomplete.\n\t\t$attempts[0]->delete();\n\t\t$this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\n\t\t// Create 3 attempts (for a quiz with 1 question), for a given lesson.\n\t\t$attempts   = $this->get_mock_sibling_attempts( 3, 1 );\n\t\t$lesson_id  = $attempts[0]->get( 'lesson_id' );\n\t\t$student_id = $attempts[0]->get( 'student_id' );\n\n\t\t// Take two passing quizzes.\n\t\t$this->take_a_quiz( 100, 65, 1, $attempts[0], 'no', 'yes' );\n\t\t//$this->take_a_quiz( 100, 65, 1, $attempts[1], 'no', 'yes' );\n\n\t\t// We have 2 passing attempts (the first two), the lesson will be marked as incomplete only after deleting all the passed attempts.\n\t\tforeach ( array_reverse( $attempts ) as $attempt ) {\n\t\t\t$this->assertTrue( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\t\t\t$attempt->delete();\n\t\t}\n\t\t$this->assertFalse( llms_is_complete( $student_id, $lesson_id, 'lesson' ) );\n\n\t}\n\n\t/**\n\t * Test get_question_objects() method when filtering out the removed questions.\n\t *\n\t * @since 5.3.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_question_objects_filter_removed() {\n\n\t\t$attempt   = $this->get_mock_attempt();\n\t\t$questions = wp_list_pluck( $attempt->get_questions(), 'id' );\n\n\t\t// Check `get_question_objects()` returns the same list of `get_questions()`.\n\t\t$this->assertEqualSets(\n\t\t\t$questions,\n\t\t\t$this->question_object_ids_list_pluck( $attempt )\n\t\t);\n\n\t\t// Delete a question.\n\t\twp_delete_post( $questions[ 1 ] );\n\n\t\t// Check `get_question_objects()` still returns the same list of `get_questions()`.\n\t\t$this->assertEqualSets(\n\t\t\t$questions,\n\t\t\t$this->question_object_ids_list_pluck( $attempt )\n\t\t);\n\n\t\t// Check `get_question_objects()` returns the same list of `get_questions()` except for the removed question\n\t\t// when the `$filter_remove` is passed as true.\n\t\t$this->assertEqualSets(\n\t\t\tarray_merge(\n\t\t\t\tarray(\n\t\t\t\t\t$questions[0]\n\t\t\t\t),\n\t\t\t\tarray_slice(\n\t\t\t\t\t$questions,\n\t\t\t\t\t2\n\t\t\t\t)\n\t\t\t),\n\t\t\t$this->question_object_ids_list_pluck( $attempt, true, true )\n\t\t);\n\n\t}\n\n\t/**\n\t * Returns a question object id given a LLMS_Quiz_Attempt\n\t *\n\t * @since 5.3.0\n\t *\n\t * @param LLMS_Quiz_Attempt $attemt Attempt object.\n\t * @return void\n\t */\n\tprivate function question_object_ids_list_pluck( $attempt, $cache = true, $filter_removed = false  ) {\n\t\treturn array_filter(\n\t\t\tarray_map(\n\t\t\t\tfunction( $qo ) {\n\t\t\t\t\treturn $qo->get('id');\n\t\t\t\t},\n\t\t\t\t$attempt->get_question_objects( $cache, $filter_removed )\n\t\t\t)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-quiz.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Quiz Model\n *\n * @package  LifterLMS_Tests/Models\n *\n * @group post_models\n * @group quizzes\n * @group quiz\n *\n * @since 3.16.0\n * @since 3.37.2 Added test coverage for many untested methods.\n * @since 4.2.0 Added test coverage for `is_orphan()` method with `$deep` param set to true.\n */\nclass LLMS_Test_LLMS_Quiz extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * class name for the model being tested by the class\n\t *\n\t * @var  string\n\t */\n\tprotected $class_name = 'LLMS_Quiz';\n\n\t/**\n\t * db post type of the model being tested\n\t *\n\t * @var  string\n\t */\n\tprotected $post_type = 'llms_quiz';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t *\n\t * This should match, exactly, the object's $properties array.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return string[]\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\t\t\t'lesson_id' => 'absint',\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'lesson_id' => 123,\n\t\t);\n\t}\n\n\n\t/**\n\t * Test the questions()->create_question() method.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_question() {\n\n\t\t$this->create( 'test title' );\n\t\t$this->assertTrue( is_numeric( $this->obj->questions()->create_question() ) );\n\n\t}\n\n\t/**\n\t * Test the questions()->delete_question() method.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_question() {\n\n\t\t$this->create( 'test title' );\n\t\t$qid = $this->obj->questions()->create_question();\n\t\t$this->assertTrue( $this->obj->questions()->delete_question( $qid ) );\n\n\t\t// belongs to another quiz, can't delete\n\t\t$this->create( 'second question' );\n\t\t$this->assertFalse( $this->obj->questions()->delete_question( $qid ) );\n\n\t\t// doesn't exist\n\t\t$this->assertFalse( $this->obj->questions()->delete_question( 999999999 ) );\n\n\t}\n\n\t/**\n\t * Test get_course() on a quiz with no parent lesson.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_course_no_lesson() {\n\n\t\t$this->create( 'test title' );\n\t\t$this->assertFalse( $this->obj->get_course() );\n\n\t}\n\n\t/**\n\t * Test get_course() on a quiz with a parent lesson which has no parent course.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_course_lesson_no_course() {\n\n\t\t$this->create( 'test title' );\n\t\t$lesson = llms_get_post( $this->factory->post->create( array( 'post_type' => 'lesson' ) ) );\n\t\t$lesson->set( 'quiz', $this->obj->get( 'id' ) );\n\n\t\t$this->assertFalse( $this->obj->get_course() );\n\n\t}\n\n\t/**\n\t * Test get_course() success.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_course() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$quiz = $lesson->get_quiz();\n\t\t$this->assertEquals( $course, $quiz->get_course() );\n\n\t}\n\n\t/**\n\t * Test get_lesson() when no value is set.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lesson_no_value() {\n\n\t\t$this->create( 'test title' );\n\t\t$this->obj->set( 'lesson_id', '' );\n\t\t$this->assertFalse( $this->obj->get_lesson() );\n\n\t}\n\n\t/**\n\t * Test get_lesson() when the value is an invalid post.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lesson_invalid() {\n\n\t\t$this->create( 'test title' );\n\t\t$post = $this->factory->post->create();\n\t\t$this->obj->set( 'lesson_id', ++$post );\n\t\t$this->assertNull( $this->obj->get_lesson() );\n\n\t}\n\n\t/**\n\t * Test get_lesson() success.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lesson() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$quiz = $lesson->get_quiz();\n\t\t$this->assertEquals( $lesson, $quiz->get_lesson() );\n\n\t}\n\n\t/**\n\t * Test the questions()->get_question() method.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_question() {\n\n\t\t$this->create( 'test title' );\n\t\t$qid = $this->obj->questions()->create_question();\n\t\t$this->assertTrue( is_a( $this->obj->questions()->get_question( $qid ), 'LLMS_Question' ) );\n\n\t\t// question doesn't belong to quiz so it should return false\n\t\t$this->create( 'second question' );\n\t\t$this->assertFalse( $this->obj->questions()->get_question( $qid ) );\n\n\t\t// question doesn't exist\n\t\t$this->assertFalse( $this->obj->questions()->get_question( 9999999 ) );\n\n\t}\n\n\t/**\n\t * Test the questions() method.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_questions() {\n\n\t\t$this->create( 'test title' );\n\t\t$i = 1;\n\t\twhile( $i <= 3 ) {\n\t\t\t$this->obj->questions()->create_question();\n\t\t\t$i++;\n\t\t}\n\n\t\t// check default 'questions'\n\t\t$questions = $this->obj->get_questions();\n\t\t$this->assertEquals( 3, count( $questions ) );\n\t\tforeach ( $questions as $question ) {\n\t\t\t$this->assertInstanceOf( 'LLMS_Question', $question );\n\t\t}\n\n\t\t// check posts return\n\t\t$questions = $this->obj->get_questions( 'posts' );\n\t\t$this->assertEquals( 3, count( $questions ) );\n\t\tforeach ( $questions as $question ) {\n\t\t\t$this->assertInstanceOf( 'WP_Post', $question );\n\t\t}\n\n\t\t// check id return\n\t\t$questions = $this->obj->get_questions( 'ids' );\n\t\t$this->assertEquals( 3, count( $questions ) );\n\t\tforeach ( $questions as $question ) {\n\t\t\t$this->assertTrue( is_numeric( $question ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the has_attempt_limit() method.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_attempt_limit() {\n\n\t\t$this->create();\n\n\t\t// No value set.\n\t\t$this->obj->set( 'limit_attempts', '' );\n\t\t$this->assertFalse( $this->obj->has_attempt_limit() );\n\n\t\t// Explicit no.\n\t\t$this->obj->set( 'limit_attempts', 'no' );\n\t\t$this->assertFalse( $this->obj->has_attempt_limit() );\n\n\t\t// Something unexpected (still no).\n\t\t$this->obj->set( 'limit_attempts', 'fake' );\n\t\t$this->assertFalse( $this->obj->has_attempt_limit() );\n\n\t\t// Yes..\n\t\t$this->obj->set( 'limit_attempts', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_attempt_limit() );\n\n\t}\n\n\t/**\n\t * Test the has_time_limit() method.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_has_time_limit() {\n\n\t\t$this->create();\n\n\t\t// No value set.\n\t\t$this->obj->set( 'limit_time', '' );\n\t\t$this->assertFalse( $this->obj->has_time_limit() );\n\n\t\t// Explicit no.\n\t\t$this->obj->set( 'limit_time', 'no' );\n\t\t$this->assertFalse( $this->obj->has_time_limit() );\n\n\t\t// Something unexpected (still no).\n\t\t$this->obj->set( 'limit_time', 'fake' );\n\t\t$this->assertFalse( $this->obj->has_time_limit() );\n\n\t\t// Yes..\n\t\t$this->obj->set( 'limit_time', 'yes' );\n\t\t$this->assertTrue( $this->obj->has_time_limit() );\n\n\t}\n\n\t/**\n\t * Test is_open() with no student.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_open_no_student() {\n\n\t\t$this->create();\n\t\t$this->assertFalse( $this->obj->is_open() );\n\n\t}\n\n\t/**\n\t * Test is_open() with a student when there's no attempt limits.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_open_with_student_no_limits() {\n\n\t\t$this->create();\n\n\t\t$user = $this->factory->student->create();\n\n\t\t// Pass in a user id.\n\t\t$this->assertTrue( $this->obj->is_open( $user ) );\n\n\t\t// Use the current session's user.\n\t\twp_set_current_user( $user );\n\t\t$this->assertTrue( $this->obj->is_open() );\n\n\t}\n\n\t/**\n\t * Test is_open() with a student when there are attempt limits.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_open_with_student_with_limits() {\n\n\t\t$course = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) );\n\t\t$lesson = $course->get_lessons()[0];\n\t\t$quiz = $lesson->get_quiz();\n\n\t\t$quiz->set( 'limit_attempts', 'yes' );\n\t\t$quiz->set( 'allowed_attempts', 1 );\n\n\t\t$user = $this->factory->student->create();\n\n\t\t// Use the current session's user.\n\t\twp_set_current_user( $user );\n\t\t$this->assertTrue( $quiz->is_open() );\n\n\t\t// Pass in a user id.\n\t\t$this->assertTrue( $quiz->is_open( $user ) );\n\n\t\t// Take the quiz.\n\t\t$this->take_quiz( $quiz->get( 'id' ), $user );\n\n\t\t// Use the current session's user.\n\t\t$this->assertFalse( $quiz->is_open() );\n\n\t\t// Pass in a user id.\n\t\t$this->assertFalse( $quiz->is_open( $user ) );\n\n\t}\n\n\t/**\n\t * Test the is_orphan() method.\n\t *\n\t * @since 3.37.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_orphan() {\n\n\t\t$this->create();\n\n\t\t$this->obj->set( 'lesson_id', '' );\n\t\t$this->assertTrue( $this->obj->is_orphan() );\n\n\t\t$this->obj->set( 'lesson_id', 123 );\n\t\t$this->assertFalse( $this->obj->is_orphan() );\n\n\t}\n\n\t/**\n\t * Test the is_orphan() method with the $deep param set to true\n\t *\n\t * @since 4.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_orphan_deep() {\n\n\t\t$this->create();\n\t\t$lesson = llms_get_post( $this->factory->post->create( array( 'post_type' => 'lesson' ) ) );\n\t\t$lesson->set( 'quiz', $this->obj->get( 'id' ) );\n\n\t\t// Quiz `lesson_id` meta unset, we expect both `is_orpan()` and `is_orphan( $deep = true )` to return true.\n\t\t$this->obj->set( 'lesson_id', '' );\n\t\t$this->assertTrue( $this->obj->is_orphan() );\n\t\t$this->assertTrue( $this->obj->is_orphan( true ) );\n\n\t\t// Quiz `lesson_id` set as `$lesson`'s id, we expect both `is_orpan()` and `is_orphan( $deep = true )` to return false.\n\t\t$this->obj->set( 'lesson_id', $lesson->get( 'id' ) );\n\t\t$this->assertFalse( $this->obj->is_orphan() );\n\t\t$this->assertFalse( $this->obj->is_orphan( true ) );\n\n\t\t// quiz `lesson_id` and the lesson's quiz id differ: we expect `is_orpan()` to return false but `is_orphan( $deep = true )` to return true.\n\t\t$lesson->set( 'quiz', 123 );\n\t\t$this->assertFalse( $this->obj->is_orphan() );\n\t\t$this->assertTrue( $this->obj->is_orphan( true ) );\n\n\t\t// quiz `lesson_id` and lesson's quiz id are equal but 123 is not a real lesson's id: we expect `is_orpan()` to return false but `is_orphan( $deep = true )` to return true.\n\t\t$this->obj->set( 'lesson_id', 123 );\n\t\t$this->assertFalse( $this->obj->is_orphan() );\n\t\t$this->assertTrue( $this->obj->is_orphan( true ) );\n\n\t}\n\n\t/**\n\t * Test the questions()->update_question() method.\n\t *\n\t * @since 3.16.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_question() {\n\n\t\t$this->create( 'test title' );\n\n\t\t// create when no id supplied\n\t\t$id = $this->obj->questions()->update_question();\n\t\t$this->assertTrue( is_numeric( $id ) );\n\n\t\t// update should return it's own id\n\t\t$this->assertEquals( $id, $this->obj->questions()->update_question( array( 'id' => $id ) ) );\n\n\t\t// can't update from another quiz\n\t\t$this->create( 'second question' );\n\t\t$this->assertFalse( $this->obj->questions()->update_question( array( 'id' => $id ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-section.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Course Model\n * @group    LLMS_Section\n * @group    LLMS_Post_Model\n * @since    3.24.0\n * @version  3.24.0\n */\nclass LLMS_Test_LLMS_Section extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * class name for the model being tested by the class\n\t * @var  string\n\t */\n\tprotected $class_name = 'LLMS_Section';\n\n\t/**\n\t * db post type of the model being tested\n\t * @var  string\n\t */\n\tprotected $post_type = 'section';\n\n\t/**\n\t * Get properties, used by test_getters_setters\n\t * This should match, exactly, the object's $properties array\n\t * @return   array\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprotected function get_properties() {\n\t\treturn array(\n\t\t\t'order' => 'absint',\n\t\t\t'parent_course' => 'absint',\n\t\t);\n\t}\n\n\t/**\n\t * Get data to fill a create post with\n\t * This is used by test_getters_setters\n\t * @return   array\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'order' => 1,\n\t\t\t'parent_course' => 12345,\n\t\t);\n\t}\n\n\t/**\n\t * the the count_elements() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_count_elements() {\n\n\t\t$section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) );\n\t\t$this->assertEquals( 0, $section->count_elements() );\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 5, 0 )[0] );\n\t\t$section = llms_get_post( $course->get_sections( 'ids' )[0] );\n\t\t$this->assertEquals( 5, $section->count_elements() );\n\n\t}\n\n\t/**\n\t * the the get_course() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_get_course() {\n\n\t\t$section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) );\n\t\t$this->assertNull( $section->get_course() );\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 5, 0 )[0] );\n\t\t$section = llms_get_post( $course->get_sections( 'ids' )[0] );\n\t\t$this->assertTrue( is_a( $section->get_course(), 'LLMS_Course' ) );\n\n\t}\n\n\t// public function test_get_next() {\n\n\t\t// $section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) );\n\t\t// $this->assertNull( $section->get_course() );\n\t\t// var_dump( $section->get_next() );\n\n\t// }\n\n\t/**\n\t * the the get_percent_complete() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_get_percent_complete() {\n\n\t\t$section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) );\n\t\t$this->assertEquals( 0, $section->get_percent_complete() );\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 4, 4, 0, 0 )[0] );\n\t\t$student = $this->get_mock_student();\n\t\t$student->enroll( $course->get( 'id' ) );\n\t\t$uid = $student->get( 'id' );\n\n\t\tforeach ( $course->get_sections() as $i => $section )  {\n\n\t\t\t$perc = ( $i + 1 ) * 25;\n\n\t\t\t// get student by ID\n\t\t\t$this->assertEquals( 0, $section->get_percent_complete( $uid ) );\n\n\t\t\t// get from current user\n\t\t\t$this->assertEquals( 0, $section->get_percent_complete() );\n\n\t\t\t// complete 50% of the lessons in the section\n\t\t\t$this->complete_courses_for_student( $uid, $course->get( 'id' ), ( $perc / 2 ) + ( $i * 12.5 ) );\n\t\t\t$this->assertEquals( 50, $section->get_percent_complete( $uid ) );\n\n\t\t\t// complete the entire section\n\t\t\t$this->complete_courses_for_student( $uid, $course->get( 'id' ), $perc );\n\t\t\t$this->assertEquals( 100, $section->get_percent_complete( $uid ) );\n\n\t\t\t// check as the current user\n\t\t\twp_set_current_user( $uid );\n\t\t\t$this->assertEquals( 100, $section->get_percent_complete() );\n\t\t\twp_set_current_user( null ); // reset\n\n\t\t}\n\n\t}\n\n\t// public function test_get_previous() {}\n\n\t/**\n\t * the the get_lessons() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_get_lessons() {\n\n\t\t$section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) );\n\t\t$lessons = $section->get_lessons( 'ids' );\n\t\t$this->assertEquals( 0, count( $lessons ) );\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 4, 0, 0 )[0] );\n\t\t$section = llms_get_post( $course->get_sections( 'ids' )[0] );\n\n\t\t// get just ids\n\t\t$lessons = $section->get_lessons( 'ids' );\n\t\t$this->assertEquals( 4, count( $lessons ) );\n\t\tarray_map( function( $id ) {\n\t\t\t$this->assertTrue( is_numeric( $id ) );\n\t\t}, $lessons );\n\n\t\t// wp post objects\n\t\t$lessons = $section->get_lessons( 'posts' );\n\t\t$this->assertEquals( 4, count( $lessons ) );\n\t\tarray_map( function( $post ) {\n\t\t\t$this->assertTrue( is_a( $post, 'WP_Post' ) );\n\t\t}, $lessons );\n\n\t\t// lesson objects\n\t\t$lessons = $section->get_lessons( 'sections' );\n\t\t$this->assertEquals( 4, count( $lessons ) );\n\t\tarray_map( function( $lesson ) {\n\t\t\t$this->assertTrue( is_a( $lesson, 'LLMS_Lesson' ) );\n\t\t}, $lessons );\n\n\t}\n\n\t/**\n\t * the the get_children_lessons() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_get_children_lessons() {\n\n\t\t$section = llms_get_post( $this->factory->post->create( array( 'post_type' => 'section' ) ) );\n\t\t$lessons = $section->get_lessons( 'ids' );\n\t\t$this->assertEquals( 0, count( $lessons ) );\n\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 1, 4, 0, 0 )[0] );\n\t\t$section = llms_get_post( $course->get_sections( 'ids' )[0] );\n\n\t\t// wp post objects\n\t\t$lessons = $section->get_lessons( 'posts' );\n\t\t$this->assertEquals( 4, count( $lessons ) );\n\t\tarray_map( function( $post ) {\n\t\t\t$this->assertTrue( is_a( $post, 'WP_Post' ) );\n\t\t}, $lessons );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-student-quizzes.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Student Quizzes Model\n *\n * @group LLMS_Student_Quizzes\n *\n * @since 6.4.0\n * @version 6.4.0\n */\nclass LLMS_Test_LLMS_Student_Quizzes extends LLMS_UnitTestCase {\n\n\t/**\n\t * Student instance.\n\t *\n\t * @var LLMS_Student\n\t */\n\tprotected $student;\n\n\t/**\n\t * Quiz's lesson instance.\n\t *\n\t * @var LLMS_Lesson\n\t */\n\tprotected $lesson;\n\n\t/**\n\t * Quiz instance.\n\t *\n\t * @var LLMS_Quiz\n\t */\n\tprotected $quiz;\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$this->student = $this->get_mock_student();\n\t\t// Create new course with quiz.\n\t\t$courses      = $this->generate_mock_courses( 1, 1, 1, 1, 1 );\n\t\t$course       = llms_get_post( $courses[0] );\n\t\t$this->lesson = $course->get_lessons()[0];\n\t\t$this->quiz   = $this->lesson->get_quiz();\n\n\t}\n\n\t/**\n\t * Test get_attempts_remaining_for_quiz().\n\t *\n\t * @since 6.4.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_attempts_remaining_for_quiz() {\n\n\t\t$this->assertEquals(\n\t\t\t'Unlimited',\n\t\t\t$this->student->quizzes()->get_attempts_remaining_for_quiz( $this->quiz->get( 'id' ) )\n\t\t);\n\n\t\t$attempt = LLMS_Quiz_Attempt::init(\n\t\t\t$this->quiz->get( 'id' ),\n\t\t\t$this->lesson->get( 'id' ),\n\t\t\t$this->student->get( 'id' )\n\t\t);\n\t\t$attempt->save();\n\n\t\t$this->assertEquals(\n\t\t\t'Unlimited',\n\t\t\t$this->student->quizzes()->get_attempts_remaining_for_quiz( $this->quiz->get( 'id' ) )\n\t\t);\n\n\t\t// Limit quiz's attempts.\n\t\t$this->quiz->set( 'limit_attempts', 'yes' );\n\t\t$this->quiz->set( 'allowed_attempts', 2 );\n\n\t\t$this->assertEquals(\n\t\t\t1,\n\t\t\t$this->student->quizzes()->get_attempts_remaining_for_quiz( $this->quiz->get( 'id' ) )\n\t\t);\n\n\t\t$attempt = LLMS_Quiz_Attempt::init(\n\t\t\t$this->quiz->get( 'id' ),\n\t\t\t$this->lesson->get( 'id' ),\n\t\t\t$this->student->get( 'id' )\n\t\t);\n\t\t$attempt->save();\n\n\t\t$this->assertEquals(\n\t\t\t0,\n\t\t\t$this->student->quizzes()->get_attempts_remaining_for_quiz( $this->quiz->get( 'id' ) )\n\t\t);\n\n\t\t// Decrease attempts limit.\n\t\t$this->quiz->set( 'allowed_attempts', 1 );\n\t\t$this->assertEquals(\n\t\t\t0,\n\t\t\t$this->student->quizzes()->get_attempts_remaining_for_quiz( $this->quiz->get( 'id' ) )\n\t\t);\n\t\t$this->assertEquals( // Allow for negative values.\n\t\t\t-1,\n\t\t\t$this->student->quizzes()->get_attempts_remaining_for_quiz( $this->quiz->get( 'id' ), true )\n\t\t);\n\n\t\t// Reset attempts limit.\n\t\t$this->quiz->set( 'limit_attempts', 'no' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-student.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Student Model\n * @group LLMS_Student\n * @group LLMS_Student_Model\n *\n * @since 3.33.0\n * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion.\n * @version 3.36.2\n */\nclass LLMS_Test_LLMS_Student extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup test\n\t *\n\t * @since 3.33.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->student   = $this->get_mock_student();\n\t\t// Create new course\n\t\t$this->course_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'course',\n\t\t));\n\t\t// Create new membership\n\t\t$this->memb_id   = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t));\n\n\t}\n\n\t/**\n\t* Functional test for the enroll() method.\n\t*\n\t* @since 3.33.0\n\t* @see user/class-llms-test-student.php for integration tests.\n\t*\n\t* @return void\n\t*/\n\tpublic function test_enroll() {\n\n\t\t// check against both courses and memberships\n\n\t\t// enroll in a non existent course/membership\n\t\t$this->assertFalse( $this->student->enroll( $this->course_id + 100, 'test_is_enrolled' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_enrolled_in_course' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_added_to_membership_level' ) );\n\t\t$this->assertFalse( $this->student->enroll( $this->memb_id + 100, 'test_is_enrolled' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_enrolled_in_course' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_added_to_membership_level' ) );\n\n\t\t// enroll a student\n\t\t$this->assertTrue( $this->student->enroll( $this->course_id, 'test_is_enrolled' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_enrolled_in_course' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_added_to_membership_level' ) );\n\t\t$this->assertTrue( $this->student->enroll( $this->memb_id, 'test_is_enrolled' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_enrolled_in_course' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_added_to_membership_level' ) );\n\n\t\t// enroll a student twice\n\t\t$this->assertFalse( $this->student->enroll( $this->course_id, 'test_is_enrolled' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_enrolled_in_course' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_added_to_membership_level' ) );\n\n\t\t// check re-enroll\n\t\t$this->student->unenroll( $this->course_id, 'test_is_enrolled', 'expired' );\n\t\t$this->assertTrue( $this->student->enroll( $this->course_id, 'test_is_enrolled' ) );\n\t\t$this->assertEquals( 2, did_action( 'llms_user_enrolled_in_course' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_added_to_membership_level' ) );\n\n\t}\n\n\t/**\n\t * Functional test for the unenroll() method.\n\t *\n\t * @since 3.33.0\n\t * @since 6.0.0 Changed use of the deprecated `llms_user_removed_from_membership_level` action hook to `llms_user_removed_from_membership`.\n\t *\n\t * @see user/class-llms-test-student.php for integration tests.\n\t *\n\t * @return void\n\t */\n\tpublic function test_unenroll() {\n\n\t\t// unenroll a non enrolled student\n\t\t$this->assertFalse( $this->student->unenroll( $this->course_id ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_removed_from_course' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_removed_from_membership' ) );\n\t\t$this->assertFalse( $this->student->unenroll( $this->memb_id ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_removed_from_course' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_removed_from_membership' ) );\n\n\t\t// unenroll a student in a course\n\t\t$this->student->enroll( $this->course_id );\n\t\t$this->assertTrue( $this->student->unenroll( $this->course_id ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_removed_from_course' ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_removed_from_membership' ) );\n\n\t\t// unenroll a student in a membership\n\t\t$this->student->enroll( $this->memb_id );\n\t\t$this->assertTrue( $this->student->unenroll( $this->memb_id ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_removed_from_course' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_removed_from_membership' ) );\n\n\t\t// try to unenroll a student with a different trigger\n\t\t$this->student->enroll( $this->memb_id );\n\t\t$res = $this->student->unenroll( $this->memb_id, $this->student->get_enrollment_trigger( $this->memb_id ) . '_test' );\n\t\t$this->assertFalse( $res );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_removed_from_course' ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_removed_from_membership' ) );\n\n\t}\n\n\t/**\n\t * Enroll student in a two memberships with overlapping courses, and make sure they aren't un-enrolled from the course they should have access to.\n\t *\n\t * @return void\n\t */\n\tpublic function test_auto_enroll_and_unenroll_with_overlapping_courses() {\n\t\t$course_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'course',\n\t\t));\n\t\t$course_id_2 = $this->factory->post->create( array(\n\t\t\t'post_type' => 'course',\n\t\t));\n\t\t$memb_id = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t));\n\t\t$memb_id_2   = $this->factory->post->create( array(\n\t\t\t'post_type' => 'llms_membership',\n\t\t));\n\t\t$membership = new LLMS_Membership( $memb_id );\n\t\t$membership->add_auto_enroll_courses( array( $course_id, $course_id_2 ) );\n\t\t$membership_2 = new LLMS_Membership( $memb_id_2 );\n\t\t$membership_2->add_auto_enroll_courses( array( $course_id_2 ) );\n\t\t$this->student->enroll( $memb_id );\n\t\t$this->student->enroll( $memb_id_2 );\n\t\t$this->assertTrue( $this->student->is_enrolled( $course_id ) );\n\t\t$this->assertTrue( $this->student->is_enrolled( $course_id_2 ) );\n\n\t\t// With no sleep, the enrollment triggers are at the same time, so the test will fail since sorting by updated date returns the first trigger.\n\t\tsleep(1);\n\n\t\t// Unenrolling in the first membership should keep them in course 2, since they have access via membership 2's auto enroll courses.\n\t\t$this->student->unenroll( $memb_id );\n\t\t$this->assertFalse( $this->student->is_enrolled( $course_id ) );\n\t\t$this->assertTrue( $this->student->is_enrolled( $course_id_2 ) );\n\n\t\t// Unenrolling in the second membership should remove them from course 2.\n\t\t$this->student->unenroll( $memb_id_2 );\n\t\t$this->assertFalse( $this->student->is_enrolled( $course_id_2 ) );\n\t}\n\n\t/**\n\t * Functional test for the delete_enrollment() method.\n\t *\n\t * @since 3.33.0\n\t * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion.\n\t * @see user/class-llms-test-student.php for integration tests.\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_enrollment() {\n\n\t\t// delete a non existent enrollment: user not enrolled at all.\n\t\t$this->assertFalse( $this->student->delete_enrollment( $this->course_id ) );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_enrollment_deleted' ) );\n\n\t\t// enroll a student.\n\t\t$this->student->enroll( $this->course_id );\n\n\t\t// delete a non existent enrollment: user enrolled with a different trigger.\n\t\t$res = $this->student->delete_enrollment( $this->course_id, $this->student->get_enrollment_trigger( $this->course_id ) . '_test' );\n\t\t$this->assertFalse( $res );\n\t\t$this->assertEquals( 0, did_action( 'llms_user_enrollment_deleted' ) );\n\n\t\t// delete an existent enrollment.\n\t\t$this->assertTrue( $this->student->delete_enrollment( $this->course_id , $this->student->get_enrollment_trigger( $this->course_id ) ) );\n\t\t$this->assertEquals( 1, did_action( 'llms_user_enrollment_deleted' ) );\n\n\t\t$this->student->enroll( $this->course_id );\n\n\t\t// delete an existent enrollment: any trigger.\n\t\t$this->assertTrue( $this->student->delete_enrollment( $this->course_id ) );\n\t\t$this->assertEquals( 2, did_action( 'llms_user_enrollment_deleted' ) );\n\n\t\t// Test auto-enrollments deletion.\n\n\t\t// create a membership.\n\t\t$membership    = new LLMS_Membership( 'new', 'Membership Title' );\n\t\t$membership_id = $membership->get('id');\n\t\t// create two courses and set them as membership auto-enrollments.\n\t\t$courses = $this->factory->course->create_many( 2, array( 0, 0, 0, 0 ) );\n\t\t$membership->set( 'auto_enroll', $courses );\n\n\t\t$actions = did_action( 'llms_user_enrollment_deleted' );\n\n\t\t// enroll a student to the membership.\n\t\t$this->student->enroll( $membership_id );\n\n\t\t$res = $this->student->delete_enrollment( $membership_id, $this->student->get_enrollment_trigger( $membership_id  ) );\n\t\t$this->assertTrue( $res );\n\t\t// test we had 3 deletion: the membership, and the related courses.\n\t\t$this->assertEquals( $actions + 3, did_action( 'llms_user_enrollment_deleted' ) );\n\t}\n\n\t/**\n\t * Test get_enrollments() returns accurate found count without SQL_CALC_FOUND_ROWS.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_enrollments_found_count() {\n\n\t\t$courses = $this->generate_mock_courses( 5, 1, 1, 0 );\n\n\t\tforeach ( $courses as $cid ) {\n\t\t\t$this->student->enroll( $cid );\n\t\t}\n\n\t\t$result = $this->student->get_enrollments( 'course', array( 'limit' => 2 ) );\n\n\t\t$this->assertSame( 5, $result['found'] );\n\t\t$this->assertSame( 2, $result['limit'] );\n\t\t$this->assertCount( 2, $result['results'] );\n\t\t$this->assertTrue( $result['more'] );\n\n\t\t$result_all = $this->student->get_enrollments( 'course', array( 'limit' => 10 ) );\n\t\t$this->assertSame( 5, $result_all['found'] );\n\t\t$this->assertCount( 5, $result_all['results'] );\n\t\t$this->assertFalse( $result_all['more'] );\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-transaction.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Transaction Model\n *\n * @group LLMS_Transaction\n * @group LLMS_Post_Model\n *\n * @since 5.9.0\n */\nclass LLMS_Test_LLMS_Transaction extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Class name for the model being tested by the class.\n\t *\n\t * @var  string\n\t */\n\tprotected $class_name = 'LLMS_Transaction';\n\n\t/**\n\t * DB post type of the model being tested.\n\t *\n\t * @var  string\n\t */\n\tprotected $post_type = 'llms_transaction';\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'api_mode'                   => 'live',\n\t\t\t'amount'                     => 25.99,\n\t\t\t'currency'                   => 'USD',\n\t\t\t'gateway_completed_date'     => '2021-02-24 23:23:59',\n\t\t\t'gateway_customer_id'        => 'customer_id',\n\t\t\t'gateway_fee_amount'         => 1.99,\n\t\t\t'gateway_source_id'          => 'source_id',\n\t\t\t'gateway_source_description' => 'Vist **** 2342',\n\t\t\t'gateway_transaction_id'     => 'transaction_id',\n\t\t\t'order_id'                   => 123,\n\t\t\t'payment_type'               => 'recurring',\n\t\t\t'payment_gateway'            => 'payway',\n\t\t\t'refund_amount'              => 2.99,\n\t\t\t'refund_data'                => array( 'stuff' => 123 ),\n\t\t);\n\t}\n\n\t/**\n\t * Test creation of the model.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_model() {\n\n\t\tllms_tests_mock_current_time( '2021-03-05 01:05:23' );\n\n\t\t$this->create( 123 );\n\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t$test = llms_get_post( $id );\n\n\t\t$this->assertEquals( $id, $test->get( 'id' ) );\n\t\t$this->assertEquals( 'llms_transaction', $test->get( 'type' ) );\n\t\t$this->assertEquals( 'Transaction for Order #123 &ndash; Mar 05, 2021 @ 01:05 AM', $test->get( 'title' ) );\n\n\t\t$this->assertEquals( '2021-03-05 01:05:23', $test->get( 'date' ) );\n\t\t$this->assertEquals( 'llms-txn-pending', $test->get( 'status' ) );\n\n\t\t$this->assertTrue( post_password_required( $id ) );\n\n\t}\n\n\t/**\n\t * Test generate_refund_id() when processing manually.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_refund_id_manual() {\n\n\t\t$this->create( 123 );\n\n\t\t$expected_id = 'ABCDE12345';\n\t\t$id_handler = function( $id ) use ( $expected_id ) {\n\t\t\treturn $expected_id;\n\t\t};\n\t\tadd_filter( 'llms_manual_refund_id', $id_handler );\n\n\t\t$this->assertEquals( \n\t\t\t$expected_id,\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->obj, 'generate_refund_id', array( 'manual', 1.00 ) )\n\t\t);\n\n\t\tremove_filter( 'llms_manual_refund_id', $id_handler );\n\n\t}\n\n\t/**\n\t * Test generate_refund_id() when processing via a gateway.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_refund_gateway() {\n\n\t\t$this->create( 123 );\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->obj, 'generate_refund_id', array( 'gateway', 1.00 ) );\n\t\t$this->assertWPErrorCodeEquals( 'llms-txn-refund-gateway-invalid', $res );\n\n\t}\n\n\t/**\n\t * Test generate_refund_id() when using a custom processing method.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_refund_custom() {\n\n\t\t$this->create( 123 );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'generate_refund_id', array( 'custom', 1.00 ) ) );\n\n\t}\n\n\t/**\n\t * Test get_refund_method_title() with manual processing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_refund_method_title_manual() {\n\n\t\t$this->create( 123 );\n\t\t$this->assertEquals( \n\t\t\t'manual refund',\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->obj, 'get_refund_method_title', array( 'manual' ) )\n\t\t);\n\n\t}\n\n\t/**\n\t * Test get_refund_method_title() with gateway processing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_refund_method_title_gateway() {\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-process-refund-title';\n\t\t\tpublic $admin_title = 'Gateway Title';\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$this->create( 123 );\n\t\t$this->obj->set( 'payment_gateway', 'fake-process-refund-title' );\n\t\t$this->assertEquals( \n\t\t\t'Gateway Title',\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->obj, 'get_refund_method_title', array( 'gateway' ) )\n\t\t);\n\n\t\t$this->unload_payment_gateway( 'fake-process-refund-title' );\n\n\t}\n\n\t/**\n\t * Test get_refund_method_title() with custom processing.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_refund_method_title_custom() {\n\n\t\t$this->create( 123 );\n\t\t$this->assertEquals( \n\t\t\t'custom',\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->obj, 'get_refund_method_title', array( 'custom' ) )\n\t\t);\t\n\n\t}\n\n\t/**\n\t * Test get_refunds() and get_refund().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_refund_and_get_refunds() {\n\n\t\t$this->create( 123 );\n\n\t\t$refund_data = array(\n\t\t\t'123' => array(\n\t\t\t\t'id'     => '123',\n\t\t\t\t'amount' => 12.99,\n\t\t\t\t'method' => 'manual',\n\t\t\t\t'date'   => llms_current_time( 'mysql' ),\n\t\t\t),\n\t\t);\n\n\t\t$this->obj->set( 'refund_data', $refund_data );\n\n\t\t// Get all.\n\t\t$this->assertEquals( $refund_data, $this->obj->get_refunds() );\n\n\t\t// Get one.\n\t\t$this->assertEquals( $refund_data['123'], $this->obj->get_refund( '123' ) );\n\n\t\t// Get one (invalid).\n\t\t$this->assertFalse( $this->obj->get_refund( '456' ) );\n\n\t}\n\n\t/**\n\t * Skip unneeded test.\n\t *\n\t * @since 5.9.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_edit_date() {\n\t\t$this->assertTrue( true );\n\t}\n\n\t/**\n\t * Test can_be_refunded().\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_be_refunded() {\n\t\t\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id' => $this->factory->order->create(),\n\t\t\t'amount'   => 25.00,\n\t\t) );\n\n\t\t$tests = array(\n\t\t\t'llms-txn-failed'    => false,\n\t\t\t'llms-txn-pending'   => false,\n\t\t\t'llms-txn-refunded'  => true,\n\t\t\t'llms-txn-succeeded' => true,\n\t\t);\n\n\t\tforeach ( $tests as $status => $expected ) {\n\t\t\t$this->obj->set( 'status', $status );\n\t\t\t$this->assertEquals( $expected, $this->obj->can_be_refunded(), $status );\n\t\t}\n\n\t\t// No money left to refund.\n\t\t$this->obj->set( 'refund_amount', 25.00 );\n\t\t$this->assertFalse( $this->obj->can_be_refunded() );\n\n\t}\n\n\t/**\n\t * Test process_refund() when the transaction isn't eligible for a refund.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_refund_no_eligible() {\n\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id' => $this->factory->order->create(),\n\t\t) );\n\n\t\t$res = $this->obj->process_refund( 25.00 );\n\t\t$this->assertWPErrorCodeEquals( 'llms-txn-refund-not-eligible', $res );\n\n\t}\n\n\t/**\n\t * Test process_refund() when the requested refund amount exceeds the remaining available amount on the transaction.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_refund_amount_too_high() {\n\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id' => $this->factory->order->create(),\n\t\t\t'amount'   => 25.00,\n\t\t) );\n\t\t$this->obj->set( 'status', 'llms-txn-succeeded' );\n\n\n\t\t$res = $this->obj->process_refund( 35.00 );\n\t\t$this->assertWPErrorCodeEquals( 'llms-txn-refund-amount-too-high', $res );\n\n\t}\n\n\t/**\n\t * Test process_refund() unknown error.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_refund_err_unknown() {\n\n\t\t$order = $this->factory->order->create_and_get();\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t'amount'   => 25.00,\n\t\t) );\n\t\t$this->obj->set( 'status', 'llms-txn-succeeded' );\t\n\n\t\t$res = $this->obj->process_refund( 10.00, 'A note', 'fake-method' );\n\t\t$this->assertWPErrorCodeEquals( 'llms-txn-refund-unknown-error', $res );\n\n\t}\n\n\t/**\n\t * Test process_refund() through a manual gateway success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_refund_manual() {\n\n\t\t$now = time();\n\t\tllms_tests_mock_current_time( $now );\n\n\t\t// Remove filter so we can test order notes.\n\t\tremove_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\n\t\t$expected_id = (string) $now;\n\t\t$id_handler = function( $id ) use ( $expected_id ) {\n\t\t\treturn $expected_id;\n\t\t};\n\t\tadd_filter( 'llms_manual_refund_id', $id_handler );\n\n\t\t$order = $this->factory->order->create_and_get();\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t'amount'   => 25.00,\n\t\t) );\n\t\t$this->obj->set( 'status', 'llms-txn-succeeded' );\t\n\n\t\t$txn_id = $this->obj->get( 'id' );\n\n\t\t// Expected ID is returned.\n\t\t$res = $this->obj->process_refund( 10.00, 'A note' );\n\t\t$this->assertSame( $res, $expected_id );\n\n\t\t// Note is recorded.\n\t\t$notes = $order->get_notes();\n\t\t$found_note = false;\n\t\tforeach ( $notes as $note ) {\n\t\t\t$expected_note = \"Refunded &#36;10.00 for transaction #{$txn_id} via manual refund [Refund ID: {$expected_id}]\\r\\nRefund Notes: \\r\\nA note\";\n\t\t\tif ( $note->comment_content === $expected_note ) {\n\t\t\t\t$found_note = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t$this->assertTrue( $found_note );\n\n\t\t// Transaction status updated.\n\t\t$this->assertEquals( 'llms-txn-refunded', get_post_status( $txn_id ) );\n\n\t\t// Refund amount updated.\n\t\t$this->assertEquals( 10.00, $this->obj->get( 'refund_amount' ) );\n\n\t\t// Refund data saved.\n\t\t$expected_data = array(\n\t\t\t$expected_id => array(\n\t\t\t\t'amount' => 10.00,\n\t\t\t\t'date'   => date( 'Y-m-d H:i:s', $now ),\n\t\t\t\t'id'     => $expected_id,\n\t\t\t\t'method' => 'manual',\n\t\t\t),\n\t\t);\n\t\t$this->assertEquals( $expected_data, $this->obj->get( 'refund_data' ) );\n\n\t\tadd_filter( 'comments_clauses', array( 'LLMS_Comments', 'exclude_order_comments' ) );\n\t\tremove_filter( 'llms_manual_refund_id', $id_handler );\n\n\t}\n\n\t/**\n\t * Test process_refund() through an invalid gateway.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_refund_gateway_invalid() {\n\n\t\t$order = $this->factory->order->create_and_get();\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id'        => $order->get( 'id' ),\n\t\t\t'amount'          => 25.00,\n\t\t\t'payment_gateway' => 'fake-process-refund',\n\t\t) );\n\t\t$this->obj->set( 'status', 'llms-txn-succeeded' );\t\t\n\n\t\t$res = $this->obj->process_refund( 10.00, '', 'gateway' );\n\t\t$this->assertWPErrorCodeEquals( 'llms-txn-refund-gateway-invalid', $res );\n\n\t}\n\n\t/**\n\t * Test process_refund() through a gateway that doesn't support refunds.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_refund_gateway_doesnt_support_refunds() {\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-process-refund';\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$order = $this->factory->order->create_and_get();\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id'        => $order->get( 'id' ),\n\t\t\t'amount'          => 25.00,\n\t\t\t'payment_gateway' => 'fake-process-refund',\n\t\t) );\n\t\t$this->obj->set( 'status', 'llms-txn-succeeded' );\t\t\n\n\t\t$res = $this->obj->process_refund( 10.00, '', 'gateway' );\n\t\t$this->assertWPErrorCodeEquals( 'llms-txn-refund-gateway-support', $res );\n\n\t\t$this->unload_payment_gateway( 'fake-process-refund' );\n\n\t}\n\n\t/**\n\t * Test process_refund() through a gateway success.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_process_refund_gateway_success() {\n\n\t\t// Setup a fake payment gateway.\n\t\t$gateway = new class() extends LLMS_Payment_Gateway {\n\t\t\tpublic $id = 'fake-process-refund';\n\t\t\tpublic $supports = array(\n\t\t\t\t'refunds' => true,\n\t\t\t);\n\t\t\tpublic function handle_pending_order( $order, $plan, $person, $coupon = false ) {}\n\t\t\tpublic function process_refund( $transaction, $amount = 0, $note = '' ) {\n\t\t\t\treturn 'ABCDE12345';\n\t\t\t}\n\t\t};\n\t\t$this->load_payment_gateway( $gateway );\n\n\t\t$order = $this->factory->order->create_and_get();\n\t\t$this->create();\n\t\t$this->obj->set( array(\n\t\t\t'order_id'        => $order->get( 'id' ),\n\t\t\t'amount'          => 25.00,\n\t\t\t'payment_gateway' => 'fake-process-refund',\n\t\t) );\n\t\t$this->obj->set( 'status', 'llms-txn-succeeded' );\t\t\n\n\t\t$res = $this->obj->process_refund( 10.00, '', 'gateway' );\n\t\t$this->assertEquals( 'ABCDE12345', $res );\n\n\t\t$this->unload_payment_gateway( 'fake-process-refund' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-user-achievement.php",
    "content": "<?php\n/**\n * Tests for awarded user achievements\n *\n * @group models\n * @group achievements\n * @group engagements\n * @group LLMS_User_Achievement\n *\n * @since 4.5.0\n * @since 6.0.0 Added tests for the new methods.\n */\nclass LLMS_Test_LLMS_User_Achievement extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Class name for the model being tested by the class\n\t *\n\t * @var string\n\t */\n\tprotected $class_name = 'LLMS_User_Achievement';\n\n\t/**\n\t * Will hold an instance of the model being tested by the class.\n\t *\n\t * @var LLMS_User_Achievement\n\t */\n\tprotected $obj;\n\n\t/**\n\t * DB post type of the model being tested\n\t *\n\t * @var string\n\t */\n\tprotected $post_type = 'llms_my_achievement';\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters.\n\t *\n\t * @since 4.5.0\n\t * @since 6.0.0 Add new properties.\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'awarded'    => '2021-12-10 23:02:59',\n\t\t\t'engagement' => 3,\n\t\t\t'related'    => 4,\n\t\t);\n\t}\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tllms()->achievements();\n\n\t}\n\n\t/**\n\t * Test the after_create() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_after_create() {\n\n\t\t$actions = did_action( 'llms_achievement_synchronized' );\n\n\t\t$template_id = $this->create_achievement_template();\n\n\t\t$this->obj = new $this->class_name( 'new', array( 'post_parent' => $template_id ) );\n\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_achievement_synchronized' ) );\n\t}\n\n\t/**\n\t * Test creation of the model\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_model() {\n\n\t\t$this->create( 'test title' );\n\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t$test = new LLMS_User_Achievement( $id );\n\n\t\t$this->assertEquals( $id, $test->get( 'id' ) );\n\t\t$this->assertEquals( $this->post_type, $test->get( 'type' ) );\n\t\t$this->assertEquals( 'test title', $test->get( 'title' ) );\n\n\t}\n\n\t/**\n\t * Test delete() method\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete() {\n\n\t\tglobal $wpdb;\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$earned   = $this->earn_achievement( $uid, $this->create_achievement_template(), $this->factory->post->create() );\n\t\t$achievement_id  = $earned[1];\n\t\t$achievement = new LLMS_User_Achievement( $achievement_id );\n\n\t\t$actions = array(\n\t\t\t'before' => did_action( 'llms_before_delete_achievement' ),\n\t\t\t'after'  => did_action( 'llms_delete_achievement' ),\n\t\t);\n\n\t\t$achievement->delete();\n\n\t\t// User meta is gone.\n\t\t$res = $wpdb->get_results( \"SELECT * FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = {$uid} AND meta_key = '_achievement_earned' AND meta_value = {$achievement_id}\" );\n\t\t$this->assertEquals( array(), $res );\n\n\t\t// Post is deleted.\n\t\t$this->assertNull( get_post( $achievement_id ) );\n\n\t\t// Ran actions.\n\t\t$this->assertEquals( ++$actions['before'], did_action( 'llms_before_delete_achievement' ) );\n\t\t$this->assertEquals( ++$actions['after'], did_action( 'llms_delete_achievement' ) );\n\n\t}\n\n\t/**\n\t * Test get_earned_date()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_earned_date() {\n\n\t\t$this->create();\n\n\t\t$date = $this->obj->post->post_date;\n\n\t\t// Request a format.\n\t\t$this->assertEquals( $date, $this->obj->get_earned_date( 'Y-m-d H:i:s' ) );\n\n\t\t// Default blog format.\n\t\t$this->assertEquals( date( 'F j, Y', strtotime( $date ) ), $this->obj->get_earned_date() );\n\n\t}\n\n\t/**\n\t * Test get_image()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_image() {\n\n\t\t// Default.\n\t\t$achievement = new LLMS_User_Achievement( $this->factory->post->create( array( 'post_type' => $this->post_type ) ) );\n\n\t\t$src = $achievement->get_image();\n\t\t$this->assertStringContainsString( 'default-achievement.png', $src );\n\n\t\t// Has image.\n\t\t$attachment = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\tset_post_thumbnail( $achievement->get( 'id' ), $attachment );\n\n\t\t$src = $achievement->get_image();\n\t\t$this->assertMatchesRegularExpression(\n\t\t\t'#http:\\/\\/example.org\\/wp-content\\/uploads\\/\\d{4}\\/\\d{2}\\/yura-timoshenko-R7ftweJR8ks-unsplash(?:(-\\d+)*(-\\d+x\\d+)*).jpeg#',\n\t\t\t$src\n\t\t);\n\n\t}\n\n\t/**\n\t * Test get_image_html()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_image_html() {\n\n\t\t// Default.\n\t\t$achievement = new LLMS_User_Achievement( $this->factory->post->create( array( 'post_type' => $this->post_type, 'post_title' => 'Test Title' ) ) );\n\n\t\t$attachment = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\tset_post_thumbnail( $achievement->get( 'id' ), $attachment );\n\n\t\t$html = $achievement->get_image_html();\n\n\t\t$this->assertEquals( 0, strpos( $html, '<img ' ) );\n\t\t$this->assertStringContainsString( 'alt=\"Test Title\"', $html );\n\t\t$this->assertStringContainsString( 'class=\"llms-achievement-img\"', $html );\n\n\t\t$this->assertEquals(\n\t\t\t1,\n\t\t\tpreg_match(\n\t\t\t\t'#src=\"http:\\/\\/example.org\\/wp-content\\/uploads\\/\\d{4}\\/\\d{2}\\/yura-timoshenko-R7ftweJR8ks-unsplash(?:(-\\d+)*(-\\d+x\\d+)*).jpeg\"#',\n\t\t\t\t$html\n\t\t\t)\n\t\t);\n\n\t}\n\n\n\t/**\n\t * Test get_related_post_id()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_related_post_id() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_achievement( $uid, $this->create_achievement_template(), $related );\n\t\t$achievement_id  = $earned[1];\n\t\t$achievement = new LLMS_User_Achievement( $achievement_id );\n\n\t\t$this->assertEquals( $related, $achievement->get_related_post_id() );\n\n\t}\n\n\t/**\n\t * Test get_user_id()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_user_id() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_achievement( $uid, $this->create_achievement_template(), $related );\n\t\t$achievement_id  = $earned[1];\n\t\t$achievement = new LLMS_User_Achievement( $achievement_id );\n\n\t\t$this->assertEquals( $uid, $achievement->get_user_id() );\n\n\t}\n\n\t/**\n\t * Test get_user_postmeta()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_user_postmeta() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_achievement( $uid, $this->create_achievement_template(), $related );\n\t\t$achievement_id  = $earned[1];\n\t\t$achievement = new LLMS_User_Achievement( $achievement_id );\n\n\t\t$expect = new stdClass();\n\t\t$expect->user_id = $uid;\n\t\t$expect->post_id = $related;\n\t\t$this->assertEquals( $expect, $achievement->get_user_postmeta() );\n\n\t}\n\n\t/**\n\t * Test is_awarded().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_awarded() {\n\n\t\t$this->create();\n\n\t\t$this->obj->set( 'status', 'publish' );\n\t\t$this->obj->set( 'awarded', '' );\n\n\t\t$this->assertFalse( $this->obj->is_awarded() );\n\n\t\t$this->obj->set( 'awarded', llms_current_time( 'mysql' ) );\n\t\t$this->assertTrue( $this->obj->is_awarded() );\n\n\t\t$this->obj->set( 'status', 'draft' );\n\t\t$this->assertFalse( $this->obj->is_awarded() );\n\n\t}\n\n\t/**\n\t * Test sync()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$user_info = array(\n\t\t\t'first_name' => 'Walter',\n\t\t\t'last_name'  => 'Sobchak',\n\t\t\t'user_email' => 'sync-achievement-user@mail.tld',\n\t\t\t'user_login' => 'sync-achievement-user'\n\t\t);\n\n\t\t$user    = $this->factory->student->create_and_get( $user_info );\n\t\t$related = $this->factory->post->create();\n\n\t\t$content          = '';\n\t\t$expected_content = '';\n\n\t\t$template    = $this->create_achievement_template( 'Title', $content, 456 );\n\t\t/** @var LLMS_User_Achievement $achievement */\n\t\t$achievement = LLMS_Unit_Test_Util::call_method( 'LLMS_Engagement_Handler', 'create', array(\n\t\t\t'achievement',\n\t\t\t$user->get( 'id' ),\n\t\t\t$template,\n\t\t\t$related\n\t\t) );\n\n\t\t$this->assertEquals( $expected_content, $achievement->get( 'content', true ) );\n\n\t\t// Update the template and sync.\n\t\t$thumbnail_id = $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\twp_update_post( array(\n\t\t\t'ID'           => $template,\n\t\t\t'post_content' => 'Updated',\n\t\t\t'post_title'   => 'Template Title',\n\t\t\t'meta_input'   => array(\n\t\t\t\t'_thumbnail_id' => $thumbnail_id,\n\t\t\t)\n\t\t) );\n\n\t\t$this->assertTrue( $achievement->sync() );\n\t\t$this->assertEquals( 'Updated', $achievement->get( 'content', true ) );\n\t\t$this->assertEquals( 'Title', $achievement->get( 'title', true ) );\n\t\t$this->assertEquals( $thumbnail_id, get_post_thumbnail_id( $achievement->get( 'id' ) ) );\n\t}\n\n\t/**\n\t * Test sync() when an error is encountered.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_errors() {\n\n\t\t$this->create();\n\t\t$this->obj->set( 'parent', $this->factory->post->create() + 1 );\n\n\t\t// This is just testing that an error is returned, the rest of the conditions are tested against LLMS_Engagement_Handler::check_post() directly.\n\t\t$this->assertFalse( $this->obj->sync() );\n\t}\n\n\t/**\n\t * Test syncing an awarded engagement with its template after removing a thumbnail.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_template_after_removing_thumbnail() {\n\n\t\t// Create a template with a thumbnail.\n\t\t$img_id      = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$title       = 'Sync Template Removing Thumbnail';\n\t\t$template_id = $this->create_achievement_template( $title, null, $img_id );\n\n\t\t// Create an awarded engagement.\n\t\t$this->create( array( 'post_parent' => $template_id ) );\n\n\t\t// Test that the awarded engagement matches the template.\n\t\t$id = $this->obj->get( 'id' );\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\n\t\t// Remove the template thumbnail.\n\t\tdelete_post_thumbnail( $template_id );\n\n\t\t// Sync the awarded engagement with the template.\n\t\t$this->assertTrue( $this->obj->sync() );\n\n\t\t// Test that the awarded engagement no longer has a thumbnail.\n\t\t$this->assertFalse( (bool) get_post_thumbnail_id( $id ) );\n\t}\n\n\t/**\n\t * Test that syncing an awarded engagement with its template twice keeps the same thumbnail.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_template_twice_keep_thumbnail() {\n\n\t\t// Create a template with a thumbnail.\n\t\t$img_id      = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$title       = 'Sync Template Twice with Thumbnail';\n\t\t$template_id = $this->create_achievement_template( $title, null, $img_id );\n\n\t\t// Create an awarded engagement.\n\t\t$this->create( array( 'post_parent' => $template_id ) );\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t// Test that the awarded engagement matches the template.\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\n\t\t// Sync (twice).\n\t\t$this->assertTrue( $this->obj->sync() );\n\t\t$this->assertTrue( $this->obj->sync() );\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/models/class-llms-test-model-llms-user-certificate.php",
    "content": "<?php\n/**\n * Tests for earned user certificates\n *\n * @group models\n * @group certificates\n * @group engagements\n * @group LLMS_User_Certificate\n *\n * @since 4.5.0\n * @since 6.0.0 Added tests for the new methods.\n */\nclass LLMS_Test_LLMS_User_Certificate extends LLMS_PostModelUnitTestCase {\n\n\t/**\n\t * Class name for the model being tested by the class\n\t *\n\t * @var string\n\t */\n\tprotected $class_name = 'LLMS_User_Certificate';\n\n\t/**\n\t * Will hold an instance of the model being tested by the class.\n\t *\n\t * @var LLMS_User_Certificate\n\t */\n\tprotected $obj = null;\n\n\t/**\n\t * DB post type of the model being tested\n\t *\n\t * @var string\n\t */\n\tprotected $post_type = 'llms_my_certificate';\n\n\t/**\n\t * Get data to fill a create post with\n\t *\n\t * This is used by test_getters_setters.\n\t *\n\t * @since 4.5.0\n\t * @since 6.0.0 Add new properties.\n\t *\n\t * @return array\n\t */\n\tprotected function get_data() {\n\t\treturn array(\n\t\t\t'allow_sharing' => 'no',\n\t\t\t'awarded'       => '2021-12-10 23:02:59',\n\t\t\t'background'    => '#eaeaea',\n\t\t\t'engagement'    => 3,\n\t\t\t'height'        => 5.5,\n\t\t\t'margins'       => array( 2, 3, 0.5, 1.83 ),\n\t\t\t'orientation'   => 'landscape',\n\t\t\t'related'       => 4,\n\t\t\t'sequential_id' => 5,\n\t\t\t'size'          => 'A4',\n\t\t\t'unit'          => 'mm',\n\t\t\t'width'         => 230,\n\t\t);\n\t}\n\n\t/**\n\t * Setup before class\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tllms()->certificates();\n\n\t}\n\n\t/**\n\t * Test sequential id increment on creation.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sequential_id_increment() {\n\n\t\t$actions = did_action( 'llms_certificate_synchronized' );\n\n\t\t$template_id = $this->create_certificate_template();\n\t\tupdate_post_meta( $template_id, '_llms_sequential_id', 25 );\n\n\t\t$cert = new $this->class_name( 'new', array( 'post_parent' => $template_id ) );\n\n\t\t// Awarded certificate not published, no sequential id set.\n\t\t$this->assertNotEquals( 'publish', $cert->get( 'status' ) );\n\t\t$this->assertEquals( 1, $cert->get( 'sequential_id' ) );\n\t\t$this->assertEquals( '', get_post_meta( $cert->get('id'), '_llms_sequential_id', true ) );\n\t\t$this->assertEquals( ++$actions, did_action( 'llms_certificate_synchronized' ) );\n\n\t\t// Publish the awarded certificate, sequential id incremented.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'          => $cert->id,\n\t\t\t\t'post_status' => 'publish',\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals( 25, $cert->get( 'sequential_id' ) );\n\t\t$this->assertEquals( 25, get_post_meta( $cert->get('id'), '_llms_sequential_id', true ) );\n\n\t\t// Save the awarded certificate again, make sure the seq id is not incremented.\n\t\twp_update_post(\n\t\t\tarray(\n\t\t\t\t'ID'          => $cert->id,\n\t\t\t\t'post_title' => 'Title changes',\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals( 25, $cert->get( 'sequential_id' ) );\n\t\t$this->assertEquals( 25, get_post_meta( $cert->get('id'), '_llms_sequential_id', true ) );\n\n\t\t// Test seq id incremented on creation if post status is publish.\n\t\t$template_id = $this->create_certificate_template();\n\t\tupdate_post_meta( $template_id, '_llms_sequential_id', 25 );\n\n\t\t$cert = new $this->class_name( 'new', array( 'post_parent' => $template_id, 'post_status' => 'publish' ) );\n\t\t$this->assertEquals( 25, $cert->get( 'sequential_id' ) );\n\t\t$this->assertEquals( 25, get_post_meta( $cert->get('id'), '_llms_sequential_id', true ) );\n\n\t\t// No parent id, nothing to increment.\n\t\t$cert = new $this->class_name( 'new', array(  'post_status' => 'publish' ) );\n\t\t$this->assertEquals( 1, $cert->get( 'sequential_id' ) );\n\t\t$this->assertEquals( '', get_post_meta( $cert->get('id'), '_llms_sequential_id', true ) );\n\n\t}\n\n\t/**\n\t * Test update_sequential_id() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_sequential_id() {\n\n\t\t$this->create();\n\t\t// No parent.\n\t\t$this->assertFalse( $this->obj->update_sequential_id() );\n\n\t\t// Set a parent.\n\t\t$template_id = $this->create_certificate_template();\n\t\tupdate_post_meta( $template_id, '_llms_sequential_id', 15 );\n\n\t\t$this->obj->set( 'parent', $template_id );\n\t\t$this->assertEquals( 15, $this->obj->update_sequential_id() );\n\n\t\t// Incremented the templates's ID.\n\t\t$this->assertEquals( 16, llms_get_certificate_sequential_id( $template_id, false ) );\n\n\t}\n\n\t/**\n\t * Test update_sequential_id() when creating several awards from a single template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_update_sequential_id_multi() {\n\n\t\t$template_id = $this->create_certificate_template();\n\n\t\t$id = 1;\n\t\twhile ( $id <= 5 ) {\n\n\t\t\t$this->create();\n\t\t\t$this->obj->set( 'parent', $template_id );\n\t\t\t$this->assertEquals( $id, $this->obj->update_sequential_id(), $id );\n\n\t\t\t$id++;\n\t\t}\n\n\t}\n\n\t/**\n\t * Test creation of the model\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_create_model() {\n\n\t\t$this->create( 'test title' );\n\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t$test = new LLMS_User_Certificate( $id );\n\n\t\t$this->assertEquals( $id, $test->get( 'id' ) );\n\t\t$this->assertEquals( $this->post_type, $test->get( 'type' ) );\n\t\t$this->assertEquals( 'test title', $test->get( 'title' ) );\n\n\t}\n\n\t/**\n\t * Test get_custom_fonts() with empty post content.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_custom_fonts() {\n\n\t\t// No content.\n\t\t$this->create();\n\t\t$this->assertEquals( array(), $this->obj->get_custom_fonts() );\n\n\t\t// Not a block.\n\t\t$this->create( array( 'post_content' => 'Not a block.' ) );\n\t\t$this->assertEquals( array(), $this->obj->get_custom_fonts() );\n\n\t\t// Block with no fonts.\n\t\t$this->create( array( 'post_content' => '<!-- wp:paragraph --><p>Fake paragraph content</p><!-- /wp:paragraph -->' ) );\n\t\t$this->assertEquals( array(), $this->obj->get_custom_fonts() );\n\n\t\t$blocks = parse_blocks( '<!-- wp:paragraph --><p>Fake paragraph content</p><!-- /wp:paragraph -->\\n<!-- wp:paragraph --><p>Fake paragraph content</p><!-- /wp:paragraph -->' );\n\n\t\t// Invalid font.\n\t\t$blocks[0]['attrs']['fontFamily'] = 'invalid';\n\t\t$this->create( array( 'post_content' => serialize_blocks( $blocks ) ) );\n\t\t$this->assertEquals( array(), $this->obj->get_custom_fonts() );\n\n\t\t// Valid fonts.\n\t\t$blocks[0]['attrs']['fontFamily'] = 'sans';\n\t\t$blocks[2]['attrs']['fontFamily'] = 'serif';\n\t\t$this->create( array( 'post_content' => serialize_blocks( $blocks ) ) );\n\t\t$this->assertEquals( array( 'sans', 'serif' ), wp_list_pluck( $this->obj->get_custom_fonts(), 'id' ) );\n\n\t\t// Dupcheck.\n\t\t$blocks[0]['attrs']['fontFamily'] = 'serif';\n\t\t$this->create( array( 'post_content' => serialize_blocks( $blocks ) ) );\n\t\t$this->assertEquals( array( 'serif' ), wp_list_pluck( $this->obj->get_custom_fonts(), 'id' ) );\n\n\t\t// Nested.\n\t\t$this->create( array( 'post_content' => '<!-- wp:group -->' . serialize_blocks( $blocks ) . '<!-- /wp:group -->' ) );\n\t\t$this->assertEquals( array( 'serif' ), wp_list_pluck( $this->obj->get_custom_fonts(), 'id' ) );\n\n\t}\n\n\t/**\n\t * Test delete() method\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete() {\n\n\t\tglobal $wpdb;\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $this->factory->post->create() );\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\t$actions = array(\n\t\t\t'before' => did_action( 'llms_before_delete_certificate' ),\n\t\t\t'after'  => did_action( 'llms_delete_certificate' ),\n\t\t);\n\n\t\t$cert->delete();\n\n\t\t// User meta is gone.\n\t\t$res = $wpdb->get_results( \"SELECT * FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = {$uid} AND meta_key = '_certificate_earned' AND meta_value = {$cert_id}\" );\n\t\t$this->assertEquals( array(), $res );\n\n\t\t// Post is deleted.\n\t\t$this->assertNull( get_post( $cert_id ) );\n\n\t\t// Ran actions.\n\t\t$this->assertEquals( ++$actions['before'], did_action( 'llms_before_delete_certificate' ) );\n\t\t$this->assertEquals( ++$actions['after'], did_action( 'llms_delete_certificate' ) );\n\n\t}\n\n\t/**\n\t * Test get_earned_date()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_earned_date() {\n\n\t\t$this->create();\n\n\t\t$date = $this->obj->post->post_date;\n\n\t\t// Request a format.\n\t\t$this->assertEquals( $date, $this->obj->get_earned_date( 'Y-m-d H:i:s' ) );\n\n\t\t// Default blog format.\n\t\t$this->assertEquals( date( 'F j, Y', strtotime( $date ) ), $this->obj->get_earned_date() );\n\n\t}\n\n\t/**\n\t * Test get_background()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_background() {\n\n\t\t$this->create();\n\t\t$this->assertEquals( '#ffffff', $this->obj->get_background() );\n\n\t\t$this->obj->set( 'background', '#eaeaea' );\n\t\t$this->assertEquals( '#eaeaea', $this->obj->get_background() );\n\n\t}\n\n\t/**\n\t * Test get_background_image()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_background_image() {\n\n\t\t// Default.\n\t\t$cert = llms_get_certificate( $this->factory->post->create( array( 'post_type' => $this->post_type ) ) );\n\n\t\t$img = $cert->get_background_image();\n\t\t$this->assertTrue( $img['is_default'] );\n\t\t$this->assertEquals( 800, $img['width'] );\n\t\t$this->assertEquals( 616, $img['height'] );\n\t\t$this->assertStringContainsString( 'default-certificate.png', $img['src'] );\n\n\t\t// Has image.\n\t\t$attachment = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\tset_post_thumbnail( $cert->get( 'id' ), $attachment );\n\n\t\t$img = $cert->get_background_image();\n\t\t$this->assertFalse( $img['is_default'] );\n\t\t$this->assertEquals( 640, $img['width'] );\n\t\t$this->assertEquals( 854, $img['height'] );\n\t\t$this->assertMatchesRegularExpression(\n\t\t\t'#http:\\/\\/example.org\\/wp-content\\/uploads\\/\\d{4}\\/\\d{2}\\/yura-timoshenko-R7ftweJR8ks-unsplash(?:(-\\d+)*(-\\d+x\\d+)*).jpeg#',\n\t\t\t$img['src']\n\t\t);\n\n\t}\n\n\t/**\n\t * Test get_dimension(), get_height(), get_width(), and get_unit()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_dimensions() {\n\n\t\t$this->create();\n\n\t\t// Letter.\n\t\t$this->obj->set( 'size', 'LETTER' );\n\n\t\t$this->assertEquals( 'in', $this->obj->get_unit() );\n\n\t\t$this->assertEquals( 8.5, $this->obj->get_width() );\n\t\t$this->assertEquals( '8.5in', $this->obj->get_width( true ) );\n\n\t\t$this->assertEquals( 11, $this->obj->get_height() );\n\t\t$this->assertEquals( '11in', $this->obj->get_height( true ) );\n\n\t\t// A4.\n\t\t$this->obj->set( 'size', 'A4' );\n\n\t\t$this->assertEquals( 'mm', $this->obj->get_unit() );\n\n\t\t$this->assertEquals( 210, $this->obj->get_width() );\n\t\t$this->assertEquals( '210mm', $this->obj->get_width( true ) );\n\n\t\t$this->assertEquals( 297, $this->obj->get_height() );\n\t\t$this->assertEquals( '297mm', $this->obj->get_height( true ) );\n\n\t\t// Custom.\n\t\t$this->obj->set( 'size', 'CUSTOM' );\n\t\t$this->obj->set( 'unit', 'in' );\n\t\t$this->obj->set( 'width', 20 );\n\t\t$this->obj->set( 'height', 25 );\n\n\t\t$this->assertEquals( 'in', $this->obj->get_unit() );\n\n\t\t$this->assertEquals( 20, $this->obj->get_width() );\n\t\t$this->assertEquals( '20in', $this->obj->get_width( true ) );\n\n\t\t$this->assertEquals( 25, $this->obj->get_height() );\n\t\t$this->assertEquals( '25in', $this->obj->get_height( true ) );\n\n\t}\n\n\t/**\n\t * Test get_dimensions_for_display()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_dimensions_for_display() {\n\n\t\t$this->create();\n\n\t\t$dimensions = $this->obj->get_dimensions_for_display();\n\t\t$this->assertEquals( '8.5in', $dimensions['height'] );\n\t\t$this->assertEquals( '11in', $dimensions['width'] );\n\n\t\t// Flip orientation.\n\t\t$this->obj->set( 'orientation', 'portrait' );\n\t\t$dimensions = $this->obj->get_dimensions_for_display();\n\t\t$this->assertEquals( '11in', $dimensions['height'] );\n\t\t$this->assertEquals( '8.5in', $dimensions['width'] );\n\n\t}\n\n\t/**\n\t * Test get_margins()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_margins() {\n\n\t\t$this->create();\n\n\t\t$this->assertEquals( array( 5, 5, 5, 5 ), $this->obj->get_margins() );\n\t\t$this->assertEquals( array( '5%', '5%', '5%', '5%' ), $this->obj->get_margins( true ) );\n\n\t}\n\n\t/**\n\t * Test get_orientation()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_orientation() {\n\n\t\t$this->create();\n\t\t$this->assertEquals( 'landscape', $this->obj->get_orientation() );\n\n\t\t$this->obj->set( 'orientation', 'portrait' );\n\t\t$this->assertEquals( 'portrait', $this->obj->get_orientation() );\n\n\t}\n\n\t/**\n\t * Test get_related_post_id()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_related_post_id() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $related );\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\t$this->assertEquals( $related, $cert->get_related_post_id() );\n\n\t}\n\n\t/**\n\t * Test get_sequential_id()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_sequential_id() {\n\n\t\t$this->create();\n\n\t\t$ids = array(\n\t\t\t1       => '000001',\n\t\t\t25      => '000025',\n\t\t\t302     => '000302',\n\t\t\t4999    => '004999',\n\t\t\t12032   => '012032',\n\t\t\t932012  => '932012',\n\t\t\t// Longer than the default max length of 6.\n\t\t\t1329101 => '1329101',\n\t\t);\n\n\t\tforeach( $ids as $raw => $formatted ) {\n\n\t\t\t$this->obj->set( 'sequential_id', $raw );\n\t\t\t$this->assertEquals( $formatted, $this->obj->get_sequential_id() );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_size()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_size() {\n\n\t\t// Default default value.\n\t\t$this->create();\n\t\t$this->assertEquals( 'LETTER', $this->obj->get_size() );\n\n\t\t// Updated default value.\n\t\tupdate_option( 'lifterlms_certificate_default_size', 'A4' );\n\t\t$this->create();\n\t\t$this->assertEquals( 'A4', $this->obj->get_size() );\n\n\t\t// Update default value again.\n\t\tupdate_option( 'lifterlms_certificate_default_size', 'USER_DEFINED' );\n\t\t$this->create();\n\t\t$this->assertEquals( 'USER_DEFINED', $this->obj->get_size() );\n\n\t\t// Explicitly set value on the cert.\n\t\t$this->obj->set( 'size', 'A3' );\n\t\t$this->assertEquals( 'A3', $this->obj->get_size() );\n\n\t\tdelete_option( 'lifterlms_certificate_default_size' );\n\n\t}\n\n\t/**\n\t * Test get_size|unit|widh|height() when using custom default values.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_sizes_user_defined() {\n\n\t\t// Default default value.\n\t\t$this->create();\n\t\t$this->assertEquals( 'LETTER', $this->obj->get_size() );\n\n\t\t// Updated default value.\n\t\tupdate_option( 'lifterlms_certificate_default_size', 'USER_DEFINED' );\n\t\t$this->create();\n\t\t$this->assertEquals( 'USER_DEFINED', $this->obj->get_size() );\n\n\t\t$this->assertEquals( 'mm', $this->obj->get_unit() );\n\n\t\t$this->assertEquals( 400, $this->obj->get_width() );\n\t\t$this->assertEquals( '400mm', $this->obj->get_width( true ) );\n\n\t\t$this->assertEquals( 400, $this->obj->get_height() );\n\t\t$this->assertEquals( '400mm', $this->obj->get_height( true ) );\n\n\t\t// Updated default values.\n\t\tupdate_option( 'lifterlms_certificate_default_user_defined_unit', 'in' );\n\t\tupdate_option( 'lifterlms_certificate_default_user_defined_width', 200 );\n\t\tupdate_option( 'lifterlms_certificate_default_user_defined_height', 150 );\n\n\t\t$this->assertEquals( 'in', $this->obj->get_unit() );\n\n\t\t$this->assertEquals( 200, $this->obj->get_width() );\n\t\t$this->assertEquals( '200in', $this->obj->get_width( true ) );\n\n\t\t$this->assertEquals( 150, $this->obj->get_height() );\n\t\t$this->assertEquals( '150in', $this->obj->get_height( true ) );\n\n\t\t// Reset custom size option\n\t\tdelete_option( 'lifterlms_certificate_default_size' );\n\t\t$this->create();\n\t\t$this->assertEquals( 'LETTER', $this->obj->get_size() );\n\n\t\t$this->assertEquals( 8.5, $this->obj->get_width() );\n\t\t$this->assertEquals( '8.5in', $this->obj->get_width( true ) );\n\n\t\t$this->assertEquals( 11, $this->obj->get_height() );\n\t\t$this->assertEquals( '11in', $this->obj->get_height( true ) );\n\n\t\t// Reset other options.\n\t\tdelete_option( 'lifterlms_certificate_default_user_defined_unit' );\n\t\tdelete_option( 'lifterlms_certificate_default_user_defined_width' );\n\t\tdelete_option( 'lifterlms_certificate_default_user_defined_height' );\n\n\t}\n\n\t/**\n\t * Test get_template_version()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_template_version() {\n\n\t\t$this->create();\n\n\t\t// No content.\n\t\t$this->assertEquals( 2, $this->obj->get_template_version() );\n\n\t\t// Some blocks.\n\t\t$blocks = serialize_blocks( array(\n\t\t\tarray(\n\t\t\t\t'blockName'    => 'core/paragraph',\n\t\t\t\t'innerContent' => array( 'Lorem ipsum dolor sit.' ),\n\t\t\t\t'attrs'        => array(),\n\t\t\t),\n\t\t) );\n\t\t$this->obj->set( 'content', $blocks );\n\t\t$this->assertEquals( 2, $this->obj->get_template_version() );\n\n\t\t// Content & no blocks.\n\t\t$this->obj->set( 'content', 'No blocks' );\n\t\t$this->assertEquals( 1, $this->obj->get_template_version() );\n\n\n\t}\n\n\t/**\n\t * Test get_user_id()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_user_id() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $related );\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\t$this->assertEquals( $uid, $cert->get_user_id() );\n\n\t}\n\n\t/**\n\t * Test get_user_postmeta()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_user_postmeta() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $related );\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\t$expect = new stdClass();\n\t\t$expect->user_id = $uid;\n\t\t$expect->post_id = $related;\n\t\t$this->assertEquals( $expect, $cert->get_user_postmeta() );\n\n\t}\n\n\t/**\n\t * Test is_awarded().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_awarded() {\n\n\t\t$this->create();\n\n\t\t$this->obj->set( 'status', 'publish' );\n\t\t$this->obj->set( 'awarded', '' );\n\n\t\t$this->assertFalse( $this->obj->is_awarded() );\n\n\t\t$this->obj->set( 'awarded', llms_current_time( 'mysql' ) );\n\t\t$this->assertTrue( $this->obj->is_awarded() );\n\n\t\t$this->obj->set( 'status', 'draft' );\n\t\t$this->assertFalse( $this->obj->is_awarded() );\n\n\t}\n\n\t/**\n\t * Test merge_content()\n\t *\n\t * @since 6.0.0\n\t * @since 6.1.0 Added testing of the `{earned_date}` merge code.\n\t * @since 6.4.0 Added testing of a reusable block with a merge code and a shortcode.\n\t *\n\t * @return void\n\t */\n\tpublic function test_merge_content_and_sync() {\n\n\t\tLLMS_Install::create_pages();\n\n\t\t$user_info = array(\n\t\t\t'first_name' => 'Walter',\n\t\t\t'last_name'  => 'Sobchak',\n\t\t\t'user_email' => 'mergecontentcertuser@mail.tld',\n\t\t\t'user_login' => 'mergecontentcertuser'\n\t\t);\n\n\t\t$user    = $this->factory->student->create_and_get( $user_info );\n\t\t$related = $this->factory->post->create();\n\n\t\t$content          = '';\n\t\t$expected_content = '';\n\t\t$date_format      = get_option( 'date_format' );\n\t\t$earned_date      = null;\n\n\t\t$merge_codes = llms_get_certificate_merge_codes();\n\t\t// Add user info shortcodes.\n\t\t$merge_codes['[llms-user display_name]'] = 'Display Name';\n\n\t\tforeach ( $merge_codes as $code => $desc ) {\n\n\t\t\t// Build the actual content for the template.\n\t\t\t$content .= \"{$desc}: {$code}\\n\\n\";\n\n\t\t\t// Build the expected content of the earned cert after merging.\n\t\t\t$expected = '';\n\t\t\tswitch ( $code ) {\n\n\t\t\t\tcase '{site_title}':\n\t\t\t\t\t$expected = 'Test Blog';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{site_url}':\n\t\t\t\t\t$expected = get_permalink( llms_get_page_id( 'myaccount' ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{current_date}':\n\t\t\t\tcase '{earned_date}':\n\t\t\t\t\t$expected    = wp_date( $date_format, llms_current_time( 'timestamp' ) );\n\t\t\t\t\t$earned_date = $expected;\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{email_address}':\n\t\t\t\t\t$expected = $user_info['user_email'];\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{first_name}':\n\t\t\t\t\t$expected = $user_info['first_name'];\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{last_name}':\n\t\t\t\t\t$expected = $user_info['last_name'];\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{student_id}':\n\t\t\t\t\t$expected = $user->get( 'id' );\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{user_login}':\n\t\t\t\t\t$expected = $user_info['user_login'];\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{certificate_id}':\n\t\t\t\t\t$expected = '[[CERTID]]';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '{sequential_id}':\n\t\t\t\t\t$expected = '000001';\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase '[llms-user display_name]':\n\t\t\t\t\t$expected = \"{$user_info['first_name']} {$user_info['last_name']}\";\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t$expected_content .= \"{$desc}: {$expected}\\n\\n\";\n\t\t}\n\n\t\t// Add a reusable block.\n\t\t$reusable_id      = $this->factory->post->create( array(\n\t\t\t'post_type'    => 'wp_block',\n\t\t\t'post_content' => \"<!-- wp:paragraph --><p>Merge Email: {email_address}</p><!-- /wp:paragraph -->\\n\" .\n\t\t\t                  \"<!-- wp:paragraph --><p>Shortcode Email: [llms-user user_email]</p><!-- /wp:paragraph -->\",\n\t\t) );\n\t\t$content          .= \"<!-- wp:block {\\\"ref\\\":{$reusable_id}} /-->\\n\\n\";\n\t\t$expected_content .= \"<!-- wp:paragraph --><p>Merge Email: {$user_info['user_email']}</p><!-- /wp:paragraph -->\\n\" .\n\t\t                     \"<!-- wp:paragraph --><p>Shortcode Email: {$user_info['user_email']}</p><!-- /wp:paragraph -->\\n\\n\";\n\n\t\t// Create a certificate template and award it to the student.\n\t\tllms_tests_mock_current_time( 'now' );\n\t\t$template = $this->create_certificate_template( 'Title', $content, 456 );\n\t\t/** @var LLMS_User_Certificate $cert */\n\t\t$cert = LLMS_Unit_Test_Util::call_method(\n\t\t\t'LLMS_Engagement_Handler',\n\t\t\t'create',\n\t\t\tarray( 'certificate', $user->get( 'id' ), $template, $related )\n\t\t);\n\n\t\t// Add the cert id (not available until the earned post exists).\n\t\t$expected_content = str_replace( '[[CERTID]]', $cert->get( 'id' ), $expected_content );\n\n\t\t$this->assertEquals( $expected_content, $cert->get( 'content', true ) );\n\n\t\t// Time travel to the future.\n\t\tllms_tests_mock_current_time( 'now +1 day' );\n\t\t$updated_date = wp_date( $date_format, llms_current_time( 'timestamp' ) );\n\n\t\t// Update the template and sync.\n\t\t$thumbnail_id = $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\twp_update_post( array(\n\t\t\t'ID'           => $template,\n\t\t\t'post_content' => 'Updated on {current_date}. Earned on {earned_date}. Login = {user_login}.',\n\t\t\t'post_title'   => 'Template Title',\n\t\t\t'meta_input'   => array(\n\t\t\t\t'_thumbnail_id' => $thumbnail_id,\n\t\t\t)\n\t\t) );\n\n\t\t$this->assertTrue( $cert->sync() );\n\t\t$expected = \"Updated on {$updated_date}. Earned on {$earned_date}. Login = {$user_info['user_login']}.\";\n\t\t$this->assertEquals( $expected, $cert->get( 'content', true ) );\n\t\t$this->assertEquals( 'Title', $cert->get( 'title', true ) );\n\t\t$this->assertEquals( $thumbnail_id, get_post_thumbnail_id( $cert->get( 'id' ) ) );\n\t}\n\n\t/**\n\t * Test get_merge_data() to ensure deprecated hooks run when they're attached.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @expectedDeprecated llms_certificate_merge_codes\n\t * @expectedDeprecated LLMS_Certificate_User::init\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_merge_data_deprecated_hook() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $this->factory->post->create() );\n\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\t$handler = function( $codes, $old_cert ) {\n\t\t\t$this->assertInstanceOf( 'LLMS_Certificate_User', $old_cert );\n\t\t\treturn $codes;\n\t\t};\n\n\t\tadd_filter( 'llms_certificate_merge_codes', $handler, 10, 2 );\n\n\t\tLLMS_Unit_Test_Util::call_method( $cert, 'get_merge_data' );\n\n\t\tremove_filter( 'llms_certificate_merge_codes', $handler, 10 );\n\n\t}\n\n\t/**\n\t * Test can_user_manage()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_user_manage() {\n\n\t\t$admin    = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\t$other    = $this->factory->student->create();\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $related );\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\t// Other student cannot manage.\n\t\t$this->assertFalse( $cert->can_user_manage() );\n\t\t$this->assertFalse( $cert->can_user_manage( $other ) );\n\n\t\t// Fake user cannot manage.\n\t\t$this->assertFalse( $cert->can_user_manage( $uid + 1 ) );\n\n\t\t// Admin can.\n\t\t$this->assertTrue( $cert->can_user_manage( $admin ) );\n\n\t\t// Owner can.\n\t\t$this->assertTrue( $cert->can_user_manage( $uid ) );\n\n\t\t// Current user cannot manage.\n\t\t$this->assertFalse( $cert->can_user_manage() );\n\n\t\t// Current User Can.\n\t\twp_set_current_user( $admin );\n\t\t$this->assertTrue( $cert->can_user_manage() );\n\n\t\t// Current user is owner.\n\t\twp_set_current_user( $uid );\n\t\t$this->assertTrue( $cert->can_user_manage() );\n\n\t}\n\n\t/**\n\t * Test can_user_view()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_can_user_view() {\n\n\t\t$uid      = $this->factory->student->create();\n\t\t$related  = $this->factory->post->create();\n\t\t$earned   = $this->earn_certificate( $uid, $this->create_certificate_template(), $related );\n\t\t$cert_id  = $earned[1];\n\t\t$cert = new LLMS_User_Certificate( $cert_id );\n\n\t\t// Any user that can manage can always view the cert.\n\t\tadd_filter( 'llms_certificate_can_user_manage', '__return_true' );\n\t\t$this->assertTrue( $cert->can_user_view() );\n\t\tremove_filter( 'llms_certificate_can_user_manage', '__return_true' );\n\n\t\tadd_filter( 'llms_certificate_can_user_manage', '__return_false' );\n\n\t\t// User cannot manage so they cannot view.\n\t\t$this->assertFalse( $cert->can_user_view() );\n\n\t\t// Unless sharing is enabled.\n\t\t$cert->set( 'allow_sharing', 'yes' );\n\t\t$this->assertTrue( $cert->can_user_view() );\n\n\t\t// Explicitly disabled.\n\t\t$cert->set( 'allow_sharing', 'no' );\n\t\t$this->assertFalse( $cert->can_user_view() );\n\n\t\tremove_filter( 'llms_certificate_can_user_manage', '__return_false' );\n\n\n\t}\n\n\t/**\n\t * Test is_sharing_enabled()\n\t *\n\t * @since 4.5.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_sharing_enabled() {\n\n\t\t$cert = new LLMS_User_Certificate( 'new', 'test' );\n\n\t\t// No set.\n\t\t$this->assertFalse( $cert->is_sharing_enabled() );\n\n\t\t// Explicitly disabled.\n\t\t$cert->set( 'allow_sharing', 'no' );\n\t\t$this->assertFalse( $cert->is_sharing_enabled() );\n\n\t\t// Enabled.\n\t\t$cert->set( 'allow_sharing', 'yes' );\n\t\t$this->assertTrue( $cert->is_sharing_enabled() );\n\n\t}\n\n\t/**\n\t * Test sync() when an error is encountered.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_errors() {\n\n\t\t$this->create();\n\t\t$this->obj->set( 'parent', $this->factory->post->create() + 1 );\n\n\t\t// This is just testing that an error is returned, the rest of the conditions are tested against LLMS_Engagement_Handler::check_post() directly.\n\t\t$this->assertFalse( $this->obj->sync() );\n\n\t}\n\n\t/**\n\t * Test sync() with a v1 template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_template_v1() {\n\n\t\t$img_id      = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$title       = 'Sync Template V1';\n\t\t$template_id = $this->create_certificate_template( $title, 'ID:{certificate_id}', $img_id );\n\t\t$template    = llms_get_certificate( $template_id, true );\n\t\t$template->set( 'background', '#000000' );\n\n\t\t$this->create();\n\t\t$this->obj->set( 'parent', $template_id );\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t$this->assertTrue( $this->obj->sync() );\n\n\t\t// Title and content updated.\n\t\t$this->assertEquals( $title, $this->obj->get( 'title' ) );\n\t\t$this->assertEquals( \"ID:{$id}\", $this->obj->get( 'content', true ) );\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\n\t\t// Layout meta isn't synced so it should return the default.\n\t\t$this->assertEquals( '#ffffff', $this->obj->get( 'background' ) );\n\n\t}\n\n\t/**\n\t * Test sync() with a v2 template.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_template_v2() {\n\n\t\t$img_id      = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$title       = 'Sync Template V2';\n\t\t$content     = serialize_blocks( array(\n\t\t\tarray(\n\t\t\t\t'blockName'    => 'core/paragraph',\n\t\t\t\t'innerContent' => array( 'ID:{certificate_id}' ),\n\t\t\t\t'attrs'        => array(),\n\t\t\t),\n\t\t) );\n\t\t$template_id = $this->create_certificate_template( $title, $content, $img_id );\n\t\t$template    = llms_get_certificate( $template_id, true );\n\n\t\t$layout_meta = array(\n\t\t\t'background'  => '#323323',\n\t\t\t'height'      => 25,\n\t\t\t'margins'     => array( 10, 5, 2.5, 1.25 ),\n\t\t\t'orientation' => 'portrait',\n\t\t\t'size'        => 'A3',\n\t\t\t'unit'        => 'mm',\n\t\t\t'width'       => 291,\n\t\t);\n\t\t$template->set_bulk( $layout_meta );\n\n\t\t$this->create();\n\t\t$this->obj->set( 'parent', $template_id );\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t$this->assertTrue( $this->obj->sync() );\n\n\t\t// Title and content updated.\n\t\t$this->assertEquals( $title, $this->obj->get( 'title' ) );\n\t\t$this->assertEquals( \"<!-- wp:paragraph -->ID:{$id}<!-- /wp:paragraph -->\", $this->obj->get( 'content', true ) );\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\n\t\t// Layout meta isn't synced so it should return the default.\n\t\tforeach ( $layout_meta as $prop => $val ) {\n\t\t\t$this->assertEquals( $val, $this->obj->get( $prop ), $prop );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test syncing an awarded engagement with its template after removing a thumbnail.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_template_after_removing_thumbnail() {\n\n\t\t// Create a template with a thumbnail.\n\t\t$img_id      = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$title       = 'Sync Template Removing Thumbnail';\n\t\t$template_id = $this->create_certificate_template( $title, 'ID:{certificate_id}', $img_id );\n\n\t\t// Create an awarded engagement.\n\t\t$this->create( array( 'post_parent' => $template_id ) );\n\n\t\t// Test that the awarded engagement matches the template.\n\t\t$id = $this->obj->get( 'id' );\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\n\t\t// Remove the template thumbnail.\n\t\tdelete_post_thumbnail( $template_id );\n\n\t\t// Sync the awarded engagement with the template.\n\t\t$this->assertTrue( $this->obj->sync() );\n\n\t\t// Test that the awarded engagement no longer has a thumbnail.\n\t\t$this->assertFalse( (bool) get_post_thumbnail_id( $id ) );\n\n\t\t// Test that the background image has returned to the default.\n\t\t$img = $this->obj->get_background_image();\n\t\t$this->assertTrue( $img['is_default'] );\n\t}\n\n\t/**\n\t * Test that syncing an awarded engagement with its template twice keeps the same thumbnail.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_sync_template_twice_keep_thumbnail() {\n\n\t\t// Create a template with a thumbnail.\n\t\t$img_id      = $this->create_attachment( 'yura-timoshenko-R7ftweJR8ks-unsplash.jpeg' );\n\t\t$title       = 'Sync Template Twice with Thumbnail';\n\t\t$template_id = $this->create_certificate_template( $title, 'ID:{certificate_id}', $img_id );\n\n\t\t// Create an awarded engagement.\n\t\t$this->create( array( 'post_parent' => $template_id ) );\n\t\t$id = $this->obj->get( 'id' );\n\n\t\t// Test that the awarded engagement matches the template.\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\n\t\t// Sync (twice).\n\t\t$this->assertTrue( $this->obj->sync() );\n\t\t$this->assertTrue( $this->obj->sync() );\n\t\t$this->assertEquals( $img_id, get_post_thumbnail_id( $id ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/notifications/class-llms-test-notification-achievement-earned.php",
    "content": "<?php\n/**\n * LLMS_Notification Achievement Earned\n *\n * @package LifterLMS/Tests/Notifications\n *\n * @group notifications\n *\n * @since 3.8.0\n */\n\nclass LLMS_Test_Notification_Achievement_Earned extends LLMS_NotificationTestCase {\n\n\t/**\n\t * The ID of the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $notification_id = 'achievement_earned';\n\n\t/**\n\t * The name of the controller class for the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $controller_class = 'LLMS_Notification_Controller_Achievement_Earned';\n\n\t/**\n\t * The name of the view class for the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $view_class = 'LLMS_Notification_View_Achievement_Earned';\n\n\t/**\n\t * Function used to setup arguments passed to a notification controller's `action_callback()` function.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int[]\n\t */\n\tprotected function setup_args() {\n\n\t\t$user_id     = $this->factory->student->create();\n\t\t$engagement  = $this->create_mock_engagement( 'course_completed', 'achievement' );\n\t\t$related_id  = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\t\t$template_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\n\t\t$attachment_id = $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\tset_post_thumbnail( $template_id, $attachment_id );\n\n\t\tllms_enroll_student( $user_id, $related_id );\n\n\t\t$earned = LLMS_Engagement_Handler::handle_achievement( array( $user_id, $template_id, $related_id, $engagement->ID ) );\n\n\t\treturn array(\n\t\t\t$user_id,\n\t\t\t$earned->get( 'id' ),\n\t\t\t$related_id,\n\t\t);\n\n\t}\n\n\t/**\n\t * Test set_merge_data()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_merge_data() {\n\n\t\t$view = $this->get_view();\n\n\t\t$user       = llms_get_student( $this->last_setup_args[0] );\n\t\t$achievement = new LLMS_User_Achievement( $this->last_setup_args[1] );\n\n\t\t// $img_url = get_the_post_thumbnail_url( $this->last_setup_args[1] );\n\n\t\t$tests = array(\n\t\t\t'{{ACHIEVEMENT_CONTENT}}'   => $achievement->get( 'content' ),\n\t\t\t// '{{ACHIEVEMENT_IMAGE}}'     => '',\n\t\t\t// '{{ACHIEVEMENT_IMAGE_URL}}' => '',\n\t\t\t'{{ACHIEVEMENT_TITLE}}'     => $achievement->get( 'title' ),\n\t\t\t'{{STUDENT_NAME}}'          => 'you',\n\t\t);\n\n\t\tforeach( $tests as $code => $expected ) {\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $view, 'set_merge_data', array( $code ) ) );\n\t\t}\n\n\t\t$this->markTestIncomplete( 'Need to add tests for achievement image' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php",
    "content": "<?php\n/**\n * LLMS_Notification Certificate Earned\n *\n * @package LifterLMS/Tests/Notifications\n *\n * @group notifications\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Notification_Certificate_Earned extends LLMS_NotificationTestCase {\n\n\t/**\n\t * The ID of the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $notification_id = 'certificate_earned';\n\n\t/**\n\t * The name of the controller class for the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $controller_class = 'LLMS_Notification_Controller_Certificate_Earned';\n\n\t/**\n\t * The name of the view class for the tested notification.\n\t *\n\t * @var string\n\t */\n\tprotected $view_class = 'LLMS_Notification_View_Certificate_Earned';\n\n\t/**\n\t * Function used to setup arguments passed to a notification controller's `action_callback()` function.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return int[]\n\t */\n\tprotected function setup_args() {\n\n\t\t$user_id     = $this->factory->student->create();\n\t\t$engagement  = $this->create_mock_engagement( 'course_completed', 'certificate' );\n\t\t$related_id  = get_post_meta( $engagement->ID, '_llms_engagement_trigger_post', true );\n\t\t$template_id = get_post_meta( $engagement->ID, '_llms_engagement', true );\n\n\t\t$attachment_id = $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\tset_post_thumbnail( $template_id, $attachment_id );\n\n\t\tllms_enroll_student( $user_id, $related_id );\n\n\t\t$earned = LLMS_Engagement_Handler::handle_certificate( array( $user_id, $template_id, $related_id, $engagement->ID ) );\n\n\t\treturn array(\n\t\t\t$user_id,\n\t\t\t$earned->get( 'id' ),\n\t\t\t$related_id,\n\t\t);\n\n\t}\n\n\t/**\n\t * Test set_merge_data()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_merge_data() {\n\n\t\t$view = $this->get_view();\n\n\t\t$user = llms_get_student( $this->last_setup_args[0] );\n\t\t$cert = new LLMS_User_Certificate( $this->last_setup_args[1] );\n\n\t\t$expected_content = apply_filters( 'the_content', 'Test Blog, ' . date( get_option( 'date_format' ) ) );\n\n\t\t$tests = array(\n\t\t\t'{{CERTIFICATE_CONTENT}}' => $expected_content,\n\t\t\t'{{CERTIFICATE_TITLE}}'   => $cert->get( 'title' ),\n\t\t\t'{{CERTIFICATE_URL}}'     => get_permalink( $cert->get( 'id' ) ),\n\t\t\t'{{STUDENT_NAME}}'        => 'you',\n\t\t\t'{{FAKE_CODE}}'           => '{{FAKE_CODE}}',\n\t\t);\n\n\t\tforeach( $tests as $code => $expected ) {\n\t\t\t$this->assertEquals( $expected, LLMS_Unit_Test_Util::call_method( $view, 'set_merge_data', array( $code ) ) );\n\t\t}\n\n\t\t$mini_cert = LLMS_Unit_Test_Util::call_method( $view, 'set_merge_data', array( '{{MINI_CERTIFICATE}}' ) );\n\t\t$this->assertEquals( 0, strpos( '<div class=\"llms-mini-cert\">', $mini_cert ) );\n\t\t$this->assertStringContainsString( \"<h2 class=\\\"llms-mini-cert-title\\\">{$cert->get( 'title' )}</h2>\", $mini_cert );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/notifications/class-llms-test-notifications-query.php",
    "content": "<?php\n/**\n * Test the notifications query.\n *\n * @package LifterLMS/Tests\n *\n * @group notifications\n * @group query\n * @group dbquery\n *\n * @since 7.1.0\n */\nclass LLMS_Test_Notifications_Query extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Test that the notifications query, using default args, sets up a count_query\n\t * and does not use SQL_CALC_FOUND_ROWS.\n\t *\n\t * @since 7.1.0\n\t * @since 10.0.0 Updated: SQL_CALC_FOUND_ROWS replaced with count_query.\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_with_default_args_sets_count_query() {\n\t\t$query = new LLMS_Notifications_Query();\n\t\t$sql = LLMS_Unit_Test_Util::call_method( $query, 'prepare_query' );\n\t\t$this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $sql );\n\n\t\t$count_query = LLMS_Unit_Test_Util::get_private_property_value( $query, 'count_query' );\n\t\t$this->assertStringStartsWith( 'SELECT COUNT(*)', $count_query );\n\t}\n\n\t/**\n\t * Test that the notifications query, passing no_found_rows as true, does not set count_query.\n\t *\n\t * @since 7.1.0\n\t * @since 10.0.0 Updated: SQL_CALC_FOUND_ROWS replaced with count_query.\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_correctly_doesnt_set_count_query() {\n\t\t$query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\t\t$sql = LLMS_Unit_Test_Util::call_method( $query, 'prepare_query' );\n\t\t$this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $sql );\n\n\t\t$count_query = LLMS_Unit_Test_Util::get_private_property_value( $query, 'count_query' );\n\t\t$this->assertEmpty( $count_query );\n\t}\n\n\t/**\n\t * Test that the notifications query's default args, includes all the available status excluding 'error'.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_query_default_args_do_not_contain_error() {\n\t\t$query = new LLMS_Notifications_Query();\n\t\t$args  = LLMS_Unit_Test_Util::call_method( $query, 'get_default_args' );\n\t\t$this->assertNotContains( 'error', $args['statuses'] );\n\t\t$this->assertEquals( array( 'new', 'sent', 'read', 'unread', 'deleted', 'failed' ), $args['statuses'] );\n\t}\n\n\t/**\n\t * Test found_results and max_pages with real notifications data.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_found_results_with_pagination() {\n\n\t\t$post_id = $this->factory->post->create();\n\n\t\tfor ( $i = 0; $i < 8; $i++ ) {\n\t\t\t$n = new LLMS_Notification();\n\t\t\t$n->create(\n\t\t\t\tarray(\n\t\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t\t'subscriber' => 1,\n\t\t\t\t\t'type'       => 'basic',\n\t\t\t\t\t'trigger_id' => 1,\n\t\t\t\t\t'user_id'    => 1,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t$query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'subscriber' => 1,\n\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t'per_page'   => 3,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertSame( 8, $query->get_found_results() );\n\t\t$this->assertSame( 3, $query->get_max_pages() );\n\t\t$this->assertSame( 3, $query->get_number_results() );\n\t}\n\n\t/**\n\t * Test that no_found_rows skips counting.\n\t *\n\t * @since 10.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_no_found_rows_skips_count() {\n\n\t\t$post_id = $this->factory->post->create();\n\n\t\t$n = new LLMS_Notification();\n\t\t$n->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t'subscriber' => 1,\n\t\t\t\t'type'       => 'basic',\n\t\t\t\t'trigger_id' => 1,\n\t\t\t\t'user_id'    => 1,\n\t\t\t)\n\t\t);\n\n\t\t$query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'subscriber'    => 1,\n\t\t\t\t'post_id'       => $post_id,\n\t\t\t\t'no_found_rows' => true,\n\t\t\t)\n\t\t);\n\n\t\t$this->assertTrue( $query->has_results() );\n\t\t$this->assertSame( 0, $query->get_found_results() );\n\t\t$this->assertSame( 0, $query->get_max_pages() );\n\t}\n\n\t/**\n\t * Test getting notifications, escluding the errored ones (default).\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notifications_no_errored() {\n\n\t\t$post_id = $this->factory->post->create();\n\n\t\t// Create two notifications.\n\t\t$n1    = new LLMS_Notification();\n\t\t$nid_1 = $n1->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t'subscriber' => 1,\n\t\t\t\t'type'       => 'basic',\n\t\t\t\t'trigger_id' => 1,\n\t\t\t\t'user_id'    => 1,\n\t\t\t)\n\t\t);\n\t\t$n2    = new LLMS_Notification();\n\t\t$nid_2 = $n2->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t'subscriber' => 1,\n\t\t\t\t'type'       => 'email',\n\t\t\t\t'trigger_id' => 1,\n\t\t\t\t'user_id'    => 1,\n\t\t\t)\n\t\t);\n\t\t// Set the last notification status as 'error'.\n\t\t$n2->set( 'status', 'error' );\n\n\t\t$n_query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'subscriber' => 1,\n\t\t\t\t'post_id'    => $post_id\n\t\t\t)\n\t\t);\n\n\t\t// Expect only the not errored notification retrieved.\n\t\t$this->assertEquals( array( $nid_1 ), array_map( 'absint', wp_list_pluck( $n_query->get_notifications(), 'id' ) ) );\n\n\t}\n\n\t/**\n\t * Test getting notifications, including the errored ones.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_notifications_with_errored() {\n\n\t\t$post_id = $this->factory->post->create();\n\n\t\t// Create two notifications.\n\t\t$n1   = new LLMS_Notification();\n\t\t$nid_1 = $n1->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t'subscriber' => 2,\n\t\t\t\t'type'       => 'basic',\n\t\t\t\t'trigger_id' => 1,\n\t\t\t\t'user_id'    => 1,\n\t\t\t)\n\t\t);\n\t\t$n2    = new LLMS_Notification();\n\t\t$nid_2 = $n2->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t'subscriber' => 2,\n\t\t\t\t'type'       => 'email',\n\t\t\t\t'trigger_id' => 1,\n\t\t\t\t'user_id'    => 1,\n\t\t\t)\n\t\t);\n\t\t// Set the last notification status as 'error'.\n\t\t$n2->set( 'status', 'error' );\n\n\t\t$n_query = new LLMS_Notifications_Query(\n\t\t\tarray(\n\t\t\t\t'subscriber' => 2,\n\t\t\t\t'post_id'    => $post_id,\n\t\t\t\t'statuses'   => array( 'new', 'sent', 'read', 'unread', 'deleted', 'failed', 'error' ),\n\t\t\t)\n\t\t);\n\n\t\t// Expect both the notifications are retrieved.\n\t\t$this->assertEqualSets( array( $nid_1, $nid_2 ), array_map( 'absint', wp_list_pluck( $n_query->get_notifications(), 'id' ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/notifications/class-llms-test-notifications.php",
    "content": "<?php\n/**\n * LLMS_Notifications Tests\n *\n * @package LifterLMS/Tests/Notifications\n *\n * @group notifications\n *\n * @since 3.8.0\n * @since 3.38.0 \"DRY\"ed existing tests and added tests for processor scheduling related functions.\n */\nclass LLMS_Test_Notifications extends LLMS_UnitTestCase {\n\n\n\t/**\n\t * Setup before class.\n\t *\n\t * Forces notifications debugging on so that we can make assertions against logged data.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\t\tparent::set_up_before_class();\n\t\tllms_maybe_define_constant( 'LLMS_NOTIFICATIONS_LOGGING', true );\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 3.38.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->main = llms()->notifications();\n\n\t}\n\n\t/**\n\t * Tear down the test case\n\t *\n\t * @since 3.38.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\n\t\t// Clear data for later tests.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'processors_to_dispatch', array() );\n\n\t}\n\n\t/**\n\t * Test dispatch_processor_async() for a fake processor.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_processor_async_fake() {\n\n\t\t$res = $this->main->dispatch_processor_async( 'fake-processor' );\n\t\t$this->assertIsWPError( $res );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-processor', $res );\n\n\t}\n\n\t/**\n\t * Test dispatch_processor_async() for a fake processor.\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_processor() {\n\n\t\t$res = $this->main->dispatch_processor_async( 'email' );\n\t\t$this->assertTrue( ! is_wp_error( $res ) );\n\n\t}\n\n\t/**\n\t * Test the get_controller() method\n\t *\n\t * @since 3.8.0\n\t * @since 3.38.0 Use $this->main for code DRYness.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_controller() {\n\n\t\t// return the controller instance\n\t\t$this->assertTrue( is_a( $this->main->get_controller( 'lesson_complete' ), 'LLMS_Notification_Controller_Lesson_Complete' ) );\n\n\t\t// return false\n\t\t$this->assertFalse( $this->main->get_controller( 'thisisveryveryfake' ) );\n\n\t}\n\n\t/**\n\t * Test get_controllers() method\n\t *\n\t * @since 3.8.0\n\t * @since 3.38.0 Use $this->main for code DRYness.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_controllers() {\n\n\t\t// should always return an array\n\t\t$this->assertTrue( is_array( $this->main->get_controllers() ) );\n\n\t\t// each item in the array must extend the controller abstract\n\t\tforeach ( $this->main->get_controllers() as $controller ) {\n\t\t\t$this->assertTrue( is_subclass_of( $controller, 'LLMS_Abstract_Notification_Controller' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_processor() method\n\t *\n\t * @since 3.8.0\n\t * @since 3.38.0 Use $this->main for code DRYness.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_processor() {\n\n\t\t// return the controller instance\n\t\t$this->assertTrue( is_a( $this->main->get_processor( 'email' ), 'LLMS_Notification_Processor_Email' ) );\n\n\t\t// return false\n\t\t$this->assertFalse( $this->main->get_processor( 'thisisveryveryfake' ) );\n\n\t}\n\n\t/**\n\t * test get_processors() method\n\t *\n\t * @since 3.8.0\n\t * @since 3.38.0 Use $this->main for code DRYness.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_processors() {\n\n\t\t// should always return an array\n\t\t$this->assertTrue( is_array( $this->main->get_processors() ) );\n\n\t\t// each item in the array must extend the processor abstract\n\t\tforeach ( $this->main->get_processors() as $processor ) {\n\t\t\t$this->assertTrue( is_subclass_of( $processor, 'LLMS_Abstract_Notification_Processor' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test schedule_processing()\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_processing() {\n\n\t\t$expect = array( 'email' );\n\n\t\t// Schedule.\n\t\t$this->main->schedule_processing( 'email' );\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'processors_to_dispatch' ) );\n\n\t\t// Don't add duplicates.\n\t\t$this->main->schedule_processing( 'email' );\n\t\t$this->assertEquals( $expect, LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'processors_to_dispatch' ) );\n\n\t}\n\n\t/**\n\t * Test schedule_processors_dispatch()\n\t *\n\t * @since 3.38.0\n\t * @since 6.0.0 Unschedule processors scheduled during earlier tests.\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_processors_dispatch() {\n\n\t\tas_unschedule_action( 'llms_dispatch_notification_processor_async', 'email' );\n\n\t\t$now = time();\n\t\tllms_tests_mock_current_time( $now );\n\n\t\t$this->main->schedule_processing( 'email' );\n\t\t$this->main->schedule_processing( 'fake-processor' );\n\n\t\t$res = $this->main->schedule_processors_dispatch();\n\n\t\t$this->assertArrayHaskey( 'email', $res );\n\t\t$this->assertArrayHaskey( 'fake-processor', $res );\n\n\t\t$this->assertEquals( $now, $res['email'] );\n\n\t\t$this->assertIsWPError( $res['fake-processor'] );\n\t\t$this->assertWPErrorCodeEquals( 'invalid-processor', $res['fake-processor'] );\n\n\t}\n\n\t/**\n\t * Test schedule_processors_dispatch() when none are scheduled\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_processors_dispatch_none_scheduled() {\n\n\t\t$this->assertEquals( array(), $this->main->schedule_processors_dispatch() );\n\n\t}\n\n\t/**\n\t * Test schedule_single_processor() when an event is already scheduled\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_single_processor_already_scheduled() {\n\n\t\t$email = $this->main->get_processor( 'email' )->push_to_queue( 1 );\n\n\t\t// Schedule the event.\n\t\t$orig = LLMS_Unit_Test_Util::call_method( $this->main, 'schedule_single_processor', array( $email, 'email' ) );\n\n\t\t// Time travel.\n\t\tllms_tests_mock_current_time( time() + HOUR_IN_SECONDS );\n\n\t\t// Schedule the event again.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'schedule_single_processor', array( $email, 'email' ) );\n\n\t\t// Original timestamp should be returned.\n\t\t$this->assertEquals( $orig, $res );\n\n\t}\n\n\t/**\n\t * Test schedule_single_processor() when an existing event does not already exist.\n\t *\n\t * @since 3.38.0\n\t * @since 6.0.0 Unschedule processors scheduled during earlier tests.\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_single_processor_new() {\n\n\t\tas_unschedule_action( 'llms_dispatch_notification_processor_async', 'email' );\n\n\t\t$email = $this->main->get_processor( 'email' )->push_to_queue( 1 );\n\n\t\t$now = time();\n\t\tllms_tests_mock_current_time( $now );\n\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'schedule_single_processor', array( $email, 'email' ) );\n\t\t$this->assertEquals( $now, $res );\n\n\t}\n\n\t/**\n\t * Test email processor task's method on errored notification.\n\t *\n\t * @since 7.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_email_processor_errored_notification_task() {\n\n\t\t$email = $this->main->get_processor( 'email' );\n\t\t$user  = $this->factory->user->create();\n\n\t\t$order = $this->get_mock_order();\n\t\t$txn   = $order->record_transaction(\n\t\t\tarray(\n\t\t\t\t'amount'             => $order->get_initial_price(\n\t\t\t\t\tarray(),\n\t\t\t\t\t'float'\n\t\t\t\t),\n\t\t\t\t'source_description' => 'Mock Payment',\n\t\t\t\t'transaction_id'     => uniqid( 'mock-' ),\n\t\t\t\t'status'             => 'llms-txn-succeeded',\n\t\t\t\t'payment_gateway'    => 'manual',\n\t\t\t)\n\t\t);\n\t\t$txn2  = $order->record_transaction(\n\t\t\tarray(\n\t\t\t\t'amount'             => $order->get_initial_price(\n\t\t\t\t\tarray(),\n\t\t\t\t\t'float'\n\t\t\t\t),\n\t\t\t\t'source_description' => 'Mock Payment',\n\t\t\t\t'transaction_id'     => uniqid( 'mock-' ),\n\t\t\t\t'status'             => 'llms-txn-succeeded',\n\t\t\t\t'payment_gateway'    => 'manual',\n\t\t\t)\n\t\t);\n\n\t\t// Create a notification for a purchase receipt on txn.\n\t\t$n1    = new LLMS_Notification();\n\t\t$nid_1 = $n1->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $txn->get( 'id' ),\n\t\t\t\t'subscriber' => $user,\n\t\t\t\t'type'       => 'basic',\n\t\t\t\t'trigger_id' => 'purchase_receipt',\n\t\t\t\t'user_id'    => 1,\n\t\t\t)\n\t\t);\n\t\t$this->assertEquals( 'new', $n1->get( 'status' ) );\n\n\t\t// Create a notification for a purchase receipt on txn2.\n\t\t$n2    = new LLMS_Notification();\n\t\t$nid_2 = $n2->create(\n\t\t\tarray(\n\t\t\t\t'post_id'    => $txn2->get( 'id' ),\n\t\t\t\t'subscriber' => $user,\n\t\t\t\t'type'       => 'basic',\n\t\t\t\t'trigger_id' => 'purchase_receipt',\n\t\t\t\t'user_id'    => 1,\n\t\t\t)\n\t\t);\n\n\t\t// Process notification email for the first transaction.\n\t\t$email_processor = $this->main->get_processor( 'email' );\n\t\t$res = LLMS_Unit_Test_Util::call_method(\n\t\t\t$email_processor, 'task',\n\t\t\tarray( $nid_1 )\n\t\t);\n\t\t$this->assertEquals( false, $res );\n\t\t$this->assertEquals( 'sent', $n1->get('status') );\n\n\t\t// Delete the first transaction so that a fatal will be triggered.\n\t\twp_delete_post( $txn->get( 'id' ) );\n\t\t$res = LLMS_Unit_Test_Util::call_method(\n\t\t\t$email_processor,\n\t\t\t'task',\n\t\t\tarray( $nid_1 )\n\t\t);\n\n\t\t$this->assertEquals( false, $res );\n\t\t$this->assertEquals( 'error', $n1->get('status') );\n\n\t\t$this->assertStringContainsString(\n\t\t\t'Error caught Call to a member function get_order() on null',\n\t\t\timplode( $this->logs->get( 'notifications' ) )\n\t\t);\n\n\t\t$this->logs->clear( 'notifications' );\n\n\t\t// Process notification email for the second transaction.\n\t\t$email_processor = $this->main->get_processor( 'email' );\n\t\t$res = LLMS_Unit_Test_Util::call_method(\n\t\t\t$email_processor, 'task',\n\t\t\tarray( $nid_2 )\n\t\t);\n\t\t$this->assertEquals( false, $res );\n\t\t// Check the previous error didn't prevent this notification to be sent.\n\t\t$this->assertEquals( 'sent', $n2->get('status') );\n\n\t\t$this->assertStringNotContainsString(\n\t\t\t'Error caught Call to a member function get_order() on null',\n\t\t\timplode( $this->logs->get( 'notifications' ) )\n\t\t);\n\n\t\t$this->logs->clear( 'notifications' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/notifications/controllers/class-llms-test-notification-controller-upcoming-payment-reminder.php",
    "content": "<?php\n/**\n * Upcoming Payment Reminder Notification Controller tests\n *\n * @package LifterLMS/Tests/Notifications/Controllers\n *\n * @group notification\n * @group notification_controller\n *\n * @since 5.2.0\n */\nclass LLMS_Test_Notification_Controller_Upcoming_Payment_Reminder extends LLMS_UnitTestCase {\n\n\t/**\n\t * LLMS_Abstract_Notification_Controller extending class instance\n\t *\n\t * @var LLMS_Abstract_Notification_Controller\n\t */\n\tprivate $controller;\n\n\t/**\n\t * Supported notification types.\n\t *\n\t * @var string[]\n\t */\n\tprivate $types;\n\n\t/**\n\t * Consider dates equal within 60 seconds\n\t *\n\t * @var int\n\t */\n\tprivate $date_delta = 60;\n\n\t/**\n\t * Set up\n\t *\n\t * @since 5.2.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->controller = LLMS_Notification_Controller_Upcoming_Payment_Reminder::instance();\n\t\t$this->types      = array_keys( $this->controller->get_supported_types() );\n\t}\n\n\t/**\n\t * Test action_callback() method\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_action_callback() {\n\n\t\t// Create order.\n\t\t$order = $this->get_mock_order();\n\t\t// Create post.\n\t\t$post_id = $this->factory->post->create();\n\n\t\t$recurring_payments_site_feature = LLMS_Site::get_feature( 'recurring_payments' );\n\n\t\tLLMS_Site::update_feature( 'recurring_payments', true );\n\n\t\t// Check notification sent for existing order and student.\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertTrue( $this->controller->action_callback( $order->get( 'id' ), $type ), $type );\n\t\t}\n\n\t\t// Check notification not sent for error gateway.\n\t\t$order->set( 'payment_gateway', 'garbage' );\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type );\n\t\t}\n\n\t\t// Check notification not sent for gateway that do not support recurring payments.\n\t\t$manual = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\t$manual->supports['recurring_payments'] = false;\n\t\t$order->set( 'gateway', 'manual' );\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type );\n\t\t}\n\n\t\t// Re-set recurring payments support for the manual gateway.\n\t\t$manual->supports['recurring_payments'] = true;\n\n\t\t// Check notification not sent for unexisting order.\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertFalse( $this->controller->action_callback( $order->get( 'id' ) + 1, $type ), $type );\n\t\t\t$this->assertFalse( $this->controller->action_callback( $post_id, $type ), $type );\n\t\t}\n\n\t\t// Check notication not sent for unexisting student.\n\t\t$order->set( 'user_id', $order->get( 'user_id' ) + 1 );\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type );\n\t\t}\n\n\t\tLLMS_Site::update_feature( 'recurring_payments', false );\n\n\t\t// Check notification not sent for staging sites.\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertFalse( $this->controller->action_callback( $order->get( 'id' ), $type ), $type );\n\t\t}\n\n\t\tLLMS_Site::update_feature( 'recurring_payments', $recurring_payments_site_feature );\n\n\t}\n\n\t/**\n\t * Test get_upcoming_payment_reminder_test()\n\t *\n\t * @since 5.2.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_upcoming_payment_reminder_test() {\n\n\t\t$plan = $this->get_mock_plan( 25.99, 1, 'lifetime', false, false );\n\t\t$plan->set( 'period', 'month' ); // Month.\n\t\t$plan->set( 'length', 3 ); // for 3 total payments.\n\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t$next_payment_date = $order->get_recurring_payment_due_date_for_scheduler();\n\n\t\t// Reminder days (prior to the payment due date): default is 1.\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertEquals(\n\t\t\t\tstrtotime( \"-1 day\", $next_payment_date ),\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->controller,\n\t\t\t\t\t'get_upcoming_payment_reminder_date',\n\t\t\t\t\tarray( $order, $type )\n\t\t\t\t),\n\t\t\t\t$type\n\t\t\t);\n\t\t}\n\n\t\t// Reminder days (prior to the payment due date): 10 and 11.\n\t\t$i = 0;\n\t\tforeach ( $this->types as $type ) {\n\n\t\t\t$days_option = LLMS_Unit_Test_Util::call_method( $this->controller, 'get_reminder_days', array( $type ) );\n\t\t\t$days = 10 + $i++;\n\t\t\t$this->controller->set_option( $type . '_reminder_days', $days );\n\n\t\t\t$this->assertEquals(\n\t\t\t\tstrtotime( \"-{$days} day\", $next_payment_date ),\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t$this->controller,\n\t\t\t\t\t'get_upcoming_payment_reminder_date',\n\t\t\t\t\tarray( $order, $type )\n\t\t\t\t),\n\t\t\t\t$type\n\t\t\t);\n\n\t\t\t$this->controller->set_option( $type . '_reminder_days', $days_option );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test schedule_upcoming_payment_reminder()\n\t *\n\t * @since 5.2.0\n\t * @since 5.3.3 Use `assertEqualsWithDelta()` in favor of 4th parameter to `assertEquals()`.\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_upcoming_payment_reminder() {\n\n\t\t$plan = $this->get_mock_plan( 25.99, 1, 'lifetime', false, false );\n\t\t$plan->set( 'period', 'month' ); // Month.\n\t\t$plan->set( 'length', 3 ); // for 3 total payments.\n\n\t\t$order = $this->get_mock_order( $plan );\n\n\t\t// Upcoming payment reminders are unscheduled.\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertFalse(\n\t\t\t\tas_next_scheduled_action(\n\t\t\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t\t\t'type'     => $type,\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$type\n\t\t\t);\n\t\t}\n\n\t\t$next_payment_date = $order->get_recurring_payment_due_date_for_scheduler();\n\n\t\t// Schedule.\n\t\t$this->controller->schedule_upcoming_payment_reminders( $order, $next_payment_date );\n\n\t\t// Check next payment reminder scheduled 1 day prior to payment due date.\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertEqualsWithDelta(\n\t\t\t\t(float) strtotime( \"-1 day\", $next_payment_date ),\n\t\t\t\tas_next_scheduled_action(\n\t\t\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t\t\t'type'     => $type,\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$this->date_delta,\n\t\t\t\t$type\n\t\t\t);\n\t\t}\n\n\t\t// Unschedule.\n\t\t$this->controller->unschedule_upcoming_payment_reminders( $order );\n\n\t\t// Fast forward.\n\t\tllms_tests_mock_current_time( date( 'Y-m-d', $next_payment_date + WEEK_IN_SECONDS ) );\n\n\t\t// Try to schedule a notification that should be happen 1 week - 1 day in the past.\n\t\tforeach ( $this->types as $type ) {\n\t\t\t$this->assertWPErrorCodeEquals( 'upcoming-payment-reminder-passed', $this->controller->schedule_upcoming_payment_reminder( $order, $type, $next_payment_date ) );\n\t\t\t$this->assertFalse(\n\t\t\t\tas_next_scheduled_action(\n\t\t\t\t\t'llms_send_upcoming_payment_reminder_notification',\n\t\t\t\t\tarray(\n\t\t\t\t\t\t'order_id' => $order->get( 'id' ),\n\t\t\t\t\t\t'type'     => $type,\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\t$type\n\t\t\t);\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/processors/class-llms-test-processor-awarded-achievements-bulk-sync.php",
    "content": "<?php\n/**\n * Test Awarded Achievements Bulk Sync background processor.\n *\n * @package LifterLMS/Tests\n *\n * @group processors\n * @group processor_awarded_achievements_bulk_sync\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Processor_Awarded_Achievements_Bulk_Sync extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var string\n\t */\n\tprivate $cron_hook_identifier;\n\n\t/**\n\t * @var LLMS_Processor_Achievement_Sync\n\t */\n\tprivate $main;\n\n\t/**\n\t * @var string\n\t */\n\tprivate $schedule_hook;\n\n\t/**\n\t * Setup before class\n\t *\n\t * Forces processor debugging on so that we can make assertions against logged data.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tllms_maybe_define_constant( 'LLMS_PROCESSORS_DEBUG', true );\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$this->main                 = llms()->processors()->get( 'achievement_sync' );\n\t\t$this->cron_hook_identifier = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cron_hook_identifier' );\n\t\t$this->schedule_hook        = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'schedule_hook' );\n\t}\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 6.0.0.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\t$this->main->cancel_process();\n\t\tparent::tear_down();\n\t}\n\n\t/**\n\t * Test dispatch_sync() when there are no awarded achievements to sync.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_sync_no_awarded_achievements() {\n\n\t\t$achievement_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_achievement',\n\t\t\t)\n\t\t);\n\n\t\t$this->main->dispatch_sync( $achievement_template );\n\n\t\t$this->assertEmpty( wp_next_scheduled( $this->cron_hook_identifier ) );\n\t}\n\n\t/**\n\t * Test dispatch_sync() when there are no publish/future awarded achievements to sync.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_sync_awarded_achievements_wrong_post_status() {\n\n\t\t$achievement_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_achievement',\n\t\t\t)\n\t\t);\n\t\t$awarded_achievements = $this->factory->post->create_many(\n\t\t\t3,\n\t\t\tarray(\n\t\t\t\t'post_parent' => $achievement_template,\n\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t'post_status' => 'draft'\n\t\t\t)\n\t\t);\n\n\t\t$this->main->dispatch_sync( $achievement_template );\n\t\t$this->assertEmpty( wp_next_scheduled( $this->cron_hook_identifier ) );\n\t}\n\n\t/**\n\t * Test dispatch_sync() when there are awarded achievements to sync.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_sync_awarded_achievements_success() {\n\n\t\t$achievement_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_achievement',\n\t\t\t)\n\t\t);\n\t\t$awarded_achievements = $this->factory->post->create_many(\n\t\t\t20,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t'post_parent' => $achievement_template,\n\t\t\t)\n\t\t);\n\n\t\t$handler = function ( $args ) {\n\t\t\t$args['per_page'] = 10;\n\t\t\treturn $args;\n\t\t};\n\n\t\tadd_filter( 'llms_processor_sync_awarded_achievements_query_args', $handler );\n\n\t\t$this->main->dispatch_sync( $achievement_template );\n\n\t\t// Test data is loaded into the queue properly.\n\t\tforeach ( LLMS_Unit_Test_Util::call_method( $this->main, 'get_batch' )->data as $i => $args ) {\n\n\t\t\t$query_args = $args['query_args'];\n\n\t\t\t$this->assertEquals( $achievement_template, $query_args['templates'] );\n\t\t\t$this->assertEquals( 10, $query_args['per_page'] );\n\t\t\t$this->assertEquals( array( 'publish', 'future' ), $query_args['status'] );\n\t\t\t$this->assertEquals( ++ $i, $query_args['page'] );\n\t\t}\n\n\t\t$this->assertEquals( 2, $i ); // Two chunks.\n\t\t$this->assertNotEmpty( wp_next_scheduled( $this->cron_hook_identifier ) );\n\n\t\tremove_filter( 'llms_processor_sync_awarded_achievements_query_args', $handler );\n\t}\n\n\t/**\n\t * Test schedule_sync() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_sync() {\n\n\t\t$achievement_template   = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_achievement',\n\t\t\t)\n\t\t);\n\t\t$achievement_template_2 = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_achievement',\n\t\t\t)\n\t\t);\n\n\t\t// The template has no awarded engagements to schedule a sync with.\n\t\tdo_action(\n\t\t\t'llms_do_awarded_achievements_bulk_sync',\n\t\t\t$achievement_template\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'no awarded achievements to bulk sync with the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t$awarded_achievements   = $this->factory->post->create_many(\n\t\t\t2,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t'post_parent' => $achievement_template,\n\t\t\t)\n\t\t);\n\t\t$awarded_achievements_2 = $this->factory->post->create_many(\n\t\t\t2,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t'post_parent' => $achievement_template_2,\n\t\t\t)\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\tdo_action(\n\t\t\t'llms_do_awarded_achievements_bulk_sync',\n\t\t\t$achievement_template\n\t\t);\n\t\t$this->assertNotEmpty( wp_next_scheduled( $this->schedule_hook, array( $achievement_template ) ) );\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync scheduled for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// A sync for a different achievement template is scheduled as well.\n\t\tdo_action(\n\t\t\t'llms_do_awarded_achievements_bulk_sync',\n\t\t\t$achievement_template_2\n\t\t);\n\t\t$this->assertNotEmpty( wp_next_scheduled( $this->schedule_hook, array( $achievement_template_2 ) ) );\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template_2 ),\n\t\t\t\t\t$achievement_template_2,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync scheduled for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template_2 ),\n\t\t\t\t\t$achievement_template_2,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// Already scheduled.\n\t\tdo_action(\n\t\t\t'llms_do_awarded_achievements_bulk_sync',\n\t\t\t$achievement_template_2\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template_2 ),\n\t\t\t\t\t$achievement_template_2,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync already scheduled for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template_2 ),\n\t\t\t\t\t$achievement_template_2,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\t}\n\n\t/**\n\t * Test LLMS_Processor_Achievement_Sync::task() and LLMS_Controller_Achievements::sync_awarded_engagements().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_task() {\n\t\t$this->markTestSkipped( 'Test fails randomly' );\n\n\t\t$achievement_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type'    => 'llms_achievement',\n\t\t\t\t'meta_input'   => array(\n\t\t\t\t\t'_llms_achievement_title' => 'A achievement title'\n\t\t\t\t),\n\t\t\t\t'post_content' => 'Achievement post content',\n\t\t\t)\n\t\t);\n\n\t\t$awarded_achievements_ids = $this->factory->post->create_many(\n\t\t\t2,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_achievement',\n\t\t\t\t'post_parent' => $achievement_template,\n\t\t\t)\n\t\t);\n\n\t\t// Manipulate awards results so to make the second sync fail.\n\t\t$filter_awards = function ( $awards ) {\n\t\t\t$awards[1]->set( 'parent', 0 ); // Remove the parent template.\n\t\t\treturn $awards;\n\t\t};\n\n\t\tadd_filter( 'llms_awards_query_get_awards', $filter_awards );\n\n\t\t$this->main->task(\n\t\t\tarray(\n\t\t\t\t'query_args' => array(\n\t\t\t\t\t'templates' => $achievement_template,\n\t\t\t\t\t'per_page'  => 20,\n\t\t\t\t\t'page'      => 1,\n\t\t\t\t\t'status'    => array(\n\t\t\t\t\t\t'publish',\n\t\t\t\t\t\t'future',\n\t\t\t\t\t),\n\t\t\t\t\t'type'      => 'achievement',\n\t\t\t\t\t'sort'      => array( 'ID', 'ASC' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\tremove_filter( 'llms_awards_query_get_awards', $filter_awards );\n\n\t\t$awarded_achievements = array();\n\t\tforeach ( $awarded_achievements_ids as $awarded_achievements_id ) {\n\t\t\t$awarded_achievements[] = new LLMS_User_Achievement( $awarded_achievements_id );\n\t\t}\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievements bulk sync task started for the achievement template %1$s (#%2$d) - chunk 1',\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievement %1$s (#%2$d) successfully synced with template %3$s (#%4$d)',\n\t\t\t\t\t$awarded_achievements[0]->get( 'title' ),\n\t\t\t\t\t$awarded_achievements[0]->get( 'id' ),\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'an error occurred while trying to sync awarded achievement %1$s (#%2$d) from template %3$s (#%4$d)',\n\t\t\t\t\t$awarded_achievements[1]->get( 'title' ),\n\t\t\t\t\t$awarded_achievements[1]->get( 'id' ),\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded achievement bulk sync completed for the achievement template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $achievement_template ),\n\t\t\t\t\t$achievement_template,\n\t\t\t\t)\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t// Check title/content sync.\n\t\t$this->assertEquals(\n\t\t\tget_post_meta( $achievement_template, '_llms_achievement_title', true ),\n\t\t\t$awarded_achievements[0]->get( 'title', true )\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tget_post( $achievement_template )->post_content,\n\t\t\t$awarded_achievements[0]->get( 'content', true )\n\t\t);\n\t\t$this->assertNotEquals(\n\t\t\tget_post_meta( $achievement_template, '_llms_achievement_title', true ),\n\t\t\t$awarded_achievements[1]->get( 'title', true )\n\t\t);\n\t\t$this->assertNotEquals(\n\t\t\tget_post( $achievement_template )->post_content,\n\t\t\t$awarded_achievements[1]->get( 'content', true )\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/processors/class-llms-test-processor-awarded-certificates-bulk-sync.php",
    "content": "<?php\n/**\n * Test Awarded Certificates Bulk Sync background processor.\n *\n * @package LifterLMS/Tests\n *\n * @group processors\n * @group processor_awarded_certificates_bulk_sync\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Processor_Awarded_Certificates_Bulk_Sync extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var string\n\t */\n\tprivate $cron_hook_identifier;\n\n\t/**\n\t * @var LLMS_Processor_Certificate_Sync\n\t */\n\tprivate $main;\n\n\t/**\n\t * @var string\n\t */\n\tprivate $schedule_hook;\n\n\t/**\n\t * Setup before class\n\t *\n\t * Forces processor debugging on so that we can make assertions against logged data.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tllms_maybe_define_constant( 'LLMS_PROCESSORS_DEBUG', true );\n\t}\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$this->main                 = llms()->processors()->get( 'certificate_sync' );\n\t\t$this->cron_hook_identifier = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cron_hook_identifier' );\n\t\t$this->schedule_hook        = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'schedule_hook' );\n\t}\n\n\t/**\n\t * Teardown the test case.\n\t *\n\t * @since 6.0.0.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\t$this->main->cancel_process();\n\t\tparent::tear_down();\n\t}\n\n\t/**\n\t * Test dispatch_sync() when there are no awarded certificates to sync.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_sync_no_awarded_certificates() {\n\n\t\t$certificate_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_certificate',\n\t\t\t)\n\t\t);\n\n\t\t$this->main->dispatch_sync( $certificate_template );\n\n\t\t$this->assertEmpty( wp_next_scheduled( $this->cron_hook_identifier ) );\n\t}\n\n\t/**\n\t * Test dispatch_sync() when there are no publish/future awarded certificates to sync.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_sync_awarded_certificates_wrong_post_status() {\n\n\t\t$certificate_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_certificate',\n\t\t\t)\n\t\t);\n\t\t$awarded_certificates = $this->factory->post->create_many(\n\t\t\t3,\n\t\t\tarray(\n\t\t\t\t'post_parent' => $certificate_template,\n\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t'post_status' => 'draft'\n\t\t\t)\n\t\t);\n\n\t\t$this->main->dispatch_sync( $certificate_template );\n\t\t$this->assertEmpty( wp_next_scheduled( $this->cron_hook_identifier ) );\n\t}\n\n\t/**\n\t * Test dispatch_sync() when there are awarded certificates to sync.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_sync_awarded_certificates_success() {\n\n\t\t$certificate_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_certificate',\n\t\t\t)\n\t\t);\n\t\t$awarded_certificates = $this->factory->post->create_many(\n\t\t\t20,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t'post_parent' => $certificate_template,\n\t\t\t)\n\t\t);\n\n\t\t$handler = function ( $args ) {\n\t\t\t$args['per_page'] = 10;\n\t\t\treturn $args;\n\t\t};\n\n\t\tadd_filter( 'llms_processor_sync_awarded_certificates_query_args', $handler );\n\n\t\t$this->main->dispatch_sync( $certificate_template );\n\n\t\t// Test data is loaded into the queue properly.\n\t\tforeach ( LLMS_Unit_Test_Util::call_method( $this->main, 'get_batch' )->data as $i => $args ) {\n\n\t\t\t$query_args = $args['query_args'];\n\n\t\t\t$this->assertEquals( $certificate_template, $query_args['templates'] );\n\t\t\t$this->assertEquals( 10, $query_args['per_page'] );\n\t\t\t$this->assertEquals( array( 'publish', 'future' ), $query_args['status'] );\n\t\t\t$this->assertEquals( ++ $i, $query_args['page'] );\n\t\t}\n\n\t\t$this->assertEquals( 2, $i ); // Two chunks.\n\t\t$this->assertNotEmpty( wp_next_scheduled( $this->cron_hook_identifier ) );\n\n\t\tremove_filter( 'llms_processor_sync_awarded_certificates_query_args', $handler );\n\t}\n\n\t/**\n\t * Test schedule_sync() method.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_sync() {\n\n\t\t$certificate_template   = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_certificate',\n\t\t\t)\n\t\t);\n\t\t$certificate_template_2 = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type' => 'llms_certificate',\n\t\t\t)\n\t\t);\n\n\t\t// The template has no awarded engagements to schedule a sync with.\n\t\tdo_action(\n\t\t\t'llms_do_awarded_certificates_bulk_sync',\n\t\t\t$certificate_template\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'no awarded certificates to bulk sync with the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t$awarded_certificates   = $this->factory->post->create_many(\n\t\t\t2,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t'post_parent' => $certificate_template,\n\t\t\t)\n\t\t);\n\t\t$awarded_certificates_2 = $this->factory->post->create_many(\n\t\t\t2,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t'post_parent' => $certificate_template_2,\n\t\t\t)\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\tdo_action(\n\t\t\t'llms_do_awarded_certificates_bulk_sync',\n\t\t\t$certificate_template\n\t\t);\n\t\t$this->assertNotEmpty( wp_next_scheduled( $this->schedule_hook, array( $certificate_template ) ) );\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync scheduled for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// A sync for a different certificate template is scheduled as well.\n\t\tdo_action(\n\t\t\t'llms_do_awarded_certificates_bulk_sync',\n\t\t\t$certificate_template_2\n\t\t);\n\t\t$this->assertNotEmpty( wp_next_scheduled( $this->schedule_hook, array( $certificate_template_2 ) ) );\n\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template_2 ),\n\t\t\t\t\t$certificate_template_2,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync scheduled for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template_2 ),\n\t\t\t\t\t$certificate_template_2,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// Already scheduled.\n\t\tdo_action(\n\t\t\t'llms_do_awarded_certificates_bulk_sync',\n\t\t\t$certificate_template_2\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template_2 ),\n\t\t\t\t\t$certificate_template_2,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync already scheduled for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template_2 ),\n\t\t\t\t\t$certificate_template_2,\n\t\t\t\t),\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t$this->logs->clear( 'processors' );\n\t}\n\n\t/**\n\t * Test LLMS_Processor_Certificate_Sync::task() and LLMS_Controller_Certificates::sync_awarded_engagements().\n\t *\n\t * @since 6.0.0\n\t * @since 7.1.0 Test logs as equal sets.\n\t *\n\t * @return void\n\t */\n\tpublic function test_task() {\n\n\t\t$this->markTestSkipped( 'Flaky test that will occasionally fail' );\n\n\t\t$certificate_template = $this->factory->post->create(\n\t\t\tarray(\n\t\t\t\t'post_type'    => 'llms_certificate',\n\t\t\t\t'meta_input'   => array(\n\t\t\t\t\t'_llms_certificate_title' => 'A certificate title'\n\t\t\t\t),\n\t\t\t\t'post_content' => 'Certificate post content',\n\t\t\t)\n\t\t);\n\n\t\t$awarded_certificates_ids = $this->factory->post->create_many(\n\t\t\t2,\n\t\t\tarray(\n\t\t\t\t'post_type'   => 'llms_my_certificate',\n\t\t\t\t'post_parent' => $certificate_template,\n\t\t\t)\n\t\t);\n\n\t\t// Manipulate awards results so to make the second sync fail.\n\t\t$filter_awards = function ( $awards ) {\n\t\t\t$awards[1]->set('parent', 0 ); // Remove the parent template.\n\t\t\treturn $awards;\n\t\t};\n\n\t\tadd_filter( 'llms_awards_query_get_awards', $filter_awards );\n\n\t\t$this->main->task(\n\t\t\tarray(\n\t\t\t\t'query_args' => array(\n\t\t\t\t\t'templates' => $certificate_template,\n\t\t\t\t\t'per_page'  => 20,\n\t\t\t\t\t'page'      => 1,\n\t\t\t\t\t'status'    => array(\n\t\t\t\t\t\t'publish',\n\t\t\t\t\t\t'future',\n\t\t\t\t\t),\n\t\t\t\t\t'type'      => 'certificate',\n\t\t\t\t\t'sort'      => array( 'ID', 'ASC' ),\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\n\t\tremove_filter( 'llms_awards_query_get_awards', $filter_awards );\n\n\t\t$awarded_certificates = array_map(\n\t\t\t'llms_get_certificate',\n\t\t\t$awarded_certificates_ids\n\t\t);\n\n\t\t$this->assertEqualSets(\n\t\t\tarray(\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificates bulk sync task started for the certificate template %1$s (#%2$d) - chunk 1',\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificate %1$s (#%2$d) successfully synced with template %3$s (#%4$d)',\n\t\t\t\t\t$awarded_certificates[0]->get( 'title' ),\n\t\t\t\t\t$awarded_certificates[0]->get( 'id' ),\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'an error occurred while trying to sync awarded certificate %1$s (#%2$d) from template %3$s (#%4$d)',\n\t\t\t\t\t$awarded_certificates[1]->get( 'title' ),\n\t\t\t\t\t$awarded_certificates[1]->get( 'id' ),\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t),\n\t\t\t\tsprintf(\n\t\t\t\t\t'awarded certificate bulk sync completed for the certificate template %1$s (#%2$d)',\n\t\t\t\t\tget_the_title( $certificate_template ),\n\t\t\t\t\t$certificate_template,\n\t\t\t\t)\n\t\t\t),\n\t\t\t$this->logs->get( 'processors' )\n\t\t);\n\n\t\t// Check title/content sync.\n\t\t$this->assertEquals(\n\t\t\tget_post_meta( $certificate_template, '_llms_certificate_title', true ),\n\t\t\t$awarded_certificates[0]->get( 'title', true )\n\t\t);\n\t\t$this->assertEquals(\n\t\t\tget_post( $certificate_template )->post_content,\n\t\t\t$awarded_certificates[0]->get( 'content', true )\n\t\t);\n\t\t$this->assertNotEquals(\n\t\t\tget_post_meta( $certificate_template, '_llms_certificate_title', true ),\n\t\t\t$awarded_certificates[1]->get( 'title', true )\n\t\t);\n\t\t$this->assertNotEquals(\n\t\t\tget_post( $certificate_template )->post_content,\n\t\t\t$awarded_certificates[1]->get( 'content', true )\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/processors/class-llms-test-processor-course-data.php",
    "content": "<?php\n/**\n * Test Course data background processor\n *\n * @package LifterLMS/Tests\n *\n * @group processors\n * @group processor_course_data\n *\n * @since 4.12.0\n */\nclass LLMS_Test_Processor_Course_Data extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup before class\n\t *\n\t * Forces processor debugging on so that we can make assertions against logged data.\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Renamed from `setUpBeforeClass()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\t\tllms_maybe_define_constant( 'LLMS_PROCESSORS_DEBUG', true );\n\n\t}\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$this->main          = llms()->processors()->get( 'course_data' );\n\t\t$this->schedule_hook = LLMS_Unit_Test_Util::get_private_property_value( $this->main, 'cron_hook_identifier' );\n\n\t}\n\n\t/**\n\t * Teardown the test case\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\t$this->main->cancel_process();\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'data', array() );\n\t\tparent::tear_down();\n\n\t}\n\n\t/**\n\t * Test dispatch_calc() when throttled by number of students\n\t *\n\t * @since 4.12.0\n\t * @since 4.21.0 Assert student enrolled count early.\n\t * @since 5.2.1 Added 5 second delta on date comparison assertion.\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_calc_throttled_by_students() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$this->factory->student->create_and_enroll_many( 2, $course_id );\n\n\t\t// Clear things so scheduling works right.\n\t\twp_unschedule_event( wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 'llms_calculate_course_data',  array( $course_id ) );\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// Fake throttling data.\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'throttle_max_students', 1 );\n\t\t$last_run = time() - HOUR_IN_SECONDS;\n\t\tupdate_post_meta( $course_id, '_llms_last_data_calc_run', $last_run );\n\n\t\t// Dispatch.\n\t\t$this->main->dispatch_calc( $course_id );\n\n\t\t/**\n\t\t * Even if a course is throttled the student count should be updated right away since it's not only used for reporting\n\t\t *\n\t\t * @link https://github.com/gocodebox/lifterlms/issues/1564\n\t\t */\n\t\t$this->assertEquals( 2, get_post_meta( $course_id, '_llms_enrolled_students', true ) );\n\n\t\t// Expected logs.\n\t\t$logs = array(\n\t\t\t\"Course data calculation dispatched for course {$course_id}.\",\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation scheduled for course {$course_id}.\",\n\t\t\t\"Course data calculation throttled for course {$course_id}.\",\n\t\t);\n\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t\t// Event scheduled.\n\t\t$this->assertEqualsWithDelta( time() + ( HOUR_IN_SECONDS * 4 ), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'throttle_max_students', 500 );\n\n\t}\n\n\t/**\n\t * Test dispatch_calc() when throttled because it's already processing for the course.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_calc_throttled_by_course() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$this->factory->student->create_and_enroll_many( 1, $course_id );\n\n\t\tupdate_post_meta( $course_id, '_llms_temp_calc_data_lock', 'yes' );\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// Dispatch.\n\t\t$this->main->dispatch_calc( $course_id );\n\n\t\t// Expected logs.\n\t\t$logs = array(\n\t\t\t\"Course data calculation dispatched for course {$course_id}.\",\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation throttled for course {$course_id}.\",\n\t\t);\n\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t}\n\n\t/**\n\t * Test dispatch_calc() when there's no students in the course\n\t *\n\t * @since 4.21.0\n \t * @since 5.2.1 Added 5 second delta on date comparison assertion.\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1596#issuecomment-821585937\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_calc_no_students() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$course    = llms_get_post( $course_id );\n\n\t\t// Mock meta data that may exist on the course (from a previous run, for example).\n\t\t$metas = array(\n\t\t\t'average_grade' => array( 95, 0 ),\n\t\t\t'average_progress' => array( 22, 0 ),\n\t\t\t'enrolled_students' => array( 204, 0 ),\n\t\t\t'last_data_calc_run' => array( time() - HOUR_IN_SECONDS, time() ),\n\t\t\t'temp_calc_data' => array( array( 123 ), array() ),\n\t\t);\n\t\tforeach ( $metas as $key => $vals ) {\n\t\t\t$course->set( $key, $vals[0] );\n\t\t}\n\n\t\t$this->main->dispatch_calc( $course_id );\n\n\t\tforeach ( $metas as $key => $vals ) {\n\t\t\t$delta = 'last_data_calc_run' === $key ? 5 : 0;\n\t\t\t$this->assertEqualsWithDelta( $vals[1], $course->get( $key ), $delta, $key );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test dispatch_calc()\n\t *\n\t * @since 4.12.0\n\t * @since 4.21.0 Assert student enrolled count early.\n\t *\n\t * @return void\n\t */\n\tpublic function test_dispatch_calc_success() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$this->factory->student->create_and_enroll_many( 5, $course_id );\n\t\t$this->logs->clear( 'processors' );\n\n\t\t$handler = function( $args ) {\n\t\t\t$args['per_page'] = 2;\n\t\t\treturn $args;\n\t\t};\n\t\tadd_filter( 'llms_data_processor_course_data_student_query_args', $handler );\n\n\t\t$this->main->dispatch_calc( $course_id );\n\n\t\t/**\n\t\t * Even if a course is throttled the student count should be updated right away since it's not only used for reporting\n\t\t *\n\t\t * @link https://github.com/gocodebox/lifterlms/issues/1564\n\t\t */\n\t\t$this->assertEquals( 5, get_post_meta( $course_id, '_llms_enrolled_students', true ) );\n\n\t\t// Logged properly.\n\t\t$this->assertEquals( array( \"Course data calculation dispatched for course {$course_id}.\" ), $this->logs->get( 'processors' ) );\n\n\t\t// Test data is loaded into the queue properly.\n\t\tforeach ( LLMS_Unit_Test_Util::call_method( $this->main, 'get_batch' )->data as $i => $args ) {\n\n\t\t\t$this->assertEquals( $course_id, $args['post_id'] );\n\t\t\t$this->assertEquals( 2, $args['per_page'] );\n\t\t\t$this->assertEquals( array( 'enrolled' ), $args['statuses'] );\n\t\t\t$this->assertEquals( ++$i, $args['page'] );\n\n\t\t}\n\n\t\t// Event scheduled.\n\t\t$this->assertTrue( ! empty( wp_next_scheduled( $this->schedule_hook ) ) );\n\n\t\tremove_filter( 'llms_data_processor_course_data_student_query_args', $handler );\n\n\t}\n\n\t/**\n\t * Test get_last_run()\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_last_run() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$this->assertEquals( 0, LLMS_Unit_Test_Util::call_method( $this->main, 'get_last_run', array( $course_id ) ) );\n\n\t\t$now = time();\n\t\tupdate_post_meta( $course_id, '_llms_last_data_calc_run', $now );\n\t\t$this->assertEquals( $now, LLMS_Unit_Test_Util::call_method( $this->main, 'get_last_run', array( $course_id ) ) );\n\n\t}\n\n\t/**\n\t * Test get_task_data()\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_task_data() {\n\n\t\t$data = array();\n\n\t\t// Default data only.\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_task_data' );\n\t\t$this->assertEquals( array(\n\t\t\t'students' => 0,\n\t\t\t'progress' => 0,\n\t\t\t'quizzes'  => 0,\n\t\t\t'grade'    => 0,\n\t\t), $res );\n\n\n\t\t// Merge in some data\n\t\t$merge = array(\n\t\t\t'progress' => 25,\n\t\t\t'students' => 203,\n\t\t\t'custom' => 'abc',\n\t\t);\n\t\t$res = LLMS_Unit_Test_Util::call_method( $this->main, 'get_task_data', array( $merge ) );\n\t\t$this->assertEquals( array(\n\t\t\t'students' => 203,\n\t\t\t'progress' => 25,\n\t\t\t'quizzes'  => 0,\n\t\t\t'grade'    => 0,\n\t\t\t'custom'   => 'abc',\n\t\t), $res );\n\n\t}\n\n\t/**\n\t * Test is_already_processing_course() when it's not processing.\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_already_processing_course() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$course    = llms_get_post( $course_id );\n\n\t\t// No meta data.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) );\n\n\t\t// Unexpected / invalid meta values.\n\t\t$course->set( 'temp_calc_data_lock', '' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) );\n\n\t\t$course->set( 'temp_calc_data_lock', 'no' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) );\n\n\t\t// Is running.\n\t\t$course->set( 'temp_calc_data_lock', 'yes' );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) );\n\n\n\t}\n\n\t/**\n\t * Test maybe_throttle()\n\t *\n\t * @since 4.12.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_throttle() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 25, $course_id ) ) );\n\n\t\t// Hasn't run recently.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 500, $course_id ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 2500, $course_id ) ) );\n\n\t\t// Should be throttled because of a recent run.\n\t\tupdate_post_meta( $course_id, '_llms_last_data_calc_run', time() - HOUR_IN_SECONDS );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 500, $course_id ) ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'maybe_throttle', array( 2500, $course_id ) ) );\n\n\t}\n\n\t/**\n\t * Test schedule_calculation()\n\t *\n\t * @since 4.21.0\n \t * @since 5.2.1 Added 5 second delta on date comparison assertions.\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_calculation() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t$expected_time = time() + HOUR_IN_SECONDS;\n\t\t$logs = array (\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation scheduled for course {$course_id}.\",\n\t\t);\n\n\t\t// Schedule an event.\n\t\t$this->main->schedule_calculation( $course_id, $expected_time );\n\t\t$this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// No duplicate scheduled.\n\t\t$this->main->schedule_calculation( $course_id );\n\t\t$this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\t\t$this->assertEquals( array( $logs[0] ), $this->logs->get( 'processors' ) );\n\n\t}\n\n\t/**\n\t * Test schedule_calculation() to ensure duplicate events aren't scheduled regardless of ID variable type\n\t *\n\t * @since 4.21.0\n \t * @since 5.2.1 Added 5 second delta on date comparison assertions.\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1600\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_calculation_string_or_int() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t$expected_time = time() + HOUR_IN_SECONDS;\n\t\t$logs = array (\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation scheduled for course {$course_id}.\",\n\t\t);\n\n\t\t// Schedule with an int.\n\t\t$this->main->schedule_calculation( $course_id, $expected_time );\n\t\t$this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t\t$this->logs->clear( 'processors' );\n\n\t\t// No duplicate should be scheduled if using a string later.\n\t\t$this->main->schedule_calculation( (string) $course_id );\n\t\t$this->assertEqualsWithDelta( $expected_time, wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\t\t$this->assertEquals( array( $logs[0] ), $this->logs->get( 'processors' ) );\n\n\t}\n\n\t/**\n\t * Test schedule_from_course()\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_from_course() {\n\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t$this->main->schedule_from_course( 123, $course_id );\n\n\t\t// Logs.\n\t\t$logs = array (\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation scheduled for course {$course_id}.\",\n\t\t);\n\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t\t// Event.\n\t\t$this->assertEqualsWithDelta( time(), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\n\t}\n\n\t/**\n\t * Test schedule_from_lesson()\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_from_lesson() {\n\n\t\t$course_id = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 1 ) );\n\t\t$lesson_id = llms_get_post( $course_id )->get_lessons( 'ids' )[0];\n\n\t\t$this->main->schedule_from_lesson( 123, $lesson_id );\n\n\t\t// Logs.\n\t\t$logs = array (\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation scheduled for course {$course_id}.\",\n\t\t);\n\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t\t// Event.\n\t\t$this->assertEqualsWithDelta( time(), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\n\t}\n\n\t/**\n\t * Test schedule_from_quiz()\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_schedule_from_quiz() {\n\n\t\t$course_id  = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 1 ) );\n\t\t$quiz_id    = llms_get_post( $course_id )->get_lessons()[0]->get( 'quiz' );\n\t\t$student_id = $this->factory->student->create();\n\t\t$attempt    = $this->take_quiz( $quiz_id, $student_id );\n\n\t\t$this->main->schedule_from_quiz( $student_id, $quiz_id, $attempt );\n\n\t\t// Logs.\n\t\t// In this particular test the process is already running because of the lesson completion triggered by the quiz.\n\t\t// This does not render the trigger entirely useless though as the quiz itself could trigger without lessons\n\t\t// when using add-ons that implement restrictions on lesson progression.\n\t\t$logs = array (\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation scheduled for course {$course_id}.\",\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t\t\"Course data calculation triggered for course {$course_id}.\",\n\t\t);\n\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t\t// Event.\n\t\t$this->assertEqualsWithDelta( time(), wp_next_scheduled( 'llms_calculate_course_data', array( $course_id ) ), 5 );\n\n\t}\n\n\t/**\n\t * Test task() method\n\t *\n\t * @since 4.12.0\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_task() {\n\n\t\t$course_id = $this->factory->course->create( array( 'sections' => 1, 'lessons' => 2, 'quizzes' => 1 ) );\n\t\t$course    = llms_get_post( $course_id );\n\t\t$students  = $this->factory->student->create_and_enroll_many( 5, $course_id );\n\n\t\tforeach ( $students as $i => $student ) {\n\t\t\t$perc = array( 0, 50, 50, 100, 100 );\n\t\t\t$this->complete_courses_for_student( $student, $course_id, $perc[ $i ] );\n\t\t}\n\n\t\t// Clear any data that may exist as a result of mock data creation above.\n\t\tdelete_post_meta( $course_id, '_llms_temp_calc_data' );\n\n\t\t// Perform task for page 1, not completed, save the data.\n\t\t$this->assertFalse( $this->main->task( array(\n\t\t\t'post_id' => $course_id,\n\t\t\t'statuses' => array( 'enrolled' ),\n\t\t\t'page'     => 1,\n\t\t\t'per_page' => 2,\n\t\t\t'sort'     => array(\n\t\t\t\t'id' => 'ASC',\n\t\t\t),\n\t\t) ) );\n\n\t\t$expect = array(\n\t\t\t'students' => 2,\n\t\t\t'progress' => floatval( 50 ),\n\t\t\t'quizzes'  => 0,\n\t\t\t'grade'    => 0,\n\t\t);\n\t\t$this->assertEquals( $expect, $course->get( 'temp_calc_data' ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) );\n\n\n\t\t// Perform task for page 2, not completed, save the data.\n\t\t$this->assertFalse( $this->main->task( array(\n\t\t\t'post_id' => $course_id,\n\t\t\t'statuses' => array( 'enrolled' ),\n\t\t\t'page'     => 2,\n\t\t\t'per_page' => 2,\n\t\t\t'sort'     => array(\n\t\t\t\t'id' => 'ASC',\n\t\t\t),\n\t\t) ) );\n\n\t\t$expect = array(\n\t\t\t'students' => 4,\n\t\t\t'progress' => floatval( 200 ),\n\t\t\t'quizzes'  => 1,\n\t\t\t'grade'    => floatval( 100 ),\n\t\t);\n\t\t$this->assertEquals( $expect, $course->get( 'temp_calc_data' ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) );\n\n\t\t// Perform task for page 3, completed.\n\t\t$this->assertFalse( $this->main->task( array(\n\t\t\t'post_id' => $course_id,\n\t\t\t'statuses' => array( 'enrolled' ),\n\t\t\t'page'     => 3,\n\t\t\t'per_page' => 2,\n\t\t\t'sort'     => array(\n\t\t\t\t'id' => 'ASC',\n\t\t\t),\n\t\t) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->main, 'is_already_processing_course', array( $course_id ) ) );\n\t\t$this->assertEmpty( $course->get( 'temp_calc_data' ) );\n\t\t$this->assertEmpty( $course->get( 'temp_calc_data_lock' ) );\n\t\t$this->assertEquals( 100, $course->get( 'average_grade' ) );\n\t\t$this->assertEquals( 60, $course->get( 'average_progress' ) );\n\t\t$this->assertEquals( 5, $course->get( 'enrolled_students' ) );\n\t\t$this->assertEqualsWithDelta( time(), $course->get( 'last_data_calc_run' ), 5 );\n\n\t}\n\n\t/**\n\t * Test deleted / nonexistant courses/posts.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_task_nonexistent_course() {\n\n\t\t$tests = array(\n\t\t\t// Deleted course.\n\t\t\t$this->factory->post->create( array( 'post_type' => 'course' ) ),\n\n\t\t\t// Not a course.\n\t\t\t$this->factory->post->create(),\n\t\t);\n\n\t\twp_delete_post( $tests[0], true );\n\n\t\t// Not a real post at all.\n\t\t$tests[] = $tests[1] + 1;\n\n\t\tforeach ( $tests as $post_id ) {\n\n\t\t\t$args = compact( 'post_id' );\n\t\t\t$this->assertFalse( $this->main->task( $args ) );\n\n\t\t\t$json = wp_json_encode( $args );\n\n\t\t\t$logs = array (\n\t\t\t\t\"Course data calculation task called for course {$post_id} with args: {$json}\",\n\t\t\t\t\"Course data calculation task skipped for course {$post_id}.\",\n\t\t\t);\n\n\t\t\t$this->assertEquals( $logs, $this->logs->get( 'processors' ) );\n\n\t\t\t$this->logs->clear( 'processors' );\n\n\t\t}\n\n\t}\n\n\n\t/**\n\t * Test dispatch_calc() with multiple courses to make sure that tasks are not duplicated in other batches.\n\t *\n\t * @since 4.21.0\n\t *\n\t * @link https://github.com/gocodebox/lifterlms/issues/1602\n\t *\n\t * @return void\n\t */\n\tpublic function test_duplicate_batch_tasks() {\n\n\t\t$course_ids[] = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\t$course_ids[] = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\t\tforeach ( $course_ids as $course_id ) {\n\t\t\t$this->factory->student->create_and_enroll_many( 5, $course_id );\n\t\t}\n\t\t$this->logs->clear( 'processors' );\n\n\t\t$handler = function ( $args ) {\n\t\t\t$args['per_page'] = 2;\n\n\t\t\treturn $args;\n\t\t};\n\t\tadd_filter( 'llms_data_processor_course_data_student_query_args', $handler );\n\n\t\t$expected_logs = array();\n\t\tforeach ( $course_ids as $course_id ) {\n\t\t\t$this->main->dispatch_calc( $course_id );\n\t\t\t$expected_logs[] = \"Course data calculation dispatched for course {$course_id}.\";\n\t\t}\n\t\t// Logged properly.\n\t\t$this->assertEquals( $expected_logs, $this->logs->get( 'processors' ) );\n\n\t\tforeach ( $course_ids as $course_id ) {\n\t\t\t$batch = LLMS_Unit_Test_Util::call_method( $this->main, 'get_batch' );\n\n\t\t\t// Test data is loaded into the queue properly.\n\t\t\tforeach ( $batch->data as $i => $student_query_args ) {\n\t\t\t\t$this->assertEquals( $course_id, $student_query_args['post_id'], $course_id );\n\t\t\t\t$this->assertEquals( 2, $student_query_args['per_page'], 'per_page' );\n\t\t\t\t$this->assertEquals( array( 'enrolled' ), $student_query_args['statuses'], 'statuses' );\n\t\t\t\t$this->assertEquals( ++ $i, $student_query_args['page'], 'page' );\n\t\t\t}\n\n\t\t\t// Simulate handling of queued batched tasks.\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->main, 'delete', array( $batch->key ) );\n\t\t}\n\n\t\tremove_filter( 'llms_data_processor_course_data_student_query_args', $handler );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/processors/class-llms-test-processors.php",
    "content": "<?php\n/**\n * Test LLMS_Processors\n *\n * @package LifterLMS/Tests\n *\n * @group processors\n *\n * @since 5.0.0\n */\nclass LLMS_Test_Processors extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup test case\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->main = LLMS_Processors::instance();\n\t}\n\n\t/**\n\t * Test `instance()`.\n\t *\n\t * @since 5.0.0\n\t * @since 5.3.0 Rename `_instance` property to `instance`.\n\t * @since 6.0.0 Removed testing of the removed `LLMS_Processors::$_instance` property.\n\t * @since 7.5.0 Removed testing of mock property.\n\t *\n\t * @runInSeparateProcess\n\t * @preserveGlobalState disabled\n\t *\n\t * @return void\n\t */\n\tpublic function test_instance() {\n\n\t\t$this->assertEquals( $this->main, LLMS_Processors::instance() );\n\n\t\tLLMS_Unit_Test_Util::set_private_property( $this->main, 'instance', null );\n\t\t$new_instance = LLMS_Processors::instance();\n\t\t$this->assertInstanceOf( 'LLMS_Processors', $new_instance );\n\n\t}\n\n\t/**\n\t * Test get()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get() {\n\n\t\t$this->assertInstanceOf( 'LLMS_Processor_Course_Data', $this->main->get( 'course_data' ) );\n\t\t$this->assertFalse( $this->main->get( 'fake' ) );\n\n\t}\n\n\t/**\n\t * Test load_processor()\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_load_processor() {\n\n\t\t$this->assertTrue( $this->main->load_processor( 'course_data' ) );\n\t\t$this->assertFalse( $this->main->load_processor( 'fake' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-checkout.php",
    "content": "<?php\n/**\n * Test the [lifterlms_checkout] Shortcode\n *\n * @group shortcodes\n *\n * @since 5.1.0\n */\nclass LLMS_Test_Shortcode_Checkout extends LLMS_ShortcodeTestCase {\n\n\t/**\n\t * Test shortcode registration.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_registration() {\n\t\t$this->assertTrue( shortcode_exists( 'lifterlms_checkout' ) );\n\t}\n\n\t/**\n\t * Retrieves the output of {@see LLMS_Shortcode_Checkout::output}.\n\t *\n\t * @since 7.0.1\n\t *\n\t * @return string\n\t */\n\tprivate function get_shortcode_output() {\n\t\treturn $this->get_output(\n\t\t\tarray(\n\t\t\t\t'LLMS_Shortcode_Checkout',\n\t\t\t\t'output'\n\t\t\t),\n\t\t\tarray( null )\n\t\t);\n\t}\n\n\t/**\n\t * Determines if the checkout opening wrapper markup is found in the given\n\t * output string.\n\t *\n\t * @since 7.0.1\n\t *\n\t * @param string $output The output string.\n\t */\n\tprivate function assertContainsOpeningWrapper( $output ) {\n\t\t$this->assertStringContainsString(\n\t\t\t'<div class=\"llms-checkout-wrapper\">',\n\t\t\t$output\n\t\t);\n\t}\n\n\t/**\n\t * Determines if the checkout closing wrapper markup is found in the given\n\t * output string.\n\t *\n\t * @since 7.0.1\n\t *\n\t * @param string $output The output string.\n\t */\n\tprivate function assertContainsClosingWrapper( $output ) {\n\t\t$this->assertStringContainsString(\n\t\t\t'</div><!-- .llms-checkout-wrapper -->',\n\t\t\t$output\n\t\t);\n\t}\n\n\t/**\n\t * Test clean_form_fields.\n\t *\n\t * @since 5.1.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_clean_form_fields() {\n\n\t\t$checks = array(\n\t\t\t'<p></p>'               => '',\n\t\t\t'<p>a</p>'              => '<p>a</p>',\n\t\t\t\"\\n\"                    => '',\n\t\t\t\"\\t\"                    => '',\n\t\t\t\"\\n\\r\\t\"                => '',\n\t\t\t\"<p></p>\\n<p>a</p>\\r\\t\" => \"<p></p>\\n<p>a</p>\\r\\t\",\n\t\t);\n\n\t\tforeach ( $checks as $check => $expect ) {\n\t\t\t$this->assertEquals(\n\t\t\t\t$expect,\n\t\t\t\tLLMS_Unit_Test_Util::call_method(\n\t\t\t\t\t'LLMS_Shortcode_Checkout',\n\t\t\t\t\t'clean_form_fields',\n\t\t\t\t\tarray( $check )\n\t\t\t\t),\n\t\t\t\t$check\n\t\t\t);\n\t\t}\n\n\t}\n\n\t/**\n\t * Test setup_plan_and_form_atts() method adds redirection hidden field when needed.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_setup_plan_and_form_atts_check_redirection() {\n\n\t\t$plan      = $this->get_mock_plan();\n\t\t$contained = '<input class=\"llms-field-input\" id=\"llms-redirect\"'; // Redirect field.\n\n\t\t$atts = LLMS_Unit_Test_Util::call_method(\n\t\t\t'LLMS_Shortcode_Checkout',\n\t\t\t'setup_plan_and_form_atts',\n\t\t\tarray(\n\t\t\t\t$plan->get('id'),\n\t\t\t\tarray(),\n\t\t\t)\n\t\t);\n\n\t\t$this->assertStringNotContainsString( $contained, $atts['form_fields'] );\n\n\t\t// Setup a redirect URL for the access plan.\n\t\t$plan->set( 'checkout_redirect_type', 'url' );\n\t\t$plan->set( 'checkout_redirect_url', 'https://example.com' );\n\t\t$atts = LLMS_Unit_Test_Util::call_method(\n\t\t\t'LLMS_Shortcode_Checkout',\n\t\t\t'setup_plan_and_form_atts',\n\t\t\tarray(\n\t\t\t\t$plan->get('id'),\n\t\t\t\tarray(),\n\t\t\t)\n\t\t);\n\t\t$contained = '<input class=\"llms-field-input\" id=\"llms-redirect\" name=\"redirect\" type=\"hidden\" value=\"https://example.com\" />';\n\t\t$this->assertStringContainsString( $contained, $atts['form_fields'] );\n\n\t\t// INPUT_GET wins over plan's setting.\n\t\t$this->mockGetRequest(\n\t\t\tarray(\n\t\t\t\t'redirect' => 'https://example-redirect-get.com',\n\t\t\t)\n\t\t);\n\t\t$atts = LLMS_Unit_Test_Util::call_method(\n\t\t\t'LLMS_Shortcode_Checkout',\n\t\t\t'setup_plan_and_form_atts',\n\t\t\tarray(\n\t\t\t\t$plan->get('id'),\n\t\t\t\tarray(),\n\t\t\t)\n\t\t);\n\t\t$contained = '<input class=\"llms-field-input\" id=\"llms-redirect\" name=\"redirect\" type=\"hidden\" value=\"https://example-redirect-get.com\" />';\n\t\t$this->assertStringContainsString( $contained, $atts['form_fields'] );\n\t}\n\n\t/**\n\t * Test setup_plan_and_form_atts() method adds redirection hidden field when needed.\n\t * Test checkout wrapper on empty cart.\n\t *\n\t * @since 7.0.0\n\t * @since 7.0.1 Updated to use new test utility methods.\n\t *\n\t * @return void\n\t */\n\n\tpublic function test_checkout_wrapper_on_empty_cart() {\n\n\t\t$output = $this->get_shortcode_output();\n\t\t$this->assertStringContainsString(\n\t\t\t'Your cart is currently empty.',\n\t\t\t$output\n\t\t);\n\t\t$this->assertContainsOpeningWrapper( $output );\n\t\t$this->assertContainsClosingWrapper( $output );\n\n\t}\n\n\t/**\n\t * Test checkout wrapper on pre checkout error.\n\t *\n\t * @since 7.0.0\n\t * @since 7.0.1 Updated to use new test utility methods.\n\t *\n\t * @return void\n\t */\n\tpublic function test_checkout_wrapper_on_pre_checkout_error() {\n\n\t\t$pre_checkout_error = function() {\n\t\t\treturn 'Pre checkout error.';\n\t\t};\n\t\tadd_filter( 'lifterlms_pre_checkout_error', $pre_checkout_error );\n\n\t\t$output = $this->get_shortcode_output();\n\t\t$this->assertStringContainsString(\n\t\t\t'Pre checkout error.',\n\t\t\t$output\n\t\t);\n\t\t$this->assertContainsOpeningWrapper( $output );\n\t\t$this->assertContainsClosingWrapper( $output );\n\n\t\tremove_filter( 'lifterlms_pre_checkout_error', $pre_checkout_error );\n\n\t}\n\n\n\t/**\n\t * Test checkout wrapper when invalid access plan is supplied.\n\t *\n\t * @since 7.0.0\n\t * @since 7.0.1 Updated to use new test utility methods.\n\t *\n\t * @return void\n\t */\n\tpublic function test_checkout_wrapper_confirm_payment_invalid_plan() {\n\n\t\t$this->mockGetRequest( array( 'plan' => $this->factory->post->create() ) );\n\n\t\t$output = $this->get_shortcode_output();\n\t\t$this->assertStringContainsString(\n\t\t\t'Invalid access plan.',\n\t\t\t$output\n\t\t);\n\t\t$this->assertContainsOpeningWrapper( $output );\n\t\t$this->assertContainsClosingWrapper( $output );\n\n\t}\n\n\t/**\n\t * Test checkout wrapper on confirm payment when no order is supplied.\n\t *\n\t * @since 7.0.0\n\t * @since 7.0.1 Updated to use new test utility methods.\n\t *\n\t * @return void\n\t */\n\tpublic function test_checkout_wrapper_confirm_payment_no_order() {\n\t\tglobal $wp;\n\t\t$wpt = $wp;\n\t\t$wp->query_vars['confirm-payment'] = true;\n\n\t\t$output = $this->get_shortcode_output();\n\t\t$this->assertStringContainsString(\n\t\t\t'Could not locate an order to confirm.',\n\t\t\t$output\n\t\t);\n\t\t$this->assertContainsOpeningWrapper( $output );\n\t\t$this->assertContainsClosingWrapper( $output );\n\n\t\t$wp = $wpt;\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Shortcode_Checkout::output} when confirming a payment\n\t * for an invalid order.\n\t *\n\t * @since 7.0.1\n\t */\n\tpublic function test_output_confirm_payment_invalid_order() {\n\n\t\tglobal $wp;\n\t\t$wpt = $wp;\n\t\t$wp->query_vars['confirm-payment'] = true;\n\n\t\t// Fake order.\n\t\t$this->mockGetRequest( array(\n\t\t\t'order' => 'order-' . wp_generate_password( 32, false ),\n\t\t) );\n\n\t\t$output = $this->get_shortcode_output();\n\t\t$this->assertStringContainsString(\n\t\t\t'Could not locate an order to confirm.',\n\t\t\t$output\n\t\t);\n\t\t$this->assertContainsOpeningWrapper( $output );\n\t\t$this->assertContainsClosingWrapper( $output );\n\n\t\t$wp = $wpt;\n\n\t}\n\n\t/**\n\t * Tests {@see LLMS_Shortcode_Checkout::output} when confirming a payment\n\t * for an invalid order.\n\t *\n\t * @since 7.0.1\n\t */\n\tpublic function test_output_confirm_payment_real_order() {\n\n\t\tglobal $wp;\n\t\t$wpt = $wp;\n\t\t$wp->query_vars['confirm-payment'] = true;\n\n\t\t$order = $this->factory->order->create_and_get();\n\n\t\t// Fake order.\n\t\t$this->mockGetRequest( array(\n\t\t\t'order' => $order->get( 'order_key' ),\n\t\t) );\n\n\t\t$output = $this->get_shortcode_output();\n\t\t$this->assertStringContainsString(\n\t\t\t'id=\"llms-product-purchase-confirm-form\"',\n\t\t\t$output\n\t\t);\n\n\t\t$wp = $wpt;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-course-progress.php",
    "content": "<?php\n/**\n * Test the [lifterlms_course_progress] Shortcode\n *\n * @group shortcodes\n *\n * @since 3.38.0\n * @version 3.38.0\n */\nclass LLMS_Test_Shortcode_Course_Progress extends LLMS_ShortcodeTestCase {\n\n\t/**\n\t * Test shortcode registration\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_registration() {\n\t\t$this->assertTrue( shortcode_exists( 'lifterlms_course_progress' ) );\n\t}\n\n\t/**\n\t * Test shortcode output\n\t *\n\t * @since 3.38.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_output() {\n\n\t\t$course = $this->factory->post->create( array(\n\t\t\t'post_type' => 'course',\n\t\t) );\n\n\t\t// Alter some globals just to emulate we're in a singular course.\n\t\tglobal $post, $wp_query;\n\t\t$temp_q = $wp_query;\n\t\t$temp_p = $post;\n\n\t\t$wp_query->queried_object = get_post($course);\n\t\t$wp_query->is_singular = true;\n\t\t$post = $wp_query->queried_object;\n\n\n\t\t$expected_shortcode = '<div class=\"llms-progress\">\n\t\t<div class=\"progress__indicator\">0%</div>\n\t\t<div class=\"llms-progress-bar\">\n\t\t\t<div class=\"progress-bar-complete\" data-progress=\"0%\"  style=\"width:0%\"></div>\n\t\t</div></div>';\n\n\t\t// Test against logged out user.\n\t\t$this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress]' );\n\t\t$this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=0]' );\n\n\t\t// Progress should not be shown to logged out users if check_enrollment=1.\n\t\t$this->assertShortcodeOutputEquals( '', '[lifterlms_course_progress check_enrollment=1]' );\n\n\t\t// Get a student and try again\n\t\t$student = $this->get_mock_student( true );\n\n\t\t$student->enroll( $course );\n\n\t\t// Student enrolled: progress should always be shown\n\t\t$this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress]' );\n\t\t$this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=0]' );\n\t\t$this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=1]' );\n\n\t\t$student->unenroll( $course );\n\t\t// Student unenrolled but logged in: same as logged out.\n\t\t$this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress]' );\n\t\t$this->assertShortcodeOutputEquals( $expected_shortcode, '[lifterlms_course_progress check_enrollment=0]' );\n\t\t$this->assertShortcodeOutputEquals( '', '[lifterlms_course_progress check_enrollment=1]' );\n\n\t\t// Reset globals alterations.\n\t\t$wp_query = $temp_q;\n\t\t$post     = $temp_p;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-hide-content.php",
    "content": "<?php\n/**\n * Test the [lifterlms_hide_content] Shortcode\n * @group    shortcodes\n * @since    3.24.1\n * @version  3.30.2\n */\nclass LLMS_Test_Shortcode_Hide_Content extends LLMS_ShortcodeTestCase {\n\n\t/**\n\t * Class name of the Shortcode Class\n\t * @var string\n\t */\n\tpublic $class_name = 'LLMS_Shortcode_Hide_Content';\n\n\tpublic function test_get_output() {\n\n\t\t// Test against logged out user.\n\t\t$this->assertShortcodeOutputEquals( '', '[lifterlms_hide_content id=\"1\"]Secrets.[/lifterlms_hide_content]' );\n\n\t\t// Logged out with multiples & different relationships.\n\t\t$this->assertShortcodeOutputEquals( '', '[lifterlms_hide_content id=\"1,2,3,4\" relation=\"any\"]Secrets.[/lifterlms_hide_content]' );\n\t\t$this->assertShortcodeOutputEquals( '', '[lifterlms_hide_content id=\"1,2,3,4\" relation=\"all\"]Secrets.[/lifterlms_hide_content]' );\n\n\t\t// Show a message\n\t\t$this->assertShortcodeOutputEquals( 'Nope.', '[lifterlms_hide_content id=\"1\" message=\"Nope.\"]Secrets.[/lifterlms_hide_content]' );\n\t\t$this->assertShortcodeOutputEquals( 'Nope.', '[lifterlms_hide_content id=\"1,2,3,4\" message=\"Nope.\" relation=\"any\"]Secrets.[/lifterlms_hide_content]' );\n\n\n\t\t// get a student and try again\n\t\t$student = $this->get_mock_student( true );\n\n\t\t// check against both courses and memberships\n\t\tforeach ( array( 'course', 'llms_membership' ) as $post_type ) {\n\n\t\t\t$ids = $this->factory->post->create_many( 3, array(\n\t\t\t\t'post_type' => $post_type,\n\t\t\t) );\n\n\t\t\t// enroll only in the first.\n\t\t\t$student->enroll( $ids[0] );\n\n\t\t\t// Can see secrets b/c enrollment.\n\t\t\t$this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id=\"%d\"]Secrets.[/lifterlms_hide_content]', $ids[0] ) );\n\n\t\t\t// Cannot see b/c no enrollment.\n\t\t\t$this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id=\"%d\"]Secrets.[/lifterlms_hide_content]', $ids[1] ) );\n\t\t\t$this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id=\"%d\"]Secrets.[/lifterlms_hide_content]', $ids[2] ) );\n\n\t\t\t// Must belong to all and does not.\n\t\t\t$this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id=\"%s\" relation=\"all\"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[1] ) );\n\n\t\t\t// Must belong to any and only belongs to one.\n\t\t\t$this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id=\"%s\" relation=\"any\"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[1] ) );\n\n\t\t\t// Enroll in another\n\t\t\t$student->enroll( $ids[2] );\n\n\t\t\t// Check two, belongs to both.\n\t\t\t$this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id=\"%s\" relation=\"all\"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[2] ) );\n\n\t\t\t// Check three.\n\t\t\t$this->assertShortcodeOutputEquals( '', sprintf( '[lifterlms_hide_content id=\"%s\" relation=\"all\"]Secrets.[/lifterlms_hide_content]', implode( ',', $ids ) ) );\n\n\t\t\t// Check any of the two (belongs to both).\n\t\t\t$this->assertShortcodeOutputEquals( 'Secrets.', sprintf( '[lifterlms_hide_content id=\"%s\" relation=\"all\"]Secrets.[/lifterlms_hide_content]', $ids[0] . ', ' . $ids[2] ) );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/shortcodes/class-llms-test-shortcode-user-info.php",
    "content": "<?php\n/**\n * Test the User Info shortcode\n *\n * @package LifterLMS/Tests\n *\n * @group shortcodes\n * @group userinfo_shortcode\n *\n * @since 5.0.0\n * @version 5.0.0\n */\nclass LLMS_Test_Shortcode_User_Info extends LLMS_ShortcodeTestCase {\n\n\t/**\n\t * Class name of the Shortcode Class\n\t * @var string\n\t */\n\tpublic $class_name = 'LLMS_Shortcode_User_Info';\n\n\t/**\n\t * Test setting attributes with no key for the first attribute (field name).\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_attributes_field_no_key() {\n\n\t\t$obj = $this->get_class();\n\n\t\t$atts = array(\n\t\t\t0 => 'first_name',\n\t\t\t'or' => 'mock',\n\t\t);\n\n\t\t$this->assertArrayHasKey( 'key', LLMS_Unit_Test_Util::call_method( $obj, 'set_attributes', array( $atts ) ) );\n\t\t$this->assertArrayHasKey( 'or', LLMS_Unit_Test_Util::call_method( $obj, 'set_attributes', array( $atts ) ) );\n\n\t}\n\n\t/**\n\t * Test setting attributes when the field name is passed.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_attributes_field_regular() {\n\n\t\t$obj = $this->get_class();\n\n\t\t$atts = array(\n\t\t\t'key' => 'first_name',\n\t\t);\n\t\t$this->assertArrayHasKey( 'key', LLMS_Unit_Test_Util::call_method( $obj, 'set_attributes', array( $atts ) ) );\n\n\t}\n\n\t/**\n\t * Test get_output() with logged out user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_output_no_user() {\n\n\t\t$this->assertShortcodeOutputEquals( '', '[llms-user first_name]' );\n\t\t$this->assertShortcodeOutputEquals( 'Pal', '[llms-user first_name or=\"Pal\"]' );\n\n\t}\n\n\t/**\n\t * Test get_output() with logged in user.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_output_with_user() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\t\twp_set_current_user( $user->ID );\n\n\t\t// No value set.\n\t\t$this->assertShortcodeOutputEquals( 'Bucko', '[llms-user first_name or=\"Bucko\"]' );\n\n\t\tupdate_user_meta( $user->ID, 'first_name', 'mock' );\n\t\t$this->assertShortcodeOutputEquals( 'mock', '[llms-user first_name]' );\n\n\t\t// Works.\n\t\t$this->assertShortcodeOutputEquals( $user->ID, '[llms-user ID]' );\n\t\t$this->assertShortcodeOutputEquals( $user->display_name, '[llms-user display_name]' );\n\t\t$this->assertShortcodeOutputEquals( $user->user_email, '[llms-user user_email]' );\n\n\t\t// Blocked.\n\t\t$this->assertShortcodeOutputEquals( '', '[llms-user user_pass]' );\n\n\t\tupdate_user_meta( $user->ID, 'llms_phone', '123456789' );\n\t\t$this->assertShortcodeOutputEquals( '123456789', '[llms-user llms_phone]' );\n\n\t}\n\n\t/**\n\t * Test output when filtering the user to display another user's information\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_output_for_another() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t$handler = function( $uid ) use( $user ) {\n\t\t\treturn $user->ID;\n\t\t};\n\t\tadd_filter( 'llms_user_info_shortcode_user_id', $handler );\n\n\t\t// Works.\n\t\t$this->assertShortcodeOutputEquals( $user->ID, '[llms-user ID]' );\n\t\t$this->assertShortcodeOutputEquals( $user->display_name, '[llms-user display_name]' );\n\t\t$this->assertShortcodeOutputEquals( $user->user_email, '[llms-user user_email]' );\n\n\t\tremove_filter( 'llms_user_info_shortcode_user_id', $handler );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/tables/class-llms-test-table-course-students.php",
    "content": "<?php\n/**\n * Test the course students reporting table.\n *\n * @package LifterLMS/Tests/Tables\n *\n * @group reporting_tables\n *\n * @since 5.10.0\n */\nclass LLMS_Test_Table_Course_Students extends LLMS_UnitTestCase {\n\t/**\n\t * The course ID used in tests.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @var int\n\t */\n\tprivate $course_id;\n\n\t/**\n\t * An instance of LLMS_Table_Course_Students to run tests on.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @var LLMS_Table_Course_Students\n\t */\n\tprivate $table;\n\n\t/**\n\t * Executes a student query and asserts that the resulting students match the given expected students.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @param array $args          Arguments to pass to {@see LLMS_Table_Course_Students::get_results()}.\n\t * @param array $expected_data The expected array of data.\n\t * @return void\n\t */\n\tprivate function assert_student_results_equal( $args, $expected_data ) {\n\n\t\t$this->table->get_results( $args );\n\t\t$actual_students = $this->table->get_tbody_data();\n\n\t\t$actual_data = array();\n\t\t/** @var LLMS_Student[] $actual_students */\n\t\tforeach ( $actual_students as $student ) {\n\t\t\tswitch ( $args['orderby'] ) {\n\t\t\t\tcase 'completed':\n\t\t\t\t\t$actual_data[ $student->get( 'id' ) ] = $student->get_completion_date( $this->course_id, 'U' );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'enrolled':\n\t\t\t\t\t$actual_data[ $student->get( 'id' ) ] = $student->get_enrollment_date( $this->course_id, 'updated', 'U' );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\t$actual_data[ $student->get( 'id' ) ] = $this->table->get_data( $args['orderby'], $student );\n\t\t\t}\n\t\t}\n\n\t\t$this->assertEquals( $expected_data, $actual_data );\n\t}\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/tables/llms.table.course.students.php';\n\t\t$this->table = new LLMS_Table_Course_Students();\n\t}\n\n\t/**\n\t * Test the generate_export_file() method.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_export_file() {\n\n\t\t// Create a course.\n\t\t$course = $this->factory->course->create_and_get();\n\t\t$args   = array( 'course_id' => $course->get( 'id' ) );\n\n\t\t// Enroll a bunch of students.\n\t\t$this->factory->student->create_and_enroll_many( 50, $course->get( 'id' ) );\n\n\t\t// Setup an instructor.\n\t\t$instructor_id = $this->factory->instructor->create();\n\t\t$course->instructors()->set_instructors( array( array( 'id' => $instructor_id ) ) );\n\t\twp_set_current_user( $instructor_id );\n\n\t\t// Unboost to make testing faster.\n\t\tadd_filter( 'llms_table_generate_export_file_per_page_boost', function () {\n\t\t\treturn 25;\n\t\t} );\n\n\t\t$file = $this->table->generate_export_file( $args );\n\n\t\t$this->assertTrue( file_exists( LLMS_TMP_DIR . $file['filename'] ) );\n\t\t$this->assertEquals( 50, $file['progress'] );\n\n\t\t$file = $this->table->generate_export_file( $args, $file['filename'] );\n\t\t$this->assertEquals( 100, $file['progress'] );\n\t}\n\n\t/**\n\t * Test generate_export_file(): prevent invalid filetypes.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_export_file_invalid_file_type() {\n\n\t\t$args = array( 'course_id' => $this->factory->course->create() );\n\n\t\t// No.\n\t\t$this->assertFalse( $this->table->generate_export_file( $args, 'f.php' ) );\n\n\t\t// Okay.\n\t\t$this->assertTrue( is_array( $this->table->generate_export_file( $args, 'ok.csv' ) ) );\n\t\t$this->assertTrue( is_array( $this->table->generate_export_file( $args ) ) );\n\t}\n\n\t/**\n\t * Test the get_export() method.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_export() {\n\n\t\t$this->course_id = $this->factory->course->create();\n\t\t$args            = array( 'course_id' => $this->course_id );\n\n\t\t// Enroll a bunch of students.\n\t\t$this->factory->student->create_and_enroll_many( 10, $this->course_id );\n\n\t\t// Setup an admin user\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $admin_id );\n\n\t\t$export = $this->table->get_export( $args );\n\t\t$this->assertTrue( count( $export ) >= 11 );\n\t\t$this->assertEquals( $this->table->get_export_header(), $export[0] );\n\t}\n\n\t/**\n\t * Test the get_results() method.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_results() {\n\n\t\t/** {@see LLMS_Table_Course_Students::get_results()} order by 'name' uses the user's last name. */\n\t\t$this->factory->student->default_generation_definitions['first_name'] = 'Student';\n\t\t$this->factory->student->default_generation_definitions['last_name']  = new WP_UnitTest_Generator_Sequence( '%s' );\n\n\t\t// Test as an administrator.\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $admin_id );\n\n\t\t// Create a course with one lesson.\n\t\t$course                 = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) );\n\t\t$this->course_id        = $course->get( 'id' );\n\t\t$lessons                = $course->get_lessons();\n\t\t$lesson_id              = reset( $lessons )->get( 'id' );\n\t\t$this->table->course_id = $this->course_id;\n\n\t\t$student_completed_dates = array();\n\t\t$student_enrolled_dates  = array();\n\t\t$student_ids             = array();\n\t\t$student_names           = array();\n\t\t$student_statuses        = array();\n\t\tfor ( $i = 0; $i < 10; $i ++ ) {\n\t\t\t// Enroll each student at a different time.\n\t\t\tllms_tests_mock_current_time( '2022-03-' . str_pad( $i + 1, 2, '0', STR_PAD_LEFT ) );\n\t\t\t$student = $this->factory->student->create_and_get();\n\t\t\t$student->enroll( $this->course_id );\n\t\t\t$student_id = $student->get( 'id' );\n\n\t\t\tif ( $student_id % 2 ) {\n\t\t\t\t// Students with odd numbered IDs have completed the course in reverse order.\n\t\t\t\t$student->mark_complete( $lesson_id, 'lesson' );\n\t\t\t\tllms_tests_mock_current_time( \"now - $student_id days\" );\n\t\t\t} else {\n\t\t\t\t// Students with odd numbered IDs have expired enrollment.\n\t\t\t\t$student->unenroll( $this->course_id );\n\t\t\t}\n\n\t\t\t// Gather expected data.\n\t\t\t$student_completed_dates[ $student_id ] = $student->get_completion_date( $this->course_id, 'U' );\n\t\t\t$student_enrolled_dates[ $student_id ]  = $student->get_enrollment_date( $this->course_id, 'updated', 'U' );\n\t\t\t$student_ids[ $student_id ]             = $this->table->get_data( 'id', $student );\n\t\t\t$student_names[ $student_id ]           = $this->table->get_data( 'name', $student );\n\t\t\t$student_statuses[ $student_id ]        = $this->table->get_data( 'status', $student );\n\t\t}\n\t\tasort( $student_completed_dates );\n\t\tasort( $student_statuses );\n\n\t\t// Default arguments.\n\t\t$args = array(\n\t\t\t'course_id' => $this->course_id,\n\t\t\t'filter'    => 'any',\n\t\t\t'filterby'  => 'status',\n\t\t\t'page'      => 1,\n\t\t\t'search'    => '',\n\t\t);\n\n\t\t// Results ordered by ascending completed date.\n\t\t$args['order']   = 'ASC';\n\t\t$args['orderby'] = 'completed';\n\t\t$expected        = $student_completed_dates;\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by descending completed date.\n\t\t$args['order']   = 'DESC';\n\t\t$args['orderby'] = 'completed';\n\t\t$expected        = $student_completed_dates;\n\t\tarsort( $expected );\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by ascending enrolled date.\n\t\t$args['order']   = 'ASC';\n\t\t$args['orderby'] = 'enrolled';\n\t\t$expected        = $student_enrolled_dates;\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by descending enrolled date.\n\t\t$args['order']   = 'DESC';\n\t\t$args['orderby'] = 'enrolled';\n\t\t$expected        = $student_enrolled_dates;\n\t\tarsort( $expected );\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by ascending ID.\n\t\t$args['order']   = 'ASC';\n\t\t$args['orderby'] = 'id';\n\t\t$expected        = $student_ids;\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by descending ID.\n\t\t$args['order']   = 'DESC';\n\t\t$args['orderby'] = 'id';\n\t\t$expected        = $student_ids;\n\t\tarsort( $expected );\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by ascending name.\n\t\t$args['order']   = 'ASC';\n\t\t$args['orderby'] = 'name';\n\t\t$expected        = $student_names;\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by descending name.\n\t\t$args['order']   = 'DESC';\n\t\t$args['orderby'] = 'name';\n\t\t$expected        = $student_names;\n\t\tarsort( $expected );\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by ascending status.\n\t\t$args['order']   = 'ASC';\n\t\t$args['orderby'] = 'status';\n\t\t$expected        = $student_statuses;\n\t\t$this->assert_student_results_equal( $args, $expected );\n\n\t\t// Results ordered by descending status.\n\t\t$args['order']   = 'DESC';\n\t\t$args['orderby'] = 'status';\n\t\t$expected        = $student_statuses;\n\t\tarsort( $expected );\n\t\t$this->assert_student_results_equal( $args, $expected );\n\t}\n\n\t/**\n\t * Test the set_args() method.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_args() {\n\n\t\t$_GET['course_id'] = 123;\n\t\t$this->assertEquals( array( 'course_id' => 123 ), $this->table->set_args() );\n\t}\n\n\t/**\n\t * Test the set_columns() method.\n\t *\n\t * @since 5.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_columns() {\n\n\t\t$cols = $this->table->set_columns();\n\t\t$this->assertTrue( is_array( $cols ) );\n\t\t$this->assertEquals( 11, count( $cols ) );\n\t\t$this->assertEquals( array(\n\t\t\t'id',\n\t\t\t'name',\n\t\t\t'name_last',\n\t\t\t'name_first',\n\t\t\t'email',\n\t\t\t'status',\n\t\t\t'enrolled',\n\t\t\t'completed',\n\t\t\t'progress',\n\t\t\t'grade',\n\t\t\t'last_lesson',\n\t\t), array_keys( $cols ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/tables/class-llms-test-table-quizzes.php",
    "content": "<?php\n/**\n * Test the quizzes reporting table.\n *\n * @package LifterLMS/Tests/Tables\n *\n * @group reporting_tables\n *\n * @since 3.36.1\n */\nclass LLMS_Test_Table_Quizzes extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup test.\n\t *\n\t * @since 3.36.1\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/tables/llms.table.quizzes.php';\n\t\t$this->table = new LLMS_Table_Quizzes();\n\n\t}\n\n\t/**\n\t * test quizzes table is empty for instructors with no courses or courses with no lessons\n\t *\n\t * @since 3.36.1\n\t *\n\t * @return void\n\t */\n\tpublic function test_no_quizzes_for_instructor_with_no_course_lesson() {\n\n\t\t// Setup a course with lessons and quizzes.\n\t\t$course = $this->factory->course->create_and_get( array(\n\t\t\t'sections' => 1,\n\t\t\t'lessons' => 3,\n\t\t\t'quizzes' => 2,\n\t\t) );\n\n\t\t// Setup an instructor.\n\t\t$instructor_id = $this->factory->instructor->create();\n\n\t\twp_set_current_user( $instructor_id );\n\n\t\t// The instructor has no courses, we expect no data.\n\t\t$table = new LLMS_Table_Quizzes();\n\t\t$table->get_results();\n\t\t$this->assertEquals( 0, count( $table->get_tbody_data() ) );\n\n\t\t// Setup a course with no lessons and assign the instructor to the course.\n\t\t$inst_course = $this->factory->course->create_and_get( array(\n\t\t\t'sections' => 1,\n\t\t\t'lessons'  => 0,\n\t\t) );\n\t\t$inst_course->instructors()->set_instructors(array(\n\t\t\tarray(\n\t\t\t\t'id' => $instructor_id,\n\t\t\t),\n\t\t));\n\n\t\t// The instructor has a course, but the course has no lessons, we expect no data.\n\t\t$table = new LLMS_Table_Quizzes();\n\t\t$table->get_results();\n\t\t$this->assertEquals( 0, count( $table->get_tbody_data() ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/tables/class-llms-test-table-students.php",
    "content": "<?php\n/**\n * Test the students reporting table.\n *\n * @package LifterLMS/Tests/Tables\n *\n * @group reporting_tables\n *\n * @since 3.28.0\n */\nclass LLMS_Test_Table_Students extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup test\n\t *\n\t * @since 3.28.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/reporting/tables/llms.table.students.php';\n\t\t$this->table = new LLMS_Table_Students();\n\n\t}\n\n\n\t/**\n\t * test the get_export() method.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_export() {\n\n\t\t// Enroll a bunch of students.\n\t\t$this->factory->student->create_and_enroll_many( 10, $this->factory->course->create() );\n\n\t\t// Setup an admin user\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $admin_id );\n\n\t\t$table = new LLMS_Table_Students();\n\t\t$export = $table->get_export();\n\t\t$this->assertTrue( count( $export ) >= 11 );\n\t\t$this->assertEquals( $table->get_export_header(), $export[0] );\n\n\t}\n\n\t/**\n\t * test the generate_export_file() method.\n\t *\n\t * @return void\n\t * @since   3.28.0\n\t * @version 3.28.1\n\t */\n\tpublic function test_generate_export_file() {\n\n\t\t// Create a course.\n\t\t$course = $this->factory->course->create_and_get();\n\n\t\t// Enroll a bunch of students.\n\t\t$this->factory->student->create_and_enroll_many( 50, $course->get( 'id' ) );\n\n\t\t// Setup an instructor.\n\t\t$instructor_id = $this->factory->instructor->create();\n\t\t$course->instructors()->set_instructors( array( array( 'id' => $instructor_id ) ) );\n\t\twp_set_current_user( $instructor_id );\n\n\t\t// unboost to make testing faster.\n\t\tadd_filter( 'llms_table_generate_export_file_per_page_boost', function() {\n\t\t\treturn 25;\n\t\t} );\n\n\t\t$table = new LLMS_Table_Students();\n\t\t$file = $table->generate_export_file();\n\n\t\t$this->assertTrue( file_exists( LLMS_TMP_DIR . $file['filename'] ) );\n\t\t$this->assertEquals( 50, $file['progress'] );\n\n\t\t$file = $table->generate_export_file( array(), $file['filename'] );\n\t\t$this->assertEquals( 100, $file['progress'] );\n\n\t}\n\n\t/**\n\t * Test generate_export_file(): prevent invalid filetypes.\n\t *\n\t * @since 3.37.15\n\t *\n\t * @return void\n\t */\n\tpublic function test_generate_export_file_invalid_file_type() {\n\n\t\t$table = new LLMS_Table_Students();\n\n\t\t// No.\n\t\t$this->assertFalse( $table->generate_export_file( array(), 'f.php' ) );\n\n\t\t// Okay.\n\t\t$this->assertTrue( is_array( $table->generate_export_file( array(), 'ok.csv' ) ) );\n\t\t$this->assertTrue( is_array( $table->generate_export_file( ) ) );\n\n\t}\n\n\t/**\n\t * test the get_results() method.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_results() {\n\n\t\t$checks = array(\n\t\t\tarray(\n\t\t\t\t'key' => 'page',\n\t\t\t\t'func' => 'get_current_page',\n\t\t\t\t'default' => 1,\n\t\t\t\t'change' => 2,\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'key' => 'order',\n\t\t\t\t'func' => 'get_order',\n\t\t\t\t'default' => 'ASC',\n\t\t\t\t'change' => 'DESC',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'key' => 'orderby',\n\t\t\t\t'func' => 'get_orderby',\n\t\t\t\t'default' => 'name',\n\t\t\t\t'change' => 'id',\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'key' => 'per_page',\n\t\t\t\t'func' => 'get_per_page',\n\t\t\t\t'default' => 25,\n\t\t\t\t'change' => 5,\n\t\t\t),\n\t\t);\n\n\t\t$result_args = wp_list_pluck( $checks, 'change', 'key' );\n\n\t\t// Setup course.\n\t\t$course = $this->factory->course->create_and_get();\n\n\t\t// Enroll a bunch of students.\n\t\t$this->factory->student->create_and_enroll_many( 10, $course->get( 'id' ) );\n\n\t\t// Current user has no access to anything.\n\t\t$table = new LLMS_Table_Students();\n\t\t$table->get_results();\n\t\t$this->assertEmpty( $table->get_tbody_data() );\n\t\tforeach ( $checks as $data ) {\n\t\t\t$this->assertEquals( $data['default'], $table->{ $data['func'] }() );\n\t\t}\n\t\t$table->get_results( $result_args );\n\t\tforeach ( $checks as $data ) {\n\t\t\t$this->assertEquals( $data['default'], $table->{ $data['func'] }() );\n\t\t}\n\n\t\t// Setup an instructor.\n\t\t$instructor_id = $this->factory->instructor->create();\n\t\t$course->instructors()->set_instructors( array( array( 'id' => $instructor_id ) ) );\n\n\t\twp_set_current_user( $instructor_id );\n\t\t$table = new LLMS_Table_Students();\n\t\t$table->get_results();\n\t\t$this->assertEquals( 10, count( $table->get_tbody_data() ) );\n\t\tforeach ( $checks as $data ) {\n\t\t\t$this->assertEquals( $data['default'], $table->{ $data['func'] }() );\n\t\t}\n\t\t$table->get_results( $result_args );\n\t\tforeach ( $checks as $data ) {\n\t\t\t$this->assertEquals( $data['change'], $table->{ $data['func'] }() );\n\t\t}\n\t\t$this->assertEquals( 2, $table->get_max_pages() );\n\t\t$this->assertTrue( $table->is_last_page() );\n\n\t\t$admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );\n\t\twp_set_current_user( $admin_id );\n\t\t$table = new LLMS_Table_Students();\n\t\t$table->get_results();\n\t\t$this->assertTrue( count( $table->get_tbody_data() ) >= 10 );\n\t\tforeach ( $checks as $data ) {\n\t\t\t$this->assertEquals( $data['default'], $table->{ $data['func'] }() );\n\t\t}\n\t\t$table->get_results( $result_args );\n\t\tforeach ( $checks as $data ) {\n\t\t\t$this->assertEquals( $data['change'], $table->{ $data['func'] }() );\n\t\t}\n\t\t$this->assertTrue( $table->get_max_pages() >= 2 );\n\n\t}\n\n\t/**\n\t * Test the set_args() method.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_args() {\n\n\t\t$this->assertEquals( array( 'per_page' => 25 ), $this->table->set_args() );\n\n\t}\n\n\t/**\n\t * Test the set_columns() method\n\t *\n\t * @since 3.28.0\n\t * @since 3.36.0 Add \"last_seen\" col.\n\t *\n\t * @return void\n\t */\n\tpublic function test_set_columns() {\n\n\t\t$cols = $this->table->set_columns();\n\t\t$this->assertTrue( is_array( $cols ) );\n\t\t$this->assertEquals( 27, count( $cols ) );\n\t\t$this->assertEquals( array (\n\t\t\t'id',\n\t\t\t'email',\n\t\t\t'name',\n\t\t\t'name_last',\n\t\t\t'name_first',\n\t\t\t'registered',\n\t\t\t'last_seen',\n\t\t\t'overall_progress',\n\t\t\t'overall_grade',\n\t\t\t'enrollments',\n\t\t\t'completions',\n\t\t\t'certificates',\n\t\t\t'achievements',\n\t\t\t'memberships',\n\t\t\t'billing_address_1',\n\t\t\t'billing_address_2',\n\t\t\t'billing_city',\n\t\t\t'billing_state',\n\t\t\t'billing_zip',\n\t\t\t'billing_country',\n\t\t\t'phone',\n\t\t\t'courses_enrolled',\n\t\t\t'courses_cancelled',\n\t\t\t'courses_expired',\n\t\t\t'memberships_enrolled',\n\t\t\t'memberships_cancelled',\n\t\t\t'memberships_expired',\n\t\t), array_keys( $cols ) );\n\n\t}\n\n\t/**\n\t * Test that variables are setup correctly during construction.\n\t *\n\t * @since 3.28.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_variables() {\n\n\t\t$this->assertEquals( 'Students', $this->table->get_title() );\n\t\t$this->table->set( 'title', 'Something Else' );\n\t\t$this->assertEquals( 'Something Else', $this->table->get_title() );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/theme-support/class-llms-test-theme-support.php",
    "content": "<?php\n/**\n * Test LLMS_Theme_Support\n *\n * @package LifterLMS/Tests\n *\n * @group theme_support\n *\n * @since 3.37.0\n * @since 4.10.0 Added tests for Twenty Twenty-One theme.\n * @since 5.9.0 Added tests for Twenty Twenty-Two\n */\nclass LLMS_Test_Theme_Support extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Array of supported themes\n\t *\n\t * template => support class name.\n\t *\n\t * @var array\n\t */\n\tprotected $supported = array(\n\t\t'twentynineteen'  => 'LLMS_Twenty_Nineteen',\n\t\t'twentytwenty'    => 'LLMS_Twenty_Twenty',\n\t\t'twentytwentyone' => 'LLMS_Twenty_Twenty_One',\n\t\t'twentytwentytwo' => 'LLMS_Twenty_Twenty_Two',\n\t);\n\n\t/**\n\t * Test get_css()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_css() {\n\t\t$this->assertEquals( 'body, .el { background: red; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red' ) ) );\n\t\t$this->assertEquals( 'body, .el { background: red; color: black; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red', 'color' => 'black' ) ) );\n\t}\n\n\n\t/**\n\t * Test get_css() with a prefix\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_css_with_prefix() {\n\t\t$this->assertEquals( '#prefix body, #prefix .el { background: red; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red' ), '#prefix' ) );\n\t\t$this->assertEquals( '#prefix body, #prefix .el { background: red; color: black; }', LLMS_Theme_Support::get_css( array( 'body', '.el' ), array( 'background' => 'red', 'color' => 'black' ), '#prefix' ) );\n\t}\n\n\t/**\n\t * Test get_css() when passing in an array for a rule\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_css_with_array_of_rules() {\n\n\t\t$css = array(\n\t\t\t'background-image' => array(\n\t\t\t\t'-webkit-radial-gradient(fake)',\n\t\t\t\t'radial-gradient(fake)',\n\t\t\t)\n\t\t);\n\n\t\t$expected = '#prefix body, #prefix .el { background-image: -webkit-radial-gradient(fake); background-image: radial-gradient(fake); }';\n\n\t\t$this->assertEquals( $expected, LLMS_Theme_Support::get_css( array( 'body', '.el' ), $css, '#prefix' ) );\n\t}\n\n\t/**\n\t * Test get_selectors_primary_color_background()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_selectors_primary_color_background() {\n\t\t$res = LLMS_Theme_Support::get_selectors_primary_color_background();\n\t\t$this->assertTrue( is_array( $res ) );\n\t\tforeach ( $res as $sel ) {\n\t\t\t$this->assertTrue( is_string( $sel ) );\n\t\t}\n\t}\n\n\t/**\n\t * Test get_selectors_primary_color_border()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_selectors_primary_color_border() {\n\t\t$res = LLMS_Theme_Support::get_selectors_primary_color_border();\n\t\t$this->assertTrue( is_array( $res ) );\n\t\tforeach ( $res as $sel ) {\n\t\t\t$this->assertTrue( is_string( $sel ) );\n\t\t}\n\t}\n\n\t/**\n\t * Test get_selectors_primary_color_text()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_selectors_primary_color_text() {\n\t\t$res = LLMS_Theme_Support::get_selectors_primary_color_text();\n\t\t$this->assertTrue( is_array( $res ) );\n\t\tforeach ( $res as $sel ) {\n\t\t\t$this->assertTrue( is_string( $sel ) );\n\t\t}\n\t}\n\n\n\t/**\n\t * Test theme support classes are loaded based on the current theme template.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_includes_no_support() {\n\n\t\tupdate_option( 'template', 'default' );\n\n\t\tforeach ( array_values( $this->supported ) as $class ) {\n\t\t\t$this->assertTrue( ! class_exists( $class ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test theme support classes are loaded based on the current theme template.\n\t *\n\t * @since 3.37.0\n\t * @since 4.3.0 Update theme support class instantiation.\n\t *\n\t * @return void\n\t */\n\tpublic function test_includes_with_support() {\n\n\t\tforeach ( $this->supported as $template => $class ) {\n\t\t\tupdate_option( 'template', $template );\n\t\t\t$support = new LLMS_Theme_Support();\n\t\t\t$support->includes();\n\t\t\t$this->assertTrue( class_exists( $class ) );\n\t\t}\n\n\t\tupdate_option( 'template', 'default' );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-one.php",
    "content": "<?php\n/**\n * Test LLMS_Twenty_Twenty theme support class\n *\n * @package LifterLMS/Tests\n *\n * @group theme_support\n *\n * @since 4.10.0\n */\nclass LLMS_Test_Twenty_Twenty_One extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 4.10.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\tupdate_option( 'template', 'twentytwentyone' );\n\t\t$support = new LLMS_Theme_Support();\n\t\t$support->includes();\n\n\t}\n\n\t/**\n\t * Tear down the test case.\n\t *\n\t * @since 4.10.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tupdate_option( 'template', 'default' );\n\n\t}\n\n\t/**\n\t * Remove all the header actions setup by `handle_page_header_wrappers()`.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tprotected function remove_header_actions() {\n\n\t\tremove_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ), 11 );\n\t\tremove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ), 99999999 );\n\t\tremove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ), -1 );\n\t\tremove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ), 99999998 );\n\n\t}\n\n\t/**\n\t * Test add_max_width_class()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_max_width_class() {\n\t\t$this->assertEquals( array( 'mock-class', 'default-max-width' ), LLMS_Twenty_Twenty_One::add_max_width_class( array( 'mock-class' ) )  );\n\t}\n\n\t/**\n\t * Test add_pagination_classes()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_pagination_classes() {\n\t\t$this->assertEquals( array( 'mock-class', 'navigation', 'pagination' ), LLMS_Twenty_Twenty_One::add_pagination_classes( array( 'mock-class' ) )  );\n\t}\n\n\t/**\n\t * Test summary\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_certificate_title_not_a_cert() {\n\n\t\tglobal $post;\n\n\t\t$types = array(\n\t\t\t'post' => array( 10, 'Untitled' ),\n\t\t\t'llms_certificate' => array( false, '' ),\n\t\t\t'llms_my_certificate' => array( false, '' ),\n\t\t);\n\n\t\t// Mock the filter callback function.\n\t\tfunction twenty_twenty_one_post_title( $title ) {\n\t\t\treturn 'Untitled';\n\t\t}\n\n\t\tforeach ( $types as $post_type => $data ) {\n\n\t\t\tadd_filter( 'the_title', 'twenty_twenty_one_post_title' );\n\n\t\t\tlist( $expect_priority, $expected_title ) = $data;\n\n\t\t\t$post = $this->factory->post->create_and_get( array( 'post_type' => $post_type, 'post_title' => '' ) );\n\n\t\t\tLLMS_Twenty_Twenty_One::handle_certificate_title();\n\n\t\t\t$this->assertEquals( $expect_priority, has_filter( 'the_title', 'twenty_twenty_one_post_title' ) );\n\t\t\t$this->assertEquals( $expected_title, get_the_title() );\n\n\t\t}\n\n\t\t$post = null;\n\n\t}\n\n\t/**\n\t * Test handle_page_header_wrappers() when the archive title is disabled.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_page_header_wrappers_no_title() {\n\n\t\t$this->remove_header_actions();\n\t\tadd_filter( 'lifterlms_show_page_title', '__return_false' );\n\n\t\tLLMS_Twenty_Twenty_One::handle_page_header_wrappers();\n\n\t\t$this->assertFalse( has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ) ) );\n\n\t\tremove_filter( 'lifterlms_show_page_title', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test handle_page_header_wrappers() when there's no archive description\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_page_header_wrappers_no_desc() {\n\n\t\t$this->remove_header_actions();\n\n\t\tLLMS_Twenty_Twenty_One::handle_page_header_wrappers();\n\n\t\t$this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ) ) );\n\t\t$this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ) ) );\n\n\t\t$this->remove_header_actions();\n\n\t}\n\n\t/**\n\t * Test handle_page_header_wrappers() when there is an archived description\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_page_header_wrappers_title_and_desc() {\n\n\t\t$this->remove_header_actions();\n\n\t\t// Output a description.\n\t\t$handler = function( $desc ) {\n\t\t\treturn 'Archive description';\n\t\t};\n\t\tadd_filter( 'llms_archive_description', $handler );\n\n\t\tLLMS_Twenty_Twenty_One::handle_page_header_wrappers();\n\n\t\t$this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap' ) ) );\n\t\t$this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'page_header_wrap_end' ) ) );\n\t\t$this->assertEquals( -1, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper' ) ) );\n\t\t$this->assertEquals( 99999998, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_One', 'output_archive_description_wrapper_end' ) ) );\n\n\t\t$this->remove_header_actions();\n\n\t\tremove_filter( 'llms_archive_description', $handler );\n\n\t}\n\n\t/**\n\t * Test modify_columns_count()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_columns_count() {\n\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 1 ) );\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 2 ) );\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 3 ) );\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_One::modify_columns_count( 999 ) );\n\n\t}\n\n\t/**\n\t * Test maybe_disable_post_navigation()\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_maybe_disable_post_navigation() {\n\n\t\tglobal $post;\n\t\t$temp = $post;\n\n\t\t$tests = array(\n\t\t\t'post'            => 'default html',\n\t\t\t'course'          => '',\n\t\t\t'llms_membership' => '',\n\t\t\t'lesson'          => '',\n\t\t\t'llms_quiz'       => '',\n\t\t);\n\n\t\tforeach ( $tests as $post_type => $expected ) {\n\n\t\t\t$post = $this->factory->post->create( compact( 'post_type' ) );\n\t\t\t$this->assertEquals( $expected, LLMS_Twenty_Twenty_One::maybe_disable_post_navigation( 'default html' ) );\n\n\t\t}\n\n\t\t$post = $temp;\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty-two.php",
    "content": "<?php\n/**\n * Test LLMS_Twenty_Twenty_Two theme support class\n *\n * @package LifterLMS/Tests\n *\n * @group theme_support\n * @group twenty_twenty_two\n *\n * @since 5.8.0\n */\nclass LLMS_Test_Twenty_Twenty_Two extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\tupdate_option( 'template', 'twentytwentytwo' );\n\t\t$support = new LLMS_Theme_Support();\n\t\t$support->includes();\n\n\t}\n\n\t/**\n\t * Tear down the test case.\n\t *\n\t * @since 5.8.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tupdate_option( 'template', 'default' );\n\n\t}\n\n\t/**\n\t * Remove all the header actions setup by `handle_page_header_wrappers()`.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tprotected function remove_header_actions() {\n\n\t\tremove_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ), 11 );\n\t\tremove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ), 99999999 );\n\t\tremove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ), -1 );\n\t\tremove_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ), 99999998 );\n\n\t}\n\n\t/**\n\t * Test handle_page_header_wrappers() when the archive title is disabled.\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_page_header_wrappers_no_title() {\n\n\t\t$this->remove_header_actions();\n\t\tadd_filter( 'lifterlms_show_page_title', '__return_false' );\n\n\t\tLLMS_Twenty_Twenty_Two::handle_page_header_wrappers();\n\n\t\t$this->assertFalse( has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ) ) );\n\n\t\tremove_filter( 'lifterlms_show_page_title', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test handle_page_header_wrappers() when there's no archive description\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_page_header_wrappers_no_desc() {\n\n\t\t$this->remove_header_actions();\n\n\t\tLLMS_Twenty_Twenty_Two::handle_page_header_wrappers();\n\n\t\t$this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ) ) );\n\t\t$this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ) ) );\n\t\t$this->assertFalse( has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ) ) );\n\n\t\t$this->remove_header_actions();\n\n\t}\n\n\t/**\n\t * Test handle_page_header_wrappers() when there is an archived description\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_handle_page_header_wrappers_title_and_desc() {\n\n\t\t$this->remove_header_actions();\n\n\t\t// Output a description.\n\t\t$handler = function( $desc ) {\n\t\t\treturn 'Archive description';\n\t\t};\n\t\tadd_filter( 'llms_archive_description', $handler );\n\n\t\tLLMS_Twenty_Twenty_Two::handle_page_header_wrappers();\n\n\t\t$this->assertEquals( 11, has_action( 'lifterlms_before_main_content', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap' ) ) );\n\t\t$this->assertEquals( 99999999, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'page_header_wrap_end' ) ) );\n\t\t$this->assertEquals( -1, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper' ) ) );\n\t\t$this->assertEquals( 99999998, has_action( 'lifterlms_archive_description', array( 'LLMS_Twenty_Twenty_Two', 'output_archive_description_wrapper_end' ) ) );\n\n\t\t$this->remove_header_actions();\n\n\t\tremove_filter( 'llms_archive_description', $handler );\n\n\t}\n\n\t/**\n\t * Test modify_columns_count()\n\t *\n\t * @since 5.8.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_modify_columns_count() {\n\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 1 ) );\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 2 ) );\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 3 ) );\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty_Two::modify_columns_count( 999 ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/theme-support/class-llms-test-twenty-twenty.php",
    "content": "<?php\n/**\n * Test LLMS_Twenty_Twenty theme support class\n *\n * @package LifterLMS/Tests\n *\n * @group theme_support\n *\n * @since 3.37.0\n */\nclass LLMS_Test_Twenty_Twenty extends LLMS_Unit_Test_Case {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 3.37.0\n\t * @since 4.3.0 Update theme support class instantiation.\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\tupdate_option( 'template', 'twentytwenty' );\n\t\t$support = new LLMS_Theme_Support();\n\t\t$support->includes();\n\n\t}\n\n\t/**\n\t * Tear down the test case.\n\t *\n\t * @since 3.37.0\n\t * @since 5.3.3 Renamed from `tearDown()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function tear_down() {\n\n\t\tparent::tear_down();\n\t\tupdate_option( 'template', 'default' );\n\n\t}\n\n\t/**\n\t * Test the hide_meta_output() method.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_hide_meta_output() {\n\n\t\t$this->assertEquals( array( 'course', 'llms_membership', 'lesson', 'llms_quiz' ), LLMS_Twenty_Twenty::hide_meta_output( array() ) );\n\t\t$this->assertEquals( array( 'existing', 'course', 'llms_membership', 'lesson', 'llms_quiz' ), LLMS_Twenty_Twenty::hide_meta_output( array( 'existing' ) ) );\n\n\t}\n\n\t/**\n\t * Test is_page_full_width() method.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_page_full_width() {\n\n\t\t// Not Set.\n\t\t$page_id = $this->factory->post->create( array( 'post_type' => 'page' ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( 'LLMS_Twenty_Twenty', 'is_page_full_width', array( $page_id ) ) );\n\n\t\t// Not full width.\n\t\tupdate_post_meta( $page_id, '_wp_page_template', 'default' );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( 'LLMS_Twenty_Twenty', 'is_page_full_width', array( $page_id ) ) );\n\n\t\t// Is full width.\n\t\tupdate_post_meta( $page_id, '_wp_page_template', 'templates/template-full-width.php' );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( 'LLMS_Twenty_Twenty', 'is_page_full_width', array( $page_id ) ) );\n\n\t}\n\n\t/**\n\t * Default values for column counts will return 1 (default \"thin\" template)\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_modify_columns_count_defaults() {\n\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty::modify_columns_count( 1 ) );\n\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty::modify_columns_count( 2 ) );\n\n\t}\n\n\t/**\n\t * Modify columns on catalogs. Returns 1 for default template and default column values for full width templates.\n\t *\n\t * @since 3.37.0\n\t *\n\t * @return [type]\n\t */\n\tpublic function test_modify_columns_count() {\n\n\t\tLLMS_Install::create_pages();\n\t\tLLMS_Install::create_visibilities();\n\n\t\tforeach ( array( 'courses', 'memberships', 'checkout' ) as $page ) {\n\n\t\t\t$page_id = llms_get_page_id( $page );\n\t\t\t$url = get_permalink( $page_id );\n\n\t\t\t$this->go_to( $url );\n\t\t\t$this->assertEquals( 1, LLMS_Twenty_Twenty::modify_columns_count( 2 ) );\n\n\t\t\tupdate_post_meta( $page_id, '_wp_page_template', 'templates/template-full-width.php' );\n\t\t\t$this->assertEquals( 2, LLMS_Twenty_Twenty::modify_columns_count( 2 ) );\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/traits/llms-test-trait-audio-video-embed.php",
    "content": "<?php\n\n/**\n * Tests for {@see LLMS_Trait_Audio_Video_Embed}.\n *\n * @group Traits\n * @group LLMS_Post_Model\n *\n * @since 5.3.0\n */\nclass LLMS_Test_Audio_Video_Embed_Trait extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var LLMS_Trait_Audio_Video_Embed\n\t */\n\tprotected $mock;\n\n\t/**\n\t * Setup before running each test in this class.\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$args       = array(\n\t\t\t'post_title' => 'Mock Post with the Audio Video Embed Trait',\n\t\t);\n\t\t$this->mock = new class( 'new', $args ) extends LLMS_Post_Model {\n\n\t\t\tuse LLMS_Trait_Audio_Video_Embed;\n\n\t\t\tprotected $db_post_type = 'course'; # Limited to 20 characters.\n\t\t\tprotected $model_post_type = 'course'; # Limited to 20 characters.\n\n\t\t\tpublic function __construct( $model, $args = array() ) {\n\n\t\t\t\t$this->construct_audio_video_embed();\n\t\t\t\tparent::__construct( $model, $args );\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Test the {@see LLMS_Trait_Audio_Video_Embed::get_audio()} method.\n\t *\n\t * @since 5.3.0\n\t */\n\tpublic function test_get_audio() {\n\n\t\t$url = 'http://example.tld/audio_embed';\n\t\t$this->mock->set( 'audio_embed', $url );\n\t\t$expected = do_shortcode( sprintf( '[%1$s src=\"%2$s\"]', 'audio', $url ) );\n\t\t$actual   = $this->mock->get_audio();\n\t\t$this->assertEquals( $expected, $actual );\n\t}\n\n\t/**\n\t * Test the {@see LLMS_Trait_Audio_Video_Embed::get_embed()} method.\n\t *\n\t * @since 5.3.0\n\t * @throws ReflectionException\n\t */\n\tpublic function test_get_embed() {\n\n\t\t# Setup this test.\n\t\t$audio_url = 'http://example.tld/audio_embed';\n\t\t$this->mock->set( 'audio_embed', $audio_url );\n\t\t$video_url = 'http://example.tld/video_embed';\n\t\t$this->mock->set( 'video_embed', $video_url );\n\t\t$expected_audio = wp_audio_shortcode( array( 'src' => $audio_url ) );\n\t\t$expected_video = wp_video_shortcode( array( 'src' => $video_url ) );\n\n\t\t# Test all optional arguments.\n\t\t$actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed' );\n\t\t$this->assertEquals( $expected_video, $actual );\n\n\t\t# Test optional $prop argument.\n\t\t$actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array( 'type' => 'video' ) );\n\t\t$this->assertEquals( $expected_video, $actual );\n\t\t$actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array( 'type' => 'audio' ) );\n\t\t$this->assertEquals( $expected_audio, $actual );\n\n\t\t# Test with all arguments.\n\t\t$actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array(\n\t\t\t'type' => 'audio',\n\t\t\t'prop' => 'audio_embed',\n\t\t) );\n\t\t$this->assertEquals( $expected_audio, $actual );\n\t\t$actual = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_embed', array(\n\t\t\t'type' => 'video',\n\t\t\t'prop' => 'video_embed',\n\t\t) );\n\t\t$this->assertEquals( $expected_video, $actual );\n\t}\n\n\t/**\n\t * Test the {@see LLMS_Trait_Audio_Video_Embed::get_video()} method.\n\t *\n\t * @since 5.3.0\n\t */\n\tpublic function test_get_video() {\n\n\t\t$url = 'http://example.tld/video_embed';\n\t\t$this->mock->set( 'video_embed', $url );\n\t\t$expected = do_shortcode( sprintf( '[%1$s src=\"%2$s\"]', 'video', $url ) );\n\t\t$actual   = $this->mock->get_video();\n\t\t$this->assertEquals( $expected, $actual );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/traits/llms-test-trait-award-default-images.php",
    "content": "<?php\n/**\n * Tests for {@see LLMS_Trait_Award_Default_Images}.\n *\n * @group traits\n * @group awards\n * @group awards_default_images\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Trait_Award_Default_Images extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->instances = array(\n\t\t\t'achievement' => llms()->achievements(),\n\t\t\t'certificate' => llms()->certificates(),\n\t\t);\n\n\t}\n\n\t/**\n\t * Test get_default_default_image_src().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_default_default_image_src() {\n\n\t\t$certificate_version_1_id = $this->factory->post->create( array(\n\t\t\t'post_type'    => 'llms_certificate',\n\t\t\t'post_content' => 'Version 1 Certificate.',\n\t\t) );\n\t\t$certificate_version_2_id = $this->factory->post->create( array(\n\t\t\t'post_type'    => 'llms_certificate',\n\t\t\t'post_content' => '<!-- wp:llms/certificate-title {\"placeholder\":\"Version 2 Certificate\"} -->',\n\t\t) );\n\n\t\tforeach ( $this->instances as $type => $instance ) {\n\n\t\t\t$this->assertStringContainsString(\n\t\t\t\t\"default-{$type}.png\",\n\t\t\t\tLLMS_Unit_Test_Util::call_method( $instance, 'get_default_default_image_src' )\n\t\t\t);\n\n\t\t\tadd_filter( 'llms_use_legacy_award_images', '__return_true' );\n\t\t\t$this->assertStringContainsString(\n\t\t\t\t\"optional_{$type}.png\",\n\t\t\t\tLLMS_Unit_Test_Util::call_method( $instance, 'get_default_default_image_src' )\n\t\t\t);\n\t\t\tremove_filter( 'llms_use_legacy_award_images', '__return_true' );\n\n\t\t\tupdate_option( \"llms_has_{$type}s_with_legacy_default_image\", 'yes', 'no' );\n\t\t\tswitch ( $type ) {\n\t\t\t\tcase 'achievement':\n\t\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t\t\"optional_{$type}.png\",\n\t\t\t\t\t\tLLMS_Unit_Test_Util::call_method( $instance, 'get_default_default_image_src' )\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'certificate':\n\t\t\t\t\t$previous_post = $GLOBALS['post'] ?? null;\n\n\t\t\t\t\t$GLOBALS['post'] = $certificate_version_1_id;\n\t\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t\t\"optional_{$type}.png\",\n\t\t\t\t\t\tLLMS_Unit_Test_Util::call_method( $instance, 'get_default_default_image_src' )\n\t\t\t\t\t);\n\n\t\t\t\t\t$GLOBALS['post'] = $certificate_version_2_id;\n\t\t\t\t\t$this->assertStringContainsString(\n\t\t\t\t\t\t\"default-{$type}.png\",\n\t\t\t\t\t\tLLMS_Unit_Test_Util::call_method( $instance, 'get_default_default_image_src' )\n\t\t\t\t\t);\n\n\t\t\t\t\t$GLOBALS['post'] = $previous_post;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdelete_option( \"llms_has_{$type}s_with_legacy_default_image\" );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_default_image() and get_default_image_id()\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_default_image() {\n\n\t\tforeach ( $this->instances as $id => $instance ) {\n\n\t\t\t$opt_name = \"lifterlms_{$id}_default_img\";\n\n\t\t\t// Non-existent option.\n\t\t\tdelete_option( $opt_name );\n\t\t\t$this->assertEquals( 0, $instance->get_default_image_id() );\n\t\t\t$this->assertStringContainsString( \"/default-{$id}.png\", $instance->get_default_image( 123 ) );\n\n\t\t\t// Empty option\n\t\t\tupdate_option( $opt_name, '' );\n\t\t\t$this->assertEquals( 0, $instance->get_default_image_id() );\n\t\t\t$this->assertStringContainsString( \"/default-{$id}.png\", $instance->get_default_image( 123 ) );\n\n\t\t\t// Non-existent attachment.\n\t\t\tupdate_option( $opt_name, 123 );\n\t\t\t$this->assertEquals( 0, $instance->get_default_image_id() );\n\t\t\t$this->assertStringContainsString( \"/default-{$id}.png\", $instance->get_default_image( 123 ) );\n\n\t\t\t// A \"real\" attachment.\n\t\t\t$attachment_id = $this->create_attachment( 'christian-fregnan-unsplash.jpg' );\n\t\t\tupdate_option( $opt_name, $attachment_id );\n\t\t\t$this->assertEquals( $attachment_id, $instance->get_default_image_id() );\n\t\t\t$this->assertMatchesRegularExpression(\n\t\t\t\t'#http:\\/\\/example.org\\/wp-content\\/uploads\\/\\d{4}\\/\\d{2}\\/christian-fregnan-unsplash(-)?\\d*.jpg#',\n\t\t\t\t$instance->get_default_image( $attachment_id )\n\t\t\t);\n\n\t\t}\n\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/traits/llms-test-trait-award-templates-post-list-table.php",
    "content": "<?php\n\n/**\n * Tests for LLMS_Trait_Award_Templates_Post_List_Table\n *\n * @group Traits\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Trait_Award_Templates_Post_List_Table extends LLMS_UnitTestCase {\n\n\tprotected $tables;\n\n\t/**\n\t * Setup before running each test in this class.\n\t *\n\t * @since 6.0.0\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\t\t$this->tables = array(\n\t\t\t'achievement' => class_exists( 'LLMS_Admin_Post_Table_Achievements' ) ?\n\t\t\t\tnew LLMS_Admin_Post_Table_Achievements()\n\t\t\t\t:\n\t\t\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/post-tables/class-llms-admin-post-table-achievements.php',\n\t\t\t'certificate' => class_exists( 'LLMS_Admin_Post_Table_Certificates' ) ?\n\t\t\t\tnew LLMS_Admin_Post_Table_Certificates()\n\t\t\t\t:\n\t\t\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/post-tables/class-llms-admin-post-table-certificates.php',\n\t\t);\n\n\t\trequire_once LLMS_PLUGIN_DIR . 'includes/admin/post-types/post-tables/class-llms-admin-post-table-awards.php';\n\n\t}\n\n\t/**\n\t * Test the add_post_actions() method.\n\t *\n\t * @since 6.0.0\n\t * @since 7.1.0 Log in as administrator so that the certificates have a post edit link set.\n\t *\n\t * @return void\n\t */\n\tpublic function test_add_post_actions() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\tforeach ( $this->tables as $pt => $table ) {\n\t\t\t// No post passed, no actions added.\n\t\t\t$this->assertEquals(\n\t\t\t\tarray(),\n\t\t\t\t$table->add_post_actions( array(), null ),\n\t\t\t\t$pt\n\t\t\t);\n\n\t\t\t// Create a post.\n\t\t\t$post = $this->factory->post->create_and_get();\n\n\t\t\t// No actions added.\n\t\t\t$this->assertEquals(\n\t\t\t\tarray(),\n\t\t\t\t$table->add_post_actions( array(), $post ),\n\t\t\t\t$pt\n\t\t\t);\n\n\t\t\t// Create a valid post type.\n\t\t\t$post = $this->factory->post->create_and_get(\n\t\t\t\tarray(\n\t\t\t\t\t'post_type' => \"llms_{$pt}\",\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t// Force the post type to not show the ui.\n\t\t\tget_post_type_object( \"llms_{$pt}\" )->show_ui = false;\n\t\t\t// Actions not added because by default the post type `show_ui` is not true.\n\t\t\t$this->assertEquals(\n\t\t\t\tarray(),\n\t\t\t\t$table->add_post_actions( array(), $post ),\n\t\t\t\t$pt\n\t\t\t);\n\n\t\t\t// Force the post type to show the ui.\n\t\t\tget_post_type_object( \"llms_{$pt}\" )->show_ui = true;\n\t\t\t$actions = $table->add_post_actions( array(), $post );\n\n\t\t\t$this->assertNotEmpty(\n\t\t\t\t$table->add_post_actions( array(), $post ),\n\t\t\t\t$pt\n\t\t\t);\n\n\t\t\t$this->assertEquals(\n\t\t\t\t$actions['llms-awards-list'],\n\t\t\t\t'achievement' === $pt ?\n\t\t\t\t\t'<a href=\"' . admin_url( 'edit.php' ) . '?llms_filter_template=' . $post->ID . '&post_type=llms_my_' . $pt . '\">View Awarded Achievements</a>'\n\t\t\t\t\t:\n\t\t\t\t\t'<a href=\"' . admin_url( 'edit.php' ) . '?llms_filter_template=' . $post->ID . '&post_type=llms_my_' . $pt . '\">View Awarded Certificates</a>',\n\t\t\t\t$pt\n\t\t\t);\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/traits/llms-test-trait-sales-page.php",
    "content": "<?php\n/**\n * Tests for {@see LLMS_Trait_Sales_Page}.\n *\n * @group Traits\n * @group LLMS_Post_Model\n *\n * @since 5.3.0\n */\nclass LLMS_Test_Sales_Page_Trait extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var LLMS_Trait_Sales_Page\n\t */\n\tprotected $mock;\n\n\t/**\n\t * Setup before running each test in this class.\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$args = array(\n\t\t\t'post_title' => 'Mock Post with the Sales Page Trait',\n\t\t);\n\t\t$this->mock = new class( 'new', $args ) extends LLMS_Post_Model {\n\n\t\t\tuse LLMS_Trait_Sales_Page;\n\n\t\t\tprotected $db_post_type = 'course';\n\t\t\tprotected $model_post_type = 'course';\n\n\t\t\tpublic function __construct( $model, $args = array() ) {\n\n\t\t\t\t$this->construct_sales_page();\n\t\t\t\tparent::__construct( $model, $args );\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Test the `construct_sales_page()` method.\n\t *\n\t * @since 5.3.0\n\t */\n\tpublic function test_construct_sales_page() {\n\t\t/**\n\t\t * {@see set_up()} should have created a mock object that called {@see LLMS_Trait_Sales_Page::construct_sales_page()}.\n\t\t */\n\t\t$properties = $this->mock->get_properties();\n\n\t\t$this->assertArrayHasKey( 'sales_page_content_page_id', $properties );\n\t\t$this->assertArrayHasKey( 'sales_page_content_type', $properties );\n\t\t$this->assertArrayHasKey( 'sales_page_content_url', $properties );\n\t}\n\n\t/**\n\t * Test the `get_sales_page_url()` method.\n\t *\n\t * @since 5.3.0\n\t */\n\tpublic function test_get_sales_page_url() {\n\n\t\t# Test \"Redirect to WordPress Page\".\n\t\t$page_id  = $this->factory()->post->create( array( 'post_type' => 'page' ) );\n\t\t$expected = get_permalink( $page_id );\n\t\t$this->mock->set( 'sales_page_content_type', 'page' );\n\t\t$this->mock->set( 'sales_page_content_page_id', $page_id );\n\t\t$actual = $this->mock->get_sales_page_url();\n\t\t$this->assertEquals( $expected, $actual );\n\n\t\t# Test \"Redirect to custom URL\".\n\t\t$expected = 'https://lifterlms.com';\n\t\t$this->mock->set( 'sales_page_content_type', 'url' );\n\t\t$this->mock->set( 'sales_page_content_url', $expected );\n\t\t$actual = $this->mock->get_sales_page_url();\n\t\t$this->assertEquals( $expected, $actual );\n\n\t\t# Test \"Display default course content\".\n\t\t$expected = get_permalink( $this->mock->get( 'id' ) );\n\t\t$this->mock->set( 'sales_page_content_type', 'none' );\n\t\t$actual = $this->mock->get_sales_page_url();\n\t\t$this->assertEquals( $expected, $actual );\n\n\t\t# Test \"Show custom content\".\n\t\t$expected = get_permalink( $this->mock->get( 'id' ) );\n\t\t$this->mock->set( 'sales_page_content_type', 'content' );\n\t\t$this->mock->set( 'excerpt', 'Please enroll in this course.' );\n\t\t$actual = $this->mock->get_sales_page_url();\n\t\t$this->assertEquals( $expected, $actual );\n\t}\n\n\t/**\n\t * Test the `has_sales_page_redirect()` method.\n\t *\n\t * @since 5.3.0\n\t */\n\tpublic function test_has_sales_page_redirect() {\n\n\t\t# Test \"Redirect to WordPress Page\".\n\t\t$page_id  = $this->factory()->post->create( array( 'post_type' => 'page' ) );\n\t\t$this->mock->set( 'sales_page_content_type', 'page' );\n\t\t$this->mock->set( 'sales_page_content_page_id', $page_id );\n\t\t$actual = $this->mock->has_sales_page_redirect();\n\t\t$this->assertTrue( $actual );\n\n\t\t# Test \"Redirect to custom URL\".\n\t\t$this->mock->set( 'sales_page_content_type', 'url' );\n\t\t$this->mock->set( 'sales_page_content_url', 'https://lifterlms.com' );\n\t\t$actual = $this->mock->has_sales_page_redirect();\n\t\t$this->assertTrue( $actual );\n\n\t\t# Test \"Display default course content\".\n\t\t$this->mock->set( 'sales_page_content_type', 'none' );\n\t\t$actual = $this->mock->has_sales_page_redirect();\n\t\t$this->assertFalse( $actual );\n\n\t\t# Test \"Show custom content\".\n\t\t$this->mock->set( 'sales_page_content_type', 'content' );\n\t\t$this->mock->set( 'excerpt', 'Please enroll in this course.' );\n\t\t$actual = $this->mock->has_sales_page_redirect();\n\t\t$this->assertFalse( $actual );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/traits/llms-test-trait-singleton.php",
    "content": "<?php\n/**\n * Tests for {@see LLMS_Trait_Singleton}.\n *\n * @group Traits\n *\n * @since 5.3.0\n * @since 6.0.0 Removed `LLMS_Test_Singleton_Trait::test_deprecated_instance()`.\n */\nclass LLMS_Test_Singleton_Trait extends LLMS_UnitTestCase {\n\n\t/**\n\t * Dynamic class name of the mock class.\n\t *\n\t * Even though this property contains a string, it is documented as a class so that it can be used like this:\n\t * `$this->mock_class::instance()`\n\t *\n\t * @since 5.3.0\n\t *\n\t * @var LLMS_Trait_Singleton|object\n\t */\n\tprotected $mock_class;\n\n\t/**\n\t * Setup before running each test in this class.\n\t *\n\t * @since 5.3.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @noinspection PhpHierarchyChecksInspection\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t# Instantiate an anonymous class that uses the trait to be tested.\n\t\t$mock = new class {\n\n\t\t\tuse LLMS_Trait_Singleton;\n\n\t\t\tprotected $color;\n\n\t\t\tprotected static $_instance = null;\n\n\t\t\tpublic static function deprecated_instance() {\n\t\t\t\tif ( is_null( self::$_instance ) ) {\n\t\t\t\t\tself::$_instance = new self();\n\t\t\t\t}\n\n\t\t\t\treturn self::$_instance;\n\t\t\t}\n\n\t\t\tpublic static function init() {\n\t\t\t\tself::$_instance = null;\n\t\t\t\tself::$instance  = null;\n\t\t\t}\n\n\t\t\tpublic function get_color() {\n\t\t\t\treturn $this->color;\n\t\t\t}\n\n\t\t\tpublic function set_color( $color ) {\n\t\t\t\t$this->color = $color;\n\t\t\t}\n\t\t};\n\n\t\t$this->mock_class = get_class( $mock );\n\t}\n\n\t/**\n\t * Test the {@see LLMS_Trait_Singleton::instance()} method.\n\t *\n\t * @since 5.3.0\n\t */\n\tpublic function test_instance() {\n\n\t\t# Test that the static instance property does not yet have an object.\n\t\t$this->mock_class::init();\n\t\t$instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' );\n\t\t$this->assertIsNotObject( $instance_property );\n\n\t\t/**\n\t\t * Test that {@see LLMS_Trait_Singleton::instance()} instantiates a new object,\n\t\t * sets it in the static `$instance` property, and returns the new object.\n\t\t */\n\t\t$object1           = $this->mock_class::instance();\n\t\t$instance_property = LLMS_Unit_Test_Util::get_private_property_value( $this->mock_class, 'instance' );\n\t\t$this->assertEquals( $object1, $instance_property );\n\n\t\t# Test that 2 instances are the same.\n\t\t$object1->set_color( 'red' );\n\t\t$object2 = $this->mock_class::instance();\n\t\t$object2->set_color( 'green' );\n\t\t$this->assertEquals( $object1, $object2 );\n\t\t$this->assertEquals( 'green', $object1->get_color() );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/traits/llms-test-trait-student-awards.php",
    "content": "<?php\n/**\n * Tests for {@see LLMS_Trait_Student_Awards}.\n *\n * @group traits\n * @group student\n * @group student_awards\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Trait_Student_awards extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test all trait methods.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_all_the_methods() {\n\n\t\t$tests = array(\n\t\t\t'achievement',\n\t\t\t'certificate',\n\t\t);\n\n\t\t$student    = $this->factory->student->create_and_get();\n\t\t$all_awards = array();\n\n\t\tforeach ( $tests as $type ) {\n\n\t\t\t$get    = \"get_{$type}s\";\n\t\t\t$earn   = \"earn_{$type}\";\n\t\t\t$create = \"create_{$type}_template\";\n\t\t\t$obj    = sprintf( 'LLMS_User_%s', ucwords( $type ) );\n\n\t\t\t$earned = $this->$earn( $student->get( 'id' ), $this->$create(), $this->factory->post->create() );\n\n\t\t\t$awards = $student->$get( array() );\n\t\t\t$this->assertInstanceOf( 'LLMS_Awards_Query', $awards );\n\n\t\t\t$post = $awards->get_results()[0];\n\t\t\t$this->assertEquals( $earned[1], $post->ID );\n\t\t\t$this->assertEquals( $student->get( 'id' ), $post->post_author );\n\n\t\t\t$award = $awards->get_awards()[0];\n\t\t\t$this->assertInstanceOf( $obj, $award );\n\t\t\t$this->assertEquals( $earned[1], $award->get( 'id' ) );\n\t\t\t$this->assertEquals( $student->get( 'id' ), $award->get_user_id() );\n\n\t\t\t$this->assertEquals( 1, $student->get_awards_count( $type ) );\n\n\t\t\t$all_awards[] = $award;\n\n\t\t}\n\n\t\t// Test mixed awards return.\n\t\t$awards_query = $student->get_awards();\n\t\t$this->assertEquals( array_reverse( $all_awards ), $awards_query->get_awards() ); // Sorted in rev. chron by default.\n\t\t$this->assertEquals( 2, $student->get_awards_count() );\n\n\t}\n\n\t/**\n\t * Test get_achievements & get_certificates() deprecated method signature.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @expectedDeprecated LLMS_Student::get_achievements()\n\t * @expectedDeprecated LLMS_Student::get_certificates()\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_methods_deprecated() {\n\n\t\tif ( '7.4' === sprintf( '%1$d.%2$d', PHP_MAJOR_VERSION, PHP_MINOR_VERSION ) ) {\n\t\t\t/**\n\t\t\t * The tests to sort by date are working inconsistently on PHP 7.4 in CI only.\n\t\t\t * I can't reproduce locally and I've determined that this this particular\n\t\t\t * test is not worth debugging further and / or rewriting since it's a deprecated\n\t\t\t * function and it *actually is working properly* in real world scenarios. The issue\n\t\t\t * is with two engagements created at the *same second* will return in an unpredictable\n\t\t\t * order on 7.4 in the GH Actions CI... It seems to work as expected locally (it'll return the\n\t\t\t * item the with smaller ID first).\n\t\t\t */\n\t\t\t$this->markTestSkipped( 'Test skipped on PHP 7.4.' );\n\t\t}\n\n\t\t$tests = array(\n\t\t\t'achievement',\n\t\t\t'certificate',\n\t\t);\n\n\t\tforeach ( $tests as $type ) {\n\n\t\t\t$get    = \"get_{$type}s\";\n\t\t\t$earn   = \"earn_{$type}\";\n\t\t\t$create = \"create_{$type}_template\";\n\t\t\t$obj    = sprintf( 'LLMS_User_%s', ucwords( $type ) );\n\t\t\t$id     = \"{$type}_id\";\n\t\t\t$load   = function( $item ) use ( $obj ) {\n\t\t\t\treturn new $obj( $item );\n\t\t\t};\n\n\t\t\t$student = $this->factory->student->create_and_get();\n\n\t\t\t$this->assertEquals( array(), $student->$get() );\n\n\t\t\t$expect = array();\n\t\t\t$i = 0;\n\t\t\twhile ( $i < 5 ) {\n\n\t\t\t\t$ts   = time() - WEEK_IN_SECONDS * rand( 5, 15 );\n\t\t\t\t$date = date( 'Y-m-d H:i:s', $ts );\n\n\t\t\t\t$related = $this->factory->post->create();\n\n\t\t\t\tllms_tests_mock_current_time( $ts );\n\t\t\t\t$earned = $this->$earn( $student->get( 'id' ), $this->$create(), $related );\n\n\t\t\t\t$obj              = new stdClass();\n\t\t\t\t$obj->post_id     = $related;\n\t\t\t\t$obj->$id         = $earned[1];\n\t\t\t\t$obj->earned_date = $date;\n\n\t\t\t\t$expect[] = $obj;\n\t\t\t\t$i++;\n\t\t\t}\n\n\t\t\t$sort_opts = array(\n\t\t\t\t'earned_date'  => 'earned_date',    // Sort by the AS value.\n\t\t\t\t'updated_date' => 'earned_date',    // Sort by the actual meta key name.\n\t\t\t\t'post_id'      => 'post_id',        // Related post id.\n\t\t\t\t$id            => $id, // Earned ID.\n\t\t\t\t'meta_value'   => $id, // actual meta value name.\n\t\t\t);\n\n\t\t\tforeach ( $sort_opts as $input_sort => $expect_sort ) {\n\n\t\t\t\t// Descending (default).\n\t\t\t\t$expected = wp_list_sort( $expect, $expect_sort, 'DESC' );\n\t\t\t\t$this->assertEquals(\n\t\t\t\t\t$expected,\n\t\t\t\t\t$student->$get( $input_sort, 'DESC', 'obj' )\n\t\t\t\t);\n\t\t\t\t$this->assertEquals(\n\t\t\t\t\tarray_map( $load, wp_list_pluck( $expected, $id ) ),\n\t\t\t\t\t$student->$get( $input_sort, 'DESC', \"{$type}s\" )\n\t\t\t\t);\n\n\t\t\t\t// Ascending.\n\t\t\t\t$expected = wp_list_sort( $expect, $expect_sort, 'ASC' );\n\t\t\t\t$this->assertEquals(\n\t\t\t\t\t$expected,\n\t\t\t\t\t$student->$get( $input_sort, 'ASC', 'obj' )\n\t\t\t\t);\n\t\t\t\t$this->assertEquals(\n\t\t\t\t\tarray_map( $load, wp_list_pluck( $expected, $id ) ),\n\t\t\t\t\t$student->$get( $input_sort, 'ASC', \"{$type}s\" )\n\t\t\t\t);\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/traits/llms-test-trait-user-engagement-type.php",
    "content": "<?php\n/**\n * Tests for {@see LLMS_Trait_User_Engagement_Type}.\n *\n * @group Traits\n *\n * @since 6.0.0\n */\nclass LLMS_Test_Trait_User_Engagement_Type extends LLMS_UnitTestCase {\n\n\t/**\n\t * @var LLMS_Trait_User_Engagement_Type\n\t */\n\tprotected $mock;\n\n\tprotected $mock_class;\n\n\t/**\n\t * Setup before running each test in this class.\n\t *\n\t * @since 6.0.0\n\t */\n\tpublic function set_up() {\n\n\t\tparent::set_up();\n\n\t\t$this->mock = new class() {\n\n\t\t\tuse LLMS_Trait_User_Engagement_Type;\n\n\t\t\tpublic function __construct() {\n\t\t\t\t$this->engagement_type = 'mock_cert';\n\t\t\t}\n\t\t};\n\t}\n\n\tpublic static function set_up_before_class() {\n\n\t\tparent::set_up_before_class();\n\n\t\tregister_post_type( 'llms_mock_cert', array(\n\t\t\t'labels' => array(\n\t\t\t\t'name'          => 'Mock Certificate Templates',\n\t\t\t\t'singular_name' => 'Mock Certificate Template'\n\t\t\t)\n\t\t) );\n\t\tregister_post_type( 'llms_my_mock_cert', array(\n\t\t\t'labels' => array(\n\t\t\t\t'name'          => 'Awarded Mock Certificates',\n\t\t\t\t'singular_name' => 'Awarded Mock Certificate'\n\t\t\t)\n\t\t) );\n\t}\n\n\t/**\n\t * Tear down after class.\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic static function tear_down_after_class() {\n\n\t\tparent::tear_down_after_class();\n\t\tunregister_post_type( 'llms_mock_cert' );\n\t\tunregister_post_type( 'llms_my_mock_cert' );\n\t}\n\n\t/**\n\t * Test get_user_engagement().\n\t *\n\t * @since 6.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_user_engagement() {\n\n\t\t/** Create a mock class, by creating a mock object, that\n\t\t * {@see LLMS_Trait_User_Engagement_Type::get_user_engagement()} can use to instantiate an object.\n\t\t */\n\t\t$this->getMockBuilder( LLMS_Abstract_User_Engagement::class )\n\t\t     ->setMockClassName( 'LLMS_User_Mock_Cert' )\n\t\t     ->setConstructorArgs( array( 'new' ) )\n\t\t     ->onlyMethods( array() ) // Do not replace any methods with configurable test doubles.\n\t\t     ->getMock();\n\n\t\t// Test a non-existing awarded engagement.\n\t\tself::assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->mock, 'get_user_engagement', array( - 1, true ) )\n\t\t);\n\n\t\t// Test a non-existing engagement template.\n\t\tself::assertFalse(\n\t\t\tLLMS_Unit_Test_Util::call_method( $this->mock, 'get_user_engagement', array( - 1, false ) )\n\t\t);\n\n\t\t// Test an engagement template.\n\t\t$template_id         = $this->factory->post->create( array( 'post_type' => 'llms_mock_cert' ) );\n\t\t$args                = array( $template_id, false );\n\t\t$engagement_template = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_user_engagement', $args );\n\t\tself::assertIsObject( $engagement_template );\n\t\tself::assertEquals( 'LLMS_User_Mock_Cert', get_class( $engagement_template ) );\n\t\tself::assertEquals( $template_id, $engagement_template->get( 'id' ) );\n\n\t\t// Test an awarded engagement.\n\t\t$awarded_id = $this->factory->post->create( array( 'post_type' => 'llms_my_mock_cert' ) );\n\t\t$args       = array( $awarded_id, true );\n\t\t/** @var LLMS_Abstract_User_Engagement $awarded_engagement */\n\t\t$awarded_engagement = LLMS_Unit_Test_Util::call_method( $this->mock, 'get_user_engagement', $args );\n\t\tself::assertIsObject( $awarded_engagement );\n\t\tself::assertEquals( 'LLMS_User_Mock_Cert', get_class( $awarded_engagement ) );\n\t\tself::assertEquals( $awarded_id, $awarded_engagement->get( 'id' ) );\n\t}\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/user/class-llms-test-abstract-user-data.php",
    "content": "<?php\n/**\n * Tests for LLMS_Abstract_User_Data\n * \n * @group LLMS_Student\n * @group abstracts\n * @group abstract_user_data\n * \n * @since 3.9.0\n */\nclass LLMS_Test_Abstract_User_Data extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test constructor and get_user_id() method.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\t$tests = array(\n\t\t\t(int) $user->ID,\n\t\t\t(string) $user->ID,\n\t\t\t$user,\n\t\t\tnew LLMS_Student( $user ),\n\t\t);\n\n\t\t$expected = new LLMS_Student( $user );\n\n\t\tforeach ( $tests as $i => $input ) {\n\t\t\t$student = new LLMS_Student( $input );\n\t\t\t$this->assertEquals( $expected, $student, $i );\n\t\t\t$this->assertEquals( $user->ID, $student->get_id(), $i );\n\t\t\t$this->assertEquals( $user, $student->get_user(), $i );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test constructor when loading the current user.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor_curr_user() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\t\twp_set_current_user( $user->ID );\n\n\t\t$student = new LLMS_Student();\n\t\t$this->assertEquals( new LLMS_Student( $user->ID ), $student );\n\t\t$this->assertEquals( $user->ID, $student->get_id() );\n\t\t$this->assertEquals( $user, $student->get_user() );\n\n\t}\n\n\t/**\n\t * Test constructor when current user autoloading is disabled.\n\t *\n\t * @since 7.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_constructor_no_autoload() {\n\n\t\t$user = $this->factory->user->create_and_get();\n\n\t\twp_set_current_user( $user->ID );\n\n\t\t$student = new LLMS_Student( null, false );\n\n\t\t$this->assertSame( 0, $student->get_id() );\n\t\t$this->assertFalse( $student->exists() );\n\n\t}\n\n\t/**\n\t * Test exists function\n\t * \n\t * @since 3.9.0\n\t * \n\t * @return void\n\t */\n\tpublic function test_exists() {\n\n\t\t$uid = $this->factory->user->create();\n\n\t\t$student = new LLMS_Student( $uid );\n\t\t$this->assertTrue( $student->exists() );\n\n\t\t$fake_student = new LLMS_Student( $uid + 1 );\n\t\t$this->assertFalse( $fake_student->exists() );\n\n\t}\n\n\t/**\n\t * test get_id method\n\t * \n\t * @since 3.9.0\n\t * \n\t * @return void\n\t */\n\tpublic function test_get_id() {\n\n\t\t$uid = $this->factory->user->create();\n\t\t$student = new LLMS_Student( $uid );\n\t\t$this->assertEquals( $uid, $student->get_id() );\n\n\t}\n\n\t/**\n\t * test get_user method\n\t * \n\t * @since 3.9.0\n\t * \n\t * @return void\n\t */\n\tpublic function test_get_user() {\n\n\t\t$uid = $this->factory->user->create();\n\t\t$student = new LLMS_Student( $uid );\n\t\t$this->assertTrue( is_a( $student->get_user(), 'WP_User' ) );\n\n\t}\n\n\t/**\n\t * Test Student Getters and Setters\n\t * \n\t * @since 3.9.0\n\t * \n\t * @return   void\n\t */\n\tpublic function test_getters_setters() {\n\n\t\t$uid     = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\t$user    = new WP_User( $uid );\n\t\t$student =  new LLMS_Student( $uid );\n\n\t\t// test some core prefixed stuff from the usermeta table\n\t\t$student->set( 'first_name', 'Student' );\n\t\t$student->set( 'last_name', 'McStudentFace' );\n\t\t$this->assertEquals( get_user_meta( $uid, 'first_name', true ), $student->get( 'first_name' ) );\n\t\t$this->assertEquals( get_user_meta( $uid, 'last_name', true ), $student->get( 'last_name' ) );\n\n\t\t// stuff from the user table\n\t\t$this->assertEquals( $user->user_email, $student->get( 'user_email' ) );\n\n\t\t// llms custom user meta\n\t\t$student->set( 'billing_address', '123 Student Place' );\n\t\t$this->assertEquals( get_user_meta( $uid, 'llms_billing_address', true ), $student->get( 'billing_address' ) );\n\n\t\t// don't prefix\n\t\t$student->set( 'this_is_third_party', '123456', false );\n\t\tadd_filter( 'llms_student_unprefixed_metas', function( $metas ) {\n\t\t\t$metas[] = 'this_is_third_party';\n\t\t\treturn $metas;\n\t\t} );\n\t\t$this->assertEquals( get_user_meta( $uid, 'this_is_third_party', true ), $student->get( 'this_is_third_party' ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/user/class-llms-test-person-handler.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Core Functions\n *\n * @group LLMS_Student\n * @group LLMS_Person_Handler\n *\n * @since 3.19.4\n * @since 3.29.4 Unknown.\n * @since 3.37.17 Add voucher-related tests.\n * @since 4.5.0 Added tests on account.signon event recorded on user registration.\n * @since 5.0.0 Update to work with changes from LLMS_Forms.\n *               Add tests for the LLMS_Person_Handler::get_login_forms() method.\n *               Login tests don't rely on deprecated option `lifterlms_registration_generate_username`.\n *               Remove tests handled by LLMS_Form_Handler: test_validate_fields_with_voucher_not_found, test_validate_fields_with_voucher_code_deleted, test_validate_fields_with_voucher_post_deleted, test_validate_fields_with_voucher_redemptions_maxed\n */\nclass LLMS_Test_Person_Handler extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test username generation\n\t * @return   void\n\t * @since    3.19.4\n\t * @version  3.19.4\n\t */\n\tpublic function test_generate_username() {\n\n\t\t// username is first part of email\n\t\t$this->assertEquals( 'mock', LLMS_Person_Handler::generate_username( 'mock@whatever.com' ) );\n\n\t\t// create a user with the mock username\n\t\t$this->factory->user->create( array(\n\t\t\t'user_login' => 'mock',\n\t\t) );\n\n\t\t// test that usernames are unique\n\t\t$i = 1;\n\t\twhile ( $i <= 5 ) {\n\t\t\t$this->factory->user->create( array(\n\t\t\t\t'user_login' => sprintf( 'mock%d', $i ),\n\t\t\t) );\n\t\t\t$this->assertEquals( sprintf( 'mock%d', $i+1 ), LLMS_Person_Handler::generate_username( 'mock@whatever.com' ) );\n\t\t\t$i++;\n\t\t}\n\n\t\t// test character sanitization\n\t\t$tests = array(\n\t\t\t'mock_mock' => 'mock_mock',\n\t\t\t\"mock'mock\" => \"mockmock\",\n\t\t\t'mock+mock' => \"mockmock\",\n\t\t\t'mock.mock' => \"mock.mock\",\n\t\t\t'mock-mock' => \"mock-mock\",\n\t\t\t'mock mock' => \"mock mock\",\n\t\t\t'mock!mock' => \"mockmock\",\n\t\t);\n\n\t\tforeach ( $tests as $email => $expect) {\n\t\t\t$this->assertEquals( $expect, LLMS_Person_Handler::generate_username( $email . '@whatever.com' ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the get_login_fields() method.\n\t *\n\t * It should return an array of LifterLMS Form Fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_login_fields() {\n\n\t\t$fields = LLMS_Person_Handler::get_login_fields();\n\n\t\t$this->assertTrue( is_array( $fields ) );\n\t\t$this->assertEquals( 5, count( $fields ) );\n\t\tforeach ( $fields as $field ) {\n\t\t\t$this->assertTrue( is_array( $field ) );\n\t\t\t$this->assertArrayHasKey( 'id', $field );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the get_login_fields() method when the layout is columns\n\t *\n\t * It should return an array of LifterLMS Form Fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_login_fields_layout_columns() {\n\n\t\t$default = LLMS_Person_Handler::get_login_fields();\n\t\t$fields  = LLMS_Person_Handler::get_login_fields( 'columns' );\n\n\t\t// Default value is \"columns\".\n\t\t$this->assertEquals( $default, $fields );\n\n\t\t$this->assertEquals( 6, $fields[0]['columns'] );\n\t\t$this->assertEquals( 6, $fields[1]['columns'] );\n\t\t$this->assertEquals( 3, $fields[2]['columns'] );\n\t\t$this->assertEquals( 6, $fields[3]['columns'] );\n\t\t$this->assertEquals( 3, $fields[4]['columns'] );\n\n\t}\n\n\t/**\n\t * Test the get_login_fields() method when the layout is stacked\n\t *\n\t * It should return an array of LifterLMS Form Fields.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_login_fields_layout_stacked() {\n\n\t\t$fields = LLMS_Person_Handler::get_login_fields( 'stacked' );\n\n\t\t$this->assertEquals( 12, $fields[0]['columns'] );\n\t\t$this->assertEquals( 12, $fields[1]['columns'] );\n\t\t$this->assertEquals( 12, $fields[2]['columns'] );\n\t\t$this->assertEquals( 6, $fields[3]['columns'] );\n\t\t$this->assertEquals( 6, $fields[4]['columns'] );\n\n\t}\n\n\t/**\n\t * Test get_login_fields() when usernames are enabled.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_login_fields_usernames_enabled() {\n\n\t\tadd_filter( 'llms_are_usernames_enabled', '__return_true' );\n\t\t$field = LLMS_Person_Handler::get_login_fields()[0];\n\t\t$this->assertEquals( 'Username or Email Address', $field['label'] );\n\t\t$this->assertEquals( 'text', $field['type'] );\n\t\tremove_filter( 'llms_are_usernames_enabled', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test get_login_fields() when usernames are disabled.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_login_fields_usernames_disabled() {\n\n\t\tadd_filter( 'llms_are_usernames_enabled', '__return_false' );\n\t\t$field = LLMS_Person_Handler::get_login_fields()[0];\n\t\t$this->assertEquals( 'Email Address', $field['label'] );\n\t\t$this->assertEquals( 'email', $field['type'] );\n\t\tremove_filter( 'llms_are_usernames_enabled', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test get_lost_password_fields() when usernames are enabled.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lost_password_fields_usernames_enabled() {\n\n\t\tadd_filter( 'llms_are_usernames_enabled', '__return_true' );\n\t\t$fields = LLMS_Person_Handler::get_lost_password_fields();\n\t\t$this->assertTrue( false !== strpos( $fields[0]['value'], 'username' ) );\n\t\t$this->assertEquals( 'Username or Email Address', $fields[1]['label'] );\n\t\t$this->assertEquals( 'text', $fields[1]['type'] );\n\t\tremove_filter( 'llms_are_usernames_enabled', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test get_lost_password_fields() when usernames are disabled.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_lost_password_fields_usernames_disabled() {\n\n\t\tadd_filter( 'llms_are_usernames_enabled', '__return_false' );\n\t\t$fields = LLMS_Person_Handler::get_lost_password_fields();\n\t\t$this->assertFalse( strpos( $fields[0]['value'], 'username' ) );\n\t\t$this->assertEquals( 'Email Address', $fields[1]['label'] );\n\t\t$this->assertEquals( 'email', $fields[1]['type'] );\n\t\tremove_filter( 'llms_are_usernames_enabled', '__return_false' );\n\n\t}\n\n\t/**\n\t * Test get_password_reset_fields() when \"custom\" password reset fields exist on the checkout form.\n\t *\n\t * @since 5.0.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_password_reset_fields_from_checkout() {\n\n\t\tupdate_option( 'lifterlms_registration_password_strength', 'yes' );\n\t\tLLMS_Forms::instance()->create( 'checkout', true );\n\n\t\tadd_filter( 'llms_password_reset_fields', function( $fields, $key, $login, $location ) {\n\t\t\t$this->assertEquals( 'checkout', $location );\n\t\t\treturn $fields;\n\t\t}, 10, 4 );\n\n\t\t$expect = array(\n\t\t\t'password',\n\t\t\t'password_confirm',\n\t\t\t'llms-password-strength-meter',\n\t\t\t'llms_lost_password_button',\n\t\t\t'llms_reset_key',\n\t\t\t'llms_reset_login',\n\t\t);\n\t\t$this->assertEquals( $expect, wp_list_pluck( LLMS_Person_Handler::get_password_reset_fields(), 'id' ) );\n\n\t}\n\n\t/**\n\t * Test get_password_reset_fields() when \"custom\" password reset fields don't exist on checkout but do exist on reg form.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Made sure no users are logged in before retrieving password reset fields.\n\t *               And avoid auto-adding of required fields like password/email when retrieving form's fields.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_password_reset_fields_from_registration() {\n\n\t\twp_update_post( array(\n\t\t\t'ID'           => LLMS_Forms::instance()->create( 'checkout', true ),\n\t\t\t'post_content' => '',\n\t\t) );\n\n\t\tLLMS_Forms::instance()->create( 'registration', true );\n\t\t// Avoid auto adding of fields like password/email.\n\t\tadd_filter( 'llms_forms_required_block_fields', '__return_empty_array' );\n\t\tadd_filter( 'llms_password_reset_fields', function( $fields, $key, $login, $location ) {\n\t\t\t$this->assertEquals( 'registration', $location );\n\t\t\treturn $fields;\n\t\t}, 10, 4 );\n\n\t\t// Log out.\n\t\twp_set_current_user( null );\n\n\t\t$expect = array(\n\t\t\t'password',\n\t\t\t'password_confirm',\n\t\t\t'llms-password-strength-meter',\n\t\t\t'llms_lost_password_button',\n\t\t\t'llms_reset_key',\n\t\t\t'llms_reset_login',\n\t\t);\n\t\t$this->assertEquals( $expect, wp_list_pluck( LLMS_Person_Handler::get_password_reset_fields(), 'id' ) );\n\n\t\tremove_filter( 'llms_forms_required_block_fields', '__return_empty_array' );\n\n\t}\n\n\t/**\n\t * Test get_password_reset_fields() when \"custom\" password reset fields don't exist on checkout but do exist on reg form.\n\t *\n\t * @since 5.0.0\n\t * @since 5.1.0 Avoid auto-adding of required fields like password/email when retrieving form's fields.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_password_reset_fields_from_fallback() {\n\n\t\t// Avoid auto adding of fields like password/email.\n\t\tadd_filter( 'llms_forms_required_block_fields', '__return_empty_array' );\n\t\tadd_filter( 'llms_password_reset_fields', function( $fields, $key, $login, $location ) {\n\t\t\t$this->assertEquals( 'fallback', $location );\n\t\t\treturn $fields;\n\t\t}, 10, 4 );\n\n\t\t$expect = array(\n\t\t\t'password',\n\t\t\t'password_confirm',\n\t\t\t'llms-password-strength-meter',\n\t\t\t'llms_lost_password_button',\n\t\t\t'llms_reset_key',\n\t\t\t'llms_reset_login',\n\t\t);\n\t\t$this->assertEquals( $expect, wp_list_pluck( LLMS_Person_Handler::get_password_reset_fields(), 'id' ) );\n\n\t\tremove_filter( 'llms_forms_required_block_fields', '__return_empty_array' );\n\n\t}\n\n\t/**\n\t * Test logging in with a username.\n\t *\n\t * @since 3.29.4\n\t * @since 5.0.0 Remove deprecated option `lifterlms_registration_generate_username` and allow username login via filter.\n\t *\n\t * @return  void\n\t */\n\tpublic function test_login_with_username() {\n\n\t\t// Enable Usernames.\n\t\tadd_filter( 'llms_are_usernames_enabled', '__return_true' );\n\n\t\t// Missing login.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_password' => 'faker',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'llms_login', $login );\n\n\t\t// Missing Password\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => 'faker',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password', $login );\n\n\t\t// Totally Invalid creds.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => '3OGgpZZ146cH3vw775aMg1R7qQIrF4ph',\n\t\t\t'llms_password' => 'Ip439RKmf0am5MWRjD38ov6M45OEYs79',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'login-error', $login );\n\n\t\t// Test against a real user with bad creds.\n\t\t$user = $this->factory->user->create_and_get( array( 'user_login' => 'test_user_login', 'user_pass' => '1234' ) );\n\t\t$uid  = $user->ID;\n\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => 'test_user_login',\n\t\t\t'llms_password' => '1',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'login-error', $login );\n\n\t\t// Success.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => 'test_user_login',\n\t\t\t'llms_password' => '1234',\n\t\t) );\n\n\t\t$this->assertEquals( $uid, $login );\n\t\twp_logout();\n\n\n\t\t// Use a fake email address in the login field.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => 'fake@whatever.com',\n\t\t\t'llms_password' => '1234',\n\t\t) );\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'login-error', $login );\n\n\t\t// Use the real email address in the login field.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => $user->user_email,\n\t\t\t'llms_password' => '1234',\n\t\t) );\n\t\t$this->assertEquals( $uid, $login );\n\t\twp_logout();\n\n\t\tremove_filter( 'llms_are_usernames_enabled', '__return_true' );\n\n\t}\n\n\t/**\n\t * Test logging in with a username.\n\t *\n\t * @since 3.29.4\n\t * @since 5.0.0 Remove deprecated option `lifterlms_registration_generate_username`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_login_with_email() {\n\n\t\t// Missing login.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_password' => 'faker',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'llms_login', $login );\n\n\t\t// Invalid email address.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => 'faker',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'llms_login', $login );\n\n\t\t// Missing password.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => 'faker@fake.tld',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'llms_password', $login );\n\n\t\t// Totally Invalid creds.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => '3OGgpZZ146cH3vw775aMg1R7qQIrF4ph@fake.tld',\n\t\t\t'llms_password' => 'Ip439RKmf0am5MWRjD38ov6M45OEYs79',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'login-error', $login );\n\n\t\t// Test against a real user with bad creds.\n\t\t$user = $this->factory->user->create_and_get( array( 'user_pass' => '1234' ) );\n\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => $user->user_email,\n\t\t\t'llms_password' => '1',\n\t\t) );\n\n\t\t$this->assertIsWPError( $login );\n\t\t$this->assertWPErrorCodeEquals( 'login-error', $login );\n\n\t\t// Success.\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => $user->user_email,\n\t\t\t'llms_password' => '1234',\n\t\t) );\n\n\t\t$this->assertEquals( $user->ID, $login );\n\t\twp_logout();\n\n\t\t// Make sure that email addresses with an apostrophe in them can login without issue.\n\t\t$user = $this->factory->user->create_and_get( array( 'user_email' => \"mock\\'mock@what.org\", 'user_pass' => '1234' ) );\n\t\t$login = LLMS_Person_Handler::login( array(\n\t\t\t'llms_login' => $user->user_email,\n\t\t\t'llms_password' => '1234',\n\t\t) );\n\n\t\t$this->assertEquals( $user->ID, $login );\n\t\twp_logout();\n\n\t}\n\n\t/**\n\t * Test account.signon event recorded on user registration\n\t *\n\t * @since 4.5.0\n\t * @since 5.0.0 Add email confirm fields to reflect new form defaults.\n\t */\n\tpublic function test_account_signon_event_recorded_on_registration_signon() {\n\n\t\t$this->markTestSkipped( 'Fails sometimes' );\n\n\t\tLLMS_Install::create_pages();\n\t\tLLMS_Forms::instance()->install( true );\n\n\t\tglobal $wpdb;\n\n\t\t$data = $this->get_mock_registration_data();\n\t\t$data['email_address'] = \"new_{$data['email_address']}\";\n\t\t$data['email_address_confirm'] = $data['email_address'];\n\n\t\t$query_signon_event = \"\n\t\t\tSELECT COUNT(*) FROM {$wpdb->prefix}lifterlms_events\n\t\t\tWHERE event_type='account'\n\t\t\tAND event_action='signon'\n\t\t\tAND object_type='user'\n\t\t\tAND actor_id='%d'\n\t\t\t\";\n\n\t\t// Test no event registered, if no signon.\n\t\t$user_id = llms_register_user( $data, 'registration', false );\n\t\t$this->assertEquals( 0, $wpdb->get_var( $wpdb->prepare( $query_signon_event, $user_id ) ) );\n\n\t\t// Test event registered when signing on registration (defaults).\n\t\t$data['email_address'] = \"new1_{$data['email_address']}\";\n\t\t$data['email_address_confirm'] = $data['email_address'];\n\t\t$user_id = llms_register_user( $data );\n\t\t$this->assertEquals( 1, $wpdb->get_var( $wpdb->prepare( $query_signon_event, $user_id ) ) );\n\n\t\t// Clean up tables.\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_events\" );\n\t\t$wpdb->query( \"TRUNCATE TABLE {$wpdb->prefix}lifterlms_events_open_sessions\" );\n\t}\n\n\t/**\n\t * Test the deprecated update() method.\n\t *\n\t * This test remains to ensure backwards compatibility.\n\t *\n\t * @since 3.26.1\n\t * @since 5.0.0 Create forms before running & update error codes to match updated codes.\n\t * @since 6.0.0 Replaced the deprecated `LLMS_Person_Handler::update` function calls with `llms_update_user()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_update() {\n\n\t\tLLMS_Install::create_pages();\n\t\tLLMS_Forms::instance()->install();\n\n\t\t$data = array();\n\n\t\t// No user Id supplied.\n\t\t$update = llms_update_user( $data, 'account' );\n\t\t$this->assertTrue( is_wp_error( $update ) );\n\t\t$this->assertEquals( 'llms-form-no-user', $update->get_error_code() );\n\n\t\t$uid = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\t$user = new WP_User( $uid );\n\n\t\t// user Id Interpreted from current logged in user.\n\t\twp_set_current_user( $uid );\n\t\t$update = llms_update_user( $data, 'account' );\n\t\t$this->assertTrue( is_wp_error( $update ) );\n\t\t$this->assertFalse( in_array( 'llms-form-no-user', $update->get_error_codes(), true ) );\n\t\twp_set_current_user( null );\n\n\t\t// Used ID explicitly passed.\n\t\t$data['user_id'] = $uid;\n\t\t$update = llms_update_user( $data, 'account' );\n\t\t$this->assertTrue( is_wp_error( $update ) );\n\t\t$this->assertTrue( in_array( 'llms-form-no-user', $update->get_error_codes(), true ) );\n\n\t}\n\n\tprivate function get_mock_registration_data( $data = array() ) {\n\n\t\t$password = wp_generate_password();\n\n\t\treturn wp_parse_args( $data, array(\n\t\t\t'user_login' => 'mocker',\n\t\t\t'email_address' => 'mocker@mock.com',\n\t\t\t'first_name' => 'Bird',\n\t\t\t'last_name' => 'Person',\n\t\t\t'llms_billing_address_1' => '1234 Street Ave.',\n\t\t\t'llms_billing_address_2' => '#567',\n\t\t\t'llms_billing_city' => 'Anywhere,',\n\t\t\t'llms_billing_state' => 'CA',\n\t\t\t'llms_billing_zip' => '12345',\n\t\t\t'llms_billing_country' => 'US',\n\t\t\t'llms_agree_to_terms' => 'yes',\n\t\t\t'password' => $password,\n\t\t\t'password_confirm' => $password,\n\t\t) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/user/class-llms-test-student-quizzes.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Student Functions\n *\n * @group quizzes\n * @group student_quizzes\n * @group LLMS_Student\n *\n * @since 3.9.0\n */\nclass LLMS_Test_Student_Quizzes extends LLMS_UnitTestCase {\n\n\t/**\n\t * Assert that two quiz attempts are deeply equal\n\t *\n\t * @since 4.21.2\n\t *\n\t * @param LLMS_Quiz_Attempt $expected Expected attempt object.\n\t * @param LLMS_Quiz_Attempt $actual   Actual attempt object.\n\t * @return void\n\t */\n\tprivate function assertAttemptsAreEqual( $expected, $actual ) {\n\n\t\t$props = array(\n\t\t\t'id',\n\t\t\t'student_id',\n\t\t\t'quiz_id',\n\t\t\t'lesson_id',\n\t\t\t'start_date',\n\t\t\t'update_date',\n\t\t\t'end_date',\n\t\t\t'status',\n\t\t\t'attempt',\n\t\t\t'grade',\n\t\t);\n\n\t\tforeach ( $props as $prop ) {\n\t\t\t$this->assertEquals( $expected->get( $prop ), $actual->get( $prop ), $prop );\n\t\t}\n\n\t}\n\n\t/**\n\t * Create a student with sample quizzes.\n\t *\n\t * @since Unknown\n\t *\n\t * @return LLMS_Student\n\t */\n\tprivate function get_student_with_quizzes( $attempts = 3 ) {\n\n\t\t$uid = $this->factory->user->create();\n\t\t$student = llms_get_student( $uid );\n\t\t$courses = $this->generate_mock_courses( $attempts, 1, 1, 1 );\n\t\t$this->complete_courses_for_student( $uid, $courses );\n\t\treturn $student;\n\n\t}\n\n\t/**\n\t * Retrieve a quiz attempt for a given student.\n\t *\n\t * @since 4.21.2\n\t *\n\t * @param LLMS_Student $student Student object.\n\t * @return LLMS_Quiz_Attempt\n\t */\n\tprivate function get_attempt( $student ) {\n\n\t\t$course  = llms_get_post( $this->generate_mock_courses( 1, 1, 1, 1 )[0] );\n\t\t$lesson  = $course->get_lessons()[0];\n\t\t$quiz    = $lesson->get_quiz();\n\n\t\t$attempt = LLMS_Quiz_Attempt::init( $quiz->get( 'id' ), $lesson->get( 'id' ), absint( $student->get( 'id' ) ) );\n\n\t\t$attempt->save();\n\n\t\treturn new LLMS_Quiz_Attempt( $attempt->get( 'id' ) );\n\n\t}\n\n\t/**\n\t * Test delete_attempt()\n\t *\n\t * @since 3.9.0\n\t * @since 3.16.11 Unknown.\n\t * @since 4.21.2 Only users who can view_grades can delete attempts.\n\t *\n\t * @return void\n\t */\n\tpublic function test_delete_attempt() {\n\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\n\t\t$i = 1;\n\t\twhile ( $i <= 5 ) {\n\n\t\t\t$student  = $this->get_student_with_quizzes();\n\t\t\t$attempts = $student->quizzes()->get_all();\n\t\t\t$id       = rand( 0, count( $attempts ) - 1 );\n\t\t\t$attempt  = $attempts[ $id ];\n\n\t\t\t$this->assertTrue( $student->quizzes()->delete_attempt( $attempt->get( 'id' ) ) );\n\t\t\t$this->assertFalse( $attempt->exists() );\n\n\t\t\t$i++;\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_all()\n\t *\n\t * @since 4.21.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_all() {\n\n\t\t$student = $this->get_student_with_quizzes( 10 );\n\n\t\t$attempts = $student->quizzes()->get_all();\n\n\t\tforeach ( $attempts as $attempt ) {\n\n\t\t\t$this->assertTrue( $attempt instanceof LLMS_Quiz_Attempt );\n\t\t\t$this->assertEquals( $student->get( 'id' ), absint( $attempt->get( 'student_id' ) ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test get_attempt_by_id() and get_attempt_by_key()\n\t *\n\t * @since 4.21.2\n\t * @since 5.9.0 Don't use an invalid fake hash length.\n\t *\n\t * @return void\n\t */\n\tpublic function test_attempt_getters() {\n\n\t\t$student = llms_get_student( $this->factory->user->create() );\n\t\t$attempt = $this->get_attempt( $student );\n\n\t\twp_set_current_user( $student->get( 'id' ) );\n\n\t\t// Get by ID.\n\t\t$this->assertAttemptsAreEqual( $attempt, $student->quizzes()->get_attempt_by_id( $attempt->get( 'id' ) ) );\n\n\t\t// Get by Key.\n\t\t$this->assertAttemptsAreEqual( $attempt, $student->quizzes()->get_attempt_by_key( $attempt->get_key() ) );\n\n\t\t// ID Doesn't exit.\n\t\t$this->assertFalse( $student->quizzes()->get_attempt_by_id( absint( $attempt->get( 'id' ) ) + 1 ) );\n\n\t\t// Key doesn't exist.\n\t\t$this->assertFalse( $student->quizzes()->get_attempt_by_key( 'FAKE' ) );\n\n\t\t// ID exists but Wrong student.\n\t\twp_set_current_user( $this->factory->user->create() );\n\t\t$this->assertFalse( $student->quizzes()->get_attempt_by_id( $attempt->get( 'id' ) ) );\n\n\t\t// Admin can view.\n\t\twp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );\n\t\t$this->assertAttemptsAreEqual( $attempt, $student->quizzes()->get_attempt_by_id( $attempt->get( 'id' ) ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/user/class-llms-test-student.php",
    "content": "<?php\n/**\n * Tests for LifterLMS Student Functions\n * @group LLMS_Student\n * @since 3.5.0\n * @since 3.33.0 Add delete enrollment tests.\n * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion.\n * @since 5.2.0 Added tests on `get_registration_date`.\n */\nclass LLMS_Test_Student extends LLMS_UnitTestCase {\n\n\t/**\n\t * Test mark_complete() and mark_incomplete() on a tracks, courses, sections, and lessons\n\t *\n\t * @since 3.5.0\n\t * @since 3.17.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_completion_incompletion() {\n\n\t\t$courses = $this->generate_mock_courses( 3, 3, 3, 0 );\n\t\t$student = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\t$track = wp_insert_term( 'test track', 'course_track' );\n\n\t\t// nothing completed\n\t\tforeach ( $courses as $c_i => $cid ) {\n\n\t\t\twp_set_object_terms( $cid, array( $track['term_id'] ), 'course_track', false );\n\n\t\t\t$this->assertFalse( llms_is_complete( $student, $cid, 'course' ) );\n\n\t\t\t// check sections\n\t\t\t$course = llms_get_post( $cid );\n\t\t\tforeach ( $course->get_sections( 'ids' ) as $s_i => $sid ) {\n\n\t\t\t\t// no data recorded\n\t\t\t\t$this->assertFalse( llms_is_complete( $student, $sid, 'section' ) );\n\n\t\t\t\t// check lessons\n\t\t\t\t$section = llms_get_post( $sid );\n\t\t\t\tforeach ( $section->get_lessons( 'ids' ) as $l_i => $lid ) {\n\n\t\t\t\t\t// no data recorded (incomplete)\n\t\t\t\t\t$this->assertFalse( llms_is_complete( $student, $sid, 'lesson' ) );\n\n\t\t\t\t\t// marked completed\n\t\t\t\t\tllms_mark_complete( $student, $lid, 'lesson' );\n\t\t\t\t\t$this->assertTrue( llms_is_complete( $student, $lid, 'lesson' ) );\n\n\t\t\t\t\t// marked incomplete\n\t\t\t\t\tllms_mark_incomplete( $student, $lid, 'lesson' );\n\t\t\t\t\t$this->assertFalse( llms_is_complete( $student, $sid, 'lesson' ) );\n\n\t\t\t\t\t// complete it again to check parents\n\t\t\t\t\tllms_mark_complete( $student, $lid, 'lesson' );\n\t\t\t\t\t$this->assertTrue( llms_is_complete( $student, $lid, 'lesson' ) );\n\n\t\t\t\t\t// parent should still be incomplete\n\t\t\t\t\tif ( $l_i <= 1 ) {\n\t\t\t\t\t\t$this->assertFalse( llms_is_complete( $student, $sid, 'section' ) );\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\t// all lessons complete\n\t\t\t\t$this->assertTrue( llms_is_complete( $student, $sid, 'section' ) );\n\n\t\t\t\t// mark last lesson as incomplete\n\t\t\t\tllms_mark_incomplete( $student, $lid, 'lesson' );\n\t\t\t\t$this->assertFalse( llms_is_complete( $student, $sid, 'section' ) );\n\n\t\t\t\t// mark complete again for parent checks\n\t\t\t\tllms_mark_complete( $student, $lid, 'lesson' );\n\t\t\t\t$this->assertTrue( llms_is_complete( $student, $sid, 'section' ) );\n\n\t\t\t\t// parent should still be incomplete\n\t\t\t\tif ( $s_i <= 1 ) {\n\t\t\t\t\t$this->assertFalse( llms_is_complete( $student, $cid, 'course' ) );\n\t\t\t\t\t$this->assertFalse( llms_is_complete( $student, $track['term_id'], 'course_track' ) );\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t$this->assertTrue( llms_is_complete( $student, $cid, 'course' ) );\n\t\t\t$this->assertTrue( llms_is_complete( $student, $track['term_id'], 'course_track' ) );\n\n\t\t\t// mark last lesson as incomplete\n\t\t\tllms_mark_incomplete( $student, $lid, 'lesson' );\n\t\t\t$this->assertFalse( llms_is_complete( $student, $cid, 'course' ) );\n\t\t\t$this->assertFalse( llms_is_complete( $student, $track['term_id'], 'course_track' ) );\n\n\t\t\t// mark complete again for parents\n\t\t\tllms_mark_complete( $student, $lid, 'lesson' );\n\t\t\t$this->assertTrue( llms_is_complete( $student, $cid, 'course' ) );\n\t\t\t$this->assertTrue( llms_is_complete( $student, $track['term_id'], 'course_track' ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test whether a user is_enrolled() in a course or membership.\n\t *\n\t * @since 3.5.0\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return   void\n\t */\n\tpublic function test_enrollment() {\n\n\t\t// Create new user\n\t\t$user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );\n\n\t\t// Create new course\n\t\t$course_id = $this->factory->post->create( array( 'post_type' => 'course' ) );\n\n\t\t// Create new membership\n\t\t$memb_id = $this->factory->post->create( array( 'post_type' => 'llms_membership' ) );\n\n\t\t// Student shouldn't be enrolled in newly created course/membership\n\t\t$this->assertFalse( llms_is_user_enrolled( $user_id, $course_id ) );\n\t\t$this->assertFalse( llms_is_user_enrolled( $user_id, $memb_id ) );\n\n\t\t// Enroll Student in newly created course/membership\n\t\tllms_enroll_student( $user_id, $course_id, 'test_is_enrolled' );\n\t\tllms_enroll_student( $user_id, $memb_id, 'test_is_enrolled' );\n\n\t\t// Student should be enrolled in course/membership\n\t\t$this->assertTrue( llms_is_user_enrolled( $user_id, $course_id ) );\n\t\t$this->assertTrue( llms_is_user_enrolled( $user_id, $memb_id ) );\n\n\t\t// Wait 1 second before unenrolling Student\n\t\t// otherwise, enrollment and unenrollment postmeta will have identical timestamps\n\t\tsleep( 1 );\n\n\t\t// Unenroll Student in newly created course/membership\n\t\tllms_unenroll_student( $user_id, $course_id, 'cancelled', 'test_is_enrolled' );\n\t\tllms_unenroll_student( $user_id, $memb_id, 'cancelled', 'test_is_enrolled' );\n\n\t\t// Student should be not enrolled in newly created course/membership\n\t\t$this->assertFalse( llms_is_user_enrolled( $user_id, $course_id ) );\n\t\t$this->assertFalse( llms_is_user_enrolled( $user_id, $memb_id ) );\n\n\t\t// these were tests against now deprecated has_access\n\t\tsleep( 1 );\n\n\t\t$student = $this->get_mock_student();\n\n\t\t$course_id = $this->generate_mock_courses()[0];\n\n\t\t// no access\n\t\t$this->assertFalse( $student->is_enrolled( $course_id ) );\n\n\t\t// has access\n\t\tllms_enroll_student( $student->get_id(), $course_id );\n\t\t$this->assertTrue( $student->is_enrolled( $course_id ) );\n\n\t\t// check access after an access plan has expired access\n\t\t$gateway = llms()->payment_gateways()->get_gateway_by_id( 'manual' );\n\t\tupdate_option( $gateway->get_option_name( 'enabled' ), 'yes' );\n\n\t\t// new student\n\t\t$student = $this->get_mock_student();\n\n\t\t// create an access plan\n\t\t$plan = new LLMS_Access_Plan( 'new', 'Test Access Plan' );\n\t\t$plan_data = array(\n\t\t\t'access_expiration' => 'limited-period',\n\t\t\t'access_length' => '1',\n\t\t\t'access_period' => 'month',\n\t\t\t'frequency' => 25,\n\t\t\t'is_free' => 'no',\n\t\t\t'length' => 0,\n\t\t\t'on_sale' => 'no',\n\t\t\t'period' => 'day',\n\t\t\t'price' => 25.00,\n\t\t\t'product_id' => $course_id,\n\t\t\t'sku' => 'accessplansku',\n\t\t\t'trial_offer' => 'no',\n\t\t);\n\t\tforeach ( $plan_data as $key => $val ) {\n\t\t\t$plan->set( $key, $val );\n\t\t}\n\n\t\t$order = new LLMS_Order( 'new' );\n\t\t$order->init( $student, $plan, $gateway );\n\n\t\t$order->set( 'status', 'llms-completed' );\n\t\tupdate_option( $gateway->get_option_name( 'enabled' ), 'no' ); // prevent potential issues elsewhere\n\n\t\t// should be enrolled with no issues\n\t\t$this->assertTrue( $student->is_enrolled( $course_id ) );\n\n\t\t// fast forward\n\t\tllms_tests_mock_current_time( date( 'Y-m-d', current_time( 'timestamp' ) + YEAR_IN_SECONDS ) );\n\n\t\tsleep( 1 ); // so the expiration status is later than the enrollment\n\n\t\t// trigger expiration\n\t\tdo_action( 'llms_access_plan_expiration', $order->get( 'id' ) );\n\n\t\t$this->assertFalse( $student->is_enrolled( $course_id ) );\n\n\t\tsleep( 1 );\n\n\t\t// manually re-enroll the student, admin enrollment should take precedence here even though they no longer have access\n\t\tllms_enroll_student( $student->get_id(), $course_id );\n\t\t$this->assertTrue( $student->is_enrolled( $course_id ) );\n\n\t}\n\n\t/**\n\t * Test get_enrollment_date()\n\t *\n\t * @since 3.17.0\n\t * @since 3.33.0 Add test after enrollment deletion.\n\t * @since 6.0.0 Replaced use of the deprecated `llms_mock_current_time()` function\n\t *              with `llms_tests_mock_current_time()` from the `lifterlms-tests` project.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_enrollment_date() {\n\n\t\t$courses = $this->generate_mock_courses( 3, 0, 0, 0 );\n\t\t$student = $this->get_mock_student();\n\n\t\t$now = time();\n\t\t$format = 'Y-m-d H:i:s';\n\n\t\t// nothing completed\n\t\tforeach ( $courses as $cid ) {\n\n\t\t\t$ts = $now + ( DAY_IN_SECONDS * rand( 1, 50 ) );\n\t\t\t$date = date( $format, $ts );\n\n\t\t\tllms_tests_mock_current_time( $date );\n\n\t\t\t// enrollment date should match currently mocked date\n\t\t\t$student->enroll( $cid );\n\t\t\t$this->assertEquals( $date, $student->get_enrollment_date( $cid, 'enrolled', $format ) );\n\n\t\t\t$ts += HOUR_IN_SECONDS;\n\t\t\t$new_date = date( $format, $ts );\n\t\t\tllms_tests_mock_current_time( $new_date );\n\n\t\t\t// updated date should be an hour later\n\t\t\t$student->unenroll( $cid );\n\t\t\t$this->assertEquals( $new_date, $student->get_enrollment_date( $cid, 'updated', $format ) );\n\n\t\t\t// enrollment date should still be the original date\n\t\t\t$this->assertEquals( $date, $student->get_enrollment_date( $cid, 'enrolled', $format ) );\n\n\t\t\t// after enrollment deletion there should be no 'updated' or 'enrolled' date\n\t\t\t$student->delete_enrollment( $cid );\n\t\t\t$this->assertFalse( $student->get_enrollment_date( $cid, 'updated', $format ) );\n\t\t\t$this->assertFalse( $student->get_enrollment_date( $cid, 'enrolled', $format ) );\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test Student Getters and Setters\n\t * @return   void\n\t * @since    3.5.1\n\t * @version  3.5.1\n\t */\n\tpublic function test_getters_setters() {\n\n\t\t$uid = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\t$user = new WP_User( $uid );\n\t\t$student = new LLMS_Student( $uid );\n\n\t\t// test some core prefixed stuff from the usermeta table\n\t\t$student->set( 'first_name', 'Student' );\n\t\t$student->set( 'last_name', 'McStudentFace' );\n\t\t$this->assertEquals( get_user_meta( $uid, 'first_name', true ), $student->get( 'first_name' ) );\n\t\t$this->assertEquals( get_user_meta( $uid, 'last_name', true ), $student->get( 'last_name' ) );\n\n\t\t// stuff from the user table\n\t\t$this->assertEquals( $user->user_email, $student->get( 'user_email' ) );\n\n\t\t// llms custom user meta\n\t\t$student->set( 'billing_address', '123 Student Place' );\n\t\t$this->assertEquals( get_user_meta( $uid, 'llms_billing_address', true ), $student->get( 'billing_address' ) );\n\n\t}\n\n\t/**\n\t * Test get_name() function\n\t * @return   void\n\t * @since    3.5.1\n\t * @version  3.5.1\n\t */\n\tpublic function test_get_name() {\n\n\t\t$uid = $this->factory->user->create( array(\n\t\t\t'role' => 'student'\n\t\t) );\n\t\t$user = new WP_User( $uid );\n\t\t$student = new LLMS_Student( $uid );\n\n\t\t// no first/last name set, should return display name\n\t\t$this->assertEquals( $user->display_name, $student->get_name() );\n\n\t\t// set a first & last name\n\t\t$uid = $this->factory->user->create( array(\n\t\t\t'first_name' => 'Student',\n\t\t\t'last_name' => 'McStudentFace',\n\t\t\t'role' => 'student'\n\t\t) );\n\t\t$student = new LLMS_Student( $uid );\n\t\t$this->assertEquals( 'Student McStudentFace', $student->get_name() );\n\n\t}\n\n\t/**\n\t * Test get_enrollment_status()\n\t *\n\t * @since 3.17.0\n\t * @since 3.33.0 Add test after enrollment deletion.\n\t * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion.\n\t * @since 4.18.0 Removed the sleep delay between status changes to test statuses with the same date & time.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_enrollment_status() {\n\n\t\t$course_id = $this->generate_mock_courses( 1, 1, 1, 0 )[0];\n\t\t$course = llms_get_post( $course_id );\n\t\t$student = llms_get_student( $this->factory->user->create( array( 'role' => 'student' ) ) );\n\n\t\t// no status\n\t\t$this->assertFalse( $student->get_enrollment_status( $course_id ) );\n\n\t\t// enrolled\n\t\t$student->enroll( $course_id );\n\t\t$this->assertEquals( 'enrolled', $student->get_enrollment_status( $course_id ) );\n\t\t$this->assertEquals( 'enrolled', $student->get_enrollment_status( $course_id, false ) );\n\t\t// check from a lesson\n\t\t$this->assertEquals( 'enrolled', $student->get_enrollment_status( $course->get_lessons( 'ids' )[0] ) );\n\t\t$this->assertEquals( 'enrolled', $student->get_enrollment_status( $course->get_lessons( 'ids' )[0] ), false );\n\n\t\t// expired\n\t\t$student->unenroll( $course_id );\n\t\t$this->assertEquals( 'expired', $student->get_enrollment_status( $course_id ) );\n\t\t$this->assertEquals( 'expired', $student->get_enrollment_status( $course_id, false ) );\n\n\t\t// deleted\n\t\t$student->delete_enrollment( $course_id );\n\t\t$this->assertFalse( $student->get_enrollment_status( $course_id ) );\n\t\t$this->assertFalse( $student->get_enrollment_status( $course_id, false ) );\n\n\t\t// Test auto-enrollments deletion.\n\t\t// create a membership.\n\t\t$membership    = new LLMS_Membership( 'new', 'Membership Title' );\n\t\t$membership_id = $membership->get('id');\n\n\t\t// set the courses as membership auto-enrollments.\n\t\t$courses = $this->generate_mock_courses( 2, 0, 0, 0 );\n\t\t$membership->set( 'auto_enroll', $courses );\n\n\t\t$student->enroll( $membership_id );\n\t\t$student->delete_enrollment( $membership_id );\n\t\t$this->assertFalse( $student->get_enrollment_status( $membership_id ) );\n\t\t$this->assertFalse( $student->get_enrollment_status( $membership_id, false ) );\n\t\t$this->assertFalse( $student->get_enrollment_status( $courses[0] ) );\n\t\t$this->assertFalse( $student->get_enrollment_status( $courses[0], false ) );\n\t\t$this->assertFalse( $student->get_enrollment_status( $courses[1] ) );\n\t\t$this->assertFalse( $student->get_enrollment_status( $courses[1], false ) );\n\n\t}\n\n\t/**\n\t * Test get_grade() method\n\t * @return   void\n\t * @since    3.24.0\n\t * @version  3.24.0\n\t */\n\tpublic function test_get_grade() {\n\n\t\t$student = $this->get_mock_student();\n\t\t$course = llms_get_post( $this->generate_mock_courses( 1, 2, 5, 5, 10 )[0] );\n\n\t\t$student->enroll( $course->get( 'id' ) );\n\n\t\t// no grade yet\n\t\t$this->assertEquals( 'N/A', $student->get_grade( $course->get( 'id' ) ) );\n\n\t\t$possible_grades = array( 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 );\n\t\t$lesson_grades = array();\n\n\t\tforeach ( $course->get_lessons() as $i => $lesson ) {\n\n\t\t\t// calculate the ongoing grade as quizzes are completed\n\t\t\tif ( 0 !== $i ) {\n\t\t\t\t$this->assertEquals( round( array_sum( $lesson_grades ) / count( $lesson_grades ), 2 ), $student->get_grade( $course->get( 'id' ), false ) );\n\t\t\t}\n\n\t\t\t// no grade on the lesson yet\n\t\t\t$this->assertEquals( 'N/A', $student->get_grade( $lesson->get( 'id' ) ) );\n\n\t\t\t$quiz_id = $lesson->get( 'quiz' );\n\t\t\tif ( ! $quiz_id ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$grade = $possible_grades[ rand( 0, count( $possible_grades ) - 1 ) ];\n\t\t\t$this->take_quiz( $quiz_id, $student->get( 'id' ), $grade );\n\t\t\t$this->assertEquals( 'N/A', $student->get_grade( $lesson->get( 'id' ) ) ); // with cache\n\t\t\t$this->assertEquals( $grade, $student->get_grade( $lesson->get( 'id' ), false ) ); // no cache\n\t\t\t$this->assertEquals( $grade, $student->get_grade( $lesson->get( 'id' ) ) ); // with  cache\n\t\t\t$lesson_grades[] = $grade;\n\n\t\t}\n\n\t\t// checkout overall course grade once completed\n\t\t$this->assertEquals( round( array_sum( $lesson_grades ) / count( $lesson_grades ), 2 ), $student->get_grade( $course->get( 'id' ), false ) ); // no cache\n\t\t$this->assertEquals( round( array_sum( $lesson_grades ) / count( $lesson_grades ), 2 ), $student->get_grade( $course->get( 'id' ) ) ); // with cache\n\n\t}\n\n\t/**\n\t * Test get_progress()\n\t *\n\t * @since 3.15.0\n\t * @since 5.3.3 Use `assestEqualsWithDelta()`.\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_progress() {\n\n\t\t$student = $this->get_mock_student();\n\n\t\t$courses = $this->generate_mock_courses( 3, 2, 5, 0 );\n\n\t\t// create a track and add all 3 courses to it\n\t\t$track_id = wp_insert_term( 'Test Course Track', 'course_track' )['term_id'];\n\t\tforeach ( $courses as $cid ) {\n\t\t\twp_set_post_terms( $cid, array( $track_id ), 'course_track' );\n\t\t}\n\n\t\t// course for most of our tests\n\t\t$course_id = $courses[0];\n\t\t$course = llms_get_post( $course_id );\n\n\t\t// check progress through course\n\t\t$i = 0;\n\t\twhile ( $i <= 100 ) {\n\n\t\t\t$this->complete_courses_for_student( $student->get( 'id' ), array( $course_id ), $i );\n\t\t\t$this->assertEquals( $i, $student->get_progress( $course_id, 'course' ) );\n\n\t\t\t$i += 10;\n\n\t\t}\n\n\t\t// check track progress\n\t\t$this->assertEqualsWithDelta( 33.33, $student->get_progress( $track_id, 'course_track' ), 0.01 );\n\t\t$this->complete_courses_for_student( $student->get( 'id' ), array( $courses[1], $courses[2] ), 100 );\n\t\t$this->assertEqualsWithDelta( 100, $student->get_progress( $track_id, 'course_track' ), 0.01 );\n\n\t\t// test the progress through a section\n\t\t$student = $this->get_mock_student();\n\t\tforeach ( $course->get_sections( 'ids' ) as $i => $section_id ) {\n\n\t\t\t$this->assertEquals( 0, $student->get_progress( $section_id, 'section' ) );\n\n\t\t\tif ( 0 === $i ) {\n\t\t\t\t$this->complete_courses_for_student( $student->get( 'id' ), array( $course_id ), 50 );\n\t\t\t\t$this->assertEquals( 100, $student->get_progress( $section_id, 'section' ) );\n\t\t\t} else {\n\t\t\t\t$this->complete_courses_for_student( $student->get( 'id' ), array( $course_id ), 80 );\n\t\t\t\t$this->assertEquals( 60, $student->get_progress( $section_id, 'section' ) );\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test LLMS_Student::get_registration_date().\n\t *\n\t * @since 5.2.0\n\t */\n\tpublic function test_get_registration_date() {\n\n\t\t$tests = array(\n\t\t\t'UTC@20' => array(\n\t\t\t\t'timezone_string'            => 'UTC',\n\t\t\t\t'user_registered'            => '2021-01-10 20:00:00',\n\t\t\t\t'expected_registration_date' => '2021-01-10',\n\t\t\t),\n\t\t\t'UTC@04' => array(\n\t\t\t\t'timezone_string'            => 'UTC',\n\t\t\t\t'user_registered'            => '2021-01-10 04:00:00',\n\t\t\t\t'expected_registration_date' => '2021-01-10',\n\t\t\t),\n\t\t\t'-5@20' => array(\n\t\t\t\t'timezone_string'            => 'America/Chicago',\n\t\t\t\t'user_registered'            => '2021-06-10 20:00:00',\n\t\t\t\t'expected_registration_date' => '2021-06-10',\n\t\t\t),\n\t\t\t'-5@04' => array(\n\t\t\t\t'timezone_string'            => 'America/Chicago',\n\t\t\t\t'user_registered'            => '2021-06-10 04:00:00',\n\t\t\t\t'expected_registration_date' => '2021-06-09',\n\t\t\t),\n\t\t\t'-5@05' => array(\n\t\t\t\t'timezone_string'            => 'America/Chicago',\n\t\t\t\t'user_registered'            => '2021-06-10 05:00:00',\n\t\t\t\t'expected_registration_date' => '2021-06-10',\n\t\t\t),\n\t\t\t'-6@20' => array(\n\t\t\t\t'timezone_string'            => 'America/Chicago',\n\t\t\t\t'user_registered'            => '2021-01-10 20:00:00',\n\t\t\t\t'expected_registration_date' => '2021-01-10',\n\t\t\t),\n\t\t\t'-6@04' => array(\n\t\t\t\t'timezone_string'            => 'America/Chicago',\n\t\t\t\t'user_registered'            => '2021-01-10 04:00:00',\n\t\t\t\t'expected_registration_date' => '2021-01-09',\n\t\t\t),\n\t\t\t'-6@06' => array(\n\t\t\t\t'timezone_string'            => 'America/Chicago',\n\t\t\t\t'user_registered'            => '2021-01-10 06:00:00',\n\t\t\t\t'expected_registration_date' => '2021-01-10',\n\t\t\t),\n\t\t\t'+9@20' => array(\n\t\t\t\t'timezone_string'            => 'Asia/Tokyo',\n\t\t\t\t'user_registered'            => '2021-08-10 20:00:00',\n\t\t\t\t'expected_registration_date' => '2021-08-11',\n\t\t\t),\n\t\t\t'+9@04' => array(\n\t\t\t\t'timezone_string'            => 'Asia/Tokyo',\n\t\t\t\t'user_registered'            => '2021-08-10 04:00:00',\n\t\t\t\t'expected_registration_date' => '2021-08-10',\n\t\t\t),\n\t\t);\n\n\t\tforeach ( $tests as $test_name => $test ) {\n\t\t\t# Set the server's local time zone.\n\t\t\tupdate_option( 'timezone_string', $test['timezone_string'] );\n\n\t\t\t# Register a new student.\n\t\t\t$user_id = $this->factory->user->create( array(\n\t\t\t\t'role'            => 'student',\n\t\t\t\t'user_registered' => $test['user_registered'],\n\t\t\t) );\n\t\t\t$student = llms_get_student( $user_id );\n\n\t\t\t# Test.\n\t\t\t$actual_registration_date = $student->get_registration_date( 'Y-m-d' );\n\t\t\t$this->assertEquals( $test['expected_registration_date'], $actual_registration_date, $test_name );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test is_enrolled() method\n\t *\n\t * @since 3.25.0\n\t * @since 3.33.0 Add test after enrollment deletion.\n\t * @since 3.36.2 Added tests on membership enrollment with related courses enrollments deletion.\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_enrolled() {\n\n\t\t$courses = $this->generate_mock_courses( 3, 1, 1, 0 );\n\n\t\t$course = llms_get_post( $courses[0] );\n\t\t$student = llms_get_student( $this->factory->user->create( array( 'role' => 'student' ) ) );\n\n\t\t// no status.\n\t\t$this->assertFalse( $student->is_enrolled( $courses[0] ) );\n\t\t$this->assertFalse( $student->is_enrolled( array( $courses[0] ) ) );\n\n\t\t// enrolled.\n\t\t$student->enroll( $courses[0] );\n\t\t$this->assertTrue( $student->is_enrolled( $courses[0] ) );\n\t\t$this->assertTrue( $student->is_enrolled( array( $courses[0] ) ) );\n\t\t// check from a lesson.\n\t\t$this->assertTrue( $student->is_enrolled( $course->get_lessons( 'ids' )[0] ) );\n\t\t$this->assertTrue( $student->is_enrolled( array( $course->get_lessons( 'ids' )[0] ) ) );\n\n\t\t// Enrolled in only one of the specified.\n\t\t$this->assertFalse( $student->is_enrolled( $courses, 'all' ) );\n\t\t$this->assertTrue( $student->is_enrolled( $courses, 'any' ) );\n\n\t\t// Enrolled in 2 of the 3.\n\t\t$student->enroll( $courses[1] );\n\t\t$this->assertFalse( $student->is_enrolled( $courses, 'all' ) );\n\t\t$this->assertTrue( $student->is_enrolled( $courses, 'any' ) );\n\n\t\t// Enrolled in all courses.\n\t\t$student->enroll( $courses[2] );\n\t\t$this->assertTrue( $student->is_enrolled( $courses, 'all' ) );\n\t\t$this->assertTrue( $student->is_enrolled( $courses, 'any' ) );\n\n\t\t$this->assertTrue( $student->is_enrolled( array( $courses[0], $courses[2] ) ) );\n\t\t$this->assertTrue( $student->is_enrolled( $courses[1], 'any' ) );\n\n\t\tsleep( 1 );\n\n\t\t// expired.\n\t\t$student->unenroll( $courses[0] );\n\t\t$this->assertFalse( $student->is_enrolled( $courses[0] ) );\n\t\t$this->assertFalse( $student->is_enrolled( array( $courses[0] ) ) );\n\n\t\t$this->assertFalse( $student->is_enrolled( $courses ) ); // default\n\t\t$this->assertFalse( $student->is_enrolled( $courses, 'all' ) );\n\t\t$this->assertTrue( $student->is_enrolled( $courses, 'any' ) );\n\n\t\t$student->unenroll( $courses[2] );\n\t\t$this->assertFalse( $student->is_enrolled( $courses, 'all' ) );\n\t\t$this->assertTrue( $student->is_enrolled( $courses, 'any' ) );\n\t\t$this->assertTrue( $student->is_enrolled( $courses, 'any' ) );\n\n\t\t$student->unenroll( $courses[1] );\n\t\t$this->assertFalse( $student->is_enrolled( $courses, 'any' ) );\n\t\t$this->assertFalse( $student->is_enrolled( $courses, 'all' ) );\n\n\t\t// deleted.\n\t\t$student->enroll( $courses[1] );\n\t\t$student->delete_enrollment( $courses[1] );\n\t\t$this->assertFalse( $student->is_enrolled( $courses[1] ) );\n\n\t\t// Test auto-enrollments deletion.\n\t\t// create a membership.\n\t\t$membership    = new LLMS_Membership( 'new', 'Membership Title' );\n\t\t$membership_id = $membership->get('id');\n\n\t\t// set the courses as membership auto-enrollments.\n\t\t$courses = $this->generate_mock_courses( 3, 0, 0, 0 );\n\t\t$membership->set( 'auto_enroll', $courses );\n\n\t\t$student->enroll( $membership_id );\n\n\t\t$student->delete_enrollment( $membership_id );\n\t\t$this->assertFalse( $student->is_enrolled( $membership_id ) );\n\t\t$this->assertFalse( $student->is_enrolled( $courses[0] ) );\n\t\t$this->assertFalse( $student->is_enrolled( $courses[1] ) );\n\t\t$this->assertFalse( $student->is_enrolled( $courses[2] ) );\n\n\t}\n\n}\n"
  },
  {
    "path": "tests/phpunit/unit-tests/user/class-llms-test-user-permissions.php",
    "content": "<?php\n/**\n * Test User Permissions and capabilities\n *\n * @package LifterLMS_Tests/Tests\n *\n * @group user_permissions\n *\n * @since 3.34.0\n */\nclass LLMS_Test_User_Permissions extends LLMS_UnitTestCase {\n\n\t/**\n\t * Setup the test case\n\t *\n\t * @since 3.34.0\n\t * @since 5.3.3 Renamed from `setUp()` for compat with WP core changes.\n\t *\n\t * @return void\n\t */\n\tpublic function set_up() {\n\t\tparent::set_up();\n\t\t$this->obj = new LLMS_User_Permissions();\n\t}\n\n\t/**\n\t * Create mock users of different roles for testing permissions.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return int[]\n\t */\n\tprivate function create_mock_users() {\n\n\t\treturn array(\n\t\t\t'student'     => $this->factory->student->create(),\n\t\t\t'admin'       => $this->factory->user->create( array( 'role' => 'administrator' ) ),\n\t\t\t'admin2'      => $this->factory->user->create( array( 'role' => 'administrator' ) ),\n\t\t\t'editor'      => $this->factory->user->create( array( 'role' => 'editor' ) ),\n\t\t\t'subscriber'  => $this->factory->user->create( array( 'role' => 'subscriber' ) ),\n\t\t\t'lms_manager' => $this->factory->user->create( array( 'role' => 'lms_manager' ) ),\n\t\t\t'instructor'  => $this->factory->user->create( array( 'role' => 'instructor' ) ),\n\t\t\t'assistant'   => $this->factory->user->create( array( 'role' => 'instructors_assistant' ) ),\n\t\t);\n\n\t}\n\n\t/**\n\t * Test the get_editable_roles method.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_get_editable_roles() {\n\n\t\t$roles = LLMS_User_Permissions::get_editable_roles();\n\t\t$this->assertEquals( array( 'instructor', 'instructors_assistant', 'lms_manager', 'student' ), $roles['lms_manager'] );\n\t\t$this->assertEquals( array( 'instructors_assistant' ), $roles['instructor'] );\n\n\t}\n\n\t/**\n\t * Test the is_current_user_instructor() method.\n\t *\n\t * @since 3.34.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_is_current_user_instructor() {\n\n\t\t$users = $this->create_mock_users();\n\n\t\t// Obviously not golfers.\n\t\tforeach ( array( 'admin', 'student', 'editor', 'subscriber', 'lms_manager', 'assistant' ) as $role ) {\n\t\t\twp_set_current_user( $users[ $role ] );\n\t\t\t$this->assertFalse( LLMS_User_Permissions::is_current_user_instructor() );\n\t\t}\n\n\t\t// Winner.\n\t\twp_set_current_user( $users['instructor'] );\n\t\t$this->assertTrue( LLMS_User_Permissions::is_current_user_instructor() );\n\n\t\t// Logged out.\n\t\twp_set_current_user( null );\n\t\t$this->assertFalse( LLMS_User_Permissions::is_current_user_instructor() );\n\n\t}\n\n\t/**\n\t * Test the user_can_manage_user method.\n\t *\n\t * @since 3.34.0\n\t * @since 3.41.0 Add tests to ensure admins can still manage other admins.\n\t *\n\t * @return void\n\t */\n\tpublic function test_user_can_manage_user() {\n\n\t\textract( $this->create_mock_users() );\n\n\t\t// WP Core roles are skipped.\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin, $student ) ) );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin, $admin2 ) ) );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin, $editor ) ) );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $editor, $student ) ) );\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $subscriber, $student ) ) );\n\n\t\t// LMS Managers can't manage WP core roles.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $admin ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $editor ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $subscriber ) ) );\n\n\t\t// LMS Managers can manage all LMS Roles (including other LMS Managers).\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $student ) ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $instructor ) ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $assistant ) ) );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager, $this->factory->user->create( array( 'role' => 'lms_manager' ) ) ) ) );\n\n\t\t// Instructor's cannot manage WP core roles.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $admin ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $editor ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $subscriber ) ) );\n\n\t\t// Instructor's cannot manage LMS Managers or students\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $lms_manager ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $student ) ) );\n\n\t\t// Instructors can only manage assistants who they \"own\".\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $assistant ) ) );\n\n\t\t$ass_obj = llms_get_instructor( $assistant );\n\t\t$ass_obj->add_parent( $instructor );\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $instructor, $assistant ) ) );\n\n\t\t// Assistant's cannot manage anything.\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $admin ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $editor ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $subscriber ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $student ) ) );\n\t\t$this->assertFalse( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $assistant, $instructor ) ) );\n\n\t\t// All LMS Roles can manage themselves.\n\t\tforeach( array( $lms_manager, $instructor, $assistant ) as $uid ) {\n\t\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $uid, $uid ) ) );\n\t\t}\n\n\t}\n\n\t/**\n\t * Test the user_can_manage_user() for users with multiple roles.\n\t *\n\t * @since 3.41.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_user_can_manage_user_multiple_roles() {\n\n\t\textract( $this->create_mock_users() );\n\n\t\t$admin = new WP_User( $admin );\n\t\t$admin->add_role( 'student' );\n\n\t\t// Admin with student role.\n\t\t$this->assertNull( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $admin->ID, $student ) ) );\n\n\t\t$lms_manager = new WP_User( $lms_manager );\n\t\t$lms_manager->add_role( 'student' );\n\n\t\t$this->assertTrue( LLMS_Unit_Test_Util::call_method( $this->obj, 'user_can_manage_user', array( $lms_manager->ID, $student ) ) );\n\n\t}\n\n\t/**\n\t * Test the editable_roles() filter for users with single roles.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_editable_roles_single_role() {\n\n\t\t$users = $this->create_mock_users();\n\n\t\t$all_roles = wp_roles()->roles;\n\n\t\t$editable_roles = LLMS_Unit_Test_Util::call_method( $this->obj, 'get_editable_roles');\n\n\t\twp_set_current_user( $users['lms_manager'] );\n\t\t$lms_manager_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\t// Assert that lms_managers can edit mapped roles.\n\t\tforeach ( $editable_roles['lms_manager'] as $editable_role ) {\n\t\t\t$this->assertContains( $editable_role, $lms_manager_editable_roles );\n\t\t}\n\n\t\twp_set_current_user( $users['instructor'] );\n\t\t$instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\t// Assert that instructor can edit mapped roles.\n\t\tforeach ( $editable_roles['instructor'] as $editable_role ) {\n\t\t\t$this->assertContains( $editable_role, $instructor_editable_roles );\n\t\t}\n\n\t\twp_set_current_user( $users['assistant'] );\n\t\t$assistant_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\t// Assert that assistants can edit all roles.\n\t\tforeach ( array_keys( $all_roles ) as $role ) {\n\t\t\t$this->assertContains( $role, $assistant_editable_roles );\n\t\t}\n\n\t\twp_set_current_user( $users['admin'] );\n\t\t$administrator_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\t// Assert that administrator can edit all roles.\n\t\tforeach ( array_keys( $all_roles ) as $role ) {\n\t\t\t$this->assertContains( $role, $administrator_editable_roles );\n\t\t}\n\t}\n\n\t/**\n\t * Test the editable_roles() filter for users with multiple roles.\n\t *\n\t * @since 4.10.0\n\t *\n\t * @return void\n\t */\n\tpublic function test_editable_roles_multiple_roles() {\n\n\t\t$users = $this->create_mock_users();\n\n\t\t$all_roles = wp_roles()->roles;\n\n\t\twp_set_current_user( $users['lms_manager'] );\n\t\t$lms_manager_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\twp_set_current_user( $users['instructor'] );\n\t\t$instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\twp_set_current_user( $users['lms_manager'] );\n\t\t$user = wp_get_current_user();\n\t\t$user->add_role( 'instructor' );\n\t\t$lms_manager_instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\t// Assert that lms_manager with instructor role has editable roles from both roles.\n\t\tforeach ( $lms_manager_editable_roles as $lms_manager_editable_role ) {\n\t\t\t$this->assertContains( $lms_manager_editable_role, $lms_manager_instructor_editable_roles );\n\t\t}\n\t\tforeach ( $instructor_editable_roles as $instructor_editable_role ) {\n\t\t\t$this->assertContains( $instructor_editable_role, $lms_manager_instructor_editable_roles );\n\t\t}\n\n\t\twp_set_current_user( $users['admin'] );\n\t\t$user = wp_get_current_user();\n\t\t$user->add_role( 'instructor' );\n\t\t$administrator_instructor_editable_roles = array_keys ( LLMS_Unit_Test_Util::call_method( $this->obj, 'editable_roles', array( $all_roles ) ) );\n\n\t\t// Assert that administrator with instructor role can edit all roles.\n\t\tforeach ( array_keys( $all_roles ) as $role ) {\n\t\t\t$this->assertContains( $role, $administrator_instructor_editable_roles );\n\t\t}\n\t}\n\n\t/**\n\t * Test student CRUD capabilities\n\t *\n\t * @since Unknown\n\t *\n\t * @return void\n\t */\n\tpublic function test_student_crud_caps() {\n\n\t\t$users = $this->create_mock_users();\n\n\t\t// These users have all student permissions regardless of the user role.\n\t\tforeach ( array( 'admin', 'lms_manager' ) as $role ) {\n\n\t\t\twp_set_current_user( $users[ $role ] );\n\t\t\t$this->assertTrue( current_user_can( 'create_students' ) );\n\t\t\tforeach ( $users as $user ) {\n\t\t\t\t// General Capability.\n\t\t\t\t$this->assertTrue( current_user_can( 'view_students' ) );\n\t\t\t\t$this->assertTrue( current_user_can( 'edit_students' ) );\n\t\t\t\t$this->assertTrue( current_user_can( 'delete_students' ) );\n\t\t\t\t// Specific User.\n\t\t\t\t$this->assertTrue( current_user_can( 'view_students', $user ) );\n\t\t\t\t$this->assertTrue( current_user_can( 'edit_students', $user ) );\n\t\t\t\t$this->assertTrue( current_user_can( 'delete_students', $user ) );\n\t\t\t}\n\n\t\t}\n\n\t\t// These users can't do anything.\n\t\tforeach ( array( 'student', 'editor', 'subscriber' ) as $role ) {\n\n\t\t\twp_set_current_user( $users[ $role ] );\n\t\t\t$this->assertFalse( current_user_can( 'create_students' ) );\n\n\t\t\tforeach ( $users as $user ) {\n\t\t\t\t// General Capability.\n\t\t\t\t$this->assertFalse( current_user_can( 'view_students' ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'edit_students' ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'delete_students' ) );\n\t\t\t\t// Specific User.\n\t\t\t\t$this->assertFalse( current_user_can( 'view_students', $user ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'edit_students', $user ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'delete_students', $user ) );\n\t\t\t}\n\n\t\t}\n\n\t\t$course_1 = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\t\t$course_2 = $this->factory->course->create_and_get( array( 'sections' => 0 ) );\n\n\t\t// These users can view their own and that's it.\n\t\tforeach ( array( 'assistant', 'instructor' ) as $role ) {\n\n\t\t\twp_set_current_user( $users[ $role ] );\n\t\t\t$this->assertFalse( current_user_can( 'create_students' ) );\n\n\t\t\tforeach ( $users as $user ) {\n\t\t\t\t// General Capability.\n\t\t\t\t$this->assertTrue( current_user_can( 'view_students' ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'edit_students' ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'delete_students' ) );\n\t\t\t\t// Specific User.\n\t\t\t\t$this->assertFalse( current_user_can( 'view_students', $user ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'edit_students', $user ) );\n\t\t\t\t$this->assertFalse( current_user_can( 'delete_students', $user ) );\n\t\t\t}\n\n\t\t\t$course_1->instructors()->set_instructors( array( array( 'id' => $users[ $role ] ) ) );\n\t\t\t$course_2->instructors()->set_instructors( array( array( 'id' => $users[ $role ] ) ) );\n\n\t\t\tforeach ( $users as $user ) {\n\n\t\t\t\tllms_enroll_student( $user, $course_1->get( 'id' ) );\n\t\t\t\t$this->assertTrue( current_user_can( 'view_students', $user ) );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t/**\n\t * Test view_grades capability errors\n\t *\n\t * @since 4.21.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_view_grades_cap_errs() {\n\n\t\t// Logged out user.\n\t\t$this->assertFalse( current_user_can( 'view_grades' ) );\n\n\t\twp_set_current_user( $this->factory->user->create() );\n\n\t\t// Missing required args.\n\t\t$this->assertFalse( current_user_can( 'view_grades' ) );\n\n\t}\n\n\t/**\n\t * Test view_grades cap in various scenarios and different user types\n\t *\n\t * @since 4.21.2\n\t *\n\t * @return void\n\t */\n\tpublic function test_view_grades_cap() {\n\n\t\t$users   = $this->create_mock_users();\n\t\t$course  = $this->factory->course->create_and_get( array( 'sections' => 1, 'lessons' => 1 ) );\n\t\t$quiz    = $course->get_lessons()[0]->get_quiz();\n\t\t$quiz_id = $quiz->get( 'id' );\n\n\t\t$users['student2'] = $this->factory->user->create( array( 'role' => 'student' ) );\n\t\t$users['student3'] = $this->factory->user->create( array( 'role' => 'student' ) );\n\n\t\tllms_enroll_student( $users['student'], $course->get( 'id' ) );\n\t\tllms_enroll_student( $users['student2'], $course->get( 'id' ) );\n\n\t\t// Can view anyone's grades.\n\t\tforeach ( array( 'admin', 'lms_manager' ) as $current_role ) {\n\t\t\twp_set_current_user( $users[ $current_role ] );\n\t\t\tforeach ( $users as $uid ) {\n\t\t\t\t$this->assertTrue( current_user_can( 'view_grades', $uid, $quiz_id ) );\n\t\t\t\t$this->assertTrue( current_user_can( 'view_grades', $uid ) );\n\t\t\t}\n\t\t}\n\n\t\t// Can't view other people's grades.\n\t\tforeach ( array( 'editor', 'subscriber', 'instructor', 'assistant', 'student2' ) as $role ) {\n\t\t\twp_set_current_user( $users[ $role ] );\n\n\t\t\t// No for others.\n\t\t\t$this->assertFalse( current_user_can( 'view_grades', $users['student'], $quiz_id ), $role );\n\t\t\t$this->assertFalse( current_user_can( 'view_grades', $users['student'] ), $role );\n\n\t\t\t// Yes for their own.\n\t\t\t$this->assertTrue( current_user_can( 'view_grades', $users[ $role ], $quiz_id ), $role );\n\t\t\t$this->assertTrue( current_user_can( 'view_grades', $users[ $role ] ), $role );\n\n\t\t}\n\n\t\t// Instructors can view their own students.\n\t\t$assistant = llms_get_instructor( $users['assistant'] );\n\t\t$assistant->add_parent( $users['instructor'] );\n\n\t\t$course->instructors()->set_instructors( array(\n\t\t\tarray( 'id' => $users['instructor'] ),\n\t\t\tarray( 'id' => $users['assistant'] ),\n\t\t) );\n\n\t\t// Can view grades for their students.\n\t\tforeach ( array( 'instructor', 'assistant' ) as $role ) {\n\n\t\t\twp_set_current_user( $users[ $role ] );\n\n\t\t\t$this->assertTrue( current_user_can( 'view_grades', $users['student'], $quiz_id ), $role );\n\t\t\t$this->assertTrue( current_user_can( 'view_grades', $users['student2'], $quiz_id ), $role );\n\n\t\t\t$this->assertFalse( current_user_can( 'view_grades', $users['student3'], $quiz_id ), $role );\n\n\t\t\t$this->assertTrue( current_user_can( 'view_grades', $users['student'] ), $role );\n\n\t\t}\n\t}\n\n\n}\n"
  },
  {
    "path": "uninstall.php",
    "content": "<?php\n/**\n * LifterLMS Uninstall.\n *\n * @package LifterLMS/Main\n *\n * @since 1.0.0\n * @version 7.2.0\n */\n\n// If uninstall not called from WordPress exit.\nif ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {\n\texit();\n}\n\nwp_clear_scheduled_hook( 'llms_check_for_expired_memberships' );\nwp_clear_scheduled_hook( 'lifterlms_cleanup_tmp' );\nwp_clear_scheduled_hook( 'llms_send_tracking_data' );\nwp_clear_scheduled_hook( 'lifterlms_engagement_award_achievement' );\nwp_clear_scheduled_hook( 'lifterlms_engagement_award_certificate' );\nwp_clear_scheduled_hook( 'lifterlms_engagement_send_email' );\nwp_clear_scheduled_hook( 'llms_end_idle_sessions' );\n\n/**\n * Only actually delete LifterLMS and Related Data when constant is defined.\n * This will prevent data loss when a plugin is deactivated.\n */\nif ( defined( 'LLMS_REMOVE_ALL_DATA' ) && true === LLMS_REMOVE_ALL_DATA ) {\n\n\tinclude_once dirname( __FILE__ ) . '/includes/class.llms.roles.php';\n\tinclude_once dirname( __FILE__ ) . '/includes/class.llms.post-types.php';\n\n\tglobal $wpdb, $wp_version;\n\n\t// Delete posts.\n\twp_trash_post( get_option( 'lifterlms_shop_page_id' ) );\n\twp_trash_post( get_option( 'lifterlms_memberships_page_id' ) );\n\twp_trash_post( get_option( 'lifterlms_checkout_page_id' ) );\n\twp_trash_post( get_option( 'lifterlms_myaccount_page_id' ) );\n\n\t// Remove roles.\n\tLLMS_Roles::remove_roles();\n\n\t// Delete options.\n\t$wpdb->query( \"DELETE FROM {$wpdb->options} WHERE option_name LIKE 'lifterlms\\_%';\" );\n\t$wpdb->query( \"DELETE FROM {$wpdb->options} WHERE option_name LIKE 'llms\\_%';\" );\n\n\t// Delete custom usermeta.\n\t$wpdb->query( \"DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'llms\\_%';\" );\n\n\t// Drop tables.\n\t$wpdb->query( \"DROP TABLE IF EXISTS {$wpdb->prefix}lifterlms_user_postmeta\" );\n\t$wpdb->query( \"DROP TABLE IF EXISTS {$wpdb->prefix}lifterlms_product_to_voucher\" );\n\t$wpdb->query( \"DROP TABLE IF EXISTS {$wpdb->prefix}lifterlms_voucher_code_redemptions\" );\n\t$wpdb->query( \"DROP TABLE IF EXISTS {$wpdb->prefix}lifterlms_vouchers_codes\" );\n\t$wpdb->query( \"DROP TABLE IF EXISTS {$wpdb->prefix}lifterlms_notifications\" );\n\t$wpdb->query( \"DROP TABLE IF EXISTS {$wpdb->prefix}lifterlms_events\" );\n\t$wpdb->query( \"DROP TABLE IF EXISTS {$wpdb->prefix}lifterlms_events_open_sessions\" );\n\n\t// Delete all post types & related meta data.\n\t$wpdb->query( \"DELETE FROM {$wpdb->posts} WHERE post_type IN ( 'course', 'section', 'lesson', 'llms_quiz', 'llms_question', 'llms_membership', 'llms_engagement', 'llms_order', 'llms_transaction', 'llms_achievement', 'llms_my_achievement', 'llms_certificate', 'llms_my_certificate', 'llms_email', 'llms_coupon', 'llms_voucher', 'llms_review', 'llms_access_plan', 'llms_form' );\" );\n\t$wpdb->query( \"DELETE meta FROM {$wpdb->postmeta} meta LEFT JOIN {$wpdb->posts} posts ON posts.ID = meta.post_id WHERE posts.ID IS NULL;\" );\n\n\t// Delete terms if > WP 4.2 (term splitting was added in 4.2).\n\tif ( version_compare( $wp_version, '4.2', '>=' ) ) {\n\n\t\t// Delete term taxonomies.\n\t\tforeach ( array( 'course_cat', 'course_difficulty', 'course_tag', 'course_track', 'membership_cat', 'membership_tag', 'llms_product_visibility', 'llms_access_plan_visibility' ) as $tax_name ) {\n\t\t\t$wpdb->delete(\n\t\t\t\t$wpdb->term_taxonomy,\n\t\t\t\tarray(\n\t\t\t\t\t'taxonomy' => $tax_name,\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Delete orphan relationships.\n\t\t$wpdb->query( \"DELETE tr FROM {$wpdb->term_relationships} tr LEFT JOIN {$wpdb->posts} posts ON posts.ID = tr.object_id WHERE posts.ID IS NULL;\" );\n\n\t\t// Delete orphan terms.\n\t\t$wpdb->query( \"DELETE t FROM {$wpdb->terms} t LEFT JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE tt.term_id IS NULL;\" );\n\n\t\t// Delete orphan term meta.\n\t\tif ( ! empty( $wpdb->termmeta ) ) {\n\t\t\t$wpdb->query( \"DELETE tm FROM {$wpdb->termmeta} tm LEFT JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id WHERE tt.term_id IS NULL;\" );\n\t\t}\n\t}\n\n\t// Delete order notes comments.\n\t$wpdb->query( \"DELETE FROM {$wpdb->comments} WHERE comment_type IN ( 'llms_order_note' );\" );\n\t$wpdb->query( \"DELETE meta FROM {$wpdb->commentmeta} meta LEFT JOIN {$wpdb->comments} comments ON comments.comment_ID = meta.comment_id WHERE comments.comment_ID IS NULL;\" );\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "/**\n * Webpack config\n *\n * @package LifterLMS/Scripts/Dev\n *\n * @since 5.5.0\n * @version 6.10.0\n */\n\nconst { resolve } = require( 'path' ),\n\tblocksConfig = require( '@lifterlms/scripts/config/blocks-webpack.config' ),\n\t{ CleanWebpackPlugin } = require( 'clean-webpack-plugin' ),\n\tgenerate = require( '@lifterlms/scripts/config/webpack.config' ),\n\tconfig = generate( {\n\t\tjs: [\n\t\t\t'admin-addons',\n\t\t\t'admin-award-certificate',\n\t\t\t'admin-certificate-editor',\n\t\t\t'admin-media-protection-block-protect',\n\t\t\t'admin-elementor-editor',\n\t\t\t'quill-wordcount',\n\n\t\t\t// Module packages.\n\t\t\t'components',\n\t\t\t'icons',\n\t\t\t'spinner',\n\t\t\t'utils',\n\t\t],\n\t\tcss: [\n\t\t\t'admin-addons'\n\t\t],\n\t} );\n\n// Remove the default directory clearer, since we include source JS in the assets/js directory we need to not clear the dest directory (for now).\nconfig.plugins = config.plugins.filter( plugin => {\n\treturn 'CleanWebpackPlugin' !== plugin.constructor.name;\n} );\n\n// Modified clean.\nconfig.plugins.push( new CleanWebpackPlugin( {\n\n\tcleanOnceBeforeBuildPatterns: [\n\t\t// Source maps.\n\t\t`assets/js/*.js.map`,\n\t],\n\n} ) );\n\n// config.entry.fontawesome = resolve( './src/scss/fontawesome.scss' );\n\nmodule.exports = [\n\tblocksConfig,\n\tconfig\n];\n"
  }
]